From 7035dc2abcb1f1ea3b60e7cc1ba12208ab8059ea Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 26 Dec 2025 15:10:14 +0000 Subject: [PATCH 1/2] docs: update readme version to 1.2.6 [skip ci] --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 6230835..bb77d1b 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ If the agent only installs the plugin, ask it to also add models under `provider ```json { - "plugin": ["opencode-antigravity-auth@1.2.4"] + "plugin": ["opencode-antigravity-auth@1.2.6"] } ``` @@ -61,7 +61,7 @@ If the agent only installs the plugin, ask it to also add models under `provider ```json { - "plugin": ["opencode-antigravity-auth@1.2.4"], + "plugin": ["opencode-antigravity-auth@1.2.6"], "provider": { "google": { "models": { @@ -111,14 +111,14 @@ mkdir -p ~/.config/opencode if [ -f ~/.config/opencode/opencode.json ]; then if command -v jq &> /dev/null; then - jq '.plugin = ((.plugin // []) + ["opencode-antigravity-auth@1.2.4"] | unique)' \ + jq '.plugin = ((.plugin // []) + ["opencode-antigravity-auth@1.2.6"] | unique)' \ ~/.config/opencode/opencode.json > /tmp/oc.json && \ mv /tmp/oc.json ~/.config/opencode/opencode.json else - echo "Add \"opencode-antigravity-auth@1.2.4\" to the plugin array manually" + echo "Add \"opencode-antigravity-auth@1.2.6\" to the plugin array manually" fi else - echo '{"plugin":["opencode-antigravity-auth@1.2.4"]}' > ~/.config/opencode/opencode.json + echo '{"plugin":["opencode-antigravity-auth@1.2.6"]}' > ~/.config/opencode/opencode.json fi ``` @@ -207,7 +207,7 @@ Version 1.2.5 introduces significant reliability improvements for Claude models. ```json { - "plugin": ["opencode-antigravity-auth@1.2.5"] + "plugin": ["opencode-antigravity-auth@1.2.6"] } ``` From c9c2745dbc5403fd12f62f815e41e880cb1fab21 Mon Sep 17 00:00:00 2001 From: Parasitic-Hollow Date: Sat, 27 Dec 2025 05:10:00 -0400 Subject: [PATCH 2/2] fix: WebFetch tool format parameter validation with Claude models Fixes NoeFabris/opencode-antigravity-auth#62 - WebFetch tool failing with "Invalid option: expected one of 'text'|'markdown'|'html'" error. Root cause: Two bugs in JSON schema cleaning for Antigravity API: 1. flattenAnyOfOneOf lost enum values when anyOf/oneOf used const pattern - Pattern like anyOf: [{const: "text"}, {const: "markdown"}] was flattened to just the first option, losing other valid values - Fix: Added tryMergeEnumFromUnion() to detect enum patterns and merge all values into a single enum array 2. removeUnsupportedKeywords incorrectly removed property NAMES - The "format" JSON Schema keyword was in UNSUPPORTED_KEYWORDS - This also removed properties NAMED "format" inside schemas - Fix: Added isInsideProperties flag to preserve property names while still removing JSON Schema keywords Added comprehensive test coverage for enum merging from anyOf/oneOf. --- src/plugin/request-helpers.test.ts | 159 +++++++++++++++++++++++++++++ src/plugin/request-helpers.ts | 96 ++++++++++++++++- 2 files changed, 250 insertions(+), 5 deletions(-) diff --git a/src/plugin/request-helpers.test.ts b/src/plugin/request-helpers.test.ts index c265927..35d01ca 100644 --- a/src/plugin/request-helpers.test.ts +++ b/src/plugin/request-helpers.test.ts @@ -17,6 +17,7 @@ import { findOrphanedToolUseIds, fixClaudeToolPairing, validateAndFixClaudeToolPairing, + cleanJSONSchemaForAntigravity, } from "./request-helpers"; describe("sanitizeThinkingPart (covered via filtering)", () => { @@ -1107,3 +1108,161 @@ describe("validateAndFixClaudeToolPairing", () => { expect(validateAndFixClaudeToolPairing([])).toEqual([]); }); }); + +describe("cleanJSONSchemaForAntigravity", () => { + describe("enum merging from anyOf/oneOf", () => { + it("merges anyOf with const values into enum (WebFetch format pattern)", () => { + // This is the exact pattern used by WebFetch's format parameter + const schema = { + type: "object", + properties: { + format: { + anyOf: [ + { const: "text" }, + { const: "markdown" }, + { const: "html" }, + ], + }, + }, + }; + + const result = cleanJSONSchemaForAntigravity(schema); + + expect(result.properties.format.enum).toEqual(["text", "markdown", "html"]); + expect(result.properties.format.anyOf).toBeUndefined(); + expect(result.properties.format.type).toBe("string"); + }); + + it("merges oneOf with const values into enum", () => { + const schema = { + type: "object", + properties: { + status: { + oneOf: [ + { const: "pending" }, + { const: "active" }, + { const: "completed" }, + ], + }, + }, + }; + + const result = cleanJSONSchemaForAntigravity(schema); + + expect(result.properties.status.enum).toEqual(["pending", "active", "completed"]); + expect(result.properties.status.oneOf).toBeUndefined(); + }); + + it("merges anyOf with single-value enums into combined enum", () => { + const schema = { + type: "object", + properties: { + level: { + anyOf: [ + { enum: ["low"] }, + { enum: ["medium"] }, + { enum: ["high"] }, + ], + }, + }, + }; + + const result = cleanJSONSchemaForAntigravity(schema); + + expect(result.properties.level.enum).toEqual(["low", "medium", "high"]); + }); + + it("merges anyOf with multi-value enums", () => { + const schema = { + type: "object", + properties: { + color: { + anyOf: [ + { enum: ["red", "blue"] }, + { enum: ["green", "yellow"] }, + ], + }, + }, + }; + + const result = cleanJSONSchemaForAntigravity(schema); + + expect(result.properties.color.enum).toEqual(["red", "blue", "green", "yellow"]); + }); + + it("does not merge anyOf with complex types (not enum pattern)", () => { + const schema = { + type: "object", + properties: { + data: { + anyOf: [ + { type: "string" }, + { type: "number" }, + ], + }, + }, + }; + + const result = cleanJSONSchemaForAntigravity(schema); + + // Should flatten to first option, not create enum + expect(result.properties.data.enum).toBeUndefined(); + expect(result.properties.data.type).toBe("string"); + }); + + it("preserves parent description when merging enum", () => { + const schema = { + type: "object", + properties: { + format: { + description: "Output format for the content", + anyOf: [ + { const: "text" }, + { const: "markdown" }, + ], + }, + }, + }; + + const result = cleanJSONSchemaForAntigravity(schema); + + expect(result.properties.format.enum).toEqual(["text", "markdown"]); + expect(result.properties.format.description).toContain("Output format"); + }); + }); + + it("adds enum hints to description", () => { + const schema = { + type: "object", + properties: { + status: { + type: "string", + enum: ["active", "inactive", "pending"], + }, + }, + }; + + const result = cleanJSONSchemaForAntigravity(schema); + + expect(result.properties.status.description).toContain("Allowed:"); + expect(result.properties.status.description).toContain("active"); + expect(result.properties.status.description).toContain("inactive"); + expect(result.properties.status.description).toContain("pending"); + }); + + it("preserves existing enum array", () => { + const schema = { + type: "object", + properties: { + level: { + type: "string", + enum: ["low", "medium", "high"], + }, + }, + }; + + const result = cleanJSONSchemaForAntigravity(schema); + + expect(result.properties.level.enum).toEqual(["low", "medium", "high"]); + }); +}); diff --git a/src/plugin/request-helpers.ts b/src/plugin/request-helpers.ts index fc2560a..be44364 100644 --- a/src/plugin/request-helpers.ts +++ b/src/plugin/request-helpers.ts @@ -293,10 +293,70 @@ function scoreSchemaOption(schema: any): { score: number; typeName: string } { return { score: 0, typeName: type || "null" }; } +/** + * Checks if an anyOf/oneOf array represents enum choices. + * Returns the merged enum values if so, otherwise null. + * + * Handles patterns like: + * - anyOf: [{ const: "a" }, { const: "b" }] + * - anyOf: [{ enum: ["a"] }, { enum: ["b"] }] + * - anyOf: [{ type: "string", const: "a" }, { type: "string", const: "b" }] + */ +function tryMergeEnumFromUnion(options: any[]): string[] | null { + if (!Array.isArray(options) || options.length === 0) { + return null; + } + + const enumValues: string[] = []; + + for (const option of options) { + if (!option || typeof option !== "object") { + return null; + } + + // Check for const value + if (option.const !== undefined) { + enumValues.push(String(option.const)); + continue; + } + + // Check for single-value enum + if (Array.isArray(option.enum) && option.enum.length === 1) { + enumValues.push(String(option.enum[0])); + continue; + } + + // Check for multi-value enum (merge all values) + if (Array.isArray(option.enum) && option.enum.length > 0) { + for (const val of option.enum) { + enumValues.push(String(val)); + } + continue; + } + + // If option has complex structure (properties, items, etc.), it's not a simple enum + if (option.properties || option.items || option.anyOf || option.oneOf || option.allOf) { + return null; + } + + // If option has only type (no const/enum), it's not an enum pattern + if (option.type && !option.const && !option.enum) { + return null; + } + } + + // Only return if we found actual enum values + return enumValues.length > 0 ? enumValues : null; +} + /** * Phase 2b: Flattens anyOf/oneOf to the best option with type hints. * { anyOf: [{ type: "string" }, { type: "number" }] } * → { type: "string", description: "(Accepts: string | number)" } + * + * Special handling for enum patterns: + * { anyOf: [{ const: "a" }, { const: "b" }] } + * → { type: "string", enum: ["a", "b"] } */ function flattenAnyOfOneOf(schema: any): any { if (!schema || typeof schema !== "object") { @@ -315,6 +375,25 @@ function flattenAnyOfOneOf(schema: any): any { const options = result[unionKey]; const parentDesc = typeof result.description === "string" ? result.description : ""; + // First, check if this is an enum pattern (anyOf with const/enum values) + // This is crucial for tools like WebFetch where format: anyOf[{const:"text"},{const:"markdown"},{const:"html"}] + const mergedEnum = tryMergeEnumFromUnion(options); + if (mergedEnum !== null) { + // This is an enum pattern - merge all values into a single enum + const { [unionKey]: _, ...rest } = result; + result = { + ...rest, + type: "string", + enum: mergedEnum, + }; + // Preserve parent description + if (parentDesc) { + result.description = parentDesc; + } + continue; + } + + // Not an enum pattern - use standard flattening logic // Score each option and find the best let bestIdx = 0; let bestScore = -1; @@ -448,26 +527,33 @@ function flattenTypeArrays(schema: any, nullableFields?: Map, /** * Phase 3: Removes unsupported keywords after hints have been extracted. + * + * IMPORTANT: Only removes JSON Schema keywords at the schema level, NOT property names. + * For example, the JSON Schema `format` keyword (like `format: "email"`) should be removed, + * but a property named `format` inside `properties` should be preserved. */ -function removeUnsupportedKeywords(schema: any): any { +function removeUnsupportedKeywords(schema: any, isInsideProperties = false): any { if (!schema || typeof schema !== "object") { return schema; } if (Array.isArray(schema)) { - return schema.map(item => removeUnsupportedKeywords(item)); + return schema.map(item => removeUnsupportedKeywords(item, false)); } const result: any = {}; for (const [key, value] of Object.entries(schema)) { - // Skip unsupported keywords - if ((UNSUPPORTED_KEYWORDS as readonly string[]).includes(key)) { + // Skip unsupported keywords, BUT NOT if we're inside a "properties" object + // because those are property NAMES, not JSON Schema keywords + if (!isInsideProperties && (UNSUPPORTED_KEYWORDS as readonly string[]).includes(key)) { continue; } // Recursively process nested objects + // When processing "properties", mark that we're inside properties so child keys aren't filtered if (typeof value === "object" && value !== null) { - result[key] = removeUnsupportedKeywords(value); + const isProperties = key === "properties"; + result[key] = removeUnsupportedKeywords(value, isProperties); } else { result[key] = value; }