@@ -48,6 +48,12 @@ class ToolCallProcessingState {
48
48
isEscaped = false
49
49
isStreamingStringValue = false
50
50
51
+ /**
52
+ * Tracks the quote character used for the current string (' or ").
53
+ * This enables support for both single-quoted and double-quoted JSON-like strings.
54
+ */
55
+ quoteChar : string | null = null
56
+
51
57
// Stack to keep track of JSON objects ({) and arrays ([).
52
58
bracketStack : ( "{" | "[" ) [ ] = [ ]
53
59
// Stack to keep track of XML tags for generating closing tags correctly.
@@ -236,6 +242,11 @@ export class StreamingToolCallProcessor {
236
242
let xml = ""
237
243
const args = state . arguments
238
244
245
+ // Track which quote type started the string (single or double)
246
+ if ( ! state . quoteChar ) {
247
+ state . quoteChar = null
248
+ }
249
+
239
250
while ( state . cursor < args . length ) {
240
251
const char = args [ state . cursor ]
241
252
@@ -260,10 +271,11 @@ export class StreamingToolCallProcessor {
260
271
// Stop processing this chunk and wait for the next one.
261
272
return xml
262
273
}
263
- } else if ( char === '"' ) {
264
- // End of string value.
274
+ } else if ( char === state . quoteChar ) {
275
+ // End of string value (support both single and double quote) .
265
276
state . inString = false
266
277
state . isStreamingStringValue = false
278
+ state . quoteChar = null
267
279
const parent = state . bracketStack [ state . bracketStack . length - 1 ]
268
280
if ( parent === "{" ) {
269
281
const tag = state . xmlTagStack . pop ( ) !
@@ -283,11 +295,11 @@ export class StreamingToolCallProcessor {
283
295
if ( char === "\\" && ! state . isEscaped ) {
284
296
state . currentString += "\\"
285
297
state . isEscaped = true
286
- } else if ( char === '"' && ! state . isEscaped ) {
298
+ } else if ( char === state . quoteChar && ! state . isEscaped ) {
287
299
state . inString = false
288
300
let finalString
289
301
try {
290
- finalString = JSON . parse ( '"' + state . currentString + '"' )
302
+ finalString = JSON . parse ( '"' + state . currentString . replace ( / " / g , '\\"' ) + '"' )
291
303
} catch ( e ) {
292
304
finalString = state . currentString
293
305
}
@@ -298,6 +310,7 @@ export class StreamingToolCallProcessor {
298
310
xml += `${ this . getIndent ( indentLevel ) } ${ this . onOpenTag ( finalString , toolName ) } `
299
311
state . parserState = ParserState . EXPECT_COLON
300
312
state . currentString = ""
313
+ state . quoteChar = null
301
314
} else {
302
315
state . currentString += char
303
316
state . isEscaped = false
@@ -395,6 +408,8 @@ export class StreamingToolCallProcessor {
395
408
}
396
409
break
397
410
case '"' :
411
+ case "'" :
412
+ // Support both double and single quote for string start
398
413
if ( state . parserState === ParserState . EXPECT_VALUE ) {
399
414
// We've encountered the start of a string that is a JSON value.
400
415
state . isStreamingStringValue = true
@@ -403,6 +418,7 @@ export class StreamingToolCallProcessor {
403
418
state . isStreamingStringValue = false
404
419
}
405
420
state . inString = true
421
+ state . quoteChar = char // Track which quote started the string
406
422
break
407
423
case ":" :
408
424
if ( state . parserState === ParserState . EXPECT_COLON ) {
0 commit comments