ことのはじめ
普段ローカルLLMを動かすのに使っている自宅の RTX5090。一応既に LMStudio の LM Link で外出先でもラップトップからは使えるけど、OpenAI 互換 API として叩けたらもっと色々使えるのになと思っていた。
あと、外部に立てた自作サービスからヘビーユーズしたい(RAG の ingest とか)要求に対して、
- オンラインモデル:高い… ペイするかを考えずにだらだら使いたい
- 無料枠(OpenRouter とか):無限には使えないし、渡せる情報か注意しないといけない
- サブスク枠(codex app server とか):人に使わせるのはどうなんだろう、適正用途でも何かの拍子に BAN されるのは御免
ということで、LMStudio をホストして OpenAI 互換 API として公開してみるか、と。
ゴール
自宅PC上の LM Studio を、
- OpenAI SDK
- OpenCode
- Cline
- その他 OpenAI互換API 対応フレームワーク
から利用可能なエンドポイントとして WWW に公開する。
こう。オレオレ SaaS 状態。
curl https://api.独自ドメイン/v1/chat/completions \
-H "Authorization: Bearer sk-オレオレAPIキー" \
-H "Content-Type: application/json" \
-d '{
"model":"qwen3.6-35b-a3b",
"messages":[
{
"role":"user",
"content":"hello"
}
]
}'
Client
↓ Authorization: Bearer sk-xxxx
https://api.独自ドメイン/v1/
↓ Cloudflare Worker
↓ CF-Access-Client-Id
↓ CF-Access-Client-Secret
https://llm.独自ドメイン
↓ Cloudflare Access
↓ Cloudflare Tunnel
LM Studio
※サブドメインの api., llm. は適当に名付けただけです。よしなに。
※Workers は無料枠を使おうとしているので一応制限はあるけど、この記事作成時点では 10万リクエスト/日なので、実質無制限かなと思い採用。例えば Cline の場合はカスタムヘッダーが付けられるので Workers なしの最小構成でも行けた。
LM StudioをCloudflare Tunnelで公開
LM Studioを http://localhost:1234 で起動。
Cloudflare Tunnelで llm.独自ドメイン → localhost:1234 へルーティング。
Cloudflare Tunnel に Public hostname を追加した直後からバックエンドは公開状態になる。Access 未設定の場合、Public hostname追加後すぐにAccessで保護するか、次章の手順により先にAccess Applicationを作成しておく。
↑ 私は既にドメインに対して Access 保護を張っていて、今回は公開アプリケーションを追加で生やすだけだった。Access 自体未設定の場合は注意。
これで WWW 側からローカルホストの LMStudio に到達可能となっているはず。
Cloudflare Accessで保護
自作 API キーを Worker で照合するだけではちょっと怖いので、llm.独自ドメイン のAccess Application を作成して二段構えにする。
-
ALLOWポリシー(メールアドレス)
ドメイン配下に API エンドポイント以外の通常の Web ページも置く場合など。今回の構成では必須ではない -
SERVICE AUTHポリシー(Service Token)
こっちが大事
Service Token を発行。
トークンの期限は自己責任でよしなに。
取得できる値は以下の2つ。いつでもローテートできる。
CF_ACCESS_CLIENT_ID
CF_ACCESS_CLIENT_SECRET
SERVICE AUTHポリシー設定時に今取得したトークンを指定。
確認:
curl https://llm.独自ドメイン/v1/models \
-H "CF-Access-Client-Id: xxx" \
-H "CF-Access-Client-Secret: yyy"
モデル一覧が返れば成功。
Worker 作成、 api サブドメイン設定
Cline 等、カスタムヘッダーが付与できるツールと併用する場合は独自 Bearer キーと api. サブドメインが不要になるのでこの章の設定は省略可能。
ここで、Cloudflare Accessは
CF-Access-Client-Id
CF-Access-Client-Secret
を要求する。しかし一般的なクライアントは
Authorization: Bearer xxx
しか送れない。そこで両者のマッピング用に Cloudflare Worker を作成する。
Workers & Pages からアプリケーションを作成。
Hello World でOK。
適当に命名。
今作った Worker が開くので設定をクリック。
先程取得した ID と SECRET を登録。あと、ここで API 利用者に渡す(bearer 認証用の)API_KEY も適当に命名して登録。
API_KEY はキー名も値も何でも良い。OpenAI のキーを真似つつ、推測されにくいように、sk-(UUID 的な文字列)にしておいた。
Worker 実装例:
export default {
async fetch(request, env) {
const auth = request.headers.get("Authorization");
if (auth !== `Bearer ${env.MY_API_KEY}`) {
return new Response("Unauthorized", {
status: 401
});
}
const url = new URL(request.url);
const upstream = new URL(
url.pathname + url.search,
"https://llm.独自ドメイン"
);
const headers = new Headers(request.headers);
headers.set(
"CF-Access-Client-Id",
env.CF_ACCESS_CLIENT_ID
);
headers.set(
"CF-Access-Client-Secret",
env.CF_ACCESS_CLIENT_SECRET
);
return fetch(upstream, {
method: request.method,
headers,
body: request.body
});
}
}
先程シークレット定義を行った設定画面の右上からコードの編集画面を開いて上記の実装をコピペ。キー名とドメインは書き換えて。
次に、Workerに api.独自ドメイン を割り当てる。ユーザーが意識するエンドポイントになる。
動作確認
モデル一覧:
curl https://api.独自ドメイン/v1/models \
-H "Authorization: Bearer sk-xxxx"
チャット:
curl https://api.独自ドメイン/v1/chat/completions \
-H "Authorization: Bearer sk-xxxx" \
-H "Content-Type: application/json" \
-d '{
"model":"qwen3.6-35b-a3b",
"messages":[
{
"role":"user",
"content":"hello"
}
]
}'
ps1 用:
$ApiKey = "sk-xxxx"
$BaseUrl = "https://api.独自ドメイン"
$body = @{
model = "qwen3.6-35b-a3b"
messages = @(
@{
role = "user"
content = "こんにちは。自己紹介してください。"
}
)
temperature = 0.7
stream = $false
} | ConvertTo-Json -Depth 10
$res = Invoke-RestMethod `
-Uri "$BaseUrl/v1/chat/completions" `
-Method Post `
-Headers @{
"Authorization" = "Bearer $ApiKey"
"Content-Type" = "application/json"
} `
-Body $body
$res.choices[0].message.content
Cline の場合、カスタムヘッダーが追加できるので Bearer キーに変換する必要がなく、Workers なし(Service Token 直通)の最小構成でも行けた。その場合、今回作ったサブドメインで言うと llm. を使用。キーはダミーでOK。
もちろん api. + Bearer キーでも良い。
補足:ハマったポイント
既存 ALLOW ポリシーと競合した話。今回新規に SERVICE AUTH ポリシーだけ追加するケースでは無関係。
先程必須ではないと言ったALLOWポリシーに邪魔された。
私は既にサブドメインをワイルドカードにして *.独自ドメイン を Access で保護していた。そのため api.独自ドメイン も巻き込まれて 302 → cloudflareaccess.com へリダイレクトされた。
蓋を開けてみればそれだけなんだけど。後述のバイパス設定も地味にミスってて結構時間溶けた。
対処として、Access Application を追加。対象は *.独自ドメイン。ここで、ポリシー無し ≠ Bypassだった。
(必要)って書いてあるのに最初空欄にしていて地味にハマった。空のままでも保存はできてしまうという。ちゃんとバイパスを明示選択しよう。
また、ポリシールールを空のままにすると保存しても反映されなかったため、とりあえず国でフィルターしておいた。
Access コントロール → アプリケーションに戻り、api.独自ドメイン 用アプリケーションを追加。
(元々あった)ALLOWポリシー用のアプリケーションと、今足した BYPASSポリシー用のアプリケーションが並んだ。
Codex に使わせようとして失敗
OpenAI互換APIとしては完成。
ところでここ見ていて、独自ドメイン+ヘッダーを Codex に渡せたら使えるかなと思ったんだけど、
[model_providers.hoge]
name = "LM Studio"
base_url = "https://llm.独自ドメイン/v1"
wire_api = "responses"
http_headers = {
"CF-Access-Client-Id" = "xxxx.access",
"CF-Access-Client-Secret" = "xxxx"
}
手元の Codex CLI 0.128.0 は /v1/responses を利用するため、tools.N.type 周辺の互換性で LM Studio 側と衝突するっぽい。
エラー吐いた。
[Server Error] {
"error": {
"message": "Invalid",
"type": "invalid_request_error",
"param": "tools.14.type",
"code": "invalid_string"
}
}
色々やってみたけど結局動かず。前に LMStudio 使えたはずだけどなーと思ったら
model = "qwen3.6-35b-a3b"
oss_provider = "lmstudio"
codex --oss
という、正規ルートっぽい構成で成功しただけだった。でもこれだと独自エンドポイント+独自ヘッダー CF-Access-Client-Id, CF-Access-Client-Secret を指定することはできない。
諦め。誰か動いたら教えてください…
まとめ
最終構成:
Client
↓ Bearer API Key
Cloudflare Worker
↓ Service Token
Cloudflare Access
↓ Tunnel
LM Studio
Cloudflare Access の Service Token をそのまま配るのではなく、Workerで Bearer API Key に変換することで、一般的な OpenAI互換クライアントから利用可能になった。
また、ドメインを Access のワイルドカード保護下に置いている場合、api.独自ドメイン 専用の Access Application を作成し、BYPASS ポリシーを明示指定して除外する必要があった。
























