Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8199f72
feat(relay): add dynamic plugin on runner
bbednarski9 Aug 23, 2026
84cf1fe
refactor(relay): use shared configured runner
bbednarski9 Aug 23, 2026
8f4316f
build(relay): update dependency to 0.8.0-rc.2
bbednarski9 Aug 24, 2026
c1f4796
fix(relay): validate plugin bundle output
bbednarski9 Aug 24, 2026
d5f0a88
refactor(relay): remove obsolete translation preflight
bbednarski9 Aug 24, 2026
33097ea
style(relay): format plugin sources
bbednarski9 Aug 24, 2026
12814d2
feat(relay): emit typed route failure marks
bbednarski9 Aug 25, 2026
83b2097
build(relay): update dependency to 0.8.0-rc.3
bbednarski9 Aug 25, 2026
7005a16
build(relay): retain focused RC3 lockfile update
bbednarski9 Aug 25, 2026
4503b10
build(relay): resolve focused plugin dependencies
bbednarski9 Aug 25, 2026
d60ad26
test(relay): cover plugin protocol mapping
bbednarski9 Aug 25, 2026
84b47da
refactor(relay): use route error kind
bbednarski9 Aug 25, 2026
85f7ff4
refactor(relay): use route error summary
bbednarski9 Aug 25, 2026
c734c76
refactor(relay): use route error summary methods
bbednarski9 Aug 25, 2026
58ae42f
feat(relay): support inline Switchyard configuration
bbednarski9 Aug 25, 2026
9ee8f9e
feat(relay): emit Switchyard routing telemetry
bbednarski9 Aug 25, 2026
bf18e6f
feat(relay): emit Switchyard token usage metrics
bbednarski9 Aug 25, 2026
08b42ff
fix(relay): retain HTTP status in failure metrics
bbednarski9 Aug 25, 2026
fd8910b
test(relay): collect bundle packaging regression
bbednarski9 Aug 28, 2026
3b3c97b
perf(relay): bypass decoding unmanaged requests
bbednarski9 Aug 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### Added

- **NeMo Relay native plugin** — a dynamically loaded integration that loads
Switchyard's standard TOML deployment and executes its named routes in
process. Managed calls require NeMo Relay 0.8.0 or newer; unknown models use
Relay's continuation unchanged.

- **NeMo Relay routing marks** — routing-model usage, measured routing
overhead, and selected-model decisions are emitted as ATOF marks. The final
serving call remains represented only by Relay's outer LLM lifecycle event to
prevent double-counting.

