133 lines
4.0 KiB
Python
133 lines
4.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Import the 2026-08-03 CN batch2 usable keys into NewAPI.
|
|
|
|
All four upstreams (iflytek_maas, dmxapi, aihubmix) are standard
|
|
OpenAI-compatible /v1 endpoints, so each channel uses type=1
|
|
(TYPE_OPENAI) and the base_url has the trailing /v1 stripped —
|
|
NewAPI appends /v1/chat/completions itself for type 1.
|
|
|
|
Each key gets its own channel. The full upstream model list (fetched
|
|
live via GET /v1/models) is synced into the channel's `models` field.
|
|
"""
|
|
|
|
import json
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
from newapi_client import (
|
|
add_channel, list_channels, get_channel, update_channel,
|
|
test_channel, TYPE_OPENAI,
|
|
)
|
|
|
|
HUNTER = Path(__file__).resolve().parent / "llm-key-hunter"
|
|
VAULT = HUNTER / "usable_keys"
|
|
|
|
# (provider label, vault json, base_url WITHOUT /v1)
|
|
# All are standard /v1 OpenAI-compatible -> type=1, no /v1 suffix.
|
|
SOURCES = [
|
|
("IFlyTekMaaS", VAULT / "iflytek_maas" / "keys.json",
|
|
"https://maas-api.cn-huabei-1.xf-yun.com"),
|
|
("DMXAPI", VAULT / "dmxapi" / "keys.json",
|
|
"https://www.dmxapi.cn"),
|
|
("AIHubMix", VAULT / "aihubmix" / "keys.json",
|
|
"https://aihubmix.com"),
|
|
]
|
|
|
|
|
|
def load_usable():
|
|
"""Yield (label, key, models, base_url) for each USABLE vault entry."""
|
|
for label, path, base_url in SOURCES:
|
|
data = json.loads(path.read_text())
|
|
for entry in data:
|
|
if entry.get("status") != "USABLE":
|
|
continue
|
|
models = entry.get("models") or []
|
|
if not models:
|
|
continue
|
|
yield label, entry["key"], models, base_url
|
|
|
|
|
|
def existing_keys():
|
|
"""Return set of API keys already configured across all channels."""
|
|
out = set()
|
|
for ch in list_channels():
|
|
try:
|
|
full = get_channel(ch["id"]) or {}
|
|
k = full.get("key", "")
|
|
if k:
|
|
out.add(k)
|
|
except Exception:
|
|
pass
|
|
return out
|
|
|
|
|
|
def main():
|
|
entries = list(load_usable())
|
|
print(f"Usable vault entries to import: {len(entries)}")
|
|
|
|
have = existing_keys()
|
|
print(f"Existing channel keys: {len(have)}\n")
|
|
|
|
added, skipped, failed = [], 0, 0
|
|
|
|
for label, key, models, base_url in entries:
|
|
if key in have:
|
|
print(f" SKIP {label}_{key[-6:]} (key already in NewAPI)")
|
|
skipped += 1
|
|
continue
|
|
|
|
name = f"{label}_{key[-6:]}"
|
|
models_csv = ",".join(models)
|
|
channel = {
|
|
"name": name,
|
|
"type": TYPE_OPENAI, # standard /v1 -> OpenAI type
|
|
"key": key,
|
|
"base_url": base_url, # NO /v1 suffix per LO's rule
|
|
"models": models_csv,
|
|
"groups": ["default"],
|
|
"model_mapping": "",
|
|
"priority": 0,
|
|
"weight": 0,
|
|
"auto_ban": 1,
|
|
}
|
|
|
|
ok, msg = add_channel(channel)
|
|
if not ok:
|
|
print(f" ADD FAIL {name}: {msg}")
|
|
failed += 1
|
|
continue
|
|
|
|
# locate the new channel id
|
|
new_id = None
|
|
for ch in list_channels():
|
|
if ch["name"] == name:
|
|
new_id = ch["id"]
|
|
break
|
|
if new_id is None:
|
|
print(f" {name} added but id not found")
|
|
failed += 1
|
|
continue
|
|
|
|
t_ok, t_msg, _ = test_channel(new_id)
|
|
if t_ok:
|
|
print(f" ✓ {name} ID={new_id} models={len(models)} [{t_msg[:60]}]")
|
|
added.append((new_id, name, len(models)))
|
|
else:
|
|
update_channel(new_id, status=2) # auto-disable on test failure
|
|
print(f" ✗ {name} ID={new_id} DISABLED: {t_msg[:80]}")
|
|
failed += 1
|
|
time.sleep(0.3)
|
|
|
|
print("\n=== Summary ===")
|
|
print(f" Added & test-passed: {len(added)}")
|
|
print(f" Failed/auto-disabled: {failed}")
|
|
print(f" Skipped (duplicate): {skipped}")
|
|
for cid, name, n in added:
|
|
print(f" ID={cid} {name} ({n} models)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|