fix(translator): shape Anthropic non-stream responses from forced-streaming providers

The non-streaming Anthropic path leaked OpenAI chat.completion bodies to
Claude clients when the upstream provider forces streaming (forced
SSE->JSON path): parseSSEToOpenAIResponse yields choices[] which Claude
Code cannot parse. Convert to Claude Messages shape for sourceFormat
CLAUDE in sseToJsonHandler.

Shared toClaudeMessageShape/openAICompletionToClaudeMessage moved to
translator/concerns/claudeShape.js to avoid circular import between
nonStreamingHandler and sseToJsonHandler. 5 new tests: sse-to-json
reassembly + Claude guard, plus existing anthropic-nonstream-shape
updated to import from the concern.
This commit is contained in:
asepharyana committed 2026-09-23 15:36:24 +07:00
1 parent b239b2a8a9
commit 3de32a10f3
5 files changed
+139 -67

No files matched your search

@@ -3,6 +3,7 @@ import { restoreToolNames } from "../../utils/opencodeFingerprint.js";
import { createErrorResult } from "../../utils/error.js";
import { HTTP_STATUS } from "../../config/runtimeConfig.js";
import { FORMATS } from "../../translator/formats.js";
import { toClaudeMessageShape } from "../../translator/concerns/claudeShape.js";
import { PROVIDERS } from "../../config/providers.js";
import { buildRequestDetail, extractRequestConfig, saveUsageStats, formatDoneLine } from "./requestDetail.js";
import { ROLE, RESPONSES_ITEM } from "../../translator/schema/index.js";
@@ -356,10 +357,17 @@ export async function handleForcedSSEToJson({ providerResponse, sourceFormat, ta
// lost on the non-streaming return path. Inlined (not imported from
// nonStreamingHandler.js) to avoid a circular import: nonStreamingHandler
// already imports parseSSEToOpenAIResponse from this module.
const finalBody = sourceFormat === FORMATS.OPENAI_RESPONSES
let finalBody = sourceFormat === FORMATS.OPENAI_RESPONSES
? chatCompletionToResponses(parsed, customToolNames)
: parsed;
// Anthropic client behind a forced-streaming chat provider: the SSE was
// OpenAI-shaped, so hand the client the Claude Messages shape (type:
// "message", content[] blocks, stop_reason) instead of leaking choices[].
if (sourceFormat === FORMATS.CLAUDE) {
finalBody = toClaudeMessageShape(finalBody);
}
return { success: true, response: new Response(JSON.stringify(restoreToolNames(finalBody, toolNameMap)), { headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" } }) };
} catch (err) {
console.error("[ChatCore] Chat Completions SSE→JSON failed:", err);