@theia/ai-openai 1.66.0-next.73 → 1.66.0

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.
Files changed (42) hide show
  1. package/lib/browser/openai-frontend-application-contribution.d.ts.map +1 -1
  2. package/lib/browser/openai-frontend-application-contribution.js +11 -4
  3. package/lib/browser/openai-frontend-application-contribution.js.map +1 -1
  4. package/lib/common/openai-language-models-manager.d.ts +6 -0
  5. package/lib/common/openai-language-models-manager.d.ts.map +1 -1
  6. package/lib/common/openai-preferences.d.ts +1 -0
  7. package/lib/common/openai-preferences.d.ts.map +1 -1
  8. package/lib/common/openai-preferences.js +17 -1
  9. package/lib/common/openai-preferences.js.map +1 -1
  10. package/lib/node/openai-backend-module.d.ts.map +1 -1
  11. package/lib/node/openai-backend-module.js +2 -0
  12. package/lib/node/openai-backend-module.js.map +1 -1
  13. package/lib/node/openai-language-model.d.ts +8 -2
  14. package/lib/node/openai-language-model.d.ts.map +1 -1
  15. package/lib/node/openai-language-model.js +43 -42
  16. package/lib/node/openai-language-model.js.map +1 -1
  17. package/lib/node/openai-language-models-manager-impl.d.ts +2 -0
  18. package/lib/node/openai-language-models-manager-impl.d.ts.map +1 -1
  19. package/lib/node/openai-language-models-manager-impl.js +9 -2
  20. package/lib/node/openai-language-models-manager-impl.js.map +1 -1
  21. package/lib/node/openai-model-utils.spec.d.ts +1 -3
  22. package/lib/node/openai-model-utils.spec.d.ts.map +1 -1
  23. package/lib/node/openai-model-utils.spec.js +250 -23
  24. package/lib/node/openai-model-utils.spec.js.map +1 -1
  25. package/lib/node/openai-request-api-context.d.ts +4 -0
  26. package/lib/node/openai-request-api-context.d.ts.map +1 -0
  27. package/lib/node/openai-request-api-context.js +18 -0
  28. package/lib/node/openai-request-api-context.js.map +1 -0
  29. package/lib/node/openai-response-api-utils.d.ts +42 -0
  30. package/lib/node/openai-response-api-utils.d.ts.map +1 -0
  31. package/lib/node/openai-response-api-utils.js +677 -0
  32. package/lib/node/openai-response-api-utils.js.map +1 -0
  33. package/package.json +7 -7
  34. package/src/browser/openai-frontend-application-contribution.ts +10 -4
  35. package/src/common/openai-language-models-manager.ts +6 -0
  36. package/src/common/openai-preferences.ts +18 -0
  37. package/src/node/openai-backend-module.ts +2 -0
  38. package/src/node/openai-language-model.ts +59 -42
  39. package/src/node/openai-language-models-manager-impl.ts +8 -1
  40. package/src/node/openai-model-utils.spec.ts +257 -22
  41. package/src/node/openai-request-api-context.ts +23 -0
  42. package/src/node/openai-response-api-utils.ts +801 -0
