model not found / model_not_found — 同一句报错,三个互不相干的原因
- 先调
/v1/models把实际支持的列表拉出来,别靠猜拼写——三种原因回同一句话,只有一种是拼写。 - id 在列表里却仍报错 = 账号没有该模型的权限(语义上是 403,很多端点故意回 404 防目录被枚举)。
- 短别名能通、写全名反而报错,是别名映射端点的正常行为,不是 bug——但短别名不是稳定标识,会让结果不可复现。
这个报错不能证明模型名写错了。它同样常见的意思是:你的账号没有那个模型的权限,或者端点会改写模型名,而你的名字不在它的映射表里。换各种拼法只解得掉三种里的一种。一个请求就能问出你碰到的是哪一种:去问端点它实际提供哪些模型。
你看到的报错
API Error: 404 {"type":"error","error":{"type":"not_found_error","message":"model: <模型 id>"}}
{"error":{"message":"The model `<模型 id>` does not exist or you do not have access to it.","type":"invalid_request_error","param":null,"code":"model_not_found"}}
404 model not found
{"type":"error","error":{"type":"not_found_error","message":"Not Found"}}
第二种是诚实的那一种——它在同一句话里把两种可能都说了,却拒绝说是哪一种。另外两种当成同一件事、只是说得更含糊。「or you do not have access to it」那半句不是废话:它指向的是403 Forbidden 的地盘,只是穿着 404 的衣服。
第四种是冒牌货。它里面没有模型名,这就是破绽:一个不含模型名的 not_found_error,通常是路由不存在,不是模型不存在。见第 4 步。
三个原因,同一句报错
| 实际上是怎么回事 | 它的表现 | 谁能修 | |
|---|---|---|---|
| A — 端点不提供的名字 | ID 格式没问题,只是不在这个端点的清单里 | 每次都当场失败。同一个端点上的其他模型正常 | 你 |
| B — 没有权限 | 模型存在也有提供;是这个账号或这把 Key 不被允许访问 | 只有那个模型或那个等级会失败。常常是在套餐、地区或账号有变动之后开始,而那变动不是你做的 | 账号所有者或端点运营方 |
| C — 别名映射 | 端点把名字映射到它自己的模型,而你那个完整 ID 不是这张映射表里的 key | 短名可以,完整名字回 404 —— 同一个端点,同一分钟内 | 你(用清单上有的名字) |
被这句话藏起来的是原因 B。从语义上它是一个授权结果——正确的回应应该是带 permission_error 的 403,那是另一页的事:403 Forbidden。很多端点刻意改回 404,这样别人就没办法靠试探请求把有哪些模型全列出来。副作用是:你为一个跟拼写无关的问题,花一小时在拼写上。
为什么短名可以、完整名字不行
兼容端点没有义务提供和上游供应商一样的模型清单,多数也不提供。它们公开的是一张别名表:短的、不带日期的等级名——haiku、sonnet、opus——每一个都路由到这个端点自己选定给那个等级的模型。有些端点还会在请求完全没指定模型时,替换成一个默认模型。
由此会有两个结果,事先不知道的话,两个看起来都像 bug:
- 你从供应商文档里复制出来、带日期的完整 ID,可能根本就不是这张表里的 key。它是一个完全合法的模型 ID,只是这个端点没有它的条目。完整名字回 404、短名回 200,是做别名映射的端点设计上就该有的行为——不是故障,也不是换个拼法就能绕过去的事。
- 短别名不是稳定的标识符。
sonnet解析到哪个模型,端点那边随时可以改,而你的配置完全不用动。今天很方便;三个月后你回头看一个结果,却说不出它是哪个模型产生的,那就是不可复现。
同样的道理,这一页不复述任何人的别名映射表,你在别处看到的写死的映射表也不值得信。一个等级名解析到哪个模型、以及请求完全不指定模型时到底会不会被替换,都是「你去问它的那一天」这个端点的性质。所以下一节的第一步是去读清单,不是去猜名字。
按这个顺序排查
1 · 问端点它实际提供哪些模型#
这一步能把三个原因分开。先做这一步。
# 权威清单——它打印出什么,你就只能点什么
curl -s https://api.9coding.com/v1/models \
-H "Authorization: Bearer $ANTHROPIC_AUTH_TOKEN" \
-H "anthropic-version: 2023-06-01" | jq -r '.data[].id'
如果端点是靠 x-api-key 认证:
curl -s https://api.9coding.com/v1/models \
-H "x-api-key: sk-9c-xxxxxxxx" \
-H "anthropic-version: 2023-06-01"
上面那个 jq 过滤器假定的是常见的返回结构。如果它什么都没打印出来,就把管道去掉直接读原始 JSON——你要的是那些标识符,它落在哪个字段名下都行。
在 Claude Code 里,/model 选择器列的是同一份清单——两个来源都算权威,而且都比凭记忆手敲 ID 强。复制 id,不要手敲。
这样读输出:
- 你那个 ID 一字不差地在清单里 → 这不是名字问题。跳到第 6 步(原因 B)。
- 清单里只有短名 → 这是别名映射。用清单上有的名字(原因 C)。
- 你的 ID 不在,但清单里有几乎一样、带日期的 ID → 名字问题(原因 A)。从清单里复制一个,不要手敲。
/v1/models自己就回 404 → 这个端点不公开模型清单。继续往第 2 步走,但要知道这件事的代价——见什么时候这不是你的问题。
2 · 一个字符一个字符比对字符串#
眼睛做不好这件事,shell 可以:
printf '%s' "$ANTHROPIC_MODEL" | od -c | tail -3
实际会出错的地方,大致按出现频率排:端点不要的供应商前缀(anthropic/、openai/),或是它要而你漏掉的前缀;地区或部署前缀;日期后缀漏了、多了,或来自另一个快照;ID 用 - 的地方你打成 .;大小写;以及复制粘贴带进来的结尾换行——od -c 会把它显示成最后那个 \n。
3 · 先搞清楚这个名字到底是从哪来的#
你改的可能是一个来源,客户端读的是另一个。
env | grep -i -E 'anthropic|model'
每一条都要看,不要只看你记得设过的那条——这一族变量的确切名字(包括那个把后台小活钉到更小模型上的变量)在不同版本之间挪过位置,所以要拿你自己正在跑的那个客户端版本去对,别拿一篇博客去对。然后去看客户端自己的配置文件,还有各项目的配置——几个月前钉死的模型是经典案例:那个快照还在提供时它就一直能用,停止提供的那天就开始回 404,而你这边什么都没动。
之后要重启客户端。长时间运行的进程会沿用它启动时的环境。
4 · 确认问题出在模型,不是路由#
curl -sS -o /dev/null -w '%{http_code}\n' https://api.9coding.com/v1/models
curl -sS -o /dev/null -w '%{http_code}\n' https://api.9coding.com/v1/messages
如果主机名解析正常,但 /v1/models 也回 404,那要怀疑的是 base URL,不是模型——最常见的是 base URL 结尾已经带了 /v1,客户端再接上自己的路径就变成 /v1/v1/models。这会回一个里面没有模型名的 Not Found,正好就是上面第四条报错。
echo "$ANTHROPIC_BASE_URL" # 正确值就是 https://api.9coding.com——结尾不要斜杠,也不要带 /v1
5 · 两个最小请求:先用完整 ID,再用别名#
# <模型 id>:从 https://api.9coding.com/v1/models 里复制一个
curl -sS -w '\n%{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"}]}'
把 <模型 id> 换成短的等级名再跑一次,然后对照这两次的结果:
- 完整 ID 回 404、短名回 200 → 原因 C。端点会映射名字;用第 1 步清单里的名字。
- 两个都回 404 → 原因 A 或 B。第 1 步的清单能决定是哪一个。
- curl 两个都回 200,但客户端还是失败 → 客户端发出的模型不是你以为的那个。回到第 3 步。
- 两个都回 401 → 这根本不是模型的问题。请看 401 Unauthorized。
6 · 名字没错、清单里也有,那就是权限问题——问一个问题就好#
换 Key 不会有帮助。一把能通过认证、却碰不到某一个特定模型的 Key,是授权结果被包在 404 的措辞里发出来,而新的 Key 有的是同一份授权。这和 403 Forbidden 那一页分的是同一件事:「你是谁」和「你被允许碰什么」不是一个问题。
与其问运营方三个含糊的问题,不如问一个准确的:
<模型 id> 吗?如果没有,你们有提供的模型里,哪一个是对应的替代?
后半句比前半句重要——在做别名映射的端点上,答案常常是一个你怎么试拼法都猜不到的名字。
什么时候这不是你的问题
- 你的 ID 在
/v1/models里,用它发请求还是回 404。清单和路由对不上。你这台机器上没有东西能让它们一致。 - 昨天还能用,而你什么都没动。要么是某个快照在上游走到了生命周期终点,要么是端点的别名表在你脚下换掉了。这两件事都有日期;也都应该出现在变更日志里。
- 同一个端点上的其他模型都正常。这一个观察就同时排除了凭证、base URL 和网络——你面对的是原因 A、B 或 C,绝不会是连接问题。
- 短名可以,完整名字不行。前面讲过了。在做别名映射的端点上,这是预期行为。
在你再花一小时改模型名之前,先去看运营方的状态页和变更日志——我们的在状态页。一个不公开模型清单的端点,是一个让这三个原因分不出来的端点。你只能为一个可能根本不是拼写的问题去猜拼写。这是端点的性质,不是你配置的问题,在把东西建在它上面之前,这件事值得先知道。
提问题时请带上 request id——9Coding 的报错响应里都有一个,形如 (request id: 2026...)。这个 id 加上时间戳、模型名和完整报错文本,就能把那一次调用查出来。只说「用不了」的反馈没法排查。
404 或 403 — 都读成「你用不到」
这里的 not_found_error 意思是对这个请求而言找不到,它把「不存在」「这里不提供」和「不是你的」折在同一句话里。不要把它读成模型不存在的证明。如果你拿到的是明确带 permission_error 的 403,那就是同一个原因 B,只是说得直白一点——见 403 Forbidden。
相关报错
- 401 Unauthorized — 凭证那一层,以及为什么换 Key 很少有用
- 403 Forbidden — 通过认证但没有权限
- Context length exceeded — 同一句报错,三种不同的上限
- 全部报错速查 — 排查手册总表