AIプロキシとLLMゲートウェイ: 何が違うのか
ネットワーク転送、APIゲートウェイ、モデル経路を分け、仕事に合う層を選んでください。
人は「AIプロキシ」を、三つの別の箱に使います。ネットワークの転送器、逆方向のAPIゲートウェイ、モデルのルーターです。この案内は、仕事を混ぜずに一つの層を選ぶ必要がある開発者向けです。IPアドレスは売りません。鍵も渡しません。プロキシが口座や地理の資格を変えるとも言いません。
01人が「AIプロキシ」でたいてい意味すること
この言い方は標準ではありません。RFC 9110(HTTP Semantics、2022年6月、STD 97)はすでに仲介をプロキシ、ゲートウェイ、トンネルに分け、一つのプロセスが要求ごとに役割を切り替えられると警告しています。製品文では、同じ語がまた別のものへ再利用されます。OpenAI風のJSON本文を受け、モデルを選ぶプロセスです。
図では三つの仕事は似て見えます。どれもクライアントと上流のあいだに座るからです。失敗する場所は違います。フォワードプロキシはTCPトンネルを開いても、供給元の401を残すことがあります。LLMゲートウェイは、網の経路は問題なくても、未知モデルのJSONエラーをHTTP 200で返すことがあります。間違った層をデバッグすると、間違った秘密を回します。
2026-10-15の区切ったローカル実行が、その分かれを具体にしました。ループバックのHTTP CONNECTプロキシが、TLSをフィクスチャのオリジンへ転送しました。別のループバックプロセスが、ローカルのBearer確認のあと、fixture-alpha 対 fixture-bravo を経路付けました。何も 127.0.0.1 を出ていません。有料のモデルAPIも呼んでいません。フィクスチャJSONは、意図して CONTROLLED_FIXTURE_NOT_A_PROVIDER とラベルしています。
業者が製品を「AIプロキシ」と呼ぶときは、実際に実装している仕事を読んでください。ホームページにgatewayという語があるからといって、再試行、ストリーミング、フォールバックがあると仮定しないでください。
02HTTPとSOCKSのフォワードプロキシ、CONNECTを含む
フォワードプロキシはクライアントが選びます。RFC 9110 §3.7は、いくつかの絶対URIの要求を受け、HTTPで満たそうとするメッセージ転送エージェントと呼んでいます。普通の http URLでは、クライアントは完全なURLをプロキシへ送ります。https では、いつもの型は違います。
RFC 9110 §9.3.6はCONNECTを定義します。クライアントはプロキシへ host:port へのトンネル構築を頼み、任意の 2xx のあと、トンネルが閉じるまで盲目転送に限るよう求めます。そのあとのトンネルは、よくTLSで包まれます。成功したCONNECTは、アプリケーションの成功ではありません。トンネルがあることだけです。それ以外の応答は、トンネルが作られなかった意味です。
SOCKS5はより古く、より下に座ります。RFC 1928(1996年3月)は、アプリケーションとトランスポートのあいだのシムとして説明し、慣例ではTCP 1080です。方式の交渉のあと、クライアントはコマンドを送ります。CONNECTは X'01' です。SOCKSはHTTPメソッドを解析しません。クライアントが指名した宛先へ、TCP(任意でUDP)を中継します。
HTTPライブラリがプロキシ越しにHTTPSを話すなら、たいていCONNECTのあと、オリジンへTLSをします。プロキシが認証するのはトンネルであり、よく Proxy-Authorization です。そのヘッダーは、モデルAPIが期待する Authorization: Bearer ではありません。混ぜると、「curlではプロキシが動くのに、SDKはまだ401」のよくある出所になります。
仕事が「このプロセスは別のIPから出るべき」だけなら、HTTPプロキシ、HTTPSプロキシ、SOCKS5プロキシの一覧から始めてください。それらのページは出口を比べます。モデルのルーターではありません。
03リバースプロキシと普通のAPIゲートウェイ
リバースプロキシは、RFC 9110が§3.7でゲートウェイとも呼ぶもので、クライアントにはオリジンとして見え、入ってきたものを別サーバーへ転送します。クライアントはそれをプロキシURLとして選びません。クライアントはただ https://api.example.com を開きました。
NGINXのリバースプロキシ案内(2026-10-15に参照)は、機械的なループを説明します。要求を proxy_pass の上流へ送り、応答を取り、戻します。既定でNGINXは Host と Connection を書き換えます。仮想ホストを Host で鍵にするAPIでは、その詳細が効きます。リバースプロキシは公開端でTLSを終端し、内側へ新しい接続を開けます。クライアントのTLSセッションはそのとき、モデル業者ではなくリバースプロキシで終わります。
この層は、経路の振り分け(/v1/ 対 /admin/)、負荷分散、ヘッダーの浄化、複数の内部サービスの前に一つのホスト名を出すことに向きます。モデル名が何かは知らなくてよいです。/v1/chat/completions を一つの上流へ転送するだけでも、HTTPの意味ではまだAPIゲートウェイであり、LLMゲートウェイではありません。
「前にNGINXを置いた」を、再試行、トークン予算、モデルのフォールバックの証明にしないでください。それらは一部の製品が足す追加機能です。読んだNGINXのページは、それらを主張していません。
04LLMゲートウェイ: 鍵、上限、経路、ログ
LLMゲートウェイはアプリケーションのサービスです。クライアントは、チャットまたは埋め込みの本文を一つのURLへ送ります。ゲートウェイは仮想鍵を確認し、予算を課すことがあり、供給元とモデルを選ぶことがあり、トークンを記録することがあり、そのあとだけ業者へ話します。複数の業者へ話すこともあります。
LiteLLMのAI Gatewayページ(2026-10-15にHTTP 200)は、OpenAI互換の端点を一つ出す自己ホストサーバーを説明しています。文書化された仕事には、鍵ごとのモデルとレート制限付きの仮想鍵、支出の追跡、負荷分散、フォールバック、再試行、ガードレール、ログがあります。同じページは、クライアントがOpenAI APIを使えるなら、クライアントコードを変えずにゲートウェイを使えると書いています。それはチャット補完の要求の形についての製品主張です。すべてのOpenAI端点、ヘッダー、エラーが1対1で写る証明ではありません。LiteLLMは導入も実行もしていません。
Cloudflare AI Gateway(ページの dateModified は2026-04-20、取得は2026-10-15)は、分析、ログ、キャッシュ、レート制限、要求の再試行、モデルのフォールバックを機能として列挙しています。動的経路のページ(2026-10-02更新)は互換についてより正確です。動的経路が受けるのはOpenAIチャット補完の要求の形だけです。Anthropic MessagesはHTTP 400を返します。その文が、「OpenAI互換」を狭く保つ理由です。Cloudflareは呼んでいません。再試行もフォールバックも実行していません。ゲートウェイの文書がそれらの機能を列挙していても、その製品に対して実行するまでは、出典だけの扱いです。
ローカルでは、それらの製品ではない小さなルーターを作りました。Authorization: Bearer local-demo-key-not-a-provider-secret を受け、fixture-alpha と fixture-bravo を二つのフィクスチャ本文へ写し、有効なローカルトークンが無いと401を返し、未知モデルには400を返しました。制御面がCONNECTではなくHTTP JSONに座ることを示すには、それで足ります。
05要求を追う: TLS、Host、本文を読めるのは誰か
一つのPOST /v1/chat/completions を、両方の設計へ通してください。
ループバック実行で使ったフォワード経路: クライアントは 127.0.0.1:54318 へTCPを開き、CONNECT 127.0.0.1:54316 HTTP/1.1 を送り、200 Connection Established を受け、そのあとオリジン証明書へTLSを終えました。TLSのあとだけ、JSON本文と、オリジン向けのBearerヘッダーを送りました。CONNECTのプロセスは対象 127.0.0.1:54316 を記録しました。フィクスチャJSONもBearerトークンも見ていません。トンネル上で観測したHTTP本文断片のトークン一覧は空でした。TLSはフィクスチャのオリジンで終わりました。オリジンは制御したフィクスチャでHTTP 200を答えました。
ゲートウェイ経路: クライアントはCONNECTなしで 127.0.0.1:54317 へTCPを開きました。そのホップではデモにTLSは使いません(ループバックの素のHTTP)。ゲートウェイは、その要求にオリジンが存在する前に、JSONの model 欄とローカルBearerトークンを読みました。無いまたは間違ったローカル認証は、成功した経路としてモデルフィクスチャへ届きませんでした。どちらも invalid_local_gateway_auth のHTTP 401でした。未知モデルは、認証が通ったあと unsupported_model_or_route のHTTP 400でした。トランスポートは上がっていました。アプリケーションが仕事を拒みました。
本番のHTTPSでは、LLMゲートウェイはほぼ必ずTLSを終端します。JSONを解析し、予算を適用し、モデルを選ぶなら、そうする必要があります。TLSの中間箱ではないCONNECTフォワードプロキシは、そのオリジンTLSを終端しません。誰かが法人プロキシでHTTPS検査を有効にするなら、それは別の配備です。プロキシはオリジンへのTLSクライアントになり、あなたへのTLSサーバーになります。CONNECTが成功したからといって、検査を仮定しないでください。
OpenAIのAPI概要(2026-10-15取得。依頼したplatform.openai.com/docs/api-referenceはここへリダイレクト)は、オリジン要求のBearer資格情報を文書化しています。x-request-id は要求の相関識別子として扱い、x-ratelimit-limit-requests のようなレート制限ヘッダーとは別です。それらの欄は供給元の会話に属します。ゲートウェイは自前の要求IDを足すことがあります。フォワードプロキシは、たいてい足しません。
06互換はクライアントの形であり、API全体ではない
SDKはよく base_url を置かせ、chat.completions.create を使い続けさせます。業者はそれをOpenAI互換と呼びます。Cloudflareの動的経路の注記がその注意です。一つの業者の中でも、互換経路は一つの要求の形だけを受け、別の形には 400 を返すことがあります。
互換端点でも、認証(仮想鍵対供給元の鍵)、モデル名(gpt-4.1 対 azure/gpt-4.1 対ゲートウェイの別名)、ストリーミングのイベント配置、ツール呼び出しの欄、ファイルアップロード、リアルタイムソケット、管理APIはまだ違えます。OpenAI自身の概要は、Responses、Realtime、Administrationを別の面として扱います。/v1/chat/completions を写しても、その木全体は写りません。
ループバックのゲートウェイは、チャット補完に見えるJSON(choices[0].message)を返しました。それはフィクスチャです。デモを製品の意味で「OpenAI互換」にはしません。
07「動いた」の三つの種類
ログを読むときは、この三つの結果を分けておいてください。
| 層 | 成功の例 | 失敗の例 | 証明しないこと |
|---|---|---|---|
| トランスポート / トンネル | CONNECT 200 Connection Established。SOCKS CONNECT応答 | プロキシ403、タイムアウト、TLSハンドシェイクエラー | モデルがプロンプトや鍵を受けたこと |
| 次ホップへのHTTP | JSON本文付きの 200、あるいは 401 / 400 でも | 空の応答、RST、リバースプロキシからのHTTP 502 | JSONがモデルの結果であること |
| アプリケーション / モデル経路 | ゲートウェイが fixture-alpha をalphaフィクスチャへ写す。供給元が補完を返す | ゲートウェイ400の未対応モデル。供給元401の無効API鍵。HTTP 200の中のモデル安全拒否 | ほかの層が健全であること。第二の業者が同じ動きをすること |
2026-10-15、ローカルスクリプトは6件と16のアサーションを記録し、すべて通りました。Python 3.9.6とLibreSSL 3.3.6です。

