diff --git a/CHANGELOG.md b/CHANGELOG.md index ae85fa62472..a7f3f685f3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 `` 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. diff --git a/src/agents/models-config.providers.google-antigravity.test.ts b/src/agents/models-config.providers.google-antigravity.test.ts new file mode 100644 index 00000000000..51fe5fb32e0 --- /dev/null +++ b/src/agents/models-config.providers.google-antigravity.test.ts @@ -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[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); + }); +}); diff --git a/src/agents/models-config.providers.ts b/src/agents/models-config.providers.ts index b4b5d810293..584b340ea11 100644 --- a/src/agents/models-config.providers.ts +++ b/src/agents/models-config.providers.ts @@ -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; }