Skip to content

Commit 4ad947d

Browse files
committed
feat: support single-quoted JSON strings in StreamingToolCallProcessor
1 parent 2c4c145 commit 4ad947d

File tree

2 files changed

+32
-4
lines changed

2 files changed

+32
-4
lines changed

src/core/task/__tests__/tool-call-helper.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,4 +310,16 @@ describe("handleOpenaiToolCallStreaming", () => {
310310
expect(xml).toContain("<echo>")
311311
expect(xml).toContain("<msg>hi</msg>")
312312
})
313+
it("should delegate to processor.processChunk single quote", () => {
314+
const processor = new StreamingToolCallProcessor()
315+
const chunk = [
316+
{
317+
index: 0,
318+
id: "call_b16cd3f11abe4188b791e50e",
319+
function: { name: "read_file", arguments: `{"args": {'file': [{'path': 'a.js'}]}}` },
320+
},
321+
]
322+
const result = handleOpenaiToolCallStreaming(processor, chunk, "openai")
323+
expect(result.chunkContent).toContain("a.js")
324+
})
313325
})

src/core/task/tool-call-helper.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ class ToolCallProcessingState {
4848
isEscaped = false
4949
isStreamingStringValue = false
5050

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+
5157
// Stack to keep track of JSON objects ({) and arrays ([).
5258
bracketStack: ("{" | "[")[] = []
5359
// Stack to keep track of XML tags for generating closing tags correctly.
@@ -236,6 +242,11 @@ export class StreamingToolCallProcessor {
236242
let xml = ""
237243
const args = state.arguments
238244

245+
// Track which quote type started the string (single or double)
246+
if (!state.quoteChar) {
247+
state.quoteChar = null
248+
}
249+
239250
while (state.cursor < args.length) {
240251
const char = args[state.cursor]
241252

@@ -260,10 +271,11 @@ export class StreamingToolCallProcessor {
260271
// Stop processing this chunk and wait for the next one.
261272
return xml
262273
}
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).
265276
state.inString = false
266277
state.isStreamingStringValue = false
278+
state.quoteChar = null
267279
const parent = state.bracketStack[state.bracketStack.length - 1]
268280
if (parent === "{") {
269281
const tag = state.xmlTagStack.pop()!
@@ -283,11 +295,11 @@ export class StreamingToolCallProcessor {
283295
if (char === "\\" && !state.isEscaped) {
284296
state.currentString += "\\"
285297
state.isEscaped = true
286-
} else if (char === '"' && !state.isEscaped) {
298+
} else if (char === state.quoteChar && !state.isEscaped) {
287299
state.inString = false
288300
let finalString
289301
try {
290-
finalString = JSON.parse('"' + state.currentString + '"')
302+
finalString = JSON.parse('"' + state.currentString.replace(/"/g, '\\"') + '"')
291303
} catch (e) {
292304
finalString = state.currentString
293305
}
@@ -298,6 +310,7 @@ export class StreamingToolCallProcessor {
298310
xml += `${this.getIndent(indentLevel)}${this.onOpenTag(finalString, toolName)}`
299311
state.parserState = ParserState.EXPECT_COLON
300312
state.currentString = ""
313+
state.quoteChar = null
301314
} else {
302315
state.currentString += char
303316
state.isEscaped = false
@@ -395,6 +408,8 @@ export class StreamingToolCallProcessor {
395408
}
396409
break
397410
case '"':
411+
case "'":
412+
// Support both double and single quote for string start
398413
if (state.parserState === ParserState.EXPECT_VALUE) {
399414
// We've encountered the start of a string that is a JSON value.
400415
state.isStreamingStringValue = true
@@ -403,6 +418,7 @@ export class StreamingToolCallProcessor {
403418
state.isStreamingStringValue = false
404419
}
405420
state.inString = true
421+
state.quoteChar = char // Track which quote started the string
406422
break
407423
case ":":
408424
if (state.parserState === ParserState.EXPECT_COLON) {

0 commit comments

Comments
 (0)