@ai-sdk/provider-utils 5.0.0-beta.23 → 5.0.0-beta.25
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 +12 -0
- package/dist/index.d.ts +80 -2
- package/dist/index.js +142 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/filter-nullable.ts +11 -0
- package/src/index.ts +6 -0
- package/src/streaming-tool-call-tracker.ts +243 -0
package/package.json
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filters `null` and `undefined` values out of a list of values.
|
|
3
|
+
*
|
|
4
|
+
* @param values - The values to filter.
|
|
5
|
+
* @returns A new array containing only non-nullish values.
|
|
6
|
+
*/
|
|
7
|
+
export function filterNullable<T>(
|
|
8
|
+
...values: Array<T | undefined | null>
|
|
9
|
+
): Array<T> {
|
|
10
|
+
return values.filter((value): value is NonNullable<T> => value != null);
|
|
11
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ export { downloadBlob } from './download-blob';
|
|
|
14
14
|
export { DownloadError } from './download-error';
|
|
15
15
|
export * from './extract-response-headers';
|
|
16
16
|
export * from './fetch-function';
|
|
17
|
+
export { filterNullable } from './filter-nullable';
|
|
17
18
|
export { createIdGenerator, generateId, type IdGenerator } from './generate-id';
|
|
18
19
|
export * from './get-error-message';
|
|
19
20
|
export * from './get-from-api';
|
|
@@ -65,6 +66,11 @@ export {
|
|
|
65
66
|
type ValidationResult,
|
|
66
67
|
} from './schema';
|
|
67
68
|
export { serializeModelOptions } from './serialize-model-options';
|
|
69
|
+
export {
|
|
70
|
+
StreamingToolCallTracker,
|
|
71
|
+
type StreamingToolCallDelta,
|
|
72
|
+
type StreamingToolCallTrackerOptions,
|
|
73
|
+
} from './streaming-tool-call-tracker';
|
|
68
74
|
export { stripFileExtension } from './strip-file-extension';
|
|
69
75
|
export * from './uint8-utils';
|
|
70
76
|
export { validateDownloadUrl } from './validate-download-url';
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import {
|
|
2
|
+
InvalidResponseDataError,
|
|
3
|
+
LanguageModelV4StreamPart,
|
|
4
|
+
SharedV4ProviderMetadata,
|
|
5
|
+
} from '@ai-sdk/provider';
|
|
6
|
+
import { generateId as defaultGenerateId } from './generate-id';
|
|
7
|
+
import { isParsableJson } from './parse-json';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Minimal interface for a streaming tool call delta from an OpenAI-compatible API.
|
|
11
|
+
*/
|
|
12
|
+
export interface StreamingToolCallDelta {
|
|
13
|
+
index?: number | null;
|
|
14
|
+
id?: string | null;
|
|
15
|
+
type?: string | null;
|
|
16
|
+
function?: {
|
|
17
|
+
name?: string | null;
|
|
18
|
+
arguments?: string | null;
|
|
19
|
+
} | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface StreamingToolCallTrackerOptions {
|
|
23
|
+
/**
|
|
24
|
+
* ID generator function for tool call IDs.
|
|
25
|
+
* Defaults to the standard generateId.
|
|
26
|
+
*/
|
|
27
|
+
generateId?: () => string;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* How to validate the `type` field on new tool call deltas.
|
|
31
|
+
* - `'none'`: no validation (default)
|
|
32
|
+
* - `'if-present'`: throw if type is present and not `'function'`
|
|
33
|
+
* - `'required'`: throw if type is not exactly `'function'`
|
|
34
|
+
*/
|
|
35
|
+
typeValidation?: 'none' | 'if-present' | 'required';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Extract provider-specific metadata from a tool call delta.
|
|
39
|
+
* Called once when a new tool call is detected.
|
|
40
|
+
* The returned metadata is stored on the tool call and passed to
|
|
41
|
+
* `buildToolCallProviderMetadata` when the tool call is finalized.
|
|
42
|
+
*/
|
|
43
|
+
extractMetadata?: (
|
|
44
|
+
delta: StreamingToolCallDelta,
|
|
45
|
+
) => SharedV4ProviderMetadata | undefined;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Build the `providerMetadata` object for a `tool-call` event.
|
|
49
|
+
* Receives the metadata previously extracted via `extractMetadata`.
|
|
50
|
+
* If `undefined` is returned, no `providerMetadata` is included in the event.
|
|
51
|
+
*/
|
|
52
|
+
buildToolCallProviderMetadata?: (
|
|
53
|
+
metadata: SharedV4ProviderMetadata | undefined,
|
|
54
|
+
) => SharedV4ProviderMetadata | undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface TrackedToolCall {
|
|
58
|
+
id: string;
|
|
59
|
+
type: 'function';
|
|
60
|
+
function: { name: string; arguments: string };
|
|
61
|
+
hasFinished: boolean;
|
|
62
|
+
metadata?: SharedV4ProviderMetadata;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Tracks streaming tool call state across multiple deltas from an
|
|
67
|
+
* OpenAI-compatible chat completion stream. Handles argument accumulation,
|
|
68
|
+
* emits tool-input-start/delta/end and tool-call events, and finalizes
|
|
69
|
+
* unfinished tool calls on flush.
|
|
70
|
+
*
|
|
71
|
+
* Used by openai, openai-compatible, groq, deepseek, and alibaba providers.
|
|
72
|
+
*/
|
|
73
|
+
export class StreamingToolCallTracker {
|
|
74
|
+
private toolCalls: TrackedToolCall[] = [];
|
|
75
|
+
private readonly _generateId: () => string;
|
|
76
|
+
private readonly typeValidation: 'none' | 'if-present' | 'required';
|
|
77
|
+
private readonly extractMetadata?: (
|
|
78
|
+
delta: StreamingToolCallDelta,
|
|
79
|
+
) => SharedV4ProviderMetadata | undefined;
|
|
80
|
+
private readonly buildToolCallProviderMetadata?: (
|
|
81
|
+
metadata: SharedV4ProviderMetadata | undefined,
|
|
82
|
+
) => SharedV4ProviderMetadata | undefined;
|
|
83
|
+
|
|
84
|
+
constructor(options: StreamingToolCallTrackerOptions = {}) {
|
|
85
|
+
this._generateId = options.generateId ?? defaultGenerateId;
|
|
86
|
+
this.typeValidation = options.typeValidation ?? 'none';
|
|
87
|
+
this.extractMetadata = options.extractMetadata;
|
|
88
|
+
this.buildToolCallProviderMetadata = options.buildToolCallProviderMetadata;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Process a tool call delta from a streaming response chunk.
|
|
93
|
+
* Emits tool-input-start, tool-input-delta, tool-input-end, and tool-call
|
|
94
|
+
* events as appropriate.
|
|
95
|
+
*/
|
|
96
|
+
processDelta(
|
|
97
|
+
toolCallDelta: StreamingToolCallDelta,
|
|
98
|
+
enqueue: (part: LanguageModelV4StreamPart) => void,
|
|
99
|
+
): void {
|
|
100
|
+
const index = toolCallDelta.index ?? this.toolCalls.length;
|
|
101
|
+
|
|
102
|
+
if (this.toolCalls[index] == null) {
|
|
103
|
+
this.processNewToolCall(index, toolCallDelta, enqueue);
|
|
104
|
+
} else {
|
|
105
|
+
this.processExistingToolCall(index, toolCallDelta, enqueue);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Finalize any unfinished tool calls. Should be called during the stream's
|
|
111
|
+
* flush handler to ensure all tool calls are properly completed.
|
|
112
|
+
*/
|
|
113
|
+
flush(enqueue: (part: LanguageModelV4StreamPart) => void): void {
|
|
114
|
+
for (const toolCall of this.toolCalls) {
|
|
115
|
+
if (!toolCall.hasFinished) {
|
|
116
|
+
this.finishToolCall(toolCall, enqueue);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private processNewToolCall(
|
|
122
|
+
index: number,
|
|
123
|
+
toolCallDelta: StreamingToolCallDelta,
|
|
124
|
+
enqueue: (part: LanguageModelV4StreamPart) => void,
|
|
125
|
+
): void {
|
|
126
|
+
if (this.typeValidation === 'required') {
|
|
127
|
+
if (toolCallDelta.type !== 'function') {
|
|
128
|
+
throw new InvalidResponseDataError({
|
|
129
|
+
data: toolCallDelta,
|
|
130
|
+
message: `Expected 'function' type.`,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
} else if (this.typeValidation === 'if-present') {
|
|
134
|
+
if (toolCallDelta.type != null && toolCallDelta.type !== 'function') {
|
|
135
|
+
throw new InvalidResponseDataError({
|
|
136
|
+
data: toolCallDelta,
|
|
137
|
+
message: `Expected 'function' type.`,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (toolCallDelta.id == null) {
|
|
143
|
+
throw new InvalidResponseDataError({
|
|
144
|
+
data: toolCallDelta,
|
|
145
|
+
message: `Expected 'id' to be a string.`,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (toolCallDelta.function?.name == null) {
|
|
150
|
+
throw new InvalidResponseDataError({
|
|
151
|
+
data: toolCallDelta,
|
|
152
|
+
message: `Expected 'function.name' to be a string.`,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
enqueue({
|
|
157
|
+
type: 'tool-input-start',
|
|
158
|
+
id: toolCallDelta.id,
|
|
159
|
+
toolName: toolCallDelta.function.name,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const metadata = this.extractMetadata?.(toolCallDelta);
|
|
163
|
+
|
|
164
|
+
this.toolCalls[index] = {
|
|
165
|
+
id: toolCallDelta.id,
|
|
166
|
+
type: 'function',
|
|
167
|
+
function: {
|
|
168
|
+
name: toolCallDelta.function.name,
|
|
169
|
+
arguments: toolCallDelta.function.arguments ?? '',
|
|
170
|
+
},
|
|
171
|
+
hasFinished: false,
|
|
172
|
+
metadata,
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const toolCall = this.toolCalls[index];
|
|
176
|
+
|
|
177
|
+
// Emit initial delta if arguments already present
|
|
178
|
+
if (toolCall.function.arguments.length > 0) {
|
|
179
|
+
enqueue({
|
|
180
|
+
type: 'tool-input-delta',
|
|
181
|
+
id: toolCall.id,
|
|
182
|
+
delta: toolCall.function.arguments,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Check if tool call is complete
|
|
187
|
+
// (some providers send the full tool call in one chunk)
|
|
188
|
+
if (isParsableJson(toolCall.function.arguments)) {
|
|
189
|
+
this.finishToolCall(toolCall, enqueue);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private processExistingToolCall(
|
|
194
|
+
index: number,
|
|
195
|
+
toolCallDelta: StreamingToolCallDelta,
|
|
196
|
+
enqueue: (part: LanguageModelV4StreamPart) => void,
|
|
197
|
+
): void {
|
|
198
|
+
const toolCall = this.toolCalls[index];
|
|
199
|
+
|
|
200
|
+
if (toolCall.hasFinished) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (toolCallDelta.function?.arguments != null) {
|
|
205
|
+
toolCall.function.arguments += toolCallDelta.function.arguments;
|
|
206
|
+
|
|
207
|
+
enqueue({
|
|
208
|
+
type: 'tool-input-delta',
|
|
209
|
+
id: toolCall.id,
|
|
210
|
+
delta: toolCallDelta.function.arguments,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Check if tool call is complete
|
|
215
|
+
if (isParsableJson(toolCall.function.arguments)) {
|
|
216
|
+
this.finishToolCall(toolCall, enqueue);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private finishToolCall(
|
|
221
|
+
toolCall: TrackedToolCall,
|
|
222
|
+
enqueue: (part: LanguageModelV4StreamPart) => void,
|
|
223
|
+
): void {
|
|
224
|
+
enqueue({
|
|
225
|
+
type: 'tool-input-end',
|
|
226
|
+
id: toolCall.id,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const providerMetadata = this.buildToolCallProviderMetadata?.(
|
|
230
|
+
toolCall.metadata,
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
enqueue({
|
|
234
|
+
type: 'tool-call',
|
|
235
|
+
toolCallId: toolCall.id ?? this._generateId(),
|
|
236
|
+
toolName: toolCall.function.name,
|
|
237
|
+
input: toolCall.function.arguments,
|
|
238
|
+
...(providerMetadata ? { providerMetadata } : {}),
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
toolCall.hasFinished = true;
|
|
242
|
+
}
|
|
243
|
+
}
|