Skip to content

feat: add Relay routing Decision API#8

Merged
bbednarski9 merged 4 commits into
NVIDIA-NeMo:topic/nemo-relay-integrationfrom
bbednarski9:feature/relay-decision-api
Jul 8, 2026
Merged

feat: add Relay routing Decision API#8
bbednarski9 merged 4 commits into
NVIDIA-NeMo:topic/nemo-relay-integrationfrom
bbednarski9:feature/relay-decision-api

Conversation

@bbednarski9

@bbednarski9 bbednarski9 commented Jun 30, 2026

Copy link
Copy Markdown

What

Summary

Adds the first, independently functional layer of the NeMo Relay integration:

  • a versioned JSON routing-request and routing-decision contract;
  • decision-only Random and LLM routing through the existing profile runtimes;
  • POST /v1/routing/decision with distinct malformed, unknown-profile, and unsupported-profile errors;
  • Relay-compatible route metadata, including the configured Switchyard target ID as backend_id.

This is the Decision API slice of a previous end-to-end implementation, split into a focused public-repository change.

Why

Relay needs Switchyard to select a configured target without dispatching that target itself. Keeping this contract and the two existing v2 routing profiles in a focused first PR makes the wire API, profile capability boundary, and server error behavior reviewable before ATOF state or Cascade routing are introduced.

How tested

Testing summary

  • cargo fmt --all --check
  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo test -p switchyard-components-v2 (46 unit tests plus profile integration suites)
  • cargo test -p switchyard-server (21 server tests plus migration coverage)
  • cargo test --workspace --exclude switchyard-py
  • cargo check -p switchyard-py
  • Loopback classifier integration verifies LLM classification without calling the selected target.
  • uv run ruff check . — not applicable; no Python source changed.
  • uv run mypy switchyard — not applicable; no Python source changed.
  • uv run pytest tests/ — not applicable; the affected API is covered by Rust component/server tests.

Checklist

  • One class per file; filename = snake_case of the primary class. (N/A: Rust API change.)
  • New public symbols exported from switchyard/__init__.py.__all__ if intended for downstream use. (N/A: symbols are exported from the Rust crate root.)
  • Unit tests added for new components / bug fixes.
  • README / --help updated if customer-facing surface changed. (DESIGN.md documents the runtime contract.)
  • Commits signed off (Signed-off-by: Your Name <email>) per the DCO.

Notes for reviewers

Review guidelines

Suggested review order:

  1. decision.rs for the wire contract, protocol/materialization validation, and response conversion.
  2. profile.rs and registry.rs for the single-runtime capability path.
  3. Random/LLM profile adapters to verify that decision-only routing stops before selected-target dispatch.
  4. Server endpoint/error tests for Relay compatibility and status-code precedence.

The legacy Relay payload still includes decision_profile.router. Serde intentionally ignores that additive field; profile_id is authoritative. The response still includes router and all five route fields required by Relay.

Previous feedback addressed

  • Replaced the parallel DecisionRuntimeRegistry design with Profile::decide() on the same Arc<dyn Profile> already used for serving; configured runtimes are built once.
  • Removed typed request-side DecisionProfile.router while retaining legacy wire compatibility for the extra field.
  • Validates schema, attempt bounds, and inbound protocol before profile lookup, so malformed requests remain 400 even when the profile ID is unknown.
  • Rejects unknown inbound profiles rather than silently treating them as OpenAI Chat.
  • Validates actual user prompt material before an LLM classifier call, including adversarial empty/assistant-only shapes.
  • Replaced fallible test .expect() usage with typed error propagation.
  • Keeps malformed (400), unknown profile (404), and unsupported profile (422) failures distinct.

ATOF ingestion and Relay snapshots are intentionally deferred to #9; Relay-aware Cascade routing and unified session metadata are deferred to #11.

Follow-up fixes

  • Keeps Random and LLM decision-response conversion with their profile implementations; decision.rs now owns only shared contract and materialization helpers.
  • Validates required identity/protocol strings and rejects contradictory materialization modes or missing current_request.body before profile execution.
  • Adds focused envelope-validation coverage.

@bbednarski9
bbednarski9 marked this pull request as ready for review July 1, 2026 04:03

- `run()` is the authoritative serving path. It owns the complete request lifecycle and is the
method a Switchyard server should call.
- `decide()` is the decision-only path. Supporting profiles reuse their request-side policy but

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bbednarski9
bbednarski9 changed the base branch from main to topic/nemo-relay-integration July 8, 2026 00:58
@bbednarski9
bbednarski9 requested a review from a team as a code owner July 8, 2026 00:58
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>

