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:
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,21 @@ export const COMPRESSION_SNAPSHOT_SCHEMA: Record<string, string> = {
|
||||
updated_at: 'INTEGER NOT NULL',
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Model Context (model-context.ts)
|
||||
// ============================================================================
|
||||
|
||||
export const MODEL_CONTEXT_TABLE = 'model_context'
|
||||
|
||||
export const MODEL_CONTEXT_SCHEMA: Record<string, string> = {
|
||||
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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user