コマンドラインツールllmが、api_baseで指定した別のサービスへOpenAIのAPIキーを送っていた問題|0.37.2での修正と、当てはまる設定の見分け方

llm 0.37.2で、OpenAIのAPIキーを別のサービスへ送る問題が2件直った

Simon Willison氏が開発するコマンドラインツールllmの0.37.2が、2026年11月12日(日本時間)に公開されました(0.37.2のリリースノート)。llmは、ターミナルからAIモデルに質問できるツールです。OpenAIのモデルのほか、設定ファイルextra-openai-models.yamlにモデルを登録すると、OpenRouterのようにOpenAIと同じ形で呼べる別のサービス(OpenAI互換のサービス)も同じコマンドで使えます。登録では、接続先のURLをapi_baseに、送る鍵の名前をapi_key_nameに書きます。

0.37.2で直ったのは、このapi_baseを設定したモデルで、利用者のOpenAIのAPIキーを接続先へ送ってしまう問題2件です。GitHubで2件のセキュリティ勧告が公開されています(GHSA-v9h7-jj4g-f862、GHSA-r639-rr7r-x72w。深刻度はどちらも「medium」)。1件目は、api_key_nameに書いた名前の鍵を保存していないと、環境変数OPENAI_API_KEYの値を送っていた問題で、2023年7月の0.6から続いていました。2件目は、OpenAIの新しい呼び出し方であるResponses APIで呼ぶモデル(responses: true)のうち、api_key_nameを書いていないものを、従来のChat Completions APIに切り替える-o chat_completions 1付きで呼んだときに、保存したOpenAIの鍵も送っていた問題です。llmの公式ドキュメントは以前から「api_baseを設定するとOpenAIの鍵は送らない」と説明していましたが、この2つの使い方では送っていました。

この記事では、受け取った鍵を記録するだけの検証用サーバーを手元に立て、0.37.1と0.37.2に同じ設定とコマンドを渡して、サーバーに何が届くかを比べました。使った鍵はすべてダミーです。当てはまる設定の見分け方、0.37.2へ上げる手順、上げたあとに出るエラーへの対処、OpenAIの鍵を作り直すかの判断をまとめます。

要点

0.37.2では、api_baseの接続先へOpenAIの鍵が届かなくなった

0.37.1と0.37.2で、15通りの設定と使い方を比べた結果です。

1件目:OPENAI_API_KEYを送る条件

api_key_nameの鍵を保存していない

0.37.1は、代わりにOPENAI_API_KEYの値を接続先へ送りました。

2件目:chat_completionsで鍵を送る条件

api_key_nameのないresponses: trueのモデルに-o chat_completions 1

0.37.1は、llm keysに保存したOpenAIの鍵も、環境変数の鍵も送りました。

0.37.2の動き

api_baseの接続先へOpenAIの鍵を送らない

1件目の使い方は、送信する前に「No key found」で止まりました。2件目の使い方は、DUMMY_KEYを送って答えが返りました。

0.37.2へすぐ上げられないとき

api_key_nameの鍵をllm keys setで保存する

1件目は、0.37.1でも接続先の鍵だけが届きました。2件目は-o chat_completions 1を付けなければ届きません。

api_key_nameの鍵が未保存だと、OPENAI_API_KEYを送っていた

1件目の問題(GHSA-v9h7-jj4g-f862)から見ていきます。api_key_nameには、llm keys setで保存した鍵の名前を書きます。この記事では、api_key_nameに書いた名前で保存した鍵を「api_key_nameの鍵」と呼びます。たとえば、OpenRouterの鍵をllm keys set openrouterで保存し、設定ファイルに次のように書くと、このモデルを呼んだときにOpenRouterの鍵が送られます(model_idはllmで使う名前、model_nameは接続先のサービスでのモデル名です)。

# extra-openai-models.yaml の例
- model_id: or-llama
  model_name: meta-llama/llama-3.3-70b-instruct
  api_base: "https://openrouter.ai/api/v1"
  api_key_name: openrouter

