mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 13:52:09 +03:00
This commit is contained in:
committed by
GitHub
parent
11cb8f0976
commit
db68c9b790
@@ -21,6 +21,7 @@ _In development — bullets added per PR; finalized at release._
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(diagnostics): non-streaming Claude responses no longer false-502 as `empty_choices`** — the v3.8.37 malformed-200 detector (#4942) only understood OpenAI `choices` and Responses-API `output` shapes, so a `/v1/messages` response that stays in Claude shape (`{type:"message", content:[…]}`) fell through to `empty_choices` → 502 (cascading to "All models failed" in a combo). Most visibly, an extended-thinking turn whose buffered body is a single **empty thinking block with a valid `signature`** (Claude Code's non-streaming Bash classifier) 502'd on every call. `detectMalformedNonStream` now understands the Claude shape: text/tool_use blocks and thinking blocks carrying a signature count as valid output, while a genuinely empty `content:[]` is still flagged. ([#5108](https://github.com/diegosouzapw/OmniRoute/issues/5108), thanks @insoln)
|
||||
- **fix(combo): empty-content 502 now fails over within the same request instead of exhausting the provider** — a leg that answers HTTP 200 with no usable completion is rewritten to `502 "Provider returned empty content"`, but the combo exhaustion classifier treated that synthetic 502 as a connection-level failure (`#1731v2`) and marked the whole provider/connection exhausted, skipping every remaining **same-provider** leg in that request. The connection is actually healthy (it just returned an empty body), so empty-content 502s are now classified as model-level transient failures: the request advances to the next leg and the rest of that provider's legs stay eligible. Genuine gateway 502s still trip connection exhaustion. ([#5085](https://github.com/diegosouzapw/OmniRoute/issues/5085), thanks @andrea-kingautomation)
|
||||
- **fix(dashboard): surface the detailed credential-validation error instead of a bare "invalid" badge** — the inline "Check" in the Add-Connection modal discarded the `error` message returned by `/api/providers/validate` and showed only an `invalid` badge. For web providers (claude-web / chatgpt-web) the real cause is often an environment error the backend already reports (e.g. `TLS impersonation client failed to start: EACCES … mkdir tls-client-node/bin`), so users were left guessing. The modal now renders the full reason next to the badge. ([#5088](https://github.com/diegosouzapw/OmniRoute/issues/5088), thanks @tkhs101)
|
||||
- **fix(executors): strip `client_metadata` from forwarded body for Cerebras and Mistral** — Cerebras returns 400 (`wrong_api_format`) and Mistral returns 422 (`extra_forbidden`) when the passthrough body carries `client_metadata` (an OpenAI Codex / Claude CLI field with no equivalent on these upstreams). The default executor now drops it for these two providers before sending downstream; other providers (notably `openai`/`codex`) keep it. (thanks @saurabh321gupta)
|
||||
|
||||
@@ -200,6 +200,38 @@ export function detectMalformedNonStream(resp: unknown): MalformedReason | null
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Claude / Anthropic Messages shape ──
|
||||
// A `/v1/messages` request to a Claude provider keeps the response in Claude shape
|
||||
// (no translation when client and provider formats both = Claude), so it reaches here
|
||||
// as `{ type:"message", content:[…] }` — which has neither `object:"response"` nor
|
||||
// `choices`. Without this branch every non-streaming Claude response (incl. plain text)
|
||||
// falls through to `empty_choices` → a false 502 (#5108, regression from #4942).
|
||||
if (body.type === "message" && Array.isArray(body.content)) {
|
||||
const hasOutput = (body.content as unknown[]).some((block) => {
|
||||
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) {
|
||||
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 "".
|
||||
if (
|
||||
b.type === "thinking" &&
|
||||
typeof b.signature === "string" &&
|
||||
(b.signature as string).length > 0
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
// Redacted thinking and tool_use are valid structural output.
|
||||
if (b.type === "redacted_thinking") return true;
|
||||
if (b.type === "tool_use" && typeof b.id === "string" && (b.id as string).length > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return hasOutput ? null : "empty_choices";
|
||||
}
|
||||
|
||||
// ── Chat Completions shape ──
|
||||
const choices = body.choices;
|
||||
if (!Array.isArray(choices) || choices.length === 0) return "empty_choices";
|
||||
|
||||
63
tests/unit/diagnostics-claude-thinking-5108.test.ts
Normal file
63
tests/unit/diagnostics-claude-thinking-5108.test.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* #5108 — Regression from #4942. A non-streaming `/v1/messages` request to a Claude
|
||||
* extended-thinking model returns HTTP 200 with a Claude-shaped body whose `content`
|
||||
* array holds only an *empty* thinking block that still carries a valid `signature`:
|
||||
*
|
||||
* content: [{ type: "thinking", thinking: "", signature: "Eo…" }]
|
||||
*
|
||||
* `detectMalformedNonStream` only understood OpenAI Chat Completions (`choices`) and
|
||||
* Responses API (`object:"response"`) shapes — a Claude body (`type:"message"`,
|
||||
* `content:[…]`) has neither, so it fell through to `empty_choices` and OmniRoute
|
||||
* returned 502 (cascading to "All models failed" inside a combo). The signature proves
|
||||
* the thinking step actually ran, so this is a valid completion, not an empty one.
|
||||
*
|
||||
* The detector must understand the Claude shape: text blocks with text, thinking blocks
|
||||
* with a signature, and tool_use blocks count as output; a genuinely empty `content:[]`
|
||||
* (or thinking with neither text nor signature) is still flagged.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { detectMalformedNonStream } from "../../open-sse/utils/diagnostics.ts";
|
||||
|
||||
const claudeMsg = (content: unknown[]) => ({
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
id: "msg_x",
|
||||
model: "claude-opus-4-8",
|
||||
content,
|
||||
stop_reason: "end_turn",
|
||||
usage: { input_tokens: 5, output_tokens: 1 },
|
||||
});
|
||||
|
||||
test("#5108 Claude thinking-only block with a signature is valid output (was 502 empty_choices)", () => {
|
||||
const body = claudeMsg([{ type: "thinking", thinking: "", signature: "EoABCDEF" }]);
|
||||
assert.equal(detectMalformedNonStream(body), null);
|
||||
});
|
||||
|
||||
test("#5108 normal Claude text response is valid output", () => {
|
||||
const body = claudeMsg([{ type: "text", text: "hello" }]);
|
||||
assert.equal(detectMalformedNonStream(body), null);
|
||||
});
|
||||
|
||||
test("#5108 Claude tool_use response is valid output", () => {
|
||||
const body = claudeMsg([{ type: "tool_use", id: "toolu_1", name: "bash", input: {} }]);
|
||||
assert.equal(detectMalformedNonStream(body), null);
|
||||
});
|
||||
|
||||
test("#5108 genuinely empty Claude content:[] is still flagged malformed", () => {
|
||||
assert.equal(detectMalformedNonStream(claudeMsg([])), "empty_choices");
|
||||
});
|
||||
|
||||
test("#5108 Claude thinking block with neither text nor signature is still flagged", () => {
|
||||
const body = claudeMsg([{ type: "thinking", thinking: "", signature: "" }]);
|
||||
assert.equal(detectMalformedNonStream(body), "empty_choices");
|
||||
});
|
||||
|
||||
// Existing OpenAI / Responses behavior must be unchanged.
|
||||
test("#5108 OpenAI chat completion still validated normally", () => {
|
||||
assert.equal(
|
||||
detectMalformedNonStream({ choices: [{ message: { content: "hi" } }] }),
|
||||
null
|
||||
);
|
||||
assert.equal(detectMalformedNonStream({ choices: [] }), "empty_choices");
|
||||
});
|
||||
Reference in New Issue
Block a user