mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-07 08:26:53 +08:00
refactor(agent): consolidate provider preset resolution
This commit is contained in:
+147
-135
@@ -51,6 +51,8 @@ class ProviderUrlPreset:
|
|||||||
id: str
|
id: str
|
||||||
label: str
|
label: str
|
||||||
value: str
|
value: str
|
||||||
|
runtime: Optional[str] = None
|
||||||
|
model_list_strategy: Optional[str] = None
|
||||||
model_list_base_url: Optional[str] = None
|
model_list_base_url: Optional[str] = None
|
||||||
models_dev_provider_id: Optional[str] = None
|
models_dev_provider_id: Optional[str] = None
|
||||||
|
|
||||||
@@ -107,17 +109,6 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
_CHATGPT_CODEX_BASE_URL = "https://chatgpt.com/backend-api/codex"
|
_CHATGPT_CODEX_BASE_URL = "https://chatgpt.com/backend-api/codex"
|
||||||
_COPILOT_CLIENT_ID = "Ov23li8tweQw6odWQebz"
|
_COPILOT_CLIENT_ID = "Ov23li8tweQw6odWQebz"
|
||||||
_DEFAULT_TIMEOUT = httpx.Timeout(15.0, connect=10.0)
|
_DEFAULT_TIMEOUT = httpx.Timeout(15.0, connect=10.0)
|
||||||
_CHATGPT_ALLOWED_OAUTH_MODELS = {
|
|
||||||
"gpt-5.1-codex",
|
|
||||||
"gpt-5.1-codex-max",
|
|
||||||
"gpt-5.1-codex-mini",
|
|
||||||
"gpt-5.2",
|
|
||||||
"gpt-5.2-codex",
|
|
||||||
"gpt-5.3-codex",
|
|
||||||
"gpt-5.4",
|
|
||||||
"gpt-5.4-mini",
|
|
||||||
"gpt-5.5",
|
|
||||||
}
|
|
||||||
_MODELS_DEV_DYNAMIC_SKIP_IDS = {
|
_MODELS_DEV_DYNAMIC_SKIP_IDS = {
|
||||||
"aihubmix",
|
"aihubmix",
|
||||||
"amazon-bedrock",
|
"amazon-bedrock",
|
||||||
@@ -133,7 +124,7 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
"v0",
|
"v0",
|
||||||
"vercel",
|
"vercel",
|
||||||
}
|
}
|
||||||
_MODELS_DEV_DYNAMIC_PROVIDER_OVERRIDES = {
|
_PROVIDER_PATCHES = {
|
||||||
"bailing": {
|
"bailing": {
|
||||||
"runtime": "openai_compatible",
|
"runtime": "openai_compatible",
|
||||||
"default_base_url": "https://api.tbox.cn/api/llm/v1",
|
"default_base_url": "https://api.tbox.cn/api/llm/v1",
|
||||||
@@ -169,6 +160,14 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
"default_base_url": "https://api.venice.ai/api/v1",
|
"default_base_url": "https://api.venice.ai/api/v1",
|
||||||
"description": "Venice AI 官方兼容端点。",
|
"description": "Venice AI 官方兼容端点。",
|
||||||
},
|
},
|
||||||
|
"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 端点。",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -203,6 +202,7 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
description="适合无回调环境,复制设备码到浏览器完成登录。",
|
description="适合无回调环境,复制设备码到浏览器完成登录。",
|
||||||
)
|
)
|
||||||
url_preset = ProviderUrlPreset
|
url_preset = ProviderUrlPreset
|
||||||
|
provider_patches = LLMProviderManager._PROVIDER_PATCHES
|
||||||
def openai_provider(
|
def openai_provider(
|
||||||
provider_id: str,
|
provider_id: str,
|
||||||
name: str,
|
name: str,
|
||||||
@@ -280,28 +280,18 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
)
|
)
|
||||||
|
|
||||||
catalog_openai_providers = (
|
catalog_openai_providers = (
|
||||||
("huggingface", "Hugging Face", "https://router.huggingface.co/v1"),
|
{"id": "huggingface", "name": "Hugging Face", "base_url": "https://router.huggingface.co/v1"},
|
||||||
("jiekou", "接口 AI", "https://api.jiekou.ai/openai"),
|
{"id": "jiekou", "name": "接口 AI", "base_url": "https://api.jiekou.ai/openai"},
|
||||||
("kilo", "Kilo Gateway", "https://api.kilo.ai/api/gateway"),
|
{"id": "kilo", "name": "Kilo Gateway", "base_url": "https://api.kilo.ai/api/gateway"},
|
||||||
("llama", "Llama", "https://api.llama.com/compat/v1/"),
|
{"id": "llama", "name": "Llama", "base_url": "https://api.llama.com/compat/v1/"},
|
||||||
("llmgateway", "LLM Gateway", "https://api.llmgateway.io/v1"),
|
{"id": "llmgateway", "name": "LLM Gateway", "base_url": "https://api.llmgateway.io/v1"},
|
||||||
("modelscope", "ModelScope", "https://api-inference.modelscope.cn/v1"),
|
{"id": "modelscope", "name": "ModelScope", "base_url": "https://api-inference.modelscope.cn/v1"},
|
||||||
("nova", "Nova", "https://api.nova.amazon.com/v1"),
|
{"id": "nova", "name": "Nova", "base_url": "https://api.nova.amazon.com/v1"},
|
||||||
("fireworks-ai", "Fireworks AI", "https://api.fireworks.ai/inference/v1/"),
|
{"id": "fireworks-ai", "name": "Fireworks AI", "base_url": "https://api.fireworks.ai/inference/v1/"},
|
||||||
("poe", "Poe", "https://api.poe.com/v1"),
|
{"id": "poe", "name": "Poe", "base_url": "https://api.poe.com/v1"},
|
||||||
("qihang-ai", "启航 AI", "https://api.qhaigc.net/v1"),
|
{"id": "qihang-ai", "name": "启航 AI", "base_url": "https://api.qhaigc.net/v1"},
|
||||||
("qiniu-ai", "七牛", "https://api.qnaigc.com/v1"),
|
{"id": "qiniu-ai", "name": "七牛", "base_url": "https://api.qnaigc.com/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 = [
|
providers = [
|
||||||
ProviderSpec(
|
ProviderSpec(
|
||||||
@@ -436,7 +426,7 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
),
|
),
|
||||||
catalog_openai_provider(
|
catalog_openai_provider(
|
||||||
provider_id="moonshot",
|
provider_id="moonshot",
|
||||||
name="Moonshot AI",
|
name="Moonshot / Kimi",
|
||||||
default_base_url="https://api.moonshot.cn/v1",
|
default_base_url="https://api.moonshot.cn/v1",
|
||||||
sort_order=62,
|
sort_order=62,
|
||||||
models_dev_provider_id="moonshotai-cn",
|
models_dev_provider_id="moonshotai-cn",
|
||||||
@@ -453,18 +443,17 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
value="https://api.moonshot.ai/v1",
|
value="https://api.moonshot.ai/v1",
|
||||||
models_dev_provider_id="moonshotai",
|
models_dev_provider_id="moonshotai",
|
||||||
),
|
),
|
||||||
|
url_preset(
|
||||||
|
id="moonshot-kimi-coding",
|
||||||
|
label="Kimi for Coding",
|
||||||
|
value="https://api.kimi.com/coding/v1",
|
||||||
|
runtime="anthropic_compatible",
|
||||||
|
model_list_strategy="anthropic_compatible",
|
||||||
|
models_dev_provider_id="kimi-for-coding",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
api_key_hint="填写 Moonshot / Kimi API Key,可在中国站与国际站端点间切换。",
|
api_key_hint="填写 Moonshot / Kimi API Key,可在中国站、国际站与 Kimi for Coding 端点间切换。",
|
||||||
description="Moonshot / Kimi 官方兼容端点。",
|
description="Moonshot / Kimi 官方端点,支持通用 API 与 Kimi for Coding 预设。",
|
||||||
),
|
|
||||||
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(
|
openai_provider(
|
||||||
provider_id="zhipu",
|
provider_id="zhipu",
|
||||||
@@ -740,16 +729,17 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
for sort_order, (provider_id, name, base_url) in enumerate(
|
for sort_order, provider_entry in enumerate(
|
||||||
catalog_openai_providers,
|
catalog_openai_providers,
|
||||||
start=200,
|
start=200,
|
||||||
):
|
):
|
||||||
overrides = catalog_openai_overrides.get(provider_id, {})
|
provider_id = provider_entry["id"]
|
||||||
|
overrides = provider_patches.get(provider_id, {})
|
||||||
providers.append(
|
providers.append(
|
||||||
catalog_openai_provider(
|
catalog_openai_provider(
|
||||||
provider_id=provider_id,
|
provider_id=provider_id,
|
||||||
name=name,
|
name=provider_entry["name"],
|
||||||
default_base_url=base_url,
|
default_base_url=provider_entry["base_url"],
|
||||||
sort_order=sort_order,
|
sort_order=sort_order,
|
||||||
api_key_hint=overrides.get("api_key_hint"),
|
api_key_hint=overrides.get("api_key_hint"),
|
||||||
description=overrides.get("description"),
|
description=overrides.get("description"),
|
||||||
@@ -846,7 +836,7 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
if not normalized_id or normalized_id in cls._MODELS_DEV_DYNAMIC_SKIP_IDS:
|
if not normalized_id or normalized_id in cls._MODELS_DEV_DYNAMIC_SKIP_IDS:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
override = cls._MODELS_DEV_DYNAMIC_PROVIDER_OVERRIDES.get(normalized_id, {})
|
override = cls._PROVIDER_PATCHES.get(normalized_id, {})
|
||||||
npm_package = str(payload.get("npm") or "").strip()
|
npm_package = str(payload.get("npm") or "").strip()
|
||||||
runtime = override.get("runtime")
|
runtime = override.get("runtime")
|
||||||
if not runtime:
|
if not runtime:
|
||||||
@@ -968,6 +958,8 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
"id": preset.id,
|
"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 "",
|
||||||
|
"runtime": preset.runtime,
|
||||||
|
"model_list_strategy": preset.model_list_strategy,
|
||||||
}
|
}
|
||||||
for preset in spec.base_url_presets
|
for preset in spec.base_url_presets
|
||||||
],
|
],
|
||||||
@@ -1038,6 +1030,8 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
normalized = (provider_id or "").strip().lower()
|
normalized = (provider_id or "").strip().lower()
|
||||||
if normalized == "minimax-coding":
|
if normalized == "minimax-coding":
|
||||||
return "minimax"
|
return "minimax"
|
||||||
|
if normalized == "kimi-coding":
|
||||||
|
return "moonshot"
|
||||||
return normalized
|
return normalized
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -1050,8 +1044,59 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
return None
|
return None
|
||||||
if normalized_provider_id == "minimax" and normalized_preset_id == "minimax-coding":
|
if normalized_provider_id == "minimax" and normalized_preset_id == "minimax-coding":
|
||||||
return "minimax-cn-coding"
|
return "minimax-cn-coding"
|
||||||
|
if normalized_provider_id == "moonshot" and normalized_preset_id == "kimi-coding":
|
||||||
|
return "moonshot-kimi-coding"
|
||||||
return normalized_preset_id
|
return normalized_preset_id
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _resolve_provider_preset(
|
||||||
|
cls,
|
||||||
|
spec: ProviderSpec,
|
||||||
|
base_url: Optional[str],
|
||||||
|
base_url_preset_id: Optional[str] = None,
|
||||||
|
) -> Optional[ProviderUrlPreset]:
|
||||||
|
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:
|
||||||
|
return preset
|
||||||
|
|
||||||
|
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:
|
||||||
|
return preset
|
||||||
|
return None
|
||||||
|
|
||||||
|
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:
|
||||||
|
return preset
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _resolve_provider_runtime(
|
||||||
|
cls,
|
||||||
|
spec: ProviderSpec,
|
||||||
|
base_url: Optional[str],
|
||||||
|
base_url_preset_id: Optional[str] = None,
|
||||||
|
) -> str:
|
||||||
|
preset = cls._resolve_provider_preset(spec, base_url, base_url_preset_id)
|
||||||
|
return preset.runtime or spec.runtime if preset else spec.runtime
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _resolve_provider_model_list_strategy(
|
||||||
|
cls,
|
||||||
|
spec: ProviderSpec,
|
||||||
|
base_url: Optional[str],
|
||||||
|
base_url_preset_id: Optional[str] = None,
|
||||||
|
) -> str:
|
||||||
|
preset = cls._resolve_provider_preset(spec, base_url, base_url_preset_id)
|
||||||
|
return preset.model_list_strategy or spec.model_list_strategy if preset else spec.model_list_strategy
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _resolve_provider_model_list_base_url(
|
def _resolve_provider_model_list_base_url(
|
||||||
cls,
|
cls,
|
||||||
@@ -1059,31 +1104,16 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
base_url: Optional[str],
|
base_url: Optional[str],
|
||||||
base_url_preset_id: Optional[str] = None,
|
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)
|
preset = cls._resolve_provider_preset(spec, base_url, base_url_preset_id)
|
||||||
if normalized_preset_id:
|
if preset:
|
||||||
for preset in spec.base_url_presets:
|
preset_value = cls._sanitize_base_url(preset.value)
|
||||||
if preset.id != normalized_preset_id:
|
return cls._sanitize_base_url(preset.model_list_base_url) or preset_value
|
||||||
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:
|
|
||||||
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
|
return normalized_base_url
|
||||||
|
|
||||||
default_base_url = cls._default_base_url_for_provider(spec)
|
return 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
|
@classmethod
|
||||||
def _resolve_provider_models_dev_provider_id(
|
def _resolve_provider_models_dev_provider_id(
|
||||||
@@ -1092,29 +1122,14 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
base_url: Optional[str],
|
base_url: Optional[str],
|
||||||
base_url_preset_id: Optional[str] = None,
|
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)
|
preset = cls._resolve_provider_preset(spec, base_url, base_url_preset_id)
|
||||||
if normalized_preset_id:
|
if preset:
|
||||||
for preset in spec.base_url_presets:
|
return preset.models_dev_provider_id or spec.models_dev_provider_id
|
||||||
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:
|
|
||||||
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
|
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
|
return spec.models_dev_provider_id
|
||||||
|
|
||||||
def resolve_model_list_base_url(
|
def resolve_model_list_base_url(
|
||||||
@@ -1584,50 +1599,33 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
)
|
)
|
||||||
return sorted(results, key=lambda i: i["name"].lower())
|
return sorted(results, key=lambda i: i["name"].lower())
|
||||||
|
|
||||||
async def _list_chatgpt_oauth_models(self) -> list[dict[str, Any]]:
|
async def _list_chatgpt_oauth_models(
|
||||||
payload = await self._models_dev_provider_payload("chatgpt")
|
self,
|
||||||
|
provider_id: str,
|
||||||
|
base_url: Optional[str] = None,
|
||||||
|
base_url_preset_id: Optional[str] = None,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
# ChatGPT OAuth 仍然是 chatgpt provider 专属能力,但模型目录不再维护
|
||||||
|
# 一份内部名单,直接跟随当前 provider 对应的 models.dev 数据。
|
||||||
|
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 [
|
return []
|
||||||
{
|
|
||||||
"id": model_id,
|
|
||||||
"name": model_id,
|
|
||||||
"context_tokens": None,
|
|
||||||
"input_tokens": None,
|
|
||||||
"output_tokens": None,
|
|
||||||
"context_tokens_k": settings.LLM_MAX_CONTEXT_TOKENS,
|
|
||||||
"supports_reasoning": True,
|
|
||||||
"supports_tools": True,
|
|
||||||
"supports_image_input": True,
|
|
||||||
"supports_audio_input": False,
|
|
||||||
"transport": "openai",
|
|
||||||
"source": "builtin",
|
|
||||||
"release_date": None,
|
|
||||||
"status": None,
|
|
||||||
}
|
|
||||||
for model_id in sorted(self._CHATGPT_ALLOWED_OAUTH_MODELS)
|
|
||||||
]
|
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
for model_id, metadata in models.items():
|
for model_id, metadata in models.items():
|
||||||
if "codex" in model_id or model_id in self._CHATGPT_ALLOWED_OAUTH_MODELS:
|
results.append(
|
||||||
match = None
|
self._normalize_model_record(
|
||||||
if model_id.startswith("gpt-"):
|
model_id=model_id,
|
||||||
try:
|
display_name=metadata.get("name") or model_id,
|
||||||
match = float(model_id.split("-")[1].replace(".mini", ""))
|
metadata=metadata,
|
||||||
except Exception as err:
|
source="models.dev",
|
||||||
print(err)
|
|
||||||
match = None
|
|
||||||
if match is not None and match > 5.4 and "codex" not in model_id:
|
|
||||||
continue
|
|
||||||
results.append(
|
|
||||||
self._normalize_model_record(
|
|
||||||
model_id=model_id,
|
|
||||||
display_name=metadata.get("name") or model_id,
|
|
||||||
metadata=metadata,
|
|
||||||
source="models.dev",
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
return sorted(results, key=lambda item: item["name"].lower())
|
return sorted(results, key=lambda item: item["name"].lower())
|
||||||
|
|
||||||
async def list_models(
|
async def list_models(
|
||||||
@@ -1640,6 +1638,11 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
) -> 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)
|
||||||
|
resolved_model_list_strategy = self._resolve_provider_model_list_strategy(
|
||||||
|
spec,
|
||||||
|
base_url,
|
||||||
|
base_url_preset_id=base_url_preset_id,
|
||||||
|
)
|
||||||
if self._resolve_provider_models_dev_provider_id(
|
if self._resolve_provider_models_dev_provider_id(
|
||||||
spec,
|
spec,
|
||||||
base_url,
|
base_url,
|
||||||
@@ -1657,15 +1660,19 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
base_url_preset_id=base_url_preset_id,
|
base_url_preset_id=base_url_preset_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
if spec.model_list_strategy == "google":
|
if resolved_model_list_strategy == "google":
|
||||||
return await self._list_models_from_google(runtime["api_key"])
|
return await self._list_models_from_google(runtime["api_key"])
|
||||||
|
|
||||||
if spec.model_list_strategy == "github_copilot":
|
if resolved_model_list_strategy == "github_copilot":
|
||||||
return await self._list_models_from_copilot(runtime["api_key"])
|
return await self._list_models_from_copilot(runtime["api_key"])
|
||||||
|
|
||||||
if spec.model_list_strategy == "chatgpt":
|
if resolved_model_list_strategy == "chatgpt":
|
||||||
if runtime.get("auth_mode") == "oauth":
|
if runtime.get("auth_mode") == "oauth":
|
||||||
return await self._list_chatgpt_oauth_models()
|
return await self._list_chatgpt_oauth_models(
|
||||||
|
provider_id=provider_id,
|
||||||
|
base_url=base_url,
|
||||||
|
base_url_preset_id=base_url_preset_id,
|
||||||
|
)
|
||||||
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"],
|
||||||
@@ -1677,7 +1684,7 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
default_headers=runtime.get("default_headers"),
|
default_headers=runtime.get("default_headers"),
|
||||||
)
|
)
|
||||||
|
|
||||||
if spec.model_list_strategy == "anthropic_compatible":
|
if resolved_model_list_strategy == "anthropic_compatible":
|
||||||
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",
|
||||||
@@ -1685,7 +1692,7 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
base_url_preset_id=base_url_preset_id,
|
base_url_preset_id=base_url_preset_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
if spec.model_list_strategy == "models_dev_only":
|
if resolved_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",
|
||||||
@@ -2188,6 +2195,11 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
base_url_preset_id,
|
base_url_preset_id,
|
||||||
)
|
)
|
||||||
spec = await self._get_provider_async(normalized_provider_id)
|
spec = await self._get_provider_async(normalized_provider_id)
|
||||||
|
resolved_runtime = self._resolve_provider_runtime(
|
||||||
|
spec,
|
||||||
|
base_url,
|
||||||
|
base_url_preset_id=normalized_base_url_preset_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
|
||||||
@@ -2212,7 +2224,7 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
|
|
||||||
result: dict[str, Any] = {
|
result: dict[str, Any] = {
|
||||||
"provider_id": normalized_provider_id,
|
"provider_id": normalized_provider_id,
|
||||||
"runtime": spec.runtime,
|
"runtime": resolved_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(
|
||||||
@@ -2290,7 +2302,7 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
if spec.runtime == "google":
|
if resolved_runtime == "google":
|
||||||
if not normalized_api_key:
|
if not normalized_api_key:
|
||||||
raise LLMProviderAuthError(f"{spec.name} 需要填写 API Key")
|
raise LLMProviderAuthError(f"{spec.name} 需要填写 API Key")
|
||||||
result.update(
|
result.update(
|
||||||
@@ -2302,7 +2314,7 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
if spec.runtime == "anthropic_compatible":
|
if resolved_runtime == "anthropic_compatible":
|
||||||
effective_base_url = normalized_base_url or self._default_base_url_for_provider(
|
effective_base_url = normalized_base_url or self._default_base_url_for_provider(
|
||||||
spec
|
spec
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1200,6 +1200,8 @@ def _llm_provider_defaults(
|
|||||||
provider_definitions: list[dict[str, Any]],
|
provider_definitions: list[dict[str, Any]],
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
normalized_provider = str(provider or "").strip().lower()
|
normalized_provider = str(provider or "").strip().lower()
|
||||||
|
if normalized_provider == "kimi-coding":
|
||||||
|
normalized_provider = "moonshot"
|
||||||
defaults = dict(LLM_PROVIDER_DEFAULTS.get(normalized_provider) or {})
|
defaults = dict(LLM_PROVIDER_DEFAULTS.get(normalized_provider) or {})
|
||||||
provider_meta = next(
|
provider_meta = next(
|
||||||
(
|
(
|
||||||
@@ -1231,6 +1233,8 @@ def _llm_provider_meta(
|
|||||||
provider_definitions: list[dict[str, Any]],
|
provider_definitions: list[dict[str, Any]],
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
normalized_provider = str(provider or "").strip().lower()
|
normalized_provider = str(provider or "").strip().lower()
|
||||||
|
if normalized_provider == "kimi-coding":
|
||||||
|
normalized_provider = "moonshot"
|
||||||
provider_meta = next(
|
provider_meta = next(
|
||||||
(
|
(
|
||||||
item
|
item
|
||||||
@@ -1784,6 +1788,8 @@ def _collect_agent_config(
|
|||||||
provider_definitions = _load_llm_provider_definitions(runtime_python=runtime_python)
|
provider_definitions = _load_llm_provider_definitions(runtime_python=runtime_python)
|
||||||
provider_choices = _llm_provider_choice_map(provider_definitions)
|
provider_choices = _llm_provider_choice_map(provider_definitions)
|
||||||
current_provider = _env_default("LLM_PROVIDER", "deepseek").lower()
|
current_provider = _env_default("LLM_PROVIDER", "deepseek").lower()
|
||||||
|
if current_provider == "kimi-coding":
|
||||||
|
current_provider = "moonshot"
|
||||||
if current_provider not in provider_choices:
|
if current_provider not in provider_choices:
|
||||||
current_provider = "deepseek"
|
current_provider = "deepseek"
|
||||||
|
|
||||||
|
|||||||
@@ -294,6 +294,107 @@ class LlmProviderRegistryTest(unittest.TestCase):
|
|||||||
"minimax-cn-coding-plan",
|
"minimax-cn-coding-plan",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_builtin_moonshot_provider_includes_kimi_for_coding_preset(self):
|
||||||
|
manager = LLMProviderManager()
|
||||||
|
|
||||||
|
provider = manager.get_provider("moonshot")
|
||||||
|
serialized = manager.list_providers()
|
||||||
|
moonshot_payload = next(item for item in serialized if item["id"] == "moonshot")
|
||||||
|
|
||||||
|
self.assertEqual(provider.name, "Moonshot / Kimi")
|
||||||
|
self.assertEqual(provider.runtime, "openai_compatible")
|
||||||
|
self.assertEqual(
|
||||||
|
tuple((preset.id, preset.label, preset.value, preset.runtime) for preset in provider.base_url_presets),
|
||||||
|
(
|
||||||
|
("moonshot-cn", "中国站", "https://api.moonshot.cn/v1", None),
|
||||||
|
("moonshot-global", "国际站", "https://api.moonshot.ai/v1", None),
|
||||||
|
(
|
||||||
|
"moonshot-kimi-coding",
|
||||||
|
"Kimi for Coding",
|
||||||
|
"https://api.kimi.com/coding/v1",
|
||||||
|
"anthropic_compatible",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
tuple(item["id"] for item in moonshot_payload["base_url_presets"]),
|
||||||
|
("moonshot-cn", "moonshot-global", "moonshot-kimi-coding"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_kimi_coding_alias_resolves_to_moonshot_provider(self):
|
||||||
|
manager = LLMProviderManager()
|
||||||
|
|
||||||
|
provider = manager.get_provider("kimi-coding")
|
||||||
|
|
||||||
|
self.assertEqual(provider.id, "moonshot")
|
||||||
|
|
||||||
|
def test_resolve_runtime_prefers_kimi_for_coding_preset_runtime(self):
|
||||||
|
manager = LLMProviderManager()
|
||||||
|
|
||||||
|
runtime = asyncio.run(
|
||||||
|
manager.resolve_runtime(
|
||||||
|
provider_id="moonshot",
|
||||||
|
model=None,
|
||||||
|
api_key="sk-test",
|
||||||
|
base_url="https://api.kimi.com/coding/v1",
|
||||||
|
base_url_preset_id="moonshot-kimi-coding",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(runtime["provider_id"], "moonshot")
|
||||||
|
self.assertEqual(runtime["runtime"], "anthropic_compatible")
|
||||||
|
self.assertEqual(runtime["base_url"], "https://api.kimi.com/coding")
|
||||||
|
|
||||||
|
def test_resolve_model_list_strategy_prefers_kimi_for_coding_preset(self):
|
||||||
|
manager = LLMProviderManager()
|
||||||
|
provider = manager.get_provider("moonshot")
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
manager._resolve_provider_model_list_strategy(
|
||||||
|
provider,
|
||||||
|
base_url="https://api.kimi.com/coding/v1",
|
||||||
|
base_url_preset_id="moonshot-kimi-coding",
|
||||||
|
),
|
||||||
|
"anthropic_compatible",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_chatgpt_oauth_models_follow_models_dev_catalog(self):
|
||||||
|
manager = LLMProviderManager()
|
||||||
|
payload = {
|
||||||
|
"openai": {
|
||||||
|
"id": "openai",
|
||||||
|
"name": "OpenAI",
|
||||||
|
"models": {
|
||||||
|
"gpt-5.5": {
|
||||||
|
"name": "GPT-5.5",
|
||||||
|
"limit": {"context": 400000},
|
||||||
|
},
|
||||||
|
"o4-mini": {
|
||||||
|
"name": "o4-mini",
|
||||||
|
"limit": {"context": 200000},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch.object(manager, "get_models_dev_data", AsyncMock(return_value=payload)):
|
||||||
|
models = asyncio.run(
|
||||||
|
manager._list_chatgpt_oauth_models(provider_id="chatgpt")
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual([item["id"] for item in models], ["gpt-5.5", "o4-mini"])
|
||||||
|
self.assertTrue(all(item["source"] == "models.dev" for item in models))
|
||||||
|
|
||||||
|
def test_chatgpt_oauth_models_return_empty_when_catalog_missing(self):
|
||||||
|
manager = LLMProviderManager()
|
||||||
|
|
||||||
|
with patch.object(manager, "get_models_dev_data", AsyncMock(return_value={})):
|
||||||
|
models = asyncio.run(
|
||||||
|
manager._list_chatgpt_oauth_models(provider_id="chatgpt")
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(models, [])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user