◉GEMINI LABEN
●ARGON — gemini-4-argon が 10/13 に GA。複雑なコーディング・エージェント・数学・科学向けです●DEEP RES — 旧 Deep Research エージェントは 10/23 に停止済み。呼び出しが失敗する場合は deep-research-preview-04-2026 へ●NANO — gemini-nano-banana-2.1 が GA(10/06)。gemini-3.1-flash-image は非推奨で停止日は未発表です●CLI — Gemini API CLI(gemini-api)がベータで登場(10/27)。エージェントやCI向けのゼロ依存クライアントです●STORE — 新規の有料プロジェクトでは生成リクエストの保存(store)が既定でオンになります(10/26)●NEW — クラッシュ報告を毎回ゼロから読ませない — 既知バグ早見表を先に grep し…●ARGON — gemini-4-argon が 10/13 に GA。複雑なコーディング・エージェント・数学・科学向けです●DEEP RES — 旧 Deep Research エージェントは 10/23 に停止済み。呼び出しが失敗する場合は deep-research-preview-04-2026 へ●NANO — gemini-nano-banana-2.1 が GA(10/06)。gemini-3.1-flash-image は非推奨で停止日は未発表です●CLI — Gemini API CLI(gemini-api)がベータで登場(10/27)。エージェントやCI向けのゼロ依存クライアントです●STORE — 新規の有料プロジェクトでは生成リクエストの保存(store)が既定でオンになります(10/26)●NEW — クラッシュ報告を毎回ゼロから読ませない — 既知バグ早見表を先に grep し…
記事一覧/API / SDK
◈ API / SDK/2026-10-30初級

「API key expired」と出ても、期限切れとは限りません。キーを作り直す前に、実際に送られているキーを確かめる

Gemini API が 400 の API_KEY_INVALID を返しても、応答だけでは期限切れか別の原因かを区別できません。環境変数の優先順位と空白・引用符を手元で確かめる診断スクリプトを、ダミーキーで動かした結果とともに残します。

Gemini API253API キー3エラー処理google-genai8トラブルシューティング39

新しい API キーを発行したのに、同じエラーが返ってきます。キーを作り直し、プロジェクトも作り直し、請求情報も確かめたのに、API key expired. Please renew the API key. が消えません。

GitHub の gemini-cli のリポジトリに、2026 年 4 月に立ったそんな issue があります。ADK 経由でエージェントを動かすと毎回このエラーになる、という報告です。コメントでは「期限切れでは」と返され、投稿者は「新しいプロジェクトで新しいキーを作っても同じです」と答えています。およそ 1 週間後に「解決しました」と書かれて閉じられましたが、原因は残されていませんでした。

原因の書かれていない issue を読んで、私自身が手を動かして確かめられる範囲を確かめました。ダミーのキーで本物のエンドポイントを呼び、google-genai の SDK(2.33.0)のソースを読んで、キーが「どこから来て、何を通って、どう送られるか」を追っています。先にお伝えすると、期限切れかどうかを応答から読み取る方法は見つかりませんでした。代わりに、キーを作り直す前に 1 分で済む確認手順が残りました。

応答は「期限切れ」と「ただの不正なキー」を区別しません

issue に貼られていた応答は HTTP 400 でした。401 や 403 ではなく 400 の INVALID_ARGUMENT で、details の ErrorInfo に reason: API_KEY_INVALID が入っています。

同じ形が、実在しないダミーのキーでも返ってきました。変わるのはメッセージだけで、API key not valid. Please pass a valid API key. になります。キーの形を変えながら gemini-3.8-flash に hi を送った結果が、次の表です。

送ったキー結果
実在しないキー(そのまま)400 INVALID_ARGUMENT、reason は API_KEY_INVALID
前後に空白同じ
末尾に改行(LF・CRLF)同じ
引用符で囲んだもの同じ
3 文字だけ同じ
空文字・未設定通信の前に手元で ValueError

reason が示しているのは「このキーは受け付けない」という事実だけです。期限、失効、コピー時の欠け、別のキーの混入は、ここでは区別されません。応答から原因を当てようとせず、手元で確かめられるものを先に潰します。私はこの順番にしました。

「期限切れだ」と決めて作り直した場合、本当の原因が手元に残っていれば、新しいキーも同じ場所で同じ理由で弾かれます。

新しいキーでも直らないとき:環境変数が 2 つあるとき

SDK のソース(_api_client.py)には、キーを環境変数から取る関数があります。docstring には「GOOGLE_API_KEY を優先する」と書かれていて、両方が設定されていれば GEMINI_API_KEY は読まれません。

手元で確かめました。ダミーのキーを 2 つ用意し、GEMINI_API_KEY にだけ新しいほうを入れた場合と、GOOGLE_API_KEY に古いほうも残した場合を比べています。前者は新しいキーが、後者は古いキーが使われました。後者では Both GOOGLE_API_KEY and GEMINI_API_KEY are set. Using GOOGLE_API_KEY. という警告が 1 行出ますが、ログレベルが WARNING の 1 行です。サーバーのログや ADK の出力に混ざると、見落としやすいと感じます。

