utilitas 1999.1.98 → 2000.1.2

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
@@ -45,31 +45,33 @@ You may be provided with some tools(functions) to help you gather information an
45
45
  const _NEED = ['js-tiktoken', 'OpenAI'];
46
46
 
47
47
  const [
48
- OPENAI, GEMINI, OLLAMA, GEMINI_25_FLASH, NOVA, DEEPSEEK_R1, MD_CODE,
49
- CLOUD_SONNET_45, AUDIO, WAV, ATTACHMENTS, OPENAI_VOICE,
50
- GPT_REASONING_EFFORT, THINK, THINK_STR, THINK_END, TOOLS_STR, TOOLS_END,
51
- TOOLS, TEXT, OK, FUNC, GPT_51, GPT_51_CODEX, GPT_5_IMAGE, GEMMA_3_27B, ANTHROPIC, v8k, ais,
48
+ OPENAI, GEMINI, OLLAMA, NOVA, DEEPSEEK_R1, MD_CODE, CLOUD_SONNET_45, AUDIO,
49
+ WAV, ATTACHMENTS, OPENAI_VOICE, GPT_REASONING_EFFORT, THINK, THINK_STR,
50
+ THINK_END, TOOLS_STR, TOOLS_END, TOOLS, TEXT, OK, FUNC, GPT_51,
51
+ GPT_51_CODEX, GPT_5_IMAGE, GEMMA_3_27B, ANTHROPIC, v8k, ais,
52
52
  MAX_TOOL_RECURSION, LOG, name, user, system, assistant, MODEL, JSON_OBJECT,
53
53
  tokenSafeRatio, CONTENT_IS_REQUIRED, OPENAI_HI_RES_SIZE, k, kT, m, minute,
54
54
  hour, gb, trimTailing, GEMINI_25_FLASH_IMAGE, IMAGE, JINA, JINA_DEEPSEARCH,
55
- GEMINI_30_PRO, SILICONFLOW, SF_DEEPSEEK_R1, MAX_TIRE, OPENROUTER_API,
56
- OPENROUTER, AUTO, TOOL,
55
+ SILICONFLOW, SF_DEEPSEEK_R1, MAX_TIRE, OPENROUTER_API, OPENROUTER, AUTO,
56
+ TOOL, S_OPENAI, S_GOOGLE, S_ANTHROPIC, ONLINE,
57
57
  ] = [
58
- 'OpenAI', 'Gemini', 'Ollama', 'gemini-2.5-flash-preview-09-2025',
59
- 'nova', 'deepseek-r1', '```', 'anthropic/claude-sonnet-4.5', 'audio',
60
- 'wav', '[ATTACHMENTS]', 'OPENAI_VOICE', 'medium', 'think', '<think>',
61
- '</think>', '<tools>', '</tools>', 'tools', 'text', 'OK', 'function',
62
- 'gpt-5.1', 'gpt-5.1-codex', 'gpt-5-image', 'gemma3:27b', 'Anthropic',
63
- 7680 * 4320, [], 30, { log: true }, 'Alan', 'user', { role: 'system' },
64
- 'assistant', 'model', 'json_object', 1.1, 'Content is required.',
65
- 2048 * 2048, x => 1024 * x, x => 1000 * x, x => 1024 * 1024 * x,
66
- x => 60 * x, x => 60 * 60 * x, x => 1024 * 1024 * 1024 * x,
67
- x => x.replace(/[\.\s]*$/, ''), 'gemini-2.5-flash-image', 'image',
68
- 'Jina', 'jina-deepsearch-v1', 'gemini-3-pro-preview', 'SiliconFlow',
69
- 'Pro/deepseek-ai/DeepSeek-R1', 768 * 768,
58
+ 'OpenAI', 'Gemini', 'Ollama', 'nova', 'deepseek-r1', '```',
59
+ 'claude-sonnet-4.5', 'audio', 'wav', '[ATTACHMENTS]', 'OPENAI_VOICE',
60
+ 'medium', 'think', '<think>', '</think>', '<tools>', '</tools>',
61
+ 'tools', 'text', 'OK', 'function', 'gpt-5.1', 'gpt-5.1-codex',
62
+ 'gpt-5-image', 'gemma3:27b', 'Anthropic', 7680 * 4320, [], 30,
63
+ { log: true }, 'Alan', 'user', { role: 'system' }, 'assistant', 'model',
64
+ 'json_object', 1.1, 'Content is required.', 2048 * 2048, x => 1024 * x,
65
+ x => 1000 * x, x => 1024 * 1024 * x, x => 60 * x, x => 60 * 60 * x,
66
+ x => 1024 * 1024 * 1024 * x, x => x.replace(/[\.\s]*$/, ''),
67
+ 'gemini-2.5-flash-image', 'image', 'Jina', 'jina-deepsearch-v1',
68
+ 'SiliconFlow', 'Pro/deepseek-ai/DeepSeek-R1', 768 * 768,
70
69
  'https://openrouter.ai/api/v1', 'OpenRouter', 'openrouter/auto', 'tool',
70
+ 'openai', 'google', 'anthropic', ':online',
71
71
  ];
