mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
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:
@@ -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:
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}`
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user