mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-05 23:02:10 +03:00
* feat(api-keys): add rename support in permissions modal Add an editable key name field at the top of the permissions modal, allowing users to rename API keys alongside existing permission settings. The backend already supported name updates via PATCH /api/keys/:id — this wires the UI to send the name field and refreshes the key list on success. Changes: - Add keyName state and text input to PermissionsModal - Update handleUpdatePermissions to validate and send name in PATCH body - Add integration test for rename via PATCH (valid, empty, too-long names) - Update E2E mock to handle PATCH requests * chore(release): bump version to 3.7.6 * chore(release): v3.7.6 — merge API key rename feature and sync docs * chore(release): expand contributor credits to 155 PRs across full project history - Expanded acknowledgment table from 29 to 53 contributors - Added 100+ previously uncredited PRs from project inception through v3.7.5 - Moved contributor credits section to v3.7.6 (current release) - Synced llm.txt version to 3.7.6 * fix: resolve security ReDoS in codex and bugs #1797 #1789 * feat(dashboard): implement remaining v3.7.6 dashboard features and fixes * fix(xiaomi-mimo): update models to V2.5, fix Token Plan validation and default region (#1823) Integrated into release/v3.7.6 * fix(dashboard): correct loadPresets ReferenceError in CostOverviewTab * fix(codex): omit compact client metadata (#1822) Integrated into release/v3.7.6 * feat(chatgpt-web): support thinking_effort (Standard/Extended) for thinking-capable models (#1821) Integrated into release/v3.7.6 * Fix endpoint visibility, A2A status, and API catalog (#1806) Integrated into release/v3.7.6 * fix(analytics): use pure SQL aggregations — no history rows loaded (#1802) Integrated into release/v3.7.6 * fix(stability): resolve codex input validation, enable combo circuit breaker, and fix broken unit tests * docs(changelog): update for stability bug fixes #1804 #1805 * fix: clear active requests and recover providers (#1824) Integrated into release/v3.7.6 * feat: inject fallback tool names to prevent upstream 400 errors (#1775) * feat: auto-restore probe-failed database to prevent data loss (#1810) * fix: safely cast inputs to strings before calling trim() to avoid crashes on numeric fields in proxy modal (#1825) * chore(release): v3.7.6 — final stability patches for production * test: update expected db probe-failure error message for auto-restore feature * chore(workflow): mandate implementation plan generation in resolve-issues * docs(changelog): rewrite v3.7.6 with complete commit-accurate entries * feat(analytics): add cost-based usage insights and activity streaks Expand usage analytics to report total cost, per-series cost totals, API key counts, and current activity streaks using pricing-aware token calculations. Also make probe-failed database recovery choose the newest backup by its embedded timestamp instead of filesystem mtime so auto-restore selects the intended snapshot reliably. * fix(mitm): enforce transparent interception on port 443 only Reject non-443 MITM port updates in the settings API and normalize stored configuration back to the required transparent interception port. Lock the dashboard port field to 443, update the validation copy, and add integration coverage to prevent stale custom ports from being accepted or surfaced. * docs(changelog): update for analytics and mitm features --------- Co-authored-by: Andrew Munsell <andrew@wizardapps.net> Co-authored-by: Antigravity Assistant <bot@antigravity.local> Co-authored-by: Gi99lin <74502520+Gi99lin@users.noreply.github.com> Co-authored-by: Sergey Morozov <tr0st@bk.ru> Co-authored-by: payne <baboialex95@gmail.com> Co-authored-by: Randi <55005611+rdself@users.noreply.github.com> Co-authored-by: Paijo <14921983+oyi77@users.noreply.github.com> Co-authored-by: ipanghu <bypanghu@163.com>
849 lines
24 KiB
TypeScript
849 lines
24 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
|
|
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-db-settings-"));
|
|
process.env.DATA_DIR = TEST_DATA_DIR;
|
|
|
|
const ORIGINAL_INITIAL_PASSWORD = process.env.INITIAL_PASSWORD;
|
|
|
|
const core = await import("../../src/lib/db/core.ts");
|
|
const combosDb = await import("../../src/lib/db/combos.ts");
|
|
const providersDb = await import("../../src/lib/db/providers.ts");
|
|
const settingsDb = await import("../../src/lib/db/settings.ts");
|
|
|
|
async function resetStorage() {
|
|
core.resetDbInstance();
|
|
|
|
for (let attempt = 0; attempt < 10; attempt++) {
|
|
try {
|
|
if (fs.existsSync(TEST_DATA_DIR)) {
|
|
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
|
}
|
|
break;
|
|
} catch (error: any) {
|
|
if ((error?.code === "EBUSY" || error?.code === "EPERM") && attempt < 9) {
|
|
await new Promise((resolve) => setTimeout(resolve, 50 * (attempt + 1)));
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
|
delete process.env.INITIAL_PASSWORD;
|
|
}
|
|
|
|
test.beforeEach(async () => {
|
|
await resetStorage();
|
|
});
|
|
|
|
test.after(async () => {
|
|
core.resetDbInstance();
|
|
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
|
|
|
if (ORIGINAL_INITIAL_PASSWORD === undefined) {
|
|
delete process.env.INITIAL_PASSWORD;
|
|
} else {
|
|
process.env.INITIAL_PASSWORD = ORIGINAL_INITIAL_PASSWORD;
|
|
}
|
|
});
|
|
|
|
test("getSettings exposes defaults and updateSettings persists typed values", async () => {
|
|
const defaults = await settingsDb.getSettings();
|
|
const updated = await settingsDb.updateSettings({
|
|
requireLogin: false,
|
|
cloudEnabled: true,
|
|
stickyRoundRobinLimit: 7,
|
|
requestRetry: 5,
|
|
maxRetryIntervalSec: 12,
|
|
label: "task-303",
|
|
});
|
|
|
|
assert.equal(defaults.cloudEnabled, false);
|
|
assert.equal(defaults.requireLogin, true);
|
|
assert.deepEqual(defaults.hiddenSidebarItems, []);
|
|
assert.equal(defaults.idempotencyWindowMs, 5000);
|
|
assert.equal(defaults.requestRetry, 3);
|
|
assert.equal(defaults.maxRetryIntervalSec, 30);
|
|
assert.equal(defaults.antigravitySignatureCacheMode, "enabled");
|
|
assert.equal(defaults.comboConfigMode, "guided");
|
|
assert.equal(defaults.mcpEnabled, false);
|
|
assert.equal(defaults.a2aEnabled, false);
|
|
assert.equal(updated.requireLogin, false);
|
|
assert.equal(updated.cloudEnabled, true);
|
|
assert.equal(updated.stickyRoundRobinLimit, 7);
|
|
assert.equal(updated.requestRetry, 5);
|
|
assert.equal(updated.maxRetryIntervalSec, 12);
|
|
assert.equal(updated.antigravitySignatureCacheMode, "enabled");
|
|
assert.equal(updated.label, "task-303");
|
|
assert.equal(await settingsDb.isCloudEnabled(), true);
|
|
});
|
|
|
|
test("INITIAL_PASSWORD marks onboarding as complete on first read", async () => {
|
|
process.env.INITIAL_PASSWORD = "bootstrap-secret";
|
|
|
|
const settings = await settingsDb.getSettings();
|
|
const stored = await settingsDb.getSettings();
|
|
|
|
assert.equal(settings.setupComplete, true);
|
|
assert.equal(settings.requireLogin, true);
|
|
assert.equal(stored.setupComplete, true);
|
|
});
|
|
|
|
test("pricing layers merge synced, models.dev and user overrides", async () => {
|
|
const db = core.getDbInstance();
|
|
|
|
db.prepare("INSERT INTO key_value (namespace, key, value) VALUES (?, ?, ?)").run(
|
|
"pricing_synced",
|
|
"layered-provider",
|
|
JSON.stringify({
|
|
"model-a": { prompt: 1, completion: 2 },
|
|
})
|
|
);
|
|
db.prepare("INSERT INTO key_value (namespace, key, value) VALUES (?, ?, ?)").run(
|
|
"models_dev_pricing",
|
|
"layered-provider",
|
|
JSON.stringify({
|
|
"model-a": { completion: 5, cached: 3 },
|
|
})
|
|
);
|
|
|
|
await settingsDb.updatePricing({
|
|
"layered-provider": {
|
|
"model-a": { prompt: 9, custom: 42 },
|
|
"model-b": { prompt: 7 },
|
|
},
|
|
});
|
|
|
|
const pricing = await settingsDb.getPricing();
|
|
const direct = await settingsDb.getPricingForModel("layered-provider", "model-a");
|
|
const cnFallback = await settingsDb.getPricingForModel("openai-cn", "gpt-4o");
|
|
|
|
assert.deepEqual(pricing["layered-provider"]["model-a"], {
|
|
prompt: 9,
|
|
completion: 5,
|
|
cached: 3,
|
|
custom: 42,
|
|
});
|
|
assert.deepEqual(direct, {
|
|
prompt: 9,
|
|
completion: 5,
|
|
cached: 3,
|
|
custom: 42,
|
|
});
|
|
assert.ok(cnFallback);
|
|
|
|
const afterModelReset = await settingsDb.resetPricing("layered-provider", "model-a");
|
|
assert.equal(afterModelReset["layered-provider"]["model-a"], undefined);
|
|
|
|
const afterProviderReset = await settingsDb.resetPricing("layered-provider");
|
|
assert.equal(afterProviderReset["layered-provider"], undefined);
|
|
|
|
await settingsDb.updatePricing({
|
|
temp: { model: { prompt: 1 } },
|
|
});
|
|
assert.deepEqual(await settingsDb.resetAllPricing(), {});
|
|
});
|
|
|
|
test("getPricingWithSources reports the winning layer for each provider/model", async () => {
|
|
const db = core.getDbInstance();
|
|
|
|
db.prepare("INSERT INTO key_value (namespace, key, value) VALUES (?, ?, ?)").run(
|
|
"pricing_synced",
|
|
"layer-source",
|
|
JSON.stringify({
|
|
"model-litellm": { prompt: 1, completion: 2 },
|
|
"model-user": { prompt: 3 },
|
|
})
|
|
);
|
|
db.prepare("INSERT INTO key_value (namespace, key, value) VALUES (?, ?, ?)").run(
|
|
"models_dev_pricing",
|
|
"layer-source",
|
|
JSON.stringify({
|
|
"model-modelsdev": { prompt: 4, completion: 5 },
|
|
"model-user": { completion: 6 },
|
|
})
|
|
);
|
|
|
|
await settingsDb.updatePricing({
|
|
"layer-source": {
|
|
"model-user": { cached: 7 },
|
|
},
|
|
});
|
|
|
|
const { pricing, sourceMap } = await settingsDb.getPricingWithSources();
|
|
|
|
assert.deepEqual(pricing["layer-source"]["model-litellm"], {
|
|
prompt: 1,
|
|
completion: 2,
|
|
});
|
|
assert.deepEqual(pricing["layer-source"]["model-modelsdev"], {
|
|
prompt: 4,
|
|
completion: 5,
|
|
});
|
|
assert.deepEqual(pricing["layer-source"]["model-user"], {
|
|
prompt: 3,
|
|
completion: 6,
|
|
cached: 7,
|
|
});
|
|
assert.equal(sourceMap["layer-source"]["model-litellm"], "litellm");
|
|
assert.equal(sourceMap["layer-source"]["model-modelsdev"], "modelsDev");
|
|
assert.equal(sourceMap["layer-source"]["model-user"], "user");
|
|
assert.equal(sourceMap.openai["gpt-4o"], "default");
|
|
});
|
|
|
|
test("LKGP values can be set, read and cleared", async () => {
|
|
assert.equal(await settingsDb.getLKGP("combo-a", "model-a"), null);
|
|
|
|
await settingsDb.setLKGP("combo-a", "model-a", "openai");
|
|
await settingsDb.setLKGP("combo-a", "model-b", "anthropic");
|
|
|
|
assert.equal(await settingsDb.getLKGP("combo-a", "model-a"), "openai");
|
|
assert.equal(await settingsDb.getLKGP("combo-a", "model-b"), "anthropic");
|
|
|
|
settingsDb.clearAllLKGP();
|
|
|
|
assert.equal(await settingsDb.getLKGP("combo-a", "model-a"), null);
|
|
});
|
|
|
|
test("pricing helpers ignore malformed synced data and LKGP falls back to raw values", async () => {
|
|
const db = core.getDbInstance();
|
|
|
|
db.prepare("INSERT INTO key_value (namespace, key, value) VALUES (?, ?, ?)").run(
|
|
"models_dev_pricing",
|
|
"broken-provider",
|
|
"{not-json"
|
|
);
|
|
db.prepare("INSERT INTO key_value (namespace, key, value) VALUES (?, ?, ?)").run(
|
|
"pricing",
|
|
"alias-provider",
|
|
JSON.stringify({
|
|
"model-a": { prompt: 7 },
|
|
})
|
|
);
|
|
db.prepare("INSERT INTO key_value (namespace, key, value) VALUES (?, ?, ?)").run(
|
|
"lkgp",
|
|
"combo-raw:model-raw",
|
|
"raw-provider-id"
|
|
);
|
|
|
|
const pricing = await settingsDb.getPricing();
|
|
|
|
assert.equal(pricing["broken-provider"], undefined);
|
|
assert.equal(await settingsDb.getPricingForModel("alias-provider", "missing-model"), null);
|
|
assert.equal(await settingsDb.getLKGP("combo-raw", "model-raw"), "raw-provider-id");
|
|
});
|
|
|
|
test("pricing helpers resolve aliased providers and tolerate no-op resets", async () => {
|
|
const db = core.getDbInstance();
|
|
|
|
db.prepare("INSERT INTO key_value (namespace, key, value) VALUES (?, ?, ?)").run(
|
|
"pricing",
|
|
"cc",
|
|
JSON.stringify({
|
|
"claude-3-5-sonnet": { prompt: 4, completion: 6 },
|
|
})
|
|
);
|
|
|
|
const aliasPricing = await settingsDb.getPricingForModel("claude", "claude-3-5-sonnet");
|
|
const missingPricing = await settingsDb.getPricingForModel("missing-provider", "missing-model");
|
|
const afterUnknownReset = await settingsDb.resetPricing("missing-provider", "missing-model");
|
|
|
|
assert.deepEqual(aliasPricing, { prompt: 4, completion: 6 });
|
|
assert.equal(missingPricing, null);
|
|
assert.equal(afterUnknownReset["missing-provider"], undefined);
|
|
});
|
|
|
|
test("settings and pricing readers skip malformed rows while merging surviving layers", async () => {
|
|
const db = core.getDbInstance();
|
|
const originalPrepare = db.prepare.bind(db);
|
|
|
|
db.prepare = (sql) => {
|
|
const text = String(sql);
|
|
|
|
if (text.includes("namespace = 'settings'")) {
|
|
return {
|
|
all: () => [
|
|
123,
|
|
{ key: 456, value: "true" },
|
|
{ key: "cloudEnabled", value: "true" },
|
|
{ key: "requireLogin", value: null },
|
|
],
|
|
};
|
|
}
|
|
|
|
if (text === "SELECT key, value FROM key_value WHERE namespace = ?") {
|
|
return {
|
|
all: (namespace) => {
|
|
if (namespace === "pricing_synced") {
|
|
return [
|
|
123,
|
|
{ key: 456, value: JSON.stringify({ ignored: true }) },
|
|
{
|
|
key: "layered-provider",
|
|
value: JSON.stringify({
|
|
"model-a": { prompt: 1, completion: 2 },
|
|
}),
|
|
},
|
|
];
|
|
}
|
|
|
|
if (namespace === "models_dev_pricing") {
|
|
return [
|
|
{ key: "broken-provider", value: "{bad" },
|
|
{ key: "missing-value", value: null },
|
|
{
|
|
key: "layered-provider",
|
|
value: JSON.stringify({
|
|
"model-a": { cached: 3 },
|
|
}),
|
|
},
|
|
];
|
|
}
|
|
|
|
if (namespace === "pricing") {
|
|
return [
|
|
{
|
|
key: "layered-provider",
|
|
value: JSON.stringify({
|
|
"model-a": { prompt: 9, custom: 42 },
|
|
"model-b": { prompt: 7 },
|
|
}),
|
|
},
|
|
{ key: null, value: JSON.stringify({ ignored: true }) },
|
|
];
|
|
}
|
|
|
|
return originalPrepare(sql).all(namespace);
|
|
},
|
|
};
|
|
}
|
|
|
|
return originalPrepare(sql);
|
|
};
|
|
|
|
try {
|
|
const settings = await settingsDb.getSettings();
|
|
const pricing = await settingsDb.getPricing();
|
|
const modelPricing = await settingsDb.getPricingForModel("layered-provider", "model-a");
|
|
|
|
assert.equal(settings.cloudEnabled, true);
|
|
assert.equal(settings.requireLogin, true);
|
|
assert.deepEqual(pricing["layered-provider"]["model-a"], {
|
|
prompt: 9,
|
|
completion: 2,
|
|
cached: 3,
|
|
custom: 42,
|
|
});
|
|
assert.deepEqual(modelPricing, {
|
|
prompt: 9,
|
|
completion: 2,
|
|
cached: 3,
|
|
custom: 42,
|
|
});
|
|
assert.equal(pricing["broken-provider"], undefined);
|
|
} finally {
|
|
db.prepare = originalPrepare;
|
|
}
|
|
});
|
|
|
|
test("proxy config migrates legacy strings and supports bulk merge updates", async () => {
|
|
const db = core.getDbInstance();
|
|
|
|
db.prepare("INSERT INTO key_value (namespace, key, value) VALUES (?, ?, ?)").run(
|
|
"proxyConfig",
|
|
"global",
|
|
JSON.stringify("http://user:pass@global.local:8080")
|
|
);
|
|
db.prepare("INSERT INTO key_value (namespace, key, value) VALUES (?, ?, ?)").run(
|
|
"proxyConfig",
|
|
"providers",
|
|
JSON.stringify({
|
|
openai: "https://provider.local:8443",
|
|
})
|
|
);
|
|
|
|
const migrated = await settingsDb.getProxyConfig();
|
|
assert.deepEqual(migrated.global, {
|
|
type: "http",
|
|
host: "global.local",
|
|
port: "8080",
|
|
username: "user",
|
|
password: "pass",
|
|
});
|
|
assert.deepEqual(migrated.providers.openai, {
|
|
type: "https",
|
|
host: "provider.local",
|
|
port: "8443",
|
|
username: "",
|
|
password: "",
|
|
});
|
|
|
|
const merged = await settingsDb.setProxyConfig({
|
|
providers: {
|
|
openai: null,
|
|
anthropic: {
|
|
type: "http",
|
|
host: "anthropic.local",
|
|
port: 9000,
|
|
},
|
|
},
|
|
keys: {
|
|
key123: {
|
|
type: "socks5",
|
|
host: "key.local",
|
|
port: 1080,
|
|
},
|
|
},
|
|
});
|
|
|
|
assert.equal(merged.providers.openai, undefined);
|
|
assert.equal(merged.providers.anthropic.host, "anthropic.local");
|
|
assert.equal((await settingsDb.getProxyForLevel("key", "key123")).host, "key.local");
|
|
|
|
await settingsDb.deleteProxyForLevel("key", "key123");
|
|
|
|
assert.equal(await settingsDb.getProxyForLevel("key", "key123"), null);
|
|
});
|
|
|
|
test("proxy config migrates socks5 and host-only entries while preserving plural lookups", async () => {
|
|
const db = core.getDbInstance();
|
|
|
|
db.prepare("INSERT INTO key_value (namespace, key, value) VALUES (?, ?, ?)").run(
|
|
"proxyConfig",
|
|
"global",
|
|
JSON.stringify("fallback-only-host")
|
|
);
|
|
db.prepare("INSERT INTO key_value (namespace, key, value) VALUES (?, ?, ?)").run(
|
|
"proxyConfig",
|
|
"providers",
|
|
JSON.stringify({
|
|
claude: "socks5://sockshost",
|
|
})
|
|
);
|
|
|
|
const migrated = await settingsDb.getProxyConfig();
|
|
assert.deepEqual(migrated.global, {
|
|
type: "http",
|
|
host: "fallback-only-host",
|
|
port: "8080",
|
|
username: "",
|
|
password: "",
|
|
});
|
|
assert.deepEqual(migrated.providers.claude, {
|
|
type: "socks5",
|
|
host: "sockshost",
|
|
port: "1080",
|
|
username: "",
|
|
password: "",
|
|
});
|
|
assert.equal((await settingsDb.getProxyForLevel("providers", "claude")).host, "sockshost");
|
|
|
|
const updated = await settingsDb.setProxyConfig({
|
|
global: null,
|
|
providers: {},
|
|
});
|
|
|
|
assert.equal(updated.global, null);
|
|
assert.equal(await settingsDb.getProxyForLevel("global"), null);
|
|
|
|
await settingsDb.deleteProxyForLevel("provider", null);
|
|
|
|
assert.equal((await settingsDb.getProxyForLevel("provider", "claude")).host, "sockshost");
|
|
});
|
|
|
|
test("proxy helpers resolve key, provider, global, and direct paths while tolerating malformed combo rows", async () => {
|
|
const db = core.getDbInstance();
|
|
const connection = await providersDb.createProviderConnection({
|
|
provider: "openai",
|
|
authType: "apikey",
|
|
name: "Proxy Resolution Target",
|
|
apiKey: "sk-proxy-resolution",
|
|
});
|
|
|
|
await settingsDb.setProxyConfig({
|
|
level: "global",
|
|
proxy: {
|
|
type: "http",
|
|
host: "global.local",
|
|
port: 8080,
|
|
},
|
|
});
|
|
await settingsDb.setProxyForLevel("provider", "openai", {
|
|
type: "https",
|
|
host: "provider.local",
|
|
port: 8443,
|
|
});
|
|
await settingsDb.setProxyForLevel("combo", "combo-broken", {
|
|
type: "socks5",
|
|
host: "combo.local",
|
|
port: 1080,
|
|
});
|
|
const combo = await combosDb.createCombo({
|
|
name: "combo-broken",
|
|
models: ["openai/gpt-4o-mini"],
|
|
strategy: "priority",
|
|
});
|
|
db.prepare("UPDATE combos SET data = ? WHERE id = ?").run("{not-json", combo.id);
|
|
|
|
const providerResolved = await settingsDb.resolveProxyForConnection((connection as any).id);
|
|
|
|
assert.equal(providerResolved.level, "provider");
|
|
assert.equal(providerResolved.proxy.host, "provider.local");
|
|
assert.deepEqual(await settingsDb.getProxyForLevel("combo", "combo-broken"), {
|
|
type: "socks5",
|
|
host: "combo.local",
|
|
port: 1080,
|
|
});
|
|
|
|
await settingsDb.deleteProxyForLevel("provider", "openai");
|
|
|
|
const globalResolved = await settingsDb.resolveProxyForConnection((connection as any).id);
|
|
|
|
assert.equal(globalResolved.level, "global");
|
|
assert.equal(globalResolved.proxy.host, "global.local");
|
|
|
|
await settingsDb.setProxyForLevel("key", (connection as any).id, {
|
|
type: "http",
|
|
host: "key.local",
|
|
port: 3128,
|
|
});
|
|
|
|
const keyResolved = await settingsDb.resolveProxyForConnection((connection as any).id);
|
|
|
|
assert.equal(keyResolved.level, "key");
|
|
assert.equal(keyResolved.proxy.host, "key.local");
|
|
|
|
await settingsDb.deleteProxyForLevel("key", (connection as any).id);
|
|
await settingsDb.deleteProxyForLevel("global", null);
|
|
|
|
const directResolved = await settingsDb.resolveProxyForConnection((connection as any).id);
|
|
|
|
assert.equal(directResolved.level, "direct");
|
|
assert.equal(directResolved.proxy, null);
|
|
});
|
|
|
|
test("proxy resolution skips combos without serialized data and falls back to provider proxies", async () => {
|
|
const db = core.getDbInstance();
|
|
const connection = await providersDb.createProviderConnection({
|
|
provider: "claude",
|
|
authType: "apikey",
|
|
name: "Proxy Null Combo",
|
|
apiKey: "sk-claude-proxy",
|
|
});
|
|
|
|
await settingsDb.setProxyForLevel("provider", "claude", {
|
|
type: "https",
|
|
host: "provider-claude.local",
|
|
port: 443,
|
|
});
|
|
|
|
const combo = await combosDb.createCombo({
|
|
name: "combo-null-data",
|
|
models: ["claude/claude-3-5-sonnet"],
|
|
strategy: "priority",
|
|
});
|
|
await settingsDb.setProxyForLevel("combo" as any, (combo as any).id, {
|
|
type: "http",
|
|
host: "combo-null.local",
|
|
port: 8080,
|
|
});
|
|
db.prepare("UPDATE combos SET data = ? WHERE id = ?").run(0, combo.id);
|
|
|
|
const resolved = await settingsDb.resolveProxyForConnection((connection as any).id);
|
|
|
|
assert.equal(resolved.level, "provider");
|
|
assert.equal(resolved.proxy.host, "provider-claude.local");
|
|
});
|
|
|
|
test("proxy resolution matches combo proxies through aliased model entries", async () => {
|
|
const connection = await providersDb.createProviderConnection({
|
|
provider: "claude",
|
|
authType: "apikey",
|
|
name: "Proxy Alias Combo",
|
|
apiKey: "sk-claude-alias",
|
|
});
|
|
|
|
const combo = await combosDb.createCombo({
|
|
name: "combo-aliased-model",
|
|
models: [{ model: "cc/claude-3-5-sonnet" }],
|
|
strategy: "priority",
|
|
});
|
|
await settingsDb.setProxyForLevel("combo", (combo as any).id, {
|
|
type: "https",
|
|
host: "combo-alias.local",
|
|
port: 443,
|
|
});
|
|
|
|
const resolved = await settingsDb.resolveProxyForConnection((connection as any).id);
|
|
|
|
assert.equal(resolved.level, "combo");
|
|
assert.equal(resolved.levelId, combo.id);
|
|
assert.equal(resolved.proxy.host, "combo-alias.local");
|
|
});
|
|
|
|
test("proxy readers normalize legacy rows, skip malformed entries, and coerce invalid globals to null", async () => {
|
|
const db = core.getDbInstance();
|
|
const originalPrepare = db.prepare.bind(db);
|
|
|
|
db.prepare = (sql) => {
|
|
const text = String(sql);
|
|
|
|
if (text.includes("namespace = 'proxyConfig'") && text.startsWith("SELECT")) {
|
|
return {
|
|
all: () => [
|
|
123,
|
|
{ key: 456, value: JSON.stringify({ ignored: true }) },
|
|
{ key: "global", value: JSON.stringify("https://user%40name:pass%2Fword@proxy.example") },
|
|
{
|
|
key: "providers",
|
|
value: JSON.stringify({
|
|
openai: "http://provider.example",
|
|
}),
|
|
},
|
|
{ key: "combos", value: JSON.stringify("not-a-map") },
|
|
{ key: "keys", value: null },
|
|
],
|
|
};
|
|
}
|
|
|
|
if (text.includes("namespace = 'proxyConfig'") && text.startsWith("INSERT OR REPLACE")) {
|
|
return { run: () => ({ changes: 1 }) };
|
|
}
|
|
|
|
return originalPrepare(sql);
|
|
};
|
|
|
|
try {
|
|
const config = await settingsDb.getProxyConfig();
|
|
assert.deepEqual(config.global, {
|
|
type: "https",
|
|
host: "proxy.example",
|
|
port: "443",
|
|
username: "user@name",
|
|
password: "pass/word",
|
|
});
|
|
assert.deepEqual(config.providers.openai, {
|
|
type: "http",
|
|
host: "provider.example",
|
|
port: "8080",
|
|
username: "",
|
|
password: "",
|
|
});
|
|
assert.equal(await settingsDb.getProxyForLevel("combo", "missing"), null);
|
|
} finally {
|
|
db.prepare = originalPrepare;
|
|
}
|
|
|
|
const updated = await settingsDb.setProxyConfig({ level: 123, id: 456, proxy: 0 });
|
|
assert.equal(updated.global, null);
|
|
assert.equal(await settingsDb.getProxyForLevel("global"), null);
|
|
});
|
|
|
|
test("cache metrics, trend and no-op update/reset methods read from usage_history", async () => {
|
|
const db = core.getDbInstance();
|
|
const now = new Date().toISOString();
|
|
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
|
|
|
|
const insertUsage = db.prepare(`
|
|
INSERT INTO usage_history (
|
|
provider, model, connection_id, api_key_id, api_key_name,
|
|
tokens_input, tokens_output, tokens_cache_read, tokens_cache_creation,
|
|
tokens_reasoning, status, success, latency_ms, ttft_ms, error_code, timestamp
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`);
|
|
|
|
insertUsage.run(
|
|
"openai",
|
|
"gpt-4.1",
|
|
"conn-1",
|
|
"key-1",
|
|
"Primary",
|
|
1000,
|
|
400,
|
|
300,
|
|
120,
|
|
0,
|
|
"200",
|
|
1,
|
|
100,
|
|
40,
|
|
null,
|
|
oneHourAgo
|
|
);
|
|
insertUsage.run(
|
|
"anthropic",
|
|
"claude-3-7-sonnet",
|
|
"conn-2",
|
|
"key-2",
|
|
"Secondary",
|
|
700,
|
|
280,
|
|
200,
|
|
80,
|
|
0,
|
|
"200",
|
|
1,
|
|
90,
|
|
30,
|
|
null,
|
|
now
|
|
);
|
|
|
|
const metrics = await settingsDb.getCacheMetrics();
|
|
const trend = await settingsDb.getCacheTrend(4);
|
|
const updateNoOp = await settingsDb.updateCacheMetrics({ anything: true });
|
|
const resetNoOp = await settingsDb.resetCacheMetrics();
|
|
|
|
assert.ok(metrics.totalRequests >= 2);
|
|
assert.ok(metrics.requestsWithCacheControl >= 2);
|
|
assert.ok(metrics.byProvider.openai);
|
|
assert.ok(metrics.byProvider.anthropic);
|
|
assert.ok(trend.length >= 1);
|
|
assert.equal(updateNoOp.totalCachedTokens, metrics.totalCachedTokens);
|
|
assert.equal(resetNoOp.totalCachedTokens, metrics.totalCachedTokens);
|
|
});
|
|
|
|
test("cache metric helpers degrade gracefully when SQLite aggregation fails", async () => {
|
|
const db = core.getDbInstance();
|
|
const originalPrepare = db.prepare.bind(db);
|
|
db.prepare = () => {
|
|
throw new Error("db offline");
|
|
};
|
|
|
|
try {
|
|
const metrics = await settingsDb.getCacheMetrics();
|
|
const updated = await settingsDb.updateCacheMetrics({ force: true });
|
|
const trend = await settingsDb.getCacheTrend(6);
|
|
const reset = await settingsDb.resetCacheMetrics();
|
|
|
|
assert.equal(metrics.totalRequests, 0);
|
|
assert.equal(updated.totalCachedTokens, 0);
|
|
assert.deepEqual(trend, []);
|
|
assert.equal(reset.requestsWithCacheControl, 0);
|
|
} finally {
|
|
db.prepare = originalPrepare;
|
|
}
|
|
});
|
|
|
|
test("cache metrics and trend coerce null aggregate fields to zero", async () => {
|
|
const db = core.getDbInstance();
|
|
const originalPrepare = db.prepare.bind(db);
|
|
|
|
db.prepare = (sql) => {
|
|
const text = String(sql);
|
|
|
|
if (
|
|
text.includes("COUNT(*) as totalRequests") &&
|
|
text.includes("SUM(tokens_input) as totalInputTokens") &&
|
|
!text.includes("GROUP BY")
|
|
) {
|
|
return {
|
|
get: () => ({
|
|
totalRequests: 2,
|
|
totalInputTokens: null,
|
|
totalCachedTokens: null,
|
|
totalCacheCreationTokens: null,
|
|
}),
|
|
};
|
|
}
|
|
|
|
if (text.match(/SELECT\s+COUNT\(\*\)\s+as\s+totalRequests\s+FROM\s+usage_history\s*$/)) {
|
|
return {
|
|
get: () => ({
|
|
totalRequests: 5,
|
|
}),
|
|
};
|
|
}
|
|
|
|
if (text.includes("GROUP BY provider")) {
|
|
return {
|
|
all: () => [
|
|
{
|
|
provider: "openai",
|
|
totalRequests: 1,
|
|
cachedRequests: 1,
|
|
inputTokens: null,
|
|
cachedTokens: null,
|
|
cacheCreationTokens: null,
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
if (text.includes("GROUP BY 'direct'")) {
|
|
return {
|
|
all: () => [
|
|
{
|
|
strategy: "direct",
|
|
requests: 2,
|
|
inputTokens: null,
|
|
cachedTokens: null,
|
|
cacheCreationTokens: null,
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
if (text.includes("strftime('%Y-%m-%dT%H:00:00Z', timestamp) as hour")) {
|
|
return {
|
|
all: () => [
|
|
{
|
|
hour: "2026-01-01T10:00:00Z",
|
|
requests: 3,
|
|
cachedRequests: 1,
|
|
inputTokens: null,
|
|
cachedTokens: null,
|
|
cacheCreationTokens: null,
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
return originalPrepare(sql);
|
|
};
|
|
|
|
try {
|
|
const metrics = await settingsDb.getCacheMetrics();
|
|
const updated = await settingsDb.updateCacheMetrics({ force: true });
|
|
const trend = await settingsDb.getCacheTrend(2);
|
|
const reset = await settingsDb.resetCacheMetrics();
|
|
|
|
assert.equal(metrics.totalRequests, 5);
|
|
assert.equal(metrics.totalInputTokens, 0);
|
|
assert.equal(metrics.totalCachedTokens, 0);
|
|
assert.equal(metrics.totalCacheCreationTokens, 0);
|
|
assert.deepEqual(metrics.byProvider.openai, {
|
|
requests: 1,
|
|
totalRequests: 1,
|
|
cachedRequests: 1,
|
|
inputTokens: 0,
|
|
cachedTokens: 0,
|
|
cacheCreationTokens: 0,
|
|
});
|
|
assert.deepEqual(metrics.byStrategy.direct, {
|
|
requests: 2,
|
|
inputTokens: 0,
|
|
cachedTokens: 0,
|
|
cacheCreationTokens: 0,
|
|
});
|
|
assert.deepEqual(trend, [
|
|
{
|
|
timestamp: "2026-01-01T10:00:00Z",
|
|
requests: 3,
|
|
cachedRequests: 1,
|
|
inputTokens: 0,
|
|
cachedTokens: 0,
|
|
cacheCreationTokens: 0,
|
|
},
|
|
]);
|
|
assert.equal(updated.totalCachedTokens, 0);
|
|
assert.equal(reset.totalCachedTokens, 0);
|
|
} finally {
|
|
db.prepare = originalPrepare;
|
|
}
|
|
});
|