mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-22 16:25:58 +00:00
fix(desktop): scope model options by profile (#62795)
Co-authored-by: embwl0x <embwl0x@users.noreply.github.com>
This commit is contained in:
parent
8967e73e67
commit
9fed768b56
21 changed files with 330 additions and 82 deletions
|
|
@ -14,18 +14,19 @@ import { PromptOverlays } from '@/components/prompt-overlays'
|
|||
import { Button } from '@/components/ui/button'
|
||||
import { ErrorState } from '@/components/ui/error-state'
|
||||
import { TitleMenuTrigger } from '@/components/ui/title-menu-trigger'
|
||||
import { getGlobalModelOptions, type HermesGateway } from '@/hermes'
|
||||
import { type HermesGateway } from '@/hermes'
|
||||
import { useI18n } from '@/i18n'
|
||||
import type { ChatMessage } from '@/lib/chat-messages'
|
||||
import { quickModelOptions, sessionTitle } from '@/lib/chat-runtime'
|
||||
import { useIncrementalExternalStoreRuntime } from '@/lib/incremental-external-store-runtime'
|
||||
import { modelOptionsQueryKey, requestModelOptions } from '@/lib/model-options'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { migrateSessionDraft } from '@/store/composer'
|
||||
import { migrateQueuedPrompts, parkQueuedPrompts } from '@/store/composer-queue'
|
||||
import { $pinnedSessionIds } from '@/store/layout'
|
||||
import { $petActive } from '@/store/pet'
|
||||
import { $petOverlayActive } from '@/store/pet-overlay'
|
||||
import { $gatewaySwapTarget, $profiles } from '@/store/profile'
|
||||
import { $activeGatewayProfile, $gatewaySwapTarget, $profiles } from '@/store/profile'
|
||||
import {
|
||||
$contextSuggestions,
|
||||
$freshDraftReady,
|
||||
|
|
@ -254,6 +255,7 @@ export function ChatView({
|
|||
const sessionAnchor = isPrimary ? 'workspace' : `session-tile:${storedId ?? ''}`
|
||||
const awaitingResponse = useStore(view.$awaitingResponse)
|
||||
const busy = useStore(view.$busy)
|
||||
const activeGatewayProfile = useStore($activeGatewayProfile)
|
||||
const contextSuggestions = useStore($contextSuggestions)
|
||||
// Per-session (SessionView) reads — a tile IS its session, so these come
|
||||
// from the view slice, not the global atoms (which track the primary only).
|
||||
|
|
@ -356,21 +358,8 @@ export function ChatView({
|
|||
const threadKey = selectedSessionId || activeSessionId || (isRoutedSessionView ? location.pathname : 'new')
|
||||
|
||||
const modelOptionsQuery = useQuery<ModelOptionsResponse>({
|
||||
queryKey: ['model-options', activeSessionId || 'global'],
|
||||
queryFn: () => {
|
||||
if (!activeSessionId) {
|
||||
return getGlobalModelOptions()
|
||||
}
|
||||
|
||||
if (!gateway) {
|
||||
throw new Error('Hermes gateway unavailable')
|
||||
}
|
||||
|
||||
return gateway.request<ModelOptionsResponse>('model.options', {
|
||||
session_id: activeSessionId,
|
||||
explicit_only: true
|
||||
})
|
||||
},
|
||||
queryKey: modelOptionsQueryKey(activeGatewayProfile, activeSessionId),
|
||||
queryFn: () => requestModelOptions({ gateway: gateway || undefined, sessionId: activeSessionId }),
|
||||
enabled: gatewayOpen
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import type { ChatMessage } from '@/lib/chat-messages'
|
|||
import { sessionTitle } from '@/lib/chat-runtime'
|
||||
import { createComposerAttachmentScope } from '@/store/composer'
|
||||
import { $pinnedSessionIds, pinSession, unpinSession } from '@/store/layout'
|
||||
import { $activeGatewayProfile } from '@/store/profile'
|
||||
import { sessionAwaitingInput } from '@/store/prompts'
|
||||
import {
|
||||
$gatewayState,
|
||||
|
|
@ -112,6 +113,7 @@ function TileChat({
|
|||
const { gatewayRef, requestGateway } = useGatewayRequest()
|
||||
const queryClient = useQueryClient()
|
||||
const { selectModel } = useModelControls({ queryClient, requestGateway })
|
||||
const activeGatewayProfile = useStore($activeGatewayProfile)
|
||||
const cwd = useStore(view.$cwd)
|
||||
const gatewayOpen = useStore($gatewayState) === 'open'
|
||||
|
||||
|
|
@ -148,10 +150,11 @@ function TileChat({
|
|||
<ModelMenuPanel
|
||||
gateway={gatewayRef.current || undefined}
|
||||
onSelectModel={selectModel}
|
||||
profile={activeGatewayProfile}
|
||||
requestGateway={requestGateway}
|
||||
/>
|
||||
) : null,
|
||||
[gatewayOpen, gatewayRef, requestGateway, selectModel]
|
||||
[activeGatewayProfile, gatewayOpen, gatewayRef, requestGateway, selectModel]
|
||||
)
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { Navigate, Route, Routes, useParams } from 'react-router-dom'
|
|||
|
||||
import { ContribBoundary } from '@/contrib/react/boundary'
|
||||
import { useContributions } from '@/contrib/react/use-contributions'
|
||||
import { $activeGatewayProfile } from '@/store/profile'
|
||||
import { $freshDraftReady, $gatewayState } from '@/store/session'
|
||||
|
||||
import { ChatView } from '../chat'
|
||||
|
|
@ -109,6 +110,7 @@ export const ChatRoutesSurface = memo(function ChatRoutesSurface({
|
|||
actions: WiringActions
|
||||
maxVoiceRecordingSeconds?: number
|
||||
}) {
|
||||
const activeGatewayProfile = useStore($activeGatewayProfile)
|
||||
const gatewayState = useStore($gatewayState)
|
||||
useContributions(ROUTES_AREA)
|
||||
const routeContributions = contributedRoutes()
|
||||
|
|
@ -128,10 +130,11 @@ export const ChatRoutesSurface = memo(function ChatRoutesSurface({
|
|||
<ModelMenuPanel
|
||||
gateway={gateway || undefined}
|
||||
onSelectModel={actions.selectModel}
|
||||
profile={activeGatewayProfile}
|
||||
requestGateway={actions.requestGateway}
|
||||
/>
|
||||
) : null,
|
||||
[actions, gateway, gatewayState]
|
||||
[actions, activeGatewayProfile, gateway, gatewayState]
|
||||
)
|
||||
|
||||
const chatView = (
|
||||
|
|
|
|||
|
|
@ -139,6 +139,7 @@ export function ContribWiring({ children }: { children: ReactNode }) {
|
|||
const resumeExhaustedSessionId = useStore($resumeExhaustedSessionId)
|
||||
const selectedStoredSessionId = useStore($selectedStoredSessionId)
|
||||
const messagingSessions = useStore($messagingSessions)
|
||||
const activeGatewayProfile = useStore($activeGatewayProfile)
|
||||
const profileScope = useStore($profileScope)
|
||||
|
||||
const routedSessionId = routeSessionId(location.pathname)
|
||||
|
|
@ -341,6 +342,7 @@ export function ContribWiring({ children }: { children: ReactNode }) {
|
|||
}, [activeSessionIdRef, busyRef, selectedStoredSessionIdRef, updateSessionState])
|
||||
|
||||
const { handleGatewayEvent } = useMessageStream({
|
||||
activeGatewayProfile,
|
||||
activeSessionIdRef,
|
||||
hydrateFromStoredSession,
|
||||
queryClient,
|
||||
|
|
@ -427,7 +429,6 @@ export function ContribWiring({ children }: { children: ReactNode }) {
|
|||
// Swapping the live gateway to another profile must re-pull that profile's
|
||||
// global model + active-profile pill (both are nanostores — the blanket
|
||||
// invalidateQueries on swap doesn't touch them).
|
||||
const activeGatewayProfile = useStore($activeGatewayProfile)
|
||||
const lastGatewayProfileRef = useRef(activeGatewayProfile)
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -899,12 +900,21 @@ export function ContribWiring({ children }: { children: ReactNode }) {
|
|||
void refreshCurrentModel()
|
||||
void queryClient.invalidateQueries({ queryKey: ['model-options'] })
|
||||
}}
|
||||
profile={activeGatewayProfile}
|
||||
requestGateway={requestGateway}
|
||||
/>
|
||||
)}
|
||||
<ModelPickerOverlay gateway={gatewayRef.current || undefined} onSelect={selectModel} />
|
||||
<ModelPickerOverlay
|
||||
gateway={gatewayRef.current || undefined}
|
||||
onSelect={selectModel}
|
||||
profile={activeGatewayProfile}
|
||||
/>
|
||||
<SessionPickerOverlay onResume={resumeSession} />
|
||||
<ModelVisibilityOverlay gateway={gatewayRef.current || undefined} onOpenProviders={openProviderSettings} />
|
||||
<ModelVisibilityOverlay
|
||||
gateway={gatewayRef.current || undefined}
|
||||
onOpenProviders={openProviderSettings}
|
||||
profile={activeGatewayProfile}
|
||||
/>
|
||||
<UpdatesOverlay />
|
||||
<GatewayConnectingOverlay />
|
||||
<BootFailureOverlay />
|
||||
|
|
|
|||
|
|
@ -16,9 +16,10 @@ import { $focusedRuntimeId, $focusedSessionState } from '@/store/session-states'
|
|||
interface ModelPickerOverlayProps {
|
||||
gateway?: HermesGateway
|
||||
onSelect: (selection: ModelSelection) => void
|
||||
profile: string
|
||||
}
|
||||
|
||||
export function ModelPickerOverlay({ gateway, onSelect }: ModelPickerOverlayProps) {
|
||||
export function ModelPickerOverlay({ gateway, onSelect, profile }: ModelPickerOverlayProps) {
|
||||
const primarySessionId = useStore($activeSessionId)
|
||||
const primaryModel = useStore($currentModel)
|
||||
const primaryProvider = useStore($currentProvider)
|
||||
|
|
@ -45,6 +46,7 @@ export function ModelPickerOverlay({ gateway, onSelect }: ModelPickerOverlayProp
|
|||
onOpenChange={setModelPickerOpen}
|
||||
onSelect={selection => onSelect({ ...selection, sessionId })}
|
||||
open={open}
|
||||
profile={profile}
|
||||
sessionId={sessionId}
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,9 +8,10 @@ import { $activeSessionId, $gatewayState } from '@/store/session'
|
|||
interface ModelVisibilityOverlayProps {
|
||||
gateway?: HermesGateway
|
||||
onOpenProviders: () => void
|
||||
profile: string
|
||||
}
|
||||
|
||||
export function ModelVisibilityOverlay({ gateway, onOpenProviders }: ModelVisibilityOverlayProps) {
|
||||
export function ModelVisibilityOverlay({ gateway, onOpenProviders, profile }: ModelVisibilityOverlayProps) {
|
||||
const activeSessionId = useStore($activeSessionId)
|
||||
const gatewayOpen = useStore($gatewayState) === 'open'
|
||||
const open = useStore($modelVisibilityOpen)
|
||||
|
|
@ -25,6 +26,7 @@ export function ModelVisibilityOverlay({ gateway, onOpenProviders }: ModelVisibi
|
|||
onOpenChange={setModelVisibilityOpen}
|
||||
onOpenProviders={onOpenProviders}
|
||||
open={open}
|
||||
profile={profile}
|
||||
sessionId={activeSessionId}
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { coerceGatewayText, coerceThinkingText, normalizePersonalityValue } from
|
|||
import { playCompletionSound } from '@/lib/completion-sound'
|
||||
import { resolveGatewayEventSessionId } from '@/lib/gateway-events'
|
||||
import { triggerHaptic } from '@/lib/haptics'
|
||||
import { modelOptionsQueryKey } from '@/lib/model-options'
|
||||
import { isProviderSetupErrorMessage } from '@/lib/provider-setup-errors'
|
||||
import { reconcileApprovalModeForProfile } from '@/store/approval-mode'
|
||||
import { clearClarifyRequest, setClarifyRequest } from '@/store/clarify'
|
||||
|
|
@ -70,6 +71,7 @@ const COMPACTION_RESUME_EVENT_TYPES = new Set([
|
|||
])
|
||||
|
||||
interface GatewayEventDeps {
|
||||
activeGatewayProfile: string
|
||||
activeSessionIdRef: MutableRefObject<string | null>
|
||||
compactedTurnRef: MutableRefObject<Set<string>>
|
||||
lastCwdInfoSessionRef: MutableRefObject<string | null>
|
||||
|
|
@ -102,6 +104,7 @@ export function useGatewayEventHandler(deps: GatewayEventDeps) {
|
|||
const {
|
||||
appendAssistantDelta,
|
||||
appendReasoningDelta,
|
||||
activeGatewayProfile,
|
||||
activeSessionIdRef,
|
||||
compactedTurnRef,
|
||||
lastCwdInfoSessionRef,
|
||||
|
|
@ -372,7 +375,8 @@ export function useGatewayEventHandler(deps: GatewayEventDeps) {
|
|||
|
||||
if (modelValueChanged || providerValueChanged) {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: explicitSid && sessionId ? ['model-options', sessionId] : ['model-options']
|
||||
queryKey:
|
||||
explicitSid && sessionId ? modelOptionsQueryKey(activeGatewayProfile, sessionId) : ['model-options']
|
||||
})
|
||||
}
|
||||
} else if (event.type === 'message.start') {
|
||||
|
|
@ -837,6 +841,7 @@ export function useGatewayEventHandler(deps: GatewayEventDeps) {
|
|||
appendAssistantDelta,
|
||||
appendReasoningDelta,
|
||||
activeSessionIdRef,
|
||||
activeGatewayProfile,
|
||||
compactedTurnRef,
|
||||
completeAssistantMessage,
|
||||
failAssistantMessage,
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import { useGatewayEventHandler } from './gateway-event'
|
|||
import { completionErrorText, delegateTaskPayloads, STREAM_DELTA_FLUSH_MS } from './utils'
|
||||
|
||||
interface MessageStreamOptions {
|
||||
activeGatewayProfile?: string
|
||||
activeSessionIdRef: MutableRefObject<string | null>
|
||||
hydrateFromStoredSession: (
|
||||
attempts?: number,
|
||||
|
|
@ -55,6 +56,7 @@ interface QueuedStreamDeltas {
|
|||
}
|
||||
|
||||
export function useMessageStream({
|
||||
activeGatewayProfile = 'default',
|
||||
activeSessionIdRef,
|
||||
hydrateFromStoredSession,
|
||||
queryClient,
|
||||
|
|
@ -613,6 +615,7 @@ export function useMessageStream({
|
|||
)
|
||||
|
||||
const handleGatewayEvent = useGatewayEventHandler({
|
||||
activeGatewayProfile,
|
||||
appendAssistantDelta,
|
||||
appendReasoningDelta,
|
||||
activeSessionIdRef,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|||
|
||||
import type { ClientSessionState } from '@/app/types'
|
||||
import { createClientSessionState } from '@/lib/chat-runtime'
|
||||
import { modelOptionsQueryKey } from '@/lib/model-options'
|
||||
import { setCurrentModel, setCurrentProvider } from '@/store/session'
|
||||
import type { RpcEvent } from '@/types/hermes'
|
||||
|
||||
|
|
@ -16,6 +17,7 @@ import { useMessageStream } from './index'
|
|||
// actually changed. message.complete must coalesce sidebar refreshes.
|
||||
|
||||
const ACTIVE_SID = 'session-active'
|
||||
const ACTIVE_PROFILE = 'compass'
|
||||
let handleEvent: ((event: RpcEvent) => void) | null = null
|
||||
let refreshHermesConfig: ReturnType<typeof vi.fn<() => Promise<void>>>
|
||||
let refreshSessions: ReturnType<typeof vi.fn<() => Promise<void>>>
|
||||
|
|
@ -26,6 +28,7 @@ function Harness() {
|
|||
const sessionStateByRuntimeIdRef = useRef(new Map<string, ClientSessionState>())
|
||||
|
||||
const stream = useMessageStream({
|
||||
activeGatewayProfile: ACTIVE_PROFILE,
|
||||
activeSessionIdRef,
|
||||
hydrateFromStoredSession: vi.fn(async () => undefined),
|
||||
queryClient,
|
||||
|
|
@ -132,7 +135,7 @@ describe('session.info model-options invalidation gating', () => {
|
|||
|
||||
sessionInfo(ACTIVE_SID, { model: 'm2', provider: 'p1', running: true })
|
||||
|
||||
expect(invalidate).toHaveBeenCalledWith({ queryKey: ['model-options', ACTIVE_SID] })
|
||||
expect(invalidate).toHaveBeenCalledWith({ queryKey: modelOptionsQueryKey(ACTIVE_PROFILE, ACTIVE_SID) })
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { cleanup, render, renderHook } from '@testing-library/react'
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { getGlobalModelInfo } from '@/hermes'
|
||||
import { modelOptionsQueryKey } from '@/lib/model-options'
|
||||
import { $activeGatewayProfile } from '@/store/profile'
|
||||
import {
|
||||
$activeSessionId,
|
||||
$currentModel,
|
||||
|
|
@ -79,6 +81,7 @@ function Harness({
|
|||
|
||||
describe('useModelControls', () => {
|
||||
beforeEach(() => {
|
||||
$activeGatewayProfile.set('default')
|
||||
$activeSessionId.set(null)
|
||||
setCurrentModel('')
|
||||
setCurrentModelSource('')
|
||||
|
|
@ -88,6 +91,7 @@ describe('useModelControls', () => {
|
|||
afterEach(() => {
|
||||
cleanup()
|
||||
vi.restoreAllMocks()
|
||||
$activeGatewayProfile.set('default')
|
||||
$activeSessionId.set(null)
|
||||
setCurrentModel('')
|
||||
setCurrentModelSource('')
|
||||
|
|
@ -201,6 +205,26 @@ describe('useModelControls', () => {
|
|||
expect(setGlobalModel).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('updates only the active profile new-chat cache', async () => {
|
||||
const queryClient = new QueryClient()
|
||||
$activeGatewayProfile.set('compass')
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useModelControls({
|
||||
queryClient,
|
||||
requestGateway: vi.fn()
|
||||
})
|
||||
)
|
||||
|
||||
await result.current.selectModel({ model: 'qwen3.6:35b-65k', provider: 'custom:local-ollama' })
|
||||
|
||||
expect(queryClient.getQueryData(modelOptionsQueryKey('compass'))).toMatchObject({
|
||||
model: 'qwen3.6:35b-65k',
|
||||
provider: 'custom:local-ollama'
|
||||
})
|
||||
expect(queryClient.getQueryData(modelOptionsQueryKey('default'))).toBeUndefined()
|
||||
})
|
||||
|
||||
it('seeds an empty composer model from global but never clobbers a pick', async () => {
|
||||
vi.mocked(getGlobalModelInfo).mockResolvedValue({ model: 'openai/gpt-5.5', provider: 'openai-codex' })
|
||||
|
||||
|
|
@ -232,7 +256,11 @@ describe('useModelControls', () => {
|
|||
vi.mocked(getGlobalModelInfo).mockResolvedValue({ model: 'openai/gpt-5.5', provider: 'openai-codex' })
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
queryClient.setQueryData(['model-options', 'global'], {
|
||||
$activeGatewayProfile.set('compass')
|
||||
queryClient.setQueryData(modelOptionsQueryKey('default'), {
|
||||
providers: [{ models: ['openrouter/owl-alpha'], name: 'OpenRouter', slug: 'openrouter' }]
|
||||
})
|
||||
queryClient.setQueryData(modelOptionsQueryKey('compass'), {
|
||||
providers: [{ models: ['openai/gpt-5.5'], name: 'OpenRouter', slug: 'openrouter' }]
|
||||
})
|
||||
|
||||
|
|
@ -253,7 +281,7 @@ describe('useModelControls', () => {
|
|||
vi.mocked(getGlobalModelInfo).mockResolvedValue({ model: 'openai/gpt-5.5', provider: 'openai-codex' })
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
queryClient.setQueryData(['model-options', 'global'], {
|
||||
queryClient.setQueryData(modelOptionsQueryKey('default'), {
|
||||
providers: [{ models: ['openrouter/glm-4.7', 'openai/gpt-5.5'], name: 'OpenRouter', slug: 'openrouter' }]
|
||||
})
|
||||
|
||||
|
|
@ -346,16 +374,16 @@ describe('useModelControls', () => {
|
|||
})
|
||||
|
||||
it('targets an explicit tile sessionId without clobbering the primary model', async () => {
|
||||
const queryClient = new QueryClient()
|
||||
$activeGatewayProfile.set('compass')
|
||||
$activeSessionId.set('primary-runtime')
|
||||
setCurrentModel('primary/model')
|
||||
setCurrentProvider('openai')
|
||||
const requestGateway = vi.fn(async () => ({ key: 'model', value: 'tile-model' }) as never)
|
||||
let controls!: Controls
|
||||
|
||||
render(<Harness onReady={value => (controls = value)} requestGateway={requestGateway} />)
|
||||
const { result } = renderHook(() => useModelControls({ queryClient, requestGateway }))
|
||||
|
||||
await expect(
|
||||
controls.selectModel({
|
||||
result.current.selectModel({
|
||||
model: 'tile-model',
|
||||
provider: 'anthropic',
|
||||
sessionId: 'tile-runtime'
|
||||
|
|
@ -370,5 +398,10 @@ describe('useModelControls', () => {
|
|||
// Primary footer untouched — the busy primary must not absorb a tile pick.
|
||||
expect($currentModel.get()).toBe('primary/model')
|
||||
expect($currentProvider.get()).toBe('openai')
|
||||
expect(queryClient.getQueryData(modelOptionsQueryKey('compass', 'tile-runtime'))).toMatchObject({
|
||||
model: 'tile-model',
|
||||
provider: 'anthropic'
|
||||
})
|
||||
expect(queryClient.getQueryData(modelOptionsQueryKey('default', 'tile-runtime'))).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@ import { useCallback, useRef } from 'react'
|
|||
import type { ModelSelection } from '@/app/shell/model-menu-panel'
|
||||
import { getGlobalModelInfo } from '@/hermes'
|
||||
import { useI18n } from '@/i18n'
|
||||
import { manualPickRemoved } from '@/lib/model-options'
|
||||
import { manualPickRemoved, modelOptionsQueryKey } from '@/lib/model-options'
|
||||
import { notifyError } from '@/store/notifications'
|
||||
import { $activeGatewayProfile } from '@/store/profile'
|
||||
import {
|
||||
$activeSessionId,
|
||||
$currentModel,
|
||||
|
|
@ -36,13 +37,19 @@ export function useModelControls({ queryClient, requestGateway }: ModelControlsO
|
|||
// callbacks once and never re-evaluate — a captured prop would be stale
|
||||
// forever. The store read is always current.
|
||||
const updateModelOptionsCache = useCallback(
|
||||
(sessionId: null | string, provider: string, model: string, includeGlobal: boolean) => {
|
||||
(
|
||||
sessionId: null | string,
|
||||
provider: string,
|
||||
model: string,
|
||||
includeGlobal: boolean,
|
||||
profile = $activeGatewayProfile.get()
|
||||
) => {
|
||||
const patch = (prev: ModelOptionsResponse | undefined) => ({ ...(prev ?? {}), provider, model })
|
||||
|
||||
queryClient.setQueryData<ModelOptionsResponse>(['model-options', sessionId || 'global'], patch)
|
||||
queryClient.setQueryData<ModelOptionsResponse>(modelOptionsQueryKey(profile, sessionId), patch)
|
||||
|
||||
if (includeGlobal) {
|
||||
queryClient.setQueryData<ModelOptionsResponse>(['model-options', 'global'], patch)
|
||||
queryClient.setQueryData<ModelOptionsResponse>(modelOptionsQueryKey(profile), patch)
|
||||
}
|
||||
},
|
||||
[queryClient]
|
||||
|
|
@ -78,7 +85,9 @@ export function useModelControls({ queryClient, requestGateway }: ModelControlsO
|
|||
return false
|
||||
}
|
||||
|
||||
const options = queryClient.getQueryData<ModelOptionsResponse>(['model-options', 'global'])
|
||||
const options = queryClient.getQueryData<ModelOptionsResponse>(
|
||||
modelOptionsQueryKey($activeGatewayProfile.get())
|
||||
)
|
||||
|
||||
return !manualPickRemoved(options?.providers, $currentProvider.get(), $currentModel.get())
|
||||
}
|
||||
|
|
@ -144,6 +153,7 @@ export function useModelControls({ queryClient, requestGateway }: ModelControlsO
|
|||
: ($sessionStates.get()[liveSessionId!]?.provider ?? '')
|
||||
|
||||
const prevSource = getCurrentModelSource()
|
||||
const liveGatewayProfile = $activeGatewayProfile.get()
|
||||
|
||||
if (touchesPrimary) {
|
||||
setCurrentModel(selection.model)
|
||||
|
|
@ -158,7 +168,13 @@ export function useModelControls({ queryClient, requestGateway }: ModelControlsO
|
|||
}))
|
||||
}
|
||||
|
||||
updateModelOptionsCache(liveSessionId, selection.provider, selection.model, touchesPrimary && !liveSessionId)
|
||||
updateModelOptionsCache(
|
||||
liveSessionId,
|
||||
selection.provider,
|
||||
selection.model,
|
||||
touchesPrimary && !liveSessionId,
|
||||
liveGatewayProfile
|
||||
)
|
||||
|
||||
// No live session yet: the pick is pure UI state. session.create reads
|
||||
// $currentModel/$currentProvider and applies it as that session's override.
|
||||
|
|
@ -173,7 +189,7 @@ export function useModelControls({ queryClient, requestGateway }: ModelControlsO
|
|||
value: `${selection.model} --provider ${selection.provider} --session`
|
||||
})
|
||||
|
||||
void queryClient.invalidateQueries({ queryKey: ['model-options', liveSessionId] })
|
||||
void queryClient.invalidateQueries({ queryKey: modelOptionsQueryKey(liveGatewayProfile, liveSessionId) })
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
|
|
@ -189,7 +205,13 @@ export function useModelControls({ queryClient, requestGateway }: ModelControlsO
|
|||
}))
|
||||
}
|
||||
|
||||
updateModelOptionsCache(liveSessionId, prevProvider, prevModel, touchesPrimary && !liveSessionId)
|
||||
updateModelOptionsCache(
|
||||
liveSessionId,
|
||||
prevProvider,
|
||||
prevModel,
|
||||
touchesPrimary && !liveSessionId,
|
||||
liveGatewayProfile
|
||||
)
|
||||
notifyError(err, copy.modelSwitchFailed)
|
||||
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import { Skeleton } from '@/components/ui/skeleton'
|
|||
import type { HermesGateway } from '@/hermes'
|
||||
import { useI18n } from '@/i18n'
|
||||
import { ChevronDown, ChevronRight } from '@/lib/icons'
|
||||
import { requestModelOptions } from '@/lib/model-options'
|
||||
import { modelOptionsQueryKey, requestModelOptions } from '@/lib/model-options'
|
||||
import {
|
||||
currentPickerSelection,
|
||||
displayModelName,
|
||||
|
|
@ -59,6 +59,7 @@ export interface ModelSelection {
|
|||
interface ModelMenuPanelProps {
|
||||
gateway?: HermesGateway
|
||||
onSelectModel: (selection: ModelSelection) => Promise<boolean> | void
|
||||
profile?: string
|
||||
requestGateway: <T>(method: string, params?: Record<string, unknown>) => Promise<T>
|
||||
}
|
||||
|
||||
|
|
@ -67,7 +68,7 @@ interface ProviderGroup {
|
|||
provider: ModelOptionProvider
|
||||
}
|
||||
|
||||
export function ModelMenuPanel({ gateway, onSelectModel, requestGateway }: ModelMenuPanelProps) {
|
||||
export function ModelMenuPanel({ gateway, onSelectModel, profile = 'default', requestGateway }: ModelMenuPanelProps) {
|
||||
const { t } = useI18n()
|
||||
const copy = t.shell.modelMenu
|
||||
const closeMenu = useContext(ModelMenuCloseContext)
|
||||
|
|
@ -87,7 +88,7 @@ export function ModelMenuPanel({ gateway, onSelectModel, requestGateway }: Model
|
|||
const collapsedProviders = useStore($collapsedProviders)
|
||||
|
||||
const modelOptions = useQuery({
|
||||
queryKey: ['model-options', activeSessionId || 'global'],
|
||||
queryKey: modelOptionsQueryKey(profile, activeSessionId),
|
||||
// Gateway-first even with no session yet: a connected (possibly remote)
|
||||
// gateway owns the model catalog, including virtual providers like `moa`
|
||||
// that the local REST fallback can't know about (#53817).
|
||||
|
|
@ -148,7 +149,7 @@ export function ModelMenuPanel({ gateway, onSelectModel, requestGateway }: Model
|
|||
setRefreshing(true)
|
||||
|
||||
try {
|
||||
const queryKey = ['model-options', activeSessionId || 'global']
|
||||
const queryKey = modelOptionsQueryKey(profile, activeSessionId)
|
||||
|
||||
const next = await requestModelOptions({ gateway, refresh: true, sessionId: activeSessionId })
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { useQuery } from '@tanstack/react-query'
|
|||
import { useState } from 'react'
|
||||
|
||||
import { useI18n } from '@/i18n'
|
||||
import { requestModelOptions } from '@/lib/model-options'
|
||||
import { modelOptionsQueryKey, requestModelOptions } from '@/lib/model-options'
|
||||
import { currentPickerSelection } from '@/lib/model-status-label'
|
||||
import { normalize } from '@/lib/text'
|
||||
import type { ModelOptionProvider, ModelPricing } from '@/types/hermes'
|
||||
|
|
@ -25,6 +25,7 @@ interface ModelPickerDialogProps {
|
|||
currentModel: string
|
||||
currentProvider: string
|
||||
onSelect: (selection: { provider: string; model: string }) => void
|
||||
profile?: string
|
||||
/**
|
||||
* Optional class to apply to DialogContent. Use to override z-index when
|
||||
* stacking the picker on top of another fixed overlay (e.g. the desktop
|
||||
|
|
@ -42,6 +43,7 @@ export function ModelPickerDialog({
|
|||
currentModel,
|
||||
currentProvider,
|
||||
onSelect,
|
||||
profile = 'default',
|
||||
contentClassName
|
||||
}: ModelPickerDialogProps) {
|
||||
const { t } = useI18n()
|
||||
|
|
@ -54,7 +56,7 @@ export function ModelPickerDialog({
|
|||
const [search, setSearch] = useState('')
|
||||
|
||||
const modelOptions = useQuery({
|
||||
queryKey: ['model-options', sessionId || 'global'],
|
||||
queryKey: modelOptionsQueryKey(profile, sessionId),
|
||||
queryFn: () => requestModelOptions({ gateway: gw, sessionId }),
|
||||
enabled: open
|
||||
})
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/u
|
|||
import { GlyphSpinner } from '@/components/ui/glyph-spinner'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import type { HermesGateway } from '@/hermes'
|
||||
import { getGlobalModelOptions } from '@/hermes'
|
||||
import { useI18n } from '@/i18n'
|
||||
import { modelOptionsQueryKey, requestModelOptions } from '@/lib/model-options'
|
||||
import { displayModelName, modelDisplayParts } from '@/lib/model-status-label'
|
||||
import { normalize } from '@/lib/text'
|
||||
import {
|
||||
|
|
@ -26,6 +26,7 @@ interface ModelVisibilityDialogProps {
|
|||
onOpenChange: (open: boolean) => void
|
||||
onOpenProviders: () => void
|
||||
open: boolean
|
||||
profile?: string
|
||||
sessionId?: string | null
|
||||
}
|
||||
|
||||
|
|
@ -34,6 +35,7 @@ export function ModelVisibilityDialog({
|
|||
onOpenChange,
|
||||
onOpenProviders,
|
||||
open,
|
||||
profile = 'default',
|
||||
sessionId
|
||||
}: ModelVisibilityDialogProps) {
|
||||
const { t } = useI18n()
|
||||
|
|
@ -42,17 +44,8 @@ export function ModelVisibilityDialog({
|
|||
const stored = useStore($visibleModels)
|
||||
|
||||
const modelOptions = useQuery({
|
||||
queryKey: ['model-options', sessionId || 'global'],
|
||||
queryFn: (): Promise<ModelOptionsResponse> => {
|
||||
if (gw && sessionId) {
|
||||
return gw.request<ModelOptionsResponse>('model.options', {
|
||||
session_id: sessionId,
|
||||
explicit_only: true
|
||||
})
|
||||
}
|
||||
|
||||
return getGlobalModelOptions()
|
||||
},
|
||||
queryKey: modelOptionsQueryKey(profile, sessionId),
|
||||
queryFn: (): Promise<ModelOptionsResponse> => requestModelOptions({ gateway: gw, sessionId }),
|
||||
enabled: open
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export function FlowPanel({
|
|||
}
|
||||
|
||||
if (flow.status === 'confirming_model') {
|
||||
return <ConfirmingModelPanel flow={flow} leaving={leaving} onBegin={onBegin} />
|
||||
return <ConfirmingModelPanel flow={flow} leaving={leaving} onBegin={onBegin} profile={ctx.profile} />
|
||||
}
|
||||
|
||||
if (flow.status === 'error') {
|
||||
|
|
@ -217,11 +217,13 @@ function CancelBtn({ size = 'default' }: { size?: 'default' | 'sm' }) {
|
|||
function ConfirmingModelPanel({
|
||||
flow,
|
||||
leaving,
|
||||
onBegin
|
||||
onBegin,
|
||||
profile
|
||||
}: {
|
||||
flow: Extract<OnboardingFlow, { status: 'confirming_model' }>
|
||||
leaving: boolean
|
||||
onBegin: () => void
|
||||
profile?: string
|
||||
}) {
|
||||
const { t } = useI18n()
|
||||
const scrambledModel = useScramble(flow.currentModel, leaving)
|
||||
|
|
@ -323,6 +325,7 @@ function ConfirmingModelPanel({
|
|||
setPickerOpen(false)
|
||||
}}
|
||||
open={pickerOpen}
|
||||
profile={profile}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ export {
|
|||
interface DesktopOnboardingOverlayProps {
|
||||
enabled: boolean
|
||||
onCompleted?: () => void
|
||||
profile: string
|
||||
requestGateway: OnboardingContext['requestGateway']
|
||||
}
|
||||
|
||||
|
|
@ -177,17 +178,25 @@ function useApiKeyCatalog(): ApiKeyOption[] {
|
|||
// → surface-out (520ms, held back by [transition-delay:660ms]). Finalize after.
|
||||
const ONBOARDING_EXIT_MS = 1180
|
||||
|
||||
export function DesktopOnboardingOverlay({ enabled, onCompleted, requestGateway }: DesktopOnboardingOverlayProps) {
|
||||
export function DesktopOnboardingOverlay({
|
||||
enabled,
|
||||
onCompleted,
|
||||
profile,
|
||||
requestGateway
|
||||
}: DesktopOnboardingOverlayProps) {
|
||||
const { t } = useI18n()
|
||||
const onboarding = useStore($desktopOnboarding)
|
||||
const boot = useStore($desktopBoot)
|
||||
const ctxRef = useRef<OnboardingContext>({ requestGateway, onCompleted })
|
||||
ctxRef.current = { requestGateway, onCompleted }
|
||||
const ctxRef = useRef<OnboardingContext>({ requestGateway, onCompleted, profile })
|
||||
ctxRef.current = { requestGateway, onCompleted, profile }
|
||||
|
||||
const ctx = useMemo<OnboardingContext>(
|
||||
() => ({
|
||||
requestGateway: (...args) => ctxRef.current.requestGateway(...args),
|
||||
onCompleted: () => ctxRef.current.onCompleted?.()
|
||||
onCompleted: () => ctxRef.current.onCompleted?.(),
|
||||
get profile() {
|
||||
return ctxRef.current.profile
|
||||
}
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
|
|||
|
||||
import { getGlobalModelOptions } from '@/hermes'
|
||||
|
||||
import { manualPickRemoved, requestModelOptions } from './model-options'
|
||||
import { manualPickRemoved, modelOptionsQueryKey, requestModelOptions } from './model-options'
|
||||
|
||||
const globalOptions = { model: 'hermes-4', provider: 'nous', providers: [] }
|
||||
|
||||
|
|
@ -49,6 +49,18 @@ describe('requestModelOptions', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('modelOptionsQueryKey', () => {
|
||||
it('isolates new-chat catalogs by active gateway profile', () => {
|
||||
expect(modelOptionsQueryKey('default')).toEqual(['model-options', 'default', 'global'])
|
||||
expect(modelOptionsQueryKey('compass')).toEqual(['model-options', 'compass', 'global'])
|
||||
expect(modelOptionsQueryKey('default')).not.toEqual(modelOptionsQueryKey('compass'))
|
||||
})
|
||||
|
||||
it('keeps session catalogs inside the owning profile namespace', () => {
|
||||
expect(modelOptionsQueryKey(' compass ', 'session-1')).toEqual(['model-options', 'compass', 'session-1'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('manualPickRemoved', () => {
|
||||
const providers = [
|
||||
{ name: 'OpenRouter', slug: 'openrouter', models: ['owl-alpha', 'gpt-5.5'] },
|
||||
|
|
|
|||
|
|
@ -44,6 +44,12 @@ interface ModelOptionsRequest {
|
|||
sessionId?: null | string
|
||||
}
|
||||
|
||||
export function modelOptionsQueryKey(profile: null | string | undefined, sessionId?: null | string) {
|
||||
const profileKey = (profile ?? '').trim() || 'default'
|
||||
|
||||
return ['model-options', profileKey, sessionId || 'global'] as const
|
||||
}
|
||||
|
||||
export function requestModelOptions({
|
||||
explicitOnly = true,
|
||||
gateway,
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ export interface DesktopOnboardingState {
|
|||
|
||||
export interface OnboardingContext {
|
||||
onCompleted?: () => void
|
||||
profile?: string
|
||||
requestGateway: <T = unknown>(method: string, params?: Record<string, unknown>) => Promise<T>
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import time
|
|||
import types
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
|
@ -8156,6 +8156,132 @@ def test_model_options_hides_unconfigured_providers_by_default(monkeypatch):
|
|||
assert calls[-1]["include_unconfigured"] is True
|
||||
|
||||
|
||||
def test_model_options_preserves_canonical_custom_row_after_agent_init(monkeypatch):
|
||||
from hermes_cli.inventory import ConfigContext
|
||||
|
||||
class _Agent:
|
||||
provider = "custom"
|
||||
model = "qwen3.6:35b-65k"
|
||||
base_url = "http://127.0.0.1:11434/v1"
|
||||
|
||||
server._sessions["custom-session"] = _session(agent=_Agent())
|
||||
monkeypatch.setattr(server, "_resolve_model", lambda: "")
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.inventory.load_picker_context",
|
||||
lambda: ConfigContext(
|
||||
current_provider="custom:local-ollama",
|
||||
current_model="qwen3.6:35b-65k",
|
||||
current_base_url="http://127.0.0.1:11434/v1",
|
||||
user_providers={},
|
||||
custom_providers=[],
|
||||
),
|
||||
)
|
||||
canonical = Mock(return_value="custom:local-ollama")
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.runtime_provider.canonical_custom_identity",
|
||||
canonical,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.model_switch.list_authenticated_providers",
|
||||
lambda **_kwargs: [
|
||||
{
|
||||
"slug": "custom:local-ollama",
|
||||
"name": "Local Ollama",
|
||||
"is_current": True,
|
||||
"is_user_defined": True,
|
||||
"models": ["qwen3.6:35b-65k"],
|
||||
"total_models": 1,
|
||||
},
|
||||
{
|
||||
"slug": "anthropic",
|
||||
"name": "Anthropic",
|
||||
"is_current": False,
|
||||
"is_user_defined": False,
|
||||
"models": ["claude-sonnet-4.6"],
|
||||
"total_models": 1,
|
||||
},
|
||||
],
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.auth.is_provider_explicitly_configured",
|
||||
lambda _slug: False,
|
||||
)
|
||||
monkeypatch.setattr("hermes_cli.inventory._apply_pricing", lambda *_args, **_kwargs: None)
|
||||
monkeypatch.setattr("hermes_cli.inventory._apply_capabilities", lambda *_args, **_kwargs: None)
|
||||
|
||||
resp = server._methods["model.options"](
|
||||
102,
|
||||
{"session_id": "custom-session", "explicit_only": True},
|
||||
)
|
||||
|
||||
assert "result" in resp, resp
|
||||
assert resp["result"]["provider"] == "custom:local-ollama"
|
||||
assert [row["slug"] for row in resp["result"]["providers"]] == [
|
||||
"custom:local-ollama"
|
||||
]
|
||||
canonical.assert_called_once_with(
|
||||
base_url="http://127.0.0.1:11434/v1",
|
||||
config_provider="custom:local-ollama",
|
||||
)
|
||||
|
||||
|
||||
def test_model_save_key_uses_credential_lifecycle_and_picker_context(monkeypatch):
|
||||
env_var = "TEST_PROVIDER_API_KEY"
|
||||
agent = object()
|
||||
picker_ctx = object()
|
||||
provider = {
|
||||
"slug": "test-provider",
|
||||
"name": "Test Provider",
|
||||
"models": ["test-model"],
|
||||
"total_models": 1,
|
||||
}
|
||||
server._sessions["save-key-session"] = _session(agent=agent)
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.auth.PROVIDER_REGISTRY",
|
||||
{
|
||||
"test-provider": types.SimpleNamespace(
|
||||
name="Test Provider",
|
||||
auth_type="api_key",
|
||||
api_key_env_vars=(env_var,),
|
||||
)
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr("hermes_cli.config.is_managed", lambda: False)
|
||||
save_credential = Mock()
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.credential_lifecycle.save_provider_env_credential",
|
||||
save_credential,
|
||||
)
|
||||
picker_context = Mock(return_value=picker_ctx)
|
||||
monkeypatch.setattr(server, "_model_picker_context", picker_context)
|
||||
build_payload = Mock(return_value={"providers": [provider]})
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.inventory.build_models_payload",
|
||||
build_payload,
|
||||
)
|
||||
monkeypatch.setenv(env_var, "previous-value")
|
||||
fake_key = "replacement-" + "value"
|
||||
|
||||
resp = server._methods["model.save_key"](
|
||||
103,
|
||||
{
|
||||
"slug": "test-provider",
|
||||
"api_key": fake_key,
|
||||
"session_id": "save-key-session",
|
||||
},
|
||||
)
|
||||
|
||||
assert "result" in resp, resp
|
||||
assert resp["result"]["provider"] == {**provider, "authenticated": True}
|
||||
save_credential.assert_called_once_with(env_var, fake_key)
|
||||
picker_context.assert_called_once_with(agent)
|
||||
build_payload.assert_called_once_with(
|
||||
picker_ctx,
|
||||
picker_hints=True,
|
||||
max_models=50,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# prompt.submit — auto-title
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -14542,10 +14542,42 @@ def _(rid, params: dict) -> dict:
|
|||
return _err(rid, 5020, str(e))
|
||||
|
||||
|
||||
def _model_picker_context(agent):
|
||||
"""Layer live session state onto config without losing custom identity."""
|
||||
from hermes_cli.inventory import load_picker_context
|
||||
|
||||
ctx = load_picker_context()
|
||||
provider = getattr(agent, "provider", "") if agent else ""
|
||||
base_url = getattr(agent, "base_url", "") if agent else ""
|
||||
if str(provider or "").strip().lower() == "custom":
|
||||
try:
|
||||
from hermes_cli.runtime_provider import canonical_custom_identity
|
||||
|
||||
provider = (
|
||||
canonical_custom_identity(
|
||||
base_url=base_url or None,
|
||||
config_provider=ctx.current_provider,
|
||||
)
|
||||
or provider
|
||||
)
|
||||
except Exception:
|
||||
logger.debug(
|
||||
"custom provider identity recovery failed (model picker)",
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
return ctx.with_overrides(
|
||||
current_provider=provider,
|
||||
current_model=(getattr(agent, "model", "") if agent else "")
|
||||
or _resolve_model(),
|
||||
current_base_url=base_url,
|
||||
)
|
||||
|
||||
|
||||
@method("model.options")
|
||||
def _(rid, params: dict) -> dict:
|
||||
try:
|
||||
from hermes_cli.inventory import build_models_payload, load_picker_context
|
||||
from hermes_cli.inventory import build_models_payload
|
||||
|
||||
session = _sessions.get(params.get("session_id", ""))
|
||||
agent = session.get("agent") if session else None
|
||||
|
|
@ -14553,13 +14585,7 @@ def _(rid, params: dict) -> dict:
|
|||
# is spawned, IT owns the live provider/model/base_url. Empty
|
||||
# agent attributes must NOT clobber disk config (with_overrides
|
||||
# is truthy-only).
|
||||
ctx = load_picker_context().with_overrides(
|
||||
current_provider=getattr(agent, "provider", "") if agent else "",
|
||||
current_model=(
|
||||
(getattr(agent, "model", "") if agent else "") or _resolve_model()
|
||||
),
|
||||
current_base_url=getattr(agent, "base_url", "") if agent else "",
|
||||
)
|
||||
ctx = _model_picker_context(agent)
|
||||
# picker_hints + canonical_order produce the TUI/desktop picker shape:
|
||||
# `authenticated`/`auth_type`/`key_env`/`warning` per row, in
|
||||
# CANONICAL_PROVIDERS declaration order. Desktop pickers default to the
|
||||
|
|
@ -14599,7 +14625,7 @@ def _(rid, params: dict) -> dict:
|
|||
try:
|
||||
from hermes_cli.auth import PROVIDER_REGISTRY
|
||||
from hermes_cli.config import is_managed
|
||||
from hermes_cli.inventory import build_models_payload, load_picker_context
|
||||
from hermes_cli.inventory import build_models_payload
|
||||
|
||||
slug = (params.get("slug") or "").strip()
|
||||
api_key = (params.get("api_key") or "").strip()
|
||||
|
|
@ -14640,13 +14666,7 @@ def _(rid, params: dict) -> dict:
|
|||
# carries `authenticated` for the TUI frontend.
|
||||
session = _sessions.get(params.get("session_id", ""))
|
||||
agent = session.get("agent") if session else None
|
||||
ctx = load_picker_context().with_overrides(
|
||||
current_provider=getattr(agent, "provider", "") if agent else "",
|
||||
current_model=(
|
||||
(getattr(agent, "model", "") if agent else "") or _resolve_model()
|
||||
),
|
||||
current_base_url=getattr(agent, "base_url", "") if agent else "",
|
||||
)
|
||||
ctx = _model_picker_context(agent)
|
||||
payload = build_models_payload(
|
||||
ctx, picker_hints=True, max_models=50,
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue