API 변경 기록
응답 머리글 · 칸 · 상태 코드 · 오류 갈래가 바뀐 날과 고칠 코드를 최근 순으로 적어요.
손님 코드가 읽는 동작이 바뀐 날만 적어요. 새 모델 · 끝나는 모델은 https://oneport.kr/changes 예요.
코드에선 끝나는 날을 GET /v1/models 줄의 retirement(date · successors)로 읽어요 — https://oneport.kr/changes 는 사람이 보는 페이지예요. 끝난 이름을 부르면 404 model_not_found 예요(2026-10-23 에 o1 · o1-pro · o3-mini · o4-mini · gpt-4.1-nano · gpt-image-1 판매가 끝났어요).
이 기록은 2026-10-26 부터예요. 그 전에 바뀐 동작은 여기 없어요 — 지금 동작은 「에러」 장(https://oneport.kr/docs/errors.md)이 기준이에요.
다만 바로 전 주(2026-10-25)에 바뀐 머리글은 이래요 — 429 key_limit_exceeded · group_limit_exceeded 에 x-should-retry: false 가 붙고(2026-10-30 부터 key_limit_exceeded 중 error.reason held_by_others 만 x-should-retry: true · Retry-After: 5 예요 — 그날 줄), effort 를 못 받는 모델에서 빼고 보낸 reasoning_effort 가 x-gateway-ignored-params 에 적혀요. 모델 회사로 보낸 글 문 응답의 x-gateway-fallback-depth(2026-08-25 부터) · x-gateway-source(2026-09-23 부터)는 이 기록 전부터 와요 — x-gateway-source 는 저희 기록용이라 코드에서 가르지 마세요.
날짜는 그 동작이 들어간 날(한국 시간)이에요. 고칠 코드에 SDK 이름이 없으면 openai SDK · Anthropic SDK 가 같은 코드예요 — 다만 줄 끝 주석에 requests · fetch 가 적힌 코드(GET /v1/requests/{id} · /v1/credits · /v1/key · /v1/keys · /v1/connectors · /v1/videos · /v1/tasks 등)는 SDK 가 아니라 그렇게 받은 응답을 읽는 코드예요. Node 줄의 err.headers?.get(…) 은 err.headers 가 Headers 객체인 판(openai SDK v5 이상) 기준이에요 — 일반 객체로 주는 판(openai SDK v4)은 err.headers?.["retry-after"] 처럼 읽어요(v4 에서 .get 을 부르면 TypeError).
2026-11-03#
- 바뀜
채팅 문으로 Gemini 서버 도구(검색 · URL 읽기 · 코드 실행)를 실으면 400
POST /v1/chat/completions
tools 나 extra_body.google.tools 에 google_search · google_search_retrieval · enterprise_web_search · google_maps · url_context · code_execution(googleSearch 같은 낙타 꼴 이름도 같아요)을 실으면 이제 모델 회사로 보내기 전에 400 invalid_request_error 로 돌려드리고, 크레딧은 안 써요. 예전엔 그대로 Gemini 로 갔어요. 문장에 뺄 도구 이름이 적혀 와요. 직접 정의한 함수 도구(type "function")와 도구 없는 호출은 그대로예요. /v1/messages · /v1/responses 의 서버 도구 규칙은 바뀌지 않았어요.
Pythontools = [t for t in tools if t.get("type") == "function"]Nodetools = tools.filter((t) => t.type === "function")
2026-11-02#
- 바뀜
초·중·고 학교 지갑의 새 키 — audience 를 안 보내면 선생님 키
POST /v1/keys
초·중·고 학교 지갑에서 관리 키로 키를 만들 때 audience 를 안 보내면 이제 선생님 키("teacher")가 서요. 예전엔 학생 키였어요. 학생 키는 "audience": "student" 를 보낼 때만 서고, 월 한도 기본값과 보호자 동의 확인은 그대로예요. 대시보드 「새 키 발급」 · 「여러 개 발급」도 「선생님」이 먼저 골라져 있어요. 이미 만든 키의 쓰임새는 바뀌지 않아요.
{"name": "과학 동아리", "audience": "student"} - 새 기능
한도 429 에 error.resets_at — 다시 열리는 시각
POST /v1/chat/completions · /v1/messages · /v1/responses · /v1/embeddings · /v1/images/* · /v1/audio/* · POST /v1/videos · POST /v1/tasks
429 key_limit_exceeded · group_limit_exceeded 몸통에 error.resets_at 이 와요(UTC ISO 시각 · GET /v1/key 의 resets_at 과 같은 값). key_day_limit 은 한국 시간 내일 0시, key_month_limit 과 매달 채워지는 강좌의 group_budget 은 다음 달 1일 0시, group_not_started 는 반 시작일 0시예요. 다시 안 열리거나(hold_exceeds_key_limit · 누적 강좌의 group_budget · group_closed · group_ended) 시각이 아니라 다른 호출을 기다리는 갈래(held_by_others)면 null 이에요. Retry-After 는 예전처럼 안 붙고 x-should-retry 도 그대로예요. POST /v1/tasks 는 처음 거절 응답에만 와요 — 같은 Idempotency-Key 로 다시 조회한 응답엔 없어요.
Python openai SDKe.body.get("resets_at") # None 이면 시각 없음Node openai SDKerr.error?.resets_at // null 이면 시각 없음Python Anthropic SDKe.body["error"].get("resets_at") # None 이면 시각 없음Node Anthropic SDKerr.error?.error?.resets_at // null 이면 시각 없음 - 바뀜
글 문 스트림 상한이 약 4분 30초에서 약 12분 50초로
POST /v1/chat/completions · /v1/messages · /v1/responses
스트림은 이제 저희가 모델 회사로 보내기 시작한 때부터 약 12분 50초(770초)에 끊겨요. 첫 응답 120초(다른 경로로 돌면 합쳐 4분) · 조각 사이 침묵 120초 · 스트림 없는 호출의 120초는 그대로예요. SDK 의 timeout 300초는 첫 응답 · 조각 사이만 재서 그대로 두면 되고, curl --max-time · 프록시처럼 전체 시간을 재는 제한은 780초로 올려 주세요. 읽기를 멈춘 흐름은 처리 시작 뒤 약 13분에 그때까지 센 몫으로 정산하고, 글 문은 접수 뒤 약 14분 안에 끝나요.
curl --max-time 780 … - 바뀜
커넥터 부탁 reason 은 그 부탁을 만든 키에만
POST /v1/connectors/requests · GET /v1/connectors/requests/{id}
부탁한 까닭 reason 은 이제 그 부탁을 만든 API 키가 부른 응답에만 와요. 같은 지갑의 다른 API 키가 조회한 응답과 열린 부탁에 합류한 POST 응답엔 칸이 없어요(null 이 아니에요). 만든 키가 지워진 부탁은 어느 키에도 안 와요. 부탁이 있다는 것과 state · message · wallet_decision 은 같은 지갑에 그대로 보여요.
Pythonr.get("reason") # 칸이 없을 수 있어요Noder.reason ?? null // 칸이 없을 수 있어요 - 고침
브라우저에서 /v1 을 빠뜨린 글 문 주소 — 안내 404 가 CORS 오류 대신 읽혀요
POST /chat/completions · /messages · /responses · GET /models(키를 실은 것) — /v1 이 빠진 주소
anthropic-dangerous-direct-browser-access: true 를 실은 브라우저 요청이면 /v1 이 빠진 글 문 주소의 404(「주소에 /v1 이 빠졌어요」)에 /v1 과 같은 CORS 머리글이 붙고, 사전 요청엔 204 로 답해요. 예전엔 브라우저에 CORS 오류로만 보였어요. 끝에 / 가 붙은 주소 · http:// 주소의 308 은 그대로라 여전히 CORS 오류로 보여요.
2026-11-01#
- 새 기능
GET /v1/key 의 group 에 반 기간(starts_on · ends_on)과 closed_reason
GET /v1/key
반(기수) 키면 group.starts_on · group.ends_on 에 반 기간이 한국 날짜(YYYY-MM-DD)로 와요 — ends_on 그날까지 써요(반이 아닌 그룹은 null). group.closed 는 이제 닫힌 강좌뿐 아니라 시작 전 · 끝난 반도 true 이고, group.closed_reason 이 다음 호출이 받을 429 group_limit_exceeded 의 error.reason(group_closed · group_not_started · group_ended — 열려 있으면 null)이에요. 예전엔 기간 밖 반 키도 closed: false 로 보였어요. 반 기간을 잠깐 못 읽으면 이 문이 503 api_error 예요.
Pythong = r.json()["group"]; blocked = g and g["closed"] # 까닭은 g["closed_reason"]Nodeconst g = (await res.json()).group; const blocked = g?.closed // 까닭은 g.closed_reason - 바뀜
/mcp — 🏫 학교장터 표시 · s2b_only 는 옵션 단위 · 물품번호 줄은 등록을 마친 번호만
POST https://schoolorder.kr/mcp
학교장터에 아직 안 올린 옵션만 있는 상품엔 🏫 · s2b_only 가 안 붙어요. 그런 옵션 줄 끝엔 「 · 학교장터 등록은 준비 중이에요」가, 그 옵션 견적엔 합계 다음에 「이 플랜은 홈페이지에서 주문해요. 학교장터 등록은 준비 중이에요.」가 붙어요. 상품 보기의 물품번호 줄은 등록을 마친 번호만이고 어느 옵션 것인지 알면 「번호 (옵션 · N개월)」로 와요 — 등록 상태를 못 읽으면 줄이 없어요. 학교용 분류도 스토어 분류와 같은 판정이라, 어느 분류로도 안 나오던 상품이 분류에 들어와요.
- 바뀜
/v1/keys — expires_on 에 빈 글을 보내면 400
POST /v1/keys · PATCH /v1/keys/{id}
expires_on 에 빈 글("")이나 공백을 보내면 이제 400 이에요(날짜 꼴 문장). 예전엔 기한 없음 · 기한 지우기로 받았어요. 기한을 지우려면 null 을 보내세요.
Pythonbody["expires_on"] = None # 기한 지우기 — "" 는 400Nodebody.expires_on = null // 기한 지우기 — "" 는 400 - 바뀜
조회 문과 /v1/keys 의 일시 실패 503 api_error 에 Retry-After: 2
GET /v1/credits · GET /v1/key · GET /v1/models · /v1/keys · /v1/keys/{id}
잔액 · 키 · 모델 목록을 잠깐 못 읽은 503 api_error 에 이제 Retry-After: 2 가 와요(x-should-retry 는 없어요) — 글 문과 같아요. 관리 키 문(/v1/keys)의 503 api_error 에도 같은 값이 와요. 그만큼 쉬고 같은 요청을 다시 부르세요. 예전엔 이 문들의 503 에 Retry-After 가 없어서, 그 머리글로 다시 부를지 정하는 코드가 이 503 을 다시 부르지 않았어요.
Pythonr.status_code == 503 and r.headers.get("retry-after") # "2" — 그만큼 쉬고 다시Noderes.status === 503 && res.headers.get("retry-after") // "2" — 그만큼 쉬고 다시 - 바뀜
/v1/chat/completions — n 이 2 이상이면 미리 잡는 출력 몫이 n 배 · n 은 1–128 정수만
POST /v1/chat/completions(Claude 밖 모델)
Claude 밖 모델(GPT · Gemini · Grok 등)에 n 을 2 이상 보내면 이제 지갑 잔액과 키 · 강좌 몫을 볼 때 출력 몫을 n 배로 잡아요(입력은 한 번) — 답을 n 개 만드니까요. 예전엔 답 하나 몫만 잡았어요. 그래서 예전엔 지나가던 요청이 402 hold_exceeds_balance · 429 key_limit_exceeded(강좌 키는 group_limit_exceeded)로 막힐 수 있어요 — n 이나 max_tokens 를 줄여 보내요. 정산은 그대로 쓴 만큼이에요. n 이 1부터 128까지 정수가 아니면 모델 회사로 보내기 전에 400 invalid_request_error · error.param "n" 이에요(요금 없음).
- 바뀜
주소(URL)로 붙인 그림 · 문서도 미리 잡는 몫에 들어가요
POST /v1/chat/completions · /v1/messages · /v1/responses
주소로 붙인 그림(/v1/messages 의 image 블록 source.type "url" · /v1/responses 의 input_image.image_url · /v1/chat/completions 의 image_url)은 한 장마다 그림 상한(Claude 4,800 · gpt-4o-mini 48,200 · 그 밖 16,400 토큰)만큼, 주소로 붙인 문서(/v1/messages 의 document 블록 source.type "url" · /v1/responses 의 input_file.file_url)는 요청 하나에 그 모델의 창 하나만큼 미리 잡아요. 예전엔 주소 글자 길이로만 어림해 몇백 토큰으로 잡혔어요. 정산은 그대로 모델 회사가 센 만큼이에요. 잔액이나 키 한도가 작아 402 · 429 가 나면 문서를 base64 로 넣어 보내세요 — 보낸 크기만큼만 잡아요(「남은 한도」 장 「최대 몫」).
- 바뀜
/mcp — 준비중 · 일시중지 상품은 값을 안 내요 · 견적 수량은 999 까지 · 주문이 안 서는 견적엔 까닭 한 줄 · 상품을 못 읽으면 isError
POST https://schoolorder.kr/mcp · 오피스오더 /mcp
스토어에서 준비중 · 일시중지인 상품은 찾기 줄에 「최저 … 원부터」가 없고, 상품 보기 · 견적은 값 · 견적서 링크 대신 「<이름> — 지금 판매하지 않는 상품이에요.」와 「상세: <상품 주소>」가 와요. 견적 수량은 999 까지예요 — 더 크게 넣으면 999 개로 셈해요(견적서와 같은 상한 · 예전엔 넣은 수 그대로 합계를 말해 견적서 합계와 갈렸어요). 그 구성으로 주문이 안 서면(추가 상품만 · 새 Business 워크스페이스 2석 미만 · 기관 전체 이용 2 이상) 합계 다음에 까닭이 한 줄 와요 — 견적서 링크는 그대로예요. 상품 보기에서 추가 상품은 「함께 쓰면 좋은 추가:」 아래로 따로 와요. 상품 보기 · 견적에서 상품 정보를 잠깐 못 읽으면 「그런 상품이 없어요」가 아니라 isError: true 로 와요 — 잠시 뒤 그대로 다시 불러요.
- 바뀜
GPT-5.6 이후 모델은 미리 잡는 입력 몫을 캐시 쓰기 단가로
POST /v1/chat/completions · /v1/responses
pricing.rates 에 cache_write 가 있는 Claude 밖 모델(10월 기준 GPT-5.6 이후)은 미리 잡는 입력 몫을 입력 단가가 아니라 캐시 쓰기 단가(입력 단가의 1.25배)로 잡아요 — 모델 회사가 새 입력을 캐시에 쓰면 그 단가로 정산돼서요. 예전엔 입력 단가로 잡아 정산이 잡은 몫을 넘을 수 있었어요. 그래서 잔액이나 키 한도가 빠듯하면 예전엔 지나가던 요청이 402 hold_exceeds_balance · 429 key_limit_exceeded 로 막힐 수 있어요 — 출력 상한이나 입력을 줄여 보내요. 정산은 그대로 쓴 만큼이에요(「남은 한도」 장 「최대 몫」).
- 바뀜
파일을 id 로 부르는 칸(file_id)은 400
POST /v1/chat/completions · /v1/messages · /v1/responses
/v1/messages 의 source.type "file" 블록 · /v1/responses 의 input_file · input_image 의 file_id · /v1/chat/completions 의 file 조각(file.file_id)을 실으면(도구 결과 안의 블록 포함 — /v1/responses 는 function_call_output · custom_tool_call_output 의 output 안도) 모델 회사로 보내기 전 · 잡아 두기 전에 400 invalid_request_error 「받지 않는 칸이에요: file_id. 요청에서 빼고 다시 보내 주세요.」예요 — 요금은 없어요. 예전엔 그대로 넘겼어요. 저희 문엔 파일을 올리는 길이 없어서 file_id 로 부를 파일이 없어요 — 파일은 base64 로 싣거나 주소(URL)로 붙여요(주소 문서는 창 하나만큼 잡아요 · 「남은 한도」 장 「최대 몫」).
Python openai SDKpart = {"type": "input_file", "filename": "a.pdf", "file_data": "data:application/pdf;base64," + b64} # file_id 대신Node openai SDKconst part = { type: "input_file", filename: "a.pdf", file_data: `data:application/pdf;base64,${b64}` } // file_id 대신Python Anthropic SDKblock = {"type": "document", "source": {"type": "base64", "media_type": "application/pdf", "data": b64}} # source.type "file" 대신Node Anthropic SDKconst block = { type: "document", source: { type: "base64", media_type: "application/pdf", data: b64 } } // source.type "file" 대신 - 바뀜
스페이스 지갑이 멈춘 키의 401 account_inactive 는 스페이스 관리자에게
/v1 의 모든 문(키를 실은 요청)
스페이스 지갑(학생 · 그룹 키)이 멈추거나 닫혔으면 401 account_inactive 의 문장이 「이 키의 지갑이 멈춰 있어요. 스페이스 관리자(선생님)에게 말해 주세요.」예요. 예전엔 스페이스 키도 admin@schoolorder.kr 로 알리라고 했어요. status · type · reason 은 그대로이고, 스페이스 아닌 지갑의 문장도 그대로예요(「인증」 장).
- 바뀜
GET /v1/videos/{id}/content — 한 번에 기다리는 시간 30초 → 2분
GET /v1/videos/{id}/content
영상 파일 받기 문의 상한이 2분이 됐어요. 영상이 막 끝난 직후엔 이 호출이 파일까지 마무리해서, 예전엔 30초에 504 로 끊길 수 있었어요. 클라이언트 읽기 시간 제한을 30초로 두셨다면 2분으로 늘려 주세요.
- 바뀜
/docs.md · /docs/<장>.md 는 핵심 판, 전체 판은 .full.md
GET https://oneport.kr/docs.md · /docs/<장>.md · /docs/<장>.full.md
문서 마크다운 주소가 이제 장마다 핵심 판(약 12K 토큰 이하)을 내요 — 맨 위 작업 카드에 첫 코드에 필요한 규칙이 있어요. 경계 사례까지 든 예전 글은 주소 끝을 .full.md 로 바꾸면 그대로 받아요(개요 장은 /docs/index.full.md). /docs/all.md 는 지금처럼 문서 전체 한 파일이에요. 주소를 스크립트로 받아 색인하고 계시면 .full.md 나 all.md 로 바꿔 주세요.
- 고침
GET /v1/models — 단가표를 못 읽으면 빈 목록이 아니라 503 api_error
GET /v1/models · GET /v1/models/{id}
모델 단가표를 잠깐 못 읽으면 예전엔 200 에 빈 목록(data: [])이 와서(모델 하나 보기는 404 model_not_found) 이 키로 부를 모델이 없는 줄로 보였어요. 이제 503 api_error 예요 — Retry-After: 2 만큼 쉬고 다시 부르세요. 모델 목록을 저장해 쓰는 앱이면 503 에 저장한 목록을 지우지 마세요.
Pythonr.status_code == 503 # 빈 목록으로 읽지 말고 Retry-After 만큼 쉬고 다시Noderes.status === 503 // 빈 목록으로 읽지 말고 Retry-After 만큼 쉬고 다시 - 고침
시작 기록을 못 적은 503(Idempotency-Key 요청) — code api_error · error.reason dispatch_unavailable · Retry-After: 2
POST /v1/chat/completions · /v1/messages · /v1/responses
Idempotency-Key 를 실은 요청을 모델 회사로 보내기 직전에 저희가 시작 기록을 못 적으면 503 api_error 예요(모델 회사로는 안 갔고 요금도 없어요). 예전엔 error.code 가 type 과 다른 durable_dispatch_unavailable 이었고 x-should-retry: false · Retry-After 없음으로 와서 사람에게 넘겨졌어요. 이제 code 는 type 과 같은 api_error, error.reason 은 dispatch_unavailable 이고 Retry-After: 2 가 오며 x-should-retry 는 없어요. 같은 Idempotency-Key · 같은 본문으로 다시 보내면 새로 실행되고 두 번 실행되지 않아요(시작 기록만 남았을 때도 같아요). 드물게 끝 기록까지 못 적으면 예전처럼 x-should-retry: false 와 조회 주소가 와요(「에러」 장 「Idempotency-Key 를 실었을 때」).
Python openai SDKe.body.get("reason") == "dispatch_unavailable" # Retry-After 만큼 쉬고 같은 Idempotency-Key 로Node openai SDKerr.error?.reason === "dispatch_unavailable" // Retry-After 만큼 쉬고 같은 Idempotency-Key 로Python Anthropic SDKe.body["error"].get("reason") == "dispatch_unavailable"Node Anthropic SDKerr.error?.error?.reason === "dispatch_unavailable" - 고침
꺼진 키 401 문장이 대시보드 버튼 이름 「폐기」를 말해요
API 키를 받는 /v1 문 전부
꺼진 키(error.reason key_revoked)의 401 문장이 「(대시보드의 새로 받기 · 끄기)」였는데 대시보드엔 「끄기」 버튼이 없어요. 이제 「(대시보드의 새로 받기 · 폐기)」예요 — 키 줄의 버튼 이름 그대로예요. type · reason 은 그대로라 코드는 바꿀 것이 없어요.
- 고침
402 · 429 문장이 따를 수 있는 출구만 말해요 — 출력 상한 없는 /v1/responses · 주소 문서
POST /v1/chat/completions · /v1/messages · /v1/responses
/v1/responses 몸통에 max_output_tokens 가 없으면(Codex CLI 처럼) 402 hold_exceeds_balance 문장이 「max_output_tokens 를 줄여」 대신 충전을 말하고, 끝에 새 대화 안내가 붙어요(입력 몫이 크면 첨부 · 기록을 줄이라는 「미리 잡는 몫 대부분이 입력」 안내가 대신 붙어요). 429 key_month_limit 문장도(학생 · 스페이스 키 말고) 「줄이면 · 8,192 토큰으로 잡아요」 대신 같은 안내예요. 주소(URL)로 붙인 문서가 있는 요청은 402 · 429 의 「미리 잡는 몫 대부분이 입력」 문장 끝이 「첨부를 줄이거나 나눠」 대신 문서를 base64 로 넣어 보내라고 해요 — 주소 문서는 크기와 상관없이 창 하나만큼 잡아서 줄여도 안 풀려요. type · error.reason 은 그대로예요.
- 고침
DELETE /v1/videos/{id} — 파일을 못 지우면 200 이 아니라 503 api_error
DELETE /v1/videos/{id}
파일을 지우다 실패해도 예전엔 200 {"deleted": true} 가 왔어요. 이제 503 api_error 예요(Retry-After 없음). 그래도 그 영상은 이미 지운 영상과 같아요(받기 404) — 5초쯤 뒤 같은 DELETE 를 다시 보내 200 을 받으면 끝이에요(「영상」 장).
Python openai SDKe.status_code == 503 # client.videos.delete 가 던진 예외 · 받기는 이미 404 — 5초쯤 뒤 같은 DELETE 를 다시(200 이면 끝)Node openai SDKerr.status === 503 // client.videos.delete 가 던진 예외 · 받기는 이미 404 — 5초쯤 뒤 같은 DELETE 를 다시(200 이면 끝) - 고침
/v1/keys/{id} — 키 id 모양(uuid)이 아니면 503 이 아니라 404
GET · PATCH · DELETE /v1/keys/{id}
키 id 자리에 uuid 모양이 아닌 값을 넣으면 예전엔 503 api_error · Retry-After: 2 가 와서 다시 보내도 같은 503 이었어요. 이제 404 예요 — 다시 보내지 말고 GET /v1/keys 목록의 id 를 그대로 쓰세요.
Pythonr.status_code == 404 # r = requests 로 부른 /v1/keys/{id} 응답 — id 를 목록에서 다시Noderes.status === 404 // res = fetch 로 부른 /v1/keys/{id} 응답 — id 를 목록에서 다시 - 고침
읽기를 멈춘 스트림도 그때까지 쓴 몫을 정산해요
POST /v1/chat/completions · /v1/messages · /v1/responses
연결을 연 채 스트림 읽기를 멈추면 예전엔 정산이 돌지 않아 사용 기록이 안 남고, 조회의 settlement 도 서지 않았어요. 이제 처리 시작 뒤 약 4분 48초에 그때까지 센 몫으로 정산해요 — 「스트림이 끊기면 그때까지 쓴 토큰은 사용해요」와 같은 규칙이에요. 스트림은 끝까지 읽거나 바로 닫으세요.
2026-10-31#
- 고침
GET /v1/models/{id} — / 가 든 id 를 인코딩 없이 불러도 돼요
GET /v1/models/{id}
openai/gpt-oss-20b · Qwen/… 처럼 id 에 / 가 든 모델을 GET /v1/models/openai/gpt-oss-20b 로(퍼센트 인코딩 없이) 부르면 이제 그 모델 한 줄이 와요. 예전엔 없는 주소로 읽혀 404 not_found_error 였어요. 목록에 없는 id 는 404 model_not_found 예요.
Pythonrequests.get(f"{BASE}/v1/models/{model_id}") # / 를 그대로Nodefetch(`${BASE}/v1/models/${modelId}`) // / 를 그대로 - 고침
/v1 문의 지원 안 하는 메서드는 JSON 405 method_not_allowed
/v1 의 문 대부분(HEAD · OPTIONS · /v1/keys 빼고 · 작업 문은 같은 날 따로)
문이 받지 않는 메서드(예: GET /v1/chat/completions)로 부르면 이제 몸통 없는 405 대신 JSON 405 예요 — error.type(· code) method_not_allowed · Allow 머리글(받는 메서드) · request-id 가 와요. /v1/messages · /v1/messages/count_tokens 는 Anthropic 봉투로 와요. 그대로 다시 보내도 같으니 Allow 의 메서드로 고쳐 보내세요.
Pythonr.status_code == 405 and r.headers.get("allow") # 받는 메서드Noderes.status === 405 && res.headers.get("allow") // 받는 메서드 - 고침
두 머리글에 다른 값을 같이 보내면 어떤 401 이든 어느 머리글을 읽었는지 알려 줘요
/v1 의 모든 문 — POST /v1/chat/completions · /v1/messages · /v1/responses 등(관리 키 문 /v1/keys 빼고)
x-api-key 와 Authorization 을 다른 값으로 같이 보내면 저희는 x-api-key 를 읽어요. 그 키로 401 authentication_error 가 나면 이제 error.reason 이 무엇이든(key_revoked · key_expired · key_not_found · account_inactive 포함) message 끝에 「x-api-key 머리글의 키를 읽었어요」가 붙어요. 예전엔 x-api-key 쪽이 sk-so- 키 꼴이 아닐 때만 붙어서, 꺼진 옛 sk-so- 키가 x-api-key 에 남아 있으면 Bearer 에 실은 새 키가 꺼진 줄로 보였어요. status · type · reason 은 그대로이고, 키를 잠깐 못 읽은 503 엔 안 붙어요.
- 고침
판매가 끝난 모델의 날짜 붙은 이름도 404 가 끝난 날 · 바꿔 넣을 모델을 알려 줘요
POST /v1/chat/completions · /v1/messages · /v1/responses · /v1/messages/count_tokens · /v1/images/generations · /v1/images/edits
판매 중인 모델의 날짜 떼기는 2026-10-26 줄이에요. 날짜를 뗀 이름의 판매가 이미 끝났으면 날짜 붙은 이름도 404 model_not_found 이고, 이제 그 message 가 날짜 없는 이름을 불렀을 때처럼 끝난 날과 바꿔 넣을 모델(「이건 어떠세요: …」 맨 앞)을 먼저 말해요. 예전엔 날짜 붙은 이름이면 끝난 날 없이 이름이 비슷한 모델만 댔어요. 저희가 다른 모델로 바꿔 부르지는 않아요 — status · type 은 그대로예요. 코드로 미리 알려면 GET /v1/models 줄의 retirement 를 보세요.
- 고침
관리 키를 잠깐 못 읽으면 401 이 아니라 503 api_error
GET · POST /v1/keys · GET · PATCH · DELETE /v1/keys/{id}
관리 키 기록을 잠깐 못 읽었을 때 예전엔 401 authentication_error 로 와서 멀쩡한 관리 키가 죽은 키처럼 보였어요. 이제 503 api_error 예요 — 잠시 뒤 그대로 다시 보내세요(이날 이 문의 오류엔 x-should-retry · Retry-After 가 없었어요 — 2026-11-01 부터 이 503 엔 Retry-After: 2). 401 은 관리 키가 아니거나 닫혔을 때만 와요 — 다시 보내도 같아요(「키 관리」 장).
Pythonr.status_code == 503 # r = requests 로 부른 /v1/keys 응답 — 잠시 뒤 다시Noderes.status === 503 // res = fetch 로 부른 /v1/keys 응답 — 잠시 뒤 다시 - 바뀜
/mcp — JSON-RPC 묶음은 20개까지 · id 없는 tools/call 은 실행하지 않아요
POST https://schoolorder.kr/mcp · 오피스오더 /mcp
한 번에 보내는 JSON-RPC 묶음(배열)은 이제 20개까지예요 — 넘으면 400 에 -32600 Invalid Request 예요. id 없이 보낸 tools/call(알림 꼴)은 실행하지 않아요 — 결과를 받으려면 id 를 실으세요. 찾기 도구는 검색어 앞 200자만 봐요.
- 고침
작업 결과 파일 — 4.5MB 넘는 파일도 받아요
GET /v1/tasks/{id}/files/{fileId}
작업 결과 파일을 이제 흘려보내서 4.5MB 넘는 pptx · pdf 도 끝까지 받아요 — 예전엔 한 덩어리로 보내 큰 파일이 상한에 걸렸어요. 응답 꼴은 그대로예요(200 · Content-Type 은 그 파일 꼴 · Content-Disposition 의 filename*). 파일을 잠깐 못 읽으면 503 file_unavailable 이에요.
- 고침
GET /v1/videos — 목록을 못 읽으면 빈 목록이 아니라 503 api_error
GET /v1/videos
영상 목록을 잠깐 못 읽으면 예전엔 200 에 빈 목록(data: [])이 와서 만든 영상이 없는 줄로 보였어요. 이제 503 api_error 예요 — 잠시 뒤 같은 요청(after 를 실었으면 같은 after)으로 다시 보내세요. limit 은 정수로 읽어요(1.5 면 1).
- 고침
영상 받기 — 보관 7일(expires_at)이 지나면 정리 전이어도 404
GET /v1/videos/{id}/content
만든 영상의 expires_at(만든 뒤 7일)이 지나면 이제 정리되기 전이어도 404 not_found_error 예요. 예전엔 정리가 돌기 전까지 받아지는 때가 있어서 받을 수 있는 기간이 들쭉날쭉했어요. 받아 둘 영상은 expires_at 전에 내려받으세요.
- 고침
읽어주기 — 손님이 읽기를 멈춰도 기한(약 4분 30초)에 저희가 끊고 0
POST /v1/audio/speech
소리를 받는 쪽이 읽기를 멈춘 채 부른 때부터 약 4분 30초가 지나면 이제 저희가 끊고 그 호출은 0 이에요(소리는 거기서 잘려요). 예전엔 기한 검사가 손님이 읽을 때만 돌아서, 늦게 읽으면 손님이 끊은 것으로 셈될 수 있었어요. 손님 쪽에서 먼저 끊으면 그대로 손님 몫이에요.
- 고침
오피스오더에서 시작한 커넥터 계정 잇기가 끝까지 돼요
GET /v1/connectors 의 link_url(오피스오더)
오피스오더 주소의 link_url 로 계정 잇기를 시작하면 예전엔 돌아오는 자리에서 「지났어요」로 끝날 수 있었어요. 이제 그대로 끝까지 이어져요 — 링크와 등록은 바꿀 것이 없어요.
- 고침
나노바나나 — 21:9 size(예: 2520x1080)를 받아요
POST /v1/images/generations · /v1/images/edits(gemini-…-image)
가로 · 세로를 줄이면 7:3 이 되는 size(1680x720 · 2520x1080 등)를 이제 21:9 비율로 넘겨요(긴 변으로 1K · 2K · 4K). 예전엔 받는다는 목록에 21:9 가 있는데도 보내기 전에 400 이었어요.
- 바뀜
Responses — 12시간 캐시("12h")는 400 invalid_request_error · error.param
POST /v1/responses
prompt_cache_options.ttl 이나 input 조각의 prompt_cache_breakpoint.ttl 에 "12h" 를 주면 이제 모델 회사로 보내기 전에 400 invalid_request_error 예요(요금 없음) — error.param 에 그 칸 이름이 와요. 예전엔 그대로 넘겼어요. 30분 캐시(기본 · "30m")는 그대로 받아요 — 12시간 칸을 빼고 다시 보내세요.
Python openai SDKe.body.get("param") # prompt_cache_options.ttl · prompt_cache_breakpoint.ttlNode openai SDKerr.error?.param // prompt_cache_options.ttl · prompt_cache_breakpoint.ttl - 바뀜
gemini-3.1-flash-lite — 채팅 문에 오디오 조각을 보내면 400
POST /v1/chat/completions
gemini-3.1-flash-lite 에 오디오 조각(input_audio · data:audio/ 로 시작하는 file 조각)을 실으면 이제 보내기 전에 400 invalid_request_error 예요(요금 없음). 예전엔 그대로 넘겼어요. 녹음을 글로 옮기려면 POST /v1/audio/transcriptions 로 보내세요. 다른 모델의 오디오는 그대로예요.
- 바뀜
끊긴 Claude 흐름 — 누적 출력 수를 못 받았으면 흘려보낸 글로 출력을 어림해요
POST /v1/chat/completions · /v1/messages(Claude · stream: true)
Claude 스트림이 끝의 누적 출력 수(message_delta 의 usage)를 받기 전에 끊기면 이제 출력은 흘려보낸 글 · 도구 인자 · 생각 글 바이트 ÷ 3 토큰과 Claude 가 그때까지 알려 준 수 가운데 큰 쪽으로 세요(미리 잡아 둔 몫을 넘지 않아요). 예전엔 Claude 가 알려 준 수만 세서 끊긴 답의 출력이 거의 안 셌어요. 끝까지 받았거나 누적 출력 수를 받은 뒤 끊긴 흐름은 그대로 Claude 가 센 토큰이에요.
- 바뀜
검열 · 작업 문 — 스페이스 키에도 전송 정책(space_content_policy)
POST /v1/moderations · POST /v1/tasks
스페이스 키로 부르면 글 문과 같은 스페이스 전송 정책을 모델 회사 · 작업실로 보내기 전에 걸어요 — 이유는 space_content_policy 의 error.code(403 blocked_term · policy_forbidden · unsupported_capability · 409 policy_changed · 503 policy_unavailable 등 · 「에러」 장 표)예요. 예전엔 두 문만 정책 없이 받은 글을 그대로 보냈어요. 작업 문은 작업실이 웹 · 도구를 써서 정책을 켠 스페이스면 unsupported_capability 이고, 새로 맡길 때만 걸어요 — 같은 Idempotency-Key 로 다시 보낸 이미 맡은 작업은 그 작업이 와요. 스페이스가 아닌 키는 그대로예요.
Python openai SDKe.body.get("code") # space_content_policy 의 이유Node openai SDKerr.error?.code // space_content_policy 의 이유 - 바뀜
그림 문 — 미리 잡는 몫에 prompt 글 몫 · 차감은 잡은 몫까지
POST /v1/images/generations · /v1/images/edits
부르기 전에 잡는 몫에 prompt 글 몫(UTF-8 2바이트를 1토큰으로 어림 × 글 입력 단가)이 더해져요 — GET /v1/models 의 max_hold_credits_per_image 엔 이 몫이 없어서 prompt 가 길면 실제로 잡는 몫이 그보다 커요(한 장 값으로 파는 Grok 은 없어요). 실제 차감은 잡은 몫을 넘지 않아요 — 받아쓰기 문도 같아요.
- 고침
그림 · 소리 · 검열 문 — 저희 쪽 사정으로 모델 회사가 받지 않으면 503 api_error provider_unavailable
POST /v1/images/generations · /v1/images/edits · /v1/audio/speech · /v1/audio/transcriptions · /v1/moderations
저희 쪽 사정으로 모델 회사가 요청을 받지 않은 실패가 이제 글 · 임베딩 문처럼 503 api_error 에 error.reason "provider_unavailable" 로 와요(요금 없음) — 소리 · 검열 문은 Retry-After: 2 도 붙어요. 예전엔 모델 회사의 상태 그대로(429 등) 와서 손님 요청이나 한도 탓으로 보였어요. 잠시 뒤 그대로 다시 보내세요.
Python openai SDKe.status_code == 503 and e.body.get("reason") == "provider_unavailable"Node openai SDKerr.status === 503 && err.error?.reason === "provider_unavailable" - 고침
Grok 영상 — 접수 응답을 잃어도 503 이 아니라 200 queued 작업
POST /v1/videos
Grok 영상 모델로 만들기를 보낸 뒤 모델 회사 응답을 잃고 저희가 다시 읽기도 못 하면 예전엔 503 api_error(Retry-After: 2)로 와서 SDK 가 다시 보내 영상이 두 번 만들어지고 두 번 차감될 수 있었어요. 이제 세운 작업을 200 queued(머리글 openai-poll-after-ms: 5000)로 돌려줘요 — 그 id 로 조회하세요.
- 새 기능
GET /v1/models 의 scheduled_change 에 상위 구간(tier) 예고
GET /v1/models · GET /v1/models/{id}
상위 구간(pricing.tier)이 있는 모델은 차감 단가 예고가 걸린 날 pricing.scheduled_change.tier 에 그날부터의 상위 구간 단가가 {"above_input_tokens", "rates": [ … ]} 로 같이 와요(rates 는 scheduled_change.rates 와 같은 꼴). 예전엔 기본 구간 단가만 와서 긴 입력 요청의 그날 단가를 미리 알 수 없었어요. 상위 구간이 없는 모델엔 이 칸이 없어요.
Pythonm["pricing"].get("scheduled_change", {}).get("tier") # 없으면 NoneNodem.pricing.scheduled_change?.tier // 없으면 undefined - 바뀜
임베딩 400 — 모델 회사가 input 이 아닌 칸을 거절하면 error.reason vendor_param_rejected · error.param
POST /v1/embeddings
모델 회사가 input 이 아닌 칸(dimensions · encoding_format 등)을 가리켜 거절한 400 invalid_request_error 는 이제 error.reason 이 "vendor_param_rejected" 이고 그 칸 이름이 error.param 으로 와요. 예전엔 "vendor_rejected" 라 크기를 줄여 나눠 보내면 되는 줄로 읽혔어요 — 이 갈래는 나눠도 같으니 그 칸 값을 고쳐요. 칸을 안 가리켰거나 input 을 가리킨 거절은 "vendor_rejected" 그대로예요. status · type · message 는 그대로예요.
Python openai SDKe.body.get("reason") == "vendor_param_rejected" # 칸 이름은 e.body.get("param")Node openai SDKerr.error?.reason === "vendor_param_rejected" // 칸 이름은 err.error?.param - 고침
모델 목록을 잠깐 못 읽으면 404 가 아니라 503 api_error
POST /v1/chat/completions · /v1/messages · /v1/responses · /v1/messages/count_tokens · /v1/moderations · /v1/embeddings · /v1/images/generations · /v1/images/edits · /v1/audio/speech · /v1/audio/transcriptions · /v1/videos
있는 모델인데 저희가 모델 목록을 잠깐 못 읽으면 예전엔 404 model_not_found(advisor 조언 모델은 400)로 와서 모델 이름이 틀린 줄로 보였어요. 이제 503 api_error 에 Retry-After: 2 가 붙어요(/v1/messages · 토큰 세기 문은 Anthropic 봉투) — 2초 뒤 그대로 다시 보내세요(요금 없음). 목록에 없는 모델은 404 model_not_found 그대로예요.
Pythone.status_code == 503 # 2초 뒤 그대로 다시 — 404 면 모델 이름을 고쳐요Nodeerr.status === 503 // 2초 뒤 그대로 다시 — 404 면 모델 이름을 고쳐요 - 고침
글 문 — 입력 어림이 모델 창보다 크면 미리 잡는 몫을 창 크기로 줄여요
POST /v1/chat/completions · /v1/messages · /v1/responses
부르기 전에 잡는 몫의 입력 어림(보낸 몸통 바이트 ÷ 3 토큰)이 그 모델의 context_window 를 넘으면 이제 창 × 1.05 토큰을 그 구간의 가장 비싼 입력 단가(입력 · 캐시 읽기 · 캐시 쓰기 가운데)로 센 값까지만 잡아요. 예전엔 큰 PDF · 그림을 실은 요청이 모델이 받을 수도 없는 만큼의 몫을 잡아 402 · 429 가 났어요. 웹 검색 · 웹 가져오기 · 코드 실행 · MCP 처럼 모델 회사 쪽에서 도는 도구를 실은 요청은 한 호출 안에서 여러 번 돌 수 있어 예전 셈 그대로예요. 실제 차감은 그대로 센 토큰만큼이에요.
- 새 기능
401 error.reason unset_key — 키 자리에 null · undefined · None
/v1 의 모든 문(관리 키 문 /v1/keys 빼고)
키 자리에 null · undefined · None 글자가 오면(환경변수 · 스크립트 속성이 비어 그 값을 그대로 붙인 경우) error.reason 이 malformed_key 대신 unset_key 예요. status · type 은 그대로이고, 문장은 이날 「키 값이 비어 있어요(…)」로 시작하는 이 갈래 문장으로 바뀌었어요 — 문장 말고 reason 으로 가르세요(「인증」 장 「키가 안 열릴 때」 표).
Python openai SDKe.body.get("reason") == "unset_key" # 변수 이름부터Node openai SDKerr.error?.reason === "unset_key" // 변수 이름부터Python Anthropic SDKe.body["error"].get("reason") == "unset_key"Node Anthropic SDKerr.error?.error?.reason === "unset_key" - 새 기능
429 error.reason hold_exceeds_key_limit — 학생 키에서 요청 하나가 월 한도보다 클 때
POST /v1/chat/completions · /v1/messages · /v1/responses 등 키 한도를 재는 문
학교 지갑의 학생 API 키에서 이 요청 하나가 미리 잡는 몫만으로 월 한도보다 크면 429 key_limit_exceeded 의 error.reason 이 key_month_limit 대신 hold_exceeds_key_limit 예요 — 쓴 몫과 상관없이 같은 요청은 늘 막혀요. max_tokens 를 줄이거나 더 가벼운 모델로 바꾸면 지나가요. status · type 은 그대로이고 문장은 이 갈래 문장이에요 — 갈래는 reason 으로 가르세요(한도 0 인 키는 key_month_limit 그대로).
Python openai SDKe.body.get("reason") == "hold_exceeds_key_limit" # max_tokens 를 줄이거나 가벼운 모델로Node openai SDKerr.error?.reason === "hold_exceeds_key_limit" // max_tokens 를 줄이거나 가벼운 모델로Python Anthropic SDKe.body["error"].get("reason") == "hold_exceeds_key_limit"Node Anthropic SDKerr.error?.error?.reason === "hold_exceeds_key_limit" - 새 기능
임베딩 403 space_content_policy 에 error.reason token_input
POST /v1/embeddings (전송 정책을 켠 스페이스 키)
전송 정책을 켠 스페이스 키로 토큰 번호(숫자) 배열 input 을 보내 막히면 403 space_content_policy(code unsupported_capability)에 error.reason token_input 이 와요 — 글(문자열) 배열로 보내면 지나가요(LangChain OpenAIEmbeddings 는 check_embedding_ctx_length=False). 문장은 그대로예요.
Pythonr.json()["error"].get("reason") == "token_input" # 글 배열로 다시Node(await res.json()).error?.reason === "token_input" // 글 배열로 다시 - 바뀜
작업 — 503 session_failed 로 거절된 id 의 GET 이 POST 와 같은 rejected
GET /v1/tasks/{id}
세션을 못 열어 POST 가 503 session_failed 로 거절한 작업 id 를 GET 하면 이제 200 · state rejected(취소를 요청했으면 cancelled) · error.code session_failed · billing not_started 예요. 예전엔 같은 id 가 failed · error.code not_started 로 보였어요. 다시 보내기는 그대로예요(「작업」 장 「다시 보낼 때」 표).
Pythontask["state"] == "rejected" and task["error"]["code"] == "session_failed"Nodetask.state === "rejected" && task.error?.code === "session_failed" - 새 기능
작업 400 에 error.param — 고칠 칸 이름
POST /v1/tasks
작업 만들기 400 invalid_request_error 에 error.param 이 와요 — 모르는 칸이면 그 칸 이름, 아니면 input · model · budget · title 가운데 틀린 칸이에요. 문장은 늘 같으니 칸은 param 으로 보세요.
Pythonr.json()["error"].get("param") # 고칠 칸Node(await res.json()).error?.param // 고칠 칸 - 고침
작업 문의 지원 안 하는 메서드는 JSON 405 method_not_allowed
GET · PUT · PATCH · DELETE /v1/tasks · POST · PUT · PATCH /v1/tasks/{id}
작업 목록을 짐작해 GET /v1/tasks 를 부르거나 /v1/tasks/{id} 에 POST 하면 이제 몸통 없는 405 대신 405 method_not_allowed(type · code 같은 값) · Allow 머리글 · request-id 가 와요. 작업 목록 문은 없어요 — 만들 때 받은 id 를 보관하세요.
- 바뀜
날짜 붙은 같은 종류 모델 이름은 404 model_not_found — 임베딩 · 읽어주기 · 받아쓰기 · 영상 · 검열
POST /v1/embeddings · /v1/audio/speech · /v1/audio/transcriptions · /v1/videos · /v1/moderations
이 다섯 문에 그 문의 모델 이름을 끝에 날짜(-YYYYMMDD)를 붙여 보내면 이제 404 model_not_found 예요(각 문의 404 문장 그대로). 예전엔 400 invalid_request_error 「… 문으로는 부를 수 없어요 — 같은 문으로 보내주세요」로 와서 스스로 어긋났어요. 이 문들은 날짜를 떼 주지 않으니 GET /v1/models 의 id 그대로 보내세요. 다른 종류 모델(예: 임베딩 문에 글 모델)은 여전히 400 이에요 — 그림 문과 같은 규칙이에요.
Pythone.status_code == 404 # 날짜 붙은 이름이면 목록 id 그대로Nodeerr.status === 404 // 날짜 붙은 이름이면 목록 id 그대로 - 고침
Claude 로 가는 채팅의 그림 cache_control 을 옮기고 · image_url.detail 은 머리글에 · PDF 이름은 문서 제목으로
POST /v1/chat/completions (Claude 모델)
그림 조각에 단 cache_control 이 이제 Claude 로 실려요 — 예전엔 말없이 버려 캐시로 잡은 그림도 캐시 없이 셌어요(그림을 캐시 쓰기 단가로 잡아요). 기본값(auto)이 아닌 image_url.detail 은 Claude 에 맞는 칸이 없어 싣지 않고, 이제 x-gateway-ignored-params 에 image_url.detail 로 적어요(그림은 원래 크기로 써요). PDF 조각의 file.filename 은 문서 제목으로 실려요(빈 이름이면 없음).
Python"image_url.detail" in r.headers.get("x-gateway-ignored-params", "") # r = with_raw_response 응답Noderes.headers.get("x-gateway-ignored-params")?.includes("image_url.detail") // res = withResponse() 의 response - 고침
MCP 서버 JSON 응답에 charset=utf-8
POST /mcp
MCP 서버의 JSON 응답(200 · 400 · 403)이 이제 Content-Type: application/json; charset=utf-8 이에요. 예전엔 charset 이 없어 Windows PowerShell 5.1 의 Invoke-RestMethod 가 한글 설명을 깨뜨려 보일 수 있었어요(PowerShell 동작).
- 바뀜
받아쓰기 space_content_policy 의 error.code 에 이유
POST /v1/audio/transcriptions
스페이스 키의 전송 정책 거절(space_content_policy · 403 · 503 · 409)에서 error.code 가 이제 type 과 같은 값이 아니라 이유예요 — unsupported_capability(403 · 받아쓰기를 못 쓰는 스페이스) · policy_unavailable(503 · 잠시 뒤 다시) · policy_changed(409 · 다시 보내면 바뀐 정책으로) 등. 글 문과 같은 꼴이에요. status · type · message 는 그대로예요. 그림 · 읽어주기 · 영상 문은 code 가 그대로 type 과 같아요.
Pythonr.json()["error"]["code"] # r = requests 로 부른 /v1/audio/transcriptions 응답 — space_content_policy 면 "unsupported_capability" 등Node(await res.json()).error.code // res = fetch 로 부른 /v1/audio/transcriptions 응답 — space_content_policy 면 "unsupported_capability" 등 - 바뀜
토큰 세기 문 — 크레딧을 산 적 없는 지갑의 키는 403 model_not_in_plan
POST /v1/messages/count_tokens
크레딧을 산 적이 없는 지갑(다 환불한 지갑 포함)의 키로 토큰 세기를 부르면 이제 /v1/messages 와 같은 403 model_not_in_plan 이에요(모델 회사로 보내기 전 · 요금 없음). 예전엔 이 문만 200 으로 세어 줘서, 연결 확인으로 쓰면 붙은 것처럼 보였어요. 잔액이 0 이어도 산 적이 있는 지갑은 그대로 200 이에요.
Pythone.status_code == 403 # anthropic SDK — client.messages.count_tokens(...) 에서 PermissionDeniedError 면 크레딧 범위 밖Nodeerr.status === 403 // anthropic SDK — client.messages.countTokens(...) 에서 PermissionDeniedError 면 크레딧 범위 밖 - 새 기능
관리 키 문 401 에도 error.reason — missing_key · malformed_key
GET · POST /v1/keys · GET · PATCH · DELETE /v1/keys/{id}
관리 키를 못 읽은 401 엔 error.reason 이 missing_key, sk-som- 관리 키가 아닌 키(sk-so- 키 포함)를 보낸 401 엔 malformed_key 가 와요. 없거나 꺼졌거나 기한이 지났거나 만든 분이 지금 지갑 주인이 아닌 관리 키의 401 엔 여전히 reason 이 없어요 — 그땐 message 를 사람에게 보여 주세요. status · type · message · code 는 그대로예요(「키 관리」 장).
Pythonr.json()["error"].get("reason") # r = requests 로 부른 /v1/keys 응답 — 401 이면 "missing_key" · "malformed_key" · 없으면 NoneNode(await res.json()).error?.reason // res = fetch 로 부른 /v1/keys 응답 — 401 이면 "missing_key" · "malformed_key" · 없으면 undefined - 고침
접수 전 · 상태 조회 503 에 Retry-After: 2 — Idempotency-Key 를 실은 요청
POST /v1/chat/completions · /v1/messages · /v1/responses (Idempotency-Key 를 실었을 때) · GET /v1/requests/{id}
접수하는 사이 API 키를 잠깐 못 가린 503 api_error 와 접수 전에 난 503 request_state_unavailable(둘 다 조회 주소 없음)에 Retry-After: 2 가 오고 x-should-retry 는 붙지 않아요(request_store_unavailable 과 같아요). 2초 기다렸다 같은 Idempotency-Key · 같은 본문으로 다시 보내세요. 상태 조회 GET /v1/requests/{id} 의 503(request_state_unavailable · request_store_unavailable)도 Retry-After: 2 · x-should-retry 없음이라 2초 뒤 같은 주소를 다시 GET 해요. 예전엔 이 셋에 x-should-retry: false 가 붙고 Retry-After 가 없어 SDK 가 멈췄어요. request_state_unavailable · request_store_unavailable 은 error.type 이에요(code 도 같은 값) — api_error 가 아니라서, type 으로 다시 보낼 갈래를 고르는 코드는 이 둘을 따로 넣어 주세요.
Pythone.response.headers.get("retry-after")Nodeerr.headers?.get("retry-after")
2026-10-30#
- 새 기능
커넥터 문 넷 — GET /v1/connectors · 부탁 문 셋
GET /v1/connectors · POST /v1/connectors/requests · GET /v1/connectors/requests · GET /v1/connectors/requests/{id}
새 문 넷이에요. GET /v1/connectors 는 이 키에서 커넥터마다 지금 할 수 있는 일(state)과 막힌 까닭(reason — 고정 이름)을 줘요. 안 열린 서비스는 POST /v1/connectors/requests(Idempotency-Key 필수)로 부탁하고, 결과는 GET /v1/connectors/requests/{id} 로 봐요. 부탁할 수 없는 서비스는 403 connector_unavailable 에 error.reason(blocked_by_admin · not_verified · declined)이 와요 — blocked_by_admin 이면 지갑 관리자에게 물어요. 그분이 그 서비스를 다시 켜면 풀려요. not_verified 면 원포트에 물어요. 학생 키는 부탁 · 목록 문이 403 student_cannot_request 예요. 부탁 줄의 wallet_decision 은 지갑 관리자의 결정(enabled · declined · blocked · null)이고 저희 확인(state)과 따로 서요 — 지갑 관리자가 거절했으면 그 한 줄이 message 로 와요. 네 문 모두 요금이 안 붙어요. 칸과 다음 행동 표는 「커넥터」 장이에요.
Pythonr.json()["error"].get("reason") # r = requests 로 POST /v1/connectors/requests 한 403 응답Node(await res.json()).error?.reason // res = fetch 로 POST /v1/connectors/requests 한 403 응답 - 새 기능
임베딩 400 에 error.reason vendor_rejected — 모델 회사가 받지 않은 요청
POST /v1/embeddings
모델 회사가 받지 않은 임베딩 요청(입력이 너무 길거나 배열이 너무 큰 경우 포함)의 400 invalid_request_error 에 error.reason 이 "vendor_rejected" 로 와요. 예전엔 reason 이 없어 message 첫머리로 갈라야 했어요 — 이제 이 값으로 가르세요. 그대로 다시 보내도 같으니 묶음을 나눠 보내요. 저희가 보내기 전에 막은 400 엔 reason 이 없어요. status · type 은 그대로예요.
Python openai SDKe.body.get("reason") == "vendor_rejected"Node openai SDKerr.error?.reason === "vendor_rejected" - 새 기능
GET /v1/key 의 month.held_credits — 아직 끝나지 않은 호출 몫
GET /v1/key
month 칸에 held_credits 가 더해졌어요 — 이 키로 보내 아직 끝나지 않은 호출(답이 흐르는 중인 글 호출 · 만드는 중인 영상 · 진행 중인 작업 등)이 잡아 둔 몫이에요. 이번 달 한도는 used_credits + held_credits + 새 호출의 최대 몫으로 재요(아래 「키 월 한도가 아직 끝나지 않은 호출 몫까지 세요」 줄) — 새 호출이 들어갈 자리는 대략 remaining_credits − held_credits 예요. used_credits · remaining_credits 는 예전처럼 끝난 호출만 센 값이에요. 한도가 없는 키는 0 이고, 이 값을 읽지 못했으면 null 이에요(모른다는 뜻). 예전엔 이 몫을 보여 주는 칸이 없어 remaining_credits 가 남았는데도 429 key_month_limit 가 오는 까닭을 알 수 없었어요.
Pythonk["month"].get("held_credits") # k = requests 로 GET /v1/key 한 r.json() — None 이면 모름Nodek.month.held_credits // k = fetch 로 GET /v1/key 한 await res.json() — null 이면 모름 - 새 기능
401 에 error.reason — 키가 안 열린 까닭
/v1 의 모든 문 — POST /v1/chat/completions · /v1/messages · /v1/responses 등(관리 키 문 /v1/keys 빼고)
키가 안 열린 401 authentication_error 에 error.reason 이 와요 — missing_key(머리글이 없거나 「Bearer 」가 빠짐) · management_key · foreign_key · malformed_key · key_not_found · key_revoked · key_expired · account_inactive 여덟 값이에요(뜻과 할 일은 「인증」 장의 「키가 안 열릴 때」 표). 예전엔 reason 이 없어 message 문장으로 갈라야 했어요 — 이제 이 값으로 가르세요. status · type 은 그대로고 키가 있는 401 의 문장도 그대로예요. 키를 잠깐 못 읽은 503 api_error 엔 reason 이 없어요(관리 키 문 /v1/keys 의 401 은 2026-10-31 부터 따로 둘 — 그날 줄). 키 없는 401 문장은 모든 문 · 커넥터 문에서 「API 키가 없어요. Authorization: Bearer 또는 x-api-key 헤더로 보내주세요.」 하나가 됐어요(예전엔 문에 따라 「API 키가 없어요.」 · 「API 키가 필요해요.」였어요). 예전에 문장 「없는 키」로 가르던 코드는 key_not_found 하나만 잡았어요 — reason == "key_not_found" 로 바꾸세요.
Python openai SDKe.body.get("reason") # 401 이면 "missing_key" · "key_revoked" · "account_inactive" …Node openai SDKerr.error?.reason // 401 이면 "missing_key" · "key_revoked" · "account_inactive" …Python Anthropic SDKe.body["error"].get("reason")Node Anthropic SDKerr.error?.error?.reason - 새 기능
실패 응답에 Location · error.status_url — Idempotency-Key 를 실은 글 문
POST /v1/chat/completions · /v1/messages · /v1/responses
Idempotency-Key 를 실어 접수된 요청이 실패하면(422 upstream_execution_unconfirmed · 402 · 429 · 5xx 등) x-gateway-status-url 과 같은 값이 Location 머리글로 오고, 몸통이 JSON 오류면 error.status_url 에도 와요(성공 응답엔 머리글 x-gateway-status-url 만 와요). 예전엔 409 request_already_exists · 503 request_state_unavailable 에만 와서, 몸통이나 Location 만 읽는 코드는 422 에서 조회 주소가 없다고 봤어요. 접수 전에 막힌 실패(401 · 400 · 413 등)엔 예전처럼 조회 주소가 없어요.
Python openai SDKe.body.get("status_url") # /v1/requests/<id> — 앞에 https://oneport.kr 를 붙여 같은 API 키로 GETNode openai SDKerr.error?.status_url // /v1/requests/<id> — 앞에 https://oneport.kr 를 붙여 같은 API 키로 GETPython Anthropic SDKe.body["error"].get("status_url")Node Anthropic SDKerr.error?.error?.status_url - 새 기능
GET /v1/models 의 scheduled_change 에 차감 단가 예고
GET /v1/models · GET /v1/models/{id}
pricing.scheduled_change 에 차감 단가가 바뀌는 예고도 실려요 — 예전엔 새 단가 줄이 시작하는 날만 실려서, 「모델과 단가」 장 표의 「YYYY-MM-DD부터」엔 있고 이 칸엔 없는 예고가 있었어요. 이제 이 칸은 그 표와 같은 값이에요. from 은 오늘 뒤 단가가 바뀌는 가장 가까운 날이고, rates 엔 그날의 기본 구간 단가가 바뀌지 않는 줄까지 전부 dimension 이름 순으로 와요(예전엔 그날 시작하는 줄만). 이제 오늘 단가와 다를 때만 칸이 와요 — 예전엔 값이 같은 새 줄에도 칸이 있었어요.
Pythonm["pricing"].get("scheduled_change") # m = GET /v1/models 의 data 한 줄 — None 이면 예고 없음Nodem.pricing.scheduled_change // m = GET /v1/models 의 data 한 줄 — 없으면 예고 없음 - 바뀜
확인이 끝난 서비스를 다시 부탁하면 approved 줄로
POST /v1/connectors/requests · GET /v1/connectors
저희 확인이 끝난(approved) 서비스를 같은 지갑에서 다시 부탁하면 저희 확인을 다시 거치지 않아요. 같은 그룹의 키끼리(그룹 없는 키끼리) 지갑 관리자가 마지막으로 정한 뒤 낸 approved 부탁이 있으면 그 줄이 200 으로 오고, 없으면 state 가 approved 인 새 줄이 201 로 서서 지갑 관리자 차례로 가요. 예전엔 state 가 received 인 새 줄이 서서 저희 확인을 다시 기다렸어요. 지갑 관리자가 거절한 뒤 다시 부탁했으면 GET /v1/connectors 의 그 서비스 줄은 지갑 관리자가 다시 정할 때까지 pending 이에요.
Pythonr.json()["state"] == "approved" # r = requests 로 POST /v1/connectors/requests 한 200 · 201 응답Node(await res.json()).state === "approved" // res = fetch 로 POST /v1/connectors/requests 한 200 · 201 응답 - 바뀜
커넥터 목록의 link_url — 도구 칸 하나라도 needs_link 면
GET /v1/connectors
link_url 이 줄 state 가 needs_link 인 줄에만 오던 것에서, tools.read · tools.write 가운데 하나라도 needs_link 면 오도록 바뀌었어요. 그래서 읽기 도구를 다시 확인하는 동안 줄 state 가 unavailable 이어도, 쓰기 도구를 이을 수 있으면 link_url 이 와요. 이을 주소가 있는지는 줄 state 가 아니라 link_url 칸이 있는지로 보세요.
Pythonrow.get("link_url") # row = requests 로 GET /v1/connectors 한 r.json()["connectors"] 의 한 줄 — state 와 상관없이 봐요Noderow.link_url // row = fetch 로 GET /v1/connectors 한 (await res.json()).connectors 의 한 줄 — state 와 상관없이 봐요 - 바뀜
커넥터 부탁 문 403 에 connector_request_id
POST /v1/connectors/requests
403 connector_unavailable 의 error.reason 이 declined · not_verified 면 error.connector_request_id 가 와요 — 거절 · 막음이 적힌 그 서비스의 부탁 줄 id 예요. 그 id 로 GET /v1/connectors/requests/{id} 를 읽어 message(까닭)를 보세요. 예전엔 부탁 목록에서 그 서비스의 부탁을 찾아야 했어요. blocked_by_admin 엔 이 칸이 없어요.
Pythonr.json()["error"].get("connector_request_id") # r = requests 로 POST /v1/connectors/requests 한 403 응답Node(await res.json()).error?.connector_request_id // res = fetch 로 POST /v1/connectors/requests 한 403 응답 - 바뀜
커넥터 부탁 문 · 작업 문의 409 에 x-should-retry: false
POST /v1/connectors/requests · POST /v1/tasks
같은 Idempotency-Key 로 본문을 바꿔 보낸 409 idempotency_conflict 에 x-should-retry: false 가 붙어요 — 글 문의 409 와 같아요. 이 머리글을 따르는 SDK 는 이 409 를 스스로 다시 보내지 않아요(예전엔 재시도를 켜 둔 SDK 가 같은 409 를 두 번 더 받았어요). 처음 본문 그대로 보내거나, 다른 요청이면 새 값을 쓰세요.
Pythonr.headers.get("x-should-retry") == "false" # r = requests 로 POST 한 409 응답 — 다시 보내지 마세요Noderes.headers.get("x-should-retry") === "false" // res = fetch 로 POST 한 409 응답 — 다시 보내지 마세요 - 바뀜
영상 접수 때 저희 쪽 사정은 503 provider_unavailable
POST /v1/videos
저희 쪽 사정으로 그 모델에 만들기 요청을 넘기지 못하면 HTTP 200 · status failed · vendor_failed 대신 503 type api_error · error.reason provider_unavailable 로 와요 — 글 문 · 임베딩 문과 같아요. 그 작업은 요금 없이 닫혀요. Retry-After(2초)만큼 기다렸다 같은 Idempotency-Key · 같은 본문으로 다시 보내면 새로 만들어요. 예전엔 vendor_failed 라 보내신 글이나 그림 문제처럼 보였어요.
Python openai SDKe.body.get("reason") == "provider_unavailable"Node openai SDKerr.error?.reason === "provider_unavailable" - 바뀜
영상 error.message · 큰 Claude 모델 402 문장이 바뀌었어요
POST /v1/videos · GET /v1/videos · GET /v1/videos/{id} · POST /v1/chat/completions · /v1/messages
영상 작업이 실패했을 때의 error.message 문장과, claude-sonnet-5 · claude-opus-5-5 를 스트림 없이 불러 미리 잡는 몫이 큰 402 문장이 바뀌었어요 — 402 문장 끝엔 스트림을 못 쓰는 도구에서 할 일이 한 줄 더 붙어요. error.code · error.reason · 상태 코드는 그대로라 그걸로 가른 코드는 그대로 돼요. 문장을 통째로 견주던 코드는 영상이면 error.code, 402 면 error.reason 으로 바꿔 주세요(402 문장의 「stream: true 로 보내거나」 낱말은 그대로 있어요).
Python openai SDKe.body.get("reason") == "hold_exceeds_balance"Node openai SDKerr.error?.reason === "hold_exceeds_balance"Python Anthropic SDKe.body["error"].get("reason") == "hold_exceeds_balance"Node Anthropic SDKerr.error?.error?.reason === "hold_exceeds_balance" - 바뀜
키 월 한도가 아직 끝나지 않은 호출 몫까지 세요
POST /v1/chat/completions · /v1/messages · /v1/responses · /v1/embeddings · /v1/images/* · /v1/audio/* · POST /v1/videos · POST /v1/tasks
키의 이번 달 한도를 잴 때 쓴 몫과 이 요청의 최대 몫에 더해, 같은 키로 보내 아직 끝나지 않은 호출이 잡아 둔 몫까지 세요. 그래서 같은 키로 여러 요청을 한꺼번에 보내면 넘게 되는 요청이 429 key_limit_exceeded 로 와요 — 예전엔 동시에 보낸 만큼 한도를 넘어 받을 수 있었어요. 지금도 앞 호출이 끝나 정산되는 순간과 겹치면 그 호출 하나 몫쯤 넘을 수 있어요. type · 상태 코드는 그대로예요 — 이 경우의 error.reason · 머리글은 같은 날 아래 줄 「키 월 한도 429 에 error.reason held_by_others」예요. 하루 한도 · 강좌 몫은 예전 그대로예요.
Python openai SDKe.body.get("reason") == "key_month_limit" # 기다려도 같아요 — 줄이거나 한도를 올려요 · "held_by_others" 면 Retry-After(5초) 뒤 그대로(아래 줄)Node openai SDKerr.error?.reason === "key_month_limit" // 기다려도 같아요 — 줄이거나 한도를 올려요 · "held_by_others" 면 Retry-After(5초) 뒤 그대로(아래 줄)Python Anthropic SDKe.body["error"].get("reason") == "key_month_limit" # 기다려도 같아요 — 줄이거나 한도를 올려요 · "held_by_others" 면 Retry-After(5초) 뒤 그대로(아래 줄)Node Anthropic SDKerr.error?.error?.reason === "key_month_limit" // 기다려도 같아요 — 줄이거나 한도를 올려요 · "held_by_others" 면 Retry-After(5초) 뒤 그대로(아래 줄) - 바뀜
키 월 한도 429 에 error.reason held_by_others — 아직 끝나지 않은 호출 몫
POST /v1/chat/completions · /v1/messages · /v1/responses · /v1/embeddings · /v1/images/* · /v1/audio/* · POST /v1/videos · POST /v1/tasks
같은 키로 아직 끝나지 않은 호출이 잡아 둔 몫까지 더해서 이번 달 한도를 넘는 429 key_limit_exceeded 의 error.reason 이 key_month_limit 대신 "held_by_others" 로 와요. 그 호출이 끝나면 풀리는 갈래라 x-should-retry: true 와 Retry-After: 5 가 붙어요 — 그만큼 기다렸다 같은 요청을 그대로 보내세요(SDK 재시도를 켜 두셨으면 SDK 가 다시 보내요 · SDK 동작). 402 의 held_by_others 와 할 일은 같아요(기다렸다 그대로) — 다만 429 는 이 키로 보낸 호출 몫이고, 402 는 같은 지갑의 다른 키까지 포함한 호출 몫이에요. key_month_limit 은 이제 쓴 몫과 이 요청 몫만으로 넘은 경우라 기다려도 안 풀려요 — 줄이거나 한도를 올리거나 month.resets_at 까지예요(x-should-retry: false 그대로). 예전엔 둘 다 key_month_limit · x-should-retry: false 라, 기다리면 풀릴 때도 줄이라는 응답이었어요. type · 상태 코드는 그대로예요. 작업 문(POST /v1/tasks)은 처음 응답에만 와요.
Python openai SDKe.body.get("reason") == "held_by_others" # 429 · 402 둘 다 — Retry-After 뒤 그대로Node openai SDKerr.error?.reason === "held_by_others" // 429 · 402 둘 다 — Retry-After 뒤 그대로Python Anthropic SDKe.body["error"].get("reason") == "held_by_others"Node Anthropic SDKerr.error?.error?.reason === "held_by_others" - 바뀜
GET /v1/models — 키 머리글을 실었는데 못 읽으면 401
GET /v1/models · GET /v1/models/{id}
Authorization 이나 x-api-key 머리글을 실었는데 키를 못 읽으면(「Bearer 」가 빠짐 · 빈 값) 다른 문과 같은 401 authentication_error(error.reason "missing_key")가 와요. 예전엔 키 없는 요청으로 읽혀 200 전체 목록이 와서, 연결 확인은 통과하고 첫 호출에서야 401 이었어요. 두 머리글이 다 없을 때만 예전처럼 키 없는 전체 목록이에요.
Python openai SDKe.body.get("reason") == "missing_key" # client.models.list() 의 AuthenticationErrorNode openai SDKerr.error?.reason === "missing_key" // client.models.list() 의 AuthenticationErrorPython Anthropic SDKe.body["error"].get("reason") == "missing_key"Node Anthropic SDKerr.error?.error?.reason === "missing_key" - 바뀜
키 실은 GET /v1/models — 월 한도로 한 장도 못 만드는 그림 모델은 빠져요
GET /v1/models · GET /v1/models/{id}
키를 실어 부른 목록에서, 그림 한 장에 먼저 잡는 몫이 그 키의 월 한도보다 큰 그림 모델이 빠져요(같으면 남아요). 예전엔 목록에 있어도 부르면 늘 429 key_limit_exceeded(key_month_limit)였어요. 이번 달에 쓴 몫이 아니라 한도 자체로 재서, 다음 달이면 부를 수 있는 모델은 남아요. 한도 없는 키, 하루 한도, 글 · 영상 모델은 예전 그대로라 목록에 있어도 429 일 수 있어요. 빠진 모델은 「한도 수정」에서 월 한도를 올리면 다시 나와요.
Python[m["id"] for m in models["data"] if m["kind"] == "image"] # 키를 실어 받은 목록 — 여기 없는 그림 모델은 이 키로 못 불러요Nodemodels.data.filter((m) => m.kind === "image").map((m) => m.id) // 키를 실어 받은 목록 — 여기 없는 그림 모델은 이 키로 못 불러요 - 바뀜
저장된 응답 · 대화를 id 로 부르는 칸은 400
POST /v1/responses · /v1/messages
previous_response_id · conversation · input 의 item_reference 항목 · id 가 있는 prompt(/v1/responses)와 id 가 있는 container(/v1/messages)를 실으면 벤더로 보내기 전에 400 invalid_request_error 가 와요 — 문장이 받지 않는 칸 이름을 알려 주고 요금은 없어요. 값이 null 이거나 id 없는 새 container 설정은 그대로 지나가요. 예전엔 그대로 넘겨 모델 회사의 답을 돌려드렸어요. 앞 대화는 앞 턴의 output 항목을 input 에 다시 실어 이어가세요(「Responses」 장 「대화 이어가기」).
Python openai SDKbody.pop("previous_response_id", None); body.pop("conversation", None) # 앞 턴 output 항목을 input 에 다시 실어요Node openai SDKdelete body.previous_response_id; delete body.conversation // 앞 턴 output 항목을 input 에 다시 실어요Python Anthropic SDKbody.pop("container", None) # id 없는 새 container 설정은 그대로 돼요Node Anthropic SDKdelete body.container // id 없는 새 container 설정은 그대로 돼요 - 바뀜
Responses 의 file_search 도구는 400
POST /v1/responses
tools 에 file_search 를 실으면 벤더로 보내기 전에 400 으로 돌려드려요(요금 없음). 파일 검색에 쓸 저장소를 만드는 문이 없어서예요 — 찾아볼 문서는 input 에 글로 실어 보내세요. 예전엔 그대로 넘기고 검색 1회마다 수수료를 셌어요.
Pythonbody["tools"] = [t for t in body.get("tools", []) if t.get("type") != "file_search"]Nodebody.tools = (body.tools ?? []).filter((t) => t.type !== "file_search") - 고침
키를 잠깐 못 읽은 503 — 키 실은 요청 · 상태 조회도 기다렸다 다시 보내는 신호로
POST /v1/chat/completions · /v1/messages · /v1/responses · GET /v1/requests/{id}
API 키를 잠깐 못 읽은 503 api_error(「지금 이 키를 확인하지 못했어요」)가 Idempotency-Key 를 실은 글 문 요청과 상태 조회에도 Retry-After: 2 와 함께 오고 x-should-retry 는 붙지 않아요 — SDK 가 그만큼 기다렸다 다시 보내요. 접수 전이라 같은 키 · 같은 본문으로 다시 보내도 두 번 실행되지 않아요. 예전엔 이 두 곳에서만 x-should-retry: false 가 붙고 Retry-After 가 없어 SDK 가 멈췄어요.
Pythone.response.headers.get("retry-after")Nodeerr.headers?.get("retry-after") - 고침
작업 문 held 응답에 Location 없음 · 시작 못 한 작업은 같은 키로 다시
POST /v1/tasks
다른 호출이 잡아 둔 몫 때문에 시작하지 않은 응답(402 · 429, error.reason held_by_others · Retry-After: 5 — 429 면 x-should-retry: true 도)에 Location 이 오지 않아요. 그 접수는 같은 Idempotency-Key 로 다시 보내면 새로 시작해서 조회할 일이 없어요 — 예전엔 Location 을 조회하면 충전 · 새 키를 말하는 반대 문장이 왔어요. 드물게 그 거절이 키에 남으면 error.reason · Retry-After 없이 와요 — 그땐 그 호출이 끝난 뒤 새 Idempotency-Key 로 보내세요(「작업」 장 표). 그리고 failed · error.code not_started 로 닫힌 작업을 같은 키 · 같은 본문으로 다시 보내면 이제 늘 새로 시작해요 — 예전엔 같은 failed 만 돌아오는 때가 있었어요.
Pythonr.json()["error"].get("reason") == "held_by_others" and "retry-after" in r.headers # r = requests 로 POST /v1/tasks 한 402 · 429 — 같은 키로 다시Node(await res.json()).error?.reason === "held_by_others" && res.headers.has("retry-after") // res = fetch 로 POST /v1/tasks 한 402 · 429 — 같은 키로 다시 - 고침
다른 호출이 잡아 둔 몫 402 — 글 문도 한 줄 문장으로
POST /v1/chat/completions · /v1/messages · /v1/responses
같은 지갑의 다른 호출이 잡아 둔 몫 때문인 402(error.reason held_by_others)의 문장이 그림 · 영상 · 임베딩 · 소리 문과 같은 한 줄이 됐어요 — 「크레딧이 부족해요. 다만 지금은 잔액이 모자란 게 아니라 같은 지갑의 다른 호출이 잡아 둔 몫 때문이에요 — 그 호출이 끝나면 풀리니 잠시 뒤 그대로 다시 보내 주세요(요금은 안 붙었어요).」 예전엔 「이 요청에 필요한 크레딧이 부족해요(최대 …)」와 충전 · 줄이기 안내 뒤에 이 한 줄이 붙어, 반대 지시 둘이 한 칸에 같이 왔어요. error.reason · type · 상태 코드 · Retry-After: 5 는 그대로예요. 문장 머리로 가르던 코드는 「크레딧이 부족해요.」만 보고 충전으로 가르지 말고, error.reason 이나 「다른 호출이 잡아 둔 몫」 낱말을 먼저 보세요.
Python openai SDKe.body.get("reason") == "held_by_others"Node openai SDKerr.error?.reason === "held_by_others"Python Anthropic SDKe.body["error"].get("reason") == "held_by_others"Node Anthropic SDKerr.error?.error?.reason === "held_by_others" - 고침
임베딩 429 rate_limit_error 에 Retry-After
POST /v1/embeddings
모델 회사가 잠시 조여 온 429 rate_limit_error 에 Retry-After(정수 초)가 와요 — 글 문 · 토큰 세기 문과 같아요. 모델 회사가 알려 준 값을 1~60초로 맞추고, 값이 없으면 5초예요. 그만큼 기다렸다 그대로 다시 보내세요. 예전엔 이 429 에 없어서 간격을 직접 정해야 했어요.
Pythone.response.headers.get("retry-after")Nodeerr.headers?.get("retry-after") - 고침
상품 조회 MCP 서버(/mcp)를 브라우저 페이지에서 부르면 CORS 머리글
POST /mcp · OPTIONS /mcp(스쿨오더 · 오피스오더 상품 조회 서버)
Origin 이 schoolorder.kr · officeorder.co.kr · localhost · 127.0.0.1 인 브라우저 페이지에서 부르면 응답에 Access-Control-Allow-Origin(그 페이지의 Origin 그대로)이 와요. 사전 요청(OPTIONS)도 204 로 받아요. 예전엔 서버가 받아도 이 머리글이 없어 브라우저가 답을 막았어요. Origin 을 안 보내는 데스크톱 · 터미널 MCP 도구는 예전 그대로이고, 다른 사이트의 페이지는 예전처럼 막혀요 — 브라우저에선 fetch 가 TypeError 로 끝나고, 403 origin not allowed 는 Origin 을 직접 실은 요청(curl 등)에만 보여요(「MCP 서버」 장).
- 고침
영상 접수 때 안전 기준 거절은 moderation_blocked 로
POST /v1/videos
grok-imagine-video 계열에 만들기를 보냈는데 AI 회사가 접수 때 안전 기준으로 거절하면, HTTP 200 · status failed 에 error.code 가 vendor_failed 로 오는 일이 있었어요 — 이제 moderation_blocked 로 와요. 그대로 다시 보내지 말고 글이나 그림을 바꿔 주세요. 반대로 안전 기준과 상관없는 접수 거절 몇 가지가 moderation_blocked 로 오던 것은 vendor_failed 로 와요. 두 경우 모두 예전처럼 요금이 안 붙어요.
Pythonvideo["error"]["code"] == "moderation_blocked" # video = requests 로 POST /v1/videos 한 r.json() · status 가 failed 일 때Nodevideo.error?.code === "moderation_blocked" // video = fetch 로 POST /v1/videos 한 await res.json() · status 가 failed 일 때 - 고침
영상 조회 · 받기를 못 읽으면 404 대신 503
GET /v1/videos/{id} · GET /v1/videos/{id}/content · POST /v1/videos
저희가 작업을 잠깐 못 읽으면 조회 · 받기가 404 not_found_error 로 오던 것을 503 api_error 로 고쳤어요 — 404 는 이제 저희가 못 읽어서 오는 일이 없어요. 조회(GET /v1/videos/{id})의 404 는 그 id 가 이 키로 볼 수 있는 영상이 아니라는 뜻이에요. 받기의 404 는 만들지 못했거나 지운 영상에도 와요. 503 이면 잠시 뒤 같은 주소를 다시 불러요 — 새 Idempotency-Key 로 다시 만들면 앞 영상도 끝나는 대로 값이 매겨져요. 만들기 응답으로 드물게 오던 502 api_error 도 503 api_error 로 와요 — 같은 Idempotency-Key 로 다시 보내면 그 작업이 와요.
Pythonr.status_code == 503 # r = requests 로 GET /v1/videos/{id} 한 응답 — 404 와 갈라 잠시 뒤 다시Noderes.status === 503 // res = fetch 로 GET /v1/videos/{id} 한 응답 — 404 와 갈라 잠시 뒤 다시 - 고침
영상 내용 거절은 접수 때든 만드는 중이든 moderation_blocked
POST /v1/videos · GET /v1/videos/{id}
AI 회사가 저작권 · 실존 인물 같은 내용으로 거절한 영상이 모델에 따라 error.code vendor_failed 로 오던 것을 moderation_blocked 로 고쳤어요 — 접수 때든 만드는 중이든 같아요. vendor_failed 는 다시 보내 볼 수 있는 실패지만 moderation_blocked 는 같은 내용으로 다시 보내도 같으니, 글이나 그림을 고쳐서 보내세요.
Pythonvideo["error"]["code"] == "moderation_blocked" # video = requests 로 GET /v1/videos/{id} 한 r.json() · status 가 failed 일 때Nodevideo.error?.code === "moderation_blocked" // video = fetch 로 GET /v1/videos/{id} 한 await res.json() · status 가 failed 일 때 - 고침
지갑 거절 뒤 다시 부탁한 줄엔 옛 거절을 싣지 않아요
POST /v1/connectors/requests · GET /v1/connectors/requests · GET /v1/connectors/requests/{id}
지갑 관리자가 거절한 뒤 다시 부탁해 선 approved 새 줄에 옛 거절 한 줄(message)과 wallet_decision declined 가 바로 붙어, 방금 낸 부탁이 또 거절된 것처럼 보이던 것을 고쳤어요. 이제 그 줄은 지갑 관리자가 다시 정하기 전까지 wallet_decision 이 null 이고 message 가 없어요. 그 앞의 옛 부탁 줄엔 지갑의 지금 결정이 와요 — 거절 그대로면 declined 와 그 한 줄이고, 지갑 관리자가 다시 정하면 옛 줄도 같이 바뀌어요.
Pythonq.get("wallet_decision") is None # q = requests 로 GET /v1/connectors/requests/{id} 한 r.json() — 다시 부탁한 approved 줄Nodeq.wallet_decision === null // q = fetch 로 GET /v1/connectors/requests/{id} 한 await res.json() — 다시 부탁한 approved 줄 - 고침
영상 접수 거절을 저희가 못 적었을 때 — 그 자리에서 다시 적고, 두 번 잇달아 못 적으면 503
POST /v1/videos
AI 회사가 접수 때 거절했는데 저희가 그 결과를 적지 못하면, 작업이 200 queued 로 남았다가 45분 뒤 vendor_failed 로 닫히던 것을 고쳤어요. 이제 그 자리에서 한 번 더 적어서, 적히면 HTTP 200 · status failed(error.code vendor_failed · 내용 거절이면 moderation_blocked)로 오고 잡아 둔 몫도 바로 풀려요 — 같은 Idempotency-Key 로 다시 보내면 그 실패 작업이 오니 다시 만들 땐 새 값을 써요. 두 번 잇달아 못 적었을 때만 503 「잠시 후 다시 시도해주세요.」가 오고, 같은 Idempotency-Key · 같은 본문으로 다시 보내면 새로 만들어요(남은 줄은 요금 없이 닫혀요).
Pythonvideo["status"] == "failed" # video = requests 로 POST /v1/videos 한 r.json() — 503 이면 같은 Idempotency-Key 로 다시 보내요Nodevideo.status === "failed" // video = fetch 로 POST /v1/videos 한 await res.json() — 503 이면 같은 Idempotency-Key 로 다시 보내요
2026-10-29#
- 새 기능
GET /v1/credits · /v1/key · /v1/keys 에 wallet_id
GET /v1/credits · GET /v1/key · GET /v1/keys(목록)
세 응답의 맨 바깥(최상위) 칸에 wallet_id 가 와요 — 그 키가 속한 지갑의 식별자예요(wal_ 로 시작하는 문자열 — 꼴이나 길이에 기대지 말고 같은지만 견주세요). 같은 지갑이면 세 문이 늘 같은 값을 줘요. 키 여러 개(sk-so- 키 · 스페이스 강좌 키 · 관리 키 sk-som-)를 한 스크립트에서 쓸 땐 이 값이 같은지 먼저 보세요 — 다르면 다른 지갑이에요. 저희 안쪽 지갑 번호가 아니고 그 번호로 되짚을 수도 없어요 — 견주는 데만 쓰세요. GET /v1/keys 는 목록 봉투 맨 바깥에 하나만 와요(줄마다 오지 않고, 만들기 · 하나 보기 · 고치기 · 끄기 응답엔 없어요). 예전엔 어느 지갑인지 알려 주는 칸이 없어서 다른 지갑의 값이 오류 없이 섞였어요.
Pythonr.json()["wallet_id"] # r = requests 로 GET /v1/credits · /v1/key · /v1/keys 한 응답Node(await res.json()).wallet_id // res = fetch 로 GET /v1/credits · /v1/key · /v1/keys 한 응답 - 새 기능
토큰 세기 문 429 rate_limit_error 에 Retry-After
POST /v1/messages/count_tokens
429 rate_limit_error 에 Retry-After(정수 초)가 와요 — /v1/messages 와 같아요. 모델 회사가 알려 준 값을 1~60초로 맞추고, 값이 없으면 5초예요. 그만큼 기다렸다 그대로 다시 보내세요. 예전엔 이 문의 429 에만 없었어요.
Pythone.response.headers.get("retry-after")Nodeerr.headers?.get("retry-after") - 새 기능
429 group_limit_exceeded 에 error.reason
POST /v1/chat/completions · /v1/messages · /v1/responses · /v1/embeddings · /v1/images/* · /v1/audio/* · POST /v1/videos · POST /v1/tasks
강좌 몫 429 group_limit_exceeded 에 error.reason 이 와요. group_budget 은 쓴 몫과 이 요청의 최대 몫을 더한 값이 강좌 몫을 넘은 거예요 — GET /v1/key 의 group.remaining_credits 가 0 보다 크면 출력 상한이나 묶음을 줄여 보세요. group_closed 는 관리자가 강좌를 닫은 것, group_not_started 는 반 시작일 전, group_ended 는 반 기간이 끝난 거예요 — 이 셋은 줄여도 안 풀려요. 예전엔 이 갈래를 문장 낱말(「그룹이 닫혀」 · 「…부터 쓸 수 있어요」 · 「… 기간이 …에 끝났어요」)로만 갈랐어요. POST /v1/tasks 는 처음 응답에만 와요. type · status · 문장과 x-should-retry: false 는 그대로예요. 1인 몫 429(group_member_limit_exceeded)엔 없어요.
Python openai SDKe.body.get("reason") == "group_budget"Node openai SDKerr.error?.reason === "group_budget"Python Anthropic SDKe.body["error"].get("reason") == "group_budget"Node Anthropic SDKerr.error?.error?.reason === "group_budget" - 바뀜
스페이스 전송 정책 content_limit 거절 문장 — 「나눠서 보내 주세요」
POST /v1/chat/completions · /v1/messages · /v1/responses · /v1/messages/count_tokens · /v1/embeddings
스페이스 전송 정책을 켠 키로 한 요청에 너무 많은 글을 담아 온 400 space_content_policy(error.code content_limit)의 문장이 「한 번에 보내는 글이 너무 많아요. 나눠서 보내 주세요.」로 바뀌었어요. 예전엔 다른 정책 거절과 같은 「…설정을 확인한 뒤 다시 시도해 주세요.」라 설정을 고칠 일처럼 읽혔어요 — 이 거절은 대개 대화 기록 · 붙인 글 · 임베딩 묶음을 나눠서 보내면 풀려요(도구 정의처럼 너무 깊게 겹친 JSON 도 같은 code 라, 그땐 구조를 얕게 해 주세요). status 400 · type · code 는 그대로예요. 문장 말고 error.code 로 가르세요. 다른 code(blocked_term · invalid_content 등)의 문장은 그대로예요.
Python openai SDKe.body.get("code") == "content_limit"Node openai SDKerr.error?.code === "content_limit"Python Anthropic SDKe.body["error"].get("code") == "content_limit"Node Anthropic SDKerr.error?.error?.code === "content_limit" - 바뀜
요청 상태 조회의 정산 금액 — settlement.chargedNano 대신 settlement.cost_credits
GET /v1/requests/{id}
settlement.chargedNano 칸이 없어지고 settlement.cost_credits 가 와요. 처음 응답 usage.cost_credits 와 같은 이름 · 같은 자예요. settled 면 그 값, not_charged 면 0, 정산이 unknown 이면 null 이에요 — null 을 0 으로 읽지 마세요. chargedNano 를 읽던 코드는 이 칸으로 바꿔 주세요.
Pythonstatus["settlement"]["cost_credits"] # status = requests 로 GET 한 r.json()Nodestatus.settlement.cost_credits // status = fetch 로 GET 한 await res.json() - 바뀜
임베딩에서 저희 쪽 사정으로 못 보낸 요청은 503 provider_unavailable
POST /v1/embeddings
모델 회사가 저희 쪽 사정으로 임베딩 요청을 받지 않으면 503 type api_error · error.reason provider_unavailable 로 와요 — 글 문과 같아요. 예전엔 모델 회사가 준 상태에 따라 429 rate_limit_error · 400 invalid_request_error 등으로 와서 보내신 키나 요청 문제처럼 보였어요. 요금은 안 붙어요 — Retry-After(2초)만큼 기다렸다 그대로 다시 보내세요. 모델 회사가 잠시 조인 429 rate_limit_error 와 요청이 틀린 400 은 예전 그대로예요.
Python openai SDKe.body.get("reason") == "provider_unavailable"Node openai SDKerr.error?.reason === "provider_unavailable" - 바뀜
접수하는 사이 꺼진 API 키는 401 authentication_error
POST /v1/chat/completions · /v1/messages · /v1/responses (Idempotency-Key 를 실었을 때)
Idempotency-Key 를 실은 요청을 접수하는 사이 API 키가 꺼졌거나 기한이 끝났거나 지갑이 멈췄으면 키 확인 단계와 같은 401 authentication_error 로 와요. 문장이 이유를 말해요(키를 잠깐 못 읽었으면 503 api_error 「지금 이 키를 확인하지 못했어요」예요). 예전엔 403 authentication_error 에 「요청 상태를 확인하지 못했어요 …」 문장이었어요. 조회 주소가 없고 접수도 안 된 건 그대로예요. 403 authentication_error 를 따로 가르던 코드는 401 로 합쳐 주세요.
Pythone.status_code == 401Nodeerr.status === 401 - 바뀜
「새로 받기」로 바꾼 키도 접수 · 조회가 이어져요
POST /v1/chat/completions · /v1/messages · /v1/responses · GET /v1/requests/{id} (Idempotency-Key 를 실었을 때)
대시보드 키 목록 · 스페이스 강좌 키 · 개인 API 키의 「새로 받기」로 키를 바꾸면 옛 API 키로 받은 Idempotency-Key 접수가 새 API 키로 옮겨 가요. 옛 API 키로 받은 x-gateway-status-url 을 새 API 키로 GET 하면 읽히고, 같은 Idempotency-Key 를 새 API 키로 보내도 같은 접수로 묶여요(처리 중이거나 끝났으면 409 request_already_exists 와 조회 주소). 예전엔 새 API 키로 조회하면 404 「요청을 찾을 수 없어요」였고, 같은 Idempotency-Key 도 새 요청으로 실행됐어요. 「폐기」만 하거나 따로 만든 키로 바꾸면 예전 그대로예요 — 폐기하기 전에 결과를 모르는 접수를 조회해 두세요. 2026-10-29 전에 「새로 받기」한 키의 접수는 옮겨지지 않았어요 — 그 접수를 봐야 하면 요청 ID 와 부른 시각을 admin@schoolorder.kr 로 알려 주세요.
- 고침
「새로 받기」 순간 처리 중이던 Idempotency-Key 요청도 끝이 적혀요
POST /v1/chat/completions · /v1/messages · /v1/responses · GET /v1/requests/{id} (Idempotency-Key 를 실었을 때)
「새로 받기」를 누르는 순간 옛 API 키로 처리 중이던 요청도 끝이 적혀요. 모델 회사로 보낸 요청은 새 API 키로 조회하면 결과(result)와 정산(settlement · cost_credits)이 읽혀요. 실행 전에 막힌 요청(402 held_by_others · 429 등)은 풀려서, 같은 Idempotency-Key 를 새 API 키로 보내면 새로 실행돼요. 접수만 되고 모델 회사로 보내기 전이었던 요청은 실행되지 않아요 — 조회는 execution not_started · result.state failed · settlement.state not_charged 이고, 같은 Idempotency-Key 를 새 API 키로 다시 보내면 새로 실행돼요. 예전엔 보낸 요청의 http_status 가 비어 있었고, 보낸 뒤 실행 전에 거절된 요청(429 rate_limit_error 등)은 started 로 남아 같은 Idempotency-Key 에 409 request_already_exists 가 계속 왔어요. 보내기 전이던 접수와 402 held_by_others 는 15분 동안 claimed 로 남았어요.
Pythonstatus["execution"] == "not_started" # status = requests 로 새 API 키로 GET 한 r.json()Nodestatus.execution === "not_started" // status = fetch 로 새 API 키로 GET 한 await res.json() - 고침
/v1/tasks 작업도 「새로 받기」를 따라가요 · 끈 키의 열린 작업은 멈춰요
POST /v1/tasks · GET · DELETE /v1/tasks/{id} · GET /v1/tasks/{id}/events · GET /v1/tasks/{id}/files/{fileId}
대시보드 키 목록 · 스페이스 강좌 키 · 개인 API 키의 「새로 받기」로 키를 바꾸면 옛 API 키로 맡긴 작업도 새 API 키로 옮겨 가요. 새 API 키로 조회 · 이벤트 · 취소 · 파일 받기를 하고, 같은 Idempotency-Key 를 새 API 키로 보내면 같은 작업이 와요. 키마다 3건 상한과 정산도 새 API 키에서 이어져요. 스페이스 개인 키를 다른 강좌의 키로 새로 받으면 그 작업은 옮겨 가지 않아요. 맡긴 키를 폐기했거나 기한이 끝났으면 열린 작업은 5분마다 도는 정리 때 멈추고, 쓴 만큼만 정산돼 cancelled 로 닫혀요. 예전엔 새 API 키로 GET · DELETE 하면 404 「작업을 찾을 수 없어요」였고, 키를 꺼도 작업은 끝날 때까지(길어야 24시간 · budget 까지) 돌았어요.
- 고침
받아쓰기 gpt-4o 계열의 prompt 는 글 입력 단가로 셈해요
POST /v1/audio/transcriptions · GET /v1/models
gpt-4o-transcribe · gpt-4o-mini-transcribe 에 prompt 를 실으면 그 글 토큰(usage.input_token_details.text_tokens)을 GET /v1/models 의 input_text 단가로 셈해요. 두 모델 줄의 pricing.rates 에 input_text 줄(100만 토큰당 · input_audio 와 같은 값)이 생기고, pricing.input_credits_per_mtok 도 와요. 부를 때 잡는 몫에도 prompt 글 몫이 더해져요. whisper-1 · gpt-transcribe 와 다른 회사 받아쓰기 모델은 그대로예요.
- 고침
실행 전에 막힌 Idempotency-Key 요청은 같은 키로 다시 받아요
POST /v1/chat/completions · /v1/messages · /v1/responses
Idempotency-Key 를 실은 요청이 접수된 뒤 모델이 실행하기 전에 막혀 끝나면(402 held_by_others · 429 rate_limit_error · 503 api_error · 529 등) 원래 Retry-After · x-should-retry 를 그대로 받아요. 그만큼 기다렸다 같은 키 · 같은 본문으로 다시 보내면 409 가 아니라 새 요청 ID 로 다시 접수돼 실행돼요. 504 error.reason "timeout" 은 키를 실어도 예전처럼 x-should-retry: false 이고 Retry-After 없이 와요 — 잠시 뒤 같은 키 · 같은 본문으로 한 번 다시 보내면 새로 접수돼요 — 고칠 코드와 따로, status 504 · error.reason "timeout" 으로 가려 한 번만 보내요. 529 · 그 밖의 5xx upstream_error 는 Retry-After 가 없을 수 있어요 — 그땐 간격을 직접 두고 같은 키로 보내세요. 예전엔 실패마다 x-should-retry: false 가 붙고 Retry-After 가 빠졌고, 같은 키는 늘 409 라 새 키로 보내야 했어요. 접수 전에 막힌 503 api_error(「지금 이 키를 확인하지 못했어요」)는 예전대로 x-should-retry: false · Retry-After 없이 와요(2026-10-30 부터는 Retry-After: 2 · x-should-retry 없음이에요 — 위 그날 줄) — 잠시 뒤 같은 키 · 같은 본문으로 다시 보내요. 400 · 403 · 404 · 그 밖의 402 도 같은 키로 다시 접수되지만 그대로 보내면 같은 실패라 고친 요청을 새 키로 보내세요. 모델 회사가 요청을 거절한 4xx upstream_error 와 결과를 모르는 422 는 예전대로 x-should-retry: false 이고 같은 키는 409 예요.
Pythone.response.headers.get("x-should-retry") != "false" and (e.response.headers.get("retry-after") is not None or e.status_code >= 500)Nodeerr.headers?.get("x-should-retry") !== "false" && (err.headers?.get("retry-after") != null || (err.status ?? 0) >= 500) - 고침
409 request_already_exists 에도 x-gateway-status-url
POST /v1/chat/completions · /v1/messages · /v1/responses
같은 Idempotency-Key · 같은 본문으로 다시 보내 받은 409 에도 x-gateway-status-url 머리글이 와요(Location · error.status_url 과 같은 값). 예전엔 이 409 에만 없어서 Location 을 따로 읽어야 했어요. 이제 조회 주소는 이 머리글 하나로 읽으면 돼요.
Pythone.response.headers.get("x-gateway-status-url")Nodeerr.headers?.get("x-gateway-status-url") - 고침
실행 전에 막힌 접수의 조회 — result failed · settlement not_charged
GET /v1/requests/{id}
execution 이 not_started 인 접수(모델이 실행하기 전에 막혀 끝난 요청)의 조회가 result.state failed · settlement.state not_charged(cost_credits 0)로 와요. 예전엔 둘 다 unknown 으로 남았어요. 이 접수는 같은 키 · 같은 본문으로 다시 보내면 새 요청 ID 로 다시 접수돼요.
- 고침
시작 기록 없는 접수 · 15분 넘게 멈춘 접수도 같은 키로 다시 받아요
POST /v1/chat/completions · /v1/messages · /v1/responses · GET /v1/requests/{id}
Idempotency-Key 를 실은 요청이 모델 회사로 보내기 전에 저희 쪽에서 실패하면 접수를 unknown 이 아니라 not_started 로 적어요. 조회는 result.state failed · settlement.state not_charged(cost_credits 0)로 오고, 같은 키 · 같은 본문으로 다시 보내면 새 요청 ID 로 다시 접수돼 실행돼요. 접수만 되고 시작하지 않은 채 멈춘 접수(claimed)도 접수 뒤 15분(900초)이 지나면 같은 모양으로 읽히고 같은 키로 다시 받아요. 예전엔 둘 다 같은 키가 계속 409 request_already_exists 였어요. 이 실패의 처음 응답엔 예전처럼 x-should-retry: false 가 붙어요 — 조회해서 execution 이 not_started 면 같은 키로 보내세요. 모델 회사로 보내기 시작한 뒤 끊긴 접수는 예전대로 unknown 이고 같은 키는 409 예요.
Pythonstatus["execution"] == "not_started" # status = requests 로 GET 한 r.json()Nodestatus.execution === "not_started" // status = fetch 로 GET 한 await res.json() - 고침
임베딩 오류 봉투 — held_by_others · Retry-After · 정책 code · 요청 ID · 구글 usage
POST /v1/embeddings
다른 호출이 잡아 둔 몫 402 에 error.reason held_by_others 와 Retry-After: 5 가 와요(문장 「다른 호출이 잡아 둔 몫」은 그대로) — 글 문과 같아요. 503 api_error 엔 Retry-After: 2 가 와요. 스페이스 전송 정책 거절(type space_content_policy)의 error.code 가 세부 이유(content_limit · blocked_term 등)예요 — 묶음이 커서 온 content_limit 은 묶음을 나누면 풀려요. 잔액을 잡은 뒤의 호출은 응답의 요청 ID(request-id · error.request_id)가 저희 사용 기록과 같은 값이라 문의할 때 그 값으로 찾아요. 구글 임베딩 모델 응답에도 usage.prompt_tokens · total_tokens 가 와요 — 저희가 구글의 토큰 세기로 세어 차감한 수예요. 예전엔 402 에 reason · Retry-After 가 없었고, 정책 거절의 code 도 space_content_policy 였고, 구글 응답엔 usage 가 없었어요.
Python openai SDKe.body.get("reason") == "held_by_others"Node openai SDKerr.error?.reason === "held_by_others" - 고침
접수 기록을 못 쓴 503 request_store_unavailable 에 Retry-After: 2
POST /v1/chat/completions · /v1/messages · /v1/responses (Idempotency-Key 를 실었을 때)
조회 주소 없이 온 503 request_store_unavailable 에 Retry-After: 2 가 오고 x-should-retry 는 빠졌어요. 문장도 「요청 상태를 확인하지 못했어요. 같은 Idempotency-Key를 유지해 주세요.」예요. 2초 기다렸다 같은 키 · 같은 본문으로 다시 보내세요 — 그사이 접수됐으면 409 와 조회 주소가, 안 됐으면 새 접수가 돼요. 예전엔 x-should-retry: false 에 「기존 요청을 조회해 주세요」 문장이었는데 조회할 주소가 없었어요. error.type 도 request_store_unavailable 이에요(api_error 아님 · 2026-10-31 줄).
Pythone.response.headers.get("retry-after")Nodeerr.headers?.get("retry-after") - 고침
영상 문 402 에도 error.reason · 503 에 Retry-After: 2
POST /v1/videos
영상 만들기 402 insufficient_credit 에 다른 문처럼 error.reason 이 와요. 같은 지갑의 다른 호출이 잡아 둔 몫이면 held_by_others 이고 Retry-After: 5 가 붙어요 — 그만큼 기다렸다 그대로 다시 보내세요. 이 영상 몫이 잔액보다 크면 hold_exceeds_balance 예요 — 더 짧게 · 낮은 해상도로 고르면 지나갈 수 있어요. 영상 문엔 below_minimum_balance 가 안 와요. 503 api_error 엔 글 문처럼 Retry-After: 2 가 붙어요. 문장은 그대로예요.
Python openai SDKe.body.get("reason") == "held_by_others"Node openai SDKerr.error?.reason === "held_by_others" - 고침
읽어주기 · 받아쓰기 요청 ID 로 사용 기록을 찾아요
POST /v1/audio/speech · POST /v1/audio/transcriptions
읽어주기 · 받아쓰기 응답의 요청 ID(request-id · x-request-id · error.request_id)가 저희 사용 기록과 같은 값이 됐어요 — 잔액을 잡은 뒤의 호출은 문의하실 때 그 값으로 바로 찾아요. 예전엔 응답에 요청 ID 는 왔지만 사용 기록엔 남지 않아서 부른 시각 · 모델 이름으로 찾았어요. 머리글 이름과 자리는 그대로예요.
- 고침
GET /v1/models 의 검열 모델은 산 크레딧의 회사와 상관없이 와요
GET /v1/models (키를 실었을 때)
키를 실어 부른 GET /v1/models 에 검열 모델(kind moderation) 줄이 산 크레딧이 정한 회사와 상관없이 와요 — 산 크레딧으로 부를 회사가 없는 키도요. POST /v1/moderations 는 무료이고 잔액 0 에서도 되는데, 예전엔 그런 키의 목록에만 없었어요.
- 고침
정산 원장 · 감사 로그 CSV 의 끝 날이 자정까지 다 들어가요
대시보드 「정산 원장 받기 (CSV)」 · 「감사 로그 받기」(?from · ?to)
?to 로 고른 끝 날의 사용이 그날 자정 전까지 다 들어가요(끝 날 다음 날이 지나 받은 파일 기준 — 그날 보낸 호출이 다 끝나야 줄이 채워져요). 예전엔 끝 날 자정 직전 1초 사이의 사용이 그 파일에도, 다음 날부터 받은 파일에도 없었어요. 감사 로그 CSV 도 정산 원장처럼 달력에 없는 날이나 from 이 to 보다 늦으면 400 이에요(예전엔 2026-02-31 같은 날을 다음 달 날로 넘겨 읽었어요).
- 고침
사용 기록의 시각은 요청을 받은 시각이에요
대시보드 「정산 원장 받기 (CSV)」 · 「감사 로그 받기」
정산 원장 CSV 의 날짜와 감사 로그 CSV 의 시각이 요청을 받은 시각이에요 — 정산 원장 머리의 「귀속 기준은 요청 발생 시각입니다」와 같아요. 예전엔 답이 끝난 뒤의 시각이라, 자정 직전에 보낸 긴 호출이 다음 날 줄로 잡혔어요. 이 바뀜 전(2026-10-29 까지) 쓴 몫은 그대로예요 — 그 전 기간을 맞출 땐 끝 날 다음 날 줄도 같이 보세요.
- 고침
스페이스 전송 정책 키도 맨 위 cache_control 을 받아요
POST /v1/messages · /v1/chat/completions
스페이스 전송 정책을 켠 키로 /v1/messages · /v1/chat/completions 맨 위에 cache_control(자동 캐시)을 실어도 지나가요 — system · messages 블록 안 cache_control 과 같은 검사예요. 예전엔 글만 보낸 요청도 맨 위 cache_control 하나로 403 space_content_policy(error.code unsupported_capability)였어요. 블록 안에 붙이던 코드는 그대로 둬도 돼요.
- 고침
기한이 끝난 키의 401 문장 — 「한도 수정」 · 「키 만들기」
API 키로 부르는 /v1 문(기한이 끝난 키)
기한이 끝난 API 키의 401 authentication_error 문장 끝이 「… 의 「한도 수정」에서 기한을 늘리거나 「키 만들기」로 새 키를 만들 수 있어요.」예요. 예전 문장의 「새 키를 받을 수 있어요」는 그 키 줄의 「새로 받기」로 읽혔는데, 그 단추는 기한이 지나면 안 눌려요. status · type 과 앞머리 「이 키는 기한이 끝났어요」는 그대로예요 — 그 낱말로 가르던 코드는 그대로 두고, 문장 끝까지 통째로 견주던 코드만 고쳐 주세요. 스페이스 키 문장(「스페이스 관리자(선생님)에게 말해 주세요」)은 그대로예요.
Python openai SDKe.body.get("message", "").startswith("이 키는 기한이 끝났어요")Node openai SDKerr.error?.message?.startsWith("이 키는 기한이 끝났어요")Python Anthropic SDKe.body["error"]["message"].startswith("이 키는 기한이 끝났어요")Node Anthropic SDKerr.error?.error?.message?.startsWith("이 키는 기한이 끝났어요") - 고침
422 upstream_execution_unconfirmed 문장 — Idempotency-Key 없이 보낸 요청은 조회 대신 문의
POST /v1/chat/completions · /v1/messages · /v1/responses (Idempotency-Key 없이 보냈을 때)
Idempotency-Key 없이 보낸 요청이 422 upstream_execution_unconfirmed 로 끝나면 문장 끝이 「요청 ID와 부른 시각을 적어 문의해 주세요.」예요. 예전엔 키를 실었든 안 실었든 「요청 ID로 처리 상태를 확인해주세요.」였는데, 처리 상태 조회(GET /v1/requests/{id})는 Idempotency-Key 를 실어 접수된 요청만 읽어서 키 없는 요청은 404 예요(이건 그대로예요). 키를 실은 요청은 문장 · status · type · x-should-retry: false 가 그대로예요 — 받은 x-gateway-status-url 을 GET 하세요. 자동으로 다시 보내지 않는 것도 그대로예요.
Pythone.request_id # 문의할 때 부른 시각과 같이Nodeerr.requestID // 문의할 때 부른 시각과 같이
2026-10-28#
- 새 기능
요청 ID 가 /v1 모든 문에
/v1 모든 문(관리 키 문 /v1/keys 빼고) · 없는 주소의 404
응답 머리글에 request-id 가 오고, 같은 값이 x-request-id 로도 와요. 성공 · 오류 · 스트림 모두요. JSON 오류 몸통엔 error.request_id 도 와요(/v1/messages 꼴이면 맨 위 request_id 도). 문의하실 때 이 값을 알려 주세요.
Pythonresp._request_id # 오류면 e.request_idNoderesp._request_id // 오류면 err.requestID - 새 기능
호출마다 쓴 몫 usage.cost_credits
POST /v1/chat/completions · /v1/messages · /v1/responses
API 키로 부른 응답의 usage 에 그 호출에서 빠진 크레딧이 와요. 소수 넷째 자리까지 올린 값이고, 0 보다 크면 적어도 0.0001 이에요. 스트림은 끝 usage 에 와요(/v1/responses 는 response.completed 의 response.usage). 정산이 2초 안에 안 끝나면 칸 없이 오니 없을 때도 돌게 짜 주세요. 잔액 앞뒤 차이로 세지 마세요.
Pythongetattr(resp.usage, "cost_credits", None)Node(resp.usage as any)?.cost_credits - 새 기능
잠시 뒤 그대로 다시 보낼 실패에 Retry-After
POST /v1/chat/completions · /v1/messages · /v1/responses
머리글 Retry-After(정수 초)가 와요. 402 error.reason held_by_others 는 5, 503 api_error 는 2, 429 rate_limit_error 는 모델 회사가 준 값을 1~60초로 맞춘 값(없으면 5)이에요. 그만큼 기다렸다 같은 요청을 보내세요. x-should-retry: false 가 붙은 실패엔 안 와요 — 그건 그대로 다시 보내지 마세요(504 error.reason "timeout" 만 요금 없이 끝난 거라 SDK 자동 재시도 말고 코드가 잠시 뒤 한 번 다시 보내도 돼요). 이날 Idempotency-Key 를 실은 요청의 실패엔 안 왔어요 — 2026-10-29 부터는 실행 전에 막힌 실패면 키를 실어도 와요(위 2026-10-29 줄).
Pythone.response.headers.get("retry-after")Nodeerr.headers?.get("retry-after") - 새 기능
기한을 넘긴 504 에 error.reason "timeout"
POST /v1/chat/completions · /v1/messages · /v1/responses
실행 전 거절로 다른 경로를 도는 사이 합쳐 4분 기한을 넘겨 504 upstream_error(x-should-retry: false)로 드릴 때 error.reason "timeout" 이 와요. 모델 회사에서 실행되기 전에 끝난 실패라 요금은 안 붙어요. x-should-retry: false 는 같은 요청이 같은 4분을 또 기다리지 않게 SDK 의 자동 재시도를 막는 표시예요 — SDK 자동 재시도 말고 코드에서 잠시 뒤 한 번 다시 보내셔도 돼요(Idempotency-Key 를 실었으면 이날은 새 키로 · 2026-10-29 부터는 같은 키로). 답이 길어서가 아니라 앞 경로들이 실행 전에 거절하는 사이 기한이 끝난 거라, 나누거나 stream 으로 바꿀 일은 아니에요. type · status · 머리글은 그대로예요. 모델 회사로 보낸 뒤 시간이 끝난 실패는 이 504 가 아니라 422 upstream_execution_unconfirmed 예요(아래 바뀜 줄).
Python openai SDKe.body.get("reason") == "timeout"Node openai SDKerr.error?.reason === "timeout"Python Anthropic SDKe.body["error"].get("reason") == "timeout"Node Anthropic SDKerr.error?.error?.reason === "timeout" - 새 기능
Idempotency-Key 와 요청 상태 조회 GET /v1/requests/{id}
POST /v1/chat/completions · /v1/messages · /v1/responses · GET /v1/requests/{id}
생성 요청에 Idempotency-Key 머리글(공백 · 제어문자 없는 ASCII(영문 · 숫자 · 기호) 8~200자)을 실으면 같은 키 · 같은 본문을 두 번 실행하지 않아요. 키는 그 API 키에 기한 없이 묶여요 — 이날은 API 키를 「새로 받기」로 바꾸면 옛 키로 접수한 요청이 새 키로 조회되지 않았고(404) 같은 Idempotency-Key 도 새 요청으로 실행됐어요(2026-10-29 부터는 「새로 받기」가 접수를 새 키로 옮겨요 — 위 2026-10-29 줄). 응답 머리글 x-gateway-status-url(/v1/requests/<id> 꼴 상대 주소 — https://oneport.kr 를 앞에 붙여요)을 같은 API 키로 GET 하면 실행 · 결과 · 정산 상태를 볼 수 있어요(답 본문은 보관하지 않아요 · https://oneport.kr/docs/reference/requests.md). 같은 키 · 같은 본문을 다시 보내면 409 request_already_exists(error.type · code 같은 값)와 Location · error.status_url 이 와요(이날 이 409 엔 x-gateway-status-url 이 없었어요 · 2026-10-29 부터 와요). 같은 키로 본문을 바꾸면 409 idempotency_conflict(error.type · code 같은 값)예요. 이날은 키를 실은 요청이 실패하면 상태와 상관없이 x-should-retry: false 가 붙고 Retry-After 가 빠졌고, 실패한 뒤 같은 키 · 같은 본문을 보내도 409 request_already_exists 였어요. 그래서 x-gateway-status-url 이 같이 온 402 held_by_others · 429 rate_limit_error · 503 api_error · 504 error.reason "timeout" · 529 처럼 실행 전에 막힌 실패도 새 Idempotency-Key 로 다시 보내야 했어요(2026-10-29 부터는 이런 실패에 원래 Retry-After 가 오고 같은 키 · 같은 본문으로 다시 받아요 — 위 2026-10-29 줄). 키를 실으면 x-gateway-request-id(request-id 와 같은 값) · x-gateway-usage-id 도 와요. 키를 안 실은 호출은 예전 그대로예요. 지금 규칙은 「에러」 장 「Idempotency-Key 를 실었을 때」(https://oneport.kr/docs/errors.md)예요.
Pythone.response.headers.get("x-gateway-status-url") or e.response.headers.get("location")Nodeerr.headers?.get("x-gateway-status-url") ?? err.headers?.get("location") - 새 기능
Claude 과부하 529 에 Retry-After
POST /v1/chat/completions · /v1/messages (Claude 모델)
같은 모델의 다른 경로까지 과부하로 막혀 529 로 드릴 때, 모델 회사가 기다릴 시간을 알려 줬으면 그 값을 1~60초로 맞춰 Retry-After(정수 초)로 실어요. 값이 없으면 예전처럼 머리글 없이 와요. 있으면 그만큼 기다렸다 같은 요청을 보내세요. 529 의 type 은 upstream_error 예요(overloaded_error 가 아니에요) — 상태 529 로 가르세요. 이날 Idempotency-Key 를 실은 요청엔 이 머리글이 안 왔어요 — 2026-10-29 부터는 와요(위 2026-10-29 줄).
Pythone.response.headers.get("retry-after")Nodeerr.headers?.get("retry-after") - 새 기능
토큰 세기 문 503 api_error 에 Retry-After: 2
POST /v1/messages/count_tokens
키를 잠깐 못 읽었거나 토큰을 셀 수 없는 503 api_error 에 Retry-After: 2(초)가 와요 — /v1/messages 와 같아요. 그만큼 기다렸다 그대로 다시 보내세요. 429 rate_limit_error 엔 이날 없었어요 — 2026-10-29 부터 와요(위 2026-10-29 줄).
Pythone.response.headers.get("retry-after")Nodeerr.headers?.get("retry-after") - 새 기능
402 error.reason 세 갈래
POST /v1/chat/completions · /v1/messages · /v1/responses · /v1/images/* · /v1/audio/* · /v1/embeddings
402 insufficient_credit 의 error.reason 이 셋이에요 — held_by_others(잠시 뒤 그대로) · hold_exceeds_balance(출력 상한이나 입력을 줄이거나 충전) · below_minimum_balance(충전). 이날 뒤의 둘이 더해졌고, 그림 · 소리 문 402 에도 held_by_others 가 와요. /v1/embeddings 엔 이날 뒤의 둘만 왔고(held_by_others 는 2026-10-29 부터 · 위 2026-10-29 줄), /v1/videos 402 엔 아직 없어요. type · status 는 그대로예요.
Python openai SDKe.body.get("reason")Node openai SDKerr.error?.reasonPython Anthropic SDKe.body["error"].get("reason")Node Anthropic SDKerr.error?.error?.reason - 새 기능
키 한도 429 에 error.reason
POST /v1/chat/completions · /v1/messages · /v1/responses · /v1/embeddings · /v1/images/* · /v1/audio/* · POST /v1/videos · POST /v1/tasks
429 key_limit_exceeded 에 error.reason 이 와요 — 이번 달 한도면 key_month_limit, 오늘 한도면 key_day_limit. 강좌 몫 429(group_limit_exceeded)엔 이날 안 왔어요 — 2026-10-29 부터 와요(위 2026-10-29 줄). POST /v1/tasks 는 처음 응답에만 와요. type · status 와 x-should-retry: false 는 그대로예요.
Python openai SDKe.body.get("reason") == "key_day_limit"Node openai SDKerr.error?.reason === "key_day_limit"Python Anthropic SDKe.body["error"].get("reason") == "key_day_limit"Node Anthropic SDKerr.error?.error?.reason === "key_day_limit" - 새 기능
도중에 끊긴 스트림의 마지막 오류 이벤트
POST /v1/chat/completions · /v1/messages · /v1/responses (stream: true)
저희 시간 제한이나 연결 끊김으로 스트림이 도중에 끝나면, 다 받은 이벤트까지 보낸 뒤 그 문의 꼴로 오류 이벤트를 하나 보내고 정상으로 닫아요. /v1/chat/completions 는 data: {"error": {"type": "upstream_error", …}}, /v1/messages 는 event: error(type api_error), /v1/responses 는 event: error(code server_error) 다음 event: response.failed 예요. 예전엔 연결만 끊겨 SDK 마다 다른 오류가 났어요. 받은 글은 잘린 답이에요.
- 바뀜
저희 쪽 사정으로 못 보낸 요청은 503 provider_unavailable
POST /v1/chat/completions · /v1/messages · /v1/responses
모델 회사가 저희 쪽 사정으로 요청을 받지 않으면 503 type api_error · error.reason provider_unavailable 로 와요. 예전엔 모델 회사가 준 상태 코드가 그대로 가서 보내신 키나 결제 문제처럼 보였어요. 요금은 안 붙어요 — Retry-After 만큼 기다렸다 그대로 다시 보내세요(Idempotency-Key 를 실었으면 이날은 Retry-After 없이 와서 2초쯤 뒤 새 키로 보내야 했어요 — 2026-10-29 부터는 Retry-After 가 그대로 오고 같은 키로 다시 받아요). 요청이 틀린 400 · 내용을 거절한 403 은 예전 그대로예요.
Python openai SDKe.body.get("reason") == "provider_unavailable"Node openai SDKerr.error?.reason === "provider_unavailable"Python Anthropic SDKe.body["error"].get("reason") == "provider_unavailable"Node Anthropic SDKerr.error?.error?.reason === "provider_unavailable" - 바뀜
보낸 뒤 결과를 모르는 실패는 422 upstream_execution_unconfirmed
POST /v1/chat/completions · /v1/messages · /v1/responses
모델 회사로 보낸 뒤 시간이 끝났거나 연결이 끊겼거나, 모델 회사가 408 · 5xx 로 답했는데 실행 전 거절로 확인되지 않았거나, 성공 머리글 뒤 몸통을 못 읽었으면 422 upstream_execution_unconfirmed 로 와요. x-should-retry: false 가 붙고 Retry-After 는 없어요. 예전엔 5xx upstream_error 로 와서 잠시 뒤 다시 보내라고 했어요. 이제는 실행됐을 수 있으니 자동으로 다시 보내지 마세요(Idempotency-Key 를 실었어도 새 키로 다시 보내지 마세요). Idempotency-Key 를 실은 요청이면 오는 x-gateway-status-url 을 같은 API 키로 GET 해서 결과와 정산을 확인하세요. 키 없이 부른 호출엔 그 주소가 없으니 요청 ID 와 부른 시각으로 문의해 주세요. 이 422 엔 요금이 안 붙어요 — 다만 모델 회사에서 실행되지 않았다는 뜻은 아니에요. 첫 응답이 120초를 넘기면 어느 모델이든 다른 경로로 다시 보내지 않아요. 그 전에도 일부 요청은 이미 이 422 로 왔어요 — 이날부터 같은 실패는 모두 이 422 예요.
Python openai SDKe.type == "upstream_execution_unconfirmed"Node openai SDKerr.type === "upstream_execution_unconfirmed"Python Anthropic SDKe.body["error"]["type"] == "upstream_execution_unconfirmed"Node Anthropic SDKerr.error?.error?.type === "upstream_execution_unconfirmed" - 바뀜
claude-sonnet-5 · claude-opus-5-5 큰 예약 — 새 anthropic-version · beta 머리글이면 보통 셈
POST /v1/chat/completions · /v1/messages (claude-sonnet-5 · claude-opus-5-5)
스트림 없이 user 글 하나(system 은 있어도 돼요)만 실어 부를 때 지갑이 크게 잡는 셈((몸통 바이트 + 16,384) 토큰 × 입력 단가 × 2 + max_tokens × 출력 단가)은 이제 anthropic-version · anthropic-beta · openai-beta 머리글을 하나도 안 실었거나 anthropic-version: 2023-06-01 하나만 실었을 때만이에요. 새 판(2026-10-21 등) · anthropic-beta · openai-beta 를 실으면 보통 셈(몸통 3바이트를 1토큰으로 친 입력 + max_tokens 만큼의 출력)으로 잡아요 — 새 판 Anthropic SDK 로 짧은 질문을 보낼 때 402 가 덜 나요. 실제 차감은 예전처럼 쓴 만큼이에요. 이 셈인지는 저희가 모델 회사로 보낼 몸통으로 가려요. /v1/messages 는 model · messages · system · max_tokens 말고 다른 칸이 실리면 보통 셈이에요(이 두 모델의 temperature · top_p 처럼 빼고 보내 x-gateway-ignored-params 에 적히는 칸은 세지 않아요). /v1/chat/completions 는 옮긴 몸통으로 가려서, 출력 상한을 max_completion_tokens 로 적거나 안 적어도(그땐 8,192) 이 셈이고 stream_options · n: 1 · stream: false 와 x-gateway-ignored-params 에 적히는 칸도 세지 않아요 — 몸통에 남는 칸(tools · stop · user · metadata · json_schema 꼴 response_format · 받는 모델의 reasoning_effort 등)을 실으면 보통 셈이에요.
- 바뀜
학생 키의 기본 월 한도
POST /v1/keys (관리 키)
학생 키를 만들 때 monthly_limit_credits 칸을 빼면 월 한도(150 크레딧)가 걸려요. audience 를 안 적은 초 · 중 · 고 지갑의 키도 학생 키예요. 예전처럼 지갑 잔액까지 쓰게 하려면 "monthly_limit_credits": null 을 적어 보내세요. 그 칸을 뺐는데 daily_limit_credits 가 그 기본값보다 크면 400 이에요. 선생님 키는 그대로예요.
"monthly_limit_credits": null - 바뀜
anthropic-version 2026-10-21 판의 temperature · top_p · top_k
POST /v1/messages · /v1/messages/count_tokens
기본값만 받는 Claude 모델(claude-sonnet-5-5 · claude-haiku-5-5 등)은 판과 상관없이 세 칸을 빼고 보내고, 응답 머리글 x-gateway-ignored-params 에 적어요(예전엔 400). 값을 읽는 모델(claude-haiku-4-5 등)에 anthropic-version 2026-10-21 이후 판이 오면 모델 회사로 보내기 전에 400 invalid_request_error 예요(요금 없음). 새 Anthropic SDK(Python 2.x · TypeScript 1.x)가 이 판을 기본으로 실어요. anthropic-version 2023-06-01 은 예전 그대로예요. /v1/chat/completions 로 Claude 를 부를 때도 답의 뜻이 안 바뀌는 칸(기본값만 받는 모델의 temperature · top_p, seed · store · presence_penalty 등, effort 를 못 받는 모델의 reasoning_effort)은 빼고 보내고 x-gateway-ignored-params 에 적어요 — 답의 뜻이 바뀌는 칸(1 이 아닌 n · logprobs 등)은 모델 회사로 보내기 전에 400 이에요. 이건 예전부터예요.
- 바뀜
키를 새로 받아도 이번 달 · 오늘 쓴 몫이 이어져요
GET /v1/key · 대시보드 키 「새로 받기」
대시보드에서 키를 새로 받으면 옛 키의 이번 달 · 오늘 쓴 몫이 새 키로 옮겨 가요(스페이스 그룹 키도). 새 키의 GET /v1/key month.used_credits · day.used_credits 와 한도 판정이 0 부터 시작하지 않아요.
- 고침
GET /v1/models/{id} 404 의 type 을 model_not_found 로
GET /v1/models/{id}
없거나 이 키로 못 부르는 모델 id 의 404 가 type not_found_error · code model_not_found 로 갈렸어요. 이제 type 도 model_not_found 예요(글 문 404 와 같아요). 상태 · 문장은 그대로예요.
Python openai SDKe.type == "model_not_found"Node openai SDKerr.type === "model_not_found" - 고침
Claude 를 /v1/chat/completions 로 받을 때의 usage
POST /v1/chat/completions (Claude 모델)
스트림이 오류로 끝나면 오류 조각 앞에 그때까지의 usage 조각(choices: [])이 와요 — 실패한 흐름에서 쓴 토큰도 셀 수 있어요. usage.prompt_tokens_details 에 1시간 캐시 쓰기 몫 cache_write_1h_tokens 가 따로 와요(있을 때만 · cache_write_tokens 는 5분 · 1시간 합계 그대로). 정상으로 끝난 스트림도 끝 usage 조각은 choices 가 빈 배열이에요 — chunk.choices[0] 을 읽기 전에 비었는지 보세요(이건 예전부터예요).
- 고침
작업 문 — 다른 호출이 잡아 둔 몫 402 · 시작 전 취소
POST /v1/tasks · GET /v1/tasks/{id}
같은 지갑의 다른 호출이 잡아 둔 몫 때문인 402 에 error.reason held_by_others 와 Retry-After: 5 가 와요. 그만큼 기다렸다 같은 Idempotency-Key 로 다시 보내면 새로 시작해요(예전엔 그 Idempotency-Key 가 계속 거절됐어요). 시작 전에 취소된 작업의 GET 은 billing.state 가 not_started, error.code 가 cancelled_before_start 예요(예전엔 settled · error null).
- 고침
그림 문의 큰 응답
POST /v1/images/generations · /v1/images/edits
성공 응답을 흘려 보내는 몸통으로 바꿔 4.5MB 가 넘는 b64_json 도 받아요. 예전엔 그 크기에서 응답이 실패했어요. 몸통 모양 · content-type · 상태는 그대로예요.
- 고침
영상 참고 그림의 형식을 파일 머리로
POST /v1/videos (input_reference)
input_reference 파일이 application/octet-stream · text/plain 으로 와도 파일 머리로 png · jpeg · webp 를 알아봐요. 예전엔 형식 없이 올린 그림도 400 이었어요(Node openai SDK 의 toFile · Python requests 가 그렇게 보내요).
2026-10-27#
- 새 기능
402 error.reason held_by_others
POST /v1/chat/completions · /v1/messages · /v1/responses · 문장은 /v1/images/* · /v1/videos · /v1/embeddings · /v1/audio/* 도
같은 지갑의 다른 호출이 잡아 둔 몫 때문인 402 insufficient_credit 에 error.reason held_by_others 가 와요. 문장 끝에도 「…다른 호출이 잡아 둔 몫 때문이에요…」가 붙어요(그림 · 영상 · 임베딩 · 소리 문은 이날 문장만). 그 호출이 끝나면 풀리니 잠시 뒤 그대로 다시 보내세요 — 충전이나 상한 줄이기가 아니에요.
Python openai SDKe.body.get("reason") == "held_by_others"Node openai SDKerr.error?.reason === "held_by_others"Python Anthropic SDKe.body["error"].get("reason") == "held_by_others"Node Anthropic SDKerr.error?.error?.reason === "held_by_others" - 새 기능
관리 키 문의 키 자리
POST /v1/keys · GET /v1/keys
자리가 모자란 400 에 error.code key_room_exceeded 와 남은 자리 error.room(0 포함)이 와요. type 은 invalid_request_error 그대로예요. GET /v1/keys 응답에 room · cap 이 와요.
Python openai SDKe.body.get("code") == "key_room_exceeded"Node openai SDKerr.error?.code === "key_room_exceeded" - 새 기능
GET /v1/models 의 tier 에 상위 구간 캐시 단가
GET /v1/models
상위 구간이 있는 모델의 pricing.tier 에 cache_read_credits_per_mtok · cache_write_credits_per_mtok 가 와요. 모델 회사가 그 구간 캐시 값을 공시한 모델에만 와요.
- 바뀜
키를 실은 GET https://oneport.kr/models 는 JSON 404
GET https://oneport.kr/models (/v1 빠진 주소)
Authorization · x-api-key 머리글을 실어 /v1 없이 부르면 사이트 페이지(HTML 200) 대신 JSON 404 not_found_error 로 Base URL 을 알려 줘요.
Python openai SDKOpenAI(base_url="https://oneport.kr/v1")Node openai SDKnew OpenAI({ baseURL: "https://oneport.kr/v1" }) - 바뀜
쓴 몫 칸은 올린 정수
GET /v1/key · GET /v1/keys · GET /v1/credits
GET /v1/key 의 month.used_credits · day.used_credits, GET /v1/keys 의 month_used_credits · day_used_credits, GET /v1/credits 의 used_credits 가 1 크레딧 아래를 올린 정수예요(예전엔 버렸어요). 세 문이 같은 값을 말해요. 한도 · 남은 몫 · 잔액은 예전처럼 버려요.
- 바뀜
도구를 실은 GPT-5.6 · GPT-6 에 reasoning_effort none
POST /v1/chat/completions
도구를 실었고 reasoning_effort 를 비운 gpt-5.6-luna · gpt-5.6-sol · gpt-5.6-terra · gpt-6-luna · gpt-6-sol · gpt-6.1-terra 는 none 을 넣어 보내요(예전엔 모델 회사 400). 응답 머리글 x-gateway-mapped-params 에 reasoning_effort->none 이 적혀요. reasoning_effort 를 보내셨으면 그대로예요. 평평한 custom 도구({"type": "custom", "name": …})도 채팅 꼴로 접어요(tools->custom).
- 고침
/v1 JSON 응답에 charset=utf-8
/v1 모든 문
Content-Type 이 application/json; charset=utf-8 이에요. Windows PowerShell 5.1(Invoke-RestMethod)에서 한글 답 · 오류가 깨지던 것을 고쳤어요. 스트림 · 소리 · 그림 바이트는 그대로예요.
- 고침
미리 잡는 몫의 셈
POST /v1/chat/completions · /v1/messages · /v1/responses
max_tokens · max_completion_tokens · max_output_tokens 를 둘 이상 보내면 큰 값으로 잡아요. /v1/responses 는 max_output_tokens(없으면 8,192)가 바닥이라 작은 max_tokens 를 더해도 줄지 않아요. data: 로 붙인 그림은 바이트 대신 한 장당 상한(Claude 4,800 · gpt-4o-mini 48,200 · 그 밖 16,400 토큰)으로 잡고, cache_control 이 있으면 5분 캐시 쓰기 단가로 잡아요. 402 · 429 가 나는 경계가 조금 움직였어요 — 실제 차감은 쓴 만큼 그대로예요.
- 고침
저희가 끊은 읽어주기는 요금 없음
POST /v1/audio/speech
200 으로 소리를 보내기 시작한 뒤 저희 시간 제한(본문 침묵 60초 · 시작부터 270초)이나 흐름 깨짐으로 끊기면 요금이 안 붙어요. 예전엔 글자 수만큼 다 빠졌어요. 끊긴 흐름은 오류로 닫혀 잘린 소리인 걸 알 수 있어요. 직접 끊으면 예전 그대로예요.
- 고침
count_tokens 의 거절 모양
POST /v1/messages/count_tokens
모델 회사가 거절하면 상태는 그대로, type 은 upstream_error(429 만 rate_limit_error)예요. 예전엔 모델 회사 몸통이 그대로 와서 invalid_request_error 로 읽혔어요. 스페이스 키는 /v1/messages 와 같은 전송 정책 · 꺼진 모델(403 model_disabled_in_space) 판정을 거쳐요.
- 고침
/v1/moderations 몸통 검사
POST /v1/moderations
몸통이 JSON 객체가 아니면 400 invalid_request_error 예요(예전엔 null 이면 500). model 이 문자열이 아니면 400 이에요 — 예전엔 말없이 기본 모델로 불렀어요.
- 고침
그림 고치기 원본이 gif 면 미리 400
POST /v1/images/edits
원본 그림이 gif 면 크레딧을 잡기 전에 400 invalid_request_error 예요. 예전엔 모델 회사로 갔다가 거절이 왔어요. 받는 형식은 png · jpeg · webp 예요.
2026-10-26#
- 바뀜
키를 잠깐 못 읽으면 503 api_error
/v1 모든 문(관리 키 문 /v1/keys 빼고)
키 기록을 잠깐 못 읽었을 때 예전엔 401 authentication_error 로 와서 죽은 키처럼 보였어요. 이제 503 api_error 예요(문장 「지금 이 키를 확인하지 못했어요」). 잠시 뒤 그대로 다시 보내세요. 없는 키 · 꺼진 키 · 기한 끝 · 지갑 멈춤은 예전처럼 401 이에요. 관리 키 문 /v1/keys 는 이날 기준으로 이때도 401 이었어요 — 2026-10-31 부터 503 api_error 예요(그날 줄).
Python openai SDKe.type == "api_error"Node openai SDKerr.type === "api_error"Python Anthropic SDKe.body["error"]["type"] == "api_error"Node Anthropic SDKerr.error?.error?.type === "api_error" - 바뀜
GET /v1/credits 가 잔액을 못 읽으면 503
GET /v1/credits
예전엔 200 에 balance_credits 0 으로 왔어요. 이제 503 api_error 예요. 0 을 잔액으로 읽지 말고 잠시 뒤 다시 부르세요. 이날 이 문의 503 엔 Retry-After 가 없었어요 — 2초쯤 쉬고 다시 부르세요(2026-11-01 부터 Retry-After: 2).
Pythonr.status_code == 503 # requestsNoderes.status === 503 // fetch - 바뀜
글 문은 끝에 날짜(-YYYYMMDD)가 붙은 모델 이름도 받아요
POST /v1/chat/completions · /v1/messages · /v1/responses · /v1/messages/count_tokens · GET /v1/models/{id}
claude-haiku-4-5 에 -YYYYMMDD 가 붙은 이름처럼 끝에 날짜가 붙었으면, 글 문 셋 · 토큰 세기 문 · GET /v1/models/{id} 는 정확히 같은 id 가 없을 때 날짜를 떼고 목록 id 로 불러요(GET /v1/models/{id} 는 날짜를 뗀 목록 줄이 와요). 예전엔 404 였어요. -YYYY-MM-DD 꼴(gpt-4o-2024-08-06)은 떼지 않아요 — 목록 id(gpt-4o)로 바꿔 보내세요. 그림 · 임베딩 · 읽어주기 · 받아쓰기 · 영상 · 검열 문은 날짜를 떼지 않아요(2026-10-31 줄).
- 바뀜
틀린 키로 부른 GET /v1/models 는 401
GET /v1/models · GET /v1/models/{id}
없는 키 · 꺼진 키 · 기한이 끝난 키 · 멈춘 지갑의 키를 실어 부르면 200 전체 목록 대신 다른 문과 같은 401 authentication_error 예요(저희가 키 확인을 잠깐 못 하면 503 api_error). 예전엔 틀린 키에도 전체 목록이 와서 연결 확인은 통과하고 첫 호출에서야 401 이었어요. 키 머리글이 아예 없으면 예전처럼 전체 목록이에요 — 머리글은 있는데 키가 비었거나 Bearer 가 빠진 경우는 2026-10-30 줄이에요.
Python openai SDKe.status_code == 401 # client.models.list() 의 AuthenticationErrorNode openai SDKerr.status === 401 // client.models.list() 의 AuthenticationError - 바뀜
/v1 을 빠뜨린 글 문은 JSON 404
POST https://oneport.kr/chat/completions · /responses · /messages (/v1 빠진 주소)
base_url 끝의 /v1 을 빠뜨려 이 세 주소로 오면 예전엔 사이트 404 페이지(HTML)였어요. 이제 404 not_found_error 에 「주소에 /v1 이 빠졌어요」와 맞는 주소가 와요. /messages 로 오는 건 대개 Vercel AI SDK 라 baseURL 에 /v1 을 붙이면 되고, Anthropic SDK · Claude Code 는 거꾸로 https://oneport.kr 까지만 적어요(문장도 두 길을 말해요). 임베딩 · 그림 · 소리 문(예: /embeddings)은 지금도 사이트 404 페이지예요 — 404 가 HTML 이면 base_url 끝의 /v1 부터 보세요.
Python openai SDKOpenAI(base_url="https://oneport.kr/v1")Node openai SDKnew OpenAI({ baseURL: "https://oneport.kr/v1" }) - 고침
그림 문의 입력 판정
POST /v1/images/edits · /v1/images/generations
/v1/images/edits 의 image 칸이 Content-Type 없이(text/plain 으로) 와도 파일 머리로 png · jpeg · webp 를 알아봐요. 그림이 아니면 크레딧을 잡기 전에 400 이에요. /v1/images/generations 에 글 모델 id 를 보내면 404 대신 400 으로 맞는 문을 알려 줘요(고치기 문과 같아요).
- 고침
GET /v1/models 의 1 아래 요율은 유효숫자 세 자리
GET /v1/models
pricing.rates 의 1 아래 값(초당 · 글자당)을 소수 둘째 자리 대신 유효숫자 세 자리로 내요. whisper-1 초당 0.092 가 0.09 로 나가 어림이 2% 넘게 모자랐어요. 차감 값은 그대로예요.