Models:支持在 Web UI 里管理可见模型 (#613)

* feat(models): add WUI model visibility filter

Store provider model visibility in Web UI app config and filter the WUI model picker/model page without rewriting Hermes CLI config or canonical model IDs.

* fix(models): sync sidebar after visibility changes
This commit is contained in:
Zhicheng Han
2026-05-11 15:24:45 +02:00
committed by GitHub
parent c6fb449a19
commit 3a1893d401
12 changed files with 710 additions and 12 deletions
+72
View File
@@ -6,6 +6,7 @@ const mockSystemApi = vi.hoisted(() => ({
checkHealth: vi.fn(),
fetchAvailableModels: vi.fn(),
updateDefaultModel: vi.fn(),
updateModelVisibility: vi.fn(),
triggerUpdate: vi.fn(),
}))
@@ -34,6 +35,77 @@ describe('App Store', () => {
expect(window.localStorage.getItem('hermes_sidebar_collapsed')).toBe('0')
})
it('loads model visibility and falls back when the configured default is hidden', async () => {
mockSystemApi.fetchAvailableModels.mockResolvedValue({
default: 'deepseek-chat',
default_provider: 'deepseek',
groups: [
{
provider: 'deepseek',
label: 'DeepSeek',
base_url: 'https://api.deepseek.com/v1',
api_key: 'sk-test',
models: ['deepseek-reasoner'],
},
],
allProviders: [],
model_visibility: {
deepseek: { mode: 'include', models: ['deepseek-reasoner'] },
},
})
const store = useAppStore()
await store.loadModels()
expect(store.modelVisibility).toEqual({
deepseek: { mode: 'include', models: ['deepseek-reasoner'] },
})
expect(store.selectedModel).toBe('deepseek-reasoner')
expect(store.selectedProvider).toBe('deepseek')
expect(store.isModelVisible('deepseek', 'deepseek-reasoner')).toBe(true)
expect(store.isModelVisible('deepseek', 'deepseek-chat')).toBe(false)
})
it('persists model visibility without changing the canonical selected model id', async () => {
mockSystemApi.fetchAvailableModels.mockResolvedValue({
default: 'deepseek-reasoner',
default_provider: 'deepseek',
groups: [
{
provider: 'deepseek',
label: 'DeepSeek',
base_url: 'https://api.deepseek.com/v1',
api_key: 'sk-test',
models: ['deepseek-reasoner'],
},
],
allProviders: [],
model_visibility: {
deepseek: { mode: 'include', models: ['deepseek-reasoner'] },
},
})
mockSystemApi.updateModelVisibility.mockResolvedValue({
success: true,
model_visibility: {
deepseek: { mode: 'include', models: ['deepseek-reasoner'] },
},
})
const store = useAppStore()
await store.setModelVisibility('deepseek', { mode: 'include', models: ['deepseek-reasoner'] })
expect(mockSystemApi.updateModelVisibility).toHaveBeenCalledWith({
provider: 'deepseek',
mode: 'include',
models: ['deepseek-reasoner'],
})
expect(store.selectedModel).toBe('deepseek-reasoner')
expect(store.selectedProvider).toBe('deepseek')
expect(mockSystemApi.updateDefaultModel).not.toHaveBeenCalled()
})
it('clears the updating state and reports failure when self-update request fails', async () => {
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
mockSystemApi.triggerUpdate.mockRejectedValue(new Error('install failed'))