@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.
@@ -1,165 +0,0 @@
1
- /*
2
- * Layer 3: Tool Extraction Engine (Multi-Format)
3
- */
4
-
5
- import type { ToolCallSource, RawToolCandidate, SecurityViolation } from './types'
6
- import { StructuralParser } from './structural-parser'
7
-
8
- export interface ExtractionResult {
9
- candidates: RawToolCandidate[]
10
- sourceHint: ToolCallSource
11
- extractionErrors: string[]
12
- }
13
-
14
- export class ToolExtractionEngine {
15
- private parser: StructuralParser
16
-
17
- constructor() {
18
- this.parser = new StructuralParser()
19
- }
20
-
21
- extract(input: string): ExtractionResult {
22
- const candidates: RawToolCandidate[] = []
23
- const errors: string[] = []
24
- let sourceHint: ToolCallSource = this.detectSourceHint(input)
25
-
26
- const jsonCandidates = this.extractJsonObjects(input)
27
- for (const candidate of jsonCandidates) {
28
- const parsed = this.tryParseJson(candidate.raw)
29
- if (parsed) {
30
- candidates.push({
31
- source: candidate.sourceHint,
32
- raw: parsed,
33
- rawString: candidate.raw,
34
- confidence: this.calculateJsonConfidence(parsed, candidate.sourceHint),
35
- })
36
- }
37
- }
38
-
39
- if (candidates.length === 0) {
40
- const reactCandidates = this.extractReAct(input)
41
- candidates.push(
42
- ...reactCandidates.map(c => ({
43
- source: 'react' as ToolCallSource,
44
- raw: c.raw,
45
- rawString: c.rawString,
46
- confidence: 0.8,
47
- }))
48
- )
49
- }
50
-
51
- if (candidates.length === 0) {
52
- const gemini = this.extractGemini(input)
53
- if (gemini) {
54
- candidates.push({
55
- source: 'gemini' as ToolCallSource,
56
- raw: gemini,
57
- rawString: JSON.stringify(gemini),
58
- confidence: 0.85,
59
- })
60
- }
61
- }
62
-
63
- return { candidates, sourceHint, extractionErrors: errors }
64
- }
65
-
66
- private extractJsonObjects(text: string): Array<{ raw: string; sourceHint: ToolCallSource }> {
67
- const results: Array<{ raw: string; sourceHint: ToolCallSource }> = []
68
- const markupCleaned = this.stripMarkup(text)
69
- const jsonSpans = StructuralParser.extractJsonFromText(markupCleaned)
70
-
71
- for (const span of jsonSpans) {
72
- const sourceHint = this.detectSourceHint(span)
73
- results.push({ raw: span, sourceHint })
74
- }
75
-
76
- if (results.length === 0) {
77
- const funcMatch = markupCleaned.match(/call_function\s*\(\s*'(\w+)'\s*,\s*(\{[\s\S]*\})\s*\)/i)
78
- if (funcMatch) {
79
- results.push({ raw: `{"name":"${funcMatch[1]}","arguments":${funcMatch[2]}}`, sourceHint: 'openai' })
80
- }
81
- }
82
-
83
- return results
84
- }
85
-
86
- private extractReAct(text: string): Array<{ raw: Record<string, unknown>; rawString: string }> {
87
- const results: Array<{ raw: Record<string, unknown>; rawString: string }> = []
88
- const patterns = [
89
- /Action:\s*(\w+)\s*\n?\s*Action Input:\s*(\{[\s\S]*?)(?=\n\s*\n|\n\s*Observation|\n\s*Final Answer|$)/i,
90
- /Action:\s*(\w+)\s+Action Input:\s*(\{[\s\S]*)/i,
91
- /\*\*Action\*\*:\s*(\w+)\s*\n?\s*\*\*Action Input\*\*:\s*(\{[\s\S]*?)(?=\n\s*\n|\n\s*Observation|\n\s*Final Answer|$)/i,
92
- ]
93
-
94
- for (const pattern of patterns) {
95
- const match = text.match(pattern)
96
- if (match) {
97
- let actionInputStr = match[2].trim()
98
- actionInputStr = actionInputStr.replace(/\n\s*Observation.*$/is, '').trim()
99
- results.push({
100
- raw: { name: match[1], arguments: this.safeParse(actionInputStr) || {} },
101
- rawString: match[0],
102
- })
103
- break
104
- }
105
- }
106
-
107
- return results
108
- }
109
-
110
- private extractGemini(text: string): Record<string, unknown> | null {
111
- const patterns = [
112
- /functionCall\s*[:=]\s*\{\s*name\s*[:=]\s*['"]([^'"]+)['"]\s*,\s*args\s*[:=]\s*(\{[\s\S]*\})\s*\}/i,
113
- /"functionCall"\s*:\s*\{\s*"name"\s*:\s*"([^"]+)"\s*,\s*"args"\s*:\s*(\{[\s\S]*?\})\s*\}/i,
114
- ]
115
- for (const pattern of patterns) {
116
- const match = text.match(pattern)
117
- if (match) return { name: match[1], arguments: this.safeParse(match[2]) || {} }
118
- }
119
- return null
120
- }
121
-
122
- private stripMarkup(text: string): string {
123
- return text
124
- .replace(/```json\s*/gi, '')
125
- .replace(/```\s*/gi, '')
126
- .replace(/<function_calls>/gi, '')
127
- .replace(/<\/function_calls>/gi, '')
128
- .trim()
129
- }
130
-
131
- private detectSourceHint(text: string): ToolCallSource {
132
- const t = this.stripMarkup(text)
133
- if (/\btool_use\b/.test(t) || /type:\s*['"]?tool_use/i.test(t)) return 'claude'
134
- if (/\bfunctionCall\b/.test(t) || /"functionCall"/.test(t)) return 'gemini'
135
- if (/\barguments\b/.test(t) && /\bname\b/.test(t)) return 'openai'
136
- if (/Action:\s*\w+\s*\n?Action Input:/i.test(t)) return 'react'
137
- return 'unknown'
138
- }
139
-
140
- private tryParseJson(raw: string): Record<string, unknown> | null {
141
- try { return JSON.parse(raw) } catch {
142
- const result = this.parser.parse(raw, 'unknown')
143
- return (result.ast?.value as Record<string, unknown>) ?? null
144
- }
145
- }
146
-
147
- private safeParse(str: string): Record<string, unknown> | null {
148
- try { return JSON.parse(str) } catch {
149
- const result = this.parser.parse(str, 'unknown')
150
- return (result.ast?.value as Record<string, unknown>) ?? null
151
- }
152
- }
153
-
154
- private calculateJsonConfidence(parsed: Record<string, unknown>, source: ToolCallSource): number {
155
- let conf = 0.5
156
- if (parsed.name) conf += 0.15
157
- if (parsed.arguments && typeof parsed.arguments === 'object') conf += 0.15
158
- if (parsed.input && typeof parsed.input === 'object') conf += 0.1
159
- if (parsed.tool) conf += 0.1
160
- if (parsed.args && typeof parsed.args === 'object') conf += 0.1
161
- if (typeof parsed.arguments === 'string') conf -= 0.2
162
- if (typeof parsed.input === 'string') conf -= 0.2
163
- return Math.min(0.95, Math.max(0.3, conf))
164
- }
165
- }
@@ -1,258 +0,0 @@
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
- }
@@ -1,245 +0,0 @@
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
- }