Skip to main content
init is the wizard sub-command in @voightxyz/sdk for apps that already call an LLM in production. It detects which SDK family you use — direct provider wrappers (@voightxyz/openai, @voightxyz/anthropic) or the Vercel AI SDK — and scaffolds the matching path so you don’t have to copy snippets from the docs. Run it from the root of your app:
cd your-app
npx -y @voightxyz/sdk init
That’s the entire setup.

What the wizard does

The flow is single-pass — no daemon, no background services, no follow-up commands:
  1. Detects providers. Reads package.json (both dependencies and devDependencies) for three keys: openai, @anthropic-ai/sdk, and ai (Vercel AI SDK). If none are found, the wizard aborts cleanly and explains what to install.
  2. Picks the path. If ai is present, the wizard takes the Vercel AI SDK path — it writes a root instrumentation.ts registering @voightxyz/vercel-ai with @vercel/otel. Otherwise it takes the direct-wrapper path — it writes src/lib/voight.ts with the wrapped OpenAI / Anthropic clients. Mixed projects (both ai and a direct provider) take the Vercel path with an explicit message, since the OTel exporter covers every underlying provider.
  3. Detects package manager. Looks for pnpm-lock.yamlyarn.lockbun.lockb → falls back to npm. The install command printed at the end matches what you actually use.
  4. Detects framework. Looks for next in deps. If present, the usage snippet at the end uses Next.js Route Handler shape (export async function POST); otherwise Express style (app.post('/api/chat', ...)).
  5. Prompts for privacy level. Three options:
    • Minimal — metadata only (tokens, latency, cost). No prompts or responses captured.
    • Standard ★ — full content with 12-pattern PII scrubbing. Required for customer-facing apps where end-user PII flows through prompts.
    • Full — raw, no scrubbing. Debugging only.
  6. Prompts for the Voight API key (vk_...). Validates it against api.voight.xyz before writing anything — typo’d keys get caught here, not in production.
  7. Prompts for an agent name. Defaults to your package.json name; press enter to accept or type a custom one.
  8. Writes the target module:
    • Direct-wrapper pathsrc/lib/voight.ts (or lib/voight.ts if your project doesn’t have src/). Contains the wrapped clients for the providers detected, plus a header comment explaining how to swap if your provider keys come from AWS Secrets Manager / Vault / Doppler instead of env.
    • Vercel AI SDK pathinstrumentation.ts at the project root (or src/instrumentation.ts if your project uses src/). Registers @vercel/otel with VoightExporter as the trace exporter and includes a header comment explaining how to activate per-user attribution via experimental_telemetry.metadata.
  9. Appends VOIGHT_KEY to .env.local. Creates the file if absent. Existing VOIGHT_KEY lines are left alone.
  10. Prints the install command + usage snippet. The install command uses your package manager and is tailored to the path:
    • Direct wrappers → pnpm add @voightxyz/openai @voightxyz/anthropic
    • Vercel AI SDK → pnpm add @voightxyz/vercel-ai @vercel/otel

What it does NOT do

  • Never touches ~/.claude, ~/.cursor, ~/.codex or any other path outside your project’s cwd. This is the key difference from setup (which wires IDE hooks globally).
  • Never asks for or stores your provider keys (OPENAI_API_KEY, ANTHROPIC_API_KEY). Those belong to your app and stay there. The Voight wrapper instruments the requests but never reads the auth header.
  • Never runs npm install for you. We print the command so you can run it in your terminal — that way you stay in control of your lockfile (and we don’t accidentally pin a version you didn’t approve).
  • Never overrides an existing src/lib/voight.ts or instrumentation.ts. If the target file already exists, the wizard aborts and asks you to move it aside (relevant if you already wire OpenTelemetry yourself — see the MultiSpanProcessor pairing docs).

Example session

