fix(proxy): stop labelling management and health rejections as auth failures - #39558
Open
mateo-berri wants to merge 1 commit into
Open
Conversation
…ailures Eleven management and health route handlers wrapped their body in a blanket `except Exception` and raised `ProxyException(type=auth_error)` whatever actually went wrong, so `/team/info` answered 404 with `type=auth_error` for a team that does not exist and `/user/update` answered 400 for an unparseable `budget_duration`, both on a valid admin key. `auth_error` is not an OpenAI error type at all, so a client branching on `type` retried the credentials instead of fixing the field. Those handlers now read the type off the exception through a shared `proxy_exception_for` helper, keeping the status each branch already answered with. A `ProxyException` raised deliberately mid-request still passes through with the type it named, so the 403 credential-attach authorization check keeps `auth_error`. Every genuine auth site under `litellm/proxy/auth/` and `ui_sso.py` is untouched
Contributor
Greptile SummaryThis PR centralizes exception conversion for selected management and health endpoints so response error types reflect the underlying exception or HTTP status while preserving existing status codes.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| litellm/proxy/common_utils/openai_error_payload.py | Adds the shared exception-to-proxy-error conversion helper and tests its status, type, parameter, and pass-through behavior. |
| litellm/proxy/health_endpoints/_health_endpoints.py | Routes health-service failures through the shared helper instead of labeling every failure as authentication-related. |
| litellm/proxy/management_endpoints/internal_user_endpoints.py | Updates /user/update error handling to derive an appropriate error type while retaining its existing default status. |
| litellm/proxy/management_endpoints/key_management_endpoints.py | Updates /key/update to use shared error normalization and retain intentional proxy exceptions. |
| litellm/proxy/management_endpoints/model_management_endpoints.py | Applies shared error normalization to the targeted model management handlers. |
| litellm/proxy/management_endpoints/organization_endpoints.py | Preserves organization endpoint status codes while removing blanket authentication-error classification. |
| litellm/proxy/management_endpoints/team_endpoints.py | Makes /team/info report the underlying request failure category rather than an authentication error. |
| litellm/proxy/proxy_server.py | Applies the shared conversion helper to the targeted queue and configuration handlers. |
| tests/test_litellm/proxy/common_utils/test_openai_error_payload.py | Covers helper behavior for proxy, HTTP, typed, untyped, and status-derived exceptions. |
| tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py | Adds endpoint regression coverage using a repository-supported test-quality suppression directive. |
Reviews (2): Last reviewed commit: "fix(proxy): stop labelling management an..." | Re-trigger Greptile
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Contributor
Author
Contributor
Author
|
bugbot run |
Contributor
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 0c38a25. Configure here.
yucheng-berri
approved these changes
Sep 3, 2026
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.
TLDR
Problem this solves:
type: auth_error/team/infosaysauth_errorfor a team that does not exist/user/updatesaysauth_errorfor a badbudget_durationauth_erroris not an OpenAI error type at alltyperetry the key, not the fieldHow it solves it:
ProxyExceptions pass through with their own typeauth_error, untouchedUser Flow
Before: a platform admin scripting the management API with a valid admin key gets every rejection labelled an auth failure, so the client re-authenticates in a loop instead of fixing the request
GET https://litellm-domain/team/info?team_id=no-such-team-6842withAuthorization: Bearer <admin key>404with{"error":{"message":"Team not found, passed team id: no-such-team-6842.","type":"auth_error","param":null,"code":"404"}}error.type, readsauth_error, refreshes the admin key, and retries the same bad team id, looping until it gives upPOST https://litellm-domain/user/updatewith{"user_id":"u6842repro","budget_duration":"not-a-duration"}and get400with"type":"auth_error"and a message prefixedAuthentication Error,POST /key/update,POST /organization/member_add,POST /model/new,POST /model/update,POST /model/delete,GET /health/services,POST /queue/chat/completionsandPOST /config/updateGET https://litellm-domain/team/infoand get401with an auth-flavoured type as well, so nothing in the response body tells them which of the two problems they actually haveAfter: the same calls come back with the type that matches what went wrong, so the client fixes the field and only retries credentials when the credentials are the problem
GET https://litellm-domain/team/info?team_id=no-such-team-6842withAuthorization: Bearer <admin key>404with{"error":{"message":"Team not found, passed team id: no-such-team-6842.","type":"invalid_request_error","param":null,"code":"404"}}, and the message no longer carries theAuthentication Error,prefixinvalid_request_error, stops retrying the key, and surfaces the bad team id to the operatorPOST https://litellm-domain/user/updatewith the same badbudget_durationand get400with"type":"invalid_request_error"404or400or422now saysinvalid_request_error, a500saysinternal_server_error, a403sayspermission_errorGET https://litellm-domain/team/infoand still get401with"type":"token_not_found_in_db", andPOST https://litellm-domain/loginwith a wrong password still answers401with"type":"auth_error", so a real credential failure is now distinguishable from a bad team idScope
92 sites across 14 files pin
ProxyErrorTypes.auth_error. This PR changes 22 of them, in the 11 blanketexcept Exceptionblocks that guard a management or health route, and deliberately leaves the other 70.Changed, one line each, all now going through a shared
proxy_exception_forhelper:POST /user/updatePOST /key/updateGET /team/infoPOST /organization/member_addPOST /model/deletePOST /model/updatePOST /model/newGET /health/servicesPOST /queue/chat/completionsPOST /config/updateGET /get/config/callbacksLeft alone on purpose:
litellm/proxy/auth/management_endpoints/ui_sso.pyproxy_server.pylogin and onboardingmodel_management_endpoints.py403 checksutils.pyhandle_exception_on_proxyThe last row is why the diff stops where it does: the shared handler is being fixed separately, so touching it here would collide.
list_keysandkey_aliasesinkey_management_endpoints.pyare the near miss. Both wrap their body in the same blanketexcept Exception, and both still hardcodemessage="Authentication Error, " + str(e)while passing the caught exception's own status through, so a 400 there answersinternal_server_errorwith an auth-flavoured message. Neither is one of the 92auth_errorsites, so neither is in this ticket's scope, and folding them in would widen the diff and force the whole QA to run again. They are worth their own follow-up.Base branch
This branches off
litellm_openai_error_payload_spend_management(PR #39542), not offlitellm_internal_staging, and can only merge once #39521, #39536, #39540 and #39542 land ahead of it. Two reasons:litellm/proxy/common_utils/openai_error_payload.py, which the fix extends, does not exist on staging yet, and #39540 and #39542 rewrite theparam=line directly above every one of the 22type=lines this PR replaces. Branching off staging would have meant a duplicate copy of that module plus 22 guaranteed conflicts.Relevant issues
Linear ticket
Resolves LIT-6842
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*,make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more@greptileaito re-request a review after pushing changes)Delays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
Two live proxies, same config, same cases, same order, differing only in the commit they booted from.
Each ran two uvicorn workers against its own Postgres database, and every call below uses a valid
admin key.
Boot on either side:
Before ran on port 21550, After on port 31620.
Before (f9051a1)
1. Sanity: a real provider call succeeds
{"id":"chatcmpl-f62b3ddf-1008-442e-9c59-035d62f33319","model":"anthropic-haiku-4-5","object":"chat.completion","choices":[{"finish_reason":"stop","index":0,"message":{"content":"lit6842","role":"assistant"}}],"usage":{"completion_tokens":6,"prompt_tokens":15,"total_tokens":21}}2.
GET /team/info, team that does not exist{"error":{"message":"{'message': 'Team not found, passed team id: no-such-team-6842.'}","type":"auth_error","param":null,"code":"404"}}3.
POST /user/update, unparseablebudget_duration{"error":{"message":"{'error': \"Invalid budget_duration 'not-a-duration'. Use a format like '1h', '24h', '7d', or '30d'.\"}","type":"auth_error","param":null,"code":"400"}}4.
POST /key/update, unparseablebudget_duration{"error":{"message":"{'error': \"Invalid budget_duration 'not-a-duration'. Use a format like '1h', '24h', '7d', or '30d'.\"}","type":"auth_error","param":null,"code":"400"}}5.
POST /organization/member_add, organization that does not exist{"error":{"message":"{'error': 'Organization not found for organization_id=no-such-org-6842'}","type":"auth_error","param":null,"code":"404"}}6.
POST /model/delete, model id that does not exist{"error":{"message":"{'error': 'Model with id=no-such-model-6842 not found in db'}","type":"auth_error","param":null,"code":"400"}}7.
POST /model/update, model id that does not exist{"error":{"message":"Authentication Error, model not found","type":"auth_error","param":null,"code":"400"}}8.
POST /model/new, samemodel_info.idtwice, so the db write fails{"error":{"message":"{'error': 'Failed to add model to db. Check your server logs for more details.'}","type":"auth_error","param":null,"code":"500"}}9.
GET /health/services, service name that is not in the list{"error":{"message":"{'error': \"Service must be in list. Service=not-a-service not in typing.Union[typing.Literal['slack_budget_alerts', 'langfuse', ...], str]\"}","type":"auth_error","param":null,"code":"400"}}10.
POST /queue/chat/completions, nopriority{"error":{"message":"Authentication Error, Router.schedule_acompletion() missing 1 required positional argument: 'priority'","type":"auth_error","param":null,"code":"400"}}11.
POST /queue/chat/completions, validpriority, model that does not exist{"error":{"message":"Authentication Error, litellm.BadRequestError: You passed in model=no-such-model-6842. There are no healthy deployments for this model. Received Model Group=no-such-model-6842\nAvailable Model Group Fallbacks=None","type":"auth_error","param":null,"code":"400"}}12.
POST /config/update,success_callbackthat is not a list of strings{"error":{"message":"Authentication Error, unhashable type: 'dict'","type":"auth_error","param":null,"code":"400"}}13.
GET /get/config/callbacks, after a scalarsuccess_callbackis stored{"error":{"message":"Authentication Error, 'int' object is not iterable","type":"auth_error","param":null,"code":"400"}}14. Auth control:
GET /team/infowith a key that does not exist{"error":{"message":"Authentication Error, Invalid proxy server token passed. Received API Key = sk-...6842, Key Hash (Token) =249e3414...","type":"token_not_found_in_db","param":"key","code":"401"}}15. Auth control:
POST /loginwith the wrong password{"error":{"message":"Invalid credentials used to access UI.\nCheck 'UI_USERNAME', 'UI_PASSWORD' in .env file","type":"auth_error","param":"invalid_credentials","code":"401"}}After (0c38a25)
1. Sanity: a real provider call succeeds
{"id":"chatcmpl-b9411883-0ab9-4228-a511-597574650218","model":"anthropic-haiku-4-5","object":"chat.completion","choices":[{"finish_reason":"stop","index":0,"message":{"content":"lit6842","role":"assistant"}}],"usage":{"completion_tokens":6,"prompt_tokens":15,"total_tokens":21}}2.
GET /team/info, team that does not exist{"error":{"message":"{'message': 'Team not found, passed team id: no-such-team-6842.'}","type":"invalid_request_error","param":null,"code":"404"}}3.
POST /user/update, unparseablebudget_duration{"error":{"message":"{'error': \"Invalid budget_duration 'not-a-duration'. Use a format like '1h', '24h', '7d', or '30d'.\"}","type":"invalid_request_error","param":null,"code":"400"}}4.
POST /key/update, unparseablebudget_duration{"error":{"message":"{'error': \"Invalid budget_duration 'not-a-duration'. Use a format like '1h', '24h', '7d', or '30d'.\"}","type":"invalid_request_error","param":null,"code":"400"}}5.
POST /organization/member_add, organization that does not exist{"error":{"message":"{'error': 'Organization not found for organization_id=no-such-org-6842'}","type":"invalid_request_error","param":null,"code":"404"}}6.
POST /model/delete, model id that does not exist{"error":{"message":"{'error': 'Model with id=no-such-model-6842 not found in db'}","type":"invalid_request_error","param":null,"code":"400"}}7.
POST /model/update, model id that does not exist{"error":{"message":"model not found","type":"invalid_request_error","param":null,"code":"400"}}8.
POST /model/new, samemodel_info.idtwice, so the db write fails{"error":{"message":"{'error': 'Failed to add model to db. Check your server logs for more details.'}","type":"internal_server_error","param":null,"code":"500"}}9.
GET /health/services, service name that is not in the list{"error":{"message":"{'error': \"Service must be in list. Service=not-a-service not in typing.Union[typing.Literal['slack_budget_alerts', 'langfuse', ...], str]\"}","type":"invalid_request_error","param":null,"code":"400"}}10.
POST /queue/chat/completions, nopriority{"error":{"message":"Router.schedule_acompletion() missing 1 required positional argument: 'priority'","type":"invalid_request_error","param":null,"code":"400"}}11.
POST /queue/chat/completions, validpriority, model that does not exist{"error":{"message":"litellm.BadRequestError: You passed in model=no-such-model-6842. There are no healthy deployments for this model. Received Model Group=no-such-model-6842\nAvailable Model Group Fallbacks=None","type":"invalid_request_error","param":null,"code":"400"}}12.
POST /config/update,success_callbackthat is not a list of strings{"error":{"message":"unhashable type: 'dict'","type":"invalid_request_error","param":null,"code":"400"}}13.
GET /get/config/callbacks, after a scalarsuccess_callbackis stored{"error":{"message":"'int' object is not iterable","type":"invalid_request_error","param":null,"code":"400"}}14. Auth control:
GET /team/infowith a key that does not exist{"error":{"message":"Authentication Error, Invalid proxy server token passed. Received API Key = sk-...6842, Key Hash (Token) =249e3414...","type":"token_not_found_in_db","param":"key","code":"401"}}15. Auth control:
POST /loginwith the wrong password{"error":{"message":"Invalid credentials used to access UI.\nCheck 'UI_USERNAME', 'UI_PASSWORD' in .env file","type":"auth_error","param":"invalid_credentials","code":"401"}}Notes from the two legs:
Authentication Error,message prefix is gone where it lied/model/newduplicate-id answers 500, arguably should be a 409/queue/chat/completionsstill leaks a raw PythonTypeErrortext/config/updatestores a scalarsuccess_callbackthat breaks readsType
🐛 Bug Fix
Caveats (if any)
Medium
type == "auth_error"see these 11 routes changelitellm/proxy/client/never branch on either typeLow
invalid_request_error, not the docs'not_found_errorauth_error, and OpenAI's contract has nonot_found_errortypeis reported verbatim at the route's default statuslitellm.RateLimitErrordoes that today, and it beatsauth_errorPOST /queue/chat/completionsstill fails on a missingpriority, pre-existing and untouchedPOST /config/updatestores a scalarsuccess_callbackthat then breaks reads, pre-existinglitellm_router_testingfails ontest_router_timeout, which fails on staging toolocal_testing_part1fails on an embedding-timeout test that fails on the base commite2e_ui_testingfails on the Playwright step on the base commit as wellosv-scanflags a gitpython advisory that PR fix(deps): raise the gitpython floor to 3.1.59 for four new advisories #39553 ownsFinal Attestation