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:
ekko
2026-05-06 15:05:44 +08:00
committed by GitHub
parent f338aeea18
commit 479e1feef6
14 changed files with 406 additions and 3 deletions
@@ -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;
+10
View File
@@ -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',
+10
View File
@@ -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',
+10
View File
@@ -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',
+10
View File
@@ -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',
+10
View File
@@ -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: 'ファイルを添付',
+10
View File
@@ -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: '파일 첨부',
+10
View File
@@ -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',
+10
View File
@@ -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: '添加附件',