Skip to content

Commit cd39190

Browse files
committed
fix(session): fold schema items into gemini union branches
1 parent da4730e commit cd39190

1 file changed

Lines changed: 55 additions & 1 deletion

File tree

packages/opencode/src/session/llm/request.ts

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,18 @@ export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: Pre
181181
return {
182182
system,
183183
messages,
184-
tools: Object.fromEntries(Object.entries(tools).toSorted(([a], [b]) => a.localeCompare(b))),
184+
tools: Object.fromEntries(
185+
Object.entries(tools)
186+
.toSorted(([a], [b]) => a.localeCompare(b))
187+
.map(([name, tool]) => {
188+
if (input.model.api.npm === "@ai-sdk/google" || input.model.api.npm === "@ai-sdk/google-vertex") {
189+
const schema = tool.inputSchema as unknown
190+
const plain = isRecord(schema) && isRecord(schema.jsonSchema) ? schema.jsonSchema : isRecord(schema) ? schema : undefined
191+
if (plain) foldArrayItems(plain)
192+
}
193+
return [name, tool]
194+
}),
195+
),
185196
params,
186197
messageTransformOptions: options,
187198
headers: {
@@ -213,6 +224,49 @@ function resolveTools(input: Pick<PrepareInput, "tools" | "agent" | "permission"
213224
return Record.filter(input.tools, (_, k) => input.user.tools?.[k] !== false && !disabled.has(k))
214225
}
215226

227+
const isRecord = (value: unknown): value is Record<string, unknown> =>
228+
typeof value === "object" && value !== null && !Array.isArray(value)
229+
230+
// @ai-sdk/google's convertJSONSchemaToOpenAPISchema splits a nullable array
231+
// written as `type: ["null", "array"]` into `anyOf: [{ type: "array" }]` but
232+
// leaves a sibling `items` dangling at the parent, which Gemini rejects. Fold
233+
// `items` into the array-typed branches of any union so the generated
234+
// function declaration carries `items` inside the array branch. Mutates the
235+
// plain schema object in place so the surrounding jsonSchema() wrapper (whose
236+
// `jsonSchema` getter returns this same reference) keeps working.
237+
const foldArrayItems = (schema: unknown): void => {
238+
if (Array.isArray(schema)) {
239+
for (const item of schema) foldArrayItems(item)
240+
return
241+
}
242+
if (!isRecord(schema)) return
243+
for (const value of Object.values(schema)) foldArrayItems(value)
244+
if (schema.items === undefined) return
245+
const type = schema.type
246+
if (Array.isArray(type) && type.includes("array")) {
247+
const arrayBranch: Record<string, unknown> = { type: "array", items: schema.items }
248+
schema.anyOf = type.includes("null") ? [arrayBranch, { type: "null" }] : [arrayBranch]
249+
delete schema.type
250+
delete schema.items
251+
return
252+
}
253+
const combiner = ["anyOf", "oneOf", "allOf"].find((key) => Array.isArray(schema[key]))
254+
if (!combiner) return
255+
const branches = schema[combiner]
256+
if (!Array.isArray(branches)) return
257+
for (const branch of branches) {
258+
if (!isRecord(branch)) continue
259+
const branchType = branch.type
260+
if (
261+
(branchType === "array" || (Array.isArray(branchType) && branchType.includes("array"))) &&
262+
branch.items === undefined
263+
) {
264+
branch.items = schema.items
265+
}
266+
}
267+
delete schema.items
268+
}
269+
216270
export function hasToolCalls(messages: ModelMessage[]): boolean {
217271
for (const msg of messages) {
218272
if (!Array.isArray(msg.content)) continue

0 commit comments

Comments
 (0)