Something went wrong. Try again.
fork of prime agent with changes i want but are not upstreamable probqbly (and some fixes)
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109/** * JSON Schema normalization that rewrites constant alternatives as `enum`. * * TypeBox expresses a closed set of values as `const` inside `anyOf`, for example * `Type.Union([Type.Literal("a"), Type.Literal("b")])`. Provider structured-output dialects do not * accept `const`: Google ignores it silently, which turns the union into "any string", and OpenAI * rejects the schema. `const` and a one-value `enum` mean the same thing, so rewriting keeps the * constraint on both. See also `StringEnum` in ./typebox-helpers.ts for authoring enums directly. */
/** * Keys whose values are instance data rather than nested schemas. The normalizer must not walk into * them, or a literal value that happens to contain a `const` key would be rewritten. */const SCHEMA_VALUE_KEYS = new Set(["const", "enum", "default", "examples"]);
/** Annotation keys that may sit next to `const` without making a node more than a constant. */const CONST_NODE_ANNOTATION_KEYS = new Set([ "title", "description", "$comment", "deprecated", "readOnly", "writeOnly", "default", "examples",]);
function isSchemaRecord(value: unknown): value is Record<string, unknown> { return typeof value === "object" && value !== null && !Array.isArray(value);}
interface ConstNode { value: unknown; type?: string;}
/** Reads a node that only pins a single value, e.g. TypeBox's `{ const: "open", type: "string" }`. */function asConstNode(node: unknown): ConstNode | undefined { if (!isSchemaRecord(node) || !("const" in node)) return undefined; for (const key of Object.keys(node)) { if (key === "const" || key === "type") continue; if (!CONST_NODE_ANNOTATION_KEYS.has(key)) return undefined; } return { value: node.const, type: typeof node.type === "string" ? node.type : undefined };}
function inferEnumType(values: unknown[]): string | undefined { if (values.every((value) => typeof value === "string")) return "string"; if (values.every((value) => typeof value === "number")) return "number"; if (values.every((value) => typeof value === "boolean")) return "boolean"; return undefined;}
function toEnumNode(node: Record<string, unknown>, keyword: string, values: unknown[], type?: string): unknown { const { [keyword]: _replaced, ...rest } = node; return { ...rest, ...(type !== undefined && { type }), enum: values };}
/** Collapses an `anyOf`/`oneOf` of pure constants into a single `enum`, preserving member order. */function collapseConstUnion(node: Record<string, unknown>): unknown { const keyword = Array.isArray(node.anyOf) ? "anyOf" : Array.isArray(node.oneOf) ? "oneOf" : undefined; if (!keyword) return undefined; const members = node[keyword] as unknown[]; if (members.length === 0) return undefined;
const constNodes: ConstNode[] = []; for (const member of members) { const constNode = asConstNode(member); if (!constNode) return undefined; constNodes.push(constNode); }
const declaredTypes = new Set(constNodes.map((entry) => entry.type).filter((type) => type !== undefined)); if (declaredTypes.size > 1) return undefined;
const values = constNodes.map((entry) => entry.value); return toEnumNode(node, keyword, values, [...declaredTypes][0] ?? inferEnumType(values));}
/** Collapses a standalone constant node into a one-value `enum`. */function collapseConstNode(node: Record<string, unknown>): unknown { const constNode = asConstNode(node); if (!constNode) return undefined; return toEnumNode(node, "const", [constNode.value], constNode.type ?? inferEnumType([constNode.value]));}
/** * Collapses constant alternatives in a JSON Schema into `enum`, recursively. * * An `anyOf`/`oneOf` whose members are all constants becomes `{ type, enum: [...] }` in member order, * and a standalone `const` becomes a one-value `enum`. Unions that are not pure constants pass through * unchanged. Pure and total: unexpected shapes are returned as they came in, and the input is never * mutated. */export function normalizeConstToEnum(schema: unknown): unknown { if (Array.isArray(schema)) return schema.map(normalizeConstToEnum); if (!isSchemaRecord(schema)) return schema;
const collapsed = collapseConstUnion(schema) ?? collapseConstNode(schema) ?? schema; if (!isSchemaRecord(collapsed)) return collapsed;
const normalized: Record<string, unknown> = {}; for (const [key, value] of Object.entries(collapsed)) { normalized[key] = SCHEMA_VALUE_KEYS.has(key) ? value : normalizeConstToEnum(value); } return normalized;}