[Bug] /api/anthropic 隐式 prompt cache 三项异常:append-only 紧邻轮无法命中上一轮前缀、cache_read 标签与实际耗时脱钩、system 前缀零缓存(附受控复现脚本)
Nobody has claimed this yet.
- Dominant language
- No language data
- Stars
- 22
- Forks
- 1
- PR merge metrics
- No merged PRs in 30d
Description
提交前确认 · Pre-submission checklist
- 我已搜索过现有 issue,确认这不是重复(最接近的是 #354,但根因与证据维度不同,见文末「与 #354 / #322 的关系」)
- 我已阅读 CONTRIBUTING.md
问题类别 · Category
API 行为 / 性能(疑似牵连计费口径,见文末「计费疑问」)
涉及的 Agent 框架 · Agent framework
Claude Code(ANTHROPIC_BASE_URL 直连 open.bigmodel.cn/api/anthropic)。复现不依赖任何客户端——下方脚本用 Python 标准库直连端点,可排除客户端前缀不稳定等因素。
严重程度 · Severity
影响体验 · Major:多轮工具调用的长会话中,几乎每个请求都在为「已报缓存命中」的前缀支付全量 prefill 等待(92k ctx 下约 5-6s/请求;真实 15-21 万 token 会话 p50 TTFT 7.8s)。
复现频率 · Reproducibility
必现(S2 append miss、S3 system 零命中每次必现;「标签命中但耗时=全量」间歇出现,多次运行可复现)
问题描述 · Description
对 https://open.bigmodel.cn/api/anthropic/v1/messages 做了一组受控实验(流式、anthropic-version: 2023-06-01、无任何中转/代理),并结合一个真实 Claude Code 多轮工具调用会话的本地 transcript 逐请求分析(timestamp + usage + 延迟),发现隐式 prompt cache 存在三项异常:
- append-only 紧邻轮无法命中上一轮前缀:请求 N+1 的消息序列以请求 N 为逐字节前缀(Anthropic 前缀缓存的标准形态,也是 Claude Code 的实际请求形态),但 N+1 的
cache_read=0、TTFT 与全量冷 prefill 一致;隔 1-2 轮之后才会命中数轮之前的旧断点(cache_read恒等于 round1 的 input,而非紧邻上一轮)。对每 8-30s 一轮的 agent 会话,等效于每轮全量重算。 cache_read标签与实际 prefill 耗时脱钩:部分请求 usage 报cache_read≈全部前缀,但 TTFT 与冷值完全一致(对照组数据见下)。真实 Claude Code 会话中全部请求都报了cache_read>0,但按 TTFT 分桶仅 3% 落在真命中区间。- system 前缀零缓存:system 为大前缀时(带或不带
cache_control、带或不带context-1mbeta header),连发完全相同的请求cache_read恒为 0。Claude Code 每个请求都携带大体积 system + tools 前缀,这部分从第一天起就没有被缓存过。
辅助观察:完全相同的请求重复发送时缓存工作良好(TTFT 1.3-1.9s vs 冷 5.5-5.7s),TTL ≥ 120s;无 cache_control 的完全相同请求也可隐式命中;cache_creation_input_tokens 从未返回过值(恒缺席/0),与 #354 观察的 cacheWriteTokens=0 一致。
复现步骤 · Steps to reproduce
export GLM_KEY="你的 API Key"
python3 glm_anthropic_cache_probe.py # 默认 glm-5.3-flash,约 14 次请求 × 92k input
脚本全文见文末「复现脚本」,仅用 Python 标准库。
实际表现 · Actual behavior(2026-08-29 实测,glm-5.3-flash,前缀 ≈92k tokens)
冷基线(随机前缀,保证 miss):TTFT ≈ 5.5-5.7s。
| 场景 | 请求 | TTFT | input | cache_read | 判读 |
|---|---|---|---|---|---|
| S1 完全相同请求 ×3 | #1 / #2 / #3 | 5.6 / 1.5 / 1.3 s | 92483 → 3 | 0 / 92480 / 92480 | 缓存基础设施可用:相同请求秒级真命中 |
| S2 append-only ×4 | round1 | 5.7s | 92478 | 0 | 冷启动,正常 |
| round2-append | 5.7s | 92490 | 0 | 上一轮是本轮的逐字节前缀,却 miss、全量重算 | |
| round3-append | 5.2s | 92503 | 0 | 连续第二轮 miss | |
| round4-append | 1.4s | 36 | 92480 | 命中的是 round1 的旧断点(92480≈round1 的 92478,而非 round2/3 的 92490/92503) | |
| S3 system 大前缀 ×2 | #1 / #2 | 5.3 / 5.2 s | 92466 | 0 / 0 | system 前缀零缓存,连发也不命中 |
| S4 无 cache_control 追加 ×3 | round1/2/3 | 5.2 / 5.1 / 1.3 s | →14 | 0 / 0 / 92480 | 追加轮同样 miss,偶发命中旧断点 |
补充运行记录(同日、同样脚本形态的更早一轮,n 更多):
- 「假命中」样本:完全相同请求连发 6 次,首发 TTFT=4.2s(冷值)但
cache_read=92416报满额命中;另有一轮 #3 报cache_read=92416、TTFT=5.1s(冷值)。即标签记命中、算力全量重算的情况稳定间歇出现(多轮连发时约 1/6)。 - TTL:间隔 120s 重发完全相同请求,仍真命中(1.2s)。
glm-5.3(非 flash)复测 S3 形态:同样零命中。- 加
anthropic-beta: context-1m-2025-08-07:行为不变。
真实 Claude Code 会话佐证(一个多轮工具调用子 agent,glm-5.3-flash + 1M beta,本地 transcript 逐请求统计):
- 全部 212 个带 usage 的 API 往返
cache_read>0(100%),中位数cache_read=205,184tokens——按标签口径几乎每轮「命中」 - 但按首块 TTFT 统计(n=116 个往返):<3s(真命中区间)仅 3%(4 个),p10/p50/p90 = 4.1 / 7.8 / 16.8 s——与该上下文规模的全量 prefill 耗时一致
- 该任务累计:input 0.60M + cache_read 45.13M + output 0.11M tokens,墙钟 ~63 分钟,其中模型侧等待占 ~68%
期望表现 · Expected behavior
- append-only 请求(上一轮请求是本轮的逐字节前缀)应命中上一轮的完整前缀,只对新增尾部做 prefill——这是 Anthropic 前缀缓存语义,也是 Claude Code 这类 agent 在长会话下可用的前提
cache_read_input_tokens应反映真实的 KV 复用量(标签 = 算力);若命中失败应报 0 而不是报满额- system / tools 前缀应参与缓存
与 #354 / #322 的关系
- #354:结论依赖
cache_read标签的真实性做积分折算(Input 6.9 / Cached 1.7 / Output 24)。本 issue 的发现 2 表明标签可能系统性虚高——若如此,#354 的积分测算还是乐观值。两者互补:#354 证明「客户端前缀重排会降低标签命中率」;本 issue 证明「即使客户端前缀完美(完全相同 / 标准 append-only),服务端仍存在紧邻轮 miss、旧断点命中、标签-耗时脱钩」。另注:#354 中提到同端点 Claude Code 可达 90%+ 命中,与本 issue 的标签口径数据一致(100% 报命中),差异在于我们同时测了耗时,发现标签与耗时脱钩。 - #322:其「恢复会话全额继承、缓存隔夜过期后每轮全价重发」与本 issue 的「append 紧邻轮 miss」是同一成本放大模式的两端(TTL 过期 / 断点丢失)。
计费疑问(请一并确认)
官方缓存文档(docs.bigmodel.cn/cn/guide/capabilities/cache)说明缓存命中按标准价 50% 计费(OpenAI 端点 cached_tokens 口径,且不含 Coding Plan 套餐)。请确认:
/api/anthropic的cache_read_input_tokens是否同样按折扣入账(按量 & Coding Plan 积分两种口径)?- 当「标签报命中、实际全量重算」(本 issue 发现 2)时,计费以哪个为准?若标签虚高,用户按折扣付费但得不到折扣对应的算力节省,与文档「降低使用成本」的承诺存在偏差。
设备 / 系统 / 环境 · Environment
- 端点:
https://open.bigmodel.cn/api/anthropic/v1/messages(stream,anthropic-version: 2023-06-01) - 模型:glm-5.3-flash(主测)、glm-5.3(复测一致);带/不带
context-1m-2025-08-07beta 行为一致 - 客户端:macOS + Python 3 标准库 urllib 直连(无代理、无中转、无 header 注入);真实会话数据来自 Claude Code 本地 transcript(timestamp + usage 逐请求分析)
复现脚本
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# GLM /api/anthropic 隐式 prompt cache 行为探针(仅标准库)
# 用法: export GLM_KEY=xxx && python3 glm_anthropic_cache_probe.py [model]
import json
import os
import sys
import time
import urllib.request
BASE = os.environ.get("GLM_BASE", "https://open.bigmodel.cn/api/anthropic")
KEY = os.environ.get("GLM_KEY", "")
MODEL = sys.argv[1] if len(sys.argv) > 1 else "glm-5.3-flash"
BIG = ("这是一段用于测试前缀缓存行为的中文填充文本,内容与任何真实任务无关,"
"仅用于占据上下文长度。" * 16000)[:160000]
def call(label, messages, system=None):
payload = {"model": MODEL, "max_tokens": 40, "stream": True, "messages": messages}
if system:
payload["system"] = system
headers = {
"Content-Type": "application/json",
"x-api-key": KEY,
"Authorization": "Bearer " + KEY,
"anthropic-version": "2023-06-01",
}
req = urllib.request.Request(BASE.rstrip("/") + "/v1/messages",
data=json.dumps(payload).encode(), headers=headers)
t0 = time.time()
ttft = None
usage = {}
with urllib.request.urlopen(req, timeout=300) as resp:
for raw in resp:
line = raw.decode("utf-8", "replace").strip()
if not line.startswith("data:"):
continue
try:
d = json.loads(line[5:])
except Exception:
continue
if d.get("type") == "content_block_delta" and ttft is None:
ttft = time.time() - t0
if d.get("type") == "message_start":
usage.update((d.get("message") or {}).get("usage") or {})
if d.get("type") == "message_delta":
usage.update(d.get("usage") or {})
print(f"[{label:20s}] TTFT={ttft:5.1f}s input={usage.get('input_tokens', '-'):>7}"
f" cache_read={usage.get('cache_read_input_tokens', '-'):>7}"
f" cache_write={usage.get('cache_creation_input_tokens', '-')}",
flush=True)
def cc(text):
return {"type": "text", "text": text, "cache_control": {"type": "ephemeral"}}
print(f"model={MODEL} prefix≈92k tokens\n")
print("== S1 完全相同请求 ×3(缓存可用性基线)==")
msgs = [
{"role": "user", "content": [cc(BIG), {"type": "text", "text": "请记住以上资料,稍后提问。"}]},
{"role": "assistant", "content": "好的,已记住。"},
{"role": "user", "content": "一句话回答:1+1等于几?"},
]
for i in range(3):
call(f"S1-identical-{i+1}", msgs)
print("\n== S2 append-only 追加对话 ×4(Claude Code 标准形态)==")
m = [{"role": "user", "content": [cc(BIG), {"type": "text", "text": "请记住以上资料,稍后提问。"}]}]
call("S2-round1", m, system="你是一个简洁的测试助手,请用一句话回答。")
for i, q in enumerate(["第一个问题:随便答。", "第二个问题:再随便答。", "第三个问题:继续随便答。"]):
m += [{"role": "assistant", "content": "好的。"}, {"role": "user", "content": [cc(q)]}]
call(f"S2-round{i+2}-append", m, system="你是一个简洁的测试助手,请用一句话回答。")
print("\n== S3 system 大前缀 ×2(system 是否参与缓存)==")
for i in range(2):
call(f"S3-system-{i+1}", [{"role": "user", "content": "一句话回答:1+1等于几?"}],
system=[cc(BIG)])
print("\n== S4 无 cache_control 的追加对话 ×3(隐式缓存对 append 的行为)==")
m = [{"role": "user", "content": BIG + "\n请记住以上资料,稍后提问。"}]
call("S4-round1", m)
for i in range(2):
m += [{"role": "assistant", "content": "好的。"}, {"role": "user", "content": f"第{i+1}个问题:随便答。"}]
call(f"S4-round{i+2}-append", m)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the attached glm_anthropic_cache_probe.py script and run its S1–S4 scenarios against /api/anthropic/v1/messages, comparing TTFT with cache_read_input_tokens. There are no repository files or tests named in the issue; done means determining the cache behavior's cause and making append-only and system prefixes report and achieve real reuse, with accurate usage labels.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- api, performance
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Needs clarification
- Newbie friendliness
- 35/100