Skip to content

Add websocket upstream bridge for /v1/responses and fix VS Code payload handling#110

Open
PenguinDOOM wants to merge 24 commits into
RayBytes:mainfrom
PenguinDOOM:atlas/websocket-bridge
Open

Add websocket upstream bridge for /v1/responses and fix VS Code payload handling#110
PenguinDOOM wants to merge 24 commits into
RayBytes:mainfrom
PenguinDOOM:atlas/websocket-bridge

Conversation

@PenguinDOOM
Copy link
Copy Markdown

Summary

  • add an opt-in websocket upstream bridge for the HTTP /v1/responses route
  • add stateful follow-up support by retaining websocket sessions behind marker-based previous_response_id reuse
  • fix thin response.completed payloads so strict consumers such as VS Code do not fail on missing output arrays

Details

  • wire new app, CLI, and env configuration for websocket-upstream and stateful modes
  • bridge websocket upstream events back to HTTP clients for both streaming and non-stream responses
  • normalize terminal completed payloads by backfilling response.output from collected items, or an empty array when the upstream omits it
  • add retained-session handling for follow-ups, including stale-marker, conflict, and capacity cases
  • document the new modes and operator configuration in the README and Docker docs

Testing

  • add regression coverage for route contracts, websocket session retention, and CLI wiring
  • cover transport-only bridging, stateful follow-ups, stale/conflict handling, and strict completed-payload compatibility

Notes

  • the feature remains opt-in
  • stateful retention is process-local
  • streaming invalid-marker behavior is documented and unchanged by this PR

PenguinDOOM and others added 20 commits May 30, 2026 17:13
- add CLI and env plumbing for responses websocket upstream mode
- persist the resolved mode in app config and cover it with focused tests, including falsey env values
- add an importable websocket bridge stub for responses route patching
- add route contracts for disabled mode, enabled mode, and explicit websocket failure paths
- route enabled responses requests through a dedicated websocket bridge
- preserve SSE streaming, non-stream aggregation, and explicit error handling without connection reuse
- add flag, env var, and scope notes for HTTP /v1/responses websocket upstream
- add manual verification steps for disabled and enabled runtime modes
- Added _build_upstream_request_event function to ensure proper payload structure for response.create events.
- Updated _send_upstream_request to log and send the modified request event.
- Enhanced unit tests to verify the correct behavior of the new request event structure and ensure existing payloads are preserved.
…store is always set to False

- Removed the "truncation" key from the normalized payload.
- Ensured "store" is set to False by default in the normalized responses payload.
- Removed unnecessary [DONE] sentinel from streaming events in responses_websocket_bridge.py.
- Adjusted response preparation logic in session.py to ensure previous_response_id is handled correctly.
- Added tests to validate normalization of responses payload and ensure store is always set to False.
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>
 - split default-off and stateful-on websocket bridge tests
 - add RED coverage for retained follow-up behavior
 - add CLI flag and env RED coverage for opt-in stateful mode
 - add default-off stateful websocket bridge flags and env wiring
 - reject invalid startup config without websocket upstream
 - enable previous_response_id only in the stateful route branch
 - add retained websocket registry with locking and bounded capacity
 - reuse upstream sockets across stateful follow-up requests
 - reject same-session contention and evict retained sockets on failure
 - backfill completed response output from output_item events
 - apply retained-session capacity limits through the full route path
 - document stateful mode requirements, buffering behavior, and manual verification steps
- Seed headerless stateful HTTP contract tests
- Preserve legacy websocket-upstream guard coverage
- Characterize streaming invalid-marker as SSE error
- Remove header-gated stateful HTTP routing
- Promote retained websocket ownership by response id
- Green marker-driven route and registry tests
- Return previous_response_not_found for stale markers
- Evict failed retained markers before retry
- Preserve SSE characterization for streaming limitation
Co-authored-by: Copilot <copilot@github.com>
- Updated the error response structure in `responses_websocket_bridge.py` to include a specific error code and message for cases where a previous response is not found.
- Modified the test in `test_routes.py` to assert the new error response format, ensuring the correct code and message are returned.
- add streaming and non-stream RED coverage for thin response.completed payloads
- keep nearby /responses route and stateful regression checks green
…t bridge

