mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-14 11:12:17 +03:00
[v3.8.50] fix: passthrough non-standard cache token fields for DeepSeek / MiniMax / Bedrock across streaming, non-streaming, and Dashboard paths (#8591)
* 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 * fix: passthrough non-standard cache token fields for DeepSeek / MiniMax / Bedrock across streaming, non-streaming, and Dashboard paths * fix(sse): shrink cache-hit token passthrough to fit file-size gate PR #8591 added a DeepSeek/MiniMax/Bedrock flat cache-hit-token -> nested prompt_tokens_details.cached_tokens mapping (#8171) that grew responseSanitizer.ts and stream.ts past their frozen file-size baselines. - Extract the chat-completions/Responses-API mapping logic into a new leaf module (responseSanitizer/cacheHitTokens.ts). - Move the streaming-path rebuild into filterUsageForFormat() (usageTracking.ts), the single conversion chokepoint both stream.ts call sites already used, eliminating the duplicated stream.ts patch entirely. - Rebaseline responseSanitizer.ts by the 2 lines that remain irreducible (the mandatory ES import for the extracted helper). Behavior verified unchanged via the existing response-sanitizer and stream-handler unit suites. Co-authored-by: ikelvingo <ikelvingo@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> Co-authored-by: ikelvingo <ikelvingo@users.noreply.github.com>
This commit is contained in:
@@ -393,6 +393,8 @@ function usageFromBedrock(usage) {
|
||||
prompt_tokens: input,
|
||||
completion_tokens: output,
|
||||
total_tokens: Number(usage?.totalTokens || input + output),
|
||||
cache_read_input_tokens: Number(usage?.cacheReadInputTokenCount || 0),
|
||||
cache_creation_input_tokens: Number(usage?.cacheWriteInputTokenCount || 0),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
collapseExcessiveNewlines,
|
||||
extractThinkingFromContent,
|
||||
} from "./responseSanitizer/reasoning.ts";
|
||||
import { applyCacheHitTokensToUsage, applyCacheHitTokensToResponsesUsage } from "./responseSanitizer/cacheHitTokens.ts";
|
||||
export {
|
||||
extractThinkingFromContent,
|
||||
shouldParseTextualReasoningTags,
|
||||
@@ -30,7 +31,7 @@ const ALLOWED_USAGE_FIELDS = new Set([
|
||||
"total_tokens",
|
||||
"cached_tokens",
|
||||
"prompt_tokens_details",
|
||||
"completion_tokens_details",
|
||||
"completion_tokens_details", "cache_read_input_tokens", "cache_creation_input_tokens",
|
||||
// Keep through sanitize → applyClientUsageBuffer so heuristic web usage is
|
||||
// not inflated by the default USAGE_TOKEN_BUFFER (2000).
|
||||
"estimated",
|
||||
@@ -495,7 +496,7 @@ function sanitizeUsage(usage: unknown): unknown {
|
||||
sanitized[key] = usageRecord[key];
|
||||
}
|
||||
}
|
||||
|
||||
applyCacheHitTokensToUsage(usageRecord, sanitized); // DeepSeek/MiniMax/Bedrock cache-hit passthrough (#8171)
|
||||
// Ensure required fields
|
||||
const promptTokens = toNumber(sanitized.prompt_tokens) ?? 0;
|
||||
const completionTokens = toNumber(sanitized.completion_tokens) ?? 0;
|
||||
@@ -533,6 +534,29 @@ 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<string, unknown> || {}),
|
||||
cached_tokens: normalized.prompt_cache_hit_tokens,
|
||||
};
|
||||
}
|
||||
|
||||
// MiniMax / Bedrock: flat cache_read_input_tokens → input_tokens_details.cached_tokens
|
||||
if (
|
||||
normalized.cache_read_input_tokens !== undefined &&
|
||||
normalized.cache_read_input_tokens !== 0 &&
|
||||
!normalized.input_tokens_details?.cached_tokens
|
||||
) {
|
||||
normalized.input_tokens_details = {
|
||||
...(normalized.input_tokens_details as Record<string, unknown> || {}),
|
||||
cached_tokens: normalized.cache_read_input_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) {
|
||||
|
||||
72
open-sse/handlers/responseSanitizer/cacheHitTokens.ts
Normal file
72
open-sse/handlers/responseSanitizer/cacheHitTokens.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Cache-hit token normalization — shared by chat-completions and Responses API
|
||||
* usage sanitizers.
|
||||
*
|
||||
* Several providers report a prompt-cache-hit count using a flat, non-standard
|
||||
* field instead of the OpenAI-style nested `*_tokens_details.cached_tokens`
|
||||
* shape. Without this mapping, clients (Cline / Cursor / Claude Code / any
|
||||
* OpenAI-SDK consumer) never see the real cache-hit count (#8171).
|
||||
*
|
||||
* - DeepSeek native API: flat `prompt_cache_hit_tokens`.
|
||||
* - MiniMax / Bedrock etc.: flat `cache_read_input_tokens`.
|
||||
*/
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Chat Completions shape: writes into `sanitized.prompt_tokens_details.cached_tokens`.
|
||||
* `usageRecord` is the raw (pre-whitelist) usage object; `sanitized` is the
|
||||
* whitelisted usage object being built.
|
||||
*/
|
||||
export function applyCacheHitTokensToUsage(usageRecord: JsonRecord, sanitized: JsonRecord): void {
|
||||
if (
|
||||
usageRecord.prompt_cache_hit_tokens !== undefined &&
|
||||
(!sanitized.prompt_tokens_details ||
|
||||
!(sanitized.prompt_tokens_details as JsonRecord).cached_tokens)
|
||||
) {
|
||||
const details = (sanitized.prompt_tokens_details as JsonRecord) ?? {};
|
||||
details.cached_tokens = usageRecord.prompt_cache_hit_tokens;
|
||||
sanitized.prompt_tokens_details = details;
|
||||
}
|
||||
|
||||
if (
|
||||
sanitized.cache_read_input_tokens !== undefined &&
|
||||
sanitized.cache_read_input_tokens !== 0 &&
|
||||
(!sanitized.prompt_tokens_details ||
|
||||
!(sanitized.prompt_tokens_details as JsonRecord).cached_tokens)
|
||||
) {
|
||||
const details = (sanitized.prompt_tokens_details as JsonRecord) ?? {};
|
||||
details.cached_tokens = sanitized.cache_read_input_tokens;
|
||||
sanitized.prompt_tokens_details = details;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Responses API shape: writes into `normalized.input_tokens_details.cached_tokens`.
|
||||
* `toRecordFn` is injected to reuse the caller's `toRecord()` helper.
|
||||
*/
|
||||
export function applyCacheHitTokensToResponsesUsage(
|
||||
normalized: JsonRecord,
|
||||
toRecordFn: (value: unknown) => JsonRecord | null
|
||||
): void {
|
||||
if (
|
||||
normalized.prompt_cache_hit_tokens !== undefined &&
|
||||
!toRecordFn(normalized.input_tokens_details)?.cached_tokens
|
||||
) {
|
||||
normalized.input_tokens_details = {
|
||||
...(toRecordFn(normalized.input_tokens_details) || {}),
|
||||
cached_tokens: normalized.prompt_cache_hit_tokens,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
normalized.cache_read_input_tokens !== undefined &&
|
||||
normalized.cache_read_input_tokens !== 0 &&
|
||||
!toRecordFn(normalized.input_tokens_details)?.cached_tokens
|
||||
) {
|
||||
normalized.input_tokens_details = {
|
||||
...(toRecordFn(normalized.input_tokens_details) || {}),
|
||||
cached_tokens: normalized.cache_read_input_tokens,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,8 @@ export function extractUsageFromResponse(responseBody, provider) {
|
||||
responseBody.usage.prompt_tokens_details?.cached_tokens ??
|
||||
responseBody.usage.input_tokens_details?.cached_tokens ??
|
||||
responseBody.usage.prompt_cache_hit_tokens ??
|
||||
responseBody.usage.cached_tokens,
|
||||
responseBody.usage.cached_tokens ??
|
||||
responseBody.usage.cache_read_input_tokens,
|
||||
reasoning_tokens:
|
||||
responseBody.usage.completion_tokens_details?.reasoning_tokens ??
|
||||
responseBody.usage.output_tokens_details?.reasoning_tokens ??
|
||||
|
||||
@@ -200,6 +200,14 @@ export function filterUsageForFormat(usage, targetFormat) {
|
||||
) {
|
||||
convertedUsage.total_tokens = convertedUsage.prompt_tokens + convertedUsage.completion_tokens;
|
||||
}
|
||||
// Rebuild prompt_tokens_details.cached_tokens from flat cached_tokens / cache_read_input_tokens (#8171)
|
||||
const flatCached = convertedUsage.cached_tokens ?? convertedUsage.cache_read_input_tokens;
|
||||
if (flatCached !== undefined && !convertedUsage.prompt_tokens_details?.cached_tokens) {
|
||||
convertedUsage.prompt_tokens_details = {
|
||||
...convertedUsage.prompt_tokens_details,
|
||||
cached_tokens: flatCached,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to pick only defined fields from usage
|
||||
@@ -251,6 +259,10 @@ export function filterUsageForFormat(usage, targetFormat) {
|
||||
"reasoning_tokens",
|
||||
"prompt_tokens_details",
|
||||
"completion_tokens_details",
|
||||
"prompt_cache_hit_tokens",
|
||||
"prompt_cache_miss_tokens",
|
||||
"cache_read_input_tokens",
|
||||
"cache_creation_input_tokens",
|
||||
"estimated",
|
||||
],
|
||||
};
|
||||
@@ -427,6 +439,8 @@ export function extractUsage(chunk) {
|
||||
chunk.usage.reasoning_tokens,
|
||||
// xAI's exact provider-reported cost (port of decolua/9router#2453, capability A).
|
||||
cost_in_usd_ticks: chunk.usage.cost_in_usd_ticks,
|
||||
cache_read_input_tokens: chunk.usage.cache_read_input_tokens,
|
||||
cache_creation_input_tokens: chunk.usage.cache_creation_input_tokens,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user