Skip to content

refactor(python): remove deprecated server stack - #343

Merged
ayushag-nv merged 1 commit into
mainfrom
nachiketb/refactor-remove-python-server
Aug 11, 2026
Merged

refactor(python): remove deprecated server stack#343
ayushag-nv merged 1 commit into
mainfrom
nachiketb/refactor-remove-python-server

Conversation

@nachiketb-nvidia

@nachiketb-nvidia nachiketb-nvidia commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

What

  • remove the deprecated Python switchyard serve command, YAML route bundles, FastAPI server, legacy chain, processors, backends, and server-only tests
  • remove the switchyard-components crate and its compatibility PyO3 bindings
  • retain the Python coding-agent launchers and the native libsy and switchyard-server bindings
  • move launcher-owned terminal and cost helpers under the launcher package
  • update CI, performance smoke coverage, packaging metadata, and docs to use the native TOML server path

This draft is the review and migration reference for the removed Python path. The implementation remains available in Git history if it needs to be consulted later.

Why

The native Rust server and libsy path now own serving, routing, translation, and observability. Keeping a second Python server duplicated behavior, dependencies, tests, and maintenance while exposing a deprecated interface.

How

  • make switchyard launch the only Python CLI command
  • reduce switchyard-py to the native server and libsy bindings
  • remove Python server dependencies and optional extras from the published package
  • build switchyard-server and use a native TOML deployment in the performance workflow
  • remove tests and documentation whose only subject was the deleted compatibility stack

What to review

  • the deletion boundary: launchers, switchyard.libsy, and switchyard_rust.server remain supported
  • the reduced PyO3 module and dependency graph
  • the performance workflow migration from Python YAML serving to native TOML serving
  • packaging and documentation changes for users of the removed APIs

Validation

  • cargo fmt --all --check
  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo test --workspace
  • uv run maturin develop
  • uv run ruff check .
  • uv run mypy switchyard switchyard_rust
  • uv run pytest tests/ -v -m "not integration" (134 passed, 2 deselected)
  • make publish (strict documentation build)
  • verified switchyard --help exposes only launch

Summary by CodeRabbit

  • Breaking Changes

    • Removed the legacy switchyard serve command and Python route-bundle server.
    • Standalone deployments now use the native server with TOML configuration.
    • The Python package exposes a smaller public API focused on CLI launchers and bindings.
  • New Features

    • Added an improved terminal launcher with live footer rendering, input forwarding, and reliable terminal cleanup.
  • Documentation

    • Updated installation, setup, server, metrics, and troubleshooting guidance for native server deployments.
    • Clarified CLI-only installation and current server commands.

Signed-off-by: nachiketb <nachiketb@nvidia.com>
@github-actions

Copy link
Copy Markdown
PR Preview Action v1.8.1

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

Built to branch gh-pages at 2026-08-10 22:31 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

Comment thread .agents/skills/switchyard-stage-router-scorer/SKILL.md

@sabhatinas sabhatinas left a comment

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.

Had one comment about stage router skill, rest looks good to me!

@ayushag-nv
ayushag-nv marked this pull request as ready for review August 11, 2026 17:27
@ayushag-nv
ayushag-nv requested a review from a team as a code owner August 11, 2026 17:27
@ayushag-nv
ayushag-nv enabled auto-merge (squash) August 11, 2026 17:28
@ayushag-nv
ayushag-nv merged commit b104765 into main Aug 11, 2026
20 of 21 checks passed
@ayushag-nv
ayushag-nv deleted the nachiketb/refactor-remove-python-server branch August 11, 2026 17:30
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 8b113f39-f5b8-46c9-9c29-1081242baa7a

📥 Commits

Reviewing files that changed from the base of the PR and between fb3fc30 and 5e6be08.

⛔ Files ignored due to path filters (2)
  • Cargo.lock is excluded by !**/*.lock, !Cargo.lock
  • uv.lock is excluded by !**/*.lock, !uv.lock
📒 Files selected for processing (239)
  • .agents/skills/switchyard-stage-router-scorer/SKILL.md
  • .github/ISSUE_TEMPLATE/bug_report.md
  • .github/workflows/ci.yml
  • .github/workflows/perf.yml
  • AGENTS.md
  • CHANGELOG.md
  • Cargo.toml
  • INSTALLATION.md
  • README.md
  • benchmark/README.md
  • benchmark/score_staged_run.py
  • crates/libsy-llm-client/src/client.rs
  • crates/libsy-llm-client/src/error.rs
  • crates/switchyard-components/Cargo.toml
  • crates/switchyard-components/src/backends/anthropic.rs
  • crates/switchyard-components/src/backends/common.rs
  • crates/switchyard-components/src/backends/context_overflow.rs
  • crates/switchyard-components/src/backends/mod.rs
  • crates/switchyard-components/src/backends/multi.rs
  • crates/switchyard-components/src/backends/openai.rs
  • crates/switchyard-components/src/backends/selection.rs
  • crates/switchyard-components/src/backends/stats.rs
  • crates/switchyard-components/src/contracts/backend.rs
  • crates/switchyard-components/src/contracts/context.rs
  • crates/switchyard-components/src/contracts/error.rs
  • crates/switchyard-components/src/contracts/ids.rs
  • crates/switchyard-components/src/contracts/mod.rs
  • crates/switchyard-components/src/contracts/roles.rs
  • crates/switchyard-components/src/contracts/types.rs
  • crates/switchyard-components/src/dimension_collector/mod.rs
  • crates/switchyard-components/src/dimension_collector/response/checks.rs
  • crates/switchyard-components/src/dimension_collector/response/mod.rs
  • crates/switchyard-components/src/dimension_collector/tool_signals.rs
  • crates/switchyard-components/src/lib.rs
  • crates/switchyard-components/src/request_processors/dimension_collector.rs
  • crates/switchyard-components/src/request_processors/mod.rs
  • crates/switchyard-components/src/request_processors/random_routing.rs
  • crates/switchyard-components/src/request_processors/stats.rs
  • crates/switchyard-components/src/response_processors/mod.rs
  • crates/switchyard-components/src/response_processors/response_signals.rs
  • crates/switchyard-components/src/response_processors/stats.rs
  • crates/switchyard-components/src/stage_router.rs
  • crates/switchyard-components/src/stats/accumulator.rs
  • crates/switchyard-components/src/stats/cache_eligibility.rs
  • crates/switchyard-components/src/stats/context.rs
  • crates/switchyard-components/src/stats/cost.rs
  • crates/switchyard-components/src/stats/mod.rs
  • crates/switchyard-components/src/stats/usage.rs
  • crates/switchyard-components/src/telemetry.rs
  • crates/switchyard-components/tests/adversarial_multi_llm_backend.rs
  • crates/switchyard-components/tests/adversarial_native_backends.rs
  • crates/switchyard-components/tests/adversarial_random_routing.rs
  • crates/switchyard-components/tests/contracts.rs
  • crates/switchyard-components/tests/stats_accumulator.rs
  • crates/switchyard-components/tests/stats_processors.rs
  • crates/switchyard-components/tests/stats_usage_shapes.rs
  • crates/switchyard-components/tests/support/config.rs
  • crates/switchyard-components/tests/support/mod.rs
  • crates/switchyard-py/Cargo.toml
  • crates/switchyard-py/src/component_bindings.rs
  • crates/switchyard-py/src/component_bindings/backends.rs
  • crates/switchyard-py/src/component_bindings/config.rs
  • crates/switchyard-py/src/component_bindings/dimension_collector.rs
  • crates/switchyard-py/src/component_bindings/request_processors.rs
  • crates/switchyard-py/src/component_bindings/response_processors.rs
  • crates/switchyard-py/src/component_bindings/stage_router.rs
  • crates/switchyard-py/src/component_bindings/stats.rs
  • crates/switchyard-py/src/errors.rs
  • crates/switchyard-py/src/interop.rs
  • crates/switchyard-py/src/interop/context.rs
  • crates/switchyard-py/src/interop/request.rs
  • crates/switchyard-py/src/interop/response.rs
  • crates/switchyard-py/src/interop/roles.rs
  • crates/switchyard-py/src/interop/subagent.rs
  • crates/switchyard-py/src/lib.rs
  • crates/switchyard-py/src/libsy_bindings.rs
  • crates/switchyard-py/src/py_serde.rs
  • crates/switchyard-py/src/translation.rs
  • crates/switchyard-server/README.md
  • docs/cli_reference.md
  • docs/getting_started.md
  • docs/internal/metrics_reference.md
  • docs/routing_algorithms/stage_router_routing.md
  • examples/minimal.py
  • examples/route.yaml
  • examples/utils.py
  • pyproject.toml
  • switchyard/__init__.py
  • switchyard/cli/command_utils.py
  • switchyard/cli/launchers/claude_code_launcher.py
  • switchyard/cli/launchers/codex_cli_launcher.py
  • switchyard/cli/launchers/cost_estimator.py
  • switchyard/cli/launchers/launcher_runtime.py
  • switchyard/cli/launchers/live_stats_footer.py
  • switchyard/cli/launchers/openclaw_launcher.py
  • switchyard/cli/launchers/session_summary.py
  • switchyard/cli/launchers/shell_tui.py
  • switchyard/cli/model_catalog/__init__.py
  • switchyard/cli/model_catalog/model_discovery.py
  • switchyard/cli/route_bundle.py
  • switchyard/cli/switchyard_cli.py
  • switchyard/lib/__init__.py
  • switchyard/lib/backends/__init__.py
  • switchyard/lib/backends/anthropic_native_llm_backend.py
  • switchyard/lib/backends/backend_format_resolver.py
  • switchyard/lib/backends/llm_target.py
  • switchyard/lib/backends/multi_llm_backend.py
  • switchyard/lib/backends/openai_llm_backend.py
  • switchyard/lib/backends/openai_native_backend.py
  • switchyard/lib/backends/stats_llm_backend.py
  • switchyard/lib/chat_request/__init__.py
  • switchyard/lib/chat_request/anthropic.py
  • switchyard/lib/chat_request/base.py
  • switchyard/lib/chat_request/openai_chat.py
  • switchyard/lib/chat_request/openai_responses.py
  • switchyard/lib/chat_response/__init__.py
  • switchyard/lib/chat_response/anthropic.py
  • switchyard/lib/chat_response/base.py
  • switchyard/lib/chat_response/openai_chat.py
  • switchyard/lib/chat_response/openai_responses.py
  • switchyard/lib/chat_response/streaming_response_accumulator.py
  • switchyard/lib/conversation_turn.py
  • switchyard/lib/endpoints/__init__.py
  • switchyard/lib/endpoints/anthropic_messages_endpoint.py
  • switchyard/lib/endpoints/base.py
  • switchyard/lib/endpoints/dispatch.py
  • switchyard/lib/endpoints/error_envelope.py
  • switchyard/lib/endpoints/models_endpoint.py
  • switchyard/lib/endpoints/openai_chat_endpoint.py
  • switchyard/lib/endpoints/outcome_metrics.py
  • switchyard/lib/endpoints/prometheus_emitter.py
  • switchyard/lib/endpoints/responses_endpoint.py
  • switchyard/lib/endpoints/route_selection.py
  • switchyard/lib/endpoints/routing_log_stats_endpoint.py
  • switchyard/lib/endpoints/sse_helpers.py
  • switchyard/lib/endpoints/stats_endpoint.py
  • switchyard/lib/endpoints/upstream_error.py
  • switchyard/lib/endpoints/upstream_error_log.py
  • switchyard/lib/llm_client.py
  • switchyard/lib/model_listing.py
  • switchyard/lib/processors/__init__.py
  • switchyard/lib/processors/format_translate.py
  • switchyard/lib/processors/model_rewrite_request_processor.py
  • switchyard/lib/processors/rl_logging_request_processor.py
  • switchyard/lib/processors/rl_logging_response_processor.py
  • switchyard/lib/processors/routing_log_response_processor.py
  • switchyard/lib/processors/stats_request_processor.py
  • switchyard/lib/processors/stats_response_processor_accumulator.py
  • switchyard/lib/prometheus_exposition.py
  • switchyard/lib/proxy_context.py
  • switchyard/lib/request_metadata.py
  • switchyard/lib/roles.py
  • switchyard/lib/route_table.py
  • switchyard/lib/startup_timing.py
  • switchyard/lib/stats_accumulator.py
  • switchyard/lib/switchyard.py
  • switchyard/lib/tracing.py
  • switchyard/server/__init__.py
  • switchyard/server/server_util.py
  • switchyard/server/switchyard_app.py
  • switchyard/telemetry.py
  • switchyard_rust/__init__.py
  • switchyard_rust/_native.py
  • switchyard_rust/components.py
  • switchyard_rust/components.pyi
  • switchyard_rust/core.py
  • switchyard_rust/libsy.py
  • switchyard_rust/server.py
  • switchyard_rust/translation.py
  • tests/_chain_test_helpers.py
  • tests/conftest.py
  • tests/contract/__init__.py
  • tests/contract/test_platform_imports.py
  • tests/contract/test_proxy_context.py
  • tests/contract/test_request_response_types.py
  • tests/e2e/_helpers.py
  • tests/e2e/conftest.py
  • tests/e2e/test_passthrough_e2e.py
  • tests/e2e/test_passthrough_responses_e2e.py
  • tests/e2e_multiturn_responses.py
  • tests/getting_started/test_getting_started.py
  • tests/readme/test_readme.py
  • tests/test_anthropic_native_llm_backend.py
  • tests/test_anthropic_openai_translation.py
  • tests/test_anthropic_output_config_strip.py
  • tests/test_anthropic_probe.py
  • tests/test_backend_format_resolver.py
  • tests/test_build_and_serve.py
  • tests/test_chat_request.py
  • tests/test_chat_response.py
  • tests/test_cli_reference_docs.py
  • tests/test_codex_multiturn_traces.py
  • tests/test_context_error_translation.py
  • tests/test_context_window_exceeded_endpoint.py
  • tests/test_cost_estimator_gemini.py
  • tests/test_endpoint_state_contract.py
  • tests/test_error_source_annotation.py
  • tests/test_format_translate_processor.py
  • tests/test_inference_e2e.py
  • tests/test_infra.py
  • tests/test_init_all_exports.py
  • tests/test_launchers.py
  • tests/test_live_stats_footer.py
  • tests/test_llm_client.py
  • tests/test_metrics_endpoint.py
  • tests/test_no_stale_module_paths.py
  • tests/test_outcome_metrics.py
  • tests/test_prometheus_emitter.py
  • tests/test_prometheus_exposition.py
  • tests/test_python_server_passthrough.py
  • tests/test_request_metadata.py
  • tests/test_request_translation_engine.py
  • tests/test_request_translation_engine_to_any_of.py
  • tests/test_response_translation_engine.py
  • tests/test_responses_openai_translation.py
  • tests/test_rl_logging.py
  • tests/test_rl_logging_e2e.py
  • tests/test_route_bundle.py
  • tests/test_route_selection_headers.py
  • tests/test_route_table.py
  • tests/test_routing_log_response_processor.py
  • tests/test_shell_tui.py
  • tests/test_sse_stream_close.py
  • tests/test_stats_accumulator.py
  • tests/test_stream_close_chain.py
  • tests/test_stream_leak_repro.py
  • tests/test_switchyard.py
  • tests/test_switchyard_app_factory.py
  • tests/test_switchyard_app_lifecycle.py
  • tests/test_switchyard_rust_component_bindings.py
  • tests/test_switchyard_rust_core_bindings.py
  • tests/test_telemetry.py
  • tests/test_tool_result_signal_collector.py
  • tests/test_tracing.py
  • tests/test_translation_engine_chaos.py
  • tests/test_upstream_error_log.py
  • tests/test_upstream_error_passthrough.py
  • tests/translation/__init__.py
  • tests/translation/test_format_fidelity_contract.py
