mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-24 04:49:48 +00:00
258 lines
7.9 KiB
TypeScript
258 lines
7.9 KiB
TypeScript
import type { ModelCompatConfig } from "../config/types.models.js";
|
|
import { normalizeToolParameterSchema } from "./pi-tools-parameter-schema.js";
|
|
export { resolveOpenAIStrictToolSetting } from "./openai-strict-tool-setting.js";
|
|
|
|
type ToolSchemaCompatInput = {
|
|
unsupportedToolSchemaKeywords?: unknown;
|
|
omitEmptyArrayItems?: unknown;
|
|
};
|
|
|
|
type ToolWithParameters = {
|
|
name?: unknown;
|
|
parameters: unknown;
|
|
};
|
|
|
|
function resolveToolSchemaModelCompat(
|
|
compat: ToolSchemaCompatInput | null | undefined,
|
|
): ModelCompatConfig | undefined {
|
|
if (!compat) {
|
|
return undefined;
|
|
}
|
|
const unsupportedToolSchemaKeywords = Array.isArray(compat.unsupportedToolSchemaKeywords)
|
|
? compat.unsupportedToolSchemaKeywords.filter(
|
|
(keyword): keyword is string => typeof keyword === "string",
|
|
)
|
|
: [];
|
|
if (unsupportedToolSchemaKeywords.length === 0 && compat.omitEmptyArrayItems !== true) {
|
|
return undefined;
|
|
}
|
|
return {
|
|
...(unsupportedToolSchemaKeywords.length > 0 ? { unsupportedToolSchemaKeywords } : {}),
|
|
...(compat.omitEmptyArrayItems === true ? { omitEmptyArrayItems: true } : {}),
|
|
};
|
|
}
|
|
|
|
export function normalizeStrictOpenAIJsonSchema(
|
|
schema: unknown,
|
|
modelCompat?: ToolSchemaCompatInput | null,
|
|
): unknown {
|
|
return normalizeStrictOpenAIJsonSchemaRecursive(
|
|
normalizeToolParameterSchema(schema ?? {}, {
|
|
modelCompat: resolveToolSchemaModelCompat(modelCompat),
|
|
}),
|
|
0,
|
|
);
|
|
}
|
|
|
|
function normalizeStrictOpenAIJsonSchemaRecursive(schema: unknown, depth: number): unknown {
|
|
if (Array.isArray(schema)) {
|
|
let changed = false;
|
|
const normalized = schema.map((entry) => {
|
|
const next = normalizeStrictOpenAIJsonSchemaRecursive(entry, depth);
|
|
changed ||= next !== entry;
|
|
return next;
|
|
});
|
|
return changed ? normalized : schema;
|
|
}
|
|
if (!schema || typeof schema !== "object") {
|
|
return schema;
|
|
}
|
|
|
|
const record = schema as Record<string, unknown>;
|
|
let changed = false;
|
|
const normalized: Record<string, unknown> = {};
|
|
for (const [key, value] of Object.entries(record)) {
|
|
const next = normalizeStrictOpenAIJsonSchemaRecursive(
|
|
value,
|
|
key === "properties" ? depth : depth + 1,
|
|
);
|
|
normalized[key] = next;
|
|
changed ||= next !== value;
|
|
}
|
|
|
|
if (normalized.type === "object") {
|
|
const properties =
|
|
normalized.properties &&
|
|
typeof normalized.properties === "object" &&
|
|
!Array.isArray(normalized.properties)
|
|
? (normalized.properties as Record<string, unknown>)
|
|
: undefined;
|
|
if (properties && Object.keys(properties).length === 0 && !Array.isArray(normalized.required)) {
|
|
normalized.required = [];
|
|
changed = true;
|
|
}
|
|
if (depth === 0 && !("additionalProperties" in normalized)) {
|
|
normalized.additionalProperties = false;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
return changed ? normalized : schema;
|
|
}
|
|
|
|
export function normalizeOpenAIStrictToolParameters<T>(
|
|
schema: T,
|
|
strict: boolean,
|
|
modelCompat?: ToolSchemaCompatInput | null,
|
|
): T {
|
|
const toolSchemaCompat = resolveToolSchemaModelCompat(modelCompat);
|
|
if (!strict) {
|
|
return normalizeToolParameterSchema(schema ?? {}, { modelCompat: toolSchemaCompat }) as T;
|
|
}
|
|
return normalizeStrictOpenAIJsonSchema(schema, toolSchemaCompat) as T;
|
|
}
|
|
|
|
export function isStrictOpenAIJsonSchemaCompatible(schema: unknown): boolean {
|
|
return isStrictOpenAIJsonSchemaCompatibleRecursive(normalizeStrictOpenAIJsonSchema(schema));
|
|
}
|
|
|
|
type OpenAIStrictToolSchemaDiagnostic = {
|
|
toolIndex: number;
|
|
toolName?: string;
|
|
violations: string[];
|
|
};
|
|
|
|
export function findOpenAIStrictToolSchemaDiagnostics(
|
|
tools: readonly ToolWithParameters[],
|
|
): OpenAIStrictToolSchemaDiagnostic[] {
|
|
return tools.flatMap((tool, toolIndex) => {
|
|
const violations = findStrictOpenAIJsonSchemaViolations(
|
|
normalizeStrictOpenAIJsonSchema(tool.parameters),
|
|
`${typeof tool.name === "string" && tool.name ? tool.name : `tool[${toolIndex}]`}.parameters`,
|
|
);
|
|
if (violations.length === 0) {
|
|
return [];
|
|
}
|
|
return [
|
|
{
|
|
toolIndex,
|
|
...(typeof tool.name === "string" && tool.name ? { toolName: tool.name } : {}),
|
|
violations,
|
|
},
|
|
];
|
|
});
|
|
}
|
|
|
|
function isStrictOpenAIJsonSchemaCompatibleRecursive(schema: unknown): boolean {
|
|
if (Array.isArray(schema)) {
|
|
return schema.every((entry) => isStrictOpenAIJsonSchemaCompatibleRecursive(entry));
|
|
}
|
|
if (!schema || typeof schema !== "object") {
|
|
return true;
|
|
}
|
|
|
|
const record = schema as Record<string, unknown>;
|
|
if ("anyOf" in record || "oneOf" in record || "allOf" in record) {
|
|
return false;
|
|
}
|
|
if (Array.isArray(record.type)) {
|
|
return false;
|
|
}
|
|
if (record.type === "object" && record.additionalProperties !== false) {
|
|
return false;
|
|
}
|
|
if (record.type === "object") {
|
|
const properties =
|
|
record.properties &&
|
|
typeof record.properties === "object" &&
|
|
!Array.isArray(record.properties)
|
|
? (record.properties as Record<string, unknown>)
|
|
: {};
|
|
const required = Array.isArray(record.required)
|
|
? record.required.filter((entry): entry is string => typeof entry === "string")
|
|
: undefined;
|
|
if (!required) {
|
|
return false;
|
|
}
|
|
const requiredSet = new Set(required);
|
|
if (Object.keys(properties).some((key) => !requiredSet.has(key))) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return Object.entries(record).every(([key, entry]) => {
|
|
if (key === "properties" && entry && typeof entry === "object" && !Array.isArray(entry)) {
|
|
return Object.values(entry as Record<string, unknown>).every((value) =>
|
|
isStrictOpenAIJsonSchemaCompatibleRecursive(value),
|
|
);
|
|
}
|
|
return isStrictOpenAIJsonSchemaCompatibleRecursive(entry);
|
|
});
|
|
}
|
|
|
|
function findStrictOpenAIJsonSchemaViolations(schema: unknown, path: string): string[] {
|
|
if (Array.isArray(schema)) {
|
|
return schema.flatMap((entry, index) =>
|
|
findStrictOpenAIJsonSchemaViolations(entry, `${path}[${index}]`),
|
|
);
|
|
}
|
|
if (!schema || typeof schema !== "object") {
|
|
return [];
|
|
}
|
|
|
|
const record = schema as Record<string, unknown>;
|
|
const violations: string[] = [];
|
|
for (const key of ["anyOf", "oneOf", "allOf"] as const) {
|
|
if (key in record) {
|
|
violations.push(`${path}.${key}`);
|
|
}
|
|
}
|
|
if (Array.isArray(record.type)) {
|
|
violations.push(`${path}.type`);
|
|
}
|
|
if (record.type === "object") {
|
|
if (record.additionalProperties !== false) {
|
|
violations.push(`${path}.additionalProperties`);
|
|
}
|
|
const properties =
|
|
record.properties &&
|
|
typeof record.properties === "object" &&
|
|
!Array.isArray(record.properties)
|
|
? (record.properties as Record<string, unknown>)
|
|
: {};
|
|
const required = Array.isArray(record.required)
|
|
? record.required.filter((entry): entry is string => typeof entry === "string")
|
|
: undefined;
|
|
if (!required) {
|
|
violations.push(`${path}.required`);
|
|
} else {
|
|
const requiredSet = new Set(required);
|
|
for (const key of Object.keys(properties)) {
|
|
if (!requiredSet.has(key)) {
|
|
violations.push(`${path}.required.${key}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (
|
|
record.properties &&
|
|
typeof record.properties === "object" &&
|
|
!Array.isArray(record.properties)
|
|
) {
|
|
for (const [key, value] of Object.entries(record.properties)) {
|
|
violations.push(...findStrictOpenAIJsonSchemaViolations(value, `${path}.properties.${key}`));
|
|
}
|
|
}
|
|
for (const [key, value] of Object.entries(record)) {
|
|
if (key === "properties") {
|
|
continue;
|
|
}
|
|
if (value && typeof value === "object") {
|
|
violations.push(...findStrictOpenAIJsonSchemaViolations(value, `${path}.${key}`));
|
|
}
|
|
}
|
|
|
|
return violations;
|
|
}
|
|
|
|
export function resolveOpenAIStrictToolFlagForInventory(
|
|
tools: readonly ToolWithParameters[],
|
|
strict: boolean | null | undefined,
|
|
): boolean | undefined {
|
|
if (strict !== true) {
|
|
return strict === false ? false : undefined;
|
|
}
|
|
return tools.every((tool) => isStrictOpenAIJsonSchemaCompatible(tool.parameters));
|
|
}
|