Listing
Filter, sort, project and paginate over the catalog. Filters combine with AND; a filter that takes a list combines its own values with OR.
curl 'localhost:8080/v1/models?provider=anthropic&mode=chat&fields=id,provider,input_cost_per_token,max_input_tokens&limit=3'
{
"total": 26,
"limit": 3,
"offset": 0,
"data": [
{ "id": "claude-haiku-4-5-20251001", "provider": "anthropic", "input_cost_per_token": 0.000001, "max_input_tokens": 200000 },
{ "id": "claude-haiku-4-5", "provider": "anthropic", "input_cost_per_token": 0.000001, "max_input_tokens": 200000 },
{ "id": "claude-3-7-sonnet-20250219", "provider": "anthropic", "input_cost_per_token": 0.000003, "max_input_tokens": 200000 }
]
}
Without fields, each element is the complete dataset entry — every attribute the catalog holds
for that model, which for a frontier model is around 30 keys.
Filters
| Parameter | Type | Behaviour |
|---|---|---|
provider | comma-separated | Case-insensitive exact match on the provider name. Several values are OR-ed. Names come from /v1/providers. |
mode | comma-separated | Same, over the 16 modes. The 8 models with no mode never match. |
q | string | Case-insensitive substring of the id only. Not a fuzzy search — for that, see the 404 suggestions. |
min_input_tokens | number | Keeps models whose max_input_tokens is at least this. Models that do not declare it are excluded. |
max_input_cost | number | Keeps models whose input_cost_per_token is at most this. Per token, so 0.000005 is $5/MTok. |
supports_* | true|false|1|0 | Any capability flag. See below. |
sort | field[:asc|desc] | See below. Default direction asc. |
fields | comma-separated | See below. |
limit | integer | Default 50, clamped to 500. Below 1 is a 400. |
offset | integer | Default 0. Negative values are treated as 0. |
# Two providers, two modes, ids only
curl 'localhost:8080/v1/models?provider=openai,anthropic&mode=chat,responses&fields=id,provider,mode&limit=10'
# Substring of the id
curl 'localhost:8080/v1/models?q=claude-sonnet&fields=id,provider&limit=3'
# -> {"total":62,"limit":3,"offset":0,"data":[{"id":"anthropic.claude-sonnet-5","provider":"bedrock_converse"}, …]}
# Numeric thresholds: a 1M context window under $5/MTok
curl 'localhost:8080/v1/models?min_input_tokens=1000000&max_input_cost=0.000005&fields=id,max_input_tokens,input_cost_per_token'
200 with
"total": 0 and an empty data. Only a malformed parameter is a
400 invalid-query, and the response names the offending
field.
Capability filters
Any key of the dataset starting with supports_ works as a filter, without being enumerated
anywhere. There are 37 of them today, and a new one added upstream becomes filterable at
the next boot — no code change.
curl 'localhost:8080/v1/models?supports_vision=true&supports_prompt_caching=true&fields=id,provider&limit=2'
# -> {"total":548, …}
The current set, from /v1/attributes:
supports_adaptive_thinking supports_native_streaming
supports_assistant_prefill supports_native_structured_output
supports_audio_input supports_none_reasoning_effort
supports_audio_output supports_nova_canvas_image_edit
supports_computer_use supports_output_config
supports_embedding_image_input supports_parallel_function_calling
supports_function_calling supports_parallel_tool_use_config
supports_image_input supports_pdf_input
supports_image_size supports_prompt_cache_breakpoint
supports_legacy_thinking supports_prompt_caching
supports_low_reasoning_effort supports_reasoning
supports_max_reasoning_effort supports_response_schema
supports_mid_conversation_system supports_sampling_params
supports_minimal_reasoning_effort supports_speed
supports_multimodal supports_system_messages
supports_tool_choice supports_url_context
supports_tool_search supports_video_input
supports_vision supports_web_search
supports_xhigh_reasoning_effort
false means "not true", not "declared false". The filter compares
key === true against what you asked for, so ?supports_vision=false returns both
the models that declare it false and the many that do not mention it at all. Use
/v1/attributes to see how many models declare a given key
before reading anything into its absence.
A value that is not a boolean is rejected rather than ignored:
curl 'localhost:8080/v1/models?supports_vision=maybe'
# 400
{ "type": "about:blank#invalid-query", "title": "\"supports_vision\" must be true or false", "status": 400, "field": "supports_vision" }
Sorting
sort=field or sort=field:desc. Any attribute works, including one only a handful
of models declare. Numbers compare numerically, everything else compares as a string.
curl 'localhost:8080/v1/models?mode=chat&sort=input_cost_per_token:asc&fields=id,provider,input_cost_per_token&limit=3'
Note what is at the top of that ascending list: entries that genuinely declare
input_cost_per_token: 0, such as anthropic.claude-mythos-preview and the
Cloudflare LoRA models. Free and unknown stay on opposite ends.
An unknown direction is a 400; an unknown field is not — it simply sorts everything to the tail.
curl 'localhost:8080/v1/models?sort=input_cost_per_token:ascending'
# 400
{ "type": "about:blank#invalid-query", "title": "The \"sort\" direction must be asc or desc", "status": 400, "field": "sort" }
Projection
fields trims each row to the keys you name. A key a model does not have is simply absent —
it is never emitted as null, because in this dataset "absent" carries meaning.
curl 'localhost:8080/v1/models?provider=bedrock&fields=id&limit=2&offset=50'
{
"total": 268, "limit": 2, "offset": 50,
"data": [
{ "id": "bedrock/ap-northeast-1/1-month-commitment/anthropic.claude-instant-v1" },
{ "id": "bedrock/ap-northeast-1/1-month-commitment/anthropic.claude-v1" }
]
}
Projection happens after pagination, so it saves bytes on the wire, not work on the server.
Pagination
total is the size of the filtered set before the page is cut, so
Math.ceil(total / limit) is the number of pages. There are no cursors and no
Link headers.
curl 'localhost:8080/v1/models?limit=100000&fields=id'
# -> 200 with {"total":3214,"limit":500,…} and 500 rows: the ceiling clamps, it does not reject
The ceiling is MAX_LIMIT (500 by default) and the default page is DEFAULT_LIMIT
(50); both are environment variables, so a self-hosted instance can raise them.
Lookup by id
Returns the complete dataset entry, with id appended.
curl localhost:8080/v1/models/claude-sonnet-4-5
{
"deprecation_date": "2026-09-29",
"cache_creation_input_token_cost": 0.00000375,
"cache_creation_input_token_cost_above_1hr": 0.000006,
"cache_creation_input_token_cost_above_1hr_above_200k_tokens": 0.000012,
"cache_read_input_token_cost": 3e-7,
"input_cost_per_token": 0.000003,
"input_cost_per_token_above_200k_tokens": 0.000006,
"output_cost_per_token": 0.000015,
"output_cost_per_token_above_200k_tokens": 0.0000225,
"provider": "anthropic",
"max_input_tokens": 200000,
"max_output_tokens": 64000,
"mode": "chat",
"search_context_cost_per_query": { "search_context_size_high": 0.01, "search_context_size_low": 0.01, "search_context_size_medium": 0.01 },
"supports_prompt_caching": true,
"supports_reasoning": true,
… more capability flags …
"prompt_cache_min_tokens": 1024,
"id": "claude-sonnet-4-5"
}
Those four _above_200k_tokens keys are the long-context tier, and the
_above_1hr ones are Anthropic's 1 hour cache TTL. The
estimator is what turns them into a price.
Ids the URL has opinions about
82% of the ids contain a /, 244 contain :, four contain a literal *.
The route is a wildcard that rejoins every segment, so all of them are passed literally — nothing to
escape, nothing to encode.
# Slashes and a colon
curl 'localhost:8080/v1/models/bedrock/us.anthropic.claude-3-5-haiku-20241022-v1:0'
# Four segments
curl 'localhost:8080/v1/models/vertex_ai/gemini-3-pro-preview'
# A literal asterisk in the id
curl 'localhost:8080/v1/models/bedrock/*/1-month-commitment/cohere.command-text-v14'
# -> 200 {"input_cost_per_second":0.011, "provider":"bedrock", …}
Case matters
The catalog holds together_ai/baai/bge-base-en-v1.5 and
together_ai/BAAI/bge-base-en-v1.5 as two separate entries with identical content. Lookup is
exact and case-sensitive first, so each of those resolves to itself with a 200. Only when
no exact match exists does a case-insensitive pass run — and if that matches more than one entry,
it is a 409 rather than a coin flip.
The by-id escape hatch
One id in the catalog ends in a slash — fireworks_ai/accounts/fireworks/models/ — and a path
cannot carry a trailing slash, because the router normalizes it away before anything sees it. That entry is
reachable only here.
curl 'localhost:8080/v1/models/by-id?id=fireworks_ai/accounts/fireworks/models/'
{
"max_tokens": 40960, "max_input_tokens": 40960, "max_output_tokens": 40960,
"input_cost_per_token": 1e-7, "output_cost_per_token": 0,
"provider": "fireworks_ai", "mode": "embedding",
"id": "fireworks_ai/accounts/fireworks/models/"
}
It behaves identically to the path form for every other id, so it is also the safer choice when you are
passing an id you did not construct yourself. Omitting id is a 400:
{ "type": "about:blank#invalid-query", "title": "Missing \"id\" parameter.", "status": 400, "field": "id" }
When a lookup fails
404 — with suggestions
A miss comes back with up to 10 candidates, tried from most to least exact: full substring of the id, then
the segment after the last /, and finally trigram similarity. That last pass is what rescues a
typo with no common substring at all.
curl localhost:8080/v1/models/claude-sonnet-9-9
# 404
{
"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",
"claude-sonnet-4-20250514", "claude-sonnet-4-5-20250929",
"claude-4-sonnet-20250514", "claude-3-7-sonnet-20250219"
]
}
409 — ambiguous except for case
curl 'localhost:8080/v1/models/together_ai/BaAi/bge-base-en-v1.5'
# 409
{
"type": "about:blank#ambiguous-model",
"title": "The identifier is ambiguous except for case.",
"status": 409,
"detail": "\"together_ai/BaAi/bge-base-en-v1.5\" matches several dataset entries.",
"candidates": ["together_ai/baai/bge-base-en-v1.5", "together_ai/BAAI/bge-base-en-v1.5"]
}
Pick one of the candidates and ask again. On /v1/estimate the same conflict is resolved with a provider field instead.
404 — the route, not the model
curl localhost:8080/v1/nope
{ "type": "about:blank#not-found", "title": "Route not found.", "status": 404, "path": "/v1/nope" }
The type slug is what tells the two 404s apart: model-not-found versus not-found.
Compare
Several models side by side, plus the union of every attribute any of them declares — that union is the row set of a comparison table, and it is sorted so the table is stable.
curl 'localhost:8080/v1/compare?ids=gpt-4o,no-such-model'
{
"requested": ["gpt-4o", "no-such-model"],
"found": 1,
"missing": ["no-such-model"],
"attributes": [
"cache_read_input_token_cost", "cache_read_input_token_cost_priority", "id",
"input_cost_per_token", "input_cost_per_token_batches", … 17 more
],
"data": [ { … the full gpt-4o entry … } ]
}
| Field | Meaning |
|---|---|
requested | The ids you asked for, in order, trimmed and with empties dropped. |
found | A count, not a list. The models themselves are in data. |
missing | Ids that did not resolve. An id that is ambiguous by case lands here too — this endpoint never answers 409. |
attributes | Sorted union of the keys present in any found model. |
data | The full entries, in the order they were requested. |
400 here is omitting
ids entirely.
Next
You have a model and its rates. The estimator turns them into a cost for a specific call — including the parts of the pricing model a raw attribute dump does not tell you, like which of those four Anthropic cache keys applies to your request.