OpenAI Agents SDK 工程笔记:ModelProvider 多模型接入、Responses/Chat Completions 切换与生产禁区

千笔-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 切换后的模型类;右侧为请求实际打到的路径;底部为五条实测失败。

目标说明

读完你应能独立完成五件事:

  1. 说清 Agent.model 是字符串时,由哪个 ModelProvider 解析,以及 MultiProvider 默认把无前缀和 openai/ 交给谁。
  2. 在「只用 OpenAI」「接一个兼容端点」「多个前缀混用」三条路径里选对接入方式,并知道何时必须 set_tracing_disabled(True) 或单独设 tracing key。
  3. 解释为什么默认是 Responses 形状,第三方只实现 Chat Completions 时会出现什么错误,以及 strict_feature_validation 的作用。
  4. 分清 openai_prefix_mode / unknown_prefix_mode 的 alias 与 model_id,避免前缀拼错被静默透传。
  5. 把五条 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 分钟落地

  1. 5 分钟:列出所有 Agent(model=...) 与 RunConfig(model_provider=...),标出带 / 的名字。
  2. 10 分钟:把 _w/provider-smoke/smoke.py 的 MockTransport 接到预发网关,只跑 P1/F3/F5。
  3. 5 分钟:检查是否调用过 set_default_openai_client;有则核对 use_for_tracing。
  4. 5 分钟:给 openai_prefix_mode / unknown_prefix_mode 写进部署文档默认值。
  5. 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 接入收成三条,对应不同「爆炸半径」:

  1. 全局默认 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。
  2. Run 级 ModelProvider :RunConfig(model_provider=OpenAIProvider(...)) 或自写 ModelProvider。适合「这一次跑法换 provider,不污染全局」。本文 smoke 的 P3/F3/F4/F5 都走这条,方便在 CI 里对单一入口做断言。
  3. 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。

相关推荐
AIGC大时代8 天前
OpenAI Agents SDK 工程笔记:MCP 工具接入与生产禁区
mcp·生产禁区·openai agents sdk·mcpserverstdio·hostedmcptool
AIGC大时代22 天前
OpenAI Agents SDK 工程笔记:sessions 记忆边界再核对与生产禁区
生产禁区·sessions·sqlitesession·sessionsettings
AIGC大时代25 天前
OpenAI Agents SDK 工程笔记:streaming 流式输出与生产禁区
openai·streaming·生产禁区·run_streamed·streamevent
AIGC大时代1 个月前
OpenAI Agents SDK 工程笔记:handoffs 多 Agent 交接与生产禁区
openai·multi agent·生产禁区·handoffs·triage
AIGC大时代1 个月前
OpenAI Agents SDK 工程笔记:tool 调用循环、max_turns 与生产禁区
服务器·数据库·笔记·tool·max_turns·functiontool·生产禁区
曲幽9 个月前
FastAPI响应实战:从JSON到HTML,轻松驾驭多种数据格式
python·html·json·fastapi·web·jinja2·responses