Skip to main content
Most production traffic is not uniformly hard. A greeting, a one-line factual lookup, and a request to design a distributed rate limiter all arrive on the same endpoint — and if every one of them is served by your most capable model, you pay top-tier prices for the easy majority. Complexity-based routing classifies each request into a complexity tier — simple, medium, or complex — and routes it to the target you configured for that tier. Your application keeps calling one virtual model name; the AI Gateway decides which model actually serves each request. Unlike the other routing strategies, the routing decision depends on what is in the request, not on weights, priorities, or measured latency.

Requirements and availability

Complexity-based routing is available on virtual models only. It is not a valid rule type in the tenant-level Routing Config YAML (gateway-load-balancing-config).
The virtual model must also meet these conditions, or it is rejected when you save it:
  • Model types must be limited to chat, completion, and responses. Embedding, image, audio, rerank, and moderation are not supported.
  • Each target serves exactly one tier, and the same model cannot appear under two tiers.
  • The classifier model (LLM strategy only) must be a catalog chat model, not a virtual model.
See Validation errors for the exact messages and fixes.

How a request is routed

1

Filter targets by metadata

If any target defines metadata_match, non-matching targets are dropped before anything else happens. Classification and escalation only ever consider the remaining targets.
2

Check the session pin

If sticky routing is configured and this session already has a pin, the pinned tier and target are reused and classification is skipped entirely.
3

Classify the request

The heuristic classifier (default) or the LLM classifier assigns the request to simple, medium, or complex.
4

Build the target chain

The targets for the classified tier come first, followed by the escalation chain — higher tiers, then lower tiers.
5

Send the request, falling forward on failure

The first target is attempted with its own retry configuration. If it fails with a fallback status code, the AI Gateway moves to the next target in the chain.
6

Pin the session

Once a target returns a successful response, the tier and target are written to the session pin (when sticky routing is enabled) so subsequent turns skip classification.

Complexity tiers

There are exactly three tiers, ordered from cheapest to most capable. These descriptions are the same ones shown next to each tier in the dashboard.
You do not have to configure all three tiers. Configuring only simple and complex, for example, is valid — requests classified as medium escalate to complex. See Escalation and fallback.

Classification strategies

Two strategies are available. The heuristic classifier is the default and costs nothing; the LLM classifier is more accurate on ambiguous prompts but adds a model call to every request.

What text gets classified

Both strategies look at the same two pieces of text, extracted from the request body:
  • The last user message — the current turn.
  • The last system or developer message — for context. This includes the top-level system field on Anthropic /messages requests and the top-level instructions field on /responses requests.
Earlier turns, assistant replies, and tool messages are ignored. Consistency across a multi-turn conversation comes from sticky routing, not from classifying the whole history.
Classification scans at most 8,000 characters of user text and 2,000 characters of system text. A prompt longer than that is still recognised as a long prompt.

Heuristic classification (default)

The heuristic classifier reads the prompt text, looks for a fixed set of signals, and picks a tier. It needs no configuration — this is what you get if you do not set classification_strategy at all.
These signals push a request toward a higher tier, listed from most to least influential:These push a request toward a lower tier:Reasoning override. Two or more distinct reasoning phrases in the user text always route to complex, whatever the other signals say.Empty prompts. A request with no classifiable text is treated as simple.The signals and keyword lists are fixed and cannot be customised. If they don’t match how difficulty shows up in your traffic, use the LLM classifier instead.

LLM classification

The AI Gateway classifies the request by calling a fast chat model you nominate. Use this when the heuristic’s keyword-based view is too coarse for your traffic — for example domain-specific prompts where difficulty is not signalled by vocabulary.
Each classifier call is a billable gateway request. It appears in your request logs, and its tokens and cost are attributed to the same tenant and subject as the request that triggered it. Use a small, fast model and keep timeout_ms tight.
  • The classifier is sent the request’s system context and user text, along with a short instruction block describing the three tiers.
  • Its reply is constrained to a JSON schema ({"tier": "simple" | "medium" | "complex"}) and capped at 256 output tokens. Models that ignore the schema and answer in prose still work — the AI Gateway extracts the first tier word it finds.
  • The call is abandoned after timeout_ms (default 2000), and the fallback strategy applies.

