Skip to content

Enable opt-in realtime streaming across client protocols - #41

Merged
maiphucgiang merged 3 commits into
mainfrom
fix/realtime-tool-streaming
Sep 24, 2026
Merged

maiphucgiang merged 3 commits into
mainfrom
fix/realtime-tool-streaming

Conversation

@maiphucgiang

@maiphucgiang maiphucgiang commented Sep 23, 2026 •

Copy link
Copy Markdown
Owner

Fixes #39.

Changes

  • Add opt-in stream_mode=realtime for incremental Chat Completions, Responses and Messages output, including reasoning and tool arguments. Keep compatible as the default, preserving aggregate validation and bounded tool repair; non-streaming requests remain validated JSON.
  • Expose --stream-mode, CODEBUDDY2API_STREAM_MODE and the WebUI enum through existing configuration precedence and source locking. Freeze mode and retained-output budget before routing for every generation request, including non-streaming failovers and aggregate repair attempts; record the entry mode in request audits.
  • Give Responses items and Anthropic blocks stable indexes, assemble fragmented tool identities and deliver arguments exactly once. Realtime Messages keeps one content block open, streams its arguments immediately and defers later blocks within the shared budget. Treat empty upstream finish reasons as non-terminal while rejecting output after an actual terminal marker.
  • Validate tool identities, declared names, JSON-object arguments and choices before successful completion. Realtime errors never regenerate tools or replay an opened response; preserve pre-response HTTP errors, bounded failover and legitimate truncation/filter outcomes. Filter-only terminals retain available usage and map to Responses response.incomplete, even without text, while ordinary empty or broken streams remain errors. Explicitly close nested streams and release capacity on errors or cancellation.
  • Add deterministic streaming regressions and synchronize English/Chinese configuration, client and rollback documentation.

Verification

  • All 62 backend regression scripts pass: 1240 unittest cases reported, including 2 existing platform-conditional skips. The realtime suite has 40 tests covering three protocols, both modes, text/tool timing, fragmented metadata, stable indexes, sequential Messages block lifecycles, terminal validation, cancellation, retained-output budgets, non-streaming snapshots and empty filter terminals. Seven additional independent review regressions pass.
  • Focused protocol, audit, failover and cancellation coverage: 290 tests and 1001 subtests pass. An additional 64 deterministic multi-tool/text/thinking interleavings preserve serial block lifecycles and exact reconstructed values. Staged contents match the full-regression source fingerprints.
  • WebUI: 133 tests across 15 files, formatting, lint, type checks and production build pass.
  • Thirteen local deployment cases at 6f4a0fc pass with zero-multiplier hy3 through intl-cli: all three protocols deliver text/reasoning and tool-argument deltas before their terminal event, reconstructed tool JSON matches, non-stream responses and tool-history round trips succeed, invalid input is rejected locally, and a cancelled stream releases capacity for the next request. The review follow-ups are covered by the backend regressions above.
  • SQLite audits for that live run confirm 11 successful requests with one upstream attempt each, one expected local error with no upstream attempt, and one correctly recorded cancellation. Usage is retained for completed calls; final health is OK with zero in-flight requests. That local tmux instance remains explicitly pinned to realtime.

Scope and rollback

  • Realtime is a server setting, not a client override. Clients must tolerate partial output followed by an error; malformed tool arguments are not regenerated. Tools require a valid tool_calls terminal marker for successful realtime completion. Models or clients needing the previous behavior can use compatible.
  • The retained-output limit includes adapter state, tool validation and deferred Messages event bytes; max_collect_bytes=0 retains the existing unlimited convention. Tool identity changes after emission are rejected. In Messages, later blocks may wait until upstream completion while the active tool remains incremental; a tool still awaiting identity does not hold unrelated text before its block starts.
  • Select compatible for runtime fallback. Before downgrading source, stop the gateway, remove the new startup option and back up current state; if stream_mode was saved, use the documented narrow offline removal with a revision increment and integrity check. Do not restore a stale database over newer claims, revocations or account state. The bilingual procedure was verified against a temporary database with unrelated state preserved.

Summary by Sourcery

Enable opt-in realtime streaming across all client protocols while preserving compatible defaults and strengthening terminal validation, resource handling, configuration, and operational safeguards.

New Features:

  • Add a server-configured opt-in realtime streaming mode for Chat Completions, Responses, and Anthropic Messages protocols.
  • Expose stream mode through CLI, environment, WebUI, Compose, configuration locking, and request audit records.

Bug Fixes:

  • Prevent invalid terminal output, malformed tool calls, identity changes, post-terminal data, and incomplete streams from being reported as successful realtime responses.
  • Preserve protocol-specific truncation and content-filter outcomes, usage data, pre-response HTTP errors, bounded failover behavior, and resource cleanup on errors or cancellation.

