mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 23:47:41 +08:00
feat(provider): implement fallback to bundled models.dev data on fetch failure
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -103,6 +103,7 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
"""统一维护 provider 目录、models.dev 缓存和 OAuth 状态。"""
|
"""统一维护 provider 目录、models.dev 缓存和 OAuth 状态。"""
|
||||||
|
|
||||||
_MODELS_DEV_URL = "https://models.dev/api.json"
|
_MODELS_DEV_URL = "https://models.dev/api.json"
|
||||||
|
_MODELS_DEV_BUNDLED_PATH = Path(__file__).with_name("models.json")
|
||||||
_MODELS_DEV_CACHE_TTL = 12 * 60 * 60
|
_MODELS_DEV_CACHE_TTL = 12 * 60 * 60
|
||||||
_CHATGPT_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann"
|
_CHATGPT_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann"
|
||||||
_CHATGPT_ISSUER = "https://auth.openai.com"
|
_CHATGPT_ISSUER = "https://auth.openai.com"
|
||||||
@@ -753,11 +754,15 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
if not self._models_dev_cache_path.exists():
|
if not self._models_dev_cache_path.exists():
|
||||||
return {}
|
payload = None
|
||||||
payload = json.loads(self._models_dev_cache_path.read_text(encoding="utf-8"))
|
else:
|
||||||
|
payload = json.loads(self._models_dev_cache_path.read_text(encoding="utf-8"))
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.warning(f"读取 models.dev provider 缓存失败: {err}")
|
logger.warning(f"读取 models.dev provider 缓存失败: {err}")
|
||||||
return {}
|
payload = None
|
||||||
|
|
||||||
|
if not isinstance(payload, dict):
|
||||||
|
payload = self._load_bundled_models_dev_payload()
|
||||||
|
|
||||||
if not isinstance(payload, dict):
|
if not isinstance(payload, dict):
|
||||||
return {}
|
return {}
|
||||||
@@ -1236,6 +1241,19 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
logger.warning(f"读取 models.dev 缓存失败: {err}")
|
logger.warning(f"读取 models.dev 缓存失败: {err}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _load_bundled_models_dev_payload(self) -> dict[str, Any] | None:
|
||||||
|
try:
|
||||||
|
if not self._MODELS_DEV_BUNDLED_PATH.exists():
|
||||||
|
return None
|
||||||
|
payload = json.loads(
|
||||||
|
self._MODELS_DEV_BUNDLED_PATH.read_text(encoding="utf-8")
|
||||||
|
)
|
||||||
|
except Exception as err:
|
||||||
|
logger.warning(f"读取本地 models.dev 离线文件失败: {err}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
return payload if isinstance(payload, dict) else None
|
||||||
|
|
||||||
async def _write_models_dev_to_disk(self, payload: dict[str, Any]) -> None:
|
async def _write_models_dev_to_disk(self, payload: dict[str, Any]) -> None:
|
||||||
try:
|
try:
|
||||||
self._models_dev_cache_path.parent.mkdir(parents=True, exist_ok=True)
|
self._models_dev_cache_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
@@ -1291,6 +1309,11 @@ class LLMProviderManager(metaclass=Singleton):
|
|||||||
self._models_dev_data = cached
|
self._models_dev_data = cached
|
||||||
self._models_dev_loaded_at = now
|
self._models_dev_loaded_at = now
|
||||||
return cached
|
return cached
|
||||||
|
bundled = self._load_bundled_models_dev_payload()
|
||||||
|
if isinstance(bundled, dict):
|
||||||
|
self._models_dev_data = bundled
|
||||||
|
self._models_dev_loaded_at = now
|
||||||
|
return bundled
|
||||||
raise LLMProviderError(f"获取 models.dev 数据失败: {err}") from err
|
raise LLMProviderError(f"获取 models.dev 数据失败: {err}") from err
|
||||||
|
|
||||||
async def _models_dev_provider_payload(
|
async def _models_dev_provider_payload(
|
||||||
|
|||||||
@@ -210,6 +210,47 @@ class LlmProviderRegistryTest(unittest.TestCase):
|
|||||||
self.assertEqual([item["id"] for item in models], ["frog-1"])
|
self.assertEqual([item["id"] for item in models], ["frog-1"])
|
||||||
self.assertEqual(models[0]["source"], "models.dev")
|
self.assertEqual(models[0]["source"], "models.dev")
|
||||||
|
|
||||||
|
def test_get_models_dev_data_falls_back_to_bundled_file_when_fetch_and_cache_fail(self):
|
||||||
|
manager = LLMProviderManager()
|
||||||
|
payload = {
|
||||||
|
"frogbot": {
|
||||||
|
"id": "frogbot",
|
||||||
|
"name": "FrogBot",
|
||||||
|
"npm": "@ai-sdk/openai-compatible",
|
||||||
|
"models": {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch.object(manager, "_fetch_models_dev", AsyncMock(side_effect=RuntimeError("offline"))), patch.object(
|
||||||
|
manager, "_load_models_dev_from_disk", AsyncMock(return_value=None)
|
||||||
|
), patch.object(manager, "_load_bundled_models_dev_payload", return_value=payload):
|
||||||
|
data = asyncio.run(manager.get_models_dev_data(force_refresh=True))
|
||||||
|
|
||||||
|
self.assertEqual(data, payload)
|
||||||
|
self.assertEqual(manager._models_dev_data, payload)
|
||||||
|
|
||||||
|
def test_cached_models_dev_payload_falls_back_to_bundled_file(self):
|
||||||
|
manager = LLMProviderManager()
|
||||||
|
payload = {
|
||||||
|
"frogbot": {
|
||||||
|
"id": "frogbot",
|
||||||
|
"name": "FrogBot",
|
||||||
|
"npm": "@ai-sdk/openai-compatible",
|
||||||
|
"api": "https://app.frogbot.ai/api/v1",
|
||||||
|
"models": {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
missing_cache_path = Path(f"/tmp/llm-provider-cache-missing-{id(manager)}.json")
|
||||||
|
|
||||||
|
with patch.object(manager, "_models_dev_cache_path", missing_cache_path), patch.object(
|
||||||
|
manager, "_load_bundled_models_dev_payload", return_value=payload
|
||||||
|
):
|
||||||
|
provider = manager.get_provider("frogbot")
|
||||||
|
|
||||||
|
self.assertEqual(provider.id, "frogbot")
|
||||||
|
self.assertEqual(provider.default_base_url, "https://app.frogbot.ai/api/v1")
|
||||||
|
|
||||||
def test_builtin_provider_includes_baidu_qianfan_base_url_presets(self):
|
def test_builtin_provider_includes_baidu_qianfan_base_url_presets(self):
|
||||||
manager = LLMProviderManager()
|
manager = LLMProviderManager()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user