@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.
Files changed (59) hide show
  1. package/LICENSE +13 -0
  2. package/README.md +292 -0
  3. package/bin/qwenproxy.mjs +11 -0
  4. package/package.json +56 -0
  5. package/src/api/models.ts +183 -0
  6. package/src/api/server.ts +126 -0
  7. package/src/cache/memory-cache.ts +186 -0
  8. package/src/core/account-manager.ts +132 -0
  9. package/src/core/accounts.ts +78 -0
  10. package/src/core/config.ts +91 -0
  11. package/src/core/database.ts +92 -0
  12. package/src/core/logger.ts +96 -0
  13. package/src/core/metrics.ts +169 -0
  14. package/src/core/model-registry.ts +30 -0
  15. package/src/core/stream-registry.ts +40 -0
  16. package/src/core/watchdog.ts +130 -0
  17. package/src/index.ts +7 -0
  18. package/src/linter/extraction-engine.ts +165 -0
  19. package/src/linter/index.ts +258 -0
  20. package/src/linter/repair-normalize.ts +245 -0
  21. package/src/linter/safety-gate.ts +219 -0
  22. package/src/linter/streaming-state-machine.ts +252 -0
  23. package/src/linter/structural-parser.ts +352 -0
  24. package/src/linter/types.ts +74 -0
  25. package/src/login.ts +228 -0
  26. package/src/routes/chat.ts +801 -0
  27. package/src/routes/upload.ts +700 -0
  28. package/src/services/playwright.ts +778 -0
  29. package/src/services/qwen.ts +500 -0
  30. package/src/tests/advanced.test.ts +227 -0
  31. package/src/tests/agenticStress.test.ts +360 -0
  32. package/src/tests/concurrency.test.ts +103 -0
  33. package/src/tests/concurrentChat.test.ts +71 -0
  34. package/src/tests/delta.test.ts +63 -0
  35. package/src/tests/index.test.ts +356 -0
  36. package/src/tests/jsonFix.test.ts +98 -0
  37. package/src/tests/linter.test.ts +151 -0
  38. package/src/tests/parallel.test.ts +42 -0
  39. package/src/tests/parser.test.ts +89 -0
  40. package/src/tests/rotation.test.ts +45 -0
  41. package/src/tests/streamingOptimizations.test.ts +328 -0
  42. package/src/tests/structureVerification.test.ts +176 -0
  43. package/src/tools/ast.ts +15 -0
  44. package/src/tools/coercion.ts +67 -0
  45. package/src/tools/confidence.ts +48 -0
  46. package/src/tools/detector.ts +40 -0
  47. package/src/tools/executor.ts +236 -0
  48. package/src/tools/parser.ts +446 -0
  49. package/src/tools/pipeline.ts +122 -0
  50. package/src/tools/registry-runtime.ts +34 -0
  51. package/src/tools/registry.ts +142 -0
  52. package/src/tools/repair.ts +42 -0
  53. package/src/tools/schema.ts +285 -0
  54. package/src/tools/types.ts +104 -0
  55. package/src/tools/validator.ts +33 -0
  56. package/src/utils/context-truncation.ts +61 -0
  57. package/src/utils/json.ts +114 -0
  58. package/src/utils/qwen-stream-parser.ts +286 -0
  59. package/src/utils/types.ts +101 -0
