본문으로 건너뛰기

자체 API 키 사용(BYOK)

사용자마다 provider 크레딧 비용을 부담하고 싶지는 않을 것입니다. 사용자가 자체 API 키를 사용하도록 합니다.

키는 브라우저에 보관되므로 서버에 사용자 키를 저장할 걱정을 하지 않아도 됩니다.

릴레이는 키를 한 번의 호출에 사용한 후 잊습니다. 전송할 때마다 키는 JSON 본문이 아니라 x-byok-* 헤더에 담깁니다.

다음 네 단계를 수행합니다.

  1. defineByok로 스토어를 생성합니다.
  2. byok.update로 UI에서 키를 저장합니다.
  3. 같은 스토어를 useChat에 전달합니다.
  4. getByokKey로 릴레이에서 키를 읽습니다.

1. 스토어 생성​

import { defineByok, defaultByokStorage } from "@tanstack/ai-client/byok";

export const byok = defineByok({
storage: defaultByokStorage(),
});

브라우저가 passkey를 지원하면 defaultByokStorage()가 passkey를 사용합니다. 지원하지 않으면 키가 이 탭의 메모리에만 보관됩니다.

2. 키 저장​

자체 UI에서 byok.update("openai", value)를 호출합니다. 라이브러리는 대화상자를 제공하지 않습니다.

useByok(byok)는 저장된 키의 상태를 제공합니다.

import { useState } from "react";
import { useByok } from "@tanstack/ai-react";
import { byok } from "./byok";

export function KeyForm() {
const snapshot = useByok(byok);
const [value, setValue] = useState("");
const [error, setError] = useState("");
const status = snapshot.status.openai;
const last4 = status && "masked" in status ? status.masked : "";

return (
<form
onSubmit={(event) => {
event.preventDefault();
const next = value.trim();
if (!next) return;
void byok
.update("openai", next)
.then(() => {
setValue("");
setError("");
})
.catch((caught: unknown) => {
setError(
caught instanceof Error ? caught.message : "Could not save key",
);
});
}}
>
<input
type="password"
autoComplete="off"
value={value}
onChange={(event) => setValue(event.target.value)}
placeholder={last4 ? `Saved ${last4}` : "Paste a key"}
/>
<button type="submit" disabled={!value.trim()}>
Save
</button>
{error ? <p>{error}</p> : null}
</form>
);
}

ts-react-chat 예제에는 복사해서 사용할 수 있는 키 아이콘 팝업이 있습니다.

3. useChat으로 전송​

같은 스토어를 전달합니다. forwardedProps.provider를 "openai"로 설정합니다. 그러면 클라이언트가 해당 키만 전송합니다.

import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
import { byok } from "./byok";

export function Chat() {
const { sendMessage, isLoading } = useChat({
connection: fetchServerSentEvents("/api/chat"),
byok,
forwardedProps: { provider: "openai", model: "gpt-5.6" },
});

return (
<button
type="button"
disabled={isLoading}
onClick={() => {
void sendMessage("Hello");
}}
>
Send
</button>
);
}

provider를 설정하지 않으면 전송 시 오류가 발생합니다. 클라이언트는 저장된 모든 키를 첨부하지 않습니다.

내장 fetch 및 XHR 어댑터는 헤더를 POST 요청에 복사합니다.

사용자 지정 connect를 작성한다면 runContext.headers를 직접 복사합니다. 연결 어댑터를 참고합니다.

4. 릴레이에서 키 읽기​

모든 API 라우트에서 다음과 같이 사용합니다.

import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from "@tanstack/ai";
import { createOpenaiChat } from "@tanstack/ai-openai";
import { openaiByok } from "@tanstack/ai-openai/byok";
import { byokMissing, getByokKey } from "@tanstack/ai/byok/server";

export async function POST(request: Request) {
const params = await chatParamsFromRequest(request);
const apiKey = getByokKey(request, openaiByok);
if (!apiKey) return byokMissing(openaiByok);

const stream = chat({
adapter: createOpenaiChat("gpt-5.6", apiKey),
messages: params.messages,
threadId: params.threadId,
runId: params.runId,
});
return toServerSentEventsResponse(stream);
}

openaiByok는 어댑터 기본 진입점이 아니라 @tanstack/ai-openai/byok에서 가져옵니다. /byok 파일은 브라우저에서 안전합니다. 기본 진입점은 provider SDK를 가져옵니다.

헤더가 우선합니다. 헤더가 비어 있으면 getByokKey가 환경에서 OPENAI_API_KEY를 읽습니다. 둘 다 비어 있으면 byokMissing이 401을 반환합니다.

주의: 원본 키를 로그에 기록하지 않습니다. 오류 문자열에는 maskKey를 사용합니다.

키를 붙여 넣고 메시지를 전송하면 릴레이가 해당 키로 OpenAI를 호출합니다.

릴레이에 이미 환경 키가 있는 경우​

기본적으로 브라우저 키가 없으면 전송하지 않습니다. 릴레이에 환경 키가 있다면 byok.setServerCoverage(true)를 호출합니다.

byok.setServerCoverage(true);

그러면 붙여 넣은 키가 없어도 전송합니다. 릴레이는 환경 키를 사용합니다. 환경 키도 비어 있으면 릴레이가 byokMissing(401)을 반환합니다. 클라이언트는 snapshot.prompt를 설정합니다.

이미지, 오디오 및 OpenRouter​

그 외의 경우는 다음과 같습니다.

  • 이미지 및 오디오 POST는 같은 스토어를 사용합니다. 생성 훅을 참고합니다.
  • OpenRouter는 OAuth로 키를 발급할 수 있습니다. OpenRouter로 로그인을 참고합니다.
  • Lovable은 @tanstack/ai-lovable/byok의 lovableByok를 사용합니다. Lovable AI Gateway를 참고합니다.