遇到問題

查詢錯誤

#貼上錯誤訊息

讀錯誤只按一個順序:先看 HTTP 狀態碼,再看 error.type,最後看 error.code。狀態碼永遠有意義;type 告訴你是誰拒絕了請求;只有閘道自己拋出的錯誤,code 才是穩定的。

#如何分支處理

  1. 1

    先按狀態碼分支

    402 是去儲值,403 是金鑰權限,429 是等待,5xx 是重試。這一層不會騙你,也不會變。

  2. 2

    再看 error.type

    xiaoxiang_error 是我們的。其他值——openai_error、claude_error、gemini_error——都是廠商自己的回應主體,原樣轉發。

  3. 3

    最後才讀 error.code

    只對 xiaoxiang_error 按 code 分支。上游的 code 屬於廠商,隨時可能變。

並不是每個錯誤都有 JSON 回應主體

限流拒絕和請求主體過大時只回傳一個狀態行。如果你的解析邏輯預設 response.json() 一定成功,那麼在錯誤分支上它會拋出例外,而不是處理錯誤。
錯誤結構

會到達客戶端的一共五種結構。五種都要處理——後三種是接入時最常被漏掉的。

閘道自身錯誤

請求被閘道自身拒絕——認證、額度、路由或請求結構問題。

http
{
  "error": {
    "message": "insufficient account balance, please top up to continue (request id: 20260910...)",
    "type": "xiaoxiang_error",
    "code": "insufficient_user_quota"
  }
}

上游錯誤,原樣轉發

廠商回傳了自己的錯誤。回應主體原樣轉發,因此 type 和 code 屬於廠商,不是我們的。

http
{
  "error": {
    "message": "This model's maximum context length is 128000 tokens.",
    "type": "invalid_request_error",
    "code": "context_length_exceeded"
  }
}

前置守衛拒絕

冪等等少數前置守衛在進入中繼之前就已回應,此時還沒有中繼錯誤結構:扁平結構、大寫 code。

http
{
  "code": "IDEMPOTENCY_IN_PROGRESS",
  "error": "a request with this Idempotency-Key is still processing"
}

非同步任務拒絕

所有任務端點——影片、圖片任務、音樂、3D——都用自己的扁平結構報告失敗。這裡沒有巢狀的 error 物件,因此按 /v1/chat/completions 寫的解析邏輯在這裡會讀到 null。

http
{
  "code": "task_not_exist",
  "message": "task does not exist",
  "data": null
}

只有狀態碼,回應主體為空

限流拒絕和請求主體過大時只回傳狀態碼,沒有回應主體。

http
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原始回應無法安全重放(例如串流回應或回應主體過大)。不要再用這個鍵重試。改為透過任務查詢端點或你自己的記錄來對帳。
log-the-request-id.ts
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 標頭,並到控制台 → 金鑰 確認金鑰狀態。否
401signature_required這個 key 開啟了請求簽名,但請求缺少 X-Timestamp、X-Nonce 或 X-Signature。為這個 key 的每個請求簽名(認證與金鑰 → 請求簽名),或在 key 上關閉簽名。否
401signature_expiredX-Timestamp 不是 10 位秒級時間戳記,或與伺服器時間相差超過 5 分鐘。回應會帶 X-Server-Timestamp。按 X-Server-Timestamp 的差值校準客戶端時間,用新的 nonce 重新簽名。是
401signature_invalidX-Signature 與這個 key 的簽名金鑰對不上,或 X-Nonce 格式不對。用控制台 → API 金鑰 裡目前的簽名金鑰,對時間戳記、nonce、方法、路徑(不含查詢參數)以換行連接後的內容簽名。否
401signature_replayed這個 X-Nonce 在 10 分鐘內已經被這個 key 用過。每個請求(包括重試)都產生新的隨機 nonce。是
403access_denied呼叫方 IP 不在該金鑰的 IP 白名單內。在控制台 → 金鑰中加入出口 IP 位址;若沒有固定出口 IP,就取消該金鑰的白名單。否
403無帳號已被停權,或該金鑰無權使用這個模型 / 通道。先檢查金鑰的模型白名單;如果沒有限制,請提交工單——問題出在帳號本身。否
400unsupported_endpoint模型存在,但不提供這個路徑。錯誤訊息裡會列出它支援的端點。改呼叫列出的端點——圖片任務型模型不會回應 /v1/chat/completions。否
400read_request_body_failed請求主體讀取失敗——上傳被截斷、Content-Type 不符,或串流提前結束。傳送完整的請求主體並帶上相符的 Content-Type。原樣重送只會以同樣方式失敗。否
402insufficient_user_quota錢包餘額不足以覆蓋本次呼叫的預扣額度。在控制台 → 錢包儲值後重試。否
429insufficient_user_quota帳號已達每日消費上限。狀態碼和限流相同,含義卻相反——退避重試不會讓它恢復。在帳號設定裡調高或取消上限,或次日再繼續。請按 code 分支處理,不要只看 429。否
429無金鑰層級的 RPM/RPD 限流或並行保護。回應主體為空,等待時長在 Retry-After 裡。先按 Retry-After 秒數等待,再以指數退避加隨機抖動重試。是
413read_request_body_failed請求主體超過允許大小。傳輸層限制會直接中斷,且不回傳回應主體。縮小請求主體——透過 /v1/media/upload-policy 上傳檔案並傳 URL,不要用 Base64。否
422invalid_request沒有通道支援該模型的這種媒體意圖——例如在只支援文生影片的路由上發送圖生影片請求。檢視模型卡確認支援的意圖,然後更換模型或意圖。否
503model_not_found模型名稱不存在,或目前沒有健康的通道可以提供服務。注意狀態碼:模型名稱寫錯回傳的是 503,不是 404。用 /v1/models 核對名稱後重試——故障轉移通常在下一次嘗試就能找到健康通道。是
500sensitive_words_detected提示詞在離開閘道之前就被內容過濾攔截了。修改提示詞。同樣的文字重試永遠失敗。否
500count_token_failed閘道無法估算計費所需的 token 數,通常是 messages 陣列結構有問題。先驗證 messages 陣列;結構沒問題的話,重試並帶上 request id 提交工單。是
500invalid_request請求無法被解析成該端點接受的結構。注意狀態碼——閘道讀不懂的請求主體會報 500,而不是 400。對照端點文件驗證 JSON 後再重試。否
502do_request_failed無法連線到廠商,或廠商回傳了閘道無法透過故障轉移恢復的錯誤。退避後重試——下一次嘗試通常會落到另一個通道。是
504do_request_failed廠商未在閘道的逾時時間內回應。以退避方式重試;長提示詞和忙碌的廠商更容易逾時。是
499do_request_failed你的客戶端在結果回傳前就斷開了連線——通常是你自己的 HTTP 逾時在生成中途觸發。調大客戶端讀取逾時。長時間生成需要幾分鐘,多數 HTTP 函式庫的預設值遠遠不夠。否
冪等守衛錯誤

