Skip to content
telemetry.dev
Esc
↑↓navigate↵open⌘Jpreview
On this page

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 openai
pip install telemetry-dev-openai

The 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

PropType
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.

Typeboolean
Defaultfalse
inject_stream_usage?bool

Python name for the same option.

Typebool
DefaultFalse

Stream 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.

PropType
traceTimeoutMs?number

Fail a span that hasn't finished after this many milliseconds. Must be a positive finite number.

Typenumber
Default300000
maxInFlight?number

Maximum open spans per connection. Past the limit, the oldest span fails and is removed. Must be a positive integer.

Typenumber
Default100

Provider 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.

Last updated on October 18, 2026

Was this page helpful?