feat: stream ensemble judge synthesis#606
Conversation
Support stream:true on ensemble models. The panel still buffers non-streaming (needed to synthesize), then the judge's tokens stream to the client by reusing the existing build_sse_stream UNMODIFIED — all change is isolated to the ensemble dispatch branch; the shared SSE builder and the single-upstream streaming path are untouched. - ensemble.rs: split run_ensemble into run_ensemble_panel (phases 1-2 + judge_request) + the judge call, so the judge leg can be streamed. - chat.rs: streaming arm in dispatch_ensemble — run the panel, resolve the judge bridge, judge.chat_stream(), feed it to build_sse_stream with an on_complete that emits one usage event per sub-call (panel + judge) and commits panel+judge tokens via add_tokens_post_stream. The "successful panel members are always billed" invariant extends to the judge-connect-failure exit (the panel already round-tripped). - Client sees the ensemble model name on every chunk; judge model names never leak; judge-resolution errors are redacted. - Panel-phase keep-alive is deferred (TODO): the panel runs before the Sse is constructed, no worse than the non-streaming ensemble path. Tests: +2 e2e (judge synthesis streams + [DONE] + per-sub-call usage; judge-connect-failure still bills the panel); replaces the obsolete streaming-rejection test. 259 core + 440 proxy green; clippy -D clean; schema-drift gate clean. Refs #601.
ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Free Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughStreaming support is added for ensemble models using an "Option A" approach: the panel phase runs buffered (non-streaming), the judge request is flipped to streaming, and its SSE output is forwarded to the client. Post-stream telemetry emits per-panel and judge usage events. ChangesEnsemble Streaming (Option A)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Note 🎁 Summarized by CodeRabbit FreeYour organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above. Comment |
Independent audit — safe to mergeA cold-review agent audited all 8 risk points; verdict: safe to merge (zero HIGH, zero MEDIUM). Verified:
LOW follow-ups (non-blocking; tracked for the hardening pass under #601):
|
) Unbreaks main: #606 merged onto a main with #607's MultiReservation API change (semantic conflict, no textual conflict). Ports the streaming-ensemble reservation code to the new API and fixes the latent un-awaited commit_tokens (panel tokens were never billed on streaming error exits). Verified green locally + CI.
Summary
Adds streaming support for
ensemblemodels (DP; follow-up to #604, tracked in #601).stream:trueto an ensemble model now streams the judge's synthesized answer token-by-token, instead of the previous 400.Design: Option A — judge-leg streaming, shared builder reused unmodified
The panel must be buffered to synthesize, so phases 1-2 run non-streaming; only the judge's tokens stream. This reuses the existing
build_sse_streamunmodified — all change is isolated to the ensemble dispatch branch; the shared SSE builder and the single-upstream streaming path are byte-for-byte untouched. (Safety property: a bug in the shared builder would hit every streaming request, so this PR deliberately doesn't touch it.)Deferred — panel-phase keep-alive. The panel fan-out runs before the
Sseis constructed, so the 15s SSE keep-alive doesn't cover it. This is no worse than the already-merged non-streaming ensemble path, which likewise holds the socket bytes-free across panel + judge; TTFB actually improves (judge-first-token vs full answer). True panel keep-alive needs an early SSE handshake before the panel completes — a self-contained follow-up kept out of this PR to avoid refactoring the shared builder. Marked with aTODO.What's in it
ensemble.rs: splitrun_ensembleintorun_ensemble_panel(phases 1-2 +judge_request) + the judge call, so the judge leg can be streamed. No behavior change to the non-streaming path (all existing executor + handler tests pass unchanged).chat.rs::dispatch_ensemblestreaming arm: run the panel → resolve the judge bridge →judge.chat_stream()→ feed it tobuild_sse_streamwith anon_completethat emits one usage event per sub-call (panel + judge,attempt_kindpanel/judge, sharingrequest_id) and commits the panel+judge tokens viaadd_tokens_post_stream(the streaming token-commit API).telemetry_handled_by_stream: truesuppresses the entry-level emit — no double-emit.Test plan
cargo test -p aisix-core— 259 pass.cargo test -p aisix-proxy— 440 pass, incl. 2 new e2e through real wiremock bridges:ensemble_streams_judge_synthesis(replaces the now-obsoleteensemble_rejects_streaming_with_400):stream:true→ SSE body contains the judge content +[DONE], every chunkmodel == "council"(not the upstream id), and the usage sink shows 2 panel + 1 judge events sharing onerequest_id.ensemble_streaming_judge_connect_failure_still_bills_panel: judge 500 on the streaming connect → 502, panel usage events still fire.cargo clippy --all-targets -- -D warningsclean; schema-drift gate clean.An independent audit will follow (merge-gate); I'll post findings + resolutions before merge.
Summary by CodeRabbit