# 提供方适配层

> transform.ts 如何按提供方规整消息、打缓存断点、设定采样与推理参数、清洗工具 schema、限定输出上限，以及 error.ts 如何识别上下文溢出。

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

[上一篇](https://daiw.org/manual/opencode/provider-catalog)拿到了 `LanguageModelV3`，可同一段会话发给 Anthropic、Gemini、Mistral 或 DeepSeek，各家对消息格式、参数名和 JSON Schema 的要求都不一样。V1 把这些差异收进 `packages/opencode/src/provider/transform.ts`：约 1900 行纯函数，判断依据只有三样——`model.api.npm`（用哪个 SDK 包）、`model.providerID` 和 `model.api.id`。

## 调用点地图

| 导出 | 调用方 | 作用 |
| --- | --- | --- |
| `options()`、`smallOptions()` | `session/llm/request.ts` | 默认提供方选项 |
| `temperature()`、`topP()`、`topK()` | 同上 | 采样默认值 |
| `maxOutputTokens()` | 同上，以及 `session/overflow.ts` | 输出上限与压缩阈值 |
| `message()`、`providerOptions()` | `session/llm.ts`、`session/llm/native-runtime.ts` | 消息规整、选项命名空间 |
| `schema()` | `session/tools.ts` | 工具参数 schema 清洗 |
| `variants()`、`reasoningVariants()` | `provider/provider.ts` | 推理档位表 |

在 AI SDK 路径上，`message()` 以 `transformParams` 中间件的形式接入。

## 选项的四层叠加

一次请求的提供方选项在 `session/llm/request.ts:80` 组装：

```ts
const variant =
  !input.small && input.model.variants && input.user.model.variant
    ? input.model.variants[input.user.model.variant]
    : {}
const base = input.small
  ? ProviderTransform.smallOptions(input.model)
  : ProviderTransform.options({
      model: input.model,
      sessionID: input.sessionID,
      providerOptions: input.provider.options,
    })
const options = mergeOptions(mergeOptions(mergeOptions(base, input.model.options), input.agent.options), variant)
```

优先级由低到高：`transform.ts` 的默认值、模型配置的 `options`、Agent 的 `options`、用户选中的 variant；之后插件还能在 `chat.params` 钩子里改掉任意一项。标题生成这类 `small` 请求不叠 variant，改用 `smallOptions()`。

`options()` 的默认值里有几条值得记住：OpenAI 系（含 Azure、Copilot、xAI）一律 `store: false`；OpenAI、Azure、xAI、Mistral 等把会话 ID 作为 `promptCacheKey`；GPT-5 系列默认 `reasoningEffort: "medium"`，在 OpenAI 原生包上还要求返回加密推理内容——服务端不存状态，多轮推理就靠它接续。

## 消息规整：一张踩坑清单

`message()` 先把模型不支持的附件（按 MIME 判断图片、音频、视频、PDF）换成一段 `ERROR: Cannot read ...` 文本，让模型转告用户，而不是让请求失败。然后处理各家禁忌：

| 对象 | 处理 |
| --- | --- |
| 所有消息 | 孤立的 UTF-16 代理项替换成 U+FFFD |
| `@ai-sdk/anthropic` | 删掉空文本与空推理块，Anthropic 拒绝空内容 |
| Claude 模型 | 工具调用 ID 里的非法字符改成下划线 |
| Mistral 系 | 工具调用 ID 压成 9 位字母数字；tool 消息后紧跟 user 消息时插一条 `Done.` |
| DeepSeek | 没有推理块的 assistant 消息补一个空推理块 |
| 声明 `interleaved` 字段的模型 | 推理文本移进 `reasoning_content` 之类的专用字段回传 |

接着 `applyCaching()` 给 Anthropic 系打提示缓存断点：

```ts
function applyCaching(msgs: ModelMessage[], model: Provider.Model): ModelMessage[] {
  const system = msgs.filter((msg) => msg.role === "system").slice(0, 2)
  const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
  // ...
}
```

（`transform.ts:358`，删减后）

前两条 system 消息加最后两条非 system 消息，最多四个断点，与 Anthropic 每次请求最多四个缓存断点的限制相符。标记对象同时写了 `anthropic`、`bedrock`、`openaiCompatible`、`copilot` 等键，由实际使用的 SDK 各取所需；若合并后的选项里已有 `cacheControl`（交给 `@ai-sdk/anthropic` 自动缓存），这一步跳过。

## 采样与推理参数

采样默认值是按模型 ID 匹配的经验值：Claude 不设温度；Gemini 2.5、3 系列的部分型号用 `temperature` 1.0、`topP` 0.95、`topK` 64；`kimi-k2` 思考版 1.0、非思考版 0.6。温度只在目录声明支持时发送，Agent 配置可以覆盖。

推理参数最繁琐。同一个“高档推理”，各 SDK 写法各不相同：

| SDK 包 | 档位的选项片段 |
| --- | --- |
| `@ai-sdk/openai` | `reasoningEffort`，外加 `reasoningSummary: "auto"` 与加密推理 `include` |
| `@ai-sdk/anthropic`（Claude 4.6 起） | `thinking: { type: "adaptive" }` 加 `effort` |
| `@ai-sdk/anthropic`（更早） | `thinking` 里的 `budgetTokens` |
| `@ai-sdk/google` | `thinkingConfig` 的 `thinkingLevel` 或 `thinkingBudget` |
| `@openrouter/ai-sdk-provider` | `reasoning: { effort }` |

哪些档位可用也有讲究：OpenAI 的 `none`、`xhigh` 是后加的，旧模型收到会返回 400，所以代码按目录中的发布日期门控（`transform.ts:583` 的注释写明了原因）。数据驱动的 `reasoningVariants()` 则直接读 models.dev 的 `reasoning_options`，手写规则只在目录没给数据时兜底。

`anthropicBlockBinding()` 上方的注释（`transform.ts:700`）值得一读：

> Fable 5.1 binds each thinking signature to the system prompt, tool list, and messages above it, and rejects the request when any of that changes. opencode re-renders parts of that prefix between turns (system prompt, tools, compaction), so ask the API to drop the affected blocks instead of failing the request.

新一代 Claude 把思考签名绑定到它之前的整段前缀，而 OpenCode 在轮次之间会重渲染系统提示、工具表，还会做[上下文压缩](https://daiw.org/manual/opencode/compaction)，前缀必然变化。所以对 Claude 5.1 及以上（Mythos 5.1 除外）默认加上 `prefixMismatchBehavior: "drop_block"`，让 API 丢弃失效的思考块而不是拒绝请求；配置 `blockBinding: false` 可以关掉。

## 工具 schema 与输出上限

`schema()` 在注册工具时调用（`session/tools.ts:98`）：OpenAI、Azure 走 `sanitizeOpenAISchema()`，照着 Codex 的 Rust 实现做兼容降级，只保留有限的关键字、布尔 schema 改成 `{ type: "string" }`，`request.ts` 还给这一系的工具统一设 `strict: false`，让 MCP 送来的不规范 schema 也能注册；Moonshot、Kimi 删掉 `$ref` 节点的兄弟关键字；Gemini 把整数枚举转成字符串，把 `type` 数组拆成 `anyOf`。

输出上限由 `maxOutputTokens()`（`transform.ts:1468`）给出：目录值与常量 `OUTPUT_TOKEN_MAX`（32000）取小，目录写 0 时回落到 32000，环境变量 `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` 可以替换常量。`session/overflow.ts` 也用它预留输出空间，算出[上下文压缩](https://daiw.org/manual/opencode/compaction)的触发阈值。插件可以在 `chat.params` 里把它清成 `undefined`，`openai` 提供方和 Copilot 上的 GPT 模型就是这样处理的。

## error.ts：把失败翻译成会话能处理的类型

`packages/opencode/src/provider/error.ts` 不到两百行，却决定了失败之后会话怎么走。上一篇 fetch 包装抛出的 `HeaderTimeoutError`、`ResponseStreamError` 被映射成可重试的 `APIError`；AI SDK 的 `APICallError` 则交给：

```ts
export function parseAPICallError(input: { providerID: ProviderV2.ID; error: APICallError }): ParsedAPICallError {
  const m = message(input.providerID, input.error)
  const body = json(input.error.responseBody)
  if (isContextOverflow(m) || input.error.statusCode === 413 || body?.error?.code === "context_length_exceeded") {
    return {
      type: "context_overflow",
      message: m,
      responseBody: input.error.responseBody,
    }
  }
  // ...
}
```

（`error.ts:172`，删减后）

`message()` 先从响应体挖出 `message`、`error` 字段，遇到网关返回的 HTML 页面就为 401、403 换上可读提示。上下文溢出有三条判据：HTTP 413、`context_length_exceeded` 错误码，以及 `@opencode-ai/llm` 导出的 `isContextOverflow()`——它用二十多条正则匹配各家的溢出措辞，并排除限流类消息。另外，`openai` 开头的提供方返回 404 也算可重试（注释说 OpenAI 偶尔会对实际可用的模型返回 404）。

这些结果在 `session/message-v2.ts` 里变成 `ContextOverflowError` 或 `APIError`：前者从不重试，[处理器](https://daiw.org/manual/opencode/processor)把它转成“需要压缩”的信号；后者交给重试逻辑，5xx 即便 SDK 没标可重试也会重试。

下一篇：[认证与提供方插件](https://daiw.org/manual/opencode/auth-and-plugins)——API key 与 OAuth 凭据存在哪里，GitHub Copilot、ChatGPT 订阅等登录方式如何经插件钩子注入请求。
