返回 JoyAI-Echo
registry.py
1 """
2 Provider Registry — single source of truth for LLM provider metadata.
3
4 Adding a new provider:
5 1. Add a ProviderSpec to PROVIDERS below.
6 2. Add a field to ProvidersConfig in config/schema.py.
7 Done. Env vars, config matching, status display all derive from here.
8
9 Order matters — it controls match priority and fallback. Gateways first.
10 Every entry writes out all fields so you can copy-paste as a template.
11 """
12
13 from __future__ import annotations
14
15 from dataclasses import dataclass
16 from typing import Any
17
18 from pydantic.alias_generators import to_snake
19
20
21 @dataclass(frozen=True)
22 class ProviderSpec:
23 """One LLM provider's metadata. See PROVIDERS below for real examples.
24
25 Placeholders in env_extras values:
26 {api_key} — the user's API key
27 {api_base} — api_base from config, or this spec's default_api_base
28 """
29
30 # identity
31 name: str # config field name, e.g. "dashscope"
32 keywords: tuple[str, ...] # model-name keywords for matching (lowercase)
33 env_key: str # env var for API key, e.g. "DASHSCOPE_API_KEY"
34 display_name: str = "" # shown in `nanobot status`
35
36 # which provider implementation to use
37 # "openai_compat" | "anthropic" | "azure_openai" | "openai_codex" | "github_copilot"
38 backend: str = "openai_compat"
39
40 # extra env vars, e.g. (("ZHIPUAI_API_KEY", "{api_key}"),)
41 env_extras: tuple[tuple[str, str], ...] = ()
42
43 # gateway / local detection
44 is_gateway: bool = False # routes any model (OpenRouter, AiHubMix)
45 is_local: bool = False # local deployment (vLLM, Ollama)
46 detect_by_key_prefix: str = "" # match api_key prefix, e.g. "sk-or-"
47 detect_by_base_keyword: str = "" # match substring in api_base URL
48 default_api_base: str = "" # OpenAI-compatible base URL for this provider
49
50 # gateway behavior
51 strip_model_prefix: bool = False # strip "provider/" before sending to gateway
52 supports_max_completion_tokens: bool = False
53
54 # per-model param overrides, e.g. (("kimi-k2.5", {"temperature": 1.0}),)
55 model_overrides: tuple[tuple[str, dict[str, Any]], ...] = ()
56
57 # OAuth-based providers (e.g., OpenAI Codex) don't use API keys
58 is_oauth: bool = False
59
60 # Direct providers skip API-key validation (user supplies everything)
61 is_direct: bool = False
62
63 # Provider supports cache_control on content blocks (e.g. Anthropic prompt caching)
64 supports_prompt_caching: bool = False
65
66 # How to inject the thinking on/off toggle into extra_body.
67 # "" — no extra_body needed (default)
68 # "thinking_type" — {"thinking": {"type": "enabled"/"disabled"}}
69 # (DeepSeek, VolcEngine, BytePlus)
70 # "enable_thinking" — {"enable_thinking": true/false} (DashScope)
71 # "reasoning_split" — {"reasoning_split": true/false} (MiniMax)
72 thinking_style: str = ""
73
74 @property
75 def label(self) -> str:
76 return self.display_name or self.name.title()
77
78
79 # ---------------------------------------------------------------------------
80 # PROVIDERS — the registry. Order = priority. Copy any entry as template.
81 # ---------------------------------------------------------------------------
82
83 PROVIDERS: tuple[ProviderSpec, ...] = (
84 # === Custom (direct OpenAI-compatible endpoint) ========================
85 ProviderSpec(
86 name="custom",
87 keywords=(),
88 env_key="",
89 display_name="Custom",
90 backend="openai_compat",
91 is_direct=True,
92 ),
93
94 # === Azure OpenAI (direct API calls with API version 2024-10-21) =====
95 ProviderSpec(
96 name="azure_openai",
97 keywords=("azure", "azure-openai"),
98 env_key="",
99 display_name="Azure OpenAI",
100 backend="azure_openai",
101 is_direct=True,
102 ),
103 # === Gateways (detected by api_key / api_base, not model name) =========
104 # Gateways can route any model, so they win in fallback.
105 # OpenRouter: global gateway, keys start with "sk-or-"
106 ProviderSpec(
107 name="openrouter",
108 keywords=("openrouter",),
109 env_key="OPENROUTER_API_KEY",
110 display_name="OpenRouter",
111 backend="openai_compat",
112 is_gateway=True,
113 detect_by_key_prefix="sk-or-",
114 detect_by_base_keyword="openrouter",
115 default_api_base="https://openrouter.ai/api/v1",
116 supports_prompt_caching=True,
117 ),
118 # AiHubMix: global gateway, OpenAI-compatible interface.
119 # strip_model_prefix=True: doesn't understand "anthropic/claude-3",
120 # strips to bare "claude-3".
121 ProviderSpec(
122 name="aihubmix",
123 keywords=("aihubmix",),
124 env_key="OPENAI_API_KEY",
125 display_name="AiHubMix",
126 backend="openai_compat",
127 is_gateway=True,
128 detect_by_base_keyword="aihubmix",
129 default_api_base="https://aihubmix.com/v1",
130 strip_model_prefix=True,
131 ),
132 # SiliconFlow (硅基流动): OpenAI-compatible gateway, model names keep org prefix
133 ProviderSpec(
134 name="siliconflow",
135 keywords=("siliconflow",),
136 env_key="OPENAI_API_KEY",
137 display_name="SiliconFlow",
138 backend="openai_compat",
139 is_gateway=True,
140 detect_by_base_keyword="siliconflow",
141 default_api_base="https://api.siliconflow.cn/v1",
142 ),
143
144 # VolcEngine (火山引擎): OpenAI-compatible gateway, pay-per-use models
145 ProviderSpec(
146 name="volcengine",
147 keywords=("volcengine", "volces", "ark"),
148 env_key="OPENAI_API_KEY",
149 display_name="VolcEngine",
150 backend="openai_compat",
151 is_gateway=True,
152 detect_by_base_keyword="volces",
153 default_api_base="https://ark.cn-beijing.volces.com/api/v3",
154 thinking_style="thinking_type",
155 ),
156
157 # VolcEngine Coding Plan (火山引擎 Coding Plan): same key as volcengine
158 ProviderSpec(
159 name="volcengine_coding_plan",
160 keywords=("volcengine-plan",),
161 env_key="OPENAI_API_KEY",
162 display_name="VolcEngine Coding Plan",
163 backend="openai_compat",
164 is_gateway=True,
165 default_api_base="https://ark.cn-beijing.volces.com/api/coding/v3",
166 strip_model_prefix=True,
167 thinking_style="thinking_type",
168 ),
169
170 # BytePlus: VolcEngine international, pay-per-use models
171 ProviderSpec(
172 name="byteplus",
173 keywords=("byteplus",),
174 env_key="OPENAI_API_KEY",
175 display_name="BytePlus",
176 backend="openai_compat",
177 is_gateway=True,
178 detect_by_base_keyword="bytepluses",
179 default_api_base="https://ark.ap-southeast.bytepluses.com/api/v3",
180 strip_model_prefix=True,
181 thinking_style="thinking_type",
182 ),
183
184 # BytePlus Coding Plan: same key as byteplus
185 ProviderSpec(
186 name="byteplus_coding_plan",
187 keywords=("byteplus-plan",),
188 env_key="OPENAI_API_KEY",
189 display_name="BytePlus Coding Plan",
190 backend="openai_compat",
191 is_gateway=True,
192 default_api_base="https://ark.ap-southeast.bytepluses.com/api/coding/v3",
193 strip_model_prefix=True,
194 thinking_style="thinking_type",
195 ),
196
197
198 # === Standard providers (matched by model-name keywords) ===============
199 # Anthropic: native Anthropic SDK
200 ProviderSpec(
201 name="anthropic",
202 keywords=("anthropic", "claude"),
203 env_key="ANTHROPIC_API_KEY",
204 display_name="Anthropic",
205 backend="anthropic",
206 supports_prompt_caching=True,
207 ),
208 # OpenAI: SDK default base URL (no override needed)
209 ProviderSpec(
210 name="openai",
211 keywords=("openai", "gpt"),
212 env_key="OPENAI_API_KEY",
213 display_name="OpenAI",
214 backend="openai_compat",
215 supports_max_completion_tokens=True,
216 ),
217 # OpenAI Codex: OAuth-based, dedicated provider
218 ProviderSpec(
219 name="openai_codex",
220 keywords=("openai-codex",),
221 env_key="",
222 display_name="OpenAI Codex",
223 backend="openai_codex",
224 detect_by_base_keyword="codex",
225 default_api_base="https://chatgpt.com/backend-api",
226 is_oauth=True,
227 ),
228 # GitHub Copilot: OAuth-based
229 ProviderSpec(
230 name="github_copilot",
231 keywords=("github_copilot", "copilot"),
232 env_key="",
233 display_name="Github Copilot",
234 backend="github_copilot",
235 default_api_base="https://api.githubcopilot.com",
236 strip_model_prefix=True,
237 is_oauth=True,
238 supports_max_completion_tokens=True,
239 ),
240 # DeepSeek: OpenAI-compatible at api.deepseek.com
241 ProviderSpec(
242 name="deepseek",
243 keywords=("deepseek",),
244 env_key="DEEPSEEK_API_KEY",
245 display_name="DeepSeek",
246 backend="openai_compat",
247 default_api_base="https://api.deepseek.com",
248 thinking_style="thinking_type",
249 ),
250 # Gemini: Google's OpenAI-compatible endpoint
251 ProviderSpec(
252 name="gemini",
253 keywords=("gemini",),
254 env_key="GEMINI_API_KEY",
255 display_name="Gemini",
256 backend="openai_compat",
257 default_api_base="https://generativelanguage.googleapis.com/v1beta/openai/",
258 ),
259 # Zhipu (智谱): OpenAI-compatible at open.bigmodel.cn
260 ProviderSpec(
261 name="zhipu",
262 keywords=("zhipu", "glm", "zai"),
263 env_key="ZAI_API_KEY",
264 display_name="Zhipu AI",
265 backend="openai_compat",
266 env_extras=(("ZHIPUAI_API_KEY", "{api_key}"),),
267 default_api_base="https://open.bigmodel.cn/api/paas/v4",
268 ),
269 # DashScope (通义): Qwen models, OpenAI-compatible endpoint
270 ProviderSpec(
271 name="dashscope",
272 keywords=("qwen", "dashscope"),
273 env_key="DASHSCOPE_API_KEY",
274 display_name="DashScope",
275 backend="openai_compat",
276 default_api_base="https://dashscope.aliyuncs.com/compatible-mode/v1",
277 thinking_style="enable_thinking",
278 ),
279 # Moonshot (月之暗面): Kimi K2.5 / K2.6 enforce temperature >= 1.0.
280 ProviderSpec(
281 name="moonshot",
282 keywords=("moonshot", "kimi"),
283 env_key="MOONSHOT_API_KEY",
284 display_name="Moonshot",
285 backend="openai_compat",
286 default_api_base="https://api.moonshot.ai/v1",
287 model_overrides=(
288 ("kimi-k2.5", {"temperature": 1.0}),
289 ("kimi-k2.6", {"temperature": 1.0}),
290 ),
291 ),
292 # MiniMax: OpenAI-compatible API
293 ProviderSpec(
294 name="minimax",
295 keywords=("minimax",),
296 env_key="MINIMAX_API_KEY",
297 display_name="MiniMax",
298 backend="openai_compat",
299 default_api_base="https://api.minimax.io/v1",
300 thinking_style="reasoning_split",
301 ),
302 # MiniMax Anthropic-compatible endpoint: supports thinking mode
303 ProviderSpec(
304 name="minimax_anthropic",
305 keywords=("minimax_anthropic",),
306 env_key="MINIMAX_API_KEY",
307 display_name="MiniMax (Anthropic)",
308 backend="anthropic",
309 default_api_base="https://api.minimax.io/anthropic",
310 ),
311 # Mistral AI: OpenAI-compatible API
312 ProviderSpec(
313 name="mistral",
314 keywords=("mistral",),
315 env_key="MISTRAL_API_KEY",
316 display_name="Mistral",
317 backend="openai_compat",
318 default_api_base="https://api.mistral.ai/v1",
319 ),
320 # Step Fun (阶跃星辰): OpenAI-compatible API
321 ProviderSpec(
322 name="stepfun",
323 keywords=("stepfun", "step"),
324 env_key="STEPFUN_API_KEY",
325 display_name="Step Fun",
326 backend="openai_compat",
327 default_api_base="https://api.stepfun.com/v1",
328 ),
329 # Xiaomi MIMO (小米): OpenAI-compatible API
330 ProviderSpec(
331 name="xiaomi_mimo",
332 keywords=("xiaomi_mimo", "mimo"),
333 env_key="XIAOMIMIMO_API_KEY",
334 display_name="Xiaomi MIMO",
335 backend="openai_compat",
336 default_api_base="https://api.xiaomimimo.com/v1",
337 ),
338 # === Local deployment (matched by config key, NOT by api_base) =========
339 # vLLM / any OpenAI-compatible local server
340 ProviderSpec(
341 name="vllm",
342 keywords=("vllm",),
343 env_key="HOSTED_VLLM_API_KEY",
344 display_name="vLLM/Local",
345 backend="openai_compat",
346 is_local=True,
347 ),
348 # Ollama (local, OpenAI-compatible)
349 ProviderSpec(
350 name="ollama",
351 keywords=("ollama", "nemotron"),
352 env_key="OLLAMA_API_KEY",
353 display_name="Ollama",
354 backend="openai_compat",
355 is_local=True,
356 detect_by_base_keyword="11434",
357 default_api_base="http://localhost:11434/v1",
358 ),
359 # LM Studio (local, OpenAI-compatible)
360 ProviderSpec(
361 name="lm_studio",
362 keywords=("lm-studio", "lmstudio", "lm_studio"),
363 env_key="LM_STUDIO_API_KEY",
364 display_name="LM Studio",
365 backend="openai_compat",
366 is_local=True,
367 detect_by_base_keyword="1234",
368 default_api_base="http://localhost:1234/v1",
369 ),
370 # === OpenVINO Model Server (direct, local, OpenAI-compatible at /v3) ===
371 ProviderSpec(
372 name="ovms",
373 keywords=("openvino", "ovms"),
374 env_key="",
375 display_name="OpenVINO Model Server",
376 backend="openai_compat",
377 is_direct=True,
378 is_local=True,
379 default_api_base="http://localhost:8000/v3",
380 ),
381 # === Auxiliary (not a primary LLM provider) ============================
382 # Groq: mainly used for Whisper voice transcription, also usable for LLM
383 ProviderSpec(
384 name="groq",
385 keywords=("groq",),
386 env_key="GROQ_API_KEY",
387 display_name="Groq",
388 backend="openai_compat",
389 default_api_base="https://api.groq.com/openai/v1",
390 ),
391 # Qianfan (百度千帆): OpenAI-compatible API
392 ProviderSpec(
393 name="qianfan",
394 keywords=("qianfan", "ernie"),
395 env_key="QIANFAN_API_KEY",
396 display_name="Qianfan",
397 backend="openai_compat",
398 default_api_base="https://qianfan.baidubce.com/v2"
399 ),
400 )
401
402
403 # ---------------------------------------------------------------------------
404 # Lookup helpers
405 # ---------------------------------------------------------------------------
406
407
408 def find_by_name(name: str) -> ProviderSpec | None:
409 """Find a provider spec by config field name, e.g. "dashscope"."""
410 normalized = to_snake(name.replace("-", "_"))
411 for spec in PROVIDERS:
412 if spec.name == normalized:
413 return spec
414 return None
415
415 lines PYTHON