@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,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,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
|
+
}
|