fix(routing): honour routing.retries on the streaming chat path - #807

Merged
jarvis9443 merged 1 commit into
mainfrom
fix/stream-retries-1119
Jul 23, 2026
Merged

fix(routing): honour routing.retries on the streaming chat path#807
jarvis9443 merged 1 commit into
mainfrom
fix/stream-retries-1119

Conversation

@jarvis9443

@jarvis9443jarvis9443 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Problem

/v1/chat/completions dispatches streaming and non-streaming requests through two separately-written loops in crates/aisix-proxy/src/chat.rs.

The non-streaming loop wraps each target in for attempt_idx in 0..=retries with exponential backoff. The streaming loop never read routing.retries at all — it walked attempt_models once and, on a retryable failure, went straight to the next target:

target A initial fails -> fallback to target B (actual)
target A initial fails -> A retry #1 -> fallback (configured)

With a single target there is nothing to fall over to, so one transient upstream error failed the request outright — retries was the only thing that could have saved it.

/v1/messages and /v1/responses don't have this gap: both run streaming and non-streaming through one loop (dispatch_to_target branches internally) that already honours retries. The streaming chat path was the only one that had drifted.

Change

Nest the same-target retry loop inside the streaming target walk, mirroring the non-streaming loop:

  • read routing.retries (default 0, so behaviour is unchanged for anyone not configuring it);
  • retryable failure → back off (retry_backoff, exponential + jitter) and re-hit the same target until retries is exhausted, then fall over;
  • non-retryable failure → stop immediately, as before;
  • target-invariant setup (model_arc / pk_arc / BridgeContext / stream budget) hoists above the retry loop; the per-attempt rate-limit reservation, telemetry record, and dispatch stay inside it.

Behaviour change

Per-attempt telemetry now emits a retry-kind record for each extra same-target attempt, so the Logs view shows initial → retry #1 → fallback instead of initial → fallback. The attempt-failed warning gains target_attempt, matching the non-streaming log line.

Only requests on a routing model with retries > 0 are affected. retries is read from the routing block, so a direct (non-group) model is still a single attempt.

Tests

  • streaming_routing_honors_same_target_retries — multi-target group, retries=1, always-502 primary: asserts the primary is hit twice and the attempt sequence is initial → retry → fallback.
  • streaming_routing_retries_single_target_before_failing — single target, retries=2: asserts three attempts all on that target before the request fails.
  • retry-backoff-e2e.test.ts gains a stream: true case asserting the same upstream hit count (3) and backoff floor as the existing non-streaming case.

All three fail before this change and pass after. Full aisix-proxy suite: 662 passed.

Fixes api7/AISIX-Cloud#1119
Fixes api7/AISIX-Cloud#1122

Summary by CodeRabbit

  • New Features

    • Streaming requests now honor configured per-model retry counts before failing over to another target.
    • Retry attempts use exponential backoff and preserve accurate attempt and usage tracking.
  • Bug Fixes

    • Streaming timeouts are applied consistently when connecting and reading response chunks.
    • Retry behavior now correctly handles single-target and fallback scenarios.

`/v1/chat/completions` dispatches streaming and non-streaming requests
through two separately-written loops in `chat.rs`. The non-streaming one
wraps each target in `for attempt_idx in 0..=retries` with exponential
backoff; the streaming one never read `routing.retries` at all — it
walked `attempt_models` once and, on a retryable failure, went straight
to the next target. With a single target it just failed the request.
`/v1/messages` and `/v1/responses` don't have this gap: both run
streaming and non-streaming through one loop that already honours
`retries`. This restores the same semantics for the streaming chat path:
same-target retry with backoff first, fail-over only once `retries` is
exhausted. Per-attempt telemetry now classifies the extra attempts as
`retry` (the records operators expect between `initial` and `fallback`),
and the attempt-failed warning carries `target_attempt` like the
non-streaming one.
Target-invariant setup (`model_arc` / `pk_arc` / `BridgeContext` /
stream budget) moves above the retry loop; the per-attempt reservation,
telemetry record, and dispatch stay inside it, matching the
non-streaming loop's structure.
Tests: two unit tests pinning the attempt sequence (multi-target
`initial → retry → fallback`, and single-target retries) plus a
streaming case in the retry-backoff E2E asserting the same upstream hit
count and backoff floor as the non-streaming one. All three fail before
this change.
Fixesapi7/AISIX-Cloud#1119Fixesapi7/AISIX-Cloud#1122
@coderabbitai

coderabbitaiBot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 49e6bca4-89c1-45fb-9897-88f161d67f6c

📥 Commits

Reviewing files that changed from the base of the PR and between 47e6252 and 64b015b.

📒 Files selected for processing (3)
  • crates/aisix-proxy/src/chat.rs
  • crates/aisix-proxy/src/lib.rs
  • tests/e2e/src/cases/retry-backoff-e2e.test.ts

📝 Walkthrough

Walkthrough

Streaming chat routing now honors routing.retries per target, applies retry backoff and effective streaming timeouts, records each attempt, and falls back only after retries are exhausted. Proxy and E2E tests cover retry ordering, single-target failure, telemetry, call counts, and timing.

Changes

Streaming routing retries

Layer / File(s)Summary
Streaming attempt orchestration
crates/aisix-proxy/src/chat.rs
The streaming path retries the same target according to routing.retries, applies per-attempt telemetry, cooldown, backoff, and effective stream timeouts, then falls back when retries are exhausted.
Streaming retry validation
crates/aisix-proxy/src/lib.rs, tests/e2e/src/cases/retry-backoff-e2e.test.ts
Tests verify same-target retries before fallback, single-target retry exhaustion, attempt metadata, upstream call counts, and streaming backoff timing.

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

Sequence Diagram(s)

sequenceDiagram
participant Client
participant StreamingDispatch
participant UpstreamTarget
participant RoutingTelemetry
Client->>StreamingDispatch: stream chat completion
StreamingDispatch->>UpstreamTarget: initial attempt
UpstreamTarget-->>StreamingDispatch: retryable failure
StreamingDispatch->>RoutingTelemetry: record failed attempt
StreamingDispatch->>UpstreamTarget: retry same target
UpstreamTarget-->>StreamingDispatch: failure
StreamingDispatch->>UpstreamTarget: fallback target attempt
UpstreamTarget-->>StreamingDispatch: stream response or failure
StreamingDispatch-->>Client: SSE completion or error
Loading

Possibly related PRs

  • api7/aisix#783: Overlaps with streaming routing attempt orchestration and per-target failure handling.
  • api7/aisix#784: Overlaps with streaming completion and telemetry handling.

Suggested reviewers:moonming

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly states the main change: applying routing.retries on the streaming chat path.
Linked Issues check✅ PassedThe PR implements streaming retries before failover and adds regression tests, matching the linked issues' core retry and recovery goals.
Out of Scope Changes check✅ PassedThe changes stay focused on streaming retry routing and related tests, with no clear unrelated scope creep.
E2e Test Quality Review✅ PassedE2E coverage hits app→router→mock upstream, names/assertions are clear, and streaming retry logic matches non-streaming parity with focused regression tests.
Security Check✅ PassedNo sensitive-data logging, auth bypass, ownership, TLS, or secret-handling regressions found in the touched retry code; it only adds retry metadata and existing BridgeError text.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/stream-retries-1119

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

@jarvis9443

Copy link
Copy Markdown
ContributorAuthor

@coderabbitai review

@coderabbitai

coderabbitaiBot commented Jul 23, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

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

@jarvis9443

Copy link
Copy Markdown
ContributorAuthor

@coderabbitai full review

@coderabbitai

coderabbitaiBot commented Jul 23, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. Your recent PR review activity is in the 95th percentile or higher among CodeRabbit users, so adaptive limits apply. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 42 minutes.

@jarvis9443

Copy link
Copy Markdown
ContributorAuthor

@coderabbitai full review

@coderabbitai

coderabbitaiBot commented Jul 23, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

