mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-05-06 20:42:43 +08:00
align llm provider registry with opencode endpoints
This commit is contained in:
@@ -768,12 +768,25 @@ class LLMHelper:
|
||||
{"id": model_id, "name": model_id}
|
||||
for model_id in await self._get_google_models(api_key or "")
|
||||
]
|
||||
model_list_base_url = base_url
|
||||
try:
|
||||
from app.agent.llm.provider import LLMProviderManager
|
||||
|
||||
model_list_base_url = (
|
||||
LLMProviderManager().resolve_model_list_base_url(
|
||||
provider_id=provider,
|
||||
base_url=base_url,
|
||||
)
|
||||
or base_url
|
||||
)
|
||||
except Exception:
|
||||
model_list_base_url = base_url
|
||||
return [
|
||||
{"id": model_id, "name": model_id}
|
||||
for model_id in await self._get_openai_compatible_models(
|
||||
provider,
|
||||
api_key or "",
|
||||
base_url,
|
||||
model_list_base_url,
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@@ -44,6 +44,16 @@ class ProviderAuthMethod:
|
||||
description: str = ""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProviderUrlPreset:
|
||||
"""前端展示用的 Base URL 预设。"""
|
||||
|
||||
label: str
|
||||
value: str
|
||||
model_list_base_url: Optional[str] = None
|
||||
models_dev_provider_id: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProviderSpec:
|
||||
"""描述一个可接入的 LLM provider。"""
|
||||
@@ -53,6 +63,7 @@ class ProviderSpec:
|
||||
runtime: str
|
||||
models_dev_provider_id: Optional[str] = None
|
||||
default_base_url: Optional[str] = None
|
||||
base_url_presets: Tuple[ProviderUrlPreset, ...] = ()
|
||||
base_url_editable: bool = False
|
||||
requires_base_url: bool = False
|
||||
supports_api_key: bool = True
|
||||
@@ -138,7 +149,158 @@ class LLMProviderManager(metaclass=Singleton):
|
||||
label="设备码授权",
|
||||
description="适合无回调环境,复制设备码到浏览器完成登录。",
|
||||
)
|
||||
return (
|
||||
url_preset = ProviderUrlPreset
|
||||
def openai_provider(
|
||||
provider_id: str,
|
||||
name: str,
|
||||
default_base_url: str,
|
||||
sort_order: int,
|
||||
*,
|
||||
models_dev_provider_id: Optional[str] = None,
|
||||
base_url_presets: Tuple[ProviderUrlPreset, ...] = (),
|
||||
api_key_hint: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
model_list_strategy: str = "openai_compatible",
|
||||
api_key_label: str = "API Key",
|
||||
) -> ProviderSpec:
|
||||
return ProviderSpec(
|
||||
id=provider_id,
|
||||
name=name,
|
||||
runtime="openai_compatible",
|
||||
models_dev_provider_id=models_dev_provider_id or provider_id,
|
||||
default_base_url=default_base_url,
|
||||
base_url_presets=base_url_presets,
|
||||
api_key_label=api_key_label,
|
||||
api_key_hint=api_key_hint or f"填写 {name} API Key。",
|
||||
model_list_strategy=model_list_strategy,
|
||||
description=description or f"{name} OpenAI-compatible 端点。",
|
||||
sort_order=sort_order,
|
||||
)
|
||||
|
||||
def catalog_openai_provider(
|
||||
provider_id: str,
|
||||
name: str,
|
||||
default_base_url: str,
|
||||
sort_order: int,
|
||||
*,
|
||||
models_dev_provider_id: Optional[str] = None,
|
||||
base_url_presets: Tuple[ProviderUrlPreset, ...] = (),
|
||||
api_key_hint: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
api_key_label: str = "API Key",
|
||||
) -> ProviderSpec:
|
||||
return openai_provider(
|
||||
provider_id=provider_id,
|
||||
name=name,
|
||||
default_base_url=default_base_url,
|
||||
sort_order=sort_order,
|
||||
models_dev_provider_id=models_dev_provider_id,
|
||||
base_url_presets=base_url_presets,
|
||||
api_key_hint=api_key_hint,
|
||||
description=description,
|
||||
model_list_strategy="models_dev_only",
|
||||
api_key_label=api_key_label,
|
||||
)
|
||||
|
||||
def anthropic_provider(
|
||||
provider_id: str,
|
||||
name: str,
|
||||
default_base_url: str,
|
||||
sort_order: int,
|
||||
*,
|
||||
models_dev_provider_id: Optional[str] = None,
|
||||
base_url_presets: Tuple[ProviderUrlPreset, ...] = (),
|
||||
api_key_hint: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
) -> ProviderSpec:
|
||||
return ProviderSpec(
|
||||
id=provider_id,
|
||||
name=name,
|
||||
runtime="anthropic_compatible",
|
||||
models_dev_provider_id=models_dev_provider_id or provider_id,
|
||||
default_base_url=default_base_url,
|
||||
base_url_presets=base_url_presets,
|
||||
api_key_hint=api_key_hint or f"填写 {name} API Key。",
|
||||
model_list_strategy="anthropic_compatible",
|
||||
description=description or f"{name} Anthropic-compatible 端点。",
|
||||
sort_order=sort_order,
|
||||
)
|
||||
|
||||
catalog_openai_providers = (
|
||||
("302ai", "302.AI", "https://api.302.ai/v1"),
|
||||
("abacus", "Abacus", "https://routellm.abacus.ai/v1"),
|
||||
("abliteration-ai", "abliteration.ai", "https://api.abliteration.ai/v1"),
|
||||
("baseten", "Baseten", "https://inference.baseten.co/v1"),
|
||||
("berget", "Berget.AI", "https://api.berget.ai/v1"),
|
||||
("chutes", "Chutes", "https://llm.chutes.ai/v1"),
|
||||
("clarifai", "Clarifai", "https://api.clarifai.com/v2/ext/openai/v1"),
|
||||
("cloudferro-sherlock", "CloudFerro Sherlock", "https://api-sherlock.cloudferro.com/openai/v1/"),
|
||||
("cloudflare-workers-ai", "Cloudflare Workers AI", "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1"),
|
||||
("cortecs", "Cortecs", "https://api.cortecs.ai/v1"),
|
||||
("digitalocean", "DigitalOcean", "https://inference.do-ai.run/v1"),
|
||||
("dinference", "DInference", "https://api.dinference.com/v1"),
|
||||
("drun", "D.Run (China)", "https://chat.d.run/v1"),
|
||||
("evroc", "evroc", "https://models.think.evroc.com/v1"),
|
||||
("fastrouter", "FastRouter", "https://go.fastrouter.ai/api/v1"),
|
||||
("fireworks-ai", "Fireworks AI", "https://api.fireworks.ai/inference/v1/"),
|
||||
("firmware", "Firmware", "https://app.frogbot.ai/api/v1"),
|
||||
("friendli", "Friendli", "https://api.friendli.ai/serverless/v1"),
|
||||
("helicone", "Helicone", "https://ai-gateway.helicone.ai/v1"),
|
||||
("hpc-ai", "HPC-AI", "https://api.hpc-ai.com/inference/v1"),
|
||||
("huggingface", "Hugging Face", "https://router.huggingface.co/v1"),
|
||||
("iflowcn", "iFlow", "https://apis.iflow.cn/v1"),
|
||||
("inception", "Inception", "https://api.inceptionlabs.ai/v1/"),
|
||||
("inference", "Inference", "https://inference.net/v1"),
|
||||
("io-net", "IO.NET", "https://api.intelligence.io.solutions/api/v1"),
|
||||
("jiekou", "Jiekou.AI", "https://api.jiekou.ai/openai"),
|
||||
("kilo", "Kilo Gateway", "https://api.kilo.ai/api/gateway"),
|
||||
("kuae-cloud-coding-plan", "KUAE Cloud Coding Plan", "https://coding-plan-endpoint.kuaecloud.net/v1"),
|
||||
("llama", "Llama", "https://api.llama.com/compat/v1/"),
|
||||
("llmgateway", "LLM Gateway", "https://api.llmgateway.io/v1"),
|
||||
("lucidquery", "LucidQuery AI", "https://lucidquery.com/api/v1"),
|
||||
("meganova", "Meganova", "https://api.meganova.ai/v1"),
|
||||
("mixlayer", "Mixlayer", "https://models.mixlayer.ai/v1"),
|
||||
("moark", "Moark", "https://moark.com/v1"),
|
||||
("modelscope", "ModelScope", "https://api-inference.modelscope.cn/v1"),
|
||||
("morph", "Morph", "https://api.morphllm.com/v1"),
|
||||
("nano-gpt", "NanoGPT", "https://nano-gpt.com/api/v1"),
|
||||
("nebius", "Nebius Token Factory", "https://api.tokenfactory.nebius.com/v1"),
|
||||
("neuralwatt", "Neuralwatt", "https://api.neuralwatt.com/v1"),
|
||||
("nova", "Nova", "https://api.nova.amazon.com/v1"),
|
||||
("novita-ai", "NovitaAI", "https://api.novita.ai/openai"),
|
||||
("ovhcloud", "OVHcloud AI Endpoints", "https://oai.endpoints.kepler.ai.cloud.ovh.net/v1"),
|
||||
("perplexity-agent", "Perplexity Agent", "https://api.perplexity.ai/v1"),
|
||||
("poe", "Poe", "https://api.poe.com/v1"),
|
||||
("privatemode-ai", "Privatemode AI", "http://localhost:8080/v1"),
|
||||
("qihang-ai", "QiHang", "https://api.qhaigc.net/v1"),
|
||||
("qiniu-ai", "Qiniu", "https://api.qnaigc.com/v1"),
|
||||
("regolo-ai", "Regolo AI", "https://api.regolo.ai/v1"),
|
||||
("requesty", "Requesty", "https://router.requesty.ai/v1"),
|
||||
("scaleway", "Scaleway", "https://api.scaleway.ai/v1"),
|
||||
("stackit", "STACKIT", "https://api.openai-compat.model-serving.eu01.onstackit.cloud/v1"),
|
||||
("stepfun", "StepFun", "https://api.stepfun.com/v1"),
|
||||
("submodel", "submodel", "https://llm.submodel.ai/v1"),
|
||||
("synthetic", "Synthetic", "https://api.synthetic.new/openai/v1"),
|
||||
("the-grid-ai", "The Grid AI", "https://api.thegrid.ai/v1"),
|
||||
("upstage", "Upstage", "https://api.upstage.ai/v1/solar"),
|
||||
("vivgrid", "Vivgrid", "https://api.vivgrid.com/v1"),
|
||||
("vultr", "Vultr", "https://api.vultrinference.com/v1"),
|
||||
("wafer.ai", "Wafer", "https://pass.wafer.ai/v1"),
|
||||
("wandb", "Weights & Biases", "https://api.inference.wandb.ai/v1"),
|
||||
("zenmux", "ZenMux", "https://zenmux.ai/api/v1"),
|
||||
)
|
||||
catalog_openai_overrides = {
|
||||
"cloudflare-workers-ai": {
|
||||
"api_key_hint": "填写 Cloudflare API Token,并将 Base URL 中的 ${CLOUDFLARE_ACCOUNT_ID} 替换为真实账户 ID。",
|
||||
"description": "Cloudflare Workers AI OpenAI-compatible 端点,需要替换账户 ID。",
|
||||
},
|
||||
"privatemode-ai": {
|
||||
"api_key_hint": "如未启用鉴权,可填写任意占位值。",
|
||||
"description": "Privatemode AI 本地 OpenAI-compatible 端点。",
|
||||
},
|
||||
}
|
||||
|
||||
providers = [
|
||||
ProviderSpec(
|
||||
id="chatgpt",
|
||||
name="ChatGPT",
|
||||
@@ -162,6 +324,14 @@ class LLMProviderManager(metaclass=Singleton):
|
||||
description="Gemini / Google AI Studio。",
|
||||
sort_order=20,
|
||||
),
|
||||
anthropic_provider(
|
||||
provider_id="anthropic",
|
||||
name="Anthropic",
|
||||
default_base_url="https://api.anthropic.com/v1",
|
||||
sort_order=25,
|
||||
api_key_hint="填写 Anthropic API Key。",
|
||||
description="Anthropic Claude 官方端点。",
|
||||
),
|
||||
ProviderSpec(
|
||||
id="deepseek",
|
||||
name="DeepSeek",
|
||||
@@ -172,6 +342,14 @@ class LLMProviderManager(metaclass=Singleton):
|
||||
description="DeepSeek 官方平台。",
|
||||
sort_order=30,
|
||||
),
|
||||
catalog_openai_provider(
|
||||
provider_id="groq",
|
||||
name="Groq",
|
||||
default_base_url="https://api.groq.com/openai/v1",
|
||||
sort_order=35,
|
||||
api_key_hint="填写 Groq API Key。",
|
||||
description="Groq 官方 OpenAI-compatible 端点。",
|
||||
),
|
||||
ProviderSpec(
|
||||
id="openrouter",
|
||||
name="OpenRouter",
|
||||
@@ -182,6 +360,14 @@ class LLMProviderManager(metaclass=Singleton):
|
||||
description="OpenRouter 聚合模型平台。",
|
||||
sort_order=40,
|
||||
),
|
||||
catalog_openai_provider(
|
||||
provider_id="xai",
|
||||
name="xAI",
|
||||
default_base_url="https://api.x.ai/v1",
|
||||
sort_order=45,
|
||||
api_key_hint="填写 xAI API Key。",
|
||||
description="xAI 官方 OpenAI-compatible 端点。",
|
||||
),
|
||||
ProviderSpec(
|
||||
id="github-copilot",
|
||||
name="GitHub Copilot",
|
||||
@@ -201,25 +387,140 @@ class LLMProviderManager(metaclass=Singleton):
|
||||
description="通过 GitHub Copilot 订阅接入。",
|
||||
sort_order=50,
|
||||
),
|
||||
ProviderSpec(
|
||||
id="siliconflow",
|
||||
name="硅基流动",
|
||||
runtime="openai_compatible",
|
||||
models_dev_provider_id="siliconflow",
|
||||
default_base_url="https://api.siliconflow.cn/v1",
|
||||
api_key_hint="填写硅基流动 API Key。",
|
||||
description="SiliconFlow 官方兼容端点。",
|
||||
sort_order=60,
|
||||
catalog_openai_provider(
|
||||
provider_id="github-models",
|
||||
name="GitHub Models",
|
||||
default_base_url="https://models.github.ai/inference",
|
||||
sort_order=55,
|
||||
api_key_label="GitHub Token",
|
||||
api_key_hint="填写具有 GitHub Models 访问权限的 GitHub Token。",
|
||||
description="GitHub Models 推理端点。",
|
||||
),
|
||||
ProviderSpec(
|
||||
id="alibaba",
|
||||
openai_provider(
|
||||
provider_id="siliconflow",
|
||||
name="硅基流动",
|
||||
default_base_url="https://api.siliconflow.cn/v1",
|
||||
sort_order=60,
|
||||
models_dev_provider_id="siliconflow-cn",
|
||||
base_url_presets=(
|
||||
url_preset(
|
||||
label="中国大陆",
|
||||
value="https://api.siliconflow.cn/v1",
|
||||
models_dev_provider_id="siliconflow-cn",
|
||||
),
|
||||
url_preset(
|
||||
label="Global",
|
||||
value="https://api.siliconflow.com/v1",
|
||||
models_dev_provider_id="siliconflow",
|
||||
),
|
||||
),
|
||||
api_key_hint="填写硅基流动 API Key,可在中国大陆与 Global 端点间切换。",
|
||||
description="SiliconFlow 官方兼容端点。",
|
||||
),
|
||||
catalog_openai_provider(
|
||||
provider_id="moonshot",
|
||||
name="Moonshot AI",
|
||||
default_base_url="https://api.moonshot.cn/v1",
|
||||
sort_order=62,
|
||||
models_dev_provider_id="moonshotai-cn",
|
||||
base_url_presets=(
|
||||
url_preset(
|
||||
label="中国站",
|
||||
value="https://api.moonshot.cn/v1",
|
||||
models_dev_provider_id="moonshotai-cn",
|
||||
),
|
||||
url_preset(
|
||||
label="国际站",
|
||||
value="https://api.moonshot.ai/v1",
|
||||
models_dev_provider_id="moonshotai",
|
||||
),
|
||||
),
|
||||
api_key_hint="填写 Moonshot / Kimi API Key,可在中国站与国际站端点间切换。",
|
||||
description="Moonshot / Kimi 官方兼容端点。",
|
||||
),
|
||||
anthropic_provider(
|
||||
provider_id="kimi-coding",
|
||||
name="Kimi for Coding",
|
||||
default_base_url="https://api.kimi.com/coding/v1",
|
||||
sort_order=63,
|
||||
models_dev_provider_id="kimi-for-coding",
|
||||
api_key_hint="填写 Moonshot / Kimi API Key。",
|
||||
description="Moonshot Kimi Coding Anthropic-compatible 端点。",
|
||||
),
|
||||
openai_provider(
|
||||
provider_id="zhipu",
|
||||
name="智谱 GLM",
|
||||
default_base_url="https://open.bigmodel.cn/api/paas/v4",
|
||||
sort_order=65,
|
||||
models_dev_provider_id="zhipuai",
|
||||
base_url_presets=(
|
||||
url_preset(
|
||||
label="Token Plan / 通用 API",
|
||||
value="https://open.bigmodel.cn/api/paas/v4",
|
||||
models_dev_provider_id="zhipuai",
|
||||
),
|
||||
url_preset(
|
||||
label="Coding Plan",
|
||||
value="https://open.bigmodel.cn/api/coding/paas/v4",
|
||||
model_list_base_url="https://open.bigmodel.cn/api/paas/v4",
|
||||
models_dev_provider_id="zhipuai-coding-plan",
|
||||
),
|
||||
),
|
||||
api_key_hint="填写智谱开放平台 API Key,可在 Token Plan / 通用 API 与 Coding Plan 端点间切换。",
|
||||
description="智谱开放平台国内站,支持通用 API 与 GLM Coding Plan 端点。",
|
||||
),
|
||||
catalog_openai_provider(
|
||||
provider_id="zai",
|
||||
name="Z.AI",
|
||||
default_base_url="https://api.z.ai/api/paas/v4",
|
||||
sort_order=66,
|
||||
base_url_presets=(
|
||||
url_preset(
|
||||
label="Token Plan / 通用 API",
|
||||
value="https://api.z.ai/api/paas/v4",
|
||||
models_dev_provider_id="zai",
|
||||
),
|
||||
url_preset(
|
||||
label="Coding Plan",
|
||||
value="https://api.z.ai/api/coding/paas/v4",
|
||||
models_dev_provider_id="zai-coding-plan",
|
||||
),
|
||||
),
|
||||
api_key_hint="填写 Z.AI API Key,可在通用 API 与 Coding Plan 端点间切换。",
|
||||
description="Z.AI 官方端点。",
|
||||
),
|
||||
openai_provider(
|
||||
provider_id="alibaba",
|
||||
name="阿里云百炼",
|
||||
runtime="openai_compatible",
|
||||
models_dev_provider_id="alibaba",
|
||||
default_base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
api_key_hint="填写 DashScope / Alibaba API Key。",
|
||||
description="阿里云百炼兼容端点。",
|
||||
sort_order=70,
|
||||
models_dev_provider_id="alibaba-cn",
|
||||
base_url_presets=(
|
||||
url_preset(
|
||||
label="中国内地 / 通用",
|
||||
value="https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
models_dev_provider_id="alibaba-cn",
|
||||
),
|
||||
url_preset(
|
||||
label="国际站 / 通用",
|
||||
value="https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
||||
models_dev_provider_id="alibaba",
|
||||
),
|
||||
url_preset(
|
||||
label="中国内地 / Coding Plan",
|
||||
value="https://coding.dashscope.aliyuncs.com/v1",
|
||||
model_list_base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
models_dev_provider_id="alibaba-coding-plan-cn",
|
||||
),
|
||||
url_preset(
|
||||
label="国际站 / Coding Plan",
|
||||
value="https://coding-intl.dashscope.aliyuncs.com/v1",
|
||||
model_list_base_url="https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
||||
models_dev_provider_id="alibaba-coding-plan",
|
||||
),
|
||||
),
|
||||
api_key_hint="填写 DashScope / Alibaba API Key,可在中国内地、国际站与 Coding Plan 端点间切换。",
|
||||
description="阿里云百炼兼容端点。",
|
||||
),
|
||||
ProviderSpec(
|
||||
id="volcengine",
|
||||
@@ -236,7 +537,19 @@ class LLMProviderManager(metaclass=Singleton):
|
||||
runtime="openai_compatible",
|
||||
models_dev_provider_id="tencent-tokenhub",
|
||||
default_base_url="https://tokenhub.tencentmaas.com/v1",
|
||||
api_key_hint="填写 Tencent API Key。",
|
||||
base_url_presets=(
|
||||
url_preset(
|
||||
label="TokenHub",
|
||||
value="https://tokenhub.tencentmaas.com/v1",
|
||||
models_dev_provider_id="tencent-tokenhub",
|
||||
),
|
||||
url_preset(
|
||||
label="Coding Plan",
|
||||
value="https://api.lkeap.cloud.tencent.com/coding/v3",
|
||||
models_dev_provider_id="tencent-coding-plan",
|
||||
),
|
||||
),
|
||||
api_key_hint="填写 Tencent API Key,可在 TokenHub 与 Coding Plan 端点间切换。",
|
||||
model_list_strategy="models_dev_only",
|
||||
description="腾讯兼容端点。",
|
||||
sort_order=90,
|
||||
@@ -261,27 +574,125 @@ class LLMProviderManager(metaclass=Singleton):
|
||||
description="Nvidia 集成推理平台。",
|
||||
sort_order=110,
|
||||
),
|
||||
ProviderSpec(
|
||||
id="minimax",
|
||||
catalog_openai_provider(
|
||||
provider_id="opencode",
|
||||
name="OpenCode",
|
||||
default_base_url="https://opencode.ai/zen/v1",
|
||||
sort_order=115,
|
||||
base_url_presets=(
|
||||
url_preset(
|
||||
label="Zen",
|
||||
value="https://opencode.ai/zen/v1",
|
||||
models_dev_provider_id="opencode",
|
||||
),
|
||||
url_preset(
|
||||
label="Go",
|
||||
value="https://opencode.ai/zen/go/v1",
|
||||
models_dev_provider_id="opencode-go",
|
||||
),
|
||||
),
|
||||
api_key_hint="填写 OpenCode API Key,可在 Zen 与 Go 端点间切换。",
|
||||
description="OpenCode Zen / Go 端点。",
|
||||
),
|
||||
anthropic_provider(
|
||||
provider_id="minimax",
|
||||
name="MiniMax",
|
||||
runtime="anthropic_compatible",
|
||||
models_dev_provider_id="minimax",
|
||||
default_base_url="https://api.minimaxi.com/anthropic/v1",
|
||||
api_key_hint="填写 MiniMax API Key。",
|
||||
model_list_strategy="anthropic_compatible",
|
||||
description="MiniMax Anthropic-compatible 端点。",
|
||||
sort_order=120,
|
||||
models_dev_provider_id="minimax-cn",
|
||||
base_url_presets=(
|
||||
url_preset(
|
||||
label="中国内地 / 通用",
|
||||
value="https://api.minimaxi.com/anthropic/v1",
|
||||
models_dev_provider_id="minimax-cn",
|
||||
),
|
||||
ProviderSpec(
|
||||
id="xiaomi",
|
||||
url_preset(
|
||||
label="国际站 / 通用",
|
||||
value="https://api.minimax.io/anthropic/v1",
|
||||
models_dev_provider_id="minimax",
|
||||
),
|
||||
),
|
||||
api_key_hint="填写 MiniMax API Key,可在中国内地与国际站通用端点间切换。",
|
||||
description="MiniMax Anthropic-compatible 通用端点。",
|
||||
),
|
||||
anthropic_provider(
|
||||
provider_id="minimax-coding",
|
||||
name="MiniMax Coding Plan",
|
||||
default_base_url="https://api.minimaxi.com/anthropic/v1",
|
||||
sort_order=121,
|
||||
models_dev_provider_id="minimax-cn-coding-plan",
|
||||
base_url_presets=(
|
||||
url_preset(
|
||||
label="中国内地 / Coding Plan",
|
||||
value="https://api.minimaxi.com/anthropic/v1",
|
||||
models_dev_provider_id="minimax-cn-coding-plan",
|
||||
),
|
||||
url_preset(
|
||||
label="国际站 / Coding Plan",
|
||||
value="https://api.minimax.io/anthropic/v1",
|
||||
models_dev_provider_id="minimax-coding-plan",
|
||||
),
|
||||
),
|
||||
api_key_hint="填写 MiniMax API Key,可在中国内地与国际站 Coding Plan 目录间切换。",
|
||||
description="MiniMax Coding Plan Anthropic-compatible 端点。",
|
||||
),
|
||||
catalog_openai_provider(
|
||||
provider_id="xiaomi",
|
||||
name="Xiaomi",
|
||||
runtime="openai_compatible",
|
||||
models_dev_provider_id="xiaomi",
|
||||
default_base_url="https://api.xiaomimimo.com/v1",
|
||||
api_key_hint="填写 Xiaomi API Key。",
|
||||
description="小米 Mimo 兼容端点。",
|
||||
sort_order=130,
|
||||
base_url_presets=(
|
||||
url_preset(
|
||||
label="标准端点",
|
||||
value="https://api.xiaomimimo.com/v1",
|
||||
models_dev_provider_id="xiaomi",
|
||||
),
|
||||
url_preset(
|
||||
label="Token Plan / 中国",
|
||||
value="https://token-plan-cn.xiaomimimo.com/v1",
|
||||
models_dev_provider_id="xiaomi-token-plan-cn",
|
||||
),
|
||||
url_preset(
|
||||
label="Token Plan / 新加坡",
|
||||
value="https://token-plan-sgp.xiaomimimo.com/v1",
|
||||
models_dev_provider_id="xiaomi-token-plan-sgp",
|
||||
),
|
||||
url_preset(
|
||||
label="Token Plan / 欧洲",
|
||||
value="https://token-plan-ams.xiaomimimo.com/v1",
|
||||
models_dev_provider_id="xiaomi-token-plan-ams",
|
||||
),
|
||||
),
|
||||
api_key_hint="填写 Xiaomi API Key,可在标准端点与各区域 Token Plan 端点间切换。",
|
||||
description="小米 Mimo 兼容端点。",
|
||||
),
|
||||
catalog_openai_provider(
|
||||
provider_id="lmstudio",
|
||||
name="LM Studio",
|
||||
default_base_url="http://127.0.0.1:1234/v1",
|
||||
sort_order=135,
|
||||
api_key_hint="如未启用鉴权,可填写任意占位值。",
|
||||
description="LM Studio 本地 OpenAI-compatible 端点。",
|
||||
),
|
||||
]
|
||||
|
||||
for sort_order, (provider_id, name, base_url) in enumerate(
|
||||
catalog_openai_providers,
|
||||
start=200,
|
||||
):
|
||||
overrides = catalog_openai_overrides.get(provider_id, {})
|
||||
providers.append(
|
||||
catalog_openai_provider(
|
||||
provider_id=provider_id,
|
||||
name=name,
|
||||
default_base_url=base_url,
|
||||
sort_order=sort_order,
|
||||
api_key_hint=overrides.get("api_key_hint"),
|
||||
description=overrides.get("description"),
|
||||
)
|
||||
)
|
||||
|
||||
providers.append(
|
||||
ProviderSpec(
|
||||
id="openai",
|
||||
name="OpenAI Compatible",
|
||||
@@ -292,9 +703,10 @@ class LLMProviderManager(metaclass=Singleton):
|
||||
supports_api_key=True,
|
||||
api_key_hint="通用 OpenAI-compatible 兜底入口,需要手动填写 Base URL。",
|
||||
description="通用 OpenAI-compatible 模型服务。",
|
||||
sort_order=200,
|
||||
),
|
||||
sort_order=1000,
|
||||
)
|
||||
)
|
||||
return tuple(providers)
|
||||
|
||||
def list_providers(self) -> list[dict[str, Any]]:
|
||||
"""返回前端可渲染的 provider 目录。"""
|
||||
@@ -305,7 +717,14 @@ class LLMProviderManager(metaclass=Singleton):
|
||||
"id": spec.id,
|
||||
"name": spec.name,
|
||||
"runtime": spec.runtime,
|
||||
"default_base_url": spec.default_base_url or "",
|
||||
"default_base_url": self._default_base_url_for_provider(spec) or "",
|
||||
"base_url_presets": [
|
||||
{
|
||||
"label": preset.label,
|
||||
"value": self._sanitize_base_url(preset.value) or "",
|
||||
}
|
||||
for preset in spec.base_url_presets
|
||||
],
|
||||
"base_url_editable": spec.base_url_editable,
|
||||
"requires_base_url": spec.requires_base_url,
|
||||
"supports_api_key": spec.supports_api_key,
|
||||
@@ -344,6 +763,65 @@ class LLMProviderManager(metaclass=Singleton):
|
||||
return None
|
||||
return value.rstrip("/")
|
||||
|
||||
@classmethod
|
||||
def _default_base_url_for_provider(cls, spec: ProviderSpec) -> Optional[str]:
|
||||
default_base_url = cls._sanitize_base_url(spec.default_base_url)
|
||||
if default_base_url:
|
||||
return default_base_url
|
||||
if not spec.base_url_presets:
|
||||
return None
|
||||
return cls._sanitize_base_url(spec.base_url_presets[0].value)
|
||||
|
||||
@classmethod
|
||||
def _resolve_provider_model_list_base_url(
|
||||
cls, spec: ProviderSpec, base_url: Optional[str]
|
||||
) -> Optional[str]:
|
||||
normalized_base_url = cls._sanitize_base_url(base_url)
|
||||
if normalized_base_url:
|
||||
for preset in spec.base_url_presets:
|
||||
preset_value = cls._sanitize_base_url(preset.value)
|
||||
if normalized_base_url != preset_value:
|
||||
continue
|
||||
return cls._sanitize_base_url(preset.model_list_base_url) or preset_value
|
||||
return normalized_base_url
|
||||
|
||||
default_base_url = cls._default_base_url_for_provider(spec)
|
||||
if default_base_url:
|
||||
for preset in spec.base_url_presets:
|
||||
preset_value = cls._sanitize_base_url(preset.value)
|
||||
if preset_value != default_base_url:
|
||||
continue
|
||||
return cls._sanitize_base_url(preset.model_list_base_url) or preset_value
|
||||
return default_base_url
|
||||
|
||||
@classmethod
|
||||
def _resolve_provider_models_dev_provider_id(
|
||||
cls, spec: ProviderSpec, base_url: Optional[str]
|
||||
) -> Optional[str]:
|
||||
normalized_base_url = cls._sanitize_base_url(base_url)
|
||||
if normalized_base_url:
|
||||
for preset in spec.base_url_presets:
|
||||
preset_value = cls._sanitize_base_url(preset.value)
|
||||
if normalized_base_url != preset_value:
|
||||
continue
|
||||
return preset.models_dev_provider_id or spec.models_dev_provider_id
|
||||
return spec.models_dev_provider_id
|
||||
|
||||
default_base_url = cls._default_base_url_for_provider(spec)
|
||||
if default_base_url:
|
||||
for preset in spec.base_url_presets:
|
||||
preset_value = cls._sanitize_base_url(preset.value)
|
||||
if preset_value != default_base_url:
|
||||
continue
|
||||
return preset.models_dev_provider_id or spec.models_dev_provider_id
|
||||
return spec.models_dev_provider_id
|
||||
|
||||
def resolve_model_list_base_url(
|
||||
self, provider_id: str, base_url: Optional[str]
|
||||
) -> Optional[str]:
|
||||
spec = self.get_provider(provider_id)
|
||||
return self._resolve_provider_model_list_base_url(spec, base_url)
|
||||
|
||||
@staticmethod
|
||||
def _httpx_proxy_key() -> str:
|
||||
"""兼容不同 httpx 版本的 proxy 参数名。"""
|
||||
@@ -492,16 +970,22 @@ class LLMProviderManager(metaclass=Singleton):
|
||||
return cached
|
||||
raise LLMProviderError(f"获取 models.dev 数据失败: {err}") from err
|
||||
|
||||
async def _models_dev_provider_payload(self, provider_id: str) -> dict[str, Any]:
|
||||
async def _models_dev_provider_payload(
|
||||
self, provider_id: str, base_url: Optional[str] = None
|
||||
) -> dict[str, Any]:
|
||||
spec = self.get_provider(provider_id)
|
||||
if not spec.models_dev_provider_id:
|
||||
models_dev_provider_id = self._resolve_provider_models_dev_provider_id(
|
||||
spec,
|
||||
base_url,
|
||||
)
|
||||
if not models_dev_provider_id:
|
||||
return {}
|
||||
return (await self.get_models_dev_data()).get(spec.models_dev_provider_id, {}) or {}
|
||||
return (await self.get_models_dev_data()).get(models_dev_provider_id, {}) or {}
|
||||
|
||||
async def _models_dev_model(
|
||||
self, provider_id: str, model_id: str
|
||||
self, provider_id: str, model_id: str, base_url: Optional[str] = None
|
||||
) -> dict[str, Any] | None:
|
||||
payload = await self._models_dev_provider_payload(provider_id)
|
||||
payload = await self._models_dev_provider_payload(provider_id, base_url=base_url)
|
||||
models = payload.get("models") if isinstance(payload, dict) else None
|
||||
if not isinstance(models, dict):
|
||||
return None
|
||||
@@ -649,7 +1133,11 @@ class LLMProviderManager(metaclass=Singleton):
|
||||
results = []
|
||||
response = await client.models.list()
|
||||
for model in response.data:
|
||||
metadata = await self._models_dev_model(provider_id, model.id) or {}
|
||||
metadata = await self._models_dev_model(
|
||||
provider_id,
|
||||
model.id,
|
||||
base_url=base_url,
|
||||
) or {}
|
||||
results.append(
|
||||
self._normalize_model_record(
|
||||
model_id=model.id,
|
||||
@@ -664,13 +1152,14 @@ class LLMProviderManager(metaclass=Singleton):
|
||||
self,
|
||||
provider_id: str,
|
||||
transport: str = "openai",
|
||||
base_url: Optional[str] = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""
|
||||
某些 provider 没有统一稳定的 models.list 行为,
|
||||
因此优先读取 models.dev 目录;若未来 provider 暴露标准 models 接口,
|
||||
再平滑补充实时刷新即可。
|
||||
"""
|
||||
payload = await self._models_dev_provider_payload(provider_id)
|
||||
payload = await self._models_dev_provider_payload(provider_id, base_url=base_url)
|
||||
models = payload.get("models") if isinstance(payload, dict) else None
|
||||
if not isinstance(models, dict):
|
||||
raise LLMProviderError(f"{provider_id} 暂无可用模型目录")
|
||||
@@ -825,9 +1314,10 @@ class LLMProviderManager(metaclass=Singleton):
|
||||
) -> list[dict[str, Any]]:
|
||||
"""返回标准化后的模型目录。"""
|
||||
spec = self.get_provider(provider_id)
|
||||
if force_refresh and spec.models_dev_provider_id:
|
||||
if self._resolve_provider_models_dev_provider_id(spec, base_url):
|
||||
# 对依赖 models.dev 的 provider 主动刷新一次缓存,保证“刷新模型列表”
|
||||
# 在使用目录型 provider 时也能拿到最新参数。
|
||||
if force_refresh:
|
||||
await self.get_models_dev_data(force_refresh=True)
|
||||
runtime = await self.resolve_runtime(
|
||||
provider_id,
|
||||
@@ -848,7 +1338,10 @@ class LLMProviderManager(metaclass=Singleton):
|
||||
return await self._list_models_from_openai_compatible(
|
||||
provider_id="chatgpt",
|
||||
api_key=runtime["api_key"],
|
||||
base_url=runtime["base_url"],
|
||||
base_url=self._resolve_provider_model_list_base_url(
|
||||
spec,
|
||||
runtime["base_url"],
|
||||
),
|
||||
default_headers=runtime.get("default_headers"),
|
||||
)
|
||||
|
||||
@@ -856,28 +1349,40 @@ class LLMProviderManager(metaclass=Singleton):
|
||||
return await self._list_models_from_models_dev_only(
|
||||
provider_id=provider_id,
|
||||
transport="anthropic",
|
||||
base_url=base_url,
|
||||
)
|
||||
|
||||
if spec.model_list_strategy == "models_dev_only":
|
||||
return await self._list_models_from_models_dev_only(
|
||||
provider_id=provider_id,
|
||||
transport="openai",
|
||||
base_url=base_url,
|
||||
)
|
||||
|
||||
# openai-compatible / deepseek 默认走官方 models 端点。
|
||||
return await self._list_models_from_openai_compatible(
|
||||
provider_id=provider_id,
|
||||
api_key=runtime["api_key"],
|
||||
base_url=runtime["base_url"],
|
||||
base_url=self._resolve_provider_model_list_base_url(
|
||||
spec,
|
||||
runtime["base_url"],
|
||||
),
|
||||
default_headers=runtime.get("default_headers"),
|
||||
)
|
||||
|
||||
async def resolve_model_metadata(
|
||||
self, provider_id: str, model_id: Optional[str]
|
||||
self,
|
||||
provider_id: str,
|
||||
model_id: Optional[str],
|
||||
base_url: Optional[str] = None,
|
||||
) -> dict[str, Any] | None:
|
||||
if not model_id:
|
||||
return None
|
||||
metadata = await self._models_dev_model(provider_id, model_id)
|
||||
metadata = await self._models_dev_model(
|
||||
provider_id,
|
||||
model_id,
|
||||
base_url=base_url,
|
||||
)
|
||||
if metadata:
|
||||
return metadata
|
||||
if provider_id == "chatgpt":
|
||||
@@ -1366,7 +1871,11 @@ class LLMProviderManager(metaclass=Singleton):
|
||||
"runtime": spec.runtime,
|
||||
"model_id": model,
|
||||
"model_record": model_record,
|
||||
"model_metadata": await self.resolve_model_metadata(provider_id, model),
|
||||
"model_metadata": await self.resolve_model_metadata(
|
||||
provider_id,
|
||||
model,
|
||||
base_url=base_url,
|
||||
),
|
||||
"default_headers": None,
|
||||
"use_responses_api": None,
|
||||
"auth_mode": "api_key",
|
||||
@@ -1401,7 +1910,8 @@ class LLMProviderManager(metaclass=Singleton):
|
||||
{
|
||||
"runtime": "openai_compatible",
|
||||
"api_key": normalized_api_key,
|
||||
"base_url": normalized_base_url or spec.default_base_url,
|
||||
"base_url": normalized_base_url
|
||||
or self._default_base_url_for_provider(spec),
|
||||
"auth_mode": "api_key",
|
||||
}
|
||||
)
|
||||
@@ -1448,7 +1958,9 @@ class LLMProviderManager(metaclass=Singleton):
|
||||
return result
|
||||
|
||||
if spec.runtime == "anthropic_compatible":
|
||||
effective_base_url = normalized_base_url or spec.default_base_url
|
||||
effective_base_url = normalized_base_url or self._default_base_url_for_provider(
|
||||
spec
|
||||
)
|
||||
if not normalized_api_key:
|
||||
raise LLMProviderAuthError(f"{spec.name} 需要填写 API Key")
|
||||
if not effective_base_url:
|
||||
@@ -1464,7 +1976,7 @@ class LLMProviderManager(metaclass=Singleton):
|
||||
)
|
||||
return result
|
||||
|
||||
effective_base_url = normalized_base_url or spec.default_base_url
|
||||
effective_base_url = normalized_base_url or self._default_base_url_for_provider(spec)
|
||||
if spec.requires_base_url and not effective_base_url:
|
||||
raise LLMProviderAuthError(f"{spec.name} 需要填写 Base URL")
|
||||
if not normalized_api_key:
|
||||
|
||||
@@ -12,7 +12,6 @@ from app.agent.llm import (
|
||||
LLMTestTimeout,
|
||||
render_auth_result_html,
|
||||
)
|
||||
from app.core.config import settings
|
||||
from app.db.models import User
|
||||
from app.db.user_oper import (
|
||||
get_current_active_superuser_async,
|
||||
|
||||
Reference in New Issue
Block a user