LocalAI/docs/laravel.md

165 lines
7.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Laravel (laravel/ai) mit dem zentralen LocalAI-Server
Das [Laravel AI SDK](https://laravel.com/docs/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:
```dotenv
LOCAL_AI_URL=http://100.73.82.219:8800/v1
LOCAL_AI_API_KEY=<KEY>
```
`config/ai.php`:
```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
```bash
php artisan make:agent Assistant
```
```php
$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.
```php
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'));
});
```
```js
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
```php
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:
```php
$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:
```bash
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
```