headroom/tests/test_proxy
Focused Instability 4dab254d52
fix: emit SSE ping before message_start on Bedrock streaming path (issue #902) (#1080)
## Description

Closes #902

Mid-turn user interjections (steering) silently dropped through the
Bedrock streaming path. The _stream_response_bedrock code path
reconstructs Anthropic SSE events from parsed StreamEvent objects
instead of passing raw bytes through, so SSE-level ping keepalives are
never forwarded to Claude Code. Claude Code relies on ping events to arm
its mid-turn steering / interruptible state; without them, queued
interjections are discarded instead of sent.

Root cause (confirmed):
- Standard direct-Anthropic path does a raw yield-chunk passthrough —
pings flow unchanged.
- Bedrock path (_stream_response_bedrock.generate()) reconstructs events
from litellm/anyllm
stream_message() output, which only yields semantic events
(message_start, content_block_*,
  message_delta, message_stop, error). No pings, ever.

Fix: emit a synthetic 'event: ping / data: {}' at stream start (before
the first message_start)
so downstream clients see the same ping-then-content cadence as a real
Anthropic stream.

Note: periodic pings for very long responses (>~25s) may be needed if
steering disarms on a timer.
This commit arms it at turn start; follow-up if reporters confirm
steering still drops on long turns.

The causal link (ping → steering) is the reporter's hypothesis from
hands-on debugging.
The observable defect (zero pings in stream) is confirmed and fixed.

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)

## Changes Made

- headroom/proxy/handlers/streaming.py: yield ping event before the
event loop in _stream_response_bedrock.generate()
- tests/test_proxy/test_bedrock_sse_ping.py: 3 new tests asserting ping
appears before message_start

## Testing

- [x] Unit tests pass (pytest)
- [x] Linting passes (ruff check .)
- [x] New tests added for new functionality

### Test Output

```
tests/test_proxy/test_bedrock_sse_ping.py::test_bedrock_stream_emits_ping_before_message_start PASSED
tests/test_proxy/test_bedrock_sse_ping.py::test_bedrock_stream_ping_has_empty_data PASSED
tests/test_proxy/test_bedrock_sse_ping.py::test_bedrock_stream_contains_message_stop PASSED
tests/test_backend_streaming_cache_metrics.py (4 tests) PASSED
7 passed in 4.41s
```

## Real Behavior Proof

- Environment: macOS, Python 3.11, headroom unit tests
- Exact command / steps: pytest
tests/test_proxy/test_bedrock_sse_ping.py -v
- Observed result: 3 new tests pass; ping appears before message_start
in Bedrock stream
- Not tested: end-to-end against live Bedrock + Claude Code (no Bedrock
credentials available)

## Review Readiness

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

## Checklist

- [x] Code follows project style guidelines
- [x] Code is commented where non-obvious
- [x] No new warnings
- [x] Tests added and passing

## Additional Notes

The Rust proxy files mentioned in the issue (sse/framing.rs,
sse/anthropic.rs) are NOT part of this fix.
Those drops are in a telemetry-only tee task that never affects the
client byte path — the Rust proxy
does a raw bytes passthrough for all responses. The defect is
Python-only, confined to the Bedrock path.

