Skip to content

Commit db4448f

Browse files
committed
refactor: extract filterNonAnthropicBlocks to shared utility
- Created src/api/transform/anthropic-filter.ts with shared filtering logic - Updated anthropic.ts and anthropic-vertex.ts to import from shared utility - Added tests for the shared utility (9 new tests) - Eliminates code duplication between the two handlers
1 parent c810bc5 commit db4448f

File tree

4 files changed

+176
-88
lines changed

4 files changed

+176
-88
lines changed

src/api/providers/anthropic-vertex.ts

Lines changed: 1 addition & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -16,54 +16,11 @@ import { safeJsonParse } from "../../shared/safeJsonParse"
1616
import { ApiStream } from "../transform/stream"
1717
import { addCacheBreakpoints } from "../transform/caching/vertex"
1818
import { getModelParams } from "../transform/model-params"
19+
import { filterNonAnthropicBlocks } from "../transform/anthropic-filter"
1920

2021
import { BaseProvider } from "./base-provider"
2122
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
2223

23-
/**
24-
* List of content block types that are NOT valid for Anthropic API.
25-
* These are internal Roo Code types or types from other providers (e.g., Gemini's thoughtSignature).
26-
* Valid Anthropic types are: text, image, tool_use, tool_result, thinking, redacted_thinking, document
27-
*/
28-
const INVALID_ANTHROPIC_BLOCK_TYPES = new Set([
29-
"reasoning", // Internal Roo Code reasoning format
30-
"thoughtSignature", // Gemini's encrypted reasoning signature
31-
])
32-
33-
/**
34-
* Filters out non-Anthropic content blocks from messages before sending to Anthropic/Vertex API.
35-
* This handles:
36-
* - Internal "reasoning" blocks (Roo Code's internal representation)
37-
* - Gemini's "thoughtSignature" blocks (encrypted reasoning continuity tokens)
38-
*
39-
* Anthropic API only accepts: text, image, tool_use, tool_result, thinking, redacted_thinking, document
40-
*/
41-
function filterNonAnthropicBlocks(messages: Anthropic.Messages.MessageParam[]): Anthropic.Messages.MessageParam[] {
42-
return messages
43-
.map((message) => {
44-
if (typeof message.content === "string") {
45-
return message
46-
}
47-
48-
const filteredContent = message.content.filter((block) => {
49-
const blockType = (block as { type: string }).type
50-
// Filter out any block types that Anthropic doesn't recognize
51-
return !INVALID_ANTHROPIC_BLOCK_TYPES.has(blockType)
52-
})
53-
54-
// If all content was filtered out, return undefined to filter the message later
55-
if (filteredContent.length === 0) {
56-
return undefined
57-
}
58-
59-
return {
60-
...message,
61-
content: filteredContent,
62-
}
63-
})
64-
.filter((message): message is Anthropic.Messages.MessageParam => message !== undefined)
65-
}
66-
6724
// https://docs.anthropic.com/en/api/claude-on-vertex-ai
6825
export class AnthropicVertexHandler extends BaseProvider implements SingleCompletionHandler {
6926
protected options: ApiHandlerOptions

src/api/providers/anthropic.ts

Lines changed: 1 addition & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -14,55 +14,12 @@ import type { ApiHandlerOptions } from "../../shared/api"
1414

1515
import { ApiStream } from "../transform/stream"
1616
import { getModelParams } from "../transform/model-params"
17+
import { filterNonAnthropicBlocks } from "../transform/anthropic-filter"
1718

1819
import { BaseProvider } from "./base-provider"
1920
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
2021
import { calculateApiCostAnthropic } from "../../shared/cost"
2122

22-
/**
23-
* List of content block types that are NOT valid for Anthropic API.
24-
* These are internal Roo Code types or types from other providers (e.g., Gemini's thoughtSignature).
25-
* Valid Anthropic types are: text, image, tool_use, tool_result, thinking, redacted_thinking, document
26-
*/
27-
const INVALID_ANTHROPIC_BLOCK_TYPES = new Set([
28-
"reasoning", // Internal Roo Code reasoning format
29-
"thoughtSignature", // Gemini's encrypted reasoning signature
30-
])
31-
32-
/**
33-
* Filters out non-Anthropic content blocks from messages before sending to Anthropic API.
34-
* This handles:
35-
* - Internal "reasoning" blocks (Roo Code's internal representation)
36-
* - Gemini's "thoughtSignature" blocks (encrypted reasoning continuity tokens)
37-
*
38-
* Anthropic API only accepts: text, image, tool_use, tool_result, thinking, redacted_thinking, document
39-
*/
40-
function filterNonAnthropicBlocks(messages: Anthropic.Messages.MessageParam[]): Anthropic.Messages.MessageParam[] {
41-
return messages
42-
.map((message) => {
43-
if (typeof message.content === "string") {
44-
return message
45-
}
46-
47-
const filteredContent = message.content.filter((block) => {
48-
const blockType = (block as { type: string }).type
49-
// Filter out any block types that Anthropic doesn't recognize
50-
return !INVALID_ANTHROPIC_BLOCK_TYPES.has(blockType)
51-
})
52-
53-
// If all content was filtered out, return undefined to filter the message later
54-
if (filteredContent.length === 0) {
55-
return undefined
56-
}
57-
58-
return {
59-
...message,
60-
content: filteredContent,
61-
}
62-
})
63-
.filter((message): message is Anthropic.Messages.MessageParam => message !== undefined)
64-
}
65-
6623
export class AnthropicHandler extends BaseProvider implements SingleCompletionHandler {
6724
private options: ApiHandlerOptions
6825
private client: Anthropic
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { Anthropic } from "@anthropic-ai/sdk"
2+
3+
import { filterNonAnthropicBlocks, INVALID_ANTHROPIC_BLOCK_TYPES } from "../anthropic-filter"
4+
5+
describe("anthropic-filter", () => {
6+
describe("INVALID_ANTHROPIC_BLOCK_TYPES", () => {
7+
it("should contain reasoning type", () => {
8+
expect(INVALID_ANTHROPIC_BLOCK_TYPES.has("reasoning")).toBe(true)
9+
})
10+
11+
it("should contain thoughtSignature type", () => {
12+
expect(INVALID_ANTHROPIC_BLOCK_TYPES.has("thoughtSignature")).toBe(true)
13+
})
14+
15+
it("should not contain valid Anthropic types", () => {
16+
expect(INVALID_ANTHROPIC_BLOCK_TYPES.has("text")).toBe(false)
17+
expect(INVALID_ANTHROPIC_BLOCK_TYPES.has("image")).toBe(false)
18+
expect(INVALID_ANTHROPIC_BLOCK_TYPES.has("tool_use")).toBe(false)
19+
expect(INVALID_ANTHROPIC_BLOCK_TYPES.has("tool_result")).toBe(false)
20+
})
21+
})
22+
23+
describe("filterNonAnthropicBlocks", () => {
24+
it("should pass through messages with string content", () => {
25+
const messages: Anthropic.Messages.MessageParam[] = [
26+
{ role: "user", content: "Hello" },
27+
{ role: "assistant", content: "Hi there!" },
28+
]
29+
30+
const result = filterNonAnthropicBlocks(messages)
31+
32+
expect(result).toEqual(messages)
33+
})
34+
35+
it("should pass through messages with valid Anthropic blocks", () => {
36+
const messages: Anthropic.Messages.MessageParam[] = [
37+
{
38+
role: "user",
39+
content: [{ type: "text", text: "Hello" }],
40+
},
41+
{
42+
role: "assistant",
43+
content: [{ type: "text", text: "Hi there!" }],
44+
},
45+
]
46+
47+
const result = filterNonAnthropicBlocks(messages)
48+
49+
expect(result).toEqual(messages)
50+
})
51+
52+
it("should filter out reasoning blocks from messages", () => {
53+
const messages: Anthropic.Messages.MessageParam[] = [
54+
{ role: "user", content: "Hello" },
55+
{
56+
role: "assistant",
57+
content: [
58+
{ type: "reasoning" as any, text: "Internal reasoning" },
59+
{ type: "text", text: "Response" },
60+
],
61+
},
62+
]
63+
64+
const result = filterNonAnthropicBlocks(messages)
65+
66+
expect(result).toHaveLength(2)
67+
expect(result[1].content).toEqual([{ type: "text", text: "Response" }])
68+
})
69+
70+
it("should filter out thoughtSignature blocks from messages", () => {
71+
const messages: Anthropic.Messages.MessageParam[] = [
72+
{ role: "user", content: "Hello" },
73+
{
74+
role: "assistant",
75+
content: [
76+
{ type: "thoughtSignature", thoughtSignature: "encrypted-sig" } as any,
77+
{ type: "text", text: "Response" },
78+
],
79+
},
80+
]
81+
82+
const result = filterNonAnthropicBlocks(messages)
83+
84+
expect(result).toHaveLength(2)
85+
expect(result[1].content).toEqual([{ type: "text", text: "Response" }])
86+
})
87+
88+
it("should remove messages that become empty after filtering", () => {
89+
const messages: Anthropic.Messages.MessageParam[] = [
90+
{ role: "user", content: "Hello" },
91+
{
92+
role: "assistant",
93+
content: [{ type: "reasoning" as any, text: "Only reasoning" }],
94+
},
95+
{ role: "user", content: "Continue" },
96+
]
97+
98+
const result = filterNonAnthropicBlocks(messages)
99+
100+
expect(result).toHaveLength(2)
101+
expect(result[0].content).toBe("Hello")
102+
expect(result[1].content).toBe("Continue")
103+
})
104+
105+
it("should handle mixed content with multiple invalid block types", () => {
106+
const messages: Anthropic.Messages.MessageParam[] = [
107+
{
108+
role: "assistant",
109+
content: [
110+
{ type: "reasoning", text: "Reasoning" } as any,
111+
{ type: "text", text: "Text 1" },
112+
{ type: "thoughtSignature", thoughtSignature: "sig" } as any,
113+
{ type: "text", text: "Text 2" },
114+
],
115+
},
116+
]
117+
118+
const result = filterNonAnthropicBlocks(messages)
119+
120+
expect(result).toHaveLength(1)
121+
expect(result[0].content).toEqual([
122+
{ type: "text", text: "Text 1" },
123+
{ type: "text", text: "Text 2" },
124+
])
125+
})
126+
})
127+
})
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { Anthropic } from "@anthropic-ai/sdk"
2+
3+
/**
4+
* List of content block types that are NOT valid for Anthropic API.
5+
* These are internal Roo Code types or types from other providers (e.g., Gemini's thoughtSignature).
6+
* Valid Anthropic types are: text, image, tool_use, tool_result, thinking, redacted_thinking, document
7+
*/
8+
export const INVALID_ANTHROPIC_BLOCK_TYPES = new Set([
9+
"reasoning", // Internal Roo Code reasoning format
10+
"thoughtSignature", // Gemini's encrypted reasoning signature
11+
])
12+
13+
/**
14+
* Filters out non-Anthropic content blocks from messages before sending to Anthropic/Vertex API.
15+
* This handles:
16+
* - Internal "reasoning" blocks (Roo Code's internal representation)
17+
* - Gemini's "thoughtSignature" blocks (encrypted reasoning continuity tokens)
18+
*
19+
* Anthropic API only accepts: text, image, tool_use, tool_result, thinking, redacted_thinking, document
20+
*/
21+
export function filterNonAnthropicBlocks(
22+
messages: Anthropic.Messages.MessageParam[],
23+
): Anthropic.Messages.MessageParam[] {
24+
return messages
25+
.map((message) => {
26+
if (typeof message.content === "string") {
27+
return message
28+
}
29+
30+
const filteredContent = message.content.filter((block) => {
31+
const blockType = (block as { type: string }).type
32+
// Filter out any block types that Anthropic doesn't recognize
33+
return !INVALID_ANTHROPIC_BLOCK_TYPES.has(blockType)
34+
})
35+
36+
// If all content was filtered out, return undefined to filter the message later
37+
if (filteredContent.length === 0) {
38+
return undefined
39+
}
40+
41+
return {
42+
...message,
43+
content: filteredContent,
44+
}
45+
})
46+
.filter((message): message is Anthropic.Messages.MessageParam => message !== undefined)
47+
}

0 commit comments

Comments
 (0)