首頁/錯誤
Uncensored API 錯誤與速率限制實戰指南
程式碼在失敗路徑上花費的時間通常多於正常路徑。此指南列出 Uncensored API 回傳的每個狀態碼,說明哪些值得重試,並提供 Python asyncio 與 Node.js 客戶端針對每分鐘 300 次請求限制的節流機制,以及從容地向您的使用者顯示 402 錯誤的方式。
更新於
重點
- 僅重試 429、503 和傳輸故障;針對原因修復 400、401、403、404 和 402。
- 錯誤以 JSON 形式到達,錯誤物件包含 code 和 message,因此先根據狀態分支,再根據 code 分支。
- 每秒補充 5 個 token 的令牌桶可讓單一金鑰保持在每分鐘 300 請求以下,無需伺服器反饋。
- 402 表示預付餘額已用完或試用期結束;將其視為產品狀態,而非崩潰。
錯誤信封
每次失敗都會回傳一個 JSON 主體,其中包含一個單一的頂層 error 物件。有兩個欄位很重要:code,一個簡短的機器字串;以及 message,用於日誌和人類閱讀的文字。
{
"error": {
"code": "no_credit",
"message": "Balance used up or trial expired."
}
}請依此順序判斷。先查看 HTTP 狀態碼,因為它會指出問題類別。僅使用 code 來區分共享相同狀態碼的情況,且不要將 message 用於邏輯判斷,因為文字可能會變更。如需成功回應的結構,請參閱 API 參考文件。
狀態碼、原因與操作
| 狀態 | 代碼 | 典型原因 | 重試? | 操作 |
|---|---|---|---|---|
| 400 | 請求無效 | JSON 格式錯誤,或提示詞 token 加上 max_tokens 超過 100,000 | 否 | 修正資料;縮減歷史記錄或降低 max_tokens |
| 401 | 金鑰無效 | 缺少標頭、拼寫錯誤或金鑰已重新生成 | 否 | 從您的密鑰庫重新載入金鑰 |
| 402 | no_credit | 餘額已用完或免費試用已過期 | 否 | 為預付餘額儲值,然後恢復 |
| 403 | content_blocked | 請求涉及未成年人的色情內容 | 否 | 不要重新發送;顯示您的政策訊息 |
| 404 | 未找到 | 路徑錯誤,例如缺少 /v1 前綴 | 否 | 檢查基礎 URL |
| 429 | 速率限制 | 單一金鑰一分鐘內超過 300 個請求 | 是 | 使用抖動退避;添加客戶端節流 |
| 503 | upstream_busy | 臨時容量壓力 | 是 | 幾秒後重試 |
403 狀態碼值得特別強調:這是政策決定,而非暫時性故障。重複發送相同的請求會浪費額度,且回傳的結果仍會相同。讓使用者最驚訝的 400 錯誤是上下文總和超限。一個 60,000 token 的提示詞加上 max_tokens 設為 8,000,總和超出 4,000 就會失敗,儘管每個數字單獨來看都是有效的。
行為良好的重試策略
使用小型且有限制的政策,且僅在重試能帶來幫助的情況下套用。
- 將可重試的失敗列入白名單: 429、503、連線重置和逾時。其他所有錯誤應立即拋出。
- 帶有抖動的指數退避: 等待 1、2、4、8 秒,再加上隨機的小數秒數,上限為 30 秒。抖動可防止大量工作程序同時重試。
- 將重試次數上限設為五次。 超過此次數後,發生停擺的可能性大於運氣不佳,且排隊中的工作應明確顯示失敗。
- 停用 SDK 自動重試(
max_retries=0)當你自行執行迴圈時,否則重試次數會倍增,導致節流機制無法正確計數。 - 從頭開始重試串流。 中途失敗的串流無法恢復;請捨棄部分文字並重新發出請求,或者保留部分內容並要求繼續輸出。
對於 503 錯誤,建議是等待幾秒鐘,因此第一次延遲應設為一到兩秒,而不是一開始就猛烈發送請求。對於 429 錯誤,解決方法是降低速率,下一節將自動化此過程。
每分鐘保持在 300 個請求以下
限制為每金鑰每分鐘 300 個請求,平均每秒五個。令牌桶會精確執行此限制:它最多可儲存少量令牌,每秒補充五個令牌,且每個請求消耗一個令牌。如果令牌桶為空,呼叫端會休眠直到有令牌為止。
Python 版本使用 asyncio。所有二十個工作程序共用一個儲存桶與鎖,因此並行協同程式無法透支。請注意,儲存桶會計算每次嘗試,包含重試在內。
import asyncio, os, random, time
from openai import AsyncOpenAI, APIStatusError, APIConnectionError
class TokenBucket:
"""Allows `rate` acquisitions per `per` seconds, with a small burst."""
def __init__(self, rate=300, per=60.0, burst=10):
self.fill = rate / per # tokens added per second (5.0)
self.cap = burst
self.tokens = burst
self.stamp = time.monotonic()
self.lock = asyncio.Lock()
async def acquire(self):
async with self.lock:
while True:
now = time.monotonic()
self.tokens = min(self.cap, self.tokens + (now - self.stamp) * self.fill)
self.stamp = now
if self.tokens >= 1:
self.tokens -= 1
return
await asyncio.sleep((1 - self.tokens) / self.fill)
client = AsyncOpenAI(base_url="https://api.uncensoredapis.com/v1",
api_key=os.environ["API_KEY"], max_retries=0)
bucket = TokenBucket()
RETRYABLE = {429, 503}
async def ask(prompt, attempts=5):
for n in range(attempts):
await bucket.acquire()
try:
r = await client.chat.completions.create(
model="uncensored",
messages=[{"role": "user", "content": prompt}],
max_tokens=200,
)
return r.choices[0].message.content
except APIStatusError as e:
if e.status_code not in RETRYABLE or n == attempts - 1:
raise
except APIConnectionError:
if n == attempts - 1:
raise
await asyncio.sleep(min(30, 2 ** n) + random.uniform(0, 1))
async def main():
prompts = [f"Give one-line fact number {i} about tides." for i in range(1, 21)]
results = await asyncio.gather(*(ask(p) for p in prompts))
print(len(results), "answers; first:", results[0])
asyncio.run(main())匯出 API_KEY 後執行。二十個請求包含在突發量加上幾秒鐘的補充內,因此你會看到它很快完成;將數量提高到 600,執行時間會拉長到約兩分鐘,這正是限制發揮作用的時候。
Node 版本使用官方套件在 ES 模組中運作,透過 Promise 佇列串聯獲取操作,因此呼叫端會依序獲得服務。
import OpenAI from "openai";
class TokenBucket {
constructor(rate = 300, perMs = 60000, burst = 10) {
this.fillPerMs = rate / perMs;
this.cap = burst;
this.tokens = burst;
this.stamp = Date.now();
this.queue = Promise.resolve();
}
acquire() {
// Serialize callers so the bucket math never races.
this.queue = this.queue.then(async () => {
for (;;) {
const now = Date.now();
this.tokens = Math.min(this.cap, this.tokens + (now - this.stamp) * this.fillPerMs);
this.stamp = now;
if (this.tokens >= 1) { this.tokens -= 1; return; }
await new Promise((r) => setTimeout(r, Math.ceil((1 - this.tokens) / this.fillPerMs)));
}
});
return this.queue;
}
}
const client = new OpenAI({
baseURL: "https://api.uncensoredapis.com/v1",
apiKey: process.env.API_KEY,
maxRetries: 0,
});
const bucket = new TokenBucket();
async function ask(prompt, attempts = 5) {
for (let n = 0; n < attempts; n++) {
await bucket.acquire();
try {
const r = await client.chat.completions.create({
model: "uncensored",
messages: [{ role: "user", content: prompt }],
max_tokens: 200,
});
return r.choices[0].message.content;
} catch (err) {
const retryable = err.status === 429 || err.status === 503 || err.status === undefined;
if (!retryable || n === attempts - 1) throw err;
await new Promise((r) => setTimeout(r, Math.min(30000, 2 ** n * 1000) + Math.random() * 1000));
}
}
}
const prompts = Array.from({ length: 20 }, (_, i) => `Give one-line fact number ${i + 1} about tides.`);
const answers = await Promise.all(prompts.map((p) => ask(p)));
console.log(answers.length, "answers; first:", answers[0]);保守設定突發量。10 的突發量允許短暫的尖峰,同時持續速率保持在每秒五個。如果多個程序共用一個金鑰,請將速率分配給它們,例如三個工作程序每個每分鐘 100 個請求,或者將所有流量導向單一佇列。
在產品中優雅地處理 402
帶有程式碼 no_credit 的 402 錯誤表示預付餘額已歸零或免費試用已過期。這是你的最終使用者絕對不應看到的錯誤堆疊資訊,通常表示只有你能修正的問題:進行儲值。
建立三個層級。第一,detect 它在單一包裝函式中集中處理,這樣就不需要每個呼叫點都記住。第二,translate 它:瀏覽器或行動應用程式收到中性狀態如 capacity_paused 並顯示友善的畫面,而不是上游訊息。第三,alert 你自己,因為每個使用者會同時受到影響;發送通知給帳戶擁有者並切換功能旗標,讓 UI 停止提交請求。
# server side: translate the upstream 402 into a product state
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from openai import AsyncOpenAI, APIStatusError
import os
app = FastAPI()
client = AsyncOpenAI(base_url="https://api.uncensoredapis.com/v1", api_key=os.environ["API_KEY"])
@app.post("/api/reply")
async def reply(body: dict):
try:
r = await client.chat.completions.create(
model="uncensored", messages=body["messages"], max_tokens=400)
return {"text": r.choices[0].message.content}
except APIStatusError as e:
if e.status_code == 402:
# Tell the client app to show a "temporarily unavailable" screen,
# and page yourself to top up the prepaid balance.
return JSONResponse({"state": "capacity_paused"}, status_code=503)
if e.status_code == 429 or e.status_code == 503:
return JSONResponse({"state": "busy_retry"}, status_code=503)
raise同時加入主動保護。將每次請求的 usage.total_tokens 總和加入你自己的帳本,並在設定的閾值發出警告,以便在使用者被阻止前進行儲值。請記住,試用額度為 $0.50,持續 7 天,因此原型機可能會因過期而觸發 402,即使錢還沒花完。規劃用的費率請見 定價頁面。
一旦餘額恢復正數,請重新執行暫停期間排隊的工作。你的包裝函式應在第一次成功時清除暫停旗標。
記錄與警報的內容
良好的記錄能讓事件持續時間變短。針對每個請求記錄:你自己的請求 ID、完成回應的 id(若存在)、HTTP 狀態碼、錯誤 code、嘗試次數以及 token 使用量。切勿記錄 Authorization 標頭或包含個人資料的完整提示詞。
有用的警示(依優先順序):任何 402(立即處理)、持續出現的 401(重新產生的金鑰未在所有地方部署)、429 速率高於呼叫總數的幾百分比(節流設定不當)以及 503 集群(擴大退避時間,考慮使用佇列)。每分鐘狀態計數的儀表板可讓你一眼看到所有四種情況。妥善處理後,這些狀態碼會變成常規訊號,而非服務中斷。
在發布前測試你的失敗路徑
從未執行過的錯誤處理只是一種猜測。你可以使用便宜的請求和自定義的測試模擬來執行大部分分支,而不必等待真正的停擺。
- 401: 呼叫
GET /v1/models並使用故意錯誤的金鑰,確認應用程式向操作員顯示設定錯誤,而非向客戶顯示。 - 404: 請求
/v1/chat/completion(單數)一次,並確認日誌行指出了路徑,以便在幾分鐘內就能發現基本 URL 的拼寫錯誤。 - 400: 發送
max_tokens設為 32,000,提示詞填充至約 50,000 token。總和超過 100,000,你可以觀察驗證路徑觸發。 - 429 與 503: 你無法隨時要求這些狀態,因此請模擬它們。將你的 HTTP 客戶端包裝在一個模擬器中,讓前兩次呼叫返回該狀態,然後成功,並斷言你的迴圈會休眠、重試並回傳最終答案。
- 402: 也模擬它,並確認 UI 顯示暫停畫面、警報觸發且沒有迴圈緊密重試。
將這些寫成在持續整合中執行的自動化測試。針對上述表格中的每個狀態各有一列的表格驅動測試,撰寫只需幾分鐘,並可保護政策免受善意的重構影響。最後,使用 API 模擬器對你自己的速率限制器進行短時間的負載測試:發送 1,000 次獲取操作,並斷言任何 60 秒視窗內的授予次數不超過 300 次加上突發量。這證明了在真實流量依賴此限制之前的算術正確性。當你稍後閱讀 agent loop guide 時,請在該處重用相同的包裝函式,因為代理程式會倍增請求數量並最先達到這些限制。
問答
429 會計入我的餘額嗎?
請檢查你自己帳本中的使用數據,不要假設。無論哪種情況,解決方法都是降低速率並使用退避機制重試。
我應該重試 403 content_blocked 回應嗎?
不需要。這是政策封鎖,相同的請求會回傳相同的結果。顯示你自己的訊息並繼續。
我的金鑰突然回傳 401。發生了什麼變化?
最常見的情況是金鑰被重新生成,這會立即使舊金鑰無效。從你的帳戶複製新金鑰並重新部署。
多少個工作程序可以共用一個金鑰?
它們共用每分鐘 300 次請求的限制。將速率分散至多個工作程序,或將請求導入單一佇列。