@llumiverse/drivers 0.23.0 → 0.24.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 (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 +140 -15
  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 +147 -0
  17. package/lib/cjs/vertexai/index.js.map +1 -1
  18. package/lib/cjs/vertexai/models/claude.js +88 -2
  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 +141 -16
  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 +148 -1
  39. package/lib/esm/vertexai/index.js.map +1 -1
  40. package/lib/esm/vertexai/models/claude.js +87 -2
  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 +5 -0
  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 +11 -0
  60. package/lib/types/vertexai/index.d.ts.map +1 -1
  61. package/lib/types/vertexai/models/claude.d.ts +8 -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 +188 -24
  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 +186 -0
  76. package/src/vertexai/models/claude.ts +97 -2
  77. package/src/vertexai/models/gemini.ts +78 -27
  78. package/src/xai/index.ts +10 -17
package/lib/esm/index.js CHANGED
@@ -5,6 +5,7 @@ export * from "./huggingface_ie.js";
5
5
  export * from "./mistral/index.js";
6
6
  export * from "./openai/azure_openai.js";
7
7
  export * from "./openai/openai.js";
8
+ export * from "./openai/openai_compatible.js";
8
9
  export * from "./replicate.js";
9
10
  export * from "./test-driver/index.js";
10
11
  export * from "./togetherai/index.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC;AACzC,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,0BAA0B,CAAC;AACzC,cAAc,oBAAoB,CAAC;AACnC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC;AACzC,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,0BAA0B,CAAC;AACzC,cAAc,oBAAoB,CAAC;AACnC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,gBAAgB,CAAC;AAC/B,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,gBAAgB,CAAC"}
@@ -1,5 +1,4 @@
1
- import { AbstractDriver, ModelType, TrainingJobStatus, getModelCapabilities, modelModalitiesToArray, supportsToolUse, } from "@llumiverse/core";
2
- import { asyncMap } from "@llumiverse/core/async";
1
+ import { AbstractDriver, ModelType, TrainingJobStatus, getConversationMeta, getModelCapabilities, incrementConversationTurn, modelModalitiesToArray, stripBase64ImagesFromConversation, supportsToolUse, truncateLargeTextInConversation, unwrapConversationArray, } from "@llumiverse/core";
3
2
  import { formatOpenAILikeMultimodalPrompt } from "./openai_format.js";
4
3
  // Helper function to convert string to CompletionResult[]
