@node-llm/core 1.9.0 → 1.10.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 (43) hide show
  1. package/README.md +35 -0
  2. package/dist/aliases.d.ts +102 -9
  3. package/dist/aliases.d.ts.map +1 -1
  4. package/dist/aliases.js +102 -9
  5. package/dist/chat/Chat.d.ts +1 -0
  6. package/dist/chat/Chat.d.ts.map +1 -1
  7. package/dist/chat/Chat.js +169 -131
  8. package/dist/chat/ChatOptions.d.ts +2 -0
  9. package/dist/chat/ChatOptions.d.ts.map +1 -1
  10. package/dist/chat/ChatStream.d.ts.map +1 -1
  11. package/dist/chat/ChatStream.js +109 -66
  12. package/dist/index.d.ts +3 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +2 -0
  15. package/dist/llm.d.ts +8 -1
  16. package/dist/llm.d.ts.map +1 -1
  17. package/dist/llm.js +156 -59
  18. package/dist/middlewares/CostGuardMiddleware.d.ts +24 -0
  19. package/dist/middlewares/CostGuardMiddleware.d.ts.map +1 -0
  20. package/dist/middlewares/CostGuardMiddleware.js +23 -0
  21. package/dist/middlewares/PIIMaskMiddleware.d.ts +23 -0
  22. package/dist/middlewares/PIIMaskMiddleware.d.ts.map +1 -0
  23. package/dist/middlewares/PIIMaskMiddleware.js +41 -0
  24. package/dist/middlewares/UsageLoggerMiddleware.d.ts +22 -0
  25. package/dist/middlewares/UsageLoggerMiddleware.d.ts.map +1 -0
  26. package/dist/middlewares/UsageLoggerMiddleware.js +30 -0
  27. package/dist/middlewares/index.d.ts +4 -0
  28. package/dist/middlewares/index.d.ts.map +1 -0
  29. package/dist/middlewares/index.js +3 -0
  30. package/dist/models/models.json +1458 -448
  31. package/dist/providers/BaseProvider.d.ts +6 -1
  32. package/dist/providers/BaseProvider.d.ts.map +1 -1
  33. package/dist/providers/BaseProvider.js +19 -0
  34. package/dist/providers/openai/OpenAIProvider.d.ts +1 -1
  35. package/dist/providers/openai/OpenAIProvider.d.ts.map +1 -1
  36. package/dist/providers/openai/OpenAIProvider.js +13 -2
  37. package/dist/types/Middleware.d.ts +98 -0
  38. package/dist/types/Middleware.d.ts.map +1 -0
  39. package/dist/types/Middleware.js +1 -0
  40. package/dist/utils/middleware-runner.d.ts +7 -0
  41. package/dist/utils/middleware-runner.d.ts.map +1 -0
  42. package/dist/utils/middleware-runner.js +20 -0
  43. package/package.json +1 -1
package/dist/chat/Chat.js CHANGED
@@ -5,6 +5,7 @@ import { ChatStream } from "./ChatStream.js";
5
5
  import { ModelRegistry } from "../models/ModelRegistry.js";
6
6
  import { Schema } from "../schema/Schema.js";
7
7
  import { toJsonSchema } from "../schema/to-json-schema.js";
8
+ import { randomUUID } from "node:crypto";
8
9
  import { z } from "zod";
9
10
  import { config } from "../config.js";
10
11
  import { ToolExecutionMode } from "../constants.js";
@@ -12,6 +13,7 @@ import { ConfigurationError } from "../errors/index.js";
12
13
  import { ChatValidator } from "./Validation.js";
13
14
  import { ToolHandler } from "./ToolHandler.js";
14
15
  import { logger } from "../utils/logger.js";
16
+ import { runMiddleware } from "../utils/middleware-runner.js";
15
17
  import { ChatResponseString } from "./ChatResponse.js";
