diff --git a/CHANGELOG.md b/CHANGELOG.md index 421143bbf0b..76026b802ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ Docs: https://docs.openclaw.ai - iOS/App Store Connect release prep: align iOS bundle identifiers under `ai.openclaw.client`, refresh Watch app icons, add Fastlane metadata/screenshot automation, and support Keychain-backed ASC auth for uploads. (#38936) Thanks @ngutman. - Mattermost/model picker: add Telegram-style interactive provider/model browsing for `/oc_model` and `/oc_models`, fix picker callback updates, and emit a normal confirmation reply when a model is selected. (#38767) thanks @mukhtharcm. - Docker/multi-stage build: restructure Dockerfile as a multi-stage build to produce a minimal runtime image without build tools, source code, or Bun; add `OPENCLAW_VARIANT=slim` build arg for a bookworm-slim variant. (#38479) Thanks @sallyom. +- Google/Gemini 3.1 Flash-Lite: add first-class `google/gemini-3.1-flash-lite-preview` support across model-id normalization, default aliases, media-understanding image lookups, Google Gemini CLI forward-compat fallback, and docs. ### Breaking diff --git a/docs/concepts/model-providers.md b/docs/concepts/model-providers.md index dec03850189..6dd4c2f9c03 100644 --- a/docs/concepts/model-providers.md +++ b/docs/concepts/model-providers.md @@ -104,8 +104,8 @@ OpenClaw ships with the pi‑ai catalog. These providers require **no** - Provider: `google` - Auth: `GEMINI_API_KEY` - Optional rotation: `GEMINI_API_KEYS`, `GEMINI_API_KEY_1`, `GEMINI_API_KEY_2`, `GOOGLE_API_KEY` fallback, and `OPENCLAW_LIVE_GEMINI_KEY` (single override) -- Example models: `google/gemini-3.1-pro-preview`, `google/gemini-3-flash-preview` -- Compatibility: legacy OpenClaw config using `google/gemini-3.1-flash-preview` is normalized to `google/gemini-3-flash-preview` +- Example models: `google/gemini-3.1-pro-preview`, `google/gemini-3-flash-preview`, `google/gemini-3.1-flash-lite-preview` +- Compatibility: legacy OpenClaw config using `google/gemini-3.1-flash-preview` is normalized to `google/gemini-3-flash-preview`, and bare `google/gemini-3.1-flash-lite` is normalized to `google/gemini-3.1-flash-lite-preview` - CLI: `openclaw onboard --auth-choice gemini-api-key` ### Google Vertex, Antigravity, and Gemini CLI diff --git a/docs/gateway/configuration-reference.md b/docs/gateway/configuration-reference.md index ceae117c81a..c69d5a373b0 100644 --- a/docs/gateway/configuration-reference.md +++ b/docs/gateway/configuration-reference.md @@ -910,14 +910,15 @@ Time format in system prompt. Default: `auto` (OS preference). **Built-in alias shorthands** (only apply when the model is in `agents.defaults.models`): -| Alias | Model | -| -------------- | ------------------------------- | -| `opus` | `anthropic/claude-opus-4-6` | -| `sonnet` | `anthropic/claude-sonnet-4-5` | -| `gpt` | `openai/gpt-5.2` | -| `gpt-mini` | `openai/gpt-5-mini` | -| `gemini` | `google/gemini-3.1-pro-preview` | -| `gemini-flash` | `google/gemini-3-flash-preview` | +| Alias | Model | +| ------------------- | -------------------------------------- | +| `opus` | `anthropic/claude-opus-4-6` | +| `sonnet` | `anthropic/claude-sonnet-4-6` | +| `gpt` | `openai/gpt-5.4` | +| `gpt-mini` | `openai/gpt-5-mini` | +| `gemini` | `google/gemini-3.1-pro-preview` | +| `gemini-flash` | `google/gemini-3-flash-preview` | +| `gemini-flash-lite` | `google/gemini-3.1-flash-lite-preview` | Your configured aliases always win over defaults. diff --git a/docs/help/faq.md b/docs/help/faq.md index 2a669c6f683..0ea9c4d92d5 100644 --- a/docs/help/faq.md +++ b/docs/help/faq.md @@ -2238,11 +2238,12 @@ Docs: [Models](/concepts/models), [Multi-Agent Routing](/concepts/multi-agent), Yes. OpenClaw ships a few default shorthands (only applied when the model exists in `agents.defaults.models`): - `opus` → `anthropic/claude-opus-4-6` -- `sonnet` → `anthropic/claude-sonnet-4-5` -- `gpt` → `openai/gpt-5.2` +- `sonnet` → `anthropic/claude-sonnet-4-6` +- `gpt` → `openai/gpt-5.4` - `gpt-mini` → `openai/gpt-5-mini` - `gemini` → `google/gemini-3.1-pro-preview` - `gemini-flash` → `google/gemini-3-flash-preview` +- `gemini-flash-lite` → `google/gemini-3.1-flash-lite-preview` If you set your own alias with the same name, your value wins. diff --git a/src/agents/model-selection.test.ts b/src/agents/model-selection.test.ts index 08ae7f1c7e2..a9029540ee1 100644 --- a/src/agents/model-selection.test.ts +++ b/src/agents/model-selection.test.ts @@ -125,6 +125,17 @@ describe("model-selection", () => { }); }); + it("normalizes gemini 3.1 flash-lite to the preview model id", () => { + expect(parseModelRef("google/gemini-3.1-flash-lite", "openai")).toEqual({ + provider: "google", + model: "gemini-3.1-flash-lite-preview", + }); + expect(parseModelRef("gemini-3.1-flash-lite", "google")).toEqual({ + provider: "google", + model: "gemini-3.1-flash-lite-preview", + }); + }); + it("keeps openai gpt-5.3 codex refs on the openai provider", () => { expect(parseModelRef("openai/gpt-5.3-codex", "anthropic")).toEqual({ provider: "openai", diff --git a/src/agents/models-config.providers.google-antigravity.test.ts b/src/agents/models-config.providers.google-antigravity.test.ts index 2d073cf362e..3886b237e27 100644 --- a/src/agents/models-config.providers.google-antigravity.test.ts +++ b/src/agents/models-config.providers.google-antigravity.test.ts @@ -53,6 +53,10 @@ describe("normalizeGoogleModelId", () => { expect(normalizeGoogleModelId("gemini-3.1-flash")).toBe("gemini-3-flash-preview"); expect(normalizeGoogleModelId("gemini-3.1-flash-preview")).toBe("gemini-3-flash-preview"); }); + + it("adds the preview suffix for gemini 3.1 flash-lite", () => { + expect(normalizeGoogleModelId("gemini-3.1-flash-lite")).toBe("gemini-3.1-flash-lite-preview"); + }); }); describe("google-antigravity provider normalization", () => { diff --git a/src/agents/models-config.providers.ts b/src/agents/models-config.providers.ts index 19e386b0d22..f88df714b72 100644 --- a/src/agents/models-config.providers.ts +++ b/src/agents/models-config.providers.ts @@ -547,6 +547,9 @@ export function normalizeGoogleModelId(id: string): string { if (id === "gemini-3.1-pro") { return "gemini-3.1-pro-preview"; } + if (id === "gemini-3.1-flash-lite") { + return "gemini-3.1-flash-lite-preview"; + } // Preserve compatibility with earlier OpenClaw docs/config that pointed at a // non-existent Gemini Flash preview ID. Google's current Flash text model is // `gemini-3-flash-preview`. diff --git a/src/agents/pi-embedded-runner/model.forward-compat.test.ts b/src/agents/pi-embedded-runner/model.forward-compat.test.ts index 56fd4654e91..310f62d0d32 100644 --- a/src/agents/pi-embedded-runner/model.forward-compat.test.ts +++ b/src/agents/pi-embedded-runner/model.forward-compat.test.ts @@ -89,6 +89,19 @@ describe("pi embedded model e2e smoke", () => { }); }); + it("builds a google-gemini-cli forward-compat fallback for gemini-3.1-flash-lite-preview", () => { + mockGoogleGeminiCliFlashTemplateModel(); + + const result = resolveModel("google-gemini-cli", "gemini-3.1-flash-lite-preview", "/tmp/agent"); + expect(result.error).toBeUndefined(); + expect(result.model).toMatchObject({ + ...GOOGLE_GEMINI_CLI_FLASH_TEMPLATE_MODEL, + id: "gemini-3.1-flash-lite-preview", + name: "gemini-3.1-flash-lite-preview", + reasoning: true, + }); + }); + it("keeps unknown-model errors for unrecognized google-gemini-cli model IDs", () => { const result = resolveModel("google-gemini-cli", "gemini-4-unknown", "/tmp/agent"); expect(result.model).toBeUndefined(); diff --git a/src/cli/daemon-cli/lifecycle.test.ts b/src/cli/daemon-cli/lifecycle.test.ts index f1e87fc4938..3f0ed6d531c 100644 --- a/src/cli/daemon-cli/lifecycle.test.ts +++ b/src/cli/daemon-cli/lifecycle.test.ts @@ -36,16 +36,17 @@ const renderGatewayPortHealthDiagnostics = vi.fn(() => ["diag: unhealthy port"]) const renderRestartDiagnostics = vi.fn(() => ["diag: unhealthy runtime"]); const resolveGatewayPort = vi.fn(() => 18789); const findGatewayPidsOnPortSync = vi.fn<(port: number) => number[]>(() => []); -const probeGateway = vi.fn< - (opts: { - url: string; - auth?: { token?: string; password?: string }; - timeoutMs: number; - }) => Promise<{ - ok: boolean; - configSnapshot: unknown; - }> ->(); +const probeGateway = + vi.fn< + (opts: { + url: string; + auth?: { token?: string; password?: string }; + timeoutMs: number; + }) => Promise<{ + ok: boolean; + configSnapshot: unknown; + }> + >(); const isRestartEnabled = vi.fn<(config?: { commands?: unknown }) => boolean>(() => true); const loadConfig = vi.fn(() => ({})); diff --git a/src/config/defaults.ts b/src/config/defaults.ts index e9e1a02c1f8..b8e20f260a1 100644 --- a/src/config/defaults.ts +++ b/src/config/defaults.ts @@ -30,6 +30,7 @@ const DEFAULT_MODEL_ALIASES: Readonly> = { // Google Gemini (3.x are preview ids in the catalog) gemini: "google/gemini-3.1-pro-preview", "gemini-flash": "google/gemini-3-flash-preview", + "gemini-flash-lite": "google/gemini-3.1-flash-lite-preview", }; const DEFAULT_MODEL_COST: ModelDefinitionConfig["cost"] = { diff --git a/src/config/model-alias-defaults.test.ts b/src/config/model-alias-defaults.test.ts index 1d844b4d1b4..96bcd611233 100644 --- a/src/config/model-alias-defaults.test.ts +++ b/src/config/model-alias-defaults.test.ts @@ -69,6 +69,7 @@ describe("applyModelDefaults", () => { models: { "google/gemini-3.1-pro-preview": { alias: "" }, "google/gemini-3-flash-preview": {}, + "google/gemini-3.1-flash-lite-preview": {}, }, }, }, @@ -80,6 +81,9 @@ describe("applyModelDefaults", () => { expect(next.agents?.defaults?.models?.["google/gemini-3-flash-preview"]?.alias).toBe( "gemini-flash", ); + expect(next.agents?.defaults?.models?.["google/gemini-3.1-flash-lite-preview"]?.alias).toBe( + "gemini-flash-lite", + ); }); it("fills missing model provider defaults", () => { diff --git a/src/media-understanding/providers/image.test.ts b/src/media-understanding/providers/image.test.ts index 80737f3f410..51c8739f43a 100644 --- a/src/media-understanding/providers/image.test.ts +++ b/src/media-understanding/providers/image.test.ts @@ -130,4 +130,104 @@ describe("describeImageWithModel", () => { expect(completeMock).toHaveBeenCalledOnce(); expect(minimaxUnderstandImageMock).not.toHaveBeenCalled(); }); + + it("normalizes deprecated google flash ids before lookup and keeps profile auth selection", async () => { + const findMock = vi.fn((provider: string, modelId: string) => { + expect(provider).toBe("google"); + expect(modelId).toBe("gemini-3-flash-preview"); + return { + provider: "google", + id: "gemini-3-flash-preview", + input: ["text", "image"], + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + }; + }); + discoverModelsMock.mockReturnValue({ find: findMock }); + completeMock.mockResolvedValue({ + role: "assistant", + api: "google-generative-ai", + provider: "google", + model: "gemini-3-flash-preview", + stopReason: "stop", + timestamp: Date.now(), + content: [{ type: "text", text: "flash ok" }], + }); + + const { describeImageWithModel } = await import("./image.js"); + + const result = await describeImageWithModel({ + cfg: {}, + agentDir: "/tmp/openclaw-agent", + provider: "google", + model: "gemini-3.1-flash-preview", + profile: "google:default", + buffer: Buffer.from("png-bytes"), + fileName: "image.png", + mime: "image/png", + prompt: "Describe the image.", + timeoutMs: 1000, + }); + + expect(result).toEqual({ + text: "flash ok", + model: "gemini-3-flash-preview", + }); + expect(findMock).toHaveBeenCalledOnce(); + expect(getApiKeyForModelMock).toHaveBeenCalledWith( + expect.objectContaining({ + profileId: "google:default", + }), + ); + expect(setRuntimeApiKeyMock).toHaveBeenCalledWith("google", "oauth-test"); + }); + + it("normalizes gemini 3.1 flash-lite ids before lookup and keeps profile auth selection", async () => { + const findMock = vi.fn((provider: string, modelId: string) => { + expect(provider).toBe("google"); + expect(modelId).toBe("gemini-3.1-flash-lite-preview"); + return { + provider: "google", + id: "gemini-3.1-flash-lite-preview", + input: ["text", "image"], + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + }; + }); + discoverModelsMock.mockReturnValue({ find: findMock }); + completeMock.mockResolvedValue({ + role: "assistant", + api: "google-generative-ai", + provider: "google", + model: "gemini-3.1-flash-lite-preview", + stopReason: "stop", + timestamp: Date.now(), + content: [{ type: "text", text: "flash lite ok" }], + }); + + const { describeImageWithModel } = await import("./image.js"); + + const result = await describeImageWithModel({ + cfg: {}, + agentDir: "/tmp/openclaw-agent", + provider: "google", + model: "gemini-3.1-flash-lite", + profile: "google:default", + buffer: Buffer.from("png-bytes"), + fileName: "image.png", + mime: "image/png", + prompt: "Describe the image.", + timeoutMs: 1000, + }); + + expect(result).toEqual({ + text: "flash lite ok", + model: "gemini-3.1-flash-lite-preview", + }); + expect(findMock).toHaveBeenCalledOnce(); + expect(getApiKeyForModelMock).toHaveBeenCalledWith( + expect.objectContaining({ + profileId: "google:default", + }), + ); + expect(setRuntimeApiKeyMock).toHaveBeenCalledWith("google", "oauth-test"); + }); }); diff --git a/src/media-understanding/providers/image.ts b/src/media-understanding/providers/image.ts index 79c36292f0c..1511a7c9bb9 100644 --- a/src/media-understanding/providers/image.ts +++ b/src/media-understanding/providers/image.ts @@ -2,6 +2,7 @@ import type { Api, Context, Model } from "@mariozechner/pi-ai"; import { complete } from "@mariozechner/pi-ai"; import { isMinimaxVlmModel, minimaxUnderstandImage } from "../../agents/minimax-vlm.js"; import { getApiKeyForModel, requireApiKey } from "../../agents/model-auth.js"; +import { normalizeModelRef } from "../../agents/model-selection.js"; import { ensureOpenClawModelsJson } from "../../agents/models-config.js"; import { coerceImageAssistantText } from "../../agents/tools/image-tool.helpers.js"; import type { ImageDescriptionRequest, ImageDescriptionResult } from "../types.js"; @@ -22,9 +23,11 @@ export async function describeImageWithModel( const { discoverAuthStorage, discoverModels } = await loadPiModelDiscoveryRuntime(); const authStorage = discoverAuthStorage(params.agentDir); const modelRegistry = discoverModels(authStorage, params.agentDir); - const model = modelRegistry.find(params.provider, params.model) as Model | null; + // Keep direct media config entries compatible with deprecated provider model aliases. + const resolvedRef = normalizeModelRef(params.provider, params.model); + const model = modelRegistry.find(resolvedRef.provider, resolvedRef.model) as Model | null; if (!model) { - throw new Error(`Unknown model: ${params.provider}/${params.model}`); + throw new Error(`Unknown model: ${resolvedRef.provider}/${resolvedRef.model}`); } if (!model.input?.includes("image")) { throw new Error(`Model does not support images: ${params.provider}/${params.model}`);