@llumiverse/drivers 0.23.0 → 0.24.0-dev.202601221707

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 (78) hide show
  1. package/README.md +141 -218
  2. package/lib/cjs/azure/azure_foundry.js +46 -2
  3. package/lib/cjs/azure/azure_foundry.js.map +1 -1
  4. package/lib/cjs/bedrock/index.js +236 -16
  5. package/lib/cjs/bedrock/index.js.map +1 -1
  6. package/lib/cjs/groq/index.js +115 -85
  7. package/lib/cjs/groq/index.js.map +1 -1
  8. package/lib/cjs/index.js +1 -0
  9. package/lib/cjs/index.js.map +1 -1
  10. package/lib/cjs/openai/index.js +310 -114
  11. package/lib/cjs/openai/index.js.map +1 -1
  12. package/lib/cjs/openai/openai_compatible.js +62 -0
  13. package/lib/cjs/openai/openai_compatible.js.map +1 -0
  14. package/lib/cjs/openai/openai_format.js +32 -39
  15. package/lib/cjs/openai/openai_format.js.map +1 -1
  16. package/lib/cjs/vertexai/index.js +165 -0
  17. package/lib/cjs/vertexai/index.js.map +1 -1
  18. package/lib/cjs/vertexai/models/claude.js +201 -3
  19. package/lib/cjs/vertexai/models/claude.js.map +1 -1
  20. package/lib/cjs/vertexai/models/gemini.js +59 -20
  21. package/lib/cjs/vertexai/models/gemini.js.map +1 -1
  22. package/lib/cjs/xai/index.js +10 -16
  23. package/lib/cjs/xai/index.js.map +1 -1
  24. package/lib/esm/azure/azure_foundry.js +46 -2
  25. package/lib/esm/azure/azure_foundry.js.map +1 -1
  26. package/lib/esm/bedrock/index.js +236 -17
  27. package/lib/esm/bedrock/index.js.map +1 -1
  28. package/lib/esm/groq/index.js +115 -85
  29. package/lib/esm/groq/index.js.map +1 -1
  30. package/lib/esm/index.js +1 -0
  31. package/lib/esm/index.js.map +1 -1
  32. package/lib/esm/openai/index.js +311 -115
  33. package/lib/esm/openai/index.js.map +1 -1
  34. package/lib/esm/openai/openai_compatible.js +55 -0
  35. package/lib/esm/openai/openai_compatible.js.map +1 -0
  36. package/lib/esm/openai/openai_format.js +32 -39
  37. package/lib/esm/openai/openai_format.js.map +1 -1
  38. package/lib/esm/vertexai/index.js +166 -1
  39. package/lib/esm/vertexai/index.js.map +1 -1
  40. package/lib/esm/vertexai/models/claude.js +199 -3
  41. package/lib/esm/vertexai/models/claude.js.map +1 -1
  42. package/lib/esm/vertexai/models/gemini.js +60 -21
  43. package/lib/esm/vertexai/models/gemini.js.map +1 -1
  44. package/lib/esm/xai/index.js +10 -16
  45. package/lib/esm/xai/index.js.map +1 -1
  46. package/lib/types/azure/azure_foundry.d.ts +7 -5
  47. package/lib/types/azure/azure_foundry.d.ts.map +1 -1
  48. package/lib/types/bedrock/index.d.ts +21 -1
  49. package/lib/types/bedrock/index.d.ts.map +1 -1
  50. package/lib/types/groq/index.d.ts.map +1 -1
  51. package/lib/types/index.d.ts +1 -0
  52. package/lib/types/index.d.ts.map +1 -1
  53. package/lib/types/openai/index.d.ts +13 -7
  54. package/lib/types/openai/index.d.ts.map +1 -1
  55. package/lib/types/openai/openai_compatible.d.ts +26 -0
  56. package/lib/types/openai/openai_compatible.d.ts.map +1 -0
  57. package/lib/types/openai/openai_format.d.ts +4 -2
  58. package/lib/types/openai/openai_format.d.ts.map +1 -1
  59. package/lib/types/vertexai/index.d.ts +15 -0
  60. package/lib/types/vertexai/index.d.ts.map +1 -1
  61. package/lib/types/vertexai/models/claude.d.ts +20 -0
  62. package/lib/types/vertexai/models/claude.d.ts.map +1 -1
  63. package/lib/types/vertexai/models/gemini.d.ts +1 -1
  64. package/lib/types/vertexai/models/gemini.d.ts.map +1 -1
  65. package/lib/types/xai/index.d.ts +2 -3
  66. package/lib/types/xai/index.d.ts.map +1 -1
  67. package/package.json +12 -12
  68. package/src/azure/azure_foundry.ts +56 -7
  69. package/src/bedrock/index.ts +297 -26
  70. package/src/groq/index.ts +120 -94
  71. package/src/index.ts +1 -0
  72. package/src/openai/index.ts +363 -136
  73. package/src/openai/openai_compatible.ts +74 -0
  74. package/src/openai/openai_format.ts +44 -54
  75. package/src/vertexai/index.ts +205 -0
  76. package/src/vertexai/models/claude.ts +233 -3
  77. package/src/vertexai/models/gemini.ts +78 -27
  78. package/src/xai/index.ts +10 -17
