@llumiverse/drivers 0.23.0-dev.20251121 → 0.24.0-dev.202601221707

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) 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 +239 -19
  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 +211 -5
  17. package/lib/cjs/vertexai/index.js.map +1 -1
  18. package/lib/cjs/vertexai/models/claude.js +201 -3
  19. package/lib/cjs/vertexai/models/claude.js.map +1 -1
  20. package/lib/cjs/vertexai/models/gemini.js +74 -25
  21. package/lib/cjs/vertexai/models/gemini.js.map +1 -1
  22. package/lib/cjs/vertexai/models/imagen.js +0 -3
  23. package/lib/cjs/vertexai/models/imagen.js.map +1 -1
  24. package/lib/cjs/xai/index.js +10 -16
  25. package/lib/cjs/xai/index.js.map +1 -1
  26. package/lib/esm/azure/azure_foundry.js +46 -2
  27. package/lib/esm/azure/azure_foundry.js.map +1 -1
  28. package/lib/esm/bedrock/index.js +239 -20
  29. package/lib/esm/bedrock/index.js.map +1 -1
  30. package/lib/esm/groq/index.js +115 -85
  31. package/lib/esm/groq/index.js.map +1 -1
  32. package/lib/esm/index.js +1 -0
  33. package/lib/esm/index.js.map +1 -1
  34. package/lib/esm/openai/index.js +311 -115
  35. package/lib/esm/openai/index.js.map +1 -1
  36. package/lib/esm/openai/openai_compatible.js +55 -0
  37. package/lib/esm/openai/openai_compatible.js.map +1 -0
  38. package/lib/esm/openai/openai_format.js +32 -39
  39. package/lib/esm/openai/openai_format.js.map +1 -1
  40. package/lib/esm/vertexai/index.js +212 -6
  41. package/lib/esm/vertexai/index.js.map +1 -1
  42. package/lib/esm/vertexai/models/claude.js +199 -3
  43. package/lib/esm/vertexai/models/claude.js.map +1 -1
  44. package/lib/esm/vertexai/models/gemini.js +75 -26
  45. package/lib/esm/vertexai/models/gemini.js.map +1 -1
  46. package/lib/esm/vertexai/models/imagen.js +1 -4
  47. package/lib/esm/vertexai/models/imagen.js.map +1 -1
  48. package/lib/esm/xai/index.js +10 -16
  49. package/lib/esm/xai/index.js.map +1 -1
  50. package/lib/types/azure/azure_foundry.d.ts +7 -5
  51. package/lib/types/azure/azure_foundry.d.ts.map +1 -1
  52. package/lib/types/bedrock/index.d.ts +22 -1
  53. package/lib/types/bedrock/index.d.ts.map +1 -1
  54. package/lib/types/groq/index.d.ts.map +1 -1
  55. package/lib/types/index.d.ts +1 -0
  56. package/lib/types/index.d.ts.map +1 -1
  57. package/lib/types/openai/index.d.ts +13 -7
  58. package/lib/types/openai/index.d.ts.map +1 -1
  59. package/lib/types/openai/openai_compatible.d.ts +26 -0
  60. package/lib/types/openai/openai_compatible.d.ts.map +1 -0
  61. package/lib/types/openai/openai_format.d.ts +4 -2
  62. package/lib/types/openai/openai_format.d.ts.map +1 -1
  63. package/lib/types/vertexai/index.d.ts +16 -0
  64. package/lib/types/vertexai/index.d.ts.map +1 -1
  65. package/lib/types/vertexai/models/claude.d.ts +20 -0
  66. package/lib/types/vertexai/models/claude.d.ts.map +1 -1
  67. package/lib/types/vertexai/models/gemini.d.ts +1 -1
  68. package/lib/types/vertexai/models/gemini.d.ts.map +1 -1
  69. package/lib/types/vertexai/models/imagen.d.ts.map +1 -1
  70. package/lib/types/xai/index.d.ts +2 -3
  71. package/lib/types/xai/index.d.ts.map +1 -1
  72. package/package.json +17 -17
  73. package/src/azure/azure_foundry.ts +56 -7
  74. package/src/bedrock/index.ts +301 -29
  75. package/src/groq/index.ts +120 -94
  76. package/src/index.ts +1 -0
  77. package/src/openai/index.ts +363 -136
  78. package/src/openai/openai_compatible.ts +74 -0
  79. package/src/openai/openai_format.ts +44 -54
  80. package/src/vertexai/index.ts +255 -6
  81. package/src/vertexai/models/claude.ts +233 -3
  82. package/src/vertexai/models/gemini.ts +95 -32
  83. package/src/vertexai/models/imagen.ts +1 -5
  84. package/src/xai/index.ts +10 -17
  85. package/lib/cjs/test/TestErrorCompletionStream.js +0 -20
  86. package/lib/cjs/test/TestErrorCompletionStream.js.map +0 -1
  87. package/lib/cjs/test/TestValidationErrorCompletionStream.js +0 -24
  88. package/lib/cjs/test/TestValidationErrorCompletionStream.js.map +0 -1
  89. package/lib/cjs/test/index.js +0 -109
  90. package/lib/cjs/test/index.js.map +0 -1
  91. package/lib/cjs/test/utils.js +0 -30
  92. package/lib/cjs/test/utils.js.map +0 -1
  93. package/lib/esm/src/adobe/firefly.js +0 -116
  94. package/lib/esm/src/adobe/firefly.js.map +0 -1
  95. package/lib/esm/src/azure/azure_foundry.js +0 -382
  96. package/lib/esm/src/azure/azure_foundry.js.map +0 -1
  97. package/lib/esm/src/bedrock/converse.js +0 -278
  98. package/lib/esm/src/bedrock/converse.js.map +0 -1
  99. package/lib/esm/src/bedrock/index.js +0 -962
  100. package/lib/esm/src/bedrock/index.js.map +0 -1
  101. package/lib/esm/src/bedrock/nova-image-payload.js +0 -203
  102. package/lib/esm/src/bedrock/nova-image-payload.js.map +0 -1
  103. package/lib/esm/src/bedrock/payloads.js +0 -2
  104. package/lib/esm/src/bedrock/payloads.js.map +0 -1
  105. package/lib/esm/src/bedrock/s3.js +0 -99
  106. package/lib/esm/src/bedrock/s3.js.map +0 -1
  107. package/lib/esm/src/bedrock/twelvelabs.js +0 -84
  108. package/lib/esm/src/bedrock/twelvelabs.js.map +0 -1
  109. package/lib/esm/src/groq/index.js +0 -286
  110. package/lib/esm/src/groq/index.js.map +0 -1
  111. package/lib/esm/src/huggingface_ie.js +0 -197
  112. package/lib/esm/src/huggingface_ie.js.map +0 -1
  113. package/lib/esm/src/index.js +0 -14
  114. package/lib/esm/src/index.js.map +0 -1
  115. package/lib/esm/src/mistral/index.js +0 -169
  116. package/lib/esm/src/mistral/index.js.map +0 -1
  117. package/lib/esm/src/mistral/types.js +0 -80
  118. package/lib/esm/src/mistral/types.js.map +0 -1
  119. package/lib/esm/src/openai/azure_openai.js +0 -68
  120. package/lib/esm/src/openai/azure_openai.js.map +0 -1
  121. package/lib/esm/src/openai/index.js +0 -464
  122. package/lib/esm/src/openai/index.js.map +0 -1
  123. package/lib/esm/src/openai/openai.js +0 -14
  124. package/lib/esm/src/openai/openai.js.map +0 -1
  125. package/lib/esm/src/openai/openai_format.js +0 -134
  126. package/lib/esm/src/openai/openai_format.js.map +0 -1
  127. package/lib/esm/src/replicate.js +0 -268
  128. package/lib/esm/src/replicate.js.map +0 -1
  129. package/lib/esm/src/test/TestErrorCompletionStream.js +0 -16
  130. package/lib/esm/src/test/TestErrorCompletionStream.js.map +0 -1
  131. package/lib/esm/src/test/TestValidationErrorCompletionStream.js +0 -20
  132. package/lib/esm/src/test/TestValidationErrorCompletionStream.js.map +0 -1
  133. package/lib/esm/src/test/index.js +0 -91
  134. package/lib/esm/src/test/index.js.map +0 -1
  135. package/lib/esm/src/test/utils.js +0 -25
  136. package/lib/esm/src/test/utils.js.map +0 -1
  137. package/lib/esm/src/test-driver/TestErrorCompletionStream.js +0 -16
  138. package/lib/esm/src/test-driver/TestErrorCompletionStream.js.map +0 -1
  139. package/lib/esm/src/test-driver/TestValidationErrorCompletionStream.js +0 -20
  140. package/lib/esm/src/test-driver/TestValidationErrorCompletionStream.js.map +0 -1
  141. package/lib/esm/src/test-driver/index.js +0 -91
  142. package/lib/esm/src/test-driver/index.js.map +0 -1
  143. package/lib/esm/src/test-driver/utils.js +0 -25
  144. package/lib/esm/src/test-driver/utils.js.map +0 -1
  145. package/lib/esm/src/togetherai/index.js +0 -122
  146. package/lib/esm/src/togetherai/index.js.map +0 -1
  147. package/lib/esm/src/togetherai/interfaces.js +0 -2
  148. package/lib/esm/src/togetherai/interfaces.js.map +0 -1
  149. package/lib/esm/src/vertexai/debug.js +0 -6
  150. package/lib/esm/src/vertexai/debug.js.map +0 -1
  151. package/lib/esm/src/vertexai/embeddings/embeddings-image.js +0 -24
  152. package/lib/esm/src/vertexai/embeddings/embeddings-image.js.map +0 -1
  153. package/lib/esm/src/vertexai/embeddings/embeddings-text.js +0 -20
  154. package/lib/esm/src/vertexai/embeddings/embeddings-text.js.map +0 -1
  155. package/lib/esm/src/vertexai/index.js +0 -383
  156. package/lib/esm/src/vertexai/index.js.map +0 -1
  157. package/lib/esm/src/vertexai/models/claude.js +0 -394
  158. package/lib/esm/src/vertexai/models/claude.js.map +0 -1
  159. package/lib/esm/src/vertexai/models/gemini.js +0 -817
  160. package/lib/esm/src/vertexai/models/gemini.js.map +0 -1
  161. package/lib/esm/src/vertexai/models/imagen.js +0 -302
  162. package/lib/esm/src/vertexai/models/imagen.js.map +0 -1
  163. package/lib/esm/src/vertexai/models/llama.js +0 -179
  164. package/lib/esm/src/vertexai/models/llama.js.map +0 -1
  165. package/lib/esm/src/vertexai/models.js +0 -32
  166. package/lib/esm/src/vertexai/models.js.map +0 -1
  167. package/lib/esm/src/watsonx/index.js +0 -157
  168. package/lib/esm/src/watsonx/index.js.map +0 -1
  169. package/lib/esm/src/watsonx/interfaces.js +0 -2
  170. package/lib/esm/src/watsonx/interfaces.js.map +0 -1
  171. package/lib/esm/src/xai/index.js +0 -64
  172. package/lib/esm/src/xai/index.js.map +0 -1
  173. package/lib/esm/test/TestErrorCompletionStream.js +0 -16
  174. package/lib/esm/test/TestErrorCompletionStream.js.map +0 -1
  175. package/lib/esm/test/TestValidationErrorCompletionStream.js +0 -20
  176. package/lib/esm/test/TestValidationErrorCompletionStream.js.map +0 -1
  177. package/lib/esm/test/index.js +0 -91
  178. package/lib/esm/test/index.js.map +0 -1
  179. package/lib/esm/test/utils.js +0 -25
  180. package/lib/esm/test/utils.js.map +0 -1
  181. package/lib/esm/tsconfig.tsbuildinfo +0 -1
  182. package/lib/types/src/adobe/firefly.d.ts +0 -29
  183. package/lib/types/src/azure/azure_foundry.d.ts +0 -49
  184. package/lib/types/src/bedrock/converse.d.ts +0 -8
  185. package/lib/types/src/bedrock/index.d.ts +0 -61
  186. package/lib/types/src/bedrock/nova-image-payload.d.ts +0 -73
  187. package/lib/types/src/bedrock/payloads.d.ts +0 -11
  188. package/lib/types/src/bedrock/s3.d.ts +0 -22
  189. package/lib/types/src/bedrock/twelvelabs.d.ts +0 -49
  190. package/lib/types/src/groq/index.d.ts +0 -26
  191. package/lib/types/src/huggingface_ie.d.ts +0 -34
  192. package/lib/types/src/index.d.ts +0 -13
  193. package/lib/types/src/mistral/index.d.ts +0 -24
  194. package/lib/types/src/mistral/types.d.ts +0 -131
  195. package/lib/types/src/openai/azure_openai.d.ts +0 -24
  196. package/lib/types/src/openai/index.d.ts +0 -24
  197. package/lib/types/src/openai/openai.d.ts +0 -14
  198. package/lib/types/src/openai/openai_format.d.ts +0 -18
  199. package/lib/types/src/replicate.d.ts +0 -47
  200. package/lib/types/src/test/TestErrorCompletionStream.d.ts +0 -8
  201. package/lib/types/src/test/TestValidationErrorCompletionStream.d.ts +0 -8
  202. package/lib/types/src/test/index.d.ts +0 -23
  203. package/lib/types/src/test/utils.d.ts +0 -4
  204. package/lib/types/src/test-driver/TestErrorCompletionStream.d.ts +0 -8
  205. package/lib/types/src/test-driver/TestValidationErrorCompletionStream.d.ts +0 -8
  206. package/lib/types/src/test-driver/index.d.ts +0 -23
  207. package/lib/types/src/test-driver/utils.d.ts +0 -4
  208. package/lib/types/src/togetherai/index.d.ts +0 -22
  209. package/lib/types/src/togetherai/interfaces.d.ts +0 -95
  210. package/lib/types/src/vertexai/debug.d.ts +0 -1
  211. package/lib/types/src/vertexai/embeddings/embeddings-image.d.ts +0 -10
  212. package/lib/types/src/vertexai/embeddings/embeddings-text.d.ts +0 -9
  213. package/lib/types/src/vertexai/index.d.ts +0 -52
  214. package/lib/types/src/vertexai/models/claude.d.ts +0 -19
  215. package/lib/types/src/vertexai/models/gemini.d.ts +0 -17
  216. package/lib/types/src/vertexai/models/imagen.d.ts +0 -74
  217. package/lib/types/src/vertexai/models/llama.d.ts +0 -19
  218. package/lib/types/src/vertexai/models.d.ts +0 -14
  219. package/lib/types/src/watsonx/index.d.ts +0 -26
  220. package/lib/types/src/watsonx/interfaces.d.ts +0 -64
  221. package/lib/types/src/xai/index.d.ts +0 -18
  222. package/lib/types/test/TestErrorCompletionStream.d.ts +0 -9
  223. package/lib/types/test/TestErrorCompletionStream.d.ts.map +0 -1
  224. package/lib/types/test/TestValidationErrorCompletionStream.d.ts +0 -9
  225. package/lib/types/test/TestValidationErrorCompletionStream.d.ts.map +0 -1
  226. package/lib/types/test/index.d.ts +0 -24
  227. package/lib/types/test/index.d.ts.map +0 -1
  228. package/lib/types/test/utils.d.ts +0 -5
  229. package/lib/types/test/utils.d.ts.map +0 -1
