Anthropic integration (aobs.integrations.anthropic)¶
wrap(client, provider=None) instruments the Anthropic Messages API on anthropic.Anthropic and
anthropic.AsyncAnthropic clients: client.messages.create (non-streaming and stream=True),
the client.messages.stream(...) helper, and client.messages.parse(...). Each call becomes an
llm span, a child of the current run or span.
Tested with anthropic 2.1.0, the real library, against a mocked HTTP transport
(httpx2.MockTransport) that returns JSON bodies and SSE streams built from the shapes in the
public Messages API documentation (sdk/python/tests/test_sdk_anthropic.py). It has not been
run against the real Anthropic API by CI; see Live smoke tests to do
that yourself. Other library versions are likely to work (the integration is duck-typed and imports
nothing from anthropic) but are not tested.
import aobs
from aobs.integrations import anthropic as aobs_anthropic
from anthropic import Anthropic
aobs.init(service_name="support-agent", endpoint="http://localhost:8000") # or AOBS_ENDPOINT
client = aobs_anthropic.wrap(Anthropic()) # provider "anthropic" for api.anthropic.com
with aobs.run("answer_ticket", agent="support_agent"):
reply = client.messages.create(
model=MODEL,
max_tokens=1024,
system="You are a support agent.",
messages=[{"role": "user", "content": "Where is order 1042?"}],
tools=TOOLS,
)
with client.messages.stream(model=MODEL, max_tokens=1024, messages=messages) as stream:
for text in stream.text_stream:
print(text, end="")
final = stream.get_final_message()
AsyncAnthropic works the same way (await client.messages.create(...),
async with client.messages.stream(...)). client.with_options(...) and client.copy(...) return
instrumented clients; every other attribute passes through to the real client.
For Anthropic models served through another platform, pass provider explicitly (for example
wrap(AnthropicBedrock(...), provider="aws.bedrock")); a client whose base URL is not
api.anthropic.com otherwise gets anthropic_compatible. Only the first-party client is tested.
Capabilities declared in the batch resource: llm.usage, llm.streaming,
llm.request_params, llm.response_metadata, llm.content, llm.errors.
What is recorded¶
| Recorded | From |
|---|---|
request_model, max_tokens, stream (temperature, top_p when passed) |
request arguments |
response_model, response id |
the Message (or the stream's message_start) |
| finish reason | stop_reason (end_turn, max_tokens, stop_sequence, tool_use, pause_turn, refusal, …) |
provider_request_id |
the request-id header (message _request_id, stream response headers, error request_id) |
usage (provider_reported) |
see below |
| streaming | time to the first content_block_delta event, number of events, completed or not |
| errors | type, HTTP status as code, retryable for 408/409/429/5xx (529 overloaded included) and connection/timeout errors |
| content (policy permitting) | system and messages → OpenTelemetry GenAI {role, parts} (below) |
input_fingerprint |
the request arguments, excluding timeout, extra_headers, extra_query, stream |
Usage: nothing invented¶
| Wire field | Anthropic field |
|---|---|
input_tokens |
input_tokens + cache_read_input_tokens + cache_creation_input_tokens |
cache_read_input_tokens |
cache_read_input_tokens |
cache_write_input_tokens |
cache_creation_input_tokens |
output_tokens |
output_tokens |
reasoning_tokens |
output_tokens_details.thinking_tokens |
total_tokens |
not reported by Anthropic: left unknown, never derived |
Anthropic's input_tokens counts only the input after the last cache breakpoint; the wire's
input_tokens means all input the model processed, with cache reads and writes as subsets of it
(event-model.md), so the integration adds the two cache counts in. A cache
field the response does not carry (or carries as null) adds nothing and stays unknown itself. A
response with no usage records no usage.
Streaming: input and cache counts come from message_delta when it carries them (cumulative) and
otherwise from message_start; output tokens come only from message_delta, because the
count in message_start is a placeholder. A stream closed before message_delta therefore records
its input tokens but leaves output tokens unknown.
Content¶
Under capture="redacted" or "full" (the default metadata_only sends none of it):
system(string or text blocks) becomes a leadingsystemmessage.textblocks →{"type": "text", "content": ...}.tool_useblocks →{"type": "tool_call", "id", "name", "arguments"}(theinputobject), the same part typeopenai_compatuses for tool calls. Tool calls are recorded as content parts of thellmspan; a tool span is created only when your tool function runs (@aobs.tool).tool_resultblocks →{"type": "tool_call_response", "id": tool_use_id, "response": ...}.thinkingblocks →{"type": "reasoning", "content": ...}(the signature is not kept).- Everything else (images, documents,
redacted_thinking, server tool blocks, …) is recorded by type only; its data is not captured. - The response becomes one
assistantmessage withfinish_reasonset tostop_reason. For streams, text, thinking and tool-call inputs are accumulated from the deltas (up to 64 KiB); a tool input whose JSON is incomplete (stream cut short) is kept as the text received.
Streams¶
- Time to first token is measured to the first
content_block_delta(themessage_startandpingevents arrive before any generated content).chunk_countcounts the events the library yields (it consumespingitself). - A stream read to the end is
okwithcompleted: true. One closed early, left early throughwith, or garbage-collected unread iscancelledwithcompleted: false. - An exception raised inside the
withblock (yours or the library's) ends the span aserror. - An
errorevent in the middle of a stream (the HTTP status was 200) is recorded with the event's error type ascode(e.g.overloaded_error) andretryableforrate_limit_error,api_error,timeout_errorandoverloaded_error. - With
client.messages.stream(...)the request (and the span) starts when the manager is entered, not whenstream(...)is called. All ways of reading the helper (iteration,text_stream,get_final_message,until_done) are observed.
Not instrumented / not recorded¶
with_raw_response/with_streaming_response,client.beta.messages,messages.batches,messages.count_tokens, and other endpoints pass through uninstrumented.- Cost (the Messages API does not report it), headers, API keys,
server_tool_usecounts,service_tier,stop_details.