@@ -19,15 +19,21 @@ import {
19
19
  TrainingJobStatus,
20
20
  TrainingOptions,
21
21
  TrainingPromptOptions,
22
+ getConversationMeta,
22
23
  getModelCapabilities,
24
+ incrementConversationTurn,
23
25
  modelModalitiesToArray,
26
+ stripBase64ImagesFromConversation,
24
27
  supportsToolUse,
28
+ truncateLargeTextInConversation,
29
+ unwrapConversationArray,
25
30
  } from "@llumiverse/core";
26
- import { asyncMap } from "@llumiverse/core/async";
27
- import { formatOpenAILikeMultimodalPrompt } from "./openai_format.js";
28
31
  import OpenAI, { AzureOpenAI } from "openai";
29
- import { ChatCompletionMessageParam } from "openai/resources/chat/completions";
30
- import { Stream } from "openai/streaming";
32
+ import { formatOpenAILikeMultimodalPrompt } from "./openai_format.js";
33
+
34
+ // Response API types
35
+ type ResponseInputItem = OpenAI.Responses.ResponseInputItem;
36
+ type EasyInputMessage = OpenAI.Responses.EasyInputMessage;
31
37
 
32
38
  // Helper function to convert string to CompletionResult[]
33
39
  function textToCompletionResult(text: string): CompletionResult[] {
@@ -48,32 +54,24 @@ export interface BaseOpenAIDriverOptions extends DriverOptions {
48
54
 
49
55
  export abstract class BaseOpenAIDriver extends AbstractDriver<
50
56
  BaseOpenAIDriverOptions,
51
- ChatCompletionMessageParam[]
57
+ ResponseInputItem[]
52
58
  > {
53
- //abstract provider: "azure_openai" | "openai" | "xai" | "azure_foundry";
54
- abstract provider: Providers.openai | Providers.azure_openai | "xai" | Providers.azure_foundry;
59
+ abstract provider: Providers.openai | Providers.azure_openai | Providers.xai | Providers.azure_foundry | Providers.openai_compatible;
55
60
  abstract service: OpenAI | AzureOpenAI;
56
61
 
57
62
  constructor(opts: BaseOpenAIDriverOptions) {
58
63
  super(opts);
59
- this.formatPrompt = formatOpenAILikeMultimodalPrompt
60
- //TODO: better type, we send back OpenAI.Chat.Completions.ChatCompletionMessageParam[] but just not compatible with Function call that we don't use here
64
+ this.formatPrompt = formatOpenAILikeMultimodalPrompt;
61
65
  }
62
66
 
63
67
  extractDataFromResponse(
64
68
  _options: ExecutionOptions,
65
- result: OpenAI.Chat.Completions.ChatCompletion
69
+ result: OpenAI.Responses.Response
66
70
  ): Completion {
67
- const tokenInfo: ExecutionTokenUsage = {
68
- prompt: result.usage?.prompt_tokens,
69
- result: result.usage?.completion_tokens,
70
- total: result.usage?.total_tokens,
71
- };
72
-
73
- const choice = result.choices[0];
71
+ const tokenInfo = mapUsage(result.usage);
74
72
 
75
- const tools = collectTools(choice.message.tool_calls);
76
- const data = choice.message.content ?? undefined;
73
+ const tools = collectTools(result.output);
74
+ const data = extractTextFromResponse(result);
77
75
 
78
76
  if (!data && !tools) {
79
77
  this.logger.error({ result }, "[OpenAI] Response is not valid");
@@ -83,37 +81,21 @@ export abstract class BaseOpenAIDriver extends AbstractDriver<
83
81
  return {
84
82
  result: textToCompletionResult(data || ''),
85
83
  token_usage: tokenInfo,
86
- finish_reason: openAiFinishReason(choice.finish_reason),
84
+ finish_reason: responseFinishReason(result, tools),
87
85
  tool_use: tools,
88
86
  };
89
87
  }
90
88
 
91
- async requestTextCompletionStream(prompt: ChatCompletionMessageParam[], options: ExecutionOptions): Promise<AsyncIterable<CompletionChunkObject>> {
89
+ async requestTextCompletionStream(prompt: ResponseInputItem[], options: ExecutionOptions): Promise<AsyncIterable<CompletionChunkObject>> {
92
90
  if (options.model_options?._option_id !== "openai-text" && options.model_options?._option_id !== "openai-thinking") {
93
91
  this.logger.warn({ options: options.model_options }, "Invalid model options");
94
92
  }
95
93
 
96
- const toolDefs = getToolDefinitions(options.tools);
97
- const useTools: boolean = toolDefs ? supportsToolUse(options.model, "openai", true) : false;
98
-
99
- const mapFn = (chunk: OpenAI.Chat.Completions.ChatCompletionChunk) => {
100
- let result = undefined
101
- if (useTools && this.provider !== "xai" && options.result_schema) {
102
- result = chunk.choices[0]?.delta?.tool_calls?.[0].function?.arguments ?? "";
103
- } else {
104
- result = chunk.choices[0]?.delta.content ?? "";
105
- }
94
+ // Include conversation history (same as non-streaming)
95
+ const conversation = updateConversation(options.conversation, prompt);
106
96
 
107
- return {
108
- result: textToCompletionResult(result),
109
- finish_reason: openAiFinishReason(chunk.choices[0]?.finish_reason ?? undefined), //Uses expected "stop" , "length" format
110
- token_usage: {
111
- prompt: chunk.usage?.prompt_tokens,
112
- result: chunk.usage?.completion_tokens,
113
- total: (chunk.usage?.prompt_tokens ?? 0) + (chunk.usage?.completion_tokens ?? 0),
114
- }
115
- } satisfies CompletionChunkObject;
116
- };
97
+ const toolDefs = getToolDefinitions(options.tools);
98
+ const useTools: boolean = toolDefs ? supportsToolUse(options.model, this.provider, true) : false;
117
99
 
118
100
  convertRoles(prompt, options.model);
119
101
 
@@ -133,35 +115,31 @@ export abstract class BaseOpenAIDriver extends AbstractDriver<
133
115
  }
134
116
  }
135
117
 
136
- const stream = await this.service.chat.completions.create({
118
+ const reasoning = model_options?.reasoning_effort ? { effort: model_options.reasoning_effort } : undefined;
119
+
120
+ const stream = await this.service.responses.create({
137
121
  stream: true,
138
- stream_options: { include_usage: true },
139
122
  model: options.model,
140
- messages: prompt,
141
- reasoning_effort: model_options?.reasoning_effort,
123
+ input: conversation,
124
+ reasoning,
142
125
  temperature: model_options?.temperature,
143
126
  top_p: model_options?.top_p,
144
- presence_penalty: model_options?.presence_penalty,
145
- frequency_penalty: model_options?.frequency_penalty,
146
- n: 1,
147
- max_completion_tokens: model_options?.max_tokens, //TODO: use max_tokens for older models, currently relying on OpenAI to handle it
127
+ max_output_tokens: model_options?.max_tokens,
148
128
  tools: useTools ? toolDefs : undefined,
149
- stop: model_options?.stop_sequence,
150
- response_format: parsedSchema ? {
151
- type: "json_schema",
152
- json_schema: {
129
+ text: parsedSchema ? {
130
+ format: {
131
+ type: "json_schema",
153
132
  name: "format_output",
154
133
  schema: parsedSchema,
155
134
  strict: strictMode,
156
135
  }
157
136
  } : undefined,
158
- } satisfies OpenAI.Chat.ChatCompletionCreateParamsStreaming
159
- ) satisfies Stream<OpenAI.Chat.Completions.ChatCompletionChunk>;
137
+ });
160
138
 
161
- return asyncMap(stream, mapFn);
139
+ return mapResponseStream(stream);
162
140
  }
163
141
 
164
- async requestTextCompletion(prompt: ChatCompletionMessageParam[], options: ExecutionOptions): Promise<Completion> {
142
+ async requestTextCompletion(prompt: ResponseInputItem[], options: ExecutionOptions): Promise<Completion> {
165
143
  if (options.model_options?._option_id !== "openai-text" && options.model_options?._option_id !== "openai-thinking") {
166
144
  this.logger.warn({ options: options.model_options }, "Invalid model options");
167
145
  }
@@ -172,9 +150,9 @@ export abstract class BaseOpenAIDriver extends AbstractDriver<
172
150
  insert_image_detail(prompt, model_options?.image_detail ?? "auto");
173
151
 
174
152
  const toolDefs = getToolDefinitions(options.tools);
175
- const useTools: boolean = toolDefs ? supportsToolUse(options.model, "openai") : false;
153
+ const useTools: boolean = toolDefs ? supportsToolUse(options.model, this.provider) : false;
176
154
 
177
- let conversation = updateConversation(options.conversation as ChatCompletionMessageParam[], prompt);
155
+ let conversation = updateConversation(options.conversation, prompt);
178
156
 
179
157
  let parsedSchema: JSONSchema | undefined = undefined;
180
158
  let strictMode = false;
@@ -189,22 +167,20 @@ export abstract class BaseOpenAIDriver extends AbstractDriver<
189
167
  }
190
168
  }
191
169
 
192
- const res = await this.service.chat.completions.create({
170
+ const reasoning = model_options?.reasoning_effort ? { effort: model_options.reasoning_effort } : undefined;
171
+
172
+ const res = await this.service.responses.create({
193
173
  stream: false,
194
174
  model: options.model,
195
- messages: conversation,
196
- reasoning_effort: model_options?.reasoning_effort,
175
+ input: conversation,
176
+ reasoning,
197
177
  temperature: model_options?.temperature,
198
178
  top_p: model_options?.top_p,
199
- presence_penalty: model_options?.presence_penalty,
200
- frequency_penalty: model_options?.frequency_penalty,
201
- n: 1,
202
- max_completion_tokens: model_options?.max_tokens, //TODO: use max_tokens for older models, currently relying on OpenAI to handle it
179
+ max_output_tokens: model_options?.max_tokens, //TODO: use max_tokens for older models, currently relying on OpenAI to handle it
203
180
  tools: useTools ? toolDefs : undefined,
204
- stop: model_options?.stop_sequence,
205
- response_format: parsedSchema ? {
206
- type: "json_schema",
207
- json_schema: {
181
+ text: parsedSchema ? {
182
+ format: {
183
+ type: "json_schema",
208
184
  name: "format_output",
209
185
  schema: parsedSchema,
210
186
  strict: strictMode,
@@ -217,8 +193,24 @@ export abstract class BaseOpenAIDriver extends AbstractDriver<
217
193
  completion.original_response = res;
218
194
  }
219
195
 
220
- conversation = updateConversation(conversation, createPromptFromResponse(res.choices[0].message));
221
- completion.conversation = conversation;
196
+ conversation = updateConversation(conversation, createAssistantMessageFromCompletion(completion));
197
+
198
+ // Increment turn counter for deferred stripping
199
+ conversation = incrementConversationTurn(conversation) as ResponseInputItem[];
200
+
201
+ // Strip large base64 image data based on options.stripImagesAfterTurns
202
+ const currentTurn = getConversationMeta(conversation).turnNumber;
203
+ const stripOptions = {
204
+ keepForTurns: options.stripImagesAfterTurns ?? Infinity,
205
+ currentTurn,
206
+ textMaxTokens: options.stripTextMaxTokens
207
+ };
208
+ let processedConversation = stripBase64ImagesFromConversation(conversation, stripOptions);
209
+
210
+ // Truncate large text content if configured
211
+ processedConversation = truncateLargeTextInConversation(processedConversation, stripOptions);
212
+
213
+ completion.conversation = processedConversation;
222
214
 
223
215
  return completion;
224
216
  }
@@ -233,6 +225,60 @@ export abstract class BaseOpenAIDriver extends AbstractDriver<
233
225
  return Promise.resolve(true);
234
226
  }
235
227
 
228
+ /**
229
+ * Build conversation context after streaming completion.
230
+ * Reconstructs the assistant message from accumulated results and applies stripping.
231
+ */
232
+ buildStreamingConversation(
233
+ prompt: ResponseInputItem[],
234
+ result: unknown[],
235
+ toolUse: unknown[] | undefined,
236
+ options: ExecutionOptions
237
+ ): ResponseInputItem[] | undefined {
238
+ // Build assistant message from accumulated CompletionResult[]
239
+ const completionResults = result as CompletionResult[];
240
+
241
+ const textContent = completionResultsToText(completionResults);
242
+
243
+ // Start with the conversation from options or the prompt
244
+ let conversation = updateConversation(options.conversation, prompt);
245
+
246
+ // Add assistant message as EasyInputMessage
247
+ if (textContent) {
248
+ const assistantMessage: EasyInputMessage = {
249
+ role: 'assistant',
250
+ content: textContent,
251
+ };
252
+ conversation = updateConversation(conversation, [assistantMessage]);
253
+ }
254
+
255
+ // Add function calls as separate items (Response API format)
256
+ if (toolUse && toolUse.length > 0) {
257
+ const functionCalls: OpenAI.Responses.ResponseFunctionToolCall[] = (toolUse as ToolUse[]).map(t => ({
258
+ type: 'function_call' as const,
259
+ call_id: t.id,
260
+ name: t.tool_name,
261
+ arguments: typeof t.tool_input === 'string' ? t.tool_input : JSON.stringify(t.tool_input ?? {}),
262
+ }));
263
+ conversation = updateConversation(conversation, functionCalls);
264
+ }
265
+
266
+ // Increment turn counter
267
+ conversation = incrementConversationTurn(conversation) as ResponseInputItem[];
268
+
269
+ // Apply stripping based on options
270
+ const currentTurn = getConversationMeta(conversation).turnNumber;
271
+ const stripOptions = {
272
+ keepForTurns: options.stripImagesAfterTurns ?? Infinity,
273
+ currentTurn,
274
+ textMaxTokens: options.stripTextMaxTokens
275
+ };
276
+ let processedConversation = stripBase64ImagesFromConversation(conversation, stripOptions);
277
+ processedConversation = truncateLargeTextInConversation(processedConversation, stripOptions);
278
+
279
+ return processedConversation as ResponseInputItem[];
280
+ }
281
+
236
282
  createTrainingPrompt(options: TrainingPromptOptions): Promise<string> {
237
283
  if (options.model.includes("gpt")) {
238
284
  return super.createTrainingPrompt(options);
@@ -377,44 +423,175 @@ function jobInfo(job: OpenAI.FineTuning.Jobs.FineTuningJob): TrainingJob {
377
423
  }
378
424
  }
379
425
 
380
- function insert_image_detail(messages: ChatCompletionMessageParam[], detail_level: string): ChatCompletionMessageParam[] {
381
- if (detail_level == "auto" || detail_level == "low" || detail_level == "high") {
382
- for (const message of messages) {
383
- if (message.role !== 'assistant' && message.content) {
384
- for (const part of message.content) {
385
- if (typeof part === "string") {
386
- continue;
426
+ function mapUsage(usage?: OpenAI.Responses.ResponseUsage | null): ExecutionTokenUsage | undefined {
427
+ if (!usage) {
428
+ return undefined;
429
+ }
430
+ return {
431
+ prompt: usage.input_tokens,
432
+ result: usage.output_tokens,
433
+ total: usage.total_tokens,
434
+ };
435
+ }
436
+
437
+ function completionResultsToText(completionResults: CompletionResult[] | undefined): string {
438
+ if (!completionResults) {
439
+ return '';
440
+ }
441
+ return completionResults
442
+ .map(r => {
443
+ switch (r.type) {
444
+ case 'text':
445
+ return r.value;
446
+ case 'json':
447
+ return typeof r.value === 'string' ? r.value : JSON.stringify(r.value);
448
+ case 'image':
449
+ // Skip images in conversation - they're in the result
450
+ return '';
451
+ default:
452
+ return String((r as any).value || '');
453
+ }
454
+ })
455
+ .join('');
456
+ }
457
+
458
+ function createAssistantMessageFromCompletion(completion: Completion): ResponseInputItem[] {
459
+ const textContent = completionResultsToText(completion.result);
460
+ const result: ResponseInputItem[] = [];
461
+
462
+ // Add assistant text message if present
463
+ if (textContent) {
464
+ const assistantMessage: EasyInputMessage = {
465
+ role: 'assistant',
466
+ content: textContent,
467
+ };
468
+ result.push(assistantMessage);
469
+ }
470
+
471
+ // Add function calls as separate items (Response API format)
472
+ if (completion.tool_use && completion.tool_use.length > 0) {
473
+ for (const t of completion.tool_use) {
474
+ const functionCall: OpenAI.Responses.ResponseFunctionToolCall = {
475
+ type: 'function_call',
476
+ call_id: t.id,
477
+ name: t.tool_name,
478
+ arguments: typeof t.tool_input === 'string'
479
+ ? t.tool_input
480
+ : JSON.stringify(t.tool_input ?? {}),
481
+ };
482
+ result.push(functionCall);
483
+ }
484
+ }
485
+
486
+ return result;
487
+ }
488
+
489
+ function mapResponseStream(stream: AsyncIterable<OpenAI.Responses.ResponseStreamEvent>): AsyncIterable<CompletionChunkObject> {
490
+ const toolCallMetadata = new Map<string, { syntheticId: string, name?: string }>();
491
+
492
+ return {
493
+ async *[Symbol.asyncIterator]() {
494
+ for await (const event of stream) {
495
+ if (event.type === 'response.output_item.added' && event.item.type === 'function_call') {
496
+ const syntheticId = `tool_${event.output_index}`;
497
+ const actualId = event.item.id ?? event.item.call_id;
498
+ if (actualId) {
499
+ toolCallMetadata.set(actualId, { syntheticId, name: event.item.name });
500
+ }
501
+ const toolUse: ToolUse & { _actual_id?: string } = {
502
+ id: syntheticId,
503
+ _actual_id: actualId,
504
+ tool_name: event.item.name,
505
+ tool_input: '' as any,
506
+ };
507
+ yield {
508
+ result: [],
509
+ tool_use: [toolUse],
510
+ } satisfies CompletionChunkObject;
511
+ } else if (event.type === 'response.function_call_arguments.delta') {
512
+ const metadata = toolCallMetadata.get(event.item_id);
513
+ const syntheticId = metadata?.syntheticId ?? `tool_${event.output_index}`;
514
+ const toolUse: ToolUse & { _actual_id?: string } = {
515
+ id: syntheticId,
516
+ _actual_id: event.item_id,
517
+ tool_name: metadata?.name ?? '',
518
+ tool_input: event.delta as any,
519
+ };
520
+ yield {
521
+ result: [],
522
+ tool_use: [toolUse],
523
+ } satisfies CompletionChunkObject;
524
+ }
525
+ // Note: We don't emit response.function_call_arguments.done because the arguments were already
526
+ // streamed via delta events. Emitting it again would duplicate the tool_input content.
527
+ // We only update the metadata to ensure the tool name is captured.
528
+ else if (event.type === 'response.function_call_arguments.done') {
529
+ // Just update metadata, don't yield (arguments already accumulated from delta events)
530
+ const metadata = toolCallMetadata.get(event.item_id);
531
+ const syntheticId = metadata?.syntheticId ?? `tool_${event.output_index}`;
532
+ const tool_name = metadata?.name ?? event.name ?? '';
533
+ if (event.item_id) {
534
+ toolCallMetadata.set(event.item_id, { syntheticId, name: tool_name });
387
535
  }
388
- if (part.type === 'image_url') {
389
- part.image_url = { ...part.image_url, detail: detail_level };
536
+ } else if (event.type === 'response.output_text.delta') {
537
+ yield {
538
+ result: textToCompletionResult(event.delta),
539
+ } satisfies CompletionChunkObject;
540
+ }
541
+ // Note: We don't emit response.output_text.done because the text was already
542
+ // streamed via delta events. Emitting it again would duplicate the content.
543
+ else if (event.type === 'response.completed' || event.type === 'response.incomplete' || event.type === 'response.failed') {
544
+ const finalTools = collectTools(event.response.output);
545
+ yield {
546
+ result: [],
547
+ finish_reason: responseFinishReason(event.response, finalTools),
548
+ token_usage: mapUsage(event.response.usage),
549
+ } satisfies CompletionChunkObject;
550
+ }
551
+ }
552
+ }
553
+ };
554
+ }
555
+
556
+ function insert_image_detail(items: ResponseInputItem[], detail_level: string): ResponseInputItem[] {
557
+ if (detail_level === "auto" || detail_level === "low" || detail_level === "high") {
558
+ for (const item of items) {
559
+ // Check if it's an EasyInputMessage or Message with content array
560
+ if ('role' in item && 'content' in item && item.role !== 'assistant') {
561
+ const content = (item as EasyInputMessage).content;
562
+ if (Array.isArray(content)) {
563
+ for (const part of content) {
564
+ if (typeof part === 'object' && part.type === 'input_image') {
565
+ (part as any).detail = detail_level;
566
+ }
390
567
  }
391
568
  }
392
569
  }
393
570
  }
394
571
  }
395
- return messages;
572
+ return items;
396
573
  }
397
574
 
398
- function convertRoles(messages: ChatCompletionMessageParam[], model: string): ChatCompletionMessageParam[] {
575
+ function convertRoles(items: ResponseInputItem[], model: string): ResponseInputItem[] {
399
576
  //New openai models use developer role instead of system
400
577
  if (model.includes("o1") || model.includes("o3")) {
401
578
  if (model.includes("o1-mini") || model.includes("o1-preview")) {
402
579
  //o1-mini and o1-preview support neither system nor developer
403
- for (const message of messages) {
404
- if (message.role === 'system') {
405
- (message.role as any) = 'user';
580
+ for (const item of items) {
581
+ if ('role' in item && (item as EasyInputMessage).role === 'system') {
582
+ (item as any).role = 'user';
406
583
  }
407
584
  }
408
585
  } else {
409
586
  //Models newer than o1 use developer role
410
- for (const message of messages) {
411
- if (message.role === 'system') {
412
- (message.role as any) = 'developer';
587
+ for (const item of items) {
588
+ if ('role' in item && (item as EasyInputMessage).role === 'system') {
589
+ (item as any).role = 'developer';
413
590
  }
414
591
  }
415
592
  }
416
593
  }
417
- return messages
594
+ return items;
418
595
  }
419
596
 
420
597
  //Structured output support is typically aligned with tool use support
@@ -427,10 +604,10 @@ function supportsSchema(model: string): boolean {
427
604
  return supportsToolUse(model, "openai");
428
605
  }
429
606
 
430
- function getToolDefinitions(tools: ToolDefinition[] | undefined | null): OpenAI.ChatCompletionTool[] | undefined {
607
+ function getToolDefinitions(tools: ToolDefinition[] | undefined | null): OpenAI.Responses.Tool[] | undefined {
431
608
  return tools ? tools.map(getToolDefinition) : undefined;
432
609
  }
433
- function getToolDefinition(toolDef: ToolDefinition): OpenAI.ChatCompletionTool {
610
+ function getToolDefinition(toolDef: ToolDefinition): OpenAI.Responses.FunctionTool {
434
611
  let parsedSchema: JSONSchema | undefined = undefined;
435
612
  let strictMode = false;
436
613
  if (toolDef.input_schema) {
@@ -446,64 +623,50 @@ function getToolDefinition(toolDef: ToolDefinition): OpenAI.ChatCompletionTool {
446
623
 
447
624
  return {
448
625
  type: "function",
449
- function: {
450
- name: toolDef.name,
451
- description: toolDef.description,
452
- parameters: parsedSchema,
453
- strict: strictMode,
454
- },
455
- } satisfies OpenAI.ChatCompletionTool;
456
- }
457
-
458
- function openAiFinishReason(finish_reason?: string): string | undefined {
459
- if (finish_reason === "tool_calls") {
460
- return "tool_use";
461
- }
462
- return finish_reason;
626
+ name: toolDef.name,
627
+ description: toolDef.description,
628
+ parameters: parsedSchema ?? null,
629
+ strict: strictMode,
630
+ };
463
631
  }
464
632
 
465
- function updateConversation(conversation: ChatCompletionMessageParam[], message: ChatCompletionMessageParam[]): ChatCompletionMessageParam[] {
466
- if (!message) {
467
- return conversation;
633
+ function updateConversation(conversation: unknown, items: ResponseInputItem[]): ResponseInputItem[] {
634
+ if (!items) {
635
+ // Unwrap array if wrapped, otherwise treat as array
636
+ const unwrapped = unwrapConversationArray<ResponseInputItem>(conversation);
637
+ return unwrapped ?? (conversation as ResponseInputItem[] || []);
468
638
  }
469
639
  if (!conversation) {
470
- return message;
640
+ return items;
471
641
  }
472
- return [...conversation, ...message];
642
+ // Unwrap array if wrapped, otherwise treat as array
643
+ const unwrapped = unwrapConversationArray<ResponseInputItem>(conversation);
644
+ const convArray = unwrapped ?? (conversation as ResponseInputItem[]);
645
+ return [...convArray, ...items];
473
646
  }
474
647
 
475
- export function collectTools(toolCalls?: OpenAI.Chat.Completions.ChatCompletionMessageToolCall[]): ToolUse[] | undefined {
476
- if (!toolCalls) {
648
+ export function collectTools(output?: OpenAI.Responses.ResponseOutputItem[]): ToolUse[] | undefined {
649
+ if (!output) {
477
650
  return undefined;
478
651
  }
479
652
 
480
653
  const tools: ToolUse[] = [];
481
- for (const call of toolCalls) {
482
- tools.push({
483
- id: call.id,
484
- tool_name: call.function.name,
485
- tool_input: JSON.parse(call.function.arguments),
486
- });
487
-
654
+ for (const item of output) {
655
+ if (item.type === 'function_call') {
656
+ const id = item.call_id || item.id;
657
+ if (!id) {
658
+ continue;
659
+ }
660
+ tools.push({
661
+ id,
662
+ tool_name: item.name ?? '',
663
+ tool_input: safeJsonParse(item.arguments),
664
+ });
665
+ }
488
666
  }
489
667
  return tools.length > 0 ? tools : undefined;
490
668
  }
491
669
 
492
- function createPromptFromResponse(response: OpenAI.Chat.Completions.ChatCompletionMessage): ChatCompletionMessageParam[] {
493
- const messages: ChatCompletionMessageParam[] = [];
494
- if (response) {
495
- messages.push({
496
- role: response.role,
497
- content: [{
498
- type: "text",
499
- text: response.content ?? ""
500
- }],
501
- tool_calls: response.tool_calls,
502
- });
503
- }
504
- return messages;
505
- }
506
-
507
670
  //For strict mode false
508
671
  function limitedSchemaFormat(schema: JSONSchema): JSONSchema {
509
672
  const formattedSchema = { ...schema };
@@ -511,6 +674,18 @@ function limitedSchemaFormat(schema: JSONSchema): JSONSchema {
511
674
  // Defaults not supported
512
675
  delete formattedSchema.default;
513
676
 
677
+ // OpenAI requires type field even in non-strict mode
678
+ // If no type is specified, default to 'object' for properties with format/editor hints,
679
+ // otherwise 'string' as a safe fallback
680
+ if (!formattedSchema.type && formattedSchema.description) {
681
+ // Properties with format: "document" or editor hints are typically objects
682
+ if (formattedSchema.format === 'document' || formattedSchema.editor) {
683
+ formattedSchema.type = 'object';
684
+ } else {
685
+ formattedSchema.type = 'string';
686
+ }
687
+ }
688
+
514
689
  if (formattedSchema?.properties) {
515
690
  // Process each property recursively
516
691
  for (const propName of Object.keys(formattedSchema.properties)) {
@@ -556,6 +731,11 @@ function openAISchemaFormat(schema: JSONSchema, nesting: number = 0): JSONSchema
556
731
  for (const propName of Object.keys(formattedSchema.properties)) {
557
732
  const property = formattedSchema.properties[propName];
558
733
 
734
+ // OpenAI strict mode requires all properties to have a type
735
+ if (!property?.type) {
736
+ throw new Error(`Property '${propName}' is missing required 'type' field for OpenAI strict mode`);
737
+ }
738
+
559
739
  // Recursively process properties
560
740
  formattedSchema.properties[propName] = openAISchemaFormat(property, nesting + 1);
561
741
 
@@ -575,4 +755,51 @@ function openAISchemaFormat(schema: JSONSchema, nesting: number = 0): JSONSchema
575
755
  }
576
756
 
577
757
  return formattedSchema
578
- }
758
+ }
759
+
760
+ function extractTextFromResponse(response: OpenAI.Responses.Response): string {
761
+ if (response.output_text) {
762
+ return response.output_text;
763
+ }
764
+
765
+ const collected: string[] = [];
766
+ for (const item of response.output ?? []) {
767
+ if (item.type === 'message') {
768
+ const text = item.content
769
+ .map(part => part.type === 'output_text' ? part.text : '')
770
+ .join('');
771
+ if (text) {
772
+ collected.push(text);
773
+ }
774
+ }
775
+ }
776
+
777
+ return collected.join("\n");
778
+ }
779
+
780
+ function responseFinishReason(response: OpenAI.Responses.Response, tools?: ToolUse[] | undefined): string | undefined {
781
+ if (tools && tools.length > 0) {
782
+ return "tool_use";
783
+ }
784
+ if (response.status === 'incomplete') {
785
+ if (response.incomplete_details?.reason === 'max_output_tokens') {
786
+ return 'length';
787
+ }
788
+ return response.incomplete_details?.reason ?? 'incomplete';
789
+ }
790
+ if (response.status && response.status !== 'completed') {
791
+ return response.status;
792
+ }
793
+ return 'stop';
794
+ }
795
+
796
+ function safeJsonParse(value: unknown): any {
797
+ if (typeof value !== 'string') {
798
+ return value;
799
+ }
800
+ try {
801
+ return JSON.parse(value);
802
+ } catch {
803
+ return value;
804
+ }
805
+ }