@@ -321,11 +321,17 @@ export class ClaudeModelDefinition implements ModelDefinition<ClaudePrompt> {
321
321
  driver.logger.warn({ options: options.model_options }, "Invalid model options");
322
322
  }
323
323
 
324
- const { payload, requestOptions } = getClaudePayload(options, prompt);
324
+ // Include conversation history (same as non-streaming)
325
+ const conversation = updateConversation(options.conversation as ClaudePrompt, prompt);
326
+
327
+ const { payload, requestOptions } = getClaudePayload(options, conversation);
325
328
  const streamingPayload: MessageStreamParams = { ...payload, stream: true };
326
329
 
327
330
  const response_stream = await client.messages.stream(streamingPayload, requestOptions);
328
331
 
332
+ // Track current tool use being built from streaming
333
+ let currentToolUse: { id: string; name: string; inputJson: string } | null = null;
334
+
329
335
  const stream = asyncMap(response_stream, async (streamEvent: RawMessageStreamEvent) => {
330
336
  switch (streamEvent.type) {
331
337
  case "message_start":
@@ -345,6 +351,22 @@ export class ClaudeModelDefinition implements ModelDefinition<ClaudePrompt> {
345
351
  finish_reason: claudeFinishReason(streamEvent.delta.stop_reason ?? undefined),
346
352
  } satisfies CompletionChunkObject;
347
353
  case "content_block_start":
354
+ // Handle tool_use blocks
355
+ if (streamEvent.content_block.type === "tool_use") {
356
+ currentToolUse = {
357
+ id: streamEvent.content_block.id,
358
+ name: streamEvent.content_block.name,
359
+ inputJson: ''
360
+ };
361
+ return {
362
+ result: [],
363
+ tool_use: [{
364
+ id: streamEvent.content_block.id,
365
+ tool_name: streamEvent.content_block.name,
366
+ tool_input: '' as any // Will be accumulated via input_json_delta
367
+ }]
368
+ } satisfies CompletionChunkObject;
369
+ }
348
370
  // Handle redacted thinking blocks
349
371
  if (streamEvent.content_block.type === "redacted_thinking" && model_options?.include_thoughts) {
350
372
  return {
@@ -359,6 +381,19 @@ export class ClaudeModelDefinition implements ModelDefinition<ClaudePrompt> {
359
381
  return {
360
382
  result: streamEvent.delta.text ? [{ type: "text", value: streamEvent.delta.text }] : []
361
383
  } satisfies CompletionChunkObject;
384
+ case "input_json_delta":
385
+ // Accumulate tool input JSON
386
+ if (currentToolUse && streamEvent.delta.partial_json) {
387
+ return {
388
+ result: [],
389
+ tool_use: [{
390
+ id: currentToolUse.id,
391
+ tool_name: '', // Name already sent in content_block_start
392
+ tool_input: streamEvent.delta.partial_json as any
393
+ }]
394
+ } satisfies CompletionChunkObject;
395
+ }
396
+ break;
362
397
  case "thinking_delta":
363
398
  if (model_options?.include_thoughts) {
364
399
  return {
@@ -377,6 +412,10 @@ export class ClaudeModelDefinition implements ModelDefinition<ClaudePrompt> {
377
412
  }
378
413
  break;
379
414
  case "content_block_stop":
415
+ // Reset current tool use tracking when block ends
416
+ if (currentToolUse) {
417
+ currentToolUse = null;
418
+ }
380
419
  // Handle the end of content blocks, for redacted thinking blocks
381
420
  if (model_options?.include_thoughts) {
382
421
  return {
@@ -406,6 +445,60 @@ function createPromptFromResponse(response: Message): ClaudePrompt {
406
445
  }
407
446
  }
408
447
 
448
+ /**
449
+ * Merge consecutive user messages in the conversation.
450
+ * This is required because Anthropic's API expects all tool_result blocks
451
+ * from a single assistant turn to be in one user message.
452
+ * When multiple tool results are added as separate user messages,
453
+ * we need to merge them before sending to the API.
454
+ */
455
+ export function mergeConsecutiveUserMessages(messages: MessageParam[]): MessageParam[] {
456
+ if (messages.length === 0) return [];
457
+
458
+ // Check if any merging is needed
459
+ const needsMerging = messages.some((msg, i) =>
460
+ i < messages.length - 1 &&
461
+ msg.role === 'user' &&
462
+ messages[i + 1].role === 'user'
463
+ );
464
+
465
+ if (!needsMerging) {
466
+ return messages;
467
+ }
468
+
469
+ const result: MessageParam[] = [];
470
+ let i = 0;
471
+
472
+ while (i < messages.length) {
473
+ const current = messages[i];
474
+
475
+ if (current.role === 'user') {
476
+ // Collect all consecutive user messages
477
+ const mergedContent: MessageParam['content'] = [];
478
+
479
+ while (i < messages.length && messages[i].role === 'user') {
480
+ const userMsg = messages[i];
481
+ if (Array.isArray(userMsg.content)) {
482
+ mergedContent.push(...userMsg.content);
483
+ } else if (typeof userMsg.content === 'string') {
484
+ mergedContent.push({ type: 'text', text: userMsg.content });
485
+ }
486
+ i++;
487
+ }
488
+
489
+ result.push({
490
+ role: 'user',
491
+ content: mergedContent
492
+ });
493
+ } else {
494
+ result.push(current);
495
+ i++;
496
+ }
497
+ }
498
+
499
+ return result;
500
+ }
501
+
409
502
  /**
410
503
  * Update the conversation messages
411
504
  * @param prompt
@@ -416,11 +509,143 @@ function updateConversation(conversation: ClaudePrompt | undefined | null, promp
416
509
  const baseSystemMessages = conversation?.system || [];
417
510
  const baseMessages = conversation?.messages || [];
418
511
  const system = baseSystemMessages.concat(prompt.system || []);
512
+ // Merge consecutive user messages to ensure tool_result blocks are properly grouped
513
+ const mergedMessages = mergeConsecutiveUserMessages(baseMessages.concat(prompt.messages || []));
419
514
  return {
420
- messages: baseMessages.concat(prompt.messages || []),
515
+ messages: mergedMessages,
421
516
  system: system.length > 0 ? system : undefined // If system is empty, set to undefined
422
517
  };
423
518
  }
519
+
520
+ /**
521
+ * Sanitize messages by removing empty text blocks.
522
+ * Claude API rejects messages with empty text content blocks ("text content blocks must be non-empty").
523
+ * This handles cases where streaming was interrupted and left empty text blocks.
524
+ *
525
+ * - Filters out empty text blocks from each message's content
526
+ * - Removes messages entirely if they have no content after filtering
527
+ */
528
+ function sanitizeMessages(messages: MessageParam[]): MessageParam[] {
529
+ const result: MessageParam[] = [];
530
+
531
+ for (const message of messages) {
532
+ if (typeof message.content === 'string') {
533
+ // String content - keep only if non-empty
534
+ if (message.content.trim()) {
535
+ result.push(message);
536
+ }
537
+ continue;
538
+ }
539
+
540
+ // Array content - filter out empty text blocks
541
+ const filteredContent = message.content.filter(block => {
542
+ if (block.type === 'text') {
543
+ return block.text && block.text.trim().length > 0;
544
+ }
545
+ // Keep all non-text blocks (tool_use, tool_result, image, etc.)
546
+ return true;
547
+ });
548
+
549
+ // Only include message if it has content after filtering
550
+ if (filteredContent.length > 0) {
551
+ result.push({
552
+ ...message,
553
+ content: filteredContent
554
+ });
555
+ }
556
+ }
557
+
558
+ return result;
559
+ }
560
+
561
+ /**
562
+ * Fix orphaned tool_use blocks in the conversation.
563
+ * @exported for testing
564
+ *
565
+ * When an agent is stopped mid-tool-execution, the assistant message contains tool_use blocks
566
+ * but no corresponding tool_result was added. The Anthropic API requires that every tool_use
567
+ * must be followed by a tool_result in the next user message.
568
+ *
569
+ * This function detects such cases and injects synthetic tool_result blocks indicating
570
+ * the tools were interrupted, allowing the conversation to continue.
571
+ */
572
+ export function fixOrphanedToolUse(messages: MessageParam[]): MessageParam[] {
573
+ if (messages.length < 2) return messages;
574
+
575
+ const result: MessageParam[] = [];
576
+
577
+ for (let i = 0; i < messages.length; i++) {
578
+ const current = messages[i];
579
+ result.push(current);
580
+
581
+ // Check if this is an assistant message with tool_use blocks
582
+ if (current.role === 'assistant' && Array.isArray(current.content)) {
583
+ const toolUseBlocks = current.content.filter(
584
+ (block): block is ContentBlockParam & { type: 'tool_use'; id: string; name: string } =>
585
+ block.type === 'tool_use'
586
+ );
587
+
588
+ if (toolUseBlocks.length > 0) {
589
+ // Check if the next message is a user message with matching tool_results
590
+ const nextMessage = messages[i + 1];
591
+
592
+ if (nextMessage && nextMessage.role === 'user' && Array.isArray(nextMessage.content)) {
593
+ // Get tool_result IDs from the next message
594
+ const toolResultIds = new Set(
595
+ nextMessage.content
596
+ .filter((block): block is ToolResultBlockParam => block.type === 'tool_result')
597
+ .map(block => block.tool_use_id)
598
+ );
599
+
600
+ // Find orphaned tool_use blocks (no matching tool_result)
601
+ const orphanedToolUse = toolUseBlocks.filter(block => !toolResultIds.has(block.id));
602
+
603
+ if (orphanedToolUse.length > 0) {
604
+ // Inject synthetic tool_results for orphaned tool_use
605
+ const syntheticResults: ToolResultBlockParam[] = orphanedToolUse.map(block => ({
606
+ type: 'tool_result',
607
+ tool_use_id: block.id,
608
+ content: `[Tool interrupted: The user stopped the operation before "${block.name}" could execute.]`
609
+ }));
610
+
611
+ // Prepend synthetic results to the next user message
612
+ const updatedNextMessage: MessageParam = {
613
+ ...nextMessage,
614
+ content: [...syntheticResults, ...nextMessage.content]
615
+ };
616
+
617
+ // Replace the next message in our iteration
618
+ messages[i + 1] = updatedNextMessage;
619
+ }
620
+ } else if (nextMessage && nextMessage.role === 'user') {
621
+ // Next message is a user message but not array content (plain text)
622
+ // We need to convert it and add tool_results
623
+ const syntheticResults: ToolResultBlockParam[] = toolUseBlocks.map(block => ({
624
+ type: 'tool_result',
625
+ tool_use_id: block.id,
626
+ content: `[Tool interrupted: The user stopped the operation before "${block.name}" could execute.]`
627
+ }));
628
+
629
+ const textContent: TextBlockParam = typeof nextMessage.content === 'string'
630
+ ? { type: 'text', text: nextMessage.content }
631
+ : { type: 'text', text: '' };
632
+
633
+ const updatedNextMessage: MessageParam = {
634
+ role: 'user',
635
+ content: [...syntheticResults, textContent]
636
+ };
637
+
638
+ messages[i + 1] = updatedNextMessage;
639
+ }
640
+ // Note: If there's no nextMessage, we leave the conversation as-is.
641
+ // The tool_use blocks are expected to be there - the next turn will provide tool_results.
642
+ }
643
+ }
644
+ }
645
+
646
+ return result;
647
+ }
648
+
424
649
  interface RequestOptions {
425
650
  headers?: Record<string, string>;
426
651
  }
@@ -440,8 +665,13 @@ function getClaudePayload(options: ExecutionOptions, prompt: ClaudePrompt): { pa
440
665
  };
441
666
  }
442
667
 
668
+ // Fix orphaned tool_use blocks (can occur when agent is stopped mid-tool-execution)
669
+ const fixedMessages = fixOrphanedToolUse(prompt.messages);
670
+ // Sanitize messages to remove empty text blocks (can occur from interrupted streaming)
671
+ const sanitizedMessages = sanitizeMessages(fixedMessages);
672
+
443
673
  const payload = {
444
- messages: prompt.messages,
674
+ messages: sanitizedMessages,
445
675
  system: prompt.system,
446
676
  tools: options.tools, // we are using the same shape as claude for tools
447
677
  temperature: model_options?.temperature,
@@ -1,16 +1,24 @@
1
1
  import {
2
2
  Content, FinishReason, FunctionCallingConfigMode, FunctionDeclaration, GenerateContentConfig, GenerateContentParameters,
3
3
  GenerateContentResponseUsageMetadata,
4
- HarmBlockThreshold, HarmCategory, Modality, Part, SafetySetting, Schema, Tool, Type
4
+ HarmBlockThreshold, HarmCategory, Modality, Part, SafetySetting, Schema, ThinkingConfig, Tool, Type
5
5
  } from "@google/genai";
6
6
  import {
7
7
  AIModel, Completion, CompletionChunkObject, CompletionResult, ExecutionOptions,
8
- ExecutionTokenUsage, getMaxTokensLimitVertexAi, JSONObject, JSONSchema, ModelType, PromptOptions, PromptRole,
9
- PromptSegment, readStreamAsBase64, StatelessExecutionOptions, ToolDefinition, ToolUse,
8
+ ExecutionTokenUsage,
9
+ getConversationMeta,
10
+ getMaxTokensLimitVertexAi,
11
+ incrementConversationTurn,
12
+ JSONObject, JSONSchema, ModelType, PromptOptions, PromptRole,
13
+ PromptSegment, readStreamAsBase64, StatelessExecutionOptions,
14
+ stripBase64ImagesFromConversation,
15
+ ToolDefinition, ToolUse,
16
+ truncateLargeTextInConversation,
17
+ unwrapConversationArray,
10
18
  VertexAIGeminiOptions
11
19
  } from "@llumiverse/core";
12
20
  import { asyncMap } from "@llumiverse/core/async";
13
- import { VertexAIDriver, GenerateContentPrompt } from "../index.js";
21
+ import { GenerateContentPrompt, VertexAIDriver } from "../index.js";
14
22
  import { ModelDefinition } from "../models.js";
15
23
 
16
24
  function supportsStructuredOutput(options: PromptOptions): boolean {
@@ -92,11 +100,7 @@ function getGeminiPayload(options: ExecutionOptions, prompt: GenerateContentProm
92
100
  presencePenalty: model_options?.presence_penalty,
93
101
  frequencyPenalty: model_options?.frequency_penalty,
94
102
  seed: model_options?.seed,
95
- thinkingConfig: thinkingConfigNeeded ?
96
- {
97
- includeThoughts: model_options?.include_thoughts ?? false,
98
- thinkingBudget: geminiThinkingBudget(options),
99
- } : undefined,
103
+ thinkingConfig: thinkingConfigNeeded ? geminiThinkingConfig(options) : undefined,
100
104
  }
101
105
 
102
106
  return {
@@ -471,11 +475,17 @@ function collectToolUseParts(content: Content): ToolUse[] | undefined {
471
475
  const parts = content.parts ?? [];
472
476
  for (const part of parts) {
473
477
  if (part.functionCall) {
474
- out.push({
478
+ const toolUse: ToolUse = {
475
479
  id: part.functionCall.name ?? '',
476
480
  tool_name: part.functionCall.name ?? '',
477
481
  tool_input: part.functionCall.args as JSONObject,
478
- });
482
+ };
483
+ // Capture thought_signature for Gemini thinking models (2.5+/3.0+)
484
+ // This must be passed back with the function response
485
+ if (part.thoughtSignature) {
486
+ toolUse.thought_signature = part.thoughtSignature;
487
+ }
488
+ out.push(toolUse);
479
489
  }
480
490
  }
481
491
  return out.length > 0 ? out : undefined;
@@ -545,6 +555,21 @@ function geminiThinkingBudget(option: StatelessExecutionOptions) {
545
555
  return undefined;
546
556
  }
547
557
 
558
+ function geminiThinkingConfig(option: StatelessExecutionOptions): ThinkingConfig | undefined {
559
+ const model_options = option.model_options as VertexAIGeminiOptions | undefined;
560
+ const include_thoughts = model_options?.include_thoughts ?? false;
561
+ if (model_options?.thinking_budget_tokens) {
562
+ return { includeThoughts: include_thoughts, thinkingBudget: model_options.thinking_budget_tokens };
563
+ }
564
+
565
+ // Set minimum thinking level by default.
566
+ // Docs: https://ai.google.dev/gemini-api/docs/thinking#set-budget
567
+ if (option.model.includes("gemini-2.5") || option.model.includes("gemini-3")) {
568
+ const thinking_budget_tokens = geminiThinkingBudget(option) ?? 0;
569
+ return { includeThoughts: include_thoughts, thinkingBudget: thinking_budget_tokens };
570
+ }
571
+ }
572
+
548
573
  export class GeminiModelDefinition implements ModelDefinition<GenerateContentPrompt> {
549
574
 
550
575
  model: AIModel
@@ -612,16 +637,18 @@ export class GeminiModelDefinition implements ModelDefinition<GenerateContentPro
612
637
  if (!msg.tool_use_id) {
613
638
  throw new Error("Tool response missing tool_use_id");
614
639
  }
640
+ // Build functionResponse part with optional thought_signature for Gemini thinking models
641
+ const functionResponsePart: Part = {
642
+ functionResponse: {
643
+ name: msg.tool_use_id,
644
+ response: formatFunctionResponse(msg.content || ''),
645
+ },
646
+ // Include thought_signature if provided (required for Gemini 2.5+/3.0+ thinking models)
647
+ thoughtSignature: msg.thought_signature,
648
+ };
615
649
  contents.push({
616
650
  role: 'user',
617
- parts: [
618
- {
619
- functionResponse: {
620
- name: msg.tool_use_id,
621
- response: formatFunctionResponse(msg.content || ''),
622
- }
623
- }
624
- ]
651
+ parts: [functionResponsePart]
625
652
  });
626
653
  } else { // PromptRole.user, PromptRole.assistant, PromptRole.safety
627
654
  const parts: Part[] = [];
@@ -635,14 +662,27 @@ export class GeminiModelDefinition implements ModelDefinition<GenerateContentPro
635
662
  // File content handling
636
663
  if (msg.files) {
637
664
  for (const f of msg.files) {
638
- const stream = await f.getStream();
639
- const data = await readStreamAsBase64(stream);
640
- parts.push({
641
- inlineData: {
642
- data,
643
- mimeType: f.mime_type
644
- }
645
- });
665
+ let fileUrl = await f.getURL();
666
+ const isGsUrl = fileUrl.startsWith('gs://') || fileUrl.startsWith('https://storage.googleapis.com/');
667
+
668
+ if (isGsUrl) {
669
+ parts.push({
670
+ fileData: {
671
+ fileUri: fileUrl,
672
+ mimeType: f.mime_type
673
+ }
674
+ });
675
+ } else {
676
+ // Inline data handling
677
+ const stream = await f.getStream();
678
+ const data = await readStreamAsBase64(stream);
679
+ parts.push({
680
+ inlineData: {
681
+ data,
682
+ mimeType: f.mime_type
683
+ }
684
+ });
685
+ }
646
686
  }
647
687
  }
648
688
 
@@ -731,9 +771,10 @@ export class GeminiModelDefinition implements ModelDefinition<GenerateContentPro
731
771
  const modelName = splits[splits.length - 1];
732
772
  options = { ...options, model: modelName };
733
773
 
734
- let conversation = updateConversation(options.conversation as Content[], prompt.contents);
774
+ let conversation = updateConversation(options.conversation, prompt.contents);
735
775
  prompt.contents = conversation;
736
776
 
777
+ // TODO: Remove hack, use global endpoint manually if needed.
737
778
  if (options.model.includes("gemini-2.5-flash-image")) {
738
779
  region = "global"; // Gemini Flash Image only available in global region, this is for nano-banana model
739
780
  }
@@ -780,12 +821,27 @@ export class GeminiModelDefinition implements ModelDefinition<GenerateContentPro
780
821
  finish_reason = "tool_use";
781
822
  }
782
823
 
824
+ // Increment turn counter for deferred stripping
825
+ conversation = incrementConversationTurn(conversation) as Content[];
826
+
827
+ // Strip large base64 image data based on options.stripImagesAfterTurns
828
+ const currentTurn = getConversationMeta(conversation).turnNumber;
829
+ const stripOptions = {
830
+ keepForTurns: options.stripImagesAfterTurns ?? Infinity,
831
+ currentTurn,
832
+ textMaxTokens: options.stripTextMaxTokens
833
+ };
834
+ let processedConversation = stripBase64ImagesFromConversation(conversation, stripOptions);
835
+
836
+ // Truncate large text content if configured
837
+ processedConversation = truncateLargeTextInConversation(processedConversation, stripOptions);
838
+
783
839
  return {
784
840
  result: result && result.length > 0 ? result : [{ type: "text" as const, value: '' }],
785
841
  token_usage: token_usage,
786
842
  finish_reason: finish_reason,
787
843
  original_response: options.include_original_response ? response : undefined,
788
- conversation,
844
+ conversation: processedConversation,
789
845
  tool_use
790
846
  } satisfies Completion;
791
847
  }
@@ -799,6 +855,10 @@ export class GeminiModelDefinition implements ModelDefinition<GenerateContentPro
799
855
  const modelName = splits[splits.length - 1];
800
856
  options = { ...options, model: modelName };
801
857
 
858
+ // Include conversation history in prompt contents (same as non-streaming)
859
+ const conversation = updateConversation(options.conversation, prompt.contents);
860
+ prompt.contents = conversation;
861
+
802
862
  if (options.model.includes("gemini-2.5-flash-image")) {
803
863
  region = "global"; // Gemini Flash Image only available in global region, this is for nano-banana model
804
864
  }
@@ -885,16 +945,19 @@ function getToolFunction(tool: ToolDefinition): FunctionDeclaration {
885
945
  };
886
946
  }
887
947
 
888
-
889
948
  /**
890
949
  * Update the conversation messages
891
950
  * @param prompt
892
951
  * @param response
893
952
  * @returns
894
953
  */
895
- function updateConversation(conversation: Content[], prompt: Content[]): Content[] {
896
- return (conversation || [] satisfies Content[]).concat(prompt);
954
+ function updateConversation(conversation: unknown, prompt: Content[]): Content[] {
955
+ // Unwrap array if wrapped, otherwise treat as array
956
+ const unwrapped = unwrapConversationArray<Content>(conversation);
957
+ const convArray = unwrapped ?? (conversation as Content[] || []);
958
+ return convArray.concat(prompt);
897
959
  }
960
+
898
961
  /**
899
962
  *
900
963
  * Gemini supports JSON output in the response. so we test if the response is a valid JSON object. otherwise we treat the response as a string.
@@ -1,5 +1,5 @@
1
1
  import {
2
- AIModel, Completion, ExecutionOptions, Modalities,
2
+ AIModel, Completion, ExecutionOptions,
3
3
  ModelType, PromptRole, PromptSegment, readStreamAsBase64, ImagenOptions
4
4
  } from "@llumiverse/core";
5
5
  import { VertexAIDriver } from "../index.js";
@@ -328,10 +328,6 @@ export class ImagenModelDefinition {
328
328
  }
329
329
  options.model_options = options.model_options as ImagenOptions | undefined;
330
330
 
331
- if (options.output_modality !== Modalities.image) {
332
- throw new Error(`Image generation requires image output_modality`);
333
- }
334
-
335
331
  const taskType: string = options.model_options?.edit_mode ?? ImagenTaskType.TEXT_IMAGE;
336
332
 
337
333
  driver.logger.info("Task type: " + taskType);
package/src/xai/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { AIModel, Completion, DriverOptions, ExecutionOptions, PromptOptions, PromptSegment } from "@llumiverse/core";
1
+ import { AIModel, DriverOptions, PromptOptions, PromptSegment, Providers } from "@llumiverse/core";
2
2
  import { formatOpenAILikeMultimodalPrompt, OpenAIPromptFormatterOptions } from "../openai/openai_format.js";
3
3
  import { FetchClient } from "@vertesia/api-fetch-client";
4
4
  import OpenAI from "openai";
@@ -15,7 +15,7 @@ export interface xAiDriverOptions extends DriverOptions {
15
15
  export class xAIDriver extends BaseOpenAIDriver {
16
16
 
17
17
  service: OpenAI;
18
- provider: "xai";
18
+ readonly provider = Providers.xai;
19
19
  xai_service: FetchClient;
20
20
  DEFAULT_ENDPOINT = "https://api.x.ai/v1";
21
21
 
@@ -31,7 +31,6 @@ export class xAIDriver extends BaseOpenAIDriver {
31
31
  baseURL: opts.endpoint ?? this.DEFAULT_ENDPOINT,
32
32
  });
33
33
  this.xai_service = new FetchClient(opts.endpoint ?? this.DEFAULT_ENDPOINT).withAuthCallback(async () => `Bearer ${opts.apiKey}`);
34
- this.provider = "xai";
35
34
  //this.formatPrompt = this._formatPrompt; //TODO: fix xai prompt formatting
36
35
  }
37
36
 
@@ -49,17 +48,9 @@ export class xAIDriver extends BaseOpenAIDriver {
49
48
 
50
49
  }
51
50
 
52
- extractDataFromResponse(_options: ExecutionOptions, result: OpenAI.Chat.Completions.ChatCompletion): Completion {
53
- return {
54
- result: result.choices[0].message.content ? [{ type: "text", value: result.choices[0].message.content }] : [],
55
- finish_reason: result.choices[0].finish_reason,
56
- token_usage: {
57
- prompt: result.usage?.prompt_tokens,
58
- result: result.usage?.completion_tokens,
59
- total: result.usage?.total_tokens,
60
- }
61
- }
62
- }
51
+ // Note: We intentionally do NOT override extractDataFromResponse here.
52
+ // The base class implementation properly handles tool_calls extraction.
53
+ // xAI's API is OpenAI-compatible and returns tool_calls in the same format.
63
54
 
64
55
  async listModels(): Promise<AIModel[]> {
65
56
  const [lm, em] = await Promise.all([
@@ -76,10 +67,12 @@ export class xAIDriver extends BaseOpenAIDriver {
76
67
  return {
77
68
  id: model.id,
78
69
  provider: this.provider,
79
- name: model.object,
80
- description: model.object,
70
+ name: model.id,
71
+ description: `${model.id} by ${model.owned_by}`,
81
72
  is_multimodal: model.input_modalities.length > 1,
82
- tags: [...model.input_modalities.map(m => `ì:${m}`), ...model.output_modalities.map(m => `ì:${m}`)],
73
+ input_modalities: model.input_modalities,
74
+ output_modalities: model.output_modalities,
75
+ tags: [...model.input_modalities.map(m => `i:${m}`), ...model.output_modalities.map(m => `o:${m}`)],
83
76
  } satisfies AIModel;
84
77
  });
85
78
 
@@ -1,20 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.TestErrorCompletionStream = void 0;
4
- const utils_js_1 = require("./utils.js");
5
- class TestErrorCompletionStream {
6
- segments;
7
- options;
8
- completion;
9
- constructor(segments, options) {
10
- this.segments = segments;
11
- this.options = options;
12
- }
13
- async *[Symbol.asyncIterator]() {
14
- yield "Started TestError. Next we will thrown an error.\n";
15
- (0, utils_js_1.sleep)(1000);
16
- (0, utils_js_1.throwError)("Testing stream completion error.", this.segments);
17
- }
18
- }
19
- exports.TestErrorCompletionStream = TestErrorCompletionStream;
20
- //# sourceMappingURL=TestErrorCompletionStream.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"TestErrorCompletionStream.js","sourceRoot":"","sources":["../../../src/test/TestErrorCompletionStream.ts"],"names":[],"mappings":";;;AACA,yCAA+C;AAE/C,MAAa,yBAAyB;IAIf;IACR;IAHX,UAAU,CAAiD;IAE3D,YAAmB,QAAyB,EACjC,OAAyB;QADjB,aAAQ,GAAR,QAAQ,CAAiB;QACjC,YAAO,GAAP,OAAO,CAAkB;IACpC,CAAC;IACD,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC;QACzB,MAAM,oDAAoD,CAAC;QAC3D,IAAA,gBAAK,EAAC,IAAI,CAAC,CAAC;QACZ,IAAA,qBAAU,EAAC,kCAAkC,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IAClE,CAAC;CACJ;AAZD,8DAYC"}
@@ -1,24 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.TestValidationErrorCompletionStream = void 0;
4
- const utils_js_1 = require("./utils.js");
5
- class TestValidationErrorCompletionStream {
6
- segments;
7
- options;
8
- completion;
9
- constructor(segments, options) {
10
- this.segments = segments;
11
- this.options = options;
12
- }
13
- async *[Symbol.asyncIterator]() {
14
- yield "Started TestValidationError.\n";
15
- await (0, utils_js_1.sleep)(1000);
16
- yield "chunk1\n";
17
- await (0, utils_js_1.sleep)(1000);
18
- yield "chunk2\n";
19
- await (0, utils_js_1.sleep)(1000);
20
- this.completion = (0, utils_js_1.createValidationErrorCompletion)(this.segments);
21
- }
22
- }
23
- exports.TestValidationErrorCompletionStream = TestValidationErrorCompletionStream;
24
- //# sourceMappingURL=TestValidationErrorCompletionStream.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"TestValidationErrorCompletionStream.js","sourceRoot":"","sources":["../../../src/test/TestValidationErrorCompletionStream.ts"],"names":[],"mappings":";;;AACA,yCAAoE;AAGpE,MAAa,mCAAmC;IAIzB;IACR;IAHX,UAAU,CAAiD;IAE3D,YAAmB,QAAyB,EACjC,OAAyB;QADjB,aAAQ,GAAR,QAAQ,CAAiB;QACjC,YAAO,GAAP,OAAO,CAAkB;IACpC,CAAC;IACD,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC;QACzB,MAAM,gCAAgC,CAAC;QACvC,MAAM,IAAA,gBAAK,EAAC,IAAI,CAAC,CAAC;QAClB,MAAM,UAAU,CAAC;QACjB,MAAM,IAAA,gBAAK,EAAC,IAAI,CAAC,CAAC;QAClB,MAAM,UAAU,CAAC;QACjB,MAAM,IAAA,gBAAK,EAAC,IAAI,CAAC,CAAC;QAClB,IAAI,CAAC,UAAU,GAAG,IAAA,0CAA+B,EAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACrE,CAAC;CACJ;AAhBD,kFAgBC"}