오후 한나절이면 스트리밍 응답을 지원하는 AI 챗봇을 만들 수 있습니다. 하지만 첫 번째 429 에러 이후에도 서비스를 유지하는 것이 진짜 실력입니다. 여기서는 TokenLab 키 하나를 사용하여 SSE 스트리밍, 서버 측 대화 메모리, 모델 허용 목록(allowlist)을 갖춘 소규모 FastAPI 서비스를 구축합니다. 이 가이드의 목표는 실제 제품 인터페이스 뒤에 배치할 수 있고, 통합 코드를 다시 작성하지 않고도 모델을 교체할 수 있는 백엔드를 만드는 것입니다. 아직 베이스 URL 교체를 완료하지 않았다면 먼저 마이그레이션 가이드를 읽어보세요. 부하 발생 시의 백오프(backoff)가 주된 관심사라면 이 가이드와 함께 AI API 속도 제한 가이드를 참고하시기 바랍니다.
핵심 요약
- 프로덕션 수준의 챗봇에는 동기식 엔드포인트, 스트리밍 엔드포인트, 서버 측 대화 상태, 모델 허용 목록, 실제 에러 핸들링, 스토리지 업그레이드 경로 등 6가지 요소가 필요합니다.
- 모든 프로세스에서
TOKENLAB_API_KEY를 로드하고 서버에 보관하세요. 인증 문서(2026-10-03 확인)에 따르면 클라이언트 측 코드에 키를 노출하거나 커밋해서는 안 됩니다. GET /v1/models는 공개되어 있으므로, 성공적인 응답이 곧 키가 작동한다는 것을 증명하지는 않습니다. 실제 채팅 요청(Quickstart, 2026-10-03 확인)으로 키를 검증하세요.- SSE는 대부분의 채팅 제품을 커버합니다. 출력이 도착한 후 끊긴 스트림은 완료된 답변이 아니라 불완전한 것으로 처리해야 합니다.
- 이 가이드의 허용 목록은 각 티어를
deepseek-v4-flash,gpt-5.5,claude-sonnet-5,gemini-3.5-flash에 매핑합니다. 각 모델은openai_chat_completions를 허용된 형식으로 나열합니다(2026-10-03 확인). - 아래 가격을 기준으로 추산하면, 입력 2,000 토큰과 출력 500 토큰의 한 턴 비용은 모델에 따라 약 $0.0006에서 $0.0075 사이입니다.
AI 챗봇 구축: 설정 및 핵심 서비스
완성된 서비스는 6가지 구성 요소로 이루어집니다:
- 스모크 테스트를 위한 동기식
/chat엔드포인트. - 실제 UI를 위한 스트리밍
/chat/stream엔드포인트. conversation_id로 구분되는 대화 상태.- 프론트엔드가 임의의 ID를 요청하지 못하도록 하는 모델 허용 목록.
- 첫 번째 429 에러에서 멈추지 않는 에러 핸들링.
- 인메모리 프로토타입에서 Redis 또는 PostgreSQL로 전환하는 경로.
이 정도면 지원 봇, 내부 어시스턴트 또는 임베디드 채팅 위젯의 첫 번째 버전으로 충분합니다. 아래의 각 코드 블록은 표시된 순서대로 main.py 파일에 추가됩니다.
1단계: 키 설정 및 스택 설치
워크스페이스의 API Keys에서 키를 생성하세요. 전체 값은 한 번만 표시되며, 키 목록의 접두사로는 요청을 인증할 수 없습니다. 모든 TokenLab 키는 sk-로 시작합니다. 첫 번째 요청은 모델의 현재 가격으로 청구되므로 결제(Billing)에서 잔액을 확인하세요(Quickstart, 2026-10-03 확인).
키는 소스 코드가 아닌 환경 변수에 저장하세요. 아래 프롬프트는 명령줄이나 셸 기록에 키가 남지 않게 하며, 현재 터미널과 그 자식 프로세스에만 적용됩니다. 배포된 앱에는 비밀 관리자(secret manager)를 사용하세요. Quickstart에는 PowerShell 버전도 있습니다.
printf 'TokenLab API key: '
read -r -s TOKENLAB_API_KEY
printf '\n'
export TOKENLAB_API_KEY
pip install fastapi uvicorn openai pydantic
TokenLab 전용 SDK는 필요 없습니다. 키와 베이스 URL을 변경하면 공식 OpenAI SDK가 그대로 작동합니다(SDK 가이드, 2026-10-03 확인).
2단계: 스키마, 허용 목록 및 메모리 정의
공유 구성 요소부터 시작합니다. 요청 스키마는 원시 model 문자열 대신 tier를 사용하며, 이 가이드 뒷부분의 모든 curl 예제는 동일한 필드를 전송합니다.
첫 번째 챗봇 데모는 보통 브라우저에 대화 내용을 유지하고 매 턴마다 다시 전송합니다. 프로토타입에는 적합하지만, 재시도, 세션 재개, 서버 측 도구가 필요해지면 복잡해집니다. 따라서 처음부터 서버가 기록을 소유하도록 합니다.
import json
import logging
import os
import time
import uuid
from collections import defaultdict
from fastapi import FastAPI, HTTPException
from fastapi.responses import StreamingResponse
from openai import APIConnectionError, APIStatusError, OpenAI, RateLimitError
from pydantic import BaseModel, Field
log = logging.getLogger("chatbot")
app = FastAPI()
client = OpenAI(
api_key=os.environ["TOKENLAB_API_KEY"],
base_url="https://api.tokenlab.sh/v1",
timeout=30.0,
max_retries=0,
)
ALLOWED_MODELS = {
"default": "deepseek-v4-flash",
"flagship": "gpt-5.5",
"balanced": "claude-sonnet-5",
"budget": "gemini-3.5-flash",
}
SYSTEM_PROMPT = "You are a helpful assistant. Be concise and direct."
MAX_HISTORY = 20 # 대화당 유지할 메시지 수 (선택 사항)
MAX_REPLY_TOKENS = 1000 # 턴당 출력 제한 (선택 사항)
class ChatRequest(BaseModel):
message: str = Field(min_length=1, max_length=4000)
tier: str = "default"
conversation_id: str | None = None
def resolve_model(tier: str) -> str:
model = ALLOWED_MODELS.get(tier)
if model is None:
raise HTTPException(status_code=400, detail=f"unknown tier: {tier}")
return model
conversations: dict[str, list[dict]] = defaultdict(list)
def build_messages(conv_id: str, user_msg: str) -> list[dict]:
history = conversations[conv_id][-MAX_HISTORY:]
return [
{"role": "system", "content": SYSTEM_PROMPT},
*history,
{"role": "user", "content": user_msg},
]
def remember(conv_id: str, user_msg: str, reply: str) -> None:
conversations[conv_id].append({"role": "user", "content": user_msg})
conversations[conv_id].append({"role": "assistant", "content": reply})
클라이언트는 Quickstart에 맞춰 max_retries=0과 30초 타임아웃을 설정합니다. 스트리밍 턴에서 SDK가 무작위로 재시도하면 비용이 중복 청구될 수 있으므로, 재시도 결정은 직접 내립니다.
3단계: 에러 매핑 및 동기식 엔드포인트 추가
SDK 예외를 상태 코드와 사용자에게 안전한 메시지로 변환하는 작은 함수를 유지합니다. 상세 내용은 사용자에게 노출하지 않고 로그에만 기록합니다.
def describe_error(exc: Exception) -> tuple[int, str]:
if isinstance(exc, RateLimitError): # 먼저 확인: APIStatusError를 상속함
return 429, "The assistant is busy. Please retry shortly."
if isinstance(exc, APIConnectionError):
return 502, "Connection problem. Please retry."
if isinstance(exc, APIStatusError):
log.error("upstream status=%s code=%s", exc.status_code, exc.code)
if exc.status_code in (401, 402, 403): # 키, 잔액 또는 권한 문제: 운영자 문제
return 503, "The assistant is unavailable right now."
if exc.status_code in (400, 404): # 잘못된 필드 또는 모델 ID: 우리 측 버그
return 500, "Something went wrong on our side."
return 502, "The assistant is unavailable right now."
return 500, "Something went wrong."
def retry_delay(exc: RateLimitError) -> float:
try:
return min(float(exc.response.headers.get("retry-after", "2")), 10.0)
except ValueError:
return 2.0
def create_with_one_retry(**kwargs):
try:
return client.chat.completions.create(**kwargs)
except RateLimitError as exc:
time.sleep(retry_delay(exc))
return client.chat.completions.create(**kwargs)
@app.post("/chat")
def chat(req: ChatRequest):
model = resolve_model(req.tier)
conv_id = req.conversation_id or str(uuid.uuid4())
messages = build_messages(conv_id, req.message)
try:
response = create_with_one_retry(
model=model, messages=messages, max_tokens=MAX_REPLY_TOKENS
)
except (APIConnectionError, APIStatusError) as exc:
status, message = describe_error(exc)
raise HTTPException(status_code=status, detail=message)
reply = response.choices[0].message.content or ""
log.info("model=%s usage=%s", model, response.usage)
remember(conv_id, req.message, reply)
return {"conversation_id": conv_id, "reply": reply}
다른 기능을 만들기 전에 요청을 한 번 실행해 보세요. 여기서 실패한다면 그 위에 계속 구축하지 마세요.
응답 스트리밍 및 AI 챗봇의 반응성 유지
대부분의 챗봇 제품이 느리게 느껴지는 이유는 모델 자체가 느려서가 아니라, 전체 응답이 도착할 때까지 UI가 비어 있기 때문입니다. SSE는 대부분의 채팅 제품을 커버하며 웹소켓보다 운영 부담이 적기 때문에 SSE를 추가합니다. 제품이 이미 일반 HTTP 기반의 브라우저 클라이언트를 실행 중이라면, SSE가 아키텍처를 더 단순하게 유지해 줍니다.
4단계: 스트리밍 엔드포인트 추가
스트리밍 문서(스트리밍 가이드, 2026-10-03 확인)에 따르면, Chat Completions 스트리밍은 /v1/chat/completions에서 SSE 청크를 기대하는 프레임워크에 유효합니다. 허용된 4개 모델 모두 이를 지원하므로 이 경로를 유지합니다. 각 이벤트를 JSON으로 인코딩하여 응답 내부의 줄 바꿈이 프레이밍을 깨뜨리지 않도록 합니다.
def sse(payload: dict) -> str:
return f"data: {json.dumps(payload)}\n\n"
@app.post("/chat/stream")
def chat_stream(req: ChatRequest):
model = resolve_model(req.tier)
conv_id = req.conversation_id or str(uuid.uuid4())
messages = build_messages(conv_id, req.message)
def generate():
parts: list[str] = []
finish_reason = None
try:
with client.chat.completions.create(
model=model,
messages=messages,
max_tokens=MAX_REPLY_TOKENS,
stream=True,
stream_options={"include_usage": True},
) as stream:
for chunk in stream:
if chunk.usage:
log.info("model=%s usage=%s", model, chunk.usage)
if not chunk.choices:
continue
choice = chunk.choices[0]
if choice.delta.content:
parts.append(choice.delta.content)
yield sse({"delta": choice.delta.content})
if choice.finish_reason:
finish_reason = choice.finish_reason
except (APIConnectionError, APIStatusError) as exc:
_, message = describe_error(exc)
yield sse({"error": message})
return
if finish_reason != "stop":
yield sse({"error": f"Reply incomplete ({finish_reason})."})
return
remember(conv_id, req.message, "".join(parts))
yield sse({"done": True, "conversation_id": conv_id})
return StreamingResponse(generate(), media_type="text/event-stream")
여기서 두 가지 세부 사항이 중요합니다. 첫째, include_usage: true는 스트림 청크에 토큰 사용량을 추가하며, 최종 사용량 청크에는 choices가 없으므로 루프가 이를 건너뜁니다. 둘째, finish_reason이 stop일 때만 턴을 저장합니다. length 완료는 토큰 제한으로 인해 답변이 잘렸음을 의미하며, 문서에서는 둘 다 정상적인 종료 조건으로 나열합니다.
에러가 생성기 내부에서 발생하므로 실패 시점에는 이미 HTTP 상태가 200입니다. 클라이언트는 error 이벤트를 읽어야 합니다. with 블록은 생성기가 종료될 때 업스트림 스트림을 닫습니다. 문서는 사용자가 중단하거나 연결을 끊을 때 업스트림을 취소할 것을 권장하므로, ASGI 서버가 연결 해제 시 실제로 생성기를 중지하는지 테스트하세요.
5단계: 브라우저에서 스트림 읽기
브라우저는 TokenLab을 직접 호출하지 말고 자체 인증된 백엔드를 호출해야 합니다. 하나의 네트워크 청크가 반드시 하나의 완전한 SSE 이벤트는 아니므로 이벤트 경계별로 파싱하세요.
async function sendMessage(payload, onDelta) {
const response = await fetch('/chat/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload), // { message, tier, conversation_id }
});
if (!response.ok || !response.body) throw new Error(`HTTP ${response.status}`);
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const events = buffer.split('\n\n');
buffer = events.pop();
for (const raw of events) {
if (!raw.startsWith('data: ')) continue;
const event = JSON.parse(raw.slice(6));
if (event.error) throw new Error(event.error);
if (event.delta) onDelta(event.delta);
}
}
}
필요할 때 Redis로 메모리 업그레이드
인메모리 딕셔너리는 단일 프로세스에는 적합합니다. 대화에 TTL, 재개 가능성 또는 다중 인스턴스가 필요해지면 Redis를 사용하세요. 대화 내용이 쿼리, 감사 또는 보고해야 하는 제품 데이터라면 PostgreSQL을 사용하세요. Redis로의 변경은 로직이 아닌 스토리지 배관 작업입니다. pip install redis를 실행한 후, build_messages와 remember가 아래 헬퍼를 통해 읽고 쓰도록 하세요.
import redis
redis_client = redis.Redis(
host=os.environ.get("REDIS_HOST", "127.0.0.1"), port=6379, decode_responses=True
)
def load_history(conv_id: str) -> list:
raw = redis_client.get(f"chat:{conv_id}")
return json.loads(raw) if raw else []
def save_history(conv_id: str, history: list) -> None:
redis_client.setex(f"chat:{conv_id}", 60 * 60 * 24, json.dumps(history))
프로덕션용 AI 챗봇 구축 시 에러 핸들링
고객은 실패가 속도 제한, 잔액 부족, 업스트림 중단 중 어디서 왔는지 신경 쓰지 않습니다. UI가 멈추는지 여부만 중요합니다. 40단어 이후에 스트림이 끊기는 지원 위젯을 상상해 보세요. TokenLab 문서에 따르면 출력이 도착한 후 중단된 스트림은 완전한 응답이 아니며, 이를 반복하면 다른 출력이 생성되어 두 번 청구될 수 있습니다(API 에러 핸들링, 2026-10-03 확인). 이것이 4단계에서 미완성 턴을 저장하지 않는 이유입니다.
6단계: 재시도 규칙 적용
예고 없이 변경될 수 있는 사람이 읽을 수 있는 message가 아닌, HTTP 상태와 code로 에러를 처리하세요. 아래 표는 문서와 우리 서비스의 동작을 결합한 것입니다.
| 상태 | 의미 (문서 기준) | 서비스 동작 |
|---|---|---|
400 |
필드, 모델 ID 또는 입력이 유효하지 않음 | 500 반환, 로그 기록, 재시도 안 함 |
401 |
키 누락, 유효하지 않음, 만료됨 또는 취소됨 | 503 반환, 운영자에게 알림 |
402 |
잔액 또는 API 키 제한 부족 (insufficient_balance, quota_exceeded) |
503 반환, 운영자에게 알림 |
403 |
키가 이 모델을 사용할 수 없음 | 503 반환, 키의 모델 목록 확인 |
404 |
리소스 또는 모델을 찾을 수 없음 | 500 반환, /v1/models와 ID 대조 |
429 |
요청 제한 도달 | Retry-After 대기, 1회 재시도 (동기식만) |
500–504 |
서비스 이용 불가 또는 네트워크 실패 | retryable이 true일 때만 재시도; retry_after 준수 |
문서에서 주의할 점 두 가지입니다. 503 all_channels_failed가 항상 일시적인 중단을 의미하지는 않습니다. retryable이 false이면 동일한 요청을 반복하지 말고, 다른 모델을 선택하기 전에 GET /v1/models를 확인하세요. 또한 Chat Completions와 Responses는 OpenAI 스타일의 error 객체를 사용하지만, Anthropic Messages와 Gemini는 자체 형식을 유지합니다. 우리 서비스는 모든 허용된 모델에서 Chat Completions를 호출하므로 하나의 파서로 작동합니다. 티어를 네이티브 형식으로 이동하면 두 번째 파서가 필요합니다.
응답 헤더의 요청 ID를 엔드포인트, 모델, 시간, 사용자 또는 작업 ID와 함께 저장하세요. 문서는 우리가 사용한 페이지에서 헤더 이름을 명시하지 않으므로, 로그를 연결하기 전에 문제 해결 경로에서 확인하세요.
나머지 프로덕션 체크리스트는 간단합니다:
- 프론트엔드 실패를 백엔드 로그와 연결할 수 있도록 자체 요청 ID를 추가하세요.
- 사용자당 동시성 및 요청 크기를 제한하세요.
message의max_length=4000은 조정해야 할 시작점입니다. MAX_HISTORY와 같이 메시지 수로 기록을 제한하세요. 결제 문서에 따르면 토큰화는 모델, 언어, 미디어 입력에 따라 다르므로 글자 수 공식은 신뢰할 수 있는 결제 추정치가 아닙니다.- 매 턴마다 모델, 지연 시간, 사용량, 완료 이유를 기록하세요.
- 사용자에게 보이는 메시지와 내부 에러 상세 내용을 분리하세요.
- 첫 번째 실제 중단이 발생하기 전에 대체 티어로 테스트 요청을 한 번 보내 폴백(fallback)이 작동하는지 확인하세요.
AI 챗봇을 위한 모델 선택 및 비용 추정
프론트엔드가 임의의 모델 문자열을 API로 전달하게 하지 마세요. 자유 텍스트 필드는 오타, 은퇴한 모델, 노출할 의도가 없었던 모델을 불러올 수 있습니다. ALLOWED_MODELS 맵은 공급자가 모델을 폐기할 때 클라이언트 업데이트 없이 모델을 교체할 수 있는 단일 지점을 제공합니다. 알 수 없는 모델 ID는 model_not_found를 반환하며, 문서는 현재 ID를 위해 /v1/models를 안내합니다. 허용 목록을 잠그기 전에 모델 디렉토리를 확인하세요.
현재 100만 토큰당 가격
이는 2026-10-03에 확인된 각 모델의 라이브 API 항목에 따른 USD 기준 TokenLab 가격입니다. 가격은 2026-10-02T16:53:30.068Z에 마지막으로 업데이트되었습니다. 4개 모델 모두 라이프사이클 단계가 active였습니다.
| 티어 | 모델 ID | 입력 / 1M | 출력 / 1M | 캐시 읽기 / 1M | 출처 |
|---|---|---|---|---|---|
| default | deepseek-v4-flash (비피크) |
$0.15 | $0.60 | $0.003 | 모델 API |
| default | deepseek-v4-flash (피크) |
$0.30 | $1.20 | $0.006 | 모델 API |
| flagship | gpt-5.5 |
$1.50 | $9.00 | $0.15 | 모델 API |
| balanced | claude-sonnet-5 |
$0.90 | $4.50 | $0.09 | 모델 API |
| budget | gemini-3.5-flash |
$0.75 | $4.50 | $0.075 | 모델 API |
deepseek-v4-flash의 경우, 항목은 중국 공휴일을 제외한 평일의 피크 시간을 나열합니다. 비피크 시간은 주말과 해당 공휴일을 포함합니다. claude-sonnet-5는 100만 토큰당 $1.125의 캐시 쓰기 가격도 나열합니다.
작업 요청 예시
입력 2,000 토큰, 출력 500 토큰인 한 턴을 가정합니다. 이 크기는 측정값이 아닌 가정입니다. gpt-5.5의 경우:
- 입력: 2,000 × $1.50 / 1,000,000 = $0.0030
- 출력: 500 × $9.00 / 1,000,000 = $0.0045
- 합계: 턴당 $0.0075 (추정치, 캐시 히트 없음)
같은 산술을 사용하면 deepseek-v4-flash는 비피크 시 $0.0006, 피크 시 $0.0012, claude-sonnet-5는 $0.00405, gemini-3.5-flash는 $0.00375로 추정됩니다. 1,000 턴에 걸쳐 gpt-5.5는 약 $7.50, deepseek-v4-flash는 $0.60에서 $1.20 정도입니다. 실제 턴은 기록이 쌓이면서 커지므로, 이 크기 산정을 신뢰하기보다는 응답이나 최종 스트림 청크에서 usage를 읽으세요.
실제로는 대부분의 대화를 기본 티어로 라우팅하고, 필요한 턴에만 gpt-5.5나 claude-sonnet-5를 예약합니다. 결제 문서에서는 토큰당 최저 가격이 항상 작업당 최저 비용은 아니라고 경고합니다. 동일한 프롬프트를 두 티어에서 실행하고 사용량(Usage)에서 최종 비용을 비교하세요.
출시 전 두 가지 확인 사항입니다. 결제 문서에서는 복사된 가격표를 하드코딩하지 말라고 하므로, 앱에 비용을 표시한다면 GET /v1/models/:model/pricing을 읽으세요(결제 및 가격, 2026-10-03 확인). 또한 deepseek-v4-flash 항목은 json-mode, prompt-cache, tool-use를 나열하지만 vision은 나열하지 않습니다. 챗봇이 이미지를 허용한다면 해당 턴을 vision이 나열된 티어로 라우팅하세요.
스모크 테스트 및 AI 챗봇 배포
7단계: 엔드투엔드 실행
TOKENLAB_API_KEY가 내보내진 터미널에서 서버를 시작하세요. curl 페이로드는 ChatRequest와 동일한 message, tier, conversation_id 필드를 사용합니다.
uvicorn main:app --reload --port 8000
# 1. 동기식 엔드포인트: 짧은 답변과 conversation_id 기대
curl -X POST http://localhost:8000/chat \
-H "Content-Type: application/json" \
-d '{"message": "Reply only with OK.", "tier": "default"}'
# 2. 스트리밍: data: {"delta": ...} 라인들, 그 다음 {"done": true, ...} 기대
curl -N -X POST http://localhost:8000/chat/stream \
-H "Content-Type: application/json" \
-d '{"message": "Hello!", "tier": "default"}'
# 3. 메모리: 위에서 반환된 conversation_id 재사용
curl -X POST http://localhost:8000/chat \
-H "Content-Type: application/json" \
-d '{"message": "What did I just say?", "tier": "default", "conversation_id": "PASTE_ID_FROM_STEP_1"}'
# 4. 강제 실패: HTTP 400 및 "unknown tier: nope" 기대
curl -i -X POST http://localhost:8000/chat \
-H "Content-Type: application/json" \
-d '{"message": "Hello!", "tier": "nope"}'
업스트림 실패를 강제하려면 잘못된 TOKENLAB_API_KEY로 서버를 재시작하세요. 클라이언트는 깔끔한 503 메시지를 받아야 하며, 로그에는 invalid_api_key와 함께 업스트림 401이 표시되어야 합니다. 그런 다음 요청(Requests)을 열어 성공한 호출의 모델, 상태, 사용량을 확인하세요. 한 턴을 스트리밍하고, 대화 하나를 보존하며, 강제 실패 시 깔끔한 에러를 반환할 수 있다면 올바른 기반을 갖춘 것입니다.
그 이후의 다음 업그레이드는 AI보다는 지루한 인프라 작업입니다:
- 인증: 한 사용자가 다른 사용자의 대화를 읽지 못하도록 합니다.
- 지속성: 세션이 배포 후에도 유지되도록 합니다.
- 사용자당 속도 제한: 시끄러운 사용자가 할당량을 소진하지 못하도록 합니다.
- 사용량 귀속: 챗봇이 고객 대면용인 경우 필요합니다.
- 백그라운드 요약: 대화에 장기 기억이 필요한 경우 필요합니다.
베이스 URL 마이그레이션이 완료되면, 모델 변경은 ALLOWED_MODELS에 대한 설정 편집만으로 가능합니다.
FAQ
AI 챗봇을 만들려면 웹소켓이 필요한가요?
아니요. 4단계에서 보여준 SSE는 대부분의 채팅 제품을 커버합니다. 웹소켓은 실시간 협업이나 서버 시작 이벤트와 같이 요청/응답 외부의 양방향 푸시가 필요할 때 가치를 더합니다. 표준 채팅 UI의 경우 SSE가 배포 및 디버그하기 더 쉽습니다.
챗봇은 기본적으로 어떤 모델을 사용해야 하나요?
deepseek-v4-flash와 같은 저가형 티어로 시작하고, 더 어려운 턴을 위해 허용 목록 뒤에 claude-sonnet-5나 gpt-5.5를 추가하세요. 2026-10-03에 확인된 위 가격이 차이를 보여줍니다. 그런 다음 두 티어에서 자신의 프롬프트를 테스트하고 사용량에서 최종 비용을 비교하세요. 라인업이 변경되므로 커밋하기 전에 모델 디렉토리를 확인하세요.
챗봇이 왜 402나 429를 반환하나요?
402는 계정 잔액이나 키 자체의 사용량 제한이 너무 낮음을 의미합니다. 코드는 insufficient_balance와 quota_exceeded이며, 요청을 반복해도 도움이 되지 않습니다. 429는 요청 제한에 도달했음을 의미하므로 Retry-After를 기다린 후 재시도하세요. 동기식 엔드포인트는 1회 재시도하며, 스트리밍 엔드포인트는 클라이언트에 에러를 보고합니다.
브라우저가 TokenLab을 직접 호출할 수 있나요?
안전하지 않습니다. 인증 문서에서는 클라이언트 측 코드에 API 키를 노출하지 말라고 명시합니다. 키는 서버 환경에 보관하고 프론트엔드 샘플처럼 브라우저가 자체 인증된 백엔드를 호출하게 하세요.
tokenlab.sh에서 API 키를 생성하고, 자금을 추가한 뒤 위의 스모크 테스트를 실행하세요.
출처
2026-10-03 기준 가격
- TokenLab Docs: Quickstart2026-10-03 기준 확인
- TokenLab Docs: Authentication2026-10-03 기준 확인
- TokenLab Docs: Streaming2026-10-03 기준 확인
- TokenLab Docs: Handle API errors2026-10-03 기준 확인
- TokenLab Docs: Billing and pricing2026-10-03 기준 확인
- TokenLab Docs: SDKs & Libraries2026-10-03 기준 확인
- TokenLab Docs: Create Chat Completion2026-10-03 기준 확인
- TokenLab Docs: List Models2026-10-03 기준 확인



