@saltcorn/large-language-model 0.9.11 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/constants.js CHANGED
@@ -7,9 +7,7 @@ const OPENAI_MODELS = [
7
7
  "gpt-5",
8
8
  "gpt-5-mini",
9
9
  "gpt-5-nano",
10
- "gpt-5.1",
11
- "gpt-5.1-mini",
12
- "gpt-5.1-nano",
10
+ "gpt-5.1",
13
11
  "gpt-5.2",
14
12
  "gpt-5.2-pro",
15
13
  "o3",
package/generate.js CHANGED
@@ -24,6 +24,8 @@ const {
24
24
  } = require("ai");
25
25
  const { createOpenAI } = require("@ai-sdk/openai");
26
26
  const OpenAI = require("openai");
27
+ const { ElevenLabsClient } = require("@elevenlabs/elevenlabs-js");
28
+
27
29
  let ollamaMod;
28
30
  if (features.esm_plugins) ollamaMod = require("ollama");
29
31
 
@@ -36,7 +38,7 @@ const getEmbedding = async (config, opts) => {
36
38
  apiKey: config.api_key,
37
39
  embed_model: opts?.embed_model || config.embed_model || config.model,
38
40
  },
39
- opts
41
+ opts,
40
42
  );
41
43
  case "OpenAI":
42
44
  return await getEmbeddingOpenAICompatible(
@@ -45,7 +47,7 @@ const getEmbedding = async (config, opts) => {
45
47
  bearer: opts?.api_key || config.api_key,
46
48
  embed_model: opts?.model || config.embed_model,
47
49
  },
48
- opts
50
+ opts,
49
51
  );
50
52
  case "OpenAI-compatible API":
51
53
  return await getEmbeddingOpenAICompatible(
@@ -59,7 +61,7 @@ const getEmbedding = async (config, opts) => {
59
61
  config.embed_model ||
60
62
  config.model,
61
63
  },
62
- opts
64
+ opts,
63
65
  );
64
66
  case "Local Ollama":
65
67
  if (config.embed_endpoint) {
@@ -72,14 +74,14 @@ const getEmbedding = async (config, opts) => {
72
74
  config.embed_model ||
73
75
  config.model,
74
76
  },
75
- opts
77
+ opts,
76
78
  );