個人開発のように、シェルの設定ファイルとプロジェクトの .env の両方にキーを書く環境では、古いほうが前面に出ます。.env に新しいキーを入れ直しても、シェル側に古い GOOGLE_API_KEY が残っていれば、送られるのは古いキーです。

issue の投稿者がこれに当たっていたかどうかは分かりません。原因が書かれていないからです。ただ「新しいキーを作っても直らない」という症状の候補としては、最初に確かめる価値があります。新しいキーを疑う前に、古いキーが残っていないかを見る習慣が要るのかもしれません。なお、ADK は内部で google-genai を使っていると理解していますが、今回は ADK 自体は動かしていません。ADK の側から「疑う順番」を整理した記事が、同じ日に別の角度で出ています(ADK のエージェントが「API key expired」で一度も動かない。文面ではなく reason を読むと、疑う順番が決まりました)。こちらは公式ページと issue から、この記事は手元の実行結果から書いた、という違いがあります。

空白・改行・引用符:SDK が直してくれる範囲

Client(api_key="AIzaNL\n") を作ると、保持されるキーは 'AIzaNL' でした。ソースにも self.api_key = self.api_key.strip() の行があります。前後の空白や改行は、このバージョンの SDK では原因になりません。

一方で、引用符は取り除かれません。.env の読み方によっては、KEY="AIza..." の引用符が値に入ったまま渡されます。また、strip してくれるのはこの SDK のこのバージョンの話です。curl や別の言語のクライアントに同じ値を渡すなら、そちらは自分で確かめる必要があります。

診断スクリプト:キーは出力せず、指紋だけを比べる

キーを作り直す前に走らせるための、70 行ほどのスクリプトです。キー本体は出力せず、SHA-256 の先頭 8 桁だけを表示します。環境変数 2 つと「実際に使われるキー」を並べ、models.list を 1 件だけ呼んで判定します。models.list は生成を伴わないので、確認のたびにトークンを使うことはありません。

"""Gemini API キー診断: キーを更新する前に、どのキーが実際に送られているかを確かめる。"""
import hashlib
import os
import sys
 
from google import genai
from google.genai import errors
 
 
def fingerprint(key: str | None) -> str:
    """キー本体は出力せず、先頭8桁のハッシュだけで同一性を比べる。"""
    if not key:
        return "(未設定)"
    return "sha256:" + hashlib.sha256(key.encode()).hexdigest()[:8]
 
 
def shape_problems(key: str) -> list[str]:
    """サーバーに投げる前に分かる形の異常。SDK が除去するものも含めて報告する。"""
    problems = []
    if key != key.strip():
        problems.append("前後に空白・改行(SDK は strip しますが、他のクライアントでは残ります)")
    if key.strip().startswith(('"', "'")) or key.strip().endswith(('"', "'")):
        problems.append("引用符が付いたまま(.env の書き方によっては値に含まれます)")
    if not key.isascii():
        problems.append("ASCII 以外の文字(コピー時の全角・不可視文字)")
    return problems
 
 
def reason_of(e: errors.APIError) -> str | None:
    """e.details は details の配列ではなく、レスポンス本文の辞書全体。"""
    body = e.details if isinstance(e.details, dict) else {}
    for d in body.get("error", {}).get("details", []):
        if d.get("@type", "").endswith("google.rpc.ErrorInfo"):
            return d.get("reason")
    return None
 
 
def classify(e: errors.APIError) -> str:
    reason = reason_of(e)
    if reason == "API_KEY_INVALID":
        return "key"        # 再試行しない。送っているキーを疑う
    if e.code == 429:
        return "quota"      # 待つ・上限を見直す
    if e.code >= 500:
        return "transient"  # 間隔を空けて再試行してよい
    return "other"          # 400 系はリクエスト内容を直す
 
 
def main() -> int:
    google_k, gemini_k = os.environ.get("GOOGLE_API_KEY"), os.environ.get("GEMINI_API_KEY")
    print(f"GOOGLE_API_KEY : {fingerprint(google_k)}")
    print(f"GEMINI_API_KEY : {fingerprint(gemini_k)}")
    if google_k and gemini_k and google_k != gemini_k:
        print("→ 2つの環境変数が食い違っています。SDK は GOOGLE_API_KEY を使います。")
 
    client = genai.Client()  # 引数なし = 実運用と同じ解決順序
    print(f"実際に使われるキー: {fingerprint(client._api_client.api_key)}")
    for name, raw in (("GOOGLE_API_KEY", google_k), ("GEMINI_API_KEY", gemini_k)):
        for p in shape_problems(raw or "x"):
            print(f"形の問題({name}):", p)
 
    try:
        next(iter(client.models.list(config={"page_size": 1})))  # トークンを消費しない
    except errors.APIError as e:
        print(f"判定: {classify(e)} / HTTP {e.code} {e.status} / reason={reason_of(e)}")
        return 1
    print("判定: ok(このキーで models.list が通りました)")
    return 0
 
 