Sign up for freeto 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

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(routing): honour routing.retries on the streaming chat path - #807

Merged
jarvis9443 merged 1 commit into
mainfrom
fix/stream-retries-1119
Jul 23, 2026
Merged

fix(routing): honour routing.retries on the streaming chat path#807
jarvis9443 merged 1 commit into
mainfrom
fix/stream-retries-1119

Conversation

@jarvis9443

@jarvis9443jarvis9443 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Problem

/v1/chat/completions dispatches streaming and non-streaming requests through two separately-written loops in crates/aisix-proxy/src/chat.rs.

The non-streaming loop wraps each target in for attempt_idx in 0..=retries with exponential backoff. The streaming loop never read routing.retries at all — it walked attempt_models once and, on a retryable failure, went straight to the next target:

target A initial fails -> fallback to target B (actual)
target A initial fails -> A retry #1 -> fallback (configured)

With a single target there is nothing to fall over to, so one transient upstream error failed the request outright — retries was the only thing that could have saved it.

/v1/messages and /v1/responses don't have this gap: both run streaming and non-streaming through one loop (dispatch_to_target branches internally) that already honours retries. The streaming chat path was the only one that had drifted.

Change

Nest the same-target retry loop inside the streaming target walk, mirroring the non-streaming loop:

  • read routing.retries (default 0, so behaviour is unchanged for anyone not configuring it);
  • retryable failure → back off (retry_backoff, exponential + jitter) and re-hit the same target until retries is exhausted, then fall over;
  • non-retryable failure → stop immediately, as before;
  • target-invariant setup (model_arc / pk_arc / BridgeContext / stream budget) hoists above the retry loop; the per-attempt rate-limit reservation, telemetry record, and dispatch stay inside it.

Behaviour change

Per-attempt telemetry now emits a retry-kind record for each extra same-target attempt, so the Logs view shows initial → retry #1 → fallback instead of initial → fallback. The attempt-failed warning gains target_attempt, matching the non-streaming log line.

Only requests on a routing model with retries > 0 are affected. retries is read from the routing block, so a direct (non-group) model is still a single attempt.

Tests

  • streaming_routing_honors_same_target_retries — multi-target group, retries=1, always-502 primary: asserts the primary is hit twice and the attempt sequence is initial → retry → fallback.
  • streaming_routing_retries_single_target_before_failing — single target, retries=2: asserts three attempts all on that target before the request fails.
  • retry-backoff-e2e.test.ts gains a stream: true case asserting the same upstream hit count (3) and backoff floor as the existing non-streaming case.

All three fail before this change and pass after. Full aisix-proxy suite: 662 passed.

Fixes api7/AISIX-Cloud#1119
Fixes api7/AISIX-Cloud#1122

Summary by CodeRabbit

  • New Features

    • Streaming requests now honor configured per-model retry counts before failing over to another target.
    • Retry attempts use exponential backoff and preserve accurate attempt and usage tracking.
  • Bug Fixes

    • Streaming timeouts are applied consistently when connecting and reading response chunks.
    • Retry behavior now correctly handles single-target and fallback scenarios.

`/v1/chat/completions` dispatches streaming and non-streaming requests
through two separately-written loops in `chat.rs`. The non-streaming one
wraps each target in `for attempt_idx in 0..=retries` with exponential
backoff; the streaming one never read `routing.retries` at all — it
walked `attempt_models` once and, on a retryable failure, went straight
to the next target. With a single target it just failed the request.
`/v1/messages` and `/v1/responses` don't have this gap: both run
streaming and non-streaming through one loop that already honours
`retries`. This restores the same semantics for the streaming chat path:
same-target retry with backoff first, fail-over only once `retries` is
exhausted. Per-attempt telemetry now classifies the extra attempts as
`retry` (the records operators expect between `initial` and `fallback`),
and the attempt-failed warning carries `target_attempt` like the
non-streaming one.
Target-invariant setup (`model_arc` / `pk_arc` / `BridgeContext` /
stream budget) moves above the retry loop; the per-attempt reservation,
telemetry record, and dispatch stay inside it, matching the
non-streaming loop's structure.
Tests: two unit tests pinning the attempt sequence (multi-target
`initial → retry → fallback`, and single-target retries) plus a
streaming case in the retry-backoff E2E asserting the same upstream hit
count and backoff floor as the non-streaming one. All three fail before
this change.
Fixesapi7/AISIX-Cloud#1119Fixesapi7/AISIX-Cloud#1122
@coderabbitai

coderabbitaiBot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 49e6bca4-89c1-45fb-9897-88f161d67f6c

📥 Commits

Reviewing files that changed from the base of the PR and between 47e6252 and 64b015b.

📒 Files selected for processing (3)
  • crates/aisix-proxy/src/chat.rs
  • crates/aisix-proxy/src/lib.rs
  • tests/e2e/src/cases/retry-backoff-e2e.test.ts

📝 Walkthrough

Walkthrough

Streaming chat routing now honors routing.retries per target, applies retry backoff and effective streaming timeouts, records each attempt, and falls back only after retries are exhausted. Proxy and E2E tests cover retry ordering, single-target failure, telemetry, call counts, and timing.

Changes

Streaming routing retries

Layer / File(s)Summary
Streaming attempt orchestration
crates/aisix-proxy/src/chat.rs
The streaming path retries the same target according to routing.retries, applies per-attempt telemetry, cooldown, backoff, and effective stream timeouts, then falls back when retries are exhausted.
Streaming retry validation
crates/aisix-proxy/src/lib.rs, tests/e2e/src/cases/retry-backoff-e2e.test.ts
Tests verify same-target retries before fallback, single-target retry exhaustion, attempt metadata, upstream call counts, and streaming backoff timing.

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

Sequence Diagram(s)

sequenceDiagram
participant Client
participant StreamingDispatch
participant UpstreamTarget
participant RoutingTelemetry
Client->>StreamingDispatch: stream chat completion
StreamingDispatch->>UpstreamTarget: initial attempt
UpstreamTarget-->>StreamingDispatch: retryable failure
StreamingDispatch->>RoutingTelemetry: record failed attempt
StreamingDispatch->>UpstreamTarget: retry same target
UpstreamTarget-->>StreamingDispatch: failure
StreamingDispatch->>UpstreamTarget: fallback target attempt
UpstreamTarget-->>StreamingDispatch: stream response or failure
StreamingDispatch-->>Client: SSE completion or error
Loading

Possibly related PRs

  • api7/aisix#783: Overlaps with streaming routing attempt orchestration and per-target failure handling.
  • api7/aisix#784: Overlaps with streaming completion and telemetry handling.

Suggested reviewers:moonming

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly states the main change: applying routing.retries on the streaming chat path.
Linked Issues check✅ PassedThe PR implements streaming retries before failover and adds regression tests, matching the linked issues' core retry and recovery goals.
Out of Scope Changes check✅ PassedThe changes stay focused on streaming retry routing and related tests, with no clear unrelated scope creep.
E2e Test Quality Review✅ PassedE2E coverage hits app→router→mock upstream, names/assertions are clear, and streaming retry logic matches non-streaming parity with focused regression tests.
Security Check✅ PassedNo sensitive-data logging, auth bypass, ownership, TLS, or secret-handling regressions found in the touched retry code; it only adds retry metadata and existing BridgeError text.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/stream-retries-1119

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

@jarvis9443

Copy link
Copy Markdown
ContributorAuthor

@coderabbitai review

@coderabbitai

coderabbitaiBot commented Jul 23, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

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

@jarvis9443

Copy link
Copy Markdown
ContributorAuthor

@coderabbitai full review

@coderabbitai

coderabbitaiBot commented Jul 23, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. Your recent PR review activity is in the 95th percentile or higher among CodeRabbit users, so adaptive limits apply. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 42 minutes.

@jarvis9443

Copy link
Copy Markdown
ContributorAuthor

@coderabbitai full review

@coderabbitai

coderabbitaiBot commented Jul 23, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

