1 line
18 KiB
JSON
1 line
18 KiB
JSON
{"repo": "0xclaw-ai/0xclaw", "files": {"0xclaw/runtime/providers/registry.py": {"315f387b00178e379e88025fc9e75136ff067c37": {"t": 1785768969.7500997, "n": 16898, "h": "fd0cda25b3cde670", "c": "\"\"\"\nProvider Registry — single source of truth for LLM provider metadata.\n\nAdding a new provider:\n 1. Add a ProviderSpec to PROVIDERS below.\n 2. Add a field to ProvidersConfig in config/schema.py.\n Done. Env vars, prefixing, config matching, status display all derive from here.\n\nOrder matters — it controls match priority and fallback. Gateways first.\nEvery entry writes out all fields so you can copy-paste as a template.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom typing import Any\n\n\n@dataclass(frozen=True)\nclass ProviderSpec:\n \"\"\"One LLM provider's metadata. See PROVIDERS below for real examples.\n\n Placeholders in env_extras values:\n {api_key} — the user's API key\n {api_base} — api_base from config, or this spec's default_api_base\n \"\"\"\n\n # identity\n name: str # config field name, e.g. \"dashscope\"\n keywords: tuple[str, ...] # model-name keywords for matching (lowercase)\n env_key: str # LiteLLM env var, e.g. \"DASHSCOPE_API_KEY\"\n display_name: str = \"\" # shown in `0xclaw status`\n\n # model prefixing\n litellm_prefix: str = \"\" # \"dashscope\" → model becomes \"dashscope/{model}\"\n skip_prefixes: tuple[str, ...] = () # don't prefix if model already starts with these\n\n # extra env vars, e.g. ((\"ZHIPUAI_API_KEY\", \"{api_key}\"),)\n env_extras: tuple[tuple[str, str], ...] = ()\n\n # gateway / local detection\n is_gateway: bool = False # routes any model (OpenRouter, AiHubMix)\n is_local: bool = False # local deployment (vLLM, Ollama)\n detect_by_key_prefix: str = \"\" # match api_key prefix, e.g. \"sk-or-\"\n detect_by_base_keyword: str = \"\" # match substring in api_base URL\n default_api_base: str = \"\" # fallback base URL\n\n # gateway behavior\n strip_model_prefix: bool = False # strip \"provider/\" before re-prefixing\n\n # per-model param overrides, e.g. ((\"kimi-k2.5\", {\"temperature\": 1.0}),)\n model_overrides: tuple[tuple[str, dict[str, Any]], ...] = ()\n\n # OAuth-based providers (e.g., OpenAI Codex) don't use API keys\n is_oauth: bool = False # if True, uses OAuth flow instead of API key\n\n # Provider supports cache_control on content blocks (e.g. Anthropic prompt caching)\n supports_prompt_caching: bool = False\n\n @property\n def label(self) -> str:\n return self.display_name or self.name.title()\n\n\n# ---------------------------------------------------------------------------\n# PROVIDERS — the registry. Order = priority. Copy any entry as template.\n# ---------------------------------------------------------------------------\n\nPROVIDERS: tuple[ProviderSpec, ...] = (\n\n # === Custom (direct OpenAI-compatible endpoint, bypasses LiteLLM) ======\n ProviderSpec(\n name=\"custom\",\n keywords=(),\n env_key=\"\",\n display_name=\"Custom\",\n litellm_prefix=\"\",\n ),\n\n # === Gateways (detected by api_key / api_base, not model name) =========\n # Gateways can route any model, so they win in fallback.\n\n # FLock.io: decentralized AI model hub, OpenAI-compatible, privacy-preserving.\n # Auth uses x-litellm-api-key header (extra_headers in config).\n ProviderSpec(\n name=\"flock\",\n keywords=(\"flock\",),\n env_key=\"FLOCK_API_KEY\",\n display_name=\"FLock.io\",\n litellm_prefix=\"openai\",\n skip_prefixes=(),\n env_extras=(),\n is_gateway=True,\n is_local=False,\n detect_by_key_prefix=\"\",\n detect_by_base_keyword=\"flock.io\",\n default_api_base=\"https://api.flock.io/v1\",\n strip_model_prefix=True,\n model_overrides=(),\n ),\n\n # OpenRouter: global gateway, keys start with \"sk-or-\"\n ProviderSpec(\n name=\"openrouter\",\n keywords=(\"openrouter\",),\n env_key=\"OPENROUTER_API_KEY\",\n display_name=\"OpenRouter\",\n litellm_prefix=\"openrouter\", # claude-3 → openrouter/claude-3\n skip_prefixes=(),\n env_extras=(),\n is_gateway=True,\n is_local=False,\n detect_by_key_prefix=\"sk-or-\",\n detect_by_base_keyword=\"openrouter\",\n default_api_base=\"https://openrouter.ai/api/v1\",\n strip_model_prefix=False,\n model_overrides=(),\n supports_prompt_caching=True,\n ),\n\n # AiHubMix: global gateway, OpenAI-compatible interface.\n # strip_model_prefix=True: it doesn't understand \"anthropic/claude-3\",\n # so we strip to bare \"claude-3\" then re-prefix as \"openai/claude-3\".\n ProviderSpec(\n name=\"aihubmix\",\n keywords=(\"aihubmix\",),\n env_key=\"OPENAI_API_KEY\", # OpenAI-compatible\n display_name=\"AiHubMix\",\n litellm_prefix=\"openai\", # → openai/{model}\n skip_prefixes=(),\n env_extras=(),\n is_gateway=True,\n is_local=False,\n detect_by_key_prefix=\"\",\n detect_by_base_keyword=\"aihubmix\",\n default_api_base=\"https://aihubmix.com/v1\",\n strip_model_prefix=True, # anthropic/claude-3 → claude-3 → openai/claude-3\n model_overrides=(),\n ),\n\n # SiliconFlow (硅基流动): OpenAI-compatible gateway, model names keep org prefix\n ProviderSpec(\n name=\"siliconflow\",\n keywords=(\"siliconflow\",),\n env_key=\"OPENAI_API_KEY\",\n display_name=\"SiliconFlow\",\n litellm_prefix=\"openai\",\n skip_prefixes=(),\n env_extras=(),\n is_gateway=True,\n is_local=False,\n detect_by_key_prefix=\"\",\n detect_by_base_keyword=\"siliconflow\",\n default_api_base=\"https://api.siliconflow.cn/v1\",\n strip_model_prefix=False,\n model_overrides=(),\n ),\n\n # VolcEngine (火山引擎): OpenAI-compatible gateway\n ProviderSpec(\n name=\"volcengine\",\n keywords=(\"volcengine\", \"volces\", \"ark\"),\n env_key=\"OPENAI_API_KEY\",\n display_name=\"VolcEngine\",\n litellm_prefix=\"volcengine\",\n skip_prefixes=(),\n env_extras=(),\n is_gateway=True,\n is_local=False,\n detect_by_key_prefix=\"\",\n detect_by_base_keyword=\"volces\",\n default_api_base=\"https://ark.cn-beijing.volces.com/api/v3\",\n strip_model_prefix=False,\n model_overrides=(),\n ),\n\n # === Standard providers (matched by model-name keywords) ===============\n\n # Anthropic: LiteLLM recognizes \"claude-*\" natively, no prefix needed.\n ProviderSpec(\n name=\"anthropic\",\n keywords=(\"anthropic\", \"claude\"),\n env_key=\"ANTHROPIC_API_KEY\",\n display_name=\"Anthropic\",\n litellm_prefix=\"\",\n skip_prefixes=(),\n env_extras=(),\n is_gateway=False,\n is_local=False,\n detect_by_key_prefix=\"\",\n detect_by_base_keyword=\"\",\n default_api_base=\"\",\n strip_model_prefix=False,\n model_overrides=(),\n supports_prompt_caching=True,\n ),\n\n # OpenAI: LiteLLM recognizes \"gpt-*\" natively, no prefix needed.\n ProviderSpec(\n name=\"openai\",\n keywords=(\"openai\", \"gpt\"),\n env_key=\"OPENAI_API_KEY\",\n display_name=\"OpenAI\",\n litellm_prefix=\"\",\n skip_prefixes=(),\n env_extras=(),\n is_gateway=False,\n is_local=False,\n detect_by_key_prefix=\"\",\n detect_by_base_keyword=\"\",\n default_api_base=\"\",\n strip_model_prefix=False,\n model_overrides=(),\n ),\n\n # OpenAI Codex: uses OAuth, not API key.\n ProviderSpec(\n name=\"openai_codex\",\n keywords=(\"openai-codex\",),\n env_key=\"\", # OAuth-based, no API key\n display_name=\"OpenAI Codex\",\n litellm_prefix=\"\", # Not routed through LiteLLM\n skip_prefixes=(),\n env_extras=(),\n is_gateway=False,\n is_local=False,\n detect_by_key_prefix=\"\",\n detect_by_base_keyword=\"\",\n default_api_base=\"https://chatgpt.com/backend-api\",\n strip_model_prefix=False,\n model_overrides=(),\n is_oauth=True, # OAuth-based authentication\n ),\n\n # Github Copilot: uses OAuth, not API key.\n ProviderSpec(\n name=\"github_copilot\",\n keywords=(\"github_copilot\", \"copilot\"),\n env_key=\"\", # OAuth-based, no API key\n display_name=\"Github Copilot\",\n litellm_prefix=\"github_copilot\", # github_copilot/model → github_copilot/model\n skip_prefixes=(\"github_copilot/\",),\n env_extras=(),\n is_gateway=False,\n is_local=False,\n detect_by_key_prefix=\"\",\n detect_by_base_keyword=\"\",\n default_api_base=\"\",\n strip_model_prefix=False,\n model_overrides=(),\n is_oauth=True, # OAuth-based authentication\n ),\n\n # DeepSeek: needs \"deepseek/\" prefix for LiteLLM routing.\n ProviderSpec(\n name=\"deepseek\",\n keywords=(\"deepseek\",),\n env_key=\"DEEPSEEK_API_KEY\",\n display_name=\"DeepSeek\",\n litellm_prefix=\"deepseek\", # deepseek-chat → deepseek/deepseek-chat\n skip_prefixes=(\"deepseek/\",), # avoid double-prefix\n env_extras=(),\n is_gateway=False,\n is_local=False,\n detect_by_key_prefix=\"\",\n detect_by_base_keyword=\"\",\n default_api_base=\"\",\n strip_model_prefix=False,\n model_overrides=(),\n ),\n\n # Gemini: needs \"gemini/\" prefix for LiteLLM.\n ProviderSpec(\n name=\"gemini\",\n keywords=(\"gemini\",),\n env_key=\"GEMINI_API_KEY\",\n display_name=\"Gemini\",\n litellm_prefix=\"gemini\", # gemini-pro → gemini/gemini-pro\n skip_prefixes=(\"gemini/\",), # avoid double-prefix\n env_extras=(),\n is_gateway=False,\n is_local=False,\n detect_by_key_prefix=\"\",\n detect_by_base_keyword=\"\",\n default_api_base=\"\",\n strip_model_prefix=False,\n model_overrides=(),\n ),\n\n # Z.ai (international): OpenAI-compatible API, routed as openai/<model> with custom api_base.\n # is_gateway=True so find_gateway() picks it up when provider=\"zhipu\" is set explicitly.\n # Auth: standard Bearer token via api_key kwarg (not a custom header like FLock).\n ProviderSpec(\n name=\"zhipu\",\n keywords=(\"zhipu\", \"glm\", \"zai\"),\n env_key=\"ZAI_API_KEY\",\n display_name=\"Z.ai\",\n litellm_prefix=\"openai\", # glm-4.7 → openai/glm-4.7 + api_base override\n skip_prefixes=(\"openai/\", \"zhipu/\", \"zai/\", \"openrouter/\", \"hosted_vllm/\"),\n env_extras=(\n (\"ZHIPUAI_API_KEY\", \"{api_key}\"),\n ),\n is_gateway=True,\n is_local=False,\n detect_by_key_prefix=\"\",\n detect_by_base_keyword=\"z.ai\",\n default_api_base=\"https://api.z.ai/api/paas/v4\",\n strip_model_prefix=True,\n model_overrides=(),\n ),\n\n # DashScope: Qwen models, needs \"dashscope/\" prefix.\n ProviderSpec(\n name=\"dashscope\",\n keywords=(\"qwen\", \"dashscope\"),\n env_key=\"DASHSCOPE_API_KEY\",\n display_name=\"DashScope\",\n litellm_prefix=\"dashscope\", # qwen-max → dashscope/qwen-max\n skip_prefixes=(\"dashscope/\", \"openrouter/\"),\n env_extras=(),\n is_gateway=False,\n is_local=False,\n detect_by_key_prefix=\"\",\n detect_by_base_keyword=\"\",\n default_api_base=\"\",\n strip_model_prefix=False,\n model_overrides=(),\n ),\n\n # Moonshot: Kimi models, needs \"moonshot/\" prefix.\n # LiteLLM requires MOONSHOT_API_BASE env var to find the endpoint.\n # Kimi K2.5 API enforces temperature >= 1.0.\n ProviderSpec(\n name=\"moonshot\",\n keywords=(\"moonshot\", \"kimi\"),\n env_key=\"MOONSHOT_API_KEY\",\n display_name=\"Moonshot\",\n litellm_prefix=\"moonshot\", # kimi-k2.5 → moonshot/kimi-k2.5\n skip_prefixes=(\"moonshot/\", \"openrouter/\"),\n env_extras=(\n (\"MOONSHOT_API_BASE\", \"{api_base}\"),\n ),\n is_gateway=False,\n is_local=False,\n detect_by_key_prefix=\"\",\n detect_by_base_keyword=\"\",\n default_api_base=\"https://api.moonshot.ai/v1\", # intl; use api.moonshot.cn for China\n strip_model_prefix=False,\n model_overrides=(\n (\"kimi-k2.5\", {\"temperature\": 1.0}),\n ),\n ),\n\n # MiniMax: needs \"minimax/\" prefix for LiteLLM routing.\n # Uses OpenAI-compatible API at api.minimax.io/v1.\n ProviderSpec(\n name=\"minimax\",\n keywords=(\"minimax\",),\n env_key=\"MINIMAX_API_KEY\",\n display_name=\"MiniMax\",\n litellm_prefix=\"minimax\", # MiniMax-M2.1 → minimax/MiniMax-M2.1\n skip_prefixes=(\"minimax/\", \"openrouter/\"),\n env_extras=(),\n is_gateway=False,\n is_local=False,\n detect_by_key_prefix=\"\",\n detect_by_base_keyword=\"\",\n default_api_base=\"https://api.minimax.io/v1\",\n strip_model_prefix=False,\n model_overrides=(),\n ),\n\n # === Local deployment (matched by config key, NOT by api_base) =========\n\n # vLLM / any OpenAI-compatible local server.\n # Detected when config key is \"vllm\" (provider_name=\"vllm\").\n ProviderSpec(\n name=\"vllm\",\n keywords=(\"vllm\",),\n env_key=\"HOSTED_VLLM_API_KEY\",\n display_name=\"vLLM/Local\",\n litellm_prefix=\"hosted_vllm\", # Llama-3-8B → hosted_vllm/Llama-3-8B\n skip_prefixes=(),\n env_extras=(),\n is_gateway=False,\n is_local=True,\n detect_by_key_prefix=\"\",\n detect_by_base_keyword=\"\",\n default_api_base=\"\", # user must provide in config\n strip_model_prefix=False,\n model_overrides=(),\n ),\n\n # === Auxiliary (not a primary LLM provider) ============================\n\n # Groq: mainly used for Whisper voice transcription, also usable for LLM.\n # Needs \"groq/\" prefix for LiteLLM routing. Placed last — it rarely wins fallback.\n ProviderSpec(\n name=\"groq\",\n keywords=(\"groq\",),\n env_key=\"GROQ_API_KEY\",\n display_name=\"Groq\",\n litellm_prefix=\"groq\", # llama3-8b-8192 → groq/llama3-8b-8192\n skip_prefixes=(\"groq/\",), # avoid double-prefix\n env_extras=(),\n is_gateway=False,\n is_local=False,\n detect_by_key_prefix=\"\",\n detect_by_base_keyword=\"\",\n default_api_base=\"\",\n strip_model_prefix=False,\n model_overrides=(),\n ),\n)\n\n\n# ---------------------------------------------------------------------------\n# Lookup helpers\n# ---------------------------------------------------------------------------\n\ndef find_by_model(model: str) -> ProviderSpec | None:\n \"\"\"Match a standard provider by model-name keyword (case-insensitive).\n Skips gateways/local — those are matched by api_key/api_base instead.\"\"\"\n model_lower = model.lower()\n model_normalized = model_lower.replace(\"-\", \"_\")\n model_prefix = model_lower.split(\"/\", 1)[0] if \"/\" in model_lower else \"\"\n normalized_prefix = model_prefix.replace(\"-\", \"_\")\n std_specs = [s for s in PROVIDERS if not s.is_gateway and not s.is_local]\n\n # Prefer explicit provider prefix — prevents `github-copilot/...codex` matching openai_codex.\n for spec in std_specs:\n if model_prefix and normalized_prefix == spec.name:\n return spec\n\n for spec in std_specs:\n if any(kw in model_lower or kw.replace(\"-\", \"_\") in model_normalized for kw in spec.keywords):\n return spec\n return None\n\n\ndef find_gateway(\n provider_name: str | None = None,\n api_key: str | None = None,\n api_base: str | None = None,\n) -> ProviderSpec | None:\n \"\"\"Detect gateway/local provider.\n\n Priority:\n 1. provider_name — if it maps to a gateway/local spec, use it directly.\n 2. api_key prefix — e.g. \"sk-or-\" → OpenRouter.\n 3. api_base keyword — e.g. \"aihubmix\" in URL → AiHubMix.\n\n A standard provider with a custom api_base (e.g. DeepSeek behind a proxy)\n will NOT be mistaken for vLLM — the old fallback is gone.\n \"\"\"\n # 1. Direct match by config key\n if provider_name:\n spec = find_by_name(provider_name)\n if spec and (spec.is_gateway or spec.is_local):\n return spec\n\n # 2. Auto-detect by api_key prefix / api_base keyword\n for spec in PROVIDERS:\n if spec.detect_by_key_prefix and api_key and api_key.startswith(spec.detect_by_key_prefix):\n return spec\n if spec.detect_by_base_keyword and api_base and spec.detect_by_base_keyword in api_base:\n return spec\n\n return None\n\n\ndef find_by_name(name: str) -> ProviderSpec | None:\n \"\"\"Find a provider spec by config field name, e.g. \"dashscope\".\"\"\"\n for spec in PROVIDERS:\n if spec.name == name:\n return spec\n return None\n"}}}} |