fix(antigravity): allow cloudcode envelope through messages guard (#7582)

* fix(antigravity): allow cloudcode envelope through guard

* fix(sse): dedupe antigravity source-format detection, shrink chat.ts under cap

resolveChatSourceFormatForPath() in chat.ts duplicated the exact
antigravity-path regex already in detectFormatFromEndpoint()
(open-sse/services/provider.ts) — the added function pushed chat.ts to
1808 lines, over the frozen file-size cap of 1797, with no baseline bump.

Remove the duplicate: add a thin detectFormatFromUrl(body, requestUrl)
wrapper next to detectFormatFromEndpoint (single source of truth for the
path/body-based format detection), and have chat.ts call it directly.
Also drop the now-single-use FORMATS import (compare against the literal
"antigravity", matching the existing convention in chatHelpers.ts) and
remove an unneeded block-scope around the pre-existing #6402 messages
guard (renamed its local to msgBody — a second, separate `const b` block
further down for temperature/top_p/max_tokens/n validation is untouched
and does not collide).

Net effect: chat.ts 1808 -> 1797 lines (exactly at the frozen cap, no
baseline change). Behavior is unchanged — same tests, same guard logic,
same antigravity bypass. Re-verified full green: typecheck:core, eslint,
file-size/complexity/cognitive-complexity/complexity-ratchets/changelog-
integrity/test-discovery gates, and the PR's own regression suites
(chat-messages-validation-6402.test.ts 26/26, mitm-server-antigravity-
route-alias.test.ts 4/4), plus the adjacent format-detection and
chat-pipeline test suites.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
Dongwook
2026-07-19 10:09:43 +09:00
committed by GitHub
parent 6ca35315bb
commit 9fce7d0fbf
4 changed files with 86 additions and 20 deletions

View File

@@ -131,6 +131,12 @@ export function detectFormatFromEndpoint(body, endpointPath = "") {
return detectFormat(body);
}
// Thin wrapper for call sites that only have the full request URL (not the bare endpoint
// path chatCore already threads) — single source of truth stays detectFormatFromEndpoint.
export function detectFormatFromUrl(body, requestUrl) {
return detectFormatFromEndpoint(body, new URL(requestUrl).pathname);
}
// Detect request format from body structure
export function detectFormat(body) {
// OpenAI Responses API:

View File

@@ -338,9 +338,9 @@ function getSqliteDb() {
/**
* Resolve the stored alias override for a source model: `{ model?, reasoningEffort? }`.
* `normalizeAliasMappings` upgrades legacy plain-string mappings (every existing install)
* into the structured shape, so both old and new saves resolve consistently. Returns
* `null` when there is no override at all for this model (passthrough).
* `normalizeAliasMappings` upgrades legacy plain-string mappings into the structured
* shape. The route-only namespace is reserved for client-facing OmniRoute model ids;
* fall back to `mitmAlias` until a route-alias writer is available.
*/
function getMappedOverride(model, agentId = "antigravity") {
return standaloneRoutingShim.resolveMappedOverride(model, agentId, {

View File

@@ -31,7 +31,7 @@ import {
HTTP_STATUS,
ANTIGRAVITY_PRE_RESPONSE_TIMEOUT_CODE,
} from "@omniroute/open-sse/config/constants.ts";
import { getTargetFormat } from "@omniroute/open-sse/services/provider.ts";
import { getTargetFormat, detectFormatFromUrl } from "@omniroute/open-sse/services/provider.ts";
import {
getModelsByProviderId,
getModelTargetFormat,
@@ -253,6 +253,8 @@ export async function handleChat(
// reasoning_effort / reasoning / object-shaped thinking always wins (backward compatible).
body = normalizeReasoningRequest(body);
const sourceFormat = detectFormatFromUrl(body, request.url);
// Early guard: an invalid `messages` field is rejected here with a clear
// OmniRoute-level 400 before any routing or upstream call (#5110, #6402).
// Without this guard, schema-invalid bodies fell through to model resolution
@@ -261,22 +263,20 @@ export async function handleChat(
// - present-but-non-array (null, number, string, object) → 400 (#6402)
// - empty array → 400 ("at least one message is required") (#5110)
// - missing entirely, when the Responses-API `input` discriminator is also
// absent → 400 (#6402). Responses-API requests use `input` (not `messages`)
// and are still unaffected.
{
const b = body as { messages?: unknown; input?: unknown };
if ("messages" in b && !Array.isArray(b.messages)) {
log.warn("CHAT", "Rejecting request with non-array messages");
return errorResponse(HTTP_STATUS.BAD_REQUEST, "messages: Expected array");
}
if (Array.isArray(b.messages) && b.messages.length === 0) {
log.warn("CHAT", "Rejecting request with empty messages array");
return errorResponse(HTTP_STATUS.BAD_REQUEST, "messages: at least one message is required");
}
if (!("messages" in b) && !("input" in b)) {
log.warn("CHAT", "Rejecting request with missing messages");
return errorResponse(HTTP_STATUS.BAD_REQUEST, "messages: Expected array, received undefined");
}
// absent → 400 (#6402). Responses-API requests use `input` (not `messages`),
// and Antigravity requests use a cloudcode `request` envelope.
const msgBody = body as { messages?: unknown; input?: unknown };
if ("messages" in msgBody && !Array.isArray(msgBody.messages)) {
log.warn("CHAT", "Rejecting request with non-array messages");
return errorResponse(HTTP_STATUS.BAD_REQUEST, "messages: Expected array");
}
if (Array.isArray(msgBody.messages) && msgBody.messages.length === 0) {
log.warn("CHAT", "Rejecting request with empty messages array");
return errorResponse(HTTP_STATUS.BAD_REQUEST, "messages: at least one message is required");
}
if (!("messages" in msgBody) && !("input" in msgBody) && sourceFormat !== "antigravity") {
log.warn("CHAT", "Rejecting request with missing messages");
return errorResponse(HTTP_STATUS.BAD_REQUEST, "messages: Expected array, received undefined");
}
// Reject non-string `model` before it reaches downstream code that calls

View File

@@ -1,6 +1,11 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
ANTIGRAVITY_MODEL_ALIASES,
ANTIGRAVITY_PUBLIC_MODELS,
} from "../../open-sse/config/antigravityModelAliases.ts";
import { AGY_PUBLIC_MODELS } from "../../open-sse/config/agyModels.ts";
import { createChatPipelineHarness } from "../integration/_chatPipelineHarness.ts";
// Regression tests for #6402 — schema-invalid `messages` fields fell through
@@ -122,3 +127,58 @@ test("#6402: Responses-API input passes the guard (messages discriminator preser
);
}
});
const ANTIGRAVITY_GEMINI_MODELS = Array.from(
new Set(
[
...ANTIGRAVITY_PUBLIC_MODELS.map((model) => model.id),
...AGY_PUBLIC_MODELS.map((model) => model.id),
...Object.keys(ANTIGRAVITY_MODEL_ALIASES),
...Object.values(ANTIGRAVITY_MODEL_ALIASES),
].filter((model) => /^(gemini(?!-claude)|rev19)/.test(model))
)
).sort();
for (const model of ANTIGRAVITY_GEMINI_MODELS) {
test(`Antigravity cloudcode envelope routes without the missing-messages 400: ${model}`, async () => {
await seedConnection("antigravity", {
accessToken: "ag-access",
providerSpecificData: { projectId: "test-project" },
});
let upstreamCalled = false;
globalThis.fetch = async () => {
upstreamCalled = true;
return new Response(
'data: {"response":{"candidates":[{"content":{"parts":[{"text":"ok"}]},"finishReason":"STOP"}]}}\n\n',
{ status: 200, headers: { "Content-Type": "text/event-stream" } }
);
};
const response = await handleChat(
buildRequest({
url: "http://localhost/v1/antigravity",
body: {
model: `antigravity/${model}`,
project: "projects/test",
request: {
contents: [{ role: "user", parts: [{ text: "Hello" }] }],
},
},
})
);
assert.equal(upstreamCalled, true, "Antigravity request should reach the upstream executor");
assert.equal(response.status, 200, "Antigravity cloudcode envelopes must not return 400");
const body = await response.text();
assert.match(body, /ok/);
});
}
test("Antigravity Gemini-family regression coverage is not accidentally empty", () => {
assert.ok(
ANTIGRAVITY_GEMINI_MODELS.length >= 20,
`expected broad Gemini-family coverage, got ${ANTIGRAVITY_GEMINI_MODELS.length}`
);
});