Skip to content

feat(routing): mid-stream fallback — resume a committed stream on fallback targets - #882

Merged
jarvis9443 merged 2 commits into
mainfrom
feat/mid-stream-fallback
Aug 4, 2026
Merged

feat(routing): mid-stream fallback — resume a committed stream on fallback targets#882
jarvis9443 merged 2 commits into
mainfrom
feat/mid-stream-fallback

Conversation

@jarvis9443

@jarvis9443 jarvis9443 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Problem

Once a streaming response's first chunk reaches the client the 200 is committed and the pre-first-chunk failover of #554 can no longer help. A mid-generation transport break / inter-chunk stall / decode failure / provider in-band error today can only terminate the stream (in-band error frame, no [DONE]) — long generations die with no recovery even when healthy fallback targets exist, and by HTTP status alone the request still looks like a success.

Change

New routing.stream_failure block — explicit opt-in, omitted keeps today's behavior exactly:

routing:
  stream_failure:
    mode: continue          # terminate (default) | continue
    on: [transport_error, read_timeout, upstream_decode_error, upstream_in_band_error]  # default: all four
    max_fallbacks: 1        # mid-stream-specific budget (deliberately tighter than pre-stream)

With continue, a qualifying mid-stream error hands the same client stream to the remaining targets (strategy order, runtime health/cooldown re-checked at switch time, failed target cooled down):

  • The fallback request = original messages + LiteLLM's verbatim continuation system instruction + the partial text as an assistant message — which is native prefill on Anthropic-wire targets. An empty partial (failure beat the first content delta) retries with untouched messages (LiteLLM's is_pre_first_chunk branch).
  • Retryability reuses is_retryable: a non-429 in-band 4xx never triggers; retry_on_429 / fallback_on_statuses apply.
  • On recovery the client sees primary content → fallback continuation → exactly one [DONE]. On exhaustion: in-band error frame, no fabricated [DONE].
  • Client-cancel safety is structural: the failover combinator (stream_failover.rs) only advances when the client pulls, so an abandoned stream can never dispatch a ghost fallback request.

Safety boundaries (deliberate divergence from LiteLLM, which has no output-shape guards and will splice a re-emitted tool call after half-delivered arguments): streams that already emitted tool-call or reasoning deltas, structured-output requests (response_format: json_object|json_schema), and partials past the 1 MiB continuation cap keep the terminate behavior.

Telemetry (#655 / #1074 contracts)

  • The failed attempt emits its own per-attempt UsageEvent at switch time: estimated partial spend (prompt + delivered partial, usage_estimated), error class/message, attempt_kind preserved.
  • The terminal event is attributed to the serving target (attempt_kind: mid_stream_fallback, its latency clock), not the pre-stream winner; when the stream still dies it now carries the terminal error_class/error_message.
  • New request-level UsageEvent.stream_outcome: success / partial_failed / partial_recovered — the HTTP status froze at 200, this is the signal that separates "200" from "stream actually completed". partial_failed also lands on plain terminate-mode failures, so the observability gain applies with the feature off.
  • Client-facing usage frames fold the failed partials' estimated usage in (LiteLLM's merge semantics); the serving attempt's own event stays attempt-scoped.
  • New counter aisix_mid_stream_fallbacks_total{model, outcome=recovered|failed}.

Scope

First phase covers /v1/chat/completions (parsed-SSE). /v1/responses and /v1/messages keep terminate semantics pending protocol-specific continuation design (issue scope agrees). served_by_target in the access log still names the pre-stream winner (the log line is emitted at head commit); usage events carry the real serving attempts.

Tests

  • Rust integration (proxy, wiremock): same-stream recovery incl. continuation-body shape assertion; default-config terminate + fallback never contacted; fallback exhaustion (error frame, no [DONE]); tool-call safety gate; on trigger-list narrowing. Unit: trigger taxonomy, continuation builder, structured-output detection, schema round-trips.
  • TS E2E (real aisix + etcd + mock upstream): real mid-body connection drop → seamless continuation (client sees one logical answer, fallback got the continuation shape); real inter-chunk stall past stream_timeoutread_timeout trigger; client abort mid-stream → fallback target never called.

Fixes api7/AISIX-Cloud#1222

