fix: normalize gemini 3 pro preview config

This commit is contained in:
Peter Steinberger
2026-05-08 06:07:01 +01:00
parent ff80167e5a
commit d4eb40248a
9 changed files with 96 additions and 16 deletions

View File

@@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
### Changes
- Google/Gemini: normalize retired `google/gemini-3-pro-preview` catalog selections to `google/gemini-3.1-pro-preview` before they are written to model config.
- Control UI: read the Quick Settings exec policy badge from `tools.exec.security` instead of the non-schema `agents.defaults.exec.security` path, so configured `full`/`deny` values render accurately. Fixes #78311. Thanks @FriedBack.
- Control UI/usage: add transcript-backed historical lineage rollups for rotated logical sessions, with current-instance vs historical-lineage scope controls and long-range presets so usage history stays visible after restarts and updates. Fixes #50701. Thanks @dev-gideon-llc and @BunsDev.
- Agents/failover: harden state-aware lane suspension by persisting quota resume transitions, restoring configured lane concurrency, preserving non-quota failure reasons, and exporting model failover events through diagnostics OTLP. Thanks @BunsDev.

View File

@@ -2,6 +2,14 @@ import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
type GoogleManifest = {
modelIdNormalization?: {
providers?: Record<
string,
{
aliases?: Record<string, string>;
}
>;
};
modelCatalog?: {
suppressions?: Array<{
provider?: string;
@@ -83,4 +91,14 @@ describe("google manifest model catalog", () => {
expect(suppressionRefs).not.toContain("google/gemini-2.5-pro");
expect(suppressionRefs).not.toContain("google/gemini-3.1-pro-preview");
});
it("normalizes retired Gemini 3 Pro aliases for all Google chat providers", () => {
const manifest = loadManifest();
for (const provider of GOOGLE_CHAT_PROVIDERS) {
expect(manifest.modelIdNormalization?.providers?.[provider]?.aliases).toMatchObject({
"gemini-3-pro": "gemini-3.1-pro-preview",
});
}
});
});

View File

@@ -18,6 +18,16 @@
"gemini-3.1-flash-preview": "gemini-3-flash-preview"
}
},
"google-gemini-cli": {
"aliases": {
"gemini-3-pro": "gemini-3.1-pro-preview",
"gemini-3-flash": "gemini-3-flash-preview",
"gemini-3.1-pro": "gemini-3.1-pro-preview",
"gemini-3.1-flash-lite": "gemini-3.1-flash-lite-preview",
"gemini-3.1-flash": "gemini-3-flash-preview",
"gemini-3.1-flash-preview": "gemini-3-flash-preview"
}
},
"google-vertex": {
"aliases": {
"gemini-3-pro": "gemini-3.1-pro-preview",

View File

@@ -1,3 +1,4 @@
import { normalizeGooglePreviewModelId } from "../plugin-sdk/provider-model-id-normalize.js";
import { normalizeProviderModelIdWithManifest } from "../plugins/manifest-model-id-normalization.js";
import type { PluginManifestRecord } from "../plugins/manifest-registry.js";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
@@ -32,19 +33,27 @@ export function normalizeStaticProviderModelId(
manifestPlugins?: readonly Pick<PluginManifestRecord, "modelIdNormalization">[];
} = {},
): string {
const normalizedProvider = normalizeProviderId(provider);
if (options.allowManifestNormalization === false) {
return model;
return normalizeBuiltInProviderModelId(normalizedProvider, model);
}
return (
const manifestModelId =
normalizeProviderModelIdWithManifest({
provider,
provider: normalizedProvider,
plugins: options.manifestPlugins,
context: {
provider,
provider: normalizedProvider,
modelId: model,
},
}) ?? model
);
}) ?? model;
return normalizeBuiltInProviderModelId(normalizedProvider, manifestModelId);
}
function normalizeBuiltInProviderModelId(provider: string, model: string): string {
if (provider === "google" || provider === "google-gemini-cli" || provider === "google-vertex") {
return normalizeGooglePreviewModelId(model);
}
return model;
}
function parseStaticModelRef(raw: string, defaultProvider: string): StaticModelRef | null {

View File

@@ -311,6 +311,18 @@ describe("model-selection", () => {
defaultProvider: "google",
expected: { provider: "google", model: "gemini-3-flash-preview" },
},
{
name: "normalizes retired google gemini 3 pro preview ids",
variants: ["google/gemini-3-pro-preview", "gemini-3-pro-preview"],
defaultProvider: "google",
expected: { provider: "google", model: "gemini-3.1-pro-preview" },
},
{
name: "normalizes retired gemini cli 3 pro preview ids",
variants: ["google-gemini-cli/gemini-3-pro-preview"],
defaultProvider: "google",
expected: { provider: "google-gemini-cli", model: "gemini-3.1-pro-preview" },
},
{
name: "normalizes gemini 3.1 flash-lite ids",
variants: ["google/gemini-3.1-flash-lite", "gemini-3.1-flash-lite"],

View File

@@ -303,11 +303,38 @@ describe("promptDefaultModel", () => {
expect(optionValues).toEqual([
"openai/gpt-5.5",
"anthropic/claude-sonnet-4-6",
"google/gemini-3-pro-preview",
"google/gemini-3.1-pro-preview",
"openai-codex/gpt-5.5",
]);
});
it("normalizes retired Google Gemini catalog rows before saving config", async () => {
loadModelCatalog.mockResolvedValue([
{ provider: "google", id: "gemini-3-pro-preview", name: "Gemini 3 Pro" },
]);
const select = vi.fn(async (params) => params.options[0]?.value as never);
const prompter = makePrompter({ select });
const result = await promptDefaultModel({
config: { agents: { defaults: {} } } as OpenClawConfig,
prompter,
allowKeep: false,
includeManual: false,
ignoreAllowlist: true,
});
expect(result.model).toBe("google/gemini-3.1-pro-preview");
expect(select.mock.calls[0]?.[0]?.options).toEqual([
expect.objectContaining({ value: "google/gemini-3.1-pro-preview" }),
]);
expect(runProviderModelSelectedHook).toHaveBeenCalledWith(
expect.objectContaining({
model: "google/gemini-3.1-pro-preview",
}),
);
});
it("uses configured provider models for default picker without loading the full catalog in replace mode", async () => {
loadModelCatalog.mockResolvedValue([
{ provider: "openai", id: "gpt-5.5", name: "GPT-5.5" },
@@ -1268,7 +1295,7 @@ describe("runtime model picker visibility", () => {
expect(optionValues).toEqual([
"openai/gpt-5.5",
"anthropic/claude-sonnet-4-6",
"google/gemini-3-pro-preview",
"google/gemini-3.1-pro-preview",
]);
expect(call?.initialValues).toEqual(["openai/gpt-5.5"]);
});
@@ -1504,7 +1531,7 @@ describe("applyModelFallbacksFromSelection", () => {
});
expect(next.agents?.defaults?.model).toEqual({
primary: "anthropic/claude-opus-4-6",
fallbacks: ["google/gemini-3-pro-preview"],
fallbacks: ["google/gemini-3.1-pro-preview"],
});
});

View File

@@ -13,6 +13,7 @@ import {
buildModelAliasIndex,
type ModelAliasIndex,
modelKey,
normalizeModelRef,
normalizeProviderId,
resolveConfiguredModelRef,
resolveModelRefFromString,
@@ -231,11 +232,12 @@ function addModelSelectOption(params: {
hasAuth: (provider: string) => boolean;
literalPrefixProviders: Set<string>;
}) {
const key = modelKey(params.entry.provider, params.entry.id);
const normalizedRef = normalizeModelRef(params.entry.provider, params.entry.id);
const key = modelKey(normalizedRef.provider, normalizedRef.model);
if (
params.seen.has(key) ||
HIDDEN_ROUTER_MODELS.has(key) ||
!isModelPickerVisibleProvider(params.entry.provider)
!isModelPickerVisibleProvider(normalizedRef.provider)
) {
return;
}
@@ -253,15 +255,15 @@ function addModelSelectOption(params: {
if (aliases?.length) {
hints.push(`alias: ${aliases.join(", ")}`);
}
const routeHint = resolveModelRouteHint(params.entry.provider);
const routeHint = resolveModelRouteHint(normalizedRef.provider);
if (routeHint) {
hints.push(routeHint);
}
if (!params.hasAuth(params.entry.provider)) {
if (!params.hasAuth(normalizedRef.provider)) {
return;
}
const label = params.literalPrefixProviders.has(normalizeProviderId(params.entry.provider))
? `${params.entry.provider}/${params.entry.id}`
const label = params.literalPrefixProviders.has(normalizeProviderId(normalizedRef.provider))
? formatLiteralProviderPrefixedModelRef(normalizedRef.provider, key)
: key;
params.options.push({
value: key,

View File

@@ -4,6 +4,7 @@ import { normalizeGooglePreviewModelId } from "./provider-model-id-normalize.js"
describe("provider model id normalization", () => {
it("routes bare Gemini 3 Pro to the current Gemini 3.1 Pro preview", () => {
expect(normalizeGooglePreviewModelId("gemini-3-pro")).toBe("gemini-3.1-pro-preview");
expect(normalizeGooglePreviewModelId("gemini-3-pro-preview")).toBe("gemini-3.1-pro-preview");
expect(normalizeGooglePreviewModelId("gemini-3.1-pro")).toBe("gemini-3.1-pro-preview");
});

View File

@@ -1,7 +1,7 @@
const ANTIGRAVITY_BARE_PRO_IDS = new Set(["gemini-3-pro", "gemini-3.1-pro", "gemini-3-1-pro"]);
export function normalizeGooglePreviewModelId(id: string): string {
if (id === "gemini-3-pro") {
if (id === "gemini-3-pro" || id === "gemini-3-pro-preview") {
return "gemini-3.1-pro-preview";
}
if (id === "gemini-3-flash") {