LLM Registry API

케파스 사내 LLM을 키 하나로, OpenAI API 형식 그대로 쓰는 입구다. 모델이 어느 서버에서 어떤 엔진으로 도는지 몰라도 되고, 그쪽이 바뀌어도 주소와 키와 코드는 그대로다.

처음이라면 사내망에 연결된 PC에서 키 발급·관리 페이지를 열어 키를 직접 발급하고(키 받기), API 설명의 "처음 시작하기"를 따라 한다. 사내망이 필요한 것은 키 발급과 관리뿐이고, 발급한 키는 외부에서도 같은 주소로 쓴다. 첫 호출 하나는 이 페이지의 처음 호출까지에 명령과 실제 응답으로 나와 있다.

이것은 무엇인가

사내에는 여러 LLM이 여러 서버에서 돈다. LLM Registry API는 그 모델들을 주소 하나 뒤에 모아 둔 입구다. 사용자는 키 하나를 받아, 그 키에 허용된 모델을 모두 같은 주소 https://llm.kephas.kr/v1로 부른다. 모델마다 서버 주소를 따로 알아 둘 필요가 없다.

요청과 응답은 OpenAI API 형식이다. 모델 목록(Models), 대화(Chat Completions, 스트리밍 포함), 텍스트 완성(Completions), 임베딩(Embeddings)을 OpenAI와 같은 경로, 같은 요청 본문, 같은 응답 모양으로 쓴다. 그래서 openai 라이브러리(Python·Node), LangChain(langchain-openai), 그 밖에 OpenAI 호환 연결을 받는 도구에 Base URL과 키 두 가지만 넣으면 된다. 이 서비스에 맞추려고 코드를 새로 쓸 필요는 없다.

모델이 다른 서버로 옮겨 가거나 모델을 돌리는 엔진이 바뀌어도 사용자에게 보이는 것은 그대로다. 주소, 키, 모델 이름, 요청과 응답의 형식이 바뀌지 않으므로 사용자 코드도 고칠 것이 없다. 어떤 모델이 어디서 어떻게 도는지는 사용자가 알 필요가 없다.

OpenAI 형식에 더해 이 서비스에만 있는 것이 둘 있다. 모델마다 붙은 두 글자 등급(모델과 등급)과, 모델 이름 대신 등급으로 모델을 고르는 방법(GET /v1/pick, "model": "grade:C2")이다.

하지 않는 것

무엇을 할 수 있나

할 수 있는 것어떻게
대화 POST /v1/chat/completions에 messages를 보내면 답이 choices[0].message.content로 온다. "stream": true를 주면 답이 만들어지는 대로 조각조각 받는다(스트리밍).
텍스트 완성 POST /v1/completions. OpenAI의 이전 형식으로, prompt에 이어 쓴 글이 choices[0].text로 온다.
임베딩 POST /v1/embeddings의 input에 문자열 하나나 문자열 배열을 보내면, 입력마다 벡터가 data[].embedding으로 온다. 검색·RAG에 쓴다(임베딩).
이미지 입력 V 등급 모델에 대화 형식으로 글과 이미지를 함께 보낸다(이미지 입력).
도구 호출 tools에 함수 정의를 주면 모델이 부를 함수와 인자를 tool_calls로 돌려준다. 모델 목록에서 tools가 true인 모델만 된다(도구 호출).
모델 목록과 등급 보기 GET /v1/models는 이 키로 쓸 수 있는 모델만, 등급과 제원을 붙여 돌려준다. 한 모델만 볼 때는 GET /v1/models/{id}(지금 있는 모델 보기).
등급으로 모델 고르기 GET /v1/pick?grade=C2로 모델 하나를 골라 이름을 받거나, 호출의 model에 grade:C2를 적어 그 자리에서 고르게 한다(모델을 고르는 세 가지 방법).

아래 예시는 모두 2026년 11월 9일에 이 서비스로 실제로 보내고 받은 것이다. 응답이 길면 필요한 부분만 옮기고, 줄인 곳은 …로 표시했다.

스트리밍

"stream": true를 주면 응답이 SSE로 온다. 한 줄에 조각 하나씩 data: {...}가 이어지고, 마지막 줄은 data: [DONE]이다. 아래는 C2 모델에 "1+1은? 숫자만 답해."를 스트리밍으로 보냈을 때 온 109줄 가운데 처음과 끝이다. 줄마다 반복되는 필드(created, model, object 등)는 …로 줄였다.

