# v0.4.52 (2026-05-17)
## Features - Add Vercel AI Gateway provider support (#1183) - rtk: Kiro format tool result compression — handle conversationState.history & currentMessage, preserve error results, ~13.6% savings (#1194) ## Fixes - openclaw: normalize agent.model object form `{primary, fallbacks}` before .startsWith → fix TypeError & 'not configured' status (#1216) - Usage Details pagination: stay inside mobile viewport <640px (#1218) - Fix test model error - Fix MIMO provider in Codex - Disable log file creation when using MITM AG
This commit is contained in:
parent
9326589452
commit
9abbb8ad9b
12 changed files with 188 additions and 79 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "9router",
|
"name": "9router",
|
||||||
"version": "0.4.50",
|
"version": "0.4.52",
|
||||||
"description": "9Router CLI - Start and manage 9Router server",
|
"description": "9Router CLI - Start and manage 9Router server",
|
||||||
"bin": {
|
"bin": {
|
||||||
"9router": "./cli.js"
|
"9router": "./cli.js"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
const http = require("http");
|
const http = require("http");
|
||||||
const https = require("https");
|
const https = require("https");
|
||||||
const crypto = require("crypto");
|
const crypto = require("crypto");
|
||||||
|
const fs = require("node:fs");
|
||||||
|
const path = require("node:path");
|
||||||
|
const os = require("node:os");
|
||||||
const { machineIdSync } = require("node-machine-id");
|
const { machineIdSync } = require("node-machine-id");
|
||||||
|
|
||||||
// Default configuration
|
// Default configuration
|
||||||
|
|
@ -12,18 +15,34 @@ const DEFAULT_CONFIG = {
|
||||||
|
|
||||||
const CLI_TOKEN_HEADER = "x-9r-cli-token";
|
const CLI_TOKEN_HEADER = "x-9r-cli-token";
|
||||||
const CLI_TOKEN_SALT = "9r-cli-auth";
|
const CLI_TOKEN_SALT = "9r-cli-auth";
|
||||||
|
const APP_NAME = "9router";
|
||||||
|
|
||||||
|
function getDataDir() {
|
||||||
|
if (process.env.DATA_DIR) return process.env.DATA_DIR;
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
return path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), APP_NAME);
|
||||||
|
}
|
||||||
|
return path.join(os.homedir(), `.${APP_NAME}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const MACHINE_ID_FILE = path.join(getDataDir(), "machine-id");
|
||||||
|
|
||||||
let config = { ...DEFAULT_CONFIG };
|
let config = { ...DEFAULT_CONFIG };
|
||||||
let cachedCliToken = null;
|
let cachedCliToken = null;
|
||||||
|
|
||||||
|
// Read raw machineId from shared file (written by server) → guarantees token match
|
||||||
|
function loadRawMachineId() {
|
||||||
|
try {
|
||||||
|
const raw = fs.readFileSync(MACHINE_ID_FILE, "utf8").trim();
|
||||||
|
if (raw) return raw;
|
||||||
|
} catch {}
|
||||||
|
try { return machineIdSync(); } catch { return ""; }
|
||||||
|
}
|
||||||
|
|
||||||
function getCliToken() {
|
function getCliToken() {
|
||||||
if (cachedCliToken !== null) return cachedCliToken;
|
if (cachedCliToken !== null) return cachedCliToken;
|
||||||
try {
|
const raw = loadRawMachineId();
|
||||||
const mid = machineIdSync();
|
cachedCliToken = raw ? crypto.createHash("sha256").update(raw + CLI_TOKEN_SALT).digest("hex").substring(0, 16) : "";
|
||||||
cachedCliToken = crypto.createHash("sha256").update(mid + CLI_TOKEN_SALT).digest("hex").substring(0, 16);
|
|
||||||
} catch {
|
|
||||||
cachedCliToken = "";
|
|
||||||
}
|
|
||||||
return cachedCliToken;
|
return cachedCliToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,24 @@ export function openaiResponsesToOpenAIRequest(model, body, stream, credentials)
|
||||||
// Group items by conversation turn
|
// Group items by conversation turn
|
||||||
let currentAssistantMsg = null;
|
let currentAssistantMsg = null;
|
||||||
let pendingToolResults = [];
|
let pendingToolResults = [];
|
||||||
|
let pendingReasoning = "";
|
||||||
|
|
||||||
const inputItems = normalizeResponsesInput(body.input);
|
const inputItems = normalizeResponsesInput(body.input);
|
||||||
if (!inputItems) return body;
|
if (!inputItems) return body;
|
||||||
|
|
||||||
|
// Extract reasoning text from summary[].text or encrypted_content fallback
|
||||||
|
const extractReasoningText = (item) => {
|
||||||
|
if (Array.isArray(item.summary)) {
|
||||||
|
const txt = item.summary.map(s => s?.text || "").filter(Boolean).join("\n");
|
||||||
|
if (txt) return txt;
|
||||||
|
}
|
||||||
|
if (Array.isArray(item.content)) {
|
||||||
|
const txt = item.content.map(c => c?.text || "").filter(Boolean).join("\n");
|
||||||
|
if (txt) return txt;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
for (const item of inputItems) {
|
for (const item of inputItems) {
|
||||||
// Determine item type - Droid CLI sends role-based items without 'type' field
|
// Determine item type - Droid CLI sends role-based items without 'type' field
|
||||||
// Fallback: if no type but has role property, treat as message
|
// Fallback: if no type but has role property, treat as message
|
||||||
|
|
@ -64,7 +78,13 @@ export function openaiResponsesToOpenAIRequest(model, body, stream, credentials)
|
||||||
return c;
|
return c;
|
||||||
})
|
})
|
||||||
: item.content;
|
: item.content;
|
||||||
result.messages.push({ role: item.role, content });
|
const msg = { role: item.role, content };
|
||||||
|
// Attach buffered reasoning to assistant turn (required by xiaomi-mimo thinking mode)
|
||||||
|
if (item.role === "assistant" && pendingReasoning) {
|
||||||
|
msg.reasoning_content = pendingReasoning;
|
||||||
|
}
|
||||||
|
pendingReasoning = "";
|
||||||
|
result.messages.push(msg);
|
||||||
}
|
}
|
||||||
else if (itemType === "function_call") {
|
else if (itemType === "function_call") {
|
||||||
// Start or append to assistant message with tool_calls
|
// Start or append to assistant message with tool_calls
|
||||||
|
|
@ -74,6 +94,10 @@ export function openaiResponsesToOpenAIRequest(model, body, stream, credentials)
|
||||||
content: null,
|
content: null,
|
||||||
tool_calls: []
|
tool_calls: []
|
||||||
};
|
};
|
||||||
|
if (pendingReasoning) {
|
||||||
|
currentAssistantMsg.reasoning_content = pendingReasoning;
|
||||||
|
pendingReasoning = "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Skip items with empty/missing name — Codex/OpenAI reject nameless tool calls (#444)
|
// Skip items with empty/missing name — Codex/OpenAI reject nameless tool calls (#444)
|
||||||
if (!item.name || typeof item.name !== "string" || item.name.trim() === "") continue;
|
if (!item.name || typeof item.name !== "string" || item.name.trim() === "") continue;
|
||||||
|
|
@ -107,7 +131,9 @@ export function openaiResponsesToOpenAIRequest(model, body, stream, credentials)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else if (itemType === "reasoning") {
|
else if (itemType === "reasoning") {
|
||||||
// Skip reasoning items - they are for display only
|
// Buffer reasoning text; attached to next assistant message/function_call
|
||||||
|
const txt = extractReasoningText(item);
|
||||||
|
if (txt) pendingReasoning = pendingReasoning ? `${pendingReasoning}\n${txt}` : txt;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "9router-app",
|
"name": "9router-app",
|
||||||
"version": "0.4.50",
|
"version": "0.4.52",
|
||||||
"description": "9Router web dashboard",
|
"description": "9Router web dashboard",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,30 @@ const MIGRATED_MARKER = path.join(DB_DIR, ".migrated-from-json");
|
||||||
// Track per-adapter so reusing same adapter skips re-run, but new adapter (after reset) re-runs.
|
// Track per-adapter so reusing same adapter skips re-run, but new adapter (after reset) re-runs.
|
||||||
const _migratedAdapters = new WeakSet();
|
const _migratedAdapters = new WeakSet();
|
||||||
|
|
||||||
|
// Thrown when row-count assertion fails. Outer transaction rolls back,
|
||||||
|
// legacy db.json kept intact, marker not written → next boot retries.
|
||||||
|
export class MigrationAborted extends Error {
|
||||||
|
constructor(message, droppedRows) {
|
||||||
|
super(message);
|
||||||
|
this.name = "MigrationAborted";
|
||||||
|
this.droppedRows = droppedRows;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert rows one-by-one, collect failures, then assert COUNT(*) matches input length.
|
||||||
|
function importWithAssertion(adapter, tableName, rows, insertFn, rowMeta) {
|
||||||
|
const dropped = [];
|
||||||
|
for (const row of rows) {
|
||||||
|
try { insertFn(row); }
|
||||||
|
catch (err) { dropped.push({ ...rowMeta(row), reason: err.message }); }
|
||||||
|
}
|
||||||
|
const inserted = adapter.get(`SELECT COUNT(*) as c FROM ${tableName}`)?.c ?? 0;
|
||||||
|
if (inserted !== rows.length) {
|
||||||
|
console.warn(`[DB][migrate] ${tableName} row-count mismatch: expected ${rows.length}, got ${inserted}. Dropped:`, dropped);
|
||||||
|
throw new MigrationAborted(`${tableName} row-count mismatch: expected ${rows.length}, got ${inserted}`, dropped);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function readJsonSafe(file) {
|
function readJsonSafe(file) {
|
||||||
if (!fs.existsSync(file)) return null;
|
if (!fs.existsSync(file)) return null;
|
||||||
try { return JSON.parse(fs.readFileSync(file, "utf-8")); } catch { return null; }
|
try { return JSON.parse(fs.readFileSync(file, "utf-8")); } catch { return null; }
|
||||||
|
|
@ -91,39 +115,45 @@ function importLegacyMain(adapter, data) {
|
||||||
if (data.settings) {
|
if (data.settings) {
|
||||||
adapter.run(`INSERT INTO settings(id, data) VALUES(1, ?) ON CONFLICT(id) DO UPDATE SET data = excluded.data`, [stringifyJson(data.settings)]);
|
adapter.run(`INSERT INTO settings(id, data) VALUES(1, ?) ON CONFLICT(id) DO UPDATE SET data = excluded.data`, [stringifyJson(data.settings)]);
|
||||||
}
|
}
|
||||||
for (const c of data.providerConnections || []) {
|
|
||||||
|
importWithAssertion(adapter, "providerConnections", data.providerConnections || [], (c) => {
|
||||||
const { id, provider, authType, name, email, priority, isActive, createdAt, updatedAt, ...rest } = c;
|
const { id, provider, authType, name, email, priority, isActive, createdAt, updatedAt, ...rest } = c;
|
||||||
adapter.run(
|
adapter.run(
|
||||||
`INSERT OR REPLACE INTO providerConnections(id, provider, authType, name, email, priority, isActive, data, createdAt, updatedAt) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
`INSERT OR REPLACE INTO providerConnections(id, provider, authType, name, email, priority, isActive, data, createdAt, updatedAt) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
[id, provider, authType || "oauth", name || null, email || null, priority || null, isActive === false ? 0 : 1, stringifyJson(rest), createdAt || new Date().toISOString(), updatedAt || new Date().toISOString()]
|
[id, provider, authType || "oauth", name || null, email || null, priority || null, isActive === false ? 0 : 1, stringifyJson(rest), createdAt || new Date().toISOString(), updatedAt || new Date().toISOString()]
|
||||||
);
|
);
|
||||||
}
|
}, (c) => ({ id: c.id ?? null, provider: c.provider ?? null, name: c.name ?? null }));
|
||||||
for (const n of data.providerNodes || []) {
|
|
||||||
|
importWithAssertion(adapter, "providerNodes", data.providerNodes || [], (n) => {
|
||||||
const { id, type, name, createdAt, updatedAt, ...rest } = n;
|
const { id, type, name, createdAt, updatedAt, ...rest } = n;
|
||||||
adapter.run(
|
adapter.run(
|
||||||
`INSERT OR REPLACE INTO providerNodes(id, type, name, data, createdAt, updatedAt) VALUES(?, ?, ?, ?, ?, ?)`,
|
`INSERT OR REPLACE INTO providerNodes(id, type, name, data, createdAt, updatedAt) VALUES(?, ?, ?, ?, ?, ?)`,
|
||||||
[id, type || null, name || null, stringifyJson(rest), createdAt || new Date().toISOString(), updatedAt || new Date().toISOString()]
|
[id, type || null, name || null, stringifyJson(rest), createdAt || new Date().toISOString(), updatedAt || new Date().toISOString()]
|
||||||
);
|
);
|
||||||
}
|
}, (n) => ({ id: n.id ?? null, type: n.type ?? null, name: n.name ?? null }));
|
||||||
for (const p of data.proxyPools || []) {
|
|
||||||
|
importWithAssertion(adapter, "proxyPools", data.proxyPools || [], (p) => {
|
||||||
const { id, isActive, testStatus, createdAt, updatedAt, ...rest } = p;
|
const { id, isActive, testStatus, createdAt, updatedAt, ...rest } = p;
|
||||||
adapter.run(
|
adapter.run(
|
||||||
`INSERT OR REPLACE INTO proxyPools(id, isActive, testStatus, data, createdAt, updatedAt) VALUES(?, ?, ?, ?, ?, ?)`,
|
`INSERT OR REPLACE INTO proxyPools(id, isActive, testStatus, data, createdAt, updatedAt) VALUES(?, ?, ?, ?, ?, ?)`,
|
||||||
[id, isActive === false ? 0 : 1, testStatus || "unknown", stringifyJson(rest), createdAt || new Date().toISOString(), updatedAt || new Date().toISOString()]
|
[id, isActive === false ? 0 : 1, testStatus || "unknown", stringifyJson(rest), createdAt || new Date().toISOString(), updatedAt || new Date().toISOString()]
|
||||||
);
|
);
|
||||||
}
|
}, (p) => ({ id: p.id ?? null }));
|
||||||
for (const k of data.apiKeys || []) {
|
|
||||||
|
importWithAssertion(adapter, "apiKeys", data.apiKeys || [], (k) => {
|
||||||
adapter.run(
|
adapter.run(
|
||||||
`INSERT OR REPLACE INTO apiKeys(id, key, name, machineId, isActive, createdAt) VALUES(?, ?, ?, ?, ?, ?)`,
|
`INSERT OR REPLACE INTO apiKeys(id, key, name, machineId, isActive, createdAt) VALUES(?, ?, ?, ?, ?, ?)`,
|
||||||
[k.id, k.key, k.name || null, k.machineId || null, k.isActive === false ? 0 : 1, k.createdAt || new Date().toISOString()]
|
[k.id, k.key, k.name || null, k.machineId || null, k.isActive === false ? 0 : 1, k.createdAt || new Date().toISOString()]
|
||||||
);
|
);
|
||||||
}
|
}, (k) => ({ id: k.id ?? null, name: k.name ?? null }));
|
||||||
for (const c of data.combos || []) {
|
|
||||||
|
importWithAssertion(adapter, "combos", data.combos || [], (c) => {
|
||||||
adapter.run(
|
adapter.run(
|
||||||
`INSERT OR REPLACE INTO combos(id, name, kind, models, createdAt, updatedAt) VALUES(?, ?, ?, ?, ?, ?)`,
|
`INSERT OR REPLACE INTO combos(id, name, kind, models, createdAt, updatedAt) VALUES(?, ?, ?, ?, ?, ?)`,
|
||||||
[c.id, c.name, c.kind || null, stringifyJson(c.models || []), c.createdAt || new Date().toISOString(), c.updatedAt || new Date().toISOString()]
|
[c.id, c.name, c.kind || null, stringifyJson(c.models || []), c.createdAt || new Date().toISOString(), c.updatedAt || new Date().toISOString()]
|
||||||
);
|
);
|
||||||
}
|
}, (c) => ({ id: c.id ?? null, name: c.name ?? null }));
|
||||||
|
|
||||||
for (const [alias, model] of Object.entries(data.modelAliases || {})) {
|
for (const [alias, model] of Object.entries(data.modelAliases || {})) {
|
||||||
adapter.run(`INSERT OR REPLACE INTO kv(scope, key, value) VALUES('modelAliases', ?, ?)`, [alias, stringifyJson(model)]);
|
adapter.run(`INSERT OR REPLACE INTO kv(scope, key, value) VALUES('modelAliases', ?, ?)`, [alias, stringifyJson(model)]);
|
||||||
}
|
}
|
||||||
|
|
@ -210,14 +240,22 @@ export async function runMigrationOnce(adapter) {
|
||||||
const backupDir = makeBackupDir("migrate-from-json");
|
const backupDir = makeBackupDir("migrate-from-json");
|
||||||
for (const f of Object.values(LEGACY_FILES)) backupFile(f, backupDir);
|
for (const f of Object.values(LEGACY_FILES)) backupFile(f, backupDir);
|
||||||
|
|
||||||
adapter.transaction(() => {
|
try {
|
||||||
importLegacyMain(adapter, legacyMain);
|
adapter.transaction(() => {
|
||||||
importLegacyUsage(adapter, legacyUsage);
|
importLegacyMain(adapter, legacyMain);
|
||||||
importLegacyDisabled(adapter, legacyDisabled);
|
importLegacyUsage(adapter, legacyUsage);
|
||||||
importLegacyDetails(adapter, legacyDetails);
|
importLegacyDisabled(adapter, legacyDisabled);
|
||||||
setMetaSync(adapter, "appVersion", getAppVersion());
|
importLegacyDetails(adapter, legacyDetails);
|
||||||
setMetaSync(adapter, "migratedAt", new Date().toISOString());
|
setMetaSync(adapter, "appVersion", getAppVersion());
|
||||||
});
|
setMetaSync(adapter, "migratedAt", new Date().toISOString());
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof MigrationAborted) {
|
||||||
|
console.error(`[DB][migrate] aborted: ${err.message} | legacy JSON kept | backup: ${backupDir}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
try { fs.writeFileSync(MIGRATED_MARKER, new Date().toISOString()); } catch {}
|
try { fs.writeFileSync(MIGRATED_MARKER, new Date().toISOString()); } catch {}
|
||||||
pruneOldBackups();
|
pruneOldBackups();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
// All intercepted domains + URL patterns per tool
|
// All intercepted domains + URL patterns per tool
|
||||||
|
|
||||||
|
const IS_DEV = process.env.NODE_ENV === "development";
|
||||||
|
|
||||||
const TARGET_HOSTS = [
|
const TARGET_HOSTS = [
|
||||||
"daily-cloudcode-pa.googleapis.com",
|
"daily-cloudcode-pa.googleapis.com",
|
||||||
"cloudcode-pa.googleapis.com",
|
"cloudcode-pa.googleapis.com",
|
||||||
|
|
@ -20,6 +22,19 @@ const MODEL_SYNONYMS = {
|
||||||
antigravity: { "gemini-default": "gemini-3-flash" },
|
antigravity: { "gemini-default": "gemini-3-flash" },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Pattern fallback: rawModel regex → canonical alias key (when exact + prefix match fail)
|
||||||
|
// Order matters: more specific patterns first. Catches AG renamed variants (e.g. gemini-pro-agent)
|
||||||
|
const MODEL_PATTERNS = {
|
||||||
|
antigravity: [
|
||||||
|
{ match: /flash/i, alias: "gemini-3-flash" },
|
||||||
|
{ match: /pro.*low|low.*pro/i, alias: "gemini-3.1-pro-low" },
|
||||||
|
{ match: /gemini.*pro|pro.*gemini/i, alias: "gemini-3.1-pro-high" },
|
||||||
|
{ match: /opus/i, alias: "claude-opus-4-6-thinking" },
|
||||||
|
{ match: /sonnet|claude/i, alias: "claude-sonnet-4-6" },
|
||||||
|
{ match: /gpt.*oss|oss/i, alias: "gpt-oss-120b-medium" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
// URL substrings whose request/response should NOT be dumped to file (telemetry, polling, empty)
|
// URL substrings whose request/response should NOT be dumped to file (telemetry, polling, empty)
|
||||||
const LOG_BLACKLIST_URL_PARTS = [
|
const LOG_BLACKLIST_URL_PARTS = [
|
||||||
"recordCodeAssistMetrics",
|
"recordCodeAssistMetrics",
|
||||||
|
|
@ -38,4 +53,4 @@ function getToolForHost(host) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { TARGET_HOSTS, URL_PATTERNS, MODEL_SYNONYMS, LOG_BLACKLIST_URL_PARTS, getToolForHost };
|
module.exports = { IS_DEV, TARGET_HOSTS, URL_PATTERNS, MODEL_SYNONYMS, MODEL_PATTERNS, LOG_BLACKLIST_URL_PARTS, getToolForHost };
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
const { err, createResponseDumper } = require("../logger");
|
const { err, createResponseDumper } = require("../logger");
|
||||||
|
const { IS_DEV } = require("../config");
|
||||||
const { fetchRouter, pipeSSE } = require("./base");
|
const { fetchRouter, pipeSSE } = require("./base");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -7,7 +8,7 @@ const { fetchRouter, pipeSSE } = require("./base");
|
||||||
* runs antigravity→openai→provider→openai→antigravity translators internally.
|
* runs antigravity→openai→provider→openai→antigravity translators internally.
|
||||||
*/
|
*/
|
||||||
async function intercept(req, res, bodyBuffer, mappedModel) {
|
async function intercept(req, res, bodyBuffer, mappedModel) {
|
||||||
const dumper = createResponseDumper(req, "intercept-antigravity");
|
const dumper = IS_DEV ? createResponseDumper(req, "intercept-antigravity") : null;
|
||||||
const isStream = req.url.includes(":streamGenerateContent");
|
const isStream = req.url.includes(":streamGenerateContent");
|
||||||
try {
|
try {
|
||||||
const body = JSON.parse(bodyBuffer.toString());
|
const body = JSON.parse(bodyBuffer.toString());
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
const { err } = require("../logger");
|
const { err } = require("../logger");
|
||||||
|
const { IS_DEV } = require("../config");
|
||||||
const { fetchRouter } = require("./base");
|
const { fetchRouter } = require("./base");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
// Debug trace log — written to data/logs/mitm/kiro-debug.log
|
// Debug trace log — written to data/logs/mitm/kiro-debug.log (dev only)
|
||||||
const DEBUG_LOG = path.join(__dirname, "../../../data/logs/mitm/kiro-debug.log");
|
const DEBUG_LOG = path.join(__dirname, "../../../data/logs/mitm/kiro-debug.log");
|
||||||
function dbg(msg) {
|
function dbg(msg) {
|
||||||
|
if (!IS_DEV) return;
|
||||||
try {
|
try {
|
||||||
fs.appendFileSync(DEBUG_LOG, `${new Date().toISOString()} ${msg}\n`);
|
fs.appendFileSync(DEBUG_LOG, `${new Date().toISOString()} ${msg}\n`);
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,16 @@ const err = (msg) => console.error(`[${time()}] ❌ [MITM] ${msg}`);
|
||||||
const DUMP_DIR = path.join(DATA_DIR, "logs", "mitm");
|
const DUMP_DIR = path.join(DATA_DIR, "logs", "mitm");
|
||||||
if (!fs.existsSync(DUMP_DIR)) fs.mkdirSync(DUMP_DIR, { recursive: true });
|
if (!fs.existsSync(DUMP_DIR)) fs.mkdirSync(DUMP_DIR, { recursive: true });
|
||||||
|
|
||||||
|
// Clear all files inside DUMP_DIR (called on MITM server start to avoid unbounded growth)
|
||||||
|
function clearDumpDir() {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(DUMP_DIR)) return;
|
||||||
|
for (const f of fs.readdirSync(DUMP_DIR)) {
|
||||||
|
try { fs.rmSync(path.join(DUMP_DIR, f), { recursive: true, force: true }); } catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
const EMPTY_BODY_RE = /^\s*(\{\s*\}|\[\s*\]|null)?\s*$/;
|
const EMPTY_BODY_RE = /^\s*(\{\s*\}|\[\s*\]|null)?\s*$/;
|
||||||
|
|
||||||
function slugify(s, max = 80) {
|
function slugify(s, max = 80) {
|
||||||
|
|
@ -93,4 +103,4 @@ function createResponseDumper(req, tag = "raw") {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { log, err, dumpRequest, createResponseDumper };
|
module.exports = { log, err, dumpRequest, createResponseDumper, clearDumpDir };
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,17 @@ const path = require("path");
|
||||||
const dns = require("dns");
|
const dns = require("dns");
|
||||||
const { promisify } = require("util");
|
const { promisify } = require("util");
|
||||||
const { execSync } = require("child_process");
|
const { execSync } = require("child_process");
|
||||||
const { log, err, dumpRequest, createResponseDumper } = require("./logger");
|
const { log, err, dumpRequest, createResponseDumper, clearDumpDir } = require("./logger");
|
||||||
const { TARGET_HOSTS, URL_PATTERNS, MODEL_SYNONYMS, getToolForHost } = require("./config");
|
const { IS_DEV, TARGET_HOSTS, URL_PATTERNS, MODEL_SYNONYMS, MODEL_PATTERNS, getToolForHost } = require("./config");
|
||||||
const { DATA_DIR, MITM_DIR } = require("./paths");
|
const { DATA_DIR, MITM_DIR } = require("./paths");
|
||||||
const { getCertForDomain } = require("./cert/generate");
|
const { getCertForDomain } = require("./cert/generate");
|
||||||
const { getMitmAlias } = require("./dbReader");
|
const { getMitmAlias } = require("./dbReader");
|
||||||
const LOCAL_PORT = 443;
|
const LOCAL_PORT = 443;
|
||||||
const IS_WIN = process.platform === "win32";
|
const IS_WIN = process.platform === "win32";
|
||||||
const ENABLE_FILE_LOG = true;
|
const ENABLE_FILE_LOG = IS_DEV;
|
||||||
|
|
||||||
|
// Clear stale dump files on every MITM start (prevents unbounded disk usage)
|
||||||
|
clearDumpDir();
|
||||||
const INTERNAL_REQUEST_HEADER = { name: "x-request-source", value: "local" };
|
const INTERNAL_REQUEST_HEADER = { name: "x-request-source", value: "local" };
|
||||||
|
|
||||||
// Host rewrite for upstream forward: PROD cloudcode-pa is rate-limited (429),
|
// Host rewrite for upstream forward: PROD cloudcode-pa is rate-limited (429),
|
||||||
|
|
@ -108,7 +111,13 @@ function getMappedModel(tool, model) {
|
||||||
if (aliases[lookup]) return aliases[lookup];
|
if (aliases[lookup]) return aliases[lookup];
|
||||||
// Prefix match fallback
|
// Prefix match fallback
|
||||||
const prefixKey = Object.keys(aliases).find(k => k && aliases[k] && (lookup.startsWith(k) || k.startsWith(lookup)));
|
const prefixKey = Object.keys(aliases).find(k => k && aliases[k] && (lookup.startsWith(k) || k.startsWith(lookup)));
|
||||||
return prefixKey ? aliases[prefixKey] : null;
|
if (prefixKey) return aliases[prefixKey];
|
||||||
|
// Pattern fallback: catches AG renamed variants (e.g. gemini-pro-agent → gemini-3.1-pro-high)
|
||||||
|
const patterns = MODEL_PATTERNS?.[tool] || [];
|
||||||
|
for (const { match, alias } of patterns) {
|
||||||
|
if (match.test(lookup) && aliases[alias]) return aliases[alias];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,9 @@ export const MITM_TOOLS = {
|
||||||
description: "Google Antigravity IDE with MITM",
|
description: "Google Antigravity IDE with MITM",
|
||||||
configType: "mitm",
|
configType: "mitm",
|
||||||
mitmDomain: "daily-cloudcode-pa.googleapis.com",
|
mitmDomain: "daily-cloudcode-pa.googleapis.com",
|
||||||
modelAliases: ["claude-opus-4-6-thinking", "claude-sonnet-4-6", "gemini-3-flash", "gpt-oss-120b-medium", "gemini-3-pro-high", "gemini-3-pro-low"],
|
modelAliases: ["claude-opus-4-6-thinking", "claude-sonnet-4-6", "gemini-3-flash", "gpt-oss-120b-medium", "gemini-3-pro-high", "gemini-3-pro-low", "gemini-pro-agent"],
|
||||||
defaultModels: [
|
defaultModels: [
|
||||||
|
{ id: "gemini-pro-agent", name: "Gemini Pro Agent (AG v1.23+ Agent Mode)", alias: "gemini-pro-agent" },
|
||||||
{ id: "gemini-3.1-pro-high", name: "Gemini 3.1 Pro High", alias: "gemini-3.1-pro-high" },
|
{ id: "gemini-3.1-pro-high", name: "Gemini 3.1 Pro High", alias: "gemini-3.1-pro-high" },
|
||||||
{ id: "gemini-3.1-pro-low", name: "Gemini 3.1 Pro Low", alias: "gemini-3.1-pro-low" },
|
{ id: "gemini-3.1-pro-low", name: "Gemini 3.1 Pro Low", alias: "gemini-3.1-pro-low" },
|
||||||
{ id: "gemini-3-flash", name: "Gemini 3 Flash / Default", alias: "gemini-3-flash" },
|
{ id: "gemini-3-flash", name: "Gemini 3 Flash / Default", alias: "gemini-3-flash" },
|
||||||
|
|
|
||||||
|
|
@ -1,52 +1,40 @@
|
||||||
import { machineIdSync } from 'node-machine-id';
|
import { machineIdSync } from 'node-machine-id';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import crypto from 'node:crypto';
|
||||||
|
import { DATA_DIR } from '@/lib/dataDir';
|
||||||
|
|
||||||
/**
|
const MACHINE_ID_FILE = path.join(DATA_DIR, 'machine-id');
|
||||||
* Get consistent machine ID using node-machine-id with salt
|
let cachedRawId = null;
|
||||||
* This ensures the same physical machine gets the same ID across runs
|
|
||||||
*
|
// Persist raw machine ID to file → guarantees CLI/server/middleware see same value
|
||||||
* @param {string} salt - Optional salt to use (defaults to environment variable)
|
// even when machineIdSync fails or returns inconsistent values across runtimes.
|
||||||
* @returns {Promise<string>} Machine ID (16-character base32)
|
function loadRawMachineId() {
|
||||||
*/
|
if (cachedRawId) return cachedRawId;
|
||||||
export async function getConsistentMachineId(salt = null) {
|
|
||||||
// For server-side, use node-machine-id with salt
|
|
||||||
const saltValue = salt || process.env.MACHINE_ID_SALT || 'endpoint-proxy-salt';
|
|
||||||
try {
|
try {
|
||||||
const rawMachineId = machineIdSync();
|
cachedRawId = fs.readFileSync(MACHINE_ID_FILE, 'utf8').trim();
|
||||||
// Create consistent ID using salt
|
if (cachedRawId) return cachedRawId;
|
||||||
const crypto = await import('crypto');
|
} catch {}
|
||||||
const hashedMachineId = crypto.createHash('sha256').update(rawMachineId + saltValue).digest('hex');
|
try {
|
||||||
// Return only first 16 characters for brevity
|
cachedRawId = machineIdSync();
|
||||||
return hashedMachineId.substring(0, 16);
|
} catch {
|
||||||
} catch (error) {
|
cachedRawId = crypto.randomUUID();
|
||||||
console.log('Error getting machine ID:', error);
|
|
||||||
// Fallback to random ID if node-machine-id fails
|
|
||||||
return crypto.randomUUID ? crypto.randomUUID() :
|
|
||||||
'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
||||||
const r = Math.random() * 16 | 0;
|
|
||||||
const v = c == 'x' ? r : (r & 0x3 | 0x8);
|
|
||||||
return v.toString(16);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||||
|
fs.writeFileSync(MACHINE_ID_FILE, cachedRawId, { mode: 0o600 });
|
||||||
|
} catch {}
|
||||||
|
return cachedRawId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getConsistentMachineId(salt = null) {
|
||||||
|
const saltValue = salt || process.env.MACHINE_ID_SALT || 'endpoint-proxy-salt';
|
||||||
|
const raw = loadRawMachineId();
|
||||||
|
return crypto.createHash('sha256').update(raw + saltValue).digest('hex').substring(0, 16);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get raw machine ID without hashing (for debugging purposes)
|
|
||||||
* @returns {Promise<string>} Raw machine ID
|
|
||||||
*/
|
|
||||||
export async function getRawMachineId() {
|
export async function getRawMachineId() {
|
||||||
// For server-side, use raw node-machine-id
|
return loadRawMachineId();
|
||||||
try {
|
|
||||||
return machineIdSync();
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Error getting raw machine ID:', error);
|
|
||||||
// Fallback to random ID if node-machine-id fails
|
|
||||||
return crypto.randomUUID ? crypto.randomUUID() :
|
|
||||||
'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
||||||
const r = Math.random() * 16 | 0;
|
|
||||||
const v = c == 'x' ? r : (r & 0x3 | 0x8);
|
|
||||||
return v.toString(16);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue