lloydzhou/tail

Tail — Send the tail, the head is cached. 名字取自 tail -f:只看新增的几行。Tail 把同样的心智模型用到 LLM 请求上—— 前缀(头部)已在网关缓存,客户端只发增量(尾部),透明节省 SDK 与 LLM Gateway 之间的上行带宽。

★ 0Forks 0PythonGitHub ↗Compare
cache-controlllm-gatewayllm-sdk

README

Tail —— LLM 上行流量优化网关

Tail — Send the tail, the head is cached.

名字取自 tail -f:只看新增的几行。Tail 把同样的心智模型用到 LLM 请求上—— 前缀(头部)已在网关缓存,客户端只发增量(尾部),透明节省 SDK 与 LLM Gateway 之间的上行流量。

能省多少?代价多少?

长上下文模型(如 DeepSeek V4、GLM-5.2 等已默认 1M token 上下文)下,多轮对话的请求体里 95%+ 是重复的前缀。Tail 只发增量,把它压到接近 0。

省了多少上行流量

以 1M token 上下文为例(DeepSeek V4 / GLM-5.2 已默认 1M,粗估 1 token ≈ 4 字节):

不用 Tail 用 Tail
单次请求体 ~4 MB(完整 messages) ~2 KB(增量 + hash)
10 轮对话上行流量 ~40 MB 首次 ~4 MB + 9 × ~2 KB ≈ 4 MB
节省 — ~90%
1000 并发对话 × 日均 10 轮 ~40 GB/天上行 ~4 GB/天上行

上下文越长、轮次越多,节省比例越高(极限情况下单轮增量仅占 0.05%,节省 99.9%)。对上行流量计费敏感的场景(企业内网→公有云 LLM Gateway、跨境调用、移动端)尤其显著。

代价:多少临时存储

Tail 的本质是用临时存储换上行流量。缓存有 TTL(30 分钟~6 小时,自动过期回收), 而流量是一次性消耗——所以这是一笔极度划算的交易。

按 v2.1 数据结构,一个 1M token 对话(500 回合)的存储开销:

key 类型 数量 单条大小 小计
meta 1 150 B 150 B
sys(若有) 1 ~8 KB 8 KB
tools(若有) 1 ~4 KB 4 KB
seg(每回合) 500 ~8 KB ~3.8 MB
pfx(Merkle 节点) 500 80 B ~40 KB
合计 ~3.9 MB

ROI:存储换流量

同一对话 10 轮:3.9 MB 存储 换 34 MB 上行流量 —— 1 MB 临时存储 ≈ 换 9 MB 流量。

考虑 TTL 过期回收后的稳态(1000 并发对话,活跃率 30%):

指标 值
稳态存储(只保留活跃对话) ~1.1 GB
日节省上行流量 ~60 GB
比率 1 GB 存储 ≈ 换 53 GB/天 流量

存储会过期回收,流量消耗不会。以阿里云公开价格估算: 用 ~1 GB ESSD 云盘(¥0.50/GB/月 ≈ ¥0.02/天) 换 ~60 GB/天的上行流量(公网按量 ¥0.80/GB ≈ ¥48/天)—— ROI 约 2500 倍。


工作原理

flowchart LR
    SDK["客户端 SDK<br/>(openai monkey patch)"] -- "精简请求<br/>(hash + 增量)" --> GW["Tail 网关<br/>(FastAPI / Lua)"]
    GW -- "X-Cache-Hash/Hit" --> SDK
    GW -- "完整请求(还原后)" --> BE["LLM 推理服务<br/>(DeepSeek / OpenAI / ...)"]
    GW <-. "缓存读写" .-> KV[("可插拔存储<br/>默认 dbm / 可选 Redis 等")]
Loading
  1. 首次请求:客户端发完整 messages → 网关缓存前缀 → 返回 X-Cache-Hash
  2. 后续请求:SDK 自动只发增量 + hash → 网关按 hash 还原完整请求 → 转发后端
  3. 乐观发送 + 自动降级:默认带 hash,缓存未命中则 SDK 自动重发全量(对调用方完全透明)

