TOML Schema

September 4, 2026 · View on GitHub

The native deployment file defines the LLM clients, targets, and routes that a Switchyard server serves. It is read by switchyard-server --config.

Validate a file without starting the server:

switchyard-server --config routes.toml --dry-run

Minimal Example

schema_version = 1

[llm_clients.openrouter]
format = "openai_chat"
base_url = "https://openrouter.ai/api/v1"
api_key_env = "OPENROUTER_API_KEY"

[targets.strong]
id = "anthropic/claude-sonnet-4.5"
llm_client = "openrouter"

[routes.default]
id = "switchyard"
type = "passthrough"
target = "strong"

schema_version must be 1. Table names under llm_clients, targets, and routes are local references; clients send the route's id as the model name.

The optional top-level fallback_client names an entry under [llm_clients]. When set, any HTTP method and path not implemented by Switchyard is forwarded through that client without translating the path, query string, body, response, or model identifier. The fallback client does not need a target. Only its base_url is used: caller end-to-end headers are forwarded, hop-by-hop headers are removed, and configured API keys, extra headers, format, and retries are not applied. When omitted, unmatched paths return 404.

schema_version, [targets], and [routes] must all be present, even when a route reaches no upstream. A file without a [targets] table is rejected with missing field targets; an empty [targets] table satisfies it. [llm_clients] defaults to empty and may be omitted.

[llm_clients.<name>]

KeyRequiredDefaultMeaning
formatYesopenai_chat, openai_responses, or anthropic_messages.
base_urlYesUpstream base URL.
api_key_envNounsetName of the environment variable holding the key. Omit to send no authentication.
forward_authNofalseForward the caller's provider credential to this upstream.
extra_headersNo{}Custom HTTP headers sent to the model server. Set credentials with api_key_env or forward_auth; the server rejects headers owned by the selected auth mode. Header names are case-insensitive.
max_retriesNo2Retry budget, 010.

The TOML never contains the secret itself. api_key_env names a variable that must exist and be non-empty when the server loads.

Set forward_auth = true to use each caller's credential instead of a server-owned key:

[llm_clients.claude]
format = "anthropic_messages"
base_url = "https://api.anthropic.com"
forward_auth = true

forward_auth cannot be combined with api_key_env. OpenAI clients forward authorization, chatgpt-account-id, and x-openai-fedramp. Anthropic clients forward authorization or x-api-key; for Claude subscription OAuth, they also forward oauth-* values from anthropic-beta and remove all other inbound beta values.

This setting gives base_url the caller's login. Enable it only when that upstream should receive the credential, and use HTTPS unless the upstream runs on loopback. Forwarding clients do not follow HTTP redirects. Check every forwarding client used by a route, including classifier and judge targets. The server rejects an Anthropic forwarding route called through an OpenAI endpoint, or an OpenAI forwarding route called through an Anthropic endpoint, before it calls an upstream.

[targets.<name>]

KeyRequiredDefaultMeaning
idYesExact model ID sent upstream.
llm_clientYesKey under [llm_clients].
extra_bodyNo{}Values merged into the upstream request when the request does not already set that key.

[routes.<name>]

Every route takes the common keys below, plus the keys for its type.

KeyRequiredDefaultMeaning
idYesPublic model ID that callers send in requests.
typeYesRouting algorithm for this route.
context_windowNounsetPositive token count advertised for this route by GET /v1/models. Unset values appear as null. This does not enforce a request limit.
tool_callingNounsetWhether GET /v1/models advertises tool-calling support for this route. Unset values appear as null.
reasoningNounsetWhether GET /v1/models advertises reasoning support to Codex direct-provider discovery. Unset routes are advertised as non-reasoning.
visionNounsetWhether GET /v1/models advertises image input to Codex direct-provider discovery. Unset routes are advertised as text-only. This is not cosmetic: Codex reads input_modalities from the model card and, when it reads text-only, replaces an attached image with the text image content omitted because you do not support image input before sending, so a route whose target can see but which does not declare vision = true loses the image in the client. Declare it only when every target the route can select accepts images.

noop

