From d7408d57a2b9f00e534eb4e2cf6edc36dce9f8e9 Mon Sep 17 00:00:00 2001 From: Yash Datta Date: Fri, 3 Jul 2026 01:27:39 +0800 Subject: [PATCH] fix: refresh stale fallback model catalog to current Claude models MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fallback MODEL_CATALOG (served by models.list until any session runs a turn and the live SDK supportedModels() list is cached) still pointed opus/sonnet at Opus 4.7 / Sonnet 4.6 — so the web UI's model picker showed old models after every daemon restart, since resumed sessions spawn their provider lazily and the live list only arrives once a turn runs. The live-list plumbing itself is intact (provider reports on first query; the picker refetches while the catalog is not live). - opus → claude-opus-4-8 (Opus 4.8), sonnet → claude-sonnet-5 (Sonnet 5); haiku stays claude-haiku-4-5-20251001 - context-window table now recognizes all current 1M families (Fable 5, Opus 4.5-4.8, Sonnet 5, Sonnet 4.6) instead of only opus-4-7 — the sonnet alias and Sonnet 4.6 sessions were being under-reported as 200k, inflating ctx% warnings 5x - stale version comments updated across daemon + TUI status bar Old ids remain valid session values (they're still-served models and resolveModelId passes any claude-* through); only the opinionated alias→current-best mapping and the window table moved. Co-Authored-By: Claude Fable 5 --- src/daemon/context-math.ts | 2 +- src/daemon/context-windows.test.ts | 35 +++++++++++---------- src/daemon/context-windows.ts | 49 ++++++++++++++++++++---------- src/daemon/models.ts | 10 +++--- src/daemon/session.ts | 4 +-- src/tui/components/StatusBar.tsx | 6 ++-- 6 files changed, 63 insertions(+), 43 deletions(-) diff --git a/src/daemon/context-math.ts b/src/daemon/context-math.ts index 107747c..f23f750 100644 --- a/src/daemon/context-math.ts +++ b/src/daemon/context-math.ts @@ -12,7 +12,7 @@ * (they're all bytes the model processed for that primary call) */ -/** Default context window for Opus 4.7 and Sonnet 4.x (with 1M beta). */ +/** Default context window for the current Opus / Sonnet families (1M). */ export const CONTEXT_WINDOW_DEFAULT = 1_000_000; /** diff --git a/src/daemon/context-windows.test.ts b/src/daemon/context-windows.test.ts index 8eaa116..0ca6ebc 100644 --- a/src/daemon/context-windows.test.ts +++ b/src/daemon/context-windows.test.ts @@ -6,45 +6,48 @@ import { } from "./context-windows"; describe("contextWindowForModel", () => { - test("opus-4-7 explicit -> 1M", () => { + test("current 1M families -> 1M", () => { + expect(contextWindowForModel("claude-opus-4-8")).toBe(ONE_MILLION_CONTEXT); expect(contextWindowForModel("claude-opus-4-7")).toBe(ONE_MILLION_CONTEXT); + expect(contextWindowForModel("claude-opus-4-6")).toBe(ONE_MILLION_CONTEXT); + expect(contextWindowForModel("claude-sonnet-5")).toBe(ONE_MILLION_CONTEXT); + expect(contextWindowForModel("claude-sonnet-4-6")).toBe(ONE_MILLION_CONTEXT); + expect(contextWindowForModel("claude-fable-5")).toBe(ONE_MILLION_CONTEXT); + }); + + test("dated point releases keep the family window", () => { + expect(contextWindowForModel("claude-opus-4-8-20260101")).toBe(ONE_MILLION_CONTEXT); expect(contextWindowForModel("claude-opus-4-7-20260101")).toBe(ONE_MILLION_CONTEXT); }); - test("opus alias -> 1M (codeoid default)", () => { + test("opus / sonnet aliases -> 1M (Opus 4.8 / Sonnet 5)", () => { expect(contextWindowForModel("opus")).toBe(ONE_MILLION_CONTEXT); + expect(contextWindowForModel("sonnet")).toBe(ONE_MILLION_CONTEXT); }); - test("sonnet / haiku aliases -> 200k", () => { - expect(contextWindowForModel("sonnet")).toBe(DEFAULT_CONTEXT_WINDOW); + test("haiku alias and full id -> 200k", () => { expect(contextWindowForModel("haiku")).toBe(DEFAULT_CONTEXT_WINDOW); - }); - - test("default opus 4.x family -> 200k (no -1m suffix)", () => { - expect(contextWindowForModel("claude-opus-4-6")).toBe(DEFAULT_CONTEXT_WINDOW); - }); - - test("sonnet / haiku full ids -> 200k", () => { - expect(contextWindowForModel("claude-sonnet-4-6")).toBe(DEFAULT_CONTEXT_WINDOW); expect(contextWindowForModel("claude-haiku-4-5")).toBe(DEFAULT_CONTEXT_WINDOW); + expect(contextWindowForModel("claude-haiku-4-5-20251001")).toBe(DEFAULT_CONTEXT_WINDOW); }); test("explicit -1m suffix -> 1M", () => { - expect(contextWindowForModel("claude-sonnet-4-6-1m")).toBe(ONE_MILLION_CONTEXT); + expect(contextWindowForModel("claude-sonnet-4-5-1m")).toBe(ONE_MILLION_CONTEXT); }); - test("unknown model -> conservative 200k miss", () => { + test("unknown claude model -> conservative 200k miss", () => { + expect(contextWindowForModel("claude-sonnet-4-0")).toBe(DEFAULT_CONTEXT_WINDOW); expect(contextWindowForModel("custom-model")).toBe(DEFAULT_CONTEXT_WINDOW); }); - test("undefined / empty -> 1M (codeoid default == opus-4-7)", () => { + test("undefined / empty -> 1M (codeoid default == opus family)", () => { expect(contextWindowForModel(undefined)).toBe(ONE_MILLION_CONTEXT); expect(contextWindowForModel(null)).toBe(ONE_MILLION_CONTEXT); expect(contextWindowForModel("")).toBe(ONE_MILLION_CONTEXT); }); test("case-insensitive", () => { - expect(contextWindowForModel("Claude-Opus-4-7")).toBe(ONE_MILLION_CONTEXT); + expect(contextWindowForModel("Claude-Opus-4-8")).toBe(ONE_MILLION_CONTEXT); expect(contextWindowForModel("OPUS")).toBe(ONE_MILLION_CONTEXT); }); }); diff --git a/src/daemon/context-windows.ts b/src/daemon/context-windows.ts index 064b84d..fea9cc3 100644 --- a/src/daemon/context-windows.ts +++ b/src/daemon/context-windows.ts @@ -4,14 +4,14 @@ * Anthropic publishes context windows per model family; today the relevant * facts for codeoid are: * - * - claude-opus-4-7 (the "1M" variant): 1,000,000 - * - claude-opus-4-x (default): 200,000 - * - claude-sonnet-4-x: 200,000 (beta 1M is gated, treat as default) + * - claude-fable-5 / claude-mythos-5: 1,000,000 + * - claude-opus-4-5 through claude-opus-4-8: 1,000,000 + * - claude-sonnet-5 / claude-sonnet-4-6: 1,000,000 * - claude-haiku-4-x: 200,000 * - * Aliases (`opus` / `sonnet` / `haiku`) resolve to the family's default - * context window. When the SDK swaps in a different concrete model under - * the alias, the daemon updates `SessionInfo.model` and we re-derive. + * Aliases (`opus` / `sonnet` / `haiku`) resolve to the family's context + * window. When the SDK swaps in a different concrete model under the + * alias, the daemon updates `SessionInfo.model` and we re-derive. * * Unknown models fall back to 200k — the conservative miss matches every * non-1M Claude model and keeps the percent-of-window accurate within @@ -21,25 +21,42 @@ export const DEFAULT_CONTEXT_WINDOW = 200_000; export const ONE_MILLION_CONTEXT = 1_000_000; +/** Model-id fragments (lowercase) whose families ship a 1M context window. */ +const ONE_MILLION_FAMILIES = [ + "fable-5", + "mythos-5", + "opus-4-5", + "opus-4.5", + "opus-4-6", + "opus-4.6", + "opus-4-7", + "opus-4.7", + "opus-4-8", + "opus-4.8", + "sonnet-5", + "sonnet-4-6", + "sonnet-4.6", +] as const; + /** * Resolve the context window for a model id (or alias). Case-insensitive. * Matches by prefix + substring so future minor versions (e.g. - * `claude-opus-4-7-1m-20260101`) don't break the table. + * `claude-opus-4-8-20260101`) don't break the table. */ export function contextWindowForModel(modelId: string | undefined | null): number { - if (!modelId) return ONE_MILLION_CONTEXT; // codeoid default; opus-4-7 is the SDK default + if (!modelId) return ONE_MILLION_CONTEXT; // codeoid default; the `opus` alias family is 1M const m = modelId.toLowerCase(); - // Explicit 1M-context Opus variant. - if (m.includes("opus-4-7") || m.includes("opus-4.7")) return ONE_MILLION_CONTEXT; + // Known 1M-context families. + for (const family of ONE_MILLION_FAMILIES) { + if (m.includes(family)) return ONE_MILLION_CONTEXT; + } if (m.includes("-1m")) return ONE_MILLION_CONTEXT; - // Aliases (matching the daemon's model resolver). - if (m === "opus" || m === "sonnet" || m === "haiku") { - return m === "opus" - ? ONE_MILLION_CONTEXT // codeoid's `opus` alias resolves to opus-4-7 (1M) - : DEFAULT_CONTEXT_WINDOW; - } + // Aliases (matching the daemon's model resolver: opus → Opus 4.8, + // sonnet → Sonnet 5 — both 1M; haiku → Haiku 4.5 at 200k). + if (m === "opus" || m === "sonnet") return ONE_MILLION_CONTEXT; + if (m === "haiku") return DEFAULT_CONTEXT_WINDOW; // Other Claude models: 200k. if (m.startsWith("claude-")) return DEFAULT_CONTEXT_WINDOW; diff --git a/src/daemon/models.ts b/src/daemon/models.ts index b1b19bb..26dbac2 100644 --- a/src/daemon/models.ts +++ b/src/daemon/models.ts @@ -44,17 +44,17 @@ export interface ModelDescriptor { */ export const MODEL_CATALOG: readonly ModelDescriptor[] = [ { - id: "claude-opus-4-7", + id: "claude-opus-4-8", alias: "opus", - label: "Opus 4.7", + label: "Opus 4.8", contextWindow: 1_000_000, tier: "premium", description: "Deepest reasoning. Best for planning, refactoring, and hard problems.", }, { - id: "claude-sonnet-4-6", + id: "claude-sonnet-5", alias: "sonnet", - label: "Sonnet 4.6", + label: "Sonnet 5", contextWindow: 1_000_000, tier: "balanced", description: "Fast and capable. Good default for day-to-day coding.", @@ -82,7 +82,7 @@ export function findModel(identifier: string): ModelDescriptor | null { /** * Resolve user input → full model id. Accepts: - * - an alias ("opus" → "claude-opus-4-7") + * - an alias ("opus" → "claude-opus-4-8") * - a full known id (passed through) * - any other string that looks like a Claude model id (passed through — * user knows what they want; we don't gatekeep) diff --git a/src/daemon/session.ts b/src/daemon/session.ts index 31a71b2..f10d1ac 100644 --- a/src/daemon/session.ts +++ b/src/daemon/session.ts @@ -201,8 +201,8 @@ export class Session { // prompt so the agent knows what it was working on. Captured inside // rotate() from the most recent user_turn episode. #lastUserTurnBeforeRotate: string | null = null; - // Claude's context window. Opus 4.7 + Sonnet 4.x (1M beta) share 1M; we - // compute occupancy against this constant. Making it tunable per-session + // Claude's context window. The current Opus and Sonnet families share 1M; + // we compute occupancy against this constant. Making it tunable per-session // was considered overkill — users rarely run sub-1M models via codeoid. static readonly CONTEXT_WINDOW = 1_000_000; diff --git a/src/tui/components/StatusBar.tsx b/src/tui/components/StatusBar.tsx index 4b06847..98f5851 100644 --- a/src/tui/components/StatusBar.tsx +++ b/src/tui/components/StatusBar.tsx @@ -67,7 +67,7 @@ export function StatusBar({ ? Math.max(0, Math.floor((Date.now() - workingSince) / 1000)) : null; // Resolve the denominator for ctx% from the CURRENT model's real window. - // Haiku 4.5 = 200k; Opus 4.7 / Sonnet 4.6 = 1M. Hardcoding 1M made a Haiku + // Haiku 4.5 = 200k; Opus 4.8 / Sonnet 5 = 1M. Hardcoding 1M made a Haiku // session at 150k look like "15%" when it's really 75% full. const contextWindow = (focused?.info.model && findModel(focused.info.model)?.contextWindow) || @@ -145,7 +145,7 @@ export function StatusBar({ <> {" · "} - {/* Prefer the resolved full id (e.g. "claude-opus-4-7") so + {/* Prefer the resolved full id (e.g. "claude-opus-4-8") so operators can see the exact version routed to the SDK. Falls through to whatever the session reports if we don't recognize it in the catalog. */} @@ -330,7 +330,7 @@ function pickCacheColor(u: { /** * Fallback context window when the session reports a model we don't recognize. - * 1M matches the default codeoid ships with (Opus 4.7 / Sonnet 4.6 both 1M). + * 1M matches the default codeoid ships with (Opus 4.8 / Sonnet 5 both 1M). * The real denominator comes from `findModel(id).contextWindow` per-session * — this only kicks in for unknown ids (e.g. a passthrough `claude-foo-bar`). */