data: {"id":"chatcmpl-534",…,"choices":[{"index":0,"delta":{"reasoning_content":"Okay","content":"","role":"assistant"}}]}
data: {"id":"chatcmpl-534",…,"choices":[{"index":0,"delta":{"reasoning_content":",","content":""}}]}
…
data: {"id":"chatcmpl-534",…,"choices":[{"index":0,"delta":{"reasoning_content":".\n","content":""}}]}
data: {"id":"chatcmpl-534",…,"choices":[{"index":0,"delta":{"content":"2"}}]}
data: {"id":"chatcmpl-534",…,"choices":[{"finish_reason":"stop","index":0,"delta":{}}]}
data: [DONE]

생각(reasoning) 모델은 생각 과정을 delta.reasoning_content로 먼저 보내고, 답은 delta.content로 뒤따른다. 화면에 답만 보여 주려면 delta.content만 이어 붙인다. 마지막 조각의 finish_reason이 끝난 까닭(stop, length)을 알려 준다. curl로 지켜볼 때는 -N을 붙이면 조각이 오는 대로 보인다.

임베딩

input에 문자열 배열을 주면 같은 순서로 벡터가 온다. data[].index가 입력의 순서다. 벡터 길이는 그 모델의 dim이고, 입력 하나의 길이는 그 모델의 ctx를 넘지 않아야 한다. 임베딩에는 E 등급 모델만 쓴다.

{"model": "grade:E", "input": ["사내 LLM 레지스트리", "검색 증강 생성"]}

응답에서 볼 곳:

model                "qwen3-embedding-0.6b"
data[0].index        0
data[0].embedding    [-0.031176517, 0.011411778, -0.014389428, …]    1024개
data[1].index        1
data[1].embedding    […]                                              1024개
usage.prompt_tokens  18

이미지 입력

V 등급 모델에는 대화 형식 그대로 이미지를 넣는다. 메시지의 content를 문자열 대신 배열로 만들고, 글("type": "text")과 이미지("type": "image_url")를 함께 담는다. 이미지는 파일을 base64로 바꾼 data URL(data:image/png;base64,…)로 넣는다.

{"model": "grade:V",
 "messages": [{"role": "user", "content": [
   {"type": "text", "text": "이 그림은 무슨 색인가요? 색 이름 한 단어로만 답하세요."},
   {"type": "image_url", "image_url": {"url": "data:image/png;base64,iVBORw0KGgo…"}}
 ]}]}

빨간색으로 칠한 64×64 PNG를 이렇게 보냈을 때 V2 모델(qwen2.5-vl-8b)은 "content": "빨강"으로 답했다. 이미지도 입력 토큰으로 센다. 이 작은 이미지와 질문 한 줄에 입력 토큰이 1,067개였다.

도구 호출

요청의 tools에 함수의 이름, 설명, 인자 형식(JSON Schema)을 적어 보내면, 모델은 그 함수를 불러야 한다고 판단할 때 답 대신 부를 함수와 인자를 돌려준다. 함수를 실제로 실행하는 것은 부른 쪽이다.

{"model": "grade:C?tools=true",
 "messages": [{"role": "user", "content": "서울의 지금 날씨를 알려 줘."}],
 "tools": [{"type": "function", "function": {
   "name": "get_weather",
   "description": "도시의 현재 날씨를 돌려준다",
   "parameters": {"type": "object",
                  "properties": {"city": {"type": "string", "description": "도시 이름"}},
                  "required": ["city"]}}}]}

돌아온 응답의 choices[0]:

"finish_reason": "tool_calls",
"index": 0,
"message": {
  "content": "",
  "role": "assistant",
  "tool_calls": [
    {"index": 0,
     "function": {"arguments": "{\"city\":\"서울\"}", "name": "get_weather"},
     "id": "call_4q04uwj0",
     "type": "function"}
  ],
  "reasoning_content": "Okay, the user is asking for the current weather in Seoul. …",
  …
}

finish_reason이 tool_calls이고, arguments는 JSON을 담은 문자열이다. 부른 쪽은 이 인자로 함수를 실행한 뒤, 모델의 이 메시지와 실행 결과를 대화에 덧붙여 다시 보낸다. 결과 메시지는 {"role": "tool", "tool_call_id": "call_4q04uwj0", "content": "…실행 결과…"} 모양이다. 위 예에 {"city":"서울","weather":"맑음","temp_c":12}를 결과로 붙여 다시 보내자 모델은 "서울의 현재 날씨는 맑습니다. 기온은 12°C입니다. …"라고 답을 마무리했다.

도구 호출은 모델 목록에서 tools가 true인 모델만 받는다. 등급으로 고를 때 grade:C?tools=true처럼 조건을 붙이면 그런 모델 가운데서 고른다.

처음 호출까지

키를 받았다면 첫 호출은 아래가 전부다. 첫 두 줄은 주소와 키를 환경변수에 두고, 셋째 명령이 C2 등급(범용 대화) 모델에 질문 하나를 보낸다. 명령은 bash나 zsh 같은 셸 기준이다(macOS·Linux의 터미널, Windows의 Git Bash나 WSL).

export OPENAI_BASE_URL=https://llm.kephas.kr/v1
export OPENAI_API_KEY=sk-…        # 받은 키 전체
curl $OPENAI_BASE_URL/chat/completions \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"grade:C2","messages":[{"role":"user","content":"대한민국의 수도는 어디인가요? 한 문장으로 답해 주세요."}]}'

2026년 11월 9일에 실제로 돌아온 응답이다. 원래는 한 줄로 오지만 읽기 쉽게 줄을 나눴고, 긴 생각 문자열과 이 페이지에서 다루지 않는 값은 …로 줄였다.

{
  "id": "chatcmpl-11",
  "created": 1794188076,
  "model": "qwen3-8b",
  "object": "chat.completion",
  "system_fingerprint": "…",
  "choices": [
    {
      "finish_reason": "stop",
      "index": 0,
      "message": {
        "content": "대한민국의 수도는 서울입니다.",
        "role": "assistant",
        "reasoning_content": "Okay, the user is asking about the capital of South Korea. … So the answer should be straightforward.\n",
        "provider_specific_fields": {"reasoning": "…", "refusal": null}
      },
      "provider_specific_fields": {}
    }
  ],
  "usage": {
    "completion_tokens": 147,
    "prompt_tokens": 31,
    "total_tokens": 178,
    "prompt_tokens_details": {"cached_tokens": 15}
  }
}

볼 곳은 다섯 군데다.

어느 모델이 골라졌는지는 응답 헤더로도 알려 준다. curl에 -i를 붙이면 본문 앞에 응답 헤더가 나오고, 그 가운데 이 두 줄이 있다.

x-kephas-picked-grade: C2
x-kephas-picked-model: qwen3-8b

이 두 헤더는 model에 grade:를 적어 불렀을 때만 붙는다. 모델 이름으로 직접 부르면 붙지 않는다.

PowerShell은 환경변수를 두는 문법과 줄 잇기, 따옴표 처리가 bash와 달라 위 명령을 그대로 쓸 수 없다. 환경변수는 $env:OPENAI_BASE_URL = "https://llm.kephas.kr/v1", $env:OPENAI_API_KEY = "sk-…"처럼 둔다. openai 라이브러리(Python·Node)는 이 두 환경변수를 스스로 읽으므로, 환경변수를 두었다면 코드에 주소와 키를 적지 않아도 된다. 클라이언트별 설정은 API 설명에, 브라우저에서 키만 넣고 실행해 보는 방법은 샘플에 있다.

주소와 키

Base URLhttps://llm.kephas.kr/v1. 사내망에서도 외부에서도 같은 주소다.
인증모든 /v1 요청에 Authorization: Bearer <키> 헤더를 붙인다. 키가 없으면 401이다.
키sk-로 시작하는 문자열. 사람마다, 서비스마다 하나씩 사내망에서 직접 발급한다(키 받기).
형식OpenAI API 호환. 라이브러리와 도구에는 Base URL과 키만 넣는다.

키 받기

키는 쓸 사람이 키 발급·관리 페이지에서 직접 발급한다. 누구에게 요청하거나 기다릴 필요가 없고, 로그인도 없다. 대신 이 페이지는 사내망에서 사내 주소로 직접 열었을 때만 키를 발급하고 관리한다. 공인 주소 https://llm.kephas.kr로 들어오는 접속은 모두 외부 입구를 거치므로, 사내 PC에서 https://llm.kephas.kr/portal을 열어도 발급과 관리가 막힌다. 이때 페이지가 사내에서 직접 여는 주소를 알려 주니, 그 주소로 다시 연다.

