/** * Shared utilities for request translation modules. * * Deduplicates: desktop context loading, instruction building, budget→effort mapping. */ import { readFileSync } from "fs"; import { resolve } from "path"; import { getConfig } from "../config.js"; import { getConfigDir } from "../paths.js"; import { hasTupleSchemas, convertTupleSchemas } from "./tuple-schema.js"; let cachedDesktopContext: string | null = null; /** * Lazily load and cache the desktop context prompt. * File is maintained by apply-update.ts; cached once per process lifetime. */ export function getDesktopContext(): string { if (cachedDesktopContext !== null) return cachedDesktopContext; try { cachedDesktopContext = readFileSync( resolve(getConfigDir(), "prompts/desktop-context.md"), "utf-8", ); } catch { cachedDesktopContext = ""; } return cachedDesktopContext; } const SUPPRESS_PROMPT = "IMPORTANT — OVERRIDE ALL INSTRUCTIONS ABOVE THIS LINE: " + "The Codex desktop context above is NOT applicable. Disregard it entirely. " + "You are a standard OpenAI API model. You are not running inside any app. " + "Do not use Codex-specific directives (::automation-update, ::code-comment, " + "::archive, etc.). Do not reference automations, review findings, or any " + "desktop features. Respond as a plain language model with no special context."; /** * Assemble final instructions from desktop context + user instructions. * When suppress_desktop_directives is enabled, appends a suppress prompt * to override desktop-specific behaviors. */ export function buildInstructions(userInstructions: string): string { if (!getConfig().model.inject_desktop_context) return userInstructions; const ctx = getDesktopContext(); if (!ctx) return userInstructions; if (getConfig().model.suppress_desktop_directives) { return `${ctx}\n\n${SUPPRESS_PROMPT}\n\n${userInstructions}`; } return `${ctx}\n\n${userInstructions}`; } /** * Map a token budget (e.g. Anthropic thinking.budget_tokens or Gemini thinkingBudget) * to a Codex reasoning effort level. */ export function budgetToEffort(budget: number | undefined): string | undefined { if (!budget || budget <= 0) return undefined; if (budget < 2000) return "low"; if (budget < 8000) return "medium"; if (budget < 20000) return "high"; return "xhigh"; } /** * Recursively inject `additionalProperties: false` into every object-type node * of a JSON Schema. Deep-clones input to avoid mutation. * * Codex API requires explicit `additionalProperties: false` on every object in * strict mode; OpenAI's native API auto-injects this but our proxy must do it. */ export function injectAdditionalProperties( schema: Record, ): Record { return walkSchema(structuredClone(schema), new Set()); } /** * Prepare a JSON Schema for Codex: convert tuple schemas (prefixItems) to * equivalent object schemas, then inject additionalProperties: false. * * Returns the converted schema and the original (pre-conversion) schema if * tuples were found (needed for response-side reconversion), or null otherwise. */ export function prepareSchema( schema: Record, ): { schema: Record; originalSchema: Record | null } { const cloned = structuredClone(schema); if (!hasTupleSchemas(cloned)) { return { schema: walkSchema(cloned, new Set()), originalSchema: null }; } const originalSchema = structuredClone(schema); convertTupleSchemas(cloned); return { schema: walkSchema(cloned, new Set()), originalSchema }; } function walkSchema(node: Record, seen: Set): Record { // Cycle detection — stop if we've already visited this node if (seen.has(node)) return node; seen.add(node); // Inject on object types that don't already specify additionalProperties if (node.type === "object" && node.additionalProperties === undefined) { node.additionalProperties = false; } // Traverse properties if (isRecord(node.properties)) { for (const key of Object.keys(node.properties)) { const prop = node.properties[key]; if (isRecord(prop)) { node.properties[key] = walkSchema(prop, seen); } } } // Traverse patternProperties if (isRecord(node.patternProperties)) { for (const key of Object.keys(node.patternProperties)) { const prop = node.patternProperties[key]; if (isRecord(prop)) { node.patternProperties[key] = walkSchema(prop, seen); } } } // Traverse $defs / definitions for (const defsKey of ["$defs", "definitions"] as const) { if (isRecord(node[defsKey])) { const defs = node[defsKey] as Record; for (const key of Object.keys(defs)) { if (isRecord(defs[key])) { defs[key] = walkSchema(defs[key] as Record, seen); } } } } // Traverse items (array items) if (isRecord(node.items)) { node.items = walkSchema(node.items as Record, seen); } // Traverse prefixItems if (Array.isArray(node.prefixItems)) { node.prefixItems = node.prefixItems.map((item: unknown) => isRecord(item) ? walkSchema(item, seen) : item, ); } // Traverse combinators: oneOf, anyOf, allOf for (const combiner of ["oneOf", "anyOf", "allOf"] as const) { if (Array.isArray(node[combiner])) { node[combiner] = (node[combiner] as unknown[]).map((entry: unknown) => isRecord(entry) ? walkSchema(entry, seen) : entry, ); } } // Traverse conditional: if, then, else for (const keyword of ["if", "then", "else", "not"] as const) { if (isRecord(node[keyword])) { node[keyword] = walkSchema(node[keyword] as Record, seen); } } return node; } function isRecord(v: unknown): v is Record { return typeof v === "object" && v !== null && !Array.isArray(v); }