@@ -0,0 +1,801 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2025 EclipseSource GmbH.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import {
18
+ ImageContent,
19
+ LanguageModelMessage,
20
+ LanguageModelResponse,
21
+ LanguageModelStreamResponsePart,
22
+ TextMessage,
23
+ TokenUsageService,
24
+ ToolCallErrorResult,
25
+ ToolRequest,
26
+ UserRequest
27
+ } from '@theia/ai-core';
28
+ import { CancellationToken, unreachable } from '@theia/core';
29
+ import { Deferred } from '@theia/core/lib/common/promise-util';
30
+ import { injectable } from '@theia/core/shared/inversify';
31
+ import { OpenAI } from 'openai';
32
+ import type { RunnerOptions } from 'openai/lib/AbstractChatCompletionRunner';
33
+ import type {
34
+ FunctionTool,
35
+ ResponseFunctionCallArgumentsDeltaEvent,
36
+ ResponseFunctionCallArgumentsDoneEvent,
37
+ ResponseFunctionToolCall,
38
+ ResponseInputItem,
39
+ ResponseStreamEvent
40
+ } from 'openai/resources/responses/responses';
41
+ import type { ResponsesModel } from 'openai/resources/shared';
42
+ import { DeveloperMessageSettings, OpenAiModelUtils } from './openai-language-model';
43
+
44
+ interface ToolCall {
45
+ id: string;
46
+ call_id?: string;
47
+ name: string;
48
+ arguments: string;
49
+ result?: unknown;
50
+ error?: Error;
51
+ executed: boolean;
52
+ }
53
+
54
+ /**
55
+ * Utility class for handling OpenAI Response API requests and tool calling cycles.
56
+ *
57
+ * This class encapsulates the complexity of the Response API's multi-turn conversation
58
+ * patterns for tool calling, keeping the main language model class clean and focused.
59
+ */
60
+ @injectable()
61
+ export class OpenAiResponseApiUtils {
62
+
63
+ /**
64
+ * Handles Response API requests with proper tool calling cycles.
65
+ * Works for both streaming and non-streaming cases.
66
+ */
67
+ async handleRequest(
68
+ openai: OpenAI,
69
+ request: UserRequest,
70
+ settings: Record<string, unknown>,
71
+ model: string,
72
+ modelUtils: OpenAiModelUtils,
73
+ developerMessageSettings: DeveloperMessageSettings,
74
+ runnerOptions: RunnerOptions,
75
+ modelId: string,
76
+ isStreaming: boolean,
77
+ tokenUsageService?: TokenUsageService,
78
+ cancellationToken?: CancellationToken
79
+ ): Promise<LanguageModelResponse> {
80
+ if (cancellationToken?.isCancellationRequested) {
81
+ return { text: '' };
82
+ }
83
+
84
+ const { instructions, input } = this.processMessages(request.messages, developerMessageSettings, model);
85
+ const tools = this.convertToolsForResponseApi(request.tools);
86
+
87
+ // If no tools are provided, use simple response handling
88
+ if (!tools || tools.length === 0) {
89
+ if (isStreaming) {
90
+ const stream = openai.responses.stream({
91
+ model: model as ResponsesModel,
92
+ instructions,
93
+ input,
94
+ ...settings
95
+ });
96
+ return { stream: this.createSimpleResponseApiStreamIterator(stream, request.requestId, modelId, tokenUsageService, cancellationToken) };
97
+ } else {
98
+ const response = await openai.responses.create({
99
+ model: model as ResponsesModel,
100
+ instructions,
101
+ input,
102
+ ...settings
103
+ });
104
+
105
+ // Record token usage if available
106
+ if (tokenUsageService && response.usage) {
107
+ await tokenUsageService.recordTokenUsage(
108
+ modelId,
109
+ {
110
+ inputTokens: response.usage.input_tokens,
111
+ outputTokens: response.usage.output_tokens,
112
+ requestId: request.requestId
113
+ }
114
+ );
115
+ }
116
+
117
+ return { text: response.output_text || '' };
118
+ }
119
+ }
120
+
121
+ // Handle tool calling with multi-turn conversation using the unified iterator
122
+ const iterator = new ResponseApiToolCallIterator(
123
+ openai,
124
+ request,
125
+ settings,
126
+ model,
127
+ modelUtils,
128
+ developerMessageSettings,
129
+ runnerOptions,
130
+ modelId,
131
+ this,
132
+ isStreaming,
133
+ tokenUsageService,
134
+ cancellationToken
135
+ );
136
+
137
+ return { stream: iterator };
138
+ }
139
+
140
+ /**
141
+ * Converts ToolRequest objects to the format expected by the Response API.
142
+ */
143
+ convertToolsForResponseApi(tools?: ToolRequest[]): FunctionTool[] | undefined {
144
+ if (!tools || tools.length === 0) {
145
+ return undefined;
146
+ }
147
+
148
+ const converted = tools.map(tool => ({
149
+ type: 'function' as const,
150
+ name: tool.name,
151
+ description: tool.description || '',
152
+ // The Response API is very strict re: JSON schema: all properties must be listed as required,
153
+ // and additional properties must be disallowed.
154
+ // https://platform.openai.com/docs/guides/function-calling#strict-mode
155
+ parameters: {
156
+ ...tool.parameters,
157
+ additionalProperties: false,
158
+ required: tool.parameters.properties ? Object.keys(tool.parameters.properties) : []
159
+ },
160
+ strict: true
161
+ }));
162
+ console.debug(`Converted ${tools.length} tools for Response API:`, converted.map(t => t.name));
163
+ return converted;
164
+ }
165
+
166
+ protected createSimpleResponseApiStreamIterator(
167
+ stream: AsyncIterable<ResponseStreamEvent>,
168
+ requestId: string,
169
+ modelId: string,
170
+ tokenUsageService?: TokenUsageService,
171
+ cancellationToken?: CancellationToken
172
+ ): AsyncIterable<LanguageModelStreamResponsePart> {
173
+ return {
174
+ async *[Symbol.asyncIterator](): AsyncIterator<LanguageModelStreamResponsePart> {
175
+ try {
176
+ for await (const event of stream) {
177
+ if (cancellationToken?.isCancellationRequested) {
178
+ break;
179
+ }
180
+
181
+ if (event.type === 'response.output_text.delta') {
182
+ yield {
183
+ content: event.delta
184
+ };
185
+ } else if (event.type === 'response.completed') {
186
+ if (tokenUsageService && event.response?.usage) {
187
+ await tokenUsageService.recordTokenUsage(
188
+ modelId,
189
+ {
190
+ inputTokens: event.response.usage.input_tokens,
191
+ outputTokens: event.response.usage.output_tokens,
192
+ requestId
193
+ }
194
+ );
195
+ }
196
+ } else if (event.type === 'error') {
197
+ console.error('Response API error:', event.message);
198
+ throw new Error(`Response API error: ${event.message}`);
199
+ }
200
+ }
201
+ } catch (error) {
202
+ console.error('Error in Response API stream:', error);
203
+ throw error;
204
+ }
205
+ }
206
+ };
207
+ }
208
+
209
+ /**
210
+ * Processes the provided list of messages by applying system message adjustments and converting
211
+ * them directly to the format expected by the OpenAI Response API.
212
+ *
213
+ * This method converts messages directly without going through ChatCompletionMessageParam types.
214
+ *
215
+ * @param messages the list of messages to process.
216
+ * @param developerMessageSettings how system and developer messages are handled during processing.
217
+ * @param model the OpenAI model identifier. Currently not used, but allows subclasses to implement model-specific behavior.
218
+ * @returns an object containing instructions and input formatted for the Response API.
219
+ */
220
+ processMessages(
221
+ messages: LanguageModelMessage[],
222
+ developerMessageSettings: DeveloperMessageSettings,
223
+ model: string
224
+ ): { instructions?: string; input: ResponseInputItem[] } {
225
+ const processed = this.processSystemMessages(messages, developerMessageSettings)
226
+ .filter(m => m.type !== 'thinking');
227
+
228
+ // Extract system/developer messages for instructions
229
+ const systemMessages = processed.filter((m): m is TextMessage => m.type === 'text' && m.actor === 'system');
230
+ const instructions = systemMessages.length > 0
231
+ ? systemMessages.map(m => m.text).join('\n')
232
+ : undefined;
233
+
234
+ // Convert non-system messages to Response API input items
235
+ const nonSystemMessages = processed.filter(m => m.actor !== 'system');
236
+ const input: ResponseInputItem[] = [];
237
+
238
+ for (const message of nonSystemMessages) {
239
+ if (LanguageModelMessage.isTextMessage(message)) {
240
+ if (message.actor === 'ai') {
241
+ // Assistant messages use ResponseOutputMessage format
242
+ input.push({
243
+ id: `msg_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`,
244
+ type: 'message',
245
+ role: 'assistant',
246
+ status: 'completed',
247
+ content: [{
248
+ type: 'output_text',
249
+ text: message.text,
250
+ annotations: []
251
+ }]
252
+ });
253
+ } else {
254
+ // User messages use input format
255
+ input.push({
256
+ type: 'message',
257
+ role: 'user',
258
+ content: [{
259
+ type: 'input_text',
260
+ text: message.text
261
+ }]
262
+ });
263
+ }
264
+ } else if (LanguageModelMessage.isToolUseMessage(message)) {
265
+ input.push({
266
+ type: 'function_call',
267
+ call_id: message.id,
268
+ name: message.name,
269
+ arguments: JSON.stringify(message.input)
270
+ });
271
+ } else if (LanguageModelMessage.isToolResultMessage(message)) {
272
+ const content = typeof message.content === 'string' ? message.content : JSON.stringify(message.content);
273
+ input.push({
274
+ type: 'function_call_output',
275
+ call_id: message.tool_use_id,
276
+ output: content
277
+ });
278
+ } else if (LanguageModelMessage.isImageMessage(message)) {
279
+ input.push({
280
+ type: 'message',
281
+ role: 'user',
282
+ content: [{
283
+ type: 'input_image',
284
+ detail: 'auto',
285
+ image_url: ImageContent.isBase64(message.image) ?
286
+ `data:${message.image.mimeType};base64,${message.image.base64data}` :
287
+ message.image.url
288
+ }]
289
+ });
290
+ } else if (LanguageModelMessage.isThinkingMessage(message)) {
291
+ // Pass
292
+ } else {
293
+ unreachable(message);
294
+ }
295
+ }
296
+
297
+ return { instructions, input };
298
+ }
299
+
300
+ protected processSystemMessages(
301
+ messages: LanguageModelMessage[],
302
+ developerMessageSettings: DeveloperMessageSettings
303
+ ): LanguageModelMessage[] {
304
+ return processSystemMessages(messages, developerMessageSettings);
305
+ }
306
+ }
307
+
308
+ /**
309
+ * Iterator for handling Response API streaming with tool calls.
310
+ * Based on the pattern from openai-streaming-iterator.ts but adapted for Response API.
311
+ */
312
+ class ResponseApiToolCallIterator implements AsyncIterableIterator<LanguageModelStreamResponsePart> {
313
+ protected readonly requestQueue = new Array<Deferred<IteratorResult<LanguageModelStreamResponsePart>>>();
314
+ protected readonly messageCache = new Array<LanguageModelStreamResponsePart>();
315
+ protected done = false;
316
+ protected terminalError: Error | undefined = undefined;
317
+
318
+ // Current iteration state
319
+ protected currentInput: ResponseInputItem[];
320
+ protected currentToolCalls = new Map<string, ToolCall>();
321
+ protected totalInputTokens = 0;
322
+ protected totalOutputTokens = 0;
323
+ protected iteration = 0;
324
+ protected readonly maxIterations: number;
325
+ protected readonly tools: FunctionTool[] | undefined;
326
+ protected readonly instructions?: string;
327
+ protected currentResponseText = '';
328
+
329
+ constructor(
330
+ protected readonly openai: OpenAI,
331
+ protected readonly request: UserRequest,
332
+ protected readonly settings: Record<string, unknown>,
333
+ protected readonly model: string,
334
+ protected readonly modelUtils: OpenAiModelUtils,
335
+ protected readonly developerMessageSettings: DeveloperMessageSettings,
336
+ protected readonly runnerOptions: RunnerOptions,
337
+ protected readonly modelId: string,
338
+ protected readonly utils: OpenAiResponseApiUtils,
339
+ protected readonly isStreaming: boolean,
340
+ protected readonly tokenUsageService?: TokenUsageService,
341
+ protected readonly cancellationToken?: CancellationToken
342
+ ) {
343
+ const { instructions, input } = utils.processMessages(request.messages, developerMessageSettings, model);
344
+ this.instructions = instructions;
345
+ this.currentInput = input;
346
+ this.tools = utils.convertToolsForResponseApi(request.tools);
347
+ this.maxIterations = runnerOptions.maxChatCompletions || 100;
348
+
349
+ // Start the first iteration
350
+ this.startIteration();
351
+ }
352
+
353
+ [Symbol.asyncIterator](): AsyncIterableIterator<LanguageModelStreamResponsePart> {
354
+ return this;
355
+ }
356
+
357
+ async next(): Promise<IteratorResult<LanguageModelStreamResponsePart>> {
358
+ if (this.messageCache.length && this.requestQueue.length) {
359
+ throw new Error('Assertion error: cache and queue should not both be populated.');
360
+ }
361
+
362
+ // Deliver all the messages we got, even if we've since terminated.
363
+ if (this.messageCache.length) {
364
+ return {
365
+ done: false,
366
+ value: this.messageCache.shift()!
367
+ };
368
+ } else if (this.terminalError) {
369
+ throw this.terminalError;
370
+ } else if (this.done) {
371
+ return {
372
+ done: true,
373
+ value: undefined
374
+ };
375
+ } else {
376
+ const deferred = new Deferred<IteratorResult<LanguageModelStreamResponsePart>>();
377
+ this.requestQueue.push(deferred);
378
+ return deferred.promise;
379
+ }
380
+ }
381
+
382
+ protected async startIteration(): Promise<void> {
383
+ try {
384
+ while (this.iteration < this.maxIterations && !this.cancellationToken?.isCancellationRequested) {
385
+ console.debug(`Starting Response API iteration ${this.iteration} with ${this.currentInput.length} input messages`);
386
+
387
+ await this.processStream();
388
+
389
+ // Check if we have tool calls that need execution
390
+ if (this.currentToolCalls.size === 0) {
391
+ // No tool calls, we're done
392
+ this.finalize();
393
+ return;
394
+ }
395
+
396
+ // Execute all tool calls
397
+ await this.executeToolCalls();
398
+
399
+ // Prepare for next iteration
400
+ this.prepareNextIteration();
401
+ this.iteration++;
402
+ }
403
+
404
+ // Max iterations reached
405
+ this.finalize();
406
+ } catch (error) {
407
+ this.terminalError = error instanceof Error ? error : new Error(String(error));
408
+ this.finalize();
409
+ }
410
+ }
411
+
412
+ protected async processStream(): Promise<void> {
413
+ this.currentToolCalls.clear();
414
+ this.currentResponseText = '';
415
+
416
+ if (this.isStreaming) {
417
+ // Use streaming API
418
+ const stream = this.openai.responses.stream({
419
+ model: this.model as ResponsesModel,
420
+ instructions: this.instructions,
421
+ input: this.currentInput,
422
+ tools: this.tools,
423
+ ...this.settings
424
+ });
425
+
426
+ for await (const event of stream) {
427
+ if (this.cancellationToken?.isCancellationRequested) {
428
+ break;
429
+ }
430
+
431
+ await this.handleStreamEvent(event);
432
+ }
433
+ } else {
434
+ // Use non-streaming API but yield results incrementally
435
+ await this.processNonStreamingResponse();
436
+ }
437
+ }
438
+
439
+ protected async processNonStreamingResponse(): Promise<void> {
440
+ const response = await this.openai.responses.create({
441
+ model: this.model as ResponsesModel,
442
+ instructions: this.instructions,
443
+ input: this.currentInput,
444
+ tools: this.tools,
445
+ ...this.settings
446
+ });
447
+
448
+ // Record token usage
449
+ if (response.usage) {
450
+ this.totalInputTokens += response.usage.input_tokens;
451
+ this.totalOutputTokens += response.usage.output_tokens;
452
+ }
453
+
454
+ // First, yield any text content from the response
455
+ this.currentResponseText = response.output_text || '';
456
+ if (this.currentResponseText) {
457
+ this.handleIncoming({ content: this.currentResponseText });
458
+ }
459
+
460
+ // Find function calls in the response
461
+ const functionCalls = response.output?.filter((item): item is ResponseFunctionToolCall => item.type === 'function_call') || [];
462
+
463
+ // Process each function call
464
+ for (const functionCall of functionCalls) {
465
+ if (functionCall.id && functionCall.name) {
466
+ const toolCall: ToolCall = {
467
+ id: functionCall.id,
468
+ call_id: functionCall.call_id || functionCall.id,
469
+ name: functionCall.name,
470
+ arguments: functionCall.arguments || '',
471
+ executed: false
472
+ };
473
+
474
+ this.currentToolCalls.set(functionCall.id, toolCall);
475
+
476
+ // Yield the tool call initiation
477
+ this.handleIncoming({
478
+ tool_calls: [{
479
+ id: functionCall.id,
480
+ finished: false,
481
+ function: {
482
+ name: functionCall.name,
483
+ arguments: functionCall.arguments || ''
484
+ }
485
+ }]
486
+ });
487
+ }
488
+ }
489
+ }
490
+
491
+ protected async handleStreamEvent(event: ResponseStreamEvent): Promise<void> {
492
+ switch (event.type) {
493
+ case 'response.output_text.delta':
494
+ this.currentResponseText += event.delta;
495
+ this.handleIncoming({ content: event.delta });
496
+ break;
497
+
498
+ case 'response.output_item.added':
499
+ if (event.item?.type === 'function_call') {
500
+ this.handleFunctionCallAdded(event.item);
501
+ }
502
+ break;
503
+
504
+ case 'response.function_call_arguments.delta':
505
+ this.handleFunctionCallArgsDelta(event);
506
+ break;
507
+
508
+ case 'response.function_call_arguments.done':
509
+ await this.handleFunctionCallArgsDone(event);
510
+ break;
511
+
512
+ case 'response.output_item.done':
513
+ if (event.item?.type === 'function_call') {
514
+ this.handleFunctionCallDone(event.item);
515
+ }
516
+ break;
517
+
518
+ case 'response.completed':
519
+ if (event.response?.usage) {
520
+ this.totalInputTokens += event.response.usage.input_tokens;
521
+ this.totalOutputTokens += event.response.usage.output_tokens;
522
+ }
523
+ break;
524
+
525
+ case 'error':
526
+ console.error('Response API error:', event.message);
527
+ throw new Error(`Response API error: ${event.message}`);
528
+ }
529
+ }
530
+
531
+ protected handleFunctionCallAdded(functionCall: ResponseFunctionToolCall): void {
532
+ if (functionCall.id && functionCall.call_id) {
533
+ console.debug(`Function call added: ${functionCall.name} with id ${functionCall.id} and call_id ${functionCall.call_id}`);
534
+
535
+ const toolCall: ToolCall = {
536
+ id: functionCall.id,
537
+ call_id: functionCall.call_id,
538
+ name: functionCall.name || '',
539
+ arguments: functionCall.arguments || '',
540
+ executed: false
541
+ };
542
+
543
+ this.currentToolCalls.set(functionCall.id, toolCall);
544
+
545
+ this.handleIncoming({
546
+ tool_calls: [{
547
+ id: functionCall.id,
548
+ finished: false,
549
+ function: {
550
+ name: functionCall.name || '',
551
+ arguments: functionCall.arguments || ''
552
+ }
553
+ }]
554
+ });
555
+ }
556
+ }
557
+
558
+ protected handleFunctionCallArgsDelta(event: ResponseFunctionCallArgumentsDeltaEvent): void {
559
+ const toolCall = this.currentToolCalls.get(event.item_id);
560
+ if (toolCall) {
561
+ toolCall.arguments += event.delta;
562
+
563
+ if (event.delta) {
564
+ this.handleIncoming({
565
+ tool_calls: [{
566
+ id: event.item_id,
567
+ function: {
568
+ arguments: event.delta
569
+ }
570
+ }]
571
+ });
572
+ }
573
+ }
574
+ }
575
+
576
+ protected async handleFunctionCallArgsDone(event: ResponseFunctionCallArgumentsDoneEvent): Promise<void> {
577
+ let toolCall = this.currentToolCalls.get(event.item_id);
578
+ if (!toolCall) {
579
+ // Create if we didn't see the added event
580
+ toolCall = {
581
+ id: event.item_id,
582
+ name: event.name || '',
583
+ arguments: event.arguments || '',
584
+ executed: false
585
+ };
586
+ this.currentToolCalls.set(event.item_id, toolCall);
587
+
588
+ this.handleIncoming({
589
+ tool_calls: [{
590
+ id: event.item_id,
591
+ finished: false,
592
+ function: {
593
+ name: event.name || '',
594
+ arguments: event.arguments || ''
595
+ }
596
+ }]
597
+ });
598
+ } else {
599
+ // Update with final values
600
+ toolCall.name = event.name || toolCall.name;
601
+ toolCall.arguments = event.arguments || toolCall.arguments;
602
+ }
603
+ }
604
+
605
+ protected handleFunctionCallDone(functionCall: ResponseFunctionToolCall): void {
606
+ if (!functionCall.id) { console.warn('Unexpected absence of ID for call ID', functionCall.call_id); return; }
607
+ const toolCall = this.currentToolCalls.get(functionCall.id);
608
+ if (toolCall && !toolCall.call_id && functionCall.call_id) {
609
+ toolCall.call_id = functionCall.call_id;
610
+ }
611
+ }
612
+
613
+ protected async executeToolCalls(): Promise<void> {
614
+ for (const [itemId, toolCall] of this.currentToolCalls) {
615
+ if (toolCall.executed) {
616
+ continue;
617
+ }
618
+
619
+ const tool = this.request.tools?.find(t => t.name === toolCall.name);
620
+ if (tool) {
621
+ try {
622
+ const result = await tool.handler(toolCall.arguments);
623
+ toolCall.result = result;
624
+
625
+ // Yield the tool call completion
626
+ this.handleIncoming({
627
+ tool_calls: [{
628
+ id: itemId,
629
+ finished: true,
630
+ function: {
631
+ name: toolCall.name,
632
+ arguments: toolCall.arguments
633
+ },
634
+ result
635
+ }]
636
+ });
637
+ } catch (error) {
638
+ console.error(`Error executing tool ${toolCall.name}:`, error);
639
+ toolCall.error = error instanceof Error ? error : new Error(String(error));
640
+
641
+ const errorResult: ToolCallErrorResult = {
642
+ type: 'error',
643
+ data: error instanceof Error ? error.message : String(error)
644
+ };
645
+
646
+ // Yield the tool call error
647
+ this.handleIncoming({
648
+ tool_calls: [{
649
+ id: itemId,
650
+ finished: true,
651
+ function: {
652
+ name: toolCall.name,
653
+ arguments: toolCall.arguments
654
+ },
655
+ result: errorResult
656
+ }]
657
+ });
658
+ }
659
+ } else {
660
+ console.warn(`Tool ${toolCall.name} not found in request tools`);
661
+ toolCall.error = new Error(`Tool ${toolCall.name} not found`);
662
+
663
+ const errorResult: ToolCallErrorResult = {
664
+ type: 'error',
665
+ data: `Tool ${toolCall.name} not found`
666
+ };
667
+
668
+ // Yield the tool call error
669
+ this.handleIncoming({
670
+ tool_calls: [{
671
+ id: itemId,
672
+ finished: true,
673
+ function: {
674
+ name: toolCall.name,
675
+ arguments: toolCall.arguments
676
+ },
677
+ result: errorResult
678
+ }]
679
+ });
680
+ }
681
+
682
+ toolCall.executed = true;
683
+ }
684
+ }
685
+
686
+ protected prepareNextIteration(): void {
687
+ // Add assistant response with the actual text that was streamed
688
+ const assistantMessage: ResponseInputItem = {
689
+ role: 'assistant',
690
+ content: this.currentResponseText
691
+ };
692
+
693
+ // Add the function calls that were made by the assistant
694
+ const functionCalls: ResponseInputItem[] = [];
695
+ for (const [itemId, toolCall] of this.currentToolCalls) {
696
+ functionCalls.push({
697
+ type: 'function_call',
698
+ call_id: toolCall.call_id || itemId,
699
+ name: toolCall.name,
700
+ arguments: toolCall.arguments
701
+ });
702
+ }
703
+
704
+ // Add tool results
705
+ const toolResults: ResponseInputItem[] = [];
706
+ for (const [itemId, toolCall] of this.currentToolCalls) {
707
+ const callId = toolCall.call_id || itemId;
708
+
709
+ if (toolCall.result !== undefined) {
710
+ const resultContent = typeof toolCall.result === 'string' ? toolCall.result : JSON.stringify(toolCall.result);
711
+ toolResults.push({
712
+ type: 'function_call_output',
713
+ call_id: callId,
714
+ output: resultContent
715
+ });
716
+ } else if (toolCall.error) {
717
+ toolResults.push({
718
+ type: 'function_call_output',
719
+ call_id: callId,
720
+ output: `Error: ${toolCall.error.message}`
721
+ });
722
+ }
723
+ }
724
+
725
+ this.currentInput = [...this.currentInput, assistantMessage, ...functionCalls, ...toolResults];
726
+ }
727
+
728
+ protected handleIncoming(message: LanguageModelStreamResponsePart): void {
729
+ if (this.messageCache.length && this.requestQueue.length) {
730
+ throw new Error('Assertion error: cache and queue should not both be populated.');
731
+ }
732
+
733
+ if (this.requestQueue.length) {
734
+ this.requestQueue.shift()!.resolve({
735
+ done: false,
736
+ value: message
737
+ });
738
+ } else {
739
+ this.messageCache.push(message);
740
+ }
741
+ }
742
+
743
+ protected async finalize(): Promise<void> {
744
+ this.done = true;
745
+
746
+ // Record final token usage
747
+ if (this.tokenUsageService && (this.totalInputTokens > 0 || this.totalOutputTokens > 0)) {
748
+ try {
749
+ await this.tokenUsageService.recordTokenUsage(
750
+ this.modelId,
751
+ {
752
+ inputTokens: this.totalInputTokens,
753
+ outputTokens: this.totalOutputTokens,
754
+ requestId: this.request.requestId
755
+ }
756
+ );
757
+ } catch (error) {
758
+ console.error('Error recording token usage:', error);
759
+ }
760
+ }
761
+
762
+ // Resolve any outstanding requests
763
+ if (this.terminalError) {
764
+ this.requestQueue.forEach(request => request.reject(this.terminalError));
765
+ } else {
766
+ this.requestQueue.forEach(request => request.resolve({ done: true, value: undefined }));
767
+ }
768
+ this.requestQueue.length = 0;
769
+ }
770
+ }
771
+
772
+ export function processSystemMessages(
773
+ messages: LanguageModelMessage[],
774
+ developerMessageSettings: DeveloperMessageSettings
775
+ ): LanguageModelMessage[] {
776
+ if (developerMessageSettings === 'skip') {
777
+ return messages.filter(message => message.actor !== 'system');
778
+ } else if (developerMessageSettings === 'mergeWithFollowingUserMessage') {
779
+ const updated = messages.slice();
780
+ for (let i = updated.length - 1; i >= 0; i--) {
781
+ if (updated[i].actor === 'system') {
782
+ const systemMessage = updated[i] as TextMessage;
783
+ if (i + 1 < updated.length && updated[i + 1].actor === 'user') {
784
+ // Merge system message with the next user message
785
+ const userMessage = updated[i + 1] as TextMessage;
786
+ updated[i + 1] = {
787
+ ...updated[i + 1],
788
+ text: systemMessage.text + '\n' + userMessage.text
789
+ } as TextMessage;
790
+ updated.splice(i, 1);
791
+ } else {
792
+ // The message directly after is not a user message (or none exists), so create a new user message right after
793
+ updated.splice(i + 1, 0, { actor: 'user', type: 'text', text: systemMessage.text });
794
+ updated.splice(i, 1);
795
+ }
796
+ }
797
+ }
798
+ return updated;
799
+ }
800
+ return messages;
801
+ }