diff --git a/CHANGELOG.md b/CHANGELOG.md index bac2f1b65aa..4123d1d9a9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Docs: https://docs.openclaw.ai - Heartbeat: strip legacy `[TOOL_CALL]...[/TOOL_CALL]` and `[TOOL_RESULT]...[/TOOL_RESULT]` pseudo-call blocks from heartbeat replies before channel delivery. Fixes #54138. Thanks @Deniable9570. - macOS/Voice Wake: send wake-word and Push-to-Talk transcripts through the selected macOS session target instead of always falling back to main WebChat. Fixes #51040. Thanks @carl-jeffrolc. - Providers/xAI: give Grok `web_search` a 60s default timeout, harden malformed xAI Responses parsing, and return structured timeout errors instead of aborting the tool call. Fixes #58063 and #58733. Thanks @dnishimura, @marvcasasola-svg, and @Nanako0129. +- Providers/configure: preserve the existing default model when adding or reauthing a provider whose plugin returns a default-model config patch. Fixes #50268. Thanks @rixcorp-oc. - Heartbeat: strip legacy `[TOOL_CALL]...[/TOOL_CALL]` and `[TOOL_RESULT]...[/TOOL_RESULT]` pseudo-call blocks from heartbeat replies before channel delivery. Fixes #54138. Thanks @Deniable9570. - macOS/Voice Wake: send wake-word and Push-to-Talk transcripts through the selected macOS session target instead of always falling back to main WebChat. Fixes #51040. Thanks @carl-jeffrolc. - Providers/xAI: give Grok `web_search` a 60s default timeout, harden malformed xAI Responses parsing, and return structured timeout errors instead of aborting the tool call. Fixes #58063 and #58733. Thanks @dnishimura, @marvcasasola-svg, and @Nanako0129. diff --git a/docs/cli/configure.md b/docs/cli/configure.md index ae48f8fca2e..ae398f5c987 100644 --- a/docs/cli/configure.md +++ b/docs/cli/configure.md @@ -10,7 +10,9 @@ title: "Configure" Interactive prompt to set up credentials, devices, and agent defaults. -The **Model** section includes a multi-select for the `agents.defaults.models` allowlist (what shows up in `/model` and the model picker). Provider-scoped setup choices merge their selected models into the existing allowlist instead of replacing unrelated providers already in the config. Re-running provider auth from configure preserves an existing `agents.defaults.model.primary`. Use `openclaw models auth login --provider --set-default` or `openclaw models set ` when you intentionally want to change the default model. +The **Model** section includes a multi-select for the `agents.defaults.models` allowlist (what shows up in `/model` and the model picker). Provider-scoped setup choices merge their selected models into the existing allowlist instead of replacing unrelated providers already in the config. + +Re-running provider auth from configure preserves an existing `agents.defaults.model.primary`, even when the provider's auth step returns a config patch with its own recommended default model. That means adding or reauthing xAI, OpenRouter, or another provider should make the new model available without taking over from your current primary model. Use `openclaw models auth login --provider --set-default` or `openclaw models set ` when you intentionally want to change the default model. When configure starts from a provider auth choice, the default-model and allowlist pickers prefer that provider automatically. For paired providers such as Volcengine and BytePlus, the same preference also matches their coding-plan variants (`volcengine-plan/*`, `byteplus-plan/*`). If the preferred-provider filter would produce an empty list, configure falls back to the unfiltered catalog instead of showing a blank picker. diff --git a/docs/concepts/model-providers.md b/docs/concepts/model-providers.md index 7ecefae1545..eadf037a3e9 100644 --- a/docs/concepts/model-providers.md +++ b/docs/concepts/model-providers.md @@ -19,6 +19,12 @@ Reference for **LLM/model providers** (not chat channels like WhatsApp/Telegram) - `models.providers.*.contextWindow` / `contextTokens` / `maxTokens` set provider-level defaults; `models.providers.*.models[].contextWindow` / `contextTokens` / `maxTokens` override them per model. - Fallback rules, cooldown probes, and session-override persistence: [Model failover](/concepts/model-failover). + + + `openclaw configure` preserves an existing `agents.defaults.model.primary` when you add or reauth a provider. Provider plugins may still return a recommended default model in their auth config patch, but configure treats that as "make this model available" when a primary model already exists, not "replace the current primary model." + + To intentionally switch the default model, use `openclaw models set ` or `openclaw models auth login --provider --set-default`. + OpenAI-family routes are prefix-specific: diff --git a/src/commands/auth-choice.apply.plugin-provider.test.ts b/src/commands/auth-choice.apply.plugin-provider.test.ts index a8a72df8efc..7e1f450f199 100644 --- a/src/commands/auth-choice.apply.plugin-provider.test.ts +++ b/src/commands/auth-choice.apply.plugin-provider.test.ts @@ -105,6 +105,7 @@ const LOCAL_AUTH_METHOD_ID = "local"; const LOCAL_PROFILE_ID = `${LOCAL_PROVIDER_ID}:default`; const LOCAL_API_KEY = "local-provider-key"; const LOCAL_DEFAULT_MODEL = `${LOCAL_PROVIDER_ID}/demo-model`; +const EXISTING_DEFAULT_MODEL = "amazon-bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0"; function buildProvider(): ProviderPlugin { return { @@ -133,6 +134,43 @@ function buildProvider(): ProviderPlugin { }; } +function buildProviderWithDefaultModelPatch(): ProviderPlugin { + return { + id: LOCAL_PROVIDER_ID, + label: LOCAL_PROVIDER_LABEL, + auth: [ + { + id: LOCAL_AUTH_METHOD_ID, + label: LOCAL_PROVIDER_LABEL, + kind: "custom", + run: async () => ({ + profiles: [ + { + profileId: LOCAL_PROFILE_ID, + credential: { + type: "api_key", + provider: LOCAL_PROVIDER_ID, + key: LOCAL_API_KEY, + }, + }, + ], + configPatch: { + agents: { + defaults: { + model: { primary: LOCAL_DEFAULT_MODEL }, + models: { + [LOCAL_DEFAULT_MODEL]: { alias: "Local default" }, + }, + }, + }, + }, + defaultModel: LOCAL_DEFAULT_MODEL, + }), + }, + ], + }; +} + function buildParams(overrides: Partial = {}): ApplyAuthChoiceParams { return { authChoice: LOCAL_PROVIDER_ID, @@ -332,6 +370,48 @@ describe("applyAuthChoiceLoadedPluginProvider", () => { }); }); + it("keeps an existing default when provider auth patches its own primary model", async () => { + const provider = buildProviderWithDefaultModelPatch(); + resolvePluginProviders.mockReturnValue([provider]); + resolveProviderPluginChoice.mockReturnValue({ + provider, + method: provider.auth[0], + }); + const note = vi.fn(async () => {}); + + const result = await applyAuthChoiceLoadedPluginProvider( + buildParams({ + config: { + agents: { + defaults: { + model: { primary: EXISTING_DEFAULT_MODEL }, + models: { + [EXISTING_DEFAULT_MODEL]: { alias: "Bedrock" }, + }, + }, + }, + }, + prompter: { + note, + } as unknown as ApplyAuthChoiceParams["prompter"], + preserveExistingDefaultModel: true, + }), + ); + + expect(result?.config.agents?.defaults?.model).toEqual({ + primary: EXISTING_DEFAULT_MODEL, + }); + expect(result?.config.agents?.defaults?.models).toEqual({ + [EXISTING_DEFAULT_MODEL]: { alias: "Bedrock" }, + [LOCAL_DEFAULT_MODEL]: { alias: "Local default" }, + }); + expect(runProviderModelSelectedHook).not.toHaveBeenCalled(); + expect(note).toHaveBeenCalledWith( + `Kept existing default model ${EXISTING_DEFAULT_MODEL}; ${LOCAL_DEFAULT_MODEL} is available.`, + "Model configured", + ); + }); + it("uses manifest-owned setup providers without loading the broad provider runtime", async () => { const provider = buildProvider(); resolveManifestProviderAuthChoice.mockReturnValue({ @@ -611,6 +691,59 @@ describe("applyAuthChoiceLoadedPluginProvider", () => { ); }); + it("preserves the existing primary model for plugin auth choices that patch defaults", async () => { + const provider = buildProviderWithDefaultModelPatch(); + resolvePluginProviders.mockReturnValue([provider]); + const note = vi.fn(async () => {}); + + const result = await applyAuthChoicePluginProvider( + buildParams({ + authChoice: `provider-plugin:${LOCAL_PROVIDER_ID}:${LOCAL_AUTH_METHOD_ID}`, + config: { + agents: { + defaults: { + model: { primary: EXISTING_DEFAULT_MODEL }, + models: { + [EXISTING_DEFAULT_MODEL]: { alias: "Bedrock" }, + }, + }, + }, + }, + prompter: { + note, + } as unknown as ApplyAuthChoiceParams["prompter"], + preserveExistingDefaultModel: true, + }), + { + authChoice: `provider-plugin:${LOCAL_PROVIDER_ID}:${LOCAL_AUTH_METHOD_ID}`, + pluginId: LOCAL_PROVIDER_ID, + providerId: LOCAL_PROVIDER_ID, + methodId: LOCAL_AUTH_METHOD_ID, + label: LOCAL_PROVIDER_LABEL, + }, + ); + + expect(result?.config.agents?.defaults?.model).toEqual({ + primary: EXISTING_DEFAULT_MODEL, + }); + expect(result?.config.agents?.defaults?.models).toEqual({ + [EXISTING_DEFAULT_MODEL]: { alias: "Bedrock" }, + [LOCAL_DEFAULT_MODEL]: { alias: "Local default" }, + }); + expect(result?.config.plugins).toEqual({ + entries: { + [LOCAL_PROVIDER_ID]: { + enabled: true, + }, + }, + }); + expect(runProviderModelSelectedHook).not.toHaveBeenCalled(); + expect(note).toHaveBeenCalledWith( + `Kept existing default model ${EXISTING_DEFAULT_MODEL}; ${LOCAL_DEFAULT_MODEL} is available.`, + "Model configured", + ); + }); + it("stops early when the plugin is disabled in config", async () => { const note = vi.fn(async () => {}); diff --git a/src/plugins/provider-auth-choice.ts b/src/plugins/provider-auth-choice.ts index 36388861ed2..7543c3ff7cd 100644 --- a/src/plugins/provider-auth-choice.ts +++ b/src/plugins/provider-auth-choice.ts @@ -130,18 +130,24 @@ async function noteDefaultModelResult(params: { async function applyDefaultModelFromAuthChoice(params: { config: OpenClawConfig; + configBeforeProviderAuth?: OpenClawConfig; selectedModel: string; selectedModelDisplay?: string; preserveExistingDefaultModel: boolean | undefined; prompter: WizardPrompter; runSelectedModelHook: (config: OpenClawConfig) => Promise; }): Promise { - const previousPrimary = resolveConfiguredDefaultModelPrimary(params.config); + const defaultModelBaseConfig = params.configBeforeProviderAuth ?? params.config; + const previousPrimary = resolveConfiguredDefaultModelPrimary(defaultModelBaseConfig); const preservesDifferentPrimary = params.preserveExistingDefaultModel === true && previousPrimary !== undefined && previousPrimary !== params.selectedModel; - const nextConfig = applyDefaultModel(params.config, params.selectedModel, { + const defaultModelConfig = + params.preserveExistingDefaultModel === true + ? restoreConfiguredPrimaryModel(params.config, defaultModelBaseConfig) + : params.config; + const nextConfig = applyDefaultModel(defaultModelConfig, params.selectedModel, { preserveExistingPrimary: params.preserveExistingDefaultModel === true, }); if (!preservesDifferentPrimary) { @@ -394,6 +400,7 @@ export async function applyAuthChoiceLoadedPluginProvider( nextConfig = enabledConfig; } + const configBeforeProviderAuth = nextConfig; const applied = await runProviderPluginAuthMethod({ config: nextConfig, env: params.env, @@ -416,6 +423,7 @@ export async function applyAuthChoiceLoadedPluginProvider( if (params.setDefaultModel) { nextConfig = await applyDefaultModelFromAuthChoice({ config: nextConfig, + configBeforeProviderAuth, selectedModel, selectedModelDisplay, preserveExistingDefaultModel: params.preserveExistingDefaultModel, @@ -488,6 +496,7 @@ export async function applyAuthChoicePluginProvider( return { config: nextConfig }; } + const configBeforeProviderAuth = nextConfig; const applied = await runProviderPluginAuthMethod({ config: nextConfig, env: params.env, @@ -509,6 +518,7 @@ export async function applyAuthChoicePluginProvider( if (params.setDefaultModel) { nextConfig = await applyDefaultModelFromAuthChoice({ config: nextConfig, + configBeforeProviderAuth, selectedModel, selectedModelDisplay, preserveExistingDefaultModel: params.preserveExistingDefaultModel,