/** * Background Task Detector — Feature 3 * * Detects when CLI tools send "background" requests (title generation, * summarization, short descriptions) and provides model degradation * recommendations to save premium model quota. * * Detection heuristics: * - System prompt patterns indicating background/utility tasks * - Very short conversations with summary-like system prompts * - X-Request-Priority header */ // ── Configuration ─────────────────────────────────────────────────────────── interface DegradationConfig { enabled: boolean; degradationMap: Record; // original → cheaper model detectionPatterns: string[]; // regex patterns for system prompt matching stats: { detected: number; tokensSaved: number; }; } const DEFAULT_DETECTION_PATTERNS = [ "generate a title", "generate title", "create a title", "create a short", "summarize this", "summarize the", "write a brief", "write a summary", "one-line summary", "one line summary", "short description", "brief description", "conversation title", "chat title", "name this conversation", "name this chat", "title for this", "suggest a title", "label this", ]; const DEFAULT_DEGRADATION_MAP: Record = { // Premium → Cheap alternatives "claude-opus-4-6": "gemini-3-flash", "claude-opus-4-6-thinking": "gemini-3-flash", "claude-opus-4-5-20251101": "gemini-3-flash", "claude-sonnet-4-5-20250929": "gemini-3-flash", "claude-sonnet-4-20250514": "gemini-3-flash", "claude-sonnet-4": "gemini-3-flash", "gemini-3.1-pro": "gemini-3-flash", "gemini-3.1-pro-high": "gemini-3-flash", "gemini-3-pro-preview": "gemini-3-flash-preview", "gemini-2.5-pro": "gemini-3-flash", "gpt-4o": "gpt-4o-mini", "gpt-5": "gpt-5-mini", "gpt-5.1": "gpt-5-mini", "gpt-5.1-codex": "gpt-5.1-codex-mini", }; // ── State ─────────────────────────────────────────────────────────────────── // Backed by globalThis so the singleton is shared across the SEPARATE webpack // module graphs Next.js builds for `instrumentation.ts` (boot-time hydration via // applyRuntimeSettings → setBackgroundDegradationConfig) and the app-route / // open-sse executors (per-request reads in the chat handler). A module-local `let` // is duplicated per graph, so the operator's opt-in (`enabled:true`) applied at boot // never reaches the request path — the degradation silently never fires (the // #5312-class module-graph bug). Mirrors systemPrompt.ts (#2470) and thinkingBudget.ts. const GLOBAL_KEY = "__omniroute_backgroundDegradation_config__"; const _store = globalThis as unknown as Record; function getConfig(): DegradationConfig { if (!_store[GLOBAL_KEY]) { _store[GLOBAL_KEY] = { enabled: false, // Disabled by default — user must opt in degradationMap: { ...DEFAULT_DEGRADATION_MAP }, detectionPatterns: [...DEFAULT_DETECTION_PATTERNS], stats: { detected: 0, tokensSaved: 0 }, }; } return _store[GLOBAL_KEY]!; } // ── Config Management ─────────────────────────────────────────────────────── /** * Set the background degradation config (called from settings API or startup). */ export function setBackgroundDegradationConfig(config: Partial): void { _store[GLOBAL_KEY] = { ...getConfig(), ...config, stats: getConfig().stats, // preserve stats across config changes }; } /** * Get current background degradation config. */ export function getBackgroundDegradationConfig(): DegradationConfig { return { ...getConfig(), degradationMap: { ...getConfig().degradationMap }, detectionPatterns: [...getConfig().detectionPatterns], stats: { ...getConfig().stats }, }; } /** * Reset stats counters. */ export function resetStats(): void { getConfig().stats = { detected: 0, tokensSaved: 0 }; } // ── Detection ─────────────────────────────────────────────────────────────── interface BackgroundMessage { role?: string; content?: unknown; } interface BackgroundTaskBody { messages?: BackgroundMessage[]; input?: BackgroundMessage[]; max_tokens?: unknown; max_completion_tokens?: unknown; max_output_tokens?: unknown; } function toMessageArray(value: unknown): BackgroundMessage[] { return Array.isArray(value) ? (value as BackgroundMessage[]) : []; } function toFiniteNumber(value: unknown): number | null { if (typeof value === "number" && Number.isFinite(value)) return value; if (typeof value === "string" && value.trim().length > 0) { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : null; } return null; } function headerValue(headers: Record | null, key: string): string { if (!headers) return ""; const value = headers[key] ?? headers[key.toLowerCase()] ?? headers[key.toUpperCase()]; return typeof value === "string" ? value.trim() : ""; } /** * Get reason label when request is a background/utility task. * * @param {object} body - Request body * @param {object} [headers] - Request headers (optional) * @returns {string | null} Reason label or null when not detected */ export function getBackgroundTaskReason( body: BackgroundTaskBody | unknown, headers: Record | null = null ): string | null { if (!body || typeof body !== "object") return null; const typedBody = body as BackgroundTaskBody; // 1. Check explicit header if (headers) { const taskType = headerValue(headers, "x-task-type"); const priority = headerValue(headers, "x-request-priority"); const initiator = headerValue(headers, "x-initiator"); const explicitValue = [taskType, priority, initiator].find(Boolean); if (explicitValue && explicitValue.toLowerCase() === "background") { return "header_background"; } } // 2. Very low max tokens usually indicates utility/background tasks const maxTokens = toFiniteNumber( typedBody.max_tokens ?? typedBody.max_completion_tokens ?? typedBody.max_output_tokens ); if (maxTokens !== null && maxTokens > 0 && maxTokens < 50) { return "low_max_tokens"; } // 3. Check system prompt for background task patterns const messages = toMessageArray(typedBody.messages ?? typedBody.input ?? []); if (!Array.isArray(messages) || messages.length === 0) return null; // Find system message const systemMsg = messages.find( (message: BackgroundMessage) => message.role === "system" || message.role === "developer" ); if (!systemMsg) return null; const systemContent = typeof systemMsg.content === "string" ? systemMsg.content.toLowerCase() : ""; if (!systemContent) return null; // Check against detection patterns const matched = getConfig().detectionPatterns.some((pattern) => systemContent.includes(pattern.toLowerCase()) ); if (!matched) return null; // 4. Additional heuristic: background tasks typically have very few messages // (system + 1-2 user messages) const userMessages = messages.filter((message: BackgroundMessage) => message.role === "user"); if (userMessages.length > 3) return null; // Too many turns for a background task return "system_prompt_pattern"; } /** * Check if a request is a background/utility task. * * @param {object} body - Request body * @param {object} [headers] - Request headers (optional) * @returns {boolean} True if the request looks like a background task */ export function isBackgroundTask( body: BackgroundTaskBody | unknown, headers: Record | null = null ): boolean { return getBackgroundTaskReason(body, headers) !== null; } /** * Get the degraded (cheaper) model for a given model. * * @param {string} originalModel - The original model ID * @returns {string} The cheaper model or original if no mapping exists */ export function getDegradedModel(originalModel: string): string { if (!originalModel) return originalModel; const degraded = getConfig().degradationMap[originalModel]; if (degraded) { getConfig().stats.detected++; return degraded; } return originalModel; } /** * Get default degradation map (for UI reset). */ export function getDefaultDegradationMap(): Record { return { ...DEFAULT_DEGRADATION_MAP }; } /** * Get default detection patterns (for UI reset). */ export function getDefaultDetectionPatterns(): string[] { return [...DEFAULT_DETECTION_PATTERNS]; }