72
72
 
73
+ const [GEMINI_25_FLASH, GEMINI_30_PRO]
74
+ = [`gemini-2.5-flash${ONLINE}`, `gemini-3-pro-preview${ONLINE}`];
73
75
  const [tool, messages, text]
74
76
  = [type => ({ type }), messages => ({ messages }), text => ({ text })];
75
77
  const [CODE_INTERPRETER, RETRIEVAL, FUNCTION]
@@ -88,7 +90,7 @@ const getProviderIcon = provider => PROVIDER_ICONS[provider] || '🔮';
88
90
  const libOpenAi = async opts => await need('openai', { ...opts, raw: true });
89
91
  const OpenAI = async opts => new (await libOpenAi(opts)).OpenAI(opts);
90
92
  const OPENAI_RULES = {
91
- source: 'openai',
93
+ source: S_OPENAI, icon: '⚛️',
92
94
  contextWindow: kT(400), maxOutputTokens: k(128),
93
95
  imageCostTokens: ~~(OPENAI_HI_RES_SIZE / MAX_TIRE * 140 + 70),
94
96
  maxFileSize: m(50), maxImageSize: OPENAI_HI_RES_SIZE,
@@ -101,7 +103,7 @@ const OPENAI_RULES = {
101
103
  };
102
104
 
103
105
  const GEMINI_RULES = {
104
- source: 'google',
106
+ source: S_GOOGLE, icon: '♊️',
105
107
  json: true, audioCostTokens: 1000 * 1000 * 1, // 8.4 hours => 1 million tokens
106
108
  imageCostTokens: ~~(v8k / MAX_TIRE * 258), maxAudioLength: hour(8.4),
107
109
  maxAudioPerPrompt: 1, maxFileSize: m(20), maxImagePerPrompt: 3000,
@@ -118,7 +120,7 @@ const GEMINI_RULES = {
118
120
  };
119
121
 
120
122
  const DEEPSEEK_R1_RULES = {
121
- contextWindow: kT(128), maxOutputTokens: k(8),
123
+ icon: '🐬', contextWindow: kT(128), maxOutputTokens: k(8),
122
124
  reasoning: true,
123
125
  };
124
126
 
@@ -126,30 +128,30 @@ const DEEPSEEK_R1_RULES = {
126
128
  // https://cloud.google.com/vertex-ai/docs/generative-ai/learn/models
127
129
  // https://openrouter.ai/docs/features/multimodal/audio (only support input audio)
128
130
  const MODELS = {
129
- [GEMINI_30_PRO]: {
130
- ...GEMINI_RULES, contextWindow: m(1), maxOutputTokens: k(64),
131
- reasoning: true, tools: true,
132
- },
131
+ // fast and balanced models
133
132
  [GEMINI_25_FLASH]: {
134
133
  ...GEMINI_RULES, contextWindow: m(1), maxOutputTokens: k(64),
135
134
  fast: true, reasoning: true, tools: true,
135
+ json: false, // issue with json output via OpenRouter
136
+ // https://gemini.google.com/app/c680748b3307790b
137
+ },
138
+ // strong and fast
139
+ [GPT_51]: { ...OPENAI_RULES, fast: true },
140
+ // stronger but slow
141
+ [GEMINI_30_PRO]: {
142
+ ...GEMINI_RULES, contextWindow: m(1), maxOutputTokens: k(64),
143
+ reasoning: true, tools: true,
136
144
  },
145
+ // models with unique capabilities
137
146
  [GEMINI_25_FLASH_IMAGE]: {
138
- ...GEMINI_RULES, contextWindow: k(64), maxOutputTokens: k(32),
147
+ ...GEMINI_RULES, icon: '🍌', label: 'Nano Banana',
148
+ contextWindow: k(64), maxOutputTokens: k(32),
139
149
  fast: true, image: true,
140
150
  },
141
- [GPT_51]: { ...OPENAI_RULES, fast: true },
142
151
  [GPT_51_CODEX]: { ...OPENAI_RULES },
143
152
  [GPT_5_IMAGE]: { ...OPENAI_RULES, image: true },
144
- [GEMMA_3_27B]: {
145
- contextWindow: kT(128), maxOutputTokens: k(8),
146
- imageCostTokens: 256, maxImageSize: 896 * 896,
147
- supportedMimeTypes: [MIME_PNG, MIME_JPEG, MIME_GIF],
148
- fast: true, json: true, vision: true,
149
- defaultProvider: OLLAMA,
150
- },
151
153
  [JINA_DEEPSEARCH]: {
152
- contextWindow: Infinity, maxInputTokens: Infinity,
154
+ label: '✴️', contextWindow: Infinity, maxInputTokens: Infinity,
153
155
  maxOutputTokens: Infinity, imageCostTokens: 0, maxImageSize: Infinity,
154
156
  supportedMimeTypes: [MIME_PNG, MIME_JPEG, MIME_TEXT, MIME_WEBP, MIME_PDF],
155
157
  reasoning: true, json: true, vision: true,
@@ -158,6 +160,7 @@ const MODELS = {
158
160
  [DEEPSEEK_R1]: DEEPSEEK_R1_RULES,
159
161
  [SF_DEEPSEEK_R1]: { ...DEEPSEEK_R1_RULES, defaultProvider: SILICONFLOW },
160
162
  [CLOUD_SONNET_45]: {
163
+ source: S_ANTHROPIC, icon: '✳️',
161
164
  contextWindow: kT(200), maxOutputTokens: kT(64),
162
165
  documentCostTokens: 3000 * 10, maxDocumentFile: m(32),
163
166
  maxDocumentPages: 100, imageCostTokens: ~~(v8k / 750),
@@ -166,6 +169,14 @@ const MODELS = {
166
169
  json: true, reasoning: true, tools: true, vision: true,
167
170
  defaultProvider: OPENROUTER,
168
171
  },
172
+ // best local model
173
+ [GEMMA_3_27B]: {
174
+ label: '❇️', contextWindow: kT(128), maxOutputTokens: k(8),
175
+ imageCostTokens: 256, maxImageSize: 896 * 896,
176
+ supportedMimeTypes: [MIME_PNG, MIME_JPEG, MIME_GIF],
177
+ fast: true, json: true, vision: true,
178
+ defaultProvider: OLLAMA,
179
+ },
169
180
  // https://docs.anthropic.com/en/docs/build-with-claude/vision
170
181
  // https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/claude/sonnet-4-5
171
182
  };
@@ -185,46 +196,47 @@ for (const n in MODELS) {
185
196
  ATTACHMENT_TOKEN_COST, MODELS[n].imageCostTokens || 0
186
197
  ) : MODELS[n].imageCostTokens;
187
198
  }
188
- MODELS[AUTO] = { name: AUTO, defaultProvider: OPENROUTER, };
189
- for (const n of [GPT_51, GPT_51_CODEX, GEMINI_30_PRO, GEMINI_25_FLASH]) {
190
- // get the most restrictive limits
191
- for (const key of [
192
- 'contextWindow', 'maxInputTokens', 'maxDocumentFile', 'maxAudioLength',
193
- 'maxImagePerPrompt', 'maxFileSize', 'maxImageSize', 'maxOutputTokens',
194
- 'maxAudioPerPrompt', 'maxDocumentPages', 'maxUrlSize', 'maxVideoLength',
195
- 'maxVideoPerPrompt',
196
- ]) {
197
- MODELS[AUTO][key] = Math.min(
198
- MODELS[AUTO][key] || Infinity, MODELS[n][key] || Infinity,
199
- );
200
- }
201
- // get the most permissive costs
202
- for (const key of [
203
- 'documentCostTokens', 'imageCostTokens', 'audioCostTokens',
204
- ]) {
205
- MODELS[AUTO][key] = Math.max(
206
- MODELS[AUTO][key] || 0, MODELS[n][key] || 0,
207
- );
208
- }
209
- // combine supported types
210
- for (const key of [
211
- 'supportedAudioTypes', 'supportedDocTypes', 'supportedMimeTypes',
212
- ]) {
213
- MODELS[AUTO][key] = [...new Set(
214
- [...MODELS[AUTO][key] || [], ...MODELS[n][key] || []]
215
- )];
216
- }
217
- // for other features, if any model supports it, then AUTO supports it
218
- for (const key of [
219
- 'json', 'reasoning', 'tools', 'vision', 'fast', 'deepsearch', 'image',
220
- ]) {
221
- MODELS[AUTO][key] = MODELS[AUTO][key] || MODELS[n][key];
222
- }
223
- // catch first possible support
224
- for (const key of ['audio']) {
225
- MODELS[AUTO][key] = MODELS[AUTO][key] || MODELS[n][key];
226
- }
227
- };
199
+ // Auto model have some issues with tools and reasoning, so we disable them here
200
+ // MODELS[AUTO] = { name: AUTO, defaultProvider: OPENROUTER, };
201
+ // for (const n of [GPT_51, GPT_51_CODEX, GEMINI_30_PRO, GEMINI_25_FLASH]) {
202
+ // // get the most restrictive limits
203
+ // for (const key of [
204
+ // 'contextWindow', 'maxInputTokens', 'maxDocumentFile', 'maxAudioLength',
205
+ // 'maxImagePerPrompt', 'maxFileSize', 'maxImageSize', 'maxOutputTokens',
206
+ // 'maxAudioPerPrompt', 'maxDocumentPages', 'maxUrlSize', 'maxVideoLength',
207
+ // 'maxVideoPerPrompt',
208
+ // ]) {
209
+ // MODELS[AUTO][key] = Math.min(
210
+ // MODELS[AUTO][key] || Infinity, MODELS[n][key] || Infinity,
211
+ // );
212
+ // }
213
+ // // get the most permissive costs
214
+ // for (const key of [
215
+ // 'documentCostTokens', 'imageCostTokens', 'audioCostTokens',
216
+ // ]) {
217
+ // MODELS[AUTO][key] = Math.max(
218
+ // MODELS[AUTO][key] || 0, MODELS[n][key] || 0,
219
+ // );
220
+ // }
221
+ // // combine supported types
222
+ // for (const key of [
223
+ // 'supportedAudioTypes', 'supportedDocTypes', 'supportedMimeTypes',
224
+ // ]) {
225
+ // MODELS[AUTO][key] = [...new Set(
226
+ // [...MODELS[AUTO][key] || [], ...MODELS[n][key] || []]
227
+ // )];
228
+ // }
229
+ // // for other features, if any model supports it, then AUTO supports it
230
+ // for (const key of [
231
+ // 'json', 'reasoning', 'tools', 'vision', 'fast', 'deepsearch', 'image',
232
+ // ]) {
233
+ // MODELS[AUTO][key] = MODELS[AUTO][key] || MODELS[n][key];
234
+ // }
235
+ // // catch first possible support
236
+ // for (const key of ['audio']) {
237
+ // MODELS[AUTO][key] = MODELS[AUTO][key] || MODELS[n][key];
238
+ // }
239
+ // };
228
240
 
229
241
  // Default models for each provider
230
242
  const DEFAULT_MODELS = {
@@ -301,7 +313,7 @@ const tools = [
301
313
  }
302
314
  },
303
315
  func: async args => (await distill(args?.url))?.summary,
304
- showReq: true,
316
+ showReq: true, replaced: ONLINE,
305
317
  },
306
318
  {
307
319
  def: {
@@ -322,8 +334,7 @@ const tools = [
322
334
  }
323
335
  },
324
336
  func: async args => await search(args?.keyword),
325
- showReq: true,
326
- depend: checkSearch,
337
+ showReq: true, replaced: ONLINE, depend: checkSearch,
327
338
  },
328
339
  ];
329
340
 
@@ -343,8 +354,8 @@ const buildAiId = (provider, model) => [
343
354
  ].map(x => ensureString(x, { case: 'SNAKE' })).join('_');
344
355
 
345
356
  const buildAiName = (provider, model) => [
346
- getProviderIcon(provider), provider,
347
- `(${isOpenrouter(provider, model) ? `${model.source}/` : ''}${model.name})`
357
+ model?.icon || getProviderIcon(provider), provider,
358
+ `(${isOpenrouter(provider, model) ? `${model.source}/` : ''}${model.label || model.name})`
348
359
  ].join(' ');
349
360
 
350
361
  const buildAiFeatures = model => Object.entries(FEATURE_ICONS).map(
@@ -446,6 +457,8 @@ const packAi = (ais, options = {}) => {
446
457
  };
447
458
 
448
459
  const getAi = async (id, options = {}) => {
460
+ options?.select || (options.select = {});
461
+ options?.jsonMode && (options.select.json = true);
449
462
  if (id) {
450
463
  const ai = ais.find(x => x.id === id);
451
464
  assert(ai, `AI not found: ${id}.`);
@@ -573,9 +586,11 @@ const getInfoEnd = text => Math.max(...[THINK_END, TOOLS_END].map(x => {
573
586
 
574
587
  // @todo: escape ``` in think and tools
575
588
  const packResp = async (resp, options) => {
589
+ // print(resp);
590
+ // return;
576
591
  if (options?.raw) { return resp; }
577
592
  let [
578
- txt, audio, images, references, simpleText, referencesMarkdown, end,
593
+ txt, audio, images, annotations, simpleText, annotationsMarkdown, end,
579
594
  json, audioMimeType,
580
595
  ] = [
581
596
  resp.text || '', // ChatGPT / Claude / Gemini / Ollama
@@ -611,39 +626,25 @@ const packResp = async (resp, options) => {
611
626
  else if (options?.simple && options?.imageMode) { return images; }
612
627
  else if (options?.simple) { return simpleText; }
613
628
  else if (options?.jsonMode) { txt = simpleText; }
614
- // references debug codes:
615
- // references = {
616
- // "segments": [
617
- // {
618
- // "startIndex": 387,
619
- // "endIndex": 477,
620
- // "text": "It also provides live weather reports from Shanghai weather stations and weather warnings.",
621
- // "indices": [
622
- // 0
623
- // ],
624
- // "confidence": [
625
- // 0.94840443
626
- // ]
627
- // },
628
- // ],
629
- // "links": [
630
- // {
631
- // "uri": "https://vertexaisearch.cloud.google.com/grounding-api-redirect/AYygrcRVExzEYZU-23c6gKNSOJjLvSpI4CHtVmYJZaTLKd5N9GF-38GNyC2c9arn689-dmmpMh0Vd85x0kQp0IVY7BQMl1ugEYzy_IlDF-L3wFqf9xWHelAZF4cJa2LnWeUQsjyyTnYFRUs7nhlVoDVu1qYF0uLtVIjdyl5NH0PM92A=",
632
- // "title": "weather-forecast.com"
633
- // },
634
- // ]
635
- // };
636
- if (references?.segments?.length && references?.links?.length) {
637
- for (let i = references.segments.length - 1; i >= 0; i--) {
638
- let idx = txt.indexOf(references.segments[i].text);
639
- if (idx < 0) { continue; }
640
- idx += references.segments[i].text.length;
641
- txt = txt.slice(0, idx)
642
- + references.segments[i].indices.map(y => ` (${y + 1})`).join('')
643
- + txt.slice(idx);
644
- }
645
- referencesMarkdown = 'References:\n\n' + references.links.map(
646
- (x, i) => `${i + 1}. [${x.title}](${x.uri})`
629
+ // annotations debug codes:
630
+ // annotations = [
631
+ // {
632
+ // "type": "url_citation",
633
+ // "url_citation": {
634
+ // "end_index": 0,
635
+ // "start_index": 0,
636
+ // "title": "在線時鐘- 目前時間- 線上時鐘- 時鐘線上 - 鬧鐘",
637
+ // "url": "https://naozhong.tw/shijian/",
638
+ // "content": "- [鬧鐘](https://naozhong.tw/)\n- [計時器](https://naozhong.tw/jishiqi/)\n- [碼錶](https://naozhong.tw/miaobiao/)\n- [時間](https://naozhong.tw/shijian/)\n\n# 現在時間\n\n加入\n\n- [編輯](javascript:;)\n- [移至頂端](javascript:;)\n- [上移](javascript:;)\n- [下移](javascript:;)\n- [刪除](javascript:;)\n\n# 最常用\n\n| | |\n| --- | --- |\n| [台北](https://naozhong.tw/shijian/%E5%8F%B0%E5%8C%97/) | 10:09:14 |\n| [北京,中國](https://naozhong.tw/shijian/%E5%8C%97%E4%BA%AC-%E4%B8%AD%E5%9C%8B/) | 10:09:14 |\n| [上海,中國](https://naozhong.tw/shijian/%E4%B8%8A%E6%B5%B7-%E4%B8%AD%E5%9C%8B/) | 10:09:14 |\n| [烏魯木齊,中國](https://naozhong.tw/shijian/%E7%83%8F%E9%AD%AF%"
639
+ // }
640
+ // },
641
+ // ];
642
+ if (annotations?.length) {
643
+ annotations = annotations.filter(x => x?.type === 'url_citation').map(
644
+ x => ({ type: x.type, ...x.url_citation })
645
+ );
646
+ annotationsMarkdown = 'References:\n\n' + annotations.map(
647
+ (x, i) => `${i + 1}. [${x.title}](${x.url})`
647
648
  ).join('\n');
648
649
  }
649
650
  txt = txt.split('\n');
@@ -672,11 +673,10 @@ const packResp = async (resp, options) => {
672
673
  }
673
674
  txt = txt.join('\n');
674
675
  !options?.delta && !options?.processing && (txt = txt.trim());
675
- print(options);
676
676
  return {
677
677
  ...text(txt), ...options?.jsonMode ? { json } : {},
678
- ...references ? { references } : {},
679
- ...referencesMarkdown ? { referencesMarkdown } : {},
678
+ ...annotations ? { annotations } : {},
679
+ ...annotationsMarkdown ? { annotationsMarkdown } : {},
680
680
  ...audio ? { audio } : {}, ...images?.length ? { images } : {},
681
681
  processing: !!options?.processing,
682
682
  model: [
@@ -797,9 +797,10 @@ const promptOpenAI = async (aiId, content, options = {}) => {
797
797
  let [
798
798
  result, resultAudio, resultImages, resultReasoning, event, resultTools,
799
799
  responded, modalities, source, reasoningEnd, reasoning_details,
800
+ annotations,
800
801
  ] = [
801
802
  options.result ?? '', Buffer.alloc(0), [], '', null, [], false,
802
- options.modalities, model?.source, false, []
803
+ options.modalities, model?.source, false, [], [],
803
804
  ];
804
805
  options.provider = provider;
805
806
  options.model = options.model || model.name;
@@ -813,8 +814,13 @@ const promptOpenAI = async (aiId, content, options = {}) => {
813
814
  } else if (!modalities && model.image) {
814
815
  modalities = [TEXT, IMAGE];
815
816
  }
816
- const googleImageMode = source === 'google' && modalities?.has?.(IMAGE);
817
- const targetModel = `${isOpenrouter(provider, model) ? `${source}/` : ''}${options.model}`;
817
+ const googleImageMode = source === S_GOOGLE && modalities?.has?.(IMAGE);
818
+ // pricy: https://openrouter.ai/docs/features/web-search
819
+ const ext = ''; // options.jsonMode ? '' : ONLINE;
820
+ const targetModel = `${isOpenrouter(provider, model) ? `${source}/` : ''}${options.model}${ext}`;
821
+ const packedTools = (targetModel.endsWith(ONLINE)
822
+ ? _tools.filter(x => x?.replaced !== ONLINE)
823
+ : _tools).map(x => x.def);
818
824
  const resp = await client.chat.completions.create({
819
825
  model: targetModel, ...history,
820
826
  ...options.jsonMode ? { response_format: { type: JSON_OBJECT } } : {},
@@ -823,10 +829,9 @@ const promptOpenAI = async (aiId, content, options = {}) => {
823
829
  modalities?.find?.(x => x === AUDIO)
824
830
  && { voice: DEFAULT_MODELS[OPENAI_VOICE], format: 'pcm16' }
825
831
  ), ...model?.tools && !googleImageMode ? {
826
- tools: options.tools ?? _tools.map(x => x.def), tool_choice: 'auto',
827
- } : {},
828
- store: true, stream: true,
829
- reasoning_effort: options.reasoning_effort,
832
+ tools: options.tools ?? packedTools, tool_choice: 'auto',
833
+ } : {}, store: true, stream: true,
834
+ reasoning: { effort: options.reasoning_effort },
830
835
  });
831
836
  for await (event of resp) {
832
837
  // print(JSON.stringify(event, null, 2));
@@ -845,8 +850,22 @@ const promptOpenAI = async (aiId, content, options = {}) => {
845
850
  const deltaAudio = delta.audio?.data ? await convert(
846
851
  delta.audio.data, { input: BASE64, expected: BUFFER }
847
852
  ) : Buffer.alloc(0);
848
- delta?.reasoning_details?.length
849
- && reasoning_details.push(...delta.reasoning_details);
853
+ delta?.annotations?.length && annotations.push(...delta.annotations);
854
+ // for anthropic reasoning details need to be merged in streaming
855
+ if (delta?.reasoning_details?.length) {
856
+ reasoning_details.length || reasoning_details.push({});
857
+ for (const item of delta.reasoning_details) {
858
+ for (const key in item) {
859
+ if (key === 'text') {
860
+ reasoning_details[0][key] = (
861
+ reasoning_details[0][key] || ''
862
+ ) + item[key];
863
+ continue;
864
+ }
865
+ reasoning_details[0][key] = item[key];
866
+ }
867
+ }
868
+ }
850
869
  for (const x of delta.tool_calls || []) {
851
870
  let curFunc = resultTools.find(y => y.index === x.index);
852
871
  curFunc || (resultTools.push(curFunc = {}));
@@ -862,9 +881,11 @@ const promptOpenAI = async (aiId, content, options = {}) => {
862
881
  options.result && deltaText
863
882
  && (responded = responded || (deltaText = `\n\n${deltaText}`));
864
883
  resultReasoning += delteReasoning;
884
+ // the \n\n is needed for Interleaved Thinking:
885
+ // tools => reasoning => tools => reasoning ...
865
886
  delteReasoning && delteReasoning === resultReasoning
866
- && (delteReasoning = `${THINK_STR}\n${delteReasoning}`);
867
- resultReasoning && deltaText && !reasoningEnd && (
887
+ && (delteReasoning = `${result ? '\n\n' : ''}${THINK_STR}\n${delteReasoning}`);
888
+ resultReasoning && (deltaText || delta.tool_calls?.length) && !reasoningEnd && (
868
889
  reasoningEnd = delteReasoning = `${delteReasoning}${THINK_END}\n\n`
869
890
  );
870
891
  deltaText = delteReasoning + deltaText;
@@ -881,12 +902,22 @@ const promptOpenAI = async (aiId, content, options = {}) => {
881
902
  }, options);
882
903
  }
883
904
  event = {
884
- role: assistant, text: result,
885
- ...reasoning_details?.length ? { reasoning_details } : {},
886
- tool_calls: resultTools,
905
+ role: assistant, text: result, tool_calls: resultTools,
887
906
  ...resultImages.length ? { images: resultImages } : {},
888
907
  ...resultAudio.length ? { audio: { data: resultAudio } } : {},
908
+ ...annotations.length ? { annotations } : {},
889
909
  };
910
+ switch (source) {
911
+ case S_ANTHROPIC:
912
+ event.content = reasoning_details.map(x => ({
913
+ type: 'thinking', thinking: x.text,
914
+ ...x.signature ? { signature: x.signature } : {},
915
+ }));
916
+ break;
917
+ case S_GOOGLE:
918
+ reasoning_details?.length
919
+ && (event.reasoning_details = reasoning_details);
920
+ }
890
921
  const { toolsResult, toolsResponse }
891
922
  = await handleToolsCall(event, { ...options, result });
892
923
  if (toolsResult.length
@@ -899,122 +930,6 @@ const promptOpenAI = async (aiId, content, options = {}) => {
899
930
  return await packResp(event, options);
900
931
  };
901
932
 
902
- // const packGeminiReferences = (chunks, supports) => {
903
- // let references = null;
904
- // if (chunks?.length && supports?.length) {
905
- // references = { segments: [], links: [] };
906
- // supports.map(s => references.segments.push({
907
- // ...s.segment, indices: s.groundingChunkIndices,
908
- // confidence: s.confidenceScores,
909
- // }));
910
- // chunks.map(c => references.links.push(c.web));
911
- // }
912
- // return references;
913
- // };
914
-
915
- // const promptGemini = async (aiId, content, options = {}) => {
916
- // let { provider, client, model } = await getAi(aiId);
917
- // let [
918
- // event, result, text, thinking, references, functionCalls, responded,
919
- // images, thinkEnd,
920
- // ] = [null, options.result ?? '', '', '', null, [], false, [], false];
921
- // options.model = options.model || model.name;
922
- // model?.image === true && (options.imageMode = true);
923
- // assert(!(options.imageMode && !model.image), 'Image mode is not supported.');
924
- // if (options.imageMode && String.isString(model.image)) {
925
- // options.model = model.image;
926
- // options.imageMode = true;
927
- // model = MODELS[options.model];
928
- // }
929
- // options.flavor = GEMINI;
930
- // const { systemPrompt: systemInstruction, history, prompt }
931
- // = await buildPrompts(model, content, options);
932
- // const responseModalities = options.modalities
933
- // || (options.imageMode ? [TEXT, IMAGE] : undefined)
934
- // || (options.audioMode ? [TEXT, AUDIO] : undefined);
935
- // const chat = client.chats.create({
936
- // model: options.model, history, config: {
937
- // responseMimeType: options.jsonMode ? MIME_JSON : MIME_TEXT,
938
- // ...model.reasoning ? {
939
- // thinkingConfig: { includeThoughts: true },
940
- // } : {}, systemInstruction, responseModalities,
941
- // ...options?.config || {}, ...model?.tools && !options.jsonMode
942
- // && ![GEMINI_25_FLASH_IMAGE].includes(options.model)
943
- // ? (options.tools ?? {
944
- // tools: [
945
- // // @todo: Gemini will failed when using these tools together.
946
- // // https://ai.google.dev/gemini-api/docs/function-calling
947
- // // { codeExecution: {} },
948
- // // { googleSearch: {} },
949
- // // { urlContext: {} },
950
- // // @todo: test these tools in next version 👆
951
- // {
952
- // functionDeclarations: (
953
- // await toolsGemini({ provider })
954
- // ).map(x => x.def)
955
- // },
956
- // ], toolConfig: { functionCallingConfig: { mode: 'AUTO' } },
957
- // }) : {},
958
- // },
959
- // });
960
- // const resp = await chat.sendMessageStream({ message: prompt });
961
- // for await (const chunk of resp) {
962
- // assert(
963
- // !chunk?.promptFeedback?.blockReason,
964
- // chunk?.promptFeedback?.blockReason
965
- // );
966
- // event = chunk?.candidates?.[0];
967
- // let [deltaText, deltaThink, deltaImages] = ['', '', []];
968
- // event?.content?.parts?.map(x => {
969
- // if (x.text && x.thought) { deltaThink = x.text; }
970
- // else if (x.text) { deltaText = x.text; }
971
- // else if (x.functionCall) { functionCalls.push(x); }
972
- // else if (x.inlineData?.mimeType === MIME_PNG) {
973
- // deltaImages.push(x.inlineData);
974
- // images.push(x.inlineData);
975
- // }
976
- // });
977
- // text += deltaText;
978
- // thinking += deltaThink;
979
- // deltaThink && deltaThink === thinking
980
- // && (deltaThink = `${THINK_STR}\n${deltaThink}`);
981
- // thinking && deltaText && !thinkEnd
982
- // && (thinkEnd = deltaThink = `${deltaThink}${THINK_END}\n\n`);
983
- // deltaText = deltaThink + deltaText;
984
- // const rfc = packGeminiReferences(
985
- // event?.groundingMetadata?.groundingChunks,
986
- // event?.groundingMetadata?.groundingSupports
987
- // );
988
- // rfc && (references = rfc);
989
- // options.result && deltaText
990
- // && (responded = responded || (deltaText = `\n\n${deltaText}`));
991
- // result += deltaText;
992
- // (deltaText || deltaImages.length) && await streamResp({
993
- // text: options.delta ? deltaText : result,
994
- // images: options.delta ? deltaImages : images,
995
- // }, options);
996
- // }
997
- // event = {
998
- // role: MODEL, parts: [
999
- // ...thinking ? [{ thought: true, text: thinking }] : [],
1000
- // ...text ? [{ text }] : [],
1001
- // ...functionCalls,
1002
- // ],
1003
- // };
1004
- // const { toolsResult, toolsResponse } = await handleToolsCall(
1005
- // event, { ...options, result, flavor: GEMINI }
1006
- // );
1007
- // if (toolsResult.length
1008
- // && countToolCalls(toolsResponse) < MAX_TOOL_RECURSION) {
1009
- // return promptGemini(aiId, content, {
1010
- // ...options || {}, result: toolsResponse,
1011
- // toolsResult: [...options?.toolsResult || [], ...toolsResult],
1012
- // });
1013
- // }
1014
- // return await packResp({
1015
- // text: mergeMsgs(toolsResponse, toolsResult), images, references,
1016
- // }, options);
1017
- // };
1018
933
 
1019
934
  const initChat = async (options = {}) => {
1020
935
  if (options.sessions) {
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": "1999.1.98",
4
+ "version": "2000.1.2",
5
5
  "private": false,
6
6
  "homepage": "https://github.com/Leask/utilitas",
7
7
  "main": "index.mjs",
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": "1999.1.98",
4
+ "version": "2000.1.2",
5
5
  "private": false,
6
6
  "homepage": "https://github.com/Leask/utilitas",
7
7
  "main": "index.mjs",