오류
모든 실패는 JSON 본문과 HTTP 상태로 돌아옵니다. 키 없이 보낸 요청에 대한 응답은 다음과 같습니다:
{
"error": {
"message": "Missing API key. Send it as: Authorization: Bearer <your OceanToken key>.",
"type": "authentication_error",
"param": null,
"code": "invalid_api_key"
}
}
분기는 **code**로 하세요. 안정적으로 유지하는 고정 어휘입니다. message는 사람이 읽는 용도이며 문구가 바뀔 수 있습니다. type은 같은 방식으로 처리하는 코드를 묶은 분류입니다. HTTP 상태도 같은 내용을 더 굵은 단위로 알려 주므로, 상태로 분기하는 기존 클라이언트도 그대로 동작합니다.
코드
| HTTP | type | code | 무슨 일이 있었나 | 대처 방법 |
|---|---|---|---|---|
| 400 | invalid_request_error | invalid_request | 요청 형식이 잘못되었습니다: 필드 누락, 모델이 받지 않는 값, 손상된 이미지. | 요청을 고치세요. 그대로 재시도하지 마세요. |
| 400 | not_found_error | model_not_found | 그 model 이름은 존재하지 않습니다. | GET /v1/models에서 이름을 고르세요. |
| 400 | insufficient_credits | insufficient_credits | 사용 가능한 크레딧을 모두 썼습니다. 아무것도 실행되지 않았고 청구도 없습니다. | 크레딧을 충전한 뒤 재시도하세요. |
| 400 | insufficient_credits | key_budget_exceeded | 이 키 자체의 크레딧 한도를 다 썼습니다(계정에는 아직 크레딧이 있을 수 있음). | 키의 한도를 올리거나, 초기화를 기다리거나, 다른 키를 쓰세요. |
| 400 | insufficient_credits | request_exceeds_balance | 잔액이 $0보다 크지만, 진행 중인 요청이 확보한 몫을 빼고 남은 금액이 이 요청의 최대 비용보다 적습니다. 최악의 경우는 입력을 전체 입력 가격으로, max_tokens만큼의 출력(모델의 출력 한도까지; max_tokens가 없으면 그 한도, 최대 16,384 토큰)을 더해 n을 곱하고, 요청별 또는 검색 요금을 더한 값으로, 모델의 어느 채널에서든 청구될 수 있는 가장 높은 가격(긴 컨텍스트와 추론 가격 포함)으로 계산합니다. 아무것도 실행되지 않았고 청구도 없습니다. | max_tokens를 낮추거나 입력을 줄이거나, 크레딧을 충전하세요. |
| 400 | content_policy_violation | content_filtered | 모델 실행 전에 안전 검사가 요청을 거부했습니다. 청구 없음. | 입력을 바꾸세요. 같은 입력으로 재시도하면 같은 결과가 나옵니다. |
| 401 | authentication_error | invalid_api_key | 키가 없거나, 알 수 없는 키이거나, 삭제된 키입니다. | Authorization: Bearer sk-... 헤더를 확인하세요. |
| 401 | authentication_error | key_expired | 키의 만료일이 지났습니다. | 새 키를 만드세요. |
| 403 | permission_error | model_not_allowed | 키는 유효하지만 이 모델은 계정에 허용된 범위 밖입니다. | GET /v1/models에 있는 모델을 쓰세요. |
| 403 | permission_error | forbidden | 이 키로는 이 작업을 할 수 없습니다. API 키 관리가 그 예로, API 키로는 키를 만들거나 바꾸거나 교체하거나 삭제할 수 없습니다. | 키는 콘솔에서 관리하세요. 그 밖에는 알맞은 권한의 키를 쓰세요. |
| 404 | not_found_error | not_found | 알 수 없는 경로이거나, 존재하지 않는 동영상 / 작업 id입니다. | URL과 id를 확인하세요. |
| 409 | idempotency_error | idempotency_key_reused | 이 Idempotency-Key는 이미 다른 본문에 쓰였습니다. | 새 요청에는 새 키를 쓰세요. 재시도를 보세요. |
| 409 | idempotency_error | idempotency_key_in_progress | 이 키를 쓴 첫 요청이 아직 실행 중입니다. | Retry-After초만큼 기다린 뒤 같은 요청을 재시도하세요. |
| 429 | rate_limit_error | rate_limited | 키의 분당 요청 수 또는 토큰 수 한도를 넘었습니다. | Retry-After까지 기다리세요. 제한을 보세요. |
| 429 | rate_limit_error | provider_overloaded | 이 모델의 모든 채널이 지금 가득 찼습니다. 재시도와 장애 조치를 보세요. | 백오프 후 재시도하거나 모델을 바꾸세요. |
| 501 | invalid_request_error | not_supported | 제공하지 않는 엔드포인트입니다: 배치(/v1/batches), 파인튜닝(/v1/fine_tuning), /v1/files 아래 파일의 목록 조회·읽기·삭제. | 재시도하지 마세요. 각 요청을 일반 엔드포인트로 보내세요. |
| 503 | api_error | temporarily_unavailable | 게이트웨이가 요청을 받는 데 필요한 정보(예: 조직의 잔액)를 그 순간 확인하지 못했습니다. 아무것도 실행되지 않았고 청구도 없습니다. | Retry-After초 후에 재시도하세요. |
| 5xx | api_error | provider_error | 게이트웨이 자체의 재시도와 장애 조치 후에도 모델이 실패했습니다. 청구 없음. | 백오프 후 재시도하세요. 계속되면 모델을 바꾸세요. |
| 408 | api_error | timeout | 스트림이 시작된 뒤 120초 동안 모델에서 아무것도 받지 못했습니다. | 백오프 후 재시도하세요. |
| 504 | api_error | timeout | 요청이 240초 제한 시간을 넘었습니다. | 응답을 스트리밍하거나, 요청을 줄이거나, 비동기 엔드포인트를 쓰세요. |
이 표에 없는 것은 모두 api_error입니다. 새 코드가 추가될 수 있지만, 기존 코드의 이름을 바꾸거나 없애지는 않습니다.
요청 ID
모델 요청에 대한 응답에는 x-oceantoken-request-id 헤더가 붙습니다. 오류와 함께 기록하고, 문의할 때 알려 주세요. 키 때문에 거부된 요청(401)은 모델에 도달하지 않았으므로 이 ID가 없습니다.
성공한 응답은 본문에도 id가 있고, 스트림의 모든 청크가 같은 id를 가집니다. 이것이 콘솔 Logs에 요청의 상태·지연 시간·비용과 함께 표시되는 Request ID이며, GET /v1/usage/{request_id}로 조회하는 id입니다.
재시도
오류가 전달되기 전에 게이트웨이는 이미 재시도했고, 모델에 다른 채널이 있으면 요청을 그쪽으로 옮겼습니다. 재시도와 장애 조치에서 게이트웨이가 처리하는 것과 사용자에게 맡기는 것을 설명합니다.
재시도는 다음 경우에만 하세요: 429, 501을 제외한 모든 5xx, timeout 오류, 네트워크 실패(연결 끊김, 클라이언트 측 시간 초과). 400번대 전부와 501 not_supported는 요청이나 계정이 바뀌기 전까지 같은 방식으로 다시 실패합니다.
지터를 넣은 지수 백오프를 쓰세요. 약 1초에서 시작해 약 30초를 상한으로 합니다. 응답에 Retry-After(초 단위)가 있으면 최소한 그만큼 기다리세요.
Retry-After가 반드시 붙는 것은 409 idempotency_key_in_progress뿐입니다. 429나 503에는 없을 수 있으니 그때는 자체 백오프를 쓰세요.
# Retry only what can succeed on a second try: 429, 408 (timeout), every 5xx
# except 501 (an endpoint that is not offered) and network failures,
# with exponential backoff and jitter, honouring Retry-After when it is sent.
import os
import random
import time
from openai import APIConnectionError, APIStatusError, OpenAI
client = OpenAI(
base_url=os.getenv("OCEANTOKEN_BASE_URL", "https://api.oceantoken.ai/v1"),
api_key=os.environ["OCEANTOKEN_API_KEY"],
max_retries=0, # the loop below does the retrying
)
def with_retries(call, attempts=5):
for attempt in range(attempts):
try:
return call()
except APIConnectionError:
if attempt == attempts - 1:
raise
delay = None # network failure: back off and try again
except APIStatusError as e:
retryable = e.status_code in (408, 429) or (e.status_code >= 500 and e.status_code != 501)
if not retryable or attempt == attempts - 1:
raise
retry_after = e.response.headers.get("retry-after")
delay = float(retry_after) if retry_after else None
time.sleep(delay if delay is not None else min(30, 2**attempt) + random.random())
models = with_retries(lambda: client.models.list())
print(len(models.data), "models")
비용이 드는 요청 재시도하기
이미지, 동영상, 음성 합성 요청은 실행될 때 청구됩니다. 네트워크 시간 초과 후 재시도하면 원래는 두 번 실행됩니다. POST /v1/images/generations, /v1/images/edits, /v1/videos, /v1/audio/speech에 Idempotency-Key 헤더(최대 255자의 고유한 문자열, 예: UUID)를 보내세요:
- 첫 요청은 평소대로 실행되고, 그 응답은 24시간 보관됩니다.
- 같은 키와 같은 본문으로 재시도하면 그 응답이
Idempotency-Replayed: true헤더와 함께 돌아옵니다. 아무것도 실행되지 않고 청구도 없습니다. - 같은 키에 다른 본문이면
409 idempotency_key_reused로 거부됩니다. - 첫 요청이 아직 실행 중일 때 재시도하면
Retry-After와 함께409 idempotency_key_in_progress를 받습니다. 5xx응답은 보관되지 않으므로, 제공사 장애 후의 재시도는 다시 실행됩니다.
키의 범위는 API 키별입니다. 다른 엔드포인트에서는 이 헤더를 무시하며, 위 엔드포인트에서도 헤더가 없는 요청은 이전과 똑같이 동작합니다. 8 MB를 넘는 응답은 보관되지 않습니다. 요청은 실행되며, 재생만 적용되지 않습니다.
스트리밍 중 오류
"stream": true일 때 첫 토큰 전에 생긴 오류는 위의 상태를 가진 일반 JSON 오류 응답입니다. 스트림이 시작된 뒤 생긴 오류는 같은 error 객체를 담은 마지막 data: 청크로 오고, 스트림이 끝납니다.
연결이 끊기거나, 사용자가 연결을 끊거나, 오류로 잘려 일찍 끝난 스트림은 모델의 일반 가격으로 다음을 청구합니다:
- 프롬프트 전체
- 그 시점까지 스트리밍된 출력. 추론 모델이 스트리밍한 추론은 답변 텍스트가 아직 오지 않았더라도 출력으로 칩니다.
스트림이 멈춘 뒤 모델이 계속 생성했을 수 있는 출력은 청구하지 않습니다. 출력을 하나도 내기 전에 끝난 스트림은 전혀 청구하지 않습니다.
잔액 소진
잔액이 $0 이하이면 유료 모델에 대한 모든 요청이 400 insufficient_credits로 거부됩니다. 아무것도 실행되지 않고 청구도 없습니다. 크레딧을 충전하고 재시도하세요. 모델 ID가 :free로 끝나는 모델은 잔액이 $0인 경우에도 계속 사용할 수 있습니다.
잔액이 $0보다 크지만 어떤 요청의 최대 비용보다 적으면, 그 요청만 400 request_exceeds_balance로 거부되고 더 작은 요청은 계속 실행됩니다.
메시지에는 그 요청의 최대 비용과 잔액이 나옵니다.
max_tokens를 낮추거나 입력을 줄여 잔액 안에서 실행하거나, 크레딧을 충전하세요.
OpenAI에서 마이그레이션
오류 본문은 OpenAI와 같은 error 객체입니다.
상태나 코드가 다른 경우는 두 가지입니다:
| 경우 | OpenAI | OceanToken |
|---|---|---|
| 잔액 소진 | 429 insufficient_quota | 400 insufficient_credits |
| 알 수 없는 모델 | 404 model_not_found | 400 model_not_found |
code 값은 모두 자체 값이며 코드에 목록이 있습니다.
몇몇 상태는 자체 의미를 가집니다: 모델이 가득 찼으면 429 provider_overloaded(OpenAI는 503), 408 timeout은 조용해진 스트림, 409는 멱등 충돌, 501 not_supported는 제공하지 않는 엔드포인트입니다.
코드가 429 insufficient_quota를 잔액 부족으로 읽는다면 대신 400 insufficient_credits를 확인하세요. 429가 아니므로 재시도 루프가 그 위에서 헛돌지 않습니다.
알 수 없는 모델은 400이므로 404를 기다리는 핸들러는 이를 보지 못합니다. code로 분기하면 양쪽 모두에서 동작합니다.
콘텐츠 정책
안전 검사에 걸린 요청은 400 content_filtered로 거부됩니다. 검사는 모델을 호출하기 전에 이루어지므로 청구되지 않습니다.
실패한 요청 찾기
모든 실패는 콘솔의 Logs에 상태와 응답 본문과 함께 기록됩니다. 시간과 모델로 찾고, 문의할 때 그 x-oceantoken-request-id를 알려 주세요.