fix(#569): merge providers.*.models and model_aliases from config.yaml

The Workspace model picker only saw entries from ~/.hermes/models.json,
the gateway /v1/models endpoint, and local provider discovery (Ollama,
Atomic Chat). Hermes Agent's actual catalog lives in ~/.hermes/config.yaml
under providers.<id>.models, providers.<id>.model, and model_aliases.
In setups where /v1/models intentionally returns only 'hermes-agent',
this meant the picker showed maybe 4 models out of the ~60 the user
had configured.

Add readClaudeConfigCatalog() to /api/models that walks providers.*.models
(strings or {id,name,provider} objects), each provider's default model,
and model_aliases (mapped to {id: alias, target: '<provider>/<model>'}).
Merge those entries via mergeModelEntries() so existing dedup and ordering
behavior is preserved, and append '+config.yaml' to the source label so
the UI/debug can tell where models came from.
This commit is contained in:
Aurora
2026-06-05 11:15:10 -04:00
parent 0a6d1bccb0
commit cf16f9a5fe

View File

@@ -12,8 +12,8 @@ import {
import { BEARER_TOKEN, CLAUDE_API } from '../../server/gateway-capabilities'
import {
ensureDiscovery,
getDiscoveredModels,
ensureProviderInConfig,
getDiscoveredModels,
} from '../../server/local-provider-discovery'
const CLAUDE_HOME = process.env.HERMES_HOME ?? process.env.CLAUDE_HOME ?? path.join(os.homedir(), '.hermes')
@@ -169,6 +169,87 @@ function readClaudeDefaultModel(): ModelEntry | null {
}
}
/**
* Read providers.*.models (+ provider default model) and model_aliases
* from ~/.hermes/config.yaml so the picker reflects the user's full Hermes
* catalog, not just /v1/models + models.json + local discovery. Fix for #569.
*/
function readClaudeConfigCatalog(): Array<ModelEntry> {
try {
if (!fs.existsSync(CONFIG_PATH)) return []
const raw = fs.readFileSync(CONFIG_PATH, 'utf-8')
const parsed = YAML.parse(raw)
if (!parsed || typeof parsed !== 'object') return []
const config = parsed as Record<string, unknown>
const out: Array<ModelEntry> = []
const seen = new Set<string>()
const pushEntry = (entry: ModelEntry) => {
if (!entry.id || seen.has(entry.id)) return
out.push(entry)
seen.add(entry.id)
}
const providers = asRecord(config.providers)
for (const [providerId, value] of Object.entries(providers)) {
const providerBlock = asRecord(value)
const providerModels = providerBlock.models
if (Array.isArray(providerModels)) {
for (const modelEntry of providerModels) {
if (typeof modelEntry === 'string') {
const id = modelEntry.trim()
if (!id) continue
pushEntry({ id, name: id, provider: providerId })
} else {
const record = asRecord(modelEntry)
const id =
readString(record.id) ||
readString(record.model) ||
readString(record.name)
if (!id) continue
pushEntry({
id,
name: readString(record.name) || id,
provider: readString(record.provider) || providerId,
})
}
}
}
const providerDefault =
readString(providerBlock.model) || readString(providerBlock.default)
if (providerDefault) {
pushEntry({
id: providerDefault,
name: providerDefault,
provider: providerId,
})
}
}
const aliases = asRecord(config.model_aliases)
for (const [alias, target] of Object.entries(aliases)) {
const aliasId = alias.trim()
if (!aliasId) continue
const targetStr = typeof target === 'string' ? target.trim() : ''
const provider =
targetStr && targetStr.includes('/')
? targetStr.split('/')[0]
: 'alias'
pushEntry({
id: aliasId,
name: targetStr ? `${aliasId}${targetStr}` : aliasId,
provider,
alias: true,
target: targetStr || undefined,
})
}
return out
} catch {
return []
}
}
/**
* Fallback: fetch models from the hermes-agent /v1/models endpoint.
*/
@@ -210,6 +291,19 @@ export const Route = createFileRoute('/api/models')({
models.unshift(defaultModel)
}
// Merge providers.*.models + provider defaults + model_aliases
// from ~/.hermes/config.yaml so the picker reflects the user's full
// Hermes catalog, not just /v1/models + models.json + local discovery.
// Fix for #569.
const configModels = readClaudeConfigCatalog()
if (configModels.length > 0) {
models = mergeModelEntries(models, configModels)
source =
source === 'models.json'
? 'models.json+config.yaml'
: `${source}+config.yaml`
}
// Merge the authoritative Hermes model catalog whenever it is
// available. Previously, a non-empty models.json stopped here, so the
// Operations picker only showed the local Workspace subset and drifted