Skip to content

Commit b608e70

Browse files
committed
fix: remove broken tool name extraction fallback in Gemini format converter
- Remove fragile string manipulation fallback that fails with Anthropic tool IDs - Throw clear error when toolIdToName map is missing required entries - Update all tests to provide toolIdToName map (reflects real usage) - Add tests to verify error throwing behavior The fallback attempted to extract tool names from IDs using lastIndexOf('-'), which completely fails with Anthropic's native tool IDs like 'toolu_01RxWc...'. Instead, now fails fast with diagnostic error showing missing ID and available IDs.
1 parent c8f7bf4 commit b608e70

File tree

2 files changed

+67
-13
lines changed

2 files changed

+67
-13
lines changed

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

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,9 @@ describe("convertAnthropicMessageToGemini", () => {
141141
})
142142

143143
it("should convert a message with tool result as string", () => {
144+
const toolIdToName = new Map<string, string>()
145+
toolIdToName.set("calculator-123", "calculator")
146+
144147
const anthropicMessage: Anthropic.Messages.MessageParam = {
145148
role: "user",
146149
content: [
@@ -153,7 +156,7 @@ describe("convertAnthropicMessageToGemini", () => {
153156
],
154157
}
155158

156-
const result = convertAnthropicMessageToGemini(anthropicMessage)
159+
const result = convertAnthropicMessageToGemini(anthropicMessage, { toolIdToName })
157160

158161
expect(result).toEqual([
159162
{
@@ -193,6 +196,9 @@ describe("convertAnthropicMessageToGemini", () => {
193196
})
194197

195198
it("should convert a message with tool result as array with text only", () => {
199+
const toolIdToName = new Map<string, string>()
200+
toolIdToName.set("search-123", "search")
201+
196202
const anthropicMessage: Anthropic.Messages.MessageParam = {
197203
role: "user",
198204
content: [
@@ -207,7 +213,7 @@ describe("convertAnthropicMessageToGemini", () => {
207213
],
208214
}
209215

210-
const result = convertAnthropicMessageToGemini(anthropicMessage)
216+
const result = convertAnthropicMessageToGemini(anthropicMessage, { toolIdToName })
211217

212218
expect(result).toEqual([
213219
{
@@ -228,6 +234,9 @@ describe("convertAnthropicMessageToGemini", () => {
228234
})
229235

230236
it("should convert a message with tool result as array with text and images", () => {
237+
const toolIdToName = new Map<string, string>()
238+
toolIdToName.set("search-123", "search")
239+
231240
const anthropicMessage: Anthropic.Messages.MessageParam = {
232241
role: "user",
233242
content: [
@@ -257,7 +266,7 @@ describe("convertAnthropicMessageToGemini", () => {
257266
],
258267
}
259268

260-
const result = convertAnthropicMessageToGemini(anthropicMessage)
269+
const result = convertAnthropicMessageToGemini(anthropicMessage, { toolIdToName })
261270

262271
expect(result).toEqual([
263272
{
@@ -290,6 +299,9 @@ describe("convertAnthropicMessageToGemini", () => {
290299
})
291300

292301
it("should convert a message with tool result containing only images", () => {
302+
const toolIdToName = new Map<string, string>()
303+
toolIdToName.set("imagesearch-123", "imagesearch")
304+
293305
const anthropicMessage: Anthropic.Messages.MessageParam = {
294306
role: "user",
295307
content: [
@@ -310,7 +322,7 @@ describe("convertAnthropicMessageToGemini", () => {
310322
],
311323
}
312324

313-
const result = convertAnthropicMessageToGemini(anthropicMessage)
325+
const result = convertAnthropicMessageToGemini(anthropicMessage, { toolIdToName })
314326

315327
expect(result).toEqual([
316328
{
@@ -336,7 +348,10 @@ describe("convertAnthropicMessageToGemini", () => {
336348
])
337349
})
338350

339-
it("should handle tool names with hyphens", () => {
351+
it("should handle tool names with hyphens using toolIdToName map", () => {
352+
const toolIdToName = new Map<string, string>()
353+
toolIdToName.set("search-files-123", "search-files")
354+
340355
const anthropicMessage: Anthropic.Messages.MessageParam = {
341356
role: "user",
342357
content: [
@@ -348,7 +363,7 @@ describe("convertAnthropicMessageToGemini", () => {
348363
],
349364
}
350365

351-
const result = convertAnthropicMessageToGemini(anthropicMessage)
366+
const result = convertAnthropicMessageToGemini(anthropicMessage, { toolIdToName })
352367

353368
expect(result).toEqual([
354369
{
@@ -368,6 +383,43 @@ describe("convertAnthropicMessageToGemini", () => {
368383
])
369384
})
370385

386+
it("should throw error when toolIdToName map is not provided", () => {
387+
const anthropicMessage: Anthropic.Messages.MessageParam = {
388+
role: "user",
389+
content: [
390+
{
391+
type: "tool_result",
392+
tool_use_id: "calculator-123",
393+
content: "result is 5",
394+
},
395+
],
396+
}
397+
398+
expect(() => convertAnthropicMessageToGemini(anthropicMessage)).toThrow(
399+
'Unable to find tool name for tool_use_id "calculator-123"',
400+
)
401+
})
402+
403+
it("should throw error when tool_use_id is not in the map", () => {
404+
const toolIdToName = new Map<string, string>()
405+
toolIdToName.set("other-tool-456", "other-tool")
406+
407+
const anthropicMessage: Anthropic.Messages.MessageParam = {
408+
role: "user",
409+
content: [
410+
{
411+
type: "tool_result",
412+
tool_use_id: "calculator-123",
413+
content: "result is 5",
414+
},
415+
],
416+
}
417+
418+
expect(() => convertAnthropicMessageToGemini(anthropicMessage, { toolIdToName })).toThrow(
419+
'Unable to find tool name for tool_use_id "calculator-123"',
420+
)
421+
})
422+
371423
it("should throw an error for unsupported content block type", () => {
372424
const anthropicMessage: Anthropic.Messages.MessageParam = {
373425
role: "user",

src/api/transform/gemini-format.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,16 @@ export function convertAnthropicContentToGemini(
7979
return []
8080
}
8181

82-
// Extract tool name from tool_use_id
83-
// 1. Try to look up the name from the provided map (reliable source)
84-
// 2. Fallback: Extract from ID if it follows "name-counter" format (heuristic)
85-
// 3. Fallback: Use ID as name (likely to fail validation but better than crashing)
86-
let toolName = toolIdToName?.get(block.tool_use_id)
82+
// Get tool name from the map (built from tool_use blocks in message history).
83+
// The map must contain the tool name - if it doesn't, this indicates a bug
84+
// where the conversation history is incomplete or tool_use blocks are missing.
85+
const toolName = toolIdToName?.get(block.tool_use_id)
8786
if (!toolName) {
88-
const lastHyphenIndex = block.tool_use_id.lastIndexOf("-")
89-
toolName = lastHyphenIndex >= 0 ? block.tool_use_id.slice(0, lastHyphenIndex) : block.tool_use_id
87+
throw new Error(
88+
`Unable to find tool name for tool_use_id "${block.tool_use_id}". ` +
89+
`This indicates the conversation history is missing the corresponding tool_use block. ` +
90+
`Available tool IDs: ${Array.from(toolIdToName?.keys() ?? []).join(", ") || "none"}`,
91+
)
9092
}
9193

9294
if (typeof block.content === "string") {

0 commit comments

Comments
 (0)