오류와 디버깅
상태 코드와 `error_type` 의 짝
/api/v1 의 오류 본문은 경로와 무관하게 언제나 같은 형상입니다.
{
"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 | 발생 조건 |
|---|---|---|
| 400 | invalid_request | 스키마 위반. 본문이 JSON 객체가 아님, messages 누락, role 없는 메시지, /generation 의 id 누락 |
| 400 | model_not_found | 모르는 모델 id |
| 401 | invalid_api_key | 키 없음·폐기됨·Authorization 헤더 없음·키 종류 불일치 |
| 401 | expired_api_key | expires_at 이 지난 키 |
| 402 | insufficient_credits | 잔액이 부채 허용선 아래이거나 키가 잔액 부족으로 정지됨 |
| 402 | key_limit_exceeded | 키에 건 사용 한도를 초과 |
| 403 | key_suspended | 관리자가 정지한 키. 충전으로 풀리지 않음 |
| 404 | no_endpoints_found | 후보 체인이 비었음 |
| 404 | not_supported | 구현하지 않는 경로, 없는 모델의 endpoints 조회, 없거나 남의 generation |
| 408 | timeout | 업스트림이 제한 시간 안에 응답하지 않음 |
| 429 | rate_limit_exceeded | 요청 빈도 제한 초과 |
| 502 | provider_error | 업스트림 오류로 후보 체인이 전부 실패 |
| 503 | model_loading | 로컬 GPU 슬롯을 기동하는 중이거나 예산 안에 못 올림 |
| 500 | server | 그 밖의 서버 오류. 상세는 서버 로그에만 남습니다 |
metadata 에 함께 오는 값
error_type | 추가 필드 |
|---|---|
model_not_found | model — 못 찾은 id |
insufficient_credits | balance_krw — 현재 잔액(원) |
key_limit_exceeded | limit_krw, usage_krw |
no_endpoints_found | models — 요청했던 모델 목록 |
not_supported | path — 없는 경로(미구현 경로 catch-all 인 경우) |
timeout | timeout_sec |
provider_error | provider_code — 업스트림이 준 상태 코드 |
model_loading | retry_after_sec |
retry_after_sec 이 있는 응답에는 표준 Retry-After 헤더도 함께 붙습니다. metadata 를 파싱하지 않아도 재시도 간격을 알 수 있게 하려는 것입니다.
오류 처리 코드
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{
"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 /generation의 provider_responses 에 남습니다. 실패한 요청도 사용 기록에 남고, 비용은 0원이며 원장에 기입되지 않습니다.
디버깅에 쓸 식별자
문제를 좁힐 때 필요한 값은 응답 헤더에 있습니다.
| 헤더 | 쓰임 |
|---|---|
X-MyIP-Request-Id | 한 요청의 모든 후보 시도를 묶는 id. 오류 응답에도 붙습니다 |
X-MyIP-Generation-Id | /generation 조회 키. 토큰·비용·시도 내역을 볼 수 있습니다 |
X-MyIP-Model / X-MyIP-Provider | 실제로 응답한 후보 |
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 를 받았다면
구현하지 않는 경로를 부른 것입니다. 어떤 경로인지 본문이 알려줍니다.
{
"error": {
"code": 404,
"message": "Endpoint not supported on MyIP OpenRouter",
"metadata": { "error_type": "not_supported", "path": "/api/v1/embeddings" }
}
}전체 목록은 지원하지 않는 엔드포인트에 있습니다.
마지막 수정 2026. 9. 5.