Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@

## [0.3.1] — Unreleased

Stricter UTF-8 tail handling, a guard against Python-style option names in `filterRelevant`, and a typed `SmartApplyError` for smartapply refusals. No change to `generateDiff` or to what `smartapply` writes.
Stricter UTF-8 tail handling, a guard against Python-style option names in `filterRelevant`, a typed `SmartApplyError` for smartapply refusals, and a new default model to replace a retired one. No change to `generateDiff` or to what `smartapply` writes.

### Behavior changes

- **`filterRelevant` now throws on snake_case options.** Passing any patchling-py keyword spelling (`on_binary`, `on_oversize`, `max_bytes`, `max_tokens`, `max_output_tokens`, `api_key`, `base_url`, `call_llm`, `on_decision`, `reasoning_effort`, `retry_base_ms`, `skip_patterns`, `max_attempts`, `retry_backoff`, `sleep`) throws a `TypeError` before any model call, naming the camelCase option to use (`skip_patterns` → `ignore`, `max_attempts: n` → `retries: n - 1`, `retry_backoff: s` → `retryBaseMs: s * 1000`; `sleep` is not supported). In 0.3.0 these keys were silently ignored, so `{ on_binary: 'include' }` quietly fell back to the default `exclude`. Code that passed such keys and relied on the defaults will now fail loudly. The guard is not configurable (the `SNAKE_CASE_OPTIONS` map lives in `src/filter.js` and is not re-exported from the package root).
- **A truncated UTF-8 tail keeps its valid prefix at any file length.** In 0.3.0 an incomplete trailing sequence was tolerated only when the buffer was exactly 8192 bytes; any other length took the binary path. Now a file that ends in a legal partial character is text with the partial character dropped (`0x61 0xC3` and `0x61 0xE0 0xA0` are `"a"`). Tails that can never become a legal character stay binary (`0x61 0xE0 0x80`, `0x61 0xED 0xA0`, `0x61 0xF0 0x80`, `0x61 0xF4 0x90`; leads C0, C1, F5-FF). Some short files that 0.3.0 skipped as binary are now sent to the decision model.
- **`smartapply` refusals throw `SmartApplyError` instead of a plain `Error`.** When smartapply refuses a model rewrite (it changed lines the diff did not add or remove, and the edit could not be replayed from the `+`/`-` lines), the thrown object is now a `SmartApplyError`, exported from the package root and matching patchling-py. What changed: the error's class and constructor (`err.constructor === SmartApplyError`, `instanceof SmartApplyError` is true; it is still `instanceof Error`), `err.name` (`'SmartApplyError'`, was `'Error'`), and therefore how it prints (`String(err)` and the first line of `err.stack` read `SmartApplyError: smartapply: refusing to write …` instead of `Error: …`). Code that checks `err.name === 'Error'`, `err.constructor === Error`, or snapshots the printed error will see the difference. What did not change: the message text (`smartapply: refusing to write "<path>": the model changed lines the diff did not add or remove`, so matching on `err.message` keeps working), when a refusal happens, and provider/LLM errors, which still propagate as the original error object and are never wrapped.

- **Default model is now `deepseek/deepseek-v4.1-flash-1017`.** The 0.3.0 default, `xiaomi/mimo-v2.5-pro-ultraspeed`, has been retired by NanoGPT and returns HTTP 410, so `generateDiff` and `smartapply` failed whenever `GPTDIFF_MODEL` was unset and no `model` was passed. `DEFAULT_MODEL` (and its `.d.ts` literal type) is now `deepseek/deepseek-v4.1-flash-1017`, the October 17, 2026 snapshot of DeepSeek V4.1 Flash as listed on NanoGPT ($0.10 / $0.60 per 1M input / output tokens, 1M context). NanoGPT lists no non-Flash V4.1 model. patchling-py uses the same default. Calls that pass `model` or set `GPTDIFF_MODEL` are unchanged. The `filterRelevant` default (`xiaomi/mimo-v2.6-flash`) is unchanged. The verified catalog snapshot (`test/fixtures/demo-models.json`) adds the new id and was re-checked on 2026-10-21.

### Docs and tests