Sign up for freeto 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

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(routing): honour routing.retries on the streaming chat path - #807

Merged
jarvis9443 merged 1 commit into
mainfrom
fix/stream-retries-1119
Jul 23, 2026
Merged

fix(routing): honour routing.retries on the streaming chat path#807
jarvis9443 merged 1 commit into
mainfrom
fix/stream-retries-1119

Conversation

@jarvis9443

@jarvis9443jarvis9443 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Problem

/v1/chat/completions dispatches streaming and non-streaming requests through two separately-written loops in crates/aisix-proxy/src/chat.rs.

The non-streaming loop wraps each target in for attempt_idx in 0..=retries with exponential backoff. The streaming loop never read routing.retries at all — it walked attempt_models once and, on a retryable failure, went straight to the next target:

target A initial fails -> fallback to target B (actual)
target A initial fails -> A retry #1 -> fallback (configured)

With a single target there is nothing to fall over to, so one transient upstream error failed the request outright — retries was the only thing that could have saved it.

/v1/messages and /v1/responses don't have this gap: both run streaming and non-streaming through one loop (dispatch_to_target branches internally) that already honours retries. The streaming chat path was the only one that had drifted.

Change

Nest the same-target retry loop inside the streaming target walk, mirroring the non-streaming loop:

  • read routing.retries (default 0, so behaviour is unchanged for anyone not configuring it);
  • retryable failure → back off (retry_backoff, exponential + jitter) and re-hit the same target until retries is exhausted, then fall over;
  • non-retryable failure → stop immediately, as before;
  • target-invariant setup (model_arc / pk_arc / BridgeContext / stream budget) hoists above the retry loop; the per-attempt rate-limit reservation, telemetry record, and dispatch stay inside it.

Behaviour change

Per-attempt telemetry now emits a retry-kind record for each extra same-target attempt, so the Logs view shows initial → retry #1 → fallback instead of initial → fallback. The attempt-failed warning gains target_attempt, matching the non-streaming log line.

Only requests on a routing model with retries > 0 are affected. retries is read from the routing block, so a direct (non-group) model is still a single attempt.

Tests

  • streaming_routing_honors_same_target_retries — multi-target group, retries=1, always-502 primary: asserts the primary is hit twice and the attempt sequence is initial → retry → fallback.
  • streaming_routing_retries_single_target_before_failing — single target, retries=2: asserts three attempts all on that target before the request fails.
  • retry-backoff-e2e.test.ts gains a stream: true case asserting the same upstream hit count (3) and backoff floor as the existing non-streaming case.

All three fail before this change and pass after. Full aisix-proxy suite: 662 passed.

Fixes api7/AISIX-Cloud#1119
Fixes api7/AISIX-Cloud#1122

Summary by CodeRabbit

  • New Features

    • Streaming requests now honor configured per-model retry counts before failing over to another target.
    • Retry attempts use exponential backoff and preserve accurate attempt and usage tracking.
  • Bug Fixes

    • Streaming timeouts are applied consistently when connecting and reading response chunks.
    • Retry behavior now correctly handles single-target and fallback scenarios.

`/v1/chat/completions` dispatches streaming and non-streaming requests
through two separately-written loops in `chat.rs`. The non-streaming one
wraps each target in `for attempt_idx in 0..=retries` with exponential
backoff; the streaming one never read `routing.retries` at all — it
walked `attempt_models` once and, on a retryable failure, went straight
to the next target. With a single target it just failed the request.
`/v1/messages` and `/v1/responses` don't have this gap: both run
streaming and non-streaming through one loop that already honours
`retries`. This restores the same semantics for the streaming chat path:
same-target retry with backoff first, fail-over only once `retries` is
exhausted. Per-attempt telemetry now classifies the extra attempts as
`retry` (the records operators expect between `initial` and `fallback`),
and the attempt-failed warning carries `target_attempt` like the
non-streaming one.
Target-invariant setup (`model_arc` / `pk_arc` / `BridgeContext` /
stream budget) moves above the retry loop; the per-attempt reservation,
telemetry record, and dispatch stay inside it, matching the
non-streaming loop's structure.
Tests: two unit tests pinning the attempt sequence (multi-target
`initial → retry → fallback`, and single-target retries) plus a
streaming case in the retry-backoff E2E asserting the same upstream hit
count and backoff floor as the non-streaming one. All three fail before
this change.
Fixesapi7/AISIX-Cloud#1119Fixesapi7/AISIX-Cloud#1122
@coderabbitai

coderabbitaiBot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 49e6bca4-89c1-45fb-9897-88f161d67f6c

📥 Commits

Reviewing files that changed from the base of the PR and between 47e6252 and 64b015b.

📒 Files selected for processing (3)
  • crates/aisix-proxy/src/chat.rs
  • crates/aisix-proxy/src/lib.rs
  • tests/e2e/src/cases/retry-backoff-e2e.test.ts

📝 Walkthrough

Walkthrough

Streaming chat routing now honors routing.retries per target, applies retry backoff and effective streaming timeouts, records each attempt, and falls back only after retries are exhausted. Proxy and E2E tests cover retry ordering, single-target failure, telemetry, call counts, and timing.

Changes

Streaming routing retries

Layer / File(s)Summary
Streaming attempt orchestration
crates/aisix-proxy/src/chat.rs
The streaming path retries the same target according to routing.retries, applies per-attempt telemetry, cooldown, backoff, and effective stream timeouts, then falls back when retries are exhausted.
Streaming retry validation
crates/aisix-proxy/src/lib.rs, tests/e2e/src/cases/retry-backoff-e2e.test.ts
Tests verify same-target retries before fallback, single-target retry exhaustion, attempt metadata, upstream call counts, and streaming backoff timing.

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

Sequence Diagram(s)

sequenceDiagram
participant Client
participant StreamingDispatch
participant UpstreamTarget
participant RoutingTelemetry
Client->>StreamingDispatch: stream chat completion
StreamingDispatch->>UpstreamTarget: initial attempt
UpstreamTarget-->>StreamingDispatch: retryable failure
StreamingDispatch->>RoutingTelemetry: record failed attempt
StreamingDispatch->>UpstreamTarget: retry same target
UpstreamTarget-->>StreamingDispatch: failure
StreamingDispatch->>UpstreamTarget: fallback target attempt
UpstreamTarget-->>StreamingDispatch: stream response or failure
StreamingDispatch-->>Client: SSE completion or error
Loading

Possibly related PRs

  • api7/aisix#783: Overlaps with streaming routing attempt orchestration and per-target failure handling.
  • api7/aisix#784: Overlaps with streaming completion and telemetry handling.

Suggested reviewers:moonming

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly states the main change: applying routing.retries on the streaming chat path.
Linked Issues check✅ PassedThe PR implements streaming retries before failover and adds regression tests, matching the linked issues' core retry and recovery goals.
Out of Scope Changes check✅ PassedThe changes stay focused on streaming retry routing and related tests, with no clear unrelated scope creep.
E2e Test Quality Review✅ PassedE2E coverage hits app→router→mock upstream, names/assertions are clear, and streaming retry logic matches non-streaming parity with focused regression tests.
Security Check✅ PassedNo sensitive-data logging, auth bypass, ownership, TLS, or secret-handling regressions found in the touched retry code; it only adds retry metadata and existing BridgeError text.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/stream-retries-1119

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

@jarvis9443

Copy link
Copy Markdown
ContributorAuthor

@coderabbitai review

@coderabbitai

coderabbitaiBot commented Jul 23, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

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

@jarvis9443

Copy link
Copy Markdown
ContributorAuthor

@coderabbitai full review

@coderabbitai

coderabbitaiBot commented Jul 23, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. Your recent PR review activity is in the 95th percentile or higher among CodeRabbit users, so adaptive limits apply. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 42 minutes.

@jarvis9443

Copy link
Copy Markdown
ContributorAuthor

@coderabbitai full review

@coderabbitai

coderabbitaiBot commented Jul 23, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

