@pedrofariasx/qwenproxy 1.1.0
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/LICENSE +13 -0
- package/README.md +292 -0
- package/bin/qwenproxy.mjs +11 -0
- package/package.json +56 -0
- package/src/api/models.ts +183 -0
- package/src/api/server.ts +126 -0
- package/src/cache/memory-cache.ts +186 -0
- package/src/core/account-manager.ts +132 -0
- package/src/core/accounts.ts +78 -0
- package/src/core/config.ts +91 -0
- package/src/core/database.ts +92 -0
- package/src/core/logger.ts +96 -0
- package/src/core/metrics.ts +169 -0
- package/src/core/model-registry.ts +30 -0
- package/src/core/stream-registry.ts +40 -0
- package/src/core/watchdog.ts +130 -0
- package/src/index.ts +7 -0
- package/src/linter/extraction-engine.ts +165 -0
- package/src/linter/index.ts +258 -0
- package/src/linter/repair-normalize.ts +245 -0
- package/src/linter/safety-gate.ts +219 -0
- package/src/linter/streaming-state-machine.ts +252 -0
- package/src/linter/structural-parser.ts +352 -0
- package/src/linter/types.ts +74 -0
- package/src/login.ts +228 -0
- package/src/routes/chat.ts +801 -0
- package/src/routes/upload.ts +700 -0
- package/src/services/playwright.ts +778 -0
- package/src/services/qwen.ts +500 -0
- package/src/tests/advanced.test.ts +227 -0
- package/src/tests/agenticStress.test.ts +360 -0
- package/src/tests/concurrency.test.ts +103 -0
- package/src/tests/concurrentChat.test.ts +71 -0
- package/src/tests/delta.test.ts +63 -0
- package/src/tests/index.test.ts +356 -0
- package/src/tests/jsonFix.test.ts +98 -0
- package/src/tests/linter.test.ts +151 -0
- package/src/tests/parallel.test.ts +42 -0
- package/src/tests/parser.test.ts +89 -0
- package/src/tests/rotation.test.ts +45 -0
- package/src/tests/streamingOptimizations.test.ts +328 -0
- package/src/tests/structureVerification.test.ts +176 -0
- package/src/tools/ast.ts +15 -0
- package/src/tools/coercion.ts +67 -0
- package/src/tools/confidence.ts +48 -0
- package/src/tools/detector.ts +40 -0
- package/src/tools/executor.ts +236 -0
- package/src/tools/parser.ts +446 -0
- package/src/tools/pipeline.ts +122 -0
- package/src/tools/registry-runtime.ts +34 -0
- package/src/tools/registry.ts +142 -0
- package/src/tools/repair.ts +42 -0
- package/src/tools/schema.ts +285 -0
- package/src/tools/types.ts +104 -0
- package/src/tools/validator.ts +33 -0
- package/src/utils/context-truncation.ts +61 -0
- package/src/utils/json.ts +114 -0
- package/src/utils/qwen-stream-parser.ts +286 -0
- package/src/utils/types.ts +101 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* UltraToolCallLinter v1.0
|
|
3
|
+
* Main public API: composable 5-layer pipeline
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
CanonicalToolCall,
|
|
8
|
+
ParserState,
|
|
9
|
+
RawToolCandidate,
|
|
10
|
+
ParseResult,
|
|
11
|
+
ToolCallSource,
|
|
12
|
+
SecurityViolation,
|
|
13
|
+
ToolDefinition,
|
|
14
|
+
ToolRegistry,
|
|
15
|
+
} from './types'
|
|
16
|
+
|
|
17
|
+
import { StreamingStateMachine } from './streaming-state-machine'
|
|
18
|
+
import { StructuralParser } from './structural-parser'
|
|
19
|
+
import { ToolExtractionEngine } from './extraction-engine'
|
|
20
|
+
import { GrammarRepairEngine, NormalizationEngine } from './repair-normalize'
|
|
21
|
+
import { SafetyGate } from './safety-gate'
|
|
22
|
+
|
|
23
|
+
export interface LinterConfig {
|
|
24
|
+
registry?: ToolRegistry
|
|
25
|
+
strictMode?: boolean
|
|
26
|
+
enableSecurityGate?: boolean
|
|
27
|
+
maxRecoveryAttempts?: number
|
|
28
|
+
minConfidenceThreshold?: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class UltraToolCallLinter {
|
|
32
|
+
private readonly streaming = new StreamingStateMachine()
|
|
33
|
+
private readonly extractor = new ToolExtractionEngine()
|
|
34
|
+
private readonly repairer = new GrammarRepairEngine()
|
|
35
|
+
private readonly normalizer = new NormalizationEngine()
|
|
36
|
+
private readonly gate = new SafetyGate()
|
|
37
|
+
|
|
38
|
+
private registry: ToolRegistry = {}
|
|
39
|
+
private strictMode: boolean = false
|
|
40
|
+
private enableSecurityGate: boolean = true
|
|
41
|
+
private maxRecoveryAttempts: number = 3
|
|
42
|
+
private minConfidenceThreshold: number = 0.3
|
|
43
|
+
|
|
44
|
+
constructor(config: LinterConfig = {}) {
|
|
45
|
+
if (config.registry) this.registry = config.registry
|
|
46
|
+
if (config.strictMode !== undefined) this.strictMode = config.strictMode
|
|
47
|
+
if (config.enableSecurityGate !== undefined) this.enableSecurityGate = config.enableSecurityGate
|
|
48
|
+
if (config.maxRecoveryAttempts !== undefined) this.maxRecoveryAttempts = config.maxRecoveryAttempts
|
|
49
|
+
if (config.minConfidenceThreshold !== undefined) this.minConfidenceThreshold = config.minConfidenceThreshold
|
|
50
|
+
|
|
51
|
+
if (this.registry) this.gate.registerRegistry(this.registry)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
setRegistry(registry: ToolRegistry): void {
|
|
55
|
+
this.registry = registry
|
|
56
|
+
this.gate.registerRegistry(registry)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
registerTool(name: string, def: ToolDefinition): void {
|
|
60
|
+
this.gate.registerTool(name, def)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
push(chunk: string): void {
|
|
64
|
+
this.streaming.push(chunk)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
parse(): ParseResult {
|
|
68
|
+
const buffer = this.streaming.getBuffer()
|
|
69
|
+
const errors: string[] = []
|
|
70
|
+
|
|
71
|
+
const { candidates, extractionErrors } = this.extractor.extract(buffer)
|
|
72
|
+
errors.push(...extractionErrors)
|
|
73
|
+
|
|
74
|
+
const toolCalls: CanonicalToolCall[] = []
|
|
75
|
+
let maxConfidence = 0
|
|
76
|
+
|
|
77
|
+
for (const candidate of candidates) {
|
|
78
|
+
const result = this.processCandidate(candidate, errors)
|
|
79
|
+
const tc: CanonicalToolCall = {
|
|
80
|
+
tool: result.tool,
|
|
81
|
+
input: result.input,
|
|
82
|
+
meta: result.meta ?? { source: candidate.source, confidence: 0, repaired: false },
|
|
83
|
+
}
|
|
84
|
+
if (tc.meta) {
|
|
85
|
+
tc.meta.confidence = result.meta?.confidence ?? 0
|
|
86
|
+
tc.meta.repaired = result.meta?.repaired ?? false
|
|
87
|
+
} else {
|
|
88
|
+
tc.meta = { source: candidate.source, confidence: result.meta?.confidence ?? 0, repaired: result.meta?.repaired ?? false }
|
|
89
|
+
}
|
|
90
|
+
toolCalls.push(tc)
|
|
91
|
+
maxConfidence = Math.max(maxConfidence, tc.meta.confidence)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this.streaming.reset()
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
text: buffer,
|
|
98
|
+
toolCalls,
|
|
99
|
+
errors,
|
|
100
|
+
confidence: maxConfidence,
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
parseText(text: string): ParseResult {
|
|
105
|
+
this.streaming.reset()
|
|
106
|
+
this.streaming.push(text)
|
|
107
|
+
return this.parse()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
parseObject(name: string, argumentsObj: Record<string, unknown>): ParseResult {
|
|
111
|
+
const candidate: RawToolCandidate = {
|
|
112
|
+
source: 'openai',
|
|
113
|
+
raw: { name, arguments: argumentsObj },
|
|
114
|
+
rawString: JSON.stringify({ name, arguments: argumentsObj }),
|
|
115
|
+
confidence: 1,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const errors: string[] = []
|
|
119
|
+
const processingResult = this.processCandidate(candidate, errors)
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
text: '',
|
|
123
|
+
toolCalls: [processingResult],
|
|
124
|
+
errors,
|
|
125
|
+
confidence: processingResult.meta?.confidence ?? 0,
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
repair(input: string): string {
|
|
130
|
+
const result = this.repairer.repair(input)
|
|
131
|
+
if (!result.repaired) {
|
|
132
|
+
const structural = new StructuralParser()
|
|
133
|
+
const parsed = structural.parse(input, 'unknown')
|
|
134
|
+
if (parsed.ast?.value) return JSON.stringify(parsed.ast.value, null, 0)
|
|
135
|
+
}
|
|
136
|
+
return JSON.stringify(result.value ?? {}, null, 0)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
extract(input: string): RawToolCandidate[] {
|
|
140
|
+
this.streaming.reset()
|
|
141
|
+
const { candidates } = this.extractor.extract(input)
|
|
142
|
+
return candidates
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
reset(): void {
|
|
146
|
+
this.streaming.reset()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
getState(): ParserState {
|
|
150
|
+
return this.streaming.getState()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
public processCandidate(
|
|
154
|
+
candidate: RawToolCandidate,
|
|
155
|
+
errors: string[]
|
|
156
|
+
): CanonicalToolCall {
|
|
157
|
+
let confidence = candidate.confidence
|
|
158
|
+
let repaired = false
|
|
159
|
+
let currentRaw = { ...candidate.raw } as Record<string, unknown>
|
|
160
|
+
|
|
161
|
+
const args = currentRaw.arguments ?? currentRaw.input ?? currentRaw.args
|
|
162
|
+
|
|
163
|
+
if (typeof args === 'string' && args.trim().length > 0) {
|
|
164
|
+
const repairResult = this.repairer.repair(args)
|
|
165
|
+
if (repairResult.repaired) {
|
|
166
|
+
currentRaw = { ...currentRaw, arguments: repairResult.value }
|
|
167
|
+
repaired = true
|
|
168
|
+
confidence = Math.min(confidence + repairResult.confidence * 0.1, 0.95)
|
|
169
|
+
this.recoveryLog('string-args-repaired', candidate, repairResult.strategy)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!currentRaw.tool && !currentRaw.name && !currentRaw.functionCall && !currentRaw.type) {
|
|
174
|
+
const repairResult = this.repairer.repair(candidate.rawString)
|
|
175
|
+
if (repairResult.repaired) {
|
|
176
|
+
currentRaw = repairResult.value as Record<string, unknown>
|
|
177
|
+
repaired = true
|
|
178
|
+
confidence = Math.min(confidence + 0.05, 0.9)
|
|
179
|
+
this.recoveryLog('synthetic-construction', candidate, repairResult.strategy)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const { toolCall, warnings } = this.normalizer.normalize(
|
|
184
|
+
{ source: candidate.source, raw: currentRaw, rawString: candidate.rawString, confidence },
|
|
185
|
+
repaired
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
if (warnings.length > 0) errors.push(...warnings)
|
|
189
|
+
|
|
190
|
+
if (!toolCall.meta) {
|
|
191
|
+
toolCall.meta = { source: candidate.source, confidence, repaired }
|
|
192
|
+
} else {
|
|
193
|
+
toolCall.meta.confidence = confidence
|
|
194
|
+
toolCall.meta.repaired = repaired
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (this.enableSecurityGate) {
|
|
198
|
+
const report = this.gate.validate(toolCall, candidate.source)
|
|
199
|
+
toolCall.meta.confidence = Math.min(toolCall.meta.confidence, report.confidence)
|
|
200
|
+
|
|
201
|
+
if (!report.isValid && !this.strictMode) {
|
|
202
|
+
const recoveryResult = this.attemptRecovery(candidate, report.violations, toolCall, errors)
|
|
203
|
+
if (recoveryResult) return recoveryResult
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
errors.push(...report.warnings)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return toolCall
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private attemptRecovery(
|
|
213
|
+
candidate: RawToolCandidate,
|
|
214
|
+
violations: SecurityViolation[],
|
|
215
|
+
failedCall: CanonicalToolCall,
|
|
216
|
+
errors: string[]
|
|
217
|
+
): CanonicalToolCall | null {
|
|
218
|
+
let attempt = 0
|
|
219
|
+
const currentCall: CanonicalToolCall = {
|
|
220
|
+
tool: failedCall.tool,
|
|
221
|
+
input: { ...failedCall.input },
|
|
222
|
+
meta: failedCall.meta ? { ...failedCall.meta } : undefined,
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
while (attempt < this.maxRecoveryAttempts) {
|
|
226
|
+
for (const v of violations) {
|
|
227
|
+
if (v.field === 'input') {
|
|
228
|
+
currentCall.input = {}
|
|
229
|
+
errors.push(`Recovered: cleared entire input`)
|
|
230
|
+
break
|
|
231
|
+
}
|
|
232
|
+
if (v.field in currentCall.input) {
|
|
233
|
+
delete currentCall.input[v.field]
|
|
234
|
+
errors.push(`Recovered: removed invalid field "${v.field}"`)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const report = this.gate.validate(currentCall, candidate.source)
|
|
239
|
+
if (report.isValid) {
|
|
240
|
+
if (currentCall.meta) {
|
|
241
|
+
currentCall.meta.repaired = true
|
|
242
|
+
currentCall.meta.confidence = Math.max(0.3, report.confidence)
|
|
243
|
+
}
|
|
244
|
+
return currentCall
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
attempt++
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return null
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private recoveryLog(event: string, candidate: RawToolCandidate, strategy: string): void {
|
|
254
|
+
if (process.env.DEBUG_ULTRA_LINTER === 'true') {
|
|
255
|
+
console.debug(`[UltraToolCallLinter] recovery ${event}: strategy=${strategy}, source=${candidate.source}`)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Layer 4: Normalization + Repair Engine
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { CanonicalToolCall, ToolCallSource, RawToolCandidate } from './types'
|
|
6
|
+
import { StructuralParser } from './structural-parser'
|
|
7
|
+
|
|
8
|
+
export interface RepairResult {
|
|
9
|
+
repaired: boolean
|
|
10
|
+
value: unknown
|
|
11
|
+
confidence: number
|
|
12
|
+
strategy: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface NormalizationResult {
|
|
16
|
+
toolCall: CanonicalToolCall
|
|
17
|
+
warnings: string[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class GrammarRepairEngine {
|
|
21
|
+
private parser: StructuralParser = new StructuralParser()
|
|
22
|
+
|
|
23
|
+
repair(input: string): RepairResult {
|
|
24
|
+
const strategies: Array<{ name: string; attempt: () => unknown | undefined }> = [
|
|
25
|
+
{ name: 'json_strip_repair', attempt: () => this.tryStripRepair(input) },
|
|
26
|
+
{ name: 'quote_fix', attempt: () => this.tryQuoteFix(input) },
|
|
27
|
+
{ name: 'trailing_comma_fix', attempt: () => this.tryTrailingCommaFix(input) },
|
|
28
|
+
{ name: 'braces_balance', attempt: () => this.tryBraceBalance(input) },
|
|
29
|
+
{ name: 'key_unquote', attempt: () => this.tryKeyUnquote(input) },
|
|
30
|
+
{ name: 'parser_resync', attempt: () => this.tryParserResync(input) },
|
|
31
|
+
{ name: 'synthetic_construction', attempt: () => this.trySyntheticConstruction(input) },
|
|
32
|
+
{ name: 'json_parse_last_resort', attempt: () => this.tryJsonParseLastResort(input) },
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
for (const strategy of strategies) {
|
|
36
|
+
try {
|
|
37
|
+
const result = strategy.attempt()
|
|
38
|
+
if (result !== undefined && result !== null) {
|
|
39
|
+
return { repaired: true, value: result, confidence: this.inferConfidence(strategy.name), strategy: strategy.name }
|
|
40
|
+
}
|
|
41
|
+
} catch { continue }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { repaired: false, value: null, confidence: 0, strategy: 'failed' }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private tryStripRepair(input: string): Record<string, unknown> | undefined {
|
|
48
|
+
const stripped = input
|
|
49
|
+
.replace(/```json\s*/gi, '')
|
|
50
|
+
.replace(/```\s*$/gm, '')
|
|
51
|
+
.replace(/```\s*/gi, '')
|
|
52
|
+
.replace(/^\s*[\[\{]\s*$/, '')
|
|
53
|
+
.replace(/[\u0000-\u001F]+/g, ' ')
|
|
54
|
+
.trim()
|
|
55
|
+
|
|
56
|
+
if (stripped === input) return undefined
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const parsed = JSON.parse(stripped)
|
|
60
|
+
if (typeof parsed === 'object' && parsed !== null) return parsed
|
|
61
|
+
} catch { /* not parseable */ }
|
|
62
|
+
|
|
63
|
+
return undefined
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private tryQuoteFix(input: string): Record<string, unknown> | undefined {
|
|
67
|
+
let fixed = input
|
|
68
|
+
const quoteCount = (fixed.match(/"/g) ?? []).length
|
|
69
|
+
if (quoteCount % 2 !== 0) fixed += '"'
|
|
70
|
+
|
|
71
|
+
if (/['"]/.test(fixed) && /(^|[,:{\s])'[a-z]/i.test(fixed)) {
|
|
72
|
+
fixed = fixed.replace(/'/g, '"')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const parsed = JSON.parse(fixed)
|
|
77
|
+
if (typeof parsed === 'object' && parsed !== null) return parsed
|
|
78
|
+
} catch { /* not parseable */ }
|
|
79
|
+
|
|
80
|
+
return undefined
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private tryTrailingCommaFix(input: string): Record<string, unknown> | undefined {
|
|
84
|
+
let fixed = input.replace(/,(\s*[}\]])/g, '$1')
|
|
85
|
+
if (fixed === input) return undefined
|
|
86
|
+
try {
|
|
87
|
+
const parsed = JSON.parse(fixed)
|
|
88
|
+
if (typeof parsed === 'object' && parsed !== null) return parsed
|
|
89
|
+
} catch { /* not parseable */ }
|
|
90
|
+
return undefined
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private tryBraceBalance(input: string): Record<string, unknown> | undefined {
|
|
94
|
+
let depth = 0, start = -1
|
|
95
|
+
for (let i = 0; i < input.length; i++) {
|
|
96
|
+
if (input[i] === '{') { if (depth === 0) start = i; depth++ }
|
|
97
|
+
else if (input[i] === '}') {
|
|
98
|
+
depth--
|
|
99
|
+
if (depth === 0 && start !== -1) {
|
|
100
|
+
try { const parsed = JSON.parse(input.slice(start, i + 1)); if (typeof parsed === 'object' && parsed !== null) return parsed }
|
|
101
|
+
catch { /* not parseable */ }
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return undefined
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private tryKeyUnquote(input: string): Record<string, unknown> | undefined {
|
|
109
|
+
if (!/^\{[\s]*[a-zA-Z_]/m.test(input)) return undefined
|
|
110
|
+
let fixed = input.replace(/([,{]\s*)([a-zA-Z_][a-zA-Z0-9_\-]*)(\s*:)/g, '$1"$2"$3')
|
|
111
|
+
try {
|
|
112
|
+
const parsed = JSON.parse(fixed)
|
|
113
|
+
if (typeof parsed === 'object' && parsed !== null) return parsed
|
|
114
|
+
} catch { /* not parseable */ }
|
|
115
|
+
return undefined
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private tryParserResync(input: string): Record<string, unknown> | undefined {
|
|
119
|
+
const result = this.parser.parse(input, 'unknown')
|
|
120
|
+
if (result.ast && result.confidence >= 0.5 && result.ast.type === 'object') {
|
|
121
|
+
return result.ast.value as Record<string, unknown>
|
|
122
|
+
}
|
|
123
|
+
return undefined
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private trySyntheticConstruction(input: string): Record<string, unknown> | undefined {
|
|
127
|
+
const nameMatch = input.match(/"name"\s*:\s*"([^"]+)"|name\s*[:=]\s*['"]?(\w+)['"]?/i)
|
|
128
|
+
const toolMatch = input.match(/"tool"\s*:\s*"([^"]+)"|tool\s*[:=]\s*['"]?(\w+)['"]?/i)
|
|
129
|
+
const funcNameMatch = input.match(/"functionCall"\s*:\s*\{\s*"name"\s*:\s*"([^"]+)"/)
|
|
130
|
+
const toolUseMatch = input.match(/"type"\s*:\s*"tool_use"[\s\S]*?"name"\s*:\s*"([^"]+)"/)
|
|
131
|
+
|
|
132
|
+
if (nameMatch || funcNameMatch || toolUseMatch || toolMatch) {
|
|
133
|
+
const toolName = nameMatch?.[1] ?? nameMatch?.[2] ?? funcNameMatch?.[1] ?? toolUseMatch?.[1] ?? toolMatch?.[1] ?? toolMatch?.[2] ?? 'unknown'
|
|
134
|
+
const inputBlock = this.extractInputBlock(input)
|
|
135
|
+
return { tool: toolName, input: inputBlock ?? {} }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return undefined
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private tryJsonParseLastResort(input: string): Record<string, unknown> | undefined {
|
|
142
|
+
try {
|
|
143
|
+
const parsed = JSON.parse(input)
|
|
144
|
+
if (typeof parsed === 'object' && parsed !== null) return parsed
|
|
145
|
+
} catch { /* last resort failed */ }
|
|
146
|
+
return undefined
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private extractInputBlock(input: string): Record<string, unknown> | undefined {
|
|
150
|
+
const argsMatch = input.match(new RegExp('"arguments"\\s*:\\s*(\\{[\\s\\S]*\\})', 'i'))
|
|
151
|
+
const inputMatch = input.match(new RegExp('"input"\\s*:\\s*(\\{[\\s\\S]*\\})', 'i'))
|
|
152
|
+
const argsMatch2 = input.match(new RegExp('"args"\\s*:\\s*(\\{[\\s\\S]*\\})', 'i'))
|
|
153
|
+
const funcArgsMatch = input.match(/"functionCall"\s*:\s*\{\s*"name"\s*:\s*"[^"]+"\s*,\s*"args"\s*:\s*(\{[\s\S]*?\})\s*\}/)
|
|
154
|
+
const toolInputMatch = input.match(/"type"\s*:\s*"tool_use"[\s\S]*?"input"\s*:\s*(\{[\s\S]*?\})\s*\}/)
|
|
155
|
+
|
|
156
|
+
const target = argsMatch?.[1] ?? inputMatch?.[1] ?? argsMatch2?.[1] ?? funcArgsMatch?.[1] ?? toolInputMatch?.[1]
|
|
157
|
+
if (target) {
|
|
158
|
+
try { return JSON.parse(target) } catch { return undefined }
|
|
159
|
+
}
|
|
160
|
+
return undefined
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private inferConfidence(strategy: string): number {
|
|
164
|
+
const map: Record<string, number> = {
|
|
165
|
+
json_strip_repair: 0.95, json_parse_last_resort: 0.9, quote_fix: 0.85,
|
|
166
|
+
trailing_comma_fix: 0.85, braces_balance: 0.8, key_unquote: 0.8,
|
|
167
|
+
parser_resync: 0.7, synthetic_construction: 0.6,
|
|
168
|
+
}
|
|
169
|
+
return map[strategy] ?? 0.5
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export class NormalizationEngine {
|
|
174
|
+
normalize(candidate: RawToolCandidate, repaired = false): NormalizationResult {
|
|
175
|
+
const raw = candidate.raw
|
|
176
|
+
const warnings: string[] = []
|
|
177
|
+
let toolName = 'unknown'
|
|
178
|
+
let inputData: Record<string, unknown> = {}
|
|
179
|
+
|
|
180
|
+
const asRecord = raw as Record<string, unknown>
|
|
181
|
+
|
|
182
|
+
if (asRecord.tool) {
|
|
183
|
+
toolName = String(asRecord.tool)
|
|
184
|
+
inputData = this.normalizeInput(asRecord.arguments ?? asRecord.input ?? asRecord.args ?? {}, warnings)
|
|
185
|
+
} else if (asRecord.name) {
|
|
186
|
+
toolName = String(asRecord.name)
|
|
187
|
+
if (asRecord.functionCall && typeof asRecord.functionCall === 'object') {
|
|
188
|
+
const fc = asRecord.functionCall as Record<string, unknown>
|
|
189
|
+
toolName = String(fc.name ?? toolName)
|
|
190
|
+
inputData = this.normalizeInput(fc.args ?? fc.input ?? {}, warnings)
|
|
191
|
+
} else if (asRecord.type === 'tool_use') {
|
|
192
|
+
inputData = this.normalizeInput(asRecord.input ?? asRecord.args ?? {}, warnings)
|
|
193
|
+
} else if (asRecord.arguments !== undefined) {
|
|
194
|
+
inputData = this.normalizeInput(asRecord.arguments, warnings)
|
|
195
|
+
} else if (asRecord.input !== undefined) {
|
|
196
|
+
inputData = this.normalizeInput(asRecord.input, warnings)
|
|
197
|
+
} else if (asRecord.args !== undefined) {
|
|
198
|
+
inputData = this.normalizeInput(asRecord.args, warnings)
|
|
199
|
+
} else if (asRecord.action) {
|
|
200
|
+
toolName = String(asRecord.action)
|
|
201
|
+
inputData = this.normalizeInput(asRecord.input ?? asRecord.arguments ?? {}, warnings)
|
|
202
|
+
} else {
|
|
203
|
+
warnings.push('No tool name found in candidate')
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
const keys = Object.keys(asRecord)
|
|
207
|
+
warnings.push('No tool name found in candidate')
|
|
208
|
+
if (keys.length === 1 && typeof asRecord[keys[0]] === 'object' && asRecord[keys[0]] !== null && !Array.isArray(asRecord[keys[0]])) {
|
|
209
|
+
toolName = String(keys[0])
|
|
210
|
+
inputData = this.normalizeInput(asRecord[keys[0]], warnings)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const toolCall: CanonicalToolCall = {
|
|
215
|
+
tool: toolName,
|
|
216
|
+
input: inputData,
|
|
217
|
+
meta: { source: candidate.source, confidence: candidate.confidence, repaired },
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return { toolCall, warnings }
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private normalizeInput(input: unknown, warnings: string[]): Record<string, unknown> {
|
|
224
|
+
if (typeof input === 'string') {
|
|
225
|
+
return this.parseInputString(input, warnings)
|
|
226
|
+
}
|
|
227
|
+
if (typeof input === 'object' && input !== null && !Array.isArray(input)) {
|
|
228
|
+
return input as Record<string, unknown>
|
|
229
|
+
}
|
|
230
|
+
warnings.push(`Input was not a valid object: ${typeof input}`)
|
|
231
|
+
return {}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private parseInputString(input: string, warnings: string[]): Record<string, unknown> {
|
|
235
|
+
if (!input || input.trim().length === 0) { warnings.push('Empty input string'); return {} }
|
|
236
|
+
try {
|
|
237
|
+
const parsed = JSON.parse(input)
|
|
238
|
+
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
|
239
|
+
return parsed as Record<string, unknown>
|
|
240
|
+
}
|
|
241
|
+
} catch { /* not parseable */ }
|
|
242
|
+
warnings.push('Input string was not valid JSON, wrapping')
|
|
243
|
+
return { raw_input: input }
|
|
244
|
+
}
|
|
245
|
+
}
|