omg/docs
SDK

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

.vibes/server.mjs
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") and openai("gpt-...") point at the platform proxy automatically. Your code never sees a key.
  • Streaming, tools, structured output. Everything from the AI SDK (ai package) is re-exported from @omg-dev/aistreamText, 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 idProvider
claude-sonnet-4-6 (default)Anthropic
claude-haiku-4-5-20251001Anthropic
claude-opus-4-8Anthropic
claude-opus-4-7Anthropic
qwen/qwen3.6-plusOpenRouter (via OpenAI shape)
deepseek/deepseek-v4-proOpenRouter (via OpenAI shape)
deepseek/deepseek-v4-flashOpenRouter (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:

  1. runWithEndUser(userId, fn) — wraps a request handler so any LLM call inside it stamps X-OMG-User: <userId> on the upstream request via AsyncLocalStorage.
  2. @omg-dev/auth's middleware verifies the inbound JWT and gives you the end-user id. You hand it to runWithEndUser. 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.