TensorZero 模型提供商架构与 Prompt Caching 支持指南:从 AGENTS.md 到 cache.rs 的源码级解析
【免费下载链接】tensorzeroTensorZero is an open-source LLMOps platform that unifies an LLM gateway, observability, evaluation, optimization, and experimentation.项目地址: https://gitcode.com/GitHub_Trending/te/tensorzero
TensorZero 作为一个统一 LLM 网关、可观测性、评测与优化能力的 LLMOps 平台,其核心引擎tensorzero-core需要对接 OpenAI、Anthropic、AWS Bedrock、Google Gemini、DeepSeek 等数十家模型提供商,而每家提供商的用量(Usage)与缓存(Prompt Caching)上报格式又各不相同。本文以 crates/tensorzero-core/src/providers/AGENTS.md 为骨架,深入解析 TensorZero 如何通过独立的tensorzero-types-providerscrate 管理提供商线格式类型,并以 crates/tensorzero-types-providers/src/cache.rs 作为"单一事实来源"统一各提供商的缓存 token 映射。读完本文,你将掌握 TensorZero 提供商模块的架构分层、全部提供商缓存字段的映射关系、从 ProviderUsage 到内部 Usage 的转换链路,以及为新增提供商接入缓存支持并运行 e2e 验证的完整流程。
一、提供商模块的组织方式与迁移策略
在 TensorZero 的源码树中,所有与外部模型提供商通信的实现代码位于 crates/tensorzero-core/src/providers,其中每个文件或子目录对应一个(或一组)提供商:
anthropic.rs、aws_bedrock.rs、aws_sagemaker.rs、azure.rs、deepseek.rs、dummy.rs、groq.rs、mistral.rs、openrouter.rs、tgi.rs、together.rs、vllm.rs、xai.rs、hyperbolic.rs、sglang.rs- 目录形式的有
fireworks/、gcp_vertex_gemini/、openai/,以及gcp_vertex_anthropic.rs、google_ai_studio_gemini.rs等 - 另有
chat_completions.rs、aws_common.rs、helpers.rs、helpers_thinking_block.rs等公共辅助模块,以及test_helpers.rs测试辅助
providers/AGENTS.md 首先给出的架构约定是:与模型提供商 API 通信相关的类型正在逐步迁移到一个独立的 crate——tensorzero-types-providers。其迁移原则非常明确:
如果你要创建新类型(例如
MyProviderUsage),优先在tensorzero-types-providers下创建,再通过 import 引用,而不是直接放进tensorzero-core。
这套策略的动机可以从 crate 的文档注释中读出:tensorzero-types-providers是一个纯 serde 类型库,只负责"外部提供商 API 的线上格式类型"(wire format types),见 crates/tensorzero-types-providers/src/lib.rs。把请求/响应的线格式类型与请求发送、流式解析、重试等运行时逻辑解耦,好处是:
- 编译边界清晰:改一个提供商的新字段,不会牵连
tensorzero-core中大量业务逻辑重编译; - 可独立测试:serde 反序列化、字段映射可以在无网关运行时环境下单独验证(例如 crates/tensorzero-types-providers/src/conversions.rs 内就内嵌了大量转换单测);
- 方便未来被其他 crate 复用(如
tensorzero-node等绑定层)。
当前该 crate 的模块清单见 lib.rs:aws_bedrock、cache、conversions、deepseek、fireworks、groq、mistral、openai、openrouter、serde_util、together、xai,覆盖了 OpenAI 兼容系、Anthropic 系、Gemini 系、DeepSeek、Fireworks、Mistral 等主流提供商,与tensorzero-core/src/providers下的实现文件一一对应。
二、Prompt Caching 的"单一事实来源":cache.rs
AGENTS.md 给出的第二条、也是最重要的架构约定是:
关于prompt caching 支持,请参见
tensorzero-types-providers/src/cache.rs。它是"哪些提供商支持缓存、它们的 API 字段名是什么、如何映射到 TensorZero 内部Usage结构体"的单一事实来源(single source of truth)。在为一个提供商新增或修改缓存 token 支持时,请遵循其中的清单(checklist)。
cache.rs自述为"文档专用模块"(documentation-only),实际缓存相关类型仍定义在各提供商模块中(如openai::OpenAIPromptTokensDetails),但它承载了全部映射决策的权威描述,见 crates/tensorzero-types-providers/src/cache.rs。
2.1 内部 Usage 的两个缓存字段
TensorZero 内部统一的用量结构体Usage定义在 crates/tensorzero-types/src/usage.rs,除input_tokens、output_tokens、cost外,与缓存相关的字段只有两个:
| 字段 | 含义 |
|---|---|
provider_cache_read_input_tokens | 从缓存中读取(命中)的 token 数,成本更低 |
provider_cache_write_input_tokens | 写入缓存的 token 数,可能成本更高 |
这两个字段都声明为Option<u32>且带#[serde(default, skip_serializing_if = "Option::is_none")],序列化时省略。它们遵循一套严格的空值语义:
None= 该提供商不报告这一指标(可能完全不支持缓存,也可能支持但响应里没带);Some(0)= 提供商支持缓存,但本次请求没有任何 token 命中缓存。
这一点在Usage::zero()中体现得很清楚:核心字段归零,但两个缓存字段初始化为None("未报告"),因为不是所有提供商都支持 prompt caching;聚合工具函数会保留遇到的任何Some值,而不是被None污染,见 crates/tensorzero-types/src/usage.rs。
三、各提供商的缓存字段映射全表
cache.rs按提供商响应的"格式族"分组,给出了完整的字段来源映射。下面按类别展开,并结合仓库源码逐一印证。
3.1 OpenAI 兼容系:prompt_tokens_details.cached_tokens
OpenAI、Groq、xAI、OpenRouter 都遵循 OpenAI chat completions 响应格式,统一复用OpenAIPromptTokensDetails结构体。该结构体定义于 crates/tensorzero-types/src/usage.rs,仅有cached_tokens: Option<u32>一个字段,并被 re-export 到 crates/tensorzero-types-providers/src/openai.rs 供各兼容提供商使用。
| 提供商 | cache_read来源 | cache_write来源 | 机制 |
|---|---|---|---|
| OpenAI | prompt_tokens_details.cached_tokens | — | 自动(>= 1024 tokens) |
| Groq | prompt_tokens_details.cached_tokens | — | 自动 |
| xAI | prompt_tokens_details.cached_tokens | — | 自动 |
| OpenRouter | prompt_tokens_details.cached_tokens | — | 取决于底层提供商 |
注意这一类提供商只报告读缓存(cache_read),cache_write一律为None。转换逻辑在 crates/tensorzero-types-providers/src/conversions.rs:provider_cache_read_input_tokens取自prompt_tokens_details.and_then(|d| d.cached_tokens),provider_cache_write_input_tokens恒为None。OpenAI 系转换的完整实现可对照 crates/tensorzero-types/src/usage.rs 中的From<OpenAIUsage> for Usage。
3.2 Anthropic 格式系:显式缓存控制
Anthropic 与 GCP Vertex Anthropic 使用cache_control显式标记缓存点,响应中同时给出读写两个字段;AWS Bedrock 使用cachePoint显式标记,字段名改为驼峰式。
| 提供商 | cache_read来源 | cache_write来源 | 机制 |
|---|---|---|---|
| Anthropic | cache_read_input_tokens | cache_creation_input_tokens | 显式(cache_control) |
| GCP Vertex Anthropic | cache_read_input_tokens | cache_creation_input_tokens | 显式(cache_control) |
| AWS Bedrock | cacheReadInputTokenCount | cacheWriteInputTokenCount | 显式(cachePoint) |
以 Anthropic 为例,其用法结构体中的cache_creation_input_tokens与cache_read_input_tokens均为Option<u32>,在From<AnthropicUsage> for Usage中直接一对一映射:provider_cache_read_input_tokens = cache_read_input_tokens、provider_cache_write_input_tokens = cache_creation_input_tokens,见 crates/tensorzero-core/src/providers/anthropic.rs。同文件内还有大量针对该映射的单测(如cache_creation_input_tokens: Some(100)与cache_read_input_tokens: Some(200)映射到内部两个字段的断言,见 anthropic.rs),并且严格区分了Some(0)与None两种语义(见 anthropic.rs)。
AWS Bedrock 的线格式类型定义在 crates/tensorzero-types-providers/src/aws_bedrock.rs,字段为cache_read_input_tokens/cache_write_input_tokens(Option<i32>),经 serde 驼峰化后即文档表格中的cacheReadInputTokenCount/cacheWriteInputTokenCount;在 crates/tensorzero-core/src/providers/aws_bedrock.rs 的转换中同样映射到内部Usage的读写字段(非流式与流式分支均有对应处理)。
3.3 Google Gemini 系:cachedContentTokenCount
| 提供商 | cache_read来源 | cache_write来源 | 机制 |
|---|---|---|---|
| GCP Vertex Gemini | usageMetadata.cachedContentTokenCount | — | 隐式(2.5+)/ 显式(CachedContent API) |
| Google AI Studio Gemini | usageMetadata.cachedContentTokenCount | — | 隐式(2.5+)/ 显式(CachedContent API) |
Gemini 系只报告 cache_read,不报告 cache_write。源码注释印证了这一字段名:GCP Vertex Gemini 的实现注释写明"Gemini 将缓存内容 token 报告为cachedContentTokenCount"(见 crates/tensorzero-core/src/providers/gcp_vertex_gemini/mod.rs),Google AI Studio 的实现注释也完全一致(见 crates/tensorzero-core/src/providers/google_ai_studio_gemini.rs)。
cache.rs还特别记录了一个重要的现实差异(截至文档标注的 2026 年 3 月):
- GCP Vertex Gemini(
aiplatform.googleapis.com)在缓存命中时会返回cachedContentTokenCount,但只是机会性(opportunistically)返回——即使 prompt 完全相同也不保证一定出现; - Google AI Studio(
generativelanguage.googleapis.com)完全不会返回该字段; - 两个提供商都能在字段存在时正确解析它。
这一提示对依赖 Gemini 缓存指标做成本核算的用户至关重要:字段缺失不代表缓存未生效,只是提供商未上报。
3.4 DeepSeek:独特的顶层字段格式
DeepSeek 不使用 OpenAI 的prompt_tokens_details,而是在 usage 顶层直接暴露两个缓存字段:
| 提供商 | cache_read来源 | cache_write来源 | 机制 |
|---|---|---|---|
| DeepSeek | prompt_cache_hit_tokens | prompt_cache_miss_tokens | 自动 |
对应的线格式类型DeepSeekUsage定义在 crates/tensorzero-types-providers/src/deepseek.rs,其文档注释明确指出"DeepSeek 的 usage 使用顶层prompt_cache_hit_tokens和prompt_cache_miss_tokens,而不是标准的 OpenAIprompt_tokens_details.cached_tokens"。字段语义上,prompt_cache_hit_tokens是从自动缓存中读取的 token,prompt_cache_miss_tokens是不在缓存中、将供后续请求写入的 token。转换时二者分别映射到provider_cache_read_input_tokens与provider_cache_write_input_tokens,见 crates/tensorzero-types-providers/src/conversions.rs。这是全表中唯一将"缓存未命中(miss)token"直接视为 write 来源的映射,与 DeepSeek 官方计费模型一致。
3.5 Fireworks:缓存信息在 HTTP 响应头而非 JSON 体
| 提供商 | cache_read来源 | cache_write来源 | 机制 |
|---|---|---|---|
| Fireworks | HTTP 头fireworks-cached-prompt-tokens | — | 自动 |
Fireworks 是最特殊的映射:缓存 token 数不写在 JSON body 里,而是放在 HTTP 响应头fireworks-cached-prompt-tokens中。tensorzero-core中负责提取该头的函数是extract_fireworks_cached_prompt_tokens(见 crates/tensorzero-core/src/providers/fireworks/mod.rs),它会读取响应头并解析为Option<u32>,随后赋值给usage.provider_cache_read_input_tokens(见 fireworks/mod.rs)。该函数自带单元测试,覆盖了"无该头返回 None"、"fireworks-cached-prompt-tokens: 42解析为 Some(42)"以及"非法值返回 None"三种场景(见 fireworks/mod.rs)。
3.6 Mistral
| 提供商 | cache_read来源 | cache_write来源 | 机制 |
|---|---|---|---|
| Mistral | prompt_tokens_details.cached_tokens | — | 自动 |
Mistral 走 OpenAI 兼容格式,复用OpenAIPromptTokensDetails,只报告 cache_read。
3.7 尚未在 JSON 响应中暴露缓存的提供商
| 提供商 | 说明 |
|---|---|
| Together | 透明后端缓存,响应中无 token 计数 |
| Hyperbolic | 不支持 prompt caching |
| vLLM | 按 OpenAI 兼容格式解析prompt_tokens_details.cached_tokens,但并非所有部署都会上报 |
| SGLang | 按 OpenAI 兼容格式解析prompt_tokens_details.cached_tokens,但并非所有部署都会上报 |
对于这四类提供商,cache_read/cache_write通常会落到None——即便底层实际发生了缓存,也因响应未暴露指标而无法统计。这一"诚实上报"的设计避免了把猜测数据写进观测系统。
四、从 ProviderUsage 到内部 Usage 的转换链路
cache.rs的映射表要落地,依赖每个提供商实现的From<ProviderUsage> for Usage转换。这条链路在代码中分为两层:
- 线格式层(
tensorzero-types-providers):定义各提供商响应的 serde 结构体(如OpenAIUsage、DeepSeekUsage、OpenAIPromptTokensDetails),只负责把 JSON 正确反序列化; - 转换层:实现
From<ProviderUsage> for Usage,把线格式字段映射进统一的内部Usage。
对于 OpenAI 兼容系与 DeepSeek,转换集中在 crates/tensorzero-types-providers/src/conversions.rs,且每个转换都配有单测。例如其中针对 xAI 的测试覆盖了"省略prompt_tokens_details时 cache_read 应为 None"与"带cached_tokens时正确取值"两种分支(见 conversions.rs)。
对于 Anthropic 这类非 OpenAI 格式的提供商,转换实现仍保留在tensorzero-core的提供商模块中(如 anthropic.rs)。从代码结构看,这正是 AGENTS.md 所述"逐步迁移"过程中的中间状态:新类型优先放入独立 crate,历史实现按需迁移。
五、新增提供商缓存支持的 5 步清单
cache.rs为开发者提供了一份明确的操作清单(见 crates/tensorzero-types-providers/src/cache.rs),当需要为某个提供商接入或修改缓存 token 支持时,按此顺序执行:
- 更新映射表:在上文对应的分组表格中为提供商添加一行;
- 复用公共类型:如果提供商使用
prompt_tokens_details.cached_tokens格式,直接在它的 usage 结构体中复用OpenAIPromptTokensDetails(从tensorzero-types-providers导入),不要重复定义; - 实现字段映射:在该提供商的
From<ProviderUsage> for Usage实现中,把线格式字段映射到provider_cache_read_input_tokens/provider_cache_write_input_tokens,并遵守None(不报告)与Some(0)(支持但零命中)的语义; - 接入 e2e 测试:在 e2e 测试配置中把该提供商加入
cache_input_tokens_inference列表; - 运行缓存 token 的 e2e 测试验证:跑通非流式与流式两条用例。
六、e2e 如何验证缓存 token 映射
缓存映射不是只靠单元测试,TensorZero 还为每个提供商跑了真实的端到端验证。测试配置层面,每个提供商的 e2e 测试都会定义cache_input_tokens_inference: Vec<E2ETestProvider>字段(见 crates/tensorzero-core/tests/e2e/providers/common.rs),例如 OpenAI、Anthropic、Groq、xAI、Mistral、DeepSeek、Fireworks、Together、Hyperbolic、vLLM、SGLang、Azure、GCP Vertex Gemini、Google AI Studio 等都在各自测试文件中填充了该列表(如 mistral.rs、deepseek.rs、fireworks.rs 等)。而像 SageMaker + TGI 这种"输入不够大且不支持缓存"的组合,则显式留空并注释原因(见 aws_sagemaker_tgi.rs)。
测试宏会把该列表注入两个用例:test_cache_input_tokens_non_streaming与test_cache_input_tokens_streaming(见 common.rs),分别调用 commonv2/cache_input_tokens.rs 中的test_cache_input_tokens_non_streaming_with_provider与流式版本。
以非流式用例为例,其验证策略(见 cache_input_tokens.rs)非常直观:
- 两次推理,共享同一大段 system prompt(
LARGE_SYSTEM_PROMPT被内嵌进测试,见 cache_input_tokens.rs),但 user 消息不同,以确保代理服务器记录两次独立响应; - 第一次请求触发缓存写入(cache write),第二次请求命中缓存(cache read);
- 断言两次请求的
input_tokens都大于 4000,证明缓存 token 被计入 input_tokens(若未计入,首次写入请求的 token 数不会这么大,见 cache_input_tokens.rs 的断言注释); - 首次请求的缓存字段只记录不硬断言——因为自动缓存类提供商(OpenAI、Groq、xAI)在首次写入时可能不返回
prompt_tokens_details,而显式缓存类提供商(Anthropic、Bedrock)会返回cache_write;第二次请求则必须报告cache_read。
测试还通过cache_control_extra_body()(见 cache_input_tokens.rs)按模型名注入所需的显式缓存标记:Anthropic / GCP Vertex Anthropic 使用/system/0/cache_control指针注入{"type": "ephemeral"},AWS Bedrock(claude-haiku-4-5、deepseek-r1、nova-lite-v1)使用/system/-指针注入{"cachePoint": {"type": "default"}};而自动缓存的 OpenAI、Groq、xAI 不需要任何标记也能跑通测试(见 cache_input_tokens.rs 的注释)。
七、实践要点与小结
结合 AGENTS.md 与 cache.rs,可以总结出 TensorZero 提供商缓存支持的三条核心实践准则:
- 以 cache.rs 为唯一权威:任何关于"哪个提供商支持缓存、字段叫什么、映射到哪"的改动,都必须先更新 crates/tensorzero-types-providers/src/cache.rs 中的映射表,保证文档与实现不漂移;
- 新类型进独立 crate:新建提供商相关类型一律放入
tensorzero-types-providers(参考 lib.rs 的模块组织),并尽量复用OpenAIPromptTokensDetails这类公共线格式类型; - 用 e2e 兜底:字段映射是否正确,最终以 commonv2/cache_input_tokens.rs 的非流式/流式用例为准,新增提供商必须加入
cache_input_tokens_inference列表并跑通验证。
这套"独立类型 crate + 单一事实来源映射表 + 转换实现 + e2e 验证"的四层结构,既保证了 TensorZero 对多提供商差异的兼容性,也让新增提供商成为一项有明确清单、可验证、可追踪的工程任务。对于希望为自家私有化模型或新厂商接入 TensorZero 的开发者,这份清单就是最直接的入手指南。
【免费下载链接】tensorzeroTensorZero is an open-source LLMOps platform that unifies an LLM gateway, observability, evaluation, optimization, and experimentation.项目地址: https://gitcode.com/GitHub_Trending/te/tensorzero
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考