From 248d7da01c44180dcac466631e24a17b585b95b0 Mon Sep 17 00:00:00 2001 From: LLL <2798142644@qq.com> Date: Thu, 10 Sep 2026 22:52:29 +0700 Subject: [PATCH] revert(qoder): drop the Responses usage plumbing from shared code The merged Qoder work also rewrote shared translator/handler code so that /v1/responses clients got token usage on response.completed. That changed behaviour for every provider, not just Qoder: proxies saw input tokens rise by the 2000-token context buffer, and the plain token mapping was replaced by one that always adds input_tokens_details. A probe confirms the Qoder benefit does not depend on those edits: the executor's coalescer already emits one include_usage-style finish chunk, so a Claude client receives input_tokens and cache_read_input_tokens with every shared file at its original state. Only the Responses path relies on the shared translator, and that path has no Qoder-owned seam to put it in. Reverts the shared files to their pre-PR state and drops the Responses usage test. The Cline envelope unwrap in nonStreamingHandler.js, which landed after the PR in the same file, is kept. --- .../handlers/chatCore/nonStreamingHandler.js | 8 +- .../handlers/chatCore/sseToJsonHandler.js | 8 +- open-sse/transformer/responsesTransformer.js | 27 ++- open-sse/translator/concerns/usage.js | 31 --- .../translator/response/openai-responses.js | 34 ++-- .../translator/response/openai-to-claude.js | 81 ++++---- open-sse/utils/stream.js | 17 +- tests/unit/openai-responses-usage.test.js | 182 ------------------ tests/unit/openai-to-claude.test.js | 27 --- 9 files changed, 74 insertions(+), 341 deletions(-) delete mode 100644 tests/unit/openai-responses-usage.test.js diff --git a/open-sse/handlers/chatCore/nonStreamingHandler.js b/open-sse/handlers/chatCore/nonStreamingHandler.js index fe94b282..d7f4f3fe 100644 --- a/open-sse/handlers/chatCore/nonStreamingHandler.js +++ b/open-sse/handlers/chatCore/nonStreamingHandler.js @@ -11,7 +11,6 @@ import { buildRequestDetail, extractRequestConfig, extractUsageFromResponse, sav import { appendRequestLog, saveRequestDetail } from "@/lib/usageDb.js"; import { decloakToolNames } from "../../utils/claudeCloaking.js"; import { ROLE, RESPONSES_ITEM } from "../../translator/schema/index.js"; -import { toResponsesUsage } from "../../translator/concerns/usage.js"; function parseToolArguments(value) { if (!value) return {}; @@ -132,8 +131,11 @@ function openAICompletionToResponses(responseBody, customToolNames = null) { background: false, error: null, output, - // Keep cached/reasoning details (input_tokens_details) — proxies bill cache hits from them - usage: toResponsesUsage(usage) || { input_tokens: 0, output_tokens: 0, total_tokens: 0 }, + usage: { + input_tokens: usage.prompt_tokens || usage.input_tokens || 0, + output_tokens: usage.completion_tokens || usage.output_tokens || 0, + total_tokens: usage.total_tokens || (usage.prompt_tokens || 0) + (usage.completion_tokens || 0), + }, }; } diff --git a/open-sse/handlers/chatCore/sseToJsonHandler.js b/open-sse/handlers/chatCore/sseToJsonHandler.js index 0da5c69f..6801be89 100644 --- a/open-sse/handlers/chatCore/sseToJsonHandler.js +++ b/open-sse/handlers/chatCore/sseToJsonHandler.js @@ -5,7 +5,6 @@ import { FORMATS } from "../../translator/formats.js"; import { PROVIDERS } from "../../config/providers.js"; import { buildRequestDetail, extractRequestConfig, saveUsageStats, formatDoneLine } from "./requestDetail.js"; import { ROLE, RESPONSES_ITEM } from "../../translator/schema/index.js"; -import { toResponsesUsage } from "../../translator/concerns/usage.js"; // Responses-API providers (e.g. codex) may emit SSE without content-type + use Responses output shape const isResponsesProvider = (p) => PROVIDERS[p]?.format === FORMATS.OPENAI_RESPONSES; @@ -98,8 +97,11 @@ function chatCompletionToResponses(responseBody, customToolNames = null) { background: false, error: null, output, - // Keep cached/reasoning details (input_tokens_details) — proxies bill cache hits from them - usage: toResponsesUsage(usage) || { input_tokens: 0, output_tokens: 0, total_tokens: 0 }, + usage: { + input_tokens: usage.prompt_tokens || usage.input_tokens || 0, + output_tokens: usage.completion_tokens || usage.output_tokens || 0, + total_tokens: usage.total_tokens || (usage.prompt_tokens || 0) + (usage.completion_tokens || 0), + }, }; } diff --git a/open-sse/transformer/responsesTransformer.js b/open-sse/transformer/responsesTransformer.js index 6558a3f1..ac84db20 100644 --- a/open-sse/transformer/responsesTransformer.js +++ b/open-sse/transformer/responsesTransformer.js @@ -6,7 +6,6 @@ import fs from "fs"; import path from "path"; -import { toResponsesUsage } from "../translator/concerns/usage.js"; // Create log directory for responses (Node.js only) export function createResponsesLogger(model, logsDir = null) { @@ -74,7 +73,6 @@ export function createResponsesApiTransformStream(logger = null) { funcArgsDone: {}, funcItemDone: {}, buffer: "", - usage: null, completedSent: false }; @@ -227,17 +225,17 @@ export function createResponsesApiTransformStream(logger = null) { const sendCompleted = (controller) => { if (!state.completedSent) { state.completedSent = true; - const response = { - id: state.responseId, - object: "response", - created_at: state.created, - status: "completed", - background: false, - error: null - }; - const usage = toResponsesUsage(state.usage); - if (usage) response.usage = usage; - emit(controller, "response.completed", { type: "response.completed", response }); + emit(controller, "response.completed", { + type: "response.completed", + response: { + id: state.responseId, + object: "response", + created_at: state.created, + status: "completed", + background: false, + error: null + } + }); } }; @@ -266,9 +264,6 @@ export function createResponsesApiTransformStream(logger = null) { continue; } - // Remember usage (finish chunk or trailing include_usage frame) for response.completed - if (parsed.usage && typeof parsed.usage === "object") state.usage = parsed.usage; - if (!parsed.choices?.length) continue; const choice = parsed.choices[0]; diff --git a/open-sse/translator/concerns/usage.js b/open-sse/translator/concerns/usage.js index 1ee28489..3ace3062 100644 --- a/open-sse/translator/concerns/usage.js +++ b/open-sse/translator/concerns/usage.js @@ -67,34 +67,3 @@ export function toOpenAIUsage(raw, kind) { if (!extract || !raw || typeof raw !== "object") return null; return buildUsage(extract(raw)); } - -// Convert an OpenAI-shaped (or already-canonical / Claude-shaped) usage object into the -// Responses API shape emitted by `response.completed`. Details objects are always present -// (like the real API) so proxies that read `input_tokens_details.cached_tokens` never see undefined. -// Returns null when there is nothing countable. -export function toResponsesUsage(usage) { - if (!usage || typeof usage !== "object") return null; - const input = n(usage.prompt_tokens ?? usage.input_tokens); - const output = n(usage.completion_tokens ?? usage.output_tokens); - if (input === 0 && output === 0) return null; - const cached = n( - usage.input_tokens_details?.cached_tokens ?? - usage.prompt_tokens_details?.cached_tokens ?? - usage.cached_tokens ?? - usage.cache_read_input_tokens - ); - const reasoning = n( - usage.output_tokens_details?.reasoning_tokens ?? - usage.completion_tokens_details?.reasoning_tokens ?? - usage.reasoning_tokens - ); - const out = { - input_tokens: input, - output_tokens: output, - total_tokens: typeof usage.total_tokens === "number" ? usage.total_tokens : input + output, - input_tokens_details: { cached_tokens: cached }, - output_tokens_details: { reasoning_tokens: reasoning }, - }; - if (usage.estimated) out.estimated = true; - return out; -} diff --git a/open-sse/translator/response/openai-responses.js b/open-sse/translator/response/openai-responses.js index 1d9a7dec..bd435f9c 100644 --- a/open-sse/translator/response/openai-responses.js +++ b/open-sse/translator/response/openai-responses.js @@ -5,7 +5,7 @@ import { register } from "../index.js"; import { FORMATS } from "../formats.js"; import { buildChunk } from "../concerns/chunk.js"; -import { buildUsage, toResponsesUsage } from "../concerns/usage.js"; +import { buildUsage } from "../concerns/usage.js"; import { fallbackToolCallId } from "../concerns/toolCall.js"; import { reasoningDelta, extractReasoningText } from "../concerns/reasoning.js"; import { ROLE, OPENAI_BLOCK, RESPONSES_ITEM, OPENAI_FINISH, MODEL_FALLBACK } from "../schema/index.js"; @@ -18,13 +18,7 @@ export function openaiToOpenAIResponsesResponse(chunk, state) { if (!chunk) { return flushEvents(state); } - - // Usage riding on the finish chunk (include_usage style, e.g. coalesced Qoder frames): - // remember it so response.completed can report tokens even outside stream.js. - if (chunk.usage && typeof chunk.usage === "object" && !state.usage) { - state.usage = chunk.usage; - } - + if (!chunk.choices?.length) return []; const events = []; @@ -374,19 +368,17 @@ function closeToolCall(state, emit, idx) { function sendCompleted(state, emit) { if (!state.completedSent) { state.completedSent = true; - const response = { - id: state.responseId, - object: "response", - created_at: state.created, - status: "completed", - background: false, - error: null - }; - // Carry provider usage (recorded by stream.js or from the finish chunk itself) in the - // Responses shape; proxies such as sub2api/Codex read tokens only from here. - const usage = toResponsesUsage(state.usage); - if (usage) response.usage = usage; - emit("response.completed", { type: "response.completed", response }); + emit("response.completed", { + type: "response.completed", + response: { + id: state.responseId, + object: "response", + created_at: state.created, + status: "completed", + background: false, + error: null + } + }); } } diff --git a/open-sse/translator/response/openai-to-claude.js b/open-sse/translator/response/openai-to-claude.js index f291e83a..3998cc84 100644 --- a/open-sse/translator/response/openai-to-claude.js +++ b/open-sse/translator/response/openai-to-claude.js @@ -67,50 +67,48 @@ function stopTextBlock(state, results) { state.textBlockStarted = false; } -function recordOpenAIUsage(chunk, state) { - if (!chunk?.usage || typeof chunk.usage !== "object") return; - - const promptTokens = typeof chunk.usage.prompt_tokens === "number" ? chunk.usage.prompt_tokens : 0; - const outputTokens = typeof chunk.usage.completion_tokens === "number" ? chunk.usage.completion_tokens : 0; - - // Extract cache tokens from prompt_tokens_details - const cachedTokens = chunk.usage.prompt_tokens_details?.cached_tokens; - const cacheCreationTokens = chunk.usage.prompt_tokens_details?.cache_creation_tokens; - const cacheReadTokens = typeof cachedTokens === "number" ? cachedTokens : 0; - const cacheCreateTokens = typeof cacheCreationTokens === "number" ? cacheCreationTokens : 0; - - // input_tokens = prompt_tokens - cached_tokens - cache_creation_tokens - // Because OpenAI's prompt_tokens includes all prompt-side tokens - const inputTokens = promptTokens - cacheReadTokens - cacheCreateTokens; - - state.usage = { - input_tokens: inputTokens, - output_tokens: outputTokens - }; - - if (cacheReadTokens > 0) { - state.usage.cache_read_input_tokens = cacheReadTokens; - } - if (cacheCreateTokens > 0) { - state.usage.cache_creation_input_tokens = cacheCreateTokens; - } -} - // Convert OpenAI stream chunk to Claude format export function openaiToClaudeResponse(chunk, state) { - if (!chunk) return null; - - // Track usage from OpenAI chunk if available - if (chunk.usage && typeof chunk.usage === "object") { - recordOpenAIUsage(chunk, state); - } - - if (!chunk.choices?.[0]) return null; + if (!chunk || !chunk.choices?.[0]) return null; const results = []; const choice = chunk.choices[0]; const delta = choice.delta; + // Track usage from OpenAI chunk if available + if (chunk.usage && typeof chunk.usage === "object") { + const promptTokens = typeof chunk.usage.prompt_tokens === "number" ? chunk.usage.prompt_tokens : 0; + const outputTokens = typeof chunk.usage.completion_tokens === "number" ? chunk.usage.completion_tokens : 0; + + // Extract cache tokens from prompt_tokens_details + const cachedTokens = chunk.usage.prompt_tokens_details?.cached_tokens; + const cacheCreationTokens = chunk.usage.prompt_tokens_details?.cache_creation_tokens; + const cacheReadTokens = typeof cachedTokens === "number" ? cachedTokens : 0; + const cacheCreateTokens = typeof cacheCreationTokens === "number" ? cacheCreationTokens : 0; + + // input_tokens = prompt_tokens - cached_tokens - cache_creation_tokens + // Because OpenAI's prompt_tokens includes all prompt-side tokens + const inputTokens = promptTokens - cacheReadTokens - cacheCreateTokens; + + state.usage = { + input_tokens: inputTokens, + output_tokens: outputTokens + }; + + // Add cache_read_input_tokens if present + if (cacheReadTokens > 0) { + state.usage.cache_read_input_tokens = cacheReadTokens; + } + + // Add cache_creation_input_tokens if present + if (cacheCreateTokens > 0) { + state.usage.cache_creation_input_tokens = cacheCreateTokens; + } + + // Note: completion_tokens_details.reasoning_tokens is already included in output_tokens + // No need to add separately as Claude expects total output_tokens + } + // First chunk - ALWAYS send message_start first if (!state.messageStartSent) { state.messageStartSent = true; @@ -223,9 +221,8 @@ export function openaiToClaudeResponse(chunk, state) { } } - // Finish (OpenAI puts this on the choice; Qoder often puts it on delta) - const finishReason = choice.finish_reason || delta?.finish_reason; - if (finishReason) { + // Finish + if (choice.finish_reason) { stopThinkingBlock(state, results); stopTextBlock(state, results); @@ -247,13 +244,13 @@ export function openaiToClaudeResponse(chunk, state) { } // Mark finish for later usage injection in stream.js - state.finishReason = finishReason; + state.finishReason = choice.finish_reason; // Use tracked usage (will be estimated in stream.js if not valid) const finalUsage = state.usage || { input_tokens: 0, output_tokens: 0 }; results.push({ type: "message_delta", - delta: { stop_reason: convertFinishReason(finishReason) }, + delta: { stop_reason: convertFinishReason(choice.finish_reason) }, usage: finalUsage }); results.push({ type: "message_stop" }); diff --git a/open-sse/utils/stream.js b/open-sse/utils/stream.js index f61fd193..15ee37d8 100644 --- a/open-sse/utils/stream.js +++ b/open-sse/utils/stream.js @@ -3,7 +3,6 @@ import { FORMATS } from "../translator/formats.js"; import { trackPendingRequest, appendRequestLog } from "@/lib/usageDb.js"; import { extractUsage, mergeUsage, hasValidUsage, estimateUsage, logUsage, addBufferToUsage, filterUsageForFormat, COLORS } from "./usageTracking.js"; import { parseSSELine, hasValuableContent, fixInvalidId, formatSSE } from "./streamHelpers.js"; -import { toResponsesUsage } from "../translator/concerns/usage.js"; import { getOpenAIResponsesEventName, isOpenAIResponsesTerminalEvent, formatIncompleteOpenAIResponsesStreamFailure } from "./responsesStreamHelpers.js"; import { dbg, isDebugEnabled } from "./debugLog.js"; @@ -202,8 +201,7 @@ export function createSSEStream(options = {}) { responsesTerminal = isOpenAIResponsesTerminalEvent(currentOpenAIResponsesEvent, parsed); - const isFinishChunk = parsed.choices?.[0]?.finish_reason - || parsed.choices?.[0]?.delta?.finish_reason; + const isFinishChunk = parsed.choices?.[0]?.finish_reason; if (isFinishChunk && !hasValidUsage(parsed.usage)) { const estimated = estimateUsage(body, totalContentLength, FORMATS.OPENAI); parsed.usage = filterUsageForFormat(estimated, FORMATS.OPENAI); @@ -367,19 +365,6 @@ export function createSSEStream(options = {}) { item.usage = filterUsageForFormat(buffered, sourceFormat); } - // Responses API clients (Codex, sub2api /v1/responses): usage lives on - // response.completed → response.usage. Same buffer/estimate policy as above. - const completedResponse = item.event === "response.completed" ? item.data?.response : null; - if (completedResponse && typeof completedResponse === "object") { - if (state.usage) { - completedResponse.usage = toResponsesUsage(addBufferToUsage(state.usage)) ?? completedResponse.usage; - } else if (!completedResponse.usage && totalContentLength > 0) { - const estimated = estimateUsage(body, totalContentLength, FORMATS.OPENAI); - completedResponse.usage = toResponsesUsage(estimated); - state.usage = estimated; - } - } - const output = formatSSE(item, sourceFormat); reqLogger?.appendConvertedChunk?.(output); controller.enqueue(sharedEncoder.encode(output)); diff --git a/tests/unit/openai-responses-usage.test.js b/tests/unit/openai-responses-usage.test.js deleted file mode 100644 index 38ec581d..00000000 --- a/tests/unit/openai-responses-usage.test.js +++ /dev/null @@ -1,182 +0,0 @@ -/** - * Responses API clients (Codex, sub2api /v1/responses) read token usage only from - * `response.completed → response.usage`. For chat-native upstreams (Qoder, most - * OpenAI-compatible providers) the translator used to emit that event without usage, - * so proxies logged 0 input / 0 output / 0 cached tokens. - */ -import { describe, expect, it, vi } from "vitest"; - -vi.mock("@/lib/usageDb.js", () => ({ - appendRequestLog: vi.fn(async () => {}), - saveRequestDetail: vi.fn(async () => {}), - saveRequestUsage: vi.fn(async () => {}), - trackPendingRequest: vi.fn(() => {}), -})); - -const { FORMATS } = await import("../../open-sse/translator/formats.js"); -const { initState } = await import("../../open-sse/translator/index.js"); -const { toResponsesUsage } = await import("../../open-sse/translator/concerns/usage.js"); -const { openaiToOpenAIResponsesResponse } = await import("../../open-sse/translator/response/openai-responses.js"); -const { createSSETransformStreamWithLogger } = await import("../../open-sse/utils/stream.js"); -const { createResponsesApiTransformStream } = await import("../../open-sse/transformer/responsesTransformer.js"); -const { addBufferToUsage } = await import("../../open-sse/utils/usageTracking.js"); -// stream.js adds the same context-safety buffer it applies to chat/claude clients -const BUFFER_TOKENS = addBufferToUsage({ prompt_tokens: 0 }).prompt_tokens; - -const QODER_FINISH_CHUNK = { - id: "chatcmpl-qoder-1", - object: "chat.completion.chunk", - created: 1_700_000_000, - model: "qmodel_38max", - choices: [{ index: 0, delta: {}, finish_reason: "stop" }], - usage: { - prompt_tokens: 27_339, - completion_tokens: 437, - total_tokens: 27_776, - prompt_tokens_details: { cached_tokens: 27_200 }, - }, -}; - -function sse(chunks) { - return chunks.map((c) => `data: ${typeof c === "string" ? c : JSON.stringify(c)}\n\n`).join(""); -} - -async function pipe(input, transform) { - const encoder = new TextEncoder(); - const stream = new ReadableStream({ - start(controller) { - controller.enqueue(encoder.encode(input)); - controller.close(); - }, - }); - const reader = stream.pipeThrough(transform).getReader(); - const decoder = new TextDecoder(); - let text = ""; - for (;;) { - const { value, done } = await reader.read(); - if (done) break; - text += decoder.decode(value, { stream: true }); - } - return text + decoder.decode(); -} - -function completedEvent(text) { - const m = text.match(/event: response\.completed\ndata: (.+)\n/); - return m ? JSON.parse(m[1]) : null; -} - -describe("toResponsesUsage", () => { - it("maps OpenAI usage (nested cached_tokens) to the Responses shape", () => { - expect(toResponsesUsage(QODER_FINISH_CHUNK.usage)).toEqual({ - input_tokens: 27_339, - output_tokens: 437, - total_tokens: 27_776, - input_tokens_details: { cached_tokens: 27_200 }, - output_tokens_details: { reasoning_tokens: 0 }, - }); - }); - - it("accepts canonical flat fields and Claude-style cache fields", () => { - expect(toResponsesUsage({ prompt_tokens: 10, completion_tokens: 2, cached_tokens: 4, reasoning_tokens: 1 })).toMatchObject({ - input_tokens: 10, - output_tokens: 2, - total_tokens: 12, - input_tokens_details: { cached_tokens: 4 }, - output_tokens_details: { reasoning_tokens: 1 }, - }); - expect(toResponsesUsage({ input_tokens: 5, output_tokens: 1, cache_read_input_tokens: 3 }).input_tokens_details.cached_tokens).toBe(3); - }); - - it("keeps the estimated marker and returns null for empty usage", () => { - expect(toResponsesUsage({ prompt_tokens: 1, completion_tokens: 1, estimated: true }).estimated).toBe(true); - expect(toResponsesUsage({})).toBeNull(); - expect(toResponsesUsage(null)).toBeNull(); - }); -}); - -describe("openai → openai-responses translator", () => { - it("puts usage from the finish chunk on response.completed", () => { - const state = initState(FORMATS.OPENAI_RESPONSES); - const events = openaiToOpenAIResponsesResponse(QODER_FINISH_CHUNK, state); - const completed = events.find((e) => e.event === "response.completed"); - expect(completed).toBeTruthy(); - expect(completed.data.response.usage).toEqual({ - input_tokens: 27_339, - output_tokens: 437, - total_tokens: 27_776, - input_tokens_details: { cached_tokens: 27_200 }, - output_tokens_details: { reasoning_tokens: 0 }, - }); - }); - - it("omits usage when the upstream never reported any", () => { - const state = initState(FORMATS.OPENAI_RESPONSES); - const events = openaiToOpenAIResponsesResponse({ ...QODER_FINISH_CHUNK, usage: undefined }, state); - const completed = events.find((e) => e.event === "response.completed"); - expect(completed.data.response.usage).toBeUndefined(); - }); -}); - -describe("stream.js translate mode: chat upstream → Responses client", () => { - const transform = () => createSSETransformStreamWithLogger( - FORMATS.OPENAI, // provider (Qoder executor emits OpenAI chunks) - FORMATS.OPENAI_RESPONSES, // client - "qoder", - null, - null, - "qmodel_38max", - null, - { model: "qd/qmodel_38max", messages: [{ role: "user", content: "hi" }] }, - ); - - it("emits provider usage (+buffer) with cached tokens on response.completed", async () => { - const out = await pipe(sse([ - { ...QODER_FINISH_CHUNK, choices: [{ index: 0, delta: { role: "assistant", content: "Hello" }, finish_reason: null }], usage: undefined }, - QODER_FINISH_CHUNK, - "[DONE]", - ]), transform()); - - const completed = completedEvent(out); - expect(completed).toBeTruthy(); - expect(completed.response.usage).toEqual({ - input_tokens: 27_339 + BUFFER_TOKENS, - output_tokens: 437, - total_tokens: 27_776 + BUFFER_TOKENS, - input_tokens_details: { cached_tokens: 27_200 }, - output_tokens_details: { reasoning_tokens: 0 }, - }); - // Responses clients terminate on response.completed (no [DONE] sentinel in translate mode) - expect(out.indexOf("event: response.completed")).toBeGreaterThan(out.indexOf("event: response.output_item.done")); - }); - - it("injects estimated usage when the upstream reports none", async () => { - const out = await pipe(sse([ - { ...QODER_FINISH_CHUNK, choices: [{ index: 0, delta: { role: "assistant", content: "Hello world" }, finish_reason: null }], usage: undefined }, - { ...QODER_FINISH_CHUNK, usage: undefined }, - "[DONE]", - ]), transform()); - - const completed = completedEvent(out); - expect(completed.response.usage).toBeTruthy(); - expect(completed.response.usage.estimated).toBe(true); - expect(completed.response.usage.input_tokens).toBeGreaterThan(0); - expect(completed.response.usage.output_tokens).toBeGreaterThan(0); - }); -}); - -describe("responsesTransformer (Chat SSE → Codex Responses SSE)", () => { - it("forwards finish-chunk usage on response.completed", async () => { - const out = await pipe(sse([ - { ...QODER_FINISH_CHUNK, choices: [{ index: 0, delta: { role: "assistant", content: "Hello" }, finish_reason: null }], usage: undefined }, - QODER_FINISH_CHUNK, - "[DONE]", - ]), createResponsesApiTransformStream()); - - const completed = completedEvent(out); - expect(completed.response.usage).toMatchObject({ - input_tokens: 27_339, - output_tokens: 437, - input_tokens_details: { cached_tokens: 27_200 }, - }); - }); -}); diff --git a/tests/unit/openai-to-claude.test.js b/tests/unit/openai-to-claude.test.js index 0c5768d7..45b67fb2 100644 --- a/tests/unit/openai-to-claude.test.js +++ b/tests/unit/openai-to-claude.test.js @@ -203,31 +203,4 @@ describe("openaiToClaudeResponse", () => { limit: 120 }); }); - - it("records usage from a choices:[] frame so the finish chunk can emit it", () => { - const state = { toolCalls: new Map() }; - expect(openaiToClaudeResponse({ - usage: { - prompt_tokens: 90, - completion_tokens: 7, - prompt_tokens_details: { cached_tokens: 30 }, - }, - choices: [], - }, state)).toBeNull(); - expect(state.usage).toEqual({ - input_tokens: 60, - output_tokens: 7, - cache_read_input_tokens: 30, - }); - - const events = openaiToClaudeResponse({ - id: "chatcmpl-qoder-finish", - model: "qoder/auto", - choices: [{ index: 0, delta: {}, finish_reason: "stop" }], - }, state); - const delta = events.find((e) => e.type === "message_delta"); - expect(delta.usage.input_tokens).toBe(60); - expect(delta.usage.output_tokens).toBe(7); - expect(delta.usage.cache_read_input_tokens).toBe(30); - }); });