diff --git a/CHANGELOG.md b/CHANGELOG.md index 87955d0f..6d85635a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,9 @@ product overview. ## Unreleased +- Update Claude Code to 2.1.306 and offer Sonnet 5.5 in the model picker, with its + native 1M context window and verified external-usage cache rates. Existing saved models remain valid. + - Find specialist channel agents with a live directory of purposes, examples and shared skills. Managers can describe/hide their channel's agent. Metadata refreshes at startup and nightly; recommendations respect the requester and reply audience and never transfer work automatically. diff --git a/FEATURES.md b/FEATURES.md index 1778bcfa..c8289cd5 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -1179,7 +1179,8 @@ A categorized catalog of what's shipped. Cross-linked to `TEST-PLAN.md` checks. change. Discovery is bounded and cached for six hours; a failed refresh retains the last good snapshot, or the bundled fallback on a cold start. - **Claude's model choices state exact versions:** the picker offers Opus 5.5, Fable 5.1, - Sonnet 5, and Haiku 4.5 under their full model IDs. It omits duplicate aliases and special + Sonnet 5.5, and Haiku 4.5 under their full model IDs. Sonnet 5.5 uses its native 1M + context window and verified cache-aware pricing for external transcript usage. It omits duplicate aliases and special modes from the normal choices. Previously saved Claude aliases and full IDs remain valid; the Slack picker shows the current saved value, and the Admin UI round-trips it. The curated list is reviewed when Anthropic releases a new model. Provider CLI/package upgrades are diff --git a/TEST-PLAN.md b/TEST-PLAN.md index a07c47ad..e187e6ea 100644 --- a/TEST-PLAN.md +++ b/TEST-PLAN.md @@ -5000,10 +5000,20 @@ exercise the `qwen-eu` adapter itself, in a scratch runtime root (no production is what exposes a new model, and `~/.codex/models_cache.json` carries `client_version` so a mixed-version probe cannot leave a stale catalog behind. - [x] Automated: Claude's picker and browser fallback show only the exact Opus 5.5, Fable 5.1, - Sonnet 5, and Haiku 4.5 IDs; a previously saved same-engine alias remains selected in + Sonnet 5.5, and Haiku 4.5 IDs; a previously saved same-engine alias remains selected in Slack, and the Admin UI consumes the registry's model/effort manifests and keeps a valid saved same-engine custom ID available (`test/model-options.test.js`, `test/model-wizard-buttons.test.js`). +- [x] Automated: Sonnet 5.5 retains a native 1M context window without runtime metadata; + external transcripts use $2 input, $10 output, $2.50/$4 cache writes and $0.10 cache reads + per million tokens, while saved Sonnet 5 remains selectable and priced independently + (`test/model-info.test.js`, `test/model-options.test.js`, `test/external-usage.test.js`). +- [ ] Live Claude/Codex: in private admin fixtures, open `/model` from a thread on each engine, + select Claude → Sonnet 5.5 → medium, and ask "Reply with exactly SONNET55_OK". + Pass: picker submits `claude-sonnet-5-5`, next Claude turn reports that provider model, + replies with the sentinel and shows a 1M window. Reopen a thread saved to Sonnet 5; + pass: it remains selected until explicitly changed. Verify the Admin UI model selector + offers Sonnet 5.5 and retains a preexisting Sonnet 5 value. - [x] Automated: the `/model` wizard's **This channel** scope applies to the thread it was clicked in. Same-harness: the thread's stale engine/model/effort overrides are cleared and the next turn resolves the channel's new model/effort. Cross-harness: the thread's Claude session gets diff --git a/containers/versions.json b/containers/versions.json index cfaa96b5..e35eacdc 100644 --- a/containers/versions.json +++ b/containers/versions.json @@ -3,7 +3,7 @@ "comment": "Pinned toolchain for the channel image. Keep `claude` and `codex` at the versions the gateway host runs, so a channel behaves identically on either runtime backend; bump deliberately, rebuild, and let the image-id fingerprint recreate the containers. `imageSpecVersion` is the image CONTRACT (paths, PATH, baked-in packages), not the pins: bump it when the image gains or moves something the daemon relies on, keep it equal to IMAGE_SPEC_VERSION in src/runtimes/container/image-paths.js (a test pins them together), and the daemon will tell an operator at boot when the built image is older.", "base": "node:22-bookworm-slim", "npm": { - "@anthropic-ai/claude-code": "2.1.300", + "@anthropic-ai/claude-code": "2.1.306", "@openai/codex": "0.162.1", "mcp-remote": "0.14.3", "vercel": "62.4.0", diff --git a/docs/COMPATIBILITY.md b/docs/COMPATIBILITY.md index 1e4025ef..db7e6307 100644 --- a/docs/COMPATIBILITY.md +++ b/docs/COMPATIBILITY.md @@ -28,7 +28,7 @@ definitions are in `TEST-PLAN.md` and `docs/RELEASE-ACCEPTANCE.md`. | --- | --- | --- | | Operating system | Linux (systemd) with rootless Podman; Ubuntu 24.04 tested | CI on Ubuntu | | Node.js | 22.13 minimum; 24 LTS. SQLite FTS5 (channel-memory search index) is present from the later 22.x builds and 24; on a build without it the daemon boots and memory search uses a plain scan | Full matrix CI | -| Claude Code | Pinned nightly target `2.1.300` (official installer) | Real CLI surface + stub transport | +| Claude Code | Pinned nightly target `2.1.306` (official installer) | Real CLI surface + stub transport | | Codex CLI | Pinned nightly target `0.162.1`. The container login relay relies on three behaviors verified on 0.156.1 (2026-09-26 spike, `TEST-PLAN.md` P4): a turn runs on an `auth.json` with an access token and an empty refresh token; a valid access token is not refreshed because of `last_refresh` age; `login status` and a turn accept a JWT whose signature segment is a placeholder. `codex exec` needs a closed stdin. Re-run the spike on a Codex bump | Real CLI surface + stub transport; the relay spike before promotion | | SQLite | Built-in `node:sqlite` | Migrations + backup/restore quick-check | | VS Code (editor attach) | Any client. Servers for `1.139.0` and `1.138.0` are shared in the channel image (image spec 1.5.0); another version downloads its own into the channel volume as before | Live image test (`npm run test:live-container`) + a real Remote-SSH / attach connection before promotion | diff --git a/public/app.js b/public/app.js index 33e0db9e..40698413 100644 --- a/public/app.js +++ b/public/app.js @@ -145,7 +145,7 @@ const MODEL_OPTIONS = { claude: [ ["claude-opus-5-5", "Opus 5.5"], ["claude-fable-5-1", "Fable 5.1"], - ["claude-sonnet-5", "Sonnet 5"], + ["claude-sonnet-5-5", "Sonnet 5.5"], ["claude-haiku-4-5", "Haiku 4.5"], ], codex: [ diff --git a/src/config/settings.js b/src/config/settings.js index bf77415a..be3f7b6e 100644 --- a/src/config/settings.js +++ b/src/config/settings.js @@ -675,7 +675,7 @@ export function getCodexModelRates() { // launch (../gateway/external-usage.js), where the only record is a transcript's token counts. // Verified against platform.claude.com/docs/en/about-claude/pricing on 2026-09-23. `cacheWrite5m` / // `cacheWrite1h` are the two cache-write TTLs and `cacheRead` the hit rate; the multipliers differ -// per family (Fable 5.1 reads at 0.025x, Opus 5.5 at 0.05x, everything else at 0.1x), which is why +// per family (Fable 5.1 reads at 0.025x, Opus/Sonnet 5.5 at 0.05x, others at 0.1x), which is why // the rate is stored outright instead of derived. The 1M context window carries no premium on // 4.6-and-later models, so there is no long-context multiplier here. export const DEFAULT_CLAUDE_RATES = { @@ -690,6 +690,8 @@ export const DEFAULT_CLAUDE_RATES = { "claude-opus-4-6": { input: 5, cacheWrite5m: 6.25, cacheWrite1h: 10, cacheRead: 0.5, output: 25 }, "claude-opus-4-5": { input: 5, cacheWrite5m: 6.25, cacheWrite1h: 10, cacheRead: 0.5, output: 25 }, "claude-opus-4-1": { input: 15, cacheWrite5m: 18.75, cacheWrite1h: 30, cacheRead: 1.5, output: 75 }, + // Sonnet 5.5: platform.claude.com/docs/en/about-claude/pricing, verified 2026-10-21. + "claude-sonnet-5-5": { input: 2, cacheWrite5m: 2.5, cacheWrite1h: 4, cacheRead: 0.1, output: 10 }, "claude-sonnet-5": { input: 2, cacheWrite5m: 2.5, cacheWrite1h: 4, cacheRead: 0.2, output: 10 }, "claude-sonnet-4-6": { input: 3, cacheWrite5m: 3.75, cacheWrite1h: 6, cacheRead: 0.3, output: 15 }, "claude-sonnet-4-5": { input: 3, cacheWrite5m: 3.75, cacheWrite1h: 6, cacheRead: 0.3, output: 15 }, diff --git a/src/engines/adapters.js b/src/engines/adapters.js index 1ecc0720..bcbe4ed0 100644 --- a/src/engines/adapters.js +++ b/src/engines/adapters.js @@ -64,7 +64,7 @@ const claude = validateEngineAdapter({ efforts: ["low", "medium", "high", "xhigh"], models: [ { label: "Opus 5.5", value: "claude-opus-5-5", description: "Current Opus model for most coding and knowledge work." }, { label: "Fable 5.1", value: "claude-fable-5-1", description: "Current Fable model for demanding long-running work." }, - { label: "Sonnet 5", value: "claude-sonnet-5", description: "Current Sonnet model for everyday work." }, + { label: "Sonnet 5.5", value: "claude-sonnet-5-5", description: "Current Sonnet model for everyday work." }, { label: "Haiku 4.5", value: "claude-haiku-4-5", description: "Current Haiku model for fast, low-cost work." }, ], mintsOwnSessionId: false, // Provider-failure kinds (stream.js claudeProviderError) the orchestrator may replay IN PLACE on diff --git a/src/gateway/model-info.js b/src/gateway/model-info.js index bc838312..af3ced3d 100644 --- a/src/gateway/model-info.js +++ b/src/gateway/model-info.js @@ -133,7 +133,7 @@ export function contextWindowFor(result = {}) { // for a 1M run and would otherwise understate the window fivefold. if (/\[1m\]/i.test(raw) || /\[1m\]/i.test(String(result.model || ""))) return 1_000_000; // These current models have 1M by default; their explicit IDs need no [1m] suffix. - if (/^claude-(?:opus-5-5|fable-5-1|sonnet-5)$/i.test(raw)) return 1_000_000; + if (/^claude-(?:opus-5-5|fable-5-1|sonnet-5(?:-5)?)$/i.test(raw)) return 1_000_000; const runtimeWindow = modelUsageContextWindow(result.raw, raw); if (runtimeWindow) return runtimeWindow; const requestWindow = Array.isArray(result.usageRequests) diff --git a/test/external-usage.test.js b/test/external-usage.test.js index ac7a4e91..5a0171ce 100644 --- a/test/external-usage.test.js +++ b/test/external-usage.test.js @@ -442,3 +442,12 @@ test("a pass reads all history by default and reports how it attributed what it assert.ok(Array.isArray(summary.errors)); assert.ok(summary.scopes.some((s) => s.scope === "host")); }); + +// Sonnet 5.5 halves the cache-read rate; old Sonnet transcripts keep their own price. +test("Sonnet 5.5 external usage prices both cache TTLs without repricing Sonnet 5", async () => { + const { getClaudeModelRates } = await import("../src/config/settings.js"); + const rates = getClaudeModelRates(); + const usage = { input_tokens: 1e6, cached_input_tokens: 1e6, cache_write_5m_tokens: 1e6, cache_write_1h_tokens: 1e6, output_tokens: 1e6 }; + assert.equal(priceClaudeTokens(usage, "claude-sonnet-5-5", rates), 18.6); + assert.equal(priceClaudeTokens(usage, "claude-sonnet-5", rates), 18.7); +}); diff --git a/test/model-info.test.js b/test/model-info.test.js index 22d83757..7240075e 100644 --- a/test/model-info.test.js +++ b/test/model-info.test.js @@ -71,7 +71,7 @@ test("a configured 1M model keeps its window even though the CLI reports the pla }); test("current versioned Claude IDs retain their default 1M window", () => { - for (const model of ["claude-opus-5-5", "claude-fable-5-1", "claude-sonnet-5"]) { + for (const model of ["claude-opus-5-5", "claude-fable-5-1", "claude-sonnet-5", "claude-sonnet-5-5"]) { assert.equal(contextWindowFor({ engine: "claude", model, raw: { model } }), 1_000_000); } assert.equal(contextWindowFor({ engine: "claude", model: "claude-haiku-4-5", raw: { model: "claude-haiku-4-5" } }), 200_000); diff --git a/test/model-options.test.js b/test/model-options.test.js index a5f3140b..6f55dd3d 100644 --- a/test/model-options.test.js +++ b/test/model-options.test.js @@ -11,8 +11,9 @@ test("Slack offers a short Claude list with exact versioned IDs", () => { const claudeValues = modelOptionsForEngine("claude").map(({ value }) => value); const codexValues = modelOptionsForEngine("codex").map(({ value }) => value); - assert.deepEqual(claudeValues, ["__default__", "claude-opus-5-5", "claude-fable-5-1", "claude-sonnet-5", "claude-haiku-4-5"]); + assert.deepEqual(claudeValues, ["__default__", "claude-opus-5-5", "claude-fable-5-1", "claude-sonnet-5-5", "claude-haiku-4-5"]); assert.ok(!codexValues.some((value) => /fable/i.test(value))); + assert.equal(modelOptionsForEngine("claude", "claude-sonnet-5").at(-1).value, "claude-sonnet-5"); assert.equal(modelOptionsForEngine("claude", "fable").at(-1).label, "Current: fable"); assert.equal(modelOptionsForEngine("claude", "claude-opus-5-5").length, claudeValues.length); }); @@ -28,6 +29,7 @@ test("admin UI consumes server model manifests and keeps exact versions as its o assert.match(source, /current && modelMatchesEngine\(current, eng\).*options\.push\(\[current, current\]\)/); assert.match(claudeList, /\["claude-opus-5-5", "Opus 5\.5"\]/); assert.match(claudeList, /\["claude-fable-5-1", "Fable 5\.1"\]/); + assert.match(claudeList, /\["claude-sonnet-5-5", "Sonnet 5\.5"\]/); assert.doesNotMatch(claudeList, /\["(?:best|fable|opusplan)",/); assert.doesNotMatch(codexList, /fable/i); });