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
2 changes: 1 addition & 1 deletion src/daemon/context-math.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down
35 changes: 19 additions & 16 deletions src/daemon/context-windows.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
49 changes: 33 additions & 16 deletions src/daemon/context-windows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand Down
10 changes: 5 additions & 5 deletions src/daemon/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions src/daemon/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
6 changes: 3 additions & 3 deletions src/tui/components/StatusBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) ||
Expand Down Expand Up @@ -145,7 +145,7 @@ export function StatusBar({
<>
<Text dimColor>{" · "}</Text>
<Text color="cyan">
{/* 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. */}
Expand Down Expand Up @@ -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`).
*/
Expand Down
Loading