Sign up for freeto 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

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(routing): honour routing.retries on the streaming chat path - #807

Merged
jarvis9443 merged 1 commit into
mainfrom
fix/stream-retries-1119
Jul 23, 2026
Merged

fix(routing): honour routing.retries on the streaming chat path#807
jarvis9443 merged 1 commit into
mainfrom
fix/stream-retries-1119

Conversation

@jarvis9443

@jarvis9443jarvis9443 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Problem

/v1/chat/completions dispatches streaming and non-streaming requests through two separately-written loops in crates/aisix-proxy/src/chat.rs.

The non-streaming loop wraps each target in for attempt_idx in 0..=retries with exponential backoff. The streaming loop never read routing.retries at all — it walked attempt_models once and, on a retryable failure, went straight to the next target:

target A initial fails -> fallback to target B (actual)
target A initial fails -> A retry #1 -> fallback (configured)

With a single target there is nothing to fall over to, so one transient upstream error failed the request outright — retries was the only thing that could have saved it.

/v1/messages and /v1/responses don't have this gap: both run streaming and non-streaming through one loop (dispatch_to_target branches internally) that already honours retries. The streaming chat path was the only one that had drifted.

Change

Nest the same-target retry loop inside the streaming target walk, mirroring the non-streaming loop:

  • read routing.retries (default 0, so behaviour is unchanged for anyone not configuring it);
  • retryable failure → back off (retry_backoff, exponential + jitter) and re-hit the same target until retries is exhausted, then fall over;
  • non-retryable failure → stop immediately, as before;
  • target-invariant setup (model_arc / pk_arc / BridgeContext / stream budget) hoists above the retry loop; the per-attempt rate-limit reservation, telemetry record, and dispatch stay inside it.

Behaviour change

Per-attempt telemetry now emits a retry-kind record for each extra same-target attempt, so the Logs view shows initial → retry #1 → fallback instead of initial → fallback. The attempt-failed warning gains target_attempt, matching the non-streaming log line.

Only requests on a routing model with retries > 0 are affected. retries is read from the routing block, so a direct (non-group) model is still a single attempt.

Tests

  • streaming_routing_honors_same_target_retries — multi-target group, retries=1, always-502 primary: asserts the primary is hit twice and the attempt sequence is initial → retry → fallback.
  • streaming_routing_retries_single_target_before_failing — single target, retries=2: asserts three attempts all on that target before the request fails.
  • retry-backoff-e2e.test.ts gains a stream: true case asserting the same upstream hit count (3) and backoff floor as the existing non-streaming case.

All three fail before this change and pass after. Full aisix-proxy suite: 662 passed.

Fixes api7/AISIX-Cloud#1119
Fixes api7/AISIX-Cloud#1122

Summary by CodeRabbit

  • New Features

    • Streaming requests now honor configured per-model retry counts before failing over to another target.
    • Retry attempts use exponential backoff and preserve accurate attempt and usage tracking.
  • Bug Fixes

    • Streaming timeouts are applied consistently when connecting and reading response chunks.
    • Retry behavior now correctly handles single-target and fallback scenarios.

`/v1/chat/completions` dispatches streaming and non-streaming requests
through two separately-written loops in `chat.rs`. The non-streaming one
wraps each target in `for attempt_idx in 0..=retries` with exponential
backoff; the streaming one never read `routing.retries` at all — it
walked `attempt_models` once and, on a retryable failure, went straight
to the next target. With a single target it just failed the request.
`/v1/messages` and `/v1/responses` don't have this gap: both run
streaming and non-streaming through one loop that already honours
`retries`. This restores the same semantics for the streaming chat path:
same-target retry with backoff first, fail-over only once `retries` is
exhausted. Per-attempt telemetry now classifies the extra attempts as
`retry` (the records operators expect between `initial` and `fallback`),
and the attempt-failed warning carries `target_attempt` like the
non-streaming one.
Target-invariant setup (`model_arc` / `pk_arc` / `BridgeContext` /
stream budget) moves above the retry loop; the per-attempt reservation,
telemetry record, and dispatch stay inside it, matching the
non-streaming loop's structure.
Tests: two unit tests pinning the attempt sequence (multi-target
`initial → retry → fallback`, and single-target retries) plus a
streaming case in the retry-backoff E2E asserting the same upstream hit
count and backoff floor as the non-streaming one. All three fail before
this change.
Fixesapi7/AISIX-Cloud#1119Fixesapi7/AISIX-Cloud#1122
@coderabbitai

coderabbitaiBot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 49e6bca4-89c1-45fb-9897-88f161d67f6c

📥 Commits

Reviewing files that changed from the base of the PR and between 47e6252 and 64b015b.

📒 Files selected for processing (3)
  • crates/aisix-proxy/src/chat.rs
  • crates/aisix-proxy/src/lib.rs
  • tests/e2e/src/cases/retry-backoff-e2e.test.ts

📝 Walkthrough

Walkthrough

Streaming chat routing now honors routing.retries per target, applies retry backoff and effective streaming timeouts, records each attempt, and falls back only after retries are exhausted. Proxy and E2E tests cover retry ordering, single-target failure, telemetry, call counts, and timing.

Changes

Streaming routing retries

Layer / File(s)Summary
Streaming attempt orchestration
crates/aisix-proxy/src/chat.rs
The streaming path retries the same target according to routing.retries, applies per-attempt telemetry, cooldown, backoff, and effective stream timeouts, then falls back when retries are exhausted.
Streaming retry validation
crates/aisix-proxy/src/lib.rs, tests/e2e/src/cases/retry-backoff-e2e.test.ts
Tests verify same-target retries before fallback, single-target retry exhaustion, attempt metadata, upstream call counts, and streaming backoff timing.

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

Sequence Diagram(s)

sequenceDiagram
participant Client
participant StreamingDispatch
participant UpstreamTarget
participant RoutingTelemetry
Client->>StreamingDispatch: stream chat completion
StreamingDispatch->>UpstreamTarget: initial attempt
UpstreamTarget-->>StreamingDispatch: retryable failure
StreamingDispatch->>RoutingTelemetry: record failed attempt
StreamingDispatch->>UpstreamTarget: retry same target
UpstreamTarget-->>StreamingDispatch: failure
StreamingDispatch->>UpstreamTarget: fallback target attempt
UpstreamTarget-->>StreamingDispatch: stream response or failure
StreamingDispatch-->>Client: SSE completion or error
Loading

Possibly related PRs

  • api7/aisix#783: Overlaps with streaming routing attempt orchestration and per-target failure handling.
  • api7/aisix#784: Overlaps with streaming completion and telemetry handling.

Suggested reviewers:moonming

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly states the main change: applying routing.retries on the streaming chat path.
Linked Issues check✅ PassedThe PR implements streaming retries before failover and adds regression tests, matching the linked issues' core retry and recovery goals.
Out of Scope Changes check✅ PassedThe changes stay focused on streaming retry routing and related tests, with no clear unrelated scope creep.
E2e Test Quality Review✅ PassedE2E coverage hits app→router→mock upstream, names/assertions are clear, and streaming retry logic matches non-streaming parity with focused regression tests.
Security Check✅ PassedNo sensitive-data logging, auth bypass, ownership, TLS, or secret-handling regressions found in the touched retry code; it only adds retry metadata and existing BridgeError text.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/stream-retries-1119

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

@jarvis9443

Copy link
Copy Markdown
ContributorAuthor

@coderabbitai review

@coderabbitai

coderabbitaiBot commented Jul 23, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

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

@jarvis9443

Copy link
Copy Markdown
ContributorAuthor

@coderabbitai full review

@coderabbitai

coderabbitaiBot commented Jul 23, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. Your recent PR review activity is in the 95th percentile or higher among CodeRabbit users, so adaptive limits apply. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 42 minutes.

@jarvis9443

Copy link
Copy Markdown
ContributorAuthor

@coderabbitai full review

@coderabbitai

coderabbitaiBot commented Jul 23, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

