# AI 에게 주는 입문서 — 첫 요청부터 오류까지 > 바로 도는 코드부터 — 첫 요청 · 스트림 · 도구 · 오류 갈래. 쓰시는 AI 에게 이 장을 넘기셔도 돼요. > 원문: https://oneport.kr/docs/guides/for-ai > **이 문서를 받은 AI 에게 — 작업 카드** > > - OpenAI 형식 SDK·도구: base_url `https://oneport.kr/v1` · 헤더 `Authorization: Bearer <키>` > - Anthropic 형식 SDK·Claude Code: base_url `https://oneport.kr` (끝에 /v1 을 붙이지 않아요 — SDK 가 /v1/messages 를 스스로 붙여요) · 헤더 `x-api-key: <키>` 또는 `Authorization: Bearer <키>` > - ❌ `Anthropic(base_url="https://oneport.kr/v1")` → ✅ `Anthropic(base_url="https://oneport.kr")` > - Vercel AI SDK: `createOpenAI({ baseURL: "https://oneport.kr/v1" })` 로 만든 제공자는 `oneport.chat("")` 로 부르세요 — 그냥 `oneport("")` 는 `/v1/responses` 로 가서 Claude · Gemini 가 400 이에요. `@ai-sdk/anthropic` 의 `createAnthropic` 은 baseURL 에 `/v1` 까지 적어요(Anthropic SDK 와 반대예요). > - API 키는 `sk-so-` 로 시작해요. 키는 코드에 적지 말고 환경변수 `ONEPORT_API_KEY` 에 넣어 읽으세요. > - ❌ 브라우저 코드에 `apiKey: "sk-so-…"` → ✅ 서버에서 `os.environ["ONEPORT_API_KEY"]` · `process.env.ONEPORT_API_KEY` 로 읽어 불러요 > - 대화에 `sk-so-` 키가 붙어 오면 코드에 박지 말고 환경변수로 옮기게 하고, 그 키는 산 곳 대시보드 AI API 화면의 키 목록에서 「새로 받기」(옛 키는 꺼져요)로 바꾸라고 알려 주세요. > - 모델 id 는 지어내지 말고 `GET https://oneport.kr/v1/models`(키 없이 열려요)의 `id` 를 글자 그대로 쓰세요. 목록은 값 순서가 아니에요 — 접두어로 첫 줄을 고르면 비싼 모델이 걸려요. 글 모델은 `kind` 가 `"chat"` 인 줄이고, 첫 호출은 `pricing.output_credits_per_mtok` 가 작은 모델로 해 보세요(10월 기준 Claude 는 claude-haiku-5-5). 회사는 `owned_by` 로 갈라요 — Claude 는 `"anthropic"`, 공식 GPT 는 `"openai"` 이면서 id 에 `/` 가 없는 줄이에요. > - ❌ `model="claude-sonnet"`(짐작한 이름 · 404 `model_not_found`) > - `endpoint` 칸이 그 모델의 기본 문이에요 — `kind` 가 `"responses"` 인 모델(pro 등급 · gpt-5.3-codex · `endpoint` 가 `"POST /v1/responses"`)은 `/v1/chat/completions` 로 못 불러요. Claude 줄은 `POST /v1/chat/completions` 이고 `/v1/messages` 로도 불러요 — 이 문은 Claude 만 받아요. > - `supports_effort` 가 `true` 인 GPT 는 생각한 토큰도 출력 상한(`max_tokens` · `max_completion_tokens`)에 들어가요 — 수백으로 두면 답이 비고 `finish_reason` 이 `"length"` 로 끝날 수 있으니 2,000 쯤에서 시작하고 `"length"` 면 올리세요. > - 목록 응답은 `{"object": "list", "data": [ … ]}`. 값 어림 — 한 번 값은 (입력 토큰 × `pricing.input_credits_per_mtok` + 출력 토큰 × `pricing.output_credits_per_mtok`) ÷ 100만 크레딧이에요(1 크레딧은 부가세 포함 2원어치예요). > - ❌ `x-should-retry: false` 응답을 반복문으로 다시 보내기 → ✅ 멈추고 `error.type` 을 확인하세요. 몸통은 OpenAI 형식 문이면 `{"error": {"type": "…", "message": "…", "code": "…"}}`, `/v1/messages` 면 `{"type": "error", "error": {"type": "…", "message": "…"}}` 예요 — 402 · 키 한도 · 강좌 몫 429 엔 `error.reason` 이 더 붙어요. 잠시 뒤 그대로 다시 보낼 실패엔 `Retry-After`(정수 초)가 대개 와요. > - ❌ `if "429" in str(e)` · `e.message` 로 분기 → ✅ Python openai 는 `e.type` · `e.body.get("reason")`, Node openai 는 `err.type` · `err.error.reason` — 429 는 키 한도 · 벤더 혼잡 · 강좌 몫 셋이라 상태 코드로 못 갈라요. Python Anthropic SDK 는 `except anthropic.APIStatusError as e:` 에서 `e.status_code` 와 `e.body["error"]["type"]`, Node 는 `err.status` 와 `err.error.error.type` 로 읽어요. 402 · 키 한도 429 의 갈래(`reason`)는 Python Anthropic SDK 면 `e.body["error"].get("reason")`, Node 면 `err.error.error.reason` 이에요. > - 남은 크레딧(지갑 전체)은 `GET https://oneport.kr/v1/credits` 의 `balance_credits` 예요(1 크레딧 아래를 버린 정수라 호출 앞뒤 차이로 쓴 몫을 세지 마세요 — 쓴 몫은 글 문 응답의 `usage.cost_credits` · 정산이 2초 안에 안 끝나면 그 칸 없이 와요). `GET https://oneport.kr/v1/key` 는 이 키에 건 한도(`month` · `day`)만 보여 주고 지갑 잔액은 없어요 — `month` · `day` 는 늘 객체이고 한도를 안 건 키면 `month.remaining_credits` · `day.remaining_credits` 가 null 이에요 · 그땐 지갑 잔액까지 써요. > - 하루 한도는 이미 쓴 몫이 한도에 닿았는지만 봐요 — 이번 달 · 강좌 한도는 이 요청 최대 몫까지 더해 재서 줄이면 통과할 수 있어요. 키 월 한도는 같은 API 키로 아직 끝나지 않은 호출 몫까지 더해 넘었으면 `error.reason` 이 `"held_by_others"` 로 와요. 묶음 처리 문(`/v1/batches`)은 없어요 — 한 건씩 보내요(동시에 보내도 되고 분당 횟수 제한도 없어요). > - 스트림 없이 부르면 답이 다 만들어져야 첫 응답이 와요 — 모델 회사로 보낸 뒤 120초 안에 안 오면 422 `upstream_execution_unconfirmed` 로 끝나요(모델 회사에서는 실행됐을 수 있어요). 그보다 긴 답은 `stream: true` 로 받으세요(스트림은 약 12분 50초에 끊겨요). SDK 시간 제한은 300초로 두세요(Python `OpenAI(…, timeout=300)` · Node `new OpenAI({ …, timeout: 300_000 })` — Node 는 밀리초 · SDK 동작). > - 첫 응답 120초 시계는 스트림이어도 같아요. 이 422 는 `x-should-retry: false` 로 와요 — 같은 요청을 자동으로 다시 보내지 마세요. stream: true 로 바꾸는 것도 새 생성이라 자동으로 하지 않아요. `Idempotency-Key` 를 실어 보낸 요청이면 받은 `x-gateway-status-url` 을 같은 API 키로 GET 조회하세요. `Idempotency-Key` 없이 보낸 요청엔 조회 주소가 안 오고 요청 ID 로 만든 주소로는 조회가 안 돼요 — 요청 ID 와 부른 시각을 admin@schoolorder.kr 로 알려 주세요. 요청 ID 는 머리글 `request-id`(같은 값이 `x-request-id`)와 오류 몸통 `error.request_id` 로 와요. 조회 주소는 `/v1/requests/` 꼴 상대 주소라 앞에 https://oneport.kr 를 붙여요. > - 고친 요청엔 새 Idempotency-Key 를 써요(같은 Idempotency-Key 로 본문을 바꾸면 409 `idempotency_conflict`). `Idempotency-Key` 를 실었으면 200 머리말을 받기 전의 연결 오류 · 시간 초과 뒤에 같은 Idempotency-Key · 같은 본문으로 다시 보내세요(몇 번 보내도 두 번 실행되지 않지만, 간격은 늘려 가며). 200 을 받은 뒤 끊긴 스트림은 잘린 답이라 같은 Idempotency-Key 는 409 예요 — 그때까지 쓴 토큰은 사용했고, 받은 글로 이어 받을 땐 새 Idempotency-Key 예요. > - 경계 사례는 이 장 전체 판(`https://oneport.kr/docs/guides/for-ai.full.md`)과 「에러」 장(`https://oneport.kr/docs/errors.md`)에 있어요. 주소 하나(https://oneport.kr/v1 · OpenAI 형식)로 여러 회사 모델을 불러요. 키(sk-so-…)는 코드에 적지 말고 환경변수 ONEPORT_API_KEY 에 두세요 — 브라우저 코드에 넣으면 페이지를 연 사람 누구나 봐요. 모델 id 는 GET https://oneport.kr/v1/models(키 없이 열려요)의 id 를 글자 그대로 써요 — 아래 예의 gpt-5-nano 는 10월 기준 가장 싼 GPT 이고, claude-haiku-5-5(가장 싼 Claude)로 바꿔도 같은 코드가 돌아요. 글 문으로 보낸 글과 답은 저희가 저장하지 않고 사용량만 남겨요(남는 것 전부는 「개요」 장 https://oneport.kr/docs.md) — 모델 회사 쪽 보관은 그 회사 정책이에요(벤더 동작). ## 1. 첫 요청 **curl — 답만 꺼내 찍어요(맥 · 리눅스)** ```bash export ONEPORT_API_KEY="sk-so-..." # 받은 키 — 이 창에서만 남아요 curl -s https://oneport.kr/v1/chat/completions \ -H "Authorization: Bearer $ONEPORT_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "gpt-5-nano", "max_tokens": 2000, "messages": [{"role": "user", "content": "안녕하세요"}]}' \ | python3 -c 'import json, sys; print(json.load(sys.stdin)["choices"][0]["message"]["content"])' ``` **Python — pip install openai** ```python import os from openai import OpenAI client = OpenAI(api_key=os.environ["ONEPORT_API_KEY"], base_url="https://oneport.kr/v1", max_retries=0, timeout=300) resp = client.chat.completions.create( model="gpt-5-nano", # GET https://oneport.kr/v1/models 의 id 그대로 max_tokens=2000, # 생각하는 모델이라 수백으로 두면 답이 빌 수 있어요 messages=[{"role": "user", "content": "안녕하세요"}], ) print(resp.choices[0].message.content) print(resp.choices[0].finish_reason, resp.usage.prompt_tokens, resp.usage.completion_tokens) print("쓴 크레딧:", getattr(resp.usage, "cost_credits", None)) # SDK 가 모르는 칸도 받아 둬요(SDK 동작) · 정산이 2초 안에 안 끝나면 None print("요청 ID:", resp._request_id) # 머리글 x-request-id(SDK 동작) ``` **TypeScript — npm install openai (서버에서 돌려요)** ```typescript import OpenAI from "openai"; const client = new OpenAI({ apiKey: process.env.ONEPORT_API_KEY, baseURL: "https://oneport.kr/v1", maxRetries: 0, timeout: 300_000 }); const resp = await client.chat.completions.create({ model: "gpt-5-nano", max_tokens: 2000, messages: [{ role: "user", content: "안녕하세요" }], }); console.log(resp.choices[0].message.content); // TS 타입엔 없는 칸이라 형 변환해 읽어요 · 정산이 2초 안에 안 끝나면 undefined console.log("쓴 크레딧:", (resp.usage as { cost_credits?: number } | undefined)?.cost_credits); console.log("요청 ID:", resp._request_id); // 머리글 x-request-id(SDK 동작) ``` 답은 choices[0].message.content 예요. finish_reason 이 length 면 출력 상한에 닿은 답이라 max_tokens 를 올리세요 — Claude 가 답을 거절하면 content_filter 로 와요. gpt-5-nano 같은 생각하는 GPT 는 생각한 토큰도 이 상한에 들어가요(벤더 동작) — 그래서 2,000 을 뒀어요. GET /v1/models 의 supports_effort 는 reasoning_effort 를 받는 모델이라는 뜻일 뿐이에요 — claude-haiku-5-5 처럼 true 인 Claude 도 reasoning_effort 를 안 보내면 저희가 아무것도 더하지 않아요. 401 error.reason "missing_key"(「API 키가 없어요. …」)면 환경변수가 빈 채로 간 거예요(새 터미널 창이면 대개 이 경우예요). Windows PowerShell 은 「curl」 장(https://oneport.kr/docs/guides/curl.md)의 「Windows PowerShell」 절을 쓰세요. SDK 가 없는 곳(구글 Apps Script 의 UrlFetchApp 등)도 curl 과 같은 POST 예요 — 구글 시트에 붙여 넣을 함수는 「구글 시트에서 쓰기」 장(https://oneport.kr/docs/guides/google-sheets.md)에 있어요. 답을 정해진 JSON 으로 받으려면 response_format 에 {"type": "json_schema", "json_schema": {"name": …, "schema": {…}}} 를 실어요 — Claude 로 갈 때도 저희가 Claude 의 구조화 출력으로 옮겨 그 모양대로 답해요({"type": "json_object"} 는 Claude 에선 모양을 강제하지 않아요 · json_schema 에 "strict": true 를 실으면 스키마를 손대지 않고 넘겨서 Claude 가 못 받는 모양이면 4xx upstream_error 예요 · strict 없이 보내면 길이 · 개수 · 범위 제약(minLength · maxItems · minimum 등)은 빼고 옮기니 받은 뒤 직접 검사하세요 · 열쇠가 정해지지 않은 object(additionalProperties 를 true 나 스키마로 적은 것 · patternProperties)는 강제하지 않고 응답 머리글 x-gateway-ignored-params 에 response_format 을 적어요 · 칸 전부는 「글」 장 https://oneport.kr/docs/reference/chat.md 의 response_format 칸). claude-haiku-5-5 같은 Claude 5 세대에 temperature · top_p 를 실어도 400 이 아니에요 — 저희가 빼고 보내고 응답 머리글 x-gateway-ignored-params 에 적어요. PDF 는 user content 에 {"type": "file", "file": {"filename": "a.pdf", "file_data": "data:application/pdf;base64,…"}} 로 넣어요 — Claude 로 갈 땐 저희가 Claude 의 PDF 형식으로 옮기고(filename 은 문서 제목으로 실어요 — 「이 PDF 이름이 뭐야」에 답할 수 있어요), GPT 는 그대로 넘겨요. 요청 한 번은 4.5MB 까지예요(base64 라 원본 약 3MB) — 넘으면 JSON 이 아닌 영어 413(FUNCTION_PAYLOAD_TOO_LARGE)이라 error.type 이 없어요. 부르기 전에 잡는 입력 몫은 쪽 수가 아니라 base64 바이트 ÷ 3 토큰이라 실제보다 크게 잡혀요 — 그 수가 pricing.tier 의 above_input_tokens 이상이면 tier 단가로 잡아요. 글자를 뽑아 보내면 어느 모델이든 되고 더 싸요. usage.cost_credits 가 그 호출에서 뗀 크레딧이에요(소수 넷째 자리까지 올린 값 · 정산이 2초 안에 안 끝나면 칸 없이 와요 — 그땐 토큰 × GET /v1/models 단가로 셈해요). 잔액(GET /v1/credits)의 앞뒤 차이로는 세지 마세요 — 정산이 답 뒤에 따로 돌고 balance_credits 는 1 크레딧 아래를 버려요. 부르기 전에는 입력(요청 몸통 3바이트 = 1토큰) × 입력 단가(Claude 는 cache_control 을 실으면, 그 밖의 모델은 pricing.rates 에 cache_write 가 있으면 캐시 쓰기 단가)와 출력 상한(max_tokens · 안 적으면 8,192 — gpt-5.6-sol · gpt-6-sol · gpt-6-luna 는 몸통이 단순하면 128,000 이고 셈이 따로예요 · 「남은 한도」 장 https://oneport.kr/docs/reference/key.md) × 출력 단가를 더한 몫을 잔액 · 키 한도에서 잡아 두고, 끝나면 쓴 만큼만 떼요 — max_tokens 를 필요한 만큼만 적으면 402 hold_exceeds_balance · 429 key_month_limit 이 덜 나요. 문의할 땐 요청 ID(머리글 request-id · 같은 값이 x-request-id)와 부른 시각 · 모델 이름을 알려 주세요. ## 2. 스트림으로 받기 **Python — 받는 대로 찍고, 잘린 답인지 가려요(위 client 그대로)** ```python import openai stream = client.chat.completions.create( model="gpt-5-nano", max_tokens=2000, messages=[{"role": "user", "content": "분수의 덧셈을 세 줄로 설명해 줘"}], stream=True, # 시작 전 실패(402 · 429 …)는 여기서 openai.APIStatusError 로 와요 — 4번 ) text, finish, usage = [], None, None try: for chunk in stream: if chunk.usage is not None: # 끝 조각 — 토큰 수만 실려 choices 가 빈 배열이에요 usage = chunk.usage for ch in chunk.choices: if ch.delta.content: text.append(ch.delta.content) print(ch.delta.content, end="", flush=True) if ch.finish_reason: finish = ch.finish_reason except openai.APIError as e: # 도중 error 조각 · 연결 끊김 — 받은 글은 text 에 남아 있어요 print("\n도중 실패:", e) answer = "".join(text) if finish is None: print("\n잘린 답이에요 — 처음부터 다시 부르지 말고 이어 받으세요(「에러」 장 「잘린 답 이어 받기」).") elif finish == "length": print("\n출력 상한에 닿았어요 — max_tokens 를 올리거나 이어 받으세요.") print("\n토큰:", usage) print("쓴 크레딧:", getattr(usage, "cost_credits", None)) # 끝 usage 조각에만 와요 — 그 조각이 없거나 정산이 2초 안에 안 끝나면 None print("요청 ID:", stream.response.headers.get("request-id")) # 도중 error 조각엔 없어요 — 머리글에서 읽어요(SDK 동작) ``` 정상으로 끝나면 finish_reason 이 든 조각, usage 만 든 조각(choices 가 빈 배열), data: [DONE] 순서로 와요 — 모델 회사와 상관없이 usage 조각이 와서 stream_options 는 안 보내셔도 돼요. 쓴 몫(usage.cost_credits)도 그 끝 usage 조각에 와요. 요청 ID 는 응답 머리글에만 있어요 — Python 은 위처럼 stream.response.headers, Node openai SDK 는 const { data: stream, response } = await client.chat.completions.create({ …, stream: true }).withResponse() 로 받아 response.headers.get("request-id") 로 읽어요(SDK 동작). finish_reason 없이 끝나면 잘린 답이에요. 그때까지 흘러간 토큰은 이미 사용했으니, 받은 글을 assistant 메시지로 붙이고 이어 써 달라는 user 메시지를 붙여 이어 받으세요 — 순서와 코드는 「에러」 장(https://oneport.kr/docs/errors.md) 「잘린 답 이어 받기」에 있어요. Claude 흐름이 도중에 끊기면(벤더 오류 · 저희 시간 제한 · 연결 끊김) error 조각 바로 앞에 그때까지의 usage 조각(choices 가 빈 배열)이 와요 — 정산이 2초 안에 끝나면 거기에 usage.cost_credits 도 실려서, 끊긴 호출의 쓴 몫도 셀 수 있어요(첫 이벤트 전에 실패했거나 손님 쪽에서 먼저 끊은 흐름엔 없어요). Claude 밖 모델은 저희 시간 제한 · 연결 끊김이면 usage 조각 없이 error 조각 하나로 끝나요. 긴 답은 stream: true 로 받으세요 — 스트림 없이 부르면 답이 다 만들어져야 첫 응답이 와서, 모델 회사로 보낸 뒤 첫 응답 제한을 넘으면 422 upstream_execution_unconfirmed 로 끝나요(§4 표). 답을 못 받았다고 무료이거나 실행되지 않았다고 판단하지 마세요. ## 3. 도구 한 번 왕복 **Python — 모델이 부른 함수를 실행하고 결과를 먹여 이어 받아요(Claude 도 같은 모양)** ```python import json tools = [{ "type": "function", "function": { "name": "get_weather", "description": "도시의 지금 날씨", "parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}, }, }] messages = [{"role": "user", "content": "서울 날씨 알려줘"}] kw = dict(model="claude-haiku-5-5", max_tokens=2000, tools=tools) resp = client.chat.completions.create(messages=messages, **kw) msg = resp.choices[0].message if msg.tool_calls: messages.append(msg) # tool_calls 가 든 assistant 메시지 그대로 for call in msg.tool_calls: args = json.loads(call.function.arguments) result = {"city": args["city"], "temp_c": 18} # 여기서 실제 함수를 불러요 messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result, ensure_ascii=False)}) resp = client.chat.completions.create(messages=messages, **kw) # 새 호출이라 요금도 따로예요 print(resp.choices[0].message.content) ``` 도구 정의 · 결과는 회사가 달라도 OpenAI 꼴 그대로예요 — Claude 로 갈 땐 저희가 번역해요. 한 턴에 도구를 여럿 불렀으면 결과도 tool_call_id 마다 하나씩 다 붙이세요. 스트림이면 도구 호출이 delta.tool_calls 조각으로 와요 — 첫 조각에 index · id · function.name 이, 뒤 조각엔 같은 index 로 function.arguments 글이 이어 와서 index 마다 이어 붙이면 돼요. 도구를 불렀으면 finish_reason 이 tool_calls, 다 답했으면 stop 이에요(Claude 도 같아요). GPT-5.6 · GPT-6 에 tools 를 실을 땐 reasoning_effort 를 빼고 보내세요(「OpenAI SDK」 장 「도구 호출」). pro 등급은 /v1/responses 로만 불러요 — 도구 모양이 달라요(「깊은 추론」 장 https://oneport.kr/docs/reference/responses.md). ## 4. 오류 — 무엇을 다시 보내나 | 상태 | error.type | 다시 보내나 | SDK 예외 클래스 | | --- | --- | --- | --- | | 402 | insufficient_credit | 기다렸다(reason held_by_others · Retry-After 5초) · 고쳐서(hold_exceeds_balance — 상한을 줄여요) · 멈춤(below_minimum_balance — 충전) — SDK 는 402 를 스스로 안 보내요 | 전용 클래스 없음 — Python APIStatusError · Node APIError | | 429 | key_limit_exceeded | 기다렸다(reason held_by_others — 같은 API 키로 보낸, 아직 끝나지 않은 다른 호출이 잡아 둔 몫 · Retry-After 5초 · x-should-retry: true 라 SDK 재시도를 켜 두면 SDK 가 해요) · 고쳐서(key_month_limit — 상한을 줄여요 · 안 되면 한도를 올리거나 한국 시간 다음 달 1일 0시 · 시각은 error.resets_at) · 멈춤(key_day_limit — 한국 시간 내일 0시에 다시 열려요 · 시각은 error.resets_at · 이미 쓴 몫만 재서 닿기 전에 보낸 호출(동시에 보낸 것 포함)은 다 지나가 그만큼 넘을 수 있어요 — 「남은 한도」 장) — key_month_limit · key_day_limit 은 x-should-retry: false 라 SDK 도 안 보내요 | RateLimitError(세 갈래 다 — type 으로 갈라요) | | 429 | rate_limit_error | 기다렸다 — 글 문 · 토큰 세기 문 · 임베딩 문은 Retry-After 가 늘 붙으니 그만큼(벤더가 준 값을 1~60초로 맞추고, 벤더 값이 없으면 5초) · SDK 재시도를 켜 두면 SDK 가 해요 | RateLimitError(세 갈래 다 — type 으로 갈라요) | | 429 | group_limit_exceeded | 고쳐서(reason group_budget — GET /v1/key 의 group.remaining_credits 가 0 보다 크면 상한을 줄여요 · 오류 몸통엔 이 칸이 없어요) · 멈춤(group_budget 인데 남은 몫 0 · group_closed 닫힌 강좌 · group_not_started · group_ended 반 기간 밖) — SDK 도 안 보내요 · 스페이스(강좌 · 반)에 붙은 키에서만 와요 | RateLimitError(세 갈래 다 — type 으로 갈라요) | | 503 | api_error | 기다렸다 — 글 문 · 토큰 세기 문 · 임베딩 문 · 영상 만들기 문 · 조회 문(/v1/credits · /v1/key · /v1/models) · 관리 키 문(/v1/keys)은 Retry-After 2초(reason provider_unavailable 도 같아요 — 키를 잠깐 못 읽은 503 과 할 일이 같아요) · SDK 재시도를 켜 두면 SDK 가 해요 · 커넥터 문(/v1/connectors …)엔 Retry-After 도 x-should-retry 도 없어 몇 초 쉬고 다시(POST 는 같은 Idempotency-Key · 같은 본문으로) | InternalServerError | | 4xx · 5xx | upstream_error | 고쳐서(요청 거절 4xx) · 기다렸다(x-should-retry: false 없이 온 5xx — 간격은 Retry-After, 없으면 코드가 정해요 · 완전한 본문으로 확인된 Anthropic overloaded_error 만 529 로 와요) · 멈춤(x-should-retry: false 가 붙은 것 — 이게 먼저예요 · 504 error.reason timeout 은 요금 없이 끝난 거라 직접 한 번 다시 보내도 돼요 — Idempotency-Key 를 실었으면 같은 Idempotency-Key 로) | 상태대로 — 5xx 는 InternalServerError(Python anthropic 은 529 가 OverloadedError) | | 422 | upstream_execution_unconfirmed | 멈춤 — 실행됐는지 몰라요 · Idempotency-Key 를 실었으면 받은 x-gateway-status-url 로 GET 조회, 안 실었으면 요청 ID 와 부른 시각으로 문의 | UnprocessableEntityError | | 400 · 404 | invalid_request_error · model_not_found | 고쳐서 — 문장이 고칠 곳을 말해요 | BadRequestError · NotFoundError | **Python — 402 · 429 를 type · reason 으로 갈라요(위 client 그대로 · 스트림이면 stream=True 를 같이 넘겨요)** ```python import time import openai def create(**params): """잠시 뒤 그대로 다시 보낼 갈래만 다시 보내요 — 전체 갈래는 「에러」 장 전체 판(https://oneport.kr/docs/errors.full.md) 「그대로 쓰는 감싸개」.""" for _ in range(5): try: return client.chat.completions.create(**params) except openai.APIStatusError as e: body = e.body if isinstance(e.body, dict) else {} # Python openai SDK 는 error 안쪽 {type, message, code, reason} 을 줘요 kind, reason = body.get("type") or e.type, body.get("reason") no_retry = e.response.headers.get("x-should-retry") == "false" wait = float(e.response.headers.get("retry-after") or 0) if no_retry or kind in ("upstream_execution_unconfirmed", "request_already_exists", "request_state_unavailable", "request_store_unavailable"): # held_by_others 밖의 한도 429(key_month_limit · key_day_limit · group_limit_exceeded) · 422 · 504 reason timeout 도 여기서 멈춰요(x-should-retry: false) # key_month_limit · group_budget 을 줄인 새 요청으로 보내려면 이 줄보다 먼저 reason 을 봐요 · 504 timeout 은 요금 없이 끝나 직접 한 번 다시 보내도 돼요 raise RuntimeError(f"자동 재전송 금지 — {e.status_code} {kind} {reason}: {body.get('message')}") from e if reason == "held_by_others": time.sleep(wait or 5) # 402 · 429 — 다른 호출이 잡아 둔 몫(429 는 같은 API 키의 호출) — 그대로 다시 elif (e.status_code == 429 and kind == "rate_limit_error") or (e.status_code == 503 and kind == "api_error"): time.sleep(wait or 2) # 벤더 혼잡 · 일시 장애 — 그대로 다시 else: # hold_exceeds_balance → max_tokens 를 줄여 한 번 더 · below_minimum_balance → 충전 # 400 · 404 · 4xx upstream_error(422 포함) → 문장(body["message"])대로 고쳐요 — 찍어 두면 고칠 곳이 보여요 raise RuntimeError(f"{e.status_code} {kind} {reason}: {body.get('message')}") from e raise RuntimeError("다시 보내도 계속 막혀요") resp = create(model="gpt-5-nano", max_tokens=2000, messages=[{"role": "user", "content": "안녕하세요"}]) print(resp.choices[0].message.content) ``` 이 예시는 Idempotency-Key 를 싣지 않지만, 실어도 위 코드가 그대로 돌아요 — 실행 전에 막힌 402 · 429 의 held_by_others · 429 rate_limit_error · 503 api_error 는 Idempotency-Key 를 실어도 x-should-retry: false 없이 Retry-After 가 와서, 같은 Idempotency-Key · 같은 본문으로 다시 보내면 새로 접수돼요. 503 request_state_unavailable · request_store_unavailable 만은 위 코드가 멈추는데, 같은 Idempotency-Key · 같은 본문으로 다시 보내도 돼요. 쓰시려면 요청 하나에 Idempotency-Key 하나를 만들어 create(..., extra_headers={"Idempotency-Key": key}) 로 넘기세요(Node openai SDK 는 client.chat.completions.create 의 둘째 인자 { headers }) — 다시 보낼 때도 같은 Idempotency-Key 예요. 값은 공백 · 제어문자 없는 ASCII(영문 · 숫자 · 기호) 8~200자이고(어기면 400 invalid_request_error), 같은 API 키 안에서만 같은 요청으로 봐요. 409 request_already_exists 는 error.type 으로 와요. 조회의 execution 이 started 면 모델 회사로 보냈다는 뜻이라 끝까지 답한 요청(result.state completed)도 여기예요 — 같은 Idempotency-Key 로는 다시 못 받고 답 본문도 조회로 못 받으니, 받은 답은 그때 저장해 두고 일을 다시 돌릴 땐 답을 받은 단계는 다시 부르지 마세요(다시 부르면 409). 조회가 503 이면 상태를 잠깐 못 읽은 거라 Retry-After(2초) 뒤 다시 조회해요(요금 없음). 받은 응답마다 같은 Idempotency-Key · 새 Idempotency-Key · 조회 · 멈춤을 고르는 표와 조회 결과로 할 일은 「요청 상태」 장(https://oneport.kr/docs/reference/requests.md)에 있어요. 분기는 상태 코드나 e.message 가 아니라 error.type 과 error.reason 으로 하세요. 문장(Python 은 e.body["message"], Node 는 err.error.message)은 API 를 부르는 개발자에게 보일 말이에요 — 400 · 401 · 404 는 고칠 곳이 이 문장에 적혀 오니 꼭 찍어 두세요(SDK 동작). max_tokens · GET /v1/key 같은 API 낱말이 들어 있어서 서비스 손님 화면엔 type · reason 으로 고른 문장을 보여 주세요. SDK 자동 재시도는 꺼 두었고(max_retries=0 · maxRetries: 0), 위 예시는 실행 전 거절이 확인된 갈래만 다시 보내요 — 표의 「SDK 재시도를 켜 두면 SDK 가 해요」는 켰을 때 얘기라, 이 장처럼 껐으면 코드가 Retry-After 만큼 기다렸다 같은 요청을 보내요(위 create()). Node SDK 는 v5 이상 err.headers.get("retry-after"), v4 는 err.headers["retry-after"] 예요(SDK 동작). 알려지지 않은 5xx · 422 실행 불명은 자동 재전송하지 않아요. 200 전 연결 오류 · 시간 초과도 그래요 — Idempotency-Key 를 실었으면 같은 Idempotency-Key · 같은 본문으로 다시 보내요. 422 조회에선 result 와 settlement 를 따로 확인하세요(같은 Idempotency-Key 로 다시 보내 409 가 와도 그 응답에 같은 x-gateway-status-url 이 와요). 일반 요청 ID만으로 조회 주소를 만들지 마세요. upstream_error 인 422 는 벤더 거절이라 고쳐 보내요. 갈래 전부(시간 초과 · 끊긴 스트림 어림 · 오늘 · 이번 달 한도가 다시 열리는 때)와 그대로 쓰는 Python · TypeScript 감싸개는 「에러」 장(https://oneport.kr/docs/errors.md)에 있어요. ## 다음에 읽을 장 - OpenAI SDK · Anthropic SDK — https://oneport.kr/docs/guides/openai-sdk.md · https://oneport.kr/docs/guides/anthropic-sdk.md (Anthropic SDK 의 base_url 은 /v1 없이) - 글 문의 칸 하나하나 — https://oneport.kr/docs/reference/chat.md - 모델과 단가 — https://oneport.kr/docs/models.md - 남은 크레딧 · 키 한도 — GET https://oneport.kr/v1/credits · GET https://oneport.kr/v1/key(요금 없음 · https://oneport.kr/docs/reference/key.md) — 키의 월 · 하루 한도는 대시보드 AI API 화면 키 줄의 「한도 수정」에서 걸어요