mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-06-11 18:50:59 +08:00
feat: support llm user agent
This commit is contained in:
@@ -469,6 +469,7 @@ class MoviePilotAgent:
|
||||
api_key=settings.LLM_API_KEY,
|
||||
base_url=settings.LLM_BASE_URL,
|
||||
base_url_preset=settings.LLM_BASE_URL_PRESET,
|
||||
user_agent=getattr(settings, "LLM_USER_AGENT", None),
|
||||
thinking_level=None,
|
||||
)
|
||||
selected_event = await eventmanager.async_send_event(
|
||||
@@ -497,6 +498,10 @@ class MoviePilotAgent:
|
||||
self._clean_optional_text(self._get_event_value(resolved_data, "base_url_preset"))
|
||||
or settings.LLM_BASE_URL_PRESET
|
||||
)
|
||||
user_agent = (
|
||||
self._clean_optional_text(self._get_event_value(resolved_data, "user_agent"))
|
||||
or getattr(settings, "LLM_USER_AGENT", None)
|
||||
)
|
||||
thinking_level = self._clean_optional_text(
|
||||
self._get_event_value(resolved_data, "thinking_level")
|
||||
)
|
||||
@@ -522,6 +527,7 @@ class MoviePilotAgent:
|
||||
"api_key": api_key,
|
||||
"base_url": base_url,
|
||||
"base_url_preset": base_url_preset,
|
||||
"user_agent": user_agent,
|
||||
"thinking_level": thinking_level,
|
||||
}
|
||||
return self._llm_runtime_config
|
||||
|
||||
@@ -602,6 +602,7 @@ class LLMHelper:
|
||||
model_name: str | None,
|
||||
api_key: str | None = None,
|
||||
base_url: str | None = None,
|
||||
user_agent: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
在 provider 目录不可用时回退到旧的直接构造逻辑。
|
||||
@@ -625,12 +626,36 @@ class LLMHelper:
|
||||
"model_id": model_name,
|
||||
"api_key": api_key_value,
|
||||
"base_url": base_url_value,
|
||||
"default_headers": None,
|
||||
"default_headers": LLMHelper._build_openai_default_headers(
|
||||
None,
|
||||
user_agent=user_agent,
|
||||
),
|
||||
"use_responses_api": None,
|
||||
"model_record": None,
|
||||
"model_metadata": None,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _build_openai_default_headers(
|
||||
default_headers: dict[str, str] | None = None,
|
||||
user_agent: str | None = None,
|
||||
) -> dict[str, str] | None:
|
||||
"""
|
||||
合并 OpenAI 兼容接口默认请求头。
|
||||
|
||||
:param default_headers: provider 运行时已解析的默认请求头
|
||||
:param user_agent: 用户配置的 User-Agent,非空时写入标准请求头
|
||||
:return: 可传给 OpenAI SDK 的请求头字典
|
||||
"""
|
||||
headers = dict(default_headers or {})
|
||||
normalized_user_agent = str(user_agent or "").strip()
|
||||
if normalized_user_agent:
|
||||
for key in list(headers.keys()):
|
||||
if key.lower() == "user-agent":
|
||||
headers.pop(key)
|
||||
headers["User-Agent"] = normalized_user_agent
|
||||
return headers or None
|
||||
|
||||
@classmethod
|
||||
def _resolve_thinking_level(
|
||||
cls,
|
||||
@@ -675,6 +700,7 @@ class LLMHelper:
|
||||
api_key: str | None = None,
|
||||
base_url: str | None = None,
|
||||
base_url_preset: str | None = None,
|
||||
user_agent: str | None = None,
|
||||
):
|
||||
"""
|
||||
获取LLM实例
|
||||
@@ -688,6 +714,7 @@ class LLMHelper:
|
||||
:param api_key: API Key。未显式传入时使用当前配置项 LLM_API_KEY。对于某些提供商(如 DeepSeek),可能需要同时提供 base_url。
|
||||
:param base_url: API Base URL。未显式传入时使用当前配置项 LLM_BASE_URL。
|
||||
:param base_url_preset: Base URL 预设。未显式传入时使用当前配置项 LLM_BASE_URL_PRESET。
|
||||
:param user_agent: OpenAI兼容接口请求 User-Agent。未显式传入时使用配置项 LLM_USER_AGENT。
|
||||
:return: LLM实例
|
||||
"""
|
||||
provider_name = str(provider if provider is not None else settings.LLM_PROVIDER).lower()
|
||||
@@ -697,6 +724,9 @@ class LLMHelper:
|
||||
base_url_preset_value = (
|
||||
base_url_preset if base_url_preset is not None else settings.LLM_BASE_URL_PRESET
|
||||
)
|
||||
user_agent_value = (
|
||||
user_agent if user_agent is not None else getattr(settings, "LLM_USER_AGENT", None)
|
||||
)
|
||||
normalized_thinking_level = cls._resolve_thinking_level(
|
||||
thinking_level=thinking_level,
|
||||
)
|
||||
@@ -711,6 +741,7 @@ class LLMHelper:
|
||||
api_key=api_key_value,
|
||||
base_url=base_url_value,
|
||||
base_url_preset_id=base_url_preset_value,
|
||||
user_agent=user_agent_value,
|
||||
)
|
||||
except Exception as err:
|
||||
logger.debug(f"LLM provider 目录不可用,回退到旧运行时逻辑: {err}")
|
||||
@@ -719,8 +750,13 @@ class LLMHelper:
|
||||
model_name=model_name,
|
||||
api_key=api_key_value,
|
||||
base_url=base_url_value,
|
||||
user_agent=user_agent_value,
|
||||
)
|
||||
model_name = runtime.get("model_id") or model_name
|
||||
default_headers = cls._build_openai_default_headers(
|
||||
runtime.get("default_headers"),
|
||||
user_agent=user_agent_value,
|
||||
)
|
||||
thinking_kwargs = cls._build_thinking_kwargs(
|
||||
provider=provider_name,
|
||||
model=model_name,
|
||||
@@ -776,7 +812,7 @@ class LLMHelper:
|
||||
streaming=streaming,
|
||||
stream_usage=True,
|
||||
anthropic_proxy=settings.PROXY_HOST,
|
||||
default_headers=runtime.get("default_headers"),
|
||||
default_headers=default_headers,
|
||||
**thinking_kwargs,
|
||||
)
|
||||
else:
|
||||
@@ -797,7 +833,7 @@ class LLMHelper:
|
||||
streaming=streaming,
|
||||
stream_usage=True,
|
||||
openai_proxy=settings.PROXY_HOST,
|
||||
default_headers=runtime.get("default_headers"),
|
||||
default_headers=default_headers,
|
||||
use_responses_api=runtime.get("use_responses_api"),
|
||||
**thinking_kwargs,
|
||||
)
|
||||
@@ -873,6 +909,7 @@ class LLMHelper:
|
||||
api_key: str | None = None,
|
||||
base_url: str | None = None,
|
||||
base_url_preset: str | None = None,
|
||||
user_agent: str | None = None,
|
||||
) -> dict:
|
||||
"""
|
||||
使用当前已保存配置执行一次最小 LLM 调用。
|
||||
@@ -888,6 +925,7 @@ class LLMHelper:
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
base_url_preset=base_url_preset,
|
||||
user_agent=user_agent,
|
||||
)
|
||||
try:
|
||||
response = await asyncio.wait_for(llm.ainvoke(prompt), timeout=timeout)
|
||||
@@ -918,6 +956,7 @@ class LLMHelper:
|
||||
api_key: str | None = None,
|
||||
base_url: str | None = None,
|
||||
base_url_preset: str | None = None,
|
||||
user_agent: str | None = None,
|
||||
force_refresh: bool = False,
|
||||
) -> List[dict[str, Any]]:
|
||||
"""
|
||||
@@ -935,6 +974,7 @@ class LLMHelper:
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
base_url_preset_id=base_url_preset,
|
||||
user_agent=user_agent,
|
||||
force_refresh=force_refresh,
|
||||
)
|
||||
except Exception as err:
|
||||
@@ -963,6 +1003,7 @@ class LLMHelper:
|
||||
provider,
|
||||
api_key or "",
|
||||
model_list_base_url,
|
||||
user_agent=user_agent,
|
||||
)
|
||||
]
|
||||
|
||||
@@ -997,7 +1038,10 @@ class LLMHelper:
|
||||
|
||||
@staticmethod
|
||||
async def _get_openai_compatible_models(
|
||||
provider: str, api_key: str, base_url: str = None
|
||||
provider: str,
|
||||
api_key: str,
|
||||
base_url: str = None,
|
||||
user_agent: str | None = None,
|
||||
) -> List[str]:
|
||||
"""获取OpenAI兼容模型列表"""
|
||||
try:
|
||||
@@ -1006,7 +1050,14 @@ class LLMHelper:
|
||||
if provider == "deepseek":
|
||||
base_url = base_url or "https://api.deepseek.com"
|
||||
|
||||
client = AsyncOpenAI(api_key=api_key, base_url=base_url)
|
||||
client = AsyncOpenAI(
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
default_headers=LLMHelper._build_openai_default_headers(
|
||||
None,
|
||||
user_agent=user_agent,
|
||||
),
|
||||
)
|
||||
models = await client.models.list()
|
||||
await client.close()
|
||||
return [model.id for model in models.data]
|
||||
|
||||
@@ -1166,6 +1166,23 @@ class LLMProviderManager(metaclass=Singleton):
|
||||
return None
|
||||
return value.rstrip("/")
|
||||
|
||||
@staticmethod
|
||||
def _merge_user_agent_header(
|
||||
default_headers: Optional[dict[str, str]],
|
||||
user_agent: Optional[str],
|
||||
) -> Optional[dict[str, str]]:
|
||||
"""
|
||||
合并用户配置的 OpenAI 兼容接口 User-Agent 请求头。
|
||||
"""
|
||||
headers = dict(default_headers or {})
|
||||
normalized_user_agent = str(user_agent or "").strip()
|
||||
if normalized_user_agent:
|
||||
for key in list(headers.keys()):
|
||||
if key.lower() == "user-agent":
|
||||
headers.pop(key)
|
||||
headers["User-Agent"] = normalized_user_agent
|
||||
return headers or None
|
||||
|
||||
@classmethod
|
||||
def _default_base_url_for_provider(cls, spec: ProviderSpec) -> Optional[str]:
|
||||
"""获取 provider 的默认 Base URL。"""
|
||||
@@ -1825,6 +1842,7 @@ class LLMProviderManager(metaclass=Singleton):
|
||||
api_key: Optional[str] = None,
|
||||
base_url: Optional[str] = None,
|
||||
base_url_preset_id: Optional[str] = None,
|
||||
user_agent: Optional[str] = None,
|
||||
force_refresh: bool = False,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""返回标准化后的模型目录。"""
|
||||
@@ -1854,6 +1872,7 @@ class LLMProviderManager(metaclass=Singleton):
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
base_url_preset_id=base_url_preset_id,
|
||||
user_agent=user_agent,
|
||||
)
|
||||
|
||||
if resolved_model_list_strategy == "google":
|
||||
@@ -1877,7 +1896,10 @@ class LLMProviderManager(metaclass=Singleton):
|
||||
runtime["base_url"],
|
||||
base_url_preset_id=base_url_preset_id,
|
||||
),
|
||||
default_headers=runtime.get("default_headers"),
|
||||
default_headers=self._merge_user_agent_header(
|
||||
runtime.get("default_headers"),
|
||||
user_agent,
|
||||
),
|
||||
)
|
||||
|
||||
if resolved_model_list_strategy == "anthropic_compatible":
|
||||
@@ -1905,7 +1927,10 @@ class LLMProviderManager(metaclass=Singleton):
|
||||
runtime["base_url"],
|
||||
base_url_preset_id=base_url_preset_id,
|
||||
),
|
||||
default_headers=runtime.get("default_headers"),
|
||||
default_headers=self._merge_user_agent_header(
|
||||
runtime.get("default_headers"),
|
||||
user_agent,
|
||||
),
|
||||
)
|
||||
|
||||
async def resolve_model_metadata(
|
||||
@@ -2398,6 +2423,7 @@ class LLMProviderManager(metaclass=Singleton):
|
||||
api_key: Optional[str] = None,
|
||||
base_url: Optional[str] = None,
|
||||
base_url_preset_id: Optional[str] = None,
|
||||
user_agent: Optional[str] = None,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
解析 provider 运行时参数。
|
||||
@@ -2428,6 +2454,7 @@ class LLMProviderManager(metaclass=Singleton):
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
base_url_preset_id=normalized_base_url_preset_id,
|
||||
user_agent=user_agent,
|
||||
)
|
||||
if item["id"] == model
|
||||
),
|
||||
@@ -2470,7 +2497,10 @@ class LLMProviderManager(metaclass=Singleton):
|
||||
"runtime": "chatgpt",
|
||||
"api_key": auth["access_token"],
|
||||
"base_url": self._CHATGPT_CODEX_BASE_URL,
|
||||
"default_headers": headers,
|
||||
"default_headers": self._merge_user_agent_header(
|
||||
headers,
|
||||
user_agent,
|
||||
),
|
||||
"use_responses_api": True,
|
||||
"auth_mode": "oauth",
|
||||
}
|
||||
@@ -2484,6 +2514,10 @@ class LLMProviderManager(metaclass=Singleton):
|
||||
"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",
|
||||
}
|
||||
)
|
||||
@@ -2508,9 +2542,12 @@ class LLMProviderManager(metaclass=Singleton):
|
||||
else "github_copilot",
|
||||
"api_key": token,
|
||||
"base_url": "https://api.githubcopilot.com",
|
||||
"default_headers": self._copilot_headers(
|
||||
token,
|
||||
include_auth=transport == "anthropic",
|
||||
"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",
|
||||
}
|
||||
@@ -2543,6 +2580,10 @@ class LLMProviderManager(metaclass=Singleton):
|
||||
"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",
|
||||
}
|
||||
)
|
||||
@@ -2557,6 +2598,7 @@ class LLMProviderManager(metaclass=Singleton):
|
||||
{
|
||||
"api_key": normalized_api_key,
|
||||
"base_url": effective_base_url,
|
||||
"default_headers": self._merge_user_agent_header(None, user_agent),
|
||||
"auth_mode": "api_key",
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user