Enhancements:

  • Stream reasoning, text, refusals, and tool arguments incrementally with stable protocol indexes and ordered block lifecycles.
  • Introduce shared tool-call validation and retained-output budgeting across accumulators, adapters, deferred events, failover attempts, and non-streaming aggregation.
  • Keep compatible mode as the default, including aggregate validation and bounded tool repair, while freezing streaming policy before routing.

Build:

  • Forward the stream mode environment variable through Docker Compose.

Deployment:

  • Document realtime configuration, client expectations, runtime fallback, and safe offline rollback of persisted settings.

Documentation:

  • Update English and Chinese advanced and client documentation for streaming modes, partial-output errors, validation behavior, and rollback procedures.

Tests:

  • Add deterministic coverage for realtime behavior across all three protocols, including interleaving, stable indexes, fragmented tool metadata, terminal validation, budgets, failover, cancellation, audits, configuration precedence, and WebUI settings.

@sourcery-ai

sourcery-ai Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

This PR adds an opt-in, server-controlled realtime streaming path for Chat Completions, Responses, and Anthropic Messages while retaining compatible aggregation and tool repair by default. It centralizes request policy snapshots, output budgets, and terminal/tool validation, adapts incremental events with stable protocol indexes, hardens post-error cleanup and auditing, and documents configuration, client expectations, and rollback procedures.

Sequence diagram for realtime streaming and terminal validation

sequenceDiagram
    participant Client
    participant Gateway
    participant Upstream
    participant Accumulator as ChatSSEAccumulator
    participant Adapter as ProtocolConverter

    Client->>Gateway: POST stream=true
    Gateway->>Gateway: _snapshot_stream_policy
    Gateway->>Upstream: Open backend stream
    Upstream-->>Accumulator: SSE output deltas
    Accumulator->>Accumulator: merge_tool_call_delta
    Accumulator-->>Adapter: Validated incremental state
    Adapter->>Adapter: _flush_tool_slot
    Adapter-->>Client: Text, reasoning, or tool deltas
    Upstream-->>Accumulator: finish_reason
    Accumulator->>Accumulator: seal_tool_identities
    Accumulator->>Gateway: result
    Gateway->>Adapter: set_validated_tools
    Adapter->>Adapter: finish
    Adapter-->>Client: Protocol terminal event
    alt Invalid terminal or tool metadata
        Adapter-->>Client: Protocol error event
    end
Loading

File-Level Changes

Change Details Files
Added a shared realtime streaming policy and bounded output-validation state across all generation protocols.
  • Introduced compatible/realtime mode selection with frozen per-request mode and collection budget.
  • Threaded shared budgets and tool state through Chat SSE validation and protocol adapters.
  • Preserved compatible aggregation, bounded tool repair, non-stream validation, failover behavior, and audit mode recording.
converter.py
app/upstream_io.py
app/observability.py
app/audit_store.py
Implemented incremental Responses and Anthropic streaming with stable output/block ordering and validated tool emission.
  • Streamed reasoning, text, refusal, and tool-argument deltas in realtime mode.
  • Delayed tool emission until identities are complete, assembles fragmented metadata append-only, and emits arguments once.
  • Added terminal-marker, post-terminal, identity, declared-name, JSON-object, and tool-choice validation without replaying malformed streams.
app/adapters/responses_adapter.py
app/adapters/anthropic_adapter.py
Exposed streaming mode through configuration, deployment, WebUI, and rollback-safe operational documentation.
  • Added CLI, environment, Compose, persisted-setting precedence, source locking, and WebUI enum support.
  • Documented realtime semantics, partial-output errors, retained-output limits, client requirements, and offline rollback procedure in English and Chinese.
app/settings.py
converter.py
docker-compose.yml
docs/advanced.md
docs/advanced.zh-CN.md
docs/clients.md
docs/clients.zh-CN.md
Added broad regression coverage for protocol behavior, resource cleanup, configuration snapshots, and auditing.
  • Covered all three protocols in both modes, including timing, stable indexes, fragmented tool identity, terminal validation, truncation/filter outcomes, and UTF-8 budgets.
  • Verified cancellation/error cleanup, capacity release, usage retention, bounded failover, audit records, configuration precedence, and WebUI validation.
tests/test_realtime_streaming.py
tests/test_environment_config.py
tests/test_webui_integration.py

Assessment against linked issues

