Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions packages/opencode/src/provider/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down
100 changes: 100 additions & 0 deletions packages/opencode/test/provider/transform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down