@andreprado/agentkit 0.1.0-alpha.8 → 0.1.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 (117) hide show
  1. package/README.md +18 -1
  2. package/docs/guides/add-channel.md +251 -7
  3. package/docs/guides/add-knowledge.md +10 -0
  4. package/docs/guides/add-managed-composio.md +165 -0
  5. package/docs/guides/add-tool.md +10 -3
  6. package/docs/guides/channel-security.md +162 -32
  7. package/docs/guides/connect-discord.md +178 -0
  8. package/docs/guides/connect-slack.md +126 -0
  9. package/docs/guides/connect-telegram.md +61 -1
  10. package/docs/guides/connect-whatsapp-evolution.md +121 -0
  11. package/docs/guides/connect-whatsapp-uazapi.md +139 -0
  12. package/docs/guides/connect-whatsapp-zapster.md +119 -16
  13. package/docs/guides/create-agent.md +31 -4
  14. package/docs/guides/debug-channel.md +159 -0
  15. package/docs/guides/improve-from-production.md +151 -0
  16. package/docs/guides/prepare-deploy.md +32 -14
  17. package/docs/guides/replay-production-traces.md +72 -0
  18. package/docs/guides/run-evals.md +95 -25
  19. package/docs/guides/security-rules.md +9 -5
  20. package/docs/guides/send-feedback.md +135 -0
  21. package/docs/guides/use-jev.md +67 -0
  22. package/docs/guides/use-provider.md +70 -3
  23. package/docs/llms-full.txt +295 -25
  24. package/docs/llms.txt +54 -7
  25. package/package.json +3 -7
  26. package/src/cli/args.ts +23 -2
  27. package/src/cli/cloud-client.ts +121 -9
  28. package/src/cli/commands/channels.ts +1185 -50
  29. package/src/cli/commands/feedback.ts +438 -0
  30. package/src/cli/commands/provider.ts +47 -0
  31. package/src/cli/commands/transcribe.ts +171 -0
  32. package/src/cli/deploy-chat-ui.ts +232 -18
  33. package/src/cli/deploy-readiness.ts +227 -14
  34. package/src/cli/help.ts +68 -8
  35. package/src/cli/index.ts +740 -35
  36. package/src/cli/new-command.ts +41 -0
  37. package/src/cloud/client.ts +4 -3
  38. package/src/cloud/contracts.ts +1 -1
  39. package/src/create-project.ts +18 -35
  40. package/src/index.ts +565 -11
  41. package/src/providers/codex-auth.ts +111 -0
  42. package/src/providers/pi.ts +88 -19
  43. package/src/providers/test.ts +36 -0
  44. package/src/providers/types.ts +8 -0
  45. package/src/runtime/channel-test-harness.ts +21 -1
  46. package/src/runtime/channels/discord.ts +896 -0
  47. package/src/runtime/channels/generic-webhook.ts +974 -0
  48. package/src/runtime/channels/slack.ts +646 -0
  49. package/src/runtime/channels/telegram.ts +466 -23
  50. package/src/runtime/channels/whatsapp-evolution.ts +1357 -0
  51. package/src/runtime/channels/whatsapp-meta.ts +9 -0
  52. package/src/runtime/channels/whatsapp-uazapi.ts +1323 -0
  53. package/src/runtime/channels/whatsapp-zapster.ts +674 -40
  54. package/src/runtime/channels.ts +89 -4
  55. package/src/runtime/chat.ts +70 -44
  56. package/src/runtime/config.ts +489 -20
  57. package/src/runtime/core/manifest.ts +75 -5
  58. package/src/runtime/core/targets.ts +5 -5
  59. package/src/runtime/deploy-readiness.ts +34 -4
  60. package/src/runtime/dev-server.ts +639 -39
  61. package/src/runtime/env.ts +8 -3
  62. package/src/runtime/evals.ts +445 -74
  63. package/src/runtime/improve.ts +868 -0
  64. package/src/runtime/inspect.ts +173 -4
  65. package/src/runtime/integrations/composio.ts +425 -0
  66. package/src/runtime/knowledge/retrieve.ts +25 -5
  67. package/src/runtime/knowledge/schema.ts +45 -1
  68. package/src/runtime/prompt-context.ts +141 -0
  69. package/src/runtime/runtime-contract.ts +71 -7
  70. package/src/runtime/skills.ts +95 -0
  71. package/src/runtime/targets/cloudflare/build.ts +1010 -208
  72. package/src/runtime/targets/container/server.ts +1 -1
  73. package/src/runtime/targets/vps/deploy.ts +26 -9
  74. package/src/runtime/tool-runner.ts +9 -1
  75. package/src/runtime/tools.ts +26 -2
  76. package/src/runtime/transcription.ts +483 -0
  77. package/src/storage/sqlite.ts +7 -2
  78. package/src/templates/blank.ts +37 -9
  79. package/src/templates/dentista.ts +40 -14
  80. package/src/templates/skills/agentkit-build-agent/SKILL.md +34 -5
  81. package/src/templates/skills/agentkit-build-agent/templates/appointment-intake.instructions.md +2 -1
  82. package/src/templates/skills/agentkit-capsule/SKILL.md +32 -3
  83. package/src/templates/skills/agentkit-capsule/references/docs-router.md +2 -2
  84. package/src/templates/skills/agentkit-channels/SKILL.md +71 -6
  85. package/src/templates/skills/agentkit-channels/references/channel-buffering.md +9 -2
  86. package/src/templates/skills/agentkit-channels/references/channel-debugging.md +34 -5
  87. package/src/templates/skills/agentkit-channels/references/discord.md +93 -0
  88. package/src/templates/skills/agentkit-channels/references/slack.md +56 -0
  89. package/src/templates/skills/agentkit-channels/references/telegram.md +39 -4
  90. package/src/templates/skills/agentkit-channels/references/whatsapp-evolution.md +57 -0
  91. package/src/templates/skills/agentkit-channels/references/whatsapp-uazapi.md +54 -0
  92. package/src/templates/skills/agentkit-channels/references/whatsapp-zapster.md +42 -8
  93. package/src/templates/skills/agentkit-database/SKILL.md +11 -0
  94. package/src/templates/skills/agentkit-deploy/SKILL.md +9 -1
  95. package/src/templates/skills/agentkit-evals/SKILL.md +77 -13
  96. package/src/templates/skills/agentkit-evals/templates/multi-turn.eval.md +13 -6
  97. package/src/templates/skills/agentkit-evals/templates/no-leak.eval.md +8 -4
  98. package/src/templates/skills/agentkit-evals/templates/smoke.eval.md +8 -4
  99. package/src/templates/skills/agentkit-evals/templates/tool-call.eval.md +16 -7
  100. package/src/templates/skills/agentkit-improve/SKILL.md +96 -0
  101. package/src/templates/skills/agentkit-improve/references/replay-side-effects.md +18 -0
  102. package/src/templates/skills/agentkit-improve/references/trace-packets.md +22 -0
  103. package/src/templates/skills/agentkit-improve/templates/regression.eval.md +18 -0
  104. package/src/templates/skills/agentkit-integrations/SKILL.md +98 -0
  105. package/src/templates/skills/agentkit-knowledge/SKILL.md +4 -1
  106. package/src/templates/skills/agentkit-prompts/SKILL.md +3 -1
  107. package/src/templates/skills/agentkit-provider/SKILL.md +29 -4
  108. package/src/templates/skills/agentkit-security/SKILL.md +5 -2
  109. package/src/templates/skills/agentkit-tools/SKILL.md +8 -1
  110. package/src/templates/skills/agentkit-tools/examples/eval-safe-external-action.tool.md +8 -8
  111. package/src/templates/skills/agentkit-tools/examples/jev-service-fit.tool.md +110 -0
  112. package/src/templates/skills/agentkit-troubleshooting/SKILL.md +25 -1
  113. package/src/templates/support.ts +42 -12
  114. package/docs/guides/agentkit-skills-architecture.md +0 -471
  115. package/docs/guides/channels-implementation-map.md +0 -243
  116. package/docs/guides/channels-production-handoff.md +0 -101
  117. package/docs/portable-deploy-release-checklist.md +0 -41
