@pedrofariasx/qwenproxy 1.2.1 → 1.2.2
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.
- package/README.md +3 -13
- package/package.json +1 -1
- package/src/api/server.ts +0 -2
- package/src/cache/memory-cache.ts +3 -2
- package/src/routes/chat.ts +123 -77
- package/src/routes/upload.ts +4 -4
- package/src/services/playwright.ts +1 -0
- package/src/services/qwen.ts +22 -13
- package/src/tools/parser.ts +10 -13
- package/src/utils/context-truncation.ts +0 -5
- package/src/linter/extraction-engine.ts +0 -165
- package/src/linter/index.ts +0 -258
- package/src/linter/repair-normalize.ts +0 -245
- package/src/linter/safety-gate.ts +0 -219
- package/src/linter/streaming-state-machine.ts +0 -252
- package/src/linter/structural-parser.ts +0 -352
- package/src/linter/types.ts +0 -74
- package/src/tests/linter.test.ts +0 -151
- package/src/tests/parallel.test.ts +0 -42
- package/src/tests/structureVerification.test.ts +0 -176
- package/src/tools/ast.ts +0 -15
- package/src/tools/coercion.ts +0 -67
- package/src/tools/confidence.ts +0 -48
- package/src/tools/detector.ts +0 -40
- package/src/tools/executor.ts +0 -236
- package/src/tools/pipeline.ts +0 -122
- package/src/tools/registry-runtime.ts +0 -34
- package/src/tools/repair.ts +0 -42
- package/src/tools/validator.ts +0 -33
|
@@ -1,352 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Layer 2: Structural AST Parser (Tolerant)
|
|
3
|
-
* Builds partial AST from JSON-like structures without strict parsing
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { PartialASTNode, ToolCallSource } from './types'
|
|
7
|
-
|
|
8
|
-
export interface StructuralParseResult {
|
|
9
|
-
ast: PartialASTNode | null
|
|
10
|
-
confidence: number
|
|
11
|
-
errors: string[]
|
|
12
|
-
sourceHint: ToolCallSource
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export class StructuralParser {
|
|
16
|
-
private pos = 0
|
|
17
|
-
private text = ''
|
|
18
|
-
private errors: string[] = []
|
|
19
|
-
|
|
20
|
-
parse(text: string, sourceHint: ToolCallSource = 'unknown'): StructuralParseResult {
|
|
21
|
-
this.pos = 0
|
|
22
|
-
this.text = text.trim()
|
|
23
|
-
this.errors = []
|
|
24
|
-
|
|
25
|
-
if (this.text.length === 0) {
|
|
26
|
-
return { ast: null, confidence: 0, errors: ['Empty input'], sourceHint }
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const nodes: PartialASTNode[] = []
|
|
30
|
-
|
|
31
|
-
while (this.pos < this.text.length) {
|
|
32
|
-
this.skipWhitespace()
|
|
33
|
-
if (this.pos >= this.text.length) break
|
|
34
|
-
const node = this.parseValue()
|
|
35
|
-
if (node) nodes.push(node)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (nodes.length === 0) {
|
|
39
|
-
return { ast: null, confidence: 0, errors: ['No parseable content'], sourceHint }
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const root: PartialASTNode = {
|
|
43
|
-
type: 'object',
|
|
44
|
-
raw: this.text,
|
|
45
|
-
confidence: this.calculateConfidence(nodes),
|
|
46
|
-
}
|
|
47
|
-
if (nodes.length === 1 && nodes[0].type === 'object' && nodes[0].value !== undefined) {
|
|
48
|
-
root.value = nodes[0].value
|
|
49
|
-
root.children = nodes[0].children
|
|
50
|
-
} else {
|
|
51
|
-
root.children = nodes
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return { ast: root, confidence: root.confidence, errors: this.errors, sourceHint }
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
private parseValue(): PartialASTNode | undefined {
|
|
58
|
-
const char = this.peek()
|
|
59
|
-
if (char === '{') return this.parseObject()
|
|
60
|
-
if (char === '[') return this.parseArray()
|
|
61
|
-
if (char === '"' || char === "'") return this.parseString()
|
|
62
|
-
if (this.isNull(char)) return this.parseNull()
|
|
63
|
-
if (this.isBoolean(char)) return this.parseBoolean()
|
|
64
|
-
if (this.isNumberStart(char)) return this.parseNumber()
|
|
65
|
-
if (this.isUnquotedKey(char)) return this.parseUnquotedValue()
|
|
66
|
-
if (this.isReActAction(char)) return this.parseReActBlock()
|
|
67
|
-
return undefined
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
private parseObject(): PartialASTNode {
|
|
71
|
-
const start = this.pos
|
|
72
|
-
this.expect('{')
|
|
73
|
-
const children: PartialASTNode[] = []
|
|
74
|
-
|
|
75
|
-
while (this.pos < this.text.length) {
|
|
76
|
-
this.skipWhitespace()
|
|
77
|
-
if (this.peek() === '}') { this.pos++; break }
|
|
78
|
-
|
|
79
|
-
const key = this.parseKey()
|
|
80
|
-
if (!key) { this.pos++; this.errors.push(`Expected key at position ${this.pos}`); continue }
|
|
81
|
-
|
|
82
|
-
this.skipWhitespace()
|
|
83
|
-
this.expectOptional(':')
|
|
84
|
-
this.skipWhitespace()
|
|
85
|
-
|
|
86
|
-
let value: PartialASTNode | undefined
|
|
87
|
-
const ch = this.peek()
|
|
88
|
-
if (ch === '"' || ch === "'") value = this.parseString()
|
|
89
|
-
else if (ch === '{') value = this.parseObject()
|
|
90
|
-
else if (ch === '[') value = this.parseArray()
|
|
91
|
-
else value = this.parseScalar()
|
|
92
|
-
|
|
93
|
-
if (value) {
|
|
94
|
-
children.push({
|
|
95
|
-
type: 'object',
|
|
96
|
-
value: value.value,
|
|
97
|
-
raw: this.text.slice(start, this.pos),
|
|
98
|
-
confidence: 1,
|
|
99
|
-
children: [key, value],
|
|
100
|
-
})
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
this.skipWhitespace()
|
|
104
|
-
if (this.expectOptional(',')) {
|
|
105
|
-
this.skipWhitespace()
|
|
106
|
-
if (this.peek() === '}') break
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const value: Record<string, unknown> = {}
|
|
111
|
-
for (const child of children) {
|
|
112
|
-
if (child.children && child.children.length >= 2) {
|
|
113
|
-
const key = child.children[0].value
|
|
114
|
-
const val = child.children[1].value
|
|
115
|
-
if (key !== undefined) value[key as string] = val
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return {
|
|
120
|
-
type: 'object',
|
|
121
|
-
value,
|
|
122
|
-
raw: this.text.slice(start, this.pos),
|
|
123
|
-
confidence: this.calculateObjectConfidence(children),
|
|
124
|
-
children,
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
private parseArray(): PartialASTNode {
|
|
129
|
-
const start = this.pos
|
|
130
|
-
this.expect('[')
|
|
131
|
-
const children: PartialASTNode[] = []
|
|
132
|
-
|
|
133
|
-
while (this.pos < this.text.length) {
|
|
134
|
-
this.skipWhitespace()
|
|
135
|
-
if (this.peek() === ']') { this.pos++; break }
|
|
136
|
-
const value = this.parseValue()
|
|
137
|
-
if (value) children.push(value)
|
|
138
|
-
this.skipWhitespace()
|
|
139
|
-
this.expectOptional(',')
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return {
|
|
143
|
-
type: 'array',
|
|
144
|
-
raw: this.text.slice(start, this.pos),
|
|
145
|
-
confidence: children.length > 0 ? 0.9 : 0.5,
|
|
146
|
-
children,
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
private parseString(): PartialASTNode {
|
|
151
|
-
const start = this.pos
|
|
152
|
-
const quote = this.peek()
|
|
153
|
-
this.expect(quote as '"' | "'")
|
|
154
|
-
let value = ''
|
|
155
|
-
try {
|
|
156
|
-
value = this.readStringContent(quote)
|
|
157
|
-
} catch {
|
|
158
|
-
this.errors.push(`Unterminated string at position ${start}`)
|
|
159
|
-
const content = this.text.slice(start + 1)
|
|
160
|
-
const idx = content.split('').findIndex((c, i) => c === quote && (i === 0 || content[i - 1] !== '\\'))
|
|
161
|
-
value = idx === -1 ? content : content.slice(0, idx)
|
|
162
|
-
this.pos = this.text.length
|
|
163
|
-
}
|
|
164
|
-
return { type: 'string', value, raw: this.text.slice(start, this.pos), confidence: 0.95 }
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
private readStringContent(quote: string): string {
|
|
168
|
-
let value = ''
|
|
169
|
-
while (this.pos < this.text.length) {
|
|
170
|
-
const char = this.text[this.pos++]
|
|
171
|
-
if (char === '\\' && this.pos < this.text.length) {
|
|
172
|
-
const next = this.text[this.pos++]
|
|
173
|
-
value += this.resolveEscapeSequence(char, next)
|
|
174
|
-
continue
|
|
175
|
-
}
|
|
176
|
-
if (char === quote) break
|
|
177
|
-
value += char
|
|
178
|
-
}
|
|
179
|
-
return value
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
private resolveEscapeSequence(_backslash: string, char: string): string {
|
|
183
|
-
const map: Record<string, string> = { n: '\n', t: '\t', r: '\r', b: '\b', f: '\f', '"': '"', "'": "'", '\\': '\\', '0': '\0' }
|
|
184
|
-
return map[char] ?? _backslash + char
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
private parseKey(): PartialASTNode | undefined {
|
|
188
|
-
const start = this.pos
|
|
189
|
-
if (this.peek() === '"' || this.peek() === "'") {
|
|
190
|
-
const key = this.parseString()
|
|
191
|
-
return { type: 'string', value: key.value, raw: key.raw, confidence: 1 }
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
let key = ''
|
|
195
|
-
while (this.pos < this.text.length) {
|
|
196
|
-
const c = this.text[this.pos]
|
|
197
|
-
if (/[a-zA-Z0-9_\-]/.test(c)) { key += c; this.pos++ }
|
|
198
|
-
else break
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (key.length === 0) { this.pos = start; return undefined }
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
private parseScalar(): PartialASTNode | undefined {
|
|
205
|
-
const start = this.pos
|
|
206
|
-
let value = ''
|
|
207
|
-
while (this.pos < this.text.length) {
|
|
208
|
-
const c = this.text[this.pos]
|
|
209
|
-
if (c === ',' || c === '}' || c === ']' || c === '\n') break
|
|
210
|
-
value += c
|
|
211
|
-
this.pos++
|
|
212
|
-
}
|
|
213
|
-
value = value.trim()
|
|
214
|
-
|
|
215
|
-
if (value === 'true') return { type: 'boolean', value: true, raw: value, confidence: 1 }
|
|
216
|
-
if (value === 'false') return { type: 'boolean', value: false, raw: value, confidence: 1 }
|
|
217
|
-
if (value === 'null' || value === 'undefined') return { type: 'null', value: null, raw: value, confidence: 0.9 }
|
|
218
|
-
const num = Number(value)
|
|
219
|
-
if (!isNaN(num) && isFinite(num)) return { type: 'number', value: num, raw: value, confidence: 0.9 }
|
|
220
|
-
if (value.startsWith('"') && value.endsWith('"')) return { type: 'string', value: value.slice(1, -1), raw: value, confidence: 0.85 }
|
|
221
|
-
if (value.startsWith("'") && value.endsWith("'")) return { type: 'string', value: value.slice(1, -1), raw: value, confidence: 0.8 }
|
|
222
|
-
return { type: 'unknown', value, raw: value, confidence: 0.3 }
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
private parseUnquotedValue(): PartialASTNode | undefined {
|
|
226
|
-
const start = this.pos
|
|
227
|
-
let value = ''
|
|
228
|
-
while (this.pos < this.text.length) {
|
|
229
|
-
const c = this.text[this.pos]
|
|
230
|
-
if (/[a-zA-Z0-9_\- ]/.test(c)) { value += c; this.pos++ }
|
|
231
|
-
else break
|
|
232
|
-
}
|
|
233
|
-
if (value.length === 0) return undefined
|
|
234
|
-
return { type: 'unknown', value: value.trim(), raw: value, confidence: 0.5 }
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
private parseNull(): PartialASTNode | undefined {
|
|
238
|
-
if (this.text.slice(this.pos, this.pos + 4) === 'null') {
|
|
239
|
-
this.pos += 4
|
|
240
|
-
return { type: 'null', value: null, raw: 'null', confidence: 1 }
|
|
241
|
-
}
|
|
242
|
-
return undefined
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
private parseBoolean(): PartialASTNode | undefined {
|
|
246
|
-
if (this.text.slice(this.pos, this.pos + 4) === 'true') {
|
|
247
|
-
this.pos += 4
|
|
248
|
-
return { type: 'boolean', value: true, raw: 'true', confidence: 1 }
|
|
249
|
-
}
|
|
250
|
-
if (this.text.slice(this.pos, this.pos + 5) === 'false') {
|
|
251
|
-
this.pos += 5
|
|
252
|
-
return { type: 'boolean', value: false, raw: 'false', confidence: 1 }
|
|
253
|
-
}
|
|
254
|
-
return undefined
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
private parseNumber(): PartialASTNode | undefined {
|
|
258
|
-
const start = this.pos
|
|
259
|
-
let value = ''
|
|
260
|
-
while (this.pos < this.text.length) {
|
|
261
|
-
const c = this.text[this.pos]
|
|
262
|
-
if (/[0-9.\-eE+]/.test(c)) { value += c; this.pos++ }
|
|
263
|
-
else break
|
|
264
|
-
}
|
|
265
|
-
if (value.length === 0) return undefined
|
|
266
|
-
const num = Number(value)
|
|
267
|
-
if (!isNaN(num) && isFinite(num)) return { type: 'number', value: num, raw: value, confidence: 0.9 }
|
|
268
|
-
return { type: 'unknown', value, raw: value, confidence: 0.3 }
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
private parseReActBlock(): PartialASTNode | undefined {
|
|
272
|
-
const start = this.pos
|
|
273
|
-
let action = ''
|
|
274
|
-
let input = ''
|
|
275
|
-
|
|
276
|
-
const actionMatch = this.text.slice(this.pos).match(/^Action:\s*(\w+)/i)
|
|
277
|
-
if (!actionMatch) return undefined
|
|
278
|
-
action = actionMatch[1]
|
|
279
|
-
this.pos += actionMatch[0].length
|
|
280
|
-
this.skipWhitespace()
|
|
281
|
-
|
|
282
|
-
const inputMatch = this.text.slice(this.pos).match(/^Action Input:\s*(\{[\s\S]*)/i)
|
|
283
|
-
if (inputMatch) {
|
|
284
|
-
input = inputMatch[1].trim()
|
|
285
|
-
this.pos += inputMatch[0].length
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
return { type: 'object', value: { action, input }, raw: this.text.slice(start, this.pos), confidence: 0.85 }
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
private isReActAction(char: string): boolean {
|
|
292
|
-
return /^Action:|^thought|^Observation|^Final Answer/i.test(this.text.slice(this.pos))
|
|
293
|
-
}
|
|
294
|
-
private isNull(char: string): boolean { return this.text.slice(this.pos, this.pos + 4) === 'null' }
|
|
295
|
-
private isBoolean(char: string): boolean {
|
|
296
|
-
return this.text.slice(this.pos, this.pos + 4) === 'true' || this.text.slice(this.pos, this.pos + 5) === 'false'
|
|
297
|
-
}
|
|
298
|
-
private isNumberStart(char: string): boolean { return /[0-9.\-]/.test(char) }
|
|
299
|
-
private isUnquotedKey(char: string): boolean { return /[a-zA-Z_]/.test(char) && !this.isNull(char) && !this.isReActAction(char) }
|
|
300
|
-
|
|
301
|
-
private skipWhitespace(): void {
|
|
302
|
-
while (this.pos < this.text.length && /\s/.test(this.text[this.pos])) this.pos++
|
|
303
|
-
}
|
|
304
|
-
private peek(): string { return this.text[this.pos] ?? '' }
|
|
305
|
-
private expect(char: string): boolean {
|
|
306
|
-
if (this.peek() === char) { this.pos++; return true }
|
|
307
|
-
this.errors.push(`Expected '${char}' at position ${this.pos}, got '${this.peek()}'`)
|
|
308
|
-
return false
|
|
309
|
-
}
|
|
310
|
-
private expectOptional(char: string): boolean {
|
|
311
|
-
if (this.peek() === char) { this.pos++; return true }
|
|
312
|
-
return false
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
private calculateConfidence(nodes: PartialASTNode[]): number {
|
|
316
|
-
if (nodes.length === 0) return 0
|
|
317
|
-
const avg = nodes.reduce((a, n) => a + n.confidence, 0) / nodes.length
|
|
318
|
-
return Math.max(0, Math.min(1, avg - Math.min(this.errors.length * 0.1, 0.5)))
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
private calculateObjectConfidence(children: PartialASTNode[]): number {
|
|
322
|
-
if (children.length === 0) return 0.1
|
|
323
|
-
let confidence = 0.7
|
|
324
|
-
if (children.some(c => c.children && c.children[0]?.type === 'string')) confidence += 0.2
|
|
325
|
-
if (this.errors.length === 0) confidence += 0.1
|
|
326
|
-
if (children.length >= 2) confidence += 0.05
|
|
327
|
-
return Math.min(0.99, confidence)
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
static extractJsonFromText(text: string): string[] {
|
|
331
|
-
const results: string[] = []
|
|
332
|
-
let depth = 0, start = -1, inString = false, escape = false
|
|
333
|
-
|
|
334
|
-
for (let i = 0; i < text.length; i++) {
|
|
335
|
-
const char = text[i]
|
|
336
|
-
if (escape) { escape = false; continue }
|
|
337
|
-
if (char === '\\' && inString) { escape = true; continue }
|
|
338
|
-
if (char === '"' && !escape) { inString = !inString; continue }
|
|
339
|
-
if (inString) continue
|
|
340
|
-
|
|
341
|
-
if (char === '{' || char === '[') {
|
|
342
|
-
if (depth === 0) start = i
|
|
343
|
-
depth++
|
|
344
|
-
} else if (char === '}' || char === ']') {
|
|
345
|
-
depth--
|
|
346
|
-
if (depth === 0 && start !== -1) { results.push(text.slice(start, i + 1)); start = -1 }
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
return results
|
|
351
|
-
}
|
|
352
|
-
}
|
package/src/linter/types.ts
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* File: types.ts
|
|
3
|
-
* Project: qwenproxy
|
|
4
|
-
* UltraToolCallLinter - Core type definitions
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export type ToolCallSource = 'openai' | 'claude' | 'gemini' | 'react' | 'unknown'
|
|
8
|
-
|
|
9
|
-
export interface CanonicalToolCall {
|
|
10
|
-
tool: string
|
|
11
|
-
input: Record<string, unknown>
|
|
12
|
-
meta?: {
|
|
13
|
-
source: ToolCallSource
|
|
14
|
-
confidence: number
|
|
15
|
-
repaired: boolean
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface ParserState {
|
|
20
|
-
buffer: string
|
|
21
|
-
insideCodeBlock: boolean
|
|
22
|
-
braceDepth: number
|
|
23
|
-
bracketDepth: number
|
|
24
|
-
inString: boolean
|
|
25
|
-
escapeNext: boolean
|
|
26
|
-
potentialToolStart: boolean
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface PartialASTNode {
|
|
30
|
-
type: 'object' | 'array' | 'string' | 'number' | 'boolean' | 'null' | 'unknown'
|
|
31
|
-
value?: unknown
|
|
32
|
-
raw: string
|
|
33
|
-
confidence: number
|
|
34
|
-
children?: PartialASTNode[]
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface RawToolCandidate {
|
|
38
|
-
source: ToolCallSource
|
|
39
|
-
raw: Record<string, unknown>
|
|
40
|
-
rawString: string
|
|
41
|
-
confidence: number
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface ParseResult {
|
|
45
|
-
text: string
|
|
46
|
-
toolCalls: CanonicalToolCall[]
|
|
47
|
-
errors: string[]
|
|
48
|
-
confidence: number
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export interface SecurityViolation {
|
|
52
|
-
type: 'shell_injection' | 'filesystem_access' | 'ssrf' | 'prompt_injection' | 'encoded_payload'
|
|
53
|
-
field: string
|
|
54
|
-
value: string
|
|
55
|
-
detail: string
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export interface ToolRegistry {
|
|
59
|
-
[name: string]: {
|
|
60
|
-
name: string
|
|
61
|
-
description: string
|
|
62
|
-
parameters: Record<string, unknown>
|
|
63
|
-
required?: string[]
|
|
64
|
-
strict?: boolean
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export interface ToolDefinition {
|
|
69
|
-
name: string
|
|
70
|
-
description: string
|
|
71
|
-
parameters: Record<string, unknown>
|
|
72
|
-
required?: string[]
|
|
73
|
-
strict?: boolean
|
|
74
|
-
}
|
package/src/tests/linter.test.ts
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* UltraToolCallLinter v1.0 - Edge Case Stress Tests
|
|
3
|
-
* Tests required by spec
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { describe, it, beforeEach, afterEach } from 'node:test'
|
|
7
|
-
import assert from 'node:assert'
|
|
8
|
-
|
|
9
|
-
import { UltraToolCallLinter } from '../linter/index.js'
|
|
10
|
-
import type { ParseResult, RawToolCandidate } from '../linter/types.js'
|
|
11
|
-
|
|
12
|
-
// Helper functions to match the old bar.ts API using direct instantiation
|
|
13
|
-
let globalLinter: UltraToolCallLinter | null = null
|
|
14
|
-
|
|
15
|
-
function getGlobalLinter(): UltraToolCallLinter {
|
|
16
|
-
if (!globalLinter) globalLinter = new UltraToolCallLinter()
|
|
17
|
-
return globalLinter
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function configure(config: { registry?: any; strictMode?: boolean; enableSecurityGate?: boolean; maxRecoveryAttempts?: number; minConfidenceThreshold?: number }): void {
|
|
21
|
-
globalLinter = new UltraToolCallLinter(config)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function parseText(input: string): ParseResult {
|
|
25
|
-
return getGlobalLinter().parseText(input)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function extract(input: string): RawToolCandidate[] {
|
|
29
|
-
return getGlobalLinter().extract(input)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function repair(input: string): string {
|
|
33
|
-
return getGlobalLinter().repair(input)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
describe('UltraToolCallLinter Edge Cases', () => {
|
|
37
|
-
it('parses truncated JSON in the middle', () => {
|
|
38
|
-
configure({ minConfidenceThreshold: 0.1 })
|
|
39
|
-
const input = 'Here is the query: {"search": {"query": "test"}}'
|
|
40
|
-
const result = parseText(input)
|
|
41
|
-
assert.ok(result.toolCalls.length >= 1)
|
|
42
|
-
assert.equal(result.toolCalls[0].tool, 'search')
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
it('parses concatenated multiple JSONs', () => {
|
|
46
|
-
const input = '{"name":"search","arguments":{"query":"foo"}}{"name":"browser_open","arguments":{"url":"http://x"}}'
|
|
47
|
-
const candidates = extract(input)
|
|
48
|
-
assert.ok(candidates.length >= 1, 'Expected at least one candidate')
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
it('parses markdown-wrapped JSON', () => {
|
|
52
|
-
const input = '```json\n{"tool":"search","input":{"query":"beer"}}\n```'
|
|
53
|
-
const result = parseText(input)
|
|
54
|
-
assert.ok(result.toolCalls.length >= 1)
|
|
55
|
-
assert.equal(result.toolCalls[0].tool, 'search')
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
it('parses human text mixed with a tool call (ReAct)', () => {
|
|
59
|
-
const input =
|
|
60
|
-
'Sure, let me search that for you.\nAction: search\nAction Input: {"query": "breaking news"}'
|
|
61
|
-
const result = parseText(input)
|
|
62
|
-
assert.ok(result.toolCalls.length >= 1, 'Expected ReAct extraction')
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
it('recovers from broken escape sequences', () => {
|
|
66
|
-
const input = '{"tool":"search","input":{"query":"test\\\\n\\\\t\\\\"}}'
|
|
67
|
-
const result = parseText(input)
|
|
68
|
-
assert.ok(result.toolCalls.length >= 1)
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
it('recovers from unicode corruption', () => {
|
|
72
|
-
const input = '{"tool":"search","input":{"query":"caf\u0000\u001Fé"}}'
|
|
73
|
-
const result = parseText(input)
|
|
74
|
-
assert.ok(result.toolCalls.length >= 1)
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
it('handles streaming with repeated chunks', () => {
|
|
78
|
-
const linter = new UltraToolCallLinter({ minConfidenceThreshold: 0.1 })
|
|
79
|
-
const chunks = [
|
|
80
|
-
'S',
|
|
81
|
-
'Su',
|
|
82
|
-
'Sur',
|
|
83
|
-
'Sure! ',
|
|
84
|
-
'Here i',
|
|
85
|
-
'Here is ',
|
|
86
|
-
'Here is the ',
|
|
87
|
-
'Here is the result: ',
|
|
88
|
-
'Here is the result: {"tool":"search","input":{"query":"async"}}',
|
|
89
|
-
]
|
|
90
|
-
for (const c of chunks) linter.push(c)
|
|
91
|
-
const result = linter.parse()
|
|
92
|
-
assert.ok(result.toolCalls.length >= 1)
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
it('extracts tool call inside a textual array', () => {
|
|
96
|
-
const input =
|
|
97
|
-
'Options:\n- `{"tool":"search","input":{"query":"foo"}}`\n- `{"tool":"search","input":{"query":"bar"}}`'
|
|
98
|
-
const candidates = extract(input)
|
|
99
|
-
assert.ok(candidates.length >= 1, 'Expected extraction from array-text format')
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
it('handles duplicate model output', () => {
|
|
103
|
-
const input =
|
|
104
|
-
'{"tool":"search","input":{"query":"dup"}}\n\n{"tool":"search","input":{"query":"dup"}}'
|
|
105
|
-
const result = parseText(input)
|
|
106
|
-
assert.ok(Array.isArray(result.toolCalls))
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
it('repair: single quotes to double quotes', () => {
|
|
110
|
-
const input = "{'tool': 'search', 'input': {'query': 'test'}}"
|
|
111
|
-
const repaired = repair(input)
|
|
112
|
-
assert.ok(repaired.includes('search'))
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
it('repair: trailing comma', () => {
|
|
116
|
-
const input = '{"tool": "search", "input": {"query": "test"},}'
|
|
117
|
-
const repaired = repair(input)
|
|
118
|
-
assert.ok(repaired.includes('search'))
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
it('repair: key without quotes', () => {
|
|
122
|
-
const input = '{tool: "search", input: {query: "test"}}'
|
|
123
|
-
const repaired = repair(input)
|
|
124
|
-
assert.ok(repaired.includes('search'))
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
it('security gate blocks destructive shell injection', () => {
|
|
128
|
-
configure({ minConfidenceThreshold: 0.0 })
|
|
129
|
-
const input = '{"tool":"search","input":{"query":"\'; rm -rf /; echo \'"}}'
|
|
130
|
-
const result = parseText(input)
|
|
131
|
-
const passed = result.toolCalls.some((call: any) => {
|
|
132
|
-
const val = (call.input.query ?? '') as string
|
|
133
|
-
return typeof val === 'string' && val.includes('rm -rf /')
|
|
134
|
-
})
|
|
135
|
-
assert.strictEqual(passed, false, 'Should not pass through destructive payload')
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
it('rejects empty string values per spec', () => {
|
|
139
|
-
configure({ minConfidenceThreshold: 0.0 })
|
|
140
|
-
const input = '{"tool":"search","input":{"query":""}}'
|
|
141
|
-
const result = parseText(input)
|
|
142
|
-
assert.ok(Array.isArray(result.toolCalls))
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
it('parseText returns canonical tool calls', () => {
|
|
146
|
-
configure({ minConfidenceThreshold: 0.1 })
|
|
147
|
-
const input = '{"tool":"search","input":{"query":"x"}}'
|
|
148
|
-
const result = parseText(input)
|
|
149
|
-
assert.ok(Array.isArray(result.toolCalls))
|
|
150
|
-
})
|
|
151
|
-
})
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { test } from 'node:test';
|
|
2
|
-
import assert from 'node:assert';
|
|
3
|
-
import { executeToolCalls } from '../tools/executor.ts';
|
|
4
|
-
import { registry } from '../tools/registry.ts';
|
|
5
|
-
import type { ToolContext } from '../tools/types.ts';
|
|
6
|
-
|
|
7
|
-
test('executeToolCalls: parallel execution', async () => {
|
|
8
|
-
let activeCount = 0;
|
|
9
|
-
let maxParallel = 0;
|
|
10
|
-
|
|
11
|
-
registry.register(
|
|
12
|
-
'parallel_tool',
|
|
13
|
-
'A tool that waits to test parallelism',
|
|
14
|
-
{ type: 'object', properties: {} },
|
|
15
|
-
async () => {
|
|
16
|
-
activeCount++;
|
|
17
|
-
maxParallel = Math.max(maxParallel, activeCount);
|
|
18
|
-
await new Promise(r => setTimeout(r, 100));
|
|
19
|
-
activeCount--;
|
|
20
|
-
return 'done';
|
|
21
|
-
}
|
|
22
|
-
);
|
|
23
|
-
|
|
24
|
-
const toolCalls = [
|
|
25
|
-
{ id: '1', name: 'parallel_tool', arguments: {} },
|
|
26
|
-
{ id: '2', name: 'parallel_tool', arguments: {} },
|
|
27
|
-
{ id: '3', name: 'parallel_tool', arguments: {} },
|
|
28
|
-
];
|
|
29
|
-
|
|
30
|
-
const context: ToolContext = {
|
|
31
|
-
messages: [],
|
|
32
|
-
turn: 0,
|
|
33
|
-
model: 'test'
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const results = await executeToolCalls(toolCalls, context);
|
|
37
|
-
|
|
38
|
-
assert.strictEqual(results.length, 3);
|
|
39
|
-
assert.ok(maxParallel > 1, `Max parallel should be > 1, got ${maxParallel}`);
|
|
40
|
-
|
|
41
|
-
registry.unregister('parallel_tool');
|
|
42
|
-
});
|