diff --git a/internal/llm/providers.go b/internal/llm/providers.go index 65cc82a..e9d233e 100644 --- a/internal/llm/providers.go +++ b/internal/llm/providers.go @@ -1,6 +1,9 @@ package llm -import "strings" +import ( + "sort" + "strings" +) // Provider holds the preset configuration for a known LLM provider. type Provider struct { @@ -130,25 +133,30 @@ func init() { // The returned Provider has its own copy of the Models slice. func LookupProvider(name string) (Provider, bool) { p, ok := registryMap[strings.ToLower(strings.TrimSpace(name))] - if ok && p.Models != nil { - models := make([]string, len(p.Models)) - copy(models, p.Models) - p.Models = models + if ok { + p = copyProvider(p) } return p, ok } -// ListProviders returns all built-in providers in registration order. -// Each returned Provider has its own copy of the Models slice. +// ListProviders returns all built-in providers sorted by provider name. +// Each returned Provider has its own copy of the Models slice in registry order. func ListProviders() []Provider { out := make([]Provider, len(registry)) for i, p := range registry { - if p.Models != nil { - models := make([]string, len(p.Models)) - copy(models, p.Models) - p.Models = models - } - out[i] = p + out[i] = copyProvider(p) } + sort.Slice(out, func(i, j int) bool { + return out[i].Name < out[j].Name + }) return out } + +func copyProvider(p Provider) Provider { + if p.Models != nil { + models := make([]string, len(p.Models)) + copy(models, p.Models) + p.Models = models + } + return p +} diff --git a/internal/llm/providers_test.go b/internal/llm/providers_test.go index 6d9fbef..a813caa 100644 --- a/internal/llm/providers_test.go +++ b/internal/llm/providers_test.go @@ -1,6 +1,7 @@ package llm import ( + "sort" "testing" ) @@ -39,7 +40,10 @@ func TestListProviders_Order(t *testing.T) { if len(providers) < 3 { t.Fatalf("expected at least 3 providers, got %d", len(providers)) } - expected := []string{"anthropic", "openai", "dashscope"} + expected := []string{"anthropic", "dashscope", "deepseek", "kimi", "mimo", "minimax", "openai", "z-ai"} + if len(providers) != len(expected) { + t.Fatalf("expected %d providers, got %d", len(expected), len(providers)) + } for i, name := range expected { if providers[i].Name != name { t.Errorf("providers[%d].Name = %q, want %q", i, providers[i].Name, name) @@ -67,6 +71,33 @@ func TestLookupProvider_ReturnsCopyOfModels(t *testing.T) { } } +func TestLookupProvider_PreservesModelOrder(t *testing.T) { + p, ok := LookupProvider("anthropic") + if !ok { + t.Fatal("anthropic not found") + } + expected := []string{"claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6", "claude-sonnet-4-6"} + if len(p.Models) != len(expected) { + t.Fatalf("expected %d models, got %d", len(expected), len(p.Models)) + } + for i, model := range expected { + if p.Models[i] != model { + t.Errorf("Models[%d] = %q, want %q", i, p.Models[i], model) + } + } +} + +func TestListProviders_ReturnsSortedProviders(t *testing.T) { + providers := ListProviders() + names := make([]string, len(providers)) + for i, p := range providers { + names[i] = p.Name + } + if !sort.StringsAreSorted(names) { + t.Errorf("providers are not sorted: %v", names) + } +} + func TestLookupProvider_AnthropicDetails(t *testing.T) { p, ok := LookupProvider("anthropic") if !ok {