israrminhas/filament-aimonitor

AI API cost monitoring and key management plugin for Filament
2,603 10
Install
composer require israrminhas/filament-aimonitor
Latest Version:v1.1.0
PHP:^8.2
License:MIT
Last Updated:Oct 2, 2026
Links: GitHub  ·  Packagist
Maintainer: Israrminhas1

Filament AI Monitor

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)
  • AiRequestLogged event 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:

  1. Exact model: a row whose model equals the request's model (case-insensitive).
  2. 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-06 and model@20241022 use the row for the base model. A different model that just shares a prefix (gpt-4o-mini vs gpt-4o) does not match.
  3. Provider default: the provider's row with Provider default enabled.
  4. 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:

  1. A custom resolver, if you register one.
  2. The global tenant() helper (e.g. stancl/tenancy).
  3. 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.

Related Packages

pdro-lucas/filament-ai-writer

AI Writer Action for Filament Forms — supports OpenAI, Claude and Gemini

10,156 8
ferarandrei1/filament-ai-chat-widget

A beautiful AI chat widget plugin for Filament v3 with OpenAI integration

208 4
spectra-php/laravel-spectra

Comprehensive observability for AI/LLM operations in Laravel applications

4,492 20

Version History

Version Released PHP Laravel License
v1.1.0 ^8.2 ^11.28|^12.0|^13.0 MIT
v1.0.0 ^8.2 ^11.28|^12.0|^13.0 MIT