fix(proxy): keep codex image-generation WS turns alive through the relay#1000
Merged
JerrettDavis merged 1 commit intoJun 16, 2026
Merged
Conversation
Contributor
PR governanceThis PR follows the template and is marked ready for human review. |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Codex /v1/responses image-generation turns failed through the proxy in two compounding ways the upstream WS connect was blind to: 1. The render phase goes silent for 20-60s (a single image_generation_call.generating event, then no data frames). The 20s ping_timeout pong deadline false-killed the still-healthy upstream mid-render with upstream_error before the image landed. 2. The finished image arrives inline as a single base64 frame larger than the websockets default 1 MiB max_size, raising PayloadTooBig exactly as it landed. Set ping_timeout=None (keep ping_interval for NAT keepalive) and max_size=None on the upstream /v1/responses connect so long silent renders survive and the image frame is accepted. Text/tool turns stream continuously and are unaffected. Adds a regression test pinning both connect kwargs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
b3dd38c to
dbf8897
Compare
JerrettDavis
approved these changes
Jun 15, 2026
JerrettDavis
left a comment
Collaborator
There was a problem hiding this comment.
LGTM! Thank you for the contribution.
Merged
chopratejas
pushed a commit
that referenced
this pull request
Jun 16, 2026
🤖 I have created a release *beep* *boop* --- <details><summary>0.26.0</summary> ## [0.26.0](v0.25.0...v0.26.0) (2026-06-16) ### Features * add Copilot BYOK provider wrapper utilities and CLI support ([#1041](#1041)) ([e67ee2a](e67ee2a)) * add dashboard agent usage stats ([#814](#814)) ([6d3f39f](6d3f39f)) * Add support for Mistral Vibe CLI ([#935](#935)) ([0932b8b](0932b8b)) * attribute reread waste to over-compression via marker check ([#901](#901)) ([f928576](f928576)) * **bedrock:** cross-region + Converse compression; bundle proxy binary in images ([#999](#999)) ([0dc2e1c](0dc2e1c)) * **dashboard:** surface compression-vs-cache net impact in Prefix Cache panel ([#913](#913)) ([2a4d300](2a4d300)) * **evals:** adversarial-input robustness grid for compressors ([#918](#918)) ([5939004](5939004)) * **parser:** detect re-issued identical tool calls as reread waste ([#909](#909)) ([7d4ae86](7d4ae86)) * **policy:** batch deep edits through one cache-bust ([#856](#856) P3a) ([#1015](#1015)) ([c2e52fe](c2e52fe)) * **policy:** consume net-cost mutation gate in ContentRouter ([#856](#856) P2) ([#905](#905)) ([553ade4](553ade4)) * **proxy:** compress AWS Bedrock InvokeModel requests via configurable upstream ([#720](#720)) ([7edb27a](7edb27a)) ### Bug Fixes * **anthropic:** strip styled Claude model ids ([#651](#651)) ([0c5c89d](0c5c89d)) * **anyllm:** forward openai api_base/api_key to the any-llm backend ([#942](#942)) ([#954](#954)) ([a7ee8a6](a7ee8a6)) * **cache:** guard None exemplar embeddings in dynamic detector ([#950](#950)) ([1ec9320](1ec9320)) * **cache:** name the missing piece in semantic detector guard ([#1018](#1018)) ([3b0bcee](3b0bcee)) * **ci:** check out repo in PR Governance label job ([#1021](#1021)) ([4558bc2](4558bc2)) * **ci:** make PR governance advisory ([#1047](#1047)) ([74dff94](74dff94)) * **codex:** compute waste signals on the OpenAI Responses path ([#898](#898)) ([b9e2761](b9e2761)) * **codex:** poll /wham/usage for subscription limits (handshake no longer sends x-codex-* headers) ([#924](#924)) ([8c00f71](8c00f71)) * **codex:** PR health label check state ([#986](#986)) ([99c874d](99c874d)) * **codex:** retag thread providers so history menu stays whole across the proxy boundary ([#1034](#1034)) ([74ae781](74ae781)) * **codex:** write canonical hooks feature flag and migrate deprecated codex_hooks ([#743](#743)) ([dff6a19](dff6a19)) * **compression:** convert tree-sitter byte offsets to char offsets ([#892](#892)) ([b1f700f](b1f700f)) * **compression:** correct JSON array item counting and entropy gate ([#887](#887)) ([d6f0f0f](d6f0f0f)) * **compression:** keep container bodies compressible in code handler ([#890](#890)) ([16ed73b](16ed73b)) * **compression:** measure short-value threshold on payload, not token ([#889](#889)) ([65b0e8c](65b0e8c)) * **compression:** use thread-local tree-sitter parsers in code handler ([#893](#893)) ([6cdb846](6cdb846)) * **gemini:** surface functionResponse payloads to waste-signal detection ([#897](#897)) ([9b0c840](9b0c840)) * **learn:** decode directory names with spaces in Windows project paths ([#997](#997)) ([#1027](#1027)) ([2d3701b](2d3701b)) * **learn:** scan subagent and workflow transcripts ([#1045](#1045)) ([0ddd4ed](0ddd4ed)) * **openclaw:** declare headroom_retrieve tool contract ([#947](#947)) ([7c8c909](7c8c909)) * **policy:** correct warm-cache penalty in net_mutation_gain to (S + dT) ([#903](#903)) ([0632eba](0632eba)) * **proxy:** add native Bedrock converse-stream route ([#917](#917)) ([b08ec15](b08ec15)) * **proxy:** keep codex image-generation WS turns alive through the relay ([#1000](#1000)) ([7dbbb40](7dbbb40)) * **proxy:** make budget enforcement actually work ([#885](#885)) ([a14ab45](a14ab45)) * **proxy:** read RTK gain stats globally by default ([#957](#957)) ([b70fccb](b70fccb)) * route v1internal code assist requests to cloudcode-pa.googleapis… ([#821](#821)) ([e20f16b](e20f16b)) * **serena:** stop the Serena dashboard popup and make --no-serena actually disable Serena ([#1003](#1003)) ([919379a](919379a)) * support Copilot Business subscription auth ([#641](#641)) ([0b4a4bd](0b4a4bd)) * wire HEADROOM_EXCLUDE_TOOLS / HEADROOM_TOOL_PROFILES into Click proxy entrypoint ([#943](#943)) ([9b7b436](9b7b436)) * **wrap:** avoid duplicate top-level keys when injecting codex provider ([#884](#884)) ([dd22cfd](dd22cfd)) ### Code Refactoring * DRY cache logic, add thread safety, fix Bash exclusion ([#704](#704)) ([e36fccd](e36fccd)) </details> --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Epicism
pushed a commit
to Epicism/headroom
that referenced
this pull request
Jun 17, 2026
…lay (headroomlabs-ai#1000) ## Description Image generation through the proxy fails. Driving Codex (`/v1/responses` over WebSocket) through Headroom, an image-generation turn never returns an image — the client retries (`Reconnecting… n/5`) and gives up, while the same prompt works when Codex talks to ChatGPT directly. Root cause: two independent defects on the upstream `websockets.connect()`, both specific to how image generation behaves on the wire: 1. **Pong deadline kills the silent render.** An image turn emits a single `response.image_generation_call.generating` event and then goes silent for 20–60s while the model renders (no data frames). The hard-coded `ping_timeout=20` treats that healthy-but-quiet connection as dead and tears it down as `upstream_error` mid-render, before the image is ready. 2. **1 MiB frame cap drops the image.** The finished image comes back inline as a single base64 frame that exceeds the `websockets` default `max_size=2**20` (1 MiB), raising `PayloadTooBig` exactly as the image lands. They compound: with only ping fixed, the session survives the silent phase (observed ~20s → ~54s) but then dies on the oversized image frame. Normal text/tool turns stream tokens continuously and stay well under 1 MiB, so neither defect affects them — which is why this only ever bit image generation. Closes: N/A (no tracking issue) ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - `headroom/proxy/handlers/openai.py`: on the upstream `/v1/responses` connect, set `ping_timeout=None` (keep `ping_interval=20` for NAT keepalive) so a long silent render is not torn down on a missing pong, and `max_size=None` so the inline base64 image frame is accepted instead of raising `PayloadTooBig`. - `tests/test_openai_codex_ws_lifecycle.py`: add `test_ws_upstream_connect_allows_large_frames_and_no_pong_deadline`, which captures the upstream connect kwargs and pins `ping_timeout is None` / `max_size is None`. ## Testing <!-- Check what you actually ran, then paste the real command output below. --> - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ ruff check . All checks passed! $ mypy headroom --ignore-missing-imports Success: no issues found in 358 source files $ pytest tests/test_openai_codex_ws_lifecycle.py -q collected 15 items tests/test_openai_codex_ws_lifecycle.py .............. [100%] ============================== 15 passed in 0.72s ============================== $ pytest tests/test_openai_codex_ws_lifecycle.py -k large_frames_and_no_pong -q collected 15 items / 14 deselected / 1 selected tests/test_openai_codex_ws_lifecycle.py . [100%] ======================= 1 passed, 14 deselected in 0.35s ======================= ``` End-to-end (managed Codex image generation through the running proxy): ```text # BEFORE fix: fails at ~20s (unpatched) / ~54s (ping-only) # WS /v1/responses completed (cause=upstream_error, # last_upstream_type=response.image_generation_call.generating) # -> client "Reconnecting… n/5", no image produced # AFTER fix: [codex] Image ready; stopping the turn. Saved image: /tmp/headroom-imagegen-test.png $ file /tmp/headroom-imagegen-test.png PNG image data, 1254 x 1254, 8-bit/color RGB, non-interlaced (908 KB) # proxy session count +1 -> the turn DID traverse the proxy and completed. ``` ## Real Behavior Proof - Environment: macOS, headroom 0.23.0 running as the Codex `model_provider` (proxy on `127.0.0.1:8787`), Codex CLI 0.139.0 driving a managed `/v1/responses` image-generation turn through the proxy. - Exact command / steps: trigger a Codex image-generation turn (gpt-image-2) with the proxy in front; observe the upstream `/v1/responses` WS session in `proxy.log` and whether a PNG is returned. - Observed result: before the change the session dies with `upstream_error` while `last_upstream_type=response.image_generation_call.generating` and no image is produced; after the change a valid 1254×1254 PNG is returned and the session traverses the proxy normally. - Not tested: the full `pytest` suite was not run locally — this machine has no Rust toolchain to rebuild the matching `_core` extension, so the complete suite (incl. the pyo3 tests) is left to CI. The affected `test_openai_codex_ws_lifecycle.py` module was run against the installed extension and passes 15/15; `ruff check .` and `mypy headroom` were run in full and pass. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) N/A. ## Additional Notes - `ruff check .` (whole repo) and `mypy headroom --ignore-missing-imports` (358 source files) were run locally and pass. The only `pytest` not run locally is the full suite, because the Rust `_core` cannot be rebuilt here without a toolchain; the directly affected lifecycle module passes 15/15 and CI runs the rest. - Documentation / CHANGELOG left unchecked — this is a focused two-line behavioral fix on the upstream WS connect; happy to add a CHANGELOG entry if preferred. - `ping_timeout=None` keeps `ping_interval` for NAT keepalive; if you'd rather bound it, a generous finite value (e.g. 300s) would also fix the render case — happy to switch. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
studyzy
pushed a commit
to studyzy/headroom
that referenced
this pull request
Jun 24, 2026
…lay (headroomlabs-ai#1000) ## Description Image generation through the proxy fails. Driving Codex (`/v1/responses` over WebSocket) through Headroom, an image-generation turn never returns an image — the client retries (`Reconnecting… n/5`) and gives up, while the same prompt works when Codex talks to ChatGPT directly. Root cause: two independent defects on the upstream `websockets.connect()`, both specific to how image generation behaves on the wire: 1. **Pong deadline kills the silent render.** An image turn emits a single `response.image_generation_call.generating` event and then goes silent for 20–60s while the model renders (no data frames). The hard-coded `ping_timeout=20` treats that healthy-but-quiet connection as dead and tears it down as `upstream_error` mid-render, before the image is ready. 2. **1 MiB frame cap drops the image.** The finished image comes back inline as a single base64 frame that exceeds the `websockets` default `max_size=2**20` (1 MiB), raising `PayloadTooBig` exactly as the image lands. They compound: with only ping fixed, the session survives the silent phase (observed ~20s → ~54s) but then dies on the oversized image frame. Normal text/tool turns stream tokens continuously and stay well under 1 MiB, so neither defect affects them — which is why this only ever bit image generation. Closes: N/A (no tracking issue) ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - `headroom/proxy/handlers/openai.py`: on the upstream `/v1/responses` connect, set `ping_timeout=None` (keep `ping_interval=20` for NAT keepalive) so a long silent render is not torn down on a missing pong, and `max_size=None` so the inline base64 image frame is accepted instead of raising `PayloadTooBig`. - `tests/test_openai_codex_ws_lifecycle.py`: add `test_ws_upstream_connect_allows_large_frames_and_no_pong_deadline`, which captures the upstream connect kwargs and pins `ping_timeout is None` / `max_size is None`. ## Testing <!-- Check what you actually ran, then paste the real command output below. --> - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ ruff check . All checks passed! $ mypy headroom --ignore-missing-imports Success: no issues found in 358 source files $ pytest tests/test_openai_codex_ws_lifecycle.py -q collected 15 items tests/test_openai_codex_ws_lifecycle.py .............. [100%] ============================== 15 passed in 0.72s ============================== $ pytest tests/test_openai_codex_ws_lifecycle.py -k large_frames_and_no_pong -q collected 15 items / 14 deselected / 1 selected tests/test_openai_codex_ws_lifecycle.py . [100%] ======================= 1 passed, 14 deselected in 0.35s ======================= ``` End-to-end (managed Codex image generation through the running proxy): ```text # BEFORE fix: fails at ~20s (unpatched) / ~54s (ping-only) # WS /v1/responses completed (cause=upstream_error, # last_upstream_type=response.image_generation_call.generating) # -> client "Reconnecting… n/5", no image produced # AFTER fix: [codex] Image ready; stopping the turn. Saved image: /tmp/headroom-imagegen-test.png $ file /tmp/headroom-imagegen-test.png PNG image data, 1254 x 1254, 8-bit/color RGB, non-interlaced (908 KB) # proxy session count +1 -> the turn DID traverse the proxy and completed. ``` ## Real Behavior Proof - Environment: macOS, headroom 0.23.0 running as the Codex `model_provider` (proxy on `127.0.0.1:8787`), Codex CLI 0.139.0 driving a managed `/v1/responses` image-generation turn through the proxy. - Exact command / steps: trigger a Codex image-generation turn (gpt-image-2) with the proxy in front; observe the upstream `/v1/responses` WS session in `proxy.log` and whether a PNG is returned. - Observed result: before the change the session dies with `upstream_error` while `last_upstream_type=response.image_generation_call.generating` and no image is produced; after the change a valid 1254×1254 PNG is returned and the session traverses the proxy normally. - Not tested: the full `pytest` suite was not run locally — this machine has no Rust toolchain to rebuild the matching `_core` extension, so the complete suite (incl. the pyo3 tests) is left to CI. The affected `test_openai_codex_ws_lifecycle.py` module was run against the installed extension and passes 15/15; `ruff check .` and `mypy headroom` were run in full and pass. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) N/A. ## Additional Notes - `ruff check .` (whole repo) and `mypy headroom --ignore-missing-imports` (358 source files) were run locally and pass. The only `pytest` not run locally is the full suite, because the Rust `_core` cannot be rebuilt here without a toolchain; the directly affected lifecycle module passes 15/15 and CI runs the rest. - Documentation / CHANGELOG left unchecked — this is a focused two-line behavioral fix on the upstream WS connect; happy to add a CHANGELOG entry if preferred. - `ping_timeout=None` keeps `ping_interval` for NAT keepalive; if you'd rather bound it, a generous finite value (e.g. 300s) would also fix the render case — happy to switch. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
studyzy
pushed a commit
to studyzy/headroom
that referenced
this pull request
Jun 24, 2026
🤖 I have created a release *beep* *boop* --- <details><summary>0.26.0</summary> ## [0.26.0](headroomlabs-ai/headroom@v0.25.0...v0.26.0) (2026-06-16) ### Features * add Copilot BYOK provider wrapper utilities and CLI support ([headroomlabs-ai#1041](headroomlabs-ai#1041)) ([e67ee2a](headroomlabs-ai@e67ee2a)) * add dashboard agent usage stats ([headroomlabs-ai#814](headroomlabs-ai#814)) ([6d3f39f](headroomlabs-ai@6d3f39f)) * Add support for Mistral Vibe CLI ([headroomlabs-ai#935](headroomlabs-ai#935)) ([0932b8b](headroomlabs-ai@0932b8b)) * attribute reread waste to over-compression via marker check ([headroomlabs-ai#901](headroomlabs-ai#901)) ([f928576](headroomlabs-ai@f928576)) * **bedrock:** cross-region + Converse compression; bundle proxy binary in images ([headroomlabs-ai#999](headroomlabs-ai#999)) ([0dc2e1c](headroomlabs-ai@0dc2e1c)) * **dashboard:** surface compression-vs-cache net impact in Prefix Cache panel ([headroomlabs-ai#913](headroomlabs-ai#913)) ([2a4d300](headroomlabs-ai@2a4d300)) * **evals:** adversarial-input robustness grid for compressors ([headroomlabs-ai#918](headroomlabs-ai#918)) ([5939004](headroomlabs-ai@5939004)) * **parser:** detect re-issued identical tool calls as reread waste ([headroomlabs-ai#909](headroomlabs-ai#909)) ([7d4ae86](headroomlabs-ai@7d4ae86)) * **policy:** batch deep edits through one cache-bust ([headroomlabs-ai#856](headroomlabs-ai#856) P3a) ([headroomlabs-ai#1015](headroomlabs-ai#1015)) ([c2e52fe](headroomlabs-ai@c2e52fe)) * **policy:** consume net-cost mutation gate in ContentRouter ([headroomlabs-ai#856](headroomlabs-ai#856) P2) ([headroomlabs-ai#905](headroomlabs-ai#905)) ([553ade4](headroomlabs-ai@553ade4)) * **proxy:** compress AWS Bedrock InvokeModel requests via configurable upstream ([headroomlabs-ai#720](headroomlabs-ai#720)) ([7edb27a](headroomlabs-ai@7edb27a)) ### Bug Fixes * **anthropic:** strip styled Claude model ids ([headroomlabs-ai#651](headroomlabs-ai#651)) ([0c5c89d](headroomlabs-ai@0c5c89d)) * **anyllm:** forward openai api_base/api_key to the any-llm backend ([headroomlabs-ai#942](headroomlabs-ai#942)) ([headroomlabs-ai#954](headroomlabs-ai#954)) ([a7ee8a6](headroomlabs-ai@a7ee8a6)) * **cache:** guard None exemplar embeddings in dynamic detector ([headroomlabs-ai#950](headroomlabs-ai#950)) ([1ec9320](headroomlabs-ai@1ec9320)) * **cache:** name the missing piece in semantic detector guard ([headroomlabs-ai#1018](headroomlabs-ai#1018)) ([3b0bcee](headroomlabs-ai@3b0bcee)) * **ci:** check out repo in PR Governance label job ([headroomlabs-ai#1021](headroomlabs-ai#1021)) ([4558bc2](headroomlabs-ai@4558bc2)) * **ci:** make PR governance advisory ([headroomlabs-ai#1047](headroomlabs-ai#1047)) ([74dff94](headroomlabs-ai@74dff94)) * **codex:** compute waste signals on the OpenAI Responses path ([headroomlabs-ai#898](headroomlabs-ai#898)) ([b9e2761](headroomlabs-ai@b9e2761)) * **codex:** poll /wham/usage for subscription limits (handshake no longer sends x-codex-* headers) ([headroomlabs-ai#924](headroomlabs-ai#924)) ([8c00f71](headroomlabs-ai@8c00f71)) * **codex:** PR health label check state ([headroomlabs-ai#986](headroomlabs-ai#986)) ([99c874d](headroomlabs-ai@99c874d)) * **codex:** retag thread providers so history menu stays whole across the proxy boundary ([headroomlabs-ai#1034](headroomlabs-ai#1034)) ([74ae781](headroomlabs-ai@74ae781)) * **codex:** write canonical hooks feature flag and migrate deprecated codex_hooks ([headroomlabs-ai#743](headroomlabs-ai#743)) ([dff6a19](headroomlabs-ai@dff6a19)) * **compression:** convert tree-sitter byte offsets to char offsets ([headroomlabs-ai#892](headroomlabs-ai#892)) ([b1f700f](headroomlabs-ai@b1f700f)) * **compression:** correct JSON array item counting and entropy gate ([headroomlabs-ai#887](headroomlabs-ai#887)) ([d6f0f0f](headroomlabs-ai@d6f0f0f)) * **compression:** keep container bodies compressible in code handler ([headroomlabs-ai#890](headroomlabs-ai#890)) ([16ed73b](headroomlabs-ai@16ed73b)) * **compression:** measure short-value threshold on payload, not token ([headroomlabs-ai#889](headroomlabs-ai#889)) ([65b0e8c](headroomlabs-ai@65b0e8c)) * **compression:** use thread-local tree-sitter parsers in code handler ([headroomlabs-ai#893](headroomlabs-ai#893)) ([6cdb846](headroomlabs-ai@6cdb846)) * **gemini:** surface functionResponse payloads to waste-signal detection ([headroomlabs-ai#897](headroomlabs-ai#897)) ([9b0c840](headroomlabs-ai@9b0c840)) * **learn:** decode directory names with spaces in Windows project paths ([headroomlabs-ai#997](headroomlabs-ai#997)) ([headroomlabs-ai#1027](headroomlabs-ai#1027)) ([2d3701b](headroomlabs-ai@2d3701b)) * **learn:** scan subagent and workflow transcripts ([headroomlabs-ai#1045](headroomlabs-ai#1045)) ([0ddd4ed](headroomlabs-ai@0ddd4ed)) * **openclaw:** declare headroom_retrieve tool contract ([headroomlabs-ai#947](headroomlabs-ai#947)) ([7c8c909](headroomlabs-ai@7c8c909)) * **policy:** correct warm-cache penalty in net_mutation_gain to (S + dT) ([headroomlabs-ai#903](headroomlabs-ai#903)) ([0632eba](headroomlabs-ai@0632eba)) * **proxy:** add native Bedrock converse-stream route ([headroomlabs-ai#917](headroomlabs-ai#917)) ([b08ec15](headroomlabs-ai@b08ec15)) * **proxy:** keep codex image-generation WS turns alive through the relay ([headroomlabs-ai#1000](headroomlabs-ai#1000)) ([7dbbb40](headroomlabs-ai@7dbbb40)) * **proxy:** make budget enforcement actually work ([headroomlabs-ai#885](headroomlabs-ai#885)) ([a14ab45](headroomlabs-ai@a14ab45)) * **proxy:** read RTK gain stats globally by default ([headroomlabs-ai#957](headroomlabs-ai#957)) ([b70fccb](headroomlabs-ai@b70fccb)) * route v1internal code assist requests to cloudcode-pa.googleapis… ([headroomlabs-ai#821](headroomlabs-ai#821)) ([e20f16b](headroomlabs-ai@e20f16b)) * **serena:** stop the Serena dashboard popup and make --no-serena actually disable Serena ([headroomlabs-ai#1003](headroomlabs-ai#1003)) ([919379a](headroomlabs-ai@919379a)) * support Copilot Business subscription auth ([headroomlabs-ai#641](headroomlabs-ai#641)) ([0b4a4bd](headroomlabs-ai@0b4a4bd)) * wire HEADROOM_EXCLUDE_TOOLS / HEADROOM_TOOL_PROFILES into Click proxy entrypoint ([headroomlabs-ai#943](headroomlabs-ai#943)) ([9b7b436](headroomlabs-ai@9b7b436)) * **wrap:** avoid duplicate top-level keys when injecting codex provider ([headroomlabs-ai#884](headroomlabs-ai#884)) ([dd22cfd](headroomlabs-ai@dd22cfd)) ### Code Refactoring * DRY cache logic, add thread safety, fix Bash exclusion ([headroomlabs-ai#704](headroomlabs-ai#704)) ([e36fccd](headroomlabs-ai@e36fccd)) </details> --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Image generation through the proxy fails. Driving Codex (
/v1/responsesoverWebSocket) through Headroom, an image-generation turn never returns an image —
the client retries (
Reconnecting… n/5) and gives up, while the same promptworks when Codex talks to ChatGPT directly.
Root cause: two independent defects on the upstream
websockets.connect(), bothspecific to how image generation behaves on the wire:
response.image_generation_call.generatingevent and then goes silent for20–60s while the model renders (no data frames). The hard-coded
ping_timeout=20treats that healthy-but-quiet connection as dead and tearsit down as
upstream_errormid-render, before the image is ready.a single base64 frame that exceeds the
websocketsdefaultmax_size=2**20(1 MiB), raising
PayloadTooBigexactly as the image lands.They compound: with only ping fixed, the session survives the silent phase
(observed ~20s → ~54s) but then dies on the oversized image frame. Normal
text/tool turns stream tokens continuously and stay well under 1 MiB, so neither
defect affects them — which is why this only ever bit image generation.
Closes: N/A (no tracking issue)
Type of Change
Changes Made
headroom/proxy/handlers/openai.py: on the upstream/v1/responsesconnect,set
ping_timeout=None(keepping_interval=20for NAT keepalive) so a longsilent render is not torn down on a missing pong, and
max_size=Noneso theinline base64 image frame is accepted instead of raising
PayloadTooBig.tests/test_openai_codex_ws_lifecycle.py: addtest_ws_upstream_connect_allows_large_frames_and_no_pong_deadline, whichcaptures the upstream connect kwargs and pins
ping_timeout is None/max_size is None.Testing
pytest)ruff check .)mypy headroom)Test Output
End-to-end (managed Codex image generation through the running proxy):
Real Behavior Proof
model_provider(proxy on
127.0.0.1:8787), Codex CLI 0.139.0 driving a managed/v1/responsesimage-generation turn through the proxy.with the proxy in front; observe the upstream
/v1/responsesWS session inproxy.logand whether a PNG is returned.upstream_errorwhile
last_upstream_type=response.image_generation_call.generatingand noimage is produced; after the change a valid 1254×1254 PNG is returned and the
session traverses the proxy normally.
pytestsuite was not run locally — this machine has noRust toolchain to rebuild the matching
_coreextension, so the completesuite (incl. the pyo3 tests) is left to CI. The affected
test_openai_codex_ws_lifecycle.pymodule was run against the installedextension and passes 15/15;
ruff check .andmypy headroomwere run infull and pass.
Review Readiness
Checklist
Screenshots (if applicable)
N/A.
Additional Notes
ruff check .(whole repo) andmypy headroom --ignore-missing-imports(358 source files) were run locally and pass. The only
pytestnot runlocally is the full suite, because the Rust
_corecannot be rebuilt herewithout a toolchain; the directly affected lifecycle module passes 15/15 and
CI runs the rest.
behavioral fix on the upstream WS connect; happy to add a CHANGELOG entry if
preferred.
ping_timeout=Nonekeepsping_intervalfor NAT keepalive; if you'd ratherbound it, a generous finite value (e.g. 300s) would also fix the render case —
happy to switch.
🤖 Generated with Claude Code