Skip to content

第 17 章 Provider 抽象 —— 模型无关的 Agent

Agent 不应绑定单一模型供应商;Provider 抽象层的核心职责是将"选哪个模型"与"Agent 怎么用模型"彻底解耦,使上层业务代码对底层 LLM API 完全透明。


17.1 Provider 抽象层的必要性

模型能力迭代速度极快——一个季度前的 SOTA 可能已被更便宜的替代品超越。如果 Agent 代码中硬编码了某个供应商的 SDK 调用方式、认证逻辑和响应格式,就会产生以下技术债务:

  1. 供应商锁定(Vendor Lock-in)。切换模型需要改动调用链的每一层,从 HTTP 请求构造、SSE 解析到 tool_use 回传。
  2. 无法多模型并行评估。生产环境经常需要 A/B 测试不同模型的效果,缺乏抽象层意味着每个模型都要写一套适配代码。
  3. 故障恢复困难。主模型限流或宕机时,没有统一接口就无法自动切换到备选模型。
  4. 推理参数碎片化。同一个"控制思考深度"的语义,Anthropic 用 thinking.budgetTokens,OpenAI 用 reasoningEffort,Google 用 thinkingConfig.thinkingBudget——没有翻译层就必须到处写 if-else。

Provider 抽象层的设计目标:上层代码只操作统一的 Model 接口,一切供应商差异在抽象层内部消化


17.2 各系统的 Provider 设计

17.2.1 OpenCode:AI SDK + 多供应商注册表

OpenCode 的 Provider 体系是目前开源 Coding Agent 中最完整的实现,其设计分三层。

第一层:models.dev 注册表。所有模型的元数据(context window、cost、capabilities、reasoning 支持)统一存储在远程 models.dev/api.json,本地缓存并定时刷新。每个模型描述符包含:

typescript
interface ModelDescriptor {
  id: string                // 模型 ID,如 "claude-sonnet-4-6"
  name: string              // 显示名称
  reasoning: boolean        // 是否支持推理模式
  tool_call: boolean        // 是否支持 tool calling
  cost: {
    input: number           // 每 M token 价格(美元)
    output: number
    cache_read?: number
    cache_write?: number
  }
  limit: {
    context: number         // 上下文窗口
    output: number          // 最大输出 token
  }
  provider?: {
    npm: string             // 对应的 AI SDK 包名
    api?: string            // 自定义 API 端点
  }
}

第二层:Bundled Provider SDK 映射。OpenCode 在启动时将所有支持的 SDK 工厂函数注册到一个静态映射表中:

typescript
const BUNDLED_PROVIDERS: Record<string, (options: any) => SDK> = {
  "@ai-sdk/anthropic": createAnthropic,
  "@ai-sdk/openai": createOpenAI,
  "@ai-sdk/google": createGoogleGenerativeAI,
  "@ai-sdk/google-vertex": createVertex,
  "@ai-sdk/amazon-bedrock": createAmazonBedrock,
  "@ai-sdk/azure": createAzure,
  "@openrouter/ai-sdk-provider": createOpenRouter,
  "@ai-sdk/xai": createXai,
  "@ai-sdk/mistral": createMistral,
  "@ai-sdk/groq": createGroq,
  "@ai-sdk/deepinfra": createDeepInfra,
  "@ai-sdk/openai-compatible": createOpenAICompatible,
  "@ai-sdk/gateway": createGateway,
  // ... 共 18+ 供应商
}

根据模型描述符中的 npm 字段,自动选择对应的 SDK 工厂创建 provider 实例。这使得新增供应商只需在注册表中加一行映射。

第三层:Custom Loader 钩子。部分供应商需要特殊处理(认证方式、区域路由、模型发现),通过 CUSTOM_LOADERS 实现:

typescript
const CUSTOM_LOADERS: Record<string, CustomLoader> = {
  "amazon-bedrock": async () => ({
    autoload: true,
    options: { region, credentialProvider },
    async getModel(sdk, modelID) {
      // 自动添加跨区域推理前缀(us.、eu.、apac.)
      return sdk.languageModel(`${regionPrefix}.${modelID}`)
    },
  }),
  openai: async () => ({
    autoload: false,
    async getModel(sdk, modelID) {
      return sdk.responses(modelID)  // 默认使用 Responses API
    },
  }),
  "github-copilot": async () => ({
    autoload: false,
    async getModel(sdk, modelID) {
      // 根据模型是否支持 Responses API 选择接口
      // 判断依据为 shouldUseCopilotResponsesApi() 内部的能力检测,
      // 而非固定的模型代际划分(各模型的 API 支持范围可能随版本更新变化)
      return shouldUseCopilotResponsesApi(modelID)
        ? sdk.responses(modelID)
        : sdk.chat(modelID)
    },
  }),
}

这种三层架构的关键优势:上层代码只需指定 providerID + modelID,Provider 层自动完成 SDK 选择、认证注入、API 端点路由和区域适配。

17.2.2 Codex:OpenAI Responses API + ChatGPT 登录 + API Key

Codex 的 Provider 设计走了一条与 OpenCode 截然不同的路——强绑定 OpenAI 生态,但提供了灵活的认证层和协议代理。

Responses API Proxy。Codex 内置了一个用 Rust 实现的最小化 HTTP 代理服务器,其唯一职责是:接收本地请求 → 注入 Authorization header → 转发到 OpenAI POST /v1/responses → 流式透传响应。

rust
struct ForwardConfig {
    upstream_url: Url,     // 默认 https://api.openai.com/v1/responses
    host_header: HeaderValue,
}

fn forward_request(client: &Client, auth_header: &'static str,
                   config: &ForwardConfig, req: Request) -> Result<()> {
    // 仅允许 POST /v1/responses
    let allow = method == Method::Post && url_path == "/v1/responses";
    if !allow { return respond_403(); }
    // 替换 Authorization,透传其余 header
    headers.insert(AUTHORIZATION, auth_header_value);
    headers.insert(HOST, config.host_header.clone());
    // 流式转发响应
    let upstream_resp = client.post(config.upstream_url.clone())
        .headers(headers).body(body).send()?;
    req.respond(response);
}

API Key 安全处理。Codex 对 API Key 的内存管理达到了近乎偏执的程度:通过 read(2) 系统调用直接从 stdin 读取(绕过 Rust 标准库的 BufReader 以避免内存残留),读取后立即用 mlock(2) 锁定包含密钥的内存页防止被 swap 到磁盘,中间变量用 zeroize 清零。

双认证模式。Codex 支持两种认证方式:

  • ChatGPT 账号登录:通过浏览器 OAuth 流或 Device Code 流获取 token,使用 ChatGPT Plus/Pro/Enterprise 配额
  • API Key:直接使用 OpenAI API Key,按量计费

虽然 Codex 不支持多供应商切换,但其 --upstream-url 参数允许将代理指向任意兼容 OpenAI Responses API 的端点,实现了有限的供应商可替换性。

17.2.3 mini-codex:环境变量驱动的轻量实现

mini-codex 展示了 Provider 抽象的最简形式——通过环境变量切换端点,用模型名前缀推断参数

rust
struct LlmConfig {
    api_key: String,           // MINI_CODEX_API_KEY 或 OPENAI_API_KEY
    base_url: String,          // MINI_CODEX_BASE_URL 或 OPENAI_BASE_URL
    model: String,             // 任意模型名
    reasoning_effort: String,  // low/medium/high
    enable_thinking: bool,     // 针对国产模型的思考模式开关
}

模型族检测通过前缀匹配实现:

rust
fn model_family(model: &str) -> &str {
    let normalized = model.trim().to_ascii_lowercase();
    if normalized.starts_with("gpt") || normalized.starts_with("o1")
       || normalized.starts_with("o3") || normalized.starts_with("o4") {
        "openai"
    } else if normalized.starts_with("gemini") { "gemini" }
      else if normalized.starts_with("qwen") { "qwen" }
      else if normalized.starts_with("deepseek") { "deepseek" }
      else if normalized.starts_with("kimi") || normalized.starts_with("moonshot") { "moonshot" }
      // ... 覆盖 doubao、hunyuan、glm、yi 等国产模型
      else { "other" }
}

