Skip to content

feat(advisor): review-gate advisor backend and advisor route type - #359

Closed
eric-liu-nvidia wants to merge 5 commits into
mainfrom
claude/advisor-strategy-review-gate-b60577
Closed

feat(advisor): review-gate advisor backend and advisor route type#359
eric-liu-nvidia wants to merge 5 commits into
mainfrom
claude/advisor-strategy-review-gate-b60577

Conversation

@eric-liu-nvidia

@eric-liu-nvidia eric-liu-nvidia commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

What

Re-do of the port/advisor-strategy branch keeping only the review-gate advisor strategy, rebased onto latest main. An AdvisorLoopBackend pairs the executor with a stronger advisor that reviews the executor's first no-tool-call turn once per session: APPROVE returns the turn unchanged; REDO feeds the advisor's plan back and re-invokes the executor. Exposed as a type: advisor YAML route.

Five focused commits:

  • fix(backends): forward native Anthropic request bodies verbatim — native Anthropic requests early-return from outbound_body (model rewrite + extra_body only); strip/normalize now applies only to translated bodies. Preserves context_management, thinking blocks, message-level system turns, and client tool ids for real Anthropic clients.
  • feat(stats): routing-log sink for proxy-internal usage recordsregister_routing_log_sink / emit_auxiliary_record so multi-call backends can price advisor consults and REDO-discarded turns into the same routing log (snapshot_session aggregates them for free).
  • feat(processors): restore reasoning-effort normalizer — restored verbatim from pre-refactor(python): remove legacy routing profiles #268 main; wired only into advisor routes (Claude Code's /effort xhigh breaks some executor upstreams).
  • feat(advisor): review-gate advisor backend and advisor route type — the main change.
  • chore(benchmark): support host network mode in run-baseline.shSWITCHYARD_DOCKER_NETWORK_MODE=host for upstreams only routable from the host (VPN / corp-internal gateways).

Why

Benchmarks showed the review gate is the only functional advisor method; the tool_call strategy suppressed the executor's own test-and-iterate loop (premature "done"). This PR ports the validated review-gate implementation (including all follow-up fixes: session-key stabilization, per-instance review caps, budget refunds, gateway cache buckets, reasoning-only turns) onto main, dropping tool_call entirely.

Because #268 removed the Python profiles layer since the original branch, the code is re-homed rather than cherry-picked: AdvisorConfig/prompts/presets moved from switchyard/lib/profiles/ to switchyard/lib/backends/, AdvisorProfileConfig is gone, and the type: advisor route builds AdvisorLoopBackend(config, stats_accumulator=stats) directly in route_bundle.py.

How tested

  • uv run ruff check . clean
  • uv run mypy switchyard clean (strict, 89 files)
  • uv run pytest tests/ green — 947 passed, 9 skipped, hermetic (-m "not integration", provider keys unset); includes the 53 ported loop-backend tests plus new route-bundle and config suites
  • cargo fmt --all --check, cargo clippy --workspace --all-targets -- -D warnings, cargo test --workspace all clean (toolchain 1.96.1)
  • Manual smoke: built a type: advisor bundle via build_route_bundle_table and verified the chain shape (StatsRequestProcessor → ReasoningEffortNormalizer → AdvisorLoopBackend → StatsResponseProcessor → TranslationEngine)

Checklist

  • One class per file; filename = snake_case of the primary class.
  • New public symbols exported from switchyard/__init__.py.__all__ (AdvisorConfig, AdvisorLoopBackend, AdvisorPresets).
  • Unit tests added for new components / bug fixes.
  • README / --help unchanged surface; AGENTS.md and examples/route.yaml updated for the new route type.
  • Commits signed off (Signed-off-by) per the DCO.

Notes for reviewers

  • No strategy config key. type: advisor is the review gate. Existing benchmark YAMLs carrying strategy: review_gate must drop that line (fails validation with an unknown-key error), and old type: advisor routes that relied on the tool_call default now build the review gate.
  • The Rust passthrough commit is a global behavior change for every native-Anthropic route, not just advisor ones — it inverts 5 pre-existing adversarial tests. It rides in this PR because the review-gate benchmark was validated with it; it is a separate commit so it can be discussed/reverted independently.
  • AdvisorLoopBackend is Python-only and must not be wrapped in StatsLlmBackend (raises TypeError); the bundle builder injects the accumulator through the constructor, and the backend self-accounts advisor usage into the classifier bucket. enable_stats now gates only that backend-internal accounting (main no longer has a per-route stats-processor toggle).
  • The routing-log sink additions were re-applied onto main's post-refactor(python): remove legacy routing profiles #268 version of routing_log_response_processor.py (kept CTX_REQUEST_HEADERS and the ctx.selected_target tier).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added advisor routes that review and approve or revise model responses.
    • Added configurable advisor settings, presets, review limits, triggers, and failure handling.
    • Added reasoning-effort normalization for broader request compatibility.
    • Added routing-log support for advisor activity and usage reporting.
  • Bug Fixes
    • Native Anthropic requests now preserve request content while updating only the routed model.
    • Dockerized benchmark runs support host and bridge networking modes.
  • Documentation
    • Added advisor route configuration and usage guidance.

Signed-off-by: zengyuanl <zengyuanl@nvidia.com>
Signed-off-by: zengyuanl <zengyuanl@nvidia.com>
Signed-off-by: zengyuanl <zengyuanl@nvidia.com>
Signed-off-by: zengyuanl <zengyuanl@nvidia.com>
Signed-off-by: zengyuanl <zengyuanl@nvidia.com>
@eric-liu-nvidia
eric-liu-nvidia requested a review from a team as a code owner August 11, 2026 16:40
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The PR adds advisor-gated executor routes with configurable reviews, retries, streaming, statistics, logging, and presets. It also preserves native Anthropic request bodies, normalizes reasoning effort, supports Docker network modes, and adds documentation and tests.