- README documents the snake_case guard and the UTF-8 tail rule; `filter.d.ts` JSDoc updated to match.
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ The browser demos explicitly use `xiaomi/mimo-v2.6-flash` for diff generation
and repair. The landing-page picker also pins `xiaomi/mimo-v2.6-pro` and
`z-ai/glm-5.3-flash`. These IDs, capabilities, and prices were checked against
[NanoGPT's public catalog](https://nano-gpt.com/api/v1/models?detailed=true)
on 2026-10-13; the offline snapshot is in `test/fixtures/demo-models.json`.
on 2026-10-13 (re-checked 2026-10-21, when the default `deepseek/deepseek-v4.1-flash-1017` was added); the offline snapshot is in `test/fixtures/demo-models.json`.
The decision demo requests MiMo's supported `none` reasoning effort.
Library environment defaults below are independent of the demo choices.

Expand All @@ -71,7 +71,7 @@ Configuration comes from environment variables (Node) or `setEnv(...)` overrides
| --- | --- | --- |
| `GPTDIFF_LLM_API_KEY` | NanoGPT API key (`sk-nano-…`) | — (required for live calls) |
| `GPTDIFF_LLM_BASE_URL` | OpenAI-compatible base URL | `https://nano-gpt.com/api/v1/` |
| `GPTDIFF_MODEL` | Model id | `xiaomi/mimo-v2.5-pro-ultraspeed` |
| `GPTDIFF_MODEL` | Model id | `deepseek/deepseek-v4.1-flash-1017` |
| `GPTDIFF_FILTER_MODEL` | Cheap model for `filterRelevant` | `xiaomi/mimo-v2.6-flash` |

```js
Expand Down
2 changes: 1 addition & 1 deletion landing/lib/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,5 @@ export function getEnv(name, fallback = undefined) {
return fallback;
}

export const DEFAULT_MODEL = 'xiaomi/mimo-v2.5-pro-ultraspeed';
export const DEFAULT_MODEL = 'deepseek/deepseek-v4.1-flash-1017';
export const DEFAULT_BASE_URL = 'https://nano-gpt.com/api/v1/';
2 changes: 1 addition & 1 deletion src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ export function setEnv(name: string, value: string | undefined): void;
* @returns {string | undefined}
*/
export function getEnv(name: string, fallback?: string): string | undefined;
export const DEFAULT_MODEL: "xiaomi/mimo-v2.5-pro-ultraspeed";
export const DEFAULT_MODEL: "deepseek/deepseek-v4.1-flash-1017";
export const DEFAULT_BASE_URL: "https://nano-gpt.com/api/v1/";
2 changes: 1 addition & 1 deletion src/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,5 @@ export function getEnv(name, fallback = undefined) {
return fallback;
}

export const DEFAULT_MODEL = 'xiaomi/mimo-v2.5-pro-ultraspeed';
export const DEFAULT_MODEL = 'deepseek/deepseek-v4.1-flash-1017';
export const DEFAULT_BASE_URL = 'https://nano-gpt.com/api/v1/';
30 changes: 30 additions & 0 deletions test/default_model.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { DEFAULT_MODEL } from '../src/index.js';
import { DEFAULT_MODEL as LANDING_DEFAULT_MODEL } from '../landing/lib/env.js';

const RETIRED = 'xiaomi/mimo-v2.5-pro-ultraspeed'; // NanoGPT returns HTTP 410
const catalog = JSON.parse(readFileSync(new URL('./fixtures/demo-models.json', import.meta.url), 'utf8'));

test('DEFAULT_MODEL is deepseek/deepseek-v4.1-flash-1017', () => {
assert.equal(DEFAULT_MODEL, 'deepseek/deepseek-v4.1-flash-1017');
assert.equal(LANDING_DEFAULT_MODEL, DEFAULT_MODEL);
});

test('retired xiaomi/mimo-v2.5-pro-ultraspeed is not the default', () => {
assert.notEqual(DEFAULT_MODEL, RETIRED);
assert.notEqual(LANDING_DEFAULT_MODEL, RETIRED);
});

test('DEFAULT_MODEL is in the verified NanoGPT catalog fixture and cheap', () => {
const m = catalog.text[DEFAULT_MODEL];
assert.ok(m, `${DEFAULT_MODEL} missing from test/fixtures/demo-models.json`);
assert.ok(m.pricing.prompt <= 0.5 && m.pricing.completion <= 1, 'default should stay cheap');
});

test('src/env.d.ts declares the new default literal', () => {
const dts = readFileSync(new URL('../src/env.d.ts', import.meta.url), 'utf8');
assert.match(dts, /DEFAULT_MODEL: "deepseek\/deepseek-v4\.1-flash-1017";/);
assert.ok(!dts.includes(RETIRED));
});
43 changes: 41 additions & 2 deletions test/fixtures/demo-models.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"verifiedOn": "2026-10-13",
"verifiedOn": "2026-10-21",
"sources": {
"text": "https://nano-gpt.com/api/v1/models?detailed=true"
},
"catalogSha256": {
"nanogpt-models.json": "0ff41c8e54be2645c386bd00a712dca92e8b9103468ea28a0fd0a7e12c2aa89e"
"nanogpt-models.json": "e4a5241a0498b8d0db13d8bb667aaafa1d3d2b162a49d917de2412f66b634f7a"
},
"text": {
"z-ai/glm-5.3-flash": {
Expand Down Expand Up @@ -125,6 +125,45 @@
"currency": "USD",
"unit": "per_million_tokens"
}
},
"deepseek/deepseek-v4.1-flash-1017": {
"created": 1792195200,
"context_length": 1048576,
"max_output_tokens": 393216,
"architecture": {
"modality": "text+image->text",
"input_modalities": [
"text",
"image"
],
"output_modalities": [
"text"
]
},
"capabilities": {
"vision": true,
"video_input": false,
"audio_input": false,
"reasoning": true,
"tool_calling": true,
"parallel_tool_calls": false,
"structured_output": false,
"pdf_upload": false,
"batch_api": true
},
"reasoning_efforts": [
"none",
"low",
"high",
"max"
],
"pricing": {
"prompt": 0.09999999999999999,
"completion": 0.6,
"cacheReadInputPer1kTokens": 3e-06,
"currency": "USD",
"unit": "per_million_tokens"
}
}
}
}
2 changes: 1 addition & 1 deletion test/live.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* GPTDIFF_LIVE=1
* GPTDIFF_LLM_API_KEY=<your NanoGPT key> (e.g. sk-nano-... from OAuth sign-in)
*
* Model defaults to xiaomi/mimo-v2.5-pro-ultraspeed (override with GPTDIFF_MODEL).
* Model defaults to deepseek/deepseek-v4.1-flash-1017 (override with GPTDIFF_MODEL).
* Base URL defaults to https://nano-gpt.com/api/v1/ (override GPTDIFF_LLM_BASE_URL).
*
* Run with: npm run test:live
Expand Down
Loading