feat: Add database table for model context length configuration (#477)
* feat: add database table for model context length configuration - Add model_context table with provider/model/context_limit fields - Implement UPSERT endpoint for model context configuration - Add priority lookup: database > config.yaml > custom_providers > cache - Add frontend click-to-edit UI in ChatInput with tooltip - Add i18n support for context editing dialog (all 8 locales) - Use context_limit field consistently across frontend and backend Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: use useMessage() composable instead of window.$message in ChatInput - Remove incorrect NMessage import (not a component) - Use useMessage() composable from naive-ui - Replace window.$message?.xxx() with message.xxx() - Fixes TypeScript build errors Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
import { request } from '../client'
|
||||
|
||||
export interface ModelContext {
|
||||
id: number
|
||||
provider: string
|
||||
model: string
|
||||
context_limit: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 provider 和 model 查询模型上下文配置
|
||||
*/
|
||||
export async function getModelContext(provider: string, model: string): Promise<ModelContext | null> {
|
||||
try {
|
||||
const res = await request<{ data: ModelContext }>(
|
||||
`/api/hermes/model-context?provider=${encodeURIComponent(provider)}&model=${encodeURIComponent(model)}`
|
||||
)
|
||||
return res.data
|
||||
} catch (err: any) {
|
||||
if (err.status === 404) return null
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置模型上下文配置(UPSERT:存在则更新,不存在则插入)
|
||||
*/
|
||||
export async function setModelContext(
|
||||
provider: string,
|
||||
model: string,
|
||||
contextLimit: number
|
||||
): Promise<ModelContext> {
|
||||
const res = await request<{ success: boolean; data: ModelContext }>(
|
||||
`/api/hermes/model-context/${encodeURIComponent(provider)}/${encodeURIComponent(model)}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ provider, model, context_limit: contextLimit }),
|
||||
}
|
||||
)
|
||||
return res.data
|
||||
}
|
||||
@@ -4,12 +4,14 @@ import { useChatStore } from '@/stores/hermes/chat'
|
||||
import { useAppStore } from '@/stores/hermes/app'
|
||||
import { useProfilesStore } from '@/stores/hermes/profiles'
|
||||
import { fetchContextLength } from '@/api/hermes/sessions'
|
||||
import { NButton, NTooltip, NSwitch } from 'naive-ui'
|
||||
import { setModelContext } from '@/api/hermes/model-context'
|
||||
import { NButton, NTooltip, NSwitch, NModal, NInputNumber, useMessage } from 'naive-ui'
|
||||
import { computed, ref, onMounted, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const chatStore = useChatStore()
|
||||
const { t } = useI18n()
|
||||
const message = useMessage()
|
||||
const inputText = ref('')
|
||||
const textareaRef = ref<HTMLTextAreaElement>()
|
||||
const fileInputRef = ref<HTMLInputElement>()
|
||||
@@ -45,6 +47,44 @@ const canSend = computed(() => inputText.value.trim() || attachments.value.lengt
|
||||
const contextLength = ref(200000)
|
||||
const FALLBACK_CONTEXT = 200000
|
||||
|
||||
// Context length editing
|
||||
const showContextEditModal = ref(false)
|
||||
const editingContextLimit = ref(200000)
|
||||
const isSavingContextLimit = ref(false)
|
||||
|
||||
async function handleEditContextLimit() {
|
||||
editingContextLimit.value = contextLength.value
|
||||
showContextEditModal.value = true
|
||||
}
|
||||
|
||||
async function saveContextLimit() {
|
||||
if (!editingContextLimit.value || editingContextLimit.value <= 0) {
|
||||
message.error(t('chat.contextEditInvalid'))
|
||||
return
|
||||
}
|
||||
|
||||
isSavingContextLimit.value = true
|
||||
try {
|
||||
const appStore = useAppStore()
|
||||
const provider = appStore.selectedProvider || ''
|
||||
const model = appStore.selectedModel || ''
|
||||
|
||||
if (!provider || !model) {
|
||||
message.error(t('chat.contextEditFailed'))
|
||||
return
|
||||
}
|
||||
|
||||
await setModelContext(provider, model, editingContextLimit.value)
|
||||
contextLength.value = editingContextLimit.value
|
||||
showContextEditModal.value = false
|
||||
message.success(t('chat.contextEditSuccess'))
|
||||
} catch (err: any) {
|
||||
message.error(`${t('chat.contextEditFailed')}: ${err.message || ''}`)
|
||||
} finally {
|
||||
isSavingContextLimit.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadContextLength() {
|
||||
try {
|
||||
const profile = useProfilesStore().activeProfileName || undefined
|
||||
@@ -247,7 +287,16 @@ function isImage(type: string): boolean {
|
||||
</div>
|
||||
|
||||
<span v-if="totalTokens > 0" class="context-info" :class="{ 'context-warning': usagePercent > 80 }">
|
||||
{{ formatTokens(totalTokens) }} / {{ formatTokens(contextLength) }} · {{ t('chat.contextRemaining') }} {{ formatTokens(remainingTokens) }}
|
||||
{{ formatTokens(totalTokens) }} /
|
||||
<NTooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<span class="context-limit-editable" @click="handleEditContextLimit">
|
||||
{{ formatTokens(contextLength) }}
|
||||
</span>
|
||||
</template>
|
||||
<span>{{ t('chat.contextClickToEdit') }}</span>
|
||||
</NTooltip>
|
||||
· {{ t('chat.contextRemaining') }} {{ formatTokens(remainingTokens) }}
|
||||
</span>
|
||||
<div v-if="totalTokens > 0" class="context-bar">
|
||||
<div
|
||||
@@ -335,6 +384,47 @@ function isImage(type: string): boolean {
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Context Length Edit Modal -->
|
||||
<NModal
|
||||
v-model:show="showContextEditModal"
|
||||
:title="t('chat.contextEditTitle')"
|
||||
:mask-closable="true"
|
||||
preset="card"
|
||||
style="width: 400px"
|
||||
>
|
||||
<div class="context-edit-content">
|
||||
<p style="margin-bottom: 16px; color: #666;">
|
||||
{{ t('chat.contextEditDesc') }}
|
||||
</p>
|
||||
<NInputNumber
|
||||
v-model:value="editingContextLimit"
|
||||
:min="1000"
|
||||
:max="10000000"
|
||||
:step="1000"
|
||||
:show-button="false"
|
||||
:placeholder="t('chat.contextEditPlaceholder')"
|
||||
style="width: 100%"
|
||||
>
|
||||
<template #suffix>
|
||||
<span style="color: #999;">tokens</span>
|
||||
</template>
|
||||
</NInputNumber>
|
||||
<div style="margin-top: 12px; font-size: 12px; color: #999;">
|
||||
{{ t('chat.contextEditHint') }}
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div style="display: flex; justify-content: flex-end; gap: 8px;">
|
||||
<NButton @click="showContextEditModal = false" :disabled="isSavingContextLimit">
|
||||
{{ t('chat.contextEditCancel') }}
|
||||
</NButton>
|
||||
<NButton type="primary" @click="saveContextLimit" :loading="isSavingContextLimit">
|
||||
{{ t('chat.contextEditSave') }}
|
||||
</NButton>
|
||||
</div>
|
||||
</template>
|
||||
</NModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -383,6 +473,19 @@ function isImage(type: string): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
.context-limit-editable {
|
||||
cursor: pointer;
|
||||
border-bottom: 1px dashed transparent;
|
||||
transition: all 0.2s ease;
|
||||
padding: 0 2px;
|
||||
|
||||
&:hover {
|
||||
border-bottom-color: $text-muted;
|
||||
background: rgba(128, 128, 128, 0.1);
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.context-bar {
|
||||
width: 60px;
|
||||
height: 4px;
|
||||
|
||||
@@ -104,6 +104,16 @@ export default {
|
||||
// Chat
|
||||
chat: {
|
||||
contextRemaining: 'übrig',
|
||||
contextClickToEdit: 'Klicken zum Bearbeiten der Kontextlänge',
|
||||
contextEditTitle: 'Kontextlänge bearbeiten',
|
||||
contextEditDesc: 'Kontextlängenlimit für aktuelles Modell festlegen (in Tokens)',
|
||||
contextEditPlaceholder: 'Kontextlänge eingeben',
|
||||
contextEditHint: 'Häufige Werte: 200k (Claude), 128k (GPT-4), 32k (GPT-3.5)',
|
||||
contextEditSave: 'Speichern',
|
||||
contextEditCancel: 'Abbrechen',
|
||||
contextEditInvalid: 'Bitte geben Sie eine gültige Kontextlänge ein',
|
||||
contextEditSuccess: 'Kontextlänge aktualisiert',
|
||||
contextEditFailed: 'Aktualisierung fehlgeschlagen',
|
||||
emptyState: 'Starten Sie eine Konversation mit Hermes Agent',
|
||||
inputPlaceholder: 'Nachricht eingeben... (Enter zum Senden, Shift+Enter fur neue Zeile)',
|
||||
attachFiles: 'Dateien anhangen',
|
||||
|
||||
@@ -114,6 +114,16 @@ export default {
|
||||
// Chat
|
||||
chat: {
|
||||
contextRemaining: 'remaining',
|
||||
contextClickToEdit: 'Click to edit context length',
|
||||
contextEditTitle: 'Edit Context Length',
|
||||
contextEditDesc: 'Set context length limit for current model (in tokens)',
|
||||
contextEditPlaceholder: 'Enter context length',
|
||||
contextEditHint: 'Common values: 200k (Claude), 128k (GPT-4), 32k (GPT-3.5)',
|
||||
contextEditSave: 'Save',
|
||||
contextEditCancel: 'Cancel',
|
||||
contextEditInvalid: 'Please enter a valid context length',
|
||||
contextEditSuccess: 'Context length updated',
|
||||
contextEditFailed: 'Update failed',
|
||||
emptyState: 'Start a conversation with Hermes Agent',
|
||||
inputPlaceholder: 'Type a message... (Enter to send, Shift+Enter for new line)',
|
||||
attachFiles: 'Attach files',
|
||||
|
||||
@@ -104,6 +104,16 @@ export default {
|
||||
// Chat
|
||||
chat: {
|
||||
contextRemaining: 'restante',
|
||||
contextClickToEdit: 'Haz clic para editar la longitud del contexto',
|
||||
contextEditTitle: 'Editar longitud del contexto',
|
||||
contextEditDesc: 'Establecer el límite de longitud del contexto para el modelo actual (en tokens)',
|
||||
contextEditPlaceholder: 'Ingresa la longitud del contexto',
|
||||
contextEditHint: 'Valores comunes: 200k (Claude), 128k (GPT-4), 32k (GPT-3.5)',
|
||||
contextEditSave: 'Guardar',
|
||||
contextEditCancel: 'Cancelar',
|
||||
contextEditInvalid: 'Por favor ingresa una longitud de contexto válida',
|
||||
contextEditSuccess: 'Longitud del contexto actualizada',
|
||||
contextEditFailed: 'Error en la actualización',
|
||||
emptyState: 'Inicia una conversacion con Hermes Agent',
|
||||
inputPlaceholder: 'Escribe un mensaje... (Enter para enviar, Shift+Enter para nueva linea)',
|
||||
attachFiles: 'Adjuntar archivos',
|
||||
|
||||
@@ -104,6 +104,16 @@ export default {
|
||||
// Chat
|
||||
chat: {
|
||||
contextRemaining: 'restant',
|
||||
contextClickToEdit: 'Cliquez pour modifier la longueur du contexte',
|
||||
contextEditTitle: 'Modifier la longueur du contexte',
|
||||
contextEditDesc: 'Définir la limite de longueur du contexte pour le modèle actuel (en tokens)',
|
||||
contextEditPlaceholder: 'Entrez la longueur du contexte',
|
||||
contextEditHint: 'Valeurs courantes : 200k (Claude), 128k (GPT-4), 32k (GPT-3.5)',
|
||||
contextEditSave: 'Enregistrer',
|
||||
contextEditCancel: 'Annuler',
|
||||
contextEditInvalid: 'Veuillez entrer une longueur de contexte valide',
|
||||
contextEditSuccess: 'Longueur du contexte mise à jour',
|
||||
contextEditFailed: 'Échec de la mise à jour',
|
||||
emptyState: 'Demarrer une conversation avec Hermes Agent',
|
||||
inputPlaceholder: 'Tapez un message... (Entree pour envoyer, Shift+Entree pour un saut de ligne)',
|
||||
attachFiles: 'Joindre des fichiers',
|
||||
|
||||
@@ -104,6 +104,16 @@ export default {
|
||||
// チャット
|
||||
chat: {
|
||||
contextRemaining: '残り',
|
||||
contextClickToEdit: 'クリックしてコンテキスト長を編集',
|
||||
contextEditTitle: 'コンテキスト長を編集',
|
||||
contextEditDesc: '現在のモデルのコンテキスト長制限を設定(トークン数)',
|
||||
contextEditPlaceholder: 'コンテキスト長を入力',
|
||||
contextEditHint: '一般的な値:200k (Claude), 128k (GPT-4), 32k (GPT-3.5)',
|
||||
contextEditSave: '保存',
|
||||
contextEditCancel: 'キャンセル',
|
||||
contextEditInvalid: '有効なコンテキスト長を入力してください',
|
||||
contextEditSuccess: 'コンテキスト長を更新しました',
|
||||
contextEditFailed: '更新に失敗しました',
|
||||
emptyState: 'Hermes Agent と会話を開始しましょう',
|
||||
inputPlaceholder: 'メッセージを入力... (Enter で送信、Shift+Enter で改行)',
|
||||
attachFiles: 'ファイルを添付',
|
||||
|
||||
@@ -104,6 +104,16 @@ export default {
|
||||
// 채팅
|
||||
chat: {
|
||||
contextRemaining: '남음',
|
||||
contextClickToEdit: '클릭하여 컨텍스트 길이 편집',
|
||||
contextEditTitle: '컨텍스트 길이 편집',
|
||||
contextEditDesc: '현재 모델의 컨텍스트 길이 제한 설정 (토큰 수)',
|
||||
contextEditPlaceholder: '컨텍스트 길이 입력',
|
||||
contextEditHint: '일반적인 값: 200k (Claude), 128k (GPT-4), 32k (GPT-3.5)',
|
||||
contextEditSave: '저장',
|
||||
contextEditCancel: '취소',
|
||||
contextEditInvalid: '유효한 컨텍스트 길이를 입력하세요',
|
||||
contextEditSuccess: '컨텍스트 길이가 업데이트되었습니다',
|
||||
contextEditFailed: '업데이트 실패',
|
||||
emptyState: 'Hermes Agent와 대화를 시작하세요',
|
||||
inputPlaceholder: '메시지를 입력하세요... (Enter로 전송, Shift+Enter로 줄바꿈)',
|
||||
attachFiles: '파일 첨부',
|
||||
|
||||
@@ -104,6 +104,16 @@ export default {
|
||||
// Chat
|
||||
chat: {
|
||||
contextRemaining: 'restante',
|
||||
contextClickToEdit: 'Clique para editar o tamanho do contexto',
|
||||
contextEditTitle: 'Editar tamanho do contexto',
|
||||
contextEditDesc: 'Definir o limite de tamanho do contexto para o modelo atual (em tokens)',
|
||||
contextEditPlaceholder: 'Digite o tamanho do contexto',
|
||||
contextEditHint: 'Valores comuns: 200k (Claude), 128k (GPT-4), 32k (GPT-3.5)',
|
||||
contextEditSave: 'Salvar',
|
||||
contextEditCancel: 'Cancelar',
|
||||
contextEditInvalid: 'Por favor, insira um tamanho de contexto válido',
|
||||
contextEditSuccess: 'Tamanho do contexto atualizado',
|
||||
contextEditFailed: 'Falha na atualização',
|
||||
emptyState: 'Inicie uma conversa com o Hermes Agent',
|
||||
inputPlaceholder: 'Digite uma mensagem... (Enter para enviar, Shift+Enter para nova linha)',
|
||||
attachFiles: 'Anexar arquivos',
|
||||
|
||||
@@ -114,6 +114,16 @@ export default {
|
||||
// 对话
|
||||
chat: {
|
||||
contextRemaining: '剩余',
|
||||
contextClickToEdit: '点击编辑上下文长度',
|
||||
contextEditTitle: '编辑上下文长度',
|
||||
contextEditDesc: '设置当前模型的上下文长度限制(token 数量)',
|
||||
contextEditPlaceholder: '请输入上下文长度',
|
||||
contextEditHint: '常见值:200k (Claude), 128k (GPT-4), 32k (GPT-3.5)',
|
||||
contextEditSave: '保存',
|
||||
contextEditCancel: '取消',
|
||||
contextEditInvalid: '请输入有效的上下文长度',
|
||||
contextEditSuccess: '上下文长度已更新',
|
||||
contextEditFailed: '更新失败',
|
||||
emptyState: '开始与 Hermes Agent 对话',
|
||||
inputPlaceholder: '输入消息... (Enter 发送,Shift+Enter 换行)',
|
||||
attachFiles: '添加附件',
|
||||
|
||||
Reference in New Issue
Block a user