Fallback when the classifier fails

If the classifier times out, errors, returns a non-2xx status, or returns nothing usable, the configured fallback strategy decides the tier. The classifier failure never fails the request.
Use heuristic to keep a content-aware decision during an outage, or static to send everything to one known tier. default_tier: complex preserves answer quality at higher spend; default_tier: simple does the opposite.

Escalation and fallback

Complexity-based routing does not just pick one target. It builds a fully ordered chain, so a request still gets served when the tier it classified into has no usable target. For a request classified into tier T, the order is:
  1. Targets in tier T, in the order you declared them.
  2. Targets in higher tiers, working upward one tier at a time.
  3. Targets in lower tiers, working downward one tier at a time.
So a request classified as simple is attempted on simple, then medium, then complex. A request classified as complex is attempted on complex, then medium, then simple. A request fails only after every target has been tried. Each target in the chain uses its own retry_config, fallback_status_codes, and fallback_candidate settings — these behave exactly as they do for the other routing strategies.
Escalation also covers tiers you did not configure. With only simple and complex targets, a request classified as medium is escalated straight to complex and the response header reports the escalation. This is how you run a two-tier setup.
Unhealthy-target cooldown does not apply here. Under the other strategies a failing target is demoted to the end of the list; under complexity-based routing, target order is always tier order. Per-target rate limits and budgets are still enforced, and a rate-limited or over-budget target is skipped.

Sticky routing

By default every turn of a conversation is classified independently, so a chat can start on your cheap model, move to your expensive one on a hard follow-up, and move back again. That is the cheapest behaviour, but it can be visibly inconsistent within one conversation. Sticky routing pins a session to the tier and target chosen on its first classified turn. Subsequent turns in the same session skip classification entirely and go straight to the pinned target.
Session identifiers tell the AI Gateway which request fields identify a session. All configured identifiers are combined into one session key, and you can mix headers and metadata:
  • key — the header or metadata field name to read.
  • sourceheaders or metadata.
TTL (ttl_seconds) must be between 300 (5 minutes) and 3600 (1 hour) — a shorter range than weight-based sticky routing allows. The window slides: every turn that reads the pin refreshes its expiry, so an active conversation stays pinned and an abandoned one expires.
  • Redis is required. Pins are stored in Redis and shared across all gateway pods. If Redis is not configured or is unreachable, sticky routing no-ops and every turn is classified independently.
  • Pins are written only after success. A target that failed is never pinned.
  • Sessions are isolated. Two different session identifier values are two different pins.
  • Stale pins are discarded. If the pinned target is no longer an eligible target of the virtual model, the request is reclassified.
  • Missing identifiers. If none of the configured identifiers resolve to a value on a request, there is no session to pin and the request is classified normally.

Configure in the dashboard

Complexity-based routing is configured on a virtual model, alongside the other routing strategies. See Create a virtual model for the full walkthrough.
1

Create the virtual model and choose Complexity

Go to AI GatewayModelsVirtual Model and add or edit a model. Give it a Name, and under Model Types select only from Chat, Completion, and Responses.Under Balance them across following targets based on, choose Complexity.
Add new Model form showing Name, Model Types with Chat selected, and Complexity chosen as the routing type

Creating a virtual model with Complexity selected as the routing type

2

Pick a classification strategy

Heuristic Classification is selected by default and needs nothing else.Choose LLM Classification instead to classify with a model, then set Classifier Model, Timeout (ms), and Fallback Strategy. Switching Fallback Strategy to Static Fallback adds a Default Tier field.
LLM Classification selected, with Classifier Model set to claude-haiku, Timeout 2000 ms, and Fallback Strategy set to Heuristic Fallback

LLM Classification with a small, fast model as the classifier

3

Set a target for each tier

The form has one Target slot under each of Simple, Medium, and Complex. Pick a model for each tier you want to serve and leave the rest empty — an empty tier is skipped and its traffic escalates.Each target carries its own Fallback status codes, plus optional Retries, Override Params, Override Headers, and Metadata Filters — all behaving as they do for the other routing strategies.
The Simple, Medium, and Complex tier sections with claude-haiku, claude-sonnet, and claude-opus selected respectively

