夜遅くに ADK でエージェントを一つ組み、adk run を叩いた直後のことです。画面に出たのは 400 INVALID_ARGUMENT、そして「API key expired. Please renew the API key.」の一行でした。
鍵を作ったのは数日前です。期限切れと言われても、心当たりがありません。それでも文面どおりに受け取って鍵を作り直し、貼り替え、同じ場所で同じエラーを見て、肩が落ちる——そんな回り道を、私自身、似た形で何度かしてきました。
最初にお伝えしたいのは、この文面は原因を言い当てていないかもしれない、ということです。見るべきは文章ではなく、同じ応答の中にある reason のほうでした。
なお、この記事は私の手元で障害を再現した記録ではありません。GitHub の gemini-cli リポジトリに 2026 年 4 月 7 日付で立った報告(#24831)の文面と、Gemini API 公式の API キーのページにもとづく、疑う順番の整理です。その報告にはコメントも対処も付いておらず、原因は公開されていません。ですから「これが答えです」とは書けず、「ここから潰すと迷いにくい」という線引きまでをお渡しします。
報告されている文面を、そのまま分解する
報告に載っていた内容は、次のとおりです。
google.genai.errors.ClientError: 400 INVALID_ARGUMENT
message: "API key expired. Please renew the API key."
details: reason = API_KEY_INVALID (generativelanguage.googleapis.com)
ここで注目したいのは、message が「expired(期限切れ)」と言っているのに、reason は API_KEY_INVALID(無効なキー)だという点です。公式の API キーのページを読み返しても、標準の鍵に有効期限が設けられているという記述は私には見つけられませんでした。
つまり、手元の症状を最も正確に表しているのは reason のほうで、「サーバーが、いま渡されたその文字列を有効な鍵として受け取れなかった」と読むのが安全です。渡した鍵が悪いのか、渡されたものが想定と違うのか。疑う入口はこの二つに割れます。
疑う順番は「渡されたもの」が先
鍵そのものを疑う前に、そもそも ADK に何が渡っているかを確かめます。理由は単純で、鍵を作り直しても、渡る経路が変わらなければ結果が変わらないからです。
公式のページには、クライアントライブラリは GEMINI_API_KEY または GOOGLE_API_KEY を読み、両方あるときは GOOGLE_API_KEY が優先される、と書かれています。ADK の公式ドキュメントが案内している設定は、エージェントのフォルダに置く .env に GOOGLE_API_KEY を書く形です。
ここに落とし穴が一つあります。シェルのほうに、以前の鍵が GOOGLE_API_KEY として残っていたらどうなるでしょうか。.env に新しい鍵を書いたつもりでも、どちらが読まれているのか、見た目では判別できません。ADK が .env の値をどの優先順位で読み込むかは、公式ページには記載がありませんでした。ですから私は「たぶんこちらだろう」で進めず、実際に調べる側に回ります。
鍵の文字列を出さずに、出どころだけを照らす
鍵を画面やログに貼るのは避けたいので、末尾 4 文字だけを出すスクリプトを使います。シェルの環境変数と、.env ファイルの中身を並べて見ます。
import os
import sys
from pathlib import Path
def tail(value):
"""鍵の末尾 4 文字だけ返す。全体は絶対に出力しない。"""
if not value:
return "(未設定)"
value = value.strip().strip('"').strip("'")
return f"…{value[-4:]} (長さ {len(value)})"
def read_dotenv(path):
result = {}
if not path.exists():
return result
for line in path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, val = line.partition("=")
result[key.strip()] = val
return result
agent_dir = Path(sys.argv[1] if len(sys.argv) > 1 else ".")
env_file = read_dotenv(agent_dir / ".env")
print("シェル GOOGLE_API_KEY :", tail(os.environ.get("GOOGLE_API_KEY")))
print("シェル GEMINI_API_KEY :", tail(os.environ.get("GEMINI_API_KEY")))
print(".env GOOGLE_API_KEY :", tail(env_file.get("GOOGLE_API_KEY")))
print(".env GEMINI_API_KEY :", tail(env_file.get("GEMINI_API_KEY")))実行は python3 probe_key.py ./my_agent のように、.env を置いたフォルダを引数に渡します。出力のうち、末尾 4 文字が AI Studio の画面で見えている鍵と一致しない行があれば、そこが古い鍵の居場所です。長さが想定と違うときは、貼り付け時に引用符や改行が混ざっている可能性が高くなります。
四行のうち、最近作った鍵の末尾と一致するものが一つもなければ、それは ADK の問題ではなく、貼った鍵そのものが違う、という意味になります。この切り分けだけで、半分の道が閉じます。
ADK を外して、鍵だけを REST で試す
次に、ADK と鍵を切り離します。同じ鍵で、モデル一覧を取る最小のリクエストを送ってみます。
# 鍵は環境変数から読み、コマンド履歴に残さない
curl -s -o /dev/null -w "%{http_code}\n" \
-H "x-goog-api-key: ${GOOGLE_API_KEY}" \
"https://generativelanguage.googleapis.com/v1beta/models"200 が返れば、鍵は生きています。疑いは ADK 側の設定や、別の変数が読まれている経路に移ります。400 や 403 が返れば、鍵そのものの状態が問題です。この二分で、「どちらの箱を開けるか」が決まります。
鍵の状態に問題があるとき、公式のページから拾える候補は次のとおりです。
- 制限のない(Unrestricted)標準の鍵は Gemini API に拒否されることがあり、しばらく使われていない制限なしの鍵は止められて「Blocked」と表示されます。新しい鍵を作るか、Gemini API に制限した鍵を使うことになります。
- 他の Google API と共用している鍵に制限をかけた場合は、Gemini へのリクエストが失敗します。Gemini 用に別の鍵を作る形になります。
- 画面に鍵が見えないときは、既存の Cloud プロジェクトを AI Studio に取り込む必要があります。
「期限切れ」という文言に引っ張られていた間は、鍵の制限と状態を見るという発想が出てきませんでした。reason を読み、状態の表を見るようにしました。順番が変わると、行き先も変わるのだと気づいたのは、何度か回り道をしたあとでした。
私が引いた線引き
この件で私が改めたのは、エラーを読む順番でした。長い間、メッセージの日本語訳や英文をそのまま原因として受け取り、鍵を作り直す方向へ走っていたのです。結果は芳しくありませんでした。いまは次の三段で止まります。
- 応答の
reasonを読みます(文面は参考にとどめます) - 末尾 4 文字で、どの経路の鍵が渡っているかを確かめます
- ADK を外し、REST で鍵だけを試します
文面は参考、reason は証拠、鍵の作り直しは最後。 この一文だけは、疲れている夜でも守るようにしています。
次にやること
まずは、手元のエージェントのフォルダで上の 4 行出力を一度だけ取ってみてください。末尾 4 文字が見えるだけで、「どの鍵を疑うか」が決まります。個人開発で壁紙アプリや Web の運用をしていると、古い鍵がシェルの片隅に残る場面は珍しくありません。関連して、鍵の優先順位を CLI で追った記録は Gemini CLI に新しいキーを渡しても「API key not valid」。読まれていたのは三か所目の古いキーでした に、GOOGLE_API_KEY と GEMINI_API_KEY の取り違えは 環境変数の食い違いの記事 に、鍵の差し替えを止めずに行う手順は 無停止での鍵ローテーション にまとめています。