feat: multi-gateway profile support, provider management overhaul, and model settings tab

- Profile-aware proxy: inject API key from profile-specific .env, route requests via X-Hermes-Profile header
- Remove auth.json dependency: built-in providers use .env, custom providers use config.yaml
- Add allProviders field to available-models response with all hardcoded provider catalogs
- Add Models tab in Settings for editing provider API keys (built-in → .env, custom → config.yaml)
- Add PUT /api/config/providers/:poolKey for updating provider credentials
- ProviderFormModal uses backend allProviders for preset dropdown
- Gateway log format support: parse both agent and gateway log formats
- Add webui server.log to log viewer with log rotation at 3MB
- Fix provider delete loading state and OAuth provider cleanup
- Setup script: require Node.js 23+, auto-upgrade if version too low

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
ekko
2026-04-19 20:59:25 +08:00
parent e7e4c386c3
commit 562261d13f
19 changed files with 635 additions and 276 deletions
@@ -28,23 +28,26 @@ async function waitForGatewayReady(upstream: string, timeoutMs: number = 5000):
return false
}
/** Resolve profile name from request */
function resolveProfile(ctx: Context): string {
return ctx.get('x-hermes-profile') || (ctx.query.profile as string) || 'default'
}
/** Resolve upstream URL for a request based on profile header/query */
function resolveUpstream(ctx: Context): string {
const mgr = getGatewayManager()
if (mgr) {
// Check X-Hermes-Profile header or ?profile= query param
const profile = ctx.get('x-hermes-profile') || (ctx.query.profile as string)
if (profile) {
const profile = resolveProfile(ctx)
if (profile && profile !== 'default') {
return mgr.getUpstream(profile)
}
// Default to active profile's upstream
return mgr.getUpstream()
}
// Fallback: static upstream from config
return config.upstream.replace(/\/$/, '')
}
export async function proxy(ctx: Context) {
const profile = resolveProfile(ctx)
const upstream = resolveUpstream(ctx)
// Rewrite path for upstream gateway:
// /api/hermes/v1/* -> /v1/* (upstream uses /v1/ prefix)
@@ -59,7 +62,7 @@ export async function proxy(ctx: Context) {
const lower = key.toLowerCase()
if (lower === 'host') {
headers['host'] = new URL(upstream).host
} else if (lower === 'authorization' || lower === 'origin' || lower === 'referer' || lower === 'connection') {
} else if (lower === 'origin' || lower === 'referer' || lower === 'connection') {
continue
} else {
const v = Array.isArray(value) ? value[0] : value
@@ -67,6 +70,15 @@ export async function proxy(ctx: Context) {
}
}
// Inject Hermes gateway API key from profile's .env
const mgr = getGatewayManager()
if (mgr) {
const apiKey = mgr.getApiKey(profile)
if (apiKey) {
headers['authorization'] = `Bearer ${apiKey}`
}
}
try {
// Build request body from raw body
let body: string | undefined