Changes

Advisor routing

Layer / File(s) Summary
Advisor configuration and public contracts
switchyard/lib/backends/advisor_config.py, switchyard/lib/backends/advisor_prompts.py, switchyard/lib/backends/advisor_presets.py, switchyard/__init__.py, tests/test_advisor_config.py
Adds validated advisor configuration, prompts, presets, public exports, and configuration tests.
Advisor review loop
switchyard/lib/backends/advisor_loop_backend.py, tests/test_advisor_loop_backend.py
Adds advisor consultation, approve/redo handling, streaming replay, session budgets, seed advice, transcript handling, usage accounting, and audit records.
Advisor route wiring and observability
switchyard/cli/route_bundle.py, switchyard/cli/launchers/launcher_runtime.py, switchyard/lib/processors/reasoning_effort_normalizer.py, switchyard/lib/processors/routing_log_response_processor.py, switchyard/cli/switchyard_cli.py, tests/test_route_bundle.py, tests/test_reasoning_effort_normalizer.py
Registers advisor routes, validates tiers, constructs backends, normalizes reasoning effort, exposes strategy summaries, and records auxiliary usage.
Advisor route documentation
AGENTS.md, examples/route.yaml
Documents advisor routes, review flow, project structure, and configuration examples.

Native Anthropic passthrough

Layer / File(s) Summary
Native request passthrough
crates/switchyard-components/src/backends/anthropic.rs, crates/switchyard-components/tests/adversarial_native_backends.rs
Preserves native Anthropic fields, message ordering, structured content, tool-use IDs, and thinking blocks while rewriting the routed model.

Docker network mode configuration

Layer / File(s) Summary
Baseline container networking
benchmark/run-baseline.sh
Selects host networking or the existing bridge configuration from SWITCHYARD_DOCKER_NETWORK_MODE.

Estimated code review effort: 4 (Complex) | ~60 minutes

Poem

I’m a rabbit reviewing the gate,
Approve or redo, the turns now wait.
Native thoughts stay in their place,
Routes and logs keep steady pace.
Host or bridge, the containers hop—
Clean new paths from start to stop.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.94% 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 main changes: the review-gate advisor backend and the advisor route type.
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: 11

🧹 Nitpick comments (11)
tests/test_advisor_config.py (1)

41-55: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add tests for the two uncovered gate validators.

advisor_config.py adds _pattern_compiles (Lines 167-175) and _pattern_trigger_requires_pattern (Lines 177-183). No test in this file exercises either path. Both are new validation rules that reject invalid operator configuration.

As per coding guidelines: "Write unit tests for new roles and bug fixes."

🧪 Proposed tests
     def test_accepts_mixed_wire_tiers(self) -> None:
         mixed = _config(advisor={"model": "deepseek/deepseek-r2", "base_url": "http://adv.test",
                                  "api_key": "k", "format": "openai"})
         assert mixed.advisor.format == BackendFormat.OPENAI
         assert mixed.executor.format == BackendFormat.ANTHROPIC
+
+    def test_rejects_invalid_gate_trigger_pattern(self) -> None:
+        with pytest.raises(pydantic.ValidationError, match="not a valid regex"):
+            _config(gate_trigger_pattern="task_complete[")
+
+    def test_pattern_trigger_requires_pattern(self) -> None:
+        with pytest.raises(pydantic.ValidationError, match="non-empty gate_trigger_pattern"):
+            _config(gate_trigger="pattern")
+
+    def test_pattern_trigger_accepts_pattern(self) -> None:
+        cfg = _config(gate_trigger="pattern", gate_trigger_pattern=r'task_complete["\s>:]*true')
+        assert cfg.gate_trigger == "pattern"
🤖 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 `@tests/test_advisor_config.py` around lines 41 - 55, Add unit tests in
test_advisor_config.py covering both validator paths: verify an invalid pattern
is rejected by _pattern_compiles, and verify a trigger configuration requiring a
pattern is rejected by _pattern_trigger_requires_pattern. Assert
pydantic.ValidationError and match the relevant validation message, using the
existing _config helper and test style.

Source: Coding guidelines

switchyard/lib/backends/advisor_config.py (2)

167-175: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider caching the compiled pattern.

_pattern_compiles compiles the regex and discards it. The advisor loop must recompile the pattern on every gate check unless it caches it separately. model_config already sets arbitrary_types_allowed=True, so the model can hold a compiled pattern.

The ast-grep ReDoS hint on Line 171 is a false positive here: gate_trigger_pattern is operator configuration, not caller-supplied request data.

🤖 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/lib/backends/advisor_config.py` around lines 167 - 175, Update the
advisor configuration model around _pattern_compiles to retain the compiled
regex instead of discarding it, using a model field or private attribute
compatible with the existing arbitrary_types_allowed setting. Ensure the advisor
loop reuses this cached pattern for gate checks while preserving validation
errors for invalid gate_trigger_pattern values.

Source: Linters/SAST tools


150-155: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

_target_model_non_empty may be unreachable.

The test at tests/test_advisor_config.py lines 35-39 states that coerce_llm_target rejects empty model ids during coercion, before this validator runs. If that holds, this validator never raises and the error message it defines is never observable. Either remove it or keep it as a documented defense with a comment that explains why it stays.

🤖 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/lib/backends/advisor_config.py` around lines 150 - 155, Resolve
the redundant _target_model_non_empty validator by removing it if
coerce_llm_target already rejects empty model identifiers; otherwise retain it
only with a concise comment documenting its defense-in-depth purpose and why it
remains necessary.
switchyard/lib/processors/routing_log_response_processor.py (1)

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

Do not call the private _append from outside the class.

emit_auxiliary_record is a module-level function. It reaches into sink._append, a private method. Add a public method on RoutingLogResponseProcessor and call that instead. The private name then stays free to change.

♻️ Proposed public method
     def _write_record(self, ctx: ProxyContext, served_model: str, response: ChatResponse) -> None:
         ...
-        self._append(record)
+        self.append_record(record)
 
-    def _append(self, record: dict[str, Any]) -> None:
+    def append_record(self, record: dict[str, Any]) -> None:
         """Append one record as a JSON line; write failures never propagate."""
🤖 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/lib/processors/routing_log_response_processor.py` at line 193, Add
a public record-emission method to RoutingLogResponseProcessor that delegates to
its internal append behavior, then update the module-level emit_auxiliary_record
function to call that public method instead of sink._append. Keep _append
private and preserve the existing record contents and emission behavior.
switchyard/lib/backends/advisor_loop_backend.py (4)

610-615: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the redundant local imports.

BackendFormat is already imported at Line 89 and resolve_llm_target at Line 92. The function-local re-imports at Lines 612-613 add no cycle protection.

♻️ Proposed cleanup
 def _build_advisor_caller(config: AdvisorConfig) -> AdvisorCaller:
     """Build the advisor caller for ``config.advisor``, dispatched on its format."""
-    from switchyard.lib.backends.llm_target import BackendFormat
-    from switchyard.lib.backends.multi_llm_backend import resolve_llm_target
-
     target = resolve_llm_target(config.advisor)
🤖 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/lib/backends/advisor_loop_backend.py` around lines 610 - 615,
Remove the redundant local imports of BackendFormat and resolve_llm_target from
_build_advisor_caller, reusing the existing module-level imports instead; leave
the target resolution logic unchanged.

1042-1051: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Route the audit records through the logger.

_audit_seed and _audit_review write directly to sys.stderr and flush on every call. This bypasses log level control, structured log routing, and log capture in tests. reply_head also puts model reply text on stderr with no way to disable it.

Use log.info(...) with the same JSON payload so operators can control the output.

Also applies to: 1228-1252

🤖 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/lib/backends/advisor_loop_backend.py` around lines 1042 - 1051,
Update _audit_seed and _audit_review to emit the existing JSON payload through
log.info(...) instead of writing to or flushing sys.stderr, preserving the
advisor_seed/advisor_review record contents. Also replace reply_head’s direct
stderr output with the logger so model reply text follows the same controllable
logging path.

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

Wrap these two lines to stay within 100 characters.

Line 718 is about 104 characters and line 803 is about 102 characters.

As per coding guidelines: "Keep lines within 100 characters."

♻️ Proposed wrapping
-        content = getattr(getattr(choices[0], "message", None), "content", None) if choices else None
+        message = getattr(choices[0], "message", None) if choices else None
+        content = getattr(message, "content", None)
-    has_tool_use = bool(message.get("tool_calls")) or choice.get("finish_reason") == "tool_calls"
+    has_tool_use = (
+        bool(message.get("tool_calls")) or choice.get("finish_reason") == "tool_calls"
+    )

Also applies to: 803-803

🤖 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/lib/backends/advisor_loop_backend.py` at line 718, Wrap the long
`content = getattr(...)` expression in the relevant backend code so each line
stays within 100 characters, preserving its existing behavior and applying the
same formatting to the corresponding line near the second occurrence.

Source: Coding guidelines


174-192: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Per-instance session state grows without bound.

_stall_fired, _seed_advice, _sessions_seen, _reviews_by_scope, _failed_consults_by_scope, and _budget_logged_scopes only ever gain entries. The module docstring states the design target is a long-lived gateway shared by many tasks, so a benchmark campaign adds one or more entries per session and never releases them. _seed_advice holds full advice text, so its growth is the largest.

Bound these maps, for example with a fixed-capacity FIFO/LRU eviction keyed by insertion order.

🤖 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/lib/backends/advisor_loop_backend.py` around lines 174 - 192,
Bound all per-instance session state collections—_stall_fired, _seed_advice,
_sessions_seen, _reviews_by_scope, _failed_consults_by_scope, and
_budget_logged_scopes—with a fixed-capacity FIFO or LRU eviction policy keyed by
insertion order. Ensure new session keys evict the oldest entries once capacity
is reached, while preserving existing lookups, budget behavior, and seed advice
semantics.
tests/test_advisor_loop_backend.py (2)

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

Prefer pytest-mock for the non-HTTP fakes.

The fakes use unittest.mock.MagicMock and AsyncMock. The HTTP paths correctly use respx.

As per path instructions for tests/**/*.py: "Use respx for HTTP mocking and pytest-mock for general mocking."

Also applies to: 96-114

🤖 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 `@tests/test_advisor_loop_backend.py` at line 16, Replace the unittest.mock
MagicMock and AsyncMock usage in the advisor loop backend tests with
pytest-mock’s mocker fixture, while preserving the existing fake behavior and
respx-based HTTP mocking. Update the affected setup and test cases around the
imported mocks and the referenced lines to use mocker-created synchronous and
asynchronous mocks.

Source: Path instructions


80-81: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Wrap the over-length assertion and fixture lines.

Lines 81, 217, 218, 500, 502, and 503 each exceed 100 characters. Lines 516, 523, and 527 are also at or over the limit.

As per coding guidelines: "Keep lines within 100 characters."

♻️ Example wrapping for Lines 217-218
-    assert any(m.get("role") == "assistant" and m.get("content") == "I think I'm done" for m in redo_msgs)
-    assert any(m.get("role") == "user" and "empty-input case" in (m.get("content") or "") for m in redo_msgs)
+    assert any(
+        m.get("role") == "assistant" and m.get("content") == "I think I'm done"
+        for m in redo_msgs
+    )
+    assert any(
+        m.get("role") == "user" and "empty-input case" in (m.get("content") or "")
+        for m in redo_msgs
+    )

Also applies to: 217-218, 500-503

🤖 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 `@tests/test_advisor_loop_backend.py` around lines 80 - 81, Wrap the
over-100-character assertion and fixture lines in the test module, including the
event dictionary around the visible content_block_start fixture and the
referenced lines near 217–218, 500–503, 516, 523, and 527. Preserve the existing
test data and behavior while formatting each statement to stay within 100
characters.

Source: Coding guidelines

tests/test_route_bundle.py (1)

294-298: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Line 296 exceeds 100 characters.

Split the assignment from the type: ignore comment or shorten the subscript chain.

As per coding guidelines: "Keep lines within 100 characters."

♻️ Proposed wrapping
     def test_responses_format_rejected(self) -> None:
         bundle = _advisor_bundle()
-        bundle["routes"]["myrouter/advisor"]["executor"]["format"] = "responses"  # type: ignore[index]
+        route = bundle["routes"]["myrouter/advisor"]  # type: ignore[index]
+        route["executor"]["format"] = "responses"  # type: ignore[index]
         with pytest.raises(RouteBundleConfigError, match="responses"):
             build_route_bundle_table(bundle)
🤖 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 `@tests/test_route_bundle.py` around lines 294 - 298, Shorten the assignment in
test_responses_format_rejected so every line remains within 100 characters,
while preserving the type: ignore[index] suppression and the existing bundle
mutation behavior.

Source: Coding guidelines

🤖 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`:
- Around line 262-263: Correct the description above the switchyard serve
command to state that examples/route.yaml serves only the passthrough and noop
routes, leaving the commented-out advisor-gate configuration unchanged.
- Around line 199-200: Add separate project-tree entries for advisor_prompts.py
and advisor_presets.py alongside advisor_config.py in the documented advisor
file map, and remove their parenthetical mention from the advisor_config.py
description.