발급은 다음 순서로 한다.

  1. 사내망에 연결된 PC의 브라우저에서 키 발급·관리 페이지를 연다. 발급과 관리가 막혔다는 안내가 보이면 안내된 사내 주소로 다시 연다.
  2. 이름(용도) 칸에 쓰는 사람과 용도를 함께 적는다. 예를 들어 "홍길동 RAG 실험"이다. 프로그램(서비스)이 쓸 키라면 맡은 사람과 서비스 이름을 적는다. 이름은 발급된 키 목록에서 누구의 어떤 키인지 알아보는 단서다. 이미 있는 이름으로는 발급되지 않는다.
  3. 허용 범위를 고른다. 보통은 member로 충분하다. 품질 1이나 실험(X) 모델이 필요하면 developer를, 정해진 모델만 쓰게 하려면 모델 직접 지정을 고르고 쓸 모델을 고른다(키의 종류).
  4. 만료를 30일, 90일, 365일, 없음 가운데서 고른다. 기본은 90일이다. 프로그램(서비스)이 쓸 키는 만료 때문에 서비스가 멈추지 않도록 모델 직접 지정과 만료 없음으로 발급한다.
  5. 비용 한도와 메모는 필요할 때만 적는다. 비용 한도는 달러 금액이고, 비워 두면 한도가 없다. 사내 모델 사용량은 비용 0으로 집계되므로 보통은 비워 둔다. 0은 넣을 수 없다.
  6. [발급]을 누르면 키 문자열 전체가 화면에 한 번만 보인다. 바로 복사해 안전한 곳에 옮겨 둔다(키는 한 번만 보인다).
  7. 복사한 키를 환경변수 OPENAI_API_KEY에, 주소 https://llm.kephas.kr/v1을 OPENAI_BASE_URL에 넣는다. 명령은 처음 호출까지에 있다.

사내망이 필요한 것은 키를 발급하고 관리할 때뿐이다. 발급한 키는 사내에서도 외부에서도 같은 주소 https://llm.kephas.kr/v1로 쓰고, API를 부르는 방법은 어디서나 같다. 외부에서는 발급할 수 없으므로, 밖에서 쓸 키도 사내에 있을 때 미리 발급해 둔다.

로그인이 없으므로 사내망에서 이 페이지를 연 사람은 누구나 발급된 모든 키의 이름, 허용 범위, 만료, 사용량을 보고, 어느 키든 정지·재개·연장·삭제할 수 있다. 이름에 쓰는 사람과 용도를 적는 것은 이 때문이다. 키를 정지하거나 삭제하기 전에는 이름을 보고 내 키인지 확인한다. 키 문자열 전체는 발급한 순간에만 보이므로, 이 페이지를 연다고 남의 키로 API를 부를 수 있게 되지는 않는다.

키는 한 번만 보인다

키 문자열 전체는 발급 직후 한 번만 보인다. 그 페이지를 떠나거나 새로 고치면 다시 볼 수 없고, 그 뒤로는 키 발급·관리 페이지의 목록에도 sk-...와 끝 네 글자로만 남는다. 발급한 사람도 다른 누구도 전체를 다시 볼 수 없으니, 보이는 즉시 복사해 안전한 곳에 옮겨 둔다. 잃어버린 키는 되살릴 수 없다. 사내망에서 키 발급·관리 페이지를 열어 새 키를 발급하고, 잃어버린 키는 목록에서 이름으로 찾아 삭제한다.

키는 비밀번호처럼 다룬다

만료와 정지

키에는 만료가 있다. 발급할 때 30일, 90일, 365일, 없음 가운데 하나로 정하고, 따로 정하지 않으면 90일이다. 만료일은 발급 직후 화면과 키 발급·관리 페이지의 목록에 나온다. 사내망에서 연 키 발급·관리 페이지에서는 키를 정지했다가 다시 쓰게 하거나(재개), 만료를 지금부터 30·90·365일 뒤로 연장하거나, 삭제할 수 있다. 어느 것이든 바로 반영된다.

만료되거나 정지된 키로 부르면 401이 오고, 메시지에 사유가 들어 있다. 첫 줄은 정지된 키, 둘째 줄은 만료된 키로 받은 실제 메시지다(뒷부분은 줄였다).

