utilitas 1999.1.97 → 1999.1.99

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, 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', 'gemma3:27b', 'Anthropic', 7680 * 4320, [],
63
- 30, { log: true }, 'Alan', 'user', { role: 'system' }, 'assistant',
64
- 'model', 'json_object', 1.1, 'Content is required.', 2048 * 2048,
65
- x => 1024 * x, x => 1000 * x, x => 1024 * 1024 * x, x => 60 * x,
66
- 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,29 +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
- [GPT_51]: { ...OPENAI_RULES, fast: true },
130
- [GPT_51_CODEX]: { ...OPENAI_RULES },
131
- [GEMINI_25_FLASH_IMAGE]: {
132
- ...GEMINI_RULES, contextWindow: k(64), maxOutputTokens: k(32),
133
- fast: true, image: true,
134
- },
131
+ // fast and balanced models
135
132
  [GEMINI_25_FLASH]: {
136
133
  ...GEMINI_RULES, contextWindow: m(1), maxOutputTokens: k(64),
137
134
  fast: true, reasoning: true, tools: true,
135
+ json: false, // issue with json output via OpenRouter
136
+ // https://gemini.google.com/app/c680748b3307790b
138
137
  },
138
+ // strong and fast
139
+ [GPT_51]: { ...OPENAI_RULES, fast: true },
140
+ // stronger but slow
139
141
  [GEMINI_30_PRO]: {
140
142
  ...GEMINI_RULES, contextWindow: m(1), maxOutputTokens: k(64),
141
143
  reasoning: true, tools: true,
142
144
  },
