Skip to content

fix(proxy): populate Exception.args so str(ProxyException) returns message (LIT-3094)#29015

Merged
krrish-berri-2 merged 4 commits into
BerriAI:litellm_oss_agent_shin_daily_branchfrom
oss-agent-shin:feat/lit-3094-proxyexception-args
May 27, 2026
Merged

fix(proxy): populate Exception.args so str(ProxyException) returns message (LIT-3094)#29015
krrish-berri-2 merged 4 commits into
BerriAI:litellm_oss_agent_shin_daily_branchfrom
oss-agent-shin:feat/lit-3094-proxyexception-args

Conversation

@oss-agent-shin

@oss-agent-shin oss-agent-shin commented May 27, 2026

Copy link
Copy Markdown
Contributor

Summary

ProxyException.__init__ stores the human-readable message on self.message but never calls super().__init__(self.message), so Exception.args stays empty and str(exc) returns "".

Logging integrations such as StandardLoggingPayloadSetup.get_error_information (consumed by proxy_track_cost_callback, Datadog, OpenTelemetry, S3, etc.) call str(original_exception) to populate StandardLoggingPayloadErrorInformation.error_message. The empty string then ends up in logs/telemetry for every ProxyException-based failure — most visibly for 401 auth errors raised before a provider is selected (llm_provider blank is expected in that case).

Fix

In ProxyException.__init__, immediately after self.message = str(message), call super().__init__(self.message). Five extra lines (one functional + four comment lines). The existing code remap logic ("No healthy deployment available" → 429, tag-routing → 401) is preserved because it runs later on self.code, not on args.

Evidence

Reproduced on clean main (commit 06f6cfc) and re-run on this branch.

Before — clean main, real ProxyException flowed through the real logging chain:

=== BEFORE FIX: ProxyException on clean main (commit 06f6cfc) ===
exc.message  = "key not allowed to access model. This key can only access models=['gpt-3.5-turbo']. Tried to access gpt-4"
str(exc)     = ''
exc.args     = ()

StandardLoggingPayloadSetup.get_error_information() returned:
  error_message = ''
  error_class   = 'ProxyException'
  error_code    = '401'
  llm_provider  = ''

>>> BUG: error_message is empty even though exc.message is populated <<<

After — patched branch, same call:

=== AFTER FIX: ProxyException on patched branch (feat/lit-3094-proxyexception-args) ===
exc.message  = "key not allowed to access model. This key can only access models=['gpt-3.5-turbo']. Tried to access gpt-4"
str(exc)     = "key not allowed to access model. This key can only access models=['gpt-3.5-turbo']. Tried to access gpt-4"
exc.args     = ("key not allowed to access model. This key can only access models=['gpt-3.5-turbo']. Tried to access gpt-4",)

StandardLoggingPayloadSetup.get_error_information() returned:
  error_message = "key not allowed to access model. This key can only access models=['gpt-3.5-turbo']. Tried to access gpt-4"
  error_class   = 'ProxyException'
  error_code    = '401'
  llm_provider  = ''

After — four real ProxyException shapes through the full proxy logging chain, all now propagate to payload.error_message:

--- 401 auth - restricted key ---
  payload.error_message     = "key not allowed to access model. This key can only access models=['gpt-3.5-turbo']. Tried to access gpt-4"
  payload.error_code        = '401'

--- 401 auth - invalid token ---
  payload.error_message     = 'Authentication Error, Invalid proxy server token passed.'
  payload.error_code        = '401'

--- 429 no healthy deployment (routing remap) ---
  payload.error_message     = 'No healthy deployment available, passed model=foo'
  payload.error_code        = '429'   # routing remap preserved

--- 400 invalid request ---
  payload.error_message     = 'missing required field: model'
  payload.error_code        = '400'

Regression tests added in tests/test_litellm/proxy/test_proxy_types.py (all pass on the patched branch):

tests/test_litellm/proxy/test_proxy_types.py::test_audit_log_masking PASSED
tests/test_litellm/proxy/test_proxy_types.py::test_internal_jobs_user_has_proxy_admin_role PASSED
tests/test_litellm/proxy/test_proxy_types.py::test_proxy_exception_non_string_message_coerced PASSED
tests/test_litellm/proxy/test_proxy_types.py::test_proxy_exception_populates_standard_logging_error_message PASSED
tests/test_litellm/proxy/test_proxy_types.py::test_proxy_exception_routing_code_override_still_works PASSED
tests/test_litellm/proxy/test_proxy_types.py::test_proxy_exception_str_returns_message PASSED
tests/test_litellm/proxy/test_proxy_types.py::test_proxy_exception_to_dict_unchanged PASSED
======================== 7 passed, 2 warnings in 9 s =========================

