mirror of
https://github.com/Routstr/routstrd.git
synced 2026-07-30 15:46:14 +00:00
fix breaking hermes config
This commit is contained in:
@@ -13,6 +13,7 @@
|
|||||||
"nostr-tools": "^2.12.0",
|
"nostr-tools": "^2.12.0",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
|
"yaml": "^2.9.0",
|
||||||
"zustand": "^5.0.5",
|
"zustand": "^5.0.5",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -325,6 +326,8 @@
|
|||||||
|
|
||||||
"y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="],
|
"y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="],
|
||||||
|
|
||||||
|
"yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
|
||||||
|
|
||||||
"yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="],
|
"yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="],
|
||||||
|
|
||||||
"yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="],
|
"yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="],
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"nostr-tools": "^2.12.0",
|
"nostr-tools": "^2.12.0",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
|
"yaml": "^2.9.0",
|
||||||
"zustand": "^5.0.5"
|
"zustand": "^5.0.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+72
-29
@@ -1,10 +1,65 @@
|
|||||||
import { existsSync, mkdirSync } from "fs";
|
import { existsSync, mkdirSync } from "fs";
|
||||||
import { readFile, writeFile } from "fs/promises";
|
import { readFile, writeFile } from "fs/promises";
|
||||||
import { dirname } from "path";
|
import { dirname } from "path";
|
||||||
|
import { parseDocument } from "yaml";
|
||||||
import type { RoutstrdConfig } from "../utils/config";
|
import type { RoutstrdConfig } from "../utils/config";
|
||||||
import type { IntegrationConfig, RoutstrModel } from "./registry";
|
import type { IntegrationConfig, RoutstrModel } from "./registry";
|
||||||
import { callDaemon, getDaemonBaseUrl } from "../utils/daemon-client";
|
import { callDaemon, getDaemonBaseUrl } from "../utils/daemon-client";
|
||||||
|
|
||||||
|
interface HermesRoutstrConfig {
|
||||||
|
baseUrl: string;
|
||||||
|
apiKey: string;
|
||||||
|
defaultModel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HermesCustomProvider {
|
||||||
|
name?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mergeHermesConfig(
|
||||||
|
content: string,
|
||||||
|
routstr: HermesRoutstrConfig,
|
||||||
|
): string {
|
||||||
|
const document = parseDocument(content || "{}", { prettyErrors: true });
|
||||||
|
if (document.errors.length > 0) {
|
||||||
|
throw document.errors[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlDisplay = routstr.baseUrl
|
||||||
|
.replace(/\/v1$/, "")
|
||||||
|
.replace(/^https?:\/\//, "");
|
||||||
|
const provider = {
|
||||||
|
name: `Routstr (${urlDisplay})`,
|
||||||
|
base_url: routstr.baseUrl,
|
||||||
|
api_key: routstr.apiKey,
|
||||||
|
model: routstr.defaultModel,
|
||||||
|
};
|
||||||
|
|
||||||
|
const isNewConfig = content.trim() === "";
|
||||||
|
if (isNewConfig) {
|
||||||
|
document.set("model", {
|
||||||
|
default: routstr.defaultModel,
|
||||||
|
provider: "custom",
|
||||||
|
base_url: routstr.baseUrl,
|
||||||
|
api_key: routstr.apiKey,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingConfig = document.toJS() as {
|
||||||
|
custom_providers?: HermesCustomProvider[];
|
||||||
|
};
|
||||||
|
const existingProviders = existingConfig.custom_providers;
|
||||||
|
const providers = Array.isArray(existingProviders) ? existingProviders : [];
|
||||||
|
if (providers.some((item) => item.name?.startsWith("Routstr ("))) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
providers.push(provider);
|
||||||
|
document.set("custom_providers", providers);
|
||||||
|
|
||||||
|
return document.toString();
|
||||||
|
}
|
||||||
|
|
||||||
export async function installHermesIntegration(
|
export async function installHermesIntegration(
|
||||||
config: RoutstrdConfig,
|
config: RoutstrdConfig,
|
||||||
apiKey: string,
|
apiKey: string,
|
||||||
@@ -26,10 +81,10 @@ export async function installHermesIntegration(
|
|||||||
|
|
||||||
if (models.length >= 3) {
|
if (models.length >= 3) {
|
||||||
defaultModel = models[2]!.id;
|
defaultModel = models[2]!.id;
|
||||||
console.log(`Set default model to 3rd available model: ${defaultModel}`);
|
console.log(`Using 3rd available model for new Hermes configurations: ${defaultModel}`);
|
||||||
} else if (models.length > 0) {
|
} else if (models.length > 0) {
|
||||||
defaultModel = models[0]!.id;
|
defaultModel = models[0]!.id;
|
||||||
console.log(`Only ${models.length} models available, using ${defaultModel} as default.`);
|
console.log(`Only ${models.length} models available, using ${defaultModel} for new Hermes configurations.`);
|
||||||
} else {
|
} else {
|
||||||
console.log("No models available from routstr daemon, using fallback default.");
|
console.log("No models available from routstr daemon, using fallback default.");
|
||||||
}
|
}
|
||||||
@@ -47,36 +102,24 @@ export async function installHermesIntegration(
|
|||||||
console.error(`Error reading ${configPath}, creating new one.`);
|
console.error(`Error reading ${configPath}, creating new one.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove existing model block
|
let newContent: string;
|
||||||
content = content.replace(/^model:\n(?: .*\n)*/gm, "");
|
try {
|
||||||
// Remove existing custom_providers block
|
newContent = mergeHermesConfig(content, {
|
||||||
content = content.replace(/^custom_providers:\n(?:- .*\n(?: .*\n)*)*/gm, "");
|
baseUrl: baseUrlV1,
|
||||||
// Clean up extra blank lines
|
apiKey,
|
||||||
content = content.replace(/\n{3,}/g, "\n\n").trim();
|
defaultModel,
|
||||||
|
});
|
||||||
const urlDisplay = baseUrl.replace(/^https?:\/\//, "");
|
} catch (error) {
|
||||||
|
console.error(`Failed to parse ${configPath} as YAML; leaving it unchanged:`, error);
|
||||||
const modelBlock = `model:
|
return;
|
||||||
default: ${defaultModel}
|
|
||||||
provider: custom
|
|
||||||
base_url: ${baseUrlV1}
|
|
||||||
api_key: ${apiKey}`;
|
|
||||||
|
|
||||||
const providerBlock = `custom_providers:
|
|
||||||
- name: Routstr (${urlDisplay})
|
|
||||||
base_url: ${baseUrlV1}
|
|
||||||
api_key: ${apiKey}
|
|
||||||
model: ${defaultModel}`;
|
|
||||||
|
|
||||||
const parts: string[] = [modelBlock];
|
|
||||||
if (content) {
|
|
||||||
parts.push(content);
|
|
||||||
}
|
}
|
||||||
parts.push(providerBlock);
|
|
||||||
|
|
||||||
const newContent = parts.join("\n\n") + "\n";
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (newContent === content) {
|
||||||
|
console.log(`${configPath} already contains current routstr settings.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
mkdirSync(dirname(configPath), { recursive: true });
|
mkdirSync(dirname(configPath), { recursive: true });
|
||||||
await writeFile(configPath, newContent);
|
await writeFile(configPath, newContent);
|
||||||
console.log(`Successfully updated ${configPath} with routstr settings.`);
|
console.log(`Successfully updated ${configPath} with routstr settings.`);
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import { describe, expect, it } from "bun:test";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
import { mergeHermesConfig } from "../../src/integrations/hermes";
|
||||||
|
|
||||||
|
const ROUTSTR = {
|
||||||
|
baseUrl: "http://localhost:8008/v1",
|
||||||
|
apiKey: "routstr-key",
|
||||||
|
defaultModel: "deepseek-v4-flash",
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("mergeHermesConfig", () => {
|
||||||
|
it("preserves the user's model and unrelated custom providers", () => {
|
||||||
|
const existing = `model:
|
||||||
|
default: user-model
|
||||||
|
provider: custom
|
||||||
|
base_url: https://user.example/v1
|
||||||
|
api_key: user-key
|
||||||
|
|
||||||
|
custom_providers:
|
||||||
|
- name: User Provider
|
||||||
|
base_url: https://user.example/v1
|
||||||
|
api_key: user-key
|
||||||
|
model: user-model
|
||||||
|
|
||||||
|
other_setting: true
|
||||||
|
`;
|
||||||
|
|
||||||
|
const merged = parse(mergeHermesConfig(existing, ROUTSTR));
|
||||||
|
|
||||||
|
expect(merged.model).toEqual({
|
||||||
|
default: "user-model",
|
||||||
|
provider: "custom",
|
||||||
|
base_url: "https://user.example/v1",
|
||||||
|
api_key: "user-key",
|
||||||
|
});
|
||||||
|
expect(merged.other_setting).toBe(true);
|
||||||
|
expect(merged.custom_providers).toContainEqual({
|
||||||
|
name: "User Provider",
|
||||||
|
base_url: "https://user.example/v1",
|
||||||
|
api_key: "user-key",
|
||||||
|
model: "user-model",
|
||||||
|
});
|
||||||
|
expect(merged.custom_providers).toContainEqual({
|
||||||
|
name: "Routstr (localhost:8008)",
|
||||||
|
base_url: ROUTSTR.baseUrl,
|
||||||
|
api_key: ROUTSTR.apiKey,
|
||||||
|
model: ROUTSTR.defaultModel,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not add a model configuration to an existing Hermes file", () => {
|
||||||
|
const merged = parse(mergeHermesConfig("other_setting: true\n", ROUTSTR));
|
||||||
|
|
||||||
|
expect(merged.model).toBeUndefined();
|
||||||
|
expect(merged.other_setting).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates a Routstr default when creating a new Hermes file", () => {
|
||||||
|
const merged = parse(mergeHermesConfig("", ROUTSTR));
|
||||||
|
|
||||||
|
expect(merged.model).toEqual({
|
||||||
|
default: ROUTSTR.defaultModel,
|
||||||
|
provider: "custom",
|
||||||
|
base_url: ROUTSTR.baseUrl,
|
||||||
|
api_key: ROUTSTR.apiKey,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps an existing Routstr provider unchanged without duplicating it", () => {
|
||||||
|
const existing = `custom_providers:
|
||||||
|
- name: Routstr (old-host:8008)
|
||||||
|
base_url: http://old-host:8008/v1
|
||||||
|
api_key: old-key
|
||||||
|
model: old-model
|
||||||
|
`;
|
||||||
|
|
||||||
|
const once = mergeHermesConfig(existing, ROUTSTR);
|
||||||
|
const twice = mergeHermesConfig(once, ROUTSTR);
|
||||||
|
const merged = parse(twice);
|
||||||
|
const routstrProviders = merged.custom_providers.filter(
|
||||||
|
(provider: { name?: string }) => provider.name?.startsWith("Routstr ("),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(routstrProviders).toHaveLength(1);
|
||||||
|
expect(routstrProviders[0]).toEqual({
|
||||||
|
name: "Routstr (old-host:8008)",
|
||||||
|
base_url: "http://old-host:8008/v1",
|
||||||
|
api_key: "old-key",
|
||||||
|
model: "old-model",
|
||||||
|
});
|
||||||
|
expect(twice).toBe(once);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects malformed YAML instead of replacing it", () => {
|
||||||
|
expect(() => mergeHermesConfig("model: [unterminated", ROUTSTR)).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user