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 client | Recorded as |
|---|---|
chat.completions.create, .parse, .stream | generation span, chat.completions |
responses.create, .parse, .stream | generation span, responses |
responses.retrieve, .cancel | generation span, responses |
embeddings.create | embedding span |
messages.create, messages.stream | generation span, messages |
beta.messages.create, beta.messages.stream | generation 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
wrapwas 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:
- The OpenAI Responses
conversationid, thenprevious_response_id. - 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:
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
AbortSignalrecords a cancelled span. - For OpenAI chat streams,
stream_options.include_usage: trueis added when you have not set the option, so usage is reported. An explicitfalsestays 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.