@llumiverse/drivers 1.0.0-dev.20260202.145450Z → 1.0.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 (240) hide show
  1. package/lib/cjs/adobe/firefly.js +120 -0
  2. package/lib/cjs/adobe/firefly.js.map +1 -0
  3. package/lib/cjs/azure/azure_foundry.js +432 -0
  4. package/lib/cjs/azure/azure_foundry.js.map +1 -0
  5. package/lib/cjs/bedrock/converse.js +359 -0
  6. package/lib/cjs/bedrock/converse.js.map +1 -0
  7. package/lib/cjs/bedrock/index.js +1441 -0
  8. package/lib/cjs/bedrock/index.js.map +1 -0
  9. package/lib/cjs/bedrock/nova-image-payload.js +207 -0
  10. package/lib/cjs/bedrock/nova-image-payload.js.map +1 -0
  11. package/lib/cjs/bedrock/payloads.js +3 -0
  12. package/lib/cjs/bedrock/payloads.js.map +1 -0
  13. package/lib/cjs/bedrock/s3.js +107 -0
  14. package/lib/cjs/bedrock/s3.js.map +1 -0
  15. package/lib/cjs/bedrock/twelvelabs.js +87 -0
  16. package/lib/cjs/bedrock/twelvelabs.js.map +1 -0
  17. package/lib/cjs/groq/index.js +326 -0
  18. package/lib/cjs/groq/index.js.map +1 -0
  19. package/lib/cjs/huggingface_ie.js +201 -0
  20. package/lib/cjs/huggingface_ie.js.map +1 -0
  21. package/lib/cjs/index.js +31 -0
  22. package/lib/cjs/index.js.map +1 -0
  23. package/lib/cjs/mistral/index.js +176 -0
  24. package/lib/cjs/mistral/index.js.map +1 -0
  25. package/lib/cjs/mistral/types.js +83 -0
  26. package/lib/cjs/mistral/types.js.map +1 -0
  27. package/lib/cjs/openai/azure_openai.js +72 -0
  28. package/lib/cjs/openai/azure_openai.js.map +1 -0
  29. package/lib/cjs/openai/index.js +1100 -0
  30. package/lib/cjs/openai/index.js.map +1 -0
  31. package/lib/cjs/openai/openai.js +21 -0
  32. package/lib/cjs/openai/openai.js.map +1 -0
  33. package/lib/cjs/openai/openai_compatible.js +63 -0
  34. package/lib/cjs/openai/openai_compatible.js.map +1 -0
  35. package/lib/cjs/openai/openai_format.js +131 -0
  36. package/lib/cjs/openai/openai_format.js.map +1 -0
  37. package/lib/cjs/package.json +3 -0
  38. package/lib/cjs/replicate.js +275 -0
  39. package/lib/cjs/replicate.js.map +1 -0
  40. package/lib/cjs/test-driver/TestErrorCompletionStream.js +20 -0
  41. package/lib/cjs/test-driver/TestErrorCompletionStream.js.map +1 -0
  42. package/lib/cjs/test-driver/TestValidationErrorCompletionStream.js +24 -0
  43. package/lib/cjs/test-driver/TestValidationErrorCompletionStream.js.map +1 -0
  44. package/lib/cjs/test-driver/index.js +109 -0
  45. package/lib/cjs/test-driver/index.js.map +1 -0
  46. package/lib/cjs/test-driver/utils.js +30 -0
  47. package/lib/cjs/test-driver/utils.js.map +1 -0
  48. package/lib/cjs/togetherai/index.js +126 -0
  49. package/lib/cjs/togetherai/index.js.map +1 -0
  50. package/lib/cjs/togetherai/interfaces.js +3 -0
  51. package/lib/cjs/togetherai/interfaces.js.map +1 -0
  52. package/lib/cjs/vertexai/debug.js +12 -0
  53. package/lib/cjs/vertexai/debug.js.map +1 -0
  54. package/lib/cjs/vertexai/embeddings/embeddings-image.js +27 -0
  55. package/lib/cjs/vertexai/embeddings/embeddings-image.js.map +1 -0
  56. package/lib/cjs/vertexai/embeddings/embeddings-text.js +23 -0
  57. package/lib/cjs/vertexai/embeddings/embeddings-text.js.map +1 -0
  58. package/lib/cjs/vertexai/index.js +635 -0
  59. package/lib/cjs/vertexai/index.js.map +1 -0
  60. package/lib/cjs/vertexai/models/claude.js +842 -0
  61. package/lib/cjs/vertexai/models/claude.js.map +1 -0
  62. package/lib/cjs/vertexai/models/gemini.js +1110 -0
  63. package/lib/cjs/vertexai/models/gemini.js.map +1 -0
  64. package/lib/cjs/vertexai/models/imagen.js +303 -0
  65. package/lib/cjs/vertexai/models/imagen.js.map +1 -0
  66. package/lib/cjs/vertexai/models/llama.js +183 -0
  67. package/lib/cjs/vertexai/models/llama.js.map +1 -0
  68. package/lib/cjs/vertexai/models.js +35 -0
  69. package/lib/cjs/vertexai/models.js.map +1 -0
  70. package/lib/cjs/watsonx/index.js +161 -0
  71. package/lib/cjs/watsonx/index.js.map +1 -0
  72. package/lib/cjs/watsonx/interfaces.js +3 -0
  73. package/lib/cjs/watsonx/interfaces.js.map +1 -0
  74. package/lib/cjs/xai/index.js +65 -0
  75. package/lib/cjs/xai/index.js.map +1 -0
  76. package/lib/esm/adobe/firefly.js +116 -0
  77. package/lib/esm/adobe/firefly.js.map +1 -0
  78. package/lib/esm/azure/azure_foundry.js +426 -0
  79. package/lib/esm/azure/azure_foundry.js.map +1 -0
  80. package/lib/esm/bedrock/converse.js +352 -0
  81. package/lib/esm/bedrock/converse.js.map +1 -0
  82. package/lib/esm/bedrock/index.js +1434 -0
  83. package/lib/esm/bedrock/index.js.map +1 -0
  84. package/lib/esm/bedrock/nova-image-payload.js +203 -0
  85. package/lib/esm/bedrock/nova-image-payload.js.map +1 -0
  86. package/lib/esm/bedrock/payloads.js +2 -0
  87. package/lib/esm/bedrock/payloads.js.map +1 -0
  88. package/lib/esm/bedrock/s3.js +99 -0
  89. package/lib/esm/bedrock/s3.js.map +1 -0
  90. package/lib/esm/bedrock/twelvelabs.js +84 -0
  91. package/lib/esm/bedrock/twelvelabs.js.map +1 -0
  92. package/lib/esm/groq/index.js +319 -0
  93. package/lib/esm/groq/index.js.map +1 -0
  94. package/lib/esm/huggingface_ie.js +197 -0
  95. package/lib/esm/huggingface_ie.js.map +1 -0
  96. package/lib/esm/index.js +15 -0
  97. package/lib/esm/index.js.map +1 -0
  98. package/lib/esm/mistral/index.js +172 -0
  99. package/lib/esm/mistral/index.js.map +1 -0
  100. package/lib/esm/mistral/types.js +80 -0
  101. package/lib/esm/mistral/types.js.map +1 -0
  102. package/lib/esm/openai/azure_openai.js +68 -0
  103. package/lib/esm/openai/azure_openai.js.map +1 -0
  104. package/lib/esm/openai/index.js +1093 -0
  105. package/lib/esm/openai/index.js.map +1 -0
  106. package/lib/esm/openai/openai.js +14 -0
  107. package/lib/esm/openai/openai.js.map +1 -0
  108. package/lib/esm/openai/openai_compatible.js +56 -0
  109. package/lib/esm/openai/openai_compatible.js.map +1 -0
  110. package/lib/esm/openai/openai_format.js +127 -0
  111. package/lib/esm/openai/openai_format.js.map +1 -0
  112. package/lib/esm/replicate.js +268 -0
  113. package/lib/esm/replicate.js.map +1 -0
  114. package/lib/esm/test-driver/TestErrorCompletionStream.js +16 -0
  115. package/lib/esm/test-driver/TestErrorCompletionStream.js.map +1 -0
  116. package/lib/esm/test-driver/TestValidationErrorCompletionStream.js +20 -0
  117. package/lib/esm/test-driver/TestValidationErrorCompletionStream.js.map +1 -0
  118. package/lib/esm/test-driver/index.js +91 -0
  119. package/lib/esm/test-driver/index.js.map +1 -0
  120. package/lib/esm/test-driver/utils.js +25 -0
  121. package/lib/esm/test-driver/utils.js.map +1 -0
  122. package/lib/esm/togetherai/index.js +122 -0
  123. package/lib/esm/togetherai/index.js.map +1 -0
  124. package/lib/esm/togetherai/interfaces.js +2 -0
  125. package/lib/esm/togetherai/interfaces.js.map +1 -0
  126. package/lib/esm/vertexai/debug.js +6 -0
  127. package/lib/esm/vertexai/debug.js.map +1 -0
  128. package/lib/esm/vertexai/embeddings/embeddings-image.js +24 -0
  129. package/lib/esm/vertexai/embeddings/embeddings-image.js.map +1 -0
  130. package/lib/esm/vertexai/embeddings/embeddings-text.js +20 -0
  131. package/lib/esm/vertexai/embeddings/embeddings-text.js.map +1 -0
  132. package/lib/esm/vertexai/index.js +630 -0
  133. package/lib/esm/vertexai/index.js.map +1 -0
  134. package/lib/esm/vertexai/models/claude.js +833 -0
  135. package/lib/esm/vertexai/models/claude.js.map +1 -0
  136. package/lib/esm/vertexai/models/gemini.js +1104 -0
  137. package/lib/esm/vertexai/models/gemini.js.map +1 -0
  138. package/lib/esm/vertexai/models/imagen.js +299 -0
  139. package/lib/esm/vertexai/models/imagen.js.map +1 -0
  140. package/lib/esm/vertexai/models/llama.js +179 -0
  141. package/lib/esm/vertexai/models/llama.js.map +1 -0
  142. package/lib/esm/vertexai/models.js +32 -0
  143. package/lib/esm/vertexai/models.js.map +1 -0
  144. package/lib/esm/watsonx/index.js +157 -0
  145. package/lib/esm/watsonx/index.js.map +1 -0
  146. package/lib/esm/watsonx/interfaces.js +2 -0
  147. package/lib/esm/watsonx/interfaces.js.map +1 -0
  148. package/lib/esm/xai/index.js +58 -0
  149. package/lib/esm/xai/index.js.map +1 -0
  150. package/lib/types/adobe/firefly.d.ts +30 -0
  151. package/lib/types/adobe/firefly.d.ts.map +1 -0
  152. package/lib/types/azure/azure_foundry.d.ts +52 -0
  153. package/lib/types/azure/azure_foundry.d.ts.map +1 -0
  154. package/lib/types/bedrock/converse.d.ts +8 -0
  155. package/lib/types/bedrock/converse.d.ts.map +1 -0
  156. package/lib/types/bedrock/index.d.ts +135 -0
  157. package/lib/types/bedrock/index.d.ts.map +1 -0
  158. package/lib/types/bedrock/nova-image-payload.d.ts +74 -0
  159. package/lib/types/bedrock/nova-image-payload.d.ts.map +1 -0
  160. package/lib/types/bedrock/payloads.d.ts +12 -0
  161. package/lib/types/bedrock/payloads.d.ts.map +1 -0
  162. package/lib/types/bedrock/s3.d.ts +23 -0
  163. package/lib/types/bedrock/s3.d.ts.map +1 -0
  164. package/lib/types/bedrock/twelvelabs.d.ts +50 -0
  165. package/lib/types/bedrock/twelvelabs.d.ts.map +1 -0
  166. package/lib/types/groq/index.d.ts +27 -0
  167. package/lib/types/groq/index.d.ts.map +1 -0
  168. package/lib/types/huggingface_ie.d.ts +35 -0
  169. package/lib/types/huggingface_ie.d.ts.map +1 -0
  170. package/lib/types/index.d.ts +15 -0
  171. package/lib/types/index.d.ts.map +1 -0
  172. package/lib/types/mistral/index.d.ts +25 -0
  173. package/lib/types/mistral/index.d.ts.map +1 -0
  174. package/lib/types/mistral/types.d.ts +127 -0
  175. package/lib/types/mistral/types.d.ts.map +1 -0
  176. package/lib/types/openai/azure_openai.d.ts +25 -0
  177. package/lib/types/openai/azure_openai.d.ts.map +1 -0
  178. package/lib/types/openai/index.d.ts +126 -0
  179. package/lib/types/openai/index.d.ts.map +1 -0
  180. package/lib/types/openai/openai.d.ts +15 -0
  181. package/lib/types/openai/openai.d.ts.map +1 -0
  182. package/lib/types/openai/openai_compatible.d.ts +31 -0
  183. package/lib/types/openai/openai_compatible.d.ts.map +1 -0
  184. package/lib/types/openai/openai_format.d.ts +21 -0
  185. package/lib/types/openai/openai_format.d.ts.map +1 -0
  186. package/lib/types/replicate.d.ts +48 -0
  187. package/lib/types/replicate.d.ts.map +1 -0
  188. package/lib/types/test-driver/TestErrorCompletionStream.d.ts +9 -0
  189. package/lib/types/test-driver/TestErrorCompletionStream.d.ts.map +1 -0
  190. package/lib/types/test-driver/TestValidationErrorCompletionStream.d.ts +9 -0
  191. package/lib/types/test-driver/TestValidationErrorCompletionStream.d.ts.map +1 -0
  192. package/lib/types/test-driver/index.d.ts +24 -0
  193. package/lib/types/test-driver/index.d.ts.map +1 -0
  194. package/lib/types/test-driver/utils.d.ts +5 -0
  195. package/lib/types/test-driver/utils.d.ts.map +1 -0
  196. package/lib/types/togetherai/index.d.ts +23 -0
  197. package/lib/types/togetherai/index.d.ts.map +1 -0
  198. package/lib/types/togetherai/interfaces.d.ts +96 -0
  199. package/lib/types/togetherai/interfaces.d.ts.map +1 -0
  200. package/lib/types/vertexai/debug.d.ts +2 -0
  201. package/lib/types/vertexai/debug.d.ts.map +1 -0
  202. package/lib/types/vertexai/embeddings/embeddings-image.d.ts +11 -0
  203. package/lib/types/vertexai/embeddings/embeddings-image.d.ts.map +1 -0
  204. package/lib/types/vertexai/embeddings/embeddings-text.d.ts +10 -0
  205. package/lib/types/vertexai/embeddings/embeddings-text.d.ts.map +1 -0
  206. package/lib/types/vertexai/index.d.ts +79 -0
  207. package/lib/types/vertexai/index.d.ts.map +1 -0
  208. package/lib/types/vertexai/models/claude.d.ts +103 -0
  209. package/lib/types/vertexai/models/claude.d.ts.map +1 -0
  210. package/lib/types/vertexai/models/gemini.d.ts +78 -0
  211. package/lib/types/vertexai/models/gemini.d.ts.map +1 -0
  212. package/lib/types/vertexai/models/imagen.d.ts +75 -0
  213. package/lib/types/vertexai/models/imagen.d.ts.map +1 -0
  214. package/lib/types/vertexai/models/llama.d.ts +20 -0
  215. package/lib/types/vertexai/models/llama.d.ts.map +1 -0
  216. package/lib/types/vertexai/models.d.ts +20 -0
  217. package/lib/types/vertexai/models.d.ts.map +1 -0
  218. package/lib/types/watsonx/index.d.ts +27 -0
  219. package/lib/types/watsonx/index.d.ts.map +1 -0
  220. package/lib/types/watsonx/interfaces.d.ts +65 -0
  221. package/lib/types/watsonx/interfaces.d.ts.map +1 -0
  222. package/lib/types/xai/index.d.ts +18 -0
  223. package/lib/types/xai/index.d.ts.map +1 -0
  224. package/package.json +18 -18
  225. package/src/bedrock/converse.ts +85 -10
  226. package/src/bedrock/error-handling.test.ts +352 -0
  227. package/src/bedrock/index.ts +293 -16
  228. package/src/groq/index.ts +9 -4
  229. package/src/mistral/index.ts +25 -22
  230. package/src/mistral/types.ts +0 -5
  231. package/src/openai/error-handling.test.ts +567 -0
  232. package/src/openai/index.ts +513 -33
  233. package/src/openai/openai_compatible.ts +7 -0
  234. package/src/openai/openai_format.ts +1 -1
  235. package/src/vertexai/index.ts +61 -13
  236. package/src/vertexai/models/claude-error-handling.test.ts +432 -0
  237. package/src/vertexai/models/claude.ts +287 -10
  238. package/src/vertexai/models/gemini-error-handling.test.ts +353 -0
  239. package/src/vertexai/models/gemini.ts +329 -52
  240. package/src/vertexai/models.ts +7 -2
