Merge pull request #914 from oyi77/feat/cliproxyapi-1-schema-settings

feat(cliproxyapi): DB schema, upstream proxy config & settings UI
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-04-02 14:36:31 -03:00
committed by GitHub
17 changed files with 1969 additions and 2 deletions

250
.omc/project-memory.json Normal file
View File

@@ -0,0 +1,250 @@
{
"version": "1.0.0",
"lastScanned": 1775016362438,
"projectRoot": "/home/openclaw/omniroute-src",
"techStack": {
"languages": [
{
"name": "JavaScript/TypeScript",
"version": ">=18.0.0 <24.0.0",
"confidence": "high",
"markers": ["package.json"]
},
{
"name": "TypeScript",
"version": null,
"confidence": "high",
"markers": ["tsconfig.json"]
}
],
"frameworks": [
{
"name": "express",
"version": "5.2.1",
"category": "backend"
},
{
"name": "next",
"version": "16.0.10",
"category": "fullstack"
},
{
"name": "react",
"version": "19.2.4",
"category": "frontend"
},
{
"name": "react-dom",
"version": "19.2.4",
"category": "frontend"
},
{
"name": "@playwright/test",
"version": "1.58.2",
"category": "testing"
},
{
"name": "vitest",
"version": "4.0.18",
"category": "testing"
}
],
"packageManager": "npm",
"runtime": "Node.js 18.0.024.0.0"
},
"build": {
"buildCommand": "npm run build",
"testCommand": "npm test",
"lintCommand": "npm run lint",
"devCommand": "npm run dev",
"scripts": {
"dev": "node scripts/run-next.mjs dev",
"build": "node scripts/build-next-isolated.mjs",
"build:cli": "node scripts/prepublish.mjs",
"start": "node scripts/run-next.mjs start",
"lint": "eslint .",
"electron:dev": "concurrently \"npm run dev\" \"wait-on http://localhost:20128 && cd electron && npm run dev\"",
"electron:build": "npm run build && cd electron && npm run build",
"electron:build:win": "npm run build && cd electron && npm run build:win",
"electron:build:mac": "npm run build && cd electron && npm run build:mac",
"electron:build:linux": "npm run build && cd electron && npm run build:linux",
"test": "node --import tsx/esm --test tests/unit/*.test.mjs",
"test:unit": "node --import tsx/esm --test tests/unit/*.test.mjs",
"test:plan3": "node --import tsx/esm --test tests/unit/plan3-p0.test.mjs",
"test:fixes": "node --import tsx/esm --test tests/unit/fixes-p1.test.mjs",
"test:security": "node --import tsx/esm --test tests/unit/security-fase01.test.mjs",
"check:cycles": "node scripts/check-cycles.mjs",
"check:route-validation:t06": "node scripts/check-route-validation.mjs",
"check:any-budget:t11": "node scripts/check-t11-any-budget.mjs",
"check:docs-sync": "node scripts/check-docs-sync.mjs",
"typecheck:core": "tsc --pretty false -p tsconfig.typecheck-core.json",
"typecheck:noimplicit:core": "tsc --pretty false -p tsconfig.typecheck-noimplicit-core.json",
"test:integration": "node --import tsx/esm --test tests/integration/*.test.mjs",
"test:e2e": "node scripts/run-playwright-tests.mjs test tests/e2e/*.spec.ts",
"test:protocols:e2e": "node scripts/run-protocol-clients-tests.mjs",
"test:vitest": "vitest run open-sse/mcp-server/__tests__/*.test.ts open-sse/services/autoCombo/__tests__/*.test.ts",
"test:ecosystem": "node scripts/run-ecosystem-tests.mjs",
"test:coverage": "c8 --exclude=tests/** --exclude=**/*.test.* --reporter=text-summary --reporter=html --reporter=json-summary --reporter=lcov --check-coverage --statements 55 --lines 55 --functions 55 --branches 60 node --import tsx/esm --test tests/unit/*.test.mjs",
"test:coverage:legacy": "c8 --exclude=open-sse --check-coverage --lines 50 --functions 50 --branches 50 node --import tsx/esm --test tests/unit/*.test.mjs",
"coverage:report": "c8 report --exclude=tests/** --exclude=**/*.test.* --reporter=text --reporter=text-summary --reporter=html --reporter=json-summary --reporter=lcov",
"coverage:report:legacy": "c8 report --exclude=open-sse --reporter=text --reporter=text-summary",
"test:all": "npm run test:unit && npm run test:vitest && npm run test:ecosystem && npm run test:e2e",
"check": "npm run lint && npm run test",
"prepublishOnly": "npm run build:cli",
"postinstall": "node scripts/postinstall.mjs",
"prepare": "husky",
"system-info": "node scripts/system-info.mjs"
}
},
"conventions": {
"namingStyle": "camelCase",
"importStyle": null,
"testPattern": null,
"fileOrganization": null
},
"structure": {
"isMonorepo": true,
"workspaces": ["open-sse"],
"mainDirectories": ["bin", "docs", "public", "scripts", "src", "tests"],
"gitBranches": {
"defaultBranch": "main",
"branchingStrategy": null
}
},
"customNotes": [],
"directoryMap": {
"bin": {
"path": "bin",
"purpose": "Executable scripts",
"fileCount": 3,
"lastAccessed": 1775016362426,
"keyFiles": ["mcp-server.mjs", "omniroute.mjs", "reset-password.mjs"]
},
"docs": {
"path": "docs",
"purpose": "Documentation",
"fileCount": 14,
"lastAccessed": 1775016362426,
"keyFiles": [
"A2A-SERVER.md",
"API_REFERENCE.md",
"ARCHITECTURE.md",
"AUTO-COMBO.md",
"CLI-TOOLS.md"
]
},
"electron": {
"path": "electron",
"purpose": null,
"fileCount": 5,
"lastAccessed": 1775016362431,
"keyFiles": ["README.md", "main.js", "package.json", "preload.js", "types.d.ts"]
},
"images": {
"path": "images",
"purpose": null,
"fileCount": 1,
"lastAccessed": 1775016362434,
"keyFiles": ["omniroute.png"]
},
"logs": {
"path": "logs",
"purpose": null,
"fileCount": 3,
"lastAccessed": 1775016362434,
"keyFiles": ["build_clean_tools.log", "build_debug.log", "build_force_clean.log"]
},
"open-sse": {
"path": "open-sse",
"purpose": null,
"fileCount": 5,
"lastAccessed": 1775016362434,
"keyFiles": ["index.ts", "package.json", "tsconfig.json", "types.d.ts"]
},
"public": {
"path": "public",
"purpose": "Public files",
"fileCount": 3,
"lastAccessed": 1775016362435,
"keyFiles": ["apple-touch-icon.svg", "favicon.svg", "icon-192.svg"]
},
"scripts": {
"path": "scripts",
"purpose": "Build/utility scripts",
"fileCount": 23,
"lastAccessed": 1775016362435,
"keyFiles": [
"bootstrap-env.mjs",
"build-next-isolated.mjs",
"check-cycles.mjs",
"check-docs-sync.mjs",
"check-route-validation.mjs"
]
},
"src": {
"path": "src",
"purpose": "Source code",
"fileCount": 4,
"lastAccessed": 1775016362435,
"keyFiles": ["instrumentation-node.ts", "instrumentation.ts", "proxy.ts", "server-init.ts"]
},
"tests": {
"path": "tests",
"purpose": "Test files",
"fileCount": 0,
"lastAccessed": 1775016362435,
"keyFiles": []
},
"electron/assets": {
"path": "electron/assets",
"purpose": "Static assets",
"fileCount": 4,
"lastAccessed": 1775016362436,
"keyFiles": ["icon.icns", "icon.ico", "icon.png"]
},
"open-sse/config": {
"path": "open-sse/config",
"purpose": "Configuration files",
"fileCount": 17,
"lastAccessed": 1775016362436,
"keyFiles": ["audioRegistry.ts", "cliFingerprints.ts", "codexInstructions.ts"]
},
"open-sse/services": {
"path": "open-sse/services",
"purpose": "Business logic services",
"fileCount": 35,
"lastAccessed": 1775016362437,
"keyFiles": ["accountFallback.ts", "accountSelector.ts", "apiKeyRotator.ts"]
},
"src/app": {
"path": "src/app",
"purpose": "Application code",
"fileCount": 7,
"lastAccessed": 1775016362438,
"keyFiles": ["error.tsx", "global-error.tsx", "globals.css"]
},
"src/lib": {
"path": "src/lib",
"purpose": "Library code",
"fileCount": 30,
"lastAccessed": 1775016362438,
"keyFiles": ["apiBridgeServer.ts", "apiKeyExposure.ts", "cacheControlSettings.ts"]
},
"src/middleware": {
"path": "src/middleware",
"purpose": "Middleware",
"fileCount": 1,
"lastAccessed": 1775016362438,
"keyFiles": ["promptInjectionGuard.ts"]
},
"src/models": {
"path": "src/models",
"purpose": "Data models",
"fileCount": 1,
"lastAccessed": 1775016362438,
"keyFiles": ["index.ts"]
}
},
"hotPaths": [],
"userDirectives": []
}

