Skip to content

feat: Move libsy::algorithm::run to libsy-llm-client. - #324

Open
grahamking wants to merge 1 commit into
mainfrom
gk-run-to-libsy-llm-client
Open

feat: Move libsy::algorithm::run to libsy-llm-client.#324
grahamking wants to merge 1 commit into
mainfrom
gk-run-to-libsy-llm-client

Conversation

@grahamking

@grahamking grahamking commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

libsy previously had two entrypoints:

  • run_stream, the main one
  • run, which wrapped run_stream and used libsy-llm-client to make actual calls.

run is a convenience many callers will want. It relies entirely on libsy-llm-client.

This PR moves run into libsy-llm-client. That's it. There's now no default_client in libsy. It never makes any calls. The crate dependency runs libsy-llm-client -> libsy.

The key change in switchyard-server is this:

  • Before: algorithm.run_observed(..). algorithm is an instance of Algorithm trait.
  • Now: switchyard_llm_client::run(..). Notice the ::.

The bulk of the PR is moving code so it's large but a small delta. It's mostly mechanical.

Internally libsy now has a drive function that does most of what run did (iterating the stream), without the HTTP client calls. libsy_llm_client::run calls drive with a set of real HTTP clients (TranslatingLlmClient). It dispatches the CallLlm to the correct client. The leaves the machinery in libsy::drive, but the user-facing piece with a fully feature LLM client is in libsy-llm-client.

