blob: fd76669f65555ad95dc86aa188941ce151d2112e [file]
export interface OpenAIStrictFunctionProjection {
args: Record<string, unknown>;
discardedKeys: string[];
}
export function createOpenAIStrictObjectSchema(input: {
properties: Record<string, Record<string, unknown>>;
}): Record<string, unknown> {
return {
type: 'object',
properties: Object.fromEntries(
Object.entries(input.properties).map(([key, schema]) => [
key,
key === 'action' ? schema : nullableSchema(schema),
]),
),
required: Object.keys(input.properties),
additionalProperties: false,
};
}
export function projectOpenAIStrictFunctionArgs(input: {
value: unknown;
knownKeys: readonly string[];
allowedKeysByAction: Readonly<Record<string, readonly string[]>>;
}): OpenAIStrictFunctionProjection {
if (!input.value || typeof input.value !== 'object' || Array.isArray(input.value)) {
throw new Error('openai_strict_function_args_not_object');
}
const descriptors = Object.getOwnPropertyDescriptors(input.value);
for (const [key, descriptor] of Object.entries(descriptors)) {
if (descriptor.get || descriptor.set) {
throw new Error(`openai_strict_function_arg_not_data_property:${key}`);
}
}
const record = input.value as Record<string, unknown>;
const unknownKeys = Object.keys(record).filter((key) => !input.knownKeys.includes(key));
if (unknownKeys.length > 0) {
throw new Error(`openai_strict_function_unknown_keys:${unknownKeys.sort().join(',')}`);
}
const action = record.action;
if (typeof action !== 'string') {
throw new Error('openai_strict_function_missing_action');
}
const allowedKeys = input.allowedKeysByAction[action];
if (!allowedKeys) {
throw new Error(`openai_strict_function_unknown_action:${action}`);
}
const args = Object.fromEntries(
Object.entries(record).filter(([key, value]) => value !== null && allowedKeys.includes(key)),
);
const discardedKeys = Object.entries(record)
.filter(([key, value]) => value !== null && !allowedKeys.includes(key))
.map(([key]) => key)
.sort();
return { args, discardedKeys };
}
function nullableSchema(schema: Record<string, unknown>): Record<string, unknown> {
const type = schema.type;
if (typeof type === 'string') {
return { ...schema, type: [type, 'null'] };
}
if (Array.isArray(type)) {
return type.includes('null') ? schema : { ...schema, type: [...type, 'null'] };
}
return { anyOf: [schema, { type: 'null' }] };
}