AI SDK
The AI SDK's provider factories read OPENAI_API_KEY from the environment by
default, and take an explicit apiKey and baseURL when you want to override
them. Both shapes are one line.
Start here if seekrit is new: three
commands put the keys in
an environment, mint a token bound to it, and export SEEKRIT_TOKEN. A token
reads everything in its
environment, so
there is no per-key or per-framework setup to do before any of the below.
1. Wrap the process
seekrit run -- next dev
seekrit run -- node server.js
Nothing to change in the app. The default openai(...) provider picks up the
injected OPENAI_API_KEY.
2. Resolve in code
Serverless and edge runtimes are the reason this shape exists: there is no process to wrap and no ambient environment, so resolve inside the handler.
import { createOpenAI } from "@ai-sdk/openai";
import { generateText } from "ai";
import { Seekrit } from "@seekrit/sdk";
export async function POST(req: Request) {
const secrets = await new Seekrit().resolve();
const openai = createOpenAI({ apiKey: secrets.OPENAI_API_KEY });
const { text } = await generateText({
model: openai("gpt-5.6-terra"),
prompt: await req.text(),
});
return Response.json({ text });
}
On Cloudflare Workers there is no process.env, so pass the token from the
Worker's own env — the one variable your platform holds:
const secrets = await new Seekrit({ token: env.SEEKRIT_TOKEN }).resolve();
The SDK is pure WebCrypto and global fetch, so it runs unchanged on Node, Bun,
Deno, browsers, and Workers.
3. Never hold the key
import { createOpenAI } from "@ai-sdk/openai";
const openai = createOpenAI({
baseURL: "http://127.0.0.1:8080/openai/v1",
apiKey: "{{seekrit:OPENAI_API_KEY}}",
});
# seekrit-proxy.toml
listen = "127.0.0.1:8080"
[[route]]
prefix = "/openai"
upstream = "https://api.openai.com"
allow = ["OPENAI_API_KEY"]
methods = ["POST"]
paths = ["/v1/chat/completions", "/v1/embeddings"]
For a provider without a first-party package, createOpenAICompatible takes the
same baseURL and apiKey.
Without running the proxy
createOpenAI takes a custom fetch, which is all the substitution needs:
import { seekritFetch } from "@seekrit/sdk/fetch";
const openai = createOpenAI({
apiKey: "{{seekrit:OPENAI_API_KEY}}",
fetch: seekritFetch({ allow: { "api.openai.com": ["OPENAI_API_KEY"] } }),
});
Same placeholder, same allowlist, no sidecar — and a weaker boundary, since it runs in your process. In-process injection sets out the trade-off.
Gotchas
-
@ai-sdk/openaicalls/v1/responses, not/v1/chat/completions. Current versions default to the Responses API, so a route or allowlist pinned to/v1/chat/completionsrefuses every request. Use/v1/**, or pin/v1/responseson purpose. -
Resolve once per request, not once per token.
resolve()is a network round trip and a decrypt; calling it inside a streaming loop turns every chunk into an API call. Resolve at the top of the handler. -
A browser bundle must never hold a service token. The SDK runs in a browser because Workers and Deno need the same code path, not because a token belongs in client JavaScript. Keep resolution server-side.
-
Tool calls are where a leak becomes expensive.
generateTextwithtoolsruns your functions with whatever credentials they close over. If those are third-party keys, that's the case for shape 3.