LocalAI/docs/laravel.md

7.5 KiB
Raw Blame History

Laravel (laravel/ai) mit dem zentralen LocalAI-Server

Das Laravel AI SDK spricht den Server wie einen OpenAI-kompatiblen Anbieter an. Damit gehen Agenten mit Tools, Streaming, Embeddings, Transkription (Spracheingabe) und Bildgenerierung.

Der Server-Teil ist getestet (Messwerte unten). Der Laravel-Code hier folgt der SDK-Doku, ist aber noch nicht in einer echten App gelaufen.

Was der Server bietet

Modell-ID (Aliase) Endpoint SDK-Funktion Dauer (gemessen)
qwen3.5-35b-a3b, glm-4.7-flash, qwen3.5-9b /v1/chat/completions Agent, Text, Tools, Streaming ~850 t/s Prompt, ~47 t/s Antwort
nomic-embed /v1/embeddings Embeddings (768 Dimensionen)
whisper (whisper-1) /v1/audio/transcriptions Transcription ~0,5 s für 7 s Sprache
z-image (gpt-image-1, dall-e-3, dall-e-2) /v1/images/generations Image 512²: ~11 s, 1024²: ~40 s, 1536×1024: ~65 s

Bilder verdrängen das Chat-Modell. Beide passen nicht gleichzeitig in die 16 GB VRAM. Der erste Bild-Request lädt das Bildmodell (+ einige Sekunden), der nächste Chat-Request lädt das Chat-Modell zurück (~5 s). Whisper, Autocomplete und Embeddings bleiben immer geladen.

1. Netz und Zugang

  • Der Rechner, auf dem Laravel läuft, muss in ALLOWED_NETS (Server-.env) liegen: localhost, LAN 192.168.178.0/24 oder NetBird 100.73.0.0/16. Ein öffentlicher Server braucht also einen NetBird-Peer.
  • Der API-Key ist LLM_API_KEY aus der Server-.env (./scripts/start.sh --show-key).

2. Konfiguration

.env der Laravel-App:

LOCAL_AI_URL=http://100.73.82.219:8800/v1
LOCAL_AI_API_KEY=<KEY>

config/ai.php:

'providers' => [
    // Text, Agent, Embeddings, Transkription
    'local' => [
        'driver' => 'openai-compatible',
        'url'    => env('LOCAL_AI_URL'),
        'key'    => env('LOCAL_AI_API_KEY'),
        'models' => [
            'text'          => ['default' => 'qwen3.5-35b-a3b'],
            'embeddings'    => ['default' => 'nomic-embed', 'dimensions' => 768],
            'transcription' => ['default' => 'whisper'],
        ],
    ],

    // Bilder: openai-compatible kann keine Bilder, der OpenAI-Treiber schon.
    // Er schickt "gpt-image-1" & Co. – der Server kennt diese Namen als Aliase.
    'local-images' => [
        'driver' => 'openai',
        'key'    => env('LOCAL_AI_API_KEY'),
        'url'    => env('LOCAL_AI_URL'),
    ],
],

local in config/ai.php als Standard für Text, Embeddings und Transkription eintragen und local-images als Standard für Bilder. Die genauen Schlüssel für die Standard-Provider stehen in der veröffentlichten config/ai.php.

3. Agent

php artisan make:agent Assistant
$response = (new Assistant)->prompt('Fasse die offenen Rechnungen zusammen', timeout: 300);

// im Controller als Stream (SSE) an den Browser
return (new Assistant)->stream($text);
  • Timeouts großzügig setzen. Das Laden eines Modells kann einige Sekunden bis Minuten dauern. Längere Agent-Läufe besser als Queue-Job.
  • Tool-Calling klappt mit qwen3.5-35b-a3b und glm-4.7-flash zuverlässig, mit qwen3.5-9b bei mehrstufigen Aufgaben schlechter.
  • Alle Anwendungen sollten dasselbe Chat-Modell nutzen (opencode, Continue, Laravel). Ein anderes Modell löst bei jedem Wechsel einen Modellwechsel aus.

4. Spracheingabe (Aufnahmetaste)

Ablauf: Browser nimmt auf → POST /voice/transcribe → Text anzeigen und bestätigen → POST /voice/ask → Agent streamt die Antwort.

