Skip to content
Merged
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
35 changes: 32 additions & 3 deletions packages/opencode/src/provider/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
}
Expand Down
29 changes: 23 additions & 6 deletions packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
},
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down