#!/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()