Switchyard Configuration

View as Markdown

Experimental NeMo Relay 0.6.0 integration: This release calls a separately running Switchyard Decision API.

Start with the Switchyard 0.6.0 setup and validation guide before using this page as the complete configuration reference.

The Switchyard component connects Relay’s CLI gateway to a separately running Switchyard Decision API. Relay owns provider credentials, backend endpoints, dispatch, retries, and trusted fallbacks. The Switchyard service selects a backend. Relay validates the decision, translates requests and responses in process through the switchyard-translation library, and dispatches directly to the selected Relay-owned target.

How the Configuration Fits Together

Configure the integration in four core steps. Profiles that use ATOF history add the fifth requirement:

  1. Set decision_api_url and authentication so Relay can call the Switchyard Decision API.
  2. Select the Switchyard routing profile with decision_profile_id.
  3. Add a Relay-owned targets binding for every Switchyard backend_id that the profile can return. Relay validates the returned backend ID, model, protocol, and endpoint exactly before dispatch.
  4. Choose a trusted same-protocol target for each inbound protocol under default_targets. Relay uses these defaults for observe-only traffic and fail-open dispatch.
  5. Configure profiles that depend on ATOF history, including StageRouter, with context_mode = "atof_required" and atof_endpoint_name, then configure the matching observability stream sink. Relay fails startup validation if the named sink is missing or invalid.

The following is a complete component configuration. The referenced environment variables must be set before Relay starts.

1version = 1
2
3[[components]]
4kind = "switchyard"
5enabled = true
6
7[components.config]
8version = 1
9mode = "enforce"
10priority = 0
11decision_api_url = "http://127.0.0.1:4000/v1/routing/decision"
12decision_profile_id = "stage-router"
13request_materialization = "recent_message_window"
14context_mode = "atof_required"
15atof_endpoint_name = "switchyard"
16decision_timeout_millis = 25
17max_retries = 3
18recent_message_count = 8
19enabled_inbound_profiles = ["openai_chat", "openai_responses", "anthropic_messages"]
20
21[components.config.decision_header_env]
22authorization = "SWITCHYARD_AUTHORIZATION"
23
24[components.config.default_targets]
25openai_chat = "openai-default"
26openai_responses = "responses-default"
27anthropic_messages = "anthropic-default"
28
29[components.config.targets.openai-default]
30model = "provider/efficient-chat"
31protocol = "openai_chat"
32endpoint = "/v1/chat/completions"
33base_url = "https://provider.example.com"
34
35[components.config.targets.openai-default.header_env]
36authorization = "OPENAI_PROVIDER_AUTHORIZATION"
37
38[components.config.targets.responses-default]
39model = "provider/efficient-responses"
40protocol = "openai_responses"
41endpoint = "/v1/responses"
42base_url = "https://provider.example.com"
43
44[components.config.targets.responses-default.header_env]
45authorization = "OPENAI_PROVIDER_AUTHORIZATION"
46
47[components.config.targets.anthropic-default]
48model = "provider/efficient-messages"
49protocol = "anthropic_messages"
50endpoint = "/v1/messages"
51base_url = "https://provider.example.com"
52
53[components.config.targets.anthropic-default.header_env]
54x-api-key = "ANTHROPIC_PROVIDER_API_KEY"
55
56[components.config.targets.capable-chat]
57model = "provider/capable-chat"
58protocol = "openai_chat"
59endpoint = "/v1/chat/completions"
60base_url = "https://provider.example.com"
61
62[components.config.targets.capable-chat.header_env]
63authorization = "OPENAI_PROVIDER_AUTHORIZATION"
64
65[[components]]
66kind = "observability"
67enabled = true
68
69[components.config]
70version = 2
71
72[components.config.atof]
73enabled = true
74
75[[components.config.atof.sinks]]
76type = "stream"
77name = "switchyard"
78url = "http://127.0.0.1:4000/v1/atof/events"
79transport = "http_post"
80field_name_policy = "preserve"
81
82[components.config.atof.sinks.header_env]
83authorization = "SWITCHYARD_AUTHORIZATION"

Decision API and Rollout Options

The following table describes the Decision API and rollout settings:

OptionDefaultDescription and constraints
version1Component configuration version.
mode"enforce"enforce applies valid decisions. observe_only records the hypothetical decision but dispatches the trusted same-protocol default once.
priority0Component execution priority.
decision_api_urlRequiredHTTP or HTTPS URL for the Switchyard Decision API.
decision_profile_idRequiredNon-empty Switchyard profile identifier sent with each routing request.
decision_timeout_millis25Decision API timeout in milliseconds; must be greater than zero.
decision_headersEmptyOptional non-sensitive static Decision API headers.
decision_header_envEmptyMap of header names to environment-variable names for sensitive Decision API headers. Referenced values must be present and non-blank.

Do not define the same header, including case variants, in both a static header map and its environment map. Keep bearer tokens, API keys, cookies, and other secrets in environment variables rather than tracked configuration.

Request Materialization

Every Decision API request contains routing identity, inbound protocol, a request summary, and the attempt number. request_materialization controls how much additional request content is included.

ModeMaterial sent to Switchyard
noneNo current_request; use identity, protocol, summary, and attempt metadata only.
summary_onlyNo current_request; explicitly selects summary-based routing.
latest_user_promptA provider-valid body containing the latest user material plus an explicit prompt field. The request fails open if no user message is available.
recent_message_windowThe most recent normalized messages, bounded by recent_message_count, re-encoded for the inbound protocol.
annotated_requestThe final intercepted provider body plus Relay’s normalized request representation.
full_bodyThe final intercepted provider request body.

Choose the least materialization needed by the selected profile. In particular, none and summary_only intentionally share the same current wire behavior but express different configuration intent.

Context, Retries, and Inbound Protocols

The following table describes context, retry, and inbound protocol settings:

OptionDefaultDescription and constraints
context_modeRequiredpayload_only or atof_required. See below.
max_retries3Provider retries after the initial attempt; maximum 10. Each retry obtains a new Switchyard decision.
recent_message_count8Message limit for recent_message_window; must be greater than zero.
enabled_inbound_profilesAll three supported protocolsNon-empty list containing openai_chat, openai_responses, and/or anthropic_messages.
atof_endpoint_nameRequired for atof_requiredName of exactly one stream sink under the observability component’s atof.sinks list.

payload_only supports request-aware routing without an ATOF dependency. Relay can create request-scoped synthetic identity when stable identity is not available.

atof_required is for profiles that classify from accumulated ATOF context. It requires stable session and request identity plus an enabled, authenticated Relay HTTP ATOF stream sink whose name matches atof_endpoint_name. The sink specifies the Switchyard ingestion URL, must use transport = "http_post", and must set field_name_policy = "preserve". Relay rejects missing or duplicate named sinks during startup validation. A local ATOF file sink does not satisfy this delivery requirement by itself.

Target Bindings and Trusted Defaults

The following table describes target bindings and trusted defaults:

OptionRequired fieldsPurpose
targets.<backend_id>model, protocol, endpoint, base_urlBinds an exact Switchyard backend ID to a Relay-owned provider destination.
targets.<backend_id>.headersNoneOptional non-sensitive static provider headers.
targets.<backend_id>.header_envNoneOptional provider header names mapped to environment-variable names. Referenced values must be present and non-blank.
default_targets.openai_chatTarget using openai_chatTrusted fallback for OpenAI Chat Completions requests.
default_targets.openai_responsesTarget using openai_responsesTrusted fallback for OpenAI Responses requests.
default_targets.anthropic_messagesTarget using anthropic_messagesTrusted fallback for Anthropic Messages requests.

Do not define the same target header, including case variants, in both headers and header_env.

Protocol and endpoint pairs are fixed:

ProtocolEndpoint
openai_chat/v1/chat/completions
openai_responses/v1/responses
anthropic_messages/v1/messages

Each protocol in enabled_inbound_profiles requires a corresponding default_targets entry. Defaults for disabled protocols can be omitted. Each configured default must refer to a target with the matching protocol. Duplicate exact bindings for model, protocol, endpoint, and base URL are rejected.

Observe-Only Rollout

Use observe-only mode to verify decisions and routing marks without sending traffic to the selected target:

1[components.config]
2mode = "observe_only"
3context_mode = "payload_only"
4request_materialization = "summary_only"

