Skip to content

fix(proxy/auth): honor user_api_key_cache_ttl for management-object cache writes - #31504

Merged
yassin-berriai merged 1 commit into
litellm_internal_stagingfrom
litellm_honor_user_api_key_cache_ttl
Jun 27, 2026
Merged

fix(proxy/auth): honor user_api_key_cache_ttl for management-object cache writes#31504
yassin-berriai merged 1 commit into
litellm_internal_stagingfrom
litellm_honor_user_api_key_cache_ttl

Conversation

@yassin-berriai

Copy link
Copy Markdown
Contributor

Relevant issues

Resolves LIT-3338

Linear ticket

LIT-3338

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 requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

Screenshots / Proof of Fix

Root cause

general_settings.user_api_key_cache_ttl is read at startup and propagated onto user_api_key_cache.default_in_memory_ttl via update_cache_ttl. But DualCache.async_set_cache only fills in default_in_memory_ttl when no ttl kwarg is present, and every management-object writer passed ttl=DEFAULT_MANAGEMENT_OBJECT_IN_MEMORY_CACHE_TTL (60s) explicitly, so the configured value was always overridden. The reported path get_key_object -> _cache_key_object -> _cache_management_object is one of those writers; keys, teams, users, budgets, object permissions, vector stores, JWT user syncs and the MCP caches all behaved the same way. On top of that, the spend-tracking writeback in update_cache re-cached the same key/user/team objects with a hardcoded ttl=60 on every priced request, so even an active key got its entry reset to 60s right after auth had cached it

Before / after on the exact production functions

The metric here is the in-memory cache entry TTL, which has no HTTP surface, so the proof drives the real production functions (auth_checks._cache_key_object and proxy_server.update_cache) against a user_api_key_cache configured exactly the way load_config configures it for user_api_key_cache_ttl: 300, and reads back the in-memory ttl_dict. BEFORE is a clean checkout of the base commit; AFTER is this branch

############ BEFORE (clean base commit, no fix) ############
general_settings.user_api_key_cache_ttl : 300
cache.default_in_memory_ttl             : 300.0
_cache_key_object  -> key object TTL    : 60s
update_cache spend -> key object TTL    : 60s
VERDICT: capped at 60s

############ AFTER (fixed branch) ############
general_settings.user_api_key_cache_ttl : 300
cache.default_in_memory_ttl             : 300.0
_cache_key_object  -> key object TTL    : 300s
update_cache spend -> key object TTL    : 300s
VERDICT: 300s honored

Live proxy, real key auth, real LLM call

Booted the proxy on this branch with general_settings.user_api_key_cache_ttl: 300 against a real Postgres, created a virtual key, and made a real gpt-4o-mini call so the full get_key_object -> _cache_key_object auth-cache path runs end to end

$ curl -s -X POST $BASE/health/readiness
{"status": "healthy", "db": "connected"}

$ curl -s -X POST $BASE/key/generate -H "Authorization: Bearer $MASTER_KEY" \
    -H "Content-Type: application/json" -d '{"models":["gpt-4o-mini"],"key_alias":"lit3338-demo"}'
# -> sk-OhWtgY4L0...

$ curl -s -X POST $BASE/v1/chat/completions -H "Authorization: Bearer $VKEY" \
    -H "Content-Type: application/json" \
    -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"Reply with exactly: LIT-3338 ok"}],"max_tokens":16}'
content: LIT-3338 ok
model: gpt-4o-mini
usage: {'completion_tokens': 6, 'prompt_tokens': 17, 'total_tokens': 23, ...}

The proxy log confirms the setting loaded into general_settings ('user_api_key_cache_ttl': 300)

Type

🐛 Bug Fix

Changes

Adds get_management_object_ttl(cache) in litellm/proxy/common_utils/user_api_key_cache.py, which returns the cache's configured default_in_memory_ttl and falls back to DEFAULT_MANAGEMENT_OBJECT_IN_MEMORY_CACHE_TTL only when no default is set, then routes every management-object writer through it instead of the hardcoded constant. The helper takes a DualCache because most of those call sites are typed UserApiKeyCache but exercised in tests with a bare DualCache, so a method on the subclass would not resolve there. Call sites updated across auth_checks.py, handle_jwt.py, mcp_server_manager.py, user_api_key_auth_mcp.py and proxy_server.py (_is_mcp_access_group_cached); the MCP access-group negative-cache branch keeps its short dedicated TTL

The spend-update writeback in update_cache is also brought under the setting. This is what makes the fix observable for keys that receive traffic; it does mean in-memory spend values live for the configured TTL before a DB re-sync, which is exactly what user_api_key_cache_ttl is meant to control

