@stabgan/openrouter-mcp-multimodal 4.6.2 → 4.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +108 -37
  2. package/dist/errors.d.ts +5 -20
  3. package/dist/errors.js +1 -10
  4. package/dist/index.js +7 -1
  5. package/dist/logger.js +54 -24
  6. package/dist/model-cache.d.ts +13 -0
  7. package/dist/model-cache.js +61 -5
  8. package/dist/openrouter-api.d.ts +14 -15
  9. package/dist/openrouter-api.js +68 -22
  10. package/dist/openrouter-openai-client.d.ts +9 -0
  11. package/dist/openrouter-openai-client.js +15 -0
  12. package/dist/tool-definitions.d.ts +24 -0
  13. package/dist/tool-definitions.js +283 -170
  14. package/dist/tool-descriptions.js +23 -15
  15. package/dist/tool-handlers/analyze-audio.js +4 -1
  16. package/dist/tool-handlers/analyze-image.js +10 -5
  17. package/dist/tool-handlers/analyze-video.js +9 -5
  18. package/dist/tool-handlers/async-chat.d.ts +17 -0
  19. package/dist/tool-handlers/async-chat.js +112 -31
  20. package/dist/tool-handlers/audio-utils.d.ts +19 -4
  21. package/dist/tool-handlers/audio-utils.js +170 -16
  22. package/dist/tool-handlers/cache.d.ts +3 -3
  23. package/dist/tool-handlers/cache.js +56 -4
  24. package/dist/tool-handlers/chat-completion.js +16 -7
  25. package/dist/tool-handlers/chat-request.d.ts +3 -0
  26. package/dist/tool-handlers/chat-request.js +28 -0
  27. package/dist/tool-handlers/completion-utils.d.ts +5 -11
  28. package/dist/tool-handlers/completion-utils.js +76 -47
  29. package/dist/tool-handlers/fetch-utils.d.ts +14 -0
  30. package/dist/tool-handlers/fetch-utils.js +331 -68
  31. package/dist/tool-handlers/generate-audio.d.ts +6 -51
  32. package/dist/tool-handlers/generate-audio.js +30 -76
  33. package/dist/tool-handlers/generate-image-dedicated.d.ts +2 -12
  34. package/dist/tool-handlers/generate-image-dedicated.js +79 -28
  35. package/dist/tool-handlers/generate-image.d.ts +3 -46
  36. package/dist/tool-handlers/generate-image.js +26 -43
  37. package/dist/tool-handlers/generate-video.d.ts +4 -3
  38. package/dist/tool-handlers/generate-video.js +51 -50
  39. package/dist/tool-handlers/get-model-info.js +1 -1
  40. package/dist/tool-handlers/health-check.js +39 -15
  41. package/dist/tool-handlers/image-utils.js +2 -2
  42. package/dist/tool-handlers/openrouter-errors.d.ts +2 -0
  43. package/dist/tool-handlers/openrouter-errors.js +138 -31
  44. package/dist/tool-handlers/path-safety.d.ts +6 -0
  45. package/dist/tool-handlers/path-safety.js +77 -14
  46. package/dist/tool-handlers/path-utils.d.ts +4 -0
  47. package/dist/tool-handlers/path-utils.js +20 -0
  48. package/dist/tool-handlers/provider-routing.d.ts +2 -0
  49. package/dist/tool-handlers/provider-routing.js +10 -0
  50. package/dist/tool-handlers/rerank.d.ts +1 -4
  51. package/dist/tool-handlers/rerank.js +43 -14
  52. package/dist/tool-handlers/search-models.js +3 -3
  53. package/dist/tool-handlers/speech-to-text.d.ts +1 -0
  54. package/dist/tool-handlers/speech-to-text.js +23 -56
  55. package/dist/tool-handlers/text-to-speech.d.ts +2 -12
  56. package/dist/tool-handlers/text-to-speech.js +29 -22
  57. package/dist/tool-handlers/tool-result-payload.d.ts +47 -0
  58. package/dist/tool-handlers/tool-result-payload.js +98 -0
  59. package/dist/tool-handlers/validate-model.js +1 -1
  60. package/dist/tool-handlers.d.ts +9 -0
  61. package/dist/tool-handlers.js +19 -10
  62. package/dist/version.d.ts +1 -1
  63. package/dist/version.js +1 -1
  64. package/package.json +1 -3
