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>
134 lines
3.8 KiB
TypeScript
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
|
|
})
|
|
}
|