Skip to content

fix(proxy): cache invalidation double-hashes token in bulk update and key rotation#25552

Merged
krrish-berri-2 merged 1 commit intoBerriAI:litellm_oss_staging_04_11_2026from
dkindlund:fix/cache-invalidation-hash-token
Apr 12, 2026
Merged

fix(proxy): cache invalidation double-hashes token in bulk update and key rotation#25552
krrish-berri-2 merged 1 commit intoBerriAI:litellm_oss_staging_04_11_2026from
dkindlund:fix/cache-invalidation-hash-token

Conversation

@dkindlund
Copy link
Copy Markdown
Contributor

Summary

  • Two remaining hash_token() calls in cache invalidation paths double-hash pre-hashed token IDs, causing cache invalidation to silently fail
  • Fix: use _hash_token_if_needed() (which passes pre-hashed tokens through unchanged), matching the pattern established by PR fix: allow hashed token_id in /key/update endpoint #24969

Root cause

When /key/update or /key/bulk_update is called with a pre-hashed token ID (not an sk- prefixed key), hash_token() produces a hash-of-hash that does not match the actual cache key. The _delete_cache_key_object call deletes nothing, and the stale cached key object (with outdated fields) remains.

This is compounded by update_cache() in proxy_server.py, which writes the stale cached key object back to user_api_key_cache with a fresh 60s TTL after every successful LLM request. As long as requests keep flowing, the stale entry never expires — the updated fields (e.g. max_budget, models, metadata) are never picked up.

PR #24969 fixed this in update_key_fn but missed two other call sites:

Location Line Before After
_process_single_key_update 1852 hash_token(key_update_item.key) _hash_token_if_needed(key_update_item.key)
_execute_virtual_key_regeneration 3724 hash_token(key) _hash_token_if_needed(key)

Reproduction steps

  1. Create a virtual key and note the token hash from the response
  2. Update the key using the token hash (not the sk- key):
    curl -X POST http://localhost:4000/key/update \
      -H "Authorization: Bearer sk-master" \
      -d '{"key": "<token_hash>", "max_budget": 100}'
  3. Verify via /key/info that the DB was updated (shows max_budget: 100)
  4. Send requests using the key — the budget is not enforced because the cached key object still has max_budget: null
  5. The stale cache entry persists indefinitely as long as requests keep flowing (each successful request refreshes the TTL via update_cache)

Related

Test plan

  • New test: test_process_single_key_update_cache_invalidation_with_token_hash — verifies the token hash is passed as-is (not double-hashed) to _delete_cache_key_object
  • Existing test_process_single_key_update continues to pass

Generated with Claude Code

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 11, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
litellm Ready Ready Preview, Comment Apr 11, 2026 4:54am

Request Review

@codspeed-hq
Copy link
Copy Markdown
Contributor

codspeed-hq Bot commented Apr 11, 2026

Merging this PR will not alter performance

✅ 16 untouched benchmarks


Comparing dkindlund:fix/cache-invalidation-hash-token (82f2006) with main (4e12d3c)

Open in CodSpeed

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 11, 2026

Greptile Summary

This PR fixes two remaining double-hashing bugs in cache invalidation paths that were missed by PR #24969. When a pre-hashed token ID (not sk- prefixed) is passed to /key/bulk_update or /key/regenerate, the old hash_token() call would produce a hash-of-hash that doesn't match any cache key, leaving stale entries to persist indefinitely. Replacing both with _hash_token_if_needed() (which passes pre-hashed tokens through unchanged) is the correct fix, consistent with the pattern already applied elsewhere in the file. Tests are added for both fixed call sites.

Confidence Score: 5/5

Safe to merge — minimal, correct fix with no behavioural change for the happy path and proper test coverage for both fixed call sites.

The two-line production change is a precise, well-scoped fix that mirrors the pattern already applied in PR #24969. Both fixed call sites now have dedicated unit tests asserting the token is passed through unchanged. No regressions are introduced; the fix only affects callers supplying pre-hashed tokens (the previously broken path). All remaining observations are P2 or lower.

No files require special attention.

Important Files Changed

Filename Overview
litellm/proxy/management_endpoints/key_management_endpoints.py Replaces hash_token() with _hash_token_if_needed() in two cache invalidation call sites (_process_single_key_update L1852 and _execute_virtual_key_regeneration L3724), correctly fixing double-hashing of pre-hashed tokens
tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py Adds two async unit tests verifying both fixed call sites pass pre-hashed tokens through to _delete_cache_key_object unchanged; no real network calls; all mocked correctly

Sequence Diagram

sequenceDiagram
    participant Client
    participant BulkUpdate as _process_single_key_update
    participant Regen as _execute_virtual_key_regeneration
    participant DB as Prisma DB
    participant Cache as user_api_key_cache

    Note over Client,Cache: /key/bulk_update with pre-hashed token ID

    Client->>BulkUpdate: key is pre-hashed token
    BulkUpdate->>DB: update_data with token
    Note over BulkUpdate: _hash_token_if_needed(key) returns token as-is
    BulkUpdate->>Cache: _delete_cache_key_object with correct hash

    Note over Client,Cache: /key/regenerate with pre-hashed token ID

    Client->>Regen: key is pre-hashed token
    Regen->>DB: update verificationtoken record
    Note over Regen: _hash_token_if_needed(key) returns token as-is
    Regen->>Cache: _delete_cache_key_object with correct hash

    Note over Client,Cache: Before fix: hash_token produced wrong hash, cache miss, stale entry persisted
Loading

Reviews (2): Last reviewed commit: "fix(proxy): use _hash_token_if_needed fo..." | Re-trigger Greptile

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 11, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

…update and key rotation

Two code paths in key_management_endpoints.py call hash_token()
unconditionally when invalidating the user_api_key_cache after a key
update.  When the caller passes a pre-hashed token ID (not an sk-
prefixed key), hash_token() double-hashes it, producing a cache key
that does not match the actual cached entry.  Cache invalidation
silently fails.

This is compounded by update_cache() which writes the stale cached key
object back with a fresh 60s TTL after every successful request,
preventing natural TTL expiry.  The stale entry (with outdated fields
like max_budget=None) persists indefinitely under load.

PR BerriAI#24969 fixed this in update_key_fn but missed two other call sites:
- _process_single_key_update (bulk update path)
- _execute_virtual_key_regeneration (key rotation path)

Fix: replace hash_token() with _hash_token_if_needed() in both
locations, matching the pattern already used elsewhere in the file.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@dkindlund dkindlund force-pushed the fix/cache-invalidation-hash-token branch from fedb4c9 to 82f2006 Compare April 11, 2026 04:53
@krrish-berri-2 krrish-berri-2 changed the base branch from main to litellm_oss_staging_04_11_2026 April 12, 2026 02:36
@krrish-berri-2 krrish-berri-2 merged commit f54e4e6 into BerriAI:litellm_oss_staging_04_11_2026 Apr 12, 2026
49 of 51 checks passed
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.

2 participants