mirror of
https://github.com/JefferyHcool/BiliNote.git
synced 2026-07-13 00:21:37 +08:00
问题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>
217 lines
8.3 KiB
Python
217 lines
8.3 KiB
Python
|
||
|
||
from app.db.model_dao import insert_model, get_all_models, get_model_by_provider_and_name, delete_model
|
||
from app.db.provider_dao import get_enabled_providers
|
||
from app.enmus.exception import ProviderErrorEnum
|
||
from app.exceptions.provider import ProviderError
|
||
from app.gpt.gpt_factory import GPTFactory
|
||
from app.gpt.provider.OpenAI_compatible_provider import OpenAICompatibleProvider
|
||
from app.models.model_config import ModelConfig
|
||
from app.services.provider import ProviderService
|
||
from app.utils.logger import get_logger
|
||
|
||
logger=get_logger(__name__)
|
||
class ModelService:
|
||
|
||
@staticmethod
|
||
def _build_model_config(provider: dict) -> ModelConfig:
|
||
return ModelConfig(
|
||
api_key=provider["api_key"],
|
||
base_url=provider["base_url"],
|
||
provider=provider["name"],
|
||
model_name='',
|
||
name=provider["name"],
|
||
)
|
||
|
||
@staticmethod
|
||
def get_model_list(provider_id: int, verbose: bool = False):
|
||
provider = ProviderService.get_provider_by_id(provider_id)
|
||
if not provider:
|
||
return []
|
||
|
||
try:
|
||
config = ModelService._build_model_config(provider)
|
||
gpt = GPTFactory().from_config(config)
|
||
models = gpt.list_models()
|
||
if verbose:
|
||
print(f"[{provider['name']}] 模型列表: {models}")
|
||
return models
|
||
except Exception as e:
|
||
print(f"[{provider['name']}] 获取模型失败: {e}")
|
||
return []
|
||
|
||
@staticmethod
|
||
def get_all_models(verbose: bool = False):
|
||
try:
|
||
raw_models = get_all_models()
|
||
if verbose:
|
||
print(f"所有模型列表: {raw_models}")
|
||
return ModelService._format_models(raw_models)
|
||
except Exception as e:
|
||
print(f"获取所有模型失败: {e}")
|
||
return []
|
||
@staticmethod
|
||
def get_all_models_safe(verbose: bool = False):
|
||
try:
|
||
raw_models = get_all_models()
|
||
if verbose:
|
||
print(f"所有模型列表: {raw_models}")
|
||
return ModelService._format_models(raw_models)
|
||
except Exception as e:
|
||
print(f"获取所有模型失败: {e}")
|
||
return []
|
||
@staticmethod
|
||
def _format_models(raw_models: list) -> list:
|
||
"""
|
||
格式化模型列表
|
||
"""
|
||
formatted = []
|
||
for model in raw_models:
|
||
formatted.append({
|
||
"id": model.get("id"),
|
||
"provider_id": model.get("provider_id"),
|
||
"model_name": model.get("model_name"),
|
||
"created_at": model.get("created_at", None), # 如果有created_at字段
|
||
})
|
||
return formatted
|
||
@staticmethod
|
||
def get_enabled_models_by_provider( provider_id: str|int,):
|
||
from app.db.model_dao import get_models_by_provider
|
||
|
||
all_models = get_models_by_provider(provider_id)
|
||
enabled_models = all_models
|
||
return enabled_models
|
||
@staticmethod
|
||
def get_all_models_by_id(provider_id: str, verbose: bool = False):
|
||
"""拉取某供应商的可选模型列表,用于设置页下拉。
|
||
|
||
历史坑(issue #417):旧实现对 get_model_list 的返回值直接取 `.data`,但
|
||
get_model_list 在 /models 调用失败时会吞掉异常返回 `[]`,于是 `[].data`
|
||
触发 AttributeError,又被这里的 except 吞成 `[]` —— 最终接口返回
|
||
`{"code":0,"msg":"success","data":[]}`,把「DeepSeek /models 取不到」伪装成
|
||
成功的空列表,用户完全看不到原因。
|
||
|
||
现在:
|
||
1. 直接捕获 /models 的真实异常(不再二次吞);
|
||
2. normalize_models 兼容 SyncPage / list / dict,绝不再 `.data` 崩;
|
||
3. 动态拿不到(失败或空)时退回内置已知清单,保证下拉非空;
|
||
4. 仍然为空且确有报错时,把报错带回去(前端可提示,不再假装成功)。
|
||
"""
|
||
from app.services.model_fallback import (
|
||
builtin_fallback_models,
|
||
normalize_models,
|
||
as_model_dicts,
|
||
)
|
||
|
||
provider = ProviderService.get_provider_by_id(provider_id)
|
||
if not provider:
|
||
logger.warning(f"[{provider_id}] 供应商不存在")
|
||
return {"models": []}
|
||
|
||
models: list = []
|
||
error: str | None = None
|
||
try:
|
||
config = ModelService._build_model_config(provider)
|
||
gpt = GPTFactory().from_config(config)
|
||
models = normalize_models(gpt.list_models())
|
||
if verbose:
|
||
print(f"[{provider['name']}] 动态模型列表: {models}")
|
||
except Exception as e:
|
||
error = str(e)
|
||
logger.warning(f"[{provider['name']}] 动态获取模型失败,尝试回退内置清单: {e}")
|
||
|
||
if not models:
|
||
fallback = builtin_fallback_models(provider)
|
||
if fallback:
|
||
logger.info(f"[{provider['name']}] /models 为空,回退内置清单: {fallback}")
|
||
models = as_model_dicts(fallback, owned_by=provider.get("name", ""))
|
||
|
||
result = {"models": models}
|
||
if not models and error:
|
||
# 既没动态结果也没兜底清单:把真实报错带回去,别再伪装成功
|
||
result["error"] = error
|
||
else:
|
||
logger.info(f"[{provider['name']}] 获取模型成功,共 {len(models)} 个")
|
||
return result
|
||
@staticmethod
|
||
def connect_test(id: str, model: str | None = None) -> bool:
|
||
"""连通性测试:发一条最小化 chat completion。
|
||
|
||
model 优先级:
|
||
1. 调用方显式传入(前端可在「模型选择」UI 里挑一个再测)
|
||
2. DB 中该 provider 已保存的第一个模型
|
||
3. 都没有 → 抛错让用户先加一个模型
|
||
"""
|
||
provider = ProviderService.get_provider_by_id(id)
|
||
if not provider:
|
||
raise ProviderError(
|
||
code=ProviderErrorEnum.NOT_FOUND.code,
|
||
message=ProviderErrorEnum.NOT_FOUND.message,
|
||
)
|
||
if not provider.get('api_key'):
|
||
raise ProviderError(
|
||
code=ProviderErrorEnum.NOT_FOUND.code,
|
||
message=ProviderErrorEnum.NOT_FOUND.message,
|
||
)
|
||
|
||
if not model:
|
||
saved_models = ModelService.get_enabled_models_by_provider(provider["id"])
|
||
if not saved_models:
|
||
raise ProviderError(
|
||
code=ProviderErrorEnum.WRONG_PARAMETER.code,
|
||
message="请先为该供应商添加至少一个模型再测试连通性",
|
||
)
|
||
model = saved_models[0]["model_name"]
|
||
|
||
ok = OpenAICompatibleProvider.test_connection(
|
||
api_key=provider.get('api_key'),
|
||
base_url=provider.get('base_url'),
|
||
model=model,
|
||
)
|
||
if ok:
|
||
return True
|
||
raise ProviderError(
|
||
code=ProviderErrorEnum.WRONG_PARAMETER.code,
|
||
message=ProviderErrorEnum.WRONG_PARAMETER.message,
|
||
)
|
||
|
||
|
||
|
||
@staticmethod
|
||
def delete_model_by_id( model_id: int) -> bool:
|
||
try:
|
||
delete_model(model_id)
|
||
return True
|
||
except Exception as e:
|
||
print(f"[{model_id}] <UNK>: {e}")
|
||
return False
|
||
@staticmethod
|
||
def add_new_model(provider_id: int, model_name: str) -> bool:
|
||
try:
|
||
# 先查供应商是否存在
|
||
provider = ProviderService.get_provider_by_id(provider_id)
|
||
if not provider:
|
||
print(f"供应商ID {provider_id} 不存在,无法添加模型")
|
||
return False
|
||
|
||
# 查询是否已存在同名模型
|
||
existing = get_model_by_provider_and_name(provider_id, model_name)
|
||
if existing:
|
||
print(f"模型 {model_name} 已存在于供应商ID {provider_id} 下,跳过插入")
|
||
return False
|
||
|
||
# 插入模型
|
||
insert_model(provider_id=provider_id, model_name=model_name)
|
||
print(f"模型 {model_name} 已成功添加到供应商ID {provider_id}")
|
||
return True
|
||
except Exception as e:
|
||
print(f"添加模型失败: {e}")
|
||
return False
|
||
|
||
if __name__ == '__main__':
|
||
# 单个 Provider 测试
|
||
print(ModelService.get_model_list(1, verbose=True))
|
||
|
||
# 所有 Provider 模型测试
|
||
# print(ModelService.get_all_models(verbose=True))
|