From ea78c51174f2a02a3b80be4bc6fc777886fa72c2 Mon Sep 17 00:00:00 2001 From: hugocasa Date: Tue, 18 Aug 2026 17:01:10 +0200 Subject: [PATCH] fix: report AI usage before tools run and price self usage consistently Co-Authored-By: Claude Opus 5 (1M context) --- .../src/lib/components/UserSettings.svelte | 11 ++++++---- .../lib/components/copilot/chat/anthropic.ts | 3 ++- .../lib/components/copilot/chat/chatLoop.ts | 20 ++++++++++++++++--- .../copilot/chat/openai-responses.ts | 3 ++- frontend/src/lib/components/copilot/lib.ts | 7 ++++++- .../lib/components/copilot/modelPricing.ts | 4 ++-- 6 files changed, 36 insertions(+), 12 deletions(-) diff --git a/frontend/src/lib/components/UserSettings.svelte b/frontend/src/lib/components/UserSettings.svelte index 4c31867900..d5811e737b 100644 --- a/frontend/src/lib/components/UserSettings.svelte +++ b/frontend/src/lib/components/UserSettings.svelte @@ -10,8 +10,7 @@ import UserInfoSettings from './settings/UserInfoSettings.svelte' import AIUserSettings from './settings/AIUserSettings.svelte' import AiUsagePanel from './workspaceSettings/AiUsagePanel.svelte' - import { copilotInfo } from '$lib/aiStore' - import { workspaceStore } from '$lib/stores' + import { copilotInfo, copilotWorkspace } from '$lib/aiStore' import { getDarkModeVariant, setDarkModeVariant, @@ -108,9 +107,13 @@ - {#if $workspaceStore} + + {#if $copilotWorkspace} diff --git a/frontend/src/lib/components/copilot/chat/anthropic.ts b/frontend/src/lib/components/copilot/chat/anthropic.ts index 7273dc8cd5..6b375cd462 100644 --- a/frontend/src/lib/components/copilot/chat/anthropic.ts +++ b/frontend/src/lib/components/copilot/chat/anthropic.ts @@ -193,7 +193,7 @@ export async function parseAnthropicCompletion( tools: Tool[], helpers: any, abortController?: AbortController, - options?: { workspace?: string } + options?: { workspace?: string; onTokenUsage?: (usage: ChatTokenUsage) => void } ): Promise { let toolCallsToProcess: ChatCompletionMessageFunctionToolCall[] = [] let error = null @@ -415,6 +415,7 @@ export async function parseAnthropicCompletion( const finalMessage = await completion.finalMessage() const tokenUsage = anthropicUsageToChatTokenUsage(finalMessage.usage) + options?.onTokenUsage?.(tokenUsage) // Process tool calls if any if (toolCallsToProcess.length > 0) { diff --git a/frontend/src/lib/components/copilot/chat/chatLoop.ts b/frontend/src/lib/components/copilot/chat/chatLoop.ts index e8530de3f1..33859377f9 100644 --- a/frontend/src/lib/components/copilot/chat/chatLoop.ts +++ b/frontend/src/lib/components/copilot/chat/chatLoop.ts @@ -338,11 +338,18 @@ export async function runChatLoop(config: ChatLoopConfig): Promise { - tokenUsage = addChatTokenUsage(tokenUsage, usage) + // Reported as the provider's usage arrives, not when the parser returns: a parser + // waits on tool execution, which can wait on a person, and a tab closed in that + // gap would drop a response that was already billed. Accounting for the turn's + // own totals stays on the return path, where every parser reports uniformly. + const reportUsage = (usage: ChatTokenUsage | null | undefined) => { if (usage && iterationModel) { config.onUsage?.(usage, iterationModel) } + } + + const trackUsage = (usage: ChatTokenUsage | null | undefined) => { + tokenUsage = addChatTokenUsage(tokenUsage, usage) // Some providers/paths report no usage (prompt 0); keep the last real one. if (usage && usage.prompt > 0) { lastIterationUsage = usage @@ -399,7 +406,14 @@ export async function runChatLoop(config: ChatLoopConfig): Promise t.def) - const parseOptions = { workspace, provider: modelProvider.provider } + // Report each response as its usage arrives rather than after the parser + // returns: a parser waits on tool execution, which can wait on a person, and + // a tab closed in that gap would drop a response the provider already billed. + const parseOptions = { + workspace, + provider: modelProvider.provider, + onTokenUsage: reportUsage + } if (isOpenAI) { const reasoningSummaryCacheKey = getReasoningSummaryCacheKey(workspace, modelProvider) diff --git a/frontend/src/lib/components/copilot/chat/openai-responses.ts b/frontend/src/lib/components/copilot/chat/openai-responses.ts index 19efff2cd4..4118eb492d 100644 --- a/frontend/src/lib/components/copilot/chat/openai-responses.ts +++ b/frontend/src/lib/components/copilot/chat/openai-responses.ts @@ -392,7 +392,7 @@ export async function parseOpenAIResponsesCompletion( addedMessages: ChatCompletionMessageParam[], tools: Tool[], helpers: any, - options?: { workspace?: string } + options?: { workspace?: string; onTokenUsage?: (usage: ChatTokenUsage) => void } ): Promise { let toolCallsToProcess: ChatCompletionMessageFunctionToolCall[] = [] let error: OpenAIError | ResponseErrorEvent | null = null @@ -566,6 +566,7 @@ export async function parseOpenAIResponsesCompletion( const finalResponse = await runner.finalResponse() const tokenUsage = openAIResponsesUsageToChatTokenUsage(finalResponse.usage) + options?.onTokenUsage?.(tokenUsage) for (const item of finalResponse.output ?? []) { if (item.type === 'web_search_call' && !surfacedWebSearchCalls.has(item.id)) { diff --git a/frontend/src/lib/components/copilot/lib.ts b/frontend/src/lib/components/copilot/lib.ts index b12ba7cbed..06f8703bc8 100644 --- a/frontend/src/lib/components/copilot/lib.ts +++ b/frontend/src/lib/components/copilot/lib.ts @@ -1162,7 +1162,11 @@ export async function parseOpenAICompletion( tools: Tool[], helpers: any, _abortController?: AbortController, // unused, for signature compatibility with parseAnthropicCompletion - options?: { workspace?: string; provider?: string } + options?: { + workspace?: string + provider?: string + onTokenUsage?: (usage: ChatTokenUsage) => void + } ): Promise<{ shouldContinue: boolean; tokenUsage: ChatTokenUsage }> { const finalToolCalls: Record = {} // The tool call currently receiving argument deltas; when the stream moves on @@ -1312,6 +1316,7 @@ export async function parseOpenAICompletion( callbacks.onMessageEnd() + options?.onTokenUsage?.(tokenUsage) // Stream over: every parsed call is queued until its turn in processToolCall. for (const toolCall of Object.values(finalToolCalls)) { if (toolCall.id) { diff --git a/frontend/src/lib/components/copilot/modelPricing.ts b/frontend/src/lib/components/copilot/modelPricing.ts index d453aebddb..e326733526 100644 --- a/frontend/src/lib/components/copilot/modelPricing.ts +++ b/frontend/src/lib/components/copilot/modelPricing.ts @@ -96,8 +96,8 @@ const MODEL_PRICES: [name: string, price: PriceEntry | null][] = [ // one, so the write rate never applies. The -mini/-nano entries must precede // the family entry, which would otherwise claim them. // Revisions past gpt-5 are priced separately by OpenAI and are not tracked here. - // `strictVariants` does not cover these: a version bump is digits, not a name - // segment, so `gpt-5-6` would otherwise still match `gpt-5`. + // The matcher's revision guard already keeps them off the family rate; these + // entries stay so a revision the guard admits still resolves to no rate. ['gpt-5.6', null], ['gpt-5.5', null], ['gpt-5.4', null],