Skip to content

Commit 627f2bb

Browse files
committed
docs(relay): document the native integration
Signed-off-by: Alex Fournier <afournier@nvidia.com>
1 parent 7a23989 commit 627f2bb

3 files changed

Lines changed: 319 additions & 2 deletions

File tree

docs/index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ It supports OpenAI Chat Completions, OpenAI Responses, and Anthropic Messages.
99
|---|---|---|
1010
| Run Switchyard as a standalone proxy for API clients | Server Path | [Build and run the Rust server](getting_started.md#server-path) |
1111
| Add Switchyard routing to a Rust application | Library Path | [`switchyard-libsy`](../crates/libsy/README.md) |
12-
| Add Switchyard routing to NeMo Relay | Native Plugin Path | [`switchyard-nemo-relay-plugin`](../crates/switchyard-nemo-relay-plugin/README.md) |
12+
| Add Switchyard routing to NeMo Relay | Native Plugin Path | [Use Switchyard with NeMo Relay](integrations/nemo_relay.md) |
1313

1414
The Server Path builds and runs the standalone `switchyard-server` binary.
1515

@@ -28,4 +28,4 @@ The Server Path builds and runs the standalone `switchyard-server` binary.
2828
- [`switchyard-libsy`](reference/rust_api.md#switchyard-libsy): embeddable routing algorithms
2929
- [`switchyard-protocol`](reference/rust_api.md#switchyard-protocol): provider-neutral API types
3030
- [`switchyard-translation`](../crates/switchyard-translation/README.md): protocol translation
31-
- [`switchyard-nemo-relay-plugin`](../crates/switchyard-nemo-relay-plugin/README.md): native NeMo Relay integration
31+
- [`switchyard-nemo-relay-plugin`](../crates/switchyard-nemo-relay-plugin/README.md): native plugin build and configuration

docs/integrations/nemo_relay.md

Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
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).

mkdocs.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ nav:
2323
- Concepts:
2424
- Core Concepts: core_concepts.md
2525
- Architecture: architecture.md
26+
- Integrations:
27+
- NeMo Relay: integrations/nemo_relay.md
2628
- Routing:
2729
- Overview: routing_algorithms/overview.md
2830
- Sub-Agent-Aware Routing: routing_algorithms/subagent_routing.md

0 commit comments

Comments
 (0)