💤 Files with no reviewable changes (106)
  • Cargo.toml
  • switchyard/lib/backends/openai_llm_backend.py
  • crates/switchyard-components/src/response_processors/mod.rs
  • switchyard/lib/init.py
  • .agents/skills/switchyard-stage-router-scorer/SKILL.md
  • switchyard/lib/chat_request/base.py
  • switchyard/lib/chat_request/openai_responses.py
  • switchyard/lib/chat_request/anthropic.py
  • crates/switchyard-components/src/dimension_collector/mod.rs
  • switchyard/lib/backends/anthropic_native_llm_backend.py
  • switchyard/lib/chat_response/openai_chat.py
  • switchyard/lib/endpoints/routing_log_stats_endpoint.py
  • crates/switchyard-components/src/contracts/roles.rs
  • switchyard/lib/chat_response/openai_responses.py
  • switchyard/lib/chat_response/anthropic.py
  • switchyard/cli/model_catalog/model_discovery.py
  • switchyard/lib/backends/openai_native_backend.py
  • benchmark/README.md
  • switchyard/lib/backends/multi_llm_backend.py
  • crates/switchyard-components/src/request_processors/dimension_collector.rs
  • switchyard/lib/backends/stats_llm_backend.py
  • crates/switchyard-py/src/component_bindings/request_processors.rs
  • crates/switchyard-py/src/interop/roles.rs
  • crates/switchyard-components/src/contracts/context.rs
  • crates/switchyard-py/src/interop.rs
  • crates/switchyard-components/src/backends/context_overflow.rs
  • crates/switchyard-components/Cargo.toml
  • crates/switchyard-py/src/component_bindings.rs
  • examples/route.yaml
  • crates/switchyard-components/src/stats/usage.rs
  • crates/switchyard-components/src/backends/stats.rs
  • crates/switchyard-py/src/component_bindings/stage_router.rs
  • crates/switchyard-py/Cargo.toml
  • crates/switchyard-py/src/component_bindings/config.rs
  • switchyard/lib/chat_request/openai_chat.py
  • crates/switchyard-components/tests/adversarial_native_backends.rs
  • crates/switchyard-py/src/interop/request.rs
  • crates/switchyard-py/src/lib.rs
  • crates/switchyard-components/src/contracts/backend.rs
  • switchyard/lib/endpoints/dispatch.py
  • crates/switchyard-components/src/backends/multi.rs
  • switchyard/cli/model_catalog/init.py
  • switchyard/lib/endpoints/responses_endpoint.py
  • switchyard/lib/chat_response/base.py
  • switchyard/lib/endpoints/error_envelope.py
  • crates/switchyard-components/tests/contracts.rs
  • crates/switchyard-components/src/response_processors/stats.rs
  • crates/switchyard-components/tests/adversarial_random_routing.rs
  • crates/switchyard-py/src/interop/subagent.rs
  • crates/switchyard-py/src/interop/context.rs
  • crates/switchyard-components/src/contracts/error.rs
  • crates/switchyard-components/src/dimension_collector/tool_signals.rs
  • crates/switchyard-py/src/component_bindings/dimension_collector.rs
  • crates/switchyard-components/src/contracts/types.rs
  • switchyard/lib/conversation_turn.py
  • crates/switchyard-components/src/dimension_collector/response/mod.rs
  • switchyard/lib/endpoints/models_endpoint.py
  • switchyard/lib/endpoints/route_selection.py
  • crates/switchyard-components/src/stats/mod.rs
  • switchyard/lib/endpoints/prometheus_emitter.py
  • crates/switchyard-components/src/backends/mod.rs
  • crates/switchyard-components/tests/support/config.rs
  • crates/switchyard-components/src/lib.rs
  • switchyard/cli/route_bundle.py
  • crates/switchyard-components/src/backends/selection.rs
  • switchyard/lib/endpoints/base.py
  • crates/switchyard-components/src/stats/accumulator.rs
  • examples/utils.py
  • crates/switchyard-components/src/response_processors/response_signals.rs
  • crates/switchyard-components/src/request_processors/random_routing.rs
  • switchyard/lib/chat_request/init.py
  • crates/switchyard-components/src/stats/cache_eligibility.rs
  • switchyard/lib/endpoints/outcome_metrics.py
  • crates/switchyard-py/src/translation.rs
  • switchyard/lib/chat_response/init.py
  • crates/switchyard-components/src/request_processors/stats.rs
  • crates/switchyard-components/tests/adversarial_multi_llm_backend.rs
  • benchmark/score_staged_run.py
  • switchyard/lib/endpoints/anthropic_messages_endpoint.py
  • switchyard/lib/endpoints/openai_chat_endpoint.py
  • switchyard/lib/backends/backend_format_resolver.py
  • crates/switchyard-py/src/component_bindings/response_processors.rs
  • crates/switchyard-components/src/backends/anthropic.rs
  • switchyard/lib/endpoints/init.py
  • crates/switchyard-components/src/contracts/mod.rs
  • crates/switchyard-py/src/component_bindings/stats.rs
  • crates/switchyard-components/src/telemetry.rs
  • crates/switchyard-components/tests/stats_accumulator.rs
  • crates/switchyard-py/src/interop/response.rs
  • switchyard/lib/backends/init.py
  • crates/switchyard-components/src/stats/context.rs
  • crates/switchyard-py/src/component_bindings/backends.rs
  • crates/switchyard-components/src/contracts/ids.rs
  • crates/switchyard-components/src/stage_router.rs
  • switchyard/lib/chat_response/streaming_response_accumulator.py
  • crates/switchyard-components/src/backends/common.rs
  • examples/minimal.py
  • crates/switchyard-components/tests/support/mod.rs
  • crates/switchyard-components/src/backends/openai.rs
  • crates/switchyard-py/src/py_serde.rs
  • crates/switchyard-components/src/dimension_collector/response/checks.rs
  • switchyard/lib/backends/llm_target.py
  • crates/switchyard-components/src/stats/cost.rs
  • crates/switchyard-components/tests/stats_processors.rs
  • crates/switchyard-components/src/request_processors/mod.rs
  • crates/switchyard-components/tests/stats_usage_shapes.rs

Walkthrough

The pull request removes the deprecated Python server and component stack, simplifies Python bindings, adds a PTY-based launcher terminal, switches workflows to switchyard-server, and updates packaging, documentation, and tests for the native Rust serving path.

Changes

Native serving transition

