mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 23:47:41 +08:00
feat(agent): merge MiniMax coding presets
This commit is contained in:
+17
-2
@@ -449,14 +449,22 @@ class LLMHelper:
|
|||||||
这主要用于单测 stub 环境以及极端的最小运行环境,正常生产路径仍优先
|
这主要用于单测 stub 环境以及极端的最小运行环境,正常生产路径仍优先
|
||||||
走 `LLMProviderManager.resolve_runtime()`。
|
走 `LLMProviderManager.resolve_runtime()`。
|
||||||
"""
|
"""
|
||||||
|
normalized_provider_name = str(provider_name or "").strip().lower()
|
||||||
|
if normalized_provider_name == "minimax-coding":
|
||||||
|
normalized_provider_name = "minimax"
|
||||||
|
|
||||||
api_key_value = api_key if api_key is not None else settings.LLM_API_KEY
|
api_key_value = api_key if api_key is not None else settings.LLM_API_KEY
|
||||||
base_url_value = base_url if base_url is not None else settings.LLM_BASE_URL
|
base_url_value = base_url if base_url is not None else settings.LLM_BASE_URL
|
||||||
if not api_key_value:
|
if not api_key_value:
|
||||||
raise ValueError("未配置LLM API Key")
|
raise ValueError("未配置LLM API Key")
|
||||||
|
|
||||||
runtime_name = provider_name if provider_name in {"google", "deepseek"} else "openai_compatible"
|
runtime_name = (
|
||||||
|
normalized_provider_name
|
||||||
|
if normalized_provider_name in {"google", "deepseek"}
|
||||||
|
else "openai_compatible"
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
"provider_id": provider_name,
|
"provider_id": normalized_provider_name,
|
||||||
"runtime": runtime_name,
|
"runtime": runtime_name,
|
||||||
"model_id": model_name,
|
"model_id": model_name,
|
||||||
"api_key": api_key_value,
|
"api_key": api_key_value,
|
||||||
@@ -510,6 +518,7 @@ class LLMHelper:
|
|||||||
thinking_level: str | None = None,
|
thinking_level: str | None = None,
|
||||||
api_key: str | None = settings.LLM_API_KEY,
|
api_key: str | None = settings.LLM_API_KEY,
|
||||||
base_url: str | None = settings.LLM_BASE_URL,
|
base_url: str | None = settings.LLM_BASE_URL,
|
||||||
|
base_url_preset: str | None = settings.LLM_BASE_URL_PRESET,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
获取LLM实例
|
获取LLM实例
|
||||||
@@ -539,6 +548,7 @@ class LLMHelper:
|
|||||||
model=model_name,
|
model=model_name,
|
||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
base_url=base_url,
|
base_url=base_url,
|
||||||
|
base_url_preset_id=base_url_preset,
|
||||||
)
|
)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.debug(f"LLM provider 目录不可用,回退到旧运行时逻辑: {err}")
|
logger.debug(f"LLM provider 目录不可用,回退到旧运行时逻辑: {err}")
|
||||||
@@ -700,6 +710,7 @@ class LLMHelper:
|
|||||||
thinking_level: str | None = None,
|
thinking_level: str | None = None,
|
||||||
api_key: str | None = None,
|
api_key: str | None = None,
|
||||||
base_url: str | None = None,
|
base_url: str | None = None,
|
||||||
|
base_url_preset: str | None = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
使用当前已保存配置执行一次最小 LLM 调用。
|
使用当前已保存配置执行一次最小 LLM 调用。
|
||||||
@@ -714,6 +725,7 @@ class LLMHelper:
|
|||||||
thinking_level=thinking_level,
|
thinking_level=thinking_level,
|
||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
base_url=base_url,
|
base_url=base_url,
|
||||||
|
base_url_preset=base_url_preset,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
response = await asyncio.wait_for(llm.ainvoke(prompt), timeout=timeout)
|
response = await asyncio.wait_for(llm.ainvoke(prompt), timeout=timeout)
|
||||||
@@ -743,6 +755,7 @@ class LLMHelper:
|
|||||||
provider: str,
|
provider: str,
|
||||||
api_key: str | None = None,
|
api_key: str | None = None,
|
||||||
base_url: str | None = None,
|
base_url: str | None = None,
|
||||||
|
base_url_preset: str | None = None,
|
||||||
force_refresh: bool = False,
|
force_refresh: bool = False,
|
||||||
) -> List[dict[str, Any]]:
|
) -> List[dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
@@ -759,6 +772,7 @@ class LLMHelper:
|
|||||||
provider_id=provider,
|
provider_id=provider,
|
||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
base_url=base_url,
|
base_url=base_url,
|
||||||
|
base_url_preset_id=base_url_preset,
|
||||||
force_refresh=force_refresh,
|
force_refresh=force_refresh,
|
||||||
)
|
)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
@@ -776,6 +790,7 @@ class LLMHelper:
|
|||||||
LLMProviderManager().resolve_model_list_base_url(
|
LLMProviderManager().resolve_model_list_base_url(
|
||||||
provider_id=provider,
|
provider_id=provider,
|
||||||
base_url=base_url,
|
base_url=base_url,
|
||||||
|
base_url_preset_id=base_url_preset,
|
||||||
)
|
)
|
||||||
or base_url
|
or base_url
|
||||||
)
|
)
|
||||||
|
|||||||
+136
-32
@@ -48,6 +48,7 @@ class ProviderAuthMethod:
|
|||||||
class ProviderUrlPreset:
|
class ProviderUrlPreset:
|
||||||
"""前端展示用的 Base URL 预设。"""
|
"""前端展示用的 Base URL 预设。"""
|
||||||
|
|
||||||
|
id: str
|
||||||
label: str
|
label: str
|
||||||
value: str
|
value: str
|
||||||
model_list_base_url: Optional[str] = None
|
model_list_base_url: Optional[str] = None
|
||||||
@@ -418,11 +419,13 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
models_dev_provider_id="siliconflow-cn",
|
models_dev_provider_id="siliconflow-cn",
|
||||||
base_url_presets=(
|
base_url_presets=(
|
||||||
url_preset(
|
url_preset(
|
||||||
|
id="siliconflow-cn",
|
||||||
label="中国大陆",
|
label="中国大陆",
|
||||||
value="https://api.siliconflow.cn/v1",
|
value="https://api.siliconflow.cn/v1",
|
||||||
models_dev_provider_id="siliconflow-cn",
|
models_dev_provider_id="siliconflow-cn",
|
||||||
),
|
),
|
||||||
url_preset(
|
url_preset(
|
||||||
|
id="siliconflow-global",
|
||||||
label="Global",
|
label="Global",
|
||||||
value="https://api.siliconflow.com/v1",
|
value="https://api.siliconflow.com/v1",
|
||||||
models_dev_provider_id="siliconflow",
|
models_dev_provider_id="siliconflow",
|
||||||
@@ -439,11 +442,13 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
models_dev_provider_id="moonshotai-cn",
|
models_dev_provider_id="moonshotai-cn",
|
||||||
base_url_presets=(
|
base_url_presets=(
|
||||||
url_preset(
|
url_preset(
|
||||||
|
id="moonshot-cn",
|
||||||
label="中国站",
|
label="中国站",
|
||||||
value="https://api.moonshot.cn/v1",
|
value="https://api.moonshot.cn/v1",
|
||||||
models_dev_provider_id="moonshotai-cn",
|
models_dev_provider_id="moonshotai-cn",
|
||||||
),
|
),
|
||||||
url_preset(
|
url_preset(
|
||||||
|
id="moonshot-global",
|
||||||
label="国际站",
|
label="国际站",
|
||||||
value="https://api.moonshot.ai/v1",
|
value="https://api.moonshot.ai/v1",
|
||||||
models_dev_provider_id="moonshotai",
|
models_dev_provider_id="moonshotai",
|
||||||
@@ -469,11 +474,13 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
models_dev_provider_id="zhipuai",
|
models_dev_provider_id="zhipuai",
|
||||||
base_url_presets=(
|
base_url_presets=(
|
||||||
url_preset(
|
url_preset(
|
||||||
|
id="zhipu-general",
|
||||||
label="Token Plan / 通用 API",
|
label="Token Plan / 通用 API",
|
||||||
value="https://open.bigmodel.cn/api/paas/v4",
|
value="https://open.bigmodel.cn/api/paas/v4",
|
||||||
models_dev_provider_id="zhipuai",
|
models_dev_provider_id="zhipuai",
|
||||||
),
|
),
|
||||||
url_preset(
|
url_preset(
|
||||||
|
id="zhipu-coding",
|
||||||
label="Coding Plan",
|
label="Coding Plan",
|
||||||
value="https://open.bigmodel.cn/api/coding/paas/v4",
|
value="https://open.bigmodel.cn/api/coding/paas/v4",
|
||||||
model_list_base_url="https://open.bigmodel.cn/api/paas/v4",
|
model_list_base_url="https://open.bigmodel.cn/api/paas/v4",
|
||||||
@@ -490,11 +497,13 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
sort_order=66,
|
sort_order=66,
|
||||||
base_url_presets=(
|
base_url_presets=(
|
||||||
url_preset(
|
url_preset(
|
||||||
|
id="zai-general",
|
||||||
label="Token Plan / 通用 API",
|
label="Token Plan / 通用 API",
|
||||||
value="https://api.z.ai/api/paas/v4",
|
value="https://api.z.ai/api/paas/v4",
|
||||||
models_dev_provider_id="zai",
|
models_dev_provider_id="zai",
|
||||||
),
|
),
|
||||||
url_preset(
|
url_preset(
|
||||||
|
id="zai-coding",
|
||||||
label="Coding Plan",
|
label="Coding Plan",
|
||||||
value="https://api.z.ai/api/coding/paas/v4",
|
value="https://api.z.ai/api/coding/paas/v4",
|
||||||
models_dev_provider_id="zai-coding-plan",
|
models_dev_provider_id="zai-coding-plan",
|
||||||
@@ -511,22 +520,26 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
models_dev_provider_id="alibaba-cn",
|
models_dev_provider_id="alibaba-cn",
|
||||||
base_url_presets=(
|
base_url_presets=(
|
||||||
url_preset(
|
url_preset(
|
||||||
|
id="alibaba-cn-general",
|
||||||
label="中国内地 / 通用",
|
label="中国内地 / 通用",
|
||||||
value="https://dashscope.aliyuncs.com/compatible-mode/v1",
|
value="https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||||
models_dev_provider_id="alibaba-cn",
|
models_dev_provider_id="alibaba-cn",
|
||||||
),
|
),
|
||||||
url_preset(
|
url_preset(
|
||||||
|
id="alibaba-global-general",
|
||||||
label="国际站 / 通用",
|
label="国际站 / 通用",
|
||||||
value="https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
value="https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
||||||
models_dev_provider_id="alibaba",
|
models_dev_provider_id="alibaba",
|
||||||
),
|
),
|
||||||
url_preset(
|
url_preset(
|
||||||
|
id="alibaba-cn-coding",
|
||||||
label="中国内地 / Coding Plan",
|
label="中国内地 / Coding Plan",
|
||||||
value="https://coding.dashscope.aliyuncs.com/v1",
|
value="https://coding.dashscope.aliyuncs.com/v1",
|
||||||
model_list_base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
|
model_list_base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||||
models_dev_provider_id="alibaba-coding-plan-cn",
|
models_dev_provider_id="alibaba-coding-plan-cn",
|
||||||
),
|
),
|
||||||
url_preset(
|
url_preset(
|
||||||
|
id="alibaba-global-coding",
|
||||||
label="国际站 / Coding Plan",
|
label="国际站 / Coding Plan",
|
||||||
value="https://coding-intl.dashscope.aliyuncs.com/v1",
|
value="https://coding-intl.dashscope.aliyuncs.com/v1",
|
||||||
model_list_base_url="https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
model_list_base_url="https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
||||||
@@ -543,10 +556,12 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
default_base_url="https://qianfan.baidubce.com/v2",
|
default_base_url="https://qianfan.baidubce.com/v2",
|
||||||
base_url_presets=(
|
base_url_presets=(
|
||||||
url_preset(
|
url_preset(
|
||||||
|
id="baidu-qianfan-general",
|
||||||
label="通用 API",
|
label="通用 API",
|
||||||
value="https://qianfan.baidubce.com/v2",
|
value="https://qianfan.baidubce.com/v2",
|
||||||
),
|
),
|
||||||
url_preset(
|
url_preset(
|
||||||
|
id="baidu-qianfan-coding",
|
||||||
label="Coding Plan",
|
label="Coding Plan",
|
||||||
value="https://qianfan.baidubce.com/v2/coding",
|
value="https://qianfan.baidubce.com/v2/coding",
|
||||||
),
|
),
|
||||||
@@ -563,10 +578,12 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
default_base_url="https://modelservice.jdcloud.com/v1",
|
default_base_url="https://modelservice.jdcloud.com/v1",
|
||||||
base_url_presets=(
|
base_url_presets=(
|
||||||
url_preset(
|
url_preset(
|
||||||
|
id="jdcloud-general",
|
||||||
label="通用 API",
|
label="通用 API",
|
||||||
value="https://modelservice.jdcloud.com/v1",
|
value="https://modelservice.jdcloud.com/v1",
|
||||||
),
|
),
|
||||||
url_preset(
|
url_preset(
|
||||||
|
id="jdcloud-coding",
|
||||||
label="Coding Plan",
|
label="Coding Plan",
|
||||||
value="https://modelservice.jdcloud.com/coding/openai/v1",
|
value="https://modelservice.jdcloud.com/coding/openai/v1",
|
||||||
),
|
),
|
||||||
@@ -593,11 +610,13 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
default_base_url="https://tokenhub.tencentmaas.com/v1",
|
default_base_url="https://tokenhub.tencentmaas.com/v1",
|
||||||
base_url_presets=(
|
base_url_presets=(
|
||||||
url_preset(
|
url_preset(
|
||||||
|
id="tencent-tokenhub",
|
||||||
label="TokenHub",
|
label="TokenHub",
|
||||||
value="https://tokenhub.tencentmaas.com/v1",
|
value="https://tokenhub.tencentmaas.com/v1",
|
||||||
models_dev_provider_id="tencent-tokenhub",
|
models_dev_provider_id="tencent-tokenhub",
|
||||||
),
|
),
|
||||||
url_preset(
|
url_preset(
|
||||||
|
id="tencent-coding",
|
||||||
label="Coding Plan",
|
label="Coding Plan",
|
||||||
value="https://api.lkeap.cloud.tencent.com/coding/v3",
|
value="https://api.lkeap.cloud.tencent.com/coding/v3",
|
||||||
models_dev_provider_id="tencent-coding-plan",
|
models_dev_provider_id="tencent-coding-plan",
|
||||||
@@ -635,11 +654,13 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
sort_order=115,
|
sort_order=115,
|
||||||
base_url_presets=(
|
base_url_presets=(
|
||||||
url_preset(
|
url_preset(
|
||||||
|
id="opencode-zen",
|
||||||
label="Zen",
|
label="Zen",
|
||||||
value="https://opencode.ai/zen/v1",
|
value="https://opencode.ai/zen/v1",
|
||||||
models_dev_provider_id="opencode",
|
models_dev_provider_id="opencode",
|
||||||
),
|
),
|
||||||
url_preset(
|
url_preset(
|
||||||
|
id="opencode-go",
|
||||||
label="Go",
|
label="Go",
|
||||||
value="https://opencode.ai/zen/go/v1",
|
value="https://opencode.ai/zen/go/v1",
|
||||||
models_dev_provider_id="opencode-go",
|
models_dev_provider_id="opencode-go",
|
||||||
@@ -656,39 +677,32 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
models_dev_provider_id="minimax-cn",
|
models_dev_provider_id="minimax-cn",
|
||||||
base_url_presets=(
|
base_url_presets=(
|
||||||
url_preset(
|
url_preset(
|
||||||
|
id="minimax-cn-general",
|
||||||
label="中国内地 / 通用",
|
label="中国内地 / 通用",
|
||||||
value="https://api.minimaxi.com/anthropic/v1",
|
value="https://api.minimaxi.com/anthropic/v1",
|
||||||
models_dev_provider_id="minimax-cn",
|
models_dev_provider_id="minimax-cn",
|
||||||
),
|
),
|
||||||
url_preset(
|
url_preset(
|
||||||
|
id="minimax-global-general",
|
||||||
label="国际站 / 通用",
|
label="国际站 / 通用",
|
||||||
value="https://api.minimax.io/anthropic/v1",
|
value="https://api.minimax.io/anthropic/v1",
|
||||||
models_dev_provider_id="minimax",
|
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(
|
url_preset(
|
||||||
|
id="minimax-cn-coding",
|
||||||
label="中国内地 / Coding Plan",
|
label="中国内地 / Coding Plan",
|
||||||
value="https://api.minimaxi.com/anthropic/v1",
|
value="https://api.minimaxi.com/anthropic/v1",
|
||||||
models_dev_provider_id="minimax-cn-coding-plan",
|
models_dev_provider_id="minimax-cn-coding-plan",
|
||||||
),
|
),
|
||||||
url_preset(
|
url_preset(
|
||||||
|
id="minimax-global-coding",
|
||||||
label="国际站 / Coding Plan",
|
label="国际站 / Coding Plan",
|
||||||
value="https://api.minimax.io/anthropic/v1",
|
value="https://api.minimax.io/anthropic/v1",
|
||||||
models_dev_provider_id="minimax-coding-plan",
|
models_dev_provider_id="minimax-coding-plan",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
api_key_hint="填写 MiniMax API Key,可在中国内地与国际站 Coding Plan 目录间切换。",
|
api_key_hint="填写 MiniMax API Key,可在中国内地、国际站、通用与 Coding Plan 目录间切换。",
|
||||||
description="MiniMax Coding Plan Anthropic-compatible 端点。",
|
description="MiniMax Anthropic-compatible 端点,支持通用与 Coding Plan 目录预设。",
|
||||||
),
|
),
|
||||||
catalog_openai_provider(
|
catalog_openai_provider(
|
||||||
provider_id="xiaomi",
|
provider_id="xiaomi",
|
||||||
@@ -697,21 +711,25 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
sort_order=130,
|
sort_order=130,
|
||||||
base_url_presets=(
|
base_url_presets=(
|
||||||
url_preset(
|
url_preset(
|
||||||
|
id="xiaomi-standard",
|
||||||
label="标准端点",
|
label="标准端点",
|
||||||
value="https://api.xiaomimimo.com/v1",
|
value="https://api.xiaomimimo.com/v1",
|
||||||
models_dev_provider_id="xiaomi",
|
models_dev_provider_id="xiaomi",
|
||||||
),
|
),
|
||||||
url_preset(
|
url_preset(
|
||||||
|
id="xiaomi-token-plan-cn",
|
||||||
label="Token Plan / 中国",
|
label="Token Plan / 中国",
|
||||||
value="https://token-plan-cn.xiaomimimo.com/v1",
|
value="https://token-plan-cn.xiaomimimo.com/v1",
|
||||||
models_dev_provider_id="xiaomi-token-plan-cn",
|
models_dev_provider_id="xiaomi-token-plan-cn",
|
||||||
),
|
),
|
||||||
url_preset(
|
url_preset(
|
||||||
|
id="xiaomi-token-plan-sgp",
|
||||||
label="Token Plan / 新加坡",
|
label="Token Plan / 新加坡",
|
||||||
value="https://token-plan-sgp.xiaomimimo.com/v1",
|
value="https://token-plan-sgp.xiaomimimo.com/v1",
|
||||||
models_dev_provider_id="xiaomi-token-plan-sgp",
|
models_dev_provider_id="xiaomi-token-plan-sgp",
|
||||||
),
|
),
|
||||||
url_preset(
|
url_preset(
|
||||||
|
id="xiaomi-token-plan-ams",
|
||||||
label="Token Plan / 欧洲",
|
label="Token Plan / 欧洲",
|
||||||
value="https://token-plan-ams.xiaomimimo.com/v1",
|
value="https://token-plan-ams.xiaomimimo.com/v1",
|
||||||
models_dev_provider_id="xiaomi-token-plan-ams",
|
models_dev_provider_id="xiaomi-token-plan-ams",
|
||||||
@@ -932,11 +950,12 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
async def _get_provider_async(
|
async def _get_provider_async(
|
||||||
self, provider_id: str, force_refresh: bool = False
|
self, provider_id: str, force_refresh: bool = False
|
||||||
) -> ProviderSpec:
|
) -> ProviderSpec:
|
||||||
|
normalized_provider_id = self._normalize_provider_id(provider_id)
|
||||||
try:
|
try:
|
||||||
return self.get_provider(provider_id)
|
return self.get_provider(normalized_provider_id)
|
||||||
except LLMProviderError:
|
except LLMProviderError:
|
||||||
await self.get_models_dev_data(force_refresh=force_refresh)
|
await self.get_models_dev_data(force_refresh=force_refresh)
|
||||||
return self.get_provider(provider_id)
|
return self.get_provider(normalized_provider_id)
|
||||||
|
|
||||||
def _serialize_provider(self, spec: ProviderSpec) -> dict[str, Any]:
|
def _serialize_provider(self, spec: ProviderSpec) -> dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
@@ -946,6 +965,7 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
"default_base_url": self._default_base_url_for_provider(spec) or "",
|
"default_base_url": self._default_base_url_for_provider(spec) or "",
|
||||||
"base_url_presets": [
|
"base_url_presets": [
|
||||||
{
|
{
|
||||||
|
"id": preset.id,
|
||||||
"label": preset.label,
|
"label": preset.label,
|
||||||
"value": self._sanitize_base_url(preset.value) or "",
|
"value": self._sanitize_base_url(preset.value) or "",
|
||||||
}
|
}
|
||||||
@@ -989,7 +1009,7 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
|
|
||||||
def get_provider(self, provider_id: str) -> ProviderSpec:
|
def get_provider(self, provider_id: str) -> ProviderSpec:
|
||||||
"""按 provider id 获取定义。"""
|
"""按 provider id 获取定义。"""
|
||||||
normalized = (provider_id or "").strip().lower()
|
normalized = self._normalize_provider_id(provider_id)
|
||||||
for spec in self._provider_specs():
|
for spec in self._provider_specs():
|
||||||
if spec.id == normalized:
|
if spec.id == normalized:
|
||||||
return spec
|
return spec
|
||||||
@@ -1014,9 +1034,39 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
return cls._sanitize_base_url(spec.base_url_presets[0].value)
|
return cls._sanitize_base_url(spec.base_url_presets[0].value)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _resolve_provider_model_list_base_url(
|
def _normalize_provider_id(cls, provider_id: str) -> str:
|
||||||
cls, spec: ProviderSpec, base_url: Optional[str]
|
normalized = (provider_id or "").strip().lower()
|
||||||
|
if normalized == "minimax-coding":
|
||||||
|
return "minimax"
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _normalize_base_url_preset_id(
|
||||||
|
cls, provider_id: str, base_url_preset_id: Optional[str]
|
||||||
) -> Optional[str]:
|
) -> Optional[str]:
|
||||||
|
normalized_provider_id = cls._normalize_provider_id(provider_id)
|
||||||
|
normalized_preset_id = str(base_url_preset_id or "").strip().lower() or None
|
||||||
|
if not normalized_preset_id:
|
||||||
|
return None
|
||||||
|
if normalized_provider_id == "minimax" and normalized_preset_id == "minimax-coding":
|
||||||
|
return "minimax-cn-coding"
|
||||||
|
return normalized_preset_id
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _resolve_provider_model_list_base_url(
|
||||||
|
cls,
|
||||||
|
spec: ProviderSpec,
|
||||||
|
base_url: Optional[str],
|
||||||
|
base_url_preset_id: Optional[str] = None,
|
||||||
|
) -> Optional[str]:
|
||||||
|
normalized_preset_id = cls._normalize_base_url_preset_id(spec.id, base_url_preset_id)
|
||||||
|
if normalized_preset_id:
|
||||||
|
for preset in spec.base_url_presets:
|
||||||
|
if preset.id != normalized_preset_id:
|
||||||
|
continue
|
||||||
|
preset_value = cls._sanitize_base_url(preset.value)
|
||||||
|
return cls._sanitize_base_url(preset.model_list_base_url) or preset_value
|
||||||
|
|
||||||
normalized_base_url = cls._sanitize_base_url(base_url)
|
normalized_base_url = cls._sanitize_base_url(base_url)
|
||||||
if normalized_base_url:
|
if normalized_base_url:
|
||||||
for preset in spec.base_url_presets:
|
for preset in spec.base_url_presets:
|
||||||
@@ -1037,8 +1087,18 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _resolve_provider_models_dev_provider_id(
|
def _resolve_provider_models_dev_provider_id(
|
||||||
cls, spec: ProviderSpec, base_url: Optional[str]
|
cls,
|
||||||
|
spec: ProviderSpec,
|
||||||
|
base_url: Optional[str],
|
||||||
|
base_url_preset_id: Optional[str] = None,
|
||||||
) -> Optional[str]:
|
) -> Optional[str]:
|
||||||
|
normalized_preset_id = cls._normalize_base_url_preset_id(spec.id, base_url_preset_id)
|
||||||
|
if normalized_preset_id:
|
||||||
|
for preset in spec.base_url_presets:
|
||||||
|
if preset.id != normalized_preset_id:
|
||||||
|
continue
|
||||||
|
return preset.models_dev_provider_id or spec.models_dev_provider_id
|
||||||
|
|
||||||
normalized_base_url = cls._sanitize_base_url(base_url)
|
normalized_base_url = cls._sanitize_base_url(base_url)
|
||||||
if normalized_base_url:
|
if normalized_base_url:
|
||||||
for preset in spec.base_url_presets:
|
for preset in spec.base_url_presets:
|
||||||
@@ -1058,10 +1118,17 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
return spec.models_dev_provider_id
|
return spec.models_dev_provider_id
|
||||||
|
|
||||||
def resolve_model_list_base_url(
|
def resolve_model_list_base_url(
|
||||||
self, provider_id: str, base_url: Optional[str]
|
self,
|
||||||
|
provider_id: str,
|
||||||
|
base_url: Optional[str],
|
||||||
|
base_url_preset_id: Optional[str] = None,
|
||||||
) -> Optional[str]:
|
) -> Optional[str]:
|
||||||
spec = self.get_provider(provider_id)
|
spec = self.get_provider(provider_id)
|
||||||
return self._resolve_provider_model_list_base_url(spec, base_url)
|
return self._resolve_provider_model_list_base_url(
|
||||||
|
spec,
|
||||||
|
base_url,
|
||||||
|
base_url_preset_id=base_url_preset_id,
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _httpx_proxy_key() -> str:
|
def _httpx_proxy_key() -> str:
|
||||||
@@ -1212,21 +1279,33 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
raise LLMProviderError(f"获取 models.dev 数据失败: {err}") from err
|
raise LLMProviderError(f"获取 models.dev 数据失败: {err}") from err
|
||||||
|
|
||||||
async def _models_dev_provider_payload(
|
async def _models_dev_provider_payload(
|
||||||
self, provider_id: str, base_url: Optional[str] = None
|
self,
|
||||||
|
provider_id: str,
|
||||||
|
base_url: Optional[str] = None,
|
||||||
|
base_url_preset_id: Optional[str] = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
spec = await self._get_provider_async(provider_id)
|
spec = await self._get_provider_async(provider_id)
|
||||||
models_dev_provider_id = self._resolve_provider_models_dev_provider_id(
|
models_dev_provider_id = self._resolve_provider_models_dev_provider_id(
|
||||||
spec,
|
spec,
|
||||||
base_url,
|
base_url,
|
||||||
|
base_url_preset_id=base_url_preset_id,
|
||||||
)
|
)
|
||||||
if not models_dev_provider_id:
|
if not models_dev_provider_id:
|
||||||
return {}
|
return {}
|
||||||
return (await self.get_models_dev_data()).get(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, base_url: Optional[str] = None
|
self,
|
||||||
|
provider_id: str,
|
||||||
|
model_id: str,
|
||||||
|
base_url: Optional[str] = None,
|
||||||
|
base_url_preset_id: Optional[str] = None,
|
||||||
) -> dict[str, Any] | None:
|
) -> dict[str, Any] | None:
|
||||||
payload = await self._models_dev_provider_payload(provider_id, base_url=base_url)
|
payload = await self._models_dev_provider_payload(
|
||||||
|
provider_id,
|
||||||
|
base_url=base_url,
|
||||||
|
base_url_preset_id=base_url_preset_id,
|
||||||
|
)
|
||||||
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
|
||||||
@@ -1394,13 +1473,18 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
provider_id: str,
|
provider_id: str,
|
||||||
transport: str = "openai",
|
transport: str = "openai",
|
||||||
base_url: Optional[str] = None,
|
base_url: Optional[str] = None,
|
||||||
|
base_url_preset_id: 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, base_url=base_url)
|
payload = await self._models_dev_provider_payload(
|
||||||
|
provider_id,
|
||||||
|
base_url=base_url,
|
||||||
|
base_url_preset_id=base_url_preset_id,
|
||||||
|
)
|
||||||
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} 暂无可用模型目录")
|
||||||
@@ -1551,11 +1635,16 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
provider_id: str,
|
provider_id: str,
|
||||||
api_key: Optional[str] = None,
|
api_key: Optional[str] = None,
|
||||||
base_url: Optional[str] = None,
|
base_url: Optional[str] = None,
|
||||||
|
base_url_preset_id: Optional[str] = None,
|
||||||
force_refresh: bool = False,
|
force_refresh: bool = False,
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""返回标准化后的模型目录。"""
|
"""返回标准化后的模型目录。"""
|
||||||
spec = await self._get_provider_async(provider_id, force_refresh=force_refresh)
|
spec = await self._get_provider_async(provider_id, force_refresh=force_refresh)
|
||||||
if self._resolve_provider_models_dev_provider_id(spec, base_url):
|
if self._resolve_provider_models_dev_provider_id(
|
||||||
|
spec,
|
||||||
|
base_url,
|
||||||
|
base_url_preset_id=base_url_preset_id,
|
||||||
|
):
|
||||||
# 对依赖 models.dev 的 provider 主动刷新一次缓存,保证“刷新模型列表”
|
# 对依赖 models.dev 的 provider 主动刷新一次缓存,保证“刷新模型列表”
|
||||||
# 在使用目录型 provider 时也能拿到最新参数。
|
# 在使用目录型 provider 时也能拿到最新参数。
|
||||||
if force_refresh:
|
if force_refresh:
|
||||||
@@ -1565,6 +1654,7 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
model=None,
|
model=None,
|
||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
base_url=base_url,
|
base_url=base_url,
|
||||||
|
base_url_preset_id=base_url_preset_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
if spec.model_list_strategy == "google":
|
if spec.model_list_strategy == "google":
|
||||||
@@ -1582,6 +1672,7 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
base_url=self._resolve_provider_model_list_base_url(
|
base_url=self._resolve_provider_model_list_base_url(
|
||||||
spec,
|
spec,
|
||||||
runtime["base_url"],
|
runtime["base_url"],
|
||||||
|
base_url_preset_id=base_url_preset_id,
|
||||||
),
|
),
|
||||||
default_headers=runtime.get("default_headers"),
|
default_headers=runtime.get("default_headers"),
|
||||||
)
|
)
|
||||||
@@ -1591,6 +1682,7 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
provider_id=provider_id,
|
provider_id=provider_id,
|
||||||
transport="anthropic",
|
transport="anthropic",
|
||||||
base_url=base_url,
|
base_url=base_url,
|
||||||
|
base_url_preset_id=base_url_preset_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
if spec.model_list_strategy == "models_dev_only":
|
if spec.model_list_strategy == "models_dev_only":
|
||||||
@@ -1598,6 +1690,7 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
provider_id=provider_id,
|
provider_id=provider_id,
|
||||||
transport="openai",
|
transport="openai",
|
||||||
base_url=base_url,
|
base_url=base_url,
|
||||||
|
base_url_preset_id=base_url_preset_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
# openai-compatible / deepseek 默认走官方 models 端点。
|
# openai-compatible / deepseek 默认走官方 models 端点。
|
||||||
@@ -1607,6 +1700,7 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
base_url=self._resolve_provider_model_list_base_url(
|
base_url=self._resolve_provider_model_list_base_url(
|
||||||
spec,
|
spec,
|
||||||
runtime["base_url"],
|
runtime["base_url"],
|
||||||
|
base_url_preset_id=base_url_preset_id,
|
||||||
),
|
),
|
||||||
default_headers=runtime.get("default_headers"),
|
default_headers=runtime.get("default_headers"),
|
||||||
)
|
)
|
||||||
@@ -1616,6 +1710,7 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
provider_id: str,
|
provider_id: str,
|
||||||
model_id: Optional[str],
|
model_id: Optional[str],
|
||||||
base_url: Optional[str] = None,
|
base_url: Optional[str] = None,
|
||||||
|
base_url_preset_id: Optional[str] = None,
|
||||||
) -> dict[str, Any] | None:
|
) -> dict[str, Any] | None:
|
||||||
if not model_id:
|
if not model_id:
|
||||||
return None
|
return None
|
||||||
@@ -1623,6 +1718,7 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
provider_id,
|
provider_id,
|
||||||
model_id,
|
model_id,
|
||||||
base_url=base_url,
|
base_url=base_url,
|
||||||
|
base_url_preset_id=base_url_preset_id,
|
||||||
)
|
)
|
||||||
if metadata:
|
if metadata:
|
||||||
return metadata
|
return metadata
|
||||||
@@ -2079,13 +2175,19 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
model: Optional[str],
|
model: Optional[str],
|
||||||
api_key: Optional[str] = None,
|
api_key: Optional[str] = None,
|
||||||
base_url: Optional[str] = None,
|
base_url: Optional[str] = None,
|
||||||
|
base_url_preset_id: Optional[str] = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
解析 provider 运行时参数。
|
解析 provider 运行时参数。
|
||||||
|
|
||||||
返回统一结构,供 `LLMHelper` 创建具体 LangChain 模型实例时使用。
|
返回统一结构,供 `LLMHelper` 创建具体 LangChain 模型实例时使用。
|
||||||
"""
|
"""
|
||||||
spec = await self._get_provider_async(provider_id)
|
normalized_provider_id = self._normalize_provider_id(provider_id)
|
||||||
|
normalized_base_url_preset_id = self._normalize_base_url_preset_id(
|
||||||
|
normalized_provider_id,
|
||||||
|
base_url_preset_id,
|
||||||
|
)
|
||||||
|
spec = await self._get_provider_async(normalized_provider_id)
|
||||||
normalized_api_key = str(api_key or "").strip() or None
|
normalized_api_key = str(api_key or "").strip() or None
|
||||||
normalized_base_url = self._sanitize_base_url(base_url)
|
normalized_base_url = self._sanitize_base_url(base_url)
|
||||||
model_record = None
|
model_record = None
|
||||||
@@ -2095,9 +2197,10 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
(
|
(
|
||||||
item
|
item
|
||||||
for item in await self.list_models(
|
for item in await self.list_models(
|
||||||
provider_id,
|
normalized_provider_id,
|
||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
base_url=base_url,
|
base_url=base_url,
|
||||||
|
base_url_preset_id=normalized_base_url_preset_id,
|
||||||
)
|
)
|
||||||
if item["id"] == model
|
if item["id"] == model
|
||||||
),
|
),
|
||||||
@@ -2108,21 +2211,22 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
model_record = None
|
model_record = None
|
||||||
|
|
||||||
result: dict[str, Any] = {
|
result: dict[str, Any] = {
|
||||||
"provider_id": provider_id,
|
"provider_id": normalized_provider_id,
|
||||||
"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(
|
"model_metadata": await self.resolve_model_metadata(
|
||||||
provider_id,
|
normalized_provider_id,
|
||||||
model,
|
model,
|
||||||
base_url=base_url,
|
base_url=base_url,
|
||||||
|
base_url_preset_id=normalized_base_url_preset_id,
|
||||||
),
|
),
|
||||||
"default_headers": None,
|
"default_headers": None,
|
||||||
"use_responses_api": None,
|
"use_responses_api": None,
|
||||||
"auth_mode": "api_key",
|
"auth_mode": "api_key",
|
||||||
}
|
}
|
||||||
|
|
||||||
if provider_id == "chatgpt":
|
if normalized_provider_id == "chatgpt":
|
||||||
auth = None
|
auth = None
|
||||||
try:
|
try:
|
||||||
auth = await self._resolve_chatgpt_oauth()
|
auth = await self._resolve_chatgpt_oauth()
|
||||||
@@ -2160,7 +2264,7 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
|
|
||||||
raise LLMProviderAuthError("请提供 API Key 或完成 ChatGPT 授权")
|
raise LLMProviderAuthError("请提供 API Key 或完成 ChatGPT 授权")
|
||||||
|
|
||||||
if provider_id == "github-copilot":
|
if normalized_provider_id == "github-copilot":
|
||||||
auth = self.get_saved_auth("github-copilot")
|
auth = self.get_saved_auth("github-copilot")
|
||||||
if auth and auth.get("type") == "oauth":
|
if auth and auth.get("type") == "oauth":
|
||||||
token = auth.get("refresh_token") or auth.get("access_token")
|
token = auth.get("refresh_token") or auth.get("access_token")
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ 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,
|
||||||
@@ -29,6 +30,7 @@ class LlmTestRequest(BaseModel):
|
|||||||
thinking_level: Optional[str] = None
|
thinking_level: Optional[str] = None
|
||||||
api_key: Optional[str] = None
|
api_key: Optional[str] = None
|
||||||
base_url: Optional[str] = None
|
base_url: Optional[str] = None
|
||||||
|
base_url_preset: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class LlmProviderAuthStartRequest(BaseModel):
|
class LlmProviderAuthStartRequest(BaseModel):
|
||||||
@@ -64,6 +66,7 @@ async def get_llm_models(
|
|||||||
provider: str,
|
provider: str,
|
||||||
api_key: Optional[str] = None,
|
api_key: Optional[str] = None,
|
||||||
base_url: Optional[str] = None,
|
base_url: Optional[str] = None,
|
||||||
|
base_url_preset: Optional[str] = None,
|
||||||
force_refresh: Optional[bool] = False,
|
force_refresh: Optional[bool] = False,
|
||||||
_: User = Depends(get_current_active_user_async),
|
_: User = Depends(get_current_active_user_async),
|
||||||
):
|
):
|
||||||
@@ -76,6 +79,7 @@ async def get_llm_models(
|
|||||||
provider=provider,
|
provider=provider,
|
||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
base_url=base_url,
|
base_url=base_url,
|
||||||
|
base_url_preset=base_url_preset,
|
||||||
force_refresh=bool(force_refresh),
|
force_refresh=bool(force_refresh),
|
||||||
)
|
)
|
||||||
return schemas.Response(
|
return schemas.Response(
|
||||||
@@ -231,6 +235,7 @@ async def llm_test(
|
|||||||
thinking_level=settings.LLM_THINKING_LEVEL,
|
thinking_level=settings.LLM_THINKING_LEVEL,
|
||||||
api_key=settings.LLM_API_KEY,
|
api_key=settings.LLM_API_KEY,
|
||||||
base_url=settings.LLM_BASE_URL,
|
base_url=settings.LLM_BASE_URL,
|
||||||
|
base_url_preset=settings.LLM_BASE_URL_PRESET,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not payload.provider:
|
if not payload.provider:
|
||||||
@@ -262,6 +267,7 @@ async def llm_test(
|
|||||||
thinking_level=payload.thinking_level,
|
thinking_level=payload.thinking_level,
|
||||||
api_key=payload.api_key,
|
api_key=payload.api_key,
|
||||||
base_url=payload.base_url,
|
base_url=payload.base_url,
|
||||||
|
base_url_preset=payload.base_url_preset,
|
||||||
)
|
)
|
||||||
if not result.get("reply_preview"):
|
if not result.get("reply_preview"):
|
||||||
return schemas.Response(
|
return schemas.Response(
|
||||||
|
|||||||
@@ -515,6 +515,8 @@ class ConfigModel(BaseModel):
|
|||||||
LLM_API_KEY: Optional[str] = None
|
LLM_API_KEY: Optional[str] = None
|
||||||
# LLM基础URL(用于自定义API端点)
|
# LLM基础URL(用于自定义API端点)
|
||||||
LLM_BASE_URL: Optional[str] = "https://api.deepseek.com"
|
LLM_BASE_URL: Optional[str] = "https://api.deepseek.com"
|
||||||
|
# LLM Base URL 预设标识,用于区分同一 Base URL 下的不同模型目录
|
||||||
|
LLM_BASE_URL_PRESET: Optional[str] = None
|
||||||
# LLM最大上下文Token数量(K)
|
# LLM最大上下文Token数量(K)
|
||||||
LLM_MAX_CONTEXT_TOKENS: int = 64
|
LLM_MAX_CONTEXT_TOKENS: int = 64
|
||||||
# LLM温度参数
|
# LLM温度参数
|
||||||
|
|||||||
@@ -1214,9 +1214,15 @@ def _llm_provider_defaults(
|
|||||||
default_base_url = str(provider_meta.get("default_base_url") or "").strip()
|
default_base_url = str(provider_meta.get("default_base_url") or "").strip()
|
||||||
if default_base_url:
|
if default_base_url:
|
||||||
defaults["base_url"] = default_base_url
|
defaults["base_url"] = default_base_url
|
||||||
|
base_url_presets = provider_meta.get("base_url_presets") or []
|
||||||
|
if isinstance(base_url_presets, list) and base_url_presets:
|
||||||
|
preset_id = str((base_url_presets[0] or {}).get("id") or "").strip()
|
||||||
|
if preset_id:
|
||||||
|
defaults["base_url_preset"] = preset_id
|
||||||
|
|
||||||
defaults.setdefault("model", _env_default("LLM_MODEL", ""))
|
defaults.setdefault("model", _env_default("LLM_MODEL", ""))
|
||||||
defaults.setdefault("base_url", _env_default("LLM_BASE_URL", ""))
|
defaults.setdefault("base_url", _env_default("LLM_BASE_URL", ""))
|
||||||
|
defaults.setdefault("base_url_preset", _env_default("LLM_BASE_URL_PRESET", ""))
|
||||||
return defaults
|
return defaults
|
||||||
|
|
||||||
|
|
||||||
@@ -1245,11 +1251,13 @@ def _load_llm_models_inner(payload: dict[str, Any]) -> list[dict[str, Any]]:
|
|||||||
provider_module = _load_llm_provider_module()
|
provider_module = _load_llm_provider_module()
|
||||||
api_key = str(payload.get("api_key") or "").strip() or None
|
api_key = str(payload.get("api_key") or "").strip() or None
|
||||||
base_url = str(payload.get("base_url") or "").strip() or None
|
base_url = str(payload.get("base_url") or "").strip() or None
|
||||||
|
base_url_preset = str(payload.get("base_url_preset") or "").strip() or None
|
||||||
models = asyncio.run(
|
models = asyncio.run(
|
||||||
provider_module.LLMProviderManager().list_models(
|
provider_module.LLMProviderManager().list_models(
|
||||||
provider_id=provider,
|
provider_id=provider,
|
||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
base_url=base_url,
|
base_url=base_url,
|
||||||
|
base_url_preset_id=base_url_preset,
|
||||||
force_refresh=False,
|
force_refresh=False,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -1261,12 +1269,14 @@ def _load_llm_models(
|
|||||||
provider: str,
|
provider: str,
|
||||||
api_key: Optional[str],
|
api_key: Optional[str],
|
||||||
base_url: Optional[str],
|
base_url: Optional[str],
|
||||||
|
base_url_preset: Optional[str],
|
||||||
runtime_python: Optional[Path] = None,
|
runtime_python: Optional[Path] = None,
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
payload = {
|
payload = {
|
||||||
"provider": str(provider or "").strip().lower(),
|
"provider": str(provider or "").strip().lower(),
|
||||||
"api_key": str(api_key or "").strip(),
|
"api_key": str(api_key or "").strip(),
|
||||||
"base_url": str(base_url or "").strip(),
|
"base_url": str(base_url or "").strip(),
|
||||||
|
"base_url_preset": str(base_url_preset or "").strip(),
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
return _load_llm_models_inner(payload)
|
return _load_llm_models_inner(payload)
|
||||||
@@ -1795,6 +1805,9 @@ def _collect_agent_config(
|
|||||||
defaults = _llm_provider_defaults(provider, provider_definitions)
|
defaults = _llm_provider_defaults(provider, provider_definitions)
|
||||||
current_model = _env_default("LLM_MODEL", defaults["model"])
|
current_model = _env_default("LLM_MODEL", defaults["model"])
|
||||||
current_base_url = _env_default("LLM_BASE_URL", defaults["base_url"])
|
current_base_url = _env_default("LLM_BASE_URL", defaults["base_url"])
|
||||||
|
current_base_url_preset = _env_default(
|
||||||
|
"LLM_BASE_URL_PRESET", defaults.get("base_url_preset", "")
|
||||||
|
)
|
||||||
api_key_label = str(provider_meta.get("api_key_label") or "API Key").strip() or "API Key"
|
api_key_label = str(provider_meta.get("api_key_label") or "API Key").strip() or "API Key"
|
||||||
api_key_hint = str(provider_meta.get("api_key_hint") or "").strip()
|
api_key_hint = str(provider_meta.get("api_key_hint") or "").strip()
|
||||||
requires_base_url = bool(provider_meta.get("requires_base_url"))
|
requires_base_url = bool(provider_meta.get("requires_base_url"))
|
||||||
@@ -1838,8 +1851,41 @@ def _collect_agent_config(
|
|||||||
"是否启用图片输入支持",
|
"是否启用图片输入支持",
|
||||||
default=_env_bool("LLM_SUPPORT_IMAGE_INPUT", True),
|
default=_env_bool("LLM_SUPPORT_IMAGE_INPUT", True),
|
||||||
),
|
),
|
||||||
|
"LLM_BASE_URL_PRESET": current_base_url_preset,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
base_url_presets = provider_meta.get("base_url_presets") or []
|
||||||
|
if isinstance(base_url_presets, list):
|
||||||
|
duplicate_value_presets = []
|
||||||
|
normalized_current_base_url = current_base_url.strip()
|
||||||
|
for item in base_url_presets:
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
continue
|
||||||
|
preset_value = str(item.get("value") or "").strip()
|
||||||
|
preset_id = str(item.get("id") or "").strip()
|
||||||
|
if not preset_id or preset_value != normalized_current_base_url:
|
||||||
|
continue
|
||||||
|
duplicate_value_presets.append(item)
|
||||||
|
|
||||||
|
if len(duplicate_value_presets) > 1:
|
||||||
|
choices: dict[str, str] = {}
|
||||||
|
default_preset = current_base_url_preset
|
||||||
|
if not default_preset or default_preset not in {
|
||||||
|
str(item.get("id") or "").strip() for item in duplicate_value_presets
|
||||||
|
}:
|
||||||
|
default_preset = str((duplicate_value_presets[0] or {}).get("id") or "").strip()
|
||||||
|
for item in duplicate_value_presets:
|
||||||
|
preset_id = str(item.get("id") or "").strip()
|
||||||
|
preset_label = str(item.get("label") or preset_id).strip()
|
||||||
|
if preset_id:
|
||||||
|
choices[preset_id] = preset_label
|
||||||
|
if choices:
|
||||||
|
config["LLM_BASE_URL_PRESET"] = _prompt_choice(
|
||||||
|
"LLM Base URL 预设",
|
||||||
|
choices=choices,
|
||||||
|
default=default_preset,
|
||||||
|
)
|
||||||
|
|
||||||
config["LLM_BASE_URL"] = _prompt_text(
|
config["LLM_BASE_URL"] = _prompt_text(
|
||||||
base_url_label,
|
base_url_label,
|
||||||
default=current_base_url,
|
default=current_base_url,
|
||||||
@@ -1849,6 +1895,7 @@ def _collect_agent_config(
|
|||||||
provider=provider,
|
provider=provider,
|
||||||
api_key=config["LLM_API_KEY"],
|
api_key=config["LLM_API_KEY"],
|
||||||
base_url=config["LLM_BASE_URL"],
|
base_url=config["LLM_BASE_URL"],
|
||||||
|
base_url_preset=config["LLM_BASE_URL_PRESET"],
|
||||||
runtime_python=runtime_python,
|
runtime_python=runtime_python,
|
||||||
)
|
)
|
||||||
config["LLM_MODEL"] = _prompt_model_choice(models, default=current_model)
|
config["LLM_MODEL"] = _prompt_model_choice(models, default=current_model)
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ _stub_module(
|
|||||||
LLM_MODEL="global-model",
|
LLM_MODEL="global-model",
|
||||||
LLM_API_KEY="global-key",
|
LLM_API_KEY="global-key",
|
||||||
LLM_BASE_URL="https://global.example.com",
|
LLM_BASE_URL="https://global.example.com",
|
||||||
|
LLM_BASE_URL_PRESET=None,
|
||||||
LLM_THINKING_LEVEL=None,
|
LLM_THINKING_LEVEL=None,
|
||||||
LLM_TEMPERATURE=0.1,
|
LLM_TEMPERATURE=0.1,
|
||||||
LLM_MAX_CONTEXT_TOKENS=64,
|
LLM_MAX_CONTEXT_TOKENS=64,
|
||||||
@@ -76,6 +77,7 @@ class LlmHelperTestCallTest(unittest.TestCase):
|
|||||||
model="deepseek-chat",
|
model="deepseek-chat",
|
||||||
api_key="sk-test",
|
api_key="sk-test",
|
||||||
base_url="https://api.deepseek.com",
|
base_url="https://api.deepseek.com",
|
||||||
|
base_url_preset="deepseek-default",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -86,6 +88,7 @@ class LlmHelperTestCallTest(unittest.TestCase):
|
|||||||
thinking_level=None,
|
thinking_level=None,
|
||||||
api_key="sk-test",
|
api_key="sk-test",
|
||||||
base_url="https://api.deepseek.com",
|
base_url="https://api.deepseek.com",
|
||||||
|
base_url_preset="deepseek-default",
|
||||||
)
|
)
|
||||||
self.assertEqual(result["provider"], "deepseek")
|
self.assertEqual(result["provider"], "deepseek")
|
||||||
self.assertEqual(result["model"], "deepseek-chat")
|
self.assertEqual(result["model"], "deepseek-chat")
|
||||||
|
|||||||
@@ -246,6 +246,54 @@ class LlmProviderRegistryTest(unittest.TestCase):
|
|||||||
self.assertIsNone(provider.models_dev_provider_id)
|
self.assertIsNone(provider.models_dev_provider_id)
|
||||||
self.assertFalse(provider.supports_model_refresh)
|
self.assertFalse(provider.supports_model_refresh)
|
||||||
|
|
||||||
|
def test_builtin_minimax_provider_merges_general_and_coding_presets(self):
|
||||||
|
manager = LLMProviderManager()
|
||||||
|
|
||||||
|
provider = manager.get_provider("minimax")
|
||||||
|
serialized = manager.list_providers()
|
||||||
|
minimax_payload = next(item for item in serialized if item["id"] == "minimax")
|
||||||
|
|
||||||
|
self.assertEqual(provider.name, "MiniMax")
|
||||||
|
self.assertEqual(provider.runtime, "anthropic_compatible")
|
||||||
|
self.assertEqual(
|
||||||
|
tuple((preset.id, preset.label, preset.value) for preset in provider.base_url_presets),
|
||||||
|
(
|
||||||
|
("minimax-cn-general", "中国内地 / 通用", "https://api.minimaxi.com/anthropic/v1"),
|
||||||
|
("minimax-global-general", "国际站 / 通用", "https://api.minimax.io/anthropic/v1"),
|
||||||
|
("minimax-cn-coding", "中国内地 / Coding Plan", "https://api.minimaxi.com/anthropic/v1"),
|
||||||
|
("minimax-global-coding", "国际站 / Coding Plan", "https://api.minimax.io/anthropic/v1"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
tuple(item["id"] for item in minimax_payload["base_url_presets"]),
|
||||||
|
(
|
||||||
|
"minimax-cn-general",
|
||||||
|
"minimax-global-general",
|
||||||
|
"minimax-cn-coding",
|
||||||
|
"minimax-global-coding",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_minimax_coding_alias_resolves_to_minimax_provider(self):
|
||||||
|
manager = LLMProviderManager()
|
||||||
|
|
||||||
|
provider = manager.get_provider("minimax-coding")
|
||||||
|
|
||||||
|
self.assertEqual(provider.id, "minimax")
|
||||||
|
|
||||||
|
def test_resolve_models_dev_provider_id_prefers_minimax_preset_id(self):
|
||||||
|
manager = LLMProviderManager()
|
||||||
|
provider = manager.get_provider("minimax")
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
manager._resolve_provider_models_dev_provider_id(
|
||||||
|
provider,
|
||||||
|
base_url="https://api.minimaxi.com/anthropic/v1",
|
||||||
|
base_url_preset_id="minimax-cn-coding",
|
||||||
|
),
|
||||||
|
"minimax-cn-coding-plan",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ class LocalSetupLlmProviderPromptTests(unittest.TestCase):
|
|||||||
provider="frogbot",
|
provider="frogbot",
|
||||||
api_key="sk-frog",
|
api_key="sk-frog",
|
||||||
base_url="https://override.example.com/v1",
|
base_url="https://override.example.com/v1",
|
||||||
|
base_url_preset="",
|
||||||
runtime_python=Path("/tmp/runtime-python"),
|
runtime_python=Path("/tmp/runtime-python"),
|
||||||
)
|
)
|
||||||
model_prompt.assert_called_once_with(models, default="")
|
model_prompt.assert_called_once_with(models, default="")
|
||||||
@@ -205,6 +206,66 @@ class LocalSetupLlmProviderPromptTests(unittest.TestCase):
|
|||||||
"https://modelservice.jdcloud.com/v1",
|
"https://modelservice.jdcloud.com/v1",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_collect_agent_config_prompts_for_duplicate_base_url_presets(self):
|
||||||
|
module = load_local_setup_module()
|
||||||
|
|
||||||
|
provider_definitions = [
|
||||||
|
{
|
||||||
|
"id": "minimax",
|
||||||
|
"name": "MiniMax",
|
||||||
|
"default_base_url": "https://api.minimaxi.com/anthropic/v1",
|
||||||
|
"api_key_label": "API Key",
|
||||||
|
"base_url_presets": [
|
||||||
|
{
|
||||||
|
"id": "minimax-cn-general",
|
||||||
|
"label": "中国内地 / 通用",
|
||||||
|
"value": "https://api.minimaxi.com/anthropic/v1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "minimax-cn-coding",
|
||||||
|
"label": "中国内地 / Coding Plan",
|
||||||
|
"value": "https://api.minimaxi.com/anthropic/v1",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
with patch.object(module, "print_step"), patch.object(
|
||||||
|
module, "_prompt_yes_no", side_effect=[True, False, True]
|
||||||
|
), patch.object(
|
||||||
|
module, "_load_llm_provider_definitions", return_value=provider_definitions
|
||||||
|
), patch.object(
|
||||||
|
module, "_prompt_provider_choice", return_value="minimax"
|
||||||
|
), patch.object(
|
||||||
|
module, "_prompt_text", side_effect=["https://api.minimaxi.com/anthropic/v1"]
|
||||||
|
), patch.object(
|
||||||
|
module, "_prompt_secret_text", return_value="sk-minimax"
|
||||||
|
), patch.object(
|
||||||
|
module, "_load_llm_models", return_value=[]
|
||||||
|
) as load_models, patch.object(
|
||||||
|
module, "_prompt_model_choice", return_value="MiniMax-M1"
|
||||||
|
), patch.object(
|
||||||
|
module, "read_env_value", return_value=None
|
||||||
|
), patch.object(
|
||||||
|
module, "_env_default", side_effect=lambda key, default="": default
|
||||||
|
), patch.object(
|
||||||
|
module, "_env_bool", side_effect=lambda key, default: default
|
||||||
|
), patch.object(
|
||||||
|
module, "_env_llm_thinking_level_default", return_value="auto"
|
||||||
|
), patch.object(
|
||||||
|
module, "_prompt_choice", side_effect=["auto", "minimax-cn-coding"]
|
||||||
|
):
|
||||||
|
config = module._collect_agent_config()
|
||||||
|
|
||||||
|
self.assertEqual(config["LLM_BASE_URL_PRESET"], "minimax-cn-coding")
|
||||||
|
load_models.assert_called_once_with(
|
||||||
|
provider="minimax",
|
||||||
|
api_key="sk-minimax",
|
||||||
|
base_url="https://api.minimaxi.com/anthropic/v1",
|
||||||
|
base_url_preset="minimax-cn-coding",
|
||||||
|
runtime_python=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ _stub_module("app.utils.crypto", HashUtils=_Dummy)
|
|||||||
_stub_module("app.utils.http", RequestUtils=_Dummy, AsyncRequestUtils=_Dummy)
|
_stub_module("app.utils.http", RequestUtils=_Dummy, AsyncRequestUtils=_Dummy)
|
||||||
_stub_module("version", APP_VERSION="test")
|
_stub_module("version", APP_VERSION="test")
|
||||||
|
|
||||||
from app.api.endpoints import system as system_endpoint
|
from app.api.endpoints import llm as system_endpoint
|
||||||
|
|
||||||
|
|
||||||
class LlmTestEndpointTest(unittest.TestCase):
|
class LlmTestEndpointTest(unittest.TestCase):
|
||||||
@@ -124,6 +124,8 @@ class LlmTestEndpointTest(unittest.TestCase):
|
|||||||
system_endpoint.settings, "LLM_API_KEY", "sk-test"
|
system_endpoint.settings, "LLM_API_KEY", "sk-test"
|
||||||
), patch.object(
|
), patch.object(
|
||||||
system_endpoint.settings, "LLM_BASE_URL", "https://api.deepseek.com"
|
system_endpoint.settings, "LLM_BASE_URL", "https://api.deepseek.com"
|
||||||
|
), patch.object(
|
||||||
|
system_endpoint.settings, "LLM_BASE_URL_PRESET", "deepseek-default"
|
||||||
), patch.object(
|
), patch.object(
|
||||||
system_endpoint.LLMHelper,
|
system_endpoint.LLMHelper,
|
||||||
"test_current_settings",
|
"test_current_settings",
|
||||||
@@ -138,6 +140,7 @@ class LlmTestEndpointTest(unittest.TestCase):
|
|||||||
thinking_level="max",
|
thinking_level="max",
|
||||||
api_key="sk-test",
|
api_key="sk-test",
|
||||||
base_url="https://api.deepseek.com",
|
base_url="https://api.deepseek.com",
|
||||||
|
base_url_preset="deepseek-default",
|
||||||
)
|
)
|
||||||
self.assertTrue(resp.success)
|
self.assertTrue(resp.success)
|
||||||
self.assertEqual(resp.data["provider"], "deepseek")
|
self.assertEqual(resp.data["provider"], "deepseek")
|
||||||
@@ -161,6 +164,7 @@ class LlmTestEndpointTest(unittest.TestCase):
|
|||||||
thinking_level="high",
|
thinking_level="high",
|
||||||
api_key="sk-live",
|
api_key="sk-live",
|
||||||
base_url="https://example.com/v1",
|
base_url="https://example.com/v1",
|
||||||
|
base_url_preset="openai-default",
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch.object(system_endpoint.settings, "AI_AGENT_ENABLE", False), patch.object(
|
with patch.object(system_endpoint.settings, "AI_AGENT_ENABLE", False), patch.object(
|
||||||
@@ -183,6 +187,7 @@ class LlmTestEndpointTest(unittest.TestCase):
|
|||||||
thinking_level="high",
|
thinking_level="high",
|
||||||
api_key="sk-live",
|
api_key="sk-live",
|
||||||
base_url="https://example.com/v1",
|
base_url="https://example.com/v1",
|
||||||
|
base_url_preset="openai-default",
|
||||||
)
|
)
|
||||||
self.assertTrue(resp.success)
|
self.assertTrue(resp.success)
|
||||||
self.assertEqual(resp.data["provider"], "openai")
|
self.assertEqual(resp.data["provider"], "openai")
|
||||||
@@ -203,6 +208,7 @@ class LlmTestEndpointTest(unittest.TestCase):
|
|||||||
model="deepseek-v4-pro",
|
model="deepseek-v4-pro",
|
||||||
api_key="sk-live",
|
api_key="sk-live",
|
||||||
base_url="https://api.deepseek.com",
|
base_url="https://api.deepseek.com",
|
||||||
|
base_url_preset="deepseek-default",
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch.object(system_endpoint.settings, "AI_AGENT_ENABLE", False), patch.object(
|
with patch.object(system_endpoint.settings, "AI_AGENT_ENABLE", False), patch.object(
|
||||||
@@ -219,6 +225,7 @@ class LlmTestEndpointTest(unittest.TestCase):
|
|||||||
thinking_level=None,
|
thinking_level=None,
|
||||||
api_key="sk-live",
|
api_key="sk-live",
|
||||||
base_url="https://api.deepseek.com",
|
base_url="https://api.deepseek.com",
|
||||||
|
base_url_preset="deepseek-default",
|
||||||
)
|
)
|
||||||
self.assertTrue(resp.success)
|
self.assertTrue(resp.success)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user