fix(proxy): preserve chatgpt responses streaming (#2012)

## Description

Closes #1956

Keep ChatGPT OAuth `/v1/responses` requests streaming when CCR retrieve
tools are present. The buffered `stream:false` conversion is still used
for regular OpenAI Responses CCR requests, but ChatGPT Codex routing now
bypasses that conversion so the upstream receives the streaming request
shape it expects.

## Type of Change

- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- [ ] Documentation update
- [ ] Refactoring
- [ ] Performance improvement
- [ ] Test update
- [ ] Other

## Changes Made

- Extracted the OpenAI Responses CCR stream-buffering decision into a
small helper.
- Excluded ChatGPT OAuth/Codex-routed requests from the buffered
`stream:false` path.
- Added tests proving regular OpenAI CCR still buffers while ChatGPT
OAuth CCR remains streaming.

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check`)
- [x] Formatting verified (`ruff format --check`)
- [ ] Manual testing performed

### Test Output

```text
$ python3 -m pytest tests/test_proxy_openai_responses_stream_ccr.py -q
collected 3 items

tests/test_proxy_openai_responses_stream_ccr.py ...                      [100%]

============================== 3 passed in 0.59s ===============================

$ .venv/bin/ruff check headroom/proxy/handlers/openai.py tests/test_proxy_openai_responses_stream_ccr.py
All checks passed!

$ .venv/bin/ruff format --check headroom/proxy/handlers/openai.py tests/test_proxy_openai_responses_stream_ccr.py
2 files already formatted
```

## Test verification (RED -> GREEN)

RED, with the ChatGPT OAuth guard temporarily removed from the buffering
decision:

```text
tests/test_proxy_openai_responses_stream_ccr.py .F.                      [100%]
FAILED tests/test_proxy_openai_responses_stream_ccr.py::test_responses_ccr_keeps_chatgpt_oauth_requests_streaming
E   AssertionError: assert not True
E    +  where True = _should_buffer(tools=[{'type': 'function', 'name': 'headroom_retrieve'}], is_chatgpt_auth=True)
```

GREEN, with this patch applied:

```text
tests/test_proxy_openai_responses_stream_ccr.py ...                      [100%]
============================== 3 passed in 0.59s ===============================
```

## Real Behavior Proof

- Environment: Linux, Python 3.12.3, pytest 9.1.1, ruff 0.14.14.
- Exact command / steps: Removed the `not is_chatgpt_auth` guard from
the CCR buffering decision, ran the targeted tests, restored the guard,
and reran the tests plus targeted ruff checks.
- Observed result: The ChatGPT OAuth streaming regression test fails
without the guard and passes with the guard, while regular OpenAI CCR
buffering remains covered.
- Not tested: Full `uv run pytest`, full-project `uv run ruff check .`,
full-project `uv run ruff format --check .`, and `uv run mypy headroom`
were not run locally; `uv run --extra dev ruff` attempted to build the
Rust extension in this worktree, so targeted checks used the existing
`.venv/bin/ruff`.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

## Checklist

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have added tests that prove my fix is effective
- [x] New and existing targeted tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules

## Screenshots (if applicable)

N/A

## Additional Notes

The existing buffered CCR path is preserved for non-ChatGPT OpenAI
Responses requests.
This commit is contained in:
Ben Younes 2026-07-11 06:10:02 +01:00 committed by GitHub
parent 70b98b6485
commit a617455f02
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 58 additions and 4 deletions

View file

@ -648,6 +648,23 @@ def _has_headroom_retrieve_tool_responses(tools: Any) -> bool:
return False
def _should_buffer_openai_responses_stream_ccr(
*,
stream: bool,
ccr_response_handler_enabled: bool,
tools: Any,
is_chatgpt_auth: bool,
) -> bool:
"""Return whether streaming Responses CCR should use buffered JSON mode."""
return bool(
stream
and ccr_response_handler_enabled
and not is_chatgpt_auth
and _has_headroom_retrieve_tool_responses(tools)
)
def _responses_input_to_items(input_data: Any) -> list[dict[str, Any]]:
"""Normalize a Responses ``input`` field into an item list for CCR continuation.
@ -4206,10 +4223,11 @@ class OpenAIHandlerMixin:
_ccr_response_handler_enabled = bool(
_ccr_response_handler and getattr(_ccr_handler_config, "enabled", True)
)
buffered_stream_ccr = bool(
stream
and _ccr_response_handler_enabled
and _has_headroom_retrieve_tool_responses(body.get("tools"))
buffered_stream_ccr = _should_buffer_openai_responses_stream_ccr(
stream=stream,
ccr_response_handler_enabled=_ccr_response_handler_enabled,
tools=body.get("tools"),
is_chatgpt_auth=is_chatgpt_auth,
)
if buffered_stream_ccr:
if body.get("stream") is not False:

View file

@ -0,0 +1,36 @@
from __future__ import annotations
from headroom.ccr import CCR_TOOL_NAME
from headroom.proxy.handlers.openai import _should_buffer_openai_responses_stream_ccr
_TOOL_TYPE_FUNCTION = "function"
_UNRELATED_TOOL_NAME = "unrelated_tool"
def _ccr_tool() -> dict[str, str]:
return {"type": _TOOL_TYPE_FUNCTION, "name": CCR_TOOL_NAME}
def _unrelated_tool() -> dict[str, str]:
return {"type": _TOOL_TYPE_FUNCTION, "name": _UNRELATED_TOOL_NAME}
def _should_buffer(*, tools: list[dict[str, str]], is_chatgpt_auth: bool) -> bool:
return _should_buffer_openai_responses_stream_ccr(
stream=True,
ccr_response_handler_enabled=True,
tools=tools,
is_chatgpt_auth=is_chatgpt_auth,
)
def test_responses_ccr_buffers_streaming_openai_requests() -> None:
assert _should_buffer(tools=[_ccr_tool()], is_chatgpt_auth=False)
def test_responses_ccr_keeps_chatgpt_oauth_requests_streaming() -> None:
assert not _should_buffer(tools=[_ccr_tool()], is_chatgpt_auth=True)
def test_responses_ccr_ignores_requests_without_retrieve_tool() -> None:
assert not _should_buffer(tools=[_unrelated_tool()], is_chatgpt_auth=False)