透明性:

  • 对调用方:openai SDK 用法零改动(openai_patch.install() 一行)
  • 对后端:收到的是标准完整 OpenAI 请求,无感知
  • 支持 streaming(SSE):网关透明转发流式响应,边收边发不缓冲

快速开始(Python 版网关)

安装

pip install tail            # 或 pip install -e . (本仓库)

启动网关(一行命令,默认零依赖 dbm 存储)

# 后端指向真实推理服务即可
python -m tail.gateway --backend https://api.deepseek.com --port 8765

可选参数:

python -m tail.gateway --backend https://api.deepseek.com \
    --storage dbm \              # 默认,零依赖(也可 redis 连外部 KV)
    --dbm-path ./tail_cache.dbm \
    --miss-mode fast_fail \      # 未命中:fast_fail(412 重试)| passthrough
    --port 8765

客户端使用(零改动)

from tail import openai_patch
openai_patch.install()        # 装一次

from openai import OpenAI
client = OpenAI(base_url="http://127.0.0.1:8765/v1", api_key="sk-...")

# 照常用,SDK 自动维护前缀缓存、只发增量、自动降级
resp = client.chat.completions.create(
    model="deepseek-chat",
    messages=[{"role": "user", "content": "Hello"}],
)
# 流式也透明支持
stream = client.chat.completions.create(
    model="deepseek-chat",
    messages=[...], stream=True,
)

目录结构

tail/                      # 客户端 + Python 网关(主)
├── openai_patch.py        # ★ openai SDK monkey patch(指纹校验 + session 隔离 + 自动重试)
└── gateway/               # ★ Python 网关(FastAPI)
    ├── app.py             # 路由(三阶段:命中还原/透明转发/streaming SSE)
    ├── storage.py         # Storage 抽象基类 + DbmStorage(零依赖默认)/ 可插拔
    ├── segment.py / merkle.py / hashing.py   # v2.1 算法(segment 切分 + Merkle 增量链)
    ├── protocol.py        # 常量 + GatewayConfig
    └── __main__.py        # 命令行入口(python -m tail.gateway ...)

openresty/                 # (可选)OpenResty/Lua 版网关,与 Python 版 cache_key 逐字节一致、可互换
├── conf/nginx.conf
└── lua/kvcache/           # hashing/segment/merkle/protocol/store/gateway

tests/                     # 测试(Python 网关 + patch 单测 + OpenResty 端到端)
docs/
├── DESIGN-chunked-cache.md   # 设计文档(v2.1 Segment-Merkle + 访问驱动续期)
└── PROTOCOL.md               # ★ 线缆协议规范(X-Cache-* 头 / 412 / cache_key 三段格式)
run.sh                     # 一键启停(Kvrocks + 网关 + 模拟后端,OpenResty 版用)

协议(摘要)

请求方向(Client → Gateway):

Header 含义
X-Cache-Hash 可选。上次响应返回的缓存哈希。
X-Cache-Prefix-Length 可选。该哈希对应的前缀消息条数。

携带哈希时,messages 可只含增量;网关负责还原完整 messages。

响应方向(Gateway → Client):

Header 含义
X-Cache-Hash 本次前缀的新哈希,客户端应保存。
X-Cache-Expire 缓存过期 Unix 时间戳(带 ±jitter 防雪崩)。
X-Cache-Hit true/false,网关是否命中。

完整线缆协议规范(X-Cache-* 头语义、412 快速失败契约、cache_key 三段哈希格式、 Segment-Merkle 链、SSE 透传契约)见 docs/PROTOCOL.md。


与 OpenAI Responses API 对比

OpenAI 2025 年推出的 Responses API 也在解决"重复发送前缀"的问题,但思路不同。两者可结合使用。

思路差异