{"error":{"message":"키가 유효하지 않다: Authentication Error, Key is blocked. …","type":"authentication_error","code":401}}
{"error":{"message":"키가 유효하지 않다: Authentication Error - Expired Key. Key Expiry time … and current time …","type":"authentication_error","code":401}}

위 두 줄은 모델 목록과 grade:로 부른 호출에서 받은 것이다. 모델 이름으로 직접 부르면 같은 사유가 영어 메시지로만 오고 code는 문자열 "401"이다. 어느 쪽이든 사내망에서 키 발급·관리 페이지를 열어 그 키를 재개하거나 연장하면 바로 다시 쓸 수 있다. 키 문자열이 틀렸을 때 모델 목록에서는 사유 없이 "키가 유효하지 않다"만 온다.

모델과 등급

모델마다 두 글자 등급이 붙는다. 첫 글자는 종류, 둘째 글자는 품질이다. 예를 들어 C2는 범용 대화 모델, E2는 범용 임베딩 모델이다. 등급 글자는 대소문자를 가리지 않는다(c2와 C2는 같다).

종류1 사내 최상2 범용3 경량X 실험
C 대화·추론·문서 작성C1C2C3CX
K 코드 생성·수정K1K2K3KX
E 임베딩(검색·RAG)E1E2E3EX
V 이미지 입력V1V2V3VX

칸마다 모델이 있는 것은 아니다. 한 칸에 모델이 여럿일 수도 있고, 아직 하나도 없을 수도 있다.

품질의 뜻

품질뜻맞는 일
1 사내 최상사내에서 가장 좋은 답을 낸다. 크고 느릴 수 있으며 긴 컨텍스트를 받는다.어려운 추론, 긴 문서
2 범용일상 업무의 기본값. 빠르고 품질이 충분하다.대부분의 일. 무엇을 쓸지 모르겠으면 2
3 경량매우 빠르다. 복잡한 추론에는 맞지 않는다.분류, 추출, 테스트
X 실험검증 전이다. 예고 없이 바뀌거나 사라질 수 있다. 등급으로 고를 때는 기본으로 빠진다.시험해 볼 때만

품질 숫자는 사내에서 성능을 재어 정한다. 아직 재지 않은 모델은 X로 들어오고, 측정이 끝나면 숫자를 받는다. 지금 붙어 있는 등급은 측정 전에 정한 임시 값이라 바뀔 수 있다. 등급이 바뀌면 grade:로 부르는 호출이 다른 모델로 갈 수 있다.

지금 있는 모델 보기

지금 어떤 모델이 어떤 등급으로 있는지는 GET /v1/models(키 필요)나 샘플에서 본다. 모델은 더해지고 바뀌므로 이 페이지에는 목록을 적지 않는다. 목록에는 그 키로 쓸 수 있는 모델만 나오므로 키마다 다르게 보인다. 같은 모델이 여러 서버에 있어도 목록에는 한 번만 나온다.

curl $OPENAI_BASE_URL/models -H "Authorization: Bearer $OPENAI_API_KEY"

응답은 {"object":"list","data":[…]}이고, data의 항목 하나는 이렇게 생겼다.

{"id":"qwen3-8b","object":"model","created":1677610602,"owned_by":"openai","grade":"C2","ctx":40960,"dim":null,"langs":["ko","en"],"tools":true,"note":"기본값(Q4_K_M)"}
필드뜻
id모델 이름. model에 이 값을 적어 부른다.
grade등급(종류+품질).
ctx한 번에 다룰 수 있는 최대 토큰 수(컨텍스트 길이).
dim임베딩 벡터의 길이. 임베딩 모델에만 있고, 나머지는 null이다.
langs지원 언어.
tools도구 호출을 받는지.
note짧은 설명.
object, created, owned_byOpenAI 형식을 맞추려고 들어 있는 필드다. 모델을 가리는 데는 쓰지 않는다.

모델을 고르는 세 가지 방법

1. 이름을 그대로 쓴다. GET /v1/models에서 본 id를 model에 적는다. 언제 불러도 그 모델이 답하므로, 결과가 한결같아야 하는 서비스 코드에 맞다.

{"model": "qwen3-8b", "messages": [{"role": "user", "content": "안녕하세요"}]}

2. GET /v1/pick으로 이름을 받는다. 등급과 조건을 주면 이 키로 쓸 수 있는 모델 하나를 골라 이름과 제원을 알려 준다. 모델을 부르지는 않는다. 서비스라면 시작할 때 한 번 pick해서 고른 이름을 로그에 남기고 그 이름으로 부른다. 코드에 이름을 박지 않으면서도 실행 중에는 모델이 바뀌지 않는다.

