CLAUDE-A follow-up: native Claude SSE in the proxy - #49
Conversation
The proxy's Claude streaming arm has carried a "synthesise a single-delta
stream from a blocking send_message" placeholder since CLAUDE-A landed in
May. It worked but defeated the purpose of streaming: clients saw one
giant chunk after the full response materialised. This replaces it with
real SSE.
Implementation:
- handle_streaming's Claude arm now calls client.stream_message(&req),
then drives stream.next_event() in a loop:
* MessageStart → capture resolved model slug + initial usage
* ContentBlockDelta::TextDelta → forward as StreamChunk::Delta(text);
drop InputJson/Thinking/Signature deltas to match the non-streaming
extract_text contract
* MessageDelta → update latest_usage (cumulative output + cache totals)
* MessageStop → flush StreamChunk::Done via send_claude_done
* Stream exhausted without MessageStop → still flush Done so the
cache write tail fires
* Mid-stream error → emit StreamChunk::Error and return
- New send_claude_done helper centralises the Usage → cache-token
translation so the MessageStop and "exhausted without stop" paths
agree. It keeps cache_creation_input_tokens / cache_read_input_tokens
honest by emitting None when Anthropic reported zero (rather than
forcing Some(0) which would lie about cache participation).
- Channel buffer widened 4 → 64. A real Claude stream produces dozens
of small deltas; the prior 4-slot bound would back-pressure the
reqwest chunk reader on every token.
- Receiver-dropped detection: tx.send().await.is_err() on a TextDelta
returns early from the pump task, freeing the reqwest socket without
waiting for MessageStop.
The synthesised path is removed entirely. If a future need for the
blocking variant returns (e.g. provider can't stream), it can come back
as a fallback branch, but right now it's just dead weight.
Tests:
- send_claude_done_emits_cache_tokens_only_when_nonzero — verifies the
Usage → StreamChunk::Done translation, including the then_some guard
that drops zero cache-read counts.
- send_claude_done_handles_missing_usage_gracefully — Done still fires
with all-None token fields when the stream ended before any usage
event arrived.
End-to-end SSE behaviour can't be unit-tested without mock-server
scaffolding the proxy module doesn't have yet; live verification via
curl --no-buffer against /v1/chat/completions with stream:true and a
configured ANTHROPIC_API_KEY is the manual smoke test.
cargo check --workspace --tests --exclude rustcode --exclude rag → clean.
cargo check -p rustcode --lib blocked locally by the known ort-sys CDN
sandbox restriction; CI verifies the rustcode compile.
https://claude.ai/code/session_01CWvyqRpgnpgZk2sXfMpzfL
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThe Claude streaming path in the OpenAI ChangesClaude Native Streaming
Sequence DiagramsequenceDiagram
participant Client
participant ProxyHandler
participant AnthropicClient
participant StreamLoop
participant SendClaudeDone
Client->>ProxyHandler: POST /v1/chat/completions
ProxyHandler->>AnthropicClient: stream_message(request)
activate StreamLoop
loop Process Stream Events
AnthropicClient-->>StreamLoop: MessageStart {model}
StreamLoop->>StreamLoop: record model_used
AnthropicClient-->>StreamLoop: MessageDelta {text delta}
StreamLoop->>Client: StreamChunk::Delta
AnthropicClient-->>StreamLoop: MessageDelta {usage, cache}
StreamLoop->>StreamLoop: accumulate Usage
end
AnthropicClient-->>StreamLoop: MessageStop or exhaustion
deactivate StreamLoop
StreamLoop->>SendClaudeDone: Usage {creation/read tokens}
SendClaudeDone->>SendClaudeDone: omit zero cache token fields
SendClaudeDone->>Client: StreamChunk::Done
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Summary
CLAUDE-A's open follow-up. The proxy's Claude streaming arm has been synthesising a single delta from a blocking
send_messageever since CLAUDE-A landed — it worked but defeated the purpose of streaming (clients saw one giant chunk after the full response materialised). This switches it to real SSE viaAnthropicClient::stream_message.What changed
handle_streaming's Claude arm now drivesstream.next_event()in a loop and translates eachStreamEvent:MessageStartUsageContentBlockDelta::TextDeltaStreamChunk::Delta(text)ContentBlockDelta::{InputJson,Thinking,Signature}extract_text)MessageDeltalatest_usage(cumulative output + cache totals)MessageStopStreamChunk::Doneviasend_claude_doneMessageStopDoneso the cache write tail firesErrStreamChunk::Errorand exitPlus:
send_claude_donecentralises theUsage → StreamChunk::Donetranslation so the two terminal paths agree. Keeps cache token counts honest by emittingNonewhen Anthropic reported zero, rather than forcingSome(0)which would lie about cache participation downstream.tx.send().await.is_err()on aTextDeltareturns early from the pump task, freeing the upstream socket without waiting forMessageStop— important for clients that disconnect mid-stream.Tests
send_claude_done_emits_cache_tokens_only_when_nonzero— verifies theUsage → Donetranslation, including thethen_someguard that drops zero cache-read counts.send_claude_done_handles_missing_usage_gracefully—Donestill fires with all-None token fields when the stream ended before any usage event arrived.End-to-end SSE behaviour isn't unit-tested — the proxy module has no mock-server scaffolding yet. Manual smoke test:
Expected: incremental SSE frames (one per text token batch), terminated by a final frame with
finish_reason:"stop"carryingcache_creation_input_tokens/cache_read_input_tokensextension fields and thedata: [DONE]sentinel.Test plan
cargo check --workspace --tests --exclude rustcode --exclude rag→ cleancargo check -p rustcode --libblocked locally by the knownort-sysCDN sandbox restriction; CI verifiessend_claude_doneunit tests cover the two terminal pathscurl --no-buffersmoke test against a deployed/v1/chat/completionswith a configuredANTHROPIC_API_KEYTODO
CLAUDE-Agains a "Native SSE follow-up done 2026-05-25" addendum.https://claude.ai/code/session_01CWvyqRpgnpgZk2sXfMpzfL
Generated by Claude Code
Summary by CodeRabbit
Improvements
Tests