Skip to content

Preserve overload status and Retry-After before Anthropic streaming - #103

Open
sanchitmonga22 wants to merge 7 commits into
issue-80-shim-keepalivefrom
codex/issue83-stream-overload
Open

sanchitmonga22 wants to merge 7 commits into
issue-80-shim-keepalivefrom
codex/issue83-stream-overload

Conversation

@sanchitmonga22

Copy link
Copy Markdown
Collaborator

Streaming /v1/messages now waits for upstream HTTP headers before committing downstream SSE. A pre-stream 429 or 503 remains that HTTP status, with the original numeric or HTTP-date Retry-After and a typed Anthropic JSON error. Previously both became HTTP 200 with an SSE error and no cooldown header; nonstream 503 also lost its cooldown.

Related to #83. The issue remains open for owner acceptance and deployment testing.

The implementation uses cpp-httplib's response-header callback on the existing pooled connection. One bounded chunk slot connects the upstream reader to the downstream writer, preserving backpressure without buffering the full generation. Failed or abandoned streams discard the socket. A connection failure after receiving HTTP headers is never retried as a stale socket; mid-stream failures retain typed SSE errors and are not replayed.

Validation:

  • Regression red on PR Reuse upstream connections in the Anthropic shim and the JetBrains proxy #82 (ded8a74): streaming 429 and 503 both returned 200 without Retry-After; nonstream 503 lost Retry-After. Existing seven transport tests passed.
  • Regression green: actual loopback HTTP requests preserve 429/503, numeric/date Retry-After, upstream bearer authentication, typed JSON, and exactly one upstream request per refusal.
  • Complete macOS C++ build against verified pinned SDK kit 0.20.37; ctest --test-dir build --output-on-failure: 12/12 passed. Existing transport tests cover successful streaming, separate concurrent connections, pooled reuse, stale connection retry and no mid-stream replay.
  • scripts/ci/check-agents-sync.sh and scripts/test/smoke.sh build/wally-cxx passed.
  • Apple MLX product-host build and Windows binary were not exercised; this was the C++ build with WALLY_APPLE_MLX_HOST=OFF, explicit OpenSSL root. Optional live/model tests retain their existing internal skips. No real API credentials or paid inference were used. Exact Claude Code retry timing and deployed gateway behavior still require final owner E2E acceptance.

Merge sequence: PR #82 → this PR → the separate #84 terminal-stream validation PR. Current base is issue-80-shim-keepalive; final integration target is main (this repository has no dev/development branch). After #82 merges, rebase this branch onto main and retarget, particularly if #82 is squash-merged. Siddhesh owns merging. Coordinate Aman's active #81 cancellation work against this updated transport; this PR does not implement the explicit remote cancel API. No automatic issue-closing keywords are used.

AmanSwar and others added 7 commits September 12, 2026 20:56
The translator built an httplib::Client per request, in the streaming sink
and in the non-streaming handler, so every request opened a TCP connection
and completed a TLS handshake -- measured at ~541 ms against the hosted
endpoint, paid before a byte of the request was sent, on every turn.

A pool now keeps keep-alive clients and lends them out per request
(src/net/upstream_pool.{h,cpp}). A pool rather than one shared client
because an httplib::Client serialises requests on its socket, and an
editor's parallel calls must not queue behind a stream that lasts minutes.
A lease returns to the pool only when its request completed cleanly; an
abandoned stream or a transport error discards the socket.

A request that fails on a REUSED connection with no status, nothing
delivered, and a connection-class error is sent once more on a fresh one
(RetryOnFreshConnection): the stale keep-alive case. A fresh connection
that fails surfaces; a request that produced output is never repeated.
Accepted, capped and logged: "no bytes back" does not prove the server
never ran the request, so a retry can in rare cases run a generation
twice. The bearer is set per lease, not baked into the client, because the
JetBrains proxy renews its token on a 401 (wired in a following commit).

Connect timeout is now 10 s (httplib default 300 s): a black-holed connect
no longer holds the editor's request for five minutes. Read timeout stays
600 s.

Break test (tests/test_wally_anthropic.cpp, fake OpenAI upstream on
loopback recording the peer port of every request):

  unmodified shim (093cd44):
    [FAIL] sequential_requests_reuse_the_upstream_connection
           Expected: same peer port on both upstream requests (one connection)
           Actual:   [62567, 62569]
    [PASS] concurrent_requests_use_separate_connections
    Results: 1 passed, 1 failed, 2 total

  with this change:
    [PASS] sequential_requests_reuse_the_upstream_connection
    [PASS] concurrent_requests_use_separate_connections
    [PASS] pool_returns_a_clean_lease_and_drops_a_discarded_one
    [PASS] pool_outlives_an_outstanding_lease
    [PASS] retry_rule_only_on_a_stale_reused_connection
    Results: 5 passed, 0 failed, 5 total
  ctest: 100% tests passed, 0 tests failed out of 11