curl "$OPENAI_BASE_URL/pick?grade=C2" -H "Authorization: Bearer $OPENAI_API_KEY"
{"object":"model.pick","id":"qwen3-8b","grade":"C2","ctx":40960,"dim":null,"langs":["ko","en"],"tools":true,"note":"기본값(Q4_K_M)","reason":"best-quality"}

3. model에 grade:를 적는다. 호출의 model 자리에 등급을 적으면, 호출을 받을 때마다 같은 규칙으로 모델을 골라 처리한다. 조건은 ? 뒤에 붙인다. SDK를 쓰지 않는 곳(curl, 도구의 설정 화면)이나 시험에 편하다. 다만 호출마다 다시 고르므로, 그사이 등급이 바뀌면 다른 모델이 답할 수 있다. 실제로 답한 모델은 응답의 model과 헤더 x-kephas-picked-model에 있다.

{"model": "grade:C2", "messages": […]}
{"model": "grade:C?max_quality=2&tools=true", "messages": […]}

2와 3은 고르는 규칙이 같다. 조건에 맞는 모델 가운데 이 키로 쓸 수 있는 것만 남기고, 품질 숫자가 작은(좋은) 것을 고른다. 품질이 같으면 기본으로 표시된 모델을, 그것도 가릴 수 없으면 먼저 등록된 모델을 고른다. 그래서 품질을 적지 않은 grade=C는 이 키로 쓸 수 있는 가장 좋은 C 모델이 된다. pick 응답의 reason은 어떻게 골랐는지를 알려 준다: best-quality(그 품질의 후보가 하나뿐), default(기본 표시 모델), first-registered(먼저 등록된 모델).

조건뜻예
grade필수. 종류 한 글자, 또는 종류+품질.C, C2, E
max_quality이 품질보다 좋은 등급은 빼고, 같거나 낮은 것 가운데서 고른다. 1·2·3 중 하나.2면 1은 빼고 2·3에서
min_ctxctx가 이 값 이상인 모델만.65536
toolstrue면 도구 호출을 받는 모델만.true
langs적은 언어를 모두 지원하는 모델만. 쉼표로 구분한다.ko,en
allow_xtrue면 실험(X) 모델도 후보에 넣는다. CX처럼 X를 직접 적으면 저절로 들어간다.true

조건에 맞는 모델이 하나도 없으면 404, 있지만 이 키로 쓸 수 없으면 403이다. 등급의 종류는 부르는 곳과 맞아야 한다. 임베딩(/v1/embeddings)에는 E만, 대화와 텍스트 완성에는 C·K·V만 쓸 수 있고, 어긋나면 400이다.

키의 종류

키마다 허용 범위가 있어서, 그 키로 부를 수 있는 모델이 정해진다. 범위는 키를 발급하는 사람이 키 발급·관리 페이지의 허용 범위 칸에서 고른다(키 받기).

종류쓸 수 있는 모델만료쓰임
member품질 2·3 등급 전부와 E130·90·365일 또는 없음. 기본 90일기본 범위. 발급할 때 범위를 따로 정하지 않으면 이것이다.
developer전부. 품질 1과 실험(X)까지위와 같다품질 1 모델이나 실험 모델이 필요할 때
모델 직접 지정발급할 때 하나하나 고른 모델만위와 같다정해진 모델만 쓰게 할 때
서비스용모델 직접 지정없음프로그램(서비스)이 쓰는 키. 만료 때문에 서비스가 멈추지 않도록 만료 없이 발급한다.

키에는 이 밖에 이름(용도), 비용 한도(선택), 메모가 붙는다. 사내 모델 사용량은 비용 0으로 집계된다. 분당 요청 수 한도를 둔 키는 한도를 넘으면 429를, 비용 한도를 넘은 키는 422(budget_exceeded)를 받는다.

그 키로 부를 수 있는 모델은 GET /v1/models에 나오는 것뿐이다. 범위 밖의 모델이나 없는 모델 이름을 부르면 403(key_model_access_denied)이 온다. 키 발급·관리 페이지에서는 발급한 키의 범위를 바꿀 수 없으므로, 범위를 넓혀야 하면 필요한 범위로 새 키를 발급해 바꿔 쓰고 쓰던 키는 삭제한다.

