From 2c44a41f2acbbd23bc378a11dee9a67ae854c46b Mon Sep 17 00:00:00 2001 From: wizardchen Date: Thu, 25 Jun 2026 16:55:17 +0800 Subject: [PATCH] feat(models): add model test debugger for saved model configs Let admins send real requests against configured chat, embedding, rerank, VLM, and ASR models from settings, with thinking controls shown only when the model supports them. --- frontend/src/api/model/index.ts | 43 +- frontend/src/components/ModelDebugDrawer.vue | 697 ++++++++++++++++++ frontend/src/i18n/locales/en-US.ts | 49 ++ frontend/src/i18n/locales/ko-KR.ts | 49 ++ frontend/src/i18n/locales/ru-RU.ts | 51 +- frontend/src/i18n/locales/zh-CN.ts | 49 ++ frontend/src/utils/request.ts | 11 +- frontend/src/utils/thinkingControl.ts | 18 + frontend/src/views/settings/ModelSettings.vue | 52 +- internal/handler/model.go | 323 ++++++++ internal/handler/model_test.go | 52 ++ internal/models/chat/thinking.go | 32 + internal/models/chat/thinking_test.go | 17 + internal/router/router.go | 2 + 14 files changed, 1438 insertions(+), 7 deletions(-) create mode 100644 frontend/src/components/ModelDebugDrawer.vue diff --git a/frontend/src/api/model/index.ts b/frontend/src/api/model/index.ts index f5caf233..9a52b855 100644 --- a/frontend/src/api/model/index.ts +++ b/frontend/src/api/model/index.ts @@ -1,4 +1,4 @@ -import { get, post, put, del } from '../../utils/request'; +import { get, post, postUpload, put, del } from '../../utils/request'; import i18n from '@/i18n' const t = (key: string) => i18n.global.t(key) @@ -142,6 +142,47 @@ export function deleteModel(id: string): Promise { }); } +export interface ModelDebugOptions { + system_prompt?: string + temperature?: number + top_p?: number + max_tokens?: number + thinking?: boolean +} + +export interface ModelDebugResult { + ok: boolean + elapsed_ms: number + request: Record + raw_response: unknown + observations: Record + error?: string +} + +export async function debugModel( + id: string, + data: { + input?: string + documents?: string[] + options?: ModelDebugOptions + file?: File | null + }, +): Promise { + const form = new FormData() + form.append('input', data.input || '') + form.append('documents', JSON.stringify(data.documents || [])) + form.append('options', JSON.stringify(data.options || {})) + if (data.file) form.append('file', data.file) + const response: any = await postUpload( + `/api/v1/models/${id}/debug`, + form, + undefined, + { timeout: 300000 }, + ) + if (response?.success && response?.data) return response.data + throw new Error(response?.message || t('error.model.getFailed')) +} + // ---------------------------------------------------------------------------- // Model credential subresource. See mcp-service.ts for the matching MCP API // shape and the design notes in internal/handler/dto/mcp.go. diff --git a/frontend/src/components/ModelDebugDrawer.vue b/frontend/src/components/ModelDebugDrawer.vue new file mode 100644 index 00000000..844155ba --- /dev/null +++ b/frontend/src/components/ModelDebugDrawer.vue @@ -0,0 +1,697 @@ + + + + + diff --git a/frontend/src/i18n/locales/en-US.ts b/frontend/src/i18n/locales/en-US.ts index 05f7ac83..fdefb20d 100755 --- a/frontend/src/i18n/locales/en-US.ts +++ b/frontend/src/i18n/locales/en-US.ts @@ -4237,6 +4237,7 @@ export default { }, actions: { addModel: 'Add Model', + debugModel: 'Model Test', setDefault: 'Set as Default' }, source: { @@ -4298,6 +4299,54 @@ export default { }, builtinTag: 'Built-in', confirmDelete: 'Delete model "{name}"?', + debug: { + title: 'Model Test', + description: 'Send a real request to a saved model and inspect the response', + groupModel: 'Select model', + groupInput: 'Test input', + groupResult: 'Result', + modelType: 'Model type', + model: 'Model', + modelPlaceholder: 'Select a model to test', + noModelsForType: 'No saved models of this type', + query: 'Input', + queryPlaceholder: 'Enter the text to send to the model', + embeddingInput: 'Text to embed', + embeddingPlaceholder: 'Enter text to generate an embedding', + vlmPrompt: 'Image prompt', + vlmPromptPlaceholder: 'For example: Describe this image in detail', + documents: 'Candidate documents', + documentsPlaceholder: 'Enter one candidate document per line', + documentsHint: 'Each non-empty line is sent as a separate document to the ReRank model', + imageFile: 'Image file', + audioFile: 'Audio file', + chooseFile: 'Choose file', + parameters: 'Request parameters', + thinking: 'Thinking mode', + thinkingDesc: 'Only applies to models that support thinking', + systemPrompt: 'System Prompt', + systemPromptPlaceholder: 'Optional system prompt', + run: 'Run test', + copyResult: 'Copy result', + history: 'History', + thinkOn: 'Thinking on', + thinkOff: 'Thinking off', + runLabel: 'Run #{n}', + success: 'Request succeeded', + failed: 'Request failed', + rawResponse: 'Response', + requestPreview: 'Request preview', + requestFailed: 'Model test request failed', + metrics: { + dimension: 'Dimensions', + resultCount: 'Result count', + answerChars: 'Answer chars', + reasoningChars: 'Reasoning chars', + reasoningReturned: 'Reasoning returned', + textChars: 'Transcript chars', + segmentCount: 'Segment count', + }, + }, }, // New: Ollama Settings ollamaSettings: { diff --git a/frontend/src/i18n/locales/ko-KR.ts b/frontend/src/i18n/locales/ko-KR.ts index f7e201ed..fcdf9f81 100755 --- a/frontend/src/i18n/locales/ko-KR.ts +++ b/frontend/src/i18n/locales/ko-KR.ts @@ -4233,6 +4233,7 @@ export default { }, actions: { addModel: "모델 추가", + debugModel: "모델 테스트", setDefault: "기본값으로 설정", }, source: { @@ -4294,6 +4295,54 @@ export default { }, builtinTag: "기본제공", confirmDelete: '모델 "{name}"을(를) 삭제하시겠습니까?', + debug: { + title: "모델 테스트", + description: "구성된 모델에 실제 요청을 보내고 응답과 소요 시간을 확인합니다", + groupModel: "모델 선택", + groupInput: "테스트 입력", + groupResult: "실행 결과", + modelType: "모델 유형", + model: "모델", + modelPlaceholder: "테스트할 모델을 선택하세요", + noModelsForType: "이 유형의 저장된 모델이 없습니다", + query: "입력 내용", + queryPlaceholder: "모델에 보낼 내용을 입력하세요", + embeddingInput: "벡터화할 텍스트", + embeddingPlaceholder: "Embedding을 생성할 텍스트를 입력하세요", + vlmPrompt: "이미지 프롬프트", + vlmPromptPlaceholder: "예: 이 이미지를 자세히 설명해 주세요", + documents: "후보 문서", + documentsPlaceholder: "후보 문서를 한 줄에 하나씩 입력하세요", + documentsHint: "비어 있지 않은 각 줄은 ReRank 모델에 별도 문서로 전송됩니다", + imageFile: "이미지 파일", + audioFile: "오디오 파일", + chooseFile: "파일 선택", + parameters: "요청 매개변수", + thinking: "사고 모드", + thinkingDesc: "사고 모드를 지원하는 모델에만 적용됩니다", + systemPrompt: "System Prompt", + systemPromptPlaceholder: "선택 사항, 시스템 프롬프트 입력", + run: "테스트 실행", + copyResult: "결과 복사", + history: "기록", + thinkOn: "사고 켜짐", + thinkOff: "사고 꺼짐", + runLabel: "{n}번째 실행", + success: "호출 성공", + failed: "호출 실패", + rawResponse: "응답 내용", + requestPreview: "요청 미리보기", + requestFailed: "모델 테스트 요청 실패", + metrics: { + dimension: "벡터 차원", + resultCount: "결과 수", + answerChars: "답변 문자 수", + reasoningChars: "추론 문자 수", + reasoningReturned: "추론 내용 반환", + textChars: "전사 문자 수", + segmentCount: "세그먼트 수", + }, + }, }, // Ollama 설정 ollamaSettings: { diff --git a/frontend/src/i18n/locales/ru-RU.ts b/frontend/src/i18n/locales/ru-RU.ts index caa5619d..f0926cf0 100755 --- a/frontend/src/i18n/locales/ru-RU.ts +++ b/frontend/src/i18n/locales/ru-RU.ts @@ -3888,6 +3888,7 @@ export default { }, actions: { addModel: 'Добавить модель', + debugModel: 'Тест модели', setDefault: 'Сделать по умолчанию' }, chat: { @@ -3948,7 +3949,55 @@ export default { viewGuide: 'Посмотреть руководство по управлению встроенными моделями' }, builtinTag: 'Встроенная', - confirmDelete: 'Удалить модель «{name}»?' + confirmDelete: 'Удалить модель «{name}»?', + debug: { + title: 'Тест модели', + description: 'Отправьте реальный запрос к настроенной модели и проверьте ответ и время выполнения', + groupModel: 'Выбор модели', + groupInput: 'Тестовый ввод', + groupResult: 'Результат', + modelType: 'Тип модели', + model: 'Модель', + modelPlaceholder: 'Выберите модель для теста', + noModelsForType: 'Нет сохранённых моделей этого типа', + query: 'Ввод', + queryPlaceholder: 'Введите текст для отправки модели', + embeddingInput: 'Текст для векторизации', + embeddingPlaceholder: 'Введите текст для создания embedding', + vlmPrompt: 'Промпт для изображения', + vlmPromptPlaceholder: 'Например: подробно опишите это изображение', + documents: 'Кандидаты документов', + documentsPlaceholder: 'Введите один документ на строку', + documentsHint: 'Каждая непустая строка отправляется в модель ReRank как отдельный документ', + imageFile: 'Файл изображения', + audioFile: 'Аудиофайл', + chooseFile: 'Выбрать файл', + parameters: 'Параметры запроса', + thinking: 'Режим размышления', + thinkingDesc: 'Применяется только к моделям с поддержкой размышления', + systemPrompt: 'System Prompt', + systemPromptPlaceholder: 'Необязательно: системный промпт', + run: 'Запустить тест', + copyResult: 'Копировать результат', + history: 'История', + thinkOn: 'Размышление вкл.', + thinkOff: 'Размышление выкл.', + runLabel: 'Запуск №{n}', + success: 'Запрос выполнен', + failed: 'Запрос не выполнен', + rawResponse: 'Ответ', + requestPreview: 'Просмотр запроса', + requestFailed: 'Не удалось выполнить тест модели', + metrics: { + dimension: 'Размерность вектора', + resultCount: 'Количество результатов', + answerChars: 'Символов в ответе', + reasoningChars: 'Символов рассуждения', + reasoningReturned: 'Рассуждение возвращено', + textChars: 'Символов транскрипции', + segmentCount: 'Количество сегментов', + }, + }, }, ollamaSettings: { title: 'Настройки Ollama', diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index affc406b..2d7fe3d8 100755 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -4237,6 +4237,7 @@ export default { }, actions: { addModel: "添加模型", + debugModel: "模型测试", setDefault: "设为默认", }, source: { @@ -4298,6 +4299,54 @@ export default { }, builtinTag: "内置", confirmDelete: "确定删除模型「{name}」吗?", + debug: { + title: "模型测试", + description: "向已配置的模型发送真实请求,查看响应与耗时", + groupModel: "选择模型", + groupInput: "测试输入", + groupResult: "运行结果", + modelType: "模型类型", + model: "模型", + modelPlaceholder: "请选择要测试的模型", + noModelsForType: "当前类型暂无已添加模型", + query: "输入内容", + queryPlaceholder: "输入要发送给模型的内容", + embeddingInput: "待向量化文本", + embeddingPlaceholder: "输入要生成 Embedding 的文本", + vlmPrompt: "图片提示词", + vlmPromptPlaceholder: "例如:请详细描述这张图片", + documents: "候选文档", + documentsPlaceholder: "每行输入一个候选文档", + documentsHint: "每个非空行会作为一个独立文档发送给 ReRank 模型", + imageFile: "图片文件", + audioFile: "音频文件", + chooseFile: "选择文件", + parameters: "请求参数", + thinking: "思考模式", + thinkingDesc: "仅对支持思考模式的模型生效", + systemPrompt: "System Prompt", + systemPromptPlaceholder: "可选,输入系统提示词", + run: "运行测试", + copyResult: "复制结果", + history: "历史记录", + thinkOn: "思考开启", + thinkOff: "思考关闭", + runLabel: "第 {n} 次运行", + success: "调用成功", + failed: "调用失败", + rawResponse: "响应内容", + requestPreview: "请求预览", + requestFailed: "模型测试请求失败", + metrics: { + dimension: "向量维度", + resultCount: "结果数量", + answerChars: "回答字符数", + reasoningChars: "推理字符数", + reasoningReturned: "返回推理内容", + textChars: "转写字符数", + segmentCount: "分段数量", + }, + }, }, // 新增:Ollama 设置 ollamaSettings: { diff --git a/frontend/src/utils/request.ts b/frontend/src/utils/request.ts index b1f3daac..152e9be9 100644 --- a/frontend/src/utils/request.ts +++ b/frontend/src/utils/request.ts @@ -257,13 +257,20 @@ export async function getDown(url: string) { return res } -export function postUpload(url: string, data = {}, onUploadProgress?: (progressEvent: any) => void) { +export function postUpload( + url: string, + data = {}, + onUploadProgress?: (progressEvent: any) => void, + config: any = {}, +) { return instance.post(url, data, { + ...config, headers: { "Content-Type": "multipart/form-data", "X-Request-ID": `${generateRandomString(12)}`, + ...(config.headers || {}), }, - onUploadProgress, + onUploadProgress: onUploadProgress || config.onUploadProgress, }); } diff --git a/frontend/src/utils/thinkingControl.ts b/frontend/src/utils/thinkingControl.ts index 97723617..122bced7 100644 --- a/frontend/src/utils/thinkingControl.ts +++ b/frontend/src/utils/thinkingControl.ts @@ -69,3 +69,21 @@ export function resolveThinkingControl( } return defaultThinkingControl(provider, modelName) } + +/** Whether the debug drawer should expose Think on/off for this saved model. */ +export function modelSupportsThinking(model: { + type: string + source: string + name: string + parameters: { + provider?: string + extra_config?: { thinking_control?: string } + } +}): boolean { + if (model.type !== 'KnowledgeQA' || model.source !== 'remote') return false + return resolveThinkingControl( + model.parameters.extra_config?.thinking_control, + model.parameters.provider || '', + model.name || '', + ) !== 'none' +} diff --git a/frontend/src/views/settings/ModelSettings.vue b/frontend/src/views/settings/ModelSettings.vue index e6a1342d..0994a0db 100644 --- a/frontend/src/views/settings/ModelSettings.vue +++ b/frontend/src/views/settings/ModelSettings.vue @@ -1,8 +1,24 @@ @@ -121,9 +138,10 @@