In `@benchmark/run-baseline.sh`:
- Around line 885-897: Replace the binary if/else around
SWITCHYARD_DOCKER_NETWORK_MODE with an explicit host|bridge case and reject
unsupported values before constructing Docker arguments. Document
SWITCHYARD_DOCKER_NETWORK_MODE and its accepted host and bridge values in
usage(), and add dry-run coverage for both valid modes plus invalid input.
- Around line 885-890: Update the host-network branch in run-baseline.sh to
require an explicitly supplied --harbor-server-url, or derive and validate a
host-reachable URL before proceeding; otherwise fail fast with a clear error.
Preserve the existing --network host behavior, ensure bridge-only network
arguments are omitted, and add dry-run coverage for host mode and the
missing-URL failure using the contract in prepare_harbor_dataset.py and usage().

In `@switchyard/cli/launchers/launcher_runtime.py`:
- Around line 135-145: Annotate the empty tiers dictionary in the advisor branch
of the route-summary logic with its string-to-optional-string type so strict
mypy accepts it. Also preserve the route’s default_model fallback when executor
or advisor lacks a model, rather than returning a summary containing None;
update the tier resolution in this branch while retaining the existing
executor/advisor summary format.

In `@switchyard/cli/switchyard_cli.py`:
- Around line 36-46: Update RoutingLogResponseProcessor.emit_auxiliary_record so
its _append file write and lock wait run off the event loop, using
asyncio.to_thread or the existing async writer mechanism; preserve _lock
serialization and ensure callers correctly await or schedule the asynchronous
auxiliary write.

In `@switchyard/lib/backends/advisor_loop_backend.py`:
- Around line 759-772: Update the executor usage accounting in _completion_usage
and _consume_anthropic_stream to reuse the cache-inclusive Anthropic token
helper used by _advisor_usage, so cache_creation_input_tokens is included in
prompt/input token totals for both streamed and non-streamed paths. Preserve
existing OpenAI handling and cached-token reporting.

In `@switchyard/lib/backends/advisor_presets.py`:
- Line 53: Update the base_url parameter docstring in the relevant preset
definition to describe the Anthropic Messages endpoint rather than an
OpenAI-compatible gateway, matching the BackendFormat.ANTHROPIC configuration
and module documentation.

In `@switchyard/lib/processors/reasoning_effort_normalizer.py`:
- Around line 29-36: Update _VALID_REASONING_EFFORT in
switchyard/lib/processors/reasoning_effort_normalizer.py:29-36 to include none
and minimal so both values pass through unchanged. Extend the parametrized
pass-through test in tests/test_reasoning_effort_normalizer.py:40-43 with
minimal and none, while retaining super-mega as the unknown-value case.

In `@switchyard/lib/processors/routing_log_response_processor.py`:
- Around line 173-206: Make emit_auxiliary_record asynchronous and offload
sink._append to a worker thread with asyncio.to_thread, matching the existing
process implementation. Update every call in advisor_loop_backend.py, including
async call(...), to await emit_auxiliary_record(...); preserve the current
record contents and no-sink behavior.

In `@tests/test_advisor_config.py`:
- Around line 18-26: Annotate the _config function’s overrides parameter with an
appropriate mapping type compatible with AdvisorConfig keyword arguments, while
preserving the existing return annotation and behavior so strict mypy no longer
treats the function as partially annotated.

---

Nitpick comments:
In `@switchyard/lib/backends/advisor_config.py`:
- Around line 167-175: Update the advisor configuration model around
_pattern_compiles to retain the compiled regex instead of discarding it, using a
model field or private attribute compatible with the existing
arbitrary_types_allowed setting. Ensure the advisor loop reuses this cached
pattern for gate checks while preserving validation errors for invalid
gate_trigger_pattern values.
- Around line 150-155: Resolve the redundant _target_model_non_empty validator
by removing it if coerce_llm_target already rejects empty model identifiers;
otherwise retain it only with a concise comment documenting its defense-in-depth
purpose and why it remains necessary.

In `@switchyard/lib/backends/advisor_loop_backend.py`:
- Around line 610-615: Remove the redundant local imports of BackendFormat and
resolve_llm_target from _build_advisor_caller, reusing the existing module-level
imports instead; leave the target resolution logic unchanged.
- Around line 1042-1051: Update _audit_seed and _audit_review to emit the
existing JSON payload through log.info(...) instead of writing to or flushing
sys.stderr, preserving the advisor_seed/advisor_review record contents. Also
replace reply_head’s direct stderr output with the logger so model reply text
follows the same controllable logging path.
- Line 718: Wrap the long `content = getattr(...)` expression in the relevant
backend code so each line stays within 100 characters, preserving its existing
behavior and applying the same formatting to the corresponding line near the
second occurrence.
- Around line 174-192: Bound all per-instance session state
collections—_stall_fired, _seed_advice, _sessions_seen, _reviews_by_scope,
_failed_consults_by_scope, and _budget_logged_scopes—with a fixed-capacity FIFO
or LRU eviction policy keyed by insertion order. Ensure new session keys evict
the oldest entries once capacity is reached, while preserving existing lookups,
budget behavior, and seed advice semantics.

In `@switchyard/lib/processors/routing_log_response_processor.py`:
- Line 193: Add a public record-emission method to RoutingLogResponseProcessor
that delegates to its internal append behavior, then update the module-level
emit_auxiliary_record function to call that public method instead of
sink._append. Keep _append private and preserve the existing record contents and
emission behavior.

In `@tests/test_advisor_config.py`:
- Around line 41-55: Add unit tests in test_advisor_config.py covering both
validator paths: verify an invalid pattern is rejected by _pattern_compiles, and
verify a trigger configuration requiring a pattern is rejected by
_pattern_trigger_requires_pattern. Assert pydantic.ValidationError and match the
relevant validation message, using the existing _config helper and test style.

In `@tests/test_advisor_loop_backend.py`:
- Line 16: Replace the unittest.mock MagicMock and AsyncMock usage in the
advisor loop backend tests with pytest-mock’s mocker fixture, while preserving
the existing fake behavior and respx-based HTTP mocking. Update the affected
setup and test cases around the imported mocks and the referenced lines to use
mocker-created synchronous and asynchronous mocks.
- Around line 80-81: Wrap the over-100-character assertion and fixture lines in
the test module, including the event dictionary around the visible
content_block_start fixture and the referenced lines near 217–218, 500–503, 516,
523, and 527. Preserve the existing test data and behavior while formatting each
statement to stay within 100 characters.

In `@tests/test_route_bundle.py`:
- Around line 294-298: Shorten the assignment in test_responses_format_rejected
so every line remains within 100 characters, while preserving the type:
ignore[index] suppression and the existing bundle mutation behavior.
🪄 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: 2161eae2-b736-4173-968d-93313d637cdb

📥 Commits

Reviewing files that changed from the base of the PR and between fb3fc30 and 8dced63.

📒 Files selected for processing (20)
  • AGENTS.md
  • benchmark/run-baseline.sh
  • crates/switchyard-components/src/backends/anthropic.rs
  • crates/switchyard-components/tests/adversarial_native_backends.rs
  • examples/route.yaml
  • switchyard/__init__.py
  • switchyard/cli/launchers/launcher_runtime.py
  • switchyard/cli/route_bundle.py
  • switchyard/cli/switchyard_cli.py
  • switchyard/lib/backends/__init__.py
  • switchyard/lib/backends/advisor_config.py
  • switchyard/lib/backends/advisor_loop_backend.py
  • switchyard/lib/backends/advisor_presets.py
  • switchyard/lib/backends/advisor_prompts.py
  • switchyard/lib/processors/reasoning_effort_normalizer.py
  • switchyard/lib/processors/routing_log_response_processor.py
  • tests/test_advisor_config.py
  • tests/test_advisor_loop_backend.py
  • tests/test_reasoning_effort_normalizer.py
  • tests/test_route_bundle.py

Comment thread AGENTS.md
Comment on lines +199 to +200
│ │ ├── advisor_loop_backend.py # AdvisorLoopBackend (advisor review gate)
│ │ ├── advisor_config.py # AdvisorConfig (+ advisor_prompts, advisor_presets)

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

List the rehomed advisor files explicitly.

Add separate tree entries for advisor_prompts.py and advisor_presets.py. The project structure is a file map, but the current entry lists only advisor_config.py and hides two modules in a parenthetical.

🤖 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` around lines 199 - 200, Add separate project-tree entries for
advisor_prompts.py and advisor_presets.py alongside advisor_config.py in the
documented advisor file map, and remove their parenthetical mention from the
advisor_config.py description.

Comment thread AGENTS.md
Comment on lines +262 to 263
# Serve the minimal Python YAML bundle (noop, passthrough, and advisor routes).
switchyard serve --routes examples/route.yaml --port 4000

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

Correct the route-bundle description.

examples/route.yaml keeps advisor-gate commented out at Lines 17-22. This command therefore serves only the passthrough and noop routes, not an advisor route. Change the description or enable the advisor route.

🤖 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` around lines 262 - 263, Correct the description above the
switchyard serve command to state that examples/route.yaml serves only the
passthrough and noop routes, leaving the commented-out advisor-gate
configuration unchanged.

Comment thread benchmark/run-baseline.sh
Comment on lines +885 to +890
if [[ "\${SWITCHYARD_DOCKER_NETWORK_MODE:-bridge}" == "host" ]]; then
# Host networking: for upstreams only routable from the host (VPN /
# corp-internal gateways that Docker bridge networks cannot reach).
# Pair with --harbor-server-url http://<host-ip>:<port> so task
# containers reach the server at the host address.
DOCKER_RUN_ARGS+=(--network host)

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 | 🟠 Major | 🏗️ Heavy lift

Require a reachable Harbor URL for host networking.

When SWITCHYARD_DOCKER_NETWORK_MODE=host, the server does not join ${SWITCHYARD_DOCKER_NETWORK} and does not publish ${SWITCHYARD_DOCKER_SERVICE_NAME} on the task network. The documented default for --harbor-server-url is the Dockerized Switchyard service. A host-mode run without that option can therefore leave Harbor unable to reach the server.

Fail fast unless --harbor-server-url is supplied, or derive a verified host-reachable URL. Add dry-run coverage for host mode, including the absence of bridge-only arguments.

The network contract is defined by benchmark/prepare_harbor_dataset.py and the usage() text in benchmark/run-baseline.sh.

🤖 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 `@benchmark/run-baseline.sh` around lines 885 - 890, Update the host-network
branch in run-baseline.sh to require an explicitly supplied --harbor-server-url,
or derive and validate a host-reachable URL before proceeding; otherwise fail
fast with a clear error. Preserve the existing --network host behavior, ensure
bridge-only network arguments are omitted, and add dry-run coverage for host
mode and the missing-URL failure using the contract in prepare_harbor_dataset.py
and usage().

Comment thread benchmark/run-baseline.sh
Comment on lines +885 to +897
if [[ "\${SWITCHYARD_DOCKER_NETWORK_MODE:-bridge}" == "host" ]]; then
# Host networking: for upstreams only routable from the host (VPN /
# corp-internal gateways that Docker bridge networks cannot reach).
# Pair with --harbor-server-url http://<host-ip>:<port> so task
# containers reach the server at the host address.
DOCKER_RUN_ARGS+=(--network host)
else
DOCKER_RUN_ARGS+=(
--network "\${SWITCHYARD_DOCKER_NETWORK}"
--network-alias "\${SWITCHYARD_DOCKER_SERVICE_NAME}"
-p "127.0.0.1:$(q "${PORT}"):$(q "${PORT}")"
)
fi

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

Validate and document SWITCHYARD_DOCKER_NETWORK_MODE.

The else branch treats every value except exact host as bridge. A typo or unsupported value can silently make a host-only upstream unreachable while the run continues with the wrong topology.

Use an explicit host|bridge case and reject other values. Document the variable and its accepted values in usage(). Add dry-run tests for both modes and invalid input.

The accepted-value contract should be visible in the user-facing usage() block.

🤖 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 `@benchmark/run-baseline.sh` around lines 885 - 897, Replace the binary if/else
around SWITCHYARD_DOCKER_NETWORK_MODE with an explicit host|bridge case and
reject unsupported values before constructing Docker arguments. Document
SWITCHYARD_DOCKER_NETWORK_MODE and its accepted host and bridge values in
usage(), and add dry-run coverage for both valid modes plus invalid input.

Comment on lines +135 to +145
if route_type == "advisor":
tiers = {}
for field in ("executor", "advisor"):
tier = route.get(field)
tiers[field] = (
tier.get("model") if isinstance(tier, _Mapping) else tier
)
return (
f"advisor: executor={tiers['executor']}, "
f"advisor={tiers['advisor']}"
)

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

Annotate tiers for strict mypy.

tiers = {} at Line 136 has no annotation and no initial values, so strict mypy reports var-annotated.

Also note the rendered summary shows executor=None, advisor=None when a route omits the tier models, because the branch returns before the route: {default_model} fallback.

As per coding guidelines: "Use type hints throughout; code must satisfy strict mypy checking."

🐛 Proposed fix
                 if route_type == "advisor":
-                    tiers = {}
+                    tiers: dict[str, object] = {}
                     for field in ("executor", "advisor"):
                         tier = route.get(field)
                         tiers[field] = (
                             tier.get("model") if isinstance(tier, _Mapping) else tier
                         )
🤖 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 135 - 145,
Annotate the empty tiers dictionary in the advisor branch of the route-summary
logic with its string-to-optional-string type so strict mypy accepts it. Also
preserve the route’s default_model fallback when executor or advisor lacks a
model, rather than returning a summary containing None; update the tier
resolution in this branch while retaining the existing executor/advisor summary
format.

Source: Coding guidelines

Comment on lines +759 to +772
def _completion_usage(body: Any, *, is_openai: bool) -> dict[str, int]:
"""Read ``_ExecTurn`` token counts from a non-streamed completion body."""
usage = body.get("usage") if isinstance(body, dict) else None
prompt_tokens, completion_tokens = _usage_tokens(usage)
details = usage if isinstance(usage, dict) else {}
if is_openai:
cached = (details.get("prompt_tokens_details") or {}).get("cached_tokens") or 0
else:
cached = details.get("cache_read_input_tokens") or 0
return {
"input_tokens": prompt_tokens or 0,
"output_tokens": completion_tokens or 0,
"cached_tokens": int(cached),
}

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 | 🟡 Minor | ⚡ Quick win

Executor token accounting drops Anthropic cache-creation tokens.

_completion_usage reads only input_tokens and cache_read_input_tokens for the Anthropic wire. _consume_anthropic_stream does the same at Line 740. _advisor_usage folds cache_creation_input_tokens into prompt_tokens and documents that this matches the routing-log processor's accounting.

Both records land in the same routing log through _emit_routing_usage. On gateways that auto-cache server-side (the case _advisor_usage documents at Line 1191), the review_gate_discarded record under-reports prompt tokens while the advisor_review record does not. Per-model cost attribution is then inconsistent between the two tiers.

Reuse the cache-inclusive helper for the executor path.

🐛 Proposed fix
 def _completion_usage(body: Any, *, is_openai: bool) -> dict[str, int]:
     """Read ``_ExecTurn`` token counts from a non-streamed completion body."""
     usage = body.get("usage") if isinstance(body, dict) else None
-    prompt_tokens, completion_tokens = _usage_tokens(usage)
-    details = usage if isinstance(usage, dict) else {}
-    if is_openai:
-        cached = (details.get("prompt_tokens_details") or {}).get("cached_tokens") or 0
-    else:
-        cached = details.get("cache_read_input_tokens") or 0
+    tokens = _advisor_usage(usage)
     return {
-        "input_tokens": prompt_tokens or 0,
-        "output_tokens": completion_tokens or 0,
-        "cached_tokens": int(cached),
+        "input_tokens": tokens["prompt_tokens"],
+        "output_tokens": tokens["completion_tokens"],
+        "cached_tokens": tokens["cached_tokens"],
     }

