Budgets & tenants
Every run belongs to a tenant (ai_runs.tenant_id). Budgets limit a tenant’s monthly spend in USD.
'budgets' => [ 'tenant-abc' => ['monthly_usd' => 50.0], 'default' => ['monthly_usd' => 100.0],],A tenant without its own entry gets the default limit — each tenant separately, not a shared pool. Without a default entry such tenants are unlimited.
Current spend vs limit — ai:budget, or from code, e.g. to show the remaining budget in your UI:
use Fomvasss\AiTasks\Support\Budget;
$budget = app(Budget::class);
$budget->getMonthlyLimit($tenantId); // ?float, null = unlimited$budget->getMonthlySpent($tenantId); // float, current month$budget->getMonthlyRemaining($tenantId); // ?float, null = unlimited$budget->getSpentBetween($tenantId, $from, $to);$budget->ensureNotExceeded($tenantId, expectedCost: 0.5); // throws BudgetExceededExceptionThe month is the calendar month in the app’s timezone, by ai_runs.created_at.
How the tenant is resolved
Section titled “How the tenant is resolved”tenantId()on the task, if it returns non-nullTenantResolver:X-Tenant-Idrequest header- authenticated user’s
tenant_id,company_idorid default_tenantfrom config (default)
Per task
Section titled “Per task”When the task already knows its tenant (it holds a model with an organization_id), override tenantId():
protected function tenantId(): ?string{ return $this->order->organization_id; // null falls back to TenantResolver}Custom resolver
Section titled “Custom resolver”Bind your own resolver in a service provider:
$this->app->scoped(\Fomvasss\AiTasks\Support\TenantResolver::class, fn () => new MyTenantResolver());A resolver only sees the current request/auth state, nothing task-specific — for that use tenantId() on the task.
Subject
Section titled “Subject”Tag the run with the record it concerns, to filter ai_runs by subject instead of only by tenant/task. Independent of tenantId():
protected function subjectType(): ?string{ return 'order';}
protected function subjectId(): ?string{ return (string) $this->order->id;}ai_runs.user_id records who started the run — for audit, spend per user and the dashboard’s User filter. By default it is auth()->id() at dispatch (in the request, before the job is queued), so a run started with nobody logged in — a scheduled job, a webhook, a system task — records null. Override userId() when the task runs on someone’s behalf without them being logged in:
protected function userId(): ?string{ return (string) $this->comment->author_id;}It is unrelated to tenantId(): the tenant pays, the user started the run. Publish and run the migration (vendor:publish --tag=ai-migrations, migrate); until then runs are stored without the column.
When the budget is exceeded
Section titled “When the budget is exceeded”Fomvasss\AiTasks\Exceptions\BudgetExceededException is thrown on send(), stream() and in the queued job. The check runs twice:
- pre-flight — before the provider call, against prior spend
- post-call — after the response, adding its actual cost
A post-call rejection means the provider already billed the request: the run is recorded as error but keeps its real cost and tokens. Spend counts every run with a recorded cost regardless of status, so nothing vanishes from later checks.
The task’s onFailed() is called in both cases.