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
}