mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
## Description
On the direct OpenAI `/v1/chat/completions` streaming path, the handler
injects
`stream_options.include_usage = True` so it can count tokens from the
trailing usage chunk —
but it does so **unconditionally**, including flipping an explicit
client `include_usage: false`
to `true` (`headroom/proxy/handlers/openai.py`):
```python
if "stream_options" not in body:
body["stream_options"] = {"include_usage": True}
elif isinstance(body.get("stream_options"), dict):
body["stream_options"]["include_usage"] = True # overrides an explicit `false`
```
When the client passed `stream_options: {"include_usage": false}` (or a
dict that set some
other key), the upstream is nevertheless asked for usage and appends a
terminal usage-only
frame:
```
data: {"id":...,"choices":[],"usage":{...}}
data: [DONE]
```
The extremely common client pattern `for chunk in stream:
chunk.choices[0].delta.content`
then raises `IndexError` on that empty-`choices` frame — for a usage
chunk the client
explicitly opted out of.
Closes: no issue filed — found while auditing the streaming
request-shaping.
## Fix
Only fill in `include_usage` when the client left the choice open — no
`stream_options` at all,
or a `stream_options` dict that doesn't mention `include_usage`. An
explicit `true`/`false` is
respected. Extracted into a small `_apply_stream_usage_option(body)`
helper (mirroring the
existing `_normalize_openai_max_tokens`) for a clean unit-test seam:
```python
stream_options = body.get("stream_options")
if stream_options is None:
body["stream_options"] = {"include_usage": True}
elif isinstance(stream_options, dict) and "include_usage" not in stream_options:
stream_options["include_usage"] = True
```
Scope note: this respects an explicit client choice, which is the
unambiguous defect. The
separate question of whether to strip the synthetic usage chunk when
Headroom injected the
option itself (the no-`stream_options` default, kept for token-counting)
touches the raw SSE
byte stream and is intentionally left out of this change.
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
## Changes Made
- `headroom/proxy/handlers/openai.py`: add
`_apply_stream_usage_option(body)` and call it from the streaming chat
path; it no longer overrides an explicit client `include_usage`.
- `tests/test_proxy/test_openai_stream_usage_option.py`: cover explicit
`false` (respected), explicit `true` (preserved), absent (injected), and
dict-without-key (filled in).
## Testing
- [x] New regression tests added
(`tests/test_proxy/test_openai_stream_usage_option.py`)
- [x] Linting/formatting clean — run with the CI-pinned `ruff==0.15.17`
- [ ] Full `pytest` deferred to CI (local-OOM reason below).
```text
$ uvx ruff@0.15.17 check headroom/proxy/handlers/openai.py tests/test_proxy/test_openai_stream_usage_option.py
All checks passed!
```
## Real Behavior Proof
- Environment: Windows 11, Python 3.10, headroom from this branch.
Importing `headroom` pulls in the torch/transformers stack and a full
`pytest` gets OOM-killed on this box, so I verified the decision logic
with a dependency-free script and left the full pytest to CI.
- Exact command / steps: ran a client body with `stream_options:
{include_usage: false}` (plus the explicit-true, absent, and
dict-without-key cases) through the old unconditional injection and the
new helper.
- Observed result: the old logic flips the client's `false` to `true`;
the new logic respects it:
```text
explicit false: OLD -> {'include_usage': True} NEW -> {'include_usage': False}
INCLUDE_USAGE RESPECT-CLIENT FIX VERIFIED (old flips false->true; new respects false)
```
- Not tested: a full streaming round-trip through a live OpenAI upstream
(needs the heavy stack + a key). The fix is confined to the
request-shaping helper and the new tests drive it directly. Full local
`pytest` deferred to CI (OOM, per above).
## 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
- [ ] New and existing unit tests pass locally with my changes — ran
lint + a standalone logic check; full pytest deferred to CI (local OOM,
disclosed above)
- [x] I have updated the CHANGELOG.md if applicable
## Additional Notes
- Small, contained change plus a helper and tests; no new dependencies.
The backend-path injection (`test_backend_anyllm` /
`test_backend_streaming_cache_metrics`) is untouched — those pass an
explicit `include_usage: true`, which is preserved.
- @JerrettDavis tagging you — this one makes a client that sent
`include_usage: false` hit an `IndexError` on the usage chunk, so it
seemed worth surfacing. Thanks!
---------
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
|
||
|---|---|---|
| .. | ||
| test_anthropic_buffered_timeout.py | ||
| test_anthropic_ccr_deferred_injection.py | ||
| test_anthropic_ccr_raise.py | ||
| test_anthropic_streaming_ccr_retrieve.py | ||
| test_anthropic_upstream_header.py | ||
| test_background_compression.py | ||
| test_bedrock_passthrough.py | ||
| test_cc_switch_reconciler.py | ||
| test_ccr_frozen_prefix_coupling.py | ||
| test_compression_failure_action.py | ||
| test_compression_timeout_config.py | ||
| test_compute_turn_id.py | ||
| test_gemini_savings_profile.py | ||
| test_header_safe_transforms.py | ||
| test_mcp_stats_aggregation.py | ||
| test_openai_backend_path.py | ||
| test_openai_chat_savings_profile.py | ||
| test_openai_responses_ccr.py | ||
| test_openai_stream_usage_option.py | ||
| test_openai_transport_path_prefix.py | ||
| test_openai_upstream_header.py | ||
| test_phase3_byte_identity.py | ||
| test_request_logger.py | ||
| test_transformations_feed.py | ||