然后根据模型族决定发送哪些参数:

rust
fn supports_reasoning_effort(family: &str) -> bool {
    matches!(family, "openai" | "gemini")
}

fn supports_enable_thinking(family: &str) -> bool {
    matches!(family, "qwen" | "deepseek" | "moonshot" | "doubao" | "hunyuan" | "glm" | "yi")
}

这种方式的优点是零依赖、即改即用——只要目标模型兼容 OpenAI Chat Completions API,改两个环境变量就能切换。缺点是无法处理差异化的认证协议和流式格式。

17.2.4 DeepAgents:LangChain init_chat_model() 统一接入

DeepAgents 利用 LangChain 生态的 init_chat_model() 实现了一行代码切换模型的能力:

python
from langchain.chat_models import init_chat_model
from langchain_core.language_models import BaseChatModel

def resolve_model(model: str | BaseChatModel) -> BaseChatModel:
    if isinstance(model, BaseChatModel):
        return model               # 已初始化的实例直接使用
    if model.startswith("openai:"):
        return init_chat_model(model, use_responses_api=True)
    return init_chat_model(model)   # "provider:model" 格式自动路由

init_chat_model 内部维护了一个 provider → ChatModel 类的注册表,支持的格式包括:

输入格式解析行为
"openai:gpt-5"创建 ChatOpenAI(model="gpt-5")
"anthropic:claude-sonnet-4-6"创建 ChatAnthropic(model_name="claude-sonnet-4-6")
"google-genai:gemini-2.5-pro"创建 ChatGoogleGenerativeAI(model="gemini-2.5-pro")
"ollama:llama3"创建 ChatOllama(model="llama3")

DeepAgents 的 create_deep_agent() 函数接受 model 参数为 str | BaseChatModel | None,默认使用 claude-sonnet-4-6

python
def create_deep_agent(model: str | BaseChatModel | None = None, ...):
    model = get_default_model() if model is None else resolve_model(model)

子 Agent 也可以使用不同模型——主 Agent 用 Claude,子 Agent 用 GPT-5,只需在 SubAgent spec 中指定 "model": "openai:gpt-5" 即可。

17.2.5 四系统对比

维度OpenCodeCodexmini-codexDeepAgents
抽象粒度SDK 工厂 + 注册表协议代理环境变量LangChain 统一接口
支持供应商数18+1 (OpenAI)理论无限*LangChain 生态全覆盖
新增供应商成本注册表加一行改 upstream URL改环境变量pip install 一个包
认证管理多策略 (OAuth, API Key, IAM)ChatGPT + API Key环境变量各 SDK 自行处理
模型元数据models.dev 远程注册表内置目录

*mini-codex 支持任何兼容 OpenAI Chat Completions API 的端点。


17.3 流式协议差异

LLM API 的流式输出没有统一标准。三大供应商的 SSE(Server-Sent Events)协议在事件类型、分片粒度和 tool_use 表示上存在显著差异。

17.3.1 Anthropic SSE 协议

Anthropic 使用细粒度的事件类型来标记内容流的每个阶段:

text
event: message_start
data: {"type":"message_start","message":{"id":"msg_01X","model":"claude-sonnet-4-6",...}}

event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}

event: content_block_start
data: {"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_01X","name":"read_file","input":{}}}

event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"path\":\"/src"}}

event: content_block_stop
data: {"type":"content_block_stop","index":1}

event: message_stop
data: {"type":"message_stop"}

关键特征:

  • content_block 索引机制:每个输出块(text、tool_use、thinking)都有唯一 index,delta 事件通过 index 关联到对应的块
  • tool_use 的 input 以 JSON 片段流式传输partial_json 需要在客户端累积拼接后才能解析
  • thinking 内容作为独立 content_blocktype: "thinking",与正文内容通过 index 区分

17.3.2 OpenAI SSE 协议

OpenAI 的 Chat Completions API 使用 choices[].delta 结构流式传输:

text
data: {"id":"chatcmpl-X","choices":[{"index":0,"delta":{"role":"assistant","content":"Hello"}}]}

data: {"id":"chatcmpl-X","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_X","type":"function","function":{"name":"shell","arguments":""}}]}}]}

data: {"id":"chatcmpl-X","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"cmd\":"}}]}}]}

data: [DONE]

Responses API 格式不同,使用扁平化的事件流:

text
event: response.output_item.added
data: {"type":"function_call","id":"fc_X","name":"shell","arguments":""}

event: response.output_item.content.delta
data: {"delta":"{\"cmd\":\"ls\"}"}

event: response.output_item.done
data: {"type":"function_call","id":"fc_X","name":"shell","arguments":"{\"cmd\":\"ls\"}"}

event: response.completed
data: {"id":"resp_X","status":"completed","usage":{...}}

关键特征:

  • tool_calls 嵌套在 delta 中:通过 tool_calls[].index 区分多个并行 tool call
  • arguments 是 JSON 字符串的增量片段:需要逐段拼接
  • [DONE] 标记结束(Chat Completions)或 response.completed 事件(Responses API)

17.3.3 Google 流式格式

Google 的 Gemini API 不使用 SSE,而是返回 JSON 数组的流式分片:

json
[{
  "candidates": [{
    "content": {
      "parts": [{"text": "Hello"}],
      "role": "model"
    }
  }]
},
{
  "candidates": [{
    "content": {
      "parts": [{"functionCall": {"name": "read_file", "args": {"path": "/src"}}}],
      "role": "model"
    }
  }]
}]

关键特征:

  • 无事件类型区分:每个 chunk 都是完整的 candidate 结构
  • tool call 以 functionCall 形式出现在 parts 中:与文本 part 同级
  • thinking 通过 thought: true 属性标记

17.3.4 统一抽象层的处理策略

OpenCode 的做法是在 AI SDK 层统一消化差异——每个供应商的 SDK 包内部实现各自的 SSE 解析器,对外暴露统一的 streamText() / generateText() 接口。上层代码只处理标准化后的事件:

typescript
// 上层代码完全不感知底层是 Anthropic SSE 还是 OpenAI SSE
const result = streamText({
  model: provider.languageModel("claude-sonnet-4-6"),
  messages,
  tools,
  maxTokens: ProviderTransform.maxOutputTokens(model),
})

for await (const chunk of result.textStream) {
  // 统一的文本流
}

但统一抽象并不能消除所有差异。OpenCode 的 ProviderTransform.message() 函数需要在发送消息前做供应商特定的规范化处理:

  • Anthropic:拒绝空内容消息,需要过滤掉 content === "" 的消息
  • Mistral:要求 toolCallId 恰好 9 个字母数字字符,需要截断/补齐
  • ClaudetoolCallId 只允许 [a-zA-Z0-9_-],需要正则替换非法字符
  • interleaved thinking 模型(如 DeepSeek、Qwen):reasoning 内容需要从标准 content 中抽取,转存到 providerOptions.openaiCompatible.reasoning_content
typescript
function normalizeMessages(msgs: ModelMessage[], model: Provider.Model): ModelMessage[] {
  if (model.api.npm === "@ai-sdk/anthropic") {
    msgs = msgs.filter(msg => msg.content !== "")
  }
  if (model.providerID === "mistral") {
    // toolCallId 截断到 9 字符,不足补 0
    part.toolCallId = part.toolCallId
      .replace(/[^a-zA-Z0-9]/g, "")
      .substring(0, 9)
      .padEnd(9, "0")
  }
  // ...
}

17.4 故障转移与熔断

17.4.1 重试策略

各系统的重试策略差异明显。

mini-codex:固定间隔重试,最多 7 次,间隔 1.5 秒。不区分可重试和不可重试错误,所有失败都重试:

rust
const API_RETRIES: usize = 7;
const API_RETRY_DELAY_MS: u64 = 1_500;

for attempt in 1..=API_RETRIES {
    match call_api() {
        Ok(result) => return Ok(result),
        Err(err) => {
            print_api_error(&format!("{err:#}"));
            if attempt < API_RETRIES {
                std::thread::sleep(Duration::from_millis(API_RETRY_DELAY_MS));
            }
        }
    }
}

OpenCode:细粒度错误分类,区分可重试和不可重试错误:

typescript
function parseAPICallError(input: { providerID: ProviderID; error: APICallError }) {
  // 上下文溢出 → 不重试,触发上下文压缩
  if (isOverflow(message) || error.statusCode === 413) {
    return { type: "context_overflow", message }
  }
  // OpenAI 的 404 有时是误报 → 标记为可重试
  if (providerID.startsWith("openai") && error.statusCode === 404) {
    return { type: "api_error", isRetryable: true }
  }
  // 其他错误使用 SDK 自带的重试判断
  return { type: "api_error", isRetryable: error.isRetryable }
}

OpenCode 维护了一组覆盖 15+ 供应商的上下文溢出检测正则:

typescript
const OVERFLOW_PATTERNS = [
  /prompt is too long/i,                    // Anthropic
  /exceeds the context window/i,            // OpenAI
  /input token count.*exceeds the maximum/i, // Google
  /maximum prompt length is \d+/i,          // xAI
  /reduce the length of the messages/i,     // Groq
  /maximum context length is \d+ tokens/i,  // OpenRouter, DeepSeek, vLLM
  /context window exceeds limit/i,          // MiniMax
  // ... 15+ 正则
]

17.4.2 SSE 超时保护

OpenCode 实现了 SSE 流的读超时保护。对于流式响应,每次 reader.read() 调用都设置了超时:

typescript
function wrapSSE(res: Response, ms: number, ctl: AbortController) {
  const reader = res.body.getReader()
  const body = new ReadableStream({
    async pull(ctrl) {
      const part = await new Promise((resolve, reject) => {
        const id = setTimeout(() => {
          const err = new Error("SSE read timed out")
          ctl.abort(err)
          reader.cancel(err)
          reject(err)
        }, ms)
        reader.read().then(
          (part) => { clearTimeout(id); resolve(part) },
          (err) => { clearTimeout(id); reject(err) },
        )
      })
      if (part.done) { ctrl.close(); return }
      ctrl.enqueue(part.value)
    },
  })
  return new Response(body, { headers: res.headers, status: res.status })
}

这种设计防止了模型 hang 住不返回数据时的无限等待。

17.4.3 流式错误检测

Codex 的 Responses API 会在 SSE 流中发送错误事件。OpenCode 的错误解析器能识别这些流中错误:

typescript
function parseStreamError(input: unknown): ParsedStreamError | undefined {
  if (body.type !== "error") return
  switch (body?.error?.code) {
    case "context_length_exceeded":
      return { type: "context_overflow", message: "Input exceeds context window" }
    case "insufficient_quota":
      return { type: "api_error", message: "Quota exceeded", isRetryable: false }
    case "usage_not_included":
      return { type: "api_error",
        message: "Upgrade to Plus: https://chatgpt.com/explore/plus",
        isRetryable: false }
  }
}

17.4.4 熔断策略设计要点

虽然现有系统没有完整实现熔断器(Circuit Breaker),但从错误处理代码中可以提炼出实用的熔断策略:

text
┌─────────────┐    连续 N 次失败    ┌─────────────┐   探活成功    ┌─────────────┐
│   CLOSED    │ ─────────────────→ │    OPEN     │ ──────────→ │ HALF-OPEN   │
│ (正常调用)   │                    │ (拒绝请求)   │              │ (试探性调用)  │
└─────────────┘                    └─────────────┘              └─────────────┘
      ↑                                  ↑                           │
      │            探活失败               │         成功              │
      └──────────────────────────────────┘←────────────────────────┘

关键参数:

  • 失败阈值 N:连续 N 次 5xx / timeout 后触发熔断(推荐 N=3~5)
  • 冷却时间:熔断后暂停该 provider 的请求(推荐 30~60 秒)
  • 探活间隔:定期发送轻量请求检测 provider 是否恢复
  • 降级策略:主 provider 熔断后自动切换到预配置的备选 provider

速率限制(Rate Limit)的检测同样重要。429 响应通常携带 Retry-After header,合理的实现应该解析该 header 并排队等待:

typescript
if (error.statusCode === 429) {
  const retryAfter = error.responseHeaders?.["retry-after"]
  const delayMs = retryAfter ? parseInt(retryAfter) * 1000 : 60_000
  await sleep(delayMs)
  // 重试或切换到备选 provider
}

17.5 推理参数管理

17.5.1 reasoning_effort vs enable_thinking

现代推理模型的"思考深度"控制参数因供应商而异,Provider 抽象层的职责之一就是翻译这些差异。

OpenAI 系(GPT-o 系列、GPT-5 系列):使用 reasoning_effort 字段,取值范围为 none | minimal | low | medium | high | xhigh,直接影响模型花在"思考"上的计算量。

Anthropic:使用 thinking 配置对象。传统模式下是二元开关 + 预算:

json
{
  "thinking": {
    "type": "enabled",
    "budgetTokens": 16000
  }
}

新一代模型(Opus 4.6、Sonnet 4.6)支持自适应模式:

json
{
  "thinking": {
    "type": "adaptive"
  },
  "effort": "high"
}

Google:使用 thinkingConfig 对象。Gemini 2.5 用 token 预算:

json
{
  "thinkingConfig": {
    "includeThoughts": true,
    "thinkingBudget": 16000
  }
}

Gemini 3.x 改用等级制:

json
{
  "thinkingConfig": {
    "includeThoughts": true,
    "thinkingLevel": "high"
  }
}

国产模型(Qwen、DeepSeek、Kimi、Doubao、Hunyuan、GLM、Yi):使用 enable_thinking: true 布尔开关,部分还需要 chat_template_args 包装:

json
{ "chat_template_args": { "enable_thinking": true } }

OpenCode 的 ProviderTransform.options() 函数集中处理了这些差异,根据 model.providerIDmodel.api.npm 组合,为每个模型生成正确的推理参数:

typescript
function options(input: { model: Provider.Model; sessionID: string }) {
  const result: Record<string, any> = {}

  // OpenAI: store=false + reasoningEffort
  if (model.providerID === "openai") {
    result["store"] = false
    if (model.api.id.includes("gpt-5")) {
      result["reasoningEffort"] = "medium"
      result["reasoningSummary"] = "auto"
    }
  }

  // Google: thinkingConfig
  if (model.api.npm === "@ai-sdk/google") {
    if (model.capabilities.reasoning) {
      result["thinkingConfig"] = { includeThoughts: true }
      if (model.api.id.includes("gemini-3")) {
        result["thinkingConfig"]["thinkingLevel"] = "high"
      }
    }
  }

  // 阿里云 DashScope:必须显式开启 enable_thinking
  if (model.providerID === "alibaba-cn" && model.capabilities.reasoning) {
    result["enable_thinking"] = true
  }

  return result
}

17.5.2 Variants:统一的思考深度挡位

OpenCode 引入了 Variants 概念——将不同供应商的推理参数映射到统一的挡位名称(low | medium | high | max),使用户可以在不了解底层参数差异的情况下调节思考深度:

typescript
function variants(model: Provider.Model): Record<string, Record<string, any>> {
  switch (model.api.npm) {
    case "@ai-sdk/openai":
      // low → reasoningEffort: "low", high → reasoningEffort: "high"
      return Object.fromEntries(
        efforts.map(e => [e, { reasoningEffort: e, reasoningSummary: "auto" }])
      )
    case "@ai-sdk/anthropic":
      // high → budgetTokens: 16000, max → budgetTokens: 31999
      return {
        high: { thinking: { type: "enabled", budgetTokens: 16000 } },
        max: { thinking: { type: "enabled", budgetTokens: 31999 } },
      }
    case "@ai-sdk/google":
      // high → thinkingBudget: 16000, max → thinkingBudget: 24576
      return {
        high: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } },
        max: { thinkingConfig: { includeThoughts: true, thinkingBudget: 24576 } },
      }
  }
}

17.5.3 计费差异

不同供应商的 token 计价模型存在三个维度的差异:

维度一:token 类别。部分供应商将 thinking token 单独计价:

  • Anthropic:input + output(thinking token 计入 output)
  • OpenAI:input + output(reasoning token 计入 output,通过 include: ["reasoning.encrypted_content"] 可缓存推理内容以降低后续调用成本)
  • Google:input + output(thoughts token 不计费)

维度二:缓存折扣。Provider 抽象层需要跟踪 cache hit 率以优化成本:

typescript
cost: {
  input: 3.0,       // $3 / M tokens
  output: 15.0,      // $15 / M tokens
  cache_read: 0.3,   // 缓存读取 = 输入价 × 10%
  cache_write: 3.75,  // 缓存写入 = 输入价 × 125%
}

OpenCode 的 applyCaching() 函数自动在消息中插入缓存控制标记,不同供应商使用不同的标记格式:

typescript
const providerOptions = {
  anthropic: { cacheControl: { type: "ephemeral" } },
  bedrock: { cachePoint: { type: "default" } },
  openaiCompatible: { cache_control: { type: "ephemeral" } },
  copilot: { copilot_cache_control: { type: "ephemeral" } },
}

维度三:上下文长度定价。部分模型在超过特定上下文长度后加价:

typescript
experimentalOver200K: {
  input: 6.0,     // 超过 200K 上下文后 input 加倍
  output: 30.0,   // 超过 200K 上下文后 output 加倍
  cache: { read: 0.6, write: 7.5 },
}

17.5.4 模型特定的采样参数

不同模型对 temperature、top_p、top_k 的最优值不同。OpenCode 的 ProviderTransform 为每个模型族维护了推荐值:

typescript
function temperature(model: Provider.Model) {
  const id = model.id.toLowerCase()
  if (id.includes("qwen")) return 0.55
  if (id.includes("claude")) return undefined  // 不设置,使用 API 默认值
  if (id.includes("gemini")) return 1.0
  if (id.includes("kimi-k2") && id.includes("thinking")) return 1.0
  if (id.includes("kimi-k2")) return 0.6
  return undefined
}

function topP(model: Provider.Model) {
  if (id.includes("qwen")) return 1
  if (id.includes("minimax-m2") || id.includes("gemini")) return 0.95
  return undefined
}

这些看似微小的参数差异,在大规模生产使用中会显著影响输出质量和一致性。Provider 抽象层的价值之一就是将这些经验知识编码为可复用的配置。


mermaid
graph TB
    subgraph "Agent 业务层"
        A[Agent Core] --> B[统一 Model 接口]
    end

    subgraph "Provider 抽象层"
        B --> C{Provider 路由}
        C --> D[参数翻译<br/>ProviderTransform]
        C --> E[认证管理<br/>ProviderAuth]
        C --> F[错误处理<br/>ProviderError]
        D --> G[Variant 映射]
        D --> H[消息规范化]
        D --> I[缓存控制注入]
    end

    subgraph "SDK 适配层"
        C --> J["@ai-sdk/anthropic"]
        C --> K["@ai-sdk/openai"]
        C --> L["@ai-sdk/google"]
        C --> M["@ai-sdk/amazon-bedrock"]
        C --> N["@ai-sdk/openai-compatible"]
        C --> O["... 18+ SDKs"]
    end

    subgraph "LLM API"
        J --> P["Anthropic API"]
        K --> Q["OpenAI API"]
        L --> R["Google API"]
        M --> S["AWS Bedrock"]
        N --> T["Any OpenAI-Compatible"]
    end