403 Forbidden — Key 是好的,而这正是问题所在
- 先别换 Key——403 表示身份已经通过了,换新 Key 带的是同一个账号的同一套权限,必然无效。
- 凭证不动,只换模型:换一个最基础的模型再发一次。基础模型 200、目标模型 403 = 权限问题,两次请求三十秒定位。
- 每个模型都 403 且和地区相关 = 地区限制;时有时无、只在某些请求上 = 端点侧策略拦截。
403 表示服务器读了你的凭证、接受了它、算出了你是谁,然后还是拒绝了这个请求。身份那一关早就过了。所以所有冲着身份去的处置——重新生成 Key、重贴一次、换一种头部写法、重装客户端——保证不会有任何变化。403 问的不是「你是谁」,它回答的是「你不可以做这件事」,而这个答案不在你的 Key 里面。
看到 403 就去换 Key,是这一类错误里最常见、也最浪费时间的一步。它花掉一小时,而且不可能成功:同一个账号重新签发的 Key,带的是一模一样的权限。换 Key 不是修,是把同一个实验再跑一遍。
你看到的报错
API Error: 403 {"type":"error","error":{"type":"permission_error","message":"You do not have permission to use this model"}}
403 Forbidden
{"error":{"message":"You do not have permission to access this resource","type":"permission_error","code":"403"}}
<html><head><title>403 Forbidden</title></head>
第四种比它看起来更重要。一个没有 JSON body 的 HTML 403,说明 API 根本没看到你的请求——是它前面的某一层在边缘就把你拦掉了。而带 "type":"permission_error" 的 JSON body,说明 API 确实看到了、看懂了,然后说不行。这是两个不同的问题,负责的人也不同。
401 和 403 — 这个分辨决定了后面所有动作
这两个常被当成同一个问题处理,而正确的做法几乎是相反的。
| 401 Unauthorized | 403 Forbidden | |
|---|---|---|
| 服务器做的判断 | 它不知道你是谁 | 它清楚知道你是谁 |
| 你的凭证 | 被拒、缺失,或格式不对 | 被接受了 |
| 换一把新 Key 有用吗 | 有用,前提是旧的那把真的坏了 | 没用 — 同一个账号,同一份权限 |
| 什么因素会改变结果 | 凭证 | 你要的那个模型、地区、功能,或账号状态 |
| 该修的地方在 | 你的配置 | 账号,或运营方的策略 |
| 该看哪一页 | 401 Unauthorized | 本页 |
不确定手上是哪一种,就看响应 body,不看状态行——见下面第 1 步。端点自己也不总是前后一致。
403 的三种来源
它们在响应里长得几乎一样,行为却完全不同。「指纹」那一列,就是你不用问任何人也能分辨的方法。
| 从哪来 | 消息的样子 | 指纹 | 谁能修 | |
|---|---|---|---|---|
| ① 权限没开 | 账号没有那个模型或那个功能 | 会点名某个具体模型或功能 | 跟模型绑定——同一份凭证换一个更基础的模型就成功 | 账号持有人,或运营方 |
| ② 地区或合规限制 | 请求从哪里发出 | 泛泛而谈,有时会提到地区或国家 | 跟位置绑定——通常是每一个模型都失败,形态相同,每次都一样 | 运营方;你的配置碰不到 |
| ③ 端点侧策略拦截 | 并发限制、内容策略、账号状态 | 泛泛而谈;常是没有 JSON body 的 HTML | 跟请求或时间绑定——换一个提示词,或过一阵子再试就会成功 | 你(改请求),或运营方 |
注意这三种指纹的共同点:没有一个跟 Key 有关。三种都是靠改动凭证以外的东西、再看结果变不变来分辨的。
按这个顺序排查
1 · 先确认它真的是 403,而且要读 body 不读状态行#
# <模型 id>:从 https://api.9coding.com/v1/models 里复制一个
curl -sS -o /tmp/403body.txt -w 'status=%{http_code}\n' https://api.9coding.com/v1/messages \
-H "Authorization: Bearer $ANTHROPIC_AUTH_TOKEN" \
-H "content-type: application/json" \
-H "anthropic-version: 2023-06-01" \
-d '{"model":"<模型 id>","max_tokens":16,"messages":[{"role":"user","content":"hi"}]}'
cat /tmp/403body.txt
看 type 这个字段,不要看那个数字:
"type":"permission_error"→ 是真正的 403,继续看本页。"type":"authentication_error"→ 状态码和 body 对不上。以 body 为准。这是一个披着 403 外衣的凭证问题,请看 401 Unauthorized。- 完全没有 JSON body — 是 HTML 或是空的 → 你在 API 那一层之前就被拦掉了,属于 ③ 的边缘侧。跟你的模型、你的 Key 都没有关系。
- 连状态码都没拿到 → 这从来就不是 403,请看连接错误。
2 · 分开 ① 和 ②③ 的关键测试 — 换模型,不换凭证#
整个手法就是这一句:把凭证固定住,去改 403 真正在管的那个东西,让两个结果自己告诉你是哪一种来源。
# <模型 id>:从 https://api.9coding.com/v1/models 里复制一个
for M in "<模型 id>" "<同一端点上一个更基础的模型>"; do
printf '%s -> ' "$M"
curl -sS -o /dev/null -w '%{http_code}\n' https://api.9coding.com/v1/messages \
-H "Authorization: Bearer $ANTHROPIC_AUTH_TOKEN" \
-H "content-type: application/json" \
-H "anthropic-version: 2023-06-01" \
-d "{\"model\":\"$M\",\"max_tokens\":16,\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}"
done
| 基础模型 | 你要的模型 | 你学到了什么 |
|---|---|---|
| 200 | 403 | ① 权限没开。凭证有效、账号正常、网络没问题,你就是没有那个模型。你机器上任何配置都变不出来 |
| 403 | 403 | 跟模型无关 → 是 ② 或 ③,去第 3 步 |
| 401 | 401 | 走错页了,请看 401 Unauthorized |
| 200 | 404 / not_found_error | 有些端点会把「你不能用的模型」报成找不到而不是被禁止,以免透露有哪些模型存在。请看 Model not available。(某个端点用哪一种形态并不统一,按你自己的端点确认。) |
第一行是多数人从来没走到的结果,因为他们在「403,一定是 Key 的问题」那里就停下来去换 Key 了。两个请求、三十秒,你就知道要不要动自己的配置。
3 · 是这个请求的问题,还是这个账号的问题?(③)#
模型不变,把请求砍到最精简。
# <模型 id>:从 https://api.9coding.com/v1/models 里复制一个
curl -sS -o /dev/null -w '%{http_code}\n' https://api.9coding.com/v1/messages \
-H "Authorization: Bearer $ANTHROPIC_AUTH_TOKEN" \
-H "content-type: application/json" \
-H "anthropic-version: 2023-06-01" \
-d '{"model":"<模型 id>","max_tokens":16,"messages":[{"role":"user","content":"hi"}]}'
- 光一句
hi能成功,你真正的请求回 403 → 账号是好的,是请求里面的某个东西踩到了策略:内容、体积、某个工具或功能被拦、某个不支持的参数。用二分法拆请求:先去掉 system prompt,再去掉 tools,再去掉附件——一次只去掉一样,每去掉一样就重跑一次。让 403 变回 200 的那一步就是答案。 - 最精简的请求也回 403 → 内容完全不是重点,去第 4 步。
4 · 是「你在哪里」的问题,不是「你是谁」的问题吗?(②)#
同一份凭证、同一个模型,换一个网络位置再跑一次。
- 别的地方通、这里不通 → ② 地区或合规限制。
- 地区限制的形状很好认:它对每一个模型都一样、每次都能复现,而且对你配置里的任何东西都没有反应。对照一下:① 是挑模型的,③ 是间歇的。
- 认它要看行为,不要看响应长什么样。位置层面的拒绝,可能由 API 以 JSON 返回,也可能是更前面的边缘层直接回一段 HTML、甚至什么都不回——形态是变的,不能当判据。能当判据的是:在这个网络上,它对所有东西、每一次都失败。
② 在你机器上没有东西可以修;而某个限制能不能绕过,是服务条款的问题,不是技术问题。花时间之前,先看运营方的条款。
5 · 确认这份凭证到底属于哪个账号#
权限挂在账号、组织或项目范围上,不挂在你环境变量里那串字上。两把看起来可以互换的 Key,可能带着完全不同的权限。
env | grep -i anthropic
每一条都要看,不要只看你记得设过的那条,项目或工作区级别的配置文件也要一起看。有一种很常见的版本:同一把 Key 在某个项目能用、在另一个项目回 403,因为第二个项目有自己的配置文件,指向另一个账号。变量不是 Key,是它背后那个账号。
什么时候这不是你的问题
以下情况说明问题在账号或运营方的策略,你这台机器上没有东西要修:
- 今天早些时候还能用、你没改任何配置,而现在每一个模型都回 403,
- 同一个账号下的其他客户端也是一样的错,
- 响应 body 是 HTML 不是 JSON,说明 API 根本没收到请求,
- 或者失败是跟着负载走,不是跟着你要的东西走。
依次去看运营方的状态页、变更日志,以及你的账号或账务状态。一个既不公开状态页也不公开变更日志的端点,是你从外面没办法排错的端点——在你把事情押上去之前,这件事值得先知道。我们的在状态页。
提问题时请带上 request id——9Coding 的报错响应里都有一个,形如 (request id: 2026...)。这个 id 加上时间戳、模型名和完整报错文本,就能把那一次调用查出来。只说「用不了」的反馈没法排查。
不要做的事
下面每一件都是冲着身份去的,而身份那一关早就过了:
- 不要重新生成 Key。同一个账号、同一份权限、同一个 403。
- 不要在
ANTHROPIC_API_KEY和ANTHROPIC_AUTH_TOKEN之间换来换去。那是 401 形状的处置;如果换了有效,你手上的本来就是 401。接 9Coding 时设ANTHROPIC_AUTH_TOKEN,Key 以sk-9c-开头,ANTHROPIC_BASE_URL就是https://api.9coding.com,结尾不要斜杠,不要带/v1。 - 不要写循环重试。403 是一个决定,不是一个队伍。重试对 429 和 529 是对的,在这里没有意义——只有 ③ 是例外,而且那时候「隔一段时间再试一次」是诊断手段,不是修法。
- 不要重装客户端。这个决定是服务器做的,客户端只是把它打印出来。
相关报错
- 401 Unauthorized — 两层认证,一样的报错信息
- Model not available — 当答案回的是 404 而不是 403
- 429 Too Many Requests 与 529 Overloaded — 真的会自己解除的那一类限制
- 连接错误 — 当你连状态码都没拿到
- 全部报错速查 — 排查手册总表