fix: add gemini 3.1 flash-lite support
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(() => ({}));
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ const DEFAULT_MODEL_ALIASES: Readonly<Record<string, string>> = {
|
||||
// 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"] = {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<Api> | 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<Api> | 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}`);
|
||||
|
||||
Reference in New Issue
Block a user