diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 6cf03fc06d0..5d680be4247 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -248,7 +248,7 @@ export namespace ProviderTransform { return standardLimit } - export function schema(_providerID: string, _modelID: string, schema: JSONSchema.BaseSchema) { + export function schema(providerID: string, modelID: string, schema: JSONSchema.BaseSchema) { /* if (["openai", "azure"].includes(providerID)) { if (schema.type === "object" && schema.properties) { @@ -265,10 +265,39 @@ export namespace ProviderTransform { } } } + */ + + // Convert integer enums to string enums for Google/Gemini + if (providerID === "google" || modelID.includes("gemini")) { + const convertIntEnumsToStrings = (obj: any): any => { + if (obj === null || typeof obj !== "object") { + return obj + } - if (providerID === "google") { + if (Array.isArray(obj)) { + return obj.map(convertIntEnumsToStrings) + } + + const result: any = {} + for (const [key, value] of Object.entries(obj)) { + if (key === "enum" && Array.isArray(value)) { + // Convert all enum values to strings + result[key] = value.map((v) => String(v)) + // If we have integer type with enum, change type to string + if (result.type === "integer" || result.type === "number") { + result.type = "string" + } + } else if (typeof value === "object" && value !== null) { + result[key] = convertIntEnumsToStrings(value) + } else { + result[key] = value + } + } + return result + } + + schema = convertIntEnumsToStrings(schema) } - */ return schema } diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index e6c64f96b5a..5fbeee00e06 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -591,6 +591,21 @@ export namespace SessionPrompt { // @ts-expect-error args.params.prompt = ProviderTransform.message(args.params.prompt, model.providerID, model.modelID) } + // Transform tool schemas for provider compatibility + if (args.params.tools && Array.isArray(args.params.tools)) { + args.params.tools = args.params.tools.map((tool: any) => { + // Tools at middleware level have inputSchema, not parameters + if (tool.inputSchema && typeof tool.inputSchema === "object") { + // Transform the inputSchema for provider compatibility + return { + ...tool, + inputSchema: ProviderTransform.schema(model.providerID, model.modelID, tool.inputSchema), + } + } + // If no inputSchema, return tool unchanged + return tool + }) + } return args.params }, }, @@ -730,6 +745,8 @@ export namespace SessionPrompt { if (Wildcard.all(key, enabledTools) === false) continue const execute = item.execute if (!execute) continue + + // Wrap execute to add plugin hooks and format output item.execute = async (args, opts) => { await Plugin.trigger( "tool.execute.before", @@ -757,17 +774,17 @@ export namespace SessionPrompt { const textParts: string[] = [] const attachments: MessageV2.FilePart[] = [] - for (const item of result.content) { - if (item.type === "text") { - textParts.push(item.text) - } else if (item.type === "image") { + for (const contentItem of result.content) { + if (contentItem.type === "text") { + textParts.push(contentItem.text) + } else if (contentItem.type === "image") { attachments.push({ id: Identifier.ascending("part"), sessionID: input.sessionID, messageID: input.processor.message.id, type: "file", - mime: item.mimeType, - url: `data:${item.mimeType};base64,${item.data}`, + mime: contentItem.mimeType, + url: `data:${contentItem.mimeType};base64,${contentItem.data}`, }) } // Add support for other types if needed