Skip to content

fix(auth): cache auth-path team object under canonical team_id key - #31418

Merged
yassin-berriai merged 1 commit into
litellm_internal_stagingfrom
litellm_lit4000_team_object_cache_key
Jun 26, 2026
Merged

fix(auth): cache auth-path team object under canonical team_id key#31418
yassin-berriai merged 1 commit into
litellm_internal_stagingfrom
litellm_lit4000_team_object_cache_key

Conversation

@yassin-berriai

@yassin-berriai yassin-berriai commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

Linear ticket

Resolves LIT-4000

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

On the auth path, the team object was cached under the raw valid_token.team_id, while get_team_object, _cache_team_object, and _update_team_cache all read and write under team_id:{id}. That raw-key write was never served back, and on a non-team (personal) key whose team_id is None the original unguarded write passed a None key straight to the cache layer; the in-memory cache tolerates None keys but Redis rejects them with a NoneType key error, so with enable_redis_auth_cache: true the team object never reached the Redis L2 and every request fell back to Postgres.

Reproduced against a live proxy with enable_redis_auth_cache: true backed by real Postgres and Redis, calling a real Anthropic model with a team-scoped key.

Setup (same for before and after):

litellm_settings:
  enable_redis_auth_cache: true
  cache: true
  cache_params:
    type: redis
    host: 127.0.0.1
    port: 6779

# create a team with a fixed id and a team-scoped key, then call the model
curl -s -X POST $PROXY/team/new -H "Authorization: Bearer $MK" -d '{"team_id":"lit4000-team","team_alias":"lit4000"}'
KEY=$(curl -s -X POST $PROXY/key/generate -H "Authorization: Bearer $MK" -d '{"team_id":"lit4000-team","models":["haiku"]}' | jq -r .key)
redis-cli FLUSHALL
curl -s -X POST $PROXY/chat/completions -H "Authorization: Bearer $KEY" \
  -d '{"model":"haiku","messages":[{"role":"user","content":"say hi in one word"}],"max_tokens":10}'
# -> {"choices":[{"message":{"content":"Hi"}}], ...}
redis-cli KEYS '*' | grep lit4000-team | sort

Before the fix, the auth path writes the full team object under the bare lit4000-team key that get_team_object never reads, in addition to the canonical key:

{model_per_team:lit4000-team:haiku}:tokens
{team:lit4000-team}:tokens
lit4000-team
spend:team:lit4000-team
team_id:lit4000-team
redis-cli TYPE lit4000-team            -> string
redis-cli GET  lit4000-team            -> {"team_alias": "lit4000", "team_id": "lit4000-team", ...}

After the fix, the bare orphan key is gone and the team object is cached only under the canonical team_id:lit4000-team that the read path serves; the real LLM call still returns "Hi":

{model_per_team:lit4000-team:haiku}:tokens
{team:lit4000-team}:tokens
spend:team:lit4000-team
team_id:lit4000-team

To confirm the ticket's expected behavior end to end (the team object caches on the first lookup and subsequent requests are served from cache rather than hitting Postgres), I ran three successive requests with the same team-scoped key on a cold start (in-memory empty + redis-cli FLUSHALL) and traced auth with OpenTelemetry v2 (LITELLM_OTEL_V2=true, console exporter). The auth span carries a postgres child span per DB lookup, so a cache hit shows up as that span disappearing.

Note for anyone reproducing this: import litellm runs load_dotenv(), which walks up to the repo .env; if that file sets OTEL_ENDPOINT, the v2 config sends spans there instead of the console. Pre-set OTEL_ENDPOINT="" OTEL_EXPORTER_OTLP_ENDPOINT="" OTEL_EXPORTER=console before launch so load_dotenv(override=False) can't repopulate them.

REQUEST 1 (cold)  [INTERNAL] auth /chat/completions
   ├─ [CLIENT] postgres get_data            db.system=postgresql   <- key lookup
   ├─ [CLIENT] postgres _get_team_db_check  db.system=postgresql   <- team lookup
   └─ [CLIENT] redis async_get/set_cache    db.system=redis        <- L2 populate

REQUEST 2  [INTERNAL] auth /chat/completions
   └─ [CLIENT] redis async_set_cache        db.system=redis        <- no postgres

REQUEST 3  [INTERNAL] auth /chat/completions
   └─ [CLIENT] redis async_set_cache        db.system=redis        <- no postgres
request | postgres spans under auth | redis spans under auth
   1     |            2              |        9
   2     |            0              |        2
   3     |            0              |        2

Only the cold request hits Postgres for the key and team objects; every subsequent request resolves auth entirely from the L2 cache, with zero postgres spans under auth. The remaining redis async_set_cache spans on requests 2 and 3 are cache-refresh writes (last-active / spend), not auth reads. The same result holds in the Postgres log_statement=all query log (3 auth SELECTs on request 1, 0 after)

Type

🐛 Bug Fix

Changes

The write in _user_api_key_auth_builder now uses f"team_id:{valid_token.team_id}", matching get_team_object / _cache_team_object / _update_team_cache, and keeps the existing guard that skips the write when team_id is None so a None key can never reach Redis.

