Skip to content

Commit 4c4eea9

Browse files
committed
fix(configure): tighten fresh setup provider UX
1 parent ffce904 commit 4c4eea9

6 files changed

Lines changed: 91 additions & 17 deletions

src/commands/configure.gateway-auth.prompt-auth-config.test.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ const mocks = vi.hoisted(() => ({
1010
promptCustomApiConfig: vi.fn(),
1111
resolvePluginProviders: vi.fn(() => []),
1212
resolveProviderPluginChoice: vi.fn<() => unknown>(() => null),
13+
resolvePreferredProviderForAuthChoice: vi.fn<() => Promise<string | undefined>>(
14+
async () => undefined,
15+
),
1316
}));
1417

1518
vi.mock("../agents/auth-profiles.js", () => ({
@@ -25,7 +28,7 @@ vi.mock("./auth-choice-prompt.js", () => ({
2528

2629
vi.mock("./auth-choice.js", () => ({
2730
applyAuthChoice: mocks.applyAuthChoice,
28-
resolvePreferredProviderForAuthChoice: vi.fn(async () => undefined),
31+
resolvePreferredProviderForAuthChoice: mocks.resolvePreferredProviderForAuthChoice,
2932
}));
3033

3134
vi.mock("./model-picker.js", async (importActual) => {
@@ -157,4 +160,19 @@ describe("promptAuthConfig", () => {
157160
}),
158161
);
159162
});
163+
164+
it("scopes the allowlist picker to the selected provider when available", async () => {
165+
mocks.promptAuthChoiceGrouped.mockResolvedValue("openai-api-key");
166+
mocks.resolvePreferredProviderForAuthChoice.mockResolvedValue("openai");
167+
mocks.applyAuthChoice.mockResolvedValue({ config: {} });
168+
mocks.promptModelAllowlist.mockResolvedValue({ models: undefined });
169+
170+
await promptAuthConfig({}, makeRuntime(), noopPrompter);
171+
172+
expect(mocks.promptModelAllowlist).toHaveBeenCalledWith(
173+
expect.objectContaining({
174+
preferredProvider: "openai",
175+
}),
176+
);
177+
});
160178
});

src/commands/configure.gateway-auth.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,13 @@ export async function promptAuthConfig(
110110
});
111111

112112
let next = cfg;
113+
const preferredProvider =
114+
authChoice === "skip"
115+
? undefined
116+
: await resolvePreferredProviderForAuthChoice({
117+
choice: authChoice,
118+
config: cfg,
119+
});
113120
if (authChoice === "custom-api-key") {
114121
const customResult = await promptCustomApiConfig({ prompter, runtime, config: next });
115122
next = customResult.config;
@@ -129,10 +136,7 @@ export async function promptAuthConfig(
129136
allowKeep: true,
130137
ignoreAllowlist: true,
131138
includeProviderPluginSetups: true,
132-
preferredProvider: await resolvePreferredProviderForAuthChoice({
133-
choice: authChoice,
134-
config: next,
135-
}),
139+
preferredProvider,
136140
workspaceDir: resolveDefaultAgentWorkspaceDir(),
137141
runtime,
138142
});
@@ -157,6 +161,7 @@ export async function promptAuthConfig(
157161
allowedKeys: modelAllowlist?.allowedKeys,
158162
initialSelections: modelAllowlist?.initialSelections,
159163
message: modelAllowlist?.message,
164+
preferredProvider,
160165
});
161166
if (allowlistSelection.models) {
162167
next = applyModelAllowlist(next, allowlistSelection.models);

src/commands/doctor-memory-search.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ describe("noteMemorySearchHealth", () => {
263263
// provider: "local". So with no local file and no API keys, warn.
264264
expect(note).toHaveBeenCalledTimes(1);
265265
const message = String(note.mock.calls[0]?.[0] ?? "");
266+
expect(message).toContain("needs at least one embedding provider");
266267
expect(message).toContain("openclaw configure --section model");
267268
});
268269

src/commands/doctor-memory-search.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,8 @@ export async function noteMemorySearchHealth(
139139

140140
note(
141141
[
142-
"Memory search is enabled but no embedding provider is configured.",
143-
"Semantic recall will not work without an embedding provider.",
142+
"Memory search is enabled, but no embedding provider is ready.",
143+
"Semantic recall needs at least one embedding provider.",
144144
gatewayProbeWarning ? gatewayProbeWarning : null,
145145
"",
146146
"Fix (pick one):",

src/commands/model-picker.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,42 @@ describe("promptModelAllowlist", () => {
181181
"anthropic/claude-opus-4-5",
182182
]);
183183
});
184+
185+
it("scopes the initial allowlist picker to the preferred provider", async () => {
186+
loadModelCatalog.mockResolvedValue([
187+
{
188+
provider: "anthropic",
189+
id: "claude-sonnet-4-5",
190+
name: "Claude Sonnet 4.5",
191+
},
192+
{
193+
provider: "openai",
194+
id: "gpt-5.4",
195+
name: "GPT-5.4",
196+
},
197+
{
198+
provider: "openai",
199+
id: "gpt-5.4-mini",
200+
name: "GPT-5.4 Mini",
201+
},
202+
]);
203+
204+
const multiselect = createSelectAllMultiselect();
205+
const prompter = makePrompter({ multiselect });
206+
const config = { agents: { defaults: {} } } as OpenClawConfig;
207+
208+
await promptModelAllowlist({
209+
config,
210+
prompter,
211+
preferredProvider: "openai",
212+
});
213+
214+
const options = multiselect.mock.calls[0]?.[0]?.options ?? [];
215+
expect(options.map((opt: { value: string }) => opt.value)).toEqual([
216+
"openai/gpt-5.4",
217+
"openai/gpt-5.4-mini",
218+
]);
219+
});
184220
});
185221

186222
describe("router model filtering", () => {

src/commands/model-picker.ts

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,16 @@ function addModelSelectOption(params: {
162162
params.seen.add(key);
163163
}
164164

165+
function matchesPreferredProvider(entryProvider: string, preferredProvider: string): boolean {
166+
if (preferredProvider === "volcengine") {
167+
return entryProvider === "volcengine" || entryProvider === "volcengine-plan";
168+
}
169+
if (preferredProvider === "byteplus") {
170+
return entryProvider === "byteplus" || entryProvider === "byteplus-plan";
171+
}
172+
return entryProvider === preferredProvider;
173+
}
174+
165175
async function promptManualModel(params: {
166176
prompter: WizardPrompter;
167177
allowBlank: boolean;
@@ -261,15 +271,7 @@ export async function promptDefaultModel(
261271
}
262272

263273
if (hasPreferredProvider && preferredProvider) {
264-
models = models.filter((entry) => {
265-
if (preferredProvider === "volcengine") {
266-
return entry.provider === "volcengine" || entry.provider === "volcengine-plan";
267-
}
268-
if (preferredProvider === "byteplus") {
269-
return entry.provider === "byteplus" || entry.provider === "byteplus-plan";
270-
}
271-
return entry.provider === preferredProvider;
272-
});
274+
models = models.filter((entry) => matchesPreferredProvider(entry.provider, preferredProvider));
273275
}
274276

275277
const agentDir = params.agentDir;
@@ -429,11 +431,16 @@ export async function promptModelAllowlist(params: {
429431
agentDir?: string;
430432
allowedKeys?: string[];
431433
initialSelections?: string[];
434+
preferredProvider?: string;
432435
}): Promise<PromptModelAllowlistResult> {
433436
const cfg = params.config;
434437
const existingKeys = resolveConfiguredModelKeys(cfg);
435438
const allowedKeys = normalizeModelKeys(params.allowedKeys ?? []);
436439
const allowedKeySet = allowedKeys.length > 0 ? new Set(allowedKeys) : null;
440+
const preferredProviderRaw = params.preferredProvider?.trim();
441+
const preferredProvider = preferredProviderRaw
442+
? normalizeProviderId(preferredProviderRaw)
443+
: undefined;
437444
const resolved = resolveConfiguredModelRef({
438445
cfg,
439446
defaultProvider: DEFAULT_PROVIDER,
@@ -477,9 +484,16 @@ export async function promptModelAllowlist(params: {
477484
const options: WizardSelectOption[] = [];
478485
const seen = new Set<string>();
479486

480-
const filteredCatalog = allowedKeySet
487+
const allowedCatalog = allowedKeySet
481488
? catalog.filter((entry) => allowedKeySet.has(modelKey(entry.provider, entry.id)))
482489
: catalog;
490+
const filteredCatalog =
491+
preferredProvider &&
492+
allowedCatalog.some((entry) => matchesPreferredProvider(entry.provider, preferredProvider))
493+
? allowedCatalog.filter((entry) =>
494+
matchesPreferredProvider(entry.provider, preferredProvider),
495+
)
496+
: allowedCatalog;
483497

484498
for (const entry of filteredCatalog) {
485499
addModelSelectOption({ entry, options, seen, aliasIndex, hasAuth });

0 commit comments

Comments
 (0)