From 9fce7d0fbfe046a687337674559cebdae5f7ab55 Mon Sep 17 00:00:00 2001 From: Dongwook Date: Sun, 19 Jul 2026 10:09:43 +0900 Subject: [PATCH] fix(antigravity): allow cloudcode envelope through messages guard (#7582) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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> --- open-sse/services/provider.ts | 6 ++ src/mitm/server.cjs | 6 +- src/sse/handlers/chat.ts | 34 +++++------ .../chat-messages-validation-6402.test.ts | 60 +++++++++++++++++++ 4 files changed, 86 insertions(+), 20 deletions(-) diff --git a/open-sse/services/provider.ts b/open-sse/services/provider.ts index 9ba0a0acff..5c26af2fcc 100644 --- a/open-sse/services/provider.ts +++ b/open-sse/services/provider.ts @@ -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: diff --git a/src/mitm/server.cjs b/src/mitm/server.cjs index e7fe31c46d..ac8a27d83c 100644 --- a/src/mitm/server.cjs +++ b/src/mitm/server.cjs @@ -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, { diff --git a/src/sse/handlers/chat.ts b/src/sse/handlers/chat.ts index e7975dabc8..10169de513 100644 --- a/src/sse/handlers/chat.ts +++ b/src/sse/handlers/chat.ts @@ -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 diff --git a/tests/unit/chat-messages-validation-6402.test.ts b/tests/unit/chat-messages-validation-6402.test.ts index 4b58f5d4ec..478181e90d 100644 --- a/tests/unit/chat-messages-validation-6402.test.ts +++ b/tests/unit/chat-messages-validation-6402.test.ts @@ -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}` + ); +});