( usage: LanguageModelV2Usage | undefined, model: string, providerMetadata?: Record<string, unknown>, metadataModelOverride?: string )
| 100 | * for display in the UI. It does NOT require the tokenizer. |
| 101 | */ |
| 102 | export function createDisplayUsage( |
| 103 | usage: LanguageModelV2Usage | undefined, |
| 104 | model: string, |
| 105 | providerMetadata?: Record<string, unknown>, |
| 106 | metadataModelOverride?: string |
| 107 | ): ChatUsageDisplay | undefined { |
| 108 | if (!usage) return undefined; |
| 109 | |
| 110 | // AI SDK v6 unified semantics: ALL providers now report inputTokens INCLUSIVE |
| 111 | // of cached tokens. Previously Anthropic excluded cached tokens from inputTokens |
| 112 | // but v6 changed this to match OpenAI/Google (inputTokens = total input including |
| 113 | // cache_read + cache_write). We always subtract both cachedInputTokens and |
| 114 | // cacheCreateTokens to get the true non-cached input count. |
| 115 | const cachedTokens = usage.cachedInputTokens ?? 0; |
| 116 | const rawInputTokens = usage.inputTokens ?? 0; |
| 117 | |
| 118 | // Extract cache creation tokens from provider metadata (Anthropic-specific) |
| 119 | // Needed before computing inputTokens since we subtract it from the total. |
| 120 | const cacheCreateTokens = |
| 121 | (providerMetadata?.anthropic as { cacheCreationInputTokens?: number } | undefined) |
| 122 | ?.cacheCreationInputTokens ?? 0; |
| 123 | |
| 124 | // Subtract both cache-read and cache-create tokens to isolate non-cached input. |
| 125 | // Math.max guards against pre-v6 historical data where inputTokens already excluded |
| 126 | // cache tokens (subtraction would go negative). |
| 127 | const inputTokens = Math.max(0, rawInputTokens - cachedTokens - cacheCreateTokens); |
| 128 | |
| 129 | // Extract reasoning tokens with fallback to provider metadata (OpenAI-specific) |
| 130 | const reasoningTokens = |
| 131 | usage.reasoningTokens ?? |
| 132 | (providerMetadata?.openai as { reasoningTokens?: number } | undefined)?.reasoningTokens ?? |
| 133 | 0; |
| 134 | |
| 135 | // Calculate output tokens excluding reasoning |
| 136 | const outputWithoutReasoning = Math.max(0, (usage.outputTokens ?? 0) - reasoningTokens); |
| 137 | |
| 138 | // Get model stats for cost calculation |
| 139 | const modelStats = getModelStats(metadataModelOverride ?? model); |
| 140 | |
| 141 | const costsIncluded = |
| 142 | (providerMetadata?.mux as { costsIncluded?: boolean } | undefined)?.costsIncluded === true; |
| 143 | |
| 144 | // Calculate costs based on model stats (undefined if model unknown) |
| 145 | let inputCost: number | undefined; |
| 146 | let cachedCost: number | undefined; |
| 147 | let cacheCreateCost: number | undefined; |
| 148 | let outputCost: number | undefined; |
| 149 | let reasoningCost: number | undefined; |
| 150 | |
| 151 | if (modelStats) { |
| 152 | const costs = calculateUsageCosts(modelStats, { |
| 153 | inputTokens, |
| 154 | cachedTokens, |
| 155 | cacheCreateTokens, |
| 156 | outputTokens: outputWithoutReasoning, |
| 157 | reasoningTokens, |
| 158 | }); |
| 159 | inputCost = costs.inputCost; |
no test coverage detected