返回博客

尽量少改旧项目:让 Chat Completions 兼容 Responses API

人工智能2408
尽量少改旧项目:让 Chat Completions 兼容 Responses API

很多 AI 应用已经围绕 /v1/chat/completions 写好了对话、流式输出和工具调用。遇到优先使用或仅支持 Responses API 的上游模型时,是否必须立即重构所有客户端?本文从 API 网关的协议转换代码出发,介绍一种对业务侵入更小的迁移方式。


1. 一个容易被低估的接口升级问题

假设你的项目已经稳定运行了一段时间,客户端发送的是大家熟悉的 Chat Completions 请求:

json
{
  "model": "your-model",
  "messages": [
    {
      "role": "system",
      "content": "你是一名编程助手"
    },
    {
      "role": "user",
      "content": "解释一下 Go 的 context"
    }
  ],
  "stream": true
}

后来,你准备接入的新模型需要使用 /v1/responses。虽然简单文本消息可以较平滑地迁移,但工具调用、结构化输出和流式响应仍存在明显差异:

如果让每个前端、后端和第三方客户端分别升级,改造范围会迅速扩大。更麻烦的是,同一套业务可能还需要同时调用旧接口模型和新接口模型。

这类问题更适合在 API 网关层解决:客户端继续发送 Chat Completions,网关根据模型和渠道配置,将请求转换成 Responses API,再把响应转换回来。


2. 转换发生在哪里

完整链路可以概括为:

text
旧版业务客户端

    │ POST /v1/chat/completions

OpenAI 兼容网关
    │  识别模型与渠道
    │  Chat 请求 → Responses 请求

Responses API 上游

    │ Responses 普通响应或 SSE 事件

OpenAI 兼容网关
    │  Responses 响应 → Chat 响应

旧版业务客户端

对于兼容层已覆盖的参数,客户端看到的入口和返回格式基本不变,协议升级被收敛在网关内部。不能无损映射的参数仍需明确报错或降级处理。

4SAPI 这类聚合中转服务为例,业务侧仍然可以使用 OpenAI SDK,只需要统一配置 base_url 和 API Key:

python
from openai import OpenAI

client = OpenAI(
    base_url="https://4sapi.com/v1",
    api_key="sk-你的中转站Key",
)

response = client.chat.completions.create(
    model="your-model",
    messages=[
        {"role": "system", "content": "你是一名严谨的技术助手"},
        {"role": "user", "content": "用三个要点解释事件循环"},
    ],
    # 如业务不需要服务端保存响应,可显式关闭。
    store=False,
)

print(response.choices[0].message.content)

至于这个模型最终走 Chat Completions 还是 Responses API,可以交给网关配置决定。


3. 第一步:按渠道和模型决定是否转换

协议转换不应该对所有请求强制开启。更稳妥的做法是增加一层策略判断。项目中的核心配置结构可以简化表示为:

go
type ChatCompletionsToResponsesPolicy struct {
    Enabled       bool
    AllChannels   bool
    ChannelIDs    []int
    ChannelTypes  []int
    ModelPatterns []string
}

这套策略实际包含两层判断:

两层条件需要同时成立。特别要注意:model_patterns 为空时不会匹配任何模型。一个只对指定渠道和 GPT-5 系列模型生效的配置示例如下:

json
{
  "enabled": true,
  "all_channels": false,
  "channel_ids": [1, 2],
  "channel_types": [1],
  "model_patterns": ["^gpt-5.*$"]
}

当前实现使用客户端请求中的原始模型名匹配 model_patterns,而不是模型映射后的上游名称。例如客户端请求 my-gpt5,后台将其映射为 gpt-5,正则仍应匹配 my-gpt5

请求进入网关后,会结合已选择的渠道和原始模型名判断是否需要转换:

go
if relayMode == ChatCompletions &&
    channelMatched(channelID, channelType) &&
    modelPatternMatched(model) {
    return chatCompletionsViaResponses(ctx, request)
}

此外,全局或当前渠道开启请求体原样透传时,网关不会进入这条 Chat → Responses 转换链路,因为“透传”和“改写请求体”本身就是互斥行为。

这种设计的价值不只是“多一个开关”。它允许团队先用测试渠道验证,再逐步扩大范围,避免一次升级影响全部线上模型。


4. 第二步:把 messages 变成 input

请求转换不能只做字段改名,因为不同角色和内容类型的语义也要保留。

system 与 developer 消息

Chat Completions 中常见的:

json
{"role": "system", "content": "回答必须简洁"}

Responses API 可以接收消息形式的输入。为了让系统级指令与普通对话更清晰地分离,这套兼容实现选择将 systemdeveloper 文本汇总到 instructions

json
{
  "instructions": "回答必须简洁"
}

当请求里同时出现多条 systemdeveloper 消息时,兼容层会按照原顺序拼接。需要注意,这是一种网关实现策略,并不是唯一合法的迁移方式。

普通文本消息

用户和助手的历史消息进入 input

json
{
  "input": [
    {
      "role": "user",
      "content": "帮我检查这段代码"
    }
  ]
}

图片、音频与文件

当目标模型和上游接口支持相应模态时,消息可以按内容类型分别映射:

go
switch part.Type {
case "text":
    // input_text 或 output_text
case "image_url":
    // input_image
case "input_audio":
    // input_audio
case "file":
    // input_file
}