Sign up for freeto 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

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(routing): honour routing.retries on the streaming chat path - #807

Merged
jarvis9443 merged 1 commit into
mainfrom
fix/stream-retries-1119
Jul 23, 2026
Merged

fix(routing): honour routing.retries on the streaming chat path#807
jarvis9443 merged 1 commit into
mainfrom
fix/stream-retries-1119

Conversation

@jarvis9443

@jarvis9443jarvis9443 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Problem

/v1/chat/completions dispatches streaming and non-streaming requests through two separately-written loops in crates/aisix-proxy/src/chat.rs.

The non-streaming loop wraps each target in for attempt_idx in 0..=retries with exponential backoff. The streaming loop never read routing.retries at all — it walked attempt_models once and, on a retryable failure, went straight to the next target:

target A initial fails -> fallback to target B (actual)
target A initial fails -> A retry #1 -> fallback (configured)

With a single target there is nothing to fall over to, so one transient upstream error failed the request outright — retries was the only thing that could have saved it.

/v1/messages and /v1/responses don't have this gap: both run streaming and non-streaming through one loop (dispatch_to_target branches internally) that already honours retries. The streaming chat path was the only one that had drifted.

Change

Nest the same-target retry loop inside the streaming target walk, mirroring the non-streaming loop:

  • read routing.retries (default 0, so behaviour is unchanged for anyone not configuring it);
  • retryable failure → back off (retry_backoff, exponential + jitter) and re-hit the same target until retries is exhausted, then fall over;
  • non-retryable failure → stop immediately, as before;
  • target-invariant setup (model_arc / pk_arc / BridgeContext / stream budget) hoists above the retry loop; the per-attempt rate-limit reservation, telemetry record, and dispatch stay inside it.

Behaviour change

Per-attempt telemetry now emits a retry-kind record for each extra same-target attempt, so the Logs view shows initial → retry #1 → fallback instead of initial → fallback. The attempt-failed warning gains target_attempt, matching the non-streaming log line.

Only requests on a routing model with retries > 0 are affected. retries is read from the routing block, so a direct (non-group) model is still a single attempt.

Tests

  • streaming_routing_honors_same_target_retries — multi-target group, retries=1, always-502 primary: asserts the primary is hit twice and the attempt sequence is initial → retry → fallback.
  • streaming_routing_retries_single_target_before_failing — single target, retries=2: asserts three attempts all on that target before the request fails.
  • retry-backoff-e2e.test.ts gains a stream: true case asserting the same upstream hit count (3) and backoff floor as the existing non-streaming case.

All three fail before this change and pass after. Full aisix-proxy suite: 662 passed.

Fixes api7/AISIX-Cloud#1119
Fixes api7/AISIX-Cloud#1122

Summary by CodeRabbit

  • New Features

    • Streaming requests now honor configured per-model retry counts before failing over to another target.
    • Retry attempts use exponential backoff and preserve accurate attempt and usage tracking.
  • Bug Fixes

    • Streaming timeouts are applied consistently when connecting and reading response chunks.
    • Retry behavior now correctly handles single-target and fallback scenarios.

`/v1/chat/completions` dispatches streaming and non-streaming requests
through two separately-written loops in `chat.rs`. The non-streaming one
wraps each target in `for attempt_idx in 0..=retries` with exponential
backoff; the streaming one never read `routing.retries` at all — it
walked `attempt_models` once and, on a retryable failure, went straight
to the next target. With a single target it just failed the request.
`/v1/messages` and `/v1/responses` don't have this gap: both run
streaming and non-streaming through one loop that already honours
`retries`. This restores the same semantics for the streaming chat path:
same-target retry with backoff first, fail-over only once `retries` is
exhausted. Per-attempt telemetry now classifies the extra attempts as
`retry` (the records operators expect between `initial` and `fallback`),
and the attempt-failed warning carries `target_attempt` like the
non-streaming one.
Target-invariant setup (`model_arc` / `pk_arc` / `BridgeContext` /
stream budget) moves above the retry loop; the per-attempt reservation,
telemetry record, and dispatch stay inside it, matching the
non-streaming loop's structure.
Tests: two unit tests pinning the attempt sequence (multi-target
`initial → retry → fallback`, and single-target retries) plus a
streaming case in the retry-backoff E2E asserting the same upstream hit
count and backoff floor as the non-streaming one. All three fail before
this change.
Fixesapi7/AISIX-Cloud#1119Fixesapi7/AISIX-Cloud#1122
@coderabbitai

coderabbitaiBot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 49e6bca4-89c1-45fb-9897-88f161d67f6c

📥 Commits

Reviewing files that changed from the base of the PR and between 47e6252 and 64b015b.

📒 Files selected for processing (3)
  • crates/aisix-proxy/src/chat.rs
  • crates/aisix-proxy/src/lib.rs
  • tests/e2e/src/cases/retry-backoff-e2e.test.ts

📝 Walkthrough

Walkthrough

Streaming chat routing now honors routing.retries per target, applies retry backoff and effective streaming timeouts, records each attempt, and falls back only after retries are exhausted. Proxy and E2E tests cover retry ordering, single-target failure, telemetry, call counts, and timing.

Changes

Streaming routing retries

Layer / File(s)Summary
Streaming attempt orchestration
crates/aisix-proxy/src/chat.rs
The streaming path retries the same target according to routing.retries, applies per-attempt telemetry, cooldown, backoff, and effective stream timeouts, then falls back when retries are exhausted.
Streaming retry validation
crates/aisix-proxy/src/lib.rs, tests/e2e/src/cases/retry-backoff-e2e.test.ts
Tests verify same-target retries before fallback, single-target retry exhaustion, attempt metadata, upstream call counts, and streaming backoff timing.

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

Sequence Diagram(s)

sequenceDiagram
participant Client
participant StreamingDispatch
participant UpstreamTarget
participant RoutingTelemetry
Client->>StreamingDispatch: stream chat completion
StreamingDispatch->>UpstreamTarget: initial attempt
UpstreamTarget-->>StreamingDispatch: retryable failure
StreamingDispatch->>RoutingTelemetry: record failed attempt
StreamingDispatch->>UpstreamTarget: retry same target
UpstreamTarget-->>StreamingDispatch: failure
StreamingDispatch->>UpstreamTarget: fallback target attempt
UpstreamTarget-->>StreamingDispatch: stream response or failure
StreamingDispatch-->>Client: SSE completion or error
Loading

Possibly related PRs

  • api7/aisix#783: Overlaps with streaming routing attempt orchestration and per-target failure handling.
  • api7/aisix#784: Overlaps with streaming completion and telemetry handling.

Suggested reviewers:moonming

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly states the main change: applying routing.retries on the streaming chat path.
Linked Issues check✅ PassedThe PR implements streaming retries before failover and adds regression tests, matching the linked issues' core retry and recovery goals.
Out of Scope Changes check✅ PassedThe changes stay focused on streaming retry routing and related tests, with no clear unrelated scope creep.
E2e Test Quality Review✅ PassedE2E coverage hits app→router→mock upstream, names/assertions are clear, and streaming retry logic matches non-streaming parity with focused regression tests.
Security Check✅ PassedNo sensitive-data logging, auth bypass, ownership, TLS, or secret-handling regressions found in the touched retry code; it only adds retry metadata and existing BridgeError text.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/stream-retries-1119

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

@jarvis9443

Copy link
Copy Markdown
ContributorAuthor

@coderabbitai review

@coderabbitai

coderabbitaiBot commented Jul 23, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

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

@jarvis9443

Copy link
Copy Markdown
ContributorAuthor

@coderabbitai full review

@coderabbitai

coderabbitaiBot commented Jul 23, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. Your recent PR review activity is in the 95th percentile or higher among CodeRabbit users, so adaptive limits apply. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 42 minutes.

@jarvis9443

Copy link
Copy Markdown
ContributorAuthor

@coderabbitai full review

@coderabbitai

coderabbitaiBot commented Jul 23, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

