Skip to content

Anthropic

Wrap the client where you construct it. Call sites do not change.

app/llm.py
import metergraph
from 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."}],
)

wrap() patches methods on the object you pass and returns that same object. Wrapping twice is a no-op.

The Anthropic seam table is short. Two methods, plus the batch results reader.

MethodRecorded as
messages.createmessages
messages.streammessages.stream
messages.batches.resultsbatch.messages
beta.messages.batches.resultsbatch.messages

AsyncAnthropic exposes the same paths, so it is wrapped identically.

app/llm.py
import metergraph
from 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 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.

Both streaming styles are covered: messages.create(stream=True) and the messages.stream helper, including its context-manager form.

app/llm.py
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()

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.

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.

app/agent.py
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)})

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.

Submitting a batch produces no row. Capture happens when you iterate the results, which is the first point at which per-item usage exists.

app/batch.py
client = metergraph.wrap(Anthropic())
batch = client.messages.batches.retrieve(batch_id) # no row
for item in client.messages.batches.results(batch_id): # one row per item
handle(item)

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() 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".

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.