@theia/ai-openai 1.76.0-next.7 → 1.76.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 (50) hide show
  1. package/lib/browser/openai-frontend-application-contribution.d.ts +18 -10
  2. package/lib/browser/openai-frontend-application-contribution.d.ts.map +1 -1
  3. package/lib/browser/openai-frontend-application-contribution.js +68 -64
  4. package/lib/browser/openai-frontend-application-contribution.js.map +1 -1
  5. package/lib/common/openai-language-models-manager.d.ts +20 -1
  6. package/lib/common/openai-language-models-manager.d.ts.map +1 -1
  7. package/lib/common/openai-preferences.d.ts +2 -1
  8. package/lib/common/openai-preferences.d.ts.map +1 -1
  9. package/lib/common/openai-preferences.js +23 -13
  10. package/lib/common/openai-preferences.js.map +1 -1
  11. package/lib/node/openai-language-model.d.ts +26 -2
  12. package/lib/node/openai-language-model.d.ts.map +1 -1
  13. package/lib/node/openai-language-model.js +44 -18
  14. package/lib/node/openai-language-model.js.map +1 -1
  15. package/lib/node/openai-language-model.spec.js +55 -8
  16. package/lib/node/openai-language-model.spec.js.map +1 -1
  17. package/lib/node/openai-language-models-manager-impl.d.ts +38 -1
  18. package/lib/node/openai-language-models-manager-impl.d.ts.map +1 -1
  19. package/lib/node/openai-language-models-manager-impl.js +88 -3
  20. package/lib/node/openai-language-models-manager-impl.js.map +1 -1
  21. package/lib/node/openai-language-models-manager-impl.spec.js +171 -0
  22. package/lib/node/openai-language-models-manager-impl.spec.js.map +1 -1
  23. package/lib/node/openai-model-defaults.d.ts.map +1 -1
  24. package/lib/node/openai-model-defaults.js +7 -0
  25. package/lib/node/openai-model-defaults.js.map +1 -1
  26. package/lib/node/openai-model-defaults.spec.js +9 -0
  27. package/lib/node/openai-model-defaults.spec.js.map +1 -1
  28. package/lib/node/openai-reasoning.d.ts +1 -2
  29. package/lib/node/openai-reasoning.d.ts.map +1 -1
  30. package/lib/node/openai-reasoning.js +6 -8
  31. package/lib/node/openai-reasoning.js.map +1 -1
  32. package/lib/node/openai-response-api-utils.d.ts +25 -1
  33. package/lib/node/openai-response-api-utils.d.ts.map +1 -1
  34. package/lib/node/openai-response-api-utils.js +104 -12
  35. package/lib/node/openai-response-api-utils.js.map +1 -1
  36. package/lib/node/openai-response-api-utils.spec.js +146 -0
  37. package/lib/node/openai-response-api-utils.spec.js.map +1 -1
  38. package/package.json +7 -7
  39. package/src/browser/openai-frontend-application-contribution.ts +82 -69
  40. package/src/common/openai-language-models-manager.ts +21 -2
  41. package/src/common/openai-preferences.ts +25 -12
  42. package/src/node/openai-language-model.spec.ts +63 -9
  43. package/src/node/openai-language-model.ts +58 -20
  44. package/src/node/openai-language-models-manager-impl.spec.ts +178 -0
  45. package/src/node/openai-language-models-manager-impl.ts +106 -6
  46. package/src/node/openai-model-defaults.spec.ts +10 -0
  47. package/src/node/openai-model-defaults.ts +8 -0
  48. package/src/node/openai-reasoning.ts +6 -9
  49. package/src/node/openai-response-api-utils.spec.ts +184 -1
  50. package/src/node/openai-response-api-utils.ts +115 -13
@@ -21,12 +21,13 @@ import {
21
21
  LanguageModelResponse,
22
22
  LanguageModelStreamResponsePart,
23
23
  TextMessage,
24
+ ThinkingResponsePart,
24
25
  ToolCallResult,
25
26
  ToolInvocationContext,
26
27
  ToolRequest,
27
28
  UserRequest
28
29
  } from '@theia/ai-core';
29
- import { CancellationToken, nls, unreachable, ILogger } from '@theia/core';
30
+ import { CancellationToken, isObject, nls, unreachable, ILogger } from '@theia/core';
30
31
  import { Deferred } from '@theia/core/lib/common/promise-util';
