Magpie 本地网关源码解读与接入核对指南
只用一家供应商时可以先直连;需要统一多个客户端时,再评估本地网关。本文用固定源码说明怎样固定上游、何时启用路由组,以及一次失败后该查哪些证据。
- 核对完整模型 ID、上游域名与路由组成员
- 失败切换受候选可用性、错误类型与输出状态限制
- 供应商凭据和网关访问 key 分开管理
- 订阅接入须另查许可与计费
- 本文为资料与源码审查,不提供实测性能分数
1. 先看接入成本是否值得
同时使用 Claude Code、Codex 和 Gemini CLI 时,供应商地址、模型名与凭据分散在几份配置里。Magpie 把这些入口集中到本机,客户端可以共用模型目录,再由网关选择上游。对于经常换模型的人,这种管理方式值得试;只用一个客户端和一个供应商的人,也要考虑多维护一个后台进程是否划算。
本文的源码审查基线为 2026 年 10 月 5 日,另有标明日期的发行说明补记;它们属于不同证据,不能合并视为同一版本的运行结果。没有安装 Magpie、读取用户凭据或发送模型请求。源码固定在提交 2e57619;原稿记录的发行版为 v0.1.980。该发行记录不表示当前最新版,也不假定发行二进制与这一提交完全一致。延迟、回答质量、实际账单及长期稳定性均未实测,因此不提供性能分数。
2026 年 10 月 5 日 13:04 UTC 复核补记:官方已于 13:00 UTC 发布 v0.1.1021,前述 v0.1.980 仍作为原审查时的发行记录保留。其间新增 gateway key 的模型/供应商范围和仅限路由组设置,v0.1.1016又修复了这些限制在 token counting 与 System One 请求中的执行。另新增网关中间件,可检查或改写代理请求、流式事件与响应;hook 失败或超时时会原样放行,不能据此把它当成强制安全边界。未固定版本的安装入口可能取得后续版本,应记录实际版本并复核 key 权限与启用的插件;本文对旧提交的审查范围和未实测说明不变。
一个具体选择:双客户端、两个候选供应商
假设团队同时用 Codex 和 Claude Code,业务代码只允许交给供应商 A;供应商 B 仅获准处理公开示例。下面的 A、B 沿用后文 relay-a、relay-b 虚构配置,不代表真实服务。
- 先直连供应商:若只需一个客户端访问 A,且客户端原生支持其接口,先保留原接法。这样排错只需核对客户端和上游;为一个不需要的自动切换能力增加网关,反而多一个故障位置。
- 经网关固定供应商:若两个客户端确实需要统一目录,业务代码使用完整的 relay-a/demo-model,核对 relay-a 的实际域名与 wire 映射。这里固定的是网关中的供应商路径,不等于供应商不会再转发,也不保证全部扩展字段原样保留。
- 使用路由组:只有公开示例同时获准发给 A、B,且两条线路各自通过最小验证后,才考虑后文的 group/daily。order 给出尝试顺序;它不能保证备用线路有额度、兼容工具或一定成功。
据此,这个团队可以让业务代码固定 A,把公开测试放进 A/B 组。若还启用意图分类,分类模型也是需要审批的数据接收方。选择依据是任务可到达哪些地方,以及是否真需要集中管理;不能只看组里有几个同名模型。
2. 请求会经过哪些地方
在常见接法里,客户端先请求 127.0.0.1:3425,Magpie 解析 provider/model 或路由组,再使用保存的供应商配置访问上游。API key 可以集中在网关一侧,但提示词、代码片段和工具结果仍会到达最终供应商。多加一个本地节点不会自动改变上游的数据保留政策。
官方提供 macOS、Windows、Linux 桌面入口,菜单栏或托盘、普通窗口、magpie tui、magpie web 和纯 CLI 各有对应实现。使用手册还说明,选择模型会修改客户端自己的配置,例如 Codex 的 config.toml;已运行的会话通常要重开。本文未逐平台验证界面、配置保留和回切效果。
3. 兼容要沿着实际请求核对
固定提交的网关端点表列出 OpenAI Chat Completions、Responses、Anthropic Messages 和 Gemini generateContent;还有流式 Gemini、两类 token counting 与模型目录入口。上游原生支持来访协议时走直通,否则走适配。源码包含流式事件、工具调用和推理内容处理,但端点存在不代表每个厂商扩展字段都无损保留。
例如项目明确写出 ChatGPT 后端只接收流式请求,网关会转换非流式调用并移除不接受的参数。模型目录里的 native_endpoints 可以帮助判断哪些入口原生直通;路由组不会提供这一字段。图片、结构化输出、工具参数、思考签名和上下文长度,仍须在所选客户端与上游组合上逐项验证,不能从一次纯文本成功推导全部可用。
排查 Claude 路由或 Codex 登录异常时,应先记录 Magpie 版本。官方在 2026 年 10 月 6 日的 v0.1.1081 发行说明中指出:供应商支持时,Claude 模型使用 Anthropic Messages API,以修正错误路由并保留缓存行为;Codex 收到支出上限等额度受限响应时,也会保持登录状态。据此排查时,建议同时核对实际协议、缓存用量与额度错误原因,避免仅凭模型名判断线路,或把额度问题直接当成登录失效。保持登录并不表示额度恢复或请求已成功。这一补记依据官方发行说明,未安装或调用验证;前述固定源码与历史观察的范围不变。
4. 把目录名与供应商实际模型名分开
客户端看到的 relay-a/demo-model 包含供应商 ID,指定这条路径会选择该供应商。路由组则用 group/daily 这样的名称。自动分组源码会把来自不同供应商的模型名先规范化:小写化、去掉末段之前的供应商前缀、统一下划线以及数字之间的点或 p,并移除符合规则的快照日期。它会保留 :batch、-thinking 等变体。用户也可以显式声明同名关系。
检查映射时可先用 magpie model wire relay-a/demo-model 查看当前上游名,再用 magpie model wires 核对全局设置。这里的 relay-a 与 demo-model 均须替换,查看命令本身不生成模型回答。若上游要求 vendor/model-20261001 这样的专用标识,应照官方目录填写,避免为了消除报错随意改名。多个上游即便归入一个组,也应保留各自映射。
因此自动同名组不能证明两条上游线路具有相同权重、上下文窗口或服务质量。裸模型名可能被解析成组;需要固定目的地时,查看完整 provider/model 更直观。另一个独立设置是 model wire,它改变发给上游的模型字段,目录和组成员仍保留原 ID。模型级映射优先于供应商通配映射,重置前者后仍可能继承后者。映射说明也指出图片请求不沿用这套命名规则。
5. 失败切换有具体前提
路由组能把多个模型及其可用账号、key 放进候选池。手册列出的 smart 优先使用仍有额度且更早重置的订阅,order 按顺序,rotate 逐轮换成员,usage 按使用量,pace 参考剩余额度与重置时间。会话黏性由 stays 控制,默认 auto 参考缓存有效性;session、turn、off 的行为不同。源码另有 manual 模式,固定当前成员,不宜把所有组都理解为自动轮询。
自动组会从可用供应商目录派生,新增或移除供应商后成员可能变化。需要可预测的备选列表时,可手建组,并在每次调整后用 magpie groups 与 magpie group 组ID 核对。成员中的账号和 key 还受可用模型、暂停状态及额度限制影响;看到组里有两条名称,并不能保证当前请求一定有两个可用候选。
切换要求仍有可用候选,而且当前答复尚未开始向客户端透传。错误判定将 401、402、403、404、408、429 与服务端错误列入可重试类别,400/422 则结合额度、模型不可用或请求形状等错误文字判断。普通缺字段错误不会因此全部重发;结构不兼容也不必让该供应商进入冷却。上下文溢出另走剩余容量筛选,不能靠换同模型账号扩大窗口。
源码还针对部分拒绝响应另设处理分支,因此不能说只在限流时切换;这里不将它作为绕过服务限制的用法。输出已经开始后,不会把另一家的续文拼到同一答复。冷却、短暂重试与首内容等待阈值各有逻辑,不能承诺每次立即成功。X-Magpie-Account 显式固定账号时只试该账号。核对 Routing 中的 tries、fail、model、served,才能知道某次请求实际发生了什么。
6. 先安装并保留现有配置
第一次接入建议从一个客户端、一家有权使用的官方 API 开始。先退出该客户端并复制其配置文件,记录原模型和供应商设置;不要把整个含凭据的目录上传给别人排错。也可用 magpie save before-magpie 保存 profile,但它不能代替独立备份。
从官网选择系统和架构下载。官方声明 macOS 构建已签名和公证,Windows、Linux 构建尚未签名;遇到系统安全提示时应核查来源。终端安装可把官方脚本先保存、阅读再运行,下面是 macOS/Linux 的写法。它会下载并安装软件,本文没有执行。记录 magpie --version 的输出,更新后遇到变化才有依据比较。
# macOS / Linux;先阅读脚本再决定安装
curl -fsSL https://usemagpie.ai/install.sh \
-o magpie-install.sh
less magpie-install.sh
sh magpie-install.sh
magpie --version
7. 用自己的供应商目录完成接入
打开 Providers → Add provider,选官方供应商预设,核对 Base URL 后再在本机界面输入自己的 API key。导入旧配置时逐条检查域名,第三方中转地址意味着内容会交给该中转方。GUI 比把密钥写进命令参数更适合第一次配置,后者可能留下 shell 历史或被进程检查工具看到。添加与刷新目录可能联系供应商;Test 会发送小请求,应先确认费用与账户权限。
完成添加后,先在 Providers 页面检查供应商 ID、展示模型和地址。magpie provider models PROVIDER 会重新获取该供应商的模型列表;magpie provider refresh PROVIDER 还会清理已经消失的选择,不要将两条命令当成完全等价。需要明确提供 models= 的服务,应从其官方文档获取列表,并记录日期;手填一个名字不会让账户获得相应权限。
自定义配置的 CLI 参数包括 url=、anthropic=、responses=、models=。url= 在保存的 Provider JSON 中对应 chat 字段。下面是按官方Provider与Group结构写的脱敏示意,域名和模型均虚构,key 为空。它用于检查两条目的地和组成员关系,不能直接使用;勿覆盖自己的 providers.json。上游地址必须从供应商文档确认,不能把模型显示名当 API ID。
{
"providers": [
{
"id": "relay-a",
"name": "示例 A",
"chat": "https://a.invalid/v1",
"key": "",
"models": [
"demo-model"
]
},
{
"id": "relay-b",
"name": "示例 B",
"chat": "https://b.invalid/v1",
"key": "",
"models": [
"demo-model"
]
}
],
"groups": [
{
"id": "daily",
"name": "每日编码",
"members": [
"relay-a/demo-model",
"relay-b/demo-model"
],
"routing": "order",
"affinity": "session"
}
]
}
8. 启动后先检查本机入口
桌面应用启动时会带起网关;纯命令行使用 magpie serve。两种方式择一,避免重复监听。下面将监听地址限制在 loopback,并关闭项目文档描述的使用人数统计;这些环境变量只作用于这次启动。另开终端运行目录检查,把 PROVIDER 与 MODEL 替换为 magpie models 中确实存在且自己有权限的值,再修改一个客户端并新开会话。
如果客户端没有出现在 Agents 列表,先确认其已安装以及实际配置目录;官方集成表会列出该客户端采用的文件。普通兼容客户端也可手工填写 Base URL 和模型,但环境变量是否生效由客户端决定。不要在全局 shell 配置里一次改掉所有工具的地址;先只在一个新终端或单独配置中验证,更容易排除旧会话与环境变量覆盖造成的误判。
curl 命令只读取本机模型目录,不发送生成请求;响应必须包含预期完整 ID。若提示连接失败,先检查网关是否运行、端口是否改变。若模型不存在,检查 provider 目录、models= 暴露范围和拼写,不要用猜出的 ID 重试。Anthropic 客户端的 Base URL 不带 /v1,OpenAI 客户端需要带;把这两种地址混用容易得到 404。
# 终端一:不要与桌面网关重复启动
MAGPIE_ADDR=127.0.0.1:3425 \
MAGPIE_NO_STATS=1 magpie serve
# 终端二:查看目录;可能需要先完成 provider 配置
magpie models
curl --fail --silent --show-error \
http://127.0.0.1:3425/v1/models
# 下两条中的 PROVIDER/MODEL 必须换成真实目录值
# 选择自己安装的客户端,只执行对应一条
magpie claude 'PROVIDER/MODEL'
magpie codex 'PROVIDER/MODEL'
9. 凭据和订阅分别设边界
默认 loopback 网关的本地 token 可任意填写,magpie 只是文档示例值。不要把这个值当远程保护。开启局域网共享后,应为每个客户端创建独立 gateway key,并设置限额;它与供应商 API key 分工不同。只改变 MAGPIE_ADDR 不等于完成访问控制。同机反向代理转发到 loopback 时,网关会视其为本机请求,代理必须自行认证。本文不提供公网暴露命令。
项目文档把 providers.json 与 plugin-auth.json 标为 0600,但文件权限不等于加密。排错输出、备份、同步、OTLP 和请求归档要分别检查,分享前删去 token、邮箱、账号标识和业务内容。项目称订阅接入会读取原客户端凭据,部分路径会刷新并回写 token;因此不能宣传为完全不接触凭据。第三方认证插件还会执行插件代码,首次需要时下载 Bun,安装前应审查来源。
Claude 订阅生成据项目说明通过本地真实 claude 二进制及 MCP 桥接。Anthropic 条款说明允许用户用自己的订阅登录未经修改的 Claude Code,同时限制第三方中介凭据和代用户路由订阅。仅凭调用官方二进制,无法认定整条转接链已获许可。
Codex/ChatGPT 登录与 API key 是不同计费路径,OpenAI 明确区分两套账单。Copilot 也受自己的席位、额度和政策约束。Google 官方 FAQ明确禁止第三方借用 Gemini CLI OAuth 访问后端,并建议第三方 agent 使用 Vertex AI 或 AI Studio API key。因此本文不上手该 OAuth 路径,也不把任何订阅写成任意客户端通用的免费 API。其他订阅及插件需各自核查许可。
10. 用一个无敏感内容的会话验收
确认目录后,在新会话先发一个短的无敏感文本任务,再测试一次只读工具调用与流式输出。每一步都去 Routing 核对 requested 模型、实际成员和上游,去 Usage 对照状态与 token。若使用组,先选 order 和两条已独立通过的线路;要检验回退,应在受控测试环境制造已知失败,不能拿生产密钥随意删除或制造真实限流。本文未执行这组端到端验证。
把“自动回退成功”拆成能核对的记录
下面是一份完全虚构、未执行的填写示例,用来解释记录方法,不是 Magpie 日志导出格式,也不是性能证据。沿用上文 A/B 组,假设 A 在向客户端透传内容前返回 429,B 随后正常结束。真实记录应从客户端、Routing、Usage 与供应商侧逐项核对;看不到的字段写“未取得”,不要根据最终回答补猜。
记录号:EXAMPLE-01(人工代号,不含真实请求 ID)
版本与时间:未执行,无安装版本、客户端版本或发生时间
输入:公开短句示例;无文件、工具结果与业务内容
入口:本机 /v1/chat/completions;流式;请求 group/daily
候选:relay-a/demo-model → relay-b/demo-model;order
目的地:a.invalid、b.invalid(虚构);均假定已获准
额外分类目的地:无(本例假定未启用意图分类)
尝试 1:A 返回 429;尚未向客户端透传内容
尝试 2:B 返回 200;输出完整结束,无中途错误
最终观察:两次尝试;B 完成;未验证工具、图片或长上下文
用量与费用:未测;不能据此比较价格或速度怎样读这份记录:若真实记录与这个设定一致,只能证明这次请求在首内容前由 A 切到 B,并完成了这一种短文本任务。429 不代表 A 没有收到请求;B 成功也不会撤回已发给 A 的内容。实际目的地应核对配置与尝试记录,模型返回的名字不能替代这一步。
如果失败发生在输出之后:假设 A 已向客户端透传一段内容,然后连接中断,记录应写“部分输出后失败”,保留客户端结束状态,并按固定源码的回退边界理解:不能期待 B 把同一答复无缝续完。HTTP 200 或看见文字都不足以判断完整成功。若客户端或用户另起请求,应单独编号;涉及工具副作用时先查执行结果,避免盲目重放。
把这份示例换成自己的验收记录时,补上实际版本、时间和入口,记录每次尝试及最终结束状态。对外分享只保留排错必要字段,将内部域名、账号与请求标识改成一致代号,删除 token、请求体和工具输出。若启用意图分类,还需单独列出分类模型及目的地,它可能接收任务内容并产生额外费用。
模型 wire 修改的是上游名字,served_model 则来自供应商回复,两者都不能独立证明模型身份。费用页面按目录价格或自定义价格估算,缺失价格、缓存计费与套餐权益都会影响结果。要比较成本和延迟,应记录版本、同一输入、输出 token、首内容时间、总耗时以及重试次数,再以供应商账单核对。未收集这些数据时,便宜、快、质量更好都只能保留为待测问题。
11. 怎样与 CC Switch 取舍
原稿审查时,CC Switch 官方 README已经列出本地协议转换、按工具的故障队列、熔断和健康监测,也有用量及费用统计。把它概括为只改配置、把 Magpie 写成唯一带代理的选择,会误导读者。二者已有明显交集,实际选择应从自己的客户端覆盖和配置恢复需求出发。
Magpie 的可核特点在于统一 provider/model 目录、自动与手动路由组,以及桌面、TUI、CLI、Web 和服务器入口。CC Switch 更集中于桌面工具配置、项目和相关资源管理。这是对官方功能组织的观察,不能代替易用性评测。第一次迁移只让一个管理器接管同一客户端配置,记录改动并测试回切,避免两者交替写文件后无法判断哪项设置生效。
12. 开始之前留下可核对的记录
把当前版本、客户端版本、供应商官方域名、完整模型 ID 和是否经过组路由记下来。只有模型目录与最小会话都通过,再逐步加入第二家供应商。含私有代码或客户资料的任务,应先确认每一个候选目的地都获准接收这些内容;自动回退会扩大实际数据接收方。
若准备回退,先停止让客户端发新请求,再恢复事先保存的原配置并新开会话。若使用 profile 回切,magpie use before-magpie 会应用保存的配置集合,执行前先确认它涵盖哪些客户端。随后检查 Base URL 已回到原目的地、模型仍存在;只有客户端已不依赖本地入口时,才停止网关。不能以旧会话还在输出,判断配置已经恢复。
这次审查的结论是 Magpie 已具备可查的多协议网关与路由实现,接入步骤也有完整官方依据。真正需要读者验证的,是自己那组客户端、模型和账户能否稳定工作,以及费用、条款和数据处理是否可接受。文中六张图为依据文档绘制的说明图,均非运行截图;所有命令与配置示例完整列在页内。
13. 补充实操:用一张固定库存表验收工具往返
2026 年 10 月 14 日补充;以下为原创、未执行的手工验收方案。本文此前的短文本回退记录,不能回答“工具调用经协议转换后还能否正确往返”。这次把任务缩小为查询两件虚构商品:既不访问真实库存,也不下单。读者可以把它交给已有的开发测试客户端执行;如果客户端不能注册测试工具或查看协议事件,就保留为待测项目,不用聊天窗口里的“我调用了工具”代替证据。
本次阅读的Magpie 当前官方手册说明,请求在上游支持来访 API 时直通,否则转换;模型目录的 native_endpoints 可辅助判断,路由组不提供这一字段。这是读取时的主分支文档,不是原文固定提交的新运行结果。下面的验收标准由本案例预先定义,不是 Magpie 宣布的兼容性保证。
任务输入固定为:“请调用 lookup_stock,查询松果杯和雾蓝笔的可售数量,按输入顺序各写一行;查不到请写未知,不要下单。”模型只接收这段任务、工具说明与参数定义;下方库存表和标准答案由测试者保管,不能同时塞进提示词,否则模型不调用工具也能抄到答案。
| 工具内固定键 | on_hand | reserved | 预先算好的 available |
|---|---|---|---|
| 松果杯 | 17 | 5 | 12 |
| 雾蓝笔 | 9 | 9 | 0 |
固定判定答案是“松果杯:12;雾蓝笔:0”。允许标点和排版不同,不允许顺序颠倒、把零写成未知,或把在库 17 当成可售 12。这个答案在运行前由 17−5、9−9 手算确定;它不来自模型输出,也不由待验收的网关转换器生成。
本例所有商品、库存和答案都是人工设计的固定练习。名称按精确键匹配,保留输入顺序及重复项,不联网、不读取真实业务数据。测试者可以在已有客户端中登记工具并按表返回结果,无需为本文另造检查器或执行包装层。
14. 工具契约与两轮交换:记录真实关联,不只看答案
工具名为 lookup_stock,说明为:“只读查询本练习的固定库存;按 items 顺序返回结果;未知商品返回 unknown;不预留、不扣减。”参数使用下面的 JSON Schema。这是参数对象,不能把它直接当成完整 HTTP 请求;测试客户端应按所用 API 放入相应工具定义字段。
{
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"type": "string"
},
"minItems": 1,
"maxItems": 3
}
},
"required": [
"items"
],
"additionalProperties": false
}测试工具对这次输入只返回以下固定对象;其他商品返回包含原名称、status 为 unknown、available 为 null 的项。调用记录留在测试客户端本地,只记练习编号、调用关联代号、完成后的参数、返回结果与调用次数。没有读文件、网络访问或写库存的权限。
{
"items": [
{
"item": "松果杯",
"status": "found",
"available": 12
},
{
"item": "雾蓝笔",
"status": "found",
"available": 0
}
]
}- 第一轮发送任务和工具定义。验收者应看到结构化工具调用,工具名为 lookup_stock,完成后的参数为 {"items":["松果杯","雾蓝笔"]}。模型在普通文本里打印这段 JSON,不算调用。
- 客户端在参数完整且校验通过后执行一次只读工具,把固定对象作为工具结果返回,并关联到这次调用。不要另发一条无关联的用户消息冒充工具结果。
- 第二轮保留 API 所需的上一轮上下文,由模型读取工具结果后给出最终答案。将最终两项值与上节预先写定的标准答案比较,同时检查正常结束状态。
OpenAI 的工具调用文档以 Responses 为例,用 function_call 的 call_id 关联 function_call_output;Claude 的工具定义文档使用 name、description 和 input_schema。跨协议转换可以改变线上字段形式,不能丢失“哪个结果属于哪个调用”的语义。不要要求不同入口具有完全相同的原始调用 ID;在各自边界内能正确配对才是本例检查项。
本例把“恰好一次调用”作为最小用例的验收要求。如果模型拆成两次查询,先记为不符合本用例,不直接判定网关有缺陷;保留实际行为,调整为单次批量查询后重新编号。未看到工具调用、参数未通过或没有完整终态时,即使答案碰巧为 12 和 0,也不能标为通过。
把参数与返回值的边界写全
下列两个工具定义分别用于 Responses 的 tools 数组和 Anthropic Messages 的 tools 数组;它们是完整的单个工具对象,不是可直接发送的 HTTP 请求。输出 schema 是本练习在客户端检查固定返回值的约定,不是供应商请求参数。若目标模型或所用 SDK 不接受某项 schema 约束,记录原始报错,保留为不适用;不要无声删去约束后当作同一个测试。
fixtures/input-schema.json 完整文件
{
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"type": "string"
},
"minItems": 1,
"maxItems": 3
}
},
"required": [
"items"
],
"additionalProperties": false
}
fixtures/output-schema.json 完整文件
{
"type": "object",
"properties": {
"items": {
"type": "array",
"minItems": 1,
"maxItems": 3,
"items": {
"oneOf": [
{
"type": "object",
"properties": {
"item": {
"type": "string"
},
"status": {
"const": "found"
},
"available": {
"type": "integer",
"minimum": 0
}
},
"required": [
"item",
"status",
"available"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"item": {
"type": "string"
},
"status": {
"const": "unknown"
},
"available": {
"type": "null"
}
},
"required": [
"item",
"status",
"available"
],
"additionalProperties": false
}
]
}
}
},
"required": [
"items"
],
"additionalProperties": false
}
fixtures/openai-responses-tool.json 完整文件
{
"type": "function",
"name": "lookup_stock",
"description": "只读查询本练习的固定库存;按 items 顺序返回结果;未知商品返回 unknown;不预留、不扣减。",
"parameters": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"type": "string"
},
"minItems": 1,
"maxItems": 3
}
},
"required": [
"items"
],
"additionalProperties": false
},
"strict": true
}
fixtures/anthropic-tool.json 完整文件
{
"name": "lookup_stock",
"description": "只读查询本练习的固定库存;按 items 顺序返回结果;未知商品返回 unknown;不预留、不扣减。",
"input_schema": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"type": "string"
},
"minItems": 1,
"maxItems": 3
}
},
"required": [
"items"
],
"additionalProperties": false
}
}
Responses 在同一边界内核对 function_call.call_id 与 function_call_output.call_id;Anthropic 核对 tool_use.id 与 tool_result.tool_use_id,结果内容由用户消息中的 tool_result 块回送。不要把 Responses 的输出 item id 当成 call_id。转换边界两侧可以使用不同代号,但对应关系必须可核对;缺少边界证据时写“证据不足”,不要用相同答案补推关联成功。完整调用和工具结果应留在相应 API 所要求的上下文中。
依据:Claude 工具结果交换说明与前述 OpenAI function calling 文档。本文未运行任何 SDK,文件不绑定版本;实际测试时填写 SDK 版本。
15. 比较三条线路,每次只改变一个条件
沿用正文虚构的 relay-a/demo-model。实际操作前由读者替换成自己的、已获准接收这份公开练习的官方 API 线路,并确认模型确实支持工具调用。先固定供应商,不开路由组,也不在这轮同时测试回退、图片、真实仓库或供应商订阅登录。
| 记录号 | 路线 | 要回答的问题 | 本稿实际结果 |
|---|---|---|---|
| T0 | 测试客户端直连同一上游,使用其原生协议 | 这个工具、模型和客户端本身能否完成两轮 | 未执行 |
| T1 | 同一客户端经 Magpie,仍用上游原生入口 | 增加网关后,参数、关联和结果是否保留 | 未执行 |
| T2 | 同一上游,经 Magpie 改用需要转换的入口 | 改变协议后的工具往返是否仍完整 | 未执行 |
每条路线先非流式,再流式;分别开新会话。记录实际客户端与网关版本、入口路径、请求模型、实际上游、是否直通、工具调用次数、完整参数、返回的两项数据、最终答案和结束状态。若某个模型或测试客户端不支持某条路线,写“不适用”及依据;不能为了凑齐矩阵悄悄换另一个模型,然后宣称只比较了网关。
这是定位实验,不是性能排名。T0 都失败,应先检查工具定义、客户端上下文衔接和模型支持;T0 成功、T1 失败,查本机入口与网关路径;T1 成功、T2 失败,才把协议转换列为优先调查方向。即使出现后一种组合,也应保存最小事件证据,再判断是请求编码、上游能力还是响应转换,不能凭一轮现象直接归罪于某个组件。
T0 与 T1 固定同一供应商、上游模型、任务、schema、采样设置、输出上限及客户端工具实现;T2 只改变来访协议所需的请求编码,使用同一客户端的对应适配器,并记下适配器差异。固定一个 provider/model,不允许路由组、备用供应商或意图分类介入。若无法证实 T2 确实经过转换,只能写“未确认”,不能因入口名字不同就认定转换被测试。
2026 年 10 月 14 日另核对了当前主分支的gateway.go:请求路径有 passthrough 与 translate 分支,选择还受供应商与模型 API 条件影响。因此 T1/T2 是要记录实际走向的实验分类,不是仅凭端点即可保证的产品行为。该主分支会变化,不能与正文旧固定提交混称同一次源码或运行验收。
16. 三个负对照,识别“看上去已经成功”
这些是读者在隔离测试客户端中安排的负对照,本文未注入或执行。先保证正常用例已跑通;否则失败无法说明负对照有效。不要在生产请求中断网、删密钥或替换真实业务结果。
- 答案对,但没有工具事件。在一份人工练习记录中只写“松果杯:12;雾蓝笔:0”,调用记录为空。预定判定为不通过工具往返验收。它说明答案比较与协议证据必须同时存在;不能把这份人工记录说成任何模型实测。
- 未知与零混淆。把第二件改为不存在的“砂白尺”,输入顺序不变。工具固定返回松果杯 found/12、砂白尺 unknown/null;标准答案变为“松果杯:12;砂白尺:未知”。如果最终说砂白尺库存为 0,工具链即使正常结束,业务语义仍未通过。还需对照工具返回和最终回答,确定错误发生在哪一步。
- 参数只到一半就断开。在能播放合成事件的本地测试客户端中,仅提供参数片段 {"items":["松果杯",随后以中断结束;不发送完整参数和正常终态。预定结果是工具执行次数为 0,并把该轮记作中断。不能自行补齐括号、猜出第二件商品并调用工具。如果没有事件回放能力,这项保留为未测,不通过真实上游故障来硬凑结果。
Claude 的流式文档展示了 input_json_delta 的 partial_json 片段及最后的 message_stop。这解释了为什么单个片段可能不是完整 JSON,也解释了“收到文字”不能代替整个消息完成。对于其他入口,按各自协议和 SDK 的完整参数及终态信号验收,不把 message_stop 这个名字机械套用过去。本例要求完整参数且该轮正常结束后才执行工具,是这份保守测试客户端的约定,不是声称所有 SDK 都这样调度。
再检查调用关联与参数校验
正常用例通过后,可在隔离客户端中安排关联代号不匹配的负例,要求拒绝把结果接入另一调用。另分别提交空数组、四件商品、数字项和额外 buy 字段,要求参数校验失败且不执行工具。负例只检查这份客户端契约;没有测试条件就保持 UNTESTED,不伪造上游请求、SSE 块、会话日志或截图。下面是人工设计表,非供应商输出。
fixtures/negative-cases.json 完整文件
{
"fixture_kind": "manual_negative_designs_not_provider_transcripts",
"cases": [
{
"id": "answer_without_call",
"design": "人工记录仅含正确的 12/0 答案,无结构化调用事件",
"expected": "拒绝认定工具往返通过",
"status": "UNTESTED"
},
{
"id": "unknown_as_zero",
"design": "unknown 用例最终回答把砂白尺写成 0",
"expected": "业务语义失败;不把未知当零",
"status": "UNTESTED"
},
{
"id": "partial_arguments",
"design": "仅有不完整参数然后中断,没有完整参数及正常终态;不是实际 SSE 片段",
"expected": "工具调用次数 0;该轮中断,不补齐、不重放",
"status": "UNTESTED"
},
{
"id": "mismatched_call_id",
"design": "隔离客户端中的结果关联代号与调用不匹配",
"expected": "停止并记录关联失败;不能拼成成功会话",
"status": "UNTESTED"
},
{
"id": "invalid_parameters",
"design": "分别用空数组、4 件商品、非字符串项和额外字段检查客户端校验",
"inputs": [
{
"items": []
},
{
"items": [
"A",
"B",
"C",
"D"
]
},
{
"items": [
12
]
},
{
"items": [
"松果杯"
],
"buy": true
}
],
"expected": "校验拒绝,工具执行次数 0",
"status": "UNTESTED"
}
]
}
17. 失败后怎样恢复,并留下能复查的结论
若 T2 的参数已经损坏,先保留脱敏后的最小事件记录,恢复到此前已通过的 T1 或 T0 配置,新建会话重跑原固定输入。恢复通过只说明可暂时回到这条已测线路,不能把失败的转换路线记为已修复。不要把 T2 的半条历史拼进 T1 的新会话,也不要在排查中同时换网关版本、模型和工具定义。
若工具已经返回,而第二轮最终回答中断,本例因为工具只读,可以在新记录中重复整个练习;原记录仍标为中断。换成真实下单、文件写入或发信工具时,必须先核对副作用,不能沿用这里的重跑办法。正文的输出后回退边界仍适用,这一补充没有验证自动续写或自动重放。
人工验收单(空白模板,不是 Magpie 导出格式)
练习编号/执行时间:待填
网关版本/测试客户端版本/上游模型:待填
路线 T0、T1 或 T2/流式与否:待填
请求入口/实际供应商/是否直通:待填
工具名与完整参数/调用次数:待填
调用与结果是否正确配对:待填
工具固定返回/最终答案/结束状态:待填
独立标准答案对照:待填
结论:未执行、通过、不通过、不适用或证据不足
失败后的恢复路线/新练习编号:待填一份合格结论应写成“指定版本、指定入口、指定上游,通过这一组只读库存查询与终态检查”,而不是“Magpie 完全兼容全部工具”。分享记录前删除凭据、真实请求标识和业务内容;本练习只需要上述合成商品与本地代号。本次实际完成的是官方资料核对和案例设计;未安装 Magpie,未注册或执行测试工具,未调用模型,未运行网关协议转换,也没有测延迟、费用或线上稳定性。
逐文件保留练习与空白验收单
先将库存和参考答案留在测试者一侧,仅发送任务和工具定义给模型;工具被真实调用后,再回送对应固定结果。下方完整文件供保存与复查,cases-private.json 将提示词、参数和答案放在一起便于人工查看,切勿把整份文件喂给模型。worksheet.json 的实际观测字段全部为空,UNTESTED 不表示通过。参考答案允许排版不同,但商品顺序、数值与未知语义必须一致。
fixtures/inventory-private.json 完整文件
{
"fixture_kind": "manual_fixed_inventory_not_observed",
"stock": {
"松果杯": {
"on_hand": 17,
"reserved": 5
},
"雾蓝笔": {
"on_hand": 9,
"reserved": 9
}
},
"unknown_rule": "精确键匹配;不存在的键返回 unknown/null;不归一化、不联网;保持输入顺序和重复项"
}
fixtures/cases-private.json 完整文件
{
"fixture_kind": "manual_cases_and_reference_answers_not_observed",
"cases": [
{
"case_id": "normal",
"prompt": "请调用 lookup_stock,查询松果杯和雾蓝笔的可售数量,按输入顺序各写一行;查不到请写未知,不要下单。",
"arguments": {
"items": [
"松果杯",
"雾蓝笔"
]
},
"tool_result": {
"items": [
{
"item": "松果杯",
"status": "found",
"available": 12
},
{
"item": "雾蓝笔",
"status": "found",
"available": 0
}
]
},
"expected_human_answer": "松果杯:12\n雾蓝笔:0",
"execution_status": "UNTESTED"
},
{
"case_id": "unknown",
"prompt": "请调用 lookup_stock,查询松果杯和砂白尺的可售数量,按输入顺序各写一行;查不到请写未知,不要下单。",
"arguments": {
"items": [
"松果杯",
"砂白尺"
]
},
"tool_result": {
"items": [
{
"item": "松果杯",
"status": "found",
"available": 12
},
{
"item": "砂白尺",
"status": "unknown",
"available": null
}
]
},
"expected_human_answer": "松果杯:12\n砂白尺:未知",
"execution_status": "UNTESTED"
}
]
}
fixtures/reference-answers.txt 完整文件
人工预先确定的参考答案,不是模型输出
normal
松果杯:12
雾蓝笔:0
依据:17−5=12;9−9=0
unknown
松果杯:12
砂白尺:未知
依据:固定库存无砂白尺,不能把 unknown/null 写成 0
必须同时有正确结构化调用、完整参数、正确关联的结果、正常终态;答案正确本身不等于工具调用通过。所有真实路线均 UNTESTED。
fixtures/worksheet.json 完整文件
{
"document_kind": "blank_manual_worksheet",
"overall_status": "UNTESTED",
"rows": [
{
"route": "T0",
"streaming": false,
"case": "normal",
"status": "UNTESTED",
"run_id": null,
"executed_at": null,
"client_version": null,
"sdk_version": null,
"gateway_version": null,
"provider": null,
"actual_model": null,
"requested_model": null,
"entry_path": null,
"upstream_protocol": null,
"native_or_translated_evidence": null,
"tool_name": null,
"complete_arguments": null,
"call_id_local_alias": null,
"result_call_id_local_alias": null,
"call_count": null,
"tool_result": null,
"final_answer": null,
"first_round_terminal": null,
"final_round_terminal": null,
"oracle_comparison": null,
"evidence_location": null,
"failure": null,
"recovery_route": null,
"recovery_run_id": null
},
{
"route": "T0",
"streaming": false,
"case": "unknown",
"status": "UNTESTED",
"run_id": null,
"executed_at": null,
"client_version": null,
"sdk_version": null,
"gateway_version": null,
"provider": null,
"actual_model": null,
"requested_model": null,
"entry_path": null,
"upstream_protocol": null,
"native_or_translated_evidence": null,
"tool_name": null,
"complete_arguments": null,
"call_id_local_alias": null,
"result_call_id_local_alias": null,
"call_count": null,
"tool_result": null,
"final_answer": null,
"first_round_terminal": null,
"final_round_terminal": null,
"oracle_comparison": null,
"evidence_location": null,
"failure": null,
"recovery_route": null,
"recovery_run_id": null
},
{
"route": "T0",
"streaming": true,
"case": "normal",
"status": "UNTESTED",
"run_id": null,
"executed_at": null,
"client_version": null,
"sdk_version": null,
"gateway_version": null,
"provider": null,
"actual_model": null,
"requested_model": null,
"entry_path": null,
"upstream_protocol": null,
"native_or_translated_evidence": null,
"tool_name": null,
"complete_arguments": null,
"call_id_local_alias": null,
"result_call_id_local_alias": null,
"call_count": null,
"tool_result": null,
"final_answer": null,
"first_round_terminal": null,
"final_round_terminal": null,
"oracle_comparison": null,
"evidence_location": null,
"failure": null,
"recovery_route": null,
"recovery_run_id": null
},
{
"route": "T0",
"streaming": true,
"case": "unknown",
"status": "UNTESTED",
"run_id": null,
"executed_at": null,
"client_version": null,
"sdk_version": null,
"gateway_version": null,
"provider": null,
"actual_model": null,
"requested_model": null,
"entry_path": null,
"upstream_protocol": null,
"native_or_translated_evidence": null,
"tool_name": null,
"complete_arguments": null,
"call_id_local_alias": null,
"result_call_id_local_alias": null,
"call_count": null,
"tool_result": null,
"final_answer": null,
"first_round_terminal": null,
"final_round_terminal": null,
"oracle_comparison": null,
"evidence_location": null,
"failure": null,
"recovery_route": null,
"recovery_run_id": null
},
{
"route": "T1",
"streaming": false,
"case": "normal",
"status": "UNTESTED",
"run_id": null,
"executed_at": null,
"client_version": null,
"sdk_version": null,
"gateway_version": null,
"provider": null,
"actual_model": null,
"requested_model": null,
"entry_path": null,
"upstream_protocol": null,
"native_or_translated_evidence": null,
"tool_name": null,
"complete_arguments": null,
"call_id_local_alias": null,
"result_call_id_local_alias": null,
"call_count": null,
"tool_result": null,
"final_answer": null,
"first_round_terminal": null,
"final_round_terminal": null,
"oracle_comparison": null,
"evidence_location": null,
"failure": null,
"recovery_route": null,
"recovery_run_id": null
},
{
"route": "T1",
"streaming": false,
"case": "unknown",
"status": "UNTESTED",
"run_id": null,
"executed_at": null,
"client_version": null,
"sdk_version": null,
"gateway_version": null,
"provider": null,
"actual_model": null,
"requested_model": null,
"entry_path": null,
"upstream_protocol": null,
"native_or_translated_evidence": null,
"tool_name": null,
"complete_arguments": null,
"call_id_local_alias": null,
"result_call_id_local_alias": null,
"call_count": null,
"tool_result": null,
"final_answer": null,
"first_round_terminal": null,
"final_round_terminal": null,
"oracle_comparison": null,
"evidence_location": null,
"failure": null,
"recovery_route": null,
"recovery_run_id": null
},
{
"route": "T1",
"streaming": true,
"case": "normal",
"status": "UNTESTED",
"run_id": null,
"executed_at": null,
"client_version": null,
"sdk_version": null,
"gateway_version": null,
"provider": null,
"actual_model": null,
"requested_model": null,
"entry_path": null,
"upstream_protocol": null,
"native_or_translated_evidence": null,
"tool_name": null,
"complete_arguments": null,
"call_id_local_alias": null,
"result_call_id_local_alias": null,
"call_count": null,
"tool_result": null,
"final_answer": null,
"first_round_terminal": null,
"final_round_terminal": null,
"oracle_comparison": null,
"evidence_location": null,
"failure": null,
"recovery_route": null,
"recovery_run_id": null
},
{
"route": "T1",
"streaming": true,
"case": "unknown",
"status": "UNTESTED",
"run_id": null,
"executed_at": null,
"client_version": null,
"sdk_version": null,
"gateway_version": null,
"provider": null,
"actual_model": null,
"requested_model": null,
"entry_path": null,
"upstream_protocol": null,
"native_or_translated_evidence": null,
"tool_name": null,
"complete_arguments": null,
"call_id_local_alias": null,
"result_call_id_local_alias": null,
"call_count": null,
"tool_result": null,
"final_answer": null,
"first_round_terminal": null,
"final_round_terminal": null,
"oracle_comparison": null,
"evidence_location": null,
"failure": null,
"recovery_route": null,
"recovery_run_id": null
},
{
"route": "T2",
"streaming": false,
"case": "normal",
"status": "UNTESTED",
"run_id": null,
"executed_at": null,
"client_version": null,
"sdk_version": null,
"gateway_version": null,
"provider": null,
"actual_model": null,
"requested_model": null,
"entry_path": null,
"upstream_protocol": null,
"native_or_translated_evidence": null,
"tool_name": null,
"complete_arguments": null,
"call_id_local_alias": null,
"result_call_id_local_alias": null,
"call_count": null,
"tool_result": null,
"final_answer": null,
"first_round_terminal": null,
"final_round_terminal": null,
"oracle_comparison": null,
"evidence_location": null,
"failure": null,
"recovery_route": null,
"recovery_run_id": null
},
{
"route": "T2",
"streaming": false,
"case": "unknown",
"status": "UNTESTED",
"run_id": null,
"executed_at": null,
"client_version": null,
"sdk_version": null,
"gateway_version": null,
"provider": null,
"actual_model": null,
"requested_model": null,
"entry_path": null,
"upstream_protocol": null,
"native_or_translated_evidence": null,
"tool_name": null,
"complete_arguments": null,
"call_id_local_alias": null,
"result_call_id_local_alias": null,
"call_count": null,
"tool_result": null,
"final_answer": null,
"first_round_terminal": null,
"final_round_terminal": null,
"oracle_comparison": null,
"evidence_location": null,
"failure": null,
"recovery_route": null,
"recovery_run_id": null
},
{
"route": "T2",
"streaming": true,
"case": "normal",
"status": "UNTESTED",
"run_id": null,
"executed_at": null,
"client_version": null,
"sdk_version": null,
"gateway_version": null,
"provider": null,
"actual_model": null,
"requested_model": null,
"entry_path": null,
"upstream_protocol": null,
"native_or_translated_evidence": null,
"tool_name": null,
"complete_arguments": null,
"call_id_local_alias": null,
"result_call_id_local_alias": null,
"call_count": null,
"tool_result": null,
"final_answer": null,
"first_round_terminal": null,
"final_round_terminal": null,
"oracle_comparison": null,
"evidence_location": null,
"failure": null,
"recovery_route": null,
"recovery_run_id": null
},
{
"route": "T2",
"streaming": true,
"case": "unknown",
"status": "UNTESTED",
"run_id": null,
"executed_at": null,
"client_version": null,
"sdk_version": null,
"gateway_version": null,
"provider": null,
"actual_model": null,
"requested_model": null,
"entry_path": null,
"upstream_protocol": null,
"native_or_translated_evidence": null,
"tool_name": null,
"complete_arguments": null,
"call_id_local_alias": null,
"result_call_id_local_alias": null,
"call_count": null,
"tool_result": null,
"final_answer": null,
"first_round_terminal": null,
"final_round_terminal": null,
"oracle_comparison": null,
"evidence_location": null,
"failure": null,
"recovery_route": null,
"recovery_run_id": null
}
]
}
下载完整手工验收离线包(SHA-256:a9a762c84474713b87722a34ee9fcee12102f4f7e92fc85fdb120ec57af3962a)。包内包含全文、完整夹具、空白验收单、参考答案与插图,不含实际模型输出或内部工作记录。
本文是否实际运行和测试了 Magpie?
没有。本文核查官方文档与固定源码,完成示例语法和图示检查,未安装网关、读取凭据、发送模型请求或测试性能。
订阅能通过网关任意转接吗?
不能据技术接入能力推断许可。各供应商的认证、账户、额度与服务条款必须分别核查,文中说明了 Google CLI OAuth 等路径的限制。