@ai-sdk/openai 4.0.43 → 4.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.
- package/CHANGELOG.md +14 -0
- package/dist/index.js +524 -186
- package/dist/index.js.map +1 -1
- package/dist/internal/index.js +517 -181
- package/dist/internal/index.js.map +1 -1
- package/package.json +2 -2
- package/src/chat/openai-chat-language-model.ts +1 -1
- package/src/responses/convert-to-openai-responses-input.ts +333 -108
- package/src/responses/expand-parallel-tool-call.ts +142 -0
- package/src/responses/openai-responses-language-model.ts +180 -38
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/openai",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.45",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@ai-sdk/provider": "4.0.7",
|
|
39
|
-
"@ai-sdk/provider-utils": "5.0.
|
|
39
|
+
"@ai-sdk/provider-utils": "5.0.28"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/node": "22.19.19",
|
|
@@ -377,7 +377,7 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
|
|
|
377
377
|
for (const toolCall of choice.message.tool_calls ?? []) {
|
|
378
378
|
content.push({
|
|
379
379
|
type: 'tool-call' as const,
|
|
380
|
-
toolCallId: toolCall.id
|
|
380
|
+
toolCallId: toolCall.id || generateId(),
|
|
381
381
|
toolName: toolCall.function.name,
|
|
382
382
|
input: toolCall.function.arguments!,
|
|
383
383
|
});
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
UnsupportedFunctionalityError,
|
|
3
3
|
type LanguageModelV4Prompt,
|
|
4
|
+
type LanguageModelV4ToolResultOutput,
|
|
5
|
+
type LanguageModelV4ToolResultPart,
|
|
4
6
|
type SharedV4ProviderOptions,
|
|
5
7
|
type LanguageModelV4ToolApprovalResponsePart,
|
|
6
8
|
type SharedV4Warning,
|
|
@@ -43,6 +45,10 @@ import {
|
|
|
43
45
|
programmaticToolCallingInputSchema,
|
|
44
46
|
programmaticToolCallingOutputSchema,
|
|
45
47
|
} from '../tool/programmatic-tool-calling';
|
|
48
|
+
import {
|
|
49
|
+
getParallelToolCallMetadata,
|
|
50
|
+
type ParallelToolCallMetadata,
|
|
51
|
+
} from './expand-parallel-tool-call';
|
|
46
52
|
|
|
47
53
|
function serializeToolCallArguments(input: unknown): string {
|
|
48
54
|
return JSON.stringify(input === undefined ? {} : input);
|
|
@@ -61,6 +67,217 @@ function mapToolCaller(
|
|
|
61
67
|
: caller;
|
|
62
68
|
}
|
|
63
69
|
|
|
70
|
+
async function convertFunctionToolResultOutput({
|
|
71
|
+
output,
|
|
72
|
+
toolName,
|
|
73
|
+
outputSchemaToolNames,
|
|
74
|
+
providerOptionsName,
|
|
75
|
+
warnings,
|
|
76
|
+
}: {
|
|
77
|
+
output: LanguageModelV4ToolResultOutput;
|
|
78
|
+
toolName: string;
|
|
79
|
+
outputSchemaToolNames: Set<string> | undefined;
|
|
80
|
+
providerOptionsName: string;
|
|
81
|
+
warnings: Array<SharedV4Warning>;
|
|
82
|
+
}): Promise<OpenAIResponsesFunctionCallOutput['output']> {
|
|
83
|
+
// `output` is always a string, but for functions with output_schema OpenAI
|
|
84
|
+
// parses the contents of that string as JSON. Text-like results therefore
|
|
85
|
+
// need JSON.stringify to become valid JSON string literals.
|
|
86
|
+
const hasOutputSchema = outputSchemaToolNames?.has(toolName);
|
|
87
|
+
|
|
88
|
+
switch (output.type) {
|
|
89
|
+
case 'text':
|
|
90
|
+
case 'error-text':
|
|
91
|
+
return hasOutputSchema ? JSON.stringify(output.value) : output.value;
|
|
92
|
+
case 'execution-denied': {
|
|
93
|
+
const reason = output.reason ?? 'Tool call execution denied.';
|
|
94
|
+
return hasOutputSchema ? JSON.stringify(reason) : reason;
|
|
95
|
+
}
|
|
96
|
+
case 'json':
|
|
97
|
+
case 'error-json':
|
|
98
|
+
return JSON.stringify(output.value);
|
|
99
|
+
case 'content':
|
|
100
|
+
return output.value
|
|
101
|
+
.map(item => {
|
|
102
|
+
const promptCacheBreakpoint = getPromptCacheBreakpoint(
|
|
103
|
+
item.providerOptions,
|
|
104
|
+
providerOptionsName,
|
|
105
|
+
);
|
|
106
|
+
switch (item.type) {
|
|
107
|
+
case 'text': {
|
|
108
|
+
return {
|
|
109
|
+
type: 'input_text' as const,
|
|
110
|
+
text: item.text,
|
|
111
|
+
...(promptCacheBreakpoint != null && {
|
|
112
|
+
prompt_cache_breakpoint: promptCacheBreakpoint,
|
|
113
|
+
}),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
case 'file': {
|
|
118
|
+
const topLevel = getTopLevelMediaType(item.mediaType);
|
|
119
|
+
const imageDetail =
|
|
120
|
+
item.providerOptions?.[providerOptionsName]?.imageDetail;
|
|
121
|
+
|
|
122
|
+
if (item.data.type === 'data') {
|
|
123
|
+
const fullMediaType = resolveFullMediaType({ part: item });
|
|
124
|
+
if (topLevel === 'image') {
|
|
125
|
+
return {
|
|
126
|
+
type: 'input_image' as const,
|
|
127
|
+
image_url: `data:${fullMediaType};base64,${convertToBase64(item.data.data)}`,
|
|
128
|
+
detail: imageDetail,
|
|
129
|
+
...(promptCacheBreakpoint != null && {
|
|
130
|
+
prompt_cache_breakpoint: promptCacheBreakpoint,
|
|
131
|
+
}),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
type: 'input_file' as const,
|
|
136
|
+
filename: item.filename ?? 'data',
|
|
137
|
+
file_data: `data:${fullMediaType};base64,${convertToBase64(item.data.data)}`,
|
|
138
|
+
...(promptCacheBreakpoint != null && {
|
|
139
|
+
prompt_cache_breakpoint: promptCacheBreakpoint,
|
|
140
|
+
}),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (item.data.type === 'url') {
|
|
145
|
+
if (topLevel === 'image') {
|
|
146
|
+
return {
|
|
147
|
+
type: 'input_image' as const,
|
|
148
|
+
image_url: item.data.url.toString(),
|
|
149
|
+
detail: imageDetail,
|
|
150
|
+
...(promptCacheBreakpoint != null && {
|
|
151
|
+
prompt_cache_breakpoint: promptCacheBreakpoint,
|
|
152
|
+
}),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
type: 'input_file' as const,
|
|
157
|
+
file_url: item.data.url.toString(),
|
|
158
|
+
...(promptCacheBreakpoint != null && {
|
|
159
|
+
prompt_cache_breakpoint: promptCacheBreakpoint,
|
|
160
|
+
}),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
warnings.push({
|
|
165
|
+
type: 'other',
|
|
166
|
+
message: `unsupported tool content part type: ${item.type} with data type: ${item.data.type}`,
|
|
167
|
+
});
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
default: {
|
|
172
|
+
warnings.push({
|
|
173
|
+
type: 'other',
|
|
174
|
+
message: `unsupported tool content part type: ${item.type}`,
|
|
175
|
+
});
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
.filter(isNonNullable);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
type ParallelToolResultGroup = {
|
|
185
|
+
metadata: ParallelToolCallMetadata;
|
|
186
|
+
results: Array<LanguageModelV4ToolResultPart>;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
function hasSameParallelToolCall(
|
|
190
|
+
first: ParallelToolCallMetadata,
|
|
191
|
+
second: ParallelToolCallMetadata,
|
|
192
|
+
): boolean {
|
|
193
|
+
return (
|
|
194
|
+
first.itemId === second.itemId &&
|
|
195
|
+
first.toolCallId === second.toolCallId &&
|
|
196
|
+
first.toolName === second.toolName &&
|
|
197
|
+
first.input === second.input &&
|
|
198
|
+
first.count === second.count
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function collectCompleteParallelToolResultGroups({
|
|
203
|
+
prompt,
|
|
204
|
+
providerOptionsName,
|
|
205
|
+
}: {
|
|
206
|
+
prompt: LanguageModelV4Prompt;
|
|
207
|
+
providerOptionsName: string;
|
|
208
|
+
}): Map<string, ParallelToolResultGroup> {
|
|
209
|
+
const pendingGroups = new Map<
|
|
210
|
+
string,
|
|
211
|
+
{
|
|
212
|
+
metadata: ParallelToolCallMetadata;
|
|
213
|
+
results: Map<number, LanguageModelV4ToolResultPart>;
|
|
214
|
+
invalid: boolean;
|
|
215
|
+
}
|
|
216
|
+
>();
|
|
217
|
+
|
|
218
|
+
for (const message of prompt) {
|
|
219
|
+
if (message.role !== 'tool') {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
for (const part of message.content) {
|
|
224
|
+
if (part.type !== 'tool-result') {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const metadata = getParallelToolCallMetadata({
|
|
229
|
+
providerOptions: part.providerOptions,
|
|
230
|
+
providerOptionsName,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
if (metadata == null) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const existing = pendingGroups.get(metadata.toolCallId);
|
|
238
|
+
if (existing == null) {
|
|
239
|
+
pendingGroups.set(metadata.toolCallId, {
|
|
240
|
+
metadata,
|
|
241
|
+
results: new Map([[metadata.index, part]]),
|
|
242
|
+
invalid: false,
|
|
243
|
+
});
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (
|
|
248
|
+
!hasSameParallelToolCall(existing.metadata, metadata) ||
|
|
249
|
+
existing.results.has(metadata.index)
|
|
250
|
+
) {
|
|
251
|
+
existing.invalid = true;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
existing.results.set(metadata.index, part);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const completeGroups = new Map<string, ParallelToolResultGroup>();
|
|
260
|
+
|
|
261
|
+
for (const [toolCallId, group] of pendingGroups) {
|
|
262
|
+
if (group.invalid || group.results.size !== group.metadata.count) {
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const results = Array.from({ length: group.metadata.count }, (_, index) =>
|
|
267
|
+
group.results.get(index),
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
if (results.every(isNonNullable)) {
|
|
271
|
+
completeGroups.set(toolCallId, {
|
|
272
|
+
metadata: group.metadata,
|
|
273
|
+
results,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return completeGroups;
|
|
279
|
+
}
|
|
280
|
+
|
|
64
281
|
type OpenAIPromptCacheBreakpoint = { mode: 'explicit' };
|
|
65
282
|
|
|
66
283
|
function getPromptCacheBreakpoint(
|
|
@@ -123,6 +340,15 @@ export async function convertToOpenAIResponsesInput({
|
|
|
123
340
|
let input: OpenAIResponsesInput = [];
|
|
124
341
|
const warnings: Array<SharedV4Warning> = [];
|
|
125
342
|
const processedApprovalIds = new Set<string>();
|
|
343
|
+
const parallelToolResultGroups =
|
|
344
|
+
hasConversation || hasPreviousResponseId
|
|
345
|
+
? collectCompleteParallelToolResultGroups({
|
|
346
|
+
prompt,
|
|
347
|
+
providerOptionsName,
|
|
348
|
+
})
|
|
349
|
+
: new Map<string, ParallelToolResultGroup>();
|
|
350
|
+
const emittedParallelToolCalls = new Set<string>();
|
|
351
|
+
const emittedParallelToolResults = new Set<string>();
|
|
126
352
|
|
|
127
353
|
for (const { role, content, providerOptions } of prompt) {
|
|
128
354
|
switch (role) {
|
|
@@ -348,6 +574,49 @@ export async function convertToOpenAIResponsesInput({
|
|
|
348
574
|
break;
|
|
349
575
|
}
|
|
350
576
|
case 'tool-call': {
|
|
577
|
+
const parallelToolCallMetadata = getParallelToolCallMetadata({
|
|
578
|
+
providerOptions: part.providerOptions,
|
|
579
|
+
providerOptionsName,
|
|
580
|
+
});
|
|
581
|
+
const parallelToolResultGroup =
|
|
582
|
+
parallelToolCallMetadata == null
|
|
583
|
+
? undefined
|
|
584
|
+
: parallelToolResultGroups.get(
|
|
585
|
+
parallelToolCallMetadata.toolCallId,
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
if (
|
|
589
|
+
parallelToolCallMetadata != null &&
|
|
590
|
+
parallelToolResultGroup != null &&
|
|
591
|
+
hasSameParallelToolCall(
|
|
592
|
+
parallelToolResultGroup.metadata,
|
|
593
|
+
parallelToolCallMetadata,
|
|
594
|
+
)
|
|
595
|
+
) {
|
|
596
|
+
if (
|
|
597
|
+
!emittedParallelToolCalls.has(
|
|
598
|
+
parallelToolResultGroup.metadata.toolCallId,
|
|
599
|
+
)
|
|
600
|
+
) {
|
|
601
|
+
emittedParallelToolCalls.add(
|
|
602
|
+
parallelToolResultGroup.metadata.toolCallId,
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
// Conversations already contain the original wrapper item.
|
|
606
|
+
// previousResponseId chains require plain client function
|
|
607
|
+
// calls to be reconstructed in full.
|
|
608
|
+
if (!hasConversation) {
|
|
609
|
+
input.push({
|
|
610
|
+
type: 'function_call',
|
|
611
|
+
call_id: parallelToolResultGroup.metadata.toolCallId,
|
|
612
|
+
name: parallelToolResultGroup.metadata.toolName,
|
|
613
|
+
arguments: parallelToolResultGroup.metadata.input,
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
|
|
351
620
|
const id = (part.providerOptions?.[providerOptionsName]?.itemId ??
|
|
352
621
|
(
|
|
353
622
|
part as {
|
|
@@ -918,6 +1187,63 @@ export async function convertToOpenAIResponsesInput({
|
|
|
918
1187
|
continue;
|
|
919
1188
|
}
|
|
920
1189
|
|
|
1190
|
+
const parallelToolCallMetadata = getParallelToolCallMetadata({
|
|
1191
|
+
providerOptions: part.providerOptions,
|
|
1192
|
+
providerOptionsName,
|
|
1193
|
+
});
|
|
1194
|
+
const parallelToolResultGroup =
|
|
1195
|
+
parallelToolCallMetadata == null
|
|
1196
|
+
? undefined
|
|
1197
|
+
: parallelToolResultGroups.get(
|
|
1198
|
+
parallelToolCallMetadata.toolCallId,
|
|
1199
|
+
);
|
|
1200
|
+
|
|
1201
|
+
if (
|
|
1202
|
+
parallelToolCallMetadata != null &&
|
|
1203
|
+
parallelToolResultGroup != null &&
|
|
1204
|
+
hasSameParallelToolCall(
|
|
1205
|
+
parallelToolResultGroup.metadata,
|
|
1206
|
+
parallelToolCallMetadata,
|
|
1207
|
+
)
|
|
1208
|
+
) {
|
|
1209
|
+
if (
|
|
1210
|
+
!emittedParallelToolResults.has(
|
|
1211
|
+
parallelToolResultGroup.metadata.toolCallId,
|
|
1212
|
+
)
|
|
1213
|
+
) {
|
|
1214
|
+
emittedParallelToolResults.add(
|
|
1215
|
+
parallelToolResultGroup.metadata.toolCallId,
|
|
1216
|
+
);
|
|
1217
|
+
|
|
1218
|
+
const toolOutputs = await Promise.all(
|
|
1219
|
+
parallelToolResultGroup.results.map(async result =>
|
|
1220
|
+
convertFunctionToolResultOutput({
|
|
1221
|
+
output: result.output,
|
|
1222
|
+
toolName: result.toolName,
|
|
1223
|
+
outputSchemaToolNames,
|
|
1224
|
+
providerOptionsName,
|
|
1225
|
+
warnings,
|
|
1226
|
+
}),
|
|
1227
|
+
),
|
|
1228
|
+
);
|
|
1229
|
+
|
|
1230
|
+
input.push({
|
|
1231
|
+
type: 'function_call_output',
|
|
1232
|
+
call_id: parallelToolResultGroup.metadata.toolCallId,
|
|
1233
|
+
// The internal wrapper returns one output containing the child
|
|
1234
|
+
// results in the same order as the original tool_uses array.
|
|
1235
|
+
output: toolOutputs
|
|
1236
|
+
.map(output =>
|
|
1237
|
+
typeof output === 'string'
|
|
1238
|
+
? output
|
|
1239
|
+
: JSON.stringify(output),
|
|
1240
|
+
)
|
|
1241
|
+
.join('\n'),
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
continue;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
921
1247
|
const output = part.output;
|
|
922
1248
|
|
|
923
1249
|
// Skip execution-denied with approvalId - already handled via tool-approval-response
|
|
@@ -1152,114 +1478,13 @@ export async function convertToOpenAIResponsesInput({
|
|
|
1152
1478
|
continue;
|
|
1153
1479
|
}
|
|
1154
1480
|
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
case 'error-text':
|
|
1163
|
-
contentValue = hasOutputSchema
|
|
1164
|
-
? JSON.stringify(output.value)
|
|
1165
|
-
: output.value;
|
|
1166
|
-
break;
|
|
1167
|
-
case 'execution-denied': {
|
|
1168
|
-
const reason = output.reason ?? 'Tool call execution denied.';
|
|
1169
|
-
contentValue = hasOutputSchema ? JSON.stringify(reason) : reason;
|
|
1170
|
-
break;
|
|
1171
|
-
}
|
|
1172
|
-
case 'json':
|
|
1173
|
-
case 'error-json':
|
|
1174
|
-
contentValue = JSON.stringify(output.value);
|
|
1175
|
-
break;
|
|
1176
|
-
case 'content':
|
|
1177
|
-
contentValue = output.value
|
|
1178
|
-
.map(item => {
|
|
1179
|
-
const promptCacheBreakpoint = getPromptCacheBreakpoint(
|
|
1180
|
-
item.providerOptions,
|
|
1181
|
-
providerOptionsName,
|
|
1182
|
-
);
|
|
1183
|
-
switch (item.type) {
|
|
1184
|
-
case 'text': {
|
|
1185
|
-
return {
|
|
1186
|
-
type: 'input_text' as const,
|
|
1187
|
-
text: item.text,
|
|
1188
|
-
...(promptCacheBreakpoint != null && {
|
|
1189
|
-
prompt_cache_breakpoint: promptCacheBreakpoint,
|
|
1190
|
-
}),
|
|
1191
|
-
};
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
case 'file': {
|
|
1195
|
-
const topLevel = getTopLevelMediaType(item.mediaType);
|
|
1196
|
-
const imageDetail =
|
|
1197
|
-
item.providerOptions?.[providerOptionsName]
|
|
1198
|
-
?.imageDetail;
|
|
1199
|
-
|
|
1200
|
-
if (item.data.type === 'data') {
|
|
1201
|
-
const fullMediaType = resolveFullMediaType({
|
|
1202
|
-
part: item,
|
|
1203
|
-
});
|
|
1204
|
-
if (topLevel === 'image') {
|
|
1205
|
-
return {
|
|
1206
|
-
type: 'input_image' as const,
|
|
1207
|
-
image_url: `data:${fullMediaType};base64,${convertToBase64(item.data.data)}`,
|
|
1208
|
-
detail: imageDetail,
|
|
1209
|
-
...(promptCacheBreakpoint != null && {
|
|
1210
|
-
prompt_cache_breakpoint: promptCacheBreakpoint,
|
|
1211
|
-
}),
|
|
1212
|
-
};
|
|
1213
|
-
}
|
|
1214
|
-
return {
|
|
1215
|
-
type: 'input_file' as const,
|
|
1216
|
-
filename: item.filename ?? 'data',
|
|
1217
|
-
file_data: `data:${fullMediaType};base64,${convertToBase64(item.data.data)}`,
|
|
1218
|
-
...(promptCacheBreakpoint != null && {
|
|
1219
|
-
prompt_cache_breakpoint: promptCacheBreakpoint,
|
|
1220
|
-
}),
|
|
1221
|
-
};
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
if (item.data.type === 'url') {
|
|
1225
|
-
if (topLevel === 'image') {
|
|
1226
|
-
return {
|
|
1227
|
-
type: 'input_image' as const,
|
|
1228
|
-
image_url: item.data.url.toString(),
|
|
1229
|
-
detail: imageDetail,
|
|
1230
|
-
...(promptCacheBreakpoint != null && {
|
|
1231
|
-
prompt_cache_breakpoint: promptCacheBreakpoint,
|
|
1232
|
-
}),
|
|
1233
|
-
};
|
|
1234
|
-
}
|
|
1235
|
-
return {
|
|
1236
|
-
type: 'input_file' as const,
|
|
1237
|
-
file_url: item.data.url.toString(),
|
|
1238
|
-
...(promptCacheBreakpoint != null && {
|
|
1239
|
-
prompt_cache_breakpoint: promptCacheBreakpoint,
|
|
1240
|
-
}),
|
|
1241
|
-
};
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
warnings.push({
|
|
1245
|
-
type: 'other',
|
|
1246
|
-
message: `unsupported tool content part type: ${item.type} with data type: ${item.data.type}`,
|
|
1247
|
-
});
|
|
1248
|
-
return undefined;
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
|
-
default: {
|
|
1252
|
-
warnings.push({
|
|
1253
|
-
type: 'other',
|
|
1254
|
-
message: `unsupported tool content part type: ${item.type}`,
|
|
1255
|
-
});
|
|
1256
|
-
return undefined;
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
})
|
|
1260
|
-
.filter(isNonNullable);
|
|
1261
|
-
break;
|
|
1262
|
-
}
|
|
1481
|
+
const contentValue = await convertFunctionToolResultOutput({
|
|
1482
|
+
output,
|
|
1483
|
+
toolName: part.toolName,
|
|
1484
|
+
outputSchemaToolNames,
|
|
1485
|
+
providerOptionsName,
|
|
1486
|
+
warnings,
|
|
1487
|
+
});
|
|
1263
1488
|
|
|
1264
1489
|
const caller = mapToolCaller(
|
|
1265
1490
|
part.providerOptions?.[providerOptionsName]?.caller as
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isJSONObject,
|
|
3
|
+
type LanguageModelV4FunctionTool,
|
|
4
|
+
type LanguageModelV4ToolCall,
|
|
5
|
+
type SharedV4ProviderOptions,
|
|
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: SharedV4ProviderOptions | 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<LanguageModelV4FunctionTool>;
|
|
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<LanguageModelV4ToolCall, 'toolCallId' | 'toolName' | 'input'>;
|
|
78
|
+
tools: Array<LanguageModelV4FunctionTool>;
|
|
79
|
+
providerOptionsName: string;
|
|
80
|
+
itemId: string;
|
|
81
|
+
}): Promise<Array<LanguageModelV4ToolCall> | 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<LanguageModelV4ToolCall> = [];
|
|
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
|
+
}
|