Anthropic
Wrap the client where you construct it. Call sites do not change.
import metergraphfrom anthropic import Anthropic
client = metergraph.wrap(Anthropic())
response = client.messages.create( model="claude-sonnet-5", max_tokens=1024, messages=[{"role": "user", "content": "Summarize this invoice."}],)import * as mg from "metergraph";import Anthropic from "@anthropic-ai/sdk";
const client = mg.wrap(new Anthropic());
const response = await client.messages.create({ model: "claude-sonnet-5", max_tokens: 1024, messages: [{ role: "user", content: "Summarize this invoice." }],});wrap() patches methods on the object you pass and returns that same object.
Wrapping twice is a no-op.
What gets instrumented
Section titled “What gets instrumented”The Anthropic seam table is short. Two methods, plus the batch results reader.
| Method | Recorded as |
|---|---|
messages.create | messages |
messages.stream | messages.stream |
messages.batches.results | batch.messages |
beta.messages.batches.results | batch.messages |
AsyncAnthropic exposes the same paths, so it is wrapped identically.
import metergraphfrom anthropic import AsyncAnthropic
client = metergraph.wrap(AsyncAnthropic())
async def summarize(text: str) -> str: response = await client.messages.create( model="claude-sonnet-5", max_tokens=1024, messages=[{"role": "user", "content": text}], ) return response.content[0].text// The Node client is already promise-based. Wrap it once and await// as usual.const client = mg.wrap(new Anthropic());The provider’s own promise type is preserved rather than replaced, so
.withResponse() and the other helpers on it keep working, and the underlying
request is still issued exactly once.
Streaming
Section titled “Streaming”Both streaming styles are covered: messages.create(stream=True) and the
messages.stream helper, including its context-manager form.
with client.messages.stream( model="claude-sonnet-5", max_tokens=1024, messages=[{"role": "user", "content": "Write a release note."}],) as stream: for text in stream.text_stream: print(text, end="") final = stream.get_final_message()const stream = client.messages.stream({ model: "claude-sonnet-5", max_tokens: 1024, messages: [{ role: "user", content: "Write a release note." }],});for await (const event of stream) { if (event.type === "content_block_delta") process.stdout.write( "text" in event.delta ? event.delta.text : "", );}const final = await stream.finalMessage();When the stream is exhausted, Metergraph enriches the row from the final
message if the helper exposes one (get_final_message() in Python,
finalMessage() in TypeScript), which is where the complete usage totals live.
Calling that helper yourself finalizes the row too, so either order works and
the row is still written once.
A stream you abandon is still recorded. Closing or aborting the stream
finalizes the row with an abandoned status rather than dropping it, and an
error mid-stream finalizes it with the error.
Time to first token is measured from the first event carrying real output.
For Anthropic that includes text and reasoning deltas, a content_block_start
for a tool_use block, and input_json_delta events. Usage and metadata
events do not start the clock.
Tool use
Section titled “Tool use”Nothing to configure. Each round trip to the API is one row, so an agent loop
produces one row per model call. Wrap the loop in a trace() to see them as
one workflow.
with metergraph.trace("support-agent"): messages = [{"role": "user", "content": question}] while True: response = client.messages.create( model="claude-sonnet-5", max_tokens=1024, messages=messages, tools=tools, ) if response.stop_reason != "tool_use": return response.content[0].text messages.append({"role": "assistant", "content": response.content}) messages.append({"role": "user", "content": run_tools(response.content)})await mg.trace("support-agent", async () => { const messages = [{ role: "user", content: question }]; for (;;) { const response = await client.messages.create({ model: "claude-sonnet-5", max_tokens: 1024, messages, tools, }); if (response.stop_reason !== "tool_use") return response.content[0]; messages.push( { role: "assistant", content: response.content }, { role: "user", content: runTools(response.content) }, ); }});tool_use and tool_result blocks are normalized into the same tool-event
shape used for every provider: call ID, name, arguments and result. See
Captured fields.
Anthropic’s stop_reason values are normalized as well, so end_turn and
OpenAI’s stop land in the same bucket and are comparable across providers.
Message Batches
Section titled “Message Batches”Submitting a batch produces no row. Capture happens when you iterate the results, which is the first point at which per-item usage exists.
client = metergraph.wrap(Anthropic())
batch = client.messages.batches.retrieve(batch_id) # no rowfor item in client.messages.batches.results(batch_id): # one row per item handle(item)const client = mg.wrap(new Anthropic());
const batch = await client.messages.batches.retrieve(batchId); // no rowfor await (const item of await client.messages.batches.results(batchId)) { handle(item); // rows here}Each row is recorded as batch.messages with service_tier: "batch", the
batch ID and the item’s custom_id. An item whose result type is anything
other than succeeded is recorded with an error status rather than dropped.
Items are deduplicated, so iterating the same batch twice does not
double-count.
The rows inherit the route, trace, session and tags that were active when you
called results(), not the context of whatever consumes the iterator
later.
Batch-first execution
Section titled “Batch-first execution”batch_first() also supports a synchronous Anthropic client. It is an
execution API rather than a capture feature, it is never captured, and a missed
deadline can bill the request twice. The semantics and the required
acknowledgements are covered under
OpenAI; pass "anthropic" as
the provider instead of "openai".
Through a gateway
Section titled “Through a gateway”In Python, an Anthropic client pointed at https://ai-gateway.vercel.sh is
detected as a gateway client automatically. See
Vercel AI Gateway.
The gateway: "openrouter" option requires an OpenAI-compatible client and is
rejected on an Anthropic client, before any provider call is made.
- Name a route to group calls by product surface
- Attribution across threads if you dispatch calls to a pool
- Serverless and short-lived jobs before deploying to a runtime that freezes
- Runnable examples: Python, Node