From 595c0dba04eda041906401bfd72841a8fd14ba3a Mon Sep 17 00:00:00 2001 From: ikelvingo Date: Sat, 25 Jul 2026 14:57:10 +0800 Subject: [PATCH] fix(#8171): map DeepSeek prompt_cache_hit_tokens into prompt_tokens_details.cached_tokens DeepSeek native API returns cache stats in flat top-level fields (prompt_cache_hit_tokens / prompt_cache_miss_tokens) instead of the standard prompt_tokens_details.cached_tokens. The usage sanitizer (sanitizeUsage / sanitizeResponsesUsage) was stripping these non-standard fields, so clients never received real cache hit counts even when the upstream served cached responses. Changes: - sanitizeUsage(): map prompt_cache_hit_tokens into prompt_tokens_details.cached_tokens when the latter is unset - sanitizeResponsesUsage(): same mapping for input_tokens_details - filterUsageForFormat(): add prompt_cache_hit_tokens and prompt_cache_miss_tokens to the default format allow list so they survive field-level filtering --- open-sse/handlers/responseSanitizer.ts | 24 ++++++++++++++++++++++++ open-sse/utils/usageTracking.ts | 2 ++ 2 files changed, 26 insertions(+) diff --git a/open-sse/handlers/responseSanitizer.ts b/open-sse/handlers/responseSanitizer.ts index 0e2ebd880c..da4003df47 100644 --- a/open-sse/handlers/responseSanitizer.ts +++ b/open-sse/handlers/responseSanitizer.ts @@ -483,6 +483,19 @@ function sanitizeUsage(usage: unknown): unknown { } } + // DeepSeek native API uses flat prompt_cache_hit_tokens (NOT + // prompt_tokens_details.cached_tokens). Map it into prompt_tokens_details + // so clients see the real cache hit count (#8171). + if ( + usageRecord.prompt_cache_hit_tokens !== undefined && + (!sanitized.prompt_tokens_details || + !(sanitized.prompt_tokens_details as Record).cached_tokens) + ) { + const details = (sanitized.prompt_tokens_details as Record) ?? {}; + details.cached_tokens = usageRecord.prompt_cache_hit_tokens; + sanitized.prompt_tokens_details = details; + } + // Ensure required fields const promptTokens = toNumber(sanitized.prompt_tokens) ?? 0; const completionTokens = toNumber(sanitized.completion_tokens) ?? 0; @@ -520,6 +533,17 @@ function sanitizeResponsesUsage(usage: unknown): unknown { normalized.output_tokens_details = normalized.completion_tokens_details; } + // DeepSeek native API: map flat prompt_cache_hit_tokens into input_tokens_details + if ( + normalized.prompt_cache_hit_tokens !== undefined && + !normalized.input_tokens_details?.cached_tokens + ) { + normalized.input_tokens_details = { + ...(normalized.input_tokens_details as Record || {}), + cached_tokens: normalized.prompt_cache_hit_tokens, + }; + } + const inputDetails = toRecord(normalized.input_tokens_details) || {}; const cachedTokens = normalized.cached_tokens ?? normalized.cache_read_input_tokens; if (cachedTokens !== undefined && inputDetails.cached_tokens === undefined) { diff --git a/open-sse/utils/usageTracking.ts b/open-sse/utils/usageTracking.ts index caef2c8f1b..a2f931b65f 100644 --- a/open-sse/utils/usageTracking.ts +++ b/open-sse/utils/usageTracking.ts @@ -235,6 +235,8 @@ export function filterUsageForFormat(usage, targetFormat) { "reasoning_tokens", "prompt_tokens_details", "completion_tokens_details", + "prompt_cache_hit_tokens", + "prompt_cache_miss_tokens", "estimated", ], };