From f3210ab862d974cd63317ac14bcdcf35dd5e38fb Mon Sep 17 00:00:00 2001 From: basedcorp99 Date: Sat, 6 Jun 2026 18:47:11 +0200 Subject: [PATCH 1/6] fix(ai): strip type-specific keys when CCA mixed-type collapse picks non-matching type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When collapseMixedTypeCombinerVariants collapses an anyOf with mixed types (e.g. string | array), it previously picked the first non-null type but indiscriminately copied ALL mergedVariantFields — including type-specific keys like "items" that only belong to array. This produced schemas like {type: "string", items: {...}} which Google Cloud Code Assist API rejects with 400. Fix: filter mergedVariantFields against the chosen types allowed keys (CLOUD_CODE_ASSIST_TYPE_SPECIFIC_KEYS) before copying, so array-only keys are dropped when the winner is string (and vice versa). Fixes 400 error on github tools "pr" parameter (anyOf string/array). --- packages/ai/src/utils/schema/normalize.ts | 8 ++++++++ packages/ai/test/schema-normalization.test.ts | 15 +++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/packages/ai/src/utils/schema/normalize.ts b/packages/ai/src/utils/schema/normalize.ts index 1b21afd67..e10a46270 100644 --- a/packages/ai/src/utils/schema/normalize.ts +++ b/packages/ai/src/utils/schema/normalize.ts @@ -505,8 +505,16 @@ function collapseMixedTypeCombinerVariants(schema: JsonObject, combiner: "anyOf" const nextSchema = copySchemaWithout(schema, combiner); const nonNullTypes = variantTypes.filter(t => t !== "null"); nextSchema.type = nonNullTypes[0] ?? variantTypes[0]; + const chosenTypeAllowedKeys = CLOUD_CODE_ASSIST_TYPE_SPECIFIC_KEYS[nextSchema.type as string] ?? {}; for (const key in mergedVariantFields) { if (!Object.hasOwn(mergedVariantFields, key)) continue; + // Drop type-specific keys that don't belong to the chosen type + if ( + !Object.hasOwn(chosenTypeAllowedKeys, key) && + !Object.hasOwn(CLOUD_CODE_ASSIST_SHARED_SCHEMA_KEYS, key) + ) { + continue; + } const value = mergedVariantFields[key]; const existingValue = nextSchema[key]; if (existingValue !== undefined && !areJsonValuesEqual(existingValue, value)) { diff --git a/packages/ai/test/schema-normalization.test.ts b/packages/ai/test/schema-normalization.test.ts index 1f6ba7688..c7e55d9d1 100644 --- a/packages/ai/test/schema-normalization.test.ts +++ b/packages/ai/test/schema-normalization.test.ts @@ -952,6 +952,21 @@ describe("normalizeSchemaForCCA", () => { properties: {}, }); }); + + it("strips array-only keys when mixed-type collapse picks a non-array type", () => { + // Regression: anyOf [{type:"string"}, {type:"array", items:{type:"string"}}] + // collapsed to {type:"string", items:{type:"string"}} which is invalid. + // The fix filters mergedVariantFields against the chosen type's allowed keys. + const normalized = normalizeSchemaForCCA({ + anyOf: [{ type: "string" }, { type: "array", items: { type: "string" } }], + description: "pr number, url, or branch", + }); + + expect(normalized).toEqual({ + type: "string", + description: "pr number, url, or branch", + }); + }); }); // --------------------------------------------------------------------------- From 2623bd75a279e08018925a511dbdf722b9b6017e Mon Sep 17 00:00:00 2001 From: basedcorp99 Date: Sat, 6 Jun 2026 18:53:52 +0200 Subject: [PATCH 2/6] test(ai): add stripResidualCombiners regression for mixed-type string|array collapse --- packages/ai/test/schema-normalization.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/ai/test/schema-normalization.test.ts b/packages/ai/test/schema-normalization.test.ts index c7e55d9d1..b2c23fbc4 100644 --- a/packages/ai/test/schema-normalization.test.ts +++ b/packages/ai/test/schema-normalization.test.ts @@ -728,6 +728,18 @@ describe("stripResidualCombiners", () => { expect(normalized.anyOf).toBeUndefined(); expect(normalized.oneOf).toBeUndefined(); }); + + it("drops array-only keys when mixed-type collapse picks string from anyOf fixpoint", () => { + const stripped = stripResidualCombiners({ + anyOf: [{ type: "string" }, { type: "array", items: { type: "string" } }], + description: "pr number, url, or branch", + }) as Record; + + expect(stripped.type).toBe("string"); + expect(stripped.items).toBeUndefined(); + expect(stripped.anyOf).toBeUndefined(); + expect(stripped.description).toBe("pr number, url, or branch"); + }); }); // --------------------------------------------------------------------------- From 807df56ba752e347678ac002ea7f78996c7a66be Mon Sep 17 00:00:00 2001 From: basedcorp99 Date: Sat, 6 Jun 2026 19:00:32 +0200 Subject: [PATCH 3/6] docs(ai): add unreleased changelog entry for CCA mixed-type combiner collapse fix --- packages/ai/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 2f8dea9f7..dd8e58bbc 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,9 @@ ## [Unreleased] +### Fixed + +- Fixed Cloud Code Assist (Antigravity / Gemini CLI) rejecting the `github` tool with HTTP 400 when the `pr` parameter schema contained `anyOf: [string, array]`. The CCA mixed-type combiner collapse picked the first non-null type (`string`) but indiscriminately copied type-specific keys from variant branches — `items` from the array variant leaked onto the string-typed result, producing `{type: "string", items: {...}}` which Google's API rejects as invalid. The collapse now filters merged variant fields against the winning type's allowed key set. ([#TBD](https://github.com/can1357/oh-my-pi/pull/TBD)) ## [15.9.67] - 2026-06-06 ### Fixed From dee4db16025a07f5b2f5db9406d371b2d84f8710 Mon Sep 17 00:00:00 2001 From: basedcorp99 Date: Sat, 6 Jun 2026 19:02:49 +0200 Subject: [PATCH 4/6] chore(ai): update changelog with PR number --- packages/ai/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index dd8e58bbc..01a4ab507 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -4,7 +4,7 @@ ### Fixed -- Fixed Cloud Code Assist (Antigravity / Gemini CLI) rejecting the `github` tool with HTTP 400 when the `pr` parameter schema contained `anyOf: [string, array]`. The CCA mixed-type combiner collapse picked the first non-null type (`string`) but indiscriminately copied type-specific keys from variant branches — `items` from the array variant leaked onto the string-typed result, producing `{type: "string", items: {...}}` which Google's API rejects as invalid. The collapse now filters merged variant fields against the winning type's allowed key set. ([#TBD](https://github.com/can1357/oh-my-pi/pull/TBD)) +- Fixed Cloud Code Assist (Antigravity / Gemini CLI) rejecting the `github` tool with HTTP 400 when the `pr` parameter schema contained `anyOf: [string, array]`. The CCA mixed-type combiner collapse picked the first non-null type (`string`) but indiscriminately copied type-specific keys from variant branches — `items` from the array variant leaked onto the string-typed result, producing `{type: "string", items: {...}}` which Google's API rejects as invalid. The collapse now filters merged variant fields against the winning type's allowed key set. ([#2002](https://github.com/can1357/oh-my-pi/pull/2002)) ## [15.9.67] - 2026-06-06 ### Fixed From 7c8fb4d8f6875900e46b0a3cd275806e24f6cd45 Mon Sep 17 00:00:00 2001 From: basedcorp99 Date: Sat, 6 Jun 2026 19:08:07 +0200 Subject: [PATCH 5/6] fix(ai): also strip sibling type-specific keys during CCA mixed-type collapse Address review feedback: - Replace `as string` assertion with typed `chosenType` local - Strip sibling keys from nextSchema that were copied via copySchemaWithout but belong to a type other than the chosen one (e.g. sibling `items` on a now-string-typed schema) - Export ALL_CCA_TYPE_SPECIFIC_KEYS from fields.ts for sibling filtering - Add regression test for the sibling-key edge case --- packages/ai/src/utils/schema/fields.ts | 16 ++++++++++++++ packages/ai/src/utils/schema/normalize.ts | 22 ++++++++++++++++--- packages/ai/test/schema-normalization.test.ts | 15 +++++++++++++ 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/packages/ai/src/utils/schema/fields.ts b/packages/ai/src/utils/schema/fields.ts index 41e9aacf1..b25006248 100644 --- a/packages/ai/src/utils/schema/fields.ts +++ b/packages/ai/src/utils/schema/fields.ts @@ -154,6 +154,22 @@ export const CLOUD_CODE_ASSIST_TYPE_SPECIFIC_KEYS: Record = buildAllCcaTypeSpecificKeys(); + +function buildAllCcaTypeSpecificKeys(): Record { + const all: Record = {}; + for (const typeKeys of Object.values(CLOUD_CODE_ASSIST_TYPE_SPECIFIC_KEYS)) { + for (const key in typeKeys) { + all[key] = true; + } + } + return all; +} + /** * Cloud Code Assist shared schema keys allowed on any type. * Used alongside CLOUD_CODE_ASSIST_TYPE_SPECIFIC_KEYS for CCA combiner collapsing. diff --git a/packages/ai/src/utils/schema/normalize.ts b/packages/ai/src/utils/schema/normalize.ts index e10a46270..0d2961f26 100644 --- a/packages/ai/src/utils/schema/normalize.ts +++ b/packages/ai/src/utils/schema/normalize.ts @@ -11,6 +11,7 @@ import { dereferenceJsonSchema } from "./dereference"; import { upgradeJsonSchemaTo202012 } from "./draft"; import { areJsonValuesEqual, mergePropertySchemas } from "./equality"; import { + ALL_CCA_TYPE_SPECIFIC_KEYS, CLOUD_CODE_ASSIST_SHARED_SCHEMA_KEYS, CLOUD_CODE_ASSIST_TYPE_SPECIFIC_KEYS, COMBINATOR_KEYS, @@ -501,11 +502,26 @@ function collapseMixedTypeCombinerVariants(schema: JsonObject, combiner: "anyOf" if (variantTypes.length < 2 || variantTypes.every(type => type === "object")) { return schema; } - const nextSchema = copySchemaWithout(schema, combiner); const nonNullTypes = variantTypes.filter(t => t !== "null"); - nextSchema.type = nonNullTypes[0] ?? variantTypes[0]; - const chosenTypeAllowedKeys = CLOUD_CODE_ASSIST_TYPE_SPECIFIC_KEYS[nextSchema.type as string] ?? {}; + const chosenType: string = nonNullTypes[0] ?? variantTypes[0]; + nextSchema.type = chosenType; + const chosenTypeAllowedKeys = CLOUD_CODE_ASSIST_TYPE_SPECIFIC_KEYS[chosenType] ?? {}; + + // Strip sibling keys that were copied from the parent and belong to a + // different type (e.g. `items` sibling on a now-string-typed schema). + for (const key in nextSchema) { + if (!Object.hasOwn(nextSchema, key)) continue; + if (key === "type") continue; + if ( + Object.hasOwn(ALL_CCA_TYPE_SPECIFIC_KEYS, key) && + !Object.hasOwn(chosenTypeAllowedKeys, key) && + !Object.hasOwn(CLOUD_CODE_ASSIST_SHARED_SCHEMA_KEYS, key) + ) { + delete nextSchema[key]; + } + } + for (const key in mergedVariantFields) { if (!Object.hasOwn(mergedVariantFields, key)) continue; // Drop type-specific keys that don't belong to the chosen type diff --git a/packages/ai/test/schema-normalization.test.ts b/packages/ai/test/schema-normalization.test.ts index b2c23fbc4..22efcb2cd 100644 --- a/packages/ai/test/schema-normalization.test.ts +++ b/packages/ai/test/schema-normalization.test.ts @@ -979,6 +979,21 @@ describe("normalizeSchemaForCCA", () => { description: "pr number, url, or branch", }); }); + + it("strips sibling type-specific keys copied from parent when mixed-type collapse picks opposing type", () => { + // Edge case: parent has a sibling `items` outside the anyOf, + // and the chosen type is string. The sibling must be stripped. + const normalized = normalizeSchemaForCCA({ + anyOf: [{ type: "string" }, { type: "array", items: { type: "number" } }], + items: { type: "string" }, + description: "pr number, url, or branch", + }); + + expect(normalized).toEqual({ + type: "string", + description: "pr number, url, or branch", + }); + }); }); // --------------------------------------------------------------------------- From f8ef2cf8e45899f28fc6475a30a10752786a3f11 Mon Sep 17 00:00:00 2001 From: basedcorp99 Date: Sat, 6 Jun 2026 20:42:06 +0200 Subject: [PATCH 6/6] fix: format CCA schema normalization --- packages/ai/src/utils/schema/normalize.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/ai/src/utils/schema/normalize.ts b/packages/ai/src/utils/schema/normalize.ts index 0d2961f26..ac50eccb7 100644 --- a/packages/ai/src/utils/schema/normalize.ts +++ b/packages/ai/src/utils/schema/normalize.ts @@ -525,10 +525,7 @@ function collapseMixedTypeCombinerVariants(schema: JsonObject, combiner: "anyOf" for (const key in mergedVariantFields) { if (!Object.hasOwn(mergedVariantFields, key)) continue; // Drop type-specific keys that don't belong to the chosen type - if ( - !Object.hasOwn(chosenTypeAllowedKeys, key) && - !Object.hasOwn(CLOUD_CODE_ASSIST_SHARED_SCHEMA_KEYS, key) - ) { + if (!Object.hasOwn(chosenTypeAllowedKeys, key) && !Object.hasOwn(CLOUD_CODE_ASSIST_SHARED_SCHEMA_KEYS, key)) { continue; } const value = mergedVariantFields[key];