Files
Hermes-ui/packages/client/src/stores/hermes/app.ts
T
ekkoandClaude Opus 4.6 8a4ab2d69d fix: provider-aware model selection to prevent cross-provider conflicts
When multiple providers share the same model name, the selector now
uses both model ID and provider as the unique identifier instead of
model name alone. Backend returns default_provider alongside default
model, and model switching sends provider to the config.

Fixes #52

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-19 15:05:05 +08:00

134 lines
3.6 KiB
TypeScript

import { defineStore } from 'pinia'
import { ref } from 'vue'
import { checkHealth, fetchAvailableModels, updateDefaultModel, triggerUpdate, type AvailableModelGroup } from '@/api/hermes/system'
const WEB_UI_VERSION = __APP_VERSION__
const SIDEBAR_COLLAPSED_KEY = 'hermes_sidebar_collapsed'
export const useAppStore = defineStore('app', () => {
const sidebarOpen = ref(false)
// Desktop-only collapsed state (icon-rail mode). Persisted to localStorage.
const sidebarCollapsed = ref(localStorage.getItem(SIDEBAR_COLLAPSED_KEY) === '1')
const connected = ref(false)
const serverVersion = ref(WEB_UI_VERSION)
const latestVersion = ref('')
const updateAvailable = ref(false)
const updating = ref(false)
const modelGroups = ref<AvailableModelGroup[]>([])
const selectedModel = ref('')
const selectedProvider = ref('')
const healthPollTimer = ref<ReturnType<typeof setInterval>>()
// Settings
const streamEnabled = ref(true)
const sessionPersistence = ref(true)
const maxTokens = ref(4096)
async function doUpdate(): Promise<boolean> {
updating.value = true
try {
const res = await triggerUpdate()
if (res.success) {
updateAvailable.value = false
await checkConnection()
}
return res.success
} finally {
updating.value = false
}
}
async function checkConnection() {
try {
const res = await checkHealth()
connected.value = res.status === 'ok'
if (res.webui_version) serverVersion.value = res.webui_version
if (res.webui_latest) latestVersion.value = res.webui_latest
updateAvailable.value = !!res.webui_update_available
} catch {
connected.value = false
}
}
async function loadModels() {
try {
const res = await fetchAvailableModels()
modelGroups.value = res.groups
selectedModel.value = res.default
selectedProvider.value = res.default_provider || ''
} catch {
// ignore
}
}
async function switchModel(modelId: string, providerOverride?: string) {
try {
// Find the group containing this model to get provider info
const group = modelGroups.value.find(g => g.models.includes(modelId))
const provider = providerOverride || group?.provider || ''
await updateDefaultModel({ default: modelId, provider })
selectedModel.value = modelId
selectedProvider.value = provider || ''
} catch (err: any) {
console.error('Failed to switch model:', err)
}
}
function startHealthPolling(interval = 30000) {
stopHealthPolling()
checkConnection()
healthPollTimer.value = setInterval(checkConnection, interval)
}
function stopHealthPolling() {
if (healthPollTimer.value) {
clearInterval(healthPollTimer.value)
healthPollTimer.value = undefined
}
}
function toggleSidebar() {
sidebarOpen.value = !sidebarOpen.value
}
function closeSidebar() {
sidebarOpen.value = false
}
function toggleSidebarCollapsed() {
sidebarCollapsed.value = !sidebarCollapsed.value
try {
localStorage.setItem(SIDEBAR_COLLAPSED_KEY, sidebarCollapsed.value ? '1' : '0')
} catch {
// ignore quota errors — fallback to in-memory only
}
}
return {
sidebarOpen,
sidebarCollapsed,
toggleSidebar,
closeSidebar,
toggleSidebarCollapsed,
connected,
serverVersion,
latestVersion,
updateAvailable,
updating,
doUpdate,
modelGroups,
selectedModel,
selectedProvider,
streamEnabled,
sessionPersistence,
maxTokens,
checkConnection,
loadModels,
switchModel,
startHealthPolling,
stopHealthPolling,
}
})