Files
omni-parse/tools/refresh_panels_free.py
2026-10-27 14:59:01 +03:00

127 lines
5.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Перепроверка реестра new-api панелей: жив ли /api/pricing и сколько моделей бесплатных.
Запуск: python tools/refresh_panels_free.py [--workers 12] [--timeout 15] [--write]
Пишет data/panels.json (+ .bak) только с --write. В выводе — сводка и топ панелей.
Секретов не касается: только публичные /api/pricing и /api/models.
"""
from __future__ import annotations
import argparse
import json
import shutil
import urllib.request
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
PANELS = ROOT / "data" / "panels.json"
UA = "curl/8.5.0"
def _get(url: str, timeout: int):
req = urllib.request.Request(url, headers={"User-Agent": UA, "Accept": "application/json"})
with urllib.request.urlopen(req, timeout=timeout) as r:
return r.getcode(), r.read()
def probe(entry: dict, timeout: int) -> dict:
host = str(entry.get("host") or entry.get("url") or "").strip()
host = host.replace("https://", "").replace("http://", "").strip("/")
out = dict(entry)
out["host"] = host
out["checked"] = True
if not host:
out.update(alive=False, free_count=0, models=0, why="no-host")
return out
base = "https://" + host
for path in ("/api/pricing", "/api/models"):
try:
code, body = _get(base + path, timeout)
except Exception as e:
out.update(alive=False, why=type(e).__name__)
continue
if code != 200:
out.update(alive=False, why="http %s" % code)
continue
try:
js = json.loads(body.decode("utf-8", "replace"))
except Exception:
out.update(alive=False, why="bad-json")
continue
if path == "/api/models":
rows = js.get("data") if isinstance(js, dict) else js
if isinstance(rows, list):
out.update(alive=True, why="models-only", models=len(rows), free_count=int(out.get("free_count") or 0))
return out
rows = js.get("data") if isinstance(js, dict) else None
if not isinstance(rows, list):
out.update(alive=False, why="no-data")
continue
free = 0
names: list[str] = []
for m in rows:
if not isinstance(m, dict):
continue
ratio = m.get("model_ratio")
cratio = m.get("completion_ratio")
quota = m.get("quota_type")
try:
# Строго как адаптер newapi_pricing: модель бесплатна только если
# нулевые И prompt, И completion. Мягкое «ratio==0 или quota==0»
# завышало счётчик в разы (760 моделей на панели с 1 бесплатной).
if ratio is None and cratio is None:
freeish = (quota == 0)
else:
freeish = float(ratio or 0) == 0.0 and float(cratio or 0) == 0.0
except Exception:
freeish = False
if freeish:
free += 1
nm = m.get("model_name") or m.get("model")
if isinstance(nm, str) and len(names) < 12:
names.append(nm)
out.update(alive=True, why="pricing", models=len(rows), free_count=free, free=names)
return out
return out
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--workers", type=int, default=12)
ap.add_argument("--timeout", type=int, default=15)
ap.add_argument("--write", action="store_true")
a = ap.parse_args()
raw = json.loads(PANELS.read_text(encoding="utf-8"))
rows = raw["panels"] if isinstance(raw, dict) else raw
print("в реестре:", len(rows))
with ThreadPoolExecutor(max_workers=a.workers) as ex:
res = list(ex.map(lambda e: probe(e, a.timeout), rows))
alive = [r for r in res if r.get("alive")]
with_free = [r for r in alive if int(r.get("free_count") or 0) > 0]
total_free = sum(int(r.get("free_count") or 0) for r in with_free)
print("живых /api/pricing:", len(alive), "| с бесплатными:", len(with_free), "| free-моделей:", total_free)
print("было: с бесплатными %d" % sum(1 for r in rows if int(r.get("free_count") or 0) > 0))
with_free.sort(key=lambda r: -int(r.get("free_count") or 0))
for r in with_free[:10]:
print(" %-28s free %-4s всего %s" % (r["host"], r.get("free_count"), r.get("models")))
from collections import Counter
print("причины отказов:", Counter(r.get("why") for r in res if not r.get("alive")).most_common(6))
if a.write:
if PANELS.exists():
shutil.copy2(PANELS, PANELS.with_suffix(".json.bak"))
payload = dict(raw) if isinstance(raw, dict) else {"panels": res}
payload["panels"] = res
PANELS.write_text(json.dumps(payload, ensure_ascii=False, indent=1), encoding="utf-8")
print("записано:", PANELS)
return 0
if __name__ == "__main__":
raise SystemExit(main())