@peopl-health/nexus 3.3.16 → 3.3.18-fix-model

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.
@@ -145,7 +145,7 @@ class TwilioProvider extends MessageProvider {
145
145
  }
146
146
  } else {
147
147
  result = await this.twilioClient.messages.create(messageParams);
148
- await saveMessage(messageParams.body, result);
148
+ await saveMessage(messageParams.body || messageData.body, result);
149
149
  }
150
150
 
151
151
  return {
@@ -50,13 +50,21 @@ class BaseAssistant {
50
50
  registerTool(config) {
51
51
  const { name, handler, description, parameters, strict } = config;
52
52
 
53
+ const toolParams = parameters
54
+ ? { ...parameters }
55
+ : { type: 'object', properties: {} };
56
+
57
+ if (toolParams.additionalProperties === undefined) {
58
+ toolParams.additionalProperties = false;
59
+ }
60
+
53
61
  this.tools.set(name, {
54
62
  schema: {
55
63
  type: 'function',
56
64
  function: {
57
65
  name,
58
66
  description: description || 'Custom assistant tool',
59
- parameters: parameters || { type: 'object', properties: {}, additionalProperties: true },
67
+ parameters: toolParams,
60
68
  ...(strict !== undefined && { strict })
61
69
  }
62
70
  },
@@ -71,7 +79,8 @@ class BaseAssistant {
71
79
  async executeTool(name, args) {
72
80
  const tool = this.tools.get(name);
73
81
  if (!tool) {
74
- throw new Error(`Unknown tool '${name}' requested by assistant`);
82
+ logger.warn('[executeTool] Rejected unregistered tool', { name, assistantId: this.assistantId });
83
+ throw new Error(`Tool '${name}' is not available`);
75
84
  }
76
85
  return await tool.execute(args);
77
86
  }
@@ -0,0 +1,19 @@
1
+ const { _resetState: _resetLLMState } = require('../config/llmConfig');
2
+ const { _resetOverrides } = require('../config/runtimeConfig');
3
+ const { _resetMetaConfig } = require('../config/metaConfig');
4
+ const { _clearClinicalContextCache } = require('../models/messageModel');
5
+ const { _resetRegistry } = require('../services/assistantResolver');
6
+ const { setPreprocessingHandler } = require('../services/preprocessingService');
7
+ const { _resetDefaultInstance } = require('../core/NexusMessaging');
8
+
9
+ function resetAll() {
10
+ _resetDefaultInstance();
11
+ _resetRegistry();
12
+ _resetLLMState();
13
+ setPreprocessingHandler(null);
14
+ _resetOverrides();
15
+ _resetMetaConfig();
16
+ _clearClinicalContextCache();
17
+ }
18
+
19
+ module.exports = { resetAll };
@@ -51,6 +51,13 @@ const getAnthropicClient = () => {
51
51
  return state.anthropicClient;
52
52
  };
53
53
 
54
+ const _resetState = () => {
55
+ state.anthropicClient = null;
56
+ state.openaiClient = null;
57
+ state.provider = null;
58
+ state.variant = process.env.VARIANT || 'assistants';
59
+ };
60
+
54
61
  module.exports = {
55
62
  get openaiClient() { return state.openaiClient; },
56
63
  get providerInstance() { return state.provider; },
@@ -59,5 +66,6 @@ module.exports = {
59
66
  setOpenAIProvider,
60
67
  getOpenAIProvider,
61
68
  requireOpenAIProvider,
62
- configureOpenAIProvider
69
+ configureOpenAIProvider,
70
+ _resetState
63
71
  };
@@ -25,7 +25,12 @@ const setMetaConfig = ({ accessToken, wabaId, apiVersion }) => {
25
25
 
26
26
  const getMetaConfig = () => metaConfig;
27
27
 
28
+ const _resetMetaConfig = () => {
29
+ metaConfig = { accessToken: null, wabaId: null, apiVersion: 'v21.0' };
30
+ };
31
+
28
32
  module.exports = {
29
33
  setMetaConfig,
30
- getMetaConfig
34
+ getMetaConfig,
35
+ _resetMetaConfig
31
36
  };
@@ -11,5 +11,7 @@ function get(key, fallback = undefined) {
11
11
  return process.env[k] !== undefined ? process.env[k] : fallback;
12
12
  }
13
13
 
14
- module.exports = { set, get };
14
+ function _resetOverrides() { overrides.clear(); }
15
+
16
+ module.exports = { set, get, _resetOverrides };
15
17
 
@@ -1,7 +1,6 @@
1
1
  const EventEmitter = require('events');
2
2
 
3
3
  const { airtable } = require('../config/airtableConfig');
4
- const runtimeConfig = require('../config/runtimeConfig');
5
4
  const llmConfigModule = require('../config/llmConfig');
6
5
  const { connect } = require('../config/mongoConfig');
7
6
 
@@ -11,7 +10,7 @@ const { Message, getMessageValues, insertMessage } = require('../models/messageM
11
10
  const { Thread } = require('../models/threadModel');
12
11
 
13
12
  const { ensureThreadExists } = require('../helpers/threadHelper');
14
- const { processTwilioMediaMessage } = require('../helpers/twilioMediaHelper');
13
+ const { enrichMessageWithTwilioMedia } = require('../helpers/twilioMediaHelper');
15
14
  const { convertTwilioToInternalFormat } = require('../helpers/twilioHelper');
16
15
 
17
16
  const { createMessagingProvider } = require('../adapters/registry');
@@ -360,25 +359,15 @@ class NexusMessaging {
360
359
  async _ensureMediaPersistence(messageData) {
361
360
  try {
362
361
  const raw = messageData?.raw;
363
- if (!raw || raw.__nexusMediaProcessed) return;
362
+ const result = await enrichMessageWithTwilioMedia(raw);
363
+ if (!result) return;
364
364
 
365
- const numMedia = parseInt(raw.NumMedia || '0', 10);
366
- if (numMedia <= 0 || !raw.MediaUrl0) return;
367
-
368
- const bucketName = runtimeConfig.get('AWS_S3_BUCKET_NAME');
369
- if (!bucketName) return;
370
-
371
- const mediaItems = await processTwilioMediaMessage(raw, bucketName);
372
- if (!mediaItems?.length) return;
373
-
374
- raw.__nexusMediaProcessed = true;
375
- const [primary, ...rest] = mediaItems;
376
- const mediaPayload = rest.length ? { ...primary, metadata: { ...primary.metadata, attachments: rest } } : primary;
365
+ const { mediaPayload, caption } = result;
377
366
 
378
367
  if (messageData.media == null) messageData.media = mediaPayload;
379
368
  if (messageData.fileUrl == null) messageData.fileUrl = mediaPayload.metadata?.presignedUrl;
380
369
  if (messageData.fileType == null) messageData.fileType = mediaPayload.mediaType;
381
- if (messageData.caption == null) messageData.caption = primary.caption;
370
+ if (messageData.caption == null) messageData.caption = caption;
382
371
  if (messageData.body == null) messageData.body = messageData.caption;
383
372
  messageData.isMedia = true;
384
373
 
@@ -483,12 +472,15 @@ const sendScheduledMessage = async (scheduledMessage) => {
483
472
  return await requireDefaultInstance().sendScheduledMessage(scheduledMessage);
484
473
  };
485
474
 
486
- module.exports = {
475
+ const _resetDefaultInstance = () => { defaultInstance = null; };
476
+
477
+ module.exports = {
487
478
  NexusMessaging,
488
479
  sendMessage,
489
480
  sendScheduledMessage,
490
481
  setDefaultInstance,
491
482
  getDefaultInstance,
492
483
  getProvider,
493
- requireProvider
484
+ requireProvider,
485
+ _resetDefaultInstance
494
486
  };
@@ -37,7 +37,7 @@ async function analyzeImage(imagePath, isSticker = false, contentType = null) {
37
37
  const imageBuffer = await fs.promises.readFile(imagePath);
38
38
  const base64Image = imageBuffer.toString('base64');
39
39
 
40
- const createImageMessage = (prompt, model = 'claude-3-7-sonnet-20250219') => anthropicClient.messages.create({
40
+ const createImageMessage = (prompt, model = 'claude-sonnet-4-5-20250929') => anthropicClient.messages.create({
41
41
  model, max_tokens: 1024,
42
42
  messages: [{ role: 'user', content: [
43
43
  { type: 'image', source: { type: 'base64', media_type: mimeType, data: base64Image } },
@@ -1,5 +1,6 @@
1
1
  const { generatePresignedUrl } = require('../config/awsConfig');
2
2
  const { Monitoreo_ID } = require('../config/airtableConfig');
3
+ const runtimeConfig = require('../config/runtimeConfig');
3
4
 
4
5
  const { validateMedia } = require('../utils/mediaValidator');
5
6
  const { logger } = require('../utils/logger');
@@ -120,6 +121,41 @@ async function processTwilioMediaMessage(twilioMessage, bucketName) {
120
121
  return mediaItems;
121
122
  }
122
123
 
124
+ async function enrichMessageWithTwilioMedia(rawMessage) {
125
+ if (!rawMessage || rawMessage.__nexusMediaProcessed) return null;
126
+
127
+ const numMedia = parseInt(rawMessage.NumMedia || '0', 10);
128
+ if (numMedia <= 0 || !rawMessage.MediaUrl0) return null;
129
+
130
+ const bucketName = runtimeConfig.get('AWS_S3_BUCKET_NAME');
131
+ if (!bucketName) {
132
+ logger.warn('[mediaEnrichment] AWS_S3_BUCKET_NAME not configured. Skipping media upload.');
133
+ return null;
134
+ }
135
+
136
+ const mediaItems = await processTwilioMediaMessage(rawMessage, bucketName);
137
+ if (!mediaItems || mediaItems.length === 0) {
138
+ logger.warn('[mediaEnrichment] Media processing returned no items');
139
+ return null;
140
+ }
141
+
142
+ const [primary, ...rest] = mediaItems;
143
+ const mediaPayload = rest.length > 0
144
+ ? { ...primary, metadata: { ...(primary.metadata || {}), attachments: rest } }
145
+ : primary;
146
+
147
+ rawMessage.__nexusMediaProcessed = true;
148
+
149
+ logger.info('[mediaEnrichment] Media processed successfully', {
150
+ primaryType: mediaPayload.mediaType,
151
+ mediaCount: mediaItems.length,
152
+ s3Key: mediaPayload.key
153
+ });
154
+
155
+ return { mediaPayload, caption: primary.caption };
156
+ }
157
+
123
158
  module.exports = {
124
- processTwilioMediaMessage
159
+ processTwilioMediaMessage,
160
+ enrichMessageWithTwilioMedia
125
161
  };
package/lib/index.d.ts CHANGED
@@ -1,6 +1,4 @@
1
1
  declare module '@peopl-health/nexus' {
2
- import mongoose from 'mongoose';
3
-
4
2
  // Core Types
5
3
  export interface MessageData {
6
4
  id?: string;
@@ -178,7 +176,6 @@ declare module '@peopl-health/nexus' {
178
176
  definition: typeof BaseAssistant | ((thread?: any) => any) | AssistantConfigDefinition
179
177
  ): any;
180
178
 
181
- export function configureAssistantsLLM(client: any): void;
182
179
  export function overrideGetAssistantById(resolver: (assistantId: string, thread?: any) => any): void;
183
180
  export function configureAssistants(config: any): void;
184
181
  export function setPreprocessingHandler(
@@ -231,23 +228,42 @@ declare module '@peopl-health/nexus' {
231
228
  disconnect(): Promise<void>;
232
229
  }
233
230
 
234
- export class AssistantManager {
235
- constructor(config?: AssistantConfig);
236
- setLLMClient(llmClient: any): void;
237
- registerAssistants(assistantConfigs: Record<string, string>): void;
238
- setHandlers(handlers: AssistantConfig['handlers']): void;
239
- createThread(code: string, assistantId: string, initialMessages?: string[]): Promise<ThreadData>;
240
- sendMessage(threadData: ThreadData, message: string, runOptions?: any): Promise<string | null>;
241
- submitToolOutputs(threadId: string, runId: string, toolOutputs: any[]): Promise<any>;
242
- addInstruction(threadData: ThreadData, instruction: string): Promise<string | null>;
243
- }
244
-
245
231
  export class MessageParser {
246
232
  constructor(config?: ParserConfig);
247
233
  parseMessage(rawMessage: any): MessageData;
248
234
  updateConfig(newConfig: ParserConfig): void;
249
235
  }
250
236
 
237
+ // LLM Providers
238
+ export class OpenAIAssistantsProvider {
239
+ constructor(options?: { apiKey?: string; organization?: string; client?: any; defaultModels?: Record<string, string> });
240
+ getVariant(): string;
241
+ getClient(): any;
242
+ createConversation(options?: any): Promise<any>;
243
+ addMessage(options: { threadId: string; messages?: any[]; role?: string; content: string; metadata?: any }): Promise<any>;
244
+ listMessages(options?: any): Promise<any>;
245
+ getRunText(options?: any): Promise<string>;
246
+ executeRun(options: { thread: any; assistant: any; tools?: any[]; config?: any }): Promise<any>;
247
+ runConversation(options?: any): Promise<any>;
248
+ getRun(options: { threadId: string; runId: string }): Promise<any>;
249
+ submitToolOutputs(options: { threadId: string; runId: string; toolOutputs: any[] }): Promise<any>;
250
+ transcribeAudio(options?: any): Promise<any>;
251
+ }
252
+
253
+ export class OpenAIResponsesProvider {
254
+ constructor(options?: { apiKey?: string; organization?: string; client?: any; defaultModels?: Record<string, string>; conversationManager?: any });
255
+ getVariant(): string;
256
+ getClient(): any;
257
+ createConversation(options?: any): Promise<any>;
258
+ addMessage(options: { threadId: string; messages?: any[]; role?: string; content: string; metadata?: any }): Promise<any>;
259
+ listMessages(options?: any): Promise<any>;
260
+ executeRun(options: { thread: any; assistant: any; message?: string; tools?: any[]; config?: any }): Promise<any>;
261
+ runConversation(config?: any): Promise<any>;
262
+ transcribeAudio(options?: any): Promise<any>;
263
+ }
264
+
265
+ export function createLLMProvider(config?: { variant?: string; [key: string]: any }): OpenAIAssistantsProvider | OpenAIResponsesProvider;
266
+
251
267
  // Main Nexus Class
252
268
  export interface NexusConfig {
253
269
  messaging?: any;
@@ -270,18 +286,15 @@ declare module '@peopl-health/nexus' {
270
286
  sendMessage(messageData: MessageData): Promise<any>;
271
287
  sendScheduledMessage(scheduledMessage: ScheduledMessage): Promise<void>;
272
288
  processMessage(rawMessage: any): Promise<any>;
273
- createAssistantThread(code: string, assistantId: string, initialMessages?: string[]): Promise<ThreadData>;
274
- sendToAssistant(code: string, message: string, runOptions?: any): Promise<string | null>;
275
289
  isConnected(): boolean;
276
290
  disconnect(): Promise<void>;
277
291
  getMessaging(): NexusMessaging;
278
292
  getStorage(): MongoStorage | null;
279
- getAssistantManager(): AssistantManager | null;
280
293
  getMessageParser(): MessageParser | null;
294
+ getLLMProvider(): OpenAIAssistantsProvider | OpenAIResponsesProvider | null;
281
295
  }
282
296
 
283
297
  // Utility Functions
284
- export function createLogger(config?: any): any;
285
298
  export function formatCode(codeBase: string): string;
286
299
  export function calculateDelay(sendTime: string | Date, timeZone?: string): number;
287
300
  export function ensureWhatsAppFormat(phoneNumber: any): string | null;
@@ -291,49 +304,17 @@ declare module '@peopl-health/nexus' {
291
304
  export function extractTitle(message: any, mediaType: string): string | null;
292
305
  export function useMongoDBAuthState(uri: string, dbName: string, sessionId: string): Promise<any>;
293
306
 
294
- // Models
295
- export const Message: mongoose.Model<any>;
296
- export const Thread: mongoose.Model<any>;
297
- export function getMessageValues(message: any, content: string): any;
298
- export function formatTimestamp(unixTimestamp: number): string;
299
-
300
- // Module Exports
301
- export const adapters: {
302
- TwilioProvider: typeof TwilioProvider;
303
- BaileysProvider: typeof BaileysProvider;
304
- };
305
-
306
- export const core: {
307
- NexusMessaging: typeof NexusMessaging;
308
- MessageProvider: typeof MessageProvider;
309
- };
310
-
311
- export const storage: {
312
- MongoStorage: typeof MongoStorage;
313
- };
314
-
315
- export const utils: {
316
- AssistantManager: typeof AssistantManager;
317
- MessageParser: typeof MessageParser;
318
- logger: any;
319
- createLogger: typeof createLogger;
320
- delay: typeof delay;
321
- formatCode: typeof formatCode;
322
- calculateDelay: typeof calculateDelay;
323
- useMongoDBAuthState: typeof useMongoDBAuthState;
324
- convertTwilioToInternalFormat: typeof convertTwilioToInternalFormat;
325
- downloadMediaFromTwilio: typeof downloadMediaFromTwilio;
326
- getMediaTypeFromContentType: typeof getMediaTypeFromContentType;
327
- extractTitle: typeof extractTitle;
328
- ensureWhatsAppFormat: typeof ensureWhatsAppFormat;
329
- };
330
-
331
- export const models: {
332
- Message: typeof Message;
333
- Thread: typeof Thread;
334
- getMessageValues: typeof getMessageValues;
335
- formatTimestamp: typeof formatTimestamp;
336
- };
307
+ export const logger: any;
308
+ export function requestIdMiddleware(req: any, res: any, next: () => void): void;
309
+ export function getRequestId(): string | null;
310
+
311
+ export function setModelDatabases(mapping: Record<string, string>): void;
312
+ export function setModelDatabase(modelName: string, dbName: string): void;
313
+ export function getModelDatabase(modelName: string): string | null;
314
+
315
+ export const routes: any;
316
+ export function setupDefaultRoutes(app: any): void;
317
+ export function createRouter(routeDefinitions: Record<string, string>, controllers: Record<string, Function>): any;
337
318
 
338
319
  // Queue Adapters
339
320
  export interface QueueJobOptions {
package/lib/index.js CHANGED
@@ -218,6 +218,7 @@ class Nexus {
218
218
  }
219
219
 
220
220
  const routes = require('./routes');
221
+ const { resetAll } = require('./config/lifecycle');
221
222
 
222
223
  module.exports = {
223
224
  Nexus,
@@ -251,5 +252,7 @@ module.exports = {
251
252
  LocalQueueAdapter,
252
253
  RedisQueueAdapter,
253
254
  createQueueAdapter,
254
- registerQueueAdapter
255
+ registerQueueAdapter,
256
+
257
+ resetAll
255
258
  };
@@ -14,6 +14,12 @@ const predictionMetricsSchema = new mongoose.Schema({
14
14
  completed: { type: Boolean, default: true },
15
15
  error: { type: String, default: null },
16
16
  timing_breakdown: { type: Object, default: {} },
17
+ token_usage: {
18
+ input_tokens: { type: Number, default: 0 },
19
+ output_tokens: { type: Number, default: 0 },
20
+ total_tokens: { type: Number, default: 0 },
21
+ model: { type: String },
22
+ },
17
23
  source: { type: String, default: () => process.env.USER_DB_MONGO }
18
24
  }, { timestamps: true });
19
25
 
@@ -28,4 +34,22 @@ const getPredictionMetrics = () => {
28
34
  return db.models.PredictionMetrics || db.model('PredictionMetrics', predictionMetricsSchema);
29
35
  };
30
36
 
31
- module.exports = { getPredictionMetrics, predictionMetricsSchema };
37
+ const getConversationTokenUsage = async (numero, since = null) => {
38
+ const Model = getPredictionMetrics();
39
+ const match = { numero, 'token_usage.total_tokens': { $gt: 0 } };
40
+ if (since) match.createdAt = { $gte: since };
41
+
42
+ const [result] = await Model.aggregate([
43
+ { $match: match },
44
+ { $group: {
45
+ _id: '$numero',
46
+ total_input_tokens: { $sum: '$token_usage.input_tokens' },
47
+ total_output_tokens: { $sum: '$token_usage.output_tokens' },
48
+ total_tokens: { $sum: '$token_usage.total_tokens' },
49
+ request_count: { $sum: 1 },
50
+ }}
51
+ ]);
52
+ return result || { _id: numero, total_input_tokens: 0, total_output_tokens: 0, total_tokens: 0, request_count: 0 };
53
+ };
54
+
55
+ module.exports = { getPredictionMetrics, getConversationTokenUsage, predictionMetricsSchema };
@@ -1,4 +1,7 @@
1
1
  const mongoose = require('mongoose');
2
+ const runtimeConfig = require('../config/runtimeConfig');
3
+
4
+ const VARIANT = runtimeConfig.get('VARIANT', 'assistants');
2
5
 
3
6
  const threadSchema = new mongoose.Schema({
4
7
  code: { type: String, required: true },
@@ -21,18 +24,15 @@ threadSchema.index({ thread_id: 1 });
21
24
  threadSchema.index({ conversation_id: 1 });
22
25
 
23
26
  threadSchema.methods.getConversationId = function() {
24
- const variant = process.env.VARIANT || 'assistants';
25
- return variant === 'assistants' ? this.thread_id : this.conversation_id;
27
+ return VARIANT === 'assistants' ? this.thread_id : this.conversation_id;
26
28
  };
27
29
 
28
30
  threadSchema.methods.getAssistantId = function() {
29
- const variant = process.env.VARIANT || 'assistants';
30
- return variant === 'assistants' ? this.assistant_id : this.prompt_id;
31
+ return VARIANT === 'assistants' ? this.assistant_id : this.prompt_id;
31
32
  };
32
33
 
33
34
  threadSchema.statics.setAssistantId = function(threadObj, assistantId) {
34
- const variant = process.env.VARIANT || 'assistants';
35
- if (variant === 'assistants') {
35
+ if (VARIANT === 'assistants') {
36
36
  threadObj.assistant_id = assistantId;
37
37
  } else {
38
38
  threadObj.prompt_id = assistantId;
@@ -41,8 +41,7 @@ threadSchema.statics.setAssistantId = function(threadObj, assistantId) {
41
41
  };
42
42
 
43
43
  threadSchema.statics.setConversationId = function(threadObj, conversationId) {
44
- const variant = process.env.VARIANT || 'assistants';
45
- if (variant === 'assistants') {
44
+ if (VARIANT === 'assistants') {
46
45
  threadObj.thread_id = conversationId;
47
46
  } else {
48
47
  threadObj.conversation_id = conversationId;
@@ -1,6 +1,6 @@
1
1
  const { trace, metrics } = require('@opentelemetry/api');
2
2
 
3
- const { initTelemetry, shutdownTelemetry } = require('../observability/telemetry');
3
+ const { initTelemetry, shutdownTelemetry, getMetricsRequestHandler } = require('../observability/telemetry');
4
4
 
5
5
  const tracer = trace.getTracer('nexus-assistant');
6
6
  const meter = metrics.getMeter('nexus-assistant');
@@ -149,6 +149,7 @@ function recordFileOperation(operationType, attributes = {}) {
149
149
  module.exports = {
150
150
  init,
151
151
  shutdown: shutdownTelemetry,
152
+ getMetricsRequestHandler,
152
153
  traceOperation,
153
154
  createSpan,
154
155
  tracer,
@@ -17,6 +17,7 @@ class TelemetryManager {
17
17
  constructor() {
18
18
  this.sdk = null;
19
19
  this.isInitialized = false;
20
+ this.prometheusExporter = null;
20
21
  }
21
22
 
22
23
  async init(config = {}) {
@@ -74,14 +75,18 @@ class TelemetryManager {
74
75
  exportTimeoutMillis: isProd ? 5000 : 2000,
75
76
  scheduledDelayMillis: isProd ? 2000 : 500,
76
77
  }),
77
- metricReader: new PrometheusExporter({ endpoint: prometheusEndpoint, port: prometheusPort }),
78
+ metricReader: this.prometheusExporter = new PrometheusExporter({
79
+ endpoint: prometheusEndpoint,
80
+ port: prometheusPort,
81
+ preventServerStart: true,
82
+ }),
78
83
  });
79
84
 
80
85
  await this.sdk.start();
81
86
  this.isInitialized = true;
82
87
 
83
88
  console.log(`🚀 OpenTelemetry initialized for "${serviceName}"`);
84
- console.log(`📊 Metrics: http://localhost:${prometheusPort}${prometheusEndpoint}`);
89
+ console.log(`📊 Metrics: available at ${prometheusEndpoint} (mounted on Express app)`);
85
90
  console.log(`🔍 Traces: ${otlpEndpoint || jaegerEndpoint}`);
86
91
  } catch (error) {
87
92
  console.error('❌ Failed to initialize OpenTelemetry:', error.message);
@@ -109,5 +114,9 @@ const telemetryManager = new TelemetryManager();
109
114
  module.exports = {
110
115
  telemetryManager,
111
116
  initTelemetry: (config) => telemetryManager.init(config),
112
- shutdownTelemetry: () => telemetryManager.shutdown()
117
+ shutdownTelemetry: () => telemetryManager.shutdown(),
118
+ getMetricsRequestHandler: () => {
119
+ if (!telemetryManager.prometheusExporter) return null;
120
+ return (req, res) => telemetryManager.prometheusExporter.getMetricsRequestHandler(req, res);
121
+ }
113
122
  };
@@ -305,11 +305,23 @@ class OpenAIResponsesProvider {
305
305
 
306
306
  let totalRetries = 0;
307
307
  let allToolsExecuted = [];
308
+ const accumulatedUsage = { input_tokens: 0, output_tokens: 0, total_tokens: 0 };
309
+ const addUsage = (usage) => {
310
+ if (!usage) return;
311
+ accumulatedUsage.input_tokens += usage.input_tokens || 0;
312
+ accumulatedUsage.output_tokens += usage.output_tokens || 0;
313
+ accumulatedUsage.total_tokens += usage.total_tokens || 0;
314
+ };
308
315
 
309
316
  const devRecord = await getRecordByFilter(Config_ID, 'responses', `{prompt_id} = "${assistantId}"`);
310
317
  let devContent = devRecord?.[0]?.content || '';
311
318
  if (promptVariables) devContent = devContent.replace(/\{\{(\w+)\}\}/g, (_, key) => promptVariables[key] ?? '');
312
319
 
320
+ if (assistant?.tools?.size) {
321
+ const toolNames = Array.from(assistant.tools.keys()).join(', ');
322
+ devContent += `\n\nYou only have access to these tools: ${toolNames}. Do not call or reference any tools not listed here.`;
323
+ }
324
+
313
325
  const messages = (context || this._convertItemsToApiFormat(additionalMessages))
314
326
  .filter(item => item.type !== 'function_call' && item.type !== 'function_call_output');
315
327
  const input = [{ role: 'developer', content: devContent }, ...messages];
@@ -329,6 +341,7 @@ class OpenAIResponsesProvider {
329
341
 
330
342
  const { result: response, retries } = await makeAPICall(input);
331
343
  totalRetries += retries;
344
+ addUsage(response.usage);
332
345
  let finalResponse = response;
333
346
 
334
347
  if (assistant && response.output) {
@@ -349,6 +362,7 @@ class OpenAIResponsesProvider {
349
362
 
350
363
  const { result: followUp, retries: followUpRetries } = await makeAPICall(currentInput);
351
364
  totalRetries += followUpRetries;
365
+ addUsage(followUp.usage);
352
366
  finalResponse = followUp;
353
367
  }
354
368
  }
@@ -360,6 +374,7 @@ class OpenAIResponsesProvider {
360
374
  object: finalResponse.object || 'response',
361
375
  tools_executed: allToolsExecuted,
362
376
  retries: totalRetries,
377
+ usage: accumulatedUsage,
363
378
  };
364
379
  }
365
380
 
@@ -4,7 +4,7 @@ async function executeFunctionCall(assistant, call) {
4
4
  const startTime = Date.now();
5
5
  const name = call.name;
6
6
  const args = call.arguments ? JSON.parse(call.arguments) : {};
7
-
7
+
8
8
  logger.info('[executeFunctionCall] Calling', { name, call_id: call.call_id });
9
9
 
10
10
  let result, success = true;
@@ -176,6 +176,12 @@ const builtInControllers = {
176
176
  };
177
177
 
178
178
  const setupDefaultRoutes = (app) => {
179
+ const { getMetricsRequestHandler } = require('../observability');
180
+ const metricsHandler = getMetricsRequestHandler();
181
+ if (metricsHandler) {
182
+ app.get('/metrics', metricsHandler);
183
+ }
184
+
179
185
  app.use('/api/assistant', createRouter(assistantRouteDefinitions, builtInControllers));
180
186
  app.use('/api/conversation', createRouter(conversationRouteDefinitions, builtInControllers));
181
187
  app.use('/api/interaction', createRouter(interactionRouteDefinitions, builtInControllers));
@@ -98,9 +98,16 @@ const getAssistantById = (assistant_id, thread) => {
98
98
  throw new Error(`Assistant with ID "${assistant_id}" not found`);
99
99
  };
100
100
 
101
+ const _resetRegistry = () => {
102
+ assistantConfig = null;
103
+ assistantRegistry = {};
104
+ customGetAssistantById = null;
105
+ };
106
+
101
107
  module.exports = {
102
108
  getAssistantById,
103
109
  configureAssistants,
104
110
  registerAssistant,
105
- overrideGetAssistantById
111
+ overrideGetAssistantById,
112
+ _resetRegistry
106
113
  };
@@ -242,6 +242,8 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
242
242
  const { output: rawOutput, completed, retries, predictionTimeMs, tools_executed } = runResult;
243
243
  const prompt = runResult.run?.prompt || null;
244
244
  const response_id = runResult.run?.id || null;
245
+ const usage = runResult.run?.usage || null;
246
+ const model = runResult.run?.model || null;
245
247
 
246
248
  const output = sanitizeOutput(rawOutput);
247
249
  if (rawOutput !== output) {
@@ -259,15 +261,28 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
259
261
  hasMedia: urls.length > 0,
260
262
  retries,
261
263
  totalMs: timings.total_ms,
262
- toolsExecuted: tools_executed?.length || 0
264
+ toolsExecuted: tools_executed?.length || 0,
265
+ token_usage: usage ? {
266
+ input_tokens: usage.input_tokens,
267
+ output_tokens: usage.output_tokens,
268
+ total_tokens: usage.total_tokens,
269
+ model,
270
+ } : undefined,
263
271
  });
264
272
 
265
273
  if (output && predictionTimeMs) {
266
- logger.debug('[replyAssistant] Storing metrics with timing_breakdown', {
274
+ logger.debug('[replyAssistant] Storing metrics with timing_breakdown', {
267
275
  timing_breakdown: timings,
268
276
  has_breakdown: !!timings.process_messages_breakdown
269
277
  });
270
-
278
+
279
+ const tokenUsage = usage ? {
280
+ input_tokens: usage.input_tokens || 0,
281
+ output_tokens: usage.output_tokens || 0,
282
+ total_tokens: usage.total_tokens || 0,
283
+ model: model || undefined,
284
+ } : undefined;
285
+
271
286
  await getPredictionMetrics().create({
272
287
  message_id: `${code}-${Date.now()}`,
273
288
  numero: code,
@@ -275,8 +290,20 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
275
290
  prediction_time_ms: predictionTimeMs,
276
291
  retry_count: retries,
277
292
  completed: completed,
278
- timing_breakdown: timings
293
+ timing_breakdown: timings,
294
+ token_usage: tokenUsage,
279
295
  }).catch(err => logger.error('[replyAssistant] Failed to store metrics', { error: err.message }));
296
+
297
+ const alertThreshold = parseInt(process.env.TOKEN_ALERT_THRESHOLD, 10);
298
+ if (alertThreshold && usage?.total_tokens > alertThreshold) {
299
+ logger.warn('[replyAssistant] Token usage spike detected', {
300
+ code: code ? `${code.substring(0, 3)}***${code.slice(-4)}` : 'unknown',
301
+ total_tokens: usage.total_tokens,
302
+ threshold: alertThreshold,
303
+ model,
304
+ assistant_id: thread.getAssistantId(),
305
+ });
306
+ }
280
307
  }
281
308
 
282
309
  return { output, tools_executed, prompt, response_id };
@@ -7,7 +7,7 @@ const { Message, insertMessage } = require('../models/messageModel');
7
7
  const { Thread } = require('../models/threadModel');
8
8
 
9
9
  const { ensureWhatsAppFormat } = require('../helpers/twilioHelper');
10
- const { processTwilioMediaMessage } = require('../helpers/twilioMediaHelper');
10
+ const { enrichMessageWithTwilioMedia } = require('../helpers/twilioMediaHelper');
11
11
 
12
12
  class MongoStorage {
13
13
  constructor(config) {
@@ -46,53 +46,22 @@ class MongoStorage {
46
46
  }
47
47
 
48
48
  async _enrichTwilioMedia(messageData = {}) {
49
- logger.debug('[_enrichTwilioMedia] Input', { messageData });
50
49
  try {
51
50
  const rawMessage = messageData?.raw;
52
- const numMedia = parseInt(rawMessage?.NumMedia || '0', 10);
53
- if (!rawMessage?.From || numMedia <= 0 || !rawMessage.MediaUrl0) return messageData;
54
-
55
- logger.info('[MongoStorage] Detected Twilio media message', {
56
- from: rawMessage.From,
57
- numMedia
58
- });
59
-
60
- const bucketName = runtimeConfig.get('AWS_S3_BUCKET_NAME');
61
- if (!bucketName) {
62
- logger.warn('[MongoStorage] AWS_S3_BUCKET_NAME not configured. Skipping media upload.');
63
- return messageData;
64
- }
65
-
66
- const mediaItems = await processTwilioMediaMessage(rawMessage, bucketName);
67
- if (!mediaItems || mediaItems.length === 0) {
68
- logger.warn('[MongoStorage] Media processing returned no items');
69
- return messageData;
70
- }
71
-
72
- const [primary, ...rest] = mediaItems;
73
- const mediaPayload = rest.length > 0
74
- ? { ...primary, metadata: { ...(primary.metadata || {}), attachments: rest } }
75
- : primary;
76
-
77
- rawMessage.__nexusMediaProcessed = true;
78
-
79
- logger.info('[MongoStorage] Media processed successfully', {
80
- primaryType: mediaPayload.mediaType,
81
- mediaCount: mediaItems.length,
82
- s3Key: mediaPayload.key
83
- });
84
-
85
- const result = {
51
+ if (!rawMessage?.From) return messageData;
52
+
53
+ const result = await enrichMessageWithTwilioMedia(rawMessage);
54
+ if (!result) return messageData;
55
+
56
+ return {
86
57
  ...messageData,
87
- media: mediaPayload,
58
+ media: result.mediaPayload,
88
59
  fileUrl: undefined,
89
- fileType: mediaPayload.mediaType,
60
+ fileType: result.mediaPayload.mediaType,
90
61
  isMedia: true,
91
- body: messageData.body || primary.caption,
92
- caption: primary.caption
62
+ body: messageData.body || result.caption,
63
+ caption: result.caption
93
64
  };
94
- logger.debug('[_enrichTwilioMedia] Output', { result });
95
- return result;
96
65
  } catch (error) {
97
66
  logger.error('[MongoStorage] Failed to enrich Twilio media message', { error });
98
67
  return messageData;
@@ -100,7 +69,6 @@ class MongoStorage {
100
69
  }
101
70
 
102
71
  buildMessageValues(messageData = {}) {
103
- logger.debug('[buildMessageValues] Input', { messageData });
104
72
  const now = new Date();
105
73
  const numero = ensureWhatsAppFormat(messageData.code || messageData.from || '');
106
74
  const isMedia = messageData.isMedia === true || (messageData.fileType && messageData.fileType !== 'text');
@@ -128,7 +96,6 @@ class MongoStorage {
128
96
  response_id: messageData.response_id || null,
129
97
  statusInfo: messageData.statusInfo || null
130
98
  };
131
- logger.debug('[buildMessageValues] Output', { values });
132
99
  return values;
133
100
  }
134
101
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "3.3.16",
3
+ "version": "3.3.18-fix-model",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",