fix: re-inject Anthropic keepalive pings on the streaming path so long generations don't idle out - #804
Open
sjawhar wants to merge 1 commit into
Conversation
Slow models emit only wire `ping` keepalives before content; the Anthropic SDK's typed stream drops them, so the proxy went silent for the whole pre-content phase and intermediaries (the ALB idle timeout) cut healthy long streams mid-flight. Emit an Anthropic-style ping when the upstream is idle > STREAM_KEEPALIVE_SECONDS.
legion-implementer
Bot
force-pushed
the
fix/anthropic-stream-keepalive
branch
from
July 8, 2026 17:55
953e997 to
46cf2c9
Compare
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.
Problem
When proxying a streaming request (
messages.create(stream=True)), the gateway can go completely silent on the client connection for the entire duration of a long generation. Any intermediary with an idle timeout (load balancer, reverse proxy, API gateway) then cuts the connection mid-stream even though the request is healthy — the client sees a truncated stream /RemoteProtocolError/ 504 while the gateway and Anthropic both consider the request successful.Root cause
Anthropic's wire protocol emits periodic
event: pingkeepalives during generation (notably when a model produces no output tokens for a while — extended thinking, or a slow/gated response). The Anthropic Python SDK's typed event stream drops thesepingevents (they aren't part ofRawMessageStreamEvent), soasync for event in client.messages.create(..., stream=True)never yields them. The gateway therefore forwards nothing to the client betweenmessage_startand the first content event. A direct connection toapi.anthropic.comsurvives the same long generation precisely because those pings keep the socket active; routed through the gateway, they're gone.Reproduction (observed)
A model that delivers its content in a burst near the end of a ~4-minute generation (first
content_block_deltaat ~235s), behind an intermediary with a 120s idle timeout:message_startarrives, then silence; connection cut at ~120s (RemoteProtocolError: peer closed connection without sending complete message body).Fix
_stream_with_keepaliveinpipeline/anthropic_processor.pywraps the emission stream and, whenever the upstream produces no event withinSTREAM_KEEPALIVE_SECONDS(15s), emits an Anthropic-styleevent: ping— only aftermessage_start, to preserve wire event ordering. The in-flight__anext__is shielded from the per-wait timeout so no real event is ever dropped, and the pending read is cancelled on generator close.Tests
tests/luthien_proxy/unit_tests/pipeline/test_anthropic_processor.py::TestStreamWithKeepalive— 4 cases: keepalives injected during a gap without dropping/reordering real events; no keepalives when fast; empty source terminates; generator close cancels the pending read. Full processor suite passes.