diff --git a/open-sse/executors/bedrock.ts b/open-sse/executors/bedrock.ts index 740e3e8bd5..875d16cc92 100644 --- a/open-sse/executors/bedrock.ts +++ b/open-sse/executors/bedrock.ts @@ -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), }; } diff --git a/open-sse/handlers/responseSanitizer.ts b/open-sse/handlers/responseSanitizer.ts index da4003df47..b1f98b01c2 100644 --- a/open-sse/handlers/responseSanitizer.ts +++ b/open-sse/handlers/responseSanitizer.ts @@ -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).cached_tokens) + ) { + const details = (sanitized.prompt_tokens_details as Record) ?? {}; + 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 || {}), + 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) { diff --git a/open-sse/handlers/responseTranslator.ts b/open-sse/handlers/responseTranslator.ts index 92fb47b7b6..79af33bf6c 100644 --- a/open-sse/handlers/responseTranslator.ts +++ b/open-sse/handlers/responseTranslator.ts @@ -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 = {}; + if (cacheRead > 0) details.cached_tokens = cacheRead; + if (cacheCreation > 0) details.cache_creation_tokens = cacheCreation; + (result.usage as Record).prompt_tokens_details = details; + } } intermediateOpenAI = result; diff --git a/open-sse/handlers/usageExtractor.ts b/open-sse/handlers/usageExtractor.ts index 114ccefa50..aab0233223 100644 --- a/open-sse/handlers/usageExtractor.ts +++ b/open-sse/handlers/usageExtractor.ts @@ -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 ?? diff --git a/open-sse/utils/stream.ts b/open-sse/utils/stream.ts index c28fde029d..f693428843 100644 --- a/open-sse/utils/stream.ts +++ b/open-sse/utils/stream.ts @@ -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 | undefined; + if (u) { + const ct = u.cached_tokens ?? u.cache_read_input_tokens; + if (ct !== undefined) { + const details = (u.prompt_tokens_details as Record) ?? {}; + 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 | undefined; + if (u) { + const ct = u.cached_tokens ?? u.cache_read_input_tokens; + if (ct !== undefined) { + const d = (u.prompt_tokens_details as Record) ?? {}; + 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) { diff --git a/open-sse/utils/usageTracking.ts b/open-sse/utils/usageTracking.ts index a2f931b65f..f9759d9b81 100644 --- a/open-sse/utils/usageTracking.ts +++ b/open-sse/utils/usageTracking.ts @@ -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, }); }