API Key Input
사용자가 자기 AI API 키를 넣는 입력 칸과, 웰컴 페이지의 AI 테스트(LlmPlayground)입니다. 로그인 안내와 토큰 사용량 표시를 포함합니다.
사용자가 자기 AI API 키를 넣는 입력입니다. 키를 한 개만 받는 ApiKeyInput, 여러 공급자(GPT, Gemini, Claude)의 키를 받는 MultiApiKeyList(표)와 MultiApiKeyTabs(탭)가 있습니다. 모두 로그인 전에도 화면에 보이지만, 입력하려고 하면 "로그인이 필요해요" 안내가 먼저 뜨고 로그인한 뒤에만 키를 저장할 수 있습니다.
onSave를 연결하면 키는 저장할 때 서버로 가고, 화면에는 끝자리(keyHint)만 남습니다. 서버에서는 암호화해 저장하고, 응답에 원본 키를 돌려주지 마세요. 브라우저 쪽에는 localStorage, 쿠키 등 어디에도 키를 저장하지 않습니다.| 컴포넌트 | 언제 쓰나 | 저장 방식 |
|---|---|---|
| ApiKeyInput | 키를 한 개만 받을 때. | onSave를 넘기면 서버로 저장. 넘기지 않으면 브라우저 메모리에만 두고 요청마다 헤더로 보냅니다(웰컴 페이지의 AI 테스트 방식). |
| MultiApiKeyList · MultiApiKeyTabs | 여러 공급자 중 서비스가 고른 것만 받을 때. | 항상 onSave로 서버에 저장합니다. |
ApiKeyInput
로그인하지 않은 상태에서 칸을 눌러 보세요. 취소는 닫기, 확인은 로그인 창을 엽니다. 로그인한 뒤에는 키를 입력하고 저장할 수 있습니다.
키는 이 브라우저 메모리에만 두고 서버에 저장하지 않아요. 새로고침하면 지워져요.
<ApiKeyInput />서버로 저장하는 방식
onSave를 넘기면 저장 버튼이 값을 서비스에 넘기고, 서비스가 서버로 보냅니다. 성공하면 입력한 키는 지워지고 끝자리만 보입니다.
<ApiKeyInput provider="claude" showModel onSave={save} />import { ApiKeyInput, type ApiKeyInputValue } from "@/meerkit";
async function save(v: ApiKeyInputValue) {
// v = { provider, model, apiKey? } — apiKey는 이번에 새로 입력한 경우에만 있습니다.
const res = await fetch("/api/ai-key", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(v) });
if (!res.ok) throw new Error("저장하지 못했어요."); // 던진 메시지가 화면에 보입니다
}
<ApiKeyInput provider="claude" showModel initial={saved} onSave={save} />메모리에만 두는 방식
onSave가 없으면 키는 브라우저 메모리에만 있고 새로고침이나 로그아웃하면 지워집니다. 요청할 때 X-LLM-API-Key 헤더로만 보내고 서버는 저장하거나 로그에 남기지 않습니다. 서버에 키를 두지 않는 가벼운 데모나 개인용 도구에 맞는 방식이고, 웰컴 페이지의 AI 테스트(LlmPlayground)가 이렇게 씁니다.
| 동작 | 방법 |
|---|---|
| 로그인 전에 입력 시도 | 칸을 누르면 로그인 안내 창이 뜹니다. 취소하면 닫히고, 확인하면 로그인 창이 열립니다. |
| 키 저장 | 로그인한 뒤 키를 입력하고 저장을 누릅니다. 공급자별 키 모양(OpenAI sk-, Gemini AIza, Claude sk-ant-)이 맞지 않으면 안내가 나옵니다. |
| 키 보기·가리기 | 칸 오른쪽의 눈 아이콘으로 입력한 글자를 확인합니다. |
| 키 지우기 | 메모리 방식: 저장하면 앞 3자리와 뒤 4자리만 보이고, 지우기를 누르면 사라집니다. |
| 모델 고르기 | showModel을 켜면 모델 선택이 생깁니다. |
import { ApiKeyInput, useLlmKeyStore } from "@/meerkit";
<ApiKeyInput provider="claude" showModel />
const apiKey = useLlmKeyStore((s) => s.apiKey);
const model = useLlmKeyStore((s) => s.model); // 비어 있으면 서비스의 기본 모델| Prop | Type | 설명 |
|---|---|---|
className | string | 바깥 요소에 붙일 클래스. |
provider | "openai" | "gemini" | "claude" | 어느 공급자의 키를 받을지. 기본 openai. 안내 문구와 키 모양 검사가 공급자에 맞게 바뀝니다. |
showModel | boolean | 모델 고르는 칸을 보입니다. 기본 false. |
models | { id, label }[] | 모델 목록을 바꿉니다. 기본은 공급자의 기본 목록(ai/providers.ts). |
onSave | (v: ApiKeyInputValue) => void | Promise<void> | 넘기면 서버로 저장하는 방식이 됩니다. 실패하면 throw 하세요. |
initial | { keyHint?, model? } | 서버에 저장된 값(onSave와 함께). 원본 키는 받지 않고 끝자리만 받습니다. |
MultiApiKeyList · MultiApiKeyTabs
서비스가 여러 공급자의 키를 받을 때 씁니다. 받을 공급자는 providers로 서비스가 고릅니다(예: GPT와 Claude만). 표 형태(MultiApiKeyList)와 탭 형태(MultiApiKeyTabs)는 값과 동작이 같고 모양만 다릅니다. 탭은 한 줄로 늘어서고 넘치면 옆으로 밀어 봅니다.
두 가지 선택은 뜻이 다릅니다
| 선택 | 뜻 | 몇 개까지 |
|---|---|---|
| 이 키로 사용 | 지금 AI를 부를 때 쓸 키입니다. 사용자가 고릅니다. | 라디오라서 하나만 고를 수 있습니다. 키가 입력된 공급자만 고를 수 있습니다. |
| 기본 키 | 사용 중인 키가 한도 초과·만료·오류로 막혔을 때 대신 쓸 키입니다. "기본 키" 옆 (i)에 마우스를 올리면 설명이 나옵니다. | 하나만 지정합니다. 키가 입력된 공급자만 지정할 수 있습니다. 사용 중인 키를 기본 키로 지정하면 다른 키로 바뀌지 않습니다. |
failoverApiKeys로 다음 상태를 계산해 저장하세요. 자동 전환이 필요 없는 서비스는 autoFailover={false}로 "기본 키"를 화면에서 뺄 수 있습니다. 공급자를 하나만 넘겨도 대체할 키가 없으니 나타나지 않습니다.| 이 키로 사용 | 공급자 | API 키 | 모델 | 기본 키 |
|---|---|---|---|---|
| GPT (OpenAI) | ||||
| Claude (Anthropic) |
<MultiApiKeyList providers={["openai", "claude"]} initial={saved} onSave={save} /><MultiApiKeyTabs initial={saved} onSave={save} /><MultiApiKeyTabs providers={["openai", "claude"]} autoFailover={false} onSave={save} />import { MultiApiKeyTabs, type ApiKeySavePayload } from "@/meerkit";
async function save(payload: ApiKeySavePayload) {
// payload = { activeProvider, defaultProvider, keys: [{ provider, model, apiKey? }] }
// activeProvider = 이 키로 사용, defaultProvider = 기본 키(자동 전환을 안 쓰면 null)
// apiKey는 이번에 새로 입력한 키만 있습니다. 없으면 서버에 저장된 키를 그대로 씁니다.
const res = await fetch("/api/ai-keys", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) });
if (!res.ok) throw new Error("저장하지 못했어요."); // 던진 메시지가 화면에 보입니다
}
<MultiApiKeyTabs providers={["openai", "gemini"]} initial={saved} onSave={save} />import { failoverApiKeys } from "@/meerkit";
// 사용 중이던 openai 키가 한도 초과(LLM_QUOTA_EXCEEDED)로 실패했다면
const { state, switchedTo } = failoverApiKeys(
{ activeProvider: "openai", defaultProvider: "claude", keys: [{ provider: "openai" }, { provider: "claude" }] },
"openai",
);
// state.activeProvider === "claude" (기본 키가 사용 중인 키가 됩니다)
// switchedTo: "claude" (바꿀 키가 없으면 null)
// 사용 중인 키 자체가 기본 키로 지정돼 있으면 전환하지 않고 switchedTo는 null입니다.| Prop | Type | 설명 |
|---|---|---|
onSave필수 | (payload) => void | Promise<void> | 저장 버튼을 누르면 호출됩니다. 실패하면 throw 하세요. |
providers | ProviderId[] | 키를 받을 공급자. 기본은 openai, gemini, claude 모두. |
models | Partial<Record<ProviderId, { id, label }[]>> | 공급자별 모델 목록을 바꿉니다. |
initial | { entries: { provider, model?, keyHint? }[]; activeProvider?; defaultProvider? } | 서버에 저장된 값. 수정 화면에서 넘기면 "저장됨 sk-…a1b2"로 보입니다. |
autoFailover | boolean | 기본 키(자동 전환)를 쓸지. 기본 true. false면 기본 키 지정이 화면에서 빠집니다. |
className | string | 바깥 요소에 붙일 클래스. |
LlmPlayground
ApiKeyInput, AiPromptInput, ThinkingIndicator을 묶어 서버의 /api/llm/chat까지 이어 둔 AI 테스트입니다. 웰컴 페이지에 기본으로 들어 있습니다. 로그인 전에는 키 칸만 보이고, 로그인하면 질문 입력창이 열리며, 응답 카드 아래에 이번 응답의 토큰 수와 예상 비용, 이 탭에서의 누적 합계가 나옵니다.
ai: {
playground: false, // 웰컴 페이지의 AI 테스트를 숨깁니다. 기본 true
},사용량과 예상 비용
- 토큰 수는 AI 응답이 돌려준 값(입력·출력·합계)을 그대로 보여 줍니다. 서버에 기록하지 않고, 이 탭의 메모리에서만 합산합니다.
- 예상 비용은
ai/pricing.ts의 모델별 단가(USD, 토큰 100만 개당)로 계산하는 참고용 값입니다. 공급자 요금은 바뀔 수 있으니 모델을 늘리거나 단가가 달라지면 이 파일을 고치세요. - 사용 기록 저장, 이용 이력, 한도는 이 기능에 들어 있지 않습니다.
서버와 주고받는 것
| 구분 | 내용 |
|---|---|
| 요청 | POST /api/llm/chat · 로그인 필요 · 헤더 X-LLM-API-Key · 본문 { message, model } |
| 응답 | { reply, model, usage: { input_tokens, output_tokens, total_tokens } } |
| 허용 모델 | gpt-4o-mini, gpt-4o (목록에 없으면 422). 공급자는 현재 OpenAI만 열려 있습니다. |
오류와 안내 문구
| code | 상태 | 뜻 |
|---|---|---|
| LLM_KEY_REQUIRED | 400 | 키 헤더가 없습니다. |
| LLM_KEY_INVALID | 401 | 키가 올바르지 않거나 사용할 수 없습니다. |
| LLM_QUOTA_EXCEEDED | 429 | 입력한 키의 크레딧·한도를 모두 썼습니다 (공급자 사이트에서 충전·확인). |
| LLM_RATE_LIMITED | 429 | 짧은 시간에 요청이 많습니다. 잠시 뒤 다시 시도합니다. |
| LLM_INPUT_TOO_LONG | 413 | 입력이 너무 깁니다. |
| LLM_TIMEOUT | 504 | 응답이 늦어지고 있습니다. |
백엔드에서 쓰기
from app.llm import chat_once
reply = chat_once("안녕", api_key=사용자가_넣은_키, model_name="gpt-4o-mini", provider="openai")
reply.text # 답
reply.usage.total_tokens # 토큰 수다른 공급자(Anthropic 등)를 열려면 langchain-anthropic을 추가하고 app/llm/client.py의 BYOK_PROVIDERS에 이름을 더합니다. 키와 질문·응답 내용은 로그에 남기지 않습니다.
