Amazon Bedrock
Use Solwyn with Amazon Bedrock — Converse API, streaming, region-aware pricing, cache and service tiers
import os
import boto3
from botocore.config import Config
from solwyn import Solwyn
bedrock = boto3.client(
"bedrock-runtime",
region_name="us-east-1",
# Recommended: let Solwyn own retries/failover instead of stacking
# botocore's default retry layer (legacy mode retries up to 5 times).
config=Config(retries={"total_max_attempts": 1}, read_timeout=60),
)
client = Solwyn(
bedrock,
api_key=os.environ["SOLWYN_API_KEY"],
)
response = client.converse(
modelId="us.anthropic.claude-3-5-sonnet-20241022-v2:0",
messages=[{"role": "user", "content": [{"text": "Explain how a CPU works in two sentences."}]}],
inferenceConfig={"maxTokens": 1024},
)
print(response["output"]["message"]["content"][0]["text"])
client.close()Solwyn intercepts the Converse API (converse and converse_stream), which works uniformly across every chat model Bedrock hosts -- Anthropic Claude, Amazon Nova, Meta Llama, Mistral, Cohere, AI21, DeepSeek, and more. The raw boto3 response dict comes back untouched.
Setup
Bedrock support adds zero new dependencies: the SDK never imports boto3. Bring your own:
pip install boto3The solwyn[bedrock] extra installs boto3>=1.34 as a convenience only -- it is not required for detection or extraction:
pip install "solwyn[bedrock]"AWS authentication (IAM credentials, profiles, assumed roles, SigV4) lives entirely on your boto3 client, exactly as it does without Solwyn. Solwyn never sees, stores, or transmits AWS credentials. There are no new environment variables -- SOLWYN_API_KEY works as for every other provider.
converse() requires the modelId keyword argument (the same requirement boto3 imposes); calling it without one raises TypeError.
Client detection
The SDK detects Bedrock clients by shape, without importing boto3: the client's class module must contain botocore and client.meta.service_model.service_name must equal "bedrock-runtime". This covers both boto3 (botocore.client) and aioboto3 (aiobotocore.client).
The bedrock control-plane client (boto3.client("bedrock")) deliberately does not match -- wrap the bedrock-runtime client, which is the one that serves inference.
Streaming
converse_stream preserves the boto3 contract: you get back a dict whose "stream" value is Solwyn's wrapper around the inner event stream. Iterate it exactly as you would with raw boto3. Token usage settles from the stream's terminal metadata event when the stream is exhausted:
result = client.converse_stream(
modelId="amazon.nova-pro-v1:0",
messages=[{"role": "user", "content": [{"text": "Hello!"}]}],
)
with result["stream"]: # settles the budget reservation even on early break
for event in result["stream"]:
...If you stop consuming the stream early, call result["stream"].close() (or wrap iteration in with result["stream"]: as above) to settle the budget reservation with whatever usage was observed. This mirrors the close obligation raw boto3 EventStreams already impose. close() settles exactly once and is safe to call repeatedly.
A stream that settles at zero tokens after producing real traffic logs an explicit warning on the solwyn.providers.bedrock logger -- never silently wrong counts. A genuinely empty stream stays silent.
Async usage
Async works with aioboto3. Pass the async client to AsyncSolwyn() inside the session context manager:
import asyncio
import os
import aioboto3
from solwyn import AsyncSolwyn
async def main():
session = aioboto3.Session()
async with session.client("bedrock-runtime", region_name="us-east-1") as bedrock:
async with AsyncSolwyn(
bedrock,
api_key=os.environ["SOLWYN_API_KEY"],
) as client:
response = await client.converse(
modelId="us.anthropic.claude-3-5-sonnet-20241022-v2:0",
messages=[{"role": "user", "content": [{"text": "Hello!"}]}],
inferenceConfig={"maxTokens": 1024},
)
print(response["output"]["message"]["content"][0]["text"])
asyncio.run(main())For async streaming, iterate with async for and close with await result["stream"].close() or async with result["stream"]:.
Supported models
The SDK recognizes Bedrock model identifiers in two shapes:
| Shape | Examples |
|---|---|
[geo.]vendor.model | amazon.nova-pro-v1:0, us.anthropic.claude-3-5-sonnet-20241022-v2:0, eu.meta.llama3-2-3b-instruct-v1:0 |
| Bedrock ARNs | arn:aws:bedrock:us-east-1:...:inference-profile/... |
The geographic inference-profile prefix is open-ended by design (us., eu., jp., apac., global., us-gov., ...), so future AWS regions match without an SDK update. Recognized vendor namespaces: ai21, amazon, anthropic, cohere, deepseek, google, luma, meta, minimax, mistral, moonshot, nvidia, openai, qwen, stability, twelvelabs, writer.
Direct-provider model ids (claude-*, gpt-*, gemini-*) intentionally do not match as Bedrock -- those belong to the direct providers.
Converse responses do not echo a model id, so the model is reported exactly as you pass it -- foundation-model id, cross-region inference profile, or full ARN. Model identifiers up to 2048 characters are supported to accommodate ARNs.
What Bedrock is priced for
Bedrock is priced for text only, and its billing scope is vendor-and-family: every Anthropic model, plus Amazon's Nova family, and nothing else. A Converse call to any other vendor or family (amazon.titan-*, meta.*, mistral.*, ...) is intercepted and budget-checked like any other, but fails loud as an unknown model on the pricing path until a verified rate card lands — rejected rather than guessed at. A model behind an opaque ARN the Cloud API cannot resolve to a price card is recorded unpriced — visible on the dashboard, never a fabricated $0. See the provider × modality matrix.
Token detail fields
Bedrock follows the same additive cache convention as direct Anthropic: usage.inputTokens covers only the non-cached input. Solwyn normalizes to a single input_tokens total:
normalized input_tokens = inputTokens
+ cacheReadInputTokens
+ cacheWriteInputTokens| Normalized field | Bedrock Converse source |
|---|---|
input_tokens | usage.inputTokens + usage.cacheReadInputTokens + usage.cacheWriteInputTokens |
output_tokens | usage.outputTokens |
cached_input_tokens | usage.cacheReadInputTokens |
cache_creation_5m_tokens / cache_creation_1h_tokens | split of usage.cacheWriteInputTokens via usage.cacheDetails |
The cache fields are optional on the wire (absent for model families without prompt caching) and default to 0.
Cache-write TTL split
Cache writes are priced differently by time-to-live, so Solwyn splits the aggregate cacheWriteInputTokens into 5-minute and 1-hour buckets using usage.cacheDetails (a list of {inputTokens, ttl} entries). The 1-hour share is whatever the details attribute to "1h", clamped to the aggregate; the remainder stays in the 5-minute bucket, so the total write count is never lost even when the breakdown is partial. When cacheDetails is absent, the entire aggregate is attributed to the 5-minute bucket -- Bedrock's default TTL.
Region
Bedrock pricing is keyed per model and region, so Solwyn reads the client's region (client.meta.region_name) and reports it as provider_region alongside the token counts. The Cloud API uses it to price each call at the correct regional rate -- including reconciliation events and budget-denied events. Other providers report no region, and the field is omitted from the wire entirely when unset.
Service tier
Bedrock can serve requests at non-standard pricing tiers (e.g. latency-optimized inference). Solwyn captures the tier from the response, because the response echo is billing ground truth: an over-quota latency-optimized request is served and billed at standard rates, and the echo reflects that. serviceTier.type wins when present; otherwise performanceConfig.latency ("optimized") is used. Values are bounded at 32 characters, with a logged warning on truncation.
If Bedrock echoes a tier value Solwyn does not recognize, the budget confirm settles at Standard rates rather than failing, and the raw (bounded) value is still reported on the metadata event. See Privacy.
Cross-provider failover
Bedrock participates in provider failover in both directions -- for example Bedrock-hosted Claude failing over to direct Anthropic, or an OpenAI primary failing over to Bedrock:
import os
import boto3
from anthropic import Anthropic
from solwyn import Solwyn
bedrock = boto3.client("bedrock-runtime", region_name="us-east-1")
client = Solwyn(
bedrock,
model="us.anthropic.claude-3-5-sonnet-20241022-v2:0",
api_key=os.environ["SOLWYN_API_KEY"],
fallback=[
(Anthropic(), "claude-sonnet-4-5"),
],
)Responses are always reshaped to the caller's dialect: a Bedrock caller served by Anthropic still gets a Converse-shaped dict (with result["stream"] yielding Bedrock event dicts), and an OpenAI caller served by Bedrock gets OpenAI-shaped objects and chunks. The region never leaks across hops -- non-Bedrock hops report no provider_region.
Error classification
botocore exceptions are classified by class name plus the response's HTTP status code:
- Failover:
ThrottlingException(429),EndpointConnectionError,ConnectTimeoutError,ProxyConnectionError-- the request provably never landed, so the next provider in the chain is tried. - Ambiguous post-send (the request may have reached the model):
ModelTimeoutException(408),ModelErrorException(424),ConnectionClosedError,ReadTimeoutError-- Solwyn re-raises the original exception and never fails over, to avoid double-charging you for a call that may have succeeded. It reports the call as possibly succeeded so the Cloud API can reconcile the charge, withprovider_regionattached for correct regional repricing.
Translation subset
Request translation applies only on cross-provider hops -- native Bedrock calls pass your kwargs straight through to boto3, untouched. On a cross-provider hop the recognized top-level keys are modelId, messages, system, inferenceConfig, and toolConfig; the inferenceConfig subset is maxTokens, temperature, topP, and stopSequences.
inferenceConfig.maxTokens is required for a cross-provider hop -- the SDK will not invent an output bound and fails loud with the structural label missing_max_tokens.
Anything outside the canonical subset raises UntranslatableRequestError instead of being silently dropped: guardrailConfig (a safety feature a hop would otherwise strip), cachePoint blocks, guardContent, reasoningContent, s3Location image sources, tool-call streaming across dialects, additionalModelRequestFields, promptVariables, requestMetadata, performanceConfig, serviceTier, and any other unrecognized kwarg. The error carries structural labels only: an unrecognized kwarg is named by its key (unsupported_kwarg.<key>); everything else maps to a fixed label -- your values and prompt content are never interpolated into it. See Exceptions.
invoke_model is not supported
invoke_model and invoke_model_with_response_stream raise ConfigurationError on a Bedrock-wrapped client instead of silently bypassing budget tracking. Their token usage lives inside a consume-once response body alongside the response content -- extracting it would require buffering customer content, which is off the table by architecture.
start_async_invoke (Bedrock's asynchronous, video-scale invocation surface) raises ConfigurationError for the same reason -- the call returns only an invocationArn, and its output and usage land out-of-band in S3, so Solwyn can neither enforce a budget nor emit a cost event for it. Rather than pass this high-cost spend through untracked, Solwyn refuses it.
Use converse() / converse_stream() (they cover every chat model Bedrock hosts), or call the unwrapped boto3 client directly for deliberately untracked calls.
Timeouts and retries
boto3 has no per-call timeout override, so the failover chain's deadline cannot shorten an in-flight Bedrock hop. Bound it on the client instead, and disable botocore's own retry layer (legacy mode retries up to 5 times, which would stack underneath Solwyn's failover):
from botocore.config import Config
bedrock = boto3.client(
"bedrock-runtime",
region_name="us-east-1",
config=Config(retries={"total_max_attempts": 1}, read_timeout=60),
)Reasoning tokens limitation
The Converse API folds reasoningContent output into outputTokens and does not break out reasoning tokens. As a result, reasoning_tokens is always 0 for Bedrock responses. This is a limitation of the Converse API, not the Solwyn SDK -- total output cost is still computed correctly from output_tokens.
Pre-call token estimation for Bedrock uses the heuristic estimator (approximately 4 characters per token). This is an estimate only, used for budget pre-checks -- settled cost always comes from the provider-reported usage on the response.
Privacy
The same wrapper-not-proxy guarantees apply: your Converse calls go directly to AWS, and Solwyn never captures, logs, or transmits prompts or responses.
- The Bedrock adapter reads only the
usage,serviceTier, andperformanceConfigfields of a response; the stream accumulator retains usage and tier values, never content-bearing events. - AWS credentials stay entirely on your boto3 client -- Solwyn never sees them.
- Cross-provider translation is an in-memory transform with no I/O and no logging; translation errors carry fixed structural labels, never your data.
- Prompt-size estimation counts characters only -- not reversible to content.
See the Privacy guide for the full posture.
Passthrough attributes
Any attribute not intercepted by Solwyn is passed through to the underlying boto3 client. You can still access any bedrock-runtime method directly.