mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-31 12:24:39 +08:00
231 lines
8.3 KiB
Python
231 lines
8.3 KiB
Python
"""
|
|
LLM Provider 运行时参数解析的唯一 owner。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Optional
|
|
|
|
from app.runtime.log import logger
|
|
|
|
|
|
class LLMProviderError(RuntimeError):
|
|
"""
|
|
通用 LLM provider 异常。
|
|
"""
|
|
|
|
|
|
class LLMProviderAuthError(LLMProviderError):
|
|
"""
|
|
LLM provider 鉴权异常。
|
|
"""
|
|
|
|
|
|
class _ProviderRuntime:
|
|
"""
|
|
LLM Provider 运行时参数解析的唯一 owner。
|
|
"""
|
|
|
|
def __getattr__(self, name: str) -> Any:
|
|
"""
|
|
将跨 owner 调用交给最终 Facade 的 MRO 解析。
|
|
"""
|
|
raise AttributeError(name)
|
|
|
|
async def resolve_runtime(
|
|
self,
|
|
provider_id: str,
|
|
model: Optional[str],
|
|
api_key: Optional[str] = None,
|
|
base_url: Optional[str] = None,
|
|
base_url_preset_id: Optional[str] = None,
|
|
user_agent: Optional[str] = None,
|
|
use_proxy: Optional[bool] = None,
|
|
) -> dict[str, Any]:
|
|
"""
|
|
解析 provider 运行时参数。
|
|
|
|
返回统一结构,供 `LLMHelper` 创建具体 LangChain 模型实例时使用。
|
|
"""
|
|
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,
|
|
use_proxy=use_proxy,
|
|
)
|
|
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_base_url = self._sanitize_base_url(base_url)
|
|
default_transport = "anthropic" if resolved_runtime == "anthropic_compatible" else "openai"
|
|
model_record = self._resolve_cached_model_record(
|
|
normalized_provider_id,
|
|
model,
|
|
base_url=base_url,
|
|
base_url_preset_id=normalized_base_url_preset_id,
|
|
transport=default_transport,
|
|
)
|
|
model_metadata = self.resolve_cached_model_metadata(
|
|
normalized_provider_id,
|
|
model,
|
|
base_url=base_url,
|
|
base_url_preset_id=normalized_base_url_preset_id,
|
|
)
|
|
|
|
result: dict[str, Any] = {
|
|
"provider_id": normalized_provider_id,
|
|
"runtime": resolved_runtime,
|
|
"model_id": model,
|
|
"model_record": model_record,
|
|
"model_metadata": model_metadata,
|
|
"model_profile_endpoint_matched": (
|
|
self._is_model_profile_endpoint_matched(
|
|
spec,
|
|
base_url,
|
|
base_url_preset_id=normalized_base_url_preset_id,
|
|
)
|
|
),
|
|
"supports_prompt_cache": self._metadata_supports_prompt_cache(model_metadata),
|
|
"default_headers": None,
|
|
"use_responses_api": None,
|
|
"auth_mode": "api_key",
|
|
}
|
|
|
|
if normalized_provider_id == "chatgpt":
|
|
auth = None
|
|
auth_error: Exception = LLMProviderAuthError("请提供 API Key 或完成 ChatGPT 授权")
|
|
try:
|
|
auth = await self._resolve_chatgpt_oauth()
|
|
except Exception as err:
|
|
auth_error = err
|
|
logger.debug(f"解析 ChatGPT OAuth 鉴权失败: {err}")
|
|
|
|
if auth:
|
|
headers = {"originator": "moviepilot"}
|
|
if auth.get("account_id"):
|
|
headers["ChatGPT-Account-Id"] = auth["account_id"]
|
|
result.update(
|
|
{
|
|
"runtime": "chatgpt",
|
|
"api_key": auth["access_token"],
|
|
"base_url": self._CHATGPT_CODEX_BASE_URL,
|
|
"default_headers": self._merge_user_agent_header(
|
|
headers,
|
|
user_agent,
|
|
),
|
|
"use_responses_api": True,
|
|
"auth_mode": "oauth",
|
|
}
|
|
)
|
|
return result
|
|
|
|
if normalized_api_key:
|
|
result.update(
|
|
{
|
|
"runtime": "openai_compatible",
|
|
"api_key": normalized_api_key,
|
|
"base_url": normalized_base_url or self._default_base_url_for_provider(spec),
|
|
"default_headers": self._merge_user_agent_header(
|
|
None,
|
|
user_agent,
|
|
),
|
|
"auth_mode": "api_key",
|
|
}
|
|
)
|
|
return result
|
|
|
|
raise auth_error
|
|
|
|
if normalized_provider_id == "github-copilot":
|
|
auth = self.get_saved_auth("github-copilot")
|
|
if auth and auth.get("type") == "oauth":
|
|
token = auth.get("refresh_token") or auth.get("access_token")
|
|
elif normalized_api_key:
|
|
token = normalized_api_key
|
|
else:
|
|
raise LLMProviderAuthError("请先完成 GitHub Copilot 授权")
|
|
|
|
transport = (model_record or {}).get("transport") or "openai"
|
|
result.update(
|
|
{
|
|
"runtime": "copilot_anthropic" if transport == "anthropic" else "github_copilot",
|
|
"api_key": token,
|
|
"base_url": "https://api.githubcopilot.com",
|
|
"default_headers": self._merge_user_agent_header(
|
|
self._copilot_headers(
|
|
token,
|
|
include_auth=transport == "anthropic",
|
|
),
|
|
user_agent,
|
|
),
|
|
"auth_mode": "oauth" if auth else "api_key",
|
|
}
|
|
)
|
|
return result
|
|
|
|
if resolved_runtime == "google":
|
|
if not normalized_api_key:
|
|
raise LLMProviderAuthError(f"{spec.name} 需要填写 API Key")
|
|
result.update(
|
|
{
|
|
"api_key": normalized_api_key,
|
|
"base_url": None,
|
|
"auth_mode": "api_key",
|
|
}
|
|
)
|
|
return result
|
|
|
|
if resolved_runtime == "bedrock":
|
|
effective_base_url = normalized_base_url or self._default_base_url_for_provider(spec)
|
|
credentials = self._parse_bedrock_credentials(normalized_api_key)
|
|
result.update(
|
|
{
|
|
"api_key": normalized_api_key,
|
|
"base_url": effective_base_url,
|
|
"aws_region": self._extract_bedrock_region(effective_base_url),
|
|
"aws_auth": credentials,
|
|
"auth_mode": "api_key",
|
|
}
|
|
)
|
|
return result
|
|
|
|
if resolved_runtime == "anthropic_compatible":
|
|
effective_base_url = normalized_base_url or self._default_base_url_for_provider(spec)
|
|
if not normalized_api_key:
|
|
raise LLMProviderAuthError(f"{spec.name} 需要填写 API Key")
|
|
if not effective_base_url:
|
|
raise LLMProviderAuthError(f"{spec.name} 缺少 Base URL")
|
|
result.update(
|
|
{
|
|
"api_key": normalized_api_key,
|
|
"base_url": self._normalize_base_url_for_anthropic(effective_base_url),
|
|
"default_headers": self._merge_user_agent_header(
|
|
None,
|
|
user_agent,
|
|
),
|
|
"auth_mode": "api_key",
|
|
}
|
|
)
|
|
return result
|
|
|
|
effective_base_url = normalized_base_url or self._default_base_url_for_provider(spec)
|
|
if spec.requires_base_url and not effective_base_url:
|
|
raise LLMProviderAuthError(f"{spec.name} 需要填写 Base URL")
|
|
if not normalized_api_key:
|
|
raise LLMProviderAuthError(f"{spec.name} 需要填写 API Key")
|
|
result.update(
|
|
{
|
|
"api_key": normalized_api_key,
|
|
"base_url": effective_base_url,
|
|
"default_headers": self._merge_user_agent_header(None, user_agent),
|
|
"auth_mode": "api_key",
|
|
}
|
|
)
|
|
return result
|