- preserve existing response.output lists in completed events
- backfill completed output from collected response.output_item.done items
- synthesize an empty output list when no completed items were observed
Copilot AI review requested due to automatic review settings May 30, 2026 08:35
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR introduces an opt-in WebSocket upstream transport for the HTTP /v1/responses endpoint, with both stateless and stateful (retained connection) modes. It refactors WebSocket connection logic into shared utilities, adds a new responses-websocket bridge, retained session registry, and corresponding CLI flags, app config, and documentation.

Changes:

  • New responses_websocket_bridge module that proxies HTTP /v1/responses requests over an upstream websocket and a responses_websocket_sessions registry to retain websocket leases by previous_response_id.
  • New CLI flags / env vars (--responses-websocket-upstream, --responses-websocket-upstream-stateful) wired through cmd_serve and create_app, with a startup validation that stateful mode requires upstream mode.
  • Behavior changes in normalize_responses_payload (forces store=False, drops truncation) and improved completed-response output backfill from response.output_item.done events; substantial new test coverage.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
chatmock/app.py Adds new config flags and validates stateful requires upstream.
chatmock/cli.py Adds CLI/env flags for the new modes and threads them to create_app.
chatmock/routes_openai.py Routes /v1/responses through the websocket bridge when enabled, including stateful HTTP bridge handling.
chatmock/responses_api.py Forces store=False, strips truncation, and backfills completed output items.
chatmock/responses_websocket_bridge.py New bridge that talks to upstream over websocket and translates to JSON / SSE.
chatmock/responses_websocket_sessions.py New retained-websocket registry with capacity / conflict / not-found semantics.
chatmock/session.py Adds explicit_previous_response_id flag and refines final-response handling.
chatmock/upstream.py Moves websocket connect helper here for reuse.
chatmock/websocket_routes.py Removes duplicate websocket connect helper, imports the shared one.
tests/test_routes.py Adds extensive websocket bridge and stateful contract tests.
tests/test_responses_websocket_sessions.py Unit tests for the retained-session registry.
tests/test_cli.py Tests CLI flag wiring for the new options.
README.md, DOCKER.md Documents the new modes and manual verification steps.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread chatmock/responses_websocket_bridge.py
Comment thread tests/test_routes.py
Comment on lines +1075 to +1087
class FakeUpstreamWebsocket:
def __init__(self, messages: list[str]) -> None:
self.sent: list[str] = []
self._messages = list(messages)

def send(self, message: str) -> None:
self.sent.append(message)

def recv(self) -> str:
return self._messages.pop(0)

def close(self) -> None:
return None
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Leaving this out of the must-fix follow-up scope. This pass was limited to correctness, concurrency, and streaming fixes; the local FakeUpstreamWebsocket cleanup can land separately as test-only cleanup.

Comment thread tests/test_routes.py
Comment on lines +1998 to +2001
class FakeUpstreamWebsocket:
def __init__(self) -> None:
self.sent: list[str] = []
self._messages = [
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Leaving this out of the must-fix follow-up scope. I kept the broader FakeUpstreamWebsocket deduplication out of this pass so the review follow-up stayed focused on the correctness and streaming fixes.

Comment thread chatmock/responses_websocket_bridge.py
Comment thread chatmock/responses_websocket_sessions.py
Comment on lines +26 to +30
@dataclass(frozen=True)
class RetainedUpstreamWebsocketLease:
response_id: str | None
upstream_ws: Any
created: bool
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Leaving this out of the must-fix follow-up scope. The assertion is intentionally on the lease.created flag as written, but the naming/readability cleanup is separate from the must-fix behavior changes addressed here.

Comment thread chatmock/routes_openai.py
Comment thread chatmock/responses_websocket_bridge.py
Comment thread chatmock/responses_api.py

if "store" not in normalized:
normalized["store"] = False
normalized["store"] = False
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Leaving this out of the must-fix follow-up scope. The current pass was limited to must-fix correctness, concurrency, and streaming items; warning/logging around forced store=false can be handled as a separate observability improvement.

Comment thread chatmock/session.py Outdated
 - add explicit-empty-output follow-up contract coverage
 - add incremental stateful streaming contract coverage
 - reserve capacity before anonymous upstream connect
 - roll back pending reservations on connect failure
 - remove eager buffering from stateful SSE responses
 - release retained leases correctly on abort or error
 - keep follow-up reuse from backfilling authoritative empty outputs
 - collapse duplicate websocket credential checks
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.

2 participants