mirror of
https://github.com/Tencent/WeKnora.git
synced 2026-06-04 13:30:32 +08:00
fix: add model display name
This commit is contained in:
@@ -52,6 +52,7 @@ type Model struct {
|
||||
ID string `json:"id"`
|
||||
TenantID uint `json:"tenant_id"`
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Type ModelType `json:"type"`
|
||||
Source ModelSource `json:"source"`
|
||||
Description string `json:"description"`
|
||||
@@ -64,6 +65,7 @@ type Model struct {
|
||||
// CreateModelRequest model creation request
|
||||
type CreateModelRequest struct {
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Type ModelType `json:"type"`
|
||||
Source ModelSource `json:"source"`
|
||||
Description string `json:"description"`
|
||||
@@ -74,6 +76,7 @@ type CreateModelRequest struct {
|
||||
// UpdateModelRequest model update request
|
||||
type UpdateModelRequest struct {
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Description string `json:"description"`
|
||||
Parameters ModelParameters `json:"parameters"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface ModelConfig {
|
||||
id?: string;
|
||||
tenant_id?: number;
|
||||
name: string;
|
||||
display_name?: string;
|
||||
type: 'KnowledgeQA' | 'Embedding' | 'Rerank' | 'VLLM' | 'ASR';
|
||||
source: 'local' | 'remote';
|
||||
description?: string;
|
||||
@@ -211,4 +212,3 @@ export function getWeKnoraCloudStatus(): Promise<WeKnoraCloudStatusResult> {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -867,7 +867,7 @@ const selectedModel = computed(() => {
|
||||
|
||||
// 模型展示名:本租户列表中有则用名称;若为共享智能体且其 model_id 不在本租户列表中则显示“共享智能体配置的模型”
|
||||
const selectedModelDisplayName = computed(() => {
|
||||
if (selectedModel.value) return selectedModel.value.name;
|
||||
if (selectedModel.value) return modelDisplayName(selectedModel.value);
|
||||
if (!selectedModelId.value) return t('input.notConfigured');
|
||||
const isSharedAgent = !!settingsStore.selectedAgentSourceTenantId;
|
||||
const modelFromAgent = agentModelId.value && agentModelId.value === selectedModelId.value;
|
||||
@@ -875,6 +875,11 @@ const selectedModelDisplayName = computed(() => {
|
||||
return t('input.notConfigured');
|
||||
});
|
||||
|
||||
const modelDisplayName = (model: ModelConfig) => {
|
||||
const displayName = model.display_name?.trim();
|
||||
return displayName || model.name;
|
||||
};
|
||||
|
||||
const updateModelDropdownPosition = () => {
|
||||
const anchor = modelButtonRef.value;
|
||||
if (!anchor) {
|
||||
@@ -2296,7 +2301,8 @@ defineExpose({
|
||||
<div v-for="model in availableModels" :key="model.id" class="model-option"
|
||||
:class="{ selected: model.id === selectedModelId }" @click="handleModelChange(model.id || '')">
|
||||
<div class="model-option-main">
|
||||
<span class="model-option-name">{{ model.name }}</span>
|
||||
<span class="model-option-name">{{ modelDisplayName(model) }}</span>
|
||||
<span v-if="model.display_name" class="model-option-raw-name">{{ model.name }}</span>
|
||||
<span v-if="model.source === 'remote'" class="model-badge-remote">{{ $t('input.remote') }}</span>
|
||||
<span v-else-if="model.parameters?.parameter_size" class="model-badge-local">
|
||||
{{ model.parameters.parameter_size }}
|
||||
@@ -2404,7 +2410,8 @@ const getImgSrc = (url: string) => {
|
||||
display: inline-flex;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 3px;
|
||||
@@ -3198,13 +3205,23 @@ const getImgSrc = (url: string) => {
|
||||
.model-option-name {
|
||||
font-size: 12px;
|
||||
color: var(--td-text-color-primary, #222);
|
||||
flex: 1;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.model-option-raw-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 11px;
|
||||
color: var(--td-text-color-placeholder, #b0b6bd);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.model-option-desc {
|
||||
font-size: 11px;
|
||||
color: var(--td-text-color-secondary, #8b9196);
|
||||
|
||||
@@ -143,6 +143,12 @@
|
||||
:disabled="formData.provider === 'weknoracloud' && wkcCredentialState !== 'configured'" />
|
||||
</div>
|
||||
|
||||
<div class="form-item">
|
||||
<label class="form-label">{{ $t('model.editor.displayNameLabel') }}</label>
|
||||
<t-input v-model="formData.displayName" :placeholder="$t('model.editor.displayNamePlaceholder')" />
|
||||
<p class="form-desc">{{ $t('model.editor.displayNameDesc') }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="formData.provider !== 'weknoracloud'" class="form-item">
|
||||
<label class="form-label required">{{ $t('model.editor.baseUrlLabel') }}</label>
|
||||
<t-input v-model="formData.baseUrl" :placeholder="getBaseUrlPlaceholder()" />
|
||||
@@ -268,6 +274,7 @@ interface ModelFormData {
|
||||
source: 'local' | 'remote'
|
||||
provider?: string // Provider identifier: openai, aliyun, zhipu, generic, etc.
|
||||
modelName: string
|
||||
displayName?: string
|
||||
baseUrl?: string
|
||||
apiKey?: string
|
||||
dimension?: number
|
||||
@@ -556,6 +563,7 @@ const formData = ref<ModelFormData>({
|
||||
source: 'local',
|
||||
provider: 'openai',
|
||||
modelName: '',
|
||||
displayName: '',
|
||||
baseUrl: '',
|
||||
apiKey: '',
|
||||
dimension: undefined,
|
||||
@@ -740,6 +748,7 @@ const resetForm = () => {
|
||||
source: 'local',
|
||||
provider: 'generic',
|
||||
modelName: '',
|
||||
displayName: '',
|
||||
baseUrl: '',
|
||||
apiKey: '',
|
||||
dimension: undefined, // 默认不填,让用户手动输入或通过检测按钮获取
|
||||
|
||||
@@ -14,11 +14,12 @@
|
||||
v-for="model in models"
|
||||
:key="model.id"
|
||||
:value="model.id"
|
||||
:label="model.name"
|
||||
:label="modelDisplayName(model)"
|
||||
>
|
||||
<div class="model-option">
|
||||
<t-icon name="check-circle-filled" class="model-icon" />
|
||||
<span class="model-name">{{ model.name }}</span>
|
||||
<span class="model-name">{{ modelDisplayName(model) }}</span>
|
||||
<span v-if="model.display_name" class="model-raw-name">{{ model.name }}</span>
|
||||
<t-tag v-if="model.is_builtin" size="small" theme="primary">{{ $t('model.builtinTag') }}</t-tag>
|
||||
<t-tag v-if="model.is_default" size="small" theme="success">{{ $t('model.defaultTag') }}</t-tag>
|
||||
</div>
|
||||
@@ -72,6 +73,11 @@ const placeholderText = computed(() => {
|
||||
return props.placeholder || t('model.selectModelPlaceholder')
|
||||
})
|
||||
|
||||
const modelDisplayName = (model: ModelConfig) => {
|
||||
const displayName = model.display_name?.trim()
|
||||
return displayName || model.name
|
||||
}
|
||||
|
||||
// 监听 allModels 变化,自动过滤当前类型的模型
|
||||
watch(() => props.allModels, (newModels) => {
|
||||
if (newModels && Array.isArray(newModels)) {
|
||||
@@ -153,8 +159,22 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.model-name {
|
||||
flex: 1;
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
font-size: 13px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.model-raw-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 12px;
|
||||
color: var(--td-text-color-placeholder);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&.add {
|
||||
@@ -165,4 +185,3 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -2974,6 +2974,9 @@ export default {
|
||||
remoteAsr: 'e.g. whisper-1'
|
||||
},
|
||||
baseUrlLabel: 'Base URL',
|
||||
displayNameLabel: 'Display name (optional)',
|
||||
displayNamePlaceholder: 'e.g. Support QA model',
|
||||
displayNameDesc: 'Used only in the UI. Runtime calls still use the model name above.',
|
||||
baseUrlPlaceholder: 'e.g. https://api.openai.com/v1',
|
||||
baseUrlPlaceholderVllm: 'e.g. http://localhost:11434/v1',
|
||||
baseUrlPlaceholderAsr: 'e.g. https://api.openai.com/v1',
|
||||
@@ -3517,6 +3520,7 @@ export default {
|
||||
remote: 'Remote',
|
||||
openaiCompatible: 'OpenAI-compatible'
|
||||
},
|
||||
rawModelName: 'Model name',
|
||||
chat: {
|
||||
title: 'Chat Models',
|
||||
desc: 'Configure large language models for chatting',
|
||||
@@ -3545,6 +3549,7 @@ export default {
|
||||
toasts: {
|
||||
nameRequired: 'Model name cannot be empty',
|
||||
nameTooLong: 'Model name cannot exceed 100 characters',
|
||||
displayNameTooLong: 'Display name cannot exceed 100 characters',
|
||||
baseUrlRequired: 'Base URL is required for remote APIs',
|
||||
baseUrlInvalid: 'Invalid Base URL, please enter a valid URL',
|
||||
dimensionInvalid: 'Embedding dimension must be between 128 and 4096',
|
||||
|
||||
@@ -2214,6 +2214,9 @@ export default {
|
||||
remoteAsr: "예: whisper-1",
|
||||
},
|
||||
baseUrlLabel: "Base URL",
|
||||
displayNameLabel: "표시 이름 (선택)",
|
||||
displayNamePlaceholder: "예: 고객지원 QA 모델",
|
||||
displayNameDesc: "UI 표시용으로만 사용되며 실제 호출은 위의 모델 이름을 사용합니다.",
|
||||
baseUrlPlaceholder: "예: https://api.openai.com/v1",
|
||||
baseUrlPlaceholderVllm: "예: http://localhost:11434/v1",
|
||||
baseUrlPlaceholderAsr: "예: https://api.openai.com/v1",
|
||||
@@ -3558,6 +3561,7 @@ export default {
|
||||
remote: "Remote",
|
||||
openaiCompatible: "OpenAI 호환",
|
||||
},
|
||||
rawModelName: "모델 이름",
|
||||
chat: {
|
||||
title: "대화 모델",
|
||||
desc: "대화용 대규모 언어 모델 설정",
|
||||
@@ -3586,6 +3590,7 @@ export default {
|
||||
toasts: {
|
||||
nameRequired: "모델 이름은 비워둘 수 없습니다",
|
||||
nameTooLong: "모델 이름은 100자를 초과할 수 없습니다",
|
||||
displayNameTooLong: "표시 이름은 100자를 초과할 수 없습니다",
|
||||
baseUrlRequired: "Remote API 유형은 Base URL이 필수입니다",
|
||||
baseUrlInvalid: "Base URL 형식이 올바르지 않습니다. 유효한 URL을 입력해주세요",
|
||||
dimensionInvalid: "Embedding 모델은 유효한 벡터 차원(128-4096)을 입력해야 합니다",
|
||||
|
||||
@@ -1933,6 +1933,9 @@ export default {
|
||||
remoteAsr: 'например: whisper-1'
|
||||
},
|
||||
baseUrlLabel: 'Base URL',
|
||||
displayNameLabel: 'Отображаемое имя (опционально)',
|
||||
displayNamePlaceholder: 'например: модель поддержки',
|
||||
displayNameDesc: 'Используется только в интерфейсе. Для вызовов по-прежнему используется имя модели выше.',
|
||||
baseUrlPlaceholder: 'например: https://api.openai.com/v1',
|
||||
baseUrlPlaceholderVllm: 'например: http://localhost:11434/v1',
|
||||
baseUrlPlaceholderAsr: 'например: https://api.openai.com/v1',
|
||||
@@ -3223,6 +3226,7 @@ export default {
|
||||
remote: 'Удалённая',
|
||||
openaiCompatible: 'Совместимо с OpenAI'
|
||||
},
|
||||
rawModelName: 'Имя модели',
|
||||
embedding: {
|
||||
title: 'Модели встраивания',
|
||||
desc: 'Модели для векторизации текста',
|
||||
@@ -3246,6 +3250,7 @@ export default {
|
||||
toasts: {
|
||||
nameRequired: 'Название модели не может быть пустым',
|
||||
nameTooLong: 'Название модели не может превышать 100 символов',
|
||||
displayNameTooLong: 'Отображаемое имя не может превышать 100 символов',
|
||||
baseUrlRequired: 'Для удалённых API требуется Base URL',
|
||||
baseUrlInvalid: 'Некорректный Base URL, укажите правильный адрес',
|
||||
dimensionInvalid: 'Размерность встраивания должна быть 128–4096',
|
||||
|
||||
@@ -2193,6 +2193,9 @@ export default {
|
||||
remoteAsr: "例如:whisper-1",
|
||||
},
|
||||
baseUrlLabel: "Base URL",
|
||||
displayNameLabel: "显示名称(可选)",
|
||||
displayNamePlaceholder: "例如:客服问答模型",
|
||||
displayNameDesc: "仅用于界面展示,实际调用仍使用上面的模型名称。",
|
||||
baseUrlPlaceholder: "例如:https://api.openai.com/v1",
|
||||
baseUrlPlaceholderVllm: "例如:http://localhost:11434/v1",
|
||||
baseUrlPlaceholderAsr: "例如:https://api.openai.com/v1",
|
||||
@@ -3511,6 +3514,7 @@ export default {
|
||||
remote: "Remote",
|
||||
openaiCompatible: "OpenAI兼容",
|
||||
},
|
||||
rawModelName: "模型名称",
|
||||
chat: {
|
||||
title: "对话模型",
|
||||
desc: "配置用于对话的大语言模型",
|
||||
@@ -3539,6 +3543,7 @@ export default {
|
||||
toasts: {
|
||||
nameRequired: "模型名称不能为空",
|
||||
nameTooLong: "模型名称不能超过100个字符",
|
||||
displayNameTooLong: "显示名称不能超过100个字符",
|
||||
baseUrlRequired: "Remote API 类型必须填写 Base URL",
|
||||
baseUrlInvalid: "Base URL 格式不正确,请输入有效的 URL",
|
||||
dimensionInvalid: "Embedding 模型必须填写有效的向量维度(128-4096)",
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
</div>
|
||||
<div class="model-card__body">
|
||||
<div class="model-card__header">
|
||||
<h3 class="model-card__title" :title="model.name">{{ model.name }}</h3>
|
||||
<h3 class="model-card__title" :title="model.name">{{ modelDisplayName(model) }}</h3>
|
||||
<span v-if="model.isBuiltin" class="model-card__pill">
|
||||
{{ $t('modelSettings.builtinTag') }}
|
||||
</span>
|
||||
@@ -85,6 +85,9 @@
|
||||
<span>{{ $t('model.editor.dimensionLabel') }} {{ model.dimension }}</span>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="model.displayName" class="model-card__raw-name" :title="model.name">
|
||||
{{ $t('modelSettings.rawModelName') }}: {{ model.name }}
|
||||
</div>
|
||||
<div v-if="model.baseUrl" class="model-card__url" :title="model.baseUrl">
|
||||
{{ model.baseUrl }}
|
||||
</div>
|
||||
@@ -161,6 +164,7 @@ function convertToLegacyFormat(model: ModelConfig) {
|
||||
return {
|
||||
id: model.id!,
|
||||
name: model.name,
|
||||
displayName: model.display_name || '',
|
||||
source: model.source,
|
||||
modelName: model.name,
|
||||
baseUrl: model.parameters.base_url || '',
|
||||
@@ -228,6 +232,11 @@ const sourceLabel = (type: ModelType) => {
|
||||
return t('modelSettings.source.remote')
|
||||
}
|
||||
|
||||
const modelDisplayName = (model: any) => {
|
||||
const displayName = typeof model.displayName === 'string' ? model.displayName.trim() : ''
|
||||
return displayName || model.name
|
||||
}
|
||||
|
||||
const emptyHint = computed(() => {
|
||||
if (activeTypeFilter.value === 'all') return t('modelSettings.chat.empty')
|
||||
const map: Record<ModelType, string> = {
|
||||
@@ -285,6 +294,11 @@ const handleModelSave = async (modelData: any) => {
|
||||
return
|
||||
}
|
||||
|
||||
if (modelData.displayName && modelData.displayName.trim().length > 100) {
|
||||
MessagePlugin.warning(t('modelSettings.toasts.displayNameTooLong'))
|
||||
return
|
||||
}
|
||||
|
||||
if (modelData.source === 'remote') {
|
||||
if (!modelData.baseUrl || !modelData.baseUrl.trim()) {
|
||||
MessagePlugin.warning(t('modelSettings.toasts.baseUrlRequired'))
|
||||
@@ -326,6 +340,7 @@ const handleModelSave = async (modelData: any) => {
|
||||
|
||||
const apiModelData: ModelConfig = {
|
||||
name: modelData.modelName.trim(),
|
||||
display_name: modelData.displayName?.trim() || '',
|
||||
type: getModelType(currentModelType.value),
|
||||
source: modelData.source,
|
||||
description: '',
|
||||
@@ -460,6 +475,7 @@ const copyModel = async (_type: ModelType, modelId: string) => {
|
||||
try {
|
||||
const newModel: ModelConfig = {
|
||||
name: generateCopyName(source.name),
|
||||
display_name: source.display_name || '',
|
||||
type: source.type,
|
||||
source: source.source,
|
||||
description: source.description || '',
|
||||
@@ -737,6 +753,16 @@ onMounted(() => {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.model-card__raw-name {
|
||||
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
color: var(--td-text-color-placeholder);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.model-card__type {
|
||||
font-weight: 500;
|
||||
color: var(--td-text-color-secondary);
|
||||
|
||||
@@ -15,18 +15,19 @@ import (
|
||||
// stripped along with every other field that could leak how a particular
|
||||
// tenant configured the upstream provider.
|
||||
type ModelResponse struct {
|
||||
ID string `json:"id"`
|
||||
TenantID uint64 `json:"tenant_id"`
|
||||
Name string `json:"name"`
|
||||
Type types.ModelType `json:"type"`
|
||||
Source types.ModelSource `json:"source"`
|
||||
Description string `json:"description"`
|
||||
Parameters ModelParametersDTO `json:"parameters"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
IsBuiltin bool `json:"is_builtin"`
|
||||
Status types.ModelStatus `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID string `json:"id"`
|
||||
TenantID uint64 `json:"tenant_id"`
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Type types.ModelType `json:"type"`
|
||||
Source types.ModelSource `json:"source"`
|
||||
Description string `json:"description"`
|
||||
Parameters ModelParametersDTO `json:"parameters"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
IsBuiltin bool `json:"is_builtin"`
|
||||
Status types.ModelStatus `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
// Per-field "configured?" map. Omitted for builtin models (no
|
||||
// per-tenant credentials). See MCPServiceResponse.Credentials.
|
||||
Credentials map[string]CredentialFieldMetadata `json:"credentials,omitempty"`
|
||||
@@ -44,7 +45,7 @@ type ModelParametersDTO struct {
|
||||
Provider string `json:"provider"`
|
||||
ExtraConfig map[string]string `json:"extra_config,omitempty"`
|
||||
CustomHeaders map[string]string `json:"custom_headers,omitempty"`
|
||||
SupportsVision bool `json:"supports_vision"`
|
||||
SupportsVision bool `json:"supports_vision"`
|
||||
AppID string `json:"app_id,omitempty"`
|
||||
}
|
||||
|
||||
@@ -88,6 +89,7 @@ func NewModelResponse(m *types.Model) *ModelResponse {
|
||||
ID: m.ID,
|
||||
TenantID: m.TenantID,
|
||||
Name: m.Name,
|
||||
DisplayName: m.DisplayName,
|
||||
Type: m.Type,
|
||||
Source: m.Source,
|
||||
Description: m.Description,
|
||||
|
||||
@@ -11,8 +11,9 @@ import (
|
||||
|
||||
func TestModelResponse_OmitsSecrets(t *testing.T) {
|
||||
m := &types.Model{
|
||||
ID: "m-1",
|
||||
Name: "gpt-x",
|
||||
ID: "m-1",
|
||||
Name: "gpt-x",
|
||||
DisplayName: "Support QA",
|
||||
Parameters: types.ModelParameters{
|
||||
APIKey: "sk-real-api-key-do-not-leak",
|
||||
AppSecret: "app-real-secret-do-not-leak",
|
||||
@@ -39,6 +40,7 @@ func TestModelResponse_OmitsSecrets(t *testing.T) {
|
||||
// Non-secret fields pass through.
|
||||
assert.Contains(t, s, "appid-public-ok-to-show")
|
||||
assert.Contains(t, s, "api.example.com")
|
||||
assert.Contains(t, s, `"display_name":"Support QA"`)
|
||||
}
|
||||
|
||||
func TestModelResponse_BuiltinStripsTenantConfig(t *testing.T) {
|
||||
|
||||
@@ -38,6 +38,7 @@ func NewModelHandler(service interfaces.ModelService) *ModelHandler {
|
||||
// Contains all fields required to create a new model in the system
|
||||
type CreateModelRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Type types.ModelType `json:"type" binding:"required"`
|
||||
Source types.ModelSource `json:"source" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
@@ -89,6 +90,7 @@ func (h *ModelHandler) CreateModel(c *gin.Context) {
|
||||
model := &types.Model{
|
||||
TenantID: tenantID,
|
||||
Name: secutils.SanitizeForLog(req.Name),
|
||||
DisplayName: secutils.SanitizeForLog(req.DisplayName),
|
||||
Type: types.ModelType(secutils.SanitizeForLog(string(req.Type))),
|
||||
Source: req.Source,
|
||||
Description: secutils.SanitizeForLog(req.Description),
|
||||
@@ -201,6 +203,7 @@ func (h *ModelHandler) ListModels(c *gin.Context) {
|
||||
// Contains fields that can be updated for an existing model
|
||||
type UpdateModelRequest struct {
|
||||
Name string `json:"name"`
|
||||
DisplayName *string `json:"display_name"`
|
||||
Description string `json:"description"`
|
||||
Parameters types.ModelParameters `json:"parameters"`
|
||||
Source types.ModelSource `json:"source"`
|
||||
@@ -256,6 +259,9 @@ func (h *ModelHandler) UpdateModel(c *gin.Context) {
|
||||
if req.Name != "" {
|
||||
model.Name = req.Name
|
||||
}
|
||||
if req.DisplayName != nil {
|
||||
model.DisplayName = secutils.SanitizeForLog(*req.DisplayName)
|
||||
}
|
||||
model.Description = req.Description
|
||||
|
||||
// SSRF validation for updated model BaseURL
|
||||
|
||||
20
internal/handler/model_test.go
Normal file
20
internal/handler/model_test.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestModelUpdateRequestDisplayNamePresence(t *testing.T) {
|
||||
var omitted UpdateModelRequest
|
||||
require.NoError(t, json.Unmarshal([]byte(`{"name":"gpt-4o"}`), &omitted))
|
||||
assert.Nil(t, omitted.DisplayName)
|
||||
|
||||
var cleared UpdateModelRequest
|
||||
require.NoError(t, json.Unmarshal([]byte(`{"display_name":""}`), &cleared))
|
||||
require.NotNil(t, cleared.DisplayName)
|
||||
assert.Equal(t, "", *cleared.DisplayName)
|
||||
}
|
||||
@@ -35,22 +35,22 @@ const (
|
||||
type ModelSource string
|
||||
|
||||
const (
|
||||
ModelSourceLocal ModelSource = "local" // Local model
|
||||
ModelSourceRemote ModelSource = "remote" // Remote model
|
||||
ModelSourceAliyun ModelSource = "aliyun" // Aliyun DashScope model
|
||||
ModelSourceZhipu ModelSource = "zhipu" // Zhipu model
|
||||
ModelSourceVolcengine ModelSource = "volcengine" // Volcengine model
|
||||
ModelSourceDeepseek ModelSource = "deepseek" // Deepseek model
|
||||
ModelSourceHunyuan ModelSource = "hunyuan" // Hunyuan model
|
||||
ModelSourceMinimax ModelSource = "minimax" // Minimax mode
|
||||
ModelSourceOpenAI ModelSource = "openai" // OpenAI model
|
||||
ModelSourceGemini ModelSource = "gemini" // Gemini model
|
||||
ModelSourceMimo ModelSource = "mimo" // Mimo model
|
||||
ModelSourceSiliconFlow ModelSource = "siliconflow" // SiliconFlow model
|
||||
ModelSourceJina ModelSource = "jina" // Jina AI model
|
||||
ModelSourceOpenRouter ModelSource = "openrouter" // OpenRouter model
|
||||
ModelSourceNvidia ModelSource = "nvidia" // NVIDIA model
|
||||
ModelSourceNovita ModelSource = "novita" // Novita AI model
|
||||
ModelSourceLocal ModelSource = "local" // Local model
|
||||
ModelSourceRemote ModelSource = "remote" // Remote model
|
||||
ModelSourceAliyun ModelSource = "aliyun" // Aliyun DashScope model
|
||||
ModelSourceZhipu ModelSource = "zhipu" // Zhipu model
|
||||
ModelSourceVolcengine ModelSource = "volcengine" // Volcengine model
|
||||
ModelSourceDeepseek ModelSource = "deepseek" // Deepseek model
|
||||
ModelSourceHunyuan ModelSource = "hunyuan" // Hunyuan model
|
||||
ModelSourceMinimax ModelSource = "minimax" // Minimax mode
|
||||
ModelSourceOpenAI ModelSource = "openai" // OpenAI model
|
||||
ModelSourceGemini ModelSource = "gemini" // Gemini model
|
||||
ModelSourceMimo ModelSource = "mimo" // Mimo model
|
||||
ModelSourceSiliconFlow ModelSource = "siliconflow" // SiliconFlow model
|
||||
ModelSourceJina ModelSource = "jina" // Jina AI model
|
||||
ModelSourceOpenRouter ModelSource = "openrouter" // OpenRouter model
|
||||
ModelSourceNvidia ModelSource = "nvidia" // NVIDIA model
|
||||
ModelSourceNovita ModelSource = "novita" // Novita AI model
|
||||
ModelSourceAzureOpenAI ModelSource = "azure_openai" // Azure OpenAI model
|
||||
)
|
||||
|
||||
@@ -65,9 +65,9 @@ type ModelParameters struct {
|
||||
APIKey string `yaml:"api_key" json:"api_key"`
|
||||
InterfaceType string `yaml:"interface_type" json:"interface_type"`
|
||||
EmbeddingParameters EmbeddingParameters `yaml:"embedding_parameters" json:"embedding_parameters"`
|
||||
ParameterSize string `yaml:"parameter_size" json:"parameter_size"` // Ollama model parameter size (e.g., "7B", "13B", "70B")
|
||||
Provider string `yaml:"provider" json:"provider"` // Provider identifier: openai, aliyun, zhipu, generic
|
||||
ExtraConfig map[string]string `yaml:"extra_config" json:"extra_config"` // Provider-specific configuration
|
||||
ParameterSize string `yaml:"parameter_size" json:"parameter_size"` // Ollama model parameter size (e.g., "7B", "13B", "70B")
|
||||
Provider string `yaml:"provider" json:"provider"` // Provider identifier: openai, aliyun, zhipu, generic
|
||||
ExtraConfig map[string]string `yaml:"extra_config" json:"extra_config"` // Provider-specific configuration
|
||||
// CustomHeaders 允许在调用远程模型 API 时附加自定义 HTTP 请求头,
|
||||
// 用途类似 Python OpenAI SDK 的 extra_headers 参数,
|
||||
// 常见场景包括透传企业网关鉴权信息、追踪 ID、路由标识等。
|
||||
@@ -111,6 +111,8 @@ type Model struct {
|
||||
TenantID uint64 `yaml:"tenant_id" json:"tenant_id"`
|
||||
// Name of the model
|
||||
Name string `yaml:"name" json:"name"`
|
||||
// Optional user-facing display name. Runtime calls still use Name.
|
||||
DisplayName string `yaml:"display_name" json:"display_name" gorm:"type:varchar(255);default:''"`
|
||||
// Type of the model
|
||||
Type ModelType `yaml:"type" json:"type"`
|
||||
// Source of the model
|
||||
|
||||
@@ -26,6 +26,7 @@ CREATE TABLE models (
|
||||
id VARCHAR(64) PRIMARY KEY,
|
||||
tenant_id INT NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
display_name VARCHAR(255) NOT NULL DEFAULT '',
|
||||
type VARCHAR(50) NOT NULL,
|
||||
source VARCHAR(50) NOT NULL,
|
||||
description TEXT,
|
||||
|
||||
@@ -36,6 +36,7 @@ CREATE TABLE IF NOT EXISTS models (
|
||||
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
tenant_id INTEGER NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
display_name VARCHAR(255) NOT NULL DEFAULT '',
|
||||
type VARCHAR(50) NOT NULL,
|
||||
source VARCHAR(50) NOT NULL,
|
||||
description TEXT,
|
||||
@@ -212,4 +213,4 @@ WITH (
|
||||
}'
|
||||
);
|
||||
CREATE INDEX ON embeddings USING hnsw ((embedding::halfvec(3584)) halfvec_cosine_ops) WITH (m = 16, ef_construction = 64) WHERE (dimension = 3584);
|
||||
CREATE INDEX ON embeddings USING hnsw ((embedding::halfvec(798)) halfvec_cosine_ops) WITH (m = 16, ef_construction = 64) WHERE (dimension = 798);
|
||||
CREATE INDEX ON embeddings USING hnsw ((embedding::halfvec(798)) halfvec_cosine_ops) WITH (m = 16, ef_construction = 64) WHERE (dimension = 798);
|
||||
|
||||
@@ -31,6 +31,7 @@ CREATE TABLE IF NOT EXISTS models (
|
||||
id VARCHAR(64) PRIMARY KEY,
|
||||
tenant_id INTEGER NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
display_name VARCHAR(255) NOT NULL DEFAULT '',
|
||||
type VARCHAR(50) NOT NULL,
|
||||
source VARCHAR(50) NOT NULL,
|
||||
description TEXT,
|
||||
|
||||
@@ -52,6 +52,7 @@ CREATE TABLE IF NOT EXISTS models (
|
||||
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
tenant_id INTEGER NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
display_name VARCHAR(255) NOT NULL DEFAULT '',
|
||||
type VARCHAR(50) NOT NULL,
|
||||
source VARCHAR(50) NOT NULL,
|
||||
description TEXT,
|
||||
@@ -201,4 +202,4 @@ CREATE INDEX IF NOT EXISTS idx_chunks_tenant_kg ON chunks(tenant_id, knowledge_i
|
||||
CREATE INDEX IF NOT EXISTS idx_chunks_parent_id ON chunks(parent_chunk_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_chunks_chunk_type ON chunks(chunk_type);
|
||||
|
||||
DO $$ BEGIN RAISE NOTICE '[Migration 000000] Initial database setup completed successfully!'; END $$;
|
||||
DO $$ BEGIN RAISE NOTICE '[Migration 000000] Initial database setup completed successfully!'; END $$;
|
||||
|
||||
1
migrations/versioned/000056_models_display_name.down.sql
Normal file
1
migrations/versioned/000056_models_display_name.down.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE models DROP COLUMN IF EXISTS display_name;
|
||||
10
migrations/versioned/000056_models_display_name.up.sql
Normal file
10
migrations/versioned/000056_models_display_name.up.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
-- Migration: 000056_models_display_name
|
||||
-- Add an optional user-facing display name for model rows. Runtime model
|
||||
-- calls continue to use models.name; this field is presentation-only.
|
||||
|
||||
DO $$ BEGIN RAISE NOTICE '[Migration 000056] Adding models.display_name column'; END $$;
|
||||
|
||||
ALTER TABLE models
|
||||
ADD COLUMN IF NOT EXISTS display_name VARCHAR(255) NOT NULL DEFAULT '';
|
||||
|
||||
DO $$ BEGIN RAISE NOTICE '[Migration 000056] Done'; END $$;
|
||||
Reference in New Issue
Block a user