ultravisor-beacon 0.0.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.
- package/package.json +30 -0
- package/source/Ultravisor-Beacon-CLI.cjs +143 -0
- package/source/Ultravisor-Beacon-CapabilityAdapter.cjs +116 -0
- package/source/Ultravisor-Beacon-CapabilityManager.cjs +132 -0
- package/source/Ultravisor-Beacon-CapabilityProvider.cjs +129 -0
- package/source/Ultravisor-Beacon-Client.cjs +568 -0
- package/source/Ultravisor-Beacon-ConnectivityHTTP.cjs +52 -0
- package/source/Ultravisor-Beacon-Executor.cjs +500 -0
- package/source/Ultravisor-Beacon-ProviderRegistry.cjs +330 -0
- package/source/Ultravisor-Beacon-Service.cjs +288 -0
- package/source/providers/Ultravisor-Beacon-Provider-FileSystem.cjs +331 -0
- package/source/providers/Ultravisor-Beacon-Provider-LLM.cjs +966 -0
- package/source/providers/Ultravisor-Beacon-Provider-Shell.cjs +95 -0
- package/test/Ultravisor-Beacon-Service_tests.js +608 -0
|
@@ -0,0 +1,966 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ultravisor Beacon Provider — LLM
|
|
3
|
+
*
|
|
4
|
+
* Built-in provider that wraps LLM API calls for multiple backends:
|
|
5
|
+
* - openai — OpenAI API (GPT-4, etc.)
|
|
6
|
+
* - anthropic — Anthropic API (Claude)
|
|
7
|
+
* - ollama — Local Ollama instance
|
|
8
|
+
* - openai-compatible — Any OpenAI-compatible API
|
|
9
|
+
*
|
|
10
|
+
* Capability: 'LLM'
|
|
11
|
+
* Actions: 'ChatCompletion' — Send messages, get completion
|
|
12
|
+
* 'Embedding' — Generate text embeddings
|
|
13
|
+
* 'ToolUse' — Chat completion with tool definitions
|
|
14
|
+
*
|
|
15
|
+
* Provider config:
|
|
16
|
+
* Backend {string} — 'openai' | 'anthropic' | 'ollama' | 'openai-compatible'
|
|
17
|
+
* BaseURL {string} — API endpoint base URL
|
|
18
|
+
* APIKey {string} — API key or $ENV_VAR_NAME for env resolution
|
|
19
|
+
* Model {string} — Default model name
|
|
20
|
+
* DefaultParameters {object} — { Temperature, MaxTokens, TopP }
|
|
21
|
+
* TimeoutMs {number} — Per-request timeout (default: 120000)
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const libHttp = require('http');
|
|
25
|
+
const libHttps = require('https');
|
|
26
|
+
const libUrl = require('url');
|
|
27
|
+
|
|
28
|
+
const libBeaconCapabilityProvider = require('../Ultravisor-Beacon-CapabilityProvider.cjs');
|
|
29
|
+
|
|
30
|
+
class UltravisorBeaconProviderLLM extends libBeaconCapabilityProvider
|
|
31
|
+
{
|
|
32
|
+
constructor(pProviderConfig)
|
|
33
|
+
{
|
|
34
|
+
super(pProviderConfig);
|
|
35
|
+
|
|
36
|
+
this.Name = 'LLM';
|
|
37
|
+
this.Capability = 'LLM';
|
|
38
|
+
|
|
39
|
+
this._Backend = this._ProviderConfig.Backend || 'openai';
|
|
40
|
+
this._BaseURL = this._ProviderConfig.BaseURL || '';
|
|
41
|
+
this._APIKeyConfig = this._ProviderConfig.APIKey || '';
|
|
42
|
+
this._Model = this._ProviderConfig.Model || '';
|
|
43
|
+
this._DefaultParameters = this._ProviderConfig.DefaultParameters || {};
|
|
44
|
+
this._TimeoutMs = this._ProviderConfig.TimeoutMs || 120000;
|
|
45
|
+
|
|
46
|
+
// Resolved at initialize time
|
|
47
|
+
this._ResolvedAPIKey = '';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get actions()
|
|
51
|
+
{
|
|
52
|
+
return {
|
|
53
|
+
'ChatCompletion':
|
|
54
|
+
{
|
|
55
|
+
Description: 'Send messages to an LLM and receive a completion.',
|
|
56
|
+
SettingsSchema:
|
|
57
|
+
[
|
|
58
|
+
{ Name: 'Messages', DataType: 'String', Required: false, Description: 'JSON array of message objects [{role, content}]' },
|
|
59
|
+
{ Name: 'SystemPrompt', DataType: 'String', Required: false, Description: 'System prompt text (prepended as system message)' },
|
|
60
|
+
{ Name: 'Model', DataType: 'String', Required: false, Description: 'Override model name' },
|
|
61
|
+
{ Name: 'Temperature', DataType: 'Number', Required: false, Description: 'Sampling temperature (0-2)' },
|
|
62
|
+
{ Name: 'MaxTokens', DataType: 'Number', Required: false, Description: 'Maximum tokens to generate' },
|
|
63
|
+
{ Name: 'TopP', DataType: 'Number', Required: false, Description: 'Nucleus sampling parameter' },
|
|
64
|
+
{ Name: 'StopSequences', DataType: 'String', Required: false, Description: 'JSON array of stop sequences' },
|
|
65
|
+
{ Name: 'ResponseFormat', DataType: 'String', Required: false, Description: '"text" or "json_object"' }
|
|
66
|
+
]
|
|
67
|
+
},
|
|
68
|
+
'Embedding':
|
|
69
|
+
{
|
|
70
|
+
Description: 'Generate embeddings for text input.',
|
|
71
|
+
SettingsSchema:
|
|
72
|
+
[
|
|
73
|
+
{ Name: 'Text', DataType: 'String', Required: true, Description: 'Text to embed (string or JSON array for batch)' },
|
|
74
|
+
{ Name: 'Model', DataType: 'String', Required: false, Description: 'Override embedding model' }
|
|
75
|
+
]
|
|
76
|
+
},
|
|
77
|
+
'ToolUse':
|
|
78
|
+
{
|
|
79
|
+
Description: 'Chat completion with tool/function definitions.',
|
|
80
|
+
SettingsSchema:
|
|
81
|
+
[
|
|
82
|
+
{ Name: 'Messages', DataType: 'String', Required: true, Description: 'JSON array of message objects' },
|
|
83
|
+
{ Name: 'Tools', DataType: 'String', Required: true, Description: 'JSON array of tool definitions' },
|
|
84
|
+
{ Name: 'Model', DataType: 'String', Required: false, Description: 'Override model name' },
|
|
85
|
+
{ Name: 'ToolChoice', DataType: 'String', Required: false, Description: '"auto", "none", or specific tool name' },
|
|
86
|
+
{ Name: 'Temperature', DataType: 'Number', Required: false, Description: 'Sampling temperature' },
|
|
87
|
+
{ Name: 'MaxTokens', DataType: 'Number', Required: false, Description: 'Maximum tokens to generate' }
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Resolve the API key from config. Supports $ENV_VAR_NAME syntax.
|
|
95
|
+
*/
|
|
96
|
+
_resolveAPIKey(pKeyConfig)
|
|
97
|
+
{
|
|
98
|
+
if (!pKeyConfig)
|
|
99
|
+
{
|
|
100
|
+
return '';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (pKeyConfig.startsWith('$'))
|
|
104
|
+
{
|
|
105
|
+
let tmpEnvVar = pKeyConfig.substring(1);
|
|
106
|
+
return process.env[tmpEnvVar] || '';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return pKeyConfig;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Validate connectivity during initialization.
|
|
114
|
+
*/
|
|
115
|
+
initialize(fCallback)
|
|
116
|
+
{
|
|
117
|
+
this._ResolvedAPIKey = this._resolveAPIKey(this._APIKeyConfig);
|
|
118
|
+
|
|
119
|
+
if (!this._BaseURL)
|
|
120
|
+
{
|
|
121
|
+
console.warn(`[LLM] No BaseURL configured for provider "${this.Name}".`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!this._Model)
|
|
125
|
+
{
|
|
126
|
+
console.warn(`[LLM] No default Model configured for provider "${this.Name}".`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// For Ollama, check that the server is reachable
|
|
130
|
+
if (this._Backend === 'ollama' && this._BaseURL)
|
|
131
|
+
{
|
|
132
|
+
let tmpParsed = new URL(this._BaseURL);
|
|
133
|
+
let tmpLib = tmpParsed.protocol === 'https:' ? libHttps : libHttp;
|
|
134
|
+
let tmpRequest = tmpLib.request(
|
|
135
|
+
{
|
|
136
|
+
hostname: tmpParsed.hostname,
|
|
137
|
+
port: tmpParsed.port,
|
|
138
|
+
path: '/api/tags',
|
|
139
|
+
method: 'GET',
|
|
140
|
+
timeout: 5000
|
|
141
|
+
},
|
|
142
|
+
function (pResponse)
|
|
143
|
+
{
|
|
144
|
+
// Consume response body to free the socket
|
|
145
|
+
pResponse.resume();
|
|
146
|
+
console.log(`[LLM] Ollama server reachable at ${tmpParsed.hostname}:${tmpParsed.port}`);
|
|
147
|
+
return fCallback(null);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
tmpRequest.on('error', function (pError)
|
|
151
|
+
{
|
|
152
|
+
console.warn(`[LLM] Ollama server not reachable at ${tmpParsed.hostname}:${tmpParsed.port}: ${pError.message}`);
|
|
153
|
+
// Non-fatal — the server may come online later
|
|
154
|
+
return fCallback(null);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
tmpRequest.on('timeout', function ()
|
|
158
|
+
{
|
|
159
|
+
tmpRequest.destroy();
|
|
160
|
+
console.warn(`[LLM] Ollama server connection timed out.`);
|
|
161
|
+
return fCallback(null);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
tmpRequest.end();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
console.log(`[LLM] Provider initialized: backend=${this._Backend}, model=${this._Model}`);
|
|
169
|
+
return fCallback(null);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Route execution to the appropriate action handler.
|
|
174
|
+
*/
|
|
175
|
+
execute(pAction, pWorkItem, pContext, fCallback, fReportProgress)
|
|
176
|
+
{
|
|
177
|
+
switch (pAction)
|
|
178
|
+
{
|
|
179
|
+
case 'ChatCompletion':
|
|
180
|
+
return this._executeChatCompletion(pWorkItem, pContext, fCallback, fReportProgress);
|
|
181
|
+
case 'Embedding':
|
|
182
|
+
return this._executeEmbedding(pWorkItem, pContext, fCallback, fReportProgress);
|
|
183
|
+
case 'ToolUse':
|
|
184
|
+
return this._executeToolUse(pWorkItem, pContext, fCallback, fReportProgress);
|
|
185
|
+
default:
|
|
186
|
+
return fCallback(new Error(`LLM Provider: unknown action "${pAction}".`));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── ChatCompletion ──────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
_executeChatCompletion(pWorkItem, pContext, fCallback, fReportProgress)
|
|
193
|
+
{
|
|
194
|
+
let tmpSettings = pWorkItem.Settings || {};
|
|
195
|
+
let tmpMessages = this._parseMessages(tmpSettings);
|
|
196
|
+
|
|
197
|
+
if (!tmpMessages || tmpMessages.length === 0)
|
|
198
|
+
{
|
|
199
|
+
return fCallback(null, {
|
|
200
|
+
Outputs: { Content: '', Model: '', FinishReason: 'error', PromptTokens: 0, CompletionTokens: 0, TotalTokens: 0, Result: '' },
|
|
201
|
+
Log: ['LLM ChatCompletion: no messages provided.']
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let tmpModel = tmpSettings.Model || this._Model;
|
|
206
|
+
let tmpRequestBody = this._buildChatRequestBody(tmpMessages, tmpModel, tmpSettings, false);
|
|
207
|
+
let tmpRequestOptions = this._buildRequestOptions('chat', tmpRequestBody);
|
|
208
|
+
|
|
209
|
+
console.log(` [LLM] ChatCompletion: model=${tmpModel}, messages=${tmpMessages.length}`);
|
|
210
|
+
|
|
211
|
+
this._makeRequest(tmpRequestOptions, tmpRequestBody, fReportProgress,
|
|
212
|
+
(pError, pResponseBody) =>
|
|
213
|
+
{
|
|
214
|
+
if (pError)
|
|
215
|
+
{
|
|
216
|
+
return fCallback(null, {
|
|
217
|
+
Outputs: { Content: '', Model: tmpModel, FinishReason: 'error', PromptTokens: 0, CompletionTokens: 0, TotalTokens: 0, Result: '' },
|
|
218
|
+
Log: [`LLM ChatCompletion failed: ${pError.message}`]
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
let tmpParsed = this._parseChatResponse(pResponseBody);
|
|
223
|
+
|
|
224
|
+
return fCallback(null, {
|
|
225
|
+
Outputs:
|
|
226
|
+
{
|
|
227
|
+
Content: tmpParsed.Content,
|
|
228
|
+
Model: tmpParsed.Model || tmpModel,
|
|
229
|
+
FinishReason: tmpParsed.FinishReason,
|
|
230
|
+
PromptTokens: tmpParsed.PromptTokens,
|
|
231
|
+
CompletionTokens: tmpParsed.CompletionTokens,
|
|
232
|
+
TotalTokens: tmpParsed.TotalTokens,
|
|
233
|
+
Result: tmpParsed.Content
|
|
234
|
+
},
|
|
235
|
+
Log: [`LLM ChatCompletion: model=${tmpParsed.Model || tmpModel}, tokens=${tmpParsed.TotalTokens}, finish=${tmpParsed.FinishReason}`]
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ── Embedding ───────────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
_executeEmbedding(pWorkItem, pContext, fCallback, fReportProgress)
|
|
243
|
+
{
|
|
244
|
+
let tmpSettings = pWorkItem.Settings || {};
|
|
245
|
+
let tmpText = tmpSettings.Text || '';
|
|
246
|
+
|
|
247
|
+
if (!tmpText)
|
|
248
|
+
{
|
|
249
|
+
return fCallback(null, {
|
|
250
|
+
Outputs: { Embedding: '[]', Dimensions: 0, Model: '', Result: '' },
|
|
251
|
+
Log: ['LLM Embedding: no text provided.']
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
let tmpModel = tmpSettings.Model || this._Model;
|
|
256
|
+
let tmpRequestBody = this._buildEmbeddingRequestBody(tmpText, tmpModel);
|
|
257
|
+
let tmpRequestOptions = this._buildRequestOptions('embedding', tmpRequestBody);
|
|
258
|
+
|
|
259
|
+
console.log(` [LLM] Embedding: model=${tmpModel}`);
|
|
260
|
+
|
|
261
|
+
this._makeRequest(tmpRequestOptions, tmpRequestBody, fReportProgress,
|
|
262
|
+
(pError, pResponseBody) =>
|
|
263
|
+
{
|
|
264
|
+
if (pError)
|
|
265
|
+
{
|
|
266
|
+
return fCallback(null, {
|
|
267
|
+
Outputs: { Embedding: '[]', Dimensions: 0, Model: tmpModel, Result: '' },
|
|
268
|
+
Log: [`LLM Embedding failed: ${pError.message}`]
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
let tmpParsed = this._parseEmbeddingResponse(pResponseBody);
|
|
273
|
+
|
|
274
|
+
return fCallback(null, {
|
|
275
|
+
Outputs:
|
|
276
|
+
{
|
|
277
|
+
Embedding: tmpParsed.Embedding,
|
|
278
|
+
Dimensions: tmpParsed.Dimensions,
|
|
279
|
+
Model: tmpParsed.Model || tmpModel,
|
|
280
|
+
Result: tmpParsed.Embedding
|
|
281
|
+
},
|
|
282
|
+
Log: [`LLM Embedding: model=${tmpParsed.Model || tmpModel}, dimensions=${tmpParsed.Dimensions}`]
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ── ToolUse ─────────────────────────────────────────────────
|
|
288
|
+
|
|
289
|
+
_executeToolUse(pWorkItem, pContext, fCallback, fReportProgress)
|
|
290
|
+
{
|
|
291
|
+
let tmpSettings = pWorkItem.Settings || {};
|
|
292
|
+
let tmpMessages = this._parseMessages(tmpSettings);
|
|
293
|
+
let tmpTools = this._safeParseJSON(tmpSettings.Tools, []);
|
|
294
|
+
|
|
295
|
+
if (!tmpMessages || tmpMessages.length === 0)
|
|
296
|
+
{
|
|
297
|
+
return fCallback(null, {
|
|
298
|
+
Outputs: { Content: '', ToolCalls: '[]', Model: '', FinishReason: 'error', PromptTokens: 0, CompletionTokens: 0, TotalTokens: 0, Result: '' },
|
|
299
|
+
Log: ['LLM ToolUse: no messages provided.']
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (!tmpTools || tmpTools.length === 0)
|
|
304
|
+
{
|
|
305
|
+
return fCallback(null, {
|
|
306
|
+
Outputs: { Content: '', ToolCalls: '[]', Model: '', FinishReason: 'error', PromptTokens: 0, CompletionTokens: 0, TotalTokens: 0, Result: '' },
|
|
307
|
+
Log: ['LLM ToolUse: no tools provided.']
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
let tmpModel = tmpSettings.Model || this._Model;
|
|
312
|
+
let tmpRequestBody = this._buildChatRequestBody(tmpMessages, tmpModel, tmpSettings, true);
|
|
313
|
+
|
|
314
|
+
// Add tools to the request body
|
|
315
|
+
this._addToolsToRequestBody(tmpRequestBody, tmpTools, tmpSettings.ToolChoice);
|
|
316
|
+
|
|
317
|
+
let tmpRequestOptions = this._buildRequestOptions('chat', tmpRequestBody);
|
|
318
|
+
|
|
319
|
+
console.log(` [LLM] ToolUse: model=${tmpModel}, messages=${tmpMessages.length}, tools=${tmpTools.length}`);
|
|
320
|
+
|
|
321
|
+
this._makeRequest(tmpRequestOptions, tmpRequestBody, fReportProgress,
|
|
322
|
+
(pError, pResponseBody) =>
|
|
323
|
+
{
|
|
324
|
+
if (pError)
|
|
325
|
+
{
|
|
326
|
+
return fCallback(null, {
|
|
327
|
+
Outputs: { Content: '', ToolCalls: '[]', Model: tmpModel, FinishReason: 'error', PromptTokens: 0, CompletionTokens: 0, TotalTokens: 0, Result: '' },
|
|
328
|
+
Log: [`LLM ToolUse failed: ${pError.message}`]
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
let tmpParsed = this._parseToolUseResponse(pResponseBody);
|
|
333
|
+
|
|
334
|
+
return fCallback(null, {
|
|
335
|
+
Outputs:
|
|
336
|
+
{
|
|
337
|
+
Content: tmpParsed.Content,
|
|
338
|
+
ToolCalls: tmpParsed.ToolCalls,
|
|
339
|
+
Model: tmpParsed.Model || tmpModel,
|
|
340
|
+
FinishReason: tmpParsed.FinishReason,
|
|
341
|
+
PromptTokens: tmpParsed.PromptTokens,
|
|
342
|
+
CompletionTokens: tmpParsed.CompletionTokens,
|
|
343
|
+
TotalTokens: tmpParsed.TotalTokens,
|
|
344
|
+
Result: tmpParsed.Content
|
|
345
|
+
},
|
|
346
|
+
Log: [`LLM ToolUse: model=${tmpParsed.Model || tmpModel}, tokens=${tmpParsed.TotalTokens}, finish=${tmpParsed.FinishReason}, tool_calls=${tmpParsed.ToolCallCount}`]
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ── Message Parsing ─────────────────────────────────────────
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Parse messages from settings. Supports Messages JSON array
|
|
355
|
+
* and SystemPrompt convenience field.
|
|
356
|
+
*/
|
|
357
|
+
_parseMessages(pSettings)
|
|
358
|
+
{
|
|
359
|
+
let tmpMessages = [];
|
|
360
|
+
|
|
361
|
+
// Parse Messages JSON if provided
|
|
362
|
+
if (pSettings.Messages)
|
|
363
|
+
{
|
|
364
|
+
let tmpParsed = this._safeParseJSON(pSettings.Messages, null);
|
|
365
|
+
|
|
366
|
+
if (Array.isArray(tmpParsed))
|
|
367
|
+
{
|
|
368
|
+
tmpMessages = tmpParsed;
|
|
369
|
+
}
|
|
370
|
+
else if (typeof pSettings.Messages === 'string' && pSettings.Messages.length > 0)
|
|
371
|
+
{
|
|
372
|
+
// Treat plain string as a single user message
|
|
373
|
+
tmpMessages.push({ role: 'user', content: pSettings.Messages });
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Prepend SystemPrompt if provided and not already in messages
|
|
378
|
+
if (pSettings.SystemPrompt)
|
|
379
|
+
{
|
|
380
|
+
let tmpHasSystem = tmpMessages.some(function (pMsg) { return pMsg.role === 'system'; });
|
|
381
|
+
|
|
382
|
+
if (!tmpHasSystem)
|
|
383
|
+
{
|
|
384
|
+
tmpMessages.unshift({ role: 'system', content: pSettings.SystemPrompt });
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return tmpMessages;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ── Request Building ────────────────────────────────────────
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Build the chat completion request body for the configured backend.
|
|
395
|
+
*/
|
|
396
|
+
_buildChatRequestBody(pMessages, pModel, pSettings, pIsToolUse)
|
|
397
|
+
{
|
|
398
|
+
let tmpTemperature = (pSettings.Temperature !== undefined && pSettings.Temperature !== '')
|
|
399
|
+
? parseFloat(pSettings.Temperature)
|
|
400
|
+
: (this._DefaultParameters.Temperature !== undefined ? this._DefaultParameters.Temperature : undefined);
|
|
401
|
+
|
|
402
|
+
let tmpMaxTokens = (pSettings.MaxTokens !== undefined && pSettings.MaxTokens !== '')
|
|
403
|
+
? parseInt(pSettings.MaxTokens, 10)
|
|
404
|
+
: (this._DefaultParameters.MaxTokens !== undefined ? this._DefaultParameters.MaxTokens : undefined);
|
|
405
|
+
|
|
406
|
+
let tmpTopP = (pSettings.TopP !== undefined && pSettings.TopP !== '')
|
|
407
|
+
? parseFloat(pSettings.TopP)
|
|
408
|
+
: (this._DefaultParameters.TopP !== undefined ? this._DefaultParameters.TopP : undefined);
|
|
409
|
+
|
|
410
|
+
let tmpStopSequences = this._safeParseJSON(pSettings.StopSequences, null);
|
|
411
|
+
|
|
412
|
+
if (this._Backend === 'anthropic')
|
|
413
|
+
{
|
|
414
|
+
return this._buildAnthropicChatBody(pMessages, pModel, tmpTemperature, tmpMaxTokens, tmpTopP, tmpStopSequences);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (this._Backend === 'ollama')
|
|
418
|
+
{
|
|
419
|
+
return this._buildOllamaChatBody(pMessages, pModel, tmpTemperature, tmpMaxTokens, tmpTopP, tmpStopSequences);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// OpenAI and openai-compatible
|
|
423
|
+
return this._buildOpenAIChatBody(pMessages, pModel, tmpTemperature, tmpMaxTokens, tmpTopP, tmpStopSequences, pSettings.ResponseFormat);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
_buildOpenAIChatBody(pMessages, pModel, pTemperature, pMaxTokens, pTopP, pStopSequences, pResponseFormat)
|
|
427
|
+
{
|
|
428
|
+
let tmpBody = {
|
|
429
|
+
model: pModel,
|
|
430
|
+
messages: pMessages
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
if (pTemperature !== undefined)
|
|
434
|
+
{
|
|
435
|
+
tmpBody.temperature = pTemperature;
|
|
436
|
+
}
|
|
437
|
+
if (pMaxTokens !== undefined)
|
|
438
|
+
{
|
|
439
|
+
tmpBody.max_tokens = pMaxTokens;
|
|
440
|
+
}
|
|
441
|
+
if (pTopP !== undefined)
|
|
442
|
+
{
|
|
443
|
+
tmpBody.top_p = pTopP;
|
|
444
|
+
}
|
|
445
|
+
if (pStopSequences)
|
|
446
|
+
{
|
|
447
|
+
tmpBody.stop = pStopSequences;
|
|
448
|
+
}
|
|
449
|
+
if (pResponseFormat === 'json_object')
|
|
450
|
+
{
|
|
451
|
+
tmpBody.response_format = { type: 'json_object' };
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return tmpBody;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
_buildAnthropicChatBody(pMessages, pModel, pTemperature, pMaxTokens, pTopP, pStopSequences)
|
|
458
|
+
{
|
|
459
|
+
// Anthropic separates system from messages
|
|
460
|
+
let tmpSystem = '';
|
|
461
|
+
let tmpMessages = [];
|
|
462
|
+
|
|
463
|
+
for (let i = 0; i < pMessages.length; i++)
|
|
464
|
+
{
|
|
465
|
+
if (pMessages[i].role === 'system')
|
|
466
|
+
{
|
|
467
|
+
tmpSystem = (tmpSystem ? tmpSystem + '\n' : '') + pMessages[i].content;
|
|
468
|
+
}
|
|
469
|
+
else
|
|
470
|
+
{
|
|
471
|
+
tmpMessages.push(pMessages[i]);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
let tmpBody = {
|
|
476
|
+
model: pModel,
|
|
477
|
+
messages: tmpMessages,
|
|
478
|
+
max_tokens: pMaxTokens || 4096
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
if (tmpSystem)
|
|
482
|
+
{
|
|
483
|
+
tmpBody.system = tmpSystem;
|
|
484
|
+
}
|
|
485
|
+
if (pTemperature !== undefined)
|
|
486
|
+
{
|
|
487
|
+
tmpBody.temperature = pTemperature;
|
|
488
|
+
}
|
|
489
|
+
if (pTopP !== undefined)
|
|
490
|
+
{
|
|
491
|
+
tmpBody.top_p = pTopP;
|
|
492
|
+
}
|
|
493
|
+
if (pStopSequences)
|
|
494
|
+
{
|
|
495
|
+
tmpBody.stop_sequences = pStopSequences;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return tmpBody;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
_buildOllamaChatBody(pMessages, pModel, pTemperature, pMaxTokens, pTopP, pStopSequences)
|
|
502
|
+
{
|
|
503
|
+
let tmpBody = {
|
|
504
|
+
model: pModel,
|
|
505
|
+
messages: pMessages,
|
|
506
|
+
stream: false
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
let tmpOptions = {};
|
|
510
|
+
|
|
511
|
+
if (pTemperature !== undefined)
|
|
512
|
+
{
|
|
513
|
+
tmpOptions.temperature = pTemperature;
|
|
514
|
+
}
|
|
515
|
+
if (pMaxTokens !== undefined)
|
|
516
|
+
{
|
|
517
|
+
tmpOptions.num_predict = pMaxTokens;
|
|
518
|
+
}
|
|
519
|
+
if (pTopP !== undefined)
|
|
520
|
+
{
|
|
521
|
+
tmpOptions.top_p = pTopP;
|
|
522
|
+
}
|
|
523
|
+
if (pStopSequences)
|
|
524
|
+
{
|
|
525
|
+
tmpOptions.stop = pStopSequences;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (Object.keys(tmpOptions).length > 0)
|
|
529
|
+
{
|
|
530
|
+
tmpBody.options = tmpOptions;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return tmpBody;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Build embedding request body.
|
|
538
|
+
*/
|
|
539
|
+
_buildEmbeddingRequestBody(pText, pModel)
|
|
540
|
+
{
|
|
541
|
+
if (this._Backend === 'ollama')
|
|
542
|
+
{
|
|
543
|
+
return {
|
|
544
|
+
model: pModel,
|
|
545
|
+
prompt: pText
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// OpenAI / openai-compatible / Anthropic (uses voyage via OpenAI-compat)
|
|
550
|
+
return {
|
|
551
|
+
model: pModel,
|
|
552
|
+
input: pText
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Add tool definitions to an existing request body.
|
|
558
|
+
*/
|
|
559
|
+
_addToolsToRequestBody(pRequestBody, pTools, pToolChoice)
|
|
560
|
+
{
|
|
561
|
+
if (this._Backend === 'anthropic')
|
|
562
|
+
{
|
|
563
|
+
// Anthropic uses a different tool format
|
|
564
|
+
pRequestBody.tools = pTools.map(function (pTool)
|
|
565
|
+
{
|
|
566
|
+
if (pTool.type === 'function')
|
|
567
|
+
{
|
|
568
|
+
// Convert from OpenAI format to Anthropic format
|
|
569
|
+
return {
|
|
570
|
+
name: pTool.function.name,
|
|
571
|
+
description: pTool.function.description || '',
|
|
572
|
+
input_schema: pTool.function.parameters || {}
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
// Already in Anthropic format
|
|
576
|
+
return pTool;
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
if (pToolChoice && pToolChoice !== 'auto' && pToolChoice !== 'none')
|
|
580
|
+
{
|
|
581
|
+
pRequestBody.tool_choice = { type: 'tool', name: pToolChoice };
|
|
582
|
+
}
|
|
583
|
+
else if (pToolChoice === 'none')
|
|
584
|
+
{
|
|
585
|
+
// Anthropic doesn't have a direct 'none' — omit tools instead
|
|
586
|
+
delete pRequestBody.tools;
|
|
587
|
+
}
|
|
588
|
+
else if (pToolChoice === 'auto')
|
|
589
|
+
{
|
|
590
|
+
pRequestBody.tool_choice = { type: 'auto' };
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
else
|
|
594
|
+
{
|
|
595
|
+
// OpenAI / openai-compatible / Ollama
|
|
596
|
+
pRequestBody.tools = pTools;
|
|
597
|
+
|
|
598
|
+
if (pToolChoice)
|
|
599
|
+
{
|
|
600
|
+
if (pToolChoice === 'auto' || pToolChoice === 'none')
|
|
601
|
+
{
|
|
602
|
+
pRequestBody.tool_choice = pToolChoice;
|
|
603
|
+
}
|
|
604
|
+
else
|
|
605
|
+
{
|
|
606
|
+
pRequestBody.tool_choice = { type: 'function', function: { name: pToolChoice } };
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// ── HTTP Request Options ────────────────────────────────────
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Build HTTP request options based on backend and action type.
|
|
616
|
+
*
|
|
617
|
+
* @param {string} pActionType - 'chat' or 'embedding'
|
|
618
|
+
* @param {object} pBody - The request body (used for Content-Length)
|
|
619
|
+
* @returns {{ url: string, options: object, bodyString: string }}
|
|
620
|
+
*/
|
|
621
|
+
_buildRequestOptions(pActionType, pBody)
|
|
622
|
+
{
|
|
623
|
+
let tmpBodyString = JSON.stringify(pBody);
|
|
624
|
+
let tmpPath = '';
|
|
625
|
+
|
|
626
|
+
switch (this._Backend)
|
|
627
|
+
{
|
|
628
|
+
case 'anthropic':
|
|
629
|
+
tmpPath = (pActionType === 'embedding') ? '/v1/embeddings' : '/v1/messages';
|
|
630
|
+
break;
|
|
631
|
+
|
|
632
|
+
case 'ollama':
|
|
633
|
+
tmpPath = (pActionType === 'embedding') ? '/api/embeddings' : '/api/chat';
|
|
634
|
+
break;
|
|
635
|
+
|
|
636
|
+
case 'openai':
|
|
637
|
+
case 'openai-compatible':
|
|
638
|
+
default:
|
|
639
|
+
tmpPath = (pActionType === 'embedding') ? '/v1/embeddings' : '/v1/chat/completions';
|
|
640
|
+
break;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
let tmpParsed = new URL(this._BaseURL);
|
|
644
|
+
|
|
645
|
+
let tmpHeaders = {
|
|
646
|
+
'Content-Type': 'application/json',
|
|
647
|
+
'Content-Length': Buffer.byteLength(tmpBodyString)
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
// Set auth headers per backend
|
|
651
|
+
if (this._Backend === 'anthropic')
|
|
652
|
+
{
|
|
653
|
+
if (this._ResolvedAPIKey)
|
|
654
|
+
{
|
|
655
|
+
tmpHeaders['x-api-key'] = this._ResolvedAPIKey;
|
|
656
|
+
}
|
|
657
|
+
tmpHeaders['anthropic-version'] = '2023-06-01';
|
|
658
|
+
}
|
|
659
|
+
else if (this._Backend !== 'ollama')
|
|
660
|
+
{
|
|
661
|
+
// OpenAI / openai-compatible use Bearer token
|
|
662
|
+
if (this._ResolvedAPIKey)
|
|
663
|
+
{
|
|
664
|
+
tmpHeaders['Authorization'] = 'Bearer ' + this._ResolvedAPIKey;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
return {
|
|
669
|
+
options:
|
|
670
|
+
{
|
|
671
|
+
hostname: tmpParsed.hostname,
|
|
672
|
+
port: tmpParsed.port || (tmpParsed.protocol === 'https:' ? 443 : 80),
|
|
673
|
+
path: tmpPath,
|
|
674
|
+
method: 'POST',
|
|
675
|
+
headers: tmpHeaders,
|
|
676
|
+
timeout: this._TimeoutMs
|
|
677
|
+
},
|
|
678
|
+
bodyString: tmpBodyString,
|
|
679
|
+
protocol: tmpParsed.protocol
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// ── HTTP Transport ──────────────────────────────────────────
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Execute an HTTP/HTTPS request and return the parsed JSON response.
|
|
687
|
+
*
|
|
688
|
+
* @param {object} pRequestInfo - From _buildRequestOptions
|
|
689
|
+
* @param {object} pBody - Original request body (unused, kept for clarity)
|
|
690
|
+
* @param {function} fReportProgress - Optional progress callback
|
|
691
|
+
* @param {function} fCallback - function(pError, pResponseBody)
|
|
692
|
+
*/
|
|
693
|
+
_makeRequest(pRequestInfo, pBody, fReportProgress, fCallback)
|
|
694
|
+
{
|
|
695
|
+
let tmpLib = pRequestInfo.protocol === 'https:' ? libHttps : libHttp;
|
|
696
|
+
|
|
697
|
+
let tmpRequest = tmpLib.request(pRequestInfo.options, function (pResponse)
|
|
698
|
+
{
|
|
699
|
+
let tmpChunks = [];
|
|
700
|
+
let tmpTotalBytes = 0;
|
|
701
|
+
|
|
702
|
+
pResponse.on('data', function (pChunk)
|
|
703
|
+
{
|
|
704
|
+
tmpChunks.push(pChunk);
|
|
705
|
+
tmpTotalBytes += pChunk.length;
|
|
706
|
+
|
|
707
|
+
// Report progress during large responses
|
|
708
|
+
if (fReportProgress && tmpTotalBytes > 0)
|
|
709
|
+
{
|
|
710
|
+
fReportProgress({
|
|
711
|
+
Message: `Receiving response: ${Math.round(tmpTotalBytes / 1024)}KB`,
|
|
712
|
+
Log: []
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
pResponse.on('end', function ()
|
|
718
|
+
{
|
|
719
|
+
let tmpRawBody = Buffer.concat(tmpChunks).toString('utf8');
|
|
720
|
+
let tmpParsedBody = null;
|
|
721
|
+
|
|
722
|
+
try
|
|
723
|
+
{
|
|
724
|
+
tmpParsedBody = JSON.parse(tmpRawBody);
|
|
725
|
+
}
|
|
726
|
+
catch (pParseError)
|
|
727
|
+
{
|
|
728
|
+
return fCallback(new Error(`Failed to parse LLM response as JSON: ${pParseError.message}. Raw: ${tmpRawBody.substring(0, 500)}`));
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// Check for API error responses
|
|
732
|
+
if (pResponse.statusCode >= 400)
|
|
733
|
+
{
|
|
734
|
+
let tmpErrorMsg = tmpParsedBody.error
|
|
735
|
+
? (tmpParsedBody.error.message || JSON.stringify(tmpParsedBody.error))
|
|
736
|
+
: `HTTP ${pResponse.statusCode}`;
|
|
737
|
+
return fCallback(new Error(`LLM API error (${pResponse.statusCode}): ${tmpErrorMsg}`));
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
return fCallback(null, tmpParsedBody);
|
|
741
|
+
});
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
tmpRequest.on('error', function (pError)
|
|
745
|
+
{
|
|
746
|
+
return fCallback(new Error(`LLM request failed: ${pError.message}`));
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
tmpRequest.on('timeout', function ()
|
|
750
|
+
{
|
|
751
|
+
tmpRequest.destroy();
|
|
752
|
+
return fCallback(new Error(`LLM request timed out after ${pRequestInfo.options.timeout}ms.`));
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
tmpRequest.write(pRequestInfo.bodyString);
|
|
756
|
+
tmpRequest.end();
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// ── Response Parsing ────────────────────────────────────────
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Parse a chat completion response into normalized outputs.
|
|
763
|
+
*/
|
|
764
|
+
_parseChatResponse(pResponseBody)
|
|
765
|
+
{
|
|
766
|
+
if (this._Backend === 'anthropic')
|
|
767
|
+
{
|
|
768
|
+
return this._parseAnthropicChatResponse(pResponseBody);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
if (this._Backend === 'ollama')
|
|
772
|
+
{
|
|
773
|
+
return this._parseOllamaChatResponse(pResponseBody);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
return this._parseOpenAIChatResponse(pResponseBody);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
_parseOpenAIChatResponse(pBody)
|
|
780
|
+
{
|
|
781
|
+
let tmpChoice = (pBody.choices && pBody.choices.length > 0) ? pBody.choices[0] : {};
|
|
782
|
+
let tmpMessage = tmpChoice.message || {};
|
|
783
|
+
let tmpUsage = pBody.usage || {};
|
|
784
|
+
|
|
785
|
+
return {
|
|
786
|
+
Content: tmpMessage.content || '',
|
|
787
|
+
Model: pBody.model || '',
|
|
788
|
+
FinishReason: tmpChoice.finish_reason || 'unknown',
|
|
789
|
+
PromptTokens: tmpUsage.prompt_tokens || 0,
|
|
790
|
+
CompletionTokens: tmpUsage.completion_tokens || 0,
|
|
791
|
+
TotalTokens: tmpUsage.total_tokens || 0
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
_parseAnthropicChatResponse(pBody)
|
|
796
|
+
{
|
|
797
|
+
let tmpContent = '';
|
|
798
|
+
|
|
799
|
+
if (pBody.content && Array.isArray(pBody.content))
|
|
800
|
+
{
|
|
801
|
+
for (let i = 0; i < pBody.content.length; i++)
|
|
802
|
+
{
|
|
803
|
+
if (pBody.content[i].type === 'text')
|
|
804
|
+
{
|
|
805
|
+
tmpContent += pBody.content[i].text;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
let tmpUsage = pBody.usage || {};
|
|
811
|
+
|
|
812
|
+
return {
|
|
813
|
+
Content: tmpContent,
|
|
814
|
+
Model: pBody.model || '',
|
|
815
|
+
FinishReason: pBody.stop_reason || 'unknown',
|
|
816
|
+
PromptTokens: tmpUsage.input_tokens || 0,
|
|
817
|
+
CompletionTokens: tmpUsage.output_tokens || 0,
|
|
818
|
+
TotalTokens: (tmpUsage.input_tokens || 0) + (tmpUsage.output_tokens || 0)
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
_parseOllamaChatResponse(pBody)
|
|
823
|
+
{
|
|
824
|
+
let tmpMessage = pBody.message || {};
|
|
825
|
+
|
|
826
|
+
return {
|
|
827
|
+
Content: tmpMessage.content || '',
|
|
828
|
+
Model: pBody.model || '',
|
|
829
|
+
FinishReason: pBody.done ? 'stop' : 'unknown',
|
|
830
|
+
PromptTokens: pBody.prompt_eval_count || 0,
|
|
831
|
+
CompletionTokens: pBody.eval_count || 0,
|
|
832
|
+
TotalTokens: (pBody.prompt_eval_count || 0) + (pBody.eval_count || 0)
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* Parse an embedding response.
|
|
838
|
+
*/
|
|
839
|
+
_parseEmbeddingResponse(pResponseBody)
|
|
840
|
+
{
|
|
841
|
+
if (this._Backend === 'ollama')
|
|
842
|
+
{
|
|
843
|
+
let tmpEmbedding = pResponseBody.embedding || [];
|
|
844
|
+
return {
|
|
845
|
+
Embedding: JSON.stringify(tmpEmbedding),
|
|
846
|
+
Dimensions: tmpEmbedding.length,
|
|
847
|
+
Model: pResponseBody.model || ''
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// OpenAI / openai-compatible
|
|
852
|
+
let tmpData = (pResponseBody.data && pResponseBody.data.length > 0) ? pResponseBody.data[0] : {};
|
|
853
|
+
let tmpEmbedding = tmpData.embedding || [];
|
|
854
|
+
|
|
855
|
+
return {
|
|
856
|
+
Embedding: JSON.stringify(tmpEmbedding),
|
|
857
|
+
Dimensions: tmpEmbedding.length,
|
|
858
|
+
Model: pResponseBody.model || ''
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* Parse a tool use response into normalized outputs.
|
|
864
|
+
*/
|
|
865
|
+
_parseToolUseResponse(pResponseBody)
|
|
866
|
+
{
|
|
867
|
+
if (this._Backend === 'anthropic')
|
|
868
|
+
{
|
|
869
|
+
return this._parseAnthropicToolUseResponse(pResponseBody);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// OpenAI / openai-compatible / Ollama
|
|
873
|
+
return this._parseOpenAIToolUseResponse(pResponseBody);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
_parseOpenAIToolUseResponse(pBody)
|
|
877
|
+
{
|
|
878
|
+
let tmpChoice = (pBody.choices && pBody.choices.length > 0) ? pBody.choices[0] : {};
|
|
879
|
+
let tmpMessage = tmpChoice.message || {};
|
|
880
|
+
let tmpUsage = pBody.usage || {};
|
|
881
|
+
let tmpToolCalls = tmpMessage.tool_calls || [];
|
|
882
|
+
|
|
883
|
+
return {
|
|
884
|
+
Content: tmpMessage.content || '',
|
|
885
|
+
ToolCalls: JSON.stringify(tmpToolCalls),
|
|
886
|
+
ToolCallCount: tmpToolCalls.length,
|
|
887
|
+
Model: pBody.model || '',
|
|
888
|
+
FinishReason: tmpChoice.finish_reason || 'unknown',
|
|
889
|
+
PromptTokens: tmpUsage.prompt_tokens || 0,
|
|
890
|
+
CompletionTokens: tmpUsage.completion_tokens || 0,
|
|
891
|
+
TotalTokens: tmpUsage.total_tokens || 0
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
_parseAnthropicToolUseResponse(pBody)
|
|
896
|
+
{
|
|
897
|
+
let tmpContent = '';
|
|
898
|
+
let tmpToolCalls = [];
|
|
899
|
+
|
|
900
|
+
if (pBody.content && Array.isArray(pBody.content))
|
|
901
|
+
{
|
|
902
|
+
for (let i = 0; i < pBody.content.length; i++)
|
|
903
|
+
{
|
|
904
|
+
if (pBody.content[i].type === 'text')
|
|
905
|
+
{
|
|
906
|
+
tmpContent += pBody.content[i].text;
|
|
907
|
+
}
|
|
908
|
+
else if (pBody.content[i].type === 'tool_use')
|
|
909
|
+
{
|
|
910
|
+
// Normalize to OpenAI-style tool_calls format for consistency
|
|
911
|
+
tmpToolCalls.push({
|
|
912
|
+
id: pBody.content[i].id,
|
|
913
|
+
type: 'function',
|
|
914
|
+
function: {
|
|
915
|
+
name: pBody.content[i].name,
|
|
916
|
+
arguments: JSON.stringify(pBody.content[i].input)
|
|
917
|
+
}
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
let tmpUsage = pBody.usage || {};
|
|
924
|
+
|
|
925
|
+
return {
|
|
926
|
+
Content: tmpContent,
|
|
927
|
+
ToolCalls: JSON.stringify(tmpToolCalls),
|
|
928
|
+
ToolCallCount: tmpToolCalls.length,
|
|
929
|
+
Model: pBody.model || '',
|
|
930
|
+
FinishReason: pBody.stop_reason || 'unknown',
|
|
931
|
+
PromptTokens: tmpUsage.input_tokens || 0,
|
|
932
|
+
CompletionTokens: tmpUsage.output_tokens || 0,
|
|
933
|
+
TotalTokens: (tmpUsage.input_tokens || 0) + (tmpUsage.output_tokens || 0)
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// ── Utilities ───────────────────────────────────────────────
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* Safely parse a JSON string, returning a fallback on failure.
|
|
941
|
+
*/
|
|
942
|
+
_safeParseJSON(pString, pFallback)
|
|
943
|
+
{
|
|
944
|
+
if (!pString || typeof pString !== 'string')
|
|
945
|
+
{
|
|
946
|
+
return pFallback;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
try
|
|
950
|
+
{
|
|
951
|
+
return JSON.parse(pString);
|
|
952
|
+
}
|
|
953
|
+
catch (pError)
|
|
954
|
+
{
|
|
955
|
+
return pFallback;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
shutdown(fCallback)
|
|
960
|
+
{
|
|
961
|
+
console.log(`[LLM] Provider "${this.Name}" shutting down (backend=${this._Backend}).`);
|
|
962
|
+
return fCallback(null);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
module.exports = UltravisorBeaconProviderLLM;
|