Relay calls the Decision API and records the hypothetical selection, but sends the request to the inbound protocol’s trusted default once. Routing retries are not applied.

Payload-Only Request-Aware Routing

Use recent request content for routing without operating the ATOF accumulator:

1[components.config]
2mode = "enforce"
3context_mode = "payload_only"
4request_materialization = "recent_message_window"
5recent_message_count = 8

ATOF-Backed StageRouter Routing

Use ATOF context when StageRouter should classify from accumulated session signals:

1[components.config]
2mode = "enforce"
3decision_profile_id = "stage-router"
4context_mode = "atof_required"
5request_materialization = "summary_only"
6atof_endpoint_name = "switchyard"

Configure the matching observability component with a named stream sink:

1[[components]]
2kind = "observability"
3enabled = true
4
5[components.config]
6version = 2
7
8[components.config.atof]
9enabled = true
10
11[[components.config.atof.sinks]]
12type = "stream"
13name = "switchyard"
14url = "http://127.0.0.1:4000/v1/atof/events"
15transport = "http_post"
16field_name_policy = "preserve"
17
18[components.config.atof.sinks.header_env]
19authorization = "SWITCHYARD_AUTHORIZATION"

Runtime and Fail-Open Behavior

The integration handles routing and failures as follows:

  • Relay validates the decision’s backend ID, model, protocol, and endpoint against its target binding before dispatch. Unknown backends, malformed decisions, and target drift fail open.
  • Decision API errors are not recursively retried. Relay immediately dispatches the trusted same-protocol default.
  • Retryable provider connection, timeout, status, context-window, and model-unavailable failures can obtain a new decision, bounded by max_retries.
  • Non-retryable provider failures use the trusted fallback immediately. After retry exhaustion, Relay dispatches that fallback once.
  • A streaming request can retry only before receiving its first upstream item. After the stream commits, Relay propagates later failures without another dispatch.
  • Relay forwards provider-specific extensions unchanged when every configured target uses the inbound protocol. If any target uses another protocol, nonportable extensions fail open to the trusted same-protocol default before the Decision API call. Request translation failures before provider dispatch and buffered response translation failures also fail open. After a streaming response emits its first item, later provider or translation failures propagate without another dispatch.
  • Routing-mark delivery is best-effort and does not change provider results.

Experimental Limitations

Consider the following experimental limitations before adopting the plugin:

  • The plugin is published as nemo-relay-switchyard but is excluded from default Relay CLI builds. Build the CLI with cargo build -p nemo-relay-cli --features switchyard.
  • The routing boundary is currently service-based. Relay calls Switchyard’s HTTP Decision API but does not start or supervise the service. Relay derives /health from decision_api_url and fails plugin activation unless the service returns {"status":"ok"}. Decision API failures after activation fail open to the trusted same-protocol default.
  • ATOF-backed profiles additionally depend on Switchyard’s HTTP ingestion and accumulator runtime, stable request and session identity, and one named, authenticated Relay ATOF HTTP stream sink. A local file sink does not populate the Switchyard accumulator.
  • Provider-protocol translation runs in Relay’s process through the switchyard-translation Rust library. Routing decisions and ATOF accumulation remain out of process.
  • The Rust Switchyard component is currently registered only by Relay’s CLI gateway. Hermes native plugins and the LangChain, LangGraph, and Deep Agents integrations do not load it in process. They receive Switchyard routing only when their provider traffic is explicitly sent through the Relay gateway.
  • The Switchyard examples are manual compatibility and trajectory workflows, not production deployment orchestration.
  • Compatibility is pinned to Switchyard commit 8f9db9a6 on topic/nemo-relay-integration. Testing another revision is deliberate and can expose contract drift.
  • The Decision API and service/library boundary are experimental.
  • Nonportable provider extensions in configurations that can translate across protocols, malformed decisions, service failures, pre-dispatch translation failures, buffered-response translation failures, and target drift fail open to Relay’s trusted same-protocol default. Failures after a streaming response commits propagate without another dispatch.
  • Secrets must remain environment-referenced. The examples generate ephemeral local credentials and are not credential-management guidance.

Run nemo-relay doctor to validate component configuration before startup. For the pinned local service and manual compatibility workflow, refer to the Switchyard integration examples.