API reference

API reference

Ten endpoints, no authentication, no rate limit headers, no pagination cursors. Everything on this page is shared ground: where the API lives, what an error looks like, and the small endpoints that describe the dataset itself.

Per-area reference

The two endpoints with real surface get a page each.

AreaCoversReference
Models/v1/models, lookup by id, by-id, /v1/compareDocs →
EstimatePOST /v1/estimate: usage fields, options, breakdown, warningsDocs →
DatasetWhere the data comes from, how it is loaded and normalizedDocs →

Base URL

The hosted instance is not live yet. https://api-llm-specs.axium-lab.com is the address the free instance will answer on. Until it is deployed, run your own — the examples below use localhost:8080, which is what bun start gives you.

Everything that queries the catalog lives under /v1. /health sits outside the version prefix, on purpose: it has to answer while the dataset is still loading, and a liveness probe should not have to know about API versions.

https://api-llm-specs.axium-lab.com/v1/models      # hosted (not live yet)
http://localhost:8080/v1/models            # bun start
http://localhost:8080/health               # note: no /v1

While the dataset is loading, every /v1 route answers 503 with not-ready and /health answers 503 with {"status":"starting","ready":false}. In practice the window is short — the file is read from disk, not downloaded — but a container orchestrator should wait for /health to return 200 before sending traffic.

Authentication

There is none. The API is free and read-only: it serves a public dataset and computes arithmetic over it. No keys, no accounts, no quotas.

No CORS headers are sent. The service does not mount CORS middleware, so a browser page on another origin cannot call it directly. Call it from a server, or add the middleware to your own deployment.

Errors

Every failure is RFC 9457 application/problem+json. The type slug is the stable part — branch on that, not on the title.

{
  "type": "about:blank#model-not-found",
  "title": "Model not found.",
  "status": 404,
  "detail": "There is no \"claude-sonnet-9-9\" entry in the dataset.",
  "suggestions": ["claude-sonnet-5", "claude-sonnet-4-5", "claude-sonnet-4-6"]
}

type, title and status are always present. Anything else is extra context for that specific failure, listed below.

Statustype slugWhenExtra members
400invalid-queryA query parameter is malformed: non-numeric limit, limit < 1, a bad sort direction, a non-boolean supports_*, a missing id or ids.field
400invalid-requestThe /v1/estimate body fails validation, or reasoning_tokens exceeds output_tokens.errors[] or detail
404model-not-foundNo entry with that id. On GET it comes with suggestions; on POST /v1/estimate it does not.detail, suggestions[] (GET only)
404not-foundThe route itself does not exist.path
409ambiguous-modelThe id matches several entries that differ only by case.candidates[]
422model-not-pricedThe model exists but carries no pricing key at all — 97 entries are like this.model
422limits-exceededUsage exceeds a limit the model declares, and options.limit_policy is "error".max_input_tokens / max_output_tokens, provided
503not-readyThe dataset is still loading. Only on /v1 routes.
500internal-errorAnything unhandled.detail
Malformed JSON answers 500, not 400. A body that is not valid JSON, or one larger than the 256 kB limit, makes the JSON parser throw and lands in the generic handler: {"type":"about:blank#internal-error","title":"Internal error.","status":500,"detail":"JSON Parse error: Unexpected EOF"}. That is a known rough edge, not a designed response.

Response conventions

Pagination

Only /v1/models paginates, with limit and offset. total is the size of the filtered result, before the page is cut — so it is the number you divide by limit to know how many pages there are.

{ "total": 2419, "limit": 3, "offset": 0, "data": [ … ] }

limit defaults to 50 and is clamped to 500 rather than rejected: ?limit=100000 answers 200 with 500 rows. A limit below 1 is an error.

The id field

The upstream catalog is a JSON object keyed by model id, so the id is not a field of the entry. This API injects it as id on the way out, which is why it appears last in a model object.

Naming

Upstream calls the provider field litellm_provider; it is renamed to provider at the single normalization boundary, so that is the name in the filter, in the responses and in the facets. Why the rename happens on the way in →

Caching

No Cache-Control, ETag or Last-Modified headers are set on API responses. The dataset's own ETag and sha256 are exposed as data, in /v1/meta, so you can tell two answers apart by the snapshot that produced them.

Health

GET /health

Liveness and readiness in one. Outside the /v1 prefix, and the only endpoint that answers before the dataset is loaded.

