mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-04 22:32:12 +03:00
agy's gemini-3.1-pro-high/-low had no alias, so resolveAntigravityModelId sent the
speculative -high/-low suffix verbatim to upstream, which rejects it (400) for
gemini-3.x. Worse, the non-stream executor branch fed the 4xx response into the SSE
collector, returning a synthetic empty {object:chat.completion} envelope that masked
the error. Alias both to gemini-3.1-pro, and surface real upstream errors via
buildErrorBody for non-ok non-stream responses. + unit tests.
This commit is contained in:
committed by
GitHub
parent
9c13d44cca
commit
62e6336aad
@@ -103,6 +103,10 @@ export const ANTIGRAVITY_PUBLIC_MODELS = Object.freeze([
|
||||
// gemini-3.x models. Only plain IDs like "gemini-2.5-flash" are proven working.
|
||||
export const ANTIGRAVITY_MODEL_ALIASES = Object.freeze({
|
||||
"gemini-3-pro-preview": "gemini-3.1-pro",
|
||||
// agy catalog exposes -high/-low budget tiers, but the upstream rejects the suffix
|
||||
// for gemini-3.x (#3229) — map them to the plain proven id.
|
||||
"gemini-3.1-pro-high": "gemini-3.1-pro",
|
||||
"gemini-3.1-pro-low": "gemini-3.1-pro",
|
||||
"gemini-3.5-flash-preview": "gemini-3.5-flash",
|
||||
"gemini-3-flash-preview": "gemini-3-flash",
|
||||
"gemini-3-pro-image-preview": "gemini-3-pro-image",
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
type ProviderCredentials,
|
||||
} from "./base.ts";
|
||||
import { applyFingerprint, isCliCompatEnabled } from "../config/cliFingerprints.ts";
|
||||
import { buildAntigravityUpstreamError } from "./antigravityUpstreamError.ts";
|
||||
import {
|
||||
PROVIDERS,
|
||||
OAUTH_ENDPOINTS,
|
||||
@@ -1354,6 +1355,29 @@ export class AntigravityExecutor extends BaseExecutor {
|
||||
// For non-streaming clients, collect the SSE stream and return a synthetic
|
||||
// non-streaming Response so chatCore doesn't need to handle SSE conversion.
|
||||
if (!stream) {
|
||||
// #3229: surface a real upstream error instead of masking a 4xx/5xx as an
|
||||
// empty `chat.completion` envelope (collectStreamToResponse synthesizes a
|
||||
// success-shaped body when the upstream returned no SSE data).
|
||||
if (!response.ok) {
|
||||
const rawBody = await response
|
||||
.clone()
|
||||
.text()
|
||||
.catch(() => "");
|
||||
const errorBody = buildAntigravityUpstreamError(
|
||||
response.status,
|
||||
response.statusText,
|
||||
rawBody
|
||||
);
|
||||
return {
|
||||
response: new Response(JSON.stringify(errorBody), {
|
||||
status: response.status,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
url,
|
||||
headers: finalHeaders,
|
||||
transformedBody: attachToolNameMap(transformedBody, requestToolNameMap),
|
||||
};
|
||||
}
|
||||
const collected = await this.collectStreamToResponse(
|
||||
response,
|
||||
model,
|
||||
|
||||
25
open-sse/executors/antigravityUpstreamError.ts
Normal file
25
open-sse/executors/antigravityUpstreamError.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Build a sanitized OpenAI-style error body for a non-ok Antigravity/agy upstream
|
||||
* response (#3229).
|
||||
*
|
||||
* The non-streaming executor path previously fed 4xx/5xx responses into the SSE
|
||||
* collector, which produced a synthetic `{"object":"chat.completion","content":""}`
|
||||
* success envelope — masking the real error. Route non-ok responses through
|
||||
* `buildErrorBody` instead so the client sees a proper error (hard rule #12).
|
||||
*/
|
||||
import { buildErrorBody } from "../utils/error.ts";
|
||||
|
||||
export function buildAntigravityUpstreamError(
|
||||
status: number,
|
||||
statusText: string,
|
||||
rawBody: string
|
||||
) {
|
||||
let upstreamDetails: unknown;
|
||||
try {
|
||||
upstreamDetails = JSON.parse(rawBody);
|
||||
} catch {
|
||||
// upstream body is not JSON (e.g. HTML error page) — omit structured details
|
||||
}
|
||||
const suffix = statusText ? `: ${statusText}` : "";
|
||||
return buildErrorBody(status, `Antigravity upstream error (${status})${suffix}`, upstreamDetails);
|
||||
}
|
||||
41
tests/unit/agy-gemini-400-3229.test.ts
Normal file
41
tests/unit/agy-gemini-400-3229.test.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* #3229 — agy `gemini-3.1-pro-high`/`-low` returned [400] but were MASKED as an empty
|
||||
* `chat.completion` envelope.
|
||||
*
|
||||
* Two parts:
|
||||
* (a) the budget-suffix ids had no alias, so `resolveAntigravityModelId` sent them
|
||||
* verbatim to upstream (which rejects -high/-low for gemini-3.x) → alias to plain.
|
||||
* (b) the non-stream branch fed the 4xx response into the SSE collector, producing a
|
||||
* synthetic `{"object":"chat.completion","content":""}` instead of a real error →
|
||||
* build a proper sanitized error body for non-ok upstream responses.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { resolveAntigravityModelId } from "../../open-sse/config/antigravityModelAliases.ts";
|
||||
import { buildAntigravityUpstreamError } from "../../open-sse/executors/antigravityUpstreamError.ts";
|
||||
|
||||
test("(a) agy gemini-3.1-pro -high/-low budget suffixes alias to the plain upstream id", () => {
|
||||
assert.equal(resolveAntigravityModelId("gemini-3.1-pro-high"), "gemini-3.1-pro");
|
||||
assert.equal(resolveAntigravityModelId("gemini-3.1-pro-low"), "gemini-3.1-pro");
|
||||
// plain id stays plain
|
||||
assert.equal(resolveAntigravityModelId("gemini-3.1-pro"), "gemini-3.1-pro");
|
||||
});
|
||||
|
||||
test("(b) a non-ok upstream response becomes a real error body, not an empty chat.completion", () => {
|
||||
const body = buildAntigravityUpstreamError(
|
||||
400,
|
||||
"Bad Request",
|
||||
JSON.stringify({ error: { code: 400, message: "Model not found: gemini-3.1-pro-high" } })
|
||||
);
|
||||
assert.notEqual((body as { object?: string }).object, "chat.completion");
|
||||
assert.ok(body.error, "must carry an error object");
|
||||
assert.equal(typeof body.error.message, "string");
|
||||
// sanitized: no raw stack traces leaked (hard rule #12)
|
||||
assert.ok(!body.error.message.includes("at /"));
|
||||
|
||||
// non-JSON upstream body still yields a valid error envelope
|
||||
const body2 = buildAntigravityUpstreamError(503, "Service Unavailable", "<html>oops</html>");
|
||||
assert.ok(body2.error);
|
||||
assert.notEqual((body2 as { object?: string }).object, "chat.completion");
|
||||
});
|
||||
Reference in New Issue
Block a user