|
| 1 | +# Use Switchyard with NeMo Relay |
| 2 | + |
| 3 | +Use the |
| 4 | +[Switchyard native plugin](../../crates/switchyard-nemo-relay-plugin/README.md) |
| 5 | +to add model routing to an existing |
| 6 | +[NeMo Relay](https://docs.nvidia.com/nemo/relay/v0.8.3/about-nemo-relay/overview#integrating-with-relay) |
| 7 | +deployment without running a second service. It runs through Relay's |
| 8 | +[native plugin system](https://docs.nvidia.com/nemo/relay/v0.8.3/build-plugins/native/about). |
| 9 | +Relay receives the application's model request and keeps its normal |
| 10 | +[middleware](https://docs.nvidia.com/nemo/relay/v0.8.3/about-nemo-relay/concepts/middleware#what-middleware-is) |
| 11 | +and |
| 12 | +[observability](https://docs.nvidia.com/nemo/relay/v0.8.3/configure-plugins/observability/about#shortest-path). |
| 13 | +Switchyard chooses and calls the configured model target. |
| 14 | + |
| 15 | +Use this integration when Relay already handles your agent's model calls and |
| 16 | +you want to: |
| 17 | + |
| 18 | +- present one route name while Switchyard chooses the model; |
| 19 | +- reuse the same Switchyard TOML deployment as `switchyard-server`; |
| 20 | +- inspect routing decisions and model attempts through Relay's existing |
| 21 | + telemetry; and |
| 22 | +- keep Relay's existing handling for models that Switchyard does not manage. |
| 23 | + |
| 24 | +If Relay is not part of the application, run the [standalone server](../getting_started.md#server-path) |
| 25 | +or embed [`switchyard-libsy`](../../crates/libsy/README.md) directly. |
| 26 | + |
| 27 | +## How Requests Flow |
| 28 | + |
| 29 | +Relay loads the plugin into its own process. For each supported model request, |
| 30 | +the plugin checks whether the requested model matches a configured Switchyard |
| 31 | +route. Matching requests go through Switchyard. Other requests are left |
| 32 | +unchanged by Switchyard and passed to the next Relay handler. The plugin uses |
| 33 | +Relay's |
| 34 | +[execution intercepts](https://docs.nvidia.com/nemo/relay/v0.8.3/about-nemo-relay/concepts/middleware#execution-intercepts) |
| 35 | +for non-streaming requests and |
| 36 | +[stream execution intercepts](https://docs.nvidia.com/nemo/relay/v0.8.3/about-nemo-relay/concepts/middleware#stream-execution-intercepts) |
| 37 | +for streaming requests. |
| 38 | + |
| 39 | +```mermaid |
| 40 | +flowchart TB |
| 41 | + caller["Agent or SDK"] |
| 42 | +
|
| 43 | + subgraph relay_process["NeMo Relay process"] |
| 44 | + relay["Relay LLM execution"] |
| 45 | + relay --> match{"Model matches a<br/>Switchyard route ID?"} |
| 46 | + match -->|"No"| next["Next Relay handler"] |
| 47 | + match -->|"Yes"| plugin["Native Switchyard plugin<br/>decode, route, translate, and call"] |
| 48 | + next --> relay_out |
| 49 | + plugin -.->|"Routing observations"| telemetry["Relay marks and metrics"] |
| 50 | + plugin -->|"Translated output"| relay_out["Relay returns response or stream"] |
| 51 | + end |
| 52 | +
|
| 53 | + target["Configured model provider"] |
| 54 | + caller --> relay |
| 55 | + plugin <-->|"Target call"| target |
| 56 | +``` |
| 57 | + |
| 58 | +For a matching request, Switchyard performs model selection, provider calls, |
| 59 | +retries, and fallback itself. Model calls used to make a routing decision, as |
| 60 | +well as calls to the selected or fallback answer model, do not run through |
| 61 | +Relay's LLM middleware again. This avoids treating a router's judge call or |
| 62 | +fallback attempt as another application request. |
| 63 | + |
| 64 | +Relay still records the caller-facing |
| 65 | +[LLM call](https://docs.nvidia.com/nemo/relay/v0.8.3/instrument-applications/instrument-llm-call#integration-pattern), |
| 66 | +while Switchyard adds |
| 67 | +[marks](https://docs.nvidia.com/nemo/relay/v0.8.3/about-nemo-relay/concepts/events#mark) |
| 68 | +and metrics for the routing work inside it. |
| 69 | + |
| 70 | +| Owner | Responsibilities | |
| 71 | +| --- | --- | |
| 72 | +| Relay | Receives the caller's request, runs Relay middleware, returns the response or stream, and exports telemetry. | |
| 73 | +| Switchyard | Chooses a target, translates formats, calls the provider, and handles configured retries and fallback. | |
| 74 | +| Model provider | Runs the model and returns its response, stream, and available usage. | |
| 75 | + |
| 76 | +For more detail, see Relay's |
| 77 | +[managed execution pipeline](https://docs.nvidia.com/nemo/relay/v0.8.3/about-nemo-relay/architecture#managed-execution-pipeline) |
| 78 | +and |
| 79 | +[plugin delivery models](https://docs.nvidia.com/nemo/relay/v0.8.3/about-nemo-relay/concepts/plugins#plugin-delivery-models). |
| 80 | + |
| 81 | +## Set Up the Plugin |
| 82 | + |
| 83 | +Follow the [plugin README](../../crates/switchyard-nemo-relay-plugin/README.md) |
| 84 | +to build and package the native library, register and enable it in Relay, and |
| 85 | +configure its deployment. Relay documents how to |
| 86 | +[add and enable a discoverable plugin](https://docs.nvidia.com/nemo/relay/v0.8.3/configure-plugins/discoverable-plugins#add-and-enable-a-plugin) |
| 87 | +and how it |
| 88 | +[validates the package before loading code](https://docs.nvidia.com/nemo/relay/v0.8.3/configure-plugins/discoverable-plugins#validate-before-loading-code). |
| 89 | + |
| 90 | +!!! note "Relay compatibility" |
| 91 | + |
| 92 | + You do not need Relay 0.8.3 specifically. The packaged |
| 93 | + [`relay-plugin.toml`](../../crates/switchyard-nemo-relay-plugin/relay-plugin.toml) |
| 94 | + is the source of truth: it currently accepts Relay `>=0.8.1,<0.9.0` and |
| 95 | + native plugin API `1`. Relay checks both before loading the library. Links |
| 96 | + on this page point to version 0.8.3 so the documentation does not drift to |
| 97 | + an unsupported Relay release. |
| 98 | + |
| 99 | +The plugin accepts exactly one Switchyard deployment source: a |
| 100 | +`switchyard_config_path` shared with `switchyard-server`, or the same version-1 |
| 101 | +deployment nested under `switchyard_config`. Both use the |
| 102 | +[Switchyard TOML schema](../reference/toml_schema.md). |
| 103 | + |
| 104 | +The plugin reuses the deployment's routes, targets, and LLM clients. It does not |
| 105 | +use Switchyard's `fallback_client` for an unmatched model. Relay's next handler |
| 106 | +decides what happens to that request. |
| 107 | + |
| 108 | +The `id` of each configured Switchyard route becomes a model name that callers |
| 109 | +can send through Relay. No additional Relay route table is required for those |
| 110 | +model names. |
| 111 | + |
| 112 | +## Request Handling |
| 113 | + |
| 114 | +The plugin handles these Relay LLM calls: |
| 115 | + |
| 116 | +- OpenAI Chat Completions (`openai.chat_completions`) |
| 117 | +- OpenAI Responses (`openai.responses`) |
| 118 | +- Anthropic Messages (`anthropic.messages`) |
| 119 | + |
| 120 | +Only requests whose `model` is a string matching a Switchyard route ID are |
| 121 | +routed. Other call types, missing or non-string model values, and unconfigured |
| 122 | +model names are left unchanged by Switchyard and passed to Relay's next handler. |
| 123 | + |
| 124 | +The caller and selected target may use different supported API formats. |
| 125 | +Switchyard normalizes the request, routes it, and returns the response in the |
| 126 | +caller's original format. If Switchyard forwards the caller's credential, both |
| 127 | +formats must use the same credential family: OpenAI-compatible or Anthropic. |
| 128 | + |
| 129 | +Not every provider-specific field has a lossless equivalent. Switchyard rejects |
| 130 | +a conversion it cannot perform safely instead of silently dropping data. |
| 131 | + |
| 132 | +### Streaming |
| 133 | + |
| 134 | +For streaming requests, Switchyard hands Relay a lazy translated stream. Relay |
| 135 | +drives delivery and cancellation and records when the caller-facing stream |
| 136 | +starts and ends. As Relay consumes the stream, Switchyard continues to translate |
| 137 | +chunks and record late usage or errors. |
| 138 | + |
| 139 | +- Initial routing marks are available when the stream opens. |
| 140 | +- An answer-call result of `ok` means the provider opened the stream. It does |
| 141 | + not guarantee that the full stream completed. |
| 142 | +- For an upstream response that remains streamed, answer token metrics appear |
| 143 | + only if the provider reports usage and the stream reaches its final event. A |
| 144 | + canceled or dropped stream may have no answer-token metrics. |
| 145 | +- Later provider failures can emit `switchyard.routing.error`. Some failures |
| 146 | + before routing or while encoding Relay output have no Switchyard mark, so the |
| 147 | + marks are not a complete request-failure log. |
| 148 | +- If Relay rejects a telemetry event, the plugin writes the error to standard |
| 149 | + error and still returns the model response. |
| 150 | + |
| 151 | +## State and Identity |
| 152 | + |
| 153 | +The plugin keeps request and response data in memory only while handling the |
| 154 | +call. For a stream, that data remains until the stream finishes or the caller |
| 155 | +drops it. The plugin does not store these payloads on disk. |
| 156 | + |
| 157 | +The plugin creates one Switchyard runner when Relay activates it and shares the |
| 158 | +runner across requests until the plugin is deactivated. Some routing algorithms |
| 159 | +keep in-memory state there, such as session affinity or an escalation decision. |
| 160 | +Each algorithm controls when that state expires. The state is not shared between |
| 161 | +Relay processes and is lost when a process restarts. See Relay's documentation |
| 162 | +on |
| 163 | +[plugin ownership](https://docs.nvidia.com/nemo/relay/v0.8.3/about-nemo-relay/concepts/plugins#ownership-and-scope) |
| 164 | +and |
| 165 | +[runtime state](https://docs.nvidia.com/nemo/relay/v0.8.3/about-nemo-relay/architecture#where-runtime-state-lives). |
| 166 | + |
| 167 | +To associate routing telemetry with the rest of an agent run, the plugin adds |
| 168 | +these fields to every routing mark, including marks that carry metric |
| 169 | +measurements. Missing values are `null`: |
| 170 | + |
| 171 | +- `session_id` |
| 172 | +- `agent_id` |
| 173 | +- `parent_agent_id` |
| 174 | +- `task_id` |
| 175 | +- `turn_id` |
| 176 | +- `correlation_id` |
| 177 | + |
| 178 | +These fields are event metadata used to correlate routing records with a run. |
| 179 | +Subscribers and log or trace exporters can read them, but Relay does not copy |
| 180 | +them into exported metric attributes. |
| 181 | + |
| 182 | +These values come from request headers rather than Relay's active scope. Relay's |
| 183 | +[session and subagent headers](https://docs.nvidia.com/nemo/relay/v0.8.3/nemo-relay-cli/basic-usage#runtime-mapping) |
| 184 | +can populate them for correlation. They do not by themselves mark a request as |
| 185 | +delegated work for Switchyard's |
| 186 | +[`subagents` router](../routing_algorithms/subagent_routing.md). For algorithms |
| 187 | +that keep per-session state, reuse a stable session ID across turns. Relay's |
| 188 | +`x-nemo-relay-session-id` header is accepted; `x-switchyard-session-id` |
| 189 | +provides an explicit override. If Relay's gateway has no stable session ID, the |
| 190 | +plugin leaves the Switchyard session ID unset. Send `x-switchyard-session-id` |
| 191 | +when an algorithm must keep the same per-session state across turns. |
| 192 | + |
| 193 | +## Routing Telemetry |
| 194 | + |
| 195 | +Switchyard sends its routing records into the same Relay telemetry stream as |
| 196 | +the caller-facing LLM call. Existing Relay |
| 197 | +[subscribers](https://docs.nvidia.com/nemo/relay/v0.8.3/about-nemo-relay/concepts/subscribers#how-subscribers-relate-to-events) |
| 198 | +can export both, so a separate Switchyard telemetry pipeline is not required. |
| 199 | +How those records appear in a backend depends on Relay's |
| 200 | +[OpenTelemetry trace projection](https://docs.nvidia.com/nemo/relay/v0.8.3/configure-plugins/observability/opentelemetry#trace-projections) |
| 201 | +and |
| 202 | +[OpenInference projection](https://docs.nvidia.com/nemo/relay/v0.8.3/configure-plugins/observability/openinference#plugin-configuration). |
| 203 | + |
| 204 | +### How Routing Appears in Traces |
| 205 | + |
| 206 | +When the caller-facing LLM call uses Relay's active agent scope as its parent, |
| 207 | +the Switchyard marks use that same scope and appear alongside the call. |
| 208 | +Switchyard does not create another nested scope. For Relay's |
| 209 | +[full and OpenInference trace projections](https://docs.nvidia.com/nemo/relay/v0.8.3/configure-plugins/observability/opentelemetry#trace-projections), |
| 210 | +[`mark_projection`](https://docs.nvidia.com/nemo/relay/v0.8.3/configure-plugins/observability/opentelemetry#trace-endpoint-fields) |
| 211 | +controls whether a backend displays each eligible mark as an event on the |
| 212 | +parent or as a visible zero-duration child span. The |
| 213 | +[`gen_ai` projection](https://docs.nvidia.com/nemo/relay/v0.8.3/configure-plugins/observability/opentelemetry#genai-projection) |
| 214 | +omits marks. With `mark_projection = "tool"`, the trace has this shape: |
| 215 | + |
| 216 | +```mermaid |
| 217 | +flowchart LR |
| 218 | + agent["Relay agent scope"] |
| 219 | + llm["LLM call"] |
| 220 | + requested["switchyard.routing.requested"] |
| 221 | + calls["switchyard.routing.llm_call<br/>routing and answer attempts"] |
| 222 | + overhead["switchyard.routing.overhead"] |
| 223 | + decision["switchyard.routing.decision"] |
| 224 | + error["switchyard.routing.error<br/>when execution fails"] |
| 225 | + agent --> llm |
| 226 | + agent --> requested |
| 227 | + agent --> calls |
| 228 | + agent --> overhead |
| 229 | + agent --> decision |
| 230 | + agent -.->|"Failure"| error |
| 231 | +``` |
| 232 | + |
| 233 | +If the application did not create an agent scope, an observability backend can |
| 234 | +display the LLM span and marks as separate roots. The metadata field |
| 235 | +`parent_agent_id` is a correlation value; it does not set Relay trace |
| 236 | +parentage. See Relay's |
| 237 | +[scope hierarchy](https://docs.nvidia.com/nemo/relay/v0.8.3/about-nemo-relay/concepts/scopes#scope-hierarchy-and-ownership) |
| 238 | +for the parentage rules. |
| 239 | + |
| 240 | +### Mark Contract |
| 241 | + |
| 242 | +Dashboards and subscribers can use `data_schema` to identify the payload |
| 243 | +contract. Each non-metric mark uses the mark name as its schema name and version |
| 244 | +`1`. Consumers should accept additional fields and values within a version. |
| 245 | +Removing or renaming a field, changing its type, or changing its meaning |
| 246 | +requires a new version. Relay's |
| 247 | +[event envelope](https://docs.nvidia.com/nemo/relay/v0.8.3/about-nemo-relay/concepts/events#fields-common-to-every-event) |
| 248 | +describes the surrounding event envelope. |
| 249 | + |
| 250 | +| Mark | Severity | Data | |
| 251 | +| --- | --- | --- | |
| 252 | +| `switchyard.routing.requested` | Info | Routing `algorithm` for a managed request. | |
| 253 | +| `switchyard.routing.llm_call` | Debug | `call_index`, model in `selected_model`, `call_role` (`routing` or `answer`), `outcome`, and `latency_ms` for each observed model call. | |
| 254 | +| `switchyard.routing.overhead` | Info | `latency_ms` spent producing the routing outcome, including routing-model calls. This is not the end-to-end request duration. | |
| 255 | +| `switchyard.routing.decision` | Info | `algorithm`, initial `selected_model`, nullable final `served_model`, and nullable `fallback_used`. | |
| 256 | +| `switchyard.routing.error` | Error | Generic failures contain `failure_kind`. Route-execution failures also contain `category` and `phase`, plus nullable `upstream_status` and `target`. | |
| 257 | + |
| 258 | +Call marks describe Switchyard observations, not every HTTP retry made inside a |
| 259 | +client. `call_role` records whether Switchyard classified the call as routing |
| 260 | +or answer work. |
| 261 | + |
| 262 | +`switchyard.routing.llm_call` uses Debug severity. It still appears in the |
| 263 | +supported trace projections, but Relay's OTLP logs default to Info. Set |
| 264 | +`minimum_severity` to `debug` to include these call records in |
| 265 | +[log export](https://docs.nvidia.com/nemo/relay/v0.8.3/configure-plugins/observability/opentelemetry#log-export). |
| 266 | + |
| 267 | +Model fields use the target's upstream model ID, not its local TOML key. For |
| 268 | +example, if `[targets.fast].id = "provider/model-a"`, the mark records |
| 269 | +`provider/model-a`, not `fast`. Relay records the model value after its request |
| 270 | +middleware runs, normally the Switchyard route ID. On the response, Relay can |
| 271 | +report the model that actually answered. |
| 272 | + |
| 273 | +`fallback_used` is `true` when the final served model differs from the initial |
| 274 | +selection and `false` when they match. It and `served_model` are `null` when the |
| 275 | +response does not provide serving metadata. If route execution fails before a |
| 276 | +response is available, the error mark describes the terminal failure instead. |
| 277 | + |
| 278 | +### Metrics |
| 279 | + |
| 280 | +| Metric | Kind and unit | Meaning and attributes | |
| 281 | +| --- | --- | --- | |
| 282 | +| `switchyard.routing.requests` | Counter, events | Managed requests, labeled by `algorithm`. | |
| 283 | +| `switchyard.routing.llm_calls` | Counter, events | Routing-model calls, labeled by `outcome`. | |
| 284 | +| `switchyard.routing.llm_call.duration` | Histogram, milliseconds | Routing-model call duration, labeled by `outcome`. | |
| 285 | +| `switchyard.routing.overhead` | Histogram, milliseconds | Time spent producing the routing outcome. | |
| 286 | +| `switchyard.routing.llm_tokens` | Counter, tokens | Provider-reported token values, labeled by `call_role`, `target_model`, and `token_type`. | |
| 287 | +| `switchyard.routing.failures` | Counter, events | Terminal failures, labeled by safe failure kind and available classification fields. | |
| 288 | + |
| 289 | +`switchyard.routing.llm_calls` and `switchyard.routing.llm_call.duration` cover |
| 290 | +routing-model calls only. Answer calls appear in the per-call marks and token |
| 291 | +metrics. |
| 292 | + |
| 293 | +Token metrics cover routing and answer calls when the provider reports usage. |
| 294 | +The plugin does not synthesize zeroes for missing values. The supported token |
| 295 | +types are `input`, `cached_input`, `cache_creation_input`, `output`, |
| 296 | +`reasoning`, and `total`. |
| 297 | + |
| 298 | +Configure delivery through Relay's |
| 299 | +[OpenTelemetry metric export](https://docs.nvidia.com/nemo/relay/v0.8.3/configure-plugins/observability/opentelemetry#metric-export). |
| 300 | + |
| 301 | +## Data Handling |
| 302 | + |
| 303 | +Switchyard routing telemetry contains a small, defined set of routing fields, |
| 304 | +not request or response content. Its marks do not contain prompts, request or |
| 305 | +response bodies, headers, credentials, raw provider response bodies, or |
| 306 | +free-form provider error messages. Relay's caller-facing LLM events can |
| 307 | +capture request and response data according to Relay's |
| 308 | +[input and output event semantics](https://docs.nvidia.com/nemo/relay/v0.8.3/about-nemo-relay/concepts/events#input-and-output-payloads), |
| 309 | +independently of these Switchyard marks. |
| 310 | + |
| 311 | +Header forwarding is part of request execution, not telemetry. Caller headers |
| 312 | +are forwarded upstream except credentials and headers owned by the HTTP client, |
| 313 | +such as connection and content headers. Authentication and configured extra |
| 314 | +headers follow the selected client's settings in the |
| 315 | +[TOML schema](../reference/toml_schema.md). |
0 commit comments