mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-21 22:21:33 +00:00
fix: normalize MCP tool schemas missing properties field for OpenAI Responses API
Tools with no parameters produce { type: "object" } schemas without a
properties field. The OpenAI Responses API rejects these, silently
crashing entire sessions.
Add properties: {} injection in normalizeToolParameters() and
convertTools() to ensure all object-type schemas include a properties
field.
Closes #58246
This commit is contained in:
@@ -276,12 +276,21 @@ export function convertTools(tools: Context["tools"]): FunctionToolDefinition[]
|
||||
if (!tools || tools.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return tools.map((tool) => ({
|
||||
type: "function" as const,
|
||||
name: tool.name,
|
||||
description: typeof tool.description === "string" ? tool.description : undefined,
|
||||
parameters: (tool.parameters ?? {}) as Record<string, unknown>,
|
||||
}));
|
||||
return tools.map((tool) => {
|
||||
const params = (tool.parameters ?? {}) as Record<string, unknown>;
|
||||
// Ensure `type: "object"` schemas include `properties` — the OpenAI Responses
|
||||
// API rejects bare `{ type: "object" }` from MCP tools with no parameters.
|
||||
const normalizedParams =
|
||||
params.type === "object" && !("properties" in params)
|
||||
? { ...params, properties: {} }
|
||||
: params;
|
||||
return {
|
||||
type: "function" as const,
|
||||
name: tool.name,
|
||||
description: typeof tool.description === "string" ? tool.description : undefined,
|
||||
parameters: normalizedParams,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function planTurnInput(params: {
|
||||
|
||||
@@ -354,6 +354,35 @@ describe("convertTools", () => {
|
||||
const result = convertTools(tools as Parameters<typeof convertTools>[0]);
|
||||
expect(result[0]?.name).toBe("ping");
|
||||
});
|
||||
|
||||
it("injects properties:{} for type:object schemas missing properties (MCP no-param tools)", () => {
|
||||
const tools = [
|
||||
{ name: "list_regions", description: "List AWS regions", parameters: { type: "object" } },
|
||||
];
|
||||
const result = convertTools(tools as unknown as Parameters<typeof convertTools>[0]);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toMatchObject({
|
||||
type: "function",
|
||||
name: "list_regions",
|
||||
description: "List AWS regions",
|
||||
parameters: { type: "object", properties: {} },
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves existing properties on type:object schemas", () => {
|
||||
const tools = [
|
||||
{
|
||||
name: "exec",
|
||||
description: "Run a command",
|
||||
parameters: { type: "object", properties: { cmd: { type: "string" } } },
|
||||
},
|
||||
];
|
||||
const result = convertTools(tools as unknown as Parameters<typeof convertTools>[0]);
|
||||
expect(result[0]?.parameters).toEqual({
|
||||
type: "object",
|
||||
properties: { cmd: { type: "string" } },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -4,6 +4,55 @@ import { normalizeToolParameters } from "./pi-tools.schema.js";
|
||||
import type { AnyAgentTool } from "./pi-tools.types.js";
|
||||
|
||||
describe("normalizeToolParameters", () => {
|
||||
it("injects properties:{} for type:object schemas missing properties (MCP no-param tools)", () => {
|
||||
const tool: AnyAgentTool = {
|
||||
name: "list_regions",
|
||||
label: "list_regions",
|
||||
description: "List all AWS regions",
|
||||
parameters: { type: "object" },
|
||||
execute: vi.fn(),
|
||||
};
|
||||
|
||||
const normalized = normalizeToolParameters(tool);
|
||||
|
||||
const parameters = normalized.parameters as Record<string, unknown>;
|
||||
expect(parameters.type).toBe("object");
|
||||
expect(parameters.properties).toEqual({});
|
||||
});
|
||||
|
||||
it("preserves existing properties on type:object schemas", () => {
|
||||
const tool: AnyAgentTool = {
|
||||
name: "query",
|
||||
label: "query",
|
||||
description: "Run a query",
|
||||
parameters: { type: "object", properties: { q: { type: "string" } } },
|
||||
execute: vi.fn(),
|
||||
};
|
||||
|
||||
const normalized = normalizeToolParameters(tool);
|
||||
|
||||
const parameters = normalized.parameters as Record<string, unknown>;
|
||||
expect(parameters.type).toBe("object");
|
||||
expect(parameters.properties).toEqual({ q: { type: "string" } });
|
||||
});
|
||||
|
||||
it("injects properties:{} for type:object with only additionalProperties", () => {
|
||||
const tool: AnyAgentTool = {
|
||||
name: "passthrough",
|
||||
label: "passthrough",
|
||||
description: "Accept any input",
|
||||
parameters: { type: "object", additionalProperties: true },
|
||||
execute: vi.fn(),
|
||||
};
|
||||
|
||||
const normalized = normalizeToolParameters(tool);
|
||||
|
||||
const parameters = normalized.parameters as Record<string, unknown>;
|
||||
expect(parameters.type).toBe("object");
|
||||
expect(parameters.properties).toEqual({});
|
||||
expect(parameters.additionalProperties).toBe(true);
|
||||
});
|
||||
|
||||
it("strips compat-declared unsupported schema keywords without provider-specific branching", () => {
|
||||
const tool: AnyAgentTool = {
|
||||
name: "demo",
|
||||
|
||||
@@ -133,6 +133,22 @@ export function normalizeToolParameters(
|
||||
});
|
||||
}
|
||||
|
||||
// MCP tools with no parameters produce `{ type: "object" }` without `properties`.
|
||||
// The OpenAI function-calling API rejects bare `{ type: "object" }` schemas.
|
||||
// Inject an empty `properties` object — semantically identical in JSON Schema.
|
||||
if (
|
||||
"type" in schema &&
|
||||
!("properties" in schema) &&
|
||||
!Array.isArray(schema.anyOf) &&
|
||||
!Array.isArray(schema.oneOf)
|
||||
) {
|
||||
const schemaWithProperties = { ...schema, properties: {} };
|
||||
return preserveToolMeta({
|
||||
...tool,
|
||||
parameters: applyProviderCleaning(schemaWithProperties),
|
||||
});
|
||||
}
|
||||
|
||||
const variantKey = Array.isArray(schema.anyOf)
|
||||
? "anyOf"
|
||||
: Array.isArray(schema.oneOf)
|
||||
|
||||
Reference in New Issue
Block a user