Build in Public · LF-09
用 OpenAI SDK 调 Gemini:从最小文本请求到可回滚的迁移
分清 SDK、服务地址和模型的兼容边界,验证非流式文本请求、错误与计费信息,再按任务样本逐项恢复参数,避免把客户端通过当成服务全面兼容。
迁移一个已经使用 OpenAI SDK 的应用,最容易低估的工作不是换地址,而是重新确认哪些行为仍成立。一个请求成功返回文字,只说明这条输入在这个服务上得到了结果;它不能同时证明旧提示词效果不变、流式可用、工具调用正确,以及异常后的账务状态一致。
本文以 EveryInfra 的 Gemini 非流式文本入口为例,给出一个可以逐项验收的迁移顺序:先锁定配置,再验证最小请求,然后检查响应与错误,最后恢复业务参数并决定是否扩大使用。它不是把所有 OpenAI 功能迁到另一个服务的通用兼容承诺。
兼容的是哪一层
首先分清三个对象:OpenAI SDK 是客户端库;服务地址决定实际接收请求的系统;模型 ID 必须属于那个服务当前提供的目录。使用同一个 npm 包,并不代表使用同一把 Key、同一套模型名或同一份服务合同。
Google 的 OpenAI 兼容层文档提供了用 OpenAI SDK 访问 Google Gemini 服务的示例。本文使用的则是 EveryInfra 地址与凭据。Google 文档中出现的模型和功能,不能自动变成 EveryInfra 的可用清单;二者的认证与计费也要分别核对。
OpenAI TypeScript SDK 文档说明了 Chat Completions 调用方式。SDK 还包含其他 API,但客户端上有一个方法,不等于目标地址实现了对应路由。尤其不要把旧项目里的 responses.create、文件上传、后台任务或会话管理仅靠换 baseURL 就视为迁移完成。
先固定配置,避免凭据和地址交叉
把环境至少拆成开发、验收与生产三组;每组明确服务地址、该服务的 Key、模型、超时和允许的功能。不要在一处改全局环境变量后,让其他仍连旧服务的代码意外带上新凭据。
本文固定 EveryInfra 的基础路径为 https://api.everyinfra.com/api/v1,不是控制台地址,也不是完整的 /chat/completions 路径。SDK 会继续拼接端点。多加一次 /v1 或把完整端点作为基础路径,可能让请求发到错误位置。
Key 只留在服务端环境,不放进浏览器打包变量、仓库、截图或日志。无需为了运行这份文档去开启允许浏览器暴露 Key 的选项。对日志也使用明确白名单,不直接输出整个 SDK 错误对象或请求头。
日志设计可对照 OWASP 的敏感信息排除清单:保留定位所需的状态和关联标识,对令牌、会话值与不必要个人信息做排除。这比依赖每个调用者手工删掉敏感字段更容易保持一致。
先用公开目录核对模型;这一步不需要业务 Key:
set -o pipefail
curl -fsS --max-time 30 'https://api.everyinfra.com/api/v1/models' \
| jq -e '{
default_model,
models: [.data[].id]
} | if (.models | length) > 0
and (.default_model as $m | .models | index($m)) != null
then . else error("invalid model catalog") end'选定目录中与需求相符的 ID 后,显式配置 EVERYINFRA_MODEL_ID。目录读取失败时停止验收,不偷偷回退到一个旧名称;默认值变动也应作为配置变更评估,而不是在不同运行中悄悄换模型。目录只证明声明,不能验证这把 Key 是否获准调用所选模型。
从最小的非流式文本请求开始
下面需要服务端 Node.js 环境与项目锁定的 openai 包。代码是单次业务调用模板;本文对它进行离线检查,不把合成响应称为线上结果。不要为跑例子升级整个项目依赖,先记录当前 SDK 版本与锁文件。
import OpenAI from "openai";
const apiKey = process.env.EVERYINFRA_API_KEY?.trim();
const model = process.env.EVERYINFRA_MODEL_ID?.trim();
if (!apiKey || !model) throw new Error("Missing EveryInfra configuration");
const client = new OpenAI({
apiKey,
baseURL: "https://api.everyinfra.com/api/v1",
maxRetries: 0,
timeout: 120_000,
});
const { data, response: http } = await client.chat.completions.create({
model,
messages: [{ role: "user", content: "用一句话解释幂等。" }],
stream: false,
}).withResponse();
const choice = data.choices?.[0];
const text = choice?.message?.content;
if (typeof text !== "string" || !text.trim()) {
throw new Error("No usable text; inspect this attempt before retrying");
}
console.log({
httpStatus: http.status,
model: data.model,
finishReason: choice.finish_reason,
requestId: http.headers.get("x-request-id"),
textReceived: true,
});
// 将 text 交给业务层;不要默认把完整生成内容写进运行日志。这里的 120 秒是演示用的客户端等待预算,不是服务响应时限或性能承诺。应用还要协调入口代理、任务执行器和用户界面的等待时间。客户端先超时后再收到服务端完成信息,属于需要核对的未知结果,不是自动判定“没有执行”。
首次验收关闭自动重试,是为了观察一次调用。OpenAI SDK 自带对部分连接和服务错误的重试机制;生产恢复重试前,应先明确目标服务对重复请求、扣费和幂等的约定,而不是只沿用 SDK 默认值。SDK 重试与超时说明
非流式通过后,不要直接改成 stream: true
当前 EveryInfra 实现将聊天请求按非流式处理。代码中写出 stream: true,或 Google 的官方兼容层支持流式,都不能证明这个地址会返回符合 SDK 预期的事件流。本文明确保留 stream: false,不把流式 UI 接到未经验收的路径上。
如果业务依赖逐字显示,先把它列为未满足的迁移条件。等待有明确契约后,再分别检查响应类型、事件顺序、结束标记、中断恢复和最终使用量。不要用一次普通 JSON 成功冒充流式验收,也不要在前端把整段文本切片播放后称为服务端流式。
工具调用、结构化输出、多模态输入、推理控制和其他可选参数同样分别确认。客户端类型允许一个字段,只说明它能被序列化;这与目标服务理解并按预期执行该字段是两回事。
响应需要同时回答三个问题
第一,是否取得符合业务要求的内容。非空文字仍可能偏题、违反输出格式,或因长度等原因没有完整结束。保留 finish_reason,检查任务自己的完成标准,不把 HTTP 200 等同于用户任务成功。
第二,哪些数据可以安全交给下游。读取 choices 和 usage 时处理缺失与类型差异;要求 JSON 的业务要另行解析并验证结构。不要因为 TypeScript 编译通过,就相信运行时的第三方响应必定符合静态声明。
第三,这次请求的费用与状态是什么。EveryInfra 的扩展计费信息不属于所有 SDK 通用类型,按服务文档单独校验;不把缺失字段补成零,也不把 token 用量直接当成钱包扣款。模型输出、请求标识和账务关联分开保存,才能解释“内容失败但 HTTP 成功”这类情况。EveryInfra API 与计费文档
SDK 官方文档说明,额外响应属性不会因为未写入静态类型就自动被移除。需要这些字段时应做运行时检查,而不是用 as any 跳过校验;.withResponse() 可同时取得解析数据与 HTTP 信息,但不能凭空创造服务没有返回的请求 ID。SDK 扩展响应与 HTTP 信息
对扩展字段定义运行时 schema 时,可参考 JSON Schema 的 object 指南,分别描述必填、可选和允许的值类型。结构检查通过仍不证明计费含义或模型结论正确,业务语义必须另验。
错误先定位,再决定是否重试
未知模型与服务故障不应走同一处理分支。对照 2026-09-04 的已部署源码基线,在本文这种 messages 完整的 REST 请求中,先鉴别 API Key,再检查模型;未知模型在扣费前产生 422 unknown_model。缺少 messages 的请求会先被输入检查拒绝,不能把这一顺序推广到所有错误组合。这里是源码核对,不是本轮真实鉴权请求的观察。
收到 unknown_model 后先刷新目录、修正配置;不要对同一个错误名称无限重试,也不要自动接受相近候选模型,因为换模型可能改变行为。401 检查目标地址与 Key,权限不足交给权限负责人,额度不足交给账户负责人;应用不应自行换另一把 Key 绕过限制。
网络异常另分两种:明确没有完成请求的失败,以及已发送但最终结果无法确认的超时或连接断开。客户端通常不能单凭异常类确定是否扣款。为每次尝试保存业务操作编号、时间、实际模型与可得请求 ID,先核对这次尝试,再决定是否发起新请求。
离线检查能验证什么,不能验证什么
离线测试可替换 SDK 的 fetch,检查最终 URL、POST 方法、认证头形状和请求体,再返回明确标注的合成响应。它适合发现重复路径、参数拼错、解析假设与错误分支的问题,不需要发送真实 prompt 或消耗账户额度。
本文原有固定 openai 包版本 7.10.0 的隔离测试覆盖过文本、用量和模拟 422;本次仍把版本与测试范围明确记录,而不把它称为最新版或服务兼容认证。新最小模板还要检查缺配置不会发请求、一次调用不自动重试、空白内容被保留为待核对结果。
2026-09-02 归档里另有 Python SDK 的非流式生产样本。它可以支持当时那次 Python 请求,不证明今天这份 TypeScript 模板已经完成真实接入。本轮没有新增真实 TS 业务调用,认证、当日文本质量和账务结果仍需获准样本验证。
按任务样本恢复业务参数
最小请求通过后,每次只恢复一类功能:先是实际提示词与上下文,再是输出约束,最后才是应用确实需要且服务已声明支持的选项。保存一个最小可重现输入,出错时可以对照,而不是一次搬入旧项目的所有参数。
建立任务样本时包含常见成功输入、空输入、长上下文、容易歧义的问题和业务要求的格式。评估是否完成任务、格式是否可解析、关键事实是否有依据;不要以逐字一致作为生成式模型的唯一成功标准。任何质量百分比都应写清样本与人工判据,不能从几次随手测试推出普遍收益。
如果真实请求成功而业务输出变差,先定位是提示词、模型、输入截断还是解析规则;不要把所有差异归因于 SDK。反过来,HTTP 都未成功时,先排查路径、认证和服务状态,调提示词通常解决不了连接问题。
让迁移可以回退,但不要自动重复业务动作
业务调用层可以保留服务地址、模型和版本的明确配置,记录这次使用哪一组;切换前保存上一个已验收配置。回退的目的是恢复后续请求的可控状态,不是将结果未知的每次请求自动向另一服务重发。
正式扩大使用前,确认一条获准的真实文本路径、一组业务样本、错误处理和账务关联都能复核。需要流式或工具功能而尚无验收证据时,保留在未迁移清单中。这样,“完成迁移”才对应具体任务与功能,而不是仅仅改了三行客户端配置。