不同模型对图片、音频和文件的支持范围并不相同,兼容层完成格式转换不代表目标模型一定具备对应能力。这一层最好使用结构化 DTO 完成,不建议直接对 JSON 字符串做替换。否则一旦遇到嵌套内容、转义字符或空字段,很容易产生难以排查的请求错误。


5. 第三步:处理工具调用差异

工具调用是协议转换中最容易遗漏的一部分。

Chat Completions 的函数工具通常是嵌套结构:

json
{
  "type": "function",
  "function": {
    "name": "get_weather",
    "description": "查询天气",
    "parameters": {
      "type": "object",
      "properties": {
        "city": {"type": "string"}
      }
    }
  }
}

转换为 Responses API 时,需要将函数信息提升到工具对象:

json
{
  "type": "function",
  "name": "get_weather",
  "description": "查询天气",
  "parameters": {
    "type": "object",
    "properties": {
      "city": {"type": "string"}
    }
  }
}

这里还有一个容易忽略的差异:Chat Completions 的函数工具默认不是严格模式,而 Responses API 的函数工具默认使用严格模式。兼容层应显式保留或设置 strict,并检查 JSON Schema 是否满足严格模式要求。只转换 namedescriptionparameters,并不一定能保证两边行为完全一致。

历史消息中的工具结果也需要通过 call_id 与原调用关联:

json
{
  "type": "function_call_output",
  "call_id": "call_123",
  "output": "{\"temperature\":26}"
}

如果缺少 call_id,当前实现会将工具输出降级成带有标记的普通用户消息,避免构造一个无法关联的 function_call_output。这种降级虽然能继续请求,但语义已经改变,因此生产环境最好记录告警;如果业务强依赖工具调用完整性,也可以选择直接返回可读错误。


6. 第四步:把 Responses 响应还原给旧客户端

非流式响应相对直接:

  1. 读取 Responses API 返回体;
  2. 提取最终文本和工具调用;
  3. 组装为 choices[0].message
  4. 将输入、输出和总 Token 映射到 Chat Completions 的 usage

最终客户端仍然收到熟悉的结构。下面为了突出关键字段,省略了 idcreatedmodel 等内容:

json
{
  "object": "chat.completion",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "Go 的 context 主要用于取消、超时和传递请求级数据。"
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 18,
    "completion_tokens": 25,
    "total_tokens": 43
  }
}

如果上游没有返回完整用量,网关可以进行估算,但计费系统应区分“上游真实用量”和“本地估算用量”,避免将估算值当成精确数据。


7. 流式响应才是真正的难点

Chat Completions 的客户端通常期待连续收到:

text
data: {"object":"chat.completion.chunk","choices":[...]}

Responses API 则会按事件类型发送内容增量、工具参数增量、完成状态和用量信息。网关需要维护一小段流状态:

对于 response.output_text.delta 这类真正的增量事件,可以直接转发其中的新增文本;如果某些事件或上游返回的是累计快照,则要避免把已有内容重复发送。一个简单做法是记录上一次内容,只输出新增部分:

go
func deltaFromPrefix(previous, current string) string {
    if strings.HasPrefix(current, previous) {
        return current[len(previous):]
    }
    return current
}

转换完成后,旧版前端仍可继续使用原来的 SSE 解析逻辑,不需要理解 Responses API 的事件类型。需要额外注意:如果流在结束前中断,最终用量事件可能无法到达,计费与日志逻辑必须准备好处理这种情况。


8. 上线前要验证的兼容边界

协议兼容不等于所有参数都能无损转换。上线前至少要测试:

还要明确不支持的参数。例如兼容层无法表达 n > 1 时,应在请求阶段直接返回说明,而不是请求成功后只保留第一条结果。


9. 为什么把兼容逻辑放在中转层

对于同时接入多家模型的应用,把协议转换集中到中转层有三个直接收益:

  1. 旧业务改动更少
    已经接入 OpenAI SDK 的服务可以尽量保留原调用方式。

  2. 模型切换更灵活
    同一客户端可以按模型路由到不同上游,不需要知道每个模型使用哪套接口。

  3. 治理能力更集中
    鉴权、限流、日志脱敏、用量统计、错误映射和渠道切换都可以在同一层完成。

但它也不是“打开开关就永远兼容”。工具调用、流式事件和新参数仍然需要持续维护。使用第三方中转服务时,还应评估数据隐私、服务稳定性和业务合规要求。


10. 最小迁移方案

如果你的项目仍在调用 /v1/chat/completions,可以按下面的顺序迁移:

  1. 保留现有客户端请求格式;
  2. 将 OpenAI SDK 的 base_url 指向兼容中转地址;
  3. 在中转后台配置上游渠道和模型映射;
  4. 仅对测试渠道开启 Chat → Responses 转换;
  5. 分别验证文本、流式和工具调用;
  6. 检查日志、Token 统计与扣费结果;
  7. 验证通过后,再按渠道或模型逐步放量。

一句话总结:Responses API 的升级成本,不一定要由每个业务客户端分别承担。通过“请求转换 + 响应还原 + 策略开关”,旧版 Chat Completions 应用可以在明确兼容边界的前提下,以较小改动接入新的上游模型。

需要统一管理多模型接口时,可以参考 4SAPI 的 OpenAI 兼容入口,先用测试 Key 跑通非流式文本请求,再逐步验证流式输出、工具调用和结构化输出等生产能力。具体支持范围应以当前渠道、模型和平台文档为准。


参考资料

标签:响应API4SAPI中转平台API兼容架构设计

推荐阅读

探索更多前沿洞察与行业干货。