feat(mitm): translate Antigravity cloudcode end-to-end (Gap B) (#4299)

The Antigravity IDE speaks cloudcode (the Gemini payload wrapped under
`request`) and expects a cloudcode reply ({response:{candidates}}). The
AgentBridge proxy forwarded that envelope verbatim to /v1/chat/completions
(OpenAI), which 400s on the missing `messages` field — so the IDE could be
decrypted/intercepted but never actually routed to a provider.

Wire the inbound cloudcode path, reusing the already-registered bidirectional
translators (no new translators needed):

- provider.ts: detectFormatFromEndpoint classifies the /antigravity path as
  sourceFormat "antigravity" (mirrors /messages -> claude), so the pipeline
  translates request antigravity->openai and response openai->antigravity.
- /v1/antigravity route (new): cloudcode-compatible endpoint — just calls
  handleChat (mirrors /v1/messages).
- server.cjs: routes cloudcode envelopes to /v1/antigravity (translates both
  ways) and plain OpenAI bodies to /v1/chat/completions, via a testable shim.

Tests: forward-target shim (cloudcode vs openai routing) + endpoint format
detection. The antigravity<->openai translators are already covered by
translator-antigravity-to-openai / translator-resp-openai-to-antigravity.

Stacked on #4285 (Gap A). Full Antigravity-IDE e2e validates on the next
standalone deploy (provider.ts + the route compile into .next).
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-19 18:01:14 -03:00
committed by GitHub
parent 915991c762
commit 0ab1876008
6 changed files with 191 additions and 1 deletions

View File

@@ -96,6 +96,15 @@ export function detectFormatFromEndpoint(body, endpointPath = "") {
return "claude";
}
// Antigravity/cloudcode-compatible inbound endpoint (D4): the AgentBridge
// proxy forwards the IDE's cloudcode envelope here. Path-based detection
// (mirrors /messages → claude) makes the pipeline translate the request
// antigravity→openai and the response openai→antigravity, so the IDE gets
// a cloudcode reply regardless of which provider actually served it.
if (/\/antigravity(?=\/|:|$)/i.test(path) || /^antigravity(?=\/|:|$)/i.test(path)) {
return "antigravity";
}
if (
/\/(?:chat\/completions|completions)(?=\/|$)/i.test(path) ||
/^(?:chat\/completions|completions)(?=\/|$)/i.test(path)

View File

@@ -0,0 +1,47 @@
import { handleChat } from "@/sse/handlers/chat";
import { initTranslators } from "@omniroute/open-sse/translator/index.ts";
let initialized = false;
/**
* Initialize translators once.
*/
async function ensureInitialized() {
if (!initialized) {
await initTranslators();
initialized = true;
console.log("[SSE] Translators initialized for /v1/antigravity");
}
}
/**
* Handle CORS preflight.
*/
export async function OPTIONS() {
return new Response(null, {
headers: {
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "*",
},
});
}
/**
* POST /v1/antigravity — Antigravity/cloudcode-compatible endpoint.
*
* Accepts the Antigravity IDE's cloudcode envelope
* { model, project, request: { contents, systemInstruction, tools, generationConfig } }
* and returns a cloudcode SSE reply
* { response: { candidates: [...], usageMetadata } }.
*
* `detectFormatFromEndpoint()` classifies the `/antigravity` path as
* sourceFormat "antigravity" (mirrors `/v1/messages` → claude), so `handleChat`
* translates the request antigravity→openai, routes it to the resolved
* provider/model, and translates the response openai→antigravity — reusing the
* already-registered bidirectional translators. The AgentBridge MITM proxy
* (`server.cjs`) forwards the IDE's intercepted cloudcode request here.
*/
export async function POST(request: Request): Promise<Response> {
await ensureInitialized();
return await handleChat(request);
}

View File

@@ -0,0 +1,55 @@
"use strict";
// =========================================================================
// Decide where the standalone proxy (server.cjs) forwards an intercepted,
// decrypted request — and which router endpoint speaks its wire format.
//
// The Antigravity IDE sends a cloudcode envelope:
// { model, project, request: { contents, systemInstruction, tools, ... } }
// and expects a cloudcode SSE reply ({ response: { candidates, ... } }).
// That envelope is forwarded to the antigravity-compatible endpoint
// (/v1/antigravity), where the pipeline translates request antigravity→openai
// and response openai→antigravity, so the IDE gets its own format back
// regardless of which provider actually served it.
//
// Plain OpenAI bodies ({ messages: [...] }) keep going to /v1/chat/completions.
//
// Pure + deterministic so it is unit-testable without the proxy.
// =========================================================================
const CHAT_PATH = "/v1/chat/completions";
const ANTIGRAVITY_PATH = "/v1/antigravity";
/**
* True when the parsed body is an Antigravity/cloudcode envelope (the IDE wraps
* the Gemini-style payload under `request`, with a `contents` array).
*/
function isCloudcodeEnvelope(body) {
return !!(
body &&
typeof body === "object" &&
!Array.isArray(body) &&
body.request &&
typeof body.request === "object" &&
Array.isArray(body.request.contents)
);
}
/**
* Resolve the forward URL + the format the IDE expects back. `baseUrl` is the
* router base (e.g. http://localhost:20128); trailing slashes are trimmed.
*/
function resolveForwardTarget(baseUrl, body) {
const base = String(baseUrl || "").replace(/\/+$/, "");
if (isCloudcodeEnvelope(body)) {
return { url: `${base}${ANTIGRAVITY_PATH}`, format: "antigravity" };
}
return { url: `${base}${CHAT_PATH}`, format: "openai" };
}
module.exports = {
resolveForwardTarget,
isCloudcodeEnvelope,
CHAT_PATH,
ANTIGRAVITY_PATH,
};

View File

@@ -135,6 +135,7 @@ function sanitizeErrorMessage(message) {
const bypassShim = require("./_internal/bypass.cjs");
const ingestShim = require("./_internal/ingest.cjs");
const forwardShim = require("./_internal/forwardTarget.cjs");
// Inspector capture (D4 fallback). The standalone proxy intercepts AgentBridge
// traffic inline (no MitmHandlerBase / agentBridgeHook), so it posts captured
@@ -470,8 +471,17 @@ async function intercept(req, res, bodyBuffer, mappedModel, sourceModel) {
const body = JSON.parse(bodyBuffer.toString());
body.model = mappedModel;
// Gap B — the Antigravity IDE speaks cloudcode (the Gemini payload wrapped
// under `request`) and expects a cloudcode reply. Forward such envelopes to
// the antigravity-compatible endpoint (which translates both directions) so
// the IDE gets its own format back; plain OpenAI bodies still go to
// chat/completions. Without this, cloudcode hits chat/completions and 400s
// on the missing `messages` field.
const forward = forwardShim.resolveForwardTarget(ROUTER_BASE_URL, body);
vlog(1, `[MITM] → forward ${forward.format} ${forward.url}`);
upstreamStartedAt = Date.now();
const response = await fetch(ROUTER_URL, {
const response = await fetch(forward.url, {
method: "POST",
headers: {
"Content-Type": "application/json",

View File

@@ -0,0 +1,24 @@
import test from "node:test";
import assert from "node:assert/strict";
import { detectFormatFromEndpoint } from "@omniroute/open-sse/services/provider.ts";
const ENVELOPE = {
model: "gemini-2.5-pro",
project: "projects/test",
request: { contents: [{ role: "user", parts: [{ text: "oi" }] }] },
};
test("detectFormatFromEndpoint classifies the /v1/antigravity path as antigravity", () => {
assert.equal(detectFormatFromEndpoint(ENVELOPE, "/v1/antigravity"), "antigravity");
assert.equal(detectFormatFromEndpoint(ENVELOPE, "/api/v1/antigravity"), "antigravity");
assert.equal(
detectFormatFromEndpoint(ENVELOPE, "/v1/antigravity:streamGenerateContent"),
"antigravity"
);
});
test("the antigravity carve-out does not disturb the other endpoint formats", () => {
assert.equal(detectFormatFromEndpoint({ messages: [] }, "/v1/messages"), "claude");
assert.equal(detectFormatFromEndpoint({ messages: [] }, "/v1/chat/completions"), "openai");
assert.equal(detectFormatFromEndpoint({ input: "x" }, "/v1/responses"), "openai-responses");
});

View File

@@ -0,0 +1,45 @@
import test from "node:test";
import assert from "node:assert/strict";
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const { resolveForwardTarget, isCloudcodeEnvelope, CHAT_PATH, ANTIGRAVITY_PATH } = require(
"../../src/mitm/_internal/forwardTarget.cjs"
);
const BASE = "http://localhost:20128";
test("cloudcode envelope routes to the antigravity endpoint", () => {
const body = {
model: "gemini-2.5-pro",
project: "projects/test",
request: { contents: [{ role: "user", parts: [{ text: "oi" }] }] },
};
assert.equal(isCloudcodeEnvelope(body), true);
const t = resolveForwardTarget(BASE, body);
assert.equal(t.url, `${BASE}${ANTIGRAVITY_PATH}`);
assert.equal(t.format, "antigravity");
});
test("plain OpenAI body routes to chat/completions", () => {
const body = { model: "gemini-2.5-pro", messages: [{ role: "user", content: "oi" }] };
assert.equal(isCloudcodeEnvelope(body), false);
const t = resolveForwardTarget(BASE, body);
assert.equal(t.url, `${BASE}${CHAT_PATH}`);
assert.equal(t.format, "openai");
});
test("base url trailing slash is trimmed", () => {
const t = resolveForwardTarget("http://localhost:20128/", { messages: [] });
assert.equal(t.url, `http://localhost:20128${CHAT_PATH}`);
});
test("non-envelope shapes are not misclassified as cloudcode", () => {
// request present but contents missing / wrong type
assert.equal(isCloudcodeEnvelope({ request: {} }), false);
assert.equal(isCloudcodeEnvelope({ request: { contents: "nope" } }), false);
assert.equal(isCloudcodeEnvelope({ contents: [{ parts: [] }] }), false); // bare gemini, no envelope
assert.equal(isCloudcodeEnvelope(null), false);
assert.equal(isCloudcodeEnvelope([]), false);
assert.equal(isCloudcodeEnvelope("string"), false);
});