Limit run-time model list waiting (#812)
This commit is contained in:
+2
-2
@@ -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",
|
||||
@@ -120,4 +120,4 @@
|
||||
"vue-tsc": "^3.2.8",
|
||||
"ws": "^8.20.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user