Sign up for freeto 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

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(routing): honour routing.retries on the streaming chat path - #807

Merged
jarvis9443 merged 1 commit into
mainfrom
fix/stream-retries-1119
Jul 23, 2026
Merged

fix(routing): honour routing.retries on the streaming chat path#807
jarvis9443 merged 1 commit into
mainfrom
fix/stream-retries-1119

Conversation

@jarvis9443

@jarvis9443jarvis9443 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Problem

/v1/chat/completions dispatches streaming and non-streaming requests through two separately-written loops in crates/aisix-proxy/src/chat.rs.

The non-streaming loop wraps each target in for attempt_idx in 0..=retries with exponential backoff. The streaming loop never read routing.retries at all — it walked attempt_models once and, on a retryable failure, went straight to the next target:

target A initial fails -> fallback to target B (actual)
target A initial fails -> A retry #1 -> fallback (configured)

With a single target there is nothing to fall over to, so one transient upstream error failed the request outright — retries was the only thing that could have saved it.

/v1/messages and /v1/responses don't have this gap: both run streaming and non-streaming through one loop (dispatch_to_target branches internally) that already honours retries. The streaming chat path was the only one that had drifted.

Change

Nest the same-target retry loop inside the streaming target walk, mirroring the non-streaming loop:

  • read routing.retries (default 0, so behaviour is unchanged for anyone not configuring it);
  • retryable failure → back off (retry_backoff, exponential + jitter) and re-hit the same target until retries is exhausted, then fall over;
  • non-retryable failure → stop immediately, as before;
  • target-invariant setup (model_arc / pk_arc / BridgeContext / stream budget) hoists above the retry loop; the per-attempt rate-limit reservation, telemetry record, and dispatch stay inside it.

Behaviour change

Per-attempt telemetry now emits a retry-kind record for each extra same-target attempt, so the Logs view shows initial → retry #1 → fallback instead of initial → fallback. The attempt-failed warning gains target_attempt, matching the non-streaming log line.

Only requests on a routing model with retries > 0 are affected. retries is read from the routing block, so a direct (non-group) model is still a single attempt.

Tests

  • streaming_routing_honors_same_target_retries — multi-target group, retries=1, always-502 primary: asserts the primary is hit twice and the attempt sequence is initial → retry → fallback.
  • streaming_routing_retries_single_target_before_failing — single target, retries=2: asserts three attempts all on that target before the request fails.
  • retry-backoff-e2e.test.ts gains a stream: true case asserting the same upstream hit count (3) and backoff floor as the existing non-streaming case.

All three fail before this change and pass after. Full aisix-proxy suite: 662 passed.

Fixes api7/AISIX-Cloud#1119
Fixes api7/AISIX-Cloud#1122

Summary by CodeRabbit

  • New Features

    • Streaming requests now honor configured per-model retry counts before failing over to another target.
    • Retry attempts use exponential backoff and preserve accurate attempt and usage tracking.
  • Bug Fixes

    • Streaming timeouts are applied consistently when connecting and reading response chunks.
    • Retry behavior now correctly handles single-target and fallback scenarios.

`/v1/chat/completions` dispatches streaming and non-streaming requests
through two separately-written loops in `chat.rs`. The non-streaming one
wraps each target in `for attempt_idx in 0..=retries` with exponential
backoff; the streaming one never read `routing.retries` at all — it
walked `attempt_models` once and, on a retryable failure, went straight
to the next target. With a single target it just failed the request.
`/v1/messages` and `/v1/responses` don't have this gap: both run
streaming and non-streaming through one loop that already honours
`retries`. This restores the same semantics for the streaming chat path:
same-target retry with backoff first, fail-over only once `retries` is
exhausted. Per-attempt telemetry now classifies the extra attempts as
`retry` (the records operators expect between `initial` and `fallback`),
and the attempt-failed warning carries `target_attempt` like the
non-streaming one.
Target-invariant setup (`model_arc` / `pk_arc` / `BridgeContext` /
stream budget) moves above the retry loop; the per-attempt reservation,
telemetry record, and dispatch stay inside it, matching the
non-streaming loop's structure.
Tests: two unit tests pinning the attempt sequence (multi-target
`initial → retry → fallback`, and single-target retries) plus a
streaming case in the retry-backoff E2E asserting the same upstream hit
count and backoff floor as the non-streaming one. All three fail before
this change.
Fixesapi7/AISIX-Cloud#1119Fixesapi7/AISIX-Cloud#1122
@coderabbitai

coderabbitaiBot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 49e6bca4-89c1-45fb-9897-88f161d67f6c

📥 Commits

Reviewing files that changed from the base of the PR and between 47e6252 and 64b015b.

📒 Files selected for processing (3)
  • crates/aisix-proxy/src/chat.rs
  • crates/aisix-proxy/src/lib.rs
  • tests/e2e/src/cases/retry-backoff-e2e.test.ts

📝 Walkthrough

Walkthrough

Streaming chat routing now honors routing.retries per target, applies retry backoff and effective streaming timeouts, records each attempt, and falls back only after retries are exhausted. Proxy and E2E tests cover retry ordering, single-target failure, telemetry, call counts, and timing.

Changes

Streaming routing retries

Layer / File(s)Summary
Streaming attempt orchestration
crates/aisix-proxy/src/chat.rs
The streaming path retries the same target according to routing.retries, applies per-attempt telemetry, cooldown, backoff, and effective stream timeouts, then falls back when retries are exhausted.
Streaming retry validation
crates/aisix-proxy/src/lib.rs, tests/e2e/src/cases/retry-backoff-e2e.test.ts
Tests verify same-target retries before fallback, single-target retry exhaustion, attempt metadata, upstream call counts, and streaming backoff timing.

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

Sequence Diagram(s)

sequenceDiagram
participant Client
participant StreamingDispatch
participant UpstreamTarget
participant RoutingTelemetry
Client->>StreamingDispatch: stream chat completion
StreamingDispatch->>UpstreamTarget: initial attempt
UpstreamTarget-->>StreamingDispatch: retryable failure
StreamingDispatch->>RoutingTelemetry: record failed attempt
StreamingDispatch->>UpstreamTarget: retry same target
UpstreamTarget-->>StreamingDispatch: failure
StreamingDispatch->>UpstreamTarget: fallback target attempt
UpstreamTarget-->>StreamingDispatch: stream response or failure
StreamingDispatch-->>Client: SSE completion or error
Loading

Possibly related PRs

  • api7/aisix#783: Overlaps with streaming routing attempt orchestration and per-target failure handling.
  • api7/aisix#784: Overlaps with streaming completion and telemetry handling.

Suggested reviewers:moonming

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly states the main change: applying routing.retries on the streaming chat path.
Linked Issues check✅ PassedThe PR implements streaming retries before failover and adds regression tests, matching the linked issues' core retry and recovery goals.
Out of Scope Changes check✅ PassedThe changes stay focused on streaming retry routing and related tests, with no clear unrelated scope creep.
E2e Test Quality Review✅ PassedE2E coverage hits app→router→mock upstream, names/assertions are clear, and streaming retry logic matches non-streaming parity with focused regression tests.
Security Check✅ PassedNo sensitive-data logging, auth bypass, ownership, TLS, or secret-handling regressions found in the touched retry code; it only adds retry metadata and existing BridgeError text.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/stream-retries-1119

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

@jarvis9443

Copy link
Copy Markdown
ContributorAuthor

@coderabbitai review

@coderabbitai

coderabbitaiBot commented Jul 23, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

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

@jarvis9443

Copy link
Copy Markdown
ContributorAuthor

@coderabbitai full review

@coderabbitai

coderabbitaiBot commented Jul 23, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. Your recent PR review activity is in the 95th percentile or higher among CodeRabbit users, so adaptive limits apply. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 42 minutes.

@jarvis9443

Copy link
Copy Markdown
ContributorAuthor

@coderabbitai full review

@coderabbitai

coderabbitaiBot commented Jul 23, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