View File

@@ -0,0 +1,8 @@
{
"session_id": "53c002c3-36a6-47c3-a52d-a8f756c264eb",
"ended_at": "2026-04-01T04:06:04.924Z",
"reason": "prompt_input_exit",
"agents_spawned": 0,
"agents_completed": 0,
"modes_used": []
}

View File

@@ -0,0 +1,245 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import { Card, Button, Input, Toggle } from "@/shared/components";
interface Settings {
cliproxyapi_fallback_enabled?: boolean;
cliproxyapi_url?: string;
cliproxyapi_fallback_codes?: string;
[key: string]: unknown;
}
interface VersionManagerEntry {
tool: string;
status: string;
installedVersion: string | null;
healthStatus: string;
port: number;
}
function isValidUrl(value: string): boolean {
try {
const url = new URL(value);
return url.protocol === "http:" || url.protocol === "https:";
} catch {
return false;
}
}
export default function CliproxyapiSettingsTab() {
const [settings, setSettings] = useState<Settings>({});
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [message, setMessage] = useState<{ type: string; text: string } | null>(null);
const [toolState, setToolState] = useState<VersionManagerEntry | null>(null);
const [toolStateError, setToolStateError] = useState<string | null>(null);
useEffect(() => {
fetch("/api/settings")
.then((r) => {
if (!r.ok) throw new Error(`Settings API returned ${r.status}`);
return r.json();
})
.then((data) => {
setSettings(data);
setLoading(false);
})
.catch((err) => {
console.error("Failed to load settings:", err);
setLoading(false);
});
fetch("/api/version-manager/status")
.then((r) => {
if (!r.ok) throw new Error(`Version manager API returned ${r.status}`);
return r.json();
})
.then((data) => {
const entry = Array.isArray(data)
? data.find((t: VersionManagerEntry) => t.tool === "cliproxyapi")
: null;
setToolState(entry ?? null);
setToolStateError(null);
})
.catch((err) => {
console.error("Failed to load version manager status:", err);
setToolStateError("Unable to reach version manager service");
setToolState(null);
});
}, []);
const updateSetting = useCallback(async (key: string, value: boolean | string) => {
if (key === "cliproxyapi_url" && typeof value === "string" && value.trim() !== "") {
if (!isValidUrl(value)) {
setMessage({ type: "error", text: "Invalid URL format. Use http:// or https://" });
return;
}
}
setSaving(true);
setMessage(null);
try {
const res = await fetch("/api/settings", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ [key]: value }),
});
if (!res.ok) {
throw new Error(`Server returned ${res.status}`);
}
await res.json();
setSettings((prev) => ({ ...prev, [key]: value }));
setMessage({ type: "success", text: "Setting saved" });
} catch {
setMessage({ type: "error", text: "Failed to save setting" });
} finally {
setSaving(false);
}
}, []);
const cpaEnabled = settings.cliproxyapi_fallback_enabled === true;
const cpaUrl = settings.cliproxyapi_url || "http://127.0.0.1:8317";
const cpaCodes = settings.cliproxyapi_fallback_codes || "502,401,403,429,503";
const statusColor =
toolState?.status === "running"
? "text-green-600 dark:text-green-400"
: toolState?.status === "error"
? "text-red-600 dark:text-red-400"
: "text-text-muted";
const statusIcon =
toolState?.status === "running"
? "check_circle"
: toolState?.status === "error"
? "error"
: "help";
return (
<div className="space-y-4">
{message && (
<div
className={`flex items-center gap-2 px-3 py-2 rounded-lg text-xs ${
message.type === "success"
? "bg-green-500/10 text-green-600 dark:text-green-400"
: "bg-red-500/10 text-red-600 dark:text-red-400"
}`}
>
<span className="material-symbols-outlined text-[14px]">
{message.type === "success" ? "check_circle" : "error"}
</span>
{message.text}
</div>
)}
<Card padding="md">
<div className="flex items-center gap-3 mb-4">
<div className="size-8 rounded-lg flex items-center justify-center bg-indigo-500/10">
<span className="material-symbols-outlined text-indigo-500 text-xl">swap_horiz</span>
</div>
<div>
<h3 className="font-medium text-sm">CLIProxyAPI Fallback</h3>
<p className="text-xs text-text-muted">
When enabled, failed requests are retried through CLIProxyAPI (localhost:8317)
</p>
</div>
</div>
<div className="space-y-4">
<div className="flex items-center justify-between">
<label className="text-sm text-text-main">Enable CLIProxyAPI Fallback</label>
<Toggle
checked={cpaEnabled}
onChange={(checked) => updateSetting("cliproxyapi_fallback_enabled", checked)}
/>
</div>
{cpaEnabled && (
<>
<div>
<label className="text-xs text-text-muted mb-1.5 block">CLIProxyAPI URL</label>
<Input
value={cpaUrl}
onChange={(e) => updateSetting("cliproxyapi_url", e.target.value)}
placeholder="http://127.0.0.1:8317"
className="w-full"
/>
</div>
<div>
<label className="text-xs text-text-muted mb-1.5 block">
Fallback Status Codes (comma-separated)
</label>
<Input
value={cpaCodes}
onChange={(e) => updateSetting("cliproxyapi_fallback_codes", e.target.value)}
placeholder="502,401,403,429,503"
className="w-full"
/>
</div>
</>
)}
</div>
</Card>
<Card padding="md">
<h3 className="font-medium text-sm mb-4">CLIProxyAPI Status</h3>
{loading ? (
<div className="flex items-center gap-2 text-text-muted text-sm">
<span className="material-symbols-outlined animate-spin text-base">
progress_activity
</span>
Loading...
</div>
) : toolStateError ? (
<p className="text-sm text-text-muted">{toolStateError}</p>
) : toolState ? (
<div className="grid grid-cols-2 gap-3">
<div className="p-3 rounded-lg bg-bg-secondary">
<p className="text-xs text-text-muted mb-1">Status</p>
<div className="flex items-center gap-1.5">
<span className={`material-symbols-outlined text-sm ${statusColor}`}>
{statusIcon}
</span>
<p className={`text-sm font-medium capitalize ${statusColor}`}>
{toolState.status?.replace("_", " ") || "Unknown"}
</p>
</div>
</div>
<div className="p-3 rounded-lg bg-bg-secondary">
<p className="text-xs text-text-muted mb-1">Version</p>
<p className="text-sm font-medium">
{toolState.installedVersion ? `v${toolState.installedVersion}` : "Not installed"}
</p>
</div>
<div className="p-3 rounded-lg bg-bg-secondary">
<p className="text-xs text-text-muted mb-1">Health</p>
<p
className={`text-sm font-medium ${
toolState.healthStatus === "healthy"
? "text-green-600 dark:text-green-400"
: toolState.healthStatus === "unhealthy"
? "text-red-600 dark:text-red-400"
: "text-text-muted"
}`}
>
{toolState.healthStatus === "healthy"
? "Healthy"
: toolState.healthStatus === "unhealthy"
? "Unhealthy"
: "Unknown"}
</p>
</div>
<div className="p-3 rounded-lg bg-bg-secondary">
<p className="text-xs text-text-muted mb-1">Port</p>
<p className="text-sm font-mono">{toolState.port || 8317}</p>
</div>
</div>
) : (
<p className="text-sm text-text-muted">CLIProxyAPI not detected</p>
)}
</Card>
</div>
);
}

View File

