본문으로 건너뛰기
API 사용법오류 처리

오류 처리

상태 코드와 오류 본문의 형태, 재시도 규칙.

오류 본문의 모양

게이트웨이가 상위 모델로 보내기 전에 직접 막은 요청은 429 와 함께 아래 본문으로 돌아옵니다. 코드 하나만 오는 게 아니라 사람이 읽을 수 있는 한 문장이 같이 오기 때문에, 최종 사용자에게 그대로 보여 주면 됩니다.

응답 본문
type GateReason = "insufficient_balance" | "budget_exceeded" | "rate_limited";

interface GateBody {
  error: GateReason;    // 코드 문자열 — === 로 비교하면 된다
  message: string;      // 사람이 읽는 평문 한 문장 (HTML 아님)
  topup_url?: string;   // 충전으로 풀리는 경우에만 — insufficient_balance 뿐이다
}
  • error 는 코드 문자열입니다 — === "insufficient_balance" 처럼 비교하세요. 문구(message)로 분기하면 안내 문장을 다듬는 순간 깨집니다.
  • message 는 평문입니다. HTML 이 아니라서 터미널·로그에 찍어도 태그가 섞이지 않고, 화면에 넣을 때는 textContent 를 쓰면 됩니다.
  • topup_url 은 URL 문자열입니다. 버튼으로 만들지 링크로 둘지는 앱이 정합니다.

message 를 innerHTML 로 꽂지 마세요

응답이 어디로 갈지는 게이트웨이가 모릅니다. 평문으로 주는 이유가 그것이고, innerHTML 로 넣으면 게이트웨이 응답이 그대로 DOM 이 되는 통로가 열립니다.

화면에 넣을 때
// 게이트웨이 응답을 화면에 넣을 때
el.textContent = e.message;   // ✅ 평문 그대로 — 태그가 섞일 일이 없다
el.innerHTML = e.message;     // ❌ 쓰지 말 것 — 응답을 DOM 으로 만드는 XSS 통로가 된다

429 세 가지를 구분하기

같은 429 라도 사용자가 할 수 있는 일이 전혀 다릅니다. 셋을 뭉뚱그리면 충전하면 풀리는 상황에도 "잠시 후 다시 시도"만 띄우게 됩니다.

  • insufficient_balance — 잔액이 0원입니다. 충전하면 즉시 재개되고, 이 코드에만 topup_url 이 붙습니다. 키를 다시 발급할 필요는 없습니다. 여기까지 오기 전에 미리 알고 싶다면 GET /v1/balance 로 잔액을 확인할 수 있습니다.
  • budget_exceeded — 키에 걸린 월 예산을 다 썼습니다. 충전으로는 풀리지 않아 topup_url 이 없습니다. 관리자에게 문의해야 합니다.
  • rate_limited — 분당 요청 한도입니다. 잠시 기다렸다 다시 보내면 됩니다.
잔액 소진 — topup_url 이 붙는다
HTTP/1.1 429 Too Many Requests
Content-Type: application/json

{
  "error": "insufficient_balance",
  "message": "잔액이 모두 소진되었습니다. 사용량 패키지를 구매하면 즉시 다시 이용할 수 있습니다.",
  "topup_url": "https://ai.cubion.kr/app/billing"
}
월 예산 소진 — topup_url 이 없다
HTTP/1.1 429 Too Many Requests
Content-Type: application/json

{
  "error": "budget_exceeded",
  "message": "이 키에 설정된 월 예산을 모두 사용했습니다. 관리자에게 문의해 주세요."
}
// topup_url 이 없다 — 충전해도 풀리지 않는 상황이라 일부러 안 보낸다.

충전 버튼은 topup_url 이 있을 때만

topup_url 유무로 분기하면 세 가지를 따로 외울 필요가 없습니다. 값이 있으면 충전 버튼을 띄우고, 없으면 message 만 보여 주세요. 잔액이 어떻게 빠지는지는 요금·잔액 에 있습니다.

재시도 규칙

429 응답에는 기다릴 시간(초)을 알려 주는 Retry-After 헤더가 붙습니다. 상위 모델이 내린 429 를 전달할 때도 상위가 준 값을 그대로 넘깁니다. 드물게 상위가 값을 주지 않는 경우가 있으니 Number(res.headers.get("Retry-After") ?? 5) 처럼 기본값을 두고 읽으세요.

import os, time, requests

URL = "https://ai.cubion.kr/v1/chat/completions"
HEAD = {
    "Authorization": f"Bearer {os.environ['GATEWAY_KEY']}",
    "Content-Type": "application/json",
}

