ClaudeのAPIキートラブル診断アプリ

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
Claude / Anthropic APIキー 診断GUI(Pydroid向け・モデル一覧取得対応)
- APIキー貼り付け
- 「モデル一覧取得」→ 利用可能モデルをドロップダウン表示
- 選んだモデルで「疎通テスト」
"""

import json
import threading
import tkinter as tk
from tkinter import ttk, messagebox

try:
    import requests
except Exception:
    requests = None

DEFAULT_ENDPOINT_MESSAGES = "https://api.anthropic.com/v1/messages"
DEFAULT_ENDPOINT_MODELS = "https://api.anthropic.com/v1/models"
DEFAULT_VERSION = "2023-06-01"


def mask_key(s: str) -> str:
    s = (s or "").strip()
    if len(s) <= 8:
        return "*" * len(s)
    return s[:4] + "*" * (len(s) - 8) + s[-4:]


def pretty_json_maybe(text: str, limit=2000) -> str:
    text = text or ""
    try:
        obj = json.loads(text)
        return json.dumps(obj, ensure_ascii=False, indent=2)[:limit]
    except Exception:
        return text[:limit]


def extract_error_message(body_text: str) -> str:
    try:
        obj = json.loads(body_text)
        if isinstance(obj, dict) and "error" in obj and isinstance(obj["error"], dict):
            return obj["error"].get("message", "") or str(obj["error"])
    except Exception:
        pass
    return ""


def analyze_response(status_code: int, body_text: str) -> str:
    detail = extract_error_message(body_text)
    lines = [f"HTTPステータス: {status_code}"]
    if detail:
        lines.append(f"詳細: {detail}")

    if status_code == 200:
        lines.append("✅ 成功:このモデルで呼び出せます(認証もOK)。")
    elif status_code == 401:
        lines.append("❌ 401 Unauthorized:APIキー認証に失敗。キーのコピペミス/別キーの可能性。")
        lines.append("対策:入力欄を全消去→貼り直し(末尾改行注意)、Consoleで新規キー作成。")
    elif status_code == 403:
        lines.append("⚠️ 403 Forbidden:権限/請求/組織設定/地域制限など。")
        lines.append("対策:ConsoleのBilling/Org/Projects権限、回線変更、VPN/プロキシOFF。")
    elif status_code == 404:
        lines.append("⚠️ 404 Not Found:指定モデルが“あなたの利用可能モデル一覧に存在しない”状態。")
        lines.append("対策:このツールの「モデル一覧取得」で出てきたIDから選んでください。")
    elif status_code == 429:
        lines.append("⚠️ 429:レート制限/上限到達。少し待つ・上限/課金を確認。")
    elif status_code >= 500:
        lines.append("⚠️ 5xx:サーバ側エラー。時間を置いて再試行。")
    else:
        lines.append("⚠️ 想定外:応答本文を確認してください。")

    return "\n".join(lines)


class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Claude APIキー診断(モデル一覧取得対応)")
        self.geometry("920x680")
        self.minsize(780, 560)

        self.key_var = tk.StringVar()
        self.version_var = tk.StringVar(value=DEFAULT_VERSION)

        self.models_endpoint_var = tk.StringVar(value=DEFAULT_ENDPOINT_MODELS)
        self.messages_endpoint_var = tk.StringVar(value=DEFAULT_ENDPOINT_MESSAGES)

        self.model_var = tk.StringVar(value="")  # 選択されたモデルID
        self.timeout_var = tk.StringVar(value="30")

        self.model_list = []  # 取得したモデルID一覧

        self._make_ui()

    def _make_ui(self):
        outer = ttk.Frame(self, padding=12)
        outer.pack(fill="both", expand=True)

        ttk.Label(outer, text="Claude (Anthropic) APIキー診断", font=("Arial", 16, "bold")).pack(anchor="w")
        ttk.Label(
            outer,
            text="手順:①APIキー貼り付け → ②モデル一覧取得 → ③モデル選択 → ④疎通テスト",
        ).pack(anchor="w", pady=(4, 12))

        # --- key ---
        lf_key = ttk.LabelFrame(outer, text="1) APIキー")
        lf_key.pack(fill="x", pady=(0, 10))

        row = ttk.Frame(lf_key)
        row.pack(fill="x", padx=10, pady=10)

        ttk.Label(row, text="APIキー:").pack(side="left")
        ttk.Entry(row, textvariable=self.key_var, show="•").pack(side="left", fill="x", expand=True, padx=(8, 8))
        ttk.Button(row, text="貼り付け", command=self.paste_from_clipboard).pack(side="left", padx=(0, 6))
        ttk.Button(row, text="クリア", command=lambda: self.key_var.set("")).pack(side="left")

        # --- settings ---
        lf_set = ttk.LabelFrame(outer, text="2) 接続設定(通常はそのままでOK)")
        lf_set.pack(fill="x", pady=(0, 10))

        g = ttk.Frame(lf_set)
        g.pack(fill="x", padx=10, pady=10)
        g.columnconfigure(1, weight=1)
        g.columnconfigure(3, weight=1)

        ttk.Label(g, text="Models Endpoint:").grid(row=0, column=0, sticky="w")
        ttk.Entry(g, textvariable=self.models_endpoint_var).grid(row=0, column=1, sticky="ew", padx=(6, 18))

        ttk.Label(g, text="Messages Endpoint:").grid(row=0, column=2, sticky="w")
        ttk.Entry(g, textvariable=self.messages_endpoint_var).grid(row=0, column=3, sticky="ew", padx=(6, 0))

        ttk.Label(g, text="anthropic-version:").grid(row=1, column=0, sticky="w", pady=(8, 0))
        ttk.Entry(g, textvariable=self.version_var).grid(row=1, column=1, sticky="ew", padx=(6, 18), pady=(8, 0))

        ttk.Label(g, text="Timeout(sec):").grid(row=1, column=2, sticky="w", pady=(8, 0))
        ttk.Entry(g, textvariable=self.timeout_var, width=8).grid(row=1, column=3, sticky="w", padx=(6, 0), pady=(8, 0))

        # --- model list / select ---
        lf_model = ttk.LabelFrame(outer, text="3) モデル(まず一覧取得して選ぶ)")
        lf_model.pack(fill="x", pady=(0, 10))

        r2 = ttk.Frame(lf_model)
        r2.pack(fill="x", padx=10, pady=10)

        ttk.Button(r2, text="モデル一覧取得(GET /v1/models)", command=self.fetch_models).pack(side="left")

        ttk.Label(r2, text="  使用モデル:").pack(side="left", padx=(12, 6))

        self.model_combo = ttk.Combobox(r2, textvariable=self.model_var, values=[], state="readonly", width=42)
        self.model_combo.pack(side="left", fill="x", expand=True)

        ttk.Button(r2, text="疎通テスト(選択モデルで送信)", command=self.test_message).pack(side="left", padx=(10, 0))

        # --- output ---
        lf_out = ttk.LabelFrame(outer, text="4) 結果ログ")
        lf_out.pack(fill="both", expand=True)

        self.output = tk.Text(lf_out, wrap="word")
        self.output.pack(fill="both", expand=True, padx=10, pady=10)
        self.output.configure(state="disabled")

        self.progress = ttk.Label(outer, text="")
        self.progress.pack(anchor="w", pady=(8, 0))

        if requests is None:
            self.log("⚠️ requests が見つかりません。Pydroidで: pip install requests")
            messagebox.showwarning("requests なし", "requests がありません。\nPydroidで: pip install requests\nを実行して再起動してください。")
        else:
            self.log("準備OK。APIキーを貼り付け→「モデル一覧取得」→モデル選択→疎通テスト。")

    def log(self, msg: str):
        self.output.configure(state="normal")
        self.output.insert("end", msg + "\n")
        self.output.see("end")
        self.output.configure(state="disabled")

    def paste_from_clipboard(self):
        try:
            text = self.clipboard_get()
        except Exception:
            messagebox.showinfo("貼り付け", "クリップボードから取得できませんでした。手動貼り付けを試してください。")
            return
        cleaned = (text or "").strip()
        self.key_var.set(cleaned)
        self.log(f"貼り付けました(マスク: {mask_key(cleaned)})")

    def set_busy(self, busy: bool, text: str = ""):
        self.progress.configure(text=text)
        self.update_idletasks()

    def _get_common(self):
        key = (self.key_var.get() or "").strip()
        if not key:
            raise ValueError("APIキーが空です。")
        version = (self.version_var.get() or "").strip()
        if not version:
            raise ValueError("anthropic-version が空です。")
        try:
            timeout = int((self.timeout_var.get() or "30").strip())
            timeout = max(5, min(timeout, 120))
        except Exception:
            timeout = 30
        return key, version, timeout

    def fetch_models(self):
        if requests is None:
            messagebox.showerror("エラー", "requests がありません。pip install requests を実行してください。")
            return

        try:
            key, version, timeout = self._get_common()
        except Exception as e:
            messagebox.showinfo("入力不足", str(e))
            return

        url = (self.models_endpoint_var.get() or "").strip()
        if not url:
            messagebox.showerror("設定エラー", "Models Endpoint が空です。")
            return

        self.log("----")
        self.log(f"モデル一覧取得: {url}")
        self.log(f"キー(マスク)={mask_key(key)}")
        self.set_busy(True, "モデル一覧取得中...")

        def worker():
            try:
                headers = {"x-api-key": key, "anthropic-version": version}
                r = requests.get(url, headers=headers, timeout=timeout)
                status = r.status_code
                body = r.text or ""
                self.after(0, self.on_models_result, status, body)
            except Exception as e:
                self.after(0, self.on_network_error, "モデル一覧取得エラー", repr(e))

        threading.Thread(target=worker, daemon=True).start()

    def on_models_result(self, status: int, body: str):
        self.set_busy(False, "")
        self.log(analyze_response(status, body))

        if status != 200:
            self.log("\n--- 応答本文(先頭) ---")
            self.log(pretty_json_maybe(body))
            return

        # 形式例: {"data":[{"id":"...","type":"model",...}, ...], "has_more": false, ...}
        models = []
        try:
            obj = json.loads(body)
            data = obj.get("data", [])
            for item in data:
                mid = item.get("id")
                if mid:
                    models.append(mid)
        except Exception:
            pass

        if not models:
            self.log("⚠️ モデルIDを抽出できませんでした。応答本文を確認してください。")
            self.log(pretty_json_maybe(body))
            return

        self.model_list = models
        self.model_combo.configure(values=models)
        self.model_var.set(models[0])
        self.log(f"✅ 利用可能モデル {len(models)} 件を取得。先頭のモデルを選択しました。")
        self.log("(ここに出たモデルIDだけが“あなたのキーで使えるモデル”です)")

    def test_message(self):
        if requests is None:
            messagebox.showerror("エラー", "requests がありません。pip install requests を実行してください。")
            return

        try:
            key, version, timeout = self._get_common()
        except Exception as e:
            messagebox.showinfo("入力不足", str(e))
            return

        endpoint = (self.messages_endpoint_var.get() or "").strip()
        if not endpoint:
            messagebox.showerror("設定エラー", "Messages Endpoint が空です。")
            return

        model = (self.model_var.get() or "").strip()
        if not model:
            messagebox.showinfo("未選択", "モデルが未選択です。先に「モデル一覧取得」を押してください。")
            return

        self.log("----")
        self.log(f"疎通テスト: {endpoint}")
        self.log(f"使用モデル: {model}")
        self.log(f"キー(マスク)={mask_key(key)}")
        self.set_busy(True, "疎通テスト中...")

        def worker():
            try:
                headers = {
                    "x-api-key": key,
                    "anthropic-version": version,
                    "content-type": "application/json",
                }
                payload = {
                    "model": model,
                    "max_tokens": 16,
                    "messages": [{"role": "user", "content": "ping"}],
                }
                r = requests.post(endpoint, headers=headers, json=payload, timeout=timeout)
                self.after(0, self.on_message_result, r.status_code, r.text or "")
            except Exception as e:
                self.after(0, self.on_network_error, "疎通テストエラー", repr(e))

        threading.Thread(target=worker, daemon=True).start()

    def on_message_result(self, status: int, body: str):
        self.set_busy(False, "")
        self.log(analyze_response(status, body))
        self.log("\n--- 応答本文(先頭) ---")
        self.log(pretty_json_maybe(body))

    def on_network_error(self, title: str, detail: str):
        self.set_busy(False, "")
        self.log(f"❌ {title}: {detail}")
        messagebox.showerror(title, detail)


if __name__ == "__main__":
    App().mainloop()

コメント

このブログの人気の投稿

ミライアイ内服薬は薬事法違反で、ほとんど効果がない詐欺ですか?

最高裁での上告理由書受理・却下の判断基準について

裁判官の忌避申立書の作成例