Returns a buffered assistant response containing OK without calling an upstream model. Use it for local smoke tests.

A noop-only deployment reaches no upstream but still needs the [targets] table, which can be empty:

schema_version = 1

[targets]

[routes.smoke]
id = "noop-route"
type = "noop"

passthrough

Sends parent requests to one target. It can also route delegated sub-agent work; see Sub-Agent-Aware Routing.

KeyRequiredMeaning
targetYesTarget used for parent and harness-maintenance requests.
subagentsNoNested passthrough or llm_classifier policy used only for delegated sub-agent work. Nested classifiers currently support only mode = "custom".

random

Splits traffic across targets. See Random Routing.

KeyRequiredDefaultMeaning
targetsYesTarget names to choose from.
weightsNoequalFinite, non-negative relative weights in targets order, with at least one positive value. Invalid weights are rejected at load time.
seedNounsetReproduces the selection sequence.

prefill_router

Routes the latest non-empty user message with a checkpoint-backed prefill classifier. Build switchyard-server with --features prefill-router and make the prefill router's Python dependencies available in the active virtual environment.

KeyRequiredDefaultMeaning
targetsYesTarget names in the exact order expected by the checkpoint outputs.
checkpointYesPath to the tensor-only router checkpoint. Relative paths use the server's working directory.
deviceNoautoPyTorch device used for encoder inference, such as cpu, cuda, or cuda:0.
cache_dirNounsetDirectory where Hugging Face caches the downloaded encoder and tokenizer.
max_lengthNo2048Maximum tokenized encoder input length; longer prompts are truncated.
batch_sizeNo32Maximum prompts per encoder forward pass.
[routes.prefill]
id = "switchyard/prefill"
type = "prefill_router"
targets = ["fast", "strong"]
checkpoint = "/models/router.pt"

llm_classifier

Runs one of three judge-backed modes: capability, escalation, or custom. classifier_target and max_output_tokens apply to all three.

KeyRequiredDefaultMeaning
modeNocapabilityClassifier behavior. Set it explicitly for new configurations.
classifier_targetYesTarget the judge is called through. Not a routing destination.
max_output_tokensNo4096Maximum completion tokens for the judge verdict. Must be at least 1.
response_format_typeNojson_schemaStructured-output mode for capability and escalation judges. Use json_object when the provider does not support JSON Schema; Switchyard adds the schema to the prompt and validates the verdict locally. Custom mode always uses its configured JSON Schema.

Capability mode classifies before serving. See LLM Classifier Routing.

KeyRequiredDefaultMeaning
strong_targetYesCapable tier.
weak_targetYesEfficient tier.
base_thresholdYesLowest solve probability that routes to the weak target. In [0, 1].
threshold_stepNo0.0Finite, non-negative amount added once for uncertain or unmatched verdicts and twice for unsupported verdicts. base_threshold + 2 * threshold_step must be at most 1.
classify_triggerNoevery_requestWhen the judge runs. every_request judges every request, tool continuations included. user_turn judges each new user message and retains that target across intervening tool calls only when requests carry a session ID; without a session ID, it behaves like every_request. new_session judges once and reuses that target for the session.
message_hash_fallbackNofalseKeys affinity on the first user message. Requires classify_trigger = "new_session".
recent_turn_windowNounsetWhen unset, the judge sees the opening task and latest user follow-up, when present. When set, it also sees trailing turns.
promptNopackaged promptReplaces the capability prompt. The packaged schema is sent separately as structured-output configuration.

Escalation mode serves the weak target first and judges the completed turn. See Escalation-Router Routing.

KeyRequiredDefaultMeaning
strong_targetYesTarget used after the session latches.
weak_targetYesTarget served before the latch.
promptNopackaged promptReplaces the trajectory-judge prompt.
escalation.confirmationsNo2Consecutive escalate verdicts required to latch. Above 1 needs a session ID.
escalation.recent_turn_windowNo28Trailing messages shown to the judge.
escalation.window_message_charsNo500Per-message cap inside that window.

Existing configurations that contain escalation but omit mode remain valid.

