@juspay/neurolink 13.4.10 → 13.5.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 (33) hide show
  1. package/CHANGELOG.md +3 -4
  2. package/README.md +3 -2
  3. package/dist/browser/neurolink.min.js +419 -419
  4. package/dist/cli/commands/decide.js +25 -5
  5. package/dist/factories/providerDescriptors.d.ts +4 -3
  6. package/dist/factories/providerDescriptors.js +6 -4
  7. package/dist/index.d.ts +1 -1
  8. package/dist/index.js +1 -1
  9. package/dist/neurolink.d.ts +4 -2
  10. package/dist/neurolink.js +19 -4
  11. package/dist/providers/catalog/provider-catalog.schema.json +64 -3
  12. package/dist/providers/catalog/schema.js +81 -1
  13. package/dist/providers/catalog/xor.json +30 -10
  14. package/dist/providers/configuredDecision.d.ts +8 -1
  15. package/dist/providers/configuredDecision.js +22 -0
  16. package/dist/providers/decide/dialects/openaiDecisions.js +8 -0
  17. package/dist/providers/decide/dialects/systemOne.js +28 -2
  18. package/dist/providers/decide/guards.d.ts +6 -0
  19. package/dist/providers/decide/guards.js +22 -0
  20. package/dist/providers/systemOneDecision.d.ts +30 -7
  21. package/dist/providers/systemOneDecision.js +71 -14
  22. package/dist/types/cli.d.ts +2 -0
  23. package/dist/types/decision.d.ts +117 -10
  24. package/dist/types/providerCatalog.d.ts +12 -1
  25. package/dist/types/providers.d.ts +2 -0
  26. package/dist/utils/decisionAnswers.d.ts +7 -1
  27. package/dist/utils/decisionAnswers.js +43 -2
  28. package/dist/utils/decisionLimits.d.ts +13 -1
  29. package/dist/utils/decisionLimits.js +52 -1
  30. package/dist/utils/decisionMedia.d.ts +4 -2
  31. package/dist/utils/decisionMedia.js +216 -14
  32. package/docs-site/static/search-index.json +24 -21
  33. package/package.json +1 -1
@@ -34,7 +34,13 @@ export function readDecisionChoice(answers, id) {
34
34
  const ranked = Object.entries(probabilities)
35
35
  .map(([name, probability]) => ({ name, probability }))
36
36
  .sort((a, b) => b.probability - a.probability);
37
- return { choice, confidence, probabilities, ranked };
37
+ return {
38
+ choice,
39
+ confidence,
40
+ probabilities,
41
+ ranked,
42
+ unknown: answer.unknown === true,
43
+ };
38
44
  }
