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("<html>no such route</html>"))
}))
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)
}
}