@providerprotocol/ai 0.0.11 → 0.0.12
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/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -10
- package/src/anthropic/index.ts +0 -3
- package/src/core/image.ts +0 -188
- package/src/core/llm.ts +0 -650
- package/src/core/provider.ts +0 -92
- package/src/google/index.ts +0 -3
- package/src/http/errors.ts +0 -112
- package/src/http/fetch.ts +0 -210
- package/src/http/index.ts +0 -31
- package/src/http/keys.ts +0 -136
- package/src/http/retry.ts +0 -205
- package/src/http/sse.ts +0 -136
- package/src/index.ts +0 -32
- package/src/ollama/index.ts +0 -3
- package/src/openai/index.ts +0 -39
- package/src/openrouter/index.ts +0 -11
- package/src/providers/anthropic/index.ts +0 -17
- package/src/providers/anthropic/llm.ts +0 -196
- package/src/providers/anthropic/transform.ts +0 -434
- package/src/providers/anthropic/types.ts +0 -213
- package/src/providers/google/index.ts +0 -17
- package/src/providers/google/llm.ts +0 -203
- package/src/providers/google/transform.ts +0 -447
- package/src/providers/google/types.ts +0 -214
- package/src/providers/ollama/index.ts +0 -43
- package/src/providers/ollama/llm.ts +0 -272
- package/src/providers/ollama/transform.ts +0 -434
- package/src/providers/ollama/types.ts +0 -260
- package/src/providers/openai/index.ts +0 -186
- package/src/providers/openai/llm.completions.ts +0 -201
- package/src/providers/openai/llm.responses.ts +0 -211
- package/src/providers/openai/transform.completions.ts +0 -561
- package/src/providers/openai/transform.responses.ts +0 -708
- package/src/providers/openai/types.ts +0 -1249
- package/src/providers/openrouter/index.ts +0 -177
- package/src/providers/openrouter/llm.completions.ts +0 -201
- package/src/providers/openrouter/llm.responses.ts +0 -211
- package/src/providers/openrouter/transform.completions.ts +0 -538
- package/src/providers/openrouter/transform.responses.ts +0 -742
- package/src/providers/openrouter/types.ts +0 -717
- package/src/providers/xai/index.ts +0 -223
- package/src/providers/xai/llm.completions.ts +0 -201
- package/src/providers/xai/llm.messages.ts +0 -195
- package/src/providers/xai/llm.responses.ts +0 -211
- package/src/providers/xai/transform.completions.ts +0 -565
- package/src/providers/xai/transform.messages.ts +0 -448
- package/src/providers/xai/transform.responses.ts +0 -678
- package/src/providers/xai/types.ts +0 -938
- package/src/types/content.ts +0 -133
- package/src/types/errors.ts +0 -85
- package/src/types/index.ts +0 -105
- package/src/types/llm.ts +0 -211
- package/src/types/messages.ts +0 -205
- package/src/types/provider.ts +0 -195
- package/src/types/schema.ts +0 -58
- package/src/types/stream.ts +0 -188
- package/src/types/thread.ts +0 -226
- package/src/types/tool.ts +0 -88
- package/src/types/turn.ts +0 -118
- package/src/utils/id.ts +0 -28
- package/src/xai/index.ts +0 -41
package/src/core/llm.ts
DELETED
|
@@ -1,650 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
LLMOptions,
|
|
3
|
-
LLMInstance,
|
|
4
|
-
LLMRequest,
|
|
5
|
-
LLMResponse,
|
|
6
|
-
InferenceInput,
|
|
7
|
-
BoundLLMModel,
|
|
8
|
-
LLMCapabilities,
|
|
9
|
-
} from '../types/llm.ts';
|
|
10
|
-
import type { UserMessage, AssistantMessage } from '../types/messages.ts';
|
|
11
|
-
import type { ContentBlock, TextBlock } from '../types/content.ts';
|
|
12
|
-
import type { Tool, ToolExecution, ToolResult } from '../types/tool.ts';
|
|
13
|
-
import type { Turn, TokenUsage } from '../types/turn.ts';
|
|
14
|
-
import type { StreamResult, StreamEvent } from '../types/stream.ts';
|
|
15
|
-
import type { Thread } from '../types/thread.ts';
|
|
16
|
-
import type { ProviderConfig } from '../types/provider.ts';
|
|
17
|
-
import { UPPError } from '../types/errors.ts';
|
|
18
|
-
import {
|
|
19
|
-
Message,
|
|
20
|
-
UserMessage as UserMessageClass,
|
|
21
|
-
ToolResultMessage,
|
|
22
|
-
isUserMessage,
|
|
23
|
-
isAssistantMessage,
|
|
24
|
-
} from '../types/messages.ts';
|
|
25
|
-
import { createTurn, aggregateUsage, emptyUsage } from '../types/turn.ts';
|
|
26
|
-
import {
|
|
27
|
-
createStreamResult,
|
|
28
|
-
toolExecutionStart,
|
|
29
|
-
toolExecutionEnd,
|
|
30
|
-
} from '../types/stream.ts';
|
|
31
|
-
import { generateShortId } from '../utils/id.ts';
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Default maximum iterations for tool execution
|
|
35
|
-
*/
|
|
36
|
-
const DEFAULT_MAX_ITERATIONS = 10;
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Create an LLM instance
|
|
40
|
-
*/
|
|
41
|
-
export function llm<TParams = unknown>(
|
|
42
|
-
options: LLMOptions<TParams>
|
|
43
|
-
): LLMInstance<TParams> {
|
|
44
|
-
const { model: modelRef, config = {}, params, system, tools, toolStrategy, structure } = options;
|
|
45
|
-
|
|
46
|
-
// Validate that the provider supports LLM
|
|
47
|
-
const provider = modelRef.provider;
|
|
48
|
-
if (!provider.modalities.llm) {
|
|
49
|
-
throw new UPPError(
|
|
50
|
-
`Provider '${provider.name}' does not support LLM modality`,
|
|
51
|
-
'INVALID_REQUEST',
|
|
52
|
-
provider.name,
|
|
53
|
-
'llm'
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Bind the model
|
|
58
|
-
const boundModel = provider.modalities.llm.bind(modelRef.modelId) as BoundLLMModel<TParams>;
|
|
59
|
-
|
|
60
|
-
// Validate capabilities at bind time
|
|
61
|
-
const capabilities = boundModel.capabilities;
|
|
62
|
-
|
|
63
|
-
// Check for structured output capability
|
|
64
|
-
if (structure && !capabilities.structuredOutput) {
|
|
65
|
-
throw new UPPError(
|
|
66
|
-
`Provider '${provider.name}' does not support structured output`,
|
|
67
|
-
'INVALID_REQUEST',
|
|
68
|
-
provider.name,
|
|
69
|
-
'llm'
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Check for tools capability
|
|
74
|
-
if (tools && tools.length > 0 && !capabilities.tools) {
|
|
75
|
-
throw new UPPError(
|
|
76
|
-
`Provider '${provider.name}' does not support tools`,
|
|
77
|
-
'INVALID_REQUEST',
|
|
78
|
-
provider.name,
|
|
79
|
-
'llm'
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Build the instance
|
|
84
|
-
const instance: LLMInstance<TParams> = {
|
|
85
|
-
model: boundModel,
|
|
86
|
-
system,
|
|
87
|
-
params,
|
|
88
|
-
capabilities,
|
|
89
|
-
|
|
90
|
-
async generate(
|
|
91
|
-
historyOrInput: Message[] | Thread | InferenceInput,
|
|
92
|
-
...inputs: InferenceInput[]
|
|
93
|
-
): Promise<Turn> {
|
|
94
|
-
const { history, messages } = parseInputs(historyOrInput, inputs);
|
|
95
|
-
return executeGenerate(
|
|
96
|
-
boundModel,
|
|
97
|
-
config,
|
|
98
|
-
system,
|
|
99
|
-
params,
|
|
100
|
-
tools,
|
|
101
|
-
toolStrategy,
|
|
102
|
-
structure,
|
|
103
|
-
history,
|
|
104
|
-
messages
|
|
105
|
-
);
|
|
106
|
-
},
|
|
107
|
-
|
|
108
|
-
stream(
|
|
109
|
-
historyOrInput: Message[] | Thread | InferenceInput,
|
|
110
|
-
...inputs: InferenceInput[]
|
|
111
|
-
): StreamResult {
|
|
112
|
-
// Check streaming capability
|
|
113
|
-
if (!capabilities.streaming) {
|
|
114
|
-
throw new UPPError(
|
|
115
|
-
`Provider '${provider.name}' does not support streaming`,
|
|
116
|
-
'INVALID_REQUEST',
|
|
117
|
-
provider.name,
|
|
118
|
-
'llm'
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
const { history, messages } = parseInputs(historyOrInput, inputs);
|
|
122
|
-
return executeStream(
|
|
123
|
-
boundModel,
|
|
124
|
-
config,
|
|
125
|
-
system,
|
|
126
|
-
params,
|
|
127
|
-
tools,
|
|
128
|
-
toolStrategy,
|
|
129
|
-
structure,
|
|
130
|
-
history,
|
|
131
|
-
messages
|
|
132
|
-
);
|
|
133
|
-
},
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
return instance;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Type guard to check if a value is a Message instance.
|
|
141
|
-
* Uses instanceof for class instances, with fallback to timestamp check
|
|
142
|
-
* for deserialized/reconstructed Message objects.
|
|
143
|
-
*/
|
|
144
|
-
function isMessageInstance(value: unknown): value is Message {
|
|
145
|
-
if (value instanceof Message) {
|
|
146
|
-
return true;
|
|
147
|
-
}
|
|
148
|
-
// Fallback for deserialized Messages that aren't class instances:
|
|
149
|
-
// Messages have 'timestamp' (Date), ContentBlocks don't
|
|
150
|
-
if (
|
|
151
|
-
typeof value === 'object' &&
|
|
152
|
-
value !== null &&
|
|
153
|
-
'timestamp' in value &&
|
|
154
|
-
'type' in value &&
|
|
155
|
-
'id' in value
|
|
156
|
-
) {
|
|
157
|
-
const obj = value as Record<string, unknown>;
|
|
158
|
-
// Message types are 'user', 'assistant', 'tool_result'
|
|
159
|
-
// ContentBlock types are 'text', 'image', 'audio', 'video', 'binary'
|
|
160
|
-
const messageTypes = ['user', 'assistant', 'tool_result'];
|
|
161
|
-
return messageTypes.includes(obj.type as string);
|
|
162
|
-
}
|
|
163
|
-
return false;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Parse inputs to determine history and new messages
|
|
168
|
-
*/
|
|
169
|
-
function parseInputs(
|
|
170
|
-
historyOrInput: Message[] | Thread | InferenceInput,
|
|
171
|
-
inputs: InferenceInput[]
|
|
172
|
-
): { history: Message[]; messages: Message[] } {
|
|
173
|
-
// Check if it's a Thread first (has 'messages' array property)
|
|
174
|
-
if (
|
|
175
|
-
typeof historyOrInput === 'object' &&
|
|
176
|
-
historyOrInput !== null &&
|
|
177
|
-
'messages' in historyOrInput &&
|
|
178
|
-
Array.isArray((historyOrInput as Thread).messages)
|
|
179
|
-
) {
|
|
180
|
-
const thread = historyOrInput as Thread;
|
|
181
|
-
const newMessages = inputs.map(inputToMessage);
|
|
182
|
-
return { history: [...thread.messages], messages: newMessages };
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Check if first arg is Message[] (history)
|
|
186
|
-
if (Array.isArray(historyOrInput)) {
|
|
187
|
-
// Empty array is empty history
|
|
188
|
-
if (historyOrInput.length === 0) {
|
|
189
|
-
const newMessages = inputs.map(inputToMessage);
|
|
190
|
-
return { history: [], messages: newMessages };
|
|
191
|
-
}
|
|
192
|
-
const first = historyOrInput[0];
|
|
193
|
-
if (isMessageInstance(first)) {
|
|
194
|
-
// It's history (Message[])
|
|
195
|
-
const newMessages = inputs.map(inputToMessage);
|
|
196
|
-
return { history: historyOrInput as Message[], messages: newMessages };
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// It's input (no history) - could be string, single Message, or ContentBlock
|
|
201
|
-
const allInputs = [historyOrInput as InferenceInput, ...inputs];
|
|
202
|
-
const newMessages = allInputs.map(inputToMessage);
|
|
203
|
-
return { history: [], messages: newMessages };
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Convert an InferenceInput to a Message
|
|
208
|
-
*/
|
|
209
|
-
function inputToMessage(input: InferenceInput): Message {
|
|
210
|
-
if (typeof input === 'string') {
|
|
211
|
-
return new UserMessageClass(input);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// It's already a Message
|
|
215
|
-
if ('type' in input && 'id' in input && 'timestamp' in input) {
|
|
216
|
-
return input as Message;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// It's a ContentBlock - wrap in UserMessage
|
|
220
|
-
const block = input as ContentBlock;
|
|
221
|
-
if (block.type === 'text') {
|
|
222
|
-
return new UserMessageClass((block as TextBlock).text);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
return new UserMessageClass([block as any]);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Execute a non-streaming generate call with tool loop
|
|
230
|
-
*/
|
|
231
|
-
async function executeGenerate<TParams>(
|
|
232
|
-
model: BoundLLMModel<TParams>,
|
|
233
|
-
config: ProviderConfig,
|
|
234
|
-
system: string | undefined,
|
|
235
|
-
params: TParams | undefined,
|
|
236
|
-
tools: Tool[] | undefined,
|
|
237
|
-
toolStrategy: LLMOptions<TParams>['toolStrategy'],
|
|
238
|
-
structure: LLMOptions<TParams>['structure'],
|
|
239
|
-
history: Message[],
|
|
240
|
-
newMessages: Message[]
|
|
241
|
-
): Promise<Turn> {
|
|
242
|
-
// Validate media capabilities for all input messages
|
|
243
|
-
validateMediaCapabilities(
|
|
244
|
-
[...history, ...newMessages],
|
|
245
|
-
model.capabilities,
|
|
246
|
-
model.provider.name
|
|
247
|
-
);
|
|
248
|
-
const maxIterations = toolStrategy?.maxIterations ?? DEFAULT_MAX_ITERATIONS;
|
|
249
|
-
const allMessages: Message[] = [...history, ...newMessages];
|
|
250
|
-
const toolExecutions: ToolExecution[] = [];
|
|
251
|
-
const usages: TokenUsage[] = [];
|
|
252
|
-
let cycles = 0;
|
|
253
|
-
|
|
254
|
-
// Track structured data from responses (providers handle extraction)
|
|
255
|
-
let structuredData: unknown;
|
|
256
|
-
|
|
257
|
-
// Tool loop
|
|
258
|
-
while (cycles < maxIterations + 1) {
|
|
259
|
-
cycles++;
|
|
260
|
-
|
|
261
|
-
const request: LLMRequest<TParams> = {
|
|
262
|
-
messages: allMessages,
|
|
263
|
-
system,
|
|
264
|
-
params,
|
|
265
|
-
tools,
|
|
266
|
-
structure,
|
|
267
|
-
config,
|
|
268
|
-
};
|
|
269
|
-
|
|
270
|
-
const response = await model.complete(request);
|
|
271
|
-
usages.push(response.usage);
|
|
272
|
-
allMessages.push(response.message);
|
|
273
|
-
|
|
274
|
-
// Track structured data from provider (if present)
|
|
275
|
-
if (response.data !== undefined) {
|
|
276
|
-
structuredData = response.data;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Check for tool calls
|
|
280
|
-
if (response.message.hasToolCalls && tools && tools.length > 0) {
|
|
281
|
-
// If provider already extracted structured data, don't try to execute tool calls
|
|
282
|
-
// (some providers use tool calls internally for structured output)
|
|
283
|
-
if (response.data !== undefined) {
|
|
284
|
-
break;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Check if we've hit max iterations (subtract 1 because we already incremented)
|
|
288
|
-
if (cycles >= maxIterations) {
|
|
289
|
-
await toolStrategy?.onMaxIterations?.(maxIterations);
|
|
290
|
-
throw new UPPError(
|
|
291
|
-
`Tool execution exceeded maximum iterations (${maxIterations})`,
|
|
292
|
-
'INVALID_REQUEST',
|
|
293
|
-
model.provider.name,
|
|
294
|
-
'llm'
|
|
295
|
-
);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// Execute tools
|
|
299
|
-
const results = await executeTools(
|
|
300
|
-
response.message,
|
|
301
|
-
tools,
|
|
302
|
-
toolStrategy,
|
|
303
|
-
toolExecutions
|
|
304
|
-
);
|
|
305
|
-
|
|
306
|
-
// Add tool results
|
|
307
|
-
allMessages.push(new ToolResultMessage(results));
|
|
308
|
-
|
|
309
|
-
continue;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// No tool calls - we're done
|
|
313
|
-
break;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// Use structured data from provider if structure was requested
|
|
317
|
-
const data = structure ? structuredData : undefined;
|
|
318
|
-
|
|
319
|
-
return createTurn(
|
|
320
|
-
allMessages.slice(history.length), // Only messages from this turn
|
|
321
|
-
toolExecutions,
|
|
322
|
-
aggregateUsage(usages),
|
|
323
|
-
cycles,
|
|
324
|
-
data
|
|
325
|
-
);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
/**
|
|
329
|
-
* Execute a streaming generate call with tool loop
|
|
330
|
-
*/
|
|
331
|
-
function executeStream<TParams>(
|
|
332
|
-
model: BoundLLMModel<TParams>,
|
|
333
|
-
config: ProviderConfig,
|
|
334
|
-
system: string | undefined,
|
|
335
|
-
params: TParams | undefined,
|
|
336
|
-
tools: Tool[] | undefined,
|
|
337
|
-
toolStrategy: LLMOptions<TParams>['toolStrategy'],
|
|
338
|
-
structure: LLMOptions<TParams>['structure'],
|
|
339
|
-
history: Message[],
|
|
340
|
-
newMessages: Message[]
|
|
341
|
-
): StreamResult {
|
|
342
|
-
// Validate media capabilities for all input messages
|
|
343
|
-
validateMediaCapabilities(
|
|
344
|
-
[...history, ...newMessages],
|
|
345
|
-
model.capabilities,
|
|
346
|
-
model.provider.name
|
|
347
|
-
);
|
|
348
|
-
|
|
349
|
-
const abortController = new AbortController();
|
|
350
|
-
|
|
351
|
-
// Shared state between generator and turn promise
|
|
352
|
-
const allMessages: Message[] = [...history, ...newMessages];
|
|
353
|
-
const toolExecutions: ToolExecution[] = [];
|
|
354
|
-
const usages: TokenUsage[] = [];
|
|
355
|
-
let cycles = 0;
|
|
356
|
-
let generatorError: Error | null = null;
|
|
357
|
-
let structuredData: unknown; // Providers extract this
|
|
358
|
-
|
|
359
|
-
// Deferred to signal when generator completes
|
|
360
|
-
let resolveGenerator: () => void;
|
|
361
|
-
let rejectGenerator: (error: Error) => void;
|
|
362
|
-
const generatorDone = new Promise<void>((resolve, reject) => {
|
|
363
|
-
resolveGenerator = resolve;
|
|
364
|
-
rejectGenerator = reject;
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
const maxIterations = toolStrategy?.maxIterations ?? DEFAULT_MAX_ITERATIONS;
|
|
368
|
-
|
|
369
|
-
// Create the async generator - this is the ONLY place that calls the API
|
|
370
|
-
async function* generateStream(): AsyncGenerator<StreamEvent, void, unknown> {
|
|
371
|
-
try {
|
|
372
|
-
while (cycles < maxIterations + 1) {
|
|
373
|
-
cycles++;
|
|
374
|
-
|
|
375
|
-
const request: LLMRequest<TParams> = {
|
|
376
|
-
messages: allMessages,
|
|
377
|
-
system,
|
|
378
|
-
params,
|
|
379
|
-
tools,
|
|
380
|
-
structure,
|
|
381
|
-
config,
|
|
382
|
-
signal: abortController.signal,
|
|
383
|
-
};
|
|
384
|
-
|
|
385
|
-
const streamResult = model.stream(request);
|
|
386
|
-
|
|
387
|
-
// Forward stream events
|
|
388
|
-
for await (const event of streamResult) {
|
|
389
|
-
yield event;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// Get the response
|
|
393
|
-
const response = await streamResult.response;
|
|
394
|
-
usages.push(response.usage);
|
|
395
|
-
allMessages.push(response.message);
|
|
396
|
-
|
|
397
|
-
// Track structured data from provider (if present)
|
|
398
|
-
if (response.data !== undefined) {
|
|
399
|
-
structuredData = response.data;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// Check for tool calls
|
|
403
|
-
if (response.message.hasToolCalls && tools && tools.length > 0) {
|
|
404
|
-
// If provider already extracted structured data, don't try to execute tool calls
|
|
405
|
-
// (some providers use tool calls internally for structured output)
|
|
406
|
-
if (response.data !== undefined) {
|
|
407
|
-
break;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
if (cycles >= maxIterations) {
|
|
411
|
-
await toolStrategy?.onMaxIterations?.(maxIterations);
|
|
412
|
-
throw new UPPError(
|
|
413
|
-
`Tool execution exceeded maximum iterations (${maxIterations})`,
|
|
414
|
-
'INVALID_REQUEST',
|
|
415
|
-
model.provider.name,
|
|
416
|
-
'llm'
|
|
417
|
-
);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
// Execute tools with event emission
|
|
421
|
-
const toolEvents: StreamEvent[] = [];
|
|
422
|
-
const results = await executeTools(
|
|
423
|
-
response.message,
|
|
424
|
-
tools,
|
|
425
|
-
toolStrategy,
|
|
426
|
-
toolExecutions,
|
|
427
|
-
(event) => toolEvents.push(event)
|
|
428
|
-
);
|
|
429
|
-
|
|
430
|
-
// Yield tool execution events
|
|
431
|
-
for (const event of toolEvents) {
|
|
432
|
-
yield event;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// Add tool results
|
|
436
|
-
allMessages.push(new ToolResultMessage(results));
|
|
437
|
-
|
|
438
|
-
continue;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
break;
|
|
442
|
-
}
|
|
443
|
-
resolveGenerator();
|
|
444
|
-
} catch (error) {
|
|
445
|
-
generatorError = error as Error;
|
|
446
|
-
rejectGenerator(error as Error);
|
|
447
|
-
throw error;
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// Turn promise waits for the generator to complete, then builds the Turn
|
|
452
|
-
const turnPromise = (async (): Promise<Turn> => {
|
|
453
|
-
await generatorDone;
|
|
454
|
-
|
|
455
|
-
if (generatorError) {
|
|
456
|
-
throw generatorError;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// Use structured data from provider if structure was requested
|
|
460
|
-
const data = structure ? structuredData : undefined;
|
|
461
|
-
|
|
462
|
-
return createTurn(
|
|
463
|
-
allMessages.slice(history.length),
|
|
464
|
-
toolExecutions,
|
|
465
|
-
aggregateUsage(usages),
|
|
466
|
-
cycles,
|
|
467
|
-
data
|
|
468
|
-
);
|
|
469
|
-
})();
|
|
470
|
-
|
|
471
|
-
return createStreamResult(generateStream(), turnPromise, abortController);
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
/**
|
|
475
|
-
* Execute tools from an assistant message
|
|
476
|
-
*/
|
|
477
|
-
async function executeTools(
|
|
478
|
-
message: AssistantMessage,
|
|
479
|
-
tools: Tool[],
|
|
480
|
-
toolStrategy: LLMOptions<unknown>['toolStrategy'],
|
|
481
|
-
executions: ToolExecution[],
|
|
482
|
-
onEvent?: (event: StreamEvent) => void
|
|
483
|
-
): Promise<ToolResult[]> {
|
|
484
|
-
const toolCalls = message.toolCalls ?? [];
|
|
485
|
-
const results: ToolResult[] = [];
|
|
486
|
-
|
|
487
|
-
// Build tool map
|
|
488
|
-
const toolMap = new Map(tools.map((t) => [t.name, t]));
|
|
489
|
-
|
|
490
|
-
// Execute tools (in parallel)
|
|
491
|
-
const promises = toolCalls.map(async (call, index) => {
|
|
492
|
-
const tool = toolMap.get(call.toolName);
|
|
493
|
-
if (!tool) {
|
|
494
|
-
return {
|
|
495
|
-
toolCallId: call.toolCallId,
|
|
496
|
-
result: `Tool '${call.toolName}' not found`,
|
|
497
|
-
isError: true,
|
|
498
|
-
};
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
const startTime = Date.now();
|
|
502
|
-
|
|
503
|
-
// Emit start event
|
|
504
|
-
onEvent?.(toolExecutionStart(call.toolCallId, tool.name, startTime, index));
|
|
505
|
-
|
|
506
|
-
// Notify strategy
|
|
507
|
-
await toolStrategy?.onToolCall?.(tool, call.arguments);
|
|
508
|
-
|
|
509
|
-
// Check before call
|
|
510
|
-
if (toolStrategy?.onBeforeCall) {
|
|
511
|
-
const shouldRun = await toolStrategy.onBeforeCall(tool, call.arguments);
|
|
512
|
-
if (!shouldRun) {
|
|
513
|
-
const endTime = Date.now();
|
|
514
|
-
onEvent?.(toolExecutionEnd(call.toolCallId, tool.name, 'Tool execution skipped', true, endTime, index));
|
|
515
|
-
return {
|
|
516
|
-
toolCallId: call.toolCallId,
|
|
517
|
-
result: 'Tool execution skipped',
|
|
518
|
-
isError: true,
|
|
519
|
-
};
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
// Check approval
|
|
524
|
-
let approved = true;
|
|
525
|
-
if (tool.approval) {
|
|
526
|
-
try {
|
|
527
|
-
approved = await tool.approval(call.arguments);
|
|
528
|
-
} catch (error) {
|
|
529
|
-
// Approval threw - propagate
|
|
530
|
-
throw error;
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
if (!approved) {
|
|
535
|
-
const endTime = Date.now();
|
|
536
|
-
const execution: ToolExecution = {
|
|
537
|
-
toolName: tool.name,
|
|
538
|
-
toolCallId: call.toolCallId,
|
|
539
|
-
arguments: call.arguments,
|
|
540
|
-
result: 'Tool execution denied',
|
|
541
|
-
isError: true,
|
|
542
|
-
duration: endTime - startTime,
|
|
543
|
-
approved: false,
|
|
544
|
-
};
|
|
545
|
-
executions.push(execution);
|
|
546
|
-
|
|
547
|
-
onEvent?.(toolExecutionEnd(call.toolCallId, tool.name, 'Tool execution denied by approval handler', true, endTime, index));
|
|
548
|
-
|
|
549
|
-
return {
|
|
550
|
-
toolCallId: call.toolCallId,
|
|
551
|
-
result: 'Tool execution denied by approval handler',
|
|
552
|
-
isError: true,
|
|
553
|
-
};
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
// Execute tool
|
|
557
|
-
try {
|
|
558
|
-
const result = await tool.run(call.arguments);
|
|
559
|
-
const endTime = Date.now();
|
|
560
|
-
|
|
561
|
-
await toolStrategy?.onAfterCall?.(tool, call.arguments, result);
|
|
562
|
-
|
|
563
|
-
const execution: ToolExecution = {
|
|
564
|
-
toolName: tool.name,
|
|
565
|
-
toolCallId: call.toolCallId,
|
|
566
|
-
arguments: call.arguments,
|
|
567
|
-
result,
|
|
568
|
-
isError: false,
|
|
569
|
-
duration: endTime - startTime,
|
|
570
|
-
approved,
|
|
571
|
-
};
|
|
572
|
-
executions.push(execution);
|
|
573
|
-
|
|
574
|
-
onEvent?.(toolExecutionEnd(call.toolCallId, tool.name, result, false, endTime, index));
|
|
575
|
-
|
|
576
|
-
return {
|
|
577
|
-
toolCallId: call.toolCallId,
|
|
578
|
-
result,
|
|
579
|
-
isError: false,
|
|
580
|
-
};
|
|
581
|
-
} catch (error) {
|
|
582
|
-
const endTime = Date.now();
|
|
583
|
-
await toolStrategy?.onError?.(tool, call.arguments, error as Error);
|
|
584
|
-
|
|
585
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
586
|
-
|
|
587
|
-
const execution: ToolExecution = {
|
|
588
|
-
toolName: tool.name,
|
|
589
|
-
toolCallId: call.toolCallId,
|
|
590
|
-
arguments: call.arguments,
|
|
591
|
-
result: errorMessage,
|
|
592
|
-
isError: true,
|
|
593
|
-
duration: endTime - startTime,
|
|
594
|
-
approved,
|
|
595
|
-
};
|
|
596
|
-
executions.push(execution);
|
|
597
|
-
|
|
598
|
-
onEvent?.(toolExecutionEnd(call.toolCallId, tool.name, errorMessage, true, endTime, index));
|
|
599
|
-
|
|
600
|
-
return {
|
|
601
|
-
toolCallId: call.toolCallId,
|
|
602
|
-
result: errorMessage,
|
|
603
|
-
isError: true,
|
|
604
|
-
};
|
|
605
|
-
}
|
|
606
|
-
});
|
|
607
|
-
|
|
608
|
-
results.push(...(await Promise.all(promises)));
|
|
609
|
-
return results;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
/**
|
|
613
|
-
* Check if messages contain media that requires specific capabilities
|
|
614
|
-
*/
|
|
615
|
-
function validateMediaCapabilities(
|
|
616
|
-
messages: Message[],
|
|
617
|
-
capabilities: LLMCapabilities,
|
|
618
|
-
providerName: string
|
|
619
|
-
): void {
|
|
620
|
-
for (const msg of messages) {
|
|
621
|
-
if (!isUserMessage(msg)) continue;
|
|
622
|
-
|
|
623
|
-
for (const block of msg.content) {
|
|
624
|
-
if (block.type === 'image' && !capabilities.imageInput) {
|
|
625
|
-
throw new UPPError(
|
|
626
|
-
`Provider '${providerName}' does not support image input`,
|
|
627
|
-
'INVALID_REQUEST',
|
|
628
|
-
providerName,
|
|
629
|
-
'llm'
|
|
630
|
-
);
|
|
631
|
-
}
|
|
632
|
-
if (block.type === 'video' && !capabilities.videoInput) {
|
|
633
|
-
throw new UPPError(
|
|
634
|
-
`Provider '${providerName}' does not support video input`,
|
|
635
|
-
'INVALID_REQUEST',
|
|
636
|
-
providerName,
|
|
637
|
-
'llm'
|
|
638
|
-
);
|
|
639
|
-
}
|
|
640
|
-
if (block.type === 'audio' && !capabilities.audioInput) {
|
|
641
|
-
throw new UPPError(
|
|
642
|
-
`Provider '${providerName}' does not support audio input`,
|
|
643
|
-
'INVALID_REQUEST',
|
|
644
|
-
providerName,
|
|
645
|
-
'llm'
|
|
646
|
-
);
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
}
|