curl localhost:8080/health
{
  "status": "ok",
  "ready": true,
  "models": 3214,
  "dataset": {
    "source": "local",
    "etag": "W/\"74e5328441bbeb0259c44db6ce9a333d7871356ea505ca8190ca23d112fdf801\"",
    "sha256": "a5682d2fb71a4e40cd2030dc71afbce28b1272cfe4f546c28f9aaa29d5472501",
    "loaded_at": "2026-08-27T06:23:42.602Z"
  },
  "startup_error": null
}
FieldMeaning
source"local" — the copy on disk was current (a 304, or upstream was unreachable). "upstream" — a newer version was downloaded at boot.
etagThe upstream ETag the local copy was last validated against. null when it is unknown or was discarded.
sha256Digest of the normalized JSON being served. Two instances serving the same digest are serving the same prices.
startup_errornull normally. A string when upstream could not be reached at boot and the local copy carried the service — worth alerting on, but not an outage.

While loading, it answers 503 with a deliberately minimal body — note it is plain JSON, not problem+json:

{ "status": "starting", "ready": false }

Dataset metadata

GET /v1/meta

Counts and provenance. /health answers "can I serve?"; this answers "what am I serving?".

curl localhost:8080/v1/meta
{
  "models": 3214,
  "providers": 127,
  "modes": 16,
  "attributes": 153,
  "dataset": {
    "source": "local",
    "upstream_url": "https://raw.githubusercontent.com/BerriAI/litellm/refs/heads/litellm_internal_staging/model_prices_and_context_window.json",
    "etag": "W/\"74e5328441bbeb0259c44db6ce9a333d7871356ea505ca8190ca23d112fdf801\"",
    "sha256": "a5682d2fb71a4e40cd2030dc71afbce28b1272cfe4f546c28f9aaa29d5472501",
    "loaded_at": "2026-08-27T06:23:42.602Z",
    "startup_error": null
  }
}

upstream_url is worth reading before you trust the numbers: the default points at an internal LiteLLM branch. Why, and what the risk is →

Providers

GET /v1/providers

The 127 providers with their model counts, ordered by count descending, ties broken alphabetically.

curl localhost:8080/v1/providers
{
  "total": 127,
  "data": [
    { "name": "fireworks_ai", "models": 313 },
    { "name": "bedrock",       "models": 268 },
    { "name": "openai",        "models": 226 },
    { "name": "azure",         "models": 221 },
    … 123 more
  ]
}

These names are exactly what ?provider= accepts on the listing endpoint.

Modes

GET /v1/modes

A mode is what the model does: chat, embedding, transcription, image generation. Eight entries declare none, which is why the count is reported separately instead of being folded in as a bucket.

{
  "total": 16,
  "models_without_mode": 8,
  "data": [
    { "name": "chat",             "models": 2419 },
    { "name": "image_generation", "models": 269 },
    { "name": "embedding",        "models": 133 },
    { "name": "responses",        "models": 89 },
    … 12 more
  ]
}

The full set: audio_speech, audio_transcription, chat, completion, embedding, guardrail, image_edit, image_generation, moderation, ocr, realtime, rerank, responses, search, vector_store, video_generation.

Attributes

GET /v1/attributes

Every key that appears on at least one model, with how many models carry it and whether it is a pricing key. This is the endpoint to hit before writing a fields= projection or a sort=, because it tells you how many models actually declare what you are about to sort by.

{
  "total": 153,
  "pricing_keys": 86,
  "data": [
    { "name": "provider",             "models": 3214, "pricing": false },
    { "name": "mode",                 "models": 3206, "pricing": false },
    { "name": "max_input_tokens",     "models": 2713, "pricing": false },
    { "name": "input_cost_per_token", "models": 2663, "pricing": true },
    … 149 more
  ]
}

pricing: true means the key name contains cost, or is tiered_pricing. It is the same test the estimator uses to decide whether a model is billable at all — a model with zero pricing keys answers 422 model-not-priced.

37 of these keys are supports_* capability flags, and every one of them works as a filter without being enumerated anywhere in the code.

Next

PageWhat it covers
ModelsFilters, sorting, projection, id lookup and its three failure modes, comparison.
EstimateEvery usage field, every option, the breakdown, the warnings, and what it cannot price.
DatasetWhere the prices come from, how a boot revalidates them, and the decisions behind the numbers.