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:
@@ -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'))
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// @vitest-environment jsdom
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
|
||||
const mockSystemApi = vi.hoisted(() => ({
|
||||
fetchAvailableModels: vi.fn(),
|
||||
updateDefaultModel: vi.fn(),
|
||||
addCustomProvider: vi.fn(),
|
||||
removeCustomProvider: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/api/hermes/system', () => mockSystemApi)
|
||||
|
||||
import { useAppStore } from '@/stores/hermes/app'
|
||||
import { useModelsStore } from '@/stores/hermes/models'
|
||||
|
||||
describe('Models Store', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.clearAllMocks()
|
||||
window.localStorage.clear()
|
||||
})
|
||||
|
||||
it('keeps the sidebar model picker in sync after provider model visibility changes', async () => {
|
||||
const visibleGroups = [
|
||||
{
|
||||
provider: 'deepseek',
|
||||
label: 'DeepSeek',
|
||||
base_url: 'https://api.deepseek.com/v1',
|
||||
api_key: 'sk-test',
|
||||
models: ['deepseek-v4-flash', 'deepseek-v4-pro'],
|
||||
available_models: ['deepseek-v4-flash', 'deepseek-v4-pro'],
|
||||
model_meta: {
|
||||
'deepseek-v4-pro': { preview: true },
|
||||
},
|
||||
},
|
||||
]
|
||||
mockSystemApi.fetchAvailableModels.mockResolvedValue({
|
||||
default: 'deepseek-v4-flash',
|
||||
default_provider: 'deepseek',
|
||||
groups: visibleGroups,
|
||||
allProviders: visibleGroups,
|
||||
model_visibility: {
|
||||
deepseek: { mode: 'include', models: ['deepseek-v4-flash', 'deepseek-v4-pro'] },
|
||||
},
|
||||
})
|
||||
|
||||
const appStore = useAppStore()
|
||||
appStore.modelGroups = [
|
||||
{
|
||||
provider: 'deepseek',
|
||||
label: 'DeepSeek',
|
||||
base_url: 'https://api.deepseek.com/v1',
|
||||
api_key: 'sk-test',
|
||||
models: ['deepseek-v4-flash'],
|
||||
available_models: ['deepseek-v4-flash', 'deepseek-v4-pro'],
|
||||
},
|
||||
]
|
||||
|
||||
const modelsStore = useModelsStore()
|
||||
await modelsStore.fetchProviders()
|
||||
|
||||
expect(modelsStore.providers[0].models).toEqual(['deepseek-v4-flash', 'deepseek-v4-pro'])
|
||||
expect(appStore.modelGroups[0].models).toEqual(['deepseek-v4-flash', 'deepseek-v4-pro'])
|
||||
expect(appStore.modelGroups[0].available_models).toEqual(['deepseek-v4-flash', 'deepseek-v4-pro'])
|
||||
expect(appStore.modelGroups[0].model_meta).toEqual({
|
||||
'deepseek-v4-pro': { preview: true },
|
||||
})
|
||||
expect(appStore.modelVisibility).toEqual({
|
||||
deepseek: { mode: 'include', models: ['deepseek-v4-flash', 'deepseek-v4-pro'] },
|
||||
})
|
||||
expect(appStore.selectedModel).toBe('deepseek-v4-flash')
|
||||
expect(appStore.selectedProvider).toBe('deepseek')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user