Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit a40b4f0

Browse files
committed
fix: transform tool blocks to text during condensing for Bedrock compatibility
- Add toolUseToText() to convert tool_use blocks to text format - Add toolResultToText() to convert tool_result blocks to text format - Add convertToolBlocksToText() to transform all tool blocks in message content - Add transformMessagesForCondensing() to apply transformation to messages - Apply transformation in summarizeConversation() before API call - Add 18 unit tests for full coverage Fixes LiteLLM/Bedrock error: 'Bedrock doesn't support tool calling without tools= param specified' When condensing conversations containing tool_use/tool_result blocks, Bedrock requires the tools parameter. By transforming these blocks to text representations, we remove this dependency while preserving semantic meaning for summarization.
1 parent 953c777 commit a40b4f0

2 files changed

Lines changed: 409 additions & 2 deletions

File tree

src/core/condense/__tests__/index.spec.ts

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ import {
1515
cleanupAfterTruncation,
1616
extractCommandBlocks,
1717
injectSyntheticToolResults,
18+
toolUseToText,
19+
toolResultToText,
20+
convertToolBlocksToText,
21+
transformMessagesForCondensing,
1822
} from "../index"
1923

2024
vi.mock("../../../api/transform/image-cleaning", () => ({
@@ -1218,3 +1222,306 @@ describe("summarizeConversation with custom settings", () => {
12181222
)
12191223
})
12201224
})
1225+
1226+
describe("toolUseToText", () => {
1227+
it("should convert tool_use block with object input to text", () => {
1228+
const block: Anthropic.Messages.ToolUseBlockParam = {
1229+
type: "tool_use",
1230+
id: "tool-123",
1231+
name: "read_file",
1232+
input: { path: "test.ts", encoding: "utf-8" },
1233+
}
1234+
1235+
const result = toolUseToText(block)
1236+
1237+
expect(result).toBe("[Tool Use: read_file]\npath: test.ts\nencoding: utf-8")
1238+
})
1239+
1240+
it("should convert tool_use block with nested object input to text", () => {
1241+
const block: Anthropic.Messages.ToolUseBlockParam = {
1242+
type: "tool_use",
1243+
id: "tool-456",
1244+
name: "write_file",
1245+
input: {
1246+
path: "output.json",
1247+
content: { key: "value", nested: { a: 1 } },
1248+
},
1249+
}
1250+
1251+
const result = toolUseToText(block)
1252+
1253+
expect(result).toContain("[Tool Use: write_file]")
1254+
expect(result).toContain("path: output.json")
1255+
expect(result).toContain("content:")
1256+
expect(result).toContain('"key"')
1257+
expect(result).toContain('"value"')
1258+
})
1259+
1260+
it("should convert tool_use block with string input to text", () => {
1261+
const block: Anthropic.Messages.ToolUseBlockParam = {
1262+
type: "tool_use",
1263+
id: "tool-789",
1264+
name: "execute_command",
1265+
input: "ls -la" as unknown as Record<string, unknown>,
1266+
}
1267+
1268+
const result = toolUseToText(block)
1269+
1270+
expect(result).toBe("[Tool Use: execute_command]\nls -la")
1271+
})
1272+
1273+
it("should handle empty object input", () => {
1274+
const block: Anthropic.Messages.ToolUseBlockParam = {
1275+
type: "tool_use",
1276+
id: "tool-empty",
1277+
name: "some_tool",
1278+
input: {},
1279+
}
1280+
1281+
const result = toolUseToText(block)
1282+
1283+
expect(result).toBe("[Tool Use: some_tool]\n")
1284+
})
1285+
})
1286+
1287+
describe("toolResultToText", () => {
1288+
it("should convert tool_result with string content to text", () => {
1289+
const block: Anthropic.Messages.ToolResultBlockParam = {
1290+
type: "tool_result",
1291+
tool_use_id: "tool-123",
1292+
content: "File contents here",
1293+
}
1294+
1295+
const result = toolResultToText(block)
1296+
1297+
expect(result).toBe("[Tool Result]\nFile contents here")
1298+
})
1299+
1300+
it("should convert tool_result with error flag to text", () => {
1301+
const block: Anthropic.Messages.ToolResultBlockParam = {
1302+
type: "tool_result",
1303+
tool_use_id: "tool-456",
1304+
content: "File not found",
1305+
is_error: true,
1306+
}
1307+
1308+
const result = toolResultToText(block)
1309+
1310+
expect(result).toBe("[Tool Result (Error)]\nFile not found")
1311+
})
1312+
1313+
it("should convert tool_result with array content to text", () => {
1314+
const block: Anthropic.Messages.ToolResultBlockParam = {
1315+
type: "tool_result",
1316+
tool_use_id: "tool-789",
1317+
content: [
1318+
{ type: "text", text: "First line" },
1319+
{ type: "text", text: "Second line" },
1320+
],
1321+
}
1322+
1323+
const result = toolResultToText(block)
1324+
1325+
expect(result).toBe("[Tool Result]\nFirst line\nSecond line")
1326+
})
1327+
1328+
it("should handle tool_result with image in array content", () => {
1329+
const block: Anthropic.Messages.ToolResultBlockParam = {
1330+
type: "tool_result",
1331+
tool_use_id: "tool-img",
1332+
content: [
1333+
{ type: "text", text: "Screenshot:" },
1334+
{ type: "image", source: { type: "base64", media_type: "image/png", data: "abc123" } },
1335+
],
1336+
}
1337+
1338+
const result = toolResultToText(block)
1339+
1340+
expect(result).toBe("[Tool Result]\nScreenshot:\n[Image]")
1341+
})
1342+
1343+
it("should handle tool_result with no content", () => {
1344+
const block: Anthropic.Messages.ToolResultBlockParam = {
1345+
type: "tool_result",
1346+
tool_use_id: "tool-empty",
1347+
}
1348+
1349+
const result = toolResultToText(block)
1350+
1351+
expect(result).toBe("[Tool Result]")
1352+
})
1353+
})
1354+
1355+
describe("convertToolBlocksToText", () => {
1356+
it("should return string content unchanged", () => {
1357+
const content = "Simple text content"
1358+
1359+
const result = convertToolBlocksToText(content)
1360+
1361+
expect(result).toBe("Simple text content")
1362+
})
1363+
1364+
it("should convert tool_use blocks to text blocks", () => {
1365+
const content: Anthropic.Messages.ContentBlockParam[] = [
1366+
{
1367+
type: "tool_use",
1368+
id: "tool-123",
1369+
name: "read_file",
1370+
input: { path: "test.ts" },
1371+
},
1372+
]
1373+
1374+
const result = convertToolBlocksToText(content)
1375+
1376+
expect(Array.isArray(result)).toBe(true)
1377+
expect((result as Anthropic.Messages.ContentBlockParam[])[0].type).toBe("text")
1378+
expect((result as Anthropic.Messages.TextBlockParam[])[0].text).toContain("[Tool Use: read_file]")
1379+
})
1380+
1381+
it("should convert tool_result blocks to text blocks", () => {
1382+
const content: Anthropic.Messages.ContentBlockParam[] = [
1383+
{
1384+
type: "tool_result",
1385+
tool_use_id: "tool-123",
1386+
content: "File contents",
1387+
},
1388+
]
1389+
1390+
const result = convertToolBlocksToText(content)
1391+
1392+
expect(Array.isArray(result)).toBe(true)
1393+
expect((result as Anthropic.Messages.ContentBlockParam[])[0].type).toBe("text")
1394+
expect((result as Anthropic.Messages.TextBlockParam[])[0].text).toContain("[Tool Result]")
1395+
})
1396+
1397+
it("should preserve non-tool blocks unchanged", () => {
1398+
const content: Anthropic.Messages.ContentBlockParam[] = [
1399+
{ type: "text", text: "Hello" },
1400+
{
1401+
type: "tool_use",
1402+
id: "tool-123",
1403+
name: "read_file",
1404+
input: { path: "test.ts" },
1405+
},
1406+
{ type: "text", text: "World" },
1407+
]
1408+
1409+
const result = convertToolBlocksToText(content)
1410+
1411+
expect(Array.isArray(result)).toBe(true)
1412+
const resultArray = result as Anthropic.Messages.ContentBlockParam[]
1413+
expect(resultArray).toHaveLength(3)
1414+
expect(resultArray[0]).toEqual({ type: "text", text: "Hello" })
1415+
expect(resultArray[1].type).toBe("text")
1416+
expect((resultArray[1] as Anthropic.Messages.TextBlockParam).text).toContain("[Tool Use: read_file]")
1417+
expect(resultArray[2]).toEqual({ type: "text", text: "World" })
1418+
})
1419+
1420+
it("should handle mixed content with multiple tool blocks", () => {
1421+
const content: Anthropic.Messages.ContentBlockParam[] = [
1422+
{
1423+
type: "tool_use",
1424+
id: "tool-1",
1425+
name: "read_file",
1426+
input: { path: "a.ts" },
1427+
},
1428+
{
1429+
type: "tool_result",
1430+
tool_use_id: "tool-1",
1431+
content: "contents of a.ts",
1432+
},
1433+
]
1434+
1435+
const result = convertToolBlocksToText(content)
1436+
1437+
expect(Array.isArray(result)).toBe(true)
1438+
const resultArray = result as Anthropic.Messages.ContentBlockParam[]
1439+
expect(resultArray).toHaveLength(2)
1440+
expect((resultArray[0] as Anthropic.Messages.TextBlockParam).text).toContain("[Tool Use: read_file]")
1441+
expect((resultArray[1] as Anthropic.Messages.TextBlockParam).text).toContain("[Tool Result]")
1442+
expect((resultArray[1] as Anthropic.Messages.TextBlockParam).text).toContain("contents of a.ts")
1443+
})
1444+
})
1445+
1446+
describe("transformMessagesForCondensing", () => {
1447+
it("should transform all messages with tool blocks to text", () => {
1448+
const messages = [
1449+
{ role: "user" as const, content: "Hello" },
1450+
{
1451+
role: "assistant" as const,
1452+
content: [
1453+
{
1454+
type: "tool_use" as const,
1455+
id: "tool-1",
1456+
name: "read_file",
1457+
input: { path: "test.ts" },
1458+
},
1459+
],
1460+
},
1461+
{
1462+
role: "user" as const,
1463+
content: [
1464+
{
1465+
type: "tool_result" as const,
1466+
tool_use_id: "tool-1",
1467+
content: "file contents",
1468+
},
1469+
],
1470+
},
1471+
]
1472+
1473+
const result = transformMessagesForCondensing(messages)
1474+
1475+
expect(result).toHaveLength(3)
1476+
expect(result[0].content).toBe("Hello")
1477+
expect(Array.isArray(result[1].content)).toBe(true)
1478+
expect((result[1].content as any[])[0].type).toBe("text")
1479+
expect((result[1].content as any[])[0].text).toContain("[Tool Use: read_file]")
1480+
expect(Array.isArray(result[2].content)).toBe(true)
1481+
expect((result[2].content as any[])[0].type).toBe("text")
1482+
expect((result[2].content as any[])[0].text).toContain("[Tool Result]")
1483+
})
1484+
1485+
it("should preserve message role and other properties", () => {
1486+
const messages = [
1487+
{
1488+
role: "assistant" as const,
1489+
content: [
1490+
{
1491+
type: "tool_use" as const,
1492+
id: "tool-1",
1493+
name: "execute",
1494+
input: { cmd: "ls" },
1495+
},
1496+
],
1497+
},
1498+
]
1499+
1500+
const result = transformMessagesForCondensing(messages)
1501+
1502+
expect(result[0].role).toBe("assistant")
1503+
})
1504+
1505+
it("should handle empty messages array", () => {
1506+
const result = transformMessagesForCondensing([])
1507+
1508+
expect(result).toEqual([])
1509+
})
1510+
1511+
it("should not mutate original messages", () => {
1512+
const originalContent = [
1513+
{
1514+
type: "tool_use" as const,
1515+
id: "tool-1",
1516+
name: "read_file",
1517+
input: { path: "test.ts" },
1518+
},
1519+
]
1520+
const messages = [{ role: "assistant" as const, content: originalContent }]
1521+
1522+
transformMessagesForCondensing(messages)
1523+
1524+
// Original should still have tool_use type
1525+
expect(messages[0].content[0].type).toBe("tool_use")
1526+
})
1527+
})

0 commit comments

Comments
 (0)