@@ -11,7 +11,11 @@ import {
11
11
  ExecutionOptions,
12
12
  ExecutionTokenUsage,
13
13
  JSONSchema,
14
+ LlumiverseError,
15
+ LlumiverseErrorContext,
14
16
  ModelType,
17
+ OpenAiDalleOptions,
18
+ OpenAiGptImageOptions,
15
19
  Providers,
16
20
  ToolDefinition,
17
21
  ToolUse,
@@ -24,11 +28,28 @@ import {
24
28
  incrementConversationTurn,
25
29
  modelModalitiesToArray,
26
30
  stripBase64ImagesFromConversation,
31
+ stripHeartbeatsFromConversation,
27
32
  supportsToolUse,
28
33
  truncateLargeTextInConversation,
29
34
  unwrapConversationArray,
30
35
  } from "@llumiverse/core";
31
36
  import OpenAI, { AzureOpenAI } from "openai";
37
+ import {
38
+ APIConnectionError,
39
+ APIConnectionTimeoutError,
40
+ APIError,
41
+ AuthenticationError,
42
+ BadRequestError,
43
+ ConflictError,
44
+ ContentFilterFinishReasonError,
45
+ InternalServerError,
46
+ LengthFinishReasonError,
47
+ NotFoundError,
48
+ OpenAIError,
49
+ PermissionDeniedError,
50
+ RateLimitError,
51
+ UnprocessableEntityError,
52
+ } from 'openai/error';
32
53
  import { formatOpenAILikeMultimodalPrompt } from "./openai_format.js";
33
54
 
34
55
  // Response API types
@@ -71,15 +92,16 @@ export abstract class BaseOpenAIDriver extends AbstractDriver<
71
92
  const tokenInfo = mapUsage(result.usage);
72
93
 
73
94
  const tools = collectTools(result.output);
74
- const data = extractTextFromResponse(result);
95
+ // Collect all parts in order (text and images)
96
+ const allResults = extractCompletionResults(result.output);
75
97
 
76
- if (!data && !tools) {
98
+ if (allResults.length === 0 && !tools) {
77
99
  this.logger.error({ result }, "[OpenAI] Response is not valid");
78
100
  throw new Error("Response is not valid: no data");
79
101
  }
80
102
 
81
103
  return {
82
- result: textToCompletionResult(data || ''),
104
+ result: allResults,
83
105
  token_usage: tokenInfo,
84
106
  finish_reason: responseFinishReason(result, tools),
85
107
  tool_use: tools,
@@ -92,11 +114,18 @@ export abstract class BaseOpenAIDriver extends AbstractDriver<
92
114
  }
93
115
 
94
116
  // Include conversation history (same as non-streaming)
95
- const conversation = updateConversation(options.conversation, prompt);
117
+ // Fix orphaned function_call items (can occur when agent is stopped mid-tool-execution)
118
+ let conversation = fixOrphanedToolUse(updateConversation(options.conversation, prompt));
96
119
 
97
120
  const toolDefs = getToolDefinitions(options.tools);
98
121
  const useTools: boolean = toolDefs ? supportsToolUse(options.model, this.provider, true) : false;
99
122
 
123
+ // When no tools are provided but conversation contains function_call/function_call_output
124
+ // items (e.g. checkpoint summary calls), convert them to text to avoid API errors
125
+ if (!useTools) {
126
+ conversation = convertOpenAIFunctionItemsToText(conversation);
127
+ }
128
+
100
129
  convertRoles(prompt, options.model);
101
130
 
102
131
  const model_options = options.model_options as any;
@@ -116,14 +145,15 @@ export abstract class BaseOpenAIDriver extends AbstractDriver<
116
145
  }
117
146
 
118
147
  const reasoning = model_options?.reasoning_effort ? { effort: model_options.reasoning_effort } : undefined;
148
+ const isReasoningModel = /\b(o1|o3|o4)\b/.test(options.model);
119
149
 
120
150
  const stream = await this.service.responses.create({
121
151
  stream: true,
122
152
  model: options.model,
123
153
  input: conversation,
124
154
  reasoning,
125
- temperature: model_options?.temperature,
126
- top_p: model_options?.top_p,
155
+ temperature: isReasoningModel ? undefined : model_options?.temperature,
156
+ top_p: isReasoningModel ? undefined : model_options?.top_p,
127
157
  max_output_tokens: model_options?.max_tokens,
128
158
  tools: useTools ? toolDefs : undefined,
129
159
  text: parsedSchema ? {
@@ -152,7 +182,14 @@ export abstract class BaseOpenAIDriver extends AbstractDriver<
152
182
  const toolDefs = getToolDefinitions(options.tools);
153
183
  const useTools: boolean = toolDefs ? supportsToolUse(options.model, this.provider) : false;
154
184
 
155
- let conversation = updateConversation(options.conversation, prompt);
185
+ // Fix orphaned function_call items (can occur when agent is stopped mid-tool-execution)
186
+ let conversation = fixOrphanedToolUse(updateConversation(options.conversation, prompt));
187
+
188
+ // When no tools are provided but conversation contains function_call/function_call_output
189
+ // items (e.g. checkpoint summary calls), convert them to text to avoid API errors
190
+ if (!useTools) {
191
+ conversation = convertOpenAIFunctionItemsToText(conversation);
192
+ }
156
193
 
157
194
  let parsedSchema: JSONSchema | undefined = undefined;
158
195
  let strictMode = false;
@@ -168,14 +205,15 @@ export abstract class BaseOpenAIDriver extends AbstractDriver<
168
205
  }
169
206
 
170
207
  const reasoning = model_options?.reasoning_effort ? { effort: model_options.reasoning_effort } : undefined;
208
+ const isReasoningModel = /\b(o1|o3|o4)\b/.test(options.model);
171
209
 
172
210
  const res = await this.service.responses.create({
173
211
  stream: false,
174
212
  model: options.model,
175
213
  input: conversation,
176
214
  reasoning,
177
- temperature: model_options?.temperature,
178
- top_p: model_options?.top_p,
215
+ temperature: isReasoningModel ? undefined : model_options?.temperature,
216
+ top_p: isReasoningModel ? undefined : model_options?.top_p,
179
217
  max_output_tokens: model_options?.max_tokens, //TODO: use max_tokens for older models, currently relying on OpenAI to handle it
180
218
  tools: useTools ? toolDefs : undefined,
181
219
  text: parsedSchema ? {
@@ -210,12 +248,25 @@ export abstract class BaseOpenAIDriver extends AbstractDriver<
210
248
  // Truncate large text content if configured
211
249
  processedConversation = truncateLargeTextInConversation(processedConversation, stripOptions);
212
250
 
251
+ // Strip old heartbeat status messages
252
+ processedConversation = stripHeartbeatsFromConversation(processedConversation, {
253
+ keepForTurns: options.stripHeartbeatsAfterTurns ?? 1,
254
+ currentTurn,
255
+ });
256
+
213
257
  completion.conversation = processedConversation;
214
258
 
215
259
  return completion;
216
260
  }
217
261
 
218
262
  protected canStream(_options: ExecutionOptions): Promise<boolean> {
263
+ // Image generation models don't support streaming
264
+ if (_options.model.includes("dall-e")
265
+ || _options.model.includes("gpt-image")
266
+ || _options.model.includes("chatgpt-image")) {
267
+ return Promise.resolve(false);
268
+ }
269
+
219
270
  if (_options.model.includes("o1")
220
271
  && !(_options.model.includes("mini") || _options.model.includes("preview"))) {
221
272
  //o1 full does not support streaming
@@ -275,6 +326,10 @@ export abstract class BaseOpenAIDriver extends AbstractDriver<
275
326
  };
276
327
  let processedConversation = stripBase64ImagesFromConversation(conversation, stripOptions);
277
328
  processedConversation = truncateLargeTextInConversation(processedConversation, stripOptions);
329
+ processedConversation = stripHeartbeatsFromConversation(processedConversation, {
330
+ keepForTurns: options.stripHeartbeatsAfterTurns ?? 1,
331
+ currentTurn,
332
+ });
278
333
 
279
334
  return processedConversation as ResponseInputItem[];
280
335
  }
@@ -339,7 +394,7 @@ export abstract class BaseOpenAIDriver extends AbstractDriver<
339
394
  //Some of these use the completions API instead of the chat completions API.
340
395
  //Others are for non-text input modalities. Therefore common to both.
341
396
  const wordBlacklist = ["embed", "whisper", "transcribe", "audio", "moderation", "tts",
342
- "realtime", "dall-e", "babbage", "davinci", "codex", "o1-pro", "computer-use", "sora"];
397
+ "realtime", "babbage", "davinci", "codex", "o1-pro", "computer-use", "sora"];
343
398
 
344
399
 
345
400
  //OpenAI has very little information, filtering based on name.
@@ -354,14 +409,20 @@ export abstract class BaseOpenAIDriver extends AbstractDriver<
354
409
  if (owner == "system") {
355
410
  owner = "openai";
356
411
  }
412
+
413
+ // Determine model type based on capabilities
414
+ let modelType = ModelType.Text;
415
+ if (m.id.includes("dall-e") || m.id.includes("gpt-image")) {
416
+ modelType = ModelType.Image;
417
+ }
418
+
419
+
357
420
  return {
358
421
  id: m.id,
359
422
  name: m.id,
360
423
  provider: this.provider,
361
424
  owner: owner,
362
- type: m.object === "model" ? ModelType.Text : ModelType.Unknown,
363
- can_stream: true,
364
- is_multimodal: m.id.includes("gpt-4"),
425
+ type: modelType,
365
426
  input_modalities: modelModalitiesToArray(modelCapability.input),
366
427
  output_modalities: modelModalitiesToArray(modelCapability.output),
367
428
  tool_support: modelCapability.tool_support,
@@ -396,6 +457,307 @@ export abstract class BaseOpenAIDriver extends AbstractDriver<
396
457
  return { values: embeddings, model } satisfies EmbeddingsResult;
397
458
  }
398
459
 
460
+ imageModels = ["dall-e", "gpt-image", "chatgpt-image"];
461
+
462
+ /**
463
+ * Determine if a model is specifically an image generation model (not conversational image model)
464
+ */
465
+ isImageModel(model: string): boolean {
466
+ // DALL-E models are standalone image generation
467
+ // gpt-image models can generate images in conversations, not standalone
468
+ return this.imageModels.some(imageModel => model.includes(imageModel));
469
+ }
470
+
471
+ /**
472
+ * Request image generation from standalone Images API
473
+ * Supports: DALL-E 2, DALL-E 3, GPT-image models (for edit/variation)
474
+ */
475
+ async requestImageGeneration(prompt: ResponseInputItem[], options: ExecutionOptions): Promise<Completion> {
476
+ this.logger.debug(`[${this.provider}] Generating image with model ${options.model}`);
477
+
478
+ const model_options = options.model_options as OpenAiDalleOptions | OpenAiGptImageOptions | undefined;
479
+
480
+ // Extract prompt text from ResponseInputItem[]
481
+ let promptText = "";
482
+ for (const item of prompt) {
483
+ if ('content' in item && typeof item.content === 'string') {
484
+ promptText += item.content + "\\n";
485
+ } else if ('content' in item && Array.isArray(item.content)) {
486
+ // Extract text from content array
487
+ for (const part of item.content) {
488
+ if ('type' in part && part.type === 'input_text' && 'text' in part) {
489
+ promptText += part.text + "\\n";
490
+ }
491
+ }
492
+ }
493
+ }
494
+ promptText = promptText.trim();
495
+
496
+ try {
497
+ const generateParams: OpenAI.Images.ImageGenerateParamsNonStreaming = {
498
+ model: options.model,
499
+ prompt: promptText,
500
+ size: model_options?.size || "1024x1024",
501
+ };
502
+
503
+ // Add DALL-E specific options
504
+ if (options.model.includes("dall-e") || model_options?._option_id === "openai-dalle") {
505
+ const dalleOptions = model_options as OpenAiDalleOptions | undefined;
506
+ generateParams.n = dalleOptions?.n || 1;
507
+ generateParams.response_format = dalleOptions?.response_format || "b64_json";
508
+
509
+ if (options.model.includes("dall-e-3")) {
510
+ generateParams.quality = dalleOptions?.image_quality || "standard";
511
+ if (dalleOptions?.style) {
512
+ generateParams.style = dalleOptions.style;
513
+ }
514
+ }
515
+ } else {
516
+ // Default for other models
517
+ generateParams.n = 1;
518
+ }
519
+
520
+ const response = await this.service.images.generate(generateParams);
521
+
522
+ // Convert response to CompletionResults
523
+ const results: CompletionResult[] = [];
524
+
525
+ if (response.data) {
526
+ for (const image of response.data) {
527
+ let imageValue: string;
528
+
529
+ if (image.b64_json) {
530
+ // Base64 format
531
+ imageValue = `data:image/png;base64,${image.b64_json}`;
532
+ } else if (image.url) {
533
+ // URL format
534
+ imageValue = image.url;
535
+ } else {
536
+ continue;
537
+ }
538
+
539
+ results.push({
540
+ type: "image",
541
+ value: imageValue
542
+ });
543
+ }
544
+ }
545
+
546
+ return {
547
+ result: results
548
+ };
549
+
550
+ } catch (error: any) {
551
+ this.logger.error({ error }, `[${this.provider}] Image generation failed`);
552
+ return {
553
+ result: [],
554
+ error: {
555
+ message: error.message,
556
+ code: error.code || 'GENERATION_FAILED'
557
+ }
558
+ };
559
+ }
560
+ }
561
+
562
+ /**
563
+ * Format OpenAI API errors into LlumiverseError with proper status codes and retryability.
564
+ *
565
+ * OpenAI API errors have a specific structure:
566
+ * - APIError.status: HTTP status code (400, 401, 403, 404, 409, 422, 429, 500+)
567
+ * - APIError.error: Error object with type, message, param, code
568
+ * - APIError.requestID: Request ID for support
569
+ * - APIError.code: Error code (e.g., 'invalid_api_key', 'rate_limit_exceeded')
570
+ * - APIError.param: Parameter that caused the error (optional)
571
+ * - APIError.type: Error type (optional)
572
+ *
573
+ * Common error types:
574
+ * - BadRequestError (400): Invalid request parameters
575
+ * - AuthenticationError (401): Invalid API key
576
+ * - PermissionDeniedError (403): Insufficient permissions
577
+ * - NotFoundError (404): Resource not found
578
+ * - ConflictError (409): Resource conflict
579
+ * - UnprocessableEntityError (422): Validation error
580
+ * - RateLimitError (429): Rate limit exceeded
581
+ * - InternalServerError (500+): Server-side errors
582
+ * - APIConnectionError: Connection issues (no status code)
583
+ * - APIConnectionTimeoutError: Request timeout (no status code)
584
+ * - LengthFinishReasonError: Response truncated due to length
585
+ * - ContentFilterFinishReasonError: Content filtered
586
+ *
587
+ * This implementation works for:
588
+ * - OpenAI API
589
+ * - Azure OpenAI
590
+ * - xAI (uses OpenAI-compatible API)
591
+ * - Azure Foundry (OpenAI-compatible)
592
+ * - Other OpenAI-compatible APIs
593
+ *
594
+ * @see https://platform.openai.com/docs/guides/error-codes
595
+ */
596
+ public formatLlumiverseError(
597
+ error: unknown,
598
+ context: LlumiverseErrorContext
599
+ ): LlumiverseError {
600
+ // Check if it's an OpenAI API error
601
+ const isOpenAIError = this.isOpenAIApiError(error);
602
+
603
+ if (!isOpenAIError) {
604
+ // Not an OpenAI API error, use default handling
605
+ throw error;
606
+ }
607
+
608
+ const apiError = error as APIError;
609
+ const httpStatusCode = apiError.status;
610
+
611
+ // Extract error message
612
+ const message = apiError.message || String(error);
613
+
614
+ // Extract additional error details (only available on APIError)
615
+ const errorCode = apiError.code;
616
+ const errorParam = apiError.param;
617
+ const errorType = apiError.type;
618
+
619
+ // Build user-facing message with status code
620
+ let userMessage = message;
621
+
622
+ // Include status code in message (for end-user visibility)
623
+ if (httpStatusCode) {
624
+ userMessage = `[${httpStatusCode}] ${userMessage}`;
625
+ }
626
+
627
+ // Add error code if available and not already in message
628
+ if (errorCode && !userMessage.includes(errorCode)) {
629
+ userMessage += ` (code: ${errorCode})`;
630
+ }
631
+
632
+ // Add parameter info if available and helpful
633
+ if (errorParam && !userMessage.toLowerCase().includes(errorParam.toLowerCase())) {
634
+ userMessage += ` [param: ${errorParam}]`;
635
+ }
636
+
637
+ // Add request ID if available (useful for OpenAI support)
638
+ if (apiError.requestID) {
639
+ userMessage += ` (Request ID: ${apiError.requestID})`;
640
+ }
641
+
642
+ // Determine retryability based on OpenAI error types
643
+ const retryable = this.isOpenAIErrorRetryable(error, httpStatusCode, errorCode, errorType);
644
+
645
+ // Use the error constructor name as the error name
646
+ const errorName = error.constructor?.name || 'OpenAIError';
647
+
648
+ return new LlumiverseError(
649
+ `[${context.provider}] ${userMessage}`,
650
+ retryable,
651
+ context,
652
+ error,
653
+ httpStatusCode,
654
+ errorName
655
+ );
656
+ }
657
+
658
+ /**
659
+ * Type guard to check if error is an OpenAI API error or OpenAI-specific error.
660
+ */
661
+ private isOpenAIApiError(error: unknown): error is APIError | OpenAIError {
662
+ return (
663
+ error !== null &&
664
+ typeof error === 'object' &&
665
+ (error instanceof APIError || error instanceof OpenAIError)
666
+ );
667
+ }
668
+
669
+ /**
670
+ * Determine if an OpenAI API error is retryable.
671
+ *
672
+ * Retryable errors:
673
+ * - RateLimitError (429): Rate limit exceeded, retry with backoff
674
+ * - InternalServerError (500+): Server-side errors
675
+ * - APIConnectionTimeoutError: Request timeout
676
+ * - Error codes: 'timeout', 'server_error', 'service_unavailable'
677
+ * - Status codes: 408, 429, 502, 503, 504, 529, 5xx
678
+ *
679
+ * Non-retryable errors:
680
+ * - BadRequestError (400): Invalid request parameters
681
+ * - AuthenticationError (401): Invalid API key
682
+ * - PermissionDeniedError (403): Insufficient permissions
683
+ * - NotFoundError (404): Resource not found
684
+ * - ConflictError (409): Resource conflict
685
+ * - UnprocessableEntityError (422): Validation error
686
+ * - LengthFinishReasonError: Length limit reached
687
+ * - ContentFilterFinishReasonError: Content filtered
688
+ * - Error codes: 'invalid_api_key', 'invalid_request_error', 'model_not_found'
689
+ * - Other 4xx client errors
690
+ *
691
+ * @param error - The error object
692
+ * @param httpStatusCode - The HTTP status code if available
693
+ * @param errorCode - The error code if available
694
+ * @param errorType - The error type if available
695
+ * @returns True if retryable, false if not retryable, undefined if unknown
696
+ */
697
+ private isOpenAIErrorRetryable(
698
+ error: unknown,
699
+ httpStatusCode: number | undefined,
700
+ errorCode: string | null | undefined,
701
+ errorType: string | undefined
702
+ ): boolean | undefined {
703
+ // Check specific OpenAI error types by class
704
+ if (error instanceof RateLimitError) return true;
705
+ if (error instanceof InternalServerError) return true;
706
+ if (error instanceof APIConnectionTimeoutError) return true;
707
+
708
+ // Non-retryable by error type
709
+ if (error instanceof BadRequestError) return false;
710
+ if (error instanceof AuthenticationError) return false;
711
+ if (error instanceof PermissionDeniedError) return false;
712
+ if (error instanceof NotFoundError) return false;
713
+ if (error instanceof ConflictError) return false;
714
+ if (error instanceof UnprocessableEntityError) return false;
715
+ if (error instanceof LengthFinishReasonError) return false;
716
+ if (error instanceof ContentFilterFinishReasonError) return false;
717
+
718
+ // Check error codes (OpenAI specific)
719
+ if (errorCode) {
720
+ // Retryable error codes
721
+ if (errorCode === 'timeout') return true;
722
+ if (errorCode === 'server_error') return true;
723
+ if (errorCode === 'service_unavailable') return true;
724
+ if (errorCode === 'rate_limit_exceeded') return true;
725
+
726
+ // Non-retryable error codes
727
+ if (errorCode === 'invalid_api_key') return false;
728
+ if (errorCode === 'invalid_request_error') return false;
729
+ if (errorCode === 'model_not_found') return false;
730
+ if (errorCode === 'insufficient_quota') return false;
731
+ if (errorCode === 'invalid_model') return false;
732
+ if (errorCode.includes('invalid_')) return false;
733
+ }
734
+
735
+ // Check error type
736
+ if (errorType === 'invalid_request_error') return false;
737
+ if (errorType === 'authentication_error') return false;
738
+
739
+ // Use HTTP status code
740
+ if (httpStatusCode !== undefined) {
741
+ if (httpStatusCode === 429) return true; // Rate limit
742
+ if (httpStatusCode === 408) return true; // Request timeout
743
+ if (httpStatusCode === 502) return true; // Bad gateway
744
+ if (httpStatusCode === 503) return true; // Service unavailable
745
+ if (httpStatusCode === 504) return true; // Gateway timeout
746
+ if (httpStatusCode === 529) return true; // Overloaded
747
+ if (httpStatusCode >= 500 && httpStatusCode < 600) return true; // Server errors
748
+ if (httpStatusCode >= 400 && httpStatusCode < 500) return false; // Client errors
749
+ }
750
+
751
+ // Connection errors without status codes
752
+ if (error instanceof APIConnectionError && !(error instanceof APIConnectionTimeoutError)) {
753
+ // Generic connection errors might be retryable (network issues)
754
+ return true;
755
+ }
756
+
757
+ // Unknown error type - let consumer decide retry strategy
758
+ return undefined;
759
+ }
760
+
399
761
  }
400
762
 
401
763
 
@@ -604,6 +966,40 @@ function supportsSchema(model: string): boolean {
604
966
  return supportsToolUse(model, "openai");
605
967
  }
606
968
 
969
+ /**
970
+ * Converts function_call and function_call_output items to text messages in OpenAI conversation.
971
+ * Preserves tool call information while removing structured items that require
972
+ * tools to be defined in the API request.
973
+ */
974
+ export function convertOpenAIFunctionItemsToText(items: ResponseInputItem[]): ResponseInputItem[] {
975
+ const hasFunctionItems = items.some(item => {
976
+ const type = (item as any).type;
977
+ return type === 'function_call' || type === 'function_call_output';
978
+ });
979
+ if (!hasFunctionItems) return items;
980
+
981
+ return items.map(item => {
982
+ const typed = item as any;
983
+ if (typed.type === 'function_call') {
984
+ const argsStr = typed.arguments || '';
985
+ const truncated = argsStr.length > 500 ? argsStr.substring(0, 500) + '...' : argsStr;
986
+ return {
987
+ role: 'assistant' as const,
988
+ content: `[Tool call: ${typed.name}(${truncated})]`,
989
+ };
990
+ }
991
+ if (typed.type === 'function_call_output') {
992
+ const output = typed.output || 'No output';
993
+ const truncated = output.length > 500 ? output.substring(0, 500) + '...' : output;
994
+ return {
995
+ role: 'user' as const,
996
+ content: `[Tool result: ${truncated}]`,
997
+ };
998
+ }
999
+ return item;
1000
+ });
1001
+ }
1002
+
607
1003
  function getToolDefinitions(tools: ToolDefinition[] | undefined | null): OpenAI.Responses.Tool[] | undefined {
608
1004
  return tools ? tools.map(getToolDefinition) : undefined;
609
1005
  }
@@ -612,10 +1008,12 @@ function getToolDefinition(toolDef: ToolDefinition): OpenAI.Responses.FunctionTo
612
1008
  let strictMode = false;
613
1009
  if (toolDef.input_schema) {
614
1010
  try {
1011
+ //TODO: type assertion here is not safe, does not work with satisfies
615
1012
  parsedSchema = openAISchemaFormat(toolDef.input_schema as JSONSchema);
616
1013
  strictMode = true;
617
1014
  }
618
1015
  catch (e) {
1016
+ //TODO: type assertion here is not safe, does not work with satisfies
619
1017
  parsedSchema = limitedSchemaFormat(toolDef.input_schema as JSONSchema);
620
1018
  strictMode = false;
621
1019
  }
@@ -667,6 +1065,43 @@ export function collectTools(output?: OpenAI.Responses.ResponseOutputItem[]): To
667
1065
  return tools.length > 0 ? tools : undefined;
668
1066
  }
669
1067
 
1068
+ /**
1069
+ * Collect all parts (text and images) from response output in order.
1070
+ * This preserves the original ordering of text and image parts.
1071
+ */
1072
+ function extractCompletionResults(output?: OpenAI.Responses.ResponseOutputItem[]): CompletionResult[] {
1073
+ if (!output) {
1074
+ return [];
1075
+ }
1076
+
1077
+ const results: CompletionResult[] = [];
1078
+ for (const item of output) {
1079
+ if (item.type === 'message') {
1080
+ // Extract text from message content
1081
+ for (const part of item.content) {
1082
+ if (part.type === 'output_text' && part.text) {
1083
+ results.push({
1084
+ type: "text",
1085
+ value: part.text
1086
+ });
1087
+ }
1088
+ }
1089
+ } else if (item.type === 'image_generation_call' && 'result' in item && item.result) {
1090
+ // GPT-image models return base64 encoded images in result field
1091
+ const base64Data = item.result;
1092
+ // Format as data URL for consistency with other image outputs
1093
+ const imageUrl = base64Data.startsWith('data:')
1094
+ ? base64Data
1095
+ : `data:image/png;base64,${base64Data}`;
1096
+ results.push({
1097
+ type: "image",
1098
+ value: imageUrl
1099
+ });
1100
+ }
1101
+ }
1102
+ return results;
1103
+ }
1104
+
670
1105
  //For strict mode false
671
1106
  function limitedSchemaFormat(schema: JSONSchema): JSONSchema {
672
1107
  const formattedSchema = { ...schema };
@@ -757,26 +1192,6 @@ function openAISchemaFormat(schema: JSONSchema, nesting: number = 0): JSONSchema
757
1192
  return formattedSchema
758
1193
  }
759
1194
 
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
1195
  function responseFinishReason(response: OpenAI.Responses.Response, tools?: ToolUse[] | undefined): string | undefined {
781
1196
  if (tools && tools.length > 0) {
782
1197
  return "tool_use";
@@ -793,6 +1208,71 @@ function responseFinishReason(response: OpenAI.Responses.Response, tools?: ToolU
793
1208
  return 'stop';
794
1209
  }
795
1210
 
1211
+ /**
1212
+ * Fix orphaned function_call items in the OpenAI Responses API conversation.
1213
+ *
1214
+ * When an agent is stopped mid-tool-execution, the conversation may contain
1215
+ * function_call items without matching function_call_output items. The OpenAI
1216
+ * Responses API requires every function_call to have a matching function_call_output.
1217
+ *
1218
+ * This function detects such cases and injects synthetic function_call_output items
1219
+ * indicating the tools were interrupted, allowing the conversation to continue.
1220
+ */
1221
+ export function fixOrphanedToolUse(items: ResponseInputItem[]): ResponseInputItem[] {
1222
+ if (items.length < 2) return items;
1223
+
1224
+ // First pass: collect all function_call_output call_ids
1225
+ const outputCallIds = new Set<string>();
1226
+ for (const item of items) {
1227
+ if ('type' in item && item.type === 'function_call_output') {
1228
+ outputCallIds.add((item as OpenAI.Responses.ResponseInputItem.FunctionCallOutput).call_id);
1229
+ }
1230
+ }
1231
+
1232
+ // Second pass: build result, injecting synthetic outputs for orphaned function_calls
1233
+ const result: ResponseInputItem[] = [];
1234
+ const pendingCalls = new Map<string, string>(); // call_id -> tool name
1235
+
1236
+ for (const item of items) {
1237
+ if ('type' in item && item.type === 'function_call') {
1238
+ const fc = item as OpenAI.Responses.ResponseFunctionToolCall;
1239
+ // Only track if there's no matching output anywhere in the conversation
1240
+ if (!outputCallIds.has(fc.call_id)) {
1241
+ pendingCalls.set(fc.call_id, fc.name ?? 'unknown');
1242
+ }
1243
+ result.push(item);
1244
+ } else if ('type' in item && item.type === 'function_call_output') {
1245
+ result.push(item);
1246
+ } else {
1247
+ // Before any non-function item, flush pending orphaned calls
1248
+ if (pendingCalls.size > 0) {
1249
+ for (const [callId, toolName] of pendingCalls) {
1250
+ result.push({
1251
+ type: 'function_call_output',
1252
+ call_id: callId,
1253
+ output: `[Tool interrupted: The user stopped the operation before "${toolName}" could execute.]`,
1254
+ });
1255
+ }
1256
+ pendingCalls.clear();
1257
+ }
1258
+ result.push(item);
1259
+ }
1260
+ }
1261
+
1262
+ // Handle trailing orphans at the end of the conversation
1263
+ if (pendingCalls.size > 0) {
1264
+ for (const [callId, toolName] of pendingCalls) {
1265
+ result.push({
1266
+ type: 'function_call_output',
1267
+ call_id: callId,
1268
+ output: `[Tool interrupted: The user stopped the operation before "${toolName}" could execute.]`,
1269
+ });
1270
+ }
1271
+ }
1272
+
1273
+ return result;
1274
+ }
1275
+
796
1276
  function safeJsonParse(value: unknown): any {
797
1277
  if (typeof value !== 'string') {
798
1278
  return value;