Sign up for freeto 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

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(routing): honour routing.retries on the streaming chat path - #807

Merged
jarvis9443 merged 1 commit into
mainfrom
fix/stream-retries-1119
Jul 23, 2026
Merged

fix(routing): honour routing.retries on the streaming chat path#807
jarvis9443 merged 1 commit into
mainfrom
fix/stream-retries-1119

Conversation

@jarvis9443

@jarvis9443jarvis9443 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Problem

/v1/chat/completions dispatches streaming and non-streaming requests through two separately-written loops in crates/aisix-proxy/src/chat.rs.

The non-streaming loop wraps each target in for attempt_idx in 0..=retries with exponential backoff. The streaming loop never read routing.retries at all — it walked attempt_models once and, on a retryable failure, went straight to the next target:

target A initial fails -> fallback to target B (actual)
target A initial fails -> A retry #1 -> fallback (configured)

With a single target there is nothing to fall over to, so one transient upstream error failed the request outright — retries was the only thing that could have saved it.

/v1/messages and /v1/responses don't have this gap: both run streaming and non-streaming through one loop (dispatch_to_target branches internally) that already honours retries. The streaming chat path was the only one that had drifted.

Change

Nest the same-target retry loop inside the streaming target walk, mirroring the non-streaming loop:

  • read routing.retries (default 0, so behaviour is unchanged for anyone not configuring it);
  • retryable failure → back off (retry_backoff, exponential + jitter) and re-hit the same target until retries is exhausted, then fall over;
  • non-retryable failure → stop immediately, as before;
  • target-invariant setup (model_arc / pk_arc / BridgeContext / stream budget) hoists above the retry loop; the per-attempt rate-limit reservation, telemetry record, and dispatch stay inside it.

Behaviour change

Per-attempt telemetry now emits a retry-kind record for each extra same-target attempt, so the Logs view shows initial → retry #1 → fallback instead of initial → fallback. The attempt-failed warning gains target_attempt, matching the non-streaming log line.

Only requests on a routing model with retries > 0 are affected. retries is read from the routing block, so a direct (non-group) model is still a single attempt.

Tests

  • streaming_routing_honors_same_target_retries — multi-target group, retries=1, always-502 primary: asserts the primary is hit twice and the attempt sequence is initial → retry → fallback.
  • streaming_routing_retries_single_target_before_failing — single target, retries=2: asserts three attempts all on that target before the request fails.
  • retry-backoff-e2e.test.ts gains a stream: true case asserting the same upstream hit count (3) and backoff floor as the existing non-streaming case.

All three fail before this change and pass after. Full aisix-proxy suite: 662 passed.

Fixes api7/AISIX-Cloud#1119
Fixes api7/AISIX-Cloud#1122

Summary by CodeRabbit

  • New Features

    • Streaming requests now honor configured per-model retry counts before failing over to another target.
    • Retry attempts use exponential backoff and preserve accurate attempt and usage tracking.
  • Bug Fixes

    • Streaming timeouts are applied consistently when connecting and reading response chunks.
    • Retry behavior now correctly handles single-target and fallback scenarios.

`/v1/chat/completions` dispatches streaming and non-streaming requests
through two separately-written loops in `chat.rs`. The non-streaming one
wraps each target in `for attempt_idx in 0..=retries` with exponential
backoff; the streaming one never read `routing.retries` at all — it
walked `attempt_models` once and, on a retryable failure, went straight
to the next target. With a single target it just failed the request.
`/v1/messages` and `/v1/responses` don't have this gap: both run
streaming and non-streaming through one loop that already honours
`retries`. This restores the same semantics for the streaming chat path:
same-target retry with backoff first, fail-over only once `retries` is
exhausted. Per-attempt telemetry now classifies the extra attempts as
`retry` (the records operators expect between `initial` and `fallback`),
and the attempt-failed warning carries `target_attempt` like the
non-streaming one.
Target-invariant setup (`model_arc` / `pk_arc` / `BridgeContext` /
stream budget) moves above the retry loop; the per-attempt reservation,
telemetry record, and dispatch stay inside it, matching the
non-streaming loop's structure.
Tests: two unit tests pinning the attempt sequence (multi-target
`initial → retry → fallback`, and single-target retries) plus a
streaming case in the retry-backoff E2E asserting the same upstream hit
count and backoff floor as the non-streaming one. All three fail before
this change.
Fixesapi7/AISIX-Cloud#1119Fixesapi7/AISIX-Cloud#1122
@coderabbitai

coderabbitaiBot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 49e6bca4-89c1-45fb-9897-88f161d67f6c

📥 Commits

Reviewing files that changed from the base of the PR and between 47e6252 and 64b015b.

📒 Files selected for processing (3)
  • crates/aisix-proxy/src/chat.rs
  • crates/aisix-proxy/src/lib.rs
  • tests/e2e/src/cases/retry-backoff-e2e.test.ts

📝 Walkthrough

Walkthrough

Streaming chat routing now honors routing.retries per target, applies retry backoff and effective streaming timeouts, records each attempt, and falls back only after retries are exhausted. Proxy and E2E tests cover retry ordering, single-target failure, telemetry, call counts, and timing.

Changes

Streaming routing retries

Layer / File(s)Summary
Streaming attempt orchestration
crates/aisix-proxy/src/chat.rs
The streaming path retries the same target according to routing.retries, applies per-attempt telemetry, cooldown, backoff, and effective stream timeouts, then falls back when retries are exhausted.
Streaming retry validation
crates/aisix-proxy/src/lib.rs, tests/e2e/src/cases/retry-backoff-e2e.test.ts
Tests verify same-target retries before fallback, single-target retry exhaustion, attempt metadata, upstream call counts, and streaming backoff timing.

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

Sequence Diagram(s)

sequenceDiagram
participant Client
participant StreamingDispatch
participant UpstreamTarget
participant RoutingTelemetry
Client->>StreamingDispatch: stream chat completion
StreamingDispatch->>UpstreamTarget: initial attempt
UpstreamTarget-->>StreamingDispatch: retryable failure
StreamingDispatch->>RoutingTelemetry: record failed attempt
StreamingDispatch->>UpstreamTarget: retry same target
UpstreamTarget-->>StreamingDispatch: failure
StreamingDispatch->>UpstreamTarget: fallback target attempt
UpstreamTarget-->>StreamingDispatch: stream response or failure
StreamingDispatch-->>Client: SSE completion or error
Loading

Possibly related PRs

  • api7/aisix#783: Overlaps with streaming routing attempt orchestration and per-target failure handling.
  • api7/aisix#784: Overlaps with streaming completion and telemetry handling.

Suggested reviewers:moonming

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly states the main change: applying routing.retries on the streaming chat path.
Linked Issues check✅ PassedThe PR implements streaming retries before failover and adds regression tests, matching the linked issues' core retry and recovery goals.
Out of Scope Changes check✅ PassedThe changes stay focused on streaming retry routing and related tests, with no clear unrelated scope creep.
E2e Test Quality Review✅ PassedE2E coverage hits app→router→mock upstream, names/assertions are clear, and streaming retry logic matches non-streaming parity with focused regression tests.
Security Check✅ PassedNo sensitive-data logging, auth bypass, ownership, TLS, or secret-handling regressions found in the touched retry code; it only adds retry metadata and existing BridgeError text.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/stream-retries-1119

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

@jarvis9443

Copy link
Copy Markdown
ContributorAuthor

@coderabbitai review

@coderabbitai

coderabbitaiBot commented Jul 23, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

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

@jarvis9443

Copy link
Copy Markdown
ContributorAuthor

@coderabbitai full review

@coderabbitai

coderabbitaiBot commented Jul 23, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. Your recent PR review activity is in the 95th percentile or higher among CodeRabbit users, so adaptive limits apply. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 42 minutes.

@jarvis9443

Copy link
Copy Markdown
ContributorAuthor

@coderabbitai full review

@coderabbitai

coderabbitaiBot commented Jul 23, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