Added a regression test that drives the real auth builder for a team-scoped key against an in-memory UserApiKeyCache and asserts the team object is served back under team_id:{id} and never under the raw team_id or a None key. Reverting the key to the raw team_id (or None) makes the canonical read miss and fails the test.

@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

@greptile-apps

greptile-apps Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes a cache key mismatch in the auth builder where the team object was written under the raw valid_token.team_id while all read paths (get_team_object, _update_team_cache) use the canonical team_id:{id} format, causing every request to fall back to Postgres even with Redis auth caching enabled.

  • user_api_key_auth.py: Changes the async_set_cache call from key=valid_token.team_id to key=f"team_id:{valid_token.team_id}", matching the read-path convention; the existing None-guard is preserved.
  • test_user_api_key_auth.py: Adds a regression test that runs the real auth builder against an in-memory UserApiKeyCache and asserts the team object is retrievable under the canonical key and absent under the raw or None key.

Confidence Score: 5/5

Safe to merge — the change is a one-line targeted correction to a cache write key that was never read back, with no behavioural change to auth logic.

The auth builder now writes the team object under the same team_id:{id} key that every read path expects, eliminating the stale orphan key and the Redis NoneType error on personal keys. The fix is isolated to a single call site, the None guard is preserved, and the accompanying regression test exercises the real builder end-to-end against an in-memory cache, making the mismatch impossible to silently re-introduce.

No files require special attention.

Important Files Changed

Filename Overview
litellm/proxy/auth/user_api_key_auth.py Single-line key fix: cache write now uses f"team_id:{valid_token.team_id}" instead of the bare valid_token.team_id, aligning it with the canonical key used by all read paths.
tests/test_litellm/proxy/auth/test_user_api_key_auth.py Adds a regression test that drives the real auth builder through an in-memory cache and asserts the team object lands under team_id:{id} and never under the raw team_id or a None key; all external calls are mocked, no real network access.

Reviews (1): Last reviewed commit: "fix(auth): cache auth-path team object u..." | Re-trigger Greptile

@greptile-apps

greptile-apps Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes a cache key mismatch in the auth path where the team object was written under the raw team_id instead of the canonical team_id:{id} format, causing every request to miss the L2 cache and fall back to Postgres. The change also prevented a NoneType key error in Redis for personal keys with team_id = None.

  • user_api_key_auth.py: One-line fix changes key=valid_token.team_id to key=f"team_id:{valid_token.team_id}", aligning the write site with get_team_object, _cache_team_object, and _update_team_cache which all use the team_id:{id} format.
  • test_user_api_key_auth.py: New regression test drives the real auth builder with a team-scoped key against an in-memory UserApiKeyCache and verifies the object is served from the canonical key and absent from both the raw and None keys.

Confidence Score: 5/5

Safe to merge; the change is a single targeted write-key correction with no risk of regressions on other paths.

The fix is a one-character-plus-prefix change at a single call site, confirmed correct by cross-checking _cache_team_object (line 1814), _update_team_cache (line 2998), and get_team_object — all three use team_id:{id}. The None-guard was already present and is retained. The new regression test validates the canonical-key write and the absence of a write under the raw or None key without making real network calls.

No files require special attention.

Important Files Changed

Filename Overview
litellm/proxy/auth/user_api_key_auth.py Single-line fix: cache key changed from raw team_id to canonical team_id:{id} format, matching the key used by get_team_object, _cache_team_object, and _update_team_cache.
tests/test_litellm/proxy/auth/test_user_api_key_auth.py New regression test drives the real auth builder with a team-scoped key, asserts the team object is stored under team_id:{id} and absent under the raw or None key; uses only mocks, no real network calls.

Reviews (2): Last reviewed commit: "fix(auth): cache auth-path team object u..." | Re-trigger Greptile

@codecov

codecov Bot commented Jun 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@yassin-berriai
yassin-berriai enabled auto-merge (squash) June 26, 2026 11:41
The auth builder cached the team object under the raw `valid_token.team_id`,
while `get_team_object`, `_cache_team_object`, and `_update_team_cache` all read
and write under `team_id:{id}`. The raw-key write was therefore never served
back, and on a non-team (personal) key, whose team_id is None, the original
unguarded version passed a None key straight to the cache layer; the in-memory
cache tolerates None keys but Redis rejects them with a NoneType key error, so
with `enable_redis_auth_cache: true` the team object never reached the L2 cache
and every request fell back to Postgres.

Write under the canonical `team_id:{id}` key, keeping the existing guard that
skips the write when team_id is None. Add a regression test that drives the real
auth builder for a team-scoped key against an in-memory cache and asserts the
team object is served back under `team_id:{id}` and never under the raw team_id
or a None key.

Resolves LIT-4000
@yassin-berriai
yassin-berriai force-pushed the litellm_lit4000_team_object_cache_key branch from 859f234 to 398961f Compare June 26, 2026 20:11
@yassin-berriai
yassin-berriai merged commit ce65836 into litellm_internal_staging Jun 26, 2026
123 checks passed
@yassin-berriai
yassin-berriai deleted the litellm_lit4000_team_object_cache_key branch June 26, 2026 20:36
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