Files
OmniRoute/tests/unit/gemini-midstream-error-4177.test.ts
Diego Rodrigues de Sa e Souza 3c9883bb73 Release v3.8.29 (#4126)
OmniRoute v3.8.29 — 115 commits since v3.8.28. Full CHANGELOG + 41 i18n mirrors. All content quality gates green (build, unit 8/8, vitest 188/188, PR test policy, quality gates extended, docs sync, quality ratchet). Remaining red CI checks are pre-existing release flakes (coverage-shard/integration/node-compat teardown), a new transitive undici advisory in electron devDeps, and a workflow-level CodeQL fail (0 open alerts). VPS-validated by the operator.
2026-06-19 06:49:01 -03:00

110 lines
3.5 KiB
TypeScript

/**
* Regression test for #4177 — Gemini mid-stream error silently swallowed.
*
* When the upstream Gemini SSE stream emits a JSON error object
* (e.g. `{"error":{"code":503,"message":"...","status":"UNAVAILABLE"}}`) instead of a
* `candidates` payload — typically after some partial reasoning content — the
* translator must surface it as `state.upstreamError` so the streaming pipeline can
* error the response out (and trigger combo fallback) rather than ending the stream
* with a default `finish_reason: "stop"` and a misleading HTTP 200.
*/
import test from "node:test";
import assert from "node:assert/strict";
const { geminiToOpenAIResponse } =
await import("../../open-sse/translator/response/gemini-to-openai.ts");
type StreamState = {
toolCalls: Map<string, unknown>;
messageId?: string;
model?: string;
upstreamError?: { status: number; type: string; code: string; message: string };
};
function createStreamingState(): StreamState {
return {
toolCalls: new Map(),
};
}
test("#4177 Gemini mid-stream 503 UNAVAILABLE is surfaced as upstreamError, not dropped", () => {
const state = createStreamingState();
const result = geminiToOpenAIResponse(
{
error: {
code: 503,
message:
"This model is currently experiencing high demand. Spikes in demand are usually temporary. Please try again later.",
status: "UNAVAILABLE",
},
},
state
);
// The error chunk produces no delta output...
assert.equal(result, null);
// ...but it MUST be recorded so stream.ts can error the stream out.
assert.ok(state.upstreamError, "expected state.upstreamError to be set for a 503 error chunk");
assert.equal(state.upstreamError.status, 503);
assert.equal(state.upstreamError.type, "server_error");
assert.equal(state.upstreamError.code, "UNAVAILABLE");
assert.match(state.upstreamError.message, /high demand/);
});
test("#4177 Gemini RESOURCE_EXHAUSTED maps to a 429 rate-limit upstreamError", () => {
const state = createStreamingState();
const result = geminiToOpenAIResponse(
{
error: {
code: 429,
message: "Resource has been exhausted (e.g. check quota).",
status: "RESOURCE_EXHAUSTED",
},
},
state
);
assert.equal(result, null);
assert.ok(state.upstreamError);
assert.equal(state.upstreamError.status, 429);
assert.equal(state.upstreamError.type, "rate_limit_error");
assert.equal(state.upstreamError.code, "RESOURCE_EXHAUSTED");
});
test("#4177 Gemini error wrapped in a `response` envelope (Antigravity/Cloud Code) is detected", () => {
const state = createStreamingState();
const result = geminiToOpenAIResponse(
{
response: {
error: { code: 503, message: "overloaded", status: "UNAVAILABLE" },
},
},
state
);
assert.equal(result, null);
assert.ok(state.upstreamError);
assert.equal(state.upstreamError.status, 503);
});
test("#4177 a valid candidate chunk does NOT set upstreamError (no false positive)", () => {
const state = createStreamingState();
const result = geminiToOpenAIResponse(
{
responseId: "resp-ok",
modelVersion: "gemini-2.5-flash",
candidates: [
{
content: { role: "model", parts: [{ text: "hello" }] },
index: 0,
},
],
usageMetadata: { promptTokenCount: 1, totalTokenCount: 2 },
},
state
);
assert.ok(result, "expected normal chunk to translate to OpenAI deltas");
assert.equal(state.upstreamError, undefined);
});