先说清边界

本文聚焦「怎么改」,而不是「字段等于什么」。所有具体的模型名、base_url、参数取值范围、错误码含义,都以官方文档和实际接口返回为准;下文给出的适配层结构属于工程分析,可独立于任何一端的实现细节使用。

一、先判断差异属于哪一层

把迁移问题拆成三层,能避免把「协议差异」和「模型行为差异」混在一起排查:

  1. 协议层:请求路径、鉴权头字段名、请求体的字段名与嵌套结构、返回体的包裹方式。
  2. 语义层:同一字段在不同后端下的取值范围、默认值、上限,以及参数之间的互相约束。
  3. 行为层:流式分片的切分粒度、事件类型、错误分类、限流与重试语义。

实践中最常见的情况是:换后端就报 4xx,多半是语义层;流式输出「少半句话」或解析中断,多半是行为层;而「密钥明明对却 401」,通常只是协议层的 base_url 或鉴权头拼错。适配层的价值就是让三层差异只收敛在一个文件里。

二、请求体字段:先盘点,再映射

不要逐字段去改业务代码。做法是先把现网调用中真正用到的参数列成清单,再按四类处理:

  • 两端同名同义:如消息数组的 role/content、采样类参数、输出长度上限一类。可以透传,但要留意默认值与可设区间可能不同。
  • 同名不同形:如消息内容既可能是字符串也可能是分段数组、工具调用相关字段的嵌套层级、结构化输出的表达方式。
  • 只有一端存在:某端特有的扩展开关。这类字段必须由适配层拦截,不能让它们原样落到另一个后端。
  • 语义相同但需换算:停止符数量上限、部分惩罚系数的取值区间等。

工程上建议维护一张「能力表」而不是散落的 if 分支,并用配置开关选择 profile:

const profiles = {
  openai: { baseUrl: "", authHeader: "Authorization", caps: { /* ... */ } },
  deepseek: { baseUrl: "", authHeader: "Authorization", caps: { /* ... */ } },
};

type Caps = {
  models: string[];
  maxOutputTokensKey: string;
  supportsTools: boolean;
  supportsJsonSchema: boolean;
  streamUsage: boolean; // 流式末帧是否带用量
  stopLimit: number;
};

调用前做一次 normalize(request),把业务层的「中立请求」翻译成目标后端的请求体;调用后再做一次 denormalize(response)。业务代码只认中立结构,切换后端只改 profile。

三、流式返回:按行缓冲,别按网络分片解析

兼容接口普遍返回 SSE。解析时有两条纪律:按行或按空行分隔缓冲,不能假设「一个网络分片 = 一条完整事件」;也不要假设结束标记一定存在、用量一定出现在首帧或末帧。

常见坑有三个:一是把增量文本直接拼给 UI,却忽略了工具调用参数也是分片下发的;二是把结束原因当成只在最后出现,实际可能单独成帧;三是把多个事件粘在一个分片里时只解析了第一条。

适配层应把每个事件归一化为统一类型,例如 delta、tool_delta、usage、done、error,再由上层消费。这样上层 UI 与状态机不需要知道当前用的是哪一套后端。

四、错误处理与鉴权

错误至少分三类:

  • 请求格式类(4xx):重试无用,应记录、告警并按策略降级。
  • 限流与超时类:需要带退避的重试,并区分请求是否幂等。
  • 鉴权类:密钥错误、头字段名不符、base_url 拼接错误都会落到这里。

鉴权头字段名、前缀写法、是否支持额外的组织级头,都应在适配层统一注入,不要在业务分支里散写。重试策略也建议放在适配层而非直接依赖 SDK 客户端,因为它与后端的错误体结构强相关;流式请求的重试必须约束在首帧到达之前。

五、可落地的改造顺序

  1. 抽出 client 工厂:base_url、鉴权头、超时、重试统一收口。
  2. 引入中立请求/响应类型,业务代码不再直接构造 SDK 请求体。
  3. 编写 normalize / denormalize 两个纯函数,并补单测:同一份中立请求在两个 profile 下应各自产出合法请求体。
  4. 用契约测试回放真实响应样本,覆盖流式、非流式与错误响应,断言归一化后结构一致。
  5. 灰度切换 profile,重点观察日志中被拦截的「只在一端存在」的字段——这些就是后续要补齐的能力缺口。

结论

迁移的核心不是背下两套参数表,而是让差异只出现在一个可测试的边界上:能力表描述差异,normalize/denormalize 收敛差异,契约测试锁住差异。做到这三点,同时支持 OpenAI SDK 与 DeepSeek API 就不再是维护两套调用逻辑,而是切换一个配置。