spicyapi文件
主要內容

錯誤碼

全部業務碼、對應的 HTTP 狀態、以及哪些重試有意義、哪些純屬浪費。

回應外殼

原生任務 API 的 JSON 回應使用以下結構;/v1 相容介面和 SSE 按各自協定解析:

{ "code": 200, "msg": "success", "data": { }, "request_id": "req_…" }
  • code 是業務碼,不是 HTTP 碼。 200 表示成功。
  • 失敗時 data 不出現(不是 null),只有 codemsgrequest_id
  • msg 是給人看的,措辭會變,語言也可以選(見錯誤說明的語言)。不要拿它做程式判斷,判斷只看 code
  • request_id 也在回應標頭 X-Request-Id 上。回報問題時附上它。

HTTP 狀態碼同時也會正確設定。兩者並行,你可以任選一種判斷方式——但只選一種code 與 HTTP 碼在細分碼上是不同名的(餘額不足是 code: 40201 + HTTP 402),混著判斷遲早會漏掉一類。

我們的建議是code404 只能說「沒找到」,40201 能說清「可用餘額不足」。

錯誤說明的語言

給人讀的錯誤說明可以用你選的語言回傳,預設是英文。會跟著語言改變的只有這三處:

  • 回應外殼裡的 msg
  • 任務失敗時的 errorMessagerecordInfo 與 Webhook 回呼都一樣;
  • 相容介面錯誤物件裡的 error.message,OpenAI、Anthropic 與 Google Gemini(/v1beta)三種格式都適用。

支援的語言代碼:enzh-Hantjakodefrespt-BRru

怎麼選語言

依下列順序決定,排在前面的優先:

  1. 請求標頭 Accept-Language,只對這一次請求生效。採用標準 HTTP 權重語法,按 q 值由高到低,取第一個能辨識的語言。地區變體會歸到對應的語言:de-DEdept-PTpt-BRzh-TWzh-HKzh-Hant 都算 zh-Hant。中文只提供繁體,所以 zh-CNzh-Hans 不會對應到任何語言,會接著看下一個候選;* 視為沒有偏好。
  2. 帳戶設定:控制台「設定 → API 錯誤語言」。用這個帳戶的 API 金鑰發出、又沒帶可辨識 Accept-Language 的請求,都照這裡的設定。多數伺服器端 SDK 與 curl 預設不帶這個標頭,所以這是最常用的方式。Webhook 回呼沒有請求標頭可以參考,一律照帳戶設定。
  3. 兩者都沒有時,用英文。

錯誤回應會帶上 Content-Language 回應標頭,標明這次錯誤說明實際用的語言。

只有給人讀的文字會變

codeerrorCode、OpenAI 相容層的 type / code、Gemini 相容層的 code / status,以及所有欄位名,都不隨語言改變。程式一律按碼判斷,不要解析文字;日誌與監控警示建議記錄 coderequest_id

範例

同一個 40201(餘額不足),預設回傳英文:

預設
HTTP/1.1 402 Payment Required
Content-Language: en

{ "code": 40201, "msg": "Insufficient balance", "request_id": "req_…" }

在請求上加一行 Accept-Language: ja,其餘與快速入門裡的建任務請求相同:

curl -X POST https://api.spicyapi.ai/api/v1/jobs/createTask \
  -H "Authorization: Bearer $SPICY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $SPICY_IDEMPOTENCY_KEY" \
  -H "Accept-Language: ja" \
  --data "$TASK_PAYLOAD"

msg 換成日文,code 仍是 40201

Accept-Language: ja
HTTP/1.1 402 Payment Required
Content-Language: ja

{ "code": 40201, "msg": "残高が不足しています", "request_id": "req_…" }

通用碼

codeHTTP含義該重試嗎
200200成功
400400請求格式或參數不合法不該。改對了再發
401401憑證無效或已失效不該。換金鑰
403403沒有存取該資源的權限不該
404404資源不存在(或不屬於你)不該
409409請求與目前狀態衝突看情況,見下文
40901409報價過期、輸入變化或價格變化重新報價並確認;已受理請求繼續用原冪等鍵恢復
413413請求本文超過伺服器端允許的大小不該。縮小請求本文,或依媒體上傳流程傳送檔案
429429請求過於頻繁。按 Retry-After 退避
500500我們這邊出錯了。退避後重試

餘額與額度

codeHTTP含義怎麼辦
40201402餘額不足以覆蓋本次任務的預估費用儲值。可用餘額見 GET /api/v1/chat/credit
40202402達到金鑰、團隊或平台的消費上限在控制台調高上限。日上限 UTC 零點重新起算,月上限每月 1 日 UTC 零點重新起算,累計上限不會自己恢復

