mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-27 04:30:59 +00:00
fix: Infer 1M context from model descriptions (#799)
This commit is contained in:
@@ -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"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user