fix(config): normalize Gemini provider catalog writes

This commit is contained in:
Peter Steinberger
2026-05-11 10:37:44 +01:00
parent e88956d235
commit e21e83db97
3 changed files with 104 additions and 1 deletions

View File

@@ -76,6 +76,7 @@ Docs: https://docs.openclaw.ai
- Google/Gemini: normalize retired nested Gemini 3 Pro Preview ids while converting manifest catalog rows into emitted provider config, so `google/gemini-3.1-pro-preview` is used for testing instead of `google/gemini-3-pro-preview`.
- Google/Gemini: normalize retired nested Gemini 3 Pro Preview ids in configured proxy/provider-auth model catalogs, so regenerated config keeps testing `google/gemini-3.1-pro-preview` instead of `google/gemini-3-pro-preview`.
- Google/Gemini: normalize retired nested Gemini 3 Pro Preview ids while onboarding provider catalog presets, so setup-emitted proxy configs test `google/gemini-3.1-pro-preview` instead of `google/gemini-3-pro-preview`.
- Google/Gemini: normalize retired Gemini 3 Pro Preview ids in provider catalog rows during generic config writes, so unrelated config changes keep testing `google/gemini-3.1-pro-preview`.
- Models: keep configured fallback chains ahead of configured primary models for override selections with duplicate model ids, preventing fallback jumps to the wrong provider. Fixes #80562.
- Native apps: advertise the Gateway protocol compatibility range so chat and node sessions can connect to v3 gateways after additive v4 client updates.
- Gateway/agents: keep stale `sessions_send` ACP manager and `web_fetch` runtime chunks importable after package updates, preventing live gateways from breaking before restart. Fixes #78804. Thanks @Gomesy72.

View File

@@ -223,6 +223,64 @@ describe("config io write prepare", () => {
expect(persisted.gateway?.port).toBe(18888);
});
it("normalizes retired Google provider catalog refs during unrelated config writes", () => {
const makeModel = (id: string, name: string) => ({
id,
name,
reasoning: true,
input: ["text" as const],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 1_048_576,
maxTokens: 65_536,
});
const sourceConfig: OpenClawConfig = {
models: {
providers: {
google: {
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
models: [makeModel("google/gemini-3-pro-preview", "Gemini 3 Pro")],
},
kilocode: {
baseUrl: "https://kilocode.test/v1",
models: [makeModel("google/gemini-3-pro-preview", "Gemini via Kilo")],
},
},
},
gateway: { port: 18789 },
};
const runtimeConfig: OpenClawConfig = {
models: {
providers: {
google: {
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
models: [makeModel("google/gemini-3.1-pro-preview", "Gemini 3 Pro")],
},
kilocode: {
baseUrl: "https://kilocode.test/v1",
models: [makeModel("google/gemini-3.1-pro-preview", "Gemini via Kilo")],
},
},
},
gateway: { port: 18789 },
};
const persisted = resolvePersistCandidateForWrite({
runtimeConfig,
sourceConfig,
nextConfig: {
...runtimeConfig,
gateway: { port: 18888 },
},
}) as OpenClawConfig;
expect(persisted.models?.providers?.google?.models).toEqual([
makeModel("google/gemini-3.1-pro-preview", "Gemini 3 Pro"),
]);
expect(persisted.models?.providers?.kilocode?.models).toEqual([
makeModel("google/gemini-3.1-pro-preview", "Gemini via Kilo"),
]);
expect(persisted.gateway?.port).toBe(18888);
});
it("allows explicit unsets to remove authored agent provider params", () => {
const sourceConfig: OpenClawConfig = {
agents: {

View File

@@ -1,4 +1,5 @@
import { isDeepStrictEqual } from "node:util";
import { normalizeConfiguredProviderCatalogModelId } from "../agents/model-ref-shared.js";
import { isRecord } from "../utils.js";
import { applyMergePatch } from "./merge-patch.js";
import { normalizeAgentModelMapForConfig, normalizeAgentModelRefForConfig } from "./model-input.js";
@@ -354,6 +355,49 @@ function normalizeAgentDefaultModelRefsForWrite(config: unknown): unknown {
return next;
}
function normalizeModelProviderCatalogRefsForWrite(config: unknown): unknown {
const providers = getPathValue(config, ["models", "providers"]);
if (!isRecord(providers)) {
return config;
}
let mutated = false;
const nextProviders: Record<string, unknown> = { ...providers };
for (const [provider, providerConfig] of Object.entries(providers)) {
if (!isRecord(providerConfig) || !Array.isArray(providerConfig.models)) {
continue;
}
let providerMutated = false;
const models = providerConfig.models.map((model) => {
if (!isRecord(model) || typeof model.id !== "string") {
return model;
}
const trimmed = model.id.trim();
if (!trimmed) {
return model;
}
const id = normalizeConfiguredProviderCatalogModelId(provider, trimmed);
if (id === model.id) {
return model;
}
providerMutated = true;
return { ...model, id };
});
if (providerMutated) {
nextProviders[provider] = { ...providerConfig, models };
mutated = true;
}
}
return mutated ? setPathValue(config, ["models", "providers"], nextProviders) : config;
}
function normalizeModelRefsForWrite(config: unknown): unknown {
return normalizeModelProviderCatalogRefsForWrite(normalizeAgentDefaultModelRefsForWrite(config));
}
function preserveUntouchedIncludes(params: {
patch: unknown;
rootAuthoredConfig: unknown;
@@ -524,7 +568,7 @@ export function resolvePersistCandidateForWrite(params: {
persistedCandidate: withSchema,
unsetPaths: params.unsetPaths,
});
return normalizeAgentDefaultModelRefsForWrite(withAuthoredParams);
return normalizeModelRefsForWrite(withAuthoredParams);
}
function readRootSchemaUri(value: unknown): string | undefined {