0.37.1までは、openrouterという名前の鍵が保存されていないと、環境変数OPENAI_API_KEYの値を代わりに使い、接続先へ送っていました。GHSA-v9h7-jj4g-f862の勧告は、きっかけとして次の3つを挙げています。

  • api_key_nameの名前を書き間違えた
  • llm keys setで鍵を保存し忘れた
  • 新しいマシンで、OPENAI_API_KEYだけを設定していた

同じ勧告によると、2023年7〜8月に出た0.6〜0.7.1では、api_key_nameの鍵を保存していてもOPENAI_API_KEYの値が優先して送られていました(0.8で修正)。この時期の版で、api_key_nameのモデルとOPENAI_API_KEYを一緒に使っていた場合は、鍵を保存していても当てはまります。

llm openai endpoint addコマンドでモデルを登録するときは、--keyに保存していない名前を渡すと「No stored key named 'thirdparty' - run 'llm keys set thirdparty' first」と表示されて登録できません(0.37.1と0.37.2で確認)。設定ファイルを手で書いたり、別のマシンへ移したりした場合に当てはまりやすい問題です。

手元では、api_baseを検証用サーバー(127.0.0.1)に向け、api_key_name: thirdpartyの鍵を保存しないまま、OPENAI_API_KEYにダミーの値を入れて同じコマンドを実行しました。サーバーが受け取ったAuthorizationヘッダー(リクエストに付けて送る鍵)を、「→」のコメント行に書いています。

$ export OPENAI_API_KEY=sk-CANARY-env-OPENAI_API_KEY   # ダミーの鍵

# 0.37.1
$ llm -m tp-named hi
Hello from canary server
# → サーバーが受け取った鍵: Bearer sk-CANARY-env-OPENAI_API_KEY

# 0.37.2
$ llm -m tp-named hi
Error: No key found - add one using 'llm keys set thirdparty'
# → サーバーへの送信なし

0.37.1では、OpenAIの鍵が接続先へ届き、検証用サーバーの答えも表示されました。0.37.2では送信する前に止まり、保存すべき鍵の名前を示すエラーになります。responses: trueのモデル(Responses APIで呼ぶ)、completion: trueのモデル(Completions APIで呼ぶ)、--no-streamを付けた場合も、届いた鍵と0.37.2のエラーは同じでした。llmをPythonから使う場合も同じで、0.37.1ではllm.get_model("tp-named").get_key()がダミーのOpenAIの鍵を返し、非同期のモデル(llm.get_async_model())もその鍵を送りました。0.37.2ではどちらもNeedsKeyExceptionで止まります。

1件目で送られるのは、環境変数OPENAI_API_KEYの値だけです。環境変数を設定せず、llm keys set openaiで保存したOpenAIの鍵だけがある状態では、0.37.1でも何も送らずにエラーになりました。このとき0.37.1は「No key found - add one using 'llm keys set thirdparty' or set the OPENAI_API_KEY environment variable」と表示し、OPENAI_API_KEYを設定するように案内します。0.37.2のエラー文からは、この案内が消えました。

api_key_nameのないresponses: trueのモデルは、-o chat_completions 1でOpenAIの鍵を送っていた

2件目の問題(GHSA-r639-rr7r-x72w)は、responses: trueを書き、api_key_nameを書いていないモデルで起きます。responses: trueは、Responses APIで接続先を呼ぶ指定です。-o chat_completions 1は、そのモデルを従来のChat Completions APIで呼ぶように切り替えるオプションです。llmのオプションの説明には、互換性のために使うものと書かれています。

api_key_nameを書いていないresponses: trueのモデルは、通常の呼び出しでは鍵の代わりにDUMMY_KEYという固定の文字列を送ります。0.37.1では、-o chat_completions 1を付けたときだけ、OpenAIの鍵を送っていました。1件目と違い、llm keys set openaiで保存した鍵も送られます。

# tp-resp: api_base と responses: true だけを書いたモデル
$ llm -m tp-resp hi
# → 両方の版: Bearer DUMMY_KEY

$ llm -m tp-resp -o chat_completions 1 hi
# → 0.37.1: Bearer sk-CANARY-env-OPENAI_API_KEY
# → 0.37.2: Bearer DUMMY_KEY

# 設定ファイルを使わずに接続先を指定するコマンド(--key なし)
$ llm openai endpoint http://127.0.0.1:18437/v1 -m remote-model \
    --responses -o chat_completions 1 hi
