跳转至

LiteLLM Prompt Caching / 提示缓存 全链路文档

本系列文档完整描述 LiteLLM 中两套互相独立的"prompt caching"机制:

  1. 上游 Prompt Cache 亲和路由(本系列重点)—— Router 层的 optional_pre_call_checks: ["prompt_caching"],作用是把"含相同 cache_control 前缀的请求"锁定到同一个 deployment,从而让上游(Anthropic/Bedrock/Vertex)的 prompt cache 真正复用。
  2. 上游 Prompt Cache 计费透传 —— 各 provider 的 cache_creation_input_tokens / cache_read_input_tokens 字段如何被解析、归一化到 Usage、写入 StandardLoggingPayload、参与成本计算。

⚠️ 常见混淆:以下三个东西互不相干,名字都带"cache":

名字 是什么 数据存哪
Prompt Cache(上游) Anthropic/OpenAI 自己的功能,重复前缀按缓存价收费 provider 侧
Prompt Cache 路由亲和 Router 的 PromptCachingDeploymentCheck,让请求黏在同一 deployment LiteLLM Redis/内存
LiteLLM 响应缓存 litellm_settings.cache: true,缓存整个 completion 响应 LiteLLM Redis/内存/S3

本系列只讲前两个。第三个(响应缓存)请看 docs/billing-and-pricing/05-cache-pricing-bugs.md 和上游文档。


文档目录

文件 内容
01-mechanism.md 路由层全链路:extract_cacheable_prefix 截取规则、SHA256 cache key、async_filter_deployments 命中后强制路由、async_log_success_event 写入时机、DualCache 双层存储、TTL 300s 硬编码
02-config-reference.md YAML 配置全字段:router_settings.optional_pre_call_checks / enable_pre_call_checks / Redis 配置 / MINIMUM_PROMPT_CACHE_TOKEN_COUNT 环境变量;幻觉字段清单enable_redis_auth_cache 不存在等)
03-provider-matrix.md 各 provider 的 cache_control 请求格式与响应字段:Anthropic 原生 / Bedrock Converse / Bedrock Invoke / Vertex AI Anthropic / Azure AI Anthropic / OpenAI 的差异矩阵
04-billing-and-cost.md cache_creation_input_token_cost / cache_read_input_token_cost / ..._above_1hr / ..._above_200k_tokens 分级定价、CacheCreationTokenDetails 5m vs 1h 拆分、已知计费 bug
05-best-practices.md 中转站场景能不能用、多 Pod / Redis 共享、最小流量门槛、MINIMUM_PROMPT_CACHE_TOKEN_COUNT 调参、Redis 排障

整体架构一览

flowchart TB
    subgraph "请求侧"
        Req["业务请求<br/>messages 含 cache_control"]
    end

    subgraph "Router 选 deployment"
        Healthy["healthy_deployments<br/>(N 个同 model_name 部署)"]
        Filter["_run_pre_call_checks<br/>router.py:6100-6140"]
    end

    subgraph "PromptCachingDeploymentCheck<br/>prompt_caching_deployment_check.py"
        Gate1{"is_prompt_caching_valid_prompt<br/>token_count >= 1024 ?"}
        Gate2{"extract_cacheable_prefix<br/>有 cache_control 块 ?"}
        Lookup["async_get_model_id<br/>(查 cache_key → model_id)"]
        BypassFilter["保留命中 deployment<br/>过滤掉其他"]
    end

    subgraph "存储层 DualCache"
        Mem["InMemoryCache<br/>(本地 60s)"]
        Redis["Redis<br/>key: deployment:{sha256}:prompt_caching<br/>value: {model_id}<br/>TTL=300s 硬编码"]
    end

    subgraph "上游 LLM"
        Provider["Anthropic / Bedrock / Vertex<br/>同一个 deployment<br/>= 同一个 cache pool"]
    end

    subgraph "Success Callback<br/>async_log_success_event"
        WriteCache["async_add_model_id<br/>写入 messages 指纹 → 实际 model_id"]
        CheckCallType{"call_type ==<br/>completion/acompletion/<br/>anthropic_messages ?"}
    end

    Req --> Healthy
    Healthy --> Filter
    Filter --> Gate1
    Gate1 -- "否" --> NoFilter[原样返回]
    Gate1 -- "是" --> Gate2
    Gate2 -- "无 cache_control" --> NoFilter
    Gate2 -- "有" --> Lookup
    Lookup -.读.-> Redis
    Lookup -.读.-> Mem
    Lookup -- "命中" --> BypassFilter
    Lookup -- "未命中" --> NoFilter
    NoFilter --> Provider
    BypassFilter --> Provider
    Provider --> CheckCallType
    CheckCallType -- "是" --> WriteCache
    WriteCache -.写.-> Redis
    WriteCache -.写.-> Mem
    CheckCallType -- "否" --> Skip[跳过写入]

    style Gate1 fill:#fdd,stroke:#c33
    style Gate2 fill:#fdd,stroke:#c33
    style BypassFilter fill:#dfd,stroke:#3a3
    style Redis fill:#ffd,stroke:#aa3

