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