Skip to content

Commit c64a732

Browse files
committed
fix(providers): preserve defaults during auth setup
1 parent dd1c6cc commit c64a732

5 files changed

Lines changed: 155 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Docs: https://docs.openclaw.ai
2222
- 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.
2323
- 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.
2424
- 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.
25+
- 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.
2526
- 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.
2627
- 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.
2728
- 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.

docs/cli/configure.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ title: "Configure"
1010
Interactive prompt to set up credentials, devices, and agent defaults.
1111

1212
<Note>
13-
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 <id> --set-default` or `openclaw models set <model>` when you intentionally want to change the default model.
13+
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.
14+
15+
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 <id> --set-default` or `openclaw models set <model>` when you intentionally want to change the default model.
1416
</Note>
1517

1618
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.

docs/concepts/model-providers.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ Reference for **LLM/model providers** (not chat channels like WhatsApp/Telegram)
1919
- `models.providers.*.contextWindow` / `contextTokens` / `maxTokens` set provider-level defaults; `models.providers.*.models[].contextWindow` / `contextTokens` / `maxTokens` override them per model.
2020
- Fallback rules, cooldown probes, and session-override persistence: [Model failover](/concepts/model-failover).
2121

22+
</Accordion>
23+
<Accordion title="Adding provider auth does not change your primary model">
24+
`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."
25+
26+
To intentionally switch the default model, use `openclaw models set <provider/model>` or `openclaw models auth login --provider <id> --set-default`.
27+
2228
</Accordion>
2329
<Accordion title="OpenAI provider/runtime split">
2430
OpenAI-family routes are prefix-specific:

src/commands/auth-choice.apply.plugin-provider.test.ts

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ const LOCAL_AUTH_METHOD_ID = "local";
105105
const LOCAL_PROFILE_ID = `${LOCAL_PROVIDER_ID}:default`;
106106
const LOCAL_API_KEY = "local-provider-key";
107107
const LOCAL_DEFAULT_MODEL = `${LOCAL_PROVIDER_ID}/demo-model`;
108+
const EXISTING_DEFAULT_MODEL = "amazon-bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0";
108109

109110
function buildProvider(): ProviderPlugin {
110111
return {
@@ -133,6 +134,43 @@ function buildProvider(): ProviderPlugin {
133134
};
134135
}
135136

137+
function buildProviderWithDefaultModelPatch(): ProviderPlugin {
138+
return {
139+
id: LOCAL_PROVIDER_ID,
140+
label: LOCAL_PROVIDER_LABEL,
141+
auth: [
142+
{
143+
id: LOCAL_AUTH_METHOD_ID,
144+
label: LOCAL_PROVIDER_LABEL,
145+
kind: "custom",
146+
run: async () => ({
147+
profiles: [
148+
{
149+
profileId: LOCAL_PROFILE_ID,
150+
credential: {
151+
type: "api_key",
152+
provider: LOCAL_PROVIDER_ID,
153+
key: LOCAL_API_KEY,
154+
},
155+
},
156+
],
157+
configPatch: {
158+
agents: {
159+
defaults: {
160+
model: { primary: LOCAL_DEFAULT_MODEL },
161+
models: {
162+
[LOCAL_DEFAULT_MODEL]: { alias: "Local default" },
163+
},
164+
},
165+
},
166+
},
167+
defaultModel: LOCAL_DEFAULT_MODEL,
168+
}),
169+
},
170+
],
171+
};
172+
}
173+
136174
function buildParams(overrides: Partial<ApplyAuthChoiceParams> = {}): ApplyAuthChoiceParams {
137175
return {
138176
authChoice: LOCAL_PROVIDER_ID,
@@ -332,6 +370,48 @@ describe("applyAuthChoiceLoadedPluginProvider", () => {
332370
});
333371
});
334372

373+
it("keeps an existing default when provider auth patches its own primary model", async () => {
374+
const provider = buildProviderWithDefaultModelPatch();
375+
resolvePluginProviders.mockReturnValue([provider]);
376+
resolveProviderPluginChoice.mockReturnValue({
377+
provider,
378+
method: provider.auth[0],
379+
});
380+
const note = vi.fn(async () => {});
381+
382+
const result = await applyAuthChoiceLoadedPluginProvider(
383+
buildParams({
384+
config: {
385+
agents: {
386+
defaults: {
387+
model: { primary: EXISTING_DEFAULT_MODEL },
388+
models: {
389+
[EXISTING_DEFAULT_MODEL]: { alias: "Bedrock" },
390+
},
391+
},
392+
},
393+
},
394+
prompter: {
395+
note,
396+
} as unknown as ApplyAuthChoiceParams["prompter"],
397+
preserveExistingDefaultModel: true,
398+
}),
399+
);
400+
401+
expect(result?.config.agents?.defaults?.model).toEqual({
402+
primary: EXISTING_DEFAULT_MODEL,
403+
});
404+
expect(result?.config.agents?.defaults?.models).toEqual({
405+
[EXISTING_DEFAULT_MODEL]: { alias: "Bedrock" },
406+
[LOCAL_DEFAULT_MODEL]: { alias: "Local default" },
407+
});
408+
expect(runProviderModelSelectedHook).not.toHaveBeenCalled();
409+
expect(note).toHaveBeenCalledWith(
410+
`Kept existing default model ${EXISTING_DEFAULT_MODEL}; ${LOCAL_DEFAULT_MODEL} is available.`,
411+
"Model configured",
412+
);
413+
});
414+
335415
it("uses manifest-owned setup providers without loading the broad provider runtime", async () => {
336416
const provider = buildProvider();
337417
resolveManifestProviderAuthChoice.mockReturnValue({
@@ -611,6 +691,59 @@ describe("applyAuthChoiceLoadedPluginProvider", () => {
611691
);
612692
});
613693

694+
it("preserves the existing primary model for plugin auth choices that patch defaults", async () => {
695+
const provider = buildProviderWithDefaultModelPatch();
696+
resolvePluginProviders.mockReturnValue([provider]);
697+
const note = vi.fn(async () => {});
698+
699+
const result = await applyAuthChoicePluginProvider(
700+
buildParams({
701+
authChoice: `provider-plugin:${LOCAL_PROVIDER_ID}:${LOCAL_AUTH_METHOD_ID}`,
702+
config: {
703+
agents: {
704+
defaults: {
705+
model: { primary: EXISTING_DEFAULT_MODEL },
706+
models: {
707+
[EXISTING_DEFAULT_MODEL]: { alias: "Bedrock" },
708+
},
709+
},
710+
},
711+
},
712+
prompter: {
713+
note,
714+
} as unknown as ApplyAuthChoiceParams["prompter"],
715+
preserveExistingDefaultModel: true,
716+
}),
717+
{
718+
authChoice: `provider-plugin:${LOCAL_PROVIDER_ID}:${LOCAL_AUTH_METHOD_ID}`,
719+
pluginId: LOCAL_PROVIDER_ID,
720+
providerId: LOCAL_PROVIDER_ID,
721+
methodId: LOCAL_AUTH_METHOD_ID,
722+
label: LOCAL_PROVIDER_LABEL,
723+
},
724+
);
725+
726+
expect(result?.config.agents?.defaults?.model).toEqual({
727+
primary: EXISTING_DEFAULT_MODEL,
728+
});
729+
expect(result?.config.agents?.defaults?.models).toEqual({
730+
[EXISTING_DEFAULT_MODEL]: { alias: "Bedrock" },
731+
[LOCAL_DEFAULT_MODEL]: { alias: "Local default" },
732+
});
733+
expect(result?.config.plugins).toEqual({
734+
entries: {
735+
[LOCAL_PROVIDER_ID]: {
736+
enabled: true,
737+
},
738+
},
739+
});
740+
expect(runProviderModelSelectedHook).not.toHaveBeenCalled();
741+
expect(note).toHaveBeenCalledWith(
742+
`Kept existing default model ${EXISTING_DEFAULT_MODEL}; ${LOCAL_DEFAULT_MODEL} is available.`,
743+
"Model configured",
744+
);
745+
});
746+
614747
it("stops early when the plugin is disabled in config", async () => {
615748
const note = vi.fn(async () => {});
616749

src/plugins/provider-auth-choice.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,18 +130,24 @@ async function noteDefaultModelResult(params: {
130130

131131
async function applyDefaultModelFromAuthChoice(params: {
132132
config: OpenClawConfig;
133+
configBeforeProviderAuth?: OpenClawConfig;
133134
selectedModel: string;
134135
selectedModelDisplay?: string;
135136
preserveExistingDefaultModel: boolean | undefined;
136137
prompter: WizardPrompter;
137138
runSelectedModelHook: (config: OpenClawConfig) => Promise<void>;
138139
}): Promise<OpenClawConfig> {
139-
const previousPrimary = resolveConfiguredDefaultModelPrimary(params.config);
140+
const defaultModelBaseConfig = params.configBeforeProviderAuth ?? params.config;
141+
const previousPrimary = resolveConfiguredDefaultModelPrimary(defaultModelBaseConfig);
140142
const preservesDifferentPrimary =
141143
params.preserveExistingDefaultModel === true &&
142144
previousPrimary !== undefined &&
143145
previousPrimary !== params.selectedModel;
144-
const nextConfig = applyDefaultModel(params.config, params.selectedModel, {
146+
const defaultModelConfig =
147+
params.preserveExistingDefaultModel === true
148+
? restoreConfiguredPrimaryModel(params.config, defaultModelBaseConfig)
149+
: params.config;
150+
const nextConfig = applyDefaultModel(defaultModelConfig, params.selectedModel, {
145151
preserveExistingPrimary: params.preserveExistingDefaultModel === true,
146152
});
147153
if (!preservesDifferentPrimary) {
@@ -394,6 +400,7 @@ export async function applyAuthChoiceLoadedPluginProvider(
394400
nextConfig = enabledConfig;
395401
}
396402

403+
const configBeforeProviderAuth = nextConfig;
397404
const applied = await runProviderPluginAuthMethod({
398405
config: nextConfig,
399406
env: params.env,
@@ -416,6 +423,7 @@ export async function applyAuthChoiceLoadedPluginProvider(
416423
if (params.setDefaultModel) {
417424
nextConfig = await applyDefaultModelFromAuthChoice({
418425
config: nextConfig,
426+
configBeforeProviderAuth,
419427
selectedModel,
420428
selectedModelDisplay,
421429
preserveExistingDefaultModel: params.preserveExistingDefaultModel,
@@ -488,6 +496,7 @@ export async function applyAuthChoicePluginProvider(
488496
return { config: nextConfig };
489497
}
490498

499+
const configBeforeProviderAuth = nextConfig;
491500
const applied = await runProviderPluginAuthMethod({
492501
config: nextConfig,
493502
env: params.env,
@@ -509,6 +518,7 @@ export async function applyAuthChoicePluginProvider(
509518
if (params.setDefaultModel) {
510519
nextConfig = await applyDefaultModelFromAuthChoice({
511520
config: nextConfig,
521+
configBeforeProviderAuth,
512522
selectedModel,
513523
selectedModelDisplay,
514524
preserveExistingDefaultModel: params.preserveExistingDefaultModel,

0 commit comments

Comments
 (0)