@ai-sdk/quiverai 2.0.43 → 2.0.45

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.
@@ -7,6 +7,7 @@ import {
7
7
  } from '@ai-sdk/provider';
8
8
  import {
9
9
  combineHeaders,
10
+ convertBase64ToUint8Array,
10
11
  convertUint8ArrayToBase64,
11
12
  createJsonErrorResponseHandler,
12
13
  createJsonResponseHandler,
@@ -22,6 +23,10 @@ import {
22
23
  quiveraiImageModelOptionsSchema,
23
24
  type QuiverAIImageModelOptions,
24
25
  } from './quiverai-image-model-options';
26
+ import {
27
+ validateQuiverAIImageUrl,
28
+ validateQuiverAIReferenceBase64,
29
+ } from './prepare-quiverai-image-reference';
25
30
  import type {
26
31
  QuiverAIImageModelId,
27
32
  QuiverAIOperation,
@@ -94,6 +99,7 @@ export class QuiverAIImageModel implements ImageModelV4 {
94
99
  n,
95
100
  prompt,
96
101
  files,
102
+ mask,
97
103
  operation,
98
104
  options: quiveraiOptions ?? {},
99
105
  });
@@ -125,6 +131,12 @@ export class QuiverAIImageModel implements ImageModelV4 {
125
131
  images: response.data.map((image, index) => ({
126
132
  index,
127
133
  mimeType: image.mime_type,
134
+ ...(image.loop_period_ms !== undefined && {
135
+ loopPeriodMs: image.loop_period_ms,
136
+ }),
137
+ ...(image.opening_animation_ms !== undefined && {
138
+ openingAnimationMs: image.opening_animation_ms,
139
+ }),
128
140
  })),
129
141
  },
130
142
  },
@@ -147,9 +159,16 @@ export class QuiverAIImageModel implements ImageModelV4 {
147
159
  }
148
160
 
149
161
  function getOperationPath(operation: QuiverAIOperation) {
150
- return operation === 'generate'
151
- ? '/svgs/generations'
152
- : '/svgs/vectorizations';
162
+ switch (operation) {
163
+ case 'generate':
164
+ return '/svgs/generations';
165
+ case 'vectorize':
166
+ return '/svgs/vectorizations';
167
+ case 'edit':
168
+ return '/svgs/edits';
169
+ case 'animate':
170
+ return '/svgs/animations';
171
+ }
153
172
  }
154
173
 
155
174
  function getGenerateReferenceLimit(modelId: string) {
@@ -170,11 +189,86 @@ function toQuiverAIImageReference(image: ImageModelV4File) {
170
189
  };
171
190
  }
172
191
 
