@devilsdev/rag-pipeline-utils 2.3.1 → 2.4.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 (160) hide show
  1. package/README.md +293 -369
  2. package/contracts/loader-contract.json +11 -6
  3. package/contracts/reranker-contract.json +18 -26
  4. package/dist/index.cjs +21190 -47
  5. package/dist/index.d.ts +298 -68
  6. package/dist/index.mjs +9 -76
  7. package/package.json +20 -8
  8. package/src/agentic/agentic-pipeline.js +235 -0
  9. package/src/agentic/iterative-retriever.js +213 -0
  10. package/src/agentic/query-planner.js +208 -0
  11. package/src/agentic/self-critique.js +102 -0
  12. package/src/ai/adaptive-retrieval.js +0 -3
  13. package/src/ai/federated-learning.js +0 -3
  14. package/src/ai/federation.js +1 -6
  15. package/src/ai/index.js +9 -20
  16. package/src/ai/model-training.js +0 -3
  17. package/src/ai/multimodal-processing.js +0 -3
  18. package/src/ai/multimodal.js +1 -6
  19. package/src/ai/orchestrator.js +1 -6
  20. package/src/ai/retrieval-engine.js +1 -6
  21. package/src/chunking/chunking-engine.js +142 -0
  22. package/src/chunking/strategies/fixed-size.js +54 -0
  23. package/src/chunking/strategies/recursive.js +127 -0
  24. package/src/chunking/strategies/semantic.js +128 -0
  25. package/src/chunking/strategies/sentence.js +32 -0
  26. package/src/chunking/strategies/structure-aware.js +138 -0
  27. package/src/citation/citation-tracker.js +161 -0
  28. package/src/citation/hallucination-detector.js +74 -0
  29. package/src/citation/source-mapper.js +95 -0
  30. package/src/cli/commands/ai-ml/adaptive-commands.js +178 -0
  31. package/src/cli/commands/ai-ml/federated-commands.js +207 -0
  32. package/src/cli/commands/ai-ml/index.js +17 -0
  33. package/src/cli/commands/ai-ml/multimodal-commands.js +187 -0
  34. package/src/cli/commands/ai-ml/shared.js +51 -0
  35. package/src/cli/commands/ai-ml/training-commands.js +228 -0
  36. package/src/cli/commands/ai-ml.js +176 -757
  37. package/src/cli/commands/plugin-hub/certification-commands.js +221 -0
  38. package/src/cli/commands/plugin-hub/event-handlers.js +64 -0
  39. package/src/cli/commands/plugin-hub/hub-commands.js +290 -0
  40. package/src/cli/commands/plugin-hub/review-commands.js +78 -0
  41. package/src/cli/commands/plugin-hub.js +106 -782
  42. package/src/cli/diagnostics/config-checker.js +81 -0
  43. package/src/cli/diagnostics/dependency-checker.js +69 -0
  44. package/src/cli/diagnostics/environment-checker.js +45 -0
  45. package/src/cli/diagnostics/performance-checker.js +51 -0
  46. package/src/cli/diagnostics/plugin-checker.js +106 -0
  47. package/src/cli/diagnostics/security-checker.js +61 -0
  48. package/src/cli/doctor-command.js +70 -369
  49. package/src/cli/enhanced-cli-commands.js +162 -706
  50. package/src/cli/handlers/config-handler.js +126 -0
  51. package/src/cli/handlers/diagnostics-handler.js +113 -0
  52. package/src/cli/handlers/ingest-handler.js +42 -0
  53. package/src/cli/handlers/init-handler.js +51 -0
  54. package/src/cli/handlers/query-handler.js +45 -0
  55. package/src/cli/handlers/run-handler.js +181 -0
  56. package/src/cli/interactive-wizard.js +99 -723
  57. package/src/cli/shared/cli-utils.js +89 -0
  58. package/src/cli/wizard/configuration.js +148 -0
  59. package/src/cli/wizard/observability.js +79 -0
  60. package/src/cli/wizard/performance.js +76 -0
  61. package/src/cli/wizard/plugin-selection.js +297 -0
  62. package/src/cli/wizard/preview-save.js +172 -0
  63. package/src/cli/wizard/project-setup.js +66 -0
  64. package/src/config/memory-optimization.js +3 -3
  65. package/src/connectors/anthropic-connector.js +179 -0
  66. package/src/connectors/base-connector.js +94 -0
  67. package/src/connectors/cohere-connector.js +143 -0
  68. package/src/connectors/local-embedder.js +171 -0
  69. package/src/connectors/memory-retriever.js +150 -0
  70. package/src/connectors/ollama-connector.js +205 -0
  71. package/src/connectors/openai-connector.js +173 -0
  72. package/src/core/create-pipeline.js +21 -1
  73. package/src/core/observability/tracing.js +2 -1
  74. package/src/core/pipeline-factory.js +0 -1
  75. package/src/core/plugin-registry.js +14 -14
  76. package/src/cost/cost-calculator.js +146 -0
  77. package/src/cost/cost-tracker.js +260 -0
  78. package/src/cost/token-budget.js +245 -0
  79. package/src/dag/core/execution-scheduler.js +2 -1
  80. package/src/dag/core/topology-validation.js +0 -3
  81. package/src/dag/dag-engine.js +3 -5
  82. package/src/dag/dag-node.js +0 -1
  83. package/src/dag/enhanced-dag-engine.js +171 -99
  84. package/src/dashboard/dashboard-generator.js +642 -0
  85. package/src/dashboard/metrics-aggregator.js +409 -0
  86. package/src/debug/execution-tracer.js +210 -0
  87. package/src/debug/trace-inspector.js +281 -0
  88. package/src/dev/dev-server.js +5 -5
  89. package/src/dev/hot-reload.js +3 -2
  90. package/src/dx/integration-templates.js +0 -3
  91. package/src/ecosystem/plugin-analytics-dashboard.js +2 -2
  92. package/src/ecosystem/plugin-hub.js +2 -2
  93. package/src/enterprise/audit-logging.js +2 -2
  94. package/src/enterprise/sso-integration.js +52 -32
  95. package/src/evaluate/context-metrics.js +127 -0
  96. package/src/evaluate/evaluator.js +4 -2
  97. package/src/evaluate/faithfulness.js +117 -0
  98. package/src/evaluate/groundedness.js +60 -0
  99. package/src/evaluate/pipeline-evaluator.js +156 -0
  100. package/src/evaluate/relevance.js +129 -0
  101. package/src/evaluate/scoring.js +27 -0
  102. package/src/graph/entity-extractor.js +183 -0
  103. package/src/graph/graph-index.js +113 -0
  104. package/src/graph/graph-retriever.js +244 -0
  105. package/src/graph/knowledge-graph.js +256 -0
  106. package/src/guardrails/guardrails-pipeline.js +136 -0
  107. package/src/guardrails/post-generation-guard.js +170 -0
  108. package/src/guardrails/pre-retrieval-guard.js +161 -0
  109. package/src/guardrails/retrieval-guard.js +126 -0
  110. package/src/index.js +255 -9
  111. package/src/loader/html-loader.js +4 -15
  112. package/src/loader/markdown-loader.js +4 -15
  113. package/src/mcp/mcp-server.js +199 -0
  114. package/src/mcp/mcp-tool-builder.js +121 -0
  115. package/src/observability/audit-logger.js +0 -2
  116. package/src/observability/event-logger.js +2 -1
  117. package/src/observability/security-monitor.js +0 -2
  118. package/src/observability/telemetry.js +0 -2
  119. package/src/reranker/cascade-reranker.js +79 -0
  120. package/src/reranker/embedding-reranker.js +124 -0
  121. package/src/reranker/llm-reranker.js +6 -3
  122. package/src/reranker/scoring-reranker.js +123 -0
  123. package/src/retrieval/bm25-search.js +157 -0
  124. package/src/retrieval/hybrid-retriever.js +111 -0
  125. package/src/retrieval/rank-fusion.js +62 -0
  126. package/src/security/jwt-config.js +4 -3
  127. package/src/security/jwt-validator.js +2 -1
  128. package/src/security/plugin-sandbox.js +0 -3
  129. package/src/security/plugin-signature-verifier.js +4 -6
  130. package/src/security/validator.js +0 -2
  131. package/src/streaming/embedding-stream.js +197 -0
  132. package/src/streaming/sse-adapter.js +168 -0
  133. package/src/streaming/stream-router.js +210 -0
  134. package/src/streaming/streaming-embedder.js +231 -0
  135. package/src/streaming/websocket-adapter.js +171 -0
  136. package/src/templates/scaffold.js +147 -0
  137. package/src/templates/template-registry.js +272 -0
  138. package/src/utils/contract-validator.js +2 -1
  139. package/src/utils/input-sanitizer.js +80 -99
  140. package/src/utils/performance-monitor.js +0 -3
  141. package/src/utils/plugin-scaffolder.js +9 -16
  142. package/src/utils/rate-limiter.js +0 -1
  143. package/src/utils/retry-policy.js +0 -3
  144. package/src/utils/safe-expression-evaluator.js +0 -3
  145. package/src/utils/secure-logger-config.js +2 -4
  146. package/src/utils/secure-logging.js +0 -3
  147. package/src/utils/structured-logger.js +0 -3
  148. package/src/ai/index-fixed.js +0 -509
  149. package/src/ai/module-template.js +0 -37
  150. package/src/ci/workflow-analyzer.js +0 -71
  151. package/src/cli/plugin-marketplace-commands.js +0 -869
  152. package/src/dev/README.md +0 -126
  153. package/src/dev/examples/hot-reload-usage.js +0 -355
  154. package/src/mocks/openai-embedder.js +0 -119
  155. package/src/mocks/openai-llm.js +0 -77
  156. package/src/mocks/pdf-loader.js +0 -21
  157. package/src/mocks/pinecone-retriever.js +0 -106
  158. package/src/scripts/script-utilities.js +0 -36
  159. package/src/utils/ci/diagnostic-reporter.js +0 -57
  160. package/src/utils/examples/error-formatter-usage.js +0 -351
