Skip to content
Open
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 packages/host-daemon-contract/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@
//
// The version mismatch is what triggers the enrolled daemon's automatic update
// instead of an `invalid-message` reconnect loop.
export const HOST_DAEMON_PROTOCOL_VERSION = 153 as const;
export const HOST_DAEMON_PROTOCOL_VERSION = 154 as const;

/**
* Absolute ceiling for any executable artifact delivered to a host daemon —
Expand Down
2 changes: 1 addition & 1 deletion packages/host-daemon-contract/test/contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1133,7 +1133,7 @@ describe("host-daemon command schemas", () => {
// mixed version. Version 113 carried the Devin Desktop open target rename
// and remains part of the protocol lineage.
it("uses the current host-daemon protocol version", () => {
expect(HOST_DAEMON_PROTOCOL_VERSION).toBe(153);
expect(HOST_DAEMON_PROTOCOL_VERSION).toBe(154);
expect(HOST_ARTIFACT_MAX_BYTES).toBe(256 * 1024 * 1024);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
{"ts":1787279795703,"run":1787279794200,"seq":10.2,"dir":"bridge→runtime","line":"{\"jsonrpc\":\"2.0\",\"id\":8,\"result\":{\"threadId\":\"thr_9e3ukzazz3\"}}"}
{"ts":1787279795704,"run":1787279794200,"seq":10.3,"dir":"bridge→runtime","line":"{\"jsonrpc\":\"2.0\",\"method\":\"thread/identity\",\"params\":{\"threadId\":\"thr_9e3ukzazz3\",\"providerThreadId\":\"54386450-a88e-4d9e-b4e1-8893a0c47239\",\"sessionRestorable\":true}}"}
{"ts":1787279795705,"run":1787279794200,"seq":10.4,"dir":"bridge→runtime","line":"{\"jsonrpc\":\"2.0\",\"method\":\"thread/delta\",\"params\":{\"threadId\":\"thr_9e3ukzazz3\",\"deltas\":[{\"kind\":\"turn.open\"},{\"kind\":\"item.textClose\",\"key\":{\"channel\":\"assistant\"},\"channel\":\"agentMessage\",\"text\":\"Not logged in · Please run /login\"}]}}"}
{"ts":1787279795706,"run":1787279794200,"seq":10.5,"dir":"bridge→runtime","line":"{\"jsonrpc\":\"2.0\",\"method\":\"thread/delta\",\"params\":{\"threadId\":\"thr_9e3ukzazz3\",\"deltas\":[{\"kind\":\"turn.open\"},{\"kind\":\"contextWindow\",\"used\":0,\"size\":null,\"estimated\":true,\"attach\":\"open\"},{\"kind\":\"usage\",\"total\":{\"totalTokens\":0,\"inputTokens\":0,\"cachedInputTokens\":0,\"outputTokens\":0,\"reasoningOutputTokens\":0},\"last\":{\"totalTokens\":0,\"inputTokens\":0,\"cachedInputTokens\":0,\"outputTokens\":0,\"reasoningOutputTokens\":0},\"modelContextWindow\":null},{\"kind\":\"provider.error\",\"message\":\"Provider error\",\"detail\":\"Not logged in · Please run /login\"},{\"kind\":\"turn.boundary\",\"status\":\"failed\",\"providerCheckpointId\":\"4a270486-b600-43dd-a6bd-07b72524f931\"}]}}"}
{"ts":1787279795706,"run":1787279794200,"seq":10.5,"dir":"bridge→runtime","line":"{\"jsonrpc\":\"2.0\",\"method\":\"thread/delta\",\"params\":{\"threadId\":\"thr_9e3ukzazz3\",\"deltas\":[{\"kind\":\"turn.open\"},{\"kind\":\"contextWindow\",\"used\":0,\"size\":1000000,\"estimated\":true,\"attach\":\"open\"},{\"kind\":\"usage\",\"total\":{\"totalTokens\":0,\"inputTokens\":0,\"cachedInputTokens\":0,\"outputTokens\":0,\"reasoningOutputTokens\":0},\"last\":{\"totalTokens\":0,\"inputTokens\":0,\"cachedInputTokens\":0,\"outputTokens\":0,\"reasoningOutputTokens\":0},\"modelContextWindow\":null},{\"kind\":\"provider.error\",\"message\":\"Provider error\",\"detail\":\"Not logged in · Please run /login\"},{\"kind\":\"turn.boundary\",\"status\":\"failed\",\"providerCheckpointId\":\"4a270486-b600-43dd-a6bd-07b72524f931\"}]}}"}
46 changes: 35 additions & 11 deletions plugins/provider-claude-code/src/bridge/__tests__/bridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4529,13 +4529,7 @@ describe("canonical model context-window hint", () => {
permissionEscalation: null,
};

// Claude reports `modelUsage.contextWindow` on some results and omits it on
// others. The legacy adapter seeded a model-derived fallback on every
// command plan that carried a model; the canonical bridge has no command
// plan, so it seeds the translator at session construction instead. Without
// that seeding, capacity read as unknown for every result Claude sent
// without the field — notably the 1M `[1m]` aliases.
it("seeds the model-derived context window for canonical sessions", async () => {
it("uses Fable's Claude Code capacity through a custom API endpoint", async () => {
const bridge = createBridgeJsonRpcTestHarness(handleLine);
const queries: ControlledClaudeQuery[] = [];
queryMock.mockImplementation(() => {
Expand All @@ -4549,22 +4543,32 @@ describe("canonical model context-window hint", () => {
threadId: "thread-context-hint",
cwd: "/tmp/worktree",
instructionMode: "append",
options: { ...canonicalOptions, model: "claude-opus-4-7[1m]" },
options: {
...canonicalOptions,
model: "claude-fable-5",
envVars: {
ANTHROPIC_BASE_URL: "http://127.0.0.1:8317",
},
},
});
await bridge.waitForResponse(1);

expect(getLatestQueryOptions().env?.ANTHROPIC_BASE_URL).toBe(
"http://127.0.0.1:8317",
);

bridge.sendRequest(2, "turn/start", {
threadId: "thread-context-hint",
providerThreadId: "thread-context-hint",
clientRequestId: "creq_23456789ab",
input: [{ type: "text", text: "hello", mentions: [] }],
options: { ...canonicalOptions, model: "claude-opus-4-7[1m]" },
options: { ...canonicalOptions, model: "claude-fable-5" },
});
await readNextPrompt(getLatestQueryCall());
await bridge.waitForResponse(2);

// A result with token usage but no `modelUsage`: the only capacity
// source left is the seeded hint.
// The SDK reports its legacy 200k BYOK fallback through a custom endpoint,
// although current Fable providers support 1M.
queries[0]?.emit({
type: "result",
subtype: "success",
Expand All @@ -4581,6 +4585,11 @@ describe("canonical model context-window hint", () => {
cache_creation_input_tokens: 30,
cache_read_input_tokens: 40,
},
modelUsage: {
"claude-fable-5": {
contextWindow: 200_000,
},
},
session_id: "session-1",
} as unknown as SDKMessage);
await bridge.flushWork();
Expand All @@ -4600,6 +4609,21 @@ describe("canonical model context-window hint", () => {
expect(contextWindowEvents.at(-1)?.contextWindowUsage).toMatchObject({
modelContextWindow: 1_000_000,
});

const tokenUsageEvents = assembleCapturedThreadEvents(
bridge.messages,
"claude-code",
).filter(
(
event,
): event is Extract<
ThreadEvent,
{ type: "thread/tokenUsage/updated" }
> => event.type === "thread/tokenUsage/updated",
);
expect(tokenUsageEvents.at(-1)?.tokenUsage).toMatchObject({
modelContextWindow: 1_000_000,
});
} finally {
queries[0]?.finish();
bridge.restore();
Expand Down
9 changes: 3 additions & 6 deletions plugins/provider-claude-code/src/bridge/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -823,13 +823,10 @@ function withTrackedPermissionEscalation(
}

/**
* Seed the translator's context-window fallback from the selected model.
* Seed the translator's context-window hint from the selected model.
*
* Claude reports `modelUsage.contextWindow` on some results and omits it on
* others; when it is missing the translator falls back to the capacity implied
* by the model id (notably the 1M `[1m]` aliases). The bridge seeds the hint
* here, on every session construction and every turn that carries a model —
* without it, capacity reads as unknown whenever Claude omits the field.
* Claude can omit `modelUsage.contextWindow`. Seed the selected model as its
* fallback; reported model entries normalize their own known capacity.
*/
function seedModelContextWindowHint(
threadSession: ThreadSession,
Expand Down
8 changes: 4 additions & 4 deletions plugins/provider-claude-code/src/delta-translation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1483,10 +1483,10 @@ export function createClaudeDeltaTranslator() {
}

/**
* Seed the context-window fallback from the selected model. Claude reports
* `modelUsage.contextWindow` on some results and omits it on others; when
* missing, capacity falls back to what the model id implies (notably the 1M
* `[1m]` aliases). Called at session construction and live model changes.
* Seed the context-window hint from the selected model. The model's known
* Claude Code capacity is a floor for `modelUsage.contextWindow`, which can
* report the generic 200k window through custom Anthropic endpoints. Called
* at session construction and live model changes.
*/
function setClaudeModelContextWindowHint(
threadId: string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ describe("claude usage and fixture translation (delta path)", () => {
);
});

// The four cases below pin the translator's use of sdk-extraction.ts's
// The cases below pin the translator's use of sdk-extraction.ts's
// resolveClaudeModelContextWindowHint. The canonical bridge calls
// setClaudeModelContextWindowHint from session construction and from the
// live model change; that the bridge really calls it is pinned separately
Expand Down
17 changes: 13 additions & 4 deletions plugins/provider-claude-code/src/sdk-extraction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,8 @@ export function extractClaudeContextWindowUsage(
args.message.modelUsage,
);
const modelContextWindow = parsedModelUsage.success
? extractModelContextWindow(parsedModelUsage.data)
? (extractModelContextWindow(parsedModelUsage.data) ??
args.fallbackModelContextWindow)
: args.fallbackModelContextWindow;
const usedTokens = args.latestRequestContextTokens ?? null;

Expand Down Expand Up @@ -356,9 +357,17 @@ function extractModelContextWindow(
if (!modelUsage) return null;

let largestContextWindow: number | null = null;
for (const usage of Object.values(modelUsage)) {
const contextWindow = toPositiveNumber(usage.contextWindow);
if (contextWindow === undefined) continue;
for (const [model, usage] of Object.entries(modelUsage)) {
const reportedContextWindow = toPositiveNumber(usage.contextWindow);
if (reportedContextWindow === undefined) continue;

// The SDK's 200k custom-endpoint value is a legacy BYOK fallback. Current
// Fable providers, including Bedrock, support 1M.
const modelContextWindow = resolveClaudeModelContextWindowHint(model);
const contextWindow =
modelContextWindow === null
? reportedContextWindow
: Math.max(reportedContextWindow, modelContextWindow);
if (largestContextWindow === null || contextWindow > largestContextWindow) {
largestContextWindow = contextWindow;
}
Expand Down
Loading