39
45
  export function readDecisionScore(answers, id) {
40
46
  const answer = answers[id];
@@ -44,6 +50,22 @@ export function readDecisionScore(answers, id) {
44
50
  const { score, confidence, legend, probabilities } = answer;
45
51
  return { score, confidence, legend, probabilities };
46
52
  }
53
+ /**
54
+ * Read an extracted value. `value` is null and `unknown` true when the model
55
+ * found nothing to extract, which is an answer, not a missing one: undefined
56
+ * still means the id is absent or the answer was another type.
57
+ */
58
+ export function readDecisionValue(answers, id) {
59
+ const answer = answers[id];
60
+ if (answer?.type !== "value") {
61
+ return undefined;
62
+ }
63
+ return {
64
+ value: answer.value,
65
+ confidence: answer.confidence,
66
+ unknown: answer.unknown === true || answer.value === null,
67
+ };
68
+ }
47
69
  /**
48
70
  * A yes/no answer carries no confidence of its own, so certainty has to be
49
71
  * inferred from how far the probability sits from a coin flip. 0.5 → 0, and
@@ -136,6 +158,15 @@ export function stripHostAnswers(result, hostIdByWireId) {
136
158
  return { ...result, answers };
137
159
  }
138
160
  const isRecordValue = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
161
+ function compilesAsRegex(pattern) {
162
+ try {
163
+ new RegExp(pattern);
164
+ return true;
165
+ }
166
+ catch {
167
+ return false;
168
+ }
169
+ }
139
170
  /**
140
171
  * Whether `value` is something a decision model accepts as content: a
141
172
  * string, number or boolean, or an array or plain object of the same.
@@ -198,7 +229,17 @@ function isWellFormedDecisionQuestion(value) {
198
229
  case "choice":
199
230
  return (isRecordValue(value.criteria) &&
200
231
  Object.keys(value.criteria).length >= 2 &&
201
- Object.values(value.criteria).every((rubric) => rubric === null || isDecisionInput(rubric)));
232
+ Object.values(value.criteria).every((rubric) => rubric === null || isDecisionInput(rubric)) &&
233
+ (value.unknown === undefined || typeof value.unknown === "boolean"));
234
+ case "value":
235
+ return ((value.dtype === undefined ||
236
+ value.dtype === "string" ||
237
+ value.dtype === "int" ||
238
+ value.dtype === "float") &&
239
+ (value.pattern === undefined ||
240
+ (typeof value.pattern === "string" &&
241
+ value.pattern.length > 0 &&
242
+ compilesAsRegex(value.pattern))));
202
243
  case "score":
203
244
  return (Array.isArray(value.criteria) &&
204
245
  value.criteria.length >= 2 &&
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * @module utils/decisionLimits
13
13
  */
14
- import type { DecisionLimits, DecisionLimitsReading, DecisionState, ProviderDescriptor } from "../types/index.js";
14
+ import type { DecisionLimits, DecisionLimitsReading, DecisionQuestion, DecisionQuestionType, DecisionState, ProviderDescriptor } from "../types/index.js";
15
15
  /**
16
16
  * Estimate how many tokens a decision model will read from `state`.
17
17
  *
@@ -37,3 +37,15 @@ export declare function estimateDecisionStateTokens(state: DecisionState, limits
37
37
  * limits.
38
38
  */
39
39
  export declare function resolveDecisionLimitsReading(descriptor: Pick<ProviderDescriptor, "name" | "decisionLimits">, model: string): DecisionLimitsReading | null;
40
+ /** What a provider reads when its limits list no `questionTypes`. */
41
+ export declare const DEFAULT_DECISION_QUESTION_TYPES: readonly DecisionQuestionType[];
42
+ /** The type a question needs a provider to read; a `choice` with `unknown` on needs `choice-unknown`. */
43
+ export declare function decisionQuestionTypeOf(question: DecisionQuestion): DecisionQuestionType;
44
+ /**
45
+ * Why a provider with these limits cannot be asked `question`, or undefined
46
+ * when it can. Applies whether or not the limits are advisory: a question type
47
+ * the server does not know is refused outright, never answered from a guess.
48
+ * `limits` is null for a provider that declares no limits at all, which reads
49
+ * the default types and has no per-question caps.
50
+ */
51
+ export declare function unsupportedDecisionQuestion(question: DecisionQuestion, limits: Pick<DecisionLimitsReading, "questionTypes" | "maxScoreLevels" | "maxChoiceOptions"> | null): string | undefined;
@@ -94,10 +94,19 @@ export function resolveDecisionLimitsReading(descriptor, model) {
94
94
  model,
95
95
  maxStateTokens: modelLimits?.maxStateTokens ?? limits.maxStateTokens,
96
96
  // Absent for a provider that caps by tokens or bytes rather than by
97
- // question count (TypeSafe, XOR).
97
+ // question count (TypeSafe).
98
98
  ...(limits.maxQuestions !== undefined
99
99
  ? { maxQuestions: limits.maxQuestions }
100
100
  : {}),
101
+ ...(limits.questionTypes !== undefined
102
+ ? { questionTypes: [...limits.questionTypes] }
103
+ : {}),
104
+ ...(limits.maxScoreLevels !== undefined
105
+ ? { maxScoreLevels: limits.maxScoreLevels }
106
+ : {}),
107
+ ...(limits.maxChoiceOptions !== undefined
108
+ ? { maxChoiceOptions: limits.maxChoiceOptions }
109
+ : {}),
101
110
  ...(nonAsciiTokensPerChar !== undefined ? { nonAsciiTokensPerChar } : {}),
102
111
  // Not model-overridable — `models` only carries `maxStateTokens` and
103
112
  // `nonAsciiTokensPerChar` — so these three come from the base limits only.
@@ -114,3 +123,45 @@ export function resolveDecisionLimitsReading(descriptor, model) {
114
123
  enforcedLocally: limits.advisory !== true,
115
124
  };
116
125
  }
126
+ /** What a provider reads when its limits list no `questionTypes`. */
127
+ export const DEFAULT_DECISION_QUESTION_TYPES = ["boolean", "choice", "score"];
128
+ /** The type a question needs a provider to read; a `choice` with `unknown` on needs `choice-unknown`. */
129
+ export function decisionQuestionTypeOf(question) {
130
+ return question.type === "choice" && question.unknown === true
131
+ ? "choice-unknown"
132
+ : question.type;
133
+ }
134
+ /**
135
+ * Why a provider with these limits cannot be asked `question`, or undefined
136
+ * when it can. Applies whether or not the limits are advisory: a question type
137
+ * the server does not know is refused outright, never answered from a guess.
138
+ * `limits` is null for a provider that declares no limits at all, which reads
139
+ * the default types and has no per-question caps.
140
+ */
141
+ export function unsupportedDecisionQuestion(question, limits) {
142
+ const types = limits?.questionTypes ?? DEFAULT_DECISION_QUESTION_TYPES;
143
+ const type = decisionQuestionTypeOf(question);
144
+ if (!types.includes(type)) {
145
+ return type === "choice-unknown"
146
+ ? "a choice with unknown: true is not accepted"
147
+ : `the question type "${type}" is not accepted (accepted: ${types.join(", ")})`;
148
+ }
149
+ if (question.type === "choice") {
150
+ const keys = Object.keys(question.criteria);
151
+ if (question.unknown === true && keys.includes("unknown")) {
152
+ return 'a choice with unknown: true cannot also name an option "unknown"';
153
+ }
154
+ const options = keys.length + (question.unknown === true ? 1 : 0);
155
+ const max = limits?.maxChoiceOptions;
156
+ if (max !== undefined && options > max) {
157
+ return `a choice may have at most ${max} options${question.unknown === true ? ", counting unknown" : ""}; this one has ${options}`;
158
+ }
159
+ }
160
+ if (question.type === "score") {
161
+ const max = limits?.maxScoreLevels;
162
+ if (max !== undefined && question.criteria.length > max) {
163
+ return `a score may have at most ${max} levels; this one has ${question.criteria.length}`;
164
+ }
165
+ }
166
+ return undefined;
167
+ }
@@ -1,9 +1,11 @@
1
1
  import type { DecisionMediaLimits, DecisionMediaResult, DecisionMediaSource } from "../types/index.js";
2
2
  /**
3
- * Turn the caller's images and video into `data:` URLs, or say why not.
4
- * Runs before any request, so every refusal costs nothing and none is retried.
3
+ * Turn the caller's images and video into `data:` URLs, and an audio clip into
4
+ * `{ data, format }`, or say why not. Runs before any request, so every
5
+ * refusal costs nothing and none is retried.
5
6
  */
6
7
  export declare function prepareDecisionMedia(request: {
7
8
  images?: readonly DecisionMediaSource[];
8
9
  video?: DecisionMediaSource;
10
+ audio?: DecisionMediaSource;
9
11
  }, limits: DecisionMediaLimits | undefined, vendor: string, alternatives: readonly string[]): Promise<DecisionMediaResult>;
@@ -7,11 +7,13 @@ const MAX_ECHOED_PATH_CHARS = 200;
7
7
  const DATA_URL = {
8
8
  image: /^data:image\/[a-z0-9.+-]+;base64,/i,
9
9
  video: /^data:video\/[a-z0-9.+-]+;base64,/i,
10
+ audio: /^data:audio\/[a-z0-9.+-]+;base64,/i,
10
11
  };
11
12
  const WEBM_HEADER = [0x1a, 0x45, 0xdf, 0xa3];
12
13
  const NOT_RECOGNISED = {
13
14
  image: "is not a recognised image format",
14
15
  video: "is not a recognised video format (MP4, MOV or WebM)",
16
+ audio: "is not a recognised audio format (WAV, FLAC, Ogg or MP3)",
15
17
  };
16
18
  /**
17
19
  * MP4 and MOV share the ISO-BMFF box. An AVIF or HEIC image shares it too, and
@@ -26,6 +28,153 @@ function sniffVideoMimeType(buffer) {
26
28
  ? "video/webm"
27
29
  : null;
28
30
  }
31
+ const ascii = (buffer, start, end) => buffer.subarray(start, end).toString("latin1");
32
+ /**
33
+ * The audio containers a decision model is known to decode. MP4 audio (M4A,
34
+ * AAC) is told apart so it can be refused by name: it was measured decoding to
35
+ * silence, with the request still answered as if it had been heard.
36
+ */
37
+ function sniffAudioFormat(buffer) {
38
+ if (ascii(buffer, 0, 4) === "RIFF" && ascii(buffer, 8, 12) === "WAVE") {
39
+ return "wav";
40
+ }
41
+ if (ascii(buffer, 0, 4) === "fLaC") {
42
+ return "flac";
43
+ }
44
+ if (ascii(buffer, 0, 4) === "OggS") {
45
+ return "ogg";
46
+ }
47
+ if (hasFtypBoxSignature(buffer)) {
48
+ return "aac";
49
+ }
50
+ if (ascii(buffer, 0, 3) === "ID3") {
51
+ return "mp3";
52
+ }
53
+ // An MPEG frame sync. Layer bits 00 mark an ADTS (AAC) stream instead.
54
+ if (buffer.length >= 2 && buffer[0] === 0xff && (buffer[1] & 0xe0) === 0xe0) {
55
+ return (buffer[1] & 0x06) === 0 ? "aac" : "mp3";
56
+ }
57
+ return null;
58
+ }
59
+ /** A WAV clip's length from its `fmt ` byte rate and `data` size, or undefined. */
60
+ function wavSeconds(buffer) {
61
+ let byteRate;
62
+ let offset = 12;
63
+ while (offset + 8 <= buffer.length) {
64
+ const id = ascii(buffer, offset, offset + 4);
65
+ const size = buffer.readUInt32LE(offset + 4);
66
+ const body = offset + 8;
67
+ if (id === "fmt " && body + 12 <= buffer.length) {
68
+ byteRate = buffer.readUInt32LE(body + 8);
69
+ }
70
+ else if (id === "data") {
71
+ // A streamed WAV leaves the size unset; what follows is the data.
72
+ const dataBytes = size === 0 || size === 0xffffffff
73
+ ? buffer.length - body
74
+ : Math.min(size, buffer.length - body);
75
+ return byteRate ? dataBytes / byteRate : undefined;
76
+ }
77
+ offset = body + size + (size % 2);
78
+ }
79
+ return undefined;
80
+ }
81
+ /** A FLAC clip's length from its STREAMINFO block, or undefined when unset. */
82
+ function flacSeconds(buffer) {
83
+ // "fLaC", a 4-byte block header, then STREAMINFO.
84
+ if (buffer.length < 26 || (buffer[4] & 0x7f) !== 0) {
85
+ return undefined;
86
+ }
87
+ const info = 8;
88
+ const sampleRate = (buffer[info + 10] << 12) |
89
+ (buffer[info + 11] << 4) |
90
+ (buffer[info + 12] >> 4);
91
+ const totalSamples = (buffer[info + 13] & 0x0f) * 2 ** 32 + buffer.readUInt32BE(info + 14);
92
+ return sampleRate > 0 && totalSamples > 0
93
+ ? totalSamples / sampleRate
94
+ : undefined;
95
+ }
96
+ /**
97
+ * An Ogg clip's length: the granule position of its last page over the stream's
98
+ * sample rate. Opus counts at 48 kHz from its pre-skip; Vorbis at the rate in
99
+ * its identification header. Undefined for any other codec, or a stream whose
100
+ * pages cannot be read.
101
+ */
102
+ function oggSeconds(buffer) {
103
+ // The first page carries the codec's identification packet.
104
+ const segments = buffer[26];
105
+ if (buffer.length < 28 || segments === undefined) {
106
+ return undefined;
107
+ }
108
+ const packet = 27 + segments;
109
+ let rate;
110
+ let preSkip = 0;
111
+ if (ascii(buffer, packet, packet + 8) === "OpusHead") {
112
+ rate = 48000;
113
+ preSkip =
114
+ buffer.length >= packet + 12 ? buffer.readUInt16LE(packet + 10) : 0;
115
+ }
116
+ else if (ascii(buffer, packet, packet + 7) === "\x01vorbis") {
117
+ rate =
118
+ buffer.length >= packet + 16
119
+ ? buffer.readUInt32LE(packet + 12)
120
+ : undefined;
121
+ }
122
+ const last = buffer.lastIndexOf("OggS");
123
+ if (!rate ||
124
+ last < 0 ||
125
+ last + 14 > buffer.length ||
126
+ buffer[last + 4] !== 0) {
127
+ return undefined;
128
+ }
129
+ const granule = buffer.readBigInt64LE(last + 6);
130
+ if (granule <= 0n) {
131
+ return undefined;
132
+ }
133
+ return Math.max(0, Number(granule) - preSkip) / rate;
134
+ }
135
+ /**
136
+ * The clip in wire form, or why it cannot be sent. A clip whose length can be
137
+ * read (WAV, FLAC, Ogg Opus or Vorbis) and that runs past what the model hears is
138
+ * refused; the model would answer from its start alone. An MP3's length is not
139
+ * read: it needs every frame header walked.
140
+ */
141
+ function encodeAudio(buffer, limits, label) {
142
+ const format = sniffAudioFormat(buffer);
143
+ if (format === null) {
144
+ return { status: "error", message: `${label} ${NOT_RECOGNISED.audio}.` };
145
+ }
146
+ if (format === "aac") {
147
+ return {
148
+ status: "error",
149
+ message: `${label} is AAC or M4A audio, which the model does not decode. Convert it to WAV, FLAC, Ogg or MP3.`,
150
+ };
151
+ }
152
+ const seconds = format === "wav"
153
+ ? wavSeconds(buffer)
154
+ : format === "flac"
155
+ ? flacSeconds(buffer)
156
+ : format === "ogg"
157
+ ? oggSeconds(buffer)
158
+ : undefined;
159
+ // Every WAV the model can open has both chunks; the server answers one
160
+ // without them with an engine error, which reads as a fault worth retrying.
161
+ if (format === "wav" && seconds === undefined) {
162
+ return {
163
+ status: "error",
164
+ message: `${label} is a WAV file without a complete fmt and data chunk.`,
165
+ };
166
+ }
167
+ if (seconds !== undefined && seconds > limits.maxSeconds) {
168
+ return {
169
+ status: "error",
170
+ message: `${label} runs ${seconds.toFixed(1)} seconds; the model hears at most ${limits.maxSeconds}. Trim it, or split it across requests.`,
171
+ };
172
+ }
173
+ return {
174
+ status: "ok",
175
+ audio: { data: buffer.toString("base64"), format },
176
+ };
177
+ }
29
178
  function encode(buffer, kind, label) {
30
179
  const mime = kind === "image" ? sniffImageMimeType(buffer) : sniffVideoMimeType(buffer);
31
180
  return mime === null
@@ -73,14 +222,15 @@ async function readMediaFile(path, label, maxBytes) {
73
222
  }
74
223
  }
75
224
  /**
76
- * Nothing here echoes a `data:` URL or a remote URL: either can carry a
225
+ * A source resolved for one kind of media, with every refusal the three kinds
226
+ * share. Nothing here echoes a `data:` URL or a remote URL: either can carry a
77
227
  * credential or megabytes of base64, and neither belongs in an error message.
78
228
  */
79
- async function toDataUrl(source, kind, label, maxBytes) {
229
+ async function readMediaSource(source, kind, label, maxBytes) {
80
230
  if (typeof source !== "string") {
81
231
  return source.length === 0
82
232
  ? { status: "error", message: `${label} is an empty Buffer.` }
83
- : encode(source, kind, label);
233
+ : { status: "buffer", buffer: source };
84
234
  }
85
235
  const value = source.trim();
86
236
  if (value === "") {
@@ -94,7 +244,7 @@ async function toDataUrl(source, kind, label, maxBytes) {
94
244
  }
95
245
  if (/^data:/i.test(value)) {
96
246
  return DATA_URL[kind].test(value)
97
- ? { status: "ok", dataUrl: value }
247
+ ? { status: "dataUrl", dataUrl: value }
98
248
  : {
99
249
  status: "error",
100
250
  message: `${label} is a data: URL that is not base64 ${kind} content.`,
@@ -107,24 +257,59 @@ async function toDataUrl(source, kind, label, maxBytes) {
107
257
  };
108
258
  }
109
259
  const file = await readMediaFile(value, label, maxBytes);
110
- return file.status === "ok" ? encode(file.buffer, kind, label) : file;
260
+ return file.status === "ok"
261
+ ? { status: "buffer", buffer: file.buffer }
262
+ : file;
263
+ }
264
+ /** An image or video as a `data:` URL: bytes are identified first, a URL of the right kind is sent as given. */
265
+ async function toDataUrl(source, kind, label, maxBytes) {
266
+ const read = await readMediaSource(source, kind, label, maxBytes);
267
+ if (read.status === "buffer") {
268
+ return encode(read.buffer, kind, label);
269
+ }
270
+ return read.status === "dataUrl"
271
+ ? { status: "ok", dataUrl: read.dataUrl }
272
+ : read;
273
+ }
274
+ /** An audio clip as bytes: a `data:audio/…` URL is decoded, since the wire takes raw base64 and the container is sniffed. */
275
+ async function readAudioSource(source, label, maxBytes) {
276
+ const read = await readMediaSource(source, "audio", label, maxBytes);
277
+ if (read.status === "dataUrl") {
278
+ const base64 = read.dataUrl.slice(read.dataUrl.indexOf(",") + 1);
279
+ return { status: "ok", buffer: Buffer.from(base64, "base64") };
280
+ }
281
+ return read.status === "buffer"
282
+ ? { status: "ok", buffer: read.buffer }
283
+ : read;
111
284
  }
112
285
  /**
113
- * Turn the caller's images and video into `data:` URLs, or say why not.
114
- * Runs before any request, so every refusal costs nothing and none is retried.
286
+ * Turn the caller's images and video into `data:` URLs, and an audio clip into
287
+ * `{ data, format }`, or say why not. Runs before any request, so every
288
+ * refusal costs nothing and none is retried.
115
289
  */
116
290
  export async function prepareDecisionMedia(request, limits, vendor, alternatives) {
117
291
  const images = request.images ?? [];
118
- if (images.length === 0 && request.video === undefined) {
292
+ if (images.length === 0 &&
293
+ request.video === undefined &&
294
+ request.audio === undefined) {
119
295
  return { status: "prepared", media: undefined };
120
296
  }
297
+ const use = alternatives.length > 0
298
+ ? ` Use a decision provider that does: ${alternatives.join(", ")}.`
299
+ : "";
121
300
  if (!limits) {
122
- const use = alternatives.length > 0
123
- ? ` Use a decision provider that does: ${alternatives.join(", ")}.`
124
- : "";
301
+ const what = images.length === 0 && request.video === undefined
302
+ ? "audio"
303
+ : "images or video";
125
304
  return {
126
305
  status: "refused",
127
- message: `${vendor} does not accept images or video.${use}`,
306
+ message: `${vendor} does not accept ${what}.${use}`,
307
+ };
308
+ }
309
+ if (request.audio !== undefined && limits.audio === undefined) {
310
+ return {
311
+ status: "refused",
312
+ message: `${vendor} does not accept audio.${use}`,
128
313
  };
129
314
  }
130
315
  if (images.length > limits.maxImages) {
@@ -152,9 +337,26 @@ export async function prepareDecisionMedia(request, limits, vendor, alternatives
152
337
  }
153
338
  video = converted.dataUrl;
154
339
  }
155
- const bytes = prepared.reduce((sum, url) => sum + url.length, video?.length ?? 0);
340
+ let audio;
341
+ if (request.audio !== undefined && limits.audio !== undefined) {
342
+ const read = await readAudioSource(request.audio, "The audio", limits.maxRequestBytes);
343
+ if (read.status === "error") {
344
+ return { status: "refused", message: read.message };
345
+ }
346
+ const encoded = encodeAudio(read.buffer, limits.audio, "The audio");
347
+ if (encoded.status === "error") {
348
+ return { status: "refused", message: encoded.message };
349
+ }
350
+ audio = encoded.audio;
351
+ }
352
+ const bytes = prepared.reduce((sum, url) => sum + url.length, (video?.length ?? 0) + (audio?.data.length ?? 0));
156
353
  return {
157
354
  status: "prepared",
158
- media: { images: prepared, ...(video ? { video } : {}), bytes },
355
+ media: {
356
+ images: prepared,
357
+ ...(video ? { video } : {}),
358
+ ...(audio ? { audio } : {}),
359
+ bytes,
360
+ },
159
361
  };
160
362
  }