if __name__ == "__main__":
    sys.exit(main())

ダミーのキーで動かした結果です。まず、古いキーを GOOGLE_API_KEY に残したまま、新しいキーを GEMINI_API_KEY に入れた場合です。

GOOGLE_API_KEY : sha256:40b5137b
GEMINI_API_KEY : sha256:2725a3cf
→ 2つの環境変数が食い違っています。SDK は GOOGLE_API_KEY を使います。
実際に使われるキー: sha256:40b5137b
判定: key / HTTP 400 INVALID_ARGUMENT / reason=API_KEY_INVALID

「実際に使われるキー」の指紋が GOOGLE_API_KEY と同じです。新しいキーを入れたつもりでも、送られているのは古いほうだと、ここで分かります。

次に、GEMINI_API_KEY の末尾に空白が残っていた場合です。

GOOGLE_API_KEY : (未設定)
GEMINI_API_KEY : sha256:7d3158e4
実際に使われるキー: sha256:a6d9ddc3
形の問題(GEMINI_API_KEY): 前後に空白・改行(SDK は strip しますが、他のクライアントでは残ります)
判定: key / HTTP 400 INVALID_ARGUMENT / reason=API_KEY_INVALID

環境変数の指紋と「実際に使われるキー」の指紋が違います。SDK が手元で値を直してから送っている証拠で、このずれ自体が、値に余計なものが付いている合図になります。

分岐は HTTP ステータスでなく reason で

エラー処理に組み込むときの落とし穴を、2 つ書き残します。

1 つめは e.details の中身です。名前から「details の配列」を想像しますが、実際はレスポンス本文の辞書全体で、トップのキーは error の 1 つだけでした。e.details[0] と書くと、期待した ErrorInfo には届きません。スクリプトの reason_of のように、error → details → ErrorInfo の順にたどります。

2 つめは再試行の判断です。400 は一般に「リクエストが悪い」ので再試行しない側に倒せますが、API_KEY_INVALID は「リクエストの中身」でなく「送っているキー」の問題です。夜間バッチのように無人で回るジョブなら、reason で分けておくと、キーの失効を「止めて知らせる」へ回せます。再試行の分け方は、夜間バッチが朝まで止まっていた — gemini-api の終了コードで「再試行してよい失敗」を分ける と、Gemini API のエラーを status で見分ける — 本番で止めない実装 にも書いています。保存設定のように「気づいた日に確かめる」類の話は、新しい有料プロジェクトでは、リクエストが保存される設定から始まります。気づいた日に確かめる三か所 にまとめました。

確かめられていないこと

書き残す以上、確かめていない範囲もお伝えしておきます。

  • 有効なキーでの成功経路:手元に有効なキーを置いていないので、判定: ok の分岐は動かしていません。コードを読んでの想定です。
  • 本物の期限切れキーの応答:再現できていません。冒頭の expired という文言は、issue からの引用です。
  • SDK のバージョン:2.33.0 のソースと挙動だけを見ています。優先順位や strip の挙動は、バージョンで変わる可能性があります。
  • ADK 経由の挙動:動かしていません。

次にこのエラーを見たら、キーを作り直す前に、まず keycheck.py を走らせようと思います。指紋が食い違っていれば、直すのはキーでなく環境のほうです。

シェア

お読みいただきありがとうございます

Gemini Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

◈ API / SDK2026-10-30
ADK のエージェントが「API key expired」で一度も動かない。文面ではなく reason を読むと、疑う順番が決まりました
ADK 経由で 400 INVALID_ARGUMENT「API key expired. Please renew the API key.」が毎回返るとき、期限切れと決めつける前に確かめる順番を整理しました。reason の読み方、環境変数と .env の食い違い、鍵の状態を REST で切り分ける短いスクリプト付きです。
◈ API / SDK2026-10-14
config に「Extra inputs are not permitted」が出たら、キーの綴りを一つずつ疑います
google-genai SDK で config を渡した途端に pydantic の ValidationError(extra_forbidden)が出る原因は、ほぼ綴りの一字違いです。手元で再現した出力と、呼び出し前に怪しいキーを洗う関数、text が None になる場合の見分け方を書き残します。
◈ API / SDK2026-08-15
generate_images は2027年まで残ります。ただし 2.20.0 以降は、手元で先に止まります
google-genai に generate_images は残っていますが、2.20.0 以降は Gemini Developer API モードだと通信の前に ValueError で止まります。SDK の期限(2027年)、モデルの期限(8月17日)、そして手元で先に起きる停止の3つを、2.18.1・2.19.0・2.33.0 で実測して切り分けました。