Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions open-sse/translator/helpers/geminiHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -843,9 +843,19 @@ export function cleanJSONSchemaForAntigravity(schema: unknown): unknown {
record.type = "object";
}

// Recurse into remaining values.
for (const value of Object.values(record)) {
if (value && typeof value === "object") {
// Recurse into remaining values. `properties` is a map keyed by arbitrary,
// user-defined property NAMES — a tool may legitimately declare a property
// called `properties` (or `required`, etc). The map itself is never a
// schema node, so it must never receive `type: "object"`; only descend
// into each entry's subschema. Mirrors the same guard already applied in
// removeUnsupportedKeywords() above.
for (const [key, value] of Object.entries(record)) {
if (!value || typeof value !== "object") continue;
if (key === "properties" && !Array.isArray(value)) {
for (const subSchema of Object.values(value as JsonRecord)) {
injectObjectType(subSchema);
}
} else {
injectObjectType(value);
}
}
Expand Down
114 changes: 114 additions & 0 deletions tests/unit/gemini-schema-properties-named-property.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import test from "node:test";
import assert from "node:assert/strict";

const { cleanJSONSchemaForAntigravity } = await import(
"../../open-sse/translator/helpers/geminiHelper.ts"
);

// Reproduces the Vertex 400:
// Invalid value at 'tools[0].function_declarations[43].parameters.properties[8].value'
// (type.googleapis.com/google.cloud.aiplatform.v1.Schema), "object"
// The github-custom_properties_write tool declares a legitimate parameter named
// `properties`. The type:"object" injection visitor mistook the root property
// MAP for a schema node (because `map.properties !== undefined`) and wrote
// `type: "object"` into the map, turning it into a 9th, non-Schema entry.
const githubCustomPropertiesWriteSchema = () => ({
type: "object",
properties: {
security_risk: { type: "string" },
summary: { type: "string" },
enterprise: { type: "string" },
level: { type: "string" },
org: { type: "string" },
owner: { type: "string" },
properties: {
type: "array",
items: {
type: "object",
properties: {},
additionalProperties: true,
},
},
repo: { type: "string" },
},
required: ["level", "properties"],
});

test("a tool parameter named `properties` does not inject type into the property map", () => {
const result = cleanJSONSchemaForAntigravity(
githubCustomPropertiesWriteSchema()
) as Record<string, unknown>;

const props = result.properties as Record<string, unknown>;

assert.deepEqual(
Object.keys(props).sort(),
[
"enterprise",
"level",
"org",
"owner",
"properties",
"repo",
"security_risk",
"summary",
],
"the root property map must keep exactly the declared parameter names"
);
assert.ok(
!Object.prototype.hasOwnProperty.call(props, "type"),
"no synthetic `type` entry may be added to the property map"
);
assert.equal(result.type, "object", "the root schema itself must stay type:object");
});

test("the legitimate `properties` parameter keeps its own array schema", () => {
const result = cleanJSONSchemaForAntigravity(
githubCustomPropertiesWriteSchema()
) as Record<string, unknown>;

const props = result.properties as Record<string, unknown>;
const propertiesParam = props.properties as Record<string, unknown>;

assert.ok(propertiesParam, "the `properties` parameter must survive cleaning");
assert.equal(propertiesParam.type, "array", "its type:array must be preserved");

const items = propertiesParam.items as Record<string, unknown>;
assert.ok(items, "its items schema must be preserved");
assert.equal(items.type, "object", "its items schema must stay a valid object schema");
assert.ok(
!("additionalProperties" in items),
"Gemini/Vertex sanitisation must still remove additionalProperties"
);
});

test("other schema keyword names used as parameter names are not treated as metadata", () => {
const input = {
type: "object",
properties: {
required: { type: "string" },
items: { type: "string" },
type: { type: "string" },
description: { type: "string" },
},
required: ["required"],
};

const result = cleanJSONSchemaForAntigravity(input) as Record<string, unknown>;
const props = result.properties as Record<string, unknown>;

assert.deepEqual(
Object.keys(props).sort(),
["description", "items", "required", "type"],
"keyword-named parameters must survive untouched and gain no siblings"
);
for (const [name, subSchema] of Object.entries(props)) {
assert.equal(
(subSchema as Record<string, unknown>).type,
"string",
`parameter \`${name}\` must keep its declared string type`
);
}
assert.equal(result.type, "object");
assert.deepEqual(result.required, ["required"]);
});
Loading