@ai-sdk/quiverai 2.0.44 → 2.0.46

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.
@@ -23,6 +23,10 @@ import {
23
23
  quiveraiImageModelOptionsSchema,
24
24
  type QuiverAIImageModelOptions,
25
25
  } from './quiverai-image-model-options';
26
+ import {
27
+ validateQuiverAIImageUrl,
28
+ validateQuiverAIReferenceBase64,
29
+ } from './prepare-quiverai-image-reference';
26
30
  import type {
27
31
  QuiverAIImageModelId,
28
32
  QuiverAIOperation,
@@ -160,6 +164,8 @@ function getOperationPath(operation: QuiverAIOperation) {
160
164
  return '/svgs/generations';
161
165
  case 'vectorize':
162
166
  return '/svgs/vectorizations';
167
+ case 'edit':
168
+ return '/svgs/edits';
163
169
  case 'animate':
164
170
  return '/svgs/animations';
165
171
  }
@@ -295,6 +301,10 @@ function buildRequestBody({
295
301
  stream: false as const,
296
302
  };
297
303
 
304
+ if (operation !== 'edit') {
305
+ rejectEditOnlyOptions(operation, options);
306
+ }
307
+
298
308
  if (operation === 'generate') {
299
309
  if (prompt == null || prompt.trim().length === 0) {
300
310
  throw new InvalidArgumentError({
@@ -324,6 +334,17 @@ function buildRequestBody({
324
334
  };
325
335
  }
326
336
 
337
+ if (operation === 'edit') {
338
+ return buildEditRequestBody({
339
+ modelId,
340
+ n,
341
+ prompt,
342
+ files,
343
+ mask,
344
+ options,
345
+ });
346
+ }
347
+
327
348
  if (operation === 'animate') {
328
349
  return buildAnimationRequestBody({
329
350
  modelId,
@@ -458,6 +479,473 @@ function buildAnimationRequestBody({
458
479
  };
459
480
  }
460
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
+
461
949
  function collectWarnings({
462
950
  size,
463
951
  aspectRatio,
@@ -16,5 +16,6 @@ export type QuiverAIImageModelId =
16
16
  * - `generate`: Text-to-SVG generation (default).
17
17
  * - `vectorize`: Convert a raster image into an SVG.
18
18
  * - `animate`: Animate a single source SVG.
19
+ * - `edit`: Edit a single source SVG with a text instruction.
19
20
  */
20
- export type QuiverAIOperation = 'generate' | 'vectorize' | 'animate';
21
+ export type QuiverAIOperation = 'generate' | 'vectorize' | 'animate' | 'edit';