fix(auth): rotate on in-stream provider errors during bootstrap - #195
fix(auth): rotate on in-stream provider errors during bootstrap#195warelik wants to merge 2 commits into
Conversation
Detect upstream HTTP 200 SSE/JSON error payloads (429, 503, 401, 403) during bootstrap before forwarding, allowing auth rotation instead of swallowing the error or forwarding broken streams. Refs router-for-me/CLIProxyAPI#4881
|
Scope note, so this PR is not mistaken for a full port. This PR carries exactly one fix from the upstream work in router-for-me/CLIProxyAPI#4881 — rotating on in-stream provider errors during stream bootstrap — and nothing else from that series. The upstream series contains a further ~17 commits touching the same subsystem, and it introduces Porting the rest of the upstream series wholesale onto that divergence would produce an unreviewable diff and would very likely delete or rewrite behavior this repository intentionally has. We are deliberately not doing that. If specific upstream fixes from that series are wanted here, they are better taken one at a time, each with its own test, against this repository's own copy of the file — and we are happy to prepare them individually on request. Upstream tracking for the full series: router-for-me/CLIProxyAPI#4881. |
…close readStreamBootstrap consulted streamError() at channel close without finishing the bootstrap state first. flushData() runs only on a blank separator line or from finish(), so an SSE error event whose data line is newline-terminated but never followed by that blank line stays buffered in dataLines: the provider error is never evaluated, the bootstrap reports closed=true, and the caller receives an empty stream instead of a routable failure it can fail over on. hasMeaningfulOutput() already returns false once streamErr is set with no content, so finalizing first cannot swallow a real completion. Regression test: TestReadStreamBootstrapFinalizesDetectorAtEOF.
|
Follow-up in b3f4bc1: finalize the bootstrap state at EOF before reporting a clean close.
Reverse bite-check — with With the fix, Same fix upstream: router-for-me/CLIProxyAPI#4881 (commit 4a001bf1, thread router-for-me/CLIProxyAPI#4881 (comment)). The test file is byte-identical in both repositories. |
|
Filed #201 for the plugin-host half of this defect: |
|
Upstream review follow-up note: router-for-me/CLIProxyAPI#4881 picked up a related fix — the empty-completion detector now recognizes bare Interactions |
After Plus kaitranntt#195 the conductor rotates within the same ExecuteStream call when a provider error envelope is detected in the bootstrap, so the test should assert err == nil and fallback content. Keep the cooldown assertions on the first-picked auth and sort the auth IDs so the error auth is deterministic.
Summary
Detect in-stream provider error envelopes during stream bootstrap and fail over to rotate credentials, preventing dead auth keys from being recorded as successful completions.
Problem
Upstream providers (e.g. Gemini, Claude, Antigravity) may return HTTP status 200 OK while embedding provider error envelopes directly inside the SSE stream body (e.g. Gemini
data: {"error":{"code":429,"message":"Resource exhausted","status":"RESOURCE_EXHAUSTED"}}or Claudeevent: error\ndata: {"type":"error","error":{"type":"overloaded_error","message":"Overloaded"}}).Because network transport succeeded (
chunk.Err == nil),streamBootstrapStatepreviously markedsawUnknownData = true. Consequently,hasMeaningfulOutput()returnedtrue, the bootstrap phase completed without error, and upon stream terminationwrapStreamResultrecordedSuccess: true.Fix
sdk/cliproxy/auth/empty_completion.go):streamErr *ErrorandcurrentEvent stringtracking tostreamBootstrapState.streamErrorEnvelope,inferHTTPStatus,parseStreamErrorFromEnvelope, andevalProviderErrorto identify provider error structures across Gemini, Claude, and OpenAI-compatible SSE streams.processSingleLine,flushData, andobserveto capture error frames, suppresssawMetadataOnly/sawUnknownData, and sethasMeaningfulOutput() = false.sdk/cliproxy/auth/conductor_stream.go):readStreamBootstrap: checksbootstrap.streamError()on chunk observation or stream EOF; aborts bootstrap if an in-stream error occurs before meaningful output, initiating credential failover.wrapStreamResult: inspects late stream chunks viadetectStreamPayloadErrorto recordSuccess: falseand trigger account cooldown if a provider error arrives mid-stream.TestExecuteStreamInStreamUnknownJSONForwardedNotRotated).TestExecuteStreamInStream400InvalidRequestNotRotated).sdk/cliproxy/auth/empty_completion_test.go):TestExecuteStreamInStreamGemini429ErrorRotatesAuth: in-stream Gemini 429 rotates auth and marks quota exceeded.TestExecuteStreamInStreamClaudeOverloadedErrorRotatesAuth: in-stream Claudeoverloaded_errorrotates to fallback auth.TestExecuteStreamInStream400InvalidRequestNotRotated: in-stream 400 invalid request returns error directly without auth rotation.TestExecuteStreamInStreamUnknownJSONForwardedNotRotated: unrecognized non-error JSON forwarded without rotation.TestExecuteStreamMidStreamInStreamErrorMarksAuthFailed: mid-stream error after initial tokens recordsSuccess: false.Testing
TMPDIR=/Users/warelik/.cache/gotmp go build ./...— exit code 0TMPDIR=/Users/warelik/.cache/gotmp go vet ./sdk/cliproxy/auth/...— exit code 0TMPDIR=/Users/warelik/.cache/gotmp go test -count=1 ./sdk/cliproxy/auth/...— exit code 0Upstream Reference
sdk/cliproxy/auth/and does not touchinternal/translator/(src/AGENTS.mdtranslator policy does not apply).