几点关键事实

  1. 整套机制只在 messages 含 cache_control 块时工作extract_cacheable_prefix 找不到任何 cache_control → cache_key 直接返回 None → 全程退化为正常路由(prompt_caching_cache.py:110-111)。
  2. 写入时机async_log_success_event 在请求成功后触发,不检查上游 response 是否真的 cache write(不看 cache_creation_input_tokens > 0)—— 只看 messages 有 cache_control + token >= 1024。
  3. tools 参数实际写死 Noneprompt_caching_deployment_check.py:41行 97 都传 tools=None,注释明说 [TODO]: add tools once standard_logging_object supports it。所以两个含相同 messages 但不同 tools 的请求会被认为是同一个 cache key
  4. TTL 300s 硬编码prompt_caching_cache.py:190-191行 211 都写 ttl=300,无 YAML 配置项可改。
  5. call_type 限定:只有 completion / acompletion / anthropic_messages 三种调用类型会触发写入(prompt_caching_deployment_check.py:61-69)。embedding、image、responses 等都不写。
  6. DualCache 共享语义:写入时双写(Redis + 内存),读取时先内存后 Redis。多 Pod 间 Redis 共享,所以 Pod A 写入的映射 Pod B 能读到。

触发前提速查

flowchart LR
    Start[请求到达]
    Q1{"messages 里有<br/>cache_control 块?"}
    Q2{"token_count >= 1024 ?"}
    Q3{"router 配了<br/>optional_pre_call_checks:<br/>[prompt_caching]?"}
    Q4{"同 model_name 下<br/>≥2 个 deployment ?"}
    Work[★ 路由黏性生效]
    NoOp[退化成普通负载均衡]

    Start --> Q3
    Q3 -- 否 --> NoOp
    Q3 -- 是 --> Q1
    Q1 -- 否 --> NoOp
    Q1 -- 是 --> Q2
    Q2 -- 否 --> NoOp
    Q2 -- 是 --> Q4
    Q4 -- 否 --> NoOp
    Q4 -- 是 --> Work

    style Work fill:#dfd,stroke:#3a3
    style NoOp fill:#fdd,stroke:#c33

4 个条件缺一不可。详见 01-mechanism.md §2 触发前提的硬约束.


速查:默认行为

维度 默认值 说明
MINIMUM_PROMPT_CACHE_TOKEN_COUNT 1024 constants.py:252-254,可通过同名环境变量覆盖
Cache key 算法 SHA256 extract_cacheable_prefix 结果序列化后哈希
Cache key 前缀范围 第一条消息 → 最后一个含 cache_control 的内容块 之后的消息不参与指纹
Cache value {"model_id": "<deployment-uuid>"} TypedDict PromptCachingCacheValue
TTL 300 秒(硬编码) prompt_caching_cache.py:190,211
存储 DualCache(Redis + 内存) 多 Pod 间通过 Redis 共享
写入触发 callback async_log_success_event 请求成功后调用
call_type 限制 completion / acompletion / anthropic_messages 其它类型不写
tools 参与 cache_key ❌ 否(写死 None) 即使代码理论上支持

决策树:你应该看哪一篇?

flowchart TD
    Q{你的问题是?}
    Q -->|"想理解整体怎么跑"| A1[01-mechanism]
    Q -->|"YAML 怎么配 / 字段查不到"| A2[02-config-reference]
    Q -->|"我用的是 bedrock/vertex/中转站<br/>能不能用 / 字段叫啥"| A3[03-provider-matrix]
    Q -->|"cache token 怎么算钱 / 价格漏配"| A4[04-billing-and-cost]
    Q -->|"中转站场景 / 多 Pod / 调试"| A5[05-best-practices]

    style Q fill:#cef,stroke:#369

一分钟自检

如果你怀疑 prompt cache 路由没生效。如果你能 SSH 到 pod 就走 shell 路径,否则走 纯 HTTP 路径

纯 HTTP 自检(无 shell 访问)

PROXY=https://your-proxy.example.com
ADMIN_KEY=sk-...

# 1. 看 callback 是否注册了 PromptCachingDeploymentCheck
curl -s "$PROXY/get/config/callbacks" \
  -H "Authorization: Bearer $ADMIN_KEY" \
  | jq '.[] | select(.name | contains("PromptCaching"))'
# 期望: 看到 PromptCachingDeploymentCheck 条目

