utilitas 2001.1.136 → 2001.1.139

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
@@ -8,9 +8,9 @@ import {
8
8
  STREAM, FILE, BASE64, BUFFER, DATAURL, MIME_TEXT, MIME_PNG, MIME_JPEG,
9
9
  MIME_MOV, MIME_MPEG, MIME_MP4, MIME_MPG, MIME_AVI, MIME_WMV, MIME_MPEGPS,
10
10
  MIME_FLV, MIME_GIF, MIME_WEBP, MIME_PDF, MIME_AAC, MIME_FLAC, MIME_MP3,
11
- MIME_MPEGA, MIME_M4A, MIME_MPGA, MIME_OPUS, MIME_PCM, MIME_WAV, MIME_WEBM,
12
- MIME_TGPP, MIME_PCM16, MIME_OGG, convert, decodeBase64DataURL, getImageSize,
13
- getMime, getTempPath,
11
+ MIME_MPEGA, MIME_M4A, MIME_MPGA, MIME_OPUS, MIME_PCM, MIME_WAV, MIME_WAVE,
12
+ MIME_WEBM, MIME_TGPP, MIME_PCM16, MIME_OGG, convert, decodeBase64DataURL,
13
+ getImageSize, getMime, getTempPath,
14
14
  } from './storage.mjs';
15
15
 
