DEV Community

Vas
Vas

Posted on • Originally published at github.com

Stop Writing AI Spaghetti in Laravel - Use a Task Orchestration Layer

You add your first AI call. It's one line, a synchronous openai()->chat(), and it works perfectly.

Six months later you have seventeen of those scattered across the codebase, no idea which ones are slow, no retry logic, a tenant who blew through $300 in one night, and a queue that processes bulk image generation at the same priority as real-time chat responses.

Making AI work in production Laravel apps involves more than calling an API. You need queues, audit logs, cost tracking, retry logic, and tests that do not hit real providers. That is what fomvasss/laravel-ai-tasks is for.

It sits on top of laravel/ai (the official Laravel AI SDK) and adds the orchestration and observability layer you actually need in production.

What you get

  • Task classes with a generator command (ai:make-task)
  • Sync, queued, and streaming execution via the AI facade
  • Every run logged to ai_runs table (request, response, tokens, cost, status)
  • Driver routing with fallback chains
  • Multi-tenant budget tracking
  • Prompt caching support for Anthropic
  • Built-in dashboard at /ai-tasks
  • AI::fake() with assertions for testing
  • Events at every stage of execution
  • Text, image, embeddings, TTS, and transcription modalities

Requirements

  • PHP 8.3+
  • Laravel 12 or 13
  • laravel/ai ^0.8

Install

composer require fomvasss/laravel-ai-tasks
php artisan vendor:publish --tag=ai-tasks-config
php artisan vendor:publish --tag=ai-migrations
php artisan migrate
Enter fullscreen mode Exit fullscreen mode

Define a task

class SummarizeTask extends AiTask
{
    public function __construct(private readonly string $text) {}

    public function modality(): string { return 'text'; }

    public function toPayload(): AiPayload
    {
        return new AiPayload(
            modality:     'text',
            messages:     [new UserMessage("Summarize: {$this->text}")],
            systemPrompt: 'Reply in 3 sentences max.',
            options:      ['temperature' => 0.3],
        );
    }

    public function postprocess(AiResponse $response): AiResponse|array
    {
        return $response;
    }
}
Enter fullscreen mode Exit fullscreen mode

Run it

// Sync
$response = AI::send(new SummarizeTask($text));

// Queued
$runId = AI::queue(new SummarizeTask($text));

// Streaming (no timeout, good for long outputs)
AI::stream(new SummarizeTask($text), fn($chunk) => print($chunk));

// Runtime driver override
AI::send(new SummarizeTask($text), drivers: 'anthropic');
Enter fullscreen mode Exit fullscreen mode

Driver routing with fallback

// config/ai-tasks.php
'routing' => [
    'summarize' => ['openai', 'anthropic'], // tries openai, falls back to anthropic
],
Enter fullscreen mode Exit fullscreen mode

Cost tracking

'anthropic' => [
    'model' => 'claude-sonnet-4-6',
    'price' => ['in' => 3.00, 'out' => 15.00],
],
Enter fullscreen mode Exit fullscreen mode

Query spend by tenant:

AiRun::where('tenant_id', $tenantId)->where('status', 'ok')->sum('cost');
Enter fullscreen mode Exit fullscreen mode

shouldRun() guard

Prevent stale queued tasks from consuming tokens:

public function shouldRun(): bool
{
    return Product::find($this->productId)?->needs_analysis ?? false;
}
Enter fullscreen mode Exit fullscreen mode

Testing without real API calls

$fake = AI::fake([
    'summarize' => 'This is a summary.',
    '*'          => 'Default fallback.',
]);

$fake->assertSent(SummarizeTask::class);
$fake->assertSentCount(1);
Enter fullscreen mode Exit fullscreen mode

Supported providers

OpenAI, Anthropic, Gemini, DeepSeek, Groq, Mistral, xAI, Ollama, ElevenLabs, and any laravel/ai-compatible provider.


Happy to answer questions in the comments. Stars and issues welcome 🙂


Support

If this package saves you time, consider supporting development (and this will help me 🥹):

  • Ko-fi
  • 🏦 Monobank
  • 💚 USDT TRC20: THLgp6DxiAtbNHvgnKV56vk1L38UuUagKf

Top comments (0)