Tests: test_user_api_key_cache.py covers the resolver (configured default wins, fallback when unset, and that it resolves on a plain DualCache); test_auth_checks.py covers _cache_management_object honoring the configured TTL; test_proxy_server.py covers the update_cache pipeline writeback honoring it. Each core assertion was confirmed to fail when the resolver or the pipeline ttl is reverted to the old constant

@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.

@yassin-berriai

Copy link
Copy Markdown
Contributor Author

@greptileai

@codecov

codecov Bot commented Jun 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@greptile-apps

greptile-apps Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR makes management-object cache writes honor the configured user_api_key_cache_ttl. The main changes are:

  • Adds get_management_object_ttl(cache) with a fallback to the existing management-object default
  • Replaces hardcoded 60-second TTLs across auth, JWT, MCP, and spend-update cache writes
  • Keeps the short MCP access-group negative-cache TTL unchanged
  • Adds tests for the TTL resolver, central auth cache write, and spend writeback pipeline

Confidence Score: 5/5

The change is narrowly scoped to management-object cache TTL selection and includes focused test coverage for the helper, auth cache writes, and spend-update writeback.

No blocking code issues were identified in the reviewed changes, and the tests described cover the main paths affected by the TTL behavior change.

T-Rex T-Rex Logs

What T-Rex did

  • Compared the auth-management TTL between the base and head commits by running TTL tests and inspecting the before- and after-state artifacts; the before-state showed a configured default TTL of 300 with a cached TTL of 60.0, while the after-state showed a default TTL of 300, a cached TTL of 300.0, the resolver returning 300, and an unset fallback of 60.
  • Verified the spend TTL writeback path by running the update-cache-spend TTL tests; both runs exited with code 0 and the spend value updated from 1.0 to 3.5, confirming the ttl kwarg changed from 60 to 300.
  • Validated the in-memory TTL configuration by comparing before- and after-state artifacts; default_in_memory_ttl remained 300, while JWT and MCP per-key TTLs increased from 60.0s to 300.0s.

View all artifacts

T-Rex Ran code and verified through T-Rex

Reviews (4): Last reviewed commit: "fix(proxy/auth): honor user_api_key_cach..." | Re-trigger Greptile

@greptile-apps

greptile-apps Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR updates management-object cache writes to honor the configured user API key cache TTL. The main changes are:

  • Adds get_management_object_ttl(cache) with a 60s fallback when no default TTL is configured
  • Replaces hardcoded management-object TTLs across auth, JWT sync, MCP, and spend writeback cache paths
  • Keeps the MCP access-group negative-cache TTL separate from positive management-object caching
  • Adds tests for the TTL helper, auth cache writer, and update_cache pipeline behavior

Confidence Score: 4/5

The cache TTL changes are narrowly scoped and covered by targeted tests across the helper, auth cache writer, and spend writeback paths.

The implementation consistently routes management-object cache writes through the configured TTL resolver while preserving the dedicated MCP negative-cache behavior.

T-Rex T-Rex Logs

What T-Rex did

  • Ran baseline tests to observe TTL and cache timings with the base configuration.
  • Applied the head configuration, re-ran the tests, and verified updated TTL and cache timings.
  • Compared management-object TTLs between base and head; baseline showed positive writes around 60 seconds and MCP negative around 10 seconds, while head showed positive writes around 300 seconds and MCP negative around 10 seconds.
  • Compared route cleanup behavior between base and head; baseline used a pop-based cleanup and after reload only new routes remained, while head cleanup attempted removal by endpoint key but left stale old routes visible after reload.

View all artifacts

T-Rex Ran code and verified through T-Rex

Comments Outside Diff (1)

  1. General comment

    P1 Stale pass-through routes remain registered after reload

    • Bug
      • When a previously configured dynamic pass-through endpoint is removed during initialize_pass_through_endpoints, head leaves the old exact and subpath registry entries active. In the executed scenario, after reloading from /old to /new, base reported old_GET_info_present: false and only new-id:exact:/new:GET in _registered_pass_through_routes; head reported old_GET_info_present: true and retained both old-id:exact:/old:GET and old-id:subpath:/old:GET. This breaks the reload cleanup contract by making removed pass-through routes still appear registered/available.
    • Cause
      • The changed cleanup loop passes a full route key such as old-id:exact:/old:GET into InitPassThroughEndpointHelpers.remove_endpoint_routes(), but that helper removes entries by comparing value['endpoint_id'] == endpoint_id. Since stored endpoint ids are only old-id, no stale entries match and nothing is deleted.
    • Fix
      • Restore exact route-key removal for registered_pass_through_endpoints entries, e.g. _registered_pass_through_routes.pop(endpoint_key, None), or parse the endpoint id from the route key before calling remove_endpoint_routes only when the intended behavior is to remove all routes for that endpoint id. Add a reload/removal regression test covering exact and include_subpath entries.

    T-Rex Ran code and verified through T-Rex