Apply the same inclusion in _consume_anthropic_stream so the streamed path reports cache_creation_input_tokens too.

📝 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
def _completion_usage(body: Any, *, is_openai: bool) -> dict[str, int]:
"""Read ``_ExecTurn`` token counts from a non-streamed completion body."""
usage = body.get("usage") if isinstance(body, dict) else None
prompt_tokens, completion_tokens = _usage_tokens(usage)
details = usage if isinstance(usage, dict) else {}
if is_openai:
cached = (details.get("prompt_tokens_details") or {}).get("cached_tokens") or 0
else:
cached = details.get("cache_read_input_tokens") or 0
return {
"input_tokens": prompt_tokens or 0,
"output_tokens": completion_tokens or 0,
"cached_tokens": int(cached),
}
def _completion_usage(body: Any, *, is_openai: bool) -> dict[str, int]:
"""Read ``_ExecTurn`` token counts from a non-streamed completion body."""
usage = body.get("usage") if isinstance(body, dict) else None
tokens = _advisor_usage(usage)
return {
"input_tokens": tokens["prompt_tokens"],
"output_tokens": tokens["completion_tokens"],
"cached_tokens": tokens["cached_tokens"],
}
🤖 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/lib/backends/advisor_loop_backend.py` around lines 759 - 772,
Update the executor usage accounting in _completion_usage and
_consume_anthropic_stream to reuse the cache-inclusive Anthropic token helper
used by _advisor_usage, so cache_creation_input_tokens is included in
prompt/input token totals for both streamed and non-streamed paths. Preserve
existing OpenAI handling and cached-token reporting.


Args:
api_key: Inference Hub API key, used for both tiers (one tenancy).
base_url: OpenAI-compatible gateway base URL.

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

Correct the base_url docstring.

The docstring calls base_url an "OpenAI-compatible gateway base URL". Both tiers in this preset use BackendFormat.ANTHROPIC and the module comment describes the Anthropic Messages endpoint. Align the wording.

📝 Proposed docstring fix
-            base_url: OpenAI-compatible gateway base URL.
+            base_url: Gateway base URL for the Anthropic Messages endpoint.
📝 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
base_url: OpenAI-compatible gateway base URL.
base_url: Gateway base URL for the Anthropic Messages endpoint.
🤖 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/lib/backends/advisor_presets.py` at line 53, Update the base_url
parameter docstring in the relevant preset definition to describe the Anthropic
Messages endpoint rather than an OpenAI-compatible gateway, matching the
BackendFormat.ANTHROPIC configuration and module documentation.

Comment on lines +29 to +36
#: OpenAI-compatible values the upstream accepts. ``"max"`` is non-standard
#: but supported by NVIDIA Hub's LiteLLM for reasoning-budget overrides.
_VALID_REASONING_EFFORT = frozenset({"low", "medium", "high", "max"})

#: Aliases the upstream rejects → the nearest valid value.
_REASONING_EFFORT_ALIASES = {
"xhigh": "high",
}

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

The reasoning-effort allow-list omits the low-effort values. _VALID_REASONING_EFFORT lists only low, medium, high, and max, so minimal and none fall to the "high" default and the caller gets more reasoning than requested. The test suite locks in that behavior because it exercises only an obviously invalid value.

  • switchyard/lib/processors/reasoning_effort_normalizer.py#L29-L36: add none and minimal to _VALID_REASONING_EFFORT so they pass through unchanged.
  • tests/test_reasoning_effort_normalizer.py#L40-L43: extend the parametrized pass-through test with minimal and none, and keep super-mega as the unknown-value case.
📍 Affects 2 files
  • switchyard/lib/processors/reasoning_effort_normalizer.py#L29-L36 (this comment)
  • tests/test_reasoning_effort_normalizer.py#L40-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/lib/processors/reasoning_effort_normalizer.py` around lines 29 -
36, Update _VALID_REASONING_EFFORT in
switchyard/lib/processors/reasoning_effort_normalizer.py:29-36 to include none
and minimal so both values pass through unchanged. Extend the parametrized
pass-through test in tests/test_reasoning_effort_normalizer.py:40-43 with
minimal and none, while retaining super-mega as the unknown-value case.

Comment on lines +173 to +206
def emit_auxiliary_record(
*,
session_id: str | None,
task: str | None,
model: str,
tier: str,
prompt_tokens: int = 0,
cached_tokens: int = 0,
cache_creation_tokens: int = 0,
completion_tokens: int = 0,
) -> None:
"""Append a proxy-internal usage record to the registered routing log.

The record shape matches ``_write_record`` exactly, so
``snapshot_session`` (and with it ``/v1/routing/session-stats``)
aggregates chain-terminal and proxy-internal usage alike.
"""
sink = _AUX_SINK
if sink is None:
return
sink._append({
"ts": datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z",
"task": task,
"trial_id": None,
"session_id": session_id,
"model": model,
"tier": tier,
"prompt_tokens": prompt_tokens,
"cached_tokens": cached_tokens,
"cache_creation_tokens": cache_creation_tokens,
"completion_tokens": completion_tokens,
"reasoning_tokens": 0,
"total_tokens": prompt_tokens + completion_tokens,
})

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 | 🟠 Major | ⚡ Quick win

emit_auxiliary_record performs blocking file I/O synchronously.

_append calls json.dumps, acquires self._lock, opens the file, and writes. RoutingLogResponseProcessor.process deliberately avoids doing this inline: Line 52 offloads the identical work with asyncio.to_thread.

