Files
wehub-resource-sync e4dcfc49aa
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:00:43 +08:00

504 lines
16 KiB
Python

"""Provider registry for DeepTutor LLM routing.
Single source of truth for provider metadata. Adding a new provider:
1. Add a ProviderSpec to PROVIDERS below.
Done. Env vars, config matching, status display all derive from here.
Order matters — it controls match priority and fallback. Gateways first.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from pydantic.alias_generators import to_snake
@dataclass(frozen=True)
class ProviderSpec:
"""Single provider metadata entry.
Placeholders in env_extras values:
{api_key} — the user's API key
{api_base} — api_base from config, or this spec's default_api_base
"""
name: str
keywords: tuple[str, ...]
env_key: str
display_name: str = ""
# Which provider implementation to use:
# "openai_compat" | "anthropic" | "azure_openai" | "openai_codex" | "github_copilot"
backend: str = "openai_compat"
env_extras: tuple[tuple[str, str], ...] = ()
is_gateway: bool = False
is_local: bool = False
detect_by_key_prefix: str = ""
detect_by_base_keyword: str = ""
default_api_base: str = ""
strip_model_prefix: bool = False
supports_max_completion_tokens: bool = False
supports_prompt_caching: bool = False
supports_stream_options: bool = True
model_overrides: tuple[tuple[str, dict[str, Any]], ...] = ()
is_oauth: bool = False
is_direct: bool = False
thinking_style: str = ""
# Substring patterns (case-insensitive) marking models whose native
# reasoning trace should be surfaced. When the caller does not pass an
# explicit reasoning_effort, the provider auto-injects "high" so the
# thinking_style flag (e.g. extra_body.thinking.type=enabled) is sent.
reasoning_model_patterns: tuple[str, ...] = ()
@property
def mode(self) -> str:
if self.is_oauth:
return "oauth"
if self.is_direct:
return "direct"
if self.is_gateway:
return "gateway"
if self.is_local:
return "local"
return "standard"
@property
def label(self) -> str:
return self.display_name or self.name.title()
PROVIDER_ALIASES = {
"azure": "azure_openai",
"azure-openai": "azure_openai",
"azureopenai": "azure_openai",
"google": "gemini",
"google_genai": "gemini",
"claude": "anthropic",
"openai_compatible": "custom",
"openai-compatible": "custom",
"anthropic_compatible": "custom_anthropic",
"anthropic-compatible": "custom_anthropic",
"volcenginecodingplan": "volcengine_coding_plan",
"volcengineCodingPlan": "volcengine_coding_plan",
"bytepluscodingplan": "byteplus_coding_plan",
"byteplusCodingPlan": "byteplus_coding_plan",
"github-copilot": "github_copilot",
"openai-codex": "openai_codex",
"lm-studio": "lm_studio",
}
def canonical_provider_name(name: str | None) -> str | None:
"""Normalize incoming provider names and legacy aliases."""
if not name:
return None
key = name.strip()
if not key:
return None
key = to_snake(key.replace("-", "_"))
return PROVIDER_ALIASES.get(key, key)
# ---------------------------------------------------------------------------
# PROVIDERS — the registry. Order = priority.
# ---------------------------------------------------------------------------
PROVIDERS: tuple[ProviderSpec, ...] = (
# === Direct (user supplies everything, no auto-detection) ===============
ProviderSpec(
name="custom",
keywords=(),
env_key="",
display_name="Custom",
backend="openai_compat",
is_direct=True,
),
ProviderSpec(
name="custom_anthropic",
keywords=(),
env_key="",
display_name="Custom (Anthropic API)",
backend="anthropic",
is_direct=True,
),
ProviderSpec(
name="azure_openai",
keywords=("azure", "azure_openai"),
env_key="",
display_name="Azure OpenAI",
backend="azure_openai",
is_direct=True,
),
# === Gateways (detected by api_key / api_base, route any model) ========
ProviderSpec(
name="openrouter",
keywords=("openrouter",),
env_key="OPENROUTER_API_KEY",
display_name="OpenRouter",
backend="openai_compat",
is_gateway=True,
detect_by_key_prefix="sk-or-",
detect_by_base_keyword="openrouter",
default_api_base="https://openrouter.ai/api/v1",
supports_prompt_caching=True,
),
ProviderSpec(
name="aihubmix",
keywords=("aihubmix",),
env_key="OPENAI_API_KEY",
display_name="AiHubMix",
backend="openai_compat",
is_gateway=True,
detect_by_base_keyword="aihubmix",
default_api_base="https://aihubmix.com/v1",
strip_model_prefix=True,
),
ProviderSpec(
name="siliconflow",
keywords=("siliconflow",),
env_key="OPENAI_API_KEY",
display_name="SiliconFlow",
backend="openai_compat",
is_gateway=True,
detect_by_base_keyword="siliconflow",
default_api_base="https://api.siliconflow.cn/v1",
),
ProviderSpec(
name="volcengine",
keywords=("volcengine", "volces", "ark"),
env_key="OPENAI_API_KEY",
display_name="VolcEngine",
backend="openai_compat",
is_gateway=True,
detect_by_base_keyword="volces",
default_api_base="https://ark.cn-beijing.volces.com/api/v3",
thinking_style="thinking_type",
),
ProviderSpec(
name="volcengine_coding_plan",
keywords=("volcengine-plan",),
env_key="OPENAI_API_KEY",
display_name="VolcEngine Coding Plan",
backend="openai_compat",
is_gateway=True,
default_api_base="https://ark.cn-beijing.volces.com/api/coding/v3",
strip_model_prefix=True,
thinking_style="thinking_type",
),
ProviderSpec(
name="byteplus",
keywords=("byteplus",),
env_key="OPENAI_API_KEY",
display_name="BytePlus",
backend="openai_compat",
is_gateway=True,
detect_by_base_keyword="bytepluses",
default_api_base="https://ark.ap-southeast.bytepluses.com/api/v3",
strip_model_prefix=True,
thinking_style="thinking_type",
),
ProviderSpec(
name="byteplus_coding_plan",
keywords=("byteplus-plan",),
env_key="OPENAI_API_KEY",
display_name="BytePlus Coding Plan",
backend="openai_compat",
is_gateway=True,
default_api_base="https://ark.ap-southeast.bytepluses.com/api/coding/v3",
strip_model_prefix=True,
thinking_style="thinking_type",
),
# === Standard providers (matched by model-name keywords) ===============
ProviderSpec(
name="anthropic",
keywords=("anthropic", "claude"),
env_key="ANTHROPIC_API_KEY",
display_name="Anthropic",
backend="anthropic",
default_api_base="https://api.anthropic.com/v1",
supports_prompt_caching=True,
),
ProviderSpec(
name="openai",
keywords=("openai", "gpt"),
env_key="OPENAI_API_KEY",
display_name="OpenAI",
backend="openai_compat",
default_api_base="https://api.openai.com/v1",
supports_max_completion_tokens=True,
),
ProviderSpec(
name="openai_codex",
keywords=("openai-codex",),
env_key="",
display_name="OpenAI Codex",
backend="openai_codex",
detect_by_base_keyword="codex",
is_oauth=True,
default_api_base="https://chatgpt.com/backend-api",
),
ProviderSpec(
name="github_copilot",
keywords=("github_copilot", "copilot"),
env_key="",
display_name="GitHub Copilot",
backend="github_copilot",
is_oauth=True,
default_api_base="https://api.githubcopilot.com",
strip_model_prefix=True,
supports_max_completion_tokens=True,
),
ProviderSpec(
name="deepseek",
keywords=("deepseek",),
env_key="DEEPSEEK_API_KEY",
display_name="DeepSeek",
backend="openai_compat",
default_api_base="https://api.deepseek.com",
thinking_style="thinking_type",
reasoning_model_patterns=("deepseek-v4-pro", "deepseek-reasoner"),
),
ProviderSpec(
name="gemini",
keywords=("gemini",),
env_key="GEMINI_API_KEY",
display_name="Gemini",
backend="openai_compat",
default_api_base="https://generativelanguage.googleapis.com/v1beta/openai/",
),
ProviderSpec(
name="zhipu",
keywords=("zhipu", "glm", "zai"),
env_key="ZAI_API_KEY",
display_name="Zhipu AI",
backend="openai_compat",
env_extras=(("ZHIPUAI_API_KEY", "{api_key}"),),
default_api_base="https://open.bigmodel.cn/api/paas/v4",
),
ProviderSpec(
name="dashscope",
keywords=("qwen", "dashscope"),
env_key="DASHSCOPE_API_KEY",
display_name="DashScope",
backend="openai_compat",
default_api_base="https://dashscope.aliyuncs.com/compatible-mode/v1",
thinking_style="enable_thinking",
),
ProviderSpec(
name="moonshot",
keywords=("moonshot", "kimi"),
env_key="MOONSHOT_API_KEY",
display_name="Moonshot",
backend="openai_compat",
default_api_base="https://api.moonshot.cn/v1",
model_overrides=(
("kimi-k2.5", {"temperature": 1.0}),
("kimi-k2.6", {"temperature": 1.0}),
),
),
ProviderSpec(
name="minimax",
keywords=("minimax",),
env_key="MINIMAX_API_KEY",
display_name="MiniMax",
backend="openai_compat",
default_api_base="https://api.minimaxi.com/v1",
thinking_style="reasoning_split",
),
ProviderSpec(
name="minimax_anthropic",
keywords=("minimax_anthropic",),
env_key="MINIMAX_API_KEY",
display_name="MiniMax (Anthropic)",
backend="anthropic",
default_api_base="https://api.minimaxi.com/anthropic",
),
ProviderSpec(
name="mistral",
keywords=("mistral",),
env_key="MISTRAL_API_KEY",
display_name="Mistral",
backend="openai_compat",
default_api_base="https://api.mistral.ai/v1",
),
ProviderSpec(
name="stepfun",
keywords=("stepfun", "step"),
env_key="STEPFUN_API_KEY",
display_name="Step Fun",
backend="openai_compat",
default_api_base="https://api.stepfun.com/v1",
),
ProviderSpec(
name="xiaomi_mimo",
keywords=("xiaomi_mimo", "mimo"),
env_key="XIAOMIMIMO_API_KEY",
display_name="Xiaomi MIMO",
backend="openai_compat",
default_api_base="https://api.xiaomimimo.com/v1",
),
# === Local deployment ==================================================
ProviderSpec(
name="vllm",
keywords=("vllm",),
env_key="HOSTED_VLLM_API_KEY",
display_name="vLLM/Local",
backend="openai_compat",
is_local=True,
),
ProviderSpec(
name="ollama",
keywords=("ollama", "nemotron"),
env_key="OLLAMA_API_KEY",
display_name="Ollama",
backend="openai_compat",
is_local=True,
detect_by_base_keyword="11434",
default_api_base="http://localhost:11434/v1",
),
ProviderSpec(
name="lm_studio",
keywords=("lm-studio", "lmstudio", "lm_studio"),
env_key="LM_STUDIO_API_KEY",
display_name="LM Studio",
backend="openai_compat",
is_local=True,
detect_by_base_keyword="1234",
default_api_base="http://localhost:1234/v1",
),
ProviderSpec(
name="llama_cpp",
keywords=("llama_cpp", "llama.cpp"),
env_key="",
display_name="llama.cpp",
backend="openai_compat",
is_local=True,
detect_by_base_keyword="8080",
default_api_base="http://localhost:8080/v1",
),
ProviderSpec(
name="lemonade",
keywords=("lemonade",),
env_key="LEMONADE_API_KEY",
display_name="Lemonade",
backend="openai_compat",
is_local=True,
detect_by_base_keyword="13305",
default_api_base="http://localhost:13305/api/v1",
),
ProviderSpec(
name="ovms",
keywords=("openvino", "ovms"),
env_key="",
display_name="OpenVINO Model Server",
backend="openai_compat",
is_direct=True,
is_local=True,
default_api_base="http://localhost:8000/v3",
),
# === Auxiliary ==========================================================
ProviderSpec(
name="nvidia_nim",
keywords=("nvidia_nim", "nvidia-nim", "nim"),
env_key="NVIDIA_NIM_API_KEY",
display_name="NVIDIA NIM",
backend="openai_compat",
is_gateway=True,
detect_by_key_prefix="nvapi-",
detect_by_base_keyword="api.nvidia.com",
default_api_base="https://integrate.api.nvidia.com/v1",
supports_stream_options=False,
),
ProviderSpec(
name="groq",
keywords=("groq",),
env_key="GROQ_API_KEY",
display_name="Groq",
backend="openai_compat",
default_api_base="https://api.groq.com/openai/v1",
),
ProviderSpec(
name="qianfan",
keywords=("qianfan", "ernie"),
env_key="QIANFAN_API_KEY",
display_name="Qianfan",
backend="openai_compat",
default_api_base="https://qianfan.baidubce.com/v2",
),
)
NANOBOT_LLM_PROVIDERS: tuple[str, ...] = tuple(spec.name for spec in PROVIDERS)
def find_by_name(name: str | None) -> ProviderSpec | None:
canonical = canonical_provider_name(name)
if not canonical:
return None
for spec in PROVIDERS:
if spec.name == canonical:
return spec
return None
def find_by_model(model: str | None) -> ProviderSpec | None:
if not model:
return None
model_lower = model.lower()
model_normalized = model_lower.replace("-", "_")
model_prefix = model_lower.split("/", 1)[0] if "/" in model_lower else ""
normalized_prefix = model_prefix.replace("-", "_")
standard_specs = [s for s in PROVIDERS if not s.is_gateway and not s.is_local]
for spec in standard_specs:
if model_prefix and normalized_prefix == spec.name:
return spec
for spec in standard_specs:
if any(
kw in model_lower or kw.replace("-", "_") in model_normalized for kw in spec.keywords
):
return spec
return None
def find_gateway(
provider_name: str | None = None,
api_key: str | None = None,
api_base: str | None = None,
) -> ProviderSpec | None:
spec = find_by_name(provider_name)
if spec and (spec.is_gateway or spec.is_local):
return spec
for spec in PROVIDERS:
if spec.detect_by_key_prefix and api_key and api_key.startswith(spec.detect_by_key_prefix):
return spec
if spec.detect_by_base_keyword and api_base and spec.detect_by_base_keyword in api_base:
return spec
return None
def strip_provider_prefix(model: str, spec: ProviderSpec | None) -> str:
"""Strip the provider/ prefix from a model name if applicable."""
if not model or not spec:
return model
if spec.strip_model_prefix and "/" in model:
return model.split("/", 1)[1]
return model
__all__ = [
"ProviderSpec",
"PROVIDERS",
"NANOBOT_LLM_PROVIDERS",
"PROVIDER_ALIASES",
"canonical_provider_name",
"find_by_name",
"find_by_model",
"find_gateway",
"strip_provider_prefix",
]