diff --git a/internal/util/gemini_schema.go b/internal/util/gemini_schema.go index 60453998..be514e64 100644 --- a/internal/util/gemini_schema.go +++ b/internal/util/gemini_schema.go @@ -431,9 +431,43 @@ func removeUnsupportedKeywords(jsonStr string) string { jsonStr, _ = sjson.Delete(jsonStr, p) } } + // Remove x-* extension fields (e.g., x-google-enum-descriptions) that are not supported by Gemini API + jsonStr = removeExtensionFields(jsonStr) return jsonStr } +// removeExtensionFields removes all x-* extension fields from the JSON schema. +// These are OpenAPI/JSON Schema extension fields that Google APIs don't recognize. +func removeExtensionFields(jsonStr string) string { + var paths []string + walkForExtensions(gjson.Parse(jsonStr), "", &paths) + sortByDepth(paths) + for _, p := range paths { + jsonStr, _ = sjson.Delete(jsonStr, p) + } + return jsonStr +} + +func walkForExtensions(value gjson.Result, path string, paths *[]string) { + if !value.IsObject() && !value.IsArray() { + return + } + + value.ForEach(func(key, val gjson.Result) bool { + keyStr := key.String() + safeKey := escapeGJSONPathKey(keyStr) + childPath := joinPath(path, safeKey) + + // Only remove x-* extension fields, but protect them if they are property definitions. + if strings.HasPrefix(keyStr, "x-") && !isPropertyDefinition(path) { + *paths = append(*paths, childPath) + } + + walkForExtensions(val, childPath, paths) + return true + }) +} + func cleanupRequiredFields(jsonStr string) string { for _, p := range findPaths(jsonStr, "required") { parentPath := trimSuffix(p, ".required") diff --git a/internal/util/gemini_schema_test.go b/internal/util/gemini_schema_test.go index ca77225e..ea63d111 100644 --- a/internal/util/gemini_schema_test.go +++ b/internal/util/gemini_schema_test.go @@ -869,3 +869,129 @@ func TestCleanJSONSchemaForAntigravity_BooleanEnumToString(t *testing.T) { t.Errorf("Boolean enum values should be converted to string format, got: %s", result) } } + +func TestRemoveExtensionFields(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "removes x- fields at root", + input: `{ + "type": "object", + "x-custom-meta": "value", + "properties": { + "foo": { "type": "string" } + } + }`, + expected: `{ + "type": "object", + "properties": { + "foo": { "type": "string" } + } + }`, + }, + { + name: "removes x- fields in nested properties", + input: `{ + "type": "object", + "properties": { + "foo": { + "type": "string", + "x-internal-id": 123 + } + } + }`, + expected: `{ + "type": "object", + "properties": { + "foo": { + "type": "string" + } + } + }`, + }, + { + name: "does NOT remove properties named x-", + input: `{ + "type": "object", + "properties": { + "x-data": { "type": "string" }, + "normal": { "type": "number", "x-meta": "remove" } + }, + "required": ["x-data"] + }`, + expected: `{ + "type": "object", + "properties": { + "x-data": { "type": "string" }, + "normal": { "type": "number" } + }, + "required": ["x-data"] + }`, + }, + { + name: "does NOT remove $schema and other meta fields (as requested)", + input: `{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "test", + "type": "object", + "properties": { + "foo": { "type": "string" } + } + }`, + expected: `{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "test", + "type": "object", + "properties": { + "foo": { "type": "string" } + } + }`, + }, + { + name: "handles properties named $schema", + input: `{ + "type": "object", + "properties": { + "$schema": { "type": "string" } + } + }`, + expected: `{ + "type": "object", + "properties": { + "$schema": { "type": "string" } + } + }`, + }, + { + name: "handles escaping in paths", + input: `{ + "type": "object", + "properties": { + "foo.bar": { + "type": "string", + "x-meta": "remove" + } + }, + "x-root.meta": "remove" + }`, + expected: `{ + "type": "object", + "properties": { + "foo.bar": { + "type": "string" + } + } + }`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := removeExtensionFields(tt.input) + compareJSON(t, tt.expected, actual) + }) + } +}