Skip to content

fix: improve OpenRouter protocol interoperability - #237

Open
nachiketb-nvidia wants to merge 1 commit into
mainfrom
fix/openrouter-stream-interoperability
Open

fix: improve OpenRouter protocol interoperability#237
nachiketb-nvidia wants to merge 1 commit into
mainfrom
fix/openrouter-stream-interoperability

Conversation

@nachiketb-nvidia

@nachiketb-nvidia nachiketb-nvidia commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

What

  • prevent inbound compression negotiation from being forwarded to upstream LLM providers
  • accept optional [DONE] termination on Anthropic-compatible SSE streams
  • preserve OpenAI Chat usage chunks that arrive after the finish chunk
  • count terminal stream failures in JSON stats and OpenTelemetry errors

Why

OpenRouter exercises provider-compatible behavior that exposed four gaps: compressed responses the HTTP client could not decode, Anthropic streams ending with [DONE], usage arriving after finish_reason, and stream failures being reported as successful in stats.

How

The fixes stay at their existing ownership boundaries in the LLM client, translation codecs, and server usage observer. Routed calls remain counted once; terminal stream failures increment only the missing error counters.

What to review

  • reserved-header handling for Accept-Encoding
  • OpenAI Chat event ordering when usage follows the terminal choices chunk
  • stream failure accounting without partial usage or latency

Validation

  • cargo test -p switchyard-llm-client forwards_metadata_headers_except_reserved
  • cargo test -p switchyard-translation
  • cargo test -p switchyard-server streaming_error_records_error_without_usage_or_latency
  • cargo clippy -p switchyard-llm-client -p switchyard-translation -p switchyard-server --all-targets -- -D warnings
  • live OpenRouter: 9/9 buffered model-format combinations returned 200 with matching /v1/stats
  • live OpenRouter streaming: Chat, Responses, and Anthropic all completed with matching usage and zero errors

Summary by CodeRabbit

  • Bug Fixes
    • Streaming failures are now recorded accurately without inflating request, token, or latency metrics.
    • Caller-provided Accept-Encoding headers are no longer forwarded unexpectedly.
    • OpenAI streaming responses no longer include synthetic zero-valued usage data.
    • Usage received after a stop event is emitted in a dedicated usage-only update.
    • Completed streaming responses consistently include the [DONE] marker across supported formats.
  • Tests
    • Expanded coverage for streaming errors, usage reporting, and completion markers.

Signed-off-by: nachiketb <nachiketb@nvidia.com>
@nachiketb-nvidia
nachiketb-nvidia requested a review from a team as a code owner July 31, 2026 21:11
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The client reserves accept-encoding. The server records streaming failures in statistics and telemetry. OpenAI Chat translation separates usage chunks from terminal chunks. SSE termination emits [DONE] for supported formats.

Changes

Header forwarding

Layer / File(s) Summary
Reserve accept-encoding
crates/libsy-llm-client/src/client.rs
The client excludes caller-provided accept-encoding from upstream requests. Tests verify the header is absent.

Stream error metrics

Layer / File(s) Summary
Record streaming failures
crates/switchyard-server/src/stats/accumulator.rs, crates/switchyard-server/src/usage_metrics.rs, crates/switchyard-server/tests/server.rs
Streaming errors increment global and per-model error counts and the switchyard.errors metric. Request, token, and latency counts remain unchanged.

Stream translation

Layer / File(s) Summary
Emit OpenAI usage chunks
crates/switchyard-translation/src/codecs/openai_chat/stream.rs, crates/switchyard-translation/tests/stream_translation.rs
Completed streams emit backend usage in a separate empty-choices chunk. Terminal chunks omit usage when no backend usage was received.
Emit SSE terminal markers
crates/switchyard-translation/src/sse.rs
done_marker returns [DONE] for supported wire formats, including Anthropic.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Poem

I’m a rabbit with metrics to count,
Headers stay home, not upstream-bound.
Usage hops in a final small chunk,
Errors are logged when streams have sunk.
[DONE] now marks the closing sound.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the pull request's main goal of improving OpenRouter protocol interoperability.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

🧹 Nitpick comments (1)
crates/switchyard-server/tests/server.rs (1)

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

Document the important stream-failure contract.

Add a short comment stating that a terminal stream failure increments error statistics and metrics without recording usage or terminal latency.

As per coding guidelines, “comments for ... important tests” are required.

