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 — 同一句報錯,三種不同的上限
- 全部報錯速查 — 排查手冊總表