korozcolt / ai-gateway
A multi-provider AI gateway for Laravel: provider connections and encrypted API tokens stored in the database, Anthropic (Claude), OpenAI, OpenAI-compatible (OpenRouter, NVIDIA Build, Azure, Mistral, Groq), Gemini and fake drivers, multimodal requests and usage metering.
Requires
- php: ^8.3
- illuminate/console: ^13.0
- illuminate/contracts: ^13.0
- illuminate/database: ^13.0
- illuminate/encryption: ^13.0
- illuminate/http: ^13.0
- illuminate/queue: ^13.0
- illuminate/support: ^13.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.27
- orchestra/testbench: ^11.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
A multi-provider AI gateway for Laravel. Provider connections and their API tokens live in your database (tokens encrypted with a dedicated key), so adding the tenth provider never touches .env. One contract, interchangeable drivers (Anthropic Claude, OpenAI, OpenAI-compatible APIs such as OpenRouter and NVIDIA Build, Gemini), multimodal requests (text, images, PDFs), per-call usage and cost metering, and failures that expose closed reason codes only, never prompts, responses or keys.
Developed and used in production projects at KOR Bytes S.A.S. and published so anyone can evaluate and use it.
Available on Packagist: korozcolt/ai-gateway · Source: github.com/korozcolt/ai-gateway · License: MIT · Latest release: see Releases
Part of the Korozcolt / KOR Bytes ecosystem
| Package | What it is |
|---|---|
| ai-gateway (this package) | AI provider gateway for Laravel |
| payments | Unified payment gateway (Wompi, MercadoPago, ePayco) for Laravel and any PHP project |
Both follow the same conventions: Composer vendor korozcolt/, PHP namespace Korbytes\, MIT license, Pest + Larastan + Pint, Keep a Changelog, GitHub Actions.
Features
- Connections in the database - URL, token, models, prices and request options per provider, managed with Eloquent, a seed file, or your own admin panel
- Encrypted tokens, one key outside the DB - AES-256-GCM with a dedicated key (never
APP_KEY), key id and rotation with retired keys - Driver architecture - closed driver list in config; adding a driver is one class and one line
- Multimodal -
AiContentPart::text(),::image(),::file()(PDF) mapped to each provider's wire format - Structured output - JSON Schema via
response_formatwhen the connection supports it, otherwise requested in the system prompt; decoded JSON on the response - Usage metering - one append-only ledger row per call (tokens, thinking/cached tokens, price snapshot, cost, provider-reported cost, latency, outcome), written through the queue so it survives a rolled-back caller transaction. No prompt or response content is ever stored
- Closed failure taxonomy -
AiFailurereason codes; exceptions never carry prompt, response or key text - Per-model options - e.g.
max_completion_tokensand no temperature for reasoning models (defaulted for OpenAI o-series and gpt-5) - Model listing - optional
ListsModelscapability (Anthropic, OpenAI) to sync the models an account can use; the app decides which ids to keep - Seeding - import providers and tokens from a JSON file kept out of git; tokens are encrypted on insert; idempotent
- Testable -
FakeAiProviderruns your whole app without network - Laravel 13 - PHP 8.3+, PostgreSQL
Supported providers
| Provider | Driver | JSON Schema | Images / PDF | Status |
|---|---|---|---|---|
| OpenRouter | openai_compatible |
Model dependent (supports_json_schema) |
Yes (image_url, file data URIs) |
Connected in KOR Bytes projects |
| NVIDIA Build | openai_compatible |
Off: schema is requested in the system prompt | Model dependent | Connected in KOR Bytes projects. Hosted trial endpoints are for evaluation only: review the provider's retention and training terms before sending personal data |
| Azure OpenAI (v1) / Mistral / Groq / any OpenAI-compatible API | openai_compatible |
Per connection | Per model | Same wire format; not verified one by one |
| Anthropic Claude | anthropic |
Yes (forced tool use, input_schema) |
Yes (base64 images, PDF documents) | Covered by HTTP-faked tests; no live evaluation yet |
| OpenAI (ChatGPT) | openai |
Yes (strict json_schema) |
Yes (image_url, file data URIs) |
Covered by HTTP-faked tests; no live evaluation yet |
| Google Gemini | gemini |
Yes (responseJsonSchema) |
Yes (inlineData) |
Covered by HTTP-faked tests; no live evaluation yet |
| Fake | fake |
n/a | n/a | Deterministic driver for tests and local work |
Whether a given model supports vision, PDFs, JSON Schema or tool-free structured output depends on the model, not on this package: declare what each connection supports and verify with your own data.
Requirements
- PHP 8.3+
- Laravel 13
- PostgreSQL (sequence-generated codes,
jsonband guard triggers)
Installation
The package is published on Packagist; no extra repository configuration is needed.
composer require korozcolt/ai-gateway php artisan migrate php artisan ai:generate-key
ai:generate-key prints the only environment variables you need; copy them into .env:
AI_CREDENTIALS_KEY_ID=... AI_CREDENTIALS_KEY=base64:...
Optionally publish the configuration and migrations:
php artisan vendor:publish --tag=ai-gateway-config php artisan vendor:publish --tag=ai-gateway-migrations
See INSTALL.md for key rotation, database roles and queue notes.
Quick Start
1. Add a provider and its token (seed file)
Create database/seeders/data/ai-providers.json (it is git-ignored in the package and should be in yours) and import it. Tokens are encrypted before they reach the database.
{
"providers": [
{
"name": "OpenRouter",
"driver": "openai_compatible",
"base_url": "https://openrouter.ai/api/v1",
"api_key": "REPLACE_ME",
"supports_json_schema": true,
"is_active": true,
"request_options": {"provider": {"data_collection": "deny", "zdr": true}},
"models": [{"id": "vendor/model", "input_price_usd_per_million": 1.5, "output_price_usd_per_million": 6}]
},
{
"name": "Google Gemini",
"driver": "gemini",
"base_url": "https://generativelanguage.googleapis.com/v1beta",
"api_key": "REPLACE_ME",
"supports_json_schema": true,
"is_active": true,
"models": [{"id": "gemini-model-id", "input_price_usd_per_million": 0.3, "output_price_usd_per_million": 2.5}]
},
{
"name": "Anthropic Claude",
"driver": "anthropic",
"base_url": "https://api.anthropic.com/v1",
"api_key": "REPLACE_ME",
"supports_json_schema": true,
"is_active": true,
"models": [{"id": "claude-model-id", "input_price_usd_per_million": 3, "output_price_usd_per_million": 15}]
},
{
"name": "OpenAI",
"driver": "openai",
"base_url": "https://api.openai.com/v1",
"api_key": "REPLACE_ME",
"supports_json_schema": true,
"is_active": true,
"models": [{"id": "gpt-model-id", "input_price_usd_per_million": 2.5, "output_price_usd_per_million": 10}]
}
]
}
php artisan ai:providers:import
or call Korbytes\AiGateway\Database\Seeders\AiProvidersSeeder from your DatabaseSeeder.
2. Make a call
use Korbytes\AiGateway\Dto\{AiContentPart, AiMessage, AiParams, AiRequest}; use Korbytes\AiGateway\Facades\AiGateway; $driver = AiGateway::provider('AIP-0002'); // already wrapped with the usage meter $response = $driver->complete(new AiRequest( system: 'Extract the fields of the document.', messages: [new AiMessage('user', [ AiContentPart::text('Identity card'), AiContentPart::image($base64, 'image/jpeg'), ])], params: new AiParams(model: 'vendor/model', temperature: 0.0, maxOutputTokens: 2000, timeoutSeconds: 60), jsonSchema: $schema, purpose: 'extraction', contextType: 'case', contextId: 123, )); $response->text; // raw text $response->json; // decoded array when a jsonSchema was requested (validate it in your app) $response->costUsd; // cost from the connection's price at call time
3. Handle failures
use Korbytes\AiGateway\Exceptions\AiProviderException; try { $response = $driver->complete($request); } catch (AiProviderException $e) { $e->reasonCode; // 'rate_limit', 'timeout', 'auth', ... }
Documentation
- Installation Guide - key generation and rotation, database roles, queues
- Usage Guide - connections, requests, multimodal, structured output, metering, extending
- Architecture - design decisions
- Releasing - tagging and Packagist
- Changelog - version history
Configuration
Environment variables
AI_CREDENTIALS_KEY_ID=... # generated by `php artisan ai:generate-key` AI_CREDENTIALS_KEY=base64:... # must differ from APP_KEY AI_CREDENTIALS_RETIRED_KEYS={"id":"base64:..."} # only while rotating
Provider tokens are not environment variables: they are stored encrypted in ai_providers. config/ai-gateway.php also exposes drivers, credentials.distinct_from, usage.queue and seed_file.
Failure codes
| Code | Meaning |
|---|---|
auth |
Invalid or unauthorized credentials (401/402/403, Gemini API_KEY_INVALID) |
rate_limit |
429 |
timeout |
Request timed out (or 202/408/504) |
invalid_request |
Provider rejected the request (400/404/413/416/422) |
content_filtered |
Blocked by the provider's safety filters |
server_error |
5xx |
connection |
Network failure |
bad_response |
Unparseable or empty provider answer |
missing_credentials |
The connection has no token |
credentials_unavailable |
The token cannot be decrypted (key not configured, rotated without keeping the retired key, or corrupt ciphertext) |
unknown_driver / unknown_provider / provider_inactive |
Connection could not be resolved |
unknown_model |
The model is not declared in the connection's models |
API Reference
Facade
AiGateway::available(); // codes of active connections AiGateway::modelsFor('AIP-0002'); // declared model ids AiGateway::connection('AIP-0002'); // AiProvider model (active only) AiGateway::provider('AIP-0002'); // metered driver AiGateway::canListModels('AIP-0002'); // does the driver support ListsModels? AiGateway::listModels('AIP-0002'); // model ids the remote account offers (not metered)
DTOs
new AiRequest(system: string, messages: list<AiMessage>, params: AiParams, jsonSchema: ?array, purpose: string = 'general', contextType: ?string, contextId: ?int); new AiParams(model: string, temperature: float, maxOutputTokens: int, timeoutSeconds: int); new AiMessage(role: 'user'|'assistant', content: string|list<AiContentPart>);
AiResponse: text, json, inputTokens, outputTokens, thinkingTokens, cachedTokens, latencyMs, model, finishReason, costUsd, reportedCostUsd.
Extending
use Korbytes\AiGateway\Providers\HttpProvider; final class MyProvider extends HttpProvider { public function key(): string { return 'my_provider'; } protected function send(PendingRequest $http, AiRequest $request): Response { /* ... */ } protected function parse(array $body): array { /* [text, AiUsage, AiFinishReason] */ } } // config/ai-gateway.php 'drivers' => [..., 'my_provider' => MyProvider::class],
Korbytes\AiGateway\Contracts\ListsModels is an optional interface (listModels(): array) a driver can implement next to AiProviderInterface. Drivers are always handed out wrapped by the usage meter, so use AiGateway::canListModels() / AiGateway::listModels() rather than instanceof on the result of provider() (the wrapper itself implements the interface and delegates).
Roadmap
listModels()for Gemini (deferred past 0.2.0)- Fallback chain across connections/models (today: do it in your application, see USAGE.md)
- Enforcement of
budget_usd(stored today for reporting; not enforced) - Optional Filament resource to manage connections
- Wider Laravel/PHP support (v0.x targets Laravel 13 / PHP 8.3)
Testing
composer test
Tests need PostgreSQL (ai_gateway_test, see phpunit.xml.dist; override locally in an ignored phpunit.xml).
Security
Never put provider tokens in .env or in your repository: use the encrypted database store. Keep the seed JSON out of git. If you discover a security issue, please follow SECURITY.md.
Credits
License
Released under the MIT License - Copyright (c) 2026 Korozcolt. You may use, copy, modify, merge, publish, distribute, sublicense and sell it, in personal, open-source and commercial projects, provided the copyright and license notice are included in all copies or substantial portions. The software is provided "as is", without warranty of any kind. See the License File for the full text.
Third-party AI providers you connect through this package have their own terms of service, pricing and data policies; you are responsible for complying with them.
🏢 Maintained by KOR Bytes S.A.S.
Need enterprise Laravel development, AI integrations, or SaaS solutions in Colombia and LATAM?
- 🌐 Website: kor-bytes.com
- 💬 WhatsApp: +57 304 397 8157
- ✉️ Email: gerencia@kor-bytes.com