fix: normalize merged gemini model config

This commit is contained in:
Peter Steinberger
2026-05-10 08:20:00 +01:00
parent 711e09c9aa
commit 036eb75a30
5 changed files with 88 additions and 3 deletions
+1
View File
@@ -65,6 +65,7 @@ Docs: https://docs.openclaw.ai
- OpenAI/Codex: point gateway missing-key recovery and wizard docs at the canonical `openai/gpt-5.5` plus Codex OAuth route, and fix trajectory export errors so they suggest the valid `openclaw sessions` command.
- Google/Gemini: normalize retired `google/gemini-3-pro-preview` primary, fallback, and model-map refs during config load and unrelated config writes so saved config keeps targeting Gemini 3.1 Pro Preview.
- Google/Gemini: normalize retired Gemini 3 Pro Preview ids inside emitted Google provider model config, so regenerated models.json rows test `google/gemini-3.1-pro-preview`.
- Google/Gemini: normalize retired Gemini 3 Pro Preview ids preserved from existing merged models.json providers so config emission keeps targeting `google/gemini-3.1-pro-preview`.
- GitHub Copilot: mint short-lived Copilot API tokens with the same `vscode-chat` integration identity used by runtime requests, and refresh legacy cached tokens missing that identity so image-capable Copilot models no longer inherit the `copilot-language-server` scope. Fixes #79946, #80074. Thanks @TurboTheTurtle.
- Plugins/doctor: drop stale managed npm install records when `openclaw doctor --fix` removes npm packages that shadow bundled plugins, so the rebuilt registry no longer resurrects the removed package metadata.
- Discord/voice: reuse or suppress late realtime consult tool calls without stealing newer speaker context or speaking forced fallback answers twice.
@@ -188,6 +188,64 @@ describe("models-config", () => {
expect(observedSnapshot).toBe(pluginMetadataSnapshot);
});
it("normalizes retired Gemini ids preserved from existing models.json rows", async () => {
const plan = await planOpenClawModelsJsonWithDeps(
{
cfg: { models: { mode: "merge", providers: {} } },
agentDir: "/tmp/openclaw-models-config-env-vars-test",
env: {},
existingRaw: "",
existingParsed: {
providers: {
google: {
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
api: "google-generative-ai",
apiKey: "GOOGLE_API_KEY", // pragma: allowlist secret
models: [
{
id: "gemini-3-pro-preview",
name: "Gemini 3 Pro",
input: ["text"],
},
],
},
},
},
},
{
resolveImplicitProviders: async () => ({
openai: {
baseUrl: "https://api.openai.com/v1",
api: "openai-responses",
apiKey: "OPENAI_API_KEY", // pragma: allowlist secret
models: [
{
id: "gpt-5.5",
name: "GPT-5.5",
input: ["text"],
reasoning: true,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 400000,
maxTokens: 128000,
},
],
},
}),
},
);
expect(plan.action).toBe("write");
if (plan.action !== "write") {
throw new Error("Expected models.json write plan");
}
const parsed = JSON.parse(plan.contents) as {
providers?: Record<string, { models?: Array<{ id?: string }> }>;
};
expect(parsed.providers?.google?.models?.map((model) => model.id)).toEqual([
"gemini-3.1-pro-preview",
]);
});
it("uses config env.vars entries for implicit provider discovery without mutating process.env", async () => {
await withTempEnv(["OPENROUTER_API_KEY", TEST_ENV_VAR], async () => {
unsetEnv(["OPENROUTER_API_KEY", TEST_ENV_VAR]);
+5 -2
View File
@@ -9,6 +9,7 @@ import {
import {
applyNativeStreamingUsageCompat,
enforceSourceManagedProviderSecrets,
normalizeProviderCatalogModelsForConfig,
normalizeProviders,
resolveImplicitProviders,
type ProviderConfig,
@@ -167,13 +168,15 @@ export async function planOpenClawModelsJsonWithDeps(
providers: normalizedProviders,
secretRefManagedProviders,
});
const normalizedMergedProviders =
normalizeProviderCatalogModelsForConfig(mergedProviders) ?? mergedProviders;
const secretEnforcedProviders =
enforceSourceManagedProviderSecrets({
providers: mergedProviders,
providers: normalizedMergedProviders,
sourceProviders: params.sourceConfigForSecrets?.models?.providers,
sourceSecretDefaults: params.sourceConfigForSecrets?.secrets?.defaults,
secretRefManagedProviders,
}) ?? mergedProviders;
}) ?? normalizedMergedProviders;
const finalProviders = applyNativeStreamingUsageCompat(secretEnforcedProviders);
const nextContents = `${JSON.stringify({ providers: finalProviders }, null, 2)}\n`;
@@ -84,6 +84,26 @@ function normalizeProviderModelsForConfig(
: { provider, mutated };
}
export function normalizeProviderCatalogModelsForConfig(
providers: ModelsConfig["providers"],
): ModelsConfig["providers"] {
if (!providers) {
return providers;
}
let mutated = false;
const next: Record<string, ProviderConfig> = {};
for (const [providerKey, provider] of Object.entries(providers)) {
const normalized = normalizeProviderModelsForConfig(providerKey, provider);
if (normalized.mutated) {
mutated = true;
}
next[providerKey] = normalized.provider;
}
return mutated ? next : providers;
}
export function normalizeProviders(params: {
providers: ModelsConfig["providers"];
agentDir: string;
+4 -1
View File
@@ -1,5 +1,8 @@
export { resolveImplicitProviders } from "./models-config.providers.implicit.js";
export { normalizeProviders } from "./models-config.providers.normalize.js";
export {
normalizeProviderCatalogModelsForConfig,
normalizeProviders,
} from "./models-config.providers.normalize.js";
export type { ProviderConfig } from "./models-config.providers.secrets.js";
export { applyNativeStreamingUsageCompat } from "./models-config.providers.policy.js";
export { enforceSourceManagedProviderSecrets } from "./models-config.providers.source-managed.js";