Amazon Bedrock

Auto-instrument the Bedrock Runtime client (boto3 on PyPI, @aws-sdk/client-bedrock-runtime on npm). Converse, ConverseStream, InvokeModel and InvokeModelWithResponseStream calls inside an active run emit an llm_call event with model id, token usage, latency, and, for Converse, prompt-cache counts, stop reason and tool use. Cost is computed server-side from the rate card, keyed on the Bedrock model id.

Install / enable

Python:

pip install "agentping-io[bedrock]"
import agentping
import boto3

agentping.init()
agentping.instrument_bedrock()

client = boto3.client("bedrock-runtime", region_name="eu-west-2")

with agentping.run("support-triage", customer_id="acme-corp"):
    reply = client.converse(
        modelId="anthropic.claude-sonnet-4-5",
        messages=[{"role": "user", "content": [{"text": "classify this ticket"}]}],
    )

boto3 builds its clients dynamically, so instrument_bedrock() patches botocore.client.BaseClient._make_api_call and acts only on the bedrock-runtime service; every other AWS call passes straight through. Idempotent and global; call it once at module load. Supported botocore versions: 1.34 up to, but not including, 3.0. Async clients (aioboto3, aiobotocore) are not covered.

TypeScript:

npm install @agentping/sdk @aws-sdk/client-bedrock-runtime
import { BedrockRuntimeClient, ConverseCommand } from "@aws-sdk/client-bedrock-runtime";
import * as agentping from "@agentping/sdk";

agentping.init({ apiKey: process.env.AGENTPING_API_KEY });

const run = agentping.run("answer-bot");
const bedrock = agentping.instrumentBedrock(
  new BedrockRuntimeClient({ region: "eu-west-2" }),
  { run },
);

const reply = await bedrock.send(
  new ConverseCommand({
    modelId: "anthropic.claude-sonnet-4-5",
    messages: [{ role: "user", content: [{ text: "hi" }] }],
  }),
);

await run.finish({ status: "success" });

instrumentBedrock wraps send and inspects the command class, so ConverseCommand, ConverseStreamCommand, InvokeModelCommand and InvokeModelWithResponseStreamCommand are recognised and everything else passes through.

What's captured

Field Converse / ConverseStream InvokeModel / InvokeModelWithResponseStream
provider bedrock bedrock
model modelId from the request modelId from the request
input_tokens usage.inputTokens plus cacheReadInputTokens plus cacheWriteInputTokens (Converse reports net of cache; the SDK reports gross) x-amzn-bedrock-input-token-count response header
output_tokens usage.outputTokens x-amzn-bedrock-output-token-count response header
cached_input_tokens usage.cacheReadInputTokens not available
cache_creation_input_tokens usage.cacheWriteInputTokens not available
finish_reason stopReason not available
tool_calls Number of toolUse blocks in the output message not available
latency_ms Wall-clock time of the call Wall-clock time of the call
stream true on ConverseStream true on InvokeModelWithResponseStream

Converse is the richer path: it normalises usage across every model family, so prefer it where the model supports it. InvokeModel bodies are model-specific and the wrapper does not parse them; token counts come from the response headers only. Bedrock reports no reasoning-token count on either path. Zero-valued fields are omitted.

A call that raises is recorded as an llm_call with status: "error", error and exception, then re-raised.

Streaming

ConverseStream is captured in full. The wrapper reads the terminal metadata event for usage, messageStop for the stop reason and counts contentBlockStart events carrying toolUse; the llm_call fires once the event stream is fully consumed or closed.

with agentping.run("answer-bot"):
    response = client.converse_stream(
        modelId="anthropic.claude-sonnet-4-5",
        messages=[{"role": "user", "content": [{"text": "explain briefly"}]}],
    )
    for event in response["stream"]:
        if "contentBlockDelta" in event:
            print(event["contentBlockDelta"]["delta"].get("text", ""), end="", flush=True)
    # llm_call is emitted here

InvokeModelWithResponseStream is recorded when the call returns, from the token-count headers, and the body stream is not observed. Models that only report usage inside the streamed body land with latency but without token counts; use Converse for those.

Prompt capture and time to first token

ConverseStream calls also report ttft_ms, the time from the request to the first contentBlockStart or contentBlockDelta event; messageStart does not count. Pulse charts its p95 per agent. InvokeModelWithResponseStream reports no ttft_ms, because its body is provider-specific.

Prompt and completion text are off by default. Turn them on once at init, with an optional cap per piece (default 4000 characters):

agentping.init(capture_llm_content=True, llm_content_max_chars=4000)
agentping.init({ captureLlmContent: true, llmContentMaxChars: 4000 });

Converse and ConverseStream calls then carry input (the messages list with each content block rendered: text blocks verbatim, others as markers such as [toolUse lookup_order], [toolResult] or [image]), system_instructions (the system text blocks joined) and output (the reply's text blocks). Each piece is clipped at the cap. The run page renders them as messages; see events for the wire shape. InvokeModel bodies are provider-specific bytes and are not captured.

Source / notes

Rates are keyed on provider plus model, and Bedrock calls carry provider: bedrock with the Bedrock model id. Claude ids (anthropic.claude-sonnet-4-5, eu.anthropic.claude-sonnet-4-5-20250929-v1:0) fold onto the default Anthropic rows, which match Bedrock's global-endpoint list price; regional endpoints carry a 10% premium, so add a bedrock override for the exact id if you need that reflected. Other vendors' ids (amazon.nova-pro-v1:0, meta.llama4-*) land with their token counts and appear in the unpriced-models list until you add the model id's per-million rates under Spend, Rate cards. See Spend and the LLM API pricing page.