這些來自中繼之前的重放守衛,因此用的是扁平的 {code, error} 結構、code 為大寫,而不是 OpenAI 那套錯誤結構。

HTTP錯誤碼含義處理方式可重試?
400IDEMPOTENCY_STREAM_UNSUPPORTED在串流請求上帶了 Idempotency-Key。串流呼叫請去掉該請求標頭;需要重放保護時改用 stream:false。否
400IDEMPOTENCY_BODY_UNREADABLE請求主體無法計算冪等帳本所需的雜湊。請傳送常規的 JSON 或 multipart 請求主體——無法緩衝的分塊請求主體會被拒絕。否
409IDEMPOTENCY_PAYLOAD_CONFLICT這個鍵已經用於另一個請求主體、路徑或查詢參數。換一個新鍵。一個鍵標識的是一個確定的請求,而不是一輪重試。否
409IDEMPOTENCY_IN_PROGRESS使用該鍵的首次請求仍在執行。按 Retry-After(2 秒)等待後用同一個鍵重試——你會拿到首次請求的回應。是
409IDEMPOTENCY_RESULT_UNREPLAYABLE首次請求已經完成,但回應被封存、無法重放。不要盲目重新提交。先透過任務端點或自己的記錄對帳。否
503IDEMPOTENCY_UNAVAILABLE冪等帳本不可用,守衛選擇失敗關閉,以免造成重複扣費。退避後重試——這次請求並未被執行。是

冪等鍵的工作方式 →

非同步任務錯誤

影片、圖片任務、音樂和 3D 端點在提交、輪詢、取消時都回傳自己的扁平結構 {code, message, data}。有一個需要預先處理的特殊之處:輪詢未知任務 ID 是 400,取消未知任務 ID 是 404。

HTTP錯誤碼含義處理方式可重試?
400task_not_exist輪詢或 remix 了一個不存在、或屬於其他帳號的任務 ID。注意狀態碼——輪詢未知任務是 400,取消未知任務卻是 404。提交時就把 task_id 寫入資料庫,並嚴格用這個值去輪詢。否
400invalid_request缺少必填欄位——remix 缺 prompt,查詢缺 task_id。先修正請求主體或路徑再重試。否
400task_channel_disable衍生任務(remix、續寫)會被鎖定到原任務的通道,而該通道現在已被停用。改為提交一個全新任務,不要從舊任務派生。否
400invalid_source_task來源任務類型不符——例如把影片任務 ID 傳給了 3D 續作端點。確認來源任務 ID 來自同一類能力。否
404task_not_exist要取消的任務 ID 不存在,或不屬於你。沒有可取消的任務——當作已經不存在處理即可。否
409task_not_cancellable任務已進入終態,沒有可取消的內容。再輪詢一次,直接使用你已經付費的結果。否
501task_cancel_not_supported該任務背後的供應商沒有提供取消 API。大多數供應商都沒有——能取消是例外,不是常態。讓任務跑完。介面上要把「取消」設計成盡力而為,而不是一種保證。否
502cancel_task_failed取消過程中無法連線到供應商,任務狀態未發生變化。重試一次,仍不行就讓它自然跑完。是

非同步任務生命週期 →

還是沒解決?

把請求 ID 或錯誤訊息傳給我們,我們來查。