Compare commits
3 Commits
8f4b1126da
...
b6ee3e071f
| Author | SHA1 | Date | |
|---|---|---|---|
| b6ee3e071f | |||
| d78116ed26 | |||
| f195e6a8aa |
@ -1,9 +1,11 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { personality } from "./commands/personality";
|
||||
import { models } from "./commands/models";
|
||||
|
||||
const COMMANDS: Record<string, (args: string[]) => Promise<void>> = {
|
||||
personality,
|
||||
models,
|
||||
};
|
||||
|
||||
async function main() {
|
||||
@ -16,6 +18,7 @@ Usage: hermes-har <command> [options]
|
||||
|
||||
Commands:
|
||||
personality Manage personality presets (list/add/remove/switch)
|
||||
models Manage providers & models (list/test/switch)
|
||||
|
||||
Run 'hermes-har <command> --help' for details.`);
|
||||
process.exit(0);
|
||||
|
||||
507
src/commands/models.ts
Normal file
507
src/commands/models.ts
Normal file
@ -0,0 +1,507 @@
|
||||
import { readFileSync, writeFileSync, existsSync } from "fs";
|
||||
import { homedir } from "os";
|
||||
import { join } from "path";
|
||||
import { parse, stringify } from "yaml";
|
||||
|
||||
const CONFIG_PATH = join(homedir(), ".hermes", "config.yaml");
|
||||
const ENV_PATH = join(homedir(), ".hermes", ".env");
|
||||
|
||||
// ── Types ──────────────────────────────────────────────────────────────
|
||||
|
||||
interface CustomProvider {
|
||||
name: string;
|
||||
base_url: string;
|
||||
api_key?: string;
|
||||
api_key_env?: string;
|
||||
api_mode?: string;
|
||||
}
|
||||
|
||||
interface AuxiliaryEntry {
|
||||
provider: string;
|
||||
model: string;
|
||||
base_url?: string;
|
||||
api_key?: string;
|
||||
timeout?: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface Config {
|
||||
model?: {
|
||||
default?: string;
|
||||
provider?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
custom_providers?: CustomProvider[];
|
||||
auxiliary?: Record<string, AuxiliaryEntry>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
const AUXILIARY_TASKS = [
|
||||
"vision",
|
||||
"web_extract",
|
||||
"compression",
|
||||
"session_search",
|
||||
"skills_hub",
|
||||
"summary",
|
||||
"title_generation",
|
||||
"approval",
|
||||
];
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
function loadConfig(): Config {
|
||||
if (!existsSync(CONFIG_PATH)) {
|
||||
throw new Error(`Config not found: ${CONFIG_PATH}`);
|
||||
}
|
||||
return parse(readFileSync(CONFIG_PATH, "utf-8")) || {};
|
||||
}
|
||||
|
||||
function saveConfig(config: Config) {
|
||||
writeFileSync(CONFIG_PATH, stringify(config, { lineWidth: 120 }), "utf-8");
|
||||
}
|
||||
|
||||
function getCustomProviders(config: Config): CustomProvider[] {
|
||||
return config.custom_providers || [];
|
||||
}
|
||||
|
||||
function findProvider(
|
||||
config: Config,
|
||||
name: string
|
||||
): CustomProvider | undefined {
|
||||
return getCustomProviders(config).find((p) => p.name === name);
|
||||
}
|
||||
|
||||
function getProviderBaseUrl(config: Config, name: string): string | null {
|
||||
const provider = findProvider(config, name);
|
||||
if (provider) return provider.base_url;
|
||||
|
||||
// Check env for standard providers
|
||||
const envMap: Record<string, string> = {
|
||||
openrouter: "https://openrouter.ai/api/v1",
|
||||
anthropic: "https://api.anthropic.com/v1",
|
||||
openai: "https://api.openai.com/v1",
|
||||
deepseek: "https://api.deepseek.com/v1",
|
||||
};
|
||||
return envMap[name] || null;
|
||||
}
|
||||
|
||||
function getProviderApiKey(provider: CustomProvider): string {
|
||||
if (provider.api_key) return provider.api_key;
|
||||
if (provider.api_key_env) {
|
||||
// Try process.env first, then read from .env file
|
||||
const envVal = process.env[provider.api_key_env];
|
||||
if (envVal) return envVal;
|
||||
if (existsSync(ENV_PATH)) {
|
||||
for (const line of readFileSync(ENV_PATH, "utf-8").split("\n")) {
|
||||
if (line.startsWith(`${provider.api_key_env}=`)) {
|
||||
return line.split("=").slice(1).join("=").trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function getAuthHeaders(provider: CustomProvider): Record<string, string> {
|
||||
const key = getProviderApiKey(provider);
|
||||
return key ? { Authorization: `Bearer ${key}` } : {};
|
||||
}
|
||||
|
||||
// ── providers ──────────────────────────────────────────────────────────
|
||||
|
||||
function providers() {
|
||||
const config = loadConfig();
|
||||
const customProviders = getCustomProviders(config);
|
||||
const mainProvider = config.model?.provider || "auto";
|
||||
const mainModel = config.model?.default || "(not set)";
|
||||
|
||||
console.log(`Main: provider=${mainProvider} model=${mainModel}\n`);
|
||||
|
||||
if (customProviders.length === 0) {
|
||||
console.log("No custom providers configured.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Custom providers:\n");
|
||||
for (const p of customProviders) {
|
||||
const active = p.name === mainProvider || `custom:${p.name}` === mainProvider ? " ◀ active" : "";
|
||||
console.log(` ${p.name}${active}`);
|
||||
console.log(` url: ${p.base_url}`);
|
||||
if (p.api_mode) console.log(` mode: ${p.api_mode}`);
|
||||
}
|
||||
console.log(`\nTotal: ${customProviders.length}`);
|
||||
}
|
||||
|
||||
// ── list ────────────────────────────────────────────────────────────────
|
||||
|
||||
async function list(providerName?: string) {
|
||||
const config = loadConfig();
|
||||
const customProviders = getCustomProviders(config);
|
||||
|
||||
const targets: CustomProvider[] = providerName
|
||||
? customProviders.filter((p) => p.name === providerName)
|
||||
: customProviders;
|
||||
|
||||
if (targets.length === 0) {
|
||||
if (providerName) {
|
||||
console.error(`Provider not found: ${providerName}`);
|
||||
console.error(
|
||||
`Available: ${customProviders.map((p) => p.name).join(", ")}`
|
||||
);
|
||||
} else {
|
||||
console.log("No custom providers configured.");
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
for (const provider of targets) {
|
||||
console.log(`\n[${provider.name}] ${provider.base_url}\n`);
|
||||
try {
|
||||
const modelsUrl = provider.base_url.replace(/\/+$/, "") + "/models";
|
||||
const resp = await fetch(modelsUrl, {
|
||||
headers: getAuthHeaders(provider),
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
console.error(` HTTP ${resp.status}: ${resp.statusText}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const data = (await resp.json()) as {
|
||||
data?: Array<{
|
||||
id: string;
|
||||
display_name?: string;
|
||||
owned_by?: string;
|
||||
}>;
|
||||
};
|
||||
const models = data.data || [];
|
||||
|
||||
if (models.length === 0) {
|
||||
console.log(" (no models)");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Group by owned_by
|
||||
const grouped: Record<string, typeof models> = {};
|
||||
for (const m of models) {
|
||||
const owner = m.owned_by || "unknown";
|
||||
if (!grouped[owner]) grouped[owner] = [];
|
||||
grouped[owner].push(m);
|
||||
}
|
||||
|
||||
for (const [owner, ownerModels] of Object.entries(grouped)) {
|
||||
console.log(` ${owner}:`);
|
||||
for (const m of ownerModels) {
|
||||
const display = m.display_name ? ` (${m.display_name})` : "";
|
||||
console.log(` ${m.id}${display}`);
|
||||
}
|
||||
}
|
||||
console.log(`\n Total: ${models.length} models`);
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
console.error(` Error fetching models: ${msg}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── test ────────────────────────────────────────────────────────────────
|
||||
|
||||
async function test(providerName?: string) {
|
||||
const config = loadConfig();
|
||||
const customProviders = getCustomProviders(config);
|
||||
|
||||
const targets: CustomProvider[] = providerName
|
||||
? customProviders.filter((p) => p.name === providerName)
|
||||
: customProviders;
|
||||
|
||||
if (targets.length === 0) {
|
||||
if (providerName) {
|
||||
console.error(`Provider not found: ${providerName}`);
|
||||
} else {
|
||||
console.log("No custom providers configured.");
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
for (const provider of targets) {
|
||||
const modelsUrl = provider.base_url.replace(/\/+$/, "") + "/models";
|
||||
const start = performance.now();
|
||||
|
||||
try {
|
||||
const resp = await fetch(modelsUrl, {
|
||||
headers: getAuthHeaders(provider),
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
|
||||
const elapsed = Math.round(performance.now() - start);
|
||||
|
||||
if (resp.ok) {
|
||||
const data = (await resp.json()) as {
|
||||
data?: Array<unknown>;
|
||||
};
|
||||
const count = data.data?.length || 0;
|
||||
console.log(` ✅ ${provider.name} — ${elapsed}ms, ${count} models`);
|
||||
} else {
|
||||
console.log(
|
||||
` ❌ ${provider.name} — HTTP ${resp.status} (${elapsed}ms)`
|
||||
);
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const elapsed = Math.round(performance.now() - start);
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
console.log(` ❌ ${provider.name} — ${msg} (${elapsed}ms)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── status ──────────────────────────────────────────────────────────────
|
||||
|
||||
function status() {
|
||||
const config = loadConfig();
|
||||
const mainProvider = config.model?.provider || "auto";
|
||||
const mainModel = config.model?.default || "(not set)";
|
||||
|
||||
console.log("=== Model Configuration ===\n");
|
||||
console.log(`Main: provider=${mainProvider} model=${mainModel}\n`);
|
||||
|
||||
const aux = config.auxiliary || {};
|
||||
const tasks = Object.keys(aux).filter((k) => typeof aux[k] === "object" && aux[k]?.provider !== undefined);
|
||||
|
||||
if (tasks.length === 0) {
|
||||
console.log("Auxiliary: (not configured)");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Auxiliary:");
|
||||
for (const task of tasks) {
|
||||
const entry = aux[task];
|
||||
const provider = entry.provider || "auto";
|
||||
const model = entry.model || "(default)";
|
||||
console.log(` ${task.padEnd(20)} provider=${provider} model=${model}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── switch ──────────────────────────────────────────────────────────────
|
||||
|
||||
interface SwitchOptions {
|
||||
provider: string;
|
||||
model?: string;
|
||||
tg?: boolean;
|
||||
global?: boolean;
|
||||
auxTasks?: string[];
|
||||
}
|
||||
|
||||
function switchConfig(opts: SwitchOptions) {
|
||||
const config = loadConfig();
|
||||
|
||||
if (opts.auxTasks && opts.auxTasks.length > 0) {
|
||||
// Switch auxiliary tasks
|
||||
if (!config.auxiliary) config.auxiliary = {} as Record<string, AuxiliaryEntry>;
|
||||
|
||||
for (const task of opts.auxTasks) {
|
||||
if (!config.auxiliary[task]) {
|
||||
config.auxiliary[task] = { provider: "", model: "", timeout: 30 };
|
||||
}
|
||||
config.auxiliary[task].provider = opts.provider;
|
||||
if (opts.model) {
|
||||
config.auxiliary[task].model = opts.model;
|
||||
}
|
||||
console.log(` auxiliary.${task} → provider=${opts.provider}${opts.model ? ` model=${opts.model}` : ""}`);
|
||||
}
|
||||
} else {
|
||||
// Switch main provider
|
||||
const providerValue = findProvider(config, opts.provider)
|
||||
? `custom:${opts.provider}`
|
||||
: opts.provider;
|
||||
|
||||
if (!config.model) config.model = {};
|
||||
config.model.provider = providerValue;
|
||||
if (opts.model) {
|
||||
config.model.default = opts.model;
|
||||
}
|
||||
console.log(` main → provider=${providerValue}${opts.model ? ` model=${opts.model}` : ""}`);
|
||||
}
|
||||
|
||||
saveConfig(config);
|
||||
console.log("\nConfig saved. New sessions will use the updated settings.");
|
||||
}
|
||||
|
||||
async function switchTelegram(opts: SwitchOptions) {
|
||||
const config = loadConfig();
|
||||
|
||||
// Build the /model command
|
||||
let command: string;
|
||||
if (opts.auxTasks && opts.auxTasks.length > 0) {
|
||||
// Hermes doesn't support /model for aux via TG, fall back to config edit
|
||||
console.log("Note: Telegram /model doesn't support auxiliary switching. Updating config directly.");
|
||||
switchConfig(opts);
|
||||
return;
|
||||
}
|
||||
|
||||
const modelPart = opts.model || config.model?.default || "";
|
||||
command = `/model ${modelPart} --provider ${opts.provider}`;
|
||||
if (opts.global) {
|
||||
command += " --global";
|
||||
}
|
||||
|
||||
// Find bot token
|
||||
let token = process.env.TELEGRAM_BOT_TOKEN || "";
|
||||
if (!token && existsSync(ENV_PATH)) {
|
||||
for (const line of readFileSync(ENV_PATH, "utf-8").split("\n")) {
|
||||
if (line.startsWith("TELEGRAM_BOT_TOKEN=")) {
|
||||
token = line.split("=").slice(1).join("=").trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
console.error("TELEGRAM_BOT_TOKEN not found");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const chatId =
|
||||
process.env.TELEGRAM_HOME_CHANNEL ||
|
||||
(config as Record<string, unknown>).TELEGRAM_HOME_CHANNEL ||
|
||||
"";
|
||||
|
||||
if (!chatId) {
|
||||
console.error("TELEGRAM_HOME_CHANNEL not configured");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const url = `https://api.telegram.org/bot${token}/sendMessage`;
|
||||
const resp = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ chat_id: chatId, text: command }),
|
||||
});
|
||||
|
||||
const result = (await resp.json()) as {
|
||||
ok: boolean;
|
||||
description?: string;
|
||||
result?: { message_id: number };
|
||||
};
|
||||
|
||||
if (result.ok) {
|
||||
console.log(`Sent: ${command}`);
|
||||
console.log(opts.global ? "Persistent change (--global)." : "Current session only.");
|
||||
// Clean up the command message
|
||||
if (result.result?.message_id) {
|
||||
await fetch(`https://api.telegram.org/bot${token}/deleteMessage`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
chat_id: chatId,
|
||||
message_id: result.result.message_id,
|
||||
}),
|
||||
}).catch(() => {});
|
||||
}
|
||||
} else {
|
||||
console.error(`Telegram API error: ${result.description}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// ── CLI ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const HELP = `hermes-har models — Manage providers & models
|
||||
|
||||
Usage:
|
||||
hermes-har models providers List custom providers
|
||||
hermes-har models list [provider] List available models
|
||||
hermes-har models test [provider] Test provider connectivity
|
||||
hermes-har models status Show current model config
|
||||
hermes-har models switch <provider> [model] Switch main provider (config)
|
||||
hermes-har models switch <provider> [model] --tg Switch via Telegram (session)
|
||||
hermes-har models switch <provider> [model] --global Persistent (with --tg)
|
||||
hermes-har models switch <provider> [model] --aux <task> Switch auxiliary task
|
||||
Multiple --aux flags supported: --aux vision --aux compression`;
|
||||
|
||||
export async function models(args: string[]) {
|
||||
const action = args[0];
|
||||
|
||||
if (!action || action === "--help" || action === "help") {
|
||||
console.log(HELP);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case "providers":
|
||||
case "provider":
|
||||
providers();
|
||||
break;
|
||||
|
||||
case "list":
|
||||
case "ls":
|
||||
await list(args[1]);
|
||||
break;
|
||||
|
||||
case "test":
|
||||
await test(args[1]);
|
||||
break;
|
||||
|
||||
case "status":
|
||||
case "st":
|
||||
status();
|
||||
break;
|
||||
|
||||
case "switch":
|
||||
case "use": {
|
||||
if (!args[1]) {
|
||||
console.error("Usage: hermes-har models switch <provider> [model] [flags]");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Parse args: switch <provider> [model] [--tg] [--global] [--aux task]*
|
||||
const provider = args[1];
|
||||
let model: string | undefined;
|
||||
let tg = false;
|
||||
let global = false;
|
||||
const auxTasks: string[] = [];
|
||||
|
||||
let i = 2;
|
||||
// model is the next positional arg if it doesn't start with --
|
||||
if (i < args.length && !args[i].startsWith("--")) {
|
||||
model = args[i];
|
||||
i++;
|
||||
}
|
||||
|
||||
while (i < args.length) {
|
||||
if (args[i] === "--tg" || args[i] === "--telegram") {
|
||||
tg = true;
|
||||
i++;
|
||||
} else if (args[i] === "--global") {
|
||||
global = true;
|
||||
i++;
|
||||
} else if (args[i] === "--aux" && i + 1 < args.length) {
|
||||
const task = args[i + 1];
|
||||
if (!AUXILIARY_TASKS.includes(task)) {
|
||||
console.error(`Unknown auxiliary task: ${task}`);
|
||||
console.error(`Available: ${AUXILIARY_TASKS.join(", ")}`);
|
||||
process.exit(1);
|
||||
}
|
||||
auxTasks.push(task);
|
||||
i += 2;
|
||||
} else {
|
||||
console.error(`Unknown flag: ${args[i]}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const opts: SwitchOptions = { provider, model, tg, global, auxTasks };
|
||||
|
||||
if (tg) {
|
||||
await switchTelegram(opts);
|
||||
} else {
|
||||
switchConfig(opts);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
console.error(`Unknown action: ${action}`);
|
||||
console.log(HELP);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user