Files
hermes-agent/apps/desktop/src/api/models.ts
Hermes Agent 2dcb8d7562 fix(desktop): degrade model settings when /api/model/info hangs
ModelSettings.refresh() wrapped all four reads in one Promise.all, and
only getMoaModels had a .catch — when the provider backend was
unreachable, getGlobalModelInfo never settled and the page showed
skeletons forever while the fast config-file reads (auxiliary, MOA)
sat finished but unrendered.

Switch to Promise.allSettled with per-call degradation: the main-model
assignment falls back to the auxiliary config read, the provider
catalog and aux/MOA sections render whatever succeeded, and the
failures surface in the load banner. getGlobalModelInfo also drops the
60s startup timeout for its own 5s budget so the client stops waiting
on a hung probe.

Fixes https://github.com/NousResearch/hermes-agent/issues/63214 (frontend half)

Co-Authored-By: embwl0x <embwl0x@users.noreply.github.com>
2026-09-25 17:17:48 -05:00

134 lines
3.8 KiB
TypeScript

import type { ModelOptionsResult } from '@hermes/shared'
import type {
AnalyticsResponse,
AuxiliaryModelsResponse,
MoaConfigResponse,
ModelAssignmentRequest,
ModelAssignmentResponse,
ModelInfoResponse
} from '@/types/hermes'
import { capabilityScoped, hermesApi, type ProfileScope, profileScoped, STARTUP_REQUEST_TIMEOUT_MS } from './client'
// /api/model/info resolves the live context window, which probes the configured
// provider's /models endpoint. An unreachable provider must not hold the Model
// Settings page hostage (the backend bounds the same probe; this is the client
// side of that budget).
const MODEL_INFO_REQUEST_TIMEOUT_MS = 5_000
export function getGlobalModelInfo(profile?: null | string): Promise<ModelInfoResponse> {
return hermesApi<ModelInfoResponse>({
...profileScoped(profile),
path: '/api/model/info',
timeoutMs: MODEL_INFO_REQUEST_TIMEOUT_MS
})
}
export function getUsageAnalytics(days = 30, profile?: ProfileScope): Promise<AnalyticsResponse> {
return window.hermesDesktop.api<AnalyticsResponse>({
...capabilityScoped(profile),
path: `/api/analytics/usage?days=${Math.max(1, Math.floor(days))}`
})
}
export function getGlobalModelOptions(
opts?: {
refresh?: boolean
includeUnconfigured?: boolean
explicitOnly?: boolean
},
profile?: ProfileScope
): Promise<ModelOptionsResult> {
const params = new URLSearchParams()
if (opts?.refresh) {
params.set('refresh', '1')
}
if (opts?.includeUnconfigured) {
params.set('include_unconfigured', '1')
}
if (opts?.explicitOnly !== false) {
params.set('explicit_only', '1')
}
return window.hermesDesktop.api<ModelOptionsResult>({
...capabilityScoped(profile),
path: params.size > 0 ? `/api/model/options?${params.toString()}` : '/api/model/options',
timeoutMs: STARTUP_REQUEST_TIMEOUT_MS
})
}
export interface RecommendedDefaultModel {
provider: string
model: string
/** True/false for Nous (free vs paid tier); null for other providers. */
free_tier: boolean | null
}
// Recommended default model for a freshly-authenticated provider. Mirrors the
// curation `hermes model` does — for Nous it honors the free/paid tier so a
// free user gets a free model instead of a paid default.
export function getRecommendedDefaultModel(provider: string, profile?: ProfileScope): Promise<RecommendedDefaultModel> {
return window.hermesDesktop.api<RecommendedDefaultModel>({
...capabilityScoped(profile),
path: `/api/model/recommended-default?provider=${encodeURIComponent(provider)}`
})
}
export function setGlobalModel(
provider: string,
model: string
): Promise<{ ok: boolean; provider: string; model: string }> {
return hermesApi<{ ok: boolean; provider: string; model: string }>({
...profileScoped(),
path: '/api/model/set',
method: 'POST',
body: {
scope: 'main',
provider,
model
}
})
}
export function getAuxiliaryModels(profile?: null | string): Promise<AuxiliaryModelsResponse> {
return hermesApi<AuxiliaryModelsResponse>({
...profileScoped(profile),
path: '/api/model/auxiliary'
})
}
export function getMoaModels(profile?: null | string): Promise<MoaConfigResponse> {
return hermesApi<MoaConfigResponse>({
...profileScoped(profile),
path: '/api/model/moa'
})
}
export function saveMoaModels(
body: MoaConfigResponse,
profile?: null | string
): Promise<MoaConfigResponse & { ok: boolean }> {
return hermesApi<MoaConfigResponse & { ok: boolean }>({
...profileScoped(profile),
path: '/api/model/moa',
method: 'PUT',
body
})
}
export function setModelAssignment(
body: ModelAssignmentRequest,
profile?: ProfileScope
): Promise<ModelAssignmentResponse> {
return window.hermesDesktop.api<ModelAssignmentResponse>({
...capabilityScoped(profile),
path: '/api/model/set',
method: 'POST',
body
})
}