Skip to content

Amazon Bedrock

SideSeat instruments the boto3 Bedrock Runtime client to capture model information, token usage, messages, and costs from both the Converse and InvokeModel APIs.

  • SideSeat running locally (sideseat)
  • Python SDK installed (pip install sideseat or uv add sideseat)
  • AWS credentials configured for Amazon Bedrock

SideSeat patches the boto3 Converse API automatically. Initialize SideSeat with the Bedrock framework, then use boto3 as usual:

from sideseat import SideSeat, Frameworks
import boto3
SideSeat(framework=Frameworks.Bedrock)
bedrock = boto3.client("bedrock-runtime", region_name="us-east-1")
response = bedrock.converse(
modelId="us.anthropic.claude-sonnet-4-5-20250929-v1:0",
system=[{"text": "Answer in one sentence."}],
messages=[{"role": "user", "content": [{"text": "What is the speed of light?"}]}],
inferenceConfig={"maxTokens": 128},
)
print(response["output"]["message"]["content"][0]["text"])

By default, each Bedrock API call produces its own independent trace. Use client.trace() to group related calls under a single root span:

client = SideSeat(framework=Frameworks.Bedrock)
bedrock = boto3.client("bedrock-runtime", region_name="us-east-1")
model_id = "us.anthropic.claude-sonnet-4-5-20250929-v1:0"
with client.trace("geography-chat"):
messages = []
# Turn 1
messages.append({"role": "user", "content": [{"text": "What is the capital of France?"}]})
response = bedrock.converse(modelId=model_id, messages=messages)
messages.append(response["output"]["message"])
# Turn 2 — follows up on the previous answer
messages.append({"role": "user", "content": [{"text": "What about Germany?"}]})
response = bedrock.converse(modelId=model_id, messages=messages)
messages.append(response["output"]["message"])
# Turn 3
messages.append({"role": "user", "content": [{"text": "Which city has a larger population?"}]})
response = bedrock.converse(modelId=model_id, messages=messages)

This produces the following span hierarchy:

geography-chat (root span)
├── chat claude-sonnet-4-5... (turn 1)
├── chat claude-sonnet-4-5... (turn 2)
└── chat claude-sonnet-4-5... (turn 3)

All three calls appear as child spans in the SideSeat UI, with the full multi-turn conversation visible in the trace detail view.

Pass session_id and user_id to client.trace() to group independent traces into a session. The SideSeat sessions view groups all traces that share the same session_id.

Each client.trace() produces its own trace with its own trace ID, but they are linked by the shared session:

from sideseat import SideSeat, Frameworks
import boto3
client = SideSeat(framework=Frameworks.Bedrock)
bedrock = boto3.client("bedrock-runtime", region_name="us-east-1")
model_id = "us.anthropic.claude-sonnet-4-5-20250929-v1:0"
session_id = "sess-abc"
user_id = "user-123"
# Trace 1: Trip planning
with client.trace("trip-planning", session_id=session_id, user_id=user_id):
messages = []
messages.append({"role": "user", "content": [{"text": "Plan a 5-day trip to Japan."}]})
response = bedrock.converse(modelId=model_id, messages=messages)
messages.append(response["output"]["message"])
messages.append({"role": "user", "content": [{"text": "Tell me more about Kyoto."}]})
response = bedrock.converse(modelId=model_id, messages=messages)
# Trace 2: Food recommendations (fresh conversation, same session)
with client.trace("food-recommendations", session_id=session_id, user_id=user_id):
messages = []
messages.append({"role": "user", "content": [{"text": "What are the must-try dishes in Tokyo?"}]})
response = bedrock.converse(modelId=model_id, messages=messages)
messages.append(response["output"]["message"])
messages.append({"role": "user", "content": [{"text": "What about street food in Osaka?"}]})
response = bedrock.converse(modelId=model_id, messages=messages)

This produces two independent traces, each with their own span hierarchy:

Trace 1: trip-planning (session_id=sess-abc, user_id=user-123)
├── chat claude-sonnet-4-5... (turn 1)
└── chat claude-sonnet-4-5... (turn 2)
Trace 2: food-recommendations (session_id=sess-abc, user_id=user-123)
├── chat claude-sonnet-4-5... (turn 1)
└── chat claude-sonnet-4-5... (turn 2)