Issue Objective Addressed Explanation
#39 Restore genuine incremental streaming for requests containing tools, including text, reasoning content, and tool-call arguments, rather than aggregating and replaying the completed response. ✅
#39 Provide a safe, configurable rollout that preserves the existing aggregation, validation, and tool-repair behavior by default while allowing deployments to opt into realtime streaming. ✅
#39 Support the streaming fix consistently across Chat Completions, Responses, and Anthropic Messages protocols without compromising terminal validation, failover behavior, resource cleanup, or documentation. ✅

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-23T22:42:52.271044Z 4d47414 New commits
🔒 Security Review ✅ Completed 2026-09-23T22:15:27.029423Z 6f4a0fc PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@sourcery-ai sourcery-ai 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.

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="converter.py" line_range="2798-2800" />
<code_context>
     client_wants_stream = _client_wants_stream(payload)
     body = {k: payload[k] for k in PASSTHROUGH_BODY_KEYS if k in payload}
     body = await run_in_threadpool(_prepare_chat_body, body, session_payload=payload)
+    stream_policy = (_snapshot_stream_policy("chat", body) if client_wants_stream else None)
+    if stream_policy is not None:
+        observe_stream_mode(stream_policy.mode)

     # Record request metadata.
</code_context>
<issue_to_address>
**issue (broader_impact):** The selected `stream_mode` is recorded only when the client requests streaming; non-streaming Chat, Responses, and Messages requests never call `observe_stream_mode`, so their audits omit the selected mode despite the request-audit requirement.

**Triggers:** When a client sends `stream: false` or omits the streaming field.

**Suggested fix:** Snapshot and record the mode for every generation request, not only inside the streaming branches.
</issue_to_address>

### Comment 2
<location path="converter.py" line_range="3767" />
<code_context>

     chat_body = await run_in_threadpool(_prepare_chat_body, chat_body, session_payload=payload)
+    client_wants_stream = _client_wants_stream(payload)
+    stream_policy = (_snapshot_stream_policy("messages", chat_body) if client_wants_stream else None)
+    if stream_policy is not None:
+        observe_stream_mode(stream_policy.mode)
</code_context>
<issue_to_address>
**issue (bug_risk):** Non-streaming requests do not receive a frozen stream policy or retained-output budget; `_nonstream_adapted` invokes `_fetch_checked_chat` without `max_collect_bytes`, causing failover attempts to read the live `CONFIG` value instead of the request-start budget.

**Triggers:** When `max_collect_bytes` changes while a non-streaming request is undergoing credential failover.

**Suggested fix:** Create and pass a request-start budget snapshot through the non-streaming fetch and failover path as well.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 2 findings to address first, and realtime mode changes when tool calls become externally visible and can emit partial tool arguments before terminal validation; a client could act on an incorrect or premature tool call, and reverting cannot undo already delivered output or upstream attempts consumed. The default remains compatible and the setting is reversible, but the new opt-in path spans multiple protocol adapters and its failure modes can outlive a source revert.

Blocking findings: converter.py:2800, converter.py:3767


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread converter.py Outdated
Comment thread converter.py Outdated

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6f4a0fcb36

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread converter.py Outdated
state["tracker"] = tracker

def completed():
merged = tracker.result()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve empty content-filter terminals in realtime mode

When a realtime upstream response ends with content_filter, content-filter, or refusal without emitting text or tool bytes—a valid filter-only outcome—this call raises UpstreamResponseError from ChatSSEAccumulator.result() before _validate_realtime_tools(..., filtered=True) or the protocol adapter can map it. Consequently Responses emits/returns a 502 error instead of response.incomplete, despite the new realtime path explicitly treating filtered terminals as legitimate; handle these terminal reasons before applying the accumulator's empty-output rejection.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3968facb18

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +587 to +591
self._close_content_blocks(events)
events.append(self._evt("content_block_start", {
"index": slot["block_idx"],
"content_block": {"type": "tool_use", "id": state["id"],
"name": state["name"], "input": {}},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Close each Anthropic block before opening the next

In realtime Messages streams, _close_content_blocks() closes only thinking/text blocks, so a second ready tool—or text/reasoning arriving after a tool—causes another content_block_start while the previous tool block remains open. For interleaved parallel calls this produces sequences such as start(tool 1), start(tool 2), …, stop(tool 1), stop(tool 2), rather than completing each Anthropic content-block lifecycle before the next block starts; strict Messages clients can reject or misassemble that stream. Buffer later blocks or close the active tool block before emitting another block start.

Useful? React with 👍 / 👎.

@maiphucgiang
maiphucgiang merged commit ee6f9b0 into main Sep 24, 2026
8 of 9 checks passed
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.

Streaming with tools is silently aggregated — tool-calling clients lose incremental output

1 participant