fix: translation failures for OpenAI-format providers in Claude CLI (#632)

- Handle reasoning_details[] array (StepFun/OpenRouter format) in sanitizer and translator
- Handle 'reasoning' field alias → reasoning_content in streaming and non-streaming paths
- Cross-map input_tokens/output_tokens ↔ prompt_tokens/completion_tokens in filterUsageForFormat
- Fix extractUsage to accept input_tokens/output_tokens as alternative field names
- All 936 tests pass
This commit is contained in:
diegosouzapw
2026-03-25 22:01:29 -03:00
parent ed2e0c5080
commit dee1d9ba74
3 changed files with 114 additions and 6 deletions

View File

@@ -172,6 +172,44 @@ function sanitizeMessage(msg: unknown): unknown {
sanitized.reasoning_content = msgRecord.reasoning_content;
}
// Handle 'reasoning' field alias (some providers use this instead of reasoning_content)
if (
msgRecord.reasoning &&
typeof msgRecord.reasoning === "string" &&
!sanitized.reasoning_content
) {
sanitized.reasoning_content = msgRecord.reasoning;
}
// Handle reasoning_details[] array (StepFun/OpenRouter format)
// Structure: [{ type: "reasoning.text", text: "...", format: "unknown", index: 0 }]
if (Array.isArray(msgRecord.reasoning_details) && !sanitized.reasoning_content) {
const reasoningParts: string[] = [];
for (const detail of msgRecord.reasoning_details) {
const detailObj = detail && typeof detail === "object" ? (detail as JsonRecord) : null;
if (!detailObj) continue;
const detailType = typeof detailObj.type === "string" ? detailObj.type : "";
const detailText =
typeof detailObj.text === "string"
? detailObj.text
: typeof detailObj.content === "string"
? detailObj.content
: "";
if (
detailText &&
(detailType === "reasoning" ||
detailType === "reasoning.text" ||
detailType === "thinking" ||
detailType === "")
) {
reasoningParts.push(detailText);
}
}
if (reasoningParts.length > 0) {
sanitized.reasoning_content = reasoningParts.join("");
}
}
// Preserve tool_calls
if (msgRecord.tool_calls) {
sanitized.tool_calls = msgRecord.tool_calls;
@@ -258,6 +296,26 @@ export function sanitizeStreamingChunk(parsed: unknown): unknown {
if (deltaRecord.content !== undefined) delta.content = deltaRecord.content;
if (deltaRecord.reasoning_content !== undefined) {
delta.reasoning_content = deltaRecord.reasoning_content;
} else if (typeof deltaRecord.reasoning === "string" && deltaRecord.reasoning) {
// Alias: some providers use 'reasoning' instead of 'reasoning_content'
delta.reasoning_content = deltaRecord.reasoning;
} else if (Array.isArray(deltaRecord.reasoning_details)) {
// StepFun/OpenRouter: reasoning_details[{type:"reasoning.text", text:"..."}]
const parts: string[] = [];
for (const detail of deltaRecord.reasoning_details) {
const d = detail && typeof detail === "object" ? (detail as JsonRecord) : null;
if (!d) continue;
const text =
typeof d.text === "string"
? d.text
: typeof d.content === "string"
? d.content
: "";
if (text) parts.push(text);
}
if (parts.length > 0) {
delta.reasoning_content = parts.join("");
}
}
if (deltaRecord.tool_calls !== undefined) delta.tool_calls = deltaRecord.tool_calls;
if (deltaRecord.function_call !== undefined)

View File

@@ -93,7 +93,18 @@ export function openaiToClaudeResponse(chunk, state) {
}
// Handle reasoning_content (thinking) - GLM, DeepSeek, etc.
const reasoningContent = delta?.reasoning_content || delta?.reasoning;
// Also supports 'reasoning' field alias and reasoning_details[] (StepFun/OpenRouter)
let reasoningContent = delta?.reasoning_content || delta?.reasoning;
if (!reasoningContent && Array.isArray(delta?.reasoning_details)) {
const parts: string[] = [];
for (const detail of delta.reasoning_details) {
if (detail && typeof detail === "object") {
const text = detail.text || detail.content;
if (typeof text === "string" && text) parts.push(text);
}
}
if (parts.length > 0) reasoningContent = parts.join("");
}
if (reasoningContent) {
stopTextBlock(state, results);

View File

@@ -66,12 +66,47 @@ export function addBufferToUsage(usage) {
export function filterUsageForFormat(usage, targetFormat) {
if (!usage || typeof usage !== "object") return usage;
// Cross-map between Claude-style and OpenAI-style field names before filtering.
// Some providers return input_tokens/output_tokens even when using OpenAI format.
const convertedUsage = { ...usage };
if (targetFormat === FORMATS.CLAUDE || targetFormat === FORMATS.OPENAI_RESPONSES) {
// OpenAI → Claude: prompt_tokens → input_tokens
if (convertedUsage.prompt_tokens !== undefined && convertedUsage.input_tokens === undefined) {
convertedUsage.input_tokens = convertedUsage.prompt_tokens;
}
if (
convertedUsage.completion_tokens !== undefined &&
convertedUsage.output_tokens === undefined
) {
convertedUsage.output_tokens = convertedUsage.completion_tokens;
}
} else {
// Claude → OpenAI: input_tokens → prompt_tokens
if (convertedUsage.input_tokens !== undefined && convertedUsage.prompt_tokens === undefined) {
convertedUsage.prompt_tokens = convertedUsage.input_tokens;
}
if (
convertedUsage.output_tokens !== undefined &&
convertedUsage.completion_tokens === undefined
) {
convertedUsage.completion_tokens = convertedUsage.output_tokens;
}
// Ensure total_tokens is set
if (
convertedUsage.total_tokens === undefined &&
convertedUsage.prompt_tokens !== undefined &&
convertedUsage.completion_tokens !== undefined
) {
convertedUsage.total_tokens = convertedUsage.prompt_tokens + convertedUsage.completion_tokens;
}
}
// Helper to pick only defined fields from usage
const pickFields = (fields) => {
const filtered = {};
for (const field of fields) {
if (usage[field] !== undefined) {
filtered[field] = usage[field];
if (convertedUsage[field] !== undefined) {
filtered[field] = convertedUsage[field];
}
}
return filtered;
@@ -230,10 +265,14 @@ export function extractUsage(chunk) {
}
// OpenAI format
if (chunk.usage && typeof chunk.usage === "object" && chunk.usage.prompt_tokens !== undefined) {
if (
chunk.usage &&
typeof chunk.usage === "object" &&
(chunk.usage.prompt_tokens !== undefined || chunk.usage.input_tokens !== undefined)
) {
return normalizeUsage({
prompt_tokens: chunk.usage.prompt_tokens,
completion_tokens: chunk.usage.completion_tokens || 0,
prompt_tokens: chunk.usage.prompt_tokens ?? chunk.usage.input_tokens ?? 0,
completion_tokens: chunk.usage.completion_tokens ?? chunk.usage.output_tokens ?? 0,
cached_tokens: chunk.usage.prompt_tokens_details?.cached_tokens,
reasoning_tokens: chunk.usage.completion_tokens_details?.reasoning_tokens,
});