遇到問題
查詢錯誤
#貼上錯誤訊息
讀錯誤只按一個順序:先看 HTTP 狀態碼,再看 error.type,最後看 error.code。狀態碼永遠有意義;type 告訴你是誰拒絕了請求;只有閘道自己拋出的錯誤,code 才是穩定的。
#如何分支處理
- 1
先按狀態碼分支
402 是去儲值,403 是金鑰權限,429 是等待,5xx 是重試。這一層不會騙你,也不會變。
- 2
再看 error.type
xiaoxiang_error 是我們的。其他值——openai_error、claude_error、gemini_error——都是廠商自己的回應主體,原樣轉發。
- 3
最後才讀 error.code
只對 xiaoxiang_error 按 code 分支。上游的 code 屬於廠商,隨時可能變。
並不是每個錯誤都有 JSON 回應主體
錯誤結構
會到達客戶端的一共五種結構。五種都要處理——後三種是接入時最常被漏掉的。
閘道自身錯誤
請求被閘道自身拒絕——認證、額度、路由或請求結構問題。
{
"error": {
"message": "insufficient account balance, please top up to continue (request id: 20260910...)",
"type": "xiaoxiang_error",
"code": "insufficient_user_quota"
}
}上游錯誤,原樣轉發
廠商回傳了自己的錯誤。回應主體原樣轉發,因此 type 和 code 屬於廠商,不是我們的。
{
"error": {
"message": "This model's maximum context length is 128000 tokens.",
"type": "invalid_request_error",
"code": "context_length_exceeded"
}
}前置守衛拒絕
冪等等少數前置守衛在進入中繼之前就已回應,此時還沒有中繼錯誤結構:扁平結構、大寫 code。
{
"code": "IDEMPOTENCY_IN_PROGRESS",
"error": "a request with this Idempotency-Key is still processing"
}非同步任務拒絕
所有任務端點——影片、圖片任務、音樂、3D——都用自己的扁平結構報告失敗。這裡沒有巢狀的 error 物件,因此按 /v1/chat/completions 寫的解析邏輯在這裡會讀到 null。
{
"code": "task_not_exist",
"message": "task does not exist",
"data": null
}只有狀態碼,回應主體為空
限流拒絕和請求主體過大時只回傳狀態碼,沒有回應主體。
HTTP/1.1 429 Too Many Requests
Retry-After: 20
Content-Length: 0回應標頭
每一次回應都記錄 request id,而不只是失敗時。客服正是憑它追蹤一次呼叫——有它是五分鐘答覆,沒有它就是一整天的猜測。
| 請求標頭 | 何時傳送 | 該怎麼用 |
|---|---|---|
| X-Oneapi-Request-Id | 每一次回應,無論成功還是失敗。 | 請和你自己的追蹤 ID 一起記錄。客服正是憑這個值處理工單;閘道的錯誤訊息裡也會附帶同一個 ID。 |
| Retry-After | 出現 429 時,以及冪等鍵仍在處理中而回傳 409 時。 | 至少等待這麼多秒。這是閘道唯一給出的退避提示。 |
| X-Idempotent-Replayed | 同一個冪等鍵再次請求、命中已存結果並被重放時。 | 值為 true 表示沒有產生新的執行、也沒有二次扣費——把回應主體當作首次請求的結果即可。 |
| X-Idempotency-Sealed | 原始回應無法安全重放(例如串流回應或回應主體過大)。 | 不要再用這個鍵重試。改為透過任務查詢端點或你自己的記錄來對帳。 |
const response = await fetch(url, init);
const requestId = response.headers.get("X-Oneapi-Request-Id");
if (!response.ok) {
// Bodies are absent on 429 and 413 — never assume JSON here.
const detail = await response.text();
logger.error({ status: response.status, requestId, detail });
throw new GatewayError(response.status, requestId, detail);
}#錯誤碼表
以下是閘道自身拋出的錯誤,type 恆為 "xiaoxiang_error"。其中兩行同為 429 但含義相反——退避之前先讀 code。
| HTTP | 錯誤碼 | 含義 | 處理方式 | 可重試? |
|---|---|---|---|---|
| 401 | 無 | Bearer 權杖缺失、格式錯誤、已刪除或已停用。這一層不寫 code,請依狀態碼判斷。 | 檢查 Authorization 標頭,並到控制台 → 金鑰 確認金鑰狀態。 | 否 |
| 401 | signature_required | 這個 key 開啟了請求簽名,但請求缺少 X-Timestamp、X-Nonce 或 X-Signature。 | 為這個 key 的每個請求簽名(認證與金鑰 → 請求簽名),或在 key 上關閉簽名。 | 否 |
| 401 | signature_expired | X-Timestamp 不是 10 位秒級時間戳記,或與伺服器時間相差超過 5 分鐘。回應會帶 X-Server-Timestamp。 | 按 X-Server-Timestamp 的差值校準客戶端時間,用新的 nonce 重新簽名。 | 是 |
| 401 | signature_invalid | X-Signature 與這個 key 的簽名金鑰對不上,或 X-Nonce 格式不對。 | 用控制台 → API 金鑰 裡目前的簽名金鑰,對時間戳記、nonce、方法、路徑(不含查詢參數)以換行連接後的內容簽名。 | 否 |
| 401 | signature_replayed | 這個 X-Nonce 在 10 分鐘內已經被這個 key 用過。 | 每個請求(包括重試)都產生新的隨機 nonce。 | 是 |
| 403 | access_denied | 呼叫方 IP 不在該金鑰的 IP 白名單內。 | 在控制台 → 金鑰中加入出口 IP 位址;若沒有固定出口 IP,就取消該金鑰的白名單。 | 否 |
| 403 | 無 | 帳號已被停權,或該金鑰無權使用這個模型 / 通道。 | 先檢查金鑰的模型白名單;如果沒有限制,請提交工單——問題出在帳號本身。 | 否 |
| 400 | unsupported_endpoint | 模型存在,但不提供這個路徑。錯誤訊息裡會列出它支援的端點。 | 改呼叫列出的端點——圖片任務型模型不會回應 /v1/chat/completions。 | 否 |
| 400 | read_request_body_failed | 請求主體讀取失敗——上傳被截斷、Content-Type 不符,或串流提前結束。 | 傳送完整的請求主體並帶上相符的 Content-Type。原樣重送只會以同樣方式失敗。 | 否 |
| 402 | insufficient_user_quota | 錢包餘額不足以覆蓋本次呼叫的預扣額度。 | 在控制台 → 錢包儲值後重試。 | 否 |
| 429 | insufficient_user_quota | 帳號已達每日消費上限。狀態碼和限流相同,含義卻相反——退避重試不會讓它恢復。 | 在帳號設定裡調高或取消上限,或次日再繼續。請按 code 分支處理,不要只看 429。 | 否 |
| 429 | 無 | 金鑰層級的 RPM/RPD 限流或並行保護。回應主體為空,等待時長在 Retry-After 裡。 | 先按 Retry-After 秒數等待,再以指數退避加隨機抖動重試。 | 是 |
| 413 | read_request_body_failed | 請求主體超過允許大小。傳輸層限制會直接中斷,且不回傳回應主體。 | 縮小請求主體——透過 /v1/media/upload-policy 上傳檔案並傳 URL,不要用 Base64。 | 否 |
| 422 | invalid_request | 沒有通道支援該模型的這種媒體意圖——例如在只支援文生影片的路由上發送圖生影片請求。 | 檢視模型卡確認支援的意圖,然後更換模型或意圖。 | 否 |
| 503 | model_not_found | 模型名稱不存在,或目前沒有健康的通道可以提供服務。注意狀態碼:模型名稱寫錯回傳的是 503,不是 404。 | 用 /v1/models 核對名稱後重試——故障轉移通常在下一次嘗試就能找到健康通道。 | 是 |
| 500 | sensitive_words_detected | 提示詞在離開閘道之前就被內容過濾攔截了。 | 修改提示詞。同樣的文字重試永遠失敗。 | 否 |
| 500 | count_token_failed | 閘道無法估算計費所需的 token 數,通常是 messages 陣列結構有問題。 | 先驗證 messages 陣列;結構沒問題的話,重試並帶上 request id 提交工單。 | 是 |
| 500 | invalid_request | 請求無法被解析成該端點接受的結構。注意狀態碼——閘道讀不懂的請求主體會報 500,而不是 400。 | 對照端點文件驗證 JSON 後再重試。 | 否 |
| 502 | do_request_failed | 無法連線到廠商,或廠商回傳了閘道無法透過故障轉移恢復的錯誤。 | 退避後重試——下一次嘗試通常會落到另一個通道。 | 是 |
| 504 | do_request_failed | 廠商未在閘道的逾時時間內回應。 | 以退避方式重試;長提示詞和忙碌的廠商更容易逾時。 | 是 |
| 499 | do_request_failed | 你的客戶端在結果回傳前就斷開了連線——通常是你自己的 HTTP 逾時在生成中途觸發。 | 調大客戶端讀取逾時。長時間生成需要幾分鐘,多數 HTTP 函式庫的預設值遠遠不夠。 | 否 |
冪等守衛錯誤
這些來自中繼之前的重放守衛,因此用的是扁平的 {code, error} 結構、code 為大寫,而不是 OpenAI 那套錯誤結構。
| HTTP | 錯誤碼 | 含義 | 處理方式 | 可重試? |
|---|---|---|---|---|
| 400 | IDEMPOTENCY_STREAM_UNSUPPORTED | 在串流請求上帶了 Idempotency-Key。 | 串流呼叫請去掉該請求標頭;需要重放保護時改用 stream:false。 | 否 |
| 400 | IDEMPOTENCY_BODY_UNREADABLE | 請求主體無法計算冪等帳本所需的雜湊。 | 請傳送常規的 JSON 或 multipart 請求主體——無法緩衝的分塊請求主體會被拒絕。 | 否 |
| 409 | IDEMPOTENCY_PAYLOAD_CONFLICT | 這個鍵已經用於另一個請求主體、路徑或查詢參數。 | 換一個新鍵。一個鍵標識的是一個確定的請求,而不是一輪重試。 | 否 |
| 409 | IDEMPOTENCY_IN_PROGRESS | 使用該鍵的首次請求仍在執行。 | 按 Retry-After(2 秒)等待後用同一個鍵重試——你會拿到首次請求的回應。 | 是 |
| 409 | IDEMPOTENCY_RESULT_UNREPLAYABLE | 首次請求已經完成,但回應被封存、無法重放。 | 不要盲目重新提交。先透過任務端點或自己的記錄對帳。 | 否 |
| 503 | IDEMPOTENCY_UNAVAILABLE | 冪等帳本不可用,守衛選擇失敗關閉,以免造成重複扣費。 | 退避後重試——這次請求並未被執行。 | 是 |
非同步任務錯誤
影片、圖片任務、音樂和 3D 端點在提交、輪詢、取消時都回傳自己的扁平結構 {code, message, data}。有一個需要預先處理的特殊之處:輪詢未知任務 ID 是 400,取消未知任務 ID 是 404。
| HTTP | 錯誤碼 | 含義 | 處理方式 | 可重試? |
|---|---|---|---|---|
| 400 | task_not_exist | 輪詢或 remix 了一個不存在、或屬於其他帳號的任務 ID。注意狀態碼——輪詢未知任務是 400,取消未知任務卻是 404。 | 提交時就把 task_id 寫入資料庫,並嚴格用這個值去輪詢。 | 否 |
| 400 | invalid_request | 缺少必填欄位——remix 缺 prompt,查詢缺 task_id。 | 先修正請求主體或路徑再重試。 | 否 |
| 400 | task_channel_disable | 衍生任務(remix、續寫)會被鎖定到原任務的通道,而該通道現在已被停用。 | 改為提交一個全新任務,不要從舊任務派生。 | 否 |
| 400 | invalid_source_task | 來源任務類型不符——例如把影片任務 ID 傳給了 3D 續作端點。 | 確認來源任務 ID 來自同一類能力。 | 否 |
| 404 | task_not_exist | 要取消的任務 ID 不存在,或不屬於你。 | 沒有可取消的任務——當作已經不存在處理即可。 | 否 |
| 409 | task_not_cancellable | 任務已進入終態,沒有可取消的內容。 | 再輪詢一次,直接使用你已經付費的結果。 | 否 |
| 501 | task_cancel_not_supported | 該任務背後的供應商沒有提供取消 API。大多數供應商都沒有——能取消是例外,不是常態。 | 讓任務跑完。介面上要把「取消」設計成盡力而為,而不是一種保證。 | 否 |
| 502 | cancel_task_failed | 取消過程中無法連線到供應商,任務狀態未發生變化。 | 重試一次,仍不行就讓它自然跑完。 | 是 |
還是沒解決?
把請求 ID 或錯誤訊息傳給我們,我們來查。