utilitas 1998.2.23 → 1998.2.24

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/lib/alan.mjs CHANGED
@@ -7,9 +7,12 @@ import { create as createUoid } from './uoid.mjs';
7
7
  import {
8
8
  log as _log,
9
9
  renderText as _renderText,
10
- base64Encode, ensureArray, ensureString, extract, ignoreErrFunc,
10
+ base64Encode, ensureArray, ensureString, extract,
11
+ ignoreErrFunc,
12
+ insensitiveCompare,
13
+ isSet,
11
14
  need, parseJson,
12
- throwError,
15
+ throwError
13
16
  } from './utilitas.mjs';
14
17
 
15
18
  const _NEED = [
@@ -82,6 +85,7 @@ const log = (cnt, opt) => _log(cnt, import.meta.url, { time: 1, ...opt || {} });
82
85
  const CONTENT_IS_REQUIRED = 'Content is required.';
83
86
  const assertContent = content => assert(content.length, CONTENT_IS_REQUIRED);
84
87
 
88
+
85
89
  const DEFAULT_MODELS = {
86
90
  [CHATGPT_MINI]: GPT_4O_MINI,
87
91
  [CHATGPT_REASONING]: GPT_O3_MINI,
@@ -123,7 +127,7 @@ const MODELS = {
123
127
  trainingData: 'Oct 2023',
124
128
  json: true,
125
129
  vision: true,
126
- reasoning: false,
130
+ tools: true,
127
131
  audio: 'gpt-4o-mini-audio-preview',
128
132
  supportedMimeTypes: [
129
133
  png, jpeg, gif, webp,
@@ -142,7 +146,7 @@ const MODELS = {
142
146
  trainingData: 'Oct 2023',
143
147
  json: true,
144
148
  vision: true,
145
- reasoning: false,
149
+ tools: true,
146
150
  audio: 'gpt-4o-audio-preview',
147
151
  supportedMimeTypes: [
148
152
  png, jpeg, gif, webp,
@@ -162,6 +166,7 @@ const MODELS = {
162
166
  json: true,
163
167
  reasoning: true,
164
168
  vision: true,
169
+ tools: true,
165
170
  // audio: 'gpt-4o-audio-preview', // fallback to GPT-4O to support audio
166
171
  supportedMimeTypes: [
167
172
  png, jpeg, gif, webp,
@@ -181,6 +186,7 @@ const MODELS = {
181
186
  json: true,
182
187
  reasoning: true,
183
188
  vision: true,
189
+ tools: true,
184
190
  // audio: 'gpt-4o-mini-audio-preview', // fallback to GPT-4O-MINI to support audio
185
191
  supportedMimeTypes: [
186
192
  png, jpeg, gif, webp,
@@ -300,6 +306,7 @@ const MODELS = {
300
306
  tokenLimitsITPM: 40000,
301
307
  tokenLimitsOTPM: 8000,
302
308
  trainingData: 'Apr 2024',
309
+ tools: true,
303
310
  supportedMimeTypes: [
304
311
  png, jpeg, gif, webp, pdf,
305
312
  ],
@@ -319,6 +326,7 @@ const MODELS = {
319
326
  tokenLimitsOTPM: 8000,
320
327
  trainingData: 'Apr 2024', // ?
321
328
  reasoning: true,
329
+ tools: true,
322
330
  supportedMimeTypes: [
323
331
  png, jpeg, gif, webp, pdf,
324
332
  ],
@@ -451,6 +459,35 @@ const countTokens = async (input, options) => {
451
459
  );
452
460
  };
453
461
 
462
+ const tools = [
463
+ {
464
+ def: {
465
+ type: 'function', strict: true, function: {
466
+ name: 'testFunctionCall',
467
+ description: 'This is a test function call',
468
+ parameters: {
469
+ type: 'object',
470
+ properties: {
471
+ a: { type: 'string', description: 'Please create a random string' },
472
+ b: { type: 'string', enum: ['A', 'B'], description: 'Enum parameter' }
473
+ },
474
+ required: ['a'],
475
+ additionalProperties: false
476
+ }
477
+ }
478
+ },
479
+ func: async args => 'OK',
480
+ },
481
+ ];
482
+
483
+ const toolsClaude = tools.map(x => ({
484
+ ...x, def: {
485
+ name: x.def.function.name,
486
+ description: x.def.function.description,
487
+ input_schema: x.def.function.parameters,
488
+ }
489
+ }));
490
+
454
491
  const selectGptAudioModel = options => {
455
492
  assert(
456
493
  MODELS[options.model]?.audio,
@@ -630,12 +667,12 @@ const packResp = async (resp, options) => {
630
667
  };
631
668
 
632
669
  const packGptResp = async (resp, options) => {
633
- const text = resp?.choices?.[0]?.message?.content // ChatGPT
634
- || resp?.choices?.[0]?.message?.audio?.transcript // ChatGPT audio mode
635
- || resp?.text?.() // Gemini
636
- || resp?.content?.text // Claude
637
- || resp?.message?.content || ''; // Ollama
638
- const audio = resp?.choices?.[0]?.message?.audio?.data; // ChatGPT audio mode
670
+ const text = resp?.choices?.[0]?.message?.content // ChatGPT
671
+ || resp?.choices?.[0]?.message?.audio?.transcript // ChatGPT audio mode
672
+ || resp?.text?.() // Gemini
673
+ || resp?.content?.find(x => x.type === 'text')?.text // Claude
674
+ || resp?.message?.content || ''; // Ollama
675
+ const audio = resp?.choices?.[0]?.message?.audio?.data; // ChatGPT audio mode
639
676
  if (options?.raw) { return resp; }
640
677
  else if (options?.simple && options?.jsonMode) { return parseJson(text); }
641
678
  else if (options?.simple && options?.audioMode) { return audio; }
@@ -645,6 +682,47 @@ const packGptResp = async (resp, options) => {
645
682
  return await packResp({ text, audio, references: resp?.references }, options);
646
683
  };
647
684
 
685
+ const handleToolsCall = async (msg, options) => {
686
+ let content = [], preRes = [], input, packMsg;
687
+ if (msg?.tool_calls?.length) {
688
+ switch (options?.flavor) {
689
+ case CLAUDE: preRes.push({ role: 'assistant', content: msg?.tool_calls }); break;
690
+ case CHATGPT: default: preRes.push({ role: 'assistant', ...msg });
691
+ }
692
+ for (const fn of msg.tool_calls) {
693
+ input = parseJson(fn?.function?.arguments || fn?.input);
694
+ switch (options?.flavor) {
695
+ case CLAUDE:
696
+ fn.input = input;
697
+ packMsg = (content, is_error) => ({
698
+ type: 'tool_result', tool_use_id: fn.id, content, is_error,
699
+ }); break;
700
+ case CHATGPT: default:
701
+ packMsg = (t, e) => ({
702
+ role: 'tool', tool_call_id: fn.id, [e ? 'error' : 'content']: t
703
+ });
704
+ }
705
+ const name = fn?.function?.name || fn?.name;
706
+ const func = tools.find(x => insensitiveCompare(
707
+ x.def?.function?.name || x?.def?.name, name
708
+ ))?.func;
709
+ if (!func) {
710
+ content.push(packMsg(`Function call failed, invalid function name: ${name}`, true));
711
+ continue;
712
+ }
713
+ try {
714
+ content.push(packMsg(await func(...Object.values(input))));
715
+ } catch (err) {
716
+ content.push(packMsg(`Function call failed: ${err.message}`, true));
717
+ }
718
+ }
719
+ switch (options?.flavor) {
720
+ case CLAUDE: content = [{ role: 'user', content }];
721
+ }
722
+ }
723
+ return [...preRes, ...content];
724
+ };
725
+
648
726
  const promptChatGPT = async (content, options = {}) => {
649
727
  const { client } = await getOpenAIClient(options);
650
728
  // https://github.com/openai/openai-node?tab=readme-ov-file#streaming-responses
@@ -674,45 +752,65 @@ const promptChatGPT = async (content, options = {}) => {
674
752
  let format;
675
753
  [format, options.audioMimeType, options.suffix]
676
754
  = options?.stream ? ['pcm16', pcm16, 'pcm.wav'] : [WAV, wav, WAV];
677
- let [resp, resultText, resultAudio, chunk] = [
755
+ let [resp, resultText, resultAudio, chunk, resultTools] = [
678
756
  await client.chat.completions.create({
679
757
  modalities, audio: options?.audio || (
680
758
  modalities?.find?.(x => x === AUDIO) && {
681
759
  voice: DEFAULT_MODELS[OPENAI_VOICE], format
682
760
  }
683
- ), ...messages([...options?.messages || [], message]),
684
- ...options?.jsonMode ? {
761
+ ), ...messages([
762
+ ...options?.messages || [], message,
763
+ ...options?.toolsResult || [],
764
+ ]), ...MODELS[options.model]?.tools ? {
765
+ tools: options?.tools ?? tools.map(x => x.def),
766
+ } : {}, ...options?.jsonMode ? {
685
767
  response_format: { type: JSON_OBJECT }
686
- } : {}, model: options.model, stream: !!options?.stream,
687
- }), '', Buffer.alloc(0), null
768
+ } : {}, model: options.model, stream: !!options?.stream, store: true,
769
+ }), '', Buffer.alloc(0), null, [],
688
770
  ];
689
- if (!options?.stream) {
690
- return await packGptResp(resp, options);
691
- }
692
- for await (chunk of resp) {
693
- const deltaText = chunk.choices[0]?.delta?.content
694
- || chunk.choices[0]?.delta?.audio?.transcript || '';
695
- const deltaAudio = chunk.choices[0]?.delta?.audio?.data ? await convert(
696
- chunk.choices[0].delta.audio.data, { input: BASE64, expected: BUFFER }
697
- ) : Buffer.alloc(0);
698
- if (deltaText === '' && !deltaAudio.length) { continue; }
699
- resultText += deltaText;
700
- resultAudio = Buffer.concat([resultAudio, deltaAudio]);
701
- const respAudio = options?.delta ? deltaAudio : resultAudio;
771
+ if (options?.stream) {
772
+ for await (chunk of resp) {
773
+ const deltaText = chunk.choices[0]?.delta?.content
774
+ || chunk.choices[0]?.delta?.audio?.transcript || '';
775
+ const deltaAudio = chunk.choices[0]?.delta?.audio?.data ? await convert(
776
+ chunk.choices[0].delta.audio.data, { input: BASE64, expected: BUFFER }
777
+ ) : Buffer.alloc(0);
778
+ const deltaFunc = chunk.choices[0]?.delta?.tool_calls || [];
779
+ for (const x in deltaFunc) {
780
+ let curFunc = resultTools.find(z => z.index === deltaFunc[x].index);
781
+ curFunc || (resultTools.push(curFunc = {}));
782
+ isSet(deltaFunc[x].index, true) && (curFunc.index = deltaFunc[x].index);
783
+ deltaFunc[x].id && (curFunc.id = deltaFunc[x].id);
784
+ deltaFunc[x].type && (curFunc.type = deltaFunc[x].type);
785
+ curFunc.function || (curFunc.function = { name: '', arguments: '' });
786
+ if (deltaFunc[x].function) {
787
+ deltaFunc[x].function.name && (curFunc.function.name += deltaFunc[x].function.name);
788
+ deltaFunc[x].function.arguments && (curFunc.function.arguments += deltaFunc[x].function.arguments);
789
+ }
790
+ }
791
+ if (deltaText === '' && !deltaAudio.length) { continue; }
792
+ resultText += deltaText;
793
+ resultAudio = Buffer.concat([resultAudio, deltaAudio]);
794
+ const respAudio = options?.delta ? deltaAudio : resultAudio;
795
+ chunk.choices[0].message = {
796
+ content: options?.delta ? deltaText : resultText,
797
+ ...respAudio.length ? { audio: { data: respAudio } } : {},
798
+ };
799
+ await ignoreErrFunc(async () => await options?.stream?.(
800
+ await packGptResp(chunk, { ...options || {}, processing: true })
801
+ ), LOG);
802
+ }
803
+ chunk.choices?.[0] || (chunk.choices = [{}]); // handle empty choices for Azure APIs
702
804
  chunk.choices[0].message = {
703
- content: options?.delta ? deltaText : resultText,
704
- ...respAudio.length ? { audio: { data: respAudio } } : {},
805
+ content: resultText, tool_calls: resultTools,
806
+ ...resultAudio.length ? { audio: { data: resultAudio } } : {},
705
807
  };
706
- await ignoreErrFunc(async () => await options?.stream?.(
707
- await packGptResp(chunk, { ...options || {}, processing: true })
708
- ), LOG);
808
+ resp = chunk;
709
809
  }
710
- chunk.choices?.[0] || (chunk.choices = [{}]); // handle empty choices for Azure APIs
711
- chunk.choices[0].message = {
712
- content: resultText,
713
- ...resultAudio.length ? { audio: { data: resultAudio } } : {},
714
- };
715
- return await packGptResp(chunk, options);
810
+ const toolsResult = await handleToolsCall(resp?.choices?.[0]?.message);
811
+ return await (toolsResult.length ? promptChatGPT(
812
+ content, { ...options || {}, toolsResult }
813
+ ) : packGptResp(resp, options));
716
814
  };
717
815
 
718
816
  const promptAzure = async (content, options = {}) => await promptChatGPT(
@@ -749,33 +847,55 @@ const promptClaude = async (content, options = {}) => {
749
847
  const resp = await client.messages.create({
750
848
  model: options.model, max_tokens: MODELS[options.model].maxOutputTokens,
751
849
  messages: [
752
- ...options?.messages || [], buildClaudeMessage(content, options)
850
+ ...options?.messages || [], buildClaudeMessage(content, options), ...options?.toolsResult || [],
753
851
  ], stream: !!options?.stream, ...reasoning ? {
754
852
  thinking: options?.thinking || { type: 'enabled', budget_tokens: 1024 },
755
- } : {} // https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking
853
+ } : {}, // https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking
854
+ ...MODELS[options.model]?.tools ? {
855
+ tools: options?.tools ?? toolsClaude.map(x => x.def),
856
+ } : {},
756
857
  });
757
- let [event, result, thinkEnd] = [null, '', ''];
858
+ let [event, txtResult, thinking, signature, result, thinkEnd, tool_calls]
859
+ = [null, '', '', '', '', '', []];
758
860
  if (options?.stream) {
759
861
  for await (event of resp) {
862
+ print(event);
760
863
  let [thkDelta, txtDelta] = [
761
864
  event?.content_block?.thinking || event?.delta?.thinking || '',
762
865
  event?.content_block?.text || event?.delta?.text || '',
763
866
  ];
867
+ txtResult += txtDelta;
868
+ thinking += thkDelta;
869
+ signature = signature || event?.content_block?.signature || event?.delta?.signature || '';
764
870
  if (reasoning) {
765
871
  !result && thkDelta && (thkDelta = `${THINK_STR}\n${thkDelta}`);
766
872
  result && txtDelta && !thinkEnd && (thinkEnd = thkDelta = `${thkDelta}\n${THINK_END}\n\n`);
767
873
  }
874
+ if (event?.content_block?.type === 'tool_use') {
875
+ tool_calls.push({ ...event?.content_block, input: '' });
876
+ } else if (event?.delta?.partial_json) {
877
+ tool_calls[tool_calls.length - 1].input += event?.delta?.partial_json;
878
+ }
768
879
  const delta = thkDelta + txtDelta;
769
880
  if (delta === '') { continue; }
770
881
  result += delta;
771
- event.content = { text: options?.delta ? delta : result };
882
+ event.content = [{ type: 'text', text: options?.delta ? delta : result }];
772
883
  await ignoreErrFunc(async () => await options.stream(
773
884
  await packGptResp(event, { ...options || {}, processing: true })
774
885
  ), LOG);
775
886
  }
776
- event.content = { text: result };
887
+ event.content = [{ type: 'text', text: tool_calls.length ? txtResult : result }];
888
+ tool_calls.length && thinking && event.content.unshift({ type: 'thinking', thinking, signature });
889
+ } else {
890
+ event = resp;
891
+ tool_calls = resp?.content?.filter?.(x => x.type === 'tool_use') || [];
892
+ }
893
+ const toolsResult = await handleToolsCall({ tool_calls }, { flavor: CLAUDE });
894
+ if (toolsResult.length) {
895
+ toolsResult[0].content.unshift(...event?.content.filter(x => x?.type !== 'tool_use'));
896
+ return await promptClaude(content, { ...options || {}, toolsResult });
777
897
  }
778
- return await packGptResp(options?.stream ? event : resp, options);
898
+ return packGptResp(event, options);
779
899
  };
780
900
 
781
901
  const uploadFile = async (input, options) => {
package/lib/manifest.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  const manifest = {
2
2
  "name": "utilitas",
3
3
  "description": "Just another common utility for JavaScript.",
4
- "version": "1998.2.23",
4
+ "version": "1998.2.24",
5
5
  "private": false,
6
6
  "homepage": "https://github.com/Leask/utilitas",
7
7
  "main": "index.mjs",
package/lib/utilitas.mjs CHANGED
@@ -1,10 +1,10 @@
1
- import { assertPath, decodeBase64DataURL, readJson } from './storage.mjs';
2
- import { basename as _basename, dirname, join, sep } from 'path';
3
1
  import { fileURLToPath } from 'node:url';
2
+ import { basename as _basename, dirname, join, sep } from 'path';
4
3
  import { promisify } from 'util';
5
4
  import { validate as verifyUuid } from 'uuid';
6
5
  import * as boxes from './boxes.mjs';
7
6
  import color from './color.mjs';
7
+ import { assertPath, decodeBase64DataURL, readJson } from './storage.mjs';
8
8
 
9
9
  const call = (f, ...a) => promisify(Array.isArray(f) ? f[0].bind(f[1]) : f)(...a);
10
10
  const invalidTime = 'Invalid time.';
@@ -923,5 +923,5 @@ export {
923
923
  verifyUrl,
924
924
  verifyUuid,
925
925
  voidFunc,
926
- which,
926
+ which
927
927
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "utilitas",
3
3
  "description": "Just another common utility for JavaScript.",
4
- "version": "1998.2.23",
4
+ "version": "1998.2.24",
5
5
  "private": false,
6
6
  "homepage": "https://github.com/Leask/utilitas",
7
7
  "main": "index.mjs",