Commit graph

2 commits

Author SHA1 Message Date
Yui(ゆい)
4b1c449c73
[codex] fix(proxy): parse CRLF SSE event terminators (#649)
## Summary
- support CRLF (`\r\n\r\n`) SSE event terminators in the byte-buffer
parser
- parse completed SSE events with `splitlines()` so LF and CRLF line
endings are handled consistently
- add a regression test for CRLF-terminated SSE events

## Why
SSE streams may be emitted with CRLF line endings by HTTP stacks. The
existing byte-buffer parser only looked for `\n\n`, so a complete
CRLF-terminated event could remain buffered and never reach usage/event
parsing.

## Validation
- `.venv/bin/pytest tests/test_sse_utf8_split.py
tests/test_streaming_usage_parser.py -q`
- `.venv/bin/ruff check headroom/proxy/helpers.py
tests/test_sse_utf8_split.py`

## Risk
Low. The change is isolated to complete-event boundary detection and
keeps the existing invalid UTF-8 behavior loud for complete events.
2026-06-10 21:16:00 -05:00
chopratejas
148ded392a fix: A8 — SSE delta arms, UTF-8 buffer, phase preservation, request-id, 413
Eliminates the Python wire-format hotfix bugs gated on Phase A's
lockdown so the proxy is safe through Phase H's Python retirement.

Bugs retired:
  - P0-7 / P4-44: Codex `phase` field is now explicitly preserved
    through the Responses-API ↔ Chat-Completions round-trip; multi
    text-part rebuild collapses to a single text part (no more
    content doubling).
  - P1-8: Bytes-level SSE event splitter
    `parse_sse_events_from_byte_buffer`; emoji/CJK split across
    chunks survive intact. Buffer is `bytearray`; UTF-8 decode happens
    only AFTER the `\n\n` event terminator is located in bytes.
    Invalid UTF-8 in a *complete* event raises (operator-visible
    diagnostic, not silent corruption).
  - P1-9: `_parse_sse_to_response` handles all delta types per
    Anthropic guide §5.1: `thinking_delta`, `signature_delta`,
    `citations_delta`. Block map keyed by `index` so out-of-order
    events reconstruct correctly. `redacted_thinking.data` preserved.
  - P4-47: Unknown Responses-API item types now log a structured
    `unknown_responses_item_type` warning so operators see new
    Codex item types in flight before they break.
  - P5-57: Rust proxy captures upstream `request-id` (Anthropic) and
    `x-request-id` (OpenAI); surfaced as `headroom-upstream-request-id`
    on the response and as a tracing span field. Distinct from the
    proxy's own `x-request-id`.
  - P5-59: Body-too-large now returns 413 (was 400). Pre-checks
    `Content-Length` and rejects without consuming the body when
    present; chunked uploads still buffer-then-fail with 413.

Configurability (no hardcodes):
  - HEADROOM_SSE_BUFFER_MAX_BYTES (default 1 MiB) — per-event cap.
  - HEADROOM_PROXY_BODY_TOO_LARGE_STATUS (default 413) — operator
    override for body-too-large status.

A7 follow-up: `_DummyAnthropicHandler._retry_request` accepts the
A3 byte-faithful kwargs (`original_body_bytes`, `body_mutated`,
`mutation_reasons`, `request_id`, `forwarder_name`, `path_for_log`)
so the existing 20 backpressure tests stay green against the real
handler signature.

The project-wide grep
  git grep 'errors="ignore"\|errors="replace"' headroom/proxy/handlers/ headroom/ccr/
returns nothing; the single remaining lossy-decode site (response-
body diagnostics, not SSE) routes through `safe_decode_for_logging`
in `headroom/proxy/helpers.py`.

Tests:
  - tests/test_sse_thinking_blocks.py (4 tests)
  - tests/test_sse_utf8_split.py (3 tests)
  - tests/test_proxy_responses_phase_preservation.py (4 tests)
  - crates/headroom-proxy/tests/integration_request_id.rs (2 tests)
  - crates/headroom-proxy/tests/integration_body_size.rs (2 tests)
2026-05-02 10:35:11 -07:00