From 3fa70f3044fe9ee63f7b08daef8862e57098b204 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 5 Apr 2026 10:43:20 +0100 Subject: [PATCH] fix(google): support gemini cli 2.5 model ids (#61261) * fix(google): realign gemini cli model defaults * fix(google): keep gemini cli defaults while adding 2.5 support * fix(google): preserve gemini template reasoning flags * fix(google): fall back to cli templates for gemini 2.5 ids * fix(google): keep gemini cli 3.1 clones local --- CHANGELOG.md | 1 + extensions/google/gemini-cli-provider.ts | 8 +- extensions/google/index.ts | 10 +- extensions/google/provider-models.test.ts | 129 ++++++++++++++-- extensions/google/provider-models.ts | 174 ++++++++++++++++------ 5 files changed, 263 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87c1cb3616f..9d93dcd1088 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -137,6 +137,7 @@ Docs: https://docs.openclaw.ai - Heartbeat: skip wake delivery when the target session lane is already busy so the pending event is retried instead of getting drained too early. (#40526) Thanks @lucky7323. - Plugin SDK/context engines: export the missing context-engine result and subagent lifecycle types from `openclaw/plugin-sdk` so context engine plugins can type `ContextEngine` implementations without local workarounds. (#61251) Thanks @DaevMithran. - Agents/errors: surface an explicit disk-full message when local session or transcript writes fail with `ENOSPC`/`disk full`, so those runs stop degrading into opaque `NO_REPLY`-style failures. Thanks @vincentkoc. +- Google Gemini CLI models: add forward-compat support for stable `gemini-2.5-*` model ids by letting the bundled CLI provider clone them from Google templates, so `gemini-2.5-flash-lite` and related configured models stop showing up as missing. (#35274) Thanks @mySebbe. - Telegram/reasoning: only create a Telegram reasoning preview lane when the session is explicitly `reasoning:stream`, so hidden `` traces from streamed replies stop surfacing as chat previews on normal sessions. Thanks @vincentkoc. - Feishu/reasoning: only expose streamed reasoning previews when the session is explicitly `reasoning:stream`, so hidden reasoning traces do not surface on normal streaming sessions. Thanks @vincentkoc. diff --git a/extensions/google/gemini-cli-provider.ts b/extensions/google/gemini-cli-provider.ts index e8778aaa511..50d02cffe58 100644 --- a/extensions/google/gemini-cli-provider.ts +++ b/extensions/google/gemini-cli-provider.ts @@ -9,7 +9,7 @@ import { buildProviderStreamFamilyHooks } from "openclaw/plugin-sdk/provider-str import { buildProviderToolCompatFamilyHooks } from "openclaw/plugin-sdk/provider-tools"; import { fetchGeminiUsage } from "openclaw/plugin-sdk/provider-usage"; import { formatGoogleOauthApiKey, parseGoogleUsageToken } from "./oauth-token-shared.js"; -import { isModernGoogleModel, resolveGoogle31ForwardCompatModel } from "./provider-models.js"; +import { isModernGoogleModel, resolveGoogleGeminiForwardCompatModel } from "./provider-models.js"; const PROVIDER_ID = "google-gemini-cli"; const PROVIDER_LABEL = "Gemini CLI OAuth"; @@ -106,7 +106,11 @@ export function registerGoogleGeminiCliProvider(api: OpenClawPluginApi) { }, }, resolveDynamicModel: (ctx) => - resolveGoogle31ForwardCompatModel({ providerId: PROVIDER_ID, ctx }), + resolveGoogleGeminiForwardCompatModel({ + providerId: PROVIDER_ID, + templateProviderId: "google", + ctx, + }), ...GOOGLE_GEMINI_CLI_PROVIDER_HOOKS, isModernModelRef: ({ modelId }) => isModernGoogleModel(modelId), formatApiKey: (cred) => formatGoogleOauthApiKey(cred), diff --git a/extensions/google/index.ts b/extensions/google/index.ts index c924c0bc36e..194a6f2bdb7 100644 --- a/extensions/google/index.ts +++ b/extensions/google/index.ts @@ -20,7 +20,7 @@ import { } from "./api.js"; import { buildGoogleGeminiCliBackend } from "./cli-backend.js"; import { formatGoogleOauthApiKey } from "./oauth-token-shared.js"; -import { isModernGoogleModel, resolveGoogle31ForwardCompatModel } from "./provider-models.js"; +import { isModernGoogleModel, resolveGoogleGeminiForwardCompatModel } from "./provider-models.js"; import { createGeminiWebSearchProvider } from "./src/gemini-web-search-provider.js"; const GOOGLE_GEMINI_CLI_PROVIDER_ID = "google-gemini-cli"; @@ -138,7 +138,11 @@ function createLazyGoogleGeminiCliProvider(): ProviderPlugin { }, normalizeModelId: ({ modelId }) => normalizeGoogleModelId(modelId), resolveDynamicModel: (ctx) => - resolveGoogle31ForwardCompatModel({ providerId: GOOGLE_GEMINI_CLI_PROVIDER_ID, ctx }), + resolveGoogleGeminiForwardCompatModel({ + providerId: GOOGLE_GEMINI_CLI_PROVIDER_ID, + templateProviderId: "google", + ctx, + }), ...GOOGLE_GEMINI_PROVIDER_HOOKS_WITH_TOOL_COMPAT, isModernModelRef: ({ modelId }) => isModernGoogleModel(modelId), formatApiKey: (cred) => formatGoogleOauthApiKey(cred), @@ -248,7 +252,7 @@ export default definePluginEntry({ normalizeGoogleProviderConfig(provider, providerConfig), normalizeModelId: ({ modelId }) => normalizeGoogleModelId(modelId), resolveDynamicModel: (ctx) => - resolveGoogle31ForwardCompatModel({ + resolveGoogleGeminiForwardCompatModel({ providerId: ctx.provider, templateProviderId: GOOGLE_GEMINI_CLI_PROVIDER_ID, ctx, diff --git a/extensions/google/provider-models.test.ts b/extensions/google/provider-models.test.ts index 4ceb7dee02f..5ec5d4d79c4 100644 --- a/extensions/google/provider-models.test.ts +++ b/extensions/google/provider-models.test.ts @@ -4,7 +4,7 @@ import type { ProviderRuntimeModel, } from "openclaw/plugin-sdk/plugin-entry"; import { describe, expect, it } from "vitest"; -import { resolveGoogle31ForwardCompatModel } from "./provider-models.js"; +import { isModernGoogleModel, resolveGoogleGeminiForwardCompatModel } from "./provider-models.js"; function createTemplateModel( provider: string, @@ -50,9 +50,54 @@ function createContext(params: { }; } -describe("resolveGoogle31ForwardCompatModel", () => { +describe("resolveGoogleGeminiForwardCompatModel", () => { + it("resolves stable gemini 2.5 flash-lite from direct google templates for Gemini CLI when available", () => { + const model = resolveGoogleGeminiForwardCompatModel({ + providerId: "google-gemini-cli", + templateProviderId: "google", + ctx: createContext({ + provider: "google-gemini-cli", + modelId: "gemini-2.5-flash-lite", + models: [createTemplateModel("google", "gemini-2.5-flash-lite")], + }), + }); + + expect(model).toMatchObject({ + provider: "google-gemini-cli", + id: "gemini-2.5-flash-lite", + api: "google-generative-ai", + reasoning: false, + }); + }); + + it("resolves stable gemini 2.5 flash-lite from Gemini CLI templates when direct google templates are unavailable", () => { + const model = resolveGoogleGeminiForwardCompatModel({ + providerId: "google-gemini-cli", + templateProviderId: "google", + ctx: createContext({ + provider: "google-gemini-cli", + modelId: "gemini-2.5-flash-lite", + models: [ + createTemplateModel("google-gemini-cli", "gemini-3.1-flash-lite-preview", { + contextWindow: 1_048_576, + api: "google-gemini-cli", + baseUrl: "https://cloudcode-pa.googleapis.com", + }), + ], + }), + }); + + expect(model).toMatchObject({ + provider: "google-gemini-cli", + id: "gemini-2.5-flash-lite", + api: "google-gemini-cli", + contextWindow: 1_048_576, + reasoning: false, + }); + }); + it("resolves gemini 3.1 pro for google aliases via an alternate template provider", () => { - const model = resolveGoogle31ForwardCompatModel({ + const model = resolveGoogleGeminiForwardCompatModel({ providerId: "google-vertex", templateProviderId: "google-gemini-cli", ctx: createContext({ @@ -66,18 +111,76 @@ describe("resolveGoogle31ForwardCompatModel", () => { provider: "google-vertex", id: "gemini-3.1-pro-preview", api: "google-gemini-cli", - reasoning: true, + reasoning: false, }); }); - it("resolves gemini 3.1 flash from direct google templates", () => { - const model = resolveGoogle31ForwardCompatModel({ + it("keeps Gemini CLI 3.1 clones sourced from CLI templates when both catalogs exist", () => { + const model = resolveGoogleGeminiForwardCompatModel({ + providerId: "google-gemini-cli", + templateProviderId: "google", + ctx: createContext({ + provider: "google-gemini-cli", + modelId: "gemini-3.1-pro-preview", + models: [ + createTemplateModel("google-gemini-cli", "gemini-3-pro-preview", { + api: "google-gemini-cli", + baseUrl: "https://cloudcode-pa.googleapis.com", + contextWindow: 1_048_576, + }), + createTemplateModel("google", "gemini-3-pro-preview", { + api: "google-generative-ai", + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + contextWindow: 200_000, + }), + ], + }), + }); + + expect(model).toMatchObject({ + provider: "google-gemini-cli", + id: "gemini-3.1-pro-preview", + api: "google-gemini-cli", + baseUrl: "https://cloudcode-pa.googleapis.com", + contextWindow: 1_048_576, + }); + }); + + it("preserves template reasoning metadata instead of forcing it on forward-compat clones", () => { + const model = resolveGoogleGeminiForwardCompatModel({ providerId: "google", templateProviderId: "google-gemini-cli", ctx: createContext({ provider: "google", modelId: "gemini-3.1-flash-preview", - models: [createTemplateModel("google", "gemini-3-flash-preview")], + models: [ + createTemplateModel("google-gemini-cli", "gemini-3-flash-preview", { + reasoning: true, + }), + ], + }), + }); + + expect(model).toMatchObject({ + provider: "google", + id: "gemini-3.1-flash-preview", + api: "google-gemini-cli", + reasoning: true, + }); + }); + + it("resolves gemini 3.1 flash from direct google templates", () => { + const model = resolveGoogleGeminiForwardCompatModel({ + providerId: "google", + templateProviderId: "google-gemini-cli", + ctx: createContext({ + provider: "google", + modelId: "gemini-3.1-flash-preview", + models: [ + createTemplateModel("google", "gemini-3-flash-preview", { + reasoning: false, + }), + ], }), }); @@ -85,12 +188,12 @@ describe("resolveGoogle31ForwardCompatModel", () => { provider: "google", id: "gemini-3.1-flash-preview", api: "google-generative-ai", - reasoning: true, + reasoning: false, }); }); it("prefers the flash-lite template before the broader flash prefix", () => { - const model = resolveGoogle31ForwardCompatModel({ + const model = resolveGoogleGeminiForwardCompatModel({ providerId: "google-vertex", templateProviderId: "google-gemini-cli", ctx: createContext({ @@ -111,7 +214,13 @@ describe("resolveGoogle31ForwardCompatModel", () => { provider: "google-vertex", id: "gemini-3.1-flash-lite-preview", contextWindow: 1_048_576, - reasoning: true, + reasoning: false, }); }); + + it("treats gemini 2.5 ids as modern google models", () => { + expect(isModernGoogleModel("gemini-2.5-pro")).toBe(true); + expect(isModernGoogleModel("gemini-2.5-flash-lite")).toBe(true); + expect(isModernGoogleModel("gemini-1.5-pro")).toBe(false); + }); }); diff --git a/extensions/google/provider-models.ts b/extensions/google/provider-models.ts index 16c4b31e2c7..7b4a7ec3985 100644 --- a/extensions/google/provider-models.ts +++ b/extensions/google/provider-models.ts @@ -4,35 +4,149 @@ import type { } from "openclaw/plugin-sdk/plugin-entry"; import { cloneFirstTemplateModel } from "openclaw/plugin-sdk/provider-model-shared"; +const GOOGLE_GEMINI_CLI_PROVIDER_ID = "google-gemini-cli"; +const GEMINI_2_5_PRO_PREFIX = "gemini-2.5-pro"; +const GEMINI_2_5_FLASH_LITE_PREFIX = "gemini-2.5-flash-lite"; +const GEMINI_2_5_FLASH_PREFIX = "gemini-2.5-flash"; const GEMINI_3_1_PRO_PREFIX = "gemini-3.1-pro"; const GEMINI_3_1_FLASH_LITE_PREFIX = "gemini-3.1-flash-lite"; const GEMINI_3_1_FLASH_PREFIX = "gemini-3.1-flash"; +const GEMINI_2_5_PRO_TEMPLATE_IDS = ["gemini-2.5-pro"] as const; +const GEMINI_2_5_FLASH_LITE_TEMPLATE_IDS = ["gemini-2.5-flash-lite"] as const; +const GEMINI_2_5_FLASH_TEMPLATE_IDS = ["gemini-2.5-flash"] as const; const GEMINI_3_1_PRO_TEMPLATE_IDS = ["gemini-3-pro-preview"] as const; const GEMINI_3_1_FLASH_LITE_TEMPLATE_IDS = ["gemini-3.1-flash-lite-preview"] as const; const GEMINI_3_1_FLASH_TEMPLATE_IDS = ["gemini-3-flash-preview"] as const; -function cloneFirstGoogleTemplateModel(params: { +type GoogleForwardCompatFamily = { + googleTemplateIds: readonly string[]; + cliTemplateIds: readonly string[]; + preferExternalFirstForCli?: boolean; +}; + +type GoogleTemplateSource = { + templateProviderId: string; + templateIds: readonly string[]; +}; + +function cloneGoogleTemplateModel(params: { providerId: string; - templateProviderId?: string; modelId: string; + templateProviderId: string; templateIds: readonly string[]; ctx: ProviderResolveDynamicModelContext; patch?: Partial; }): ProviderRuntimeModel | undefined { - const templateProviderIds = [params.providerId, params.templateProviderId] - .map((providerId) => providerId?.trim()) - .filter((providerId): providerId is string => Boolean(providerId)); + return cloneFirstTemplateModel({ + providerId: params.templateProviderId, + modelId: params.modelId, + templateIds: params.templateIds, + ctx: params.ctx, + patch: { + ...params.patch, + provider: params.providerId, + }, + }); +} - for (const templateProviderId of new Set(templateProviderIds)) { - const model = cloneFirstTemplateModel({ - providerId: templateProviderId, - modelId: params.modelId, - templateIds: params.templateIds, +function isGoogleGeminiCliProvider(providerId: string): boolean { + return providerId.trim().toLowerCase() === GOOGLE_GEMINI_CLI_PROVIDER_ID; +} + +function templateIdsForProvider( + templateProviderId: string, + family: GoogleForwardCompatFamily, +): readonly string[] { + return isGoogleGeminiCliProvider(templateProviderId) + ? family.cliTemplateIds + : family.googleTemplateIds; +} + +function buildGoogleTemplateSources(params: { + providerId: string; + templateProviderId?: string; + family: GoogleForwardCompatFamily; +}): GoogleTemplateSource[] { + const preferredExternalFirst = + isGoogleGeminiCliProvider(params.providerId) && + params.family.preferExternalFirstForCli === true; + const orderedTemplateProviderIds = preferredExternalFirst + ? [params.templateProviderId, params.providerId] + : [params.providerId, params.templateProviderId]; + + const seen = new Set(); + const sources: GoogleTemplateSource[] = []; + for (const providerId of orderedTemplateProviderIds) { + const trimmed = providerId?.trim(); + if (!trimmed || seen.has(trimmed)) { + continue; + } + seen.add(trimmed); + sources.push({ + templateProviderId: trimmed, + templateIds: templateIdsForProvider(trimmed, params.family), + }); + } + return sources; +} + +export function resolveGoogleGeminiForwardCompatModel(params: { + providerId: string; + templateProviderId?: string; + ctx: ProviderResolveDynamicModelContext; +}): ProviderRuntimeModel | undefined { + const trimmed = params.ctx.modelId.trim(); + const lower = trimmed.toLowerCase(); + + let family: GoogleForwardCompatFamily; + if (lower.startsWith(GEMINI_2_5_PRO_PREFIX)) { + family = { + googleTemplateIds: GEMINI_2_5_PRO_TEMPLATE_IDS, + cliTemplateIds: GEMINI_3_1_PRO_TEMPLATE_IDS, + preferExternalFirstForCli: true, + }; + } else if (lower.startsWith(GEMINI_2_5_FLASH_LITE_PREFIX)) { + family = { + googleTemplateIds: GEMINI_2_5_FLASH_LITE_TEMPLATE_IDS, + cliTemplateIds: GEMINI_3_1_FLASH_LITE_TEMPLATE_IDS, + preferExternalFirstForCli: true, + }; + } else if (lower.startsWith(GEMINI_2_5_FLASH_PREFIX)) { + family = { + googleTemplateIds: GEMINI_2_5_FLASH_TEMPLATE_IDS, + cliTemplateIds: GEMINI_3_1_FLASH_TEMPLATE_IDS, + preferExternalFirstForCli: true, + }; + } else if (lower.startsWith(GEMINI_3_1_PRO_PREFIX)) { + family = { + googleTemplateIds: GEMINI_3_1_PRO_TEMPLATE_IDS, + cliTemplateIds: GEMINI_3_1_PRO_TEMPLATE_IDS, + }; + } else if (lower.startsWith(GEMINI_3_1_FLASH_LITE_PREFIX)) { + family = { + googleTemplateIds: GEMINI_3_1_FLASH_LITE_TEMPLATE_IDS, + cliTemplateIds: GEMINI_3_1_FLASH_LITE_TEMPLATE_IDS, + }; + } else if (lower.startsWith(GEMINI_3_1_FLASH_PREFIX)) { + family = { + googleTemplateIds: GEMINI_3_1_FLASH_TEMPLATE_IDS, + cliTemplateIds: GEMINI_3_1_FLASH_TEMPLATE_IDS, + }; + } else { + return undefined; + } + + for (const source of buildGoogleTemplateSources({ + providerId: params.providerId, + templateProviderId: params.templateProviderId, + family, + })) { + const model = cloneGoogleTemplateModel({ + providerId: params.providerId, + modelId: trimmed, + templateProviderId: source.templateProviderId, + templateIds: source.templateIds, ctx: params.ctx, - patch: { - ...params.patch, - provider: params.providerId, - }, }); if (model) { return model; @@ -42,35 +156,7 @@ function cloneFirstGoogleTemplateModel(params: { return undefined; } -export function resolveGoogle31ForwardCompatModel(params: { - providerId: string; - templateProviderId?: string; - ctx: ProviderResolveDynamicModelContext; -}): ProviderRuntimeModel | undefined { - const trimmed = params.ctx.modelId.trim(); - const lower = trimmed.toLowerCase(); - - let templateIds: readonly string[]; - if (lower.startsWith(GEMINI_3_1_PRO_PREFIX)) { - templateIds = GEMINI_3_1_PRO_TEMPLATE_IDS; - } else if (lower.startsWith(GEMINI_3_1_FLASH_LITE_PREFIX)) { - templateIds = GEMINI_3_1_FLASH_LITE_TEMPLATE_IDS; - } else if (lower.startsWith(GEMINI_3_1_FLASH_PREFIX)) { - templateIds = GEMINI_3_1_FLASH_TEMPLATE_IDS; - } else { - return undefined; - } - - return cloneFirstGoogleTemplateModel({ - providerId: params.providerId, - templateProviderId: params.templateProviderId, - modelId: trimmed, - templateIds, - ctx: params.ctx, - patch: { reasoning: true }, - }); -} - export function isModernGoogleModel(modelId: string): boolean { - return modelId.trim().toLowerCase().startsWith("gemini-3"); + const lower = modelId.trim().toLowerCase(); + return lower.startsWith("gemini-2.5") || lower.startsWith("gemini-3"); }