OBLETHdocs

Auto routing

Let the gateway choose a model for each request that asks for model auto, and tune how it chooses.

8 min readUpdated for v2.4.0Edit on GitHub

Ask for"model": "auto"
Works onChat completions, Responses, Messages and Verdicts
Tune itSettings › Routing, and the Playground's Router tab

Send a request

curl http://localhost:8088/v1/chat/completions \
  -H "Authorization: Bearer $OBLETH_API_KEY" -H "Content-Type: application/json" \
  -d '{"model": "auto", "messages": [{"role": "user", "content": "Write a Python function that parses ISO-8601 durations."}]}'

The gateway picks a registered chat model before admission. The request is then queued, billed and logged under the model it picked. Other request types (embeddings, audio, images) must name a model.

With difficulty tiering on, send x-obleth-effort: low, medium or high to set the request's difficulty yourself.

If no model can serve the request, it fails with 503 no model is available to satisfy the auto request.

How a model is chosen

The router works on the live model list, in three stages.

  1. Filter. Models that cannot serve the request are dropped: disabled, staged, deprecated or retired, unhealthy, not eligible for auto, outside the tenant's model list, too small a context window, or missing a capability it needs (tools, a JSON schema, images). A matching boon can stand in for vision or schema support.
  2. Tier (optional). With difficulty tiering on, models too weak for the request's difficulty on its topics are dropped. This stage narrows the field but never empties it.
  3. Score. Each survivor is scored on spare capacity, this request's estimated cost, and tag fit, then multiplied by its route bias.

At temperature 0 the top score wins, ties broken by name. Above 0, the pick is sampled from the scores, spreading traffic across near-equal models.

Tags and difficulty

Each model can carry tags from a fixed list, set in the Routing section of its page:

coding · general · reasoning · math · vision · long-context · fast · creative · writing

creative is fiction and verse. writing is any prose to draft or edit: emails, letters, essays, posts, and rewrites for tone, length or grammar.

A tag can carry a strength from 1 to 3, such as coding:3, for difficulty tiering.

A request's tags and difficulty come from the classifier, when one is set, or else from keyword heuristics. The heuristics find only coding, math, vision, long-context and writing.

The classifier is a small, fast chat model that you register like any other. It answers through verdicts: one yes/no question per tag that the candidate models carry, plus a three-level difficulty score, each read from the first token's probabilities. The questions share a prompt prefix, so with prefix caching they cost about as much as one. A tag applies above p(yes) 0.5, at most three per request. coding and writing are never applied together: "write a SQL query" says yes to both, so only the stronger of the two is kept.

A classifier failure routes cheaper, never more expensive: unanswered tags are not applied, unanswered difficulty is 1, and with no answers the heuristics are used. A slow or missing classifier never blocks a request. Pick a model that answers directly, not a reasoning model. To build one, see Training a classifier.

Conversations

By default each request is routed afresh. The classifier reads the newest user message, so a conversation that opens with small talk and moves on to code is routed as code from that turn on.

A follow-up with no topic of its own, such as "make it shorter" or "thanks", classifies as general alone. The user message before it then decides, up to three user messages back, so the follow-up keeps the conversation's topic. Those messages were classified when they were sent, so the answer usually comes from the cache.

Tool results are not user messages: every step of an agent's tool loop is classified on the request that started it. Spare capacity is scored on every request, so two turns on the same topic can still go to different models when one is busy. Without a classifier, the heuristics read the whole conversation.

Keep a conversation on its model

Routing every turn afresh lets a conversation move between models as its turns change difficulty or wording, or as load shifts, and each move changes style and quality mid-task. With Stay with a model while it answers well on under Conversations in Settings › Routing (sticky_enabled on /api/v1/settings/auto-router, off by default), a conversation stays on the model that answered its last turn and moves up a level only when that model is not doing a good job:

  • the client sends the same request again after it was answered (a retry or regenerate);
  • the classifier reads the user's newest message as saying the last answer was wrong (one more single-token question, asked only on follow-up turns);
  • the last answer was broken: no content and no tool call, or tool-call arguments that are not JSON;
  • the model made the same tool calls three turns in a row, or two tool turns in a row came back with errors.

A move up raises the conversation's floor one level on its topics and leaves the model that failed out; at the top level it goes to another model there. A harder turn raises the floor to its difficulty, and the floor never drops. On a new topic the model stays when it holds a level there at least the turn's difficulty; otherwise the turn is routed within the new topic, no lower than the conversation's level. When the model cannot take a request (it is down, the context is too long, a capability is missing, or the key may no longer use it), the turn goes to another model at the conversation's level or above. The first turn of a conversation is routed as before.

The gateway keeps no conversation id and stores none of the conversation's text. After a turn is served it stores the model and level for two hours under a hash of the user message and the answer (the tool-call ids for a tool turn; reasoning and whitespace are ignored), and the next request finds it by hashing its last assistant message. A conversation whose client edits the last answer is routed as a new one.

Every auto response names its model in x-obleth-auto-model. With stickiness on, x-obleth-auto-reason says why:

ReasonMeaning
freshNo earlier turn found; routed as a new conversation
keptStayed on the conversation's model
escalated:<signal>Moved up a level; the signal is regenerate, complaint, bad_output, tool_loop or tool_errors
harderA harder turn raised the level
topicA new topic the model holds no level in
forcedThe model could not take the request

