diff --git a/src/cli/__tests__/model-providers.test.ts b/src/cli/__tests__/model-providers.test.ts new file mode 100644 index 0000000..910d3c9 --- /dev/null +++ b/src/cli/__tests__/model-providers.test.ts @@ -0,0 +1,66 @@ +import Anthropic from "@anthropic-ai/sdk"; +import { describe, expect, test } from "bun:test"; + +import { MODEL_PROVIDERS } from "../model-providers"; + +const MINIMAX_ANTHROPIC_PROVIDERS = [ + { + id: "minimax_anthropic_cn", + baseURL: "https://api.minimaxi.com/anthropic", + }, + { + id: "minimax_anthropic_global", + baseURL: "https://api.minimax.io/anthropic", + }, +] as const; + +const MINIMAX_MODEL_IDS = ["MiniMax-M3", "MiniMax-M2.7"] as const; + +describe("MiniMax Anthropic providers", () => { + test("use SDK-compatible base URLs", () => { + for (const expected of MINIMAX_ANTHROPIC_PROVIDERS) { + expect(MODEL_PROVIDERS.find(({ id }) => id === expected.id)).toMatchObject({ + ...expected, + providerType: "anthropic", + defaultModelName: "MiniMax-M3", + }); + } + }); + + for (const provider of MINIMAX_ANTHROPIC_PROVIDERS) { + for (const model of MINIMAX_MODEL_IDS) { + test(`${provider.id} sends ${model} to one /v1/messages path`, async () => { + let requestURL = ""; + const client = new Anthropic({ + apiKey: "test-api-key", + baseURL: provider.baseURL, + fetch: async (input) => { + requestURL = typeof input === "string" ? input : input instanceof URL ? input.href : input.url; + return new Response( + JSON.stringify({ + id: "msg_test", + type: "message", + role: "assistant", + model, + content: [], + stop_reason: "end_turn", + stop_sequence: null, + usage: { input_tokens: 1, output_tokens: 1 }, + }), + { headers: { "content-type": "application/json" } }, + ); + }, + }); + + await client.messages.create({ + model, + max_tokens: 1, + messages: [{ role: "user", content: "test" }], + }); + + expect(requestURL).toBe(`${provider.baseURL}/v1/messages`); + expect(requestURL.match(/\/v1\/messages/g)).toHaveLength(1); + }); + } + } +}); diff --git a/src/cli/bootstrap/model-wizard.tsx b/src/cli/bootstrap/model-wizard.tsx index 5b9bd61..d1c5475 100644 --- a/src/cli/bootstrap/model-wizard.tsx +++ b/src/cli/bootstrap/model-wizard.tsx @@ -27,8 +27,11 @@ type ModelWizardProps = { function ModelWizard({ onComplete, onAbort }: ModelWizardProps) { const [step, setStep] = useState("provider"); const [providerIndex, setProviderIndex] = useState(0); + const selectedProvider = MODEL_PROVIDERS[providerIndex]!; const [apiKey, setApiKey] = useState(""); const [modelName, setModelName] = useState(""); + const suggestedModelName = selectedProvider.defaultModelName ?? ""; + const effectiveModelName = modelName.trim() || suggestedModelName; const [customBaseURL, setCustomBaseURL] = useState(""); const [pendingEntry, setPendingEntry] = useState(null); @@ -72,11 +75,9 @@ function ModelWizard({ onComplete, onAbort }: ModelWizardProps) { { isActive: step === "provider" }, ); - const selectedProvider = MODEL_PROVIDERS[providerIndex]!; - const finishWithBaseURL = (url: string) => { setCustomBaseURL(url); - const entry = buildModelEntry(url, apiKey, modelName, selectedProvider.providerType); + const entry = buildModelEntry(url, apiKey, effectiveModelName, selectedProvider.providerType); setPendingEntry(entry); setStep("confirm"); }; @@ -85,7 +86,7 @@ function ModelWizard({ onComplete, onAbort }: ModelWizardProps) { if (!selectedProvider.baseURL) { setStep("baseURL"); } else { - const entry = buildModelEntry(selectedProvider.baseURL, apiKey, modelName, selectedProvider.providerType); + const entry = buildModelEntry(selectedProvider.baseURL, apiKey, effectiveModelName, selectedProvider.providerType); setPendingEntry(entry); setStep("confirm"); } @@ -142,12 +143,12 @@ function ModelWizard({ onComplete, onAbort }: ModelWizardProps) { if (step === "modelName") { return ( - Enter a model name + Enter a model name{suggestedModelName ? ` (default: ${suggestedModelName})` : ""} Model: diff --git a/src/cli/model-providers.ts b/src/cli/model-providers.ts index 8b14b70..ea376af 100644 --- a/src/cli/model-providers.ts +++ b/src/cli/model-providers.ts @@ -5,6 +5,7 @@ export type ModelProviderConfig = { id: string; baseURL: string; providerType: ProviderType; + defaultModelName?: string; }; export const MODEL_PROVIDERS: ModelProviderConfig[] = [ @@ -18,8 +19,34 @@ export const MODEL_PROVIDERS: ModelProviderConfig[] = [ providerType: "openai", }, { label: "Qwen (Aliyun)", id: "qwen", baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1", providerType: "openai" }, - { label: "Minimax (Domestic)", id: "minimax_cn", baseURL: "https://api.minimaxi.com/v1", providerType: "openai" }, - { label: "Minimax (Global)", id: "minimax_global", baseURL: "https://api.minimax.io/v1", providerType: "openai" }, + { + label: "Minimax (Domestic)", + id: "minimax_cn", + baseURL: "https://api.minimaxi.com/v1", + providerType: "openai", + defaultModelName: "MiniMax-M3", + }, + { + label: "Minimax (Global)", + id: "minimax_global", + baseURL: "https://api.minimax.io/v1", + providerType: "openai", + defaultModelName: "MiniMax-M3", + }, + { + label: "Minimax Anthropic (Domestic)", + id: "minimax_anthropic_cn", + baseURL: "https://api.minimaxi.com/anthropic", + providerType: "anthropic", + defaultModelName: "MiniMax-M3", + }, + { + label: "Minimax Anthropic (Global)", + id: "minimax_anthropic_global", + baseURL: "https://api.minimax.io/anthropic", + providerType: "anthropic", + defaultModelName: "MiniMax-M3", + }, { label: "GLM (Zhipu AI)", id: "glm", baseURL: "https://open.bigmodel.cn/api/paas/v4", providerType: "openai" }, { label: "Kimi (Moonshot)", id: "kimi", baseURL: "https://api.moonshot.cn/v1", providerType: "openai" }, { label: "DeepSeek (OpenAI compatible)", id: "deepseek", baseURL: "https://api.deepseek.com/v1", providerType: "openai" },