diff --git a/CHANGELOG.md b/CHANGELOG.md index 167d884..b3b97d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## [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 @@ -10,6 +10,8 @@ Stricter UTF-8 tail handling, a guard against Python-style option names in `filt - **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 "": 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. diff --git a/README.md b/README.md index 26d17df..287fb8b 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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 diff --git a/landing/lib/env.js b/landing/lib/env.js index 2728268..b556f77 100644 --- a/landing/lib/env.js +++ b/landing/lib/env.js @@ -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/'; diff --git a/src/env.d.ts b/src/env.d.ts index a0b3528..e8605fb 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -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/"; diff --git a/src/env.js b/src/env.js index 2728268..b556f77 100644 --- a/src/env.js +++ b/src/env.js @@ -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/'; diff --git a/test/default_model.test.js b/test/default_model.test.js new file mode 100644 index 0000000..a5e23ca --- /dev/null +++ b/test/default_model.test.js @@ -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)); +}); diff --git a/test/fixtures/demo-models.json b/test/fixtures/demo-models.json index 46cb215..1d3d756 100644 --- a/test/fixtures/demo-models.json +++ b/test/fixtures/demo-models.json @@ -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": { @@ -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" + } } } } diff --git a/test/live.test.js b/test/live.test.js index 4182720..75e48f5 100644 --- a/test/live.test.js +++ b/test/live.test.js @@ -5,7 +5,7 @@ * GPTDIFF_LIVE=1 * GPTDIFF_LLM_API_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