OpenAI SDK 迁移:只改 base_url 就能接入小鱼API

迁移只改两处:api_key 与 base_url

已经有 OpenAI SDK 代码的人,把 api_keybase_url 换成小鱼API的两个值就能直接跑,其余参数一行不用动。最小可用版本贴在下面,把 YOUR_KEY 换成 xyuai.cc 后台生成的令牌即可:

from openai import OpenAI

client = OpenAI(
    api_key="YOUR_KEY",
    base_url="https://xyuapi.top/v1",
)

resp = client.chat.completions.create(
    model="gemini-2.5-pro",
    messages=[{"role": "user", "content": "用一句话解释什么是向量数据库"}],
)
print(resp.choices[0].message.content)

地址结尾的 /v1 属于路径的一部分,既不能省,也不能重复写成 /v1/v1。能正常打印出结果,就说明 Key、网络、模型名三项全对。

为什么只改这两处就够

小鱼API 提供的是 OpenAI 兼容接口,请求体结构、响应体字段、错误码含义都沿用同一套约定,SDK 内部拼装 URL 时只是把域名拼在前面。所以 chat.completionsembeddingsmodels.list 这些方法全部照常工作,返回的对象结构也一致,下游解析代码不用跟着改。

不改代码的兜底做法

代码散落在好几个文件、短期不方便统一改时,用环境变量覆盖更省事,SDK 会自动读取,构造函数里连 base_url 都不用写:

$env:OPENAI_API_KEY="YOUR_KEY"
$env:OPENAI_BASE_URL="https://xyuapi.top/v1"   # Windows PowerShell
export OPENAI_API_KEY="YOUR_KEY"                # macOS / Linux
export OPENAI_BASE_URL="https://xyuapi.top/v1"

怎么确认请求真的打到了新地址

别只看代码,让程序自己说更准。Python 里打印 client.base_url,Node 里打印 client.baseURL,两行就能看出地址到底生效没有:参数名写成 baseUrl 的项目,打印出来还是原来的地址,一眼就暴露。再补一次最小请求,返回正常就说明地址、Key、模型名三样都对。这行打印建议留在项目启动日志里,将来换地址时也能立刻确认生效没有。

顺带能搬过来的生态

凡是基于 OpenAI 协议写的上层库,改完地址都能接上,包括 LangChain、LlamaIndex、Vercel AI SDK、Spring AI、AutoGen。真正需要动手改的只有那些自己签请求、自己拼 HTTP 的代码,这类代码要按接口文档重写一遍。

Python SDK:迁移前后逐行对比

迁移前的写法

from openai import OpenAI

client = OpenAI(api_key="sk-original-key")   # 不写 base_url,打到默认地址

resp = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "你好"}],
)

迁移后的写法

from openai import OpenAI

client = OpenAI(
    api_key="YOUR_KEY",
    base_url="https://xyuapi.top/v1",     # 只多了这一行
)

resp = client.chat.completions.create(
    model="claude-sonnet-4-5-thinking",   # 模型名换成列表里的
    messages=[{"role": "user", "content": "你好"}],
)

三处容易被忽略的差异

一是 model 字段,模型名要和平台列表逐个字符对上,写 claude-sonnet-4.5 或者 sonnet-4-5 都会返回模型不存在。二是超时,跨域链路的首字延迟比同机房高,思考型模型尤其明显,timeout 给到 120 秒起步。三是重试,SDK 内置重试只覆盖 429 和 5xx,业务层还要再包一层兜底。

0.x 旧版 SDK 的写法差异

openai==0.28 那一代用的是模块级函数,参数名也不同,项目如果还停在这个版本上,可以这样写:

# 旧版 0.28 的写法
import openai

openai.api_key = "YOUR_KEY"
openai.api_base = "https://xyuapi.top/v1"   # 注意是 api_base,不是 base_url

resp = openai.ChatCompletion.create(
    model="deepseek-v3.2-thinking",
    messages=[{"role": "user", "content": "你好"}],
)

这个版本早已停止维护,建议升到 1.40 以上。改动量其实不大,核心就是把 ChatCompletion.create 换成 client.chat.completions.create,顺便把参数从字典风格换成关键字风格。

Node.js SDK:迁移前后对比

迁移前的写法

import OpenAI from "openai";

const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

const resp = await client.chat.completions.create({
  model: "gpt-4o",
  messages: [{ role: "user", content: "你好" }],
});

迁移后的写法

import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.XYU_API_KEY,
  baseURL: "https://xyuapi.top/v1",   // 注意这里是大写 URL
});

