22 convertToModelMessages ,
33 createUIMessageStream ,
44 createUIMessageStreamResponse ,
5+ stepCountIs ,
56 streamText ,
67} from "ai"
78import { z } from "zod"
@@ -63,6 +64,28 @@ function isMinimalDiagram(xml: string): boolean {
6364 return ! stripped . includes ( 'id="2"' )
6465}
6566
67+ // Helper function to fix tool call inputs for Bedrock API
68+ // Bedrock requires toolUse.input to be a JSON object, not a string
69+ function fixToolCallInputs ( messages : any [ ] ) : any [ ] {
70+ return messages . map ( ( msg ) => {
71+ if ( msg . role !== "assistant" || ! Array . isArray ( msg . content ) ) {
72+ return msg
73+ }
74+ const fixedContent = msg . content . map ( ( part : any ) => {
75+ if ( part . type === "tool-call" && typeof part . input === "string" ) {
76+ try {
77+ return { ...part , input : JSON . parse ( part . input ) }
78+ } catch {
79+ // If parsing fails, wrap the string in an object
80+ return { ...part , input : { rawInput : part . input } }
81+ }
82+ }
83+ return part
84+ } )
85+ return { ...msg , content : fixedContent }
86+ } )
87+ }
88+
6689// Helper function to create cached stream response
6790function createCachedStreamResponse ( xml : string ) : Response {
6891 const toolCallId = `cached-${ Date . now ( ) } `
@@ -189,9 +212,12 @@ ${lastMessageText}
189212 // Convert UIMessages to ModelMessages and add system message
190213 const modelMessages = convertToModelMessages ( messages )
191214
215+ // Fix tool call inputs for Bedrock API (requires JSON objects, not strings)
216+ const fixedMessages = fixToolCallInputs ( modelMessages )
217+
192218 // Filter out messages with empty content arrays (Bedrock API rejects these)
193219 // This is a safety measure - ideally convertToModelMessages should handle all cases
194- let enhancedMessages = modelMessages . filter (
220+ let enhancedMessages = fixedMessages . filter (
195221 ( msg : any ) =>
196222 msg . content && Array . isArray ( msg . content ) && msg . content . length > 0 ,
197223 )
@@ -267,6 +293,7 @@ ${lastMessageText}
267293
268294 const result = streamText ( {
269295 model,
296+ stopWhen : stepCountIs ( 5 ) ,
270297 messages : allMessages ,
271298 ...( providerOptions && { providerOptions } ) ,
272299 ...( headers && { headers } ) ,
@@ -277,6 +304,32 @@ ${lastMessageText}
277304 userId,
278305 } ) ,
279306 } ) ,
307+ // Repair malformed tool calls (model sometimes generates invalid JSON with unescaped quotes)
308+ experimental_repairToolCall : async ( { toolCall } ) => {
309+ // The toolCall.input contains the raw JSON string that failed to parse
310+ const rawJson =
311+ typeof toolCall . input === "string" ? toolCall . input : null
312+
313+ if ( rawJson ) {
314+ try {
315+ // Fix unescaped quotes: x="520" should be x=\"520\"
316+ const fixed = rawJson . replace (
317+ / ( [ a - z A - Z ] ) = " ( \d + ) " / g,
318+ '$1=\\"$2\\"' ,
319+ )
320+ const parsed = JSON . parse ( fixed )
321+ return {
322+ type : "tool-call" as const ,
323+ toolCallId : toolCall . toolCallId ,
324+ toolName : toolCall . toolName ,
325+ input : JSON . stringify ( parsed ) ,
326+ }
327+ } catch {
328+ // Repair failed, return null
329+ }
330+ }
331+ return null
332+ } ,
280333 onFinish : ( { text, usage, providerMetadata } ) => {
281334 console . log (
282335 "[Cache] Full providerMetadata:" ,
@@ -342,7 +395,9 @@ IMPORTANT: Keep edits concise:
342395- Only include the lines that are changing, plus 1-2 surrounding lines for context if needed
343396- Break large changes into multiple smaller edits
344397- Each search must contain complete lines (never truncate mid-line)
345- - First match only - be specific enough to target the right element` ,
398+ - First match only - be specific enough to target the right element
399+
400+ ⚠️ JSON ESCAPING: Every " inside string values MUST be escaped as \\". Example: x=\\"100\\" y=\\"200\\" - BOTH quotes need backslashes!` ,
346401 inputSchema : z . object ( {
347402 edits : z
348403 . array (
0 commit comments