mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 11:30:43 +00:00
fix(models/auth): merge agents.defaults.models on provider login
`openclaw models auth login` was replacing `agents.defaults.models` wholesale whenever a provider returned a `configPatch` with that key, even if the patch only listed the new default model. Re-authenticating an OAuth provider such as OpenAI Codex wiped aliases and per-model params for every other provider. Make replacement opt-in via `ProviderAuthResult.replaceDefaultModels`. Ordinary logins merge their allowlist patch so unrelated entries survive; the Anthropic -> Claude CLI migration opts in because it renames keys the merge path would otherwise keep stale. Fixes #69414. Made-with: Cursor
This commit is contained in:
committed by
Peter Steinberger
parent
5bd8254f61
commit
14d1c9c4f0
@@ -39,6 +39,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Models/auth: merge provider-owned default-model additions from `openclaw models auth login` instead of replacing `agents.defaults.models`, so re-authenticating an OAuth provider such as OpenAI Codex no longer wipes other providers' aliases and per-model params. Migrations that must rename keys (Anthropic -> Claude CLI) opt in with `replaceDefaultModels`. Fixes #69414. (#70435) Thanks @neeravmakwana.
|
||||
- Media understanding/audio: prefer configured or key-backed STT providers before auto-detected local Whisper CLIs, so installed local transcription tools no longer shadow API providers such as Groq/OpenAI in `tools.media.audio` auto mode. Fixes #68727.
|
||||
- Providers/OpenAI: lock the auth picker wording for OpenAI API key, Codex browser login, and Codex device pairing so the setup choices no longer imply a mixed Codex/API-key auth path. (#67848) Thanks @tmlxrd.
|
||||
- Agents/BTW: route `/btw` side questions through provider stream registration with the session workspace, so Ollama provider URL construction and workspace-scoped hooks apply correctly. Fixes #68336. (#70413) Thanks @suboss87.
|
||||
|
||||
@@ -180,6 +180,8 @@ export function buildAnthropicCliMigrationResult(
|
||||
},
|
||||
},
|
||||
},
|
||||
// Rewrites `anthropic/*` -> `claude-cli/*`; merge would keep stale keys.
|
||||
replaceDefaultModels: true,
|
||||
defaultModel,
|
||||
notes: [
|
||||
"Claude CLI auth detected; switched Anthropic model selection to the local Claude CLI backend.",
|
||||
|
||||
@@ -154,23 +154,28 @@ vi.mock("../provider-auth-helpers.js", () => {
|
||||
null
|
||||
);
|
||||
}),
|
||||
applyProviderAuthConfigPatch: vi.fn((cfg: OpenClawConfig, patch: unknown) => {
|
||||
const merged = mergePatch(cfg, patch);
|
||||
const patchModels = (patch as { agents?: { defaults?: { models?: unknown } } })?.agents
|
||||
?.defaults?.models;
|
||||
return isRecord(patchModels)
|
||||
? {
|
||||
...merged,
|
||||
agents: {
|
||||
...merged.agents,
|
||||
defaults: {
|
||||
...merged.agents?.defaults,
|
||||
models: patchModels,
|
||||
applyProviderAuthConfigPatch: vi.fn(
|
||||
(cfg: OpenClawConfig, patch: unknown, options?: { replaceDefaultModels?: boolean }) => {
|
||||
const merged = mergePatch(cfg, patch);
|
||||
if (!options?.replaceDefaultModels) {
|
||||
return merged;
|
||||
}
|
||||
const patchModels = (patch as { agents?: { defaults?: { models?: unknown } } })?.agents
|
||||
?.defaults?.models;
|
||||
return isRecord(patchModels)
|
||||
? {
|
||||
...merged,
|
||||
agents: {
|
||||
...merged.agents,
|
||||
defaults: {
|
||||
...merged.agents?.defaults,
|
||||
models: patchModels,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: merged;
|
||||
}),
|
||||
}
|
||||
: merged;
|
||||
},
|
||||
),
|
||||
applyDefaultModel: vi.fn((cfg: OpenClawConfig, model: string) => ({
|
||||
...cfg,
|
||||
agents: {
|
||||
@@ -482,6 +487,7 @@ describe("modelsAuthLoginCommand", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
replaceDefaultModels: true,
|
||||
notes: [
|
||||
"Claude CLI auth detected; switched Anthropic model selection to the local Claude CLI backend.",
|
||||
"Existing Anthropic auth profiles are kept for rollback.",
|
||||
@@ -573,6 +579,38 @@ describe("modelsAuthLoginCommand", () => {
|
||||
expect(runtime.log).toHaveBeenCalledWith("Default model set to claude-cli/claude-sonnet-4-6");
|
||||
});
|
||||
|
||||
it("preserves other providers' allowlist entries on an openai-codex OAuth login", async () => {
|
||||
const runtime = createRuntime();
|
||||
const existingModels = {
|
||||
"anthropic/claude-sonnet-4-6": { alias: "sonnet" },
|
||||
"anthropic/claude-opus-4-6": { alias: "opus" },
|
||||
"moonshot/kimi-k2.5": { alias: "kimi" },
|
||||
"openai-codex/gpt-5.4": { alias: "gpt54" },
|
||||
};
|
||||
currentConfig = { agents: { defaults: { models: existingModels } } };
|
||||
runProviderAuth.mockResolvedValue({
|
||||
profiles: [
|
||||
{
|
||||
profileId: "openai-codex:user@example.com",
|
||||
credential: {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "a",
|
||||
refresh: "r",
|
||||
expires: Date.now() + 60_000,
|
||||
email: "user@example.com",
|
||||
},
|
||||
},
|
||||
],
|
||||
configPatch: { agents: { defaults: { models: { "openai-codex/gpt-5.4": {} } } } },
|
||||
defaultModel: "openai-codex/gpt-5.4",
|
||||
});
|
||||
|
||||
await modelsAuthLoginCommand({ provider: "openai-codex" }, runtime);
|
||||
|
||||
expect(lastUpdatedConfig?.agents?.defaults?.models).toEqual(existingModels);
|
||||
});
|
||||
|
||||
it("survives lockout clearing failure without blocking login", async () => {
|
||||
const runtime = createRuntime();
|
||||
mocks.loadAuthProfileStoreForRuntime.mockImplementation(() => {
|
||||
|
||||
@@ -246,7 +246,9 @@ async function persistProviderAuthResult(params: {
|
||||
await updateConfig((cfg) => {
|
||||
let next = cfg;
|
||||
if (params.result.configPatch) {
|
||||
next = applyProviderAuthConfigPatch(next, params.result.configPatch);
|
||||
next = applyProviderAuthConfigPatch(next, params.result.configPatch, {
|
||||
replaceDefaultModels: params.result.replaceDefaultModels,
|
||||
});
|
||||
}
|
||||
for (const profile of params.result.profiles) {
|
||||
next = applyAuthProfileConfig(next, {
|
||||
|
||||
@@ -3,38 +3,43 @@ import type { OpenClawConfig } from "../config/config.js";
|
||||
import { applyProviderAuthConfigPatch } from "./provider-auth-choice-helpers.js";
|
||||
|
||||
describe("applyProviderAuthConfigPatch", () => {
|
||||
it("replaces patched default model maps instead of recursively merging them", () => {
|
||||
const base = {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: {
|
||||
primary: "anthropic/claude-sonnet-4-6",
|
||||
fallbacks: ["anthropic/claude-opus-4-6", "openai/gpt-5.2"],
|
||||
},
|
||||
models: {
|
||||
"anthropic/claude-sonnet-4-6": { alias: "Sonnet" },
|
||||
"anthropic/claude-opus-4-6": { alias: "Opus" },
|
||||
"openai/gpt-5.2": {},
|
||||
},
|
||||
const base = {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "anthropic/claude-sonnet-4-6", fallbacks: ["openai/gpt-5.2"] },
|
||||
models: {
|
||||
"anthropic/claude-sonnet-4-6": { alias: "Sonnet" },
|
||||
"anthropic/claude-opus-4-6": { alias: "Opus" },
|
||||
"openai/gpt-5.2": {},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
it("merges default model maps by default so other providers survive login", () => {
|
||||
const patch = { agents: { defaults: { models: { "openai-codex/gpt-5.4": {} } } } };
|
||||
const next = applyProviderAuthConfigPatch(base, patch);
|
||||
expect(next.agents?.defaults?.models).toEqual({
|
||||
...base.agents.defaults.models,
|
||||
"openai-codex/gpt-5.4": {},
|
||||
});
|
||||
expect(next.agents?.defaults?.model).toEqual(base.agents.defaults.model);
|
||||
});
|
||||
|
||||
it("replaces the allowlist only when replaceDefaultModels is set", () => {
|
||||
const patch = {
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"claude-cli/claude-sonnet-4-6": { alias: "Sonnet" },
|
||||
"claude-cli/claude-opus-4-6": { alias: "Opus" },
|
||||
"openai/gpt-5.2": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const next = applyProviderAuthConfigPatch(base, patch);
|
||||
|
||||
const next = applyProviderAuthConfigPatch(base, patch, { replaceDefaultModels: true });
|
||||
expect(next.agents?.defaults?.models).toEqual(patch.agents.defaults.models);
|
||||
expect(next.agents?.defaults?.model).toEqual(base.agents?.defaults?.model);
|
||||
expect(next.agents?.defaults?.model).toEqual(base.agents.defaults.model);
|
||||
});
|
||||
|
||||
it("keeps normal recursive merges for unrelated provider auth patch fields", () => {
|
||||
|
||||
@@ -63,9 +63,13 @@ export function mergeConfigPatch<T>(base: T, patch: unknown): T {
|
||||
return next as T;
|
||||
}
|
||||
|
||||
export function applyProviderAuthConfigPatch(cfg: OpenClawConfig, patch: unknown): OpenClawConfig {
|
||||
export function applyProviderAuthConfigPatch(
|
||||
cfg: OpenClawConfig,
|
||||
patch: unknown,
|
||||
options?: { replaceDefaultModels?: boolean },
|
||||
): OpenClawConfig {
|
||||
const merged = mergeConfigPatch(cfg, patch);
|
||||
if (!isPlainRecord(patch)) {
|
||||
if (!options?.replaceDefaultModels || !isPlainRecord(patch)) {
|
||||
return merged;
|
||||
}
|
||||
|
||||
@@ -81,7 +85,7 @@ export function applyProviderAuthConfigPatch(cfg: OpenClawConfig, patch: unknown
|
||||
...merged.agents,
|
||||
defaults: {
|
||||
...merged.agents?.defaults,
|
||||
// Provider auth migrations can intentionally replace the exact allowlist.
|
||||
// Opt-in replacement for migrations that rename/remove model keys.
|
||||
models: patchModels as NonNullable<
|
||||
NonNullable<OpenClawConfig["agents"]>["defaults"]
|
||||
>["models"],
|
||||
|
||||
@@ -155,7 +155,9 @@ export async function runProviderPluginAuthMethod(params: {
|
||||
|
||||
let nextConfig = params.config;
|
||||
if (result.configPatch) {
|
||||
nextConfig = applyProviderAuthConfigPatch(nextConfig, result.configPatch);
|
||||
nextConfig = applyProviderAuthConfigPatch(nextConfig, result.configPatch, {
|
||||
replaceDefaultModels: result.replaceDefaultModels,
|
||||
});
|
||||
}
|
||||
|
||||
for (const profile of result.profiles) {
|
||||
|
||||
@@ -240,6 +240,12 @@ export type ProviderAuthResult = {
|
||||
configPatch?: Partial<OpenClawConfig>;
|
||||
defaultModel?: string;
|
||||
notes?: string[];
|
||||
/**
|
||||
* Opt in to replace `agents.defaults.models` wholesale with the patch map.
|
||||
* Default behavior merges the map so other providers' entries survive.
|
||||
* Set only from migrations that intentionally rename/remove model keys.
|
||||
*/
|
||||
replaceDefaultModels?: boolean;
|
||||
};
|
||||
|
||||
/** Interactive auth context passed to provider login/setup methods. */
|
||||
|
||||
Reference in New Issue
Block a user