Migrating from OpenAI
Behest is OpenAI-SDK compatible. For most apps, switching is a one-line change: swap baseURL and hand the client a Behest JWT instead of your OpenAI key. Everything else — streaming, tool calls, structured outputs, the entire chat.completions surface — works unchanged.
Step 1: move your OpenAI key to Behest
Dashboard → Project → Providers → OpenAI → paste your sk-.... Behest encrypts it at rest and forwards requests on your behalf. You stop sending the raw OpenAI key from your servers.
Already have multiple provider keys (Anthropic, Google, etc.)? Add them too — Behest handles model routing for you.
Step 2: swap the SDK config
Node / TypeScript
Before:
import OpenAI from "openai";
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });Important — Behest is not a one-header swap. OpenAI accepts a long-lived
sk-...as the Bearer on/v1/chat/completions. Behest does not: Kong requires a short-lived JWT (minted per end-user). Thebehest_sk_live_...API key authenticates only the mint endpoint. Dropping your API key intonew OpenAI({ apiKey: BEHEST_KEY, baseURL })and callingchat.completions.createwill return 401. Use the v1.5 SDK (which mints for you) or mint manually.
After — recommended: v1.5 Behest SDK
import { Behest } from "@behest/client-ts";
const behest = new Behest({
key: process.env.BEHEST_KEY, // behest_sk_live_...
baseUrl: `https://${process.env.BEHEST_SLUG}.behest.app`,
});
// The SDK calls /v1/auth/mint under the hood per request, attaches the JWT
// as Authorization, injects X-Session-Id, and streams the response.
await behest.chat.completions.create({ messages, user_id: userId });After — if you want to keep using the raw OpenAI SDK: do the mint yourself on the server, pass the resulting JWT (not your API key) to OpenAI's client.
import OpenAI from "openai";
// 1) Mint a per-user JWT on your backend.
const r = await fetch(
`https://${process.env.BEHEST_SLUG}.behest.app/v1/auth/mint`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.BEHEST_KEY}`,
},
body: JSON.stringify({ user_id: userId, session_id: crypto.randomUUID() }),
}
);
const { access_token: behestJwt, session_id: sessionId } = await r.json();
// 2) Hand the MINTED JWT to the OpenAI SDK as apiKey. Do not pass BEHEST_KEY here.
const client = new OpenAI({
apiKey: behestJwt,
baseURL: `https://${process.env.BEHEST_SLUG}.behest.app/v1`,
defaultHeaders: { "X-Session-Id": sessionId },
});
await client.chat.completions.create({ messages, model: "gpt-4o-mini" });Python
from behest import Behest
behest = Behest() # reads BEHEST_KEY + BEHEST_BASE_URL from env
# Mints a JWT per call, scoped to user_id.
behest.chat.completions.create(messages=..., model="gpt-4o-mini", user_id=user_id)Raw OpenAI Python works too — same rule: mint first, pass the JWT.
import os, uuid, requests
from openai import OpenAI
session_id = str(uuid.uuid4())
mint = requests.post(
f"https://{os.environ['BEHEST_SLUG']}.behest.app/v1/auth/mint",
headers={"Authorization": f"Bearer {os.environ['BEHEST_KEY']}"},
json={"user_id": user_id, "session_id": session_id},
).json()
client = OpenAI(
api_key=mint["access_token"], # JWT, NOT BEHEST_KEY
base_url=f"https://{os.environ['BEHEST_SLUG']}.behest.app/v1",
default_headers={"X-Session-Id": session_id},
)cURL
- curl https://api.openai.com/v1/chat/completions \
- -H "Authorization: Bearer $OPENAI_KEY" \
+ # 1) Mint a JWT first. BEHEST_KEY only authenticates the mint endpoint.
+ ACCESS_TOKEN=$(curl -sS -X POST https://amber-fox-042.behest.app/v1/auth/mint \
+ -H "Authorization: Bearer $BEHEST_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{"user_id":"user_123","session_id":"'$(uuidgen)'"}' | jq -r '.access_token')
+
+ # 2) Call chat with the minted JWT.
+ curl https://amber-fox-042.behest.app/v1/chat/completions \
+ -H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hi"}]}'Step 3: nothing else changes
All of these work identically:
chat.completions.create(streaming and non-streaming)tool_calls/ function callingresponse_format: { type: "json_schema", ... }structured outputs- Vision / multimodal messages
embeddings.createmoderations.create
If something OpenAI-compatible doesn't work, it's a bug — open an issue.
What you get for free
- Per-user rate limits and usage — pass
user: userIdor mint a per-user JWT; no more "top customer blows the key's RPM". - No key in the browser — mint short-lived JWTs server-side; frontend gets a 15-min token.
- Cost visibility — dashboard breaks down cost by model, user, and day.
- Guardrails — PII redaction and prompt-injection detection in front of OpenAI.
- BYOK + fallback — keep using your OpenAI key; add Anthropic/Google for failover.
- Persistent threads — Behest stores conversation history for you (
X-Thread-Id).
Gotchas
- Model names: default pass-through. If you want Behest to pick a model based on capability or cost, set a project default and omit
modelin the request. - Organization header: OpenAI's
OpenAI-Organizationheader is ignored — Behest uses project-scoped routing. - Assistants API: not yet supported. If you rely on it, stay on OpenAI directly and wire Behest for
chat.completionspaths. Follow the roadmap. - Rate limit headers: Behest sets its own
x-ratelimit-*headers reflecting your tier, not OpenAI's org-level limits.
Adding per-user auth incrementally
You don't have to rewrite everything at once. Start with the drop-in baseURL swap above (server-to-server, API key in header). Later, move browsers to JWTs by:
- Add a
/api/tokenendpoint (Node / Next.js / Python). - Update your frontend to fetch a JWT and call Behest directly.
- Delete the server-side proxy.
Net effect: lower latency, full streaming, no bandwidth through your server.