KO ▾

OpenAI 호환 무검열 LLM APIuncensoredapis.com

홈오류

Uncensored API을 위한 오류 및 속도 제한 실전 플레이북

프로덕션 코드는 성공 경로보다 실패 경로에서 더 많은 시간을 보냅니다. 이 안내서는 Uncensored API이 반환하는 각 상태 코드를 나열하고, 재시도가 적합한 코드를 알려주며, Python asyncio와 Node용 분당 300개 요청 제한에 대한 클라이언트 측 속도 제한과 402를 사용자에게 차분하게 표시하는 방법을 제공합니다.

업데이트일:

핵심 사항

  1. 429, 503 및 전송 오류만 재시도하고, 400, 401, 403, 404 및 402는 근본 원인을 수정합니다.
  2. 오류는 코드와 메시지를 포함하는 오류 객체와 함께 JSON으로 전달되므로, 먼저 상태 코드를 기준으로 분기한 후 코드를 확인하세요.
  3. 초당 5개의 토큰이 충전되는 토큰 버킷은 서버의 거부 없이 분당 300개 요청을 하나의 키로 유지합니다.
  4. 402는 선불 잔액이 고갈되었거나 체험 기간이 만료되었음을 의미합니다. 이를 크래시가 아닌 제품 상태로 처리하세요.

오류 엔벨로프

모든 실패는 단일 최상위 error 객체를 포함하는 JSON 본문을 반환합니다. 두 가지 필드가 중요합니다: code(짧은 기계 문자열)과 message(로그와 사람이 읽기 위한 텍스트).

{
  "error": {
    "code": "no_credit",
    "message": "Balance used up or trial expired."
  }
}

다음 순서로 판단하십시오. 먼저 HTTP 상태 코드를 확인하십시오. 이는 문제의 클래스를 알려줍니다. code는 동일한 상태를 공유하는 경우를 구분하기 위해 사용하십시오. 문구가 변경될 수 있으므로 message를 로직을 위해 파싱하지 마십시오. 성공적인 응답의 형태에 대해서는 API 레퍼런스를 참조하십시오.

상태 코드, 원인 및 조치

상태코드일반적인 원인재시도?조치
400잘못된 요청잘못된 JSON 또는 프롬프트 토큰과 max_tokens의 합계가 100,000을 초과함아니요페이로드를 수정하세요; 기록을 줄이거나 max_tokens을 낮추세요
401잘못된 키누락된 헤더, 오타 또는 재생성된 키아니요시크릿 스토어에서 키를 다시 로드하세요
402no_credit잔액 고갈 또는 무료 체험 종료아니요선불 잔액 충전 후 재개
403content_blocked미성년자가 포함된 성적 콘텐츠가 요청됨아니요재전송하지 마세요; 자체 정책 메시지 표시
404찾을 수 없음잘못된 경로 (예: 누락된 /v1 접두사)아니요Base URL 확인
429속도 제한하나의 키로 분당 300회 초과 요청예지터와 함께 백오프; 클라이언트 스로틀링 추가
503upstream_busy임시 용량 압력예몇 초 후 재시도

403은 강조할 가치가 있습니다. 이는 일시적 오류가 아닌 정책 결정입니다. 동일한 요청을 재시도하면 할당량이 낭비되고 동일한 응답이 반환됩니다. 사람들이 가장 놀라는 400 케이스는 컨텍스트 합계입니다. max_tokens가 8,000인 60,000 토큰 프롬프트는 4,000이 오버플로우되어 개별적으로 모든 숫자가 유효함에도 불구하고 실패합니다.

올바르게 동작하는 재시도 정책

작고 제한된 정책을 사용하고, 재시도가 도움이 되는 곳에 적용하십시오.

  1. 재시도 가능한 오류를 화이트리스트에 추가하십시오: 429, 503, 연결 재설정 및 시간 초과. 그 외의 모든 오류는 즉시 발생시킵니다.
  2. 지터가 있는 지수 백오프: 1, 2, 4, 8초 대기 후 1초 미만의 무작위 시간을 추가하되 최대 30초로 제한합니다. 지터는 많은 워커가 동시에 재시도하는 것을 방지합니다.
  3. 시도 횟수를 5회로 제한하십시오. 그 이상은 운 나쁨보다 장애일 가능성이 높으며, 대기 중인 작업은 명확하게 실패해야 합니다.
  4. SDK 자동 재시비 비활성화(max_retries=0)은 자체 루프를 실행할 때 사용하십시오. 그렇지 않으면 시도 횟수가 기하급수적으로 증가하여 스로틀러가 카운트를 잃습니다.
  5. 스트림을 처음부터 재시도하세요. 중간에 실패한 스트림은 재개할 수 없으므로 부분 텍스트를 버리고 요청을 다시 보내거나, 그대로 유지하고 계속 요청하세요.

503의 경우 몇 초간 기다리는 것이 권장되므로, 즉시 공격하기보다 첫 지연 시간을 1~2초로 시작하십시오. 429의 해결책은 속도 조절이며, 다음 섹션에서 이를 자동화합니다.

분당 300개 요청 미만 유지

제한은 키당 분당 300개 요청이며, 이는 평균 초당 5개입니다. 토큰 버킷은 이를 정확히 강제합니다. 작은 버스트 토큰을 최대까지 보유하며 초당 5개 속도로 충전되고 각 요청은 1개의 토큰을 소모합니다. 버킷이 비면 호출자는 토큰이 생성될 때까지 대기합니다.

