Skip to content

fix(caching): write the response-cache SET to Redis at once instead of on the post-call batch - #43973

Merged
yassin-berriai merged 3 commits into
mainfrom
litellm_response_cache_set_direct
Oct 1, 2026
Merged

yassin-berriai merged 3 commits into
mainfrom
litellm_response_cache_set_direct

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

How it solves it:

User Flow

Before: a developer behind a proxy with a Redis cache and a slow success callback pays for the same answer twice

  1. They send POST https://litellm-domain/v1/messages with a prompt and get msg_A back
  2. Right after, they send the exact same POST https://litellm-domain/v1/messages
  3. A fresh provider call comes back with a new id msg_B, billed again

After: the second request is served from the cache

  1. They send POST https://litellm-domain/v1/messages with a prompt and get msg_A back
  2. Right after, they send the exact same POST https://litellm-domain/v1/messages
  3. The same msg_A comes back from the cache, no second provider call

Affected release

Regression in v1.105.0-dev.1 (introduced by #43779)

Pre-Submission checklist

  • I have added meaningful tests
  • The handful of test files covering my change pass locally, e.g. uv run pytest tests/unit/<your_test_file>.py -v
  • My PR passes all required CI/CD checks (e.g., lint, schema.d.ts sync check, etc.)
  • 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

Screenshots / Proof of Fix

Live proxy, real Redis at 127.0.0.1:6379, real Anthropic and OpenAI calls, and a success callback that sleeps 0.5s (the shape of a slow logging integration)

model_list:
  - model_name: claude-haiku-4-5
    litellm_params: {model: anthropic/claude-haiku-4-5, api_key: os.environ/ANTHROPIC_API_KEY}
  - model_name: gpt-5.4-mini
    litellm_params: {model: openai/gpt-5.4-mini, api_key: os.environ/OPENAI_API_KEY}
general_settings:
  master_key: os.environ/LITELLM_MASTER_KEY
litellm_settings:
  callbacks: slow_callback.slow_logger   # async_log_success_event: await asyncio.sleep(0.5)
  cache: true
  cache_params: {type: redis, host: 127.0.0.1, port: 6379}

Each run sends two back-to-back identical requests with a fresh random marker in the prompt, so every run starts with a cold key. A cache hit returns the first call's provider id, a miss returns a new one

curl -s localhost:4000/v1/messages -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'content-type: application/json' \
  -d '{"model":"claude-haiku-4-5","max_tokens":16,"messages":[{"role":"user","content":"say hi <uuid>"}]}'
curl -s localhost:4000/v1/chat/completions -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'content-type: application/json' \
  -d '{"model":"gpt-5.4-mini","messages":[{"role":"user","content":"say hi <uuid>"}]}'

Before (424bfd8, merge base)

/v1/messages

  1. Run 1: call 1 msg_011Cfaknn3vifvxKf2aDK6k9, call 2 msg_011CfaknqY2AbmJFpM1ec3P6, a second provider call
  2. Run 2: call 1 msg_011CfakntqTc9jb75mu3EmmQ, call 2 msg_011CfaknwFKLkKv5ATVtdxxP, a second provider call
  3. Run 3: call 1 msg_011CfaknypbuZtmC7MoFrCFv, call 2 msg_011Cfako2Ndvbt4UJLJg1KAG, a second provider call

/v1/chat/completions

  1. Run 1: both calls chatcmpl-EU19GJrS2fJXeeQKFLrIyiG5SgMwA, call 2 carries x-litellm-cache-key, cache hit
  2. Run 2: both calls chatcmpl-EU19HGgJQKro90iE8tcmyExQDnHkM, cache hit
  3. Run 3: both calls chatcmpl-EU19ILT3hqgtPB4f4n7fS5gQOc6S9, cache hit

After (e791607)

/v1/messages

  1. Run 1: both calls msg_011CfamqVedscTbbbtPjbQ1f, cache hit
  2. Run 2: both calls msg_011CfamqYUnzFmpgVQwajbR3, cache hit
  3. Run 3: both calls msg_011CfamqbLwaRxu5YBXZ2mhb, cache hit

/v1/chat/completions

  1. Run 1: both calls chatcmpl-EU1MVgHhyCzsckKQWHSm5GGSA62Wh, call 2 carries x-litellm-cache-key, cache hit
  2. Run 2: both calls chatcmpl-EU1MVeK8gFd0x4E2xesvuqP6458RC, cache hit
  3. Run 3: both calls chatcmpl-EU1MW49WX0hjDM8UjvleUvG3SqoAR, cache hit

Integration suite

  1. tests/integration/observability/test_cache_hit_guardrail_metrics.py against a local proxy, Postgres, Redis and the scripted upstream
  2. Merge base: test_cache_hit_post_call_reject_keeps_deployment_labels_anthropic fails with assert 2 == 1 provider requests
  3. This branch: 18 passed, 1 skipped

Unit tests and mutation check

  1. tests/unit/caching: 560 passed, 1 skipped
  2. With the merge-base caching.py and dual_cache.py copied back in, both new tests in test_request_redis_batch_post_call.py fail (response not in Redis before the post-call flush)

Type

🐛 Bug Fix

Caveats (if any)

Low

  • Chat completions did not reproduce live before or after, only /v1/messages missed
  • /v1/responses covered by the integration suite only, it passed on both sides
  • Unrelated _slot_stash line in the test file got reflowed by ruff format

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

Link to Devin session: https://app.devin.ai/sessions/ca842b11eda64990a199ffbd982d8826
Open in Devin Desktop: https://app.devin.ai/desktop/session/ca842b11eda64990a199ffbd982d8826?variant=devin
Requested by: @yassin-berriai

yassin-berriai and others added 2 commits October 1, 2026 02:46
…f on the post-call batch

The post-call Redis batch only goes out after every success callback finishes or the 1s deadline, so an
identical request sent right after the first response missed the cache and went to the provider again.
Response-cache writes go straight to Redis again, the counters, rate limits, TPM and slot releases keep
riding the post-call batch

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

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

… strict ruff gate

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@greptile-apps

greptile-apps Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[Medium risk] Changes when response cache writes hit Redis.

The PR appears safe to merge based on the reviewed changes.

Summary

The PR restores direct Redis writes for response-cache entries while leaving spend, rate-limit, and deployment updates in the post-call batch. It updates the unit tests to check that response entries are available before that batch flushes.

Reviews (1) · Last reviewed commit: "fix(caching): re-export DualCache explic..."

@codecov

codecov Bot commented Oct 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed

codspeed Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_response_cache_set_direct (e791607) with main (ae60fd1)

Open in CodSpeed

from .base_cache import BaseCache
from .disk_cache import DiskCache
from .dual_cache import DualCache
from .dual_cache import DualCache # noqa: F401 # re-exported, callers import DualCache from litellm.caching.caching
@yassin-berriai
yassin-berriai merged commit 321be01 into main Oct 1, 2026
96 of 98 checks passed
@yassin-berriai
yassin-berriai deleted the litellm_response_cache_set_direct branch October 1, 2026 03:39
jan-sauer-reef added a commit to jan-sauer-reef/litellm that referenced this pull request Oct 1, 2026
…ject_key_prefix

* upstream/main: (62 commits)
  fix(guardrails): scan Responses API input in Azure Prompt Shield (BerriAI#43786)
  feat(lens): investigate sampled traces and retain batch results (BerriAI#43942)
  fix(proxy): restore pre-config-wins handling of pass-through endpoints (BerriAI#43962)
  fix(cost-map): raise baseten DeepSeek-V4.1-Flash max output to 262144 (BerriAI#43916)
  chore(cost-map): add deprecation date for anthropic claude-sonnet-4-5 (BerriAI#43898)
  chore(cost-map): add fireworks inkling priority prices from the prices api (BerriAI#43949)
  feat(guardrails): honor litellm_params.timeout in every HTTP guardrail (BerriAI#43134)
  test(e2e): typed per-test metadata for the e2e suite (BerriAI#42044)
  fix(caching): write the response-cache SET to Redis at once instead of on the post-call batch (BerriAI#43973)
  feat(ui): filter tags by name and description on the Tag Management page (BerriAI#42949)
  feat(providers): add Cortecs as an OpenAI-compatible provider (BerriAI#43872)
  feat(e2e): record each e2e test's steps, starting with ProxyClient (BerriAI#42393)
  test(ci): repair stale tests and move retired OpenAI text-completion fixtures (BerriAI#43958)
  feat(proxy): record in spend logs whether a request used a client-forwarded Anthropic OAuth token (BerriAI#43063)
  fix(azure_storage): keep the DataLakeServiceClient alive until its TTL elapses (BerriAI#43082)
  chore(deps): bump gitpython and tornado, extend diskcache osv ignore to Nov 1 (BerriAI#43961)
  fix(guardrails): treat an unknown straiker api_version as unset instead of skipping the guardrail (BerriAI#43956)
  fix(azure_storage): name Data Lake objects without base64 padding or slashes (BerriAI#43914)
  fix(grayswan): send request conversation and tool calls to post-call monitor (BerriAI#43770)
  chore(cost-map): sync openrouter prices from the models API (BerriAI#43950)
  ...
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