feat: read providers from cfg registry instead of config.yaml

- providers/list/test now read from cfg (HERMES_CUSTOM_PROVIDERS)
- switch writes target provider into config.yaml custom_providers and restarts gateway
- standard providers (openrouter, anthropic) clear custom_providers on switch
- fallback to config.yaml if cfg unavailable

Signed-off-by: Xiaonuo <xiaonuo@shazhou.work>
This commit is contained in:
Xiaonuo 2026-04-21 11:07:22 +08:00
parent f1c65b2c1b
commit d736d43f72

View File

@ -1,10 +1,12 @@
import { readFileSync, writeFileSync, existsSync } from "fs"; import { readFileSync, writeFileSync, existsSync } from "fs";
import { execSync } from "child_process";
import { homedir } from "os"; import { homedir } from "os";
import { join } from "path"; import { join } from "path";
import { parse, stringify } from "yaml"; import { parse, stringify } from "yaml";
const CONFIG_PATH = join(homedir(), ".hermes", "config.yaml"); const CONFIG_PATH = join(homedir(), ".hermes", "config.yaml");
const ENV_PATH = join(homedir(), ".hermes", ".env"); const ENV_PATH = join(homedir(), ".hermes", ".env");
const CFG_PROVIDERS_KEY = "HERMES_CUSTOM_PROVIDERS";
// ── Types ────────────────────────────────────────────────────────────── // ── Types ──────────────────────────────────────────────────────────────
@ -72,10 +74,43 @@ function saveConfig(config: Config) {
writeFileSync(CONFIG_PATH, stringify(config, { lineWidth: 120 }), "utf-8"); writeFileSync(CONFIG_PATH, stringify(config, { lineWidth: 120 }), "utf-8");
} }
/**
* Read all available providers from cfg (central registry).
* Falls back to config.yaml custom_providers if cfg unavailable.
*/
function getCfgProviders(): CustomProvider[] {
try {
const raw = execSync(`cfg get ${CFG_PROVIDERS_KEY}`, {
encoding: "utf-8",
timeout: 5000,
}).trim();
if (!raw) return [];
return JSON.parse(raw) as CustomProvider[];
} catch {
return [];
}
}
/**
* Get providers from cfg (authoritative source).
* The config.yaml custom_providers is now only a "currently active" slot.
*/
function getAllProviders(): CustomProvider[] {
const cfgProviders = getCfgProviders();
if (cfgProviders.length > 0) return cfgProviders;
// Fallback to config.yaml if cfg has nothing
const config = loadConfig();
return config.custom_providers || [];
}
function getCustomProviders(config: Config): CustomProvider[] { function getCustomProviders(config: Config): CustomProvider[] {
return config.custom_providers || []; return config.custom_providers || [];
} }
function findProviderFromRegistry(name: string): CustomProvider | undefined {
return getAllProviders().find((p) => p.name === name);
}
function findProvider( function findProvider(
config: Config, config: Config,
name: string name: string
@ -123,42 +158,41 @@ function getAuthHeaders(provider: CustomProvider): Record<string, string> {
function providers() { function providers() {
const config = loadConfig(); const config = loadConfig();
const customProviders = getCustomProviders(config); const allProviders = getAllProviders();
const mainProvider = config.model?.provider || "auto"; const mainProvider = config.model?.provider || "auto";
const mainModel = config.model?.default || "(not set)"; const mainModel = config.model?.default || "(not set)";
console.log(`Main: provider=${mainProvider} model=${mainModel}\n`); console.log(`Main: provider=${mainProvider} model=${mainModel}\n`);
if (customProviders.length === 0) { if (allProviders.length === 0) {
console.log("No custom providers configured."); console.log("No custom providers configured.");
return; return;
} }
console.log("Custom providers:\n"); console.log("Custom providers (from cfg):\n");
for (const p of customProviders) { for (const p of allProviders) {
const active = p.name === mainProvider || `custom:${p.name}` === mainProvider ? " ◀ active" : ""; const active = p.name === mainProvider || `custom:${p.name}` === mainProvider ? " ◀ active" : "";
console.log(` ${p.name}${active}`); console.log(` ${p.name}${active}`);
console.log(` url: ${p.base_url}`); console.log(` url: ${p.base_url}`);
if (p.api_mode) console.log(` mode: ${p.api_mode}`); if (p.api_mode) console.log(` mode: ${p.api_mode}`);
} }
console.log(`\nTotal: ${customProviders.length}`); console.log(`\nTotal: ${allProviders.length}`);
} }
// ── list ──────────────────────────────────────────────────────────────── // ── list ────────────────────────────────────────────────────────────────
async function list(providerName?: string) { async function list(providerName?: string) {
const config = loadConfig(); const allProviders = getAllProviders();
const customProviders = getCustomProviders(config);
const targets: CustomProvider[] = providerName const targets: CustomProvider[] = providerName
? customProviders.filter((p) => p.name === providerName) ? allProviders.filter((p) => p.name === providerName)
: customProviders; : allProviders;
if (targets.length === 0) { if (targets.length === 0) {
if (providerName) { if (providerName) {
console.error(`Provider not found: ${providerName}`); console.error(`Provider not found: ${providerName}`);
console.error( console.error(
`Available: ${customProviders.map((p) => p.name).join(", ")}` `Available: ${allProviders.map((p) => p.name).join(", ")}`
); );
} else { } else {
console.log("No custom providers configured."); console.log("No custom providers configured.");
@ -220,12 +254,11 @@ async function list(providerName?: string) {
// ── test ──────────────────────────────────────────────────────────────── // ── test ────────────────────────────────────────────────────────────────
async function test(providerName?: string) { async function test(providerName?: string) {
const config = loadConfig(); const allProviders = getAllProviders();
const customProviders = getCustomProviders(config);
const targets: CustomProvider[] = providerName const targets: CustomProvider[] = providerName
? customProviders.filter((p) => p.name === providerName) ? allProviders.filter((p) => p.name === providerName)
: customProviders; : allProviders;
if (targets.length === 0) { if (targets.length === 0) {
if (providerName) { if (providerName) {
@ -322,21 +355,45 @@ function switchConfig(opts: SwitchOptions) {
console.log(` auxiliary.${task} → provider=${opts.provider}${opts.model ? ` model=${opts.model}` : ""}`); console.log(` auxiliary.${task} → provider=${opts.provider}${opts.model ? ` model=${opts.model}` : ""}`);
} }
} else { } else {
// Switch main provider // Switch main provider — look up from cfg registry
const providerValue = findProvider(config, opts.provider) const registryProvider = findProviderFromRegistry(opts.provider);
? `custom:${opts.provider}`
: opts.provider; if (registryProvider) {
// Write this provider into config.yaml custom_providers (replace all)
if (!config.model) config.model = {}; config.custom_providers = [registryProvider];
config.model.provider = providerValue; const providerValue = `custom:${registryProvider.name}`;
if (opts.model) { if (!config.model) config.model = {};
config.model.default = opts.model; config.model.provider = providerValue;
if (opts.model) {
config.model.default = opts.model;
}
console.log(` main → provider=${providerValue}${opts.model ? ` model=${opts.model}` : ""}`);
console.log(` config.yaml custom_providers updated with ${registryProvider.name}`);
} else {
// Standard provider (openrouter, anthropic, etc.)
if (!config.model) config.model = {};
config.model.provider = opts.provider;
if (opts.model) {
config.model.default = opts.model;
}
// Clear custom_providers since switching to standard
delete config.custom_providers;
console.log(` main → provider=${opts.provider}${opts.model ? ` model=${opts.model}` : ""}`);
} }
console.log(` main → provider=${providerValue}${opts.model ? ` model=${opts.model}` : ""}`);
} }
saveConfig(config); saveConfig(config);
console.log("\nConfig saved. Run 'hermes gateway restart' or start a new session to apply.");
// Restart gateway
try {
execSync("systemctl --user restart hermes-gateway 2>/dev/null || true", {
encoding: "utf-8",
timeout: 10000,
});
console.log("\nConfig saved & gateway restarted.");
} catch {
console.log("\nConfig saved. Restart gateway manually to apply.");
}
} }
async function switchTelegram(opts: SwitchOptions) { async function switchTelegram(opts: SwitchOptions) {