Spaces:
Paused
Paused
File size: 5,988 Bytes
d6c3bb0 d817d67 4a940a5 e25a730 d6c3bb0 4a940a5 d6c3bb0 d817d67 2def35e d817d67 d6c3bb0 d817d67 d6c3bb0 921d606 d6c3bb0 d817d67 d6c3bb0 4779e44 6220911 4779e44 e25a730 6220911 4779e44 6220911 4779e44 6220911 4779e44 6220911 4779e44 6220911 4779e44 6220911 4779e44 6220911 4779e44 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 | /**
* 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);
}
|