Skip to content

refactor(proxy): ban raw datetime.fromisoformat via parse_utc_datetime helper - #34128

Open
ryan-crabbe-berri wants to merge 3 commits into
litellm_internal_stagingfrom
litellm_ban_unnormalized_datetime_compare
Open

refactor(proxy): ban raw datetime.fromisoformat via parse_utc_datetime helper#34128
ryan-crabbe-berri wants to merge 3 commits into
litellm_internal_stagingfrom
litellm_ban_unnormalized_datetime_compare

Conversation

@ryan-crabbe-berri

@ryan-crabbe-berri ryan-crabbe-berri commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

Follow-up to #33840, which fixed one aware-vs-naive datetime crash in _get_temp_budget_increase. This PR eliminates the class

Linear ticket

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Screenshots / Proof of Fix

All runs against a live proxy on localhost:4000 at commit 509ef61 hitting the real Groq API (groq/openai/gpt-oss-120b registered via /model/new)

The documented tz-aware format from #33840 keeps working through the new helper. /key/update with "temp_budget_expiry": "2026-08-01T00:00:00Z" stores aware metadata and the key serves:

== stored metadata ==
{'temp_budget_expiry': '2026-08-01T00:00:00+00:00', 'temp_budget_increase': 100.0}
== completion with tz-aware temp budget key ==
{"id":"chatcmpl-...","choices":[{"finish_reason":"stop",...}],"usage":{"completion_tokens":42,...}}
HTTP 200

The legacy naive format also still serves:

curl -s $BASE/key/update ... -d "{\"key\":\"$KEY2\",\"temp_budget_increase\":100,\"temp_budget_expiry\":\"2026-08-01T00:00:00\"}"
naive-expiry completion: HTTP 200

Key expiry enforcement (the three collapsed guard sites in user_api_key_auth.py) still rejects expired keys, now with unambiguous aware timestamps in the error:

KEY=$(curl -s $BASE/key/generate ... -d '{"models":["groq-gpt-oss-120b"],"duration":"5s"}' | ...)
fresh short-lived key: HTTP 200
# 70s later
{"error":{"message":"Authentication Error - Expired Key. Key Expiry time 2026-07-21 18:34:48.126000+00:00 and current time 2026-07-21 18:35:53.979481+00:00","type":"expired_key","param":"sk-...b440","code":"401"}}
after expiry: HTTP 401

The MCP credential-status bug fix is proven by a red-to-green regression test (reverting the one-line fix makes test_get_mcp_oauth_user_credential_status_naive_past_expiry_is_expired fail); a live repro needs a connected OAuth MCP server, which the unit test simulates at the exact seam that swallowed the TypeError

Type

🐛 Bug Fix
🧹 Refactoring

Changes

datetime.fromisoformat on strings from API input, DB metadata, or serialized state returns aware datetimes when the string carries Z/+00:00 and naive ones otherwise. Comparing the two raises TypeError, and the proxy had 27 call sites each hand-rolling (or forgetting) the same tzinfo guard. #33840 fixed one such crash; grepping for the pattern found another live one plus 15 sites that only stayed safe by carrying duplicated guard code

This PR adds parse_utc_datetime in litellm/litellm_core_utils/datetime_utils.py as the single parse entrypoint: accepts str | datetime, handles the Z suffix on Python 3.10 (stdlib only accepts it from 3.11), and assumes naive values are UTC, so results always compare safely against datetime.now(timezone.utc). All 27 fromisoformat sites under litellm/proxy/ now go through it, and the per-site guards are deleted

It also fixes a real bug found by the sweep: get_mcp_oauth_user_credential_status compared an unnormalized parse against aware now inside a bare except Exception, so a tz-naive expires_at raised TypeError, the except swallowed it, and the credential never reported as expired. The except is narrowed to ValueError and the parse normalized

Two semgrep rules in .semgrep/rules/python/reliability/naive-datetime.yml make the pattern unrepresentable: raw datetime.fromisoformat is banned under litellm/proxy/ (ERROR), and a repo-wide taint rule flags comparing or subtracting an unnormalized fromisoformat result. Both run in the existing Semgrep CI job and are at zero findings on this branch

Behavior notes: the model-cost-map and anthropic-beta-headers reload timers in proxy_server.py moved from naive utcnow() to aware UTC, so last_run/next_run in their status endpoints now carry a +00:00 suffix (same instants). In usage_endpoints.py, a start_date with a time but no offset now filters as UTC instead of naive. Partition-drop cutoffs in spend_logs_partition_manager.py compare aware-vs-aware; production callers already passed aware UTC

