@theia/ai-openai 1.66.0-next.67 → 1.66.0-next.80
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/lib/browser/openai-frontend-application-contribution.d.ts.map +1 -1
- package/lib/browser/openai-frontend-application-contribution.js +11 -4
- package/lib/browser/openai-frontend-application-contribution.js.map +1 -1
- package/lib/common/openai-language-models-manager.d.ts +6 -0
- package/lib/common/openai-language-models-manager.d.ts.map +1 -1
- package/lib/common/openai-preferences.d.ts +1 -0
- package/lib/common/openai-preferences.d.ts.map +1 -1
- package/lib/common/openai-preferences.js +17 -1
- package/lib/common/openai-preferences.js.map +1 -1
- package/lib/node/openai-backend-module.d.ts.map +1 -1
- package/lib/node/openai-backend-module.js +2 -0
- package/lib/node/openai-backend-module.js.map +1 -1
- package/lib/node/openai-language-model.d.ts +8 -2
- package/lib/node/openai-language-model.d.ts.map +1 -1
- package/lib/node/openai-language-model.js +43 -42
- package/lib/node/openai-language-model.js.map +1 -1
- package/lib/node/openai-language-models-manager-impl.d.ts +2 -0
- package/lib/node/openai-language-models-manager-impl.d.ts.map +1 -1
- package/lib/node/openai-language-models-manager-impl.js +9 -2
- package/lib/node/openai-language-models-manager-impl.js.map +1 -1
- package/lib/node/openai-model-utils.spec.d.ts +1 -3
- package/lib/node/openai-model-utils.spec.d.ts.map +1 -1
- package/lib/node/openai-model-utils.spec.js +250 -23
- package/lib/node/openai-model-utils.spec.js.map +1 -1
- package/lib/node/openai-request-api-context.d.ts +4 -0
- package/lib/node/openai-request-api-context.d.ts.map +1 -0
- package/lib/node/openai-request-api-context.js +18 -0
- package/lib/node/openai-request-api-context.js.map +1 -0
- package/lib/node/openai-response-api-utils.d.ts +42 -0
- package/lib/node/openai-response-api-utils.d.ts.map +1 -0
- package/lib/node/openai-response-api-utils.js +677 -0
- package/lib/node/openai-response-api-utils.js.map +1 -0
- package/package.json +6 -6
- package/src/browser/openai-frontend-application-contribution.ts +10 -4
- package/src/common/openai-language-models-manager.ts +6 -0
- package/src/common/openai-preferences.ts +18 -0
- package/src/node/openai-backend-module.ts +2 -0
- package/src/node/openai-language-model.ts +59 -42
- package/src/node/openai-language-models-manager-impl.ts +8 -1
- package/src/node/openai-model-utils.spec.ts +257 -22
- package/src/node/openai-request-api-context.ts +23 -0
- 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
|
+
}
|