diff --git a/client/model.go b/client/model.go index a87a8d01..0f7617fc 100644 --- a/client/model.go +++ b/client/model.go @@ -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"` diff --git a/frontend/src/api/model/index.ts b/frontend/src/api/model/index.ts index 1102adc2..9f68291d 100644 --- a/frontend/src/api/model/index.ts +++ b/frontend/src/api/model/index.ts @@ -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 { }) }) } - diff --git a/frontend/src/components/Input-field.vue b/frontend/src/components/Input-field.vue index fd6b5f47..6865bff5 100644 --- a/frontend/src/components/Input-field.vue +++ b/frontend/src/components/Input-field.vue @@ -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({
- {{ model.name }} + {{ modelDisplayName(model) }} + {{ model.name }} {{ $t('input.remote') }} {{ 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); diff --git a/frontend/src/components/ModelEditorDialog.vue b/frontend/src/components/ModelEditorDialog.vue index e5caceaf..1091d01b 100644 --- a/frontend/src/components/ModelEditorDialog.vue +++ b/frontend/src/components/ModelEditorDialog.vue @@ -143,6 +143,12 @@ :disabled="formData.provider === 'weknoracloud' && wkcCredentialState !== 'configured'" />
+
+ + +

{{ $t('model.editor.displayNameDesc') }}

+
+
@@ -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({ 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, // 默认不填,让用户手动输入或通过检测按钮获取 diff --git a/frontend/src/components/ModelSelector.vue b/frontend/src/components/ModelSelector.vue index cff6f1bc..aff8b61d 100644 --- a/frontend/src/components/ModelSelector.vue +++ b/frontend/src/components/ModelSelector.vue @@ -14,11 +14,12 @@ v-for="model in models" :key="model.id" :value="model.id" - :label="model.name" + :label="modelDisplayName(model)" >
- {{ model.name }} + {{ modelDisplayName(model) }} + {{ model.name }} {{ $t('model.builtinTag') }} {{ $t('model.defaultTag') }}
@@ -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(() => { } } - diff --git a/frontend/src/i18n/locales/en-US.ts b/frontend/src/i18n/locales/en-US.ts index 1189f12f..a15553a5 100755 --- a/frontend/src/i18n/locales/en-US.ts +++ b/frontend/src/i18n/locales/en-US.ts @@ -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', diff --git a/frontend/src/i18n/locales/ko-KR.ts b/frontend/src/i18n/locales/ko-KR.ts index f55794c3..d763e03c 100755 --- a/frontend/src/i18n/locales/ko-KR.ts +++ b/frontend/src/i18n/locales/ko-KR.ts @@ -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)을 입력해야 합니다", diff --git a/frontend/src/i18n/locales/ru-RU.ts b/frontend/src/i18n/locales/ru-RU.ts index 099e76b7..d8828968 100755 --- a/frontend/src/i18n/locales/ru-RU.ts +++ b/frontend/src/i18n/locales/ru-RU.ts @@ -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', diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index 7950c961..b6bb222e 100755 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -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)", diff --git a/frontend/src/views/settings/ModelSettings.vue b/frontend/src/views/settings/ModelSettings.vue index 1964d165..8a51b2f5 100644 --- a/frontend/src/views/settings/ModelSettings.vue +++ b/frontend/src/views/settings/ModelSettings.vue @@ -57,7 +57,7 @@
-

{{ model.name }}

+

{{ modelDisplayName(model) }}

{{ $t('modelSettings.builtinTag') }} @@ -85,6 +85,9 @@ {{ $t('model.editor.dimensionLabel') }} {{ model.dimension }}
+
+ {{ $t('modelSettings.rawModelName') }}: {{ model.name }} +
{{ model.baseUrl }}
@@ -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 = { @@ -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); diff --git a/internal/handler/dto/model.go b/internal/handler/dto/model.go index 99ea9e06..7b121399 100644 --- a/internal/handler/dto/model.go +++ b/internal/handler/dto/model.go @@ -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, diff --git a/internal/handler/dto/model_test.go b/internal/handler/dto/model_test.go index 4dfc7736..c40f0cf6 100644 --- a/internal/handler/dto/model_test.go +++ b/internal/handler/dto/model_test.go @@ -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) { diff --git a/internal/handler/model.go b/internal/handler/model.go index dfebf2d3..854c9914 100644 --- a/internal/handler/model.go +++ b/internal/handler/model.go @@ -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 diff --git a/internal/handler/model_test.go b/internal/handler/model_test.go new file mode 100644 index 00000000..7abed79a --- /dev/null +++ b/internal/handler/model_test.go @@ -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) +} diff --git a/internal/types/model.go b/internal/types/model.go index b8c08581..b0cdac21 100644 --- a/internal/types/model.go +++ b/internal/types/model.go @@ -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 diff --git a/migrations/mysql/00-init-db.sql b/migrations/mysql/00-init-db.sql index c7c843d8..319f6102 100644 --- a/migrations/mysql/00-init-db.sql +++ b/migrations/mysql/00-init-db.sql @@ -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, diff --git a/migrations/paradedb/00-init-db.sql b/migrations/paradedb/00-init-db.sql index 2d4771ad..6bc78389 100644 --- a/migrations/paradedb/00-init-db.sql +++ b/migrations/paradedb/00-init-db.sql @@ -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); \ No newline at end of file +CREATE INDEX ON embeddings USING hnsw ((embedding::halfvec(798)) halfvec_cosine_ops) WITH (m = 16, ef_construction = 64) WHERE (dimension = 798); diff --git a/migrations/sqlite/000000_init.up.sql b/migrations/sqlite/000000_init.up.sql index 95d90e6a..d95a8194 100644 --- a/migrations/sqlite/000000_init.up.sql +++ b/migrations/sqlite/000000_init.up.sql @@ -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, diff --git a/migrations/versioned/000000_init.up.sql b/migrations/versioned/000000_init.up.sql index 9e82831a..8a5b9988 100644 --- a/migrations/versioned/000000_init.up.sql +++ b/migrations/versioned/000000_init.up.sql @@ -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 $$; \ No newline at end of file +DO $$ BEGIN RAISE NOTICE '[Migration 000000] Initial database setup completed successfully!'; END $$; diff --git a/migrations/versioned/000056_models_display_name.down.sql b/migrations/versioned/000056_models_display_name.down.sql new file mode 100644 index 00000000..41f3b71e --- /dev/null +++ b/migrations/versioned/000056_models_display_name.down.sql @@ -0,0 +1 @@ +ALTER TABLE models DROP COLUMN IF EXISTS display_name; diff --git a/migrations/versioned/000056_models_display_name.up.sql b/migrations/versioned/000056_models_display_name.up.sql new file mode 100644 index 00000000..3fb9ea96 --- /dev/null +++ b/migrations/versioned/000056_models_display_name.up.sql @@ -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 $$;