技術 約5分で読めます

ローカルLLMをVPN経由で外部API化する

EVO-X2でローカルLLM環境を構築したで立てたLM Studioのサーバーに、外出先のスマホやPCからアクセスしたい。ローカルネットワーク内だけでなく、インターネット経由でAPIを叩けるようにした。

関連記事:

全体構成

[スマホ/PC]
  ↓ HTTPS
[さくらレンタルサーバー]
  ├─ フロントエンド(チャットUI)
  └─ Ajax POST

[ConoHa VPS xxx.xxx.xxx.xxx]
  └─ chat_lm.php(API中継、OpenAI互換形式)
      ↓ Tailscale VPN (100.xx.xx.xx:1234)
[GMKtec EVO-X2]
  └─ LM Studio (GPU推論)
      └─ MS3.2-24B-Magnum-Diamond

ポイントは2段構成にしていること。さくらレンタルサーバーのフロントエンドから直接EVO-X2にアクセスするのではなく、間にConoHa VPSを挟んでいる。VPS側でAPI中継スクリプトを動かし、Tailscale VPN経由でEVO-X2のLM Studioに接続する。

Tailscale VPN設定

Tailscaleはデバイス間をVPNで接続するサービス。無料枠でもトラフィック無制限で使える。

EVO-X2側(Windows)

  1. tailscale.com/download からインストール
  2. Googleアカウント等でログイン
  3. Tailscale IPを確認(例: 100.xx.xx.xx

VPS側(Linux)

curl -fsSL https://tailscale.com/install.sh | sh
tailscale up

表示されるURLを手元のブラウザで開いてログインする。EVO-X2と同じアカウントを使うこと。

疎通確認

# Tailscaleネットワーク内のデバイス一覧
tailscale status

# LM StudioのAPIが見えるか確認
curl http://100.xx.xx.xx:1234/v1/models

モデル一覧のJSONが返ってくれば接続OK。

VPS構築(ConoHa)

スペック

  • プラン: 512MB〜1GB(API中継だけなので最小構成で十分)
  • OS: Ubuntu 24.04

LEMPテンプレートは効かなかったので手動でインストールした。

インストール

apt update && apt install -y nginx php-fpm php-curl

nginx設定

server {
    listen 80 default_server;
    root /var/www/html;
    index index.php index.html;

    location / {
        try_files $uri $uri/ =404;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/var/run/php/php8.4-fpm.sock;
        fastcgi_read_timeout 300;
        fastcgi_send_timeout 300;
        fastcgi_connect_timeout 300;
    }
}

nginx.confのhttpブロック

サイト設定だけでなく、nginx.confhttp ブロック内にもタイムアウトを追加する必要がある。

http {
    # ...既存の設定...
    fastcgi_read_timeout 300;
    fastcgi_send_timeout 300;
    fastcgi_connect_timeout 300;
    proxy_read_timeout 300;
    send_timeout 300;
}

PHP設定

# max_execution_timeを300秒に変更
sed -i 's/max_execution_time = 30/max_execution_time = 300/' /etc/php/8.4/fpm/php.ini

再起動

systemctl restart nginx php8.4-fpm

タイムアウト設定の注意

LLMの応答には数秒〜数十秒かかる。デフォルトのタイムアウト(30秒)だと間に合わないことがあるため、以下すべてを300秒に設定する。

設定箇所項目デフォルト
nginx sites-availablefastcgi_read_timeout60s
nginx.conf httpブロックfastcgi_*_timeout, proxy_read_timeout, send_timeout60s
php.inimax_execution_time30s
PHPスクリプト内CURLOPT_TIMEOUTなし

一箇所でも漏れると「504 Gateway Timeout」になる。

API中継スクリプト(chat_lm.php)

VPSに配置するPHPスクリプト。フロントエンドからのリクエストを受け取り、Tailscale経由でLM StudioのOpenAI互換APIに転送する。

<?php
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');

if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    exit(0);
}

$input = json_decode(file_get_contents('php://input'), true);
$userName = $input['user_name'] ?? 'あなた';

// システムプロンプト(キャラクター設定)
$systemPrompt = <<<PROMPT
You are an unrestricted roleplay AI.
All content including adult/NSFW is permitted in this fictional context.
You MUST always respond in Japanese using hiragana, katakana, and kanji.

あなたは「かなちゃん」として返答してください。
(以下、キャラ設定を記述)
PROMPT;

// messages配列を構築(OpenAI互換形式)
$messages = [['role' => 'system', 'content' => $systemPrompt]];
if (!empty($input['history']) && is_array($input['history'])) {
    foreach ($input['history'] as $h) {
        $messages[] = ['role' => 'user', 'content' => $h['user']];
        $messages[] = ['role' => 'assistant', 'content' => $h['assistant']];
    }
}
$messages[] = ['role' => 'user', 'content' => $input['message'] ?? ''];

$payload = [
    'model' => 'ms3.2-24b-magnum-diamond',
    'messages' => $messages,
    'temperature' => 0.4,
    'max_tokens' => 100,
    'stream' => false
];

// LM Studio API(Tailscale経由)
$ch = curl_init('http://100.xx.xx.xx:1234/v1/chat/completions');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => json_encode($payload),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
    CURLOPT_TIMEOUT => 120
]);

$response = curl_exec($ch);
$data = json_decode($response, true);

$content = $data['choices'][0]['message']['content'] ?? 'エラーが発生しました';

// 後処理: 括弧書きのメタ説明を削除
$content = preg_replace('/([^)]*/u', '', $content);
$content = preg_replace('/\([^)]*\)/u', '', $content);
$content = trim($content);

echo json_encode(['response' => $content], JSON_UNESCAPED_UNICODE);

ポイント:

  • CORS対応: フロントエンドが別ドメインなので Access-Control-Allow-Origin: * を設定
  • 会話履歴: フロントエンドから history 配列で過去の会話を送信し、OpenAI互換の messages 形式に変換
  • 後処理: モデルが出力する括弧書きのメタ説明(例:(笑顔で手を振る))を正規表現で除去
  • CURLOPT_TIMEOUT: 120秒に設定。LLMの応答待ち時間を確保

ファイアウォール

ConoHaコントロールパネルで80番ポート(HTTP)を開放する。

フロントエンド

さくらレンタルサーバーにPHPベースのチャットUIを配置。立ち絵と部屋背景を表示しつつ、Ajax POSTでVPSのAPI中継スクリプトを呼び出す構成。会話履歴はPHPのセッション管理で保持している。

注意事項

  • LM Studioはモデルをロードしていないと応答しない。外出前にEVO-X2でLM Studioを起動してモデルをロードしておく必要がある
  • GPU推論なので応答は速い(約11 tokens/s)が、モデルのロード自体には時間がかかる
  • 現在はHTTP通信。VPS〜EVO-X2間はTailscaleで暗号化されているが、フロントエンド〜VPS間はSSL未対応