這兩類重試沒有意義——餘額和上限不會因為你多請求幾次而變化。

權限

codeHTTP含義怎麼辦
40301403該金鑰不允許呼叫此模型在控制台放開這把金鑰的模型範圍
40302403來源 IP 位址不在該金鑰的白名單內補白名單,或關掉這把金鑰的 IP 限制
40303403該地區暫不提供服務無法繞過

全部不可重試。

模型可用性

codeHTTP含義該重試嗎
50301503該模型目前不可用。稍後重試,或換一個模型

50301 表示建立任務時這個模型就無法受理:模型未上架、沒有生效中的價格,或暫時沒有可用的生成容量。臨時容量問題可以有限重試;模型未開放或沒有有效價格時,重複請求不會恢復可用性。重新讀取目錄,只有使用者確認後才切換模型。

受理後的生成失敗體現在 state=failed

createTask 回傳任務 ID 之後才發生的生成失敗,會出現在 recordInfostate: "failed"errorCode / errorMessage 裡,凍結金額全額釋放。不要自行新增公開 OpenAPI 未宣告的同步業務碼分支。

任務失敗碼

受理之後的失敗落在 recordInfoerrorCode 上。它是一個封閉集合:內部診斷碼不會出現在這裡,讀到列表之外的值一律按 upstream_failed 處理。

errorCode含義該重試嗎
invalid_request參數沒有被接受不該。改對參數再提交
unsupported_combination目前不支援這組參數不該。換一組參數
content_rejected內容策略拒絕了這次請求不該。改提示詞,或換一個模型
rate_limited生成側當時繁忙。退避後重試
upstream_unavailable生成能力暫時不可用。稍後重試
invalid_asset參考素材已不可用,但要先把素材重新上傳
generation_failed已經開始生成但沒有產出看情況。同一組參數很可能會再失敗一次
timeout超過任務時限仍未完成看情況。可以縮短時長或降低解析度再試
upstream_failed其餘:以上未涵蓋的失敗。退避後重試

以上每一種都會全額釋放凍結金額。errorMessage 是同一件事的自然語言說明,措辭會變——判斷只看 errorCode

哪些錯誤會計費

先區分任務終態與用戶端連線狀態。

明確拒絕建立的請求、終態為 failed / expired 的任務及歷史 canceled 記錄不計生成費用。用戶端逾時或斷開連線不等於任務失敗;受理後的任務不可取消,應恢復原任務並檢視最終狀態和結算。串流中途斷線時,若已產生有效用量,仍可能計費。

