Skip to content

Commit 53a30bb

Browse files
committed
feat(relay): support inline Switchyard configuration
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
1 parent b6d6135 commit 53a30bb

6 files changed

Lines changed: 183 additions & 14 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/switchyard-nemo-relay-plugin/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,7 @@ switchyard-protocol.workspace = true
2626
switchyard-runner.workspace = true
2727
switchyard-translation.workspace = true
2828
tokio.workspace = true
29+
toml = "1.1"
30+
31+
[dev-dependencies]
32+
tempfile = "3"

crates/switchyard-nemo-relay-plugin/README.md

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# Switchyard NeMo Relay Plugin
22

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

77
The plugin does not define a second routing or target configuration language.
88
`switchyard-server` and Relay therefore use the same targets, client pooling,
@@ -16,22 +16,50 @@ the generated `relay-plugin.toml` manifest. The plugin requires NeMo Relay
1616

1717
## Configure Relay
1818

19-
Point the dynamic plugin configuration at an existing Switchyard deployment:
19+
Use exactly one Switchyard deployment source. To share an existing deployment
20+
file with `switchyard-server`, configure its path:
2021

2122
```toml
2223
[[plugins.dynamic]]
23-
plugin_id = "nvidia.switchyard"
24+
manifest = "./plugins/switchyard/relay-plugin.toml"
2425

2526
[plugins.dynamic.config]
2627
priority = 0
27-
deployment_path = "/etc/switchyard/routes.toml"
28+
switchyard_config_path = "/etc/switchyard/routes.toml"
2829
```
2930

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

36+
To keep the deployment in the Relay configuration, nest the same version-1
37+
Switchyard configuration under `switchyard_config`:
38+
39+
```toml
40+
[[plugins.dynamic]]
41+
manifest = "./plugins/switchyard/relay-plugin.toml"
42+
43+
[plugins.dynamic.config]
44+
priority = 0
45+
46+
[plugins.dynamic.config.switchyard_config]
47+
schema_version = 1
48+
49+
[plugins.dynamic.config.switchyard_config.llm_clients.primary]
50+
format = "openai_chat"
51+
base_url = "https://example.test/v1"
52+
53+
[plugins.dynamic.config.switchyard_config.targets.default]
54+
id = "example/model"
55+
llm_client = "primary"
56+
57+
[plugins.dynamic.config.switchyard_config.routes.default]
58+
id = "switchyard/default"
59+
type = "passthrough"
60+
target = "default"
61+
```
62+
3563
## Request handling
3664

3765
For OpenAI Chat Completions, OpenAI Responses, and Anthropic Messages calls,

crates/switchyard-nemo-relay-plugin/config.schema.json

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,37 @@
44
"description": "Runs a Switchyard deployment through its shared in-process runner.",
55
"type": "object",
66
"additionalProperties": false,
7-
"required": ["deployment_path"],
7+
"oneOf": [
8+
{ "required": ["switchyard_config_path"] },
9+
{ "required": ["switchyard_config"] }
10+
],
811
"properties": {
912
"priority": {
1013
"type": "integer",
1114
"default": 0,
1215
"description": "NeMo Relay execution-intercept priority."
1316
},
14-
"deployment_path": {
17+
"executor": {
18+
"type": "object",
19+
"additionalProperties": false,
20+
"description": "Optional NeMo Relay SDK executor override.",
21+
"properties": {
22+
"worker_threads": {
23+
"type": "integer",
24+
"minimum": 1,
25+
"description": "Number of worker threads for the plugin's SDK-owned executor."
26+
}
27+
}
28+
},
29+
"switchyard_config_path": {
1530
"type": "string",
1631
"minLength": 1,
1732
"description": "Path to a Switchyard version-1 TOML deployment shared with switchyard-server."
33+
},
34+
"switchyard_config": {
35+
"type": "object",
36+
"minProperties": 1,
37+
"description": "Inline Switchyard version-1 deployment using the same schema as switchyard-server."
1838
}
1939
}
2040
}

crates/switchyard-nemo-relay-plugin/src/config.rs

Lines changed: 104 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
use std::path::PathBuf;
55

66
use serde::Deserialize;
7+
use serde_json::{Map, Value};
78
use switchyard_protocol::WireFormat;
89
use switchyard_runner::Runner;
910

