ハンズオン 3: mitmproxy で生の LLM 通信を覗く¶
LiteLLM はクライアントから OpenAI 互換の形で受けて、裏で Anthropic / Gemini 等のプロバイダ固有形式に翻訳してから外に投げている。この翻訳された後の生 HTTP リクエスト / レスポンスを mitmproxy で直接覗く。
ゴール¶
- LiteLLM が「裏で何を送っているか」を自分の目で確認する
- Anthropic / OpenAI / Gemini のネイティブ API 形式の違いを実機で見る
- 「デバッグで一番真実に近いレイヤ」としての mitmproxy の使い所を把握する
事前準備¶
mise run up:litellmで LiteLLM + mitmproxy が両方起動している.envに少なくとも 1 つの LLM API キーが入っている- ハンズオン 1 でチャットを投げた履歴が少し溜まっている
本リポジトリでの mitmproxy の位置付け¶
services/litellm/docker-compose.yml を見ると、mitmproxy は LiteLLM のアウトバウンド HTTP プロキシとして設定されている:
litellm:
environment:
- HTTP_PROXY=http://mitmproxy:8080
- HTTPS_PROXY=http://mitmproxy:8080
- REQUESTS_CA_BUNDLE=/home/mitmproxy/.mitmproxy/mitmproxy-ca-cert.pem
- SSL_CERT_FILE=/home/mitmproxy/.mitmproxy/mitmproxy-ca-cert.pem
つまり LiteLLM が Anthropic / OpenAI / Gemini に投げる全 HTTPS リクエストが mitmproxy 経由になっていて、mitmproxy が TLS を MITM して中身を読める状態にしている (テスト / デバッグ用途)。
手順¶
1. mitmweb にアクセス¶
.env の MITMPROXY_WEB_PASSWORD でログイン (空なら認証なし)。
mitmweb の UI が開く。左側にリクエスト一覧、右側に選択したリクエストの詳細。
2. Open WebUI から何か 1 つ質問を投げる¶
ハンズオン 1 の要領で、Open WebUI から新しい質問を投げる:
3. mitmweb に戻って観察¶
リクエスト一覧に新しい行が追加されているはず。ホスト名は:
- Anthropic モデルを使ったなら
api.anthropic.com - OpenAI モデルなら
api.openai.com - Gemini なら
generativelanguage.googleapis.com
を叩く行が並ぶ。
4. リクエスト詳細を開く¶
1 つ選んで、右側の詳細を見る:
Request タブ
- URL (プロバイダのネイティブエンドポイント)
- Headers (認証トークンが入っている、機密なので扱い注意)
- Body (JSON、プロバイダネイティブ形式)
OpenAI の場合:
{
"model": "gpt-5.4",
"messages": [
{ "role": "system", "content": "..." },
{ "role": "user", "content": "富士山の高さは?" }
]
}
system も messages 配列内に入る。
Anthropic の場合:
{
"model": "claude-sonnet-4-6",
"max_tokens": 4096,
"messages": [
{ "role": "user", "content": "富士山の高さは?" }
],
"system": "..."
}
systemが別フィールドになっているのが OpenAI 形式との違い (theory 02 の表と見比べ)max_tokensが必須
Gemini の場合:
{
"contents": [
{ "role": "user", "parts": [{ "text": "富士山の高さは?" }] }
],
"systemInstruction": { "parts": [{ "text": "..." }] }
}
全く別物。messages ではなく contents、role 名も user / model、parts で画像等も同じ配列に混在できる構造。
5. Response を見る¶
同じリクエスト行の Response タブで、プロバイダからの生レスポンスを確認。
Anthropic の場合:
{
"id": "msg_xxxxxx",
"type": "message",
"role": "assistant",
"content": [
{ "type": "text", "text": "富士山の高さは約 3,776 メートルです。" }
],
"model": "claude-sonnet-4-6",
"stop_reason": "end_turn",
"usage": {
"input_tokens": 12,
"output_tokens": 28
}
}
contentが配列になっていて、各要素がtypeを持つusageのフィールド名がinput_tokens/output_tokens(OpenAI はprompt_tokens/completion_tokens)stop_reasonはend_turn(OpenAI はstop)
LiteLLM はこれをOpenAI 互換形式に変換してクライアントに返している。theory 02 の「OpenAI 互換がデファクト」の翻訳レイヤがここで動いている。
6. ツール呼び出しのリクエストを見る (次のハンズオン後に)¶
ハンズオン 4 で agent-demo を動かすと、tools フィールドを含むリクエストが見えるようになる。その時点でもう一度 mitmweb に戻ってくると、ネイティブ形式のツール定義がどう表現されているかが確認できる (OpenAI は tools / Anthropic は tools / Gemini は tools.functionDeclarations)。
7. ストリーミングのチャンクを見る¶
Open WebUI から応答を受け取ると、実際には SSE (Server-Sent Events) でストリーミングされていることが多い。mitmweb では Response の中身がチャンクごとに分かれて表示される (data: {...} の繰り返し)。各チャンクには 1 〜 数トークンずつの差分が入っている。
これが LLM が「1 トークンずつ生成している」実機証拠。theory 15 LLM の仕組み の「次トークン予測の連鎖」を目で見ている状態。
何に使えるか¶
mitmproxy は普段使いのツールではないが、以下の場面で活躍する:
- LiteLLM の翻訳が怪しいとき: クライアント側のエラーが分かりにくいときに、生のプロバイダレスポンスを見る
- プロバイダ固有機能のデバッグ: Anthropic の
cache_control/ Gemini のthinkingConfig等、LiteLLM 互換層ではうまく通らない機能 - API 使用量の確認: 何を送ったか / レスポンスが何だったかを正確に把握
- プロバイダ固有の制限に引っかかったとき: rate limit / content filter / その他エラーの生フォーマット
本番では動かしっぱなしにせず、調査時だけ有効化するのが通例。
セキュリティ上の注意¶
- mitmproxy は TLS MITM をするので、認証トークンが mitmweb の UI から見える。機密扱いすること
- ホストに向けて公開しない (本リポジトリでは Traefik で LAN 内限定の
*.home.arpaに限定済み) - 本番環境では無効化するか、アクセス制御を厳しくする
観察できた現象の対応章¶
| 観察 | 対応する座学 |
|---|---|
| プロバイダごとに request/response 形式が違う | 02 LLM の 1 回の呼び出し "API は会社ごとに違う" |
| LiteLLM が形式を翻訳している | 同上 |
| usage のフィールド名が違う | 03 トークンとコンテキストウィンドウ |
| SSE チャンクで 1 トークンずつ来る | 15 LLM の仕組み "次トークン予測の連鎖" |