package llm import ( "context" "net/http" "net/http/httptest" "strings" "testing" ) func TestListModelsReadsTheCatalogue(t *testing.T) { var gotPath, gotKey string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotPath, gotKey = r.URL.Path, r.Header.Get("x-api-key") w.Header().Set("content-type", "application/json") w.Write([]byte(`{"data":[ {"id":"claude-opus-5","display_name":"Claude Opus 5","type":"model"}, {"id":"claude-haiku-4-5-20251001","display_name":"Claude Haiku 4.5","type":"model"} ],"has_more":false}`)) })) defer srv.Close() c := New(WithBaseURL(srv.URL), WithAPIKey("k")) got, err := c.ListModels(context.Background()) if err != nil { t.Fatalf("ListModels: %v", err) } if gotPath != "/v1/models" { t.Errorf("asked %s, want /v1/models", gotPath) } if gotKey != "k" { t.Errorf("the catalogue request was not authenticated: x-api-key = %q", gotKey) } if len(got) != 2 { t.Fatalf("got %d models, want 2: %+v", len(got), got) } if got[0].ID != "claude-opus-5" || got[0].DisplayName != "Claude Opus 5" { t.Errorf("first model = %+v", got[0]) } } // Pointed at a gateway that does not serve the catalogue, the call has to fail // in a way the caller can fall back from — not hang, and not panic on a body // that is HTML. func TestListModelsFailsCleanlyOnAGatewayWithoutIt(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) w.Write([]byte("no such route")) })) defer srv.Close() c := New(WithBaseURL(srv.URL), WithAPIKey("k")) if _, err := c.ListModels(context.Background()); err == nil { t.Fatal("a 404 catalogue came back as success") } } func TestListModelsRefusesWithoutCredentials(t *testing.T) { c := New(WithBaseURL("http://127.0.0.1:1")) c.apiKey, c.authToken = "", "" _, err := c.ListModels(context.Background()) if err == nil { t.Fatal("listed models with no credentials") } if !strings.Contains(err.Error(), "credentials") { t.Errorf("the error does not name the cause: %v", err) } } // Deprecated models are still returned by the API. Offering one as a default // in a fresh setup is a trap, so they are filtered out. func TestListModelsDropsDeprecatedOnes(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`{"data":[ {"id":"claude-opus-5","display_name":"Claude Opus 5"}, {"id":"claude-2.1","display_name":"Claude 2.1","deprecated":true} ]}`)) })) defer srv.Close() got, err := New(WithBaseURL(srv.URL), WithAPIKey("k")).ListModels(context.Background()) if err != nil { t.Fatal(err) } for _, m := range got { if m.ID == "claude-2.1" { t.Error("a deprecated model was offered") } } } // The fallback is what the wizard shows when the catalogue cannot be reached, // so it has to stand on its own: every entry priced, and the default present. func TestKnownModelsIsAUsableFallback(t *testing.T) { known := KnownModels() if len(known) == 0 { t.Fatal("no fallback models") } var hasDefault bool for _, m := range known { if m.ID == DefaultModel { hasDefault = true } if _, ok := prices[m.ID]; !ok { t.Errorf("%s is offered but has no pricing", m.ID) } if m.DisplayName == "" { t.Errorf("%s has no display name", m.ID) } } if !hasDefault { t.Errorf("the fallback list does not include the default model %s", DefaultModel) } }