転送はオリジンHTTP 200を出しました。ゲートウェイ経路は二つの別フィクスチャ経路を出しました。認証失敗はゲートウェイに401で留まりました。未対応モデルはゲートウェイに400で留まりました。再試行、ストリーミング、フォールバックは実行しておらず、主張しません。
自分のIPのページは、ブラウザからそのページへ届いたアドレスを報告します。プロキシチェッカーは、渡した端点のサーバー側探査です。どちらも、Python SDKプロセスが取る経路は示しません。LLM APIの資格も立てません。チェッカーは、モデル業者がIPを受けること、鍵が有効なこと、その口座を使ってよいことも証明しません。
08仕事に合う層を選ぶ
問題がTCPセッションの出口なら、フォワードのHTTP CONNECTまたはSOCKS5プロキシを使ってください。法人の出口、業者へ届かない開発用ノート、公開されたデータセンターまたはモバイルの出口です。SDKのプロキシ設定(または HTTPS_PROXY)を構成し、供給元のAPI鍵は、オリジンが見る要求に置いてください。プロキシにモデル選びを期待しないでください。
問題が一つのホスト名、多くの内部サービス、端のTLS、またはヘッダー方針なら、リバースプロキシまたは汎用APIゲートウェイを使ってください。DNSをそのゲートウェイへ向けます。自分で足していない限り、トークン支出を知ると期待しないでください。
問題が多くのクライアント、多くのモデル、一つの方針なら、LLMゲートウェイを使ってください。仮想鍵を発行し、予算を置き、方針が許すならプロンプトを記録し、別名を供給元のモデルへ写します。供給元の鍵はゲートウェイのホストに置き、すべてのノートには置かないでください。業者のいまの互換注記を読んでください。再試行やフォールバックを仮定しないでください。Pythonプロセスが一つだけで共有方針が無いなら、ゲートウェイは余分な動く部品かもしれません。LiteLLM自身の文書も、その軸で「ゲートウェイ対SDK」を分けています。
買うまたは作る前のチェックリストです。
- 実際に見た失敗を書いてください。TCP、TLS、HTTPステータス、またはJSONエラー。
- TLSを終端するのは誰かに名前を付けてください。
- トンネル用、ゲートウェイ用、供給元用の秘密に、それぞれ名前を付けてください。
- クライアントが送るモデル文字列と、供給元が期待する文字列に名前を付けてください。
- ストリーミング、再試行、フォールバックが、その製品で文書化され、試験されているかを確認してください。実行していなければ主張しないでください。
- クライアントの要求の形(チャット補完対別スキーマ)を確認してください。
- 欲しかったのが別の出口IPだけなら、フォワードプロキシで止め、モデルルーターは飛ばしてください。
上の番号付き表とWebP図は、同日の別CONNECT+TLSループバック実験から来ています。その実験は、実際のクライアント選択の転送です。HTTP CONNECTのあと、オリジンへTLSです。下のコピー用レシピではありません。
この公開レシピは、127.0.0.1の素のHTTPだけです。CONNECTトンネルは開きません。TLSも使いません。モデル業者も呼びません。Forward ホップは常に固定の origin_port へPOSTし、クライアントはオリジン形の経路(/v1/chat/completions)を使います。RFC 9110の意味では、それはリバース風の、固定上流の通過であり、クライアントが選ぶHTTPフォワードプロキシではありません。同じスクリプトのアプリケーションモデルルーターと、そのホップを比べてください。期待出力の forward_http を、クライアントが選んだフォワードプロキシの証明として読まないでください。
準備: Python 3.9以上。標準ライブラリだけ。パッケージなし。API鍵なし。
- 次のスクリプトを、所有する任意のディレクトリへ forwarding_vs_routing.py として保存してください。
- 実行: python3 forwarding_vs_routing.py
- stdoutは、スクリプトのあとの期待出力と一致する必要があります。
#!/usr/bin/env python3
"""Plain-HTTP loopback demo: forwarding vs model routing. Stdlib only."""
from __future__ import annotations
import json
import threading
from http.client import HTTPConnection
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
BIND = "127.0.0.1"
KEY = "***"
LABEL = "CONTROLLED_FIXTURE_NOT_A_PROVIDER"
TIMEOUT = 5
class Origin(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def log_message(self, *_args):
return
def do_POST(self):
n = int(self.headers.get("Content-Length") or 0)
self.rfile.read(n)
body = json.dumps(
{
"label": LABEL,
"origin": "loopback-http-origin",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "FIXTURE: forwarded origin",
},
}
],
}
).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.send_header("Connection", "close")
self.end_headers()
self.wfile.write(body)
class Gateway(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
ROUTES = {
"fixture-alpha": {"label": LABEL, "route": "alpha"},
"fixture-bravo": {"label": LABEL, "route": "bravo"},
}
def log_message(self, *_args):
return
def _send(self, code, obj):
raw = json.dumps(obj).encode()
self.send_response(code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(raw)))
self.send_header("Connection", "close")
self.end_headers()
self.wfile.write(raw)
def do_POST(self):
n = int(self.headers.get("Content-Length") or 0)
raw = self.rfile.read(n)
auth = self.headers.get("Authorization") or ""
try:
model = json.loads(raw.decode() or "{}").get("model")
except json.JSONDecodeError:
model = None
if auth != "Bearer " + KEY:
self._send(401, {"error": "invalid_local_gateway_auth", "label": LABEL})
return
route = self.ROUTES.get(model)
if route is None:
self._send(
400,
{
"error": "unsupported_model_or_route",
"requested": model,
"label": LABEL,
},
)
return
self._send(200, route)
class Forward(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
origin_port = 0
def log_message(self, *_args):
return
def do_POST(self):
n = int(self.headers.get("Content-Length") or 0)
body = self.rfile.read(n)
upstream = HTTPConnection(BIND, self.origin_port, timeout=TIMEOUT)
try:
skip = {"host", "proxy-connection"}
headers = {k: v for k, v in self.headers.items() if k.lower() not in skip}
upstream.request("POST", self.path, body=body, headers=headers)
resp = upstream.getresponse()
data = resp.read()
status = resp.status
ctype = resp.getheader("Content-Type") or "application/json"
finally:
upstream.close()
self.send_response(status)
self.send_header("Content-Type", ctype)
self.send_header("Content-Length", str(len(data)))
self.send_header("Connection", "close")
self.end_headers()
self.wfile.write(data)
def start(handler):
httpd = ThreadingHTTPServer((BIND, 0), handler)
httpd.daemon_threads = False
httpd.timeout = 0.3
thread = threading.Thread(
target=httpd.serve_forever,
name="srv-%s" % httpd.server_address[1],
daemon=False,
)
thread.start()
return httpd, thread
def post(port, path, payload, headers):
conn = HTTPConnection(BIND, port, timeout=TIMEOUT)
try:
raw = json.dumps(payload).encode()
hdrs = {"Content-Type": "application/json"}
hdrs.update(headers)
conn.request("POST", path, body=raw, headers=hdrs)
resp = conn.getresponse()
return resp.status, json.loads(resp.read().decode())
finally:
conn.close()
def main():
origin_httpd = gw_httpd = fwd_httpd = None
threads = []
try:
origin_httpd, origin_thread = start(Origin)
threads.append(origin_thread)
Forward.origin_port = origin_httpd.server_address[1]
fwd_httpd, fwd_thread = start(Forward)
threads.append(fwd_thread)
gw_httpd, gw_thread = start(Gateway)
threads.append(gw_thread)
ping = {
"model": "ignored-by-forward",
"messages": [{"role": "user", "content": "ping"}],
}
rows = []
status, body = post(
fwd_httpd.server_address[1],
"/v1/chat/completions",
ping,
{"Authorization": "***"},
)
rows.append(("forward_http", status, body.get("origin") or body.get("error")))
assert status == 200
assert body.get("label") == LABEL
assert body.get("origin") == "loopback-http-origin"
gw = gw_httpd.server_address[1]
status, body = post(
gw,
"/v1/chat/completions",
{"model": "fixture-alpha", "messages": []},
{"Authorization": "Bearer " + KEY},
)
rows.append(("gateway_route_alpha", status, body.get("route") or body.get("error")))
assert status == 200 and body.get("route") == "alpha"
status, body = post(
gw,
"/v1/chat/completions",
{"model": "fixture-bravo", "messages": []},
{"Authorization": "Bearer " + KEY},
)
rows.append(("gateway_route_bravo", status, body.get("route") or body.get("error")))
assert status == 200 and body.get("route") == "bravo"
status, body = post(
gw,
"/v1/chat/completions",
{"model": "fixture-alpha", "messages": []},
{},
)
rows.append(("gateway_auth_missing", status, body.get("error")))
assert status == 401
status, body = post(
gw,
"/v1/chat/completions",
{"model": "fixture-alpha", "messages": []},
{"Authorization": "***"},
)
rows.append(("gateway_auth_invalid", status, body.get("error")))
assert status == 401
status, body = post(
gw,
"/v1/chat/completions",
{"model": "no-such-model", "messages": []},
{"Authorization": "Bearer " + KEY},
)
rows.append(("gateway_unsupported_model", status, body.get("error")))
assert status == 400
print("CONTROLLED_FIXTURE public recipe — not a provider call, not TLS")
for name, status, extra in rows:
print("%s HTTP %s %s" % (name, status, extra))
print("assertions=6 ok")
return 0
finally:
for httpd in (origin_httpd, gw_httpd, fwd_httpd):
if httpd is not None:
httpd.shutdown()
httpd.server_close()
for thread in threads:
thread.join(timeout=TIMEOUT)
if __name__ == "__main__":
raise SystemExit(main())
期待する出力:
CONTROLLED_FIXTURE public recipe — not a provider call, not TLS
forward_http HTTP 200 loopback-http-origin
gateway_route_alpha HTTP 200 alpha
gateway_route_bravo HTTP 200 bravo
gateway_auth_missing HTTP 401 invalid_local_gateway_auth
gateway_auth_invalid HTTP 401 invalid_local_gateway_auth
gateway_unsupported_model HTTP 400 unsupported_model_or_route
assertions=6 ok
forward_http の行は、その固定上流の通過です。表とWebPのCONNECT+TLS実験が、転送の証明のままです。
ネットワークプロキシは、送る必要のあるモデルAPI鍵を変えますか?
HTTP CONNECTはTLSのJSON本文を読みますか?
ゲートウェイ認証が供給元認証と違うのはなぜですか?
「OpenAI互換」は実際に何を意味しますか?
HTTPSのモデルAPIで、SOCKS5はHTTP CONNECTの代わりになりますか?
プロキシチェッカーは、LLM APIがそのIPを受けることを証明しますか?
すべてのLLMゲートウェイは、同じように再試行し、フォールバックし、ストリームしますか?
関連記事
レジデンシャルプロキシとは何か、どう動くか
レジデンシャルプロキシは一般向けISPのアドレスから出ます。プール、スティッキーセッション、サイトから見えるもの、購入前に供給元へ聞くことを説明します。
4分2026年のスクレイピング向けプロキシ: 購入チェックリスト
2026年のスクレイピング購入は、供給元ではなく相手から始めます。出自、セッション、地理、課金、本番前の実地試験のチェックリストです。
5分スティッキーとローテーション: セッション制御の仕組み
スティッキーとローテーション: リクエストごととタイマーのローテーションの動き、スティッキーが新しいIPに勝つとき、CookieとIP変更の関係、セッションIDの自分での試験。
8分

