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],