fix(translator): accept Claude Messages shape in non-stream malformed-200 guard (#5156)

Integrated into release/v3.8.39
This commit is contained in:
Anton
2026-06-28 08:09:27 +02:00
committed by GitHub
parent c06fa4e8e1
commit 4e705b9e00
2 changed files with 77 additions and 6 deletions

View File

@@ -168,6 +168,9 @@ export function synthResponsesFailure(reason?: MalformedReason): string {
* - Tool-call responses (content=null + tool_calls=[…]) are also valid.
* - Responses API function_call / other structural items count as output even
* when they carry no user-visible text.
* - Claude Messages shape (type:"message" + content[]) is checked directly,
* since a Claude client receives the body in that shape (no
* `choices`/`object:"response"`).
*/
export function detectMalformedNonStream(resp: unknown): MalformedReason | null {
if (!resp || typeof resp !== "object") return "empty_choices";
@@ -213,16 +216,25 @@ export function detectMalformedNonStream(resp: unknown): MalformedReason | null
// throws on `null.type` (that would crash the whole non-stream classifier).
if (block === null || typeof block !== "object") return false;
const b = block as Record<string, unknown>;
// Text block with visible text.
if (b.type === "text" && typeof b.text === "string" && (b.text as string).length > 0) {
// Text block with visible text. `convertOpenAINonStreamingToClaude` emits
// "(empty response)" as a placeholder when the upstream produced no content,
// so treat that sentinel as empty — a genuinely empty completion still trips
// the guard (parity with the OpenAI `content:""` path).
if (
b.type === "text" &&
typeof b.text === "string" &&
(b.text as string).length > 0 &&
b.text !== "(empty response)"
) {
return true;
}
// Extended-thinking block: a non-empty `signature` is cryptographic proof the
// thinking step ran, so it is a valid completion even when the thinking text is "".
// Extended-thinking block: valid when it carries visible thinking text OR a
// non-empty `signature` (cryptographic proof the thinking step ran, so it is a
// valid completion even when the thinking text is "").
if (
b.type === "thinking" &&
typeof b.signature === "string" &&
(b.signature as string).length > 0
((typeof b.thinking === "string" && (b.thinking as string).length > 0) ||
(typeof b.signature === "string" && (b.signature as string).length > 0))
) {
return true;
}

View File

@@ -209,6 +209,65 @@ test("detectMalformedNonStream allows Responses API function_call items as valid
assert.equal(detectMalformedNonStream(body), null);
});
// ── (c2) detectMalformedNonStream — Claude Messages shape ────────────────────
test("detectMalformedNonStream returns null for Claude message with text content", () => {
const body = {
type: "message",
role: "assistant",
content: [{ type: "text", text: "Hi!" }],
stop_reason: "end_turn",
};
assert.equal(detectMalformedNonStream(body), null);
});
test("detectMalformedNonStream returns null for Claude message with tool_use block", () => {
const body = {
type: "message",
role: "assistant",
content: [{ type: "tool_use", id: "toolu_1", name: "search", input: {} }],
stop_reason: "tool_use",
};
assert.equal(detectMalformedNonStream(body), null);
});
test("detectMalformedNonStream returns null for Claude message with thinking block", () => {
const body = {
type: "message",
role: "assistant",
content: [{ type: "thinking", thinking: "let me think" }],
stop_reason: "end_turn",
};
assert.equal(detectMalformedNonStream(body), null);
});
test("detectMalformedNonStream returns 'empty_choices' for Claude message with empty content", () => {
const body = { type: "message", role: "assistant", content: [], stop_reason: "end_turn" };
assert.equal(detectMalformedNonStream(body), "empty_choices");
});
test("detectMalformedNonStream returns 'empty_choices' for Claude message with empty-text block", () => {
const body = {
type: "message",
role: "assistant",
content: [{ type: "text", text: "" }],
stop_reason: "end_turn",
};
assert.equal(detectMalformedNonStream(body), "empty_choices");
});
test("detectMalformedNonStream returns 'empty_choices' for Claude message with (empty response) placeholder", () => {
// convertOpenAINonStreamingToClaude substitutes this placeholder for empty
// upstream content; it must still be treated as malformed.
const body = {
type: "message",
role: "assistant",
content: [{ type: "text", text: "(empty response)" }],
stop_reason: "end_turn",
};
assert.equal(detectMalformedNonStream(body), "empty_choices");
});
// ── (d) no stack trace leakage ───────────────────────────────────────────────
test("synthOpenAIErrorChunk message does NOT contain stack trace path", () => {