Limit run-time model list waiting (#812)
This commit is contained in:
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hermes-web-ui",
|
"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",
|
"description": "Self-hosted AI chat dashboard for Hermes Agent — multi-model web UI with multi-platform integration",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -120,4 +120,4 @@
|
|||||||
"vue-tsc": "^3.2.8",
|
"vue-tsc": "^3.2.8",
|
||||||
"ws": "^8.20.0"
|
"ws": "^8.20.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,7 @@ export interface ChangelogEntry {
|
|||||||
|
|
||||||
export const changelog: ChangelogEntry[] = [
|
export const changelog: ChangelogEntry[] = [
|
||||||
{
|
{
|
||||||
version: '0.5.27',
|
version: '0.5.28',
|
||||||
date: '2026-05-17',
|
date: '2026-05-17',
|
||||||
changes: [
|
changes: [
|
||||||
'changelog.new_0_5_27_1',
|
'changelog.new_0_5_27_1',
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export const useAppStore = defineStore('app', () => {
|
|||||||
const sessionPersistence = ref(true)
|
const sessionPersistence = ref(true)
|
||||||
const maxTokens = ref(4096)
|
const maxTokens = ref(4096)
|
||||||
let modelsLoadPromise: Promise<void> | null = null
|
let modelsLoadPromise: Promise<void> | null = null
|
||||||
let modelsLoadedAt = 0
|
let modelsLastRequestedAt = 0
|
||||||
|
|
||||||
async function doUpdate(): Promise<boolean> {
|
async function doUpdate(): Promise<boolean> {
|
||||||
updating.value = true
|
updating.value = true
|
||||||
@@ -134,12 +134,12 @@ export const useAppStore = defineStore('app', () => {
|
|||||||
async function loadModels(force = false) {
|
async function loadModels(force = false) {
|
||||||
if (!hasApiKey()) return
|
if (!hasApiKey()) return
|
||||||
if (!force && modelsLoadPromise) return modelsLoadPromise
|
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 () => {
|
modelsLoadPromise = (async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetchAvailableModels()
|
const res = await fetchAvailableModels()
|
||||||
applyAvailableModelsResponse(res)
|
applyAvailableModelsResponse(res)
|
||||||
modelsLoadedAt = Date.now()
|
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
} finally {
|
} finally {
|
||||||
@@ -149,6 +149,16 @@ export const useAppStore = defineStore('app', () => {
|
|||||||
return modelsLoadPromise
|
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() {
|
async function reloadModels() {
|
||||||
return loadModels(true)
|
return loadModels(true)
|
||||||
}
|
}
|
||||||
@@ -300,6 +310,7 @@ export const useAppStore = defineStore('app', () => {
|
|||||||
maxTokens,
|
maxTokens,
|
||||||
checkConnection,
|
checkConnection,
|
||||||
loadModels,
|
loadModels,
|
||||||
|
waitForModelsForRun,
|
||||||
reloadModels,
|
reloadModels,
|
||||||
applyAvailableModelsResponse,
|
applyAvailableModelsResponse,
|
||||||
switchModel,
|
switchModel,
|
||||||
|
|||||||
@@ -910,7 +910,7 @@ export const useChatStore = defineStore('chat', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
await appStore.loadModels()
|
await appStore.waitForModelsForRun()
|
||||||
const sessionModel = activeSession.value?.model || appStore.selectedModel
|
const sessionModel = activeSession.value?.model || appStore.selectedModel
|
||||||
const isBridgeSource = activeSession.value?.source === 'cli'
|
const isBridgeSource = activeSession.value?.source === 'cli'
|
||||||
const sessionProvider = activeSession.value?.provider || appStore.selectedProvider
|
const sessionProvider = activeSession.value?.provider || appStore.selectedProvider
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// @vitest-environment jsdom
|
// @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'
|
import { createPinia, setActivePinia } from 'pinia'
|
||||||
|
|
||||||
const mockSystemApi = vi.hoisted(() => ({
|
const mockSystemApi = vi.hoisted(() => ({
|
||||||
@@ -23,6 +23,10 @@ describe('App Store', () => {
|
|||||||
window.localStorage.clear()
|
window.localStorage.clear()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers()
|
||||||
|
})
|
||||||
|
|
||||||
it('persists desktop sidebar collapsed state to localStorage', () => {
|
it('persists desktop sidebar collapsed state to localStorage', () => {
|
||||||
const store = useAppStore()
|
const store = useAppStore()
|
||||||
|
|
||||||
@@ -224,6 +228,40 @@ describe('App Store', () => {
|
|||||||
expect(store.displayModelName('unknown', 'deepseek')).toBe('unknown')
|
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 () => {
|
it('keeps aliases scoped to their provider when model IDs overlap', async () => {
|
||||||
mockSystemApi.fetchAvailableModels.mockResolvedValue({
|
mockSystemApi.fetchAvailableModels.mockResolvedValue({
|
||||||
default: 'shared-model',
|
default: 'shared-model',
|
||||||
|
|||||||
Reference in New Issue
Block a user