Summary by CodeRabbit

  • New Features

    • Added configurable recovery for failures occurring after streaming responses begin.
    • Streams can terminate or continue with fallback targets for selected failure types.
    • Added limits for fallback attempts and continuation context for partially generated responses.
    • Added streaming outcome details and fallback metrics for monitoring.
  • Bug Fixes

    • Client cancellations no longer trigger unnecessary fallback requests.
    • Unsupported structured outputs and unsafe partial responses terminate cleanly.

…lback targets

Once a streaming response's first chunk is delivered the 200 is
committed and the pre-stream retry/failover loop is out of reach: a
mid-generation transport break, inter-chunk stall, decode failure, or
provider in-band error could only terminate the stream (in-band error
frame, no [DONE]). Long generations died with no recovery even when
healthy fallback targets existed (AISIX-Cloud#1222).

Add routing.stream_failure — explicit opt-in, default terminate:

  stream_failure:
    mode: continue            # terminate (default) | continue
    on: [transport_error, read_timeout,
         upstream_decode_error, upstream_in_band_error]   # default: all
    max_fallbacks: 1

With continue, a qualifying mid-stream error hands the SAME client
stream to the remaining targets (strategy order, runtime state
re-checked, cooldown/health recorded). The fallback request carries
the original messages plus LiteLLM's verbatim continuation
instruction and the partial text as an assistant message — native
prefill on Anthropic-wire targets. Retryability reuses is_retryable
(non-429 in-band 4xx never triggers; retry_on_429 and
fallback_on_statuses apply).

Safety boundaries (deliberate divergence from LiteLLM, which falls
back with no output-shape guards): streams that emitted tool-call or
reasoning deltas, structured-output requests
(response_format json_object/json_schema), and partials past the 1MiB
continuation cap keep the terminate behavior.

Telemetry: the failed attempt emits its own per-attempt UsageEvent
with estimated partial spend (prompt + delivered partial, #655/#1074
contracts); the terminal event is attributed to the SERVING target
(attempt_kind mid_stream_fallback) with the terminal error class when
the stream still dies; new request-level stream_outcome
(success | partial_failed | partial_recovered) separates 'HTTP 200'
from 'stream completed' — partial_failed also lands on plain
terminate-mode failures. Client-facing usage frames fold the partial
estimate in (LiteLLM's merge semantics). New counter
aisix_mid_stream_fallbacks_total{model,outcome}.

Client-cancel safety is structural: the failover combinator advances
only when the client pulls, so an abandoned stream can never dispatch
a ghost fallback request (#1094 family).

First phase covers /v1/chat/completions; /v1/responses and
/v1/messages keep terminate semantics pending protocol-specific
continuation design.
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 31 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: be900f95-1d01-488a-93a8-dcdb69c87ab3

📥 Commits

Reviewing files that changed from the base of the PR and between 64e2bf2 and 93722d6.

📒 Files selected for processing (6)
  • crates/aisix-core/src/models/routing.rs
  • crates/aisix-proxy/src/chat.rs
  • crates/aisix-proxy/src/stream_failover.rs
  • schemas/resources/model.schema.json
  • schemas/resources/routing.schema.json
  • tests/e2e/src/cases/mid-stream-fallback-e2e.test.ts
📝 Walkthrough

Walkthrough

The PR adds configurable mid-stream failure handling. Streaming requests can terminate or continue on fallback targets. The proxy preserves partial output, emits continuation requests, tracks partial usage, and records stream outcomes and fallback metrics.

Changes

Mid-stream failover

Layer / File(s) Summary
Stream failure contract
crates/aisix-core/src/models/routing.rs, crates/aisix-core/src/lib.rs, crates/aisix-core/src/models/mod.rs, schemas/resources/*.json, crates/aisix-admin/src/openapi.rs
Routing models and schemas define stream_failure, fallback modes, trigger types, defaults, and fallback limits.
Mid-stream failover engine
crates/aisix-proxy/src/stream_failover.rs, crates/aisix-proxy/src/lib.rs
The proxy classifies eligible failures, preserves partial content, builds continuation requests, dispatches fallback targets, and handles exhaustion and unsafe output.
Streaming integration and telemetry
crates/aisix-proxy/src/chat.rs, crates/aisix-obs/src/metrics.rs, crates/aisix-obs/src/usage.rs, crates/aisix-proxy/src/routing.rs
Streaming state tracks serving targets, partial usage, terminal errors, stream outcomes, failed attempts, and fallback metrics.
Failover behavior validation
crates/aisix-proxy/src/lib.rs, tests/e2e/src/cases/mid-stream-fallback-e2e.test.ts
Tests cover connection drops, read timeouts, default termination, fallback exhaustion, tool-call safety, trigger filtering, and client cancellation.

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

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Proxy
  participant Primary
  participant Fallback
  Client->>Proxy: Start streaming chat completion
  Proxy->>Primary: Forward request
  Primary-->>Proxy: Partial chunks and mid-stream failure
  Proxy->>Fallback: Send continuation request with partial output
  Fallback-->>Proxy: Remaining completion chunks
  Proxy-->>Client: Combined stream and one [DONE]
Loading

Possibly related PRs

  • api7/aisix#794: Shares streaming usage accounting and partial token propagation.
  • api7/aisix#807: Shares streaming attempt and failure handling in chat.rs.
  • api7/aisix#816: Shares routing and streaming retry or failover behavior.

Suggested reviewers: moonming

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
E2e Test Quality Review ⚠️ Warning The E2E file covers only connection drops, read stalls, and client cancellation; default termination, exhaustion/max_fallbacks, in-band/decode filtering, and safety gates remain Rust-only. Add E2E cases for default terminate, fallback exhaustion and max_fallbacks boundaries, in-band/decode trigger filtering, and tool/structured-output safety; assert raw SSE has exactly one or no [DONE] as required.
✅ 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 clearly and concisely describes the primary change: resuming committed streams on fallback targets.
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.
Security Check ✅ Passed Security review found no vulnerabilities across all 7 categories: no credential leakage in logs, no unencrypted secret storage, proper authorization boundaries, no cross-resource access issues, cor...
✨ 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 feat/mid-stream-fallback

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

🧹 Nitpick comments (4)
crates/aisix-core/src/models/routing.rs (2)

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

Remove the internal tracker id from the public model comment.

The doc comment on StreamFailure is rendered into schemas/resources/routing.schema.json (line 178) and into the Admin API OpenAPI description. AISIX-Cloud#1222 is internal shorthand and has no meaning for an API consumer. Sibling fields in Routing carry no tracker ids. Keep the issue reference in the PR description or in a non-doc comment.

♻️ Proposed comment rewrite
-/// Mid-stream failure policy for streaming responses (AISIX-Cloud#1222).
+/// Mid-stream failure policy for streaming responses.
 ///
 /// Applies only to failures that occur after the response head (and
 /// possibly some chunks) reached the client; failures before the first
 /// chunk keep using the regular retry/failover loop.

Regenerate the schemas after the edit:

cargo run -p aisix-core --bin dump-schema

As per coding guidelines: "Write model comments as public API reference text, avoid internal shorthand" and "Regenerate resource schemas after changing model comments with cargo run -p aisix-core --bin dump-schema".

🤖 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/aisix-core/src/models/routing.rs` around lines 195 - 199, Remove the
internal “AISIX-Cloud#1222” tracker reference from the public doc comment on
StreamFailure while preserving the behavioral description of mid-stream
failures. Regenerate the resource schemas using the aisix-core dump-schema
command so the generated routing schema and Admin API documentation reflect the
updated comment.

Source: Coding guidelines


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

Add unit tests for the new accessors and the serialized wire values.

This test module pins the wire contract for every other routing knob: sticky_parses_and_defaults_false, when_all_unavailable_parses_try_anyway, and when_all_unavailable_rejects_unknown_value. stream_failure has no equivalent here. The proxy tests exercise behavior, but nothing in this crate pins that mode: "continue" and on: ["read_timeout"] deserialize, that an unknown mode is rejected, or that max_fallbacks_or_default() returns 1 and on_or_default() returns all four triggers when unset.

💚 Proposed test
#[test]
fn stream_failure_parses_and_defaults() {
    let r: Routing = serde_json::from_str(
        r#"{"targets":[{"model":"a"},{"model":"b"}],
            "stream_failure":{"mode":"continue","on":["read_timeout"]}}"#,
    )
    .unwrap();
    let sf = r.stream_failure.as_ref().unwrap();
    assert_eq!(sf.mode_or_default(), StreamFailureMode::Continue);
    assert_eq!(sf.on_or_default(), &[StreamFailureTrigger::ReadTimeout]);
    // Unset max_fallbacks defaults to one mid-stream attempt.
    assert_eq!(sf.max_fallbacks_or_default(), 1);

    // Omitted `on` selects every trigger class.
    let all: Routing = serde_json::from_str(
        r#"{"targets":[{"model":"a"}],"stream_failure":{}}"#,
    )
    .unwrap();
    let sf = all.stream_failure.as_ref().unwrap();
    assert_eq!(sf.mode_or_default(), StreamFailureMode::Terminate);
    assert_eq!(sf.on_or_default().len(), 4);

    // Unknown mode is rejected by the strict enum.
    assert!(serde_json::from_str::<Routing>(
        r#"{"targets":[{"model":"a"}],"stream_failure":{"mode":"resume"}}"#,
    )
    .is_err());
}

As per coding guidelines: "Prioritize end-to-end coverage over unit or integration coverage when coverage is limited, and never skip, disable, or use .only to silence a failing test."

🤖 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/aisix-core/src/models/routing.rs` around lines 220 - 241, Add a
routing test covering StreamFailure deserialization and accessor defaults. In
the existing routing test module, verify mode "continue", on ["read_timeout"],
and max_fallbacks_or_default() returning 1; also verify omitted on defaults to
all four triggers and the default mode is Terminate, and assert that an unknown
mode such as "resume" is rejected.

Source: Coding guidelines

crates/aisix-obs/src/metrics.rs (1)

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

aisix_mid_stream_fallbacks_total ships with no test coverage at any level. The PR adds the metric, its recorder, and both outcome values, but no test asserts the counter, its model label, or either outcome. If record_mid_stream_fallback is never called, or is called with the wrong label, the whole change set still passes. Every comparable counter in crates/aisix-obs/src/metrics.rs has a rendering test, and the proxy suite already scrapes state.metrics.render() elsewhere.

  • crates/aisix-obs/src/metrics.rs#L1190-L1203: add a unit test that calls record_mid_stream_fallback once with recovered = true and once with recovered = false, then asserts the rendered exposition carries aisix_mid_stream_fallbacks_total with outcome="recovered" and outcome="failed" under the expected model label, each at 1.
  • crates/aisix-proxy/src/lib.rs#L4771-L4788: keep the Metrics handle from build_state in mid_stream_failure_continues_on_fallback_target_in_same_stream, then assert the scrape contains model="smart" with outcome="recovered".
  • crates/aisix-proxy/src/lib.rs#L4889-L4899: apply the same handle-and-scrape pattern in mid_stream_fallback_exhaustion_surfaces_error_without_done, asserting outcome="failed".
🤖 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/aisix-obs/src/metrics.rs` around lines 1190 - 1203, The new mid-stream
fallback metric lacks coverage. In crates/aisix-obs/src/metrics.rs:1190-1203,
add a unit test for record_mid_stream_fallback that records both recovered and
failed outcomes and verifies rendered aisix_mid_stream_fallbacks_total samples
have model and outcome labels with value 1. In
crates/aisix-proxy/src/lib.rs:4771-4788, retain the Metrics handle from
build_state in mid_stream_failure_continues_on_fallback_target_in_same_stream
and assert the scrape includes model="smart", outcome="recovered"; apply the
same pattern at crates/aisix-proxy/src/lib.rs:4889-4899 for
mid_stream_fallback_exhaustion_surfaces_error_without_done, asserting
outcome="failed".
crates/aisix-proxy/src/stream_failover.rs (1)

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

Log the configuration-error skips.

The runtime-state skip at lines 339-343 emits a debug event naming the target and the status. The three misconfiguration skips here emit nothing. If every remaining candidate is misconfigured, the client receives last_err — the original upstream error — and the operator gets no signal that the fallback chain was abandoned for a configuration reason. resolve_provider_key already builds a descriptive message (see crates/aisix-proxy/src/dispatch.rs lines 75-91) that is discarded here.

♻️ Proposed fix
         let model = &attempt.model;
-        let Ok(provider) = crate::dispatch::require_provider(model) else {
-            continue;
-        };
+        let provider = match crate::dispatch::require_provider(model) {
+            Ok(p) => p,
+            Err(err) => {
+                tracing::warn!(
+                    request_id = %plan.request_id,
+                    target = %model.display_name,
+                    error = %err,
+                    "skipping mid-stream fallback candidate (misconfigured target)",
+                );
+                continue;
+            }
+        };
         let provider = provider.to_ascii_lowercase();
         let snapshot = plan.state.snapshot.load();
-        let Ok(pk_entry) = crate::dispatch::resolve_provider_key(&snapshot, model) else {
-            continue;
-        };
+        let pk_entry = match crate::dispatch::resolve_provider_key(&snapshot, model) {
+            Ok(pk) => pk,
+            Err(err) => {
+                tracing::warn!(
+                    request_id = %plan.request_id,
+                    target = %model.display_name,
+                    error = %err,
+                    "skipping mid-stream fallback candidate (provider key unresolved)",
+                );
+                continue;
+            }
+        };
         let Some(bridge) = crate::dispatch::resolve_bridge(&plan.state.hub, &pk_entry.value) else {
+            tracing::warn!(
+                request_id = %plan.request_id,
+                target = %model.display_name,
+                "skipping mid-stream fallback candidate (no registered bridge)",
+            );
             continue;
         };
🤖 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/aisix-proxy/src/stream_failover.rs` around lines 346 - 357, Update the
fallback candidate handling around require_provider, resolve_provider_key, and
resolve_bridge to emit debug events before each configuration-error continue,
including the target/model and the descriptive error where available. Reuse the
error message produced by resolve_provider_key, and clearly identify
provider-resolution and bridge-resolution skips while preserving the existing
continue behavior.
🤖 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/aisix-proxy/src/chat.rs`:
- Around line 1732-1746: Update the record_mid_stream_fallback call to derive
recovered from the mid_stream_fallbacks recovery state rather than comparing
stream_outcome to "partial_recovered". Preserve the existing stream_outcome
classification while ensuring any stream with a successful mid-stream fallback
is recorded as recovered, including client-aborted or guardrail-blocked cases.
- Around line 1655-1679: Update stream_failover::acquire_fallback_stream to call
reserve_routing_target before dispatching each fallback target through
bridge.chat_stream. Skip targets whose reservation is rejected, and retain the
successful reservation so its usage is included in post_stream_keys and billed
to that fallback target. Ensure the MidStreamPlan fallback flow carries this
reservation through post-stream accounting.
- Around line 4705-4715: Update the usage handling in build_sse_stream so
mid_stream_extra is folded into only one terminal usage frame after
stream_failover::wrap completes, rather than every usage-bearing chunk. Preserve
the serving-attempt usage captured by comp and ensure earlier usage chunks
remain unmodified while the final client-facing usage includes the accumulated
failed-attempt estimate once.

In `@crates/aisix-proxy/src/stream_failover.rs`:
- Around line 358-377: Reserve the fallback target’s per-model quota before
dispatching each continuation in the loop containing bridge.chat_stream. Resolve
the gate from attempt.model using the existing quota::reserve_model_only flow,
skip the candidate and continue when reservation is refused, and commit or drop
the reservation along the existing success and failure paths.
- Around line 151-213: Update stream_failover::wrap so the
Box::pin(async_stream::stream! { ... }) generator is passed through
crate::request_id::in_request_span before returning. Preserve the existing
generator body and ChatChunkStream behavior while ensuring tracing and outbound
requests execute within the request span.

In `@schemas/resources/model.schema.json`:
- Around line 669-715: Update the doc comments for StreamFailure and
StreamFailureMode in routing.rs to remove the internal AISIX-Cloud tracker
reference and replace the unresolved StreamFailure::mode rustdoc link with a
standalone public description of the enum. Regenerate the schema using
dump-schema so the StreamFailure and StreamFailureMode descriptions contain only
consumer-facing API text.

In `@tests/e2e/src/cases/mid-stream-fallback-e2e.test.ts`:
- Around line 119-195: Set an explicit 30-second timeout on the test named
“connection drop mid-stream continues on the fallback target in the same
stream,” matching the timeout used by its sibling tests. Keep the test logic and
assertions unchanged.

---

Nitpick comments:
In `@crates/aisix-core/src/models/routing.rs`:
- Around line 195-199: Remove the internal “AISIX-Cloud#1222” tracker reference
from the public doc comment on StreamFailure while preserving the behavioral
description of mid-stream failures. Regenerate the resource schemas using the
aisix-core dump-schema command so the generated routing schema and Admin API
documentation reflect the updated comment.
- Around line 220-241: Add a routing test covering StreamFailure deserialization
and accessor defaults. In the existing routing test module, verify mode
"continue", on ["read_timeout"], and max_fallbacks_or_default() returning 1;
also verify omitted on defaults to all four triggers and the default mode is
Terminate, and assert that an unknown mode such as "resume" is rejected.

In `@crates/aisix-obs/src/metrics.rs`:
- Around line 1190-1203: The new mid-stream fallback metric lacks coverage. In
crates/aisix-obs/src/metrics.rs:1190-1203, add a unit test for
record_mid_stream_fallback that records both recovered and failed outcomes and
verifies rendered aisix_mid_stream_fallbacks_total samples have model and
outcome labels with value 1. In crates/aisix-proxy/src/lib.rs:4771-4788, retain
the Metrics handle from build_state in
mid_stream_failure_continues_on_fallback_target_in_same_stream and assert the
scrape includes model="smart", outcome="recovered"; apply the same pattern at
crates/aisix-proxy/src/lib.rs:4889-4899 for
mid_stream_fallback_exhaustion_surfaces_error_without_done, asserting
outcome="failed".

In `@crates/aisix-proxy/src/stream_failover.rs`:
- Around line 346-357: Update the fallback candidate handling around
require_provider, resolve_provider_key, and resolve_bridge to emit debug events
before each configuration-error continue, including the target/model and the
descriptive error where available. Reuse the error message produced by
resolve_provider_key, and clearly identify provider-resolution and
bridge-resolution skips while preserving the existing continue behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e52bccc6-9943-4b2f-b539-20bcb7b7a2fc

📥 Commits

Reviewing files that changed from the base of the PR and between 5bddcf9 and 64e2bf2.

📒 Files selected for processing (13)
  • crates/aisix-admin/src/openapi.rs
  • crates/aisix-core/src/lib.rs
  • crates/aisix-core/src/models/mod.rs
  • crates/aisix-core/src/models/routing.rs
  • crates/aisix-obs/src/metrics.rs
  • crates/aisix-obs/src/usage.rs
  • crates/aisix-proxy/src/chat.rs
  • crates/aisix-proxy/src/lib.rs
  • crates/aisix-proxy/src/routing.rs
  • crates/aisix-proxy/src/stream_failover.rs
  • schemas/resources/model.schema.json
  • schemas/resources/routing.schema.json
  • tests/e2e/src/cases/mid-stream-fallback-e2e.test.ts

Comment thread crates/aisix-proxy/src/chat.rs
Comment thread crates/aisix-proxy/src/chat.rs
Comment thread crates/aisix-proxy/src/chat.rs
Comment thread crates/aisix-proxy/src/stream_failover.rs
Comment thread crates/aisix-proxy/src/stream_failover.rs
Comment thread schemas/resources/model.schema.json
Comment thread tests/e2e/src/cases/mid-stream-fallback-e2e.test.ts Outdated
…mpt usage isolation, honest metric

- Reserve the fallback target's own rate-limit layers before the
  continuation dispatch (reserve_routing_target, AISIX-Cloud#1087
  parity): refused reservation skips the candidate as a recorded 429
  attempt without burning fallback budget; a granted one converts to a
  stream-lifetime concurrency hold (released on switch/stream end) and
  registers its keys for post-stream TPM accounting in the completion
  closure.
- Reset the pump's usage accumulators when the serving attempt
  switches (shared attempt_seq): a per-chunk-usage provider (Gemini)
  would otherwise max-wins-mix the failed attempt's partial counters
  into the serving attempt's terminal event and double-fold with the
  estimated partial.
- The mid-stream fallback metric now reports recovered = no terminal
  stream error, so a client that disconnects after a successful switch
  (or a guardrail block on recovered content) no longer counts as a
  failed failover.
- Schema polish: drop the issue-tracker shorthand and the rustdoc
  intra-doc link from the public StreamFailure/StreamFailureMode
  descriptions (regenerated).
- e2e: explicit 30s timeout on the connection-drop test, matching its
  siblings.
@jarvis9443
jarvis9443 merged commit 7d519b7 into main Aug 4, 2026
12 checks passed
@jarvis9443
jarvis9443 deleted the feat/mid-stream-fallback branch August 4, 2026 10:42
jarvis9443 added a commit that referenced this pull request Aug 5, 2026
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