@peopl-health/nexus 4.7.0-dev.0 → 4.7.0-dev.1

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.
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "4.7.0-dev.0",
3
+ "version": "4.7.0-dev.1",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",