Skip to main content

Routing policies

Most AI gateways offer 2โ€“3 routing modes. Ferro Labs ships 8 โ€” covering everything from simple single-provider setups to content-aware routing and live A/B testing. Set strategy.mode in config.yaml to choose one.

Singleโ€‹

Always routes to the first target. Best for single-provider setups or when you want explicit control.

Use this when: you have one provider and want the simplest possible config.

strategy:
mode: single

targets:
- virtual_key: openai
Performance

Single is the lightest strategy โ€” zero overhead beyond the proxy hop. Ideal for latency-sensitive single-provider deployments.

Fallbackโ€‹

Tries targets in order. On failure (error or retryable status code), the next target is attempted with exponential backoff. Use this for high-availability setups.

Use this when: uptime matters more than anything โ€” your chatbot must always respond, even if the primary provider is down.

strategy:
mode: fallback

targets:
- virtual_key: openai
retry:
attempts: 3
on_status_codes: [429, 502, 503]
initial_backoff_ms: 100
- virtual_key: anthropic
retry:
attempts: 2
- virtual_key: gemini

If all targets fail, the last error is returned to the client.

Pro tip

Combine fallback with circuit breakers to skip providers that are consistently failing, rather than waiting for retries to timeout on every request.

Weighted load balancingโ€‹

Distributes requests across targets by weight. Weights are relative โ€” a weight of 70 and 30 sends 70% to the first target and 30% to the second.

Use this when: you want to spread load across providers for cost or capacity reasons.

strategy:
mode: loadbalance

targets:
- virtual_key: openai
weight: 70
- virtual_key: anthropic
weight: 30

Only targets that support the requested model are candidates for selection.

Performance

Weight evaluation adds negligible overhead โ€” a single random number generation per request. Equivalent to single-strategy latency for practical purposes.

Conditionalโ€‹

Evaluates rules in order. The first matching rule determines the target. Each rule has a key (the request field to inspect, e.g. model), a value (the exact value to match), and a target_key (the target to route to).

Use this when: different models should route to specific providers โ€” e.g., all GPT models to OpenAI, all Claude models to Anthropic.

strategy:
mode: conditional
conditions:
- key: model
value: gpt-4o
target_key: openai
- key: model
value: gpt-4o-mini
target_key: openai
- key: model
value: claude-3-5-sonnet-20241022
target_key: anthropic
- key: model
value: gemini-1.5-flash
target_key: gemini

targets:
- virtual_key: openai
- virtual_key: anthropic
- virtual_key: gemini

If no rule matches, the request falls through to the first target.

Pro tip

Conditional routing pairs well with model aliases. Alias smart โ†’ claude-3-5-sonnet-20241022 (aliases resolve before routing), then add a conditional rule for key: model, value: claude-3-5-sonnet-20241022.

Least-latencyโ€‹

Routes to the target with the lowest P50 latency as measured by a rolling latency tracker. On a cold start (no latency data yet) it picks a target randomly.

Use this when: you have multiple fast providers and want to minimise time-to-first-token automatically.

strategy:
mode: least-latency

targets:
- virtual_key: openai
- virtual_key: groq
- virtual_key: anthropic
Performance

Adds a mutex read on the rolling latency map per request โ€” typically under 1ยตs. The latency tracker updates asynchronously after each response, so it does not add to request latency.

Cost-optimizedโ€‹

Uses the built-in model catalog (2,500+ entries with pricing data) to estimate the input token cost for each target, then routes to the cheapest compatible provider. Falls back to the first compatible target if cost data is unavailable.

Use this when: you want to minimize spend without manually choosing models โ€” let the catalog handle it.

strategy:
mode: cost-optimized
unpriced_strategy: fallback # fallback (default) | skip | allow

targets:
- virtual_key: openai
- virtual_key: together
- virtual_key: deepseek
- virtual_key: gemini

Cost ranking uses input/prompt tokens only, estimated from the request via a ~4 chars/token heuristic, priced against the catalog's input_per_m_tokens field (USD per 1M prompt tokens). The target with the lowest estimated cost for the matched model wins. Pricing comes from the model-catalog (a remote release with an embedded fallback, refreshed every 24h, overridable via the FERRO_MODEL_CATALOG_URL env var). unpriced_strategy controls how targets with no pricing data are handled: fallback prefers priced candidates then the first compatible unpriced target, skip rejects unpriced candidates, and allow treats missing prices as zero cost.

Pro tip

Combine cost-optimized with fallback by adding retry to each target โ€” if the cheapest provider fails, the gateway retries with the next cheapest.

Content-basedโ€‹

Routes based on the content of the user's messages. Rules are evaluated in order; the first match wins. If no rule matches, the request falls through to the first target.

Use this when: different types of queries should go to different specialized models โ€” code to a coding model, translation to a translation service, general chat to a cost-efficient default.

Three condition types are supported:

TypeBehavior
prompt_containsCase-insensitive substring match on any user message
prompt_not_containsMatches when NO user message contains the value
prompt_regexGo regular-expression match on any user message

Regex patterns are compiled at gateway startup. An invalid regex causes a startup error โ€” there is no silent misrouting.

strategy:
mode: content-based
content_conditions:
- type: prompt_contains
value: "translate"
target_key: deepl-provider
- type: prompt_regex
value: "(?i)(code|function|class|def |import )"
target_key: openai
- type: prompt_contains
value: "summarize"
target_key: anthropic

targets:
- virtual_key: deepl-provider
- virtual_key: openai
- virtual_key: anthropic
Performance

Substring matching (prompt_contains) is near-zero cost. Regex matching adds overhead proportional to pattern complexity, but patterns are pre-compiled at startup so the hot path is a single regexp.MatchString call.

A/B testโ€‹

Splits traffic across variants by configured weights. Each variant carries a label field, which is currently emitted only to the gateway's DEBUG logs.

Use this when: you want to compare quality, latency, or cost between two providers on live traffic without client-side changes.

strategy:
mode: ab-test
ab_variants:
- target_key: openai
weight: 70
label: control
- target_key: anthropic
weight: 30
label: challenger

targets:
- virtual_key: openai
- virtual_key: anthropic

Weights are relative โ€” 70 and 30 send 70% of traffic to openai and 30% to anthropic. If a weight is 0, the variant is treated as weight 1 (equal distribution with remaining variants). Negative weights are rejected at gateway startup.

The label field is currently visible only in the gateway's DEBUG logs โ€” it is not written to events or a queryable log column.

Pro tip

Combine A/B test with the request-logger plugin persisting to Postgres to compare aggregate latency and token usage across live traffic. Per-variant attribution currently requires reading the variant label from the gateway's DEBUG logs.

Combining strategies with circuit breakersโ€‹

All strategies respect per-target circuit breakers. A target whose circuit breaker is open is excluded from selection.

targets:
- virtual_key: openai
circuit_breaker:
failure_threshold: 5
success_threshold: 2
timeout: "30s"

The circuit breaker opens after failure_threshold consecutive failures, stays open for timeout, then enters half-open state where it allows one probe request. After success_threshold successes it closes again.

  • Configuration reference โ€” full YAML reference for all strategy modes
  • Use cases โ€” recipe-style configurations for common scenarios
  • Benchmarks โ€” performance data for different routing strategies
  • Plugins โ€” combine routing with safety and observability plugins