Proposed comment
+// A terminal stream failure records errors without usage or terminal latency.
 async fn streaming_error_records_error_without_usage_or_latency() -> TestResult {
🤖 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/tests/server.rs` at line 1133, Above the
streaming_error_records_error_without_usage_or_latency test, add a brief comment
documenting that terminal stream failures increment error statistics and metrics
while recording neither usage nor terminal latency.

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 `@crates/switchyard-server/src/usage_metrics.rs`:
- Around line 94-102: Normalize the optional tier value at the start of
record_stream_error, trimming whitespace and converting empty values to None,
then pass the normalized tier to both StatsAccumulator::record_stream_error and
attributes. Apply the same normalization consistently in the related token and
latency metric recording paths that currently pass the original tier.

In `@crates/switchyard-translation/src/sse.rs`:
- Around line 17-22: Update done_marker and the decode_stream flow so data:
[DONE] produces a distinct terminal result instead of being treated as an empty
frame. Detect the marker in parse_json_sse_frame or its caller, return the
marker-specific result, and break decoding immediately while preserving
empty-frame skipping and normal frame processing.

---

Nitpick comments:
In `@crates/switchyard-server/tests/server.rs`:
- Line 1133: Above the streaming_error_records_error_without_usage_or_latency
test, add a brief comment documenting that terminal stream failures increment
error statistics and metrics while recording neither usage nor terminal latency.
🪄 Autofix (Beta)

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: 036c77bf-f046-4ebe-b59d-f6648bdfd457

📥 Commits

Reviewing files that changed from the base of the PR and between d363dfc and c70579a.

📒 Files selected for processing (7)
  • crates/libsy-llm-client/src/client.rs
  • crates/switchyard-server/src/stats/accumulator.rs
  • crates/switchyard-server/src/usage_metrics.rs
  • crates/switchyard-server/tests/server.rs
  • crates/switchyard-translation/src/codecs/openai_chat/stream.rs
  • crates/switchyard-translation/src/sse.rs
  • crates/switchyard-translation/tests/stream_translation.rs

Comment on lines +94 to +102
// Records a terminal stream failure after the routed call was already counted.
fn record_stream_error(stats: &StatsAccumulator, model: &str, tier: Option<&str>) {
stats.record_stream_error(model, tier);
global::meter("switchyard")
.u64_counter("switchyard.errors")
.build()
.add(1, &attributes(model, tier));
}

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

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 5 'fn normalized_tier|normalized_tier\(' \
  crates/switchyard-server/src/stats \
  crates/switchyard-server/src/usage_metrics.rs

rg -n -C 6 'record_stream_error|fn attributes|attributes\(' \
  crates/switchyard-server/src/usage_metrics.rs \
  crates/switchyard-server/src/stats

Repository: NVIDIA-NeMo/Switchyard

Length of output: 11508


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- usage_metrics.rs ---'
cat -n crates/switchyard-server/src/usage_metrics.rs | sed -n '1,210p'

printf '%s\n' '--- tier-related call sites ---'
rg -n -C 4 'record_stream_error|record_usage|record_latency|tier\s*[:=]|tier\.as_deref|observe' \
  crates/switchyard-server/src --glob '*.rs'

Repository: NVIDIA-NeMo/Switchyard

Length of output: 38461


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- routing_tier definitions and implementations ---'
rg -n -C 8 'fn routing_tier|routing_tier\(|struct .*Decision|enum .*Decision|tier:' \
  crates --glob '*.rs'

printf '%s\n' '--- tier normalization tests and related assertions ---'
rg -n -C 6 'normalized_tier|whitespace|trim\(\)|tier.*empty|Some\("\s' \
  crates/switchyard-server --glob '*.rs'

Repository: NVIDIA-NeMo/Switchyard

Length of output: 50379


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- production Decision implementations that override routing_tier ---'
rg -n -C 6 'impl\s+Decision\s+for|fn\s+routing_tier\s*\(' \
  crates/switchyard-server crates/libsy crates/switchyard-components crates/protocol \
  --glob '*.rs' | sed -n '1,260p'

printf '%s\n' '--- behavioral probe for normalized_tier ---'
python3 - <<'PY'
def normalized_tier(tier):
    if tier is None:
        return None
    tier = tier.strip()
    return tier if tier else None

for value in (None, "", "   ", " capable ", "capable", "capable\n"):
    print(repr(value), "=>", repr(normalized_tier(value)))
PY

Repository: NVIDIA-NeMo/Switchyard

Length of output: 16227


Normalize tier before recording metrics.

StatsAccumulator trims tier and removes empty values, but attributes uses the original value. This affects switchyard.errors, token metrics, and latency metrics. Normalize tier before passing it to both statistics and metric recording.

🤖 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/src/usage_metrics.rs` around lines 94 - 102,
Normalize the optional tier value at the start of record_stream_error, trimming
whitespace and converting empty values to None, then pass the normalized tier to
both StatsAccumulator::record_stream_error and attributes. Apply the same
normalization consistently in the related token and latency metric recording
paths that currently pass the original tier.

Comment on lines +17 to +22
/// The optional terminal SSE marker accepted for all supported wire formats.
/// Native Anthropic streams can end at `message_stop`, while compatible
/// providers may additionally send `[DONE]`.
#[inline]
pub(crate) fn done_marker(format: WireFormat) -> Option<&'static str> {
match format {
WireFormat::OpenAiChat | WireFormat::OpenAiResponses => Some("[DONE]"),
WireFormat::AnthropicMessages => None,
}
pub(crate) fn done_marker(_format: WireFormat) -> Option<&'static str> {
Some("[DONE]")

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

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# Expect a marker-specific branch that exits the decode loop.
rg -n -C 16 \
  'fn decode_stream|parse_json_sse_frame|while let Some\(line\)|done_marker' \
  crates/switchyard-translation/src

Repository: NVIDIA-NeMo/Switchyard

Length of output: 27886


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# Inspect all marker/parser call sites and stream tests.
rg -n -C 8 \
  'parse_json_sse_frame|done_marker|decode_stream\(|decode_all|data: \[DONE\]' \
  crates/switchyard-translation/src

# Deterministically model the current control flow for a terminal marker followed
# by another valid frame. This does not execute repository code.
python3 - <<'PY'
from pathlib import Path

helpers = Path("crates/switchyard-translation/src/helpers.rs").read_text()
sse = Path("crates/switchyard-translation/src/sse.rs").read_text()

required = [
    "let marker = sse::done_marker(source);",
    "if let Some(value) = sse::parse_json_sse_frame(&frame, marker)",
    "frame.clear();",
    "while let Some(line) = lines.next().await",
]
for text in required:
    assert text in helpers, f"missing expected helper control-flow text: {text}"

assert "if data.is_empty() || done_marker.is_some_and(|marker| data == marker)" in sse
assert "return Ok(None);" in sse

# Equivalent abstraction of the observed loop:
# None is skipped, frame state is cleared, and the loop reads the next frame.
frames = ["data: {\"text\":\"before\"}", "data: [DONE]", "data: {\"text\":\"after\"}"]
decoded = []
for frame in frames:
    data = frame.removeprefix("data: ").rstrip()
    if not data or data == "[DONE]":
        continue
    decoded.append(data)

assert decoded == ['{"text":"before"}', '{"text":"after"}']
print("Current parser/decoder shape: [DONE] yields None, None is skipped, and a later frame is decoded.")
PY

Repository: NVIDIA-NeMo/Switchyard

Length of output: 22361


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail
rg -n -C 8 'parse_json_sse_frame|done_marker|decode_stream\(|decode_all|data: \[DONE\]' crates/switchyard-translation/src
python3 - <<'PY'
from pathlib import Path
helpers = Path("crates/switchyard-translation/src/helpers.rs").read_text()
sse = Path("crates/switchyard-translation/src/sse.rs").read_text()
assert "return Ok(None);" in sse
assert "if let Some(value) = sse::parse_json_sse_frame(&frame, marker)" in helpers
frames = ["data: {\"text\":\"before\"}", "data: [DONE]", "data: {\"text\":\"after\"}"]
decoded = []
for frame in frames:
    data = frame.removeprefix("data: ").rstrip()
    if not data or data == "[DONE]":
        continue
    decoded.append(data)
assert decoded == ['{"text":"before"}', '{"text":"after"}']
print("Current parser/decoder shape: [DONE] yields None, None is skipped, and a later frame is decoded.")
PY

Repository: NVIDIA-NeMo/Switchyard

Length of output: 22361


Stop decoding when data: [DONE] is received.

parse_json_sse_frame returns None for both empty frames and [DONE]. decode_stream skips None and continues reading. If the upstream keeps the connection open, decoding waits for EOF and accepts frames after the terminal marker. Return a marker-specific result and break the loop.

🤖 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-translation/src/sse.rs` around lines 17 - 22, Update
done_marker and the decode_stream flow so data: [DONE] produces a distinct
terminal result instead of being treated as an empty frame. Detect the marker in
parse_json_sse_frame or its caller, return the marker-specific result, and break
decoding immediately while preserving empty-frame skipping and normal frame
processing.

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