192
+ const maxAnimationSourceBase64Length = 1_066_668;
193
+
194
+ function toQuiverAIAnimationSource(image: ImageModelV4File) {
195
+ if (image.type === 'url') {
196
+ let url: URL;
197
+ try {
198
+ url = new URL(image.url);
199
+ } catch {
200
+ throw new InvalidArgumentError({
201
+ argument: 'files',
202
+ message: 'QuiverAI animate requires a valid HTTP or HTTPS SVG URL.',
203
+ });
204
+ }
205
+
206
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
207
+ throw new InvalidArgumentError({
208
+ argument: 'files',
209
+ message: 'QuiverAI animate requires an HTTP or HTTPS SVG URL.',
210
+ });
211
+ }
212
+
213
+ return { url: image.url };
214
+ }
215
+
216
+ let base64: string;
217
+ let bytes: Uint8Array;
218
+
219
+ if (typeof image.data === 'string') {
220
+ const dataUrlMatch =
221
+ /^data:image\/svg\+xml(?:;[^,]*)?;base64,([\s\S]+)$/i.exec(image.data);
222
+ const encodedData = dataUrlMatch?.[1] ?? image.data;
223
+
224
+ try {
225
+ bytes = convertBase64ToUint8Array(encodedData);
226
+ } catch {
227
+ throw new InvalidArgumentError({
228
+ argument: 'files',
229
+ message:
230
+ 'QuiverAI animate requires the source SVG string to be valid base64 or an SVG data URL.',
231
+ });
232
+ }
233
+ base64 = convertUint8ArrayToBase64(bytes);
234
+ } else {
235
+ bytes = image.data;
236
+ base64 = convertUint8ArrayToBase64(bytes);
237
+ }
238
+
239
+ if (!isSvg(bytes)) {
240
+ throw new InvalidArgumentError({
241
+ argument: 'files',
242
+ message: 'QuiverAI animate requires the input file to contain SVG data.',
243
+ });
244
+ }
245
+
246
+ if (base64.length > maxAnimationSourceBase64Length) {
247
+ throw new InvalidArgumentError({
248
+ argument: 'files',
249
+ message: `QuiverAI animate accepts at most ${maxAnimationSourceBase64Length} base64 characters for the source SVG.`,
250
+ });
251
+ }
252
+
253
+ return { base64 };
254
+ }
255
+
256
+ function isSvg(data: Uint8Array): boolean {
257
+ const head = new TextDecoder('utf-8', { fatal: false })
258
+ .decode(data.subarray(0, 4096))
259
+ .trimStart();
260
+
261
+ return /^(?:(?:<\?xml[\s\S]*?\?>|<!--[\s\S]*?-->|<!DOCTYPE[\s\S]*?>)\s*)*<svg(?:\s|>)/i.test(
262
+ head,
263
+ );
264
+ }
265
+
173
266
  function buildRequestBody({
174
267
  modelId,
175
268
  n,
176
269
  prompt,
177
270
  files,
271
+ mask,
178
272
  operation,
179
273
  options,
180
274
  }: {
@@ -182,6 +276,7 @@ function buildRequestBody({
182
276
  n: number;
183
277
  prompt: string | undefined;
184
278
  files: ImageModelV4File[] | undefined;
279
+ mask: ImageModelV4File | undefined;
185
280
  operation: QuiverAIOperation;
186
281
  options: QuiverAIImageModelOptions;
187
282
  }) {
@@ -206,6 +301,10 @@ function buildRequestBody({
206
301
  stream: false as const,
207
302
  };
208
303
 
304
+ if (operation !== 'edit') {
305
+ rejectEditOnlyOptions(operation, options);
306
+ }
307
+
209
308
  if (operation === 'generate') {
210
309
  if (prompt == null || prompt.trim().length === 0) {
211
310
  throw new InvalidArgumentError({
@@ -235,6 +334,28 @@ function buildRequestBody({
235
334
  };
236
335
  }
237
336
 
337
+ if (operation === 'edit') {
338
+ return buildEditRequestBody({
339
+ modelId,
340
+ n,
341
+ prompt,
342
+ files,
343
+ mask,
344
+ options,
345
+ });
346
+ }
347
+
348
+ if (operation === 'animate') {
349
+ return buildAnimationRequestBody({
350
+ modelId,
351
+ n,
352
+ prompt,
353
+ files,
354
+ mask,
355
+ options,
356
+ });
357
+ }
358
+
238
359
  if (files == null || files.length === 0) {
239
360
  throw new InvalidArgumentError({
240
361
  argument: 'files',
@@ -267,6 +388,564 @@ function buildRequestBody({
267
388
  };
268
389
  }
269
390
 
391
+ function buildAnimationRequestBody({
392
+ modelId,
393
+ n,
394
+ prompt,
395
+ files,
396
+ mask,
397
+ options,
398
+ }: {
399
+ modelId: string;
400
+ n: number;
401
+ prompt: string | undefined;
402
+ files: ImageModelV4File[] | undefined;
403
+ mask: ImageModelV4File | undefined;
404
+ options: QuiverAIImageModelOptions;
405
+ }) {
406
+ if (modelId !== 'arrow-2' && modelId !== 'arrow-2-telos') {
407
+ throw new InvalidArgumentError({
408
+ argument: 'modelId',
409
+ message:
410
+ 'QuiverAI animate is supported by the "arrow-2" and "arrow-2-telos" models.',
411
+ });
412
+ }
413
+
414
+ if (files == null || files.length === 0) {
415
+ throw new InvalidArgumentError({
416
+ argument: 'files',
417
+ message:
418
+ 'QuiverAI animate requires exactly one source SVG in prompt.images.',
419
+ });
420
+ }
421
+
422
+ if (files.length !== 1) {
423
+ throw new InvalidArgumentError({
424
+ argument: 'files',
425
+ message:
426
+ 'QuiverAI animate accepts exactly one source SVG in prompt.images.',
427
+ });
428
+ }
429
+
430
+ if (n !== 1) {
431
+ throw new InvalidArgumentError({
432
+ argument: 'n',
433
+ message:
434
+ 'QuiverAI animate returns one SVG per request. Set maxImagesPerCall to 1 in generateImage to animate multiple times.',
435
+ });
436
+ }
437
+
438
+ if (mask != null) {
439
+ throw new InvalidArgumentError({
440
+ argument: 'mask',
441
+ message: 'QuiverAI animate does not support masks.',
442
+ });
443
+ }
444
+
445
+ if (prompt != null && prompt.trim().length === 0) {
446
+ throw new InvalidArgumentError({
447
+ argument: 'prompt',
448
+ message:
449
+ 'QuiverAI animate requires a non-empty prompt when an animation instruction is provided.',
450
+ });
451
+ }
452
+
453
+ const unsupportedOptions = [
454
+ ['instructions', options.instructions],
455
+ ['topP', options.topP],
456
+ ['presencePenalty', options.presencePenalty],
457
+ ['attributes', options.attributes],
458
+ ['autoCrop', options.autoCrop],
459
+ ['targetSize', options.targetSize],
460
+ ].filter((option): option is [string, NonNullable<unknown>] => {
461
+ return option[1] !== undefined;
462
+ });
463
+
464
+ if (unsupportedOptions.length > 0) {
465
+ throw new InvalidArgumentError({
466
+ argument: `providerOptions.quiverai.${unsupportedOptions[0][0]}`,
467
+ message: `QuiverAI animate does not support providerOptions.quiverai.${unsupportedOptions[0][0]}.`,
468
+ });
469
+ }
470
+
471
+ return {
472
+ model: modelId,
473
+ svg_source: toQuiverAIAnimationSource(files[0]),
474
+ prompt,
475
+ temperature: options.temperature,
476
+ max_output_tokens: options.maxOutputTokens,
477
+ reasoning_effort: options.reasoningEffort,
478
+ stream: false as const,
479
+ };
480
+ }
481
+
482
+ const editModelIds = new Set(['arrow-2', 'arrow-2-telos']);
483
+ const MAX_EDIT_SVG_BYTES = 200_000;
484
+
485
+ function buildEditRequestBody({
486
+ modelId,
487
+ n,
488
+ prompt,
489
+ files,
490
+ mask,
491
+ options,
492
+ }: {
493
+ modelId: string;
494
+ n: number;
495
+ prompt: string | undefined;
496
+ files: ImageModelV4File[] | undefined;
497
+ mask: ImageModelV4File | undefined;
498
+ options: QuiverAIImageModelOptions;
499
+ }) {
500
+ if (!editModelIds.has(modelId)) {
501
+ throw new InvalidArgumentError({
502
+ argument: 'modelId',
503
+ message:
504
+ 'QuiverAI SVG editing is supported by the "arrow-2" and "arrow-2-telos" models.',
505
+ });
506
+ }
507
+
508
+ if (prompt == null || prompt.trim().length === 0) {
509
+ throw new InvalidArgumentError({
510
+ argument: 'prompt',
511
+ message:
512
+ 'QuiverAI SVG editing requires a non-empty instruction in generateImage prompt.text.',
513
+ });
514
+ }
515
+
516
+ if (prompt.length > 4000) {
517
+ throw new InvalidArgumentError({
518
+ argument: 'prompt',
519
+ message:
520
+ 'QuiverAI SVG editing instructions must contain at most 4000 characters.',
521
+ });
522
+ }
523
+
524
+ if (files == null || files.length === 0) {
525
+ throw new InvalidArgumentError({
526
+ argument: 'files',
527
+ message:
528
+ 'QuiverAI SVG editing requires one source SVG in generateImage prompt.images.',
529
+ });
530
+ }
531
+
532
+ if (files.length !== 1) {
533
+ throw new InvalidArgumentError({
534
+ argument: 'files',
535
+ message: 'QuiverAI SVG editing accepts exactly one source SVG.',
536
+ });
537
+ }
538
+
539
+ if (n !== 1) {
540
+ throw new InvalidArgumentError({
541
+ argument: 'n',
542
+ message:
543
+ 'QuiverAI SVG editing returns exactly one SVG per request. Set maxImagesPerCall to 1 in generateImage to edit multiple times.',
544
+ });
545
+ }
546
+
547
+ if (mask != null) {
548
+ throw new InvalidArgumentError({
549
+ argument: 'mask',
550
+ message: 'QuiverAI SVG editing does not support masks.',
551
+ });
552
+ }
553
+
554
+ const unsupportedOptions = [
555
+ ['instructions', options.instructions],
556
+ ['attributes', options.attributes],
557
+ ['topP', options.topP],
558
+ ['presencePenalty', options.presencePenalty],
559
+ ['autoCrop', options.autoCrop],
560
+ ['targetSize', options.targetSize],
561
+ ].flatMap(([name, value]) => (value == null ? [] : [name]));
562
+
563
+ if (unsupportedOptions.length > 0) {
564
+ throw new InvalidArgumentError({
565
+ argument: 'providerOptions',
566
+ message: `QuiverAI SVG editing does not support these provider options: ${unsupportedOptions.join(
567
+ ', ',
568
+ )}.`,
569
+ });
570
+ }
571
+
572
+ const referenceImages = options.referenceImages?.map((reference, index) => {
573
+ if ('url' in reference) {
574
+ return {
575
+ url: validateQuiverAIImageUrl(reference.url),
576
+ };
577
+ }
578
+
579
+ validateQuiverAIReferenceBase64(
580
+ reference.base64,
581
+ `providerOptions.quiverai.referenceImages[${index}]`,
582
+ );
583
+ return { base64: reference.base64 };
584
+ });
585
+ const settings = {
586
+ max_output_tokens: options.maxOutputTokens,
587
+ orchestrator_max_output_tokens: options.orchestratorMaxOutputTokens,
588
+ shallow_max_output_tokens: options.shallowMaxOutputTokens,
589
+ temperature: options.temperature,
590
+ };
591
+ const hasSettings = Object.values(settings).some(value => value != null);
592
+
593
+ return {
594
+ model: modelId,
595
+ prompt,
596
+ ...toQuiverAIEditSource(files[0]),
597
+ reference_images: referenceImages,
598
+ max_review_steps: options.maxReviewSteps,
599
+ reasoning_effort: options.reasoningEffort,
600
+ ...(hasSettings && { settings }),
601
+ stream: false as const,
602
+ };
603
+ }
604
+
605
+ function rejectEditOnlyOptions(
606
+ operation: Exclude<QuiverAIOperation, 'edit'>,
607
+ options: QuiverAIImageModelOptions,
608
+ ) {
609
+ const editOnlyOptions = [
610
+ ['referenceImages', options.referenceImages],
611
+ ['maxReviewSteps', options.maxReviewSteps],
612
+ ['orchestratorMaxOutputTokens', options.orchestratorMaxOutputTokens],
613
+ ['shallowMaxOutputTokens', options.shallowMaxOutputTokens],
614
+ ].flatMap(([name, value]) => (value == null ? [] : [name]));
615
+
616
+ if (editOnlyOptions.length > 0) {
617
+ throw new InvalidArgumentError({
618
+ argument: 'providerOptions',
619
+ message: `QuiverAI ${operation} does not support these edit-only provider options: ${editOnlyOptions.join(
620
+ ', ',
621
+ )}.`,
622
+ });
623
+ }
624
+ }
625
+
626
+ function toQuiverAIEditSource(file: ImageModelV4File) {
627
+ if (file.type === 'url') {
628
+ return {
629
+ svg_source: {
630
+ url: validateQuiverAIImageUrl(file.url),
631
+ },
632
+ };
633
+ }
634
+
635
+ let data: Uint8Array;
636
+ try {
637
+ data =
638
+ typeof file.data === 'string'
639
+ ? convertBase64ToUint8Array(file.data)
640
+ : file.data;
641
+ } catch (cause) {
642
+ throw new InvalidArgumentError({
643
+ argument: 'files',
644
+ message: 'QuiverAI SVG source data must be valid base64 or binary data.',
645
+ cause,
646
+ });
647
+ }
648
+
649
+ if (data.length === 0 || data.length > MAX_EDIT_SVG_BYTES) {
650
+ throw new InvalidArgumentError({
651
+ argument: 'files',
652
+ message: `QuiverAI SVG source data must contain 1-${MAX_EDIT_SVG_BYTES} bytes.`,
653
+ });
654
+ }
655
+
656
+ let svg: string;
657
+ try {
658
+ svg = new TextDecoder('utf-8', { fatal: true }).decode(data);
659
+ } catch (cause) {
660
+ throw new InvalidArgumentError({
661
+ argument: 'files',
662
+ message: 'QuiverAI SVG source data must be valid UTF-8.',
663
+ cause,
664
+ });
665
+ }
666
+
667
+ if (svg.length > MAX_EDIT_SVG_BYTES || !isSvgMarkup(svg)) {
668
+ throw new InvalidArgumentError({
669
+ argument: 'files',
670
+ message: 'QuiverAI SVG source data must contain a complete SVG document.',
671
+ });
672
+ }
673
+
674
+ return {
675
+ svg_source: {
676
+ base64: convertUint8ArrayToBase64(data),
677
+ },
678
+ };
679
+ }
680
+
681
+ function isSvgMarkup(svg: string) {
682
+ const document = svg.replace(/^\uFEFF/, '');
683
+ const elements: string[] = [];
684
+ let position = 0;
685
+ let rootSeen = false;
686
+ let rootClosed = false;
687
+ let doctypeSeen = false;
688
+
689
+ while (position < document.length) {
690
+ if (document[position] !== '<') {
691
+ const nextTag = document.indexOf('<', position);
692
+ const end = nextTag === -1 ? document.length : nextTag;
693
+ const text = document.slice(position, end);
694
+
695
+ if (
696
+ (elements.length === 0 && text.trim().length > 0) ||
697
+ text.includes(']]>') ||
698
+ !hasValidXmlReferences(text)
699
+ ) {
700
+ return false;
701
+ }
702
+
703
+ position = end;
704
+ continue;
705
+ }
706
+
707
+ if (document.startsWith('<!--', position)) {
708
+ const commentEnd = document.indexOf('-->', position + 4);
709
+ if (
710
+ commentEnd === -1 ||
711
+ document.slice(position + 4, commentEnd).includes('--')
712
+ ) {
713
+ return false;
714
+ }
715
+ position = commentEnd + 3;
716
+ continue;
717
+ }
718
+
719
+ if (document.startsWith('<?', position)) {
720
+ const instructionEnd = document.indexOf('?>', position + 2);
721
+ if (instructionEnd === -1) {
722
+ return false;
723
+ }
724
+ position = instructionEnd + 2;
725
+ continue;
726
+ }
727
+
728
+ if (document.startsWith('<![CDATA[', position)) {
729
+ if (elements.length === 0) {
730
+ return false;
731
+ }
732
+ const cdataEnd = document.indexOf(']]>', position + 9);
733
+ if (cdataEnd === -1) {
734
+ return false;
735
+ }
736
+ position = cdataEnd + 3;
737
+ continue;
738
+ }
739
+
740
+ if (document.slice(position, position + 9).toUpperCase() === '<!DOCTYPE') {
741
+ if (
742
+ rootSeen ||
743
+ doctypeSeen ||
744
+ elements.length > 0 ||
745
+ !/[\t\n\r ]/.test(document[position + 9] ?? '')
746
+ ) {
747
+ return false;
748
+ }
749
+ const doctypeName = readXmlName(
750
+ document,
751
+ skipXmlWhitespace(document, position + 9),
752
+ );
753
+ if (doctypeName?.name.toLowerCase() !== 'svg') {
754
+ return false;
755
+ }
756
+ const doctypeEnd = findDoctypeEnd(document, doctypeName.end);
757
+ if (doctypeEnd === -1) {
758
+ return false;
759
+ }
760
+ doctypeSeen = true;
761
+ position = doctypeEnd;
762
+ continue;
763
+ }
764
+
765
+ if (document.startsWith('<!', position)) {
766
+ return false;
767
+ }
768
+
769
+ if (document.startsWith('</', position)) {
770
+ const closingTag = readXmlName(document, position + 2);
771
+ if (closingTag == null) {
772
+ return false;
773
+ }
774
+ let tagEnd = skipXmlWhitespace(document, closingTag.end);
775
+ if (document[tagEnd] !== '>') {
776
+ return false;
777
+ }
778
+ const expectedTag = elements.pop();
779
+ if (expectedTag !== closingTag.name) {
780
+ return false;
781
+ }
782
+ tagEnd += 1;
783
+ if (elements.length === 0) {
784
+ rootClosed = true;
785
+ }
786
+ position = tagEnd;
787
+ continue;
788
+ }
789
+
790
+ if (rootClosed) {
791
+ return false;
792
+ }
793
+
794
+ const openingTag = readXmlName(document, position + 1);
795
+ if (openingTag == null) {
796
+ return false;
797
+ }
798
+ if (!rootSeen) {
799
+ if (openingTag.name.toLowerCase() !== 'svg') {
800
+ return false;
801
+ }
802
+ rootSeen = true;
803
+ }
804
+
805
+ const attributes = new Set<string>();
806
+ let tagPosition = openingTag.end;
807
+ while (tagPosition < document.length) {
808
+ const beforeWhitespace = tagPosition;
809
+ tagPosition = skipXmlWhitespace(document, tagPosition);
810
+
811
+ if (document.startsWith('/>', tagPosition)) {
812
+ tagPosition += 2;
813
+ if (elements.length === 0) {
814
+ rootClosed = true;
815
+ }
816
+ position = tagPosition;
817
+ break;
818
+ }
819
+
820
+ if (document[tagPosition] === '>') {
821
+ elements.push(openingTag.name);
822
+ position = tagPosition + 1;
823
+ break;
824
+ }
825
+
826
+ if (tagPosition === beforeWhitespace) {
827
+ return false;
828
+ }
829
+
830
+ const attribute = readXmlName(document, tagPosition);
831
+ if (attribute == null || attributes.has(attribute.name)) {
832
+ return false;
833
+ }
834
+ attributes.add(attribute.name);
835
+
836
+ tagPosition = skipXmlWhitespace(document, attribute.end);
837
+ if (document[tagPosition] !== '=') {
838
+ return false;
839
+ }
840
+ tagPosition = skipXmlWhitespace(document, tagPosition + 1);
841
+
842
+ const quote = document[tagPosition];
843
+ if (quote !== '"' && quote !== "'") {
844
+ return false;
845
+ }
846
+ const valueEnd = document.indexOf(quote, tagPosition + 1);
847
+ if (
848
+ valueEnd === -1 ||
849
+ document.slice(tagPosition + 1, valueEnd).includes('<') ||
850
+ !hasValidXmlReferences(document.slice(tagPosition + 1, valueEnd))
851
+ ) {
852
+ return false;
853
+ }
854
+ tagPosition = valueEnd + 1;
855
+ }
856
+
857
+ if (tagPosition >= document.length && position !== document.length) {
858
+ return false;
859
+ }
860
+ }
861
+
862
+ return rootSeen && rootClosed && elements.length === 0;
863
+ }
864
+
865
+ function isXmlNameStart(character: string | undefined) {
866
+ return character != null && /[A-Z_a-z:\u0080-\uFFFF]/.test(character);
867
+ }
868
+
869
+ function isXmlNameCharacter(character: string | undefined) {
870
+ return (
871
+ character != null && /[-.0-9A-Z_a-z:\u00B7\u0080-\uFFFF]/.test(character)
872
+ );
873
+ }
874
+
875
+ function readXmlName(value: string, position: number) {
876
+ if (!isXmlNameStart(value[position])) {
877
+ return undefined;
878
+ }
879
+
880
+ const start = position;
881
+ position += 1;
882
+ while (isXmlNameCharacter(value[position])) {
883
+ position += 1;
884
+ }
885
+
886
+ return {
887
+ name: value.slice(start, position),
888
+ end: position,
889
+ };
890
+ }
891
+
892
+ function skipXmlWhitespace(value: string, position: number) {
893
+ while (/[\t\n\r ]/.test(value[position] ?? '')) {
894
+ position += 1;
895
+ }
896
+ return position;
897
+ }
898
+
899
+ function hasValidXmlReferences(value: string) {
900
+ let position = value.indexOf('&');
901
+ while (position !== -1) {
902
+ const end = value.indexOf(';', position + 1);
903
+ if (end === -1) {
904
+ return false;
905
+ }
906
+ const reference = value.slice(position + 1, end);
907
+ if (
908
+ !/^#\d+$/.test(reference) &&
909
+ !/^#x[\dA-Fa-f]+$/.test(reference) &&
910
+ !/^[A-Z_a-z:\u0080-\uFFFF][-.0-9A-Z_a-z:\u00B7\u0080-\uFFFF]*$/.test(
911
+ reference,
912
+ )
913
+ ) {
914
+ return false;
915
+ }
916
+ position = value.indexOf('&', end + 1);
917
+ }
918
+ return true;
919
+ }
920
+
921
+ function findDoctypeEnd(value: string, position: number) {
922
+ let subsetDepth = 0;
923
+ let quote: '"' | "'" | undefined;
924
+
925
+ while (position < value.length) {
926
+ const character = value[position];
927
+ if (quote != null) {
928
+ if (character === quote) {
929
+ quote = undefined;
930
+ }
931
+ } else if (character === '"' || character === "'") {
932
+ quote = character;
933
+ } else if (character === '[') {
934
+ subsetDepth += 1;
935
+ } else if (character === ']') {
936
+ if (subsetDepth === 0) {
937
+ return -1;
938
+ }
939
+ subsetDepth -= 1;
940
+ } else if (character === '>' && subsetDepth === 0) {
941
+ return position + 1;
942
+ }
943
+ position += 1;
944
+ }
945
+
946
+ return -1;
947
+ }
948
+
270
949
  function collectWarnings({
271
950
  size,
272
951
  aspectRatio,
@@ -331,6 +1010,8 @@ const svgUsageSchema = z.object({
331
1010
  const svgDocumentSchema = z.object({
332
1011
  svg: z.string().min(1),
333
1012
  mime_type: z.literal('image/svg+xml'),
1013
+ loop_period_ms: z.number().int().nonnegative().nullish(),
1014
+ opening_animation_ms: z.number().int().nonnegative().nullish(),
334
1015
  });
335
1016
 
336
1017
  const svgGenerationResponseSchema = z.object({