AI & agents
Zero-config AI SDK access from any Vibes app. No keys, no baseURL, automatic per-end-user attribution.
Every Vibes sandbox can call LLMs through the platform with no setup. You
import the providers from @omg-dev/ai, write your streamText / generateText
call, and the runtime handles the API key, the upstream URL, and per-end-user
cost attribution.
Cost is billed to you (the app creator). The end-user identifier is used for your own dashboards and per-user features (rate limits, free tiers, etc.) — the platform doesn't pass costs through to your end users.
TL;DR
import { streamText, anthropic, runWithEndUser } from "@omg-dev/ai"
import { createAuthMiddleware } from "@omg-dev/auth"
const verify = createAuthMiddleware("vibes")
Bun.serve({
port: 3000,
hostname: "0.0.0.0",
async fetch(req) {
const auth = await verify(req)
return runWithEndUser(auth?.userId, () => handler(req))
},
})
async function handler(req: Request) {
if (new URL(req.url).pathname !== "/api/chat") {
return new Response("not found", { status: 404 })
}
const { messages } = await req.json()
const result = streamText({
model: anthropic("claude-sonnet-4-6"),
messages,
})
return result.toTextStreamResponse()
}That's it. No apiKey, no baseURL, no per-call headers. Every LLM call
inside the runWithEndUser scope is attributed to the verified end-user
in your usage dashboard.
What you get
- Zero config.
anthropic("claude-sonnet-4-6")andopenai("gpt-...")point at the platform proxy automatically. Your code never sees a key. - Streaming, tools, structured output. Everything from the AI SDK (
aipackage) is re-exported from@omg-dev/ai—streamText,generateText,streamObject,generateObject,tool,stepCountIs, plus the types. - Automatic end-user attribution when paired with
@omg-dev/auth— the cost dashboard breaks down by app and by end-user without any per-call bookkeeping.
Available models
@omg-dev/ai exposes the Anthropic and OpenAI provider shapes. The platform
proxy currently routes the following:
| Model id | Provider |
|---|---|
claude-sonnet-4-6 (default) | Anthropic |
claude-haiku-4-5-20251001 | Anthropic |
claude-opus-4-8 | Anthropic |
claude-opus-4-7 | Anthropic |
qwen/qwen3.6-plus | OpenRouter (via OpenAI shape) |
deepseek/deepseek-v4-pro | OpenRouter (via OpenAI shape) |
deepseek/deepseek-v4-flash | OpenRouter (via OpenAI shape) |
Use anthropic() for the Claude family and openai() for the
OpenAI-compatible models.
How attribution works
Two pieces, both invisible to most app code:
runWithEndUser(userId, fn)— wraps a request handler so any LLM call inside it stampsX-OMG-User: <userId>on the upstream request viaAsyncLocalStorage.@omg-dev/auth's middleware verifies the inbound JWT and gives you the end-user id. You hand it torunWithEndUser. Two lines.
The platform proxy reads that header, resolves it against the deploy's
app_id, and writes a row keyed by (owner, app, end-user, model) into
usage_logs. The Convex getAppUsage(appId) action returns a per-end-user
breakdown for your dashboard.
If auth?.userId is missing (request is unauthenticated), runWithEndUser
falls through and the call is logged as anonymous (user_id = ""). It still
counts against your owner-level total — you pay for it either way.
Escape hatch: explicit headers
If you want to attribute a call to something other than the auth subject — a
bot identity, a cron job, a per-conversation tag — pass headers directly:
const result = streamText({
model: anthropic("claude-haiku-4-5-20251001"),
messages,
headers: { "X-OMG-User": "cron-summarizer" },
})Explicit headers always win over the ALS context.
Tools / agents
Standard AI SDK shape — tool({ description, inputSchema, execute }),
multi-step with stopWhen: stepCountIs(N). The provider, ALS attribution,
and proxy plumbing are unchanged:
import { generateText, anthropic, tool, stepCountIs } from "@omg-dev/ai"
import { z } from "zod"
const result = await generateText({
model: anthropic("claude-sonnet-4-6"),
stopWhen: stepCountIs(5),
tools: {
getWeather: tool({
description: "Get weather for a city",
inputSchema: z.object({ city: z.string() }),
execute: async ({ city }) => ({ city, temperatureF: 72 }),
}),
},
messages,
})Each step's tool calls and final response all attribute to the same end-user.
Reading usage
Owner-level (everything you've spent across all apps):
import { useAction } from "convex/react"
import { api } from "../convex/_generated/api"
const myUsage = useAction(api.usage.getMyUsage)
// → { aggregates: [{ model, totalInputTokens, totalCostUsd, ... }], totalCostUsd }Per-app, broken down by end-user:
const appUsage = useAction(api.usage.getAppUsage)
const r = await appUsage({ appId: "<app-id>" })
// → { breakdown: [{ userId, model, totalCostUsd, requestCount, ... }], totalCostUsd }The appId for a deploy is on the deploy response (GET /v1/deploys/{slug})
or in the deploy create result.
Deploy classification
A project that depends on ai or any @ai-sdk/* (which @omg-dev/ai
transitively pulls in) is automatically deployed to a runtime VM, not the
static-asset path — the in-VM proxy is only reachable from a running sandbox.
You don't have to opt in; the deploy classifier checks package.json for AI
deps and routes accordingly.
If your app is otherwise a pure SPA and you only need AI on a server route, that one server route forces the runtime path for the whole deploy. Keep the function thin and the static SPA renders fast.