mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-07 13:02:07 +00:00
Each model remembers its own reasoning effort / fast mode (localStorage,
like model-visibility): editing a model's effort/fast in the submenu
writes its preset, and selecting a model restores its preset onto the
session (capability-gated, Hermes defaults when unset). Every row shows
its own remembered settings (grayed), and the row label and edit submenu
read the same effective value so they can't disagree.
Presets are desktop-client state only — applyModelPreset() no-ops without
a live session id, so selecting a model can't fall through to the
gateway's persistent agent.reasoning_effort / agent.service_tier writes.
Inactive variant `-fast` edits stay preset-only: toggleFast() records
{ fast } on the base model and only swaps models when the row is active,
and selectFamily() honors a saved variant-fast preset by selecting the
`-fast` sibling id.
86 lines
2.8 KiB
TypeScript
86 lines
2.8 KiB
TypeScript
import { atom } from 'nanostores'
|
|
|
|
import { persistString, storedString } from '@/lib/storage'
|
|
|
|
import { notifyError } from './notifications'
|
|
import { setCurrentFastMode, setCurrentReasoningEffort } from './session'
|
|
|
|
const STORAGE_KEY = 'hermes.desktop.model-presets'
|
|
|
|
/** Per-model reasoning/fast preset, remembered globally across sessions and
|
|
* re-applied to the session whenever that model is selected. Unset dimensions
|
|
* fall back to the Hermes default (medium effort, no fast). */
|
|
export interface ModelPreset {
|
|
effort?: string
|
|
fast?: boolean
|
|
}
|
|
|
|
type RequestGateway = <T>(method: string, params?: Record<string, unknown>) => Promise<T>
|
|
|
|
/** Stable `provider::model` key (matches the visibility-store format). */
|
|
export const modelPresetKey = (provider: string, model: string): string => `${provider}::${model}`
|
|
|
|
function load(): Record<string, ModelPreset> {
|
|
const raw = storedString(STORAGE_KEY)
|
|
|
|
if (!raw) {
|
|
return {}
|
|
}
|
|
|
|
try {
|
|
const parsed = JSON.parse(raw)
|
|
|
|
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? (parsed as Record<string, ModelPreset>) : {}
|
|
} catch {
|
|
return {}
|
|
}
|
|
}
|
|
|
|
export const $modelPresets = atom<Record<string, ModelPreset>>(load())
|
|
|
|
export function getModelPreset(provider: string, model: string): ModelPreset {
|
|
return $modelPresets.get()[modelPresetKey(provider, model)] ?? {}
|
|
}
|
|
|
|
/** Merge a partial preset for one model and persist. */
|
|
export function setModelPreset(provider: string, model: string, patch: ModelPreset): void {
|
|
const key = modelPresetKey(provider, model)
|
|
const next = { ...$modelPresets.get(), [key]: { ...$modelPresets.get()[key], ...patch } }
|
|
|
|
$modelPresets.set(next)
|
|
persistString(STORAGE_KEY, JSON.stringify(next))
|
|
}
|
|
|
|
/** Push a model's preset onto the active session (optimistic + gateway).
|
|
* `undefined` skips that dimension; values are capability-gated upstream.
|
|
* No-ops without a session — the gateway's `config.set` reasoning/fast fall
|
|
* back to persistent (global/profile) config when none matches, so selecting
|
|
* a model must not reach it (else it rewrites `agent.*`, defaults included). */
|
|
export async function applyModelPreset(
|
|
{ effort, fast }: ModelPreset,
|
|
ctx: { failMessage: string; request: RequestGateway; sessionId: null | string }
|
|
): Promise<void> {
|
|
if (!ctx.sessionId) {
|
|
return
|
|
}
|
|
|
|
if (effort !== undefined) {
|
|
setCurrentReasoningEffort(effort)
|
|
}
|
|
|
|
if (fast !== undefined) {
|
|
setCurrentFastMode(fast)
|
|
}
|
|
|
|
try {
|
|
if (effort !== undefined) {
|
|
await ctx.request('config.set', { key: 'reasoning', session_id: ctx.sessionId, value: effort })
|
|
}
|
|
|
|
if (fast !== undefined) {
|
|
await ctx.request('config.set', { key: 'fast', session_id: ctx.sessionId, value: fast ? 'fast' : 'normal' })
|
|
}
|
|
} catch (err) {
|
|
notifyError(err, ctx.failMessage)
|
|
}
|
|
}
|