From 88714d6803456a93b2b490416235dfd8dcdf3012 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 12 May 2026 13:31:30 +0100 Subject: [PATCH] fix: normalize oauth auth-result config patches --- CHANGELOG.md | 1 + src/plugin-sdk/provider-auth-result.test.ts | 72 +++++++++++ src/plugin-sdk/provider-auth-result.ts | 132 ++++++++++++++++++-- 3 files changed, 196 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1117891fc06..79d8273264b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ Docs: https://docs.openclaw.ai - Gateway/OpenAI HTTP: honor `max_completion_tokens` and `max_tokens` on inbound `/v1/chat/completions` requests so client-provided token caps reach the upstream provider via `streamParams.maxTokens`, with `max_completion_tokens` taking precedence when both are sent. Thanks @Lellansin. - Models/OpenAI CLI auth: make `openclaw models auth login --provider openai` start the ChatGPT/Codex account login by default, while `--method api-key` remains the explicit OpenAI API-key setup path. +- Google/Gemini: normalize retired Gemini 3 Pro Preview ids inside explicit SDK OAuth auth-result config patches, so provider helpers emit `google/gemini-3.1-pro-preview` for Gemini 3.1 testing. - Google/Gemini: normalize retired Gemini 3 Pro Preview ids inside SDK OAuth auth-result default config patches, so helper-built provider auth flows emit `google/gemini-3.1-pro-preview` for Gemini 3.1 testing. - Google/Gemini: normalize retired Gemini 3 Pro Preview ids returned by direct `openclaw models auth login --set-default` provider auth flows before writing config, so Gemini testing targets `google/gemini-3.1-pro-preview`. - Google/Gemini: normalize retired Gemini 3 Pro Preview ids in provider catalog rows when API-key onboarding only reapplies the agent default, so emitted config keeps testing `google/gemini-3.1-pro-preview`. diff --git a/src/plugin-sdk/provider-auth-result.test.ts b/src/plugin-sdk/provider-auth-result.test.ts index f16102634de..f1e8c9c4170 100644 --- a/src/plugin-sdk/provider-auth-result.test.ts +++ b/src/plugin-sdk/provider-auth-result.test.ts @@ -20,4 +20,76 @@ describe("buildOauthProviderAuthResult", () => { }, }); }); + + it("normalizes retired Gemini refs inside explicit config patches", () => { + const result = buildOauthProviderAuthResult({ + providerId: "google", + defaultModel: "google/gemini-3-pro-preview", + access: "access-token", + configPatch: { + agents: { + defaults: { + model: { + primary: "google/gemini-3-pro-preview", + fallbacks: ["google/gemini-3-pro-preview", "openai/gpt-5.5"], + }, + models: { + "google/gemini-3-pro-preview": { alias: "gemini" }, + }, + }, + }, + models: { + providers: { + google: { + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + models: [ + { + id: "google/gemini-3-pro-preview", + name: "Gemini 3 Pro", + contextWindow: 1_048_576, + maxTokens: 65_536, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + reasoning: true, + }, + ], + }, + }, + }, + }, + }); + + expect(result.defaultModel).toBe("google/gemini-3.1-pro-preview"); + expect(result.configPatch).toEqual({ + agents: { + defaults: { + model: { + primary: "google/gemini-3.1-pro-preview", + fallbacks: ["google/gemini-3.1-pro-preview", "openai/gpt-5.5"], + }, + models: { + "google/gemini-3.1-pro-preview": { alias: "gemini" }, + }, + }, + }, + models: { + providers: { + google: { + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + models: [ + { + id: "google/gemini-3.1-pro-preview", + name: "Gemini 3 Pro", + contextWindow: 1_048_576, + maxTokens: 65_536, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + reasoning: true, + }, + ], + }, + }, + }, + }); + }); }); diff --git a/src/plugin-sdk/provider-auth-result.ts b/src/plugin-sdk/provider-auth-result.ts index c7e9c40aa85..93834de2b01 100644 --- a/src/plugin-sdk/provider-auth-result.ts +++ b/src/plugin-sdk/provider-auth-result.ts @@ -1,9 +1,122 @@ import { buildAuthProfileId } from "../agents/auth-profiles/identity.js"; import type { AuthProfileCredential } from "../agents/auth-profiles/types.js"; -import { normalizeAgentModelRefForConfig } from "../config/model-input.js"; +import { normalizeConfiguredProviderCatalogModelId } from "../agents/model-ref-shared.js"; +import { + normalizeAgentModelMapForConfig, + normalizeAgentModelRefForConfig, +} from "../config/model-input.js"; +import type { ModelProviderConfig } from "../config/types.models.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import type { ProviderAuthResult } from "../plugins/types.js"; +function normalizeAgentModelConfigForAuthResult(value: unknown): unknown { + if (typeof value === "string") { + return normalizeAgentModelRefForConfig(value); + } + if (!value || typeof value !== "object" || Array.isArray(value)) { + return value; + } + + let mutated = false; + const next: Record = { ...(value as Record) }; + if (typeof next.primary === "string") { + const primary = normalizeAgentModelRefForConfig(next.primary); + if (primary !== next.primary) { + next.primary = primary; + mutated = true; + } + } + if (Array.isArray(next.fallbacks)) { + const originalFallbacks = next.fallbacks; + const fallbacks = originalFallbacks.map((fallback) => + typeof fallback === "string" ? normalizeAgentModelRefForConfig(fallback) : fallback, + ); + if (fallbacks.some((fallback, index) => fallback !== originalFallbacks[index])) { + next.fallbacks = fallbacks; + mutated = true; + } + } + return mutated ? next : value; +} + +function normalizeProviderConfigModelIdsForAuthResult( + provider: string, + providerConfig: ModelProviderConfig, +): ModelProviderConfig { + const models = providerConfig.models; + if (!Array.isArray(models) || models.length === 0) { + return providerConfig; + } + + let mutated = false; + const nextModels = models.map((model) => { + const id = normalizeConfiguredProviderCatalogModelId(provider, model.id); + if (id === model.id) { + return model; + } + mutated = true; + return Object.assign({}, model, { id }); + }); + return mutated ? { ...providerConfig, models: nextModels } : providerConfig; +} + +function normalizeProviderAuthConfigPatchModelRefs( + patch: Partial, +): Partial { + let next = patch; + const defaults = patch.agents?.defaults; + if (defaults) { + let nextDefaults = defaults; + if (defaults.model !== undefined) { + const model = normalizeAgentModelConfigForAuthResult(defaults.model); + if (model !== defaults.model) { + nextDefaults = { ...nextDefaults, model: model as typeof defaults.model }; + } + } + if (defaults.models) { + const models = normalizeAgentModelMapForConfig(defaults.models); + if (models !== defaults.models) { + nextDefaults = { ...nextDefaults, models }; + } + } + if (nextDefaults !== defaults) { + next = { + ...next, + agents: { + ...next.agents, + defaults: nextDefaults, + }, + }; + } + } + + const providers = patch.models?.providers; + if (!providers) { + return next; + } + + let mutated = false; + const nextProviders = { ...providers }; + for (const [provider, providerConfig] of Object.entries(providers)) { + const normalized = normalizeProviderConfigModelIdsForAuthResult(provider, providerConfig); + if (normalized === providerConfig) { + continue; + } + nextProviders[provider] = normalized; + mutated = true; + } + + return mutated + ? { + ...next, + models: { + ...next.models, + providers: nextProviders, + }, + } + : next; +} + /** Build the standard auth result payload for OAuth-style provider login flows. */ export function buildOauthProviderAuthResult(params: { providerId: string; @@ -41,17 +154,18 @@ export function buildOauthProviderAuthResult(params: { return { profiles: [{ profileId, credential }], - configPatch: + configPatch: normalizeProviderAuthConfigPatchModelRefs( params.configPatch ?? - ({ - agents: { - defaults: { - models: { - [defaultModel]: {}, + ({ + agents: { + defaults: { + models: { + [defaultModel]: {}, + }, }, }, - }, - } as Partial), + } as Partial), + ), defaultModel, notes: params.notes, };