@@ -85,7 +85,27 @@ module.exports.createRagPipeline = function createRagPipeline({
85
85
  const answer = (await llmI.generate?.(query, results, options)) ?? '';
86
86
  // tiny delay so durations > 0 in timing tests
87
87
  await new Promise((r) => setTimeout(r, 5));
88
- return { success: true, query, results };
88
+
89
+ const result = { success: true, query, results };
90
+
91
+ if (options.citations) {
92
+ const { CitationTracker } = require('../citation/citation-tracker');
93
+ const tracker = new CitationTracker(options.citationOptions || {});
94
+ result.answer = String(answer);
95
+ result.citations = tracker.track(String(answer), results);
96
+ }
97
+
98
+ if (options.evaluate) {
99
+ const { PipelineEvaluator } = require('../evaluate/pipeline-evaluator');
100
+ const evaluator = new PipelineEvaluator(options.evaluateOptions || {});
101
+ result.answer = result.answer || String(answer);
102
+ result.evaluation = evaluator.evaluate(
103
+ { query, answer: String(answer), results },
104
+ { citationResult: result.citations },
105
+ );
106
+ }
107
+
108
+ return result;
89
109
  })();
90
110
 
91
111
  try {
@@ -10,6 +10,7 @@ const {
10
10
  EventSeverity,
11
11
  PipelineEventLogger,
12
12
  } = require('../../observability/event-logger.js');
13
+ const { logger } = require('../../utils/logger');
13
14
 
14
15
  const _logger = global.eventLogger || eventLogger || new PipelineEventLogger();
15
16
  global.eventLogger = _logger;
@@ -233,7 +234,7 @@ class Span {
233
234
  */
234
235
  end(endTime = new Date()) {
235
236
  if (this.endTime !== null) {
236
- console.warn(`Span ${this.name} already ended`); // eslint-disable-line no-console
237
+ logger.warn(`Span ${this.name} already ended`);
237
238
  return;
238
239
  }
239
240
 
@@ -57,4 +57,3 @@ async function createPipeline({
57
57
  module.exports = createPipeline;
58
58
  module.exports.createPipeline = createPipeline;
59
59
  module.exports.createRagPipeline = createPipeline; // Alias for backward compatibility
60
- module.exports.default = module.exports;
@@ -6,6 +6,7 @@ const Ajv = require('ajv');
6
6
  const {
7
7
  createPluginVerifier,
8
8
  } = require('../security/plugin-signature-verifier.js');
9
+ const { logger } = require('../utils/logger');
9
10
 
10
11
  class PluginRegistry {
11
12
  constructor(options = {}) {
@@ -46,9 +47,9 @@ class PluginRegistry {
46
47
  process.env.NODE_ENV !== 'production' &&
47
48
  !this._disableContractWarnings
48
49
  ) {
49
- console.warn(
50
+ logger.warn(
50
51
  `[PLUGIN_REGISTRY] Warning: Failed to compile contract schema: ${error.message}`,
51
- ); // eslint-disable-line no-console
52
+ );
52
53
  }
53
54
  this._contractSchema = null;
54
55
  }
@@ -93,9 +94,9 @@ class PluginRegistry {
93
94
  process.env.NODE_ENV !== 'production' &&
94
95
  !this._disableContractWarnings
95
96
  ) {
96
- console.warn(
97
+ logger.warn(
97
98
  `[PLUGIN_REGISTRY] Warning: Failed to load contract schema: ${error.message}`,
98
- ); // eslint-disable-line no-console
99
+ );
99
100
  }
100
101
  }
101
102
  return null;
@@ -218,7 +219,7 @@ class PluginRegistry {
218
219
 
219
220
  this._contractWarningsShown.add(warningKey);
220
221
 
221
- console.warn(
222
+ logger.warn(
222
223
  `
223
224
  ╔════════════════════════════════════════════════════════════════════════════╗
224
225
  ║ [PLUGIN_REGISTRY] Missing Contract for '${type}' plugins ║
@@ -251,7 +252,7 @@ class PluginRegistry {
251
252
  ║ new PluginRegistry({ disableContractWarnings: true }) ║
252
253
  ╚════════════════════════════════════════════════════════════════════════════╝
253
254
  `.trim(),
254
- ); // eslint-disable-line no-console
255
+ );
255
256
  }
256
257
 
257
258
  _loadContracts() {
@@ -289,7 +290,7 @@ class PluginRegistry {
289
290
 
290
291
  if (process.env.NODE_ENV === 'production') {
291
292
  // In production, log error but continue (fail open)
292
- console.error(`[PLUGIN_REGISTRY] ERROR: ${error.message}`); // eslint-disable-line no-console
293
+ logger.error(`[PLUGIN_REGISTRY] ERROR: ${error.message}`);
293
294
  } else {
294
295
  // In development, throw to fail fast
295
296
  throw error;
@@ -312,9 +313,9 @@ class PluginRegistry {
312
313
  process.env.NODE_ENV !== 'production' &&
313
314
  !this._disableContractWarnings
314
315
  ) {
315
- console.warn(
316
+ logger.warn(
316
317
  `[PLUGIN_REGISTRY] Warning: Failed to load contract for '${type}': ${error.message}`,
317
- ); // eslint-disable-line no-console
318
+ );
318
319
  }
319
320
  }
320
321
  }
@@ -329,9 +330,9 @@ class PluginRegistry {
329
330
  process.env.NODE_ENV !== 'production' &&
330
331
  !this._disableContractWarnings
331
332
  ) {
332
- console.warn(
333
+ logger.warn(
333
334
  `[PLUGIN_REGISTRY] Warning: Contracts directory not accessible: ${error.message}`,
334
- ); // eslint-disable-line no-console
335
+ );
335
336
  }
336
337
  }
337
338
  }
@@ -487,9 +488,9 @@ class PluginRegistry {
487
488
 
488
489
  // In production, this should go to a secure audit log
489
490
  if (process.env.NODE_ENV === 'production') {
490
- console.warn('[AUDIT]', JSON.stringify(auditEntry)); // eslint-disable-line no-console
491
+ logger.warn('[AUDIT]', JSON.stringify(auditEntry));
491
492
  } else {
492
- console.log('[AUDIT]', auditEntry); // eslint-disable-line no-console
493
+ logger.info('[AUDIT]', auditEntry);
493
494
  }
494
495
  }
495
496
 
@@ -537,4 +538,3 @@ const registry = new PluginRegistry({
537
538
  // CJS+ESM interop pattern
538
539
  module.exports = registry;
539
540
  module.exports.PluginRegistry = PluginRegistry;
540
- module.exports.default = module.exports;
@@ -0,0 +1,146 @@
1
+ 'use strict';
2
+
3
+ const { EventEmitter } = require('events');
4
+
5
+ /**
6
+ * Built-in pricing table for common LLM and embedding models.
7
+ * Prices are in USD per `unit` tokens.
8
+ * @type {Record<string, {input: number, output: number, unit: number}>}
9
+ */
10
+ const PRICING = {
11
+ 'gpt-4': { input: 0.03, output: 0.06, unit: 1000 },
12
+ 'gpt-4-turbo': { input: 0.01, output: 0.03, unit: 1000 },
13
+ 'gpt-4o': { input: 0.005, output: 0.015, unit: 1000 },
14
+ 'gpt-4o-mini': { input: 0.00015, output: 0.0006, unit: 1000 },
15
+ 'gpt-3.5-turbo': { input: 0.0005, output: 0.0015, unit: 1000 },
16
+ 'claude-3-opus': { input: 0.015, output: 0.075, unit: 1000 },
17
+ 'claude-3-sonnet': { input: 0.003, output: 0.015, unit: 1000 },
18
+ 'claude-3-haiku': { input: 0.00025, output: 0.00125, unit: 1000 },
19
+ 'claude-3.5-sonnet': { input: 0.003, output: 0.015, unit: 1000 },
20
+ 'cohere-command': { input: 0.001, output: 0.002, unit: 1000 },
21
+ 'text-embedding-3-small': { input: 0.00002, output: 0, unit: 1000 },
22
+ 'text-embedding-3-large': { input: 0.00013, output: 0, unit: 1000 },
23
+ };
24
+
25
+ /**
26
+ * Maps token counts to cost estimates by provider and model.
27
+ *
28
+ * @example
29
+ * const calc = new CostCalculator();
30
+ * const cost = calc.estimate('gpt-4o', 1500, 500);
31
+ * console.log(cost.totalCost); // 0.015
32
+ */
33
+ class CostCalculator extends EventEmitter {
34
+ /**
35
+ * @param {object} [options]
36
+ * @param {Record<string, {input: number, output: number, unit: number}>} [options.customPricing]
37
+ * Additional or override model pricing entries.
38
+ */
39
+ constructor(options = {}) {
40
+ super();
41
+ this.pricing = { ...PRICING, ...(options.customPricing || {}) };
42
+ }
43
+
44
+ /**
45
+ * Resolve a model key from the pricing table.
46
+ * Tries exact match (case-insensitive), then partial match.
47
+ * @param {string} model
48
+ * @returns {string|null} The matched pricing key or null.
49
+ * @private
50
+ */
51
+ _resolveModel(model) {
52
+ const lower = model.toLowerCase();
53
+ const keys = Object.keys(this.pricing);
54
+
55
+ // Exact case-insensitive match
56
+ const exact = keys.find((k) => k.toLowerCase() === lower);
57
+ if (exact) return exact;
58
+
59
+ // Partial match — model string contains a known key or vice-versa
60
+ const partial = keys.find(
61
+ (k) => lower.includes(k.toLowerCase()) || k.toLowerCase().includes(lower),
62
+ );
63
+ return partial || null;
64
+ }
65
+
66
+ /**
67
+ * Estimate the cost for a given token count.
68
+ *
69
+ * @param {string} model - Model identifier (case-insensitive, partial match supported).
70
+ * @param {number} inputTokens - Number of input / prompt tokens.
71
+ * @param {number} [outputTokens=0] - Number of output / completion tokens.
72
+ * @returns {{inputCost: number, outputCost: number, totalCost: number, model: string,
73
+ * inputTokens: number, outputTokens: number, currency: string, warning?: string}}
74
+ */
75
+ estimate(model, inputTokens, outputTokens = 0) {
76
+ const resolvedKey = this._resolveModel(model);
77
+
78
+ if (!resolvedKey) {
79
+ return {
80
+ inputCost: 0,
81
+ outputCost: 0,
82
+ totalCost: 0,
83
+ model,
84
+ inputTokens,
85
+ outputTokens,
86
+ currency: 'USD',
87
+ warning: 'Unknown model',
88
+ };
89
+ }
90
+
91
+ const price = this.pricing[resolvedKey];
92
+ const inputCost = (inputTokens / price.unit) * price.input;
93
+ const outputCost = (outputTokens / price.unit) * price.output;
94
+ const totalCost = inputCost + outputCost;
95
+
96
+ return {
97
+ inputCost,
98
+ outputCost,
99
+ totalCost,
100
+ model: resolvedKey,
101
+ inputTokens,
102
+ outputTokens,
103
+ currency: 'USD',
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Register custom model pricing.
109
+ *
110
+ * @param {string} model - Model identifier.
111
+ * @param {{input: number, output: number, unit: number}} pricing - Pricing definition.
112
+ */
113
+ addModel(model, pricing) {
114
+ if (
115
+ !pricing ||
116
+ typeof pricing.input !== 'number' ||
117
+ typeof pricing.output !== 'number'
118
+ ) {
119
+ throw new Error(
120
+ 'Pricing must include numeric "input" and "output" fields',
121
+ );
122
+ }
123
+ this.pricing[model] = { unit: 1000, ...pricing };
124
+ }
125
+
126
+ /**
127
+ * List all known model names.
128
+ * @returns {string[]}
129
+ */
130
+ listModels() {
131
+ return Object.keys(this.pricing);
132
+ }
133
+
134
+ /**
135
+ * Retrieve pricing for a specific model.
136
+ *
137
+ * @param {string} model - Model identifier (supports fuzzy resolution).
138
+ * @returns {{input: number, output: number, unit: number}|null}
139
+ */
140
+ getModelPricing(model) {
141
+ const key = this._resolveModel(model);
142
+ return key ? { ...this.pricing[key] } : null;
143
+ }
144
+ }
145
+
146
+ module.exports = { CostCalculator, PRICING };
@@ -0,0 +1,260 @@
1
+ 'use strict';
2
+
3
+ const { EventEmitter } = require('events');
4
+ const { CostCalculator } = require('./cost-calculator');
5
+ const { TokenBudget } = require('./token-budget');
6
+
7
+ /**
8
+ * Default tracker configuration.
9
+ * @type {{model: string, embeddingModel: string, enableBudget: boolean, budgetOptions: object}}
10
+ */
11
+ const DEFAULT_CONFIG = {
12
+ model: 'gpt-3.5-turbo',
13
+ embeddingModel: 'text-embedding-3-small',
14
+ enableBudget: false,
15
+ budgetOptions: {},
16
+ };
17
+
18
+ /**
19
+ * Estimated tokens per character when actual token counts are unavailable.
20
+ * @private
21
+ */
22
+ const CHARS_PER_TOKEN = 4;
23
+
24
+ /**
25
+ * Wraps pipeline execution to automatically track token usage and costs.
26
+ *
27
+ * @example
28
+ * const tracker = new CostTracker({ model: 'gpt-4o', enableBudget: true, budgetOptions: { maxCost: 10 } });
29
+ * const wrapped = tracker.wrap(pipeline);
30
+ * const result = await wrapped.run(input);
31
+ * console.log(result.cost); // { inputTokens, outputTokens, estimatedCost, model }
32
+ * console.log(tracker.getSummary());
33
+ */
34
+ class CostTracker extends EventEmitter {
35
+ /**
36
+ * @param {object} [options]
37
+ * @param {string} [options.model='gpt-3.5-turbo'] - Default LLM model for cost estimation.
38
+ * @param {string} [options.embeddingModel='text-embedding-3-small'] - Default embedding model.
39
+ * @param {boolean} [options.enableBudget=false] - Whether to enforce a token/cost budget.
40
+ * @param {object} [options.budgetOptions] - Options forwarded to {@link TokenBudget}.
41
+ * @param {object} [options.calculatorOptions] - Options forwarded to {@link CostCalculator}.
42
+ */
43
+ constructor(options = {}) {
44
+ super();
45
+ this.config = { ...DEFAULT_CONFIG, ...options };
46
+ this.calculator = new CostCalculator(options.calculatorOptions);
47
+ this.budget = this.config.enableBudget
48
+ ? new TokenBudget(this.config.budgetOptions)
49
+ : null;
50
+ this.sessions = [];
51
+ this.currentSession = null;
52
+ }
53
+
54
+ /**
55
+ * Extract token usage from a pipeline result.
56
+ * Checks common locations: result.usage, result.evaluation, or estimates from content length.
57
+ * @param {object} result
58
+ * @returns {{inputTokens: number, outputTokens: number}}
59
+ * @private
60
+ */
61
+ _extractUsage(result) {
62
+ // Direct usage field (OpenAI-style)
63
+ if (result.usage) {
64
+ return {
65
+ inputTokens:
66
+ result.usage.prompt_tokens || result.usage.inputTokens || 0,
67
+ outputTokens:
68
+ result.usage.completion_tokens || result.usage.outputTokens || 0,
69
+ };
70
+ }
71
+
72
+ // Evaluation metadata
73
+ if (result.evaluation && result.evaluation.tokens) {
74
+ return {
75
+ inputTokens: result.evaluation.tokens.input || 0,
76
+ outputTokens: result.evaluation.tokens.output || 0,
77
+ };
78
+ }
79
+
80
+ // Estimate from content length
81
+ const inputText = result.input || result.query || result.prompt || '';
82
+ const outputText = result.output || result.answer || result.response || '';
83
+ return {
84
+ inputTokens: Math.ceil(String(inputText).length / CHARS_PER_TOKEN),
85
+ outputTokens: Math.ceil(String(outputText).length / CHARS_PER_TOKEN),
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Wrap a pipeline so that every `.run()` call is cost-tracked.
91
+ *
92
+ * @param {object} pipeline - Object with a `.run()` method.
93
+ * @returns {object} A proxy pipeline whose `.run()` enriches results with cost data.
94
+ */
95
+ wrap(pipeline) {
96
+ const tracker = this;
97
+
98
+ return {
99
+ ...pipeline,
100
+
101
+ /**
102
+ * Execute the wrapped pipeline and attach cost metadata to the result.
103
+ * @param {...*} args - Arguments forwarded to the original pipeline.run().
104
+ * @returns {Promise<object>} The pipeline result enriched with a `.cost` property.
105
+ */
106
+ run: async (...args) => {
107
+ if (!tracker.currentSession) {
108
+ tracker.startSession({ autoStarted: true });
109
+ }
110
+
111
+ const result = await pipeline.run(...args);
112
+
113
+ const { inputTokens, outputTokens } = tracker._extractUsage(result);
114
+ const model = result.model || tracker.config.model;
115
+ const costEstimate = tracker.calculator.estimate(
116
+ model,
117
+ inputTokens,
118
+ outputTokens,
119
+ );
120
+
121
+ // Budget enforcement
122
+ if (tracker.budget) {
123
+ const budgetCheck = tracker.budget.check(
124
+ inputTokens + outputTokens,
125
+ costEstimate.totalCost,
126
+ );
127
+ if (!budgetCheck.allowed) {
128
+ const err = new Error(`Budget exceeded: ${budgetCheck.reason}`);
129
+ err.code = 'BUDGET_EXCEEDED';
130
+ err.budgetCheck = budgetCheck;
131
+ throw err;
132
+ }
133
+ tracker.budget.record(
134
+ inputTokens + outputTokens,
135
+ costEstimate.totalCost,
136
+ );
137
+ }
138
+
139
+ // Attach cost data to result
140
+ result.cost = {
141
+ inputTokens,
142
+ outputTokens,
143
+ estimatedCost: costEstimate.totalCost,
144
+ model: costEstimate.model,
145
+ currency: 'USD',
146
+ };
147
+
148
+ // Record in session
149
+ const entry = {
150
+ timestamp: Date.now(),
151
+ inputTokens,
152
+ outputTokens,
153
+ cost: costEstimate.totalCost,
154
+ model: costEstimate.model,
155
+ };
156
+
157
+ if (tracker.currentSession) {
158
+ tracker.currentSession.entries.push(entry);
159
+ tracker.currentSession.totalTokens += inputTokens + outputTokens;
160
+ tracker.currentSession.totalCost += costEstimate.totalCost;
161
+ }
162
+
163
+ tracker.emit('tracked', entry);
164
+
165
+ return result;
166
+ },
167
+ };
168
+ }
169
+
170
+ /**
171
+ * Start a new tracking session.
172
+ *
173
+ * @param {object} [metadata={}] - Arbitrary metadata to attach to the session.
174
+ * @returns {{id: string, startTime: number, metadata: object, entries: Array,
175
+ * totalTokens: number, totalCost: number}}
176
+ */
177
+ startSession(metadata = {}) {
178
+ const session = {
179
+ id: `session_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
180
+ startTime: Date.now(),
181
+ endTime: null,
182
+ metadata,
183
+ entries: [],
184
+ totalTokens: 0,
185
+ totalCost: 0,
186
+ };
187
+
188
+ this.currentSession = session;
189
+ this.sessions.push(session);
190
+ return session;
191
+ }
192
+
193
+ /**
194
+ * End the current tracking session and compute totals.
195
+ *
196
+ * @returns {object|null} The closed session, or null if no session was active.
197
+ */
198
+ endSession() {
199
+ if (!this.currentSession) return null;
200
+
201
+ this.currentSession.endTime = Date.now();
202
+ const closed = this.currentSession;
203
+ this.currentSession = null;
204
+ return closed;
205
+ }
206
+
207
+ /**
208
+ * Return an aggregate summary across all sessions.
209
+ *
210
+ * @returns {{totalTokens: number, totalCost: number, sessionCount: number,
211
+ * avgCostPerQuery: number, breakdown: Record<string, {tokens: number, cost: number, count: number}>}}
212
+ */
213
+ getSummary() {
214
+ let totalTokens = 0;
215
+ let totalCost = 0;
216
+ let totalOps = 0;
217
+ const breakdown = {};
218
+
219
+ for (const session of this.sessions) {
220
+ for (const entry of session.entries) {
221
+ totalTokens += entry.inputTokens + entry.outputTokens;
222
+ totalCost += entry.cost;
223
+ totalOps += 1;
224
+
225
+ if (!breakdown[entry.model]) {
226
+ breakdown[entry.model] = { tokens: 0, cost: 0, count: 0 };
227
+ }
228
+ breakdown[entry.model].tokens += entry.inputTokens + entry.outputTokens;
229
+ breakdown[entry.model].cost += entry.cost;
230
+ breakdown[entry.model].count += 1;
231
+ }
232
+ }
233
+
234
+ return {
235
+ totalTokens,
236
+ totalCost,
237
+ sessionCount: this.sessions.length,
238
+ avgCostPerQuery: totalOps > 0 ? totalCost / totalOps : 0,
239
+ breakdown,
240
+ };
241
+ }
242
+
243
+ /**
244
+ * Return the full history of tracked operations across all sessions.
245
+ *
246
+ * @returns {Array<{timestamp: number, inputTokens: number, outputTokens: number,
247
+ * cost: number, model: string, sessionId: string}>}
248
+ */
249
+ getHistory() {
250
+ const history = [];
251
+ for (const session of this.sessions) {
252
+ for (const entry of session.entries) {
253
+ history.push({ ...entry, sessionId: session.id });
254
+ }
255
+ }
256
+ return history;
257
+ }
258
+ }
259
+
260
+ module.exports = { CostTracker, DEFAULT_CONFIG };