$ cd ~/projects/customer-support-saas
$ npx -y @voightxyz/sdk init

  Voight · init → /Users/jane/projects/customer-support-saas

  ✓ Detected providers: openai (^4.79.2) + @anthropic-ai/sdk (^0.96.0)
  ✓ Detected package manager: pnpm
  ✓ Detected framework: Next.js

  Privacy level for this project?

    1) Minimal · metadata only (tokens, latency, cost)
    2) Standard ★ Required for customer-facing apps — PII scrubbing on
    3) Full · raw, no scrubbing (debugging only)

  > 2

  ✓ Privacy: standard

  Now we need your Voight API key:
    1. Open  → https://voight.xyz/dashboard
    2. Sign in (Google / X / wallet)
    3. Settings → Generate key → copy the vk_… secret

  Paste it here: vk_aBcD…

  ✓ Validating key with api.voight.xyz…
  ✓ Key valid

  Agent name (defaults to "customer-support-saas")? production-chat-api

  ✓ Agent: production-chat-api

  ✓ Wrote src/lib/voight.ts
  ✓ Created .env.local with VOIGHT_KEY

  Use it in your handlers:

    // app/api/chat/route.ts
    import { openai, withTrace } from '@/lib/voight'

    export async function POST(req: Request) {
      return withTrace(
        async () => {
          const r = await openai.chat.completions.create({ ... })
          return Response.json({ reply: r.choices[0].message })
        },
        {
          routeTag: 'POST /api/chat',
          tags: { userId: 'user_123', plan: 'pro' },
        },
      )
    }

  One more step — install the wrappers:
    pnpm add @voightxyz/openai @voightxyz/anthropic

  Then open https://voight.xyz/dashboard/ai-apps

Example session — Vercel AI SDK path

When the wizard detects ai in package.json, it takes the Vercel path. The prompts are identical (privacy + key + agent name); only the written file and the install command differ:
$ cd ~/projects/ai-chatbot
$ npx -y @voightxyz/sdk init

  Voight · init → /Users/jane/projects/ai-chatbot

  ✓ Detected providers: ai (^6.0.0) + openai (^4.79.2)
    (using the Vercel AI SDK path — covers both)
  ✓ Detected package manager: pnpm
  ✓ Detected framework: Next.js

  ✓ Privacy: standard
  ✓ Key valid
  ✓ Agent: production-chat-api

  ✓ Wrote instrumentation.ts
  ✓ Created .env.local with VOIGHT_KEY

  Activate telemetry on your existing calls:

    // app/api/chat/route.ts
    import { openai } from '@ai-sdk/openai'
    import { streamText } from 'ai'

    export async function POST(req: Request) {
      const result = streamText({
        model: openai('gpt-4o-mini'),
        prompt: (await req.json()).prompt,
        experimental_telemetry: {
          isEnabled: true,
          metadata: { userId: 'user_123', plan: 'pro' },
        },
      })
      return result.toAIStreamResponse()
    }

  Per-user attribution: anything you put in `metadata` shows up
  under `metadata.tags.*` in Voight — `userId` powers the Users sub-tab.

  One more step — install the Voight exporter:
    pnpm add @voightxyz/vercel-ai @vercel/otel

  Then open https://voight.xyz/dashboard/ai-apps

Generated file

Direct-wrapper path

src/lib/voight.ts after a dual-provider install with agent: 'production-chat-api', privacy: 'standard':
// ──────────────────────────────────────────────────────────────────
// Voight observability — generated by `npx @voightxyz/sdk init`
//
// Both wrapped clients use the standard `new OpenAI()` /
// `new Anthropic()` constructors, which read OPENAI_API_KEY and
// ANTHROPIC_API_KEY from process.env. This is the convention for
// most apps (the values typically live in .env / .env.local).
//
// If your provider keys come from elsewhere (AWS Secrets Manager,
// HashiCorp Vault, Doppler, a database, etc.), adjust the
// constructor calls below to pass the key explicitly, e.g.:
//
//   const openai = wrapOpenAI(
//     new OpenAI({ apiKey: await loadFromVault('openai') }),
//     { agent: 'production-chat-api', privacy: 'standard' }
//   )
//
// The Voight wrapper never sees the provider key — it wraps
// whatever client you construct.
// ──────────────────────────────────────────────────────────────────

import OpenAI from 'openai'
import Anthropic from '@anthropic-ai/sdk'
import { wrapOpenAI } from '@voightxyz/openai'
import { wrapAnthropic } from '@voightxyz/anthropic'

export const openai = wrapOpenAI(new OpenAI(), {
  agent: 'production-chat-api',
  privacy: 'standard',
})

