bherila / genai-laravel
Provider-agnostic GenAI client for Laravel: Gemini, AWS Bedrock, and more
Requires
- php: ^8.2
- illuminate/contracts: ^12|^13
- illuminate/http: ^12|^13
- illuminate/support: ^12|^13
Requires (Dev)
- dompdf/dompdf: ^3
- laravel/pint: ^1
- orchestra/testbench: ^10|^11
- phpoffice/phpspreadsheet: ^4|^5
- phpoffice/phpword: ^1
- phpstan/phpstan: ^2
- phpunit/phpunit: ^10|^11
Suggests
- dompdf/dompdf: Required together with phpoffice/phpword to render DOC / DOCX / ODT / RTF to PDF before sending — preserves formatting and lets providers use their native PDF pipelines (Gemini vision, Anthropic document blocks). Not loaded if absent.
- phpoffice/phpspreadsheet: Enables automatic XLSX / XLS / ODS / CSV → plain-text conversion when sending spreadsheet files to Anthropic or Gemini (^4|^5). Not loaded if absent.
- phpoffice/phpword: Enables automatic DOC / DOCX / ODT / RTF → PDF conversion (when dompdf/dompdf is also installed) before sending to Anthropic or Gemini. Not loaded if absent.
- sentry/sentry: Enables optional Sentry AI monitoring instrumentation when using a Sentry PHP SDK version that supports SpanContext::make (^4.23).
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-08 05:15:15 UTC
README
Provider-agnostic GenAI client for Laravel. Supports Google Gemini, AWS Bedrock (Claude), and Anthropic direct API through a single interface.
Installation
composer require bherila/genai-laravel
Publish the config:
php artisan vendor:publish --tag=genai-config
Configuration
Set your provider in .env:
# Default provider: gemini, bedrock, or anthropic GENAI_PROVIDER=gemini # Gemini GEMINI_API_KEY=your-key GEMINI_MODEL=gemini-2.0-flash # Bedrock — uses Bearer-token auth, not AWS SigV4 BEDROCK_API_KEY=your-bedrock-bearer-token BEDROCK_SESSION_TOKEN= # optional, for temporary credentials BEDROCK_REGION=us-east-1 BEDROCK_MODEL=us.anthropic.claude-haiku-4-20250514-v1:0 # Anthropic ANTHROPIC_API_KEY=your-key ANTHROPIC_MODEL=claude-sonnet-4-6 ANTHROPIC_MAX_TOKENS=8192
Bedrock auth: this package authenticates against Bedrock with a bearer token (
Authorization: Bearer …), not AWS SigV4.BEDROCK_API_KEYis the bearer token itself — there is no separateBEDROCK_SECRET_KEY. If you are coming from the AWS SDK and have IAM access-key-ID + secret-access-key credentials, those are not the right shape for this package; use a Bedrock bearer token instead.
Usage
Fluent builder (recommended)
GenAiRequest provides a uniform call site regardless of provider. Pass any GenAiClient to ::with() — the rest of the chain is identical.
use Bherila\GenAiLaravel\GenAiRequest; use Bherila\GenAiLaravel\Clients\GenAiClientFactory; $client = GenAiClientFactory::make('anthropic'); // or 'bedrock', 'gemini' $response = GenAiRequest::with($client) ->system('You are a financial analyst.') ->withFile(base64_encode(file_get_contents($path)), 'application/pdf') ->prompt('Extract key figures.') ->generate(); echo $response->text; // or foreach ($response->toolCalls as $call) { // ['name' => 'extract_data', 'input' => [...]] }
Using multiple providers in one application
// Different tenants, different providers — call site is identical $client = match ($user->ai_provider) { 'anthropic' => new AnthropicClient(apiKey: $user->anthropic_key, model: 'claude-sonnet-4-6'), 'bedrock' => new BedrockClient(apiKey: $creds->key, modelId: $creds->model, region: 'us-east-1'), default => new GeminiClient(apiKey: $user->gemini_key), }; $response = GenAiRequest::with($client) ->system($systemPrompt) ->withFiles($files) // [['base64' => '...', 'mimeType' => 'application/pdf'], ...] ->prompt($userPrompt) ->tools($toolConfig) ->generate();
Tool calling
Define tools once with Schema + ToolDefinition. Each client converts to its native wire format internally.
use Bherila\GenAiLaravel\Schema; use Bherila\GenAiLaravel\ToolChoice; use Bherila\GenAiLaravel\ToolConfig; use Bherila\GenAiLaravel\ToolDefinition; $toolConfig = new ToolConfig( tools: [ new ToolDefinition( name: 'extract_invoice', description: 'Extract invoice fields', inputSchema: Schema::object([ 'vendor' => Schema::string('Vendor name'), 'amount' => Schema::number('Total amount due'), 'due_date' => Schema::string('Due date in YYYY-MM-DD'), ], required: ['vendor', 'amount']), ), ], choice: ToolChoice::any(), ); $response = GenAiRequest::with($client) ->withFile($base64, 'application/pdf') ->prompt('Extract the invoice data.') ->tools($toolConfig) ->generate(); $call = $response->toolCallByName('extract_invoice'); // ['name' => 'extract_invoice', 'input' => ['vendor' => 'Acme', 'amount' => 1500.00, ...]]
Schema helpers
Schema::string('Optional description') Schema::number() Schema::integer() Schema::boolean() Schema::object(['field' => Schema::string()], required: ['field']) Schema::arrayOf(Schema::string()) Schema::enum(['a', 'b', 'c'], 'Pick one') Schema::fromArray(['type' => 'string', 'format' => 'date']) // wrap raw JSON Schema
Tool choice
ToolChoice::auto() // model decides whether to call a tool ToolChoice::any() // model must call at least one tool ToolChoice::none() // model must not call any tool ToolChoice::tool('my_fn') // model must call this specific tool
Gemini File API (large files)
$fileRef = $client->uploadFile($stream, 'application/pdf', 'report.pdf'); try { $response = GenAiRequest::with($client) ->messages([[ 'role' => 'user', 'content' => [ContentBlock::text('Summarise this report.')], ]]) ->generate(); // or use the lower-level method directly: $raw = $client->converseWithFileRef($fileRef, 'application/pdf', 'Summarise.'); } finally { $client->deleteFile($fileRef); }
Dependency injection (single provider)
When your app uses one provider, bind it in a service provider and inject GenAiClient:
// AppServiceProvider $this->app->singleton(GenAiClient::class, fn () => GenAiClientFactory::make());
use Bherila\GenAiLaravel\Contracts\GenAiClient; class MyService { public function __construct(private readonly GenAiClient $ai) {} public function analyse(string $text): string { return GenAiRequest::with($this->ai) ->prompt($text) ->generate() ->text; } }
Facade
use Bherila\GenAiLaravel\Facades\GenAi; $response = GenAi::converse($system, $messages, $toolConfig);
GenAiResponse
generate() always returns a GenAiResponse:
| Property / method | Description |
|---|---|
->text |
Concatenated text output |
->toolCalls |
[['name' => '...', 'input' => [...]], ...] |
->usage |
Normalised Usage (tokens, cache tokens) — see below |
->raw |
Provider-specific raw response array |
->hasToolCalls() |
Whether the model called any tool |
->firstToolCall() |
First tool call, or null |
->toolCallByName('fn') |
Named tool call, or null |
Token usage and cost
Every response exposes a Usage object with provider-agnostic token counts. The
clients normalise the three different wire shapes (Anthropic input_tokens /
Bedrock inputTokens / Gemini promptTokenCount) into one API:
$response = GenAiRequest::with($client)->prompt('...')->generate(); $response->usage->inputTokens; // non-cached prompt tokens $response->usage->outputTokens; // completion tokens $response->usage->totalTokens; $response->usage->cacheReadInputTokens; // served from prompt cache $response->usage->cacheCreationInputTokens; // written to prompt cache $response->usage->raw; // provider-specific payload // Estimate cost in USD given per-million-token prices for the model you used. $cost = $response->usage->estimatedCostUsd( inputPerMillion: 3.00, outputPerMillion: 15.00, cacheReadPerMillion: 0.30, cacheCreationPerMillion: 3.75, );
The three input buckets are non-overlapping (the Gemini adapter subtracts
cachedContentTokenCount from promptTokenCount to match Anthropic/Bedrock
semantics), so summing them gives total input work billed.
Retry behaviour
All providers retry transient failures transparently. 429 honors the
Retry-After: <seconds> response header; 502 / 503 / 504 use exponential
backoff. 400 / 401 / 403 / 404 are never retried. After the budget is spent,
GenAiRateLimitException::$retryAfter carries the last server-suggested delay
so you can re-queue work.
GENAI_RETRY_MAX_ATTEMPTS=3 # total attempts including the first; 1 disables retries GENAI_RETRY_BACKOFF_BASE_MS=1000 # exponential backoff base (no Retry-After header) GENAI_RETRY_BACKOFF_MAX_MS=30000 # cap on any single sleep
Override per client by passing a RetryStrategy to the constructor — useful in
tests, where injecting a sleeper closure keeps the suite fast:
use Bherila\GenAiLaravel\Http\RetryStrategy; new AnthropicClient( apiKey: '...', retry: new RetryStrategy(maxAttempts: 1), // disable retries );
Listing models
Every client implements listModels(): ModelInfo[], hitting each provider's
catalog endpoint and normalising the result:
$client = GenAi::client('anthropic'); // or 'bedrock', 'gemini' foreach ($client->listModels() as $model) { $model->id; // call-ready identifier $model->name; // human-readable display name $model->provider; // "anthropic" | "bedrock" | "gemini" $model->description; // free-form, when provided $model->inputTokenLimit; // context window, when advertised $model->outputTokenLimit; // max completion tokens, when advertised $model->inputCostPerMillionTokens; // null — no provider returns pricing $model->outputCostPerMillionTokens; // null — no provider returns pricing $model->raw; // provider-specific entry }
Endpoints used: Anthropic GET /v1/models, Bedrock
GET https://bedrock.{region}.amazonaws.com/foundation-models (control-plane,
not bedrock-runtime), Gemini GET /v1beta/models. Gemini entries that don't
support generateContent (embeddings, etc.) are filtered out. None of the
provider catalog APIs currently return pricing, so the cost fields are nullable
— populate them yourself via PricingBook if you need cost tracking alongside
model selection.
Pricing table (PricingBook)
Supply your own per-million-token prices for any of the three providers
(anthropic, bedrock, gemini) and the package will both decorate
ModelInfo and turn Usage records into dollar costs:
use Bherila\GenAiLaravel\PricingBook; $book = PricingBook::fromArray([ 'anthropic' => [ 'claude-sonnet-4-6' => ['input' => 3.0, 'output' => 15.0, 'cache_read' => 0.3, 'cache_creation' => 3.75], ], 'bedrock' => [ 'us.anthropic.claude-haiku-4-20250514-v1:0' => ['input' => 0.8, 'output' => 4.0], ], 'gemini' => [ 'gemini-2.0-flash' => ['input' => 0.1, 'output' => 0.4], ], ]); // Decorate listModels() output with prices $models = $book->enrichAll($client->listModels()); // Compute cost for a specific call $cost = $book->estimateCost($response->usage, $client->provider(), $client->model());
PricingBook::fromConfig() reads the same shape from the genai.pricing config
key, so application-wide pricing can live alongside provider config. Existing
non-null cost fields on a ModelInfo are preserved by enrich(), and
estimateCost() / priceFor() return null when no price is registered for
the requested (provider, modelId).
File type support
Each provider accepts a different set of file formats natively. The clients
validate MIME types up front and fail fast with an actionable error rather than
round-tripping a request the API is going to reject. Images (PNG / JPEG / GIF /
WebP) are routed to the correct image block shape automatically.
For Anthropic and Gemini — which only accept PDF and text-type documents — this
package can auto-convert Office formats by treating phpoffice/phpword (+ a PDF
renderer) and phpoffice/phpspreadsheet as optional peer dependencies:
- Word docs (
.doc,.docx,.odt,.rtf) → PDF via PhpWord + Dompdf so layout, tables, and fonts survive. The rendered PDF is sent through Anthropic's native PDF pipeline or Gemini's PDF vision pipeline. - Spreadsheets (
.xlsx,.xls,.ods,.csv) → tab-separated text via PhpSpreadsheet. Cell data is emitted as a text block with a=== Sheet: <name> ===header per sheet.
Neither dependency is in require — when a peer is missing the client falls
back to a clear GenAiFatalException telling the caller what to install.
| MIME type | Gemini | Bedrock | Anthropic |
|---|---|---|---|
application/pdf |
✅ (vision) | ✅ document |
✅ document |
text/plain |
✅ | ✅ | ✅ document |
text/markdown |
✅ (text only) | ✅ | convert to text |
text/html |
✅ (text only) | ✅ | convert to text |
text/csv |
auto-convert 📊 | ✅ | auto-convert 📊 |
application/xml |
✅ (text only) | — | convert to text |
application/msword (.doc) |
auto-convert 📄 | ✅ | auto-convert 📄 |
.docx (…wordprocessingml.document) |
auto-convert 📄 | ✅ | auto-convert 📄 |
.odt (OpenDocument Text) |
auto-convert 📄 | — | auto-convert 📄 |
application/rtf |
auto-convert 📄 | — | auto-convert 📄 |
application/vnd.ms-excel (.xls) |
auto-convert 📊 | ✅ | auto-convert 📊 |
.xlsx (…spreadsheetml.sheet) |
auto-convert 📊 | ✅ | auto-convert 📊 |
.ods (OpenDocument Spreadsheet) |
auto-convert 📊 | — | auto-convert 📊 |
image/png, image/jpeg, image/gif, image/webp |
✅ inline_data |
✅ image block |
✅ image block |
- 📄 Word → PDF requires
phpoffice/phpwordand a PhpWord PDF renderer (dompdf/dompdfrecommended — alternatives:mpdf/mpdf,tecnickcom/tcpdf). Install withcomposer require phpoffice/phpword dompdf/dompdf. - 📊 Spreadsheet → text requires
phpoffice/phpspreadsheet. Install withcomposer require phpoffice/phpspreadsheet.
Bedrock natively accepts the Office formats via its own document block (the
Converse API lists pdf, csv, doc, docx, xls, xlsx, html, txt, md as native
formats), so no conversion runs for Bedrock requests.
Note: PowerPoint (
.ppt,.pptx,.odp) auto-conversion is not included in this PR — the only available PHP library (phpoffice/phppresentation) pins an olderphpoffice/phpspreadsheetversion that currently has open security advisories. Until that's resolved upstream, convert PowerPoint files to PDF yourself (e.g. vialibreoffice --convert-to pdf) before sending them.
Providers
| Feature | Gemini | Bedrock | Anthropic |
|---|---|---|---|
| File upload API | ✅ uploadFile() |
❌ inline only | ❌ inline only |
| Inline file bytes | ✅ | ✅ | ✅ |
| Tool/function calling | ✅ | ✅ | ✅ |
| File size limit | 20 MB | 4.5 MB | 4.5 MB |
| System prompts | ✅ | ✅ | ✅ |
listModels() |
✅ | ✅ (control-plane) | ✅ |
checkCredentials() |
✅ | ✅ | ✅ |
| Pricing in catalog | ❌ | ❌ | ❌ |
| Image blocks (PNG/JPEG/GIF/WebP) | ✅ | ✅ | ✅ |
| Office-format documents | auto-convert 📄📊 | ✅ native | auto-convert 📄📊 |
| Auto DOC/DOCX → PDF (with phpword + dompdf) | ✅ | n/a | ✅ |
| Auto XLSX/XLS/ODS/CSV → text (with phpspreadsheet) | ✅ | n/a | ✅ |
License
This package is released under the MIT License.