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;
}