Skip to content

Commit 0ca165b

Browse files
committed
Pass tools to convertToModelMessages to enable toModelOutput
This is the critical missing piece - without passing tools to convertToModelMessages, the toModelOutput function is never called and images are sent as JSON instead of media content. Added test to verify image tool results are converted to media content when tools are provided to convertToModelMessages.
1 parent 5dfc0e2 commit 0ca165b

File tree

2 files changed

+98
-1
lines changed

2 files changed

+98
-1
lines changed

src/services/aiService.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -455,9 +455,13 @@ export class AIService extends EventEmitter {
455455
log.debug_obj(`${workspaceId}/2a_redacted_messages.json`, redactedForProvider);
456456

457457
// Convert CmuxMessage to ModelMessage format using Vercel AI SDK utility
458+
// Pass earlyTools so convertToModelMessages can use toModelOutput for tool results
459+
// (earlyTools has stub config but same tool definitions with toModelOutput functions)
458460
// Type assertion needed because CmuxMessage has custom tool parts for interrupted tools
459461
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
460-
const modelMessages = convertToModelMessages(redactedForProvider as any);
462+
const modelMessages = convertToModelMessages(redactedForProvider as any, {
463+
tools: earlyTools,
464+
});
461465
log.debug_obj(`${workspaceId}/2_model_messages.json`, modelMessages);
462466

463467
// Apply ModelMessage transforms based on provider requirements
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { describe, it, expect } from "bun:test";
2+
import { convertToModelMessages } from "ai";
3+
import { createFileReadTool } from "@/services/tools/file_read";
4+
import type { UIMessage } from "ai";
5+
import * as fs from "fs";
6+
import * as path from "path";
7+
import * as os from "os";
8+
9+
describe("convertToModelMessages with tools", () => {
10+
it("should use toModelOutput for image file_read results", async () => {
11+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "convert-test-"));
12+
13+
try {
14+
// Create a minimal PNG
15+
const png = Buffer.from([
16+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44,
17+
0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1f,
18+
0x15, 0xc4, 0x89, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x63, 0x00,
19+
0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0d, 0x0a, 0x2d, 0xb4, 0x00, 0x00, 0x00, 0x00, 0x49,
20+
0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
21+
]);
22+
const imgPath = path.join(tmpDir, "test.png");
23+
fs.writeFileSync(imgPath, png);
24+
25+
// Create tool and execute
26+
const tool = createFileReadTool({ cwd: tmpDir, tempDir: tmpDir });
27+
const result = await tool.execute!(
28+
{ filePath: imgPath },
29+
{ toolCallId: "test", messages: [] }
30+
);
31+
32+
// Create a message with tool result
33+
const messages: UIMessage[] = [
34+
{
35+
id: "1",
36+
role: "user",
37+
parts: [{ type: "text", text: "Read image" }],
38+
},
39+
{
40+
id: "2",
41+
role: "assistant",
42+
parts: [
43+
{
44+
type: "dynamic-tool",
45+
toolCallId: "call_1",
46+
toolName: "file_read",
47+
state: "output-available",
48+
input: { filePath: imgPath },
49+
output: result,
50+
},
51+
],
52+
},
53+
];
54+
55+
// Convert without tools - should get JSON
56+
const withoutTools = convertToModelMessages(messages);
57+
const toolMessage = withoutTools.find((m) => m.role === "tool");
58+
expect(toolMessage).toBeDefined();
59+
if (toolMessage && toolMessage.role === "tool") {
60+
const content = toolMessage.content[0];
61+
expect(content.type).toBe("tool-result");
62+
if (content.type === "tool-result") {
63+
// Without tools, output should be JSON
64+
expect(content.output.type).toBe("json");
65+
}
66+
}
67+
68+
// Convert with tools - should use toModelOutput and get media content
69+
const withTools = convertToModelMessages(messages, {
70+
tools: { file_read: tool },
71+
});
72+
const toolMessageWithTools = withTools.find((m) => m.role === "tool");
73+
expect(toolMessageWithTools).toBeDefined();
74+
if (toolMessageWithTools && toolMessageWithTools.role === "tool") {
75+
const content = toolMessageWithTools.content[0];
76+
expect(content.type).toBe("tool-result");
77+
if (content.type === "tool-result") {
78+
// With tools, toModelOutput should convert images to media content
79+
expect(content.output.type).toBe("content");
80+
if (content.output.type === "content") {
81+
expect(content.output.value).toHaveLength(1);
82+
expect(content.output.value[0].type).toBe("media");
83+
if (content.output.value[0].type === "media") {
84+
expect(content.output.value[0].mediaType).toBe("image/png");
85+
}
86+
}
87+
}
88+
}
89+
} finally {
90+
fs.rmSync(tmpDir, { recursive: true, force: true });
91+
}
92+
});
93+
});

0 commit comments

Comments
 (0)