diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index c05bf75c46f..b4f1aaca4d5 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -772,6 +772,12 @@ export namespace ProviderTransform { result.items = {} } + // Remove properties/required from non-object types (Gemini rejects these) + if (result.type && result.type !== "object") { + delete result.properties + delete result.required + } + return result } diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 8e28f1209e1..0743049fe06 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -293,6 +293,116 @@ describe("ProviderTransform.schema - gemini array items", () => { }) }) +describe("ProviderTransform.schema - gemini non-object properties removal", () => { + const geminiModel = { + providerID: "google", + api: { + id: "gemini-3-pro", + }, + } as any + + test("removes properties from non-object types", () => { + const schema = { + type: "object", + properties: { + data: { + type: "string", + properties: { invalid: { type: "string" } }, + }, + }, + } as any + + const result = ProviderTransform.schema(geminiModel, schema) as any + + expect(result.properties.data.type).toBe("string") + expect(result.properties.data.properties).toBeUndefined() + }) + + test("removes required from non-object types", () => { + const schema = { + type: "object", + properties: { + data: { + type: "array", + items: { type: "string" }, + required: ["invalid"], + }, + }, + } as any + + const result = ProviderTransform.schema(geminiModel, schema) as any + + expect(result.properties.data.type).toBe("array") + expect(result.properties.data.required).toBeUndefined() + }) + + test("removes properties and required from nested non-object types", () => { + const schema = { + type: "object", + properties: { + outer: { + type: "object", + properties: { + inner: { + type: "number", + properties: { bad: { type: "string" } }, + required: ["bad"], + }, + }, + }, + }, + } as any + + const result = ProviderTransform.schema(geminiModel, schema) as any + + expect(result.properties.outer.properties.inner.type).toBe("number") + expect(result.properties.outer.properties.inner.properties).toBeUndefined() + expect(result.properties.outer.properties.inner.required).toBeUndefined() + }) + + test("keeps properties and required on object types", () => { + const schema = { + type: "object", + properties: { + data: { + type: "object", + properties: { name: { type: "string" } }, + required: ["name"], + }, + }, + } as any + + const result = ProviderTransform.schema(geminiModel, schema) as any + + expect(result.properties.data.type).toBe("object") + expect(result.properties.data.properties).toBeDefined() + expect(result.properties.data.required).toEqual(["name"]) + }) + + test("does not affect non-gemini providers", () => { + const openaiModel = { + providerID: "openai", + api: { + id: "gpt-4", + }, + } as any + + const schema = { + type: "object", + properties: { + data: { + type: "string", + properties: { invalid: { type: "string" } }, + }, + }, + } as any + + const result = ProviderTransform.schema(openaiModel, schema) as any + + expect(result.properties.data.properties).toBeDefined() + }) +}) + describe("ProviderTransform.message - DeepSeek reasoning content", () => { test("DeepSeek with tool calls includes reasoning_content in providerOptions", () => { const msgs = [