@@ -0,0 +1,169 @@
1
+ import { EventEmitter } from 'events'
2
+ import { config } from './config.js'
3
+
4
+ interface MetricPoint {
5
+ value: number
6
+ timestamp: number
7
+ labels?: Record<string, string>
8
+ }
9
+
10
+ type MetricType = 'counter' | 'gauge' | 'histogram' | 'summary'
11
+
12
+ interface MetricDefinition {
13
+ name: string
14
+ type: MetricType
15
+ help: string
16
+ values: Map<string, MetricPoint>
17
+ histogramBuckets?: number[]
18
+ }
19
+
20
+ export class Metrics extends EventEmitter {
21
+ private metrics: Map<string, MetricDefinition> = new Map()
22
+ private collectionInterval: NodeJS.Timeout | null = null
23
+ private exportCallback: ((metrics: Map<string, MetricDefinition>) => void) | null = null
24
+
25
+ constructor() {
26
+ super()
27
+ this.registerDefaults()
28
+ }
29
+
30
+ private registerDefaults(): void {
31
+ const defaults: Array<[string, MetricType, string]> = [
32
+ ['requests.total', 'counter', 'Total requests processed'],
33
+ ['requests.errors', 'counter', 'Total request errors'],
34
+ ['latency.request', 'histogram', 'Request latency (ms)'],
35
+ ['streams.active', 'gauge', 'Active SSE streams'],
36
+ ['streams.errors', 'counter', 'Stream errors'],
37
+ ['memory.heap.used', 'gauge', 'Heap memory used (bytes)'],
38
+ ['memory.heap.total', 'gauge', 'Heap memory total (bytes)'],
39
+ ['cache.set', 'counter', 'Cache set operations'],
40
+ ['cache.hit', 'counter', 'Cache hits'],
41
+ ['cache.miss', 'counter', 'Cache misses'],
42
+ ['cache.deleted', 'counter', 'Cache deletions'],
43
+ ['cache.flushed', 'counter', 'Cache flushes'],
44
+ ['cache.value.size', 'histogram', 'Cache value size (bytes)'],
45
+ ['cache.get.latency', 'histogram', 'Cache get latency (ms)'],
46
+ ['watchdog.ram.status', 'gauge', 'Watchdog RAM status (0=ok, 1=warning, 2=critical)'],
47
+ ['watchdog.overall', 'gauge', 'Watchdog overall status (0=healthy, 1=degraded, 2=unhealthy)'],
48
+ ['watchdog.recovery.triggered', 'counter', 'Recovery attempts triggered'],
49
+ ['watchdog.recovery.success', 'counter', 'Successful recoveries'],
50
+ ['watchdog.recovery.failed', 'counter', 'Failed recoveries'],
51
+ ]
52
+
53
+ for (const [name, type, help] of defaults) {
54
+ this.metrics.set(name, {
55
+ name,
56
+ type,
57
+ help,
58
+ values: new Map(),
59
+ histogramBuckets: type === 'histogram' ? [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000] : undefined,
60
+ })
61
+ }
62
+ }
63
+
64
+ increment(name: string, value: number = 1, labels?: Record<string, string>): void {
65
+ const metric = this.metrics.get(name)
66
+ if (!metric || metric.type !== 'counter') return
67
+
68
+ const key = labels ? JSON.stringify(labels) : 'default'
69
+ const current = metric.values.get(key)?.value || 0
70
+ metric.values.set(key, { value: current + value, timestamp: Date.now(), labels })
71
+ this.emit('metric', { name, type: 'counter', value: current + value, labels })
72
+ }
73
+
74
+ decrement(name: string, labels?: Record<string, string>): void {
75
+ this.increment(name, -1, labels)
76
+ }
77
+
78
+ gauge(name: string, value: number, labels?: Record<string, string>): void {
79
+ const metric = this.metrics.get(name)
80
+ if (!metric || metric.type !== 'gauge') return
81
+
82
+ const key = labels ? JSON.stringify(labels) : 'default'
83
+ metric.values.set(key, { value, timestamp: Date.now(), labels })
84
+ this.emit('metric', { name, type: 'gauge', value, labels })
85
+ }
86
+
87
+ histogram(name: string, value: number, labels?: Record<string, string>): void {
88
+ const metric = this.metrics.get(name)
89
+ if (!metric || metric.type !== 'histogram') return
90
+
91
+ const key = labels ? JSON.stringify(labels) : 'default'
92
+ const existing = metric.values.get(key)
93
+ const data = existing?.value || { count: 0, sum: 0, buckets: new Map<number, number>() }
94
+
95
+ if (typeof data === 'object' && data !== null) {
96
+ data.count++
97
+ data.sum += value
98
+ for (const bucket of metric.histogramBuckets || []) {
99
+ data.buckets.set(bucket, (data.buckets.get(bucket) || 0) + (value <= bucket ? 1 : 0))
100
+ }
101
+ }
102
+
103
+ metric.values.set(key, { value: data as any, timestamp: Date.now(), labels })
104
+ this.emit('metric', { name, type: 'histogram', value, labels })
105
+ }
106
+
107
+ startCollection(): void {
108
+ if (this.collectionInterval) return
109
+
110
+ this.collectionInterval = setInterval(() => {
111
+ this.collectSystemMetrics()
112
+ if (this.exportCallback) {
113
+ this.exportCallback(this.metrics)
114
+ }
115
+ }, config.metrics.interval)
116
+ }
117
+
118
+ private collectSystemMetrics(): void {
119
+ const mem = process.memoryUsage()
120
+ this.gauge('memory.heap.used', mem.heapUsed)
121
+ this.gauge('memory.heap.total', mem.heapTotal)
122
+ }
123
+
124
+ setExportCallback(callback: (metrics: Map<string, MetricDefinition>) => void): void {
125
+ this.exportCallback = callback
126
+ }
127
+
128
+ get(name: string, labels?: Record<string, string>): MetricPoint | null {
129
+ const metric = this.metrics.get(name)
130
+ if (!metric) return null
131
+ const key = labels ? JSON.stringify(labels) : 'default'
132
+ return metric.values.get(key) || null
133
+ }
134
+
135
+ getAll(): Map<string, MetricDefinition> {
136
+ return new Map(this.metrics)
137
+ }
138
+
139
+ formatPrometheus(): string {
140
+ let output = ''
141
+ for (const metric of this.metrics.values()) {
142
+ output += `# HELP ${metric.name} ${metric.help}\n`
143
+ output += `# TYPE ${metric.name} ${metric.type}\n`
144
+
145
+ for (const [key, point] of metric.values) {
146
+ const labelsStr = point.labels
147
+ ? `{${Object.entries(point.labels).map(([k, v]) => `${k}="${v}"`).join(',')}}`
148
+ : ''
149
+ output += `${metric.name}${labelsStr} ${point.value} ${point.timestamp}\n`
150
+ }
151
+ }
152
+ return output
153
+ }
154
+
155
+ reset(): void {
156
+ for (const metric of this.metrics.values()) {
157
+ metric.values.clear()
158
+ }
159
+ }
160
+
161
+ stopCollection(): void {
162
+ if (this.collectionInterval) {
163
+ clearInterval(this.collectionInterval)
164
+ this.collectionInterval = null
165
+ }
166
+ }
167
+ }
168
+
169
+ export const metrics = new Metrics()
@@ -0,0 +1,30 @@
1
+ const modelContextWindows: Record<string, number> = {
2
+ 'qwen-max': 32768,
3
+ 'qwen-max-latest': 32768,
4
+ 'qwen-plus': 131072,
5
+ 'qwen-plus-latest': 131072,
6
+ 'qwen-turbo': 131072,
7
+ 'qwen-turbo-latest': 131072,
8
+ 'qwen-long': 1000000,
9
+ 'qwen-coder': 131072,
10
+ 'qwen-coder-plus': 131072,
11
+ }
12
+
13
+ const defaultContextWindow = 131072
14
+
15
+ export function setModelContextWindow(modelId: string, contextWindow: number): void {
16
+ modelContextWindows[modelId] = contextWindow
17
+ }
18
+
19
+ export function getModelContextWindow(modelId: string): number {
20
+ const baseId = modelId.replace('-no-thinking', '')
21
+ return modelContextWindows[baseId] ?? defaultContextWindow
22
+ }
23
+
24
+ export function syncModelContextWindows(models: Array<{ id: string; context_window?: number }>): void {
25
+ for (const m of models) {
26
+ if (m.context_window) {
27
+ modelContextWindows[m.id] = m.context_window
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,40 @@
1
+ import { metrics } from './metrics.js'
2
+
3
+ const activeStreams = new Map<string, {
4
+ abortController: AbortController;
5
+ accountId: string;
6
+ uiSessionId: string;
7
+ targetResponseId: string;
8
+ headers: Record<string, string>;
9
+ }>();
10
+
11
+ export function registerStream(key: string, entry: {
12
+ abortController: AbortController;
13
+ accountId: string;
14
+ uiSessionId: string;
15
+ targetResponseId: string;
16
+ headers: Record<string, string>;
17
+ }): void {
18
+ activeStreams.set(key, entry)
19
+ metrics.gauge('streams.active', activeStreams.size)
20
+ }
21
+
22
+ export function getStream(key: string): ReturnType<typeof activeStreams.get> {
23
+ return activeStreams.get(key)
24
+ }
25
+
26
+ export function removeStream(key: string): void {
27
+ activeStreams.delete(key)
28
+ metrics.gauge('streams.active', activeStreams.size)
29
+ }
30
+
31
+ export function abortStream(key: string): boolean {
32
+ const entry = activeStreams.get(key)
33
+ if (entry) {
34
+ entry.abortController.abort()
35
+ activeStreams.delete(key)
36
+ metrics.gauge('streams.active', activeStreams.size)
37
+ return true
38
+ }
39
+ return false
40
+ }
@@ -0,0 +1,130 @@
1
+ import { EventEmitter } from 'events'
2
+ import { config } from './config.js'
3
+ import { metrics } from './metrics.js'
4
+
5
+ interface HealthStatus {
6
+ ram: 'ok' | 'warning' | 'critical'
7
+ streams: 'ok' | 'congested' | 'blocked'
8
+ overall: 'healthy' | 'degraded' | 'unhealthy'
9
+ }
10
+
11
+ export class Watchdog extends EventEmitter {
12
+ private checkInterval: NodeJS.Timeout | null = null
13
+ private consecutiveFailures: number = 0
14
+ private recoveryInProgress: boolean = false
15
+
16
+ start(): void {
17
+ if (this.checkInterval) return
18
+
19
+ this.checkInterval = setInterval(() => {
20
+ this.performHealthCheck().catch(error => {
21
+ this.emit('check:error', error)
22
+ this.consecutiveFailures++
23
+ })
24
+ }, config.watchdog.checkInterval)
25
+
26
+ this.emit('started')
27
+ }
28
+
29
+ private async performHealthCheck(): Promise<void> {
30
+ const status: HealthStatus = {
31
+ ram: this.checkRAM(),
32
+ streams: this.checkStreams(),
33
+ overall: 'healthy',
34
+ }
35
+
36
+ status.overall = this.calculateOverall(status)
37
+
38
+ if (status.overall === 'unhealthy') {
39
+ this.consecutiveFailures++
40
+ if (this.consecutiveFailures >= config.watchdog.consecutiveFailuresThreshold && !this.recoveryInProgress) {
41
+ await this.triggerRecovery(status)
42
+ }
43
+ } else {
44
+ this.consecutiveFailures = 0
45
+ }
46
+
47
+ this.emit('health:check', status)
48
+ metrics.gauge('watchdog.ram.status', status.ram === 'ok' ? 0 : status.ram === 'warning' ? 1 : 2)
49
+ metrics.gauge('watchdog.overall', status.overall === 'healthy' ? 0 : status.overall === 'degraded' ? 1 : 2)
50
+ }
51
+
52
+ private checkRAM(): 'ok' | 'warning' | 'critical' {
53
+ const mem = process.memoryUsage()
54
+ const usagePercent = (mem.heapUsed / mem.heapTotal) * 100
55
+
56
+ if (usagePercent > config.watchdog.ram.criticalThreshold) return 'critical'
57
+ if (usagePercent > config.watchdog.ram.warningThreshold) return 'warning'
58
+ return 'ok'
59
+ }
60
+
61
+ private checkStreams(): 'ok' | 'congested' | 'blocked' {
62
+ const activeStreams = metrics.get('streams.active')?.value || 0
63
+ if (activeStreams > config.watchdog.streams.criticalThreshold) return 'blocked'
64
+ if (activeStreams > config.watchdog.streams.warningThreshold) return 'congested'
65
+ return 'ok'
66
+ }
67
+
68
+ private calculateOverall(status: HealthStatus): 'healthy' | 'degraded' | 'unhealthy' {
69
+ const critical = ['critical', 'blocked']
70
+ const warning = ['warning', 'congested']
71
+
72
+ const values = Object.values(status).filter(v => typeof v === 'string') as string[]
73
+ if (values.some(v => critical.includes(v))) return 'unhealthy'
74
+ if (values.some(v => warning.includes(v))) return 'degraded'
75
+ return 'healthy'
76
+ }
77
+
78
+ private async triggerRecovery(status: HealthStatus): Promise<void> {
79
+ if (this.recoveryInProgress) return
80
+ this.recoveryInProgress = true
81
+
82
+ this.emit('recovery:start', status)
83
+ metrics.increment('watchdog.recovery.triggered')
84
+
85
+ try {
86
+ if (status.ram === 'critical') {
87
+ await this.recoverRAM()
88
+ }
89
+ if (status.streams === 'blocked') {
90
+ await this.recoverStreams()
91
+ }
92
+
93
+ this.emit('recovery:complete')
94
+ metrics.increment('watchdog.recovery.success')
95
+ } catch (error: any) {
96
+ this.emit('recovery:error', error)
97
+ metrics.increment('watchdog.recovery.failed')
98
+ } finally {
99
+ this.recoveryInProgress = false
100
+ }
101
+ }
102
+
103
+ private async recoverRAM(): Promise<void> {
104
+ if (global.gc) global.gc()
105
+ await new Promise(resolve => setTimeout(resolve, 100))
106
+ this.emit('recovery:ram:freed')
107
+ }
108
+
109
+ private async recoverStreams(): Promise<void> {
110
+ this.emit('recovery:streams:throttled')
111
+ }
112
+
113
+ stop(): void {
114
+ if (this.checkInterval) {
115
+ clearInterval(this.checkInterval)
116
+ this.checkInterval = null
117
+ }
118
+ this.emit('stopped')
119
+ }
120
+
121
+ getStatus(): Promise<HealthStatus> {
122
+ const status: HealthStatus = {
123
+ ram: this.checkRAM(),
124
+ streams: this.checkStreams(),
125
+ overall: 'healthy',
126
+ }
127
+ status.overall = this.calculateOverall(status)
128
+ return Promise.resolve(status)
129
+ }
130
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ import 'dotenv/config'
2
+ import { startServer } from './api/server.js'
3
+
4
+ startServer().catch(error => {
5
+ console.error('Failed to start server:', error)
6
+ process.exit(1)
7
+ })
@@ -0,0 +1,165 @@
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
+ }