> ## Documentation Index
> Fetch the complete documentation index at: https://www.truefoundry.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Auto Routing

> Classify each request as simple, medium, or complex and route it to the cheapest model that can handle it.

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.

**Auto 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](/docs/ai-gateway/virtual-model) name; the AI Gateway decides which model actually serves each request.

```mermaid theme={"dark"}
flowchart LR
  App[Your application] --> VM["model: my-group/smart-chat"]
  VM --> CL{Classify complexity}
  CL -->|simple| S["openai-main/gpt-4o-mini"]
  CL -->|medium| M["openai-main/gpt-4o"]
  CL -->|complex| C["anthropic-main/claude-sonnet-4"]
```

Unlike the other routing strategies, the routing decision depends on **what is in the request**, not on weights, priorities, or measured latency.

| Strategy                                                                | Decides based on                      |
| ----------------------------------------------------------------------- | ------------------------------------- |
| [Weight-based](/docs/ai-gateway/virtual-model#weight-based-routing)     | A fixed traffic split you configure   |
| [Priority-based](/docs/ai-gateway/virtual-model#priority-based-routing) | Target order and target health        |
| [Latency-based](/docs/ai-gateway/virtual-model#latency-based-routing)   | Recent measured latency per target    |
| **Auto Routing**                                                        | **The content of the request itself** |

<Tip>
  In benchmarks, Auto Routing cut cost 50–70% at \~98% of quality. [See the full benchmark](#benchmark-results).
</Tip>

## Requirements and availability

Complexity-based routing is **generally available** — it is offered to every tenant in the console with no flag to request or enable. Note that this makes the routing type available to select; it does not change the default, which is still weight-based routing when you create a virtual model.

<Warning>
  Auto Routing is available on **virtual models only**. It is not a valid rule type in the tenant-level [Routing Config](/docs/ai-gateway/load-balancing-overview) YAML (`gateway-load-balancing-config`).
</Warning>

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](#validation-errors) for the exact messages and fixes.

## How a request is routed

<Steps>
  <Step title="Filter targets by metadata">
    If any target defines [`metadata_match`](/docs/ai-gateway/virtual-model#metadata-based-target-filtering), non-matching targets are dropped before anything else happens. Classification and escalation only ever consider the remaining targets.
  </Step>

  <Step title="Check the conversation pin">
    For a multi-turn request, the AI Gateway looks up the tier and target that previously served the conversation. [Conversation pinning](#conversation-pinning) is automatic and requires no virtual-model configuration.
  </Step>

  <Step title="Classify the request">
    The [heuristic classifier](#heuristic-classification-default) (default) or the [LLM classifier](#llm-classification) assigns the current turn to `simple`, `medium`, or `complex`. If the conversation was previously pinned higher, it stays at that higher tier. A conversation already pinned to `complex` skips classification.
  </Step>

  <Step title="Build the target chain">
    The targets for the selected tier come first. This is either the current classification or a higher tier retained by the conversation pin. The [escalation chain](#escalation-and-fallback) follows — higher tiers, then lower tiers.
  </Step>

  <Step title="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.
  </Step>

  <Step title="Update the conversation pin">
    Once a target returns a successful response, the AI Gateway records the tier and target for the next turn. The pin can move to a higher tier, but it never moves down during an active conversation.
  </Step>
</Steps>

## Complexity tiers

There are exactly three tiers, ordered from cheapest to most capable.

| Tier      | Typical requests                                                   | Point it at                     |
| --------- | ------------------------------------------------------------------ | ------------------------------- |
| `simple`  | Quick answers, lookups, classification, simple rewrites            | Your fastest, lowest-cost model |
| `medium`  | Multi-step summaries, drafting, standard code, light reasoning     | A balanced mid-tier model       |
| `complex` | Deep reasoning, long context, hard problem-solving, agentic chains | Your most capable model         |

These descriptions are the same ones shown next to each tier in the dashboard.

<Note>
  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](#escalation-and-fallback).
</Note>

## 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 each classified turn. A turn whose conversation is already pinned to `complex` skips classification because it cannot move any higher.

|                          | Heuristic (default)    | LLM classifier                                                                  |
| ------------------------ | ---------------------- | ------------------------------------------------------------------------------- |
| Added latency            | None — runs in-process | One chat completion before the request is forwarded                             |
| Added cost               | None                   | One classifier call per request                                                 |
| Determinism              | Fully deterministic    | Depends on the classifier model                                                 |
| Needs a classifier model | No                     | Yes                                                                             |
| Behaviour on failure     | Cannot fail            | Applies the configured [fallback strategy](#fallback-when-the-classifier-fails) |

### 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`](/docs/ai-gateway/messages-overview) requests and the top-level `instructions` field on [`/responses`](/docs/ai-gateway/responses-api) requests.

Earlier turns, assistant replies, and tool messages are **ignored by the classifier**. Consistency across a multi-turn conversation comes from [conversation pinning](#conversation-pinning) instead.

<Note>
  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.
</Note>

### 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.

```yaml theme={"dark"}
routing_config:
  type: complexity-based-routing
  classification_strategy:
    type: heuristic          # this is the default — the block can be omitted
```

<Accordion title="What the heuristic looks for">
  These signals push a request toward a **higher** tier, listed from most to least influential:

  | Signal                      | What it matches                                                                                                                                                          |
  | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
  | Code                        | Programming keywords in the user or system text — `function`, `class`, `api`, `docker`, `sql`, language names, and similar.                                              |
  | Explicit reasoning requests | Phrases in the user text that ask the model to reason — `step by step`, `think through`, `pros and cons`, `compare and contrast`, `explain your reasoning`, and similar. |
  | Technical vocabulary        | Systems and ML terms — `architecture`, `distributed`, `neural network`, `concurrency`, `throughput`, and similar.                                                        |
  | Prompt length               | Long user prompts.                                                                                                                                                       |
  | Multi-step structure        | Cues such as `first … then`, `step 2`, numbered or lettered lists.                                                                                                       |
  | Many questions at once      | More than three question marks in one request.                                                                                                                           |

  These push a request toward a **lower** tier:

  | Signal                         | What it matches                                                            |
  | ------------------------------ | -------------------------------------------------------------------------- |
  | Lookup and small-talk phrasing | `what is`, `who is`, `how many`, `define`, `hello`, `thanks`, and similar. |
  | Short prompts                  | Very short user prompts.                                                   |

  **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](#llm-classification) instead.
</Accordion>

<Accordion title="Worked examples">
  | Prompt                                                                                                       | Signals matched                       | Tier      |
  | ------------------------------------------------------------------------------------------------------------ | ------------------------------------- | --------- |
  | `Hi, thanks!`                                                                                                | short prompt, small talk              | `simple`  |
  | `What is the capital of France?`                                                                             | short prompt, lookup phrasing         | `simple`  |
  | `Write a Python function that reverses a linked list.`                                                       | code                                  | `medium`  |
  | `Design a distributed rate limiter. Walk me through the trade-offs step by step and explain your reasoning.` | two reasoning phrases → override      | `complex` |
  | `Our gRPC service has high tail latency under concurrency. Analyze this and break down the likely causes.`   | technical vocabulary, reasoning, code | `complex` |
</Accordion>

### 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.

```yaml theme={"dark"}
routing_config:
  type: complexity-based-routing
  classification_strategy:
    type: llm-classifier
    model: openai-main/gpt-4o-mini    # must be a catalog chat model
    timeout_ms: 2000                  # default: 2000
    fallback_strategy:                # required
      type: heuristic
```

<Note>
  `fallback_strategy` is **required** whenever `classification_strategy.type` is `llm-classifier`. The console pre-fills it with the heuristic fallback, so this only affects configs written by hand or through the API — omitting the key there is rejected with `must have required property 'fallback_strategy'`.

  `{"type": "heuristic"}` reproduces the behaviour that used to apply implicitly, so add it to existing configs to keep them working unchanged. `classification_strategy` itself remains optional and still defaults to the heuristic strategy.
</Note>

<Warning>
  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.
</Warning>

<Accordion title="How the classifier call works">
  * 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](#fallback-when-the-classifier-fails) applies.
</Accordion>

#### 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.

<CodeGroup>
  ```yaml Heuristic fallback theme={"dark"}
  classification_strategy:
    type: llm-classifier
    model: openai-main/gpt-4o-mini
    fallback_strategy:
      type: heuristic       # classify with the built-in heuristic instead
  ```

  ```yaml Static fallback theme={"dark"}
  classification_strategy:
    type: llm-classifier
    model: openai-main/gpt-4o-mini
    fallback_strategy:
      type: static
      default_tier: medium  # always use this tier; default: medium
  ```
</CodeGroup>

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

Auto 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.

```mermaid theme={"dark"}
flowchart LR
  R["Request classified: medium"] --> A["medium target"]
  A -->|fails or absent| B["complex target"]
  B -->|fails| C["simple target"]
  C -->|fails| E[Request fails]
```

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](/docs/ai-gateway/virtual-model#retries-and-fallbacks).

<Note>
  **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.
</Note>

<Warning>
  **Unhealthy-target cooldown does not apply here.** Under the other strategies a failing target is demoted to the end of the list; under Auto Routing, target order is always tier order. Per-target [rate limits and budgets](/docs/ai-gateway/ratelimiting) are still enforced, and a rate-limited or over-budget target is skipped.
</Warning>

## Conversation pinning

Auto Routing automatically keeps a multi-turn conversation on a consistent tier and target. You do not configure `sticky_routing`, add a conversation header, or choose a TTL on the virtual model.

The pin behaves like a ratchet:

1. The first turn is classified normally. After a target responds successfully, its tier and target are recorded.
2. Each later user turn is classified again.
3. If the new turn is harder, the conversation moves up to the higher tier and the pin is updated after a successful response.
4. If the new turn is easier, the existing higher tier wins. A short follow-up such as “thanks” therefore cannot move an active conversation back to a cheaper model.
5. Once a conversation reaches `complex`, later turns skip classification because no higher tier exists.

This balances cost and consistency: conversations can gain capability when the task becomes harder, but do not lose capability midway through the interaction.

Nothing is required from your application: keep sending the conversation history as the API you call already expects. A pin lasts through **10 minutes of inactivity** and is refreshed on every turn, so an active conversation stays on its tier.

<Note>
  Single-turn requests — legacy `/completions`, and `/responses` with a string `input` — have no conversation to follow, so they are classified individually every time.
</Note>

A target that failed is never recorded as the conversation's next target.

## Benchmark results

We benchmarked Auto Routing against sending every request to a single top-tier model (Claude Opus 5), over the same prompts. Cost is computed from the tokens each request actually used at list prices, and quality retained is the router's pass rate divided by the baseline's. Every answer is graded deterministically; generated code is run against unit tests, and math and multiple-choice are matched to answer keys.

| Workload                                              | Cost savings | Quality retained            |
| ----------------------------------------------------- | ------------ | --------------------------- |
| Graded academic benchmarks (11 datasets, 550 prompts) | 69%          | 98%                         |
| Realistic production traffic (chat, developer, agent) | 80%          | cost-only (no ground truth) |

Against a prior-generation top model the graded savings are about 50%; the exact figure depends on which model you route away from. Auto Routing was also faster on average, since most requests skip the top-tier reasoning model.

A few honest notes: savings depend on your traffic mix, and the free heuristic classifier gives up some accuracy on short-but-hard prompts (the LLM classifier recovers most of it at lower savings). Figures measured August 2026 on a Claude Haiku / Sonnet / Opus tier ladder.

To measure your own savings, see [Observability](#observability) — the gateway logs the resolved tier for every request.

## Configure in the dashboard

Auto Routing is configured on a virtual model, alongside the other routing strategies. See [Create a virtual model](/docs/ai-gateway/virtual-model-advanced) for the full walkthrough.

<Steps>
  <Step title="Create the virtual model and choose Complexity">
    Go to **AI Gateway** → **Models** → **Virtual 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**.

    <Frame caption="Creating a virtual model with Complexity selected as the routing type">
      <img src="https://mintcdn.com/truefoundry/37iXLSUR8PsbB1lE/images/ai-gateway/complexity-routing-create-model.png?fit=max&auto=format&n=37iXLSUR8PsbB1lE&q=85&s=70d26f141049b868573626dcecc34d72" alt="Add new Model form showing Name, Model Types with Chat selected, and Complexity chosen as the routing type" width="1024" height="583" data-path="images/ai-gateway/complexity-routing-create-model.png" />
    </Frame>
  </Step>

  <Step title="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.

    <Frame caption="LLM Classification with a small, fast model as the classifier">
      <img src="https://mintcdn.com/truefoundry/37iXLSUR8PsbB1lE/images/ai-gateway/complexity-routing-llm-classifier.png?fit=max&auto=format&n=37iXLSUR8PsbB1lE&q=85&s=108134ff1d100985c795fbb6dfed127e" alt="LLM Classification selected, with Classifier Model set to claude-haiku, Timeout 2000 ms, and Fallback Strategy set to Heuristic Fallback" width="1024" height="583" data-path="images/ai-gateway/complexity-routing-llm-classifier.png" />
    </Frame>
  </Step>

  <Step title="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](#escalation-and-fallback).

    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.

    <Frame caption="A target for each tier, from lowest to highest cost">
      <img src="https://mintcdn.com/truefoundry/37iXLSUR8PsbB1lE/images/ai-gateway/complexity-routing-tier-targets.png?fit=max&auto=format&n=37iXLSUR8PsbB1lE&q=85&s=012438431138984267e4fefc875198c1" alt="The Simple, Medium, and Complex tier sections with claude-haiku, claude-sonnet, and claude-opus selected respectively" width="1024" height="583" data-path="images/ai-gateway/complexity-routing-tier-targets.png" />
    </Frame>
  </Step>
</Steps>

<Warning>
  **Current dashboard limitation.** The virtual model form supports **one target per tier**. To configure multiple targets in the same tier, apply the virtual model manifest with [`tfy apply`](/docs/using-tfy-apply) or the API instead.
</Warning>

## Configuration reference

<CodeGroup>
  ```yaml YAML theme={"dark"}
  routing_config:
    type: complexity-based-routing

    # How requests are classified. Optional; defaults to { type: heuristic }.
    classification_strategy:
      type: heuristic | llm-classifier

      # llm-classifier only:
      model: string                     # catalog chat model FQN (not a virtual model)
      timeout_ms: integer               # default: 2000
      fallback_strategy:                # required when type is llm-classifier
        type: heuristic | static
        default_tier: simple | medium | complex   # static only; required when type is static

    load_balance_targets:
      - target: string                  # catalog model FQN, e.g. openai-main/gpt-4o
        tier: simple | medium | complex # required; each target serves exactly one tier

        retry_config:
          attempts: integer             # retries on the SAME target; default: 0
          delay: integer                # ms between retries; default: 100
          on_status_codes: string[]     # default: ["429","500","502","503"]

        fallback_status_codes: string[] # codes that move to the NEXT target in the chain
                                        # default: ["401","403","404","408","429","500","502","503"]
        fallback_candidate: boolean     # eligible to receive escalated traffic; default: true

        metadata_match:                 # all pairs must match resolved request metadata
          key: value

        headers_override:
          set:
            header-name: header-value
          remove:
            - header-name

        override_params:
          temperature: number
          max_tokens: integer
          prompt_version_fqn: string
  ```
</CodeGroup>

## Examples

<AccordionGroup>
  <Accordion title="Cost-optimised three-tier chat model">
    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.

    ```yaml theme={"dark"}
    routing_config:
      type: complexity-based-routing
      load_balance_targets:
        - target: openai-main/gpt-4o-mini
          tier: simple
        - target: openai-main/gpt-4o
          tier: medium
        - target: anthropic-main/claude-sonnet-4
          tier: complex
    ```
  </Accordion>

  <Accordion title="Two tiers — cheap and capable only">
    Skip `medium` entirely. Requests classified as `medium` escalate to the `complex` target.

    ```yaml theme={"dark"}
    routing_config:
      type: complexity-based-routing
      load_balance_targets:
        - target: openai-main/gpt-4o-mini
          tier: simple
        - target: anthropic-main/claude-sonnet-4
          tier: complex
    ```
  </Accordion>

  <Accordion title="LLM classifier with a conservative static fallback">
    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.

    ```yaml theme={"dark"}
    routing_config:
      type: complexity-based-routing
      classification_strategy:
        type: llm-classifier
        model: openai-main/gpt-4o-mini
        timeout_ms: 1500
        fallback_strategy:
          type: static
          default_tier: complex
      load_balance_targets:
        - target: openai-main/gpt-4o-mini
          tier: simple
        - target: openai-main/gpt-4o
          tier: medium
        - target: anthropic-main/claude-sonnet-4
          tier: complex
    ```
  </Accordion>

  <Accordion title="Per-tier resilience with retries and a second complex target">
    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.

    ```yaml theme={"dark"}
    routing_config:
      type: complexity-based-routing
      load_balance_targets:
        - target: openai-main/gpt-4o-mini
          tier: simple
          retry_config:
            attempts: 2
            delay: 100
        - target: openai-main/gpt-4o
          tier: medium
        - target: anthropic-main/claude-sonnet-4
          tier: complex
          fallback_status_codes: ["429", "500", "502", "503"]
        - target: bedrock-main/claude-sonnet-4
          tier: complex
    ```
  </Accordion>

  <Accordion title="Cheaper prompts per tier">
    Pair each tier with a prompt version tuned for that model, using [per-target overrides](/docs/ai-gateway/virtual-model#model-specific-prompt-overrides).

    ```yaml theme={"dark"}
    routing_config:
      type: complexity-based-routing
      load_balance_targets:
        - target: openai-main/gpt-4o-mini
          tier: simple
          override_params:
            max_tokens: 512
            prompt_version_fqn: chat_prompt:internal/my-app/concise-answer:1
        - target: anthropic-main/claude-sonnet-4
          tier: complex
          override_params:
            prompt_version_fqn: chat_prompt:internal/my-app/deep-reasoning:1
    ```
  </Accordion>
</AccordionGroup>

## Observability

### Which tier served the request?

Every response carries an `x-tfy-applied-rules` header. For Auto 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`:

```json theme={"dark"}
{
  "routing_config": {
    "rule_id": "my-group/smart-chat",
    "requested_model": "my-group/smart-chat",
    "resolved_model": "anthropic-main/claude-sonnet-4",
    "complexity": { "tier": "complex", "cause": "heuristic" },
    "routing_model_order": [
      { "model": "anthropic-main/claude-sonnet-4", "reason": "complexity-tier" },
      { "model": "openai-main/gpt-4o", "reason": "complexity-escalation" },
      { "model": "openai-main/gpt-4o-mini", "reason": "complexity-escalation" }
    ]
  }
}
```

`cause` tells you which mechanism produced the tier:

| `cause`          | Meaning                                                                                                                                                          |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `heuristic`      | The built-in heuristic classifier decided the tier — either as the configured strategy, or as the fallback after an LLM classifier failure.                      |
| `llm_classifier` | The LLM classifier returned the tier.                                                                                                                            |
| `session_pin`    | An existing conversation pin kept the request on a higher tier than the current turn required. Classification is skipped only when the pin is already `complex`. |
| `default`        | The LLM classifier failed and a `static` fallback strategy applied its `default_tier`.                                                                           |

`reason` on each entry in `routing_model_order` tells you why that target is in that position:

| `reason`                 | Meaning                                                             |
| ------------------------ | ------------------------------------------------------------------- |
| `complexity-tier`        | Target belongs to the tier the request classified into.             |
| `complexity-escalation`  | Target belongs to a different tier and is in the chain as failover. |
| `complexity-session-pin` | Target was placed first because a sticky session pin selected it.   |

The actual model that served the request is also returned in the `x-tfy-resolved-model` [response header](/docs/ai-gateway/headers#response-headers).

### Metrics

Every routing decision increments a counter, so you can see your tier mix and how often escalation happens.

| Metric                                          | Type    | Labels                                                                |
| ----------------------------------------------- | ------- | --------------------------------------------------------------------- |
| `ai_gateway_complexity_routing_decisions_total` | Counter | `tenant_name`, `model_name`, `decided_tier`, `resolved_tier`, `cause` |

* **`decided_tier`** — the tier assigned to the current turn before target fallback. For a `complex` pin that skips classification, this remains `complex`.
* **`resolved_tier`** — the tier of the first target actually attempted.

When the two differ, check `cause`. A `session_pin` means the conversation deliberately stayed on a higher tier than the current turn's classification. Another cause means the classified tier had no eligible target and routing escalated to another tier. A persistent non-pin gap is a signal that a tier is misconfigured or missing. See [Prometheus and Grafana integration](/docs/ai-gateway/prometheus-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`:

| Error                                                                                                            | Fix                                                                                                                                                          |
| ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `cannot use Auto Routing with model_types [embedding]. Auto Routing supports only [chat, completion, responses]` | Remove the unsupported model types, or serve them from a separate virtual model. Mixed lists are rejected too — declaring both `chat` and `embedding` fails. |
| Duplicate target across tiers                                                                                    | Assign the model to a single tier.                                                                                                                           |
| `classifier model "…" could not be resolved in this tenant`                                                      | Use a model FQN that exists in your model catalog.                                                                                                           |
| `classifier model "…" cannot be a virtual model`                                                                 | Point the classifier at a catalog model.                                                                                                                     |
| `classifier model "…" must support [chat]`                                                                       | Pick a model whose model types include `chat`.                                                                                                               |

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

<AccordionGroup>
  <Accordion title="How much can I actually save?">
    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](/docs/ai-gateway/analytics-model-metrics). Your saving is that share multiplied by the price difference between tiers.
  </Accordion>

  <Accordion title="Can I use Auto Routing in the tenant-level Routing Config YAML?">
    No. It is a virtual-model-only routing type. Create a virtual model and point your clients at it.
  </Accordion>

  <Accordion title="Can I use it for embeddings, images, or audio?">
    No — a virtual model declaring those model types with Auto Routing is rejected at save time. Use [weight-, priority-, or latency-based routing](/docs/ai-gateway/virtual-model#routing-strategies) for them.
  </Accordion>

  <Accordion title="Can I customise how the heuristic classifies requests?">
    No, its signals are fixed. If the heuristic misclassifies your traffic, switch to the [LLM classifier](#llm-classification) and nominate your own judging model.
  </Accordion>

  <Accordion title="Does the LLM classifier call show up in my usage and cost?">
    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. A conversation already pinned to `complex` does not make another classifier call.
  </Accordion>

  <Accordion title="What happens if the classifier is down?">
    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`.
  </Accordion>

  <Accordion title="What if the tier a request classified into has no target?">
    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.
  </Accordion>

  <Accordion title="Can a conversation change models mid-conversation?">
    Yes, but only when it needs more capability or the previous target fails. [Conversation pinning](#conversation-pinning) prevents later turns from moving to a lower tier. A harder follow-up can move the conversation to a higher tier, and a successful fallback can update the target used by later turns.
  </Accordion>

  <Accordion title="Do unhealthy-target cooldowns apply?">
    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.
  </Accordion>

  <Accordion title="Does it work with streaming?">
    Yes. Classification finishes before the upstream request is made, so streaming responses are unaffected.
  </Accordion>

  <Accordion title="Does it work with the Anthropic Messages API and the Responses API?">
    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, and multi-turn conversations are pinned the same way.
  </Accordion>
</AccordionGroup>

## Next steps

* [Virtual Models overview](/docs/ai-gateway/virtual-model) — the other routing strategies, per-target options, and health detection.
* [Create a virtual model](/docs/ai-gateway/virtual-model-advanced) — step-by-step setup.
* [Analytics](/docs/ai-gateway/analytics-model-metrics) — compare cost and latency per target once traffic is flowing.
