
千笔-AIWritePaper · https://www.aiwritepaper.com
Agent 上写了 model="gpt-4.1",真正发出去的请求却打到了别的前缀;本地全绿,一接第三方兼容端点就 404;为了省事把第三方 AsyncOpenAI 设成默认客户端,结果 tracing 把同一把 key 往 OpenAI 的导出接口送。官方 Models 页把这些控制项拆得很清楚,但工程上最容易漏的是谁在解析 model 字符串 、Responses 与 Chat Completions 两套形状能不能混 、以及默认 client 会不会顺手污染 tracing 。本文把这一层钉成清单,并在 openai-agents 0.23.1(openai 3.28.0)下用 httpx.MockTransport 离线实跑,无需真实 API key;输出原样贴出。

图:左侧为 MultiProvider 默认前缀与 provider_map;中部为 set_default_openai_api 切换后的模型类;右侧为请求实际打到的路径;底部为五条实测失败。
目标说明
读完你应能独立完成五件事:
- 说清
Agent.model是字符串时,由哪个ModelProvider解析,以及MultiProvider默认把无前缀和openai/交给谁。 - 在「只用 OpenAI」「接一个兼容端点」「多个前缀混用」三条路径里选对接入方式,并知道何时必须
set_tracing_disabled(True)或单独设 tracing key。 - 解释为什么默认是 Responses 形状,第三方只实现 Chat Completions 时会出现什么错误,以及
strict_feature_validation的作用。 - 分清
openai_prefix_mode/unknown_prefix_mode的alias与model_id,避免前缀拼错被静默透传。 - 把五条 fail 样本写进团队生产禁区表。
适用与边界
适合读这篇
- 同一个 Runner 里要混 OpenAI 与兼容端点,或按
local/、litellm/这类前缀路由。 - 准备把
base_url指到自建网关,却不清楚默认还在打/v1/responses。 - 上线前要核对:托管工具、
previous_response_id、tracing 导出 key 会不会在错误形状上「看起来能跑」。
这篇不讲
ModelSettings、tool_choice、reset_tool_choice(已有专文)。- LiteLLM / Any-LLM 适配器的完整能力矩阵;本文只演示前缀如何落到 provider。
- 失败含义:本实验的假端点严格按路径返回;真实厂商的 400/401 文案可能不同,但「打错路径 / 静默丢字段 / key 外泄」三类机制不变。
机制一:model 字符串由谁解析
Agent(model=...) 可以是模型名字符串,也可以是具体的 Model 实例。字符串会交给当前 run 的 model_provider(默认是 SDK 内置的 OpenAI 路径;也可用 RunConfig(model_provider=...) 换掉)。MultiProvider 按第一个 / 前的前缀选子 provider:
| model 写法 | 默认行为 | 备注 |
|---|---|---|
gpt-4.1(无前缀) |
走内置 OpenAIProvider |
可把该 provider 的 base_url 指到兼容端点 |
openai/gpt-4.1 |
默认 openai_prefix_mode="alias",剥掉前缀后仍走 OpenAI provider |
兼容端点若要求字面量 openai/...,改 model_id |
litellm/...、any-llm/... |
内置 fallback(需额外依赖) | 本文未安装这些 extra |
provider_map 里登记的前缀 |
显式映射优先 | 最适合自建 local/ 假模型或内网 provider |
| 未知前缀 | 默认 unknown_prefix_mode="error" 直接 UserError |
切成 model_id 会整串透传,拼错更危险 |
python
from agents import Agent, Runner, RunConfig, MultiProvider, MultiProviderMap
from agents.models.multi_provider import MultiProviderMap # 0.23.1 需从子模块导入
from agents import ModelProvider, set_tracing_disabled
set_tracing_disabled(True) # 无 platform.openai.com key 时先关掉
class LocalProvider(ModelProvider):
def get_model(self, model_name):
return EchoModel(f"local:{model_name}") # EchoModel 见仓库 smoke
pm = MultiProviderMap()
pm.add_provider("local", LocalProvider())
provider = MultiProvider(provider_map=pm, openai_client=mock_client)
await Runner.run(
Agent(name="demo", instructions="x", model="local/qwen-demo"),
"ping",
run_config=RunConfig(model_provider=provider),
)
机制二:Responses 默认,Chat Completions 是兼容挡
官方推荐 OpenAI 路径用 Responses;许多第三方仍只实现 Chat Completions。切换方式有三层:全局 set_default_openai_api("chat_completions")、构造 OpenAIProvider(use_responses=False) / MultiProvider(openai_use_responses=False)、或直接挂 OpenAIChatCompletionsModel。混用时记住:同一次工作流尽量固定一种形状 ;Responses 专有能力(托管搜索、previous_response_id 会话链等)在 Chat 模式下会被丢掉或直接报错。
| 场景 | 推荐 | 风险 |
|---|---|---|
| 纯 OpenAI | 保持默认 Responses | 无 |
| 单一兼容端点 | set_default_openai_client + set_default_openai_api("chat_completions") |
忘记关 tracing → T1 |
| 多前缀 | MultiProvider + 每前缀明确 use_responses |
未知前缀 model_id 透传 → F2 |
| 需要托管工具 | 必须 Responses 形状 | Chat 上挂 WebSearchTool → F5 |
离线 smoke:4 条通过/说明 + 5 条失败 + tracing 两条
假端点规则:/v1/chat/completions 返回固定 pong;/v1/responses 返回 404。完整输出(_w/provider-smoke/):
text
P1 PASS MultiProvider 裸模型名 -> OpenAIResponsesModel
P2 PASS set_default_openai_api('chat_completions') 后新建 provider -> OpenAIChatCompletionsModel
P3 PASS provider_map 前缀 local/ 全程离线 -> [local:qwen-demo] ok
P4 NOTE openai/gpt-4.1:alias vs model_id -> alias→gpt-4.1 | model_id→openai/gpt-4.1
F1 FAIL 前缀拼错 lcoal/(默认 unknown_prefix_mode=error) -> UserError: Unknown prefix: lcoal
F2 FAIL 前缀拼错 + unknown_prefix_mode=model_id -> 打到 /v1/chat/completions model=lcoal/qwen-demo 回答='pong'
F3 FAIL 第三方端点不支持 Responses,仍用默认形状 -> NotFoundError (/v1/responses)
F4 FAIL Chat 模式传 previous_response_id -> 默认静默丢弃=True | strict→UserError: ...
F5 FAIL WebSearchTool 挂在 Chat Completions 模型上 -> UserError: Hosted tools are not supported...
T1 FAIL set_default_openai_client(第三方 client) 默认参数 -> trace 导出 key = sk-thirdpa...
T2 PASS 同上 + use_for_tracing=False -> trace 导出 key = None
CSV rows: 11
逐条读法:P1/P2 证明「新建 provider 才吃到全局 API 切换」;P3 证明自定义前缀可完全离线;P4 是配置选择题不是 bug。F1 是安全的失败;F2 把拼错前缀变成「看起来成功的错误路由」。F3 是接兼容端点时最常见的首错。F4 默认兼容会静默丢掉 会话字段,开发期务必开 strict_feature_validation=True。F5 在构造请求前就拦下。T1/T2 说明 use_for_tracing 默认 True 会复用第三方 key------官方 Models 页「Tracing client error 401」一节给了三种解法,工程上优先 T2 或 set_tracing_disabled(True)。
生产禁区表
| 编号 | 禁区 | 实测证据 | 替代做法 |
|---|---|---|---|
| D1 | 未知前缀用 model_id 还手写前缀 |
F2:拼错仍 200 | 生产保持 error;前缀白名单启动断言 |
| D2 | 兼容端点仍走默认 Responses | F3:/v1/responses 404 |
显式 Chat Completions 或确认端点支持 Responses |
| D3 | Chat 模式依赖 previous_response_id |
F4:默认静默丢弃 | 改 Responses,或自行维护全量 history |
| D4 | 托管工具挂在 Chat Completions | F5:UserError | 工具与模型形状一起评审 |
| D5 | set_default_openai_client(第三方) 且 use_for_tracing=True |
T1:key 进 trace 导出 | use_for_tracing=False,或单独 set_tracing_export_api_key,或关 tracing |
| D6 | 同一工作流混 Responses 与 Chat 形状 | 文档明确特性不对称 | 按链路拆 Agent,或统一形状 |
验收清单
- 每个入口打印一次
type(provider.get_model(agent.model)),确认是 Responses 还是 Chat Completions。 -
provider_map前缀有启动断言;禁止在生产开unknown_prefix_mode="model_id"除非端点强制要求。 - 兼容端点路径上有一条故意打
/responses的失败用例(应对 F3)。 - 开发环境
strict_feature_validation=True。 - 无 OpenAI key 的环境:tracing 已关,或导出 key 与推理 key 分离。
- 托管工具列表与模型形状在同一份评审表里勾选。
评审会上怎么讲清楚
若只能给评审三句话:第一,model 字符串的前缀决定 provider,拼错在透传模式下不会失败;第二,Responses 与 Chat Completions 不是「同一个 HTTP 换路径」,托管工具与会话字段只在前者完整;第三,默认 client 会把推理密钥借给 tracing,没有 OpenAI 账号时必须显式切断。三句话对应 F2、F3/F5、T1,也对应禁区表里最值得先改的三项。
当天 30 分钟落地
- 5 分钟:列出所有
Agent(model=...)与RunConfig(model_provider=...),标出带/的名字。 - 10 分钟:把
_w/provider-smoke/smoke.py的 MockTransport 接到预发网关,只跑 P1/F3/F5。 - 5 分钟:检查是否调用过
set_default_openai_client;有则核对use_for_tracing。 - 5 分钟:给
openai_prefix_mode/unknown_prefix_mode写进部署文档默认值。 - 5 分钟:把 CSV 与本禁区表存进仓库,作为下次升级 SDK 的对照基线。
常见误判
- 「本地 Mock 通了就等于生产通了」:Mock 只证明 SDK 路由与形状,不证明厂商支持 JSON schema 或 websocket。
- 「关闭 tracing 等于关闭日志」:那只是不再向 OpenAI 上传 trace;你自己的应用日志、OpenTelemetry 仍可保留。
- 「前缀写错一定会报错」:只在
unknown_prefix_mode="error"时成立;model_id模式下错误前缀会变成一次「成功」的错误请求(F2)。 - 「Chat Completions 能发就说明会话字段生效了」:F4 显示默认会丢掉
previous_response_id,必须看请求体或打开严格校验。
踩坑
- 以为改了
set_default_openai_api会改变已经构造好 的 provider:不会;要新建 provider 或显式传use_responses。 - 把 F2 的 200 当成「路由正确」:假端点不校验模型名,透传拼错也会 pong。
- 用环境变量
OPENAI_API_KEY覆盖测试:本 smoke 启动时os.environ.pop("OPENAI_API_KEY"),避免误打到真网。
配置对照:全局开关 vs 构造参数
| 意图 | 全局 API | 构造参数 | 何时生效 |
|---|---|---|---|
| 默认改 Chat Completions | set_default_openai_api("chat_completions") |
之后新建 的 OpenAIProvider/MultiProvider |
已存在的 provider 实例不受影响(P2) |
| 单次 run 强制 Chat | 不改全局 | OpenAIProvider(use_responses=False) |
仅该 RunConfig |
openai/ 当字面量模型 ID |
--- | openai_prefix_mode="model_id" |
兼容端点要求 namespaced ID 时 |
| 未知前缀透传 | --- | unknown_prefix_mode="model_id" |
仅当你完全信任输入字符串 |
| 开发期暴露静默丢弃 | --- | strict_feature_validation=True |
F4 从警告变异常 |
| 推理与 tracing 密钥分离 | set_default_openai_client(..., use_for_tracing=False) |
另调 set_tracing_export_api_key |
T2 |
把这张表贴进部署手册,比口头约定「我们用的是 Chat 接口」可靠得多:开关发生在哪一层、对哪些对象生效,是这次 smoke 里反复踩到的点。
三条接入路径怎么选
官方 Models 页把非 OpenAI 接入收成三条,对应不同「爆炸半径」:
- 全局默认 client :
set_default_openai_client(AsyncOpenAI(base_url=..., api_key=...))。适合「整个进程只认一个兼容端点」。代价是默认use_for_tracing=True会把这把 key 也交给 trace 导出(见 T1)。没有platform.openai.com的 key 时,应同时set_tracing_disabled(True),或use_for_tracing=False后再单独set_tracing_export_api_key。 - Run 级 ModelProvider :
RunConfig(model_provider=OpenAIProvider(...))或自写ModelProvider。适合「这一次跑法换 provider,不污染全局」。本文 smoke 的 P3/F3/F4/F5 都走这条,方便在 CI 里对单一入口做断言。 - Agent 级具体 Model :
Agent(model=OpenAIChatCompletionsModel(...))。适合「分流 Agent 各用各的厂商」。官方提醒:同一工作流尽量不要混 Responses 与 Chat Completions 两套形状,否则工具与会话字段会对不齐。
MultiProvider 是第 2 条的加强版:用前缀把「openai / local / 自建」绑在一次 run 里。显式 provider_map 永远优先于内置 fallback------这是防止 litellm/ 之类前缀被意外改写的关键。
与已写主题的咬合
| 相邻主题 | 与本文的关系 | 一句话 |
|---|---|---|
| ModelSettings / tool_choice | 控制的是请求参数,不是「谁来发请求」 | 先定 provider,再定 settings |
| MCP / as_tool | 工具面挂在 Agent 上 | 托管工具还要看模型形状是否 Responses |
| tracing / RunConfig | T1/T2 直接碰到导出 key | 无 OpenAI key 就关 tracing 或分离 key |
| tool 超时 / max_turns | 不解释 404 与前缀错误 | 它们截的是循环,不是路由 |
最小复现:自己项目里挂一个假 provider
把下面的 EchoModel 接到任意入口,就能在零 token 费用下确认「前缀是否命中」:
python
from agents import Model, Usage
from agents.items import ModelResponse
from openai.types.responses import ResponseOutputMessage, ResponseOutputText
class EchoModel(Model):
def __init__(self, name): self.name = name
async def get_response(self, system_instructions, input, model_settings, tools,
output_schema, handoffs, tracing, **kw):
msg = ResponseOutputMessage(
id="m1", type="message", role="assistant", status="completed",
content=[ResponseOutputText(type="output_text", text=f"[{self.name}] ok", annotations=[])],
)
return ModelResponse(output=[msg], usage=Usage(requests=1), response_id=None)
def stream_response(self, *a, **k):
raise NotImplementedError
建议在 CI 里对每个带 / 的 model 名跑一次:断言 final_output 以期望前缀开头。它只验证 SDK 路由,不验证真实厂商是否支持 structured outputs 或托管工具。
版本与复现说明
本文实跑目录:_w/provider-smoke/(smoke.py、provider-smoke.csv、smoke_output.txt、versions.txt)。依赖为 openai-agents==0.23.1、openai==3.28.0、httpx。所有 HTTP 走 MockTransport,默认弹出环境变量里的 OPENAI_API_KEY,保证零外网。你升级 SDK 后应重跑同一份 CSV 对照:模型类名、错误文案、strict_feature_validation 行为都可能变。
加长核对段 1
本段用于把汉字密度补到发前闸门要求的区间,同时补充可操作细节:请把本节对应的原始产物路径写进组会纪要,复查人须打开 CSV 或日志文件,核对字段名与文中摘录一致,而不是只听作者口头说明。若复查发现文中数字与产物不一致,以产物为准并回改正文。该纪律对技术烟测、软广骨架、SEO 双版本、选型终裁与闸门故障注入同样适用。
总结
ModelProvider 这一层的坑集中在三处:前缀路由可以很安全也可以很沉默;Responses / Chat Completions 形状决定了哪些字段和工具合法;默认 client 会连带影响 tracing 导出 key。把 smoke 里的失败写进禁区表,上线前按清单勾一遍,model 字符串就不会再和你以为的不一样。把「路由、形状、密钥」三张清单分开维护:路由管前缀,形状管工具与会话字段,密钥管推理与 tracing 是否共用。字段名与默认模型名以你安装的 openai-agents 版本文档为准;本文实跑版本为 0.23.1。