@ai-sdk/openai-compatible 2.0.79 → 2.0.81

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.
@@ -13,6 +13,7 @@ import {
13
13
  type SharedV3Warning,
14
14
  } from '@ai-sdk/provider';
15
15
  import {
16
+ StreamingToolCallTracker,
16
17
  combineHeaders,
17
18
  createEventSourceResponseHandler,
18
19
  createJsonErrorResponseHandler,
@@ -23,6 +24,7 @@ import {
23
24
  type FetchFunction,
24
25
  type ParseResult,
25
26
  type ResponseHandler,
27
+ type StreamingToolCallDelta,
26
28
  } from '@ai-sdk/provider-utils';
27
29
  import { z } from 'zod/v4';
28
30
  import { resolveProviderOptionsKey, toCamelCase } from '../utils/to-camel-case';
@@ -41,6 +43,20 @@ import {
41
43
  import type { MetadataExtractor } from './openai-compatible-metadata-extractor';
42
44
  import { prepareTools } from './openai-compatible-prepare-tools';
43
45
 
46
+ type OpenAICompatibleStreamingToolCallDelta = StreamingToolCallDelta & {
47
+ extra_content?: {
48
+ google?: {
49
+ thought_signature?: string | null;
50
+ } | null;
51
+ } | null;
52
+ };
53
+
54
+ type PendingToolCall = {
55
+ id: string | null;
56
+ bufferedArguments: string;
57
+ extraContent: OpenAICompatibleStreamingToolCallDelta['extra_content'];
58
+ };
59
+
44
60
  export type OpenAICompatibleChatConfig = {
45
61
  provider: string;
46
62
  headers: () => Record<string, string | undefined>;
@@ -410,34 +426,70 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV3 {
410
426
  fetch: this.config.fetch,
411
427
  });
412
428
 
413
- const toolCalls: Array<{
414
- id: string;
415
- type: 'function';
416
- function: {
417
- name: string;
418
- arguments: string;
419
- };
420
- hasFinished: boolean;
421
- thoughtSignature?: string;
422
- }> = [];
429
+ const providerOptionsName = metadataKey;
430
+ let toolCallTracker: StreamingToolCallTracker<OpenAICompatibleStreamingToolCallDelta>;
423
431
 
424
432
  // Buffers tool-call deltas by `index` until `function.name` is known.
425
433
  // Some OpenAI-compatible providers send the first delta without
426
- // `function.name`.
427
- const pendingToolCalls = new Map<
428
- number,
429
- {
430
- id: string | null;
431
- bufferedArguments: string;
432
- thoughtSignature: string | undefined;
434
+ // `function.name`, which the shared tracker rejects on first chunk.
435
+ const pendingToolCalls = new Map<number, PendingToolCall>();
436
+ const forwardedToolCallIndices = new Set<number>();
437
+
438
+ const processToolCallDelta = (
439
+ toolCallDelta: OpenAICompatibleStreamingToolCallDelta,
440
+ ) => {
441
+ const index = toolCallDelta.index;
442
+
443
+ if (index == null || forwardedToolCallIndices.has(index)) {
444
+ toolCallTracker.processDelta(toolCallDelta);
445
+ return;
446
+ }
447
+
448
+ let pending = pendingToolCalls.get(index);
449
+ if (pending == null) {
450
+ pending = {
451
+ id: toolCallDelta.id ?? null,
452
+ bufferedArguments: '',
453
+ extraContent: toolCallDelta.extra_content ?? null,
454
+ };
455
+ pendingToolCalls.set(index, pending);
456
+ } else {
457
+ if (pending.id == null && toolCallDelta.id != null) {
458
+ pending.id = toolCallDelta.id;
459
+ }
460
+ if (
461
+ pending.extraContent == null &&
462
+ toolCallDelta.extra_content != null
463
+ ) {
464
+ pending.extraContent = toolCallDelta.extra_content;
465
+ }
466
+ }
467
+
468
+ const argumentsDelta = toolCallDelta.function?.arguments;
469
+ if (argumentsDelta != null) {
470
+ pending.bufferedArguments += argumentsDelta;
433
471
  }
434
- >();
472
+
473
+ const name = toolCallDelta.function?.name;
474
+ if (name != null) {
475
+ toolCallTracker.processDelta({
476
+ index,
477
+ id: pending.id,
478
+ function: {
479
+ name,
480
+ arguments: pending.bufferedArguments,
481
+ },
482
+ extra_content: pending.extraContent ?? undefined,
483
+ });
484
+ pendingToolCalls.delete(index);
485
+ forwardedToolCallIndices.add(index);
486
+ }
487
+ };
435
488
 
436
489
  let finishReason: LanguageModelV3FinishReason | undefined;
437
490
  let usage: z.infer<typeof openaiCompatibleTokenUsageSchema> | undefined =
438
491
  undefined;
439
492
  let isFirstChunk = true;
440
- const providerOptionsName = metadataKey;
441
493
  let isActiveReasoning = false;
442
494
  let isActiveText = false;
443
495
  const convertUsage = (
@@ -451,6 +503,22 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV3 {
451
503
  LanguageModelV3StreamPart
452
504
  >({
453
505
  start(controller) {
506
+ toolCallTracker =
507
+ new StreamingToolCallTracker<OpenAICompatibleStreamingToolCallDelta>(
508
+ controller,
509
+ {
510
+ generateId,
511
+ extractMetadata: delta => {
512
+ const thoughtSignature =
513
+ delta.extra_content?.google?.thought_signature;
514
+
515
+ return thoughtSignature
516
+ ? { [providerOptionsName]: { thoughtSignature } }
517
+ : undefined;
518
+ },
519
+ buildToolCallProviderMetadata: metadata => metadata,
520
+ },
521
+ );
454
522
  controller.enqueue({ type: 'stream-start', warnings });
455
523
  },
456
524
 
@@ -580,144 +648,7 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV3 {
580
648
  }
581
649
 
582
650
  for (const toolCallDelta of delta.tool_calls) {
583
- const index = toolCallDelta.index ?? toolCalls.length;
584
-
585
- if (toolCalls[index] == null) {
586
- if (toolCallDelta.index != null) {
587
- // Buffer deltas until `function.name` is known. Some
588
- // OpenAI-compatible providers send the first delta
589
- // without `function.name`.
590
- let pending = pendingToolCalls.get(index);
591
- if (pending == null) {
592
- pending = {
593
- id: toolCallDelta.id ?? null,
594
- bufferedArguments: '',
595
- thoughtSignature:
596
- toolCallDelta.extra_content?.google
597
- ?.thought_signature ?? undefined,
598
- };
599
- pendingToolCalls.set(index, pending);
600
- } else {
601
- if (pending.id == null && toolCallDelta.id != null) {
602
- pending.id = toolCallDelta.id;
603
- }
604
- if (
605
- pending.thoughtSignature == null &&
606
- toolCallDelta.extra_content?.google
607
- ?.thought_signature != null
608
- ) {
609
- pending.thoughtSignature =
610
- toolCallDelta.extra_content.google.thought_signature;
611
- }
612
- }
613
-
614
- const argumentsDelta = toolCallDelta.function?.arguments;
615
- if (argumentsDelta != null) {
616
- pending.bufferedArguments += argumentsDelta;
617
- }
618
-
619
- const name = toolCallDelta.function?.name;
620
- if (name == null) {
621
- continue; // wait for the delta that carries the name
622
- }
623
-
624
- pendingToolCalls.delete(index);
625
-
626
- if (pending.id == null) {
627
- throw new InvalidResponseDataError({
628
- data: toolCallDelta,
629
- message: `Expected 'id' to be a string.`,
630
- });
631
- }
632
-
633
- controller.enqueue({
634
- type: 'tool-input-start',
635
- id: pending.id,
636
- toolName: name,
637
- });
638
-
639
- toolCalls[index] = {
640
- id: pending.id,
641
- type: 'function',
642
- function: {
643
- name,
644
- arguments: pending.bufferedArguments,
645
- },
646
- hasFinished: false,
647
- thoughtSignature: pending.thoughtSignature,
648
- };
649
- } else {
650
- if (toolCallDelta.id == null) {
651
- throw new InvalidResponseDataError({
652
- data: toolCallDelta,
653
- message: `Expected 'id' to be a string.`,
654
- });
655
- }
656
-
657
- if (toolCallDelta.function?.name == null) {
658
- throw new InvalidResponseDataError({
659
- data: toolCallDelta,
660
- message: `Expected 'function.name' to be a string.`,
661
- });
662
- }
663
-
664
- controller.enqueue({
665
- type: 'tool-input-start',
666
- id: toolCallDelta.id,
667
- toolName: toolCallDelta.function.name,
668
- });
669
-
670
- toolCalls[index] = {
671
- id: toolCallDelta.id,
672
- type: 'function',
673
- function: {
674
- name: toolCallDelta.function.name,
675
- arguments: toolCallDelta.function.arguments ?? '',
676
- },
677
- hasFinished: false,
678
- thoughtSignature:
679
- toolCallDelta.extra_content?.google
680
- ?.thought_signature ?? undefined,
681
- };
682
- }
683
-
684
- const toolCall = toolCalls[index];
685
-
686
- if (
687
- toolCall.function?.name != null &&
688
- toolCall.function?.arguments != null
689
- ) {
690
- // send delta if the argument text has already started:
691
- if (toolCall.function.arguments.length > 0) {
692
- controller.enqueue({
693
- type: 'tool-input-delta',
694
- id: toolCall.id,
695
- delta: toolCall.function.arguments,
696
- });
697
- }
698
- }
699
-
700
- continue;
701
- }
702
-
703
- // existing tool call, merge if not finished
704
- const toolCall = toolCalls[index];
705
-
706
- if (toolCall.hasFinished) {
707
- continue;
708
- }
709
-
710
- if (toolCallDelta.function?.arguments != null) {
711
- toolCall.function!.arguments +=
712
- toolCallDelta.function?.arguments ?? '';
713
- }
714
-
715
- // send delta
716
- controller.enqueue({
717
- type: 'tool-input-delta',
718
- id: toolCall.id,
719
- delta: toolCallDelta.function.arguments ?? '',
720
- });
651
+ processToolCallDelta(toolCallDelta);
721
652
  }
722
653
  }
723
654
  },
@@ -731,44 +662,18 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV3 {
731
662
  controller.enqueue({ type: 'text-end', id: 'txt-0' });
732
663
  }
733
664
 
734
- // Tool-call deltas that never received a `function.name` are
735
- // invalid, preserving the original invalid-response semantics.
665
+ // Forward pending deltas so the tracker preserves the existing
666
+ // invalid-response behavior for calls that never receive a name.
736
667
  for (const [index, pending] of pendingToolCalls) {
737
- throw new InvalidResponseDataError({
738
- data: {
739
- index,
740
- id: pending.id,
741
- function: { arguments: pending.bufferedArguments },
742
- },
743
- message: `Expected 'function.name' to be a string.`,
668
+ toolCallTracker.processDelta({
669
+ index,
670
+ id: pending.id,
671
+ function: { arguments: pending.bufferedArguments },
744
672
  });
745
673
  }
674
+ pendingToolCalls.clear();
746
675
 
747
- // go through all tool calls and send the ones that are not finished
748
- for (const toolCall of toolCalls.filter(
749
- toolCall => !toolCall.hasFinished,
750
- )) {
751
- controller.enqueue({
752
- type: 'tool-input-end',
753
- id: toolCall.id,
754
- });
755
-
756
- controller.enqueue({
757
- type: 'tool-call',
758
- toolCallId: toolCall.id ?? generateId(),
759
- toolName: toolCall.function.name,
760
- input: toolCall.function.arguments,
761
- ...(toolCall.thoughtSignature
762
- ? {
763
- providerMetadata: {
764
- [providerOptionsName]: {
765
- thoughtSignature: toolCall.thoughtSignature,
766
- },
767
- },
768
- }
769
- : {}),
770
- });
771
- }
676
+ toolCallTracker.flush();
772
677
 
773
678
  if (finishReason == null) {
774
679
  finishReason = { unified: 'error', raw: undefined };