diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index 03e749c113..c60b379a0e 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -32,7 +32,11 @@ import { appendRequestLog, saveCallLog, } from "@/lib/usageDb"; -import { getLoggedInputTokens, getLoggedOutputTokens } from "@/lib/usage/tokenAccounting"; +import { + getLoggedInputTokens, + getLoggedOutputTokens, + formatUsageLog, +} from "@/lib/usage/tokenAccounting"; import { recordCost } from "@/domain/costRules"; import { calculateCost } from "@/lib/usage/costCalculator"; import { CLAUDE_OAUTH_TOOL_PREFIX } from "../translator/request/openai-to-claude.ts"; @@ -1432,7 +1436,7 @@ export async function handleChatCore({ // Save structured call log with full payloads const cacheUsageLogMeta = buildCacheUsageLogMeta(usage); if (usage && typeof usage === "object") { - const msg = `[${new Date().toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit" })}] 📊 [USAGE] ${provider.toUpperCase()} | in=${getLoggedInputTokens(usage)} | out=${getLoggedOutputTokens(usage)}${connectionId ? ` | account=${connectionId.slice(0, 8)}...` : ""}`; + const msg = `[${new Date().toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit" })}] 📊 [USAGE] ${provider.toUpperCase()} | ${formatUsageLog(usage)}${connectionId ? ` | account=${connectionId.slice(0, 8)}...` : ""}`; console.log(`${COLORS.green}${msg}${COLORS.reset}`); // Track cache token metrics diff --git a/open-sse/handlers/usageExtractor.ts b/open-sse/handlers/usageExtractor.ts index 1a9910eded..eed7c43cdb 100644 --- a/open-sse/handlers/usageExtractor.ts +++ b/open-sse/handlers/usageExtractor.ts @@ -46,11 +46,18 @@ export function extractUsageFromResponse(responseBody, provider) { (responseBody.usage.input_tokens !== undefined || responseBody.usage.output_tokens !== undefined) ) { + const inputTokens = responseBody.usage.input_tokens || 0; + const cacheRead = responseBody.usage.cache_read_input_tokens || 0; + const cacheCreation = responseBody.usage.cache_creation_input_tokens || 0; + + // Total prompt tokens = input + cache_read + cache_creation (per Claude API docs) + const promptTokens = inputTokens + cacheRead + cacheCreation; + return { - prompt_tokens: responseBody.usage.input_tokens || 0, + prompt_tokens: promptTokens, completion_tokens: responseBody.usage.output_tokens || 0, - cache_read_input_tokens: responseBody.usage.cache_read_input_tokens, - cache_creation_input_tokens: responseBody.usage.cache_creation_input_tokens, + cache_read_input_tokens: cacheRead, + cache_creation_input_tokens: cacheCreation, }; } diff --git a/src/app/(dashboard)/dashboard/cache/page.tsx b/src/app/(dashboard)/dashboard/cache/page.tsx index eaa51e4ad6..04434d0595 100644 --- a/src/app/(dashboard)/dashboard/cache/page.tsx +++ b/src/app/(dashboard)/dashboard/cache/page.tsx @@ -4,6 +4,7 @@ import { useState, useEffect, useCallback } from "react"; import { Card, Button, EmptyState } from "@/shared/components"; import { useNotificationStore } from "@/store/notificationStore"; import { useTranslations } from "next-intl"; +import CacheStatsCard from "../settings/components/CacheStatsCard"; // ─── Types ─────────────────────────────────────────────────────────────────── @@ -286,9 +287,9 @@ export default function CachePage() { {t("behaviorDeterministic")} {t.rich("behaviorBypass", { - header: () => ( + header: (chunks) => ( - X-OmniRoute-No-Cache: true + {chunks} ), })} @@ -296,9 +297,9 @@ export default function CachePage() { {t("behaviorTwoTier")} {t.rich("behaviorTtl", { - envVar: () => ( + envVar: (chunks) => ( - SEMANTIC_CACHE_TTL_MS + {chunks} ), })} @@ -333,6 +334,9 @@ export default function CachePage() { + + {/* Claude Prompt Cache Metrics */} + )} diff --git a/src/app/(dashboard)/dashboard/settings/page.tsx b/src/app/(dashboard)/dashboard/settings/page.tsx index 7aa69f112c..445964dc08 100644 --- a/src/app/(dashboard)/dashboard/settings/page.tsx +++ b/src/app/(dashboard)/dashboard/settings/page.tsx @@ -16,8 +16,6 @@ import CodexServiceTierTab from "./components/CodexServiceTierTab"; import SystemPromptTab from "./components/SystemPromptTab"; import ModelAliasesTab from "./components/ModelAliasesTab"; import BackgroundDegradationTab from "./components/BackgroundDegradationTab"; - -import CacheStatsCard from "./components/CacheStatsCard"; import ResilienceTab from "./components/ResilienceTab"; const tabs = [ @@ -88,7 +86,6 @@ export default function SettingsPage() { - )} diff --git a/src/lib/db/migrations/012_fix_token_input_cache_tokens.sql b/src/lib/db/migrations/012_fix_token_input_cache_tokens.sql new file mode 100644 index 0000000000..20a6877517 --- /dev/null +++ b/src/lib/db/migrations/012_fix_token_input_cache_tokens.sql @@ -0,0 +1,15 @@ +-- Migration 012: Fix tokens_input to include cache tokens +-- +-- Problem: Historical data stored tokens_input as just the base input_tokens +-- from the API, not including cache_read and cache_creation tokens. +-- +-- Per Claude API docs: +-- Total input tokens = input_tokens + cache_creation_input_tokens + cache_read_input_tokens +-- +-- This migration corrects historical records by adding cache tokens to tokens_input. +-- Only affects records where cache tokens exist. + +-- Update tokens_input to include cache tokens +UPDATE usage_history +SET tokens_input = tokens_input + tokens_cache_read + tokens_cache_creation +WHERE tokens_cache_read > 0 OR tokens_cache_creation > 0; \ No newline at end of file diff --git a/src/lib/usage/tokenAccounting.ts b/src/lib/usage/tokenAccounting.ts index 1326c86363..bb4da25f3c 100644 --- a/src/lib/usage/tokenAccounting.ts +++ b/src/lib/usage/tokenAccounting.ts @@ -52,19 +52,9 @@ export function getLoggedInputTokens(tokens: unknown): number { return toFiniteNumber(tokenRecord.input_tokens); } + // prompt_tokens from translator already includes input + cache_read + cache_creation + // Do NOT subtract cached tokens - we want the total billable prompt tokens const promptTokens = toFiniteNumber(tokenRecord.prompt_tokens); - if (promptTokens <= 0) return 0; - - const promptDetails = getPromptTokenDetails(tokenRecord); - const cachedFromDetails = toFiniteNumber(promptDetails.cached_tokens); - if (cachedFromDetails > 0) { - return Math.max(promptTokens - cachedFromDetails, 0); - } - - if ("cached_tokens" in tokenRecord && !("cache_read_input_tokens" in tokenRecord)) { - return Math.max(promptTokens - toFiniteNumber(tokenRecord.cached_tokens), 0); - } - return promptTokens; } @@ -73,7 +63,17 @@ export function getLoggedOutputTokens(tokens: unknown): number { if (tokenRecord.output !== undefined && tokenRecord.output !== null) { return toFiniteNumber(tokenRecord.output); } - return toFiniteNumber( - tokenRecord.completion_tokens ?? tokenRecord.output_tokens - ); + return toFiniteNumber(tokenRecord.completion_tokens ?? tokenRecord.output_tokens); +} + +export function formatUsageLog(tokens: unknown): string { + const input = getLoggedInputTokens(tokens); + const output = getLoggedOutputTokens(tokens); + const cacheRead = getPromptCacheReadTokens(tokens); + + let msg = `in=${input} | out=${output}`; + if (cacheRead > 0) { + msg += ` | CR=${cacheRead}`; + } + return msg; }