fix: Infer 1M context from model descriptions (#799)

This commit is contained in:
Ben Brandt
2026-06-22 12:37:07 +02:00
committed by GitHub
parent 8dc8c86426
commit 508453c288
3 changed files with 50 additions and 11 deletions

View File

@@ -40,7 +40,7 @@ export default [
"default-case": "error", "default-case": "error",
"prefer-const": "error", "prefer-const": "error",
"no-var": "error", "no-var": "error",
eqeqeq: ["error", "always"], eqeqeq: ["error", "smart"],
curly: ["error", "all"], curly: ["error", "all"],
}, },
}, },

View File

@@ -2652,19 +2652,27 @@ export class ClaudeAcpAgent {
o.id === configId && typeof o.currentValue === "string" ? { ...o, currentValue: value } : o, o.id === configId && typeof o.currentValue === "string" ? { ...o, currentValue: value } : o,
); );
} else if (configId === "model") { } else if (configId === "model") {
// `ModelInfo.supportsAutoMode` is the canonical SDK signal for clamping
// modes below; its `displayName`/`description` also let us infer the
// context window for semantic aliases (e.g. `default`) whose ID alone
// carries no "1m" token.
const newModelInfo = session.modelInfos.find((m) => m.value === value);
if (session.models.currentModelId !== value) { if (session.models.currentModelId !== value) {
// The cached context window was learned for the previous model; reset // The cached context window was learned for the previous model; reset
// to the new model's heuristic so mid-stream updates between now and // to the new model's heuristic so mid-stream updates between now and
// the next `result` reflect the user's selection instead of the old // the next `result` reflect the user's selection instead of the old
// model's window. // model's window.
session.contextWindowSize = inferContextWindowFromModel(value) ?? DEFAULT_CONTEXT_WINDOW; session.contextWindowSize =
inferContextWindowFromModel(
value,
newModelInfo?.displayName,
newModelInfo?.description,
) ?? DEFAULT_CONTEXT_WINDOW;
} }
session.models = { ...session.models, currentModelId: value }; session.models = { ...session.models, currentModelId: value };
// Recompute availableModes for the new model and clamp the current // Recompute availableModes for the new model and clamp the current
// mode if the SDK no longer offers it (today: "auto" on Haiku). // mode if the SDK no longer offers it (today: "auto" on Haiku).
// `ModelInfo.supportsAutoMode` is the canonical SDK signal.
const newModelInfo = session.modelInfos.find((m) => m.value === value);
const newAvailableModes = buildAvailableModes(newModelInfo); const newAvailableModes = buildAvailableModes(newModelInfo);
// Capture BEFORE mutating session.modes so the log message reflects // Capture BEFORE mutating session.modes so the log message reflects
// the invalidated mode rather than "default". // the invalidated mode rather than "default".
@@ -3219,7 +3227,11 @@ export class ClaudeAcpAgent {
abortController, abortController,
emitRawSDKMessages: sessionMeta?.claudeCode?.emitRawSDKMessages ?? false, emitRawSDKMessages: sessionMeta?.claudeCode?.emitRawSDKMessages ?? false,
contextWindowSize: contextWindowSize:
inferContextWindowFromModel(models.currentModelId) ?? DEFAULT_CONTEXT_WINDOW, inferContextWindowFromModel(
models.currentModelId,
currentModelInfo?.displayName,
currentModelInfo?.description,
) ?? DEFAULT_CONTEXT_WINDOW,
taskState, taskState,
toolUseCache: {}, toolUseCache: {},
messageIdToUuid: new Map(), messageIdToUuid: new Map(),
@@ -4377,14 +4389,21 @@ function commonPrefixLength(a: string, b: string) {
return i; return i;
} }
/** Best-effort first guess of a model's context window from its ID, used only /** Best-effort first guess of a model's context window, used only as a
* as a fallback when the SDK's authoritative `getContextUsage` is unavailable * fallback when the SDK's authoritative `getContextUsage` is unavailable (and
* (and until a `result` message arrives with the `modelUsage` value). * until a `result` message arrives with the `modelUsage` value).
*
* Anthropic 1M-context variants encode "1m" as a distinct token in the SDK * Anthropic 1M-context variants encode "1m" as a distinct token in the SDK
* model ID (e.g., "claude-opus-4-6-1m"), which `\b1m\b` catches without also * model ID (e.g., "claude-opus-4-6-1m"), which `\b1m\b` catches without also
* matching things like "10m" or embedded substrings. */ * matching things like "10m" or embedded substrings. Semantic aliases like
function inferContextWindowFromModel(model: string): number | null { * `default` carry no such token in the ID, but the SDK's human-facing
if (/\b1m\b/i.test(model)) return 1_000_000; * `displayName`/`description` do (e.g. "Opus 4.7 (1M context)"), so callers
* pass those too — the same `\b1m\b` token appears in "1M context". The SDK's
* `ModelInfo` exposes no structured context-window field, so this text scan is
* the only pre-`result` signal available. A miss falls back to the default
* window and is corrected by `result.modelUsage` within one turn. */
function inferContextWindowFromModel(...texts: Array<string | undefined>): number | null {
if (texts.some((text) => text != null && /\b1m\b/i.test(text))) return 1_000_000;
return null; return null;
} }

View File

@@ -3072,6 +3072,26 @@ describe("usage_update computation", () => {
expect(usageUpdates[1].update.size).toBe(1000000); expect(usageUpdates[1].update.size).toBe(1000000);
}); });
it("infers the 1M window from a model's description when the ID lacks a 1m token (issue #596)", async () => {
// Semantic aliases like `default` resolve to a 1M-context model but carry
// no "1m" token in the modelId — the SDK signals 1M only via the
// human-facing displayName/description (e.g. "Opus 4.7 with 1M context").
// Inference must read those so the session reports the correct window from
// the first mid-stream update instead of the 200k placeholder.
const { agent } = createMockAgentWithCapture();
injectSession(agent, [{ type: "system", subtype: "session_state_changed", state: "idle" }]);
const session = agent.sessions["test-session"];
session.models = { currentModelId: "claude-sonnet-4-6", availableModels: [] };
session.modelInfos = [
{ value: "default", displayName: "Default", description: "Opus 4.7 with 1M context" },
] as any;
expect(session.contextWindowSize).toBe(200000);
await (agent as any).applyConfigOptionValue("test-session", session, "model", "default");
expect(session.contextWindowSize).toBe(1000000);
});
it("result with no matching modelUsage preserves the learned window", async () => { it("result with no matching modelUsage preserves the learned window", async () => {
// A turn whose `result.modelUsage` doesn't contain the current top-level // A turn whose `result.modelUsage` doesn't contain the current top-level
// model (e.g. no top-level assistant message, or only a subagent ran) must // model (e.g. no top-level assistant message, or only a subagent ran) must