@@ -3,7 +3,10 @@
3
3
  * Used by both image-utils and audio-utils to avoid duplication.
4
4
  */
5
5
  import dns from 'node:dns/promises';
6
+ import http from 'node:http';
7
+ import https from 'node:https';
6
8
  import net from 'node:net';
9
+ import { createBrotliDecompress, createGunzip, createInflate } from 'node:zlib';
7
10
  import { readFileSync } from 'node:fs';
8
11
  import { fileURLToPath } from 'node:url';
9
12
  import path from 'node:path';
@@ -37,9 +40,25 @@ export function readEnvInt(name, fallback, min = 1) {
37
40
  const raw = process.env[name];
38
41
  if (raw === undefined || raw === '')
39
42
  return fallback;
40
- const n = parseInt(raw, 10);
43
+ if (!/^\d+$/.test(raw))
44
+ return fallback;
45
+ const n = Number(raw);
41
46
  return Number.isFinite(n) && n >= min ? n : fallback;
42
47
  }
48
+ /** Normalize dotted / decimal / octal / shorthand IPv4 literals via the URL parser. */
49
+ function normalizeIPv4Literal(host) {
50
+ const trimmed = host.trim().toLowerCase();
51
+ if (/^\d{1,3}(\.\d{1,3}){3}$/.test(trimmed))
52
+ return trimmed;
53
+ try {
54
+ const parsed = new URL(`http://${trimmed}/`);
55
+ const h = parsed.hostname;
56
+ return /^\d{1,3}(\.\d{1,3}){3}$/.test(h) ? h : null;
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ }
43
62
  function ipv4ToUint(ip) {
44
63
  const parts = ip.split('.').map((p) => parseInt(p, 10));
45
64
  if (parts.length !== 4 || parts.some((p) => !Number.isInteger(p) || p < 0 || p > 255)) {
@@ -49,7 +68,10 @@ function ipv4ToUint(ip) {
49
68
  }
50
69
  /** Blocks RFC1918, loopback, link-local, CGNAT, metadata. */
51
70
  export function isBlockedIPv4(ip) {
52
- const n = ipv4ToUint(ip);
71
+ const normalized = normalizeIPv4Literal(ip);
72
+ if (!normalized)
73
+ return false;
74
+ const n = ipv4ToUint(normalized);
53
75
  if (n >>> 24 === 127)
54
76
  return true;
55
77
  if (n >>> 24 === 10)
@@ -179,10 +201,74 @@ export function isBlockedIPv6(ip) {
179
201
  return false;
180
202
  }
181
203
  function isIPv4Literal(host) {
182
- return /^\d{1,3}(\.\d{1,3}){3}$/.test(host);
204
+ return normalizeIPv4Literal(host) !== null;
183
205
  }
184
- /** Resolve hostname and ensure the resolved address is not private/link-local. */
185
- export async function assertUrlSafeForFetch(urlString) {
206
+ /** Vitest-only hooks; inert outside `process.env.VITEST === 'true'`. */
207
+ let testDnsLookup = null;
208
+ let testTrustedCa;
209
+ let testAllowLoopbackResolution = false;
210
+ /** @internal Test seam — never active in production. */
211
+ export function __setFetchUtilsTestHooks(hooks) {
212
+ if (process.env.VITEST !== 'true')
213
+ return;
214
+ if ('dnsLookup' in hooks)
215
+ testDnsLookup = hooks.dnsLookup ?? null;
216
+ if ('trustedCa' in hooks)
217
+ testTrustedCa = hooks.trustedCa;
218
+ if ('allowLoopbackResolution' in hooks) {
219
+ testAllowLoopbackResolution = hooks.allowLoopbackResolution ?? false;
220
+ }
221
+ }
222
+ /** @internal Test seam — never active in production. */
223
+ export function __resetFetchUtilsTestHooks() {
224
+ testDnsLookup = null;
225
+ testTrustedCa = undefined;
226
+ testAllowLoopbackResolution = false;
227
+ }
228
+ function isAddressBlocked(address, family) {
229
+ if (family === 4)
230
+ return isBlockedIPv4(address);
231
+ return isBlockedIPv6(address);
232
+ }
233
+ function isLoopbackForTest(address, family) {
234
+ if (family === 4) {
235
+ const normalized = normalizeIPv4Literal(address);
236
+ if (!normalized)
237
+ return false;
238
+ return ipv4ToUint(normalized) >>> 24 === 127;
239
+ }
240
+ const groups = expandIPv6(address);
241
+ if (!groups)
242
+ return false;
243
+ return (groups[0] === 0 &&
244
+ groups[1] === 0 &&
245
+ groups[2] === 0 &&
246
+ groups[3] === 0 &&
247
+ groups[4] === 0 &&
248
+ groups[5] === 0 &&
249
+ groups[6] === 0 &&
250
+ groups[7] === 1);
251
+ }
252
+ function assertAddressAllowed(address, family) {
253
+ if (testAllowLoopbackResolution &&
254
+ process.env.VITEST === 'true' &&
255
+ isLoopbackForTest(address, family)) {
256
+ return;
257
+ }
258
+ if (isAddressBlocked(address, family))
259
+ throw new Error('Blocked host');
260
+ }
261
+ async function lookupHostAddresses(host) {
262
+ if (testDnsLookup && process.env.VITEST === 'true') {
263
+ return testDnsLookup(host);
264
+ }
265
+ const records = await dns.lookup(host, { all: true, verbatim: true });
266
+ return records.map((r) => ({
267
+ address: r.address,
268
+ family: r.family === 6 ? 6 : 4,
269
+ }));
270
+ }
271
+ async function validateUrlAndResolveAddresses(urlString) {
186
272
  let url;
187
273
  try {
188
274
  url = new URL(urlString);
@@ -201,74 +287,253 @@ export async function assertUrlSafeForFetch(urlString) {
201
287
  throw new Error('Blocked host');
202
288
  }
203
289
  if (isIPv4Literal(host)) {
204
- if (isBlockedIPv4(host))
205
- throw new Error('Blocked host');
206
- return url;
290
+ const normalized = normalizeIPv4Literal(host);
291
+ assertAddressAllowed(normalized, 4);
292
+ return { url, addresses: [{ address: normalized, family: 4 }] };
207
293
  }
208
294
  if (host.includes(':') && !host.startsWith('[')) {
209
- if (isBlockedIPv6(host))
210
- throw new Error('Blocked host');
211
- return url;
295
+ assertAddressAllowed(host, 6);
296
+ return { url, addresses: [{ address: host, family: 6 }] };
212
297
  }
213
298
  let lookupHost = host;
214
299
  if (host.startsWith('[') && host.endsWith(']')) {
215
300
  lookupHost = host.slice(1, -1);
216
- if (isBlockedIPv6(lookupHost))
217
- throw new Error('Blocked host');
218
- return url;
301
+ assertAddressAllowed(lookupHost, 6);
302
+ return { url, addresses: [{ address: lookupHost, family: 6 }] };
219
303
  }
220
- const records = await dns.lookup(lookupHost, { all: true, verbatim: true });
304
+ const records = await lookupHostAddresses(lookupHost);
221
305
  if (!records.length)
222
306
  throw new Error('Could not resolve host');
307
+ const addresses = [];
223
308
  for (const r of records) {
224
- const { address, family } = r;
225
- if (family === 4) {
226
- if (isBlockedIPv4(address))
227
- throw new Error('Blocked host');
309
+ assertAddressAllowed(r.address, r.family);
310
+ addresses.push(r);
311
+ }
312
+ return { url, addresses };
313
+ }
314
+ /** Resolve hostname and ensure the resolved address is not private/link-local. */
315
+ export async function assertUrlSafeForFetch(urlString) {
316
+ const { url } = await validateUrlAndResolveAddresses(urlString);
317
+ return url;
318
+ }
319
+ // Pin the socket to a pre-validated IP so connect-time DNS cannot rebind to private space.
320
+ function pinnedLookup(address, family) {
321
+ const deliver = (cb) => {
322
+ cb(null, address, family);
323
+ };
324
+ return (hostname, options, callback) => {
325
+ if (typeof options === 'function') {
326
+ deliver(options);
327
+ return;
328
+ }
329
+ if (!callback)
330
+ return;
331
+ const opts = typeof options === 'number' ? { family: options } : (options ?? {});
332
+ if (opts.all) {
333
+ callback(null, [{ address, family }]);
334
+ return;
335
+ }
336
+ deliver(callback);
337
+ };
338
+ }
339
+ function isConnectError(err) {
340
+ if (!(err instanceof Error))
341
+ return false;
342
+ const code = err.code;
343
+ return (code === 'ECONNREFUSED' ||
344
+ code === 'ECONNRESET' ||
345
+ code === 'ETIMEDOUT' ||
346
+ code === 'EHOSTUNREACH' ||
347
+ code === 'ENETUNREACH' ||
348
+ code === 'EPIPE');
349
+ }
350
+ function requestPort(url) {
351
+ if (url.port)
352
+ return Number(url.port);
353
+ return url.protocol === 'https:' ? 443 : 80;
354
+ }
355
+ function buildRequestOptions(url, pinned, timeoutMs) {
356
+ return {
357
+ hostname: url.hostname,
358
+ host: url.host,
359
+ port: requestPort(url),
360
+ path: `${url.pathname}${url.search}`,
361
+ method: 'GET',
362
+ headers: {
363
+ 'User-Agent': FETCH_USER_AGENT,
364
+ Accept: 'image/*, audio/*, video/*, */*;q=0.8',
365
+ // Prefer identity; decompress defensively if the origin ignores this.
366
+ 'Accept-Encoding': 'identity',
367
+ Host: url.host,
368
+ },
369
+ lookup: pinnedLookup(pinned.address, pinned.family),
370
+ signal: AbortSignal.timeout(timeoutMs),
371
+ };
372
+ }
373
+ function pinnedHttpRequest(url, pinned, timeoutMs) {
374
+ return new Promise((resolve, reject) => {
375
+ const options = buildRequestOptions(url, pinned, timeoutMs);
376
+ const req = http.request(options, (res) => resolve(res));
377
+ req.on('error', (err) => {
378
+ if (options.signal?.aborted) {
379
+ reject(new Error('Fetch timed out'));
380
+ return;
381
+ }
382
+ reject(err);
383
+ });
384
+ req.end();
385
+ });
386
+ }
387
+ function pinnedHttpsRequest(url, pinned, timeoutMs, ca) {
388
+ return new Promise((resolve, reject) => {
389
+ const options = buildRequestOptions(url, pinned, timeoutMs);
390
+ const req = https.request({ ...options, ca }, (res) => resolve(res));
391
+ req.on('error', (err) => {
392
+ if (options.signal?.aborted) {
393
+ reject(new Error('Fetch timed out'));
394
+ return;
395
+ }
396
+ reject(err);
397
+ });
398
+ req.end();
399
+ });
400
+ }
401
+ async function pinnedRequest(url, pinned, timeoutMs) {
402
+ const ca = testTrustedCa && process.env.VITEST === 'true' ? testTrustedCa : undefined;
403
+ if (url.protocol === 'https:') {
404
+ return pinnedHttpsRequest(url, pinned, timeoutMs, ca);
405
+ }
406
+ if (url.protocol === 'http:') {
407
+ return pinnedHttpRequest(url, pinned, timeoutMs);
408
+ }
409
+ throw new Error('Only HTTP(S) URLs are allowed');
410
+ }
411
+ async function pinnedRequestWithFallback(url, addresses, timeoutMs) {
412
+ if (!addresses.length)
413
+ throw new Error('Could not resolve host');
414
+ let lastError;
415
+ for (const pinned of addresses) {
416
+ try {
417
+ return await pinnedRequest(url, pinned, timeoutMs);
228
418
  }
229
- else if (family === 6) {
230
- if (isBlockedIPv6(address))
231
- throw new Error('Blocked host');
419
+ catch (err) {
420
+ if (err instanceof Error && err.message === 'Fetch timed out')
421
+ throw err;
422
+ lastError = err;
423
+ if (!isConnectError(err))
424
+ throw err;
232
425
  }
233
426
  }
234
- return url;
427
+ throw lastError instanceof Error ? lastError : new Error('Connection failed');
428
+ }
429
+ function drainIncomingMessage(res) {
430
+ return new Promise((resolve) => {
431
+ const finish = () => resolve();
432
+ res.on('end', finish);
433
+ res.on('close', finish);
434
+ res.on('error', finish);
435
+ res.resume();
436
+ });
235
437
  }
236
- async function readResponseBodyWithLimit(res, maxBytes) {
237
- const declared = res.headers.get('content-length');
238
- if (declared) {
239
- const n = parseInt(declared, 10);
240
- if (Number.isFinite(n) && n > maxBytes) {
241
- throw new Error('Response too large');
438
+ function parseContentEncodings(raw) {
439
+ if (!raw)
440
+ return [];
441
+ const out = [];
442
+ for (const part of raw.split(',')) {
443
+ const enc = part.trim().toLowerCase();
444
+ if (!enc || enc === 'identity')
445
+ continue;
446
+ switch (enc) {
447
+ case 'gzip':
448
+ case 'x-gzip':
449
+ case 'deflate':
450
+ case 'br':
451
+ out.push(enc);
452
+ break;
453
+ default:
454
+ throw new Error(`Unsupported Content-Encoding: ${enc}`);
242
455
  }
243
456
  }
244
- const reader = res.body?.getReader();
245
- if (!reader) {
246
- const buf = Buffer.from(await res.arrayBuffer());
247
- if (buf.length > maxBytes)
248
- throw new Error('Response too large');
249
- return buf;
457
+ return out;
458
+ }
459
+ function createDecompressTransform(encoding) {
460
+ switch (encoding) {
461
+ case 'gzip':
462
+ case 'x-gzip':
463
+ return createGunzip();
464
+ case 'deflate':
465
+ return createInflate();
466
+ case 'br':
467
+ return createBrotliDecompress();
468
+ default: {
469
+ const _exhaustive = encoding;
470
+ throw new Error(`Unsupported Content-Encoding: ${String(_exhaustive)}`);
471
+ }
472
+ }
473
+ }
474
+ function bodyStreamForResponse(res, encodings) {
475
+ let stream = res;
476
+ for (const enc of [...encodings].reverse()) {
477
+ stream = stream.pipe(createDecompressTransform(enc));
478
+ }
479
+ return stream;
480
+ }
481
+ async function readIncomingMessageWithLimit(res, maxBytes) {
482
+ let encodings;
483
+ try {
484
+ encodings = parseContentEncodings(headerValue(res.headers, 'content-encoding'));
485
+ }
486
+ catch (err) {
487
+ await releaseIncomingMessage(res);
488
+ throw err;
250
489
  }
490
+ if (!encodings.length) {
491
+ const declared = res.headers['content-length'];
492
+ if (declared) {
493
+ const n = parseInt(String(declared), 10);
494
+ if (Number.isFinite(n) && n > maxBytes) {
495
+ res.destroy();
496
+ throw new Error('Response too large');
497
+ }
498
+ }
499
+ }
500
+ const stream = bodyStreamForResponse(res, encodings);
251
501
  const chunks = [];
252
502
  let total = 0;
253
- for (;;) {
254
- const { done, value } = await reader.read();
255
- if (done)
256
- break;
257
- total += value.byteLength;
258
- if (total > maxBytes) {
259
- // Cancel the underlying body so the server connection can be released.
260
- try {
261
- await reader.cancel();
262
- }
263
- catch {
264
- /* ignore */
503
+ try {
504
+ for await (const chunk of stream) {
505
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
506
+ total += buf.length;
507
+ if (total > maxBytes) {
508
+ res.destroy();
509
+ if ('destroy' in stream && typeof stream.destroy === 'function') {
510
+ stream.destroy();
511
+ }
512
+ throw new Error('Response too large');
265
513
  }
266
- throw new Error('Response too large');
514
+ chunks.push(buf);
267
515
  }
268
- chunks.push(Buffer.from(value));
516
+ }
517
+ catch (err) {
518
+ if (err instanceof Error && err.message === 'Response too large')
519
+ throw err;
520
+ res.destroy();
521
+ if ('destroy' in stream && typeof stream.destroy === 'function') {
522
+ stream.destroy();
523
+ }
524
+ throw err;
269
525
  }
270
526
  return Buffer.concat(chunks);
271
527
  }
528
+ async function releaseIncomingMessage(res) {
529
+ await drainIncomingMessage(res);
530
+ }
531
+ function headerValue(headers, name) {
532
+ const raw = headers[name.toLowerCase()];
533
+ if (raw === undefined)
534
+ return null;
535
+ return Array.isArray(raw) ? (raw[0] ?? null) : raw;
536
+ }
272
537
  /**
273
538
  * Parse an RFC 2397 data URL into `{ mediaType, base64 }`. Accepts MIME
274
539
  * parameters (`data:audio/wav;charset=binary;base64,...`) and the bare
@@ -297,35 +562,33 @@ export function parseBase64DataUrl(source) {
297
562
  export async function fetchHttpResource(urlString, opts) {
298
563
  let current = urlString;
299
564
  for (let hop = 0; hop <= opts.maxRedirects; hop++) {
300
- const validated = await assertUrlSafeForFetch(current);
565
+ const { url: validated, addresses } = await validateUrlAndResolveAddresses(current);
301
566
  const target = validated.href;
302
- const controller = new AbortController();
303
- const t = setTimeout(() => controller.abort(), opts.timeoutMs);
304
567
  let res;
305
568
  try {
306
- res = await fetch(target, {
307
- redirect: 'manual',
308
- signal: controller.signal,
309
- headers: {
310
- 'User-Agent': FETCH_USER_AGENT,
311
- Accept: 'image/*, audio/*, video/*, */*;q=0.8',
312
- },
313
- });
569
+ res = await pinnedRequestWithFallback(validated, addresses, opts.timeoutMs);
314
570
  }
315
- finally {
316
- clearTimeout(t);
571
+ catch (err) {
572
+ if (err instanceof Error && err.message === 'Fetch timed out') {
573
+ throw new Error('Fetch timed out');
574
+ }
575
+ throw err;
317
576
  }
318
- if (res.status >= 300 && res.status < 400) {
319
- const loc = res.headers.get('location');
577
+ const statusCode = res.statusCode ?? 0;
578
+ if (statusCode >= 300 && statusCode < 400) {
579
+ const loc = headerValue(res.headers, 'location');
580
+ await releaseIncomingMessage(res);
320
581
  if (!loc)
321
582
  throw new Error('Redirect without Location header');
322
583
  current = new URL(loc, target).href;
323
584
  continue;
324
585
  }
325
- if (!res.ok)
326
- throw new Error(`HTTP ${res.status}`);
327
- const buffer = await readResponseBodyWithLimit(res, opts.maxBytes);
328
- return { buffer, contentType: res.headers.get('content-type') };
586
+ if (statusCode < 200 || statusCode >= 300) {
587
+ await releaseIncomingMessage(res);
588
+ throw new Error(`HTTP ${statusCode}`);
589
+ }
590
+ const buffer = await readIncomingMessageWithLimit(res, opts.maxBytes);
591
+ return { buffer, contentType: headerValue(res.headers, 'content-type') };
329
592
  }
330
593
  throw new Error('Too many redirects');
331
594
  }
@@ -1,4 +1,5 @@
1
1
  import OpenAI from 'openai';
2
+ export { detectAudioFormat } from './audio-utils.js';
2
3
  export interface GenerateAudioToolRequest {
3
4
  prompt: string;
4
5
  model?: string;
@@ -8,60 +9,14 @@ export interface GenerateAudioToolRequest {
8
9
  }
9
10
  /** Create a 44-byte WAV header for raw PCM16 data at `sampleRate` Hz. */
10
11
  export declare function createWavHeader(dataLength: number, sampleRate?: number): Buffer;
11
- /**
12
- * Detect audio container format from magic bytes. Uses `Buffer.subarray()`
13
- * (not deprecated `slice()`). MP3 detection is intentionally strict:
14
- * - Accept ID3v2 tags (`'ID3'`) as unambiguous MP3.
15
- * - Accept raw frame sync only when every MPEG header field falls in a
16
- * non-reserved range: version != 0b01, layer != 0b00, bitrate != 0b1111,
17
- * sample rate index != 0b11. This removes the false positives that a
18
- * sync-word-only check produces on random binary.
19
- */
20
- export declare function detectAudioFormat(data: Buffer): {
21
- ext: string;
22
- mimeType: string;
23
- };
24
12
  export declare function wrapPcmInWav(pcmData: Buffer, sampleRate?: number): Buffer;
25
- /** Strip existing extension (if any) and append a new one. */
26
- export declare function replaceExtension(filePath: string, newExt: string): string;
13
+ /** Decode each streamed base64 fragment and concatenate binary (joining strings corrupts padding). */
14
+ export declare function assembleBase64AudioChunks(chunks: string[]): Buffer;
27
15
  export declare function handleGenerateAudio(request: {
28
16
  params: {
29
17
  arguments: GenerateAudioToolRequest;
30
18
  };
31
- }, openai: OpenAI): Promise<import("../errors.js").ToolErrorResult | {
32
- content: ({
33
- type: "text";
34
- text: string;
35
- mimeType?: undefined;
36
- data?: undefined;
37
- } | {
38
- type: "audio";
39
- mimeType: string;
40
- data: string;
41
- text?: undefined;
42
- })[];
43
- _meta: {
44
- server_version: string;
45
- save_path: string;
46
- mime: string;
47
- size_bytes: number;
48
- };
49
- } | {
50
- content: ({
51
- type: "text";
52
- text: string;
53
- mimeType?: undefined;
54
- data?: undefined;
55
- } | {
56
- type: "audio";
57
- mimeType: string;
58
- data: string;
59
- text?: undefined;
60
- })[];
61
- _meta: {
62
- server_version: string;
63
- mime: string;
64
- size_bytes: number;
65
- save_path?: undefined;
66
- };
19
+ }, openai: OpenAI): Promise<{
20
+ content: import("./tool-result-payload.js").BinaryToolContent[];
21
+ _meta: Record<string, unknown>;
67
22
  }>;