mirror of
https://github.com/NoeFabris/opencode-antigravity-auth.git
synced 2026-05-21 12:54:56 +00:00
Merge pull request #63 from Parasitic-Hollow/fix/webfetch-format-validation
fix: WebFetch tool format parameter validation with Claude models
This commit is contained in:
12
README.md
12
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"]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -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"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<string, string[]>,
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user