seekrit
Docs/AI SDK

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.

note

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/openai calls /v1/responses, not /v1/chat/completions. Current versions default to the Responses API, so a route or allowlist pinned to /v1/chat/completions refuses every request. Use /v1/**, or pin /v1/responses on 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. generateText with tools runs your functions with whatever credentials they close over. If those are third-party keys, that's the case for shape 3.