israrminhas / filament-aimonitor
AI API cost monitoring and key management plugin for Filament v4 & v5
Requires
- php: ^8.2
- filament/filament: ^4.0|^5.0
- illuminate/contracts: ^11.28|^12.0|^13.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- orchestra/testbench: ^9.0|^10.0|^11.0
- pestphp/pest: ^3.0|^4.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0
- pestphp/pest-plugin-livewire: ^3.0|^4.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
A Filament plugin (v4 and v5) for monitoring AI API usage, costs, and managing API keys across multiple providers (OpenAI, Anthropic, Gemini, Perplexity).
Features
- Track AI API requests with token counts and automatic cost calculation, including prompt-cache reads and writes
- Manage encrypted API keys for multiple providers with priority-based rotation
- Configure model-specific pricing with provider defaults, a global fallback, and automatic matching of dated model snapshots (
gpt-4o-2024-08-06→gpt-4o) - Price image, video and other non-token models per image, second or request
- Dashboard with period (7 / 30 / 90 / 365 days) and provider filters, usage analytics, and cost trends
- Recalculate costs for past requests after adding or changing pricing
- Per-user spending tracking and limits
- Multi-tenancy support (
tenant()helper, Filament tenancy, or a custom resolver) AiRequestLoggedevent for alerts, budgets, and integrations
Requirements
- PHP 8.2+
- Laravel 11.28+ (tested up to Laravel 13)
- Filament 4.x (Livewire 3) or Filament 5.x (Livewire 4)
Installation
composer require israrminhas/filament-aimonitor
Publish and run migrations:
php artisan vendor:publish --tag="ai-monitor-migrations"
php artisan migrate
Publish config (optional):
php artisan vendor:publish --tag="ai-monitor-config"
Import current list prices for popular OpenAI, Anthropic, Gemini and Perplexity models (optional):
php artisan ai-monitor:setup-pricing # skips models you already priced php artisan ai-monitor:setup-pricing --force # overwrite with the bundled prices
Provider prices change often. Check the imported values against each provider's pricing page.
Upgrading from v1.0
Pricing rows can now be priced per image, second or request (see Per-Unit Pricing), which adds a pricing_unit column to ai_model_pricings. Publish the new migration and run it:
php artisan vendor:publish --tag="ai-monitor-migrations"
php artisan migrate
Migrations you already published are skipped. Existing rows get the unit tokens, so their costs do not change.
Upgrading from the Filament 4 version
No database changes are needed. Update the package, then run php artisan filament:upgrade. If you had published the package's views, delete resources/views/vendor/ai-monitor and publish them again, since ai-monitor-dashboard.blade.php, ai-top-models-widget.blade.php and ai-user-usage-widget.blade.php were removed.
Register the Plugin
Add the plugin to your Filament panel in app/Providers/Filament/AdminPanelProvider.php:
use Filament\AiMonitor\AiMonitorPlugin; public function panel(Panel $panel): Panel { return $panel ->plugins([ AiMonitorPlugin::make(), ]); }
The plugin works on any panel ID and path; links are generated for the panel it is registered on.
Plugin Options
AiMonitorPlugin::make() ->navigationGroup('Observability') // null to ungroup; defaults to config('ai-monitor.navigation_group') ->navigationSort(10) // offset added to the sort of every AI Monitor nav item ->dashboard() // pass false to hide any of these ->requestsResource() ->pricingResource() ->apiKeysResource();
Styling
The plugin uses only Filament's built-in components and classes, so no custom theme or Tailwind @source entry is required.
Helper Functions
The package provides three global helper functions:
ai_log() - Log AI Requests
// Log any AI request ai_log([ 'provider' => 'openai', 'model' => 'gpt-4o', 'request_type' => 'chat', 'prompt_tokens' => 150, 'completion_tokens' => 50, 'status' => 'success', 'user_id' => auth()->id(), ]);
Cost is automatically calculated from your pricing configuration. The total_tokens and occurred_at are auto-filled if not provided.
ai_key() - Get API Key
// Get the highest priority active API key for a provider $apiKey = ai_key('openai'); $apiKey = ai_key('anthropic'); $apiKey = ai_key('gemini');
ai_cost() - Calculate Cost
// Calculate cost for tokens without logging $cost = ai_cost('openai', 'gpt-4o', 1000, 500); // Returns cost in USD based on your pricing config // With prompt-cache reads and writes (see Prompt Caching) $cost = ai_cost('anthropic', 'claude-sonnet-5', 2013, 134, cacheReadTokens: 0, cacheWriteTokens: 22568);
Using the Services
AiUsageLogger
use Filament\AiMonitor\Services\AiUsageLogger; $logger = app(AiUsageLogger::class); // Generic log $logger->log([ 'provider' => 'openai', 'model' => 'gpt-4o', 'prompt_tokens' => 100, 'completion_tokens' => 50, 'status' => 'success', 'user_id' => auth()->id(), 'meta' => ['conversation_id' => 123], ]); // Provider-specific shortcuts $logger->logOpenAi([...]); $logger->logAnthropic([...]); $logger->logGemini([...]); $logger->logPerplexity([...]);
AiKeyManager
use Filament\AiMonitor\Services\AiKeyManager; $keyManager = app(AiKeyManager::class); // Get single key (highest priority) $key = $keyManager->getKey('openai'); // Get all active keys for a provider $keys = $keyManager->getAllKeys('openai'); // Check if provider has any active keys if ($keyManager->hasProvider('anthropic')) { // ... }
AiPricingService
use Filament\AiMonitor\Services\AiPricingService; $pricing = app(AiPricingService::class); // Get pricing for a model $rates = $pricing->getPricing('openai', 'gpt-4o'); // Returns: ['input_per_1k' => 0.005, 'output_per_1k' => 0.015, 'pricing_unit' => 'tokens'] // Check if pricing exists if ($pricing->hasPricing('anthropic', 'claude-sonnet-5')) { // ... } // Calculate cost $cost = $pricing->calculateCost('openai', 'gpt-4o', 1000, 500); // Get all configured providers $providers = $pricing->getProviders(); // Get models for a provider $models = $pricing->getModelsForProvider('openai');
AiUsageLimitService
use Filament\AiMonitor\Services\AiUsageLimitService; $limitService = app(AiUsageLimitService::class); // Get user's monthly spend $spent = $limitService->getUserMonthlySpend($userId); // Get full limit status $status = $limitService->getUserLimitStatus($user); // Returns: // [ // 'limit' => 100.00, // 'spent' => 45.50, // 'remaining' => 54.50, // 'percent_used' => 45.5, // 'state' => 'ok', // 'ok', 'warning', 'over', 'no-limit' // ]
Usage Examples
OpenAI Integration
use OpenAI\Laravel\Facades\OpenAI; $response = OpenAI::chat()->create([ 'model' => 'gpt-4o', 'messages' => [ ['role' => 'user', 'content' => 'Hello!'], ], ]); // Log the request ai_log([ 'provider' => 'openai', 'model' => $response->model, 'request_type' => 'chat', 'prompt_tokens' => $response->usage->promptTokens, 'completion_tokens' => $response->usage->completionTokens, 'status' => 'success', 'user_id' => auth()->id(), ]);
Anthropic Integration
$response = Http::withHeaders([ 'x-api-key' => ai_key('anthropic'), 'anthropic-version' => '2023-06-01', ])->post('https://api.anthropic.com/v1/messages', [ 'model' => 'claude-3-5-sonnet-20241022', 'max_tokens' => 1024, 'messages' => [['role' => 'user', 'content' => 'Hello!']], ]); $data = $response->json(); ai_log([ 'provider' => 'anthropic', 'model' => $data['model'], 'request_type' => 'chat', 'prompt_tokens' => $data['usage']['input_tokens'], 'completion_tokens' => $data['usage']['output_tokens'], 'status' => $response->successful() ? 'success' : 'failed', 'user_id' => auth()->id(), 'meta' => [ 'cache_read_tokens' => $data['usage']['cache_read_input_tokens'] ?? 0, 'cache_write_tokens' => $data['usage']['cache_creation_input_tokens'] ?? 0, ], ]);
Anthropic's input_tokens already excludes cache reads and writes, so it can be logged as prompt_tokens directly. See Prompt Caching.
With Error Handling
try { $response = OpenAI::chat()->create([...]); ai_log([ 'provider' => 'openai', 'model' => 'gpt-4o', 'prompt_tokens' => $response->usage->promptTokens, 'completion_tokens' => $response->usage->completionTokens, 'status' => 'success', 'user_id' => auth()->id(), ]); } catch (\Exception $e) { ai_log([ 'provider' => 'openai', 'model' => 'gpt-4o', 'prompt_tokens' => 0, 'completion_tokens' => 0, 'status' => 'failed', 'user_id' => auth()->id(), 'meta' => ['error' => $e->getMessage()], ]); }
User Spending Limits
Add Columns to Users Table
php artisan make:migration add_ai_limits_to_users_table
Schema::table('users', function (Blueprint $table) { $table->decimal('ai_monthly_limit_usd', 10, 4)->nullable(); $table->integer('ai_alert_threshold_percent')->default(80); });
Check Limits Before AI Calls
use Filament\AiMonitor\Services\AiUsageLimitService; $limitService = app(AiUsageLimitService::class); $status = $limitService->getUserLimitStatus(auth()->user()); if ($status['state'] === 'over') { throw new \Exception('Monthly AI spending limit reached.'); } if ($status['state'] === 'warning') { // Notify user they're approaching limit }
Pricing Resolution
When a request is logged, its cost is calculated from the first match below:
- Exact model: a row whose model equals the request's model (case-insensitive).
- Snapshot of a priced model:
gpt-4o-2024-08-06,claude-sonnet-4-6-20260101,claude-3-5-sonnet-latest,gemini-2.5-pro-preview-05-06andmodel@20241022use the row for the base model. A different model that just shares a prefix (gpt-4o-minivsgpt-4o) does not match. - Provider default: the provider's row with Provider default enabled.
- Global fallback: the row with Global fallback enabled.
If nothing matches, the request is stored with cost_usd = null and flagged on the dashboard.
Prompt Caching
Anthropic, and Claude on Amazon Bedrock, bill prompt-cache tokens separately from regular input: a cache read costs 0.1× and a cache write 1.25× the model's input price (5-minute TTL). They are reported as their own counters, not as part of the input tokens. Log them in meta and they are priced with the rest of the request:
ai_log([ 'provider' => 'anthropic', 'model' => 'claude-sonnet-5', 'prompt_tokens' => 2013, // uncached input only 'completion_tokens' => 134, 'meta' => [ 'cache_read_tokens' => 0, 'cache_write_tokens' => 22568, ], ]);
Important
When you pass cache tokens, prompt_tokens must be the uncached input only, otherwise cached tokens are charged twice. Anthropic's input_tokens already excludes them. Some SDKs report a total instead: in laravel/ai 1.0, $usage->inputTokens includes cache reads and writes, so log $usage->uncachedInputTokens() as prompt_tokens, and $usage->cacheReadInputTokens / $usage->cacheWriteInputTokens as cache_read_tokens / cache_write_tokens.
The multipliers are set per provider in config/ai-monitor.php and apply to the input price of the matched pricing row:
'cache_multipliers' => [ 'anthropic' => ['read' => 0.1, 'write' => 1.25], 'bedrock' => ['read' => 0.1, 'write' => 1.25], ],
Providers without an entry do not price cache tokens. OpenAI, Gemini and others include cached tokens in the prompt tokens and discount them at model-specific rates, so a single factor per provider would be wrong for them; log their prompt tokens as usual. If a provider bills cache tokens on top of the prompt tokens, add an entry for it. If you use Anthropic's 1-hour cache TTL, cache writes cost 2× the input price: set 'write' => 2.0.
Cost recalculation (below) uses the same meta counters.
Per-Unit Pricing
Not every model is billed by the token: image models often cost a fixed amount per generated image, video and audio models per second, and some APIs per request. Set Pricing unit on the pricing row to Images, Seconds or Requests. Its input and output prices are then per unit instead of per 1K tokens, and you log the number of units in prompt_tokens and completion_tokens:
// Pricing row: unit "Images", input 0, output 0.04 ai_log([ 'provider' => 'google', 'model' => 'imagen-4', 'request_type' => 'image', 'prompt_tokens' => 0, // input units 'completion_tokens' => 2, // output units: two images ]); // cost_usd: 0.08 // Pricing row: unit "Seconds", input 0, output 0.40 ai_log([ 'provider' => 'google', 'model' => 'veo-3', 'request_type' => 'video', 'completion_tokens' => 8, // an 8-second clip ]); // cost_usd: 3.20
Cache tokens are not priced on these rows. The counts are stored in the token columns, so they also appear in the token totals on the dashboard. The price columns keep their names input_per_1k and output_per_1k.
Recalculating Costs
Added or changed pricing after requests were logged? Recalculate them:
- From the panel: AI Requests → Recalculate missing costs, or select requests and use the Recalculate cost bulk action.
- From the CLI:
php artisan ai-monitor:recalculate-costs # only requests missing a cost php artisan ai-monitor:recalculate-costs --all --since=2026-01-01 php artisan ai-monitor:recalculate-costs --all --include-manual # also overwrite manually logged costs
Events
Every call to ai_log() / AiUsageLogger::log() dispatches Filament\AiMonitor\Events\AiRequestLogged, which you can use for budget alerts, Slack notifications, and so on:
use Filament\AiMonitor\Events\AiRequestLogged; use Filament\AiMonitor\Services\AiUsageLimitService; use Illuminate\Support\Facades\Event; Event::listen(function (AiRequestLogged $event) { if ($user = $event->request->user) { $status = app(AiUsageLimitService::class)->getUserLimitStatus($user); if ($status['state'] === 'over') { // notify... } } });
Multi-Tenancy Support
When tenant_support is enabled, every AI Monitor model is scoped to the current tenant, and tenant_id is filled automatically on create. The current tenant is resolved from, in order:
- A custom resolver, if you register one.
- The global
tenant()helper (e.g. stancl/tenancy). - The current Filament panel tenant (
Filament::getTenant()).
// config/ai-monitor.php 'tenant_support' => true,
// AppServiceProvider::boot() use Filament\AiMonitor\Support\Tenancy; Tenancy::resolveUsing(fn () => auth()->user()?->team_id);
The plugin's resources set $isScopedToTenant = false, so they work on Filament panels with tenancy without needing an ownership relationship on the plugin's models.
Dashboard
The dashboard (/{panel}/aimonitor) has a period filter (7 days, 30 days, 90 days, 12 months) and a provider filter that apply to every widget. The filters are kept in the URL and session.
| Widget | Description |
|---|---|
| Setup Alert | Shown when pricing or API keys are missing, or requests have no cost |
| Stats Overview | Requests, cost, tokens and success rate, with change vs the previous period |
| Cost & Request Trends | Daily cost and request line chart |
| Cost by Provider | Doughnut chart breakdown |
| Top Models by Cost | Five most expensive models |
| Usage by User | Five highest-spending users |
| Recent Requests | Latest calls, linking to the request detail page |
The widgets also work on your own dashboards; without page filters they show the last 30 days.
Testing
composer install
composer test
The test suite runs against whichever Filament version is installed. CI runs it on Filament 4 and 5.
License
MIT License. See LICENSE for details.