16
18
  export class Chat {
17
19
  provider;
@@ -20,10 +22,12 @@ export class Chat {
20
22
  messages = [];
21
23
  systemMessages = [];
22
24
  executor;
25
+ middlewares = [];
23
26
  constructor(provider, model, options = {}, retryConfig = { attempts: 1, delayMs: 0 }) {
24
27
  this.provider = provider;
25
28
  this.model = model;
26
29
  this.options = options;
30
+ this.middlewares = options.middlewares || [];
27
31
  this.executor = new Executor(provider, retryConfig);
28
32
  if (options.systemPrompt) {
29
33
  this.withInstructions(options.systemPrompt);
@@ -302,6 +306,8 @@ export class Chat {
302
306
  * Ask the model a question
303
307
  */
304
308
  async ask(content, options) {
309
+ const requestId = randomUUID();
310
+ const state = {};
305
311
  let messageContent = content;
306
312
  const files = [...(options?.images ?? []), ...(options?.files ?? [])];
307
313
  if (files.length > 0) {
@@ -332,146 +338,74 @@ export class Chat {
332
338
  }
333
339
  };
334
340
  }
335
- const executeOptions = {
341
+ // Prepare Middleware Context
342
+ const context = {
343
+ requestId,
344
+ provider: this.provider.id,
336
345
  model: this.model,
337
346
  messages: [...this.systemMessages, ...this.messages],
338
- tools: this.options.tools,
339
- temperature: options?.temperature ?? this.options.temperature,
340
- max_tokens: options?.maxTokens ?? this.options.maxTokens ?? config.maxTokens,
341
- headers: { ...this.options.headers, ...options?.headers },
342
- response_format: responseFormat, // Pass to provider
343
- requestTimeout: options?.requestTimeout ?? this.options.requestTimeout ?? config.requestTimeout,
344
- thinking: options?.thinking ?? this.options.thinking,
345
- signal: options?.signal,
346
- ...this.options.params
347
+ options: this.options,
348
+ state
347
349
  };
348
- // --- Content Policy Hooks (Input) ---
349
- if (this.options.onBeforeRequest) {
350
- const messagesToProcess = [...this.systemMessages, ...this.messages];
351
- const result = await this.options.onBeforeRequest(messagesToProcess);
352
- if (result) {
353
- // If the hook returned modified messages, use them for this request
354
- executeOptions.messages = result;
355
- }
356
- }
357
- const totalUsage = { input_tokens: 0, output_tokens: 0, total_tokens: 0 };
358
- const trackUsage = (u) => {
359
- if (u) {
360
- // Fallback cost calculation if provider didn't return it
361
- if (u.cost === undefined) {
362
- const withCost = ModelRegistry.calculateCost(u, this.model, this.provider.id);
363
- u.cost = withCost.cost;
364
- u.input_cost = withCost.input_cost;
365
- u.output_cost = withCost.output_cost;
366
- }
367
- totalUsage.input_tokens += u.input_tokens;
368
- totalUsage.output_tokens += u.output_tokens;
369
- totalUsage.total_tokens += u.total_tokens;
370
- if (u.cached_tokens) {
371
- totalUsage.cached_tokens = (totalUsage.cached_tokens ?? 0) + u.cached_tokens;
372
- }
373
- if (u.cost !== undefined) {
374
- totalUsage.cost = (totalUsage.cost ?? 0) + u.cost;
375
- }
376
- if (u.input_cost !== undefined) {
377
- totalUsage.input_cost = (totalUsage.input_cost ?? 0) + u.input_cost;
378
- }
379
- if (u.output_cost !== undefined) {
380
- totalUsage.output_cost = (totalUsage.output_cost ?? 0) + u.output_cost;
350
+ try {
351
+ // 1. onRequest Hook
352
+ await runMiddleware(this.middlewares, "onRequest", context);
353
+ // Re-read mutable context
354
+ const messagesToUse = context.messages || [];
355
+ const executeOptions = {
356
+ model: this.model,
357
+ messages: messagesToUse,
358
+ tools: this.options.tools,
359
+ temperature: options?.temperature ?? this.options.temperature,
360
+ max_tokens: options?.maxTokens ?? this.options.maxTokens ?? config.maxTokens,
361
+ headers: { ...this.options.headers, ...options?.headers },
362
+ response_format: responseFormat, // Pass to provider
363
+ requestTimeout: options?.requestTimeout ?? this.options.requestTimeout ?? config.requestTimeout,
364
+ thinking: options?.thinking ?? this.options.thinking,
365
+ signal: options?.signal,
366
+ ...this.options.params
367
+ };
368
+ // --- Content Policy Hooks (Input) ---
369
+ if (this.options.onBeforeRequest) {
370
+ const result = await this.options.onBeforeRequest(executeOptions.messages);
371
+ if (result) {
372
+ executeOptions.messages = result;
381
373
  }
382
374
  }
383
- };
384
- // First round
385
- if (this.options.onNewMessage)
386
- this.options.onNewMessage();
387
- let response = await this.executor.executeChat(executeOptions);
388
- trackUsage(response.usage);
389
- let assistantMessage = new ChatResponseString(response.content ?? "", response.usage ?? { input_tokens: 0, output_tokens: 0, total_tokens: 0 }, this.model, this.provider.id, response.thinking, response.reasoning, response.tool_calls, response.finish_reason, this.options.schema);
390
- // --- Content Policy Hooks (Output - Turn 1) ---
391
- if (this.options.onAfterResponse) {
392
- const result = await this.options.onAfterResponse(assistantMessage);
393
- if (result) {
394
- assistantMessage = result;
395
- }
396
- }
397
- this.messages.push({
398
- role: "assistant",
399
- content: assistantMessage?.toString() || null,
400
- tool_calls: response.tool_calls,
401
- usage: response.usage
402
- });
403
- if (this.options.onEndMessage && (!response.tool_calls || response.tool_calls.length === 0)) {
404
- this.options.onEndMessage(assistantMessage);
405
- }
406
- const maxToolCalls = options?.maxToolCalls ?? this.options.maxToolCalls ?? 5;
407
- let stepCount = 0;
408
- while (response.tool_calls && response.tool_calls.length > 0) {
409
- // Dry-run mode: stop after proposing tools
410
- if (!ToolHandler.shouldExecuteTools(response.tool_calls, this.options.toolExecution)) {
411
- break;
412
- }
413
- stepCount++;
414
- if (stepCount > maxToolCalls) {
415
- throw new Error(`[NodeLLM] Maximum tool execution calls (${maxToolCalls}) exceeded.`);
416
- }
417
- for (const toolCall of response.tool_calls) {
418
- // Human-in-the-loop: check for approval
419
- if (this.options.toolExecution === ToolExecutionMode.CONFIRM) {
420
- const approved = await ToolHandler.requestToolConfirmation(toolCall, this.options.onConfirmToolCall);
421
- if (!approved) {
422
- this.messages.push(this.provider.formatToolResultMessage(toolCall.id, "Action cancelled by user."));
423
- continue;
375
+ const totalUsage = { input_tokens: 0, output_tokens: 0, total_tokens: 0 };
376
+ const trackUsage = (u) => {
377
+ if (u) {
378
+ // Fallback cost calculation if provider didn't return it
379
+ if (u.cost === undefined) {
380
+ const withCost = ModelRegistry.calculateCost(u, this.model, this.provider.id);
381
+ u.cost = withCost.cost;
382
+ u.input_cost = withCost.input_cost;
383
+ u.output_cost = withCost.output_cost;
424
384
  }
425
- }
426
- try {
427
- const toolResult = await ToolHandler.execute(toolCall, this.options.tools, this.options.onToolCallStart, this.options.onToolCallEnd);
428
- this.messages.push(this.provider.formatToolResultMessage(toolResult.tool_call_id, toolResult.content));
429
- }
430
- catch (error) {
431
- let currentError = error;
432
- const directive = await this.options.onToolCallError?.(toolCall, currentError);
433
- if (directive === "STOP") {
434
- throw currentError;
385
+ totalUsage.input_tokens += u.input_tokens;
386
+ totalUsage.output_tokens += u.output_tokens;
387
+ totalUsage.total_tokens += u.total_tokens;
388
+ if (u.cached_tokens) {
389
+ totalUsage.cached_tokens = (totalUsage.cached_tokens ?? 0) + u.cached_tokens;
435
390
  }
436
- if (directive === "RETRY") {
437
- try {
438
- const toolResult = await ToolHandler.execute(toolCall, this.options.tools, this.options.onToolCallStart, this.options.onToolCallEnd);
439
- this.messages.push(this.provider.formatToolResultMessage(toolResult.tool_call_id, toolResult.content));
440
- continue;
441
- }
442
- catch (retryError) {
443
- // If retry also fails, fall through to default logic
444
- currentError = retryError;
445
- }
391
+ if (u.cost !== undefined) {
392
+ totalUsage.cost = (totalUsage.cost ?? 0) + u.cost;
446
393
  }
447
- this.messages.push(this.provider.formatToolResultMessage(toolCall.id, `Fatal error executing tool '${toolCall.function.name}': ${currentError.message}`, { isError: true }));
448
- if (directive === "CONTINUE") {
449
- continue;
394
+ if (u.input_cost !== undefined) {
395
+ totalUsage.input_cost = (totalUsage.input_cost ?? 0) + u.input_cost;
450
396
  }
451
- // Default short-circuit logic
452
- const errorObj = currentError;
453
- const isFatal = errorObj.fatal === true || errorObj.status === 401 || errorObj.status === 403;
454
- if (isFatal) {
455
- throw currentError;
397
+ if (u.output_cost !== undefined) {
398
+ totalUsage.output_cost = (totalUsage.output_cost ?? 0) + u.output_cost;
456
399
  }
457
- logger.error(`Tool execution failed for '${toolCall.function.name}':`, currentError);
458
400
  }
459
- }
460
- response = await this.executor.executeChat({
461
- model: this.model,
462
- messages: [...this.systemMessages, ...this.messages],
463
- tools: this.options.tools,
464
- temperature: options?.temperature ?? this.options.temperature,
465
- max_tokens: options?.maxTokens ?? this.options.maxTokens ?? config.maxTokens,
466
- headers: this.options.headers,
467
- response_format: responseFormat,
468
- requestTimeout: options?.requestTimeout ?? this.options.requestTimeout ?? config.requestTimeout,
469
- signal: options?.signal,
470
- ...this.options.params
471
- });
401
+ };
402
+ // First round
403
+ if (this.options.onNewMessage)
404
+ this.options.onNewMessage();
405
+ let response = await this.executor.executeChat(executeOptions);
472
406
  trackUsage(response.usage);
473
- assistantMessage = new ChatResponseString(response.content ?? "", response.usage ?? { input_tokens: 0, output_tokens: 0, total_tokens: 0 }, this.model, this.provider.id, response.thinking, response.reasoning, response.tool_calls, response.finish_reason, this.options.schema);
474
- // --- Content Policy Hooks (Output - Tool Turns) ---
407
+ let assistantMessage = new ChatResponseString(response.content ?? "", response.usage ?? { input_tokens: 0, output_tokens: 0, total_tokens: 0 }, this.model, this.provider.id, response.thinking, response.reasoning, response.tool_calls, response.finish_reason, this.options.schema);
408
+ // --- Content Policy Hooks (Output - Turn 1) ---
475
409
  if (this.options.onAfterResponse) {
476
410
  const result = await this.options.onAfterResponse(assistantMessage);
477
411
  if (result) {
@@ -487,10 +421,114 @@ export class Chat {
487
421
  if (this.options.onEndMessage && (!response.tool_calls || response.tool_calls.length === 0)) {
488
422
  this.options.onEndMessage(assistantMessage);
489
423
  }
424
+ const maxToolCalls = options?.maxToolCalls ?? this.options.maxToolCalls ?? 5;
425
+ let stepCount = 0;
426
+ while (response.tool_calls && response.tool_calls.length > 0) {
427
+ // Dry-run mode: stop after proposing tools
428
+ if (!ToolHandler.shouldExecuteTools(response.tool_calls, this.options.toolExecution)) {
429
+ break;
430
+ }
431
+ stepCount++;
432
+ if (stepCount > maxToolCalls) {
433
+ throw new Error(`[NodeLLM] Maximum tool execution calls (${maxToolCalls}) exceeded.`);
434
+ }
435
+ for (const toolCall of response.tool_calls) {
436
+ // Human-in-the-loop: check for approval
437
+ if (this.options.toolExecution === ToolExecutionMode.CONFIRM) {
438
+ const approved = await ToolHandler.requestToolConfirmation(toolCall, this.options.onConfirmToolCall);
439
+ if (!approved) {
440
+ this.messages.push(this.provider.formatToolResultMessage(toolCall.id, "Action cancelled by user."));
441
+ continue;
442
+ }
443
+ }
444
+ // 2. onToolCallStart Hook
445
+ await runMiddleware(this.middlewares, "onToolCallStart", context, toolCall);
446
+ try {
447
+ const toolResult = await ToolHandler.execute(toolCall, this.options.tools, this.options.onToolCallStart, this.options.onToolCallEnd);
448
+ // 3. onToolCallEnd Hook
449
+ await runMiddleware(this.middlewares, "onToolCallEnd", context, toolCall, toolResult.content);
450
+ this.messages.push(this.provider.formatToolResultMessage(toolResult.tool_call_id, toolResult.content));
451
+ }
452
+ catch (error) {
453
+ let currentError = error;
454
+ // 4. onToolCallError Hook
455
+ const middlewareDirective = await runMiddleware(this.middlewares, "onToolCallError", context, toolCall, currentError);
456
+ const directive = middlewareDirective ||
457
+ (await this.options.onToolCallError?.(toolCall, currentError));
458
+ if (directive === "STOP") {
459
+ throw currentError;
460
+ }
461
+ if (directive === "RETRY") {
462
+ // ... retry logic (simplified: recurse or duplicate logic? adhering to original logic)
463
+ // Original logic duplicated the execution block. For brevity in this replacement, I'll simplified retry to "try once more"
464
+ try {
465
+ // Retry Hook? Maybe skip start hook on retry or re-run?
466
+ // Let's assume onToolCallStart fires again for cleanliness?
467
+ // Or just execute directly to match existing behavior.
468
+ // Existing logs show we just call ToolHandler.execute again.
469
+ const toolResult = await ToolHandler.execute(toolCall, this.options.tools, this.options.onToolCallStart, this.options.onToolCallEnd);
470
+ await runMiddleware(this.middlewares, "onToolCallEnd", context, toolCall, toolResult.content);
471
+ this.messages.push(this.provider.formatToolResultMessage(toolResult.tool_call_id, toolResult.content));
472
+ continue;
473
+ }
474
+ catch (retryError) {
475
+ currentError = retryError;
476
+ await runMiddleware(this.middlewares, "onToolCallError", context, toolCall, currentError);
477
+ }
478
+ }
479
+ this.messages.push(this.provider.formatToolResultMessage(toolCall.id, `Fatal error executing tool '${toolCall.function.name}': ${currentError.message}`, { isError: true }));
480
+ if (directive === "CONTINUE") {
481
+ continue;
482
+ }
483
+ // Default short-circuit logic
484
+ const errorObj = currentError;
485
+ const isFatal = errorObj.fatal === true || errorObj.status === 401 || errorObj.status === 403;
486
+ if (isFatal) {
487
+ throw currentError;
488
+ }
489
+ logger.error(`Tool execution failed for '${toolCall.function.name}':`, currentError);
490
+ }
491
+ }
492
+ response = await this.executor.executeChat({
493
+ model: this.model,
494
+ messages: [...this.systemMessages, ...this.messages], // Use updated history
495
+ tools: this.options.tools,
496
+ temperature: options?.temperature ?? this.options.temperature,
497
+ max_tokens: options?.maxTokens ?? this.options.maxTokens ?? config.maxTokens,
498
+ headers: this.options.headers,
499
+ response_format: responseFormat, // Pass to provider
500
+ requestTimeout: options?.requestTimeout ?? this.options.requestTimeout ?? config.requestTimeout,
501
+ signal: options?.signal,
502
+ ...this.options.params
503
+ });
504
+ trackUsage(response.usage);
505
+ assistantMessage = new ChatResponseString(response.content ?? "", response.usage ?? { input_tokens: 0, output_tokens: 0, total_tokens: 0 }, this.model, this.provider.id, response.thinking, response.reasoning, response.tool_calls, response.finish_reason, this.options.schema);
506
+ if (this.options.onAfterResponse) {
507
+ const result = await this.options.onAfterResponse(assistantMessage);
508
+ if (result)
509
+ assistantMessage = result;
510
+ }
511
+ this.messages.push({
512
+ role: "assistant",
513
+ content: assistantMessage?.toString() || null,
514
+ tool_calls: response.tool_calls,
515
+ usage: response.usage
516
+ });
517
+ if (this.options.onEndMessage &&
518
+ (!response.tool_calls || response.tool_calls.length === 0)) {
519
+ this.options.onEndMessage(assistantMessage);
520
+ }
521
+ }
522
+ const finalResponse = new ChatResponseString(assistantMessage.toString() || "", totalUsage, this.model, this.provider.id, assistantMessage.thinking, assistantMessage.reasoning, response.tool_calls, assistantMessage.finish_reason, this.options.schema);
523
+ // 5. onResponse Hook
524
+ await runMiddleware(this.middlewares, "onResponse", context, finalResponse);
525
+ return finalResponse;
526
+ }
527
+ catch (err) {
528
+ // 6. onError Hook
529
+ await runMiddleware(this.middlewares, "onError", context, err);
530
+ throw err;
490
531
  }
491
- // For the final return, we might want to aggregate reasoning too if it happened in multiple turns?
492
- // Usually reasoning only happens once or we just want the last one.
493
- return new ChatResponseString(assistantMessage.toString() || "", totalUsage, this.model, this.provider.id, assistantMessage.thinking, assistantMessage.reasoning, response.tool_calls, assistantMessage.finish_reason, this.options.schema);
494
532
  }
495
533
  /**
496
534
  * Streams the model's response to a user question.
@@ -1,3 +1,4 @@
1
+ import { Middleware } from "../types/Middleware.js";
1
2
  import { Message } from "./Message.js";
2
3
  import { ToolResolvable } from "./Tool.js";
3
4
  import { Schema } from "../schema/Schema.js";
@@ -6,6 +7,7 @@ import { ToolExecutionMode } from "../constants.js";
6
7
  import { ResponseFormat, ThinkingConfig } from "../providers/Provider.js";
7
8
  export interface ChatOptions {
8
9
  systemPrompt?: string;
10
+ middlewares?: Middleware[];
9
11
  messages?: Message[];
10
12
  tools?: ToolResolvable[];
11
13
  temperature?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"ChatOptions.d.ts","sourceRoot":"","sources":["../../src/chat/ChatOptions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAE1E,MAAM,WAAW,WAAW;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC;IACrB,KAAK,CAAC,EAAE,cAAc,EAAE,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,kBAAkB,KAAK,IAAI,CAAC;IACrD,eAAe,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;IAC9C,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAC7D,eAAe,CAAC,EAAE,CAChB,QAAQ,EAAE,OAAO,EACjB,KAAK,EAAE,KAAK,KACT,MAAM,GAAG,UAAU,GAAG,OAAO,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,UAAU,GAAG,OAAO,GAAG,IAAI,CAAC,CAAC;IAC1F,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,aAAa,CAAC,EAAE,iBAAiB,CAAC;IAClC,iBAAiB,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;IACtE,eAAe,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,CAAC;IACrE,eAAe,CAAC,EAAE,CAAC,QAAQ,EAAE,kBAAkB,KAAK,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAC;CACxF"}
1
+ {"version":3,"file":"ChatOptions.d.ts","sourceRoot":"","sources":["../../src/chat/ChatOptions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAE1E,MAAM,WAAW,WAAW;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;IAC3B,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC;IACrB,KAAK,CAAC,EAAE,cAAc,EAAE,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,kBAAkB,KAAK,IAAI,CAAC;IACrD,eAAe,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;IAC9C,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAC7D,eAAe,CAAC,EAAE,CAChB,QAAQ,EAAE,OAAO,EACjB,KAAK,EAAE,KAAK,KACT,MAAM,GAAG,UAAU,GAAG,OAAO,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,UAAU,GAAG,OAAO,GAAG,IAAI,CAAC,CAAC;IAC1F,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,aAAa,CAAC,EAAE,iBAAiB,CAAC;IAClC,iBAAiB,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;IACtE,eAAe,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,CAAC;IACrE,eAAe,CAAC,EAAE,CAAC,QAAQ,EAAE,kBAAkB,KAAK,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAC;CACxF"}
@@ -1 +1 @@
1
- {"version":3,"file":"ChatStream.d.ts","sourceRoot":"","sources":["../../src/chat/ChatStream.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EACL,WAAW,EAIZ,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAyB,MAAM,0BAA0B,CAAC;AAEtF,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAGhD,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAUvC;;;GAGG;AACH,qBAAa,UAAU;IAKnB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAN1B,OAAO,CAAC,QAAQ,CAAY;IAC5B,OAAO,CAAC,cAAc,CAAY;gBAGf,QAAQ,EAAE,QAAQ,EAClB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,WAAgB,EAC1C,QAAQ,CAAC,EAAE,OAAO,EAAE,EACpB,cAAc,CAAC,EAAE,OAAO,EAAE;IA6B5B,IAAI,OAAO,IAAI,SAAS,OAAO,EAAE,CAEhC;IAED,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,EAAE,EAAE,OAAO,GAAE,UAAe,GAAG,MAAM,CAAC,SAAS,CAAC;CA0RrF"}
1
+ {"version":3,"file":"ChatStream.d.ts","sourceRoot":"","sources":["../../src/chat/ChatStream.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EACL,WAAW,EAIZ,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAyB,MAAM,0BAA0B,CAAC;AAEtF,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAGhD,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAavC;;;GAGG;AACH,qBAAa,UAAU;IAKnB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAN1B,OAAO,CAAC,QAAQ,CAAY;IAC5B,OAAO,CAAC,cAAc,CAAY;gBAGf,QAAQ,EAAE,QAAQ,EAClB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,WAAgB,EAC1C,QAAQ,CAAC,EAAE,OAAO,EAAE,EACpB,cAAc,CAAC,EAAE,OAAO,EAAE;IA6B5B,IAAI,OAAO,IAAI,SAAS,OAAO,EAAE,CAEhC;IAED,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,EAAE,EAAE,OAAO,GAAE,UAAe,GAAG,MAAM,CAAC,SAAS,CAAC;CA8WrF"}
@@ -9,6 +9,8 @@ import { ChatValidator } from "./Validation.js";
9
9
  import { ToolHandler } from "./ToolHandler.js";
10
10
  import { logger } from "../utils/logger.js";
11
11
  import { ModelRegistry } from "../models/ModelRegistry.js";
12
+ import { runMiddleware } from "../utils/middleware-runner.js";
13
+ import { randomUUID } from "node:crypto";
12
14
  /**
13
15
  * Internal handler for chat streaming logic.
14
16
  * Wraps the provider's stream with side effects like history updates and events.
@@ -58,6 +60,9 @@ export class ChatStream {
58
60
  ...requestOptions,
59
61
  headers: { ...baseOptions.headers, ...requestOptions.headers }
60
62
  };
63
+ const requestId = randomUUID();
64
+ const state = {};
65
+ const middlewares = options.middlewares || [];
61
66
  // Process Multimodal Content
62
67
  let messageContent = content;
63
68
  const files = [...(requestOptions.images ?? []), ...(requestOptions.files ?? [])];
@@ -71,63 +76,73 @@ export class ChatStream {
71
76
  ChatValidator.validateTools(provider, model, true, options);
72
77
  }
73
78
  messages.push({ role: "user", content: messageContent });
74
- if (!provider.stream) {
75
- throw new Error("Streaming not supported by provider");
76
- }
77
- // Process Schema/Structured Output
78
- let responseFormat = options.responseFormat;
79
- if (!responseFormat && options.schema) {
80
- ChatValidator.validateStructuredOutput(provider, model, true, options);
81
- const jsonSchema = toJsonSchema(options.schema.definition.schema);
82
- responseFormat = {
83
- type: "json_schema",
84
- json_schema: {
85
- name: options.schema.definition.name,
86
- description: options.schema.definition.description,
87
- strict: options.schema.definition.strict ?? true,
88
- schema: jsonSchema
79
+ // Prepare Middleware Context
80
+ const context = {
81
+ requestId,
82
+ provider: provider.id,
83
+ model: model,
84
+ messages: [...systemMessages, ...messages],
85
+ options: options,
86
+ state
87
+ };
88
+ try {
89
+ // 1. onRequest Hook
90
+ await runMiddleware(middlewares, "onRequest", context);
91
+ if (!provider.stream) {
92
+ throw new Error("Streaming not supported by provider");
93
+ }
94
+ // Process Schema/Structured Output
95
+ let responseFormat = options.responseFormat;
96
+ if (!responseFormat && options.schema) {
97
+ ChatValidator.validateStructuredOutput(provider, model, true, options);
98
+ const jsonSchema = toJsonSchema(options.schema.definition.schema);
99
+ responseFormat = {
100
+ type: "json_schema",
101
+ json_schema: {
102
+ name: options.schema.definition.name,
103
+ description: options.schema.definition.description,
104
+ strict: options.schema.definition.strict ?? true,
105
+ schema: jsonSchema
106
+ }
107
+ };
108
+ }
109
+ let isFirst = true;
110
+ const maxToolCalls = options.maxToolCalls ?? 5;
111
+ let stepCount = 0;
112
+ const totalUsage = { input_tokens: 0, output_tokens: 0, total_tokens: 0 };
113
+ const trackUsage = (u) => {
114
+ if (u) {
115
+ // Fallback cost calculation if provider didn't return it
116
+ if (u.cost === undefined) {
117
+ const withCost = ModelRegistry.calculateCost(u, model, provider.id);
118
+ u.cost = withCost.cost;
119
+ u.input_cost = withCost.input_cost;
120
+ u.output_cost = withCost.output_cost;
121
+ }
122
+ totalUsage.input_tokens += u.input_tokens;
123
+ totalUsage.output_tokens += u.output_tokens;
124
+ totalUsage.total_tokens += u.total_tokens;
125
+ if (u.cached_tokens) {
126
+ totalUsage.cached_tokens = (totalUsage.cached_tokens ?? 0) + u.cached_tokens;
127
+ }
128
+ if (u.cost !== undefined) {
129
+ totalUsage.cost = (totalUsage.cost ?? 0) + u.cost;
130
+ }
89
131
  }
90
132
  };
91
- }
92
- if (!provider.stream) {
93
- throw new Error("Streaming not supported by provider");
94
- }
95
- let isFirst = true;
96
- const maxToolCalls = options.maxToolCalls ?? 5;
97
- let stepCount = 0;
98
- const totalUsage = { input_tokens: 0, output_tokens: 0, total_tokens: 0 };
99
- const trackUsage = (u) => {
100
- if (u) {
101
- // Fallback cost calculation if provider didn't return it
102
- if (u.cost === undefined) {
103
- const withCost = ModelRegistry.calculateCost(u, model, provider.id);
104
- u.cost = withCost.cost;
105
- u.input_cost = withCost.input_cost;
106
- u.output_cost = withCost.output_cost;
133
+ let assistantResponse;
134
+ while (true) {
135
+ stepCount++;
136
+ if (stepCount > maxToolCalls) {
137
+ throw new Error(`[NodeLLM] Maximum tool execution calls (${maxToolCalls}) exceeded during streaming.`);
107
138
  }
108
- totalUsage.input_tokens += u.input_tokens;
109
- totalUsage.output_tokens += u.output_tokens;
110
- totalUsage.total_tokens += u.total_tokens;
111
- if (u.cached_tokens) {
112
- totalUsage.cached_tokens = (totalUsage.cached_tokens ?? 0) + u.cached_tokens;
113
- }
114
- if (u.cost !== undefined) {
115
- totalUsage.cost = (totalUsage.cost ?? 0) + u.cost;
116
- }
117
- }
118
- };
119
- while (true) {
120
- stepCount++;
121
- if (stepCount > maxToolCalls) {
122
- throw new Error(`[NodeLLM] Maximum tool execution calls (${maxToolCalls}) exceeded during streaming.`);
123
- }
124
- let fullContent = "";
125
- let fullReasoning = "";
126
- const thinking = { text: "" };
127
- let toolCalls;
128
- let currentTurnUsage;
129
- try {
130
- let requestMessages = [...systemMessages, ...messages];
139
+ let fullContent = "";
140
+ let fullReasoning = "";
141
+ const thinking = { text: "" };
142
+ let toolCalls;
143
+ let currentTurnUsage;
144
+ context.messages = [...systemMessages, ...messages];
145
+ let requestMessages = context.messages; // Use up-to-date messages from context
131
146
  if (options.onBeforeRequest) {
132
147
  const result = await options.onBeforeRequest(requestMessages);
133
148
  if (result) {
@@ -180,7 +195,7 @@ export class ChatStream {
180
195
  trackUsage(currentTurnUsage);
181
196
  }
182
197
  }
183
- let assistantResponse = new ChatResponseString(fullContent || "", currentTurnUsage || { input_tokens: 0, output_tokens: 0, total_tokens: 0 }, model, provider.id, thinking.text || thinking.signature ? thinking : undefined, fullReasoning || undefined, toolCalls, undefined, // finish_reason
198
+ assistantResponse = new ChatResponseString(fullContent || "", currentTurnUsage || { input_tokens: 0, output_tokens: 0, total_tokens: 0 }, model, provider.id, thinking.text || thinking.signature ? thinking : undefined, fullReasoning || undefined, toolCalls, undefined, // finish_reason
184
199
  options.schema);
185
200
  if (options.onAfterResponse) {
186
201
  const result = await options.onAfterResponse(assistantResponse);
@@ -212,34 +227,62 @@ export class ChatStream {
212
227
  continue;
213
228
  }
214
229
  }
230
+ // 2. onToolCallStart Hook
231
+ await runMiddleware(middlewares, "onToolCallStart", context, toolCall);
215
232
  try {
216
233
  const toolResult = await ToolHandler.execute(toolCall, options.tools, options.onToolCallStart, options.onToolCallEnd);
234
+ // 3. onToolCallEnd Hook
235
+ await runMiddleware(middlewares, "onToolCallEnd", context, toolCall, toolResult.content);
217
236
  messages.push(provider.formatToolResultMessage(toolResult.tool_call_id, toolResult.content));
218
237
  }
219
238
  catch (error) {
220
- const err = error;
221
- const directive = await options.onToolCallError?.(toolCall, err);
239
+ let currentError = error;
240
+ // 4. onToolCallError Hook
241
+ const middlewareDirective = await runMiddleware(middlewares, "onToolCallError", context, toolCall, currentError);
242
+ const directive = middlewareDirective || (await options.onToolCallError?.(toolCall, currentError));
222
243
  if (directive === "STOP") {
223
244
  throw error;
224
245
  }
225
- messages.push(provider.formatToolResultMessage(toolCall.id, `Fatal error executing tool '${toolCall.function.name}': ${err.message}`, { isError: true }));
246
+ if (directive === "RETRY") {
247
+ try {
248
+ const toolResult = await ToolHandler.execute(toolCall, options.tools, options.onToolCallStart, options.onToolCallEnd);
249
+ await runMiddleware(middlewares, "onToolCallEnd", context, toolCall, toolResult.content);
250
+ messages.push(provider.formatToolResultMessage(toolResult.tool_call_id, toolResult.content));
251
+ continue;
252
+ }
253
+ catch (retryError) {
254
+ currentError = retryError;
255
+ await runMiddleware(middlewares, "onToolCallError", context, toolCall, currentError);
256
+ }
257
+ }
258
+ messages.push(provider.formatToolResultMessage(toolCall.id, `Fatal error executing tool '${toolCall.function.name}': ${currentError.message}`, { isError: true }));
226
259
  if (directive === "CONTINUE") {
227
260
  continue;
228
261
  }
229
- const isFatal = err.fatal === true || err.status === 401 || err.status === 403;
262
+ const isFatal = currentError.fatal === true ||
263
+ currentError.status === 401 ||
264
+ currentError.status === 403;
230
265
  if (isFatal) {
231
- throw err;
266
+ throw currentError;
232
267
  }
233
- logger.error(`Tool execution failed for '${toolCall.function.name}':`, error);
268
+ logger.error(`Tool execution failed for '${toolCall.function.name}':`, currentError);
234
269
  }
235
270
  }
271
+ // Loop continues -> streaming next chunk
236
272
  }
237
- catch (error) {
238
- if (error instanceof Error && error.name === "AbortError") {
239
- // Aborted
240
- }
241
- throw error;
273
+ // 5. onResponse Hook
274
+ if (assistantResponse) {
275
+ await runMiddleware(middlewares, "onResponse", context, assistantResponse);
276
+ }
277
+ }
278
+ catch (err) {
279
+ // 6. onError Hook
280
+ await runMiddleware(middlewares, "onError", context, err);
281
+ if (err instanceof Error && err.name === "AbortError") {
282
+ // Aborted, still maybe want onError? Middleware logic says "onError".
283
+ // But rethrow for sure.
242
284
  }
285
+ throw err;
243
286
  }
244
287
  };
245
288
  return new Stream(() => sideEffectGenerator(this, this.provider, this.model, this.messages, this.systemMessages, this.options, controller, content, options), controller);
package/dist/index.d.ts CHANGED
@@ -6,6 +6,9 @@ export * from "./chat/ChatResponse.js";
6
6
  export * from "./chat/Chat.js";
7
7
  export * from "./chat/ChatStream.js";
8
8
  export * from "./streaming/Stream.js";
9
+ export * from "./errors/index.js";
10
+ export type { Middleware, MiddlewareContext } from "./types/Middleware.js";
11
+ export * from "./middlewares/index.js";
9
12
  export { z } from "zod";
10
13
  export { NodeLLM, LegacyNodeLLM, createLLM, NodeLLMCore, Transcription, Moderation, Embedding, ModelRegistry, PricingRegistry } from "./llm.js";
11
14
  export { config } from "./config.js";