Automatic capture
How capture at the fetch boundary turns provider traffic into runs, generations, tool steps and sessions.
On this page
Capture happens at the HTTP boundary. The SDK observes the runtime's global
fetch, so any client or framework that uses it is recorded without being
wrapped. The request body names the model call; the response is read from a
clone while your code consumes the original.
Which requests become spans
| Request | Recorded as |
|---|---|
POST …/chat/completions | generation span, chat.completions |
POST …/responses, GET …/responses/{id}, POST …/responses/{id}/cancel | generation span, responses; polls complete a background response |
POST …/embeddings | embedding span |
POST …/messages on api.anthropic.com, or with an anthropic-version header | generation span, messages |
gen_ai.system is 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 retry of the same request within a
minute of a failure is recorded as attempt 2, 3 and so on.
Not captured
- Clients constructed before the SDK loaded; they kept the original
fetch. - Clients given their own
fetchoption. - Non-JSON request bodies, such as audio uploads.
- Endpoints outside the table above. Record those yourself with a manual generation span.
How runs are drawn
Inside an inbound HTTP request handled by http.Server or https.Server —
Express, Fastify, NestJS, Next.js — every model call belongs to one run named
METHOD /path. The run is sent only when a model call or an explicit span
actually happened, ends when the response finishes, and is marked failed on a
5xx status.
Outside a request — scripts, queues, workers — a model call starts a run. If the response asks for tools, the run stays open for the tool step and the follow-up call carrying the tool results. It ends on a response without tool calls, a failure, a new user message, ten idle minutes, or process exit.
An explicit trace() takes precedence over both.
Automatic runs also fill their transcript with the current user message from the
first provider call and the latest textual model answer; tool arguments and
reasoning stay on the generation span. They carry
metadata["tracehatch.capture"] = "auto".
How sessions are chosen
In order:
- The
x-tracehatch-session-idandx-tracehatch-user-idrequest headers. setSession()/setUser()in your code.- The OpenAI Responses
conversationid orprevious_response_id. - A fingerprint of the conversation's system prompt and first user message, which every later turn resends.
The OpenAI user field and Anthropic metadata.user_id name the end user.
Derived session ids are stable across processes, restarts and the gateway, so
the same conversation lands in one session even behind a load balancer.
How tool steps are found
Tool calls in a response are paired with the tool results in the next request —
chat tool messages, Responses function_call_output items, Anthropic
tool_result blocks — and recorded as tool spans with the call id, name,
arguments, output, error flag and the time between the two calls.
If you also time that tool with tool(), pass the
provider's call id so the two are recognised as one execution:
await tool("search docs", () => search(args), {
attributes: { "tool.call_id": call.id },
})The explicit span then wins, keeping its real timing and retry events. A matching name alone cannot identify one tool execution reliably.
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.
The same rules without the SDK
The provider gateway applies all of the above to traffic
it proxies, including session derivation and tool pairing, because it parses the
same provider shapes. The log endpoint derives sessions from
chat-style messages and creates one tool span per entry you send, but it
cannot infer what it never sees.