Some libsy integration tests relied on libsy-llm-client, so they now live in libsy-llm-client. Observation / metrics that relied on knowing host / port and so on also moved to libsy-llm-client (libsy doesn't know about these things).

To review start with crates/switchyard-server/src/lib.rs to see the point of the change.

Part of #310

Assisted-by: Claude:Opus 5 medium
Signed-off-by: Graham King grahamk@nvidia.com

@grahamking

Copy link
Copy Markdown
Contributor Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Libsy now delegates model-call serving to hosts through drive and run_stream. The LLM client crate provides ClientRouter, provider execution, and client-call observability. Server and Python integrations pass routing separately from algorithm targets.

Changes

Host-driven model routing

Layer / File(s) Summary
Host-side execution contract
crates/libsy/src/core/..., crates/libsy/src/error.rs, crates/libsy/src/observability.rs
Libsy removes target-owned clients and legacy run methods. It adds host-side drive, serving test utilities, explicit RoutedRequest calls, and public observability attribution.
Routed client execution and observability
crates/libsy-llm-client/src/..., crates/libsy-llm-client/Cargo.toml
The client crate adds ClientRouter and run. It routes model calls and records aggregate, streamed, error, and cancellation telemetry.
Server and Python routing integration
crates/switchyard-server/..., crates/switchyard-py/...
Routes and Python algorithms store client mappings separately from core targets. Execution passes the selected ClientRouter to the LLM client runtime.
Algorithm migration and validation
crates/libsy/src/algorithms/..., crates/libsy-llm-client/tests/observability.rs, crates/libsy/README.md, crates/libsy-llm-client/README.md
Algorithm tests and observability tests use explicit serving callbacks, shared drive helpers, and routed clients. Documentation describes the new host-driven flow.

Estimated code review effort: 5 (Critical) | ~120 minutes

Poem

A rabbit watched the call stream run,
With hosts now serving every one.
Routers chose the model’s door,
Spans recorded streams and more.
“Hop!” said the bunny, “the paths align!”

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the primary change: moving libsy::algorithm::run and its call-making behavior into libsy-llm-client.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (4)
crates/libsy-llm-client/Cargo.toml (1)

22-35: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Centralize the observability dependency versions in the workspace.

Add the OpenTelemetry and tracing declarations to root [workspace.dependencies], then inherit them in this crate and the other crates that use them. Keep crate-specific features local. tracing-opentelemetry 0.33 targets opentelemetry 0.32.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/libsy-llm-client/Cargo.toml` around lines 22 - 35, Move the versions
for opentelemetry, opentelemetry_sdk, and tracing-opentelemetry into the root
[workspace.dependencies], preserving the compatible opentelemetry
0.32/tracing-opentelemetry 0.33 pairing. Update this crate and every other crate
using these dependencies to inherit workspace versions, while keeping
crate-specific feature selections local.
crates/libsy-llm-client/src/run.rs (1)

91-119: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider distinguishing a router misconfiguration from a failed model call.

clients.route(&target) returns LlmClientError::Configuration when the router maps models and has no entry for the selected one. Line 116 wraps that in LibsyError::client_call, and Line 118 forwards it to the algorithm through respond. The algorithm then treats a host configuration gap the same as an upstream failure and may route around it. For a classifier judge that fails open, this hides the missing mapping instead of surfacing it.

This is a deliberate trade-off if fail-open is preferred. If not, return the routing error from serve so the run aborts with the configuration error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/libsy-llm-client/src/run.rs` around lines 91 - 119, Update serve so a
clients.route(&target) configuration error is returned directly instead of being
wrapped by LibsyError::client_call and passed through call.respond. Preserve the
existing client-call error wrapping and response flow for successfully routed
clients whose model call fails, while ensuring missing model mappings abort the
run with the routing configuration error.
crates/libsy/src/core/algorithm.rs (2)

313-369: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Confirm that dropping in-flight calls at ReturnToAgent is the intended contract, and document it.

The loop breaks as soon as Step::ReturnToAgent arrives. Every future still in in_flight is then dropped without completing. For first-wins hedging this is the desired behavior, and the tests cover it. The doc comment does not state it, so a host author cannot know that a serve future may be cancelled mid-call. Add one sentence to the doc so hosts do not put non-cancel-safe work (for example, a non-idempotent write or a required cleanup step) inside serve.

📝 Proposed doc addition
 /// `serve` owns the call: it performs it however the host likes and must fulfill the promise
 /// with [`CallLlmRequest::respond`]. A failed *model* call belongs in `respond` — the
 /// algorithm may route around it. Returning `Err` from `serve` aborts the whole run, so
 /// reserve it for infrastructure failures. Calls are served concurrently, so an algorithm
 /// that offloads several at once (hedging, fan-out) gets real parallelism.
+///
+/// A `serve` future that is still in flight when the algorithm returns its final response is
+/// dropped, so `serve` must be cancel-safe.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/libsy/src/core/algorithm.rs` around lines 313 - 369, Update the doc
comment for drive to state that when Step::ReturnToAgent is received, any still
in-flight serve futures are dropped and may be cancelled before completion. Warn
hosts not to place non-cancel-safe operations or required cleanup solely inside
serve, while preserving the existing first-wins behavior.

739-745: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the dead let _ = target; statement.

target is already consumed at Line 736 to build the decision. The let _ = target; line is leftover from the migration away from call_llm_target and adds no effect.

♻️ Proposed cleanup
             driver.info(ctx.clone(), decision.clone()).await?;
-            let _ = target;
             driver
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/libsy/src/core/algorithm.rs` around lines 739 - 745, Remove the
redundant `let _ = target;` statement before the `driver.call_llm` invocation;
`target` is already consumed when constructing `decision`, so leave the existing
`RoutedRequest` call unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/libsy-llm-client/README.md`:
- Around line 146-165: Add the switchyard_protocol::Decision import to the
README Rust example so the selected_model() call on trace decisions resolves
through the trait. Keep the existing route function and imports unchanged apart
from bringing Decision into scope.

In `@crates/libsy-llm-client/src/observability.rs`:
- Around line 275-309: Update ClientStreamObserver::observe_chunk so handling
LlmResponseChunk::MessageStop records the normal terminal outcome by setting
self.terminal before or alongside recording finish reasons. Preserve the
existing complete() behavior and cancellation handling for streams that end
without MessageStop.

In `@crates/libsy/src/algorithms/llm_class.rs`:
- Around line 1800-1834: Update the test helper Queue::take so an exhausted
queue panics instead of returning "unexpected call", ensuring unexpected model
or judge requests fail the test immediately. Preserve the existing queued
response behavior for available entries and apply the same fix to the
corresponding helper usage around the second referenced section.

In `@crates/libsy/src/algorithms/noop.rs`:
- Around line 97-100: Update the test around the Noop algorithm and drive call
to pass a Serve closure that panics when invoked, replacing echo(). Keep the
test asserting Noop’s synthesized response while ensuring any unexpected
offloaded LLM call fails immediately.

In `@crates/switchyard-server/src/config.rs`:
- Around line 178-198: The router construction paths must reject duplicate keys
instead of allowing ClientRouter to silently overwrite clients: in
crates/switchyard-server/src/config.rs lines 178-198, update build_client_router
to detect repeated target.id values mapped to different llm_client sections and
return an error, and align the startup warning in build at lines 69-75 with this
routing model; in crates/switchyard-py/src/libsy_bindings.rs lines 315-327,
reject duplicate target names in target_cores before collecting them into the
router. Also make the shared ClientRouter contract explicit by adding a fallible
try_new duplicate check or documenting new’s last-write-wins behavior.

---

Nitpick comments:
In `@crates/libsy-llm-client/Cargo.toml`:
- Around line 22-35: Move the versions for opentelemetry, opentelemetry_sdk, and
tracing-opentelemetry into the root [workspace.dependencies], preserving the
compatible opentelemetry 0.32/tracing-opentelemetry 0.33 pairing. Update this
crate and every other crate using these dependencies to inherit workspace
versions, while keeping crate-specific feature selections local.

In `@crates/libsy-llm-client/src/run.rs`:
- Around line 91-119: Update serve so a clients.route(&target) configuration
error is returned directly instead of being wrapped by LibsyError::client_call
and passed through call.respond. Preserve the existing client-call error
wrapping and response flow for successfully routed clients whose model call
fails, while ensuring missing model mappings abort the run with the routing
configuration error.

In `@crates/libsy/src/core/algorithm.rs`:
- Around line 313-369: Update the doc comment for drive to state that when
Step::ReturnToAgent is received, any still in-flight serve futures are dropped
and may be cancelled before completion. Warn hosts not to place non-cancel-safe
operations or required cleanup solely inside serve, while preserving the
existing first-wins behavior.
- Around line 739-745: Remove the redundant `let _ = target;` statement before
the `driver.call_llm` invocation; `target` is already consumed when constructing
`decision`, so leave the existing `RoutedRequest` call unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 899ae0b3-08fb-49c3-b352-5e1d7e4517ae

📥 Commits

Reviewing files that changed from the base of the PR and between 31e0afb and 892f6a5.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock, !Cargo.lock
📒 Files selected for processing (27)
  • crates/libsy-llm-client/Cargo.toml
  • crates/libsy-llm-client/README.md
  • crates/libsy-llm-client/src/lib.rs
  • crates/libsy-llm-client/src/observability.rs
  • crates/libsy-llm-client/src/run.rs
  • crates/libsy-llm-client/tests/observability.rs
  • crates/libsy/Cargo.toml
  • crates/libsy/README.md
  • crates/libsy/src/algorithms/fall_through.rs
  • crates/libsy/src/algorithms/llm_class.rs
  • crates/libsy/src/algorithms/noop.rs
  • crates/libsy/src/algorithms/passthrough.rs
  • crates/libsy/src/algorithms/rand.rs
  • crates/libsy/src/algorithms/stage.rs
  • crates/libsy/src/algorithms/subagent_affinity_tests.rs
  • crates/libsy/src/algorithms/util/llm_judge.rs
  • crates/libsy/src/core.rs
  • crates/libsy/src/core/algorithm.rs
  • crates/libsy/src/core/testing.rs
  • crates/libsy/src/error.rs
  • crates/libsy/src/lib.rs
  • crates/libsy/src/observability.rs
  • crates/switchyard-py/Cargo.toml
  • crates/switchyard-py/src/libsy_bindings.rs
  • crates/switchyard-server/src/config.rs
  • crates/switchyard-server/src/lib.rs
  • crates/switchyard-server/tests/server.rs
💤 Files with no reviewable changes (2)
  • crates/libsy/Cargo.toml
  • crates/libsy/src/error.rs

Comment thread crates/libsy-llm-client/README.md
Comment thread crates/libsy-llm-client/src/observability.rs
Comment thread crates/libsy/src/algorithms/llm_class.rs
Comment thread crates/libsy/src/algorithms/noop.rs
Comment thread crates/switchyard-server/src/config.rs
@grahamking
grahamking force-pushed the gk-run-to-libsy-llm-client branch from f967193 to b2c3631 Compare August 6, 2026 20:47
@grahamking
grahamking marked this pull request as ready for review August 6, 2026 20:51
@grahamking
grahamking requested a review from a team as a code owner August 6, 2026 20:51
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1

🚀 View preview at
https://NVIDIA-NeMo.github.io/Switchyard/pr-preview/pr-324/

Built to branch gh-pages at 2026-08-07 13:56 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@grahamking
grahamking force-pushed the gk-run-to-libsy-llm-client branch from b769a42 to c74d5e9 Compare August 7, 2026 13:05
`libsy` previously had two entrypoints:
- `run_stream`, the main one
- `run`, which wrapped `run_stream` and used `libsy-llm-client` to make actual calls.

`run` is a convenience many callers will want. It relies entirely on `libsy-llm-client`.

This PR moves `run` into `libsy-llm-client`. That's it. There's now no `default_client` in `libsy`. It never makes any calls. The crate dependency runs `libsy-llm-client` -> `libsy`.

The key change in `switchyard-server` is this:
- Before: `algorithm.run_observed(..)`. `algorithm` is an instance of `Algorithm` trait.
- Now: `switchyard_llm_client::run(..)`. Notice the `::`.

The bulk of the PR is moving code so it's large but a small delta. It's mostly mechanical.

Internally `libsy` now has a `drive` function that does most of what `run` did (iterating the stream), without the HTTP client calls. `libsy_llm_client::run` calls `drive` with a set of real HTTP clients (`TranslatingLlmClient`). It dispatches the `CallLlm` to the correct client. The leaves the machinery in `libsy::drive`, but the user-facing piece with a fully feature LLM client is in `libsy-llm-client`.

Some `libsy` integration tests relied on `libsy-llm-client`, so they now live in `libsy-llm-client`. Observation / metrics that relied on knowing host / port and so on also moved to `libsy-llm-client` (`libsy` doesn't know about these things).

To review start with `crates/switchyard-server/src/lib.rs` to see the point of the change.

Part of #310

Assisted-by: Claude:Opus 5 medium
Signed-off-by: Graham King <grahamk@nvidia.com>
@grahamking
grahamking force-pushed the gk-run-to-libsy-llm-client branch from faae198 to a88c8d3 Compare August 7, 2026 13:55
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.

1 participant