OpenAI Responses API Tail
缓存位置 OpenAI 服务端存储(store=true) 网关 + 客户端侧(你自己的基础设施)
复用机制 previous_response_id 链式引用上一次响应 X-Cache-Hash 协商 + 增量 messages
绑定厂商 仅 OpenAI(响应对象存在 OpenAI 侧) 厂商无关(任何 OpenAI 兼容 API:DeepSeek/Qwen/本地 vLLM 等)
换模型 ❌ 换模型会断链(上一轮响应不可用于其他模型) ✅ 无影响(缓存按 model 维度隔离)
数据所有权 OpenAI 持有 30 天(官方策略) 完全自控(可立即删除、私有部署)
计费 即使 previous_response_id,链上所有历史 input token 仍按 input 计费 后端收到的就是完整请求,计费不变
协议兼容 Responses API(新协议,需改代码迁移) Chat Completions(零改动)

什么时候用哪个?

  • 只用 OpenAI、且能接受 30 天服务端存储 → Responses API 够用,无需 Tail
  • 用 DeepSeek / Qwen / 本地 vLLM / 多厂商 → Tail(Responses API 不支持非 OpenAI)
  • 数据合规要求自控(金融/医疗/政企) → Tail(缓存在你自己的网关,不进第三方)
  • 想省的是"上行流量"而非"token 计费" → Tail 直接生效;Responses API 的省流量效果类似,但依赖服务端实现
  • 两者可叠加:用 Tail 省上行流量,同时后端是 OpenAI 时也可用 Responses API

本质区别

Responses API 是"把状态交给厂商保管"——换取便捷,但锁定厂商、数据留存厂商侧。 Tail 是"状态自己管"——多写一层网关,换来自控、跨厂商、协议兼容。


关键设计

  1. v2.1 Segment-Merkle:messages 按 LLM 回合切 segment,三段独立 hash(system/tools/messages),组合 cache_key = sys::tools::pfx;加一段只增 O(1) 节点;跨对话内容寻址复用。
  2. 访问驱动续期:缓存节点读一次续一个 TTL,活跃对话链永不过期,沉寂对话自然消亡。
  3. Storage 抽象:对齐 7 方法接口,默认 DbmStorage(标准库 dbm,零依赖),可插拔 Redis 等。
  4. SDK 一致性:前缀指纹校验(防 compact/编辑/重排静默错误)+ 多 session 上下文隔离 + 自动降级全量重发。
  5. 透明 streaming:SSE 边收边发,不缓冲,text/event-stream 原样透传。
  6. 乐观发送 + fast_fail:默认带 hash,未命中返回 412 由 SDK 重试一次(零额外 RTT)。

详见 docs/DESIGN-chunked-cache.md。


测试

# Python 网关单测 + 端到端(含 streaming SSE)
python3 -m pytest tests/test_gateway_py.py -v

# openai patch 单测(纯 Python)
python3 -m pytest tests/test_openai_patch.py -v
层 数量 覆盖
Python 网关 27 算法一致性 / dbm roundtrip / ASGI 端到端 / streaming SSE 透传 / 缓存命中
openai patch 18 多轮增量 / 多模型 / compact 降级 / 多 session 隔离 / 重试
OpenResty 端到端 15 三段存储 / 命中还原 / reload 持久 / 访问驱动续期 / streaming

两个网关实现(可互换)

Python 版(主) OpenResty 版(可选)
框架 FastAPI + uvicorn OpenResty + Lua
存储 dbm(零依赖默认)/ Redis 等 Kvrocks(硬盘)
启动 python -m tail.gateway --backend URL ./run.sh start(需编译 OpenResty+Kvrocks)
cache_key 逐字节一致 逐字节一致

两者产出的 X-Cache-Hash 完全相同(同样的 messages → 同样的 sys::tools::pfx),可互换或共存。Python 版零依赖开箱即用;OpenResty 版性能更高(适合大流量生产)。


License

MIT

Contributors

lloydzhou

Issues