Every AI feature in the CMS — image generation, text drafting, translations, alt text, the site generator — bills directly to the site owner's own account with each provider. The AI Usage & Costs page (Dashboard → Settings → AI Usage, admin-only) turns those opaque provider bills into a report inside the dashboard: spend over time, month-to-date total, cost per billing line item, how many images were generated and what each one roughly cost, and token consumption per text model.
Providers
| Provider | Status | Data source |
|---|---|---|
| OpenAI | Live | Organization Costs API (/v1/organization/costs) + Usage APIs (/usage/images, /usage/completions) |
| Claude (Anthropic) | Live | Admin API (/v1/organizations/cost_report + /v1/organizations/usage_report/messages) |
| Google Gemini | No reporting API exists | Google exposes no key-based spend/usage endpoint for the Gemini API — the page says so and deep-links to AI Studio's usage page and Google Cloud Billing instead of pretending otherwise |
fal.ai and Stability are deliberately out of scope.
OpenAI setup — the Admin key
OpenAI only serves billing data to an organization Admin API key (sk-admin-…); the regular project key used for generation calls is refused with a 401/403. The Admin key is created at platform.openai.com → Settings → Organization → Admin keys and pasted into Settings → API Keys → OpenAI → Admin API Key (setting ai.openai_admin_key). It is stored separately from the generation key and is never used for generation calls. Until it's saved, the AI Usage page shows a connect card that links to both the API Keys page and OpenAI's key console.
Anthropic setup — the Admin key
Same shape as OpenAI: billing data requires an Admin API key (sk-ant-admin…), created at console.anthropic.com → Settings → Admin keys and pasted into Settings → API Keys → Claude (Anthropic) → Admin API Key (setting ai.claude_admin_key). Two Anthropic-specific caveats: Admin keys exist only for Anthropic organization accounts (individual accounts get a connect card that explains this), and the Admin API's cost amounts arrive as decimal strings in cents — the provider converts to dollars. Claude generates no images, so the image-generation card and table are hidden entirely for this provider (the summary row drops to three cards).
What the page shows
- Summary cards — spend over the selected window (7/30/60/90 days), month-to-date spend (flagged as partial when the window starts after the 1st), images generated with an approximate per-image cost, and total input/output tokens.
- Balance remaining — OpenAI does not expose remaining credit balance through any API, so the card says exactly that and deep-links to the provider's billing console. The report shape carries a nullable
balanceso a future provider that does expose one renders it automatically. - Daily spend — a zero-filled bar chart over the window (UTC days, matching how billing APIs bucket).
- Cost breakdown — spend per billing line item (
gpt-image-2, images,gpt-4o, input, …), largest first, with share-of-total bars. - Image generation — images and requests per model from the usage API. The per-image figure comes from cross-referencing image-matching cost line items against the image count; line-item naming isn't contractual, so when nothing matches the count shows without a $/image figure.
- Text models — input/output tokens per model.
Behaviour notes
- Reports are cached for one hour per provider + window (
AiCostReports::CACHE_TTL_SECONDS); the Refresh button bypasses the cache. Failures are never cached. - The page never fetches during navigation — the first fetch fires via
wire:initafter the page paints, so a slow billing API can't block the dashboard. Provider errors render as a friendly card (including the "that's not an Admin key" case) with a retry button. - Costs lag real usage by minutes to hours on the provider side; the empty state says so.
Architecture (adding a provider)
- app/Support/AiCosts/AiCostProvider.php — the interface:
key/label/supportsReporting/supportsImageGeneration/isConfigured/keyHint/keyConsoleUrl/billingUrl/fetch(days).fetch()returns one normalized report shape (currency, window total, month-to-date, nullable balance, zero-filled daily series, line items, images, tokens) and throwsRuntimeExceptionwith a user-presentable message on failure.supportsReporting() === falserenders the vendor as an explanatory card (Gemini);supportsImageGeneration() === falsehides the image card/table (Claude). - app/Support/AiCosts/OpenAiCostsProvider.php — OpenAI; paginates each endpoint via
next_pagecursors with a hard page cap. - app/Support/AiCosts/AnthropicCostsProvider.php — Anthropic Admin API; same cursor pagination,
x-api-key+anthropic-versionheaders, hand-builtgroup_by[]=query strings (the API rejects PHP's indexedgroup_by[0]=form), cents→dollars conversion. - app/Support/AiCosts/GoogleGeminiCostsProvider.php — the no-reporting-API stub for Gemini;
fetch()is never called. - app/Support/AiCosts/AiCostReports.php — registry + hourly cache. Adding a future provider is one new class registered in
providers()— the page itself needs no other changes. - resources/views/pages/dashboard/settings/⚡ai-usage.blade.php — the Volt page; route
dashboard.settings.ai-usage(admin group inroutes/cms.php).
Tests: tests/Feature/AiUsageReportTest.php.