feat: Add support for builtin models in the application

- Introduced `is_builtin` property in the model configuration to identify builtin models.
- Updated frontend components to display a tag for builtin models in the ModelSelector and ModelSettings.
- Enhanced the ModelEditorDialog to prevent editing and deletion of builtin models.
- Implemented logic in the backend to hide sensitive information for builtin models and restrict their modification.
- Added informative UI elements to clarify the nature of builtin models in the settings and editor views.
This commit is contained in:
wizardchen
2025-11-14 12:09:52 +08:00
parent a1fb794650
commit bb6dff80b5
13 changed files with 571 additions and 49 deletions
+173
View File
@@ -0,0 +1,173 @@
# 内置模型管理指南
## 概述
内置模型是系统级别的模型配置,对所有租户可见,但敏感信息会被隐藏,且不可编辑或删除。内置模型通常用于提供系统默认的模型配置,确保所有租户都能使用统一的模型服务。
## 内置模型特性
- **所有租户可见**:内置模型对所有租户都可见,无需单独配置
- **安全保护**:内置模型的敏感信息(API Key、Base URL)会被隐藏,无法查看详情
- **只读保护**:内置模型不能被编辑或删除,只能设置为默认模型
- **统一管理**:由系统管理员统一维护,确保配置一致性和安全性
## 如何添加内置模型
内置模型需要通过数据库直接插入。以下是添加内置模型的步骤:
### 1. 准备模型数据
首先,确保你已经有了要设置为内置模型的模型配置信息,包括:
- 模型名称(name)
- 模型类型(type):`KnowledgeQA`、`Embedding`、`Rerank` 或 `VLLM`
- 模型来源(source):`local` 或 `remote`
- 模型参数(parameters):包括 base_url、api_key 等
- 租户ID(tenant_id):建议使用小于10000的租户ID,避免冲突
### 2. 执行 SQL 插入语句
使用以下 SQL 语句插入内置模型:
```sql
-- 示例:插入一个 LLM 内置模型
INSERT INTO models (
id,
tenant_id,
name,
type,
source,
description,
parameters,
is_default,
status,
is_builtin
) VALUES (
'builtin-llm-001', -- 使用固定ID,建议使用 builtin- 前缀
10000, -- 租户ID(使用第一个租户)
'GPT-4', -- 模型名称
'KnowledgeQA', -- 模型类型
'remote', -- 模型来源
'内置 LLM 模型', -- 描述
'{"base_url": "https://api.openai.com/v1", "api_key": "sk-xxx"}'::jsonb, -- 参数(JSON格式)
false, -- 是否默认
'active', -- 状态
true -- 标记为内置模型
) ON CONFLICT (id) DO NOTHING;
-- 示例:插入一个 Embedding 内置模型
INSERT INTO models (
id,
tenant_id,
name,
type,
source,
description,
parameters,
is_default,
status,
is_builtin
) VALUES (
'builtin-embedding-001',
10000,
'text-embedding-ada-002',
'Embedding',
'remote',
'内置 Embedding 模型',
'{"base_url": "https://api.openai.com/v1", "api_key": "sk-xxx", "embedding_parameters": {"dimension": 1536, "truncate_prompt_tokens": 0}}'::jsonb,
false,
'active',
true
) ON CONFLICT (id) DO NOTHING;
-- 示例:插入一个 ReRank 内置模型
INSERT INTO models (
id,
tenant_id,
name,
type,
source,
description,
parameters,
is_default,
status,
is_builtin
) VALUES (
'builtin-rerank-001',
10000,
'bge-reranker-base',
'Rerank',
'remote',
'内置 ReRank 模型',
'{"base_url": "https://api.example.com/v1", "api_key": "xxx"}'::jsonb,
false,
'active',
true
) ON CONFLICT (id) DO NOTHING;
-- 示例:插入一个 VLLM 内置模型
INSERT INTO models (
id,
tenant_id,
name,
type,
source,
description,
parameters,
is_default,
status,
is_builtin
) VALUES (
'builtin-vllm-001',
10000,
'gpt-4-vision',
'VLLM',
'remote',
'内置 VLLM 模型',
'{"base_url": "https://api.openai.com/v1", "api_key": "sk-xxx"}'::jsonb,
false,
'active',
true
) ON CONFLICT (id) DO NOTHING;
```
### 3. 验证插入结果
执行以下 SQL 查询验证内置模型是否成功插入:
```sql
SELECT id, name, type, is_builtin, status
FROM models
WHERE is_builtin = true
ORDER BY type, created_at;
```
## 注意事项
1. **ID 命名规范**:建议使用 `builtin-{type}-{序号}` 的格式,例如 `builtin-llm-001`、`builtin-embedding-001`
2. **租户ID**:内置模型可以属于任意租户,但建议使用第一个租户ID(通常是 10000)
3. **参数格式**:`parameters` 字段必须是有效的 JSON 格式
4. **幂等性**:使用 `ON CONFLICT (id) DO NOTHING` 确保重复执行不会报错
5. **安全性**:内置模型的 API Key 和 Base URL 在前端会被自动隐藏,但数据库中的原始数据仍然存在,请妥善保管数据库访问权限
## 将现有模型设置为内置模型
如果你已经有一个模型,想将其设置为内置模型,可以使用 UPDATE 语句:
```sql
UPDATE models
SET is_builtin = true
WHERE id = '模型ID' AND name = '模型名称';
```
## 移除内置模型
如果需要移除内置模型标记(恢复为普通模型),执行:
```sql
UPDATE models
SET is_builtin = false
WHERE id = '模型ID';
```
注意:移除内置模型标记后,该模型将恢复为普通模型,可以被编辑和删除。
+1
View File
@@ -19,6 +19,7 @@ export interface ModelConfig {
parameter_size?: string; // Ollama模型参数大小 (e.g., "7B", "13B", "70B")
};
is_default?: boolean;
is_builtin?: boolean;
status?: string;
created_at?: string;
updated_at?: string;
+78 -20
View File
@@ -781,12 +781,12 @@ const handleCancel = () => {
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
background: rgba(0, 0, 0, 0.45);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
backdrop-filter: blur(4px);
backdrop-filter: blur(6px);
overflow: hidden; // 防止背景滚动
}
@@ -798,7 +798,7 @@ const handleCancel = () => {
max-height: 85vh;
background: #fff;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.05);
display: flex;
flex-direction: column;
overflow: hidden;
@@ -813,41 +813,43 @@ const handleCancel = () => {
height: 32px;
border: none;
background: #f5f5f5;
border-radius: 6px;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: #666;
transition: all 0.2s ease;
transition: background-color 0.2s ease, color 0.2s ease;
z-index: 10;
&:hover {
background: #e5e5e5;
color: #000;
background: rgba(7, 192, 95, 0.1);
color: #07C05F;
}
}
// 标题区域
.modal-header {
padding: 24px 32px 20px;
border-bottom: 1px solid #e5e5e5;
padding: 28px 32px 20px;
border-bottom: 1px solid #f0f0f0;
flex-shrink: 0;
background: linear-gradient(to bottom, #ffffff 0%, #fafafa 100%);
}
.modal-title {
margin: 0 0 8px 0;
font-family: "PingFang SC";
font-size: 18px;
font-size: 20px;
font-weight: 600;
color: #000000e6;
color: #1a1a1a;
letter-spacing: -0.3px;
}
.modal-desc {
margin: 0;
font-family: "PingFang SC";
font-size: 14px;
color: #00000066;
color: #666666;
line-height: 22px;
}
@@ -855,7 +857,8 @@ const handleCancel = () => {
.modal-body {
flex: 1;
overflow-y: auto;
padding: 24px 32px;
padding: 28px 32px;
background: #ffffff;
// 自定义滚动条
&::-webkit-scrollbar {
@@ -870,9 +873,10 @@ const handleCancel = () => {
&::-webkit-scrollbar-thumb {
background: #d0d0d0;
border-radius: 3px;
transition: background 0.2s;
&:hover {
background: #b0b0b0;
background: #07C05F;
}
}
@@ -894,16 +898,18 @@ const handleCancel = () => {
.form-label {
display: block;
margin-bottom: 8px;
margin-bottom: 10px;
font-family: "PingFang SC";
font-size: 14px;
font-weight: 500;
color: #000000e6;
color: #333333;
letter-spacing: -0.2px;
&.required::after {
content: '*';
color: #FA5151;
margin-left: 4px;
font-weight: 600;
}
}
@@ -914,26 +920,53 @@ const handleCancel = () => {
:deep(.t-input-number) {
width: 100%;
font-size: 14px;
transition: all 0.2s ease;
.t-input__inner,
input {
font-size: 14px;
border-radius: 6px;
transition: all 0.2s ease;
}
&:hover .t-input__inner,
&:hover input {
border-color: #07C05F;
}
&.t-is-focused .t-input__inner,
&.t-is-focused input {
border-color: #07C05F;
box-shadow: 0 0 0 2px rgba(7, 192, 95, 0.1);
}
}
// 单选按钮组
:deep(.t-radio-group) {
display: flex;
gap: 16px;
gap: 20px;
.t-radio {
margin-right: 0;
font-size: 14px;
transition: all 0.2s ease;
&:hover {
.t-radio__label {
color: #07C05F;
}
}
}
.t-radio__label {
font-size: 14px;
color: #000000e6;
color: #333333;
transition: color 0.2s ease;
}
.t-radio__input:checked + .t-radio__label {
color: #07C05F;
font-weight: 500;
}
}
@@ -949,12 +982,37 @@ const handleCancel = () => {
// 底部按钮区域
.modal-footer {
padding: 16px 32px;
border-top: 1px solid #e5e5e5;
padding: 20px 32px;
border-top: 1px solid #f0f0f0;
display: flex;
justify-content: flex-end;
gap: 12px;
flex-shrink: 0;
background: #fafafa;
:deep(.t-button) {
min-width: 88px;
height: 36px;
font-weight: 500;
border-radius: 6px;
&.t-button--theme-primary {
background: #07C05F;
border-color: #07C05F;
&:hover {
background: #06b04d;
border-color: #06b04d;
}
}
&.t-button--variant-outline {
&:hover {
border-color: #07C05F;
color: #07C05F;
}
}
}
}
// 过渡动画
@@ -19,6 +19,7 @@
<div class="model-option">
<t-icon name="check-circle-filled" class="model-icon" />
<span class="model-name">{{ model.name }}</span>
<t-tag v-if="model.is_builtin" size="small" theme="primary">内置</t-tag>
<t-tag v-if="model.is_default" size="small" theme="success">{{ $t('model.defaultTag') }}</t-tag>
</div>
</t-option>
@@ -535,6 +535,16 @@ watch(() => props.visible, async (newVal) => {
}, 300)
}
})
// 监听全局设置弹窗关闭后刷新模型列表
watch(
() => uiStore.showSettingsModal,
async (visible, previous) => {
if (!visible && previous && props.visible) {
await loadAllModels()
}
}
)
</script>
<style scoped lang="less">
+171 -13
View File
@@ -3,6 +3,25 @@
<div class="section-header">
<h2>{{ $t('modelSettings.title') }}</h2>
<p class="section-description">{{ $t('modelSettings.description') }}</p>
<!-- 内置模型说明 -->
<div class="builtin-models-info">
<div class="info-box">
<div class="info-header">
<t-icon name="info-circle" class="info-icon" />
<span class="info-title">内置模型</span>
</div>
<div class="info-content">
<p>内置模型对所有租户可见,敏感信息会被隐藏,且不可编辑或删除。</p>
<p class="doc-link">
<t-icon name="link" class="link-icon" />
<a href="https://github.com/Tencent/WeKnora/blob/main/docs/BUILTIN_MODELS.md" target="_blank" rel="noopener noreferrer">
查看内置模型管理指南
</a>
</p>
</div>
</div>
</div>
</div>
<!-- 对话模型 -->
@@ -12,19 +31,20 @@
<h3>{{ $t('modelSettings.chat.title') }}</h3>
<p>{{ $t('modelSettings.chat.desc') }}</p>
</div>
<t-button size="small" theme="primary" @click="openAddDialog('chat')">
<t-button size="small" theme="primary" @click="openAddDialog('chat')" class="add-model-btn">
<template #icon>
<t-icon name="add" />
<t-icon name="add" class="add-icon" />
</template>
{{ $t('modelSettings.actions.addModel') }}
</t-button>
</div>
<div v-if="chatModels.length > 0" class="model-list-container">
<div v-for="model in chatModels" :key="model.id" class="model-card">
<div v-for="model in chatModels" :key="model.id" class="model-card" :class="{ 'builtin-model': model.isBuiltin }">
<div class="model-info">
<div class="model-name">
{{ model.name }}
<t-tag v-if="model.isBuiltin" theme="primary" size="small">内置</t-tag>
<t-tag v-if="model.isDefault" theme="success" size="small">{{ $t('common.default') }}</t-tag>
</div>
<div class="model-meta">
@@ -61,19 +81,20 @@
<h3>{{ $t('modelSettings.embedding.title') }}</h3>
<p>{{ $t('modelSettings.embedding.desc') }}</p>
</div>
<t-button size="small" theme="primary" @click="openAddDialog('embedding')">
<t-button size="small" theme="primary" @click="openAddDialog('embedding')" class="add-model-btn">
<template #icon>
<t-icon name="add" />
<t-icon name="add" class="add-icon" />
</template>
{{ $t('modelSettings.actions.addModel') }}
</t-button>
</div>
<div v-if="embeddingModels.length > 0" class="model-list-container">
<div v-for="model in embeddingModels" :key="model.id" class="model-card">
<div v-for="model in embeddingModels" :key="model.id" class="model-card" :class="{ 'builtin-model': model.isBuiltin }">
<div class="model-info">
<div class="model-name">
{{ model.name }}
<t-tag v-if="model.isBuiltin" theme="primary" size="small">内置</t-tag>
<t-tag v-if="model.isDefault" theme="success" size="small">{{ $t('common.default') }}</t-tag>
</div>
<div class="model-meta">
@@ -111,19 +132,20 @@
<h3>{{ $t('modelSettings.rerank.title') }}</h3>
<p>{{ $t('modelSettings.rerank.desc') }}</p>
</div>
<t-button size="small" theme="primary" @click="openAddDialog('rerank')">
<t-button size="small" theme="primary" @click="openAddDialog('rerank')" class="add-model-btn">
<template #icon>
<t-icon name="add" />
<t-icon name="add" class="add-icon" />
</template>
{{ $t('modelSettings.actions.addModel') }}
</t-button>
</div>
<div v-if="rerankModels.length > 0" class="model-list-container">
<div v-for="model in rerankModels" :key="model.id" class="model-card">
<div v-for="model in rerankModels" :key="model.id" class="model-card" :class="{ 'builtin-model': model.isBuiltin }">
<div class="model-info">
<div class="model-name">
{{ model.name }}
<t-tag v-if="model.isBuiltin" theme="primary" size="small">内置</t-tag>
<t-tag v-if="model.isDefault" theme="success" size="small">{{ $t('common.default') }}</t-tag>
</div>
<div class="model-meta">
@@ -160,19 +182,20 @@
<h3>{{ $t('modelSettings.vllm.title') }}</h3>
<p>{{ $t('modelSettings.vllm.desc') }}</p>
</div>
<t-button size="small" theme="primary" @click="openAddDialog('vllm')">
<t-button size="small" theme="primary" @click="openAddDialog('vllm')" class="add-model-btn">
<template #icon>
<t-icon name="add" />
<t-icon name="add" class="add-icon" />
</template>
{{ $t('modelSettings.actions.addModel') }}
</t-button>
</div>
<div v-if="vllmModels.length > 0" class="model-list-container">
<div v-for="model in vllmModels" :key="model.id" class="model-card">
<div v-for="model in vllmModels" :key="model.id" class="model-card" :class="{ 'builtin-model': model.isBuiltin }">
<div class="model-info">
<div class="model-name">
{{ model.name }}
<t-tag v-if="model.isBuiltin" theme="primary" size="small">内置</t-tag>
<t-tag v-if="model.isDefault" theme="success" size="small">{{ $t('common.default') }}</t-tag>
</div>
<div class="model-meta">
@@ -272,7 +295,8 @@ function convertToLegacyFormat(model: ModelConfig) {
baseUrl: model.parameters.base_url || '',
apiKey: model.parameters.api_key || '',
dimension: model.parameters.embedding_parameters?.dimension,
isDefault: model.is_default || false
isDefault: model.is_default || false,
isBuiltin: model.is_builtin || false
}
}
@@ -330,6 +354,11 @@ const openAddDialog = (type: 'chat' | 'embedding' | 'rerank' | 'vllm') => {
// 编辑模型
const editModel = (type: 'chat' | 'embedding' | 'rerank' | 'vllm', model: any) => {
// 内置模型不能编辑
if (model.isBuiltin) {
MessagePlugin.warning('内置模型不能编辑')
return
}
currentModelType.value = type
editingModel.value = { ...model }
showDialog.value = true
@@ -412,6 +441,13 @@ const handleModelSave = async (modelData: any) => {
// 删除模型
const deleteModel = async (type: 'chat' | 'embedding' | 'rerank' | 'vllm', modelId: string) => {
// 检查是否是内置模型
const model = allModels.value.find(m => m.id === modelId)
if (model?.is_builtin) {
MessagePlugin.warning('内置模型不能删除')
return
}
try {
await deleteModelAPI(modelId)
MessagePlugin.success(t('modelSettings.toasts.deleted'))
@@ -441,6 +477,18 @@ const setDefault = async (type: 'chat' | 'embedding' | 'rerank' | 'vllm', modelI
const getModelOptions = (type: 'chat' | 'embedding' | 'rerank' | 'vllm', model: any) => {
const options: any[] = []
// 内置模型不能编辑和删除,只能设为默认
if (model.isBuiltin) {
// 如果不是默认模型,显示"设为默认"选项
if (!model.isDefault) {
options.push({
content: t('modelSettings.actions.setDefault'),
value: `set-default-${type}-${model.id}`
})
}
return options
}
// 如果不是默认模型,显示"设为默认"选项
if (!model.isDefault) {
options.push({
@@ -556,7 +604,20 @@ onMounted(() => {
line-height: 1.5;
}
}
}
// 添加模型按钮样式优化
:deep(.add-model-btn) {
display: inline-flex;
align-items: center;
gap: 6px;
font-weight: 500;
.add-icon {
font-size: 16px;
width: 16px;
height: 16px;
}
}
.model-list-container {
@@ -585,6 +646,39 @@ onMounted(() => {
border-color: #07C05F;
background: #ffffff;
}
// 内置模型置灰样式
&.builtin-model {
opacity: 0.7;
background: #f5f5f5;
border-color: #d9d9d9;
cursor: not-allowed;
&:hover {
border-color: #d9d9d9;
background: #f5f5f5;
opacity: 0.8;
}
.model-info {
.model-name {
color: #999999;
}
.model-meta {
color: #999999;
.source-tag {
background: #e5e5e5;
color: #999999;
}
}
}
.model-actions {
opacity: 0.6;
}
}
}
.model-info {
@@ -661,4 +755,68 @@ onMounted(() => {
margin: 0 0 16px 0;
}
}
.builtin-models-info {
margin-top: 20px;
.info-box {
background: #f8f9fa;
border: 1px solid #e5e7eb;
border-left: 3px solid #07C05F;
border-radius: 4px;
padding: 12px 16px;
}
.info-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
.info-icon {
font-size: 16px;
color: #07C05F;
}
.info-title {
font-size: 14px;
font-weight: 500;
color: #333333;
}
}
.info-content {
font-size: 13px;
line-height: 1.6;
color: #666666;
p {
margin: 0 0 8px 0;
&.doc-link {
margin-top: 8px;
display: flex;
align-items: center;
gap: 6px;
.link-icon {
font-size: 14px;
color: #07C05F;
}
a {
color: #07C05F;
text-decoration: none;
font-weight: 500;
transition: color 0.2s;
&:hover {
color: #06b04d;
text-decoration: underline;
}
}
}
}
}
}
</style>
+2 -2
View File
@@ -28,7 +28,7 @@ func (r *modelRepository) Create(ctx context.Context, m *types.Model) error {
func (r *modelRepository) GetByID(ctx context.Context, tenantID uint, id string) (*types.Model, error) {
var m types.Model
if err := r.db.WithContext(ctx).Where("id = ?", id).Where(
"tenant_id = ? or tenant_id = 0", tenantID,
"tenant_id = ? OR is_builtin = true", tenantID,
).First(&m).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
@@ -44,7 +44,7 @@ func (r *modelRepository) List(
) ([]*types.Model, error) {
var models []*types.Model
query := r.db.WithContext(ctx).Where(
"tenant_id = ? or tenant_id = 0", tenantID,
"tenant_id = ? OR is_builtin = true", tenantID,
)
if modelType != "" {
+29 -2
View File
@@ -178,6 +178,20 @@ func (s *modelService) UpdateModel(ctx context.Context, model *types.Model) erro
logger.Info(ctx, "Start updating model")
logger.Infof(ctx, "Updating model ID: %s, name: %s", model.ID, model.Name)
// Check if the model is builtin - builtin models cannot be updated
tenantID := ctx.Value(types.TenantIDContextKey).(uint)
existingModel, err := s.repo.GetByID(ctx, tenantID, model.ID)
if err != nil {
logger.ErrorWithFields(ctx, err, map[string]interface{}{
"model_id": model.ID,
})
return err
}
if existingModel != nil && existingModel.IsBuiltin {
logger.Warnf(ctx, "Attempted to update builtin model: %s", model.ID)
return errors.New("builtin models cannot be updated")
}
// If this model is set as default, unset other default models of the same type
if model.IsDefault {
logger.Infof(ctx, "Model is set as default, clearing other default models of type: %s", model.Type)
@@ -192,7 +206,7 @@ func (s *modelService) UpdateModel(ctx context.Context, model *types.Model) erro
}
// Update model in repository
err := s.repo.Update(ctx, model)
err = s.repo.Update(ctx, model)
if err != nil {
logger.ErrorWithFields(ctx, err, map[string]interface{}{
"model_id": model.ID,
@@ -213,8 +227,21 @@ func (s *modelService) DeleteModel(ctx context.Context, id string) error {
tenantID := ctx.Value(types.TenantIDContextKey).(uint)
logger.Infof(ctx, "Tenant ID: %d", tenantID)
// Check if the model is builtin - builtin models cannot be deleted
existingModel, err := s.repo.GetByID(ctx, tenantID, id)
if err != nil {
logger.ErrorWithFields(ctx, err, map[string]interface{}{
"model_id": id,
})
return err
}
if existingModel != nil && existingModel.IsBuiltin {
logger.Warnf(ctx, "Attempted to delete builtin model: %s", id)
return errors.New("builtin models cannot be deleted")
}
// Delete model from repository
err := s.repo.Delete(ctx, tenantID, id)
err = s.repo.Delete(ctx, tenantID, id)
if err != nil {
logger.ErrorWithFields(ctx, err, map[string]interface{}{
"model_id": id,
+16 -8
View File
@@ -1155,28 +1155,36 @@ func (h *InitializationHandler) buildConfigResponse(ctx context.Context, models
// 按类型分组模型
for _, model := range models {
// Hide sensitive information for builtin models
baseURL := model.Parameters.BaseURL
apiKey := model.Parameters.APIKey
if model.IsBuiltin {
baseURL = ""
apiKey = ""
}
switch model.Type {
case types.ModelTypeKnowledgeQA:
config["llm"] = map[string]interface{}{
"source": string(model.Source),
"modelName": model.Name,
"baseUrl": model.Parameters.BaseURL,
"apiKey": model.Parameters.APIKey,
"baseUrl": baseURL,
"apiKey": apiKey,
}
case types.ModelTypeEmbedding:
config["embedding"] = map[string]interface{}{
"source": string(model.Source),
"modelName": model.Name,
"baseUrl": model.Parameters.BaseURL,
"apiKey": model.Parameters.APIKey,
"baseUrl": baseURL,
"apiKey": apiKey,
"dimension": model.Parameters.EmbeddingParameters.Dimension,
}
case types.ModelTypeRerank:
config["rerank"] = map[string]interface{}{
"enabled": true,
"modelName": model.Name,
"baseUrl": model.Parameters.BaseURL,
"apiKey": model.Parameters.APIKey,
"baseUrl": baseURL,
"apiKey": apiKey,
}
case types.ModelTypeVLLM:
if config["multimodal"] == nil {
@@ -1187,8 +1195,8 @@ func (h *InitializationHandler) buildConfigResponse(ctx context.Context, models
multimodal := config["multimodal"].(map[string]interface{})
multimodal["vlm"] = map[string]interface{}{
"modelName": model.Name,
"baseUrl": model.Parameters.BaseURL,
"apiKey": model.Parameters.APIKey,
"baseUrl": baseURL,
"apiKey": apiKey,
}
}
}
+60 -4
View File
@@ -27,6 +27,37 @@ func NewModelHandler(service interfaces.ModelService) *ModelHandler {
return &ModelHandler{service: service}
}
// hideSensitiveInfo hides sensitive information (APIKey, BaseURL) for builtin models
// Returns a copy of the model with sensitive fields cleared if it's a builtin model
func hideSensitiveInfo(model *types.Model) *types.Model {
if !model.IsBuiltin {
return model
}
// Create a copy with sensitive information hidden
return &types.Model{
ID: model.ID,
TenantID: model.TenantID,
Name: model.Name,
Type: model.Type,
Source: model.Source,
Description: model.Description,
Parameters: types.ModelParameters{
// Hide APIKey and BaseURL for builtin models
BaseURL: "",
APIKey: "",
// Keep other parameters like embedding dimensions
EmbeddingParameters: model.Parameters.EmbeddingParameters,
ParameterSize: model.Parameters.ParameterSize,
},
IsDefault: model.IsDefault,
IsBuiltin: model.IsBuiltin,
Status: model.Status,
CreatedAt: model.CreatedAt,
UpdatedAt: model.UpdatedAt,
}
}
// CreateModelRequest defines the structure for model creation requests
// Contains all fields required to create a new model in the system
type CreateModelRequest struct {
@@ -82,9 +113,13 @@ func (h *ModelHandler) CreateModel(c *gin.Context) {
}
logger.Infof(ctx, "Model created successfully, ID: %s, Name: %s", model.ID, model.Name)
// Hide sensitive information for builtin models (though newly created models are unlikely to be builtin)
responseModel := hideSensitiveInfo(model)
c.JSON(http.StatusCreated, gin.H{
"success": true,
"data": model,
"data": responseModel,
})
}
@@ -119,9 +154,16 @@ func (h *ModelHandler) GetModel(c *gin.Context) {
}
logger.Infof(ctx, "Retrieved model successfully, ID: %s, Name: %s", model.ID, model.Name)
// Hide sensitive information for builtin models
responseModel := hideSensitiveInfo(model)
if model.IsBuiltin {
logger.Infof(ctx, "Builtin model detected, hiding sensitive information for model: %s", model.ID)
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": model,
"data": responseModel,
})
}
@@ -150,9 +192,19 @@ func (h *ModelHandler) ListModels(c *gin.Context) {
}
logger.Infof(ctx, "Retrieved model list successfully, Tenant ID: %d, Total: %d models", tenantID, len(models))
// Hide sensitive information for builtin models in the list
responseModels := make([]*types.Model, len(models))
for i, model := range models {
responseModels[i] = hideSensitiveInfo(model)
if model.IsBuiltin {
logger.Infof(ctx, "Builtin model detected in list, hiding sensitive information for model: %s", model.ID)
}
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": models,
"data": responseModels,
})
}
@@ -224,9 +276,13 @@ func (h *ModelHandler) UpdateModel(c *gin.Context) {
}
logger.Infof(ctx, "Model updated successfully, ID: %s", id)
// Hide sensitive information for builtin models (though builtin models cannot be updated)
responseModel := hideSensitiveInfo(model)
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": model,
"data": responseModel,
})
}
+2
View File
@@ -66,6 +66,8 @@ type Model struct {
Parameters ModelParameters `yaml:"parameters" json:"parameters" gorm:"type:json"`
// Whether the model is the default model
IsDefault bool `yaml:"is_default" json:"is_default"`
// Whether the model is a builtin model (visible to all tenants)
IsBuiltin bool `yaml:"is_builtin" json:"is_builtin" gorm:"default:false"`
// Model status, default: active, possible: downloading, download_failed
Status ModelStatus `yaml:"status" json:"status"`
// Creation time of the model
@@ -0,0 +1,14 @@
-- 07-add-builtin-models.sql (rollback)
-- Remove is_builtin field from models table
BEGIN;
-- Drop index
DROP INDEX IF EXISTS idx_models_is_builtin;
-- Remove is_builtin column
ALTER TABLE models
DROP COLUMN IF EXISTS is_builtin;
COMMIT;
@@ -0,0 +1,14 @@
-- 07-add-builtin-models.sql
-- Add is_builtin field to models table to support builtin models visible to all tenants
BEGIN;
-- Add is_builtin column to models table
ALTER TABLE models
ADD COLUMN IF NOT EXISTS is_builtin BOOLEAN NOT NULL DEFAULT false;
-- Add index for is_builtin field to improve query performance
CREATE INDEX IF NOT EXISTS idx_models_is_builtin ON models(is_builtin);
COMMIT;