77
79
  } else {
78
80
  if (!ollamaMod) throw new Error("Not implemented for this backend");
79
81
 
80
82
  const { Ollama } = ollamaMod;
81
83
  const ollama = new Ollama(
82
- config.ollama_host ? { host: config.ollama_host } : undefined
84
+ config.ollama_host ? { host: config.ollama_host } : undefined,
83
85
  );
84
86
  const olres = await ollama.embeddings({
85
87
  model: opts?.model || config.embed_model || config.model,
@@ -110,7 +112,7 @@ const getImageGeneration = async (config, opts) => {
110
112
  model: opts?.model || config.model,
111
113
  responses_api: config.responses_api,
112
114
  },
113
- opts
115
+ opts,
114
116
  );
115
117
  default:
116
118
  throw new Error("Image generation not implemented for this backend");
@@ -119,9 +121,22 @@ const getImageGeneration = async (config, opts) => {
119
121
 
120
122
  const getAudioTranscription = async (
121
123
  { backend, apiKey, api_key, provider, ai_sdk_provider },
122
- opts
124
+ opts,
123
125
  ) => {
124
- switch (backend) {
126
+ switch (opts.backend || backend) {
127
+ case "ElevenLabs":
128
+ const transcription = await new ElevenLabsClient({
129
+ apiKey: opts?.api_key || api_key || apiKey,
130
+ }).speechToText.convert({
131
+ file: await (await File.findOne(opts.file)).get_contents(),
132
+ modelId: opts.model || "scribe_v2", // Model to use
133
+ tagAudioEvents: true, // Tag audio events like laughter, applause, etc.
134
+ languageCode: opts.languageCode || "eng", // Language of the audio file. If set to null, the model will detect the language automatically.
135
+ numSpeakers: opts.numSpeakers || null, // Language of the audio file. If set to null, the model will detect the language automatically.
136
+ diarize: !!opts.diarize, // Whether to annotate who is speaking
137
+ diarizationThreshold: opts.diarizationThreshold || null,
138
+ });
139
+ return transcription;
125
140
  case "OpenAI":
126
141
  const client = new OpenAI({
127
142
  apiKey: opts?.api_key || api_key || apiKey,
@@ -129,10 +144,10 @@ const getAudioTranscription = async (
129
144
  const fp = opts.file.location
130
145
  ? opts.file.location
131
146
  : typeof opts.file === "string"
132
- ? await (
133
- await File.findOne(opts.file)
134
- ).location
135
- : null;
147
+ ? await (
148
+ await File.findOne(opts.file)
149
+ ).location
150
+ : null;
136
151
  const model = opts?.model || "whisper-1";
137
152
  const diarize = model === "gpt-4o-transcribe-diarize";
138
153
  const transcript1 = await client.audio.transcriptions.create({
@@ -156,8 +171,8 @@ const getAudioTranscription = async (
156
171
  (Buffer.isBuffer(opts.file)
157
172
  ? opts.file
158
173
  : typeof opts.file === "string"
159
- ? await (await File.findOne(opts.file)).get_contents()
160
- : await opts.file.get_contents());
174
+ ? await (await File.findOne(opts.file)).get_contents()
175
+ : await opts.file.get_contents());
161
176
  const extra = {};
162
177
  if (opts.prompt)
163
178
  extra.providerOptions = {
@@ -178,6 +193,104 @@ const getAudioTranscription = async (
178
193
  }
179
194
  };
180
195
 
196
+ const last = (xs) => xs[xs.length - 1];
197
+
198
+ const toolResponse = async (
199
+ { backend, apiKey, api_key, provider, ai_sdk_provider, responses_api },
200
+ opts,
201
+ ) => {
202
+ let chat = opts.chat;
203
+ let result = opts.prompt;
204
+ //console.log("chat", JSON.stringify(chat, null, 2));
205
+ switch (opts.backend || backend) {
206
+ case "OpenAI":
207
+ {
208
+ let tool_call_chat, tool_call;
209
+ if (!((opts.tool_call_id && opts.tool_name) || opts.tool_call)) {
210
+ if (opts.tool_call) tool_call_chat = opts.tool_call;
211
+ else
212
+ tool_call_chat = last(
213
+ chat.filter((c) => c.tool_calls || c.type === "function_call"),
214
+ );
215
+
216
+ tool_call = tool_call_chat.tool_calls
217
+ ? tool_call_chat.tool_calls[0] //original api
218
+ : tool_call_chat; //responses api
219
+ }
220
+ const content =
221
+ result && typeof result !== "string"
222
+ ? JSON.stringify(result)
223
+ : result || "Action run";
224
+ const new_chat_item = responses_api
225
+ ? {
226
+ type: "function_call_output",
227
+ call_id:
228
+ opts.tool_call?.tool_call_id ||
229
+ opts.tool_call_id ||
230
+ tool_call.call_id,
231
+ output: content,
232
+ }
233
+ : {
234
+ role: "tool",
235
+ tool_call_id:
236
+ opts.tool_call?.tool_call_id ||
237
+ opts.tool_call_id ||
238
+ tool_call.toolCallId ||
239
+ tool_call.id,
240
+ content,
241
+ };
242
+
243
+ chat.push(new_chat_item);
244
+ }
245
+ break;
246
+ case "AI SDK":
247
+ {
248
+ let tool_call, tc;
249
+ if (!((opts.tool_call_id && opts.tool_name) || opts.tool_call)) {
250
+ if (opts.tool_call) tool_call = opts.tool_call;
251
+ else
252
+ tool_call = last(
253
+ chat.filter(
254
+ (c) =>
255
+ c.role === "assistant" &&
256
+ Array.isArray(c.content) &&
257
+ c.content.some((cc) => cc.type === "tool-call"),
258
+ ),
259
+ );
260
+
261
+ tc = tool_call.content[0];
262
+ }
263
+
264
+ chat.push({
265
+ role: "tool",
266
+ content: [
267
+ {
268
+ type: "tool-result",
269
+ toolCallId:
270
+ opts.tool_call?.tool_call_id ||
271
+ opts.tool_call_id ||
272
+ tc.toolCallId,
273
+ toolName:
274
+ opts.tool_call?.tool_name || opts.tool_name || tc.toolName,
275
+ output:
276
+ !result || typeof result === "string"
277
+ ? {
278
+ type: "text",
279
+ value: result || "Action run",
280
+ }
281
+ : {
282
+ type: "json",
283
+ value: JSON.parse(JSON.stringify(result)),
284
+ },
285
+ },
286
+ ],
287
+ });
288
+ }
289
+ break;
290
+ default:
291
+ }
292
+ };
293
+
181
294
  const getCompletion = async (config, opts) => {
182
295
  switch (config.backend) {
183
296
  case "AI SDK":
@@ -187,7 +300,7 @@ const getCompletion = async (config, opts) => {
187
300
  apiKey: config.api_key,
188
301
  model: opts?.model || config.model,
189
302
  },
190
- opts
303
+ opts,
191
304
  );
192
305
  case "OpenAI":
193
306
  return await getCompletionOpenAICompatible(
@@ -199,7 +312,7 @@ const getCompletion = async (config, opts) => {
199
312
  model: opts?.model || config.model,
200
313
  responses_api: config.responses_api,
201
314
  },
202
- opts
315
+ opts,
203
316
  );
204
317
  case "OpenAI-compatible API":
205
318
  return await getCompletionOpenAICompatible(
@@ -213,7 +326,7 @@ const getCompletion = async (config, opts) => {
213
326
  apiKey: opts?.api_key || config.api_key,
214
327
  model: opts?.model || config.model,
215
328
  },
216
- opts
329
+ opts,
217
330
  );
218
331
  case "Local Ollama":
219
332
  return await getCompletionOpenAICompatible(
@@ -223,14 +336,14 @@ const getCompletion = async (config, opts) => {
223
336
  : "http://localhost:11434/v1/chat/completions",
224
337
  model: opts?.model || config.model,
225
338
  },
226
- opts
339
+ opts,
227
340
  );
228
341
  case "Local llama.cpp":
229
342
  //TODO only check if unsafe plugins not allowed
230
343
  const isRoot = db.getTenantSchema() === db.connectObj.default_schema;
231
344
  if (!isRoot)
232
345
  throw new Error(
233
- "llama.cpp inference is not permitted on subdomain tenants"
346
+ "llama.cpp inference is not permitted on subdomain tenants",
234
347
  );
235
348
  let hyperStr = "";
236
349
  if (opts.temperature) hyperStr += ` --temp ${opts.temperature}`;
@@ -240,7 +353,7 @@ const getCompletion = async (config, opts) => {
240
353
 
241
354
  const { stdout, stderr } = await exec(
242
355
  `./main -m ${config.model_path} -p "${opts.prompt}" ${nstr}${hyperStr}`,
243
- { cwd: config.llama_dir }
356
+ { cwd: config.llama_dir },
244
357
  );
245
358
  return stdout;
246
359
  case "Google Vertex AI":
@@ -271,12 +384,13 @@ const getCompletionAISDK = async (
271
384
  systemPrompt,
272
385
  prompt,
273
386
  debugResult,
387
+ appendToChat,
274
388
  debugCollector,
275
389
  chat = [],
276
390
  api_key,
277
391
  endpoint,
278
392
  ...rest
279
- }
393
+ },
280
394
  ) => {
281
395
  const use_model_name = rest.model || model;
282
396
  let model_obj = getAiSdkModel({
@@ -298,7 +412,7 @@ const getCompletionAISDK = async (
298
412
  ...(Array.isArray(chat.content) ? { content: chat.content.map(f) } : {}),
299
413
  };
300
414
  };
301
- const newChat = chat.map(modifyChat);
415
+ const newChat = appendToChat ? chat : chat.map(modifyChat);
302
416
 
303
417
  const body = {
304
418
  ...rest,
@@ -312,6 +426,9 @@ const getCompletionAISDK = async (
312
426
  ...(prompt ? [{ role: "user", content: prompt }] : []),
313
427
  ],
314
428
  };
429
+ if (appendToChat && chat && prompt) {
430
+ chat.push({ role: "user", content: prompt });
431
+ }
315
432
  if (rest.temperature || temperature) {
316
433
  const str_or_num = rest.temperature || temperature;
317
434
  body.temperature = +str_or_num;
@@ -327,6 +444,9 @@ const getCompletionAISDK = async (
327
444
  "gpt-5",
328
445
  "gpt-5-nano",
329
446
  "gpt-5-mini",
447
+ "gpt-5.1",
448
+ "gpt-5.1-codex",
449
+ "gpt-5.2",
330
450
  ].includes(use_model_name)
331
451
  )
332
452
  body.temperature = 0.7;
@@ -335,9 +455,9 @@ const getCompletionAISDK = async (
335
455
  const prevTools = [...body.tools];
336
456
  body.tools = {};
337
457
  prevTools.forEach((t) => {
338
- body.tools[t.function.name] = tool({
339
- description: t.function.description,
340
- inputSchema: jsonSchema(t.function.parameters),
458
+ body.tools[t.name || t.function.name] = tool({
459
+ description: t.description || t.function.description,
460
+ inputSchema: jsonSchema(t.parameters || t.function.parameters),
341
461
  });
342
462
  });
343
463
  }
@@ -352,11 +472,21 @@ const getCompletionAISDK = async (
352
472
  let results;
353
473
  if (rest.streamCallback) {
354
474
  delete body.streamCallback;
355
- results = await streamText(body);
356
- for await (const textPart of results.textStream) {
475
+ const results1 = await streamText(body);
476
+ for await (const textPart of results1.textStream) {
357
477
  rest.streamCallback(textPart);
358
478
  }
479
+ results = {
480
+ response: await results1.response,
481
+ text: await results1.text,
482
+ steps: await results1.steps,
483
+ };
359
484
  } else results = await generateText(body);
485
+
486
+ if (appendToChat && chat) {
487
+ chat.push(...results.response.messages);
488
+ }
489
+
360
490
  if (debugResult)
361
491
  console.log("AI SDK response", JSON.stringify(results, null, 2));
362
492
  else getState().log(6, `AI SDK response ${JSON.stringify(results)}`);
@@ -372,6 +502,14 @@ const getCompletionAISDK = async (
372
502
  content: await results.text,
373
503
  messages: (await results.response).messages,
374
504
  ai_sdk: true,
505
+ hasToolCalls: allToolCalls.length,
506
+ getToolCalls() {
507
+ return allToolCalls.map((tc) => ({
508
+ tool_name: tc.toolName,
509
+ input: tc.input,
510
+ tool_call_id: tc.toolCallId,
511
+ }));
512
+ },
375
513
  };
376
514
  } else return results.text;
377
515
  };
@@ -384,10 +522,11 @@ const getCompletionOpenAICompatible = async (
384
522
  debugResult,
385
523
  debugCollector,
386
524
  chat = [],
525
+ appendToChat,
387
526
  api_key,
388
527
  endpoint,
389
528
  ...rest
390
- }
529
+ },
391
530
  ) => {
392
531
  const headers = {
393
532
  "Content-Type": "application/json",
@@ -425,63 +564,67 @@ const getCompletionOpenAICompatible = async (
425
564
  delete body.streamCallback;
426
565
  }
427
566
  if (responses_api) {
567
+ delete body.tool_choice;
428
568
  for (const tool of body.tools || []) {
429
- if (tool.type !== "function") continue;
569
+ if (tool.type !== "function" || !tool.function) continue;
430
570
  tool.name = tool.function.name;
431
571
  tool.description = tool.function.description;
432
572
  tool.parameters = tool.function.parameters;
433
573
  if (tool.function.required) tool.required = tool.function.required;
434
574
  delete tool.function;
435
575
  }
436
- const newChat = [];
437
- (chat || []).forEach((c) => {
438
- if (c.tool_calls) {
439
- c.tool_calls.forEach((tc) => {
440
- newChat.push({
441
- id: tc.id,
442
- type: "function_call",
443
- call_id: tc.call_id,
444
- name: tc.name,
445
- arguments: tc.arguments,
576
+ let newChat;
577
+ if (!appendToChat) {
578
+ newChat = [];
579
+ (chat || []).forEach((c) => {
580
+ if (c.tool_calls) {
581
+ c.tool_calls.forEach((tc) => {
582
+ newChat.push({
583
+ id: tc.id,
584
+ type: "function_call",
585
+ call_id: tc.call_id,
586
+ name: tc.name,
587
+ arguments: tc.arguments,
588
+ });
446
589
  });
447
- });
448
- } else if (c.content?.image_calls) {
449
- c.content.image_calls.forEach((ic) => {
590
+ } else if (c.content?.image_calls) {
591
+ c.content.image_calls.forEach((ic) => {
592
+ newChat.push({
593
+ ...ic,
594
+ result: undefined,
595
+ filename: undefined,
596
+ });
597
+ });
598
+ } else if (c.content?.mcp_calls) {
599
+ c.content.mcp_calls.forEach((ic) => {
600
+ newChat.push({
601
+ ...ic,
602
+ });
603
+ });
604
+ } else if (c.role === "tool") {
450
605
  newChat.push({
451
- ...ic,
452
- result: undefined,
453
- filename: undefined,
606
+ type: "function_call_output",
607
+ call_id: c.call_id,
608
+ output: c.content,
454
609
  });
455
- });
456
- } else if (c.content?.mcp_calls) {
457
- c.content.mcp_calls.forEach((ic) => {
610
+ } else {
611
+ const fcontent = (c) => {
612
+ if (c.type === "image_url")
613
+ return {
614
+ type: "input_image",
615
+ image_url: c.image_url.url,
616
+ };
617
+ else return c;
618
+ };
458
619
  newChat.push({
459
- ...ic,
620
+ ...c,
621
+ content: Array.isArray(c.content)
622
+ ? c.content.map(fcontent)
623
+ : c.content,
460
624
  });
461
- });
462
- } else if (c.role === "tool") {
463
- newChat.push({
464
- type: "function_call_output",
465
- call_id: c.call_id,
466
- output: c.content,
467
- });
468
- } else {
469
- const fcontent = (c) => {
470
- if (c.type === "image_url")
471
- return {
472
- type: "input_image",
473
- image_url: c.image_url.url,
474
- };
475
- else return c;
476
- };
477
- newChat.push({
478
- ...c,
479
- content: Array.isArray(c.content)
480
- ? c.content.map(fcontent)
481
- : c.content,
482
- });
483
- }
484
- });
625
+ }
626
+ });
627
+ } else newChat = chat;
485
628
  body.input = [
486
629
  {
487
630
  role: "system",
@@ -502,6 +645,9 @@ const getCompletionOpenAICompatible = async (
502
645
  ...(prompt ? [{ role: "user", content: prompt }] : []),
503
646
  ];
504
647
  }
648
+ if (appendToChat && chat && prompt) {
649
+ chat.push({ role: "user", content: prompt });
650
+ }
505
651
  if (debugResult)
506
652
  console.log(
507
653
  "OpenAI request",
@@ -509,14 +655,14 @@ const getCompletionOpenAICompatible = async (
509
655
  "to",
510
656
  chatCompleteEndpoint,
511
657
  "headers",
512
- JSON.stringify(headers)
658
+ JSON.stringify(headers),
513
659
  );
514
660
  else
515
661
  getState().log(
516
662
  6,
517
663
  `OpenAI request ${JSON.stringify(
518
- body
519
- )} to ${chatCompleteEndpoint} headers ${JSON.stringify(headers)}`
664
+ body,
665
+ )} to ${chatCompleteEndpoint} headers ${JSON.stringify(headers)}`,
520
666
  );
521
667
  if (debugCollector) debugCollector.request = body;
522
668
  const reqTimeStart = Date.now();
@@ -608,7 +754,7 @@ const getCompletionOpenAICompatible = async (
608
754
  : streamParts.join("");
609
755
  }
610
756
  const results = await rawResponse.json();
611
- //console.log("results", results);
757
+
612
758
  if (debugResult)
613
759
  console.log("OpenAI response", JSON.stringify(results, null, 2));
614
760
  else getState().log(6, `OpenAI response ${JSON.stringify(results)}`);
@@ -618,36 +764,49 @@ const getCompletionOpenAICompatible = async (
618
764
  }
619
765
 
620
766
  if (results.error) throw new Error(`OpenAI error: ${results.error.message}`);
767
+ if (appendToChat && chat) {
768
+ if (responses_api) chat.push(...results.output);
769
+ else chat.push(results.choices[0].message);
770
+ }
621
771
  if (responses_api) {
622
772
  const textOutput = results.output
623
773
  .filter((o) => o.type === "message")
624
774
  .map((o) => o.content.map((c) => c.text).join(""))
625
775
  .join("");
776
+ const tool_calls = emptyToUndefined(
777
+ results.output
778
+ .filter((o) => o.type === "function_call")
779
+ .map((o) => ({
780
+ function: { name: o.name, arguments: o.arguments },
781
+ ...o,
782
+ })),
783
+ );
626
784
  return results.output.some(
627
785
  (o) =>
628
786
  o.type === "function_call" ||
629
787
  o.type === "image_generation_call" ||
630
788
  o.type === "mcp_list_tools" ||
631
- o.type === "mcp_call"
789
+ o.type === "mcp_call",
632
790
  )
633
791
  ? {
634
- tool_calls: emptyToUndefined(
635
- results.output
636
- .filter((o) => o.type === "function_call")
637
- .map((o) => ({
638
- function: { name: o.name, arguments: o.arguments },
639
- ...o,
640
- }))
641
- ),
792
+ tool_calls,
642
793
  image_calls: emptyToUndefined(
643
- results.output.filter((o) => o.type === "image_generation_call")
794
+ results.output.filter((o) => o.type === "image_generation_call"),
644
795
  ),
645
796
  mcp_calls: emptyToUndefined(
646
797
  results.output.filter(
647
- (o) => o.type === "mcp_call" || o.type === "mcp_list_tools"
648
- )
798
+ (o) => o.type === "mcp_call" || o.type === "mcp_list_tools",
799
+ ),
649
800
  ),
650
801
  content: textOutput || null,
802
+ hasToolCalls: tool_calls?.length,
803
+ getToolCalls() {
804
+ return tool_calls.map((tc) => ({
805
+ tool_name: tc.function.name,
806
+ input: JSON.parse(tc.function.arguments),
807
+ tool_call_id: tc.call_id,
808
+ }));
809
+ },
651
810
  }
652
811
  : textOutput || null;
653
812
  } else
@@ -655,6 +814,14 @@ const getCompletionOpenAICompatible = async (
655
814
  ? {
656
815
  tool_calls: results?.choices?.[0]?.message?.tool_calls,
657
816
  content: results?.choices?.[0]?.message?.content || null,
817
+ hasToolCalls: results?.choices?.[0]?.message?.tool_calls.length,
818
+ getToolCalls() {
819
+ return results?.choices?.[0]?.message?.tool_calls.map((tc) => ({
820
+ tool_name: tc.function.name,
821
+ input: JSON.parse(tc.function.arguments),
822
+ tool_call_id: tc.id,
823
+ }));
824
+ },
658
825
  }
659
826
  : results?.choices?.[0]?.message?.content || null;
660
827
  };
@@ -673,7 +840,7 @@ const getImageGenOpenAICompatible = async (
673
840
  n,
674
841
  output_format,
675
842
  response_format,
676
- }
843
+ },
677
844
  ) => {
678
845
  const { imageEndpoint, bearer, apiKey, image_model } = config;
679
846
  const headers = {
@@ -710,7 +877,7 @@ const getImageGenOpenAICompatible = async (
710
877
 
711
878
  const getEmbeddingOpenAICompatible = async (
712
879
  config,
713
- { prompt, model, debugResult }
880
+ { prompt, model, debugResult },
714
881
  ) => {
715
882
  const { embeddingsEndpoint, bearer, apiKey, embed_model } = config;
716
883
  const headers = {
@@ -747,7 +914,7 @@ const getEmbeddingAISDK = async (config, { prompt, model, debugResult }) => {
747
914
  case "OpenAI":
748
915
  const openai = createOpenAI({ apiKey: apiKey });
749
916
  model_obj = openai.textEmbeddingModel(
750
- model_name || "text-embedding-3-small"
917
+ model_name || "text-embedding-3-small",
751
918
  );
752
919
  //providerOptions.openai = {};
753
920
  break;
@@ -800,7 +967,7 @@ const initOAuth2Client = async (config) => {
800
967
  const oauth2Client = new google.auth.OAuth2(
801
968
  client_id,
802
969
  client_secret,
803
- redirect_uri
970
+ redirect_uri,
804
971
  );
805
972
  oauth2Client.setCredentials(pluginCfg.tokens);
806
973
  return oauth2Client;
@@ -868,7 +1035,7 @@ const getCompletionGoogleVertex = async (config, opts, oauth2Client) => {
868
1035
  chatParams.tools = [
869
1036
  {
870
1037
  functionDeclarations: opts.tools.map((t) =>
871
- prepFuncArgsForChat(t.function)
1038
+ prepFuncArgsForChat(t.function),
872
1039
  ),
873
1040
  },
874
1041
  ];
@@ -910,7 +1077,7 @@ const getEmbeddingGoogleVertex = async (config, opts, oauth2Client) => {
910
1077
  helpers.toValue({
911
1078
  content: p,
912
1079
  task_type: config.task_type || "RETRIEVAL_QUERY",
913
- })
1080
+ }),
914
1081
  );
915
1082
  } else {
916
1083
  instances = [
@@ -942,4 +1109,5 @@ module.exports = {
942
1109
  getEmbedding,
943
1110
  getImageGeneration,
944
1111
  getAudioTranscription,
1112
+ toolResponse,
945
1113
  };
package/index.js CHANGED
@@ -11,6 +11,7 @@ const {
11
11
  getEmbedding,
12
12
  getImageGeneration,
13
13
  getAudioTranscription,
14
+ toolResponse
14
15
  } = require("./generate");
15
16
  const { OPENAI_MODELS } = require("./constants.js");
16
17
  const { eval_expression } = require("@saltcorn/data/models/expression");
@@ -381,7 +382,10 @@ const functions = (config) => {
381
382
  },
382
383
  isAsync: true,
383
384
  description: "Generate text with GPT",
384
- arguments: [{ name: "prompt", type: "String" }],
385
+ arguments: [
386
+ { name: "prompt", type: "String", required: true },
387
+ { name: "options", type: "JSON", tstype: "any" },
388
+ ],
385
389
  },
386
390
  llm_image_generate: {
387
391
  run: async (prompt, opts) => {
@@ -390,7 +394,10 @@ const functions = (config) => {
390
394
  },
391
395
  isAsync: true,
392
396
  description: "Generate image",
393
- arguments: [{ name: "prompt", type: "String" }],
397
+ arguments: [
398
+ { name: "prompt", type: "String", required: true },
399
+ { name: "options", type: "JSON", tstype: "any" },
400
+ ],
394
401
  },
395
402
  llm_embedding: {
396
403
  run: async (prompt, opts) => {
@@ -399,7 +406,10 @@ const functions = (config) => {
399
406
  },
400
407
  isAsync: true,
401
408
  description: "Get vector embedding",
402
- arguments: [{ name: "prompt", type: "String" }],
409
+ arguments: [
410
+ { name: "prompt", type: "String", required: true },
411
+ { name: "options", type: "JSON", tstype: "any" },
412
+ ],
403
413
  },
404
414
  llm_transcribe: {
405
415
  run: async (opts) => {
@@ -408,7 +418,21 @@ const functions = (config) => {
408
418
  },
409
419
  isAsync: true,
410
420
  description: "Get vector embedding",
411
- arguments: [{ name: "prompt", type: "String" }],
421
+ arguments: [
422
+ { name: "options", type: "JSON", tstype: "any", required: true },
423
+ ],
424
+ },
425
+ llm_add_tool_response: {
426
+ run: async (prompt, opts) => {
427
+ const result = await toolResponse(config, { prompt, ...opts });
428
+ return result;
429
+ },
430
+ isAsync: true,
431
+ description: "Insert the response to a tool call into a chat",
432
+ arguments: [
433
+ { name: "prompt", type: "String", required: true },
434
+ { name: "options", type: "JSON", tstype: "any" },
435
+ ],
412
436
  },
413
437
  };
414
438
  };
@@ -432,7 +456,7 @@ const routes = (config) => {
432
456
  const oauth2Client = new google.auth.OAuth2(
433
457
  client_id,
434
458
  client_secret,
435
- redirect_uri
459
+ redirect_uri,
436
460
  );
437
461
  const authUrl = oauth2Client.generateAuthUrl({
438
462
  access_type: "offline",
@@ -459,7 +483,7 @@ const routes = (config) => {
459
483
  const oauth2Client = new google.auth.OAuth2(
460
484
  client_id,
461
485
  client_secret,
462
- redirect_uri
486
+ redirect_uri,
463
487
  );
464
488
  let plugin = await Plugin.findOne({ name: "large-language-model" });
465
489
  if (!plugin) {
@@ -475,8 +499,8 @@ const routes = (config) => {
475
499
  req.flash(
476
500
  "warning",
477
501
  req.__(
478
- "No refresh token received. Please revoke the plugin's access and try again."
479
- )
502
+ "No refresh token received. Please revoke the plugin's access and try again.",
503
+ ),
480
504
  );
481
505
  } else {
482
506
  const newConfig = { ...(plugin.configuration || {}), tokens };
@@ -488,7 +512,7 @@ const routes = (config) => {
488
512
  });
489
513
  req.flash(
490
514
  "success",
491
- req.__("Authentication successful! You can now use Vertex AI.")
515
+ req.__("Authentication successful! You can now use Vertex AI."),
492
516
  );
493
517
  }
494
518
  } catch (error) {
@@ -615,13 +639,13 @@ module.exports = {
615
639
  prompt_formula,
616
640
  row,
617
641
  user,
618
- "llm_generate prompt formula"
642
+ "llm_generate prompt formula",
619
643
  );
620
644
  else prompt = row[prompt_field];
621
645
  const opts = {};
622
646
  if (override_config) {
623
647
  const altcfg = config.altconfigs.find(
624
- (c) => c.name === override_config
648
+ (c) => c.name === override_config,
625
649
  );
626
650
  opts.endpoint = altcfg.endpoint;
627
651
  opts.model = altcfg.model;
@@ -679,7 +703,8 @@ module.exports = {
679
703
  {
680
704
  name: "answer_field",
681
705
  label: "Response variable",
682
- sublabel: "Set the generated response object to this context variable. The subfield <code>text</code> holds the string transcription",
706
+ sublabel:
707
+ "Set the generated response object to this context variable. The subfield <code>text</code> holds the string transcription",
683
708
  type: "String",
684
709
  required: true,
685
710
  },
@@ -766,7 +791,7 @@ module.exports = {
766
791
  else
767
792
  await table.updateRow(
768
793
  { [answer_field]: ans.text },
769
- row[table.pk_name]
794
+ row[table.pk_name],
770
795
  );
771
796
  },
772
797
  },
@@ -879,7 +904,7 @@ module.exports = {
879
904
  prompt_formula,
880
905
  row,
881
906
  user,
882
- "llm_generate prompt formula"
907
+ "llm_generate prompt formula",
883
908
  );
884
909
  else prompt = row[prompt_field];
885
910
 
@@ -906,7 +931,7 @@ module.exports = {
906
931
  "image/png",
907
932
  imgContents,
908
933
  user?.id,
909
- min_role || 1
934
+ min_role || 1,
910
935
  );
911
936
  upd[answer_field] = file.path_to_serve;
912
937
  }
@@ -998,7 +1023,7 @@ module.exports = {
998
1023
  sublabel:
999
1024
  "Use this context variable to store the chat history for subsequent prompts",
1000
1025
  type: "String",
1001
- }
1026
+ },
1002
1027
  );
1003
1028
  } else if (table) {
1004
1029
  const jsonFields = table.fields
@@ -1022,7 +1047,7 @@ module.exports = {
1022
1047
  type: "String",
1023
1048
  required: true,
1024
1049
  attributes: { options: jsonFields },
1025
- }
1050
+ },
1026
1051
  );
1027
1052
  }
1028
1053
 
@@ -1051,7 +1076,7 @@ module.exports = {
1051
1076
  input_type: "section_header",
1052
1077
  label: "JSON fields to generate",
1053
1078
  },
1054
- fieldsField
1079
+ fieldsField,
1055
1080
  );
1056
1081
  return cfgFields;
1057
1082
  },
@@ -1077,7 +1102,7 @@ module.exports = {
1077
1102
  if (model) opts.model = model;
1078
1103
  if (override_config) {
1079
1104
  const altcfg = config.altconfigs.find(
1080
- (c) => c.name === override_config
1105
+ (c) => c.name === override_config,
1081
1106
  );
1082
1107
  opts.endpoint = altcfg.endpoint;
1083
1108
  opts.model = altcfg.model;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/large-language-model",
3
- "version": "0.9.11",
3
+ "version": "1.0.0",
4
4
  "description": "Large language models and functionality for Saltcorn",
5
5
  "main": "index.js",
6
6
  "dependencies": {
@@ -13,7 +13,14 @@
13
13
  "googleapis": "^144.0.0",
14
14
  "ai": "5.0.44",
15
15
  "@ai-sdk/openai": "2.0.30",
16
- "openai": "6.16.0"
16
+ "openai": "6.16.0",
17
+ "@elevenlabs/elevenlabs-js": "2.31.0"
18
+ },
19
+ "devDependencies": {
20
+ "jest": "^29.7.0"
21
+ },
22
+ "scripts": {
23
+ "test": "jest tests --runInBand"
17
24
  },
18
25
  "author": "Tom Nielsen",
19
26
  "license": "MIT",
@@ -21,11 +28,12 @@
21
28
  "eslintConfig": {
22
29
  "extends": "eslint:recommended",
23
30
  "parserOptions": {
24
- "ecmaVersion": 2020
31
+ "ecmaVersion": 2024
25
32
  },
26
33
  "env": {
27
34
  "node": true,
28
- "es6": true
35
+ "es6": true,
36
+ "jest/globals": true
29
37
  },
30
38
  "rules": {
31
39
  "no-unused-vars": "off",
@@ -0,0 +1,34 @@
1
+ module.exports = [
2
+ {
3
+ name: "OpenAI completions",
4
+ model: "gpt-5.1",
5
+ api_key: process.env.OPENAI_API_KEY,
6
+ backend: "OpenAI",
7
+ embed_model: "text-embedding-3-small",
8
+ image_model: "gpt-image-1",
9
+ temperature: 0.7,
10
+ responses_api: false,
11
+ ai_sdk_provider: "OpenAI",
12
+ },
13
+ {
14
+ name: "OpenAI responses",
15
+ model: "gpt-5.1",
16
+ api_key: process.env.OPENAI_API_KEY,
17
+ backend: "OpenAI",
18
+ embed_model: "text-embedding-3-small",
19
+ image_model: "gpt-image-1",
20
+ temperature: 0.7,
21
+ responses_api: true,
22
+ ai_sdk_provider: "OpenAI",
23
+ },
24
+ {
25
+ name: "AI SDK OpenAI",
26
+ model: "gpt-5.1",
27
+ api_key: process.env.OPENAI_API_KEY,
28
+ backend: "AI SDK",
29
+ embed_model: "text-embedding-3-small",
30
+ image_model: "gpt-image-1",
31
+ temperature: 0.7,
32
+ ai_sdk_provider: "OpenAI",
33
+ },
34
+ ];
@@ -0,0 +1,200 @@
1
+ const { getState } = require("@saltcorn/data/db/state");
2
+ const View = require("@saltcorn/data/models/view");
3
+ const Table = require("@saltcorn/data/models/table");
4
+ const Plugin = require("@saltcorn/data/models/plugin");
5
+
6
+ const { mockReqRes } = require("@saltcorn/data/tests/mocks");
7
+ const { afterAll, beforeAll, describe, it, expect } = require("@jest/globals");
8
+
9
+ afterAll(require("@saltcorn/data/db").close);
10
+ beforeAll(async () => {
11
+ await require("@saltcorn/data/db/reset_schema")();
12
+ await require("@saltcorn/data/db/fixtures")();
13
+
14
+ getState().registerPlugin("base", require("@saltcorn/data/base-plugin"));
15
+ });
16
+
17
+ // run with:
18
+ // saltcorn dev:plugin-test -d ~/large-language-model/
19
+
20
+ jest.setTimeout(30000);
21
+
22
+ for (const nameconfig of require("./configs")) {
23
+ const { name, ...config } = nameconfig;
24
+ describe("llm_generate function with " + name, () => {
25
+ beforeAll(async () => {
26
+ getState().registerPlugin(
27
+ "@saltcorn/large-language-model",
28
+ require(".."),
29
+ config,
30
+ );
31
+ });
32
+
33
+ it("generates text", async () => {
34
+ const answer = await getState().functions.llm_generate.run(
35
+ "What is the Capital of France?",
36
+ );
37
+ //console.log({ answer });
38
+
39
+ expect(typeof answer).toBe("string");
40
+ expect(answer).toContain("Paris");
41
+ });
42
+ it("generates text with system prompt", async () => {
43
+ const answer = await getState().functions.llm_generate.run(
44
+ "What is the name of the last week day in a normal work week?",
45
+ {
46
+ systemPrompt: "Answer in German, even when questions are in English",
47
+ },
48
+ );
49
+ //console.log({ answer });
50
+
51
+ expect(typeof answer).toBe("string");
52
+ expect(answer).toContain("Freitag");
53
+ });
54
+ it("generates text with chat history", async () => {
55
+ const chat = [
56
+ {
57
+ role: "user",
58
+ content: "What is the capital of France?",
59
+ },
60
+ {
61
+ role: "assistant",
62
+ content: "Paris.",
63
+ },
64
+ ];
65
+ const answer = await getState().functions.llm_generate.run(
66
+ "What is the name of the river running through this city?",
67
+ {
68
+ chat,
69
+ },
70
+ );
71
+ //console.log({ answer });
72
+
73
+ expect(typeof answer).toBe("string");
74
+ expect(answer).toContain("Seine");
75
+ expect(chat.length).toBe(2);
76
+ });
77
+ it("generates text with chat history and no prompt", async () => {
78
+ const answer = await getState().functions.llm_generate.run("", {
79
+ chat: [
80
+ {
81
+ role: "user",
82
+ content: "What is the capital of France?",
83
+ },
84
+ {
85
+ role: "assistant",
86
+ content: "Paris.",
87
+ },
88
+ {
89
+ role: "user",
90
+ content: "What is the name of the river running through this city?",
91
+ },
92
+ ],
93
+ });
94
+ //console.log({ answer });
95
+
96
+ expect(typeof answer).toBe("string");
97
+ expect(answer).toContain("Seine");
98
+ });
99
+ it("uses tools", async () => {
100
+ const answer = await getState().functions.llm_generate.run(
101
+ "Generate a list of EU capitals in a structured format using the provided tool",
102
+ cities_tool,
103
+ );
104
+ expect(typeof answer).toBe("object");
105
+ const cities = answer.ai_sdk
106
+ ? answer.tool_calls[0].input?.cities
107
+ : JSON.parse(answer.tool_calls[0].function.arguments).cities;
108
+ expect(cities.length).toBe(27);
109
+ });
110
+ it("appends to chat history", async () => {
111
+ const chat = [];
112
+ const answer1 = await getState().functions.llm_generate.run(
113
+ "What is the Capital of France?",
114
+ {
115
+ chat,
116
+ appendToChat: true,
117
+ },
118
+ );
119
+ const answer2 = await getState().functions.llm_generate.run(
120
+ "What is the name of the river running through this city?",
121
+ {
122
+ chat,
123
+ appendToChat: true,
124
+ },
125
+ );
126
+ //console.log({ answer });
127
+
128
+ expect(typeof answer2).toBe("string");
129
+ expect(answer2).toContain("Seine");
130
+ expect(chat.length).toBe(4);
131
+ });
132
+ it("tool use sequence", async () => {
133
+ const chat = [];
134
+ const answer = await getState().functions.llm_generate.run(
135
+ "Generate a list of EU capitals in a structured format using the provided tool",
136
+ { chat, appendToChat: true, ...cities_tool, streamCallback() {} },
137
+ );
138
+ expect(typeof answer).toBe("object");
139
+
140
+ const tc = answer.getToolCalls()[0];
141
+
142
+ const cities = tc.input.cities;
143
+ expect(cities.length).toBe(27);
144
+
145
+ await getState().functions.llm_add_tool_response.run("List received", {
146
+ chat,
147
+ tool_call: tc,
148
+ });
149
+
150
+ const answer1 = await getState().functions.llm_generate.run(
151
+ "Make the same list in a structured format using the provided tool but for the original 12 member countries of the EU",
152
+ { chat, appendToChat: true, ...cities_tool },
153
+ );
154
+
155
+ const cities1 = answer1.getToolCalls()[0].input?.cities;
156
+
157
+ expect(cities1.length).toBe(12);
158
+ });
159
+ });
160
+ }
161
+
162
+ const cities_tool = {
163
+ tools: [
164
+ {
165
+ type: "function",
166
+ function: {
167
+ name: "cities",
168
+ description: "Provide a list of cities by country and city name",
169
+ parameters: {
170
+ type: "object",
171
+ properties: {
172
+ cities: {
173
+ type: "array",
174
+ items: {
175
+ type: "object",
176
+ properties: {
177
+ country_name: {
178
+ type: "string",
179
+ description: "Country name",
180
+ },
181
+ city_name: {
182
+ type: "string",
183
+ description: "City name",
184
+ },
185
+ },
186
+ required: ["country_name", "city_name"],
187
+ },
188
+ },
189
+ },
190
+ },
191
+ },
192
+ },
193
+ ],
194
+ tool_choice: {
195
+ type: "function",
196
+ function: {
197
+ name: "cities",
198
+ },
199
+ },
200
+ };