# → 0.37.1: Bearer sk-CANARY-env-OPENAI_API_KEY
# → 0.37.2: Bearer DUMMY_KEY

環境変数を外し、llm keys set openaiで保存したダミーの鍵だけにして同じことを試すと、0.37.1ではその鍵がサーバーへ届きました。api_key_nameを書き、その名前の鍵を保存しているモデルでは、0.37.1でも-o chat_completions 1のときに保存した鍵が正しく届きます。勧告によると、2件目は0.32a2から0.37.1までが対象です。

api_baseだけのモデルとapi_key_nameの鍵を保存したモデルは、0.37.1でもOpenAIの鍵を送らなかった

15通りのうち、届く鍵が0.37.2で変わったのは、ここまでの2件に当てはまる使い方だけです。api_baseだけを書いたモデルや、api_key_nameの鍵を保存したモデルは、0.37.1でもOpenAIの鍵を送っていませんでした。代表的な組み合わせを表にまとめます。

設定と使い方 鍵の置き場所 0.37.1で届いた鍵 0.37.2で届いた鍵
api_key_nameあり(その鍵は未保存) OPENAI_API_KEY OpenAIの鍵 送信なし(No key found)
api_key_nameあり(その鍵は未保存) llm keysのopenaiだけ 送信なし(No key found) 送信なし(No key found)
api_key_nameあり(その鍵を保存済み) OPENAI_API_KEYも設定 保存した接続先の鍵 保存した接続先の鍵
api_baseだけ(Chat Completions) OPENAI_API_KEY DUMMY_KEY DUMMY_KEY
responses: true、api_key_nameなし(通常の呼び出し) OPENAI_API_KEY DUMMY_KEY DUMMY_KEY
responses: true、api_key_nameなしに-o chat_completions 1 OPENAI_API_KEY OpenAIの鍵 DUMMY_KEY
responses: true、api_key_nameなしに-o chat_completions 1 llm keysのopenaiだけ OpenAIの鍵 DUMMY_KEY
llm openai endpoint URL(--keyなし、Chat Completions) OPENAI_API_KEY DUMMY_KEY DUMMY_KEY
llm openai endpoint URL --responses -o chat_completions 1(--keyなし) OPENAI_API_KEY OpenAIの鍵 DUMMY_KEY

「OpenAIの鍵」は、鍵の置き場所の列に書いたOpenAIの鍵がそのまま届いたことを表します。「DUMMY_KEY」は、llmが鍵の代わりに送る固定の文字列です。2026年11月12日、macOSでllm 0.37.1と0.37.2(PyPI)を使い、127.0.0.1の検証用サーバーが受け取ったAuthorizationヘッダーで確かめました。

表の9行のうち、0.37.1で手元のサーバーにOpenAIの鍵が届いたのは太字の4行です。15通り全体では、0.37.1の7通りで届きました。届いた7通りのうち表に載せていない3通りは、1行目と同じ条件で、responses: trueのモデル、completion: trueのモデルを使った場合と、--no-streamを付けた場合です。0.37.2では、15通りのどれでもOpenAIの鍵は届きませんでした。

OpenAIの鍵を送る設定かを、3つのコマンドで確かめる

1件目に当てはまるかは、設定ファイルの中身、保存済みの鍵の名前、環境変数の3つで決まります。2件目は、これに加えて-o chat_completions 1を使ったかどうかで決まります。次の順に確かめます。

OpenAIの鍵が接続先へ届く使い方かを確かめる順番

1. 設定ファイル

extra-openai-models.yamlにapi_baseを書いたモデルがあるか。なければ1件目は当てはまらず、2件目は3のllm openai endpointの使い方だけを確かめます。

2. 1件目(OPENAI_API_KEYを送る)の条件

api_key_nameの名前がllm keys listに出てこず、OPENAI_API_KEYも設定している。両方そろうと、0.37.1はOpenAIの鍵を送ります。0.6〜0.7.1を使っていた時期は、鍵を保存していても当てはまります。

3. 2件目(chat_completionsで送る)の条件