def call_gateway(body, retried=False):
    res = requests.post(URL, headers=HEAD, json=body)
    if res.ok:
        return res.json()

    try:
        e = res.json()
    except ValueError:
        e = {}

    if e.get("error") == "insufficient_balance":
        show_notice(e.get("message", ""))          # 평문이라 그대로 띄워도 안전하다
        if e.get("topup_url"):                     # 이 코드에만 붙는다
            show_topup_button(e["topup_url"])
        return None

    if e.get("error") == "budget_exceeded":
        show_notice(e.get("message", ""))          # 충전으로는 안 풀린다 — 충전 버튼을 띄우지 말 것
        return None

    if e.get("error") == "rate_limited" and not retried:
        # Retry-After 가 없는 경로도 있으므로 기본값을 둔다. 재시도는 한 번만.
        wait = int(res.headers.get("Retry-After") or 5)
        time.sleep(wait)
        return call_gateway(body, retried=True)

    raise RuntimeError(e.get("message") or e.get("error") or f"HTTP {res.status_code}")
  • 재시도는 한 번만. 막힌 채로 계속 두드리면 한도가 풀리지 않고, 그대로 재시도 폭주가 됩니다.
  • insufficient_balance · budget_exceeded 는 재시도 대상이 아닙니다. 기다린다고 풀리지 않으니 곧바로 사용자에게 알리세요.
  • 401 · 403 · 400 도 재시도하지 마세요. 요청이나 키를 고쳐야 하는 오류입니다.

요청 추적 헤더

모든 /v1 응답(오류 포함)에 아래 헤더가 붙습니다. 문의할 때 X-Request-Id 하나만 알려 주시면 해당 요청의 기록을 바로 찾을 수 있습니다.

헤더뜻
X-Request-Id게이트웨이가 요청마다 만든 ID(UUID). 우리 쪽 요청 기록과 같은 값입니다.
X-Client-Request-Id요청에 이 헤더를 보내면 기록에 함께 남고 응답에도 그대로 돌아옵니다. 문서 ID·작업 ID 처럼 여러분 쪽 식별자를 넣으세요(영문·숫자·기호 128자까지).
X-Gateway-Attempts상위 호출 횟수. 1 보다 크면 다른 키·모델로 폴백했거나 한도 때문에 재시도했다는 뜻입니다.
X-Gateway-Model실제로 처리한 모델. feature: 이름으로 불렀거나 폴백 모델로 넘어갔을 때 확인하세요.
예
curl -i https://ai.cubion.kr/v1/chat/completions \
  -H "Authorization: Bearer $GATEWAY_KEY" \
  -H "X-Client-Request-Id: wi-2026-00412" \
  -H "Content-Type: application/json" \
  -d '{"model":"gpt-4.1","messages":[{"role":"user","content":"안녕"}]}'

HTTP/1.1 200 OK
x-request-id: 3f0c2a4e-8a51-4a6f-9b3e-0d2f7c1e9a10
x-client-request-id: wi-2026-00412
x-gateway-attempts: 1
x-gateway-model: gpt-4.1

상태 코드

게이트웨이가 내보내는 오류는 아래가 전부입니다.

상태error언제 / 어떻게
401unauthorized키가 틀렸거나 폐기·비활성 상태입니다. API 키 에서 다시 발급하세요.
403scope_forbidden키 권한 밖의 엔드포인트입니다({"error":"scope_forbidden","need":"ai"}). 포털에서 발급한 키는 /v1/* AI 호출만 쓸 수 있습니다.
403model_not_allowed이 키로 쓸 수 없는 모델입니다. 응답에 model 이 함께 옵니다 — 모델 목록 이 내려주는 범위에서 고르세요.
429insufficient_balance잔액이 0원입니다. topup_url 로 보내 충전하면 즉시 재개됩니다.
429budget_exceeded키에 걸린 월 예산을 다 썼습니다. 충전으로는 풀리지 않아 topup_url 이 없습니다 — 관리자에게 문의해 주세요.
429rate_limited분당 요청 한도를 넘었습니다. Retry-After 초만큼(없으면 몇 초) 기다린 뒤 한 번만 재시도하세요.
400invalid_json · missing model본문이 JSON 이 아니거나 model 이 비었습니다.
400input_too_long · unsupported_format음성 합성 에서 입력 길이나 response_format 이 모델이 받는 범위를 넘었습니다. 응답에 허용치(max · supported)가 함께 옵니다.
502upstream_unreachable · no_upstream_for_model상위 모델 쪽 장애이거나 그 모델을 받아 줄 곳이 없습니다.
502upstream_auth_failed게이트웨이 뒤의 제공사 키가 거절됐습니다(운영 쪽 문제). 다른 키가 있으면 자동으로 넘어가고, 모두 거절되면 이 코드가 옵니다. 운영자에게 자동으로 알림이 갑니다 — 몇 분 뒤 다시 시도하세요.
503upstream_quota_exhausted게이트웨이 뒤의 AI 제공사 쪽 이용 한도가 소진됐습니다. 여러분의 잔액과는 무관하고, 운영자에게 자동으로 알림이 갔습니다. 운영자가 조치하기 전에는 다시 불러도 풀리지 않습니다.

400 은 요청을 고쳐야 풀립니다. 502 는 잠시 후 다시 부르면 대개 풀리지만, 계속 같은 모델에서만 난다면 다른 모델로 바꿔 보세요. 503 upstream_quota_exhausted 는 짧은 간격으로 재시도하지 마세요 — 몇 분 단위로 다시 확인하거나, 다른 제공사의 모델로 바꾸면 됩니다.