@openai/agents-extensions 0.3.7 → 0.3.9

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.
package/dist/aiSdk.mjs CHANGED
@@ -1,6 +1,26 @@
1
1
  import { createGenerationSpan, resetCurrentSpan, setCurrentSpan, Usage, UserError, withGenerationSpan, getLogger, } from '@openai/agents';
2
2
  import { isZodObject } from '@openai/agents/utils';
3
3
  import { encodeUint8ArrayToBase64 } from '@openai/agents/utils';
4
+ function getSpecVersion(model) {
5
+ const spec = model?.specificationVersion;
6
+ if (!spec) {
7
+ // Default to v2 for backward compatibility with older AI SDK model wrappers.
8
+ return 'v2';
9
+ }
10
+ if (spec === 'v2') {
11
+ return 'v2';
12
+ }
13
+ if (typeof spec === 'string' && spec.toLowerCase().startsWith('v3')) {
14
+ return 'v3';
15
+ }
16
+ return 'unknown';
17
+ }
18
+ function ensureSupportedModel(model) {
19
+ const spec = getSpecVersion(model);
20
+ if (spec === 'unknown') {
21
+ throw new UserError(`Unsupported AI SDK specificationVersion: ${String(model?.specificationVersion)}. Only v2 and v3 are supported.`);
22
+ }
23
+ }
4
24
  /**
5
25
  * @internal
6
26
  * Converts a list of model items to a list of language model V2 messages.
@@ -9,23 +29,52 @@ import { encodeUint8ArrayToBase64 } from '@openai/agents/utils';
9
29
  * @param items - The items to convert.
10
30
  * @returns The list of language model V2 messages.
11
31
  */
