Persist custom Hermes models (#913)

This commit is contained in:
ekko
2026-05-21 20:55:19 +08:00
committed by GitHub
parent 4d89767847
commit ff1f471745
9 changed files with 418 additions and 13 deletions
@@ -22,6 +22,11 @@ export interface AppConfig {
// These aliases never replace the canonical model ID sent back to Hermes.
modelAliases?: Record<string, Record<string, string>>
// Web UI-only manually entered model IDs. Keys are provider -> model IDs.
// This lets users persist provider-supported models that are absent from a
// provider catalog response without changing Hermes Agent config.yaml.
customModels?: Record<string, string[]>
// Web UI-only model picker visibility. This filters what the WUI exposes in
// its sidebar/model pages and never renames or rewrites Hermes canonical
// provider/model IDs. Hermes CLI config remains the upstream source of truth.
@@ -30,6 +30,41 @@ import { filterBridgeToolCallMarkupDelta } from './bridge-delta'
const BRIDGE_USAGE_FLUSH_DELAY_MS = 200
function stringValue(value: unknown): string {
return typeof value === 'string' ? value.trim() : ''
}
function looksLikeAgentFailure(value: string): boolean {
return /\bAPI call failed after\b/i.test(value)
|| /\bHTTP\s+(?:4\d\d|5\d\d)\b/i.test(value)
|| /\b(?:401|403|429|500|502|503|504)\b/.test(value) && /\b(?:unauthorized|forbidden|rate limit|unavailable|failed|error)\b/i.test(value)
}
export function bridgeTerminalError(chunk: Pick<AgentBridgeOutput, 'status' | 'error' | 'result'>): string | null {
const result = chunk.result && typeof chunk.result === 'object' && !Array.isArray(chunk.result)
? chunk.result as Record<string, unknown>
: null
const resultError = result
? stringValue(result.error)
|| stringValue(result.exception)
|| stringValue(result.message)
: ''
const finalResponse = result ? stringValue(result.final_response) : ''
if (chunk.status === 'error') {
return stringValue(chunk.error) || resultError || finalResponse || 'Agent run failed'
}
if (result?.failed === true || result?.completed === false) {
return resultError || finalResponse || 'Agent reported failure'
}
if (resultError) return resultError
if (finalResponse && looksLikeAgentFailure(finalResponse)) return finalResponse
return null
}
export async function handleBridgeRun(
nsp: ReturnType<Server['of']>,
socket: Socket,
@@ -491,13 +526,14 @@ async function applyBridgeChunkAsync(
state.runId = undefined
state.activeRunMarker = undefined
state.events = []
const eventName = chunk.status === 'error' ? 'run.failed' : 'run.completed'
const terminalError = bridgeTerminalError(chunk)
const eventName = terminalError ? 'run.failed' : 'run.completed'
const payload = {
event: eventName,
run_id: chunk.run_id,
output: chunk.output || state.bridgeOutput || '',
result: chunk.result,
error: chunk.error,
error: terminalError || chunk.error,
inputTokens: usage.inputTokens,
outputTokens: usage.outputTokens,
queue_remaining: state.queue.length,