mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-29 10:02:04 +00:00
fix: harden OpenAI tool replay compatibility
This commit is contained in:
@@ -101,6 +101,39 @@ describe("buildProviderToolCompatFamilyHooks", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves nested empty property schemas and object annotations", () => {
|
||||
const hooks = buildProviderToolCompatFamilyHooks("openai");
|
||||
const parameters = {
|
||||
type: "object",
|
||||
properties: {
|
||||
payload: {},
|
||||
mode: {
|
||||
type: "string",
|
||||
default: {},
|
||||
const: {},
|
||||
},
|
||||
},
|
||||
required: ["payload", "mode"],
|
||||
additionalProperties: false,
|
||||
};
|
||||
const tools = [{ name: "demo", description: "", parameters }] as never;
|
||||
|
||||
const normalized = hooks.normalizeToolSchemas({
|
||||
provider: "openai",
|
||||
modelId: "gpt-5.4",
|
||||
modelApi: "openai-responses",
|
||||
model: {
|
||||
provider: "openai",
|
||||
api: "openai-responses",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
id: "gpt-5.4",
|
||||
} as never,
|
||||
tools,
|
||||
});
|
||||
|
||||
expect(normalized[0]?.parameters).toEqual(parameters);
|
||||
});
|
||||
|
||||
it("does not tighten permissive object schemas just to satisfy strict mode", () => {
|
||||
const hooks = buildProviderToolCompatFamilyHooks("openai");
|
||||
const permissiveParameters = {
|
||||
|
||||
@@ -183,7 +183,7 @@ export function normalizeOpenAIToolSchemas(
|
||||
}
|
||||
|
||||
function normalizeOpenAIStrictCompatSchema(schema: unknown): unknown {
|
||||
return normalizeOpenAIStrictCompatSchemaRecursive(schema);
|
||||
return normalizeOpenAIStrictCompatSchemaRecursive(schema, { promoteEmptyObject: true });
|
||||
}
|
||||
|
||||
function shouldApplyOpenAIToolCompat(ctx: ProviderNormalizeToolSchemasContext): boolean {
|
||||
@@ -217,11 +217,62 @@ function isOpenAICodexBaseUrl(baseUrl: string): boolean {
|
||||
return /^https:\/\/chatgpt\.com\/backend-api(?:\/|$)/i.test(baseUrl);
|
||||
}
|
||||
|
||||
function normalizeOpenAIStrictCompatSchemaRecursive(schema: unknown): unknown {
|
||||
type NormalizeOpenAIStrictCompatOptions = {
|
||||
promoteEmptyObject: boolean;
|
||||
};
|
||||
|
||||
const OPENAI_STRICT_COMPAT_SCHEMA_MAP_KEYS = new Set([
|
||||
"$defs",
|
||||
"definitions",
|
||||
"dependentSchemas",
|
||||
"patternProperties",
|
||||
"properties",
|
||||
]);
|
||||
|
||||
const OPENAI_STRICT_COMPAT_SCHEMA_NESTED_KEYS = new Set([
|
||||
"additionalProperties",
|
||||
"allOf",
|
||||
"anyOf",
|
||||
"contains",
|
||||
"else",
|
||||
"if",
|
||||
"items",
|
||||
"not",
|
||||
"oneOf",
|
||||
"prefixItems",
|
||||
"propertyNames",
|
||||
"then",
|
||||
"unevaluatedItems",
|
||||
"unevaluatedProperties",
|
||||
]);
|
||||
|
||||
function normalizeOpenAIStrictCompatSchemaMap(schema: unknown): unknown {
|
||||
if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
|
||||
return schema;
|
||||
}
|
||||
|
||||
let changed = false;
|
||||
const normalized: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(schema as Record<string, unknown>)) {
|
||||
const next = normalizeOpenAIStrictCompatSchemaRecursive(value, {
|
||||
promoteEmptyObject: false,
|
||||
});
|
||||
normalized[key] = next;
|
||||
changed ||= next !== value;
|
||||
}
|
||||
return changed ? normalized : schema;
|
||||
}
|
||||
|
||||
function normalizeOpenAIStrictCompatSchemaRecursive(
|
||||
schema: unknown,
|
||||
options: NormalizeOpenAIStrictCompatOptions,
|
||||
): unknown {
|
||||
if (Array.isArray(schema)) {
|
||||
let changed = false;
|
||||
const normalized = schema.map((entry) => {
|
||||
const next = normalizeOpenAIStrictCompatSchemaRecursive(entry);
|
||||
const next = normalizeOpenAIStrictCompatSchemaRecursive(entry, {
|
||||
promoteEmptyObject: false,
|
||||
});
|
||||
changed ||= next !== entry;
|
||||
return next;
|
||||
});
|
||||
@@ -235,22 +286,21 @@ function normalizeOpenAIStrictCompatSchemaRecursive(schema: unknown): unknown {
|
||||
let changed = false;
|
||||
const normalized: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(record)) {
|
||||
const next =
|
||||
key === "properties" && value && typeof value === "object" && !Array.isArray(value)
|
||||
? Object.fromEntries(
|
||||
Object.entries(value as Record<string, unknown>).map(
|
||||
([propertyName, propertyValue]) => [
|
||||
propertyName,
|
||||
normalizeOpenAIStrictCompatSchemaRecursive(propertyValue),
|
||||
],
|
||||
),
|
||||
)
|
||||
: normalizeOpenAIStrictCompatSchemaRecursive(value);
|
||||
const next = OPENAI_STRICT_COMPAT_SCHEMA_MAP_KEYS.has(key)
|
||||
? normalizeOpenAIStrictCompatSchemaMap(value)
|
||||
: OPENAI_STRICT_COMPAT_SCHEMA_NESTED_KEYS.has(key)
|
||||
? normalizeOpenAIStrictCompatSchemaRecursive(value, {
|
||||
promoteEmptyObject: false,
|
||||
})
|
||||
: value;
|
||||
normalized[key] = next;
|
||||
changed ||= next !== value;
|
||||
}
|
||||
|
||||
if (Object.keys(normalized).length === 0) {
|
||||
if (!options.promoteEmptyObject) {
|
||||
return schema;
|
||||
}
|
||||
return {
|
||||
type: "object",
|
||||
properties: {},
|
||||
|
||||
Reference in New Issue
Block a user