# Conflicts:
#	crates/switchyard-components-v2/src/lib.rs
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
@bbednarski9
bbednarski9 force-pushed the feature/relay-decision-api branch from 35e6e49 to c5a9024 Compare July 8, 2026 13:27
@bbednarski9
bbednarski9 merged commit bae270b into NVIDIA-NeMo:topic/nemo-relay-integration Jul 8, 2026
14 checks passed
rapids-bot Bot pushed a commit to NVIDIA/NeMo-Relay that referenced this pull request Jul 13, 2026
> [!WARNING]
> **BREAKING CHANGE:** **[Rust source compatibility]** This PR adds `FlowError::Upstream` and new fields to the public `AtofEndpointConfig`, `AtofEndpointSectionConfig`, and `EmitMarkEventParams` types. Downstream exhaustive `FlowError` matches must handle the new variant, and direct struct literals must initialize the new fields or migrate to the provided constructor/builder APIs. Builder-based mark emission and default Relay configurations remain compatible.

#### Overview

This adds Relay's first-party integration with the [Switchyard](https://github.com/NVIDIA-NeMo/Switchyard) Decision API. The plugin routes buffered and streaming LLM requests through an external Switchyard service, executes the selected Relay-owned backend, and records causal model-routing optimization evidence.

Switchyard owns provider-protocol translation through its `switchyard-translation` Rust library. Relay consumes that library only when the native Switchyard plugin is enabled; default Relay builds do not include either the plugin or the translation dependency.

- [x] I confirm this contribution is my own work, or I have the right to submit it under this project's license.
- [x] I searched existing issues and open pull requests, and this does not duplicate existing work.

#### Details

##### Plugin and execution behavior

- Add the standalone `nemo-relay-switchyard` first-party plugin.
- Normalize stable Relay identity and build `switchyard.routing_request.v1` requests.
- Validate selected routes and optional counterfactual baselines against exact Relay-owned backend bindings.
- Support enforce and observe-only modes, six request-materialization modes, bounded retry-aware dispatch, protocol-specific trusted fallbacks, and no retry after the first upstream stream item.
- Record one model-routing optimization contribution only after the selected route commits. Failed decisions, failed provider attempts, and fallback dispatches do not claim routing savings.
- Preserve Switchyard routing metadata, including snapshot freshness, decision source, event count, and turn depth.
- Continue using Relay's existing ATOF exporter and lifecycle; the plugin does not install local routers, feature accumulators, or a duplicate event subscriber.

##### Translation ownership and optional dependency

Request, buffered-response, and SSE translation across OpenAI Chat Completions, OpenAI Responses, and Anthropic Messages use Switchyard's public `switchyard-translation` library instead of a Relay-local translation engine.