Sign up for freeto 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

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(routing): honour routing.retries on the streaming chat path - #807

Merged
jarvis9443 merged 1 commit into
mainfrom
fix/stream-retries-1119
Jul 23, 2026
Merged

fix(routing): honour routing.retries on the streaming chat path#807
jarvis9443 merged 1 commit into
mainfrom
fix/stream-retries-1119

Conversation

@jarvis9443

@jarvis9443jarvis9443 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Problem

/v1/chat/completions dispatches streaming and non-streaming requests through two separately-written loops in crates/aisix-proxy/src/chat.rs.

The non-streaming loop wraps each target in for attempt_idx in 0..=retries with exponential backoff. The streaming loop never read routing.retries at all — it walked attempt_models once and, on a retryable failure, went straight to the next target:

target A initial fails -> fallback to target B (actual)
target A initial fails -> A retry #1 -> fallback (configured)

With a single target there is nothing to fall over to, so one transient upstream error failed the request outright — retries was the only thing that could have saved it.

/v1/messages and /v1/responses don't have this gap: both run streaming and non-streaming through one loop (dispatch_to_target branches internally) that already honours retries. The streaming chat path was the only one that had drifted.

Change

Nest the same-target retry loop inside the streaming target walk, mirroring the non-streaming loop:

  • read routing.retries (default 0, so behaviour is unchanged for anyone not configuring it);
  • retryable failure → back off (retry_backoff, exponential + jitter) and re-hit the same target until retries is exhausted, then fall over;
  • non-retryable failure → stop immediately, as before;
  • target-invariant setup (model_arc / pk_arc / BridgeContext / stream budget) hoists above the retry loop; the per-attempt rate-limit reservation, telemetry record, and dispatch stay inside it.

Behaviour change

Per-attempt telemetry now emits a retry-kind record for each extra same-target attempt, so the Logs view shows initial → retry #1 → fallback instead of initial → fallback. The attempt-failed warning gains target_attempt, matching the non-streaming log line.

Only requests on a routing model with retries > 0 are affected. retries is read from the routing block, so a direct (non-group) model is still a single attempt.

Tests

  • streaming_routing_honors_same_target_retries — multi-target group, retries=1, always-502 primary: asserts the primary is hit twice and the attempt sequence is initial → retry → fallback.
  • streaming_routing_retries_single_target_before_failing — single target, retries=2: asserts three attempts all on that target before the request fails.
  • retry-backoff-e2e.test.ts gains a stream: true case asserting the same upstream hit count (3) and backoff floor as the existing non-streaming case.

All three fail before this change and pass after. Full aisix-proxy suite: 662 passed.

Fixes api7/AISIX-Cloud#1119
Fixes api7/AISIX-Cloud#1122

Summary by CodeRabbit

  • New Features

    • Streaming requests now honor configured per-model retry counts before failing over to another target.
    • Retry attempts use exponential backoff and preserve accurate attempt and usage tracking.
  • Bug Fixes

    • Streaming timeouts are applied consistently when connecting and reading response chunks.
    • Retry behavior now correctly handles single-target and fallback scenarios.

`/v1/chat/completions` dispatches streaming and non-streaming requests
through two separately-written loops in `chat.rs`. The non-streaming one
wraps each target in `for attempt_idx in 0..=retries` with exponential
backoff; the streaming one never read `routing.retries` at all — it
walked `attempt_models` once and, on a retryable failure, went straight
to the next target. With a single target it just failed the request.
`/v1/messages` and `/v1/responses` don't have this gap: both run
streaming and non-streaming through one loop that already honours
`retries`. This restores the same semantics for the streaming chat path:
same-target retry with backoff first, fail-over only once `retries` is
exhausted. Per-attempt telemetry now classifies the extra attempts as
`retry` (the records operators expect between `initial` and `fallback`),
and the attempt-failed warning carries `target_attempt` like the
non-streaming one.
Target-invariant setup (`model_arc` / `pk_arc` / `BridgeContext` /
stream budget) moves above the retry loop; the per-attempt reservation,
telemetry record, and dispatch stay inside it, matching the
non-streaming loop's structure.
Tests: two unit tests pinning the attempt sequence (multi-target
`initial → retry → fallback`, and single-target retries) plus a
streaming case in the retry-backoff E2E asserting the same upstream hit
count and backoff floor as the non-streaming one. All three fail before
this change.
Fixesapi7/AISIX-Cloud#1119Fixesapi7/AISIX-Cloud#1122
@coderabbitai

coderabbitaiBot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 49e6bca4-89c1-45fb-9897-88f161d67f6c

📥 Commits

Reviewing files that changed from the base of the PR and between 47e6252 and 64b015b.

📒 Files selected for processing (3)
  • crates/aisix-proxy/src/chat.rs
  • crates/aisix-proxy/src/lib.rs
  • tests/e2e/src/cases/retry-backoff-e2e.test.ts

📝 Walkthrough

Walkthrough

Streaming chat routing now honors routing.retries per target, applies retry backoff and effective streaming timeouts, records each attempt, and falls back only after retries are exhausted. Proxy and E2E tests cover retry ordering, single-target failure, telemetry, call counts, and timing.

Changes

Streaming routing retries

Layer / File(s)Summary
Streaming attempt orchestration
crates/aisix-proxy/src/chat.rs
The streaming path retries the same target according to routing.retries, applies per-attempt telemetry, cooldown, backoff, and effective stream timeouts, then falls back when retries are exhausted.
Streaming retry validation
crates/aisix-proxy/src/lib.rs, tests/e2e/src/cases/retry-backoff-e2e.test.ts
Tests verify same-target retries before fallback, single-target retry exhaustion, attempt metadata, upstream call counts, and streaming backoff timing.

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

Sequence Diagram(s)

sequenceDiagram
participant Client
participant StreamingDispatch
participant UpstreamTarget
participant RoutingTelemetry
Client->>StreamingDispatch: stream chat completion
StreamingDispatch->>UpstreamTarget: initial attempt
UpstreamTarget-->>StreamingDispatch: retryable failure
StreamingDispatch->>RoutingTelemetry: record failed attempt
StreamingDispatch->>UpstreamTarget: retry same target
UpstreamTarget-->>StreamingDispatch: failure
StreamingDispatch->>UpstreamTarget: fallback target attempt
UpstreamTarget-->>StreamingDispatch: stream response or failure
StreamingDispatch-->>Client: SSE completion or error
Loading

Possibly related PRs

  • api7/aisix#783: Overlaps with streaming routing attempt orchestration and per-target failure handling.
  • api7/aisix#784: Overlaps with streaming completion and telemetry handling.

Suggested reviewers:moonming

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly states the main change: applying routing.retries on the streaming chat path.
Linked Issues check✅ PassedThe PR implements streaming retries before failover and adds regression tests, matching the linked issues' core retry and recovery goals.
Out of Scope Changes check✅ PassedThe changes stay focused on streaming retry routing and related tests, with no clear unrelated scope creep.
E2e Test Quality Review✅ PassedE2E coverage hits app→router→mock upstream, names/assertions are clear, and streaming retry logic matches non-streaming parity with focused regression tests.
Security Check✅ PassedNo sensitive-data logging, auth bypass, ownership, TLS, or secret-handling regressions found in the touched retry code; it only adds retry metadata and existing BridgeError text.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/stream-retries-1119

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

@jarvis9443

Copy link
Copy Markdown
ContributorAuthor

@coderabbitai review

@coderabbitai

coderabbitaiBot commented Jul 23, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

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

@jarvis9443

Copy link
Copy Markdown
ContributorAuthor

@coderabbitai full review

@coderabbitai

coderabbitaiBot commented Jul 23, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. Your recent PR review activity is in the 95th percentile or higher among CodeRabbit users, so adaptive limits apply. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 42 minutes.

@jarvis9443

Copy link
Copy Markdown
ContributorAuthor

@coderabbitai full review

@coderabbitai

coderabbitaiBot commented Jul 23, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

Sign up for freeto 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

@jarvis9443