Conditional routing strategy
Conditional routes each request by matching a field of the request against rules you write, not by weight, latency, or cost. It optimizes for deterministic pinning โ "this model always goes to this backend", "this tenant's traffic only ever goes here" โ the shape a compliance or contractual requirement needs, where a pool mode's willingness to pick a different provider is the wrong behaviour, not a convenience. Set strategy.mode: conditional to use it.
Write rules like "model X goes to provider Y" or "user vip goes to provider Z". The first matching rule wins; anything unmatched goes to the first target. A rule names either one target or an ordered chain of targets (target_keys). The gateway tries the chain in order, moving on only when a provider is at fault, and never reaches for a target the rule did not name. A rule with one target is exact: if that target is down, the client gets the corresponding error.
What happens to a requestโ
With the rules in the YAML below (gpt-4o โ OpenAI, claude- prefix โ Anthropic then Bedrock, user vip โ OpenAI, OpenAI first in targets):
| Client asks for | Rule | Chain | If the first target fails |
|---|---|---|---|
gpt-4o | exact model match | OpenAI | the client gets OpenAI's error |
claude-sonnet-4-6 | prefix claude- | Anthropic, then Bedrock | Bedrock answers after a provider-side failure (5xx, timeout, open circuit); a 400 from Anthropic goes back to the client |
any model, "user": "vip" after the model rules | user match | OpenAI | the client gets OpenAI's error |
mistral-large | no match | OpenAI (targets[0]) | 404 model_not_found if OpenAI does not serve it โ even if another target does |
Behaviourโ
conditional is a named mode: the candidates for a request are exactly what the matched rule names โ its target_keys chain, or target_key as a one-entry chain โ and nothing else. Conditional.SelectTargets (internal/strategies/conditional.go) evaluates strategy.conditions[] in declared order; the first rule whose key/value matches the request wins, and when none matches, the no-match fallback (targets[0]) is the whole answer.
The pipeline walks a chain the way it walks a pool: it advances to the next member only after a failover-safe failure (a transport failure, an attempt that timed out waiting on the target, 408, 429, 5xx, a provider's own context-length overflow, an open circuit, or a full concurrency queue), skips a member whose circuit is open or that is parked after a 429, and returns any other 4xx to the client. What it never does is substitute a target outside the chain. A rule with one target is therefore exact: a model it does not serve, a surface it cannot handle, or an open circuit is the corresponding error (404, 404, 503) rather than a sibling's answer.
The matcher key is a closed set, validated at load: a key outside it is a ferrogw validate / startup error, not a live-request failure. Since v1.5.2 the set covers the request's shape as well as its model, and one allow-listed header โ no other request header ever reaches a rule.
key | Matches when | value |
|---|---|---|
model | the request's model equals value exactly | a model name |
model_prefix | the request's model starts with value | a prefix such as claude- |
user | the request's user field equals value | a user id |
stream | the request is ("true") or is not ("false") a streaming request | "true" | "false" |
has_tools | the request carries ("true") or does not carry ("false") a tools array | "true" | "false" |
metadata | the entry named by field in the X-Gateway-Metadata request header equals value | a string |
X-Gateway-Metadata is a JSON object of at most 32 string, number or boolean values within 4 KiB, accepted on /v1/chat/completions and /v1/completions, and never forwarded to a provider; a malformed header is the caller's 400. user also applies to embeddings and image requests, which carry the field; stream, has_tools and metadata are chat-only and match nothing on the other surfaces.
Config keysโ
| Key | Type | Default | Description |
|---|---|---|---|
strategy.mode | string | โ | Set to conditional. |
strategy.conditions[].key | string | โ | model | model_prefix | user | stream | has_tools | metadata. Closed set โ an unrecognized value is a load error. |
strategy.conditions[].value | string | โ | What key is matched against. stream and has_tools accept only "true" or "false". |
strategy.conditions[].field | string | โ | The metadata entry a key: metadata rule reads. Required there, refused elsewhere. |
strategy.conditions[].target_key | string | โ | The one target this rule routes to; must name a configured targets[].virtual_key. Sugar for a one-entry target_keys. |
strategy.conditions[].target_keys | []string | โ | The rule's ordered target chain. Every entry must be a declared target, none may repeat, and exactly one of target_key and target_keys is set. |
Minimal working YAMLโ
strategy:
mode: conditional
conditions:
- key: model
value: gpt-4o
target_key: openai
- key: model_prefix
value: claude-
target_keys: [anthropic, bedrock] # Anthropic first; Bedrock stands in only for a provider-side failure
- key: user
value: vip
target_key: openai
- key: metadata
field: tier
value: gold
target_key: openai
targets:
- virtual_key: openai # targets[0] doubles as the no-match fallback
- virtual_key: anthropic
- virtual_key: bedrock
When to useโ
- Deterministic pinning: a specific model, tenant or request shape must always be served by a specific backend, for compliance, contractual, or data-residency reasons โ not "prefer this provider," but "only this provider."
- Routing whole model families by prefix (
claude-,gpt-,gemini-) to their native provider without listing every model name individually. - A pinned rule that still needs a stand-in: name the stand-in in
target_keys, and it is used only when the preferred member is at fault. - Any setup where
fallback's orloadbalance's willingness to pick a different provider is the behaviour you need to prevent, not add.
Gotchasโ
- A model the matched chain doesn't serve is
404 model_not_found, even when another configured target serves it. The rule is a decision about which targets handle this request, not a preference among several. Reaching another target means naming it in the rule. - A one-target rule whose target is down answers
503, not a sibling. Before v1.5.2 an open circuit on the matched target borrowed a healthy sibling fromtargets[]; it no longer does. Put the sibling intarget_keysif you want it used. GET /v1/modelsmirrors the same restriction. A model missing from that listing under this mode is the signal that a rule is needed for it, before a request ever hits the 404.- The no-match fallback is
targets[0], alone. Put the target you want unmatched requests to land on first. - The condition-key set is closed on purpose. Anything outside the six keys above โ a typo, a field name from another mode, an arbitrary header โ is rejected at load, not silently ignored or routed to the fallback.
- The chain is walked like a pool, and stops at its end. Retry (
targets[].retry) re-asks a member before the walk moves on; a member whose circuit is open or that is parked after a429is passed over; a deterministic4xxfrom a member stops the walk; a stream that has begun is never failed over mid-stream.