Skip to content

Commit 1798134

Browse files
daniel-lxsclaude
andcommitted
fix: eliminate type safety holes from RooMessage migration
Replace `as any` / `as unknown as` casts with proper RooMessage types across the storage and API pipeline: - Add UserContentPart alias and content-part type guards to rooMessage.ts - Migrate maybeRemoveImageBlocks, formatResponse, processUserContentMentions, validateAndFixToolResultIds, condense system, and convertToOpenAiMessages from Anthropic SDK types to AI SDK RooMessage types - Change initiateTaskLoop/recursivelyMakeClineRequests signatures from Anthropic.Messages.ContentBlockParam[] to UserContentPart[] - Type the assistant message builder in Task.ts, remove double-casts in the API pipeline - Remove unused Anthropic imports from 7 source files - Update ToolResponse type and presentAssistantMessage to use ImagePart - All functions accept both AI SDK (tool-call/tool-result) and legacy Anthropic (tool_use/tool_result) formats for backward compatibility Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
1 parent abda7ed commit 1798134

File tree

24 files changed

+815
-1064
lines changed

24 files changed

+815
-1064
lines changed

src/api/providers/base-openai-compatible-provider.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { Anthropic } from "@anthropic-ai/sdk"
21
import OpenAI from "openai"
32

43
import type { ModelInfo } from "@roo-code/types"
@@ -91,7 +90,7 @@ export abstract class BaseOpenAiCompatibleProvider<ModelName extends string>
9190
model,
9291
max_tokens,
9392
temperature,
94-
messages: [{ role: "system" as const, content: systemPrompt }, ...convertToOpenAiMessages(messages as any)],
93+
messages: [{ role: "system" as const, content: systemPrompt }, ...convertToOpenAiMessages(messages)],
9594
stream: true,
9695
stream_options: { include_usage: true },
9796
tools: this.convertToolsForOpenAI(metadata?.tools),

src/api/transform/__tests__/image-cleaning.spec.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import type { ModelInfo } from "@roo-code/types"
44

55
import { ApiHandler } from "../../index"
6-
import { ApiMessage } from "../../../core/task-persistence/apiMessages"
76
import { maybeRemoveImageBlocks } from "../image-cleaning"
87

