summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAiden Cline <[email protected]>2026-01-21 23:54:39 -0600
committerAiden Cline <[email protected]>2026-01-21 23:54:44 -0600
commitc2844697f38807d928368fdcd1e195e84a079077 (patch)
treeb80d012f016ef5de4684dda27960c236a1fccb59
parentfc0210c2fdd3194754dbe1eeff094e3038ffecbc (diff)
downloadopencode-c2844697f38807d928368fdcd1e195e84a079077.tar.gz
opencode-c2844697f38807d928368fdcd1e195e84a079077.zip
fix: ensure images are properly returned as tool results
-rw-r--r--packages/opencode/src/session/message-v2.ts75
-rw-r--r--packages/opencode/src/session/prompt.ts12
-rw-r--r--packages/opencode/test/session/message-v2.test.ts24
3 files changed, 64 insertions, 47 deletions
diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts
index d0f2beb74..83ca72add 100644
--- a/packages/opencode/src/session/message-v2.ts
+++ b/packages/opencode/src/session/message-v2.ts
@@ -435,6 +435,40 @@ export namespace MessageV2 {
export function toModelMessages(input: WithParts[], model: Provider.Model): ModelMessage[] {
const result: UIMessage[] = []
+ const toolNames = new Set<string>()
+
+ const toModelOutput = (output: unknown) => {
+ if (typeof output === "string") {
+ return { type: "text", value: output }
+ }
+
+ if (typeof output === "object") {
+ const outputObject = output as {
+ text: string
+ attachments?: Array<{ mime: string; url: string }>
+ }
+ const attachments = (outputObject.attachments ?? []).filter((attachment) => {
+ return attachment.url.startsWith("data:") && attachment.url.includes(",")
+ })
+
+ return {
+ type: "content",
+ value: [
+ { type: "text", text: outputObject.text },
+ ...attachments.map((attachment) => ({
+ type: "media",
+ mediaType: attachment.mime,
+ data: iife(() => {
+ const commaIndex = attachment.url.indexOf(",")
+ return commaIndex === -1 ? attachment.url : attachment.url.slice(commaIndex + 1)
+ }),
+ })),
+ ],
+ }
+ }
+
+ return { type: "json", value: output as never }
+ }
for (const msg of input) {
if (msg.parts.length === 0) continue
@@ -505,31 +539,24 @@ export namespace MessageV2 {
type: "step-start",
})
if (part.type === "tool") {
+ toolNames.add(part.tool)
if (part.state.status === "completed") {
- if (part.state.attachments?.length) {
- result.push({
- id: Identifier.ascending("message"),
- role: "user",
- parts: [
- {
- type: "text",
- text: `The tool ${part.tool} returned the following attachments:`,
- },
- ...part.state.attachments.map((attachment) => ({
- type: "file" as const,
- url: attachment.url,
- mediaType: attachment.mime,
- filename: attachment.filename,
- })),
- ],
- })
- }
+ const outputText = part.state.time.compacted ? "[Old tool result content cleared]" : part.state.output
+ const attachments = part.state.time.compacted ? [] : (part.state.attachments ?? [])
+ const output =
+ attachments.length > 0
+ ? {
+ text: outputText,
+ attachments,
+ }
+ : outputText
+
assistantMessage.parts.push({
type: ("tool-" + part.tool) as `tool-${string}`,
state: "output-available",
toolCallId: part.callID,
input: part.state.input,
- output: part.state.time.compacted ? "[Old tool result content cleared]" : part.state.output,
+ output,
...(differentModel ? {} : { callProviderMetadata: part.metadata }),
})
}
@@ -568,7 +595,15 @@ export namespace MessageV2 {
}
}
- return convertToModelMessages(result.filter((msg) => msg.parts.some((part) => part.type !== "step-start")))
+ const tools = Object.fromEntries(Array.from(toolNames).map((toolName) => [toolName, { toModelOutput }]))
+
+ return convertToModelMessages(
+ result.filter((msg) => msg.parts.some((part) => part.type !== "step-start")),
+ {
+ //@ts-expect-error (convertToModelMessages expects a ToolSet but only actually needs tools[name]?.toModelOutput)
+ tools,
+ },
+ )
}
export const stream = fn(Identifier.schema("session"), async function* (sessionID) {
diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts
index 185c97a75..de6278820 100644
--- a/packages/opencode/src/session/prompt.ts
+++ b/packages/opencode/src/session/prompt.ts
@@ -722,12 +722,6 @@ export namespace SessionPrompt {
)
return result
},
- toModelOutput(result) {
- return {
- type: "text",
- value: result.output,
- }
- },
})
}
@@ -819,12 +813,6 @@ export namespace SessionPrompt {
content: result.content, // directly return content to preserve ordering when outputting to model
}
}
- item.toModelOutput = (result) => {
- return {
- type: "text",
- value: result.output,
- }
- }
tools[key] = item
}
diff --git a/packages/opencode/test/session/message-v2.test.ts b/packages/opencode/test/session/message-v2.test.ts
index b8d056433..2f632ad1c 100644
--- a/packages/opencode/test/session/message-v2.test.ts
+++ b/packages/opencode/test/session/message-v2.test.ts
@@ -262,7 +262,7 @@ describe("session.message-v2.toModelMessage", () => {
])
})
- test("converts assistant tool completion into tool-call + tool-result messages and emits attachment message", () => {
+ test("converts assistant tool completion into tool-call + tool-result messages with attachments", () => {
const userID = "m-user"
const assistantID = "m-assistant"
@@ -304,7 +304,7 @@ describe("session.message-v2.toModelMessage", () => {
type: "file",
mime: "image/png",
filename: "attachment.png",
- url: "https://example.com/attachment.png",
+ url: "data:image/png;base64,Zm9v",
},
],
},
@@ -320,18 +320,6 @@ describe("session.message-v2.toModelMessage", () => {
content: [{ type: "text", text: "run tool" }],
},
{
- role: "user",
- content: [
- { type: "text", text: "The tool bash returned the following attachments:" },
- {
- type: "file",
- mediaType: "image/png",
- filename: "attachment.png",
- data: "https://example.com/attachment.png",
- },
- ],
- },
- {
role: "assistant",
content: [
{ type: "text", text: "done", providerOptions: { openai: { assistant: "meta" } } },
@@ -352,7 +340,13 @@ describe("session.message-v2.toModelMessage", () => {
type: "tool-result",
toolCallId: "call-1",
toolName: "bash",
- output: { type: "text", value: "ok" },
+ output: {
+ type: "content",
+ value: [
+ { type: "text", text: "ok" },
+ { type: "media", mediaType: "image/png", data: "Zm9v" },
+ ],
+ },
providerOptions: { openai: { tool: "meta" } },
},
],