오류와 디버깅

상태 코드와 `error_type` 의 짝

/api/v1 의 오류 본문은 경로와 무관하게 언제나 같은 형상입니다.

json
{
  "error": {
    "code": 404,
    "message": "요청을 처리할 수 있는 엔드포인트가 없습니다.",
    "metadata": { "error_type": "no_endpoints_found", "models": ["google/gemma-4-26b-a4b"] }
  }
}
  • error.code항상 HTTP 상태 코드와 같습니다. 둘이 어긋나는 경우는 없습니다.
  • error.message 는 사람이 읽는 문장입니다. 문구는 예고 없이 바뀔 수 있으니 분기 판단에 쓰지 마세요.
  • error.metadata.error_type 이 기계가 읽는 값입니다. 분기는 여기에 거세요.

상태 코드와 error_type

상태error_type발생 조건
400invalid_request스키마 위반. 본문이 JSON 객체가 아님, messages 누락, role 없는 메시지, /generationid 누락
400model_not_found모르는 모델 id
401invalid_api_key키 없음·폐기됨·Authorization 헤더 없음·키 종류 불일치
401expired_api_keyexpires_at 이 지난 키
402insufficient_credits잔액이 부채 허용선 아래이거나 키가 잔액 부족으로 정지됨
402key_limit_exceeded키에 건 사용 한도를 초과
403key_suspended관리자가 정지한 키. 충전으로 풀리지 않음
404no_endpoints_found후보 체인이 비었음
404not_supported구현하지 않는 경로, 없는 모델의 endpoints 조회, 없거나 남의 generation
408timeout업스트림이 제한 시간 안에 응답하지 않음
429rate_limit_exceeded요청 빈도 제한 초과
502provider_error업스트림 오류로 후보 체인이 전부 실패
503model_loading로컬 GPU 슬롯을 기동하는 중이거나 예산 안에 못 올림
500server그 밖의 서버 오류. 상세는 서버 로그에만 남습니다

metadata 에 함께 오는 값

error_type추가 필드
model_not_foundmodel — 못 찾은 id
insufficient_creditsbalance_krw — 현재 잔액(원)
key_limit_exceededlimit_krw, usage_krw
no_endpoints_foundmodels — 요청했던 모델 목록
not_supportedpath — 없는 경로(미구현 경로 catch-all 인 경우)
timeouttimeout_sec
provider_errorprovider_code — 업스트림이 준 상태 코드
model_loadingretry_after_sec

retry_after_sec 이 있는 응답에는 표준 Retry-After 헤더도 함께 붙습니다. metadata 를 파싱하지 않아도 재시도 간격을 알 수 있게 하려는 것입니다.

오류 처리 코드

python
import os, time, requests

def call(payload, attempts=3):
    for attempt in range(attempts):
        response = requests.post(
            "https://openrouter.myip.co.kr/api/v1/chat/completions",
            headers={"Authorization": f"Bearer {os.environ['MYIP_API_KEY']}"},
            json=payload,
            timeout=650,
        )
        if response.ok:
            return response.json()

        body = response.json()
        error_type = body["error"]["metadata"]["error_type"]

        # 충전이나 관리자 조치가 필요한 것들. 재시도해봐야 소용없다.
        if error_type in {"invalid_api_key", "expired_api_key", "key_suspended",
                          "insufficient_credits", "key_limit_exceeded",
                          "invalid_request", "model_not_found", "not_supported"}:
            raise RuntimeError(f"{response.status_code} {error_type}: {body['error']['message']}")

        # 기다렸다 다시 하면 되는 것들.
        if error_type in {"model_loading", "rate_limit_exceeded", "timeout", "provider_error"}:
            wait = int(response.headers.get("Retry-After", 2 ** attempt))
            time.sleep(wait)
            continue

        raise RuntimeError(f"{response.status_code} {error_type}")

    raise RuntimeError("재시도 횟수를 모두 썼습니다")

스트리밍 중의 오류

스트림이 시작된 뒤(첫 바이트 전송 후)에는 상태 코드를 바꿀 수 없습니다. 그래서 오류가 SSE 청크 안에 실려 옵니다.

data: {"id":"gen-…","object":"chat.completion.chunk","model":"google/gemma-4-26b-a4b","provider":"MyIP Local GPU","error":{"code":502,"message":"upstream disconnected","metadata":{"error_type":"provider_error"}},"choices":[{"index":0,"delta":{"content":""},"finish_reason":"error"}]}

data: [DONE]

HTTP 200 이어도 청크의 error 필드를 확인해야 합니다. 자세한 내용은 스트리밍에 있습니다.

503 model_loading 이 뜨는 이유

이 서비스는 모델을 우리 GPU 팜에서 직접 서빙합니다. 요청 시점에 그 모델의 GPU 슬롯이 내려가 있으면 게이트웨이가 슬롯을 깨우고 기다립니다. 기동 예산(모델별로 다르며, 로컬 슬롯 설정에서 옵니다) 안에 못 올라오고 대신 쓸 다른 후보도 없으면 503 model_loading 입니다.

HTTP/1.1 503 Service Unavailable
Retry-After: 300
X-MyIP-Currency: KRW
json
{
  "error": {
    "code": 503,
    "message": "로컬 모델을 기동하는 중입니다. 잠시 후 다시 시도하세요.",
    "metadata": { "error_type": "model_loading", "retry_after_sec": 300 }
  }
}

Retry-After 만큼 기다렸다 다시 부르면 대개 성공합니다. 기동은 이미 시작되어 있습니다. retry_after_sec 값은 모델마다 다르니 하드코딩하지 말고 헤더를 읽으세요. 배경은 로컬 GPU 모델에 있습니다.

502 provider_error 와 후보 체인

한 요청은 여러 후보를 순서대로 시도할 수 있습니다. 첫 바이트를 보내기 전이라면 실패한 후보를 버리고 다음으로 넘어갑니다. 체인의 모든 후보가 실패했을 때만 502 가 나갑니다. metadata.provider_code 에 마지막 후보가 준 상태 코드가 들어 있습니다.

무엇을 몇 번 시도했는지는 GET /generationprovider_responses 에 남습니다. 실패한 요청도 사용 기록에 남고, 비용은 0원이며 원장에 기입되지 않습니다.

디버깅에 쓸 식별자

문제를 좁힐 때 필요한 값은 응답 헤더에 있습니다.

헤더쓰임
X-MyIP-Request-Id한 요청의 모든 후보 시도를 묶는 id. 오류 응답에도 붙습니다
X-MyIP-Generation-Id/generation 조회 키. 토큰·비용·시도 내역을 볼 수 있습니다
X-MyIP-Model / X-MyIP-Provider실제로 응답한 후보
bash
curl -i https://openrouter.myip.co.kr/api/v1/chat/completions \
  -H "Authorization: Bearer $MYIP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"google/gemma-4-26b-a4b","messages":[{"role":"user","content":"hi"}]}' \
  | grep -i '^x-myip'

404 not_supported 를 받았다면

구현하지 않는 경로를 부른 것입니다. 어떤 경로인지 본문이 알려줍니다.

json
{
  "error": {
    "code": 404,
    "message": "Endpoint not supported on MyIP OpenRouter",
    "metadata": { "error_type": "not_supported", "path": "/api/v1/embeddings" }
  }
}

전체 목록은 지원하지 않는 엔드포인트에 있습니다.

마지막 수정 2026. 9. 5.