Persist custom Hermes models (#913)

This commit is contained in:
ekko
2026-05-21 20:55:19 +08:00
committed by GitHub
parent 4d89767847
commit ff1f471745
9 changed files with 418 additions and 13 deletions
@@ -130,6 +130,27 @@ describe('models controller — model visibility', () => {
deepseek: { mode: 'include', models: ['deepseek-reasoner'] },
})
})
it('merges Web UI custom models into available provider groups', async () => {
mockReadAppConfig.mockResolvedValue({
customModels: {
deepseek: ['gemma-4-26b-a4b-it', 'deepseek-chat'],
},
})
const ctx = makeCtx()
await ctrl.getAvailable(ctx)
expect(ctx.status).toBe(200)
expect(ctx.body.groups[0]).toMatchObject({
provider: 'deepseek',
models: ['deepseek-chat', 'deepseek-reasoner', 'gemma-4-26b-a4b-it'],
available_models: ['deepseek-chat', 'deepseek-reasoner', 'gemma-4-26b-a4b-it'],
})
expect(ctx.body.custom_models).toEqual({
deepseek: ['gemma-4-26b-a4b-it', 'deepseek-chat'],
})
})
it('accepts OAuth providers stored in credential_pool entries', async () => {
mockExistsSync.mockReturnValue(true)
mockReadFileSync.mockReturnValue(JSON.stringify({
@@ -281,6 +302,63 @@ describe('models controller — model visibility', () => {
})
})
it('adds and removes custom models in web-ui app config only', async () => {
mockReadAppConfig.mockResolvedValueOnce({
customModels: { deepseek: ['existing'] },
})
mockWriteAppConfig.mockResolvedValueOnce({
customModels: { deepseek: ['existing', 'manual-model'] },
})
const addCtx = makeCtx({ provider: 'deepseek', model: 'manual-model' })
await ctrl.addCustomModel(addCtx)
expect(mockWriteAppConfig).toHaveBeenCalledWith({
customModels: { deepseek: ['existing', 'manual-model'] },
})
expect(addCtx.body).toEqual({
success: true,
custom_models: { deepseek: ['existing', 'manual-model'] },
})
mockReadAppConfig.mockResolvedValueOnce({
customModels: { deepseek: ['existing', 'manual-model'] },
})
mockWriteAppConfig.mockResolvedValueOnce({
customModels: { deepseek: ['existing'] },
})
const removeCtx = makeCtx({ provider: 'deepseek', model: 'manual-model' })
await ctrl.removeCustomModel(removeCtx)
expect(mockWriteAppConfig).toHaveBeenLastCalledWith({
customModels: { deepseek: ['existing'] },
})
expect(removeCtx.body).toEqual({
success: true,
custom_models: { deepseek: ['existing'] },
})
})
it('removes custom models from query params when DELETE body is missing', async () => {
mockReadAppConfig.mockResolvedValueOnce({
customModels: { deepseek: ['manual-model'] },
})
mockWriteAppConfig.mockResolvedValueOnce({
customModels: {},
})
const ctx = makeCtx()
ctx.request.body = undefined
ctx.query = { provider: 'deepseek', model: 'manual-model' }
await ctrl.removeCustomModel(ctx)
expect(ctx.status).toBe(200)
expect(mockWriteAppConfig).toHaveBeenCalledWith({ customModels: {} })
expect(ctx.body).toEqual({ success: true, custom_models: {} })
})
it('rejects empty include lists', async () => {
const ctx = makeCtx({ provider: 'deepseek', mode: 'include', models: [] })
await ctrl.setModelVisibility(ctx)
@@ -0,0 +1,71 @@
import { describe, expect, it } from 'vitest'
import { bridgeTerminalError } from '../../packages/server/src/services/hermes/run-chat/handle-bridge-run'
describe('bridge terminal error detection', () => {
it('uses bridge status errors directly', () => {
expect(bridgeTerminalError({
status: 'error',
error: 'bridge crashed',
result: null,
} as any)).toBe('bridge crashed')
})
it('surfaces agent result failure flags as run failures', () => {
expect(bridgeTerminalError({
status: 'complete',
error: undefined,
result: {
failed: true,
completed: false,
error: 'API call failed after 3 retries. HTTP 503: no available channel',
final_response: 'API call failed after 3 retries. HTTP 503: no available channel',
},
} as any)).toBe('API call failed after 3 retries. HTTP 503: no available channel')
})
it('falls back to final_response for failed results without an error field', () => {
expect(bridgeTerminalError({
status: 'complete',
result: {
completed: false,
final_response: 'API call failed after 3 retries: timeout',
},
} as any)).toBe('API call failed after 3 retries: timeout')
})
it('surfaces HTTP auth/provider errors even when failure flags are missing', () => {
expect(bridgeTerminalError({
status: 'complete',
result: {
final_response: 'API call failed after 3 retries. HTTP 403: forbidden',
},
} as any)).toBe('API call failed after 3 retries. HTTP 403: forbidden')
expect(bridgeTerminalError({
status: 'complete',
result: {
error: 'HTTP 401: unauthorized',
},
} as any)).toBe('HTTP 401: unauthorized')
})
it('surfaces generic provider result errors even without failed flags', () => {
expect(bridgeTerminalError({
status: 'complete',
result: {
error: '分组 subrouter 下模型 test 无可用渠道(distributor',
},
} as any)).toBe('分组 subrouter 下模型 test 无可用渠道(distributor')
})
it('does not flag successful complete results', () => {
expect(bridgeTerminalError({
status: 'complete',
result: {
completed: true,
final_response: 'done',
},
} as any)).toBeNull()
})
})