143
- [GEMMA_3_27B]: {
144
- contextWindow: kT(128), maxOutputTokens: k(8),
145
- imageCostTokens: 256, maxImageSize: 896 * 896,
146
- supportedMimeTypes: [MIME_PNG, MIME_JPEG, MIME_GIF],
147
- fast: true, json: true, vision: true,
148
- defaultProvider: OLLAMA,
145
+ // models with unique capabilities
146
+ [GEMINI_25_FLASH_IMAGE]: {
147
+ ...GEMINI_RULES, icon: '🍌', label: 'Nano Banana',
148
+ contextWindow: k(64), maxOutputTokens: k(32),
149
+ fast: true, image: true,
149
150
  },
151
+ [GPT_51_CODEX]: { ...OPENAI_RULES },
152
+ [GPT_5_IMAGE]: { ...OPENAI_RULES, image: true },
150
153
  [JINA_DEEPSEARCH]: {
151
- contextWindow: Infinity, maxInputTokens: Infinity,
154
+ label: '✴️', contextWindow: Infinity, maxInputTokens: Infinity,
152
155
  maxOutputTokens: Infinity, imageCostTokens: 0, maxImageSize: Infinity,
153
156
  supportedMimeTypes: [MIME_PNG, MIME_JPEG, MIME_TEXT, MIME_WEBP, MIME_PDF],
154
157
  reasoning: true, json: true, vision: true,
@@ -157,6 +160,7 @@ const MODELS = {
157
160
  [DEEPSEEK_R1]: DEEPSEEK_R1_RULES,
158
161
  [SF_DEEPSEEK_R1]: { ...DEEPSEEK_R1_RULES, defaultProvider: SILICONFLOW },
159
162
  [CLOUD_SONNET_45]: {
163
+ source: S_ANTHROPIC, icon: '✳️',
160
164
  contextWindow: kT(200), maxOutputTokens: kT(64),
161
165
  documentCostTokens: 3000 * 10, maxDocumentFile: m(32),
162
166
  maxDocumentPages: 100, imageCostTokens: ~~(v8k / 750),
@@ -165,6 +169,14 @@ const MODELS = {
165
169
  json: true, reasoning: true, tools: true, vision: true,
166
170
  defaultProvider: OPENROUTER,
167
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
+ },
168
180
  // https://docs.anthropic.com/en/docs/build-with-claude/vision
169
181
  // https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/claude/sonnet-4-5
170
182
  };
@@ -184,46 +196,47 @@ for (const n in MODELS) {
184
196
  ATTACHMENT_TOKEN_COST, MODELS[n].imageCostTokens || 0
185
197
  ) : MODELS[n].imageCostTokens;
186
198
  }
187
- MODELS[AUTO] = { name: AUTO, defaultProvider: OPENROUTER, };
188
- for (const n of [GPT_51, GPT_51_CODEX, GEMINI_30_PRO, GEMINI_25_FLASH]) {
189
- // get the most restrictive limits
190
- for (const key of [
191
- 'contextWindow', 'maxInputTokens', 'maxDocumentFile', 'maxAudioLength',
192
- 'maxImagePerPrompt', 'maxFileSize', 'maxImageSize', 'maxOutputTokens',
193
- 'maxAudioPerPrompt', 'maxDocumentPages', 'maxUrlSize', 'maxVideoLength',
194
- 'maxVideoPerPrompt',
195
- ]) {
196
- MODELS[AUTO][key] = Math.min(
197
- MODELS[AUTO][key] || Infinity, MODELS[n][key] || Infinity,
198
- );
199
- }
200
- // get the most permissive costs
201
- for (const key of [
202
- 'documentCostTokens', 'imageCostTokens', 'audioCostTokens',
203
- ]) {
204
- MODELS[AUTO][key] = Math.max(
205
- MODELS[AUTO][key] || 0, MODELS[n][key] || 0,
206
- );
207
- }
208
- // combine supported types
209
- for (const key of [
210
- 'supportedAudioTypes', 'supportedDocTypes', 'supportedMimeTypes',
211
- ]) {
212
- MODELS[AUTO][key] = [...new Set(
213
- [...MODELS[AUTO][key] || [], ...MODELS[n][key] || []]
214
- )];
215
- }
216
- // for other features, if any model supports it, then AUTO supports it
217
- for (const key of [
218
- 'json', 'reasoning', 'tools', 'vision', 'fast', 'deepsearch', 'image',
219
- ]) {
220
- MODELS[AUTO][key] = MODELS[AUTO][key] || MODELS[n][key];
221
- }
222
- // catch first possible support
223
- for (const key of ['audio']) {
224
- MODELS[AUTO][key] = MODELS[AUTO][key] || MODELS[n][key];
225
- }
226
- };
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
+ // };
227
240
 
228
241
  // Default models for each provider
229
242
  const DEFAULT_MODELS = {
@@ -255,7 +268,7 @@ const tokenRatioByCharacters = Math.max(
255
268
  );
256
269
 
257
270
 
258
- let tokeniser;
271
+ let tokeniser, _tools;
259
272
 
260
273
  const unifyProvider = provider => {
261
274
  assert(provider = (provider || '').trim(), 'AI provider is required.');
@@ -300,7 +313,7 @@ const tools = [
300
313
  }
301
314
  },
302
315
  func: async args => (await distill(args?.url))?.summary,
303
- showReq: true,
316
+ showReq: true, replaced: ONLINE,
304
317
  },
305
318
  {
306
319
  def: {
@@ -321,12 +334,11 @@ const tools = [
321
334
  }
322
335
  },
323
336
  func: async args => await search(args?.keyword),
324
- showReq: true,
325
- depend: checkSearch,
337
+ showReq: true, replaced: ONLINE, depend: checkSearch,
326
338
  },
327
339
  ];
328
340
 
329
- const toolsOpenAI = async () => {
341
+ const packTools = async () => {
330
342
  const _tools = [];
331
343
  for (const t of tools) {
332
344
  (t.depend ? await t.depend() : true) ? _tools.push(t) : log(
@@ -342,8 +354,8 @@ const buildAiId = (provider, model) => [
342
354
  ].map(x => ensureString(x, { case: 'SNAKE' })).join('_');
343
355
 
344
356
  const buildAiName = (provider, model) => [
345
- getProviderIcon(provider), provider,
346
- `(${isOpenrouter(provider, model) ? `${model.source}/` : ''}${model.name})`
357
+ model?.icon || getProviderIcon(provider), provider,
358
+ `(${isOpenrouter(provider, model) ? `${model.source}/` : ''}${model.label || model.name})`
347
359
  ].join(' ');
348
360
 
349
361
  const buildAiFeatures = model => Object.entries(FEATURE_ICONS).map(
@@ -383,6 +395,7 @@ const init = async (options = {}) => {
383
395
  }
384
396
  assert(models.length,
385
397
  `Model name or description is required for provider: ${provider}.`);
398
+ _tools || (_tools = await packTools());
386
399
  switch (provider) {
387
400
  case JINA:
388
401
  assertApiKey(provider, options);
@@ -444,6 +457,8 @@ const packAi = (ais, options = {}) => {
444
457
  };
445
458
 
446
459
  const getAi = async (id, options = {}) => {
460
+ options?.select || (options.select = {});
461
+ options?.jsonMode && (options.select.json = true);
447
462
  if (id) {
448
463
  const ai = ais.find(x => x.id === id);
449
464
  assert(ai, `AI not found: ${id}.`);
@@ -571,9 +586,11 @@ const getInfoEnd = text => Math.max(...[THINK_END, TOOLS_END].map(x => {
571
586
 
572
587
  // @todo: escape ``` in think and tools
573
588
  const packResp = async (resp, options) => {
589
+ // print(resp);
590
+ // return;
574
591
  if (options?.raw) { return resp; }
575
592
  let [
576
- txt, audio, images, references, simpleText, referencesMarkdown, end,
593
+ txt, audio, images, annotations, simpleText, annotationsMarkdown, end,
577
594
  json, audioMimeType,
578
595
  ] = [
579
596
  resp.text || '', // ChatGPT / Claude / Gemini / Ollama
@@ -609,39 +626,25 @@ const packResp = async (resp, options) => {
609
626
  else if (options?.simple && options?.imageMode) { return images; }
610
627
  else if (options?.simple) { return simpleText; }
611
628
  else if (options?.jsonMode) { txt = simpleText; }
612
- // references debug codes:
613
- // references = {
614
- // "segments": [
615
- // {
616
- // "startIndex": 387,
617
- // "endIndex": 477,
618
- // "text": "It also provides live weather reports from Shanghai weather stations and weather warnings.",
619
- // "indices": [
620
- // 0
621
- // ],
622
- // "confidence": [
623
- // 0.94840443
624
- // ]
625
- // },
626
- // ],
627
- // "links": [
628
- // {
629
- // "uri": "https://vertexaisearch.cloud.google.com/grounding-api-redirect/AYygrcRVExzEYZU-23c6gKNSOJjLvSpI4CHtVmYJZaTLKd5N9GF-38GNyC2c9arn689-dmmpMh0Vd85x0kQp0IVY7BQMl1ugEYzy_IlDF-L3wFqf9xWHelAZF4cJa2LnWeUQsjyyTnYFRUs7nhlVoDVu1qYF0uLtVIjdyl5NH0PM92A=",
630
- // "title": "weather-forecast.com"
631
- // },
632
- // ]
633
- // };
634
- if (references?.segments?.length && references?.links?.length) {
635
- for (let i = references.segments.length - 1; i >= 0; i--) {
636
- let idx = txt.indexOf(references.segments[i].text);
637
- if (idx < 0) { continue; }
638
- idx += references.segments[i].text.length;
639
- txt = txt.slice(0, idx)
640
- + references.segments[i].indices.map(y => ` (${y + 1})`).join('')
641
- + txt.slice(idx);
642
- }
643
- referencesMarkdown = 'References:\n\n' + references.links.map(
644
- (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})`
645
648
  ).join('\n');
646
649
  }
647
650
  txt = txt.split('\n');
@@ -672,11 +675,14 @@ const packResp = async (resp, options) => {
672
675
  !options?.delta && !options?.processing && (txt = txt.trim());
673
676
  return {
674
677
  ...text(txt), ...options?.jsonMode ? { json } : {},
675
- ...references ? { references } : {},
676
- ...referencesMarkdown ? { referencesMarkdown } : {},
678
+ ...annotations ? { annotations } : {},
679
+ ...annotationsMarkdown ? { annotationsMarkdown } : {},
677
680
  ...audio ? { audio } : {}, ...images?.length ? { images } : {},
678
681
  processing: !!options?.processing,
679
- model: options?.model,
682
+ model: [
683
+ options.provider, options?.router?.provider,
684
+ options?.router?.model || options?.model,
685
+ ].filter(x => x).join('/'),
680
686
  };
681
687
  };
682
688
 
@@ -790,11 +796,13 @@ const promptOpenAI = async (aiId, content, options = {}) => {
790
796
  let { provider, client, model } = await getAi(aiId);
791
797
  let [
792
798
  result, resultAudio, resultImages, resultReasoning, event, resultTools,
793
- responded, modalities, source, reasoningEnd
799
+ responded, modalities, source, reasoningEnd, reasoning_details,
800
+ annotations,
794
801
  ] = [
795
802
  options.result ?? '', Buffer.alloc(0), [], '', null, [], false,
796
- options.modalities, model?.source, false
803
+ options.modalities, model?.source, false, [], [],
797
804
  ];
805
+ options.provider = provider;
798
806
  options.model = options.model || model.name;
799
807
  const { history }
800
808
  = await buildPrompts(MODELS[options.model], content, options);
@@ -806,8 +814,13 @@ const promptOpenAI = async (aiId, content, options = {}) => {
806
814
  } else if (!modalities && model.image) {
807
815
  modalities = [TEXT, IMAGE];
808
816
  }
809
- const googleImageMode = source === 'google' && modalities?.has?.(IMAGE);
810
- 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);
811
824
  const resp = await client.chat.completions.create({
812
825
  model: targetModel, ...history,
813
826
  ...options.jsonMode ? { response_format: { type: JSON_OBJECT } } : {},
@@ -816,14 +829,15 @@ const promptOpenAI = async (aiId, content, options = {}) => {
816
829
  modalities?.find?.(x => x === AUDIO)
817
830
  && { voice: DEFAULT_MODELS[OPENAI_VOICE], format: 'pcm16' }
818
831
  ), ...model?.tools && !googleImageMode ? {
819
- tools: options.tools ?? (await toolsOpenAI()).map(x => x.def),
820
- tool_choice: 'auto',
821
- } : {},
822
- store: true, stream: true,
832
+ tools: options.tools ?? packedTools, tool_choice: 'auto',
833
+ } : {}, store: true, stream: true,
823
834
  reasoning_effort: options.reasoning_effort,
824
835
  });
825
836
  for await (event of resp) {
826
837
  // print(JSON.stringify(event, null, 2));
838
+ event?.provider && event?.model && (options.router = {
839
+ provider: event.provider, model: event.model,
840
+ });
827
841
  event = event?.choices?.[0] || {};
828
842
  const delta = event.delta || {};
829
843
  let [delteReasoning, deltaText] = [
@@ -836,6 +850,22 @@ const promptOpenAI = async (aiId, content, options = {}) => {
836
850
  const deltaAudio = delta.audio?.data ? await convert(
837
851
  delta.audio.data, { input: BASE64, expected: BUFFER }
838
852
  ) : Buffer.alloc(0);
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
+ }
839
869
  for (const x of delta.tool_calls || []) {
840
870
  let curFunc = resultTools.find(y => y.index === x.index);
841
871
  curFunc || (resultTools.push(curFunc = {}));
@@ -851,9 +881,11 @@ const promptOpenAI = async (aiId, content, options = {}) => {
851
881
  options.result && deltaText
852
882
  && (responded = responded || (deltaText = `\n\n${deltaText}`));
853
883
  resultReasoning += delteReasoning;
884
+ // the \n\n is needed for Interleaved Thinking:
885
+ // tools => reasoning => tools => reasoning ...
854
886
  delteReasoning && delteReasoning === resultReasoning
855
- && (delteReasoning = `${THINK_STR}\n${delteReasoning}`);
856
- resultReasoning && deltaText && !reasoningEnd && (
887
+ && (delteReasoning = `${result ? '\n\n' : ''}${THINK_STR}\n${delteReasoning}`);
888
+ resultReasoning && (deltaText || delta.tool_calls?.length) && !reasoningEnd && (
857
889
  reasoningEnd = delteReasoning = `${delteReasoning}${THINK_END}\n\n`
858
890
  );
859
891
  deltaText = delteReasoning + deltaText;
@@ -873,7 +905,19 @@ const promptOpenAI = async (aiId, content, options = {}) => {
873
905
  role: assistant, text: result, tool_calls: resultTools,
874
906
  ...resultImages.length ? { images: resultImages } : {},
875
907
  ...resultAudio.length ? { audio: { data: resultAudio } } : {},
908
+ ...annotations.length ? { annotations } : {},
876
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
+ }
877
921
  const { toolsResult, toolsResponse }
878
922
  = await handleToolsCall(event, { ...options, result });
879
923
  if (toolsResult.length
@@ -886,122 +930,6 @@ const promptOpenAI = async (aiId, content, options = {}) => {
886
930
  return await packResp(event, options);
887
931
  };
888
932
 
889
- // const packGeminiReferences = (chunks, supports) => {
890
- // let references = null;
891
- // if (chunks?.length && supports?.length) {
892
- // references = { segments: [], links: [] };
893
- // supports.map(s => references.segments.push({
894
- // ...s.segment, indices: s.groundingChunkIndices,
895
- // confidence: s.confidenceScores,
896
- // }));
897
- // chunks.map(c => references.links.push(c.web));
898
- // }
899
- // return references;
900
- // };
901
-
902
- // const promptGemini = async (aiId, content, options = {}) => {
903
- // let { provider, client, model } = await getAi(aiId);
904
- // let [
905
- // event, result, text, thinking, references, functionCalls, responded,
906
- // images, thinkEnd,
907
- // ] = [null, options.result ?? '', '', '', null, [], false, [], false];
908
- // options.model = options.model || model.name;
909
- // model?.image === true && (options.imageMode = true);
910
- // assert(!(options.imageMode && !model.image), 'Image mode is not supported.');
911
- // if (options.imageMode && String.isString(model.image)) {
912
- // options.model = model.image;
913
- // options.imageMode = true;
914
- // model = MODELS[options.model];
915
- // }
916
- // options.flavor = GEMINI;
917
- // const { systemPrompt: systemInstruction, history, prompt }
918
- // = await buildPrompts(model, content, options);
919
- // const responseModalities = options.modalities
920
- // || (options.imageMode ? [TEXT, IMAGE] : undefined)
921
- // || (options.audioMode ? [TEXT, AUDIO] : undefined);
922
- // const chat = client.chats.create({
923
- // model: options.model, history, config: {
924
- // responseMimeType: options.jsonMode ? MIME_JSON : MIME_TEXT,
925
- // ...model.reasoning ? {
926
- // thinkingConfig: { includeThoughts: true },
927
- // } : {}, systemInstruction, responseModalities,
928
- // ...options?.config || {}, ...model?.tools && !options.jsonMode
929
- // && ![GEMINI_25_FLASH_IMAGE].includes(options.model)
930
- // ? (options.tools ?? {
931
- // tools: [
932
- // // @todo: Gemini will failed when using these tools together.
933
- // // https://ai.google.dev/gemini-api/docs/function-calling
934
- // // { codeExecution: {} },
935
- // // { googleSearch: {} },
936
- // // { urlContext: {} },
937
- // // @todo: test these tools in next version 👆
938
- // {
939
- // functionDeclarations: (
940
- // await toolsGemini({ provider })
941
- // ).map(x => x.def)
942
- // },
943
- // ], toolConfig: { functionCallingConfig: { mode: 'AUTO' } },
944
- // }) : {},
945
- // },
946
- // });
947
- // const resp = await chat.sendMessageStream({ message: prompt });
948
- // for await (const chunk of resp) {
949
- // assert(
950
- // !chunk?.promptFeedback?.blockReason,
951
- // chunk?.promptFeedback?.blockReason
952
- // );
953
- // event = chunk?.candidates?.[0];
954
- // let [deltaText, deltaThink, deltaImages] = ['', '', []];
955
- // event?.content?.parts?.map(x => {
956
- // if (x.text && x.thought) { deltaThink = x.text; }
957
- // else if (x.text) { deltaText = x.text; }
958
- // else if (x.functionCall) { functionCalls.push(x); }
959
- // else if (x.inlineData?.mimeType === MIME_PNG) {
960
- // deltaImages.push(x.inlineData);
961
- // images.push(x.inlineData);
962
- // }
963
- // });
964
- // text += deltaText;
965
- // thinking += deltaThink;
966
- // deltaThink && deltaThink === thinking
967
- // && (deltaThink = `${THINK_STR}\n${deltaThink}`);
968
- // thinking && deltaText && !thinkEnd
969
- // && (thinkEnd = deltaThink = `${deltaThink}${THINK_END}\n\n`);
970
- // deltaText = deltaThink + deltaText;
971
- // const rfc = packGeminiReferences(
972
- // event?.groundingMetadata?.groundingChunks,
973
- // event?.groundingMetadata?.groundingSupports
974
- // );
975
- // rfc && (references = rfc);
976
- // options.result && deltaText
977
- // && (responded = responded || (deltaText = `\n\n${deltaText}`));
978
- // result += deltaText;
979
- // (deltaText || deltaImages.length) && await streamResp({
980
- // text: options.delta ? deltaText : result,
981
- // images: options.delta ? deltaImages : images,
982
- // }, options);
983
- // }
984
- // event = {
985
- // role: MODEL, parts: [
986
- // ...thinking ? [{ thought: true, text: thinking }] : [],
987
- // ...text ? [{ text }] : [],
988
- // ...functionCalls,
989
- // ],
990
- // };
991
- // const { toolsResult, toolsResponse } = await handleToolsCall(
992
- // event, { ...options, result, flavor: GEMINI }
993
- // );
994
- // if (toolsResult.length
995
- // && countToolCalls(toolsResponse) < MAX_TOOL_RECURSION) {
996
- // return promptGemini(aiId, content, {
997
- // ...options || {}, result: toolsResponse,
998
- // toolsResult: [...options?.toolsResult || [], ...toolsResult],
999
- // });
1000
- // }
1001
- // return await packResp({
1002
- // text: mergeMsgs(toolsResponse, toolsResult), images, references,
1003
- // }, options);
1004
- // };
1005
933
 
1006
934
  const initChat = async (options = {}) => {
1007
935
  if (options.sessions) {
package/lib/gen.mjs CHANGED
@@ -10,11 +10,11 @@ const _NEED = ['OpenAI', '@google/genai'];
10
10
  const log = (cnt, opt) => _log(cnt, import.meta.url, { time: 1, ...opt || {} });
11
11
  const [
12
12
  clients, OPENAI, GEMINI, BASE64, FILE, BUFFER, ERROR_GENERATING,
13
- IMAGEN_MODEL, OPENAI_MODEL, VEO_MODEL,
13
+ IMAGEN_MODEL, OPENAI_MODEL, VEO_MODEL, IMAGEN_UPSCALE_MODEL,
14
14
  ] = [
15
15
  {}, 'OPENAI', 'GEMINI', 'BASE64', 'FILE', 'BUFFER',
16
16
  'Error generating media.', 'imagen-4.0-ultra-generate-001',
17
- 'gpt-image-1', 'veo-3.1-generate-preview',
17
+ 'gpt-image-1', 'veo-3.1-generate-preview', 'imagen-4.0-upscale-preview',
18
18
  ];
19
19
 
20
20
  const init = async (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": "1999.1.97",
4
+ "version": "1999.1.99",
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.97",
4
+ "version": "1999.1.99",
5
5
  "private": false,
6
6
  "homepage": "https://github.com/Leask/utilitas",
7
7
  "main": "index.mjs",