mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-18 13:14:56 +03:00
Compare commits
3 Commits
fix/10096-
...
fix/10104-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d3dba924a | ||
|
|
af32519841 | ||
|
|
c4ee73dee0 |
@@ -0,0 +1 @@
|
||||
- fix(antigravity): strip trailing model turn for native Gemini requests too, not just Claude (#10104)
|
||||
@@ -442,9 +442,10 @@ function sanitizeAntigravityGeminiRequest(
|
||||
* `"assistant"`). Mirrors the trailing-strip pop-loop already used for Mistral
|
||||
* (#3396), Copilot (#5802), and the CC-bridge in `claudeCodeCompatible.ts`.
|
||||
*
|
||||
* Scoped strictly to the Claude path by the caller (`isClaude` branch only) — native
|
||||
* Gemini models via Antigravity must be unaffected, since Vertex-Claude is the only
|
||||
* documented rejection surface.
|
||||
* Wired in by the caller for both the Claude path (`isClaude`) and native Gemini
|
||||
* models (`isGemini`, #10104) — newer Gemini endpoints reject a trailing `model` turn
|
||||
* with the same "ending with a model turn" class of 400 that Claude hits via Vertex.
|
||||
* Other model families routed through Antigravity are left untouched.
|
||||
*
|
||||
* Guard: never strip `contents` down to empty — an empty `contents` array is itself
|
||||
* an invalid request, so at least one entry (even a lone trailing "model" turn) is
|
||||
@@ -468,6 +469,20 @@ function stripTrailingAntigravityAssistantTurn(
|
||||
return request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Newer Antigravity Gemini chat families reject a request ending on a model turn.
|
||||
* Keep this explicit rather than matching every model containing "gemini": image
|
||||
* generation has a separate request contract, and the older 2.5 family is not part
|
||||
* of the rejection evidence for #10104.
|
||||
*/
|
||||
function isAntigravityGeminiChatModel(upstreamModel: string): boolean {
|
||||
const normalizedModel = upstreamModel.toLowerCase();
|
||||
if (/(?:^|-)image(?:-|$)/.test(normalizedModel)) {
|
||||
return false;
|
||||
}
|
||||
return /^gemini-(?:3(?:\.\d+)?(?:-[a-z0-9-]+)?|pro-agent)$/.test(normalizedModel);
|
||||
}
|
||||
|
||||
// Test-only export so the unit suite can exercise the strip logic directly.
|
||||
export const __test_stripTrailingAntigravityAssistantTurn = stripTrailingAntigravityAssistantTurn;
|
||||
|
||||
@@ -673,6 +688,14 @@ export class AntigravityExecutor extends BaseExecutor {
|
||||
|
||||
const upstreamModel = await cleanModelName(model, modelIdOverride);
|
||||
const isClaude = upstreamModel.toLowerCase().includes("claude");
|
||||
// #10104: newer Gemini endpoints reject a request ending on a `model` turn with
|
||||
// HTTP 400 "Requests ending with a model turn are not supported" — the same
|
||||
// rejection surface Claude hits via Vertex (see stripTrailingAntigravityAssistantTurn's
|
||||
// doc comment above). Native Gemini models routed through Antigravity (`agy/gemini-*`,
|
||||
// e.g. the Gemini 3.x Flash/Pro tiers from PR #8013's catalog) need the same guarded
|
||||
// strip. Scoped to models whose id names Gemini so unrelated model families are
|
||||
// untouched; the strip itself never empties `contents` (see the guard above).
|
||||
const isGemini = isAntigravityGeminiChatModel(upstreamModel);
|
||||
const baseBody = bodyRecord;
|
||||
const normalizedBody = shouldStripCloudCodeThinking(this.provider, upstreamModel)
|
||||
? stripCloudCodeThinkingConfig(baseBody)
|
||||
@@ -736,11 +759,16 @@ export class AntigravityExecutor extends BaseExecutor {
|
||||
: normalizedRequest?.toolConfig,
|
||||
};
|
||||
|
||||
// Note: sanitizeAntigravityGeminiRequest() applies a Claude-only field whitelist
|
||||
// (dropping fields native Gemini requests may legitimately carry), so the Gemini
|
||||
// branch only runs the trailing-turn strip — never the sanitize/whitelist step.
|
||||
const transformedRequest = isClaude
|
||||
? stripTrailingAntigravityAssistantTurn(
|
||||
sanitizeAntigravityGeminiRequest(rawTransformedRequest)
|
||||
)
|
||||
: rawTransformedRequest;
|
||||
: isGemini
|
||||
? stripTrailingAntigravityAssistantTurn(rawTransformedRequest)
|
||||
: rawTransformedRequest;
|
||||
|
||||
applyAntigravityGenerationDefaults(transformedRequest, upstreamModel);
|
||||
|
||||
|
||||
@@ -44,14 +44,42 @@ test("(a) strips a single trailing assistant (model) turn for Claude models", as
|
||||
assert.equal(contents.at(-1)?.role, "user");
|
||||
});
|
||||
|
||||
test("(b) does NOT strip a trailing model turn for non-Claude (native Gemini) models", async () => {
|
||||
test("(b) strips a trailing model turn for native Gemini models too (#10104)", async () => {
|
||||
// Newer Gemini endpoints reject a request ending on a `model` turn with the same
|
||||
// class of 400 Claude hits via Vertex ("Requests ending with a model turn are not
|
||||
// supported"), so native Gemini models routed through Antigravity get the same
|
||||
// guarded strip as the Claude path.
|
||||
const request = await transform("antigravity/gemini-3.1-pro", [
|
||||
{ role: "user", parts: [{ text: "Hello" }] },
|
||||
{ role: "model", parts: [{ text: "Hi there" }] },
|
||||
]);
|
||||
const contents = request.contents as Array<{ role: string }>;
|
||||
assert.equal(contents.length, 2);
|
||||
assert.equal(contents.at(-1)?.role, "model", "native Gemini requests via Antigravity are untouched");
|
||||
assert.equal(contents.length, 1);
|
||||
assert.equal(contents.at(-1)?.role, "user", "transformed native Gemini request must end on user");
|
||||
});
|
||||
|
||||
test("(b2) native Gemini 3.6 Flash tiers get the strip (#10104)", async () => {
|
||||
for (const tier of ["high", "medium", "low"]) {
|
||||
const request = await transform(`antigravity/gemini-3.6-flash-${tier}`, [
|
||||
{ role: "user", parts: [{ text: "Hello" }] },
|
||||
{ role: "model", parts: [{ text: "Hi there" }] }, // trailing model turn -> 400 source
|
||||
]);
|
||||
const contents = request.contents as Array<{ role: string }>;
|
||||
assert.equal(contents.length, 1, `${tier}: trailing model turn should be stripped`);
|
||||
assert.equal(contents.at(-1)?.role, "user", `${tier}: request must end on user`);
|
||||
}
|
||||
});
|
||||
|
||||
test("(b3) image and older Gemini families keep their separate request contract", async () => {
|
||||
for (const model of ["antigravity/gemini-3.1-flash-image", "antigravity/gemini-2.5-flash"]) {
|
||||
const request = await transform(model, [
|
||||
{ role: "user", parts: [{ text: "Hello" }] },
|
||||
{ role: "model", parts: [{ text: "Hi there" }] },
|
||||
]);
|
||||
const contents = request.contents as Array<{ role: string }>;
|
||||
assert.equal(contents.length, 2, `${model}: non-target family must be unchanged`);
|
||||
assert.equal(contents.at(-1)?.role, "model", `${model}: model turn must be preserved`);
|
||||
}
|
||||
});
|
||||
|
||||
test("(c) a Claude conversation already ending on user is unchanged", async () => {
|
||||
|
||||
Reference in New Issue
Block a user