Spaces:
Runtime error
Runtime error
| /** | |
| * scripts/test/_vpsClient.mjs | |
| * | |
| * Reusable Phase-3 VPS HTTP client for OmniRoute combo live-smoke tests. | |
| * NOT a test file — intentionally placed in scripts/test/ so check:test-discovery | |
| * does not scan it. | |
| * | |
| * Combo create/delete mechanism: SSH-sqlite fallback. | |
| * /api/combos requires management auth (returns 401 unauthenticated). | |
| * We insert/delete rows directly via: | |
| * execFileSync("ssh", ["root@192.168.0.15", "sqlite3", "/root/.omniroute/storage.sqlite", SQL]) | |
| * Values are static test-scoped data — no untrusted interpolation. | |
| * | |
| * combos table schema (PRAGMA table_info): | |
| * id TEXT PK, name TEXT NOT NULL, data TEXT NOT NULL (JSON blob), | |
| * created_at TEXT NOT NULL, updated_at TEXT NOT NULL, | |
| * system_message TEXT, tool_filter_regex TEXT, | |
| * context_cache_protection INTEGER DEFAULT 0, sort_order INTEGER NOT NULL DEFAULT 0 | |
| * | |
| * The `data` column stores the full combo as JSON (name, models[], strategy, config, | |
| * id, createdAt, updatedAt, version, sortOrder). | |
| */ | |
| import { execFileSync } from "node:child_process"; | |
| import { randomUUID } from "node:crypto"; | |
| // --------------------------------------------------------------------------- | |
| // Config | |
| // --------------------------------------------------------------------------- | |
| const BASE_URL = process.env.COMBO_LIVE_BASE_URL ?? "http://192.168.0.15:20128"; | |
| const API_KEY = process.env.COMBO_LIVE_API_KEY ?? null; | |
| const VPS_SSH_HOST = "root@192.168.0.15"; | |
| const VPS_DB_PATH = "/root/.omniroute/storage.sqlite"; | |
| // --------------------------------------------------------------------------- | |
| // Nonce counter — increments per call so semantic cache cannot serve stale | |
| // --------------------------------------------------------------------------- | |
| let _nonceCounter = 0; | |
| export function nonce() { | |
| return ++_nonceCounter; | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Shared fetch helpers | |
| // --------------------------------------------------------------------------- | |
| function authHeaders() { | |
| const h = { "Content-Type": "application/json" }; | |
| if (API_KEY) h["Authorization"] = `Bearer ${API_KEY}`; | |
| return h; | |
| } | |
| async function fetchJson(path, options = {}) { | |
| const url = `${BASE_URL}${path}`; | |
| const res = await fetch(url, { ...options, headers: { ...authHeaders(), ...(options.headers ?? {}) } }); | |
| let body; | |
| try { | |
| body = await res.json(); | |
| } catch { | |
| body = null; | |
| } | |
| return { status: res.status, ok: res.ok, body }; | |
| } | |
| // --------------------------------------------------------------------------- | |
| // health() — GET /api/monitoring/health | |
| // --------------------------------------------------------------------------- | |
| export async function health() { | |
| const { status, body } = await fetchJson("/api/monitoring/health"); | |
| return { | |
| status, | |
| version: body?.version ?? null, | |
| uptime: body?.uptime ?? null, | |
| raw: body, | |
| }; | |
| } | |
| // --------------------------------------------------------------------------- | |
| // chat() — POST /v1/chat/completions (non-streaming) | |
| // --------------------------------------------------------------------------- | |
| export async function chat(model, { maxTokens = 16, content } = {}) { | |
| const n = nonce(); | |
| const userContent = content ?? `ping ${n}`; | |
| const payload = { | |
| model, | |
| stream: false, | |
| max_tokens: maxTokens, | |
| temperature: 0, | |
| messages: [{ role: "user", content: userContent }], | |
| }; | |
| const { status, body } = await fetchJson("/v1/chat/completions", { | |
| method: "POST", | |
| body: JSON.stringify(payload), | |
| }); | |
| const text = body?.choices?.[0]?.message?.content ?? null; | |
| return { | |
| status, | |
| model: body?.model ?? model, | |
| text, | |
| raw: body, | |
| }; | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Combo create/delete via SSH sqlite | |
| // --------------------------------------------------------------------------- | |
| function sshSqlite(sql) { | |
| // Pipe SQL via stdin to avoid shell quoting issues with complex SQL statements. | |
| // ssh + sqlite3 reads from stdin when no trailing SQL arg is given. | |
| return execFileSync("ssh", [VPS_SSH_HOST, "sqlite3", VPS_DB_PATH], { | |
| input: sql, | |
| encoding: "utf8", | |
| timeout: 15_000, | |
| }).trim(); | |
| } | |
| /** | |
| * createCombo(def) — inserts a combo row via SSH sqlite. | |
| * | |
| * def shape: | |
| * { name, strategy, models, config } | |
| * | |
| * models: array of "providerId/model" strings OR | |
| * array of { providerId, model, connectionId?, weight? } objects. | |
| * | |
| * config: optional object (judgeModel, fusionTuning, etc.) | |
| * | |
| * Returns the combo id string. | |
| */ | |
| export function createCombo(def) { | |
| const id = randomUUID(); | |
| const now = new Date().toISOString(); | |
| const strategy = def.strategy ?? "priority"; | |
| const config = def.config ?? {}; | |
| const name = def.name; | |
| // Normalise models to the shape the DB expects | |
| const rawModels = def.models ?? []; | |
| const models = rawModels.map((m, idx) => { | |
| if (typeof m === "string") { | |
| // "providerId/model" shorthand | |
| const slashIdx = m.indexOf("/"); | |
| const providerId = slashIdx >= 0 ? m.slice(0, slashIdx) : m; | |
| const model = slashIdx >= 0 ? m.slice(slashIdx + 1) : m; | |
| const slugName = name.toLowerCase().replace(/[^a-z0-9]/g, "-"); | |
| const slugModel = model.toLowerCase().replace(/[^a-z0-9]/g, "-"); | |
| return { | |
| id: `${slugName}-model-${idx + 1}-${providerId}-${slugModel}-${randomUUID()}`, | |
| kind: "model", | |
| model: `${providerId}/${model}`, | |
| providerId, | |
| weight: 1, | |
| }; | |
| } | |
| // Already an object | |
| const providerId = m.providerId; | |
| const model = m.model; | |
| const slugName = name.toLowerCase().replace(/[^a-z0-9]/g, "-"); | |
| const slugModel = (model ?? "").toLowerCase().replace(/[^a-z0-9]/g, "-"); | |
| return { | |
| id: m.id ?? `${slugName}-model-${idx + 1}-${providerId}-${slugModel}-${randomUUID()}`, | |
| kind: "model", | |
| model: model.includes("/") ? model : `${providerId}/${model}`, | |
| providerId, | |
| ...(m.connectionId ? { connectionId: m.connectionId } : {}), | |
| weight: m.weight ?? 1, | |
| }; | |
| }); | |
| const dataObj = { | |
| name, | |
| models, | |
| strategy, | |
| config, | |
| id, | |
| createdAt: now, | |
| updatedAt: now, | |
| version: 2, | |
| sortOrder: 9999, | |
| }; | |
| const dataJson = JSON.stringify(dataObj).replace(/'/g, "''"); | |
| const nameSafe = name.replace(/'/g, "''"); | |
| const sql = `INSERT INTO combos (id, name, data, created_at, updated_at, sort_order) VALUES ('${id}', '${nameSafe}', '${dataJson}', '${now}', '${now}', 9999);`; | |
| sshSqlite(sql); | |
| return id; | |
| } | |
| /** | |
| * deleteCombo(nameOrId) — deletes a combo by name or id via SSH sqlite. | |
| * Only deletes __live_test__* prefixed combos as a safety guard. | |
| */ | |
| export function deleteCombo(nameOrId) { | |
| if (!nameOrId.startsWith("__live_test__")) { | |
| throw new Error(`deleteCombo safety guard: refusing to delete '${nameOrId}' — only __live_test__* combos allowed.`); | |
| } | |
| const safe = nameOrId.replace(/'/g, "''"); | |
| // Try delete by name first, then by id | |
| sshSqlite(`DELETE FROM combos WHERE name='${safe}' OR id='${safe}';`); | |
| } | |
| // --------------------------------------------------------------------------- | |
| // listHealthyProviders(candidates) — probe each "provider/model" candidate | |
| // --------------------------------------------------------------------------- | |
| /** | |
| * For each "provider/model" string, fire a minimal chat() and keep those | |
| * returning HTTP 200 with non-empty text. | |
| * | |
| * @param {string[]} candidates - array of "provider/model" strings | |
| * @returns {Promise<string[]>} healthy candidates | |
| */ | |
| export async function listHealthyProviders(candidates) { | |
| const results = await Promise.allSettled( | |
| candidates.map(async (c) => { | |
| const r = await chat(c, { maxTokens: 16 }); | |
| return r.status === 200 && r.text ? c : null; | |
| }) | |
| ); | |
| return results | |
| .map((r) => (r.status === "fulfilled" ? r.value : null)) | |
| .filter(Boolean); | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Preflight self-check (run directly: node scripts/test/_vpsClient.mjs) | |
| // --------------------------------------------------------------------------- | |
| const isMain = process.argv[1]?.endsWith("_vpsClient.mjs") || | |
| import.meta.url === `file://${process.argv[1]}`; | |
| if (isMain) { | |
| (async () => { | |
| console.log("=== OmniRoute VPS Phase-3 Preflight ==="); | |
| console.log(`Base URL: ${BASE_URL}`); | |
| console.log(`API key: ${API_KEY ? "set (Bearer)" : "not set (REQUIRE_API_KEY=false)"}`); | |
| console.log(); | |
| // 1. Health | |
| console.log("--- health() ---"); | |
| try { | |
| const h = await health(); | |
| console.log(` status: ${h.status}`); | |
| console.log(` version: ${h.version}`); | |
| } catch (e) { | |
| console.error(` ERROR: ${e.message}`); | |
| } | |
| // 2. Combo mechanism probe | |
| console.log(); | |
| console.log("--- combo create/delete mechanism ---"); | |
| console.log(" /api/combos GET (unauthenticated):", (() => { | |
| try { | |
| const r = execFileSync("curl", ["-s", "-o", "/dev/null", "-w", "%{http_code}", `${BASE_URL}/api/combos`], { encoding: "utf8", timeout: 5000 }); | |
| return r.trim(); | |
| } catch { return "error"; } | |
| })()); | |
| console.log(" Mechanism: SSH sqlite fallback (management API requires auth)"); | |
| console.log(" SSH host:", VPS_SSH_HOST); | |
| console.log(" DB path:", VPS_DB_PATH); | |
| // 3. Create + delete a probe combo via SSH sqlite | |
| console.log(); | |
| console.log("--- createCombo / deleteCombo (SSH sqlite) ---"); | |
| const probeName = "__live_test__probe"; | |
| let probeId; | |
| try { | |
| probeId = createCombo({ | |
| name: probeName, | |
| strategy: "priority", | |
| models: ["groq/llama-3.1-8b-instant"], | |
| config: {}, | |
| }); | |
| console.log(` created id: ${probeId}`); | |
| deleteCombo(probeName); | |
| console.log(` deleted OK`); | |
| } catch (e) { | |
| console.error(` ERROR: ${e.message}`); | |
| // Attempt cleanup on error | |
| if (probeId) { | |
| try { deleteCombo(probeName); } catch {} | |
| } | |
| } | |
| // 4. ollama-cloud chat probe | |
| console.log(); | |
| console.log("--- chat probe: ollama-cloud/glm-5.2 ---"); | |
| try { | |
| const r = await chat("ollama-cloud/glm-5.2", { maxTokens: 16 }); | |
| console.log(` status: ${r.status}`); | |
| console.log(` model: ${r.model}`); | |
| console.log(` text: ${r.text ? r.text.slice(0, 80) : "(empty)"}`); | |
| if (r.status !== 200 || !r.text) { | |
| console.log(" NOTE: ollama-cloud/glm-5.2 did not return a valid response."); | |
| console.log(" raw error:", JSON.stringify(r.raw?.error ?? r.raw).slice(0, 200)); | |
| } | |
| } catch (e) { | |
| console.error(` ERROR: ${e.message}`); | |
| } | |
| // 5. Quick healthy provider scan over a small candidate set | |
| console.log(); | |
| console.log("--- listHealthyProviders (subset scan) ---"); | |
| const candidates = [ | |
| "groq/llama-3.1-8b-instant", | |
| "gemini/gemini-2.0-flash", | |
| "deepseek/deepseek-chat", | |
| "cerebras/llama3.1-8b", | |
| "ollama-cloud/glm-5.2", | |
| ]; | |
| try { | |
| const healthy = await listHealthyProviders(candidates); | |
| console.log(` tested: ${candidates.join(", ")}`); | |
| console.log(` healthy (${healthy.length}/${candidates.length}): ${healthy.join(", ") || "(none)"}`); | |
| } catch (e) { | |
| console.error(` ERROR: ${e.message}`); | |
| } | |
| console.log(); | |
| console.log("=== Preflight complete ==="); | |
| })(); | |
| } | |