ローカル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)
- tailscale.com/download からインストール
- Googleアカウント等でログイン
- 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.conf の http ブロック内にもタイムアウトを追加する必要がある。
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-available | fastcgi_read_timeout | 60s |
| nginx.conf httpブロック | fastcgi_*_timeout, proxy_read_timeout, send_timeout | 60s |
| php.ini | max_execution_time | 30s |
| 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未対応