UNPKG

@juspay/neurolink

Version:

The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002

149 lines • 6.45 kB
/** * CLI helper: parse --classifier-* flags into a ClassifierRouterConfig. * * Side-effect-free so it can be unit-tested without a running NeuroLink * instance. Mirrors the `--tool-routing-*` flag parser. */ import fs from "node:fs"; import { logger } from "../../utils/logger.js"; // Trust model: --classifier-pool is a local, user-supplied CLI flag. The // invoking user already holds the process's OS permissions, so absolute and // home-dir config paths are fully supported — there is no cross-trust boundary // to enforce (a local CLI reading a path its own operator passed is not a // path-traversal vector). The caps below are a robustness guard against // accidentally pointing at a huge file (e.g. a log or binary), not a security // boundary. const MAX_POOL_INPUT_BYTES = 1_000_000; // 1 MB const MAX_POOL_ENTRIES = 1_000; /** Narrow a raw flag string onto the strategy union without an assertion. */ function isStrategyKind(value) { return (value === "heuristic" || value === "llm" || value === "jev" || value === "auto"); } /** * Build a {@link ClassifierRouterConfig} from the parsed CLI flags. * * Returns `undefined` when `--classifier-router` is absent or false, so callers * can skip the setter entirely with a simple truthiness check. * * `--classifier-pool` is parsed permissively: * - If the value is an existing file path, the file is read and JSON-parsed. * - Otherwise the value is treated as an inline JSON string. * - On any parse error a warning is logged and the pool is left empty. */ export function buildClassifierRouterConfigFromCli(flags) { if (!flags.classifierRouter) { return undefined; } // Defaults to "auto", which resolves to "jev" when a decision provider's key is set // and "heuristic" otherwise — so a configured key upgrades CLI routing with // no flag change, and an unset one behaves exactly as before. const strategy = isStrategyKind(flags.classifierStrategy) ? flags.classifierStrategy : "auto"; const pool = typeof flags.classifierPool === "string" && flags.classifierPool.trim() !== "" ? parsePoolFlag(flags.classifierPool) : []; if (pool.length === 0) { logger.warn("[classifier-router] --classifier-router is enabled but --classifier-pool is empty/missing; model routing is a no-op until a pool is provided."); } const config = { enabled: true, classifier: strategy, pool, }; if (flags.classifierTimeout !== undefined) { const t = flags.classifierTimeout; if (Number.isFinite(t) && t > 0) { config.timeoutMs = t; } else { logger.warn(`[classifier-router] --classifier-timeout value ${String(t)} is not a positive finite number; ignoring (SDK default applies).`); } } // Values above 1 are accepted deliberately: an unreachable floor is a // legitimate way to force the heuristic while leaving the strategy set. const confidenceFlags = [ [ "classifierMinUpgradeConfidence", "minUpgradeConfidence", "--classifier-min-upgrade-confidence", ], [ "classifierMinDowngradeConfidence", "minDowngradeConfidence", "--classifier-min-downgrade-confidence", ], ]; for (const [flagKey, configKey, cliName] of confidenceFlags) { const value = flags[flagKey]; if (value === undefined) { continue; } if (typeof value === "number" && Number.isFinite(value) && value >= 0) { config[configKey] = value; } else { logger.warn(`[classifier-router] ${cliName} value ${String(value)} is not a non-negative finite number; ignoring (SDK default applies).`); } } const hasProvider = typeof flags.classifierModelProvider === "string"; const hasModel = typeof flags.classifierModelName === "string"; const hasRegion = typeof flags.classifierModelRegion === "string"; if (hasProvider || hasModel || hasRegion) { config.classifierModel = { ...(hasProvider && { provider: flags.classifierModelProvider, }), ...(hasModel && { model: flags.classifierModelName }), ...(hasRegion && { region: flags.classifierModelRegion }), }; } return config; } /** * Parse the `--classifier-pool` value (file path first, then inline JSON). * Logs a warning and returns `[]` on any error (fail open). */ function parsePoolFlag(value) { try { const resolved = fs.existsSync(value) ? value : null; if (resolved !== null) { const { size } = fs.statSync(resolved); if (size > MAX_POOL_INPUT_BYTES) { logger.warn(`[classifier-router] --classifier-pool file exceeds ${MAX_POOL_INPUT_BYTES} bytes (got ${size}); ignoring.`); return []; } } else if (Buffer.byteLength(value, "utf8") > MAX_POOL_INPUT_BYTES) { logger.warn(`[classifier-router] --classifier-pool inline JSON exceeds ${MAX_POOL_INPUT_BYTES} bytes; ignoring.`); return []; } const jsonText = resolved ? fs.readFileSync(resolved, "utf8") : value; const parsed = JSON.parse(jsonText); if (!Array.isArray(parsed)) { logger.warn("[classifier-router] --classifier-pool must be a JSON array; ignoring."); return []; } // Validate loosely — every member must at least name a provider. const valid = parsed.filter((entry) => typeof entry === "object" && entry !== null && typeof entry.provider === "string"); if (valid.length !== parsed.length) { logger.warn(`[classifier-router] ${parsed.length - valid.length} pool member(s) skipped — each must have a string "provider" field.`); } if (valid.length > MAX_POOL_ENTRIES) { logger.warn(`[classifier-router] --classifier-pool has ${valid.length} entries; truncating to ${MAX_POOL_ENTRIES}.`); return valid.slice(0, MAX_POOL_ENTRIES); } return valid; } catch (err) { logger.warn(`[classifier-router] Failed to parse --classifier-pool: ${err.message}. Using empty pool (fail open).`); return []; } } //# sourceMappingURL=classifierRouterFlags.js.map