기록되는 것과 되지 않는 것

항목기록
요청 수키별로 남는다.
입력·출력 토큰 수남는다. 응답의 usage에 적힌 그 숫자다. 스트리밍 호출도 남는다.
부른 모델남는다. grade:로 불렀으면 실제로 골라진 모델 이름으로 남는다.
시각남는다.
프롬프트(messages, prompt, input, 넣은 이미지)저장하지 않는다.
응답 본문(답, 생각, 벡터)저장하지 않는다.

키별 사용량은 사내망에서 연 키 발급·관리 페이지의 키 상세에서 본다. 모델별 요청 수와 입력·출력 토큰 수, 그리고 최근 호출 30건(시각, 모델, 토큰)이다. 이 페이지에는 로그인이 없으므로 사내망에서 연 사람은 누구나 모든 키의 사용량을 볼 수 있다. 프롬프트와 응답 본문은 저장하지 않으므로 여기에도 나오지 않는다. 키 하나를 여럿이 쓰면 이 기록으로 사람을 가릴 수 없으므로, 키는 사람마다, 서비스마다 따로 발급한다.

알아 둘 점

처음 부르는 모델은 첫 응답이 느릴 수 있다

한동안 쓰이지 않은 모델은 처음 부를 때 서버가 모델을 메모리에 올리느라 첫 응답이 수십 초 걸릴 수 있다. 이어지는 호출은 빠르다. 프로그램의 시간 제한(timeout)은 이 첫 호출을 견딜 만큼 넉넉히 둔다.

품질 1은 느리다

품질 1 모델은 크고 느리다. 빨리 답해야 하는 일에는 품질 2나 3을 쓴다.

생각 모델과 max_tokens

생각(reasoning) 모델(예: qwen3-8b, gpt-oss-120b)은 답(content)보다 생각 과정(reasoning_content)을 먼저 만든다. 생각도 출력 토큰을 쓰므로, max_tokens를 작게 주면 생각만 하다가 끝나 content가 빈 문자열로 온다. C2 모델에 "max_tokens": 16으로 "대한민국의 수도는 어디인가요?"를 보냈을 때 실제로 이렇게 왔다.

choices[0].finish_reason              "length"
choices[0].message.content            ""
choices[0].message.reasoning_content  "…"    (생각 도중에 끊긴 문자열)
usage.completion_tokens               16

finish_reason이 length이고 content가 비어 있으면 이 경우다. max_tokens는 넉넉히 주거나 아예 지정하지 않는다. 스트리밍에서도 생각이 delta.reasoning_content로 먼저 오고 답이 뒤따른다.

점검·전환 중에는 503, 잠시 뒤 다시 부른다

점검이나 엔진 전환 중에는 일부 모델이 1~2분 동안 503을 돌려준다. 모델 서버 쪽에서 오류가 나면 502가 온다. 어느 쪽이든 다른 모델로 바꿔 주지 않으므로, 잠시 기다렸다가 같은 요청을 다시 보낸다. 프로그램이라면 간격을 늘려 가며 몇 번 다시 시도하게 만든다.

여럿이 함께 쓰는 서버다

모델 서버는 사내 여러 사람이 함께 쓴다. 큰 작업이 몰리면 모두의 응답이 느려질 수 있다. 응답이 평소보다 느리면 이 때문일 수 있다.

요청 크기와 시간

한 요청의 본문은 32MB까지 받는다. 넘으면 처리하지 않고 본문이 빈 502를 돌려준다. 이미지는 base64로 바꾸면 원래 파일보다 3분의 1쯤 커지므로, 큰 이미지는 줄여서 보낸다. 한 요청은 600초 안에 끝나야 한다.

실패하면 보이는 것

오류는 OpenAI와 같은 모양 {"error":{"message":"…","type":"…","code":…}}으로 온다. 이 입구가 직접 낸 오류는 code가 숫자이고 메시지가 한국어로 시작한다. 모델 이름으로 직접 부른 호출에서 난 오류는 메시지가 영어이고 code가 문자열("403")일 수 있다. 무슨 오류인지는 HTTP 상태 코드로 가린다.

