LiteLLM Prompt Caching / 提示缓存 全链路文档¶
本系列文档完整描述 LiteLLM 中两套互相独立的"prompt caching"机制:
- 上游 Prompt Cache 亲和路由(本系列重点)—— Router 层的
optional_pre_call_checks: ["prompt_caching"],作用是把"含相同 cache_control 前缀的请求"锁定到同一个 deployment,从而让上游(Anthropic/Bedrock/Vertex)的 prompt cache 真正复用。 - 上游 Prompt Cache 计费透传 —— 各 provider 的
cache_creation_input_tokens/cache_read_input_tokens字段如何被解析、归一化到Usage、写入StandardLoggingPayload、参与成本计算。
⚠️ 常见混淆:以下三个东西互不相干,名字都带"cache":
名字 是什么 数据存哪 Prompt Cache(上游) Anthropic/OpenAI 自己的功能,重复前缀按缓存价收费 provider 侧 Prompt Cache 路由亲和 Router 的 PromptCachingDeploymentCheck,让请求黏在同一 deploymentLiteLLM 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
几点关键事实:
- 整套机制只在 messages 含
cache_control块时工作。extract_cacheable_prefix找不到任何cache_control→ cache_key 直接返回 None → 全程退化为正常路由(prompt_caching_cache.py:110-111)。 - 写入时机:
async_log_success_event在请求成功后触发,不检查上游 response 是否真的 cache write(不看cache_creation_input_tokens > 0)—— 只看 messages 有 cache_control + token >= 1024。 - tools 参数实际写死 None:prompt_caching_deployment_check.py:41 和 行 97 都传
tools=None,注释明说[TODO]: add tools once standard_logging_object supports it。所以两个含相同 messages 但不同 tools 的请求会被认为是同一个 cache key。 - TTL 300s 硬编码:prompt_caching_cache.py:190-191、行 211 都写
ttl=300,无 YAML 配置项可改。 call_type限定:只有completion/acompletion/anthropic_messages三种调用类型会触发写入(prompt_caching_deployment_check.py:61-69)。embedding、image、responses 等都不写。- 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"}}
]
}
或者消息级别:
没有 cache_control 块整套机制完全 no-op(prompt_caching_cache.py:110-111:extract_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 普遍偏短,可以降低阈值:
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 场景)¶
适用:业务 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