Skip to content

Configuration

config/ai-tasks.php holds models, prices, routing and budgets. API keys and provider URLs live in config/ai.php (laravel/ai) — see Providers.

KeyEnvDefaultDescription
defaultAI_DEFAULTopenaiDriver used when no routing rule matches and no driver is passed explicitly
default_tenantAI_DEFAULT_TENANTdefaultTenant ID when the request can’t be resolved to a tenant, see Budgets & tenants
tableAI_TASKS_TABLEai_runsTable for run records
store_requestAI_STORE_REQUESTfalseStore messages, system prompt and the task’s constructor arguments in ai_runs.request

store_request is off by default because prompts may contain sensitive data. Without it a failed run can’t be rebuilt: ai:retry, the dashboard Retry button and webhook completion need the stored constructor arguments (tasks whose constructor has no required parameters are the exception).

'dashboard' => [
'enabled' => env('AI_DASHBOARD_ENABLED', true),
'path' => env('AI_DASHBOARD_PATH', 'ai-tasks'),
'middleware' => ['web'],
'poll_interval' => env('AI_DASHBOARD_POLL', 3), // seconds; 0 = off
'theme' => env('AI_DASHBOARD_THEME', 'system'), // light|dark|system
'per_page' => env('AI_DASHBOARD_PER_PAGE', 50),
'stuck_after_minutes' => env('AI_DASHBOARD_STUCK_AFTER', 15),
],

Details — Dashboard.

'queues' => [
'default' => env('AI_QUEUE', 'ai'), // provider calls — slow, needs a long timeout
'post' => env('AI_QUEUE_POST', 'ai-post'), // postprocess — fast
],

A task can override both per class with viaQueues(), see Queued tasks.

The key of each driver must match a provider name in config/ai.php.

'drivers' => [
'openai' => [
'model' => env('OPENAI_MODEL', 'gpt-5.6-luna'),
'embed_model' => env('OPENAI_EMBED_MODEL', 'text-embedding-3-small'),
'image_model' => env('OPENAI_IMAGE_MODEL', 'gpt-image-2'),
'audio_model' => env('OPENAI_AUDIO_MODEL', 'gpt-4o-mini-tts'),
'price' => [
'in' => 0.20,
'out' => 1.20,
'cache_write' => 0.25,
'cache_read' => 0.02,
'per_char' => env('OPENAI_TTS_PRICE_PER_CHAR', 15.0),
],
'webhook' => [
'secret' => env('OPENAI_WEBHOOK_SECRET'),
],
],
// anthropic, gemini, deepseek, groq, mistral, xai, ollama, openrouter, eleven, null
],
KeyDescription
modelDefault model for text
embed_modelModel for embed
image_modelModel for image
audio_modelModel for audio (TTS)
priceRates in USD, null — cost is not tracked. See below
pricesPer-model rates keyed by model name, override price
webhook.secretSecret for verifying incoming webhooks, see Webhooks

A task can pick another model per request with options['model'] in its payload.

KeyUnitUsed for
inper 1M tokensinput tokens billed at full price
outper 1M tokensoutput tokens, reasoning included
cache_readper 1M tokenscached input read; falls back to in when missing
cache_writeper 1M tokenscached input write; falls back to in when missing
per_charper 1M charactersaudio (TTS) — the provider returns no usage, so cost is approximated from the input length
per_minuteper minute of audiotranscription, when the provider reports the duration

Details and the prices override — Cost tracking.

The pre-configured null driver returns an empty response — useful for local development.

Task name → ordered chain of drivers. The first available one is used, the next is tried on a transient failure.

'routing' => [
'summarize' => ['openai', 'gemini'],
'chat' => ['anthropic'],
'tts' => ['openai', 'eleven'],
],

The task name comes from AiTask::name() — by default the class name without Task, in snake_case. See Driver routing.

Global pipes applied to every AiResponse before the task’s own postprocess() — on send(), stream() and the queued path alike:

'postprocess' => [
'enabled' => true,
'pipes' => [
App\Ai\Pipes\StripMarkdownFences::class,
],
],

A pipe is a regular Laravel pipeline stage. AiResponse is immutable, so return a new instance:

use Fomvasss\AiTasks\DTO\AiResponse;
class StripMarkdownFences
{
public function handle(AiResponse $resp, \Closure $next)
{
return $next(new AiResponse(
ok: $resp->ok,
content: preg_replace('/^```\w*\n|\n```$/', '', (string) $resp->content),
usage: $resp->usage,
raw: $resp->raw,
error: $resp->error,
toolCalls: $resp->toolCalls,
structured: $resp->structured,
finishReason: $resp->finishReason,
pendingApprovals: $resp->pendingApprovals,
));
}
}

The bundled EnsureJson and SanitizeHtml pipes are empty examples, deprecated and removed in 4.0. QualityScore is a demo that adds quality to usage.

Monthly spend limit per tenant in USD:

'budgets' => [
'default' => ['monthly_usd' => 100],
'tenant-abc' => ['monthly_usd' => 50],
],

See Budgets & tenants.

'approvals' => [
'ttl_minutes' => env('AI_APPROVAL_TTL', 60),
'reject_reason' => env('AI_APPROVAL_REJECT_REASON'),
],

ttl_minutes — a paused run older than this can’t be resumed (null = no limit); a task overrides it with approvalTtlMinutes(). reject_reason — text sent with a rejection that has none, so the model answers instead of ending with an empty reply. See Resuming.

'webhook_middleware' => ['api'],

Applied to the POST /ai-webhooks/{driver} route, see Webhooks.