#!/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()
コメント
コメントを投稿