diff --git a/bun.lock b/bun.lock index 7cf8cd3..90ea4e3 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "": { "name": "@highflame/codeoid", "dependencies": { - "@anthropic-ai/claude-agent-sdk": "^0.3.220", + "@anthropic-ai/claude-agent-sdk": "^0.3.258", "@google/generative-ai": "^0.24.1", "@grammyjs/auto-retry": "^2.0.2", "@highflame/codeoid-core": "^0.4.0", @@ -54,23 +54,23 @@ "packages": { "@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.3.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-p+CMKJ93HFmLkjXKlXiVGlMQEuRb6H0MokBSwUsX+S6BRX8eV5naFZpQJFfJHjRZY0Hmnqy1/r6UWl3x+19zYA=="], - "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.3.220", "", { "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.220", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.220", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.220", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.220", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.220", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.220", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.220", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.220" }, "peerDependencies": { "@anthropic-ai/sdk": ">=0.93.0", "@modelcontextprotocol/sdk": "^1.29.0", "zod": "^4.0.0" } }, "sha512-glc7SdwPkOkLw8oxwLo9PKTdLJGqW/PIR4urWXFoRtX9YllwozsEVc5Tc1+EvLSkfrsxPJqQWqOgpjUOQXf1oA=="], + "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.3.258", "", { "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.258", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.258", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.258", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.258", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.258", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.258", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.258", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.258" }, "peerDependencies": { "@anthropic-ai/sdk": ">=0.93.0", "@modelcontextprotocol/sdk": "^1.29.0", "zod": "^4.0.0" } }, "sha512-RxJ5fSPCGCxX5qO/b4IPXhldvtLHeYBAzTUJ4eOzO+gTrepZQSDmwSlQD6nnoEquKGJzOMHCjhdEtBfDjbDWUg=="], - "@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.220", "", { "os": "darwin", "cpu": "arm64" }, "sha512-7VxlbEosK7DODiOnsjoVd0DSJzbnaPrM2jelMHI0y8zx1UnLS3WC6EFUXbvy74F2sXqEznh2tzn7EKWInaRN6Q=="], + "@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.258", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Hrhzc9WVGSid+DghdTcpVr/8fyXnTD6KeSlDpKx6Wru47J/Nq7RTYiZJt+cex+O2ehaHMEcuYEgoqJ3K/X9NlA=="], - "@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.220", "", { "os": "darwin", "cpu": "x64" }, "sha512-X9RwDsSmbF6ultKZroaip+DL8WRgC64gHbrAwrRlAFSPNZV7zmJyP2ur8rW7KrxqmtuehdMMkw8+SAC/6hD2PA=="], + "@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.258", "", { "os": "darwin", "cpu": "x64" }, "sha512-AVqxGX4988J5cS+TMqIzH85+sbsLhJu5Ou9TIALcO/v2Z9ze8GK4vX2ydAYvU/SRnjTvEaiITX+Xcm5afP1IbQ=="], - "@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.220", "", { "os": "linux", "cpu": "arm64" }, "sha512-WkROPwWskqhKR9XgnmseHQ6rLi9zM9qt57IWoToIjL/eXOqDWipp7JXZ1L5ud+LrA42dunHPZfBwD/vXZ+A7LA=="], + "@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.258", "", { "os": "linux", "cpu": "arm64" }, "sha512-Jj3K1Ip7WpyMouZCjd7kgV3KswUBF62WAnyG0iaYvKZJvXgYKbIAkjcQ2F2Rx5ZuRUNWAVncE9LeHmdIdx78VQ=="], - "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.220", "", { "os": "linux", "cpu": "arm64" }, "sha512-OHoZOZ8Cf2TBr6oXIXPwyvUxj9jrq2w8E4poA8dMpacXszcPSPiCQCMuuOh4aWJzfeJE1+TtWxhKMVb2csXyZQ=="], + "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.258", "", { "os": "linux", "cpu": "arm64" }, "sha512-I/BLt2vdvqK2B2px526U1lw7Rv+SI+Ld22+wLwu8gLRQk5SYhSW9dmMYEO+GCeF7vQzfzJvMQ4IzbbY+aSGACg=="], - "@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.3.220", "", { "os": "linux", "cpu": "x64" }, "sha512-tkTJFnpR9VifvWX2fmkCAPkT6+8Wk/gVu8B5jsVekKZPiZoWRHmMXO30BnZn+f0TZhgYP+82PSX3S8crH1kn+w=="], + "@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.3.258", "", { "os": "linux", "cpu": "x64" }, "sha512-2MJeFVJM/3xwZASP3yn2OuQ9RHIoS30DC/B7oG1XPYcbToPLH4QIfCPLWbSQfqCdp+NEBupLMM9BWpDz4s8Q0g=="], - "@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.220", "", { "os": "linux", "cpu": "x64" }, "sha512-K+FWj+LcGhC1Z7wqeWoLxm1iemcba5xKpLLFVwYm4V6HyMx3ruYd/2r2TiQtjT+JWeNFWIys0ScHiItR6vWAiA=="], + "@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.258", "", { "os": "linux", "cpu": "x64" }, "sha512-sM7GzRyrOpFhwMn2Ng8nLiWK6cc04uCEu3Zh9mrJS2r3iQu1TryHKoPTjc2Ip0N75sHmwhNodgoRs8NtG1Gkkw=="], - "@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.220", "", { "os": "win32", "cpu": "arm64" }, "sha512-rIwgq0UwQExWl6KrHUyC4w5KwpL9l6nd95aUTx6RitexaAuEw//xtfTVLnuE4hDDQZFkzEwpdKc3nxDWoGcUbA=="], + "@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.258", "", { "os": "win32", "cpu": "arm64" }, "sha512-n/Vf6oXAo9EZVSSM5+9d+8dFrUrX9cbgSHK/1njkvykWAN5xsfBbikJYqwhbW84GCYkpXYM+gGNZe23h0fHldw=="], - "@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.3.220", "", { "os": "win32", "cpu": "x64" }, "sha512-MuOuXhbr66HlGaWXD2f3w0k2PsvmnbkwcUZ0dAe2poFLdl72GC2dapwwOBefxm9QmoNqk9+jmv/dSKGOVWyvLw=="], + "@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.3.258", "", { "os": "win32", "cpu": "x64" }, "sha512-UDbXE6n37ZMUogVVYEWX901NNmbyXUv1VvGYN/vfOiIWKUJXCAypam71dBlYFhlAhVX28qCd64w+pTZDYQfYQA=="], "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.104.1", "", { "dependencies": { "json-schema-to-ts": "^3.1.1", "standardwebhooks": "^1.0.0" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-gGACa/+IaiXzRRmF96aOhamoBgapKRBiFWbmmTFP8aMkpaEcuStF+Q61bjo4vPxBM7gqWJNZqsngslRdnLHv0Q=="], diff --git a/package.json b/package.json index 3a6f73b..2e7c906 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "prepublishOnly": "bun run build:web" }, "dependencies": { - "@anthropic-ai/claude-agent-sdk": "^0.3.220", + "@anthropic-ai/claude-agent-sdk": "^0.3.258", "@google/generative-ai": "^0.24.1", "@grammyjs/auto-retry": "^2.0.2", "@highflame/codeoid-core": "^0.4.0", diff --git a/src/daemon/models.ts b/src/daemon/models.ts index 3aba5fe..4796514 100644 --- a/src/daemon/models.ts +++ b/src/daemon/models.ts @@ -44,9 +44,9 @@ export interface ModelDescriptor { */ export const MODEL_CATALOG: readonly ModelDescriptor[] = [ { - id: "claude-opus-4-8", + id: "claude-opus-5", alias: "opus", - label: "Opus 4.8", + label: "Opus 5", contextWindow: 1_000_000, tier: "premium", description: "Deepest reasoning. Best for planning, refactoring, and hard problems.", @@ -169,12 +169,33 @@ export function fallbackModelInfos(): ModelInfo[] { })); } +/** + * Strip a context-window variant suffix: `opus[1m]` → `opus`. + * + * The backend advertises variants as a bracketed suffix on the VALUE, not the + * display name — the live list reports `opus[1m]` / "Opus (1M context)". A + * user (or a stored session) typing the bare alias `opus` must still match it, + * or resolution silently falls through to the baked-in catalog below and + * pins them to whatever point release that catalog last knew about. + * + * Exported for unit testing. + */ +export function stripVariantSuffix(value: string): string { + const i = value.indexOf("["); + return i === -1 ? value : value.slice(0, i); +} + /** * Resolve user input → a canonical model value against a (live or fallback) - * `ModelInfo[]`. Matches by exact `value` or case-insensitive `displayName` - * (so `opus` matches the backend's "Opus"), with a `claude-*` passthrough. - * Returns null when the input matches nothing — the caller surfaces the set - * of valid values. + * `ModelInfo[]`. Matches by exact `value`, by `value` with its variant suffix + * stripped (so `opus` matches the backend's `opus[1m]`), or by + * case-insensitive `displayName` (so `fable` matches "Fable"), with a + * `claude-*` passthrough. Returns null when the input matches nothing — the + * caller surfaces the set of valid values. + * + * Exact matches are checked across the WHOLE list before any suffix-stripped + * match, so a backend offering both `opus` and `opus[1m]` resolves `opus` to + * the exact entry rather than to whichever came first. */ export function resolveAgainstList( input: string, @@ -185,6 +206,9 @@ export function resolveAgainstList( const lower = t.toLowerCase(); for (const m of models) { if (m.value.toLowerCase() === lower) return m.value; + } + for (const m of models) { + if (stripVariantSuffix(m.value).toLowerCase() === lower) return m.value; if (m.displayName.toLowerCase() === lower) return m.value; } if (/^claude-/i.test(t)) return t; diff --git a/src/tests/models.test.ts b/src/tests/models.test.ts index 708f870..1e26c05 100644 --- a/src/tests/models.test.ts +++ b/src/tests/models.test.ts @@ -17,27 +17,51 @@ import { DEFAULT_MODEL_ALIAS, fallbackModelInfos, resolveAgainstList, + stripVariantSuffix, } from "../daemon/models.js"; import { Store } from "../daemon/store.js"; import { SessionManager, DEFAULT_PROVIDER_ID } from "../daemon/session-manager.js"; import { TranscriptStore } from "../daemon/transcript.js"; describe("resolveAgainstList (live-backend resolution)", () => { + // Verbatim from `supportedModels()` on claude-agent-sdk 0.3.258. The + // previous fixture said the Opus entry's displayName was "Opus", which made + // the alias resolve by display-name match; the backend actually reports + // "Opus (1M context)", so `opus` matched nothing and silently fell through + // to the baked-in catalog. Keep this fixture honest to the real payload. const live = [ { value: "default", displayName: "Default (recommended)", isDefault: true }, - { value: "opus[1m]", displayName: "Opus" }, + { value: "opus[1m]", displayName: "Opus (1M context)" }, + { value: "claude-fable-5-1[1m]", displayName: "Fable" }, { value: "sonnet", displayName: "Sonnet" }, + { value: "haiku", displayName: "Haiku" }, ]; it("matches an exact value", () => { expect(resolveAgainstList("opus[1m]", live)).toBe("opus[1m]"); + expect(resolveAgainstList("sonnet", live)).toBe("sonnet"); }); - it("matches a display name case-insensitively (alias-like)", () => { + it("matches a bare alias against a variant-suffixed value", () => { expect(resolveAgainstList("opus", live)).toBe("opus[1m]"); expect(resolveAgainstList("OPUS", live)).toBe("opus[1m]"); }); - it("passes through a full claude-* id", () => { - expect(resolveAgainstList("claude-fable-5[1m]", live)).toBe("claude-fable-5[1m]"); + it("matches a display name case-insensitively", () => { + expect(resolveAgainstList("fable", live)).toBe("claude-fable-5-1[1m]"); + expect(resolveAgainstList("Default (recommended)", live)).toBe("default"); + }); + it("prefers an exact value over a suffix-stripped match", () => { + const both = [{ value: "opus[1m]", displayName: "Opus (1M context)" }, { value: "opus", displayName: "Opus" }]; + expect(resolveAgainstList("opus", both)).toBe("opus"); + }); + it("matches a bare full id against its variant-suffixed entry", () => { + // Typing the plain id resolves to the 1M variant the backend actually + // offers, rather than falling through to the claude-* passthrough. + expect(resolveAgainstList("claude-fable-5-1", live)).toBe("claude-fable-5-1[1m]"); + }); + it("passes through a claude-* id the backend didn't advertise", () => { + // The catalog and the live list both go stale between releases; the + // passthrough is what keeps a brand-new id reachable in the meantime. + expect(resolveAgainstList("claude-opus-6", live)).toBe("claude-opus-6"); }); it("returns null for an unknown value", () => { expect(resolveAgainstList("o", live)).toBeNull(); @@ -45,6 +69,17 @@ describe("resolveAgainstList (live-backend resolution)", () => { }); }); +describe("stripVariantSuffix", () => { + it("strips a bracketed context-window variant", () => { + expect(stripVariantSuffix("opus[1m]")).toBe("opus"); + expect(stripVariantSuffix("claude-fable-5[1m]")).toBe("claude-fable-5"); + }); + it("leaves an unsuffixed value alone", () => { + expect(stripVariantSuffix("sonnet")).toBe("sonnet"); + expect(stripVariantSuffix("")).toBe(""); + }); +}); + describe("fallbackModelInfos", () => { it("renders the built-in catalog as ModelInfo with a default", () => { const infos = fallbackModelInfos(); @@ -55,6 +90,15 @@ describe("fallbackModelInfos", () => { }); describe("MODEL_CATALOG shape", () => { + // The catalog is only the pre-first-report fallback, but a stale entry here + // is not harmless: it silently pins the alias to a superseded model on every + // path that misses the live list. `opus` sat on claude-opus-4-8 well after + // Opus 5 shipped. This asserts the generation, not the point release, so a + // real bump stays a one-line edit while a whole generation going stale fails. + it("maps the premium alias to the current Opus generation", () => { + expect(resolveModelId("opus")).toBe("claude-opus-5"); + }); + it("covers the three canonical tiers", () => { expect(MODEL_CATALOG.length).toBeGreaterThanOrEqual(3); const tiers = new Set(MODEL_CATALOG.map((m) => m.tier));