The dependency is pinned to the immutable topic-branch commit [`8f9db9a6a47f848cdff1d262276ba25a8ae9cbc8`](NVIDIA-NeMo/Switchyard@8f9db9a), which includes the Decision API series, StageRouter baseline/freshness behavior, and [Switchyard #46](NVIDIA-NeMo/Switchyard#46).

The Relay CLI exposes this through `--features switchyard`:

- Without that feature, `nemo-relay-cli` contains neither `nemo-relay-switchyard` nor `switchyard-translation`.
- With that feature, the plugin and its pinned Switchyard translation library are compiled and registered.
- Relay continues to own target bindings, credentials, dispatch, retries, fallbacks, lifecycle, and observability.
- Relay retains narrow fail-open policy checks for provider extensions that cannot be translated without data loss.

##### Runtime requirements

The current experimental deployment requires a separately running Switchyard service from the pinned topic commit. The plugin does not start or supervise that service.

Switchyard availability is not a Relay startup requirement. It is a request-time dependency: connection failures, timeouts, malformed decisions, selected-route drift, unsupported extensions, and missing required identity fail open to the configured same-protocol default. ATOF-backed profiles additionally require Relay's ATOF HTTP exporter to target Switchyard's `/v1/atof/events` endpoint with preserved field names and environment-referenced authentication.

The public examples document this service boundary, verify the exact Switchyard revision before startup, and intentionally remain manual compatibility workflows rather than production orchestration.

##### Optimization accounting and observability

This PR builds on the plugin-neutral accounting contract merged in [NeMo Relay #389](#389). Applied routing contributions carry the selected and baseline model transition, decision identity, backend and tier, attempt, rollout mode, reason, and router metadata. Observe-only decisions remain visible as non-applied evidence.

Relay core combines routing and other plugin contributions at LLM close while retaining explicit token evidence for downstream repricing. Existing requested, decision, retry, fallback, and error routing marks remain available alongside the canonical optimization contribution.

The generic ATIF change that attributes `step.model_name` to the effective response model is intentionally separated into [NeMo Relay #399](#399). This PR still ensures translated provider responses carry the actual selected model; #399 independently controls how ATIF presents it.

##### Testing summary

- `cargo fmt --all -- --check`
- `cargo clippy -p nemo-relay-switchyard --all-targets -- -D warnings`
- `cargo clippy -p nemo-relay-cli --features switchyard --all-targets -- -D warnings`
- `cargo check -p nemo-relay-cli` — proves the default CLI builds without Switchyard.
- `cargo check -p nemo-relay-cli --features switchyard` — proves the opt-in dependency path.
- `cargo test -p nemo-relay-switchyard` — 31 passed.
- `cargo test -p nemo-relay-cli gateway::tests` — 33 passed without the feature.
- `cargo test -p nemo-relay-cli --features switchyard gateway::tests` — 33 passed with the feature.
- `cargo test -p nemo-relay-cli --features switchyard --test switchyard_process_e2e` — buffered, streaming, retry, and fail-open process boundary passed after the split.
- `examples/switchyard/run-real-e2e.sh` against the exact pinned Switchyard service — passed with the expected StageRouter route sequence `provider/weak`, `provider/strong`, `provider/strong`.
- The pinned Switchyard translation crate separately passes 61 request, response, streaming, extension, and round-trip tests under Rust 1.93.

##### Previous feedback addressed

- Split the implementation at repository ownership boundaries.
- Replaced Relay's custom translation engine with the Switchyard-owned reusable library.
- Kept the dependency compile-time optional for users who do not enable the Relay-native Switchyard plugin.
- Removed in-process Random, LLM, and Cascade routers, Relay-side feature accumulation, duplicate Switchyard subscriptions, and the global fallback target.
- Replaced literal credentials with environment-referenced headers and activation-time validation.
- Hardened internal dispatch headers and structured upstream-failure metadata.
- Standardized routing terminology on StageRouter.
- Standardized model-routing evidence on Relay's shared optimization contract instead of adding a second post-call savings mark.
- Moved generic ATIF effective-model attribution into focused PR #399.
- Moved Fern documentation into focused experimental docs PR #400 so this functional PR remains code-focused.
- Kept public examples free of internal-only providers, credentials, Visor source references, and private infrastructure assumptions.

#### Where should the reviewer start?

1. `crates/switchyard/src/component.rs` — Decision API lifecycle, exact target and baseline validation, retries, fallback, and terminal route commitment.
2. `crates/switchyard/src/translation.rs` and `stream_translation.rs` — thin policy adapters over the Switchyard-owned translation library.
3. `crates/cli/Cargo.toml` and the `cfg(feature = "switchyard")` registration paths — compile-time optionality.
4. `crates/cli/src/gateway.rs` — reserved dispatch headers and structured retry-aware upstream failures.
5. `crates/cli/tests/switchyard_process_e2e.rs` — process-boundary buffered, streaming, and fail-open coverage.
6. `examples/switchyard/README.md` and `run-real-e2e.sh` — pinned experimental service workflow.

#### Related Issues: (use one of the action keywords Closes / Fixes / Resolves / Relates to)

- Relates to [NVIDIA/NeMo-Relay #389](#389).
- Relates to [NVIDIA/NeMo-Relay #399](#399).
- Relates to [NVIDIA/NeMo-Relay #400](#400).
- Relates to [NVIDIA-NeMo/Switchyard #46](NVIDIA-NeMo/Switchyard#46).
- Paired with the Switchyard Decision API topic series:
  - [NVIDIA-NeMo/Switchyard #8](NVIDIA-NeMo/Switchyard#8)
  - [NVIDIA-NeMo/Switchyard #9](NVIDIA-NeMo/Switchyard#9)
  - [NVIDIA-NeMo/Switchyard #11](NVIDIA-NeMo/Switchyard#11)
  - [NVIDIA-NeMo/Switchyard #35](NVIDIA-NeMo/Switchyard#35)

Authors:
  - Bryan Bednarski (https://github.com/bbednarski9)

Approvers:
  - Will Killian (https://github.com/willkill07)
  - Maryam Najafian (https://github.com/mnajafian-nv)
  - https://github.com/Salonijain27

URL: #369
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants