Mastra接入自定义OpenAI兼容API:url配置、静默5步限制与双层重试陷阱

Mastra 接入自定义 OpenAI 兼容端点的实测避坑指南。
Aman Kumar 在 Mastra 1.78.0 上对自定义 OpenAI 兼容端点做了系统实测,发现多处默认行为容易踩坑:model 对象的 id 中第一个斜杠前只是标签、url 必须自带 /v1、apiKey 不会回退环境变量;工具循环默认五步后静默停止并返回空文本;AI SDK 与 Mastra 两层重试会相乘,一次 503 可放大成 18 次请求、99 秒等待;模型数组故障转移在默认错误处理器下会先耗尽主模型六次请求;流式响应默认不带 stream_options 导致 usage 为空。作者逐项给出配置修正,并汇总成接入清单。
Mastra 是 TypeScript 生态里逐渐被人注意的 agent 与工作流框架,官方示例大多用 "openai/gpt-4o" 这类模型字符串,走内置 provider 注册表。一旦你想把同一个 agent 指向自建 vLLM、LiteLLM、Ollama 的 /v1 端点,或者任意 OpenAI 兼容网关,这条便捷路径就断了。
Mastra 用一个小配置对象解决这件事,不需要额外的 provider 包。但作者 Aman Kumar 在 @mastra/core 1.78.0 + Node 20.19.2 上对着一个记录全部请求体的本地 mock 服务(可返回工具调用、401、带 Retry-After 的 429、503)实测后,发现几个默认行为值得所有接入自定义端点的人提前知道。下面把结论拆开讲。

最小可用配置:object 形式的 model
最简写法是把 model 从字符串换成一个对象,包含 id、url、apiKey:
const agent = new Agent({
id: "support",
name: "support",
instructions: "You answer in two sentences.",
model: {
id: "apiclaw/YOUR_MODEL_ID",
url: "https://apiclaw.biz/v1",
apiKey: process.env.APICLAW_API_KEY,
},
});
另有一种 { providerId, modelId, url, apiKey } 形式,实测发出的请求完全一致。请求发到 /v1/chat/completions,user agent 是 ai-sdk/openai-compatible——底层就是 Vercel AI SDK 的 OpenAI 兼容 provider。整个过程没有 GET /v1/models 探测,也没有多余参数。
id 与 url 的四个坑
第一个斜杠前只是标签
id 里第一个 / 之前的部分会被剥掉,只有后面的内容作为 model 字段发出去。所以 id: "apiclaw/deepseek/deepseek-chat" 实际发送的是 "model": "deepseek/deepseek-chat"。这对模型 ID 本身含斜杠的网关是好事,但意味着你不能省略前缀。不提供 url 时,Mastra 会拿这个前缀去注册表查找,找不到就报 No API URL found for apiclaw/YOUR_MODEL_ID。
OpenAI 兼容端点之所以能作为通用标准,源于 OpenAI Chat Completions API 的广泛复刻:vLLM、LiteLLM、Ollama、LocalAI 等都以 /v1/chat/completions 作为统一入口,字段名与语义对齐 OpenAI。Mastra 底层的 Vercel AI SDK 提供了 @ai-sdk/openai-compatible provider,专门用于接入这类非官方实现。它只依赖最基础的 model、messages、tools 等字段,不做模型列表探测,也不强行加入供应商特有的参数。这正是 Mastra 的 model 对象能够直接指向自定义网关的原因:它本质上是在构造一个 OpenAI 兼容 provider 实例。
url 必须自带 /v1
Mastra 只是在你给的地址后面拼上 /chat/completions,不会自己补 /v1。写成 url: "http://127.0.0.1:8766",请求会发到 /chat/completions,多数服务直接 404。尾部多一个斜杠(.../v1/)无害,仍然落到 /v1/chat/completions。
apiKey 不回退到环境变量
省略 apiKey、只在环境里设 APICLAW_API_KEY,请求会连 Authorization 头都不带,服务端返回 401。自定义 provider 需要自己读取环境变量传进去。
api: "responses" 会切换端点
加上这个标志后,agent 改为 POST 到 /v1/responses,用 input 数组替代 messages。大量 OpenAI 兼容服务只实现了 Chat Completions,除非服务文档明确支持 Responses API,否则别开。作者的 mock 对这条路返回 404,agent 抛出 AI_APICallError: Unknown path /v1/responses。自定义 header 则和预期一致,headers: { "X-Team": "docs" } 会出现在每个请求上。
工具循环在 5 步后静默停止
工具调用是最耗时间排查的部分。正常的一次工具调用分两步:首个请求带 tools 和 "tool_choice": "auto",模型返回工具调用,Mastra 执行 execute 并发出第二个请求,携带 system、user、assistant tool call、tool result 四条消息。
当 mock 一直请求工具(能力较弱的模型判断不了何时结束时常这样),agent 发出了五次请求、执行了五次工具,然后正常返回——但 text 是空字符串,finishReason 是 "tool-calls"。不抛错、不打日志。如果代码只读 res.text,用户看到的就是一个空白回答。
作者给出两个修复方向:
// 每次调用调整步数预算
const res = await agent.generate(prompt, { maxSteps: 3 });
// 把以工具调用收尾的运行视为未完成
if (res.finishReason === "tool-calls" || !res.text) {
// 换更强模型重试,或返回明确错误
}
maxSteps: 3 时正好三次请求、三次工具执行。还要注意每一步都会重发完整对话,第五个请求已携带十条消息,按 token 计费成本正是从这里涨起来的;按请求数计费的套餐则是一个问题吃掉五次额度。
Agent 工具循环(tool loop)的工作方式是:模型返回 tool_calls 后,Agent 执行对应工具,把结果作为 role: "tool" 的消息追加到对话中,再次请求模型,直到模型不再请求工具或达到步数上限。由于每一轮都会重发完整对话历史,消息条数随步数线性增长,token 消耗也会快速上升。maxSteps 限制的正是这个循环的轮数。若模型能力不足、始终认为需要继续调用工具,循环就会在上限处截断,最终响应中没有面向用户的文本,只有工具调用记录。
重试有两层,而且会相乘
这是最出人意料的结果。把 agent 指向一个始终返回 503 的模型,配置与请求数如下:
- 默认配置:3 次请求,6 秒失败
- agent 上设
maxRetries: 0:仍是 3 次,6 秒 modelSettings: { maxRetries: 0 }:仍是 3 次,6 秒modelSettings: { maxRetries: 5 }:18 次请求,99 秒- agent 上设
errorProcessorDefaults: false:1 次请求,0.1 秒 generate调用上传errorProcessors: []:1 次请求,0 秒
第一层是 AI SDK 的模型调用重试,由 modelSettings.maxRetries(或 model 数组里某一项的 maxRetries)控制。它在 Mastra 里默认为 0,采用指数退避(日志里是 1、2、4、8、16 秒),并且在 429 时遵守 Retry-After: 2。
第二层是 Mastra 默认的错误处理器。不论 maxRetries 设成什么,它都会把失败的生成再重试两次,间隔约三秒。两层同时开启时,外层三次尝试各自跑满六次内层尝试,一个上游故障放大成 18 次请求、99 秒等待。401 则立即失败,不重试。
作者最终选择只留一层,并显式配置:
const agent = new Agent({
// ...
errorProcessorDefaults: false, // 关掉外层重试
});
await agent.generate(prompt, { modelSettings: { maxRetries: 2 } }); // 3 次尝试,遵守 Retry-After
也可以保留默认、把 maxRetries 压在 0。关键是别同时调高两层;如果你自己在 generate() 外面还包了自己的重试,那就是第三层。
指数退避(exponential backoff)是客户端重试的常见策略:每次失败后等待时间按倍数增长,通常还会加入随机抖动以避免大量请求同时重试造成二次冲击。Mastra 内层由 AI SDK 控制,日志中可见 1、2、4、8、16 秒的间隔;遇到 429 时则优先遵循服务端返回的 Retry-After 头。外层 Mastra 错误处理器则采用固定约三秒的间隔再试两次。两层叠加时,总请求数等于内层尝试次数乘以外层尝试次数,等待时间也相应累加,因此必须明确只保留一层重试。
用 model 数组做故障转移
model 也接受数组,一项失败就切下一项:
model: [
{ model: { id: "apiclaw/YOUR_PRIMARY_MODEL", url, apiKey }, maxRetries: 1 },
{ model: { id: "apiclaw/YOUR_BACKUP_MODEL", url, apiKey }, maxRetries: 0 },
],
这套机制本身可用,但在默认错误处理器仍开启的情况下,主模型先吃掉六次请求(两次内层尝试乘三次外层),备用模型才收到第一个请求,整个调用耗时 9 秒。依赖故障转移的话,务必关掉外层重试,让切换在一两次失败后就发生。
流式响应拿不到 usage
agent.stream() 文本流正常,但请求里没有 stream_options,遵循 OpenAI 行为的服务端就不会发 usage chunk,await stream.usage 返回空的 token 计数。要记录 token,就绕过 Mastra 的对象配置,直接用 AI SDK provider 构造模型实例再传给 Mastra:
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
const apiclaw = createOpenAICompatible({
name: "apiclaw",
baseURL: "https://apiclaw.biz/v1",
apiKey: process.env.APICLAW_API_KEY,
includeUsage: true,
});
const agent = new Agent({ id: "a", name: "a", instructions: "...", model: apiclaw("YOUR_MODEL_ID") });
这样请求会带上 "stream_options": {"include_usage": true},usage 就能报告输入、输出和总 token。generate() 两条路径下都能报 usage。
接入清单
把上述实测结论压成一份可直接对照的清单:
- 使用
{ id: "label/YOUR_MODEL_ID", url, apiKey },第一个斜杠之后的内容才会作为模型名发送。 url里必须带/v1,apiKey必须显式传入。- 除非服务端实现了
/v1/responses,否则不要设api。 - 工具运行后检查
finishReason,并且有意识地设置maxSteps。 - 只选一层重试:
errorProcessorDefaults: false加modelSettings.maxRetries,或者保留默认但把maxRetries设为 0。 - 流式场景要 token 统计,就用
createOpenAICompatible并设includeUsage: true。
这些结论是在 Mastra 1.78.0 上验证的,不同版本默认值可能变化,升级时值得对着 mock 服务重跑一遍。
相关推荐

Workbuddy零基础入门:桌面AI智能体如何落地工作流
Workbuddy桌面AI智能体零基础教程解读:从安装、积分领取到文件管理与自动化工作流搭建,对比CodeX的适用场景与成本差异,帮助职场人完成从AI提问者到AI管理者的思维转变。

OpenAI智能体新栈:计算机操作、决策API与超速工作流
OpenAI Dev Day发布智能体新栈:Computer Use操作速度超越普通人,Decisions API基于Luna实现极低延迟分类,Agents API开放同源计算机操作能力,GPT-6.1 Sol成本仅为Astra七分之一。本文梳理核心更新、实测数据与开发者落地要点。

梁文峰开源推理框架DeepInfo,硬刚美国禁令
DeepSeek在开源模型后再开源推理框架DeepInfo,包含GPU/CPU交叉流水线、小模型预判大模型复核、华为昇腾与英伟达芯片全面适配等技术,直面美国联邦禁令。本文解析其对AI产业落地的意义与开源策略。