obleth_auto_routes_total{model,reason} counts the same reasons (per_turn when stickiness is off), and the request's auto_route span records the decision.

Tune it in Settings

Settings › Routing shows readiness findings in its header (listed at the end of this page), then sets the classifier (on or off, its model and timeout), the default model for Anthropic clients, the default model for verdicts, whether conversations keep their model, and the weights, from a profile or by hand:

ProfileCapacityCostTagTiering
Balanced (default)0.60.40.5off
Best answer0.40.050.9on
Cost saver0.50.90.4off

Changing any value makes the profile Custom. Changes reach every gateway within 15 seconds.

The same settings are GET and PUT /api/v1/settings/auto-router. A PUT changes only the fields it sends:

curl -X PUT http://localhost:9180/api/v1/settings/auto-router \
  -H "Authorization: Bearer $OBLETH_ADMIN_TOKEN" -H "Content-Type: application/json" \
  -d '{"classifier_enabled": true, "classifier_model": "qwen3-0.6b", "classifier_timeout_ms": 250}'

GET /api/v1/router/readiness returns the readiness findings.

Try a prompt in the Playground

The Playground's Router tab shows where a prompt would go without sending it, and how each candidate scored.

The Playground Router tab: a coding prompt routed to qwen3-coder-30b, the classify, filter, score and pick stages, each candidate's score broken into capacity, cost and tag, and the router weight sliders

The weight sliders change only the simulation, with live and draft scores side by side, until you choose Apply to gateway…. See Playground.

Simulating a decision

POST /api/v1/router/simulate runs the same code as the data plane against the live fleet and returns the decision without dispatching anything:

curl -X POST http://localhost:9180/api/v1/router/simulate \
  -H "Authorization: Bearer $OBLETH_ADMIN_TOKEN" -H "Content-Type: application/json" \
  -d '{"prompt": "Prove the AM-GM inequality", "classify": true}'

Every body field is optional and overrides a live input: prompt or messages, max_tokens, tenant_id, effort, the capability flags, any weight, busyness (in-flight counts per model) and uniform (a fixed random draw). classify: true uses the live classifier instead of the heuristics, and reads a messages conversation turn by turn as a real request is read. The response lists the pick, the tags and difficulty with their source, every scored candidate, and each dropped model with its filter.

A traced request also records an auto_route span explaining the decision it served.

Per-model controls

On a model's page, under Routing:

  • Eligible for auto off keeps the model out of auto. Clients can still call it by name.
  • Route bias multiplies the model's score (0.1 to 3.0, 1.0 neutral), to prefer or avoid a model without retagging or repricing it. A small bias such as 1.05 breaks a price tie.
Scoring in full
spare     = 1 − in_flight / cap        # cap: the model's max in flight, or default_soft_cap (8)
cost      = 1 for the cheapest estimate of this request, 0 for the dearest
base      = capacity_weight × spare + cost_weight × cost
score     = tag_weight × tag_match + (1 − tag_weight) × base    # base alone when the request has no tags
final     = score × route_bias

The cost estimate uses the model's prices, the prompt estimate, and its observed average answer length (500 tokens until it has answered). tag_match is the share of the request's tags the model carries. Weights are clamped to 0 to 1 and temperature to 0 to 2.

Difficulty tiering

Request difficulty comes from the x-obleth-effort header, else the classifier, else heuristics: a pasted stack trace is 3; phrases such as "root cause", or a prompt over 8,000 characters or 32,000 estimated tokens, are 2; anything else is 1.

A model's level on a tag follows tier_source: hybrid (default) derives it from price rank among the models with that tag unless the tag declares one; derived always uses price rank; declared uses declared levels, and a bare tag is 1.

Classifier limits and guardrails

Each classification reads one user message and the system message, up to 2,000 bytes. The user message comes first and the system message is cut to fit, so a long system prompt cannot hide the request. At most 16 tags are asked about. Each fan-out is bounded by classifier_timeout_ms (default 250 ms), and a timeout or an empty answer ends the look-back, so a slow classifier costs at most one timeout per request. Results are cached for 5 minutes per classifier model, so switching the classifier takes effect on the next request.

For a tenant with input guardrails, the classifier is not called, because it would read the prompt before the scan.

OBLETH_AUTO_CLASSIFIER_ENABLED, OBLETH_AUTO_CLASSIFIER_MODEL and OBLETH_AUTO_CLASSIFIER_TIMEOUT_MS seed these settings on first boot; saved settings win after that.

Readiness findings
CodeSeverityMeaning
classifier_offinfoNo classifier; tags come from the heuristics
classifier_missingwarnThe classifier model is not registered or is disabled
classifier_is_autowarnThe classifier is set to auto, which cannot classify itself
uncovered_tagwarn or infoNo eligible model carries a tag. warn for tags the heuristics can emit
long_output_modelwarnAn eligible model averages 4,000 or more output tokens per answer
price_tieinfoModels with the same prices, tags and bias tie on every request
tiering_offinfoDifficulty is classified but not used
tiering_inertwarnTiering uses declared levels, but no model declares one