繁中 ▾

OpenAI 相容無審查 LLM APIuncensoredapis.com

首頁錯誤

Uncensored API 錯誤與速率限制實戰指南

程式碼在失敗路徑上花費的時間通常多於正常路徑。此指南列出 Uncensored API 回傳的每個狀態碼,說明哪些值得重試,並提供 Python asyncio 與 Node.js 客戶端針對每分鐘 300 次請求限制的節流機制,以及從容地向您的使用者顯示 402 錯誤的方式。

更新於

重點

  1. 僅重試 429、503 和傳輸故障;針對原因修復 400、401、403、404 和 402。
  2. 錯誤以 JSON 形式到達,錯誤物件包含 code 和 message,因此先根據狀態分支,再根據 code 分支。
  3. 每秒補充 5 個 token 的令牌桶可讓單一金鑰保持在每分鐘 300 請求以下,無需伺服器反饋。
  4. 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金鑰無效缺少標頭、拼寫錯誤或金鑰已重新生成否從您的密鑰庫重新載入金鑰
402no_credit餘額已用完或免費試用已過期否為預付餘額儲值,然後恢復
403content_blocked請求涉及未成年人的色情內容否不要重新發送;顯示您的政策訊息
404未找到路徑錯誤,例如缺少 /v1 前綴否檢查基礎 URL
429速率限制單一金鑰一分鐘內超過 300 個請求是使用抖動退避;添加客戶端節流
503upstream_busy臨時容量壓力是幾秒後重試

403 狀態碼值得特別強調:這是政策決定,而非暫時性故障。重複發送相同的請求會浪費額度,且回傳的結果仍會相同。讓使用者最驚訝的 400 錯誤是上下文總和超限。一個 60,000 token 的提示詞加上 max_tokens 設為 8,000,總和超出 4,000 就會失敗,儘管每個數字單獨來看都是有效的。

行為良好的重試策略

使用小型且有限制的政策,且僅在重試能帶來幫助的情況下套用。

  1. 將可重試的失敗列入白名單: 429、503、連線重置和逾時。其他所有錯誤應立即拋出。
  2. 帶有抖動的指數退避: 等待 1、2、4、8 秒,再加上隨機的小數秒數,上限為 30 秒。抖動可防止大量工作程序同時重試。
  3. 將重試次數上限設為五次。 超過此次數後,發生停擺的可能性大於運氣不佳,且排隊中的工作應明確顯示失敗。
  4. 停用 SDK 自動重試(max_retries=0)當你自行執行迴圈時,否則重試次數會倍增,導致節流機制無法正確計數。
  5. 從頭開始重試串流。 中途失敗的串流無法恢復;請捨棄部分文字並重新發出請求,或者保留部分內容並要求繼續輸出。

對於 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 次請求的限制。將速率分散至多個工作程序,或將請求導入單一佇列。

只差一張表單,即可取得金鑰

建立帳戶,複製金鑰,更改基本 URL。這就是整個設定。