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}
|
{"id": model_id, "name": model_id}
|
||||||
for model_id in await self._get_google_models(api_key or "")
|
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 [
|
return [
|
||||||
{"id": model_id, "name": model_id}
|
{"id": model_id, "name": model_id}
|
||||||
for model_id in await self._get_openai_compatible_models(
|
for model_id in await self._get_openai_compatible_models(
|
||||||
provider,
|
provider,
|
||||||
api_key or "",
|
api_key or "",
|
||||||
base_url,
|
model_list_base_url,
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,16 @@ class ProviderAuthMethod:
|
|||||||
description: str = ""
|
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)
|
@dataclass(frozen=True)
|
||||||
class ProviderSpec:
|
class ProviderSpec:
|
||||||
"""描述一个可接入的 LLM provider。"""
|
"""描述一个可接入的 LLM provider。"""
|
||||||
@@ -53,6 +63,7 @@ class ProviderSpec:
|
|||||||
runtime: str
|
runtime: str
|
||||||
models_dev_provider_id: Optional[str] = None
|
models_dev_provider_id: Optional[str] = None
|
||||||
default_base_url: Optional[str] = None
|
default_base_url: Optional[str] = None
|
||||||
|
base_url_presets: Tuple[ProviderUrlPreset, ...] = ()
|
||||||
base_url_editable: bool = False
|
base_url_editable: bool = False
|
||||||
requires_base_url: bool = False
|
requires_base_url: bool = False
|
||||||
supports_api_key: bool = True
|
supports_api_key: bool = True
|
||||||
@@ -138,7 +149,158 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
label="设备码授权",
|
label="设备码授权",
|
||||||
description="适合无回调环境,复制设备码到浏览器完成登录。",
|
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(
|
ProviderSpec(
|
||||||
id="chatgpt",
|
id="chatgpt",
|
||||||
name="ChatGPT",
|
name="ChatGPT",
|
||||||
@@ -162,6 +324,14 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
description="Gemini / Google AI Studio。",
|
description="Gemini / Google AI Studio。",
|
||||||
sort_order=20,
|
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(
|
ProviderSpec(
|
||||||
id="deepseek",
|
id="deepseek",
|
||||||
name="DeepSeek",
|
name="DeepSeek",
|
||||||
@@ -172,6 +342,14 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
description="DeepSeek 官方平台。",
|
description="DeepSeek 官方平台。",
|
||||||
sort_order=30,
|
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(
|
ProviderSpec(
|
||||||
id="openrouter",
|
id="openrouter",
|
||||||
name="OpenRouter",
|
name="OpenRouter",
|
||||||
@@ -182,6 +360,14 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
description="OpenRouter 聚合模型平台。",
|
description="OpenRouter 聚合模型平台。",
|
||||||
sort_order=40,
|
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(
|
ProviderSpec(
|
||||||
id="github-copilot",
|
id="github-copilot",
|
||||||
name="GitHub Copilot",
|
name="GitHub Copilot",
|
||||||
@@ -201,25 +387,140 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
description="通过 GitHub Copilot 订阅接入。",
|
description="通过 GitHub Copilot 订阅接入。",
|
||||||
sort_order=50,
|
sort_order=50,
|
||||||
),
|
),
|
||||||
ProviderSpec(
|
catalog_openai_provider(
|
||||||
id="siliconflow",
|
provider_id="github-models",
|
||||||
name="硅基流动",
|
name="GitHub Models",
|
||||||
runtime="openai_compatible",
|
default_base_url="https://models.github.ai/inference",
|
||||||
models_dev_provider_id="siliconflow",
|
sort_order=55,
|
||||||
default_base_url="https://api.siliconflow.cn/v1",
|
api_key_label="GitHub Token",
|
||||||
api_key_hint="填写硅基流动 API Key。",
|
api_key_hint="填写具有 GitHub Models 访问权限的 GitHub Token。",
|
||||||
description="SiliconFlow 官方兼容端点。",
|
description="GitHub Models 推理端点。",
|
||||||
sort_order=60,
|
|
||||||
),
|
),
|
||||||
ProviderSpec(
|
openai_provider(
|
||||||
id="alibaba",
|
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="阿里云百炼",
|
name="阿里云百炼",
|
||||||
runtime="openai_compatible",
|
|
||||||
models_dev_provider_id="alibaba",
|
|
||||||
default_base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
|
default_base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||||
api_key_hint="填写 DashScope / Alibaba API Key。",
|
|
||||||
description="阿里云百炼兼容端点。",
|
|
||||||
sort_order=70,
|
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(
|
ProviderSpec(
|
||||||
id="volcengine",
|
id="volcengine",
|
||||||
@@ -236,7 +537,19 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
runtime="openai_compatible",
|
runtime="openai_compatible",
|
||||||
models_dev_provider_id="tencent-tokenhub",
|
models_dev_provider_id="tencent-tokenhub",
|
||||||
default_base_url="https://tokenhub.tencentmaas.com/v1",
|
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",
|
model_list_strategy="models_dev_only",
|
||||||
description="腾讯兼容端点。",
|
description="腾讯兼容端点。",
|
||||||
sort_order=90,
|
sort_order=90,
|
||||||
@@ -261,27 +574,125 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
description="Nvidia 集成推理平台。",
|
description="Nvidia 集成推理平台。",
|
||||||
sort_order=110,
|
sort_order=110,
|
||||||
),
|
),
|
||||||
ProviderSpec(
|
catalog_openai_provider(
|
||||||
id="minimax",
|
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",
|
name="MiniMax",
|
||||||
runtime="anthropic_compatible",
|
|
||||||
models_dev_provider_id="minimax",
|
|
||||||
default_base_url="https://api.minimaxi.com/anthropic/v1",
|
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,
|
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",
|
||||||
|
),
|
||||||
|
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 通用端点。",
|
||||||
),
|
),
|
||||||
ProviderSpec(
|
anthropic_provider(
|
||||||
id="xiaomi",
|
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",
|
name="Xiaomi",
|
||||||
runtime="openai_compatible",
|
|
||||||
models_dev_provider_id="xiaomi",
|
|
||||||
default_base_url="https://api.xiaomimimo.com/v1",
|
default_base_url="https://api.xiaomimimo.com/v1",
|
||||||
api_key_hint="填写 Xiaomi API Key。",
|
|
||||||
description="小米 Mimo 兼容端点。",
|
|
||||||
sort_order=130,
|
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(
|
ProviderSpec(
|
||||||
id="openai",
|
id="openai",
|
||||||
name="OpenAI Compatible",
|
name="OpenAI Compatible",
|
||||||
@@ -292,9 +703,10 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
supports_api_key=True,
|
supports_api_key=True,
|
||||||
api_key_hint="通用 OpenAI-compatible 兜底入口,需要手动填写 Base URL。",
|
api_key_hint="通用 OpenAI-compatible 兜底入口,需要手动填写 Base URL。",
|
||||||
description="通用 OpenAI-compatible 模型服务。",
|
description="通用 OpenAI-compatible 模型服务。",
|
||||||
sort_order=200,
|
sort_order=1000,
|
||||||
),
|
)
|
||||||
)
|
)
|
||||||
|
return tuple(providers)
|
||||||
|
|
||||||
def list_providers(self) -> list[dict[str, Any]]:
|
def list_providers(self) -> list[dict[str, Any]]:
|
||||||
"""返回前端可渲染的 provider 目录。"""
|
"""返回前端可渲染的 provider 目录。"""
|
||||||
@@ -305,7 +717,14 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
"id": spec.id,
|
"id": spec.id,
|
||||||
"name": spec.name,
|
"name": spec.name,
|
||||||
"runtime": spec.runtime,
|
"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,
|
"base_url_editable": spec.base_url_editable,
|
||||||
"requires_base_url": spec.requires_base_url,
|
"requires_base_url": spec.requires_base_url,
|
||||||
"supports_api_key": spec.supports_api_key,
|
"supports_api_key": spec.supports_api_key,
|
||||||
@@ -344,6 +763,65 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
return None
|
return None
|
||||||
return value.rstrip("/")
|
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
|
@staticmethod
|
||||||
def _httpx_proxy_key() -> str:
|
def _httpx_proxy_key() -> str:
|
||||||
"""兼容不同 httpx 版本的 proxy 参数名。"""
|
"""兼容不同 httpx 版本的 proxy 参数名。"""
|
||||||
@@ -492,16 +970,22 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
return cached
|
return cached
|
||||||
raise LLMProviderError(f"获取 models.dev 数据失败: {err}") from err
|
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)
|
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 {}
|
||||||
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(
|
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:
|
) -> 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
|
models = payload.get("models") if isinstance(payload, dict) else None
|
||||||
if not isinstance(models, dict):
|
if not isinstance(models, dict):
|
||||||
return None
|
return None
|
||||||
@@ -649,7 +1133,11 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
results = []
|
results = []
|
||||||
response = await client.models.list()
|
response = await client.models.list()
|
||||||
for model in response.data:
|
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(
|
results.append(
|
||||||
self._normalize_model_record(
|
self._normalize_model_record(
|
||||||
model_id=model.id,
|
model_id=model.id,
|
||||||
@@ -664,13 +1152,14 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
self,
|
self,
|
||||||
provider_id: str,
|
provider_id: str,
|
||||||
transport: str = "openai",
|
transport: str = "openai",
|
||||||
|
base_url: Optional[str] = None,
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
某些 provider 没有统一稳定的 models.list 行为,
|
某些 provider 没有统一稳定的 models.list 行为,
|
||||||
因此优先读取 models.dev 目录;若未来 provider 暴露标准 models 接口,
|
因此优先读取 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
|
models = payload.get("models") if isinstance(payload, dict) else None
|
||||||
if not isinstance(models, dict):
|
if not isinstance(models, dict):
|
||||||
raise LLMProviderError(f"{provider_id} 暂无可用模型目录")
|
raise LLMProviderError(f"{provider_id} 暂无可用模型目录")
|
||||||
@@ -825,10 +1314,11 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""返回标准化后的模型目录。"""
|
"""返回标准化后的模型目录。"""
|
||||||
spec = self.get_provider(provider_id)
|
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 主动刷新一次缓存,保证“刷新模型列表”
|
# 对依赖 models.dev 的 provider 主动刷新一次缓存,保证“刷新模型列表”
|
||||||
# 在使用目录型 provider 时也能拿到最新参数。
|
# 在使用目录型 provider 时也能拿到最新参数。
|
||||||
await self.get_models_dev_data(force_refresh=True)
|
if force_refresh:
|
||||||
|
await self.get_models_dev_data(force_refresh=True)
|
||||||
runtime = await self.resolve_runtime(
|
runtime = await self.resolve_runtime(
|
||||||
provider_id,
|
provider_id,
|
||||||
model=None,
|
model=None,
|
||||||
@@ -848,7 +1338,10 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
return await self._list_models_from_openai_compatible(
|
return await self._list_models_from_openai_compatible(
|
||||||
provider_id="chatgpt",
|
provider_id="chatgpt",
|
||||||
api_key=runtime["api_key"],
|
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"),
|
default_headers=runtime.get("default_headers"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -856,28 +1349,40 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
return await self._list_models_from_models_dev_only(
|
return await self._list_models_from_models_dev_only(
|
||||||
provider_id=provider_id,
|
provider_id=provider_id,
|
||||||
transport="anthropic",
|
transport="anthropic",
|
||||||
|
base_url=base_url,
|
||||||
)
|
)
|
||||||
|
|
||||||
if spec.model_list_strategy == "models_dev_only":
|
if spec.model_list_strategy == "models_dev_only":
|
||||||
return await self._list_models_from_models_dev_only(
|
return await self._list_models_from_models_dev_only(
|
||||||
provider_id=provider_id,
|
provider_id=provider_id,
|
||||||
transport="openai",
|
transport="openai",
|
||||||
|
base_url=base_url,
|
||||||
)
|
)
|
||||||
|
|
||||||
# openai-compatible / deepseek 默认走官方 models 端点。
|
# openai-compatible / deepseek 默认走官方 models 端点。
|
||||||
return await self._list_models_from_openai_compatible(
|
return await self._list_models_from_openai_compatible(
|
||||||
provider_id=provider_id,
|
provider_id=provider_id,
|
||||||
api_key=runtime["api_key"],
|
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"),
|
default_headers=runtime.get("default_headers"),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def resolve_model_metadata(
|
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:
|
) -> dict[str, Any] | None:
|
||||||
if not model_id:
|
if not model_id:
|
||||||
return None
|
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:
|
if metadata:
|
||||||
return metadata
|
return metadata
|
||||||
if provider_id == "chatgpt":
|
if provider_id == "chatgpt":
|
||||||
@@ -1366,7 +1871,11 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
"runtime": spec.runtime,
|
"runtime": spec.runtime,
|
||||||
"model_id": model,
|
"model_id": model,
|
||||||
"model_record": model_record,
|
"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,
|
"default_headers": None,
|
||||||
"use_responses_api": None,
|
"use_responses_api": None,
|
||||||
"auth_mode": "api_key",
|
"auth_mode": "api_key",
|
||||||
@@ -1401,7 +1910,8 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
{
|
{
|
||||||
"runtime": "openai_compatible",
|
"runtime": "openai_compatible",
|
||||||
"api_key": normalized_api_key,
|
"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",
|
"auth_mode": "api_key",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -1448,7 +1958,9 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
if spec.runtime == "anthropic_compatible":
|
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:
|
if not normalized_api_key:
|
||||||
raise LLMProviderAuthError(f"{spec.name} 需要填写 API Key")
|
raise LLMProviderAuthError(f"{spec.name} 需要填写 API Key")
|
||||||
if not effective_base_url:
|
if not effective_base_url:
|
||||||
@@ -1464,7 +1976,7 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
)
|
)
|
||||||
return result
|
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:
|
if spec.requires_base_url and not effective_base_url:
|
||||||
raise LLMProviderAuthError(f"{spec.name} 需要填写 Base URL")
|
raise LLMProviderAuthError(f"{spec.name} 需要填写 Base URL")
|
||||||
if not normalized_api_key:
|
if not normalized_api_key:
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ from app.agent.llm import (
|
|||||||
LLMTestTimeout,
|
LLMTestTimeout,
|
||||||
render_auth_result_html,
|
render_auth_result_html,
|
||||||
)
|
)
|
||||||
from app.core.config import settings
|
|
||||||
from app.db.models import User
|
from app.db.models import User
|
||||||
from app.db.user_oper import (
|
from app.db.user_oper import (
|
||||||
get_current_active_superuser_async,
|
get_current_active_superuser_async,
|
||||||
|
|||||||
Reference in New Issue
Block a user