31
32
  import { injectable, inject, named } from '@theia/core/shared/inversify';
32
33
  import { OpenAI } from 'openai';
@@ -116,20 +117,20 @@ export class OpenAiResponseApiUtils {
116
117
  // If no tools are provided, use simple response handling
117
118
  if (!tools || tools.length === 0) {
118
119
  if (isStreaming) {
119
- const stream = openai.responses.stream({
120
+ const stream = this.streamWithReasoningSummaryFallback(effectiveSettings, modelId, sentSettings => openai.responses.stream({
120
121
  model: model as ResponsesModel,
121
122
  instructions,
122
123
  input,
123
- ...effectiveSettings
124
- });
124
+ ...sentSettings
125
+ }));
125
126
  return { stream: this.createSimpleResponseApiStreamIterator(stream, cancellationToken) };
126
127
  } else {
127
- const response = await openai.responses.create({
128
+ const response = await this.sendWithReasoningSummaryFallback(effectiveSettings, modelId, sentSettings => openai.responses.create({
128
129
  model: model as ResponsesModel,
129
130
  instructions,
130
131
  input,
131
- ...effectiveSettings
132
- });
132
+ ...sentSettings
133
+ }));
133
134
 
134
135
  return {
135
136
  text: response.output_text || '',
@@ -189,12 +190,99 @@ export class OpenAiResponseApiUtils {
189
190
  return converted;
190
191
  }
191
192
 
193
+ /**
194
+ * Maps a reasoning-summary stream event to a thinking part. With `reasoning.summary` set, the Responses API streams
195
+ * one or more `summary_text` parts per reasoning item; parts after the first are separated by a blank line.
196
+ */
197
+ reasoningSummaryThought(event: ResponseStreamEvent): ThinkingResponsePart | undefined {
198
+ if (event.type === 'response.reasoning_summary_part.added') {
199
+ return event.summary_index > 0 ? { thought: '\n\n', signature: '' } : undefined;
200
+ }
201
+ if (event.type === 'response.reasoning_summary_text.delta') {
202
+ return { thought: event.delta, signature: '' };
203
+ }
204
+ return undefined;
205
+ }
206
+
207
+ /**
208
+ * Model ids whose organization rejected `reasoning.summary` (OpenAI requires a verified organization for summaries).
209
+ * Later requests to these models omit the field.
210
+ */
211
+ protected readonly reasoningSummaryRejectedModels = new Set<string>();
212
+
213
+ protected withoutRejectedReasoningSummary(settings: Record<string, unknown>, modelId: string): Record<string, unknown> {
214
+ const reasoning = settings.reasoning;
215
+ if (!this.reasoningSummaryRejectedModels.has(modelId) || !isObject(reasoning) || !('summary' in reasoning)) {
216
+ return settings;
217
+ }
218
+ const withoutSummary = { ...reasoning };
219
+ delete withoutSummary.summary;
220
+ return { ...settings, reasoning: withoutSummary };
221
+ }
222
+
223
+ /**
224
+ * Returns `true` and remembers the rejection when `error` is the 400 an unverified organization gets for `reasoning.summary`,
225
+ * so the request can be sent once more without it.
226
+ */
227
+ protected handleReasoningSummaryRejection(error: unknown, sentSettings: Record<string, unknown>, modelId: string): boolean {
228
+ const reasoning = sentSettings.reasoning;
229
+ if (!(error instanceof OpenAI.BadRequestError) || error.param !== 'reasoning.summary' || !isObject(reasoning) || !('summary' in reasoning)) {
230
+ return false;
231
+ }
232
+ this.logger.warn(`Model ${modelId} rejected reasoning summaries (${error.message}); continuing without them.`);
233
+ this.reasoningSummaryRejectedModels.add(modelId);
234
+ return true;
235
+ }
236
+
237
+ /**
238
+ * Sends a Responses API request, retrying once without `reasoning.summary` if the organization is not allowed to request it.
239
+ */
240
+ async sendWithReasoningSummaryFallback<T>(
241
+ settings: Record<string, unknown>,
242
+ modelId: string,
243
+ send: (settings: Record<string, unknown>) => Promise<T>
244
+ ): Promise<T> {
245
+ const sentSettings = this.withoutRejectedReasoningSummary(settings, modelId);
246
+ try {
247
+ return await send(sentSettings);
248
+ } catch (error) {
249
+ if (!this.handleReasoningSummaryRejection(error, sentSettings, modelId)) {
250
+ throw error;
251
+ }
252
+ return send(this.withoutRejectedReasoningSummary(settings, modelId));
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Streaming counterpart of {@link sendWithReasoningSummaryFallback}. The rejection arrives before any event, so nothing is emitted twice.
258
+ */
259
+ async *streamWithReasoningSummaryFallback(
260
+ settings: Record<string, unknown>,
261
+ modelId: string,
262
+ open: (settings: Record<string, unknown>) => AsyncIterable<ResponseStreamEvent>
263
+ ): AsyncIterable<ResponseStreamEvent> {
264
+ const sentSettings = this.withoutRejectedReasoningSummary(settings, modelId);
265
+ let received = false;
266
+ try {
267
+ for await (const event of open(sentSettings)) {
268
+ received = true;
269
+ yield event;
270
+ }
271
+ } catch (error) {
272
+ if (received || !this.handleReasoningSummaryRejection(error, sentSettings, modelId)) {
273
+ throw error;
274
+ }
275
+ yield* open(this.withoutRejectedReasoningSummary(settings, modelId));
276
+ }
277
+ }
278
+
192
279
  protected createSimpleResponseApiStreamIterator(
193
280
  stream: AsyncIterable<ResponseStreamEvent>,
194
281
  cancellationToken?: CancellationToken
195
282
  ): AsyncIterable<LanguageModelStreamResponsePart> {
196
283
 
197
284
  const logger = this.logger;
285
+ const reasoningSummaryThought = (event: ResponseStreamEvent): ThinkingResponsePart | undefined => this.reasoningSummaryThought(event);
198
286
 
199
287
  return {
200
288
  async *[Symbol.asyncIterator](): AsyncIterator<LanguageModelStreamResponsePart> {
@@ -210,6 +298,11 @@ export class OpenAiResponseApiUtils {
210
298
  yield {
211
299
  content: event.delta
212
300
  };
301
+ } else if (event.type === 'response.reasoning_summary_part.added' || event.type === 'response.reasoning_summary_text.delta') {
302
+ const thought = reasoningSummaryThought(event);
303
+ if (thought) {
304
+ yield thought;
305
+ }
213
306
  } else if (event.type === 'response.output_item.done' && event.item?.type === 'compaction') {
214
307
  yield {
215
308
  compaction: {
@@ -500,13 +593,13 @@ class ResponseApiToolCallIterator implements AsyncIterableIterator<LanguageModel
500
593
 
501
594
  if (this.isStreaming) {
502
595
  // Use streaming API
503
- const stream = this.openai.responses.stream({
596
+ const stream = this.utils.streamWithReasoningSummaryFallback(this.settings, this.modelId, settings => this.openai.responses.stream({
504
597
  model: this.model as ResponsesModel,
505
598
  instructions: this.instructions,
506
599
  input: this.currentInput,
507
600
  tools: this.tools,
508
- ...this.settings
509
- });
601
+ ...settings
602
+ }));
510
603
 
511
604
  for await (const event of stream) {
512
605
  if (this.cancellationToken?.isCancellationRequested) {
@@ -521,13 +614,13 @@ class ResponseApiToolCallIterator implements AsyncIterableIterator<LanguageModel
521
614
  }
522
615
 
523
616
  protected async processNonStreamingResponse(): Promise<void> {
524
- const response = await this.openai.responses.create({
617
+ const response = await this.utils.sendWithReasoningSummaryFallback(this.settings, this.modelId, settings => this.openai.responses.create({
525
618
  model: this.model as ResponsesModel,
526
619
  instructions: this.instructions,
527
620
  input: this.currentInput,
528
621
  tools: this.tools,
529
- ...this.settings
530
- });
622
+ ...settings
623
+ }));
531
624
 
532
625
  // Record token usage
533
626
  if (response.usage) {
@@ -619,6 +712,15 @@ class ResponseApiToolCallIterator implements AsyncIterableIterator<LanguageModel
619
712
  }
620
713
  break;
621
714
 
715
+ case 'response.reasoning_summary_part.added':
716
+ case 'response.reasoning_summary_text.delta': {
717
+ const thought = this.utils.reasoningSummaryThought(event);
718
+ if (thought) {
719
+ this.handleIncoming(thought);
720
+ }
721
+ break;
722
+ }
723
+
622
724
  case 'response.completed':
623
725
  if (event.response?.usage) {
624
726
  this.handleIncoming({