mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
fix(translator): accept Claude Messages shape in non-stream malformed-200 guard (#5156)
Integrated into release/v3.8.39
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user