키를 받아 첫 호출을 하는 데서 시작해 모델 고르기, 대화와 스트리밍, 임베딩, 이미지 입력, 도구 호출, 언어별 설정, 오류 대처까지 순서대로 따라 할 수 있게 쓴 사용 안내서다.
API Base URL은 https://llm.kephas.kr/v1이고, 인증은 요청 머리글 Authorization: Bearer <키> 한 줄이다. 사내망과 외부에서 같은 주소를 쓴다.
LLM Registry API는 케파스 사내 LLM 여러 개를 OpenAI API와 같은 형식의 입구 하나로 묶은 것이다. 사용자는 키 하나만 가지고, 그 키로 허용된 모델을 이름이나 등급으로 골라 부른다. 모델이 어느 서버에서 어떤 방식으로 도는지는 알 필요가 없고, 그쪽이 바뀌어도 주소·키·코드는 그대로다.
형식은 OpenAI API의 모델 목록(Models), 대화(Chat Completions, 스트리밍 포함), 완성(Completions), 임베딩(Embeddings)이다. 그래서 openai 라이브러리(Python·Node), LangChain(langchain-openai), 그 밖의 OpenAI 호환 도구에 Base URL과 키만 넣으면 된다. 이 서비스 전용 SDK는 아직 없다. OpenAI에 없는 것이 두 가지 더 있다. 모델마다 붙은 등급과, 등급과 조건을 주면 모델 하나를 골라 주는 GET /v1/pick이다.
이 페이지는 쓰는 방법을 설명한다. 엔드포인트별 인자·응답·오류의 정확한 명세는 API 레퍼런스에, 키를 넣고 브라우저에서 순서대로 실행해 보는 안내는 샘플에 있다. 아래 예시의 응답은 2026년 11월에 실제로 받은 것이다. 응답은 원래 한 줄짜리 JSON으로 오지만 읽기 쉽게 줄을 나누기도 했고, 긴 값과 이 설명에 필요 없는 필드는 …로 줄였다. 모델과 등급은 바뀌므로 같은 요청이라도 답은 다를 수 있다.
키를 받아 첫 대답을 읽기까지 다섯 단계다. 명령은 bash 기준이다(Linux, macOS, Windows의 Git Bash·WSL). Windows PowerShell이나 명령 프롬프트(cmd)를 쓰면 ②는 아래 「환경변수 설정」을, ③·④는 「클라이언트별 설정」의 Windows PowerShell 예를 따른다.
키는 쓸 사람이 사내망에서 직접 발급한다. 사내망에 연결된 PC에서 키 발급·관리 페이지를 열고, 이름(용도) 칸에 쓰는 사람과 용도(예: "홍길동 RAG 실험")를 적고, 허용 범위와 만료를 고른 뒤 [발급]을 누른다. 처음이면 허용 범위는 member, 만료는 90일 그대로 두면 된다. 그러면 sk-로 시작하는 긴 키가 화면에 한 번만 보인다. 페이지를 떠나거나 새로 고치면 다시 볼 수 없으니, 바로 복사해 안전한 곳에 옮겨 둔다.
공인 주소 https://llm.kephas.kr로 연 키 발급·관리 페이지는 외부 입구를 거치므로, 사내 PC에서 열어도 발급이 막힌다. 이때 페이지가 사내에서 직접 여는 주소를 알려 주니 그 주소로 다시 연다. 사내망이 필요한 것은 발급과 관리뿐이고, 발급한 키는 사내든 외부든 어디서나 쓴다. 각 칸의 뜻과 발급한 뒤의 관리는 아래 「키」에 있다.
주소와 키를 환경변수 두 개에 넣는다. 이름을 OPENAI_BASE_URL과 OPENAI_API_KEY로 하면 openai 라이브러리가 따로 설정하지 않아도 이 둘을 읽는다.
export OPENAI_BASE_URL=https://llm.kephas.kr/v1 export OPENAI_API_KEY=sk-…받은 키…
이 설정은 지금 연 터미널에서만 유지된다. 터미널을 새로 열 때마다 다시 넣지 않으려면 아래 「환경변수 설정」을 본다.
키가 맞는지, 그 키로 어떤 모델을 쓸 수 있는지를 먼저 본다.
curl -s $OPENAI_BASE_URL/models -H "Authorization: Bearer $OPENAI_API_KEY"
{"object":"list","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":"gemma3-4b","object":"model",…,"grade":"C3",…},
…
{"id":"qwen3-embedding-0.6b","object":"model",…,"mode":"embedding","grade":"E2","ctx":32768,"dim":1024,"langs":["ko","en"],"tools":false,"note":"다국어 임베딩"}]}
목록이 나오면 키와 주소가 맞다. id가 호출할 때 쓰는 모델 이름이고 grade가 등급이다. 각 필드의 뜻은 아래 「모델 목록과 등급」에 있다. 목록 대신 {"error":…}가 오면 message를 읽는다. 키를 넣지 않았으면 Authorization: Bearer <키>가 필요하다, 키가 틀렸으면 키가 유효하지 않다가 온다. 둘 다 상태 코드 401이다.
한 줄짜리 JSON을 보기 좋게 보려면 명령 뒤에 | jq .(jq가 있을 때)나 | python -m json.tool을 붙인다.
모델 이름 자리에 grade:C2를 넣으면 이 키로 쓸 수 있는 범용 대화 모델(C2 등급)이 골라진다. ③에서 본 이름(예: qwen3-8b)을 직접 넣어도 된다. -i는 응답 머리글까지 보여 달라는 뜻이다.
curl -s -i $OPENAI_BASE_URL/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"grade:C2","messages":[{"role":"user","content":"대한민국의 수도는 어디인가요? 한 문장으로 답하세요."}]}'
HTTP/2 200
content-type: application/json
…
x-kephas-picked-grade: C2
x-kephas-picked-model: qwen3-8b
{"id":"chatcmpl-725","created":1794188747,"model":"qwen3-8b","object":"chat.completion",…,
"choices":[{"finish_reason":"stop","index":0,
"message":{"content":"대한민국의 수도는 서울입니다.",
"role":"assistant",
"reasoning_content":"Okay, the user is asking about the capital of South Korea. Let me recall. … Yeah, that's correct.\n",
…}}],
"usage":{"completion_tokens":127,"prompt_tokens":29,"total_tokens":156,…}}
Windows의 Git Bash나 명령 프롬프트에서 curl 명령줄에 한글을 직접 넣으면 한글이 깨진 채 전달될 수 있다. 이때 모델은 엉뚱한 대답을 하거나 400이 온다. Windows에서는 본문을 파일로 저장해 보내거나(「클라이언트별 설정」의 curl), PowerShell 함수나 라이브러리를 쓴다. WSL 안의 curl은 문제가 없다.
| 어디 | 무엇 |
|---|---|
choices[0].message.content | 모델의 대답. 대부분은 이것만 읽으면 된다. bash에서는 | jq -r '.choices[0].message.content'로 대답만 뽑을 수 있다. |
choices[0].message.reasoning_content | 생각(reasoning) 모델이 대답하기 전에 만든 생각 과정. 생각 모델이 아니면 없다. 사용자에게 보여 줄지는 쓰는 쪽이 정한다(「생각(reasoning) 모델 다루기」). |
choices[0].finish_reason | 끝난 이유. stop은 모델이 스스로 끝낸 것, length는 max_tokens에 닿아 잘린 것, tool_calls는 도구를 불러 달라는 요청이다(「도구 호출」). |
model | 실제로 답한 모델 이름. grade:로 불러도 여기에는 실제 이름이 온다. |
usage | 입력 토큰 수(prompt_tokens)와 출력 토큰 수(completion_tokens). 생각 과정도 출력 토큰에 들어간다. 위 예는 한 문장 대답에 출력 토큰이 127개였다. 이 숫자가 키별 사용 기록에 남는다. |
머리글 x-kephas-picked-model, x-kephas-picked-grade | grade:로 불렀을 때 실제로 고른 모델과 그 등급. 모델을 이름으로 부르면 붙지 않는다. |
id, created, object | OpenAI 형식의 응답 번호, 만든 시각(유닉스 시간), 응답의 종류. |
여기까지 되면 준비는 끝났다. 이어지는 절은 이 다섯 단계와 그다음에 쓸 기능을 하나씩 자세히 설명한다.
키는 쓸 사람이 키 발급·관리 페이지에서 직접 발급한다. 누구에게 요청할 필요가 없고, 로그인도 없다. 대신 이 페이지는 사내망에서 사내 주소로 직접 열었을 때만 키를 발급하고 관리한다. 공인 주소 https://llm.kephas.kr로 들어오는 접속은 모두 외부 입구를 거치므로, 사내 PC에서 https://llm.kephas.kr/portal을 열어도 발급과 관리가 막힌다. 이때 페이지가 사내에서 직접 여는 주소를 알려 준다.
발급은 다음 순서로 한다.
OPENAI_API_KEY에 넣는다. OPENAI_BASE_URL에는 https://llm.kephas.kr/v1을 넣는다.사내망이 필요한 것은 발급과 관리뿐이다. 발급한 키는 사내든 외부든 어디서나 https://llm.kephas.kr/v1로 쓰고, API를 부르는 방법도 어디서나 같다. 외부에서는 발급할 수 없으므로, 밖에서 쓸 키도 사내에 있을 때 미리 발급해 둔다.
로그인이 없으므로 사내망에서 이 페이지를 연 사람은 누구나 발급된 모든 키의 이름·허용 범위·만료·사용량을 보고, 어느 키든 정지·재개·연장·삭제할 수 있다. 이름에 쓰는 사람과 용도를 적는 것은 이 때문이다. 키를 정지하거나 삭제하기 전에는 이름을 보고 내 키인지 확인한다. 키 문자열 전체는 발급한 순간에만 보이므로, 이 페이지를 연다고 남의 키로 API를 부를 수 있게 되지는 않는다.
키는 sk-로 시작하는 긴 문자열이다. 전체 문자열은 발급 직후 한 번만 보이고, 그 페이지를 떠나거나 새로 고치면 다시 볼 수 없다. 그 뒤 키 발급·관리 페이지의 목록에는 sk-...끝4자처럼 끝 네 글자만 보인다. 발급한 사람도 다른 누구도 전체 문자열을 다시 볼 수 없으므로, 보이는 자리에서 바로 옮겨 둔다.
키는 비밀번호처럼 다룬다. 키를 가진 사람은 누구든 그 키의 범위 안에서 모델을 부를 수 있고, 그 사용은 키 주인의 기록으로 남는다.
.env 파일에 두고, 그 파일은 저장소에 올리지 않는다(.gitignore에 .env를 넣는다. 「환경변수 설정」).키에는 만료가 있다. 발급할 때 30·90·365일 또는 만료 없음 중에서 정하고, 따로 정하지 않으면 90일이다. 만료된 키로 부르면 401이 오고 message에 Expired Key가 들어 있다. 정지된 키로 부르면 401과 함께 Key is blocked가 온다.
만료가 가까우면 사내망에서 키 발급·관리 페이지를 열어 발급된 키 목록에서 그 키를 고르고, 키 상세에서 30·90·365일 가운데 하나를 골라 연장한다. 연장하면 만료일이 연장한 때부터 그 기간 뒤로 다시 정해진다. 남은 기간에 더해지는 것이 아니다. 이미 만료된 키도 연장하면 다시 쓸 수 있고, 정지된 키는 키 상세에서 재개하면 다시 쓸 수 있다. 정지·재개·연장·삭제는 모두 즉시 반영되므로, 연장되거나 재개된 키는 바꿀 것 없이 그대로 계속 쓴다. 만료일은 발급 직후 화면과 발급된 키 목록, 키 상세에 나온다.
키는 되살릴 수 없다. 사내망에서 키 발급·관리 페이지를 열어 잃어버린 키를 목록에서 이름으로 찾아 삭제하고, 새 키를 발급한다. 이미 있는 이름으로는 발급되지 않으므로, 같은 이름을 다시 쓰려면 잃어버린 키를 먼저 삭제한다. 새 키를 발급하면 환경변수와 .env 파일의 값을 바꾼다.
사용 기록은 키 단위로 남는다. 키 하나를 여럿이 쓰면 누가 얼마나 썼는지 나눌 수 없고, 한 사람 때문에 키를 정지하면 모두가 함께 멈춘다. 사람마다, 프로그램마다 따로 발급한다.
키마다 쓸 수 있는 모델의 범위(허용 범위)가 정해져 있다. 범위 밖의 모델은 /v1/models 목록에 나오지 않고, 이름으로 불러도 403이 온다.
| 종류 | 쓸 수 있는 모델 | 만료 | 맞는 자리 |
|---|---|---|---|
| member | 품질 2·3 등급 전부와 E1. 그 밖의 품질 1과 실험(X)은 쓸 수 없다. | 30·90·365일 또는 없음(기본 90일) | 대부분의 사용자. 일상 업무와 개발. |
| developer | 전부. 품질 1과 실험(X) 등급을 포함한다. | 위와 같다 | 큰 모델이나 실험 모델을 시험해야 하는 개발자. |
| 모델 직접 지정 | 발급할 때 고른 모델만. | 위와 같다 | 정해진 모델만 쓰게 하고 싶을 때. |
| 서비스용 | 발급할 때 고른 모델만. | 없음 | 사람이 아니라 프로그램(서비스)이 쓰는 키. |
member와 developer는 등급으로 범위를 정한다. 그래서 새 모델이 그 범위의 등급으로 들어오면 키를 다시 발급하지 않아도 바로 목록에 보인다. 모델을 직접 지정한 키와 서비스용 키는 고른 모델만 쓴다. 키 발급·관리 페이지에서는 발급한 키의 범위를 바꿀 수 없으므로, 쓰던 모델이 바뀌거나 빠지면 새 모델을 고른 키를 새로 발급해 바꿔 쓰고 쓰던 키는 삭제한다. member 키로 품질 1 모델이 필요해진 경우처럼 범위를 넓혀야 할 때도 그 범위로 새 키를 발급한다.
주소와 키는 환경변수 OPENAI_BASE_URL과 OPENAI_API_KEY에 넣는다. openai 라이브러리(Python·Node)는 클라이언트를 만들 때 아무 인자도 주지 않으면(OpenAI(), new OpenAI()) 이 두 변수를 저절로 읽는다. 그 밖의 도구도 이 이름을 따르는 경우가 많다. 이렇게 두면 코드에 키를 적지 않아도 되고, 키를 바꿀 때 코드를 고치지 않아도 된다.
export OPENAI_BASE_URL=https://llm.kephas.kr/v1 export OPENAI_API_KEY=sk-…
지금 연 터미널과 거기서 실행한 프로그램에만 적용된다. 확인은 echo $OPENAI_BASE_URL로 한다. 터미널을 열 때마다 적용되게 하려면 같은 두 줄을 ~/.bashrc(bash)나 ~/.zshrc(zsh, macOS의 기본 셸)의 끝에 넣고 터미널을 새로 연다.
이 방법은 키를 홈 디렉터리의 설정 파일에 평문으로 남긴다. 여럿이 함께 쓰는 서버나 공용 컴퓨터에서는 영구 설정을 하지 말고 쓸 때마다 넣거나, 아래 .env 파일을 chmod 600 .env로 본인만 읽게 해서 쓴다.
지금 창에서만 쓸 때:
$env:OPENAI_BASE_URL = "https://llm.kephas.kr/v1" $env:OPENAI_API_KEY = "sk-…"
확인은 echo $env:OPENAI_BASE_URL로 한다. 창을 닫으면 사라진다.
이 사용자 계정에 영구히 둘 때:
setx OPENAI_BASE_URL "https://llm.kephas.kr/v1" setx OPENAI_API_KEY "sk-…"
setx는 이 사용자의 환경변수에 값을 저장한다. 이미 열려 있는 창에는 반영되지 않는다. 새로 연 PowerShell, 명령 프롬프트, 편집기부터 적용되며, 편집기 안의 터미널은 편집기를 다시 띄워야 한다. 지금 창에서도 바로 쓰려면 위의 $env: 두 줄을 함께 실행한다. 영구 설정을 지우려면 다음을 실행한다.
[Environment]::SetEnvironmentVariable("OPENAI_API_KEY", $null, "User")
[Environment]::SetEnvironmentVariable("OPENAI_BASE_URL", $null, "User")
set OPENAI_BASE_URL=https://llm.kephas.kr/v1 set OPENAI_API_KEY=sk-…
= 앞뒤에 빈칸을 두지 않고, 값을 따옴표로 감싸지 않는다(감싸면 따옴표까지 값이 된다). 확인은 echo %OPENAI_BASE_URL%로 한다. 지금 창에서만 유지되며, 영구히 두려면 위의 setx를 명령 프롬프트에서도 똑같이 쓴다.
프로젝트마다 다른 키를 쓰거나 터미널 설정을 건드리고 싶지 않으면 프로젝트 폴더에 .env 파일을 둔다.
OPENAI_BASE_URL=https://llm.kephas.kr/v1 OPENAI_API_KEY=sk-…
이 파일은 저절로 읽히지 않는다. 프로그램이 시작할 때 읽게 한다.
# Python (pip install python-dotenv) from dotenv import load_dotenv load_dotenv() # .env를 읽어 환경변수로 올린다. OpenAI()를 만들기 전에 부른다 # Node 20.6 이상: 실행할 때 파일을 지정한다 node --env-file=.env app.mjs
그리고 .env가 저장소에 올라가지 않게 .gitignore에 한 줄을 넣는다.
echo ".env" >> .gitignore
이미 커밋해 올렸다면 파일을 지워도 저장소 기록에 키가 남는다. 사내망에서 키 발급·관리 페이지를 열어 그 키를 정지하거나 삭제하고 새 키를 발급한다.
함께 일하는 사람에게는 값을 비운 .env.example을 저장소에 두어, 어떤 변수가 필요한지만 알린다.
GET /v1/models는 이 키로 쓸 수 있는 모델만 돌려준다. 키마다 결과가 다를 수 있고, 같은 키라도 모델이 들어오고 빠지면 달라진다. 지금 어떤 모델과 등급이 있는지는 언제나 이 목록이나 샘플에서 본다.
curl -s $OPENAI_BASE_URL/models -H "Authorization: Bearer $OPENAI_API_KEY"
{
"object": "list",
"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": "qwen3-embedding-0.6b", "object": "model", "created": 1677610602, "owned_by": "openai", "mode": "embedding",
"grade": "E2", "ctx": 32768, "dim": 1024, "langs": ["ko", "en"], "tools": false, "note": "다국어 임베딩"}
]
}
| 필드 | 뜻 |
|---|---|
id | 모델 이름. 호출의 model에 이 값을 넣는다. 실제 모델 이름 그대로이고 별칭은 없다. |
object | 늘 "model". |
created, owned_by | OpenAI 형식을 맞추려고 채운 값이다. 모델이 언제 들어왔는지, 누가 만들었는지를 뜻하지 않는다. |
grade | 등급. 종류 한 글자와 품질 한 글자(아래 「등급 체계」). |
ctx | 최대 컨텍스트 길이(토큰 수). 대화 모델은 입력과 출력을 합한 길이, 임베딩 모델은 입력 하나의 길이 한도다. |
dim | 임베딩 벡터의 차원(숫자 개수). 임베딩 모델에만 있고, 다른 모델은 null. |
langs | 다루는 언어의 코드. 예: ["ko","en"]. |
tools | 도구 호출(function calling)을 지원하면 true. |
note | 짧은 설명. 기본값 표시, 용도, 주의할 점 같은 것이 들어간다. |
mode | 임베딩 모델에는 "embedding"이 붙는다. |
목록에는 실제 모델만 나온다. grade:C2 같은 등급 이름은 목록에 없다(「모델 고르기」). 같은 모델이 여러 서버에 올라 있어도 목록에는 한 번만 나오고, 어느 서버가 요청을 받을지는 서비스가 정한다.
curl -s $OPENAI_BASE_URL/models/qwen3-8b -H "Authorization: Bearer $OPENAI_API_KEY"
{"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)"}
없는 모델이거나 이 키의 범위 밖이면 404가 온다. 두 경우를 구분하지 않는다. 다음은 member 키로 품질 1 모델을 조회한 예다.
{"error":{"message":"모델이 없거나 이 키로 쓸 수 없다: gpt-oss-120b","type":"invalid_request_error","code":404}}
등급은 두 글자다. 앞 글자는 무엇을 하는 모델인지(종류), 뒤 글자는 어느 수준인지(품질)를 말한다. 예를 들어 C2는 범용 대화 모델, E2는 범용 임베딩 모델, CX는 검증 중인 대화 모델이다. 대소문자는 가리지 않는다(c2와 C2는 같다).
| 종류 | 뜻 |
|---|---|
| C | 대화·추론·문서 작성 |
| K | 코드 생성·수정 |
| E | 임베딩(검색·RAG) |
| V | 이미지 입력 |
| 품질 | 뜻 | 특징 | 맞는 자리 |
|---|---|---|---|
| 1 | 사내 최상 | 느릴 수 있다. 컨텍스트가 길다. | 결과의 질이 가장 중요할 때 |
| 2 | 범용 | 빠르고 품질이 충분하다. | 일상 업무의 기본값 |
| 3 | 경량 | 매우 빠르다. 복잡한 추론에는 맞지 않는다. | 분류·추출·테스트 |
| X | 실험 | 검증 전이다. 예고 없이 바뀌거나 사라질 수 있다. 기본 선택에서 빠진다. | 시험해 볼 때만 |
숫자가 작을수록 품질이 높다. 품질 숫자는 내부 성능 측정으로 정한다.
측정하기 전의 모델은 X로 들어오고, 측정이 끝나면 숫자를 받는다. 지금 붙어 있는 등급은 측정 전 값이라 바뀔 수 있다. 등급이 바뀌면 같은 grade:C2나 pick 조건이 다른 모델을 고를 수 있다. 결과가 늘 같아야 하는 프로그램은 「모델 고르기」의 「서비스 코드에서」 방식을 따른다.
X 등급 모델은 기본 선택에서 빠진다. pick이나 grade:로 고를 때 X를 후보에 넣으려면 allow_x=true를 주거나 CX처럼 X를 직접 쓴다. member 키로는 X 모델을 쓸 수 없다.
모델을 정하는 방법은 세 가지다. 모두 같은 키로 쓰고, 한 프로그램 안에서 섞어 써도 된다.
| 방법 | 어떻게 | 맞는 자리 | 알아 둘 점 |
|---|---|---|---|
| 이름 | model에 /v1/models의 id를 넣는다(예: "qwen3-8b"). | 결과가 늘 같아야 하는 코드. 임베딩을 저장해 두고 쓰는 코드는 반드시 이 방법. | 그 모델이 빠지면 403이 온다. 그때 이름을 바꾼다. |
GET /v1/pick | 등급과 조건을 주면 모델 하나의 이름과 제원을 돌려준다. 그 이름으로 부른다. | 이름을 코드에 박고 싶지 않은 서비스. 시작할 때 한 번 고른다. | 고른 이름을 로그에 남기면 언제 어느 모델을 썼는지 알 수 있다. |
model에 grade: | model에 "grade:C2"처럼 등급을 쓰면 호출할 때마다 골라서 처리한다. | 설정 화면에 모델 이름만 넣을 수 있는 도구, curl 시험, 짧은 스크립트. | 호출마다 다시 고르므로, 쓰는 도중 등급이 바뀌면 다른 모델이 답할 수 있다. |
pick과 grade:는 같은 규칙으로 고른다.
품질 글자를 생략하면(C) 그 종류에서 이 키로 쓸 수 있는 가장 좋은 모델을, 품질까지 쓰면(C2) 그 품질 안에서 고른다. 그래서 member 키로 grade=C를 주면 품질 1 모델은 범위 밖이라 빠지고 C2 모델이 골라진다. 조건에 맞는 모델이 없을 때 다른 종류나 다른 품질의 모델로 바꿔 주지는 않는다.
curl -s "$OPENAI_BASE_URL/pick?grade=C&max_quality=2&tools=true" -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"}
주소에 &가 들어가므로 셸에서는 주소 전체를 큰따옴표로 감싼다. 응답의 id가 고른 모델 이름이고, grade·ctx·dim·langs·tools·note는 /v1/models의 같은 필드다. reason은 왜 그 모델이 골라졌는지를 말한다.
| reason | 뜻 |
|---|---|
best-quality | 가장 좋은 품질의 후보가 하나뿐이었다. |
default | 같은 품질의 후보가 여럿이어서 기본(default)으로 표시된 모델을 골랐다. |
first-registered | 같은 품질의 후보가 여럿이고 기본 표시가 없어서 먼저 등록된 모델을 골랐다. |
| 인자 | 뜻 | 예 |
|---|---|---|
grade (필수) | 종류 한 글자, 또는 종류와 품질. 대소문자를 가리지 않는다. | C, C2, e, V |
max_quality | 1·2·3 중 하나. 이 품질보다 좋은 등급은 빼고 같거나 낮은 것 중에서 고른다. | 2면 품질 1을 빼고 2·3 중에서 |
min_ctx | 양의 정수. 컨텍스트가 이 이상인 모델만. | 100000 |
tools | true면 도구 호출을 지원하는 모델만. | true |
langs | 쉼표로 구분한 언어 코드. 이 언어를 모두 다루는 모델만. | ko,en |
allow_x | true면 실험(X) 모델도 후보에 넣는다. grade에 X를 직접 쓰면(CX) 저절로 들어간다. | true |
아래는 member 키로 실제로 받은 답이다.
# 가볍고 빠른 대화 모델: 품질 3만
curl -s "$OPENAI_BASE_URL/pick?grade=C&max_quality=3" -H "Authorization: Bearer $OPENAI_API_KEY"
{"object":"model.pick","id":"gemma3-4b","grade":"C3","ctx":131072,…,"reason":"best-quality"}
# 긴 문서: 컨텍스트 10만 토큰 이상
curl -s "$OPENAI_BASE_URL/pick?grade=C&min_ctx=100000" -H "Authorization: Bearer $OPENAI_API_KEY"
{"object":"model.pick","id":"gemma3-4b","grade":"C3","ctx":131072,…,"reason":"best-quality"}
# 도구 호출이 되는 대화 모델
curl -s "$OPENAI_BASE_URL/pick?grade=C&tools=true" -H "Authorization: Bearer $OPENAI_API_KEY"
{"object":"model.pick","id":"qwen3-8b","grade":"C2",…,"tools":true,…,"reason":"best-quality"}
# 임베딩 모델
curl -s "$OPENAI_BASE_URL/pick?grade=E" -H "Authorization: Bearer $OPENAI_API_KEY"
{"object":"model.pick","id":"qwen3-embedding-0.6b","grade":"E2","ctx":32768,"dim":1024,…,"reason":"best-quality"}
min_ctx 예처럼 조건이 품질보다 먼저 적용된다. 컨텍스트가 짧은 C2 모델이 조건에서 빠지고, 조건에 맞는 C3 모델이 골라졌다.
고르지 못하면 이렇게 온다.
# 맞는 모델이 없다: 404
curl -s "$OPENAI_BASE_URL/pick?grade=C&langs=ja" -H "Authorization: Bearer $OPENAI_API_KEY"
{"error":{"message":"조건에 맞는 모델이 없다. 조건을 완화하라(품질·min_ctx·tools·langs, 실험 모델은 allow_x=true)","type":"model_pick_failed","code":404}}
# 맞는 모델은 있지만 이 키(member)로는 못 쓴다: 403
curl -s "$OPENAI_BASE_URL/pick?grade=C1" -H "Authorization: Bearer $OPENAI_API_KEY"
{"error":{"message":"조건에 맞는 모델은 있으나 이 키로는 쓸 수 없다","type":"model_pick_failed","code":403}}
# 인자가 틀렸다: 400
curl -s "$OPENAI_BASE_URL/pick?grade=C&max_quality=5" -H "Authorization: Bearer $OPENAI_API_KEY"
{"error":{"message":"max_quality는 1·2·3 중 하나","type":"model_pick_failed","code":400}}
404면 조건을 줄인다. 403이면 그 등급이 이 키의 범위 밖이다. 꼭 필요하면 그 등급을 포함하는 범위(품질 1이나 실험 X면 developer)로 새 키를 발급한다(「키의 종류」).
대화(/v1/chat/completions), 완성(/v1/completions), 임베딩(/v1/embeddings)의 model 자리에 grade: 뒤로 등급을 쓰면, 위 규칙으로 실제 모델을 골라 그 모델로 처리한다. 조건은 물음표 뒤에 pick과 같은 형식으로 붙인다.
"model": "grade:C2" "model": "grade:C?max_quality=2&tools=true" "model": "grade:E"
응답 본문의 model에는 실제 모델 이름이 오고, 응답 머리글 x-kephas-picked-model과 x-kephas-picked-grade에 고른 모델과 등급이 붙는다(「처음 시작하기」의 ④). 고르지 못하면 pick과 같은 403·404가 온다.
등급의 종류는 엔드포인트와 맞아야 한다. 임베딩에는 E만, 대화와 완성에는 C·K·V만 쓴다. 어긋나면 모델을 부르기 전에 400으로 끝난다.
{"error":{"message":"임베딩에는 E 등급만 쓸 수 있다(요청: grade:C)","type":"invalid_request_error","code":400}}
{"error":{"message":"대화·완성에는 C/K/V 등급만 쓸 수 있다(요청: grade:E)","type":"invalid_request_error","code":400}}
grade:는 호출할 때마다 다시 고르고, 모델 정보는 60초마다 새로 읽는다. 그래서 쓰는 도중 등급이 바뀌면 다음 호출부터 다른 모델이 답할 수 있다. 한 작업 안에서 결과가 일정해야 하면 아래 방식을 쓴다.
오래 도는 프로그램은 시작할 때 한 번 pick해서 고른 이름을 로그에 남기고, 이후에는 그 이름으로 부른다. 이렇게 하면 실행 도중에 모델이 바뀌어 결과가 달라지는 일이 없고, 나중에 로그를 보고 어느 모델이 답했는지 확인할 수 있다. 바뀐 등급을 반영하려면 프로그램을 다시 시작한다.
# Python (pip install openai)
import os
import httpx
from openai import OpenAI
base = os.environ["OPENAI_BASE_URL"]
key = os.environ["OPENAI_API_KEY"]
r = httpx.get(f"{base}/pick", params={"grade": "C", "max_quality": "2", "tools": "true"},
headers={"Authorization": f"Bearer {key}"})
r.raise_for_status()
picked = r.json()
print(f"사용 모델: {picked['id']} ({picked['grade']}, {picked['reason']})") # 로그에 남긴다
MODEL = picked["id"]
client = OpenAI()
resp = client.chat.completions.create(
model=MODEL,
messages=[{"role": "user", "content": "대한민국의 수도는 어디인가요? 한 문장으로 답하세요."}],
)
print(resp.choices[0].message.content)
httpx는 openai 라이브러리를 설치할 때 함께 설치된다. requests를 써도 된다.
Node 18 이상에서는 라이브러리 없이 fetch만으로도 된다.
// app.mjs (Node 18 이상)
const BASE = process.env.OPENAI_BASE_URL;
const auth = { Authorization: `Bearer ${process.env.OPENAI_API_KEY}` };
const r = await fetch(`${BASE}/pick?grade=C&max_quality=2&tools=true`, { headers: auth });
if (!r.ok) throw new Error(`pick 실패 ${r.status}: ${await r.text()}`);
const picked = await r.json();
console.log(`사용 모델: ${picked.id} (${picked.grade}, ${picked.reason})`);
const MODEL = picked.id;
async function ask(text) {
const res = await fetch(`${BASE}/chat/completions`, {
method: "POST",
headers: { ...auth, "Content-Type": "application/json" },
body: JSON.stringify({ model: MODEL, messages: [{ role: "user", content: text }] }),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
return (await res.json()).choices[0].message.content;
}
console.log(await ask("대한민국의 수도는 어디인가요? 한 문장으로 답하세요."));
$ node app.mjs 사용 모델: qwen3-8b (C2, best-quality) 대한민국의 수도는 서울입니다.
POST /v1/chat/completions에 모델과 메시지 목록(messages)을 보낸다. 형식은 OpenAI와 같다.
curl -s $OPENAI_BASE_URL/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"qwen3-8b","messages":[{"role":"user","content":"대한민국의 수도는 어디인가요? 한 문장으로 답하세요."}]}'
messages의 항목마다 role과 content가 있다. role은 system(지시), user(사용자의 말), assistant(모델의 앞선 대답), tool(도구 실행 결과, 「도구 호출」) 넷이다. 대답은 choices[0].message.content에 온다. 응답 읽는 법은 「처음 시작하기」의 ⑤와 같다.
맨 앞에 role이 system인 메시지를 두면 모델이 지킬 역할, 말투, 대답 형식을 정할 수 있다. 시스템 메시지는 대화 내내 같은 것을 맨 앞에 둔다.
{"model":"gemma3-4b","temperature":0,
"messages":[
{"role":"system","content":"너는 사내 업무를 돕는 비서다. 한국어로 한 문장만 답한다."},
{"role":"user","content":"내 이름은 김민수야."},
{"role":"assistant","content":"반갑습니다, 김민수 님."},
{"role":"user","content":"내 이름이 뭐라고 했지?"}]}
"message": {"content": "김민수 님입니다.", "role": "assistant", …}
"finish_reason": "stop"
서버는 앞선 대화를 기억하지 않는다. 대화를 이어 가려면 지금까지 주고받은 메시지를 모두 messages에 차례로 넣어 매번 보낸다. 위 예에서 모델이 이름을 안 것은 앞선 user·assistant 메시지가 함께 갔기 때문이다.
# Python: 이력을 쌓아 가며 묻는다
messages = [{"role": "system", "content": "너는 사내 업무를 돕는 비서다."}]
def ask(text):
messages.append({"role": "user", "content": text})
r = client.chat.completions.create(model=MODEL, messages=messages)
answer = r.choices[0].message.content
messages.append({"role": "assistant", "content": answer}) # 대답도 이력에 넣는다
return answer
이력이 길어지면 입력 토큰이 늘어 느려진다. 이력 전체가 모델의 ctx보다 길어지지 않게, 오래된 메시지는 빼거나 요약해서 넣는다. 생각 모델의 reasoning_content는 이력에 넣지 않고 content만 넣으면 된다.
| 인자 | 뜻 | 범위·예 |
|---|---|---|
max_tokens | 대답(출력)의 최대 토큰 수. 여기에 닿으면 잘리고 finish_reason이 length가 된다. 생각 모델은 생각 과정도 이 한도에 든다. 지정하지 않으면 모델이 스스로 멈출 때까지 쓴다. | 예: 1024 |
temperature | 대답의 무작위성. 0에 가까울수록 같은 질문에 비슷한 답이, 높을수록 다양한 답이 나온다. | 0~2. 분류·추출은 0, 글쓰기는 0.7 안팎 |
top_p | 확률이 높은 낱말부터 누적 확률이 이 값에 이를 때까지만 후보로 삼는다. 보통 temperature와 둘 중 하나만 조절한다. | 0~1. 예: 0.9 |
stop | 이 문자열이 나오면 거기서 멈춘다. 문자열 하나나 문자열 배열. | 예: ["\n\n"] |
stream | true면 만들어지는 대로 조각을 받는다(아래 「스트리밍」). | 기본 false |
tools, tool_choice | 도구 호출(「도구 호출」). |
인자를 함께 쓴 예다.
{"model":"gemma3-4b","max_tokens":200,"temperature":0.2,"top_p":0.9,
"messages":[
{"role":"system","content":"너는 사내 문서를 요약하는 도우미다. 답은 한국어로 짧게 쓴다."},
{"role":"user","content":"다음 문장을 한 줄로 요약해 줘: 회의실은 사내 포털에서 예약하며, 예약은 최대 2주 전부터 가능하고 취소는 회의 1시간 전까지 해야 한다."}]}
"content": "회의실은 사내 포털에서 최대 2주 전 예약 가능하며, 회의 1시간 전까지 취소해야 합니다." "finish_reason": "stop"
stop의 예다. 1부터 10까지 쉼표로 나열하게 하고 stop에 "5"를 주면 5가 나오기 직전에 멈춘다.
{"model":"gemma3-4b","temperature":0,"stop":["5"],
"messages":[{"role":"user","content":"1부터 10까지 숫자를 쉼표로 구분해 한 줄로 나열해. 다른 말은 하지 마."}]}
"content": "1, 2, 3, 4, " "finish_reason": "stop"
여기 없는 OpenAI 인자도 보낼 수 있지만, 받아들이는지와 어떻게 동작하는지는 모델마다 다르다. 쓰기 전에 한 번 시험해 본다.
stream을 true로 주면 대답이 다 만들어질 때까지 기다리지 않고, 만들어지는 대로 조각(chunk)을 받는다. 형식은 SSE(server-sent events)다. 조각 하나가 data: 로 시작하는 한 줄이고, 조각 사이에는 빈 줄이 있으며, 마지막 줄은 data: [DONE]이다.
curl -s -N $OPENAI_BASE_URL/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"gemma3-4b","stream":true,"temperature":0,"messages":[{"role":"user","content":"인사말을 다섯 글자 이내로 써 주세요."}]}'
data: {"id":"chatcmpl-10","created":1794188747,"model":"gemma3-4b","object":"chat.completion.chunk",…,"choices":[{"index":0,"delta":{"content":"네","role":"assistant"}}]}
data: {"id":"chatcmpl-10",…,"choices":[{"index":0,"delta":{"content":","}}]}
data: {"id":"chatcmpl-10",…,"choices":[{"index":0,"delta":{"content":" 좋습니다"}}]}
…
data: {"id":"chatcmpl-10",…,"choices":[{"finish_reason":"stop","index":0,"delta":{}}]}
data: [DONE]
-N은 curl이 받은 것을 모아 두지 않고 바로 화면에 찍게 한다. 조각마다 choices[0].delta.content를 이어 붙이면 전체 대답이 된다. 마지막 조각은 delta가 비어 있고 finish_reason이 들어 있다. 생각 모델은 delta.reasoning_content 조각이 먼저 오고 그다음에 content 조각이 온다(「생각(reasoning) 모델 다루기」).
스트리밍 응답에는 토큰 수(usage)가 기본으로 들어 있지 않다. 필요하면 "stream_options":{"include_usage":true}를 함께 보낸다. 그러면 [DONE] 바로 앞 조각에 usage가 붙어 온다.
data: {"id":"chatcmpl-838",…,"choices":[{"index":0,"delta":{}}],"usage":{"completion_tokens":11,"prompt_tokens":12,"total_tokens":23,…}}
data: [DONE]
Python(openai)에서 받는 법. 생각 과정은 표준 오류로, 대답은 표준 출력으로 보낸다.
import sys
from openai import OpenAI
client = OpenAI()
stream = client.chat.completions.create(
model="grade:C2",
messages=[{"role": "user", "content": "1 더하기 1은? 숫자만 답하세요."}],
stream=True,
)
for chunk in stream:
if not chunk.choices:
continue
delta = chunk.choices[0].delta
thinking = getattr(delta, "reasoning_content", None) # 생각 모델만
if thinking:
print(thinking, end="", file=sys.stderr, flush=True)
if delta.content:
print(delta.content, end="", flush=True)
print()
Node(openai)에서 받는 법.
import OpenAI from "openai";
const client = new OpenAI();
const stream = await client.chat.completions.create({
model: "grade:C2",
messages: [{ role: "user", content: "1 더하기 1은? 숫자만 답하세요." }],
stream: true,
});
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta ?? {};
if (delta.reasoning_content) process.stderr.write(delta.reasoning_content); // 생각
if (delta.content) process.stdout.write(delta.content); // 답
}
process.stdout.write("\n");
라이브러리 없이 Node 18 이상의 fetch로 받는 법. SSE 줄을 직접 나눠 읽는다.
// stream.mjs (Node 18 이상)
const res = await fetch(`${process.env.OPENAI_BASE_URL}/chat/completions`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "grade:C2",
stream: true,
messages: [{ role: "user", content: "1 더하기 1은? 숫자만 답하세요." }],
}),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const decoder = new TextDecoder();
let buf = "";
for await (const part of res.body) {
buf += decoder.decode(part, { stream: true });
let i;
while ((i = buf.indexOf("\n")) >= 0) {
const line = buf.slice(0, i).trim();
buf = buf.slice(i + 1);
if (!line.startsWith("data: ")) continue; // 빈 줄은 건너뛴다
const data = line.slice(6);
if (data === "[DONE]") continue; // 끝 표시
const msg = JSON.parse(data);
if (msg.error) throw new Error(msg.error.message);
const delta = msg.choices?.[0]?.delta ?? {};
if (delta.reasoning_content) process.stderr.write(delta.reasoning_content); // 생각
if (delta.content) process.stdout.write(delta.content); // 답
}
}
process.stdout.write("\n");
$ node stream.mjs 2>/dev/null 2
2>/dev/null을 빼면 영어로 된 생각 과정이 먼저 흐르고 그 뒤에 대답 2가 나온다.
메시지 목록 대신 글 한 덩어리(prompt)를 주고 이어 쓰게 하는 옛 형식 POST /v1/completions도 받는다. 새로 짜는 코드는 대화 형식을 쓴다.
curl -s $OPENAI_BASE_URL/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"gemma3-4b","prompt":"대한민국의 수도는","max_tokens":10,"temperature":0}'
{"id":"cmpl-306","object":"text_completion","created":1794188034,"model":"gemma3-4b",
"choices":[{"finish_reason":"length","index":0,"text":"대한민국의 수도는 **서울특별시**입니다.","logprobs":null}],
"usage":{"completion_tokens":10,"prompt_tokens":15,"total_tokens":25,…},…}
어떤 모델(예: gpt-oss-120b, qwen3-8b)은 대답하기 전에 생각 과정을 먼저 만든다. 이 서비스는 생각 과정을 대답(content)과 나누어 reasoning_content에 담아 보낸다. 생각 과정은 질문이 한국어여도 영어로 나올 수 있다. 어떤 모델이 생각 모델인지는 한 번 불러서 reasoning_content가 오는지로 알 수 있다.
스트리밍이 아니면 choices[0].message.reasoning_content에 문자열로 오고, 대답은 따로 choices[0].message.content에 온다. 다음은 developer 키로 gpt-oss-120b를 부른 예다.
{"model":"gpt-oss-120b","max_tokens":300,"messages":[{"role":"user","content":"2 더하기 3은? 숫자만 답하세요."}]}
"message": {"content": "5",
"role": "assistant",
"reasoning_content": "The user asks in Korean: … Means \"What is 2 plus 3? Answer only the number.\" So answer: 5.",
…}
"finish_reason": "stop"
스트리밍이면 조각마다 choices[0].delta.reasoning_content로 생각 과정이 먼저 오고, 생각이 끝나면 choices[0].delta.content로 대답이 온다. 다음은 qwen3-8b의 예다(조각 사이의 빈 줄은 뺐다).
data: {"id":"chatcmpl-619","created":1794188019,"model":"qwen3-8b","object":"chat.completion.chunk",…,"choices":[{"index":0,"delta":{"reasoning_content":"Okay","content":"","role":"assistant"}}]}
data: {"id":"chatcmpl-619",…,"choices":[{"index":0,"delta":{"reasoning_content":",","content":""}}]}
data: {"id":"chatcmpl-619",…,"choices":[{"index":0,"delta":{"reasoning_content":" the","content":""}}]}
… (이 예에서는 생각 조각이 290개 이어졌다)
data: {"id":"chatcmpl-619",…,"choices":[{"index":0,"delta":{"reasoning_content":".\n","content":""}}]}
data: {"id":"chatcmpl-619",…,"choices":[{"index":0,"delta":{"content":"2"}}]}
data: {"id":"chatcmpl-619",…,"choices":[{"finish_reason":"stop","index":0,"delta":{}}]}
data: [DONE]
생각 조각에도 빈 content("")가 함께 올 수 있고, 모델에 따라서는 첫 조각이 role과 빈 content만 담고 오기도 한다. content가 비어 있지 않을 때만 대답으로 이어 붙이면 어느 경우든 맞게 읽힌다.
max_tokens는 생각 과정과 대답을 합한 출력 전체의 한도다. 한도가 작으면 생각만 하다가 끝나서 content가 빈 문자열로 온다. 이때 finish_reason은 length다.
{"model":"qwen3-8b","max_tokens":30,"messages":[{"role":"user","content":"대한민국의 수도는 어디인가요?"}]}
"message": {"content": "",
"role": "assistant",
"reasoning_content": "Okay, the user is asking where the capital of South Korea is. Let me think. I know that South Korea's capital is Seoul.",
…}
"finish_reason": "length"
"usage": {"completion_tokens": 30, "prompt_tokens": 22, …}
length면 max_tokens를 늘려 다시 보낸다.getattr(message, "reasoning_content", None)처럼 꺼낸다.임베딩은 글을 숫자 벡터로 바꾼다. 뜻이 비슷한 글은 벡터도 가깝다. 검색, 문서 찾기(RAG), 비슷한 글 묶기에 쓴다. POST /v1/embeddings에 E 등급 모델과 input을 보낸다.
curl -s $OPENAI_BASE_URL/embeddings \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"qwen3-embedding-0.6b","input":["회의실 예약 방법","휴가 신청 절차"]}'
{"model":"qwen3-embedding-0.6b",
"data":[{"embedding":[-0.06832887,-0.044867307,-0.011467212,…],"index":0,"object":"embedding"},
{"embedding":[-0.007947193,-0.051021032,-0.006071914,…],"index":1,"object":"embedding"}],
"object":"list",
"usage":{"completion_tokens":0,"prompt_tokens":14,"total_tokens":14,…}}
input은 문자열 하나 또는 문자열 배열이다. 문자열 하나를 보내면 data에 하나가 온다. 배열을 보내면 data에 같은 순서로 하나씩 오고, index가 input에서의 순서다. 벡터 하나의 길이는 그 모델의 dim과 같다(이 모델은 1024).
벡터는 같은 모델이 만든 것끼리만 비교할 수 있다. 모델이 다르면 차원이 같아도 값의 뜻이 다르다. 그래서 벡터를 저장해 두고 나중에 검색하는 코드는 모델 이름을 고정해서 부르고, 벡터와 함께 모델 이름을 저장한다. grade:E는 부를 때마다 그때 가장 좋은 E 모델을 고르므로, 등급이 바뀌거나 새 E 모델이 들어오면 저장된 벡터와 맞지 않는 벡터가 나온다. grade:E는 한 번 쓰고 버리는 시험에만 쓴다. 임베딩 모델을 바꿀 때는 저장된 글 전체를 새 모델로 다시 임베딩한다.
입력 하나의 길이는 그 모델의 ctx(토큰 수) 이하여야 한다. 넘는 입력은 오류 없이 앞부분 ctx 토큰만으로 벡터가 만들어지고, 뒷부분은 버려진다. 예를 들어 ctx가 32768인 모델에 그보다 긴 글 두 개를 끝 문장만 다르게 해서 보내면 200이 오고 두 벡터가 똑같다. 긴 문서는 미리 문단 같은 단위로 나눠 조각마다 임베딩한다. 몇 토큰으로 계산됐는지는 응답의 usage.prompt_tokens로 확인한다.
여러 글을 배열 하나에 묶어 보내면 호출 수가 줄어든다. 다만 한 요청의 본문은 32MB, 처리 시간은 600초를 넘을 수 없으므로 글이 아주 많으면 적당한 묶음(예: 수십~수백 개)으로 나눠 차례로 보낸다.
grade:를 쓸 때는 E만 된다. grade:C를 주면 400이다(「모델 고르기」).[[9906, 1917]])은 받지 않는다. 400이 온다. 글(문자열)로 보낸다. LangChain을 쓸 때 이 때문에 설정 하나가 필요하다(「클라이언트별 설정」의 LangChain).encoding_format은 float(기본, 숫자 배열)와 base64 둘 다 된다. 그래서 openai 라이브러리처럼 base64로 받아 풀어 쓰는 클라이언트도 그대로 쓸 수 있다.V 등급 모델(예: qwen2.5-vl-8b)은 글과 함께 이미지를 받아 그 내용을 보고 답한다. OpenAI 형식대로 메시지의 content를 배열로 쓰고, 글은 text 항목에, 이미지는 image_url 항목에 data URL(base64)로 넣는다.
{"model":"qwen2.5-vl-8b","max_tokens":50,
"messages":[{"role":"user","content":[
{"type":"text","text":"이 이미지는 무슨 색인가요? 한 단어로 답하세요."},
{"type":"image_url","image_url":{"url":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA…"}}]}]}
"message": {"content": "빨간색", "role": "assistant", …}
"finish_reason": "stop"
"usage": {"completion_tokens": 4, "prompt_tokens": 1062, "total_tokens": 1066, …}
위 예의 이미지는 64×64 크기의 빨간 사각형 PNG다. 이미지도 입력 토큰으로 계산되며, 이 작은 그림 한 장에 입력 토큰이 1,000개 넘게 잡혔다. 모델 이름 대신 grade:V를 쓰면 이 키로 쓸 수 있는 V 등급 모델이 골라지고 응답 머리글에 고른 모델이 붙는다.
data URL은 data:, 그림의 형식(MIME 형식), ;base64,, 그리고 파일 내용을 base64로 바꾼 문자열을 차례로 붙인 것이다. PNG는 data:image/png;base64,…, JPEG는 data:image/jpeg;base64,…처럼 쓴다.
# bash (Linux): 파일을 base64 한 줄로. macOS는 base64 -i photo.png
base64 -w0 photo.png
# Python
import base64
url = "data:image/png;base64," + base64.b64encode(open("photo.png", "rb").read()).decode()
// Node
import { readFileSync } from "node:fs";
const url = "data:image/png;base64," + readFileSync("photo.png").toString("base64");
# PowerShell
$url = "data:image/png;base64," + [Convert]::ToBase64String([IO.File]::ReadAllBytes("$PWD\photo.png"))
Python으로 그림 한 장을 보내는 전체 예다.
import base64
from openai import OpenAI
client = OpenAI()
b64 = base64.b64encode(open("photo.png", "rb").read()).decode()
r = client.chat.completions.create(
model="grade:V",
messages=[{"role": "user", "content": [
{"type": "text", "text": "이 이미지를 한 문장으로 설명해 주세요."},
{"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64}"}},
]}],
)
print(r.choices[0].message.content)
https://…)를 그대로 넣으면 400이 온다. message에 "image URLs are not currently supported, please use base64 encoded data instead"가 들어 있다. 파일을 받아 data URL로 바꿔 넣는다.도구 호출(function calling)은 모델이 바로 답하는 대신 "이 함수를 이 인자로 불러 달라"고 요청하게 하는 방법이다. 함수는 모델이 아니라 내 프로그램이 실행하고, 그 결과를 다시 모델에 주면 모델이 결과를 바탕으로 대답을 만든다. /v1/models에서 tools가 true인 모델만 된다(예: qwen3-8b). 등급으로 고를 때는 tools=true 조건을 쓴다(grade:C?tools=true).
curl -s $OPENAI_BASE_URL/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "qwen3-8b",
"messages": [{"role": "user", "content": "서울 날씨 어때?"}],
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"description": "도시의 현재 날씨를 알려 준다",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string", "description": "도시 이름"}},
"required": ["city"]
}
}
}]
}'
"finish_reason": "tool_calls",
"message": {
"content": "",
"role": "assistant",
"tool_calls": [{"index": 0,
"function": {"arguments": "{\"city\":\"Seoul\"}", "name": "get_weather"},
"id": "call_3nyw21ur",
"type": "function"}],
"reasoning_content": "Okay, the user is asking about the weather in Seoul. … Let me structure the tool call accordingly.\n",
…
}
finish_reason이 tool_calls이고, tool_calls에 부를 함수의 이름과 인자가 들어 있다. arguments는 JSON 문자열이므로 파싱해서 쓴다. 모델은 인자를 바꿔 적기도 한다. 이 예에서는 "서울"을 "Seoul"로 적었다. 내 함수가 그 값을 받아들일 수 있는지 확인하고, 처리할 수 없으면 그 사실을 결과로 돌려준다.
내 프로그램이 함수를 실행한 뒤, 앞의 메시지들에 모델이 보낸 assistant 메시지(tool_calls 포함)와 role이 tool인 결과 메시지를 붙여 다시 보낸다. tool 메시지의 tool_call_id는 tool_calls의 id와 같아야 한다. tools 정의도 다시 보낸다.
{
"model": "qwen3-8b",
"tools": [ …1과 같다… ],
"messages": [
{"role": "user", "content": "서울 날씨 어때?"},
{"role": "assistant", "content": "",
"tool_calls": [{"index": 0, "id": "call_3nyw21ur", "type": "function",
"function": {"name": "get_weather", "arguments": "{\"city\":\"Seoul\"}"}}]},
{"role": "tool", "tool_call_id": "call_3nyw21ur",
"content": "{\"city\":\"서울\",\"temp_c\":18,\"sky\":\"맑음\"}"}
]
}
"finish_reason": "stop",
"message": {"content": "서울의 현재 날씨는 18°C로 맑습니다.", "role": "assistant", …}
모델이 다시 tool_calls를 보내면 같은 과정을 되풀이한다. finish_reason이 stop이 되면 끝이다. Python으로 쓰면 다음과 같다.
import json
from openai import OpenAI
client = OpenAI()
tools = [{"type": "function", "function": {
"name": "get_weather",
"description": "도시의 현재 날씨를 알려 준다",
"parameters": {"type": "object",
"properties": {"city": {"type": "string", "description": "도시 이름"}},
"required": ["city"]}}}]
def get_weather(city):
return {"city": city, "temp_c": 18, "sky": "맑음"} # 실제로는 날씨 정보를 가져온다
messages = [{"role": "user", "content": "서울 날씨 어때?"}]
for _ in range(5): # 도구 호출이 끝없이 이어지지 않게 횟수를 제한한다
r = client.chat.completions.create(model="qwen3-8b", messages=messages, tools=tools)
msg = r.choices[0].message
if not msg.tool_calls:
print(msg.content)
break
messages.append({"role": "assistant", "content": msg.content or "",
"tool_calls": [tc.model_dump() for tc in msg.tool_calls]})
for tc in msg.tool_calls:
args = json.loads(tc.function.arguments)
result = get_weather(**args)
messages.append({"role": "tool", "tool_call_id": tc.id,
"content": json.dumps(result, ensure_ascii=False)})
false인 모델에 tools를 넣으면 400이 온다. message에 그 모델이 도구를 지원하지 않는다는 내용이 있다.tool_choice도 함께 보낼 수 있지만 모델에 따라 다르게 동작할 수 있으니 시험해 보고 쓴다.어느 클라이언트든 넣을 것은 셋이다. Base URL https://llm.kephas.kr/v1, 키, 그리고 모델 이름(또는 grade:). 아래 예는 「환경변수 설정」대로 두 환경변수가 있다고 본다. 예에 나오는 모델 이름(qwen3-8b 등)은 예일 뿐이고, 실제로는 /v1/models에 있는 이름을 쓴다.
# 모델 목록
curl -s $OPENAI_BASE_URL/models -H "Authorization: Bearer $OPENAI_API_KEY"
# 등급과 조건으로 모델 하나 고르기
curl -s "$OPENAI_BASE_URL/pick?grade=C&tools=true" -H "Authorization: Bearer $OPENAI_API_KEY"
# 대화 (응답 머리글까지 보려면 -i를 붙인다)
curl -s $OPENAI_BASE_URL/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" -H "Content-Type: application/json" \
-d '{"model":"grade:C2","messages":[{"role":"user","content":"안녕하세요"}]}'
# 대화, 스트리밍
curl -s -N $OPENAI_BASE_URL/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" -H "Content-Type: application/json" \
-d '{"model":"grade:C2","stream":true,"messages":[{"role":"user","content":"안녕하세요"}]}'
# 임베딩
curl -s $OPENAI_BASE_URL/embeddings \
-H "Authorization: Bearer $OPENAI_API_KEY" -H "Content-Type: application/json" \
-d '{"model":"grade:E","input":["첫 문장","둘째 문장"]}'
Windows의 Git Bash에서 한글이 든 본문을 보낼 때는 본문을 파일로 저장하고 --data-binary @파일로 보낸다. 파일의 내용이 바이트 그대로 가므로 한글이 깨지지 않는다.
cat > body.json <<'EOF'
{"model":"grade:C2","messages":[{"role":"user","content":"대한민국의 수도는 어디인가요?"}]}
EOF
curl -s $OPENAI_BASE_URL/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" -H "Content-Type: application/json" \
--data-binary @body.json
Windows PowerShell 5.1에서 curl은 curl이 아니라 Invoke-WebRequest의 다른 이름이다. curl을 쓰려면 curl.exe라고 적는다. 또 PowerShell 5.1은 외부 프로그램에 넘기는 인자에서 큰따옴표를 지워 버려서, 작은따옴표로 감싼 JSON을 그대로 넘기면 400(Invalid JSON payload)이 온다. PowerShell에서는 아래 Windows PowerShell 예를 쓴다.
pip install openai
from openai import OpenAI
client = OpenAI() # OPENAI_BASE_URL·OPENAI_API_KEY를 읽는다
# 환경변수 없이 쓰려면: OpenAI(base_url="https://llm.kephas.kr/v1", api_key=키)
# 모델 목록과 모델 하나
for m in client.models.list():
print(m.id, getattr(m, "grade", None), getattr(m, "ctx", None))
m = client.models.retrieve("qwen3-8b")
print(m.model_dump()) # grade·ctx·dim·langs·tools·note까지 모두
# 대화
r = client.chat.completions.create(
model="grade:C2",
messages=[
{"role": "system", "content": "한국어로 짧게 답한다."},
{"role": "user", "content": "대한민국의 수도는 어디인가요?"},
],
max_tokens=1024,
)
print(r.model) # 실제로 답한 모델
print(r.choices[0].message.content)
print(getattr(r.choices[0].message, "reasoning_content", None)) # 생각 모델만
# 임베딩
e = client.embeddings.create(model="qwen3-embedding-0.6b", input=["회의실 예약 방법", "휴가 신청 절차"])
vectors = [d.embedding for d in e.data] # 각각 dim개(이 모델은 1024개)의 숫자 목록
print(len(vectors), len(vectors[0]))
스트리밍은 위 「스트리밍」의 Python 예를 본다. 고른 모델을 응답 머리글에서 보려면 raw = client.chat.completions.with_raw_response.create(...)로 부르고 raw.headers.get("x-kephas-picked-model")을 읽은 뒤 raw.parse()로 응답을 꺼낸다.
오류는 예외로 온다. openai.APIStatusError의 status_code에 상태 코드가, message에 서버가 보낸 설명이 있다. 라이브러리는 연결 오류, 429, 5xx를 기본으로 두 번 더 시도하며 횟수는 OpenAI(max_retries=…)로 바꾼다.
npm install openai
아래는 ES 모듈 형식이다. 파일 이름을 .mjs로 하거나 package.json에 "type": "module"을 넣는다. Node 18 이상을 쓴다.
import OpenAI from "openai";
const client = new OpenAI(); // OPENAI_BASE_URL·OPENAI_API_KEY를 읽는다
// 환경변수 없이: new OpenAI({ baseURL: "https://llm.kephas.kr/v1", apiKey: 키 })
// 모델 목록
for await (const m of client.models.list()) console.log(m.id, m.grade, m.ctx);
// 대화
const r = await client.chat.completions.create({
model: "grade:C2",
messages: [{ role: "user", content: "대한민국의 수도는 어디인가요?" }],
});
console.log(r.model, r.choices[0].message.content);
console.log(r.choices[0].message.reasoning_content); // 생각 모델만
// 고른 모델을 머리글에서 보기
const { data, response } = await client.chat.completions
.create({ model: "grade:C2", messages: [{ role: "user", content: "안녕하세요" }] })
.withResponse();
console.log(response.headers.get("x-kephas-picked-model"), data.choices[0].message.content);
// 임베딩
const e = await client.embeddings.create({
model: "qwen3-embedding-0.6b",
input: ["회의실 예약 방법", "휴가 신청 절차"],
});
console.log(e.data.length, e.data[0].embedding.length);
스트리밍은 위 「스트리밍」의 Node 예를 본다. 오류는 OpenAI.APIError 예외로 오고 status에 상태 코드가 있다. 이 라이브러리도 연결 오류, 429, 5xx를 기본으로 두 번 더 시도한다(maxRetries로 바꾼다).
패키지를 설치할 수 없는 곳에서는 Node 18 이상에 들어 있는 fetch로 부른다. 「모델 고르기」의 「서비스 코드에서」와 「대화」의 「스트리밍」에 그 예가 있다.
Windows에 기본으로 들어 있는 Windows PowerShell 5.1은 한글을 다룰 때 두 가지를 조심해야 한다. 문자열 본문을 그대로 보내면 한글이 ?로 바뀌어 가고, 대화 응답은 머리글에 문자 집합(charset)이 없어 한글이 깨진 채 읽힌다. 아래 함수는 본문을 UTF-8 바이트로 보내고 응답을 UTF-8로 풀어서 이 두 문제를 피한다.
# 모델 목록
$h = @{ Authorization = "Bearer $env:OPENAI_API_KEY" }
(Invoke-RestMethod -Uri "$env:OPENAI_BASE_URL/models" -Headers $h).data | Format-Table id, grade, ctx, note
# 대화·임베딩: 한글이 깨지지 않게 UTF-8로 보내고 UTF-8로 읽는다
function Invoke-Llm([string]$Path, $Body) {
$json = $Body | ConvertTo-Json -Depth 10
$res = Invoke-WebRequest -Method Post -Uri "$env:OPENAI_BASE_URL$Path" -UseBasicParsing `
-Headers @{ Authorization = "Bearer $env:OPENAI_API_KEY" } `
-ContentType "application/json; charset=utf-8" `
-Body ([Text.Encoding]::UTF8.GetBytes($json))
[Text.Encoding]::UTF8.GetString($res.RawContentStream.ToArray()) | ConvertFrom-Json
}
$r = Invoke-Llm "/chat/completions" @{
model = "grade:C2"
messages = @(@{ role = "user"; content = "대한민국의 수도는 어디인가요? 한 문장으로 답하세요." })
}
$r.model
$r.choices[0].message.content
$e = Invoke-Llm "/embeddings" @{ model = "grade:E"; input = @("회의실 예약 방법", "휴가 신청 절차") }
$e.data[0].embedding.Count
id grade ctx note -- ----- --- ---- qwen3-8b C2 40960 기본값(Q4_K_M) … qwen3-8b 대한민국의 수도는 서울입니다. 1024
4xx·5xx가 오면 Invoke-WebRequest가 예외를 던진다. 상태 코드와 서버가 보낸 본문은 이렇게 꺼낸다.
try {
Invoke-Llm "/chat/completions" @{ model = "gpt-oss-120b"; messages = @(@{ role = "user"; content = "hi" }) }
} catch {
$_.Exception.Response.StatusCode.value__ # 403
$_.ErrorDetails.Message # {"error":{"message":"The requested model 'gpt-oss-120b' is not available for this API key, …"}}
}
Invoke-WebRequest는 응답이 다 올 때까지 기다리므로 스트리밍을 조각조각 받을 수 없다. 스트리밍이 필요하면 Python이나 Node를 쓴다.
pip install langchain-openai
import os
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
base = "https://llm.kephas.kr/v1"
key = os.environ["OPENAI_API_KEY"]
chat = ChatOpenAI(model="grade:C2", base_url=base, api_key=key, max_tokens=1024)
print(chat.invoke("대한민국의 수도는 어디인가요?").content)
for chunk in chat.stream("1부터 5까지 세어 줘"):
print(chunk.content, end="", flush=True)
emb = OpenAIEmbeddings(model="qwen3-embedding-0.6b", base_url=base, api_key=key,
check_embedding_ctx_length=False)
vectors = emb.embed_documents(["회의실 예약 방법", "휴가 신청 절차"])
query_vector = emb.embed_query("회의실은 어떻게 빌리나요?")
OpenAIEmbeddings에는 check_embedding_ctx_length=False가 필요하다. 기본값(True)이면 LangChain이 글을 OpenAI의 토크나이저로 잘라 토큰 번호 배열로 바꿔 보내는데, 이 서비스의 임베딩 모델은 토큰 번호 배열 입력을 받지 않고 400을 돌려준다. False로 두면 글을 문자열 그대로 보낸다. 그 대신 LangChain이 긴 글을 잘라 주지 않으므로, ctx를 넘는 글은 오류 없이 뒷부분이 버려진다(「임베딩」). 문서는 LangChain의 텍스트 분할기 등으로 미리 나눠서 넣는다.
채팅 프로그램, 편집기 확장, 자동화 도구처럼 "OpenAI 호환" 연결을 받는 프로그램은 설정 화면에 다음을 넣는다.
| 설정 항목(도구마다 이름이 다르다) | 넣을 값 |
|---|---|
| 제공자 종류(Provider) | OpenAI 또는 OpenAI 호환(OpenAI-compatible) |
| API Base URL, Base URL, Endpoint, API 주소 | https://llm.kephas.kr/v1 |
| API Key, 키 | 받은 키(sk-…) |
| Model, 모델 이름 | /v1/models의 id(예: qwen3-8b) 또는 grade:C2 |
주소에는 끝의 /v1까지 넣는다. 저장한 뒤 도구에 모델 목록이 보이면 주소와 키가 맞게 들어간 것이다. 목록이 보이지 않으면 /v1이 빠졌거나 두 번 들어가지 않았는지 본다.
모델 이름을 직접 적는 칸이 있으면 grade:C2도 된다. 서버가 받은 이름을 보고 모델을 고르기 때문이다. 목록에서 고르기만 하는 도구는 목록에 grade: 항목이 없으므로 실제 이름을 고른다. 오래 두고 쓰는 설정이라면 실제 이름을 넣는 편이 결과가 일정하다.
이 서비스는 이미지 주소(https://…)와 토큰 번호 배열 입력을 받지 않는다. 도구가 그런 형식으로 보내면 400이 오므로, 도구에서 이미지를 data URL(base64)로, 임베딩 입력을 글 그대로 보내도록 설정한다.
오류는 HTTP 상태 코드와 함께 다음 형식의 본문으로 온다.
{"error":{"message":"사람이 읽을 설명","type":"오류의 종류","code":401}}
code는 이 서비스의 입구에서 만든 오류면 숫자(401), 모델 쪽을 거쳐 온 오류면 문자열("401")일 수 있고, param 같은 필드가 더 붙기도 한다. message도 한국어일 때와 영어일 때가 있다. 판단은 HTTP 상태 코드로 하고, message는 사람이 원인을 찾는 데 쓴다. 관리자에게 물을 때는 상태 코드와 message를 함께 알린다(키는 지운다).
| 코드 | 무엇이 잘못됐나 | 할 일 |
|---|---|---|
| 400 | 요청이 잘못됐다. JSON이 깨졌거나(Invalid JSON payload), 필수 인자가 빠졌거나(Missing required parameter: 'messages'), pick 인자가 틀렸거나(max_quality는 1·2·3 중 하나), 등급 종류가 엔드포인트와 맞지 않거나(임베딩에는 E 등급만 쓸 수 있다), 모델이 받지 않는 입력이다(도구를 지원하지 않는 모델에 tools, 이미지 주소, 토큰 번호 배열). | message를 읽고 요청을 고친다. 고치지 않고 다시 보내면 같은 결과가 온다. |
| 401 | 키가 없거나 틀렸거나, 정지·만료됐다. 정지면 Key is blocked, 만료면 Expired Key가 message에 있다. | 머리글이 Authorization: Bearer <키> 형식인지, 키 앞뒤에 빈칸이나 줄바꿈이 붙지 않았는지 본다. 정지·만료면 사내망에서 키 발급·관리 페이지를 열어 재개하거나 연장한다. |
| 403 | 이 키로 쓸 수 없는 모델이거나, 없는 모델 이름이다(type key_model_access_denied). pick과 grade:에서는 조건에 맞는 모델은 있지만 이 키의 범위 밖이라는 뜻이다(type model_pick_failed). | /v1/models에서 이름을 확인한다. 오타가 아니고 꼭 필요한 모델이면 그 모델을 쓸 수 있는 범위로 새 키를 발급한다. |
| 404 | pick이나 grade:의 조건에 맞는 모델이 없다. /v1/models/{id}에서 모델이 없거나 범위 밖이다. 또는 주소가 틀렸다. | 조건을 줄인다. 주소에 /v1이 제대로 들어갔는지 본다. |
| 422 | 키에 정해 둔 비용 한도를 넘었다(Budget has been exceeded). 이 상태의 키로 /v1/models나 /v1/pick을 부르면 502가 온다. | 사내망에서 연 키 발급·관리 페이지의 키 상세에서 사용액과 한도를 확인한다. 그 페이지에서는 발급한 키의 한도를 바꿀 수 없으므로, 한도를 비우거나 넉넉히 정한 새 키를 발급해 바꿔 쓴다. |
| 429 | 요청 속도 한도를 둔 키가 한도를 넘었다(Rate limit exceeded). message에 한도가 풀리는 시각이 있다. 한도는 그렇게 정해 둔 키에만 있다. | 그 시각이 지난 뒤 다시 보낸다. 한꺼번에 많이 보내는 프로그램은 동시에 보내는 수를 줄인다. |
| 501 | 임베딩에 임베딩 모델이 아닌 모델을 넣었다. | dim이 있는 모델(E 등급)을 쓴다. |
| 502 | 모델 서버 쪽에서 오류가 났다. 요청 본문이 32MB를 넘어 입구가 요청을 끊었을 때도 본문 없는 502가 온다. | 잠시 뒤 다시 보낸다. 본문이 큰 요청이면 크기를 줄인다. 계속되면 관리자에게 알린다. |
| 503 | 모델 서버가 응답하지 않는다. 점검이나 엔진 전환 중에는 일부 모델이 1~2분 동안 503을 돌려준다. | 잠시 뒤 다시 보낸다. 다른 모델로 몰래 바꿔 주지 않으므로, 급하면 직접 다른 모델을 골라 부른다. |
# 키를 넣지 않았다 (401)
{"error":{"message":"Authorization: Bearer <키>가 필요하다","type":"authentication_error","code":401}}
# 키가 틀렸다 (401)
{"error":{"message":"키가 유효하지 않다","type":"authentication_error","code":401}}
# 정지된 키 (401)
{"error":{"message":"키가 유효하지 않다: Authentication Error, Key is blocked. …","type":"authentication_error","code":401}}
# 만료된 키 (401)
{"error":{"message":"키가 유효하지 않다: Authentication Error - Expired Key. Key Expiry time 2026-11-09 01:37:31… and current time …","type":"authentication_error","code":401}}
# member 키로 품질 1 모델을 이름으로 불렀다 (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"}}
# 대화에 E 등급을 주었다 (400)
{"error":{"message":"대화·완성에는 C/K/V 등급만 쓸 수 있다(요청: grade:E)","type":"invalid_request_error","code":400}}
# JSON이 깨졌다 (400)
{"error":{"message":"Invalid JSON payload: unexpected end of data: line 1 column 33 (char 32)","type":"invalid_request_error","param":"request_body","code":"400"}}
# messages를 빠뜨렸다 (400)
{"error":{"message":"/chat/completions: Missing required parameter: 'messages'.","type":"invalid_request_error","param":"messages","code":"400"}}
# 속도 한도를 넘었다 (429)
{"error":{"message":"Rate limit exceeded for api_key: …. Limit type: requests. Current limit: 1, Remaining: 0. Limit resets at: 2026-11-09 01:38:34 UTC","type":"throttling_error","param":null,"code":"429"}}
원인이 같아도 어느 엔드포인트로 불렀는지에 따라 message의 문구와 type이 다를 수 있다. 예를 들어 정지된 키로 대화를 부르면 type이 auth_error인 영어 문구가 온다. 상태 코드는 같다.
429, 502, 503과 연결 끊김은 잠시 뒤 다시 보내면 될 수 있다. 바로 되풀이하지 말고 간격을 늘려 가며(예: 2초, 4초, 8초) 몇 번만 다시 보낸다. 400, 401, 403, 404, 422, 501은 다시 보내도 결과가 같으니 원인을 고친다. openai 라이브러리(Python·Node)는 연결 오류, 429, 5xx를 기본으로 두 번 더 시도한다.
max_quality=2).stream: true로 받아 만들어지는 대로 보여 준다. 전체 시간은 같아도 첫 글자를 빨리 볼 수 있다.503이 오면 느린 것이 아니라 그 모델 서버가 잠시 응답하지 않는 것이다. 「오류와 대처」를 본다.
키별로 요청 수, 입력·출력 토큰 수, 모델, 시각이 기록된다. 이 사용량(모델별 요청 수·입력 토큰·출력 토큰, 최근 호출 30건)은 사내망에서 연 키 발급·관리 페이지의 키 상세에 나온다. 그 페이지에는 로그인이 없으므로 사내망의 누구나 모든 키의 사용량을 볼 수 있다. 프롬프트와 응답 본문은 저장하지 않는다.
샘플에 넣은 키는 그 브라우저(localStorage)에만 저장되고, 요청할 때 머리글에 실려 이 서비스로만 간다. 공용 컴퓨터에서 샘플을 썼다면 끝난 뒤 브라우저에서 이 사이트의 데이터를 지운다.
쓸 수 있다. 사내망과 외부에서 같은 주소 https://llm.kephas.kr/v1을 쓰고, 키만 있으면 된다. 키를 발급하고 관리하는 일만 사내망에서 한다(「키」). 다만 사내 구성원을 위한 서비스이고 공개 서비스가 아니다.
안 된다. 사용 기록이 키 단위로 남아 누가 썼는지 나눌 수 없고, 키를 정지하면 쓰던 사람 모두가 멈춘다. 사람마다, 프로그램마다 따로 발급한다. 여럿이 쓰는 프로그램은 그 프로그램의 서비스용 키를 발급한다.
부르는 방법에 따라 다르다.
grade:로 부르면: 고칠 것이 없지만 다음 호출부터 다른 모델이 답할 수 있다.주소와 키는 모델이 어느 서버로 옮겨 가도 그대로다.
모르겠으면 대화는 grade:C2로 시작한다. 분류·추출처럼 단순하고 많은 일은 C3, 결과의 질이 가장 중요하면 C1(developer 키 필요), 검색·RAG는 E, 그림을 읽으려면 V를 쓴다. 써 보고 정해지면 프로그램에는 그 모델 이름을 넣는다.
/v1/models에 있는 이름이나 grade:를 쓴다.GET /v1/pick), model의 grade:는 이 서비스에만 있다.아직 없다. openai 라이브러리(Python·Node)나 LangChain을 그대로 쓴다. pick처럼 OpenAI에 없는 기능은 HTTP로 직접 부른다(「모델 고르기」의 「서비스 코드에서」).
/v1/models를 다시 불러 본다. 목록은 언제나 지금 상태를 보여 주고, 모델마다 note에 짧은 설명이 붙는다. 서비스 코드라면 시작할 때 pick 결과나 쓰는 모델의 grade를 로그에 남겨 두면, 바뀐 때를 로그로 비교할 수 있다. 실험(X) 모델은 예고 없이 바뀌거나 사라질 수 있다.
누구에게 요청하지 않아도 된다. 키는 쓸 사람이 사내망에서 직접 발급한다(「키」의 「받는 법」).
OPENAI_API_KEY에, 주소 https://llm.kephas.kr/v1을 OPENAI_BASE_URL에 넣는다(「환경변수 설정」).발급한 키는 사내든 외부든 어디서나 쓴다. 외부에서는 발급할 수 없으므로, 밖에서 쓸 키도 사내에 있을 때 발급해 둔다.
여기서 다루지 않은 인자와 응답 필드의 정확한 명세는 API 레퍼런스에 있다. 서비스 전체의 소개는 홈에 있다.