Files
BiliNote/backend/tests/test_model_fallback.py
huangjianwu bebf2e8c61 fix(backend): 修复 DeepSeek 模型列表为空 & whisper 下载不走代理 (#417)
问题1 — DeepSeek 模型下拉为空:
provider 的 /models 调用失败时,get_model_list 吞掉异常返回 [],
get_all_models_by_id 再对 [] 取 .data 触发 AttributeError 又被吞,
最终接口返回 {"code":0,"msg":"success","data":[]},把失败伪装成空成功,
用户看不到任何原因。
- 捕获并回传真实错误,不再二次吞
- 新增 model_fallback.normalize_models 兼容 SyncPage/list/dict,绝不再 .data 崩
- 内置供应商提供已知模型兜底清单(DeepSeek→deepseek-chat/deepseek-reasoner,
  Qwen→qwen-plus 等),动态拿不到时回退,保证下拉非空;动态可用时仍以动态为准

问题2 — whisper 模型下载不走代理(Docker 里代理没生效):
snapshot_download 既不读 UI 配的代理,HF_ENDPOINT 又固定 hf-mirror.com。
- ProxyConfigManager.apply_to_env() 把生效代理 export 到 HTTP(S)_PROXY/ALL_PROXY,
  huggingface_hub 即可复用;在每次下载前与启动时应用(覆盖转写按需下载)
- 网络类下载报错翻译成可操作提示(配代理 / 改 HF_ENDPOINT / 检查容器外网)
- .env.example 补充 HF_ENDPOINT 与代理覆盖说明

新增 tests/test_model_fallback.py、tests/test_proxy_apply_env.py(14 用例)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 13:54:44 +08:00

90 lines
3.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import importlib.util
import pathlib
import unittest
ROOT = pathlib.Path(__file__).resolve().parents[1]
MODULE_PATH = ROOT / "app" / "services" / "model_fallback.py"
spec = importlib.util.spec_from_file_location("model_fallback", MODULE_PATH)
if spec is None or spec.loader is None:
raise ImportError("model_fallback module spec not found")
model_fallback = importlib.util.module_from_spec(spec)
spec.loader.exec_module(model_fallback)
builtin_fallback_models = model_fallback.builtin_fallback_models
normalize_models = model_fallback.normalize_models
as_model_dicts = model_fallback.as_model_dicts
class FakeModel:
"""模拟 openai SDK 的 Modelpydantic对象。"""
def __init__(self, mid, created=None, owned_by="x"):
self.id = mid
self.created = created
self.object = "model"
self.owned_by = owned_by
def dict(self):
return {"id": self.id, "created": self.created, "object": self.object, "owned_by": self.owned_by}
class FakeSyncPage:
def __init__(self, data):
self.data = data
class TestBuiltinFallbackModels(unittest.TestCase):
def test_deepseek_by_id_has_fallback(self):
# issue #417DeepSeek /models 不稳定,必须有兜底清单
models = builtin_fallback_models({"id": "deepseek", "name": "DeepSeek"})
self.assertIn("deepseek-chat", models)
self.assertIn("deepseek-reasoner", models)
def test_match_by_name_case_insensitive(self):
models = builtin_fallback_models({"id": "whatever-uuid", "name": "deepseek"})
self.assertIn("deepseek-chat", models)
def test_unknown_provider_returns_empty(self):
self.assertEqual(builtin_fallback_models({"id": "nope", "name": "nope"}), [])
def test_none_provider_returns_empty(self):
self.assertEqual(builtin_fallback_models(None), [])
class TestNormalizeModels(unittest.TestCase):
def test_syncpage_with_models(self):
page = FakeSyncPage([FakeModel("deepseek-chat"), FakeModel("deepseek-reasoner")])
out = normalize_models(page)
self.assertEqual([m["id"] for m in out], ["deepseek-chat", "deepseek-reasoner"])
def test_plain_list_of_models(self):
out = normalize_models([FakeModel("a"), FakeModel("b")])
self.assertEqual([m["id"] for m in out], ["a", "b"])
def test_empty_list_does_not_raise(self):
# 关键回归:旧代码对 [] 取 .data 会 AttributeError
self.assertEqual(normalize_models([]), [])
def test_list_of_dicts(self):
out = normalize_models([{"id": "x", "object": "model"}])
self.assertEqual(out[0]["id"], "x")
def test_drops_entries_without_id(self):
out = normalize_models([{"object": "model"}, {"id": "ok"}])
self.assertEqual([m["id"] for m in out], ["ok"])
def test_none_returns_empty(self):
self.assertEqual(normalize_models(None), [])
class TestAsModelDicts(unittest.TestCase):
def test_shape_matches_sdk(self):
out = as_model_dicts(["deepseek-chat"], owned_by="DeepSeek")
self.assertEqual(out[0]["id"], "deepseek-chat")
self.assertEqual(out[0]["object"], "model")
self.assertEqual(out[0]["owned_by"], "DeepSeek")
if __name__ == "__main__":
unittest.main()