mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 15:38:19 +08:00
feat: add LLM proxy toggle
This commit is contained in:
@@ -470,6 +470,7 @@ class MoviePilotAgent:
|
|||||||
base_url=settings.LLM_BASE_URL,
|
base_url=settings.LLM_BASE_URL,
|
||||||
base_url_preset=settings.LLM_BASE_URL_PRESET,
|
base_url_preset=settings.LLM_BASE_URL_PRESET,
|
||||||
user_agent=settings.LLM_USER_AGENT,
|
user_agent=settings.LLM_USER_AGENT,
|
||||||
|
use_proxy=settings.LLM_USE_PROXY,
|
||||||
thinking_level=None,
|
thinking_level=None,
|
||||||
)
|
)
|
||||||
selected_event = await eventmanager.async_send_event(
|
selected_event = await eventmanager.async_send_event(
|
||||||
@@ -502,6 +503,9 @@ class MoviePilotAgent:
|
|||||||
self._clean_optional_text(self._get_event_value(resolved_data, "user_agent"))
|
self._clean_optional_text(self._get_event_value(resolved_data, "user_agent"))
|
||||||
or settings.LLM_USER_AGENT
|
or settings.LLM_USER_AGENT
|
||||||
)
|
)
|
||||||
|
use_proxy = self._get_event_value(resolved_data, "use_proxy")
|
||||||
|
if use_proxy is None:
|
||||||
|
use_proxy = settings.LLM_USE_PROXY
|
||||||
thinking_level = self._clean_optional_text(
|
thinking_level = self._clean_optional_text(
|
||||||
self._get_event_value(resolved_data, "thinking_level")
|
self._get_event_value(resolved_data, "thinking_level")
|
||||||
)
|
)
|
||||||
@@ -528,6 +532,7 @@ class MoviePilotAgent:
|
|||||||
"base_url": base_url,
|
"base_url": base_url,
|
||||||
"base_url_preset": base_url_preset,
|
"base_url_preset": base_url_preset,
|
||||||
"user_agent": user_agent,
|
"user_agent": user_agent,
|
||||||
|
"use_proxy": bool(use_proxy),
|
||||||
"thinking_level": thinking_level,
|
"thinking_level": thinking_level,
|
||||||
}
|
}
|
||||||
return self._llm_runtime_config
|
return self._llm_runtime_config
|
||||||
|
|||||||
+90
-18
@@ -137,6 +137,57 @@ def _get_httpx_proxy_key() -> str:
|
|||||||
return "proxies"
|
return "proxies"
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_llm_proxy(use_proxy: bool | None = None) -> str | None:
|
||||||
|
"""
|
||||||
|
解析本次 LLM 调用应使用的系统代理地址。
|
||||||
|
"""
|
||||||
|
should_use_proxy = settings.LLM_USE_PROXY if use_proxy is None else use_proxy
|
||||||
|
return settings.PROXY_HOST if should_use_proxy and settings.PROXY_HOST else None
|
||||||
|
|
||||||
|
|
||||||
|
def _build_httpx_proxy_kwargs(proxy_url: str | None) -> dict[str, str]:
|
||||||
|
"""
|
||||||
|
构造兼容当前 httpx 版本的代理参数。
|
||||||
|
"""
|
||||||
|
if not proxy_url:
|
||||||
|
return {}
|
||||||
|
return {_get_httpx_proxy_key(): proxy_url}
|
||||||
|
|
||||||
|
|
||||||
|
def _build_google_client_args(proxy_url: str | None) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
构造 Google SDK 透传给 httpx 的客户端参数。
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"trust_env": False,
|
||||||
|
**_build_httpx_proxy_kwargs(proxy_url),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _build_httpx_client(
|
||||||
|
proxy_url: str | None,
|
||||||
|
*,
|
||||||
|
async_client: bool = False,
|
||||||
|
timeout: float | None = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
构造显式代理策略的 httpx 客户端。
|
||||||
|
|
||||||
|
当关闭 LLM 代理时也返回 trust_env=False 的客户端,避免 httpx 自动读取
|
||||||
|
进程环境变量中的代理配置。
|
||||||
|
"""
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
client_cls = httpx.AsyncClient if async_client else httpx.Client
|
||||||
|
kwargs: dict[str, Any] = {
|
||||||
|
"trust_env": False,
|
||||||
|
**_build_httpx_proxy_kwargs(proxy_url),
|
||||||
|
}
|
||||||
|
if timeout is not None:
|
||||||
|
kwargs["timeout"] = timeout
|
||||||
|
return client_cls(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
def _deepseek_thinking_toggle(extra_body: Any) -> bool | None:
|
def _deepseek_thinking_toggle(extra_body: Any) -> bool | None:
|
||||||
"""
|
"""
|
||||||
解析 DeepSeek extra_body 中显式传入的 thinking 开关。
|
解析 DeepSeek extra_body 中显式传入的 thinking 开关。
|
||||||
@@ -733,6 +784,7 @@ class LLMHelper:
|
|||||||
base_url: str | None = None,
|
base_url: str | None = None,
|
||||||
base_url_preset: str | None = None,
|
base_url_preset: str | None = None,
|
||||||
user_agent: str | None = None,
|
user_agent: str | None = None,
|
||||||
|
use_proxy: bool | None = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
获取LLM实例
|
获取LLM实例
|
||||||
@@ -747,6 +799,7 @@ class LLMHelper:
|
|||||||
:param base_url: API Base URL。未显式传入时使用当前配置项 LLM_BASE_URL。
|
:param base_url: API Base URL。未显式传入时使用当前配置项 LLM_BASE_URL。
|
||||||
:param base_url_preset: Base URL 预设。未显式传入时使用当前配置项 LLM_BASE_URL_PRESET。
|
:param base_url_preset: Base URL 预设。未显式传入时使用当前配置项 LLM_BASE_URL_PRESET。
|
||||||
:param user_agent: OpenAI兼容接口请求 User-Agent。未显式传入时使用配置项 LLM_USER_AGENT。
|
:param user_agent: OpenAI兼容接口请求 User-Agent。未显式传入时使用配置项 LLM_USER_AGENT。
|
||||||
|
:param use_proxy: 是否为本次 LLM 调用使用系统代理。未显式传入时使用配置项 LLM_USE_PROXY。
|
||||||
:return: LLM实例
|
:return: LLM实例
|
||||||
"""
|
"""
|
||||||
provider_name = str(provider if provider is not None else settings.LLM_PROVIDER).lower()
|
provider_name = str(provider if provider is not None else settings.LLM_PROVIDER).lower()
|
||||||
@@ -772,6 +825,7 @@ class LLMHelper:
|
|||||||
base_url=base_url_value,
|
base_url=base_url_value,
|
||||||
base_url_preset_id=base_url_preset_value,
|
base_url_preset_id=base_url_preset_value,
|
||||||
user_agent=user_agent_value,
|
user_agent=user_agent_value,
|
||||||
|
use_proxy=use_proxy,
|
||||||
)
|
)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.debug(f"LLM provider 目录不可用,回退到旧运行时逻辑: {err}")
|
logger.debug(f"LLM provider 目录不可用,回退到旧运行时逻辑: {err}")
|
||||||
@@ -797,6 +851,7 @@ class LLMHelper:
|
|||||||
model=model_name,
|
model=model_name,
|
||||||
runtime=runtime,
|
runtime=runtime,
|
||||||
)
|
)
|
||||||
|
llm_proxy = _resolve_llm_proxy(use_proxy)
|
||||||
|
|
||||||
if runtime["runtime"] == "google":
|
if runtime["runtime"] == "google":
|
||||||
# 修补 Gemini 2.5 思考模型的 thought_signature 兼容性
|
# 修补 Gemini 2.5 思考模型的 thought_signature 兼容性
|
||||||
@@ -807,18 +862,13 @@ class LLMHelper:
|
|||||||
# 会导致工具调用时报错 400
|
# 会导致工具调用时报错 400
|
||||||
from langchain_google_genai import ChatGoogleGenerativeAI
|
from langchain_google_genai import ChatGoogleGenerativeAI
|
||||||
|
|
||||||
client_args = None
|
|
||||||
if settings.PROXY_HOST:
|
|
||||||
proxy_key = _get_httpx_proxy_key()
|
|
||||||
client_args = {proxy_key: settings.PROXY_HOST}
|
|
||||||
|
|
||||||
model = ChatGoogleGenerativeAI(
|
model = ChatGoogleGenerativeAI(
|
||||||
model=model_name,
|
model=model_name,
|
||||||
api_key=runtime["api_key"],
|
api_key=runtime["api_key"],
|
||||||
retries=3,
|
retries=3,
|
||||||
temperature=settings.LLM_TEMPERATURE,
|
temperature=settings.LLM_TEMPERATURE,
|
||||||
streaming=streaming,
|
streaming=streaming,
|
||||||
client_args=client_args,
|
client_args=_build_google_client_args(llm_proxy),
|
||||||
**thinking_kwargs,
|
**thinking_kwargs,
|
||||||
)
|
)
|
||||||
elif runtime["runtime"] == "deepseek":
|
elif runtime["runtime"] == "deepseek":
|
||||||
@@ -833,6 +883,8 @@ class LLMHelper:
|
|||||||
temperature=settings.LLM_TEMPERATURE,
|
temperature=settings.LLM_TEMPERATURE,
|
||||||
streaming=streaming,
|
streaming=streaming,
|
||||||
stream_usage=True,
|
stream_usage=True,
|
||||||
|
http_client=_build_httpx_client(llm_proxy),
|
||||||
|
http_async_client=_build_httpx_client(llm_proxy, async_client=True),
|
||||||
**thinking_kwargs,
|
**thinking_kwargs,
|
||||||
)
|
)
|
||||||
elif runtime["runtime"] in {"anthropic_compatible", "copilot_anthropic"}:
|
elif runtime["runtime"] in {"anthropic_compatible", "copilot_anthropic"}:
|
||||||
@@ -846,7 +898,7 @@ class LLMHelper:
|
|||||||
temperature=settings.LLM_TEMPERATURE,
|
temperature=settings.LLM_TEMPERATURE,
|
||||||
streaming=streaming,
|
streaming=streaming,
|
||||||
stream_usage=True,
|
stream_usage=True,
|
||||||
anthropic_proxy=settings.PROXY_HOST,
|
anthropic_proxy=llm_proxy,
|
||||||
default_headers=default_headers,
|
default_headers=default_headers,
|
||||||
**thinking_kwargs,
|
**thinking_kwargs,
|
||||||
)
|
)
|
||||||
@@ -867,7 +919,15 @@ class LLMHelper:
|
|||||||
temperature=settings.LLM_TEMPERATURE,
|
temperature=settings.LLM_TEMPERATURE,
|
||||||
streaming=streaming,
|
streaming=streaming,
|
||||||
stream_usage=True,
|
stream_usage=True,
|
||||||
openai_proxy=settings.PROXY_HOST,
|
openai_proxy=llm_proxy,
|
||||||
|
**(
|
||||||
|
{}
|
||||||
|
if llm_proxy
|
||||||
|
else {
|
||||||
|
"http_client": _build_httpx_client(llm_proxy),
|
||||||
|
"http_async_client": _build_httpx_client(llm_proxy, async_client=True),
|
||||||
|
}
|
||||||
|
),
|
||||||
default_headers=default_headers,
|
default_headers=default_headers,
|
||||||
use_responses_api=use_responses_api,
|
use_responses_api=use_responses_api,
|
||||||
**thinking_kwargs,
|
**thinking_kwargs,
|
||||||
@@ -945,6 +1005,7 @@ class LLMHelper:
|
|||||||
base_url: str | None = None,
|
base_url: str | None = None,
|
||||||
base_url_preset: str | None = None,
|
base_url_preset: str | None = None,
|
||||||
user_agent: str | None = None,
|
user_agent: str | None = None,
|
||||||
|
use_proxy: bool | None = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
使用当前已保存配置执行一次最小 LLM 调用。
|
使用当前已保存配置执行一次最小 LLM 调用。
|
||||||
@@ -961,6 +1022,7 @@ class LLMHelper:
|
|||||||
base_url=base_url,
|
base_url=base_url,
|
||||||
base_url_preset=base_url_preset,
|
base_url_preset=base_url_preset,
|
||||||
user_agent=user_agent,
|
user_agent=user_agent,
|
||||||
|
use_proxy=use_proxy,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
response = await asyncio.wait_for(llm.ainvoke(prompt), timeout=timeout)
|
response = await asyncio.wait_for(llm.ainvoke(prompt), timeout=timeout)
|
||||||
@@ -992,6 +1054,7 @@ class LLMHelper:
|
|||||||
base_url: str | None = None,
|
base_url: str | None = None,
|
||||||
base_url_preset: str | None = None,
|
base_url_preset: str | None = None,
|
||||||
user_agent: str | None = None,
|
user_agent: str | None = None,
|
||||||
|
use_proxy: bool | None = None,
|
||||||
force_refresh: bool = False,
|
force_refresh: bool = False,
|
||||||
) -> List[dict[str, Any]]:
|
) -> List[dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
@@ -1010,6 +1073,7 @@ class LLMHelper:
|
|||||||
base_url=base_url,
|
base_url=base_url,
|
||||||
base_url_preset_id=base_url_preset,
|
base_url_preset_id=base_url_preset,
|
||||||
user_agent=user_agent,
|
user_agent=user_agent,
|
||||||
|
use_proxy=use_proxy,
|
||||||
force_refresh=force_refresh,
|
force_refresh=force_refresh,
|
||||||
)
|
)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
@@ -1017,7 +1081,10 @@ class LLMHelper:
|
|||||||
if provider == "google":
|
if provider == "google":
|
||||||
return [
|
return [
|
||||||
{"id": model_id, "name": model_id}
|
{"id": model_id, "name": model_id}
|
||||||
for model_id in await self._get_google_models(api_key or "")
|
for model_id in await self._get_google_models(
|
||||||
|
api_key or "",
|
||||||
|
use_proxy=use_proxy,
|
||||||
|
)
|
||||||
]
|
]
|
||||||
try:
|
try:
|
||||||
from app.agent.llm.provider import LLMProviderManager
|
from app.agent.llm.provider import LLMProviderManager
|
||||||
@@ -1039,24 +1106,23 @@ class LLMHelper:
|
|||||||
api_key or "",
|
api_key or "",
|
||||||
model_list_base_url,
|
model_list_base_url,
|
||||||
user_agent=user_agent,
|
user_agent=user_agent,
|
||||||
|
use_proxy=use_proxy,
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def _get_google_models(api_key: str) -> List[str]:
|
async def _get_google_models(api_key: str, use_proxy: bool | None = None) -> List[str]:
|
||||||
"""获取Google模型列表(使用 google-genai SDK v1)"""
|
"""获取Google模型列表(使用 google-genai SDK v1)"""
|
||||||
try:
|
try:
|
||||||
from google import genai
|
from google import genai
|
||||||
from google.genai.types import HttpOptions
|
from google.genai.types import HttpOptions
|
||||||
|
|
||||||
http_options = None
|
llm_proxy = _resolve_llm_proxy(use_proxy)
|
||||||
if settings.PROXY_HOST:
|
google_client_args = _build_google_client_args(llm_proxy)
|
||||||
proxy_key = _get_httpx_proxy_key()
|
http_options = HttpOptions(
|
||||||
proxy_args = {proxy_key: settings.PROXY_HOST}
|
client_args=google_client_args,
|
||||||
http_options = HttpOptions(
|
async_client_args=google_client_args,
|
||||||
client_args=proxy_args,
|
)
|
||||||
async_client_args=proxy_args,
|
|
||||||
)
|
|
||||||
|
|
||||||
client = genai.Client(api_key=api_key, http_options=http_options)
|
client = genai.Client(api_key=api_key, http_options=http_options)
|
||||||
models = await client.aio.models.list()
|
models = await client.aio.models.list()
|
||||||
@@ -1077,6 +1143,7 @@ class LLMHelper:
|
|||||||
api_key: str,
|
api_key: str,
|
||||||
base_url: str = None,
|
base_url: str = None,
|
||||||
user_agent: str | None = None,
|
user_agent: str | None = None,
|
||||||
|
use_proxy: bool | None = None,
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
"""获取OpenAI兼容模型列表"""
|
"""获取OpenAI兼容模型列表"""
|
||||||
try:
|
try:
|
||||||
@@ -1092,6 +1159,11 @@ class LLMHelper:
|
|||||||
None,
|
None,
|
||||||
user_agent=user_agent,
|
user_agent=user_agent,
|
||||||
),
|
),
|
||||||
|
http_client=_build_httpx_client(
|
||||||
|
_resolve_llm_proxy(use_proxy),
|
||||||
|
async_client=True,
|
||||||
|
timeout=15.0,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
models = await client.models.list()
|
models = await client.models.list()
|
||||||
await client.close()
|
await client.close()
|
||||||
|
|||||||
+115
-33
@@ -1085,14 +1085,20 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
return builtin_specs + self._dynamic_provider_specs(builtin_specs)
|
return builtin_specs + self._dynamic_provider_specs(builtin_specs)
|
||||||
|
|
||||||
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,
|
||||||
|
use_proxy: Optional[bool] = None,
|
||||||
) -> ProviderSpec:
|
) -> ProviderSpec:
|
||||||
"""异步获取指定 provider 的 ProviderSpec 实例。"""
|
"""异步获取指定 provider 的 ProviderSpec 实例。"""
|
||||||
normalized_provider_id = self._normalize_provider_id(provider_id)
|
normalized_provider_id = self._normalize_provider_id(provider_id)
|
||||||
try:
|
try:
|
||||||
return self.get_provider(normalized_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,
|
||||||
|
use_proxy=use_proxy,
|
||||||
|
)
|
||||||
return self.get_provider(normalized_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]:
|
||||||
@@ -1132,11 +1138,16 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
}
|
}
|
||||||
|
|
||||||
async def list_providers_async(
|
async def list_providers_async(
|
||||||
self, force_refresh: bool = False
|
self,
|
||||||
|
force_refresh: bool = False,
|
||||||
|
use_proxy: Optional[bool] = None,
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""返回前端可渲染的 provider 目录,并优先补齐 models.dev 动态平台。"""
|
"""返回前端可渲染的 provider 目录,并优先补齐 models.dev 动态平台。"""
|
||||||
try:
|
try:
|
||||||
await self.get_models_dev_data(force_refresh=force_refresh)
|
await self.get_models_dev_data(
|
||||||
|
force_refresh=force_refresh,
|
||||||
|
use_proxy=use_proxy,
|
||||||
|
)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.debug(f"加载 models.dev provider 目录失败,回退内置列表: {err}")
|
logger.debug(f"加载 models.dev provider 目录失败,回退内置列表: {err}")
|
||||||
return self.list_providers()
|
return self.list_providers()
|
||||||
@@ -1327,10 +1338,14 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
params = httpx.Client.__init__.__code__.co_varnames
|
params = httpx.Client.__init__.__code__.co_varnames
|
||||||
return "proxy" if "proxy" in params else "proxies"
|
return "proxy" if "proxy" in params else "proxies"
|
||||||
|
|
||||||
def _build_httpx_kwargs(self) -> dict[str, Any]:
|
def _build_httpx_kwargs(self, use_proxy: Optional[bool] = None) -> dict[str, Any]:
|
||||||
"""构造用于 httpx 客户端的参数,如代理等。"""
|
"""构造用于 httpx 客户端的参数,如代理等。"""
|
||||||
kwargs: dict[str, Any] = {"timeout": self._DEFAULT_TIMEOUT}
|
should_use_proxy = settings.LLM_USE_PROXY if use_proxy is None else use_proxy
|
||||||
if settings.PROXY_HOST:
|
kwargs: dict[str, Any] = {
|
||||||
|
"timeout": self._DEFAULT_TIMEOUT,
|
||||||
|
"trust_env": False,
|
||||||
|
}
|
||||||
|
if should_use_proxy and settings.PROXY_HOST:
|
||||||
kwargs[self._httpx_proxy_key()] = settings.PROXY_HOST
|
kwargs[self._httpx_proxy_key()] = settings.PROXY_HOST
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
@@ -1441,15 +1456,19 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.warning(f"写入 models.dev 缓存失败: {err}")
|
logger.warning(f"写入 models.dev 缓存失败: {err}")
|
||||||
|
|
||||||
async def _fetch_models_dev(self) -> dict[str, Any]:
|
async def _fetch_models_dev(self, use_proxy: Optional[bool] = None) -> dict[str, Any]:
|
||||||
"""通过网络请求获取最新 models.dev 数据。"""
|
"""通过网络请求获取最新 models.dev 数据。"""
|
||||||
headers = {"User-Agent": "MoviePilot/1.0"}
|
headers = {"User-Agent": "MoviePilot/1.0"}
|
||||||
async with httpx.AsyncClient(**self._build_httpx_kwargs()) as client:
|
async with httpx.AsyncClient(**self._build_httpx_kwargs(use_proxy)) as client:
|
||||||
response = await client.get(self._MODELS_DEV_URL, headers=headers)
|
response = await client.get(self._MODELS_DEV_URL, headers=headers)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
async def get_models_dev_data(self, force_refresh: bool = False) -> dict[str, Any]:
|
async def get_models_dev_data(
|
||||||
|
self,
|
||||||
|
force_refresh: bool = False,
|
||||||
|
use_proxy: Optional[bool] = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
返回 models.dev 原始数据。
|
返回 models.dev 原始数据。
|
||||||
|
|
||||||
@@ -1475,7 +1494,7 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
return cached
|
return cached
|
||||||
|
|
||||||
try:
|
try:
|
||||||
payload = await self._fetch_models_dev()
|
payload = await self._fetch_models_dev(use_proxy=use_proxy)
|
||||||
self._models_dev_data = payload
|
self._models_dev_data = payload
|
||||||
self._models_dev_loaded_at = now
|
self._models_dev_loaded_at = now
|
||||||
await self._write_models_dev_to_disk(payload)
|
await self._write_models_dev_to_disk(payload)
|
||||||
@@ -1499,9 +1518,13 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
provider_id: str,
|
provider_id: str,
|
||||||
base_url: Optional[str] = None,
|
base_url: Optional[str] = None,
|
||||||
base_url_preset_id: Optional[str] = None,
|
base_url_preset_id: Optional[str] = None,
|
||||||
|
use_proxy: Optional[bool] = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""获取指定 provider 在 models.dev 中的完整负载。"""
|
"""获取指定 provider 在 models.dev 中的完整负载。"""
|
||||||
spec = await self._get_provider_async(provider_id)
|
spec = await self._get_provider_async(
|
||||||
|
provider_id,
|
||||||
|
use_proxy=use_proxy,
|
||||||
|
)
|
||||||
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,
|
||||||
@@ -1509,7 +1532,9 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
)
|
)
|
||||||
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(use_proxy=use_proxy)
|
||||||
|
).get(models_dev_provider_id, {}) or {}
|
||||||
|
|
||||||
async def _models_dev_model(
|
async def _models_dev_model(
|
||||||
self,
|
self,
|
||||||
@@ -1517,12 +1542,14 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
model_id: str,
|
model_id: str,
|
||||||
base_url: Optional[str] = None,
|
base_url: Optional[str] = None,
|
||||||
base_url_preset_id: Optional[str] = None,
|
base_url_preset_id: Optional[str] = None,
|
||||||
|
use_proxy: Optional[bool] = None,
|
||||||
) -> dict[str, Any] | None:
|
) -> dict[str, Any] | None:
|
||||||
"""获取指定模型的 models.dev 元数据。"""
|
"""获取指定模型的 models.dev 元数据。"""
|
||||||
payload = await self._models_dev_provider_payload(
|
payload = await self._models_dev_provider_payload(
|
||||||
provider_id,
|
provider_id,
|
||||||
base_url=base_url,
|
base_url=base_url,
|
||||||
base_url_preset_id=base_url_preset_id,
|
base_url_preset_id=base_url_preset_id,
|
||||||
|
use_proxy=use_proxy,
|
||||||
)
|
)
|
||||||
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):
|
||||||
@@ -1621,19 +1648,23 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
return normalized[:-3]
|
return normalized[:-3]
|
||||||
return normalized
|
return normalized
|
||||||
|
|
||||||
async def _list_models_from_google(self, api_key: str) -> list[dict[str, Any]]:
|
async def _list_models_from_google(
|
||||||
|
self,
|
||||||
|
api_key: str,
|
||||||
|
use_proxy: Optional[bool] = None,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
"""从 Google AI Studio 获取模型列表。"""
|
"""从 Google AI Studio 获取模型列表。"""
|
||||||
from google import genai
|
from google import genai
|
||||||
from google.genai.types import HttpOptions
|
from google.genai.types import HttpOptions
|
||||||
|
|
||||||
http_options = None
|
should_use_proxy = settings.LLM_USE_PROXY if use_proxy is None else use_proxy
|
||||||
if settings.PROXY_HOST:
|
client_args: dict[str, Any] = {"trust_env": False}
|
||||||
proxy_key = self._httpx_proxy_key()
|
if should_use_proxy and settings.PROXY_HOST:
|
||||||
proxy_args = {proxy_key: settings.PROXY_HOST}
|
client_args[self._httpx_proxy_key()] = settings.PROXY_HOST
|
||||||
http_options = HttpOptions(
|
http_options = HttpOptions(
|
||||||
client_args=proxy_args,
|
client_args=client_args,
|
||||||
async_client_args=proxy_args,
|
async_client_args=client_args,
|
||||||
)
|
)
|
||||||
|
|
||||||
client = genai.Client(api_key=api_key, http_options=http_options)
|
client = genai.Client(api_key=api_key, http_options=http_options)
|
||||||
response = await client.aio.models.list()
|
response = await client.aio.models.list()
|
||||||
@@ -1643,7 +1674,11 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
if "generateContent" not in supported:
|
if "generateContent" not in supported:
|
||||||
continue
|
continue
|
||||||
model_id = model.name
|
model_id = model.name
|
||||||
metadata = await self._models_dev_model("google", model_id) or {}
|
metadata = await self._models_dev_model(
|
||||||
|
"google",
|
||||||
|
model_id,
|
||||||
|
use_proxy=use_proxy,
|
||||||
|
) or {}
|
||||||
results.append(
|
results.append(
|
||||||
self._normalize_model_record(
|
self._normalize_model_record(
|
||||||
model_id=model_id,
|
model_id=model_id,
|
||||||
@@ -1660,6 +1695,7 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
api_key: str,
|
api_key: str,
|
||||||
base_url: str,
|
base_url: str,
|
||||||
default_headers: Optional[dict[str, str]] = None,
|
default_headers: Optional[dict[str, str]] = None,
|
||||||
|
use_proxy: Optional[bool] = None,
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""通过 OpenAI 兼容接口获取模型列表。"""
|
"""通过 OpenAI 兼容接口获取模型列表。"""
|
||||||
from openai import AsyncOpenAI
|
from openai import AsyncOpenAI
|
||||||
@@ -1670,6 +1706,7 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
default_headers=default_headers,
|
default_headers=default_headers,
|
||||||
timeout=15.0,
|
timeout=15.0,
|
||||||
max_retries=2,
|
max_retries=2,
|
||||||
|
http_client=httpx.AsyncClient(**self._build_httpx_kwargs(use_proxy)),
|
||||||
)
|
)
|
||||||
results = []
|
results = []
|
||||||
response = await client.models.list()
|
response = await client.models.list()
|
||||||
@@ -1678,6 +1715,7 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
provider_id,
|
provider_id,
|
||||||
model.id,
|
model.id,
|
||||||
base_url=base_url,
|
base_url=base_url,
|
||||||
|
use_proxy=use_proxy,
|
||||||
) or {}
|
) or {}
|
||||||
results.append(
|
results.append(
|
||||||
self._normalize_model_record(
|
self._normalize_model_record(
|
||||||
@@ -1695,6 +1733,7 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
transport: str = "openai",
|
transport: str = "openai",
|
||||||
base_url: Optional[str] = None,
|
base_url: Optional[str] = None,
|
||||||
base_url_preset_id: Optional[str] = None,
|
base_url_preset_id: Optional[str] = None,
|
||||||
|
use_proxy: Optional[bool] = None,
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
某些 provider 没有统一稳定的 models.list 行为,
|
某些 provider 没有统一稳定的 models.list 行为,
|
||||||
@@ -1705,6 +1744,7 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
provider_id,
|
provider_id,
|
||||||
base_url=base_url,
|
base_url=base_url,
|
||||||
base_url_preset_id=base_url_preset_id,
|
base_url_preset_id=base_url_preset_id,
|
||||||
|
use_proxy=use_proxy,
|
||||||
)
|
)
|
||||||
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):
|
||||||
@@ -1741,9 +1781,13 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
headers["Authorization"] = f"Bearer {token}"
|
headers["Authorization"] = f"Bearer {token}"
|
||||||
return headers
|
return headers
|
||||||
|
|
||||||
async def _list_models_from_copilot(self, token: str) -> list[dict[str, Any]]:
|
async def _list_models_from_copilot(
|
||||||
|
self,
|
||||||
|
token: str,
|
||||||
|
use_proxy: Optional[bool] = None,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
"""从 GitHub Copilot 端点获取模型列表。"""
|
"""从 GitHub Copilot 端点获取模型列表。"""
|
||||||
async with httpx.AsyncClient(**self._build_httpx_kwargs()) as client:
|
async with httpx.AsyncClient(**self._build_httpx_kwargs(use_proxy)) as client:
|
||||||
response = await client.get(
|
response = await client.get(
|
||||||
"https://api.githubcopilot.com/models",
|
"https://api.githubcopilot.com/models",
|
||||||
headers=self._copilot_headers(token),
|
headers=self._copilot_headers(token),
|
||||||
@@ -1780,7 +1824,11 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
|
|
||||||
limits = ((item.get("capabilities") or {}).get("limits") or {})
|
limits = ((item.get("capabilities") or {}).get("limits") or {})
|
||||||
supports = ((item.get("capabilities") or {}).get("supports") or {})
|
supports = ((item.get("capabilities") or {}).get("supports") or {})
|
||||||
metadata = await self._models_dev_model("github-copilot", model_id) or {}
|
metadata = await self._models_dev_model(
|
||||||
|
"github-copilot",
|
||||||
|
model_id,
|
||||||
|
use_proxy=use_proxy,
|
||||||
|
) or {}
|
||||||
results.append(
|
results.append(
|
||||||
self._normalize_model_record(
|
self._normalize_model_record(
|
||||||
model_id=model_id,
|
model_id=model_id,
|
||||||
@@ -1811,6 +1859,7 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
provider_id: str,
|
provider_id: str,
|
||||||
base_url: Optional[str] = None,
|
base_url: Optional[str] = None,
|
||||||
base_url_preset_id: Optional[str] = None,
|
base_url_preset_id: Optional[str] = None,
|
||||||
|
use_proxy: Optional[bool] = None,
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""获取开启 OAuth 的 ChatGPT 模型列表。"""
|
"""获取开启 OAuth 的 ChatGPT 模型列表。"""
|
||||||
# ChatGPT OAuth 仍然是 chatgpt provider 专属能力,但模型目录不再维护
|
# ChatGPT OAuth 仍然是 chatgpt provider 专属能力,但模型目录不再维护
|
||||||
@@ -1819,6 +1868,7 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
provider_id,
|
provider_id,
|
||||||
base_url=base_url,
|
base_url=base_url,
|
||||||
base_url_preset_id=base_url_preset_id,
|
base_url_preset_id=base_url_preset_id,
|
||||||
|
use_proxy=use_proxy,
|
||||||
)
|
)
|
||||||
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):
|
||||||
@@ -1843,10 +1893,15 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
base_url: Optional[str] = None,
|
base_url: Optional[str] = None,
|
||||||
base_url_preset_id: Optional[str] = None,
|
base_url_preset_id: Optional[str] = None,
|
||||||
user_agent: Optional[str] = None,
|
user_agent: Optional[str] = None,
|
||||||
|
use_proxy: Optional[bool] = 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,
|
||||||
|
use_proxy=use_proxy,
|
||||||
|
)
|
||||||
resolved_model_list_strategy = self._resolve_provider_model_list_strategy(
|
resolved_model_list_strategy = self._resolve_provider_model_list_strategy(
|
||||||
spec,
|
spec,
|
||||||
base_url,
|
base_url,
|
||||||
@@ -1860,7 +1915,10 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
# 对依赖 models.dev 的 provider 主动刷新一次缓存,保证“刷新模型列表”
|
# 对依赖 models.dev 的 provider 主动刷新一次缓存,保证“刷新模型列表”
|
||||||
# 在使用目录型 provider 时也能拿到最新参数。
|
# 在使用目录型 provider 时也能拿到最新参数。
|
||||||
if force_refresh:
|
if force_refresh:
|
||||||
await self.get_models_dev_data(force_refresh=True)
|
await self.get_models_dev_data(
|
||||||
|
force_refresh=True,
|
||||||
|
use_proxy=use_proxy,
|
||||||
|
)
|
||||||
|
|
||||||
if resolved_model_list_strategy == "manual":
|
if resolved_model_list_strategy == "manual":
|
||||||
# 万擎等推理点型平台没有稳定的全局模型目录,模型 ID 需要用户从控制台复制。
|
# 万擎等推理点型平台没有稳定的全局模型目录,模型 ID 需要用户从控制台复制。
|
||||||
@@ -1873,13 +1931,20 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
base_url=base_url,
|
base_url=base_url,
|
||||||
base_url_preset_id=base_url_preset_id,
|
base_url_preset_id=base_url_preset_id,
|
||||||
user_agent=user_agent,
|
user_agent=user_agent,
|
||||||
|
use_proxy=use_proxy,
|
||||||
)
|
)
|
||||||
|
|
||||||
if resolved_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"],
|
||||||
|
use_proxy=use_proxy,
|
||||||
|
)
|
||||||
|
|
||||||
if resolved_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"],
|
||||||
|
use_proxy=use_proxy,
|
||||||
|
)
|
||||||
|
|
||||||
if resolved_model_list_strategy == "chatgpt":
|
if resolved_model_list_strategy == "chatgpt":
|
||||||
if runtime.get("auth_mode") == "oauth":
|
if runtime.get("auth_mode") == "oauth":
|
||||||
@@ -1887,6 +1952,7 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
provider_id=provider_id,
|
provider_id=provider_id,
|
||||||
base_url=base_url,
|
base_url=base_url,
|
||||||
base_url_preset_id=base_url_preset_id,
|
base_url_preset_id=base_url_preset_id,
|
||||||
|
use_proxy=use_proxy,
|
||||||
)
|
)
|
||||||
return await self._list_models_from_openai_compatible(
|
return await self._list_models_from_openai_compatible(
|
||||||
provider_id="chatgpt",
|
provider_id="chatgpt",
|
||||||
@@ -1900,6 +1966,7 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
runtime.get("default_headers"),
|
runtime.get("default_headers"),
|
||||||
user_agent,
|
user_agent,
|
||||||
),
|
),
|
||||||
|
use_proxy=use_proxy,
|
||||||
)
|
)
|
||||||
|
|
||||||
if resolved_model_list_strategy == "anthropic_compatible":
|
if resolved_model_list_strategy == "anthropic_compatible":
|
||||||
@@ -1908,6 +1975,7 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
transport="anthropic",
|
transport="anthropic",
|
||||||
base_url=base_url,
|
base_url=base_url,
|
||||||
base_url_preset_id=base_url_preset_id,
|
base_url_preset_id=base_url_preset_id,
|
||||||
|
use_proxy=use_proxy,
|
||||||
)
|
)
|
||||||
|
|
||||||
if resolved_model_list_strategy == "models_dev_only":
|
if resolved_model_list_strategy == "models_dev_only":
|
||||||
@@ -1916,6 +1984,7 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
transport="openai",
|
transport="openai",
|
||||||
base_url=base_url,
|
base_url=base_url,
|
||||||
base_url_preset_id=base_url_preset_id,
|
base_url_preset_id=base_url_preset_id,
|
||||||
|
use_proxy=use_proxy,
|
||||||
)
|
)
|
||||||
|
|
||||||
# openai-compatible / deepseek 默认走官方 models 端点。
|
# openai-compatible / deepseek 默认走官方 models 端点。
|
||||||
@@ -1931,6 +2000,7 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
runtime.get("default_headers"),
|
runtime.get("default_headers"),
|
||||||
user_agent,
|
user_agent,
|
||||||
),
|
),
|
||||||
|
use_proxy=use_proxy,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def resolve_model_metadata(
|
async def resolve_model_metadata(
|
||||||
@@ -1939,6 +2009,7 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
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,
|
base_url_preset_id: Optional[str] = None,
|
||||||
|
use_proxy: Optional[bool] = None,
|
||||||
) -> dict[str, Any] | None:
|
) -> dict[str, Any] | None:
|
||||||
"""解析并返回指定模型在 models.dev 中的元数据。"""
|
"""解析并返回指定模型在 models.dev 中的元数据。"""
|
||||||
if not model_id:
|
if not model_id:
|
||||||
@@ -1948,13 +2019,18 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
model_id,
|
model_id,
|
||||||
base_url=base_url,
|
base_url=base_url,
|
||||||
base_url_preset_id=base_url_preset_id,
|
base_url_preset_id=base_url_preset_id,
|
||||||
|
use_proxy=use_proxy,
|
||||||
)
|
)
|
||||||
if metadata:
|
if metadata:
|
||||||
return metadata
|
return metadata
|
||||||
if provider_id == "chatgpt":
|
if provider_id == "chatgpt":
|
||||||
return await self._models_dev_model("openai", model_id)
|
return await self._models_dev_model(
|
||||||
|
"openai",
|
||||||
|
model_id,
|
||||||
|
use_proxy=use_proxy,
|
||||||
|
)
|
||||||
if provider_id == "openai":
|
if provider_id == "openai":
|
||||||
models_dev = await self.get_models_dev_data()
|
models_dev = await self.get_models_dev_data(use_proxy=use_proxy)
|
||||||
return models_dev.get("openai", {}).get("models", {}).get(model_id)
|
return models_dev.get("openai", {}).get("models", {}).get(model_id)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -2424,6 +2500,7 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
base_url: Optional[str] = None,
|
base_url: Optional[str] = None,
|
||||||
base_url_preset_id: Optional[str] = None,
|
base_url_preset_id: Optional[str] = None,
|
||||||
user_agent: Optional[str] = None,
|
user_agent: Optional[str] = None,
|
||||||
|
use_proxy: Optional[bool] = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
解析 provider 运行时参数。
|
解析 provider 运行时参数。
|
||||||
@@ -2435,7 +2512,10 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
normalized_provider_id,
|
normalized_provider_id,
|
||||||
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,
|
||||||
|
use_proxy=use_proxy,
|
||||||
|
)
|
||||||
resolved_runtime = self._resolve_provider_runtime(
|
resolved_runtime = self._resolve_provider_runtime(
|
||||||
spec,
|
spec,
|
||||||
base_url,
|
base_url,
|
||||||
@@ -2455,6 +2535,7 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
base_url=base_url,
|
base_url=base_url,
|
||||||
base_url_preset_id=normalized_base_url_preset_id,
|
base_url_preset_id=normalized_base_url_preset_id,
|
||||||
user_agent=user_agent,
|
user_agent=user_agent,
|
||||||
|
use_proxy=use_proxy,
|
||||||
)
|
)
|
||||||
if item["id"] == model
|
if item["id"] == model
|
||||||
),
|
),
|
||||||
@@ -2474,6 +2555,7 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
model,
|
model,
|
||||||
base_url=base_url,
|
base_url=base_url,
|
||||||
base_url_preset_id=normalized_base_url_preset_id,
|
base_url_preset_id=normalized_base_url_preset_id,
|
||||||
|
use_proxy=use_proxy,
|
||||||
),
|
),
|
||||||
"default_headers": None,
|
"default_headers": None,
|
||||||
"use_responses_api": None,
|
"use_responses_api": None,
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ class LlmTestRequest(BaseModel):
|
|||||||
base_url: Optional[str] = None
|
base_url: Optional[str] = None
|
||||||
base_url_preset: Optional[str] = None
|
base_url_preset: Optional[str] = None
|
||||||
user_agent: Optional[str] = None
|
user_agent: Optional[str] = None
|
||||||
|
use_proxy: Optional[bool] = None
|
||||||
|
|
||||||
|
|
||||||
class LlmProviderAuthStartRequest(BaseModel):
|
class LlmProviderAuthStartRequest(BaseModel):
|
||||||
@@ -77,6 +78,7 @@ async def get_llm_models(
|
|||||||
base_url: Optional[str] = None,
|
base_url: Optional[str] = None,
|
||||||
base_url_preset: Optional[str] = None,
|
base_url_preset: Optional[str] = None,
|
||||||
user_agent: Optional[str] = None,
|
user_agent: Optional[str] = None,
|
||||||
|
use_proxy: Optional[bool] = 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),
|
||||||
):
|
):
|
||||||
@@ -91,6 +93,7 @@ async def get_llm_models(
|
|||||||
base_url=base_url,
|
base_url=base_url,
|
||||||
base_url_preset=base_url_preset,
|
base_url_preset=base_url_preset,
|
||||||
user_agent=user_agent,
|
user_agent=user_agent,
|
||||||
|
use_proxy=use_proxy,
|
||||||
force_refresh=bool(force_refresh),
|
force_refresh=bool(force_refresh),
|
||||||
)
|
)
|
||||||
return schemas.Response(
|
return schemas.Response(
|
||||||
@@ -250,6 +253,7 @@ async def llm_test(
|
|||||||
base_url=settings.LLM_BASE_URL,
|
base_url=settings.LLM_BASE_URL,
|
||||||
base_url_preset=settings.LLM_BASE_URL_PRESET,
|
base_url_preset=settings.LLM_BASE_URL_PRESET,
|
||||||
user_agent=settings.LLM_USER_AGENT,
|
user_agent=settings.LLM_USER_AGENT,
|
||||||
|
use_proxy=settings.LLM_USE_PROXY,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not payload.provider:
|
if not payload.provider:
|
||||||
@@ -282,6 +286,7 @@ async def llm_test(
|
|||||||
base_url=payload.base_url,
|
base_url=payload.base_url,
|
||||||
base_url_preset=payload.base_url_preset,
|
base_url_preset=payload.base_url_preset,
|
||||||
user_agent=payload.user_agent,
|
user_agent=payload.user_agent,
|
||||||
|
use_proxy=payload.use_proxy,
|
||||||
)
|
)
|
||||||
if not result.get("reply_preview"):
|
if not result.get("reply_preview"):
|
||||||
return schemas.Response(
|
return schemas.Response(
|
||||||
|
|||||||
@@ -562,6 +562,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调用是否使用系统代理
|
||||||
|
LLM_USE_PROXY: bool = True
|
||||||
# LLM Base URL 预设标识,用于区分同一 Base URL 下的不同模型目录
|
# LLM Base URL 预设标识,用于区分同一 Base URL 下的不同模型目录
|
||||||
LLM_BASE_URL_PRESET: Optional[str] = None
|
LLM_BASE_URL_PRESET: Optional[str] = None
|
||||||
# LLM最大上下文Token数量(K)
|
# LLM最大上下文Token数量(K)
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ class AgentLLMProviderEventData(ChainEventData):
|
|||||||
Agent LLM 供应商选择事件数据。
|
Agent LLM 供应商选择事件数据。
|
||||||
|
|
||||||
事件发出方会带入当前系统配置作为默认值;插件可覆盖 provider、base_url、
|
事件发出方会带入当前系统配置作为默认值;插件可覆盖 provider、base_url、
|
||||||
api_key、model、user_agent 等字段,并通过 selected_provider_id 标记本次选择,方便
|
api_key、model、user_agent、use_proxy 等字段,并通过 selected_provider_id 标记本次选择,方便
|
||||||
后续用量事件精确回写到同一个配额条目。
|
后续用量事件精确回写到同一个配额条目。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -79,6 +79,7 @@ class AgentLLMProviderEventData(ChainEventData):
|
|||||||
model: Optional[str] = Field(default=None, description="模型名称")
|
model: Optional[str] = Field(default=None, description="模型名称")
|
||||||
base_url_preset: Optional[str] = Field(default=None, description="Base URL 预设ID")
|
base_url_preset: Optional[str] = Field(default=None, description="Base URL 预设ID")
|
||||||
user_agent: Optional[str] = Field(default=None, description="OpenAI兼容接口User-Agent")
|
user_agent: Optional[str] = Field(default=None, description="OpenAI兼容接口User-Agent")
|
||||||
|
use_proxy: Optional[bool] = Field(default=None, description="是否使用系统代理")
|
||||||
thinking_level: Optional[str] = Field(default=None, description="思考模式级别")
|
thinking_level: Optional[str] = Field(default=None, description="思考模式级别")
|
||||||
selected_provider_id: Optional[str] = Field(default=None, description="插件侧供应商ID")
|
selected_provider_id: Optional[str] = Field(default=None, description="插件侧供应商ID")
|
||||||
selected_provider_name: Optional[str] = Field(default=None, description="插件侧供应商名称")
|
selected_provider_name: Optional[str] = Field(default=None, description="插件侧供应商名称")
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ class AgentTokensEventsTest(unittest.IsolatedAsyncioTestCase):
|
|||||||
base_url="https://tokens.example.com/v1",
|
base_url="https://tokens.example.com/v1",
|
||||||
base_url_preset=None,
|
base_url_preset=None,
|
||||||
user_agent="AgentTokens-UA/1.0",
|
user_agent="AgentTokens-UA/1.0",
|
||||||
|
use_proxy=True,
|
||||||
thinking_level=None,
|
thinking_level=None,
|
||||||
)
|
)
|
||||||
self.assertEqual("provider-1", agent._llm_provider_selection["selected_provider_id"])
|
self.assertEqual("provider-1", agent._llm_provider_selection["selected_provider_id"])
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ _stub_module(
|
|||||||
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,
|
||||||
|
LLM_USE_PROXY=True,
|
||||||
PROXY_HOST=None,
|
PROXY_HOST=None,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -188,6 +189,7 @@ class LlmHelperTestCallTest(unittest.TestCase):
|
|||||||
base_url="https://api.deepseek.com",
|
base_url="https://api.deepseek.com",
|
||||||
base_url_preset="deepseek-default",
|
base_url_preset="deepseek-default",
|
||||||
user_agent=None,
|
user_agent=None,
|
||||||
|
use_proxy=None,
|
||||||
)
|
)
|
||||||
self.assertEqual(result["provider"], "deepseek")
|
self.assertEqual(result["provider"], "deepseek")
|
||||||
self.assertEqual(result["model"], "deepseek-chat")
|
self.assertEqual(result["model"], "deepseek-chat")
|
||||||
@@ -489,6 +491,7 @@ class LlmHelperTestCallTest(unittest.TestCase):
|
|||||||
"base_url": "https://updated.example.com/v1",
|
"base_url": "https://updated.example.com/v1",
|
||||||
"base_url_preset_id": "updated-preset",
|
"base_url_preset_id": "updated-preset",
|
||||||
"user_agent": None,
|
"user_agent": None,
|
||||||
|
"use_proxy": None,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.assertEqual(len(llm_calls), 1)
|
self.assertEqual(len(llm_calls), 1)
|
||||||
@@ -500,6 +503,46 @@ class LlmHelperTestCallTest(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(llm_calls[0].get("default_headers"), {"X-Test": "1"})
|
self.assertEqual(llm_calls[0].get("default_headers"), {"X-Test": "1"})
|
||||||
|
|
||||||
|
def test_get_llm_applies_proxy_only_when_enabled(self):
|
||||||
|
"""LLM 构造时应按独立开关决定是否传入系统代理。"""
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
class _FakeChatOpenAI:
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
calls.append(kwargs)
|
||||||
|
self.model = kwargs["model"]
|
||||||
|
self.profile = None
|
||||||
|
|
||||||
|
with patch.object(llm_module.settings, "PROXY_HOST", "http://proxy.example.com:7890"), patch.dict(
|
||||||
|
sys.modules,
|
||||||
|
{"langchain_openai": SimpleNamespace(ChatOpenAI=_FakeChatOpenAI)},
|
||||||
|
):
|
||||||
|
asyncio.run(
|
||||||
|
llm_module.LLMHelper.get_llm(
|
||||||
|
provider="openai",
|
||||||
|
model="gpt-5-mini",
|
||||||
|
api_key="sk-test",
|
||||||
|
base_url="https://api.example.com/v1",
|
||||||
|
use_proxy=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
asyncio.run(
|
||||||
|
llm_module.LLMHelper.get_llm(
|
||||||
|
provider="openai",
|
||||||
|
model="gpt-5-mini",
|
||||||
|
api_key="sk-test",
|
||||||
|
base_url="https://api.example.com/v1",
|
||||||
|
use_proxy=False,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(calls[0].get("openai_proxy"), "http://proxy.example.com:7890")
|
||||||
|
self.assertNotIn("http_client", calls[0])
|
||||||
|
self.assertNotIn("http_async_client", calls[0])
|
||||||
|
self.assertIsNone(calls[1].get("openai_proxy"))
|
||||||
|
self.assertIn("http_client", calls[1])
|
||||||
|
self.assertIn("http_async_client", calls[1])
|
||||||
|
|
||||||
def test_get_llm_passes_user_agent_as_openai_default_header(self):
|
def test_get_llm_passes_user_agent_as_openai_default_header(self):
|
||||||
calls = []
|
calls = []
|
||||||
|
|
||||||
|
|||||||
@@ -182,11 +182,15 @@ class LlmProviderRegistryTest(unittest.TestCase):
|
|||||||
with patch.object(
|
with patch.object(
|
||||||
manager,
|
manager,
|
||||||
"get_models_dev_data",
|
"get_models_dev_data",
|
||||||
AsyncMock(side_effect=lambda force_refresh=False: manager.__dict__.update({"_models_dev_data": payload}) or payload),
|
AsyncMock(
|
||||||
|
side_effect=lambda force_refresh=False, use_proxy=None: manager.__dict__.update(
|
||||||
|
{"_models_dev_data": payload}
|
||||||
|
) or payload
|
||||||
|
),
|
||||||
) as fetch_mock:
|
) as fetch_mock:
|
||||||
providers = asyncio.run(manager.list_providers_async())
|
providers = asyncio.run(manager.list_providers_async())
|
||||||
|
|
||||||
fetch_mock.assert_awaited_once_with(force_refresh=False)
|
fetch_mock.assert_awaited_once_with(force_refresh=False, use_proxy=None)
|
||||||
self.assertIn("frogbot", {item["id"] for item in providers})
|
self.assertIn("frogbot", {item["id"] for item in providers})
|
||||||
|
|
||||||
def test_list_models_uses_dynamic_provider_after_refresh(self):
|
def test_list_models_uses_dynamic_provider_after_refresh(self):
|
||||||
@@ -207,7 +211,7 @@ class LlmProviderRegistryTest(unittest.TestCase):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async def _load_models_dev(force_refresh: bool = False):
|
async def _load_models_dev(force_refresh: bool = False, use_proxy=None):
|
||||||
manager._models_dev_data = payload
|
manager._models_dev_data = payload
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
|
|||||||
@@ -147,6 +147,8 @@ class LlmTestEndpointTest(unittest.TestCase):
|
|||||||
system_endpoint.settings, "LLM_BASE_URL_PRESET", "deepseek-default"
|
system_endpoint.settings, "LLM_BASE_URL_PRESET", "deepseek-default"
|
||||||
), patch.object(
|
), patch.object(
|
||||||
system_endpoint.settings, "LLM_USER_AGENT", "MoviePilot-Test/1.0"
|
system_endpoint.settings, "LLM_USER_AGENT", "MoviePilot-Test/1.0"
|
||||||
|
), patch.object(
|
||||||
|
system_endpoint.settings, "LLM_USE_PROXY", True
|
||||||
), patch.object(
|
), patch.object(
|
||||||
system_endpoint.LLMHelper,
|
system_endpoint.LLMHelper,
|
||||||
"test_current_settings",
|
"test_current_settings",
|
||||||
@@ -163,6 +165,7 @@ class LlmTestEndpointTest(unittest.TestCase):
|
|||||||
base_url="https://api.deepseek.com",
|
base_url="https://api.deepseek.com",
|
||||||
base_url_preset="deepseek-default",
|
base_url_preset="deepseek-default",
|
||||||
user_agent="MoviePilot-Test/1.0",
|
user_agent="MoviePilot-Test/1.0",
|
||||||
|
use_proxy=True,
|
||||||
)
|
)
|
||||||
self.assertTrue(resp.success)
|
self.assertTrue(resp.success)
|
||||||
self.assertEqual(resp.data["provider"], "deepseek")
|
self.assertEqual(resp.data["provider"], "deepseek")
|
||||||
@@ -188,6 +191,7 @@ class LlmTestEndpointTest(unittest.TestCase):
|
|||||||
base_url="https://example.com/v1",
|
base_url="https://example.com/v1",
|
||||||
base_url_preset="openai-default",
|
base_url_preset="openai-default",
|
||||||
user_agent="MoviePilot-Custom/1.0",
|
user_agent="MoviePilot-Custom/1.0",
|
||||||
|
use_proxy=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch.object(system_endpoint.settings, "AI_AGENT_ENABLE", False), patch.object(
|
with patch.object(system_endpoint.settings, "AI_AGENT_ENABLE", False), patch.object(
|
||||||
@@ -212,6 +216,7 @@ class LlmTestEndpointTest(unittest.TestCase):
|
|||||||
base_url="https://example.com/v1",
|
base_url="https://example.com/v1",
|
||||||
base_url_preset="openai-default",
|
base_url_preset="openai-default",
|
||||||
user_agent="MoviePilot-Custom/1.0",
|
user_agent="MoviePilot-Custom/1.0",
|
||||||
|
use_proxy=False,
|
||||||
)
|
)
|
||||||
self.assertTrue(resp.success)
|
self.assertTrue(resp.success)
|
||||||
self.assertEqual(resp.data["provider"], "openai")
|
self.assertEqual(resp.data["provider"], "openai")
|
||||||
@@ -234,6 +239,7 @@ class LlmTestEndpointTest(unittest.TestCase):
|
|||||||
base_url="https://api.deepseek.com",
|
base_url="https://api.deepseek.com",
|
||||||
base_url_preset="deepseek-default",
|
base_url_preset="deepseek-default",
|
||||||
user_agent=None,
|
user_agent=None,
|
||||||
|
use_proxy=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch.object(system_endpoint.settings, "AI_AGENT_ENABLE", False), patch.object(
|
with patch.object(system_endpoint.settings, "AI_AGENT_ENABLE", False), patch.object(
|
||||||
@@ -252,6 +258,7 @@ class LlmTestEndpointTest(unittest.TestCase):
|
|||||||
base_url="https://api.deepseek.com",
|
base_url="https://api.deepseek.com",
|
||||||
base_url_preset="deepseek-default",
|
base_url_preset="deepseek-default",
|
||||||
user_agent=None,
|
user_agent=None,
|
||||||
|
use_proxy=None,
|
||||||
)
|
)
|
||||||
self.assertTrue(resp.success)
|
self.assertTrue(resp.success)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user