From 2da2057a37ecfc7c3efc2051e2ef7aa7198d45fb Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 12 Jan 2026 06:02:39 +0000 Subject: [PATCH] feat(model): add /model picker --- CHANGELOG.md | 1 + docs/providers/minimax.md | 4 +- docs/start/faq.md | 7 + src/agents/pi-embedded-runner.test.ts | 6 +- src/auto-reply/reply.directive.test.ts | 17 +- src/auto-reply/reply.triggers.test.ts | 120 +++++++ src/auto-reply/reply/directive-handling.ts | 399 +++++++++++++++------ src/commands/auth-choice-prompt.ts | 5 +- src/commands/auth-choice.test.ts | 7 + src/commands/onboard-helpers.ts | 4 +- 10 files changed, 434 insertions(+), 136 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a32535230..dc24979f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ ### Changes - CLI: simplify configure section selection (single-select with optional add-more). - Onboarding/CLI: group model/auth choice by provider and label Z.AI as GLM 4.7. +- Auto-reply: add compact `/model` picker (models + available providers) and show provider endpoints in `/model status`. - Plugins: add extension loader (tools/RPC/CLI/services), discovery paths, and config schema + Control UI labels (uiHints). - Plugins: add `clawdbot plugins install` (path/tgz/npm), plus `list|info|enable|disable|doctor` UX. - Plugins: voice-call plugin now real (Twilio/log), adds start/status RPC/CLI/tool + tests. diff --git a/docs/providers/minimax.md b/docs/providers/minimax.md index 3d5857e76..98d91dc25 100644 --- a/docs/providers/minimax.md +++ b/docs/providers/minimax.md @@ -151,8 +151,8 @@ Use the interactive config wizard to set MiniMax without editing JSON: ## Configuration options -- `models.providers.minimax.baseUrl`: `https://api.minimax.io/v1` or `https://api.minimax.io/anthropic`. -- `models.providers.minimax.api`: `openai-completions` (cloud) or `anthropic-messages` (API). +- `models.providers.minimax.baseUrl`: prefer `https://api.minimax.io/anthropic` (Anthropic-compatible); `https://api.minimax.io/v1` is optional for OpenAI-compatible payloads. +- `models.providers.minimax.api`: prefer `anthropic-messages`; `openai-completions` is optional for OpenAI-compatible payloads. - `models.providers.minimax.apiKey`: MiniMax API key (`MINIMAX_API_KEY`). - `models.providers.minimax.models`: define `id`, `name`, `reasoning`, `contextWindow`, `maxTokens`, `cost`. - `agents.defaults.models`: alias models you want in the allowlist. diff --git a/docs/start/faq.md b/docs/start/faq.md index cd596a44e..948f7f6fd 100644 --- a/docs/start/faq.md +++ b/docs/start/faq.md @@ -496,6 +496,12 @@ Use the `/model` command as a standalone message: You can list available models with `/model`, `/model list`, or `/model status`. +`/model` (and `/model list`) shows a compact, numbered picker. Select by number: + +``` +/model 3 +``` + You can also force a specific auth profile for the provider (per session): ``` @@ -504,6 +510,7 @@ You can also force a specific auth profile for the provider (per session): ``` Tip: `/model status` shows which agent is active, which `auth-profiles.json` file is being used, and which auth profile will be tried next. +It also shows the configured provider endpoint (`baseUrl`) and API mode (`api`) when available. ### Why do I see “Model … is not allowed” and then no reply? diff --git a/src/agents/pi-embedded-runner.test.ts b/src/agents/pi-embedded-runner.test.ts index 0b2ddb360..2dd1d0714 100644 --- a/src/agents/pi-embedded-runner.test.ts +++ b/src/agents/pi-embedded-runner.test.ts @@ -611,12 +611,12 @@ describe("runEmbeddedPiAgent", () => { models: { providers: { minimax: { - baseUrl: "https://api.minimax.io/v1", - api: "openai-completions", + baseUrl: "https://api.minimax.io/anthropic", + api: "anthropic-messages", apiKey: "sk-minimax-test", models: [ { - id: "minimax-m2.1", + id: "MiniMax-M2.1", name: "MiniMax M2.1", reasoning: false, input: ["text"], diff --git a/src/auto-reply/reply.directive.test.ts b/src/auto-reply/reply.directive.test.ts index 70a940ddf..84b6c55d9 100644 --- a/src/auto-reply/reply.directive.test.ts +++ b/src/auto-reply/reply.directive.test.ts @@ -1449,9 +1449,9 @@ describe("directive behavior", () => { const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text).toContain("anthropic/claude-opus-4-5"); - expect(text).toContain("openai/gpt-4.1-mini"); + expect(text).toContain("Pick: /model <#> or /model "); + expect(text).toContain("gpt-4.1-mini — openai"); expect(text).not.toContain("claude-sonnet-4-1"); - expect(text).toContain("auth:"); expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); }); }); @@ -1512,9 +1512,9 @@ describe("directive behavior", () => { ); const text = Array.isArray(res) ? res[0]?.text : res?.text; - expect(text).toContain("anthropic/claude-opus-4-5"); - expect(text).toContain("openai/gpt-4.1-mini"); - expect(text).toContain("auth:"); + expect(text).toContain("Pick: /model <#> or /model "); + expect(text).toContain("claude-opus-4-5 — anthropic"); + expect(text).toContain("gpt-4.1-mini — openai"); expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); }); }); @@ -1544,9 +1544,9 @@ describe("directive behavior", () => { ); const text = Array.isArray(res) ? res[0]?.text : res?.text; - expect(text).toContain("Model catalog unavailable"); - expect(text).toContain("anthropic/claude-opus-4-5"); - expect(text).toContain("openai/gpt-4.1-mini"); + expect(text).toContain("Pick: /model <#> or /model "); + expect(text).toContain("claude-opus-4-5 — anthropic"); + expect(text).toContain("gpt-4.1-mini — openai"); expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); }); }); @@ -1574,7 +1574,6 @@ describe("directive behavior", () => { ); const text = Array.isArray(res) ? res[0]?.text : res?.text; - expect(text).toContain("auth:"); expect(text).not.toContain("missing (missing)"); expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); }); diff --git a/src/auto-reply/reply.triggers.test.ts b/src/auto-reply/reply.triggers.test.ts index 5d962a8c5..7413e93d0 100644 --- a/src/auto-reply/reply.triggers.test.ts +++ b/src/auto-reply/reply.triggers.test.ts @@ -27,6 +27,30 @@ const usageMocks = vi.hoisted(() => ({ vi.mock("../infra/provider-usage.js", () => usageMocks); +const modelCatalogMocks = vi.hoisted(() => ({ + loadModelCatalog: vi.fn().mockResolvedValue([ + { + provider: "anthropic", + id: "claude-opus-4-5", + name: "Claude Opus 4.5", + contextWindow: 200000, + }, + { + provider: "openrouter", + id: "anthropic/claude-opus-4-5", + name: "Claude Opus 4.5 (OpenRouter)", + contextWindow: 200000, + }, + { provider: "openai", id: "gpt-4.1-mini", name: "GPT-4.1 mini" }, + { provider: "openai", id: "gpt-5.2", name: "GPT-5.2" }, + { provider: "openai-codex", id: "gpt-5.2", name: "GPT-5.2 (Codex)" }, + { provider: "minimax", id: "MiniMax-M2.1", name: "MiniMax M2.1" }, + ]), + resetModelCatalogCacheForTest: vi.fn(), +})); + +vi.mock("../agents/model-catalog.js", () => modelCatalogMocks); + import { resolveAgentWorkspaceDir } from "../agents/agent-scope.js"; import { abortEmbeddedPiRun, @@ -264,6 +288,102 @@ describe("trigger handling", () => { }); }); + it("shows a quick /model picker grouped by model with providers", async () => { + await withTempHome(async (home) => { + const cfg = makeCfg(home); + const res = await getReplyFromConfig( + { + Body: "/model", + From: "telegram:111", + To: "telegram:111", + ChatType: "direct", + Provider: "telegram", + Surface: "telegram", + SessionKey: "telegram:slash:111", + }, + {}, + cfg, + ); + + const text = Array.isArray(res) ? res[0]?.text : res?.text; + const normalized = normalizeTestText(text ?? ""); + expect(normalized).toContain( + "Pick: /model <#> or /model ", + ); + expect(normalized).toContain( + "1) claude-opus-4-5 — anthropic, openrouter", + ); + expect(normalized).toContain("3) gpt-5.2 — openai, openai-codex"); + expect(normalized).not.toContain("reasoning"); + expect(normalized).not.toContain("image"); + }); + }); + + it("selects a model by index via /model <#>", async () => { + await withTempHome(async (home) => { + const cfg = makeCfg(home); + const sessionKey = "telegram:slash:111"; + + const res = await getReplyFromConfig( + { + Body: "/model 3", + From: "telegram:111", + To: "telegram:111", + ChatType: "direct", + Provider: "telegram", + Surface: "telegram", + SessionKey: sessionKey, + }, + {}, + cfg, + ); + + const text = Array.isArray(res) ? res[0]?.text : res?.text; + expect(normalizeTestText(text ?? "")).toContain( + "Model set to openai/gpt-5.2", + ); + + const store = loadSessionStore(cfg.session.store); + expect(store[sessionKey]?.providerOverride).toBe("openai"); + expect(store[sessionKey]?.modelOverride).toBe("gpt-5.2"); + }); + }); + + it("includes endpoint details in /model status when configured", async () => { + await withTempHome(async (home) => { + const cfg = { + ...makeCfg(home), + models: { + providers: { + minimax: { + baseUrl: "https://api.minimax.io/anthropic", + api: "anthropic-messages", + }, + }, + }, + }; + const res = await getReplyFromConfig( + { + Body: "/model status", + From: "telegram:111", + To: "telegram:111", + ChatType: "direct", + Provider: "telegram", + Surface: "telegram", + SessionKey: "telegram:slash:111", + }, + {}, + cfg, + ); + + const text = Array.isArray(res) ? res[0]?.text : res?.text; + const normalized = normalizeTestText(text ?? ""); + expect(normalized).toContain( + "[minimax] endpoint: https://api.minimax.io/anthropic api: anthropic-messages auth:", + ); + }); + }); + it("rejects /restart by default", async () => { await withTempHome(async (home) => { const res = await getReplyFromConfig( diff --git a/src/auto-reply/reply/directive-handling.ts b/src/auto-reply/reply/directive-handling.ts index 801e3ae8f..6e156db8e 100644 --- a/src/auto-reply/reply/directive-handling.ts +++ b/src/auto-reply/reply/directive-handling.ts @@ -316,6 +316,121 @@ const resolveProfileOverride = (params: { return { profileId: raw }; }; +type ModelPickerCatalogEntry = { + provider: string; + id: string; + name?: string; +}; + +type ModelPickerItem = { + model: string; + providers: string[]; + providerModels: Record; +}; + +const MODEL_PICK_PROVIDER_PREFERENCE = [ + "anthropic", + "openai", + "openai-codex", + "minimax", + "google", + "zai", + "openrouter", + "opencode", + "github-copilot", + "groq", + "cerebras", + "mistral", + "xai", + "lmstudio", +] as const; + +function normalizeModelFamilyId(id: string): string { + const trimmed = id.trim(); + if (!trimmed) return trimmed; + const parts = trimmed.split("/").filter(Boolean); + return parts.length > 0 ? (parts[parts.length - 1] ?? trimmed) : trimmed; +} + +function sortProvidersForPicker(providers: string[]): string[] { + const pref = new Map( + MODEL_PICK_PROVIDER_PREFERENCE.map((provider, idx) => [provider, idx]), + ); + return providers.sort((a, b) => { + const pa = pref.get(a); + const pb = pref.get(b); + if (pa !== undefined && pb !== undefined) return pa - pb; + if (pa !== undefined) return -1; + if (pb !== undefined) return 1; + return a.localeCompare(b); + }); +} + +function buildModelPickerItems( + catalog: ModelPickerCatalogEntry[], +): ModelPickerItem[] { + const byModel = new Map }>(); + for (const entry of catalog) { + const provider = normalizeProviderId(entry.provider); + const model = normalizeModelFamilyId(entry.id); + if (!provider || !model) continue; + const existing = byModel.get(model); + if (existing) { + existing.providerModels[provider] = entry.id; + continue; + } + byModel.set(model, { providerModels: { [provider]: entry.id } }); + } + const out: ModelPickerItem[] = []; + for (const [model, data] of byModel.entries()) { + const providers = sortProvidersForPicker(Object.keys(data.providerModels)); + out.push({ model, providers, providerModels: data.providerModels }); + } + out.sort((a, b) => + a.model.toLowerCase().localeCompare(b.model.toLowerCase()), + ); + return out; +} + +function pickProviderForModel(params: { + item: ModelPickerItem; + preferredProvider?: string; +}): { provider: string; model: string } | null { + const preferred = params.preferredProvider + ? normalizeProviderId(params.preferredProvider) + : undefined; + if (preferred && params.item.providerModels[preferred]) { + return { + provider: preferred, + model: params.item.providerModels[preferred], + }; + } + const first = params.item.providers[0]; + if (!first) return null; + return { + provider: first, + model: params.item.providerModels[first] ?? params.item.model, + }; +} + +function resolveProviderEndpointLabel( + provider: string, + cfg: ClawdbotConfig, +): { endpoint?: string; api?: string } { + const normalized = normalizeProviderId(provider); + const providers = (cfg.models?.providers ?? {}) as Record< + string, + { baseUrl?: string; api?: string } | undefined + >; + const entry = providers[normalized]; + const endpoint = entry?.baseUrl?.trim(); + const api = entry?.api?.trim(); + return { + endpoint: endpoint || undefined, + api: api || undefined, + }; +} + export type InlineDirectives = { cleaned: string; hasThinkDirective: boolean; @@ -527,111 +642,90 @@ export async function handleDirectiveOnly(params: { directives.hasElevatedDirective && !runtimeIsSandboxed; if (directives.hasModelDirective) { - const modelDirective = directives.rawModelDirective?.trim().toLowerCase(); - const isModelListAlias = - modelDirective === "status" || modelDirective === "list"; - if (!directives.rawModelDirective || isModelListAlias) { + const rawDirective = directives.rawModelDirective?.trim(); + const directive = rawDirective?.toLowerCase(); + const wantsStatus = directive === "status"; + const wantsList = !rawDirective || directive === "list"; + + if ((wantsList || wantsStatus) && directives.rawModelProfile) { + return { text: "Auth profile override requires a model selection." }; + } + + const resolvedDefault = resolveConfiguredModelRef({ + cfg: params.cfg, + defaultProvider, + defaultModel, + }); + const pickerCatalog: ModelPickerCatalogEntry[] = (() => { + if (allowedModelCatalog.length > 0) return allowedModelCatalog; + const keys = new Set(); + const out: ModelPickerCatalogEntry[] = []; + for (const raw of Object.keys( + params.cfg.agents?.defaults?.models ?? {}, + )) { + const resolved = resolveModelRefFromString({ + raw: String(raw), + defaultProvider, + aliasIndex, + }); + if (!resolved) continue; + const key = modelKey(resolved.ref.provider, resolved.ref.model); + if (keys.has(key)) continue; + keys.add(key); + out.push({ + provider: resolved.ref.provider, + id: resolved.ref.model, + name: resolved.ref.model, + }); + } + if (out.length === 0 && resolvedDefault.model) { + const key = modelKey(resolvedDefault.provider, resolvedDefault.model); + keys.add(key); + out.push({ + provider: resolvedDefault.provider, + id: resolvedDefault.model, + name: resolvedDefault.model, + }); + } + return out; + })(); + + if (wantsList) { + const items = buildModelPickerItems(pickerCatalog); + if (items.length === 0) return { text: "No models available." }; + const current = `${params.provider}/${params.model}`; + const lines: string[] = [ + `Current: ${current}`, + "Pick: /model <#> or /model ", + ]; + for (const [idx, item] of items.entries()) { + lines.push(`${idx + 1}) ${item.model} — ${item.providers.join(", ")}`); + } + lines.push("", "More: /model status"); + return { text: lines.join("\n") }; + } + + if (wantsStatus) { const modelsPath = `${agentDir}/models.json`; const formatPath = (value: string) => shortenHomePath(value); - const authMode: ModelAuthDetailMode = - modelDirective === "status" ? "verbose" : "compact"; - if (allowedModelCatalog.length === 0) { - const resolvedDefault = resolveConfiguredModelRef({ - cfg: params.cfg, - defaultProvider, - defaultModel, - }); - const fallbackKeys = new Set(); - const fallbackCatalog: Array<{ - provider: string; - id: string; - }> = []; - for (const raw of Object.keys( - params.cfg.agents?.defaults?.models ?? {}, - )) { - const resolved = resolveModelRefFromString({ - raw: String(raw), - defaultProvider, - aliasIndex, - }); - if (!resolved) continue; - const key = modelKey(resolved.ref.provider, resolved.ref.model); - if (fallbackKeys.has(key)) continue; - fallbackKeys.add(key); - fallbackCatalog.push({ - provider: resolved.ref.provider, - id: resolved.ref.model, - }); - } - if (fallbackCatalog.length === 0 && resolvedDefault.model) { - const key = modelKey(resolvedDefault.provider, resolvedDefault.model); - fallbackKeys.add(key); - fallbackCatalog.push({ - provider: resolvedDefault.provider, - id: resolvedDefault.model, - }); - } - if (fallbackCatalog.length === 0) { - return { text: "No models available." }; - } - const authByProvider = new Map(); - for (const entry of fallbackCatalog) { - if (authByProvider.has(entry.provider)) continue; - const auth = await resolveAuthLabel( - entry.provider, - params.cfg, - modelsPath, - agentDir, - authMode, - ); - authByProvider.set(entry.provider, formatAuthLabel(auth)); - } - const current = `${params.provider}/${params.model}`; - const defaultLabel = `${defaultProvider}/${defaultModel}`; - const lines = [ - `Current: ${current}`, - `Default: ${defaultLabel}`, - `Agent: ${activeAgentId}`, - `Auth file: ${formatPath(resolveAuthStorePathForDisplay(agentDir))}`, - `⚠️ Model catalog unavailable; showing configured models only.`, - ]; - const byProvider = new Map(); - for (const entry of fallbackCatalog) { - const models = byProvider.get(entry.provider); - if (models) { - models.push(entry); - continue; - } - byProvider.set(entry.provider, [entry]); - } - for (const provider of byProvider.keys()) { - const models = byProvider.get(provider); - if (!models) continue; - const authLabel = authByProvider.get(provider) ?? "missing"; - lines.push(""); - lines.push(`[${provider}] auth: ${authLabel}`); - for (const entry of models) { - const label = `${entry.provider}/${entry.id}`; - const aliases = aliasIndex.byKey.get(label); - const aliasSuffix = - aliases && aliases.length > 0 ? ` (${aliases.join(", ")})` : ""; - lines.push(` • ${label}${aliasSuffix}`); - } - } - return { text: lines.join("\n") }; - } + const authMode: ModelAuthDetailMode = "verbose"; + const catalog = pickerCatalog; + if (catalog.length === 0) return { text: "No models available." }; + const authByProvider = new Map(); - for (const entry of allowedModelCatalog) { - if (authByProvider.has(entry.provider)) continue; + for (const entry of catalog) { + const provider = normalizeProviderId(entry.provider); + if (authByProvider.has(provider)) continue; const auth = await resolveAuthLabel( - entry.provider, + provider, params.cfg, modelsPath, agentDir, authMode, ); - authByProvider.set(entry.provider, formatAuthLabel(auth)); + authByProvider.set(provider, formatAuthLabel(auth)); } + const current = `${params.provider}/${params.model}`; const defaultLabel = `${defaultProvider}/${defaultModel}`; const lines = [ @@ -644,26 +738,32 @@ export async function handleDirectiveOnly(params: { lines.push(`(previous selection reset to default)`); } - // Group models by provider - const byProvider = new Map(); - for (const entry of allowedModelCatalog) { - const models = byProvider.get(entry.provider); + const byProvider = new Map(); + for (const entry of catalog) { + const provider = normalizeProviderId(entry.provider); + const models = byProvider.get(provider); if (models) { models.push(entry); continue; } - byProvider.set(entry.provider, [entry]); + byProvider.set(provider, [entry]); } - // Iterate over provider groups for (const provider of byProvider.keys()) { const models = byProvider.get(provider); if (!models) continue; const authLabel = authByProvider.get(provider) ?? "missing"; + const endpoint = resolveProviderEndpointLabel(provider, params.cfg); + const endpointSuffix = endpoint.endpoint + ? ` endpoint: ${endpoint.endpoint}` + : " endpoint: default"; + const apiSuffix = endpoint.api ? ` api: ${endpoint.api}` : ""; lines.push(""); - lines.push(`[${provider}] auth: ${authLabel}`); + lines.push( + `[${provider}]${endpointSuffix}${apiSuffix} auth: ${authLabel}`, + ); for (const entry of models) { - const label = `${entry.provider}/${entry.id}`; + const label = `${provider}/${entry.id}`; const aliases = aliasIndex.byKey.get(label); const aliasSuffix = aliases && aliases.length > 0 ? ` (${aliases.join(", ")})` : ""; @@ -672,9 +772,6 @@ export async function handleDirectiveOnly(params: { } return { text: lines.join("\n") }; } - if (directives.rawModelProfile && !modelDirective) { - throw new Error("Auth profile override requires a model selection."); - } } if (directives.hasThinkDirective && !directives.thinkLevel) { @@ -835,17 +932,87 @@ export async function handleDirectiveOnly(params: { let modelSelection: ModelDirectiveSelection | undefined; let profileOverride: string | undefined; if (directives.hasModelDirective && directives.rawModelDirective) { - const resolved = resolveModelDirectiveSelection({ - raw: directives.rawModelDirective, - defaultProvider, - defaultModel, - aliasIndex, - allowedModelKeys, - }); - if (resolved.error) { - return { text: resolved.error }; + const raw = directives.rawModelDirective.trim(); + if (/^[0-9]+$/.test(raw)) { + const resolvedDefault = resolveConfiguredModelRef({ + cfg: params.cfg, + defaultProvider, + defaultModel, + }); + const pickerCatalog: ModelPickerCatalogEntry[] = (() => { + if (allowedModelCatalog.length > 0) return allowedModelCatalog; + const keys = new Set(); + const out: ModelPickerCatalogEntry[] = []; + for (const rawKey of Object.keys( + params.cfg.agents?.defaults?.models ?? {}, + )) { + const resolved = resolveModelRefFromString({ + raw: String(rawKey), + defaultProvider, + aliasIndex, + }); + if (!resolved) continue; + const key = modelKey(resolved.ref.provider, resolved.ref.model); + if (keys.has(key)) continue; + keys.add(key); + out.push({ + provider: resolved.ref.provider, + id: resolved.ref.model, + name: resolved.ref.model, + }); + } + if (out.length === 0 && resolvedDefault.model) { + const key = modelKey(resolvedDefault.provider, resolvedDefault.model); + keys.add(key); + out.push({ + provider: resolvedDefault.provider, + id: resolvedDefault.model, + name: resolvedDefault.model, + }); + } + return out; + })(); + + const items = buildModelPickerItems(pickerCatalog); + const index = Number.parseInt(raw, 10) - 1; + const item = Number.isFinite(index) ? items[index] : undefined; + if (!item) { + return { + text: `Invalid model selection "${raw}". Use /model to list.`, + }; + } + const picked = pickProviderForModel({ + item, + preferredProvider: params.provider, + }); + if (!picked) { + return { + text: `Invalid model selection "${raw}". Use /model to list.`, + }; + } + const key = `${picked.provider}/${picked.model}`; + const aliases = aliasIndex.byKey.get(key); + const alias = aliases && aliases.length > 0 ? aliases[0] : undefined; + modelSelection = { + provider: picked.provider, + model: picked.model, + isDefault: + picked.provider === defaultProvider && picked.model === defaultModel, + ...(alias ? { alias } : {}), + }; + } else { + const resolved = resolveModelDirectiveSelection({ + raw, + defaultProvider, + defaultModel, + aliasIndex, + allowedModelKeys, + }); + if (resolved.error) { + return { text: resolved.error }; + } + modelSelection = resolved.selection; } - modelSelection = resolved.selection; if (modelSelection) { if (directives.rawModelProfile) { const profileResolved = resolveProfileOverride({ diff --git a/src/commands/auth-choice-prompt.ts b/src/commands/auth-choice-prompt.ts index 57a4c7f76..6256a726b 100644 --- a/src/commands/auth-choice-prompt.ts +++ b/src/commands/auth-choice-prompt.ts @@ -48,10 +48,7 @@ export async function promptAuthChoiceGrouped(params: { const methodSelection = (await params.prompter.select({ message: `${group.label} auth method`, - options: [ - ...group.options, - { value: BACK_VALUE, label: "Back" }, - ], + options: [...group.options, { value: BACK_VALUE, label: "Back" }], })) as string; if (methodSelection === BACK_VALUE) { diff --git a/src/commands/auth-choice.test.ts b/src/commands/auth-choice.test.ts index b20a1e9e1..d85d5d2ad 100644 --- a/src/commands/auth-choice.test.ts +++ b/src/commands/auth-choice.test.ts @@ -85,6 +85,13 @@ describe("applyAuthChoice", () => { expect(text).toHaveBeenCalledWith( expect.objectContaining({ message: "Enter MiniMax API key" }), ); + expect(result.config.models?.providers?.minimax).toMatchObject({ + baseUrl: "https://api.minimax.io/anthropic", + api: "anthropic-messages", + }); + expect(result.config.agents?.defaults?.model).toMatchObject({ + primary: "minimax/MiniMax-M2.1", + }); expect(result.config.auth?.profiles?.["minimax:default"]).toMatchObject({ provider: "minimax", mode: "api_key", diff --git a/src/commands/onboard-helpers.ts b/src/commands/onboard-helpers.ts index 8e0383cb5..187f4d382 100644 --- a/src/commands/onboard-helpers.ts +++ b/src/commands/onboard-helpers.ts @@ -31,12 +31,12 @@ import type { ResetScope, } from "./onboard-types.js"; -export function guardCancel(value: T, runtime: RuntimeEnv): T { +export function guardCancel(value: T | symbol, runtime: RuntimeEnv): T { if (isCancel(value)) { cancel(stylePromptTitle("Setup cancelled.") ?? "Setup cancelled."); runtime.exit(0); } - return value; + return value as T; } export function summarizeExistingConfig(config: ClawdbotConfig): string {