Tests: tests/test_litellm/litellm_core_utils/test_datetime_utils.py covers the helper contract (naive-as-UTC, Z suffix, offset preservation, datetime passthrough, invalid input). The MCP regression test proves the swallowed-TypeError bug cannot return. Existing partition-manager tests updated to the aware convention. 480 tests across the touched suites pass

Follow-up from review: parse_utc_datetime now raises TypeError for input that is neither str nor datetime, mirroring datetime.fromisoformat's own contract. Without this, a truthy non-string expires_at in stored credential JSON leaked AttributeError past the except (ValueError, TypeError) handlers the refactored sites already carry, turning the MCP credential-status endpoint into a 500. That endpoint also omits non-string expires_at/connected_at from its response instead of failing response validation, with a test proving a malformed stored expiry degrades gracefully

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

…e helper

Extract parse_utc_datetime into litellm_core_utils/datetime_utils.py as the
single ISO-8601 parse entrypoint for the proxy: it accepts str or datetime,
handles the Z suffix on Python 3.10, and assumes naive values are UTC so
results always compare safely against datetime.now(timezone.utc).

Convert all 27 raw fromisoformat sites under litellm/proxy/ to the helper,
collapsing the hand-rolled tzinfo guards they each carried. This also fixes a
real bug in get_mcp_oauth_user_credential_status: a tz-naive expires_at raised
TypeError against the aware now(), the bare except swallowed it, and the
credential never read as expired.

Two semgrep rules enforce the pattern in CI: raw fromisoformat is banned under
litellm/proxy/, and a repo-wide taint rule flags comparing or subtracting an
unnormalized fromisoformat result.
@greptile-apps

greptile-apps Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR centralizes UTC-aware datetime parsing across the proxy. The main changes are:

  • Adds parse_utc_datetime with support for strings, datetime values, naive timestamps, offsets, and the Z suffix
  • Replaces raw proxy-side datetime.fromisoformat calls and removes duplicated timezone guards
  • Adds Semgrep rules to prevent unsafe datetime parsing and comparisons
  • Fixes MCP credential expiry checks and safely handles malformed timestamp fields
  • Adds focused tests for the parser, MCP credential status, partition handling, and reload timestamps

Confidence Score: 5/5

This looks safe to merge.

  • The malformed credential value is now caught without causing response validation to fail.
  • The shared parser has a clear runtime contract and focused tests.
  • No blocking issues were found in the updated code.

Important Files Changed

Filename Overview
litellm/litellm_core_utils/datetime_utils.py Adds the shared parser, UTC normalization, and predictable errors for unsupported input types
litellm/proxy/management_endpoints/mcp_management_endpoints.py Normalizes credential expiry values and prevents malformed timestamp types from failing response validation
.semgrep/rules/python/reliability/naive-datetime.yml Adds checks that ban raw proxy datetime parsing and flag comparisons using unnormalized parsed timestamps
tests/test_litellm/litellm_core_utils/test_datetime_utils.py Covers naive, UTC, offset, datetime, invalid-string, and unsupported-type inputs
tests/test_litellm/proxy/management_endpoints/test_mcp_management_endpoints.py Adds focused coverage for naive expired credentials and malformed non-string expiry values

Reviews (2): Last reviewed commit: "fix(datetime_utils): raise TypeError for..." | Re-trigger Greptile

Comment thread litellm/proxy/management_endpoints/mcp_management_endpoints.py Outdated
@codecov

codecov Bot commented Jul 21, 2026

Copy link
Copy Markdown

@codspeed-hq

codspeed-hq Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_ban_unnormalized_datetime_compare (e315984) with litellm_internal_staging (212a921)1

Open in CodSpeed

Footnotes

  1. No successful run was found on litellm_internal_staging (48fdaaa) during the generation of this report, so 212a921 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

Greptile flagged that narrowing get_mcp_oauth_user_credential_status from a
bare except to except ValueError left a truthy non-string expires_at (possible
in raw DB JSON) raising AttributeError through the endpoint as a 500. The same
gap applied to every refactored site that catches (ValueError, TypeError):
datetime.fromisoformat raised TypeError for non-strings, which those handlers
were written against, but the helper's passthrough branch leaked
AttributeError instead.

parse_utc_datetime now validates its input and raises TypeError for anything
that is not str or datetime, restoring the stdlib error contract at every call
site. The MCP status endpoint catches it and also omits non-string
expires_at/connected_at values from the response instead of failing response
validation.
@ryan-crabbe-berri

Copy link
Copy Markdown
Contributor Author

@greptileai re review

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.

1 participant