mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-22 16:25:58 +00:00
A manual composer pick stays sticky across new chats (intended), but if that model later disappears from the provider/config, every new chat kept trying the dead model and 404'd. Fall back to the profile default in that one case only. refreshCurrentModel now consults the model-options cache the composer already populates (no extra fetch): a manual pick is preserved unless manualPickRemoved() proves it's gone — provider present, non-empty catalog, model absent. An unknown/absent provider, an empty (re-auth/unconfigured) list, or a not-yet-loaded catalog all preserve the pick, so a still-valid selection is never clobbered.
87 lines
3 KiB
TypeScript
87 lines
3 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import { getGlobalModelOptions } from '@/hermes'
|
|
|
|
import { manualPickRemoved, requestModelOptions } from './model-options'
|
|
|
|
const globalOptions = { model: 'hermes-4', provider: 'nous', providers: [] }
|
|
|
|
vi.mock('@/hermes', () => ({
|
|
getGlobalModelOptions: vi.fn(() => Promise.resolve(globalOptions))
|
|
}))
|
|
|
|
describe('requestModelOptions', () => {
|
|
afterEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it('uses the connected gateway even before a session exists', async () => {
|
|
const gatewayPayload = { model: 'BeastMode', provider: 'moa', providers: [] }
|
|
|
|
const gateway = {
|
|
request: vi.fn(() => Promise.resolve(gatewayPayload))
|
|
}
|
|
|
|
await expect(requestModelOptions({ gateway: gateway as never, sessionId: null })).resolves.toBe(gatewayPayload)
|
|
|
|
expect(gateway.request).toHaveBeenCalledWith('model.options', { explicit_only: true })
|
|
expect(getGlobalModelOptions).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('passes the active session id and refresh flag through the gateway', async () => {
|
|
const gateway = {
|
|
request: vi.fn(() => Promise.resolve(globalOptions))
|
|
}
|
|
|
|
await requestModelOptions({ gateway: gateway as never, refresh: true, sessionId: 'session-1' })
|
|
|
|
expect(gateway.request).toHaveBeenCalledWith('model.options', {
|
|
explicit_only: true,
|
|
refresh: true,
|
|
session_id: 'session-1'
|
|
})
|
|
})
|
|
|
|
it('falls back to REST when no gateway is connected', async () => {
|
|
await requestModelOptions({ refresh: true })
|
|
|
|
expect(getGlobalModelOptions).toHaveBeenCalledWith({ explicitOnly: true, refresh: true })
|
|
})
|
|
})
|
|
|
|
describe('manualPickRemoved', () => {
|
|
const providers = [
|
|
{ name: 'OpenRouter', slug: 'openrouter', models: ['owl-alpha', 'gpt-5.5'] },
|
|
{ name: 'Nous', slug: 'nous', models: [] } // present but unconfigured / re-auth
|
|
]
|
|
|
|
it('flags a pick whose model was dropped from a populated provider', () => {
|
|
expect(manualPickRemoved(providers, 'openrouter', 'nemotron-removed')).toBe(true)
|
|
})
|
|
|
|
it('keeps a pick that is still in the catalog', () => {
|
|
expect(manualPickRemoved(providers, 'openrouter', 'gpt-5.5')).toBe(false)
|
|
})
|
|
|
|
it('matches the provider by name as well as slug', () => {
|
|
expect(manualPickRemoved(providers, 'OpenRouter', 'gpt-5.5')).toBe(false)
|
|
expect(manualPickRemoved(providers, 'OpenRouter', 'gone')).toBe(true)
|
|
})
|
|
|
|
it('never clobbers when the provider is absent (ambiguous / deauth)', () => {
|
|
expect(manualPickRemoved(providers, 'anthropic', 'claude-sonnet-4.6')).toBe(false)
|
|
})
|
|
|
|
it('never clobbers when the provider has an empty model list (re-auth)', () => {
|
|
expect(manualPickRemoved(providers, 'nous', 'hermes-4')).toBe(false)
|
|
})
|
|
|
|
it('never clobbers on a not-yet-loaded or empty catalog', () => {
|
|
expect(manualPickRemoved(undefined, 'openrouter', 'gpt-5.5')).toBe(false)
|
|
expect(manualPickRemoved([], 'openrouter', 'gpt-5.5')).toBe(false)
|
|
})
|
|
|
|
it('never clobbers when there is no pick', () => {
|
|
expect(manualPickRemoved(providers, '', '')).toBe(false)
|
|
})
|
|
})
|