오류 본문 형식 및 읽기 순서
모든 실패 응답은 동일한 구조를 가집니다:
{"error":{"code":"...","message":"..."}}
읽기 순서는 다음 세 단계로 고정됩니다:
- HTTP 상태 코드: 오류의 대분류를 결정합니다.
error.code: 구체적인 원인을 결정하며, 프로그램에서 분기 처리에 사용합니다.error.message: 사용자에게 표시하거나 로그에 기록합니다. 문자열 매칭에 사용하지 마세요.
먼저 명확한 경계를 설정하세요. 재시도로 해결 가능한 경우(429, 503 및 네트워크 타임아웃)와 수정해야 해결할 수 있는 경우(기타)입니다. 이 두 가지를 혼동하면 402에 대해 계속 재시도하여 초당 수십 개의 유효하지 않은 요청을 보내는 등 온라인 사고의 가장 흔한 원인이 됩니다.
로그에는 다음 네 가지 필드를 고정적으로 기록해야 합니다: 시간, 상태 코드, error.code, 현재 요청의 프롬프트 토큰 수 추정치. 문제가 발생하면 특정 오류 유형이 갑자기 증가했는지, 아니면 전체 실패율인지 한눈에 파악할 수 있습니다. 경고 규칙도 설정하세요: 402 오류가 발생하면 즉시 알림(서비스가 사용자에게 사용 불가 상태임을 의미), 429 오류는 비율을 확인합니다. 간헐적 발생은 무시하고, 지속적 발생은 동시성 설계 문제를 의미합니다.
상태 코드 빠른 참조
| 상태 코드 | error.code | 의미 | 재시도? |
|---|---|---|---|
| 400 | — | 잘못된 요청입니다. 프롬프트와 max_tokens의 합계가 64k를 초과했습니다. | 아니요 |
| 401 | — | 키가 유효하지 않거나 누락됨 | 아니요 |
| 402 | no_credit | 잔액 소진 또는 무료 체험 크레딧 만료 | 아니요 |
| 403 | content_blocked | 콘텐츠 차단됨 | 아니요 |
| 404 | — | 엔드포인트가 존재하지 않음 | 아니요 |
| 429 | — | 속도 제한 트리거 | 예, 백오프 적용 |
| 503 | upstream_busy | 서비스 일시적으로 혼잡 | 예, 몇 초 후 |
표에서 '—'은 특별히 처리해야 할 고정 code 문자열이 없음을 의미합니다. 상태 코드별로 분기 처리하면 됩니다.
4xx: 요청 수정, 재시도 금지
400 잘못된 요청
- 원인 1: JSON 형식 오류 (일반적으로 수동으로 문자열을 작성할 때 따옴표 이스케이프를 누락한 경우).
- 원인 2: prompt + max_tokens이 100,000을 초과함. 긴 대화에서 가장 흔하게 발생합니다.
- 원인 3: 요청 본문이 8 MB를 초과함.
- 수정 방법: JSON 라이브러리로 직렬화하세요; 전송 전 토큰 수를 추정하고, 초과 시 이전 대화 내용을 줄이거나 max_tokens을 줄이세요. 긴 컨텍스트 실전 가이드를 참조하세요.
401 키 관련 오류
- 헤더 누락 또는
Bearer접두사 누락. - 키를 재생성하면 이전 키는 즉시 무효화되지만, 일부 서버에서는 여전히 이전 키를 사용하고 있을 수 있습니다.
- 환경 변수가 컨테이너나 정기 작업에 전달되지 않았습니다.
402 no_credit
잔액이 소진되었거나 7일 무료 체험 기간이 만료되었습니다. 계정 페이지에서 선불 크레딧을 충전하면 서비스를 다시 사용할 수 있습니다. 서비스의 잔액을 직접 모니터링하여 사용자가 오류를 보고한 후에야 비로소 알아차리는 상황을 방지하는 것을 권장합니다.
403 content_blocked
콘텐츠가 차단되었습니다. 성인 콘텐츠, 허구적 내용 및 논쟁적인 주제들은 일반적으로 거부되지 않지만, 미성년자 관련 성적 콘텐츠는 소설이나 역할극을 포함해 모두 차단됩니다. 403 오류가 발생하면 문장을 바꿔서 다시 시도하기보다 입력값과 대화 기록에 해당 콘텐츠가 있는지 확인해야 합니다.
404 엔드포인트 없음
사용 가능한 엔드포인트는 POST /v1/chat/completions과 GET /v1/models 두 가지뿐입니다. 경로에 /v1이 누락되었거나 불필요한 슬래시가 추가되었거나 철자가 틀렸거나, 지원하지 않는 embeddings나 이미지 관련 API를 호출하면 404 오류가 발생합니다.
400 오류 중 하나가 쉽게 오해되는 경우입니다. 긴 대화의 토큰 수는 라운드마다 선형적으로 증가하므로, 낮에는 테스트가 잘되다가 수십 라운드 이후에 갑자기 400 오류가 발생할 수 있습니다. 이는 API의 불안정성이 아니라 컨텍스트 창이 가득 차서 발생한 것입니다. 해결책은 대화 기록에 상한선을 두는 것입니다. 임계값을 초과하면 가장 오래된 라운드를 삭제하거나, 오래된 내용을 요약하여 압축하세요. 오류가 발생한 후에 처리하기보다 요청을 보내기 전에 미리 예상하고 대비해야 합니다.
401 오류를 해결하는 팁: 키의 앞뒤 4글자만 로그에 출력하고 전체 내용은 출력하지 마세요. 계정 페이지에 표시된 값과 비교하여 프로세스에서 읽은 키가 실제로 내가 생각한 키인지 확인하세요. 특히 컨테이너와 정기 작업 환경에서는 환경 변수 누락 또는 이전 값 읽기가 가장 흔한 원인입니다.
429 및 503: 재시도가 필요한 두 가지 오류
429 속도 제한
각 키당 분당 300개의 요청이 허용됩니다. 배치 작업, 여러 인스턴스가 하나의 키를 공유하는 경우, 그리고 재시도 폭풍이 발생하면 이 제한에 도달할 수 있습니다. 대응책은 두 단계로 나뉩니다. 먼저 클라이언트 측에서 속도 제한을 설정하고(아래 코드 참조), 429 오류 발생 시 지수 백오프 방식으로 재시도하세요.
503 upstream_busy
서비스가 일시적으로 바쁩니다. 몇 초 후 다시 시도하면 됩니다. 즉시 연속으로 요청하거나 1초 안에 10번 재시도하면 상황이 악화될 뿐이므로 피하세요.
네트워크 타임아웃
긴 텍스트 생성에는 시간이 오래 걸리므로 타임아웃을 너무 짧게 설정하지 마세요. 예시는 120초입니다. 타임아웃 후 재시도 시 주의하세요: 이전 요청이 이미 서버에서 실행 중일 수 있어 중복 호출 시 크레딧이 중복 소모될 수 있습니다. 생성 요청의 경우 재시도 횟수를 줄이고, 스트리밍 출력을 활용하여 단일 대기 시간을 단축하세요.
import os
import random
import time
import requests
URL = "https://api.wushenchaapi.com/v1/chat/completions"
HEADERS = {
"Authorization": "Bearer " + os.environ["API_KEY"],
"Content-Type": "application/json",
}
RETRYABLE = {429, 503} # 只重试这两类
FATAL = {400, 401, 402, 403, 404}
class ApiError(Exception):
def __init__(self, status, code, message):
super().__init__(f"{status} {code}: {message}")
self.status, self.code = status, code
def call(payload, max_retries=5, base=1.0, cap=30.0):
for attempt in range(max_retries + 1):
try:
r = requests.post(URL, headers=HEADERS, json=payload, timeout=120)
except (requests.ConnectionError, requests.Timeout):
r = None # 网络层失败,按可重试处理
if r is not None and r.ok:
return r.json()
if r is not None:
try:
err = r.json().get("error", {})
except ValueError:
err = {}
if r.status_code in FATAL or r.status_code not in RETRYABLE:
raise ApiError(r.status_code, err.get("code"), err.get("message"))
if attempt == max_retries:
raise ApiError(r.status_code if r is not None else 0, "retry_exhausted", "重试次数用尽")
delay = min(cap, base * (2 ** attempt)) * random.uniform(0.5, 1.0)
time.sleep(delay)
if __name__ == "__main__":
out = call({"model": "uncensored", "max_tokens": 100,
"messages": [{"role": "user", "content": "回复一个字:好"}]})
print(out["choices"][0]["message"]["content"])핵심: 백오프 공식은 min(cap, base × 2^n) × 랜덤 계수이며, 랜덤 계수는 여러 클라이언트가 동시에 재시도하여 충돌하는 것을 방지합니다. FATAL 상태 코드는 한 번도 재시도하지 않습니다.
클라이언트 측 속도 제한 및 진단 요청
429 오류가 발생한 후에야 백오프를 시작하기보다, 사전에 속도 제한을 설정하세요. 아래 도구는 1분 내 요청 수가 설정값을 넘지 않도록 보장하며 스레드 안전합니다:
import threading
import time
class RateGate:
"""简单的客户端限速:保证一分钟内请求数不超过 limit。"""
def __init__(self, limit=240): # 留出余量,低于 300/分钟
self.limit, self.stamps, self.lock = limit, [], threading.Lock()
def wait(self):
while True:
with self.lock:
now = time.time()
self.stamps = [t for t in self.stamps if now - t < 60]
if len(self.stamps) < self.limit:
self.stamps.append(now)
return
sleep_for = 60 - (now - self.stamps[0])
time.sleep(max(sleep_for, 0.05))문제 해결 시 가장 깔끔한 방법은 비즈니스 코드와 분리하여 curl로 최소한의 요청을 보내는 것입니다. -i 옵션을 사용하면 상태 행과 응답 헤더를 동시에 확인할 수 있습니다:
curl -i https://api.wushenchaapi.com/v1/chat/completions \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"uncensored","max_tokens":20,"messages":[{"role":"user","content":"ping"}]}'curl은 잘 되지만 비즈니스 코드가 작동하지 않으면 문제가 코드나 환경(프록시, 환경 변수, 인코딩)에 있습니다. curl도 작동하지 않으면 키, 잔액 및 네트워크 상태를 확인하세요.
속도 제한 값을 300이 아닌 240으로 설정하는 이유: 여러 인스턴스 배포 시 각 인스턴스의 속도 제한 합계가 총량을 초과할 수 있으며, 재시도로 인해 추가 할당량이 소모됩니다. 20% 여유분을 두는 것이 가장 안전한 방법입니다. 여러 인스턴스가 있다면 각 인스턴스의 할당량을 총량에 따라 균등하게 나누거나 공유 카운터를 사용하여 일괄 조정하세요.
문제 해결 체크리스트
- 상태 코드만 보지 말고 body 내의
error.code를 확인하세요. - 401: 키 접두사, 환경 변수, 키 재생성 여부 확인
- 400: JSON 유효성, 프롬프트 + max_tokens이 100,000을 초과했는지, 요청 본문이 8 MB를 초과했는지 확인
- 402: 잔액 및 무료 체험 기간.
- 403: 입력값과 기록에 금지된 콘텐츠가 포함되어 있는지 확인
- 404: 경로가 /v1/chat/completions 또는 /v1/models인지 확인
- 429: 여러 인스턴스가 하나의 키를 공유하고 있는지, 클라이언트 측 속도 제한이 적용되었는지 확인
- 503: 백오프 재시도가 적용되었는지, 간격이 최소 몇 초인지 확인
- 타임아웃: timeout 값이 충분히 큰지, 스트리밍으로 변경 가능한지 확인
- 위 모든 항목이 제외되었을 경우: curl 최소 요청으로 재현하세요.
방금 연동했다면 연동 튜토리얼을 먼저 확인하세요. 추가 파라미터는 문서에서 확인하세요.
체크리스트 사용법: 문제가 발생하면 위에서 아래로 순서대로 항목을 하나씩 제외해 가며 원인을 찾습니다. 건너뛰지 마세요. 대부분의 '이상한' 오류는 상위 네 가지 항목에 원인이 있습니다.
추가 경험칙: 매번 문제 해결 결론을 팀 문서에 기록하세요. 그러면 다음에 동일한 오류가 발생했을 때 바로 대응할 수 있습니다.