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.
This commit is contained in:
LLL
2026-09-10 22:52:29 +07:00
parent 998bb3d975
commit 248d7da01c
9 changed files with 74 additions and 341 deletions

View File

@@ -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),
},
};
}

View File

@@ -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),
},
};
}

View File

@@ -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];

View File

@@ -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;
}

View File

@@ -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
}
});
}
}

View File

@@ -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" });

View File

@@ -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));