Limit run-time model list waiting (#812)

This commit is contained in:
ekko
2026-05-17 12:51:23 +08:00
committed by GitHub
parent 5e8f8bd4a1
commit 6516d86dfc
5 changed files with 57 additions and 8 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "hermes-web-ui",
"version": "0.5.27",
"version": "0.5.28",
"description": "Self-hosted AI chat dashboard for Hermes Agent — multi-model web UI with multi-platform integration",
"repository": {
"type": "git",
+1 -1
View File
@@ -6,7 +6,7 @@ export interface ChangelogEntry {
export const changelog: ChangelogEntry[] = [
{
version: '0.5.27',
version: '0.5.28',
date: '2026-05-17',
changes: [
'changelog.new_0_5_27_1',
+14 -3
View File
@@ -44,7 +44,7 @@ export const useAppStore = defineStore('app', () => {
const sessionPersistence = ref(true)
const maxTokens = ref(4096)
let modelsLoadPromise: Promise<void> | null = null
let modelsLoadedAt = 0
let modelsLastRequestedAt = 0
async function doUpdate(): Promise<boolean> {
updating.value = true
@@ -134,12 +134,12 @@ export const useAppStore = defineStore('app', () => {
async function loadModels(force = false) {
if (!hasApiKey()) return
if (!force && modelsLoadPromise) return modelsLoadPromise
if (!force && modelGroups.value.length > 0 && Date.now() - modelsLoadedAt < MODELS_CACHE_TTL_MS) return
if (!force && modelsLastRequestedAt > 0 && Date.now() - modelsLastRequestedAt < MODELS_CACHE_TTL_MS) return
modelsLastRequestedAt = Date.now()
modelsLoadPromise = (async () => {
try {
const res = await fetchAvailableModels()
applyAvailableModelsResponse(res)
modelsLoadedAt = Date.now()
} catch {
// ignore
} finally {
@@ -149,6 +149,16 @@ export const useAppStore = defineStore('app', () => {
return modelsLoadPromise
}
async function waitForModelsForRun(timeoutMs = 15000) {
if (!hasApiKey()) return
const pending = modelsLoadPromise || (modelsLastRequestedAt === 0 ? loadModels() : null)
if (!pending) return
await Promise.race([
pending,
new Promise<void>(resolve => setTimeout(resolve, timeoutMs)),
])
}
async function reloadModels() {
return loadModels(true)
}
@@ -300,6 +310,7 @@ export const useAppStore = defineStore('app', () => {
maxTokens,
checkConnection,
loadModels,
waitForModelsForRun,
reloadModels,
applyAvailableModelsResponse,
switchModel,
+1 -1
View File
@@ -910,7 +910,7 @@ export const useChatStore = defineStore('chat', () => {
}
const appStore = useAppStore()
await appStore.loadModels()
await appStore.waitForModelsForRun()
const sessionModel = activeSession.value?.model || appStore.selectedModel
const isBridgeSource = activeSession.value?.source === 'cli'
const sessionProvider = activeSession.value?.provider || appStore.selectedProvider
+39 -1
View File
@@ -1,5 +1,5 @@
// @vitest-environment jsdom
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createPinia, setActivePinia } from 'pinia'
const mockSystemApi = vi.hoisted(() => ({
@@ -23,6 +23,10 @@ describe('App Store', () => {
window.localStorage.clear()
})
afterEach(() => {
vi.useRealTimers()
})
it('persists desktop sidebar collapsed state to localStorage', () => {
const store = useAppStore()
@@ -224,6 +228,40 @@ describe('App Store', () => {
expect(store.displayModelName('unknown', 'deepseek')).toBe('unknown')
})
it('does not refetch available models within the cache window after an empty response', async () => {
mockSystemApi.fetchAvailableModels.mockResolvedValue({
default: '',
default_provider: '',
groups: [],
allProviders: [],
})
const store = useAppStore()
await store.loadModels()
await store.loadModels()
expect(mockSystemApi.fetchAvailableModels).toHaveBeenCalledTimes(1)
})
it('waits only up to the run timeout for the first available models request', async () => {
vi.useFakeTimers()
mockSystemApi.fetchAvailableModels.mockReturnValue(new Promise(() => {}))
const store = useAppStore()
let resolved = false
const waitPromise = store.waitForModelsForRun(15000).then(() => {
resolved = true
})
expect(mockSystemApi.fetchAvailableModels).toHaveBeenCalledTimes(1)
await vi.advanceTimersByTimeAsync(14999)
expect(resolved).toBe(false)
await vi.advanceTimersByTimeAsync(1)
await waitPromise
expect(resolved).toBe(true)
expect(store.modelGroups).toEqual([])
})
it('keeps aliases scoped to their provider when model IDs overlap', async () => {
mockSystemApi.fetchAvailableModels.mockResolvedValue({
default: 'shared-model',