pi 的两种请求格式:openai-completions 与 anthropic-messages

packages/coding-agent/docs/custom-provider.md:221 对 api 字段的全部说明是一句话:

The api field determines which streaming implementation is used.

可用值在 packages/ai/src/types.ts:17-29,注册在 packages/ai/src/compat.ts:177-188,按 model.api 分发在 packages/ai/src/models.ts:792。两个值之间的差异如下,右侧两列的行号在 packages/ai/src/api/ 下。

维度 openai-completions anthropic-messages
实现文件 openai-completions.ts,请求体构造 806 行 anthropic-messages.ts,请求体构造 1053 行
消息容器 messages[],工具结果是一条 role:"tool" 消息带 tool_call_id messages[],工具结果是 user 消息里的 tool_result block
系统提示词 messages 里一条,role 取 system 还是 developer 由 compat.supportsDeveloperRole 决定(1216) 顶层 system 字段,content block 数组,可挂 cache_control(1080-1085);走 OAuth 时强制先插一条 You are Claude Code, Anthropic's official CLI for Claude.(1070-1076)
输出上限字段 max_completion_tokens,部分网关要 max_tokens,看 compat.maxTokensField(827-830) max_tokens 必填,缺省回落 model.maxTokens(1057)
thinking 字段名一家一个,compat.thinkingFormat 选:reasoning_effort、openrouter(reasoning.effort)、together(reasoning.enabled)、qwen(顶层 enable_thinking)、chat-template(chat_template_kwargs)、deepseek、zai、baseten、qwen-chat-template(字段表 docs/models.md:474) thinking.budget_tokens(1145),或 adaptive:thinking.type:"adaptive" + output_config.effort(1136,自定义网关靠 compat.forceAdaptiveThinking)
thinking 回放 无签名概念,部分厂商要空 reasoning_content(requiresReasoningContentOnAssistantMessages) thinking block 带 signature,多轮必须原样回传,签名丢了会一直 400;空签名走 compat.allowEmptySignature(1298-1318)
temperature 正常发 开了 extended thinking 就不发,Opus 4.7+ 一类直接不支持(1087-1095)
工具定义 function schema,strict 与否看 compat.supportsStrictMode 每个工具默认带 eager_input_streaming: true;网关不认就去掉该字段、改发 fine-grained-tool-streaming-2025-05-14 beta(docs/models.md:393);最后一个工具定义可挂 cache_control
prompt 缓存 原生没有。compat.cacheControlFormat:"anthropic" 借 Anthropic 标记打在 system / 最后一个工具 / 最后一个文本块(1119、1156、1170),prompt_cache_key 只有 openai-responses 才发 cache_control: {type:"ephemeral"},长保留 ttl:"1h" 受 compat.supportsLongCacheRetention 控制
usage 统计 要主动开 stream_options: { include_usage: true }(819),网关不支持就关(supportsUsageInStreaming,Ollama 一类必须关) 响应自带 cache_read_input_tokens / cache_creation_input_tokens,映射到 usage.cacheRead / cacheWrite(610-611、763-767)
结束原因 finish_reason;不发就 pi 按流结束推断 stop/toolUse(supportsFinishReason) 原生 stop reason,另有 betas[] 请求字段(1061)
鉴权头 Authorization: Bearer 加 key x-api-key 或 Authorization,SDK 带 anthropic-version,beta 能力走 anthropic-beta(978-1013)

completions 把东西全塞进 messages 数组,可选字段看网关实现;anthropic 是 system / tools / content block 各归各位,缓存和 thinking 是结构化字段。同一个模型换一种格式,token 计费点、缓存命中、thinking 是否保留都不一样。

两个例子

取自 ~/.pi/agent/models.json,apiKey 换成 ***。

token-plan(openai-completions)

{
  "token-plan": {
    "baseUrl": "https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1",
    "api": "openai-completions",
    "apiKey": "***",
    "models": [
      {
        "id": "qwen3.8-flash",
        "reasoning": true,
        "compat": { "thinkingFormat": "qwen", "supportsDeveloperRole": false },
        "contextWindow": 1000000,
        "maxTokens": 131072,
        "input": ["text", "image"],
        "thinkingLevelMap": {
          "off": null, "minimal": null, "low": null, "medium": null,
          "high": "high", "xhigh": null, "max": "max"
        }
      }
    ]
  }
}

三条配置各补一个缺口:thinkingFormat: "qwen" 是因为这家不认 reasoning_effort;supportsDeveloperRole: false 是因为 /compatible-mode/v1 不接受 developer 角色;thinkingLevelMap 里一串 null 表示 off/minimal/low/medium/xhigh 它没有,/thinking 菜单里就不出现,只剩 high 和 max。前两条展开说。

thinking 开关放在哪一层

「顶层」指 POST body 的最外一层,和 model、messages、max_completion_tokens 平级。packages/ai/src/api/openai-completions.ts 里 params 就是那个对象,compat.thinkingFormat 决定往它上挂什么字段:

thinkingFormat 挂上去的字段 源码
不填 顶层 reasoning_effort: "high" 955-957
"qwen" 顶层 enable_thinking: true 879-880
"qwen-chat-template" 顶层 chat_template_kwargs: { enable_thinking: true, preserve_thinking: true } 887-891
"together" 顶层 reasoning: { enabled: true },带 effort 时再加 reasoning_effort 939-947

同一个 qwen3.8-flash、同一个 high 档,三种写法发出去的 body 区分如下(messages 略):

{ "model": "qwen3.8-flash", "max_completion_tokens": 131072, "reasoning_effort": "high" }
{ "model": "qwen3.8-flash", "max_completion_tokens": 131072, "enable_thinking": true }
{ "model": "qwen3.8-flash", "max_completion_tokens": 131072,
  "chat_template_kwargs": { "enable_thinking": true, "preserve_thinking": true } }

三家读的字段不重叠。默认写法发 reasoning_effort,DashScope 不读它,于是 /thinking 换了档但 body 里那个字段没人接,表现就是 thinking 不生效;改成 "qwen" 之后发的是 enable_thinking,它才认。

enable_thinking 是布尔,只说开还是关,不带档位。880-886 那段在 supportsReasoningEffort 为真时会同时发 reasoning_effort,两家字段一起给;这个开关默认就是真(1635-1636 的排除名单里没有阿里云),所以不用额外配。

developer 是什么

messages[].role 的取值之一。OpenAI 在推理模型上把系统指令的角色从 system 改叫 developer,Chat Completions 的类型里两个都在。pi 选哪个的逻辑在 openai-completions.ts:1214-1218:

if (context.systemPrompt) {
  const useDeveloperRole = model.reasoning && compat.supportsDeveloperRole;
  const role = useDeveloperRole ? "developer" : "system";
  params.push({ role: role, content: sanitizeSurrogates(context.systemPrompt) });
}

两个条件同时成立才发 developer:模型标了 reasoning: true,并且 supportsDeveloperRole 为真。后者不写就走自动判定(1634):

supportsDeveloperRole: isOpenRouterDeveloperRoleModel || (!isNonStandard && !isOpenRouter),

isNonStandard 是一份写死的厂商名单(1600-1615):cerebras、xai、together、chutes、deepseek、zai、moonshot、opencode、cloudflare 两种、nvidia、ant-ling。token-plan.cn-beijing.maas.aliyuncs.com 一个都不匹配,isOpenRouter 也是假,于是默认判成真。加上我这条模型 reasoning: true,不写 supportsDeveloperRole: false 的话 pi 就把系统提示词以 developer 发过去,而兼容模式的角色枚举里没有这个值,请求会被拒。写 false 就是把角色退回 system。

同一份自动判定里其他几项也一样靠名单:supportsStore: !isNonStandard(1633)、requiresReasoningContentOnAssistantMessages: isDeepSeek(1643)。自定义网关地址不在名单里就按标准 OpenAI 处理,所以例 2 里 agentrouter 那三条覆盖成 openai-completions 的模型一条 compat 都没写,是赌它兼容得比较全。

agentrouter(一个 provider 里两种 api 并存)

{
  "agentrouter": {
    "baseUrl": "https://agentrouter.org",
    "api": "anthropic-messages",
    "apiKey": "***",
    "models": [
      { "id": "glm-5.3", "api": "openai-completions", "baseUrl": "https://agentrouter.org/v1", "contextWindow": 1000000 },
      { "id": "gpt-5.6-sol", "api": "openai-completions", "baseUrl": "https://agentrouter.org/v1", "contextWindow": 1000000 },
      { "id": "deepseek-v4-flash", "api": "openai-completions", "baseUrl": "https://agentrouter.org/v1", "contextWindow": 1000000 },
      { "id": "claude-opus-5", "contextWindow": 1000000 },
      { "id": "claude-opus-4-8", "contextWindow": 1000000 },
      { "id": "claude-fable-5", "contextWindow": 1000000 }
    ]
  }
}

provider 级 anthropic-messages 给三个 claude 当默认,glm / gpt / deepseek 在 model 级覆盖成 openai-completions,同时覆盖 baseUrl 到 /v1:根路径的 anthropic 兼容端点和 /v1/chat/completions 是这家网关的两个入口。api 能落在 model 级(docs/models.md:130、docs/models.md:203)是这份配置能混用的前提。