Vectrum
Sign In
87 lines · 2.5 KB · Go
package llm

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"time"
)

// modelPageSize is what one GET /v1/models asks for. The list is short enough
// that this is almost always the only request.
const modelPageSize = 100

// maxModelPages stops a paginating endpoint that never says it is done from
// looping forever behind a picker that is waiting on it.
const maxModelPages = 10

// Model is one entry from GET /v1/models. MaxInputTokens is the context
// window; it was added to the API in 2026 and is zero on an endpoint that
// does not report it, which the caller has to treat as "not said" rather than
// as a model with no context.
type Model struct {
	ID             string    `json:"id"`
	DisplayName    string    `json:"display_name"`
	CreatedAt      time.Time `json:"created_at"`
	MaxInputTokens int       `json:"max_input_tokens"`
	MaxTokens      int       `json:"max_tokens"`
}

type modelPage struct {
	Data    []Model `json:"data"`
	HasMore bool    `json:"has_more"`
	LastID  string  `json:"last_id"`
}

// ListModels returns the models the endpoint offers, in the order it gave
// them — which from Anthropic is newest first.
//
// Not every endpoint serves this route: a gateway that only proxies
// /v1/messages answers 404, and the error says so rather than being turned
// into an empty list, because "no models" and "this endpoint cannot be asked"
// are different things to tell someone.
func (c *Client) ListModels(ctx context.Context) ([]Model, error) {
	if !c.HasCredentials() {
		return nil, fmt.Errorf("llm: no credentials: set ANTHROPIC_API_KEY, or run `ant auth login`")
	}

	var out []Model
	after := ""
	for page := 0; page < maxModelPages; page++ {
		q := url.Values{"limit": {fmt.Sprint(modelPageSize)}}
		if after != "" {
			q.Set("after_id", after)
		}

		req, err := c.newRequest(ctx, http.MethodGet, "/v1/models?"+q.Encode(), nil)
		if err != nil {
			return nil, err
		}
		resp, err := c.HTTP.Do(req)
		if err != nil {
			return nil, fmt.Errorf("llm: %w", err)
		}
		raw, err := io.ReadAll(resp.Body)
		resp.Body.Close()
		if err != nil {
			return nil, fmt.Errorf("llm: reading models: %w", err)
		}
		if resp.StatusCode != http.StatusOK {
			return nil, parseAPIError(resp.StatusCode, resp.Header.Get("retry-after"), raw)
		}

		var p modelPage
		if err := json.Unmarshal(raw, &p); err != nil {
			return nil, fmt.Errorf("llm: decoding models: %w", err)
		}
		out = append(out, p.Data...)
		if !p.HasMore || p.LastID == "" {
			break
		}
		after = p.LastID
	}
	return out, nil
}