Skip to content

What is recorded

How a wrapped client becomes runs, generations, tool steps and sessions.

On this page

Capture is explicit. wrap replaces the methods a provider client exposes, so the call you already make is recorded on its way through: the parameters name the model call, and the result is read as your code receives it. Streams are assembled chunk by chunk as you iterate them.

Which requests become spans

Method on a wrapped clientRecorded as
chat.completions.create, .parse, .streamgeneration span, chat.completions
responses.create, .parse, .streamgeneration span, responses
responses.retrieve, .cancelgeneration span, responses
embeddings.createembedding span
messages.create, messages.streamgeneration span, messages
beta.messages.create, beta.messages.streamgeneration span, messages

The client's own helpers work as they do unwrapped: .withResponse() and .asResponse() are still there, messages.stream().on("text", ...) and stream.finalText() are usable, and a stream consumed through finalMessage(), finalResponse() or done() is recorded the same as one you iterate.

gen_ai.system comes from the client's own base URL: openai or anthropic on the official hosts; a known name for Azure OpenAI, Groq, Mistral, Together, OpenRouter, DeepSeek, xAI, Perplexity, Fireworks, Cerebras, Gemini's OpenAI-compatible endpoint and Ollama; and the hostname otherwise. Where the system is unknown, pricing follows the model name.

Recorded per call: the model and request settings, reported token usage (including cached, cache-creation and reasoning tokens), finish reasons, tool call counts, time to first token and tokens per second for streams, the HTTP status, and the provider error type — rate_limit, timeout, auth, invalid_request, provider_error, incomplete_stream or incomplete_response. A provider client's own retries happen inside the call you wrapped, so a retried request is one span carrying its final outcome — a rate limit it recovered from is not recorded at all. Lower the client's maxRetries when you need those failures to surface.

Not captured

  • Calls through a client you did not wrap, or through the original reference when wrap was not assigned back.
  • Non-JSON request bodies, such as audio uploads.
  • Methods outside the table above. Record those yourself with a manual generation span.

How runs are drawn

One user turn is one run, without naming it. A model call made with no run in progress starts one; requests that continue the same conversation's agent loop join it; the run ends when a response asks for no more tools, when the next user turn arrives, after five minutes idle, or when the process flushes. Embeddings calls and other unsignalled calls are runs of their own.

observe(fn) and trace(name, fn) still work and take precedence when you want to name the run yourself; an observed function called inside a run becomes a step of that run rather than a second run.

How sessions are chosen

Every wrapped call is read for the conversation it belongs to, so sessions need no setup:

  1. The OpenAI Responses conversation id, then previous_response_id.
  2. Otherwise a fingerprint of the conversation's system prompt and first user message, which every later turn resends.

Derived ids look like auto- followed by 32 hex characters and are identical across processes and restarts, so the same conversation lands in one session even behind a load balancer.

The end user comes from the OpenAI user field or Anthropic metadata.user_id.

setSession() and setUser() — or observe(fn, { sessionId, user }) — take precedence over all of the above, for the cases below where the request does not carry what is needed.

Interpolated timestamps and UUIDs are masked before the fingerprint is taken, so a system prompt carrying the current time or a request id still yields one session.

A conversation that keeps its history on the provider's side and resends neither its opening nor a conversation id cannot be fingerprinted at all. Call setSession() for those.

How tool steps are recorded

Tool results carried by a request are paired with the tool calls of the previous response, so a tool the model asked for becomes a tool span with its name, arguments and output — without you instrumenting anything.

The duration of an inferred step is the gap between the two model calls, not the execution: it starts when the previous response finished and ends when the next request began, so anything else your code did in between is inside it.

For the real timing, wrap the execution with tool() where your code already runs it, and pass the provider's call id:

Time the tool where you run itTypeScript
await tool("search docs", () => search(args), {
  input: args,
  attributes: { "tool.call_id": call.id },
})

The explicit span then replaces the inferred one, keeping its real timing and retry events. A matching name alone cannot identify one tool execution reliably, which is why the call id matters.

A tool your agent runs on its own — one the model never asked for — has no provider traffic to infer from, so tool() is the only way it appears.

Streams

The SDK reads its own copy of the stream while your code reads the response, so a stream returned out of a trace() callback and consumed later is still recorded. The run's end waits for the stream, bounded by two minutes, and flush() waits for those ends too.

  • A stream your code neither reads nor cancels is recorded as cancelled once that bound passes.
  • Cancelling through an AbortSignal records a cancelled span.
  • For OpenAI chat streams, stream_options.include_usage: true is added when you have not set the option, so usage is reported. An explicit false stays false.
  • Assembled output is bounded; beyond it the span is marked truncated and keeps the full byte count.

Prompt identity

The system prompt each call carries — chat system/developer messages, Responses instructions, Anthropic system — identifies a prompt and one of its versions. The version hashes the whole text after normalisation; the prompt hashes the opening line, which edits below it leave alone.

Only those two hashes and the opening line travel on the span. Turning body capture off therefore still yields prompts, versions, usage and cost — just no text to show or diff.

Only what you wrapped

All of the above applies to calls made through a wrapped client. A call through an unwrapped client is not recorded at all — there is no global hook that could see it. That is the trade: coverage is a decision you make in your own source, where it is visible in review, rather than a property of how the process started.