26ce1241c6
Use the default headroom health probe instead of the goosed fetch adapter so active mode routes deepseek-no-think through the proxy, and add an E2E script that verifies goosed pong via headroom with fail-open portal checks. Co-authored-by: Cursor <cursoragent@cursor.com>
190 lines
5.7 KiB
JavaScript
190 lines
5.7 KiB
JavaScript
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);
|
|
}
|
|
}
|