Custom mode validates the judge's JSON against response_schema, resolves the policy selector, and routes to any configured target label.

KeyRequiredDefaultMeaning
targetsYesTwo or more target names available to the policy.
default_targetYesTarget used when the judge fails or its verdict cannot be routed.
promptYesJudge system prompt. The configured inner schema is sent separately as structured-output configuration.
response_schemaYesInner JSON Schema encoded as a TOML string. Switchyard adds the provider wrapper.
policyYesPolicy table. target_selector accepts a JSON Pointer such as /decision/target.
classify_triggerNoevery_requestWhen the judge runs. every_request judges every request, tool continuations included. user_turn judges each new user message and retains that target across intervening tool calls only when requests carry a session ID; without a session ID, it behaves like every_request. new_session judges once and reuses that target for the session.
message_hash_fallbackNofalseKeys affinity on the first user message. Requires classify_trigger = "new_session".
recent_turn_windowNounsetWhen unset, the judge sees the opening task and latest user follow-up, when present. When set, it also sees trailing turns.

Classifier prompts must not contain {{RESPONSE_SCHEMA}}. Switchyard supplies the schema automatically: through the structured-output request in json_schema mode, or in the prompt in json_object mode.

stage_router

Scores tool signals to pick a tier per turn. See Stage-Router Routing for the optional handoff_notes and classifier tables and for tuning.

KeyRequiredDefaultMeaning
capable_targetYesCapable tier.
efficient_targetYesEfficient tier.
pickerYesefficient_first, or capable_first (experimental, unbenchmarked). Tier used when the signals are not confident.
confidence_thresholdYesCorroboration a decisive pick needs. In [0, 1].
recent_turn_windowNo3Trailing tool results the signals are computed over.
capable_system_promptNounsetSystem prompt handed to the capable tier.
efficient_system_promptNounsetSystem prompt handed to the efficient tier.
classifier.classify_triggerNoevery_requestWhen the judge runs. See the llm_classifier route. new_session has no effect here.
classifier.response_format_typeNojson_schemaStructured-output mode for the optional classifier judge. Use json_object when the classifier provider does not support JSON Schema; Switchyard adds the schema to the prompt and validates the verdict locally.
subagentsNounsetNested passthrough or custom llm_classifier policy used only for delegated sub-agent work. See Sub-Agent-Aware Routing.

composite

Composes other algorithms, letting one set another's configuration. Today a classifier sets the tier a stage router falls open to when its own signals are not confident, leaving its scoring and escalation logic untouched. See Composite Routing.

KeyRequiredDefaultMeaning
classifier.targetYesTarget the tier judge is called through. Not a routing destination.
classifier.base_thresholdYesp_solve floor that still routes to the efficient tier. In [0, 1].
classifier.classify_triggerYesuser_turn re-picks the tier whenever the user speaks, new_session picks once and holds it. every_request is rejected here: a judge call per tool step is the cost this route exists to avoid.
classifier.message_hash_fallbackNofalseRetains the tier by hashing the first user message, for clients that send no session ID. Unlike the llm_classifier route, this works on either trigger. Conversations opening with the same text share a tier.
stage.capable_targetYesCapable tier.
stage.efficient_targetYesEfficient tier.
stage.confidence_thresholdYesCorroboration a decisive signal needs. In [0, 1].
stage.recent_turn_windowNo3Trailing tool results the signals are computed over.
stage.capable_system_promptNounsetSystem prompt handed to the capable tier.
stage.efficient_system_promptNounsetSystem prompt handed to the efficient tier.
subagentsNounsetNested policy used only for delegated sub-agent work.

The tier is retained per session. A deployment that sends no session ID needs classifier.message_hash_fallback = true, which keys on the first user message instead. The stage table takes no picker: the classifier supplies that tier per turn. A turn the classifier cannot reach falls open to the efficient tier. Leaving out classifier is recommended: that judge runs ahead of the fall-open tier.

Validation Errors

--dry-run prefixes configuration failures with invalid server config <path>:. Within that wrapper, TOML deserialization errors start with failed to parse TOML:, while errors from validating the built configuration retain their inner message unchanged.