已经有 OpenAI SDK 代码的人,把 api_key 和 base_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.completions、embeddings、models.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 的代码,这类代码要按接口文档重写一遍。
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,业务层还要再包一层兜底。
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,顺便把参数从字典风格换成关键字风格。
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);
Python 用蛇形命名的 base_url,Node.js 用驼峰命名的 baseURL。写成 baseUrl 不会报错,会被当成未知字段静默忽略,请求于是打到默认地址上去,返回 401。这类问题排查起来最费时间,因为错误信息完全没有指向配置文件。
老项目用 require 引入时要注意,新版 SDK 优先以 ESM 发布,require("openai") 在低版本 Node 上可能直接报错。Node 20 以上没有问题,Node 18 上稳妥的做法是改用动态导入,或者在 package.json 里显式声明模块类型。
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)
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);
}
开启 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 生效,超时不重试
)
| 状态码 | 报错关键字 | 真实原因 | 处理方式 |
|---|---|---|---|
| 401 | invalid api key | 令牌写错、被删除、复制时带了空格 | 重新复制令牌,检查首尾空白 |
| 404 | model not found | 模型名不在可用列表里 | 对照模型列表逐字核对 |
| 400 | invalid request | 传了不被支持的参数 | 去掉多余参数后重试 |
| 429 | rate limit | 触发并发或频率上限 | 指数退避,降低同时请求数 |
| 500 / 502 | upstream 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-pro | 0.031 元/次 | 长文档理解、图片识别、日常问答 |
deepseek-v3.2-thinking | 0.049 元/次 | 数学推理、代码生成、结构化输出 |
claude-sonnet-4-5-thinking | 0.09 元/次 | 长文写作、复杂多步流程 |
gpt-5.5 | 0.2 元/次 | 难题推理、高要求代码任务 |
价格按调用次数计算,不按 token 计算。这对输出长的任务更有利:同样一次调用,让模型写三千字和写一百字,花费是一样的。
claude-sonnet-4-5-thinking 里的连字符和 4-5 都是名字的组成部分,写成 claude-sonnet-4.5、claude_sonnet_4_5 一律找不到。稳妥做法是把模型名集中写成常量放在配置文件里,不要在业务代码里到处散落硬编码字符串,将来换模型只改一个地方。
按次计费下,省钱的着力点不在压缩输出长度,而在减少无效调用。缓存相同提问的结果、把多次小请求合并成一次批量请求、给明显答不上来的输入做前置过滤,这三招比调参数管用。
| 现象 | 大概率原因 | 怎么确认 | 怎么修 |
|---|---|---|---|
| 401 invalid api key | 令牌带了换行,或用了别家的 Key | 打印令牌长度和首尾字符 | 重新复制,strip() 后使用 |
| 404 model not found | 模型名写错,或用成别家的别名 | 打印实际传入的 model 值 | 对照模型列表逐字改 |
| 请求打到默认地址 | 地址参数写错位置,如 Node 写成 baseUrl | 打印客户端实例上的地址属性 | 改成 base_url 或 baseURL |
| 路径重复 | 地址写成 https://xyuapi.top/v1/v1 | 打印实际请求的完整 URL | 保留一个 /v1 |
| SSL 证书校验失败 | 本机根证书缺失,或抓包工具装了自签证书 | curl -v https://xyuapi.top/v1/models | 更新系统根证书,关掉抓包工具 |
| 流式输出中断 | 长时间无数据被中间设备断开 | 看断开时间是否固定 | 缩短单次输出,断流后续写 |
少写 /v1 时 SDK 会把请求发到根路径,通常返回 404 或者一段 HTML 错误页。SDK 解析不出 JSON,抛出来的是难以理解的解析错误,而不是清晰的状态码。看到 JSON 解析报错时,先回头检查这一项。
触发 429 时正确的方向是降低并发,而不是换令牌。用信号量或者队列把同时在飞的请求压到三到五个以内,配合指数退避,绝大多数 429 都会消失。把并发堆高只会让失败率更高,吞吐反而下降。
断流和接口报错不同,SDK 抛不出明确异常。建议自己记录已收到的文本长度,一旦连接在 finish_reason 到达之前结束,就带着已生成内容重新请求一次,让模型续写。重试上限设两次,避免长时间卡在同一个请求上。
最直接的办法是拉一次模型列表,或者写一个最小请求打过去,看返回里有没有报模型不存在。集群里的模型是陆续上线的,名字也可能随版本调整,把这项校验做成程序启动时的一次探活更稳,出问题能早点发现,而不是等到用户提问才暴雷。
跑一条明确要求输出两千字以上的提示词,看返回内容是否完整。被截断的典型表现是句子在中途断掉,finish_reason 的值是长度上限而不是正常结束。这类问题用「你好」这类短问答测不出来,只有在长输出场景下才会暴露,而线上业务恰恰多是长输出。
本机跑得通的流式代码,放到服务器或者容器里表现可能完全不同,中间的反向代理、负载均衡都会影响。至少要在真正部署的那套环境里完整跑一次,确认文字能连续吐完,而不是被攒成一整段在最后一起出现。
api_key 已从源码移到环境变量或配置文件,没有硬编码。base_url 结尾是 /v1,且整条地址里只出现一次。model 字段的值能在平台模型列表里逐字找到。timeout 不小于 120 秒,长文任务给到 300 秒。max_retries 设为 2 到 3,认证类异常直接抛出不做重试。choices 做了判空处理。https://xyuai.cc/v1,主域名异常时临时切换。迁移本身没有难度,工作量集中在两行配置和一次模型名核对上。真正容易出问题的是那些不报错却又不生效的细节:Node.js 里 baseURL 的大小写、地址末尾多写的 /v1、超时设得过短导致的偶发失败。按上面的清单过一遍,一个中等规模的项目半小时内可以迁移完成,之后继续用原有的一套调用代码即可。