194 lines
6.2 KiB
Bash
Executable File
194 lines
6.2 KiB
Bash
Executable File
#!/usr/bin/env python3
|
|
"""Backend per il plasmoide KDE Plasma 6: Antigravity Usage Dashboard.
|
|
|
|
Interroga il gateway Google CloudCode PA (https://daily-cloudcode-pa.googleapis.com)
|
|
leggendo il token OAuth da ~/.gemini/antigravity-cli/antigravity-oauth-token
|
|
ed eseguendo il refresh automatico se scaduto o non valido.
|
|
Restituisce lo stato delle quote per le 3 famiglie di modelli:
|
|
- Gemini (Gemini 3.x Flash / Pro)
|
|
- Claude (Claude Sonnet 4.6 / Opus 4.6)
|
|
- GPT-OSS (GPT-OSS 120B)
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
import time
|
|
import urllib.error
|
|
import urllib.request
|
|
from datetime import datetime, timezone
|
|
|
|
TOKEN_FILE = os.path.expanduser("~/.gemini/antigravity-cli/antigravity-oauth-token")
|
|
OAUTH_URL = "https://oauth2.googleapis.com/token"
|
|
BASE_URL = "https://daily-cloudcode-pa.googleapis.com"
|
|
CLIENT_ID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com"
|
|
CLIENT_SECRET = "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"
|
|
UA = "antigravity/cli/1.1.17"
|
|
API_CLIENT = "google-cloud-sdk vscode_cloudshelleditor/0.1"
|
|
METADATA = json.dumps({"ideType": "ANTIGRAVITY", "platform": "LINUX", "pluginType": "GEMINI"})
|
|
|
|
|
|
def get_token_data():
|
|
if not os.path.exists(TOKEN_FILE):
|
|
return None
|
|
try:
|
|
with open(TOKEN_FILE, "r", encoding="utf-8") as f:
|
|
return json.load(f)
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def save_token_data(data):
|
|
try:
|
|
tmp = TOKEN_FILE + ".tmp"
|
|
with open(tmp, "w", encoding="utf-8") as f:
|
|
json.dump(data, f, indent=2)
|
|
os.replace(tmp, TOKEN_FILE)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def refresh_token(refresh_tok):
|
|
req_body = json.dumps({
|
|
"client_id": CLIENT_ID,
|
|
"client_secret": CLIENT_SECRET,
|
|
"refresh_token": refresh_tok,
|
|
"grant_type": "refresh_token"
|
|
}).encode("utf-8")
|
|
req = urllib.request.Request(OAUTH_URL, data=req_body, headers={"Content-Type": "application/json"})
|
|
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
res = json.loads(resp.read().decode("utf-8"))
|
|
return res.get("access_token"), int(res.get("expires_in", 3600))
|
|
|
|
|
|
def call_api(path, token, payload):
|
|
url = f"{BASE_URL}{path}"
|
|
headers = {
|
|
"Authorization": f"Bearer {token}",
|
|
"Content-Type": "application/json",
|
|
"User-Agent": UA,
|
|
"X-Goog-Api-Client": API_CLIENT,
|
|
"Client-Metadata": METADATA
|
|
}
|
|
body = json.dumps(payload).encode("utf-8")
|
|
req = urllib.request.Request(url, data=body, headers=headers)
|
|
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
return json.loads(resp.read().decode("utf-8"))
|
|
|
|
|
|
def parse_iso_epoch(ts):
|
|
if not ts:
|
|
return 0
|
|
try:
|
|
dt = datetime.fromisoformat(ts.replace("Z", "+00:00"))
|
|
return int(dt.timestamp())
|
|
except Exception:
|
|
return 0
|
|
|
|
|
|
def main():
|
|
data = get_token_data()
|
|
if not data or "token" not in data:
|
|
print(json.dumps({
|
|
"ok": False,
|
|
"error": "no_token",
|
|
"detail": f"Nessun token OAuth in {TOKEN_FILE}"
|
|
}))
|
|
sys.exit(0)
|
|
|
|
tok = data["token"]
|
|
access_tok = tok.get("access_token")
|
|
refresh_tok = tok.get("refresh_token")
|
|
|
|
project_id = "healthy-topic-dwr0v"
|
|
try:
|
|
quota_data = call_api("/v1internal:retrieveUserQuota", access_tok, {"project": project_id})
|
|
except urllib.error.HTTPError as e:
|
|
if e.code in (401, 403) and refresh_tok:
|
|
try:
|
|
new_acc, exp_in = refresh_token(refresh_tok)
|
|
tok["access_token"] = new_acc
|
|
save_token_data(data)
|
|
access_tok = new_acc
|
|
quota_data = call_api("/v1internal:retrieveUserQuota", access_tok, {"project": project_id})
|
|
except Exception as e2:
|
|
print(json.dumps({
|
|
"ok": False,
|
|
"error": "auth_error",
|
|
"detail": f"Refresh fallito: {e2}"
|
|
}))
|
|
sys.exit(0)
|
|
else:
|
|
print(json.dumps({
|
|
"ok": False,
|
|
"error": f"http_{e.code}",
|
|
"detail": f"Errore server Google HTTP {e.code}"
|
|
}))
|
|
sys.exit(0)
|
|
except Exception as e:
|
|
print(json.dumps({
|
|
"ok": False,
|
|
"error": "network_error",
|
|
"detail": str(e)
|
|
}))
|
|
sys.exit(0)
|
|
|
|
buckets = quota_data.get("buckets", [])
|
|
bucket_map = {b.get("modelId"): b for b in buckets}
|
|
|
|
# 1. Gemini Family
|
|
gem_b = (bucket_map.get("gemini-3.1-pro-high") or
|
|
bucket_map.get("gemini-3.7-flash-high") or
|
|
bucket_map.get("gemini-3.5-flash-low") or {})
|
|
gem_rem = float(gem_b.get("remainingFraction", 1.0))
|
|
gem_reset_str = gem_b.get("resetTime", "")
|
|
gemini_info = {
|
|
"id": "gemini",
|
|
"name": "Gemini 3.x",
|
|
"models": "Pro / Flash",
|
|
"remaining": round(gem_rem, 4),
|
|
"used_percent": round((1.0 - gem_rem) * 100, 1),
|
|
"reset_time": gem_reset_str,
|
|
"reset_at": parse_iso_epoch(gem_reset_str)
|
|
}
|
|
|
|
# 2. Claude Family
|
|
cld_b = (bucket_map.get("claude-sonnet-4-6") or
|
|
bucket_map.get("claude-opus-4-6-thinking") or {})
|
|
cld_rem = float(cld_b.get("remainingFraction", 1.0))
|
|
cld_reset_str = cld_b.get("resetTime", "")
|
|
claude_info = {
|
|
"id": "claude",
|
|
"name": "Claude 4.6",
|
|
"models": "Sonnet / Opus",
|
|
"remaining": round(cld_rem, 4),
|
|
"used_percent": round((1.0 - cld_rem) * 100, 1),
|
|
"reset_time": cld_reset_str,
|
|
"reset_at": parse_iso_epoch(cld_reset_str)
|
|
}
|
|
|
|
# 3. GPT-OSS Family
|
|
gpt_b = bucket_map.get("gpt-oss-120b-medium") or {}
|
|
gpt_rem = float(gpt_b.get("remainingFraction", 1.0))
|
|
gpt_reset_str = gpt_b.get("resetTime", "")
|
|
gpt_info = {
|
|
"id": "gpt-oss",
|
|
"name": "GPT-OSS",
|
|
"models": "120B Medium",
|
|
"remaining": round(gpt_rem, 4),
|
|
"used_percent": round((1.0 - gpt_rem) * 100, 1),
|
|
"reset_time": gpt_reset_str,
|
|
"reset_at": parse_iso_epoch(gpt_reset_str)
|
|
}
|
|
|
|
out = {
|
|
"ok": True,
|
|
"project": project_id,
|
|
"groups": [gemini_info, claude_info, gpt_info]
|
|
}
|
|
print(json.dumps(out, indent=2))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|