fix: OpenAI tool schema compatibility
This commit is contained in:
@@ -218,6 +218,13 @@ function normalizeToolParameters(tool: AnyAgentTool): AnyAgentTool {
|
||||
: undefined;
|
||||
if (!schema) return tool;
|
||||
|
||||
// Provider quirks:
|
||||
// - Gemini rejects several JSON Schema keywords, so we scrub those.
|
||||
// - OpenAI rejects function tool schemas unless the *top-level* is `type: "object"`.
|
||||
// (TypeBox root unions compile to `{ anyOf: [...] }` without `type`).
|
||||
//
|
||||
// Normalize once here so callers can always pass `tools` through unchanged.
|
||||
|
||||
// If schema already has type + properties (no top-level anyOf to merge),
|
||||
// still clean it for Gemini compatibility
|
||||
if (
|
||||
@@ -231,12 +238,33 @@ function normalizeToolParameters(tool: AnyAgentTool): AnyAgentTool {
|
||||
};
|
||||
}
|
||||
|
||||
if (!Array.isArray(schema.anyOf)) return tool;
|
||||
// Some tool schemas (esp. unions) may omit `type` at the top-level. If we see
|
||||
// object-ish fields, force `type: "object"` so OpenAI accepts the schema.
|
||||
if (
|
||||
!("type" in schema) &&
|
||||
(typeof schema.properties === "object" ||
|
||||
Array.isArray(schema.required)) &&
|
||||
!Array.isArray(schema.anyOf) &&
|
||||
!Array.isArray(schema.oneOf)
|
||||
) {
|
||||
return {
|
||||
...tool,
|
||||
parameters: cleanSchemaForGemini({ ...schema, type: "object" }),
|
||||
};
|
||||
}
|
||||
|
||||
const variantKey = Array.isArray(schema.anyOf)
|
||||
? "anyOf"
|
||||
: Array.isArray(schema.oneOf)
|
||||
? "oneOf"
|
||||
: null;
|
||||
if (!variantKey) return tool;
|
||||
const variants = schema[variantKey] as unknown[];
|
||||
const mergedProperties: Record<string, unknown> = {};
|
||||
const requiredCounts = new Map<string, number>();
|
||||
let objectVariants = 0;
|
||||
|
||||
for (const entry of schema.anyOf) {
|
||||
for (const entry of variants) {
|
||||
if (!entry || typeof entry !== "object") continue;
|
||||
const props = (entry as { properties?: unknown }).properties;
|
||||
if (!props || typeof props !== "object") continue;
|
||||
@@ -277,9 +305,16 @@ function normalizeToolParameters(tool: AnyAgentTool): AnyAgentTool {
|
||||
const nextSchema: Record<string, unknown> = { ...schema };
|
||||
return {
|
||||
...tool,
|
||||
// Flatten union schemas into a single object schema:
|
||||
// - Gemini doesn't allow top-level `type` together with `anyOf`.
|
||||
// - OpenAI rejects schemas without top-level `type: "object"`.
|
||||
// Merging properties preserves useful enums like `action` while keeping schemas portable.
|
||||
parameters: cleanSchemaForGemini({
|
||||
...nextSchema,
|
||||
type: nextSchema.type ?? "object",
|
||||
type: "object",
|
||||
...(typeof nextSchema.title === "string" ? { title: nextSchema.title } : {}),
|
||||
...(typeof nextSchema.description === "string"
|
||||
? { description: nextSchema.description }
|
||||
: {}),
|
||||
properties:
|
||||
Object.keys(mergedProperties).length > 0
|
||||
? mergedProperties
|
||||
@@ -518,5 +553,7 @@ export function createClawdbotCodingTools(options?: {
|
||||
const sandboxed = sandbox
|
||||
? filterToolsByPolicy(globallyFiltered, sandbox.tools)
|
||||
: globallyFiltered;
|
||||
// Always normalize tool JSON Schemas before handing them to pi-agent/pi-ai.
|
||||
// Without this, some providers (notably OpenAI) will reject root-level union schemas.
|
||||
return sandboxed.map(normalizeToolParameters);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user