LM Studio + Laragon: LLM PHP App in locale

Image

LM Studio + Laragon: LLM PHP App in locale

Mi sono divertito un sacco a realizzare l’immagine del nerd che gongola con la birra “localhost”, volevo evitare la solita copertina con i loghetti dei software. E forse questo paragrafo rovinerà l’indicizzazione di questa pagina, ma non importa, in questo magazine non siamo ossessionati dal bisogno di popolarità.

Tra soluzioni cloud e API proprietarie, spicca una strada alternativa e sempre più interessante, il self-hosting. Questo articolo pensato per sviluppatori alle prime armi ma curiosi, ti guiderà passo-passo nella configurazione locale di LM Studio, nell’esposizione delle sue API, e infine nell’accesso al modello GPT-OSS-20B tramite un’applicazione PHP ospitata in Laragon. Un progetto pratico e replicabile, perfetto per chi vuole sperimentare, imparare o iniziare a costruire soluzioni aziendali autonome e sostenibili. Si presuppone che il lettore possieda conoscenze di base in ambito di reti e programmazione.

LM Studio: la base operativa

LM Studio è un’interfaccia desktop per l’uso locale di modelli di linguaggio open-source, come LLaMA, Mistral e GPT-OSS. Uno dei suoi punti di forza è la possibilità di esporre localmente un’API compatibile con OpenAI, rendendo l’integrazione con app esistenti straordinariamente semplice.

1. Installazione di LM Studio

  • Scarica LM Studio da lmstudio.ai
  • Installa come un normale programma desktop (disponibile per Windows, macOS e Linux)
  • Avvia LM Studio e scegli un modello, ad esempio GPT-OSS-20B, ottimo compromesso tra potenza e leggerezza.

2. Esporre le API

Premetto che nel momento in cui sto scrivendo la documentazione ufficiale non è proprio una meraviglia, soprattutto per gli utenti alle prime armi. Vediamo di colmare questo gap con uno screenshot.

  • Una volta caricato il tuo modello preferito, nella colonna di sinistra seleziona Developer.
  • In alto, accanto a Status: Running clicca su Settings e copia la configurazione che vedi nello screenshot per esporre le API di LM Studio SOLO in localhost. Se vuoi renderle disponibili a tutta la tua rete locale dovrai abilitare anche Serve on Local Network.

Con LM Studio abbiamo finito, passiamo al web server in locale, il mio preferito è Laragon.

Laragon: web server facile

1. Scarica Laragon

  • Vai alla pagina Download e prendi l’ultima Full (consigliata), con Apache/Nginx, MySQL, PHP, Node, Python, ecc.

2. Installa

  • Avvia l’installer → “Next, Next, Next…”.
  • Percorso consigliato: C:\laragon.
  • (Opzionale) Spunta Run Laragon when Windows starts per averlo pronto ad ogni avvio.

3. Avvia i servizi

  • Apri Laragon e clicca Start All (Apache/Nginx, MySQL, ecc.).
  • Con Quick Start puoi creare un progetto (es. WordPress) in pochi minuti.

4. Dove mettere i siti (Document Root) + URL “belli”

  • Metti i progetti in C:\laragon\www\.
  • Con Auto Virtual Hosts ottieni URL del tipo http://miosito.test (basta mettere la cartella miosito in www e fare Reload).
  • Il pattern {name}.test è configurabile da Menu → Preferences.

5. HTTPS locale in 1 click

  • Attiva Menu → Apache → SSL → Enabled: il tuo sito sarà raggiungibile anche in https://miosito.test.

6. Database & phpMyAdmin

  • Laragon può aggiungere phpMyAdmin dal menu Tools → Quick add → phpmyadmin.
  • Con PHP 8.4+ usa phpMyAdmin 6 (voce phpmyadmin6.0snapshot).
  • La pagina di download indica anche i due collegamenti: phpMyAdmin 5 e phpMyAdmin 6 nel menu MySQL.

7. Cambiare versioni di PHP/Apache

  • Aggiungi versioni con Tools → Quick add → [PHP/Apache/Nginx Version].
  • Cambia la versione di PHP da Menu → PHP → Version → [scelta].

8. Crea il primo progetto “al volo”

  • Crea la cartella myapp in cui mettete i file php in C:\laragon\www\myapp e genera http://myapp.test.

