401 Unauthorized 只有一句话的意思:服务端没认出来你是谁。它跟你的网络、跟你的提示词、跟模型都没关系。看到 401,你现在该做的不是重启客户端,而是把请求原样丢给 curl 复现一次。
排查路径按这个顺序走,别跳步:先确认这把 Key 本身是活的 → 再确认 Key 传输过程中没被改脏 → 再确认 Authorization 头真的发出去了 → 最后才查客户端的 base_url 和额度权限。前两步能解决八成的 401。
真正要分的是这三种,它们报错长得很像,处理方式完全不同:
You didn't provide an API key. You need to provide your API key in an Authorization header using Bearer auth (i.e. Authorization: Bearer YOUR_KEY)。Incorrect API key provided: sk-abc***xyz,注意它会把你的 Key 头尾各露几个字符,这是最有用的排错线索——对比一下它打出来的那串,跟你以为的那串是不是一回事。{"error":{"message":"Invalid API key","type":"invalid_request_error","code":"invalid_api_key"}},或者平台侧返回 令牌已禁用、该令牌无权访问模型。先别改代码,先把这三条跑一遍。用的是小鱼API 主入口 https://xyuapi.top/v1:
# 1. 最小验证:只验 Key,不带任何业务参数
curl -s -o /dev/null -w '%{http_code}\n' https://xyuapi.top/v1/models \
-H "Authorization: Bearer $KEY"
# 2. 完整看响应体,401 的原文会告诉你它到底收到了什么
curl -i -s https://xyuapi.top/v1/models -H "Authorization: Bearer $KEY"
# 3. 故意带一个错的 Key,做对照组
curl -i -s https://xyuapi.top/v1/models -H "Authorization: Bearer sk-wrong-key-test"
第 1 条返回 200,说明 Key 是活的,问题在你的客户端或业务请求。返回 401,问题在 Key 或调用方式,继续往下看。第 3 条是对照组:拿错误的 Key 去撞,看错误长什么样,再跟你实际的报错比对,一眼就能判断你的请求到底有没有把 Key 带进去。
这四种状态码经常被当成一回事,其实处理路径完全不一样:
| 状态码 | 原文关键词 | 真实含义 | 你现在该做什么 |
|---|---|---|---|
| 401 | Incorrect API key / invalid_api_key | Key 没带、带错、或已失效 | 跑上面第 2 条命令看原文 |
| 401 | You didn't provide an API key | 请求头压根没这个字段 | 检查代码或客户端有没有真的加上 Authorization |
| 403 | insufficient permissions / 无权访问 | Key 是真的,但这个模型或分组不允许 | 换模型,或换对应分组的令牌 |
| 402 | insufficient quota / 余额不足 | 账户余额不够 | 充值,或换有额度的 Key |
| 429 | Rate limit reached | Key 没问题,请求太密 | 降并发、加退避 |
记住一句:403 是「我认识你,但你不能进这扇门」,401 是「我不认识你」。 这两个搞混,会白查半天。
/v1/models 是个不带业务参数的接口,正好拿来做认证测试。它只要 Key,不要提示词,也不消耗额度。你现在敲这条:
curl -s https://xyuapi.top/v1/models -H "Authorization: Bearer $KEY" | head -20
返回一长串模型 ID,就是 200,Key 没问题。返回 {"error":{"message":"Incorrect API key provided..."}},这个 Key 就是错的了。
服务端在报错里会把 Key 前后各截几个字符。比如它说 Incorrect API key provided: sk-abc***xyz,你就去你的配置里找 sk-abc...xyz。对不上,说明你读的根本不是同一份配置——最常见的是改了一个 .env,程序读的是另一个,或者改了代码,跑的却是上一次构建的产物。
# macOS / Linux
echo -n "$KEY" | wc -c
# Windows PowerShell
$env:KEY.Length
平台上的 Key 长度是固定的。数出来多了 1 到 2 个字符,基本就是复制时带上了换行或空格;少了,就是复制不全,双击选中复制最容易漏掉首尾。这一步看着土,但「肉眼看着一模一样、实际字节不一样」这类问题,只有数长度才能发现。
.env 里写成 API_KEY="sk-xxx",程序把引号也读进去了。Bearer 。然后你在代码里又拼了一次 Authorization: Bearer ,变成 Bearer Bearer sk-xxx。.env 很容易中这一条。cat -A .env | grep API_KEY
正常应该是 API_KEY=sk-xxxxxxxx$。如果你看到 API_KEY=sk-xxxxxxxx^M$(Windows 换行)、API_KEY="sk-xxx"$(引号)、或者开头有个 M-oM-;M-?(BOM),病根就在这儿。
$k = $env:OPENAI_API_KEY
$k.Length
[System.Text.Encoding]::UTF8.GetBytes($k) -join ','
长度对不对、有没有可疑的非 ASCII 字节,一眼就看得出来。比如 226 开头的那串字节,多半就是被塞进去的全角空格;结尾冒出 13 或 10 的,就是换行符。这种字符在编辑器里完全看不见,但发给服务端就是错的,所以必须用命令看,不能靠眼睛。
把 Key 直接硬写到测试脚本里跑一次。能通,说明 Key 本身没问题,问题全在配置读取链路;不通,问题在 Key 或调用方式。这个二分法比逐个猜配置快得多。
把请求原样手打一遍,不过任何客户端:
KEY='sk-你的key'
curl -i -X POST https://xyuapi.top/v1/chat/completions \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"model":"gemini-2.5-pro","messages":[{"role":"user","content":"ping"}]}'
手打能通、程序里不通,问题百分之百在你的请求构造或中间层,不用再怀疑平台。
| 你写成了这样 | 结果 | 修法 |
|---|---|---|
Authorization: sk-xxx | 没有 Bearer 前缀,多数网关直接判 401 | 补上 Bearer ,注意后面有一个空格 |
Authorization: Bearer: sk-xxx | 冒号多了一个 | 冒号只出现在字段名后面 |
Authorization: Bearer sk-xxx | 尾部夹了空格 | 去掉空格,用变量而不是手打 |
用 api-key / x-api-key 头 | 各家客户端习惯不同 | 走 OpenAI 兼容协议时统一用 Authorization: Bearer |
有些客户端界面上有「API Key」和「Bearer Token」两个输入框,或者把鉴权头字段名做成可填的。填错地方的结果就是请求里压根没这个头,服务端回你 You didn't provide an API key。看到这句原文,第一反应就该是「我的头没发出去」,而不是「我的 Key 坏了」。
如果你在代码和平台之间还挂了自建网关、Nginx 反向代理、或者公司网络出口的审计设备,Header 是可能被改写甚至丢掉的。做法很简单:把请求指向平台主入口直连一次,再指向你的中间层一次,两次结果不一样,问题就在中间层。
OpenAI 兼容客户端里那个输入框要填的是到 /v1 为止:
https://xyuapi.top/v1https://xyuapi.top,少了 /v1https://xyuapi.top/v1/chat/completions,多填了路径https://xyuapi.top/v1/,尾部多一个斜杠,部分客户端会拼成 //v1/...备用入口是 https://xyuai.cc/v1,规则一样。两个入口都能填,用哪个都行。
这类客户端一般有「API 提供商」下拉和「API Key」「API 域名」两个框。顺序是:先把提供商切成「OpenAI 兼容」或「自定义」,再填域名,最后填 Key。先填 Key 再切提供商,有些客户端会把 Key 清空,然后你看到 401,以为是 Key 错了。
自部署版本常常是环境变量注进去的。变量名写错一个字母(OPENAI_API_KEY 写成 OPEN_AI_API_KEY),程序读不到就是空值,请求里自然没头。改完记得重启容器,环境变量不会热加载。
API_KEY = sk-xxx 这种等号两边带空格的写法,在很多解析器里值会跟着带上空格。写成 API_KEY=sk-xxx,不加引号,不加空格。
api_key = "YOUR_API_KEY" 这种模板代码,忘了替换就是 401,而且它的报错长得跟真 Key 错一模一样,很容易被骗。搜一下代码里有没有 YOUR_、your-key、xxx 这类残留。
个别情况下非流式能通、流式 401,通常是客户端对 SSE 请求单独走了一套 header 拼装逻辑。遇到这种,先切回非流式确认 Key 没问题,再去查流式那条链路。
另外还有一种被误当成 401 的情况:客户端在正式请求之前会先发一个探测请求去拉模型列表,探测失败时界面上直接弹「认证失败」,但真实原因是那个探测地址写错了。判断办法是打开客户端的日志或开发者模式,看它实际请求的完整 URL 是什么。
有的网关在余额不足时返回 402 加 insufficient quota,有的直接返回 401 或 403。所以看到 401 先别急着换 Key,去后台看一眼余额和令牌状态,这一步 10 秒,能省掉半小时。
后台把令牌停用之后,网关侧的返回通常还是 401。特征是你什么都没改,昨天能跑今天不能跑。这时候去后台看令牌列表,状态栏会写明白。
平台上的令牌一般挂在某个分组上,分组决定它能调哪些模型。用低价分组去调高价模型,可能返回 403,也可能返回一个含糊的 401。换一个「全模型」分组的令牌复现一次,能通就是这个原因。
拿后台里另一个确定在用的令牌,填进同一个客户端的同一个位置,跑一次。换了 Key 就好,说明是这把 Key 的权限或余额问题;换了还不行,说明是配置问题。
import os, requests
def check_key(base_url="https://xyuapi.top/v1", timeout=(10, 30)):
key = os.environ.get("OPENAI_API_KEY", "")
if not key:
return "缺少环境变量 OPENAI_API_KEY"
if key != key.strip():
return "Key 首尾有空白字符,请去掉"
if key.startswith("Bearer "):
return "Key 里已经带了 Bearer 前缀,代码里不要再拼一次"
try:
r = requests.get(f"{base_url}/models",
headers={"Authorization": f"Bearer {key}"},
timeout=timeout)
except requests.exceptions.Timeout:
return "连接超时:先查网络,不是 Key 的问题"
except requests.exceptions.ConnectionError as e:
return f"连不上:{e}"
if r.status_code == 200:
return "Key 正常"
if r.status_code == 401:
return f"401 认证失败:{r.text[:200]}"
return f"意外状态码 {r.status_code}:{r.text[:200]}"
print(check_key())
这段代码的价值不在「能跑通」,而在把缺 Key、Key 脏了、网络不通、认证失败这四种情况报成四句不同的话。以后再出问题,看一行日志就知道该查哪一层,不用重新走一遍今天的流程。放在服务启动的第一行调用它,比出事之后翻半天日志划算得多。
| 你看到的原文 | 直接动作 |
|---|---|
You didn't provide an API key | 请求头没发出去:检查客户端 Key 栏是否填了、代码里 header 是否真的加上 |
Incorrect API key provided: sk-abc***xyz | 对比它打印的字符和你配置里的值,先排除读错配置文件 |
invalid_api_key 或 令牌已禁用 | 去后台看令牌状态和余额 |
/v1/models 返回 200,业务请求 401 | Key 没问题,查业务请求的 header 拼装和中间层 |
| 手打 curl 通,客户端不通 | 查客户端 base_url 是否精确到 /v1 |
| 昨天能跑今天不能跑,代码没改 | 余额、令牌状态、分组权限三处一起看 |
主入口 https://xyuapi.top/v1,备用 https://xyuai.cc/v1。两个都是 OpenAI 兼容协议,直接用 /v1/chat/completions 和 /v1/models。小鱼API 是 AI API 接入平台,Key 在后台自助生成,随时可以新建和停用,建议测试用一个、正式用一个,出问题好定位。
Chatbox、Cherry Studio、NextChat、Cline、RooCode、KiloCode、LobeChat,以及任何支持自定义 OpenAI 地址的客户端,填法都是同一套:地址填到 /v1,Key 填进 API Key 栏,模型名从 /v1/models 里挑。
按次计费为主,一次请求一个固定价,输入长度不影响价格,所以长提示词不会让单价变高。常用的几个:gemini-2.5-pro 0.031 元/次,deepseek-v4-flash-thinking 0.05 元/次,claude-sonnet-4-5-thinking 0.09 元/次,gpt-5.5 0.2 元/次。最低充值 7 元,支付宝微信都能付,不需要海外信用卡。
401 这个错最麻烦的地方是它太笼统,网络没问题、模型没问题,它照样报。所以别猜,跑命令:/v1/models 验 Key,cat -A 验脏字符,手打 curl 验 header。三条命令跑完,答案一定在里面。