@peopl-health/nexus 5.0.0-dev.453 → 5.0.0-dev.458

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.
@@ -5,7 +5,6 @@ const { withTracing } = require('../../utils/tracingDecorator');
5
5
  const { logger } = require('../../utils/logger');
6
6
 
7
7
  const { getRecordByFilter } = require('../../services/airtableService.js');
8
- const { withThreadRecovery } = require('./threadRecoveryHelper');
9
8
 
10
9
  function getCurRow(baseID, code) {
11
10
  if (code.endsWith('@g.us')) {
@@ -29,11 +28,7 @@ const runAssistantAndWait = async ({ thread, assistant, message = null, runConfi
29
28
  const { polling, tools: configTools, ...config } = runConfig;
30
29
  const tools = assistant.getToolSchemas?.() || configTools || [];
31
30
 
32
- return withThreadRecovery(
33
- (currentThread = thread) => provider.executeRun({ thread: currentThread, message, assistant, tools, config, polling }),
34
- thread,
35
- variant
36
- );
31
+ return provider.executeRun({ thread, message, assistant, tools, config, polling });
37
32
  };
38
33
 
39
34
  const runAssistantWithRetries = async (thread, assistant, runConfig, patientReply = null) => {
@@ -3,6 +3,7 @@ const { OpenAIResponsesProvider } = require('./providers/OpenAIResponsesProvider
3
3
  const { handleFunctionCalls } = require('./providers/OpenAIResponsesProviderTools');
4
4
  const { OpenRouterProvider } = require('./providers/OpenRouterProvider');
5
5
  const { AnthropicProvider } = require('./providers/AnthropicProvider');
6
+ const { GatewayProvider } = require('./providers/GatewayProvider');
6
7
 
7
8
  const { composePrompt, resolveTools, clearCache } = require('./services/promptComposerService');
8
9
  const { createAssistant, preProcessMessages, switchAssistant } = require('./services/assistantService');
@@ -15,7 +16,6 @@ const { BaseAssistant } = require('./assistants/BaseAssistant');
15
16
  const { AssistantProcessor } = require('./AssistantProcessor');
16
17
 
17
18
  const { getCurRow, runAssistantAndWait, runAssistantWithRetries } = require('./helpers/assistantHelper');
18
- const { withThreadRecovery } = require('./helpers/threadRecoveryHelper');
19
19
  const { analyzeImage, buildPrompt } = require('./helpers/llmsHelper');
20
20
 
21
21
  const { getProviderVariant, getAnthropicClient, setOpenAIProvider, getOpenAIProvider, requireOpenAIProvider, configureOpenAIProvider } = require('./config/llmConfig');
@@ -30,6 +30,7 @@ module.exports = {
30
30
  handleFunctionCalls,
31
31
  OpenRouterProvider,
32
32
  AnthropicProvider,
33
+ GatewayProvider,
33
34
  composePrompt,
34
35
  resolveTools,
35
36
  clearCache,
@@ -47,7 +48,6 @@ module.exports = {
47
48
  getCurRow,
48
49
  runAssistantAndWait,
49
50
  runAssistantWithRetries,
50
- withThreadRecovery,
51
51
  analyzeImage,
52
52
  buildPrompt,
53
53
  getProviderVariant,
@@ -51,7 +51,7 @@ class BaseLLMProvider {
51
51
 
52
52
  const toolSchemas = this._resolveToolSchemas({ assistant, toolIds, filtered, toolDescriptions });
53
53
  const devContent = this._buildInstructions({ resolvedPrompt, toolSchemas, prePromptResult, additionalInstructions });
54
- const input = this._buildInput({ context, prePromptResult, additionalInstructions, promptVariables });
54
+ const input = this._buildInput({ context, prePromptResult, additionalInstructions, promptVariables, modelConfig });
55
55
 
56
56
  trace?.setPrompt({ resolved: devContent, resolvedPresetId, resolvedPresetVersion });
57
57
 
@@ -137,13 +137,13 @@ class BaseLLMProvider {
137
137
  return { role: 'user', content: CONVERSATION_CONTINUATION };
138
138
  }
139
139
 
140
- _buildInput({ context, prePromptResult, additionalInstructions, promptVariables }) {
140
+ _buildInput({ context, prePromptResult, additionalInstructions, promptVariables, modelConfig = null }) {
141
141
  let messages = this._convertItemsToApiFormat(context || []);
142
142
  if (Array.isArray(prePromptResult?.additionalMessages) && prePromptResult.additionalMessages.length > 0) {
143
143
  messages = [...messages, ...this._convertItemsToApiFormat(prePromptResult.additionalMessages)];
144
144
  }
145
145
  if (additionalInstructions) {
146
- messages = [...messages, this._operatorInstructionTurn(additionalInstructions)];
146
+ messages = [...messages, this._operatorInstructionTurn(additionalInstructions, modelConfig)];
147
147
  }
148
148
  return this._sanitizeInput([...this._buildMemoryMessage(promptVariables), ...messages]);
149
149
  }
@@ -160,12 +160,8 @@ class BaseLLMProvider {
160
160
  throw new Error(`${this.constructor.name} must implement _invokeModel()`);
161
161
  }
162
162
 
163
- async createConversation() {
164
- throw new Error(`createConversation is not supported by ${this.constructor.name}`);
165
- }
166
-
167
- async listMessages() {
168
- throw new Error(`listMessages is not supported by ${this.constructor.name}`);
163
+ _mapModelConfig(modelConfig) {
164
+ return modelConfig;
169
165
  }
170
166
 
171
167
  async transcribeAudio() {
@@ -420,13 +416,6 @@ class BaseLLMProvider {
420
416
  if (segments.length > 1) logger.debug(`[${this.constructor.name}] Concatenated transcript detected, using last segment`, { segmentCount: segments.length });
421
417
  return segments[segments.length - 1].trim();
422
418
  }
423
-
424
- _ensureId(value) {
425
- if (!value) throw new Error('Identifier value is required');
426
- if (typeof value === 'string') return value;
427
- if (value?.id) return value.id;
428
- throw new Error('Unable to resolve identifier value');
429
- }
430
419
  }
431
420
 
432
421
  module.exports = {
@@ -0,0 +1,84 @@
1
+ const { composePrompt } = require('../services/promptComposerService');
2
+
3
+ const { BaseLLMProvider } = require('./BaseLLMProvider');
4
+
5
+ const VENDOR_VARIANT = {
6
+ openai: 'responses',
7
+ anthropic: 'anthropic',
8
+ openrouter: 'openrouter',
9
+ };
10
+
11
+ class GatewayProvider extends BaseLLMProvider {
12
+ constructor(options = {}) {
13
+ super(options);
14
+ this.variant = 'gateway';
15
+ this.supportsPromptRegistry = true;
16
+ this._subs = {};
17
+ }
18
+
19
+ _defaultSub() {
20
+ return this._sub('openai');
21
+ }
22
+
23
+ getClient() {
24
+ return this._defaultSub().getClient();
25
+ }
26
+
27
+ transcribeAudio(options) {
28
+ return this._defaultSub().transcribeAudio(options);
29
+ }
30
+
31
+ _operatorInstructionTurn(additionalInstructions, modelConfig = null) {
32
+ const { vendor } = this._route(modelConfig?.model);
33
+ return this._sub(vendor)._operatorInstructionTurn(additionalInstructions);
34
+ }
35
+
36
+ _route(model) {
37
+ if (!model) return { vendor: 'openai', model: undefined };
38
+ const slash = model.indexOf('/');
39
+ if (slash === -1) return { vendor: 'openai', model };
40
+ const vendor = model.slice(0, slash);
41
+ if (vendor === 'openai' || vendor === 'anthropic' || vendor === 'openrouter') {
42
+ return { vendor, model: model.slice(slash + 1) };
43
+ }
44
+ return { vendor: 'openrouter', model };
45
+ }
46
+
47
+ _sub(vendor) {
48
+ if (this._subs[vendor]) return this._subs[vendor];
49
+ const variant = VENDOR_VARIANT[vendor];
50
+ const { createLLMProvider } = require('./createLLMProvider');
51
+ try {
52
+ this._subs[vendor] = createLLMProvider({
53
+ variant,
54
+ conversationManager: this.conversationManager,
55
+ sessionManager: this.sessionManager,
56
+ });
57
+ } catch (err) {
58
+ throw new Error(`Gateway routed a model to '${vendor}' but its provider could not be created: ${err.message}`);
59
+ }
60
+ return this._subs[vendor];
61
+ }
62
+
63
+ async _invokeModel(request, options = {}) {
64
+ const { vendor, model } = this._route(request?.modelConfig?.model);
65
+ const modelConfig = { ...request.modelConfig, model };
66
+ return this._sub(vendor)._invokeModel({ ...request, modelConfig }, options);
67
+ }
68
+
69
+ async runStructured(params = {}) {
70
+ const { promptId = null, presetId = null, variables = null } = params;
71
+ let model = null;
72
+ if (promptId || presetId) {
73
+ const composed = await composePrompt({ promptId, presetId, variables });
74
+ model = composed.modelConfig?.model || null;
75
+ }
76
+ const { vendor, model: routedModel } = this._route(model);
77
+ if (vendor !== 'openai') {
78
+ throw new Error(`runStructured is only supported for OpenAI models; routed model '${model}' resolved to '${vendor}'`);
79
+ }
80
+ return this._sub('openai').runStructured({ ...params, model: routedModel });
81
+ }
82
+ }
83
+
84
+ module.exports = { GatewayProvider };
@@ -10,9 +10,6 @@ const { logger } = require('../../utils/logger');
10
10
  const { BaseLLMProvider } = require('./BaseLLMProvider');
11
11
  const { handleFunctionCalls } = require('./OpenAIResponsesProviderTools');
12
12
 
13
- const CONVERSATION_PREFIX = 'conv_';
14
- const MAX_ITEMS_ON_CREATE = 20;
15
- const MAX_ITEMS_PER_BATCH = 20;
16
13
  const DEFAULT_MAX_FUNCTION_ROUNDS = 5;
17
14
 
18
15
  class OpenAIResponsesProvider extends BaseLLMProvider {
@@ -46,76 +43,12 @@ class OpenAIResponsesProvider extends BaseLLMProvider {
46
43
  this.supportsPromptRegistry = true;
47
44
 
48
45
  this.responses = this.client.responses;
49
- this.conversations = this.client.conversations;
50
46
  }
51
47
 
52
48
  _operatorInstructionTurn(additionalInstructions) {
53
49
  return { role: 'developer', content: `[Instrucción del operador para tu próxima respuesta al paciente]: ${additionalInstructions}` };
54
50
  }
55
51
 
56
- async createConversation({ metadata, messages = [], toolResources } = {}) {
57
- const maxHistoricalMessages = this.conversationManager.maxHistoricalMessages;
58
- const capped = messages.length > maxHistoricalMessages;
59
- const messagesToProcess = capped ? messages.slice(-maxHistoricalMessages) : messages;
60
- if (capped) {
61
- logger.warn(`[createConversation] Capped ${messages.length} → ${maxHistoricalMessages} messages`);
62
- }
63
-
64
- const allItems = this._convertItemsToApiFormat(messagesToProcess);
65
- const initialItems = allItems.slice(0, MAX_ITEMS_ON_CREATE);
66
- const remainingItems = allItems.slice(MAX_ITEMS_ON_CREATE);
67
-
68
- if (remainingItems.length) {
69
- logger.info(`[createConversation] Batching: ${initialItems.length} initial + ${remainingItems.length} remaining`);
70
- }
71
-
72
- const conversation = await this._createConversationWithItems(metadata, initialItems, toolResources);
73
-
74
- if (remainingItems.length) {
75
- try {
76
- await this._addItemsInBatches(conversation.id, remainingItems);
77
- } catch (error) {
78
- logger.error('[createConversation] Failed to add remaining messages', { error: error.message });
79
- }
80
- }
81
-
82
- return conversation;
83
- }
84
-
85
- async _createConversationWithItems(metadata, items, toolResources) {
86
- const payload = {
87
- metadata,
88
- items: items.length ? items : undefined,
89
- tool_resources: toolResources
90
- };
91
- const { result } = await retryWithBackoff(
92
- () => this.conversations.create(payload),
93
- { providerName: this.constructor.name }
94
- );
95
- return result;
96
- }
97
-
98
- async _addItemsInBatches(threadId, items, batchSize = MAX_ITEMS_PER_BATCH) {
99
- if (!items?.length) return;
100
-
101
- const id = this._ensurethreadId(threadId);
102
- for (let i = 0; i < items.length; i += batchSize) {
103
- const batch = this._convertItemsToApiFormat(items.slice(i, i + batchSize));
104
- await retryWithBackoff(
105
- () => this.conversations.items.create(id, { items: batch }),
106
- { providerName: this.constructor.name }
107
- );
108
- }
109
- logger.info(`[_addItemsInBatches] Added ${items.length} messages in ${Math.ceil(items.length / batchSize)} batches`);
110
- }
111
-
112
- async listMessages({ threadId, order = 'desc', limit } = {}) {
113
- return this.client.conversations.items.list(
114
- this._ensurethreadId(threadId),
115
- { order, limit }
116
- );
117
- }
118
-
119
52
  async _invokeModel({
120
53
  instructions, input, toolSchemas = [], toolChoice = 'auto', followUpToolChoice = 'auto',
121
54
  modelConfig, metadata = {}, assistant, phiProcessor = null,
@@ -200,7 +133,7 @@ class OpenAIResponsesProvider extends BaseLLMProvider {
200
133
  return { response: finalResponse, toolCalls: allToolCalls, toolsExecuted: allToolsExecuted, retries: totalRetries, usage: accumulatedUsage };
201
134
  }
202
135
 
203
- async runStructured({ promptId = null, presetId = null, instructions = null, variables = null, input = [], text = null, metadata = null } = {}) {
136
+ async runStructured({ promptId = null, presetId = null, instructions = null, variables = null, input = [], text = null, metadata = null, model = null } = {}) {
204
137
  let modelConfig = null;
205
138
  let resolvedInstructions = instructions;
206
139
  if (promptId || presetId) {
@@ -213,6 +146,7 @@ class OpenAIResponsesProvider extends BaseLLMProvider {
213
146
  ...this._mapModelConfig(modelConfig || {}),
214
147
  instructions: resolvedInstructions || '',
215
148
  };
149
+ if (model) apiConfig.model = model;
216
150
  if (!apiConfig.model) apiConfig.model = this.defaults.responseModel;
217
151
  if (text) apiConfig.text = text;
218
152
  if (metadata) {
@@ -227,25 +161,12 @@ class OpenAIResponsesProvider extends BaseLLMProvider {
227
161
  return result;
228
162
  }
229
163
 
230
- _mapModelConfig(modelConfig) {
231
- return modelConfig;
232
- }
233
-
234
164
  async transcribeAudio({ file, model, language, responseFormat, temperature, prompt } = {}) {
235
165
  return this.client.audio.transcriptions.create({
236
166
  model: model || this.defaults.transcriptionModel,
237
167
  file, language, response_format: responseFormat, temperature, prompt,
238
168
  });
239
169
  }
240
-
241
- _ensurethreadId(value) {
242
- const id = this._ensureId(value);
243
- if (!id.startsWith(CONVERSATION_PREFIX)) {
244
- throw new Error(`Expected conversation id to start with '${CONVERSATION_PREFIX}'`);
245
- }
246
- return id;
247
- }
248
-
249
170
  }
250
171
 
251
172
  module.exports = {
@@ -1,6 +1,11 @@
1
1
  const { OpenAI } = require('openai');
2
2
 
3
+ const { retryWithBackoff } = require('../../utils/retryUtils');
4
+ const { withTiming } = require('../../utils/tracingDecorator');
5
+ const { safeParse } = require('../../utils/jsonUtils');
6
+
3
7
  const { OpenAIResponsesProvider } = require('./OpenAIResponsesProvider');
8
+ const { handleFunctionCalls } = require('./OpenAIResponsesProviderTools');
4
9
 
5
10
  const OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1';
6
11
  const DEFAULT_MODEL_VENDOR = 'openai';
@@ -30,9 +35,146 @@ class OpenRouterProvider extends OpenAIResponsesProvider {
30
35
  this.transcriptionClient = null;
31
36
  }
32
37
 
38
+ async _invokeModel({
39
+ instructions, input, toolSchemas = [], toolChoice = 'auto', followUpToolChoice = 'auto',
40
+ modelConfig, assistant, phiProcessor = null, trace = null
41
+ }, options = {}) {
42
+ const executeTools = options.toolExecutor || handleFunctionCalls;
43
+ let totalRetries = 0;
44
+ const allToolsExecuted = [];
45
+ const allToolCalls = [];
46
+ const accumulatedUsage = { input_tokens: 0, output_tokens: 0, total_tokens: 0 };
47
+ const addUsage = (usage) => {
48
+ if (!usage) return;
49
+ accumulatedUsage.input_tokens += usage.prompt_tokens || 0;
50
+ accumulatedUsage.output_tokens += usage.completion_tokens || 0;
51
+ accumulatedUsage.total_tokens += usage.total_tokens || 0;
52
+ };
53
+
54
+ const messages = this._toChatMessages(instructions, input);
55
+ const apiCallConfig = { ...this._mapModelConfig(modelConfig) };
56
+ if (toolSchemas.length > 0) {
57
+ apiCallConfig.tools = toolSchemas;
58
+ apiCallConfig.tool_choice = toolChoice;
59
+ }
60
+
61
+ const makeAPICall = (msgs) => retryWithBackoff(
62
+ () => this.client.chat.completions.create({ messages: msgs, ...apiCallConfig }),
63
+ { providerName: this.constructor.name }
64
+ );
65
+
66
+ const { result: { result: response, retries }, durationMs } = await withTiming(() => makeAPICall(messages));
67
+ totalRetries += retries;
68
+ addUsage(response.usage);
69
+ let producingIteration = trace?.addIteration({ response, durationMs, apiCallConfig }) || null;
70
+ let finalResponse = response;
71
+ const currentMessages = [...messages];
72
+
73
+ if (assistant && toolSchemas.length > 0) {
74
+ apiCallConfig.tool_choice = followUpToolChoice;
75
+
76
+ for (let round = 1; round <= this.maxFunctionRounds; round++) {
77
+ const message = finalResponse.choices?.[0]?.message;
78
+ const toolCalls = message?.tool_calls || [];
79
+ if (!toolCalls.length) break;
80
+
81
+ const functionCalls = toolCalls.map(tc => ({
82
+ name: tc.function.name,
83
+ arguments: tc.function.arguments,
84
+ call_id: tc.id,
85
+ }));
86
+ allToolCalls.push(...toolCalls.map(tc => ({ name: tc.function.name, arguments: safeParse(tc.function.arguments), call_id: tc.id })));
87
+ const { outputs, toolsExecuted } = await executeTools(functionCalls, assistant, phiProcessor, producingIteration);
88
+
89
+ currentMessages.push(
90
+ message,
91
+ ...outputs.map(o => ({ role: 'tool', tool_call_id: o.call_id, content: this._toToolContent(o.output) }))
92
+ );
93
+ allToolsExecuted.push(...toolsExecuted);
94
+
95
+ const { result: { result: followUp, retries: followUpRetries }, durationMs: followUpDurationMs } = await withTiming(() => makeAPICall(currentMessages));
96
+ totalRetries += followUpRetries;
97
+ addUsage(followUp.usage);
98
+ producingIteration = trace?.addIteration({ response: followUp, durationMs: followUpDurationMs, apiCallConfig }) || null;
99
+ finalResponse = followUp;
100
+ }
101
+ }
102
+
103
+ return { response: this._normalizeResponse(finalResponse), toolCalls: allToolCalls, toolsExecuted: allToolsExecuted, retries: totalRetries, usage: accumulatedUsage };
104
+ }
105
+
106
+ _toChatMessages(instructions, input) {
107
+ const systemParts = [];
108
+ if (instructions) systemParts.push(instructions);
109
+
110
+ const outputsByCallId = new Map();
111
+ for (const item of input || []) {
112
+ if (item.type === 'function_call_output') outputsByCallId.set(item.call_id, item.output);
113
+ }
114
+
115
+ const messages = [];
116
+ for (const item of input || []) {
117
+ const type = item.type || 'message';
118
+ if (type === 'function_call') {
119
+ if (!outputsByCallId.has(item.call_id)) continue;
120
+ messages.push({
121
+ role: 'assistant',
122
+ content: null,
123
+ tool_calls: [{ id: item.call_id, type: 'function', function: { name: item.name, arguments: item.arguments || '{}' } }],
124
+ });
125
+ messages.push({ role: 'tool', tool_call_id: item.call_id, content: this._toToolContent(outputsByCallId.get(item.call_id)) });
126
+ continue;
127
+ }
128
+ if (type === 'function_call_output') continue;
129
+ const content = item.content;
130
+ if (item.role === 'developer' || item.role === 'system') {
131
+ if (content) systemParts.push(content);
132
+ continue;
133
+ }
134
+ messages.push({ role: item.role === 'assistant' ? 'assistant' : 'user', content });
135
+ }
136
+
137
+ const system = systemParts.join('\n\n');
138
+ return system ? [{ role: 'system', content: system }, ...messages] : messages;
139
+ }
140
+
141
+ _toToolContent(output) {
142
+ return typeof output === 'string' ? output : JSON.stringify(output ?? '');
143
+ }
144
+
145
+ _normalizeResponse(response) {
146
+ const choice = response.choices?.[0];
147
+ const outputText = (choice?.message?.content || '').trim();
148
+ const output = outputText
149
+ ? [{ type: 'message', role: 'assistant', content: [{ type: 'output_text', text: outputText }] }]
150
+ : [];
151
+ const usage = response.usage
152
+ ? {
153
+ input_tokens: response.usage.prompt_tokens || 0,
154
+ output_tokens: response.usage.completion_tokens || 0,
155
+ total_tokens: response.usage.total_tokens || 0,
156
+ }
157
+ : undefined;
158
+
159
+ return {
160
+ id: response.id,
161
+ object: 'response',
162
+ model: response.model,
163
+ status: choice?.finish_reason === 'content_filter' ? 'refused' : 'completed',
164
+ stop_reason: choice?.finish_reason,
165
+ output,
166
+ output_text: outputText,
167
+ usage,
168
+ raw: response,
169
+ };
170
+ }
171
+
33
172
  _mapModelConfig(modelConfig) {
34
- const mapped = { ...modelConfig };
173
+ const mapped = { ...(modelConfig || {}) };
174
+ if (mapped.max_tokens == null && mapped.max_output_tokens != null) mapped.max_tokens = mapped.max_output_tokens;
35
175
  delete mapped.store;
176
+ delete mapped.text;
177
+ delete mapped.max_output_tokens;
36
178
  mapped.model = this._toOpenRouterModel(mapped.model || this.defaults.responseModel);
37
179
  return mapped;
38
180
  }
@@ -54,14 +196,6 @@ class OpenRouterProvider extends OpenAIResponsesProvider {
54
196
  file, language, response_format: responseFormat, temperature, prompt,
55
197
  });
56
198
  }
57
-
58
- async createConversation() {
59
- throw new Error('createConversation is not supported by OpenRouterProvider');
60
- }
61
-
62
- async listMessages() {
63
- throw new Error('listMessages is not supported by OpenRouterProvider');
64
- }
65
199
  }
66
200
 
67
201
  module.exports = {
@@ -6,11 +6,13 @@ const { logger } = require('../../utils/logger');
6
6
  const { OpenAIResponsesProvider } = require('./OpenAIResponsesProvider');
7
7
  const { OpenRouterProvider } = require('./OpenRouterProvider');
8
8
  const { AnthropicProvider } = require('./AnthropicProvider');
9
+ const { GatewayProvider } = require('./GatewayProvider');
9
10
 
10
11
  const PROVIDER_VARIANTS = {
11
12
  responses: OpenAIResponsesProvider,
12
13
  openrouter: OpenRouterProvider,
13
14
  anthropic: AnthropicProvider,
15
+ gateway: GatewayProvider,
14
16
  };
15
17
 
16
18
  const VARIANT_API_KEY = {
@@ -14,7 +14,7 @@ const { fetchConversationData, processConversations, startConversation } = requi
14
14
  const { getMessages, countMessages, aggregateMessages } = require('../services/messageService');
15
15
 
16
16
  const { sendMessage } = require('../core/NexusMessaging');
17
- const { withThreadRecovery, getProviderVariant, getOpenAIProvider } = require('../clinical');
17
+ const { getProviderVariant } = require('../clinical');
18
18
 
19
19
  const Message = mongoose.models.Message;
20
20
 
@@ -470,65 +470,45 @@ const markMessagesAsReadController = async (req, res) => {
470
470
  const getOpenAIThreadMessagesController = async (req, res) => {
471
471
  try {
472
472
  const { code } = req.params;
473
- const { limit = 50, order = 'desc', runId } = req.query;
474
- const variant = getProviderVariant();
475
-
473
+ const { order = 'desc' } = req.query;
474
+
476
475
  if (!code) {
477
476
  return res.status(400).json({
478
477
  success: false,
479
478
  error: 'Phone number is required'
480
479
  });
481
480
  }
482
-
483
- let thread = await Thread.findOne({ code: code }).sort({ createdAt: -1 });
481
+
482
+ const thread = await Thread.findOne({ code: code }).sort({ createdAt: -1 });
484
483
  if (!thread) {
485
484
  return res.status(404).json({
486
485
  success: false,
487
- error: 'No active OpenAI thread found for this phone number'
486
+ error: 'No active thread found for this phone number'
488
487
  });
489
488
  }
490
-
491
- const provider = getOpenAIProvider({ instantiate: true, variant });
492
- if (!provider) throw new Error('OpenAI provider not initialized');
493
-
494
- const queryParams = {
495
- threadId: thread.conversation_id,
496
- order,
497
- limit: parseInt(limit),
498
- ...(runId && { runId })
499
- };
500
- let threadRecreated = false;
501
-
502
- const messages = await withThreadRecovery(
503
- async (currentThread = thread) => {
504
- if (currentThread !== thread) {
505
- thread = currentThread;
506
- queryParams.threadId = currentThread.getConversationId();
507
- threadRecreated = true;
508
- }
509
- return await provider.listMessages(queryParams);
510
- },
511
- thread,
512
- variant
489
+
490
+ const parsedLimit = Math.min(Math.max(parseInt(req.query.limit) || 50, 1), 100);
491
+ const messages = await getMessages(
492
+ { numero: code, group_id: null },
493
+ { sort: { createdAt: order === 'asc' ? 1 : -1 }, limit: parsedLimit }
513
494
  );
514
-
495
+ messages.forEach(msg => { msg.body = msg.plainBody || msg.body; });
496
+
515
497
  res.status(200).json({
516
498
  success: true,
517
- variant,
499
+ variant: getProviderVariant(),
518
500
  phoneNumber: code,
519
501
  code,
520
502
  threadId: thread.conversation_id,
521
503
  assistantId: thread.assistant_id,
522
- messages: messages.data || messages,
523
- hasMore: messages.has_more || false,
524
- threadRecreated,
525
- pagination: { limit: parseInt(limit), order }
504
+ messages,
505
+ pagination: { limit: parsedLimit, order }
526
506
  });
527
507
  } catch (error) {
528
- logger.error('Error fetching OpenAI thread messages:', { error: error.message });
529
- res.status(500).json({
530
- success: false,
531
- error: error.message || 'Failed to fetch OpenAI thread messages'
508
+ logger.error('Error fetching thread messages:', { error: error.message });
509
+ res.status(500).json({
510
+ success: false,
511
+ error: error.message || 'Failed to fetch thread messages'
532
512
  });
533
513
  }
534
514
  };
package/lib/index.d.ts CHANGED
@@ -273,14 +273,29 @@ declare module '@peopl-health/nexus' {
273
273
  constructor(options?: { apiKey?: string; organization?: string; client?: any; defaultModels?: Record<string, string>; conversationManager?: any });
274
274
  getVariant(): string;
275
275
  getClient(): any;
276
- createConversation(options?: any): Promise<any>;
277
- listMessages(options?: any): Promise<any>;
278
276
  executeRun(options: { thread: any; assistant: any; message?: string; tools?: any[]; config?: any }): Promise<any>;
279
277
  runConversation(config?: any): Promise<any>;
280
278
  transcribeAudio(options?: any): Promise<any>;
281
279
  }
282
280
 
283
- export function createLLMProvider(config?: { variant?: string; [key: string]: any }): OpenAIResponsesProvider;
281
+ export class GatewayProvider {
282
+ constructor(options?: { conversationManager?: any; sessionManager?: any; [key: string]: any });
283
+ getVariant(): string;
284
+ executeRun(options: { thread: any; assistant: any; message?: string; tools?: any[]; config?: any }): Promise<any>;
285
+ runConversation(config?: any): Promise<any>;
286
+ runStructured(params?: { promptId?: string; presetId?: string; variables?: any; input?: any[]; text?: any; metadata?: any }): Promise<any>;
287
+ }
288
+
289
+ export interface LLMProvider {
290
+ getVariant(): string;
291
+ getClient?(): any;
292
+ executeRun(options: { thread: any; assistant: any; message?: string; tools?: any[]; config?: any }): Promise<any>;
293
+ runConversation(config?: any): Promise<any>;
294
+ runStructured?(params?: { promptId?: string; presetId?: string; variables?: any; input?: any[]; text?: any; metadata?: any }): Promise<any>;
295
+ transcribeAudio?(options?: any): Promise<any>;
296
+ }
297
+
298
+ export function createLLMProvider(config?: { variant?: string; [key: string]: any }): LLMProvider;
284
299
 
285
300
  // Main Nexus Class
286
301
  export interface NexusConfig {
@@ -324,7 +339,7 @@ declare module '@peopl-health/nexus' {
324
339
  getMessaging(): NexusMessaging;
325
340
  getStorage(): MongoStorage | null;
326
341
  getMessageParser(): MessageParser | null;
327
- getLLMProvider(): OpenAIResponsesProvider | null;
342
+ getLLMProvider(): LLMProvider | null;
328
343
  }
329
344
 
330
345
  // Utility Functions
package/lib/index.js CHANGED
@@ -7,6 +7,7 @@ const {
7
7
  OpenAIResponsesProvider,
8
8
  OpenRouterProvider,
9
9
  AnthropicProvider,
10
+ GatewayProvider,
10
11
  configureOpenAIProvider,
11
12
  BaseAssistant,
12
13
  registerAssistant,
@@ -201,6 +202,7 @@ module.exports = {
201
202
  OpenAIResponsesProvider,
202
203
  OpenRouterProvider,
203
204
  AnthropicProvider,
205
+ GatewayProvider,
204
206
  BaseAssistant,
205
207
  registerAssistant,
206
208
  overrideGetAssistantById,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "5.0.0-dev.453",
3
+ "version": "5.0.0-dev.458",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",
@@ -1,50 +0,0 @@
1
- const { createLLMProvider } = require('../providers/createLLMProvider');
2
- const { logger } = require('../../utils/logger');
3
-
4
- const { Thread } = require('../../models/threadModel');
5
-
6
- const THREAD_ERROR_CODES = ['thread_not_found', 'invalid_thread_id'];
7
- const THREAD_ERROR_MESSAGES = ['No thread found', 'thread does not exist', 'Invalid thread'];
8
-
9
- const isThreadNotFoundError = (error) =>
10
- error?.status === 404 ||
11
- THREAD_ERROR_CODES.includes(error?.code) ||
12
- THREAD_ERROR_MESSAGES.some(msg => error?.message?.includes(msg));
13
-
14
- const recreateThread = async (thread, variant) => {
15
- const provider = createLLMProvider({ variant });
16
- const newConversation = await provider.createConversation({
17
- metadata: { code: thread.code }
18
- });
19
-
20
- await Thread.updateOne(
21
- { code: thread.code },
22
- { $set: { conversation_id: newConversation.id } }
23
- );
24
-
25
- logger.info('[threadRecovery] Recreated', {
26
- oldId: thread.conversation_id,
27
- newId: newConversation.id,
28
- code: thread.code
29
- });
30
- return Thread.findOne({ code: thread.code });
31
- };
32
-
33
- const withThreadRecovery = async (operation, thread, variant) => {
34
- try {
35
- return await operation();
36
- } catch (error) {
37
- if (isThreadNotFoundError(error) && thread) {
38
- logger.warn('[threadRecovery] Thread not found, recreating', { code: thread.code });
39
- const recoveredThread = await recreateThread(thread, variant);
40
- return operation(recoveredThread);
41
- }
42
- throw error;
43
- }
44
- };
45
-
46
- module.exports = {
47
- isThreadNotFoundError,
48
- recreateThread,
49
- withThreadRecovery
50
- };