5
4
  function textToCompletionResult(text) {
@@ -17,17 +16,11 @@ export class BaseOpenAIDriver extends AbstractDriver {
17
16
  constructor(opts) {
18
17
  super(opts);
19
18
  this.formatPrompt = formatOpenAILikeMultimodalPrompt;
20
- //TODO: better type, we send back OpenAI.Chat.Completions.ChatCompletionMessageParam[] but just not compatible with Function call that we don't use here
21
19
  }
22
20
  extractDataFromResponse(_options, result) {
23
- const tokenInfo = {
24
- prompt: result.usage?.prompt_tokens,
25
- result: result.usage?.completion_tokens,
26
- total: result.usage?.total_tokens,
27
- };
28
- const choice = result.choices[0];
29
- const tools = collectTools(choice.message.tool_calls);
30
- const data = choice.message.content ?? undefined;
21
+ const tokenInfo = mapUsage(result.usage);
22
+ const tools = collectTools(result.output);
23
+ const data = extractTextFromResponse(result);
31
24
  if (!data && !tools) {
32
25
  this.logger.error({ result }, "[OpenAI] Response is not valid");
33
26
  throw new Error("Response is not valid: no data");
@@ -35,7 +28,7 @@ export class BaseOpenAIDriver extends AbstractDriver {
35
28
  return {
36
29
  result: textToCompletionResult(data || ''),
37
30
  token_usage: tokenInfo,
38
- finish_reason: openAiFinishReason(choice.finish_reason),
31
+ finish_reason: responseFinishReason(result, tools),
39
32
  tool_use: tools,
40
33
  };
41
34
  }
@@ -43,26 +36,10 @@ export class BaseOpenAIDriver extends AbstractDriver {
43
36
  if (options.model_options?._option_id !== "openai-text" && options.model_options?._option_id !== "openai-thinking") {
44
37
  this.logger.warn({ options: options.model_options }, "Invalid model options");
45
38
  }
39
+ // Include conversation history (same as non-streaming)
40
+ const conversation = updateConversation(options.conversation, prompt);
46
41
  const toolDefs = getToolDefinitions(options.tools);
47
- const useTools = toolDefs ? supportsToolUse(options.model, "openai", true) : false;
48
- const mapFn = (chunk) => {
49
- let result = undefined;
50
- if (useTools && this.provider !== "xai" && options.result_schema) {
51
- result = chunk.choices[0]?.delta?.tool_calls?.[0].function?.arguments ?? "";
52
- }
53
- else {
54
- result = chunk.choices[0]?.delta.content ?? "";
55
- }
56
- return {
57
- result: textToCompletionResult(result),
58
- finish_reason: openAiFinishReason(chunk.choices[0]?.finish_reason ?? undefined), //Uses expected "stop" , "length" format
59
- token_usage: {
60
- prompt: chunk.usage?.prompt_tokens,
61
- result: chunk.usage?.completion_tokens,
62
- total: (chunk.usage?.prompt_tokens ?? 0) + (chunk.usage?.completion_tokens ?? 0),
63
- }
64
- };
65
- };
42
+ const useTools = toolDefs ? supportsToolUse(options.model, this.provider, true) : false;
66
43
  convertRoles(prompt, options.model);
67
44
  const model_options = options.model_options;
68
45
  insert_image_detail(prompt, model_options?.image_detail ?? "auto");
@@ -78,30 +55,26 @@ export class BaseOpenAIDriver extends AbstractDriver {
78
55
  strictMode = false;
79
56
  }
80
57
  }
81
- const stream = await this.service.chat.completions.create({
58
+ const reasoning = model_options?.reasoning_effort ? { effort: model_options.reasoning_effort } : undefined;
59
+ const stream = await this.service.responses.create({
82
60
  stream: true,
83
- stream_options: { include_usage: true },
84
61
  model: options.model,
85
- messages: prompt,
86
- reasoning_effort: model_options?.reasoning_effort,
62
+ input: conversation,
63
+ reasoning,
87
64
  temperature: model_options?.temperature,
88
65
  top_p: model_options?.top_p,
89
- presence_penalty: model_options?.presence_penalty,
90
- frequency_penalty: model_options?.frequency_penalty,
91
- n: 1,
92
- max_completion_tokens: model_options?.max_tokens, //TODO: use max_tokens for older models, currently relying on OpenAI to handle it
66
+ max_output_tokens: model_options?.max_tokens,
93
67
  tools: useTools ? toolDefs : undefined,
94
- stop: model_options?.stop_sequence,
95
- response_format: parsedSchema ? {
96
- type: "json_schema",
97
- json_schema: {
68
+ text: parsedSchema ? {
69
+ format: {
70
+ type: "json_schema",
98
71
  name: "format_output",
99
72
  schema: parsedSchema,
100
73
  strict: strictMode,
101
74
  }
102
75
  } : undefined,
103
76
  });
104
- return asyncMap(stream, mapFn);
77
+ return mapResponseStream(stream);
105
78
  }
106
79
  async requestTextCompletion(prompt, options) {
107
80
  if (options.model_options?._option_id !== "openai-text" && options.model_options?._option_id !== "openai-thinking") {
@@ -111,7 +84,7 @@ export class BaseOpenAIDriver extends AbstractDriver {
111
84
  const model_options = options.model_options;
112
85
  insert_image_detail(prompt, model_options?.image_detail ?? "auto");
113
86
  const toolDefs = getToolDefinitions(options.tools);
114
- const useTools = toolDefs ? supportsToolUse(options.model, "openai") : false;
87
+ const useTools = toolDefs ? supportsToolUse(options.model, this.provider) : false;
115
88
  let conversation = updateConversation(options.conversation, prompt);
116
89
  let parsedSchema = undefined;
117
90
  let strictMode = false;
@@ -125,22 +98,19 @@ export class BaseOpenAIDriver extends AbstractDriver {
125
98
  strictMode = false;
126
99
  }
127
100
  }
128
- const res = await this.service.chat.completions.create({
101
+ const reasoning = model_options?.reasoning_effort ? { effort: model_options.reasoning_effort } : undefined;
102
+ const res = await this.service.responses.create({
129
103
  stream: false,
130
104
  model: options.model,
131
- messages: conversation,
132
- reasoning_effort: model_options?.reasoning_effort,
105
+ input: conversation,
106
+ reasoning,
133
107
  temperature: model_options?.temperature,
134
108
  top_p: model_options?.top_p,
135
- presence_penalty: model_options?.presence_penalty,
136
- frequency_penalty: model_options?.frequency_penalty,
137
- n: 1,
138
- max_completion_tokens: model_options?.max_tokens, //TODO: use max_tokens for older models, currently relying on OpenAI to handle it
109
+ max_output_tokens: model_options?.max_tokens, //TODO: use max_tokens for older models, currently relying on OpenAI to handle it
139
110
  tools: useTools ? toolDefs : undefined,
140
- stop: model_options?.stop_sequence,
141
- response_format: parsedSchema ? {
142
- type: "json_schema",
143
- json_schema: {
111
+ text: parsedSchema ? {
112
+ format: {
113
+ type: "json_schema",
144
114
  name: "format_output",
145
115
  schema: parsedSchema,
146
116
  strict: strictMode,
@@ -151,8 +121,20 @@ export class BaseOpenAIDriver extends AbstractDriver {
151
121
  if (options.include_original_response) {
152
122
  completion.original_response = res;
153
123
  }
154
- conversation = updateConversation(conversation, createPromptFromResponse(res.choices[0].message));
155
- completion.conversation = conversation;
124
+ conversation = updateConversation(conversation, createAssistantMessageFromCompletion(completion));
125
+ // Increment turn counter for deferred stripping
126
+ conversation = incrementConversationTurn(conversation);
127
+ // Strip large base64 image data based on options.stripImagesAfterTurns
128
+ const currentTurn = getConversationMeta(conversation).turnNumber;
129
+ const stripOptions = {
130
+ keepForTurns: options.stripImagesAfterTurns ?? Infinity,
131
+ currentTurn,
132
+ textMaxTokens: options.stripTextMaxTokens
133
+ };
134
+ let processedConversation = stripBase64ImagesFromConversation(conversation, stripOptions);
135
+ // Truncate large text content if configured
136
+ processedConversation = truncateLargeTextInConversation(processedConversation, stripOptions);
137
+ completion.conversation = processedConversation;
156
138
  return completion;
157
139
  }
158
140
  canStream(_options) {
@@ -164,6 +146,47 @@ export class BaseOpenAIDriver extends AbstractDriver {
164
146
  }
165
147
  return Promise.resolve(true);
166
148
  }
149
+ /**
150
+ * Build conversation context after streaming completion.
151
+ * Reconstructs the assistant message from accumulated results and applies stripping.
152
+ */
153
+ buildStreamingConversation(prompt, result, toolUse, options) {
154
+ // Build assistant message from accumulated CompletionResult[]
155
+ const completionResults = result;
156
+ const textContent = completionResultsToText(completionResults);
157
+ // Start with the conversation from options or the prompt
158
+ let conversation = updateConversation(options.conversation, prompt);
159
+ // Add assistant message as EasyInputMessage
160
+ if (textContent) {
161
+ const assistantMessage = {
162
+ role: 'assistant',
163
+ content: textContent,
164
+ };
165
+ conversation = updateConversation(conversation, [assistantMessage]);
166
+ }
167
+ // Add function calls as separate items (Response API format)
168
+ if (toolUse && toolUse.length > 0) {
169
+ const functionCalls = toolUse.map(t => ({
170
+ type: 'function_call',
171
+ call_id: t.id,
172
+ name: t.tool_name,
173
+ arguments: typeof t.tool_input === 'string' ? t.tool_input : JSON.stringify(t.tool_input ?? {}),
174
+ }));
175
+ conversation = updateConversation(conversation, functionCalls);
176
+ }
177
+ // Increment turn counter
178
+ conversation = incrementConversationTurn(conversation);
179
+ // Apply stripping based on options
180
+ const currentTurn = getConversationMeta(conversation).turnNumber;
181
+ const stripOptions = {
182
+ keepForTurns: options.stripImagesAfterTurns ?? Infinity,
183
+ currentTurn,
184
+ textMaxTokens: options.stripTextMaxTokens
185
+ };
186
+ let processedConversation = stripBase64ImagesFromConversation(conversation, stripOptions);
187
+ processedConversation = truncateLargeTextInConversation(processedConversation, stripOptions);
188
+ return processedConversation;
189
+ }
167
190
  createTrainingPrompt(options) {
168
191
  if (options.model.includes("gpt")) {
169
192
  return super.createTrainingPrompt(options);
@@ -286,44 +309,169 @@ function jobInfo(job) {
286
309
  details
287
310
  };
288
311
  }
289
- function insert_image_detail(messages, detail_level) {
290
- if (detail_level == "auto" || detail_level == "low" || detail_level == "high") {
291
- for (const message of messages) {
292
- if (message.role !== 'assistant' && message.content) {
293
- for (const part of message.content) {
294
- if (typeof part === "string") {
295
- continue;
312
+ function mapUsage(usage) {
313
+ if (!usage) {
314
+ return undefined;
315
+ }
316
+ return {
317
+ prompt: usage.input_tokens,
318
+ result: usage.output_tokens,
319
+ total: usage.total_tokens,
320
+ };
321
+ }
322
+ function completionResultsToText(completionResults) {
323
+ if (!completionResults) {
324
+ return '';
325
+ }
326
+ return completionResults
327
+ .map(r => {
328
+ switch (r.type) {
329
+ case 'text':
330
+ return r.value;
331
+ case 'json':
332
+ return typeof r.value === 'string' ? r.value : JSON.stringify(r.value);
333
+ case 'image':
334
+ // Skip images in conversation - they're in the result
335
+ return '';
336
+ default:
337
+ return String(r.value || '');
338
+ }
339
+ })
340
+ .join('');
341
+ }
342
+ function createAssistantMessageFromCompletion(completion) {
343
+ const textContent = completionResultsToText(completion.result);
344
+ const result = [];
345
+ // Add assistant text message if present
346
+ if (textContent) {
347
+ const assistantMessage = {
348
+ role: 'assistant',
349
+ content: textContent,
350
+ };
351
+ result.push(assistantMessage);
352
+ }
353
+ // Add function calls as separate items (Response API format)
354
+ if (completion.tool_use && completion.tool_use.length > 0) {
355
+ for (const t of completion.tool_use) {
356
+ const functionCall = {
357
+ type: 'function_call',
358
+ call_id: t.id,
359
+ name: t.tool_name,
360
+ arguments: typeof t.tool_input === 'string'
361
+ ? t.tool_input
362
+ : JSON.stringify(t.tool_input ?? {}),
363
+ };
364
+ result.push(functionCall);
365
+ }
366
+ }
367
+ return result;
368
+ }
369
+ function mapResponseStream(stream) {
370
+ const toolCallMetadata = new Map();
371
+ return {
372
+ async *[Symbol.asyncIterator]() {
373
+ for await (const event of stream) {
374
+ if (event.type === 'response.output_item.added' && event.item.type === 'function_call') {
375
+ const syntheticId = `tool_${event.output_index}`;
376
+ const actualId = event.item.id ?? event.item.call_id;
377
+ if (actualId) {
378
+ toolCallMetadata.set(actualId, { syntheticId, name: event.item.name });
296
379
  }
297
- if (part.type === 'image_url') {
298
- part.image_url = { ...part.image_url, detail: detail_level };
380
+ const toolUse = {
381
+ id: syntheticId,
382
+ _actual_id: actualId,
383
+ tool_name: event.item.name,
384
+ tool_input: '',
385
+ };
386
+ yield {
387
+ result: [],
388
+ tool_use: [toolUse],
389
+ };
390
+ }
391
+ else if (event.type === 'response.function_call_arguments.delta') {
392
+ const metadata = toolCallMetadata.get(event.item_id);
393
+ const syntheticId = metadata?.syntheticId ?? `tool_${event.output_index}`;
394
+ const toolUse = {
395
+ id: syntheticId,
396
+ _actual_id: event.item_id,
397
+ tool_name: metadata?.name ?? '',
398
+ tool_input: event.delta,
399
+ };
400
+ yield {
401
+ result: [],
402
+ tool_use: [toolUse],
403
+ };
404
+ }
405
+ // Note: We don't emit response.function_call_arguments.done because the arguments were already
406
+ // streamed via delta events. Emitting it again would duplicate the tool_input content.
407
+ // We only update the metadata to ensure the tool name is captured.
408
+ else if (event.type === 'response.function_call_arguments.done') {
409
+ // Just update metadata, don't yield (arguments already accumulated from delta events)
410
+ const metadata = toolCallMetadata.get(event.item_id);
411
+ const syntheticId = metadata?.syntheticId ?? `tool_${event.output_index}`;
412
+ const tool_name = metadata?.name ?? event.name ?? '';
413
+ if (event.item_id) {
414
+ toolCallMetadata.set(event.item_id, { syntheticId, name: tool_name });
415
+ }
416
+ }
417
+ else if (event.type === 'response.output_text.delta') {
418
+ yield {
419
+ result: textToCompletionResult(event.delta),
420
+ };
421
+ }
422
+ // Note: We don't emit response.output_text.done because the text was already
423
+ // streamed via delta events. Emitting it again would duplicate the content.
424
+ else if (event.type === 'response.completed' || event.type === 'response.incomplete' || event.type === 'response.failed') {
425
+ const finalTools = collectTools(event.response.output);
426
+ yield {
427
+ result: [],
428
+ finish_reason: responseFinishReason(event.response, finalTools),
429
+ token_usage: mapUsage(event.response.usage),
430
+ };
431
+ }
432
+ }
433
+ }
434
+ };
435
+ }
436
+ function insert_image_detail(items, detail_level) {
437
+ if (detail_level === "auto" || detail_level === "low" || detail_level === "high") {
438
+ for (const item of items) {
439
+ // Check if it's an EasyInputMessage or Message with content array
440
+ if ('role' in item && 'content' in item && item.role !== 'assistant') {
441
+ const content = item.content;
442
+ if (Array.isArray(content)) {
443
+ for (const part of content) {
444
+ if (typeof part === 'object' && part.type === 'input_image') {
445
+ part.detail = detail_level;
446
+ }
299
447
  }
300
448
  }
301
449
  }
302
450
  }
303
451
  }
304
- return messages;
452
+ return items;
305
453
  }
306
- function convertRoles(messages, model) {
454
+ function convertRoles(items, model) {
307
455
  //New openai models use developer role instead of system
308
456
  if (model.includes("o1") || model.includes("o3")) {
309
457
  if (model.includes("o1-mini") || model.includes("o1-preview")) {
310
458
  //o1-mini and o1-preview support neither system nor developer
311
- for (const message of messages) {
312
- if (message.role === 'system') {
313
- message.role = 'user';
459
+ for (const item of items) {
460
+ if ('role' in item && item.role === 'system') {
461
+ item.role = 'user';
314
462
  }
315
463
  }
316
464
  }
317
465
  else {
318
466
  //Models newer than o1 use developer role
319
- for (const message of messages) {
320
- if (message.role === 'system') {
321
- message.role = 'developer';
467
+ for (const item of items) {
468
+ if ('role' in item && item.role === 'system') {
469
+ item.role = 'developer';
322
470
  }
323
471
  }
324
472
  }
325
473
  }
326
- return messages;
474
+ return items;
327
475
  }
328
476
  //Structured output support is typically aligned with tool use support
329
477
  //Not true for realtime models, which do not support structured output, but do support tool use.
@@ -352,62 +500,63 @@ function getToolDefinition(toolDef) {
352
500
  }
353
501
  return {
354
502
  type: "function",
355
- function: {
356
- name: toolDef.name,
357
- description: toolDef.description,
358
- parameters: parsedSchema,
359
- strict: strictMode,
360
- },
503
+ name: toolDef.name,
504
+ description: toolDef.description,
505
+ parameters: parsedSchema ?? null,
506
+ strict: strictMode,
361
507
  };
362
508
  }
363
- function openAiFinishReason(finish_reason) {
364
- if (finish_reason === "tool_calls") {
365
- return "tool_use";
366
- }
367
- return finish_reason;
368
- }
369
- function updateConversation(conversation, message) {
370
- if (!message) {
371
- return conversation;
509
+ function updateConversation(conversation, items) {
510
+ if (!items) {
511
+ // Unwrap array if wrapped, otherwise treat as array
512
+ const unwrapped = unwrapConversationArray(conversation);
513
+ return unwrapped ?? (conversation || []);
372
514
  }
373
515
  if (!conversation) {
374
- return message;
516
+ return items;
375
517
  }
376
- return [...conversation, ...message];
518
+ // Unwrap array if wrapped, otherwise treat as array
519
+ const unwrapped = unwrapConversationArray(conversation);
520
+ const convArray = unwrapped ?? conversation;
521
+ return [...convArray, ...items];
377
522
  }
378
- export function collectTools(toolCalls) {
379
- if (!toolCalls) {
523
+ export function collectTools(output) {
524
+ if (!output) {
380
525
  return undefined;
381
526
  }
382
527
  const tools = [];
383
- for (const call of toolCalls) {
384
- tools.push({
385
- id: call.id,
386
- tool_name: call.function.name,
387
- tool_input: JSON.parse(call.function.arguments),
388
- });
528
+ for (const item of output) {
529
+ if (item.type === 'function_call') {
530
+ const id = item.call_id || item.id;
531
+ if (!id) {
532
+ continue;
533
+ }
534
+ tools.push({
535
+ id,
536
+ tool_name: item.name ?? '',
537
+ tool_input: safeJsonParse(item.arguments),
538
+ });
539
+ }
389
540
  }
390
541
  return tools.length > 0 ? tools : undefined;
391
542
  }
392
- function createPromptFromResponse(response) {
393
- const messages = [];
394
- if (response) {
395
- messages.push({
396
- role: response.role,
397
- content: [{
398
- type: "text",
399
- text: response.content ?? ""
400
- }],
401
- tool_calls: response.tool_calls,
402
- });
403
- }
404
- return messages;
405
- }
406
543
  //For strict mode false
407
544
  function limitedSchemaFormat(schema) {
408
545
  const formattedSchema = { ...schema };
409
546
  // Defaults not supported
410
547
  delete formattedSchema.default;
548
+ // OpenAI requires type field even in non-strict mode
549
+ // If no type is specified, default to 'object' for properties with format/editor hints,
550
+ // otherwise 'string' as a safe fallback
551
+ if (!formattedSchema.type && formattedSchema.description) {
552
+ // Properties with format: "document" or editor hints are typically objects
553
+ if (formattedSchema.format === 'document' || formattedSchema.editor) {
554
+ formattedSchema.type = 'object';
555
+ }
556
+ else {
557
+ formattedSchema.type = 'string';
558
+ }
559
+ }
411
560
  if (formattedSchema?.properties) {
412
561
  // Process each property recursively
413
562
  for (const propName of Object.keys(formattedSchema.properties)) {
@@ -443,6 +592,10 @@ function openAISchemaFormat(schema, nesting = 0) {
443
592
  // Process each property recursively
444
593
  for (const propName of Object.keys(formattedSchema.properties)) {
445
594
  const property = formattedSchema.properties[propName];
595
+ // OpenAI strict mode requires all properties to have a type
596
+ if (!property?.type) {
597
+ throw new Error(`Property '${propName}' is missing required 'type' field for OpenAI strict mode`);
598
+ }
446
599
  // Recursively process properties
447
600
  formattedSchema.properties[propName] = openAISchemaFormat(property, nesting + 1);
448
601
  // Process arrays with items of type object
@@ -461,4 +614,47 @@ function openAISchemaFormat(schema, nesting = 0) {
461
614
  }
462
615
  return formattedSchema;
463
616
  }
617
+ function extractTextFromResponse(response) {
618
+ if (response.output_text) {
619
+ return response.output_text;
620
+ }
621
+ const collected = [];
622
+ for (const item of response.output ?? []) {
623
+ if (item.type === 'message') {
624
+ const text = item.content
625
+ .map(part => part.type === 'output_text' ? part.text : '')
626
+ .join('');
627
+ if (text) {
628
+ collected.push(text);
629
+ }
630
+ }
631
+ }
632
+ return collected.join("\n");
633
+ }
634
+ function responseFinishReason(response, tools) {
635
+ if (tools && tools.length > 0) {
636
+ return "tool_use";
637
+ }
638
+ if (response.status === 'incomplete') {
639
+ if (response.incomplete_details?.reason === 'max_output_tokens') {
640
+ return 'length';
641
+ }
642
+ return response.incomplete_details?.reason ?? 'incomplete';
643
+ }
644
+ if (response.status && response.status !== 'completed') {
645
+ return response.status;
646
+ }
647
+ return 'stop';
648
+ }
649
+ function safeJsonParse(value) {
650
+ if (typeof value !== 'string') {
651
+ return value;
652
+ }
653
+ try {
654
+ return JSON.parse(value);
655
+ }
656
+ catch {
657
+ return value;
658
+ }
659
+ }
464
660
  //# sourceMappingURL=index.js.map