9. Scriviamo il codice della nostra applicazione php

Prima programmiamo index.php per la UI

PHP
<!doctype html>
<html lang="it">
<head>
  <meta charset="utf-8">
  <title>Chat LLM locale (LM Studio) — Demo PHP</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style>
    body{font-family:system-ui,Segoe UI,Arial,sans-serif;max-width:760px;margin:32px auto;padding:0 16px}
    textarea{width:100%;min-height:140px;padding:10px}
    pre{background:#f6f6f6;padding:12px;border-radius:8px;white-space:pre-wrap}
    .row{margin:12px 0}
    .muted{color:#666}
    button{padding:10px 16px;border:0;border-radius:8px;background:black;color:white;cursor:pointer}
    button:hover{opacity:.9}
    .card{border:1px solid #eee;border-radius:12px;padding:16px;margin-top:16px}
    .error{color:#b00020}
  </style>
</head>
<body>
  <h1>Chat con LLM locale (LM Studio)</h1>
  <p class="muted">Endpoint locale chiamato da PHP: <code>http://localhost:1234/v1/chat/completions</code></p>

  <form id="chat-form" class="card">
    <div class="row">
      <label for="prompt"><strong>Prompt</strong></label><br>
      <textarea id="prompt" name="prompt" placeholder="Scrivi la tua richiesta..."></textarea>
    </div>
    <div class="row">
      <label for="temperature">Temperature (0–1)</label><br>
      <input id="temperature" name="temperature" type="number" min="0" max="1" step="0.1" value="0.7">
    </div>
    <div class="row">
      <label for="max_tokens">Max tokens (opzionale)</label><br>
      <input id="max_tokens" name="max_tokens" type="number" placeholder="es. 512">
    </div>
    <div class="row">
      <button type="submit">Invia</button>
    </div>
    <p class="muted">Assicurati in LM Studio: <em>Settings → Developer → Enable Local Inference Server</em> (porta predefinita 1234).</p>
  </form>

  <div id="output" class="card" style="display:none;">
    <h3>Risposta del modello</h3>
    <pre id="answer"></pre>
  </div>

  <div id="error" class="card" style="display:none;">
    <h3>Errore</h3>
    <pre class="error" id="errText"></pre>
  </div>

  <script>
    const form = document.getElementById('chat-form');
    const outBox = document.getElementById('output');
    const ans = document.getElementById('answer');
    const errBox = document.getElementById('error');
    const errText = document.getElementById('errText');

    form.addEventListener('submit', async (e) => {
      e.preventDefault();
      outBox.style.display = 'none';
      errBox.style.display = 'none';

      const prompt = document.getElementById('prompt').value.trim();
      const temperature = document.getElementById('temperature').value;
      const max_tokens = document.getElementById('max_tokens').value;

      if (!prompt) {
        errText.textContent = 'Prompt mancante.';
        errBox.style.display = 'block';
        return;
      }

      ans.textContent = 'Sto pensando...';
      outBox.style.display = 'block';

      try {
        const res = await fetch('chat.php', {
          method: 'POST',
          headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' },
          body: new URLSearchParams({ prompt, temperature, max_tokens })
        });
        const data = await res.json();

        if (!res.ok || data.error) {
          errText.textContent = (data.error ? JSON.stringify(data.error, null, 2) : `HTTP ${res.status}`);
          errBox.style.display = 'block';
          outBox.style.display = 'none';
          return;
        }

        ans.textContent = data.content || '(nessuna risposta)';
        outBox.style.display = 'block';
      } catch (err) {
        errText.textContent = String(err);
        errBox.style.display = 'block';
        outBox.style.display = 'none';
      }
    });
  </script>
</body>
</html>

Poi codifichiamo chat.php per la connessioni alle API

PHP
<?php
// chat.php — chiama l'API locale di LM Studio (compatibile OpenAI)

header('Content-Type: application/json; charset=utf-8');

$BASE_URL = 'http://localhost:1234/v1';         // cambia se usi altra porta/host
$MODEL    = 'openai/gpt-oss-20b';               // deve corrispondere al modello caricato in LM Studio
$SYSTEM   = 'Sei un assistente conciso e utile.';

$prompt = trim($_POST['prompt'] ?? '');
if ($prompt === '') {
    http_response_code(400);
    echo json_encode(['error' => 'Prompt mancante']);
    exit;
}

// Opzioni opzionali dal form
$temperature = isset($_POST['temperature']) && $_POST['temperature'] !== '' ? floatval($_POST['temperature']) : 0.7;
$max_tokens  = isset($_POST['max_tokens']) && $_POST['max_tokens'] !== '' ? intval($_POST['max_tokens']) : null;

$payload = [
    'model'    => $MODEL,
    'messages' => [
        ['role' => 'system', 'content' => $SYSTEM],
        ['role' => 'user',   'content' => $prompt],
    ],
    'temperature' => $temperature,
];
if ($max_tokens !== null) $payload['max_tokens'] = $max_tokens;

$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL            => $BASE_URL . '/chat/completions',
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        'Content-Type: application/json',
        // Se hai configurato un token sul server locale, sblocca la riga sotto:
        // 'Authorization: Bearer not-needed',
    ],
    CURLOPT_POSTFIELDS     => json_encode($payload, JSON_UNESCAPED_UNICODE),
    CURLOPT_CONNECTTIMEOUT => 5,
    CURLOPT_TIMEOUT        => 60,
]);

$response = curl_exec($ch);
$err      = curl_error($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($response === false) {
    http_response_code(500);
    echo json_encode(['error' => ['type' => 'curl_error', 'message' => $err]]);
    exit;
}

$data = json_decode($response, true);

// Se LM Studio risponde con errore o HTTP non 2xx
if ($httpCode < 200 || $httpCode >= 300) {
    http_response_code($httpCode);
    echo json_encode(['error' => $data ?: $response], JSON_UNESCAPED_UNICODE);
    exit;
}

// Estrai il testo in stile OpenAI Chat Completions
$content = $data['choices'][0]['message']['content'] ?? null;

echo json_encode([
    'content' => $content,
    'model'   => $MODEL,
    'usage'   => $data['usage'] ?? null,
], JSON_UNESCAPED_UNICODE);

Puntiamo il browser a http://myapp.test

Con questo setup iniziale hai già in mano una base solida, semplice da mantenere e pronta a crescere. Agganciando Bootstrap per dare struttura all’interfaccia e qualche animazione leggera durante l’attesa della risposta dell’LLM, l’esperienza diventerebbe subito più fluida e piacevole.

Ma il vero divertimento inizia adesso, potremmo introdurre un avatar virtuale che ci assista, presenti i contenuti e ci guidi tra le funzioni, trasformando una semplice chat in un’interazione più naturale e memorabile. Da qui si aprono strade praticamente infinite, streaming token-by-token, prompt library condivisa, logging e analisi delle conversazioni, autenticazione, fino a integrazioni con servizi esterni.

In sintesi, abbiamo costruito le fondamenta giuste. Il prossimo passo è rifinire la UX e sperimentare con piccoli miglioramenti iterativi che, sommati, faranno la differenza tra un prototipo funzionale e uno strumento capace di generare valore reale.

Se l’articolo ti è piaciuto restiamo in contatto su linkedin a https://www.linkedin.com/in/andreatonin/

Laragon #PHP #SviluppoLocale #LMStudio #LLM #API #Apache #MySQL #SviluppoWeb #DevTools

Banner

Releated Posts

ComfyUI – Reference Conditioning

In ComfyUI (soprattutto con modelli come Flux) il Reference Conditioning è un nodo che permette di usare una…

DiByAndrea Tonin Apr 9, 2026

ComfyUI per generare anime: NewBie Image Exp0.1

NewBie Image (spesso indicato come NewBie-image-Exp0.1) è un modello text-to-image in stile anime/ACG pensato per generare illustrazioni con…

DiByAndrea Tonin Apr 9, 2026

Comfy UI: OpenPose

Quando si parla di “OpenPose” in giro per ComfyUI, spesso si intende una cosa molto concreta: prendere una…

DiByAndrea Tonin Apr 9, 2026

L’AI in Cooperativa come scelta organizzativa

Negli ultimi mesi sono stato molto impegnato con docenze in cooperative anche molto diverse tra loro. In aula…

DiByAndrea Tonin Apr 9, 2026