16
16
  import {
@@ -45,37 +45,32 @@ const STT_PROMPT = 'Please transcribe the audio into clean text. Return only the
45
45
  const _NEED = ['OpenAI', '@google/genai'];
46
46
 
47
47
  const [
48
- OPENAI, GOOGLE, OLLAMA, NOVA, DEEPSEEK_32, MD_CODE, CLOUD_OPUS_46, AUDIO,
49
- WAV, OPENAI_VOICE, GPT_REASONING_EFFORT, THINK, THINK_STR, THINK_END,
50
- TOOLS_STR, TOOLS_END, TOOLS, TEXT, OK, FUNC, GPT_54, GPT_IMAGE_15,
51
- GEMMA_3_27B, ANTHROPIC, ais, MAX_TOOL_RECURSION, LOG, name, user, system,
52
- assistant, JSON_OBJECT, PROMPT_IS_REQUIRED, k, trimTailing, trimBeginning,
53
- GEMINI_30_PRO_IMAGE, IMAGE, JINA, SILICONFLOW, SF_DEEPSEEK_32,
54
- OPENROUTER_API, OPENROUTER, AUTO, TOOL, ONLINE, GEMINI_31_PRO,
55
- GEMINI_30_FLASH, VEO_31, IMAGEN_4_UPSCALE, ERROR_GENERATING,
56
- GEMINI_25_FLASH_TTS, GEMINI_25_PRO_TTS, wav, PNG_EXT, GPT_4O_MIMI_TTS,
57
- GPT_4O_TRANSCRIBE, INVALID_AUDIO, OGG_EXT, ELLIPSIS, TOP_LIMIT, ATTACHMENT,
58
- PROCESSING, CURSOR, LN1, LN2, TOP, DEEPSEEK, DEEP_RESEARCH_PRO,
59
- MD_CODE_ESCAPED, VERTEX, LYRIA_002,
48
+ OPENAI, GOOGLE, OLLAMA, NOVA, MD_CODE, CLOUD_OPUS_47, AUDIO, WAV,
49
+ OPENAI_VOICE, GPT_REASONING_EFFORT, THINK, THINK_STR, THINK_END, TOOLS_STR,
50
+ TOOLS_END, TOOLS, TEXT, OK, FUNC, GPT_55, GPT_IMAGE_2, GEMMA_4_31B,
51
+ ANTHROPIC, ais, MAX_TOOL_RECURSION, LOG, name, user, system, assistant,
52
+ JSON_OBJECT, PROMPT_IS_REQUIRED, k, trimTailing, trimBeginning,
53
+ GEMINI_30_PRO_IMAGE, IMAGE, OPENROUTER_API, OPENROUTER, AUTO, TOOL, ONLINE,
54
+ GEMINI_31_PRO, GEMINI_30_FLASH, VEO_31, IMAGEN_4_UPSCALE, ERROR_GENERATING,
55
+ GEMINI_25_PRO_TTS, wav, GPT_AUDIO, ELLIPSIS, TOP_LIMIT, ATTACHMENT,
56
+ PROCESSING, CURSOR, LN1, LN2, TOP, DEEP_RESEARCH_MAX, MD_CODE_ESCAPED,
57
+ VERTEX, LYRIA_3, GPT_55_MINI,
60
58
  ] = [
61
- 'OpenAI', 'Google', 'Ollama', 'nova', 'deepseek-3.2-speciale', '```',
62
- 'claude-opus-4.6', 'audio', 'wav', 'OPENAI_VOICE', 'medium', 'think',
63
- '<think>', '</think>', '<tools>', '</tools>', 'tools', 'text', 'OK',
64
- 'function', 'gpt-5.4', 'gpt-image-1.5', 'gemma3:27b', 'Anthropic', [],
65
- 30, { log: true }, 'Alan', 'user', { role: 'system' },
66
- { role: 'assistant' }, 'json_object', 'Prompt is required.',
67
- x => 1000 * x, x => x.replace(/[\.\s]*$/, ''),
68
- x => x.replace(/^[\.\s]*/, ''), 'gemini-3-pro-image-preview', 'image',
69
- 'Jina', 'SiliconFlow', 'deepseek-ai/DeepSeek-V3.2-exp',
70
- 'https://openrouter.ai/api/v1', 'OpenRouter', 'openrouter/auto', 'tool',
71
- ':online', 'gemini-3.1-pro-preview', 'gemini-3-flash-preview',
59
+ 'OpenAI', 'Google', 'Ollama', 'nova', '```', 'claude-opus-4.7', 'audio',
60
+ 'wav', 'OPENAI_VOICE', 'medium', 'think', '<think>', '</think>',
61
+ '<tools>', '</tools>', 'tools', 'text', 'OK', 'function', 'gpt-5.5',
62
+ 'gpt-5.4-image-2', 'gemma4:31b', 'Anthropic', [], 30, { log: true },
63
+ 'Alan', 'user', { role: 'system' }, { role: 'assistant' },
64
+ 'json_object', 'Prompt is required.', x => 1000 * x,
65
+ x => x.replace(/[\.\s]*$/, ''), x => x.replace(/^[\.\s]*/, ''),
66
+ 'gemini-3-pro-image-preview', 'image', 'https://openrouter.ai/api/v1',
67
+ 'OpenRouter', 'openrouter/auto', 'tool', ':online',
68
+ 'gemini-3.1-pro-preview', 'gemini-3-flash-preview',
72
69
  'veo-3.1-generate-preview', 'imagen-4.0-upscale-preview',
73
- 'Error generating content.', 'gemini-2.5-flash-preview-tts',
74
- 'gemini-2.5-pro-preview-tts', 'wav', 'png', 'gpt-4o-mini-tts',
75
- 'gpt-4o-transcribe', 'Invalid audio data.', 'ogg', '...', 3,
76
- 'ATTACHMENT', { processing: true }, '', '\n\n', '\n\n\n', 'top',
77
- 'DeepSeek', 'deep-research-pro-preview-12-2025', '\`\`\`', 'Vertex',
78
- 'lyria-002',
70
+ 'Error generating content.', 'gemini-2.5-pro-preview-tts', 'wav',
71
+ 'gpt-audio', '...', 3, 'ATTACHMENT', { processing: true }, '', '\n\n',
72
+ '\n\n\n', 'top', 'deep-research-max-preview-04-2026', '\`\`\`',
73
+ 'Vertex', 'lyria-3-pro-preview', 'gpt-5-mini',
79
74
  ];
80
75
 
81
76
  const [joinL1, joinL2]
@@ -98,70 +93,41 @@ const m = x => k(k(x));
98
93
  const [MAX_TOKENS, ATTACHMENT_TOKEN_COST] = [m(1), k(10)];
99
94
 
100
95
  const MODEL_ICONS = {
101
- [OPENROUTER]: '🔀', [OPENAI]: '⚛️', [GOOGLE]: '♊️', // [JINA]: '✴️',
102
- [OLLAMA]: '🦙', [ANTHROPIC]: '✳️', [SILICONFLOW]: '🧬', [DEEPSEEK]: '🐬',
103
- [VERTEX]: '📐',
96
+ [OPENROUTER]: '🔀', [OPENAI]: '⚛️', [GOOGLE]: '♊️', [OLLAMA]: '🦙',
97
+ [ANTHROPIC]: '✳️', [VERTEX]: '❇️',
104
98
  };
105
99
 
106
100
  const FEATURE_ICONS = {
107
101
  audio: '🔊', 'deep-research': '🔍', fast: '⚡️', hearing: '👂', hidden: '🙈',
108
- image: '🎨', reasoning: '🧠', structured: '📊', tools: '🧰', video: '🎬',
109
- vision: '👁️', // finetune: '🔧',
110
- };
111
-
112
- const DEEPSEEK_32_RULES = {
113
- source: DEEPSEEK, contextWindow: k(163.8), maxOutputTokens: k(65.5),
114
- structured: true, tools: true, reasoning: true,
102
+ image: '🎨', music: '🎵', reasoning: '🧠', structured: '📊', tools: '🧰',
103
+ video: '🎬', vision: '👁️', // finetune: '🔧',
115
104
  };
116
105
 
117
106
  const MODELS = {
118
107
  // models with generation capabilities
119
- [LYRIA_002]: {
120
- source: VERTEX, maxInputTokens: 480,
121
- audio: true, defaultProvider: VERTEX,
122
- },
108
+ [LYRIA_3]: { music: true },
123
109
  [IMAGEN_4_UPSCALE]: {
124
110
  source: GOOGLE, maxInputTokens: 0, image: true, vision: true,
125
111
  supportedMimeTypes: [MIME_PNG, MIME_JPEG], defaultProvider: VERTEX,
126
112
  },
127
113
  [VEO_31]: {
128
- source: GOOGLE, maxInputTokens: 1024, attachmentTokenCost: 0,
114
+ source: GOOGLE, maxInputTokens: k(1), attachmentTokenCost: 0,
129
115
  video: true, vision: true,
130
116
  supportedMimeTypes: [MIME_PNG, MIME_JPEG], defaultProvider: GOOGLE,
131
117
  },
132
- [GPT_IMAGE_15]: {
133
- source: OPENAI, contextWindow: k(400), maxOutputTokens: k(128),
134
- hearing: true, image: true, reasoning: true, structured: true,
135
- tools: true, vision: true, defaultProvider: OPENAI,
136
- supportedMimeTypes: [MIME_PNG, MIME_JPEG, MIME_GIF, MIME_WEBP],
137
- },
138
118
  // tts/stt models
139
- [GEMINI_25_FLASH_TTS]: {
140
- source: GOOGLE, maxInputTokens: k(32),
141
- audio: true, fast: true, hidden: true, defaultProvider: GOOGLE,
142
- },
143
119
  [GEMINI_25_PRO_TTS]: {
144
120
  source: GOOGLE, maxInputTokens: k(32),
145
121
  audio: true, hidden: true, defaultProvider: GOOGLE,
146
122
  },
147
- [GPT_4O_MIMI_TTS]: {
148
- source: OPENAI, maxInputTokens: k(2),
149
- audio: true, fast: true, hidden: true, defaultProvider: OPENAI,
150
- },
151
- [GPT_4O_TRANSCRIBE]: {
152
- source: OPENAI, maxInputTokens: 0,
153
- hearing: true, fast: true, hidden: true, defaultProvider: OPENAI,
154
- },
123
+ [GPT_AUDIO]: { hidden: true, },
155
124
  // agents with deep-research capabilities
156
- [DEEP_RESEARCH_PRO]: { // https://ai.google.dev/gemini-api/docs/deep-research
125
+ [DEEP_RESEARCH_MAX]: { // https://ai.google.dev/gemini-api/docs/deep-research
157
126
  source: GOOGLE, contextWindow: m(1.05), maxOutputTokens: k(65.5),
158
127
  'deep-research': true, reasoning: true, defaultProvider: GOOGLE,
159
128
  },
160
- // best Chinese models
161
- [DEEPSEEK_32]: DEEPSEEK_32_RULES,
162
- [SF_DEEPSEEK_32]: { ...DEEPSEEK_32_RULES, defaultProvider: SILICONFLOW },
163
129
  // best local model
164
- [GEMMA_3_27B]: {
130
+ [GEMMA_4_31B]: {
165
131
  source: GOOGLE, contextWindow: k(128), maxOutputTokens: k(8),
166
132
  fast: true, structured: true, vision: true,
167
133
  supportedMimeTypes: [MIME_PNG, MIME_JPEG, MIME_GIF],
@@ -186,7 +152,7 @@ for (const n in MODELS) {
186
152
  }
187
153
  // Auto model have some issues with tools and reasoning, so we disable them here
188
154
  // MODELS[AUTO] = { name: AUTO, defaultProvider: OPENROUTER, };
189
- // for (const n of [GPT_54, GEMINI_31_PRO, GEMINI_30_FLASH]) {
155
+ // for (const n of [GPT_55, GEMINI_31_PRO, GEMINI_30_FLASH]) {
190
156
  // // get the most restrictive limits
191
157
  // for (const key of [
192
158
  // 'contextWindow', 'maxInputTokens', 'maxOutputTokens',
@@ -224,19 +190,23 @@ for (const n in MODELS) {
224
190
  // Default models for each provider
225
191
  const DEFAULT_MODELS = {
226
192
  [OPENROUTER]: [
227
- // fast and balanced models
193
+ // GPT: fast
194
+ GPT_55_MINI,
195
+ // GPT: strong
196
+ GPT_55,
197
+ // Gemini: fast
228
198
  GEMINI_30_FLASH,
229
- // strong and fast
230
- GPT_54,
231
- // stronger but slow
199
+ // Gemini: strong
232
200
  GEMINI_31_PRO,
233
- // models with generation capabilities
201
+ // Claude: Opus
202
+ CLOUD_OPUS_47,
203
+ // Multimedia generation models
234
204
  GEMINI_30_PRO_IMAGE,
235
- // models with code capabilities
236
- CLOUD_OPUS_46,
205
+ LYRIA_3,
206
+ // tts/stt models
207
+ // GPT_AUDIO,
237
208
  ],
238
- [SILICONFLOW]: SF_DEEPSEEK_32,
239
- [OLLAMA]: GEMMA_3_27B,
209
+ [OLLAMA]: GEMMA_4_31B,
240
210
  [OPENAI_VOICE]: NOVA,
241
211
  };
242
212
 
@@ -273,13 +243,6 @@ const adaptOpenRouterModels = async () => {
273
243
  model.source = model.label.split(':')[0] || model.id.split('/')[0];
274
244
  model.outputModalities = model.architecture.output_modalities;
275
245
  model.defaultProvider = OPENROUTER;
276
- [
277
- 'architecture', 'canonical_slug', 'context_length', 'created',
278
- 'default_parameters', 'expiration_date', 'hugging_face_id',
279
- 'per_request_limits', 'pricing', 'supported_parameters',
280
- 'top_provider',
281
- ].map(key => delete model[key]);
282
- // Backward compatibility: {
283
246
  if (model.inputModalities.includes('image')
284
247
  || model.inputModalities.includes('video')
285
248
  || model.inputModalities.includes('file')) {
@@ -294,18 +257,15 @@ const adaptOpenRouterModels = async () => {
294
257
  if (model.outputModalities.includes('image')) {
295
258
  model.image = true;
296
259
  }
297
- // if (model.outputModalities.includes('audio')) {
298
- // model.audio = true;
299
- // }
260
+ if (model.outputModalities.includes('audio')) {
261
+ model.audio = true;
262
+ }
300
263
  // https://gemini.google.com/app/c680748b3307790b
301
264
  // issue with json output via OpenRouter
302
- if (/structured/i.test(model.description)
303
- && model.name !== 'gemini-3-flash') {
304
- model.structured = true;
305
- }
306
- if (/tool/i.test(model.description)) {
307
- model.tools = true;
308
- }
265
+ model.structured = !!(model.supported_parameters?.includes('structured_outputs')
266
+ && ![GEMINI_30_FLASH].includes(model.name));
267
+ model.tools = !!(model.supported_parameters?.includes('tools')
268
+ || model.supported_parameters?.includes('tool_choice'));
309
269
  if (/flash|lite|mini|nano|tiny|small/i.test(model.name)) {
310
270
  model.fast = true;
311
271
  }
@@ -322,7 +282,8 @@ const adaptOpenRouterModels = async () => {
322
282
  if (model.hearing) {
323
283
  model.supportedMimeTypes.push(
324
284
  MIME_AAC, MIME_FLAC, MIME_MP3, MIME_MPEGA, MIME_M4A,
325
- MIME_MPGA, MIME_OPUS, MIME_PCM, MIME_WAV, MIME_OGG,
285
+ MIME_MPGA, MIME_OPUS, MIME_PCM, MIME_WAV, MIME_WAVE,
286
+ MIME_OGG,
326
287
  );
327
288
  }
328
289
  if (model.name === GEMINI_30_PRO_IMAGE) {
@@ -344,10 +305,7 @@ const adaptOpenRouterModels = async () => {
344
305
  );
345
306
  }
346
307
  if (model.hearing) {
347
- model.supportedMimeTypes.push(MIME_WAV);
348
- }
349
- if (model.name === GPT_54) {
350
- model.fast = true;
308
+ model.supportedMimeTypes.push(MIME_WAV, MIME_WAVE);
351
309
  }
352
310
  break;
353
311
  case ANTHROPIC:
@@ -359,15 +317,21 @@ const adaptOpenRouterModels = async () => {
359
317
  }
360
318
  break;
361
319
  }
362
- // }
320
+ [
321
+ 'architecture', 'canonical_slug', 'context_length', 'created',
322
+ 'default_parameters', 'expiration_date', 'hugging_face_id',
323
+ 'per_request_limits', 'pricing', 'top_provider',
324
+ 'supported_parameters',
325
+ ].map(key => delete model[key]);
363
326
  model.label = model.label.split(':').pop().trim();
364
- MODELS[model.name] = model;
327
+ MODELS[model.name] = { ...MODELS[model.name] || {}, ...model };
328
+ MODELS[model.name].music && (MODELS[model.name].audio = false);
365
329
  }
366
330
  };
367
331
 
368
332
  const unifyProvider = provider => {
369
333
  assert(provider = (provider || '').trim(), 'AI provider is required.');
370
- for (let type of [OPENROUTER, GOOGLE, OPENAI, OLLAMA, SILICONFLOW, VERTEX]) { // JINA,
334
+ for (let type of [OPENROUTER, GOOGLE, OPENAI, OLLAMA, VERTEX]) {
371
335
  if (insensitiveCompare(provider, type)) { return type; }
372
336
  }
373
337
  throwError(`Invalid AI provider: ${provider}.`);
@@ -487,7 +451,7 @@ const setupAi = ai => {
487
451
 
488
452
  const OpenAI = async opts => {
489
453
  const lib = await libOpenAi(opts);
490
- return { toFile: lib.toFile, client: new (lib).OpenAI(opts) };
454
+ return { client: new (lib).OpenAI(opts) };
491
455
  };
492
456
 
493
457
  const init = async (options = {}) => {
@@ -537,6 +501,10 @@ const init = async (options = {}) => {
537
501
  const location = options.location || 'global';
538
502
  var client = new GoogleGenAI({
539
503
  vertexai: true, location, ...options,
504
+ googleAuthOptions: {
505
+ keyFile: options.credentials,
506
+ scopes: ['https://www.googleapis.com/auth/cloud-platform']
507
+ },
540
508
  });
541
509
  const auth = new GoogleAuth({
542
510
  keyFile: options.credentials,
@@ -550,17 +518,6 @@ const init = async (options = {}) => {
550
518
  });
551
519
  }
552
520
  break;
553
- case OPENAI:
554
- assertApiKey(provider, options);
555
- var { client, toFile } = await OpenAI({ ...options });
556
- for (let model of models) {
557
- setupAi({
558
- provider, model, client, toFile,
559
- prompt: model.defaultProvider === OPENAI
560
- ? promptOpenAI : promptOpenRouter, priority,
561
- });
562
- }
563
- break;
564
521
  case OLLAMA:
565
522
  // https://github.com/ollama/ollama/blob/main/docs/openai.md
566
523
  const baseURL = 'http://localhost:11434/v1/';
@@ -570,7 +527,7 @@ const init = async (options = {}) => {
570
527
  });
571
528
  for (let model of models) {
572
529
  setupAi({
573
- provider, model, client, prompt: promptOpenRouter, priority,
530
+ provider, model, client, prompt: promptOpenAI, priority,
574
531
  });
575
532
  ignoreErrFunc(async () => {
576
533
  phLog(await (await fetch(`${baseURL}completions`, {
@@ -581,26 +538,16 @@ const init = async (options = {}) => {
581
538
  }, { log: phLog });
582
539
  }
583
540
  break;
584
- case SILICONFLOW:
585
- assertApiKey(provider, options);
586
- var client = await OpenAI({
587
- baseURL: 'https://api.siliconflow.cn/v1', ...options,
588
- });
589
- for (let model of models) {
590
- setupAi({
591
- provider, model, client, prompt: promptOpenRouter, priority,
592
- });
593
- }
594
- break;
595
541
  default:
596
542
  assertApiKey(provider, options);
597
543
  var { client } = await OpenAI({
598
- baseURL: OPENROUTER_API, ...options || {},
544
+ baseURL: provider === OPENROUTER ? OPENROUTER_API : undefined,
545
+ ...options || {},
599
546
  });
600
547
  for (let model of models) {
601
548
  setupAi({
602
- provider: provider || OPENROUTER, model, client,
603
- prompt: promptOpenRouter, priority,
549
+ provider, model, client, prompt: promptOpenAI, priority,
550
+ options: { ...options || {} },
604
551
  });
605
552
  }
606
553
  }
@@ -610,9 +557,9 @@ const init = async (options = {}) => {
610
557
 
611
558
  const packAi = (ais, options = {}) => {
612
559
  let res = options.basic ? ais.map(x => ({
613
- id: x.id, name: x.name, features: x.features,
614
- initOrder: x.initOrder, priority: x.priority,
615
- provider: x.provider, model: x.model,
560
+ id: x.id, name: x.name, features: x.features, initOrder: x.initOrder,
561
+ priority: x.priority, provider: x.provider, model: x.model,
562
+ options: x.options,
616
563
  })) : ais;
617
564
  if (options.all && !Object.keys(options.select).length && !options.withHidden) {
618
565
  res = res.filter(x => !x.model.hidden);
@@ -684,7 +631,8 @@ const buildMessage = async (content, options) => {
684
631
  };
685
632
  } else if ([
686
633
  MIME_AAC, MIME_FLAC, MIME_MP3, MIME_MPEGA, MIME_M4A, MIME_MPGA,
687
- MIME_OPUS, MIME_PCM, MIME_WAV, MIME_TGPP, MIME_PCM16, MIME_OGG,
634
+ MIME_OPUS, MIME_PCM, MIME_WAV, MIME_WAVE, MIME_TGPP, MIME_PCM16,
635
+ MIME_OGG,
688
636
  ].includes?.(x.mime_type)) {
689
637
  return {
690
638
  type: 'input_audio',
@@ -745,13 +693,14 @@ const packResp = async (resp, options) => {
745
693
  if (options?.raw) { return resp; }
746
694
  let [
747
695
  txt, audio, images, annotations, simpleText, annotationsMarkdown, end,
748
- json, audioMimeType,
696
+ json, audioMimeType, audioSuffix,
749
697
  ] = [
750
698
  resp.text || '', // ChatGPT / Claude / Gemini / Ollama
751
699
  resp?.audio?.data, // ChatGPT audio mode
752
700
  resp?.images || [], // Gemini images via Openrouter
753
701
  resp?.references, // Gemini references
754
- '', '', '', null, MIME_PCM16,
702
+ '', '', '', null, resp?.audio?.mimeType || resp?.audio?.mime || MIME_PCM16,
703
+ null,
755
704
  ];
756
705
  simpleText = txt;
757
706
  while ((end = getInfoEnd(simpleText))) {
@@ -762,10 +711,17 @@ const packResp = async (resp, options) => {
762
711
  const str = simpleText.indexOf(x);
763
712
  str >= 0 && (simpleText = simpleText.slice(0, str).trim());
764
713
  });
714
+ audioSuffix = audioMimeType === MIME_MPEGA
715
+ ? 'mp3' : audioMimeType.split('/').pop();
765
716
  audio = await ignoreErrFunc(async () => ({
766
- data: await packPcmToWav(audio, {
767
- input: BUFFER, expected: BUFFER, suffix: 'pcm.wav', ...options
768
- }), mime: audioMimeType,
717
+ data: [MIME_PCM, MIME_PCM16].includes(audioMimeType)
718
+ ? await packPcmToWav(audio, {
719
+ input: BUFFER, expected: BUFFER, suffix: 'pcm.wav', ...options
720
+ })
721
+ : await convert(audio, {
722
+ input: BUFFER, expected: BUFFER, suffix: audioSuffix, ...options
723
+ }),
724
+ mime: audioMimeType, mimeType: audioMimeType,
769
725
  }));
770
726
  // bug fix: Google Gemini returns duplicate first image: {
771
727
  images && images.length > 1 && images.shift();
@@ -793,7 +749,6 @@ const packResp = async (resp, options) => {
793
749
  // "title": "在線時鐘- 目前時間- 線上時鐘- 時鐘線上 - 鬧鐘",
794
750
  // "url": "https://naozhong.tw/shijian/",
795
751
  // "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%",
796
- // "dateTime": "2025-03-13 06:48:01" // jina deep-research only
797
752
  // }
798
753
  // },
799
754
  // ];
@@ -806,22 +761,6 @@ const packResp = async (resp, options) => {
806
761
  ).join('\n');
807
762
  }
808
763
  txt = txt.split('\n');
809
- // const [reJinaStr, reJinaEnd]
810
- // = [`^\s*(${THINK_STR})`, `(${THINK_END})\s*$`].map(x => new RegExp(x));
811
- // const fixJina = [];
812
- // for (let l of txt) {
813
- // let catched = false;
814
- // if (reJinaStr.test(l)) {
815
- // fixJina.push(THINK_STR);
816
- // l = l.replace(reJinaStr, '');
817
- // }
818
- // if (reJinaEnd.test(l)) {
819
- // l = l.replace(reJinaEnd, '');
820
- // catched = true;
821
- // }
822
- // fixJina.push(l, ...catched ? [THINK_END, ''] : []);
823
- // }
824
- // txt = fixJina;
825
764
  // Fixed Markdown ``` not in new line for Gemini {
826
765
  for (let i in txt) {
827
766
  const reCode = new RegExp(`^(.+)(${MD_CODE})$`);
@@ -977,8 +916,8 @@ const mergeMsgs = (resp, calls) => [resp, ...calls.length ? [
977
916
  `⚠️ Tools recursion limit reached: ${MAX_TOOL_RECURSION}`
978
917
  ] : []].map(x => x.trim()).join('\n\n');
979
918
 
980
- const promptOpenRouter = async (aiId, content, options = {}) => {
981
- let { provider, client, model } = await getAi(aiId);
919
+ const promptOpenAI = async (aiId, content, options = {}) => {
920
+ let { provider, client, model, options: opts } = await getAi(aiId);
982
921
  let [
983
922
  result, resultAudio, resultImages, resultReasoning, event, resultTools,
984
923
  responded, modalities, source, reasoningEnd, reasoning_details,
@@ -998,11 +937,19 @@ const promptOpenRouter = async (aiId, content, options = {}) => {
998
937
  } else if (!modalities && model.image) {
999
938
  modalities = [TEXT, IMAGE];
1000
939
  }
940
+ const audioConfig = options.audio || (
941
+ source === OPENAI && modalities?.find?.(x => x === AUDIO)
942
+ && { voice: DEFAULT_MODELS[OPENAI_VOICE], format: 'pcm16' }
943
+ );
944
+ const audioMimeType = model.name === LYRIA_3 ? MIME_MPEGA : MIME_PCM16;
1001
945
  const googleImageMode = source === GOOGLE && modalities?.includes?.(IMAGE);
946
+ const imageConfig = model.name === GPT_IMAGE_2
947
+ ? { aspect_ratio: '16:9', image_size: '2K' } : googleImageMode
948
+ ? { aspect_ratio: '16:9', image_size: '4K' } : null;
1002
949
  const packedTools = _tools.map(x => x.def);
1003
950
  const ext = provider === OPENROUTER && !packedTools?.find(
1004
951
  x => x.function.name === 'searchWeb'
1005
- ) && !options.jsonMode ? ONLINE : '';
952
+ ) && !options.jsonMode && !options.select ? ONLINE : ''; // `select` typically comes from a tool invocation.
1006
953
  // if (provider === OPENAI) {
1007
954
  // // need more debug, currently openrouter is priority
1008
955
  // packedTools.push(...[
@@ -1022,20 +969,19 @@ const promptOpenRouter = async (aiId, content, options = {}) => {
1022
969
  ]);
1023
970
  }
1024
971
  const resp = await client.chat.completions.create({
1025
- model: `${isOpenrouter(provider, model) ? `${source}/` : ''}${options.model}${ext}`,
1026
- ...history,
972
+ model: isOpenrouter(provider, model) ? (
973
+ `${source}/${options.model}${ext}`.toLowerCase()
974
+ + (opts?.preset || '')
975
+ ) : `${options.model}${ext}`, ...history,
1027
976
  ...options.jsonMode ? { response_format: { type: JSON_OBJECT } } : {},
1028
977
  ...provider === OLLAMA ? { keep_alive: -1 } : {},
1029
- modalities, audio: options.audio || (
1030
- modalities?.find?.(x => x === AUDIO)
1031
- && { voice: DEFAULT_MODELS[OPENAI_VOICE], format: 'pcm16' }
1032
- ), ...model?.tools ? {
978
+ modalities, ...audioConfig ? { audio: audioConfig } : {},
979
+ ...model?.tools ? {
1033
980
  tools: options.tools ?? packedTools, tool_choice: 'auto',
1034
- } : {}, store: true, stream: true, ...googleImageMode ? {
1035
- // bug: image_size not working
981
+ } : {}, store: true, stream: true, ...imageConfig ? {
1036
982
  // https://openrouter.ai/docs/features/multimodal/image-generation
1037
983
  // https://openrouter.ai/google/gemini-3-pro-image-preview/api
1038
- image_config: { aspect_ratio: '16:9', image_size: '4K' },
984
+ image_config: imageConfig,
1039
985
  } : {}, ...options.reasoning_effort ? {
1040
986
  reasoning: { effort: options.reasoning_effort },
1041
987
  } : {},
@@ -1104,14 +1050,18 @@ const promptOpenRouter = async (aiId, content, options = {}) => {
1104
1050
  (deltaText || deltaAudio?.length || deltaImages.length)
1105
1051
  && await streamResp({
1106
1052
  text: options.delta ? deltaText : result,
1107
- ...respAudio.length ? { audio: { data: respAudio } } : {},
1053
+ ...respAudio.length ? {
1054
+ audio: { data: respAudio, mimeType: audioMimeType }
1055
+ } : {},
1108
1056
  ...respImages.length ? { images: respImages } : {},
1109
1057
  }, options);
1110
1058
  }
1111
1059
  event = {
1112
1060
  ...assistant, text: result, tool_calls: resultTools,
1113
1061
  ...resultImages.length ? { images: resultImages } : {},
1114
- ...resultAudio.length ? { audio: { data: resultAudio } } : {},
1062
+ ...resultAudio.length ? {
1063
+ audio: { data: resultAudio, mimeType: audioMimeType }
1064
+ } : {},
1115
1065
  ...annotations.length ? { annotations } : {},
1116
1066
  };
1117
1067
  switch (source) {
@@ -1129,7 +1079,7 @@ const promptOpenRouter = async (aiId, content, options = {}) => {
1129
1079
  = await handleToolsCall(event, { ...options, result });
1130
1080
  if (toolsResult.length
1131
1081
  && countToolCalls(toolsResponse) < MAX_TOOL_RECURSION) {
1132
- return promptOpenRouter(aiId, content, {
1082
+ return promptOpenAI(aiId, content, {
1133
1083
  ...options, toolsResult, result: toolsResponse,
1134
1084
  });
1135
1085
  }
@@ -1138,11 +1088,11 @@ const promptOpenRouter = async (aiId, content, options = {}) => {
1138
1088
  };
1139
1089
 
1140
1090
  const promptGoogle = async (aiId, prompt, options = {}) => {
1141
- let { provider, client, model, options: opts } = await getAi(aiId);
1091
+ let { provider, client, model } = await getAi(aiId);
1142
1092
  const target_model = options?.model || model.name;
1143
1093
  const M = MODELS[target_model];
1144
1094
  prompt = ensureString(prompt, { trim: true });
1145
- [IMAGEN_4_UPSCALE, LYRIA_002].includes(M.name) || assertPrompt(prompt);
1095
+ [IMAGEN_4_UPSCALE, LYRIA_3].includes(M.name) || assertPrompt(prompt);
1146
1096
  M.tts && (prompt = `${options?.prompt || TTS_PROMPT}:\n\n${prompt}`);
1147
1097
  prompt = trimText(prompt, { limit: M.maxInputTokens });
1148
1098
  if (M.name === IMAGEN_4_UPSCALE) {
@@ -1236,30 +1186,38 @@ const promptGoogle = async (aiId, prompt, options = {}) => {
1236
1186
  };
1237
1187
  }
1238
1188
  } else if (M?.audio) { // https://ai.google.dev/gemini-api/docs/speech-generation#voices
1239
- if (M.name === LYRIA_002) {
1240
- const token = await opts.authClient.getAccessToken();
1241
- const region = 'us-central1'; // opts.location;
1242
- const url = `https://${region}-aiplatform.googleapis.com/v1/projects/${opts.project}/locations/${region}/publishers/google/models/${M.name}:predict`;
1243
- const payload = { instances: [{ prompt }] };
1244
- const response = await fetch(url, {
1245
- method: 'POST',
1246
- headers: {
1247
- 'Authorization': `Bearer ${token.token}`,
1248
- 'Content-Type': 'application/json'
1249
- },
1250
- body: JSON.stringify(payload)
1189
+ if (M.name === LYRIA_3) {
1190
+ var resp = await client.interactions.create({
1191
+ model: M.name,
1192
+ input: [{ type: TEXT, text: prompt }],
1193
+ ...(options?.config ? { generation_config: options.config } : {}),
1251
1194
  });
1252
- if (!response.ok) {
1253
- throwError(`API Error: ${response.status} - ${await response.text()}`);
1254
- }
1255
- const data = await response.json();
1256
- var rawAudio = data?.predictions?.[0]?.bytesBase64Encoded;
1257
- assert(rawAudio, ERROR_GENERATING);
1195
+ assert(!resp?.error, resp?.error?.message || ERROR_GENERATING);
1196
+ if (options?.generateRaw) { return resp; }
1197
+ await tryUntil(async () => {
1198
+ if (resp?.status === 'completed') { return; }
1199
+ assert(
1200
+ !['failed', 'cancelled', 'incomplete'].includes(resp?.status),
1201
+ `Google music generation failed: ${resp?.status}`,
1202
+ );
1203
+ resp = await client.interactions.get(resp.id);
1204
+ assert(
1205
+ resp?.status === 'completed',
1206
+ `Waiting for Google music generation: ${resp.id}`,
1207
+ );
1208
+ }, { maxTry: 60 * 10, log });
1209
+ const outputs = resp?.outputs || [];
1210
+ const rawAudio = outputs.find(x => x?.type === AUDIO && x?.data);
1211
+ const mimeType = rawAudio?.mime_type || rawAudio?.mimeType || MIME_MPEGA;
1212
+ const suffix = mimeType === MIME_MPEGA ? 'mp3' : mimeType.split('/').pop();
1213
+ assert(rawAudio?.data, ERROR_GENERATING);
1258
1214
  resp = {
1259
- text: '', audio: {
1260
- data: await convert(rawAudio, {
1261
- input: BASE64, suffix: 'wav', ...options || {},
1262
- }), mimeType: MIME_WAV,
1215
+ text: outputs.filter(x => x?.type === TEXT && x?.text)
1216
+ .map(x => x.text).join(LN1),
1217
+ audio: {
1218
+ data: await convert(rawAudio.data, {
1219
+ input: BASE64, suffix, ...options || {},
1220
+ }), mimeType,
1263
1221
  }, model: packModelId([provider, M.source, M.name]),
1264
1222
  };
1265
1223
  } else {
@@ -1352,97 +1310,6 @@ const promptGoogle = async (aiId, prompt, options = {}) => {
1352
1310
  return { ...resp, processing: false };
1353
1311
  };
1354
1312
 
1355
- const promptOpenAI = async (aiId, prompt, options = {}) => {
1356
- let { provider, client, toFile, model } = await getAi(aiId);
1357
- const target_model = options?.model || model.name;
1358
- const M = MODELS[target_model];
1359
- prompt = ensureString(prompt, { trim: true });
1360
- if (M?.image) {
1361
- assertPrompt(prompt);
1362
- prompt = trimText(prompt, { limit: M.maxInputTokens });
1363
- var resp = await client.images.generate({
1364
- model: M.name, moderation: 'low', output_format: 'png',
1365
- prompt, quality: 'high', size: '1536x1024',
1366
- // n = 1, // 1 ~ 10
1367
- // @TODO: 支持 streaming image generation
1368
- // https://platform.openai.com/docs/guides/image-generation?api=responses
1369
- // Please note that right now, only gpt-image-1 and gpt-image-1-mini are
1370
- // supported in the Responses API. We're working on support for gpt-image-1.5.
1371
- // partial_images: 3, stream: true,
1372
- // @TODO: 支持 image editing {}
1373
- ...options?.params || {},
1374
- });
1375
- if (!options?.raw) {
1376
- // print(resp);
1377
- resp = {
1378
- text: '', images: await Promise.all(resp.data.map(
1379
- async (x, i) => {
1380
- const buffer = await convert(
1381
- x.b64_json, { input: BASE64 }
1382
- );
1383
- return {
1384
- data: await convert(buffer, {
1385
- input: BUFFER, suffix: PNG_EXT, ...options || {}
1386
- }),
1387
- mimeType: (await getMime(
1388
- buffer, `${i}.${resp.output_format}`
1389
- ))?.mime || MIME_PNG,
1390
- }
1391
- })
1392
- ), model: packModelId([provider, M.source, M.name]),
1393
- };
1394
- }
1395
- } else if (M?.audio) {
1396
- assertPrompt(prompt);
1397
- const ins_prompt = options?.prompt || `${TTS_PROMPT}.`;
1398
- prompt = trimText(prompt, {
1399
- limit: M.maxInputTokens - countTokens(ins_prompt),
1400
- });
1401
- // https://platform.openai.com/docs/api-reference/audio/createSpeech
1402
- var resp = await client.audio.speech.create({
1403
- model: M.name, voice: DEFAULT_MODELS[OPENAI_VOICE],
1404
- instructions: ins_prompt, response_format: 'opus',
1405
- input: prompt, ...options?.params || {},
1406
- });
1407
- if (!options?.raw) {
1408
- resp = {
1409
- text: '', audio: {
1410
- data: await convert(Buffer.from(
1411
- await resp.arrayBuffer()
1412
- ), { suffix: OGG_EXT, ...options || {} }),
1413
- mimeType: MIME_OGG,
1414
- }, model: packModelId([provider, M.source, M.name]),
1415
- };
1416
- }
1417
- } else if (M?.hearing) {
1418
- const audio = options?.attachments?.[0]?.data || options?.attachments?.[0];
1419
- assert(audio, 'Audio attachment is required.');
1420
- const input = ensureString(options?.input, { case: 'UP' });
1421
- const { content, cleanup } = await convert(audio, {
1422
- input: options?.input, ...options || {}, expected: STREAM, INVALID_AUDIO,
1423
- suffix: ['', BUFFER].includes(input) ? OGG_EXT : null,
1424
- withCleanupFunc: true,
1425
- });
1426
- var resp = await client.audio.transcriptions.create({
1427
- file: await toFile(content), model: M.name,
1428
- response_format: 'text', ...options?.params || {},
1429
- });
1430
- await cleanup();
1431
- if (!options?.raw) {
1432
- resp = {
1433
- text: resp.trim(),
1434
- model: packModelId([provider, M.source, M.name]),
1435
- };
1436
- }
1437
- } else {
1438
- throwError('Unsupported model.');
1439
- }
1440
- await streamResp(
1441
- { ...resp, processing: true }, { ...options, noPack: true }
1442
- );
1443
- return { ...resp, processing: false };
1444
- };
1445
-
1446
1313
  const initChat = async (options = {}) => {
1447
1314
  if (options.sessions) {
1448
1315
  assert(
@@ -1461,7 +1328,7 @@ const initChat = async (options = {}) => {
1461
1328
  countTokens(chatConfig.systemPrompt), await getAi(null, { all: true })
1462
1329
  ];
1463
1330
  for (const ai of ais.filter(x => ![
1464
- IMAGEN_4_UPSCALE, VEO_31, GPT_4O_TRANSCRIBE, LYRIA_002,
1331
+ IMAGEN_4_UPSCALE, VEO_31,
1465
1332
  ].includes(x.model.name))) {
1466
1333
  const mxPmpt = ai.model.maxInputTokens / 2;
1467
1334
  assert(spTokens < mxPmpt,
@@ -1666,15 +1533,17 @@ const distillFile = async (attachments, o) => {
1666
1533
  };
1667
1534
 
1668
1535
  const tts = async (content, options = {}) => {
1669
- const resp = await prompt(
1670
- content, { select: { audio: true, fast: true }, ...options, raw: false }
1671
- );
1536
+ const resp = await prompt(content, {
1537
+ select: { audio: true, fast: true }, ...options, raw: false,
1538
+ audioMode: true,
1539
+ });
1672
1540
  return options.raw ? resp : resp?.audio?.data;
1673
1541
  };
1674
1542
 
1675
- const stt = async (audio, options = {}) => await distillFile(
1676
- audio, { prompt: STT_PROMPT, ...options }
1677
- );
1543
+ const stt = async (audio, options = {}) => await distillFile(audio, {
1544
+ select: { hearing: true, fast: true },
1545
+ prompt: STT_PROMPT, ...options
1546
+ });
1678
1547
 
1679
1548
  const prompt = async (input, options = {}) => {
1680
1549
  const ai = await getAi(options?.aiId, options);
@@ -1789,19 +1658,18 @@ export default init;
1789
1658
  export {
1790
1659
  _NEED,
1791
1660
  _NO_RENDER,
1792
- CLOUD_OPUS_46,
1661
+ CLOUD_OPUS_47,
1793
1662
  CODE_INTERPRETER,
1794
- DEEPSEEK_32,
1795
1663
  DEFAULT_MODELS,
1796
1664
  FEATURE_ICONS,
1797
1665
  FUNCTION,
1798
- GEMINI_25_FLASH_TTS,
1799
1666
  GEMINI_30_FLASH,
1800
1667
  GEMINI_25_PRO_TTS,
1801
1668
  GEMINI_30_PRO_IMAGE,
1802
1669
  GEMINI_31_PRO,
1803
- GPT_IMAGE_15,
1804
- GPT_54,
1670
+ GPT_IMAGE_2,
1671
+ GPT_55,
1672
+ GPT_55_MINI,
1805
1673
  INSTRUCTIONS,
1806
1674
  MODELS,
1807
1675
  OPENAI_VOICE,
@@ -1822,7 +1690,7 @@ export {
1822
1690
  k,
1823
1691
  listOpenAIModels,
1824
1692
  prompt,
1825
- promptOpenRouter,
1693
+ promptOpenAI,
1826
1694
  resetSession,
1827
1695
  setSession,
1828
1696
  stt,