Repository navigation
fix(server): Claude context meter shows the window for models outside the catalog #30051
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,6 +25,7 @@ import { | |
| addClaudeLedgerUsage, | ||
| claudeLedgerDelta, | ||
| claudeModelLedger, | ||
| claudeReportedContextWindows, | ||
| normalizeClaudeTurnTokenUsage, | ||
| type ClaudeLedgerUsage, | ||
| type ClaudeModelLedger, | ||
|
|
@@ -221,12 +222,6 @@ import { | |
| export const CLAUDE_PROVIDER = ProviderDriverKind.make("claudeAgent"); | ||
| const CLAUDE_AGENT_SDK_QUERY_PROTOCOL = "claude-agent-sdk.query" as const; | ||
|
|
||
| function claudeContextWindow(modelSelection: ModelSelection): number | null { | ||
| return ( | ||
| resolveClaudeCatalogContextWindowTokens(BUNDLED_CLAUDE_MODEL_CATALOG, modelSelection) ?? null | ||
| ); | ||
| } | ||
|
|
||
| interface ClaudeCacheCreation { | ||
| readonly ephemeral_1h_input_tokens: number; | ||
| readonly ephemeral_5m_input_tokens: number; | ||
|
|
@@ -273,15 +268,14 @@ export function claudeProviderTurnTokenUsage( | |
| readonly cache_creation?: ClaudeCacheCreation | null; | ||
| readonly output_tokens: number; | ||
| }, | ||
| modelSelection: ModelSelection, | ||
| maxTokens: number | null, | ||
| updatedAt: string, | ||
| autoCompactWindow = "", | ||
| requestedAt?: DateTime.Utc, | ||
| ) { | ||
| const inputTokens = claudeMessageInputTokens(usage); | ||
| const outputTokens = usage.output_tokens; | ||
| const promptCacheTtlMs = claudePromptCacheTtlMs(usage.cache_creation); | ||
| const maxTokens = claudeContextWindow(modelSelection); | ||
| // The cache lifetime starts with the request, not when its response ends. | ||
| const promptCacheExpiresAt = | ||
| promptCacheTtlMs === undefined || requestedAt === undefined | ||
|
|
@@ -4122,6 +4116,7 @@ interface ActiveClaudeTurnContext { | |
| requestedAt?: DateTime.Utc; | ||
| usage: ClaudeMessageUsage | null; | ||
| } | null; | ||
| compactionTokens: number | undefined; | ||
| readonly toolCalls: Map<string, ActiveClaudeToolCall>; | ||
| // A successful status can precede the boundary that supplies its token counts. | ||
| compaction: Extract<OrchestrationV2TurnItem, { type: "compaction" }> | null; | ||
|
|
@@ -4620,6 +4615,12 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( | |
| BUNDLED_CLAUDE_MODEL_CATALOG, | ||
| adapterOptions.settings.customModels, | ||
| ); | ||
| // Results report the window Claude Code compacts against; outside the catalog it is the only source. | ||
| const reportedContextWindows = new Map<string, number>(); | ||
| const claudeContextWindow = (modelSelection: ModelSelection): number | null => | ||
| resolveClaudeCatalogContextWindowTokens(BUNDLED_CLAUDE_MODEL_CATALOG, modelSelection) ?? | ||
| reportedContextWindows.get(compileClaudeModelSelection(modelSelection).apiModelId) ?? | ||
| null; | ||
| const continuationRequests = adapterOptions.continuationRequests ?? { | ||
| offer: () => Effect.void, | ||
| }; | ||
|
|
@@ -4852,6 +4853,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( | |
| const queryContext = yield* Ref.make<ClaudeLiveQueryContext | null>(null); | ||
| // Wake buffers replay result frames, so ledger growth is measured once on arrival. | ||
| const resultLedgerUsage = new WeakMap<SDKMessage, ClaudeLedgerUsage>(); | ||
| const resultsReportingNewContextWindow = new WeakSet<SDKMessage>(); | ||
| // A worktree handoff changes the project settings even within one session. | ||
| let bypassCache: { cwd: string | null; availability: ClaudeBypassAvailability } | null = | ||
| null; | ||
|
|
@@ -5138,21 +5140,36 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( | |
| // Usage without input is a placeholder, not a context measurement. | ||
| const emitContextUsage = Effect.fnUntraced(function* ( | ||
| context: ActiveClaudeTurnContext, | ||
| usage: ClaudeMessageUsage, | ||
| usage: ClaudeMessageUsage | number, | ||
| requestStarted = false, | ||
| ) { | ||
| const now = yield* DateTime.now; | ||
| // Only message_start marks the request start; an assistant snapshot | ||
| // arrives after generation, so it leaves the cache expiry unknown. | ||
| if (requestStarted && context.streamUsage !== null) context.streamUsage.requestedAt = now; | ||
| const tokenUsage = claudeProviderTurnTokenUsage( | ||
| usage, | ||
| context.input.modelSelection, | ||
| DateTime.formatIso(now), | ||
| adapterOptions.settings.autoCompactWindow, | ||
| context.streamUsage?.requestedAt, | ||
| ); | ||
| if (tokenUsage.inputTokens <= 0) return; | ||
| const maxTokens = claudeContextWindow(context.input.modelSelection); | ||
| const tokenUsage = | ||
| typeof usage === "number" | ||
| ? { | ||
| usedTokens: usage, | ||
| maxTokens, | ||
| ...claudeAutoCompactThreshold( | ||
| adapterOptions.settings.autoCompactWindow, | ||
| maxTokens, | ||
| ), | ||
| updatedAt: DateTime.formatIso(now), | ||
| } | ||
| : claudeProviderTurnTokenUsage( | ||
| usage, | ||
| maxTokens, | ||
| DateTime.formatIso(now), | ||
| adapterOptions.settings.autoCompactWindow, | ||
| context.streamUsage?.requestedAt, | ||
| ); | ||
| if (typeof usage !== "number") { | ||
| if (claudeMessageInputTokens(usage) <= 0) return; | ||
| context.compactionTokens = undefined; | ||
| } | ||
| yield* emitProviderEvent({ | ||
| type: "provider_turn.updated", | ||
| driver: CLAUDE_PROVIDER, | ||
|
|
@@ -8827,38 +8844,8 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( | |
| typeof postTokens === "number" && Number.isFinite(postTokens) && postTokens > 0 | ||
| ? Math.round(postTokens) | ||
| : undefined; | ||
| if (afterTokenCount !== undefined) { | ||
| const maxTokens = claudeContextWindow(context.input.modelSelection); | ||
| yield* emitProviderEvent({ | ||
| type: "provider_turn.updated", | ||
| driver: CLAUDE_PROVIDER, | ||
| threadId: context.input.threadId, | ||
| providerTurn: { | ||
| id: context.providerTurnId, | ||
| providerThreadId: context.input.providerThread.id, | ||
| nodeId: context.input.rootNodeId, | ||
| runAttemptId: context.input.attemptId, | ||
| nativeTurnRef: { | ||
| driver: CLAUDE_PROVIDER, | ||
| nativeId: context.nativeTurnId, | ||
| strength: "strong", | ||
| }, | ||
| ordinal: context.providerTurnOrdinal, | ||
| status: "running", | ||
| startedAt: context.startedAt, | ||
| completedAt: null, | ||
| tokenUsage: { | ||
| usedTokens: afterTokenCount, | ||
| maxTokens, | ||
| ...claudeAutoCompactThreshold( | ||
| adapterOptions.settings.autoCompactWindow, | ||
| maxTokens, | ||
| ), | ||
| updatedAt: DateTime.formatIso(now), | ||
| }, | ||
| }, | ||
| }); | ||
| } | ||
| context.compactionTokens = afterTokenCount; | ||
| if (afterTokenCount !== undefined) yield* emitContextUsage(context, afterTokenCount); | ||
| yield* emitProviderEvent({ | ||
| type: "turn_item.updated", | ||
| driver: CLAUDE_PROVIDER, | ||
|
|
@@ -9584,6 +9571,10 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( | |
| } | ||
|
|
||
| if (message.type === "result") { | ||
| const contextUsage = context.streamUsage?.usage ?? context.compactionTokens; | ||
| if (resultsReportingNewContextWindow.has(message) && contextUsage !== undefined) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Refresh each turn that has not emitted the window. If two sessions use the same custom model concurrently, both can emit usage with
|
||
| yield* emitContextUsage(context, contextUsage); | ||
| } | ||
| const completedAt = yield* DateTime.now; | ||
| const interrupted = (yield* Ref.get(interruptedTurns)).has(context.providerTurnId); | ||
| const wasSteered = (yield* Ref.get(steeredTurns)).has(context.providerTurnId); | ||
|
|
@@ -10646,6 +10637,13 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( | |
| growth === undefined && context.modelLedger !== undefined | ||
| ? undefined | ||
| : ledger; | ||
| for (const [model, window] of claudeReportedContextWindows( | ||
| message.modelUsage, | ||
| )) { | ||
| if (reportedContextWindows.get(model) === window) continue; | ||
| reportedContextWindows.set(model, window); | ||
| resultsReportingNewContextWindow.add(message); | ||
| } | ||
| } | ||
| return handleSdkMessage({ query: querySession, message }); | ||
| }), | ||
|
|
@@ -10784,6 +10782,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( | |
| blocks: new Map(), | ||
| }, | ||
| streamUsage: null, | ||
| compactionTokens: undefined, | ||
| toolCalls: new Map(), | ||
| compaction: null, | ||
| ignoredTaskIds: new Set(), | ||
|
|
@@ -11348,8 +11347,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( | |
| driver: CLAUDE_PROVIDER, | ||
| providerSessionId: input.providerSessionId, | ||
| providerSession: session, | ||
| getModelContextWindow: (selection) => | ||
| resolveClaudeCatalogContextWindowTokens(BUNDLED_CLAUDE_MODEL_CATALOG, selection), | ||
| getModelContextWindow: (selection) => claudeContextWindow(selection) ?? undefined, | ||
| events: Stream.fromEffectRepeat(Queue.take(events)), | ||
| backgroundTasksEndWithProcess: true, | ||
| hasPendingBackgroundWork: Effect.gen(function* () { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Retain the last valid usage measurement.
A later
message_startcan replace valid usage with a zero-input placeholder. The result then selects that placeholder, whichemitContextUsagerejects, so the newly reported window is never emitted. This sequence is covered by an existing usage test. (raw.githubusercontent.com)Retain the last accepted measurement separately and clear it at compaction.