Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions open-sse/services/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
47 changes: 47 additions & 0 deletions src/app/api/v1/antigravity/route.ts
Original file line number Diff line number Diff line change
@@ -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");
}
}
Comment on lines +9 to +15

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since initTranslators is a synchronous function, wrapping it in an async function and using await introduces an unnecessary asynchronous gap (microtask yield) before initialized = true is set. If multiple requests hit this endpoint concurrently at startup, they can all pass the if (!initialized) check and call initTranslators() multiple times. Removing async/await makes the initialization completely synchronous and race-condition free.

Suggested change
async function ensureInitialized() {
if (!initialized) {
await initTranslators();
initialized = true;
console.log("[SSE] Translators initialized for /v1/antigravity");
}
}
function ensureInitialized() {
if (!initialized) {
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);
}
Comment on lines +44 to +47

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since ensureInitialized is now synchronous, we can remove the await keyword when calling it.

Suggested change
export async function POST(request: Request): Promise<Response> {
await ensureInitialized();
return await handleChat(request);
}
export async function POST(request: Request): Promise<Response> {
ensureInitialized();
return await handleChat(request);
}

55 changes: 55 additions & 0 deletions src/mitm/_internal/forwardTarget.cjs
Original file line number Diff line number Diff line change
@@ -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,
};
12 changes: 11 additions & 1 deletion src/mitm/server.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand Down
24 changes: 24 additions & 0 deletions tests/unit/antigravity-format-detection.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
45 changes: 45 additions & 0 deletions tests/unit/mitm-forward-target.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
Loading