Layer / File(s) Summary
Native loading and binding boundary
switchyard_rust/*, crates/switchyard-py/*, crates/libsy-llm-client/*, switchyard/__init__.py
Native extension loading uses a shared loader. Legacy component, translation, and typed error bindings are removed.
Legacy server and package removal
switchyard/cli/*, pyproject.toml
The serve command, route-bundle integration, server extras, and obsolete logger configuration are removed.
Launcher terminal runtime
switchyard/cli/launchers/*, tests/test_shell_tui.py, tests/test_live_stats_footer.py
ShellTUI provides PTY execution, terminal resizing, output forwarding, footer rendering, and exit-status handling. Launcher imports and footer tests use the new runtime.
Native server documentation and operations
INSTALLATION.md, AGENTS.md, .github/workflows/*, docs/*, README.md
Installation, development, performance, issue-template, and server documentation now use CLI-only installation and native TOML server commands.
Validation and stale-path checks
tests/*, .github/workflows/ci.yml
Tests and CI validate removal of legacy commands and paths, updated package extras, and revised launcher behavior.

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

Poem

A rabbit hops through Rust-lit lanes,
While Python’s old server fades.
PTYs hum and footers gleam,
TOML guides the native stream.
Tests chase stale paths from the burrow,
Clean launch commands leave no sorrow.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 39.62% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary change: removing the deprecated Python server stack.
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.

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: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/workflows/ci.yml (1)

216-227: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Validate the top-level command allowlist.

The current checks reject selected removed commands but do not prove that switchyard --help exposes only launch. An additional stale top-level command can pass both checks.

  • .github/workflows/ci.yml#L216-L227: run switchyard --help and assert that launch is the only available subcommand.
  • tests/test_cli_reference_docs.py#L50-L56: assert that _subparsers(_build_parser()) has exactly {"launch"}.
🤖 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 @.github/workflows/ci.yml around lines 216 - 227, The CLI validation must
prove that the top-level command allowlist contains only launch. In
.github/workflows/ci.yml lines 216-227, add a switchyard --help check that
confirms launch is the sole available subcommand; in
tests/test_cli_reference_docs.py lines 50-56, assert that
_subparsers(_build_parser()) equals {"launch"}.
🧹 Nitpick comments (1)
crates/switchyard-py/src/libsy_bindings.rs (1)

31-39: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider naming the failing header in the error message.

The http crate errors are generic, for example "invalid HTTP header name". A caller that passes many headers cannot tell which entry failed. Include the header name in the message. Do not include the header value, because header values often carry API keys.

try_append is safe here. The input is a HashMap, so keys are unique, and try_append fails only when the map exceeds its maximum size.

♻️ Proposed error-context improvement
     for (name, value) in headers {
-        let name = HeaderName::from_bytes(name.as_bytes())
-            .map_err(|error| PyValueError::new_err(error.to_string()))?;
-        let value = HeaderValue::from_str(value)
-            .map_err(|error| PyValueError::new_err(error.to_string()))?;
+        let header_name = HeaderName::from_bytes(name.as_bytes())
+            .map_err(|error| PyValueError::new_err(format!("header {name}: {error}")))?;
+        // Never include the header value in the error: values can carry credentials.
+        let value = HeaderValue::from_str(value)
+            .map_err(|error| PyValueError::new_err(format!("header {name}: {error}")))?;
         result
-            .try_append(name, value)
-            .map_err(|error| PyValueError::new_err(error.to_string()))?;
+            .try_append(header_name, value)
+            .map_err(|error| PyValueError::new_err(format!("header {name}: {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/switchyard-py/src/libsy_bindings.rs` around lines 31 - 39, Update the
header parsing errors in the loop over headers to include the failing header
name alongside the underlying error message, for both HeaderName::from_bytes and
HeaderValue::from_str failures. Do not include the header value, and preserve
the existing try_append handling.
🤖 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 `@AGENTS.md`:
- Line 142: Update the architecture and project-structure fenced blocks in
AGENTS.md to use the text language identifier on both opening fences, resolving
the markdownlint MD040 violations while leaving their contents unchanged.

In `@crates/switchyard-py/src/errors.rs`:
- Around line 10-19: Add a deprecation and migration record for the removed PyO3
exports, covering SwitchyardRuntimeError and the typed Switchyard*Error classes,
before finalizing the new LibsyError-only API in register and py_libsy_error.
Document the replacement behavior and migration path using the repository’s
established deprecation-plan location and format.

In `@crates/switchyard-server/README.md`:
- Around line 168-169: Update the README latency definition around
switchyard_total_latency_ms to state that timing ends after the wrapped stream
completes, not when the stream handle arrives. Keep the response-completion
wording consistent with stream completion and replace “connection accept” with
“connection acceptance.”

In `@docs/routing_algorithms/stage_router_routing.md`:
- Around line 154-157: Replace the universal lowest-threshold guidance in the
benchmark calibration section with picker-specific rules: for capable_first,
lower confidence_threshold only enough to rescue RESCUE without over-escalating
harmful LOSS downgrades; for efficient_first, calibrate it to increase
escalation for beneficial LOSS cases while avoiding unnecessary RESCUE changes.
Keep the corroborative scorer context and require selecting the best threshold
independently for each picker mode.

In `@switchyard_rust/__init__.py`:
- Line 4: Before removing the package-root exports from switchyard_rust, add an
explicit deprecation plan for the 34 previously public names. Preserve the
current exports during the deprecation period and document the planned removal
timeline and migration path for callers.

In `@switchyard/__init__.py`:
- Around line 4-14: Document this as a breaking release in CHANGELOG.md,
including symbol-level migration guidance for the removed package-level exports,
quiet_dependency_loggers, and SwitchyardRuntimeError base. Cover
switchyard/__init__.py, switchyard/cli/command_utils.py, and
switchyard_rust/libsy.py; no direct code changes are required at these sites
unless you choose to retain compatibility shims through a documented removal
release.

In `@switchyard/cli/launchers/launcher_runtime.py`:
- Around line 88-91: Update configure_debug_file_logging to attach a
logging.NullHandler to the root logger immediately after clearing its handlers,
preventing logging.lastResort from writing dependency warnings to stderr. Revise
silence_launch_loggers’ docstring to accurately describe the terminal-logging
behavior and new scope.

In `@switchyard/cli/launchers/shell_tui.py`:
- Around line 172-175: Snapshot a bounded footer height once per paint in the
shell TUI layout flow, and use that same value for footer rendering, shell row
calculation, scroll-region setup, and child PTY sizing. When the snapshot
differs from the previous layout, update TIOCSWINSZ and the scroll region
immediately rather than waiting for an outer-terminal resize. Add a regression
test that changes footer height between paints and verifies the child PTY and
rendered layout stay synchronized.
- Around line 388-392: Update the stdin forwarding loop around os.read and
master_fd so partial writes and BlockingIOError from os.write are handled
without losing bytes. Maintain a pending-output buffer, append newly read stdin
data, and monitor master_fd for writability until all buffered bytes are
written, while preserving the existing EOF break behavior.
- Around line 316-419: Refactor ShellTUI.run and its blocking event loop to an
async lifecycle, replacing the background footer thread and blocking
select/waits with asyncio-compatible tasks and awaits while preserving terminal
setup, child I/O, cleanup, and exit-code behavior. Update synchronous launcher
boundaries that invoke run to call the async lifecycle through asyncio.run(),
and adjust related lifecycle helpers to remain consistently awaitable.

---

Outside diff comments:
In @.github/workflows/ci.yml:
- Around line 216-227: The CLI validation must prove that the top-level command
allowlist contains only launch. In .github/workflows/ci.yml lines 216-227, add a
switchyard --help check that confirms launch is the sole available subcommand;
in tests/test_cli_reference_docs.py lines 50-56, assert that
_subparsers(_build_parser()) equals {"launch"}.

---

Nitpick comments:
In `@crates/switchyard-py/src/libsy_bindings.rs`:
- Around line 31-39: Update the header parsing errors in the loop over headers
to include the failing header name alongside the underlying error message, for
both HeaderName::from_bytes and HeaderValue::from_str failures. Do not include
the header value, and preserve the existing try_append handling.
🪄 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: 8b113f39-f5b8-46c9-9c29-1081242baa7a

📥 Commits

Reviewing files that changed from the base of the PR and between fb3fc30 and 5e6be08.

⛔ Files ignored due to path filters (2)
  • Cargo.lock is excluded by !**/*.lock, !Cargo.lock
  • uv.lock is excluded by !**/*.lock, !uv.lock
📒 Files selected for processing (239)
  • .agents/skills/switchyard-stage-router-scorer/SKILL.md
  • .github/ISSUE_TEMPLATE/bug_report.md
  • .github/workflows/ci.yml
  • .github/workflows/perf.yml
  • AGENTS.md
  • CHANGELOG.md
  • Cargo.toml
  • INSTALLATION.md
  • README.md
  • benchmark/README.md
  • benchmark/score_staged_run.py
  • crates/libsy-llm-client/src/client.rs
  • crates/libsy-llm-client/src/error.rs
  • crates/switchyard-components/Cargo.toml
  • crates/switchyard-components/src/backends/anthropic.rs
  • crates/switchyard-components/src/backends/common.rs
  • crates/switchyard-components/src/backends/context_overflow.rs
  • crates/switchyard-components/src/backends/mod.rs
  • crates/switchyard-components/src/backends/multi.rs
  • crates/switchyard-components/src/backends/openai.rs
  • crates/switchyard-components/src/backends/selection.rs
  • crates/switchyard-components/src/backends/stats.rs
  • crates/switchyard-components/src/contracts/backend.rs
  • crates/switchyard-components/src/contracts/context.rs
  • crates/switchyard-components/src/contracts/error.rs
  • crates/switchyard-components/src/contracts/ids.rs
  • crates/switchyard-components/src/contracts/mod.rs
  • crates/switchyard-components/src/contracts/roles.rs
  • crates/switchyard-components/src/contracts/types.rs
  • crates/switchyard-components/src/dimension_collector/mod.rs
  • crates/switchyard-components/src/dimension_collector/response/checks.rs
  • crates/switchyard-components/src/dimension_collector/response/mod.rs
  • crates/switchyard-components/src/dimension_collector/tool_signals.rs
  • crates/switchyard-components/src/lib.rs
  • crates/switchyard-components/src/request_processors/dimension_collector.rs
  • crates/switchyard-components/src/request_processors/mod.rs
  • crates/switchyard-components/src/request_processors/random_routing.rs
  • crates/switchyard-components/src/request_processors/stats.rs
  • crates/switchyard-components/src/response_processors/mod.rs
  • crates/switchyard-components/src/response_processors/response_signals.rs
  • crates/switchyard-components/src/response_processors/stats.rs
  • crates/switchyard-components/src/stage_router.rs
  • crates/switchyard-components/src/stats/accumulator.rs
  • crates/switchyard-components/src/stats/cache_eligibility.rs
  • crates/switchyard-components/src/stats/context.rs
  • crates/switchyard-components/src/stats/cost.rs
  • crates/switchyard-components/src/stats/mod.rs
  • crates/switchyard-components/src/stats/usage.rs
  • crates/switchyard-components/src/telemetry.rs
  • crates/switchyard-components/tests/adversarial_multi_llm_backend.rs
  • crates/switchyard-components/tests/adversarial_native_backends.rs
  • crates/switchyard-components/tests/adversarial_random_routing.rs
  • crates/switchyard-components/tests/contracts.rs
  • crates/switchyard-components/tests/stats_accumulator.rs
  • crates/switchyard-components/tests/stats_processors.rs
  • crates/switchyard-components/tests/stats_usage_shapes.rs
  • crates/switchyard-components/tests/support/config.rs
  • crates/switchyard-components/tests/support/mod.rs
  • crates/switchyard-py/Cargo.toml
  • crates/switchyard-py/src/component_bindings.rs
  • crates/switchyard-py/src/component_bindings/backends.rs
  • crates/switchyard-py/src/component_bindings/config.rs
  • crates/switchyard-py/src/component_bindings/dimension_collector.rs
  • crates/switchyard-py/src/component_bindings/request_processors.rs
  • crates/switchyard-py/src/component_bindings/response_processors.rs
  • crates/switchyard-py/src/component_bindings/stage_router.rs
  • crates/switchyard-py/src/component_bindings/stats.rs
  • crates/switchyard-py/src/errors.rs
  • crates/switchyard-py/src/interop.rs
  • crates/switchyard-py/src/interop/context.rs
  • crates/switchyard-py/src/interop/request.rs
  • crates/switchyard-py/src/interop/response.rs
  • crates/switchyard-py/src/interop/roles.rs
  • crates/switchyard-py/src/interop/subagent.rs
  • crates/switchyard-py/src/lib.rs
  • crates/switchyard-py/src/libsy_bindings.rs
  • crates/switchyard-py/src/py_serde.rs
  • crates/switchyard-py/src/translation.rs
  • crates/switchyard-server/README.md
  • docs/cli_reference.md
  • docs/getting_started.md
  • docs/internal/metrics_reference.md
  • docs/routing_algorithms/stage_router_routing.md
  • examples/minimal.py
  • examples/route.yaml
  • examples/utils.py
  • pyproject.toml
  • switchyard/__init__.py
  • switchyard/cli/command_utils.py
  • switchyard/cli/launchers/claude_code_launcher.py
  • switchyard/cli/launchers/codex_cli_launcher.py
  • switchyard/cli/launchers/cost_estimator.py
  • switchyard/cli/launchers/launcher_runtime.py
  • switchyard/cli/launchers/live_stats_footer.py
  • switchyard/cli/launchers/openclaw_launcher.py
  • switchyard/cli/launchers/session_summary.py
  • switchyard/cli/launchers/shell_tui.py
  • switchyard/cli/model_catalog/__init__.py
  • switchyard/cli/model_catalog/model_discovery.py
  • switchyard/cli/route_bundle.py
  • switchyard/cli/switchyard_cli.py
  • switchyard/lib/__init__.py
  • switchyard/lib/backends/__init__.py
  • switchyard/lib/backends/anthropic_native_llm_backend.py
  • switchyard/lib/backends/backend_format_resolver.py
  • switchyard/lib/backends/llm_target.py
  • switchyard/lib/backends/multi_llm_backend.py
  • switchyard/lib/backends/openai_llm_backend.py
  • switchyard/lib/backends/openai_native_backend.py
  • switchyard/lib/backends/stats_llm_backend.py
  • switchyard/lib/chat_request/__init__.py
  • switchyard/lib/chat_request/anthropic.py
  • switchyard/lib/chat_request/base.py
  • switchyard/lib/chat_request/openai_chat.py
  • switchyard/lib/chat_request/openai_responses.py
  • switchyard/lib/chat_response/__init__.py
  • switchyard/lib/chat_response/anthropic.py
  • switchyard/lib/chat_response/base.py
  • switchyard/lib/chat_response/openai_chat.py
  • switchyard/lib/chat_response/openai_responses.py
  • switchyard/lib/chat_response/streaming_response_accumulator.py
  • switchyard/lib/conversation_turn.py
  • switchyard/lib/endpoints/__init__.py
  • switchyard/lib/endpoints/anthropic_messages_endpoint.py
  • switchyard/lib/endpoints/base.py
  • switchyard/lib/endpoints/dispatch.py
  • switchyard/lib/endpoints/error_envelope.py
  • switchyard/lib/endpoints/models_endpoint.py
  • switchyard/lib/endpoints/openai_chat_endpoint.py
  • switchyard/lib/endpoints/outcome_metrics.py
  • switchyard/lib/endpoints/prometheus_emitter.py
  • switchyard/lib/endpoints/responses_endpoint.py
  • switchyard/lib/endpoints/route_selection.py
  • switchyard/lib/endpoints/routing_log_stats_endpoint.py
  • switchyard/lib/endpoints/sse_helpers.py
  • switchyard/lib/endpoints/stats_endpoint.py
  • switchyard/lib/endpoints/upstream_error.py
  • switchyard/lib/endpoints/upstream_error_log.py
  • switchyard/lib/llm_client.py
  • switchyard/lib/model_listing.py
  • switchyard/lib/processors/__init__.py
  • switchyard/lib/processors/format_translate.py
  • switchyard/lib/processors/model_rewrite_request_processor.py
  • switchyard/lib/processors/rl_logging_request_processor.py
  • switchyard/lib/processors/rl_logging_response_processor.py
  • switchyard/lib/processors/routing_log_response_processor.py
  • switchyard/lib/processors/stats_request_processor.py
  • switchyard/lib/processors/stats_response_processor_accumulator.py
  • switchyard/lib/prometheus_exposition.py
  • switchyard/lib/proxy_context.py
  • switchyard/lib/request_metadata.py
  • switchyard/lib/roles.py
  • switchyard/lib/route_table.py
  • switchyard/lib/startup_timing.py
  • switchyard/lib/stats_accumulator.py
  • switchyard/lib/switchyard.py
  • switchyard/lib/tracing.py
  • switchyard/server/__init__.py
  • switchyard/server/server_util.py
  • switchyard/server/switchyard_app.py
  • switchyard/telemetry.py
  • switchyard_rust/__init__.py
  • switchyard_rust/_native.py
  • switchyard_rust/components.py
  • switchyard_rust/components.pyi
  • switchyard_rust/core.py
  • switchyard_rust/libsy.py
  • switchyard_rust/server.py
  • switchyard_rust/translation.py
  • tests/_chain_test_helpers.py
  • tests/conftest.py
  • tests/contract/__init__.py
  • tests/contract/test_platform_imports.py
  • tests/contract/test_proxy_context.py
  • tests/contract/test_request_response_types.py
  • tests/e2e/_helpers.py
  • tests/e2e/conftest.py
  • tests/e2e/test_passthrough_e2e.py
  • tests/e2e/test_passthrough_responses_e2e.py
  • tests/e2e_multiturn_responses.py
  • tests/getting_started/test_getting_started.py
  • tests/readme/test_readme.py
  • tests/test_anthropic_native_llm_backend.py
  • tests/test_anthropic_openai_translation.py
  • tests/test_anthropic_output_config_strip.py
  • tests/test_anthropic_probe.py
  • tests/test_backend_format_resolver.py
  • tests/test_build_and_serve.py
  • tests/test_chat_request.py
  • tests/test_chat_response.py
  • tests/test_cli_reference_docs.py
  • tests/test_codex_multiturn_traces.py
  • tests/test_context_error_translation.py
  • tests/test_context_window_exceeded_endpoint.py
  • tests/test_cost_estimator_gemini.py
  • tests/test_endpoint_state_contract.py
  • tests/test_error_source_annotation.py
  • tests/test_format_translate_processor.py
  • tests/test_inference_e2e.py
  • tests/test_infra.py
  • tests/test_init_all_exports.py
  • tests/test_launchers.py
  • tests/test_live_stats_footer.py
  • tests/test_llm_client.py
  • tests/test_metrics_endpoint.py
  • tests/test_no_stale_module_paths.py
  • tests/test_outcome_metrics.py
  • tests/test_prometheus_emitter.py
  • tests/test_prometheus_exposition.py
  • tests/test_python_server_passthrough.py
  • tests/test_request_metadata.py
  • tests/test_request_translation_engine.py
  • tests/test_request_translation_engine_to_any_of.py
  • tests/test_response_translation_engine.py
  • tests/test_responses_openai_translation.py
  • tests/test_rl_logging.py
  • tests/test_rl_logging_e2e.py
  • tests/test_route_bundle.py
  • tests/test_route_selection_headers.py
  • tests/test_route_table.py
  • tests/test_routing_log_response_processor.py
  • tests/test_shell_tui.py
  • tests/test_sse_stream_close.py
  • tests/test_stats_accumulator.py
  • tests/test_stream_close_chain.py
  • tests/test_stream_leak_repro.py
  • tests/test_switchyard.py
  • tests/test_switchyard_app_factory.py
  • tests/test_switchyard_app_lifecycle.py
  • tests/test_switchyard_rust_component_bindings.py
  • tests/test_switchyard_rust_core_bindings.py
  • tests/test_telemetry.py
  • tests/test_tool_result_signal_collector.py
  • tests/test_tracing.py
  • tests/test_translation_engine_chaos.py
  • tests/test_upstream_error_log.py
  • tests/test_upstream_error_passthrough.py
  • tests/translation/__init__.py
  • tests/translation/test_format_fidelity_contract.py
💤 Files with no reviewable changes (106)
  • Cargo.toml
  • switchyard/lib/backends/openai_llm_backend.py
  • crates/switchyard-components/src/response_processors/mod.rs
  • switchyard/lib/init.py
  • .agents/skills/switchyard-stage-router-scorer/SKILL.md
  • switchyard/lib/chat_request/base.py
  • switchyard/lib/chat_request/openai_responses.py
  • switchyard/lib/chat_request/anthropic.py
  • crates/switchyard-components/src/dimension_collector/mod.rs
  • switchyard/lib/backends/anthropic_native_llm_backend.py
  • switchyard/lib/chat_response/openai_chat.py
  • switchyard/lib/endpoints/routing_log_stats_endpoint.py
  • crates/switchyard-components/src/contracts/roles.rs
  • switchyard/lib/chat_response/openai_responses.py
  • switchyard/lib/chat_response/anthropic.py
  • switchyard/cli/model_catalog/model_discovery.py
  • switchyard/lib/backends/openai_native_backend.py
  • benchmark/README.md
  • switchyard/lib/backends/multi_llm_backend.py
  • crates/switchyard-components/src/request_processors/dimension_collector.rs
  • switchyard/lib/backends/stats_llm_backend.py
  • crates/switchyard-py/src/component_bindings/request_processors.rs
  • crates/switchyard-py/src/interop/roles.rs
  • crates/switchyard-components/src/contracts/context.rs
  • crates/switchyard-py/src/interop.rs
  • crates/switchyard-components/src/backends/context_overflow.rs
  • crates/switchyard-components/Cargo.toml
  • crates/switchyard-py/src/component_bindings.rs
  • examples/route.yaml
  • crates/switchyard-components/src/stats/usage.rs
  • crates/switchyard-components/src/backends/stats.rs
  • crates/switchyard-py/src/component_bindings/stage_router.rs
  • crates/switchyard-py/Cargo.toml
  • crates/switchyard-py/src/component_bindings/config.rs
  • switchyard/lib/chat_request/openai_chat.py
  • crates/switchyard-components/tests/adversarial_native_backends.rs
  • crates/switchyard-py/src/interop/request.rs
  • crates/switchyard-py/src/lib.rs
  • crates/switchyard-components/src/contracts/backend.rs
  • switchyard/lib/endpoints/dispatch.py
  • crates/switchyard-components/src/backends/multi.rs
  • switchyard/cli/model_catalog/init.py
  • switchyard/lib/endpoints/responses_endpoint.py
  • switchyard/lib/chat_response/base.py
  • switchyard/lib/endpoints/error_envelope.py
  • crates/switchyard-components/tests/contracts.rs
  • crates/switchyard-components/src/response_processors/stats.rs
  • crates/switchyard-components/tests/adversarial_random_routing.rs
  • crates/switchyard-py/src/interop/subagent.rs
  • crates/switchyard-py/src/interop/context.rs
  • crates/switchyard-components/src/contracts/error.rs
  • crates/switchyard-components/src/dimension_collector/tool_signals.rs
  • crates/switchyard-py/src/component_bindings/dimension_collector.rs
  • crates/switchyard-components/src/contracts/types.rs
  • switchyard/lib/conversation_turn.py
  • crates/switchyard-components/src/dimension_collector/response/mod.rs
  • switchyard/lib/endpoints/models_endpoint.py
  • switchyard/lib/endpoints/route_selection.py
  • crates/switchyard-components/src/stats/mod.rs
  • switchyard/lib/endpoints/prometheus_emitter.py
  • crates/switchyard-components/src/backends/mod.rs
  • crates/switchyard-components/tests/support/config.rs
  • crates/switchyard-components/src/lib.rs
  • switchyard/cli/route_bundle.py
  • crates/switchyard-components/src/backends/selection.rs
  • switchyard/lib/endpoints/base.py
  • crates/switchyard-components/src/stats/accumulator.rs
  • examples/utils.py
  • crates/switchyard-components/src/response_processors/response_signals.rs
  • crates/switchyard-components/src/request_processors/random_routing.rs
  • switchyard/lib/chat_request/init.py
  • crates/switchyard-components/src/stats/cache_eligibility.rs
  • switchyard/lib/endpoints/outcome_metrics.py
  • crates/switchyard-py/src/translation.rs
  • switchyard/lib/chat_response/init.py
  • crates/switchyard-components/src/request_processors/stats.rs
  • crates/switchyard-components/tests/adversarial_multi_llm_backend.rs
  • benchmark/score_staged_run.py
  • switchyard/lib/endpoints/anthropic_messages_endpoint.py
  • switchyard/lib/endpoints/openai_chat_endpoint.py
  • switchyard/lib/backends/backend_format_resolver.py
  • crates/switchyard-py/src/component_bindings/response_processors.rs
  • crates/switchyard-components/src/backends/anthropic.rs
  • switchyard/lib/endpoints/init.py
  • crates/switchyard-components/src/contracts/mod.rs
  • crates/switchyard-py/src/component_bindings/stats.rs
  • crates/switchyard-components/src/telemetry.rs
  • crates/switchyard-components/tests/stats_accumulator.rs
  • crates/switchyard-py/src/interop/response.rs
  • switchyard/lib/backends/init.py
  • crates/switchyard-components/src/stats/context.rs
  • crates/switchyard-py/src/component_bindings/backends.rs
  • crates/switchyard-components/src/contracts/ids.rs
  • crates/switchyard-components/src/stage_router.rs
  • switchyard/lib/chat_response/streaming_response_accumulator.py
  • crates/switchyard-components/src/backends/common.rs
  • examples/minimal.py
  • crates/switchyard-components/tests/support/mod.rs
  • crates/switchyard-components/src/backends/openai.rs
  • crates/switchyard-py/src/py_serde.rs
  • crates/switchyard-components/src/dimension_collector/response/checks.rs
  • switchyard/lib/backends/llm_target.py
  • crates/switchyard-components/src/stats/cost.rs
  • crates/switchyard-components/tests/stats_processors.rs
  • crates/switchyard-components/src/request_processors/mod.rs
  • crates/switchyard-components/tests/stats_usage_shapes.rs

Comment thread AGENTS.md
Everything flows through a fixed-shape chain enforced at construction time:
The supported serving path is native Rust:

```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add language identifiers to the new fenced blocks.

markdownlint-cli2 reports MD040 for the architecture and project-structure fences. Add text to both opening fences.

Proposed fix
-```
+```text

Also applies to: 158-158

🧰 Tools
🪛 markdownlint-cli2 (0.23.2)

[warning] 142-142: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 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 `@AGENTS.md` at line 142, Update the architecture and project-structure fenced
blocks in AGENTS.md to use the text language identifier on both opening fences,
resolving the markdownlint MD040 violations while leaving their contents
unchanged.

Source: Linters/SAST tools

Comment on lines +10 to 19
create_exception!(_switchyard_rust, LibsyError, PyRuntimeError);

/// Converts libsy execution failures into one stable Python exception.
pub(crate) fn py_libsy_error(error: impl std::fmt::Display) -> PyErr {
LibsyError::new_err(error.to_string())
}

/// Converts core Switchyard errors into typed Python runtime errors.
///
/// `ContextWindowExceeded` and `ContextPoolExhausted` carry typed fields
/// (`target_id`, `model`, `last_target_id`, `reason`) — we attach those as
/// Python attributes on the raised exception so callers can inspect them
/// programmatically and `backend_error_with_ctx` can recover the typed
/// variant when a Python `LLMBackend` re-raises through the Rust chain.
/// Without these attrs the variant collapses to the string message and the
/// compatibility retry code stamps `"unknown"` for `target_id`/`model`.
pub(crate) fn py_core_error(error: SwitchyardError) -> PyErr {
let message = error.to_string();
match error {
SwitchyardError::InvalidConfig(_) => SwitchyardConfigError::new_err(message),
SwitchyardError::InvalidId(_) => SwitchyardInvalidIdError::new_err(message),
SwitchyardError::DuplicateRegistration { .. } => {
SwitchyardDuplicateRegistrationError::new_err(message)
}
SwitchyardError::ModelNotFound { .. } => SwitchyardModelNotFoundError::new_err(message),
SwitchyardError::UnsupportedRequestType { .. } => {
SwitchyardUnsupportedRequestTypeError::new_err(message)
}
SwitchyardError::InvalidRequest(_) => SwitchyardInvalidRequestError::new_err(message),
SwitchyardError::Processor(_) => SwitchyardProcessorError::new_err(message),
SwitchyardError::Backend(_) => SwitchyardBackendError::new_err(message),
SwitchyardError::Upstream(_) => SwitchyardUpstreamError::new_err(message),
SwitchyardError::UpstreamHttp {
status_code, body, ..
} => {
let err = SwitchyardUpstreamError::new_err(message);
attach_upstream_http_attrs(&err, status_code, &body);
err
}
SwitchyardError::ContextWindowExceeded {
target_id, model, ..
} => {
let err = SwitchyardContextWindowExceededError::new_err(message);
attach_attrs(&err, &[("target_id", &target_id), ("model", &model)]);
err
}
SwitchyardError::ContextPoolExhausted {
last_target_id,
reason,
} => {
let err = SwitchyardContextPoolExhaustedError::new_err(message);
attach_attrs(
&err,
&[("last_target_id", &last_target_id), ("reason", &reason)],
);
err
}
SwitchyardError::Other(_) => SwitchyardRuntimeError::new_err(message),
}
}

/// Set string attributes on a freshly-constructed `PyErr`'s exception value.
/// Errors are intentionally ignored — attribute attachment is best-effort
/// diagnostic metadata, never the path-of-correctness for raising the error.
fn attach_attrs(err: &PyErr, attrs: &[(&str, &str)]) {
Python::attach(|py| {
let bound = err.value(py);
for (name, value) in attrs {
let _ = bound.setattr(*name, *value);
}
});
}

/// Attach typed upstream HTTP details for endpoint error handling.
fn attach_upstream_http_attrs(err: &PyErr, status_code: u16, body: &str) {
Python::attach(|py| {
let bound = err.value(py);
let _ = bound.setattr("status_code", status_code);
let _ = bound.setattr("body", body);
});
}

/// Registers typed exception classes in the native extension module.
pub(crate) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> {
let py = module.py();
module.add(
"SwitchyardRuntimeError",
py.get_type::<SwitchyardRuntimeError>(),
)?;
module.add("LibsyError", py.get_type::<LibsyError>())?;
module.add(
"SwitchyardConfigError",
py.get_type::<SwitchyardConfigError>(),
)?;
module.add(
"SwitchyardInvalidIdError",
py.get_type::<SwitchyardInvalidIdError>(),
)?;
module.add(
"SwitchyardDuplicateRegistrationError",
py.get_type::<SwitchyardDuplicateRegistrationError>(),
)?;
module.add(
"SwitchyardModelNotFoundError",
py.get_type::<SwitchyardModelNotFoundError>(),
)?;
module.add(
"SwitchyardUnsupportedRequestTypeError",
py.get_type::<SwitchyardUnsupportedRequestTypeError>(),
)?;
module.add(
"SwitchyardInvalidRequestError",
py.get_type::<SwitchyardInvalidRequestError>(),
)?;
module.add(
"SwitchyardProcessorError",
py.get_type::<SwitchyardProcessorError>(),
)?;
module.add(
"SwitchyardBackendError",
py.get_type::<SwitchyardBackendError>(),
)?;
module.add(
"SwitchyardUpstreamError",
py.get_type::<SwitchyardUpstreamError>(),
)?;
module.add(
"SwitchyardContextWindowExceededError",
py.get_type::<SwitchyardContextWindowExceededError>(),
)?;
module.add(
"SwitchyardContextPoolExhaustedError",
py.get_type::<SwitchyardContextPoolExhaustedError>(),
)?;
Ok(())
module.add("LibsyError", module.py().get_type::<LibsyError>())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find residual references to removed typed exceptions and check the exported error surface.

# All exception types still created or registered in the PyO3 crate.
rg -nP --type=rust 'create_exception!|add\("(\w*Error)"' crates/switchyard-py/src

# Any Python/stub/doc reference to a Switchyard-specific error name other than LibsyError.
rg -nP -g '!target' -g '*.py' -g '*.pyi' -g '*.md' '\b[A-Z]\w*Error\b' \
  | rg -v '\b(LibsyError|RuntimeError|ValueError|TypeError|OSError|KeyError|FileNotFoundError)\b'

# Confirm the declared exception surface in the Python stubs.
fd -t f -e py -e pyi . switchyard_rust --exec rg -n 'Error' {}

Repository: NVIDIA-NeMo/Switchyard

Length of output: 483


🏁 Script executed:

#!/bin/bash
set -u

printf '%s\n' '--- relevant change summary ---'
git status --short
git diff --stat -- crates/switchyard-py/src/errors.rs switchyard_rust crates

printf '%s\n' '--- current error bindings ---'
cat -n crates/switchyard-py/src/errors.rs
rg -n -C 3 'LibsyError|Error' crates/switchyard-py/src/libsy_bindings.rs switchyard_rust 2>/dev/null || true

printf '%s\n' '--- removed exception identifiers from the diff ---'
git diff --unified=80 -- crates/switchyard-py/src/errors.rs crates/switchyard-py/src/libsy_bindings.rs switchyard_rust \
  | rg '^[+-].*(create_exception!|Error|exception)' || true

printf '%s\n' '--- repository references to likely typed exceptions ---'
rg -n --hidden -g '!target' -g '!node_modules' \
  '(LibsyError|[A-Z][A-Za-z0-9_]*(Error|Exception))' . \
  | head -n 500

printf '%s\n' '--- deprecation-plan references ---'
rg -n --hidden -g '!target' -i \
  '(deprecat|removed|renamed|typed exception|exception surface)' \
  README.md docs crates switchyard_rust 2>/dev/null | head -n 300 || true

Repository: NVIDIA-NeMo/Switchyard

Length of output: 50378


🏁 Script executed:

#!/bin/bash
set -u

printf '%s\n' '--- commit and error-surface diff ---'
git rev-parse HEAD
git diff --unified=20 HEAD^ HEAD -- crates/switchyard-py/src/errors.rs crates/switchyard-py/src/libsy_bindings.rs switchyard_rust/libsy.py switchyard/libsy/__init__.py
printf '%s\n' '--- previous error binding source ---'
git show HEAD^:crates/switchyard-py/src/errors.rs 2>/dev/null | cat -n || true

printf '%s\n' '--- Python consumers of the binding ---'
rg -n --hidden -g '*.py' -g '*.pyi' -g '*.md' \
  'from (switchyard|switchyard_rust)(\.[A-Za-z0-9_]+)* import|import switchyard(_rust)?|LibsyError|NoTargets|TargetNotFound|AlgorithmError|DriverError|MissingFinalResponse|AllTargetsExcluded' \
  . | head -n 400

printf '%s\n' '--- public package exports and documentation ---'
rg -n --hidden -g '*.py' -g '*.pyi' -g '*.md' \
  '(__all__|LibsyError|NoTargets|TargetNotFound|AlgorithmError|DriverError|MissingFinalResponse|AllTargetsExcluded)' \
  switchyard switchyard_rust docs README.md crates/switchyard-py 2>/dev/null | head -n 400 || true

printf '%s\n' '--- likely deprecation records ---'
rg -n --hidden -g '!target' -i \
  '(deprecat|breaking change|migration|removed exception|typed exception)' \
  CHANGELOG* docs README.md switchyard switchyard_rust crates 2>/dev/null | head -n 300 || true

printf '%s\n' '--- exact current exception declarations and references ---'
python3 - <<'PY'
from pathlib import Path
import re

root = Path(".")
declared = []
for path in root.rglob("*"):
    if not path.is_file() or "target" in path.parts or ".git" in path.parts:
        continue
    try:
        text = path.read_text()
    except (UnicodeDecodeError, OSError):
        continue
    for match in re.finditer(r"create_exception!\([^,]+,\s*([A-Za-z_]\w*)", text):
        declared.append((match.group(1), str(path)))
print("declared:", declared)
for name, path in declared:
    refs = []
    pattern = re.compile(rf"\b{re.escape(name)}\b")
    for candidate in root.rglob("*"):
        if not candidate.is_file() or "target" in candidate.parts or ".git" in candidate.parts:
            continue
        try:
            content = candidate.read_text()
        except (UnicodeDecodeError, OSError):
            continue
        if pattern.search(content):
            refs.append(str(candidate))
    print(name, "references:", sorted(set(refs)))
PY

Repository: NVIDIA-NeMo/Switchyard

Length of output: 32265


Add a deprecation plan for the removed PyO3 exception exports.

The previous binding exported SwitchyardRuntimeError and the typed Switchyard*Error classes. No repository consumer references them, but no deprecation or migration record exists. Add the required plan before removing these public exports.

🤖 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/switchyard-py/src/errors.rs` around lines 10 - 19, Add a deprecation
and migration record for the removed PyO3 exports, covering
SwitchyardRuntimeError and the typed Switchyard*Error classes, before finalizing
the new LibsyError-only API in register and py_libsy_error. Document the
replacement behavior and migration path using the repository’s established
deprecation-plan location and format.

Source: Coding guidelines

Comment on lines +168 to +169
decoded, so it measures request ingress through response completion. It still excludes connection
accept and TLS handshake, which hyper completes before the server sees the request.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- README context ---'
sed -n '145,190p' crates/switchyard-server/README.md
printf '%s\n' '--- latency references ---'
rg -n -C 3 'switchyard_total_latency_ms|stream|SSE|total_latency|response completion|connection accept|connection acceptance' crates

Repository: NVIDIA-NeMo/Switchyard

Length of output: 50379


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- README lines 155-185 ---'
sed -n '155,185p' crates/switchyard-server/README.md
printf '%s\n' '--- server latency symbols ---'
rg -n -C 5 'switchyard_total_latency_ms|latency_ms|stream.*(start|end)|SSE|stream handle|response completion|connection accept' crates/switchyard-server
printf '%s\n' '--- server source files ---'
git ls-files crates/switchyard-server | sed -n '1,120p'

Repository: NVIDIA-NeMo/Switchyard

Length of output: 39763


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- latency call sites ---'
rg -n -C 8 'record_usage|record_latency|total_latency|Instant::now|elapsed\(\)|RunObservation|ResponseBody|Sse<|frame_stream|stream handle' crates/switchyard-server/src crates/switchyard-server/tests
printf '%s\n' '--- lib outline ---'
ast-grep outline crates/switchyard-server/src/lib.rs
printf '%s\n' '--- usage metrics ---'
cat -n crates/switchyard-server/src/usage_metrics.rs | sed -n '1,190p'

Repository: NVIDIA-NeMo/Switchyard

Length of output: 50378


🏁 Script executed:

#!/bin/bash
set -e
set -o pipefail
printf '%s\n' '--- usage_metrics stream wrapper ---'
cat -n crates/switchyard-server/src/usage_metrics.rs | sed -n '16,115p'
printf '%s\n' '--- endpoint flow ---'
cat -n crates/switchyard-server/src/lib.rs | sed -n '445,625p'
printf '%s\n' '--- routed request flow ---'
cat -n crates/switchyard-server/src/lib.rs | sed -n '677,745p'
printf '%s\n' '--- SSE framing ---'
cat -n crates/switchyard-server/src/sse.rs | sed -n '13,75p'

Repository: NVIDIA-NeMo/Switchyard

Length of output: 17550


🏁 Script executed:

#!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path

usage = Path("crates/switchyard-server/src/usage_metrics.rs").read_text()
lib = Path("crates/switchyard-server/src/lib.rs").read_text()

observe = usage[usage.index("LlmResponse::Stream(mut stream)"):usage.index("\n    };\n\n    Response", usage.index("LlmResponse::Stream(mut stream)"))]
assert "while let Some(item) = stream.next().await" in observe
assert "record_terminal(&stats, &usage, &model, started, cache_eligible);" in observe
assert observe.index("while let Some(item)") < observe.index("record_terminal(&stats, &usage")

endpoint = lib[lib.index("let response = render_error_response"):lib.index("\n        response", lib.index("let response = render_error_response"))]
assert endpoint.index("request_log.emit(&response)") < endpoint.index("response")

record_terminal = usage[usage.index("fn record_terminal"):usage.index("\n}\n\nfn attributes", usage.index("fn record_terminal"))]
assert "started.elapsed()" in record_terminal
print("switchyard_total_latency_ms: recorded after streamed source completion")
print("request log latency: recorded after response construction, before handler return")
PY

Repository: NVIDIA-NeMo/Switchyard

Length of output: 249


🏁 Script executed:

#!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path

usage = Path("crates/switchyard-server/src/usage_metrics.rs").read_text()
lib = Path("crates/switchyard-server/src/lib.rs").read_text()

stream_start = usage.index("LlmResponse::Stream(mut stream)")
stream_end = usage.index("\n    };\n\n    let llm_response", stream_start) if "\n    };\n\n    let llm_response" in usage[stream_start:] else usage.index("\n    };\n\n    LlmResponse::Stream", stream_start)
stream = usage[stream_start:stream_end]
assert "while let Some(item) = stream.next().await" in stream
assert "record_terminal(&stats, &usage, &model, started, cache_eligible);" in stream
assert stream.index("while let Some(item)") < stream.index("record_terminal(&stats, &usage")

emit = "request_log.emit(&response)"
response_return = "response\n}"
emit_pos = lib.index(emit, lib.index("async fn handle_endpoint_inner"))
return_pos = lib.index(response_return, emit_pos)
assert emit_pos < return_pos

terminal_start = usage.index("fn record_terminal")
terminal_end = usage.index("\n}\n\nfn attributes", terminal_start)
terminal = usage[terminal_start:terminal_end]
assert "started.elapsed()" in terminal

print("switchyard_total_latency_ms: recorded after streamed source completion")
print("request log latency: recorded after response construction, before handler return")
PY

Repository: NVIDIA-NeMo/Switchyard

Length of output: 265


🏁 Script executed:

#!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path

usage_lines = Path("crates/switchyard-server/src/usage_metrics.rs").read_text().splitlines()
lib_lines = Path("crates/switchyard-server/src/lib.rs").read_text().splitlines()

def line_number(lines, text, start=0):
    for n, line in enumerate(lines[start:], start + 1):
        if text in line:
            return n
    raise SystemExit(f"missing source text: {text}")

stream_line = line_number(usage_lines, "LlmResponse::Stream(mut stream)")
poll_line = line_number(usage_lines, "while let Some(item) = stream.next().await", stream_line - 1)
terminal_line = line_number(
    usage_lines,
    "record_terminal(&stats, &usage, &model, started, cache_eligible);",
    poll_line - 1,
)
elapsed_line = line_number(usage_lines, "let total_latency = started.elapsed();")

inner_line = line_number(lib_lines, "async fn handle_endpoint_inner")
emit_line = line_number(lib_lines, "request_log.emit(&response)", inner_line - 1)
return_line = line_number(lib_lines, "        response", emit_line - 1)

assert stream_line <= poll_line < terminal_line
assert elapsed_line > 0
assert inner_line < emit_line < return_line

print(f"stream polling begins at usage_metrics.rs:{poll_line}")
print(f"total latency is recorded at usage_metrics.rs:{terminal_line}, after polling")
print(f"request log latency is emitted at lib.rs:{emit_line}, before handler return at lib.rs:{return_line}")
PY

Repository: NVIDIA-NeMo/Switchyard

Length of output: 351


Correct the streaming latency definition in lines 178–180.

switchyard_total_latency_ms is recorded after the wrapped stream ends. Do not state that both clocks stop when the stream handle arrives. Keep the response-completion description consistent with stream completion, and use connection acceptance instead of connection accept.

🧰 Tools
🪛 LanguageTool

[grammar] ~169-~169: Use a hyphen to join words.
Context: ...on accept and TLS handshake, which hyper completes before the server sees the req...

(QB_NEW_EN_HYPHEN)

🤖 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/switchyard-server/README.md` around lines 168 - 169, Update the README
latency definition around switchyard_total_latency_ms to state that timing ends
after the wrapped stream completes, not when the stream handle arrives. Keep the
response-completion wording consistent with stream completion and replace
“connection accept” with “connection acceptance.”

Comment on lines +154 to +157
Sweep a few candidate thresholds in representative benchmark runs. Choose the
lowest threshold that rescues the RESCUE quadrant without over-escalating the
LOSS quadrant. Because the scorer is corroborative, a `0.5` threshold takes
roughly 1.5 signals of agreement.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Calibrate confidence_threshold separately for each picker.

The current rule is not valid for both picker modes. With capable_first, lowering the threshold sends more turns to efficient; RESCUE is a beneficial downgrade, while LOSS is a harmful downgrade. With efficient_first, lowering the threshold sends more turns to capable; LOSS is the case that benefits from escalation. Replace the universal “lowest threshold” rule with picker-specific guidance.

Proposed direction
-Sweep a few candidate thresholds in representative benchmark runs. Choose the
-lowest threshold that rescues the RESCUE quadrant without over-escalating the
-LOSS quadrant.
+Sweep candidate thresholds separately for each picker. With `capable_first`,
+balance beneficial RESCUE downgrades against harmful LOSS downgrades. With
+`efficient_first`, tune escalation for LOSS cases while limiting unnecessary
+escalation. Treat `0.5` as a starting point, not a picker-independent rule.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Sweep a few candidate thresholds in representative benchmark runs. Choose the
lowest threshold that rescues the RESCUE quadrant without over-escalating the
LOSS quadrant. Because the scorer is corroborative, a `0.5` threshold takes
roughly 1.5 signals of agreement.
Sweep candidate thresholds separately for each picker. With `capable_first`,
balance beneficial RESCUE downgrades against harmful LOSS downgrades. With
`efficient_first`, tune escalation for LOSS cases while limiting unnecessary
escalation. Treat `0.5` as a starting point, not a picker-independent rule.
Because the scorer is corroborative, a `0.5` threshold takes roughly 1.5 signals of agreement.
🤖 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 `@docs/routing_algorithms/stage_router_routing.md` around lines 154 - 157,
Replace the universal lowest-threshold guidance in the benchmark calibration
section with picker-specific rules: for capable_first, lower
confidence_threshold only enough to rescue RESCUE without over-escalating
harmful LOSS downgrades; for efficient_first, calibrate it to increase
escalation for beneficial LOSS cases while avoiding unnecessary RESCUE changes.
Keep the corroborative scorer context and require selecting the best threshold
independently for each picker mode.

"TranslationEngine",
"is_native_translation_available",
]
"""Python wrappers for Switchyard's native libsy and server bindings."""

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find root-package imports of switchyard_rust that no longer resolve.

# Direct "from switchyard_rust import X" usages (submodule imports are excluded by the dot check below).
rg -nP -g '*.py' -g '*.pyi' -g '*.md' -g '*.rst' -g '*.toml' 'from\s+switchyard_rust\s+import\s+'

# Attribute access on the package object, e.g. switchyard_rust.SomeName
rg -nP -g '*.py' -g '*.pyi' -g '*.md' '\bswitchyard_rust\.(?!libsy\b|server\b|_native\b)\w+'

# Show what the sibling modules actually export.
fd -t f -e py . switchyard_rust --exec rg -n '^__all__|^def |^class ' {}

Repository: NVIDIA-NeMo/Switchyard

Length of output: 160


🏁 Script executed:

#!/bin/bash
set -u

printf '%s\n' '--- repository files ---'
git ls-files | sed -n '1,160p'

printf '%s\n' '--- package files ---'
fd -t f . switchyard_rust 2>/dev/null || true

printf '%s\n' '--- root imports and attribute access across tracked text files ---'
rg -n --hidden -g '! .git' \
  -e 'from[[:space:]]+switchyard_rust[[:space:]]+import' \
  -e 'import[[:space:]]+switchyard_rust' \
  -e '\bswitchyard_rust\.[A-Za-z_][A-Za-z0-9_]*' . || true

printf '%s\n' '--- package initializer and sibling exports ---'
if [ -f switchyard_rust/__init__.py ]; then
  cat -n switchyard_rust/__init__.py
fi
for f in switchyard_rust/*.py switchyard_rust/*.pyi; do
  [ -f "$f" ] || continue
  printf '%s\n' "--- $f ---"
  cat -n "$f" | sed -n '1,220p'
done

Repository: NVIDIA-NeMo/Switchyard

Length of output: 16315


🏁 Script executed:

#!/bin/bash
set -u

printf '%s\n' '--- relevant diff ---'
git diff -- switchyard_rust/__init__.py switchyard_rust/libsy.py switchyard_rust/server.py \
  .github/workflows/publish.yml .github/workflows/package-portability.yml || true

printf '%s\n' '--- prior initializer, if available ---'
git show HEAD^:switchyard_rust/__init__.py 2>/dev/null | cat -n || true

printf '%s\n' '--- exact root-import contexts ---'
rg -n -C 3 \
  -e 'from[[:space:]]+switchyard_rust[[:space:]]+import' \
  -e 'import[[:space:]]+switchyard_rust' \
  -g '*.py' -g '*.pyi' -g '*.md' -g '*.rst' -g '*.toml' -g '*.yml' -g '*.yaml' . || true

printf '%s\n' '--- public names exported by package initializer ---'
python3 - <<'PY'
import ast
from pathlib import Path

path = Path("switchyard_rust/__init__.py")
tree = ast.parse(path.read_text())
public = []
for node in tree.body:
    if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
        if not node.name.startswith("_"):
            public.append(node.name)
    elif isinstance(node, ast.Assign):
        for target in node.targets:
            if isinstance(target, ast.Name) and not target.id.startswith("_"):
                public.append(target.id)
    elif isinstance(node, ast.AnnAssign):
        if isinstance(node.target, ast.Name) and not node.target.id.startswith("_"):
            public.append(node.target.id)
    elif isinstance(node, ast.ImportFrom):
        public.extend(
            alias.asname or alias.name
            for alias in node.names
            if not (alias.asname or alias.name).startswith("_")
        )
print("public_initializer_bindings:", sorted(set(public)))
PY

Repository: NVIDIA-NeMo/Switchyard

Length of output: 7700


🏁 Script executed:

#!/bin/bash
set -u

python3 - <<'PY'
import ast
import subprocess
from pathlib import Path

old_text = subprocess.run(
    ["git", "show", "HEAD^:switchyard_rust/__init__.py"],
    check=True,
    capture_output=True,
    text=True,
).stdout
old_tree = ast.parse(old_text)
old_all = []
for node in old_tree.body:
    if isinstance(node, ast.Assign):
        for target in node.targets:
            if isinstance(target, ast.Name) and target.id == "__all__":
                old_all = [elt.value for elt in node.value.elts if isinstance(elt, ast.Constant)]
current_text = Path("switchyard_rust/__init__.py").read_text()
current_tree = ast.parse(current_text)
current_all = []
for node in current_tree.body:
    if isinstance(node, ast.Assign):
        for target in node.targets:
            if isinstance(target, ast.Name) and target.id == "__all__":
                current_all = [elt.value for elt in node.value.elts if isinstance(elt, ast.Constant)]

removed = sorted(set(old_all) - set(current_all))
print("previous_root_all:", old_all)
print("current_root_all:", current_all)
print("removed_root_exports:", removed)

root_imports = []
attribute_uses = []
for raw in subprocess.run(
    ["git", "ls-files", "*.py", "*.pyi"],
    check=True,
    capture_output=True,
    text=True,
).stdout.splitlines():
    path = Path(raw)
    try:
        tree = ast.parse(path.read_text(), filename=str(path))
    except SyntaxError:
        continue
    for node in ast.walk(tree):
        if isinstance(node, ast.ImportFrom) and node.module == "switchyard_rust":
            for alias in node.names:
                if alias.name in removed:
                    root_imports.append((str(path), node.lineno, alias.name))
        if isinstance(node, ast.Attribute) and node.attr in removed:
            value = node.value
            if isinstance(value, ast.Name) and value.id == "switchyard_rust":
                attribute_uses.append((str(path), node.lineno, node.attr))
print("removed_root_imports:", root_imports)
print("direct_removed_attribute_uses:", attribute_uses)
PY

printf '%s\n' '--- deprecation or migration references ---'
rg -n -i 'deprecat|backward.compat|breaking.change|migration|package root|root import|switchyard_rust\.(components|core|translation)' \
  CHANGELOG.md README.md INSTALLATION.md CONTRIBUTING.md AGENTS.md .github switchyard_rust switchyard tests pyproject.toml \
  2>/dev/null | sed -n '1,240p' || true

Repository: NVIDIA-NeMo/Switchyard

Length of output: 2547


Add an explicit deprecation plan before removing the package-root exports. The previous root exported 34 public names. No current callers use them, but their removal is still a public Python API reduction.

🤖 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 `@switchyard_rust/__init__.py` at line 4, Before removing the package-root
exports from switchyard_rust, add an explicit deprecation plan for the 34
previously public names. Preserve the current exports during the deprecation
period and document the planned removal timeline and migration path for callers.

Source: Coding guidelines

Comment thread switchyard/__init__.py
Comment on lines +4 to +14
"""Switchyard's Python launcher and libsy bindings."""

from importlib import metadata as _metadata
from typing import TYPE_CHECKING, Any

from switchyard.lib.backends import (
AnthropicNativeBackend,
OpenAiNativeBackend,
)
from switchyard.lib.backends.llm_target import (
BackendFormat,
LlmTarget,
)
from switchyard.lib.chat_request import (
AnthropicChatRequest,
OpenAIChatRequest,
ResponsesChatRequest,
)
from switchyard.lib.chat_response import (
AnthropicChatResponse,
AnthropicResponseStream,
AnthropicStreamingChatResponse,
AnyResponseStream,
CompletionChatResponse,
ResponsesApiChatResponse,
ResponsesApiStream,
ResponsesApiStreamingChatResponse,
ResponseStream,
StreamingChatResponse,
)
from switchyard.lib.processors.rl_logging_request_processor import RlLoggingRequestProcessor
from switchyard.lib.processors.rl_logging_response_processor import RlLoggingResponseProcessor
from switchyard.lib.request_metadata import RequestMetadata
from switchyard.lib.roles import (
LLMBackend,
)
from switchyard.lib.route_table import RouteTable
from switchyard.lib.switchyard import Switchyard
from switchyard_rust.components import RandomRoutingProcessorConfig
from switchyard_rust.core import (
ChatRequest,
ChatRequestType,
ChatResponse,
ChatResponseType,
)
from switchyard_rust.translation import TranslationEngine

if TYPE_CHECKING:
from switchyard.lib.endpoints.anthropic_messages_endpoint import (
AnthropicMessagesEndpoint,
)
from switchyard.lib.endpoints.models_endpoint import ModelsEndpoint
from switchyard.lib.endpoints.openai_chat_endpoint import (
OpenAIChatEndpoint,
)
from switchyard.lib.endpoints.responses_endpoint import ResponsesEndpoint
from switchyard.server.switchyard_app import build_switchyard_app


def __getattr__(name: str) -> Any:
"""Lazy-load optional server exports that require the ``server`` extra."""
if name == "OpenAIChatEndpoint":
from switchyard.lib.endpoints.openai_chat_endpoint import (
OpenAIChatEndpoint,
)

return OpenAIChatEndpoint
if name == "AnthropicMessagesEndpoint":
from switchyard.lib.endpoints.anthropic_messages_endpoint import (
AnthropicMessagesEndpoint,
)

return AnthropicMessagesEndpoint
if name == "ResponsesEndpoint":
from switchyard.lib.endpoints.responses_endpoint import ResponsesEndpoint

return ResponsesEndpoint
if name == "ModelsEndpoint":
from switchyard.lib.endpoints.models_endpoint import ModelsEndpoint

return ModelsEndpoint
if name == "build_switchyard_app":
from switchyard.server.switchyard_app import build_switchyard_app

return build_switchyard_app
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")


__all__ = [
# ChatRequest types
"AnthropicChatRequest",
"ChatRequest",
"ChatRequestType",
"OpenAIChatRequest",
"ResponsesChatRequest",
# Chain infrastructure
"Switchyard",
"LLMBackend",
"AnthropicNativeBackend",
"OpenAiNativeBackend",
"OpenAIChatEndpoint",
"AnthropicMessagesEndpoint",
"ResponsesEndpoint",
"ModelsEndpoint",
"build_switchyard_app",
# Route dispatch table
"RouteTable",
"RequestMetadata",
"RlLoggingRequestProcessor",
"RlLoggingResponseProcessor",
# Random Routing usage case
"BackendFormat",
"RandomRoutingProcessorConfig",
"LlmTarget",
# Deterministic (LLM-classifier) routing usage case
# Translation engine
"TranslationEngine",
# ChatResponse types
"AnthropicChatResponse",
"ChatResponse",
"ChatResponseType",
"CompletionChatResponse",
"StreamingChatResponse",
"ResponsesApiChatResponse",
"ResponsesApiStreamingChatResponse",
"AnthropicStreamingChatResponse",
"ResponseStream",
"ResponsesApiStream",
"AnthropicResponseStream",
"AnyResponseStream",
]

try:
__version__ = _metadata.version("nemo-switchyard")
except _metadata.PackageNotFoundError:
# A source checkout may not have installed distribution metadata.
__version__ = "0.0.0+unknown"

__all__ = ["__version__"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- changed files ---'
git diff --stat
git diff --name-status

printf '%s\n' '--- relevant file inventory ---'
git ls-files \
  'switchyard/__init__.py' \
  'switchyard/cli/command_utils.py' \
  'switchyard_rust/libsy.py' \
  'pyproject.toml' \
  'setup.cfg' \
  'mypy.ini' \
  'docs/**' \
  'tests/**' | sed -n '1,240p'

printf '%s\n' '--- package initializer ---'
cat -n switchyard/__init__.py

printf '%s\n' '--- command utilities ---'
cat -n switchyard/cli/command_utils.py | sed -n '1,180p'

printf '%s\n' '--- Rust binding module ---'
cat -n switchyard_rust/libsy.py | sed -n '1,180p'

printf '%s\n' '--- public symbol references ---'
rg -n --hidden \
  -g '!\\.git/**' \
  -e 'quiet_dependency_loggers' \
  -e 'SwitchyardRuntimeError' \
  -e 'LibsyError' \
  -e 'from switchyard import|import switchyard' \
  -e '__all__' .

Repository: NVIDIA-NeMo/Switchyard

Length of output: 23947


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- repository state ---'
git status --short
git log --oneline -5

printf '%s\n' '--- parent versions of the affected files ---'
for file in switchyard/__init__.py switchyard/cli/command_utils.py switchyard_rust/libsy.py; do
  printf '\n--- %s at HEAD^ ---\n' "$file"
  git show "HEAD^:$file" 2>&1 | sed -n '1,180p' || true
done

printf '%s\n' '--- package-level and submodule exports ---'
for file in switchyard/libsy/__init__.py switchyard/cli/launch_command.py pyproject.toml docs/reference/rust_api.md CONTRIBUTING.md; do
  if test -f "$file"; then
    printf '\n--- %s ---\n' "$file"
    cat -n "$file" | sed -n '1,220p'
  fi
done

printf '%s\n' '--- historical declarations of the named symbols ---'
git grep -n -E 'quiet_dependency_loggers|SwitchyardRuntimeError|LibsyError' HEAD^ -- \
  switchyard switchyard_rust crates tests docs 2>/dev/null | sed -n '1,240p'

Repository: NVIDIA-NeMo/Switchyard

Length of output: 47483


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- commit metadata ---'
git show -s --format=fuller HEAD

printf '%s\n' '--- affected commit diff ---'
git diff --find-renames HEAD^ HEAD -- \
  switchyard/__init__.py \
  switchyard/cli/command_utils.py \
  switchyard/cli/switchyard_cli.py \
  switchyard_rust/libsy.py \
  switchyard_rust/core.py \
  switchyard_rust/__init__.py \
  crates/switchyard-py/src/errors.rs \
  docs \
  CHANGELOG.md

printf '%s\n' '--- current Rust Python exports and exception declarations ---'
for file in switchyard_rust/__init__.py switchyard_rust/core.py crates/switchyard-py/src/errors.rs switchyard/cli/switchyard_cli.py; do
  printf '\n--- %s ---\n' "$file"
  cat -n "$file" | sed -n '1,220p'
done

printf '%s\n' '--- release and migration references ---'
rg -n --hidden -g '!\\.git/**' -i \
  -e 'deprecated|deprecat|breaking|migration|compatib|server stack|0\\.2\\.0|quiet_dependency_loggers|SwitchyardRuntimeError' \
  README.md CHANGELOG.md docs CONTRIBUTING.md pyproject.toml switchyard switchyard_rust crates .github 2>/dev/null | sed -n '1,260p'

Repository: NVIDIA-NeMo/Switchyard

Length of output: 50380


Add an explicit migration plan for the removed Python APIs.

This commit removes package-level exports, quiet_dependency_loggers, and the SwitchyardRuntimeError base of LibsyError. Existing imports and exception handlers can fail. Retain compatibility shims through a documented removal release, or document this as a breaking release with symbol-level replacement guidance in CHANGELOG.md.

📍 Affects 3 files
  • switchyard/__init__.py#L4-L14 (this comment)
  • switchyard/cli/command_utils.py#L16-L16
  • switchyard_rust/libsy.py#L43-L43
🤖 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 `@switchyard/__init__.py` around lines 4 - 14, Document this as a breaking
release in CHANGELOG.md, including symbol-level migration guidance for the
removed package-level exports, quiet_dependency_loggers, and
SwitchyardRuntimeError base. Cover switchyard/__init__.py,
switchyard/cli/command_utils.py, and switchyard_rust/libsy.py; no direct code
changes are required at these sites unless you choose to retain compatibility
shims through a documented removal release.

Source: Coding guidelines

Comment on lines 88 to 91
def silence_launch_loggers(*, local_logger: logging.Logger) -> None:
"""Keep dependency chatter out of a child process terminal UI."""
for noisy in (
"switchyard",
"httpx",
"httpcore",
"openai",
"anthropic",
):
logging.getLogger(noisy).setLevel(logging.WARNING)
logging.getLogger("switchyard").setLevel(logging.WARNING)
local_logger.setLevel(logging.INFO)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Dependency log records can still reach stderr and corrupt the terminal UI.

silence_launch_loggers now sets the level only on the switchyard logger. Records from httpx, httpcore, openai, and anthropic still propagate to the root logger. configure_debug_file_logging removes every root handler at lines 67-69, so Python falls back to logging.lastResort, which writes to stderr at WARNING and above. A single dependency warning then prints over the PTY terminal UI. The removed dependency logger levels previously prevented this.

The docstring also states the function keeps dependency chatter out of the terminal UI, but the body no longer configures any dependency logger.

Attach a logging.NullHandler to the root logger, or raise the root level, so the last-resort handler never writes to stderr. This keeps the change small and does not restore per-dependency configuration.

🔧 Proposed fix to stop last-resort output

Apply in configure_debug_file_logging, after the root handlers are cleared:

     root = logging.getLogger()
     for handler in root.handlers[:]:
         root.removeHandler(handler)
         handler.close()
     root.setLevel(logging.WARNING)
+    # Without a root handler, logging falls back to lastResort and writes
+    # dependency warnings to stderr, which corrupts the PTY terminal UI.
+    root.addHandler(logging.NullHandler())

Then align the docstring with the new scope:

 def silence_launch_loggers(*, local_logger: logging.Logger) -> None:
-    """Keep dependency chatter out of a child process terminal UI."""
+    """Lower switchyard log output so it does not disturb the terminal UI."""
     logging.getLogger("switchyard").setLevel(logging.WARNING)
     local_logger.setLevel(logging.INFO)
🤖 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 `@switchyard/cli/launchers/launcher_runtime.py` around lines 88 - 91, Update
configure_debug_file_logging to attach a logging.NullHandler to the root logger
immediately after clearing its handlers, preventing logging.lastResort from
writing dependency warnings to stderr. Revise silence_launch_loggers’ docstring
to accurately describe the terminal-logging behavior and new scope.

@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.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/workflows/ci.yml (1)

216-227: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Validate the top-level command allowlist.

The current checks reject selected removed commands but do not prove that switchyard --help exposes only launch. An additional stale top-level command can pass both checks.

  • .github/workflows/ci.yml#L216-L227: run switchyard --help and assert that launch is the only available subcommand.
  • tests/test_cli_reference_docs.py#L50-L56: assert that _subparsers(_build_parser()) has exactly {"launch"}.
🤖 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 @.github/workflows/ci.yml around lines 216 - 227, The CLI validation must
prove that the top-level command allowlist contains only launch. In
.github/workflows/ci.yml lines 216-227, add a switchyard --help check that
confirms launch is the sole available subcommand; in
tests/test_cli_reference_docs.py lines 50-56, assert that
_subparsers(_build_parser()) equals {"launch"}.
🧹 Nitpick comments (1)
crates/switchyard-py/src/libsy_bindings.rs (1)

31-39: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider naming the failing header in the error message.

The http crate errors are generic, for example "invalid HTTP header name". A caller that passes many headers cannot tell which entry failed. Include the header name in the message. Do not include the header value, because header values often carry API keys.

try_append is safe here. The input is a HashMap, so keys are unique, and try_append fails only when the map exceeds its maximum size.

♻️ Proposed error-context improvement
     for (name, value) in headers {
-        let name = HeaderName::from_bytes(name.as_bytes())
-            .map_err(|error| PyValueError::new_err(error.to_string()))?;
-        let value = HeaderValue::from_str(value)
-            .map_err(|error| PyValueError::new_err(error.to_string()))?;
+        let header_name = HeaderName::from_bytes(name.as_bytes())
+            .map_err(|error| PyValueError::new_err(format!("header {name}: {error}")))?;
+        // Never include the header value in the error: values can carry credentials.
+        let value = HeaderValue::from_str(value)
+            .map_err(|error| PyValueError::new_err(format!("header {name}: {error}")))?;
         result
-            .try_append(name, value)
-            .map_err(|error| PyValueError::new_err(error.to_string()))?;
+            .try_append(header_name, value)
+            .map_err(|error| PyValueError::new_err(format!("header {name}: {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/switchyard-py/src/libsy_bindings.rs` around lines 31 - 39, Update the
header parsing errors in the loop over headers to include the failing header
name alongside the underlying error message, for both HeaderName::from_bytes and
HeaderValue::from_str failures. Do not include the header value, and preserve
the existing try_append handling.
🤖 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 `@AGENTS.md`:
- Line 142: Update the architecture and project-structure fenced blocks in
AGENTS.md to use the text language identifier on both opening fences, resolving
the markdownlint MD040 violations while leaving their contents unchanged.

In `@crates/switchyard-py/src/errors.rs`:
- Around line 10-19: Add a deprecation and migration record for the removed PyO3
exports, covering SwitchyardRuntimeError and the typed Switchyard*Error classes,
before finalizing the new LibsyError-only API in register and py_libsy_error.
Document the replacement behavior and migration path using the repository’s
established deprecation-plan location and format.

In `@crates/switchyard-server/README.md`:
- Around line 168-169: Update the README latency definition around
switchyard_total_latency_ms to state that timing ends after the wrapped stream
completes, not when the stream handle arrives. Keep the response-completion
wording consistent with stream completion and replace “connection accept” with
“connection acceptance.”

In `@docs/routing_algorithms/stage_router_routing.md`:
- Around line 154-157: Replace the universal lowest-threshold guidance in the
benchmark calibration section with picker-specific rules: for capable_first,
lower confidence_threshold only enough to rescue RESCUE without over-escalating
harmful LOSS downgrades; for efficient_first, calibrate it to increase
escalation for beneficial LOSS cases while avoiding unnecessary RESCUE changes.
Keep the corroborative scorer context and require selecting the best threshold
independently for each picker mode.

In `@switchyard_rust/__init__.py`:
- Line 4: Before removing the package-root exports from switchyard_rust, add an
explicit deprecation plan for the 34 previously public names. Preserve the
current exports during the deprecation period and document the planned removal
timeline and migration path for callers.

In `@switchyard/__init__.py`:
- Around line 4-14: Document this as a breaking release in CHANGELOG.md,
including symbol-level migration guidance for the removed package-level exports,
quiet_dependency_loggers, and SwitchyardRuntimeError base. Cover
switchyard/__init__.py, switchyard/cli/command_utils.py, and
switchyard_rust/libsy.py; no direct code changes are required at these sites
unless you choose to retain compatibility shims through a documented removal
release.

In `@switchyard/cli/launchers/launcher_runtime.py`:
- Around line 88-91: Update configure_debug_file_logging to attach a
logging.NullHandler to the root logger immediately after clearing its handlers,
preventing logging.lastResort from writing dependency warnings to stderr. Revise
silence_launch_loggers’ docstring to accurately describe the terminal-logging
behavior and new scope.

In `@switchyard/cli/launchers/shell_tui.py`:
- Around line 172-175: Snapshot a bounded footer height once per paint in the
shell TUI layout flow, and use that same value for footer rendering, shell row
calculation, scroll-region setup, and child PTY sizing. When the snapshot
differs from the previous layout, update TIOCSWINSZ and the scroll region
immediately rather than waiting for an outer-terminal resize. Add a regression
test that changes footer height between paints and verifies the child PTY and
rendered layout stay synchronized.
- Around line 388-392: Update the stdin forwarding loop around os.read and
master_fd so partial writes and BlockingIOError from os.write are handled
without losing bytes. Maintain a pending-output buffer, append newly read stdin
data, and monitor master_fd for writability until all buffered bytes are
written, while preserving the existing EOF break behavior.
- Around line 316-419: Refactor ShellTUI.run and its blocking event loop to an
async lifecycle, replacing the background footer thread and blocking
select/waits with asyncio-compatible tasks and awaits while preserving terminal
setup, child I/O, cleanup, and exit-code behavior. Update synchronous launcher
boundaries that invoke run to call the async lifecycle through asyncio.run(),
and adjust related lifecycle helpers to remain consistently awaitable.

---

Outside diff comments:
In @.github/workflows/ci.yml:
- Around line 216-227: The CLI validation must prove that the top-level command
allowlist contains only launch. In .github/workflows/ci.yml lines 216-227, add a
switchyard --help check that confirms launch is the sole available subcommand;
in tests/test_cli_reference_docs.py lines 50-56, assert that
_subparsers(_build_parser()) equals {"launch"}.

---

Nitpick comments:
In `@crates/switchyard-py/src/libsy_bindings.rs`:
- Around line 31-39: Update the header parsing errors in the loop over headers
to include the failing header name alongside the underlying error message, for
both HeaderName::from_bytes and HeaderValue::from_str failures. Do not include
the header value, and preserve the existing try_append handling.
🪄 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: 8b113f39-f5b8-46c9-9c29-1081242baa7a

📥 Commits

Reviewing files that changed from the base of the PR and between fb3fc30 and 5e6be08.

⛔ Files ignored due to path filters (2)
  • Cargo.lock is excluded by !**/*.lock, !Cargo.lock
  • uv.lock is excluded by !**/*.lock, !uv.lock
📒 Files selected for processing (239)
  • .agents/skills/switchyard-stage-router-scorer/SKILL.md
  • .github/ISSUE_TEMPLATE/bug_report.md
  • .github/workflows/ci.yml
  • .github/workflows/perf.yml
  • AGENTS.md
  • CHANGELOG.md
  • Cargo.toml
  • INSTALLATION.md
  • README.md
  • benchmark/README.md
  • benchmark/score_staged_run.py
  • crates/libsy-llm-client/src/client.rs
  • crates/libsy-llm-client/src/error.rs
  • crates/switchyard-components/Cargo.toml
  • crates/switchyard-components/src/backends/anthropic.rs
  • crates/switchyard-components/src/backends/common.rs
  • crates/switchyard-components/src/backends/context_overflow.rs
  • crates/switchyard-components/src/backends/mod.rs
  • crates/switchyard-components/src/backends/multi.rs
  • crates/switchyard-components/src/backends/openai.rs
  • crates/switchyard-components/src/backends/selection.rs
  • crates/switchyard-components/src/backends/stats.rs
  • crates/switchyard-components/src/contracts/backend.rs
  • crates/switchyard-components/src/contracts/context.rs
  • crates/switchyard-components/src/contracts/error.rs
  • crates/switchyard-components/src/contracts/ids.rs
  • crates/switchyard-components/src/contracts/mod.rs
  • crates/switchyard-components/src/contracts/roles.rs
  • crates/switchyard-components/src/contracts/types.rs
  • crates/switchyard-components/src/dimension_collector/mod.rs
  • crates/switchyard-components/src/dimension_collector/response/checks.rs
  • crates/switchyard-components/src/dimension_collector/response/mod.rs
  • crates/switchyard-components/src/dimension_collector/tool_signals.rs
  • crates/switchyard-components/src/lib.rs
  • crates/switchyard-components/src/request_processors/dimension_collector.rs
  • crates/switchyard-components/src/request_processors/mod.rs
  • crates/switchyard-components/src/request_processors/random_routing.rs
  • crates/switchyard-components/src/request_processors/stats.rs
  • crates/switchyard-components/src/response_processors/mod.rs
  • crates/switchyard-components/src/response_processors/response_signals.rs
  • crates/switchyard-components/src/response_processors/stats.rs
  • crates/switchyard-components/src/stage_router.rs
  • crates/switchyard-components/src/stats/accumulator.rs
  • crates/switchyard-components/src/stats/cache_eligibility.rs
  • crates/switchyard-components/src/stats/context.rs
  • crates/switchyard-components/src/stats/cost.rs
  • crates/switchyard-components/src/stats/mod.rs
  • crates/switchyard-components/src/stats/usage.rs
  • crates/switchyard-components/src/telemetry.rs
  • crates/switchyard-components/tests/adversarial_multi_llm_backend.rs
  • crates/switchyard-components/tests/adversarial_native_backends.rs
  • crates/switchyard-components/tests/adversarial_random_routing.rs
  • crates/switchyard-components/tests/contracts.rs
  • crates/switchyard-components/tests/stats_accumulator.rs
  • crates/switchyard-components/tests/stats_processors.rs
  • crates/switchyard-components/tests/stats_usage_shapes.rs
  • crates/switchyard-components/tests/support/config.rs
  • crates/switchyard-components/tests/support/mod.rs
  • crates/switchyard-py/Cargo.toml
  • crates/switchyard-py/src/component_bindings.rs
  • crates/switchyard-py/src/component_bindings/backends.rs
  • crates/switchyard-py/src/component_bindings/config.rs
  • crates/switchyard-py/src/component_bindings/dimension_collector.rs
  • crates/switchyard-py/src/component_bindings/request_processors.rs
  • crates/switchyard-py/src/component_bindings/response_processors.rs
  • crates/switchyard-py/src/component_bindings/stage_router.rs
  • crates/switchyard-py/src/component_bindings/stats.rs
  • crates/switchyard-py/src/errors.rs
  • crates/switchyard-py/src/interop.rs
  • crates/switchyard-py/src/interop/context.rs
  • crates/switchyard-py/src/interop/request.rs
  • crates/switchyard-py/src/interop/response.rs
  • crates/switchyard-py/src/interop/roles.rs
  • crates/switchyard-py/src/interop/subagent.rs
  • crates/switchyard-py/src/lib.rs
  • crates/switchyard-py/src/libsy_bindings.rs
  • crates/switchyard-py/src/py_serde.rs
  • crates/switchyard-py/src/translation.rs
  • crates/switchyard-server/README.md
  • docs/cli_reference.md
  • docs/getting_started.md
  • docs/internal/metrics_reference.md
  • docs/routing_algorithms/stage_router_routing.md
  • examples/minimal.py
  • examples/route.yaml
  • examples/utils.py
  • pyproject.toml
  • switchyard/__init__.py
  • switchyard/cli/command_utils.py
  • switchyard/cli/launchers/claude_code_launcher.py
  • switchyard/cli/launchers/codex_cli_launcher.py
  • switchyard/cli/launchers/cost_estimator.py
  • switchyard/cli/launchers/launcher_runtime.py
  • switchyard/cli/launchers/live_stats_footer.py
  • switchyard/cli/launchers/openclaw_launcher.py
  • switchyard/cli/launchers/session_summary.py
  • switchyard/cli/launchers/shell_tui.py
  • switchyard/cli/model_catalog/__init__.py
  • switchyard/cli/model_catalog/model_discovery.py
  • switchyard/cli/route_bundle.py
  • switchyard/cli/switchyard_cli.py
  • switchyard/lib/__init__.py
  • switchyard/lib/backends/__init__.py
  • switchyard/lib/backends/anthropic_native_llm_backend.py
  • switchyard/lib/backends/backend_format_resolver.py
  • switchyard/lib/backends/llm_target.py
  • switchyard/lib/backends/multi_llm_backend.py
  • switchyard/lib/backends/openai_llm_backend.py
  • switchyard/lib/backends/openai_native_backend.py
  • switchyard/lib/backends/stats_llm_backend.py
  • switchyard/lib/chat_request/__init__.py
  • switchyard/lib/chat_request/anthropic.py
  • switchyard/lib/chat_request/base.py
  • switchyard/lib/chat_request/openai_chat.py
  • switchyard/lib/chat_request/openai_responses.py
  • switchyard/lib/chat_response/__init__.py
  • switchyard/lib/chat_response/anthropic.py
  • switchyard/lib/chat_response/base.py
  • switchyard/lib/chat_response/openai_chat.py
  • switchyard/lib/chat_response/openai_responses.py
  • switchyard/lib/chat_response/streaming_response_accumulator.py
  • switchyard/lib/conversation_turn.py
  • switchyard/lib/endpoints/__init__.py
  • switchyard/lib/endpoints/anthropic_messages_endpoint.py
  • switchyard/lib/endpoints/base.py
  • switchyard/lib/endpoints/dispatch.py
  • switchyard/lib/endpoints/error_envelope.py
  • switchyard/lib/endpoints/models_endpoint.py
  • switchyard/lib/endpoints/openai_chat_endpoint.py
  • switchyard/lib/endpoints/outcome_metrics.py
  • switchyard/lib/endpoints/prometheus_emitter.py
  • switchyard/lib/endpoints/responses_endpoint.py
  • switchyard/lib/endpoints/route_selection.py
  • switchyard/lib/endpoints/routing_log_stats_endpoint.py
  • switchyard/lib/endpoints/sse_helpers.py
  • switchyard/lib/endpoints/stats_endpoint.py
  • switchyard/lib/endpoints/upstream_error.py
  • switchyard/lib/endpoints/upstream_error_log.py
  • switchyard/lib/llm_client.py
  • switchyard/lib/model_listing.py
  • switchyard/lib/processors/__init__.py
  • switchyard/lib/processors/format_translate.py
  • switchyard/lib/processors/model_rewrite_request_processor.py
  • switchyard/lib/processors/rl_logging_request_processor.py
  • switchyard/lib/processors/rl_logging_response_processor.py
  • switchyard/lib/processors/routing_log_response_processor.py
  • switchyard/lib/processors/stats_request_processor.py
  • switchyard/lib/processors/stats_response_processor_accumulator.py
  • switchyard/lib/prometheus_exposition.py
  • switchyard/lib/proxy_context.py
  • switchyard/lib/request_metadata.py
  • switchyard/lib/roles.py
  • switchyard/lib/route_table.py
  • switchyard/lib/startup_timing.py
  • switchyard/lib/stats_accumulator.py
  • switchyard/lib/switchyard.py
  • switchyard/lib/tracing.py
  • switchyard/server/__init__.py
  • switchyard/server/server_util.py
  • switchyard/server/switchyard_app.py
  • switchyard/telemetry.py
  • switchyard_rust/__init__.py
  • switchyard_rust/_native.py
  • switchyard_rust/components.py
  • switchyard_rust/components.pyi
  • switchyard_rust/core.py
  • switchyard_rust/libsy.py
  • switchyard_rust/server.py
  • switchyard_rust/translation.py
  • tests/_chain_test_helpers.py
  • tests/conftest.py
  • tests/contract/__init__.py
  • tests/contract/test_platform_imports.py
  • tests/contract/test_proxy_context.py
  • tests/contract/test_request_response_types.py
  • tests/e2e/_helpers.py
  • tests/e2e/conftest.py
  • tests/e2e/test_passthrough_e2e.py
  • tests/e2e/test_passthrough_responses_e2e.py
  • tests/e2e_multiturn_responses.py
  • tests/getting_started/test_getting_started.py
  • tests/readme/test_readme.py
  • tests/test_anthropic_native_llm_backend.py
  • tests/test_anthropic_openai_translation.py
  • tests/test_anthropic_output_config_strip.py
  • tests/test_anthropic_probe.py
  • tests/test_backend_format_resolver.py
  • tests/test_build_and_serve.py
  • tests/test_chat_request.py
  • tests/test_chat_response.py
  • tests/test_cli_reference_docs.py
  • tests/test_codex_multiturn_traces.py
  • tests/test_context_error_translation.py
  • tests/test_context_window_exceeded_endpoint.py
  • tests/test_cost_estimator_gemini.py
  • tests/test_endpoint_state_contract.py
  • tests/test_error_source_annotation.py
  • tests/test_format_translate_processor.py
  • tests/test_inference_e2e.py
  • tests/test_infra.py
  • tests/test_init_all_exports.py
  • tests/test_launchers.py
  • tests/test_live_stats_footer.py
  • tests/test_llm_client.py
  • tests/test_metrics_endpoint.py
  • tests/test_no_stale_module_paths.py
  • tests/test_outcome_metrics.py
  • tests/test_prometheus_emitter.py
  • tests/test_prometheus_exposition.py
  • tests/test_python_server_passthrough.py
  • tests/test_request_metadata.py
  • tests/test_request_translation_engine.py
  • tests/test_request_translation_engine_to_any_of.py
  • tests/test_response_translation_engine.py
  • tests/test_responses_openai_translation.py
  • tests/test_rl_logging.py
  • tests/test_rl_logging_e2e.py
  • tests/test_route_bundle.py
  • tests/test_route_selection_headers.py
  • tests/test_route_table.py
  • tests/test_routing_log_response_processor.py
  • tests/test_shell_tui.py
  • tests/test_sse_stream_close.py
  • tests/test_stats_accumulator.py
  • tests/test_stream_close_chain.py
  • tests/test_stream_leak_repro.py
  • tests/test_switchyard.py
  • tests/test_switchyard_app_factory.py
  • tests/test_switchyard_app_lifecycle.py
  • tests/test_switchyard_rust_component_bindings.py
  • tests/test_switchyard_rust_core_bindings.py
  • tests/test_telemetry.py
  • tests/test_tool_result_signal_collector.py
  • tests/test_tracing.py
  • tests/test_translation_engine_chaos.py
  • tests/test_upstream_error_log.py
  • tests/test_upstream_error_passthrough.py
  • tests/translation/__init__.py
  • tests/translation/test_format_fidelity_contract.py
💤 Files with no reviewable changes (106)
  • Cargo.toml
  • switchyard/lib/backends/openai_llm_backend.py
  • crates/switchyard-components/src/response_processors/mod.rs
  • switchyard/lib/init.py
  • .agents/skills/switchyard-stage-router-scorer/SKILL.md
  • switchyard/lib/chat_request/base.py
  • switchyard/lib/chat_request/openai_responses.py
  • switchyard/lib/chat_request/anthropic.py
  • crates/switchyard-components/src/dimension_collector/mod.rs
  • switchyard/lib/backends/anthropic_native_llm_backend.py
  • switchyard/lib/chat_response/openai_chat.py
  • switchyard/lib/endpoints/routing_log_stats_endpoint.py
  • crates/switchyard-components/src/contracts/roles.rs
  • switchyard/lib/chat_response/openai_responses.py
  • switchyard/lib/chat_response/anthropic.py
  • switchyard/cli/model_catalog/model_discovery.py
  • switchyard/lib/backends/openai_native_backend.py
  • benchmark/README.md
  • switchyard/lib/backends/multi_llm_backend.py
  • crates/switchyard-components/src/request_processors/dimension_collector.rs
  • switchyard/lib/backends/stats_llm_backend.py
  • crates/switchyard-py/src/component_bindings/request_processors.rs
  • crates/switchyard-py/src/interop/roles.rs
  • crates/switchyard-components/src/contracts/context.rs
  • crates/switchyard-py/src/interop.rs
  • crates/switchyard-components/src/backends/context_overflow.rs
  • crates/switchyard-components/Cargo.toml
  • crates/switchyard-py/src/component_bindings.rs
  • examples/route.yaml
  • crates/switchyard-components/src/stats/usage.rs
  • crates/switchyard-components/src/backends/stats.rs
  • crates/switchyard-py/src/component_bindings/stage_router.rs
  • crates/switchyard-py/Cargo.toml
  • crates/switchyard-py/src/component_bindings/config.rs
  • switchyard/lib/chat_request/openai_chat.py
  • crates/switchyard-components/tests/adversarial_native_backends.rs
  • crates/switchyard-py/src/interop/request.rs
  • crates/switchyard-py/src/lib.rs
  • crates/switchyard-components/src/contracts/backend.rs
  • switchyard/lib/endpoints/dispatch.py
  • crates/switchyard-components/src/backends/multi.rs
  • switchyard/cli/model_catalog/init.py
  • switchyard/lib/endpoints/responses_endpoint.py
  • switchyard/lib/chat_response/base.py
  • switchyard/lib/endpoints/error_envelope.py
  • crates/switchyard-components/tests/contracts.rs
  • crates/switchyard-components/src/response_processors/stats.rs
  • crates/switchyard-components/tests/adversarial_random_routing.rs
  • crates/switchyard-py/src/interop/subagent.rs
  • crates/switchyard-py/src/interop/context.rs
  • crates/switchyard-components/src/contracts/error.rs
  • crates/switchyard-components/src/dimension_collector/tool_signals.rs
  • crates/switchyard-py/src/component_bindings/dimension_collector.rs
  • crates/switchyard-components/src/contracts/types.rs
  • switchyard/lib/conversation_turn.py
  • crates/switchyard-components/src/dimension_collector/response/mod.rs
  • switchyard/lib/endpoints/models_endpoint.py
  • switchyard/lib/endpoints/route_selection.py
  • crates/switchyard-components/src/stats/mod.rs
  • switchyard/lib/endpoints/prometheus_emitter.py
  • crates/switchyard-components/src/backends/mod.rs
  • crates/switchyard-components/tests/support/config.rs
  • crates/switchyard-components/src/lib.rs
  • switchyard/cli/route_bundle.py
  • crates/switchyard-components/src/backends/selection.rs
  • switchyard/lib/endpoints/base.py
  • crates/switchyard-components/src/stats/accumulator.rs
  • examples/utils.py
  • crates/switchyard-components/src/response_processors/response_signals.rs
  • crates/switchyard-components/src/request_processors/random_routing.rs
  • switchyard/lib/chat_request/init.py
  • crates/switchyard-components/src/stats/cache_eligibility.rs
  • switchyard/lib/endpoints/outcome_metrics.py
  • crates/switchyard-py/src/translation.rs
  • switchyard/lib/chat_response/init.py
  • crates/switchyard-components/src/request_processors/stats.rs
  • crates/switchyard-components/tests/adversarial_multi_llm_backend.rs
  • benchmark/score_staged_run.py
  • switchyard/lib/endpoints/anthropic_messages_endpoint.py
  • switchyard/lib/endpoints/openai_chat_endpoint.py
  • switchyard/lib/backends/backend_format_resolver.py
  • crates/switchyard-py/src/component_bindings/response_processors.rs
  • crates/switchyard-components/src/backends/anthropic.rs
  • switchyard/lib/endpoints/init.py
  • crates/switchyard-components/src/contracts/mod.rs
  • crates/switchyard-py/src/component_bindings/stats.rs
  • crates/switchyard-components/src/telemetry.rs
  • crates/switchyard-components/tests/stats_accumulator.rs
  • crates/switchyard-py/src/interop/response.rs
  • switchyard/lib/backends/init.py
  • crates/switchyard-components/src/stats/context.rs
  • crates/switchyard-py/src/component_bindings/backends.rs
  • crates/switchyard-components/src/contracts/ids.rs
  • crates/switchyard-components/src/stage_router.rs
  • switchyard/lib/chat_response/streaming_response_accumulator.py
  • crates/switchyard-components/src/backends/common.rs
  • examples/minimal.py
  • crates/switchyard-components/tests/support/mod.rs
  • crates/switchyard-components/src/backends/openai.rs
  • crates/switchyard-py/src/py_serde.rs
  • crates/switchyard-components/src/dimension_collector/response/checks.rs
  • switchyard/lib/backends/llm_target.py
  • crates/switchyard-components/src/stats/cost.rs
  • crates/switchyard-components/tests/stats_processors.rs
  • crates/switchyard-components/src/request_processors/mod.rs
  • crates/switchyard-components/tests/stats_usage_shapes.rs
🛑 Comments failed to post (3)
switchyard/cli/launchers/shell_tui.py (3)

172-175: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Synchronize dynamic footer height with the child PTY.

footer_height can change between the reads on Lines 172-175. The child PTY size is set at startup on Line 337 and only updated during an outer-terminal resize on Lines 301-304.

If the footer grows during a session, footer rendering reserves fewer shell rows but the child still writes for its old PTY height. The child can overwrite the footer.

Snapshot one bounded footer height per paint. When that height changes, update TIOCSWINSZ, the scroll region, and footer rendering from the same layout value. Add a regression test that changes footer height between paints. Based on learnings: “Define verifiable success criteria, write regression tests for bugs and new validation, and verify each step.”

🤖 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 `@switchyard/cli/launchers/shell_tui.py` around lines 172 - 175, Snapshot a
bounded footer height once per paint in the shell TUI layout flow, and use that
same value for footer rendering, shell row calculation, scroll-region setup, and
child PTY sizing. When the snapshot differs from the previous layout, update
TIOCSWINSZ and the scroll region immediately rather than waiting for an
outer-terminal resize. Add a regression test that changes footer height between
paints and verifies the child PTY and rendered layout stay synchronized.

Source: Learnings


316-419: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline switchyard/cli/launchers/shell_tui.py --items all
rg -n -C 3 'ShellTUI\(|\.run\(\)|asyncio\.run|async def' \
  switchyard/cli tests

Repository: NVIDIA-NeMo/Switchyard

Length of output: 9368


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- shell_tui.py structure and run implementation ---'
ast-grep outline switchyard/cli/launchers/shell_tui.py --items all
sed -n '1,120p' switchyard/cli/launchers/shell_tui.py
sed -n '300,430p' switchyard/cli/launchers/shell_tui.py

printf '%s\n' '--- launcher call contexts ---'
sed -n '130,215p' switchyard/cli/launchers/claude_code_launcher.py
sed -n '165,215p' switchyard/cli/launchers/codex_cli_launcher.py
sed -n '215,250p' switchyard/cli/launchers/openclaw_launcher.py

printf '%s\n' '--- async and asyncio.run usage in launcher paths ---'
rg -n -C 4 'def launch|def run|async def|asyncio\.run|ShellTUI' switchyard/cli tests

Repository: NVIDIA-NeMo/Switchyard

Length of output: 30795


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import ast
from pathlib import Path

target = Path("switchyard/cli/launchers/shell_tui.py")
tree = ast.parse(target.read_text())

run = next(
    node for node in ast.walk(tree)
    if isinstance(node, ast.FunctionDef) and node.name == "run"
)
calls = {
    node.func.attr if isinstance(node.func, ast.Attribute) else getattr(node.func, "id", "")
    for node in ast.walk(run)
    if isinstance(node, ast.Call)
}
print(f"run_is_async={isinstance(run, ast.AsyncFunctionDef)}")
print(f"run_calls_select={bool({'select'} & calls)}")
print(f"run_constructs_thread={any(isinstance(n, ast.Call) and isinstance(n.func, ast.Attribute) and n.func.attr == 'Thread' for n in ast.walk(run))}")

for path in sorted(Path("switchyard/cli").rglob("*.py")):
    source = path.read_text()
    if "ShellTUI" not in source:
        continue
    module = ast.parse(source)
    matches = []
    for node in ast.walk(module):
        if isinstance(node, ast.Call):
            text = ast.unparse(node)
            if ".run()" in text or "tui.run()" in text:
                matches.append((node.lineno, text))
    if matches:
        print(path)
        for lineno, text in matches:
            print(f"  {lineno}: {text}")
    print(f"  asyncio.run_present={'asyncio.run' in source}")
PY

Repository: NVIDIA-NeMo/Switchyard

Length of output: 905


Use an async lifecycle for ShellTUI.

run is synchronous and owns a blocking select loop plus a background thread. Refactor it to async APIs. Use asyncio.run() at synchronous launcher boundaries.

🤖 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 `@switchyard/cli/launchers/shell_tui.py` around lines 316 - 419, Refactor
ShellTUI.run and its blocking event loop to an async lifecycle, replacing the
background footer thread and blocking select/waits with asyncio-compatible tasks
and awaits while preserving terminal setup, child I/O, cleanup, and exit-code
behavior. Update synchronous launcher boundaries that invoke run to call the
async lifecycle through asyncio.run(), and adjust related lifecycle helpers to
remain consistently awaitable.

Source: Coding guidelines


388-392: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

python - <<'PY'
import fcntl
import os
import pty

master_fd, slave_fd = pty.openpty()
try:
    flags = fcntl.fcntl(master_fd, fcntl.F_GETFL)
    fcntl.fcntl(master_fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
    payload = b"x" * 1_000_000
    try:
        written = os.write(master_fd, payload)
        print(f"requested={len(payload)} written={written}")
    except BlockingIOError:
        print("write raised BlockingIOError")
finally:
    os.close(master_fd)
    os.close(slave_fd)
PY

Repository: NVIDIA-NeMo/Switchyard

Length of output: 516


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="switchyard/cli/launchers/shell_tui.py"
printf '%s\n' '--- relevant source ---'
sed -n '300,430p' "$file"
printf '%s\n' '--- select/read/write references ---'
rg -n -C 3 'select|os\.read|os\.write|master_fd|stdin_fd|BlockingIOError|pending' "$file"
printf '%s\n' '--- callers and tests ---'
rg -n -C 3 'ShellTUI|shell_tui|launch_shell|footer_height' switchyard tests 2>/dev/null || true

Repository: NVIDIA-NeMo/Switchyard

Length of output: 26536


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import errno
import fcntl
import os

read_fd, write_fd = os.pipe()
try:
    flags = fcntl.fcntl(write_fd, fcntl.F_GETFL)
    fcntl.fcntl(write_fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)

    # Fill the nonblocking output queue, then issue the same kind of write
    # used by the launcher.
    while True:
        try:
            os.write(write_fd, b"x" * 65536)
        except BlockingIOError:
            break

    payload = b"y" * 65536
    try:
        written = os.write(write_fd, payload)
        print(f"nonblocking_write=requested:{len(payload)} written:{written}")
    except BlockingIOError as exc:
        print(f"nonblocking_write=requested:{len(payload)} error:{exc.errno == errno.EAGAIN}")

    print("source_write_has_retry_or_exception_handling=false")
finally:
    os.close(read_fd)
    os.close(write_fd)
PY

printf '%s\n' '--- helper implementation and focused tests ---'
sed -n '55,80p' switchyard/cli/launchers/shell_tui.py
sed -n '330,405p' tests/test_shell_tui.py

Repository: NVIDIA-NeMo/Switchyard

Length of output: 3815


Retain stdin bytes that the nonblocking PTY cannot accept.

os.write(master_fd, data) can raise BlockingIOError or write only part of data. Buffer unwritten bytes and monitor master_fd for writability until the buffer is empty.

🤖 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 `@switchyard/cli/launchers/shell_tui.py` around lines 388 - 392, Update the
stdin forwarding loop around os.read and master_fd so partial writes and
BlockingIOError from os.write are handled without losing bytes. Maintain a
pending-output buffer, append newly read stdin data, and monitor master_fd for
writability until all buffered bytes are written, while preserving the existing
EOF break behavior.

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.

3 participants