mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-04 14:22:09 +03:00
fix: passthrough non-standard cache token fields for DeepSeek / MiniMax / Bedrock across streaming, non-streaming, and Dashboard paths
This commit is contained in:
@@ -389,6 +389,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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,8 @@ const ALLOWED_USAGE_FIELDS = new Set([
|
||||
"cached_tokens",
|
||||
"prompt_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",
|
||||
@@ -496,6 +498,18 @@ function sanitizeUsage(usage: unknown): unknown {
|
||||
sanitized.prompt_tokens_details = details;
|
||||
}
|
||||
|
||||
// MiniMax / Bedrock etc.: flat cache_read_input_tokens → nested prompt_tokens_details
|
||||
if (
|
||||
sanitized.cache_read_input_tokens !== undefined &&
|
||||
sanitized.cache_read_input_tokens !== 0 &&
|
||||
(!sanitized.prompt_tokens_details ||
|
||||
!(sanitized.prompt_tokens_details as Record<string, unknown>).cached_tokens)
|
||||
) {
|
||||
const details = (sanitized.prompt_tokens_details as Record<string, unknown>) ?? {};
|
||||
details.cached_tokens = sanitized.cache_read_input_tokens;
|
||||
sanitized.prompt_tokens_details = details;
|
||||
}
|
||||
|
||||
// Ensure required fields
|
||||
const promptTokens = toNumber(sanitized.prompt_tokens) ?? 0;
|
||||
const completionTokens = toNumber(sanitized.completion_tokens) ?? 0;
|
||||
@@ -544,6 +558,18 @@ function sanitizeResponsesUsage(usage: unknown): unknown {
|
||||
};
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
||||
@@ -546,6 +546,15 @@ export function translateNonStreamingResponse(
|
||||
completion_tokens: completionTokens,
|
||||
total_tokens: promptTokens + completionTokens,
|
||||
};
|
||||
// Non-streaming Claude→OpenAI translation: preserve cache fields
|
||||
const cacheRead = toNumber(usage.cache_read_input_tokens, 0);
|
||||
const cacheCreation = toNumber(usage.cache_creation_input_tokens, 0);
|
||||
if (cacheRead > 0 || cacheCreation > 0) {
|
||||
const details: Record<string, unknown> = {};
|
||||
if (cacheRead > 0) details.cached_tokens = cacheRead;
|
||||
if (cacheCreation > 0) details.cache_creation_tokens = cacheCreation;
|
||||
(result.usage as Record<string, unknown>).prompt_tokens_details = details;
|
||||
}
|
||||
}
|
||||
|
||||
intermediateOpenAI = result;
|
||||
|
||||
@@ -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 ??
|
||||
|
||||
@@ -1019,6 +1019,24 @@ export function createSSEStream(options: StreamOptions = {}) {
|
||||
itemSanitized.usage = filterUsageForFormat(buffered, sourceFormat);
|
||||
}
|
||||
|
||||
// Streaming path patch: extractUsage has already consumed prompt_cache_hit_tokens
|
||||
// into flat cached_tokens, but the standard OpenAI nested field prompt_tokens_details
|
||||
// was lost from state.usage. Rebuild the nested structure from the flat cached_tokens
|
||||
// so agent clients (Cline / Cursor / Claude Code etc.) see cache hit counts.
|
||||
if (sourceFormat === FORMATS.OPENAI) {
|
||||
const u = itemSanitized?.usage as Record<string, unknown> | undefined;
|
||||
if (u) {
|
||||
const ct = u.cached_tokens ?? u.cache_read_input_tokens;
|
||||
if (ct !== undefined) {
|
||||
const details = (u.prompt_tokens_details as Record<string, unknown>) ?? {};
|
||||
if (details.cached_tokens === undefined) {
|
||||
details.cached_tokens = ct;
|
||||
u.prompt_tokens_details = details;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
sourceFormat === FORMATS.CLAUDE &&
|
||||
shouldInjectClaudeEmptyResponseBeforeCurrentEvent(claudeEmptyResponseLifecycle, itemSanitized)
|
||||
@@ -1896,6 +1914,20 @@ export function createSSEStream(options: StreamOptions = {}) {
|
||||
} else if (isFinishChunk && usage) {
|
||||
const buffered = addBufferToUsage(usage);
|
||||
parsed.usage = filterUsageForFormat(buffered, FORMATS.OPENAI);
|
||||
|
||||
// Rebuild prompt_tokens_details.cached_tokens from flat cached_tokens / cache_read_input_tokens
|
||||
const u = parsed?.usage as Record<string, unknown> | undefined;
|
||||
if (u) {
|
||||
const ct = u.cached_tokens ?? u.cache_read_input_tokens;
|
||||
if (ct !== undefined) {
|
||||
const d = (u.prompt_tokens_details as Record<string, unknown>) ?? {};
|
||||
if (d.cached_tokens === undefined) {
|
||||
d.cached_tokens = ct;
|
||||
u.prompt_tokens_details = d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output = `data: ${JSON.stringify(parsed)}\n\n`;
|
||||
injectedUsage = true;
|
||||
} else if (textualToolCallConverted) {
|
||||
|
||||
@@ -237,6 +237,8 @@ export function filterUsageForFormat(usage, targetFormat) {
|
||||
"completion_tokens_details",
|
||||
"prompt_cache_hit_tokens",
|
||||
"prompt_cache_miss_tokens",
|
||||
"cache_read_input_tokens",
|
||||
"cache_creation_input_tokens",
|
||||
"estimated",
|
||||
],
|
||||
};
|
||||
@@ -401,13 +403,16 @@ export function extractUsage(chunk) {
|
||||
chunk.usage.prompt_tokens_details?.cached_tokens ??
|
||||
chunk.usage.input_tokens_details?.cached_tokens ??
|
||||
chunk.usage.prompt_cache_hit_tokens ??
|
||||
chunk.usage.cached_tokens,
|
||||
chunk.usage.cached_tokens ??
|
||||
chunk.usage.cache_read_input_tokens,
|
||||
reasoning_tokens:
|
||||
chunk.usage.completion_tokens_details?.reasoning_tokens ??
|
||||
chunk.usage.output_tokens_details?.reasoning_tokens ??
|
||||
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