From c90fd8972554396596420f06d824052056bfbb83 Mon Sep 17 00:00:00 2001 From: echoVic Date: Mon, 16 Feb 2026 17:58:01 +0800 Subject: [PATCH] fix: sanitize tool schemas for strict JSON Schema validators (#13737) Built-in tool schemas (like `question`) break on providers with strict JSON Schema validation (Codex, Vertex AI, SGLang) due to two issues: 1. Zod `.meta()` injects `$schema` and `ref` keywords that are invalid in OpenAI function parameter schemas 2. When `additionalProperties: false` is set, strict validators require ALL property keys to be in the `required` array Fix: Add universal schema sanitization in `ProviderTransform.schema()` that runs before provider-specific transforms: - Strip Zod meta fields (`$schema`, `ref`) - For strict mode objects, ensure all properties are in `required` and wrap optional ones with `anyOf: [original, {type: 'null'}]` Closes #13737 --- packages/opencode/src/provider/transform.ts | 41 +++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 759dab440d40..fc3befbebe04 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -856,6 +856,47 @@ export namespace ProviderTransform { } export function schema(model: Provider.Model, schema: JSONSchema.BaseSchema | JSONSchema7): JSONSchema7 { + // Universal schema sanitization for strict JSON Schema validators (Codex, Vertex AI, SGLang, etc.) + // 1. Remove Zod .meta() injected fields ($schema, ref) that are invalid in function parameter schemas + // 2. When additionalProperties is false, ensure all property keys are in required array + const sanitizeStrict = (obj: any): any => { + if (obj === null || typeof obj !== "object") return obj + if (Array.isArray(obj)) return obj.map(sanitizeStrict) + + const result: any = {} + for (const [key, value] of Object.entries(obj)) { + // Strip Zod meta fields that break strict validators + if (key === "$schema" || key === "ref") continue + if (typeof value === "object" && value !== null) { + result[key] = sanitizeStrict(value) + } else { + result[key] = value + } + } + + // Strict mode: additionalProperties=false requires all properties in required + if ( + result.type === "object" && + result.properties && + result.additionalProperties === false + ) { + const allKeys = Object.keys(result.properties) + const existing = new Set(Array.isArray(result.required) ? result.required : []) + result.required = allKeys.filter((k) => existing.has(k)) + // Wrap optional properties with anyOf [..., {type: "null"}] so they can be omitted + for (const k of allKeys) { + if (!existing.has(k) && result.properties[k]) { + result.properties[k] = { + anyOf: [result.properties[k], { type: "null" }], + } + result.required.push(k) + } + } + } + + return result + } + schema = sanitizeStrict(schema) /* if (["openai", "azure"].includes(providerID)) { if (schema.type === "object" && schema.properties) {