@pedrofariasx/qwenproxy 1.2.2 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,23 +1,20 @@
1
1
  import { test } from 'node:test';
2
2
  import assert from 'node:assert';
3
- import { getIncrementalDelta } from '../routes/chat.ts';
3
+ import { getIncrementalDelta } from '../routes/chat.js';
4
4
 
5
5
  test('getIncrementalDelta: handles strictly cumulative stream correctly', () => {
6
6
  let accumulated = '';
7
7
 
8
- // Step 1
9
8
  let chunk1 = 'const x = 1;';
10
9
  let res1 = getIncrementalDelta(accumulated, chunk1);
11
10
  assert.strictEqual(res1.delta, 'const x = 1;');
12
11
  accumulated = res1.matchedContent;
13
12
 
14
- // Step 2
15
13
  let chunk2 = 'const x = 1;\nconst y = 2;';
16
14
  let res2 = getIncrementalDelta(accumulated, chunk2);
17
15
  assert.strictEqual(res2.delta, '\nconst y = 2;');
18
16
  accumulated = res2.matchedContent;
19
17
 
20
- // Step 3
21
18
  let chunk3 = 'const x = 1;\nconst y = 2;\nconst z = 3;';
22
19
  let res3 = getIncrementalDelta(accumulated, chunk3);
23
20
  assert.strictEqual(res3.delta, '\nconst z = 3;');
@@ -29,19 +26,16 @@ test('getIncrementalDelta: handles strictly cumulative stream correctly', () =>
29
26
  test('getIncrementalDelta: handles strictly incremental stream correctly', () => {
30
27
  let accumulated = '';
31
28
 
32
- // Step 1
33
29
  let chunk1 = 'const x = 1;';
34
30
  let res1 = getIncrementalDelta(accumulated, chunk1);
35
31
  assert.strictEqual(res1.delta, 'const x = 1;');
36
32
  accumulated = res1.matchedContent;
37
33
 
38
- // Step 2
39
34
  let chunk2 = '\nconst y = 2;';
40
35
  let res2 = getIncrementalDelta(accumulated, chunk2);
41
36
  assert.strictEqual(res2.delta, '\nconst y = 2;');
42
37
  accumulated = res2.matchedContent;
43
38
 
44
- // Step 3
45
39
  let chunk3 = '\nconst z = 3;';
46
40
  let res3 = getIncrementalDelta(accumulated, chunk3);
47
41
  assert.strictEqual(res3.delta, '\nconst z = 3;');
@@ -51,13 +45,89 @@ test('getIncrementalDelta: handles strictly incremental stream correctly', () =>
51
45
  });
52
46
 
53
47
  test('getIncrementalDelta: does not suffer from false-positive repetitive word overlap bugs', () => {
54
- // Previously, if oldStr ended in a common keyword and newStr started/contained the same keyword,
55
- // it would incorrectly match them and strip them. Let's verify this is fixed.
56
48
  let accumulated = 'import { useState } from \'react\';\nimport {';
57
49
  let nextChunk = ' Button } from \'@/components/ui/button\';';
58
50
 
59
51
  let res = getIncrementalDelta(accumulated, nextChunk);
60
- // It should treat the next chunk as strictly incremental and return it unchanged.
61
52
  assert.strictEqual(res.delta, ' Button } from \'@/components/ui/button\';');
62
53
  assert.strictEqual(res.matchedContent, 'import { useState } from \'react\';\nimport { Button } from \'@/components/ui/button\';');
63
54
  });
55
+
56
+ test('getIncrementalDelta: empty oldStr returns newStr as delta', () => {
57
+ const res = getIncrementalDelta('', 'hello world');
58
+ assert.strictEqual(res.delta, 'hello world');
59
+ assert.strictEqual(res.matchedContent, 'hello world');
60
+ assert.strictEqual(res.contentLength, 11);
61
+ });
62
+
63
+ test('getIncrementalDelta: identical strings return empty delta', () => {
64
+ const str = 'some content here';
65
+ const res = getIncrementalDelta(str, str, str.length, str.slice(-64));
66
+ assert.strictEqual(res.delta, '');
67
+ assert.strictEqual(res.matchedContent, str);
68
+ });
69
+
70
+ test('getIncrementalDelta: completely different strings concatenate', () => {
71
+ const res = getIncrementalDelta('abc', 'xyz');
72
+ assert.strictEqual(res.delta, 'xyz');
73
+ assert.strictEqual(res.matchedContent, 'abcxyz');
74
+ });
75
+
76
+ test('getIncrementalDelta: uses prevLength fast path when suffix matches', () => {
77
+ const oldStr = 'hello world';
78
+ const prevLength = oldStr.length;
79
+ const prevSuffix = oldStr.slice(-64);
80
+ const newStr = 'hello world extended';
81
+
82
+ const res = getIncrementalDelta(oldStr, newStr, prevLength, prevSuffix);
83
+ assert.strictEqual(res.delta, ' extended');
84
+ assert.strictEqual(res.matchedContent, newStr);
85
+ assert.strictEqual(res.contentLength, newStr.length);
86
+ });
87
+
88
+ test('getIncrementalDelta: large string with tiny delta falls back to concatenation for safety', () => {
89
+ const oldStr = 'x'.repeat(3000);
90
+ const tinyDelta = 'a';
91
+ const newStr = oldStr + tinyDelta;
92
+
93
+ const res = getIncrementalDelta(oldStr, newStr, oldStr.length, oldStr.slice(-64));
94
+ assert.strictEqual(res.matchedContent, newStr);
95
+ });
96
+
97
+ test('getIncrementalDelta: contentSuffix tracks last 64 characters', () => {
98
+ const longStr = 'a'.repeat(100);
99
+ const res = getIncrementalDelta('', longStr);
100
+ assert.strictEqual(res.contentSuffix.length, 64);
101
+ assert.strictEqual(res.contentSuffix, 'a'.repeat(64));
102
+ });
103
+
104
+ test('getIncrementalDelta: handles segment-based prefix matching', () => {
105
+ const prefix = 'a'.repeat(200);
106
+ const oldStr = prefix + 'OLD';
107
+ const newStr = prefix + 'NEW';
108
+
109
+ const res = getIncrementalDelta(oldStr, newStr);
110
+ assert.ok(res.delta.length > 0);
111
+ });
112
+
113
+ test('getIncrementalDelta: works through a realistic multi-chunk stream', () => {
114
+ let accumulated = '';
115
+ const chunks = [
116
+ 'The',
117
+ 'The quick',
118
+ 'The quick brown',
119
+ 'The quick brown fox',
120
+ 'The quick brown fox jumps',
121
+ 'The quick brown fox jumps over the lazy dog.',
122
+ ];
123
+
124
+ let finalDelta = '';
125
+ for (const chunk of chunks) {
126
+ const res = getIncrementalDelta(accumulated, chunk, accumulated.length, accumulated.slice(-64));
127
+ finalDelta += res.delta;
128
+ accumulated = res.matchedContent;
129
+ }
130
+
131
+ assert.strictEqual(accumulated, 'The quick brown fox jumps over the lazy dog.');
132
+ assert.strictEqual(finalDelta, 'The quick brown fox jumps over the lazy dog.');
133
+ });
@@ -1,98 +1,110 @@
1
- import { robustParseJSON } from '../utils/json.ts';
2
-
3
- const problematicString = '{"name": "suggest", "arguments": {"suggest": "Landing page para escritório de advocacia criada em src/pages/Index.tsx", "actions": [{"label": "Revisar alterações", "description": "Executar review local das mudanças não commitadas", "prompt": "/local-review-uncommitted"}]})';
4
-
5
- console.log('Testing problematic string...');
6
- try {
7
- const result = robustParseJSON(problematicString);
8
- console.log('Successfully parsed:', JSON.stringify(result, null, 2));
9
- if (result.name === 'suggest' && result.arguments.actions.length === 1) {
10
- console.log(' Problematic string test passed!');
11
- } else {
12
- console.error('❌ Result structure is incorrect');
13
- }
14
- } catch (e) {
15
- console.error(' Failed to parse problematic string:', e);
16
- }
17
-
18
- const missingBraces = '{"name": "test", "arguments": {"foo": "bar"';
19
- console.log('\nTesting missing braces...');
20
- try {
21
- const result = robustParseJSON(missingBraces);
22
- console.log('Successfully parsed:', JSON.stringify(result, null, 2));
23
- if (result.arguments.foo === 'bar') {
24
- console.log(' Missing braces test passed!');
25
- } else {
26
- console.error('❌ Result structure is incorrect');
27
- }
28
- } catch (e) {
29
- console.error(' Failed to parse missing braces:', e);
30
- }
31
-
32
- const controlChars = '{"name": "control", "msg": "line 1\\nline 2"}';
33
- console.log('\nTesting control characters in string...');
34
- try {
35
- // Note: in a real string from the model, it would be a literal newline
36
- const literalNewline = '{"name": "control", "msg": "line 1\\nline 2"}'.replace('\\n', '\n');
37
- const result = robustParseJSON(literalNewline);
38
- console.log('Successfully parsed:', JSON.stringify(result, null, 2));
39
- if (result.msg.includes('line 1') && result.msg.includes('line 2')) {
40
- console.log('✅ Control characters test passed!');
41
- } else {
42
- console.error('❌ Result content is incorrect');
43
- }
44
- } catch (e) {
45
- console.error('❌ Failed to parse control characters:', e);
46
- }
47
-
48
- const crazyCase = `{"name": "suggest", "arguments": {"suggest": "Landing page criada para escritório de advocacia com design corporativo", "actions": [{"label": "Revisar código local", "description": "Exec<tool_call>\n{"name": "bashutar revisão local das", "arguments": alterações {"command": não commitadas", "npm run lint "prompt", "description":": "/local-review "Run lint-uncommitted"}] to verify code quality})"}}`;
49
- console.log('\nTesting crazy nested hallucination case...');
50
- try {
51
- const result = robustParseJSON(crazyCase);
52
- console.log('Successfully parsed (at least some of it):', JSON.stringify(result, null, 2));
53
- console.log('✅ Crazy case handled without crashing!');
54
- } catch (e: any) {
55
- console.log('⚠️ Crazy case failed (too malformed), but error was:', e.message);
56
- }
57
-
58
- const invalidEscapes = '{"path": "C:\\\\Users\\\\name\\\\Documents"}';
59
- console.log('\nTesting invalid backslash escapes in string...');
60
- try {
61
- const result = robustParseJSON(invalidEscapes);
62
- console.log('Successfully parsed:', JSON.stringify(result, null, 2));
63
- if (result.path === 'C:\\\\Users\\\\name\\\\Documents' || result.path === 'C:\\Users\\name\\Documents') {
64
- console.log(' Invalid backslash escapes test passed!');
65
- } else {
66
- console.error('❌ Result path is incorrect:', result.path);
67
- }
68
- } catch (e) {
69
- console.error('❌ Failed to parse invalid backslash escapes:', e);
70
- }
71
-
72
- const doubleKey = '{"name": "name": "create_file", "arguments": {"path": "b.txt"}}';
73
- console.log('\nTesting double key hallucination...');
74
- try {
75
- const result = robustParseJSON(doubleKey);
76
- console.log('Successfully parsed:', JSON.stringify(result, null, 2));
77
- if (result.name === 'create_file') {
78
- console.log('✅ Double key test passed!');
79
- } else {
80
- console.error('❌ Result name is incorrect:', result.name);
81
- }
82
- } catch (e) {
83
- console.error('❌ Failed to parse double key:', e);
84
- }
85
-
86
- const unquotedArgs = '{"name":"Read",arguments:{"file_path":"test.ts","limit":100}}';
87
- console.log('\nTesting unquoted arguments key...');
88
- try {
89
- const result = robustParseJSON(unquotedArgs);
90
- console.log('Successfully parsed:', JSON.stringify(result, null, 2));
91
- if (result.arguments && result.arguments.limit === 100) {
92
- console.log(' Unquoted arguments test passed!');
93
- } else {
94
- console.error(' Result structure is incorrect');
95
- }
96
- } catch (e) {
97
- console.error(' Failed to parse unquoted arguments:', e);
98
- }
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { robustParseJSON } from '../utils/json.js';
4
+
5
+ test('robustParseJSON: valid JSON passes through directly', () => {
6
+ const result = robustParseJSON('{"name": "test", "arguments": {"a": 1}}');
7
+ assert.deepStrictEqual(result, { name: 'test', arguments: { a: 1 } });
8
+ });
9
+
10
+ test('robustParseJSON: returns null for empty string', () => {
11
+ assert.strictEqual(robustParseJSON(''), null);
12
+ });
13
+
14
+ test('robustParseJSON: returns null for non-object string', () => {
15
+ assert.strictEqual(robustParseJSON('just plain text'), null);
16
+ });
17
+
18
+ test('robustParseJSON: handles markdown code fence wrapping', () => {
19
+ const result = robustParseJSON('```json\n{"name": "test"}\n```');
20
+ assert.deepStrictEqual(result, { name: 'test' });
21
+ });
22
+
23
+ test('robustParseJSON: handles missing closing braces', () => {
24
+ const result = robustParseJSON('{"name": "test", "arguments": {"foo": "bar"');
25
+ assert.ok(result);
26
+ assert.strictEqual(result.arguments.foo, 'bar');
27
+ });
28
+
29
+ test('robustParseJSON: handles missing closing brackets', () => {
30
+ const result = robustParseJSON('{"items": [1, 2, 3');
31
+ assert.ok(result);
32
+ assert.deepStrictEqual(result.items, [1, 2, 3]);
33
+ });
34
+
35
+ test('robustParseJSON: handles double key hallucination', () => {
36
+ const result = robustParseJSON('{"name": "name": "create_file", "arguments": {"path": "b.txt"}}');
37
+ assert.ok(result);
38
+ assert.strictEqual(result.name, 'create_file');
39
+ });
40
+
41
+ test('robustParseJSON: handles unquoted keys', () => {
42
+ const result = robustParseJSON('{"name":"Read",arguments:{"file_path":"test.ts","limit":100}}');
43
+ assert.ok(result);
44
+ assert.strictEqual(result.arguments.limit, 100);
45
+ });
46
+
47
+ test('robustParseJSON: handles control characters in string values', () => {
48
+ const literalNewline = '{"name": "control", "msg": "line 1\nline 2"}';
49
+ const result = robustParseJSON(literalNewline);
50
+ assert.ok(result);
51
+ assert.ok(result.msg.includes('line 1'));
52
+ assert.ok(result.msg.includes('line 2'));
53
+ });
54
+
55
+ test('robustParseJSON: handles Windows path backslashes', () => {
56
+ const result = robustParseJSON('{"path": "C:\\\\Users\\\\name\\\\Documents"}');
57
+ assert.ok(result);
58
+ assert.ok(
59
+ result.path === 'C:\\Users\\name\\Documents' || result.path === 'C:\\\\Users\\\\name\\\\Documents',
60
+ `Unexpected path: ${result.path}`
61
+ );
62
+ });
63
+
64
+ test('robustParseJSON: handles trailing comma', () => {
65
+ const result = robustParseJSON('{"name": "test", "value": 42,}');
66
+ assert.ok(result);
67
+ assert.strictEqual(result.name, 'test');
68
+ assert.strictEqual(result.value, 42);
69
+ });
70
+
71
+ test('robustParseJSON: handles complex nested suggest payload', () => {
72
+ const payload = '{"name": "suggest", "arguments": {"suggest": "Landing page criada", "actions": [{"label": "Revisar", "description": "Review", "prompt": "/local-review-uncommitted"}]})';
73
+ const result = robustParseJSON(payload);
74
+ assert.ok(result);
75
+ assert.strictEqual(result.name, 'suggest');
76
+ assert.ok(result.arguments.actions.length >= 1);
77
+ });
78
+
79
+ test('robustParseJSON: handles deeply nested malformed JSON gracefully', () => {
80
+ const crazy = `{"name": "suggest", "arguments": {"suggest": "ok", "actions": [{"label": "test"<tool_call>\n{"name": "broken"}]}}`;
81
+ const result = robustParseJSON(crazy);
82
+ assert.ok(result === null || typeof result === 'object');
83
+ });
84
+
85
+ test('robustParseJSON: strips leading text before first brace', () => {
86
+ const result = robustParseJSON('some text before {"name": "found"}');
87
+ assert.ok(result);
88
+ assert.strictEqual(result.name, 'found');
89
+ });
90
+
91
+ test('robustParseJSON: handles array values correctly', () => {
92
+ const result = robustParseJSON('{"items": ["a", "b", "c"]}');
93
+ assert.ok(result);
94
+ assert.deepStrictEqual(result.items, ['a', 'b', 'c']);
95
+ });
96
+
97
+ test('robustParseJSON: handles numeric values', () => {
98
+ const result = robustParseJSON('{"count": 42, "ratio": 3.14}');
99
+ assert.ok(result);
100
+ assert.strictEqual(result.count, 42);
101
+ assert.strictEqual(result.ratio, 3.14);
102
+ });
103
+
104
+ test('robustParseJSON: handles boolean and null values', () => {
105
+ const result = robustParseJSON('{"active": true, "deleted": false, "data": null}');
106
+ assert.ok(result);
107
+ assert.strictEqual(result.active, true);
108
+ assert.strictEqual(result.deleted, false);
109
+ assert.strictEqual(result.data, null);
110
+ });
@@ -6,7 +6,7 @@ import { fileURLToPath } from 'node:url';
6
6
  import net from 'node:net';
7
7
  import { serve } from '@hono/node-server';
8
8
  import { app } from '../api/server.js';
9
- import { initPlaywright, closePlaywright } from '../services/playwright.ts';
9
+ import { initPlaywright, closePlaywright } from '../services/playwright.js';
10
10
 
11
11
  const __filename = fileURLToPath(import.meta.url);
12
12
  const __dirname = path.dirname(__filename);
@@ -1,6 +1,6 @@
1
1
  import { test } from 'node:test';
2
2
  import assert from 'node:assert';
3
- import { StreamingToolParser } from '../tools/parser.ts';
3
+ import { StreamingToolParser } from '../tools/parser.js';
4
4
 
5
5
  test('StreamingToolParser: basic tool call', () => {
6
6
  const parser = new StreamingToolParser();
@@ -54,7 +54,7 @@ test('StreamingToolParser: flush partial content', () => {
54
54
  test('StreamingToolParser: robust parsing of malformed JSON', () => {
55
55
  const parser = new StreamingToolParser();
56
56
 
57
- const res = parser.feed('<tool_call>{"name": "broken", "arguments": {"a": 1</tool_call>');
57
+ const res = parser.feed('<tool_call>{"name": "broken", "arguments": {"a": 1}</tool_call>');
58
58
  assert.strictEqual(res.toolCalls.length, 1);
59
59
  assert.strictEqual(res.toolCalls[0].name, 'broken');
60
60
  assert.deepStrictEqual(res.toolCalls[0].arguments, { a: 1 });
@@ -87,3 +87,41 @@ test('StreamingToolParser: handles multiple tool calls in array format', () => {
87
87
  assert.strictEqual(result.toolCalls[1].name, 'read');
88
88
  assert.strictEqual(result.toolCalls[0].arguments.command, 'ls');
89
89
  });
90
+
91
+ test('StreamingToolParser: double-escaped quotes in JSON', () => {
92
+ const parser = new StreamingToolParser();
93
+
94
+ const input = '<tool_call>{\\"name\\": \\"edit\\", \\"arguments\\": {\\"filePath\\": \\"/tmp/test.txt\\", \\"content\\": \\"hello\\"}}</tool_call>';
95
+ const res = parser.feed(input);
96
+ assert.strictEqual(res.toolCalls.length, 1);
97
+ assert.strictEqual(res.toolCalls[0].name, 'edit');
98
+ assert.strictEqual(res.toolCalls[0].arguments.filePath, '/tmp/test.txt');
99
+ });
100
+
101
+ test('StreamingToolParser: double-escaped quotes in XML parameters', () => {
102
+ const parser = new StreamingToolParser();
103
+
104
+ const input = '<tool_call>\n<name>write</name>\n<parameter name=\\"content\\">&lt;div&gt;hello &amp; world&lt;/div&gt;</parameter>\n</tool_call>';
105
+ const res = parser.feed(input);
106
+ assert.strictEqual(res.toolCalls.length, 1);
107
+ assert.strictEqual(res.toolCalls[0].name, 'write');
108
+ assert.strictEqual(res.toolCalls[0].arguments.content, '<div>hello & world</div>');
109
+ });
110
+
111
+ test('StreamingToolParser: truncated JSON with unclosed string', () => {
112
+ const parser = new StreamingToolParser();
113
+
114
+ const res = parser.feed('<tool_call>{"name": "bash", "arguments": {"command": "echo hello</tool_call>');
115
+ assert.strictEqual(res.toolCalls.length, 1);
116
+ assert.strictEqual(res.toolCalls[0].name, 'bash');
117
+ assert.strictEqual(typeof res.toolCalls[0].arguments.command, 'string');
118
+ });
119
+
120
+ test('StreamingToolParser: flush double-escaped tool call', () => {
121
+ const parser = new StreamingToolParser();
122
+
123
+ parser.feed('<tool_call>{\\"name\\": \\"recover\\",\\"arguments\\": {\\"a\\": \\"val');
124
+ const flushed = parser.flush();
125
+ assert.strictEqual(flushed.toolCalls.length, 1);
126
+ assert.strictEqual(flushed.toolCalls[0].name, 'recover');
127
+ });
@@ -30,6 +30,29 @@ function decodeXmlEntities(value: string): string {
30
30
  .replace(/&amp;/g, '&');
31
31
  }
32
32
 
33
+ function unescapeDoubleEscaped(content: string): string {
34
+ const trimmed = content.trim();
35
+ if (!trimmed) return content;
36
+
37
+ const isJsonLike = trimmed.startsWith('{') || trimmed.startsWith('[');
38
+ const isXmlLike = trimmed.startsWith('<');
39
+
40
+ if (!isJsonLike && !isXmlLike) return content;
41
+
42
+ const firstQuoteIdx = trimmed.indexOf('"');
43
+ if (firstQuoteIdx === -1) return content;
44
+
45
+ const firstEscapedQuoteIdx = trimmed.indexOf('\\"');
46
+
47
+ if (firstEscapedQuoteIdx !== -1 && (firstQuoteIdx === -1 || firstEscapedQuoteIdx < firstQuoteIdx)) {
48
+ return content
49
+ .replace(/\\"/g, '"')
50
+ .replace(/\\\\/g, '\\');
51
+ }
52
+
53
+ return content;
54
+ }
55
+
33
56
  function coerceParameterValue(rawValue: string): unknown {
34
57
  const value = decodeXmlEntities(rawValue.trim());
35
58
  if (value === 'true') return true;
@@ -136,19 +159,67 @@ function parseRecoverableXmlToolCall(
136
159
  return { name: toolName, arguments: args };
137
160
  }
138
161
 
162
+ // ─── String-Aware Tag Detection ─────────────────────────────────────────────────
163
+
164
+ function findToolEndIndex(buffer: string): number {
165
+ const tagLen = TOOL_END.length;
166
+ const limit = buffer.length - tagLen;
167
+ let inString = false;
168
+ let escaped = false;
169
+
170
+ for (let i = 0; i <= limit; i++) {
171
+ const ch = buffer[i];
172
+ if (escaped) { escaped = false; continue; }
173
+ if (ch === '\\') { escaped = true; continue; }
174
+ if (ch === '"') { inString = !inString; continue; }
175
+ if (inString || ch !== '<') continue;
176
+ let match = true;
177
+ for (let j = 1; j < tagLen; j++) {
178
+ const c = buffer.charCodeAt(i + j);
179
+ const t = TOOL_END.charCodeAt(j);
180
+ if (c !== t && (c | 0x20) !== (t | 0x20)) { match = false; break; }
181
+ }
182
+ if (match) return i;
183
+ }
184
+ return -1;
185
+ }
186
+
139
187
  // ─── Partial Tag Detection ─────────────────────────────────────────────────────
140
188
 
141
189
  const TOOL_START_LITERAL = '<tool_call>';
142
190
 
143
191
  function findPartialToolOpenIndex(buffer: string): number {
144
- const lower = buffer.toLowerCase();
145
- // Check if there's a partial opening tag like `<tool_call` without closing `>`
146
- const idx = lower.lastIndexOf('<tool_call');
147
- if (idx !== -1 && lower.indexOf('>', idx) === -1) return idx;
192
+ const prefix = '<tool_call';
193
+ const prefixLen = prefix.length;
194
+ const bufLen = buffer.length;
195
+
196
+ let lastPartialIdx = -1;
197
+ for (let i = bufLen - 1; i >= Math.max(0, bufLen - prefixLen - 1); i--) {
198
+ if (buffer[i] !== '<') continue;
199
+ let match = true;
200
+ for (let j = 1; j < prefixLen && i + j < bufLen; j++) {
201
+ const c = buffer.charCodeAt(i + j);
202
+ const t = prefix.charCodeAt(j);
203
+ if (c !== t && ((c | 0x20) !== (t | 0x20))) { match = false; break; }
204
+ }
205
+ if (match) {
206
+ lastPartialIdx = i;
207
+ break;
208
+ }
209
+ }
210
+ if (lastPartialIdx !== -1 && buffer.indexOf('>', lastPartialIdx) === -1) return lastPartialIdx;
148
211
 
149
- // Check for partial prefix at end (e.g. `<tool`, `<tool_`, `<tool_c`)
150
212
  for (let i = 1; i < TOOL_START_LITERAL.length; i++) {
151
- if (lower.endsWith(TOOL_START_LITERAL.substring(0, i))) return buffer.length - i;
213
+ const sub = TOOL_START_LITERAL.substring(0, i);
214
+ const subLen = sub.length;
215
+ if (bufLen < subLen) continue;
216
+ let match = true;
217
+ for (let j = 0; j < subLen; j++) {
218
+ const c = buffer.charCodeAt(bufLen - subLen + j);
219
+ const t = sub.charCodeAt(j);
220
+ if (c !== t && (c | 0x20) !== (t | 0x20)) { match = false; break; }
221
+ }
222
+ if (match) return bufLen - i;
152
223
  }
153
224
  return -1;
154
225
  }
@@ -211,10 +282,8 @@ export class StreamingToolParser {
211
282
  }
212
283
  break;
213
284
  }
214
- } else {
215
- // Inside tool: look for </tool_call>
216
- const lowerBuffer = this.buffer.toLowerCase();
217
- const endIdx = lowerBuffer.indexOf(TOOL_END);
285
+ } else {
286
+ const endIdx = this.buffer.indexOf(TOOL_END);
218
287
  if (endIdx !== -1) {
219
288
  const content = this.buffer.substring(0, endIdx);
220
289
  this.buffer = this.buffer.substring(endIdx + TOOL_END.length);
@@ -284,9 +353,8 @@ export class StreamingToolParser {
284
353
  // ─── Internal Methods ──────────────────────────────────────────────────────
285
354
 
286
355
  private processToolContent(content: string, result: ParserResult): void {
287
- const t = content.trim();
356
+ let t = content.trim();
288
357
  if (!t) {
289
- // Empty tool call - malformed. Restore lead-in if possible.
290
358
  logger.warn('[parser] Dropping empty tool call block');
291
359
  if (this.emittedToolCallCount === 0 && this.pendingLeadIn.trim().length > 0) {
292
360
  result.text += this.pendingLeadIn;
@@ -295,7 +363,8 @@ export class StreamingToolParser {
295
363
  return;
296
364
  }
297
365
 
298
- // 1) Try Hermes-style XML <parameter> format first
366
+ t = unescapeDoubleEscaped(t);
367
+
299
368
  const xmlParsed = parseXmlParameterToolCall(t, this.currentOpenTag, this.tools);
300
369
  if (xmlParsed) {
301
370
  result.toolCalls.push({
@@ -358,8 +427,9 @@ export class StreamingToolParser {
358
427
  }
359
428
 
360
429
  private tryRecoverToolCall(block: string): ParsedToolCall | null {
361
- // Try full parse first
362
- const xmlParsed = parseXmlParameterToolCall(block, this.currentOpenTag, this.tools);
430
+ const unescaped = unescapeDoubleEscaped(block);
431
+
432
+ const xmlParsed = parseXmlParameterToolCall(unescaped, this.currentOpenTag, this.tools);
363
433
  if (xmlParsed) {
364
434
  return {
365
435
  id: `call_${crypto.randomUUID()}`,
@@ -368,8 +438,7 @@ export class StreamingToolParser {
368
438
  };
369
439
  }
370
440
 
371
- // Try recoverable (unclosed parameters)
372
- const recovered = parseRecoverableXmlToolCall(block, this.currentOpenTag, this.tools);
441
+ const recovered = parseRecoverableXmlToolCall(unescaped, this.currentOpenTag, this.tools);
373
442
  if (recovered) {
374
443
  return {
375
444
  id: `call_${crypto.randomUUID()}`,
@@ -378,11 +447,10 @@ export class StreamingToolParser {
378
447
  };
379
448
  }
380
449
 
381
- // Try JSON (single or multiple)
382
- const jsonParsed = this.parseToolContent(block);
450
+ const jsonParsed = this.parseToolContent(unescaped);
383
451
  if (jsonParsed.length > 0) {
384
452
  const first = jsonParsed[0];
385
- const attrName = extractToolName(this.currentOpenTag, block);
453
+ const attrName = extractToolName(this.currentOpenTag, unescaped);
386
454
  if (attrName && !first.name) first.name = attrName;
387
455
  if (first.name) return first;
388
456
  }
@@ -31,7 +31,7 @@ function truncateSemantically(content: string, maxChars: number): string {
31
31
  }
32
32
 
33
33
  export function truncateMessages(
34
- messages: Array<{ role: string; content: string | null | any[] }>,
34
+ messages: Array<{ role: string; content: string | null | any[] | Record<string, unknown> }>,
35
35
  maxContextLength: number,
36
36
  systemPrompt: string = ''
37
37
  ): Array<{ role: string; content: string }> {
package/src/utils/json.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * Robust JSON parsing utilities
5
5
  */
6
6
 
7
- function sanitizeAndBalance(input: string): { result: string; openBraces: number; openBrackets: number } {
7
+ function sanitizeAndBalance(input: string): { result: string; openBraces: number; openBrackets: number; inString: boolean } {
8
8
  let out = '';
9
9
  let openBraces = 0;
10
10
  let openBrackets = 0;
@@ -48,11 +48,12 @@ function sanitizeAndBalance(input: string): { result: string; openBraces: number
48
48
  if (char === ']') openBrackets--;
49
49
  }
50
50
  }
51
- return { result: out, openBraces, openBrackets };
51
+ return { result: out, openBraces, openBrackets, inString };
52
52
  }
53
53
 
54
- function closeBraces(input: string, openBraces: number, openBrackets: number): string {
54
+ function closeBraces(input: string, openBraces: number, openBrackets: number, inString: boolean = false): string {
55
55
  let out = input;
56
+ if (inString) out += '"';
56
57
  if (openBrackets > 0) out += ']'.repeat(openBrackets);
57
58
  if (openBraces > 0) out += '}'.repeat(openBraces);
58
59
  return out;
@@ -79,7 +80,7 @@ export function robustParseJSON(str: string): any {
79
80
  cleaned = cleaned.slice(0, -1).trim();
80
81
  }
81
82
 
82
- const { result: fixedJson, openBraces, openBrackets } = sanitizeAndBalance(cleaned);
83
+ const { result: fixedJson, openBraces, openBrackets, inString } = sanitizeAndBalance(cleaned);
83
84
  let lastBalancedIndex = -1;
84
85
 
85
86
  { let ob = 0, bk = 0, ins = false, esc = false;
@@ -99,15 +100,15 @@ export function robustParseJSON(str: string): any {
99
100
  let tempJson = fixedJson;
100
101
  if (lastBalancedIndex !== -1 && (openBraces !== 0 || openBrackets !== 0 || fixedJson.length > lastBalancedIndex + 1)) {
101
102
  tempJson = fixedJson.substring(0, lastBalancedIndex + 1);
102
- } else if (openBraces > 0 || openBrackets > 0) {
103
- tempJson = closeBraces(fixedJson, openBraces, openBrackets);
103
+ } else if (openBraces > 0 || openBrackets > 0 || inString) {
104
+ tempJson = closeBraces(fixedJson, openBraces, openBrackets, inString);
104
105
  }
105
106
 
106
107
  try { return JSON.parse(tempJson); } catch (e) {
107
108
  let aggressive = fixedJson.trim();
108
109
  if (aggressive.endsWith(',')) aggressive = aggressive.slice(0, -1);
109
- const { result: aggFixed, openBraces: ob, openBrackets: bk } = sanitizeAndBalance(aggressive);
110
- try { return JSON.parse(closeBraces(aggFixed, ob, bk)); } catch {
110
+ const { result: aggFixed, openBraces: ob, openBrackets: bk, inString: aggInString } = sanitizeAndBalance(aggressive);
111
+ try { return JSON.parse(closeBraces(aggFixed, ob, bk, aggInString)); } catch {
111
112
  return null;
112
113
  }
113
114
  }