feat(hermes): add provider routing config

This commit is contained in:
晴天
2026-05-26 03:00:31 +08:00
parent fb7ae3f15c
commit ac2282591d
8 changed files with 787 additions and 28 deletions

View File

@@ -3335,6 +3335,8 @@ const HERMES_APPROVAL_CRON_MODES = new Set(['deny', 'approve'])
const HERMES_LOGGING_LEVELS = new Set(['DEBUG', 'INFO', 'WARNING'])
const HERMES_AGENT_IMAGE_INPUT_MODES = new Set(['auto', 'native', 'text'])
const HERMES_PROMPT_CACHE_TTLS = new Set(['5m', '1h'])
const HERMES_PROVIDER_ROUTING_SORTS = new Set(['price', 'throughput', 'latency'])
const HERMES_PROVIDER_ROUTING_DATA_COLLECTION = new Set(['allow', 'deny'])
const HERMES_DISPLAY_TOOL_PROGRESS_VALUES = new Set(['off', 'new', 'all', 'verbose'])
const HERMES_DISPLAY_STREAMING_VALUES = new Set(['inherit', 'true', 'false'])
const HERMES_DISPLAY_RESUME_VALUES = new Set(['full', 'minimal'])
@@ -3506,6 +3508,36 @@ function normalizeHermesPromptCacheTtl(value, strict = false) {
return '5m'
}
function normalizeHermesProviderRoutingSort(value, strict = false) {
const sort = String(value ?? '').trim().toLowerCase() || 'price'
if (HERMES_PROVIDER_ROUTING_SORTS.has(sort)) return sort
if (strict) throw new Error('provider_routing.sort 必须是 price、throughput 或 latency')
return 'price'
}
function normalizeHermesProviderRoutingDataCollection(value, strict = false) {
const policy = String(value ?? '').trim().toLowerCase() || 'allow'
if (HERMES_PROVIDER_ROUTING_DATA_COLLECTION.has(policy)) return policy
if (strict) throw new Error('provider_routing.data_collection 必须是 allow 或 deny')
return 'allow'
}
function normalizeHermesProviderRoutingList(value, key) {
const seen = new Set()
const normalized = []
for (const item of normalizeHermesMultilineList(value)) {
const provider = String(item ?? '').trim().toLowerCase()
if (!/^[a-zA-Z0-9_.-]+$/.test(provider)) {
throw new Error(`${key} 只能包含字母、数字、下划线、点和短横线`)
}
if (!seen.has(provider)) {
seen.add(provider)
normalized.push(provider)
}
}
return normalized
}
function normalizeHermesAuxiliaryProvider(value, key, strict = false) {
const provider = String(value ?? '').trim().toLowerCase() || 'auto'
if (HERMES_AUXILIARY_PROVIDERS.has(provider)) return provider
@@ -3766,6 +3798,45 @@ export function mergeHermesOpenrouterCacheConfig(config = {}, form = {}) {
return next
}
export function buildHermesProviderRoutingConfigValues(config = {}) {
const root = config && typeof config === 'object' && !Array.isArray(config) ? config : {}
const providerRouting = root.provider_routing && typeof root.provider_routing === 'object' && !Array.isArray(root.provider_routing)
? root.provider_routing
: {}
return {
providerRoutingSort: normalizeHermesProviderRoutingSort(providerRouting.sort, false),
providerRoutingOnly: normalizeHermesProviderRoutingList(providerRouting.only || [], 'provider_routing.only').join('\n'),
providerRoutingIgnore: normalizeHermesProviderRoutingList(providerRouting.ignore || [], 'provider_routing.ignore').join('\n'),
providerRoutingOrder: normalizeHermesProviderRoutingList(providerRouting.order || [], 'provider_routing.order').join('\n'),
providerRoutingRequireParameters: readHermesBool(providerRouting.require_parameters, false),
providerRoutingDataCollection: normalizeHermesProviderRoutingDataCollection(providerRouting.data_collection, false),
}
}
export function mergeHermesProviderRoutingConfig(config = {}, form = {}) {
const next = mergeConfigsPreservingFields({}, config && typeof config === 'object' && !Array.isArray(config) ? config : {})
const currentValues = buildHermesProviderRoutingConfigValues(next)
const providerRouting = next.provider_routing && typeof next.provider_routing === 'object' && !Array.isArray(next.provider_routing)
? mergeConfigsPreservingFields(next.provider_routing, {})
: {}
providerRouting.sort = normalizeHermesProviderRoutingSort(Object.hasOwn(form, 'providerRoutingSort') ? form.providerRoutingSort : currentValues.providerRoutingSort, true)
providerRouting.require_parameters = formHermesBool(form, 'providerRoutingRequireParameters', currentValues.providerRoutingRequireParameters)
providerRouting.data_collection = normalizeHermesProviderRoutingDataCollection(Object.hasOwn(form, 'providerRoutingDataCollection') ? form.providerRoutingDataCollection : currentValues.providerRoutingDataCollection, true)
for (const [field, formKey] of [
['only', 'providerRoutingOnly'],
['ignore', 'providerRoutingIgnore'],
['order', 'providerRoutingOrder'],
]) {
const values = normalizeHermesProviderRoutingList(Object.hasOwn(form, formKey) ? form[formKey] : currentValues[formKey], `provider_routing.${field}`)
if (values.length) providerRouting[field] = values
else delete providerRouting[field]
}
next.provider_routing = providerRouting
return next
}
function hermesAuxiliaryTask(root, key) {
const auxiliary = root.auxiliary && typeof root.auxiliary === 'object' && !Array.isArray(root.auxiliary)
? root.auxiliary
@@ -10952,6 +11023,27 @@ const handlers = {
}
},
hermes_provider_routing_config_read() {
const { configPath, exists, config } = readHermesConfigYamlObject()
return {
exists,
configPath,
values: buildHermesProviderRoutingConfigValues(config),
}
},
hermes_provider_routing_config_save({ form } = {}) {
const { configPath, config } = readHermesConfigYamlObject()
const next = mergeHermesProviderRoutingConfig(config, form || {})
const backup = writeHermesConfigYamlObject(configPath, next)
return {
ok: true,
configPath,
backup,
values: buildHermesProviderRoutingConfigValues(next),
}
},
hermes_auxiliary_config_read() {
const { configPath, exists, config } = readHermesConfigYamlObject()
return {