mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-05 06:42:12 +03:00
This commit is contained in:
committed by
GitHub
parent
2b0c129ab6
commit
598bebae26
@@ -14,6 +14,7 @@ _In development — bullets added per PR; finalized at release._
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **agent-bridge (antigravity):** unwrap the cloudcode-pa `.request` envelope when converting Antigravity IDE requests. The real IDE sends `cloudcode-pa.googleapis.com/v1internal:generateContent` with the Gemini request nested under `.request` (`{ project, model, request: { contents, systemInstruction, generationConfig } }`), but the bridge read those fields at the top level — yielding an empty conversation, so prompts hung mid-execution. The legacy `/v1beta/models/<model>:generateContent` top-level shape still works (#4294 — thanks @shabeer)
|
||||
- **dashboard:** add a GitHub releases fallback to the "Update Available" lookup. After the v3.8.28 fix added an npm-registry HTTP fallback, the banner could still stay hidden on networks that reach GitHub (where the news feed already loads) but not `registry.npmjs.org`. `resolveLatestVersion()` now tries npm CLI → npm registry → GitHub releases (`/repos/diegosouzapw/OmniRoute/releases/latest`) before giving up, and logs a warning only when all three fail (#4100)
|
||||
- **command-code:** omit `max_tokens` when the client omits it so the upstream applies the model's native default, fixing `400 "expected <=200000"` on `/alpha/generate` for high-cap models; an explicit oversized client value is clamped to the 200k endpoint ceiling (#5221 — thanks @adivekar-utexas)
|
||||
- **combo:** wire session stickiness into the round-robin dispatch path. Multi-turn conversations from clients that send no session id (Codex CLI, Claude Code, most OpenAI-compatible tools) were rotated to a different connection on every turn by round-robin combos, busting the upstream prompt-cache → cold high-reasoning starts, intermittent `504`s and throughput collapse under concurrency. The weighted/priority paths already honored per-conversation stickiness; the round-robin handler returned before reaching it. Round-robin now starts the rotation at the conversation's sticky connection (failover to the other targets is preserved), and different conversations still spread across connections — only intra-conversation rotation is removed (#3825 — thanks @bypanghu, @jpsn123, @xz-dev)
|
||||
|
||||
@@ -43,9 +43,37 @@ interface GeminiRequestBody {
|
||||
systemInstruction?: GeminiContent;
|
||||
contents?: GeminiContent[];
|
||||
generationConfig?: GeminiGenerationConfig;
|
||||
/**
|
||||
* Antigravity IDE talks to `cloudcode-pa.googleapis.com/v1internal:generateContent`,
|
||||
* whose envelope nests the real Gemini request one level down:
|
||||
* `{ project, model, userAgent, requestType, request: { contents, systemInstruction,
|
||||
* generationConfig, … } }`
|
||||
* (see `open-sse/translator/request/antigravity-to-openai.ts`). The legacy
|
||||
* `/v1beta/models/<model>:generateContent` path instead carries those fields at the top
|
||||
* level. We must read whichever level actually holds the conversation (#4294).
|
||||
*/
|
||||
request?: GeminiRequestBody;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the object that actually holds the Gemini conversation fields. Antigravity's
|
||||
* cloudcode-pa envelope wraps them under `.request`; the legacy `/v1beta` path puts them at
|
||||
* the top level. Without this unwrap, a real Antigravity request yields zero messages, so
|
||||
* the upstream gets an empty conversation and the IDE prompt hangs (#4294).
|
||||
*/
|
||||
function resolveGeminiSource(body: GeminiRequestBody): GeminiRequestBody {
|
||||
const inner = body.request;
|
||||
if (
|
||||
inner &&
|
||||
typeof inner === "object" &&
|
||||
("contents" in inner || "systemInstruction" in inner || "generationConfig" in inner)
|
||||
) {
|
||||
return inner;
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
interface OpenAIChatMessage {
|
||||
role: "system" | "user" | "assistant";
|
||||
content: string;
|
||||
@@ -81,16 +109,20 @@ export function convertGeminiToOpenAI(
|
||||
model: string,
|
||||
stream: boolean,
|
||||
): OpenAIChatBody {
|
||||
// Unwrap the cloudcode-pa envelope (`.request`) used by the real Antigravity IDE; fall
|
||||
// back to the top level for the legacy `/v1beta` shape. (#4294)
|
||||
const src = resolveGeminiSource(geminiBody);
|
||||
|
||||
const messages: OpenAIChatMessage[] = [];
|
||||
|
||||
// System instruction
|
||||
if (geminiBody.systemInstruction) {
|
||||
const systemText = joinPartsText(geminiBody.systemInstruction.parts);
|
||||
if (src.systemInstruction) {
|
||||
const systemText = joinPartsText(src.systemInstruction.parts);
|
||||
if (systemText) messages.push({ role: "system", content: systemText });
|
||||
}
|
||||
|
||||
// Chat turns
|
||||
for (const content of geminiBody.contents || []) {
|
||||
for (const content of src.contents || []) {
|
||||
const role: OpenAIChatMessage["role"] = content.role === "model" ? "assistant" : "user";
|
||||
messages.push({ role, content: joinPartsText(content.parts) });
|
||||
}
|
||||
@@ -101,7 +133,7 @@ export function convertGeminiToOpenAI(
|
||||
stream: !!stream,
|
||||
};
|
||||
|
||||
const cfg = geminiBody.generationConfig || {};
|
||||
const cfg = src.generationConfig || {};
|
||||
if (cfg.maxOutputTokens != null) openaiBody.max_tokens = cfg.maxOutputTokens;
|
||||
if (cfg.temperature != null) openaiBody.temperature = cfg.temperature;
|
||||
if (cfg.topP != null) openaiBody.top_p = cfg.topP;
|
||||
|
||||
@@ -97,6 +97,68 @@ test("antigravity handler — converts raw Gemini body before forwarding", async
|
||||
assert.equal(forwarded.thinkingConfig, undefined);
|
||||
});
|
||||
|
||||
test("convertGeminiToOpenAI — unwraps the cloudcode-pa `.request` envelope (#4294)", () => {
|
||||
// Shape the real Antigravity IDE sends to cloudcode-pa /v1internal:generateContent.
|
||||
const out = convertGeminiToOpenAI(
|
||||
{
|
||||
project: "projects/123",
|
||||
model: "gemini-3-pro",
|
||||
userAgent: "Antigravity",
|
||||
requestType: "GENERATE",
|
||||
request: {
|
||||
systemInstruction: { parts: [{ text: "be brief" }] },
|
||||
contents: [
|
||||
{ role: "user", parts: [{ text: "hello" }] },
|
||||
{ role: "model", parts: [{ text: "hi there" }] },
|
||||
],
|
||||
generationConfig: { maxOutputTokens: 256, temperature: 0.4 },
|
||||
},
|
||||
} as Record<string, unknown>,
|
||||
"ag-claude-opus-4-6-thinking",
|
||||
true
|
||||
);
|
||||
|
||||
assert.equal(out.model, "ag-claude-opus-4-6-thinking");
|
||||
// Without the unwrap these would be empty → upstream gets an empty conversation → hang.
|
||||
assert.deepEqual(out.messages, [
|
||||
{ role: "system", content: "be brief" },
|
||||
{ role: "user", content: "hello" },
|
||||
{ role: "assistant", content: "hi there" },
|
||||
]);
|
||||
assert.equal(out.max_tokens, 256);
|
||||
assert.equal(out.temperature, 0.4);
|
||||
});
|
||||
|
||||
test("antigravity handler — forwards a cloudcode envelope request with real messages (#4294)", async () => {
|
||||
const r = await runHandler(
|
||||
new AntigravityHandler(),
|
||||
{
|
||||
project: "projects/123",
|
||||
model: "gemini-3-pro",
|
||||
request: {
|
||||
contents: [{ role: "user", parts: [{ text: "ping" }] }],
|
||||
generationConfig: { maxOutputTokens: 64 },
|
||||
},
|
||||
},
|
||||
"ag-claude-opus-4-6-thinking",
|
||||
{
|
||||
upstreamBody: "data: pong\n\n",
|
||||
url: "/v1internal:streamGenerateContent",
|
||||
}
|
||||
);
|
||||
|
||||
assert.ok(r.fetchCalled);
|
||||
const forwarded = JSON.parse(r.fetchBody);
|
||||
assert.equal(forwarded.model, "ag-claude-opus-4-6-thinking");
|
||||
assert.equal(forwarded.stream, true);
|
||||
// The prompt must survive the conversion (the hang was an empty messages array).
|
||||
assert.deepEqual(forwarded.messages, [{ role: "user", content: "ping" }]);
|
||||
assert.equal(forwarded.max_tokens, 64);
|
||||
// Envelope wrapper fields must not leak into the OpenAI body.
|
||||
assert.equal(forwarded.request, undefined);
|
||||
assert.equal(forwarded.project, undefined);
|
||||
});
|
||||
|
||||
test("antigravity handler — non-streaming URL yields stream:false", async () => {
|
||||
const r = await runHandler(
|
||||
new AntigravityHandler(),
|
||||
|
||||
Reference in New Issue
Block a user