@@ -1,5 +1,7 @@
1
1
  import type {
2
2
  ChannelAdapter,
3
+ ChannelMediaDownloadInput,
4
+ ChannelMediaDownloadResult,
3
5
  ChannelSendInput,
4
6
  ChannelStatusResult,
5
7
  RawWebhookEvent,
@@ -86,13 +88,37 @@ export const telegramChannelAdapter: ChannelAdapter = {
86
88
 
87
89
  const text = readString(message, "text");
88
90
 
89
- if (!text) {
91
+ if (text) {
90
92
  return [
91
93
  {
92
94
  ...base,
93
- kind: "unsupported",
95
+ kind: "message",
96
+ externalIdentity: identity,
97
+ message: {
98
+ role: "user",
99
+ contentType: "text",
100
+ content: text,
101
+ providerMessageId: readNumberishString(message, "message_id"),
102
+ },
103
+ },
104
+ ];
105
+ }
106
+
107
+ const audio = normalizeTelegramAudio(message);
108
+
109
+ if (audio) {
110
+ return [
111
+ {
112
+ ...base,
113
+ kind: "message",
94
114
  externalIdentity: identity,
95
- unsupportedReason: "unsupported_message_type:non_text",
115
+ message: {
116
+ role: "user",
117
+ contentType: "audio",
118
+ content: "",
119
+ providerMessageId: readNumberishString(message, "message_id"),
120
+ audio,
121
+ },
96
122
  },
97
123
  ];
98
124
  }
@@ -100,30 +126,17 @@ export const telegramChannelAdapter: ChannelAdapter = {
100
126
  return [
101
127
  {
102
128
  ...base,
103
- kind: "message",
129
+ kind: "unsupported",
104
130
  externalIdentity: identity,
105
- message: {
106
- role: "user",
107
- contentType: "text",
108
- content: text,
109
- providerMessageId: readNumberishString(message, "message_id"),
110
- },
131
+ unsupportedReason: "unsupported_message_type:non_text",
111
132
  },
112
133
  ];
113
134
  },
135
+ async downloadMedia(input) {
136
+ return downloadTelegramMedia(input);
137
+ },
114
138
  async sendMessage(input) {
115
- const request = buildTelegramSendMessageRequest(input);
116
-
117
- return {
118
- ok: true,
119
- status: "sent",
120
- providerRequestId: request.idempotencyKey,
121
- providerMetadata: {
122
- method: request.method,
123
- url: request.url,
124
- body: request.body,
125
- },
126
- };
139
+ return sendTelegramMessage(input);
127
140
  },
128
141
  getStatus(input) {
129
142
  if (!input.secrets.TELEGRAM_BOT_TOKEN) {
@@ -152,7 +165,289 @@ export type TelegramSendMessageRequest = {
152
165
  idempotencyKey: string;
153
166
  };
154
167
 
168
+ type TelegramFetch = (url: string, init: RequestInit) => Promise<Response>;
169
+ const TELEGRAM_MAX_DOWNLOAD_BYTES = 20_000_000;
170
+
155
171
  export function buildTelegramSendMessageRequest(input: ChannelSendInput): TelegramSendMessageRequest {
172
+ return buildTelegramSendMessageRequestInternal(input, "<redacted>");
173
+ }
174
+
175
+ export async function sendTelegramMessage(
176
+ input: ChannelSendInput,
177
+ fetcher: TelegramFetch = fetch,
178
+ ): Promise<Awaited<ReturnType<ChannelAdapter["sendMessage"]>>> {
179
+ if (input.secrets.AGENTKIT_CHANNEL_SEND_DRY_RUN === "1") {
180
+ const request = buildTelegramSendMessageRequest(input);
181
+
182
+ return {
183
+ ok: true,
184
+ status: "stubbed",
185
+ providerRequestId: request.idempotencyKey,
186
+ providerMetadata: {
187
+ method: request.method,
188
+ url: request.url,
189
+ body: request.body,
190
+ },
191
+ };
192
+ }
193
+
194
+ const token = input.secrets.TELEGRAM_BOT_TOKEN;
195
+
196
+ if (!token) {
197
+ return {
198
+ ok: false,
199
+ retryable: false,
200
+ code: "channel_secret_missing",
201
+ message: "Secret TELEGRAM_BOT_TOKEN is not set for this channel.",
202
+ };
203
+ }
204
+
205
+ let request: TelegramSendMessageRequest;
206
+ let actualRequest: TelegramSendMessageRequest;
207
+
208
+ try {
209
+ request = buildTelegramSendMessageRequest(input);
210
+ actualRequest = buildTelegramSendMessageRequestInternal(input, token);
211
+ } catch (error) {
212
+ return {
213
+ ok: false,
214
+ retryable: false,
215
+ code: "channel_send_failed",
216
+ message: error instanceof Error ? error.message : String(error),
217
+ };
218
+ }
219
+
220
+ let response: Response;
221
+
222
+ try {
223
+ response = await fetcher(actualRequest.url, {
224
+ method: actualRequest.method,
225
+ headers: {
226
+ "Content-Type": "application/json",
227
+ },
228
+ body: JSON.stringify(actualRequest.body),
229
+ });
230
+ } catch (error) {
231
+ const errorMessage = (error instanceof Error ? error.message : String(error)).replace(token, "<redacted>");
232
+
233
+ return {
234
+ ok: false,
235
+ retryable: true,
236
+ code: "channel_provider_unavailable",
237
+ message: `Telegram sendMessage request failed before a response: ${errorMessage}`,
238
+ providerRequestId: request.idempotencyKey,
239
+ providerMetadata: {
240
+ method: request.method,
241
+ url: request.url,
242
+ body: request.body,
243
+ },
244
+ };
245
+ }
246
+
247
+ const payload = await response.json().catch(() => null);
248
+
249
+ if (!response.ok || !isRecord(payload) || payload.ok !== true) {
250
+ const description =
251
+ isRecord(payload) && typeof payload.description === "string"
252
+ ? payload.description
253
+ : `Telegram sendMessage returned HTTP ${response.status}.`;
254
+ const syntheticExpectedFailure = isSyntheticTelegramChatId(request.body.chat_id) && /chat not found/i.test(description);
255
+
256
+ return {
257
+ ok: false,
258
+ retryable: syntheticExpectedFailure ? false : response.status === 429 || response.status >= 500,
259
+ code: syntheticExpectedFailure ? "synthetic_expected_failure" : "channel_send_failed",
260
+ message: syntheticExpectedFailure
261
+ ? "Synthetic Telegram smoke reached AgentKit. Telegram correctly rejected the fake chat ID."
262
+ : description,
263
+ providerRequestId: request.idempotencyKey,
264
+ providerMetadata: {
265
+ method: request.method,
266
+ url: request.url,
267
+ body: request.body,
268
+ },
269
+ };
270
+ }
271
+
272
+ const result = isRecord(payload.result) ? payload.result : undefined;
273
+ const messageId = readNumberishString(result ?? {}, "message_id");
274
+
275
+ if (!messageId) {
276
+ return {
277
+ ok: false,
278
+ retryable: false,
279
+ code: "channel_send_failed",
280
+ message: "Telegram sendMessage did not return result.message_id.",
281
+ providerRequestId: request.idempotencyKey,
282
+ providerMetadata: {
283
+ method: request.method,
284
+ url: request.url,
285
+ body: request.body,
286
+ },
287
+ };
288
+ }
289
+
290
+ return {
291
+ ok: true,
292
+ status: "sent",
293
+ providerRequestId: request.idempotencyKey,
294
+ providerMessageId: messageId,
295
+ providerMetadata: {
296
+ method: request.method,
297
+ url: request.url,
298
+ body: request.body,
299
+ },
300
+ };
301
+ }
302
+
303
+ export async function downloadTelegramMedia(
304
+ input: ChannelMediaDownloadInput,
305
+ fetcher: TelegramFetch = fetch,
306
+ ): Promise<ChannelMediaDownloadResult> {
307
+ const token = input.secrets.TELEGRAM_BOT_TOKEN;
308
+
309
+ if (!token) {
310
+ return {
311
+ ok: false,
312
+ retryable: false,
313
+ code: "channel_secret_missing",
314
+ message: "Secret TELEGRAM_BOT_TOKEN is not set for this channel.",
315
+ };
316
+ }
317
+
318
+ const audio = input.message.audio;
319
+
320
+ if (!audio?.providerMediaId) {
321
+ return {
322
+ ok: false,
323
+ retryable: false,
324
+ code: "channel_audio_download_unavailable",
325
+ message: "Telegram audio message is missing file_id.",
326
+ };
327
+ }
328
+
329
+ let fileResponse: Response;
330
+
331
+ try {
332
+ const url = new URL(`/bot${token}/getFile`, telegramApiBaseUrl(input.secrets));
333
+ url.searchParams.set("file_id", audio.providerMediaId);
334
+ fileResponse = await fetcher(url.href, { method: "GET" });
335
+ } catch (error) {
336
+ return {
337
+ ok: false,
338
+ retryable: true,
339
+ code: "channel_provider_unavailable",
340
+ message: `Telegram getFile request failed before a response: ${redactToken(
341
+ error instanceof Error ? error.message : String(error),
342
+ token,
343
+ )}`,
344
+ };
345
+ }
346
+
347
+ const filePayload = await fileResponse.json().catch(() => null);
348
+
349
+ if (!fileResponse.ok || !isRecord(filePayload) || filePayload.ok !== true || !isRecord(filePayload.result)) {
350
+ const description =
351
+ isRecord(filePayload) && typeof filePayload.description === "string"
352
+ ? filePayload.description
353
+ : `Telegram getFile returned HTTP ${fileResponse.status}.`;
354
+
355
+ return {
356
+ ok: false,
357
+ retryable: fileResponse.status === 429 || fileResponse.status >= 500,
358
+ code: fileResponse.status === 429 || fileResponse.status >= 500
359
+ ? "channel_provider_unavailable"
360
+ : "channel_media_download_failed",
361
+ message: redactToken(description, token),
362
+ providerMetadata: {
363
+ status: fileResponse.status,
364
+ },
365
+ };
366
+ }
367
+
368
+ const filePath = readString(filePayload.result, "file_path");
369
+
370
+ if (!filePath) {
371
+ return {
372
+ ok: false,
373
+ retryable: false,
374
+ code: "channel_media_download_failed",
375
+ message: "Telegram getFile did not return result.file_path.",
376
+ providerMetadata: {
377
+ status: fileResponse.status,
378
+ },
379
+ };
380
+ }
381
+
382
+ const maxDownloadBytes = Math.min(input.maxBytes ?? TELEGRAM_MAX_DOWNLOAD_BYTES, TELEGRAM_MAX_DOWNLOAD_BYTES);
383
+ const fileSize = readNumber(filePayload.result, "file_size") ?? audio.sizeBytes;
384
+
385
+ if (fileSize !== undefined && fileSize > maxDownloadBytes) {
386
+ return {
387
+ ok: false,
388
+ retryable: false,
389
+ code: "channel_audio_too_large",
390
+ message: `Telegram audio is ${fileSize} bytes, which exceeds the configured ${maxDownloadBytes} byte limit.`,
391
+ providerMetadata: {
392
+ sizeBytes: fileSize,
393
+ },
394
+ };
395
+ }
396
+
397
+ let mediaResponse: Response;
398
+
399
+ try {
400
+ mediaResponse = await fetcher(`${telegramApiBaseUrl(input.secrets)}/file/bot${token}/${filePath}`, { method: "GET" });
401
+ } catch (error) {
402
+ return {
403
+ ok: false,
404
+ retryable: true,
405
+ code: "channel_provider_unavailable",
406
+ message: `Telegram file download failed before a response: ${redactToken(
407
+ error instanceof Error ? error.message : String(error),
408
+ token,
409
+ )}`,
410
+ };
411
+ }
412
+
413
+ if (!mediaResponse.ok) {
414
+ return {
415
+ ok: false,
416
+ retryable: mediaResponse.status === 429 || mediaResponse.status >= 500,
417
+ code: mediaResponse.status === 429 || mediaResponse.status >= 500
418
+ ? "channel_provider_unavailable"
419
+ : "channel_media_download_failed",
420
+ message: `Telegram file download returned HTTP ${mediaResponse.status}.`,
421
+ providerMetadata: {
422
+ status: mediaResponse.status,
423
+ },
424
+ };
425
+ }
426
+
427
+ const bytes = await readResponseBytesWithLimit(mediaResponse, maxDownloadBytes);
428
+
429
+ if (!bytes.ok) {
430
+ return bytes;
431
+ }
432
+
433
+ return {
434
+ ok: true,
435
+ audio: bytes.audio,
436
+ filename: audio.filename ?? basename(filePath) ?? `${audio.providerMediaId}.ogg`,
437
+ ...(audio.mimeType ? { mimeType: audio.mimeType } : {}),
438
+ ...(audio.durationSeconds !== undefined ? { durationSeconds: audio.durationSeconds } : {}),
439
+ sizeBytes: bytes.audio.byteLength,
440
+ providerMetadata: {
441
+ fileSize: fileSize ?? bytes.audio.byteLength,
442
+ },
443
+ };
444
+ }
445
+
446
+ function isSyntheticTelegramChatId(chatId: string): boolean {
447
+ return chatId === "456" || chatId.startsWith("agentkit-synthetic-");
448
+ }
449
+
450
+ function buildTelegramSendMessageRequestInternal(input: ChannelSendInput, token: string): TelegramSendMessageRequest {
156
451
  const chatId = input.externalIdentity.key.match(/:chat:([^:]+)$/)?.[1];
157
452
 
158
453
  if (!chatId) {
@@ -161,7 +456,7 @@ export function buildTelegramSendMessageRequest(input: ChannelSendInput): Telegr
161
456
 
162
457
  return {
163
458
  method: "POST",
164
- url: "https://api.telegram.org/bot<redacted>/sendMessage",
459
+ url: `https://api.telegram.org/bot${token}/sendMessage`,
165
460
  body: {
166
461
  chat_id: chatId,
167
462
  text: input.message.content,
@@ -170,6 +465,48 @@ export function buildTelegramSendMessageRequest(input: ChannelSendInput): Telegr
170
465
  };
171
466
  }
172
467
 
468
+ function normalizeTelegramAudio(message: Record<string, unknown>) {
469
+ const voice = readRecord(message, "voice");
470
+
471
+ if (Object.keys(voice).length > 0) {
472
+ const fileId = readString(voice, "file_id");
473
+
474
+ if (!fileId) {
475
+ return null;
476
+ }
477
+
478
+ return {
479
+ providerMediaId: fileId,
480
+ kind: "voice" as const,
481
+ mimeType: readString(voice, "mime_type") ?? "audio/ogg",
482
+ filename: `${fileId}.ogg`,
483
+ ...(readNumber(voice, "duration") !== undefined ? { durationSeconds: readNumber(voice, "duration") } : {}),
484
+ ...(readNumber(voice, "file_size") !== undefined ? { sizeBytes: readNumber(voice, "file_size") } : {}),
485
+ };
486
+ }
487
+
488
+ const audio = readRecord(message, "audio");
489
+
490
+ if (Object.keys(audio).length > 0) {
491
+ const fileId = readString(audio, "file_id");
492
+
493
+ if (!fileId) {
494
+ return null;
495
+ }
496
+
497
+ return {
498
+ providerMediaId: fileId,
499
+ kind: "audio" as const,
500
+ ...(readString(audio, "mime_type") ? { mimeType: readString(audio, "mime_type") } : {}),
501
+ filename: readString(audio, "file_name") ?? `${fileId}.mp3`,
502
+ ...(readNumber(audio, "duration") !== undefined ? { durationSeconds: readNumber(audio, "duration") } : {}),
503
+ ...(readNumber(audio, "file_size") !== undefined ? { sizeBytes: readNumber(audio, "file_size") } : {}),
504
+ };
505
+ }
506
+
507
+ return null;
508
+ }
509
+
173
510
  export function parseTelegramWebhookInfo(input: unknown, checkedAt: string): ChannelStatusResult {
174
511
  if (!isRecord(input) || input.ok !== true || !isRecord(input.result)) {
175
512
  return {
@@ -235,6 +572,112 @@ function readNumberishString(value: Record<string, unknown>, key: string): strin
235
572
  return undefined;
236
573
  }
237
574
 
575
+ function telegramApiBaseUrl(env: Record<string, string | undefined>): string {
576
+ return env.AGENTKIT_TELEGRAM_API_BASE_URL ?? "https://api.telegram.org";
577
+ }
578
+
579
+ function basename(path: string): string | null {
580
+ const name = path.split("/").filter(Boolean).at(-1);
581
+ return name && name.length > 0 ? name : null;
582
+ }
583
+
584
+ async function readResponseBytesWithLimit(
585
+ response: Response,
586
+ maxBytes: number,
587
+ ): Promise<
588
+ | {
589
+ ok: true;
590
+ audio: Uint8Array;
591
+ }
592
+ | {
593
+ ok: false;
594
+ retryable: false;
595
+ code: "channel_audio_too_large";
596
+ message: string;
597
+ providerMetadata?: Record<string, unknown>;
598
+ }
599
+ > {
600
+ const contentLength = response.headers.get("content-length");
601
+ const declaredBytes = contentLength ? Number(contentLength) : undefined;
602
+
603
+ if (declaredBytes !== undefined && Number.isFinite(declaredBytes) && declaredBytes > maxBytes) {
604
+ return {
605
+ ok: false,
606
+ retryable: false,
607
+ code: "channel_audio_too_large",
608
+ message: `Audio response declares ${declaredBytes} bytes, which exceeds the configured ${maxBytes} byte limit.`,
609
+ providerMetadata: {
610
+ sizeBytes: declaredBytes,
611
+ },
612
+ };
613
+ }
614
+
615
+ if (!response.body) {
616
+ const audio = new Uint8Array(await response.arrayBuffer());
617
+
618
+ if (audio.byteLength > maxBytes) {
619
+ return {
620
+ ok: false,
621
+ retryable: false,
622
+ code: "channel_audio_too_large",
623
+ message: `Audio response is ${audio.byteLength} bytes, which exceeds the configured ${maxBytes} byte limit.`,
624
+ providerMetadata: {
625
+ sizeBytes: audio.byteLength,
626
+ },
627
+ };
628
+ }
629
+
630
+ return { ok: true, audio };
631
+ }
632
+
633
+ const reader = response.body.getReader();
634
+ const chunks: Uint8Array[] = [];
635
+ let totalBytes = 0;
636
+
637
+ try {
638
+ while (true) {
639
+ const chunk = await reader.read();
640
+
641
+ if (chunk.done) {
642
+ break;
643
+ }
644
+
645
+ totalBytes += chunk.value.byteLength;
646
+
647
+ if (totalBytes > maxBytes) {
648
+ await reader.cancel().catch(() => undefined);
649
+ return {
650
+ ok: false,
651
+ retryable: false,
652
+ code: "channel_audio_too_large",
653
+ message: `Audio response exceeded the configured ${maxBytes} byte limit while downloading.`,
654
+ providerMetadata: {
655
+ sizeBytes: totalBytes,
656
+ },
657
+ };
658
+ }
659
+
660
+ chunks.push(chunk.value);
661
+ }
662
+ } finally {
663
+ reader.releaseLock();
664
+ }
665
+
666
+ const audio = new Uint8Array(totalBytes);
667
+ let offset = 0;
668
+
669
+ for (const chunk of chunks) {
670
+ audio.set(chunk, offset);
671
+ offset += chunk.byteLength;
672
+ }
673
+
674
+ return { ok: true, audio };
675
+ }
676
+
677
+ function redactToken(value: string, token: string): string {
678
+ return token ? value.replaceAll(token, "<redacted>") : value;
679
+ }
680
+
238
681
  function redactId(value: string): string {
239
682
  if (value.length <= 2) {
240
683
  return "*".repeat(value.length);