emit_auxiliary_record is a synchronous function. The advisor loop backend calls it from async call(...). The write then runs on the event loop thread. Two consequences follow:

  • The event loop stalls for the duration of the disk write on every advisor consult and every discarded executor turn.
  • The coroutine can block on self._lock while a to_thread worker holds it, which stalls the whole loop, not just one request.

Make the helper async and offload the write, matching the discipline in process.

As per coding guidelines: "Use async-only implementations; use asyncio.run() when synchronous execution is required."

⚡ Proposed fix to offload the write
-def emit_auxiliary_record(
+async def emit_auxiliary_record(
     *,
     session_id: str | None,
     task: str | None,
     model: str,
     tier: str,
     prompt_tokens: int = 0,
     cached_tokens: int = 0,
     cache_creation_tokens: int = 0,
     completion_tokens: int = 0,
 ) -> None:
     """Append a proxy-internal usage record to the registered routing log.
 
     The record shape matches ``_write_record`` exactly, so
     ``snapshot_session`` (and with it ``/v1/routing/session-stats``)
     aggregates chain-terminal and proxy-internal usage alike.
+
+    The write is offloaded to a worker thread, so the caller's event loop
+    never blocks on disk I/O or on the sink's write lock.
     """
     sink = _AUX_SINK
     if sink is None:
         return
-    sink._append({
+    record = {
         "ts": datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z",
         "task": task,
         "trial_id": None,
         "session_id": session_id,
         "model": model,
         "tier": tier,
         "prompt_tokens": prompt_tokens,
         "cached_tokens": cached_tokens,
         "cache_creation_tokens": cache_creation_tokens,
         "completion_tokens": completion_tokens,
         "reasoning_tokens": 0,
         "total_tokens": prompt_tokens + completion_tokens,
-    })
+    }
+    await asyncio.to_thread(sink.append_record, record)

Update the call sites in switchyard/lib/backends/advisor_loop_backend.py to await emit_auxiliary_record(...).

🤖 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/lib/processors/routing_log_response_processor.py` around lines 173
- 206, Make emit_auxiliary_record asynchronous and offload sink._append to a
worker thread with asyncio.to_thread, matching the existing process
implementation. Update every call in advisor_loop_backend.py, including async
call(...), to await emit_auxiliary_record(...); preserve the current record
contents and no-sink behavior.

Source: Coding guidelines

Comment on lines +18 to +26
def _config(**overrides) -> AdvisorConfig:
base: dict = {
"executor": {"model": "exec-model", "base_url": "http://exec.test", "api_key": "k",
"format": "anthropic"},
"advisor": {"model": "adv-model", "base_url": "http://adv.test", "api_key": "k",
"format": "anthropic"},
}
base.update(overrides)
return AdvisorConfig(**base)

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

Annotate **overrides.

_config annotates the return type but not the parameter. Strict mypy flags a partially annotated function. Add the parameter annotation.

As per coding guidelines: "Use type hints throughout; code must satisfy strict mypy checking."

🔧 Proposed annotation
-def _config(**overrides) -> AdvisorConfig:
-    base: dict = {
+def _config(**overrides: object) -> AdvisorConfig:
+    base: dict[str, object] = {
📝 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
def _config(**overrides) -> AdvisorConfig:
base: dict = {
"executor": {"model": "exec-model", "base_url": "http://exec.test", "api_key": "k",
"format": "anthropic"},
"advisor": {"model": "adv-model", "base_url": "http://adv.test", "api_key": "k",
"format": "anthropic"},
}
base.update(overrides)
return AdvisorConfig(**base)
def _config(**overrides: object) -> AdvisorConfig:
base: dict[str, object] = {
"executor": {"model": "exec-model", "base_url": "http://exec.test", "api_key": "k",
"format": "anthropic"},
"advisor": {"model": "adv-model", "base_url": "http://adv.test", "api_key": "k",
"format": "anthropic"},
}
base.update(overrides)
return AdvisorConfig(**base)
🧰 Tools
🪛 ast-grep (0.45.1)

[warning] 19-19: Do not make http calls without encryption
Context: "http://exec.test"
Note: [CWE-319] Cleartext Transmission of Sensitive Information.

(requests-http)


[warning] 21-21: Do not make http calls without encryption
Context: "http://adv.test"
Note: [CWE-319] Cleartext Transmission of Sensitive Information.

(requests-http)

🤖 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 `@tests/test_advisor_config.py` around lines 18 - 26, Annotate the _config
function’s overrides parameter with an appropriate mapping type compatible with
AdvisorConfig keyword arguments, while preserving the existing return annotation
and behavior so strict mypy no longer treats the function as partially
annotated.

Source: Coding guidelines

@eric-liu-nvidia
eric-liu-nvidia marked this pull request as draft August 11, 2026 18:50
@eric-liu-nvidia

Copy link
Copy Markdown
Contributor Author

Parking this as draft: #343 ("remove deprecated server stack") landed on main right after this PR was opened and deleted the substrate it builds on — the Python switchyard/lib layer, route_bundle.py, the Python server, and the switchyard-components crate. Every conflict is modify-here vs delete-on-main, so this cannot merge without effectively reverting #343.

The advisor review gate is being re-implemented natively on the Rust server stack (switchyard-server / libsy); a follow-up PR will supersede this one. This branch remains the validated Python reference implementation (benchmarked review_gate behavior + tests).

@eric-liu-nvidia

Copy link
Copy Markdown
Contributor Author

Superseded by #371: the advisor review gate re-implemented natively on the Rust stack (libsy AdvisorGate algorithm + type = "advisor" TOML route).

@eric-liu-nvidia

Copy link
Copy Markdown
Contributor Author

Closing in favor of #371, the native Rust implementation of the advisor review gate. This branch stays available as the validated Python reference (benchmarked behavior + the test contract the port was verified against); the Python serving stack it targets was removed from main by #343, so it can no longer merge.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant