feat(model): 引入 is_v2_spec_format 函数以优化模型规格判断逻辑

This commit is contained in:
Wenjie Zhang 2026-04-26 20:59:40 +08:00
parent 2e0b4a8358
commit a8eb5ab6fe
10 changed files with 48 additions and 30 deletions

View File

@ -5,6 +5,7 @@ from langchain.chat_models import BaseChatModel, init_chat_model
from pydantic import SecretStr
from yuxi import config
from yuxi.services.model_cache import is_v2_spec_format
from yuxi.utils import get_docker_safe_url
from yuxi.utils.logging_config import logger
@ -64,8 +65,8 @@ def load_chat_model(fully_specified_name: str, **kwargs) -> BaseChatModel:
"""
Load a chat model from a fully specified name.
"""
# v2 判断:如果 spec 包含冒号,尝试走 v2 路径
if ":" in fully_specified_name:
# v2 判断:第一个特殊字符为冒号则走 v2 路径
if is_v2_spec_format(fully_specified_name):
from yuxi.services.model_cache import model_cache
info = model_cache.get_model_info(fully_specified_name)

View File

@ -5,6 +5,7 @@ from openai import AsyncOpenAI
from tenacity import before_sleep_log, retry, retry_if_exception_type, stop_after_attempt, wait_exponential
from yuxi import config
from yuxi.services.model_cache import is_v2_spec_format
from yuxi.utils import logger
@ -126,13 +127,16 @@ def select_model_v2(spec: str) -> OpenAIBase:
def select_model(model_provider=None, model_name=None, model_spec=None):
"""根据模型提供者选择模型"""
# v2 判断:如果 spec 包含冒号且在缓存中存在,走 v2 路径
if model_spec and ":" in model_spec:
if model_spec and is_v2_spec_format(model_spec):
from yuxi.services.model_cache import model_cache
if model_cache.is_v2_spec(model_spec):
return select_model_v2(model_spec)
available = model_cache.get_all_specs("chat")
available_ids = [s.spec for s in available[:10]]
raise ValueError(f"未找到 V2 模型: '{model_spec}'。可用聊天模型 ({len(available)}): {available_ids}")
logger.warning(
f"旧版本的模型选择逻辑已废弃,建议尽快迁移至新的模型配置;"
f"当前模型选择参数: provider={model_provider}, model_name={model_name}, spec={model_spec}"

View File

@ -7,6 +7,7 @@ import httpx
import requests
from yuxi import config
from yuxi.services.model_cache import is_v2_spec_format
from yuxi.utils import get_docker_safe_url, hashstr, logger
@ -288,7 +289,7 @@ def get_embedding_model_info_by_id(model_id: str) -> dict:
api_key 已从环境变量解析为实际值
"""
# V2 spec 检测
if isinstance(model_id, str) and ":" in model_id:
if isinstance(model_id, str) and is_v2_spec_format(model_id):
from yuxi.services.model_cache import model_cache
info = model_cache.get_model_info(model_id)
@ -321,8 +322,8 @@ def select_embedding_model(model_id):
V1 格式: provider/model_name斜杠分隔
V2 格式: provider_id:model_id冒号分隔
"""
# V2 spec 检测:包含 ":" 且存在于缓存中
if isinstance(model_id, str) and ":" in model_id:
# V2 spec 检测:第一个特殊字符为冒号且存在于缓存中
if isinstance(model_id, str) and is_v2_spec_format(model_id):
from yuxi.services.model_cache import model_cache
if model_cache.is_v2_spec(model_id):
@ -385,7 +386,7 @@ async def test_embedding_model_status_by_spec(spec: str) -> dict:
V2 spec 格式: provider_id:model_id冒号分隔
"""
try:
if ":" in spec:
if is_v2_spec_format(spec):
from yuxi.services.model_cache import model_cache
if model_cache.is_v2_spec(spec):

View File

@ -22,6 +22,22 @@ _DEFAULT_REDIS_URL = "redis://redis:6379/0"
_CACHE_TTL_SECONDS = 5
def is_v2_spec_format(spec: str) -> bool:
"""判断 spec 是否为 V2 格式(基于格式特征而非缓存查找)。
V2 格式的特征是第一个特殊字符为冒号provider_id:model_id
V1 格式的特征是第一个特殊字符为斜杠provider/model_name
Provider ID 中不包含斜杠因此第一个特殊字符决定了格式类型
"""
colon_pos = spec.find(":")
slash_pos = spec.find("/")
if colon_pos == -1:
return False
if slash_pos == -1:
return True
return colon_pos < slash_pos
@dataclass(frozen=True)
class ModelInfo:
"""不可变的模型信息,供运行时使用。"""
@ -245,8 +261,8 @@ def resolve_model_spec(spec: str) -> ModelInfo:
if not spec:
raise ValueError("spec 不能为空")
# V2: 优先查找缓存
if ":" in spec:
# V2: 第一个特殊字符为冒号则走 v2 路径
if is_v2_spec_format(spec):
info = model_cache.get_model_info(spec)
if info:
return info

View File

@ -15,6 +15,7 @@ from yuxi.repositories.model_provider_repository import (
update_model_provider,
)
from yuxi.config.builtin_providers import BUILTIN_PROVIDERS
from yuxi.services.model_cache import is_v2_spec_format
from yuxi.storage.postgres.models_business import ModelProvider
VALID_MODEL_TYPES = {"chat", "embedding", "rerank"}
@ -375,7 +376,7 @@ async def test_model_status_by_spec(spec: str) -> dict:
from yuxi.services.model_cache import model_cache
# V2: 从缓存识别模型类型并分派
if ":" in spec:
if is_v2_spec_format(spec):
info = model_cache.get_model_info(spec)
if info:
if info.model_type == "embedding":

View File

@ -18,6 +18,7 @@ from yuxi.plugins.parser import Parser, SUPPORTED_FILE_EXTENSIONS, is_supported_
from yuxi.knowledge.utils import calculate_content_hash
from yuxi.knowledge.utils.kb_utils import parse_minio_url
from yuxi.models.embed import test_all_embedding_models_status, test_embedding_model_status
from yuxi.services.model_cache import is_v2_spec_format
from yuxi.storage.postgres.models_business import User
from yuxi.storage.minio.client import MinIOClient, StorageError, aupload_file_to_minio, get_minio_client
from yuxi.utils import logger
@ -149,8 +150,8 @@ async def create_database(
if not embed_model_name:
raise HTTPException(status_code=400, detail="embed_model_name 不能为空")
# V2 embedding model (spec 格式: provider_id:model_id使用冒号分隔)
if ":" in embed_model_name:
# V2 embedding model (spec 格式: provider_id:model_id第一个特殊字符为冒号)
if is_v2_spec_format(embed_model_name):
from yuxi.services.model_cache import model_cache
info = model_cache.get_model_info(embed_model_name)
@ -443,7 +444,9 @@ async def add_documents(
db_id, file_id, indexing_params, operator_id=current_user.user_id
)
# 2. 执行入库(传入 indexing_params 确保使用的参数与用户设置一致)
result = await knowledge_base.index_file(db_id, file_id, operator_id=current_user.user_id, params=indexing_params)
result = await knowledge_base.index_file(
db_id, file_id, operator_id=current_user.user_id, params=indexing_params
)
processed_items.append(result)
except Exception as index_error:
logger.error(f"自动入库失败 {item} (file_id={file_id}): {index_error}")

View File

@ -1063,7 +1063,7 @@ const confirmDeleteConfig = async () => {
flex-shrink: 0;
&.open {
width: 400px;
width: 360px;
}
.sidebar-header {
@ -1074,7 +1074,7 @@ const confirmDeleteConfig = async () => {
border-bottom: 1px solid var(--gray-150);
background: var(--gray-0);
flex-shrink: 0;
min-width: 400px;
min-width: 360px;
z-index: 10;
.header-top-row {
@ -1150,7 +1150,7 @@ const confirmDeleteConfig = async () => {
flex: 1;
overflow-y: auto;
padding: 10px 12px 8px;
min-width: 400px;
min-width: 360px;
.agent-info {
.agent-basic-info {

View File

@ -293,7 +293,7 @@ const renameChat = async (chatId) => {
.conversation-actions {
position: absolute;
top: 50%;
right: 8px;
right: 4px;
display: flex;
align-items: center;
opacity: 0;

View File

@ -7,7 +7,7 @@ import {
BarChart3,
ClipboardList,
Blocks,
Cpu,
Box,
PanelLeftClose,
PanelLeftOpen,
MessageCirclePlus
@ -154,8 +154,8 @@ const mainList = computed(() => {
items.push({
name: '模型配置',
path: '/model-config',
icon: Cpu,
activeIcon: Cpu
icon: Box,
activeIcon: Box
})
items.push({

View File

@ -661,14 +661,6 @@ onMounted(loadProviders)
</div>
</div>
</div>
<!-- Empty State -->
<a-empty v-if="!filteredProviders.length" :image="false" description="暂无供应商">
<a-button type="primary" class="lucide-icon-btn" @click="openCreateProviderModal">
<Plus :size="14" />
新增供应商
</a-button>
</a-empty>
</div>
<!-- Provider Edit Modal -->
@ -1182,7 +1174,7 @@ onMounted(loadProviders)
width: 40px;
height: 40px;
border-radius: 8px;
background: var(--main-50);
background: #e1f6fb; // 使
overflow: hidden;
img {