98
describe("maybeRemoveImageBlocks", () => {
@@ -24,7 +23,7 @@ describe("maybeRemoveImageBlocks", () => {
2423

2524
it("should handle empty messages array", () => {
2625
const apiHandler = createMockApiHandler(true)
27-
const messages: ApiMessage[] = []
26+
const messages: any[] = []
2827

2928
const result = maybeRemoveImageBlocks(messages, apiHandler)
3029

@@ -34,7 +33,7 @@ describe("maybeRemoveImageBlocks", () => {
3433

3534
it("should not modify messages with no image blocks", () => {
3635
const apiHandler = createMockApiHandler(true)
37-
const messages: ApiMessage[] = [
36+
const messages: any[] = [
3837
{
3938
role: "user",
4039
content: "Hello, world!",
@@ -53,7 +52,7 @@ describe("maybeRemoveImageBlocks", () => {
5352

5453
it("should not modify messages with array content but no image blocks", () => {
5554
const apiHandler = createMockApiHandler(true)
56-
const messages: ApiMessage[] = [
55+
const messages: any[] = [
5756
{
5857
role: "user",
5958
content: [
@@ -77,7 +76,7 @@ describe("maybeRemoveImageBlocks", () => {
7776

7877
it("should not modify image blocks when API handler supports images", () => {
7978
const apiHandler = createMockApiHandler(true)
80-
const messages: ApiMessage[] = [
79+
const messages: any[] = [
8180
{
8281
role: "user",
8382
content: [
@@ -106,7 +105,7 @@ describe("maybeRemoveImageBlocks", () => {
106105

107106
it("should convert image blocks to text descriptions when API handler doesn't support images", () => {
108107
const apiHandler = createMockApiHandler(false)
109-
const messages: ApiMessage[] = [
108+
const messages: any[] = [
110109
{
111110
role: "user",
112111
content: [
@@ -149,7 +148,7 @@ describe("maybeRemoveImageBlocks", () => {
149148

150149
it("should handle mixed content messages with multiple text and image blocks", () => {
151150
const apiHandler = createMockApiHandler(false)
152-
const messages: ApiMessage[] = [
151+
const messages: any[] = [
153152
{
154153
role: "user",
155154
content: [
@@ -212,7 +211,7 @@ describe("maybeRemoveImageBlocks", () => {
212211

213212
it("should handle multiple messages with image blocks", () => {
214213
const apiHandler = createMockApiHandler(false)
215-
const messages: ApiMessage[] = [
214+
const messages: any[] = [
216215
{
217216
role: "user",
218217
content: [
@@ -293,7 +292,7 @@ describe("maybeRemoveImageBlocks", () => {
293292

294293
it("should preserve additional message properties", () => {
295294
const apiHandler = createMockApiHandler(false)
296-
const messages: ApiMessage[] = [
295+
const messages: any[] = [
297296
{
298297
role: "user",
299298
content: [

src/api/transform/__tests__/openai-format.spec.ts

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// npx vitest run api/transform/__tests__/openai-format.spec.ts
22

3-
import { Anthropic } from "@anthropic-ai/sdk"
43
import OpenAI from "openai"
4+
import type { RooMessage } from "../../../core/task-persistence/rooMessage"
55

66
import {
77
convertToOpenAiMessages,
@@ -13,7 +13,7 @@ import { normalizeMistralToolCallId } from "../mistral-format"
1313

1414
describe("convertToOpenAiMessages", () => {
1515
it("should convert simple text messages", () => {
16-
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
16+
const anthropicMessages: any[] = [
1717
{
1818
role: "user",
1919
content: "Hello",
@@ -37,7 +37,7 @@ describe("convertToOpenAiMessages", () => {
3737
})
3838

3939
it("should handle messages with image content", () => {
40-
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
40+
const anthropicMessages: any[] = [
4141
{
4242
role: "user",
4343
content: [
@@ -77,7 +77,7 @@ describe("convertToOpenAiMessages", () => {
7777
})
7878

7979
it("should handle assistant messages with tool use (no normalization without normalizeToolCallId)", () => {
80-
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
80+
const anthropicMessages: any[] = [
8181
{
8282
role: "assistant",
8383
content: [
@@ -113,7 +113,7 @@ describe("convertToOpenAiMessages", () => {
113113
})
114114

115115
it("should handle user messages with tool results (no normalization without normalizeToolCallId)", () => {
116-
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
116+
const anthropicMessages: any[] = [
117117
{
118118
role: "user",
119119
content: [
@@ -136,7 +136,7 @@ describe("convertToOpenAiMessages", () => {
136136
})
137137

138138
it("should normalize tool call IDs when normalizeToolCallId function is provided", () => {
139-
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
139+
const anthropicMessages: any[] = [
140140
{
141141
role: "assistant",
142142
content: [
@@ -173,7 +173,7 @@ describe("convertToOpenAiMessages", () => {
173173
})
174174

175175
it("should not normalize tool call IDs when normalizeToolCallId function is not provided", () => {
176-
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
176+
const anthropicMessages: any[] = [
177177
{
178178
role: "assistant",
179179
content: [
@@ -208,7 +208,7 @@ describe("convertToOpenAiMessages", () => {
208208
})
209209

210210
it("should use custom normalization function when provided", () => {
211-
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
211+
const anthropicMessages: any[] = [
212212
{
213213
role: "assistant",
214214
content: [
@@ -235,7 +235,7 @@ describe("convertToOpenAiMessages", () => {
235235
// have content set to "" instead of undefined. Gemini (via OpenRouter) requires
236236
// every message to have at least one "parts" field, which fails if content is undefined.
237237
// See: ROO-425
238-
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
238+
const anthropicMessages: any[] = [
239239
{
240240
role: "assistant",
241241
content: [
@@ -265,7 +265,7 @@ describe("convertToOpenAiMessages", () => {
265265
// of an empty string. Gemini (via OpenRouter) requires function responses to have
266266
// non-empty content in the "parts" field, and an empty string causes validation failure
267267
// with error: "Unable to submit request because it must include at least one parts field"
268-
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
268+
const anthropicMessages: any[] = [
269269
{
270270
role: "user",
271271
content: [
@@ -289,15 +289,15 @@ describe("convertToOpenAiMessages", () => {
289289
})
290290

291291
it('should use "(empty)" placeholder for tool result with undefined content (Gemini compatibility)', () => {
292-
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
292+
const anthropicMessages: any[] = [
293293
{
294294
role: "user",
295295
content: [
296296
{
297297
type: "tool_result",
298298
tool_use_id: "tool-456",
299299
// content is undefined/not provided
300-
} as Anthropic.ToolResultBlockParam,
300+
},
301301
],
302302
},
303303
]
@@ -311,15 +311,15 @@ describe("convertToOpenAiMessages", () => {
311311
})
312312

313313
it('should use "(empty)" placeholder for tool result with empty array content (Gemini compatibility)', () => {
314-
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
314+
const anthropicMessages: any[] = [
315315
{
316316
role: "user",
317317
content: [
318318
{
319319
type: "tool_result",
320320
tool_use_id: "tool-789",
321321
content: [], // Empty array
322-
} as Anthropic.ToolResultBlockParam,
322+
},
323323
],
324324
},
325325
]
@@ -337,7 +337,7 @@ describe("convertToOpenAiMessages", () => {
337337
// This test ensures that user messages with empty text blocks are filtered out
338338
// to prevent "must include at least one parts field" error from Gemini (via OpenRouter).
339339
// Empty text blocks can occur in edge cases during message construction.
340-
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
340+
const anthropicMessages: any[] = [
341341
{
342342
role: "user",
343343
content: [
@@ -365,7 +365,7 @@ describe("convertToOpenAiMessages", () => {
365365

366366
it("should not create user message when all text blocks are empty (Gemini compatibility)", () => {
367367
// If all text blocks are empty, no user message should be created
368-
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
368+
const anthropicMessages: any[] = [
369369
{
370370
role: "user",
371371
content: [
@@ -387,7 +387,7 @@ describe("convertToOpenAiMessages", () => {
387387
})
388388

389389
it("should preserve image blocks when filtering empty text blocks", () => {
390-
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
390+
const anthropicMessages: any[] = [
391391
{
392392
role: "user",
393393
content: [
@@ -426,7 +426,7 @@ describe("convertToOpenAiMessages", () => {
426426

427427
describe("mergeToolResultText option", () => {
428428
it("should merge text content into last tool message when mergeToolResultText is true", () => {
429-
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
429+
const anthropicMessages: any[] = [
430430
{
431431
role: "user",
432432
content: [
@@ -456,7 +456,7 @@ describe("convertToOpenAiMessages", () => {
456456
})
457457

458458
it("should merge text into last tool message when multiple tool results exist", () => {
459-
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
459+
const anthropicMessages: any[] = [
460460
{
461461
role: "user",
462462
content: [
@@ -489,7 +489,7 @@ describe("convertToOpenAiMessages", () => {
489489
})
490490

491491
it("should NOT merge text when images are present (fall back to user message)", () => {
492-
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
492+
const anthropicMessages: any[] = [
493493
{
494494
role: "user",
495495
content: [
@@ -519,7 +519,7 @@ describe("convertToOpenAiMessages", () => {
519519
})
520520

521521
it("should create separate user message when mergeToolResultText is false", () => {
522-
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
522+
const anthropicMessages: any[] = [
523523
{
524524
role: "user",
525525
content: [
@@ -548,7 +548,7 @@ describe("convertToOpenAiMessages", () => {
548548
})
549549

550550
it("should work with normalizeToolCallId when mergeToolResultText is true", () => {
551-
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
551+
const anthropicMessages: any[] = [
552552
{
553553
role: "user",
554554
content: [
@@ -581,7 +581,7 @@ describe("convertToOpenAiMessages", () => {
581581
})
582582

583583
it("should handle user messages with only text content (no tool results)", () => {
584-
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
584+
const anthropicMessages: any[] = [
585585
{
586586
role: "user",
587587
content: [
@@ -906,7 +906,7 @@ describe("convertToOpenAiMessages", () => {
906906
})
907907

908908
it("should handle messages without reasoning_details", () => {
909-
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
909+
const anthropicMessages: any[] = [
910910
{
911911
role: "assistant",
912912
content: [{ type: "text", text: "Simple response" }],
Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,32 @@
1-
import { ApiMessage } from "../../core/task-persistence/apiMessages"
1+
import { type RooMessage } from "../../core/task-persistence/rooMessage"
22

33
import { ApiHandler } from "../index"
44

55
/* Removes image blocks from messages if they are not supported by the Api Handler */
6-
export function maybeRemoveImageBlocks(messages: ApiMessage[], apiHandler: ApiHandler): ApiMessage[] {
6+
export function maybeRemoveImageBlocks(messages: RooMessage[], apiHandler: ApiHandler): RooMessage[] {
77
// Check model capability ONCE instead of for every message
88
const supportsImages = apiHandler.getModel().info.supportsImages
99

10+
if (supportsImages) {
11+
return messages
12+
}
13+
1014
return messages.map((message) => {
11-
// Handle array content (could contain image blocks).
12-
let { content } = message
13-
if (Array.isArray(content)) {
14-
if (!supportsImages) {
15-
// Convert image blocks to text descriptions.
16-
content = content.map((block) => {
17-
if (block.type === "image") {
18-
// Convert image blocks to text descriptions.
19-
// Note: We can't access the actual image content/url due to API limitations,
20-
// but we can indicate that an image was present in the conversation.
21-
return {
22-
type: "text",
23-
text: "[Referenced image in conversation]",
24-
}
25-
}
26-
return block
27-
})
28-
}
15+
// Only process messages with a role and array content
16+
if (!("role" in message) || !Array.isArray(message.content)) {
17+
return message
2918
}
30-
return { ...message, content }
19+
20+
const content = message.content.map((block: any) => {
21+
if (block.type === "image") {
22+
return {
23+
type: "text" as const,
24+
text: "[Referenced image in conversation]",
25+
}
26+
}
27+
return block
28+
})
29+
30+
return { ...message, content } as typeof message
3131
})
3232
}

0 commit comments

Comments
 (0)