diff --git a/packages/client/src/api/hermes/model-context.ts b/packages/client/src/api/hermes/model-context.ts new file mode 100644 index 0000000..cbd9830 --- /dev/null +++ b/packages/client/src/api/hermes/model-context.ts @@ -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 { + 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 { + 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 +} diff --git a/packages/client/src/components/hermes/chat/ChatInput.vue b/packages/client/src/components/hermes/chat/ChatInput.vue index a052fd4..31b8c3d 100644 --- a/packages/client/src/components/hermes/chat/ChatInput.vue +++ b/packages/client/src/components/hermes/chat/ChatInput.vue @@ -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() const fileInputRef = ref() @@ -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 { - {{ formatTokens(totalTokens) }} / {{ formatTokens(contextLength) }} · {{ t('chat.contextRemaining') }} {{ formatTokens(remainingTokens) }} + {{ formatTokens(totalTokens) }} / + + + {{ t('chat.contextClickToEdit') }} + + · {{ t('chat.contextRemaining') }} {{ formatTokens(remainingTokens) }}
+ + + +
+

+ {{ t('chat.contextEditDesc') }} +

+ + + +
+ {{ t('chat.contextEditHint') }} +
+
+ +
@@ -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; diff --git a/packages/client/src/i18n/locales/de.ts b/packages/client/src/i18n/locales/de.ts index d8b531f..8db917c 100644 --- a/packages/client/src/i18n/locales/de.ts +++ b/packages/client/src/i18n/locales/de.ts @@ -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', diff --git a/packages/client/src/i18n/locales/en.ts b/packages/client/src/i18n/locales/en.ts index ec7486e..7eccf35 100644 --- a/packages/client/src/i18n/locales/en.ts +++ b/packages/client/src/i18n/locales/en.ts @@ -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', diff --git a/packages/client/src/i18n/locales/es.ts b/packages/client/src/i18n/locales/es.ts index e157edc..bc08782 100644 --- a/packages/client/src/i18n/locales/es.ts +++ b/packages/client/src/i18n/locales/es.ts @@ -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', diff --git a/packages/client/src/i18n/locales/fr.ts b/packages/client/src/i18n/locales/fr.ts index 72cd44f..2f250e7 100644 --- a/packages/client/src/i18n/locales/fr.ts +++ b/packages/client/src/i18n/locales/fr.ts @@ -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', diff --git a/packages/client/src/i18n/locales/ja.ts b/packages/client/src/i18n/locales/ja.ts index 3358300..8416c8f 100644 --- a/packages/client/src/i18n/locales/ja.ts +++ b/packages/client/src/i18n/locales/ja.ts @@ -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: 'ファイルを添付', diff --git a/packages/client/src/i18n/locales/ko.ts b/packages/client/src/i18n/locales/ko.ts index a043fd4..8d3ab01 100644 --- a/packages/client/src/i18n/locales/ko.ts +++ b/packages/client/src/i18n/locales/ko.ts @@ -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: '파일 첨부', diff --git a/packages/client/src/i18n/locales/pt.ts b/packages/client/src/i18n/locales/pt.ts index dc965ab..2f5e84d 100644 --- a/packages/client/src/i18n/locales/pt.ts +++ b/packages/client/src/i18n/locales/pt.ts @@ -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', diff --git a/packages/client/src/i18n/locales/zh.ts b/packages/client/src/i18n/locales/zh.ts index 1b9f936..6fcb026 100644 --- a/packages/client/src/i18n/locales/zh.ts +++ b/packages/client/src/i18n/locales/zh.ts @@ -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: '添加附件', diff --git a/packages/server/src/controllers/hermes/models.ts b/packages/server/src/controllers/hermes/models.ts index 6b52ea8..38598a0 100644 --- a/packages/server/src/controllers/hermes/models.ts +++ b/packages/server/src/controllers/hermes/models.ts @@ -5,6 +5,8 @@ import { readConfigYaml, writeConfigYaml, fetchProviderModels, buildModelGroups, import { buildProviderModelMap, PROVIDER_PRESETS } from '../../shared/providers' import { getCopilotModelsDetailed, resolveCopilotOAuthToken, type CopilotModelMeta } from '../../services/hermes/copilot-models' import { readAppConfig } from '../../services/app-config' +import { getDb } from '../../db' +import { MODEL_CONTEXT_TABLE } from '../../db/hermes/schemas' const PROVIDER_MODEL_CATALOG = buildProviderModelMap() @@ -236,3 +238,126 @@ export async function setConfigModel(ctx: any) { ctx.body = { error: err.message } } } + +/** + * 设置模型上下文配置(UPSERT:存在则更新,不存在则插入) + * 支持路径参数和查询参数两种方式 + */ +export async function updateModelContext(ctx: any) { + // 支持两种方式: + // 1. 路径参数: /api/hermes/model-context/:provider/:model + // 2. 查询参数: /api/hermes/model-context?provider=xxx&model=xxx + let provider: string | undefined + let model: string | undefined + + // 优先从路径参数获取 + if (ctx.params.provider && ctx.params.model) { + provider = ctx.params.provider + model = ctx.params.model + } else { + // 从查询参数获取 + const query = ctx.query as { provider?: string; model?: string } + provider = query.provider + model = query.model + } + + // 如果没有参数,从请求体获取 + if (!provider || !model) { + const body = ctx.request.body as { provider?: string; model?: string; context_limit?: number } + provider = body.provider + model = body.model + } + + const { context_limit } = ctx.request.body as { context_limit: number } + + if (!provider || !model || !context_limit) { + ctx.status = 400 + ctx.body = { error: 'Missing required fields: provider, model, context_limit' } + return + } + + if (typeof context_limit !== 'number' || context_limit <= 0) { + ctx.status = 400 + ctx.body = { error: 'Context limit must be a positive number' } + return + } + + try { + const db = getDb() + if (!db) { + ctx.status = 500 + ctx.body = { error: 'Database not available' } + return + } + + // 使用 REPLACE 实现 UPSERT:存在则替换,不存在则插入 + db.prepare( + `REPLACE INTO ${MODEL_CONTEXT_TABLE} (provider, model, context_limit) VALUES (?, ?, ?)` + ).run(provider, model, context_limit) + + // 查询并返回更新后的数据 + const row = db.prepare( + `SELECT id, provider, model, context_limit FROM ${MODEL_CONTEXT_TABLE} WHERE provider = ? AND model = ?` + ).get(provider, model) as { id: number; provider: string; model: string; context_limit: number } + + ctx.body = { + success: true, + data: row + } + } catch (err: any) { + ctx.status = 500 + ctx.body = { error: err.message } + } +} + +/** + * 查询模型上下文配置 + */ +export async function getModelContext(ctx: any) { + // 支持两种方式: + // 1. 路径参数: /api/hermes/model-context/:provider/:model + // 2. 查询参数: /api/hermes/model-context?provider=xxx&model=xxx + let provider: string | undefined + let model: string | undefined + + // 优先从路径参数获取 + if (ctx.params.provider && ctx.params.model) { + provider = ctx.params.provider + model = ctx.params.model + } else { + // 从查询参数获取 + const query = ctx.query as { provider?: string; model?: string } + provider = query.provider + model = query.model + } + + if (!provider || !model) { + ctx.status = 400 + ctx.body = { error: 'Missing provider or model parameter' } + return + } + + try { + const db = getDb() + if (!db) { + ctx.status = 500 + ctx.body = { error: 'Database not available' } + return + } + + const row = db.prepare( + `SELECT id, provider, model, context_limit FROM ${MODEL_CONTEXT_TABLE} WHERE provider = ? AND model = ?` + ).get(provider, model) as { id: number; provider: string; model: string; context_limit: number } | undefined + + if (!row) { + ctx.status = 404 + ctx.body = { error: 'Model context not found' } + return + } + + ctx.body = { data: { ...row, limit: row.context_limit } } + } catch (err: any) { + ctx.status = 500 + ctx.body = { error: err.message } + } +} diff --git a/packages/server/src/db/hermes/schemas.ts b/packages/server/src/db/hermes/schemas.ts index 7995dab..7c5f6d6 100644 --- a/packages/server/src/db/hermes/schemas.ts +++ b/packages/server/src/db/hermes/schemas.ts @@ -89,6 +89,21 @@ export const COMPRESSION_SNAPSHOT_SCHEMA: Record = { updated_at: 'INTEGER NOT NULL', } +// ============================================================================ +// Model Context (model-context.ts) +// ============================================================================ + +export const MODEL_CONTEXT_TABLE = 'model_context' + +export const MODEL_CONTEXT_SCHEMA: Record = { + id: 'INTEGER PRIMARY KEY AUTOINCREMENT', + provider: 'TEXT NOT NULL', + model: 'TEXT NOT NULL', + context_limit: 'INTEGER NOT NULL', +} + +export const MODEL_CONTEXT_INDEX = 'CREATE UNIQUE INDEX IF NOT EXISTS idx_model_context_provider_model ON model_context(provider, model)' + // ============================================================================ // Group Chat (services/hermes/group-chat/index.ts) // ============================================================================ @@ -469,6 +484,13 @@ export function initAllHermesTables(retryCount = 0): void { // Compression snapshot syncTable(COMPRESSION_SNAPSHOT_TABLE, COMPRESSION_SNAPSHOT_SCHEMA) + // Model context + syncTable(MODEL_CONTEXT_TABLE, MODEL_CONTEXT_SCHEMA, { + indexes: { + idx_model_context_provider_model: MODEL_CONTEXT_INDEX, + } + }) + // Group chat - basic tables syncTable(GC_ROOMS_TABLE, GC_ROOMS_SCHEMA) syncTable(GC_MESSAGES_TABLE, GC_MESSAGES_SCHEMA) diff --git a/packages/server/src/routes/hermes/models.ts b/packages/server/src/routes/hermes/models.ts index 4e9b9f1..9b98de9 100644 --- a/packages/server/src/routes/hermes/models.ts +++ b/packages/server/src/routes/hermes/models.ts @@ -6,3 +6,9 @@ export const modelRoutes = new Router() modelRoutes.get('/api/hermes/available-models', ctrl.getAvailable) modelRoutes.get('/api/hermes/config/models', ctrl.getConfigModels) modelRoutes.put('/api/hermes/config/model', ctrl.setConfigModel) + +// Model context routes +modelRoutes.get('/api/hermes/model-context', ctrl.getModelContext) +modelRoutes.get('/api/hermes/model-context/:provider/:model', ctrl.getModelContext) +modelRoutes.put('/api/hermes/model-context/:provider/:model', ctrl.updateModelContext) +modelRoutes.put('/api/hermes/model-context', ctrl.updateModelContext) diff --git a/packages/server/src/services/hermes/model-context.ts b/packages/server/src/services/hermes/model-context.ts index 252322c..1b103ba 100644 --- a/packages/server/src/services/hermes/model-context.ts +++ b/packages/server/src/services/hermes/model-context.ts @@ -2,6 +2,8 @@ import { resolve, join } from 'path' import { homedir } from 'os' import { readFileSync, existsSync, statSync } from 'fs' import yaml from 'js-yaml' +import { getDb } from '../../db' +import { MODEL_CONTEXT_TABLE } from '../../db/hermes/schemas' const HERMES_BASE = resolve(homedir(), '.hermes') const MODELS_DEV_CACHE = resolve(HERMES_BASE, 'models_dev_cache.json') @@ -234,6 +236,25 @@ function lookupContextFromCache(modelName: string, provider: string | null): num * 3. models_dev_cache.json, scoped to model.provider when configured * 4. DEFAULT_CONTEXT_LENGTH (200K hardcoded fallback) */ +/** + * 从数据库 model_context 表查找上下文长度(最高优先级) + */ +function lookupContextFromDatabase(modelName: string, provider: string | null): number | null { + const db = getDb() + if (!db) return null + + try { + // 尝试精确匹配 provider 和 model + const row = db + .prepare(`SELECT context_limit FROM ${MODEL_CONTEXT_TABLE} WHERE provider = ? AND model = ?`) + .get(provider || 'default', modelName) as { context_limit: number } | undefined + + return row?.context_limit || null + } catch { + return null + } +} + export function getModelContextLength(profile?: string): number { const profileDir = getProfileDir(profile) const config = loadConfig(profileDir) @@ -242,12 +263,17 @@ export function getModelContextLength(profile?: string): number { const model = getDefaultModel(config) if (!model) return DEFAULT_CONTEXT_LENGTH + const provider = getDefaultProvider(config) + + // 0. Database model_context table (highest priority) + const dbCtx = lookupContextFromDatabase(model, provider) + if (dbCtx && dbCtx > 0) return dbCtx + // 1. Global context_length override in config.yaml const configCtx = getConfigContextLength(config) if (configCtx && configCtx > 0) return configCtx // 2. Custom provider context_length - const provider = getDefaultProvider(config) const customCtx = lookupCustomProviderContextLength(config, model, provider) if (customCtx && customCtx > 0) return customCtx