Each trace starts a fresh conversation with its own message history. The SideSeat sessions view groups them by session_id.

converse_stream responses are fully captured, including token counts aggregated from stream metadata:

from sideseat import SideSeat, Frameworks
import boto3
client = SideSeat(framework=Frameworks.Bedrock)
bedrock = boto3.client("bedrock-runtime", region_name="us-east-1")
response = bedrock.converse_stream(
modelId="us.anthropic.claude-sonnet-4-5-20250929-v1:0",
system=[{"text": "Answer in one sentence."}],
messages=[{"role": "user", "content": [{"text": "What is the boiling point of water?"}]}],
inferenceConfig={"maxTokens": 128},
)
for event in response["stream"]:
if "contentBlockDelta" in event:
print(event["contentBlockDelta"]["delta"].get("text", ""), end="", flush=True)

Claude’s extended thinking is captured for both sync and streaming calls. Thinking blocks appear in the message thread alongside the final response.

from sideseat import SideSeat, Frameworks
import boto3
client = SideSeat(framework=Frameworks.Bedrock)
bedrock = boto3.client("bedrock-runtime", region_name="us-east-1")
response = bedrock.converse(
modelId="us.anthropic.claude-sonnet-4-5-20250929-v1:0",
system=[{"text": "You are a math tutor. Show your work."}],
messages=[{"role": "user", "content": [{"text": "What is 27 * 453?"}]}],
inferenceConfig={"maxTokens": 8192},
additionalModelRequestFields={
"thinking": {"type": "enabled", "budget_tokens": 1024}
},
)
for block in response["output"]["message"]["content"]:
if "reasoningContent" in block:
print(f"Thinking: {block['reasoningContent']['reasoningText']['text']}")
elif "text" in block:
print(f"Answer: {block['text']}")

Tool definitions, tool use requests, and tool results are all captured:

from sideseat import SideSeat, Frameworks
import boto3
client = SideSeat(framework=Frameworks.Bedrock)
bedrock = boto3.client("bedrock-runtime", region_name="us-east-1")
model_id = "us.anthropic.claude-sonnet-4-5-20250929-v1:0"
tool_config = {
"tools": [{
"toolSpec": {
"name": "get_weather",
"description": "Get the current weather for a location.",
"inputSchema": {
"json": {
"type": "object",
"properties": {
"location": {"type": "string", "description": "City name"}
},
"required": ["location"],
}
},
}
}]
}
# Step 1: model requests a tool call
messages = [{"role": "user", "content": [{"text": "What's the weather in Paris?"}]}]
response = bedrock.converse(
modelId=model_id,
system=[{"text": "Use tools when available."}],
messages=messages,
toolConfig=tool_config,
)
assistant_msg = response["output"]["message"]
messages.append(assistant_msg)
# Step 2: return the tool result
tool_use = next(b["toolUse"] for b in assistant_msg["content"] if "toolUse" in b)
messages.append({
"role": "user",
"content": [{"toolResult": {
"toolUseId": tool_use["toolUseId"],
"content": [{"text": "Sunny, 22C"}],
}}],
})
# Step 3: model produces the final answer
response = bedrock.converse(
modelId=model_id,
system=[{"text": "Use tools when available."}],
messages=messages,
toolConfig=tool_config,
)

SideSeat captures all three steps as separate spans, each with full message details.

SideSeat also instruments invoke_model and invoke_model_with_response_stream for direct Claude Messages API access:

from sideseat import SideSeat, Frameworks
import boto3
import json
SideSeat(framework=Frameworks.Bedrock)
bedrock = boto3.client("bedrock-runtime", region_name="us-east-1")
body = json.dumps({
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": 128,
"system": "Answer in one sentence.",
"messages": [{"role": "user", "content": "What is the speed of light?"}],
})
response = bedrock.invoke_model(
modelId="us.anthropic.claude-sonnet-4-5-20250929-v1:0",
body=body,
contentType="application/json",
)
result = json.loads(response["body"].read())
print(result["content"][0]["text"])

Amazon Bedrock uses the standard AWS credential chain:

Terminal window
# Environment variables
export AWS_ACCESS_KEY_ID=xxx
export AWS_SECRET_ACCESS_KEY=xxx
export AWS_REGION=us-east-1
# Or AWS profile
export AWS_PROFILE=my-profile