BringYourOwnKey (BYOK) is a lightweight, framework-agnostic library that lets users supply their own LLM API keys directly in your app β no proxy, no extra backend service required. A browser-side widget collects and stores the key locally; a one-function backend helper reads it from request headers. Your existing frontend + backend architecture stays exactly as it is.
Browser (your frontend) Your FastAPI / Starlette backend
ββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββββ
β byok.getHeaders() βββyour fetch()ββ>β creds = extract_byok(req) β
β { β β # creds.api_key β
β X-BYOK-Api-Key, β β # creds.provider β
β X-BYOK-Provider, β β # creds.model β
β X-BYOK-Model β β β
β } β β client = Groq(creds.api_key)β
ββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββββ
Key stored in localStorage only. You create the LLM client.
Library never makes requests for you. Library never touches LLMs.
- Zero extra services β no LiteLLM proxy, no auth middleware backend. Drop it into any existing project.
- You own the request β
getHeaders()returns a plain object you spread into your ownfetch(). The library never calls your API on your behalf. - Framework agnostic β frontend is a standard Web Component (
<byok-settings>); works in React, Vue, Svelte, Next.js, or plain HTML. Backend is pure Starlette; works with FastAPI or bare Starlette. - Secure by design β keys live only in the user's browser (
localStorage). They never touch your server's storage or logs. - Built-in provider presets β Groq (free tier), OpenAI, and Google Gemini ship out of the box, each with model lists and key validation.
- Minimal surface β 4 Python exports, 6 TypeScript exports. Nothing else to learn.
- TypeScript (Web Components / Custom Elements)
- No framework dependency β works with React, Vue, Svelte, Next.js, or plain HTML
- Python 3.9+
- Starlette β₯ 0.27 (FastAPI compatible β FastAPI is built on Starlette)
- The AI/ML components:
- LLM provider selection and model configuration are documented
- API keys are managed client-side only β never stored server-side
- Key format validation is implemented per provider before submission
- Rate limit handling is delegated to the caller (you control the request)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β User's Browser β
β β
β βββββββββββββββββββ saves ββββββββββββββββββββββββββββββββ β
β β <byok-settings>β ββββββββββ> β localStorage β β
β β Web Component β β { activeProvider, keys, ... }β β
β βββββββββββββββββββ ββββββββββββββββββββββββββββββββ β
β β² opens β reads β
β β βΌ β
β βββββββββββββββββββ ββββββββββββββββββββββββββββββββ β
β β Your App UI β β byok.getHeaders() β β
β β (settings btn) β β β X-BYOK-Api-Key β β
β βββββββββββββββββββ β β X-BYOK-Provider β β
β β β X-BYOK-Model β β
β ββββββββββββββββ¬ββββββββββββββββ β
ββββββββββββββββββββββββββββββββββββββββββββββββββΌββββββββββββββββββββ
β spread into fetch()
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Your FastAPI / Starlette Backend β
β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β @app.post("/api/chat") β β
β β async def chat(request: Request): β β
β β creds = extract_byok(request) β reads the 3 headers β β
β β client = Groq(api_key=creds.api_key) β β
β β # ... call LLM with user's own key β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
App loads
β
βΌ
byok.guardFirstRun()
β
ββ Key already saved? ββYesββ> App ready, proceed normally
β
ββ No key yet
β
βΌ
<byok-settings> modal opens (first-run banner shown)
β
ββ User picks provider + model + pastes key
β
ββ Key validated client-side (prefix + length check)
β
ββ Saved to localStorage ββ> modal closes ββ> App ready
β
βΌ
User triggers an AI action
β
βΌ
byok.getHeaders() called
β
βΌ
fetch("/api/...", { headers: {...headers} })
β
βΌ
Backend: extract_byok(request)
β
βΌ
LLM called with user's own key
-
First-time setup β User lands on the app, no key configured.
guardFirstRun()auto-opens the settings modal. User picks Groq (free), pastes their key, clicks Save. App proceeds immediately β no page reload needed. -
Changing provider or model β User clicks the βοΈ settings button in your app.
openSettings()opens the modal. User switches from Groq to Gemini, saves. All subsequentgetHeaders()calls return the new provider's key automatically. -
Calling the backend β Your frontend calls
byok.getHeaders(), spreads the three headers into anyfetch()call. The backend callsextract_byok(request)to get a typedBYOKCredentialsobject and immediately creates the LLM client with the user's key.
- Frontend: Node.js 18+ and npm (only needed if using a bundler; plain HTML works without a build step)
- Backend: Python 3.9+, pip
# Minimal (Starlette only):
pip install -e .
# With FastAPI:
pip install -e ".[fastapi]"
# Development (pytest, uvicorn, fastapi):
pip install -e ".[dev]"from fastapi import FastAPI, Request
from byok import extract_byok
from groq import Groq
app = FastAPI()
@app.post("/api/analyze")
async def analyze(request: Request, body: dict):
creds = extract_byok(request)
# creds.api_key β "gsk_..."
# creds.provider β "groq"
# creds.model β "groq/llama-3.3-70b-versatile"
client = Groq(api_key=creds.api_key)
response = client.chat.completions.create(
model=creds.model or "llama-3.3-70b-versatile",
messages=[{"role": "user", "content": body["text"]}],
)
return {"result": response.choices[0].message.content}from fastapi import FastAPI
from byok import BYOKMiddleware, get_byok
app = FastAPI()
app.add_middleware(BYOKMiddleware)
@app.post("/api/analyze")
async def analyze(body: dict):
creds = get_byok() # no request param needed
...# Using npm / bundler:
cd frontend
npm install
npm run build # outputs to frontend/dist/
# OR import directly in plain HTML (no build step):
# <script type="module">
# import { createBYOK, PROVIDERS } from "./frontend/dist/byok.js";
# </script>import { createBYOK, PROVIDERS } from "@byok-lib/frontend";
const byok = createBYOK({
projectId: "my-app",
providers: [PROVIDERS.groq, PROVIDERS.openai, PROVIDERS.gemini],
accentColor: "#6366f1",
});await byok.guardFirstRun();
// User now definitely has a key saveddocument.getElementById("settings-btn")!.addEventListener("click", () => {
byok.openSettings({ onSave: (provider, key, model) => console.log("Saved") });
});const res = await fetch("/api/analyze", {
method: "POST",
headers: { "Content-Type": "application/json", ...byok.getHeaders() },
body: JSON.stringify({ text }),
});| Provider | ID | Key prefix | Free tier | Models |
|---|---|---|---|---|
| Groq | PROVIDERS.groq |
gsk_ |
β Yes | Llama 3.3 70B, Llama 3.1 8B, Compound, Compound Mini, Qwen3 32B, Kimi K2, GPT OSS 120B/20B |
| OpenAI | PROVIDERS.openai |
sk- |
β No | GPT-4o, GPT-4o Mini |
| Google Gemini | PROVIDERS.gemini |
AIza |
β Yes | Gemini 2.5 Flash, Flash-Lite, Pro |
| Export | Signature | Description |
|---|---|---|
extract_byok |
(request: Request) β BYOKCredentials |
Reads the 3 BYOK headers. Raises HTTP 401 if key or provider is missing. |
BYOKMiddleware |
app.add_middleware(BYOKMiddleware, skip_paths={...}) |
Auto-extracts headers on every request. Skips /, /health, /docs, /openapi.json, /redoc by default. |
get_byok |
() β BYOKCredentials |
Returns credentials stored by BYOKMiddleware. Raises HTTP 401 if called outside middleware context. |
BYOKCredentials |
dataclass(api_key, provider, model?) |
Immutable, frozen credentials container. |
| Export | Type | Description |
|---|---|---|
createBYOK(config) |
Function | Creates a BYOK instance. Config accepts projectId, providers[], optional accentColor. |
getHeaders() |
() β BYOKHeaders | null |
Returns the 3 headers ready to spread. Returns null if no key is configured. |
guardFirstRun() |
() β Promise<boolean> |
Opens the modal if unconfigured. Resolves true if already set, false after the user saves. |
openSettings(opts) |
({ onSave?, onCancel? }) β SettingsUI |
Opens the settings modal programmatically. |
PROVIDERS |
Object | Pre-built provider presets: PROVIDERS.groq, PROVIDERS.openai, PROVIDERS.gemini. |
KeyManager |
Class | Direct localStorage access. Use keyManager.clearAll() to reset a user's saved key. |
β Don't forget to star this repository if you find it useful! β
Thank you for considering contributing to this project! Contributions are highly appreciated and welcomed. To ensure smooth collaboration, please refer to our Contribution Guidelines.
This project is licensed under the GNU General Public License v3.0. See the LICENSE file for details.
Thanks a lot for spending your time helping BringYourOwnKey grow. Keep rocking π₯
Β© 2026 AOSSIE