12
- export function itemsToLanguageV2Messages(model, items) {
32
+ export function itemsToLanguageV2Messages(model, items, modelSettings) {
13
33
  const messages = [];
14
34
  let currentAssistantMessage;
35
+ let pendingReasonerReasoning;
36
+ const flushPendingReasonerReasoningToMessages = () => {
37
+ if (!(shouldIncludeReasoningContent(model, modelSettings) &&
38
+ pendingReasonerReasoning)) {
39
+ return;
40
+ }
41
+ const reasoningPart = {
42
+ type: 'reasoning',
43
+ text: pendingReasonerReasoning.text,
44
+ providerOptions: pendingReasonerReasoning.providerOptions,
45
+ };
46
+ if (currentAssistantMessage &&
47
+ Array.isArray(currentAssistantMessage.content) &&
48
+ currentAssistantMessage.role === 'assistant') {
49
+ currentAssistantMessage.content.unshift(reasoningPart);
50
+ currentAssistantMessage.providerOptions = {
51
+ ...pendingReasonerReasoning.providerOptions,
52
+ ...currentAssistantMessage.providerOptions,
53
+ };
54
+ }
55
+ else {
56
+ messages.push({
57
+ role: 'assistant',
58
+ content: [reasoningPart],
59
+ providerOptions: pendingReasonerReasoning.providerOptions,
60
+ });
61
+ }
62
+ pendingReasonerReasoning = undefined;
63
+ };
15
64
  for (const item of items) {
16
65
  if (item.type === 'message' || typeof item.type === 'undefined') {
17
66
  const { role, content, providerData } = item;
18
67
  if (role === 'system') {
68
+ flushPendingReasonerReasoningToMessages();
19
69
  messages.push({
20
70
  role: 'system',
21
71
  content: content,
22
- providerOptions: {
23
- ...(providerData ?? {}),
24
- },
72
+ providerOptions: toProviderOptions(providerData, model),
25
73
  });
26
74
  continue;
27
75
  }
28
76
  if (role === 'user') {
77
+ flushPendingReasonerReasoningToMessages();
29
78
  messages.push({
30
79
  role,
31
80
  content: typeof content === 'string'
@@ -36,9 +85,7 @@ export function itemsToLanguageV2Messages(model, items) {
36
85
  return {
37
86
  type: 'text',
38
87
  text: c.text,
39
- providerOptions: {
40
- ...(contentProviderData ?? {}),
41
- },
88
+ providerOptions: toProviderOptions(contentProviderData, model),
42
89
  };
43
90
  }
44
91
  if (c.type === 'input_image') {
@@ -55,9 +102,7 @@ export function itemsToLanguageV2Messages(model, items) {
55
102
  type: 'file',
56
103
  data: url,
57
104
  mediaType: 'image/*',
58
- providerOptions: {
59
- ...(contentProviderData ?? {}),
60
- },
105
+ providerOptions: toProviderOptions(contentProviderData, model),
61
106
  };
62
107
  }
63
108
  if (c.type === 'input_file') {
@@ -65,9 +110,7 @@ export function itemsToLanguageV2Messages(model, items) {
65
110
  }
66
111
  throw new UserError(`Unknown content type: ${c.type}`);
67
112
  }),
68
- providerOptions: {
69
- ...(providerData ?? {}),
70
- },
113
+ providerOptions: toProviderOptions(providerData, model),
71
114
  });
72
115
  continue;
73
116
  }
@@ -76,23 +119,39 @@ export function itemsToLanguageV2Messages(model, items) {
76
119
  messages.push(currentAssistantMessage);
77
120
  currentAssistantMessage = undefined;
78
121
  }
122
+ const assistantProviderOptions = toProviderOptions(providerData, model);
123
+ const assistantContent = content
124
+ .filter((c) => c.type === 'output_text')
125
+ .map((c) => {
126
+ const { providerData: contentProviderData } = c;
127
+ return {
128
+ type: 'text',
129
+ text: c.text,
130
+ providerOptions: toProviderOptions(contentProviderData, model),
131
+ };
132
+ });
133
+ if (shouldIncludeReasoningContent(model, modelSettings) &&
134
+ pendingReasonerReasoning) {
135
+ assistantContent.unshift({
136
+ type: 'reasoning',
137
+ text: pendingReasonerReasoning.text,
138
+ providerOptions: pendingReasonerReasoning.providerOptions,
139
+ });
140
+ messages.push({
141
+ role,
142
+ content: assistantContent,
143
+ providerOptions: {
144
+ ...pendingReasonerReasoning.providerOptions,
145
+ ...assistantProviderOptions,
146
+ },
147
+ });
148
+ pendingReasonerReasoning = undefined;
149
+ continue;
150
+ }
79
151
  messages.push({
80
152
  role,
81
- content: content
82
- .filter((c) => c.type === 'output_text')
83
- .map((c) => {
84
- const { providerData: contentProviderData } = c;
85
- return {
86
- type: 'text',
87
- text: c.text,
88
- providerOptions: {
89
- ...(contentProviderData ?? {}),
90
- },
91
- };
92
- }),
93
- providerOptions: {
94
- ...(providerData ?? {}),
95
- },
153
+ content: assistantContent,
154
+ providerOptions: assistantProviderOptions,
96
155
  });
97
156
  continue;
98
157
  }
@@ -104,27 +163,38 @@ export function itemsToLanguageV2Messages(model, items) {
104
163
  currentAssistantMessage = {
105
164
  role: 'assistant',
106
165
  content: [],
107
- providerOptions: {
108
- ...(item.providerData ?? {}),
109
- },
166
+ providerOptions: toProviderOptions(item.providerData, model),
110
167
  };
111
168
  }
112
169
  if (Array.isArray(currentAssistantMessage.content) &&
113
170
  currentAssistantMessage.role === 'assistant') {
171
+ // Reasoner models (e.g., DeepSeek Reasoner) require reasoning_content on tool-call messages.
172
+ if (shouldIncludeReasoningContent(model, modelSettings) &&
173
+ pendingReasonerReasoning) {
174
+ currentAssistantMessage.content.push({
175
+ type: 'reasoning',
176
+ text: pendingReasonerReasoning.text,
177
+ providerOptions: pendingReasonerReasoning.providerOptions,
178
+ });
179
+ currentAssistantMessage.providerOptions = {
180
+ ...pendingReasonerReasoning.providerOptions,
181
+ ...currentAssistantMessage.providerOptions,
182
+ };
183
+ pendingReasonerReasoning = undefined;
184
+ }
114
185
  const content = {
115
186
  type: 'tool-call',
116
187
  toolCallId: item.callId,
117
188
  toolName: item.name,
118
189
  input: parseArguments(item.arguments),
119
- providerOptions: {
120
- ...(item.providerData ?? {}),
121
- },
190
+ providerOptions: toProviderOptions(item.providerData, model),
122
191
  };
123
192
  currentAssistantMessage.content.push(content);
124
193
  }
125
194
  continue;
126
195
  }
127
196
  else if (item.type === 'function_call_result') {
197
+ flushPendingReasonerReasoningToMessages();
128
198
  if (currentAssistantMessage) {
129
199
  messages.push(currentAssistantMessage);
130
200
  currentAssistantMessage = undefined;
@@ -134,16 +204,12 @@ export function itemsToLanguageV2Messages(model, items) {
134
204
  toolCallId: item.callId,
135
205
  toolName: item.name,
136
206
  output: convertToAiSdkOutput(item.output),
137
- providerOptions: {
138
- ...(item.providerData ?? {}),
139
- },
207
+ providerOptions: toProviderOptions(item.providerData, model),
140
208
  };
141
209
  messages.push({
142
210
  role: 'tool',
143
211
  content: [toolResult],
144
- providerOptions: {
145
- ...(item.providerData ?? {}),
146
- },
212
+ providerOptions: toProviderOptions(item.providerData, model),
147
213
  });
148
214
  continue;
149
215
  }
@@ -171,22 +237,29 @@ export function itemsToLanguageV2Messages(model, items) {
171
237
  if (item.type === 'reasoning' &&
172
238
  item.content.length > 0 &&
173
239
  typeof item.content[0].text === 'string') {
240
+ // Only forward provider data when it targets this model so signatures stay scoped correctly.
241
+ if (shouldIncludeReasoningContent(model, modelSettings)) {
242
+ pendingReasonerReasoning = {
243
+ text: item.content[0].text,
244
+ providerOptions: toProviderOptions(item.providerData, model),
245
+ };
246
+ continue;
247
+ }
174
248
  messages.push({
175
249
  role: 'assistant',
176
250
  content: [
177
251
  {
178
252
  type: 'reasoning',
179
253
  text: item.content[0].text,
180
- providerOptions: { ...(item.providerData ?? {}) },
254
+ providerOptions: toProviderOptions(item.providerData, model),
181
255
  },
182
256
  ],
183
- providerOptions: {
184
- ...(item.providerData ?? {}),
185
- },
257
+ providerOptions: toProviderOptions(item.providerData, model),
186
258
  });
187
259
  continue;
188
260
  }
189
261
  if (item.type === 'unknown') {
262
+ flushPendingReasonerReasoningToMessages();
190
263
  messages.push({ ...(item.providerData ?? {}) });
191
264
  continue;
192
265
  }
@@ -196,6 +269,7 @@ export function itemsToLanguageV2Messages(model, items) {
196
269
  const itemType = item;
197
270
  throw new UserError(`Unknown item type: ${itemType}`);
198
271
  }
272
+ flushPendingReasonerReasoningToMessages();
199
273
  if (currentAssistantMessage) {
200
274
  messages.push(currentAssistantMessage);
201
275
  }
@@ -381,6 +455,122 @@ function convertStructuredOutputsToAiSdkOutput(outputs) {
381
455
  function isRecord(value) {
382
456
  return typeof value === 'object' && value !== null;
383
457
  }
458
+ function getModelIdentifier(model) {
459
+ return `${model.provider}:${model.modelId}`;
460
+ }
461
+ function isProviderDataForModel(providerData, model) {
462
+ const providerDataModel = providerData.model;
463
+ if (typeof providerDataModel !== 'string') {
464
+ return true;
465
+ }
466
+ const target = getModelIdentifier(model).toLowerCase();
467
+ const pdLower = providerDataModel.toLowerCase();
468
+ return (pdLower === target ||
469
+ pdLower === model.modelId.toLowerCase() ||
470
+ pdLower === model.provider.toLowerCase());
471
+ }
472
+ function isGeminiModel(model) {
473
+ const target = getModelIdentifier(model).toLowerCase();
474
+ return (target.includes('gemini') || model.modelId.toLowerCase().includes('gemini'));
475
+ }
476
+ function isDeepSeekModel(model) {
477
+ const target = getModelIdentifier(model).toLowerCase();
478
+ return (target.includes('deepseek') ||
479
+ model.modelId.toLowerCase().includes('deepseek') ||
480
+ model.provider.toLowerCase().includes('deepseek'));
481
+ }
482
+ function shouldIncludeReasoningContent(model, modelSettings) {
483
+ const target = getModelIdentifier(model).toLowerCase();
484
+ const modelIdLower = model.modelId.toLowerCase();
485
+ // DeepSeek models require reasoning_content to be sent alongside tool calls when
486
+ // either the dedicated reasoner model is used or thinking mode is explicitly enabled.
487
+ const isDeepSeekReasoner = target.includes('deepseek-reasoner') ||
488
+ modelIdLower.includes('deepseek-reasoner');
489
+ if (isDeepSeekReasoner) {
490
+ return true;
491
+ }
492
+ if (!isDeepSeekModel(model)) {
493
+ return false;
494
+ }
495
+ return hasEnabledDeepSeekThinking(modelSettings?.providerData);
496
+ }
497
+ function hasEnabledDeepSeekThinking(providerData) {
498
+ if (!isRecord(providerData)) {
499
+ return false;
500
+ }
501
+ const thinkingOption = [
502
+ providerData.thinking,
503
+ providerData.deepseek?.thinking,
504
+ providerData.providerOptions?.thinking,
505
+ providerData.providerOptions?.deepseek?.thinking,
506
+ ].find((value) => value !== undefined);
507
+ return isThinkingEnabled(thinkingOption);
508
+ }
509
+ function isThinkingEnabled(option) {
510
+ if (option === undefined || option === null) {
511
+ return false;
512
+ }
513
+ if (option === true) {
514
+ return true;
515
+ }
516
+ if (typeof option === 'string') {
517
+ return option.toLowerCase() === 'enabled';
518
+ }
519
+ if (isRecord(option)) {
520
+ const type = option.type ?? option.mode ?? option.status;
521
+ if (typeof type === 'string') {
522
+ return type.toLowerCase() === 'enabled';
523
+ }
524
+ }
525
+ return false;
526
+ }
527
+ function toProviderOptions(providerData, model) {
528
+ if (!isRecord(providerData)) {
529
+ return {};
530
+ }
531
+ if (!isProviderDataForModel(providerData, model)) {
532
+ return {};
533
+ }
534
+ const options = { ...providerData };
535
+ delete options.model;
536
+ delete options.responseId;
537
+ delete options.response_id;
538
+ if (isGeminiModel(model)) {
539
+ const googleFields = isRecord(options.google) ? { ...options.google } : {};
540
+ const thoughtSignature = googleFields.thoughtSignature ??
541
+ googleFields.thought_signature ??
542
+ options.thoughtSignature ??
543
+ options.thought_signature;
544
+ if (thoughtSignature) {
545
+ googleFields.thoughtSignature = thoughtSignature;
546
+ }
547
+ if (Object.keys(googleFields).length > 0) {
548
+ options.google = googleFields;
549
+ }
550
+ delete options.thoughtSignature;
551
+ delete options.thought_signature;
552
+ }
553
+ return options;
554
+ }
555
+ function buildBaseProviderData(model, responseId) {
556
+ const base = { model: getModelIdentifier(model) };
557
+ if (responseId) {
558
+ base.responseId = responseId;
559
+ }
560
+ return base;
561
+ }
562
+ function mergeProviderData(base, ...sources) {
563
+ const merged = {};
564
+ if (isRecord(base)) {
565
+ Object.assign(merged, base);
566
+ }
567
+ for (const src of sources) {
568
+ if (isRecord(src)) {
569
+ Object.assign(merged, src);
570
+ }
571
+ }
572
+ return Object.keys(merged).length > 0 ? merged : undefined;
573
+ }
384
574
  function getImageInlineMediaType(source) {
385
575
  if (typeof source.mediaType === 'string' && source.mediaType.length > 0) {
386
576
  return source.mediaType;
@@ -476,6 +666,7 @@ export class AiSdkModel {
476
666
  #model;
477
667
  #logger = getLogger('openai-agents:extensions:ai-sdk');
478
668
  constructor(model) {
669
+ ensureSupportedModel(model);
479
670
  this.#model = model;
480
671
  }
481
672
  async getResponse(request) {
@@ -493,7 +684,7 @@ export class AiSdkModel {
493
684
  content: [{ type: 'text', text: request.input }],
494
685
  },
495
686
  ]
496
- : itemsToLanguageV2Messages(this.#model, request.input);
687
+ : itemsToLanguageV2Messages(this.#model, request.input, request.modelSettings);
497
688
  if (request.systemInstructions) {
498
689
  input = [
499
690
  {
@@ -534,8 +725,10 @@ export class AiSdkModel {
534
725
  this.#logger.debug('Request:', JSON.stringify(aiSdkRequest, null, 2));
535
726
  }
536
727
  const result = await this.#model.doGenerate(aiSdkRequest);
728
+ const baseProviderData = buildBaseProviderData(this.#model, result.response?.id);
537
729
  const output = [];
538
730
  const resultContent = result.content ?? [];
731
+ // Emit reasoning before tool calls so Anthropic thinking signatures propagate into the next turn.
539
732
  // Extract and add reasoning items FIRST (required by Anthropic: thinking blocks must precede tool_use blocks)
540
733
  const reasoningParts = resultContent.filter((c) => c && c.type === 'reasoning');
541
734
  for (const reasoningPart of reasoningParts) {
@@ -545,7 +738,7 @@ export class AiSdkModel {
545
738
  content: [{ type: 'input_text', text: reasoningText }],
546
739
  rawContent: [{ type: 'reasoning_text', text: reasoningText }],
547
740
  // Preserve provider-specific metadata (including signature for Anthropic extended thinking)
548
- providerData: reasoningPart.providerMetadata ?? undefined,
741
+ providerData: mergeProviderData(baseProviderData, reasoningPart.providerMetadata),
549
742
  });
550
743
  }
551
744
  const toolCalls = resultContent.filter((c) => c && c.type === 'tool-call');
@@ -577,8 +770,8 @@ export class AiSdkModel {
577
770
  name: toolCall.toolName,
578
771
  arguments: toolCallArguments,
579
772
  status: 'completed',
580
- providerData: toolCall.providerMetadata ??
581
- (hasToolCalls ? result.providerMetadata : undefined),
773
+ providerData: mergeProviderData(baseProviderData, toolCall.providerMetadata ??
774
+ (hasToolCalls ? result.providerMetadata : undefined)),
582
775
  });
583
776
  }
584
777
  // Some of other platforms may return both tool calls and text.
@@ -593,7 +786,7 @@ export class AiSdkModel {
593
786
  content: [{ type: 'output_text', text: textItem.text }],
594
787
  role: 'assistant',
595
788
  status: 'completed',
596
- providerData: result.providerMetadata,
789
+ providerData: mergeProviderData(baseProviderData, result.providerMetadata),
597
790
  });
598
791
  }
599
792
  }
@@ -603,18 +796,10 @@ export class AiSdkModel {
603
796
  const response = {
604
797
  responseId: result.response?.id ?? 'FAKE_ID',
605
798
  usage: new Usage({
606
- inputTokens: Number.isNaN(result.usage?.inputTokens)
607
- ? 0
608
- : (result.usage?.inputTokens ?? 0),
609
- outputTokens: Number.isNaN(result.usage?.outputTokens)
610
- ? 0
611
- : (result.usage?.outputTokens ?? 0),
612
- totalTokens: (Number.isNaN(result.usage?.inputTokens)
613
- ? 0
614
- : (result.usage?.inputTokens ?? 0)) +
615
- (Number.isNaN(result.usage?.outputTokens)
616
- ? 0
617
- : (result.usage?.outputTokens ?? 0)) || 0,
799
+ inputTokens: extractTokenCount(result.usage, 'inputTokens'),
800
+ outputTokens: extractTokenCount(result.usage, 'outputTokens'),
801
+ totalTokens: extractTokenCount(result.usage, 'inputTokens') +
802
+ extractTokenCount(result.usage, 'outputTokens') || 0,
618
803
  }),
619
804
  output,
620
805
  providerData: result,
@@ -702,7 +887,7 @@ export class AiSdkModel {
702
887
  content: [{ type: 'text', text: request.input }],
703
888
  },
704
889
  ]
705
- : itemsToLanguageV2Messages(this.#model, request.input);
890
+ : itemsToLanguageV2Messages(this.#model, request.input, request.modelSettings);
706
891
  if (request.systemInstructions) {
707
892
  input = [
708
893
  {
@@ -722,6 +907,7 @@ export class AiSdkModel {
722
907
  const responseFormat = getResponseFormat(request.outputType);
723
908
  const aiSdkRequest = {
724
909
  tools,
910
+ toolChoice: toolChoiceToLanguageV2Format(request.modelSettings.toolChoice),
725
911
  prompt: input,
726
912
  temperature: request.modelSettings.temperature,
727
913
  topP: request.modelSettings.topP,
@@ -739,13 +925,15 @@ export class AiSdkModel {
739
925
  this.#logger.debug('Request (streamed):', JSON.stringify(aiSdkRequest, null, 2));
740
926
  }
741
927
  const { stream } = await this.#model.doStream(aiSdkRequest);
928
+ const baseProviderData = buildBaseProviderData(this.#model);
742
929
  let started = false;
743
930
  let responseId;
744
931
  let usagePromptTokens = 0;
745
932
  let usageCompletionTokens = 0;
746
933
  const functionCalls = {};
747
934
  let textOutput;
748
- // State for tracking reasoning blocks (for Anthropic extended thinking)
935
+ // State for tracking reasoning blocks (for Anthropic extended thinking):
936
+ // Track reasoning deltas so we can preserve Anthropic signatures even when text is redacted.
749
937
  const reasoningBlocks = {};
750
938
  for await (const part of stream) {
751
939
  if (!started) {
@@ -801,9 +989,7 @@ export class AiSdkModel {
801
989
  name: part.toolName,
802
990
  arguments: part.input ?? '',
803
991
  status: 'completed',
804
- ...(part.providerMetadata
805
- ? { providerData: part.providerMetadata }
806
- : {}),
992
+ providerData: mergeProviderData(baseProviderData, part.providerMetadata),
807
993
  };
808
994
  }
809
995
  break;
@@ -815,12 +1001,8 @@ export class AiSdkModel {
815
1001
  break;
816
1002
  }
817
1003
  case 'finish': {
818
- usagePromptTokens = Number.isNaN(part.usage?.inputTokens)
819
- ? 0
820
- : (part.usage?.inputTokens ?? 0);
821
- usageCompletionTokens = Number.isNaN(part.usage?.outputTokens)
822
- ? 0
823
- : (part.usage?.outputTokens ?? 0);
1004
+ usagePromptTokens = extractTokenCount(part.usage, 'inputTokens');
1005
+ usageCompletionTokens = extractTokenCount(part.usage, 'outputTokens');
824
1006
  break;
825
1007
  }
826
1008
  case 'error': {
@@ -841,7 +1023,7 @@ export class AiSdkModel {
841
1023
  content: [{ type: 'input_text', text: reasoningBlock.text }],
842
1024
  rawContent: [{ type: 'reasoning_text', text: reasoningBlock.text }],
843
1025
  // Preserve provider-specific metadata (including signature for Anthropic extended thinking)
844
- providerData: reasoningBlock.providerMetadata ?? undefined,
1026
+ providerData: mergeProviderData(baseProviderData, reasoningBlock.providerMetadata, responseId ? { responseId } : undefined),
845
1027
  });
846
1028
  }
847
1029
  }
@@ -851,10 +1033,14 @@ export class AiSdkModel {
851
1033
  role: 'assistant',
852
1034
  content: [textOutput],
853
1035
  status: 'completed',
1036
+ providerData: mergeProviderData(baseProviderData, responseId ? { responseId } : undefined),
854
1037
  });
855
1038
  }
856
1039
  for (const fc of Object.values(functionCalls)) {
857
- outputs.push(fc);
1040
+ outputs.push({
1041
+ ...fc,
1042
+ providerData: mergeProviderData(baseProviderData, fc.providerData, responseId ? { responseId } : undefined),
1043
+ });
858
1044
  }
859
1045
  const finalEvent = {
860
1046
  type: 'response_done',
@@ -960,6 +1146,19 @@ export class AiSdkModel {
960
1146
  export function aisdk(model) {
961
1147
  return new AiSdkModel(model);
962
1148
  }
1149
+ function extractTokenCount(usage, key) {
1150
+ const val = usage?.[key];
1151
+ if (typeof val === 'number') {
1152
+ return Number.isNaN(val) ? 0 : val;
1153
+ }
1154
+ // Handle Google AI SDK object format ({ total: number, ... })
1155
+ if (typeof val === 'object' &&
1156
+ val !== null &&
1157
+ typeof val.total === 'number') {
1158
+ return val.total;
1159
+ }
1160
+ return 0;
1161
+ }
963
1162
  export function parseArguments(args) {
964
1163
  if (!args) {
965
1164
  return {};