import { spawn } from 'node:child_process'; import { resolveDeepseekNoThinkProxyBaseUrl, } from './deepseek-no-think-proxy.mjs'; export const DEFAULT_HEADROOM_PROXY_PORT = 8787; export const MEMIND_HEADROOM_MODES = Object.freeze(['off', 'shadow', 'active']); export const DEFAULT_HEADROOM_EXCLUDED_SKILLS = Object.freeze([ 'static-page-publish', 'page-data-collect', 'aider-development', ]); function normalizeMode(value, fallback = 'off') { const raw = String(value ?? fallback).trim().toLowerCase(); return MEMIND_HEADROOM_MODES.includes(raw) ? raw : fallback; } export function resolveHeadroomMode(env = process.env) { return normalizeMode(env.MEMIND_HEADROOM_MODE, 'off'); } export function resolveHeadroomProxyPort(env = process.env) { const port = Number(env.MEMIND_HEADROOM_PROXY_PORT ?? DEFAULT_HEADROOM_PROXY_PORT); return Number.isFinite(port) && port > 0 ? port : DEFAULT_HEADROOM_PROXY_PORT; } export function resolveHeadroomProxyBaseUrl(env = process.env) { const explicit = String(env.MEMIND_HEADROOM_PROXY_BASE_URL ?? '').trim(); if (explicit) return explicit.replace(/\/$/, ''); const host = String(env.MEMIND_HEADROOM_PROXY_HOST ?? '127.0.0.1').trim() || '127.0.0.1'; return `http://${host}:${resolveHeadroomProxyPort(env)}/v1`; } export function resolveHeadroomUpstreamBaseUrl(env = process.env) { const explicit = String(env.MEMIND_HEADROOM_UPSTREAM_BASE_URL ?? '').trim(); if (explicit) return explicit.replace(/\/$/, ''); return resolveDeepseekNoThinkProxyBaseUrl(env); } export function parseHeadroomExcludedSkills(env = process.env) { const raw = String(env.MEMIND_HEADROOM_EXCLUDED_SKILLS ?? '').trim(); if (!raw) return [...DEFAULT_HEADROOM_EXCLUDED_SKILLS]; return raw.split(',').map((item) => item.trim()).filter(Boolean); } export function isHeadroomEligibleForSkill(skillId, env = process.env) { if (resolveHeadroomMode(env) === 'off') return false; const skill = String(skillId ?? '').trim(); if (!skill) return true; return !parseHeadroomExcludedSkills(env).includes(skill); } /** * Decide whether goosed should talk to headroom instead of the direct compat proxy. * fail-open: active mode falls back to the original apiUrl when headroom is unreachable. */ export function resolveGoosedApiUrlWithHeadroom({ apiUrl, mode = resolveHeadroomMode(), headroomReachable = false, eligible = true, env = process.env, } = {}) { const normalizedMode = normalizeMode(mode, 'off'); const original = String(apiUrl ?? '').trim(); if (!original || normalizedMode === 'off' || !eligible) { return { apiUrl: original, mode: normalizedMode, routed: false, eligible, }; } const headroomBase = resolveHeadroomProxyBaseUrl(env); if (normalizedMode === 'shadow') { return { apiUrl: original, mode: 'shadow', routed: false, eligible: true, wouldRouteTo: headroomBase, upstreamBaseUrl: resolveHeadroomUpstreamBaseUrl(env), }; } if (!headroomReachable) { return { apiUrl: original, mode: 'active', routed: false, eligible: true, fallback: 'headroom_unreachable', wouldRouteTo: headroomBase, }; } return { apiUrl: headroomBase, mode: 'active', routed: true, eligible: true, upstreamBaseUrl: resolveHeadroomUpstreamBaseUrl(env), }; } export function buildHeadroomRunObservation({ skillId, env = process.env } = {}) { const mode = resolveHeadroomMode(env); const eligible = isHeadroomEligibleForSkill(skillId, env); return { mode, eligible, excludedSkills: parseHeadroomExcludedSkills(env), proxyBaseUrl: mode !== 'off' ? resolveHeadroomProxyBaseUrl(env) : null, upstreamBaseUrl: mode !== 'off' ? resolveHeadroomUpstreamBaseUrl(env) : null, }; } export async function ensureHeadroomProxyRunning({ env = process.env, spawnImpl = spawn, startupWaitMs = 20_000, } = {}) { const baseUrl = resolveHeadroomProxyBaseUrl(env); if (await probeHeadroomProxyReachable({ baseUrl, env })) { return { started: false, baseUrl, reachable: true }; } const upstream = resolveHeadroomUpstreamBaseUrl(env); const port = resolveHeadroomProxyPort(env); const host = String(env.MEMIND_HEADROOM_PROXY_HOST ?? '127.0.0.1').trim() || '127.0.0.1'; const openaiApiRoot = upstream.replace(/\/v1\/?$/, ''); const child = spawnImpl( 'headroom', ['proxy', '--port', String(port), '--host', host, '--openai-api-url', openaiApiRoot], { detached: true, stdio: 'ignore', env: { ...process.env, ...env, OPENAI_TARGET_API_URL: upstream, HEADROOM_OUTPUT_SHAPER: '0', }, }, ); child.unref?.(); const deadline = Date.now() + startupWaitMs; while (Date.now() < deadline) { await new Promise((resolve) => setTimeout(resolve, 1000)); if (await probeHeadroomProxyReachable({ baseUrl, env })) { return { started: true, baseUrl, reachable: true, upstream, port }; } } return { started: true, baseUrl, reachable: false, upstream, port }; } export async function probeHeadroomProxyReachable({ baseUrl, fetchImpl = fetch, timeoutMs = 1500, env = process.env, } = {}) { const root = String(baseUrl ?? resolveHeadroomProxyBaseUrl(env)) .trim() .replace(/\/v1\/?$/i, ''); if (!root) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetchImpl(`${root}/v1/models`, { method: 'GET', signal: controller.signal, headers: { Authorization: 'Bearer headroom-probe' }, }); // Proxy alive when it responds, even with auth/upstream errors. return response.status > 0; } catch { return false; } finally { clearTimeout(timer); } }