api_key_nameのないresponses: trueのモデルを-o chat_completions 1で使った。llm openai endpoint URL --responses -o chat_completions 1を--keyなしで使った場合も同じです。

2か3に当てはまったら

0.37.2へ上げます。2に当てはまったモデルは、エラー文に出た名前で接続先の鍵をllm keys setで保存します。そのうえで最後の節に従い、OpenAIの鍵を作り直すかを決めます。

確かめるコマンドは次の3つです。設定ファイルは、鍵を保存するkeys.jsonと同じフォルダーにあります。echoの行は、OPENAI_API_KEYが設定されているときだけ文を表示し、鍵の値は画面に出しません。

# 1. 設定ファイルで、api_base・api_key_name・responses を書いたモデルを探す
grep -n -E 'model_id|api_base|api_key_name|responses' "$(dirname "$(llm keys path)")/extra-openai-models.yaml"

# 2. 保存済みの鍵の名前を表示する(値は出ない)
llm keys list

# 3. OPENAI_API_KEY が設定されているかを表示する(値は出ない)
echo "${OPENAI_API_KEY:+OPENAI_API_KEY は設定されています}"

検証用の設定で実行した結果です(設定ファイルの行は一部を省いています)。api_key_name: thirdpartyのthirdpartyがllm keys listに出てこず、OPENAI_API_KEYも設定されているため、tp-namedは1件目に当てはまります。tp-respは、-o chat_completions 1を付けて使っていれば2件目に当てはまります。

$ grep -n -E 'model_id|api_base|api_key_name|responses' "$(dirname "$(llm keys path)")/extra-openai-models.yaml"
1:- model_id: tp-named
3:  api_base: "http://127.0.0.1:18437/v1"
4:  api_key_name: thirdparty
...
18:- model_id: tp-resp
20:  api_base: "http://127.0.0.1:18437/v1"
21:  responses: true
$ llm keys list
openai
$ echo "${OPENAI_API_KEY:+OPENAI_API_KEY は設定されています}"
OPENAI_API_KEY は設定されています

3つのコマンドで分かるのは、いまの設定です。以前は鍵を保存していなかった、別のマシンで使っていた、という場合は、そのときの設定に当てはめて判断します。

0.37以降なら、llm openai endpoint list --jsonでもapi_key_nameを表示できます。ただし0.37.1では、Azure向けの設定などでapi_version: 2024-02-01のように引用符なしの日付を書いていると、TypeError: Object of type date is not JSON serializableで止まりました(0.37.2で修正済み)。上のgrepは、版や設定ファイルの書き方にかかわらず使えます。

当てはまるモデルを実際に使ったかは、llm logs -m モデルID -n 0 -sで調べられます。このコマンドは、そのモデルの実行記録を日時(UTC)とプロンプト付きで一覧します(-n 0は全件を表示する指定です)。-sの代わりに--jsonを付けてllm logs -m モデルID -n 0 --jsonとすると、-o chat_completions 1を付けた実行には"chat_completions": trueが残っています。

この一覧に出ない実行もあります。エラーで止まった実行(最後の節の401の例で確かめました)、llmでプロンプトを実行するときに-n(--no-log)を付けた実行、llm logs offで記録を止めていた間の実行、llm openai endpoint URLで実行した分、Pythonから呼んだ分です。一覧に出てこなくても、使っていなかったことの裏付けにはなりません。いま記録が止まっているかは、llm logs statusで分かります(止まっていればLogging is OFFと表示されます)。

0.37.2へ上げ、「No key found」が出たらapi_key_nameの鍵を保存する

llmのインストール手順のページにある、入れた方法ごとの更新コマンドです。

# 入れた方法に合わせて、どれか1つを実行する
uv tool upgrade llm     # uv
pipx upgrade llm        # pipx
pip install -U llm      # pip
brew upgrade llm        # Homebrew

llm --version           # 0.37.2 以降になったかを確かめる

Homebrewのllmは、2026年11月12日6時44分(日本時間)の時点で0.37.1でした。brew upgrade llmのあともllm --versionが0.37.1のままなら、インストール手順のページが案内しているllm install -U llmで、llmが入っている環境の中から更新できます。uvの仮想環境に入れた0.37.1で実行すると、0.37.2に上がりました。

$ llm --version
llm, version 0.37.1
$ llm install -U llm
...
Successfully installed llm-0.37.2
$ llm --version
llm, version 0.37.2

0.37.2へ上げたあと、api_key_nameの鍵を保存していないモデルは「Error: No key found - add one using 'llm keys set thirdparty'」で止まります。エラー文に出た名前で、接続先のサービスの鍵を保存すれば使えるようになります。

# エラー文に出た名前で、接続先のサービスの鍵を保存する(入力した鍵は画面に出ない)
llm keys set thirdparty

このエラーは、OPENAI_API_KEYに接続先のサービスの鍵(OpenAIの鍵とは別のもの)をわざと入れて使っていた人にも出ます。0.37.1までは、api_key_nameの鍵がないときにOPENAI_API_KEYの値を送っていたので、その使い方でも動いていました。0.37.2からは、api_baseを設定したモデルではOPENAI_API_KEYを使わないため、同じようにllm keys setで保存し直します。

すぐに0.37.2へ上げられない環境でも、api_key_nameに書いた名前の鍵をすべてllm keys setで保存すれば、0.37.1で接続先の鍵だけが届きました。2件目については、responses: trueのモデルに-o chat_completions 1を付けない運用にするか、api_key_nameを書いてその鍵を保存すれば、0.37.1でもOpenAIの鍵は届きませんでした。llm openai endpointで接続先を直接指定する場合も、--responsesと-o chat_completions 1を一緒に使わなければ、0.37.1で届いたのはDUMMY_KEYだけでした。

OpenAIの鍵を作り直すかは、接続先を誰が管理しているかで決める

当てはまる使い方をしていた場合、OpenAIの鍵はapi_baseの接続先へ届いています。コマンドが成功したか失敗したかでは、届いたかどうかを見分けられません。鍵を作り直すかは、接続先を誰が管理しているかで決めます。

自分だけが管理するサーバー

OpenAIの鍵は作り直さず、0.37.2へ上げる

手元のパソコンで動かすOpenAI互換のサーバーなどです。届いた鍵は自分の管理下にあります。リクエストのヘッダーを記録する設定にしていたら、その記録を消します。

他社のサービス、共有のサーバー、中継するプロキシー

OpenAIの鍵を作り直す

鍵は相手に届いています。社内の共有サーバーなど自分以外も管理する接続先や、暗号化しないhttp://で別のマシンへ送っていた接続先も、こちらに入れます。

成功と失敗の両方で鍵が届くことは、手元で確かめました。すべてのリクエストを401で断る検証用サーバーに向けると、0.37.1では、サーバーがダミーのOpenAIの鍵を受け取ったうえで、llmに「Error: Error code: 401 - Incorrect API key provided」と表示されました(この文言は検証用サーバーが返したもので、実際のサービスでは各社のエラー文になります)。llm logsで一覧しても、この実行は出てきませんでした。一方、鍵を確かめずに答える検証用サーバーでは、答えが普段どおり表示され、実行記録にも残りました。受け取った鍵でOpenAIへ中継するプロキシーのように、鍵を確かめずに答える接続先では、成功した実行でも鍵は相手に届いています。

他社のサービスなどへ届いた可能性がある場合は、OpenAIの管理画面で新しいAPIキーを作り、llm keys set openaiとOPENAI_API_KEYを新しい鍵に差し替えてから、古い鍵を削除します。シェルの設定ファイルなど、古い鍵を書いたほかの場所も差し替え、管理画面の利用状況に覚えのない利用がないかも確かめます。自分だけが管理するサーバーでも他社のサービスでも、0.37.2へ上げれば、この2つの使い方でOpenAIの鍵が送られることはなくなります。

参考資料

検証環境

  • macOS 27.0.1(Apple Silicon)、Python 3.13.14(uv 0.11.32の仮想環境)
  • llm 0.37.1と0.37.2(PyPI)、OpenAI Python SDK 3.49.0
  • 検証日: 2026年11月12日

この記事は、AIが調査と下書きを担当し、上記の環境で実際に動かして結果を確かめ、下書きとは別の担当が公開前に本文と出典を確認しています。制作と訂正の方針は編集・訂正方針で公開しています。