@samchon/openapi
Version:
Universal OpenAPI to LLM function calling schemas. Transform any Swagger/OpenAPI document into type-safe schemas for OpenAI, Claude, Qwen, and more.
531 lines (519 loc) • 23.7 kB
JavaScript
import { LlmTypeChecker } from "../utils/LlmTypeChecker.mjs";
import { NamingConvention } from "../utils/NamingConvention.mjs";
import { OpenApiConstraintShifter } from "../utils/OpenApiConstraintShifter.mjs";
import { OpenApiTypeChecker } from "../utils/OpenApiTypeChecker.mjs";
import { OpenApiValidator } from "../utils/OpenApiValidator.mjs";
import { JsonDescriptionUtil } from "../utils/internal/JsonDescriptionUtil.mjs";
import { LlmDescriptionInverter } from "./LlmDescriptionInverter.mjs";
import { LlmParametersFinder } from "./LlmParametersComposer.mjs";
var LlmSchemaComposer;
(function(LlmSchemaComposer) {
LlmSchemaComposer.parameters = props => {
const config = LlmSchemaComposer.getConfig(props.config);
const entity = LlmParametersFinder.parameters({
...props,
method: "LlmSchemaComposer.parameters"
});
if (entity.success === false) return entity;
const $defs = {};
const result = transform({
...props,
config,
$defs,
schema: entity.value
});
if (result.success === false) return result;
return {
success: true,
value: {
...result.value,
additionalProperties: false,
$defs,
description: OpenApiTypeChecker.isReference(props.schema) ? JsonDescriptionUtil.cascade({
prefix: "#/components/schemas/",
components: props.components,
schema: {
...props.schema,
description: result.value.description
},
escape: true
}) : result.value.description
}
};
};
LlmSchemaComposer.schema = props => transform({
config: LlmSchemaComposer.getConfig(props.config),
components: props.components,
$defs: props.$defs,
schema: props.schema,
accessor: props.accessor,
refAccessor: props.refAccessor
});
const transform = props => {
const union = [];
const attribute = {
title: props.schema.title,
description: props.schema.description,
deprecated: props.schema.deprecated,
readOnly: props.schema.readOnly,
writeOnly: props.schema.writeOnly,
example: props.schema.example,
examples: props.schema.examples,
...Object.fromEntries(Object.entries(props.schema).filter(([key, value]) => key.startsWith("x-") && value !== undefined))
};
const reasons = [];
OpenApiTypeChecker.visit({
closure: (next, accessor) => {
if (props.config.strict === true) {
reasons.push(...validateStrict(next, accessor));
}
if (OpenApiTypeChecker.isTuple(next)) reasons.push({
accessor,
schema: next,
message: `LLM does not allow tuple type.`
}); else if (OpenApiTypeChecker.isReference(next)) {
const key = next.$ref.split("#/components/schemas/")[1];
if (props.components.schemas?.[key] === undefined) reasons.push({
schema: next,
accessor,
message: `unable to find reference type ${JSON.stringify(key)}.`
});
}
},
components: props.components,
schema: props.schema,
accessor: props.accessor,
refAccessor: props.refAccessor
});
if (reasons.length > 0) return {
success: false,
error: {
method: "LlmSchemaComposer.schema",
message: "Failed to compose LLM schema",
reasons
}
};
const visitConstant = input => {
const insert = value => {
const matched = union.find(u => u?.type === typeof value);
if (matched !== undefined) {
matched.enum ?? (matched.enum = []);
matched.enum.push(value);
} else union.push({
type: typeof value,
enum: [ value ]
});
};
if (OpenApiTypeChecker.isConstant(input)) insert(input.const); else if (OpenApiTypeChecker.isOneOf(input)) input.oneOf.forEach(visitConstant);
};
const visit = (input, accessor) => {
if (OpenApiTypeChecker.isOneOf(input)) {
input.oneOf.forEach((s, i) => visit(s, `${accessor}.oneOf[${i}]`));
} else if (OpenApiTypeChecker.isReference(input)) {
const key = input.$ref.split("#/components/schemas/")[1];
const target = props.components.schemas?.[key];
if (target === undefined) return; else if (props.config.reference === true || OpenApiTypeChecker.isRecursiveReference({
components: props.components,
schema: input
})) {
const out = () => {
union.push({
...input,
$ref: `#/$defs/${key}`
});
};
if (props.$defs[key] !== undefined) return out();
props.$defs[key] = {};
const converted = transform({
config: props.config,
components: props.components,
$defs: props.$defs,
schema: target,
refAccessor: props.refAccessor,
accessor: `${props.refAccessor ?? "$def"}[${JSON.stringify(key)}]`
});
if (converted.success === false) return;
props.$defs[key] = converted.value;
return out();
} else {
const length = union.length;
visit(target, accessor);
visitConstant(target);
if (length === union.length - 1) union[union.length - 1] = {
...union[union.length - 1],
description: JsonDescriptionUtil.cascade({
prefix: "#/components/schemas/",
components: props.components,
schema: input,
escape: true
})
}; else attribute.description = JsonDescriptionUtil.cascade({
prefix: "#/components/schemas/",
components: props.components,
schema: input,
escape: true
});
}
} else if (OpenApiTypeChecker.isObject(input)) {
const properties = Object.fromEntries(Object.entries(input.properties ?? {}).map(([key, value]) => {
const converted = transform({
config: props.config,
components: props.components,
$defs: props.$defs,
schema: value,
refAccessor: props.refAccessor,
accessor: `${props.accessor ?? "$input.schema"}.properties[${JSON.stringify(key)}]`
});
if (converted.success === false) {
reasons.push(...converted.error.reasons);
return [ key, null ];
}
return [ key, converted.value ];
}).filter(([, value]) => value !== null));
if (Object.values(properties).some(v => v === null)) return;
const additionalProperties = (() => {
if (typeof input.additionalProperties === "object" && input.additionalProperties !== null) {
const converted = transform({
config: props.config,
components: props.components,
$defs: props.$defs,
schema: input.additionalProperties,
refAccessor: props.refAccessor,
accessor: `${accessor}.additionalProperties`
});
if (converted.success === false) {
reasons.push(...converted.error.reasons);
return null;
}
return converted.value;
}
return props.config.strict === true ? false : input.additionalProperties;
})();
if (additionalProperties === null) return;
union.push({
...input,
properties,
additionalProperties,
required: input.required ?? [],
description: props.config.strict === true ? JsonDescriptionUtil.take(input) : input.description
});
} else if (OpenApiTypeChecker.isArray(input)) {
const items = transform({
config: props.config,
components: props.components,
$defs: props.$defs,
schema: input.items,
refAccessor: props.refAccessor,
accessor: `${accessor}.items`
});
if (items.success === false) {
reasons.push(...items.error.reasons);
return;
}
union.push(props.config.strict === true ? OpenApiConstraintShifter.shiftArray({
...input,
items: items.value
}) : {
...input,
items: items.value
});
} else if (OpenApiTypeChecker.isString(input)) union.push(props.config.strict === true ? OpenApiConstraintShifter.shiftString({
...input
}) : input); else if (OpenApiTypeChecker.isNumber(input) || OpenApiTypeChecker.isInteger(input)) union.push(props.config.strict === true ? OpenApiConstraintShifter.shiftNumeric({
...input
}) : input); else if (OpenApiTypeChecker.isTuple(input)) return; else if (OpenApiTypeChecker.isConstant(input) === false) union.push({
...input
});
};
visitConstant(props.schema);
visit(props.schema, props.accessor ?? "$input.schema");
if (reasons.length > 0) return {
success: false,
error: {
method: "LlmSchemaComposer.schema",
message: "Failed to compose LLM schema",
reasons
}
}; else if (union.length === 0) return {
success: true,
value: {
...attribute,
type: undefined
}
}; else if (union.length === 1) return {
success: true,
value: {
...attribute,
...union[0],
description: props.config.strict === true && LlmTypeChecker.isReference(union[0]) ? undefined : union[0].description ?? attribute.description
}
};
return {
success: true,
value: {
...attribute,
anyOf: union.map(u => ({
...u,
description: props.config.strict === true && LlmTypeChecker.isReference(u) ? undefined : u.description
})),
"x-discriminator": OpenApiTypeChecker.isOneOf(props.schema) && props.schema.discriminator !== undefined && props.schema.oneOf.length === union.length && union.every(e => LlmTypeChecker.isReference(e) || LlmTypeChecker.isNull(e)) ? {
propertyName: props.schema.discriminator.propertyName,
mapping: props.schema.discriminator.mapping !== undefined ? Object.fromEntries(Object.entries(props.schema.discriminator.mapping).map(([key, value]) => [ key, `#/$defs/${value.split("/").at(-1)}` ])) : undefined
} : undefined
}
};
};
LlmSchemaComposer.separate = props => {
const convention = props.convention ?? ((key, type) => `${key}.${NamingConvention.capitalize(type)}`);
const [llm, human] = separateObject({
predicate: props.predicate,
convention,
$defs: props.parameters.$defs,
schema: props.parameters
});
if (llm === null || human === null) return {
llm: llm ?? {
type: "object",
properties: {},
required: [],
additionalProperties: false,
$defs: {}
},
human
};
const output = {
llm: {
...llm,
$defs: Object.fromEntries(Object.entries(props.parameters.$defs).filter(([key]) => key.endsWith(".Llm"))),
additionalProperties: false
},
human: {
...human,
$defs: Object.fromEntries(Object.entries(props.parameters.$defs).filter(([key]) => key.endsWith(".Human"))),
additionalProperties: false
}
};
for (const key of Object.keys(props.parameters.$defs)) if (key.endsWith(".Llm") === false && key.endsWith(".Human") === false) delete props.parameters.$defs[key];
if (Object.keys(output.llm.properties).length !== 0) {
const components = {};
output.validate = OpenApiValidator.create({
components,
schema: LlmSchemaComposer.invert({
components,
schema: output.llm,
$defs: output.llm.$defs
}),
required: true,
equals: props.equals
});
}
return output;
};
const separateStation = props => {
if (props.predicate(props.schema) === true) return [ null, props.schema ]; else if (LlmTypeChecker.isUnknown(props.schema) || LlmTypeChecker.isAnyOf(props.schema)) return [ props.schema, null ]; else if (LlmTypeChecker.isObject(props.schema)) return separateObject({
predicate: props.predicate,
convention: props.convention,
$defs: props.$defs,
schema: props.schema
}); else if (LlmTypeChecker.isArray(props.schema)) return separateArray({
predicate: props.predicate,
convention: props.convention,
$defs: props.$defs,
schema: props.schema
}); else if (LlmTypeChecker.isReference(props.schema)) return separateReference({
predicate: props.predicate,
convention: props.convention,
$defs: props.$defs,
schema: props.schema
});
return [ props.schema, null ];
};
const separateArray = props => {
const [x, y] = separateStation({
predicate: props.predicate,
convention: props.convention,
$defs: props.$defs,
schema: props.schema.items
});
return [ x !== null ? {
...props.schema,
items: x
} : null, y !== null ? {
...props.schema,
items: y
} : null ];
};
const separateObject = props => {
if (Object.keys(props.schema.properties ?? {}).length === 0 && !!props.schema.additionalProperties === false) return [ props.schema, null ];
const llm = {
...props.schema,
properties: {},
additionalProperties: props.schema.additionalProperties
};
const human = {
...props.schema,
properties: {}
};
for (const [key, value] of Object.entries(props.schema.properties ?? {})) {
const [x, y] = separateStation({
predicate: props.predicate,
convention: props.convention,
$defs: props.$defs,
schema: value
});
if (x !== null) llm.properties[key] = x;
if (y !== null) human.properties[key] = y;
}
if (typeof props.schema.additionalProperties === "object" && props.schema.additionalProperties !== null) {
const [dx, dy] = separateStation({
predicate: props.predicate,
convention: props.convention,
$defs: props.$defs,
schema: props.schema.additionalProperties
});
llm.additionalProperties = dx ?? false;
human.additionalProperties = dy ?? false;
}
return [ !!Object.keys(llm.properties).length || !!llm.additionalProperties ? shrinkRequired(llm) : null, !!Object.keys(human.properties).length || human.additionalProperties ? shrinkRequired(human) : null ];
};
const separateReference = props => {
const key = props.schema.$ref.split("#/$defs/")[1];
const humanKey = props.convention(key, "human");
const llmKey = props.convention(key, "llm");
if (props.$defs?.[humanKey] || props.$defs?.[llmKey]) return [ props.$defs?.[llmKey] ? {
...props.schema,
$ref: `#/$defs/${llmKey}`
} : null, props.$defs?.[humanKey] ? {
...props.schema,
$ref: `#/$defs/${humanKey}`
} : null ];
props.$defs[llmKey] = {};
props.$defs[humanKey] = {};
const schema = props.$defs?.[key];
const [llm, human] = separateStation({
predicate: props.predicate,
convention: props.convention,
$defs: props.$defs,
schema
});
if (llm !== null) Object.assign(props.$defs[llmKey], llm);
if (human !== null) Object.assign(props.$defs[humanKey], human);
if (llm === null || human === null) {
delete props.$defs[llmKey];
delete props.$defs[humanKey];
return llm === null ? [ null, props.schema ] : [ props.schema, null ];
}
return [ llm !== null ? {
...props.schema,
$ref: `#/$defs/${llmKey}`
} : null, human !== null ? {
...props.schema,
$ref: `#/$defs/${humanKey}`
} : null ];
};
const shrinkRequired = s => {
s.required = s.required.filter(key => s.properties?.[key] !== undefined);
return s;
};
LlmSchemaComposer.invert = props => {
const union = [];
const attribute = {
title: props.schema.title,
description: props.schema.description,
deprecated: props.schema.deprecated,
readOnly: props.schema.readOnly,
writeOnly: props.schema.writeOnly,
example: props.schema.example,
examples: props.schema.examples,
...Object.fromEntries(Object.entries(props.schema).filter(([key, value]) => key.startsWith("x-") && value !== undefined))
};
const next = schema => LlmSchemaComposer.invert({
components: props.components,
$defs: props.$defs,
schema
});
const visit = schema => {
var _a;
if (LlmTypeChecker.isArray(schema)) union.push({
...schema,
...LlmDescriptionInverter.array(schema.description),
items: next(schema.items)
}); else if (LlmTypeChecker.isObject(schema)) union.push({
...schema,
properties: Object.fromEntries(Object.entries(schema.properties).map(([key, value]) => [ key, next(value) ])),
additionalProperties: typeof schema.additionalProperties === "object" && schema.additionalProperties !== null ? next(schema.additionalProperties) : schema.additionalProperties
}); else if (LlmTypeChecker.isAnyOf(schema)) schema.anyOf.forEach(visit); else if (LlmTypeChecker.isReference(schema)) {
const key = schema.$ref.split("#/$defs/")[1];
if (props.components.schemas?.[key] === undefined) {
(_a = props.components).schemas ?? (_a.schemas = {});
props.components.schemas[key] = {};
props.components.schemas[key] = next(props.$defs[key] ?? {});
}
union.push({
...schema,
$ref: `#/components/schemas/${key}`
});
} else if (LlmTypeChecker.isBoolean(schema)) if (!!schema.enum?.length) schema.enum.forEach(v => union.push({
const: v
})); else union.push(schema); else if (LlmTypeChecker.isInteger(schema) || LlmTypeChecker.isNumber(schema)) if (!!schema.enum?.length) schema.enum.forEach(v => union.push({
const: v
})); else union.push({
...schema,
...LlmDescriptionInverter.numeric(schema.description),
...{
enum: undefined
}
}); else if (LlmTypeChecker.isString(schema)) if (!!schema.enum?.length) schema.enum.forEach(v => union.push({
const: v
})); else union.push({
...schema,
...LlmDescriptionInverter.string(schema.description),
...{
enum: undefined
}
}); else union.push({
...schema
});
};
visit(props.schema);
return {
...attribute,
...union.length === 0 ? {
type: undefined
} : union.length === 1 ? {
...union[0]
} : {
oneOf: union.map(u => ({
...u,
nullable: undefined
})),
discriminator: LlmTypeChecker.isAnyOf(props.schema) && props.schema["x-discriminator"] !== undefined ? {
propertyName: props.schema["x-discriminator"].propertyName,
mapping: props.schema["x-discriminator"].mapping !== undefined ? Object.fromEntries(Object.entries(props.schema["x-discriminator"].mapping).map(([key, value]) => [ key, `#/components/schemas/${value.split("/").at(-1)}` ])) : undefined
} : undefined
}
};
};
LlmSchemaComposer.getConfig = config => ({
reference: config?.reference ?? true,
strict: config?.strict ?? false
});
})(LlmSchemaComposer || (LlmSchemaComposer = {}));
const validateStrict = (schema, accessor) => {
const reasons = [];
if (OpenApiTypeChecker.isObject(schema)) {
if (!!schema.additionalProperties) reasons.push({
schema,
accessor: `${accessor}.additionalProperties`,
message: "LLM does not allow additionalProperties in strict mode, the dynamic key typed object."
});
for (const key of Object.keys(schema.properties ?? {})) if (schema.required?.includes(key) === false) reasons.push({
schema,
accessor: `${accessor}.properties.${key}`,
message: "LLM does not allow optional properties in strict mode."
});
}
return reasons;
};
export { LlmSchemaComposer };
//# sourceMappingURL=LlmSchemaComposer.mjs.map