情況是否扣款
API 明確拒絕 createTask,未建立任務不扣。建任務和凍結在同一筆交易內完成;閘道器錯誤或未收到回應不證明拒絕
提交後網路逾時、網頁關閉或連線中斷待確認。用原冪等鍵恢復受理結果,不能當成免費重試
createTask 回傳 202,任務後來 failed / expired不扣。凍結金額全額釋放
舊資料中的 canceled 任務不扣。這是相容狀態,不代表目前可以取消任務
createTask 回傳 202,任務 succeeded,按實際用量結算,但不超過受理時的凍結額;少用釋放差額,之後不補扣超過凍結額的金額
recordInfochat/creditcommon/* 的任何錯誤不扣。這些介面本來就不計費
觸發速率限制(429)、被冪等鍵擋下(重複提交)不扣。前者請求沒被受理,後者回傳的是首次那個任務,只扣過一次

失敗不計費是承諾而不是盡力而為:即使我們的程式在中途崩潰,補救掃描也會把凍結的錢退回去。可透過任務結算狀態和餘額核對處理結果,見計費

409 什麼時候值得重試

情境值得重試嗎
同鍵、同請求並行時極少出現的冪等鍵衝突值得。退避幾百毫秒,帶同一個鍵和同一份請求重試。見冪等鍵
同一個鍵已經用於不同請求或另一把 API Key不值得。停止並修正鍵的歸屬;重試不會改變衝突
輸出檔案還在轉存值得。等幾秒再取得下載網址

重試該怎麼寫

不要無腦重試

40040140201 這一類,換多少次結果都一樣。上表裡標「不該」的,重試是純粹的浪費——而且每一次都在啃你自己的速率限制額度。

指數退避 + 隨機抖動(避免一批失敗的請求同時回來打成驚群):

JavaScript
// 409 與具體操作有關,必須由呼叫點結合操作語義處理。
const RETRYABLE = new Set([429, 500, 50301]);

async function callWithRetry(path, init, { attempts = 4 } = {}) {
  for (let i = 0; i < attempts; i++) {
    const res = await fetch(`https://api.spicyapi.ai/api/v1${path}`, init);
    const body = await res.json();

    if (body.code === 200) return body.data;
    if (!RETRYABLE.has(body.code)) {
      throw new Error(`${body.code} ${body.msg} (request_id=${body.request_id})`);
    }

    // 觸發速率限制時,伺服器端已經告訴你該等多久,別自己猜
    const retryAfter = Number(res.headers.get('retry-after'));
    const backoff = Number.isFinite(retryAfter) && retryAfter > 0
      ? retryAfter * 1000
      : 2 ** i * 1000 + Math.random() * 500;

    await new Promise((r) => setTimeout(r, backoff));
  }
  throw new Error('重試次數已用盡');
}
Python
import random, time, requests

# 409 與具體操作有關,必須由呼叫點結合操作語義處理。
RETRYABLE = {429, 500, 50301}
BASE = "https://api.spicyapi.ai/api/v1"


def call_with_retry(method: str, path: str, attempts: int = 4, **kwargs) -> dict:
    for i in range(attempts):
        res = requests.request(method, f"{BASE}{path}", **kwargs)
        body = res.json()

        if body["code"] == 200:
            return body["data"]
        if body["code"] not in RETRYABLE:
            raise RuntimeError(f'{body["code"]} {body["msg"]} (request_id={body.get("request_id")})')

        # 觸發速率限制時,伺服器端已經告訴你該等多久,別自己猜
        retry_after = res.headers.get("retry-after")
        backoff = float(retry_after) if retry_after else 2**i + random.random() * 0.5
        time.sleep(backoff)

    raise RuntimeError("重試次數已用盡")
Go
package main

import (
	"encoding/json"
	"fmt"
	"io"
	"math/rand"
	"net/http"
	"strconv"
	"time"
)

// retryable 是值得重試的業務碼。不在這張表裡的碼,
// 換多少次結果都一樣——重試只是在啃自己的速率限制額度。
// 生成本身是否失敗,要從 recordInfo 的 state 讀。
var retryable = map[int]bool{429: true, 500: true, 50301: true}

type envelope struct {
	Code      int             `json:"code"`
	Msg       string          `json:"msg"`
	Data      json.RawMessage `json:"data"`
	RequestID string          `json:"request_id"`
}

func callWithRetry(newReq func() (*http.Request, error), attempts int, out any) error {
	for i := 0; i < attempts; i++ {
		req, err := newReq()
		if err != nil {
			return err
		}
		res, err := http.DefaultClient.Do(req)
		if err != nil {
			// 連線層失敗:請求可能已經到達。建任務時務必帶同一個
			// Idempotency-Key,否則這裡的重試會變成重複生成。
			time.Sleep(backoff(i, ""))
			continue
		}

		var env envelope
		decErr := json.NewDecoder(res.Body).Decode(&env)
		io.Copy(io.Discard, res.Body)
		res.Body.Close()
		if decErr != nil {
			return decErr
		}

		if env.Code == 200 {
			return json.Unmarshal(env.Data, out)
		}
		if !retryable[env.Code] {
			return fmt.Errorf("%d %s (request_id=%s)", env.Code, env.Msg, env.RequestID)
		}
		// 觸發速率限制時,伺服器端已經告訴你該等多久,別自己猜
		time.Sleep(backoff(i, res.Header.Get("Retry-After")))
	}
	return fmt.Errorf("重試次數已用盡")
}

// backoff 優先採信 Retry-After,否則指數退避加隨機抖動
//(抖動是為了避免一批失敗的請求同時回來打成驚群)。
func backoff(attempt int, retryAfter string) time.Duration {
	if s, err := strconv.Atoi(retryAfter); err == nil && s > 0 {
		return time.Duration(s) * time.Second
	}
	base := time.Duration(1<<attempt) * time.Second
	return base + time.Duration(rand.Intn(500))*time.Millisecond
}

重試 createTask一定要帶上同一個 Idempotency-Key,否則「上一次其實成功了,只是回應遺失了」會變成重複生成、重複扣費。

疑難排解

拿到一個看不懂的錯誤時,依這個順序看:

  1. request_id —— 回報問題時給我們這個,我們能直接定位到那一次請求。
  2. code —— 上面的表。
  3. 控制台的請求日誌 —— 每一次呼叫的請求參數、回應內容、耗時與計費都在那裡。

500msg 恆為一句通用的「服務暫時不可用」,不含任何內部細節——這是刻意的,內部結構、元件名稱、堆疊都不會出現在回應裡。這類問題只能靠 request_id 查。

本頁目錄