@@ -8,7 +8,7 @@ SPDX-License-Identifier: Apache-2.0
88This crate builds the external ` nvidia.switchyard ` native plugin. It embeds
99` switchyard-libsy ` , drives ` Algorithm::run_stream ` , and uses
1010` switchyard-llm-client ` for provider HTTP calls. Managed calls use Relay's
11- generic asynchronous middleware hooks and do not require a targeted provider
11+ public Rust LLM execution-intercept SDK and do not require a targeted provider
1212continuation from Relay.
1313
1414The plugin uses NeMo Relay native API v1. It depends on the small
@@ -59,47 +59,87 @@ This boundary has two important consequences:
5959 transport activity is therefore not represented as nested Relay LLM
6060 lifecycle events. Relay records the outer managed call and the plugin emits
6161 Switchyard routing marks; bridging Switchyard transport spans into Relay is
62- future work. With an exported Agent or turn scope, Relay 0.7 projects the
63- outer LLM span and routing-decision span as siblings in one OpenInference
64- trace. The native callback cannot parent an asynchronous decision mark
65- directly under the active LLM event; an embedded host that invokes an LLM
66- without an exported parent scope would therefore produce an orphan decision
67- span.
62+ future work. Routing marks are delivered through the public SDK while Relay
63+ is polling the active execution callback or stream, so they remain attached
64+ to the current Relay scope stack.
6865- Switchyard owns provider URLs, credentials, HTTP retry behavior, and
6966 translation for managed calls. Relay neither validates nor transports those
7067 target details.
7168
72- ## Native API v1 and asynchronous execution
73-
74- The manifest remains ` compat.native_api = "1" ` , but this rebuilt plugin requires
75- the generic C host-table v3 extension shipped by Relay 0.7. It registers through
76- v3's completion-based buffered and incremental streaming hooks, returns
77- ` Pending ` immediately, and performs libsy and HTTP work on a plugin-owned Tokio
78- runtime. Relay's runtime workers therefore do not wait synchronously for
79- provider I/O.
80-
81- The v3 stream hook retains Relay's bounded 32-event output queue. The plugin
82- retries a logical event when that queue is full, checks cancellation between
83- attempts, and releases every completion, continuation, stream, and captured
84- scope handle exactly once. Managed HTTP work is selected against Relay caller
85- cancellation, so dropping a buffered or streaming call drops its in-flight
86- request future.
87-
88- Unmanaged profiles use the same generic v3 continuation hooks for pass-through.
89- V3's downstream stream callback has continue/cancel control but no asynchronous
90- acknowledgement, so it cannot provide true end-to-end backpressure. The adapter
91- uses a nonblocking bridge capped at 8 MiB of queued, encoded event payloads; it
92- also caps the queue at 256 events and safely rejects a pass-through stream that
93- outruns either bound. The byte cap does not include transient JSON parsing or
94- in-memory representation overhead. Direct host forwarding without this bound would
95- still require a small generic Relay hook, but not a targeted-provider contract.
96- Managed Switchyard streams do not use this pass-through bridge. None of the
97- managed HTTP or routing operations requires a targeted LLM continuation.
98-
99- This is still a raw C boundary: Switchyard contains a small ownership adapter
100- for host strings and v3 handles because Relay 0.7 does not expose a safe Rust
101- facade for the generic async surface. That adapter is transport-independent;
102- all HTTP and routing behavior remains in Switchyard.
69+ ## Native API v1 and the public Rust SDK
70+
71+ The manifest uses ` compat.native_api = "1" ` . The implementation registers with
72+ ` PluginContext::register_llm_execution_intercept ` and
73+ ` PluginContext::register_llm_stream_execution_intercept ` , receives typed
74+ ` LlmRequest ` , ` LlmNext ` , and ` LlmStreamNext ` values, and returns the SDK's JSON
75+ result or ` LlmJsonStream ` . The ` nemo-relay-plugin ` SDK owns the C callback
76+ trampolines, host strings, continuation handles, panic containment, and native
77+ stream lifecycle. Switchyard contains no raw C callback or host-table adapter.
78+
79+ Switchyard performs libsy and provider I/O on a plugin-owned Tokio runtime. A
80+ buffered SDK callback waits for that executor to finish. A managed streaming
81+ callback returns a pull-based Rust iterator backed by a bounded 32-message
82+ channel; the async producer waits when the consumer is slow. Dropping that
83+ iterator closes the channel and aborts its in-flight routing task.
84+
85+ The public native API v1 callback shapes do not provide full in-flight caller
86+ cancellation. A buffered callback has no cancellation token, so a provider
87+ request already in progress continues until it responds or reaches the client
88+ timeout after the caller disconnects. Relay can cancel a streaming iterator
89+ between pulls, but its synchronous ` Iterator::next ` call cannot be interrupted
90+ while it is waiting for the next provider event. The configured 120-second
91+ inactivity timeout bounds both cases. Supporting prompt disconnect propagation
92+ would require an asynchronous public SDK callback or stream-polling contract;
93+ the plugin does not bypass the SDK to recover that behavior.
94+
95+ ### Relay runtime capacity
96+
97+ In Relay 0.7, the safe native LLM callbacks run directly in Relay's asynchronous
98+ middleware future; Relay does not move them to Tokio's separate blocking pool.
99+ Consequently, each active buffered Switchyard call occupies a normal Relay
100+ Tokio worker while it waits for the plugin executor, and a streaming call
101+ occupies a worker while its synchronous ` Iterator::next ` waits for the next
102+ provider event. Exhausting those workers can delay unrelated Relay work.
103+
104+ The Relay CLI constructs a Tokio multi-thread runtime without setting an
105+ explicit worker count. Tokio therefore defaults to the number of CPU cores
106+ available to the process and honors its ` TOKIO_WORKER_THREADS ` environment
107+ variable. Operators can provide additional capacity while using this native API
108+ v1 integration, for example:
109+
110+ ``` bash
111+ TOKIO_WORKER_THREADS=32 nemo-relay ...
112+ ```
113+
114+ This adjusts the normal asynchronous worker pool, not Tokio's blocking pool;
115+ increasing ` max_blocking_threads ` does not address this integration. There is no
116+ universal recommended value. Size the pool with headroom above the expected
117+ number of concurrent managed calls and Relay's other work, then validate it
118+ under representative provider latency and streaming concurrency. Increasing
119+ the worker count mitigates starvation but does not restore cancellation or make
120+ the synchronous boundary non-blocking.
121+
122+ Embedded Relay hosts own their Tokio runtime and should configure the same
123+ capacity explicitly:
124+
125+ ``` rust
126+ let runtime = tokio :: runtime :: Builder :: new_multi_thread ()
127+ . worker_threads (32 )
128+ . enable_all ()
129+ . build ()? ;
130+ ```
131+
132+ The durable resolution is a public Relay SDK callback and stream-polling
133+ contract that can yield while provider I/O is pending and receive caller
134+ cancellation. Until that exists, keep the provider timeout bounded, provision
135+ worker capacity, and load-test the intended concurrency rather than relying on
136+ thread-pool growth alone.
137+
138+ Unmanaged profiles call the typed ` LlmNext ` or ` LlmStreamNext ` continuation and
139+ return its result directly. Managed streams use the bounded Switchyard channel;
140+ unmanaged streams use Relay's SDK-owned pull iterator without an additional
141+ bridge. None of the managed HTTP or routing operations requires a targeted LLM
142+ continuation or direct access to Relay's C ABI.
103143
104144## Supported routers
105145
@@ -144,11 +184,10 @@ preservation contract and an explicit reject-lossy stream policy.
144184During release-candidate validation the manifest declares
145185` compat.native_api = "1" ` and Relay ` >=0.7.0-rc.4,<1.0 ` , and the Rust SDK uses
146186the exact published ` 0.7.0-rc.4 ` crate. Before release, move both lower bounds
147- to stable ` 0.7.0 ` . The manifest API value selects the released v1 plugin
148- contract; the binary is built against the V3 C host table shipped on the Relay
149- 0.7 line, which is why the minimum Relay version is not 0.6. Rebuild the bundle
150- when changing SDK versions rather than assuming Rust dynamic-library
151- compatibility from the manifest value alone.
187+ to stable ` 0.7.0 ` . The manifest API value selects Relay's released native
188+ plugin contract; plugin authors use its safe Rust SDK rather than the underlying
189+ C table directly. Rebuild the bundle when changing SDK versions rather than
190+ assuming Rust dynamic-library compatibility from the manifest value alone.
152191
153192A Relay project can configure a seeded weighted-random router as follows:
154193
0 commit comments