532 lines
22 KiB
Python
532 lines
22 KiB
Python
"""LLM configuration management API endpoints."""
|
||
|
||
from typing import List, Optional
|
||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||
from langchain_openai import ChatOpenAI
|
||
from langchain_core.messages import AIMessage
|
||
from sqlalchemy.orm import Session
|
||
from sqlalchemy import or_, select, delete, update
|
||
|
||
from loguru import logger
|
||
|
||
from th_agenter.llm.embed.embed_llm import BGEEmbedLLM, EmbedLLM
|
||
from th_agenter.llm.online.online_llm import OnlineLLM
|
||
from ...db.database import get_session
|
||
from ...models.user import User
|
||
from ...models.llm_config import LLMConfig
|
||
from th_agenter.llm.base_llm import LLMConfig_DataClass
|
||
from ...core.simple_permissions import require_super_admin, require_authenticated_user
|
||
from ...schemas.llm_config import (
|
||
LLMConfigCreate, LLMConfigUpdate, LLMConfigResponse,
|
||
LLMConfigTest
|
||
)
|
||
from th_agenter.services.document_processor import get_document_processor
|
||
from utils.util_exceptions import HxfResponse
|
||
|
||
router = APIRouter(prefix="/llm-configs", tags=["llm-configs"])
|
||
|
||
@router.get("/", response_model=List[LLMConfigResponse], summary="获取大模型配置列表")
|
||
async def get_llm_configs(
|
||
skip: int = Query(0, ge=0),
|
||
limit: int = Query(100, ge=1, le=1000),
|
||
search: Optional[str] = Query(None),
|
||
provider: Optional[str] = Query(None),
|
||
is_active: Optional[bool] = Query(None),
|
||
is_embedding: Optional[bool] = Query(None),
|
||
session: Session = Depends(get_session),
|
||
current_user: User = Depends(require_authenticated_user)
|
||
):
|
||
"""获取大模型配置列表."""
|
||
session.title = "获取大模型配置列表"
|
||
session.desc = f"START: 获取大模型配置列表, skip={skip}, limit={limit}, search={search}, provider={provider}, is_active={is_active}, is_embedding={is_embedding}"
|
||
stmt = select(LLMConfig)
|
||
|
||
# 搜索
|
||
if search:
|
||
stmt = stmt.where(
|
||
or_(
|
||
LLMConfig.name.ilike(f"%{search}%"),
|
||
LLMConfig.model_name.ilike(f"%{search}%"),
|
||
LLMConfig.description.ilike(f"%{search}%")
|
||
)
|
||
)
|
||
|
||
# 服务商筛选
|
||
if provider:
|
||
stmt = stmt.where(LLMConfig.provider == provider)
|
||
|
||
# 状态筛选
|
||
if is_active is not None:
|
||
stmt = stmt.where(LLMConfig.is_active == is_active)
|
||
|
||
# 模型类型筛选
|
||
if is_embedding is not None:
|
||
stmt = stmt.where(LLMConfig.is_embedding == is_embedding)
|
||
|
||
# 排序
|
||
stmt = stmt.order_by(LLMConfig.name)
|
||
|
||
# 分页
|
||
stmt = stmt.offset(skip).limit(limit)
|
||
configs = (await session.execute(stmt)).scalars().all()
|
||
session.desc = f"SUCCESS: 获取 {len(configs)} 个大模型配置 ..."
|
||
return HxfResponse([config.to_dict(include_sensitive=True) for config in configs])
|
||
|
||
|
||
@router.get("/providers", summary="获取支持的大模型服务商列表")
|
||
async def get_llm_providers(
|
||
session: Session = Depends(get_session),
|
||
current_user: User = Depends(require_authenticated_user)
|
||
):
|
||
"""获取支持的大模型服务商列表."""
|
||
session.desc = "START: 获取支持的大模型服务商列表"
|
||
stmt = select(LLMConfig.provider).distinct()
|
||
providers = (await session.execute(stmt)).scalars().all()
|
||
session.desc = f"SUCCESS: 获取 {len(providers)} 个大模型服务商"
|
||
return HxfResponse([provider for provider in providers if provider])
|
||
|
||
|
||
|
||
@router.get("/active", response_model=List[LLMConfigResponse], summary="获取所有激活的大模型配置")
|
||
async def get_active_llm_configs(
|
||
is_embedding: Optional[bool] = Query(None),
|
||
session: Session = Depends(get_session),
|
||
current_user: User = Depends(require_authenticated_user)
|
||
):
|
||
"""获取所有激活的大模型配置."""
|
||
session.desc = f"START: 获取所有激活的大模型配置, is_embedding={is_embedding}"
|
||
stmt = select(LLMConfig).where(LLMConfig.is_active == True)
|
||
|
||
if is_embedding is not None:
|
||
stmt = stmt.where(LLMConfig.is_embedding == is_embedding)
|
||
|
||
stmt = stmt.order_by(LLMConfig.created_at)
|
||
configs = (await session.execute(stmt)).scalars().all()
|
||
session.desc = f"SUCCESS: 获取 {len(configs)} 个激活的大模型配置"
|
||
return HxfResponse([config.to_dict(include_sensitive=True) for config in configs])
|
||
|
||
@router.get("/default", response_model=LLMConfigResponse, summary="获取默认大模型配置")
|
||
async def get_default_llm_config(
|
||
is_embedding: bool = Query(False, description="是否获取嵌入模型默认配置"),
|
||
session: Session = Depends(get_session),
|
||
current_user: User = Depends(require_authenticated_user)
|
||
):
|
||
"""获取默认大模型配置."""
|
||
session.desc = f"START: 获取默认大模型配置, is_embedding={is_embedding}"
|
||
stmt = select(LLMConfig).where(
|
||
LLMConfig.is_default == True,
|
||
LLMConfig.is_embedding == is_embedding,
|
||
LLMConfig.is_active == True
|
||
)
|
||
config = (await session.execute(stmt)).scalar_one_or_none()
|
||
|
||
if not config:
|
||
model_type = "嵌入模型" if is_embedding else "对话模型"
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail=f"未找到默认{model_type}配置"
|
||
)
|
||
|
||
session.desc = f"SUCCESS: 获取默认大模型配置, is_embedding={is_embedding}"
|
||
return HxfResponse(config.to_dict(include_sensitive=True))
|
||
|
||
@router.get("/{config_id}", response_model=LLMConfigResponse, summary="获取大模型配置详情")
|
||
async def get_llm_config(
|
||
config_id: int,
|
||
session: Session = Depends(get_session),
|
||
current_user: User = Depends(require_authenticated_user)
|
||
):
|
||
"""获取大模型配置详情."""
|
||
stmt = select(LLMConfig).where(LLMConfig.id == config_id)
|
||
config = (await session.execute(stmt)).scalar_one_or_none()
|
||
if not config:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail="大模型配置不存在"
|
||
)
|
||
|
||
return HxfResponse(config.to_dict(include_sensitive=True))
|
||
|
||
|
||
@router.post("/", response_model=LLMConfigResponse, status_code=status.HTTP_201_CREATED, summary="创建大模型配置")
|
||
async def create_llm_config(
|
||
config_data: LLMConfigCreate,
|
||
session: Session = Depends(get_session),
|
||
current_user: User = Depends(require_super_admin)
|
||
):
|
||
"""创建大模型配置."""
|
||
# 检查配置名称是否已存在
|
||
# 先保存当前用户名,避免在refresh后访问可能导致MissingGreenlet错误
|
||
username = current_user.username
|
||
session.desc = f"START: 创建大模型配置, name={config_data.name}"
|
||
stmt = select(LLMConfig).where(LLMConfig.name == config_data.name)
|
||
existing_config = (await session.execute(stmt)).scalar_one_or_none()
|
||
if existing_config:
|
||
session.desc = f"ERROR: 配置名称已存在, name={config_data.name}"
|
||
raise HTTPException(
|
||
status_code=status.HTTP_400_BAD_REQUEST,
|
||
detail="配置名称已存在"
|
||
)
|
||
|
||
# 创建配置对象
|
||
config = LLMConfig_DataClass(
|
||
name=config_data.name,
|
||
provider=config_data.provider,
|
||
model_name=config_data.model_name,
|
||
api_key=config_data.api_key,
|
||
base_url=config_data.base_url,
|
||
max_tokens=config_data.max_tokens,
|
||
temperature=config_data.temperature,
|
||
top_p=config_data.top_p,
|
||
frequency_penalty=config_data.frequency_penalty,
|
||
presence_penalty=config_data.presence_penalty,
|
||
description=config_data.description,
|
||
is_active=config_data.is_active,
|
||
is_default=config_data.is_default,
|
||
is_embedding=config_data.is_embedding,
|
||
extra_config=config_data.extra_config or {}
|
||
)
|
||
|
||
# 验证配置
|
||
validation_result = config.validate_config()
|
||
if not validation_result['valid']:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_400_BAD_REQUEST,
|
||
detail=validation_result['error']
|
||
)
|
||
|
||
# 如果设为默认,取消同类型的其他默认配置
|
||
if config_data.is_default:
|
||
stmt = update(LLMConfig).where(
|
||
LLMConfig.is_embedding == config_data.is_embedding
|
||
).values({"is_default": False})
|
||
await session.execute(stmt)
|
||
|
||
session.desc = f"验证大模型配置, config_data"
|
||
# 创建配置
|
||
config = LLMConfig_DataClass(
|
||
name=config_data.name,
|
||
provider=config_data.provider,
|
||
model_name=config_data.model_name,
|
||
api_key=config_data.api_key,
|
||
base_url=config_data.base_url,
|
||
max_tokens=config_data.max_tokens,
|
||
temperature=config_data.temperature,
|
||
top_p=config_data.top_p,
|
||
frequency_penalty=config_data.frequency_penalty,
|
||
presence_penalty=config_data.presence_penalty,
|
||
description=config_data.description,
|
||
is_active=config_data.is_active,
|
||
is_default=config_data.is_default,
|
||
is_embedding=config_data.is_embedding,
|
||
extra_config=config_data.extra_config or {}
|
||
)
|
||
# Audit fields are set automatically by SQLAlchemy event listener
|
||
|
||
session.add(config)
|
||
await session.commit()
|
||
await session.refresh(config)
|
||
session.desc = f"SUCCESS: 创建大模型配置, name={config.name} by user {username}"
|
||
return HxfResponse(config.to_dict())
|
||
|
||
|
||
@router.put("/{config_id}", response_model=LLMConfigResponse, summary="更新大模型配置")
|
||
async def update_llm_config(
|
||
config_id: int,
|
||
config_data: LLMConfigUpdate,
|
||
session: Session = Depends(get_session),
|
||
current_user: User = Depends(require_authenticated_user)
|
||
):
|
||
"""更新大模型配置."""
|
||
username = current_user.username
|
||
session.desc = f"START: 更新大模型配置, id={config_id}"
|
||
stmt = select(LLMConfig).where(LLMConfig.id == config_id)
|
||
config = (await session.execute(stmt)).scalar_one_or_none()
|
||
if not config:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail="大模型配置不存在"
|
||
)
|
||
|
||
# 检查配置名称是否已存在(排除自己)
|
||
if config_data.name and config_data.name != config.name:
|
||
stmt = select(LLMConfig).where(
|
||
LLMConfig.name == config_data.name,
|
||
LLMConfig.id != config_id
|
||
)
|
||
existing_config = (await session.execute(stmt)).scalar_one_or_none()
|
||
if existing_config:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_400_BAD_REQUEST,
|
||
detail="配置名称已存在"
|
||
)
|
||
|
||
# 如果设为默认,取消同类型的其他默认配置
|
||
if config_data.is_default is True:
|
||
# 获取当前配置的embedding类型,如果更新中包含is_embedding则使用新值
|
||
is_embedding = config_data.is_embedding if config_data.is_embedding is not None else config.is_embedding
|
||
stmt = update(LLMConfig).where(
|
||
LLMConfig.is_embedding == is_embedding,
|
||
LLMConfig.id != config_id
|
||
).values({"is_default": False})
|
||
await session.execute(stmt)
|
||
|
||
# 更新字段
|
||
update_data = config_data.model_dump(exclude_unset=True)
|
||
was_embedding = config.is_embedding
|
||
for field, value in update_data.items():
|
||
setattr(config, field, value)
|
||
|
||
await session.commit()
|
||
await session.refresh(config)
|
||
|
||
# 如果设置为默认嵌入模型,更新文档处理器的嵌入模型
|
||
if config.is_embedding and config.is_default:
|
||
try:
|
||
document_processor = await get_document_processor(session)
|
||
await document_processor._init_embeddings(session)
|
||
except Exception as e:
|
||
logger.warning(f"更新文档处理器嵌入模型失败: {str(e)}")
|
||
# 不阻止更新配置,只记录警告
|
||
|
||
session.desc = f"SUCCESS: 更新大模型配置, id={config_id} by user {username}"
|
||
return HxfResponse(config.to_dict())
|
||
|
||
@router.delete("/{config_id}", status_code=status.HTTP_204_NO_CONTENT, summary="删除大模型配置")
|
||
async def delete_llm_config(
|
||
config_id: int,
|
||
session: Session = Depends(get_session),
|
||
current_user: User = Depends(require_super_admin)
|
||
):
|
||
"""删除大模型配置."""
|
||
username = current_user.username
|
||
session.desc = f"START: 删除大模型配置, id={config_id}"
|
||
stmt = select(LLMConfig).where(LLMConfig.id == config_id)
|
||
config = (await session.execute(stmt)).scalar_one_or_none()
|
||
if not config:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail="大模型配置不存在"
|
||
)
|
||
session.desc = f"待删除大模型记录 {config.to_dict()}"
|
||
# TODO: 检查是否有对话或其他功能正在使用该配置
|
||
# 这里可以添加相关的检查逻辑
|
||
|
||
# 删除配置
|
||
await session.delete(config)
|
||
await session.commit()
|
||
|
||
session.desc = f"SUCCESS: 删除大模型配置成功, id={config_id} by user {username}"
|
||
return HxfResponse({"message": "LLM config deleted successfully"})
|
||
|
||
@router.post("/{config_id}/test", summary="测试连接大模型配置")
|
||
async def test_llm_config(
|
||
config_id: int,
|
||
test_data: LLMConfigTest,
|
||
session: Session = Depends(get_session),
|
||
current_user: User = Depends(require_authenticated_user)
|
||
):
|
||
"""测试连接大模型配置."""
|
||
username = current_user.username
|
||
session.desc = f"TEST: 测试连接大模型配置 {config_id} by user {username}"
|
||
stmt = select(LLMConfig).where(LLMConfig.id == config_id)
|
||
config = (await session.execute(stmt)).scalar_one_or_none()
|
||
if not config:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail="大模型配置不存在"
|
||
)
|
||
|
||
logger.info(f"TEST: 测试连接大模型配置 {config_id} by user {username}")
|
||
config_name = config.name
|
||
# 验证配置
|
||
validation_result = config.validate_config()
|
||
logger.info(f"TEST: 验证大模型配置 {config_name} validation_result = {validation_result}")
|
||
if not validation_result["valid"]:
|
||
return {
|
||
"success": False,
|
||
"message": f"配置验证失败: {validation_result['error']}",
|
||
"details": validation_result
|
||
}
|
||
|
||
session.desc = f"准备测试LLM功能 > 测试连接大模型配置 {config.to_dict()}"
|
||
# 尝试创建客户端并发送测试请求
|
||
try:
|
||
# # 这里应该根据不同的服务商创建相应的客户端
|
||
# # 由于具体的客户端实现可能因服务商而异,这里提供一个通用的框架
|
||
|
||
test_message = test_data.message or "Hello, this is a test message."
|
||
session.desc = f"准备测试LLM功能 > test_message = {test_message}"
|
||
|
||
# 处理 base_url:如果包含 /chat/completions,需要移除
|
||
original_base_url = config.base_url
|
||
if config.base_url and '/chat/completions' in config.base_url:
|
||
# 移除 /chat/completions 后缀,ChatOpenAI 会自动添加
|
||
config.base_url = config.base_url.replace('/chat/completions', '').rstrip('/')
|
||
logger.info(f"调整 base_url: {original_base_url} -> {config.base_url}")
|
||
|
||
# 处理 SiliconFlow 的模型名称格式
|
||
original_model_name = config.model_name
|
||
if 'siliconflow' in config.base_url.lower() and '/' not in config.model_name:
|
||
# SiliconFlow 需要 org/model 格式,尝试自动转换
|
||
model_name_lower = config.model_name.lower()
|
||
if 'deepseek' in model_name_lower or 'r1' in model_name_lower:
|
||
# 尝试常见的 DeepSeek 模型格式
|
||
if 'r1' in model_name_lower:
|
||
config.model_name = 'deepseek-ai/DeepSeek-R1'
|
||
elif 'v3' in model_name_lower:
|
||
config.model_name = 'deepseek-ai/DeepSeek-V3'
|
||
else:
|
||
config.model_name = f'deepseek-ai/{config.model_name}'
|
||
logger.info(f"调整 SiliconFlow 模型名称: {original_model_name} -> {config.model_name}")
|
||
|
||
if config.is_embedding:
|
||
config.provider = "ollama"
|
||
streaming_llm = BGEEmbedLLM(config)
|
||
else:
|
||
streaming_llm = OnlineLLM(config)
|
||
session.desc = f"创建{'EmbeddingLLM' if config.is_embedding else 'OnlineLLM'}完毕 > 测试连接大模型配置 {config.to_dict()}"
|
||
streaming_llm.load_model() # 加载模型
|
||
session.desc = f"加载模型完毕,模型名称:{config.model_name},base_url: {config.base_url},准备测试对话..."
|
||
|
||
if config.is_embedding:
|
||
# 测试嵌入模型,使用嵌入API而非聊天API
|
||
test_text = test_message or "Hello, this is a test message for embedding"
|
||
response = streaming_llm.embed_query(test_text)
|
||
else:
|
||
# 测试聊天模型
|
||
from langchain_core.messages import SystemMessage, HumanMessage
|
||
messages = [
|
||
SystemMessage(content="你是一个简洁的助手,回答控制在50字以内"),
|
||
HumanMessage(content=test_message)
|
||
]
|
||
# 使用异步调用
|
||
import asyncio
|
||
if hasattr(streaming_llm.model, 'ainvoke'):
|
||
response = await streaming_llm.model.ainvoke(messages)
|
||
else:
|
||
# 如果模型不支持异步,使用同步调用但在线程中执行
|
||
response = await asyncio.to_thread(streaming_llm.model.invoke, messages)
|
||
session.desc = f"测试连接大模型配置 {config_name} 成功 >>> 响应: {type(response)}"
|
||
|
||
return HxfResponse({
|
||
"success": True,
|
||
"message": "LLM测试成功",
|
||
"request": test_message,
|
||
"response": response.content if hasattr(response, 'content') else response, # 使用转换后的字典
|
||
"latency_ms": 150, # 模拟延迟
|
||
"config_info": config.to_dict()
|
||
})
|
||
|
||
except Exception as test_error:
|
||
import traceback
|
||
error_detail = str(test_error)
|
||
error_traceback = traceback.format_exc()
|
||
logger.error(f"测试LLM配置失败: {error_detail}\n{error_traceback}")
|
||
session.desc = f"ERROR: 测试连接大模型配置 {config.name} 失败, error: {error_detail}"
|
||
|
||
# 提取更详细的错误信息
|
||
if hasattr(test_error, 'response') and hasattr(test_error.response, 'status_code'):
|
||
error_detail = f"HTTP {test_error.response.status_code}: {error_detail}"
|
||
# 如果是模型不存在的错误,提供更详细的提示
|
||
if "Model does not exist" in error_detail or "model" in error_detail.lower():
|
||
original_model = original_model_name if 'original_model_name' in locals() else (config.model_name if 'config' in locals() else "N/A")
|
||
if 'siliconflow' in (original_base_url if 'original_base_url' in locals() else (config.base_url if 'config' in locals() else "")).lower():
|
||
error_detail += f"\n提示:SiliconFlow 的模型名称格式应为 'org/model-name'(如 'deepseek-ai/DeepSeek-R1'),当前配置: {original_model}"
|
||
elif "Not Found" in error_detail or "404" in error_detail:
|
||
error_detail = f"API端点未找到。请检查 base_url 配置是否正确。当前 base_url: {original_base_url if 'original_base_url' in locals() else config.base_url}"
|
||
|
||
return HxfResponse({
|
||
"success": False,
|
||
"message": f"LLM测试失败: {error_detail}",
|
||
"test_message": test_message if 'test_message' in locals() else "N/A",
|
||
"config_info": config.to_dict() if 'config' in locals() else {}
|
||
})
|
||
|
||
@router.post("/{config_id}/toggle-status", summary="切换大模型配置状态")
|
||
async def toggle_llm_config_status(
|
||
config_id: int,
|
||
session: Session = Depends(get_session),
|
||
current_user: User = Depends(require_super_admin)
|
||
):
|
||
"""切换大模型配置状态."""
|
||
username = current_user.username
|
||
session.desc = f"START: 切换大模型配置状态, id={config_id} by user {username}"
|
||
|
||
stmt = select(LLMConfig).where(LLMConfig.id == config_id)
|
||
config = (await session.execute(stmt)).scalar_one_or_none()
|
||
if not config:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail="大模型配置不存在"
|
||
)
|
||
|
||
# 切换状态
|
||
config.is_active = not config.is_active
|
||
# Audit fields are set automatically by SQLAlchemy event listener
|
||
|
||
await session.commit()
|
||
await session.refresh(config)
|
||
|
||
status_text = "激活" if config.is_active else "禁用"
|
||
session.desc = f"SUCCESS: 切换大模型配置状态: {config.name} {status_text} by user {username}"
|
||
|
||
return HxfResponse({
|
||
"message": f"配置已{status_text}",
|
||
"is_active": config.is_active
|
||
})
|
||
|
||
|
||
@router.post("/{config_id}/set-default", summary="设置默认大模型配置")
|
||
async def set_default_llm_config(
|
||
config_id: int,
|
||
session: Session = Depends(get_session),
|
||
current_user: User = Depends(require_authenticated_user)
|
||
):
|
||
"""设置默认大模型配置."""
|
||
username = current_user.username
|
||
session.desc = f"START: 设置大模型配置 {config_id} 为默认 by user {username}"
|
||
|
||
stmt = select(LLMConfig).where(LLMConfig.id == config_id)
|
||
config = (await session.execute(stmt)).scalar_one_or_none()
|
||
if not config:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail="大模型配置不存在"
|
||
)
|
||
|
||
# 检查配置是否激活
|
||
if not config.is_active:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_400_BAD_REQUEST,
|
||
detail="只能将激活的配置设为默认"
|
||
)
|
||
|
||
# 取消同类型的其他默认配置
|
||
stmt = update(LLMConfig).where(
|
||
LLMConfig.is_embedding == config.is_embedding,
|
||
LLMConfig.id != config_id
|
||
).values({"is_default": False})
|
||
await session.execute(stmt)
|
||
|
||
# 设置当前配置为默认
|
||
config.is_default = True
|
||
config.set_audit_fields(current_user.id, is_update=True)
|
||
|
||
await session.commit()
|
||
await session.refresh(config)
|
||
|
||
model_type = "嵌入模型" if config.is_embedding else "对话模型"
|
||
# 如果设置为默认嵌入模型,更新文档处理器的嵌入模型
|
||
if config.is_embedding:
|
||
try:
|
||
document_processor = await get_document_processor(session)
|
||
await document_processor._init_embeddings(session)
|
||
except Exception as e:
|
||
logger.warning(f"更新文档处理器嵌入模型失败: {str(e)}")
|
||
# 不阻止设置默认配置,只记录警告
|
||
session.desc = f"SUCCESS: 设置大模型配置 {config.name} ({model_type}) 为默认 by user {username}"
|
||
return HxfResponse({
|
||
"message": f"已将 {config.name} 设为默认{model_type}配置",
|
||
"is_default": config.is_default
|
||
}) |