Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/aihubmix_skills.json
T

1 line
121 KiB
JSON
Raw 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.
{"repo": "aihubmix/skills", "files": {"skills/aihubmixApi/scripts/aihubmixApi.py": {"53938717da78fb7d59acd3309791aca792326c13": {"t": 1785674652.5670595, "n": 114387, "h": "ffa0f7d6056a0a20", "c": "#!/usr/bin/env python3\n\"\"\"\nSmall AIHubMix API helper for Codex skills.\n\nThe script intentionally keeps volatile model data out of the skill. It reads\nmodel metadata from the live models API by default, or from a caller-provided\nJSON file for local validation.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nfrom datetime import datetime\nimport io\nimport json\nimport os\nimport platform\nimport re\nimport shutil\nimport subprocess\nimport sys\nimport tarfile\nimport textwrap\nimport urllib.error\nimport urllib.request\nfrom pathlib import Path\nfrom typing import Any\n\n\n# Force UTF-8 stdout/stderr so Chinese output is not mangled to GBK mojibake\n# on Windows or when the stream is redirected/piped (default code page there\n# is cp936, not UTF-8).\nfor _stream in (sys.stdout, sys.stderr):\n try:\n _stream.reconfigure(encoding=\"utf-8\")\n except (AttributeError, ValueError): # non-reconfigurable stream\n pass\n\n\n# Output language for status lines/labels: \"en\" (default, neutral) or \"zh\".\n# Set via the global --lang flag or AIHUBMIX_LANG; main() resolves it into _LANG.\n_LANG = (os.environ.get(\"AIHUBMIX_LANG\") or \"en\").strip().lower()\nif _LANG not in (\"en\", \"zh\"):\n _LANG = \"en\"\n\n\ndef _t(en: str, zh: str) -> str:\n \"\"\"Pick output text by the resolved language (data values are never translated).\"\"\"\n return zh if _LANG == \"zh\" else en\n\n\nDEFAULT_MODELS_URL = \"https://aihubmix.com/api/v1/models\"\nDEFAULT_SERVER_URL = \"https://aihubmix.com\"\nDEFAULT_PRICE_UNIT = \"USD / 1M tokens\"\nSDK_PACKAGE_NAME = \"@aihubmix/ai-sdk-provider\"\nSDK_RECOMMENDED_VERSION = \"2.1.0\"\nSDK_REGISTRY_URL = \"https://registry.npmjs.org/@aihubmix%2Fai-sdk-provider\"\nOPENAPI_REPO_URL = \"https://github.com/AIhubmix/aihubmix-openapi\"\nOPENAPI_JSON_URL = \"https://aihubmix.com/openapi.json\"\nCONTRACT_JSON_URL = \"https://aihubmix.com/contract.json\"\nGATEWAY_ERRORS_URL = \"https://github.com/AIhubmix/aihubmix-openapi/blob/main/gateway/errors.yml\"\n\n# Companion `aihubmix` CLI. The repo URL is the durable anchor shown everywhere\n# (stable even if the install scripts move); the raw install scripts are the\n# per-OS executable entrypoints, derived from the same repo path so there is a\n# single source of truth and the CLI can always be re-found.\nCLI_BIN = \"aihubmix\"\nCLI_REPO = \"AIhubmix/platfrom-cli\"\nCLI_REPO_URL = f\"https://github.com/{CLI_REPO}\"\nCLI_RAW_BASE = f\"https://raw.githubusercontent.com/{CLI_REPO}/main\"\nCLI_INSTALL_PS1 = f\"{CLI_RAW_BASE}/install.ps1\"\nCLI_INSTALL_SH = f\"{CLI_RAW_BASE}/install.sh\"\n\nPROTOCOL_ALIASES = {\n \"chat\": \"chat\",\n \"chat-completions\": \"chat\",\n \"model-response\": \"responses\",\n \"model-responses\": \"responses\",\n \"response\": \"responses\",\n \"completion\": \"completions\",\n \"embedding\": \"embeddings\",\n \"image\": \"images\",\n \"image-generation\": \"images\",\n \"video\": \"videos\",\n \"retrieve-video\": \"video-retrieve\",\n \"download-video\": \"video-content\",\n \"video-download\": \"video-content\",\n \"remix-video\": \"video-remix\",\n \"speech\": \"audio-speech\",\n \"tts\": \"audio-speech\",\n \"transcription\": \"audio-transcriptions\",\n \"translation\": \"audio-translations\",\n \"moderation\": \"moderations\",\n \"message\": \"messages\",\n \"anthropic\": \"messages\",\n \"generate-content\": \"gemini\",\n \"vertex\": \"gemini\",\n \"google\": \"gemini\",\n}\n\nPROTOCOL_SPECS: dict[str, dict[str, Any]] = {\n \"chat\": {\n \"label\": \"OpenAI-Compatible Chat\",\n \"method\": \"POST\",\n \"path\": \"/v1/chat/completions\",\n \"family\": \"openai\",\n },\n \"responses\": {\n \"label\": \"OpenAI Model Responses\",\n \"method\": \"POST\",\n \"path\": \"/v1/responses\",\n \"family\": \"openai\",\n },\n \"completions\": {\n \"label\": \"OpenAI-Compatible Legacy Completion\",\n \"method\": \"POST\",\n \"path\": \"/v1/completions\",\n \"family\": \"openai\",\n },\n \"embeddings\": {\n \"label\": \"OpenAI-Compatible Embeddings\",\n \"method\": \"POST\",\n \"path\": \"/v1/embeddings\",\n \"family\": \"openai\",\n },\n \"images\": {\n \"label\": \"OpenAI-Compatible Image Generation\",\n \"method\": \"POST\",\n \"path\": \"/v1/images/generations\",\n \"family\": \"openai\",\n },\n \"videos\": {\n \"label\": \"OpenAI-Compatible Video Creation\",\n \"method\": \"POST\",\n \"path\": \"/v1/videos\",\n \"family\": \"openai\",\n },\n \"video-retrieve\": {\n \"label\": \"OpenAI-Compatible Video Retrieval\",\n \"method\": \"GET\",\n \"path\": \"/v1/videos/{video_id}\",\n \"family\": \"openai\",\n \"needs_video_id\": True,\n },\n \"video-delete\": {\n \"label\": \"OpenAI-Compatible Video Deletion\",\n \"method\": \"DELETE\",\n \"path\": \"/v1/videos/{video_id}\",\n \"family\": \"openai\",\n \"needs_video_id\": True,\n },\n \"video-content\": {\n \"label\": \"OpenAI-Compatible Video Content Download\",\n \"method\": \"GET\",\n \"path\": \"/v1/videos/{video_id}/content\",\n \"family\": \"openai\",\n \"needs_video_id\": True,\n },\n \"video-remix\": {\n \"label\": \"OpenAI-Compatible Video Remix\",\n \"method\": \"POST\",\n \"path\": \"/v1/videos/{video_id}/remix\",\n \"family\": \"openai\",\n \"needs_video_id\": True,\n },\n \"audio-speech\": {\n \"label\": \"OpenAI-Compatible Speech\",\n \"method\": \"POST\",\n \"path\": \"/v1/audio/speech\",\n \"family\": \"openai\",\n \"binary_response\": True,\n },\n \"audio-transcriptions\": {\n \"label\": \"OpenAI-Compatible Audio Transcription\",\n \"method\": \"POST\",\n \"path\": \"/v1/audio/transcriptions\",\n \"family\": \"openai\",\n \"multipart\": True,\n },\n \"audio-translations\": {\n \"label\": \"OpenAI-Compatible Audio Translation\",\n \"method\": \"POST\",\n \"path\": \"/v1/audio/translations\",\n \"family\": \"openai\",\n \"multipart\": True,\n },\n \"moderations\": {\n \"label\": \"OpenAI-Compatible Moderation\",\n \"method\": \"POST\",\n \"path\": \"/v1/moderations\",\n \"family\": \"openai\",\n },\n \"messages\": {\n \"label\": \"Claude/Anthropic-Compatible Messages\",\n \"method\": \"POST\",\n \"path\": \"/v1/messages\",\n \"family\": \"anthropic\",\n },\n \"gemini\": {\n \"label\": \"Google Vertex/Gemini-Compatible Generate Content\",\n \"method\": \"POST\",\n \"path\": \"/gemini/v1beta/models/{model}:generateContent\",\n \"family\": \"gemini\",\n },\n}\n\n\ndef display_models_source() -> str:\n return DEFAULT_MODELS_URL\n\n\ndef display_openapi_source(filename: str = \"openapi.json\") -> str:\n if filename == \"openapi.json\":\n return OPENAPI_JSON_URL\n if filename == \"contract.json\":\n return CONTRACT_JSON_URL\n return OPENAPI_REPO_URL\n\n\ndef display_error_contract_source() -> str:\n return OPENAPI_JSON_URL\n\n\ndef load_json_source(source: str | None) -> Any:\n # Always fetch live (no caching): every lookup hits the source directly.\n source = source or os.environ.get(\"AIHUBMIX_MODELS_URL\") or DEFAULT_MODELS_URL\n if source == \"-\":\n return json.load(sys.stdin)\n if source.startswith((\"http://\", \"https://\")):\n request = urllib.request.Request(\n source,\n headers={\n \"Accept\": \"application/json\",\n \"User-Agent\": \"aihubmixApi-skill/0.1\",\n },\n )\n try:\n with urllib.request.urlopen(request, timeout=20) as response:\n return json.load(response)\n except urllib.error.HTTPError as exc:\n detail = exc.read().decode(\"utf-8\", errors=\"replace\")\n raise SystemExit(f\"HTTP {exc.code} while fetching {source}: {detail}\") from exc\n except urllib.error.URLError as exc:\n raise SystemExit(f\"Could not fetch {source}: {exc.reason}\") from exc\n\n path = Path(source).expanduser()\n with path.open(\"r\", encoding=\"utf-8\") as handle:\n return json.load(handle)\n\n\ndef extract_models(payload: Any) -> list[dict[str, Any]]:\n return sorted(extract_models_in_source_order(payload), key=lambda item: model_id(item).lower())\n\n\ndef extract_models_in_source_order(payload: Any) -> list[dict[str, Any]]:\n if isinstance(payload, list):\n candidates = payload\n elif isinstance(payload, dict):\n candidates = None\n for key in (\"data\", \"models\", \"items\", \"list\", \"result\"):\n value = payload.get(key)\n if isinstance(value, list):\n candidates = value\n break\n if isinstance(value, dict):\n nested = extract_models_in_source_order(value)\n if nested:\n candidates = nested\n break\n if candidates is None:\n candidates = [payload] if any(k in payload for k in (\"id\", \"model\", \"model_id\")) else []\n else:\n candidates = []\n\n return [item for item in candidates if isinstance(item, dict)]\n\n\ndef model_id(model: dict[str, Any]) -> str:\n for key in (\"model_id\", \"id\", \"model\", \"name\", \"slug\"):\n value = model.get(key)\n if value is not None:\n return str(value)\n return \"\"\n\n\ndef model_label(model: dict[str, Any]) -> str:\n for key in (\"model_name\", \"name\", \"display_name\", \"label\", \"title\"):\n value = model.get(key)\n if value:\n return str(value)\n return model_id(model)\n\n\ndef developer(model: dict[str, Any]) -> str:\n for key in (\"developer\", \"provider\", \"developer_id\", \"provider_id\", \"owned_by\"):\n value = model.get(key)\n if value:\n return str(value)\n return \"-\"\n\n\ndef context_length(model: dict[str, Any]) -> str:\n for key in (\n \"context_length\",\n \"context_window\",\n \"max_context\",\n \"max_context_length\",\n \"input_context_length\",\n ):\n value = model.get(key)\n if value is not None:\n return str(value)\n return \"-\"\n\n\ndef max_output(model: dict[str, Any]) -> str:\n for key in (\"max_output\", \"max_output_tokens\", \"output_context_length\"):\n value = model.get(key)\n if value is not None:\n return str(value)\n return \"-\"\n\n\ndef format_tokens(value: str) -> str:\n if value in {\"\", \"-\"}:\n return \"-\"\n try:\n number = int(float(value))\n except ValueError:\n return value\n if number == 0:\n return \"0\"\n if number >= 1_000_000 and number % 1_000_000 == 0:\n return f\"{number // 1_000_000}M\"\n if number >= 1_000_000:\n compact_value = number / 1_000_000\n return f\"{compact_value:.2f}\".rstrip(\"0\").rstrip(\".\") + \"M\"\n if number >= 1_000 and number % 1_000 == 0:\n return f\"{number // 1_000}K\"\n if number >= 1_000:\n compact_value = number / 1_000\n return f\"{compact_value:.1f}\".rstrip(\"0\").rstrip(\".\") + \"K\"\n return str(number)\n\n\ndef context_display(model: dict[str, Any]) -> str:\n return format_tokens(context_length(model))\n\n\ndef max_output_display(model: dict[str, Any]) -> str:\n return format_tokens(max_output(model))\n\n\ndef compact(value: Any) -> str:\n if value is None:\n return \"-\"\n if isinstance(value, (str, int, float, bool)):\n return str(value)\n if isinstance(value, list):\n return \",\".join(compact(item) for item in value[:8]) or \"-\"\n if isinstance(value, dict):\n parts = []\n for key in sorted(value):\n item = value[key]\n if item is not None:\n parts.append(f\"{key}={compact(item)}\")\n return \"; \".join(parts) or \"-\"\n return str(value)\n\n\ndef pricing(model: dict[str, Any]) -> str:\n value = model.get(\"pricing\") or model.get(\"price\") or model.get(\"prices\")\n return compact(value)\n\n\ndef price_unit() -> str:\n return os.environ.get(\"AIHUBMIX_PRICE_UNIT\", DEFAULT_PRICE_UNIT)\n\n\ndef query_time_label() -> str:\n now = datetime.now().astimezone()\n offset = now.strftime(\"%z\")\n if offset:\n offset_label = f\"UTC{offset[:3]}:{offset[3:]}\"\n else:\n offset_label = \"local timezone\"\n stamp = now.strftime(\"%Y-%m-%d %H:%M:%S\")\n return _t(f\"{stamp} (local time, {offset_label})\", f\"{stamp} (本地时间,{offset_label})\")\n\n\ndef types(model: dict[str, Any]) -> str:\n value = model.get(\"types\") or model.get(\"type\") or model.get(\"endpoints\")\n return compact(value)\n\n\ndef features(model: dict[str, Any]) -> str:\n value = model.get(\"features\") or model.get(\"capabilities\") or model.get(\"input_modalities\")\n return compact(value)\n\n\ndef endpoint_display(model: dict[str, Any]) -> str:\n raw = str(model.get(\"endpoints\") or \"\").strip()\n if not raw:\n model_type = str(model.get(\"types\") or \"\")\n if \"llm\" in model_type and \"image_generation\" not in model_type:\n return \"not declared; use OpenAI-compatible chat example by default\"\n return \"not declared; choose endpoint by model type and verify against contract\"\n labels = {\n \"chat_completions\": \"OpenAI-compatible chat\",\n \"responses\": \"OpenAI Responses\",\n \"gemini_api\": \"Gemini native\",\n \"claude_api\": \"Claude/Anthropic-compatible\",\n \"embeddings\": \"Embeddings\",\n \"image_generation\": \"Image generation\",\n \"audio\": \"Audio\",\n \"moderations\": \"Moderation\",\n \"videos\": \"Video\",\n }\n parts = [part.strip() for part in raw.replace(\";\", \",\").split(\",\") if part.strip()]\n mapped = [labels.get(part, part) for part in parts]\n return \"; \".join(mapped) if mapped else raw\n\n\ndef endpoint_note(model: dict[str, Any]) -> str:\n raw = str(model.get(\"endpoints\") or \"\").strip()\n return raw if raw else \"-\"\n\n\ndef suggested_protocol(model: dict[str, Any]) -> str | None:\n raw_endpoint = compact(model.get(\"endpoints\")).lower()\n raw_type = compact(model.get(\"types\") or model.get(\"type\")).lower()\n searchable = model_search_text(model).lower()\n\n if \"gemini_api\" in raw_endpoint:\n return \"gemini\"\n if \"claude_api\" in raw_endpoint:\n return \"messages\"\n if \"responses\" in raw_endpoint:\n return \"responses\"\n if \"chat_completions\" in raw_endpoint:\n return \"chat\"\n if \"embedding\" in raw_endpoint or \"embedding\" in raw_type:\n return \"embeddings\"\n if any(value in raw_endpoint or value in raw_type for value in (\"image_generation\", \"image\")):\n return \"images\"\n if any(value in raw_endpoint or value in raw_type for value in (\"video\", \"videos\")):\n return \"videos\"\n if any(value in raw_endpoint or value in raw_type for value in (\"speech\", \"tts\")):\n return \"audio-speech\"\n if \"transcription\" in raw_endpoint or \"transcription\" in raw_type:\n return \"audio-transcriptions\"\n if \"translation\" in raw_endpoint or \"translation\" in raw_type:\n return \"audio-translations\"\n if \"moderation\" in raw_endpoint or \"moderation\" in raw_type:\n return \"moderations\"\n if \"audio\" in raw_endpoint or \"audio\" in raw_type:\n return \"audio-transcriptions\"\n if \"llm\" in raw_type or \"chat\" in raw_type:\n return \"chat\"\n if any(value in searchable for value in (\"gemini\", \"claude\", \"anthropic\")):\n if \"gemini\" in searchable:\n return \"gemini\"\n return \"messages\"\n return \"chat\"\n\n\ndef protocol_name(protocol: str) -> str:\n protocol = normalize_protocol(protocol)\n return PROTOCOL_SPECS[protocol][\"label\"]\n\n\ndef protocol_auth_label(protocol: str) -> str:\n header_name, header_value = auth_header(protocol)\n return f\"{header_name}: {header_value}\"\n\n\ndef print_user_overview(selected: list[dict[str, Any]], query: str | None = None) -> None:\n print(\"## Quick Start Overview\")\n print()\n if query:\n print(f\"- Searched: `{query}`\")\n print(f\"- Models shown: `{len(selected)}`\")\n print(f\"- Price unit: `{price_unit()}`\")\n print(\"- How to use: copy a `model_id`, check context/price, then run the example below.\")\n print()\n print(\"| model_id | protocol | context | max_output | price |\")\n print(\"|---|---|---:|---:|---|\")\n for item in selected[:5]:\n protocol = suggested_protocol(item)\n protocol_label = protocol_name(protocol) if protocol else \"verify endpoint by model type\"\n print(\n \"| \"\n + \" | \".join(\n [\n f\"`{model_id(item)}`\",\n protocol_label,\n context_display(item),\n max_output_display(item),\n pricing(item).replace(\"|\", \"\\\\|\"),\n ]\n )\n + \" |\"\n )\n print()\n print(\"## Technical Details\")\n print()\n\n\ndef example_intro(kind: str, os_name: str | None = None) -> str:\n if kind == \"command\":\n shell_name = \"Windows PowerShell\" if os_name == \"windows\" else \"macOS/Linux shell\"\n return (\n f\"Use this {shell_name} example when you want to test the model directly from a terminal. \"\n \"It asks for `AIHUBMIX_API_KEY`, sends one request, and prints the model reply.\"\n )\n if kind == \"js\":\n return (\n \"Use this JavaScript example inside a Node.js project or service. \"\n \"Run the environment setup command below before starting the script.\"\n )\n if kind == \"python\":\n return (\n \"Use this Python example for a quick backend or script test. \"\n \"Run the environment setup command below before starting the script.\"\n )\n return \"Use this example as a minimal starting point.\"\n\n\ndef env_setup_block(kind: str, os_name: str | None = None) -> tuple[str, str]:\n if kind == \"command\":\n return \"\", \"\"\n if os_name == \"windows\":\n return (\n \"PowerShell\",\n '$env:AIHUBMIX_API_KEY = Read-Host \"Paste AIHubMix API Key\"',\n )\n if kind == \"js\":\n return (\n \"bash\",\n 'export AIHUBMIX_API_KEY=\"sk-...\"',\n )\n return (\n \"bash\",\n 'export AIHUBMIX_API_KEY=\"sk-...\"',\n )\n\n\ndef print_env_setup(kind: str, os_name: str | None = None) -> None:\n lang, command = env_setup_block(kind, os_name)\n if not command:\n return\n print(\"先设置环境变量,把 `sk-...` 换成你的 AIHubMix API Key:\")\n print()\n print(f\"```{lang}\")\n print(command)\n print(\"```\")\n print()\n\n\ndef print_model_switch_hint(selected: list[dict[str, Any]]) -> None:\n if len(selected) <= 1:\n return\n shown = \", \".join(f\"`{model_id(item)}`\" for item in selected[:6])\n print(f\"可选模型 ID:{shown}\")\n print(\"想切换模型时,直接把示例里的 `model` / `model_id` 换成对应 ID 即可。\")\n print()\n\n\ndef availability_note(model: dict[str, Any]) -> str:\n desc = str(model.get(\"desc\") or \"\")\n lowered = desc.lower()\n notes = []\n if \"temporarily suspended\" in lowered or \"suspended\" in lowered:\n notes.append(\"access note in desc\")\n if \"expensive\" in lowered or \"high price\" in lowered:\n notes.append(\"cost note in desc\")\n return \", \".join(notes) if notes else \"-\"\n\n\nLATEST_QUERY_TERMS = (\"latest\", \"newest\", \"recent\", \"最新\", \"新模型\", \"最近\")\n\nGENERIC_QUERY_TOKENS = {\n \"aihubmix\",\n \"api\",\n \"info\",\n \"interface\",\n \"latest\",\n \"model\",\n \"models\",\n \"newest\",\n \"price\",\n \"pricing\",\n \"recent\",\n}\n\nMODEL_ALIAS_GROUPS: list[tuple[tuple[str, ...], tuple[str, ...]]] = [\n ((\"月之暗面\", \"moonshot\", \"kimi\"), (\"moonshot\", \"kimi\")),\n ((\"谷歌\", \"google\", \"gemini\"), (\"google\", \"gemini\")),\n ((\"claude\", \"anthropic\"), (\"claude\", \"anthropic\")),\n ((\"opus\",), (\"opus\",)),\n ((\"sonnet\",), (\"sonnet\",)),\n ((\"haiku\",), (\"haiku\",)),\n ((\"openai\", \"chatgpt\", \"gpt\"), (\"openai\", \"gpt\")),\n ((\"qwen\", \"通义\", \"通义千问\", \"千问\", \"alibaba\", \"阿里\"), (\"qwen\", \"通义\", \"千问\", \"alibaba\")),\n ((\"deepseek\", \"深度求索\"), (\"deepseek\", \"深度求索\")),\n ((\"zhipu\", \"智谱\", \"glm\"), (\"zhipu\", \"智谱\", \"glm\")),\n ((\"doubao\", \"豆包\", \"字节\", \"火山\", \"volcengine\"), (\"doubao\", \"volcengine\", \"字节\", \"火山\")),\n ((\"minimax\", \"海螺\"), (\"minimax\", \"hailuo\", \"海螺\")),\n ((\"零一万物\", \"01ai\", \"01-ai\", \"yi\"), (\"01-ai\", \"yi\", \"零一\")),\n ((\"baichuan\", \"百川\"), (\"baichuan\", \"百川\")),\n ((\"mistral\",), (\"mistral\",)),\n ((\"llama\", \"meta\"), (\"llama\", \"meta\")),\n ((\"grok\", \"xai\", \"x.ai\"), (\"grok\", \"xai\", \"x.ai\")),\n]\n\n\ndef normalize_search_text(value: str) -> str:\n return re.sub(r\"[\\W_]+\", \"\", value.lower(), flags=re.UNICODE)\n\n\ndef query_wants_latest(query: str) -> bool:\n lowered = query.lower()\n return any(term in lowered for term in LATEST_QUERY_TERMS)\n\n\ndef model_search_text(model: dict[str, Any]) -> str:\n keys = (\n \"model_id\",\n \"id\",\n \"model\",\n \"model_name\",\n \"name\",\n \"display_name\",\n \"label\",\n \"title\",\n \"developer\",\n \"provider\",\n \"developer_id\",\n \"provider_id\",\n \"owned_by\",\n \"desc\",\n \"description\",\n \"types\",\n \"type\",\n \"endpoints\",\n \"features\",\n \"capabilities\",\n \"input_modalities\",\n )\n return \" \".join(compact(model.get(key)) for key in keys if model.get(key) is not None)\n\n\ndef add_query_group(\n groups: list[list[str]],\n labels: list[str],\n seen: set[tuple[str, ...]],\n label: str,\n terms: tuple[str, ...] | list[str],\n) -> None:\n cleaned = tuple(dict.fromkeys(term.strip().lower() for term in terms if term.strip()))\n if not cleaned:\n return\n key = tuple(sorted(normalize_search_text(term) for term in cleaned if normalize_search_text(term)))\n if not key or key in seen:\n return\n groups.append(list(cleaned))\n labels.append(f\"{label} -> {', '.join(cleaned)}\" if label else \", \".join(cleaned))\n seen.add(key)\n\n\ndef build_query_plan(query: str) -> dict[str, Any]:\n lowered = query.lower()\n compact_query = normalize_search_text(query)\n groups: list[list[str]] = []\n labels: list[str] = []\n seen: set[tuple[str, ...]] = set()\n matched_alias_triggers: set[str] = set()\n\n for triggers, terms in MODEL_ALIAS_GROUPS:\n matched_trigger = None\n for trigger in triggers:\n trigger_lower = trigger.lower()\n trigger_compact = normalize_search_text(trigger)\n if trigger_lower in lowered or (trigger_compact and trigger_compact in compact_query):\n matched_trigger = trigger\n matched_alias_triggers.update(normalize_search_text(item) for item in triggers)\n break\n if matched_trigger:\n add_query_group(groups, labels, seen, matched_trigger, terms)\n\n for token in re.findall(r\"[a-z0-9]+(?:[._-]?[a-z0-9]+)*\", lowered):\n token_compact = normalize_search_text(token)\n if not token_compact or token in GENERIC_QUERY_TOKENS or token_compact in matched_alias_triggers:\n continue\n add_query_group(groups, labels, seen, token, (token,))\n\n if not groups and query.strip():\n add_query_group(groups, labels, seen, \"raw query\", (query.strip(),))\n\n return {\n \"groups\": groups,\n \"labels\": labels,\n \"wants_latest\": query_wants_latest(query),\n \"compact_query\": compact_query,\n }\n\n\ndef term_match_score(model: dict[str, Any], raw_text: str, compact_text: str, term: str) -> int:\n term_lower = term.lower()\n term_compact = normalize_search_text(term)\n model_id_lower = model_id(model).lower()\n model_id_compact = normalize_search_text(model_id(model))\n\n if term_lower == model_id_lower or (term_compact and term_compact == model_id_compact):\n return 100\n if term_lower and term_lower in raw_text:\n return 25\n if term_compact and term_compact in compact_text:\n return 20\n return 0\n\n\ndef model_query_score(model: dict[str, Any], plan: dict[str, Any]) -> int:\n raw_text = model_search_text(model).lower()\n compact_text = normalize_search_text(raw_text)\n groups = plan[\"groups\"]\n if not groups:\n return 0\n\n score = 0\n for group in groups:\n group_score = max(term_match_score(model, raw_text, compact_text, term) for term in group)\n if group_score == 0:\n return 0\n score += group_score\n\n compact_query = plan[\"compact_query\"]\n model_id_compact = normalize_search_text(model_id(model))\n if compact_query and compact_query == model_id_compact:\n score += 200\n elif compact_query and compact_query in model_id_compact:\n score += 80\n return score\n\n\ndef rank_matching_models(\n models: list[dict[str, Any]],\n query: str,\n limit: int | None = None,\n preserve_source_order: bool = False,\n) -> tuple[list[dict[str, Any]], dict[str, Any]]:\n plan = build_query_plan(query)\n scored = [\n (index, model_query_score(item, plan), item)\n for index, item in enumerate(models)\n ]\n matches = [(index, score, item) for index, score, item in scored if score > 0]\n if preserve_source_order:\n ordered = sorted(matches, key=lambda row: row[0])\n else:\n ordered = sorted(matches, key=lambda row: (-row[1], model_id(row[2]).lower()))\n selected = [item for _, _, item in ordered]\n if limit is not None:\n selected = selected[:limit]\n return selected, plan\n\n\ndef find_model(models: list[dict[str, Any]], target: str) -> dict[str, Any] | None:\n selected, _ = rank_matching_models(models, target, limit=1)\n return selected[0] if selected else None\n\n\ndef print_table(headers: list[str], rows: list[list[str]]) -> None:\n widths = [len(header) for header in headers]\n for row in rows:\n for idx, cell in enumerate(row):\n widths[idx] = max(widths[idx], len(cell))\n print(\" | \".join(header.ljust(widths[idx]) for idx, header in enumerate(headers)))\n print(\"-+-\".join(\"-\" * width for width in widths))\n for row in rows:\n print(\" | \".join(cell.ljust(widths[idx]) for idx, cell in enumerate(row)))\n\n\ndef cmd_list(args: argparse.Namespace) -> None:\n models = extract_models(load_json_source(args.source))\n if args.query:\n models, _ = rank_matching_models(models, args.query)\n if args.json:\n print(json.dumps(models[: args.limit], ensure_ascii=False, indent=2))\n return\n rows = []\n for item in models[: args.limit]:\n rows.append(\n [\n model_id(item),\n model_label(item),\n developer(item),\n context_display(item),\n max_output_display(item),\n types(item),\n ]\n )\n print_table([\"model_id\", \"name\", \"provider\", \"context\", \"max_output\", \"types\"], rows)\n print(f\"\\nsource: {display_models_source()}\")\n\n\ndef cmd_get(args: argparse.Namespace) -> None:\n models = extract_models(load_json_source(args.source))\n item = find_model(models, args.model)\n if not item:\n raise SystemExit(f\"Model not found: {args.model}\")\n if args.json:\n print(json.dumps(item, ensure_ascii=False, indent=2))\n return\n rows = [\n [\"model_id\", model_id(item)],\n [\"name\", model_label(item)],\n [\"provider\", developer(item)],\n [\"context_length\", context_display(item)],\n [\"max_output\", max_output_display(item)],\n [\"pricing\", pricing(item)],\n [\"price_unit\", price_unit()],\n [\"types/endpoints\", types(item)],\n [\"features/modalities\", features(item)],\n ]\n print_table([\"field\", \"value\"], rows)\n print(\"\\nRaw model data is available with --json.\")\n\n\ndef cmd_compare(args: argparse.Namespace) -> None:\n models = extract_models(load_json_source(args.source))\n selected = []\n missing = []\n for target in args.models:\n item = find_model(models, target)\n if item:\n selected.append(item)\n else:\n missing.append(target)\n if args.json:\n print(json.dumps({\"models\": selected, \"missing\": missing}, ensure_ascii=False, indent=2))\n return\n rows = []\n for item in selected:\n rows.append(\n [\n model_id(item),\n developer(item),\n context_display(item),\n max_output_display(item),\n pricing(item),\n features(item),\n ]\n )\n print_table([\"model_id\", \"provider\", \"context\", \"max_output\", \"pricing\", \"features\"], rows)\n if missing:\n print(\"\\nmissing: \" + \", \".join(missing))\n print(f\"\\nPricing units: {price_unit()}. Values are displayed exactly as returned by the models API.\")\n\n\ndef cmd_latest_report(args: argparse.Namespace) -> None:\n payload = load_json_source(args.source)\n models = extract_models_in_source_order(payload)\n selected = models[: args.limit]\n if not selected:\n raise SystemExit(\"No models found in the selected source.\")\n\n source = display_models_source()\n unit = price_unit()\n now = query_time_label()\n\n print(f\"# AIHubMix Latest Models Report\")\n print()\n print(f\"- Data source: `{source}`\")\n print(f\"- Query time: `{now}`\")\n print(\"- Latest definition: first records returned by `/api/v1/models` in source order; no model_id sorting is applied.\")\n print(f\"- Price unit: `{unit}`\")\n print(\"- Price values: displayed exactly as returned in each model's `pricing` object.\")\n print()\n print_user_overview(selected)\n print(\"| # | model_id | name | developer_id | pricing | context_length(tokens) | max_output(tokens) | types/endpoints | features | notes |\")\n print(\"|---:|---|---|---:|---|---:|---:|---|---|---|\")\n for index, item in enumerate(selected, start=1):\n type_endpoint = types(item)\n endpoints = compact(item.get(\"endpoints\"))\n if endpoints != \"-\" and endpoints not in type_endpoint:\n type_endpoint = f\"{type_endpoint}; endpoints={endpoints}\" if type_endpoint != \"-\" else f\"endpoints={endpoints}\"\n print(\n \"| \"\n + \" | \".join(\n [\n str(index),\n f\"`{model_id(item)}`\",\n model_label(item).replace(\"|\", \"\\\\|\"),\n developer(item),\n pricing(item).replace(\"|\", \"\\\\|\"),\n context_display(item),\n max_output_display(item),\n type_endpoint.replace(\"|\", \"\\\\|\"),\n features(item).replace(\"|\", \"\\\\|\"),\n availability_note(item).replace(\"|\", \"\\\\|\"),\n ]\n )\n + \" |\"\n )\n print()\n print_model_switch_hint(selected)\n\n first_model = model_id(selected[0])\n protocol = suggested_protocol(selected[0])\n print()\n if protocol is None:\n print(\"## Example\")\n print()\n print(\"The first selected model is not a plain text/chat model. Choose the endpoint by model type and verify it with `protocols` before generating a call example.\")\n return\n\n print(f\"## Suggested {protocol_name(protocol)} Call Example\")\n print()\n print(f\"- URL: `{base_url()}{endpoint_path(protocol, first_model)}`\")\n print(f\"- Auth: `{protocol_auth_label(protocol)}`\")\n print(\"- Replace `model` with any compatible `model_id` from the table when needed.\")\n print()\n os_name = detect_os(args.os)\n print(f\"### Command Line ({'Windows PowerShell' if os_name == 'windows' else 'macOS/Linux shell'})\")\n print()\n print(example_intro(\"command\", os_name))\n print()\n print(\"```\" + (\"powershell\" if os_name == \"windows\" else \"bash\"))\n print(render_cmd_example(protocol, first_model, os_name))\n print(\"```\")\n print()\n print(\"### JavaScript\")\n print()\n print(example_intro(\"js\"))\n print()\n print_env_setup(\"js\", os_name)\n print(\"```js\")\n print(render_js_example(protocol, first_model))\n print(\"```\")\n\n\ndef cmd_report(args: argparse.Namespace) -> None:\n payload = load_json_source(args.source)\n wants_latest = query_wants_latest(args.query)\n models = extract_models_in_source_order(payload) if wants_latest else extract_models(payload)\n selected, query_plan = rank_matching_models(\n models,\n args.query,\n limit=args.limit,\n preserve_source_order=wants_latest,\n )\n if not selected:\n raise SystemExit(f\"No models matched query: {args.query}\")\n\n source = display_models_source()\n now = query_time_label()\n matched_terms = \"; \".join(query_plan[\"labels\"]) if query_plan[\"labels\"] else \"raw query\"\n\n print(\"# AIHubMix Model Interface Report\")\n print()\n print(f\"- Query: `{args.query}`\")\n print(f\"- Normalized query terms: `{matched_terms}`\")\n if query_plan[\"wants_latest\"]:\n print(\"- Latest intent: `detected`; matched rows keep `/api/v1/models` source order.\")\n print(f\"- Data source: `{source}`\")\n print(f\"- Query time: `{now}`\")\n print(f\"- Matched models shown: `{len(selected)}`\")\n print(f\"- Price unit: `{price_unit()}`\")\n print(\"- Price values: displayed exactly as returned in each model's `pricing` object.\")\n print()\n print_user_overview(selected, args.query)\n print(\"| model_id | name | developer_id | pricing | context | max_output | type | callable entrypoints | raw endpoints | features | modalities | notes |\")\n print(\"|---|---|---:|---|---:|---:|---|---|---|---|---|---|\")\n for item in selected:\n print(\n \"| \"\n + \" | \".join(\n [\n f\"`{model_id(item)}`\",\n model_label(item).replace(\"|\", \"\\\\|\"),\n developer(item),\n pricing(item).replace(\"|\", \"\\\\|\"),\n context_display(item),\n max_output_display(item),\n compact(item.get(\"types\")).replace(\"|\", \"\\\\|\"),\n endpoint_display(item).replace(\"|\", \"\\\\|\"),\n endpoint_note(item).replace(\"|\", \"\\\\|\"),\n compact(item.get(\"features\")).replace(\"|\", \"\\\\|\"),\n compact(item.get(\"input_modalities\")).replace(\"|\", \"\\\\|\"),\n availability_note(item).replace(\"|\", \"\\\\|\"),\n ]\n )\n + \" |\"\n )\n print()\n print_model_switch_hint(selected)\n\n first_model = model_id(selected[0])\n protocol = suggested_protocol(selected[0])\n print()\n if protocol is None:\n print(\"## Example\")\n print()\n print(\"The matched model is not a plain text/chat model. Choose the endpoint by model type and verify it with `protocols` before generating a call example.\")\n return\n\n print(f\"## Suggested {protocol_name(protocol)} Endpoint\")\n print()\n print(\"- Method: `POST`\")\n print(f\"- URL: `{base_url()}{endpoint_path(protocol, first_model)}`\")\n print(f\"- Auth: `{protocol_auth_label(protocol)}`\")\n print()\n print(\"## Example\")\n print()\n os_name = detect_os(args.os)\n print(f\"### Command Line ({'Windows PowerShell' if os_name == 'windows' else 'macOS/Linux shell'})\")\n print()\n print(example_intro(\"command\", os_name))\n print()\n print(\"```\" + (\"powershell\" if os_name == \"windows\" else \"bash\"))\n print(render_cmd_example(protocol, first_model, os_name))\n print(\"```\")\n print()\n print(\"### JavaScript\")\n print()\n print(example_intro(\"js\"))\n print()\n print_env_setup(\"js\", os_name)\n print(\"```js\")\n print(render_js_example(protocol, first_model))\n print(\"```\")\n\n\ndef load_openapi() -> dict[str, Any] | None:\n \"\"\"Fetch the live OpenAPI spec from the remote endpoint (no local checkout).\"\"\"\n url = os.environ.get(\"AIHUBMIX_OPENAPI_URL\") or OPENAPI_JSON_URL\n try:\n return json.loads(read_url_bytes(url).decode(\"utf-8\", errors=\"replace\"))\n except ValueError:\n return None\n\n\ndef _error_statuses_from_spec(spec: dict[str, Any]) -> dict[str, dict[str, str]]:\n \"\"\"Collect declared 4XX/5XX responses (code -> description/schema) from the spec.\"\"\"\n statuses: dict[str, dict[str, str]] = {}\n for _method, _path, operation in iter_operations(spec):\n for code, response in (operation.get(\"responses\") or {}).items():\n code = str(code)\n if code in statuses or not re.match(r\"^[45]\\d\\d$|^[45]XX$\", code, re.IGNORECASE):\n continue\n if not isinstance(response, dict):\n continue\n schema_ref = \"#/components/schemas/GatewayError\"\n for media in (response.get(\"content\") or {}).values():\n ref = ((media or {}).get(\"schema\") or {}).get(\"$ref\")\n if ref:\n schema_ref = ref\n break\n statuses[code] = {\n \"description\": str(response.get(\"description\", \"\")).strip(),\n \"schema\": schema_ref,\n }\n return statuses\n\n\ndef load_error_contract() -> dict[str, Any]:\n \"\"\"Derive the GatewayError contract from the live OpenAPI spec (no local checkout).\"\"\"\n spec = load_openapi()\n if not spec:\n raise SystemExit(\n f\"Could not load the OpenAPI spec from {OPENAPI_JSON_URL} to derive the error contract.\"\n )\n return {\n \"source\": display_error_contract_source(),\n \"schema_name\": \"GatewayError\",\n \"envelope\": {\n \"top_level\": \"error\",\n \"required\": [\"error.message\"],\n \"fields\": {\n \"error.message\": \"Human-readable error message.\",\n \"error.type\": \"OpenAI-compatible category; upstream value may pass through and enum is not fixed.\",\n \"error.param\": \"Offending field name, when applicable.\",\n \"error.code\": \"Sub-error code; may be string or integer.\",\n },\n },\n \"request_id\": \"Use the X-Request-Id response header. It is not in the JSON body.\",\n \"legacy_upstream\": \"The legacy upstream field is no longer emitted.\",\n \"statuses\": _error_statuses_from_spec(spec),\n }\n\n\ndef iter_operations(spec: dict[str, Any]) -> list[tuple[str, str, dict[str, Any]]]:\n ops = []\n for path, path_item in (spec.get(\"paths\") or {}).items():\n if not isinstance(path_item, dict):\n continue\n for method, operation in path_item.items():\n if method.lower() not in {\"get\", \"post\", \"put\", \"patch\", \"delete\"}:\n continue\n if isinstance(operation, dict):\n ops.append((method.upper(), path, operation))\n return ops\n\n\ndef cmd_protocols(args: argparse.Namespace) -> None:\n spec = load_openapi()\n if not spec:\n raise SystemExit(f\"Could not load the OpenAPI spec from {OPENAPI_JSON_URL}.\")\n operations = iter_operations(spec)\n if args.json:\n print(json.dumps({\"source\": display_openapi_source(), \"operations\": operations}, ensure_ascii=False, indent=2))\n return\n rows = []\n for method, endpoint, operation in operations:\n security = operation.get(\"security\") or spec.get(\"security\") or []\n rows.append(\n [\n method,\n endpoint,\n operation.get(\"operationId\", \"-\"),\n compact(security),\n \"yes\" if operation.get(\"requestBody\") else \"no\",\n ]\n )\n print_table([\"method\", \"path\", \"operation_id\", \"security\", \"request_body\"], rows)\n print(f\"\\nsource: {display_openapi_source()}\")\n\n\ndef cmd_error_contract(args: argparse.Namespace) -> None:\n contract = load_error_contract()\n if args.json:\n print(json.dumps(contract, ensure_ascii=False, indent=2))\n return\n\n print(\"# AIHubMix Gateway Error Contract\")\n print()\n print(f\"- Source: `{display_error_contract_source()}`\")\n print(f\"- Schema: `{contract['schema_name']}`\")\n print(\"- Body shape: top-level `error` object.\")\n print(\"- Required body field: `error.message`.\")\n print(f\"- Request ID: `{contract['request_id']}`\")\n print(f\"- Legacy upstream field: `{contract['legacy_upstream']}`\")\n print()\n print(\"## GatewayError Fields\")\n print()\n rows = [[field, description] for field, description in contract[\"envelope\"][\"fields\"].items()]\n print_table([\"field\", \"meaning\"], rows)\n print()\n print(\"## Status Responses\")\n print()\n status_order = [\"400\", \"401\", \"403\", \"404\", \"429\", \"5XX\"]\n rows = []\n statuses = contract[\"statuses\"]\n for status in status_order:\n item = statuses.get(status, {})\n rows.append([status, item.get(\"description\", \"-\"), item.get(\"schema\", \"#/components/schemas/GatewayError\")])\n print_table([\"status\", \"description\", \"schema\"], rows)\n print()\n print(\"## Troubleshooting Rule\")\n print()\n print(\"Use the live HTTP status, JSON error body, and `X-Request-Id` as the observed evidence.\")\n print(\"Use this contract to verify the error envelope and status category; do not assume `error.type` is a fixed enum.\")\n\n\ndef load_package_json(path: Path) -> dict[str, Any] | None:\n if not path.exists():\n return None\n with path.open(\"r\", encoding=\"utf-8\") as handle:\n data = json.load(handle)\n return data if isinstance(data, dict) else None\n\n\ndef find_installed_sdk_package(start: str | None = None) -> tuple[dict[str, Any] | None, Path | None]:\n explicit = os.environ.get(\"AIHUBMIX_SDK_PACKAGE_JSON\")\n candidates: list[Path] = []\n if explicit:\n candidates.append(Path(explicit).expanduser())\n cwd = Path(start).expanduser() if start else Path.cwd()\n for path in [cwd, *cwd.parents]:\n candidates.append(path / \"node_modules\" / \"@aihubmix\" / \"ai-sdk-provider\" / \"package.json\")\n seen: set[str] = set()\n for candidate in candidates:\n try:\n resolved = candidate.resolve()\n except OSError:\n resolved = candidate\n key = str(resolved).lower()\n if key in seen:\n continue\n seen.add(key)\n data = load_package_json(candidate)\n if data:\n return data, candidate\n return None, None\n\n\ndef fetch_npm_package_metadata() -> dict[str, Any] | None:\n request = urllib.request.Request(\n SDK_REGISTRY_URL,\n headers={\n \"Accept\": \"application/json\",\n \"User-Agent\": \"aihubmixApi-skill/0.1\",\n },\n )\n try:\n with urllib.request.urlopen(request, timeout=20) as response:\n payload = json.load(response)\n except urllib.error.HTTPError as exc:\n detail = exc.read().decode(\"utf-8\", errors=\"replace\")\n raise SystemExit(f\"HTTP {exc.code} while checking npm registry: {detail}\") from exc\n except urllib.error.URLError as exc:\n raise SystemExit(f\"Could not check npm registry: {exc.reason}\") from exc\n return payload if isinstance(payload, dict) else None\n\n\ndef read_url_bytes(url: str) -> bytes:\n request = urllib.request.Request(\n url,\n headers={\"User-Agent\": \"aihubmixApi-skill/0.1\"},\n )\n try:\n with urllib.request.urlopen(request, timeout=30) as response:\n return response.read()\n except urllib.error.HTTPError as exc:\n detail = exc.read().decode(\"utf-8\", errors=\"replace\")\n raise SystemExit(f\"HTTP {exc.code} while fetching {url}: {detail}\") from exc\n except urllib.error.URLError as exc:\n raise SystemExit(f\"Could not fetch {url}: {exc.reason}\") from exc\n\n\ndef parse_sdk_package_texts(package_json: dict[str, Any], dts_text: str, js_text: str, source: str) -> dict[str, Any]:\n method_candidates = [\n \"languageModel\",\n \"chat\",\n \"responses\",\n \"completion\",\n \"embedding\",\n \"embeddingModel\",\n \"image\",\n \"imageModel\",\n \"textEmbedding\",\n \"textEmbeddingModel\",\n \"transcription\",\n \"transcriptionModel\",\n \"speech\",\n \"speechModel\",\n ]\n tool_candidates = [\"codeInterpreter\", \"fileSearch\", \"imageGeneration\", \"webSearchPreview\", \"webSearch\"]\n export_candidates = [\"aihubmix\", \"createAihubmix\"]\n provider_methods = [name for name in method_candidates if re.search(rf\"\\b{name}\\b\", dts_text)]\n tools = [name for name in tool_candidates if re.search(rf\"\\b{name}\\b\", dts_text)]\n exports = [name for name in export_candidates if re.search(rf\"\\b{name}\\b\", dts_text)]\n env_vars = sorted(set(re.findall(r'\"(AIHUBMIX_[A-Z0-9_]+)\"', js_text + dts_text)))\n base_urls = sorted(set(re.findall(r'\"(https://aihubmix\\.com[^\"]*)\"', js_text + dts_text)))\n header_candidates = [\"Authorization\", \"APP-Code\", \"Content-Type\", \"x-api-key\", \"x-goog-api-key\"]\n headers = [name for name in header_candidates if name in js_text]\n routing = []\n if 'startsWith(\"claude-\")' in js_text:\n routing.append(\"Claude model IDs starting with claude- use Anthropic-compatible messages with x-api-key.\")\n if 'startsWith(\"gemini\")' in js_text or 'startsWith(\"imagen\")' in js_text:\n routing.append(\"Gemini or Imagen model IDs use Gemini native base URL unless suffixed with -nothink or -search.\")\n if \"OpenAICompatibleChatLanguageModel\" in js_text:\n routing.append(\"Other chat models use OpenAI-compatible chat through https://aihubmix.com/v1.\")\n\n return {\n \"source\": source,\n \"package\": str(package_json.get(\"name\") or SDK_PACKAGE_NAME),\n \"version\": str(package_json.get(\"version\") or \"-\"),\n \"exports\": exports,\n \"provider_methods\": provider_methods,\n \"tools\": tools,\n \"environment_variable\": env_vars[0] if env_vars else \"-\",\n \"base_urls\": base_urls,\n \"headers\": headers,\n \"routing\": routing,\n }\n\n\ndef load_sdk_info_from_directory(package_dir: Path, source_label: str) -> dict[str, Any]:\n package_json = load_package_json(package_dir / \"package.json\") or {}\n dts_text = (package_dir / \"dist\" / \"index.d.ts\").read_text(encoding=\"utf-8\")\n js_text = (package_dir / \"dist\" / \"index.js\").read_text(encoding=\"utf-8\")\n return parse_sdk_package_texts(package_json, dts_text, js_text, source_label)\n\n\ndef load_sdk_info_from_tarball(tarball: bytes, source_label: str) -> dict[str, Any]:\n with tarfile.open(fileobj=io.BytesIO(tarball), mode=\"r:gz\") as archive:\n files: dict[str, bytes] = {}\n for name in (\"package/package.json\", \"package/dist/index.d.ts\", \"package/dist/index.js\"):\n member = archive.getmember(name)\n extracted = archive.extractfile(member)\n if not extracted:\n raise SystemExit(f\"Could not read {name} from SDK tarball.\")\n files[name] = extracted.read()\n package_json = json.loads(files[\"package/package.json\"].decode(\"utf-8\"))\n dts_text = files[\"package/dist/index.d.ts\"].decode(\"utf-8\")\n js_text = files[\"package/dist/index.js\"].decode(\"utf-8\")\n return parse_sdk_package_texts(package_json, dts_text, js_text, source_label)\n\n\ndef load_sdk_info(args: argparse.Namespace) -> dict[str, Any]:\n if args.package_dir:\n return load_sdk_info_from_directory(Path(args.package_dir).expanduser(), str(Path(args.package_dir).expanduser()))\n\n installed, package_json_path = find_installed_sdk_package(args.project)\n if package_json_path and args.source in {\"auto\", \"installed\"}:\n return load_sdk_info_from_directory(package_json_path.parent, str(package_json_path.parent))\n if args.source == \"installed\":\n raise SystemExit(\"SDK package is not installed in the selected project path.\")\n\n metadata = fetch_npm_package_metadata()\n target_version = args.version or SDK_RECOMMENDED_VERSION\n versions = metadata.get(\"versions\") or {}\n package_meta = versions.get(target_version)\n if not isinstance(package_meta, dict):\n raise SystemExit(f\"Could not find {SDK_PACKAGE_NAME}@{target_version} in npm registry metadata.\")\n tarball_url = ((package_meta.get(\"dist\") or {}).get(\"tarball\"))\n if not tarball_url:\n raise SystemExit(f\"No tarball URL found for {SDK_PACKAGE_NAME}@{target_version}.\")\n return load_sdk_info_from_tarball(read_url_bytes(str(tarball_url)), f\"npm tarball {SDK_PACKAGE_NAME}@{target_version}\")\n\n\ndef cmd_sdk_info(args: argparse.Namespace) -> None:\n info = load_sdk_info(args)\n if args.json:\n print(json.dumps(info, ensure_ascii=False, indent=2))\n return\n\n print(\"# AIHubMix SDK Provider Info\")\n print()\n print(f\"- Package: `{info['package']}`\")\n print(f\"- Version: `{info['version']}`\")\n print(f\"- Source: `{info['source']}`\")\n print(\"- Data role: SDK/provider usage source; not model price, context, or availability source.\")\n print()\n print(\"## Provider Surface\")\n print()\n rows = []\n for name in info[\"provider_methods\"]:\n if name in {\"chat\", \"languageModel\"}:\n purpose = \"text/chat generation\"\n elif name == \"responses\":\n purpose = \"OpenAI Responses API style model\"\n elif \"embedding\" in name.lower():\n purpose = \"text embeddings\"\n elif name.startswith(\"image\"):\n purpose = \"image generation\"\n elif \"transcription\" in name.lower():\n purpose = \"audio transcription\"\n elif \"speech\" in name.lower():\n purpose = \"speech synthesis\"\n elif name == \"completion\":\n purpose = \"legacy text completion\"\n else:\n purpose = \"provider method\"\n rows.append([name, purpose])\n print_table([\"method\", \"purpose\"], rows)\n print()\n print(\"## Tools\")\n print()\n print(\", \".join(info[\"tools\"]) if info[\"tools\"] else \"-\")\n print()\n print(\"## Auth And Routing\")\n print()\n print_table(\n [\"field\", \"value\"],\n [\n [\"api_key_env\", info[\"environment_variable\"]],\n [\"base_urls\", compact(info[\"base_urls\"])],\n [\"headers\", compact(info[\"headers\"])],\n [\"exports\", compact(info[\"exports\"])],\n ],\n )\n if info[\"routing\"]:\n print()\n print(\"## Routing Notes\")\n for index, item in enumerate(info[\"routing\"], start=1):\n print(f\"{index}. {item}\")\n print()\n print(\"## Boundary\")\n print(\"Use `/api/v1/models` for live model price/context/availability. Use this SDK info for JavaScript provider usage.\")\n\n\ndef version_tuple(version: str) -> tuple[int, ...]:\n parts: list[int] = []\n for part in version.split(\".\"):\n digits = \"\"\n for char in part:\n if char.isdigit():\n digits += char\n else:\n break\n parts.append(int(digits or 0))\n return tuple(parts)\n\n\ndef cmd_sdk_check(args: argparse.Namespace) -> None:\n installed, installed_path = find_installed_sdk_package(args.project)\n installed_version = str(installed.get(\"version\")) if installed else \"-\"\n latest_version = \"-\"\n registry_source = SDK_REGISTRY_URL\n metadata = None\n if not args.offline:\n metadata = fetch_npm_package_metadata()\n latest_version = str((metadata.get(\"dist-tags\") or {}).get(\"latest\") or \"-\")\n target_version = args.version or SDK_RECOMMENDED_VERSION\n\n print(\"# AIHubMix SDK Check\")\n print()\n print(f\"- Package: `{SDK_PACKAGE_NAME}`\")\n print(f\"- Recommended version for this skill: `{SDK_RECOMMENDED_VERSION}`\")\n print(f\"- Target version: `{target_version}`\")\n print(f\"- Installed version: `{installed_version}`\")\n print(f\"- Installed package.json: `{installed_path or '-'}`\")\n print(f\"- Registry latest: `{latest_version}`\")\n print(f\"- Registry source: `{registry_source if not args.offline else 'not checked (--offline)'}`\")\n print(\"- Auto update: `not performed`\")\n\n status = []\n if installed_version == \"-\":\n status.append(\"SDK is not installed in the selected project path.\")\n elif installed_version == target_version:\n status.append(\"Installed SDK matches the target version.\")\n else:\n status.append(\"Installed SDK differs from the target version.\")\n\n if latest_version != \"-\":\n if version_tuple(latest_version) > version_tuple(target_version):\n status.append(\"A newer npm version is available; review changelog before updating.\")\n elif latest_version == target_version:\n status.append(\"Target version is the npm latest version.\")\n else:\n status.append(\"Target version is newer than registry latest; verify the package tag.\")\n else:\n status.append(\"Latest npm version was not checked.\")\n\n print()\n print(\"## Status\")\n for item in status:\n print(f\"- {item}\")\n\n print()\n print(\"## Manual Commands\")\n print()\n print(\"```bash\")\n print(f\"npm view {SDK_PACKAGE_NAME} version\")\n print(f\"npm install {SDK_PACKAGE_NAME}@{target_version} ai\")\n print(\"```\")\n print()\n print(\"Only run install/update commands after the user confirms the target project and version.\")\n\n\ndef base_url() -> str:\n return os.environ.get(\"AIHUBMIX_BASE_URL\", DEFAULT_SERVER_URL).rstrip(\"/\")\n\n\ndef normalize_protocol(protocol: str) -> str:\n normalized = PROTOCOL_ALIASES.get(protocol, protocol)\n if normalized not in PROTOCOL_SPECS:\n raise ValueError(normalized)\n return normalized\n\n\ndef endpoint_path(protocol: str, model: str) -> str:\n protocol = normalize_protocol(protocol)\n path = PROTOCOL_SPECS[protocol][\"path\"]\n return path.replace(\"{model}\", model).replace(\"{video_id}\", \"video_123\")\n\n\ndef example_payload(protocol: str, model: str) -> dict[str, Any]:\n protocol = normalize_protocol(protocol)\n if protocol == \"chat\":\n return {\n \"model\": model,\n \"messages\": [{\"role\": \"user\", \"content\": \"Say hello in one short sentence.\"}],\n }\n if protocol == \"responses\":\n return {\"model\": model, \"input\": \"Say hello in one short sentence.\"}\n if protocol == \"messages\":\n return {\n \"model\": model,\n \"max_tokens\": 128,\n \"messages\": [{\"role\": \"user\", \"content\": \"Say hello in one short sentence.\"}],\n }\n if protocol == \"gemini\":\n return {\"contents\": [{\"role\": \"user\", \"parts\": [{\"text\": \"Say hello in one short sentence.\"}]}]}\n if protocol == \"completions\":\n return {\"model\": model, \"prompt\": \"Say hello in one short sentence.\", \"max_tokens\": 64}\n if protocol == \"embeddings\":\n return {\"model\": model, \"input\": \"Text to embed with AIHubMix.\"}\n if protocol == \"images\":\n return {\"model\": model, \"prompt\": \"A clean product mockup on a white background.\", \"size\": \"1024x1024\"}\n if protocol == \"videos\":\n return {\"model\": model, \"prompt\": \"A short cinematic shot of a city at sunrise.\"}\n if protocol == \"video-remix\":\n return {\"model\": model, \"prompt\": \"Make the video brighter and more cinematic.\"}\n if protocol == \"audio-speech\":\n return {\"model\": model, \"input\": \"Hello from AIHubMix.\", \"voice\": \"alloy\", \"response_format\": \"mp3\"}\n if protocol in {\"audio-transcriptions\", \"audio-translations\"}:\n return {\"model\": model, \"file\": \"@/path/to/audio.mp3\"}\n if protocol == \"moderations\":\n return {\"model\": model, \"input\": \"I want to check whether this text is safe.\"}\n if protocol in {\"video-retrieve\", \"video-delete\", \"video-content\"}:\n return {}\n raise ValueError(protocol)\n\n\ndef auth_header(protocol: str) -> tuple[str, str]:\n protocol = normalize_protocol(protocol)\n family = PROTOCOL_SPECS[protocol][\"family\"]\n if family == \"anthropic\":\n return \"x-api-key\", \"$AIHUBMIX_API_KEY\"\n if family == \"gemini\":\n return \"x-goog-api-key\", \"$AIHUBMIX_API_KEY\"\n return \"Authorization\", \"Bearer $AIHUBMIX_API_KEY\"\n\n\ndef detect_os(requested: str) -> str:\n if requested != \"auto\":\n return requested\n system = platform.system().lower()\n if system.startswith(\"win\"):\n return \"windows\"\n if system == \"darwin\":\n return \"mac\"\n return \"linux\"\n\n\ndef powershell_literal(value: str) -> str:\n return \"'\" + value.replace(\"'\", \"''\") + \"'\"\n\n\ndef powershell_response_printer() -> str:\n return (\n \"if ($response.choices -and $response.choices.Count -gt 0) {\\n\"\n \" [Console]::WriteLine($response.choices[0].message.content)\\n\"\n \"} elseif ($response.output_text) {\\n\"\n \" [Console]::WriteLine($response.output_text)\\n\"\n \"} elseif ($response.content -and $response.content.Count -gt 0) {\\n\"\n \" [Console]::WriteLine($response.content[0].text)\\n\"\n \"} elseif ($response.candidates -and $response.candidates.Count -gt 0) {\\n\"\n \" [Console]::WriteLine($response.candidates[0].content.parts[0].text)\\n\"\n \"} else {\\n\"\n \" $response | ConvertTo-Json -Depth 20\\n\"\n \"}\\n\"\n )\n\n\ndef powershell_response_printer_for(protocol: str) -> str:\n protocol = normalize_protocol(protocol)\n if protocol == \"embeddings\":\n return (\n \"if ($response.data -and $response.data.Count -gt 0) {\\n\"\n \" [Console]::WriteLine(\\\"embedding dimensions: \\\" + $response.data[0].embedding.Count)\\n\"\n \"} else {\\n\"\n \" $response | ConvertTo-Json -Depth 20\\n\"\n \"}\\n\"\n )\n if protocol == \"images\":\n return (\n \"if ($response.data -and $response.data.Count -gt 0) {\\n\"\n \" $item = $response.data[0]\\n\"\n \" if ($item.url) { [Console]::WriteLine($item.url) } elseif ($item.b64_json) { [Console]::WriteLine($item.b64_json) } else { $item | ConvertTo-Json -Depth 20 }\\n\"\n \"} else {\\n\"\n \" $response | ConvertTo-Json -Depth 20\\n\"\n \"}\\n\"\n )\n if protocol in {\"videos\", \"video-remix\", \"video-retrieve\"}:\n return (\n \"if ($response.id) {\\n\"\n \" [Console]::WriteLine(\\\"video id: \\\" + $response.id)\\n\"\n \"} else {\\n\"\n \" $response | ConvertTo-Json -Depth 20\\n\"\n \"}\\n\"\n )\n if protocol in {\"audio-transcriptions\", \"audio-translations\"}:\n return (\n \"if ($response.text) {\\n\"\n \" [Console]::WriteLine($response.text)\\n\"\n \"} else {\\n\"\n \" $response | ConvertTo-Json -Depth 20\\n\"\n \"}\\n\"\n )\n if protocol == \"moderations\":\n return (\n \"if ($response.results -and $response.results.Count -gt 0) {\\n\"\n \" $response.results[0] | ConvertTo-Json -Depth 20\\n\"\n \"} else {\\n\"\n \" $response | ConvertTo-Json -Depth 20\\n\"\n \"}\\n\"\n )\n return powershell_response_printer()\n\n\ndef shell_response_parser() -> str:\n return (\n \"import json,sys; d=json.load(sys.stdin); \"\n \"text=(d.get('output_text') or ((d.get('choices') or [{}])[0].get('message') or {}).get('content') \"\n \"or ((d.get('content') or [{}])[0]).get('text') \"\n \"or ((((d.get('candidates') or [{}])[0].get('content') or {}).get('parts') or [{}])[0]).get('text')); \"\n \"print(text if text is not None else json.dumps(d, ensure_ascii=False))\"\n )\n\n\ndef shell_response_parser_for(protocol: str) -> str:\n protocol = normalize_protocol(protocol)\n if protocol == \"embeddings\":\n return (\n \"import json,sys; d=json.load(sys.stdin); data=d.get('data') or []; \"\n \"print('embedding dimensions: ' + str(len((data[0] if data else {}).get('embedding') or [])))\"\n )\n if protocol == \"images\":\n return (\n \"import json,sys; d=json.load(sys.stdin); item=(d.get('data') or [{}])[0]; \"\n \"print(item.get('url') or item.get('b64_json') or json.dumps(d, ensure_ascii=False))\"\n )\n if protocol in {\"videos\", \"video-remix\", \"video-retrieve\"}:\n return (\n \"import json,sys; d=json.load(sys.stdin); \"\n \"print(('video id: ' + str(d.get('id'))) if d.get('id') else json.dumps(d, ensure_ascii=False))\"\n )\n if protocol in {\"audio-transcriptions\", \"audio-translations\"}:\n return \"import json,sys; d=json.load(sys.stdin); print(d.get('text') or json.dumps(d, ensure_ascii=False))\"\n if protocol == \"moderations\":\n return (\n \"import json,sys; d=json.load(sys.stdin); \"\n \"print(json.dumps((d.get('results') or [d])[0], ensure_ascii=False))\"\n )\n return shell_response_parser()\n\n\ndef render_cmd_example(protocol: str, model: str, os_name: str) -> str:\n protocol = normalize_protocol(protocol)\n url = base_url() + endpoint_path(protocol, model)\n payload = example_payload(protocol, model)\n spec = PROTOCOL_SPECS[protocol]\n method = spec[\"method\"]\n is_get_or_delete = method in {\"GET\", \"DELETE\"}\n is_multipart = bool(spec.get(\"multipart\"))\n is_binary = bool(spec.get(\"binary_response\") or protocol == \"video-content\")\n needs_video_id = bool(spec.get(\"needs_video_id\"))\n body = json.dumps(payload, ensure_ascii=False, indent=2)\n header_name, _ = auth_header(protocol)\n\n if os_name == \"windows\":\n if spec[\"family\"] == \"anthropic\":\n auth_line = '\"x-api-key\" = $AIHUBMIX_API_KEY'\n elif spec[\"family\"] == \"gemini\":\n auth_line = '\"x-goog-api-key\" = $AIHUBMIX_API_KEY'\n else:\n auth_line = '\"Authorization\" = \"Bearer $AIHUBMIX_API_KEY\"'\n if is_get_or_delete:\n if is_binary:\n return (\n \"$OutputEncoding = [System.Text.Encoding]::UTF8\\n\"\n \"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8\\n\"\n \"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12\\n\"\n \"$AIHUBMIX_API_KEY = Read-Host \\\"Paste AIHubMix API Key\\\"\\n\"\n \"$videoId = Read-Host \\\"Paste video id\\\"\\n\"\n f\"$url = {powershell_literal(url)}.Replace('video_123', $videoId)\\n\\n\"\n \"Invoke-WebRequest `\\n\"\n \" -Uri $url `\\n\"\n \" -Method Get `\\n\"\n f\" -Headers @{{ {auth_line} }} `\\n\"\n \" -OutFile 'aihubmix-video.mp4'\\n\\n\"\n \"[Console]::WriteLine('Saved to aihubmix-video.mp4')\"\n )\n return (\n \"$OutputEncoding = [System.Text.Encoding]::UTF8\\n\"\n \"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8\\n\"\n \"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12\\n\"\n \"$AIHUBMIX_API_KEY = Read-Host \\\"Paste AIHubMix API Key\\\"\\n\"\n \"$videoId = Read-Host \\\"Paste video id\\\"\\n\"\n f\"$url = {powershell_literal(url)}.Replace('video_123', $videoId)\\n\\n\"\n \"$response = Invoke-RestMethod `\\n\"\n \" -Uri $url `\\n\"\n f\" -Method {method.title()} `\\n\"\n f\" -Headers @{{ {auth_line}; \\\"Accept\\\" = \\\"application/json\\\" }}\\n\\n\"\n f\"{powershell_response_printer_for(protocol)}\"\n )\n if is_multipart:\n return (\n \"$OutputEncoding = [System.Text.Encoding]::UTF8\\n\"\n \"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8\\n\"\n \"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12\\n\"\n \"$AIHUBMIX_API_KEY = Read-Host \\\"Paste AIHubMix API Key\\\"\\n\"\n \"$audioPath = Read-Host \\\"Paste local audio file path\\\"\\n\\n\"\n \"$form = @{\\n\"\n f\" model = {powershell_literal(model)}\\n\"\n \" file = Get-Item -LiteralPath $audioPath\\n\"\n \"}\\n\\n\"\n \"$response = Invoke-RestMethod `\\n\"\n f\" -Uri {powershell_literal(url)} `\\n\"\n \" -Method Post `\\n\"\n f\" -Headers @{{ {auth_line}; \\\"Accept\\\" = \\\"application/json\\\" }} `\\n\"\n \" -Form $form\\n\\n\"\n f\"{powershell_response_printer_for(protocol)}\"\n )\n if needs_video_id:\n return (\n \"$OutputEncoding = [System.Text.Encoding]::UTF8\\n\"\n \"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8\\n\"\n \"[Console]::InputEncoding = [System.Text.Encoding]::UTF8\\n\"\n \"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12\\n\"\n \"$AIHUBMIX_API_KEY = Read-Host \\\"Paste AIHubMix API Key\\\"\\n\"\n \"$videoId = Read-Host \\\"Paste video id\\\"\\n\"\n f\"$url = {powershell_literal(url)}.Replace('video_123', $videoId)\\n\\n\"\n \"$body = @'\\n\"\n f\"{body}\\n\"\n \"'@\\n\\n\"\n \"$response = Invoke-RestMethod `\\n\"\n \" -Uri $url `\\n\"\n f\" -Method {method.title()} `\\n\"\n f\" -Headers @{{ {auth_line}; \\\"Accept\\\" = \\\"application/json\\\" }} `\\n\"\n \" -ContentType \\\"application/json\\\" `\\n\"\n \" -Body $body\\n\\n\"\n f\"{powershell_response_printer_for(protocol)}\\n\"\n \"# To inspect the full response, uncomment the next line:\\n\"\n \"# $response | ConvertTo-Json -Depth 20\"\n )\n if is_binary:\n return (\n \"$OutputEncoding = [System.Text.Encoding]::UTF8\\n\"\n \"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8\\n\"\n \"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12\\n\"\n \"$AIHUBMIX_API_KEY = Read-Host \\\"Paste AIHubMix API Key\\\"\\n\\n\"\n \"$body = @'\\n\"\n f\"{body}\\n\"\n \"'@\\n\\n\"\n \"Invoke-WebRequest `\\n\"\n f\" -Uri {powershell_literal(url)} `\\n\"\n \" -Method Post `\\n\"\n f\" -Headers @{{ {auth_line} }} `\\n\"\n \" -ContentType \\\"application/json\\\" `\\n\"\n \" -Body $body `\\n\"\n \" -OutFile 'aihubmix-output.mp3'\\n\\n\"\n \"[Console]::WriteLine('Saved to aihubmix-output.mp3')\"\n )\n return (\n \"$OutputEncoding = [System.Text.Encoding]::UTF8\\n\"\n \"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8\\n\"\n \"[Console]::InputEncoding = [System.Text.Encoding]::UTF8\\n\"\n \"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12\\n\"\n \"$AIHUBMIX_API_KEY = Read-Host \\\"Paste AIHubMix API Key\\\"\\n\\n\"\n \"$body = @'\\n\"\n f\"{body}\\n\"\n \"'@\\n\\n\"\n \"$response = Invoke-RestMethod `\\n\"\n f\" -Uri {powershell_literal(url)} `\\n\"\n \" -Method Post `\\n\"\n f\" -Headers @{{ {auth_line}; \\\"Accept\\\" = \\\"application/json\\\" }} `\\n\"\n \" -ContentType \\\"application/json\\\" `\\n\"\n \" -Body $body\\n\\n\"\n f\"{powershell_response_printer_for(protocol)}\\n\"\n \"# To inspect the full response, uncomment the next line:\\n\"\n \"# $response | ConvertTo-Json -Depth 20\"\n )\n\n if spec[\"family\"] == \"anthropic\":\n auth_arg = '-H \"x-api-key: $AIHUBMIX_API_KEY\"'\n elif spec[\"family\"] == \"gemini\":\n auth_arg = '-H \"x-goog-api-key: $AIHUBMIX_API_KEY\"'\n else:\n auth_arg = '-H \"Authorization: Bearer $AIHUBMIX_API_KEY\"'\n if is_get_or_delete:\n if is_binary:\n return textwrap.dedent(\n f\"\"\"\\\n read -rsp \"Paste AIHubMix API Key: \" AIHUBMIX_API_KEY; echo\n read -rp \"Paste video id: \" video_id\n\n curl -L -s {json.dumps(url)} \\\\\n {auth_arg} \\\\\n -o aihubmix-video.mp4\n\n echo \"Saved to aihubmix-video.mp4\"\n \"\"\"\n ).strip().replace(\"video_123\", \"${video_id}\")\n return textwrap.dedent(\n f\"\"\"\\\n read -rsp \"Paste AIHubMix API Key: \" AIHUBMIX_API_KEY; echo\n read -rp \"Paste video id: \" video_id\n\n curl -s -X {method} {json.dumps(url)} \\\\\n {auth_arg} \\\\\n -H \"Accept: application/json\" \\\\\n | python3 -c {json.dumps(shell_response_parser_for(protocol))}\n \"\"\"\n ).strip().replace(\"video_123\", \"${video_id}\")\n if is_multipart:\n return textwrap.dedent(\n f\"\"\"\\\n read -rsp \"Paste AIHubMix API Key: \" AIHUBMIX_API_KEY; echo\n read -rp \"Paste local audio file path: \" audio_path\n\n curl -s {json.dumps(url)} \\\\\n {auth_arg} \\\\\n -H \"Accept: application/json\" \\\\\n -F model={json.dumps(model)} \\\\\n -F file=@\"$audio_path\" \\\\\n | python3 -c {json.dumps(shell_response_parser_for(protocol))}\n \"\"\"\n ).strip()\n if needs_video_id:\n compact_payload_video = json.dumps(payload, ensure_ascii=False)\n return textwrap.dedent(\n f\"\"\"\\\n read -rsp \"Paste AIHubMix API Key: \" AIHUBMIX_API_KEY; echo\n read -rp \"Paste video id: \" video_id\n\n curl -s -X {method} {json.dumps(url)} \\\\\n {auth_arg} \\\\\n -H \"Content-Type: application/json\" \\\\\n -d {json.dumps(compact_payload_video)} \\\\\n | python3 -c {json.dumps(shell_response_parser_for(protocol))}\n \"\"\"\n ).strip().replace(\"video_123\", \"${video_id}\")\n if is_binary:\n compact_payload_binary = json.dumps(payload, ensure_ascii=False)\n return textwrap.dedent(\n f\"\"\"\\\n read -rsp \"Paste AIHubMix API Key: \" AIHUBMIX_API_KEY; echo\n\n curl -L -s {json.dumps(url)} \\\\\n {auth_arg} \\\\\n -H \"Content-Type: application/json\" \\\\\n -d {json.dumps(compact_payload_binary)} \\\\\n -o aihubmix-output.mp3\n\n echo \"Saved to aihubmix-output.mp3\"\n \"\"\"\n ).strip()\n compact_payload = json.dumps(payload, ensure_ascii=False)\n return textwrap.dedent(\n f\"\"\"\\\n read -rsp \"Paste AIHubMix API Key: \" AIHUBMIX_API_KEY; echo\n\n curl -s {json.dumps(url)} \\\\\n {auth_arg} \\\\\n -H \"Content-Type: application/json\" \\\\\n -d {json.dumps(compact_payload)} \\\\\n | python3 -c {json.dumps(shell_response_parser_for(protocol))}\n \"\"\"\n ).strip()\n\n\ndef render_js_example(protocol: str, model: str) -> str:\n protocol = normalize_protocol(protocol)\n if protocol == \"chat\":\n return textwrap.dedent(\n f\"\"\"\\\n import {{ aihubmix }} from \"@aihubmix/ai-sdk-provider\";\n import {{ generateText }} from \"ai\";\n\n const {{ text }} = await generateText({{\n model: aihubmix({json.dumps(model)}),\n prompt: \"Say hello in one short sentence.\",\n }});\n\n console.log(text);\n \"\"\"\n ).strip()\n\n payload = example_payload(protocol, model)\n path = endpoint_path(protocol, model)\n spec = PROTOCOL_SPECS[protocol]\n header_name, header_value = auth_header(protocol)\n header_value_js = (\n \"process.env.AIHUBMIX_API_KEY\"\n if spec[\"family\"] in {\"anthropic\", \"gemini\"}\n else '`Bearer ${process.env.AIHUBMIX_API_KEY}`'\n )\n payload_js = json.dumps(payload, ensure_ascii=False, indent=2)\n payload_body_lines = [\" body: JSON.stringify(\" + payload_js.splitlines()[0]]\n payload_body_lines.extend(\" \" + line for line in payload_js.splitlines()[1:])\n payload_body_lines[-1] = payload_body_lines[-1] + \"),\"\n if spec.get(\"multipart\"):\n return textwrap.dedent(\n f\"\"\"\\\n import {{ createReadStream }} from \"node:fs\";\n\n const form = new FormData();\n form.set(\"model\", {json.dumps(model)});\n form.set(\"file\", createReadStream(process.env.AUDIO_FILE ?? \"./audio.mp3\"));\n\n const response = await fetch(\"{base_url()}{path}\", {{\n method: \"POST\",\n headers: {{\n {json.dumps(header_name)}: {header_value_js},\n }},\n body: form,\n }});\n\n if (!response.ok) {{\n throw new Error(`${{response.status}} ${{await response.text()}}`);\n }}\n\n const data = await response.json();\n console.log(data.text ?? JSON.stringify(data, null, 2));\n \"\"\"\n ).strip()\n if spec.get(\"binary_response\"):\n return \"\\n\".join(\n [\n 'import { writeFile } from \"node:fs/promises\";',\n \"\",\n f'const response = await fetch(\"{base_url()}{path}\", {{',\n ' method: \"POST\",',\n \" headers: {\",\n f\" {json.dumps(header_name)}: {header_value_js},\",\n ' \"Content-Type\": \"application/json\",',\n \" },\",\n *payload_body_lines,\n \"});\",\n \"\",\n \"if (!response.ok) {\",\n \" throw new Error(`${response.status} ${await response.text()}`);\",\n \"}\",\n \"\",\n 'await writeFile(\"aihubmix-output.mp3\", Buffer.from(await response.arrayBuffer()));',\n 'console.log(\"Saved to aihubmix-output.mp3\");',\n ]\n )\n if protocol == \"video-content\":\n return textwrap.dedent(\n f\"\"\"\\\n import {{ writeFile }} from \"node:fs/promises\";\n\n const videoId = process.env.AIHUBMIX_VIDEO_ID ?? \"video_123\";\n const response = await fetch(\"{base_url()}{path}\".replace(\"video_123\", videoId), {{\n method: \"GET\",\n headers: {{\n {json.dumps(header_name)}: {header_value_js},\n }},\n }});\n\n if (!response.ok) {{\n throw new Error(`${{response.status}} ${{await response.text()}}`);\n }}\n\n await writeFile(\"aihubmix-video.mp4\", Buffer.from(await response.arrayBuffer()));\n console.log(\"Saved to aihubmix-video.mp4\");\n \"\"\"\n ).strip()\n if spec[\"method\"] in {\"GET\", \"DELETE\"}:\n return textwrap.dedent(\n f\"\"\"\\\n const videoId = process.env.AIHUBMIX_VIDEO_ID ?? \"video_123\";\n const response = await fetch(\"{base_url()}{path}\".replace(\"video_123\", videoId), {{\n method: {json.dumps(spec[\"method\"])},\n headers: {{\n {json.dumps(header_name)}: {header_value_js},\n \"Accept\": \"application/json\",\n }},\n }});\n\n if (!response.ok) {{\n throw new Error(`${{response.status}} ${{await response.text()}}`);\n }}\n\n const data = await response.json();\n console.log(data.id ? `video id: ${{data.id}}` : JSON.stringify(data, null, 2));\n \"\"\"\n ).strip()\n if spec.get(\"needs_video_id\"):\n path_expr = f'\"{base_url()}{path}\".replace(\"video_123\", videoId)'\n return \"\\n\".join(\n [\n 'const videoId = process.env.AIHUBMIX_VIDEO_ID ?? \"video_123\";',\n f\"const response = await fetch({path_expr}, {{\",\n f' method: {json.dumps(spec[\"method\"])},',\n \" headers: {\",\n f\" {json.dumps(header_name)}: {header_value_js},\",\n ' \"Content-Type\": \"application/json\",',\n \" },\",\n *payload_body_lines,\n \"});\",\n \"\",\n \"if (!response.ok) {\",\n \" throw new Error(`${response.status} ${await response.text()}`);\",\n \"}\",\n \"\",\n \"const data = await response.json();\",\n \"console.log(data.id ? `video id: ${data.id}` : JSON.stringify(data, null, 2));\",\n ]\n )\n payload_json = json.dumps(payload, ensure_ascii=False, indent=2)\n payload_lines = [\" body: JSON.stringify(\" + payload_json.splitlines()[0]]\n payload_lines.extend(\" \" + line for line in payload_json.splitlines()[1:])\n payload_lines[-1] = payload_lines[-1] + \"),\"\n return \"\\n\".join(\n [\n f'const response = await fetch(\"{base_url()}{path}\", {{',\n f' method: {json.dumps(spec[\"method\"])},',\n \" headers: {\",\n f\" {json.dumps(header_name)}: {header_value_js},\",\n ' \"Content-Type\": \"application/json\",',\n \" },\",\n *payload_lines,\n \"});\",\n \"\",\n \"if (!response.ok) {\",\n \" throw new Error(`${response.status} ${await response.text()}`);\",\n \"}\",\n \"\",\n \"const data = await response.json();\",\n \"const text =\",\n \" data.output_text ??\",\n \" data.choices?.[0]?.message?.content ??\",\n \" data.content?.[0]?.text ??\",\n \" data.candidates?.[0]?.content?.parts?.[0]?.text ??\",\n \" data.data?.[0]?.url ??\",\n \" data.data?.[0]?.b64_json ??\",\n \" data.text ??\",\n \" (data.id ? `video id: ${data.id}` : undefined);\",\n \"\",\n \"console.log(text ?? JSON.stringify(data, null, 2));\",\n ]\n )\n\n\ndef cmd_example(args: argparse.Namespace) -> None:\n protocol = args.protocol\n model = args.model\n os_name = detect_os(args.os)\n if args.lang == \"both\":\n print(f\"## Command Line ({'Windows PowerShell' if os_name == 'windows' else 'macOS/Linux shell'})\")\n print()\n print(example_intro(\"command\", os_name))\n print()\n print(\"```\" + (\"powershell\" if os_name == \"windows\" else \"bash\"))\n print(render_cmd_example(protocol, model, os_name))\n print(\"```\")\n print()\n print(\"## JavaScript\")\n print()\n print(example_intro(\"js\"))\n print()\n print_env_setup(\"js\", os_name)\n print(\"```js\")\n print(render_js_example(protocol, model))\n print(\"```\")\n return\n\n url = base_url() + endpoint_path(protocol, model)\n payload = example_payload(protocol, model)\n header_name, header_value = auth_header(protocol)\n body = json.dumps(payload, ensure_ascii=False, indent=2)\n\n if args.lang == \"json\":\n print(body)\n return\n\n if args.lang == \"curl\":\n print(render_cmd_example(protocol, model, os_name))\n return\n\n if args.lang == \"python\":\n if normalize_protocol(protocol) == \"chat\":\n print(\n textwrap.dedent(\n f\"\"\"\\\n import os\n from openai import OpenAI\n\n client = OpenAI(\n api_key=os.environ[\"AIHUBMIX_API_KEY\"],\n base_url=\"{base_url()}/v1\",\n )\n\n response = client.chat.completions.create(\n model={model!r},\n messages=[{{\"role\": \"user\", \"content\": \"Say hello in one short sentence.\"}}],\n )\n print(response.choices[0].message.content)\n \"\"\"\n ).strip()\n )\n else:\n print(\"# Use the command-line or JavaScript example for this endpoint shape.\")\n return\n\n if args.lang == \"js\":\n print(render_js_example(protocol, model))\n return\n\n raise ValueError(args.lang)\n\n\ndef parse_error_body(body_text: str | None, body_file: str | None) -> dict[str, Any] | None:\n if body_file:\n body_text = Path(body_file).read_text(encoding=\"utf-8\")\n if not body_text:\n return None\n try:\n data = json.loads(body_text)\n return data if isinstance(data, dict) else {\"raw\": data}\n except json.JSONDecodeError:\n return {\"raw\": body_text}\n\n\ndef cmd_troubleshoot(args: argparse.Namespace) -> None:\n body = parse_error_body(args.body, args.body_file)\n error = body.get(\"error\") if isinstance(body, dict) and isinstance(body.get(\"error\"), dict) else {}\n message = error.get(\"message\") or (body or {}).get(\"message\") if isinstance(body, dict) else None\n kind = error.get(\"type\")\n param = error.get(\"param\")\n code = error.get(\"code\")\n\n print(\"Observed\")\n print_table(\n [\"field\", \"value\"],\n [\n [\"status\", str(args.status or \"-\")],\n [\"endpoint\", args.endpoint or \"-\"],\n [\"model\", args.model or \"-\"],\n [\"error.type\", str(kind or \"-\")],\n [\"error.param\", str(param or \"-\")],\n [\"error.code\", str(code or \"-\")],\n [\"error.message\", str(message or \"-\")],\n [\"request_id\", args.request_id or \"check X-Request-Id response header\"],\n ],\n )\n\n if args.contract:\n contract = load_error_contract()\n status_key = \"5XX\" if args.status and args.status >= 500 else str(args.status or \"\")\n status_contract = contract[\"statuses\"].get(status_key, {})\n is_gateway_shape = isinstance(error, dict) and bool(message)\n print(\"\\nContract Match\")\n print_table(\n [\"field\", \"value\"],\n [\n [\"contract_source\", display_error_contract_source()],\n [\"schema\", contract[\"schema_name\"]],\n [\"status_contract\", status_contract.get(\"description\", \"not declared\")],\n [\"body_shape\", \"GatewayError-like\" if is_gateway_shape else \"not enough evidence\"],\n [\"request_id_rule\", contract[\"request_id\"]],\n [\"error_type_rule\", contract[\"envelope\"][\"fields\"][\"error.type\"]],\n ],\n )\n\n print(\"\\nSuggested checks\")\n suggestions = []\n status = args.status\n if status in (401, 403):\n suggestions.extend(\n [\n \"Confirm AIHUBMIX_API_KEY is present and sent with the right header for the endpoint.\",\n \"Use Authorization: Bearer for OpenAI-shaped endpoints, x-api-key for /v1/messages, and x-goog-api-key or ?key= for Gemini-shaped endpoints.\",\n ]\n )\n elif status == 404:\n suggestions.extend(\n [\n \"Confirm the endpoint path matches the protocol contract.\",\n \"Confirm the model exists in /api/v1/models and the model ID is copied exactly.\",\n ]\n )\n elif status == 400:\n suggestions.extend(\n [\n \"Validate the request body against aihubmix-openapi openapi.json or contract.json.\",\n \"Check whether the model supports the requested endpoint, modality, and feature fields.\",\n ]\n )\n elif status == 429:\n suggestions.append(\"Check rate limits, quota, and retry policy before re-sending the same request.\")\n elif status and status >= 500:\n suggestions.extend(\n [\n \"Treat this as gateway or upstream failure; preserve status, error body, and X-Request-Id.\",\n \"Retry only when the operation is idempotent or the client can tolerate duplicate work.\",\n ]\n )\n else:\n suggestions.append(\"Start with status, error body, endpoint, model ID, and X-Request-Id.\")\n\n if args.model:\n suggestions.append(\"Run `models get <model_id>` to inspect context_length, max_output, endpoints, and features.\")\n if args.endpoint:\n suggestions.append(\"Run `protocols` to inspect the endpoint security scheme and requestBody contract.\")\n for index, item in enumerate(suggestions, start=1):\n print(f\"{index}. {item}\")\n\n\n# --- Preflight (doctor) + image-input candidates --------------------------\n\n\ndef _openai_base(explicit: str | None = None) -> str:\n \"\"\"OpenAI-compatible base URL, tolerant of a trailing /v1 already present.\"\"\"\n raw = (explicit or os.environ.get(\"AIHUBMIX_BASE_URL\") or DEFAULT_SERVER_URL).rstrip(\"/\")\n return raw if raw.endswith(\"/v1\") else raw + \"/v1\"\n\n\ndef _http_request(\n url: str,\n method: str = \"GET\",\n headers: dict[str, str] | None = None,\n body: dict[str, Any] | None = None,\n timeout: float = 30,\n) -> tuple[int | None, str]:\n \"\"\"Minimal JSON HTTP call. Returns (status, text); status is None on network error.\"\"\"\n data = json.dumps(body).encode(\"utf-8\") if body is not None else None\n request = urllib.request.Request(url, data=data, method=method, headers=headers or {})\n try:\n with urllib.request.urlopen(request, timeout=timeout) as response:\n status = getattr(response, \"status\", None) or response.getcode()\n return status, response.read().decode(\"utf-8\", errors=\"replace\")\n except urllib.error.HTTPError as exc:\n return exc.code, exc.read().decode(\"utf-8\", errors=\"replace\")\n except urllib.error.URLError as exc:\n return None, f\"URLError: {exc.reason}\"\n except (TimeoutError, OSError) as exc:\n return None, f\"network error: {exc}\"\n\n\ndef _mask_secret(secret: str) -> str:\n secret = (secret or \"\").strip()\n if not secret:\n return \"(none)\"\n if len(secret) <= 12:\n return secret[:2] + \"…\" + secret[-2:]\n return secret[:5] + \"…\" + secret[-4:]\n\n\ndef _redact(text: str, *secrets: str) -> str:\n \"\"\"Scrub keys from upstream text (AIHubMix echoes the key in invalid-key errors).\"\"\"\n out = text or \"\"\n for secret in secrets:\n secret = (secret or \"\").strip()\n if len(secret) >= 6:\n out = out.replace(secret, \"[REDACTED]\")\n if secret.lower().startswith(\"sk-\"):\n out = out.replace(secret[3:], \"[REDACTED]\")\n return re.sub(r\"sk-[A-Za-z0-9]{6,}\", \"[REDACTED]\", out)\n\n\ndef _extract_chat_reply(body: str) -> str | None:\n try:\n data = json.loads(body)\n content = data[\"choices\"][0][\"message\"][\"content\"]\n except (ValueError, KeyError, IndexError, TypeError):\n return None\n if isinstance(content, list):\n return \" \".join(part.get(\"text\", \"\") for part in content if isinstance(part, dict)).strip()\n return str(content).strip()\n\n\ndef _looks_like_auth_error(body: str) -> bool:\n low = (body or \"\").lower()\n return any(\n token in low\n for token in (\n \"invalid key\", \"invalid api key\", \"incorrect api key\", \"unauthorized\",\n \"authentication\", \"no permission\", \"无效\", \"鉴权\", \"未授权\", \"令牌\",\n )\n )\n\n\ndef _looks_like_balance_error(body: str) -> bool:\n low = (body or \"\").lower()\n return any(\n token in low\n for token in (\n \"insufficient balance\", \"insufficient_quota\", \"insufficient funds\", \"no balance\",\n \"balance is not enough\", \"arrearage\", \"余额不足\", \"余额已用\", \"欠费\", \"额度不足\",\n )\n )\n\n\ndef _solid_png_data_url(rgb: tuple[int, int, int] = (20, 90, 210), size: int = 64) -> str:\n \"\"\"Build a tiny solid-color PNG data URL (stdlib only) for probing image input.\"\"\"\n import base64\n import struct\n import zlib\n\n def chunk(tag: bytes, payload: bytes) -> bytes:\n return (\n struct.pack(\">I\", len(payload))\n + tag\n + payload\n + struct.pack(\">I\", zlib.crc32(tag + payload) & 0xFFFFFFFF)\n )\n\n signature = b\"\\x89PNG\\r\\n\\x1a\\n\"\n ihdr = struct.pack(\">IIBBBBB\", size, size, 8, 2, 0, 0, 0) # 8-bit RGB\n raw = b\"\".join(b\"\\x00\" + bytes(rgb) * size for _ in range(size))\n png = signature + chunk(b\"IHDR\", ihdr) + chunk(b\"IDAT\", zlib.compress(raw)) + chunk(b\"IEND\", b\"\")\n return \"data:image/png;base64,\" + base64.b64encode(png).decode(\"ascii\")\n\n\ndef cmd_doctor(args: argparse.Namespace) -> int:\n key = (args.key or os.environ.get(\"AIHUBMIX_API_KEY\") or \"\").strip()\n base = _openai_base(args.base_url)\n timeout = args.timeout\n\n print(_t(\"AIHubMix preflight (doctor)\", \"AIHubMix 预检 (doctor)\"))\n print(f\"base_url: {base}\")\n print(f\"key: {_mask_secret(key)}\\n\")\n\n if not key:\n print(_t(\"[✗] KEY missing — set AIHUBMIX_API_KEY or pass --key <key>\",\n \"[✗] KEY 未提供 — 设置环境变量 AIHUBMIX_API_KEY 或传 --key <key>\"))\n print(_t(\"\\nResult: FAIL. No API key available.\", \"\\n结论: 失败。没有可用的 API key。\"))\n return 1\n print(_t(\"[✓] KEY provided\", \"[✓] KEY 已提供\"))\n\n headers = {\n \"Authorization\": f\"Bearer {key}\",\n \"Content-Type\": \"application/json\",\n \"Accept\": \"application/json\",\n \"User-Agent\": \"aihubmixApi-skill/0.1\",\n }\n\n # The public /v1/models list does NOT require auth, so it cannot validate a\n # key. Only a real chat call can — use a tiny one as the auth + text probe.\n probe_model = args.model or \"gpt-4o-mini\"\n payload = {\"model\": probe_model, \"messages\": [{\"role\": \"user\", \"content\": \"ping\"}], \"max_tokens\": 8, \"temperature\": 0}\n status, body = _http_request(f\"{base}/chat/completions\", method=\"POST\", headers=headers, body=payload, timeout=timeout)\n reply = _extract_chat_reply(body) if status == 200 else None\n if reply is not None:\n note = \"\" if args.model else _t(\" (probe model gpt-4o-mini)\", \"(探测模型 gpt-4o-mini)\")\n print(_t(f\"[✓] AUTH key valid, {probe_model} call OK{note} → {reply[:60]!r}\",\n f\"[✓] AUTH key 有效,{probe_model} 调用成功{note} → {reply[:60]!r}\"))\n elif status == 402 or _looks_like_balance_error(body):\n print(_t(f\"[✗] BILLING key valid but insufficient balance/quota — HTTP {status}: {_redact(body, key)[:300]}\",\n f\"[✗] 余额 key 有效但余额/额度不足 — HTTP {status}: {_redact(body, key)[:300]}\"))\n print(_t(\"\\nResult: FAIL (billing). The key works; the account needs a top-up.\",\n \"\\n结论: 失败(计费)。key 本身可用,但账户需要充值。\"))\n return 1\n elif status in (401, 403) or _looks_like_auth_error(body):\n print(_t(f\"[✗] AUTH invalid key / no permission — HTTP {status}: {_redact(body, key)[:300]}\",\n f\"[✗] AUTH key 无效/无权限 — HTTP {status}: {_redact(body, key)[:300]}\"))\n print(_t(\"\\nResult: FAIL. Check the key is correct, enabled, and funded.\",\n \"\\n结论: 失败。请检查 key 是否正确、已启用、有余额。\"))\n return 1\n else:\n info = f\"HTTP {status}\" if status else _t(\"network unreachable\", \"网络不可达\")\n print(_t(f\"[⚠] AUTH unconfirmed — probing with {probe_model} returned {info}: {_redact(body, key)[:200]}\",\n f\"[⚠] AUTH 未能确认 — 用 {probe_model} 探测返回 {info}: {_redact(body, key)[:200]}\"))\n print(_t(\" Usually the model is not available to your key (key likely fine); retry with --model <a model you can access>.\",\n \" 多半是该模型对你的 key 不可用,而非 key 失效;用 --model <你有权限的模型> 重测。\"))\n print(_t(\"\\nResult: NOT PASSED (could not confirm key). Retry with --model <available model>.\",\n \"\\n结论: 未通过(无法确认 key)。建议用 --model 指定可用模型重测。\"))\n return 1\n\n overall_ok = True\n\n if args.image:\n if not args.model:\n print(_t(\"[–] IMAGE skipped (image probe needs --model <id>; you/the LLM pick which vision model)\",\n \"[–] IMAGE 跳过(图片实测需要 --model <id>;该测哪个视觉模型由你/LLM 决定)\"))\n else:\n payload = {\n \"model\": args.model,\n \"max_tokens\": 16,\n \"temperature\": 0,\n \"messages\": [\n {\n \"role\": \"user\",\n \"content\": [\n {\"type\": \"text\", \"text\": \"What is the dominant color of this image? Answer with one word.\"},\n {\"type\": \"image_url\", \"image_url\": {\"url\": _solid_png_data_url()}},\n ],\n }\n ],\n }\n status, body = _http_request(f\"{base}/chat/completions\", method=\"POST\", headers=headers, body=payload, timeout=timeout)\n reply = _extract_chat_reply(body) if status == 200 else None\n if reply is not None:\n print(_t(f\"[✓] IMAGE {args.model} accepts image input → {reply[:60]!r}\",\n f\"[✓] IMAGE {args.model} 接受图片输入 → {reply[:60]!r}\"))\n else:\n overall_ok = False\n print(_t(f\"[✗] IMAGE {args.model} no image support or call failed — HTTP {status}: {_redact(body, key)[:300]}\",\n f\"[✗] IMAGE {args.model} 不支持图片或调用失败 — HTTP {status}: {_redact(body, key)[:300]}\"))\n\n print()\n if overall_ok:\n tail = (\n _t(\"Tested capabilities all OK.\", \"已实测的能力均正常。\")\n if args.model\n else _t(\"Key valid; add --model <id> (and --image) to test a specific model.\",\n \"key 有效;加 --model <id>(可再加 --image)可实测具体模型。\")\n )\n print(_t(f\"Result: PASS. {tail}\", f\"结论: 通过。{tail}\"))\n return 0\n print(_t(\"Result: some checks did not pass — see the [✗] items above.\",\n \"结论: 部分检查未通过,见上面的 [✗] 项。\"))\n return 1\n\n\n# Categories that cannot accept chat input, dropped from input-modality\n# candidate ranges (e.g. vision) even when the noisy modality field tags them.\n_NON_CHAT_RE = re.compile(\n r\"(embed|rerank|\\bbge\\b|\\bbce\\b|\\bgte\\b|\\bm3e\\b|jina|moderation|\"\n r\"whisper|\\btts\\b|text-to-speech|speech|transcrib|\"\n r\"flux|stable-diffusion|\\bsdxl\\b|dall-?e|midjourney|imagen|gpt-image|qwen-image|\"\n r\"seedream|seededit|kolors|cogview|ideogram|recraft|hunyuan-image|\"\n r\"\\bveo\\b|sora|kling|runway|video|hailuo|seedance|vidu)\",\n re.IGNORECASE,\n)\n\n\ndef _is_non_chat(model: dict[str, Any]) -> bool:\n \"\"\"Drop models that cannot accept chat input (embeddings, generation, audio, video).\"\"\"\n if suggested_protocol(model) in {\n \"embeddings\", \"images\", \"videos\", \"moderations\",\n \"audio-speech\", \"audio-transcriptions\", \"audio-translations\",\n }:\n return True\n return bool(_NON_CHAT_RE.search(f\"{model_id(model)} {model_label(model)}\"))\n\n\n# Capability discovery is deliberately broad + heuristic. Each spec scores a\n# model by up to three signals and the caller/LLM picks from the range:\n# protocol: suggested_protocol(model) lands in the capability's endpoint set\n# modality: the model's metadata text contains a capability keyword\n# family: the model id/name matches a known family regex\n# protocol/family are treated as reliable; modality-only is noisy. The tool\n# never picks a default and never asserts a hard capability fact.\nCAPABILITY_SPECS: dict[str, dict[str, Any]] = {\n \"vision\": {\n \"label\": \"图片输入 / 看图(chat 模型接受图片)\",\n \"aliases\": [\"image-input\", \"multimodal\", \"mm\", \"vlm\", \"看图\", \"图片输入\", \"多模态\"],\n \"protocols\": None,\n \"modality_tokens\": [\"image\", \"vision\", \"multimodal\", \"visual\"],\n \"require_chat\": True,\n \"family\": (\n r\"gpt-4o|gpt-4\\.1|gpt-4-vision|gpt-4-turbo|gpt-5|chatgpt-4o|o1|o3|o4-mini|\"\n r\"gemini|claude-3|claude-4|claude-sonnet|claude-opus|claude-haiku|\"\n r\"qwen[\\w.-]*vl|qwen-vl|glm-4v|glm-[\\d.]+v|cogvlm|cogagent|yi-vision|yi-vl|\"\n r\"step-1v|step-1o|step-3|internvl|llava|pixtral|moondream|\"\n r\"llama[\\w.-]*vision|grok[\\w.-]*vision|grok-2-vision|grok-4|\"\n r\"doubao[\\w.-]*vision|ernie[\\w.-]*vl|minicpm-v|nova-lite|nova-pro|\"\n r\"phi-3[\\w.-]*vision|phi-4[\\w.-]*multimodal|kimi[\\w.-]*vl|kimi-latest|moonshot[\\w.-]*vl|\"\n r\"mistral-small-3|molmo|deepseek-vl\"\n ),\n },\n \"image-gen\": {\n \"label\": \"图像生成 / 文生图(text-to-image)\",\n \"aliases\": [\"t2i\", \"image-generation\", \"imagegen\", \"生图\", \"文生图\", \"画图\"],\n \"protocols\": {\"images\"},\n \"modality_tokens\": [\"image_generation\", \"text-to-image\"],\n \"require_chat\": False,\n \"family\": (\n r\"flux|kontext|stable-diffusion|\\bsd3\\b|\\bsdxl\\b|dall-?e|midjourney|\\bmj\\b|\"\n r\"imagen|gpt-image|qwen-image|hunyuan-image|hidream|seedream|seededit|kolors|\"\n r\"cogview|ideogram|recraft|wanx|gemini[\\w.-]*image|nano-banana|grok[\\w.-]*image|playground-v\"\n ),\n },\n \"video\": {\n \"label\": \"视频生成(text/image-to-video)\",\n \"aliases\": [\"t2v\", \"video-generation\", \"生视频\", \"文生视频\", \"视频\"],\n \"protocols\": {\"videos\"},\n \"modality_tokens\": [\"video\"],\n \"require_chat\": False,\n \"family\": (\n r\"veo|sora|kling|runway|gen-?3|hailuo|minimax[\\w.-]*video|seedance|vidu|\"\n r\"wan[\\w.-]*|cogvideo|pika|luma|dream-machine|ray-?2|pixverse|mochi|\\bltx\\b\"\n ),\n },\n \"audio-tts\": {\n \"label\": \"语音合成(文字转语音 TTS)\",\n \"aliases\": [\"tts\", \"text-to-speech\", \"speech\", \"语音合成\"],\n \"protocols\": {\"audio-speech\"},\n \"modality_tokens\": [\"speech\", \"text-to-speech\", \"tts\"],\n \"require_chat\": False,\n \"family\": (\n r\"\\btts\\b|text-to-speech|speech-0|cosyvoice|fish[\\w.-]*audio|elevenlabs|\"\n r\"gpt-4o[\\w.-]*tts|qwen[\\w.-]*tts|doubao[\\w.-]*tts|minimax[\\w.-]*speech|index-tts\"\n ),\n },\n \"audio-stt\": {\n \"label\": \"语音识别 / 转写(STT/ASR)\",\n \"aliases\": [\"stt\", \"asr\", \"transcription\", \"whisper\", \"语音识别\", \"转写\"],\n \"protocols\": {\"audio-transcriptions\", \"audio-translations\"},\n \"modality_tokens\": [\"transcription\", \"translation\"],\n \"require_chat\": False,\n \"family\": (\n r\"whisper|transcrib|gpt-4o[\\w.-]*transcribe|qwen[\\w.-]*audio|sensevoice|paraformer|fun-?asr\"\n ),\n },\n \"embedding\": {\n \"label\": \"向量嵌入(embedding)\",\n \"aliases\": [\"embed\", \"embeddings\", \"向量\", \"嵌入\"],\n \"protocols\": {\"embeddings\"},\n \"modality_tokens\": [\"embedding\"],\n \"require_chat\": False,\n \"exclude\": r\"rerank\",\n \"family\": (\n r\"embed|text-embedding|\\bbge\\b|bge-[\\w.-]+|\\bbce\\b|\\bgte\\b|\\bm3e\\b|jina[\\w.-]*embed|\"\n r\"nomic[\\w.-]*embed|qwen[\\w.-]*embedding|conan-embedding|doubao[\\w.-]*embedding|\\bgme\\b\"\n ),\n },\n \"rerank\": {\n \"label\": \"重排(rerank)\",\n \"aliases\": [\"reranker\", \"重排\"],\n \"protocols\": None,\n \"modality_tokens\": [\"rerank\"],\n \"require_chat\": False,\n \"family\": r\"rerank|reranker|bge[\\w.-]*rerank|bce[\\w.-]*rerank|jina[\\w.-]*rerank|qwen3?-?rerank\",\n },\n}\n\nfor _spec in CAPABILITY_SPECS.values():\n _spec[\"_family_re\"] = re.compile(_spec[\"family\"], re.IGNORECASE)\n _spec[\"_exclude_re\"] = re.compile(_spec[\"exclude\"], re.IGNORECASE) if _spec.get(\"exclude\") else None\n\n\ndef resolve_capability(name: str | None) -> str:\n key = (name or \"vision\").strip().lower()\n if key in CAPABILITY_SPECS:\n return key\n for cap, spec in CAPABILITY_SPECS.items():\n if key in [alias.lower() for alias in spec[\"aliases\"]]:\n return cap\n raise SystemExit(f\"未知能力: {name}。可选: {', '.join(CAPABILITY_SPECS)}\")\n\n\ndef _capability_signals(model: dict[str, Any], spec: dict[str, Any]) -> list[str]:\n signals: list[str] = []\n if spec[\"protocols\"] and suggested_protocol(model) in spec[\"protocols\"]:\n signals.append(\"protocol\")\n cap_text = \" \".join(\n compact(model.get(key))\n for key in (\"features\", \"capabilities\", \"input_modalities\", \"modality\", \"modalities\", \"types\", \"type\", \"endpoints\")\n if model.get(key) is not None\n ).lower()\n if any(token in cap_text for token in spec[\"modality_tokens\"]):\n signals.append(\"modality\")\n if spec[\"_family_re\"].search(f\"{model_id(model)} {model_label(model)}\"):\n signals.append(\"family\")\n return signals\n\n\n# --- Companion aihubmix CLI helpers (account data; opt-in only) -------------\n\n\ndef _find_aihubmix() -> str | None:\n \"\"\"Locate the companion aihubmix CLI on PATH or known install locations.\"\"\"\n found = shutil.which(CLI_BIN)\n if found:\n return found\n candidates: list[Path] = []\n local = os.environ.get(\"LOCALAPPDATA\")\n if local:\n candidates.append(Path(local) / \"aihubmix\" / \"bin\" / \"aihubmix.exe\")\n home = Path.home()\n candidates += [home / \".local\" / \"bin\" / CLI_BIN, Path(\"/usr/local/bin\") / CLI_BIN]\n for candidate in candidates:\n try:\n if candidate.is_file():\n return str(candidate)\n except OSError:\n continue\n return None\n\n\ndef _install_aihubmix() -> bool:\n \"\"\"Run the official per-OS installer (explicit / opt-in only). Returns success.\"\"\"\n if platform.system().lower().startswith(\"win\"):\n cmd = [\"powershell\", \"-NoProfile\", \"-Command\", f\"irm {CLI_INSTALL_PS1} | iex\"]\n else:\n cmd = [\"sh\", \"-c\", f\"curl -fsSL {CLI_INSTALL_SH} | sh\"]\n try:\n return subprocess.run(cmd, timeout=300).returncode == 0\n except (OSError, subprocess.SubprocessError):\n return False\n\n\ndef cmd_install_cli(args: argparse.Namespace) -> int:\n existing = _find_aihubmix()\n if existing and not args.force:\n print(_t(f\"aihubmix CLI already installed: {existing}\", f\"aihubmix CLI 已安装:{existing}\"))\n print(_t(f\"Project: {CLI_REPO_URL}\", f\"项目主页:{CLI_REPO_URL}\"))\n return 0\n print(_t(f\"Installing the aihubmix CLI (project: {CLI_REPO_URL}) ...\",\n f\"正在安装 aihubmix CLI(项目主页:{CLI_REPO_URL})...\"))\n if _install_aihubmix() and _find_aihubmix():\n print(_t(\"Done. Next: run `aihubmix login` to authenticate.\",\n \"完成。下一步:运行 `aihubmix login` 登录。\"))\n return 0\n print(_t(f\"Install failed. Install manually — see {CLI_REPO_URL}\",\n f\"安装失败。请手动安装,见 {CLI_REPO_URL}\"))\n return 1\n\n\ndef _my_callable_models(auto_install: bool = False) -> set[str]:\n \"\"\"Lowercased ids the user's token can actually call (`aihubmix models list -j`).\"\"\"\n exe = _find_aihubmix()\n if not exe and auto_install:\n print(_t(\"aihubmix CLI not found; installing (--auto-install) ...\",\n \"未找到 aihubmix CLI,正在自动安装(--auto-install)...\"), file=sys.stderr)\n if _install_aihubmix():\n exe = _find_aihubmix()\n if not exe:\n raise SystemExit(_t(\n f\"--mine needs the aihubmix CLI. Run `install-cli` (or add --auto-install). Project: {CLI_REPO_URL}\",\n f\"--mine 需要 aihubmix CLI。先运行 `install-cli`(或加 --auto-install)。项目主页:{CLI_REPO_URL}\"))\n try:\n result = subprocess.run([exe, \"models\", \"list\", \"-j\"], capture_output=True, text=True, timeout=60)\n except (OSError, subprocess.SubprocessError) as exc:\n raise SystemExit(f\"`aihubmix models list` failed: {exc}\")\n if result.returncode != 0:\n detail = (result.stderr or result.stdout or \"\").strip()\n if \"未登录\" in detail or \"login\" in detail.lower() or \"token\" in detail.lower():\n raise SystemExit(_t(\n f\"Not logged in. Run `aihubmix login` first, or drop --mine. Project: {CLI_REPO_URL}\",\n f\"尚未登录。请先 `aihubmix login`,或去掉 --mine。项目主页:{CLI_REPO_URL}\"))\n raise SystemExit(f\"`aihubmix models list` error: {detail[:200]}\")\n try:\n data = json.loads(result.stdout)\n except ValueError:\n raise SystemExit(\"could not parse `aihubmix models list -j` output\")\n rows = data.get(\"data\") if isinstance(data, dict) else data\n return {\n str(row.get(\"model\")).strip().lower()\n for row in (rows or [])\n if isinstance(row, dict) and row.get(\"model\")\n }\n\n\ndef cmd_candidates(args: argparse.Namespace) -> None:\n cap = resolve_capability(getattr(args, \"capability\", None))\n spec = CAPABILITY_SPECS[cap]\n models = extract_models(load_json_source(args.source))\n\n mine = None\n if getattr(args, \"mine\", False):\n mine = _my_callable_models(auto_install=getattr(args, \"auto_install\", False))\n\n picked: list[tuple[dict[str, Any], bool, str]] = []\n for item in models:\n if spec[\"require_chat\"] and _is_non_chat(item):\n continue\n exclude_re = spec.get(\"_exclude_re\")\n if exclude_re and exclude_re.search(f\"{model_id(item)} {model_label(item)}\"):\n continue\n if mine is not None and model_id(item).strip().lower() not in mine:\n continue # keep only models the user's token can actually call\n signals = _capability_signals(item, spec)\n if not signals:\n continue\n strong = (\"protocol\" in signals) or (\"family\" in signals)\n picked.append((item, strong, \"+\".join(signals)))\n # Reliable protocol/family matches first, then noisier modality-only ones.\n picked.sort(key=lambda row: (0 if row[1] else 1, model_id(row[0]).lower()))\n selected = [(item, signal) for item, _strong, signal in picked[: args.limit]]\n\n if args.json:\n print(json.dumps(\n [\n {\n \"capability\": cap,\n \"mine\": mine is not None,\n \"model_id\": model_id(item),\n \"name\": model_label(item),\n \"signal\": signal,\n \"features\": features(item),\n \"context\": context_display(item),\n }\n for item, signal in selected\n ],\n ensure_ascii=False,\n indent=2,\n ))\n return\n\n mine_en = \" callable by your token\" if mine is not None else \"\"\n mine_zh = \"(已按你的 token 可调过滤)\" if mine is not None else \"\"\n print(_t(\n f\"AIHubMix candidate models for capability '{cap}'{mine_en} (broad heuristic: protocol / family / modality; no default picked)\",\n f\"AIHubMix【{spec['label']}】候选模型{mine_zh}(宽口径启发式:协议 / 家族名 / modality 字段;不替你挑默认)\",\n ))\n if mine is not None and not selected:\n print(_t(\n f\"\\nNone of the '{cap}' candidates are callable by your token. Try without --mine, or check `aihubmix models list`.\",\n f\"\\n你的 token 在【{cap}】下没有可调候选。可去掉 --mine,或查 `aihubmix models list`。\",\n ))\n return\n print(_t(\n \"Note: a candidate is not a guarantee; pick per task (you/the LLM), then confirm with `doctor`.\",\n \"注意:候选不等于确定可用;请由调用方/LLM 按任务从中挑选,再用 `doctor` 实测确认。\",\n ))\n print()\n rows = [\n [model_id(item), model_label(item), signal, features(item) or \"-\", context_display(item)]\n for item, signal in selected\n ]\n print_table([\"model_id\", \"name\", \"signal\", \"features/modalities\", \"context\"], rows)\n mine_foot_en = \" | filtered to your token\" if mine is not None else \"\"\n mine_foot_zh = \" | 已按你的 token 过滤\" if mine is not None else \"\"\n print(_t(\n f\"\\ncapability: {cap} | {len(selected)} candidates{mine_foot_en} | source: {display_models_source()}\",\n f\"\\n能力: {cap} | 共 {len(selected)} 个候选{mine_foot_zh} | source: {display_models_source()}\",\n ))\n print(_t(\n \"signal: protocol=endpoint type; family=known family name (both reliable); modality=metadata keyword only (noisy, verify).\",\n \"signal: protocol=接口类型匹配;family=按已知家族名匹配(均较可靠);modality=仅元数据出现关键词(噪声大,需实测)。\",\n ))\n\n\ndef build_parser() -> argparse.ArgumentParser:\n parser = argparse.ArgumentParser(description=\"Query AIHubMix models and API contract helpers.\")\n parser.add_argument(\n \"--lang\", choices=[\"en\", \"zh\"],\n help=\"Output language for status lines/labels (default: env AIHUBMIX_LANG or en).\",\n )\n subparsers = parser.add_subparsers(dest=\"command\", required=True)\n\n list_parser = subparsers.add_parser(\"list\", help=\"List models from the models API.\")\n list_parser.add_argument(\"--source\", help=\"Models JSON URL, local JSON file, or '-' for stdin.\")\n list_parser.add_argument(\"--query\", help=\"Filter models by substring.\")\n list_parser.add_argument(\"--limit\", type=int, default=50)\n list_parser.add_argument(\"--json\", action=\"store_true\")\n list_parser.set_defaults(func=cmd_list)\n\n get_parser = subparsers.add_parser(\"get\", help=\"Show one model by ID or substring.\")\n get_parser.add_argument(\"model\")\n get_parser.add_argument(\"--source\")\n get_parser.add_argument(\"--json\", action=\"store_true\")\n get_parser.set_defaults(func=cmd_get)\n\n compare_parser = subparsers.add_parser(\"compare\", help=\"Compare pricing and context for models.\")\n compare_parser.add_argument(\"models\", nargs=\"+\")\n compare_parser.add_argument(\"--source\")\n compare_parser.add_argument(\"--json\", action=\"store_true\")\n compare_parser.set_defaults(func=cmd_compare)\n\n latest_parser = subparsers.add_parser(\"latest-report\", help=\"Report the first N models with pricing, context, and an example.\")\n latest_parser.add_argument(\"--source\", help=\"Models JSON URL, local JSON file, or '-' for stdin.\")\n latest_parser.add_argument(\"--limit\", type=int, default=5)\n latest_parser.add_argument(\"--os\", choices=[\"auto\", \"windows\", \"mac\", \"linux\"], default=\"auto\")\n latest_parser.set_defaults(func=cmd_latest_report)\n\n report_parser = subparsers.add_parser(\"report\", help=\"Report models matching a query with pricing, context, endpoint, and examples.\")\n report_parser.add_argument(\"query\")\n report_parser.add_argument(\"--source\", help=\"Models JSON URL, local JSON file, or '-' for stdin.\")\n report_parser.add_argument(\"--limit\", type=int, default=20)\n report_parser.add_argument(\"--os\", choices=[\"auto\", \"windows\", \"mac\", \"linux\"], default=\"auto\")\n report_parser.set_defaults(func=cmd_report)\n\n protocols_parser = subparsers.add_parser(\"protocols\", help=\"Summarize live aihubmix OpenAPI operations (remote).\")\n protocols_parser.add_argument(\"--json\", action=\"store_true\")\n protocols_parser.set_defaults(func=cmd_protocols)\n\n error_contract_parser = subparsers.add_parser(\"error-contract\", help=\"Summarize the GatewayError contract from the live OpenAPI spec (remote).\")\n error_contract_parser.add_argument(\"--json\", action=\"store_true\")\n error_contract_parser.set_defaults(func=cmd_error_contract)\n\n sdk_parser = subparsers.add_parser(\"sdk-check\", help=\"Check @aihubmix/ai-sdk-provider version without installing or updating it.\")\n sdk_parser.add_argument(\"--project\", help=\"Project directory to search for node_modules.\")\n sdk_parser.add_argument(\"--version\", help=\"Target SDK version to compare against.\")\n sdk_parser.add_argument(\"--offline\", action=\"store_true\", help=\"Skip npm registry latest-version check.\")\n sdk_parser.set_defaults(func=cmd_sdk_check)\n\n sdk_info_parser = subparsers.add_parser(\"sdk-info\", help=\"Summarize @aihubmix/ai-sdk-provider capabilities and auth/routing defaults.\")\n sdk_info_parser.add_argument(\"--project\", help=\"Project directory to search for installed node_modules.\")\n sdk_info_parser.add_argument(\"--package-dir\", help=\"Path to an unpacked @aihubmix/ai-sdk-provider package directory.\")\n sdk_info_parser.add_argument(\"--version\", default=SDK_RECOMMENDED_VERSION, help=\"SDK version to inspect from npm when no package is installed.\")\n sdk_info_parser.add_argument(\n \"--source\",\n choices=[\"auto\", \"installed\", \"npm\"],\n default=\"auto\",\n help=\"SDK metadata source. npm reads the package tarball from the npm registry.\",\n )\n sdk_info_parser.add_argument(\"--json\", action=\"store_true\")\n sdk_info_parser.set_defaults(func=cmd_sdk_info)\n\n example_parser = subparsers.add_parser(\"example\", help=\"Generate minimal API call examples.\")\n example_parser.add_argument(\n \"protocol\",\n choices=sorted([*PROTOCOL_SPECS.keys(), *PROTOCOL_ALIASES.keys()]),\n help=\"Protocol or endpoint shape to generate.\",\n )\n example_parser.add_argument(\"--model\", required=True)\n example_parser.add_argument(\"--lang\", choices=[\"both\", \"curl\", \"python\", \"js\", \"json\"], default=\"both\")\n example_parser.add_argument(\"--os\", choices=[\"auto\", \"windows\", \"mac\", \"linux\"], default=\"auto\")\n example_parser.set_defaults(func=cmd_example)\n\n troubleshoot_parser = subparsers.add_parser(\"troubleshoot\", help=\"Explain common API failure checks.\")\n troubleshoot_parser.add_argument(\"--status\", type=int)\n troubleshoot_parser.add_argument(\"--body\", help=\"JSON error body string.\")\n troubleshoot_parser.add_argument(\"--body-file\", help=\"Path to a JSON error body.\")\n troubleshoot_parser.add_argument(\"--endpoint\")\n troubleshoot_parser.add_argument(\"--model\")\n troubleshoot_parser.add_argument(\"--request-id\")\n troubleshoot_parser.add_argument(\"--contract\", action=\"store_true\", help=\"Also fetch the live GatewayError contract (remote) and show a match.\")\n troubleshoot_parser.set_defaults(func=cmd_troubleshoot)\n\n doctor_parser = subparsers.add_parser(\n \"doctor\",\n help=\"Preflight an AIHubMix key: verify auth, then optionally smoke-test text and image input on a chosen model.\",\n )\n doctor_parser.add_argument(\"--key\", help=\"API key. Defaults to env AIHUBMIX_API_KEY. Never printed in full.\")\n doctor_parser.add_argument(\"--base-url\", help=\"OpenAI-compatible base. Defaults to env AIHUBMIX_BASE_URL or https://aihubmix.com/v1.\")\n doctor_parser.add_argument(\"--model\", help=\"Model id to smoke-test (text; also image with --image).\")\n doctor_parser.add_argument(\"--image\", action=\"store_true\", help=\"Also probe image input on --model.\")\n doctor_parser.add_argument(\"--timeout\", type=float, default=30)\n doctor_parser.set_defaults(func=cmd_doctor)\n\n candidates_parser = subparsers.add_parser(\n \"candidates\",\n help=\"List a BROAD candidate range for a capability (vision/image-gen/video/audio-tts/audio-stt/embedding/rerank); caller/LLM picks, tool chooses no default.\",\n )\n candidates_parser.add_argument(\n \"--capability\", \"-c\", required=True,\n help=\"Capability or alias. One of: \" + \", \".join(CAPABILITY_SPECS) + \".\",\n )\n candidates_parser.add_argument(\"--source\", help=\"Models JSON URL, local JSON file, or '-' for stdin.\")\n candidates_parser.add_argument(\"--limit\", type=int, default=40)\n candidates_parser.add_argument(\"--json\", action=\"store_true\")\n candidates_parser.add_argument(\"--mine\", action=\"store_true\", help=\"Filter to models your token can actually call (needs the aihubmix CLI + login).\")\n candidates_parser.add_argument(\"--auto-install\", action=\"store_true\", help=\"With --mine: install the aihubmix CLI automatically if missing.\")\n candidates_parser.set_defaults(func=cmd_candidates)\n\n vision_parser = subparsers.add_parser(\n \"vision-candidates\",\n help=\"Alias for `candidates --capability vision`.\",\n )\n vision_parser.add_argument(\"--source\", help=\"Models JSON URL, local JSON file, or '-' for stdin.\")\n vision_parser.add_argument(\"--limit\", type=int, default=40)\n vision_parser.add_argument(\"--json\", action=\"store_true\")\n vision_parser.add_argument(\"--mine\", action=\"store_true\", help=\"Filter to models your token can actually call.\")\n vision_parser.add_argument(\"--auto-install\", action=\"store_true\", help=\"With --mine: install the aihubmix CLI automatically if missing.\")\n vision_parser.set_defaults(func=cmd_candidates, capability=\"vision\")\n\n install_cli_parser = subparsers.add_parser(\n \"install-cli\",\n help=\"Install the companion aihubmix CLI via its official per-OS installer.\",\n )\n install_cli_parser.add_argument(\"--force\", action=\"store_true\", help=\"Reinstall even if already present.\")\n install_cli_parser.set_defaults(func=cmd_install_cli)\n\n return parser\n\n\ndef main() -> int:\n parser = build_parser()\n args = parser.parse_args()\n global _LANG\n _LANG = (args.lang or os.environ.get(\"AIHUBMIX_LANG\") or \"en\").strip().lower()\n if _LANG not in (\"en\", \"zh\"):\n _LANG = \"en\"\n result = args.func(args)\n return result if isinstance(result, int) else 0\n\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\n"}}}}