const resp = await client.chat.completions.create({
  model: "gpt-5.5",
  messages: [{ role: "user", content: "你好" }],
});
console.log(resp.choices[0].message.content);

baseURL 的大小写坑

Python 用蛇形命名的 base_url,Node.js 用驼峰命名的 baseURL。写成 baseUrl 不会报错,会被当成未知字段静默忽略,请求于是打到默认地址上去,返回 401。这类问题排查起来最费时间,因为错误信息完全没有指向配置文件。

CommonJS 项目的写法

老项目用 require 引入时要注意,新版 SDK 优先以 ESM 发布,require("openai") 在低版本 Node 上可能直接报错。Node 20 以上没有问题,Node 18 上稳妥的做法是改用动态导入,或者在 package.json 里显式声明模块类型。

流式输出怎么写

Python 的 stream=True

stream = client.chat.completions.create(
    model="deepseek-v3.2-thinking",
    messages=[{"role": "user", "content": "写一段 200 字的项目说明"}],
    stream=True,
    stream_options={"include_usage": True},   # 末尾多带一帧用量
)

for chunk in stream:
    if not chunk.choices:
        continue
    delta = chunk.choices[0].delta
    if delta.content:
        print(delta.content, end="", flush=True)

Node.js 的 for await

const stream = await client.chat.completions.create({
  model: "claude-sonnet-4-5-thinking",
  messages: [{ role: "user", content: "写一段 200 字的项目说明" }],
  stream: true,
});

for await (const chunk of stream) {
  const text = chunk.choices?.[0]?.delta?.content;
  if (text) process.stdout.write(text);
}

流式下怎么拿 usage

开启 stream_options 之后,最后一帧的 choices 是空数组,usage 字段才有值。遍历时必须先判断 chunk.choices 是否为空再取 delta,否则会抛 NoneType 或者 undefined 错误,这是流式代码最常见的崩溃点。

断流了怎么续写

流式请求到一半 TCP 断开时,SDK 不会自动重连。稳妥做法是把已经收到的文本拼起来,重新发一次请求,在 messages 里补一条 assistant 消息带上已生成内容,提示模型接着往下写。

超时、重试与错误处理

超时要分层设置

思考型模型的推理耗时和输出长度强相关,三千字的长回答首字可能要等四十秒。把 timeout 设成 30 秒,表现就是偶发超时、偶发成功,很难定位。建议对话类给 120 秒,长文生成给 300 秒。

自动重试怎么配

client = OpenAI(
    api_key="YOUR_KEY",
    base_url="https://xyuapi.top/v1",
    timeout=120.0,
    max_retries=3,    # 只对 429 与 5xx 生效,超时不重试
)

错误码对照表

状态码报错关键字真实原因处理方式
401invalid api key令牌写错、被删除、复制时带了空格重新复制令牌,检查首尾空白
404model not found模型名不在可用列表里对照模型列表逐字核对
400invalid request传了不被支持的参数去掉多余参数后重试
429rate limit触发并发或频率上限指数退避,降低同时请求数
500 / 502upstream error上游模型侧抖动退避后重试,一般两次内恢复
连接超时connection timed out网络链路或 DNS 问题检查网络,必要时切备用域名

捕获异常的标准写法

import time
import openai

def ask(prompt, model="gemini-2.5-pro", retries=3):
    for i in range(retries):
        try:
            resp = client.chat.completions.create(
                model=model,
                messages=[{"role": "user", "content": prompt}],
            )
            return resp.choices[0].message.content
        except openai.AuthenticationError:
            raise                                  # Key 错了,重试没有意义
        except openai.RateLimitError:
            time.sleep(2 ** i)
        except (openai.APITimeoutError, openai.InternalServerError):
            time.sleep(2 ** i)
    raise RuntimeError("多次重试后仍然失败")

要点在于区分「重试有意义」和「重试没意义」的异常。认证失败、模型名不存在这类属于请求本身有问题,重试一百次也是同样结果,应该直接抛给上层处理,而不是白白耗掉配额。

模型名与按次计费

常用模型与单价

模型名单价适合干什么
gemini-2.5-pro0.031 元/次长文档理解、图片识别、日常问答
deepseek-v3.2-thinking0.049 元/次数学推理、代码生成、结构化输出
claude-sonnet-4-5-thinking0.09 元/次长文写作、复杂多步流程
gpt-5.50.2 元/次难题推理、高要求代码任务

价格按调用次数计算,不按 token 计算。这对输出长的任务更有利:同样一次调用,让模型写三千字和写一百字,花费是一样的。

