# 模型适配层

> adapters 包的 model 目录怎样用 Vercel AI SDK 的三个 provider 包对接 anthropic-messages、openai-chat-completions、openai-responses 三种协议，把流式输出归一成运行时事件；推理签名、缓存断点、请求头、超时重试、两个 SDK 补丁和官方套餐网关都落在这一层。

- 作者：David（道雾轩）
- 专栏：ZCode 源码解读（https://daiw.net/manual/zcode.md）
- 最后更新：2026-09-21
- 原文：https://daiw.net/manual/zcode/model-adapters
- 转载与引用：请注明出处并附原文链接（https://daiw.net/about/copyright）

回合循环只认 `@zcode/contracts` 里的 `Model` 接口：`streamText` 吐出统一的 `ModelStreamEvent`，`generateText` 返回一次性结果。把它接到真实模型服务上的，是 `apps/zcode-cli/packages/adapters/src/model`，52 个文件、12327 行，入口的第一行注释就写明“Model adapters backed by the Vercel AI SDK”（`apps/zcode-cli/packages/adapters/src/model/index.ts:1`）。同级的 `adapters/src/provider` 目前只有一行 `export {};`，是个空占位（`apps/zcode-cli/packages/adapters/src/provider/index.ts:2`）。

上一篇讲了 Registry 怎样给出一个模型的全部静态事实；这一篇从 `ModelFactory` 拿到这些事实开始，一直讲到字节离开进程。回合里怎样消费事件、流断了怎样续上，见[一次模型请求：流式、工具并发与恢复](https://daiw.net/manual/zcode/model-step)。

## 分层

装配点在 bootstrap：`createRuntimeAiSdkModelExecutionConfig` 准备默认请求头与网络配置，`createModelAdapter` 构造唯一的 `AiSdkModelAdapter`（`apps/zcode-cli/packages/bootstrap/src/app/create-app.ts:523`、`530`），`ApiProviderModelRuntime` 每次按 Registry 查到的 Provider 与模型调用 `createModel`（`apps/zcode-cli/packages/bootstrap/src/app/provider-registry-model-runtime.ts:65`）。一次流式请求自上而下经过这些层：

```mermaid
flowchart TB
  CORE["core 回合循环<br/>Model.streamText"] --> EM["ExecutableModel<br/>能力与选项校验"]
  EM --> AD["AiSdkModelAdapter<br/>历史投影与逐次鉴权"]
  AD --> RUN["runStreamText<br/>重试、空闲超时、准入"]
  RUN --> OPT["createStreamTextOptions<br/>消息、工具、请求头"]
  OPT --> SDK["ai 包的 streamText"]
  SDK --> PK["三个 provider 工厂之一"]
  PK --> F1["协议兼容 fetch<br/>chat-completions 无此层"]
  F1 --> F2["选项映射 fetch"]
  F2 --> F3["业务错误 fetch"]
  F3 --> F4["官方套餐网关 fetch"]
  F4 --> F5["代理与 CA fetch"]
  F5 --> NET["模型服务"]
```

| 层 | 主要文件 | 做什么 |
| --- | --- | --- |
| `ExecutableModel` | `model.ts` | 校验输出上限与推理档位落在规格内；模型不支持工具、结构化输出、图片、PDF 或视频时，请求发出前就拒绝（`apps/zcode-cli/packages/adapters/src/model/model.ts:106`、`155`） |
| `AiSdkModelAdapter` | `runner.ts` | 绑定 Provider 快照，账号型模型每次尝试前取鉴权材料，anthropic 协议下投影推理历史 |
| `runStreamText` 等 | `runner-stream.ts`、`runner-generate.ts` | 尝试循环：准入、空闲超时、失败分类、退避、状态事件 |
| `createStreamTextOptions` | `runner-options.ts` | 转换消息与工具，合并请求头，关闭 SDK 自带重试 |
| `AiSdkModelExecution` | `model-execution.ts` | 按 API 类型选 provider 工厂，拼好 fetch 链 |

## 三个 SDK 包对应三种协议

依赖声明是 `ai` 6.0.193、`@ai-sdk/anthropic` 3.0.81、`@ai-sdk/openai` 与 `@ai-sdk/openai-compatible` 各一个范围版本（`apps/zcode-cli/packages/adapters/package.json:102`、`112`），锁文件里后两者解析为 3.0.65 与 2.0.60（`pnpm-lock.yaml:141`、`144`）。Registry 的 API 类型先映射成三种 kind（`apps/zcode-cli/packages/adapters/src/model/model-execution.ts:363`），再各自创建工厂（`model-execution.ts:281`）：

```ts
    switch (providerConfig.kind) {
      case "openai": {
        const provider = createOpenAI({
          apiKey,
          baseURL: providerConfig.baseURL,
          fetch: createOpenAIResponsesJsonCompatFetch(optionFetch),
          headers,
        });
        return provider.responses as LanguageModelFactory;
      }

      case "anthropic": {
        const provider = createAnthropic({
          apiKey,
          baseURL: normalizeAnthropicBaseURL(providerConfig.baseURL),
          fetch: createAnthropicCompatFetch(optionFetch),
          headers: withAnthropicAuthorizationHeader(apiKey, headers),
        });
        return provider as LanguageModelFactory;
      }
```

| API 类型 | 工厂 | 这一层额外做的事 |
| --- | --- | --- |
| `anthropic-messages` | `createAnthropic` | 地址不以 `/v1` 结尾就补上，因为 SDK 只在后面拼 `/messages`；除 `x-api-key` 外再补一个 `Authorization: Bearer`，兼容两种都读的网关（`model-execution.ts:388`、`411`） |
| `openai-responses` | `createOpenAI` 的 `responses` | 非流式响应缺 `message.id` 或 `annotations` 时补齐，免得 SDK 整个拒收（`apps/zcode-cli/packages/adapters/src/model/openai-responses-json-compat.ts:50`、`66`） |
| `openai-chat-completions` | `createOpenAICompatible` | 显式打开流式用量 `includeUsage`，结构化输出按模型能力开启，否则 SDK 会把 JSON Schema 静默降级（`model-execution.ts:310`、`313`） |

`@ai-sdk/openai` 只用于 Responses 协议，Chat Completions 一律走 openai-compatible 包。SDK 自己的重试被 `maxRetries: 0` 关掉，重试全部由这一层掌控；`allowSystemInMessages` 打开，因为系统提示词由 core 组装（`apps/zcode-cli/packages/adapters/src/model/runner-options.ts:149`、`150`）。`bindModel` 在创建 Model 时冻结 Provider 快照，之后 Registry 热更新不会让已有 Model 悄悄换地址或协议，请求期只允许替换 API Key 和请求头（`model-execution.ts:170`、`374`）。

账号型模型的鉴权是逐次取的：适配层给每次尝试挂一个 `refreshRuntimeHeadersBeforeAttempt`，闲时模型只认创建时注入的执行作用域来源，其他账号模型走 core 的请求头端口，取不到就以 `ModelRequestAuthMissing` 失败（`apps/zcode-cli/packages/adapters/src/model/runner.ts:164`、`181`）。端口在桌面端经协议向 Host 请求、总时限 180000 毫秒（`apps/zcode-cli/packages/bootstrap/src/zcode-protocol/provider-runtime-headers.ts:16`），独立 CLI 则直接读本地凭据，细节见[账号、Coding Plan 与闲时计划](https://daiw.net/manual/zcode/accounts-plans)。

## 请求头

| 来源 | 请求头 | 出处 |
| --- | --- | --- |
| 进程默认 | `User-Agent: ZCode/<版本>`、`HTTP-Referer`（控制面地址）、`X-Title`、`X-ZCode-App-Version`、`X-Release-Channel`、`X-Client-Language`、`X-Client-Timezone`、`X-ZCode-Agent: glm` | `apps/zcode-cli/packages/bootstrap/src/model-config.ts:47` |
| 运行平台 | `X-Platform`（平台加架构）、`X-Os-Category`、`X-Os-Version` | `apps/zcode-cli/packages/bootstrap/src/runtime-platform-headers.ts:5` |
| 仅 openrouter.ai | `X-OpenRouter-Title: ZCode`、`X-OpenRouter-Categories: programming-app` | `packages/shared/src/openrouter-attribution.ts:1` |
| 每次请求 | `x-request-id`、`x-zcode-trace-id`、`x-zcode-session-type`、`x-session-id`、`x-query-id`；OpenCode Go 另加 `x-opencode-session` | `apps/zcode-cli/packages/adapters/src/model/runner-attribution.ts:16` |
| 仅 anthropic 协议 | 请求体 `metadata.user_id`：含设备 ID 与会话 ID 的 JSON 串 | `apps/zcode-cli/packages/adapters/src/model/anthropic-request-metadata.ts:7` |

`X-Title` 在命令行里是 `Z Code@cli`，进程参数里有 `app-server` 或 `agent-server`（即被桌面端拉起）时是 `Z Code@electron`（`model-config.ts:75`）。进程默认头与 Provider 配置里的自定义头先按不区分大小写的方式合并，后者覆盖前者（`model-execution.ts:205`，`apps/zcode-cli/packages/adapters/src/model/model-request-headers.ts:1`）；每次请求的归因头最后叠上（`runner-options.ts:143`）。`x-zcode-session-type` 取 `main`、`subagent`、`other` 之一，注释说 Coding Plan 服务端用它区分请求来源；工作流子会话一律记为 `other`（`runner-attribution.ts:20`，`apps/zcode-cli/packages/core/src/runtime/methods/model-request-session-type.ts:6`）。会话 ID 与查询 ID 发出前会剥掉内部前缀 `sess_`、`subagent_agent_`、`query_`（`runner-attribution.ts:73`）。写日志与状态事件时，名字里带 authorization、api-key、token、secret、cookie 的头以及 `x-off-peak-ticket-id` 都被替换成 `[redacted]`（`apps/zcode-cli/packages/adapters/src/model/runner-network-headers.ts:3`）。

## 消息与工具的翻译

`toAiSdkMessages` 把 core 的消息翻成 SDK 的 `ModelMessage`（`apps/zcode-cli/packages/adapters/src/model/transform.ts:46`），多数处理只发生在序列化边界，不改会话里存的事实：

- **开头的多段 system**：core 为了缓存断点故意保留多段，openai-compatible 协议下按原顺序合并成一段，因为部分旧式 chat 模板只认一个 system（`apps/zcode-cli/packages/adapters/src/model/system-message-compat.ts:21`）；anthropic 协议下，SDK 把会话中途 system 的纯文本扩成块数组，兼容 fetch 再把它恢复成字符串（`apps/zcode-cli/packages/adapters/src/model/anthropic-stream-compat.ts:28`）。
- **空内容与媒体**：只有附件的用户消息正文为空时补 `(no content)`（`transform.ts:386`）；模型不支持的媒体换成一段文字说明（`apps/zcode-cli/packages/adapters/src/model/media-transform-policy.ts:8`）；视频以 `video/*` 的 file 部件交给打过补丁的 SDK 包（`transform.ts:459`），带视频的工具结果则转成文字，媒体挪到紧随其后的用户消息里（`transform.ts:109`）。
- **工具**：anthropic 协议下关闭 SDK 默认的细粒度工具输入流 `eagerInputStreaming`，注释说不少兼容网关不认这个字段；`strict` 模式只给 Anthropic 首方模型；模型要求 MFJS 形态时，把局部 `$ref` 提升到 `$defs`（`apps/zcode-cli/packages/adapters/src/model/tool-transform.ts:85`、`95`、`232`），规则集只为 Moonshot 站点上的 Kimi K3 打开这一项（`config/provider/zcode-builtin.json:4062`）。
- **原生 WebSearch**：只有 anthropic 协议且模型声明支持时，才编码成 `webSearch_20260209` 服务端工具，`maxUses` 缺省 8，其余情况直接报错（`tool-transform.ts:249`）。WebSearch 工具本身见 [WebFetch 与 WebSearch](https://daiw.net/manual/zcode/web-tools)。

## 提示词缓存：四个断点

放不放、放在哪由 core 决定，适配层只负责搬运：

- 系统提示词最多拼成三段，CLI 前缀、稳定正文、动态段，每段都带 `{ type: "ephemeral" }`（`apps/zcode-cli/packages/core/src/context/builder.ts:230`）。
- 请求消息投影完成后，先清掉所有非 system 消息上的标记，再给最后一条非 system 消息打上；主动压缩的请求不拿压缩提示本身当缓存写入点，标记前移一条，落在它之前的真实上下文上（`apps/zcode-cli/packages/core/src/runtime/helpers/provider-request-messages.ts:292`，`apps/zcode-cli/packages/core/src/runtime/methods/compact-active-helpers.ts:88`）。回合循环刻意在投影之后才打标记，免得被合成的条目抢走锚点（`apps/zcode-cli/packages/core/src/runtime/methods/turn-loop.ts:170`）。
- 适配层把每条消息的 `cacheControl` 原样放进 `providerOptions.anthropic.cacheControl`（`transform.ts:499`），没有为另外两种协议做任何映射。

三段 system 加一条消息，最多正好四个断点。命中情况由 `normalizeUsage` 从 SDK 用量里取出 `cacheReadTokens` 与 `cacheWriteTokens`（`apps/zcode-cli/packages/adapters/src/model/runner-normalization.ts:16`）。系统提示词各段的内容见[系统提示词、上下文与提醒](https://daiw.net/manual/zcode/context-builder)。

## 流式事件归一

`toModelStreamEvent` 把 SDK 的 `TextStreamPart` 逐个翻成运行时事件：`text-*`、`reasoning-*`、`tool-input-*` 改成下划线形式，推理事件带上 `providerMetadata`（签名在里面），`tool-call` 的参数只取 SDK 最终解析的那份，`finish` 带结束原因、归一后的用量和合并了响应体的元数据，识别不了的块只记日志（`runner-normalization.ts:62`）。工具调用还要过 `StreamingToolCallAssembler` 做名称与参数校验（`runner-stream.ts:163`）。

难点在“什么时候还能重试”。一次尝试开始时，SDK 会先吐出一串不含正文的前奏事件；这些事件先暂存，等到第一个真正的输出才一并放行（`apps/zcode-cli/packages/adapters/src/model/stream-retry-boundary.ts:6`）：

```ts
const RETRY_SAFE_PRELUDE_STREAM_EVENT_TYPES = new Set<ModelStreamEvent["type"]>([
  "start",
  "text_start",
  "text_end",
  "reasoning_start",
  "reasoning_end",
  "tool_input_start",
  "tool_input_delta",
  "tool_input_end",
]);

export function isRetrySafePreludeStreamEvent(event: ModelStreamEvent): boolean {
  // SDK 把纯签名转成空 reasoning_delta；将其视为已输出会停止 adapter retry，
  // 而 core 无文本、无完整工具调用时也无法恢复。空 delta 随前奏暂存，成功时保留签名原序释放。
  if (event.type === "reasoning_delta" || event.type === "text_delta") {
    return event.text.length === 0;
  }
  return RETRY_SAFE_PRELUDE_STREAM_EVENT_TYPES.has(event.type);
}
```

在此之前失败，适配层可以换一次物理请求重来，用户看不到半截输出；一旦有可见事件发出就不再重放，交给 core 的流恢复（`apps/zcode-cli/packages/adapters/src/model/runner-stream.ts:1469`）。另外几种“看起来成功”的失败也在这里截住：HTTP 200 的 SSE 里夹带业务错误帧时，fetch 层让流以 `ProviderBusinessError` 出错，再按业务码决定重不重试（`model-execution.ts:815`）；业务码只出现在 finish 块的响应体里时，自然结束后以终止错误抛出，不当成可重试的空响应（`runner-stream.ts:478`）；既无输出也无业务码的空补全最多再试 1 次（`apps/zcode-cli/packages/adapters/src/model/empty-completion-retry.ts:8`）。压缩请求另开 raw 块，从 anthropic 原始事件里提炼 `compact_stream_boundary`，供 core 判断能否降级为非流式请求（`runner-options.ts:153`，`runner-stream.ts:1129`），见[上下文压缩](https://daiw.net/manual/zcode/compaction)。

## 推理内容：签名与回放

推理强度本身由选项映射写进请求体，见[上一篇](https://daiw.net/manual/zcode/provider-config)。难的是把上一轮的思考块原样回传：Anthropic 协议要求 thinking 块带签名，而各家兼容服务的实现参差不齐。

- **收的时候**：有的服务把签名放在 `content_block_start` 里，SDK 只从 `signature_delta` 读，兼容 fetch 在块结束前补一个合成的 `signature_delta`；助手侧冒出来的裸 `tool_result` 块整块丢弃；非流式 JSON 里没签名的 thinking 块删掉，保住正文与用量（`anthropic-stream-compat.ts:141`、`256`、`269`）。
- **发的时候**：只对 anthropic 协议，每个逻辑请求投影一次推理历史（`runner.ts:358`）。其他模型留下的带签名推理删掉，但同一服务的新旧身份视为兼容，例如旧的 `builtin:zai-coding-plan` 与个人、团队 Coding Plan；随后删掉只剩推理的助手消息、结尾的推理和纯空白消息（`apps/zcode-cli/packages/adapters/src/model/reasoning-history-normalization.ts:18`、`31`）。同模型留下的无签名块不删，序列化时带一个空签名（`apps/zcode-cli/packages/adapters/src/model/anthropic-reasoning-metadata.ts:23`）。
- **被拒的时候**：服务端返回 400 且文案指向 thinking 块签名，就只在本次请求的副本里删掉带签名的推理，再额外试一次，不占普通重试预算，也不写回会话历史（`reasoning-history-normalization.ts:76`，`runner-stream.ts:181`）。
- **Responses 协议**：没有 `previousResponseId` 的无状态回放里，丢掉推理的 item 引用，因为部分兼容端点不支持（`transform.ts:300`、`335`）。

## 超时与重试

| 项 | 默认值 | 出处 |
| --- | --- | --- |
| 流空闲超时 | 600000 毫秒，配置键 `modelStream.idleTimeoutMs`；每多重试一次加 30000 | `apps/zcode-cli/packages/contracts/src/config/index.ts:284`、`apps/zcode-cli/packages/adapters/src/model/stream-idle-timeout.ts:5` |
| 重试次数 | 10 次，即最多 11 次请求 | `apps/zcode-cli/packages/adapters/src/model/retry-policy.ts:13`、`28` |
| 退避 | 2000 毫秒起、每次乘 2、封顶 60000，再乘 0.5 到 1 的随机系数 | `retry-policy.ts:14`、`apps/zcode-cli/packages/adapters/src/model/runner-retry.ts:38` |
| 环境变量 | `ZCODE_MODEL_RETRY_MAX_RETRIES`、`ZCODE_MODEL_RETRY_BASE_DELAY_MS`、`ZCODE_MODEL_RETRY_BACKOFF_FACTOR`、`ZCODE_MODEL_RETRY_MAX_DELAY_MS` | `retry-policy.ts:18` |
| 工作流子会话 | 重试不设上限 | `model-request-session-type.ts:21` |
| 闲时排队 | 429 或业务码 3105 按服务端给的等待时间、最多 5 分钟，缺省 60 秒，不占重试预算 | `apps/zcode-cli/packages/adapters/src/model/offpeak-retry.ts:21` |

README 里的默认值与代码一致（`apps/zcode-cli/README.md:248`）。空闲超时由 `readNextWithStreamIdleTimeout` 在每次读事件时计时，超时就中止本次尝试并记为可重试（`stream-idle-timeout.ts:98`）。退避计算如下（`runner-retry.ts:38`）：

```ts
export function calculateRetryDelay(
  retry: ResolvedAiSdkModelRetryOptions,
  attempt: number,
  retryAfterMs?: number,
): number {
  const uncapped = retry.baseDelayMs * retry.backoffFactor ** Math.max(0, attempt - 1);
  const capped = Math.min(uncapped, retry.maxDelayMs);
  // provider 会返回几十秒到数分钟的 retry-after；
  // 旧 60s 上限会把合法限流等待退化为本地短退避。
  if (isReasonableRetryAfterMs(retryAfterMs, uncapped)) {
    return retryAfterMs;
  }

  if (!retry.jitter || capped === 0) {
    return capped;
  }

  return Math.round(capped * (0.5 + Math.random() * 0.5));
}
```

服务端给的 `Retry-After` 不超过 5 分钟、或小于未封顶的指数值时直接采用，可以超过 60 秒的本地封顶（`runner-retry.ts:15`、`223`）。哪些失败可重试由 `classifyModelFailure` 决定：429、529、5xx、超时、网络与代理错误、流空闲超时可以重试，401、403、400、422、TLS 错误和上下文超限不重试（`apps/zcode-cli/packages/adapters/src/model/failure-classifier.ts:183`、`207`、`247`）；供应商业务码另有一张表，例如 3008 到 3010 的并发上限默认不重试，只有工作流流量才重试（`apps/zcode-cli/packages/adapters/src/model/failure-provider-business-codes.ts:108`，`runner-stream.ts:1466`）。每次尝试发出前要向进程级准入端口取票，退避期间把票还回去，所以并发上限约束的是服务端真正看到的在途请求数（`runner-stream.ts:238`、`825`）。

## 两个补丁：让 SDK 认识视频

根 `package.json` 的 `patchedDependencies` 给两个 AI SDK 包打了补丁，第三个补丁针对桌面端的 `@arms/rum-electron` 日志采集，与模型无关（`package.json:87`）。两个 AI SDK 补丁做的是同一件事：在 SDK 把 file 部件转成请求内容的分支里，插入一个 `video/*` 分支。anthropic 补丁把它转成与图片块同构的 `video` 块（`patches/@ai-sdk__anthropic@3.0.81.patch:5`）：

```diff
@@ -2265,6 +2265,19 @@ async function convertToAnthropicMessagesPrompt({
                         },
                         cache_control: cacheControl
                       });
+                    } else if (part.mediaType.startsWith("video/")) {
+                      // zcode patch: video/* file part -> video content block (image-block-isomorphic
+                      // shape used by Anthropic-style compatible gateways; official Anthropic API has
+                      // no video block yet). Video bytes are serialized as base64.
+                      anthropicContent.push({
+                        type: "video",
+                        source: {
+                          type: "base64",
+                          media_type: part.mediaType,
+                          data: (0, import_provider_utils14.convertToBase64)(part.data)
+                        },
+                        cache_control: cacheControl
+                      });
                     } else if (part.mediaType === "application/pdf") {
```

openai-compatible 补丁则转成 `video_url` 部件，内容是 base64 的 data URL（`patches/@ai-sdk__openai-compatible@2.0.60.patch:9`）。补丁注释自己也说明，官方 Anthropic API 还没有视频块，这个形状是给兼容 Anthropic 协议的网关用的；规则集里声明支持视频输入的正是 GLM-5.3-Flash 这类模型和 Coding Plan 站点（`zcode-builtin.json:998`、`4012`）。补丁按精确版本登记（`pnpm-lock.yaml:15`），而 `@ai-sdk/openai-compatible` 的依赖声明是范围版本 `^2.0.58`（`apps/zcode-cli/packages/adapters/package.json:104`），锁文件变动时补丁要一并核对。

## 业务错误与官方套餐网关

fetch 链里有两层与智谱的服务形态直接相关。

**业务错误**。不少兼容服务（包括 zcode-plan）用 HTTP 200 加 `success: false` 或非零 `code` 报错，403 也未必带 JSON 的 Content-Type（`model-execution.ts:450`）。`createProviderBusinessErrorFetch` 对非 2xx 响应一律尝试读业务体，对 SSE 逐帧检查错误帧，读取上限 64000 字符，识别后先把原始响应体消费完、最多等 1000 毫秒，保证连接能复用（`model-execution.ts:147`、`437`、`474`）。错误里保留响应头，`Retry-After` 才不会在分类前丢失（`model-execution.ts:554`）。

**官方套餐网关**。发往两个官方 Coding Plan 端点的请求，在进入用户代理之前被改写到 ZCode 平台网关（`apps/zcode-cli/packages/adapters/src/model/official-coding-plan-gateway.ts:22`）：

| 官方端点 | 网关路径（相对控制面地址） |
| --- | --- |
| `https://open.bigmodel.cn/api/anthropic/v1/messages` | `/api/v1/ultra/anthropic/v1/messages` |
| `https://api.z.ai/api/anthropic/v1/messages` | `/api/v1/ultra-zai/anthropic/v1/messages` |

文件头注释说网关负责套餐权益校验等平台侧处理，客户端只换地址，方法、请求体、鉴权头与响应原样透传（`official-coding-plan-gateway.ts:4`）。匹配按协议、主机、端口、路径精确比较，显式的 Host 头会被去掉（`official-coding-plan-gateway.ts:87`、`146`）。注释说“用户自建 provider 与第三方模型服务不受影响”；从代码看，判断只看最终 URL，用 Coding Plan API Key 模板建的个人 Provider 同样指向这两个端点，也会经过网关。改写放在代理之前，`noProxy` 按实际发送的网关地址判定（`model-execution.ts:511`）。

## 能力元数据从哪来

适配层不自己维护任何模型能力表，全部来自 Registry 里解析好的 `ModelConfig`，也就是[上一篇](https://daiw.net/manual/zcode/provider-config)那套规则的结果：

| 字段 | 在哪里起作用 |
| --- | --- |
| `contextWindow` | core 按剩余窗口裁剪单次请求的输出预算，也用于压缩阈值（`apps/zcode-cli/packages/core/src/runtime/methods/model-token-limits.ts:17`） |
| `inputFormat` | 请求前拒绝或替换不支持的图片、PDF、视频（`model.ts:155`，`media-transform-policy.ts:8`） |
| `supportsToolCall`、`supportsJsonSchemaOutput` | 请求前校验；后者决定 openai-compatible 的结构化输出，anthropic 协议的非流式结构化输出强制走 `outputFormat`（`runner-options.ts:191`） |
| `supportsNativeWebSearch` | 能否编码服务端 WebSearch 工具（`tool-transform.ts:260`） |
| `supportsMidConversationSystem` | core 是否把会话中途的提醒投影成 system 消息（`apps/zcode-cli/packages/core/src/runtime/helpers/runtime-provider-request-messages.ts:17`） |
| `requiresMfjsToolSchema` | 工具 schema 的 `$ref` 提升（`tool-transform.ts:100`） |
| `optionSpecs` | 档位与输出上限的合法范围，以及编译进 fetch 的选项映射（`model-execution.ts:187`） |

输出上限的具体值每次请求由 core 决定，ModelFactory 不替它绑定默认值（`provider-registry-model-runtime.ts:71`）。

下一篇：[账号、Coding Plan 与闲时计划](https://daiw.net/manual/zcode/accounts-plans)——`zcode login` 之后，账号登录态怎样变成这一层每次请求前取到的那份凭据。
