Spaces:
Paused
Paused
| type JsonRecord = Record<string, unknown>; | |
| function asRecord(value: unknown): JsonRecord { | |
| return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {}; | |
| } | |
| function toFiniteNumber(value: unknown): number { | |
| if (typeof value === "number" && Number.isFinite(value)) return value; | |
| if (typeof value === "string" && value.trim().length > 0) { | |
| const parsed = Number(value); | |
| return Number.isFinite(parsed) ? parsed : 0; | |
| } | |
| return 0; | |
| } | |
| function getPromptTokenDetails(tokens: unknown): JsonRecord { | |
| const tokenRecord = asRecord(tokens); | |
| const promptDetails = asRecord(tokenRecord.prompt_tokens_details); | |
| if (Object.keys(promptDetails).length > 0) return promptDetails; | |
| return asRecord(tokenRecord.input_tokens_details); | |
| } | |
| export function getPromptCacheReadTokens(tokens: unknown): number { | |
| const tokenRecord = asRecord(tokens); | |
| const promptDetails = getPromptTokenDetails(tokenRecord); | |
| return toFiniteNumber( | |
| tokenRecord.cacheRead ?? | |
| tokenRecord.cache_read_input_tokens ?? | |
| tokenRecord.cached_tokens ?? | |
| promptDetails.cached_tokens | |
| ); | |
| } | |
| export function getPromptCacheCreationTokens(tokens: unknown): number { | |
| const tokenRecord = asRecord(tokens); | |
| const promptDetails = getPromptTokenDetails(tokenRecord); | |
| return toFiniteNumber( | |
| tokenRecord.cacheCreation ?? | |
| tokenRecord.cache_creation_input_tokens ?? | |
| promptDetails.cache_creation_tokens | |
| ); | |
| } | |
| export function getLoggedInputTokens(tokens: unknown): number { | |
| const tokenRecord = asRecord(tokens); | |
| if (tokenRecord.input !== undefined && tokenRecord.input !== null) { | |
| return toFiniteNumber(tokenRecord.input); | |
| } | |
| if (tokenRecord.input_tokens !== undefined && tokenRecord.input_tokens !== null) { | |
| // Anthropic / anthropic-compatible-cc streaming: input_tokens is only the | |
| // non-cached portion. The cache counters sit as separate top-level fields | |
| // (cache_read_input_tokens, cache_creation_input_tokens). We need to add | |
| // them to get the true total input. | |
| return ( | |
| toFiniteNumber(tokenRecord.input_tokens) + | |
| toFiniteNumber(tokenRecord.cache_read_input_tokens) + | |
| toFiniteNumber(tokenRecord.cache_creation_input_tokens) | |
| ); | |
| } | |
| // prompt_tokens from translator/extractor already includes cache tokens: | |
| // - OpenAI format: prompt_tokens inherently includes cached | |
| // - Claude non-streaming: extractUsageFromResponse sums input + cache_read + cache_creation | |
| // - Claude streaming: extractUsage (after fix) sums input + cache_read + cache_creation | |
| // Do NOT add cache fields here — would double-count. | |
| const promptTokens = toFiniteNumber(tokenRecord.prompt_tokens); | |
| return promptTokens; | |
| } | |
| export function getLoggedOutputTokens(tokens: unknown): number { | |
| const tokenRecord = asRecord(tokens); | |
| if (tokenRecord.output !== undefined && tokenRecord.output !== null) { | |
| return toFiniteNumber(tokenRecord.output); | |
| } | |
| return toFiniteNumber(tokenRecord.completion_tokens ?? tokenRecord.output_tokens); | |
| } | |
| /** | |
| * Return the reasoning/thinking output token count. | |
| * Checks multiple field locations used by different providers: | |
| * - completion_tokens_details.reasoning_tokens (OpenAI, OpenRouter) | |
| * - reasoning_tokens (GitHub — top-level) | |
| * - reasoning (usage_history DB format) | |
| */ | |
| export function getReasoningTokens(tokens: unknown): number { | |
| const tokenRecord = asRecord(tokens); | |
| const completionDetails = asRecord(tokenRecord.completion_tokens_details); | |
| return toFiniteNumber( | |
| tokenRecord.reasoning ?? tokenRecord.reasoning_tokens ?? completionDetails.reasoning_tokens | |
| ); | |
| } | |
| // ─── Nullable variants ────────────────────────────────────────────────── | |
| // Return `null` when the provider simply doesn't report the field, | |
| // vs `0` when the provider explicitly reported zero. | |
| function hasAnyKey(record: JsonRecord, keys: string[]): boolean { | |
| return keys.some((k) => record[k] !== undefined && record[k] !== null); | |
| } | |
| /** | |
| * Return prompt cache-read tokens, or `null` if the provider didn't | |
| * report any cache-read field at all. | |
| */ | |
| export function getPromptCacheReadTokensOrNull(tokens: unknown): number | null { | |
| const tokenRecord = asRecord(tokens); | |
| const promptDetails = getPromptTokenDetails(tokenRecord); | |
| if ( | |
| hasAnyKey(tokenRecord, ["cacheRead", "cache_read_input_tokens", "cached_tokens"]) || | |
| hasAnyKey(promptDetails, ["cached_tokens"]) | |
| ) { | |
| return getPromptCacheReadTokens(tokens); | |
| } | |
| return null; | |
| } | |
| /** | |
| * Return prompt cache-creation (write) tokens, or `null` if the | |
| * provider didn't report any cache-creation field at all. | |
| */ | |
| export function getPromptCacheCreationTokensOrNull(tokens: unknown): number | null { | |
| const tokenRecord = asRecord(tokens); | |
| const promptDetails = getPromptTokenDetails(tokenRecord); | |
| if ( | |
| hasAnyKey(tokenRecord, ["cacheCreation", "cache_creation_input_tokens"]) || | |
| hasAnyKey(promptDetails, ["cache_creation_tokens"]) | |
| ) { | |
| return getPromptCacheCreationTokens(tokens); | |
| } | |
| return null; | |
| } | |
| /** | |
| * Return reasoning tokens, or `null` if the provider didn't report | |
| * any reasoning field at all. | |
| */ | |
| export function getReasoningTokensOrNull(tokens: unknown): number | null { | |
| const tokenRecord = asRecord(tokens); | |
| const completionDetails = asRecord(tokenRecord.completion_tokens_details); | |
| if ( | |
| hasAnyKey(tokenRecord, ["reasoning", "reasoning_tokens"]) || | |
| hasAnyKey(completionDetails, ["reasoning_tokens"]) | |
| ) { | |
| return getReasoningTokens(tokens); | |
| } | |
| return null; | |
| } | |
| export function formatUsageLog(tokens: unknown): string { | |
| const input = getLoggedInputTokens(tokens); | |
| const output = getLoggedOutputTokens(tokens); | |
| const cacheRead = getPromptCacheReadTokens(tokens); | |
| const cacheWrite = getPromptCacheCreationTokens(tokens); | |
| const reasoning = getReasoningTokens(tokens); | |
| let msg = `in=${input} | out=${output}`; | |
| if (cacheRead > 0) { | |
| msg += ` | CR=${cacheRead}`; | |
| } | |
| if (cacheWrite > 0) { | |
| msg += ` | CW=${cacheWrite}`; | |
| } | |
| if (reasoning > 0) { | |
| msg += ` | R=${reasoning}`; | |
| } | |
| return msg; | |
| } | |