diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 28e8d97ab6d..eafe382aba9 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -621,6 +621,35 @@ export namespace ProviderTransform { } */ + // Ensure 'required' is always an array for object types to satisfy strict backends (e.g., SGLang) + const ensureRequiredField = (obj: any): any => { + if (obj === null || typeof obj !== "object") { + return obj + } + + if (Array.isArray(obj)) { + return obj.map(ensureRequiredField) + } + + const result: any = {} + for (const [key, value] of Object.entries(obj)) { + if (typeof value === "object" && value !== null) { + result[key] = ensureRequiredField(value) + } else { + result[key] = value + } + } + + // Add required: [] for object types that don't have it + if (result.type === "object" && !result.required) { + result.required = [] + } + + return result + } + + schema = ensureRequiredField(schema) + // Convert integer enums to string enums for Google/Gemini if (model.providerID === "google" || model.api.id.includes("gemini")) { const sanitizeGemini = (obj: any): any => { diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 32b1ecb2444..2c2e59a93cb 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -167,6 +167,106 @@ describe("ProviderTransform.maxOutputTokens", () => { }) }) +describe("ProviderTransform.schema - required field for strict backends", () => { + const genericModel = { + providerID: "openai-compatible", + api: { + id: "custom-model", + }, + } as any + + test("adds required: [] to empty object schemas", () => { + const schema = { + type: "object", + properties: {}, + } as any + + const result = ProviderTransform.schema(genericModel, schema) as any + + expect(result.required).toEqual([]) + }) + + test("preserves existing required field", () => { + const schema = { + type: "object", + properties: { + name: { type: "string" }, + }, + required: ["name"], + } as any + + const result = ProviderTransform.schema(genericModel, schema) as any + + expect(result.required).toEqual(["name"]) + }) + + test("adds required: [] to nested object schemas", () => { + const schema = { + type: "object", + properties: { + nested: { + type: "object", + properties: {}, + }, + }, + } as any + + const result = ProviderTransform.schema(genericModel, schema) as any + + expect(result.required).toEqual([]) + expect(result.properties.nested.required).toEqual([]) + }) + + test("handles deeply nested object schemas", () => { + const schema = { + type: "object", + properties: { + level1: { + type: "object", + properties: { + level2: { + type: "object", + properties: { + value: { type: "string" }, + }, + }, + }, + }, + }, + } as any + + const result = ProviderTransform.schema(genericModel, schema) as any + + expect(result.required).toEqual([]) + expect(result.properties.level1.required).toEqual([]) + expect(result.properties.level1.properties.level2.required).toEqual([]) + }) + + test("handles array of objects", () => { + const schema = { + type: "array", + items: { + type: "object", + properties: {}, + }, + } as any + + const result = ProviderTransform.schema(genericModel, schema) as any + + expect(result.items.required).toEqual([]) + }) + + test("does not add required to non-object types", () => { + const schema = { + type: "string", + } as any + + const result = ProviderTransform.schema(genericModel, schema) as any + + expect(result.required).toBeUndefined() + }) +}) + describe("ProviderTransform.schema - gemini array items", () => { test("adds missing items for array properties", () => { const geminiModel = {