import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Skeleton } from '@/components/ui/skeleton' import { Switch } from '@/components/ui/switch' import { getAuxiliaryModels, getGlobalModelInfo, getGlobalModelOptions, getMoaModels, getRecommendedDefaultModel, saveHermesConfig, saveMoaModels, setEnvVar, setModelAssignment } from '@/hermes' import type { AuxiliaryModelsResponse, MoaConfigResponse, MoaModelSlot, ModelOptionProvider, StaleAuxAssignment } from '@/hermes' import { useI18n } from '@/i18n' import { isCodeSkewRestartRequired } from '@/lib/code-skew-error' import { AlertTriangle, Cpu, Loader2 } from '@/lib/icons' import { DEFAULT_REASONING_EFFORT, REASONING_EFFORT_VALUES } from '@/lib/reasoning-effort' import { cn } from '@/lib/utils' import { setMainModelAssignment } from '@/store/cron-model-impact' import { notifyError, readableError } from '@/store/notifications' import { startManualLocalEndpoint, startManualOnboarding, startManualProviderOAuth } from '@/store/onboarding' import { hermesConfigCacheWriter, invalidateHermesConfig, useHermesConfigRecord } from '../hooks/use-config-record' import { useOnProfileSwitch } from '../hooks/use-on-profile-switch' import { CONTROL_TEXT } from './constants' import { getNested, setNested } from './helpers' import { ListRow, Pill, SectionHeading } from './primitives' import { useDeepLinkHighlight } from './use-deep-link-highlight' // Skeleton mirror of the Model settings DOM so the page keeps its shape while // the provider/model catalog loads, instead of collapsing to a centered // spinner. Same containers/rhythm as the real render below. export function ModelSettingsSkeleton() { return (
{[0, 1, 2, 3].map(row => (
))}
) } // agent.service_tier stores "fast"/"priority"/"on" for fast; anything else is // normal (mirrors tui_gateway _load_service_tier). const isFastTier = (tier: unknown): boolean => ['fast', 'priority', 'on'].includes( String(tier ?? '') .trim() .toLowerCase() ) // A provider row is "ready" to pick a model from when it reports models. The // backend now surfaces the full `hermes model` universe (every canonical // provider), so unconfigured providers come back with `authenticated:false` // and an empty `models` list — those need a setup step before a model exists. function isProviderReady(p?: ModelOptionProvider): boolean { return !!p && (p.authenticated !== false || (p.models?.length ?? 0) > 0) } // Mirrors `_AUX_TASK_SLOTS` in hermes_cli/web_server.py. Friendly labels and // hints make the assignments readable; raw task keys (vision, mcp, …) are // opaque to most users. interface AuxTaskMeta { key: string } const AUX_TASKS: readonly AuxTaskMeta[] = [ { key: 'vision' }, { key: 'compression' }, { key: 'skills_hub' }, { key: 'approval' }, { key: 'mcp' }, { key: 'title_generation' }, { key: 'review' }, { key: 'curator' } ] const NO_PROVIDERS: readonly ModelOptionProvider[] = [{ name: '—', slug: '', models: [] }] // Radix {mainProviderOptions.map(provider => ( {provider.name} ))} {needsSetup ? ( setupIsApiKey ? ( <> setApiKeyDraft(event.target.value)} onKeyDown={event => { if (event.key === 'Enter') { void activateApiKeyProvider() } }} placeholder={t.settings.runtime.pasteProviderKey( selectedProviderRow?.key_env ?? t.settings.runtime.apiKey )} type="password" value={apiKeyDraft} /> ) : ( ) ) : ( <> )} {needsSetup && !setupIsApiKey && selectedProviderRow && (

{selectedProviderRow?.auth_type === 'api_key' ? `${selectedProviderRow?.name} needs an API key — set it up to choose a model.` : `${selectedProviderRow?.name} signs in through your browser — Hermes runs the flow for you.`}

)} {config && mainModel && (reasoningSupported || fastSupported) && (
{m.defaultsLabel} {reasoningSupported && (
{m.reasoning}
)} {fastSupported && ( )}
)} {error && (
{error} {skewRestart && ( )}
)} {switchStaleAux.length > 0 && (
void resetAuxiliaryModels()} slots={switchStaleAux} taskLabel={auxiliaryTaskLabel} />
)}

{m.auxiliaryDesc}

{switchStaleAux.length === 0 && persistentStaleAux.length > 0 && (
void resetAuxiliaryModels()} slots={persistentStaleAux} taskLabel={auxiliaryTaskLabel} />
)}
{AUX_TASKS.map(meta => { const copy = m.tasks[meta.key] ?? { label: meta.key, hint: meta.key } const current = auxiliary?.tasks.find(entry => entry.task === meta.key) const isAuto = !current || !current.provider || current.provider === 'auto' const isEditing = editingAuxTask === meta.key return (
) } below={ isEditing && (
) } description={ {isAuto ? m.autoUseMain : `${current.provider} · ${current.model || m.providerDefault}`} } title={ {copy.label} {copy.hint} } />
) })}
{moa && currentMoaPreset && (

{t.settings.runtime.moa.description}

setNewMoaPresetName(event.target.value)} placeholder={t.settings.runtime.moa.newPreset} value={newMoaPresetName} />
{t.settings.runtime.moa.default} {moa.default_preset}
{currentMoaPreset.reference_models.map((slot, index) => ( updateMoaPreset(prev => ({ ...prev, reference_models: prev.reference_models.map((s, i) => i === index ? { ...s, enabled: checked === true } : s ) })) } /> } below={
} className={cn(slot.enabled === false && 'opacity-60')} description={ {slot.provider} · {slot.model || m.model} } key={`${selectedMoaPreset}-${index}`} title={t.settings.runtime.moa.reference(index + 1)} /> ))}
} description={ {currentMoaPreset.aggregator.provider} · {currentMoaPreset.aggregator.model} } title={t.settings.runtime.moa.aggregator} />
)} ) }