Repository navigation
refactor(chat): guard the blocking HTTPException arm so it can't log status 200 - #159
Conversation
…eption The blocking path's `except HTTPException: raise` arm re-raises without recording exc.status_code, so the finally writes a request-log row with the handler's initial status_code = 200 — a failed request logged as a success (cost ~0, no error_type), poisoning every status-filtered analytics/spend query. The sibling UpstreamProviderError and generic-Exception arms already record theirs; this closes the third. Closes Continuum-AI-Corp#158
There was a problem hiding this comment.
🐳 OrcaCode Review
✅ No findings — nothing to flag in this PR. Great work!
OrcaCode Review — Route Smarter. Ship Safer. Spend Less.
Engine-reported: 249 calls · 6.8M tokens · 94% cached
❤️ Share · Install OrcaCode Review
Free on GitHub — the review runs on your own OrcaRouter key. If it helped, a shout-out goes a long way.
Share: X · Reddit · LinkedIn
Follow: X · Discord · LinkedIn · OrcaRouter
|
Happy to take this one. It merges clean onto But I can't reproduce the bug as described, and I think the premise in #158 is wrong. #158 says:
I drove exactly that path end-to-end: two fake upstreams (local BYOK on one port, hosted on another), local pinned to 429, blocking (non-stream) requests, then read the rows back out of The 429 is already recorded correctly. Two runs, same result. The reason is that the adapter doesn't raise
Worth noting the test here passes because it injects the exception directly: fake.acompletion = AsyncMock(side_effect=HTTPException(status_code=429, ...))That's a shape the real adapter never produces — so the test confirms the handler's logic, but not that the path is reachable. None of that makes the change wrong. Leaving one of three arms able to leak What I'd like to adjust is the framing: #158 is written as a live data-integrity bug ("Analytics and spend history count failed requests as successes"), and on current |
The arm is unreachable on current main: the adapter blanket-translates every exception into UpstreamProviderError (packages/litellm_adapter/client.py, acompletion), so the hosted-fallback 429 from Continuum-AI-Corp#153 lands in the UpstreamProviderError arm, which already records its status. The guard is kept as a latent-trap fix, not a bug fix.
|
No repro on my side either — you're right, and the premise in #158 was wrong. I verified it the same way you did, plus the structural reason it can't happen at all: Reworded rather than merged on a wrong rationale:
Please do merge it as a defensive guard. The reason I'd keep on the record is the one in the reworded #158: it's the only one of the three arms that can leave One thing your audit surfaced that is real, now listed under "Out of scope" in #158 instead of growing this diff: every pre-dispatch rejection (allowlist 403 at |
There was a problem hiding this comment.
🐳 OrcaCode Review
✅ No findings — nothing to flag in this PR. Great work!
OrcaCode Review — Route Smarter. Ship Safer. Spend Less.
Engine-reported: 195 calls · 4.4M tokens · 93% cached
❤️ Share · Install OrcaCode Review
Free on GitHub — the review runs on your own OrcaRouter key. If it helped, a shout-out goes a long way.
Share: X · Reddit · LinkedIn
Follow: X · Discord · LinkedIn · OrcaRouter
|
Merged. And thanks for the correction on my correction — your structural argument is stronger than my grep, and I've confirmed it: # packages/litellm_adapter/client.py:201-204
try:
resp = await self._router.acompletion(**kwargs)
except Exception as exc:
raise _translate_error(exc) from excA blanket I also verified it at the branch level rather than by inference this time. Instrumented all three arms and ran every blocking-path failure mode, on
Withdrawing one thing from my own comment: I wrote that an On the out-of-scope item — it reproduces, and it's worth its own issue. Your line numbers are from #91, so I located the equivalents on Confirmed end-to-end, 3 identical runs:
So an operator can't see that a key is being blocked by its allowlist, or that auto-routing is failing to resolve — those requests simply don't exist in analytics. Different class from a mis-recorded row, as you said, and arguably worse for the allowlist case, since that's exactly the signal you'd want when debugging why a key "doesn't work". I'll open an issue with this repro unless you'd rather own it — say the word. It'll want the streaming pre-dispatch gap folded in too, since it's the same shape. |
…status 200 (Continuum-AI-Corp#159) * fix(chat): log the real status when a blocking request raises HTTPException The blocking path's `except HTTPException: raise` arm re-raises without recording exc.status_code, so the finally writes a request-log row with the handler's initial status_code = 200 — a failed request logged as a success (cost ~0, no error_type), poisoning every status-filtered analytics/spend query. The sibling UpstreamProviderError and generic-Exception arms already record theirs; this closes the third. Closes Continuum-AI-Corp#158 * docs(chat): correct the rationale on the blocking HTTPException guard The arm is unreachable on current main: the adapter blanket-translates every exception into UpstreamProviderError (packages/litellm_adapter/client.py, acompletion), so the hosted-fallback 429 from Continuum-AI-Corp#153 lands in the UpstreamProviderError arm, which already records its status. The guard is kept as a latent-trap fix, not a bug fix.
Orca-Code-Review — push 2
✅ no blocking findings
Not a live bug — defensive guard
Closes #158, which has been reworded (and typed
Task) accordingly.#158 claimed an
HTTPExceptionfrom inside the blockingtryalready lands inrequests_logasstatus_code = 200. That premise was wrong — @xizhuomengcontin showed it in review by driving the #153 hosted-fallback 429 end-to-end onmain, where the row is already(429, 'rate_limit_error', 0).I verified it independently, and the arm is unreachable for a stronger reason than "the adapter doesn't raise
HTTPException":packages/litellm_adapter/client.py:201-204—acompletionwraps the router call in a blanketexcept Exception as exc: raise _translate_error(exc) from exc, so whatever litellm raises arrives asUpstreamProviderError.HTTPExceptionsubclassesException, so it would be translated too.packages/litellm_adapter/client.py:76— the hosted-fallback rate-limit signal becomesUpstreamProviderError(http_status=429, error_type="rate_limit_error")→ theexcept UpstreamProviderErrorarm, which already recordsexc.http_status.HTTPExceptionanywhere inpackages/; everyraise HTTPExceptionin the engine is pre-dispatch (:403allowlist,:442/:509/:517auto-resolution,:549budget — all before thetry) or is an except arm itself.So this PR changes behaviour on no reachable path — hence
refactor, notfix.Why merge it anyway
except HTTPExceptionis the only one of the three arms that leaves the handler-localstatus_codeat its initial 200, so itsfinallywould write a success-shaped row (cost ~0,error_typeNULL) for a failed request. It becomes live the first time anything inside thattryraisesHTTPException, which is plausible near-term given where the hosted-fallback work is heading. #91's blocking budget settlement already has to work around the same shape of problem (status_code < 400plus a "was a completion dict actually delivered" gate) — this removes that footgun at the root.Fix
Unchanged: record
status_code = exc.status_codein the HTTPException arm before re-raising, matching the sibling arms. Two lines + one integration test.Push 2 (
986aac6) is comment and docstring only: it deletes the wrong rationale ("Raised from inside the try today by the hosted-fallback signal (HTTPException(429))") from the new code comment and from the test docstring, and states the accurate one. No behaviour change; suite re-run green,ruffclean.Deliberately out of scope:
ERROR_TYPE_HEADER— a follow-up if there's appetite; the guard doesn't need it.tryso no row is written at all, and the streaming path's twin writes no row on a pre-streamHTTPExceptioneither.Test plan
test_chat_completion_blocking_httpexception_logs_real_status— injectedHTTPException(429)→ response 429 and log rowstatus_code == 429,cost_microcents == 0. The injection is a shape the real adapter never produces, so this pins the handler's arm logic; it is not evidence the path is reachable.986aac6ruff check app packages testscleanInteraction with #91
Discovered while reviewing #91's blocking budget settlement, which keys off
status_code < 400: #91 had to additionally gate on "a completion dict was actually delivered" so an early failure couldn't be charged the full remaining allowance (push 6). Landing this first removes that footgun at the root; #91's rebase is a 3-line touch on the adjacent arm. Both orders work; this is the smaller change.One follow-up that rebase should pick up: #91's settlement comment (
eef1eff) justifies itsresponsegate partly by "the re-raised HTTPException above, whose status_code never left 200". Once this lands that parenthetical is stale — the arm does record the status now, and the gate's remaining justification is the no-usage-delivered case.