summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorRami Chowdhury <[email protected]>2025-07-18 16:02:54 -0400
committerGitHub <[email protected]>2025-07-18 16:02:54 -0400
commitf1da70b1de24ba006d9c4577315e147fcd1a06f3 (patch)
treea2db28602f75ac1acc2cc12260e40786a58e2b30 /packages
parent5c9d1910afcf2a7a24582819fdd784e274e85f9a (diff)
downloadopencode-f1da70b1de24ba006d9c4577315e147fcd1a06f3.tar.gz
opencode-f1da70b1de24ba006d9c4577315e147fcd1a06f3.zip
feat(provider): add Gemini tool schema sanitization (#1132)
Diffstat (limited to 'packages')
-rw-r--r--packages/opencode/src/provider/provider.ts59
1 files changed, 58 insertions, 1 deletions
diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts
index 43e52ef45..df3a0e481 100644
--- a/packages/opencode/src/provider/provider.ts
+++ b/packages/opencode/src/provider/provider.ts
@@ -494,7 +494,10 @@ export namespace Provider {
...t,
parameters: optionalToNullable(t.parameters),
})),
- google: TOOLS,
+ google: TOOLS.map((t) => ({
+ ...t,
+ parameters: sanitizeGeminiParameters(t.parameters),
+ })),
}
export async function tools(providerID: string) {
@@ -508,6 +511,60 @@ export namespace Provider {
return TOOL_MAPPING[providerID] ?? TOOLS
}
+ function sanitizeGeminiParameters(schema: z.ZodTypeAny, visited = new Set()): z.ZodTypeAny {
+ if (!schema || visited.has(schema)) {
+ return schema
+ }
+ visited.add(schema)
+
+ if (schema instanceof z.ZodDefault) {
+ const innerSchema = schema.removeDefault()
+ // Handle Gemini's incompatibility with `default` on `anyOf` (unions).
+ if (innerSchema instanceof z.ZodUnion) {
+ // The schema was `z.union(...).default(...)`, which is not allowed.
+ // We strip the default and return the sanitized union.
+ return sanitizeGeminiParameters(innerSchema, visited)
+ }
+ // Otherwise, the default is on a regular type, which is allowed.
+ // We recurse on the inner type and then re-apply the default.
+ return sanitizeGeminiParameters(innerSchema, visited).default(schema._def.defaultValue())
+ }
+
+ if (schema instanceof z.ZodOptional) {
+ return z.optional(sanitizeGeminiParameters(schema.unwrap(), visited))
+ }
+
+ if (schema instanceof z.ZodObject) {
+ const newShape: Record<string, z.ZodTypeAny> = {}
+ for (const [key, value] of Object.entries(schema.shape)) {
+ newShape[key] = sanitizeGeminiParameters(value as z.ZodTypeAny, visited)
+ }
+ return z.object(newShape)
+ }
+
+ if (schema instanceof z.ZodArray) {
+ return z.array(sanitizeGeminiParameters(schema.element, visited))
+ }
+
+ if (schema instanceof z.ZodUnion) {
+ // This schema corresponds to `anyOf` in JSON Schema.
+ // We recursively sanitize each option in the union.
+ const sanitizedOptions = schema.options.map((option: z.ZodTypeAny) => sanitizeGeminiParameters(option, visited))
+ return z.union(sanitizedOptions as [z.ZodTypeAny, z.ZodTypeAny, ...z.ZodTypeAny[]])
+ }
+
+ if (schema instanceof z.ZodString) {
+ const newSchema = z.string({ description: schema.description })
+ const safeChecks = ["min", "max", "length", "regex", "startsWith", "endsWith", "includes", "trim"]
+ // rome-ignore lint/suspicious/noExplicitAny: <explanation>
+ ;(newSchema._def as any).checks = (schema._def as z.ZodStringDef).checks.filter((check) =>
+ safeChecks.includes(check.kind),
+ )
+ return newSchema
+ }
+
+ return schema
+ }
function optionalToNullable(schema: z.ZodTypeAny): z.ZodTypeAny {
if (schema instanceof z.ZodObject) {
const shape = schema.shape