はじめに
LLM API の接続で詰まったとき、私はつい SDK の書き方から疑いがちです。base_url の渡し方、client の初期化、環境変数の読み込み順など、見る場所が一気に増えます。
ただ、OpenAI 互換のルーターを使っているなら、SDK を見る前に HTTP レイヤーだけでかなり切り分けられます。今回は Flatkey AI の OpenAI 互換エンドポイントを例に、curl だけで 200 OK まで確認した手順をメモします。
この記事でやること
- API key と base URL が届いているかを見る
-
modelが今の key で使えるかを見る - stream なしの Chat Completions で最小応答を見る
- stream ありの SSE 応答を curl で見る
- SDK に進む前に、どこで詰まっているかを分ける
なぜ curl から始めるか
SDK は便利ですが、失敗したときに見る層が多いです。依存ライブラリの version、client の生成方法、環境変数の読み込み、retry 設定、timeout 設定が同時に関係します。
curl なら、まず HTTP request と response だけに寄せられます。header が足りないのか、JSON が壊れているのか、model 名が違うのか、stream の読み方だけが違うのかを、かなり素朴に分けられます。私はここを飛ばすと、実装ではなく前提条件の方で時間を溶かしがちです。
環境
今回確認した前提は次です。
| 項目 | 値 |
|---|---|
| shell | zsh |
| tool | curl |
| base URL | https://router.flatkey.ai/v1 |
| endpoint |
/models, /chat/completions
|
| model | gemini-2.5-flash-lite |
base URL と model は、自分の環境に合わせて置き換えてください。Flatkey AI の場合も、画面やドキュメントで表示されている最新の base URL を見るのが安全だと思います。
なお、今回はあえて Python や Node.js の SDK は使いません。SDK の問題を否定したいのではなく、まず router が素の HTTP request にどう返すかを見たいからです。この段階で失敗するなら、アプリコードに入る前に直すべき前提があります。この段階で成功するなら、次は SDK の初期化や環境変数の渡し方を見ればよい、という分け方です。
まず変数を置く
直書きすると履歴に残るので、私は最低限これだけ環境変数に逃がしています。
export FLATKEY_API_KEY="<your_api_key>"
export BASE_URL="https://router.flatkey.ai/v1"
export MODEL="gemini-2.5-flash-lite"
以降の curl は、この 3 つが入っている前提です。記事用には placeholder を書いていますが、実行時は本物の key を入れます。
/v1/models で key と base URL を見る
最初に Chat Completions を叩くより、/models を見る方が切り分けやすいです。ここで 200 が返るなら、少なくとも key と base URL の組み合わせはサーバーまで届いています。
curl -sS "$BASE_URL/models" \
-H "Authorization: Bearer $FLATKEY_API_KEY"
今回の環境では HTTP 200 が返り、モデル一覧の中に使いたい model がありました。抜粋するとこうです。
{
"id": "gemini-2.5-flash-lite",
"object": "model",
"owned_by": "google gemini"
}
この段階で model が見つからない場合、私は Chat Completions の payload をいじる前に、まず model 名を疑います。公開カタログにある名前と、今の API key で使える名前が同じとは限らないためです。
もう一つ見ておきたいのは、/models が返した一覧をその日のログとして残すことです。昨日使えた model が今日も同じ route で使えるとは限らないので、後から見返せる証拠があると会話が早くなります。
stream なしで /v1/chat/completions を見る
次に、stream なしで一番小さい会話を投げます。ここでは返答を固定しやすいように、短い文だけ返してもらいます。
curl -sS "$BASE_URL/chat/completions" \
-H "Authorization: Bearer $FLATKEY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "'"$MODEL"'",
"messages": [
{ "role": "user", "content": "Reply with only: flatkey-curl-ok" }
],
"temperature": 0,
"max_tokens": 16
}'
私の環境では HTTP 200 で、本文の抜粋はこうでした。
{
"model": "gemini-2.5-flash-lite",
"choices": [
{
"message": {
"content": "flatkey-curl-ok"
}
}
],
"usage": {
"total_tokens": 17
}
}
ここまで通れば、少なくとも次の 4 点は確認できています。
- Authorization header がサーバーに届いている
- JSON body と
Content-Typeが最低限正しい -
modelがこの endpoint で使えている - 非 stream の response body を受け取れている
SDK 側の client 初期化を見るのは、この後でよいと思います。
-w で HTTP ステータスを分けて見る
curl の出力だけを眺めていると、JSON body と HTTP status が混ざって見づらいです。私は一度ファイルに落として、status を別に出すことが多いです。
curl -sS -o /tmp/flatkey-chat.json -w '%{http_code}\n' \
"$BASE_URL/chat/completions" \
-H "Authorization: Bearer $FLATKEY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "'"$MODEL"'",
"messages": [
{ "role": "user", "content": "Reply with only: flatkey-curl-ok" }
],
"temperature": 0,
"max_tokens": 16
}'
これで標準出力には 200 だけが出ます。body は /tmp/flatkey-chat.json に残るので、あとで jq などで見ればよいです。
jq '.choices[0].message.content, .usage.total_tokens' /tmp/flatkey-chat.json
この形にしておくと、CI やサポート用の一時確認でも扱いやすいです。雑に全部貼るより、status と body を分けた方が人間にもログにも優しい気がします。
stream ありは -N で見る
stream の確認では stream: true を付け、curl には -N を付けます。-N は buffering を抑えて、SSE の data: 行をその場で見たいときに使っています。
curl -sS -N "$BASE_URL/chat/completions" \
-H "Authorization: Bearer $FLATKEY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "'"$MODEL"'",
"messages": [
{ "role": "user", "content": "Reply with only: stream-ok" }
],
"temperature": 0,
"max_tokens": 16,
"stream": true
}'
今回の環境では、だいたい次のような SSE が返りました。
data: {"choices":[{"delta":{"content":"","role":"assistant"},"finish_reason":null,"index":0}],"usage":null}
data: {"choices":[{"delta":{"content":"stream-ok"},"finish_reason":null,"index":0}],"usage":null}
data: {"choices":[{"delta":{},"finish_reason":"stop","index":0}],"usage":null}
data: {"choices":[],"usage":{"total_tokens":11}}
data: [DONE]
ここで見たいのは、きれいな文章ではなく、data: が分割して流れてくることと、最後に [DONE] まで届くことです。stream なしは通るのに stream ありだけ詰まる場合は、SDK より先に proxy、timeout、buffering、SSE の読み取り側を疑う余地があります。
どこで詰まったかを分ける
私の中では、curl の確認はだいたいこの順番です。
| 見る場所 | curl で確認すること | 次に疑うもの |
|---|---|---|
| key |
/models が 200 になるか |
key の値、環境変数、Bearer の付け忘れ |
| base URL |
/v1/models に到達するか |
host、path、末尾の /v1
|
| model | 一覧に model があるか | model 名、権限、利用可能な route |
| body | 非 stream chat が 200 になるか |
messages、JSON、Content-Type
|
| stream |
data: と [DONE] が見えるか |
-N、proxy buffering、timeout |
この表の上から順に潰すと、SDK の issue なのか、HTTP の issue なのかを分けやすくなります。逆に /models が通っていない状態で SDK のオプションを変え続けると、私はかなり遠回りしがちです。
実行ログとして残すもの
あとで誰かに相談する可能性があるなら、curl の結果はその場で少し整えて残しておくと助かります。私なら、時刻、base URL、endpoint、model、HTTP status、response body の抜粋、stream の場合は [DONE] まで届いたかを残します。
逆に、API key、社内の完全な request ID、顧客データを含む prompt は残さないか、必ず伏せます。疎通確認の記事や issue に貼るなら、content は flatkey-curl-ok のような固定文字列にしておく方が安全です。再現に必要な情報と、公開すると困る情報を分けるだけでも、デバッグの心理的な負担が下がると思います。
まとめ
OpenAI 互換ルーターの疎通確認は、いきなりアプリに組み込むより、curl だけで段階を分ける方が楽でした。
- まず
/modelsで key と base URL を見る - 次に stream なしの
/chat/completionsで 200 と本文を見る - HTTP status と body は分けて保存すると調査しやすい
- stream ありは
curl -Nでdata:と[DONE]を見る - ここまで通ってから SDK の設定を見る
この順番にしておくと、作業メモもそのまま再現手順として渡しやすいですし、少しだけ安心です。
Flatkey AI は OpenAI 互換の base URL と Bearer key で呼べるので、この手順をそのまま小さい疎通確認に使えました。ただ、どのサービスでも base URL と model 名は手元の画面で確認するのが一番確実だと思います。
おわりに
SDK の設定で詰まったと思っていたものが、実は model 名や base URL の取り違えだった、ということは普通にあります。私も何度かやっています。
curl だけで 200 OK を見てから SDK に戻ると、疑う範囲がかなり狭くなります。間違いあったらコメントください。よろしくお願いします。