---------

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-08-04 21:41:58 -05:00
..
test_anthropic_buffered_timeout.py fix(proxy): add an Anthropic buffered read-timeout override (#1331) 2026-06-23 22:46:31 -05:00
test_anthropic_ccr_deferred_injection.py fix(proxy): stop toggling headroom_retrieve in the Anthropic tools array (#2672) 2026-08-03 16:18:11 -07:00
test_anthropic_ccr_raise.py fix(anthropic): CCR exception must re-raise, not silently swallow (#838) 2026-06-10 21:11:46 -05:00
test_anthropic_streaming_ccr_retrieve.py fix(proxy): keep buffered CCR streams alive (#2479) 2026-07-22 06:09:05 -07:00
test_anthropic_upstream_header.py fix(proxy): honor x-headroom-base-url on /v1/messages route (#1763) 2026-07-09 12:43:40 -05:00
test_background_compression.py perf(compression): take large cold-start contexts off the synchronous kompress path (#1171) (#1298) 2026-06-23 10:48:06 -05:00
test_bedrock_passthrough.py feat(proxy): compress AWS Bedrock InvokeModel requests via configurable upstream (#720) 2026-06-16 14:56:16 -05:00
test_bedrock_sse_ping.py fix: emit SSE ping before message_start on Bedrock streaming path (issue #902) (#1080) 2026-08-04 21:41:58 -05:00
test_cc_switch_reconciler.py feat(proxy): cc-switch reconciler — keep Headroom in the request path alongside cc-switch (#1030) 2026-06-17 08:41:16 -05:00
test_ccr_frozen_prefix_coupling.py fix(proxy): stop toggling headroom_retrieve in the Anthropic tools array (#2672) 2026-08-03 16:18:11 -07:00
test_compression_failure_action.py fix(codex): fail open for proxy compression timeout 2026-06-04 14:04:49 +05:30
test_compression_timeout_config.py feat(proxy): make COMPRESSION_TIMEOUT_SECONDS configurable via env (#946) (#991) 2026-06-16 21:10:01 -05:00
test_compute_turn_id.py fix(proxy): strip cache_control before hashing turn_id 2026-04-23 16:04:57 +02:00
test_gemini_savings_profile.py fix(proxy/gemini): None-guard token counts from usageMetadata (#2347) 2026-07-17 12:10:33 -07:00
test_header_safe_transforms.py feat(transforms): attribute read_lifecycle + smart_crush tags (#249) 2026-06-11 11:51:26 -05:00
test_mcp_stats_aggregation.py fix(proxy): Strands MCP bundle + backend path fixes + Codex fail-closed protection 2026-05-21 11:00:14 -07:00
test_model_router.py feat(proxy): add opt-in cost-aware model router (#1706) (#2205) 2026-07-15 19:58:17 +00:00
test_model_router_wiring.py feat(proxy): add opt-in cost-aware model router (#1706) (#2205) 2026-07-15 19:58:17 +00:00
test_openai_backend_path.py fix(proxy): skip max_tokens rename for backend-routed openai chat (#2401) 2026-07-18 16:47:10 -07:00
test_openai_chat_savings_profile.py fix(proxy/openai): None-guard usage token counts on the chat path (#2431) 2026-07-19 22:15:22 -07:00
test_openai_responses_ccr.py fix(proxy): keep buffered CCR streams alive (#2479) 2026-07-22 06:09:05 -07:00
test_openai_stream_usage_option.py fix(proxy/openai): respect explicit stream_options.include_usage (#2026) 2026-07-13 00:43:16 -04:00
test_openai_transport_path_prefix.py fix(codex): OpenCode Zen telemetry attribution (#1648) 2026-07-07 11:35:21 -05:00
test_openai_upstream_header.py fix(proxy): preserve sub-path in X-Headroom-Base-Url custom upstream (#2037) (#2127) 2026-07-13 19:53:45 -04:00
test_phase3_byte_identity.py perf(compression): take large cold-start contexts off the synchronous kompress path (#1171) (#1298) 2026-06-23 10:48:06 -05:00
test_request_logger.py feat(proxy): log compressed messages alongside original request (#261) 2026-06-11 19:02:54 -05:00
test_settings_fresh_process_precedence.py fix(proxy): dedupe Codex WS request logging for accurate mixed-provider dashboards (#2189) 2026-07-15 18:18:34 +00:00
test_settings_store.py fix(proxy): dedupe Codex WS request logging for accurate mixed-provider dashboards (#2189) 2026-07-15 18:18:34 +00:00
test_transformations_feed.py fix(proxy): scope CORS to loopback + gate operator/content endpoints (#1226) 2026-06-21 00:50:55 -07:00