mirror of https://github.com/openclaw/openclaw.git
fix(configure): tighten fresh setup provider UX
This commit is contained in:
parent
ffce904a10
commit
4c4eea97e9
|
|
@ -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",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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):",
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
Loading…
Reference in New Issue