@voightxyz/openai
v0.1.8
Published
Drop-in observability for the OpenAI Node SDK. Wrap your client once and every chat.completions.create and responses.create call lands in Voight: prompts, tokens, cache reads, tool calls, latency, errors.
Maintainers
Readme
@voightxyz/openai
Voight observability for the OpenAI SDK. Wrap your OpenAI client and capture every model call — prompts, tokens, cache reads, tool calls, costs, latency, errors — surfaced live in the Voight dashboard.
Same backend and dashboard as @voightxyz/anthropic. Drop in whichever provider your app uses; events from both land side-by-side under the same agent.
Quick setup with the wizard
If your app already imports openai, the lowest-friction install is the wizard from the main SDK:
cd your-app
npx -y @voightxyz/sdk initIt detects openai (and @anthropic-ai/sdk if present) in your package.json, prompts for a privacy level + Voight key + agent name, and writes a ready-to-import src/lib/voight.ts with the wrapped client. 30 seconds, zero copy-paste. Full walkthrough at docs.voight.xyz/ai-apps/wizard.
Continue below if you'd rather wire it manually.
Install
npm install openai @voightxyz/openaiQuick start
import OpenAI from 'openai'
import { wrapOpenAI } from '@voightxyz/openai'
const client = wrapOpenAI(new OpenAI(), {
voightApiKey: process.env.VOIGHT_KEY,
agent: 'my-prod-agent',
})
const response = await client.chat.completions.create({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: 'Hello' }],
})That's it — every call is captured automatically. Visit your Voight dashboard to see them in real time.
Tracing & per-user attribution
For production apps where you want to group every LLM call inside one request into one trace, and attribute cost per end-user with one line of code, wrap each request boundary with withTrace:
import OpenAI from 'openai'
import { wrapOpenAI, withTrace, log } from '@voightxyz/openai'
const openai = wrapOpenAI(new OpenAI(), {
agent: 'production-chat-api',
privacy: 'standard',
})
app.post('/api/chat', async (req, res) => {
await withTrace(
async () => {
log('chat request received')
const reply = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: req.body.prompt }],
})
res.json({ reply })
},
{
routeTag: 'POST /api/chat',
tags: { userId: req.user.id, plan: req.user.plan },
},
)
})Every wrapped LLM call inside the withTrace block automatically inherits the routeTag and tags. The tags.userId field drives per-user spend tracking — the dashboard's Users sub-tab populates with per-customer cost as soon as your first request lands.
The same withTrace exported here also works in @voightxyz/anthropic — they share an async-context store, so an app calling both providers inside one request gets one trace, not two.
What's captured
| Signal | Where it lands |
|---|---|
| Model id (with version suffix) | model |
| Prompt messages | input.messages (or input.input for Responses API) |
| Response text | metadata.responseText |
| Token counts (input / output / total) | metadata.tokens |
| Cache reads (prompt_tokens_details.cached_tokens) | metadata.tokens.cache_read |
| Reasoning tokens (o1, o3 — Responses API only) | metadata.tokens.reasoning |
| Tool / function calls | metadata.toolCalls + toolExecuted |
| Streaming flag | metadata.streaming |
| API surface used (chat.completions vs responses) | metadata.api |
| Trace grouping (auto UUID or explicit) | metadata.sessionId |
| Finish reason / response status | metadata.finishReason |
| Latency (ms) | durationMs |
| Errors (re-thrown to the caller) | errorMessage + outcome: 'failed' |
Supported endpoints
client.chat.completions.create— legacy chat completions (non-streaming + streaming)client.responses.create— Responses API (non-streaming + streaming, function calls, reasoning models)
The wrapper passes everything else through untouched. Embeddings, images, audio, and the Azure OpenAI client are on the 0.2.0 roadmap.
Options
| Option | Type | Default | Purpose |
| --- | --- | --- | --- |
| voightApiKey | string | process.env.VOIGHT_KEY | Your Voight key from the dashboard |
| agent | string | process.env.VOIGHT_AGENT → HOSTNAME → 'unknown-agent' | Stable identifier surfaced in the dashboard |
| apiBase | string | https://api.voight.xyz | Override for self-hosted deployments |
| privacy | 'minimal' \| 'standard' \| 'full' | 'standard' | Capture aggressiveness |
| sessionId | string | auto UUID v4 | Trace grouping. Stable across calls of one wrapper instance |
| enabled | boolean | true | Kill switch — returns the original client untouched |
| otel | boolean | false | Emit captured calls as OpenTelemetry spans alongside the direct ingest. See OpenTelemetry side-channel below. |
OpenTelemetry side-channel
By default the wrapper POSTs each captured call directly to api.voight.xyz. Set otel: true to additionally emit each call as an OpenTelemetry span — useful when the host process already runs an OTel pipeline (Langfuse, Phoenix, Datadog, Sentry, or @voightxyz/vercel-ai) and you want Voight events to appear there too.
const client = wrapOpenAI(new OpenAI(), { agent: 'my-app', otel: true })Each span is named voight.openai.chat (or voight.openai.responses) and carries the standard gen_ai.* semantic-convention attributes (gen_ai.system, gen_ai.request.model, gen_ai.usage.input_tokens, gen_ai.usage.output_tokens, gen_ai.usage.cache_read_input_tokens, gen_ai.response.finish_reasons) plus the parallel Vercel-style ai.* namespace for cross-tool compatibility.
The direct ingest is unchanged — otel: true is purely additive. Default otel: false ships byte-identical behaviour to 0.1.6.
Dedup marker
Every emitted span carries voight.source: 'wrapper'. If you also use @voightxyz/vercel-ai ≥ 0.1.1 in the same process, that exporter recognises the marker and skips wrapper-emitted spans — no duplicate events in your dashboard.
Optional peer dependency
@opentelemetry/api is now an optional peer dependency. If you never set otel: true, nothing changes — no extra install, no runtime cost. If you set otel: true but the package isn't installed, the wrapper logs a single warning and falls back to direct ingest only.
Privacy
Three levels apply to prompts, response text, and tool-call arguments. The function name in toolExecuted always survives as a tag (not user content).
| Level | Prompts | Response text | Tool arguments | Tokens / timing / model |
| --- | --- | --- | --- | --- |
| minimal | dropped | dropped | dropped | kept |
| standard (default) | scrubbed | scrubbed | scrubbed | kept |
| full | verbatim | verbatim | verbatim | kept |
Standard scrubs 12 patterns: PEM private keys, JWTs, Anthropic / OpenAI / Stripe live / GitHub / AWS / Slack / Voight API keys, emails, E.164 phones, and Luhn-validated credit cards.
See CHANGELOG.md for release notes.
License
Apache 2.0
Voight is the observability and debugging infrastructure for autonomous systems, built by Galaxyhub Labs Inc. Company, team and traction: https://voight.xyz/company
