fix(configure): tighten fresh setup provider UX

This commit is contained in:
Vincent Koc
2026-03-21 08:30:55 -07:00
parent ffce904a10
commit 4c4eea97e9
6 changed files with 91 additions and 17 deletions

View File

@@ -10,6 +10,9 @@ const mocks = vi.hoisted(() => ({
promptCustomApiConfig: vi.fn(),
resolvePluginProviders: vi.fn(() => []),
resolveProviderPluginChoice: vi.fn<() => unknown>(() => null),
resolvePreferredProviderForAuthChoice: vi.fn<() => Promise<string | undefined>>(
async () => undefined,
),
}));
vi.mock("../agents/auth-profiles.js", () => ({
@@ -25,7 +28,7 @@ vi.mock("./auth-choice-prompt.js", () => ({
vi.mock("./auth-choice.js", () => ({
applyAuthChoice: mocks.applyAuthChoice,
resolvePreferredProviderForAuthChoice: vi.fn(async () => undefined),
resolvePreferredProviderForAuthChoice: mocks.resolvePreferredProviderForAuthChoice,
}));
vi.mock("./model-picker.js", async (importActual) => {
@@ -157,4 +160,19 @@ describe("promptAuthConfig", () => {
}),
);
});
it("scopes the allowlist picker to the selected provider when available", async () => {
mocks.promptAuthChoiceGrouped.mockResolvedValue("openai-api-key");
mocks.resolvePreferredProviderForAuthChoice.mockResolvedValue("openai");
mocks.applyAuthChoice.mockResolvedValue({ config: {} });
mocks.promptModelAllowlist.mockResolvedValue({ models: undefined });
await promptAuthConfig({}, makeRuntime(), noopPrompter);
expect(mocks.promptModelAllowlist).toHaveBeenCalledWith(
expect.objectContaining({
preferredProvider: "openai",
}),
);
});
});

View File

@@ -110,6 +110,13 @@ export async function promptAuthConfig(
});
let next = cfg;
const preferredProvider =
authChoice === "skip"
? undefined
: await resolvePreferredProviderForAuthChoice({
choice: authChoice,
config: cfg,
});
if (authChoice === "custom-api-key") {
const customResult = await promptCustomApiConfig({ prompter, runtime, config: next });
next = customResult.config;
@@ -129,10 +136,7 @@ export async function promptAuthConfig(
allowKeep: true,
ignoreAllowlist: true,
includeProviderPluginSetups: true,
preferredProvider: await resolvePreferredProviderForAuthChoice({
choice: authChoice,
config: next,
}),
preferredProvider,
workspaceDir: resolveDefaultAgentWorkspaceDir(),
runtime,
});
@@ -157,6 +161,7 @@ export async function promptAuthConfig(
allowedKeys: modelAllowlist?.allowedKeys,
initialSelections: modelAllowlist?.initialSelections,
message: modelAllowlist?.message,
preferredProvider,
});
if (allowlistSelection.models) {
next = applyModelAllowlist(next, allowlistSelection.models);

View File

@@ -263,6 +263,7 @@ describe("noteMemorySearchHealth", () => {
// provider: "local". So with no local file and no API keys, warn.
expect(note).toHaveBeenCalledTimes(1);
const message = String(note.mock.calls[0]?.[0] ?? "");
expect(message).toContain("needs at least one embedding provider");
expect(message).toContain("openclaw configure --section model");
});

View File

@@ -139,8 +139,8 @@ export async function noteMemorySearchHealth(
note(
[
"Memory search is enabled but no embedding provider is configured.",
"Semantic recall will not work without an embedding provider.",
"Memory search is enabled, but no embedding provider is ready.",
"Semantic recall needs at least one embedding provider.",
gatewayProbeWarning ? gatewayProbeWarning : null,
"",
"Fix (pick one):",

View File

@@ -181,6 +181,42 @@ describe("promptModelAllowlist", () => {
"anthropic/claude-opus-4-5",
]);
});
it("scopes the initial allowlist picker to the preferred provider", async () => {
loadModelCatalog.mockResolvedValue([
{
provider: "anthropic",
id: "claude-sonnet-4-5",
name: "Claude Sonnet 4.5",
},
{
provider: "openai",
id: "gpt-5.4",
name: "GPT-5.4",
},
{
provider: "openai",
id: "gpt-5.4-mini",
name: "GPT-5.4 Mini",
},
]);
const multiselect = createSelectAllMultiselect();
const prompter = makePrompter({ multiselect });
const config = { agents: { defaults: {} } } as OpenClawConfig;
await promptModelAllowlist({
config,
prompter,
preferredProvider: "openai",
});
const options = multiselect.mock.calls[0]?.[0]?.options ?? [];
expect(options.map((opt: { value: string }) => opt.value)).toEqual([
"openai/gpt-5.4",
"openai/gpt-5.4-mini",
]);
});
});
describe("router model filtering", () => {

View File

@@ -162,6 +162,16 @@ function addModelSelectOption(params: {
params.seen.add(key);
}
function matchesPreferredProvider(entryProvider: string, preferredProvider: string): boolean {
if (preferredProvider === "volcengine") {
return entryProvider === "volcengine" || entryProvider === "volcengine-plan";
}
if (preferredProvider === "byteplus") {
return entryProvider === "byteplus" || entryProvider === "byteplus-plan";
}
return entryProvider === preferredProvider;
}
async function promptManualModel(params: {
prompter: WizardPrompter;
allowBlank: boolean;
@@ -261,15 +271,7 @@ export async function promptDefaultModel(
}
if (hasPreferredProvider && preferredProvider) {
models = models.filter((entry) => {
if (preferredProvider === "volcengine") {
return entry.provider === "volcengine" || entry.provider === "volcengine-plan";
}
if (preferredProvider === "byteplus") {
return entry.provider === "byteplus" || entry.provider === "byteplus-plan";
}
return entry.provider === preferredProvider;
});
models = models.filter((entry) => matchesPreferredProvider(entry.provider, preferredProvider));
}
const agentDir = params.agentDir;
@@ -429,11 +431,16 @@ export async function promptModelAllowlist(params: {
agentDir?: string;
allowedKeys?: string[];
initialSelections?: string[];
preferredProvider?: string;
}): Promise<PromptModelAllowlistResult> {
const cfg = params.config;
const existingKeys = resolveConfiguredModelKeys(cfg);
const allowedKeys = normalizeModelKeys(params.allowedKeys ?? []);
const allowedKeySet = allowedKeys.length > 0 ? new Set(allowedKeys) : null;
const preferredProviderRaw = params.preferredProvider?.trim();
const preferredProvider = preferredProviderRaw
? normalizeProviderId(preferredProviderRaw)
: undefined;
const resolved = resolveConfiguredModelRef({
cfg,
defaultProvider: DEFAULT_PROVIDER,
@@ -477,9 +484,16 @@ export async function promptModelAllowlist(params: {
const options: WizardSelectOption[] = [];
const seen = new Set<string>();
const filteredCatalog = allowedKeySet
const allowedCatalog = allowedKeySet
? catalog.filter((entry) => allowedKeySet.has(modelKey(entry.provider, entry.id)))
: catalog;
const filteredCatalog =
preferredProvider &&
allowedCatalog.some((entry) => matchesPreferredProvider(entry.provider, preferredProvider))
? allowedCatalog.filter((entry) =>
matchesPreferredProvider(entry.provider, preferredProvider),
)
: allowedCatalog;
for (const entry of filteredCatalog) {
addModelSelectOption({ entry, options, seen, aliasIndex, hasAuth });