When a desktop/dashboard session had no agent built yet and the user explicitly
picked a provider in the model picker, config.set('model', ...) would first try
to initialize the agent from the (possibly broken) config default provider —
failing before the user's explicit switch could take effect, trapping them on a
misconfigured default.
config.set now pre-parses the model flags: if an explicit --provider is present
and no agent exists yet, it skips the default-provider agent build and routes
straight through _apply_model_switch with the explicit provider. _apply_model_switch
gained a parsed_flags passthrough (avoids double-parsing) and only falls back to
resolve_runtime_provider(requested=None) when no explicit provider was given.
The desktop hook now sends config.set instead of slash.exec for active-session
model changes, so errors from the selected provider surface to the user instead
of being swallowed.
Co-authored-by: rodboev <rod.boev@gmail.com>
This commit is contained in:
co-authored by
rodboev
parent
2a08b8c86f
commit
ed20f5ed06
@@ -1,5 +1,5 @@
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { QueryClient } from '@tanstack/react-query'
|
||||
import { cleanup, render, renderHook } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { getGlobalModelInfo } from '@/hermes'
|
||||
@@ -13,12 +13,51 @@ import {
|
||||
|
||||
import { useModelControls } from './use-model-controls'
|
||||
|
||||
const setGlobalModel = vi.fn()
|
||||
const notifyError = vi.fn()
|
||||
|
||||
vi.mock('@/hermes', () => ({
|
||||
getGlobalModelInfo: vi.fn(),
|
||||
setGlobalModel: vi.fn()
|
||||
setGlobalModel: (...args: Parameters<typeof setGlobalModel>) => setGlobalModel(...args)
|
||||
}))
|
||||
|
||||
describe('useModelControls.refreshCurrentModel', () => {
|
||||
vi.mock('@/i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: {
|
||||
desktop: {
|
||||
modelSwitchFailed: 'Model switch failed'
|
||||
}
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/store/notifications', () => ({
|
||||
notifyError: (...args: Parameters<typeof notifyError>) => notifyError(...args)
|
||||
}))
|
||||
|
||||
type Controls = ReturnType<typeof useModelControls>
|
||||
|
||||
function Harness({
|
||||
activeSessionId,
|
||||
onReady,
|
||||
requestGateway
|
||||
}: {
|
||||
activeSessionId: string | null
|
||||
onReady: (controls: Controls) => void
|
||||
requestGateway: <T = unknown>(method: string, params?: Record<string, unknown>) => Promise<T>
|
||||
}) {
|
||||
const controls = useModelControls({
|
||||
activeSessionId,
|
||||
queryClient: new QueryClient(),
|
||||
requestGateway
|
||||
})
|
||||
|
||||
onReady(controls)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
describe('useModelControls', () => {
|
||||
beforeEach(() => {
|
||||
$activeSessionId.set(null)
|
||||
setCurrentModel('')
|
||||
@@ -26,6 +65,7 @@ describe('useModelControls.refreshCurrentModel', () => {
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
vi.restoreAllMocks()
|
||||
$activeSessionId.set(null)
|
||||
setCurrentModel('')
|
||||
@@ -74,4 +114,55 @@ describe('useModelControls.refreshCurrentModel', () => {
|
||||
expect($currentModel.get()).toBe('deepseek/deepseek-v4-pro')
|
||||
expect($currentProvider.get()).toBe('deepseek')
|
||||
})
|
||||
|
||||
it('routes active-session picker changes through config.set with an explicit provider', async () => {
|
||||
const requestGateway = vi.fn(async () => ({ key: 'model', value: 'claude-sonnet-4.6' }) as never)
|
||||
let controls!: Controls
|
||||
|
||||
render(
|
||||
<Harness
|
||||
activeSessionId="session-1"
|
||||
onReady={value => (controls = value)}
|
||||
requestGateway={requestGateway}
|
||||
/>
|
||||
)
|
||||
|
||||
await expect(
|
||||
controls.selectModel({
|
||||
model: 'claude-sonnet-4.6',
|
||||
persistGlobal: false,
|
||||
provider: 'anthropic'
|
||||
})
|
||||
).resolves.toBe(true)
|
||||
|
||||
expect(requestGateway).toHaveBeenCalledWith('config.set', {
|
||||
session_id: 'session-1',
|
||||
key: 'model',
|
||||
value: 'claude-sonnet-4.6 --provider anthropic'
|
||||
})
|
||||
expect(requestGateway).not.toHaveBeenCalledWith('slash.exec', expect.anything())
|
||||
})
|
||||
|
||||
it('keeps the global path on setGlobalModel when there is no active session', async () => {
|
||||
setGlobalModel.mockResolvedValue(undefined)
|
||||
let controls!: Controls
|
||||
|
||||
render(
|
||||
<Harness
|
||||
activeSessionId={null}
|
||||
onReady={value => (controls = value)}
|
||||
requestGateway={vi.fn()}
|
||||
/>
|
||||
)
|
||||
|
||||
await expect(
|
||||
controls.selectModel({
|
||||
model: 'claude-sonnet-4.6',
|
||||
persistGlobal: false,
|
||||
provider: 'anthropic'
|
||||
})
|
||||
).resolves.toBe(true)
|
||||
|
||||
expect(setGlobalModel).toHaveBeenCalledWith('anthropic', 'claude-sonnet-4.6')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -82,9 +82,10 @@ export function useModelControls({ activeSessionId, queryClient, requestGateway
|
||||
|
||||
try {
|
||||
if (activeSessionId) {
|
||||
await requestGateway('slash.exec', {
|
||||
await requestGateway('config.set', {
|
||||
session_id: activeSessionId,
|
||||
command: `/model ${selection.model} --provider ${selection.provider}${selection.persistGlobal ? ' --global' : ''}`
|
||||
key: 'model',
|
||||
value: `${selection.model} --provider ${selection.provider}${selection.persistGlobal ? ' --global' : ''}`
|
||||
})
|
||||
|
||||
if (selection.persistGlobal) {
|
||||
|
||||
Reference in New Issue
Block a user