export const anthropic = wrapAnthropic(new Anthropic(), {
  agent: 'production-chat-api',
  privacy: 'standard',
})

// Re-exports — import everything from one place.
export { withTrace, log } from '@voightxyz/openai'
If only one provider is detected, only that provider’s block is emitted. Single import path either way: import { openai, anthropic, withTrace, log } from '@/lib/voight'.

Vercel AI SDK path

instrumentation.ts after a Vercel install with agent: 'production-chat-api', privacy: 'standard':
// ──────────────────────────────────────────────────────────────────
// Voight observability — generated by `npx @voightxyz/sdk init`
//
// Registers the Voight OpenTelemetry exporter for every Vercel
// AI SDK call (streamText / generateText / streamObject /
// generateObject). To opt a call into observability, flip
// `experimental_telemetry: { isEnabled: true }` on it.
//
// For per-user attribution, pass a metadata bag:
//
//   experimental_telemetry: {
//     isEnabled: true,
//     metadata: { userId: session.user.id, plan: session.user.plan },
//   }
//
// The Voight exporter lifts `ai.telemetry.metadata.*` attributes
// onto `metadata.tags.*` so the dashboard Users sub-tab + per-tag
// filter pills populate automatically.
// ──────────────────────────────────────────────────────────────────

import { registerOTel } from '@vercel/otel'
import { VoightExporter } from '@voightxyz/vercel-ai'

export function register() {
  registerOTel({
    serviceName: 'production-chat-api',
    traceExporter: new VoightExporter({
      agent: 'production-chat-api',
      privacy: 'standard',
    }),
  })
}
The file lives at instrumentation.ts in the project root (Next.js convention) — or src/instrumentation.ts if your project already uses src/. See the @voightxyz/vercel-ai docs for the full capture matrix.

Non-interactive flags

If you’re scripting the install (CI, Dockerfile, dotfile bootstrap), pass everything as flags:
npx -y @voightxyz/sdk init \
  --privacy=standard \
  --key=vk_aBcD... \
  --agent=production-chat-api
Reads VOIGHT_KEY and VOIGHT_PRIVACY from env too if you’d rather not pass them inline. Refuses to write anything if the key validation fails — same safety as the interactive flow.

Manual install (skip the wizard)

The wizard is convenience, not requirement. If you’d rather wire things up by hand, the OpenAI SDK and Anthropic SDK pages walk through the same install — npm install, wrap your client, set VOIGHT_KEY. Three commands, no wizard.

Troubleshooting

The wizard only operates on projects that already use one of those SDKs. If you’re in a monorepo, cd into the app folder first (e.g. cd apps/web). If your project doesn’t use either yet, install the provider SDK first:
npm install openai
npm install @anthropic-ai/sdk
Then re-run the wizard.
The key you pasted isn’t recognised by api.voight.xyz. Common causes:
  • Typo in the paste — Voight keys start with vk_ and are about 50 characters
  • The key was revoked — check dashboard/settings and generate a new one
  • Wrong key entirely (e.g. you pasted an OpenAI key by mistake)
Fix and re-run — no files were written.
The wizard refuses to overwrite an existing file. Either move it aside (mv src/lib/voight.ts src/lib/voight.ts.bak) or delete it, then re-run.
Not an error — the wizard leaves your existing VOIGHT_KEY alone instead of overwriting it. If you genuinely want to rotate the key, edit .env.local manually.
The wizard picks the package manager whose lockfile it sees first (pnpm → yarn → bun → npm). If you have stale lockfiles from a previous tool, delete them. The wizard never runs the install itself — you can always run pnpm add ... / yarn add ... / bun add ... regardless of what the wizard printed.

Safety guarantees

init is implemented in src/init.ts of @voightxyz/sdk with zero edits to the existing setup, hook, or any other module. It cannot affect users running setup or hook because:
  • The CLI dispatches by sub-command name (setup / init / hook) — the setup branch runs first and returns before reaching init.
  • init.ts has no imports from setup.ts. There’s no shared mutable state, no shared global config.
  • init writes only inside process.cwd() — no homedir() calls, no ~/.config paths. The shape is enforced by the test suite (256 tests, including a structural assertion that init never produces a write path outside cwd).

Next