@ai-sdk/openai 3.0.98 → 3.0.100
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/CHANGELOG.md +13 -0
- package/dist/index.js +564 -231
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +488 -153
- package/dist/index.mjs.map +1 -1
- package/dist/internal/index.js +584 -253
- package/dist/internal/index.js.map +1 -1
- package/dist/internal/index.mjs +485 -152
- package/dist/internal/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/responses/convert-to-openai-responses-input.ts +309 -90
- package/src/responses/expand-parallel-tool-call.ts +142 -0
- package/src/responses/openai-responses-language-model.ts +171 -29
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/openai",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.100",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@ai-sdk/provider": "3.0.15",
|
|
40
|
-
"@ai-sdk/provider-utils": "4.0.
|
|
40
|
+
"@ai-sdk/provider-utils": "4.0.47"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "20.17.24",
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
UnsupportedFunctionalityError,
|
|
3
3
|
type LanguageModelV3Prompt,
|
|
4
|
+
type LanguageModelV3ToolResultOutput,
|
|
5
|
+
type LanguageModelV3ToolResultPart,
|
|
4
6
|
type LanguageModelV3ToolApprovalResponsePart,
|
|
5
7
|
type SharedV3ProviderOptions,
|
|
6
8
|
type SharedV3Warning,
|
|
@@ -33,11 +35,206 @@ import type {
|
|
|
33
35
|
OpenAIResponsesInput,
|
|
34
36
|
OpenAIResponsesReasoning,
|
|
35
37
|
} from './openai-responses-api';
|
|
38
|
+
import {
|
|
39
|
+
getParallelToolCallMetadata,
|
|
40
|
+
type ParallelToolCallMetadata,
|
|
41
|
+
} from './expand-parallel-tool-call';
|
|
36
42
|
|
|
37
43
|
function serializeToolCallArguments(input: unknown): string {
|
|
38
44
|
return JSON.stringify(input === undefined ? {} : input);
|
|
39
45
|
}
|
|
40
46
|
|
|
47
|
+
async function convertFunctionToolResultOutput({
|
|
48
|
+
output,
|
|
49
|
+
providerOptionsName,
|
|
50
|
+
warnings,
|
|
51
|
+
}: {
|
|
52
|
+
output: LanguageModelV3ToolResultOutput;
|
|
53
|
+
providerOptionsName: string;
|
|
54
|
+
warnings: Array<SharedV3Warning>;
|
|
55
|
+
}): Promise<OpenAIResponsesFunctionCallOutput['output']> {
|
|
56
|
+
switch (output.type) {
|
|
57
|
+
case 'text':
|
|
58
|
+
case 'error-text':
|
|
59
|
+
return output.value;
|
|
60
|
+
case 'execution-denied':
|
|
61
|
+
return output.reason ?? 'Tool call execution denied.';
|
|
62
|
+
case 'json':
|
|
63
|
+
case 'error-json':
|
|
64
|
+
return JSON.stringify(output.value);
|
|
65
|
+
case 'content':
|
|
66
|
+
return output.value
|
|
67
|
+
.map(item => {
|
|
68
|
+
const promptCacheBreakpoint = getPromptCacheBreakpoint(
|
|
69
|
+
item.providerOptions,
|
|
70
|
+
providerOptionsName,
|
|
71
|
+
);
|
|
72
|
+
switch (item.type) {
|
|
73
|
+
case 'text': {
|
|
74
|
+
return {
|
|
75
|
+
type: 'input_text' as const,
|
|
76
|
+
text: item.text,
|
|
77
|
+
...(promptCacheBreakpoint != null && {
|
|
78
|
+
prompt_cache_breakpoint: promptCacheBreakpoint,
|
|
79
|
+
}),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
case 'image-data': {
|
|
84
|
+
return {
|
|
85
|
+
type: 'input_image' as const,
|
|
86
|
+
image_url: `data:${item.mediaType};base64,${item.data}`,
|
|
87
|
+
detail:
|
|
88
|
+
item.providerOptions?.[providerOptionsName]?.imageDetail,
|
|
89
|
+
...(promptCacheBreakpoint != null && {
|
|
90
|
+
prompt_cache_breakpoint: promptCacheBreakpoint,
|
|
91
|
+
}),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
case 'image-url': {
|
|
96
|
+
return {
|
|
97
|
+
type: 'input_image' as const,
|
|
98
|
+
image_url: item.url,
|
|
99
|
+
detail:
|
|
100
|
+
item.providerOptions?.[providerOptionsName]?.imageDetail,
|
|
101
|
+
...(promptCacheBreakpoint != null && {
|
|
102
|
+
prompt_cache_breakpoint: promptCacheBreakpoint,
|
|
103
|
+
}),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
case 'file-data': {
|
|
108
|
+
return {
|
|
109
|
+
type: 'input_file' as const,
|
|
110
|
+
filename: item.filename ?? 'data',
|
|
111
|
+
file_data: `data:${item.mediaType};base64,${item.data}`,
|
|
112
|
+
...(promptCacheBreakpoint != null && {
|
|
113
|
+
prompt_cache_breakpoint: promptCacheBreakpoint,
|
|
114
|
+
}),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
case 'file-url': {
|
|
119
|
+
return {
|
|
120
|
+
type: 'input_file' as const,
|
|
121
|
+
file_url: item.url,
|
|
122
|
+
...(promptCacheBreakpoint != null && {
|
|
123
|
+
prompt_cache_breakpoint: promptCacheBreakpoint,
|
|
124
|
+
}),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
default: {
|
|
129
|
+
warnings.push({
|
|
130
|
+
type: 'other',
|
|
131
|
+
message: `unsupported tool content part type: ${item.type}`,
|
|
132
|
+
});
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
.filter(isNonNullable);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
type ParallelToolResultGroup = {
|
|
142
|
+
metadata: ParallelToolCallMetadata;
|
|
143
|
+
results: Array<LanguageModelV3ToolResultPart>;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
function hasSameParallelToolCall(
|
|
147
|
+
first: ParallelToolCallMetadata,
|
|
148
|
+
second: ParallelToolCallMetadata,
|
|
149
|
+
): boolean {
|
|
150
|
+
return (
|
|
151
|
+
first.itemId === second.itemId &&
|
|
152
|
+
first.toolCallId === second.toolCallId &&
|
|
153
|
+
first.toolName === second.toolName &&
|
|
154
|
+
first.input === second.input &&
|
|
155
|
+
first.count === second.count
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function collectCompleteParallelToolResultGroups({
|
|
160
|
+
prompt,
|
|
161
|
+
providerOptionsName,
|
|
162
|
+
}: {
|
|
163
|
+
prompt: LanguageModelV3Prompt;
|
|
164
|
+
providerOptionsName: string;
|
|
165
|
+
}): Map<string, ParallelToolResultGroup> {
|
|
166
|
+
const pendingGroups = new Map<
|
|
167
|
+
string,
|
|
168
|
+
{
|
|
169
|
+
metadata: ParallelToolCallMetadata;
|
|
170
|
+
results: Map<number, LanguageModelV3ToolResultPart>;
|
|
171
|
+
invalid: boolean;
|
|
172
|
+
}
|
|
173
|
+
>();
|
|
174
|
+
|
|
175
|
+
for (const message of prompt) {
|
|
176
|
+
if (message.role !== 'tool') {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
for (const part of message.content) {
|
|
181
|
+
if (part.type !== 'tool-result') {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const metadata = getParallelToolCallMetadata({
|
|
186
|
+
providerOptions: part.providerOptions,
|
|
187
|
+
providerOptionsName,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
if (metadata == null) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const existing = pendingGroups.get(metadata.toolCallId);
|
|
195
|
+
if (existing == null) {
|
|
196
|
+
pendingGroups.set(metadata.toolCallId, {
|
|
197
|
+
metadata,
|
|
198
|
+
results: new Map([[metadata.index, part]]),
|
|
199
|
+
invalid: false,
|
|
200
|
+
});
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (
|
|
205
|
+
!hasSameParallelToolCall(existing.metadata, metadata) ||
|
|
206
|
+
existing.results.has(metadata.index)
|
|
207
|
+
) {
|
|
208
|
+
existing.invalid = true;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
existing.results.set(metadata.index, part);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const completeGroups = new Map<string, ParallelToolResultGroup>();
|
|
217
|
+
|
|
218
|
+
for (const [toolCallId, group] of pendingGroups) {
|
|
219
|
+
if (group.invalid || group.results.size !== group.metadata.count) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const results = Array.from({ length: group.metadata.count }, (_, index) =>
|
|
224
|
+
group.results.get(index),
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
if (results.every(isNonNullable)) {
|
|
228
|
+
completeGroups.set(toolCallId, {
|
|
229
|
+
metadata: group.metadata,
|
|
230
|
+
results,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return completeGroups;
|
|
236
|
+
}
|
|
237
|
+
|
|
41
238
|
type OpenAIPromptCacheBreakpoint = { mode: 'explicit' };
|
|
42
239
|
|
|
43
240
|
function getPromptCacheBreakpoint(
|
|
@@ -93,6 +290,15 @@ export async function convertToOpenAIResponsesInput({
|
|
|
93
290
|
let input: OpenAIResponsesInput = [];
|
|
94
291
|
const warnings: Array<SharedV3Warning> = [];
|
|
95
292
|
const processedApprovalIds = new Set<string>();
|
|
293
|
+
const parallelToolResultGroups =
|
|
294
|
+
hasConversation || hasPreviousResponseId
|
|
295
|
+
? collectCompleteParallelToolResultGroups({
|
|
296
|
+
prompt,
|
|
297
|
+
providerOptionsName,
|
|
298
|
+
})
|
|
299
|
+
: new Map<string, ParallelToolResultGroup>();
|
|
300
|
+
const emittedParallelToolCalls = new Set<string>();
|
|
301
|
+
const emittedParallelToolResults = new Set<string>();
|
|
96
302
|
|
|
97
303
|
for (const { role, content, providerOptions } of prompt) {
|
|
98
304
|
switch (role) {
|
|
@@ -279,6 +485,49 @@ export async function convertToOpenAIResponsesInput({
|
|
|
279
485
|
break;
|
|
280
486
|
}
|
|
281
487
|
case 'tool-call': {
|
|
488
|
+
const parallelToolCallMetadata = getParallelToolCallMetadata({
|
|
489
|
+
providerOptions: part.providerOptions,
|
|
490
|
+
providerOptionsName,
|
|
491
|
+
});
|
|
492
|
+
const parallelToolResultGroup =
|
|
493
|
+
parallelToolCallMetadata == null
|
|
494
|
+
? undefined
|
|
495
|
+
: parallelToolResultGroups.get(
|
|
496
|
+
parallelToolCallMetadata.toolCallId,
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
if (
|
|
500
|
+
parallelToolCallMetadata != null &&
|
|
501
|
+
parallelToolResultGroup != null &&
|
|
502
|
+
hasSameParallelToolCall(
|
|
503
|
+
parallelToolResultGroup.metadata,
|
|
504
|
+
parallelToolCallMetadata,
|
|
505
|
+
)
|
|
506
|
+
) {
|
|
507
|
+
if (
|
|
508
|
+
!emittedParallelToolCalls.has(
|
|
509
|
+
parallelToolResultGroup.metadata.toolCallId,
|
|
510
|
+
)
|
|
511
|
+
) {
|
|
512
|
+
emittedParallelToolCalls.add(
|
|
513
|
+
parallelToolResultGroup.metadata.toolCallId,
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
// Conversations already contain the original wrapper item.
|
|
517
|
+
// previousResponseId chains require plain client function
|
|
518
|
+
// calls to be reconstructed in full.
|
|
519
|
+
if (!hasConversation) {
|
|
520
|
+
input.push({
|
|
521
|
+
type: 'function_call',
|
|
522
|
+
call_id: parallelToolResultGroup.metadata.toolCallId,
|
|
523
|
+
name: parallelToolResultGroup.metadata.toolName,
|
|
524
|
+
arguments: parallelToolResultGroup.metadata.input,
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
530
|
+
|
|
282
531
|
const id = (part.providerOptions?.[providerOptionsName]?.itemId ??
|
|
283
532
|
(
|
|
284
533
|
part as {
|
|
@@ -709,6 +958,61 @@ export async function convertToOpenAIResponsesInput({
|
|
|
709
958
|
continue;
|
|
710
959
|
}
|
|
711
960
|
|
|
961
|
+
const parallelToolCallMetadata = getParallelToolCallMetadata({
|
|
962
|
+
providerOptions: part.providerOptions,
|
|
963
|
+
providerOptionsName,
|
|
964
|
+
});
|
|
965
|
+
const parallelToolResultGroup =
|
|
966
|
+
parallelToolCallMetadata == null
|
|
967
|
+
? undefined
|
|
968
|
+
: parallelToolResultGroups.get(
|
|
969
|
+
parallelToolCallMetadata.toolCallId,
|
|
970
|
+
);
|
|
971
|
+
|
|
972
|
+
if (
|
|
973
|
+
parallelToolCallMetadata != null &&
|
|
974
|
+
parallelToolResultGroup != null &&
|
|
975
|
+
hasSameParallelToolCall(
|
|
976
|
+
parallelToolResultGroup.metadata,
|
|
977
|
+
parallelToolCallMetadata,
|
|
978
|
+
)
|
|
979
|
+
) {
|
|
980
|
+
if (
|
|
981
|
+
!emittedParallelToolResults.has(
|
|
982
|
+
parallelToolResultGroup.metadata.toolCallId,
|
|
983
|
+
)
|
|
984
|
+
) {
|
|
985
|
+
emittedParallelToolResults.add(
|
|
986
|
+
parallelToolResultGroup.metadata.toolCallId,
|
|
987
|
+
);
|
|
988
|
+
|
|
989
|
+
const toolOutputs = await Promise.all(
|
|
990
|
+
parallelToolResultGroup.results.map(async result =>
|
|
991
|
+
convertFunctionToolResultOutput({
|
|
992
|
+
output: result.output,
|
|
993
|
+
providerOptionsName,
|
|
994
|
+
warnings,
|
|
995
|
+
}),
|
|
996
|
+
),
|
|
997
|
+
);
|
|
998
|
+
|
|
999
|
+
input.push({
|
|
1000
|
+
type: 'function_call_output',
|
|
1001
|
+
call_id: parallelToolResultGroup.metadata.toolCallId,
|
|
1002
|
+
// The internal wrapper returns one output containing the child
|
|
1003
|
+
// results in the same order as the original tool_uses array.
|
|
1004
|
+
output: toolOutputs
|
|
1005
|
+
.map(output =>
|
|
1006
|
+
typeof output === 'string'
|
|
1007
|
+
? output
|
|
1008
|
+
: JSON.stringify(output),
|
|
1009
|
+
)
|
|
1010
|
+
.join('\n'),
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
712
1016
|
const output = part.output;
|
|
713
1017
|
|
|
714
1018
|
// Skip execution-denied with approvalId - already handled via tool-approval-response
|
|
@@ -897,96 +1201,11 @@ export async function convertToOpenAIResponsesInput({
|
|
|
897
1201
|
continue;
|
|
898
1202
|
}
|
|
899
1203
|
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
break;
|
|
906
|
-
case 'execution-denied':
|
|
907
|
-
contentValue = output.reason ?? 'Tool call execution denied.';
|
|
908
|
-
break;
|
|
909
|
-
case 'json':
|
|
910
|
-
case 'error-json':
|
|
911
|
-
contentValue = JSON.stringify(output.value);
|
|
912
|
-
break;
|
|
913
|
-
case 'content':
|
|
914
|
-
contentValue = output.value
|
|
915
|
-
.map(item => {
|
|
916
|
-
const promptCacheBreakpoint = getPromptCacheBreakpoint(
|
|
917
|
-
item.providerOptions,
|
|
918
|
-
providerOptionsName,
|
|
919
|
-
);
|
|
920
|
-
switch (item.type) {
|
|
921
|
-
case 'text': {
|
|
922
|
-
return {
|
|
923
|
-
type: 'input_text' as const,
|
|
924
|
-
text: item.text,
|
|
925
|
-
...(promptCacheBreakpoint != null && {
|
|
926
|
-
prompt_cache_breakpoint: promptCacheBreakpoint,
|
|
927
|
-
}),
|
|
928
|
-
};
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
case 'image-data': {
|
|
932
|
-
return {
|
|
933
|
-
type: 'input_image' as const,
|
|
934
|
-
image_url: `data:${item.mediaType};base64,${item.data}`,
|
|
935
|
-
detail:
|
|
936
|
-
item.providerOptions?.[providerOptionsName]
|
|
937
|
-
?.imageDetail,
|
|
938
|
-
...(promptCacheBreakpoint != null && {
|
|
939
|
-
prompt_cache_breakpoint: promptCacheBreakpoint,
|
|
940
|
-
}),
|
|
941
|
-
};
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
case 'image-url': {
|
|
945
|
-
return {
|
|
946
|
-
type: 'input_image' as const,
|
|
947
|
-
image_url: item.url,
|
|
948
|
-
detail:
|
|
949
|
-
item.providerOptions?.[providerOptionsName]
|
|
950
|
-
?.imageDetail,
|
|
951
|
-
...(promptCacheBreakpoint != null && {
|
|
952
|
-
prompt_cache_breakpoint: promptCacheBreakpoint,
|
|
953
|
-
}),
|
|
954
|
-
};
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
case 'file-data': {
|
|
958
|
-
return {
|
|
959
|
-
type: 'input_file' as const,
|
|
960
|
-
filename: item.filename ?? 'data',
|
|
961
|
-
file_data: `data:${item.mediaType};base64,${item.data}`,
|
|
962
|
-
...(promptCacheBreakpoint != null && {
|
|
963
|
-
prompt_cache_breakpoint: promptCacheBreakpoint,
|
|
964
|
-
}),
|
|
965
|
-
};
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
case 'file-url': {
|
|
969
|
-
return {
|
|
970
|
-
type: 'input_file' as const,
|
|
971
|
-
file_url: item.url,
|
|
972
|
-
...(promptCacheBreakpoint != null && {
|
|
973
|
-
prompt_cache_breakpoint: promptCacheBreakpoint,
|
|
974
|
-
}),
|
|
975
|
-
};
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
default: {
|
|
979
|
-
warnings.push({
|
|
980
|
-
type: 'other',
|
|
981
|
-
message: `unsupported tool content part type: ${item.type}`,
|
|
982
|
-
});
|
|
983
|
-
return undefined;
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
})
|
|
987
|
-
.filter(isNonNullable);
|
|
988
|
-
break;
|
|
989
|
-
}
|
|
1204
|
+
const contentValue = await convertFunctionToolResultOutput({
|
|
1205
|
+
output,
|
|
1206
|
+
providerOptionsName,
|
|
1207
|
+
warnings,
|
|
1208
|
+
});
|
|
990
1209
|
|
|
991
1210
|
input.push({
|
|
992
1211
|
type: 'function_call_output',
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isJSONObject,
|
|
3
|
+
type LanguageModelV3FunctionTool,
|
|
4
|
+
type LanguageModelV3ToolCall,
|
|
5
|
+
type SharedV3ProviderOptions,
|
|
6
|
+
} from '@ai-sdk/provider';
|
|
7
|
+
import { safeParseJSON } from '@ai-sdk/provider-utils';
|
|
8
|
+
|
|
9
|
+
const parallelToolName = 'parallel';
|
|
10
|
+
const recipientNamePrefix = 'functions.';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Preserves the original wrapper identity so child results can be sent back as
|
|
14
|
+
* one function output when Responses API server-side state is used.
|
|
15
|
+
*/
|
|
16
|
+
export type ParallelToolCallMetadata = {
|
|
17
|
+
itemId: string;
|
|
18
|
+
toolCallId: string;
|
|
19
|
+
toolName: string;
|
|
20
|
+
input: string;
|
|
21
|
+
index: number;
|
|
22
|
+
count: number;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function getParallelToolCallMetadata({
|
|
26
|
+
providerOptions,
|
|
27
|
+
providerOptionsName,
|
|
28
|
+
}: {
|
|
29
|
+
providerOptions: SharedV3ProviderOptions | undefined;
|
|
30
|
+
providerOptionsName: string;
|
|
31
|
+
}): ParallelToolCallMetadata | undefined {
|
|
32
|
+
const metadata = providerOptions?.[providerOptionsName]?.parallelToolCall;
|
|
33
|
+
|
|
34
|
+
if (
|
|
35
|
+
!isJSONObject(metadata) ||
|
|
36
|
+
typeof metadata.itemId !== 'string' ||
|
|
37
|
+
typeof metadata.toolCallId !== 'string' ||
|
|
38
|
+
typeof metadata.toolName !== 'string' ||
|
|
39
|
+
typeof metadata.input !== 'string' ||
|
|
40
|
+
typeof metadata.index !== 'number' ||
|
|
41
|
+
!Number.isInteger(metadata.index) ||
|
|
42
|
+
typeof metadata.count !== 'number' ||
|
|
43
|
+
!Number.isInteger(metadata.count) ||
|
|
44
|
+
metadata.index < 0 ||
|
|
45
|
+
metadata.count <= metadata.index
|
|
46
|
+
) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return metadata as ParallelToolCallMetadata;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function isUndeclaredParallelToolCall({
|
|
54
|
+
toolName,
|
|
55
|
+
tools,
|
|
56
|
+
}: {
|
|
57
|
+
toolName: string;
|
|
58
|
+
tools: Array<LanguageModelV3FunctionTool>;
|
|
59
|
+
}): boolean {
|
|
60
|
+
return (
|
|
61
|
+
toolName === parallelToolName &&
|
|
62
|
+
!tools.some(tool => tool.name === parallelToolName)
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Expands the internal parallel tool wrapper that OpenAI models can emit as a
|
|
68
|
+
* regular function call. The wrapper is only recognized when every nested
|
|
69
|
+
* recipient is a declared client-side function tool.
|
|
70
|
+
*/
|
|
71
|
+
export async function expandParallelToolCall({
|
|
72
|
+
toolCall,
|
|
73
|
+
tools,
|
|
74
|
+
providerOptionsName,
|
|
75
|
+
itemId,
|
|
76
|
+
}: {
|
|
77
|
+
toolCall: Pick<LanguageModelV3ToolCall, 'toolCallId' | 'toolName' | 'input'>;
|
|
78
|
+
tools: Array<LanguageModelV3FunctionTool>;
|
|
79
|
+
providerOptionsName: string;
|
|
80
|
+
itemId: string;
|
|
81
|
+
}): Promise<Array<LanguageModelV3ToolCall> | undefined> {
|
|
82
|
+
if (!isUndeclaredParallelToolCall({ toolName: toolCall.toolName, tools })) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const parsedInput = await safeParseJSON({ text: toolCall.input });
|
|
87
|
+
|
|
88
|
+
if (!parsedInput.success || !isJSONObject(parsedInput.value)) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const toolUses = parsedInput.value.tool_uses;
|
|
93
|
+
if (!Array.isArray(toolUses) || toolUses.length === 0) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const availableToolNames = new Set(tools.map(tool => tool.name));
|
|
98
|
+
const expandedToolCalls: Array<LanguageModelV3ToolCall> = [];
|
|
99
|
+
|
|
100
|
+
for (const [index, toolUse] of toolUses.entries()) {
|
|
101
|
+
if (!isJSONObject(toolUse)) {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const recipientName = toolUse.recipient_name;
|
|
106
|
+
const parameters = toolUse.parameters;
|
|
107
|
+
|
|
108
|
+
if (
|
|
109
|
+
typeof recipientName !== 'string' ||
|
|
110
|
+
!recipientName.startsWith(recipientNamePrefix) ||
|
|
111
|
+
!isJSONObject(parameters)
|
|
112
|
+
) {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const toolName = recipientName.slice(recipientNamePrefix.length);
|
|
117
|
+
if (toolName.length === 0 || !availableToolNames.has(toolName)) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
expandedToolCalls.push({
|
|
122
|
+
type: 'tool-call',
|
|
123
|
+
toolCallId: `${toolCall.toolCallId}_${index}`,
|
|
124
|
+
toolName,
|
|
125
|
+
input: JSON.stringify(parameters),
|
|
126
|
+
providerMetadata: {
|
|
127
|
+
[providerOptionsName]: {
|
|
128
|
+
parallelToolCall: {
|
|
129
|
+
itemId,
|
|
130
|
+
toolCallId: toolCall.toolCallId,
|
|
131
|
+
toolName: toolCall.toolName,
|
|
132
|
+
input: toolCall.input,
|
|
133
|
+
index,
|
|
134
|
+
count: toolUses.length,
|
|
135
|
+
} satisfies ParallelToolCallMetadata,
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return expandedToolCalls;
|
|
142
|
+
}
|