@@ -21,19 +22,66 @@ pub(crate) fn protocol_from_call(name: &str) -> Option<WireFormat> {
2122
pub(crate) struct SwitchyardConfig {
2223
#[serde(default)]
2324
pub(crate) priority: i32,
24-
pub(crate) deployment_path: PathBuf,
25+
#[serde(default)]
26+
pub(crate) switchyard_config_path: Option<PathBuf>,
27+
#[serde(default)]
28+
pub(crate) switchyard_config: Option<Map<String, Value>>,
2529
}
2630

2731
impl SwitchyardConfig {
2832
pub(crate) fn load_runner(&self) -> Result<Runner, String> {
29-
Runner::load(&self.deployment_path).map_err(|error| error.to_string())
33+
match (&self.switchyard_config_path, &self.switchyard_config) {
34+
(Some(path), None) => Runner::load(path).map_err(|error| error.to_string()),
35+
(None, Some(config)) => toml::to_string(config)
36+
.map_err(|error| format!("failed to serialize Switchyard configuration: {error}"))
37+
.and_then(|source| Runner::from_toml(&source).map_err(|error| error.to_string())),
38+
(Some(_), Some(_)) => Err(
39+
"configure exactly one of switchyard_config_path or switchyard_config".to_string(),
40+
),
41+
(None, None) => {
42+
Err("configure one of switchyard_config_path or switchyard_config".to_string())
43+
}
44+
}
3045
}
3146
}
3247

3348
#[cfg(test)]
3449
mod tests {
50+
use std::io::Write;
51+
52+
use serde_json::json;
53+
use tempfile::NamedTempFile;
54+
3555
use super::*;
3656

57+
fn inline_deployment() -> Map<String, Value> {
58+
json!({
59+
"schema_version": 1,
60+
"llm_clients": {
61+
"primary": {
62+
"format": "openai_chat",
63+
"base_url": "https://example.test/v1"
64+
}
65+
},
66+
"targets": {
67+
"default": {
68+
"id": "example/model",
69+
"llm_client": "primary"
70+
}
71+
},
72+
"routes": {
73+
"default": {
74+
"id": "switchyard/default",
75+
"type": "passthrough",
76+
"target": "default"
77+
}
78+
}
79+
})
80+
.as_object()
81+
.unwrap()
82+
.clone()
83+
}
84+
3785
#[test]
3886
fn maps_supported_relay_call_names_to_wire_formats() {
3987
let cases = [
@@ -47,4 +95,58 @@ mod tests {
4795
assert_eq!(protocol_from_call(name), expected, "{name}");
4896
}
4997
}
98+
99+
#[test]
100+
fn builds_runner_from_inline_deployment() {
101+
let config = SwitchyardConfig {
102+
priority: 0,
103+
switchyard_config_path: None,
104+
switchyard_config: Some(inline_deployment()),
105+
};
106+
107+
let runner = config.load_runner().unwrap();
108+
assert!(runner.route("switchyard/default").is_some());
109+
}
110+
111+
#[test]
112+
fn builds_runner_from_deployment_path() {
113+
let mut deployment = NamedTempFile::new().unwrap();
114+
write!(
115+
deployment,
116+
"{}",
117+
toml::to_string(&inline_deployment()).unwrap()
118+
)
119+
.unwrap();
120+
let config = SwitchyardConfig {
121+
priority: 0,
122+
switchyard_config_path: Some(deployment.path().to_path_buf()),
123+
switchyard_config: None,
124+
};
125+
126+
let runner = config.load_runner().unwrap();
127+
assert!(runner.route("switchyard/default").is_some());
128+
}
129+
130+
#[test]
131+
fn requires_exactly_one_configuration_source() {
132+
let neither = SwitchyardConfig {
133+
priority: 0,
134+
switchyard_config_path: None,
135+
switchyard_config: None,
136+
};
137+
assert!(matches!(
138+
neither.load_runner(),
139+
Err(error) if error.contains("configure one of switchyard_config_path or switchyard_config")
140+
));
141+
142+
let both = SwitchyardConfig {
143+
priority: 0,
144+
switchyard_config_path: Some(PathBuf::from("routes.toml")),
145+
switchyard_config: Some(inline_deployment()),
146+
};
147+
assert!(matches!(
148+
both.load_runner(),
149+
Err(error) if error.contains("configure exactly one of switchyard_config_path or switchyard_config")
150+
));
151+
}
50152
}

crates/switchyard-nemo-relay-plugin/src/lib.rs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,9 @@ fn register_stream(
122122
}
123123

124124
fn parse_config(plugin_config: &Map<String, Json>) -> Result<SwitchyardConfig, String> {
125-
serde_json::from_value(Json::Object(plugin_config.clone()))
125+
let mut config = plugin_config.clone();
126+
config.remove("executor");
127+
serde_json::from_value(Json::Object(config))
126128
.map_err(|error| format!("invalid Switchyard configuration: {error}"))
127129
}
128130

@@ -135,9 +137,20 @@ mod tests {
135137
use super::*;
136138

137139
#[test]
138-
fn plugin_configuration_requires_a_deployment_path() {
140+
fn plugin_configuration_requires_a_switchyard_source() {
139141
let config = json!({"priority": 0});
140-
let error = parse_config(config.as_object().unwrap()).unwrap_err();
141-
assert!(error.contains("deployment_path"));
142+
assert!(matches!(
143+
parse_config(config.as_object().unwrap()).and_then(SwitchyardRuntime::new),
144+
Err(error) if error.contains("switchyard_config_path or switchyard_config")
145+
));
146+
}
147+
148+
#[test]
149+
fn plugin_configuration_ignores_the_sdk_executor_override() {
150+
let config = json!({
151+
"executor": {"worker_threads": 4},
152+
"switchyard_config_path": "routes.toml"
153+
});
154+
assert!(parse_config(config.as_object().unwrap()).is_ok());
142155
}
143156
}

0 commit comments

Comments
 (0)