@@ -21,6 +21,7 @@ import CacheStatsCard from "./components/CacheStatsCard";
import CacheSettingsTab from "./components/CacheSettingsTab";
import MemorySkillsTab from "./components/MemorySkillsTab";
import ResilienceTab from "./components/ResilienceTab";
import CliproxyapiSettingsTab from "./components/CliproxyapiSettingsTab";
const tabs = [
{ id: "general", labelKey: "general", icon: "settings" },
@@ -113,7 +114,12 @@ export default function SettingsPage() {
{activeTab === "resilience" && <ResilienceTab />}
{activeTab === "advanced" && <ProxyTab />}
{activeTab === "advanced" && (
<div className="flex flex-col gap-6">
<ProxyTab />
<CliproxyapiSettingsTab />
</div>
)}
</div>
{/* App Info */}

View File

@@ -8,6 +8,7 @@ import { updateSettingsSchema } from "@/shared/validation/settingsSchemas";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
import { setCliCompatProviders } from "../../../../open-sse/config/cliFingerprints";
import { getConsistentMachineId } from "@/shared/utils/machineId";
import { validateProxyUrl, upsertUpstreamProxyConfig } from "@/lib/db/upstreamProxy";
export async function GET() {
try {
@@ -102,6 +103,29 @@ export async function PATCH(request) {
const settings = await updateSettings(body);
// Sync CLIProxyAPI settings to upstream_proxy_config table
const cpaUrl = rawBody.cliproxyapi_url as string | undefined;
const cpaFallback = rawBody.cliproxyapi_fallback_enabled as boolean | undefined;
if (cpaUrl && typeof cpaUrl === "string") {
const urlValidation = validateProxyUrl(cpaUrl);
if (urlValidation.valid === false) {
return NextResponse.json(
{ error: `Invalid CLIProxyAPI URL: ${urlValidation.error}` },
{ status: 400 }
);
}
}
if (cpaFallback !== undefined || cpaUrl !== undefined) {
const enabled =
cpaFallback ?? (settings as Record<string, unknown>).cliproxyapi_fallback_enabled;
const mode = enabled ? "fallback" : "native";
await upsertUpstreamProxyConfig({
providerId: "cliproxyapi",
mode,
enabled: !!enabled,
});
}
// Clear health check log cache if that setting was updated
if ("hideHealthCheckLogs" in body) {
clearHealthCheckLogCache();

View File

@@ -0,0 +1,22 @@
-- 014_create_memories.sql
-- Memories table for persistent context storage.
-- Stores structured conversation memories with support for different memory types.
CREATE TABLE IF NOT EXISTS memories (
id TEXT PRIMARY KEY,
api_key_id TEXT NOT NULL,
session_id TEXT,
type TEXT NOT NULL CHECK(type IN ('factual', 'episodic', 'procedural', 'semantic')),
key TEXT,
content TEXT NOT NULL,
metadata TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
expires_at TEXT
);
-- Indexes for performance optimization
CREATE INDEX IF NOT EXISTS idx_memories_api_key ON memories(api_key_id);
CREATE INDEX IF NOT EXISTS idx_memories_session ON memories(session_id);
CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type);
CREATE INDEX IF NOT EXISTS idx_memories_expires ON memories(expires_at);

View File

@@ -0,0 +1,4 @@
-- 014_create_memories_down.sql
-- DOWN Migration: Remove memories table (Rollback)
DROP TABLE IF EXISTS memories;

View File

@@ -0,0 +1,37 @@
-- 015_create_skills.sql
-- Skills table for tool/function capability injection.
-- Stores skill definitions with schemas and execution tracking.
CREATE TABLE IF NOT EXISTS skills (
id TEXT PRIMARY KEY,
api_key_id TEXT NOT NULL,
name TEXT NOT NULL,
version TEXT NOT NULL DEFAULT '1.0.0',
description TEXT,
schema TEXT NOT NULL,
handler TEXT NOT NULL,
enabled INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS skill_executions (
id TEXT PRIMARY KEY,
skill_id TEXT NOT NULL,
api_key_id TEXT NOT NULL,
session_id TEXT,
input TEXT NOT NULL,
output TEXT,
status TEXT NOT NULL CHECK(status IN ('pending', 'running', 'success', 'error', 'timeout')),
error_message TEXT,
duration_ms INTEGER,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (skill_id) REFERENCES skills(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_skills_api_key ON skills(api_key_id);
CREATE INDEX IF NOT EXISTS idx_skills_name ON skills(name);
CREATE INDEX IF NOT EXISTS idx_skill_executions_skill ON skill_executions(skill_id);
CREATE INDEX IF NOT EXISTS idx_skill_executions_api_key ON skill_executions(api_key_id);
CREATE INDEX IF NOT EXISTS idx_skill_executions_status ON skill_executions(status);
CREATE INDEX IF NOT EXISTS idx_skill_executions_created ON skill_executions(created_at);

View File

@@ -0,0 +1,5 @@
-- 015_create_skills_down.sql
-- Rollback skills and skill_executions tables
DROP TABLE IF EXISTS skill_executions;
DROP TABLE IF EXISTS skills;

View File

@@ -0,0 +1,56 @@
-- Migration 016: Version Manager & Upstream Proxy tables
--
-- Adds two tables for CLIProxyAPI integration:
-- version_manager — binary lifecycle management for CLI tools (CLIProxyAPI, etc.)
-- upstream_proxy_config — per-provider routing mode (native vs CLIProxyAPI vs fallback)
-- --------------------------------------------------------------------------
-- Table: version_manager
-- Tracks installed versions, process state, and update settings for
-- externally managed CLI tools (initially CLIProxyAPI).
-- --------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS version_manager (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tool TEXT NOT NULL UNIQUE, -- 'cliproxyapi' | 'omniroute'
current_version TEXT, -- latest known release version
installed_version TEXT, -- currently installed version
pinned_version TEXT, -- user-pinned version (null = auto)
binary_path TEXT, -- absolute path to binary
status TEXT NOT NULL DEFAULT 'not_installed', -- not_installed | installed | running | stopped | error
pid INTEGER, -- process ID when running
port INTEGER DEFAULT 8317, -- managed tool's port
api_key TEXT, -- API key for CLIProxyAPI (from its config)
management_key TEXT, -- CLIProxyAPI management secret key
auto_update INTEGER NOT NULL DEFAULT 1, -- 1 = auto-update enabled
auto_start INTEGER NOT NULL DEFAULT 0, -- 1 = start with OmniRoute
last_health_check TEXT, -- ISO timestamp
last_update_check TEXT, -- ISO timestamp
health_status TEXT DEFAULT 'unknown', -- unknown | healthy | unhealthy | timeout
config_overrides TEXT, -- JSON blob for CLIProxyAPI config overrides
error_message TEXT, -- last error message
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- --------------------------------------------------------------------------
-- Table: upstream_proxy_config
-- Per-provider routing configuration for CLIProxyAPI passthrough mode.
-- Determines whether each provider uses OmniRoute's native executor,
-- delegates to CLIProxyAPI, or uses fallback (native first, then CLIProxyAPI).
-- --------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS upstream_proxy_config (
id INTEGER PRIMARY KEY AUTOINCREMENT,
provider_id TEXT NOT NULL UNIQUE, -- e.g., 'antigravity', 'claude', 'codex'
mode TEXT NOT NULL DEFAULT 'native', -- 'native' | 'cliproxyapi' | 'fallback'
cliproxyapi_model_mapping TEXT, -- JSON: { "ag/gemini-3-pro": "gemini-3-pro-high", ... }
native_priority INTEGER NOT NULL DEFAULT 1, -- order in fallback chain (1 = first)
cliproxyapi_priority INTEGER NOT NULL DEFAULT 2, -- order in fallback chain
enabled INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_upc_provider ON upstream_proxy_config(provider_id);
CREATE INDEX IF NOT EXISTS idx_upc_mode ON upstream_proxy_config(mode);
CREATE INDEX IF NOT EXISTS idx_vm_tool ON version_manager(tool);
CREATE INDEX IF NOT EXISTS idx_vm_status ON version_manager(status);

231
src/lib/db/upstreamProxy.ts Normal file
View File

@@ -0,0 +1,231 @@
/** Upstream proxy config persistence for upstream_proxy_config table. */
import { getDbInstance } from "./core";
interface UpstreamProxyConfig {
id: number;
providerId: string;
mode: string;
cliproxyapiModelMapping: Record<string, unknown> | null;
nativePriority: number;
cliproxyapiPriority: number;
enabled: boolean;
createdAt: string;
updatedAt: string;
}
interface UpstreamProxyRow {
id: unknown;
provider_id: unknown;
mode: unknown;
cliproxyapi_model_mapping: unknown;
native_priority: unknown;
cliproxyapi_priority: unknown;
enabled: unknown;
created_at: unknown;
updated_at: unknown;
}
function toRecord(value: unknown): Record<string, unknown> {
return value && typeof value === "object" ? (value as Record<string, unknown>) : {};
}
const BLOCKED_HOSTNAMES = ["metadata.google.internal", "169.254.169.254", "metadata.aws.internal"];
function isPrivateHost(hostname: string): boolean {
// CLIProxyAPI runs on localhost:8317 — allow loopback explicitly
if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1") return false;
if (BLOCKED_HOSTNAMES.includes(hostname)) return true;
if (
/^10\./.test(hostname) ||
/^172\.(1[6-9]|2\d|3[01])\./.test(hostname) ||
/^192\.168\./.test(hostname)
)
return true;
if (
/^0\./.test(hostname) ||
/^127\./.test(hostname) ||
/^224\./.test(hostname) ||
/^169\.254\./.test(hostname)
)
return true;
return false;
}
export function validateProxyUrl(
url: string
): { valid: true; url: string } | { valid: false; error: string } {
try {
const parsed = new URL(url);
if (!["http:", "https:"].includes(parsed.protocol)) {
return {
valid: false,
error: `Unsupported protocol "${parsed.protocol}" — use http or https`,
};
}
if (isPrivateHost(parsed.hostname)) {
return {
valid: false,
error: `Proxy URL cannot point to private/internal address "${parsed.hostname}"`,
};
}
return { valid: true, url };
} catch {
return { valid: false, error: `Invalid URL: "${url}"` };
}
}
function rowToConfig(record: Record<string, unknown>): UpstreamProxyConfig {
return {
id: record.id as number,
providerId: record.provider_id as string,
mode: record.mode as string,
cliproxyapiModelMapping:
record.cliproxyapi_model_mapping && typeof record.cliproxyapi_model_mapping === "string"
? JSON.parse(record.cliproxyapi_model_mapping)
: null,
nativePriority: record.native_priority as number,
cliproxyapiPriority: record.cliproxyapi_priority as number,
enabled: record.enabled === 1 || record.enabled === true,
createdAt: record.created_at as string,
updatedAt: record.updated_at as string,
};
}
export async function getUpstreamProxyConfigs() {
const db = getDbInstance();
const rows = db
.prepare("SELECT * FROM upstream_proxy_config ORDER BY provider_id")
.all() as UpstreamProxyRow[];
return rows.map((row) => rowToConfig(toRecord(row)));
}
export async function getUpstreamProxyConfig(providerId: string) {
const db = getDbInstance();
const row = db
.prepare("SELECT * FROM upstream_proxy_config WHERE provider_id = ?")
.get(providerId) as UpstreamProxyRow | undefined;
if (!row) return null;
return rowToConfig(toRecord(row));
}
export async function upsertUpstreamProxyConfig(data: {
providerId: string;
mode?: string;
cliproxyapiModelMapping?: Record<string, unknown> | null;
nativePriority?: number;
cliproxyapiPriority?: number;
enabled?: boolean;
}) {
const db = getDbInstance();
const mode = data.mode ?? "native";
const cliproxyapiModelMapping =
data.cliproxyapiModelMapping !== undefined
? JSON.stringify(data.cliproxyapiModelMapping)
: null;
const nativePriority = data.nativePriority ?? 1;
const cliproxyapiPriority = data.cliproxyapiPriority ?? 2;
const enabled = data.enabled !== false ? 1 : 0;
db.prepare(
`INSERT INTO upstream_proxy_config
(provider_id, mode, cliproxyapi_model_mapping, native_priority, cliproxyapi_priority, enabled, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
ON CONFLICT(provider_id) DO UPDATE SET
mode = excluded.mode,
cliproxyapi_model_mapping = excluded.cliproxyapi_model_mapping,
native_priority = excluded.native_priority,
cliproxyapi_priority = excluded.cliproxyapi_priority,
enabled = excluded.enabled,
updated_at = datetime('now')`
).run(
data.providerId,
mode,
cliproxyapiModelMapping,
nativePriority,
cliproxyapiPriority,
enabled
);
return getUpstreamProxyConfig(data.providerId);
}
export async function updateUpstreamProxyConfig(
providerId: string,
updates: Record<string, unknown>
) {
const db = getDbInstance();
const current = await getUpstreamProxyConfig(providerId);
if (!current) {
throw new Error(`Provider ${providerId} not found`);
}
const sets: string[] = ["updated_at = datetime('now')"];
const params: unknown[] = [];
if (updates.mode !== undefined) {
sets.push("mode = ?");
params.push(updates.mode);
}
if (updates.cliproxyapiModelMapping !== undefined) {
sets.push("cliproxyapi_model_mapping = ?");
params.push(
updates.cliproxyapiModelMapping === null
? null
: JSON.stringify(updates.cliproxyapiModelMapping)
);
}
if (updates.nativePriority !== undefined) {
sets.push("native_priority = ?");
params.push(updates.nativePriority);
}
if (updates.cliproxyapiPriority !== undefined) {
sets.push("cliproxyapi_priority = ?");
params.push(updates.cliproxyapiPriority);
}
if (updates.enabled !== undefined) {
sets.push("enabled = ?");
params.push(updates.enabled === true ? 1 : 0);
}
params.push(providerId);
db.prepare(`UPDATE upstream_proxy_config SET ${sets.join(", ")} WHERE provider_id = ?`).run(
...params
);
return getUpstreamProxyConfig(providerId);
}
export async function deleteUpstreamProxyConfig(providerId: string) {
const db = getDbInstance();
const result = db
.prepare("DELETE FROM upstream_proxy_config WHERE provider_id = ?")
.run(providerId);
return result.changes > 0;
}
export async function getProvidersByMode(mode: string) {
const db = getDbInstance();
const rows = db
.prepare(
"SELECT * FROM upstream_proxy_config WHERE mode = ? AND enabled = 1 ORDER BY provider_id"
)
.all(mode) as UpstreamProxyRow[];
return rows.map((row) => rowToConfig(toRecord(row)));
}
export async function getFallbackChainForProvider(providerId: string) {
const config = await getUpstreamProxyConfig(providerId);
if (!config) return [];
const chain: { executor: "native" | "cliproxyapi"; priority: number }[] = [];
if (config.enabled) {
chain.push({ executor: "native", priority: config.nativePriority });
if (config.mode === "cliproxyapi" || config.mode === "fallback") {
chain.push({ executor: "cliproxyapi", priority: config.cliproxyapiPriority });
}
}
chain.sort((a, b) => a.priority - b.priority);
return chain;
}

View File

@@ -0,0 +1,318 @@
/** Version manager tool state persistence. */
import { getDbInstance } from "./core";
interface VersionManagerRow {
id?: unknown;
tool?: unknown;
current_version?: unknown;
installed_version?: unknown;
pinned_version?: unknown;
binary_path?: unknown;
status?: unknown;
pid?: unknown;
port?: unknown;
api_key?: unknown;
management_key?: unknown;
auto_update?: unknown;
auto_start?: unknown;
last_health_check?: unknown;
last_update_check?: unknown;
health_status?: unknown;
config_overrides?: unknown;
error_message?: unknown;
created_at?: unknown;
updated_at?: unknown;
}
function toRecord(value: unknown): Record<string, unknown> {
return value && typeof value === "object" ? (value as Record<string, unknown>) : {};
}
function parseConfigOverrides(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== "string" || value.trim() === "") return null;
try {
const parsed = JSON.parse(value);
return typeof parsed === "object" && parsed !== null ? parsed : null;
} catch {
return null;
}
}
function stringifyConfigOverrides(value: Record<string, unknown> | null): string | null {
if (value === null) return null;
try {
return JSON.stringify(value);
} catch {
return null;
}
}
interface VersionManagerTool {
id: number;
tool: string;
currentVersion: string | null;
installedVersion: string | null;
pinnedVersion: string | null;
binaryPath: string | null;
status: string;
pid: number | null;
port: number;
apiKey: string | null;
managementKey: string | null;
autoUpdate: boolean;
autoStart: boolean;
lastHealthCheck: string | null;
lastUpdateCheck: string | null;
healthStatus: string;
configOverrides: Record<string, unknown> | null;
errorMessage: string | null;
createdAt: string;
updatedAt: string;
}
function rowToVersionManager(row: VersionManagerRow): VersionManagerTool {
const record = toRecord(row);
return {
id: typeof record.id === "number" ? record.id : 0,
tool: typeof record.tool === "string" ? record.tool : "",
currentVersion:
record.current_version === null
? null
: typeof record.current_version === "string"
? record.current_version
: null,
installedVersion:
record.installed_version === null
? null
: typeof record.installed_version === "string"
? record.installed_version
: null,
pinnedVersion:
record.pinned_version === null
? null
: typeof record.pinned_version === "string"
? record.pinned_version
: null,
binaryPath:
record.binary_path === null
? null
: typeof record.binary_path === "string"
? record.binary_path
: null,
status: typeof record.status === "string" ? record.status : "not_installed",
pid: record.pid === null ? null : typeof record.pid === "number" ? record.pid : null,
port: typeof record.port === "number" ? record.port : 8317,
apiKey:
record.api_key === null ? null : typeof record.api_key === "string" ? record.api_key : null,
managementKey:
record.management_key === null
? null
: typeof record.management_key === "string"
? record.management_key
: null,
autoUpdate:
record.auto_update === 1 || record.auto_update === true || record.auto_update === "1",
autoStart: record.auto_start === 1 || record.auto_start === true || record.auto_start === "1",
lastHealthCheck:
record.last_health_check === null
? null
: typeof record.last_health_check === "string"
? record.last_health_check
: null,
lastUpdateCheck:
record.last_update_check === null
? null
: typeof record.last_update_check === "string"
? record.last_update_check
: null,
healthStatus: typeof record.health_status === "string" ? record.health_status : "unknown",
configOverrides: parseConfigOverrides(record.config_overrides),
errorMessage:
record.error_message === null
? null
: typeof record.error_message === "string"
? record.error_message
: null,
createdAt: typeof record.created_at === "string" ? record.created_at : "",
updatedAt: typeof record.updated_at === "string" ? record.updated_at : "",
};
}
export async function getVersionManagerStatus(): Promise<VersionManagerTool[]> {
const db = getDbInstance();
const rows = db.prepare("SELECT * FROM version_manager").all() as VersionManagerRow[];
return rows.map(rowToVersionManager);
}
export async function getVersionManagerTool(tool: string): Promise<VersionManagerTool | null> {
const db = getDbInstance();
const row = db.prepare("SELECT * FROM version_manager WHERE tool = ?").get(tool) as
| VersionManagerRow
| undefined;
if (!row) return null;
return rowToVersionManager(row);
}
export async function upsertVersionManagerTool(data: {
tool: string;
currentVersion?: string | null;
installedVersion?: string | null;
pinnedVersion?: string | null;
binaryPath?: string | null;
status?: string;
pid?: number | null;
port?: number;
apiKey?: string | null;
managementKey?: string | null;
autoUpdate?: boolean;
autoStart?: boolean;
healthStatus?: string;
configOverrides?: Record<string, unknown> | null;
errorMessage?: string | null;
}): Promise<VersionManagerTool> {
const db = getDbInstance();
db.prepare(
`
INSERT INTO version_manager (
tool, current_version, installed_version, pinned_version, binary_path,
status, pid, port, api_key, management_key, auto_update, auto_start,
health_status, config_overrides, error_message, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
ON CONFLICT(tool) DO UPDATE SET
current_version = excluded.current_version,
installed_version = excluded.installed_version,
pinned_version = excluded.pinned_version,
binary_path = excluded.binary_path,
status = excluded.status,
pid = excluded.pid,
port = excluded.port,
api_key = excluded.api_key,
management_key = excluded.management_key,
auto_update = excluded.auto_update,
auto_start = excluded.auto_start,
health_status = excluded.health_status,
config_overrides = excluded.config_overrides,
error_message = excluded.error_message,
updated_at = datetime('now')
`
).run(
data.tool,
data.currentVersion ?? null,
data.installedVersion ?? null,
data.pinnedVersion ?? null,
data.binaryPath ?? null,
data.status ?? "not_installed",
data.pid ?? null,
data.port ?? 8317,
data.apiKey ?? null,
data.managementKey ?? null,
data.autoUpdate !== undefined ? (data.autoUpdate ? 1 : 0) : 1,
data.autoStart !== undefined ? (data.autoStart ? 1 : 0) : 0,
data.healthStatus ?? "unknown",
stringifyConfigOverrides(data.configOverrides ?? null),
data.errorMessage ?? null
);
const result = await getVersionManagerTool(data.tool);
if (!result) throw new Error("Failed to retrieve inserted version manager tool");
return result;
}
export async function updateVersionManagerTool(
tool: string,
updates: Record<string, unknown>
): Promise<VersionManagerTool | null> {
const db = getDbInstance();
const existing = await getVersionManagerTool(tool);
if (!existing) return null;
const ALLOWED_COLUMNS = new Set([
"currentVersion",
"installedVersion",
"pinnedVersion",
"binaryPath",
"status",
"pid",
"port",
"apiKey",
"managementKey",
"autoUpdate",
"autoStart",
"healthStatus",
"configOverrides",
"errorMessage",
]);
const sets: string[] = ["updated_at = datetime('now')"];
const params: Record<string, unknown> = { tool };
for (const [key, value] of Object.entries(updates)) {
if (!ALLOWED_COLUMNS.has(key)) continue;
const dbKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
if (key === "configOverrides") {
sets.push("config_overrides = @configOverrides");
params.configOverrides = stringifyConfigOverrides(value as Record<string, unknown> | null);
} else if (key === "autoUpdate" || key === "autoStart") {
sets.push(`${dbKey} = @${key}`);
params[key] = value === true ? 1 : 0;
} else if (value === null) {
sets.push(`${dbKey} = null`);
} else {
sets.push(`${dbKey} = @${key}`);
params[key] = value;
}
}
db.prepare(`UPDATE version_manager SET ${sets.join(", ")} WHERE tool = @tool`).run(params);
return getVersionManagerTool(tool);
}
export async function deleteVersionManagerTool(tool: string): Promise<boolean> {
const db = getDbInstance();
const result = db.prepare("DELETE FROM version_manager WHERE tool = ?").run(tool);
return result.changes > 0;
}
export async function updateToolHealth(tool: string, healthStatus: string): Promise<boolean> {
const db = getDbInstance();
const result = db
.prepare(
"UPDATE version_manager SET health_status = ?, last_health_check = datetime('now') WHERE tool = ?"
)
.run(healthStatus, tool);
return result.changes > 0;
}
export async function updateToolVersion(
tool: string,
field: "current_version" | "installed_version",
version: string
): Promise<boolean> {
const db = getDbInstance();
const result = db
.prepare(`UPDATE version_manager SET ${field} = ?, updated_at = datetime('now') WHERE tool = ?`)
.run(version, tool);
return result.changes > 0;
}
export async function setToolStatus(
tool: string,
status: string,
pid?: number,
errorMessage?: string
): Promise<boolean> {
const db = getDbInstance();
const result = db
.prepare(
pid !== undefined
? "UPDATE version_manager SET status = ?, pid = ?, error_message = ?, updated_at = datetime('now') WHERE tool = ?"
: "UPDATE version_manager SET status = ?, error_message = ?, updated_at = datetime('now') WHERE tool = ?"
)
.run(
...(pid !== undefined
? [status, pid, errorMessage ?? null, tool]
: [status, errorMessage ?? null, tool])
);
return result.changes > 0;
}

View File

@@ -210,6 +210,28 @@ export {
export type { QuotaSnapshotRow, ProviderUtilizationPoint } from "@/shared/types/utilization";
export {
getVersionManagerStatus,
getVersionManagerTool,
upsertVersionManagerTool,
updateVersionManagerTool,
deleteVersionManagerTool,
updateToolHealth,
updateToolVersion,
setToolStatus,
} from "./db/versionManager";
export {
getUpstreamProxyConfigs,
getUpstreamProxyConfig,
upsertUpstreamProxyConfig,
updateUpstreamProxyConfig,
deleteUpstreamProxyConfig,
getProvidersByMode,
getFallbackChainForProvider,
validateProxyUrl,
} from "./db/upstreamProxy";
export {
getProviderLimitsCache,
getAllProviderLimitsCache,

View File

@@ -617,12 +617,35 @@ export function isAnthropicCompatibleProvider(providerId) {
return typeof providerId === "string" && providerId.startsWith(ANTHROPIC_COMPATIBLE_PREFIX);
}
export const UPSTREAM_PROXY_PROVIDERS = {
cliproxyapi: {
id: "cliproxyapi",
alias: "cpa",
name: "CLIProxyAPI",
icon: "proxy",
color: "#6366F1",
textIcon: "CPA",
website: "https://github.com/router-for-me/CLIProxyAPI",
defaultPort: 8317,
healthEndpoint: "/v1/models",
managementPrefix: "/v0/management",
configDir: "~/.cli-proxy-api",
binaryName: "cli-proxy-api",
githubRepo: "router-for-me/CLIProxyAPI",
},
};
export function isClaudeCodeCompatibleProvider(providerId) {
return typeof providerId === "string" && providerId.startsWith(CLAUDE_CODE_COMPATIBLE_PREFIX);
}
// All providers (combined)
export const AI_PROVIDERS = { ...FREE_PROVIDERS, ...OAUTH_PROVIDERS, ...APIKEY_PROVIDERS };
export const AI_PROVIDERS = {
...FREE_PROVIDERS,
...OAUTH_PROVIDERS,
...APIKEY_PROVIDERS,
...UPSTREAM_PROXY_PROVIDERS,
};
// Auth methods
export const AUTH_METHODS = {

View File

@@ -1320,3 +1320,11 @@ export const updateAutoDisableAccountsSchema = z
threshold: z.number().int().min(1).max(10).optional(),
})
.strict();
export const versionManagerToolSchema = z.object({
tool: z.string().trim().min(1),
});
export const versionManagerInstallSchema = versionManagerToolSchema.extend({
version: z.string().trim().optional(),
});

View File

@@ -0,0 +1,340 @@
import { describe, it, beforeEach, afterEach, after } from "node:test";
import assert from "node:assert/strict";
import path from "node:path";
import fs from "node:fs";
import os from "node:os";
import Database from "better-sqlite3";
const fileTmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "omni-dbupc-test-"));
const SCHEMA = `
CREATE TABLE IF NOT EXISTS upstream_proxy_config (
id INTEGER PRIMARY KEY AUTOINCREMENT,
provider_id TEXT NOT NULL UNIQUE,
mode TEXT NOT NULL DEFAULT 'native',
cliproxyapi_model_mapping TEXT,
native_priority INTEGER NOT NULL DEFAULT 1,
cliproxyapi_priority INTEGER NOT NULL DEFAULT 2,
enabled INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
`;
let testDb;
let testDbPath;
beforeEach(() => {
testDbPath = path.join(fileTmpDir, `test-${Date.now()}.db`);
testDb = new Database(testDbPath);
testDb.exec(SCHEMA);
});
afterEach(() => {
testDb.close();
try {
fs.unlinkSync(testDbPath);
} catch {}
});
after(() => {
if (fs.existsSync(fileTmpDir)) fs.rmSync(fileTmpDir, { recursive: true, force: true });
});
function upsert(db, data) {
db.prepare(
`
INSERT OR REPLACE INTO upstream_proxy_config
(provider_id, mode, cliproxyapi_model_mapping, native_priority, cliproxyapi_priority, enabled, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
`
).run(
data.providerId,
data.mode ?? "native",
data.cliproxyapiModelMapping !== undefined
? JSON.stringify(data.cliproxyapiModelMapping)
: null,
data.nativePriority ?? 1,
data.cliproxyapiPriority ?? 2,
data.enabled !== false ? 1 : 0
);
return getConfig(db, data.providerId);
}
function getConfig(db, providerId) {
const row = db
.prepare("SELECT * FROM upstream_proxy_config WHERE provider_id = ?")
.get(providerId);
if (!row) return null;
return {
id: row.id,
providerId: row.provider_id,
mode: row.mode,
cliproxyapiModelMapping:
row.cliproxyapi_model_mapping && typeof row.cliproxyapi_model_mapping === "string"
? JSON.parse(row.cliproxyapi_model_mapping)
: null,
nativePriority: row.native_priority,
cliproxyapiPriority: row.cliproxyapi_priority,
enabled: row.enabled === 1,
createdAt: row.created_at,
updatedAt: row.updated_at,
};
}
function getAllConfigs(db) {
return db
.prepare("SELECT * FROM upstream_proxy_config ORDER BY provider_id")
.all()
.map((r) => ({
id: r.id,
providerId: r.provider_id,
mode: r.mode,
cliproxyapiModelMapping:
r.cliproxyapi_model_mapping && typeof r.cliproxyapi_model_mapping === "string"
? JSON.parse(r.cliproxyapi_model_mapping)
: null,
nativePriority: r.native_priority,
cliproxyapiPriority: r.cliproxyapi_priority,
enabled: r.enabled === 1,
createdAt: r.created_at,
updatedAt: r.updated_at,
}));
}
function getProvidersByMode(db, mode) {
return db
.prepare(
"SELECT * FROM upstream_proxy_config WHERE mode = ? AND enabled = 1 ORDER BY provider_id"
)
.all(mode)
.map((r) => ({
id: r.id,
providerId: r.provider_id,
mode: r.mode,
cliproxyapiModelMapping: r.cliproxyapi_model_mapping
? JSON.parse(r.cliproxyapi_model_mapping)
: null,
nativePriority: r.native_priority,
cliproxyapiPriority: r.cliproxyapi_priority,
enabled: r.enabled === 1,
}));
}
function getFallbackChain(db, providerId) {
const config = getConfig(db, providerId);
if (!config) return [];
const chain = [];
if (config.enabled) {
chain.push({ executor: "native", priority: config.nativePriority });
if (config.mode === "cliproxyapi" || config.mode === "fallback") {
chain.push({ executor: "cliproxyapi", priority: config.cliproxyapiPriority });
}
}
chain.sort((a, b) => a.priority - b.priority);
return chain;
}
describe("db/upstreamProxy (logic)", () => {
describe("upsertUpstreamProxyConfig", () => {
it("should insert and read back", () => {
const config = upsert(testDb, { providerId: "claude", mode: "native" });
assert.equal(config.providerId, "claude");
assert.equal(config.mode, "native");
assert.equal(config.enabled, true);
assert.equal(config.nativePriority, 1);
assert.equal(config.cliproxyapiPriority, 2);
assert.equal(config.cliproxyapiModelMapping, null);
assert.ok(config.id > 0);
});
it("should replace on conflict", () => {
upsert(testDb, { providerId: "t", mode: "native" });
const replaced = upsert(testDb, { providerId: "t", mode: "fallback" });
assert.equal(replaced.mode, "fallback");
});
it("should store model mapping as JSON", () => {
const mapping = { "ag/gemini-3-pro": "gemini-3-pro-high" };
const config = upsert(testDb, { providerId: "mapped", cliproxyapiModelMapping: mapping });
assert.deepEqual(config.cliproxyapiModelMapping, mapping);
});
it("should store null model mapping", () => {
const config = upsert(testDb, { providerId: "nomap", cliproxyapiModelMapping: null });
assert.equal(config.cliproxyapiModelMapping, null);
});
it("should handle enabled=false", () => {
assert.equal(upsert(testDb, { providerId: "dis", enabled: false }).enabled, false);
});
it("should set custom priorities", () => {
const config = upsert(testDb, {
providerId: "pri",
nativePriority: 5,
cliproxyapiPriority: 10,
});
assert.equal(config.nativePriority, 5);
assert.equal(config.cliproxyapiPriority, 10);
});
});
describe("getUpstreamProxyConfig", () => {
it("should return null for non-existent provider", () => {
assert.equal(getConfig(testDb, "ghost"), null);
});
it("should return config for provider", () => {
upsert(testDb, { providerId: "claude", mode: "fallback" });
assert.equal(getConfig(testDb, "claude").mode, "fallback");
});
});
describe("getUpstreamProxyConfigs", () => {
it("should return all ordered by provider_id", () => {
upsert(testDb, { providerId: "zebra" });
upsert(testDb, { providerId: "alpha" });
const configs = getAllConfigs(testDb);
assert.equal(configs.length, 2);
assert.equal(configs[0].providerId, "alpha");
assert.equal(configs[1].providerId, "zebra");
});
it("should return empty array", () => {
assert.equal(getAllConfigs(testDb).length, 0);
});
});
describe("updateUpstreamProxyConfig", () => {
it("should update individual fields", () => {
upsert(testDb, { providerId: "u", mode: "native" });
testDb
.prepare(
"UPDATE upstream_proxy_config SET mode = ?, updated_at = datetime('now') WHERE provider_id = ?"
)
.run("cliproxyapi", "u");
assert.equal(getConfig(testDb, "u").mode, "cliproxyapi");
});
it("should update model mapping", () => {
upsert(testDb, { providerId: "u2" });
testDb
.prepare(
"UPDATE upstream_proxy_config SET cliproxyapi_model_mapping = ?, updated_at = datetime('now') WHERE provider_id = ?"
)
.run(JSON.stringify({ k: "v" }), "u2");
assert.deepEqual(getConfig(testDb, "u2").cliproxyapiModelMapping, { k: "v" });
});
it("should update multiple fields", () => {
upsert(testDb, { providerId: "m" });
testDb
.prepare(
"UPDATE upstream_proxy_config SET mode = ?, native_priority = ?, enabled = ?, updated_at = datetime('now') WHERE provider_id = ?"
)
.run("fallback", 3, 0, "m");
const config = getConfig(testDb, "m");
assert.equal(config.mode, "fallback");
assert.equal(config.nativePriority, 3);
assert.equal(config.enabled, false);
});
it("should set model mapping to null", () => {
upsert(testDb, { providerId: "n", cliproxyapiModelMapping: { a: 1 } });
testDb
.prepare(
"UPDATE upstream_proxy_config SET cliproxyapi_model_mapping = NULL WHERE provider_id = ?"
)
.run("n");
assert.equal(getConfig(testDb, "n").cliproxyapiModelMapping, null);
});
});
describe("deleteUpstreamProxyConfig", () => {
it("should delete existing config", () => {
upsert(testDb, { providerId: "del" });
assert.equal(
testDb.prepare("DELETE FROM upstream_proxy_config WHERE provider_id = ?").run("del")
.changes,
1
);
assert.equal(getConfig(testDb, "del"), null);
});
it("should return 0 for non-existent", () => {
assert.equal(
testDb.prepare("DELETE FROM upstream_proxy_config WHERE provider_id = ?").run("ghost")
.changes,
0
);
});
});
describe("getProvidersByMode", () => {
it("should filter by mode and enabled", () => {
upsert(testDb, { providerId: "p1", mode: "fallback", enabled: true });
upsert(testDb, { providerId: "p2", mode: "fallback", enabled: true });
upsert(testDb, { providerId: "p3", mode: "native", enabled: true });
upsert(testDb, { providerId: "p4", mode: "fallback", enabled: false });
const results = getProvidersByMode(testDb, "fallback");
assert.equal(results.length, 2);
assert.ok(results.every((r) => r.enabled));
});
it("should return empty for no matches", () => {
assert.equal(getProvidersByMode(testDb, "cliproxyapi").length, 0);
});
});
describe("getFallbackChainForProvider", () => {
it("should return empty for non-existent provider", () => {
assert.deepEqual(getFallbackChain(testDb, "ghost"), []);
});
it("should return native-only for native mode", () => {
upsert(testDb, { providerId: "native-only", mode: "native" });
const chain = getFallbackChain(testDb, "native-only");
assert.equal(chain.length, 1);
assert.equal(chain[0].executor, "native");
});
it("should return native+cliproxyapi for fallback mode", () => {
upsert(testDb, {
providerId: "fb",
mode: "fallback",
nativePriority: 1,
cliproxyapiPriority: 2,
});
const chain = getFallbackChain(testDb, "fb");
assert.equal(chain.length, 2);
assert.equal(chain[0].executor, "native");
assert.equal(chain[1].executor, "cliproxyapi");
});
it("should return both for cliproxyapi mode", () => {
upsert(testDb, { providerId: "cpa", mode: "cliproxyapi" });
const chain = getFallbackChain(testDb, "cpa");
assert.equal(chain.length, 2);
});
it("should sort by priority", () => {
upsert(testDb, {
providerId: "rev",
mode: "fallback",
nativePriority: 10,
cliproxyapiPriority: 1,
});
const chain = getFallbackChain(testDb, "rev");
assert.equal(chain[0].executor, "cliproxyapi");
assert.equal(chain[0].priority, 1);
assert.equal(chain[1].executor, "native");
assert.equal(chain[1].priority, 10);
});
it("should return empty chain when disabled", () => {
upsert(testDb, { providerId: "dis-fb", mode: "fallback", enabled: false });
assert.deepEqual(getFallbackChain(testDb, "dis-fb"), []);
});
});
});

View File

@@ -0,0 +1,368 @@
import { describe, it, beforeEach, afterEach, after } from "node:test";
import assert from "node:assert/strict";
import path from "node:path";
import fs from "node:fs";
import os from "node:os";
import Database from "better-sqlite3";
const fileTmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "omni-dbvm-test-"));
const SCHEMA = `
CREATE TABLE IF NOT EXISTS version_manager (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tool TEXT NOT NULL UNIQUE,
current_version TEXT,
installed_version TEXT,
pinned_version TEXT,
binary_path TEXT,
status TEXT NOT NULL DEFAULT 'not_installed',
pid INTEGER,
port INTEGER DEFAULT 8317,
api_key TEXT,
management_key TEXT,
auto_update INTEGER NOT NULL DEFAULT 1,
auto_start INTEGER NOT NULL DEFAULT 0,
last_health_check TEXT,
last_update_check TEXT,
health_status TEXT DEFAULT 'unknown',
config_overrides TEXT,
error_message TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
`;
let testDb;
let testDbPath;
beforeEach(() => {
testDbPath = path.join(fileTmpDir, `test-${Date.now()}.db`);
testDb = new Database(testDbPath);
testDb.exec(SCHEMA);
});
afterEach(() => {
testDb.close();
try {
fs.unlinkSync(testDbPath);
} catch {}
});
after(() => {
if (fs.existsSync(fileTmpDir)) fs.rmSync(fileTmpDir, { recursive: true, force: true });
});
function upsertTool(db, data) {
db.prepare(
`
INSERT INTO version_manager (
tool, current_version, installed_version, pinned_version, binary_path,
status, pid, port, api_key, management_key, auto_update, auto_start,
health_status, config_overrides, error_message, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
ON CONFLICT(tool) DO UPDATE SET
current_version = excluded.current_version,
installed_version = excluded.installed_version,
pinned_version = excluded.pinned_version,
binary_path = excluded.binary_path,
status = excluded.status,
pid = excluded.pid,
port = excluded.port,
api_key = excluded.api_key,
management_key = excluded.management_key,
auto_update = excluded.auto_update,
auto_start = excluded.auto_start,
health_status = excluded.health_status,
config_overrides = excluded.config_overrides,
error_message = excluded.error_message,
updated_at = datetime('now')
`
).run(
data.tool,
data.currentVersion ?? null,
data.installedVersion ?? null,
data.pinnedVersion ?? null,
data.binaryPath ?? null,
data.status ?? "not_installed",
data.pid ?? null,
data.port ?? 8317,
data.apiKey ?? null,
data.managementKey ?? null,
data.autoUpdate !== undefined ? (data.autoUpdate ? 1 : 0) : 1,
data.autoStart !== undefined ? (data.autoStart ? 1 : 0) : 0,
data.healthStatus ?? "unknown",
data.configOverrides !== undefined
? data.configOverrides
? JSON.stringify(data.configOverrides)
: null
: null,
data.errorMessage ?? null
);
return db.prepare("SELECT * FROM version_manager WHERE tool = ?").get(data.tool);
}
function parseConfigOverrides(value) {
if (!value || typeof value !== "string" || value.trim() === "") return null;
try {
const parsed = JSON.parse(value);
return typeof parsed === "object" && parsed !== null ? parsed : null;
} catch {
return null;
}
}
function toTool(row) {
if (!row) return null;
return {
id: row.id,
tool: row.tool,
currentVersion: row.current_version,
installedVersion: row.installed_version,
pinnedVersion: row.pinned_version,
binaryPath: row.binary_path,
status: row.status,
pid: row.pid,
port: row.port,
apiKey: row.api_key,
managementKey: row.management_key,
autoUpdate: row.auto_update === 1,
autoStart: row.auto_start === 1,
lastHealthCheck: row.last_health_check,
lastUpdateCheck: row.last_update_check,
healthStatus: row.health_status,
configOverrides: parseConfigOverrides(row.config_overrides),
errorMessage: row.error_message,
createdAt: row.created_at,
updatedAt: row.updated_at,
};
}
describe("db/versionManager (logic)", () => {
describe("upsertVersionManagerTool", () => {
it("should insert a new tool and read it back", () => {
const tool = toTool(
upsertTool(testDb, {
tool: "cliproxyapi",
installedVersion: "6.9.7",
binaryPath: "/tmp/bin/cliproxyapi",
status: "installed",
})
);
assert.equal(tool.tool, "cliproxyapi");
assert.equal(tool.installedVersion, "6.9.7");
assert.equal(tool.binaryPath, "/tmp/bin/cliproxyapi");
assert.equal(tool.status, "installed");
assert.equal(tool.port, 8317);
assert.equal(tool.autoUpdate, true);
assert.equal(tool.autoStart, false);
assert.equal(tool.healthStatus, "unknown");
assert.ok(tool.id > 0);
});
it("should update on conflict (upsert)", () => {
upsertTool(testDb, { tool: "test-tool", status: "installed" });
const tool = toTool(
upsertTool(testDb, { tool: "test-tool", installedVersion: "7.0.0", status: "running" })
);
assert.equal(tool.installedVersion, "7.0.0");
assert.equal(tool.status, "running");
});
it("should store boolean fields as 0/1", () => {
const tool = toTool(
upsertTool(testDb, { tool: "bool-test", autoUpdate: false, autoStart: true })
);
assert.equal(tool.autoUpdate, false);
assert.equal(tool.autoStart, true);
});
it("should store config overrides as JSON", () => {
const tool = toTool(upsertTool(testDb, { tool: "cfg", configOverrides: { port: 9999 } }));
assert.deepEqual(tool.configOverrides, { port: 9999 });
});
it("should handle null config overrides", () => {
const tool = toTool(upsertTool(testDb, { tool: "null-cfg" }));
assert.equal(tool.configOverrides, null);
});
it("should handle pid and error message", () => {
const tool = toTool(
upsertTool(testDb, { tool: "pid-test", pid: 12345, errorMessage: "err" })
);
assert.equal(tool.pid, 12345);
assert.equal(tool.errorMessage, "err");
});
});
describe("getVersionManagerTool", () => {
it("should return null for non-existent tool", () => {
assert.equal(
toTool(testDb.prepare("SELECT * FROM version_manager WHERE tool = ?").get("ghost")),
null
);
});
it("should return tool by name", () => {
upsertTool(testDb, { tool: "findme", installedVersion: "1.0.0" });
const tool = toTool(
testDb.prepare("SELECT * FROM version_manager WHERE tool = ?").get("findme")
);
assert.equal(tool.installedVersion, "1.0.0");
});
});
describe("getVersionManagerStatus", () => {
it("should return all tools", () => {
upsertTool(testDb, { tool: "a" });
upsertTool(testDb, { tool: "b" });
assert.equal(testDb.prepare("SELECT * FROM version_manager").all().length, 2);
});
it("should return empty when no tools", () => {
assert.equal(testDb.prepare("SELECT * FROM version_manager").all().length, 0);
});
});
describe("updateVersionManagerTool (partial)", () => {
it("should update pinnedVersion and autoUpdate", () => {
upsertTool(testDb, { tool: "upd", status: "installed" });
testDb
.prepare(
"UPDATE version_manager SET pinned_version = ?, auto_update = ?, updated_at = datetime('now') WHERE tool = ?"
)
.run("6.8.0", 0, "upd");
const tool = toTool(
testDb.prepare("SELECT * FROM version_manager WHERE tool = ?").get("upd")
);
assert.equal(tool.pinnedVersion, "6.8.0");
assert.equal(tool.autoUpdate, false);
assert.equal(tool.status, "installed");
});
it("should set fields to null", () => {
upsertTool(testDb, { tool: "nulls", installedVersion: "1.0.0", pid: 42 });
testDb
.prepare("UPDATE version_manager SET installed_version = NULL, pid = NULL WHERE tool = ?")
.run("nulls");
const tool = toTool(
testDb.prepare("SELECT * FROM version_manager WHERE tool = ?").get("nulls")
);
assert.equal(tool.installedVersion, null);
assert.equal(tool.pid, null);
});
});
describe("deleteVersionManagerTool", () => {
it("should delete existing tool", () => {
upsertTool(testDb, { tool: "del" });
assert.equal(
testDb.prepare("DELETE FROM version_manager WHERE tool = ?").run("del").changes,
1
);
assert.equal(
testDb.prepare("SELECT * FROM version_manager WHERE tool = ?").get("del"),
undefined
);
});
it("should return 0 changes for non-existent", () => {
assert.equal(
testDb.prepare("DELETE FROM version_manager WHERE tool = ?").run("ghost").changes,
0
);
});
});
describe("updateToolHealth", () => {
it("should update health_status and last_health_check", () => {
upsertTool(testDb, { tool: "h" });
const r = testDb
.prepare(
"UPDATE version_manager SET health_status = ?, last_health_check = datetime('now') WHERE tool = ?"
)
.run("healthy", "h");
assert.equal(r.changes, 1);
const tool = toTool(testDb.prepare("SELECT * FROM version_manager WHERE tool = ?").get("h"));
assert.equal(tool.healthStatus, "healthy");
assert.ok(tool.lastHealthCheck);
});
it("should return 0 changes for non-existent", () => {
assert.equal(
testDb
.prepare("UPDATE version_manager SET health_status = ? WHERE tool = ?")
.run("healthy", "ghost").changes,
0
);
});
});
describe("updateToolVersion", () => {
it("should update current_version", () => {
upsertTool(testDb, { tool: "v1" });
testDb
.prepare("UPDATE version_manager SET current_version = ? WHERE tool = ?")
.run("7.0.0", "v1");
assert.equal(
testDb.prepare("SELECT * FROM version_manager WHERE tool = ?").get("v1").current_version,
"7.0.0"
);
});
it("should update installed_version", () => {
upsertTool(testDb, { tool: "v2" });
testDb
.prepare("UPDATE version_manager SET installed_version = ? WHERE tool = ?")
.run("7.0.0", "v2");
assert.equal(
testDb.prepare("SELECT * FROM version_manager WHERE tool = ?").get("v2").installed_version,
"7.0.0"
);
});
});
describe("setToolStatus", () => {
it("should update status with pid", () => {
upsertTool(testDb, { tool: "s1" });
testDb
.prepare("UPDATE version_manager SET status = ?, pid = ?, error_message = ? WHERE tool = ?")
.run("running", 9999, "ok", "s1");
const row = testDb.prepare("SELECT * FROM version_manager WHERE tool = ?").get("s1");
assert.equal(row.status, "running");
assert.equal(row.pid, 9999);
assert.equal(row.error_message, "ok");
});
it("should update status without pid", () => {
upsertTool(testDb, { tool: "s2" });
testDb
.prepare("UPDATE version_manager SET status = ?, error_message = ? WHERE tool = ?")
.run("error", "crashed", "s2");
const row = testDb.prepare("SELECT * FROM version_manager WHERE tool = ?").get("s2");
assert.equal(row.status, "error");
});
it("should return 0 for non-existent", () => {
assert.equal(
testDb
.prepare("UPDATE version_manager SET status = ? WHERE tool = ?")
.run("running", "ghost").changes,
0
);
});
});
describe("parseConfigOverrides", () => {
it("should parse valid JSON", () => {
assert.deepEqual(parseConfigOverrides('{"key":"val"}'), { key: "val" });
});
it("should return null for invalid JSON", () => {
assert.equal(parseConfigOverrides("not-json"), null);
assert.equal(parseConfigOverrides(""), null);
assert.equal(parseConfigOverrides(null), null);
assert.equal(parseConfigOverrides("123"), null);
});
});
});