fix(agents): remove unsupported JSON Schema keywords for Cloud Code Assist API
Cloud Code Assist API requires strict JSON Schema draft 2020-12 compliance and rejects keywords like patternProperties, additionalProperties, $schema, $id, $ref, $defs, and definitions. This extends cleanSchemaForGemini to: - Remove all unsupported keywords from tool schemas - Add oneOf literal flattening (matching existing anyOf behavior) - Add test to verify no unsupported keywords remain in tool schemas
This commit is contained in:
@@ -331,4 +331,52 @@ describe("createClawdbotCodingTools", () => {
|
|||||||
expect(tools.some((tool) => tool.name === "Bash")).toBe(true);
|
expect(tools.some((tool) => tool.name === "Bash")).toBe(true);
|
||||||
expect(tools.some((tool) => tool.name === "browser")).toBe(false);
|
expect(tools.some((tool) => tool.name === "browser")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("removes unsupported JSON Schema keywords for Cloud Code Assist API compatibility", () => {
|
||||||
|
const tools = createClawdbotCodingTools();
|
||||||
|
|
||||||
|
// Helper to recursively check schema for unsupported keywords
|
||||||
|
const unsupportedKeywords = new Set([
|
||||||
|
"patternProperties",
|
||||||
|
"additionalProperties",
|
||||||
|
"$schema",
|
||||||
|
"$id",
|
||||||
|
"$ref",
|
||||||
|
"$defs",
|
||||||
|
"definitions",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const findUnsupportedKeywords = (
|
||||||
|
schema: unknown,
|
||||||
|
path: string,
|
||||||
|
): string[] => {
|
||||||
|
const found: string[] = [];
|
||||||
|
if (!schema || typeof schema !== "object") return found;
|
||||||
|
if (Array.isArray(schema)) {
|
||||||
|
schema.forEach((item, i) => {
|
||||||
|
found.push(...findUnsupportedKeywords(item, `${path}[${i}]`));
|
||||||
|
});
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
for (const [key, value] of Object.entries(
|
||||||
|
schema as Record<string, unknown>,
|
||||||
|
)) {
|
||||||
|
if (unsupportedKeywords.has(key)) {
|
||||||
|
found.push(`${path}.${key}`);
|
||||||
|
}
|
||||||
|
if (value && typeof value === "object") {
|
||||||
|
found.push(...findUnsupportedKeywords(value, `${path}.${key}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const tool of tools) {
|
||||||
|
const violations = findUnsupportedKeywords(
|
||||||
|
tool.parameters,
|
||||||
|
`${tool.name}.parameters`,
|
||||||
|
);
|
||||||
|
expect(violations).toEqual([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -195,12 +195,24 @@ function tryFlattenLiteralAnyOf(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keywords that Cloud Code Assist API rejects (not compliant with their JSON Schema subset)
|
||||||
|
const UNSUPPORTED_SCHEMA_KEYWORDS = new Set([
|
||||||
|
"patternProperties",
|
||||||
|
"additionalProperties",
|
||||||
|
"$schema",
|
||||||
|
"$id",
|
||||||
|
"$ref",
|
||||||
|
"$defs",
|
||||||
|
"definitions",
|
||||||
|
]);
|
||||||
|
|
||||||
function cleanSchemaForGemini(schema: unknown): unknown {
|
function cleanSchemaForGemini(schema: unknown): unknown {
|
||||||
if (!schema || typeof schema !== "object") return schema;
|
if (!schema || typeof schema !== "object") return schema;
|
||||||
if (Array.isArray(schema)) return schema.map(cleanSchemaForGemini);
|
if (Array.isArray(schema)) return schema.map(cleanSchemaForGemini);
|
||||||
|
|
||||||
const obj = schema as Record<string, unknown>;
|
const obj = schema as Record<string, unknown>;
|
||||||
const hasAnyOf = "anyOf" in obj && Array.isArray(obj.anyOf);
|
const hasAnyOf = "anyOf" in obj && Array.isArray(obj.anyOf);
|
||||||
|
const hasOneOf = "oneOf" in obj && Array.isArray(obj.oneOf);
|
||||||
|
|
||||||
// Try to flatten anyOf of literals to a single enum BEFORE processing
|
// Try to flatten anyOf of literals to a single enum BEFORE processing
|
||||||
// This handles Type.Union([Type.Literal("a"), Type.Literal("b")]) patterns
|
// This handles Type.Union([Type.Literal("a"), Type.Literal("b")]) patterns
|
||||||
@@ -221,14 +233,28 @@ function cleanSchemaForGemini(schema: unknown): unknown {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try to flatten oneOf of literals similarly
|
||||||
|
if (hasOneOf) {
|
||||||
|
const flattened = tryFlattenLiteralAnyOf(obj.oneOf as unknown[]);
|
||||||
|
if (flattened) {
|
||||||
|
const result: Record<string, unknown> = {
|
||||||
|
type: flattened.type,
|
||||||
|
enum: flattened.enum,
|
||||||
|
};
|
||||||
|
for (const key of ["description", "title", "default", "examples"]) {
|
||||||
|
if (key in obj && obj[key] !== undefined) {
|
||||||
|
result[key] = obj[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const cleaned: Record<string, unknown> = {};
|
const cleaned: Record<string, unknown> = {};
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(obj)) {
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
// Skip unsupported schema features for Gemini:
|
// Skip keywords that Cloud Code Assist API doesn't support
|
||||||
// - patternProperties: not in OpenAPI 3.0 subset
|
if (UNSUPPORTED_SCHEMA_KEYWORDS.has(key)) {
|
||||||
// - const: convert to enum with single value instead
|
|
||||||
if (key === "patternProperties") {
|
|
||||||
// Gemini doesn't support patternProperties - skip it
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,8 +264,8 @@ function cleanSchemaForGemini(schema: unknown): unknown {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip 'type' if we have 'anyOf' — Gemini doesn't allow both
|
// Skip 'type' if we have 'anyOf' or 'oneOf' — Gemini doesn't allow both
|
||||||
if (key === "type" && hasAnyOf) {
|
if (key === "type" && (hasAnyOf || hasOneOf)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,13 +287,6 @@ function cleanSchemaForGemini(schema: unknown): unknown {
|
|||||||
} else if (key === "allOf" && Array.isArray(value)) {
|
} else if (key === "allOf" && Array.isArray(value)) {
|
||||||
// Clean each allOf variant
|
// Clean each allOf variant
|
||||||
cleaned[key] = value.map((variant) => cleanSchemaForGemini(variant));
|
cleaned[key] = value.map((variant) => cleanSchemaForGemini(variant));
|
||||||
} else if (
|
|
||||||
key === "additionalProperties" &&
|
|
||||||
value &&
|
|
||||||
typeof value === "object"
|
|
||||||
) {
|
|
||||||
// Recursively clean additionalProperties schema
|
|
||||||
cleaned[key] = cleanSchemaForGemini(value);
|
|
||||||
} else {
|
} else {
|
||||||
cleaned[key] = value;
|
cleaned[key] = value;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user