Documentation Index
Fetch the complete documentation index at: https://docs.voight.xyz/llms.txt
Use this file to discover all available pages before exploring further.
@voightxyz/openai instruments the official OpenAI Node SDK. Wrap your client once and every chat.completions.create — non-streaming or streaming — lands in Voight with prompts, tokens, cache reads, tool calls, latency, and errors.
This is the same SDK-instrumentation model Sentry / Vercel AI / LangChain use. Same backend, same dashboard as library mode — events from both land side-by-side under the same agent.
Currently published as 0.1.0-beta.3 under both @latest and @beta tags. Stable 0.1.0 ships once the surface has run a week of real traffic without changes.
Install
npm install openai @voightxyz/openai
Requirements:
- Node.js 18+ (uses global
fetch)
openai SDK 4.0.0+
Quick 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 dashboard to see them.
Options
| Option | Type | Default | Purpose |
|---|
voightApiKey | string | env VOIGHT_KEY | Your Voight key from the dashboard |
agent | string | 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 |
enabled | boolean | true | Kill switch — when false, returns the original client untouched (zero overhead) |
A missing or empty API key is non-fatal: the wrapper prints a one-line warning and returns the original client. Production keeps running.
What’s captured
| Signal | Field on the event |
|---|
Model id (with version suffix, e.g. gpt-4o-mini-2024-07-18) | model |
| Prompt messages | input.messages |
| Response text | metadata.responseText |
| Token counts (input / output / total) | metadata.tokens |
Cache reads (prompt_tokens_details.cached_tokens) | metadata.tokens.cache_read |
| Tool / function calls (full array) | metadata.toolCalls |
| First tool’s name (audit-log compat) | toolExecuted |
| Streaming flag | metadata.streaming |
| Finish reason | metadata.finishReason |
| Latency in milliseconds | durationMs |
Errors (re-thrown to the caller, recorded with outcome: 'failed') | errorMessage, outcome |
| Capture level used | metadata.privacyLevel |
Streaming
Streaming works without setup. The wrapper auto-injects stream_options.include_usage: true so the final chunk carries the token count, and a per-index aggregator reassembles tool-call argument fragments.
const stream = await client.chat.completions.create({
model: 'gpt-4o-mini',
stream: true,
messages: [{ role: 'user', content: 'count to five' }],
})
for await (const chunk of stream) {
process.stdout.write(chunk.choices[0]?.delta?.content ?? '')
}
An explicit stream_options: { include_usage: false } from the caller is preserved — you opt out of token capture for streaming events but everything else is still captured.
const response = await client.chat.completions.create({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: "what's the weather in Tokyo?" }],
tools: [{
type: 'function',
function: {
name: 'get_weather',
description: 'Get weather for a city',
parameters: {
type: 'object',
properties: { location: { type: 'string' } },
required: ['location'],
},
},
}],
})
On the captured event:
toolExecuted: 'get_weather' — first tool’s name. Renders in the audit-log DETAIL column with the same shape as Bash/Edit hook events.
metadata.toolCalls: [{ id, name, arguments }] — full array. arguments is the raw JSON string the model produced (we don’t parse it — invalid JSON is a real failure mode you need to debug).
Streaming function calls work the same — fragment deltas across chunks are concatenated per tool index.
Privacy
Three levels apply to prompts, response text, and tool-call arguments. The function name in toolExecuted is treated as a tag (not user content) and survives all levels.
| Level | Prompt messages | Response text | Tool arguments | toolExecuted (name) |
|---|
minimal | dropped | dropped | dropped | kept |
standard | 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. Token counts, model ids, and timing are NEVER scrubbed — they’re numeric or tags, no PII risk.
See PII patterns for the full catalogue.
How it compares
| Use case | Reach for |
|---|
| Coding agent (Claude Code, Cursor) capturing your dev sessions | Hooks-based SDK |
| Autonomous TS/JS bot you wrote yourself emitting custom events | Library mode |
| Production app calling OpenAI in user-facing flows | This package |
| Production app calling Anthropic | @voightxyz/anthropic (next release) |
| Anything else (Python, Go, Rust) | HTTP API |
The packages coexist — wrap your OpenAI client AND call voight.log() for your own domain events under the same agent.
Source
What’s next
responses.create (Responses API)
- Embeddings, image generation, audio
- Azure OpenAI client
- Same shape for Anthropic (
@voightxyz/anthropic)
See the changelog for shipped releases.