AI platforms
Claude API and Claude SDK
Send the OpenTelemetry spans that Anthropic's Claude API and Claude SDKs record to Otis, and choose which route counts each request.
Anthropic records OpenTelemetry spans for your Claude requests in two places. The Claude SDK records spans in your app's process, on the tracer provider your app registers. The Claude API records spans on Anthropic's servers and sends them to a destination that an organization admin adds in the Claude Console. Otis reads both, so you can send your Claude usage to Otis from Anthropic's own spans instead of wrapping your Anthropic client with otis.wrap().
Choosing a route
The two routes record different things:
- Claude SDK tracing records each request your app makes, in your app. Its spans can carry a conversation ID and the user who made the request. Otis needs them to build sessions, tasks and per-user analytics.
- A Claude Console destination records each request on Anthropic's side, including the model requests and server tools that run inside it. It covers requests from every client in the workspaces you choose, on the Claude API and Claude Platform on AWS, except requests to Anthropic's OpenAI-compatible endpoint. Its spans carry no user and no conversation ID.
Use Claude SDK tracing wherever you can. Add a Console destination when some of your traffic doesn't go through an instrumented SDK. When both routes send the same request, Otis counts it once, as Counting each request once describes.
If you already wrap your Anthropic client with otis.wrap(), keep that, and turn the client's own tracing off. From @anthropic-ai/sdk 0.134.0, the client records its own spans on the tracer provider your app registers. If that provider also sends to Otis, each call reaches Otis twice, and the Otis SDK logs one warning when it sees this. Create the wrapped client with openTelemetry: false. Setting ANTHROPIC_OPEN_TELEMETRY=false instead turns tracing off for every Anthropic client in your app, so use it only if every one of them is wrapped.
A Console destination also records the calls you wrap. Leave the workspaces those calls run in out of the destination, as Workspaces and Otis environments describes, or Otis counts each of those calls twice.
Claude SDK tracing
The Claude SDK writes its spans to the tracer provider your app registers. In a TypeScript app, add OtisSpanProcessor to that provider, as Add an Otis span processor describes, and update @runotis/sdk to its latest version. Then create the Anthropic client with content mode on, so that its spans carry the messages:
import Anthropic from "@anthropic-ai/sdk";
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
import { initOtis, OtisSpanProcessor } from "@runotis/sdk";
const otis = initOtis({ apiKey: process.env.OTIS_API_KEY!, serviceName: "my-app" });
// The provider the Claude SDK records on. Keep your other processors beside Otis's.
const provider = new NodeTracerProvider({
spanProcessors: [new OtisSpanProcessor({ apiKey: process.env.OTIS_API_KEY! })],
});
provider.register();
const anthropic = new Anthropic({
openTelemetry: { traces: { contentMode: "content" } },
});
// Around each request:
const message = await otis.withContext({ userId: user.id, sessionId: session.id }, () =>
anthropic.messages.create(
{ model: "claude-opus-4-6", max_tokens: 1024, messages },
{ openTelemetry: { conversationId: conversation.id } },
),
);Two settings tie each request to your users:
withContext. When a Claude SDK span starts insideotis.withContext(), on the instanceinitOtis()returns,OtisSpanProcessoradds that context's user, session and chat IDs to the copy of the span it sends to Otis. It hashes the user and session IDs asotis.wrap()does, so a person matches across both. Your other backends receive the span unchanged. This needsOtisSpanProcessor:OtisExporterreceives a span only after it has ended, so it can't add them.conversationId. Pass the conversation's ID in the options of each request that belongs to a conversation. Otis reads it as the chat ID and keeps each conversation's turns together. When both are set, the Anthropic client'sconversationIdwins over achatIdfromwithContext. Anthropic records the ID in both content modes, so keep personal data out of it.
The Claude SDKs for Python, Go, Java and other languages record the same spans. Send them to Otis with an OTLP exporter, as Point an OTLP exporter at Otis ingest describes, and set user.id and session.id on each span, as Identifying sessions and users shows.
For a streamed request, read the stream with messages.stream(). A stream read from messages.create({ stream: true }) records no response on its span, so Otis can't show Claude's reply.
Claude Console destination
An organization admin adds the destination on the OpenTelemetry settings page of the Claude Console, with Add destination. Add one for each Otis environment you send to, production and development. Each Ingest Key belongs to one of those environments, and Otis analyzes production data only. Customization describes Ingest Keys.
Fill in the destination's fields:
- Provider. Select Custom.
- OTLP endpoint URL. Enter
https://ingest.runotis.com. The API adds/v1/tracesitself, so leave OTLP traces endpoint URL under Advanced settings empty. - Request headers. Add an
Authorizationheader with the valueBearer YOUR_INGEST_KEY, and replaceYOUR_INGEST_KEYwith an Ingest Key for the Otis environment that the destination feeds. - Content mode. Select Full content. With Metadata only, Otis records each request's model, tokens and timing but no messages.
- Workspaces. Choose the workspaces whose requests the destination sends, as the next section describes.
- Maximum content size in MB, under Advanced settings. Set it to 0.75, Anthropic's default, if it isn't already. Don't leave it empty: an empty field sends as much content as the API allows, and a delivery can then be larger than Otis accepts.
Then click Test in the destination's More actions menu. The API sends one test span, and Otis accepts it. Once requests flow, the destination's Delivery column shows Healthy.
If your organization has a HIPAA, FERPA or K-12 data-handling restriction with Anthropic, its destinations send only metadata, whatever their content mode. Claude SDK tracing isn't affected.
The API records no spans for a request whose traceparent header has the sampled flag off. If your app samples its own traces and passes their context to the Claude API, the destination receives only the sampled requests, and Otis can't tell the rest from requests your app never made. Sample the propagated context at 100%, or let Claude SDK spans carry the count.
Workspaces and Otis environments
A destination sends the requests of the Claude workspaces it covers. Its Workspaces setting takes three values:
- All covers every workspace in the organization, including ones created later.
- Selected covers only the workspaces you select.
- All except covers every workspace but the ones you select.
The Ingest Key on a destination decides whether its spans count as production or development data in Otis. The Claude API's spans carry no deployment environment of their own. To keep a test or staging workspace out of your production analytics, leave it out of the production destination, with Selected or All except, and cover it with a second destination that carries a development Ingest Key.
What Otis records
Each Claude request becomes one AI call in Otis, with:
- the model that served the request
- input and output tokens, with cache reads and cache writes, and the 1-hour cache writes that Anthropic bills at a higher rate
- the cost of those tokens at the model's price
- the time to first token of a streamed request
- the user's last message and Claude's reply
- the tool calls in the request: server tools and MCP connector tools that the API runs, and your own tools that the Claude SDK's tool runner runs, each with its arguments and result
Each request to the Messages API resends the earlier messages of its conversation. When Otis has already stored those messages from an earlier request, it stores only the messages the new request added. The AI call's page in the Otis app shows the whole conversation on its Input tab, and the Otis MCP server returns it to your coding agent.
Otis doesn't count the tokens that server-side compaction uses, or the tokens of a Managed Agents outcome evaluation. For a conversation that compacts, or a session with an outcome, Otis's token and cost totals can read lower than your Anthropic bill.
Conversations and users from a Console destination
The Claude API's spans carry no user ID and no conversation ID. The Messages API has no conversation ID, and Anthropic leaves a request's metadata.user_id off its spans. Otis groups the requests of one conversation by their messages instead. Each request resends the earlier messages, so Otis links it to the request before it. The requests it links share a chat ID that starts with msgroot_, and a later request's Input tab shows the conversation up to it.
The grouping has two limits:
- A long conversation splits. The link holds while the conversation fits within the destination's maximum content size. Past it, Anthropic leaves the earliest messages out, and each later request stands alone.
- Identical openings join. Two conversations whose first exchange is word for word the same, such as a fixed greeting and its reply, share a chat ID.
With a Console destination alone, Otis reports your Claude usage, cost, latency and errors, but has no users or sessions to attribute them to. For per-user analytics, sessions and tasks, and conversations that stay together however long they get, use Claude SDK tracing with conversationId and withContext.
Counting each request once
When both routes record the same request, Otis counts its tokens and cost once. It decides, for each Claude workspace, which route carries the count:
- What your instrumentation recorded. When the
otis-instrumentskill sets up your codebase, it records which routes your app uses. If you add or remove a route later, ask your coding agent to update the record. - What Otis sees arriving. Once a day, Otis checks which routes each workspace's spans come from. Where that disagrees with what was recorded, what arrives decides for that workspace, and verification warns you.
Until Otis has checked a new workspace, it counts that workspace by what your instrumentation recorded, and counts both routes if nothing was recorded. Usage already counted isn't counted again when the route changes.
Otis counts most reliably when every request in a workspace goes through the same route, so where you can, give a client without Claude SDK tracing an API key from a workspace of its own. In a workspace that mixes the two, Otis counts each Claude SDK span, and counts an API span only when its request carried no trace context. For that count to hold:
- Register the W3C trace context propagator in each app that uses Claude SDK tracing, so that the Claude SDK sends the trace context with each request. OpenTelemetry for Go, Java and PHP needs it registered explicitly, as Anthropic's setup guide shows.
- Don't pass trace context to the Claude API by hand from a client without Claude SDK tracing. Otis would then count none of that client's requests.
Within the API route, Anthropic records a request's usage on more than one of its spans. Otis counts it once, for a request that completes and for one your app cancels.
Managed Agents sessions
A Managed Agents session becomes a session in Otis, and each of its threads becomes a chat. Its Anthropic session ID stays on each span, so you can find the same session in the Claude Console.
Privacy
Otis scans the content of Claude's spans for personal data before storing it, as it scans prompts from the Otis SDK. That covers the messages, the system prompt, tool arguments and results, and a grader's explanation of a session outcome. Privacy describes redaction.
Limits
Three things can lose Claude API spans without a warning in your app:
- Request rate. Otis accepts a limited number of requests a minute for each Otis environment. The API retries a refused delivery a limited number of times and then drops its spans. If your organization makes many Claude requests each second, contact the Otis team before you add the destination, so that the limit for your project is set to fit.
- Request size. A maximum content size above the default can make one delivery larger than Otis accepts, and the spans in it are lost.
- Failed deliveries. The API disables a destination whose deliveries have all failed for 24 hours, and drops its spans until you enable it again. If you revoke the Ingest Key that a destination uses, put a new one in its header the same day.
Checklist
- Claude SDK tracing sends through
OtisSpanProcessor, with content mode on, orotis.wrap()is used and the client's own tracing is off - Each request runs inside
withContext, and requests that belong to a conversation pass aconversationId - A Console destination, if you use one, for each Otis environment, with the endpoint, an Ingest Key for that environment in the
Authorizationheader, Full content and a maximum content size of 0.75 MB - Test and staging workspaces covered by the development destination, and workspaces whose calls are wrapped with
otis.wrap()left out - Propagated traces sampled at 100%, or the Claude SDK carrying the count
- In a workspace where some requests use Claude SDK tracing and others don't, the W3C trace context propagator registered, and no trace context passed to the API by hand