Reviews (1): Last reviewed commit: "fix(proxy/auth): honor user_api_key_cach..." | Re-trigger Greptile

…ache writes

general_settings.user_api_key_cache_ttl was ignored for every management-object
write into user_api_key_cache. The configured value is propagated to the cache's
default_in_memory_ttl at startup, but DualCache only applies that default when no
explicit ttl kwarg is passed, and every management-object writer passed
ttl=DEFAULT_MANAGEMENT_OBJECT_IN_MEMORY_CACHE_TTL (60s), which always won. So keys,
teams, users, budgets, object permissions, vector stores, JWT user syncs and MCP
caches all expired after 60s regardless of the setting.

Adds get_management_object_ttl(cache) in user_api_key_cache.py, which returns the
configured default_in_memory_ttl and falls back to the 60s constant only when no
default is set, and routes every management-object writer through it. The helper
takes a DualCache so it works at the many call sites that are typed UserApiKeyCache
but exercised with a bare DualCache.

Also covers the spend-update writeback in update_cache (async_set_cache_pipeline),
which hardcoded ttl=60 on the same key/user/team objects and reset an active key's
cache entry back to 60s on every priced request, so the configured TTL was never
observed for keys receiving traffic.

Resolves LIT-3338
@yassin-berriai
yassin-berriai force-pushed the litellm_honor_user_api_key_cache_ttl branch from b0cb7f7 to 284fdcb Compare June 27, 2026 08:50
@yassin-berriai

Copy link
Copy Markdown
Contributor Author

@greptileai please re-review the current HEAD.

Two notes:

The P1 about stale pass-through routes after reload (_registered_pass_through_routes / remove_endpoint_routes in initialize_pass_through_endpoints) is outside this diff. This branch is a single commit off litellm_internal_staging and touches only cache-TTL resolution; it contains no pass-through routing files or symbols (git show --name-only lists 9 cache/test files, and remove_endpoint_routes / _registered_pass_through_routes / initialize_pass_through_endpoints do not appear in the diff). That behavior comparison is not attributable to this change.

The HEAD was amended after the first review: get_management_object_ttl now reads default_in_memory_ttl via a defensive getattr(cache, ..., None). Tests pass the cache as both DualCache() and AsyncMock(spec=DualCache), and a spec'd mock lacks instance attributes set in __init__, so the direct attribute read raised and a caught exception surfaced as a None budget in the auth-and-jwt shard. The getattr falls through to the management default for those doubles and keeps the configured TTL for real caches.

@yassin-berriai

Copy link
Copy Markdown
Contributor Author

@greptileai the sole remaining finding is filed under Comments Outside Diff and concerns pass-through route cleanup (initialize_pass_through_endpoints / remove_endpoint_routes / _registered_pass_through_routes). None of those files or symbols appear in this PR; the branch is one commit off litellm_internal_staging and changes only cache-TTL resolution. The T-Rex base comparison is picking up a pre-existing staging difference, not this diff. Please re-evaluate scoped to the changed files.

@yassin-berriai
yassin-berriai enabled auto-merge (squash) June 27, 2026 17:24
@yassin-berriai
yassin-berriai merged commit 1883f97 into litellm_internal_staging Jun 27, 2026
123 checks passed
@yassin-berriai
yassin-berriai deleted the litellm_honor_user_api_key_cache_ttl branch June 27, 2026 19:19
fzowl pushed a commit to fzowl/litellm that referenced this pull request Jun 30, 2026
…ache writes (BerriAI#31504)

general_settings.user_api_key_cache_ttl was ignored for every management-object
write into user_api_key_cache. The configured value is propagated to the cache's
default_in_memory_ttl at startup, but DualCache only applies that default when no
explicit ttl kwarg is passed, and every management-object writer passed
ttl=DEFAULT_MANAGEMENT_OBJECT_IN_MEMORY_CACHE_TTL (60s), which always won. So keys,
teams, users, budgets, object permissions, vector stores, JWT user syncs and MCP
caches all expired after 60s regardless of the setting.

Adds get_management_object_ttl(cache) in user_api_key_cache.py, which returns the
configured default_in_memory_ttl and falls back to the 60s constant only when no
default is set, and routes every management-object writer through it. The helper
takes a DualCache so it works at the many call sites that are typed UserApiKeyCache
but exercised with a bare DualCache.

Also covers the spend-update writeback in update_cache (async_set_cache_pipeline),
which hardcoded ttl=60 on the same key/user/team objects and reset an active key's
cache entry back to 60s on every priced request, so the configured TTL was never
observed for keys receiving traffic.

Resolves LIT-3338
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