Five new regression tests cover:

  1. str(ProxyException) == message (the immediate bug)
  2. Full StandardLoggingPayloadSetup.get_error_information(...) chain returns the populated message
  3. to_dict() shape unchanged
  4. "No healthy deployment available" → 429 code remap still works
  5. Non-string message is still coerced

Notes for reviewers

  • Existing # NOTE: DO NOT MODIFY THIS comment in ProxyException is about the OpenAI-shape mapping (code, type, param, openai_code, to_dict() keys). This change does not touch any of that — it only populates Exception.args so str(self) works.
  • Pushed via the GitHub Contents API (current bot token lacks repo/workflow scopes for git push), one commit per file plus one cleanup commit. Net diff is the two-file change above; intermediate commit messages are noisier than the final state.
  • Tooling friction (sandbox volatility) made the cleanup commit (959288c) necessary; the cumulative effect against the base branch is +5 / 0 in _types.py and +74 / 0 in the test file.

Fixes LIT-3094.

Verification (ship-pr)

  • change-type: litellm/proxy/_types.py (exception class, backend logging surface) + tests/test_litellm/proxy/test_proxy_types.py (tests). Required evidence: chained call output through the actual logging code path. PASS — see Evidence section.
  • evidence present: PASS (3 fenced blocks: clean-main repro, patched-branch verification, four real ProxyException shapes through StandardLoggingPayloadSetup.get_error_information chain).
  • image sanity: N/A (no images — terminal/Python captures inlined).
  • image content matches caption: N/A.
  • backend evidence from running system: PASS — outputs came from real Python imports of both the unpatched-main branch and the patched branch, exercising the same code path StandardLoggingPayloadSetup.get_error_information that proxy_track_cost_callback, Datadog, OpenTelemetry, and S3 callbacks call inside the running proxy.

…ssage

Adds super().__init__(self.message) to ProxyException.__init__ so that
str(exc) returns the stored message instead of empty string. Fixes LIT-3094.
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@greptile-apps

greptile-apps Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes ProxyException never calling super().__init__(), which left Exception.args empty and caused str(exc) to return "". As a result, all logging integrations that call str(original_exception) (Datadog, OpenTelemetry, S3, etc.) were recording a blank error_message for every ProxyException-based failure.

  • Adds a single super().__init__(self.message) call in ProxyException.__init__, placed after self.message = str(message) and before any other attribute assignments, which is the correct location.
  • Adds five regression tests covering the str(exc) fix, the full StandardLoggingPayloadSetup.get_error_information chain, to_dict() backwards compatibility, the "No healthy deployment available" → 429 code remap, and non-string message coercion.

Confidence Score: 5/5

Safe to merge — a minimal, well-tested one-line fix with no side effects on existing behaviour.

The change is a single super().__init__() call that fixes a long-standing gap. It is placed correctly in the constructor, does not alter any existing attribute assignments or the OpenAI-shape mapping, and is covered by five targeted regression tests that all pass.

No files require special attention.

Important Files Changed

Filename Overview
litellm/proxy/_types.py Adds super().__init__(self.message) in ProxyException.__init__ so Exception.args is populated and str(exc) returns the human-readable message instead of an empty string.
tests/test_litellm/proxy/test_proxy_types.py Adds five focused regression tests covering str(exc), the full StandardLoggingPayloadSetup.get_error_information chain, to_dict() shape stability, the 429 routing-code remap, and non-string message coercion. All tests are pure unit tests with no real network calls.

Reviews (1): Last reviewed commit: "fix(proxy): clean up unintended drift; k..." | Re-trigger Greptile

@codecov

codecov Bot commented May 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@krrish-berri-2 krrish-berri-2 merged commit 1fe911d into BerriAI:litellm_oss_agent_shin_daily_branch May 27, 2026
44 checks passed
yuneng-berri added a commit that referenced this pull request Jun 20, 2026
…x-0620

chore(release): backport #29015, #29444, #29447, #30480, #30573 to stable/1.88.x and cut 1.88.4
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.

3 participants