v3 M3: finalize z.ai against the real usage endpoint (monitor/quota/limit)

Probing a live GLM Coding Plan key showed the Anthropic-compat /v1/messages
response carries NO rate-limit headers. z.ai instead exposes a dedicated status
endpoint (found via github.com/rygel/AIUsageTracker):

    GET https://api.z.ai/api/monitor/usage/quota/limit
    Authorization: <raw key>   (no "Bearer")   Accept-Language: en-US,en
    -> data.limits[] of TOKENS_LIMIT windows {percentage 0-100, nextResetTime ms, unit,number}
       + data.level (plan tier)

ZaiProvider now GETs that: the shortest TOKENS_LIMIT window -> the 5h bar (s/sr),
the longest -> the weekly bar (w/wr); the monthly TIME_LIMIT (web-tool quota) is
ignored; level -> status ("Lite"/"Pro"/…), any window at 100% -> "limited". It's
a status GET, so it does NOT spend the prompt-metered plan — no self-throttle
needed, polls on the normal cadence. Monitor URL derives from the configured
base host.

Verified live: s=1% (5h, resets ~4.9h), w=2% (weekly, resets ~6.5d), st="Lite".
test_zai rewritten for the JSON envelope (9 tests). probe_zai.py repointed to the
monitor endpoint. 122 passed, 2 Linux-only failures (baseline).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wenil
2026-07-10 08:53:32 +03:00
co-authored by Claude Opus 4.8
parent ac0742d92c
commit 4eeb6e242b
3 changed files with 192 additions and 183 deletions
+82 -74
View File
@@ -1,34 +1,51 @@
"""z.ai (GLM Coding Plan) provider (v3 M3). """z.ai (GLM Coding Plan) provider (v3 M3).
z.ai exposes an Anthropic-compatible endpoint (default z.ai exposes a dedicated usage endpoint that returns the Coding Plan's rate-limit
``https://api.z.ai/api/anthropic``), so we reach it the same way Claude Code does windows directly — no need to send a (quota-consuming) chat request:
with ``ANTHROPIC_BASE_URL`` pointed at z.ai: a ``/v1/messages`` POST with a Bearer
API key. The key + base URL come from the config the control panel writes
(``providers.zai.base_url`` / ``.api_key``) — the "paste your key" field.
Two z.ai-specific cares vs. Anthropic: GET https://api.z.ai/api/monitor/usage/quota/limit
* Auth is a static API key (Bearer), not the OAuth token — no self-refresh. Authorization: <api_key> # raw key, NOT "Bearer …"
* The GLM Coding Plan meters by *prompts*, so polling every 60s would burn the Accept-Language: en-US,en
user's quota. We self-throttle: hit the network at most once per
``poll_interval_s`` (default 15 min) and serve a cached status in between.
Rate-limit parsing assumes z.ai proxies Anthropic's unified rate-limit headers. {"code":200,"success":true,"data":{"level":"lite","limits":[
If a real z.ai response doesn't carry them, poll() still reports "connected" and {"type":"TIME_LIMIT", "unit":5,"number":1, ...}, # monthly web-tool quota (ignored)
logs the limit-ish headers it DID return, so the mapping can be finalized against {"type":"TOKENS_LIMIT","unit":3,"number":5,"percentage":1,"nextResetTime":<ms>}, # 5h window
a live key (see tools/probe_zai.py). {"type":"TOKENS_LIMIT","unit":6,"number":1,"percentage":2,"nextResetTime":<ms>}]}} # weekly window
We surface the two TOKENS_LIMIT windows as Claude's short (5h) + weekly bars: of
the token windows, the shortest is the 5h bar and the longest the weekly bar.
``percentage`` is already 0-100; ``nextResetTime`` is epoch ms. The key + base URL
come from the config the control panel's z.ai field writes. This is a status GET,
so — unlike a chat call — it doesn't spend the prompt-metered plan, and can poll
on the normal cadence.
Discovered from github.com/rygel/AIUsageTracker (ZaiProvider.cs).
""" """
from __future__ import annotations from __future__ import annotations
import time import time
from urllib.parse import urlsplit
import httpx import httpx
from .base import Provider, ProviderStatus from .base import Provider, ProviderStatus
DEFAULT_BASE = "https://api.z.ai/api/anthropic" DEFAULT_HOST = "https://api.z.ai"
DEFAULT_MODEL = "glm-4.6" _MONITOR_PATH = "/api/monitor/usage/quota/limit"
DEFAULT_POLL_INTERVAL_S = 900 # protect the prompt-metered plan: >= 15 min between calls
# z.ai window "unit" enum → seconds, used only to rank the token windows by length
# (shortest = the 5h bar, longest = the weekly bar). Best-effort; unknown → hours.
_UNIT_SECONDS = {1: 1, 2: 60, 3: 3600, 4: 86400, 5: 2_592_000, 6: 604_800}
def _monitor_url(base_url: str) -> str:
"""The usage endpoint on the same host as the configured (Anthropic-compat)
base URL, or the public z.ai host when unset."""
parts = urlsplit(base_url or "")
if parts.scheme and parts.netloc:
return f"{parts.scheme}://{parts.netloc}{_MONITOR_PATH}"
return DEFAULT_HOST + _MONITOR_PATH
class ZaiProvider(Provider): class ZaiProvider(Provider):
@@ -38,8 +55,6 @@ class ZaiProvider(Provider):
def __init__(self, transport: httpx.BaseTransport | None = None) -> None: def __init__(self, transport: httpx.BaseTransport | None = None) -> None:
self._transport = transport # tests inject an httpx.MockTransport self._transport = transport # tests inject an httpx.MockTransport
self._cache: ProviderStatus | None = None
self._cache_at = 0.0
async def poll(self) -> ProviderStatus: async def poll(self) -> ProviderStatus:
from daemon.claude_usage_daemon_windows import log from daemon.claude_usage_daemon_windows import log
@@ -50,45 +65,20 @@ class ZaiProvider(Provider):
conf = provider_conf(load_config(), "zai") conf = provider_conf(load_config(), "zai")
key = (conf.get("api_key") or "").strip() key = (conf.get("api_key") or "").strip()
base = (conf.get("base_url") or "").strip().rstrip("/") or DEFAULT_BASE base = (conf.get("base_url") or "").strip()
model = (conf.get("model") or "").strip() or DEFAULT_MODEL
try:
interval = float(conf.get("poll_interval_s") or DEFAULT_POLL_INTERVAL_S)
except (TypeError, ValueError):
interval = DEFAULT_POLL_INTERVAL_S
if not key: if not key:
return ProviderStatus(ok=False, st="no key", auth_problem=True) return ProviderStatus(ok=False, st="no key", auth_problem=True)
# Self-throttle: reuse the cached reading until the interval elapses, so the url = _monitor_url(base)
# prompt-metered plan isn't drained by the 60s daemon poll. headers = {"Authorization": key, "Accept-Language": "en-US,en"}
now = time.time()
if self._cache is not None and (now - self._cache_at) < interval:
return self._cache
st = await self._fetch(base, key, model, log)
# Cache stable outcomes (fresh data or a definitive auth failure); leave a
# transient blip uncached so it retries on the next tick.
if st.ok or st.auth_problem:
self._cache, self._cache_at = st, now
return st
async def _fetch(self, base: str, key: str, model: str, log) -> ProviderStatus:
url = f"{base}/v1/messages"
headers = {
"Authorization": f"Bearer {key}",
"anthropic-version": "2023-06-01",
"Content-Type": "application/json",
}
body = {"model": model, "max_tokens": 1, "messages": [{"role": "user", "content": "hi"}]}
client_kw = {"timeout": 20.0} client_kw = {"timeout": 20.0}
if self._transport is not None: if self._transport is not None:
client_kw["transport"] = self._transport client_kw["transport"] = self._transport
try: try:
async with httpx.AsyncClient(**client_kw) as http: async with httpx.AsyncClient(**client_kw) as http:
resp = await http.post(url, headers=headers, json=body) resp = await http.get(url, headers=headers)
except httpx.HTTPError as e: except httpx.HTTPError as e:
log(f"z.ai call failed: {e}") # transient — no auth flag log(f"z.ai call failed: {e}") # transient — retry next tick
return ProviderStatus(ok=False, st="offline") return ProviderStatus(ok=False, st="offline")
if resp.status_code in (401, 403): if resp.status_code in (401, 403):
log(f"z.ai auth rejected: HTTP {resp.status_code}") log(f"z.ai auth rejected: HTTP {resp.status_code}")
@@ -96,40 +86,58 @@ class ZaiProvider(Provider):
if resp.status_code >= 400: if resp.status_code >= 400:
log(f"z.ai HTTP {resp.status_code}: {resp.text[:150]}") log(f"z.ai HTTP {resp.status_code}: {resp.text[:150]}")
return ProviderStatus(ok=False, st=f"http {resp.status_code}") return ProviderStatus(ok=False, st=f"http {resp.status_code}")
return self._parse(resp, log) try:
body = resp.json()
except ValueError:
log("z.ai: non-JSON usage response")
return ProviderStatus(ok=False, st="error")
if not body.get("success") or not isinstance(body.get("data"), dict):
# e.g. {"code":401,...,"success":false} → treat as an auth/config problem
code = body.get("code")
log(f"z.ai usage error: code={code} msg={body.get('msg')!r}")
return ProviderStatus(ok=False, st="bad key",
auth_problem=code in (401, 403))
return self._to_status(body["data"])
@staticmethod @staticmethod
def _parse(resp: httpx.Response, log) -> ProviderStatus: def _to_status(data: dict) -> ProviderStatus:
h = resp.headers limits = data.get("limits") or []
now = time.time() # Only the token windows are the coding rate limit; TIME_LIMIT is the
# separate monthly web-tool quota.
toks = [l for l in limits if isinstance(l, dict) and l.get("type") == "TOKENS_LIMIT"]
def pct(util: str) -> float: def win_seconds(l: dict) -> int:
# Anthropic reports utilization as a 0-1 fraction. return _UNIT_SECONDS.get(l.get("unit"), 3600) * int(l.get("number") or 1)
def pct(l: dict | None) -> float:
if not l:
return 0.0
try: try:
return round(float(util) * 100.0, 1) return round(float(l.get("percentage") or 0.0), 1)
except (TypeError, ValueError): except (TypeError, ValueError):
return 0.0 return 0.0
def reset_min(ts: str) -> int: def reset_min(l: dict | None) -> int:
try: ts = l.get("nextResetTime") if l else None
m = (float(ts) - now) / 60.0 if not isinstance(ts, (int, float)):
except (TypeError, ValueError):
return -1 return -1
if ts > 1e10: # epoch ms → s
ts = ts / 1000.0
m = (ts - time.time()) / 60.0
return int(round(m)) if m > 0 else -1 return int(round(m)) if m > 0 else -1
# Assume z.ai proxies Anthropic's unified rate-limit headers. short = min(toks, key=win_seconds) if toks else None
if h.get("anthropic-ratelimit-unified-5h-utilization") is not None: weekly = max(toks, key=win_seconds) if len(toks) > 1 else None
level = str(data.get("level") or "").strip()
st = level.capitalize() if level else "allowed"
if pct(short) >= 100 or pct(weekly) >= 100:
st = "limited"
if not toks: # authenticated but no token windows reported
return ProviderStatus(ok=True, st=st or "allowed")
return ProviderStatus( return ProviderStatus(
s=pct(h.get("anthropic-ratelimit-unified-5h-utilization")), s=pct(short), sr=reset_min(short),
sr=reset_min(h.get("anthropic-ratelimit-unified-5h-reset")), w=pct(weekly), wr=reset_min(weekly),
w=pct(h.get("anthropic-ratelimit-unified-7d-utilization")), st=st, ok=True,
wr=reset_min(h.get("anthropic-ratelimit-unified-7d-reset")),
st=h.get("anthropic-ratelimit-unified-5h-status", "allowed"),
ok=True,
) )
# Connected but no known limit headers — surface what z.ai DID return so the
# mapping can be finalized against a live response (tools/probe_zai.py).
limitish = sorted(k for k in h.keys()
if any(w in k.lower() for w in ("limit", "rate", "quota", "usage")))
log(f"z.ai: no unified rate-limit headers; limit-ish headers = {limitish or '<none>'}")
return ProviderStatus(ok=True, st="allowed")
+96 -76
View File
@@ -1,7 +1,8 @@
"""v3 M3 — z.ai (GLM) provider: Anthropic-compatible poll + quota-safe throttle. """v3 M3 — z.ai (GLM) provider: usage from the monitor/quota endpoint.
Uses httpx.MockTransport so no real z.ai call is made. Covers header mapping, Uses httpx.MockTransport so no real z.ai call is made. Covers the token-window
auth rejection, the no-known-headers fallback, and the self-throttle cache. mapping (5h -> s, weekly -> w), raw-key auth, the ignored monthly TIME_LIMIT,
and auth/error handling.
""" """
import asyncio import asyncio
@@ -10,7 +11,7 @@ import time
import httpx import httpx
from daemon.providers.zai import ZaiProvider, DEFAULT_BASE from daemon.providers.zai import ZaiProvider, DEFAULT_HOST, _MONITOR_PATH
def _run(coro): def _run(coro):
@@ -33,108 +34,127 @@ def _config(tmp_path, monkeypatch, zai: dict):
monkeypatch.setenv("LOCALAPPDATA", str(tmp_path)) monkeypatch.setenv("LOCALAPPDATA", str(tmp_path))
def _transport(handler): def _tok(pct, reset_ms, unit=3, number=5):
return httpx.MockTransport(handler) return {"type": "TOKENS_LIMIT", "unit": unit, "number": number,
"percentage": pct, "nextResetTime": reset_ms}
def test_maps_unified_rate_limit_headers(tmp_path, monkeypatch): def _time_limit(pct=0):
_config(tmp_path, monkeypatch, {"enabled": True, "api_key": "sk-zai"}) return {"type": "TIME_LIMIT", "unit": 5, "number": 1, "usage": 100,
now = time.time() "currentValue": 0, "remaining": 100, "percentage": pct,
"nextResetTime": 9999999999999, "usageDetails": []}
def _ok_resp(limits, level="lite"):
return httpx.Response(200, json={"code": 200, "msg": "Operation successful",
"success": True, "data": {"level": level, "limits": limits}})
def _provider_returning(resp_or_fn):
def handler(request):
return resp_or_fn(request) if callable(resp_or_fn) else resp_or_fn
return ZaiProvider(transport=httpx.MockTransport(handler))
def test_maps_token_windows(tmp_path, monkeypatch):
_config(tmp_path, monkeypatch, {"enabled": True, "api_key": "sk-zai",
"base_url": "https://api.z.ai/api/anthropic"})
now_ms = time.time() * 1000
seen = {} seen = {}
def handler(request: httpx.Request) -> httpx.Response: def handler(request: httpx.Request) -> httpx.Response:
seen["url"] = str(request.url) seen["url"] = str(request.url)
seen["auth"] = request.headers.get("authorization") seen["auth"] = request.headers.get("authorization")
return httpx.Response(200, headers={ seen["lang"] = request.headers.get("accept-language")
"anthropic-ratelimit-unified-5h-utilization": "0.42", seen["method"] = request.method
"anthropic-ratelimit-unified-5h-reset": str(now + 3600), return _ok_resp([
"anthropic-ratelimit-unified-5h-status": "allowed", _time_limit(), # ignored monthly web-tool quota
"anthropic-ratelimit-unified-7d-utilization": "0.10", _tok(1, now_ms + 3600_000, unit=3, number=5), # 5h window -> short
"anthropic-ratelimit-unified-7d-reset": str(now + 7200), _tok(2, now_ms + 6 * 86400_000, unit=6, number=1), # weekly window -> weekly
}, json={"ok": True}) ])
st = _run(ZaiProvider(transport=_transport(handler)).poll()) st = _run(ZaiProvider(transport=httpx.MockTransport(handler)).poll())
assert st.ok is True assert st.ok is True
assert st.s == 42.0 and 58 <= st.sr <= 60 assert st.s == 1.0 and 58 <= st.sr <= 60
assert st.w == 10.0 and 118 <= st.wr <= 120 assert st.w == 2.0 and 8000 <= st.wr <= 8700 # ~6 days in minutes
assert st.st == "allowed" assert st.st == "Lite"
assert seen["url"] == f"{DEFAULT_BASE}/v1/messages" # default base used # raw key auth (no "Bearer"), monitor endpoint on the configured host, GET
assert seen["auth"] == "Bearer sk-zai" # Bearer auth assert seen["method"] == "GET"
assert seen["url"] == f"{DEFAULT_HOST}{_MONITOR_PATH}"
assert seen["auth"] == "sk-zai"
assert seen["lang"] == "en-US,en"
def test_no_key_reports_no_key(tmp_path, monkeypatch): def test_no_key(tmp_path, monkeypatch):
_config(tmp_path, monkeypatch, {"enabled": True, "api_key": ""}) _config(tmp_path, monkeypatch, {"enabled": True, "api_key": ""})
called = {"n": 0} calls = {"n": 0}
def handler(request): def handler(request):
called["n"] += 1 calls["n"] += 1
return httpx.Response(200) return _ok_resp([])
st = _run(ZaiProvider(transport=_transport(handler)).poll()) st = _run(ZaiProvider(transport=httpx.MockTransport(handler)).poll())
assert st.ok is False and st.st == "no key" and st.auth_problem is True assert st.ok is False and st.st == "no key" and st.auth_problem is True
assert called["n"] == 0 # never hit the network assert calls["n"] == 0
def test_auth_rejection(tmp_path, monkeypatch): def test_http_401_is_bad_key(tmp_path, monkeypatch):
_config(tmp_path, monkeypatch, {"enabled": True, "api_key": "bad"}) _config(tmp_path, monkeypatch, {"enabled": True, "api_key": "bad"})
p = _provider_returning(httpx.Response(401, json={"error": "unauthorized"}))
def handler(request): st = _run(p.poll())
return httpx.Response(401, json={"error": "unauthorized"})
st = _run(ZaiProvider(transport=_transport(handler)).poll())
assert st.ok is False and st.st == "bad key" and st.auth_problem is True assert st.ok is False and st.st == "bad key" and st.auth_problem is True
def test_connected_without_known_headers(tmp_path, monkeypatch): def test_body_success_false_is_bad_key(tmp_path, monkeypatch):
_config(tmp_path, monkeypatch, {"enabled": True, "api_key": "bad"})
p = _provider_returning(httpx.Response(200, json={"code": 401, "msg": "auth", "success": False, "data": None}))
st = _run(p.poll())
assert st.ok is False and st.st == "bad key" and st.auth_problem is True
def test_single_token_window(tmp_path, monkeypatch):
_config(tmp_path, monkeypatch, {"enabled": True, "api_key": "sk"})
now_ms = time.time() * 1000
p = _provider_returning(_ok_resp([_tok(37, now_ms + 3600_000)]))
st = _run(p.poll())
assert st.s == 37.0 and st.w == 0.0 and st.wr == -1 and st.ok is True
def test_hundred_percent_is_limited(tmp_path, monkeypatch):
_config(tmp_path, monkeypatch, {"enabled": True, "api_key": "sk"})
now_ms = time.time() * 1000
p = _provider_returning(_ok_resp([
_tok(100, now_ms + 60_000, unit=3, number=5),
_tok(40, now_ms + 6 * 86400_000, unit=6, number=1)]))
st = _run(p.poll())
assert st.st == "limited" and st.s == 100.0
def test_only_time_limit_reports_connected(tmp_path, monkeypatch):
_config(tmp_path, monkeypatch, {"enabled": True, "api_key": "sk"})
p = _provider_returning(_ok_resp([_time_limit(5)], level="pro"))
st = _run(p.poll())
assert st.ok is True and st.st == "Pro" and st.s == 0.0
def test_transient_error_reported(tmp_path, monkeypatch):
_config(tmp_path, monkeypatch, {"enabled": True, "api_key": "sk"}) _config(tmp_path, monkeypatch, {"enabled": True, "api_key": "sk"})
def handler(request): def handler(request):
return httpx.Response(200, headers={"x-some-quota": "5"}, json={"ok": True}) raise httpx.ConnectError("boom")
st = _run(ZaiProvider(transport=_transport(handler)).poll()) st = _run(ZaiProvider(transport=httpx.MockTransport(handler)).poll())
assert st.ok is True and st.st == "allowed" and st.s == 0.0 assert st.ok is False and st.st == "offline"
def test_custom_base_url_and_model(tmp_path, monkeypatch): def test_custom_base_host(tmp_path, monkeypatch):
_config(tmp_path, monkeypatch, { _config(tmp_path, monkeypatch, {"enabled": True, "api_key": "sk",
"enabled": True, "api_key": "sk", "base_url": "https://zzz.example/anthropic/"}) "base_url": "https://zzz.example/api/anthropic"})
seen = {} seen = {}
def handler(request): def handler(request):
seen["url"] = str(request.url) seen["url"] = str(request.url)
seen["body"] = json.loads(request.content) return _ok_resp([])
return httpx.Response(200, json={"ok": True})
_run(ZaiProvider(transport=_transport(handler)).poll()) _run(ZaiProvider(transport=httpx.MockTransport(handler)).poll())
assert seen["url"] == "https://zzz.example/anthropic/v1/messages" # trailing slash trimmed assert seen["url"] == f"https://zzz.example{_MONITOR_PATH}" # monitor path on the same host
assert seen["body"]["model"] == "glm-4.6"
def test_self_throttle_serves_cache(tmp_path, monkeypatch):
_config(tmp_path, monkeypatch, {"enabled": True, "api_key": "sk"})
calls = {"n": 0}
def handler(request):
calls["n"] += 1
return httpx.Response(200, json={"ok": True})
p = ZaiProvider(transport=_transport(handler))
st1 = _run(p.poll())
st2 = _run(p.poll()) # within the 15-min interval
assert calls["n"] == 1 # second served from cache
assert st1 is st2
def test_transient_error_not_cached(tmp_path, monkeypatch):
_config(tmp_path, monkeypatch, {"enabled": True, "api_key": "sk"})
calls = {"n": 0}
def handler(request):
calls["n"] += 1
raise httpx.ConnectError("boom")
p = ZaiProvider(transport=_transport(handler))
st1 = _run(p.poll())
st2 = _run(p.poll())
assert st1.ok is False and st1.st == "offline"
assert calls["n"] == 2 # blip retried, not cached
+12 -31
View File
@@ -1,17 +1,13 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""One-off diagnostic: see exactly what z.ai's Anthropic-compatible endpoint """One-off diagnostic for the z.ai GLM Coding Plan usage endpoint (the source
returns, so the ZaiProvider's rate-limit header mapping can be finalized against ZaiProvider reads). Dumps the raw quota JSON so the window mapping can be
a real response (the header names aren't documented). checked against a live plan.
Usage (key via env so it never lands in shell history): Usage (key via env so it never lands in shell history):
ZAI_API_KEY=... python tools/probe_zai.py [base_url] [model] ZAI_API_KEY=... python tools/probe_zai.py [host]
Defaults: base = https://api.z.ai/api/anthropic , model = glm-4.6 Default host = https://api.z.ai . It's a status GET — it does NOT spend the
prompt-metered plan. The API key is read from the environment and never printed.
Prints the HTTP status and the FULL response header set (highlighting any header
mentioning limit/rate/quota/usage/reset), plus a short body preview. Sends a
1-token "hi" — on a prompt-metered plan that is one prompt, so run it sparingly.
The API key is read from the environment and never printed.
""" """
import json import json
import os import os
@@ -19,36 +15,21 @@ import sys
import httpx import httpx
BASE = (sys.argv[1] if len(sys.argv) > 1 else "https://api.z.ai/api/anthropic").rstrip("/") HOST = (sys.argv[1] if len(sys.argv) > 1 else "https://api.z.ai").rstrip("/")
MODEL = sys.argv[2] if len(sys.argv) > 2 else "glm-4.6"
KEY = os.environ.get("ZAI_API_KEY") or os.environ.get("ANTHROPIC_AUTH_TOKEN") KEY = os.environ.get("ZAI_API_KEY") or os.environ.get("ANTHROPIC_AUTH_TOKEN")
if not KEY: if not KEY:
sys.exit("Set ZAI_API_KEY in the environment first (it is not printed).") sys.exit("Set ZAI_API_KEY in the environment first (it is not printed).")
url = f"{BASE}/v1/messages" url = f"{HOST}/api/monitor/usage/quota/limit"
headers = { print(f"GET {url}")
"Authorization": f"Bearer {KEY}",
"anthropic-version": "2023-06-01",
"Content-Type": "application/json",
}
body = {"model": MODEL, "max_tokens": 1, "messages": [{"role": "user", "content": "hi"}]}
print(f"POST {url} (model={MODEL})")
try: try:
resp = httpx.post(url, headers=headers, json=body, timeout=30.0) resp = httpx.get(url, headers={"Authorization": KEY, "Accept-Language": "en-US,en"}, timeout=30.0)
except httpx.HTTPError as e: except httpx.HTTPError as e:
sys.exit(f"request failed: {e}") sys.exit(f"request failed: {e}")
print(f"HTTP {resp.status_code}\n") print(f"HTTP {resp.status_code}\n")
INTEREST = ("limit", "rate", "quota", "usage", "reset", "remaining")
print("--- response headers ---")
for k in sorted(resp.headers.keys()):
mark = " <<<" if any(w in k.lower() for w in INTEREST) else ""
print(f" {k}: {resp.headers[k]}{mark}")
print("\n--- body preview ---")
try: try:
print(json.dumps(resp.json(), ensure_ascii=False)[:600]) print(json.dumps(resp.json(), indent=2, ensure_ascii=False))
except ValueError: except ValueError:
print(resp.text[:600]) print(resp.text[:1000])