Python 버전은 asyncio를 사용합니다. 20개 작업은 하나의 버킷과 잠금을 공유하므로 동시 코루틴이 이를 초과하지 않습니다. 버킷은 모든 시도(재시도 포함)를 카운트한다는 점에 유의하십시오.

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를 내보내어 실행하세요. 20개 요청은 버스트와 몇 초간의 충전 내에 들어갈 수 있으므로 빠르게 완료됩니다. 600으로 높이면 실행 시간이 약 2분으로 늘어나는데, 이는 제한이 제 역할을 하고 있음을 의미합니다.

ES 모듈에서 공식 패키지를 사용하는 Node 버전은 promise queue를 통해 획득을 체이닝하여 호출자가 순서대로 처리되도록 합니다.

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은 지속 속도가 초당 5개일 때 짧은 스파이크를 허용합니다. 여러 프로세스가 하나의 키를 공유하는 경우 속도를 분할하십시오. 예를 들어 3개의 워커가 각각 분당 100개의 요청을 처리하거나, 모든 트래픽을 하나의 큐로 라우팅하십시오.

제품에서 402 오류를 우아하게 처리하기

코드 no_credit이 있는 402 오류는 선불 잔고가 0에 도달했거나 무료 체험 기간이 만료되었음을 의미합니다. 이는 최종 사용자가 스택 트레이스로 절대 보서는 안 되는 유일한 오류이며, 일반적으로 귀하만 해결할 수 있는 문제(충전)를 나타냅니다.

세 가지 레이어를 구축하십시오. 첫째, 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, 시도 횟수 및 토큰 사용량. 인증 헤더나 개인 데이터를 포함하는 전체 프롬프트는 절대 로그에 남기지 마세요.

우선순위에 따른 유용한 알림: 모든 402(즉시), 지속된 401 실행(재생성된 키가 모든 곳에 배포되지 않음), 호출의 몇 퍼센트 이상인 429 속도(스로틀링이 잘못 설정됨), 503 클러스터(더 넓은 범위로 백오프, 큐 고려). 분당 상태 카운트의 대시보드는 이 네 가지를 한눈에 볼 수 있게 합니다. 잘 처리되면 이러한 코드는 장애가 아닌 일상적인 신호가 됩니다.

출시 전 실패 경로 테스트

실행되지 않은 오류 처리는 추측에 불과합니다. 저렴한 요청과 자체 테스트 더블을 사용하여 실제 장애를 기다리지 않고도 대부분의 분기를 테스트할 수 있습니다.

  • 401: 의도적으로 잘못된 키로 GET /v1/models 호출하여 앱이 사용자에게가 아닌 운영자에게 구성 오류를 표시하는지 확인하십시오.
  • 404: 단일 /v1/chat/completion 요청을 한 번 보내고 로그 줄에 경로가 명시되어 있어 base URL의 오타가 몇 분 안에 명확해지도록 확인하십시오.
  • 400: 프롬프트를 약 50,000 토큰으로 패딩하여 max_tokens 32,000을 보내십시오. 합계가 100,000을 초과하므로 검증 경로가 작동하는 것을 확인할 수 있습니다.
  • 429 및 503: 이러한 오류는 필요할 때 즉시 발생하지 않으므로 스텁 처리하세요. HTTP 클라이언트를 감싸서 첫 두 호출에서 해당 상태를 발생시키고 성공하게 한 후, 루프가 대기, 재시도 및 최종 응답 반환을 수행하는지 확인하세요.
  • 402: 이것도 스텁 처리하고 UI가 일시 중지 화면을 표시하며 알림이 발생하고 긴밀한 루프에서 재시동하지 않는지 확인하십시오.

이것들을 지속적 통합에서 실행되는 자동화된 테스트로 작성하십시오. 위의 표의 각 상태당 한 행이 있는 테이블 기반 테스트는 작성하는 데 몇 분 정도 걸리며 정책이 의도적인 리팩토링으로부터 보호됩니다. 마지막으로 API를 스텁 처리하여 자체 스로틀러에 짧은 부하 테스트를 실행하십시오: 1,000개의 획득을 발생시키고 60초 창에 300개의 승인 plus 버스트보다 더 많은 것이 포함되지 않았다고 확인하십시오. 이는 실제 트래픽이 이에 의존하기 전에 산술을 증명합니다. 나중에 에전트 루프 가이드를 읽을 때 에이전트가 요청 수를 기하급수적으로 증가시키고 이러한 제한에 먼저 도달하므로 동일한 래퍼를 재사용하십시오.

질문과 답변

429는 내 잔고에 포함되나요?

가정하지 말고 자체 원장의 사용량 수치를 확인하십시오. 어쨌든 해결책은 속도를 줄이고 백오프와 함께 재시동하는 것입니다.

403 content_blocked 응답을 재시동해야 하나요?

아니요. 이는 정책 차단이며 동일한 요청은 동일한 결과를 반환합니다. 자체 메시지를 표시하고 넘어가십시오.

내 키가 갑자기 401을 반환합니다. 무엇이 변경되었나요?

대부분의 경우 키가 재생성되어 이전 키가 즉시 무효화됩니다. 계정에서 새 키를 복사하고 재배포하십시오.

하나의 키를 공유할 수 있는 워커 수는?

모든 워커는 분당 300개 요청 한도를 공유합니다. 속도를 워커 간에 분할하거나 호출을 하나의 큐로 모으십시오.

키는 양식 하나만 작성하면 받을 수 있습니다

계정을 생성하고 키를 복사한 다음 base URL을 변경하십시오. 이것이 전체 설정입니다.