# 2. 看 router_settings 里有没有这个字段
curl -s "$PROXY/router/settings" \
  -H "Authorization: Bearer $ADMIN_KEY" \
  | jq '.current_values.optional_pre_call_checks'
# 期望: ["prompt_caching"]
# 注意: 可能返回 null 但实际生效, 看 02-config-reference.md §9 UI 不可见的事实

# 3. 实际跑两次相同前缀的请求,对比 usage
# 看 05-best-practices.md §5.3 HTTP-only 验证 - c 跑实际请求看 usage

Shell 自检(需要 SSH / kubectl / redis-cli)

1. 配置是否正确开了?

# YAML 模式
grep -E "optional_pre_call_checks|prompt_caching" proxy_server_config.yaml

# DB 模式(SQL 直查)
psql -c "SELECT param_value FROM \"LiteLLM_Config\" WHERE param_name='router_settings'" | jq

期望看到 optional_pre_call_checks: ["prompt_caching"]没有这行整个机制根本不会注册router.py:1257-1258)。

2. messages 里有没有 cache_control?

发请求前先打印 messages JSON,看是否有:

{
  "role": "user",
  "content": [
    {"type": "text", "text": "...", "cache_control": {"type": "ephemeral"}}
  ]
}

或者消息级别:

{"role": "system", "content": "...", "cache_control": {"type": "ephemeral"}}

没有 cache_control 块整套机制完全 no-opprompt_caching_cache.py:110-111extract_cacheable_prefix 返回空列表 → get_prompt_caching_cache_key 返回 None)。

3. token 数够不够 1024?

from litellm.utils import token_counter
print(token_counter(model="claude-3-5-sonnet-20241022", messages=your_messages))

不到 1024 → is_prompt_caching_valid_prompt 返回 False → 路由检查直接跳过。

如果你的业务 prompt 普遍偏短,可以降低阈值:

export MINIMUM_PROMPT_CACHE_TOKEN_COUNT=512

4. Redis 里有没有写入?

.venv312/bin/python -c "
import os, redis
r = redis.Redis(host=os.environ['REDIS_HOST'], port=int(os.environ['REDIS_PORT']),
                password=os.environ.get('REDIS_PASSWORD'))
keys = r.keys('deployment:*:prompt_caching')
print('prompt_caching keys:', len(keys))
for k in keys[:5]:
    print(f'  {k.decode()[:80]}...  TTL={r.ttl(k)}s')
"
  • 看到 key → 路由黏性映射已建立
  • 看不到 key → 走前面 3 步排查

5. 同 model_name 是否真有 ≥2 个 deployment?

# YAML 模式
grep -B1 "model_name:" proxy_server_config.yaml | head -40

# DB 模式(纯 HTTP)
curl -s "$PROXY/v1/model/info" \
  -H "Authorization: Bearer $ADMIN_KEY" \
  | jq '.data | group_by(.model_name) | map({model_name: .[0].model_name, count: length}) | sort_by(-.count)'

每个 model_name 至少出现 2 次(count >= 2),否则 router 没得选,filter 等于 no-op。


三个最常用配置组合

A. 完全没开(当前你 proxy 的状态)

# router_settings 里没有 optional_pre_call_checks
# 所有请求负载均衡随机分发到任一 deployment
# 上游 prompt cache 命中率取决于运气(同一前缀刚好打到同一上游账号)

B. 基础启用(前提:≥2 同名 deployment)

router_settings:
  optional_pre_call_checks: ["prompt_caching"]
  redis_host: os.environ/REDIS_HOST              # 多 Pod 必须
  redis_port: os.environ/REDIS_PORT
  redis_password: os.environ/REDIS_PASSWORD

适用:你的多个 deployment 是 AWS 多账户 / 多区域 / 多中转站,且各自维护独立的 cache pool

C. 阈值降低(短 prompt 场景)

# 部署时通过环境变量
export MINIMUM_PROMPT_CACHE_TOKEN_COUNT=512
router_settings:
  optional_pre_call_checks: ["prompt_caching"]
  redis_host: ...

适用:业务 prompt 平均 600~900 tokens,但通过 cache_control 标记了固定的 system block。注意:上游本身有 1024 token 的最小命中限制(Anthropic 文档),降低 LiteLLM 这边的阈值只是让路由层愿意"尝试黏性",并不能让上游真的命中 cache。所以通常不必改

详细推荐组合见 05-best-practices.md.


关于本系列文档

  • 代码行号引用基于 master 分支同步点(2026-05),上游可能漂移,遇到 grep 不到时按文件名 + 函数名定位
  • 实战经验来自本项目 prod / dev 调研,不是搬运上游 docs
  • 相关上游文档:
  • https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching
  • https://docs.litellm.ai/docs/proxy/prompt_caching