- **Advisor-gate routing** — new `advisor` route type pairing the serving
executor with a stronger judge-only advisor that reviews terminal turns:
APPROVE releases the buffered turn, REDO discards it and feeds the advisor's
Expand Down
90 changes: 90 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ members = [
"crates/prefill-router",
"crates/switchyard-py",
"crates/protocol",
"crates/switchyard-nemo-relay-plugin",
"crates/switchyard-runner",
"crates/switchyard-server",
"crates/switchyard-skill-distillation",
Expand All @@ -34,6 +35,7 @@ http = "1"
httpdate = "1"
jsonschema = { version = "0.49.4", default-features = false }
jsonptr = { version = "0.8.1", default-features = false, features = ["std", "json", "resolve"] }
nemo-relay-plugin = { git = "https://github.com/NVIDIA/NeMo-Relay.git", tag = "0.8.0-rc.3" }
parking_lot = "0.12"
rand = "0.10"
regex = "1"
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ algorithm you write yourself.
- **Protocol Translation**: convert between OpenAI Chat, Anthropic Messages, and OpenAI Responses formats
- **Multi-Backend Routing**: random routing, LLM-as-classifier routing, signal-driven stage-router, or your own algorithm
- **Operational Metrics**: Prometheus metrics cover requests, errors, latency, tokens, and routing overhead
- **NeMo Relay Plugin**: run random, classifier, escalation, or stage routing in Relay while Switchyard owns provider HTTP dispatch

## Maturity

Expand Down Expand Up @@ -129,6 +130,7 @@ configured LLM client selects one upstream format.
- **[`switchyard-libsy`](crates/libsy/README.md)**: embed routing algorithms in a Rust application
- **[`switchyard-protocol`](crates/protocol/README.md)**: provider-neutral request, response, and streaming types
- **[`switchyard-translation`](crates/switchyard-translation/README.md)**: request, response, and stream translation
- **[`switchyard-nemo-relay-plugin`](crates/switchyard-nemo-relay-plugin/README.md)**: install Switchyard as a native NeMo Relay plugin

## Community

Expand Down
32 changes: 32 additions & 0 deletions crates/switchyard-nemo-relay-plugin/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

[package]
name = "switchyard-nemo-relay-plugin"
version.workspace = true
description = "Switchyard-owned HTTP routing plugin for NeMo Relay"
authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
publish = false

[lib]
crate-type = ["cdylib"]

[dependencies]
futures-util.workspace = true
http.workspace = true
nemo-relay-plugin.workspace = true
serde.workspace = true
serde_json.workspace = true
switchyard-llm-client.workspace = true
switchyard-protocol.workspace = true
switchyard-runner.workspace = true
switchyard-translation.workspace = true
tokio.workspace = true
toml = "1.1"

[dev-dependencies]
tempfile = "3"
118 changes: 118 additions & 0 deletions crates/switchyard-nemo-relay-plugin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Switchyard NeMo Relay Plugin

`switchyard-nemo-relay-plugin` is a native NeMo Relay dynamic plugin. It loads
a standard Switchyard TOML deployment from a file or Relay's nested plugin
configuration and executes its configured routes through `switchyard-runner`.

The plugin does not define a second routing or target configuration language.
`switchyard-server` and Relay therefore use the same targets, client pooling,
algorithm construction, retry policy, and route validation.

## Install

Build the platform bundle with the package script, then configure Relay to load
the generated `relay-plugin.toml` manifest. The plugin requires NeMo Relay
`>=0.8.0,<1.0`.

## Configure Relay

Use exactly one Switchyard deployment source. To share an existing deployment
file with `switchyard-server`, configure its path:

```toml
[[plugins.dynamic]]
manifest = "./plugins/switchyard/relay-plugin.toml"

[plugins.dynamic.config]
priority = 0
switchyard_config_path = "/etc/switchyard/routes.toml"
```

`switchyard_config_path` is a Switchyard version-1 TOML deployment, accepted by both
`switchyard-server` and `switchyard-runner`. See the
[server configuration guide](../switchyard-server/CONFIGURATION.md) for the
deployment schema and routing algorithms.

To keep the deployment in the Relay configuration, nest the same version-1
Switchyard configuration under `switchyard_config`:

```toml
[[plugins.dynamic]]
manifest = "./plugins/switchyard/relay-plugin.toml"

[plugins.dynamic.config]
priority = 0

[plugins.dynamic.config.switchyard_config]
schema_version = 1

[plugins.dynamic.config.switchyard_config.llm_clients.primary]
format = "openai_chat"
base_url = "https://example.test/v1"

[plugins.dynamic.config.switchyard_config.targets.default]
id = "example/model"
llm_client = "primary"

[plugins.dynamic.config.switchyard_config.routes.default]
id = "switchyard/default"
type = "passthrough"
target = "default"
```

## Request handling

For OpenAI Chat Completions, OpenAI Responses, and Anthropic Messages calls,
the plugin decodes the Relay request and checks the requested model against the
deployment's route IDs.

- A configured route is executed by `switchyard-runner`.
- An unknown model calls Relay's continuation unchanged.
- The returned provider response is encoded back into the caller's wire format.
- Streaming responses are returned as unpolled translated streams; Relay owns
cancellation and the outer serving-call lifecycle.

Each route's target client must use the caller's wire format: `openai_chat`,
`openai_responses`, or `anthropic_messages`. The runner selects the upstream
backend from that format rather than translating a route to a different
provider API. When one upstream model must serve multiple caller formats,
declare a target and route for each corresponding client format.

The plugin emits a routing request mark, routing-model call marks, measured
routing-overhead marks, and a selected-model decision mark. Token usage is
emitted as Switchyard metrics for both routing-model and answer-model calls;
Relay retains ownership of the outer LLM lifecycle.

## Observability

When Relay is configured with OTLP logs and metrics exporters, the plugin emits
typed telemetry through Relay's native plugin runtime:

- Routing request, decision, and overhead marks are Info logs.
- Per-routing-model call marks are Debug logs, including their outcome and
latency, but not token usage.
- Terminal routing and response-finalization failures are Error logs. Their
payload contains only the safe Switchyard failure summary; it excludes
provider response bodies and free-form provider messages.
- Metrics use bounded attributes only: algorithm for
`switchyard.routing.requests`; outcome for `switchyard.routing.llm_calls`
and `switchyard.routing.llm_call.duration`;
and safe failure kind, category, phase, and optional upstream HTTP status for
`switchyard.routing.failures`. `switchyard.routing.overhead` records total
routing latency, including routing-model calls; durations use milliseconds.
- `switchyard.routing.llm_tokens` records normalized token usage with
`call_role` (`routing` or `answer`), configured `target_model`, and
`token_type` attributes. A provider may omit usage for streaming responses;
the plugin does not synthesize zero-value measurements.

The plugin does not attach sessions, requests, or provider messages as metric
attributes. `target_model` comes from the configured Switchyard target set,
rather than arbitrary caller input, keeping the metric cardinality bounded by
the deployment.

## Failure policy

`switchyard-llm-client` owns provider retry and route-candidate fallback
behavior. The plugin does not maintain a separate trusted-default target or
rerun routing after an execution failure. Failures outside the shared runner,
including response translation failures, are returned to Relay.
40 changes: 40 additions & 0 deletions crates/switchyard-nemo-relay-plugin/config.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Switchyard NeMo Relay Plugin",
"description": "Runs a Switchyard deployment through its shared in-process runner.",
"type": "object",
"additionalProperties": false,
"oneOf": [
{ "required": ["switchyard_config_path"] },
{ "required": ["switchyard_config"] }
],
"properties": {
"priority": {
"type": "integer",
"default": 0,
"description": "NeMo Relay execution-intercept priority."
},
"executor": {
"type": "object",
"additionalProperties": false,
"description": "Optional NeMo Relay SDK executor override.",
"properties": {
"worker_threads": {
"type": "integer",
"minimum": 1,
"description": "Number of worker threads for the plugin's SDK-owned executor."
}
}
},
"switchyard_config_path": {
"type": "string",
"minLength": 1,
"description": "Path to a Switchyard version-1 TOML deployment shared with switchyard-server."
},
"switchyard_config": {
"type": "object",
"minProperties": 1,
"description": "Inline Switchyard version-1 deployment using the same schema as switchyard-server."
}
}
}
Loading
Loading