# 三种 API 流 · 一套词汇统一三家后端

> grok 同时支持 OpenAI Chat Completions、Anthropic Messages、OpenAI Responses 三种 API 格式。这一章看它怎么把三种迥异的流，归一成同一套 SamplingEvent 与中立会话模型——这正是「换个模型就能跑」的底层原因。

- 作者：David（道雾轩）
- 专栏：Grok Build 源码解读（https://daiw.org/manual/grok-build.md）
- 最后更新：2026-09-15
- 原文：https://daiw.org/manual/grok-build/api-streams
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

# 三种 API 流 · 一套词汇统一三家后端

grok 不绑死某一家模型。你可以用它连 xAI 自己的模型，也可以 [自定义模型](https://daiw.org/manual/grok-build/custom-models)接 Ollama、OpenAI 兼容端点、Anthropic。能做到这点，靠的是采样器把**三种 API 格式**统一成了一套内部词汇。

## 三种后端

`ApiBackend` 枚举（`sampling-types/types.rs`）三选一：

| 后端 | 对应 | 特点 |
| --- | --- | --- |
| `ChatCompletions` | OpenAI Chat Completions（默认） | 最通用，多数兼容端点走这条 |
| `Messages` | Anthropic Messages | 块结构（Text/ToolUse/Thinking 分块），缓存字段独立 |
| `Responses` | OpenAI Responses / xAI | 最丰富：reasoning、后端托管工具、context 详情 |

## 统一的关键：中立模型 + 同一套事件

不管哪种后端，采样器都把它们收敛到**同一个中立模型**（`sampling-types/conversation.rs`）：

- 请求侧 `ConversationRequest`：items + tools + hosted_tools + 各种 `x-grok-*` 头（外加结构化输出的 JSON schema、`prompt_cache_key` 等字段）。
- 响应侧 `ConversationResponse`：items + stop_reason + usage + cost，外加服务端报来的 doom-loop 信号。
- 历史单元 `ConversationItem`：`System / User / Assistant / ToolResult / BackendToolCall / Reasoning` 的扁平有序列表。

中立模型到各家线上格式的请求构造，拆在同目录 `conversation/` 下的 `chat_completions.rs`、`messages.rs`、`responses.rs` 里。

每种后端有一个**流变换**（`stream/{chat_completions,messages,responses}.rs`），都是 `async_stream::stream!` 生成器，把该后端五花八门的 chunk 归一成同一套 `SamplingEvent`，并保证**恰好一个终止事件**（`Completed` 或 `Failed`）。所以循环上层根本不关心底下是哪家 API。

```mermaid
flowchart LR
  subgraph L1[L1 客户端]
    CC[chat_completions chunk]
    MS[messages event]
    RS[responses event]
  end
  CC --> T1[stream_chat_completions]
  MS --> T2[stream_messages]
  RS --> T3[stream_responses]
  T1 --> EV[统一 SamplingEvent 流]
  T2 --> EV
  T3 --> EV
  EV --> A[会话循环只认这一套]
```

## 魔鬼在 token 账里

三家把「用了多少 token」报得都不一样，采样器得替它们对齐（这直接关系到[压缩](https://daiw.org/manual/grok-build/compaction)何时触发、[成本](https://daiw.org/manual/grok-build/telemetry-usage)怎么算）：

- **Anthropic** 把缓存分开报：`input_tokens`（未缓存）+ `cache_read_input_tokens` + `cache_creation_input_tokens`，采样器把三者相加才是完整 prompt token。
- **Responses** 的 `total_tokens` 被 SSE 解码器改写成「当前上下文实际长度」（`context_details`），而计费字段保持累计——因为 async-openai 的类型建模不了 xAI 这些扩展字段，grok 用**原始 JSON 注入 + 宽容解析**把它们接回来。
- **成本**：xAI 在 `usage.cost_in_usd_ticks` 里报费用（1 美元 = 10¹⁰ tick）。async-openai 的类型化 Response 事件没有这个字段，解码器就从原始 JSON 读出，塞进 `response.metadata` 的自定义 key（`xai.cost_usd_ticks`）捎给流变换。

## 两个值得学的设计

- **字节稳定的历史顺序**：`ConversationItem` 刻意保留 `[Reasoning, tool_call, …, message]` 的交错顺序，且跨轮**字节稳定**——这样服务端的 KV 前缀缓存才能命中，省钱省延迟。
- **prompt 缓存**：走 Anthropic 时，会话→Messages 的构造（`conversation/messages.rs` 的 `apply_cache_breakpoints`）会打三个 `cache_control: ephemeral` 断点——最后一个 system 块、最新一条消息的末尾块、上一次请求结束的位置。只标 system prompt 的话，整段对话前缀都进不了缓存；第四个断点位则刻意留给开了自动缓存的网关（API 最多只收四个）。走 Responses 时改用 `prompt_cache_key` 做粘性路由。两者都对应 llm-to-agent 的 [prompt-caching](https://daiw.org/manual/llm-to-agent/prompt-caching)。

## 和《从 LLM 到 Coding Agent》对照

那本的 [raw-llm-api](https://daiw.org/manual/llm-to-agent/raw-llm-api) 讲了「一种」API 的请求响应。grok 的增量是：真实产品要同时伺候三家，于是多了一层「中立模型 + 每后端一个变换」的翻译层。这层翻译，就是「一份 agent 逻辑，换任意模型都能跑」的代价与红利。

---

上一篇：[采样器 · actor 模型](https://daiw.org/manual/grok-build/sampler-actor) · 下一篇：[流式解析与事件](https://daiw.org/manual/grok-build/streaming-events)
