OpenAI
Trace OpenAI chat, Responses API, embedding, media, batch, and stream calls from TypeScript or Python.
Wrap your OpenAI client, and each call you make with it becomes a span in telemetry.dev. Your calls and their return values don’t change.
What you get
- Chat messages, Responses API input, system instructions, model parameters, and embedding input
- Response IDs, response models, finish reasons, output messages, errors, and time to first chunk
- Input, output, total, cache-read, and reasoning token counts when OpenAI returns them, plus text, image, and audio token counts as
gen_ai.usage.{text,image,audio}.{input_tokens,output_tokens,cache_read.input_tokens} - Tool-call arguments rebuilt from chat stream deltas
- Image, speech, transcription, translation, and batch calls
- Realtime WebSocket responses, in TypeScript only
The integration never records embedding vectors or image, audio, and video bytes, including binary fields nested in Responses API input and output.
Install
npm i @telemetry-dev/sdk @telemetry-dev/openai openaipip install telemetry-dev-openaiThe Python package also installs telemetry-dev and openai.
| Language | Requirements |
|---|---|
| TypeScript | Node.js >=20.19.0, @telemetry-dev/sdk ^0.1.8, openai >=6 <8 |
| Python | Python >=3.10, telemetry-dev >=0.2.9, openai >=2,<4 |
Trace a call
Set your telemetry.dev project API key and your OpenAI key. Copy the telemetry.dev key from the project setup page; it starts with td_live_.
export TELEMETRY_DEV_API_KEY=td_live_...
export OPENAI_API_KEY=sk-...
Call init(), wrap the client, and use it as usual:
import OpenAI from "openai";
import { flush, init, shutdown } from "@telemetry-dev/sdk";
import { wrapOpenAI } from "@telemetry-dev/openai";
init({ serviceName: "openai-app" });
const client = wrapOpenAI(new OpenAI());
try {
const completion = await client.chat.completions.create({
model: "gpt-4o-mini",
messages: [
{ role: "system", content: "You are a helpful bot" },
{ role: "user", content: "Tell me a joke about OpenTelemetry" },
],
max_completion_tokens: 200,
});
console.log(completion.choices[0]?.message.content ?? "(no content)");
} finally {
await flush();
await shutdown();
}import telemetry_dev
from openai import OpenAI
from telemetry_dev_openai import wrap_openai
telemetry_dev.init(service_name="openai-app")
client = wrap_openai(OpenAI())
try:
completion = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "You are a helpful bot"},
{"role": "user", "content": "Tell me a joke about OpenTelemetry"},
],
max_completion_tokens=200,
)
print(completion.choices[0].message.content or "(no content)")
finally:
telemetry_dev.flush()
telemetry_dev.shutdown()Run the script, then open the trace explorer. The trace has one chat gpt-4o-mini generation span with the messages, the reply, and token usage.
Without an API key, init() turns telemetry off and the wrapped client works as before.
Instrument all clients
To trace every client without wrapping each one, call the global function once at startup:
import OpenAI from "openai";
import { flush, init, shutdown } from "@telemetry-dev/sdk";
import { instrumentOpenAI } from "@telemetry-dev/openai";
init({ serviceName: "openai-app" });
instrumentOpenAI();
const client = new OpenAI();
await client.embeddings.create({
model: "text-embedding-3-small",
input: "OpenTelemetry",
});
await flush();
await shutdown();import telemetry_dev
from openai import OpenAI
from telemetry_dev_openai import instrument_openai
telemetry_dev.init(service_name="openai-app")
instrument_openai()
client = OpenAI()
client.embeddings.create(model="text-embedding-3-small", input="OpenTelemetry")
telemetry_dev.flush()
telemetry_dev.shutdown()In TypeScript, global instrumentation patches the ES module build of openai. A client created with require("openai") comes from the CommonJS build and isn’t covered, so wrap it with wrapOpenAI().
API
| TypeScript | Python | What it does |
|---|---|---|
wrapOpenAI(client, options?) |
wrap_openai(client, *, inject_stream_usage=False) |
Instruments one client and returns it. Wrapping the same client again has no effect. Python accepts synchronous and asynchronous clients. |
instrumentOpenAI(options?) |
instrument_openai(*, inject_stream_usage=False) |
Instruments every client through the OpenAI resource classes. |
uninstrumentOpenAI() |
uninstrument_openai() |
Removes global instrumentation. Clients you wrapped yourself stay instrumented. |
wrapOpenAIRealtime(connection, options?) |
None | Instruments one Realtime WebSocket connection. See Realtime. |
Options
injectStreamUsage?boolean
TypeScript name. Add stream_options.include_usage to streamed chat requests that don't set it, and hide the extra usage-only chunk from your code.
booleanfalseinject_stream_usage?bool
Python name for the same option.
boolFalseStream usage injection is off by default because some endpoints reject stream_options. Azure OpenAI requests with data_sources return a 400 error when it is present.
Traced calls
The integration traces synchronous and asynchronous calls.
| OpenAI call | Span name | Span type |
|---|---|---|
chat.completions.create() and parse() |
chat {model} |
generation |
responses.create() and parse() |
chat {model} |
generation |
responses.retrieve() with stream: true, and responses.stream({ response_id }) |
chat unknown |
generation |
embeddings.create() |
embeddings {model} |
embedding |
images.generate(), edit(), and createVariation() (create_variation() in Python) |
image {model} |
generation |
audio.speech.create() |
speech {model} |
generation |
audio.transcriptions.create() |
transcription {model} |
generation |
audio.translations.create() |
translation {model} |
generation |
batches.create(), retrieve(), and cancel() |
openai.batch.{create,retrieve,cancel} |
span |
chat.completions.stream() and responses.stream() for a new response go through create(), so they are traced too. A responses.retrieve() call without streaming has no span. A retrieved stream starts as chat unknown because the model name arrives with the first event.
Image, speech, transcription, and translation spans use the operation generate_content. Image spans keep the prompt and token usage. In TypeScript, non-streamed image spans also keep the image count and the revised prompts. Speech spans keep the input text but not the returned audio. Transcription and translation spans keep the returned text and token usage.
Batch spans use the operations openai.batch.create, openai.batch.retrieve, and openai.batch.cancel, because OpenTelemetry defines no standard batch operation. Each records the batch ID and status. Python doesn’t trace batch list pagination.
OpenAI shut down its Videos API on September 24, 2026. Python doesn’t trace client.videos. TypeScript still traces videos.create() (video {model}) and videos.retrieve() (video unknown) when the installed SDK has client.videos, for compatible endpoints.
In TypeScript, traced calls keep asResponse() and withResponse(). Calling either one also records the HTTP status code and the request ID on the span.
Streaming
A stream span stays open until your code reads the stream to the end, closes it, or the stream fails. A stream you never read never finishes its span. Errors during iteration are recorded on the span and still reach your code.
The integration sends requests unchanged. To record token usage for streamed chat, set stream_options: { include_usage: true } yourself or turn on injectStreamUsage. Responses API streams report usage without this.
Chat streams combine content, refusal text, and tool-call argument deltas. Responses streams end on a response.completed, response.incomplete, response.failed, or error event.
In Python, wrap or instrument the client before you first access with_raw_response or with_streaming_response. The OpenAI SDK binds those methods on first access, so later instrumentation doesn’t reach them.
A speech span for a streamed or downloaded response covers the request, not your later reads of the bytes. A transcription stream span stays open until you finish reading the stream.
Realtime
In TypeScript, wrap each Realtime WebSocket connection with wrapOpenAIRealtime(). instrumentOpenAI() doesn’t cover Realtime, and Python has no Realtime support.
The OpenAI Realtime WebSocket client needs the ws package:
npm i ws
npm i -D @types/ws
import { OpenAIRealtimeWS } from "openai/realtime/ws";
import { wrapOpenAIRealtime } from "@telemetry-dev/openai";
const realtime = wrapOpenAIRealtime(
new OpenAIRealtimeWS({ model: "gpt-realtime" }),
{ model: "gpt-realtime" },
);
Each response.create event starts one generation span. The span ends on response.done, a transport error, or the connection closing, and it records output timing and token usage by modality.
To match responses that finish out of order, the wrapper adds a private correlation field to response.metadata and keeps your fields. OpenAI allows at most 16 metadata fields. If you already send 16, the request goes out unchanged and its span fails at once, because the wrapper can’t match its events.
traceTimeoutMs?number
Fail a span that hasn't finished after this many milliseconds. Must be a positive finite number.
number300000maxInFlight?number
Maximum open spans per connection. Past the limit, the oldest span fails and is removed. Must be a positive integer.
number100Provider names
The span’s gen_ai.provider.name comes from the client:
| Client | Provider |
|---|---|
AzureOpenAI (and AsyncAzureOpenAI in Python) |
azure.ai.openai |
Base URL on openrouter.ai or a subdomain |
openrouter |
Base URL on groq.com or a subdomain |
groq |
Base URL on x.ai or a subdomain |
x_ai |
Base URL on deepseek.com or a subdomain |
deepseek |
Base URL on together.xyz or a subdomain |
together_ai |
Base URL on fireworks.ai or a subdomain |
fireworks_ai |
| Any other client | openai |
Captured content
Prompts and responses follow the core SDK’s captureInput, captureOutput, and mask settings (capture_input, capture_output, and mask in Python). With output capture off, response IDs, models, finish reasons, usage, timing, and errors are still recorded.
The capture settings don’t cover stop sequences, metadata and attributes you pass yourself, or exception messages and stack traces. Redact those separately if they can hold sensitive data. See privacy for server-side redaction.
Send telemetry before exit
In a short script, call flush() and then shutdown() before the process exits, as in the example above. In serverless code, call flush() before the handler returns. In TypeScript you can pass a waitUntil function to init() instead. See the TypeScript SDK and Python SDK guides for details.