상태뜻할 일
400요청 인자가 틀렸다. 등급 종류가 부르는 곳과 맞지 않을 때도 여기다.메시지를 보고 요청을 고친다.
401키가 없거나, 틀렸거나, 정지·만료됐다. 정지·만료면 메시지에 사유가 있다.키를 확인한다. 정지·만료면 사내망에서 키 발급·관리 페이지를 열어 재개하거나 연장한다.
403이 키로 쓸 수 없는 모델이거나 없는 모델 이름이다. 등급으로 골랐다면, 조건에 맞는 모델은 있지만 이 키의 범위 밖이라는 뜻이다.GET /v1/models로 쓸 수 있는 모델을 확인한다. 범위를 넓혀야 하면 그 범위로 새 키를 발급한다.
404등급 조건에 맞는 모델이 없다. GET /v1/models/{id}에서는 없거나 이 키로 쓸 수 없는 모델이다.조건을 완화한다(품질, min_ctx, tools, langs, 실험 모델은 allow_x=true).
429분당 요청 수 한도를 넘었다. 한도를 둔 키에서만 난다.잠시 뒤 다시 보낸다. 한도가 맞지 않으면 관리자와 상의한다.
502모델 서버 쪽에서 오류가 났다. 요청 본문이 32MB를 넘었을 때도 502가 온다(이때는 본문이 비어 있다).잠시 뒤 다시 부른다. 계속되면 관리자에게 알린다.
503모델 서버가 응답하지 않는다(점검, 엔진 전환 중 등).1~2분 뒤 다시 부른다.

실제로 받은 오류 몇 가지:

# 키 없이 모델 목록 — 401
{"error":{"message":"Authorization: Bearer <키>가 필요하다","type":"authentication_error","code":401}}

# 틀린 키 — 401
{"error":{"message":"키가 유효하지 않다","type":"authentication_error","code":401}}

# member 키로 범위 밖 모델을 이름으로 — 403
{"error":{"message":"The requested model 'gpt-oss-120b' is not available for this API key, or the model name is invalid. Check the models available to you and try again.","type":"key_model_access_denied","param":"model","code":"403"}}

# member 키로 grade:C1 — 403
{"error":{"message":"조건에 맞는 모델은 있으나 이 키로는 쓸 수 없다","type":"model_pick_failed","code":403}}

# 조건에 맞는 모델이 없는 pick — 404
{"error":{"message":"조건에 맞는 모델이 없다. 조건을 완화하라(품질·min_ctx·tools·langs, 실험 모델은 allow_x=true)","type":"model_pick_failed","code":404}}

# 임베딩에 grade:C — 400
{"error":{"message":"임베딩에는 E 등급만 쓸 수 있다(요청: grade:C)","type":"invalid_request_error","code":400}}

어디로 가면 되나

페이지무엇이 있나
API 설명 사용 안내서. 처음 시작하기부터 키 받기, 등급과 모델 고르기, 클라이언트별 설정, 오류 대처, 자주 묻는 질문까지 차례로 설명한다. 처음 쓰는 사람은 여기서 시작한다.
API 레퍼런스 엔드포인트별 요청 인자, 응답 필드, 오류 코드와 메시지의 정확한 명세. 코드를 쓰다가 필드 하나, 오류 하나를 찾아볼 때.
샘플 키를 넣고 순서대로 실행해 보는 안내 문서. 실행 블록이 설명 사이에 있다. 요청은 그 브라우저에서 llm.kephas.kr로 바로 가고, 넣은 키는 그 브라우저(localStorage)에만 저장된다.
키 발급·관리 키를 쓸 사람이 직접 발급하고 관리하는 페이지. 키 발급, 정지·재개, 연장, 삭제, 키별 사용량. 로그인은 없고, 사내망에서 사내 주소로 직접 열었을 때만 발급과 관리가 된다. 공인 주소로 열면 외부 입구를 거치므로 막히고, 대신 사내에서 직접 여는 주소를 알려 준다. 사내망의 누구나 모든 키를 보고 다룰 수 있으므로, 발급할 때 이름에 쓰는 사람과 용도를 적는다.

상태

/healthz는 입구가 요청을 받고 있는지만 알려 준다. 모델 서버가 살아 있는지, 어떤 모델이 지금 답할 수 있는지는 알려 주지 않는다. GET /v1/models에 나오는 모델도 그 키로 쓸 수 있다는 뜻이지, 지금 답할 수 있다는 뜻은 아니다. 특정 모델의 상태는 그 모델을 짧게 불러 보면 안다. 모델 서버가 응답하지 않으면 503이 온다.