codex-proxy / src /translation /shared-utils.ts
icebear
fix: disable desktop context injection by default (#95) (#97)
921d606 unverified
raw
history blame
5.99 kB
/**
* 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<string, unknown>,
): Record<string, unknown> {
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<string, unknown>,
): { schema: Record<string, unknown>; originalSchema: Record<string, unknown> | 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<string, unknown>, seen: Set<object>): Record<string, unknown> {
// 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<string, unknown>;
for (const key of Object.keys(defs)) {
if (isRecord(defs[key])) {
defs[key] = walkSchema(defs[key] as Record<string, unknown>, seen);
}
}
}
}
// Traverse items (array items)
if (isRecord(node.items)) {
node.items = walkSchema(node.items as Record<string, unknown>, 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<string, unknown>, seen);
}
}
return node;
}
function isRecord(v: unknown): v is Record<string, unknown> {
return typeof v === "object" && v !== null && !Array.isArray(v);
}