@llumiverse/drivers 1.0.0-dev.20260224.234313Z → 1.0.0-dev.20260331.080752Z

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 (68) hide show
  1. package/lib/cjs/bedrock/converse.js +86 -12
  2. package/lib/cjs/bedrock/converse.js.map +1 -1
  3. package/lib/cjs/bedrock/index.js +208 -1
  4. package/lib/cjs/bedrock/index.js.map +1 -1
  5. package/lib/cjs/groq/index.js +7 -4
  6. package/lib/cjs/groq/index.js.map +1 -1
  7. package/lib/cjs/openai/index.js +457 -26
  8. package/lib/cjs/openai/index.js.map +1 -1
  9. package/lib/cjs/openai/openai_compatible.js +1 -0
  10. package/lib/cjs/openai/openai_compatible.js.map +1 -1
  11. package/lib/cjs/vertexai/index.js +42 -0
  12. package/lib/cjs/vertexai/index.js.map +1 -1
  13. package/lib/cjs/vertexai/models/claude.js +230 -2
  14. package/lib/cjs/vertexai/models/claude.js.map +1 -1
  15. package/lib/cjs/vertexai/models/gemini.js +261 -41
  16. package/lib/cjs/vertexai/models/gemini.js.map +1 -1
  17. package/lib/cjs/vertexai/models.js +1 -1
  18. package/lib/cjs/vertexai/models.js.map +1 -1
  19. package/lib/esm/bedrock/converse.js +80 -6
  20. package/lib/esm/bedrock/converse.js.map +1 -1
  21. package/lib/esm/bedrock/index.js +207 -2
  22. package/lib/esm/bedrock/index.js.map +1 -1
  23. package/lib/esm/groq/index.js +7 -4
  24. package/lib/esm/groq/index.js.map +1 -1
  25. package/lib/esm/openai/index.js +456 -27
  26. package/lib/esm/openai/index.js.map +1 -1
  27. package/lib/esm/openai/openai_compatible.js +1 -0
  28. package/lib/esm/openai/openai_compatible.js.map +1 -1
  29. package/lib/esm/vertexai/index.js +43 -1
  30. package/lib/esm/vertexai/index.js.map +1 -1
  31. package/lib/esm/vertexai/models/claude.js +229 -3
  32. package/lib/esm/vertexai/models/claude.js.map +1 -1
  33. package/lib/esm/vertexai/models/gemini.js +262 -43
  34. package/lib/esm/vertexai/models/gemini.js.map +1 -1
  35. package/lib/esm/vertexai/models.js +1 -1
  36. package/lib/esm/vertexai/models.js.map +1 -1
  37. package/lib/types/bedrock/converse.d.ts +1 -2
  38. package/lib/types/bedrock/converse.d.ts.map +1 -1
  39. package/lib/types/bedrock/index.d.ts +53 -1
  40. package/lib/types/bedrock/index.d.ts.map +1 -1
  41. package/lib/types/openai/index.d.ts +96 -1
  42. package/lib/types/openai/index.d.ts.map +1 -1
  43. package/lib/types/openai/openai_compatible.d.ts +5 -0
  44. package/lib/types/openai/openai_compatible.d.ts.map +1 -1
  45. package/lib/types/openai/openai_format.d.ts +1 -1
  46. package/lib/types/vertexai/index.d.ts +11 -1
  47. package/lib/types/vertexai/index.d.ts.map +1 -1
  48. package/lib/types/vertexai/models/claude.d.ts +64 -1
  49. package/lib/types/vertexai/models/claude.d.ts.map +1 -1
  50. package/lib/types/vertexai/models/gemini.d.ts +61 -1
  51. package/lib/types/vertexai/models/gemini.d.ts.map +1 -1
  52. package/lib/types/vertexai/models.d.ts +6 -1
  53. package/lib/types/vertexai/models.d.ts.map +1 -1
  54. package/package.json +9 -9
  55. package/src/bedrock/converse.ts +85 -10
  56. package/src/bedrock/error-handling.test.ts +352 -0
  57. package/src/bedrock/index.ts +225 -1
  58. package/src/groq/index.ts +9 -4
  59. package/src/openai/error-handling.test.ts +567 -0
  60. package/src/openai/index.ts +505 -29
  61. package/src/openai/openai_compatible.ts +7 -0
  62. package/src/openai/openai_format.ts +1 -1
  63. package/src/vertexai/index.ts +56 -5
  64. package/src/vertexai/models/claude-error-handling.test.ts +432 -0
  65. package/src/vertexai/models/claude.ts +273 -7
  66. package/src/vertexai/models/gemini-error-handling.test.ts +353 -0
  67. package/src/vertexai/models/gemini.ts +304 -48
  68. package/src/vertexai/models.ts +7 -2
@@ -1,8 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.BaseOpenAIDriver = void 0;
4
+ exports.convertOpenAIFunctionItemsToText = convertOpenAIFunctionItemsToText;
4
5
  exports.collectTools = collectTools;
6
+ exports.fixOrphanedToolUse = fixOrphanedToolUse;
5
7
  const core_1 = require("@llumiverse/core");
8
+ const error_1 = require("openai/error");
6
9
  const openai_format_js_1 = require("./openai_format.js");
7
10
  // Helper function to convert string to CompletionResult[]
8
11
  function textToCompletionResult(text) {
@@ -24,13 +27,14 @@ class BaseOpenAIDriver extends core_1.AbstractDriver {
24
27
  extractDataFromResponse(_options, result) {
25
28
  const tokenInfo = mapUsage(result.usage);
26
29
  const tools = collectTools(result.output);
27
- const data = extractTextFromResponse(result);
28
- if (!data && !tools) {
30
+ // Collect all parts in order (text and images)
31
+ const allResults = extractCompletionResults(result.output);
32
+ if (allResults.length === 0 && !tools) {
29
33
  this.logger.error({ result }, "[OpenAI] Response is not valid");
30
34
  throw new Error("Response is not valid: no data");
31
35
  }
32
36
  return {
33
- result: textToCompletionResult(data || ''),
37
+ result: allResults,
34
38
  token_usage: tokenInfo,
35
39
  finish_reason: responseFinishReason(result, tools),
36
40
  tool_use: tools,
@@ -41,9 +45,15 @@ class BaseOpenAIDriver extends core_1.AbstractDriver {
41
45
  this.logger.warn({ options: options.model_options }, "Invalid model options");
42
46
  }
43
47
  // Include conversation history (same as non-streaming)
44
- const conversation = updateConversation(options.conversation, prompt);
48
+ // Fix orphaned function_call items (can occur when agent is stopped mid-tool-execution)
49
+ let conversation = fixOrphanedToolUse(updateConversation(options.conversation, prompt));
45
50
  const toolDefs = getToolDefinitions(options.tools);
46
51
  const useTools = toolDefs ? (0, core_1.supportsToolUse)(options.model, this.provider, true) : false;
52
+ // When no tools are provided but conversation contains function_call/function_call_output
53
+ // items (e.g. checkpoint summary calls), convert them to text to avoid API errors
54
+ if (!useTools) {
55
+ conversation = convertOpenAIFunctionItemsToText(conversation);
56
+ }
47
57
  convertRoles(prompt, options.model);
48
58
  const model_options = options.model_options;
49
59
  insert_image_detail(prompt, model_options?.image_detail ?? "auto");
@@ -90,7 +100,13 @@ class BaseOpenAIDriver extends core_1.AbstractDriver {
90
100
  insert_image_detail(prompt, model_options?.image_detail ?? "auto");
91
101
  const toolDefs = getToolDefinitions(options.tools);
92
102
  const useTools = toolDefs ? (0, core_1.supportsToolUse)(options.model, this.provider) : false;
93
- let conversation = updateConversation(options.conversation, prompt);
103
+ // Fix orphaned function_call items (can occur when agent is stopped mid-tool-execution)
104
+ let conversation = fixOrphanedToolUse(updateConversation(options.conversation, prompt));
105
+ // When no tools are provided but conversation contains function_call/function_call_output
106
+ // items (e.g. checkpoint summary calls), convert them to text to avoid API errors
107
+ if (!useTools) {
108
+ conversation = convertOpenAIFunctionItemsToText(conversation);
109
+ }
94
110
  let parsedSchema = undefined;
95
111
  let strictMode = false;
96
112
  if (options.result_schema && supportsSchema(options.model)) {
@@ -140,10 +156,21 @@ class BaseOpenAIDriver extends core_1.AbstractDriver {
140
156
  let processedConversation = (0, core_1.stripBase64ImagesFromConversation)(conversation, stripOptions);
141
157
  // Truncate large text content if configured
142
158
  processedConversation = (0, core_1.truncateLargeTextInConversation)(processedConversation, stripOptions);
159
+ // Strip old heartbeat status messages
160
+ processedConversation = (0, core_1.stripHeartbeatsFromConversation)(processedConversation, {
161
+ keepForTurns: options.stripHeartbeatsAfterTurns ?? 1,
162
+ currentTurn,
163
+ });
143
164
  completion.conversation = processedConversation;
144
165
  return completion;
145
166
  }
146
167
  canStream(_options) {
168
+ // Image generation models don't support streaming
169
+ if (_options.model.includes("dall-e")
170
+ || _options.model.includes("gpt-image")
171
+ || _options.model.includes("chatgpt-image")) {
172
+ return Promise.resolve(false);
173
+ }
147
174
  if (_options.model.includes("o1")
148
175
  && !(_options.model.includes("mini") || _options.model.includes("preview"))) {
149
176
  //o1 full does not support streaming
@@ -191,6 +218,10 @@ class BaseOpenAIDriver extends core_1.AbstractDriver {
191
218
  };
192
219
  let processedConversation = (0, core_1.stripBase64ImagesFromConversation)(conversation, stripOptions);
193
220
  processedConversation = (0, core_1.truncateLargeTextInConversation)(processedConversation, stripOptions);
221
+ processedConversation = (0, core_1.stripHeartbeatsFromConversation)(processedConversation, {
222
+ keepForTurns: options.stripHeartbeatsAfterTurns ?? 1,
223
+ currentTurn,
224
+ });
194
225
  return processedConversation;
195
226
  }
196
227
  createTrainingPrompt(options) {
@@ -244,7 +275,7 @@ class BaseOpenAIDriver extends core_1.AbstractDriver {
244
275
  //Some of these use the completions API instead of the chat completions API.
245
276
  //Others are for non-text input modalities. Therefore common to both.
246
277
  const wordBlacklist = ["embed", "whisper", "transcribe", "audio", "moderation", "tts",
247
- "realtime", "dall-e", "babbage", "davinci", "codex", "o1-pro", "computer-use", "sora"];
278
+ "realtime", "babbage", "davinci", "codex", "o1-pro", "computer-use", "sora"];
248
279
  //OpenAI has very little information, filtering based on name.
249
280
  result = result.filter((m) => {
250
281
  return !wordBlacklist.some((word) => m.id.includes(word));
@@ -256,14 +287,17 @@ class BaseOpenAIDriver extends core_1.AbstractDriver {
256
287
  if (owner == "system") {
257
288
  owner = "openai";
258
289
  }
290
+ // Determine model type based on capabilities
291
+ let modelType = core_1.ModelType.Text;
292
+ if (m.id.includes("dall-e") || m.id.includes("gpt-image")) {
293
+ modelType = core_1.ModelType.Image;
294
+ }
259
295
  return {
260
296
  id: m.id,
261
297
  name: m.id,
262
298
  provider: this.provider,
263
299
  owner: owner,
264
- type: m.object === "model" ? core_1.ModelType.Text : core_1.ModelType.Unknown,
265
- can_stream: true,
266
- is_multimodal: m.id.includes("gpt-4"),
300
+ type: modelType,
267
301
  input_modalities: (0, core_1.modelModalitiesToArray)(modelCapability.input),
268
302
  output_modalities: (0, core_1.modelModalitiesToArray)(modelCapability.output),
269
303
  tool_support: modelCapability.tool_support,
@@ -288,6 +322,289 @@ class BaseOpenAIDriver extends core_1.AbstractDriver {
288
322
  }
289
323
  return { values: embeddings, model };
290
324
  }
325
+ imageModels = ["dall-e", "gpt-image", "chatgpt-image"];
326
+ /**
327
+ * Determine if a model is specifically an image generation model (not conversational image model)
328
+ */
329
+ isImageModel(model) {
330
+ // DALL-E models are standalone image generation
331
+ // gpt-image models can generate images in conversations, not standalone
332
+ return this.imageModels.some(imageModel => model.includes(imageModel));
333
+ }
334
+ /**
335
+ * Request image generation from standalone Images API
336
+ * Supports: DALL-E 2, DALL-E 3, GPT-image models (for edit/variation)
337
+ */
338
+ async requestImageGeneration(prompt, options) {
339
+ this.logger.debug(`[${this.provider}] Generating image with model ${options.model}`);
340
+ const model_options = options.model_options;
341
+ // Extract prompt text from ResponseInputItem[]
342
+ let promptText = "";
343
+ for (const item of prompt) {
344
+ if ('content' in item && typeof item.content === 'string') {
345
+ promptText += item.content + "\\n";
346
+ }
347
+ else if ('content' in item && Array.isArray(item.content)) {
348
+ // Extract text from content array
349
+ for (const part of item.content) {
350
+ if ('type' in part && part.type === 'input_text' && 'text' in part) {
351
+ promptText += part.text + "\\n";
352
+ }
353
+ }
354
+ }
355
+ }
356
+ promptText = promptText.trim();
357
+ try {
358
+ const generateParams = {
359
+ model: options.model,
360
+ prompt: promptText,
361
+ size: model_options?.size || "1024x1024",
362
+ };
363
+ // Add DALL-E specific options
364
+ if (options.model.includes("dall-e") || model_options?._option_id === "openai-dalle") {
365
+ const dalleOptions = model_options;
366
+ generateParams.n = dalleOptions?.n || 1;
367
+ generateParams.response_format = dalleOptions?.response_format || "b64_json";
368
+ if (options.model.includes("dall-e-3")) {
369
+ generateParams.quality = dalleOptions?.image_quality || "standard";
370
+ if (dalleOptions?.style) {
371
+ generateParams.style = dalleOptions.style;
372
+ }
373
+ }
374
+ }
375
+ else {
376
+ // Default for other models
377
+ generateParams.n = 1;
378
+ }
379
+ const response = await this.service.images.generate(generateParams);
380
+ // Convert response to CompletionResults
381
+ const results = [];
382
+ if (response.data) {
383
+ for (const image of response.data) {
384
+ let imageValue;
385
+ if (image.b64_json) {
386
+ // Base64 format
387
+ imageValue = `data:image/png;base64,${image.b64_json}`;
388
+ }
389
+ else if (image.url) {
390
+ // URL format
391
+ imageValue = image.url;
392
+ }
393
+ else {
394
+ continue;
395
+ }
396
+ results.push({
397
+ type: "image",
398
+ value: imageValue
399
+ });
400
+ }
401
+ }
402
+ return {
403
+ result: results
404
+ };
405
+ }
406
+ catch (error) {
407
+ this.logger.error({ error }, `[${this.provider}] Image generation failed`);
408
+ return {
409
+ result: [],
410
+ error: {
411
+ message: error.message,
412
+ code: error.code || 'GENERATION_FAILED'
413
+ }
414
+ };
415
+ }
416
+ }
417
+ /**
418
+ * Format OpenAI API errors into LlumiverseError with proper status codes and retryability.
419
+ *
420
+ * OpenAI API errors have a specific structure:
421
+ * - APIError.status: HTTP status code (400, 401, 403, 404, 409, 422, 429, 500+)
422
+ * - APIError.error: Error object with type, message, param, code
423
+ * - APIError.requestID: Request ID for support
424
+ * - APIError.code: Error code (e.g., 'invalid_api_key', 'rate_limit_exceeded')
425
+ * - APIError.param: Parameter that caused the error (optional)
426
+ * - APIError.type: Error type (optional)
427
+ *
428
+ * Common error types:
429
+ * - BadRequestError (400): Invalid request parameters
430
+ * - AuthenticationError (401): Invalid API key
431
+ * - PermissionDeniedError (403): Insufficient permissions
432
+ * - NotFoundError (404): Resource not found
433
+ * - ConflictError (409): Resource conflict
434
+ * - UnprocessableEntityError (422): Validation error
435
+ * - RateLimitError (429): Rate limit exceeded
436
+ * - InternalServerError (500+): Server-side errors
437
+ * - APIConnectionError: Connection issues (no status code)
438
+ * - APIConnectionTimeoutError: Request timeout (no status code)
439
+ * - LengthFinishReasonError: Response truncated due to length
440
+ * - ContentFilterFinishReasonError: Content filtered
441
+ *
442
+ * This implementation works for:
443
+ * - OpenAI API
444
+ * - Azure OpenAI
445
+ * - xAI (uses OpenAI-compatible API)
446
+ * - Azure Foundry (OpenAI-compatible)
447
+ * - Other OpenAI-compatible APIs
448
+ *
449
+ * @see https://platform.openai.com/docs/guides/error-codes
450
+ */
451
+ formatLlumiverseError(error, context) {
452
+ // Check if it's an OpenAI API error
453
+ const isOpenAIError = this.isOpenAIApiError(error);
454
+ if (!isOpenAIError) {
455
+ // Not an OpenAI API error, use default handling
456
+ throw error;
457
+ }
458
+ const apiError = error;
459
+ const httpStatusCode = apiError.status;
460
+ // Extract error message
461
+ const message = apiError.message || String(error);
462
+ // Extract additional error details (only available on APIError)
463
+ const errorCode = apiError.code;
464
+ const errorParam = apiError.param;
465
+ const errorType = apiError.type;
466
+ // Build user-facing message with status code
467
+ let userMessage = message;
468
+ // Include status code in message (for end-user visibility)
469
+ if (httpStatusCode) {
470
+ userMessage = `[${httpStatusCode}] ${userMessage}`;
471
+ }
472
+ // Add error code if available and not already in message
473
+ if (errorCode && !userMessage.includes(errorCode)) {
474
+ userMessage += ` (code: ${errorCode})`;
475
+ }
476
+ // Add parameter info if available and helpful
477
+ if (errorParam && !userMessage.toLowerCase().includes(errorParam.toLowerCase())) {
478
+ userMessage += ` [param: ${errorParam}]`;
479
+ }
480
+ // Add request ID if available (useful for OpenAI support)
481
+ if (apiError.requestID) {
482
+ userMessage += ` (Request ID: ${apiError.requestID})`;
483
+ }
484
+ // Determine retryability based on OpenAI error types
485
+ const retryable = this.isOpenAIErrorRetryable(error, httpStatusCode, errorCode, errorType);
486
+ // Use the error constructor name as the error name
487
+ const errorName = error.constructor?.name || 'OpenAIError';
488
+ return new core_1.LlumiverseError(`[${context.provider}] ${userMessage}`, retryable, context, error, httpStatusCode, errorName);
489
+ }
490
+ /**
491
+ * Type guard to check if error is an OpenAI API error or OpenAI-specific error.
492
+ */
493
+ isOpenAIApiError(error) {
494
+ return (error !== null &&
495
+ typeof error === 'object' &&
496
+ (error instanceof error_1.APIError || error instanceof error_1.OpenAIError));
497
+ }
498
+ /**
499
+ * Determine if an OpenAI API error is retryable.
500
+ *
501
+ * Retryable errors:
502
+ * - RateLimitError (429): Rate limit exceeded, retry with backoff
503
+ * - InternalServerError (500+): Server-side errors
504
+ * - APIConnectionTimeoutError: Request timeout
505
+ * - Error codes: 'timeout', 'server_error', 'service_unavailable'
506
+ * - Status codes: 408, 429, 502, 503, 504, 529, 5xx
507
+ *
508
+ * Non-retryable errors:
509
+ * - BadRequestError (400): Invalid request parameters
510
+ * - AuthenticationError (401): Invalid API key
511
+ * - PermissionDeniedError (403): Insufficient permissions
512
+ * - NotFoundError (404): Resource not found
513
+ * - ConflictError (409): Resource conflict
514
+ * - UnprocessableEntityError (422): Validation error
515
+ * - LengthFinishReasonError: Length limit reached
516
+ * - ContentFilterFinishReasonError: Content filtered
517
+ * - Error codes: 'invalid_api_key', 'invalid_request_error', 'model_not_found'
518
+ * - Other 4xx client errors
519
+ *
520
+ * @param error - The error object
521
+ * @param httpStatusCode - The HTTP status code if available
522
+ * @param errorCode - The error code if available
523
+ * @param errorType - The error type if available
524
+ * @returns True if retryable, false if not retryable, undefined if unknown
525
+ */
526
+ isOpenAIErrorRetryable(error, httpStatusCode, errorCode, errorType) {
527
+ // Check specific OpenAI error types by class
528
+ if (error instanceof error_1.RateLimitError)
529
+ return true;
530
+ if (error instanceof error_1.InternalServerError)
531
+ return true;
532
+ if (error instanceof error_1.APIConnectionTimeoutError)
533
+ return true;
534
+ // Non-retryable by error type
535
+ if (error instanceof error_1.BadRequestError)
536
+ return false;
537
+ if (error instanceof error_1.AuthenticationError)
538
+ return false;
539
+ if (error instanceof error_1.PermissionDeniedError)
540
+ return false;
541
+ if (error instanceof error_1.NotFoundError)
542
+ return false;
543
+ if (error instanceof error_1.ConflictError)
544
+ return false;
545
+ if (error instanceof error_1.UnprocessableEntityError)
546
+ return false;
547
+ if (error instanceof error_1.LengthFinishReasonError)
548
+ return false;
549
+ if (error instanceof error_1.ContentFilterFinishReasonError)
550
+ return false;
551
+ // Check error codes (OpenAI specific)
552
+ if (errorCode) {
553
+ // Retryable error codes
554
+ if (errorCode === 'timeout')
555
+ return true;
556
+ if (errorCode === 'server_error')
557
+ return true;
558
+ if (errorCode === 'service_unavailable')
559
+ return true;
560
+ if (errorCode === 'rate_limit_exceeded')
561
+ return true;
562
+ // Non-retryable error codes
563
+ if (errorCode === 'invalid_api_key')
564
+ return false;
565
+ if (errorCode === 'invalid_request_error')
566
+ return false;
567
+ if (errorCode === 'model_not_found')
568
+ return false;
569
+ if (errorCode === 'insufficient_quota')
570
+ return false;
571
+ if (errorCode === 'invalid_model')
572
+ return false;
573
+ if (errorCode.includes('invalid_'))
574
+ return false;
575
+ }
576
+ // Check error type
577
+ if (errorType === 'invalid_request_error')
578
+ return false;
579
+ if (errorType === 'authentication_error')
580
+ return false;
581
+ // Use HTTP status code
582
+ if (httpStatusCode !== undefined) {
583
+ if (httpStatusCode === 429)
584
+ return true; // Rate limit
585
+ if (httpStatusCode === 408)
586
+ return true; // Request timeout
587
+ if (httpStatusCode === 502)
588
+ return true; // Bad gateway
589
+ if (httpStatusCode === 503)
590
+ return true; // Service unavailable
591
+ if (httpStatusCode === 504)
592
+ return true; // Gateway timeout
593
+ if (httpStatusCode === 529)
594
+ return true; // Overloaded
595
+ if (httpStatusCode >= 500 && httpStatusCode < 600)
596
+ return true; // Server errors
597
+ if (httpStatusCode >= 400 && httpStatusCode < 500)
598
+ return false; // Client errors
599
+ }
600
+ // Connection errors without status codes
601
+ if (error instanceof error_1.APIConnectionError && !(error instanceof error_1.APIConnectionTimeoutError)) {
602
+ // Generic connection errors might be retryable (network issues)
603
+ return true;
604
+ }
605
+ // Unknown error type - let consumer decide retry strategy
606
+ return undefined;
607
+ }
291
608
  }
292
609
  exports.BaseOpenAIDriver = BaseOpenAIDriver;
293
610
  function jobInfo(job) {
@@ -489,6 +806,39 @@ function supportsSchema(model) {
489
806
  }
490
807
  return (0, core_1.supportsToolUse)(model, "openai");
491
808
  }
809
+ /**
810
+ * Converts function_call and function_call_output items to text messages in OpenAI conversation.
811
+ * Preserves tool call information while removing structured items that require
812
+ * tools to be defined in the API request.
813
+ */
814
+ function convertOpenAIFunctionItemsToText(items) {
815
+ const hasFunctionItems = items.some(item => {
816
+ const type = item.type;
817
+ return type === 'function_call' || type === 'function_call_output';
818
+ });
819
+ if (!hasFunctionItems)
820
+ return items;
821
+ return items.map(item => {
822
+ const typed = item;
823
+ if (typed.type === 'function_call') {
824
+ const argsStr = typed.arguments || '';
825
+ const truncated = argsStr.length > 500 ? argsStr.substring(0, 500) + '...' : argsStr;
826
+ return {
827
+ role: 'assistant',
828
+ content: `[Tool call: ${typed.name}(${truncated})]`,
829
+ };
830
+ }
831
+ if (typed.type === 'function_call_output') {
832
+ const output = typed.output || 'No output';
833
+ const truncated = output.length > 500 ? output.substring(0, 500) + '...' : output;
834
+ return {
835
+ role: 'user',
836
+ content: `[Tool result: ${truncated}]`,
837
+ };
838
+ }
839
+ return item;
840
+ });
841
+ }
492
842
  function getToolDefinitions(tools) {
493
843
  return tools ? tools.map(getToolDefinition) : undefined;
494
844
  }
@@ -549,6 +899,42 @@ function collectTools(output) {
549
899
  }
550
900
  return tools.length > 0 ? tools : undefined;
551
901
  }
902
+ /**
903
+ * Collect all parts (text and images) from response output in order.
904
+ * This preserves the original ordering of text and image parts.
905
+ */
906
+ function extractCompletionResults(output) {
907
+ if (!output) {
908
+ return [];
909
+ }
910
+ const results = [];
911
+ for (const item of output) {
912
+ if (item.type === 'message') {
913
+ // Extract text from message content
914
+ for (const part of item.content) {
915
+ if (part.type === 'output_text' && part.text) {
916
+ results.push({
917
+ type: "text",
918
+ value: part.text
919
+ });
920
+ }
921
+ }
922
+ }
923
+ else if (item.type === 'image_generation_call' && 'result' in item && item.result) {
924
+ // GPT-image models return base64 encoded images in result field
925
+ const base64Data = item.result;
926
+ // Format as data URL for consistency with other image outputs
927
+ const imageUrl = base64Data.startsWith('data:')
928
+ ? base64Data
929
+ : `data:image/png;base64,${base64Data}`;
930
+ results.push({
931
+ type: "image",
932
+ value: imageUrl
933
+ });
934
+ }
935
+ }
936
+ return results;
937
+ }
552
938
  //For strict mode false
553
939
  function limitedSchemaFormat(schema) {
554
940
  const formattedSchema = { ...schema };
@@ -623,23 +1009,6 @@ function openAISchemaFormat(schema, nesting = 0) {
623
1009
  }
624
1010
  return formattedSchema;
625
1011
  }
626
- function extractTextFromResponse(response) {
627
- if (response.output_text) {
628
- return response.output_text;
629
- }
630
- const collected = [];
631
- for (const item of response.output ?? []) {
632
- if (item.type === 'message') {
633
- const text = item.content
634
- .map(part => part.type === 'output_text' ? part.text : '')
635
- .join('');
636
- if (text) {
637
- collected.push(text);
638
- }
639
- }
640
- }
641
- return collected.join("\n");
642
- }
643
1012
  function responseFinishReason(response, tools) {
644
1013
  if (tools && tools.length > 0) {
645
1014
  return "tool_use";
@@ -655,6 +1024,68 @@ function responseFinishReason(response, tools) {
655
1024
  }
656
1025
  return 'stop';
657
1026
  }
1027
+ /**
1028
+ * Fix orphaned function_call items in the OpenAI Responses API conversation.
1029
+ *
1030
+ * When an agent is stopped mid-tool-execution, the conversation may contain
1031
+ * function_call items without matching function_call_output items. The OpenAI
1032
+ * Responses API requires every function_call to have a matching function_call_output.
1033
+ *
1034
+ * This function detects such cases and injects synthetic function_call_output items
1035
+ * indicating the tools were interrupted, allowing the conversation to continue.
1036
+ */
1037
+ function fixOrphanedToolUse(items) {
1038
+ if (items.length < 2)
1039
+ return items;
1040
+ // First pass: collect all function_call_output call_ids
1041
+ const outputCallIds = new Set();
1042
+ for (const item of items) {
1043
+ if ('type' in item && item.type === 'function_call_output') {
1044
+ outputCallIds.add(item.call_id);
1045
+ }
1046
+ }
1047
+ // Second pass: build result, injecting synthetic outputs for orphaned function_calls
1048
+ const result = [];
1049
+ const pendingCalls = new Map(); // call_id -> tool name
1050
+ for (const item of items) {
1051
+ if ('type' in item && item.type === 'function_call') {
1052
+ const fc = item;
1053
+ // Only track if there's no matching output anywhere in the conversation
1054
+ if (!outputCallIds.has(fc.call_id)) {
1055
+ pendingCalls.set(fc.call_id, fc.name ?? 'unknown');
1056
+ }
1057
+ result.push(item);
1058
+ }
1059
+ else if ('type' in item && item.type === 'function_call_output') {
1060
+ result.push(item);
1061
+ }
1062
+ else {
1063
+ // Before any non-function item, flush pending orphaned calls
1064
+ if (pendingCalls.size > 0) {
1065
+ for (const [callId, toolName] of pendingCalls) {
1066
+ result.push({
1067
+ type: 'function_call_output',
1068
+ call_id: callId,
1069
+ output: `[Tool interrupted: The user stopped the operation before "${toolName}" could execute.]`,
1070
+ });
1071
+ }
1072
+ pendingCalls.clear();
1073
+ }
1074
+ result.push(item);
1075
+ }
1076
+ }
1077
+ // Handle trailing orphans at the end of the conversation
1078
+ if (pendingCalls.size > 0) {
1079
+ for (const [callId, toolName] of pendingCalls) {
1080
+ result.push({
1081
+ type: 'function_call_output',
1082
+ call_id: callId,
1083
+ output: `[Tool interrupted: The user stopped the operation before "${toolName}" could execute.]`,
1084
+ });
1085
+ }
1086
+ }
1087
+ return result;
1088
+ }
658
1089
  function safeJsonParse(value) {
659
1090
  if (typeof value !== 'string') {
660
1091
  return value;