@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,142 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* File: registry.ts
|
|
3
|
+
* Project: qwenproxy
|
|
4
|
+
* Tool registry with register/lookup and OpenAI-compatible schema export
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
FunctionToolDefinition,
|
|
9
|
+
JsonSchema,
|
|
10
|
+
ToolContext,
|
|
11
|
+
ToolHandler,
|
|
12
|
+
ToolRegistration,
|
|
13
|
+
} from './types';
|
|
14
|
+
import { validateAgainstSchema } from './schema.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Central tool registry. Tools are registered at startup and looked up by name
|
|
18
|
+
* during the execution loop.
|
|
19
|
+
*/
|
|
20
|
+
export class ToolRegistry {
|
|
21
|
+
private tools = new Map<string, ToolRegistration>();
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Register a new tool.
|
|
25
|
+
* @param name Unique tool name (must match the function name the LLM will emit)
|
|
26
|
+
* @param description Human-readable description (sent to the LLM)
|
|
27
|
+
* @param parameters JSON Schema describing the tool's parameters
|
|
28
|
+
* @param handler Async function that executes the tool
|
|
29
|
+
* @param strict When true, additionalProperties:false is enforced and
|
|
30
|
+
* missing required fields are rejected (default true)
|
|
31
|
+
*/
|
|
32
|
+
register<TArgs = any, TResult = any>(
|
|
33
|
+
name: string,
|
|
34
|
+
description: string,
|
|
35
|
+
parameters: JsonSchema,
|
|
36
|
+
handler: ToolHandler<TArgs, TResult>,
|
|
37
|
+
strict = true
|
|
38
|
+
): void {
|
|
39
|
+
if (this.tools.has(name)) {
|
|
40
|
+
throw new Error(`Tool '${name}' is already registered`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// If strict mode, ensure the schema enforces additionalProperties: false
|
|
44
|
+
const enforcedParams = strict
|
|
45
|
+
? { ...parameters, additionalProperties: false }
|
|
46
|
+
: parameters;
|
|
47
|
+
|
|
48
|
+
this.tools.set(name, {
|
|
49
|
+
name,
|
|
50
|
+
description,
|
|
51
|
+
parameters: enforcedParams,
|
|
52
|
+
strict,
|
|
53
|
+
handler: handler as ToolHandler,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Unregister a tool by name. Useful for testing.
|
|
59
|
+
*/
|
|
60
|
+
unregister(name: string): boolean {
|
|
61
|
+
return this.tools.delete(name);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Look up a tool by name. Returns undefined if not found.
|
|
66
|
+
*/
|
|
67
|
+
get(name: string): ToolRegistration | undefined {
|
|
68
|
+
return this.tools.get(name);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Check whether a tool with the given name exists.
|
|
73
|
+
*/
|
|
74
|
+
has(name: string): boolean {
|
|
75
|
+
return this.tools.has(name);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Return all registered tool names.
|
|
80
|
+
*/
|
|
81
|
+
listNames(): string[] {
|
|
82
|
+
return Array.from(this.tools.keys());
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Return the OpenAI-compatible tool definitions array
|
|
87
|
+
* (for inclusion in the `tools` field of the request body sent to the LLM).
|
|
88
|
+
*/
|
|
89
|
+
toOpenAITools(): FunctionToolDefinition[] {
|
|
90
|
+
const defs: FunctionToolDefinition[] = [];
|
|
91
|
+
for (const tool of this.tools.values()) {
|
|
92
|
+
defs.push({
|
|
93
|
+
type: 'function',
|
|
94
|
+
function: {
|
|
95
|
+
name: tool.name,
|
|
96
|
+
description: tool.description,
|
|
97
|
+
parameters: tool.parameters,
|
|
98
|
+
strict: tool.strict,
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return defs;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Validate a tool call's arguments against the registered schema, then
|
|
107
|
+
* invoke the handler. Returns a serialised result string.
|
|
108
|
+
*
|
|
109
|
+
* @throws SchemaValidationError if validation fails
|
|
110
|
+
* @throws Error if the tool is not found
|
|
111
|
+
*/
|
|
112
|
+
async execute(
|
|
113
|
+
toolName: string,
|
|
114
|
+
rawArgs: Record<string, unknown>,
|
|
115
|
+
context: ToolContext
|
|
116
|
+
): Promise<string> {
|
|
117
|
+
const registration = this.tools.get(toolName);
|
|
118
|
+
if (!registration) {
|
|
119
|
+
throw new Error(`Unknown tool: '${toolName}'`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Strict validation
|
|
123
|
+
const validatedArgs = validateAgainstSchema(
|
|
124
|
+
rawArgs,
|
|
125
|
+
registration.parameters,
|
|
126
|
+
`$.${toolName}`
|
|
127
|
+
) as Record<string, unknown>;
|
|
128
|
+
|
|
129
|
+
const result = await registration.handler(validatedArgs, context);
|
|
130
|
+
|
|
131
|
+
// Serialize result
|
|
132
|
+
if (typeof result === 'string') {
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
return JSON.stringify(result, null, 2);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Singleton registry instance shared across the application.
|
|
141
|
+
*/
|
|
142
|
+
export const registry = new ToolRegistry();
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface RepairedTool {
|
|
2
|
+
name: string;
|
|
3
|
+
arguments: unknown;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function repairToolCall(extracted: Record<string, unknown>): RepairedTool | null {
|
|
7
|
+
if ('tool' in extracted && !('name' in extracted)) {
|
|
8
|
+
return {
|
|
9
|
+
name: String(extracted.tool),
|
|
10
|
+
arguments: extracted.arguments || {},
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if ('function' in extracted && typeof extracted.function === 'object' && extracted.function !== null) {
|
|
15
|
+
const fn = extracted.function as Record<string, unknown>;
|
|
16
|
+
if ('name' in fn) {
|
|
17
|
+
return {
|
|
18
|
+
name: String(fn.name),
|
|
19
|
+
arguments: fn.arguments || {},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if ('function_call' in extracted && typeof extracted.function_call === 'object' && extracted.function_call !== null) {
|
|
25
|
+
const fn = extracted.function_call as Record<string, unknown>;
|
|
26
|
+
if ('name' in fn) {
|
|
27
|
+
return {
|
|
28
|
+
name: String(fn.name),
|
|
29
|
+
arguments: fn.arguments || {},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if ('name' in extracted) {
|
|
35
|
+
return {
|
|
36
|
+
name: String(extracted.name),
|
|
37
|
+
arguments: extracted.arguments || {},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* File: schema.ts
|
|
3
|
+
* Project: qwenproxy
|
|
4
|
+
* Strict JSON Schema validator for tool calling
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { JsonSchema } from './types';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Error thrown when schema validation fails.
|
|
11
|
+
*/
|
|
12
|
+
export class SchemaValidationError extends Error {
|
|
13
|
+
public readonly path: string;
|
|
14
|
+
public readonly value: unknown;
|
|
15
|
+
|
|
16
|
+
constructor(message: string, path: string, value?: unknown) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = 'SchemaValidationError';
|
|
19
|
+
this.path = path;
|
|
20
|
+
this.value = value;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Validates a value against a JSON Schema with strict type checking.
|
|
26
|
+
* Throws SchemaValidationError on failure.
|
|
27
|
+
* Returns the validated (possibly coerced) value on success.
|
|
28
|
+
*/
|
|
29
|
+
export function validateAgainstSchema(
|
|
30
|
+
value: unknown,
|
|
31
|
+
schema: JsonSchema,
|
|
32
|
+
path: string = '$'
|
|
33
|
+
): unknown {
|
|
34
|
+
// Handle nullable schemas
|
|
35
|
+
if (schema.nullable && (value === null || value === undefined)) {
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
switch (schema.type) {
|
|
40
|
+
case 'object':
|
|
41
|
+
return validateObject(value, schema, path);
|
|
42
|
+
case 'array':
|
|
43
|
+
return validateArray(value, schema, path);
|
|
44
|
+
case 'string':
|
|
45
|
+
return validateString(value, schema, path);
|
|
46
|
+
case 'number':
|
|
47
|
+
case 'integer':
|
|
48
|
+
return validateNumber(value, schema, path);
|
|
49
|
+
case 'boolean':
|
|
50
|
+
return validateBoolean(value, schema, path);
|
|
51
|
+
case 'null':
|
|
52
|
+
if (value !== null) {
|
|
53
|
+
throw new SchemaValidationError(
|
|
54
|
+
`Expected null at ${path}, got ${typeof value}`,
|
|
55
|
+
path,
|
|
56
|
+
value
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
default:
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function validateObject(
|
|
66
|
+
value: unknown,
|
|
67
|
+
schema: JsonSchema,
|
|
68
|
+
path: string
|
|
69
|
+
): Record<string, unknown> {
|
|
70
|
+
if (value === null || value === undefined) {
|
|
71
|
+
throw new SchemaValidationError(
|
|
72
|
+
`Expected object at ${path}, got ${value === null ? 'null' : 'undefined'}`,
|
|
73
|
+
path,
|
|
74
|
+
value
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
78
|
+
throw new SchemaValidationError(
|
|
79
|
+
`Expected object at ${path}, got ${typeof value}`,
|
|
80
|
+
path,
|
|
81
|
+
value
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const obj = value as Record<string, unknown>;
|
|
86
|
+
const validated: Record<string, unknown> = {};
|
|
87
|
+
|
|
88
|
+
// Check required properties
|
|
89
|
+
if (schema.required) {
|
|
90
|
+
for (const req of schema.required) {
|
|
91
|
+
if (!(req in obj) || obj[req] === undefined) {
|
|
92
|
+
throw new SchemaValidationError(
|
|
93
|
+
`Missing required property '${req}' at ${path}`,
|
|
94
|
+
`${path}.${req}`,
|
|
95
|
+
undefined
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Validate and collect properties
|
|
102
|
+
const properties = schema.properties || {};
|
|
103
|
+
const seenKeys = new Set<string>();
|
|
104
|
+
|
|
105
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
106
|
+
seenKeys.add(key);
|
|
107
|
+
const propSchema = properties[key];
|
|
108
|
+
if (propSchema) {
|
|
109
|
+
validated[key] = validateAgainstSchema(val, propSchema, `${path}.${key}`);
|
|
110
|
+
} else if (schema.additionalProperties === false) {
|
|
111
|
+
throw new SchemaValidationError(
|
|
112
|
+
`Unexpected property '${key}' at ${path} (additionalProperties is false)`,
|
|
113
|
+
`${path}.${key}`,
|
|
114
|
+
val
|
|
115
|
+
);
|
|
116
|
+
} else if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
|
|
117
|
+
validated[key] = validateAgainstSchema(
|
|
118
|
+
val,
|
|
119
|
+
schema.additionalProperties as JsonSchema,
|
|
120
|
+
`${path}.${key}`
|
|
121
|
+
);
|
|
122
|
+
} else {
|
|
123
|
+
validated[key] = val;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Apply defaults for missing properties
|
|
128
|
+
for (const [key, propSchema] of Object.entries(properties)) {
|
|
129
|
+
if (!seenKeys.has(key) && propSchema.default !== undefined) {
|
|
130
|
+
validated[key] = propSchema.default;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return validated;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function validateArray(
|
|
138
|
+
value: unknown,
|
|
139
|
+
schema: JsonSchema,
|
|
140
|
+
path: string
|
|
141
|
+
): unknown[] {
|
|
142
|
+
if (!Array.isArray(value)) {
|
|
143
|
+
throw new SchemaValidationError(
|
|
144
|
+
`Expected array at ${path}, got ${typeof value}`,
|
|
145
|
+
path,
|
|
146
|
+
value
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (schema.minItems !== undefined && value.length < schema.minItems) {
|
|
151
|
+
throw new SchemaValidationError(
|
|
152
|
+
`Array at ${path} has ${value.length} items, minimum is ${schema.minItems}`,
|
|
153
|
+
path,
|
|
154
|
+
value
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (schema.maxItems !== undefined && value.length > schema.maxItems) {
|
|
159
|
+
throw new SchemaValidationError(
|
|
160
|
+
`Array at ${path} has ${value.length} items, maximum is ${schema.maxItems}`,
|
|
161
|
+
path,
|
|
162
|
+
value
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (schema.items) {
|
|
167
|
+
return value.map((item, i) =>
|
|
168
|
+
validateAgainstSchema(item, schema.items!, `${path}[${i}]`)
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return value;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function validateString(
|
|
176
|
+
value: unknown,
|
|
177
|
+
schema: JsonSchema,
|
|
178
|
+
path: string
|
|
179
|
+
): string {
|
|
180
|
+
if (typeof value !== 'string') {
|
|
181
|
+
// Strict: no coercion from numbers/booleans
|
|
182
|
+
throw new SchemaValidationError(
|
|
183
|
+
`Expected string at ${path}, got ${typeof value}`,
|
|
184
|
+
path,
|
|
185
|
+
value
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (schema.minLength !== undefined && value.length < schema.minLength) {
|
|
190
|
+
throw new SchemaValidationError(
|
|
191
|
+
`String at ${path} is ${value.length} chars, minimum is ${schema.minLength}`,
|
|
192
|
+
path,
|
|
193
|
+
value
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (schema.maxLength !== undefined && value.length > schema.maxLength) {
|
|
198
|
+
throw new SchemaValidationError(
|
|
199
|
+
`String at ${path} is ${value.length} chars, maximum is ${schema.maxLength}`,
|
|
200
|
+
path,
|
|
201
|
+
value
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (schema.pattern && !new RegExp(schema.pattern).test(value)) {
|
|
206
|
+
throw new SchemaValidationError(
|
|
207
|
+
`String at ${path} does not match pattern '${schema.pattern}'`,
|
|
208
|
+
path,
|
|
209
|
+
value
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (schema.enum && !schema.enum.includes(value)) {
|
|
214
|
+
throw new SchemaValidationError(
|
|
215
|
+
`Value '${value}' at ${path} is not one of [${schema.enum.map(e => `'${e}'`).join(', ')}]`,
|
|
216
|
+
path,
|
|
217
|
+
value
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return value;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function validateNumber(
|
|
225
|
+
value: unknown,
|
|
226
|
+
schema: JsonSchema,
|
|
227
|
+
path: string
|
|
228
|
+
): number {
|
|
229
|
+
if (typeof value !== 'number' || Number.isNaN(value)) {
|
|
230
|
+
throw new SchemaValidationError(
|
|
231
|
+
`Expected number at ${path}, got ${typeof value}`,
|
|
232
|
+
path,
|
|
233
|
+
value
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (schema.type === 'integer' && !Number.isInteger(value)) {
|
|
238
|
+
throw new SchemaValidationError(
|
|
239
|
+
`Expected integer at ${path}, got float ${value}`,
|
|
240
|
+
path,
|
|
241
|
+
value
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (schema.minimum !== undefined && value < schema.minimum) {
|
|
246
|
+
throw new SchemaValidationError(
|
|
247
|
+
`Number ${value} at ${path} is below minimum ${schema.minimum}`,
|
|
248
|
+
path,
|
|
249
|
+
value
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (schema.maximum !== undefined && value > schema.maximum) {
|
|
254
|
+
throw new SchemaValidationError(
|
|
255
|
+
`Number ${value} at ${path} is above maximum ${schema.maximum}`,
|
|
256
|
+
path,
|
|
257
|
+
value
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (schema.enum && !schema.enum.includes(value)) {
|
|
262
|
+
throw new SchemaValidationError(
|
|
263
|
+
`Value ${value} at ${path} is not one of [${schema.enum.join(', ')}]`,
|
|
264
|
+
path,
|
|
265
|
+
value
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return value;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function validateBoolean(
|
|
273
|
+
value: unknown,
|
|
274
|
+
schema: JsonSchema,
|
|
275
|
+
path: string
|
|
276
|
+
): boolean {
|
|
277
|
+
if (typeof value !== 'boolean') {
|
|
278
|
+
throw new SchemaValidationError(
|
|
279
|
+
`Expected boolean at ${path}, got ${typeof value}`,
|
|
280
|
+
path,
|
|
281
|
+
value
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
return value;
|
|
285
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* File: types.ts
|
|
3
|
+
* Project: qwenproxy
|
|
4
|
+
* Tool system types
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* JSON Schema definition following the OpenAI function calling spec.
|
|
9
|
+
*/
|
|
10
|
+
export interface JsonSchema {
|
|
11
|
+
type: string;
|
|
12
|
+
properties?: Record<string, JsonSchema>;
|
|
13
|
+
items?: JsonSchema;
|
|
14
|
+
required?: string[];
|
|
15
|
+
enum?: unknown[];
|
|
16
|
+
const?: unknown;
|
|
17
|
+
default?: unknown;
|
|
18
|
+
description?: string;
|
|
19
|
+
additionalProperties?: boolean | JsonSchema;
|
|
20
|
+
anyOf?: JsonSchema[];
|
|
21
|
+
oneOf?: JsonSchema[];
|
|
22
|
+
allOf?: JsonSchema[];
|
|
23
|
+
not?: JsonSchema;
|
|
24
|
+
if?: JsonSchema;
|
|
25
|
+
then?: JsonSchema;
|
|
26
|
+
else?: JsonSchema;
|
|
27
|
+
minimum?: number;
|
|
28
|
+
maximum?: number;
|
|
29
|
+
minLength?: number;
|
|
30
|
+
maxLength?: number;
|
|
31
|
+
pattern?: string;
|
|
32
|
+
format?: string;
|
|
33
|
+
minItems?: number;
|
|
34
|
+
maxItems?: number;
|
|
35
|
+
uniqueItems?: boolean;
|
|
36
|
+
nullable?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* OpenAI-compatible function tool definition.
|
|
41
|
+
*/
|
|
42
|
+
export interface FunctionToolDefinition {
|
|
43
|
+
type: 'function';
|
|
44
|
+
function: {
|
|
45
|
+
name: string;
|
|
46
|
+
description?: string;
|
|
47
|
+
parameters?: JsonSchema;
|
|
48
|
+
strict?: boolean;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Internal tool registration entry.
|
|
54
|
+
*/
|
|
55
|
+
export interface ToolRegistration {
|
|
56
|
+
name: string;
|
|
57
|
+
description: string;
|
|
58
|
+
parameters: JsonSchema;
|
|
59
|
+
strict: boolean;
|
|
60
|
+
handler: ToolHandler;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Handler function signature for a registered tool.
|
|
65
|
+
* Receives the parsed and validated arguments.
|
|
66
|
+
* Returns the result as a string (or object that will be JSON-stringified).
|
|
67
|
+
*/
|
|
68
|
+
export type ToolHandler<TArgs = any, TResult = any> = (
|
|
69
|
+
args: TArgs,
|
|
70
|
+
context: ToolContext
|
|
71
|
+
) => Promise<TResult>;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Context passed to tool handlers during execution.
|
|
75
|
+
*/
|
|
76
|
+
export interface ToolContext {
|
|
77
|
+
/** The original messages from the request */
|
|
78
|
+
messages: unknown[];
|
|
79
|
+
/** The current turn number in the execution loop */
|
|
80
|
+
turn: number;
|
|
81
|
+
/** The model being used */
|
|
82
|
+
model: string;
|
|
83
|
+
/** Custom state or services can be attached here */
|
|
84
|
+
[key: string]: any;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A parsed tool call from the LLM response.
|
|
89
|
+
*/
|
|
90
|
+
export interface ParsedToolCall {
|
|
91
|
+
id: string;
|
|
92
|
+
name: string;
|
|
93
|
+
arguments: Record<string, unknown>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Result of executing a single tool call.
|
|
98
|
+
*/
|
|
99
|
+
export interface ToolCallResult {
|
|
100
|
+
toolCallId: string;
|
|
101
|
+
name: string;
|
|
102
|
+
result: string;
|
|
103
|
+
isError: boolean;
|
|
104
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import Ajv from 'ajv';
|
|
2
|
+
import type { ToolCallAST, ToolDefinition } from './ast.js';
|
|
3
|
+
|
|
4
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
5
|
+
|
|
6
|
+
export interface ValidationResult {
|
|
7
|
+
valid: boolean;
|
|
8
|
+
errors: any[] | null;
|
|
9
|
+
missingFields: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function validateToolCall(ast: ToolCallAST, toolDef: ToolDefinition): ValidationResult {
|
|
13
|
+
const validate = ajv.compile(toolDef.schema);
|
|
14
|
+
const valid = validate(ast.arguments);
|
|
15
|
+
|
|
16
|
+
const missingFields: string[] = [];
|
|
17
|
+
if (toolDef.schema && typeof toolDef.schema === 'object' && 'required' in toolDef.schema) {
|
|
18
|
+
const required = (toolDef.schema as any).required;
|
|
19
|
+
if (Array.isArray(required)) {
|
|
20
|
+
for (const field of required) {
|
|
21
|
+
if (!(ast.arguments && typeof ast.arguments === 'object' && field in (ast.arguments as Record<string, unknown>))) {
|
|
22
|
+
missingFields.push(field);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
valid,
|
|
30
|
+
errors: validate.errors ?? null,
|
|
31
|
+
missingFields,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export interface TruncatedMessage {
|
|
2
|
+
role: string;
|
|
3
|
+
content: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function estimateTokenCount(text: string): number {
|
|
7
|
+
return Math.ceil(text.length / 3.5);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function truncateMessages(
|
|
11
|
+
messages: Array<{ role: string; content: string | null | any[] }>,
|
|
12
|
+
maxContextLength: number,
|
|
13
|
+
systemPrompt: string = ''
|
|
14
|
+
): Array<{ role: string; content: string }> {
|
|
15
|
+
const systemTokens = estimateTokenCount(systemPrompt);
|
|
16
|
+
const availableTokens = maxContextLength - systemTokens - 500;
|
|
17
|
+
|
|
18
|
+
if (availableTokens <= 0) {
|
|
19
|
+
return [{ role: 'user', content: systemPrompt }];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const result: Array<{ role: string; content: string }> = [];
|
|
23
|
+
let usedTokens = 0;
|
|
24
|
+
|
|
25
|
+
const normalizedMessages = messages.map(msg => {
|
|
26
|
+
let contentStr = '';
|
|
27
|
+
if (Array.isArray(msg.content)) {
|
|
28
|
+
contentStr = msg.content.map((c: any) => c.text || JSON.stringify(c)).join('\n');
|
|
29
|
+
} else if (typeof msg.content === 'object' && msg.content !== null) {
|
|
30
|
+
contentStr = JSON.stringify(msg.content);
|
|
31
|
+
} else {
|
|
32
|
+
contentStr = msg.content || '';
|
|
33
|
+
}
|
|
34
|
+
return { role: msg.role, content: contentStr };
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
for (let i = normalizedMessages.length - 1; i >= 0; i--) {
|
|
38
|
+
const msg = normalizedMessages[i];
|
|
39
|
+
const msgTokens = estimateTokenCount(msg.content);
|
|
40
|
+
|
|
41
|
+
if (usedTokens + msgTokens <= availableTokens) {
|
|
42
|
+
result.unshift(msg);
|
|
43
|
+
usedTokens += msgTokens;
|
|
44
|
+
} else {
|
|
45
|
+
const remainingTokens = availableTokens - usedTokens;
|
|
46
|
+
if (remainingTokens > 100) {
|
|
47
|
+
const truncatedContent = msg.content.slice(0, remainingTokens * 3.5);
|
|
48
|
+
result.unshift({ role: msg.role, content: `[Truncated] ${truncatedContent}...` });
|
|
49
|
+
}
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (result.length === 0 && normalizedMessages.length > 0) {
|
|
55
|
+
const lastMsg = normalizedMessages[normalizedMessages.length - 1];
|
|
56
|
+
const truncatedContent = lastMsg.content.slice(0, Math.max(200, availableTokens * 3.5));
|
|
57
|
+
result.push({ role: lastMsg.role, content: `[Truncated] ${truncatedContent}...` });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return result;
|
|
61
|
+
}
|