@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,446 @@
1
+ /*
2
+ * File: parser.ts
3
+ * Project: qwenproxy
4
+ * Streaming parser for <tool_call> tags - OpenAI Compatible
5
+ * Supports both JSON and Hermes-style XML <parameter> formats.
6
+ */
7
+
8
+ import { v4 as uuidv4 } from 'uuid';
9
+ import { robustParseJSON } from '../utils/json.js';
10
+ import { logger } from '../core/logger.js';
11
+ import type { ParsedToolCall } from './types';
12
+ import type { FunctionToolDefinition } from './types';
13
+
14
+ export interface ParserResult {
15
+ text: string;
16
+ toolCalls: ParsedToolCall[];
17
+ }
18
+
19
+ // ─── XML Helpers ───────────────────────────────────────────────────────────────
20
+
21
+ const TOOL_OPEN_RE = /<tool_call\b[^>]*>/i;
22
+ const TOOL_END = '</tool_call>';
23
+
24
+ function decodeXmlEntities(value: string): string {
25
+ return value
26
+ .replace(/&quot;/g, '"')
27
+ .replace(/&apos;/g, "'")
28
+ .replace(/&lt;/g, '<')
29
+ .replace(/&gt;/g, '>')
30
+ .replace(/&amp;/g, '&');
31
+ }
32
+
33
+ function coerceParameterValue(rawValue: string): unknown {
34
+ const value = decodeXmlEntities(rawValue.trim());
35
+ if (value === 'true') return true;
36
+ if (value === 'false') return false;
37
+ if (value === 'null') return null;
38
+ if (/^-?\d+(?:\.\d+)?$/.test(value)) return Number(value);
39
+ if ((value.startsWith('{') && value.endsWith('}')) || (value.startsWith('[') && value.endsWith(']'))) {
40
+ try { return JSON.parse(value); } catch {}
41
+ }
42
+ return value;
43
+ }
44
+
45
+ /**
46
+ * Extract tool name from the opening tag attribute or a <name> child element.
47
+ */
48
+ function extractToolName(openTag: string, block: string): string {
49
+ const combined = `${openTag}\n${block}`;
50
+ const attrMatch = combined.match(/<tool_call\b[^>]*\bname\s*=\s*["']([^"']+)["']/i);
51
+ if (attrMatch) return attrMatch[1];
52
+
53
+ const nameTagMatch = block.match(/<name>([\s\S]*?)<\/name>/i);
54
+ if (nameTagMatch) return decodeXmlEntities(nameTagMatch[1].trim());
55
+
56
+ return '';
57
+ }
58
+
59
+ /**
60
+ * Infer tool name by matching parameter keys against tool definitions.
61
+ * Only returns a name if exactly one tool matches all argument keys.
62
+ */
63
+ function inferToolNameFromParameters(args: Record<string, unknown>, tools: FunctionToolDefinition[]): string {
64
+ const argKeys = Object.keys(args);
65
+ if (argKeys.length === 0 || !Array.isArray(tools)) return '';
66
+
67
+ const matches = tools.filter((tool) => {
68
+ const fn = tool?.type === 'function' ? tool.function : (tool as any)?.function;
69
+ const properties = fn?.parameters?.properties || {};
70
+ return argKeys.every(k => Object.prototype.hasOwnProperty.call(properties, k));
71
+ });
72
+
73
+ if (matches.length === 1) {
74
+ const fn = matches[0]?.type === 'function' ? matches[0].function : (matches[0] as any)?.function;
75
+ return fn?.name || '';
76
+ }
77
+
78
+ return '';
79
+ }
80
+
81
+ /**
82
+ * Parse Hermes-style XML <parameter name="...">value</parameter> format.
83
+ */
84
+ function parseXmlParameterToolCall(
85
+ block: string,
86
+ openTag: string,
87
+ tools: FunctionToolDefinition[]
88
+ ): { name: string; arguments: Record<string, unknown> } | null {
89
+ const args: Record<string, unknown> = {};
90
+ const parameterRe = /<parameter\b[^>]*\bname\s*=\s*["']([^"']+)["'][^>]*>([\s\S]*?)<\/parameter>/gi;
91
+ let match: RegExpExecArray | null;
92
+ while ((match = parameterRe.exec(block)) !== null) {
93
+ args[match[1]] = coerceParameterValue(match[2]);
94
+ }
95
+
96
+ if (Object.keys(args).length === 0) return null;
97
+
98
+ const toolName = extractToolName(openTag, block) || inferToolNameFromParameters(args, tools);
99
+ if (!toolName) return null;
100
+
101
+ return { name: toolName, arguments: args };
102
+ }
103
+
104
+ /**
105
+ * Try to recover a tool call from a block that may have unclosed <parameter> tags
106
+ * (e.g. stream was cut off before </parameter> or </tool_call>).
107
+ */
108
+ function parseRecoverableXmlToolCall(
109
+ block: string,
110
+ openTag: string,
111
+ tools: FunctionToolDefinition[]
112
+ ): { name: string; arguments: Record<string, unknown> } | null {
113
+ const args: Record<string, unknown> = {};
114
+
115
+ // First, extract all properly closed parameters
116
+ const closedParameterRe = /<parameter\b[^>]*\bname\s*=\s*["']([^"']+)["'][^>]*>([\s\S]*?)<\/parameter>/gi;
117
+ let match: RegExpExecArray | null;
118
+ let lastClosedEnd = 0;
119
+ while ((match = closedParameterRe.exec(block)) !== null) {
120
+ args[match[1]] = coerceParameterValue(match[2]);
121
+ lastClosedEnd = closedParameterRe.lastIndex;
122
+ }
123
+
124
+ // Then look for an unclosed parameter at the tail
125
+ const tail = block.substring(lastClosedEnd);
126
+ const unclosedMatch = tail.match(/<parameter\b[^>]*\bname\s*=\s*["']([^"']+)["'][^>]*>([\s\S]*)$/i);
127
+ if (unclosedMatch) {
128
+ args[unclosedMatch[1]] = coerceParameterValue(unclosedMatch[2]);
129
+ }
130
+
131
+ if (Object.keys(args).length === 0) return null;
132
+
133
+ const toolName = extractToolName(openTag, block) || inferToolNameFromParameters(args, tools);
134
+ if (!toolName) return null;
135
+
136
+ return { name: toolName, arguments: args };
137
+ }
138
+
139
+ // ─── Partial Tag Detection ─────────────────────────────────────────────────────
140
+
141
+ const TOOL_START_LITERAL = '<tool_call>';
142
+
143
+ function findPartialToolOpenIndex(buffer: string): number {
144
+ const lower = buffer.toLowerCase();
145
+ // Check if there's a partial opening tag like `<tool_call` without closing `>`
146
+ const idx = lower.lastIndexOf('<tool_call');
147
+ if (idx !== -1 && lower.indexOf('>', idx) === -1) return idx;
148
+
149
+ // Check for partial prefix at end (e.g. `<tool`, `<tool_`, `<tool_c`)
150
+ for (let i = 1; i < TOOL_START_LITERAL.length; i++) {
151
+ if (lower.endsWith(TOOL_START_LITERAL.substring(0, i))) return buffer.length - i;
152
+ }
153
+ return -1;
154
+ }
155
+
156
+ // ─── StreamingToolParser ───────────────────────────────────────────────────────
157
+
158
+ export class StreamingToolParser {
159
+ private buffer = '';
160
+ private insideTool = false;
161
+ private currentOpenTag = TOOL_START_LITERAL;
162
+ private emittedToolCallCount = 0;
163
+ private pendingLeadIn = '';
164
+ private tools: FunctionToolDefinition[] = [];
165
+
166
+ /**
167
+ * @param tools - Optional array of tool definitions for name inference
168
+ */
169
+ constructor(tools: FunctionToolDefinition[] = []) {
170
+ this.tools = tools;
171
+ }
172
+
173
+ /**
174
+ * Update the tools list (e.g. if received after construction).
175
+ */
176
+ setTools(tools: FunctionToolDefinition[]): void {
177
+ this.tools = tools;
178
+ }
179
+
180
+ feed(chunk: string): ParserResult {
181
+ this.buffer += chunk;
182
+ const result: ParserResult = { text: '', toolCalls: [] };
183
+
184
+ while (this.buffer.length > 0) {
185
+ if (!this.insideTool) {
186
+ const match = this.buffer.match(TOOL_OPEN_RE);
187
+ if (match && match.index !== undefined) {
188
+ // Text before the tool call tag
189
+ const textBefore = this.buffer.substring(0, match.index);
190
+ result.text += textBefore;
191
+ this.insideTool = true;
192
+ this.currentOpenTag = match[0];
193
+ this.buffer = this.buffer.substring(match.index + match[0].length);
194
+ continue;
195
+ } else {
196
+ // No full open tag found. Check for partial at end.
197
+ const partialIdx = findPartialToolOpenIndex(this.buffer);
198
+ const flushIndex = partialIdx === -1 ? this.buffer.length : partialIdx;
199
+ if (flushIndex > 0) {
200
+ const textToEmit = this.buffer.substring(0, flushIndex);
201
+ // Only emit as content if no tool calls have been emitted yet
202
+ if (this.emittedToolCallCount === 0) {
203
+ result.text += textToEmit;
204
+ }
205
+ this.buffer = this.buffer.substring(flushIndex);
206
+ }
207
+ break;
208
+ }
209
+ } else {
210
+ // Inside tool: look for </tool_call>
211
+ const lowerBuffer = this.buffer.toLowerCase();
212
+ const endIdx = lowerBuffer.indexOf(TOOL_END);
213
+ if (endIdx !== -1) {
214
+ const content = this.buffer.substring(0, endIdx);
215
+ this.buffer = this.buffer.substring(endIdx + TOOL_END.length);
216
+ this.processToolContent(content, result);
217
+ this.insideTool = false;
218
+ this.currentOpenTag = TOOL_START_LITERAL;
219
+ if (this.buffer.length > 0) {
220
+ const nextMatch = this.buffer.match(TOOL_OPEN_RE);
221
+ if (nextMatch && nextMatch.index !== undefined) {
222
+ result.text += this.buffer.substring(0, nextMatch.index);
223
+ this.insideTool = true;
224
+ this.currentOpenTag = nextMatch[0];
225
+ this.buffer = this.buffer.substring(nextMatch.index + nextMatch[0].length);
226
+ } else {
227
+ const partialIdx = findPartialToolOpenIndex(this.buffer);
228
+ const flushIdx = partialIdx === -1 ? this.buffer.length : partialIdx;
229
+ result.text += this.buffer.substring(0, flushIdx);
230
+ this.buffer = this.buffer.substring(flushIdx);
231
+ }
232
+ }
233
+ } else {
234
+ break; // Wait for more data
235
+ }
236
+ }
237
+ }
238
+
239
+ return result;
240
+ }
241
+
242
+ flush(): ParserResult {
243
+ const result: ParserResult = { text: '', toolCalls: [] };
244
+ if (!this.buffer && !this.pendingLeadIn) return result;
245
+
246
+ if (this.insideTool) {
247
+ const trimmed = this.buffer.trim();
248
+ if (trimmed.length > 0) {
249
+ const recovered = this.tryRecoverToolCall(trimmed);
250
+ if (recovered) {
251
+ result.toolCalls.push(recovered);
252
+ this.emittedToolCallCount++;
253
+ } else {
254
+ logger.warn('[parser] Dropping unrecoverable unclosed tool call at end of stream');
255
+ result.text += this.pendingLeadIn;
256
+ result.text += this.currentOpenTag + this.buffer + TOOL_END;
257
+ }
258
+ } else {
259
+ result.text += this.pendingLeadIn;
260
+ }
261
+ } else {
262
+ result.text += this.buffer;
263
+ }
264
+
265
+ this.buffer = '';
266
+ this.insideTool = false;
267
+ this.currentOpenTag = TOOL_START_LITERAL;
268
+ return result;
269
+ }
270
+
271
+ getEmittedToolCallCount(): number {
272
+ return this.emittedToolCallCount;
273
+ }
274
+
275
+ isInsideTool(): boolean {
276
+ return this.insideTool;
277
+ }
278
+
279
+ /**
280
+ * Get any lead-in text that was captured before tool calls.
281
+ * Useful for fallback content when tool calls fail to parse.
282
+ */
283
+ getPendingLeadIn(): string {
284
+ return this.pendingLeadIn;
285
+ }
286
+
287
+ // ─── Internal Methods ──────────────────────────────────────────────────────
288
+
289
+ private processToolContent(content: string, result: ParserResult): void {
290
+ const t = content.trim();
291
+ if (!t) {
292
+ // Empty tool call - malformed. Restore lead-in if possible.
293
+ logger.warn('[parser] Dropping empty tool call block');
294
+ if (this.emittedToolCallCount === 0 && this.pendingLeadIn.trim().length > 0) {
295
+ result.text += this.pendingLeadIn;
296
+ }
297
+ this.pendingLeadIn = '';
298
+ return;
299
+ }
300
+
301
+ // 1) Try Hermes-style XML <parameter> format first
302
+ const xmlParsed = parseXmlParameterToolCall(t, this.currentOpenTag, this.tools);
303
+ if (xmlParsed) {
304
+ result.toolCalls.push({
305
+ id: `call_${uuidv4()}`,
306
+ name: xmlParsed.name,
307
+ arguments: xmlParsed.arguments,
308
+ });
309
+ this.emittedToolCallCount++;
310
+ this.pendingLeadIn = '';
311
+ return;
312
+ }
313
+
314
+ // 2) Try JSON array format
315
+ if (t.startsWith('[')) {
316
+ try {
317
+ const arr = JSON.parse(t);
318
+ for (const item of arr) {
319
+ const tc = this.parseToolCall(item);
320
+ if (tc) {
321
+ result.toolCalls.push(tc);
322
+ this.emittedToolCallCount++;
323
+ }
324
+ }
325
+ this.pendingLeadIn = '';
326
+ return;
327
+ } catch {
328
+ // Fall through to JSON object parsing
329
+ }
330
+ }
331
+
332
+ // 3) Try JSON object format (single or multiple)
333
+ if (t.startsWith('{') || t.includes('"name"')) {
334
+ const calls = this.parseToolContent(t);
335
+ if (calls.length > 0) {
336
+ for (const tc of calls) {
337
+ if (!tc.name || tc.name === '') {
338
+ const attrName = extractToolName(this.currentOpenTag, t);
339
+ if (attrName) tc.name = attrName;
340
+ }
341
+ if (tc.name) {
342
+ result.toolCalls.push(tc);
343
+ this.emittedToolCallCount++;
344
+ }
345
+ }
346
+ this.pendingLeadIn = '';
347
+ return;
348
+ }
349
+ }
350
+
351
+ // 4) Tool call is malformed and unrecoverable.
352
+ logger.warn('[parser] Dropping malformed tool call block', {
353
+ contentPreview: t.substring(0, 500),
354
+ hasName: t.includes('"name"') || t.includes('"tool"') || t.includes('tool_name'),
355
+ hasArgs: t.includes('"arguments"') || t.includes('"args"') || t.includes('"parameters"') || t.includes('"input"'),
356
+ first100Chars: t.substring(0, 100)
357
+ });
358
+ result.text += this.pendingLeadIn;
359
+ result.text += this.currentOpenTag + content + TOOL_END;
360
+ this.pendingLeadIn = '';
361
+ }
362
+
363
+ private tryRecoverToolCall(block: string): ParsedToolCall | null {
364
+ // Try full parse first
365
+ const xmlParsed = parseXmlParameterToolCall(block, this.currentOpenTag, this.tools);
366
+ if (xmlParsed) {
367
+ return {
368
+ id: `call_${uuidv4()}`,
369
+ name: xmlParsed.name,
370
+ arguments: xmlParsed.arguments,
371
+ };
372
+ }
373
+
374
+ // Try recoverable (unclosed parameters)
375
+ const recovered = parseRecoverableXmlToolCall(block, this.currentOpenTag, this.tools);
376
+ if (recovered) {
377
+ return {
378
+ id: `call_${uuidv4()}`,
379
+ name: recovered.name,
380
+ arguments: recovered.arguments,
381
+ };
382
+ }
383
+
384
+ // Try JSON (single or multiple)
385
+ const jsonParsed = this.parseToolContent(block);
386
+ if (jsonParsed.length > 0) {
387
+ const first = jsonParsed[0];
388
+ const attrName = extractToolName(this.currentOpenTag, block);
389
+ if (attrName && !first.name) first.name = attrName;
390
+ if (first.name) return first;
391
+ }
392
+
393
+ return null;
394
+ }
395
+
396
+ private parseToolContent(str: string): ParsedToolCall[] {
397
+ const calls: ParsedToolCall[] = [];
398
+
399
+ // Try parsing as single JSON first
400
+ try {
401
+ const parsed = robustParseJSON(str);
402
+ if (parsed && typeof parsed === 'object') {
403
+ const tc = this.parseToolCall(parsed);
404
+ if (tc) calls.push(tc);
405
+ }
406
+ } catch {}
407
+
408
+ // Always try line-by-line parsing for multi-JSON content (independent of single parse)
409
+ if (str.includes('\n')) {
410
+ const lines = str.split('\n').map(l => l.trim()).filter(l => l.startsWith('{') && l.endsWith('}'));
411
+ for (const line of lines) {
412
+ try {
413
+ const parsed = JSON.parse(line);
414
+ if (parsed && typeof parsed === 'object') {
415
+ const tc = this.parseToolCall(parsed);
416
+ if (tc && !calls.some(c => c.name === tc.name && JSON.stringify(c.arguments) === JSON.stringify(tc.arguments))) {
417
+ calls.push(tc);
418
+ }
419
+ }
420
+ } catch {}
421
+ }
422
+ }
423
+
424
+ return calls;
425
+ }
426
+
427
+ private parseToolCall(parsed: any): ParsedToolCall | null {
428
+ if (!parsed || typeof parsed !== 'object') return null;
429
+
430
+ const name = parsed.name || parsed.function?.name || parsed.tool_name || parsed.tool;
431
+ if (!name || typeof name !== 'string' || name.length === 0) return null;
432
+
433
+ let args = parsed.arguments || parsed.function?.arguments || parsed.args || parsed.parameters || parsed.input || {};
434
+ if (typeof args === 'string') {
435
+ try { args = JSON.parse(args); }
436
+ catch { args = {}; }
437
+ }
438
+ if (typeof args !== 'object' || args === null) args = {};
439
+
440
+ return {
441
+ id: parsed.id || parsed.tool_call_id || `call_${uuidv4()}`,
442
+ name,
443
+ arguments: args,
444
+ };
445
+ }
446
+ }
@@ -0,0 +1,122 @@
1
+ import type { ToolCallAST, ToolDefinition } from './ast.js';
2
+ import { detectToolCalls } from './detector.js';
3
+ import { repairToolCall } from './repair.js';
4
+ import { RequestToolRegistry } from './registry-runtime.js';
5
+ import { coerceArguments } from './coercion.js';
6
+ import { validateToolCall, type ValidationResult } from './validator.js';
7
+ import { calculateConfidence } from './confidence.js';
8
+ import { v4 as uuidv4 } from 'uuid';
9
+
10
+ export interface PipelineResult {
11
+ textContent: string;
12
+ toolCalls: ToolCallAST[];
13
+ errors: Array<{
14
+ toolName?: string;
15
+ code: string;
16
+ message: string;
17
+ details?: any;
18
+ }>;
19
+ }
20
+
21
+ const CONFIDENCE_THRESHOLD = 0.7;
22
+
23
+ export function processToolCalls(
24
+ text: string,
25
+ requestTools: unknown[]
26
+ ): PipelineResult {
27
+ const registry = new RequestToolRegistry(requestTools);
28
+ const detected = detectToolCalls(text);
29
+
30
+ const toolCalls: ToolCallAST[] = [];
31
+ const errors: PipelineResult['errors'] = [];
32
+ let textContent = text;
33
+
34
+ for (const det of detected) {
35
+ while (textContent.includes(det.raw)) {
36
+ textContent = textContent.replace(det.raw, '').trim();
37
+ }
38
+ }
39
+
40
+ for (const det of detected) {
41
+ const repaired = repairToolCall(det.extracted);
42
+ if (!repaired) {
43
+ errors.push({
44
+ code: 'MALFORMED_TOOL_CALL',
45
+ message: 'Could not repair or identify tool call structure',
46
+ details: det.extracted,
47
+ });
48
+ continue;
49
+ }
50
+
51
+ if (!registry.has(repaired.name)) {
52
+ errors.push({
53
+ toolName: repaired.name,
54
+ code: 'UNKNOWN_TOOL',
55
+ message: `Tool '${repaired.name}' is not registered or provided in the request`,
56
+ });
57
+ continue;
58
+ }
59
+
60
+ const toolDef = registry.get(repaired.name)!;
61
+ const coercedArgs = coerceArguments(repaired.arguments, toolDef.schema);
62
+
63
+ const ast: ToolCallAST = {
64
+ id: `call_${uuidv4()}`,
65
+ name: repaired.name,
66
+ arguments: coercedArgs,
67
+ raw: det.raw,
68
+ confidence: 0.0,
69
+ };
70
+
71
+ const validation: ValidationResult = validateToolCall(ast, toolDef);
72
+ const confidenceResult = calculateConfidence(ast, toolDef);
73
+ ast.confidence = confidenceResult.score;
74
+
75
+ if (!validation.valid) {
76
+ if (validation.missingFields.length > 0) {
77
+ errors.push({
78
+ toolName: ast.name,
79
+ code: 'MISSING_REQUIRED_FIELD',
80
+ message: `Missing required fields: ${validation.missingFields.join(', ')}`,
81
+ details: validation.errors,
82
+ });
83
+ } else {
84
+ errors.push({
85
+ toolName: ast.name,
86
+ code: 'SCHEMA_VALIDATION_FAILED',
87
+ message: 'Arguments do not match the tool schema',
88
+ details: validation.errors,
89
+ });
90
+ }
91
+ continue;
92
+ }
93
+
94
+ if (ast.confidence >= CONFIDENCE_THRESHOLD) {
95
+ toolCalls.push(ast);
96
+ } else {
97
+ errors.push({
98
+ toolName: ast.name,
99
+ code: 'LOW_CONFIDENCE',
100
+ message: `Tool call confidence ${ast.confidence} is below threshold ${CONFIDENCE_THRESHOLD}`,
101
+ details: confidenceResult.reasons,
102
+ });
103
+ }
104
+ }
105
+
106
+ return {
107
+ textContent,
108
+ toolCalls,
109
+ errors,
110
+ };
111
+ }
112
+
113
+ export function formatOpenAIToolCalls(toolCalls: ToolCallAST[]): any[] {
114
+ return toolCalls.map(tc => ({
115
+ id: tc.id,
116
+ type: 'function',
117
+ function: {
118
+ name: tc.name,
119
+ arguments: JSON.stringify(tc.arguments),
120
+ },
121
+ }));
122
+ }
@@ -0,0 +1,34 @@
1
+ import type { ToolDefinition } from './ast.js';
2
+
3
+ export class RequestToolRegistry {
4
+ private registry: Map<string, ToolDefinition>;
5
+
6
+ constructor(requestTools: unknown[] = []) {
7
+ this.registry = new Map();
8
+ for (const tool of requestTools) {
9
+ if (tool && typeof tool === 'object') {
10
+ const t = tool as any;
11
+ if (t.type === 'function' && t.function && typeof t.function === 'object') {
12
+ const fn = t.function;
13
+ this.registry.set(fn.name, {
14
+ name: fn.name,
15
+ description: fn.description,
16
+ schema: fn.parameters || {},
17
+ });
18
+ }
19
+ }
20
+ }
21
+ }
22
+
23
+ has(name: string): boolean {
24
+ return this.registry.has(name);
25
+ }
26
+
27
+ get(name: string): ToolDefinition | undefined {
28
+ return this.registry.get(name);
29
+ }
30
+
31
+ list(): ToolDefinition[] {
32
+ return Array.from(this.registry.values());
33
+ }
34
+ }