diff --git a/internal/llm/providers.go b/internal/llm/providers.go index 1f097dfe..f821843b 100644 --- a/internal/llm/providers.go +++ b/internal/llm/providers.go @@ -338,6 +338,22 @@ var registry = []Provider{ "deepseek/deepseek-chat", }, }, + { + Name: "mistral", + DisplayName: "Mistral AI", + Protocol: ProtocolOpenAIChatCompletions, + BaseURL: "https://api.mistral.ai/v1", + EnvVar: "MISTRAL_API_KEY", + // Deliberately minimal list to keep this preset low-maintenance for + // alibaba/open-code-review maintainers. Users can point to any other + // Mistral model via `ocr config set model `; the preset only + // seeds the picker UI. See https://docs.mistral.ai/getting-started/models/models_overview/ + Models: []string{ + "codestral-latest", + "mistral-large-latest", + "mistral-small-latest", + }, + }, } var registryMap map[string]Provider diff --git a/internal/llm/providers_test.go b/internal/llm/providers_test.go index 1e3113c1..4a90cea9 100644 --- a/internal/llm/providers_test.go +++ b/internal/llm/providers_test.go @@ -75,7 +75,7 @@ func TestListProviders_Order(t *testing.T) { if len(providers) < 3 { t.Fatalf("expected at least 3 providers, got %d", len(providers)) } - expected := []string{"anthropic", "baidu-qianfan", "dashscope", "dashscope-tokenplan", "deepseek", "edenai", "hy-tokenplan", "iflytek", "kimi", "litellm", "mimo", "minimax", "minimax-cn", "ollama-cloud", "openai", "tencent-tokenhub", "volcengine", "z-ai", "z-ai-coding"} + expected := []string{"anthropic", "baidu-qianfan", "dashscope", "dashscope-tokenplan", "deepseek", "edenai", "hy-tokenplan", "iflytek", "kimi", "litellm", "mimo", "minimax", "minimax-cn", "mistral", "ollama-cloud", "openai", "tencent-tokenhub", "volcengine", "z-ai", "z-ai-coding"} if len(providers) != len(expected) { t.Fatalf("expected %d providers, got %d", len(expected), len(providers)) } @@ -279,6 +279,41 @@ func TestLookupProvider_LiteLLMDetails(t *testing.T) { } } +func TestLookupProvider_MistralDetails(t *testing.T) { + p, ok := LookupProvider("mistral") + if !ok { + t.Fatal("mistral not found") + } + if p.DisplayName != "Mistral AI" { + t.Errorf("DisplayName = %q, want %q", p.DisplayName, "Mistral AI") + } + if p.Protocol != ProtocolOpenAIChatCompletions { + t.Errorf("Protocol = %q, want %q", p.Protocol, ProtocolOpenAIChatCompletions) + } + if p.BaseURL != "https://api.mistral.ai/v1" { + t.Errorf("BaseURL = %q, want %q", p.BaseURL, "https://api.mistral.ai/v1") + } + if p.EnvVar != "MISTRAL_API_KEY" { + t.Errorf("EnvVar = %q, want %q", p.EnvVar, "MISTRAL_API_KEY") + } + if p.AuthHeader != "" { + t.Errorf("AuthHeader = %q, want empty (OpenAI-compatible uses Bearer by default)", p.AuthHeader) + } + expectedModels := []string{ + "codestral-latest", + "mistral-large-latest", + "mistral-small-latest", + } + if len(p.Models) != len(expectedModels) { + t.Fatalf("Models length = %d, want %d", len(p.Models), len(expectedModels)) + } + for i, model := range expectedModels { + if p.Models[i] != model { + t.Errorf("Models[%d] = %q, want %q", i, p.Models[i], model) + } + } +} + // TestProviders_AllProtocolsCanonical verifies every registry entry uses a // canonical protocol constant — no stale "openai" / "anthropic" literals that // would bypass NormalizeProtocol downstream.