模型名必须逐字对上

claude-sonnet-4-5-thinking 里的连字符和 4-5 都是名字的组成部分,写成 claude-sonnet-4.5claude_sonnet_4_5 一律找不到。稳妥做法是把模型名集中写成常量放在配置文件里,不要在业务代码里到处散落硬编码字符串,将来换模型只改一个地方。

怎么控制成本

按次计费下,省钱的着力点不在压缩输出长度,而在减少无效调用。缓存相同提问的结果、把多次小请求合并成一次批量请求、给明显答不上来的输入做前置过滤,这三招比调参数管用。

六个高频坑与排查顺序

现象大概率原因怎么确认怎么修
401 invalid api key令牌带了换行,或用了别家的 Key打印令牌长度和首尾字符重新复制,strip() 后使用
404 model not found模型名写错,或用成别家的别名打印实际传入的 model对照模型列表逐字改
请求打到默认地址地址参数写错位置,如 Node 写成 baseUrl打印客户端实例上的地址属性改成 base_urlbaseURL
路径重复地址写成 https://xyuapi.top/v1/v1打印实际请求的完整 URL保留一个 /v1
SSL 证书校验失败本机根证书缺失,或抓包工具装了自签证书curl -v https://xyuapi.top/v1/models更新系统根证书,关掉抓包工具
流式输出中断长时间无数据被中间设备断开看断开时间是否固定缩短单次输出,断流后续写

base_url 少写 /v1 会怎样

少写 /v1 时 SDK 会把请求发到根路径,通常返回 404 或者一段 HTML 错误页。SDK 解析不出 JSON,抛出来的是难以理解的解析错误,而不是清晰的状态码。看到 JSON 解析报错时,先回头检查这一项。

并发限制怎么绕开

触发 429 时正确的方向是降低并发,而不是换令牌。用信号量或者队列把同时在飞的请求压到三到五个以内,配合指数退避,绝大多数 429 都会消失。把并发堆高只会让失败率更高,吞吐反而下降。

流式断流怎么重试

断流和接口报错不同,SDK 抛不出明确异常。建议自己记录已收到的文本长度,一旦连接在 finish_reason 到达之前结束,就带着已生成内容重新请求一次,让模型续写。重试上限设两次,避免长时间卡在同一个请求上。

迁移之后要验证的三件事

验证模型名确实可用

最直接的办法是拉一次模型列表,或者写一个最小请求打过去,看返回里有没有报模型不存在。集群里的模型是陆续上线的,名字也可能随版本调整,把这项校验做成程序启动时的一次探活更稳,出问题能早点发现,而不是等到用户提问才暴雷。

验证长输出没有被截断

跑一条明确要求输出两千字以上的提示词,看返回内容是否完整。被截断的典型表现是句子在中途断掉,finish_reason 的值是长度上限而不是正常结束。这类问题用「你好」这类短问答测不出来,只有在长输出场景下才会暴露,而线上业务恰恰多是长输出。

验证流式在目标环境的真实表现

本机跑得通的流式代码,放到服务器或者容器里表现可能完全不同,中间的反向代理、负载均衡都会影响。至少要在真正部署的那套环境里完整跑一次,确认文字能连续吐完,而不是被攒成一整段在最后一起出现。

迁移完成的自检清单

  1. api_key 已从源码移到环境变量或配置文件,没有硬编码。
  2. base_url 结尾是 /v1,且整条地址里只出现一次。
  3. model 字段的值能在平台模型列表里逐字找到。
  4. timeout 不小于 120 秒,长文任务给到 300 秒。
  5. max_retries 设为 2 到 3,认证类异常直接抛出不做重试。
  6. 流式代码里对空 choices 做了判空处理。
  7. 用真实业务提示词跑一遍,确认输出质量和迁移前一致。
  8. 记下备用域名 https://xyuai.cc/v1,主域名异常时临时切换。

小结

迁移本身没有难度,工作量集中在两行配置和一次模型名核对上。真正容易出问题的是那些不报错却又不生效的细节:Node.js 里 baseURL 的大小写、地址末尾多写的 /v1、超时设得过短导致的偶发失败。按上面的清单过一遍,一个中等规模的项目半小时内可以迁移完成,之后继续用原有的一套调用代码即可。

相关阅读

🚀 想要立即使用?来小鱼API体验全系列AI模型

查看全部产品

支持Gemini / Claude / GPT / Grok / DeepSeek · 国内直连 · 支付宝/微信