use App\Ai\Agents\Assistant;
use Illuminate\Http\Request;
use Laravel\Ai\Transcription;

Route::post('/voice/transcribe', function (Request $request) {
    $request->validate(['audio' => 'required|file|max:20480']);

    return ['text' => trim((string) Transcription::fromUpload($request->file('audio'))->generate())];
});

Route::post('/voice/ask', function (Request $request) {
    $request->validate(['text' => 'required|string|max:4000']);

    return (new Assistant)->stream($request->string('text'));
});
let rec, chunks = [];
const mime = MediaRecorder.isTypeSupported('audio/webm;codecs=opus') ? 'audio/webm;codecs=opus' : 'audio/mp4';

async function startRecording() {
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
  rec = new MediaRecorder(stream, { mimeType: mime });
  chunks = [];
  rec.ondataavailable = e => chunks.push(e.data);
  rec.onstop = async () => {
    stream.getTracks().forEach(t => t.stop());
    const fd = new FormData();
    fd.append('audio', new Blob(chunks, { type: mime }), mime.includes('webm') ? 'voice.webm' : 'voice.m4a');
    const res = await fetch('/voice/transcribe', {
      method: 'POST', body: fd,
      headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content },
    });
    const { text } = await res.json();
    showForConfirmation(text);   // eigenes UI: Text anzeigen/bearbeiten, dann an /voice/ask senden
  };
  rec.start();
}
const stopRecording = () => rec?.stop();
// Taste: pointerdown → startRecording(), pointerup → stopRecording()
  • HTTPS ist Pflicht. Browser geben das Mikrofon nur auf https:// oder localhost frei. Das betrifft die Laravel-App, nicht die Verbindung von Laravel zum KI-Server.
  • Formate: Chrome/Firefox liefern WebM/Opus, Safari MP4/AAC. Der Server wandelt alles per ffmpeg selbst um, Laravel muss nichts konvertieren (getestet: WAV, WebM, M4A).
  • Sprache: Der Server erkennt die Sprache automatisch.
  • Bestätigung vor dem Absenden: Whisper verhört sich manchmal bei Namen und Fachbegriffen. Bei Agenten, deren Tools etwas ausführen, den Text deshalb erst anzeigen.

5. Bilder

use Laravel\Ai\Image;

$path = Image::of('Ein rotes Fahrrad vor einer gelben Hauswand')
    ->landscape()
    ->timeout(300)
    ->generate()          // mit 'local-images' als Standard-Provider für Bilder
    ->store();
  • square() = 1024×1024 (~40 s), landscape()/portrait() = 1536×1024 (~65 s). quality() wird ignoriert.
  • Bilder am besten in einem Queue-Job erzeugen (->queue()->then(...)), nicht im Web-Request.
  • Als Agent-Tool: php artisan make:tool GenerateImage. In handle() das Bild erzeugen, speichern und die URL zurückgeben. Bedenken: Danach muss das Chat-Modell neu geladen werden (~5 s), bevor der Agent weiterantwortet.
  • Falls der OpenAI-Treiber in einer neueren SDK-Version Parameter schickt, die der Server ablehnt, geht es auch direkt:
    $b64 = Http::withToken(config('ai.providers.local.key'))->timeout(300)
        ->post(config('ai.providers.local.url').'/images/generations', [
            'model' => 'z-image', 'prompt' => $prompt, 'size' => '1024x1024',
        ])->json('data.0.b64_json');
    Storage::put($path, base64_decode($b64));
    

6. Fehlersuche

Symptom Ursache
401 API-Key falsch
403 Laravel-Server nicht in ALLOWED_NETS
Timeout beim ersten Request Modell lädt noch → Timeout erhöhen
Bild dauert > 1 min normal bei 1536×1024 und nach einem Modellwechsel
Mikrofon-Abfrage erscheint nicht Seite läuft nicht über HTTPS
Leeres Transkript Aufnahme zu kurz/stumm. Mindestlänge im Frontend prüfen

Schnelltest vom Laravel-Server aus:

curl -H "Authorization: Bearer $KEY" http://<server>:8800/v1/audio/transcriptions -F model=whisper -F file=@test.webm
curl -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' http://<server>:8800/v1/images/generations \
  -d '{"model":"z-image","prompt":"a red bicycle","size":"512x512"}' | head -c 200