…tput rule (#80)

Two integration tests on the translator, plus the fake upstreams moved into
tests/fake_upstream.h so the JetBrains proxy's suite can share them.

A half-open upstream (raw sockets, POSIX-only; the retry rule's table test
covers every platform) answers the first request on a connection, keeps it
open, then reads the second and closes without answering. That is the stale
keep-alive shape the client cannot see before sending -- httplib's
is_socket_alive already catches a peer that sent FIN, so a FIN-after-answer
server would never reach the retry. The translator must retry once on a
fresh connection: the upstream sees three requests, the first two on one
connection and the third on another.

A reused upstream that dies mid-stream (two frames, then the connection
drops) must NOT be retried: the error is connection-class with no status,
so only "bytes reached the caller" stops a second generation. The upstream
sees exactly two requests.

An "editor abandons the stream" variant was written and removed: with the
retry rule neutered to always-true the upstream still saw only two requests,
i.e. on loopback the translator's writes to the closed reader kept succeeding
for longer than the stream lasted, so the test could not observe the abort
path and passed for the wrong reason. Abandonment is Error::Canceled, now an
explicit row of the table test.

Break tests, each neutered in src/anthropic/messages.cpp and restored:

  retry removed (attempt < 1):
    [FAIL] stale_reused_connection_is_retried_once_on_a_fresh_one
           Actual:   200, 502; [62702, 62702]
    Results: 6 passed, 1 failed, 7 total

  received_any ignored in the retry decision:
    [FAIL] upstream_dying_mid_stream_is_not_retried
           Actual:   200, 200; [62817, 62817, 62819]
    Results: 7 passed, 1 failed, 8 total

  restored: Results: 7 passed, 0 failed, 7 total
The proxy behind `wally clion` / `wally rustrover` built an httplib::Client
per request at two sites -- the streaming sink and the non-streaming
handler -- with the same cost the translator paid: a TCP connect and a TLS
handshake before every request. It now takes leases from the same
UpstreamPool.

The streaming sink already tried twice, the second time with a token the
console had just renewed after a 401. That stays, and composes with the
stale-connection retry: each is allowed once, on its own condition -- a 401
body renews the token, a connection-class failure with nothing received on
a REUSED connection takes a fresh one -- and neither can duplicate output,
because nothing reaches the sink until an event stream is recognised. The
bearer is set per lease for exactly this reason: the renewed token must ride
the next attempt.

Break tests (tests/test_wally_ide_proxy.cpp, sharing tests/fake_upstream.h):

  PostOnce retry removed (attempt < 1):
    [FAIL] proxy_stale_reused_connection_is_retried_once
           Actual:   200, 502; [63164, 63164]
    Results: 1 passed, 1 failed, 2 total

  non-streaming site back to a per-request client:
    [FAIL] proxy_sequential_requests_reuse_the_upstream_connection
           Actual:   [63170, 63172]
    [FAIL] proxy_stale_reused_connection_is_retried_once
           Actual:   200, 200; [63176, 63178]
    Results: 0 passed, 2 failed, 2 total

  restored: Results: 2 passed, 0 failed, 2 total
- UpstreamLease move-assignment dropped the client it already held instead
  of returning it to the pool (or discarding it). Nothing moves-assigns a
  lease today; it is still the wrong contract, and now it releases the held
  client exactly as end of scope would before taking the other's.
- The proxy tests' FreePort() probe bound a port and dropped the Server
  object: httplib's stop() only closes the listening socket while running,
  and ~Server() does not close it at all, so each call leaked a listening
  socket. The probe now listens, stops, and joins, which closes it. (An
  assertion that StartProxy received the requested port was tried and
  removed: with the leaky probe StartProxy still got the port on macOS, so
  the assertion could not be shown to fail.)
- The fake upstream's mode flags are written by the test thread and read by
  server threads; they are atomics now.

ctest: 100% tests passed, 0 tests failed out of 12
@coderabbitai

coderabbitai Bot commented Sep 13, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 73bc9583-db10-40d6-b6ee-fd9a0ea2d532

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sanchitmonga22

Copy link
Copy Markdown
Collaborator Author

Final independent review: PR #103 at af0a4823ee6e69414da2413cc2b525b524a91eae has no unresolved blocking findings in its scoped diff. Reviewed the published GitHub diff, body and comments against the existing PR #82 transport base. The fix preserves pre-stream 429/503 status, numeric/date Retry-After and bearer authentication, with bounded read-ahead and no replay after response headers.

Independent checks: full CTest 12/12 passed at implementation commit 68f6b2d; the final tests-only update at af0a482 passed the targeted Anthropic suite again. Optional live/model harness entries are not live proof. Exact Claude retry timing, packaged Swift/MLX, Windows and deployed E2E remain acceptance work. CodeRabbit skipped the non-default base; its status is not an independent code review.

@Siddhesh2377 merge order: #82#103#104. Rebase/retarget each dependent PR to main after its predecessor merges, then rerun checks; do not merge into a dependency branch. Coordinate issue #81 cancellation work with this transport. Issue #83 remains open; this review does not claim deployment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants