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