ubxty / bedrock-ai
A Laravel package for seamless AWS Bedrock integration with multi-key rotation, cross-region inference, usage tracking, pricing, and powerful CLI tools.
Installs: 0
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/ubxty/bedrock-ai
Requires
- php: ^8.2
- aws/aws-sdk-php: ^3.300
- illuminate/cache: ^11.0|^12.0
- illuminate/console: ^11.0|^12.0
- illuminate/http: ^11.0|^12.0
- illuminate/support: ^11.0|^12.0
Requires (Dev)
- orchestra/testbench: ^9.0|^10.0
- phpunit/phpunit: ^11.0
README
A Laravel package for seamless AWS Bedrock integration. Provides multi-key credential rotation, cross-region inference profiles, CloudWatch usage tracking, real-time pricing, and powerful CLI tools—all with zero boilerplate.
Table of Contents
- Features
- Requirements
- Installation
- Quick Start
- Configuration
- Usage
- CLI Commands
- Architecture
- Error Handling
- API Reference
- Testing
- License
Features
| Feature | Description |
|---|---|
| Multi-key rotation | Configure multiple AWS credential sets with automatic failover |
| Cross-region inference | Automatic us./eu. prefix for newer models (Claude 3.5+, Nova, Llama 3.3+) |
| Dual mode | AWS SDK or HTTP Bearer token (ABSK) — auto-detected from credentials |
| Rate limit retry | Exponential backoff with configurable retries per key |
| Titan + Claude support | Handles different request/response formats transparently |
| Converse API | Unified AWS Converse API across all model providers |
| Streaming | Real-time token streaming via invokeModelWithResponseStream |
| Conversation Builder | Fluent multi-turn conversation API with chaining |
| Model Aliases | Define short names for model IDs (e.g., 'claude' → full model ID) |
| Token Estimation | Pre-call token count and cost estimation |
| Laravel Events | BedrockInvoked, BedrockRateLimited, BedrockKeyRotated events |
| Invocation Logger | Auto-log all invocations with configurable channels |
| Health Check Route | Registerable /health/bedrock endpoint for monitoring |
| CloudWatch usage | Track input/output tokens, invocations, latency from CloudWatch metrics |
| Real-time pricing | Fetch current Bedrock pricing from the AWS Pricing API |
| Cost limits | Configurable daily/monthly spending limits |
| 5 CLI commands | Configure, test, list models, view usage, fetch pricing |
| Facade + DI | Use Bedrock::invoke() or inject BedrockManager |
Requirements
- PHP 8.2+
- Laravel 11 or 12
aws/aws-sdk-php^3.300- AWS credentials with Bedrock access
Installation
composer require ubxty/bedrock-ai
Publish the configuration file:
php artisan vendor:publish --tag=bedrock-config
Or use the interactive wizard:
php artisan bedrock:configure
Quick Start
use Ubxty\BedrockAi\Facades\Bedrock; // Simple invocation $result = Bedrock::invoke( modelId: 'anthropic.claude-3-5-sonnet-20241022-v2:0', systemPrompt: 'You are a helpful assistant.', userMessage: 'What is the capital of France?' ); echo $result['response']; // "The capital of France is Paris." echo $result['total_tokens']; // 42 echo $result['cost']; // 0.000234 echo $result['latency_ms']; // 850
Configuration
Basic Setup
Add these to your .env:
BEDROCK_AWS_KEY=AKIA... BEDROCK_AWS_SECRET=your-secret-key BEDROCK_REGION=us-east-1
Or use an ABSK Bearer Token (auto-detected):
BEDROCK_AWS_KEY=ABSKxxxxxxxxxxxxxxx BEDROCK_AWS_SECRET=ABSKxxxxxxxxxxxxxxx BEDROCK_REGION=us-east-1
Multiple AWS Keys (Failover)
In config/bedrock.php, add multiple keys to a connection. If the first key hits a rate limit or fails, the client automatically tries the next:
'connections' => [ 'default' => [ 'keys' => [ [ 'label' => 'Primary', 'aws_key' => env('BEDROCK_AWS_KEY'), 'aws_secret' => env('BEDROCK_AWS_SECRET'), 'region' => 'us-east-1', ], [ 'label' => 'Backup', 'aws_key' => env('BEDROCK_AWS_KEY_2'), 'aws_secret' => env('BEDROCK_AWS_SECRET_2'), 'region' => 'us-west-2', ], ], ], ],
Multiple Connections
Define separate connections for different environments or use cases:
'connections' => [ 'default' => [ 'keys' => [/* ... */], ], 'production' => [ 'keys' => [/* ... */], ], 'staging' => [ 'keys' => [/* ... */], ], ],
Switch at runtime:
Bedrock::invoke('anthropic.claude...', $system, $user, connection: 'production'); // or $client = Bedrock::client('staging');
Cost Limits
BEDROCK_DAILY_LIMIT=10.00 BEDROCK_MONTHLY_LIMIT=300.00
When exceeded, a CostLimitExceededException is thrown.
Pricing & Usage API
If you use separate IAM credentials for the Pricing or CloudWatch APIs:
BEDROCK_PRICING_KEY=AKIA... BEDROCK_PRICING_SECRET=your-pricing-secret BEDROCK_USAGE_KEY=AKIA... BEDROCK_USAGE_SECRET=your-usage-secret BEDROCK_USAGE_REGION=us-east-1
If not set, the package falls back to the default connection's first key.
Usage
Facade
use Ubxty\BedrockAi\Facades\Bedrock; // Invoke a model $result = Bedrock::invoke('anthropic.claude-3-5-sonnet-20241022-v2:0', $system, $user); // Test connection $test = Bedrock::testConnection(); // List models $models = Bedrock::fetchModels(); // Check if configured if (Bedrock::isConfigured()) { /* ... */ } // Pricing $pricing = Bedrock::pricing()->getPricing(); // Usage $usage = Bedrock::usage()->getAggregatedUsage(30);
Dependency Injection
use Ubxty\BedrockAi\BedrockManager; class MyService { public function __construct(protected BedrockManager $bedrock) {} public function analyze(string $text): string { $result = $this->bedrock->invoke( 'anthropic.claude-3-5-sonnet-20241022-v2:0', 'You are an analyst.', $text, maxTokens: 2048 ); return $result['response']; } }
Invoking Models
$result = Bedrock::invoke( modelId: 'anthropic.claude-3-5-sonnet-20241022-v2:0', systemPrompt: 'You are a medical assistant.', userMessage: 'Explain hypertension in simple terms.', maxTokens: 1024, temperature: 0.5, pricing: [ 'input_price_per_1k' => 0.003, 'output_price_per_1k' => 0.015, ] ); // $result structure: // [ // 'response' => 'Hypertension, or high blood pressure...', // 'input_tokens' => 45, // 'output_tokens' => 230, // 'total_tokens' => 275, // 'cost' => 0.003585, // 'latency_ms' => 1250, // 'status' => 'success', // 'key_used' => 'Primary', // 'model_id' => 'us.anthropic.claude-3-5-sonnet-20241022-v2:0', // ]
Supported models include:
- Claude 3.5 Sonnet/Haiku, Claude 3 Opus/Sonnet/Haiku, Claude 4
- Amazon Titan Text Express/Lite/Premier
- Amazon Nova Pro/Lite/Micro
- Meta Llama 3/3.1/3.2/3.3/4
- Mistral Large/Small/Mixtral/Ministral
- Cohere Command R/R+
- AI21 Jamba
Model Aliases
Define short names for frequently used model IDs in config/bedrock.php:
'aliases' => [ 'claude' => 'anthropic.claude-sonnet-4-20250514-v1:0', 'haiku' => 'anthropic.claude-3-5-haiku-20241022-v1:0', 'nova' => 'amazon.nova-pro-v1:0', ],
Use aliases anywhere a model ID is accepted:
Bedrock::invoke('claude', 'You are helpful.', 'Hello!'); $builder = Bedrock::conversation('haiku'); // Register aliases at runtime Bedrock::aliases()->register('fast', 'anthropic.claude-3-5-haiku-20241022-v1:0');
Conversation Builder
Fluent API for multi-turn conversations:
$conversation = Bedrock::conversation('claude') ->system('You are a medical assistant.') ->user('What causes headaches?') ->maxTokens(2048) ->temperature(0.5); // Send and get response $result = $conversation->send(); echo $result['response']; // Continue the conversation (assistant response is auto-added) $conversation->user('Tell me more about migraines'); $result2 = $conversation->send(); // Estimate tokens and cost before sending $estimate = $conversation->estimate(); echo "Estimated input tokens: {$estimate['input_tokens']}"; echo "Fits in context: " . ($estimate['fits'] ? 'yes' : 'no'); echo "Estimated cost: \${$estimate['estimated_cost']}"; // Reset conversation (keeps system prompt and settings) $conversation->reset();
Converse API
The AWS Converse API provides a unified interface across all model providers—no need to handle different request formats:
// Direct converse call $result = Bedrock::converse( modelId: 'anthropic.claude-sonnet-4-20250514-v1:0', messages: [ ['role' => 'user', 'content' => 'What is PHP?'], ['role' => 'assistant', 'content' => 'PHP is a programming language.'], ['role' => 'user', 'content' => 'What makes it special?'], ], systemPrompt: 'You are a helpful teacher.', maxTokens: 1024 ); // Or get the client directly $converseClient = Bedrock::converseClient(); $result = $converseClient->converse($modelId, $messages);
Streaming Responses
Stream responses in real-time for chat UIs and long outputs:
// Stream with InvokeModel API $result = Bedrock::stream( modelId: 'anthropic.claude-sonnet-4-20250514-v1:0', systemPrompt: 'You are helpful.', userMessage: 'Write a poem about PHP.', onChunk: function (string $chunk, array $metadata) { echo $chunk; // Print each token as it arrives flush(); } ); // Stream with Converse API $streamingClient = Bedrock::streamingClient(); $result = $streamingClient->converseStream( modelId: 'anthropic.claude-sonnet-4-20250514-v1:0', messages: [['role' => 'user', 'content' => 'Hello!']], onChunk: fn(string $text) => echo $text, systemPrompt: 'Be concise.' );
Token Estimation
Estimate token usage and cost before making API calls:
use Ubxty\BedrockAi\Support\TokenEstimator; // Estimate tokens in a string $tokens = TokenEstimator::estimate($text); // Full invocation estimation $estimation = TokenEstimator::estimateInvocation( systemPrompt: $system, userMessage: $user, modelId: 'anthropic.claude-sonnet-4-v1:0', maxOutputTokens: 4096 ); echo "Input tokens: ~{$estimation['input_tokens']}"; echo "Fits in context: " . ($estimation['fits'] ? 'yes' : 'no'); echo "Available output tokens: {$estimation['available_output']}"; // Estimate cost $cost = TokenEstimator::estimateCost($system, $user, 1000, [ 'input_price_per_1k' => 0.003, 'output_price_per_1k' => 0.015, ]); echo "Estimated cost: \${$cost}";
Listing Models
// Raw model summaries from Bedrock $raw = Bedrock::listModels(); // Normalized with specs $models = Bedrock::fetchModels(); foreach ($models as $model) { echo "{$model['name']} ({$model['model_id']}) - Context: {$model['context_window']}"; }
Testing Connection
$result = Bedrock::testConnection(); if ($result['success']) { echo "Connected! Found {$result['model_count']} models in {$result['response_time']}ms"; }
Pricing Data
$pricing = Bedrock::pricing()->getPricing(); foreach ($pricing as $modelId => $data) { echo "{$data['model_name']}: Input \${$data['input_price']}/1K, Output \${$data['output_price']}/1K\n"; } // Force refresh from AWS $fresh = Bedrock::pricing()->refreshPricing();
Usage Tracking
$tracker = Bedrock::usage(); // Active models from CloudWatch $models = $tracker->getActiveModels(); // Aggregated usage $usage = $tracker->getAggregatedUsage(days: 30); foreach ($usage as $modelId => $data) { echo "{$modelId}: {$data['invocations']} calls, {$data['total_tokens']} tokens\n"; } // Daily trend for charts $trend = $tracker->getDailyTrend(30); // Cost estimation $costs = $tracker->calculateCosts($usage, $pricingMap); echo "Total cost: \${$costs['total_cost']}";
Events
The package fires Laravel events for observability. Listen to them in your EventServiceProvider or with closures:
use Ubxty\BedrockAi\Events\BedrockInvoked; use Ubxty\BedrockAi\Events\BedrockRateLimited; use Ubxty\BedrockAi\Events\BedrockKeyRotated; // In a listener or closure Event::listen(BedrockInvoked::class, function (BedrockInvoked $event) { // $event->modelId, $event->inputTokens, $event->outputTokens // $event->cost, $event->latencyMs, $event->keyUsed, $event->connection }); Event::listen(BedrockRateLimited::class, function (BedrockRateLimited $event) { // $event->modelId, $event->keyLabel, $event->retryAttempt, $event->waitSeconds Notification::send($admin, new RateLimitAlert($event)); }); Event::listen(BedrockKeyRotated::class, function (BedrockKeyRotated $event) { // $event->fromKeyLabel, $event->toKeyLabel, $event->reason, $event->modelId });
Invocation Logging
Auto-log every Bedrock invocation for auditing and cost tracking:
BEDROCK_LOGGING_ENABLED=true BEDROCK_LOG_CHANNEL=bedrock
In config/bedrock.php:
'logging' => [ 'enabled' => env('BEDROCK_LOGGING_ENABLED', false), 'channel' => env('BEDROCK_LOG_CHANNEL', 'stack'), ],
Each invocation logs: model ID, tokens (input/output/total), cost, latency, status, and key used.
Health Check Route
Register a health check endpoint for monitoring dashboards:
BEDROCK_HEALTH_CHECK_ENABLED=true
In config/bedrock.php:
'health_check' => [ 'enabled' => env('BEDROCK_HEALTH_CHECK_ENABLED', false), 'path' => '/health/bedrock', 'middleware' => ['auth:sanctum'], // optional ],
The endpoint returns:
{
"status": "healthy",
"message": "Connection successful! Found 42 available models.",
"response_time_ms": 350,
"model_count": 42
}
CLI Commands
bedrock:configure
Interactive wizard for first-time setup.
php artisan bedrock:configure # Show current config (masked secrets) php artisan bedrock:configure --show # Auto-test after configuring php artisan bedrock:configure --test
What it does:
- Prompts for AWS Key, Secret, Region, Label
- Auto-detects SDK vs HTTP Bearer mode
- Optionally configures Pricing API credentials
- Generates
.enventries - Optionally writes to
.envautomatically - Tests the connection
bedrock:test
Test connection and optionally invoke a model.
# Basic connection test php artisan bedrock:test # Test a specific model php artisan bedrock:test anthropic.claude-3-5-sonnet-20241022-v2:0 # Custom prompt php artisan bedrock:test anthropic.claude-3-5-sonnet-20241022-v2:0 --prompt="Explain gravity" # Test all credential keys php artisan bedrock:test --all-keys # JSON output php artisan bedrock:test anthropic.claude-3-5-sonnet-20241022-v2:0 --json
bedrock:models
List available foundation models.
# All models php artisan bedrock:models # Filter by name/ID php artisan bedrock:models --filter=claude # Filter by provider php artisan bedrock:models --provider=anthropic # JSON output php artisan bedrock:models --json
bedrock:usage
View CloudWatch usage metrics.
# Last 30 days (default) php artisan bedrock:usage # Custom time range php artisan bedrock:usage --days=7 # Show daily breakdown php artisan bedrock:usage --daily # JSON output php artisan bedrock:usage --json
bedrock:pricing
Fetch real-time pricing from the AWS Pricing API.
# All models php artisan bedrock:pricing # Filter php artisan bedrock:pricing --filter=claude # Force refresh (bypass cache) php artisan bedrock:pricing --refresh # JSON output php artisan bedrock:pricing --json
Architecture
Cross-Region Inference Profiles
Newer models (Claude 3.5+, Nova, Llama 3.3+, Claude 4) cannot be invoked directly. They require cross-region inference profiles with a us. or eu. prefix. This package handles this automatically:
anthropic.claude-3-5-sonnet-20241022-v2:0
→ us.anthropic.claude-3-5-sonnet-20241022-v2:0 (in us-east-1)
→ eu.anthropic.claude-3-5-sonnet-20241022-v2:0 (in eu-west-1)
Models that require inference profiles:
anthropic.claude-3-5-*anthropic.claude-3-7-*anthropic.claude-sonnet-4*,claude-opus-4*,claude-haiku-4*amazon.nova-*meta.llama3-3*,meta.llama4*
You can register additional patterns:
use Ubxty\BedrockAi\Client\InferenceProfileResolver; InferenceProfileResolver::addPattern('custom.model-prefix-');
Multi-Key Rotation & Retry
Request → Key 1 → Rate Limited → Retry (2s) → Retry (4s) → Retry (8s) → Key 2 → Success
- Each key gets up to
max_retriesattempts (default: 3) with exponential backoff - On persistent failure, the next key is tried
- All keys exhausted →
RateLimitExceptionorBedrockException
HTTP Bearer Token Mode (ABSK)
If your AWS key or secret starts with ABSK, the client automatically uses HTTP Bearer token mode instead of the AWS SDK. This is useful for:
- Bedrock API keys distributed via AWS console
- Environments without full IAM credentials
Model Spec Resolution
The ModelSpecResolver maps model IDs to known context windows and max token limits:
use Ubxty\BedrockAi\Models\ModelSpecResolver; $specs = ModelSpecResolver::resolve('anthropic.claude-3-5-sonnet-20241022-v2:0'); // ['context_window' => 200000, 'max_tokens' => 8192]
Error Handling
The package provides specific exception types:
use Ubxty\BedrockAi\Exceptions\BedrockException; use Ubxty\BedrockAi\Exceptions\RateLimitException; use Ubxty\BedrockAi\Exceptions\ConfigurationException; use Ubxty\BedrockAi\Exceptions\CostLimitExceededException; try { $result = Bedrock::invoke($modelId, $system, $user); } catch (RateLimitException $e) { // All keys exhausted after retries // $e->getModelId(), $e->getKeyLabel() } catch (CostLimitExceededException $e) { // Daily or monthly limit exceeded // $e->getLimitType(), $e->getLimit(), $e->getCurrentSpend() } catch (ConfigurationException $e) { // Missing credentials or connection } catch (BedrockException $e) { // General Bedrock errors (model not found, access denied, etc.) // User-friendly messages are automatically extracted }
Raw Bedrock errors are automatically mapped to user-friendly messages:
| Raw Error | Friendly Message |
|---|---|
model identifier is invalid |
Invalid model: This model ID is not valid for Bedrock. |
doesn't support on-demand throughput |
Model unavailable: This model requires provisioned throughput. |
Malformed input request |
Request error: This model may not support text chat. |
end of its life |
Model deprecated: This model version has been retired. |
AccessDeniedException |
Access denied: You don't have permission to use this model. |
ResourceNotFoundException |
Model not found: The requested model does not exist in this region. |
API Reference
BedrockManager
| Method | Returns | Description |
|---|---|---|
client(?string $connection) |
BedrockClient |
Get a client for the given connection |
invoke(string $modelId, ...) |
array |
Invoke a model on the default connection |
converse(array $messages, ...) |
array |
Invoke via the Converse API |
converseClient(?string $connection) |
ConverseClient |
Get a Converse API client |
stream(string $modelId, ..., callable $onChunk) |
array |
Stream a model response |
streamingClient(?string $connection) |
StreamingClient |
Get a streaming client |
conversation(?string $modelId) |
ConversationBuilder |
Start a fluent conversation |
aliases() |
ModelAliasResolver |
Get the alias resolver |
resolveAlias(string $alias) |
string |
Resolve an alias to a model ID |
getLogger() |
InvocationLogger |
Get the invocation logger |
testConnection(?string $connection) |
array |
Test connection |
listModels(?string $connection) |
array |
List raw model summaries |
fetchModels(?string $connection) |
array |
List normalized models with specs |
pricing() |
PricingService |
Get the pricing service |
usage() |
UsageTracker |
Get the usage tracker |
isConfigured(?string $connection) |
bool |
Check if configured |
BedrockClient::invoke() Return Value
[
'response' => string, // The model's text response
'input_tokens' => int, // Tokens in the prompt
'output_tokens' => int, // Tokens in the response
'total_tokens' => int, // input + output
'cost' => float, // Estimated cost in USD
'latency_ms' => int, // End-to-end latency in milliseconds
'status' => 'success', // Always 'success' (failures throw)
'key_used' => string, // Label of the credential key that worked
'model_id' => string, // Resolved model ID (with inference prefix if applied)
]
PricingService
| Method | Returns | Description |
|---|---|---|
getPricing() |
array |
Cached pricing data |
refreshPricing() |
array |
Force-refresh from AWS |
testConnection() |
array |
Test Pricing API access |
UsageTracker
| Method | Returns | Description |
|---|---|---|
getActiveModels() |
array |
Models with CloudWatch metrics |
getModelUsage(string $modelId, int $days) |
array |
Per-model daily metrics |
getAggregatedUsage(int $days) |
array |
Aggregated across all models |
calculateCosts(array $usage, array $pricingMap) |
array |
Cost estimation |
getDailyTrend(int $days) |
array |
Daily breakdown for charts |
testConnection() |
array |
Test CloudWatch access |
Testing
The package includes 179 tests with 346 assertions covering all components.
cd packages/ubxty/bedrock-ai
composer install
./vendor/bin/phpunit
License
MIT License. See LICENSE for details.