Vectrum
Sign In
114 lines · 3.4 KB · Go
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)
	}
}