A target for each tier, from lowest to highest cost

Current dashboard limitations. The virtual model form supports one target per tier, and sticky routing is not yet exposed in the UI. To configure multiple targets in the same tier or to enable sticky routing, apply the virtual model manifest with tfy apply or the API instead. Both are fully supported by the AI Gateway.

Configuration reference

Examples

The common starting point: cheap model for the easy majority, mid-tier for everyday work, top model for the hard tail. No classifier cost, no extra latency.
Skip medium entirely. Requests classified as medium escalate to the complex target.
Use a small model to classify, and route everything to the complex tier if that classifier is ever unavailable — quality never regresses, spend rises only during the outage.
Pin each conversation to the tier its first turn classified into, for 15 minutes of inactivity.
Multiple targets in the same tier are attempted in declaration order before escalating. This shape requires the YAML manifest or API — the dashboard form allows one target per tier.
Pair each tier with a prompt version tuned for that model, using per-target overrides.

Observability

Which tier served the request?

Every response carries an x-tfy-applied-rules header. For complexity-based routing it includes a complexity block with the tier that was served and how the tier was decided, plus the full ordered chain in routing_model_order:
cause tells you which mechanism produced the tier: reason on each entry in routing_model_order tells you why that target is in that position: The actual model that served the request is also returned in the x-tfy-resolved-model response header.

Metrics

Every routing decision increments a counter, so you can see your tier mix and how often escalation happens.
  • decided_tier — the tier the request classified into.
  • resolved_tier — the tier of the first target actually attempted.
When the two differ, the request was escalated because the classified tier had no eligible target. A persistent gap is a signal that a tier is misconfigured or missing. See Prometheus and Grafana integration for scraping setup.

Traces and logs

  • Each LLM classifier call produces a ComplexityBasedRouting: LLMClassifier span, with the classifier’s own chat-completion span nested beneath it — so classifier latency is visible separately from the served model’s latency.
  • The request span carries tfy.complexity.tier and tfy.complexity.cause.
  • The gateway logs each decision with the tier, the cause, the signals that matched, and the resolved model — useful for checking why a given request landed on the tier it did.

Validation errors

These configurations are rejected when you save the virtual model, with a 400: The three classifier checks also run at request time, so a classifier model that is later deleted or changed surfaces the same error on the request rather than misrouting silently.

FAQ

That depends on your traffic mix, so measure it. Break ai_gateway_complexity_routing_decisions_total down by resolved_tier to see what share of traffic landed on each tier, and compare per-model cost in analytics. Your saving is that share multiplied by the price difference between tiers.
No. It is a virtual-model-only routing type. Create a virtual model and point your clients at it.
No — a virtual model declaring those model types with complexity-based routing is rejected at save time. Use weight-, priority-, or latency-based routing for them.
No, its signals are fixed. If the heuristic misclassifies your traffic, switch to the LLM classifier and nominate your own judging model.
Yes. It appears in request logs, and its tokens and cost are attributed to the same tenant and subject as the request that triggered it. Use a small, cheap model and keep timeout_ms tight.
The request is never failed by a classifier problem. On timeout, error, non-2xx, or an unusable reply, the configured fallback_strategy decides the tier — the built-in heuristic by default, or a fixed default_tier if you chose static.
It escalates to the next higher tier, and if there is none, down to lower tiers. The response header reports the tier actually used, and the metric records decided_tier and resolved_tier separately so you can spot it.
Each turn is classified independently unless you enable sticky routing — a follow-up that reads as harder than the opening message routes to a higher tier. To keep one conversation on one target, add sticky_routing with a session identifier your client always sends.
No. Targets are ordered by tier and escalation, never reordered by health. Per-target retries, fallback status codes, rate limits, and budgets all still apply.
Yes. Classification finishes before the upstream request is made, so streaming responses are unaffected.
Yes. A chat virtual model serves both /chat/completions and /messages, and a responses virtual model serves /responses. In both cases the top-level system prompt (system and instructions respectively) is included as classification context.

Next steps