fix(provider): normalize bare gemini-3 Pro model IDs for google-antigravity (#24145)

* fix(provider): normalize bare gemini-3 Pro model IDs for google-antigravity

The Antigravity Cloud Code Assist API requires a thinking-tier suffix
(-low or -high) for all Gemini 3 Pro variants.  When a user configures
a bare model ID like `gemini-3.1-pro`, the API returns a 404 because it
only recognises `gemini-3.1-pro-low` or `gemini-3.1-pro-high`.

Add `normalizeAntigravityModelId()` that appends `-low` (the default
tier) to bare Pro model IDs, and apply it during provider normalisation
for `google-antigravity`.  Also refactor the per-provider model
normalisation into a shared `normalizeProviderModels()` helper.

Closes #24071

Co-authored-by: Cursor <cursoragent@cursor.com>

* Tests: cover antigravity model ID normalization

* Changelog: note antigravity pro tier normalization

* Tests: type antigravity model helper inputs

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
Sid
2026-02-27 07:53:46 +08:00
committed by GitHub
parent 17578d77e1
commit e6be26ef1c
3 changed files with 118 additions and 2 deletions

View File

@@ -71,6 +71,7 @@ Docs: https://docs.openclaw.ai
- Daemon/macOS launchd: forward proxy env vars into supervised service environments, keep LaunchAgent `KeepAlive=true` semantics, and harden restart sequencing to `print -> bootout -> wait old pid exit -> bootstrap -> kickstart`. (#27276) thanks @frankekn.
- Gateway/macOS restart-loop hardening: detect OpenClaw-managed supervisor markers during SIGUSR1 restart handoff, clean stale gateway PIDs before `/restart` launchctl/systemctl triggers, and set LaunchAgent `ThrottleInterval=60` to bound launchd retry storms during lock-release races. Landed from contributor PRs #27655 (@taw0002), #27448 (@Sid-Qin), and #27650 (@kevinWangSheng). (#27605, #27590, #26904, #26736)
- Models/MiniMax auth header defaults: set `authHeader: true` for both onboarding-generated MiniMax API providers and implicit built-in MiniMax (`minimax`, `minimax-portal`) provider templates so first requests no longer fail with MiniMax `401 authentication_error` due to missing `Authorization` header. Landed from contributor PRs #27622 by @riccoyuanft and #27631 by @kevinWangSheng. (#27600, #15303)
- Models/Google Antigravity IDs: normalize bare `gemini-3-pro`, `gemini-3.1-pro`, and `gemini-3-1-pro` model IDs to the default `-low` thinking tier so provider requests no longer fail with 404 when the tier suffix is omitted. (#24145) Thanks @byungsker.
- Auth/Auth profiles: normalize `auth-profiles.json` alias fields (`mode -> type`, `apiKey -> key`) before credential validation so entries copied from `openclaw.json` auth examples are no longer silently dropped. (#26950) thanks @byungsker.
- Models/Google Gemini: treat `google` (Gemini API key auth profile) as a reasoning-tag provider to prevent `<think>` leakage, and add forward-compat model fallback for `google-gemini-cli` `gemini-3.1-pro*` / `gemini-3.1-flash*` IDs to avoid false unknown-model errors. (#26551, #26524) Thanks @byungsker.
- Models/Profile suffix parsing: centralize trailing `@profile` parsing and only treat `@` as a profile separator when it appears after the final `/`, preserving model IDs like `openai/@cf/...` and `openrouter/@preset/...` across `/model` directive parsing and allowlist model resolution, with regression coverage.

View File

@@ -0,0 +1,87 @@
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import {
normalizeAntigravityModelId,
normalizeProviders,
type ProviderConfig,
} from "./models-config.providers.js";
function buildModel(id: string): NonNullable<ProviderConfig["models"]>[number] {
return {
id,
name: id,
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 1,
maxTokens: 1,
};
}
function buildProvider(modelIds: string[]): ProviderConfig {
return {
baseUrl: "https://example.invalid/v1",
api: "openai-completions",
apiKey: "EXAMPLE_KEY",
models: modelIds.map((id) => buildModel(id)),
};
}
describe("normalizeAntigravityModelId", () => {
it.each(["gemini-3-pro", "gemini-3.1-pro", "gemini-3-1-pro"])(
"adds default -low suffix to bare pro id: %s",
(id) => {
expect(normalizeAntigravityModelId(id)).toBe(`${id}-low`);
},
);
it.each([
"gemini-3-pro-low",
"gemini-3-pro-high",
"gemini-3.1-flash",
"claude-opus-4-6-thinking",
])("keeps already-tiered and non-pro ids unchanged: %s", (id) => {
expect(normalizeAntigravityModelId(id)).toBe(id);
});
});
describe("google-antigravity provider normalization", () => {
it("normalizes bare gemini pro IDs only for google-antigravity providers", () => {
const agentDir = mkdtempSync(join(tmpdir(), "openclaw-test-"));
const providers = {
"google-antigravity": buildProvider([
"gemini-3-pro",
"gemini-3.1-pro",
"gemini-3-1-pro",
"gemini-3-pro-high",
"claude-opus-4-6-thinking",
]),
openai: buildProvider(["gpt-5"]),
};
const normalized = normalizeProviders({ providers, agentDir });
expect(normalized).not.toBe(providers);
expect(normalized?.["google-antigravity"]?.models.map((model) => model.id)).toEqual([
"gemini-3-pro-low",
"gemini-3.1-pro-low",
"gemini-3-1-pro-low",
"gemini-3-pro-high",
"claude-opus-4-6-thinking",
]);
expect(normalized?.openai).toBe(providers.openai);
});
it("returns original providers object when no antigravity IDs need normalization", () => {
const agentDir = mkdtempSync(join(tmpdir(), "openclaw-test-"));
const providers = {
"google-antigravity": buildProvider(["gemini-3-pro-low", "claude-opus-4-6-thinking"]),
};
const normalized = normalizeProviders({ providers, agentDir });
expect(normalized).toBe(providers);
});
});

View File

@@ -391,10 +391,22 @@ export function normalizeGoogleModelId(id: string): string {
return id;
}
function normalizeGoogleProvider(provider: ProviderConfig): ProviderConfig {
const ANTIGRAVITY_BARE_PRO_IDS = new Set(["gemini-3-pro", "gemini-3.1-pro", "gemini-3-1-pro"]);
export function normalizeAntigravityModelId(id: string): string {
if (ANTIGRAVITY_BARE_PRO_IDS.has(id)) {
return `${id}-low`;
}
return id;
}
function normalizeProviderModels(
provider: ProviderConfig,
normalizeId: (id: string) => string,
): ProviderConfig {
let mutated = false;
const models = provider.models.map((model) => {
const nextId = normalizeGoogleModelId(model.id);
const nextId = normalizeId(model.id);
if (nextId === model.id) {
return model;
}
@@ -404,6 +416,14 @@ function normalizeGoogleProvider(provider: ProviderConfig): ProviderConfig {
return mutated ? { ...provider, models } : provider;
}
function normalizeGoogleProvider(provider: ProviderConfig): ProviderConfig {
return normalizeProviderModels(provider, normalizeGoogleModelId);
}
function normalizeAntigravityProvider(provider: ProviderConfig): ProviderConfig {
return normalizeProviderModels(provider, normalizeAntigravityModelId);
}
export function normalizeProviders(params: {
providers: ModelsConfig["providers"];
agentDir: string;
@@ -470,6 +490,14 @@ export function normalizeProviders(params: {
normalizedProvider = googleNormalized;
}
if (normalizedKey === "google-antigravity") {
const antigravityNormalized = normalizeAntigravityProvider(normalizedProvider);
if (antigravityNormalized !== normalizedProvider) {
mutated = true;
}
normalizedProvider = antigravityNormalized;
}
next[key] = normalizedProvider;
}