OpenAI
Trace OpenAI chat, Responses API, embedding, and stream calls from TypeScript or Python.
The integration records OpenAI calls without changes to the OpenAI calls. Make a project at telemetry.dev. Copy an API key from the project setup page. Keys use the td_live_... format.
What it captures
The integration records:
- 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 usage when OpenAI supplies these values
- Text, image, and audio token counts from OpenAI usage details, as
gen_ai.usage.{text,image,audio}.{input_tokens,output_tokens,cache_read.input_tokens} - Tool-call deltas and arguments from chat streams
- Image, speech, transcription, and translation calls, and batch create, retrieve, and cancel calls
- Video create and retrieve calls and Realtime WebSocket responses in TypeScript only
- The provider from the client:
azure.ai.openaifor Azure OpenAI, a matching provider for known OpenAI-compatible hosts, andopenaifor other calls.
Embedding vectors are not part of the span output. Image, audio, and video bytes are never captured, including binary fields nested in Responses API input and output.
Install
npm i @telemetry-dev/sdk @telemetry-dev/openai openaipip install telemetry-dev-openaiTypeScript
| Requirement | Version |
|---|---|
@telemetry-dev/sdk peer |
^0.1.0 |
openai peer |
>=6 <8 |
Node.js from @telemetry-dev/sdk |
>=20.19.0 |
Python
| Requirement | Version |
|---|---|
| Python | >=3.10 |
telemetry-dev |
>=0.2.6 |
openai |
>=2,<3 |
Quickstart
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 os
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=os.getenv("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()Global instrumentation
Use global instrumentation when client wrappers are not practical:
import OpenAI from "openai";
import { flush, init, shutdown } from "@telemetry-dev/sdk";
import {
instrumentOpenAI,
uninstrumentOpenAI,
} from "@telemetry-dev/openai";
init({ serviceName: "openai-app" });
instrumentOpenAI();
const client = new OpenAI();
try {
await client.embeddings.create({
model: "text-embedding-3-small",
input: "OpenTelemetry",
});
} finally {
uninstrumentOpenAI();
await flush();
await shutdown();
}
API
TypeScript
| Function | Purpose |
|---|---|
wrapOpenAI(client, options?) |
Instruments one client. More calls for the same client have no effect. |
instrumentOpenAI(options?) |
Instruments clients with the OpenAI resource prototypes. |
uninstrumentOpenAI() |
Restores the resource prototypes. Client wrappers stay active. |
wrapOpenAIRealtime(connection, options?) |
Instruments one Realtime WebSocket connection. Refer to Realtime. |
Python
| Function | Purpose |
|---|---|
wrap_openai(client, *, inject_stream_usage=False) |
Instruments one synchronous or asynchronous client. |
instrument_openai(*, inject_stream_usage=False) |
Instruments the OpenAI resource classes. |
uninstrument_openai() |
Restores the resource methods. |
Options
TypeScript
injectStreamUsage?boolean
Add stream_options.include_usage when the caller does not set it. Hide the final usage-only chunk.
booleanfalseinjectStreamUsage is opt-in. Azure OpenAI requests with data_sources reject stream_options.
Python
inject_stream_usage?bool
Add stream_options.include_usage and hide the final usage-only chunk.
boolFalseThe integration instruments chat create and parse, Responses create, retrieve, and parse, embeddings create, images, audio, and batches. It instruments synchronous and asynchronous resources.
Calls
| OpenAI call | Span |
|---|---|
chat.completions.create() |
chat {model} generation |
responses.create() |
chat {model} generation |
Streamed responses.retrieve() |
chat unknown generation |
embeddings.create() |
embeddings {model} embedding |
The integration keeps asResponse() and withResponse() on traced promises. These methods add the request ID and HTTP status to the span.
Media and batches
Image, audio, and video calls produce generation spans with operation generate_content. The span names differ by language:
| OpenAI call | TypeScript span | Python span |
|---|---|---|
images.generate(), images.edit(), image variations |
image {model} |
generate_content {model} |
audio.speech.create() |
speech {model} |
generate_content {model} |
audio.transcriptions.create() |
transcription {model} |
generate_content {model} |
audio.translations.create() |
translation {model} |
generate_content {model} |
videos.create() |
video {model} |
No span |
videos.retrieve() |
video unknown |
No span |
batches.create(), retrieve(), and cancel() |
openai.batch.{action} |
openai.batch.{action} |
Image spans keep the prompt and token usage. In TypeScript, non-streamed image spans also keep the image count and revised prompts. Speech spans keep the text input but not the returned audio. Transcription and translation spans keep the returned text and token usage.
Batch spans use the custom operations openai.batch.create, openai.batch.retrieve, and openai.batch.cancel, because OpenTelemetry defines no standard batch operation. Each span records the batch ID and status. Batch list pagination has no instrumentation in Python.
TypeScript traces video calls when the installed OpenAI SDK has client.videos. The Python integration does not instrument client.videos, so video calls from Python produce no spans.
Realtime
The TypeScript integration traces Realtime WebSocket connections through an explicit wrapper. instrumentOpenAI() does not cover them.
Install the OpenAI Realtime client’s optional ws dependency and its TypeScript types:
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 finishes on response.done, a transport error, or an early close, and it records output timing and modality usage.
To match responses that complete out of order, the wrapper adds a private correlation field to response.metadata and keeps the caller’s fields. OpenAI allows at most 16 metadata fields. When the caller already sends 16, the request goes out unchanged and the span fails immediately, because the wrapper cannot match its events.
traceTimeoutMs?number
Fail an unfinished span after this many milliseconds. Must be a positive finite number.
number300000maxInFlight?number
Maximum open spans for one connection. Past this limit, the oldest span fails and is removed. Must be a positive integer.
number100The Python integration does not wrap Realtime connections.
Streaming
TypeScript
Chat streams combine content, refusal, and tool-call argument deltas. Responses streams close on completed, incomplete, failed, or error events.
responses.stream({ response_id }) has instrumentation. Its span starts as chat unknown because the first response event supplies the model. A non-streamed responses.retrieve() call has no span.
The integration records an iteration error and returns the error to the caller. Consume or close each stream to finish its span.
Python
chat.completions.stream() and responses.stream() use the instrumented methods. The integration does not instrument the stream helpers directly.
responses.stream(response_id=...) has instrumentation. A retrieve() call without stream=True has no span.
The default request does not change. Set stream_options={"include_usage": True} or use inject_stream_usage=True to record streamed chat usage.
Instrument the client before access to with_raw_response or with_streaming_response. The OpenAI SDK stores bound methods at the first access to each namespace.
Speech spans with a streamed or downloaded response cover the request, not the later byte reads. A transcription stream span stays open until you consume the stream.
Provider detection
AzureOpenAI and AsyncAzureOpenAI calls use provider azure.ai.openai.
A client with an OpenAI-compatible base URL on one of these domains, or a subdomain of one, uses the matching provider:
| Base URL domain | Provider |
|---|---|
openrouter.ai |
openrouter |
groq.com |
groq |
x.ai |
x_ai |
deepseek.com |
deepseek |
together.xyz |
together_ai |
fireworks.ai |
fireworks_ai |
Other clients use openai.
Flush
TypeScript: For a short script, call await flush() and await shutdown() before exit. In serverless code, call await flush() before return. You can also pass the work to waitUntil.
Python: For a short script, call telemetry_dev.flush() and telemetry_dev.shutdown() before exit. In serverless code, call flush() before the runtime freezes.
Open the trace explorer to examine the spans. Refer to the quickstart for API-key setup.