Files
portal/tools/llm-judge-config.mjs
T

85 lines
3.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* llm-judge-config — the Layer 4 enabling-gate for router-gate v4.
*
* The LLM-judge engine (llm-judge.mjs) is fully built but MUST stay OFF until
* the owner deliberately turns it on, because enabling it incurs real LLM cost
* (~$3001500/month per the v4.1 amendment). This module is the single switch.
*
* SAFE-BY-DEFAULT CONTRACT:
* enabled === true ⇔ the explicit flag ROUTER_LLM_JUDGE_ENABLED is truthy
* AND a key is resolvable (keychain first, then env).
* Anything else → enabled:false. Building this file does NOT enable the judge:
* with no flag and no key the gate is closed. keychainGet errors degrade to
* "no key, disabled" (never throw).
*
* Activation (a separate, owner-driven step — NOT done here):
* 1. store the API key in the OS keychain (or set ROUTER_LLM_KEY),
* 2. set ROUTER_LLM_JUDGE_ENABLED=1,
* 3. register the enforce-llm-judge-* hooks in .claude/settings.json.
* Cost starts only after all three.
*/
import { JUDGE_MODELS } from './llm-judge.mjs';
const ENABLE_FLAG = 'ROUTER_LLM_JUDGE_ENABLED';
const KEY_ENV = 'ROUTER_LLM_KEY';
const BASE_URL_ENV = 'ROUTER_LLM_BASE_URL';
const KEYCHAIN_SERVICE = 'router-gate-llm-judge';
const KEYCHAIN_ACCOUNT = 'default';
function isTruthyFlag(v) {
if (typeof v !== 'string') return false;
return v.trim().toLowerCase() === '1' || v.trim().toLowerCase() === 'true';
}
/**
* Resolve the Layer 4 judge configuration.
*
* @param {object} [args]
* @param {object} [args.env] - environment map (defaults to process.env)
* @param {Function} [args.keychainGet] - () => string|null, OS-keychain reader (injectable for tests)
* @returns {{enabled:boolean, apiKey:string|null, baseUrl:string|null, models:string[]}}
*/
export function resolveJudgeConfig({ env = process.env, keychainGet = defaultKeychainGet } = {}) {
let keychainKey = null;
try {
const v = keychainGet();
keychainKey = v ? String(v) : null;
} catch {
keychainKey = null;
}
const envKey = env[KEY_ENV] ? String(env[KEY_ENV]) : null;
const apiKey = keychainKey || envKey || null;
const flagOn = isTruthyFlag(env[ENABLE_FLAG]);
const enabled = flagOn && apiKey !== null;
return {
enabled,
apiKey,
baseUrl: env[BASE_URL_ENV] ? String(env[BASE_URL_ENV]) : null,
models: JUDGE_MODELS.multi,
};
}
/**
* Default OS-keychain reader. Lazily loads `keytar`; returns null if keytar is
* absent or the entry is missing. Never throws (caller also guards).
*/
export function defaultKeychainGet() {
try {
// Lazy require keeps the native dep optional — tests inject keychainGet and
// never hit this path; the no-op posture means missing keytar => no key.
const require = createRequire(import.meta.url);
const keytar = require('keytar');
const v = keytar.getPassword ? keytar.getPasswordSync?.(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT) : null;
return v || null;
} catch {
return null;
}
}
import { createRequire } from 'node:module';
export const _internals = { ENABLE_FLAG, KEY_ENV, BASE_URL_ENV, KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT, isTruthyFlag };