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 @@