@llumiverse/drivers 0.19.0 → 0.20.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 (49) hide show
  1. package/lib/cjs/bedrock/converse.js +181 -123
  2. package/lib/cjs/bedrock/converse.js.map +1 -1
  3. package/lib/cjs/bedrock/index.js +152 -70
  4. package/lib/cjs/bedrock/index.js.map +1 -1
  5. package/lib/cjs/vertexai/index.js +1 -0
  6. package/lib/cjs/vertexai/index.js.map +1 -1
  7. package/lib/cjs/vertexai/models/claude.js +227 -118
  8. package/lib/cjs/vertexai/models/claude.js.map +1 -1
  9. package/lib/cjs/vertexai/models/gemini.js +110 -70
  10. package/lib/cjs/vertexai/models/gemini.js.map +1 -1
  11. package/lib/cjs/vertexai/models/imagen.js +2 -2
  12. package/lib/cjs/vertexai/models/imagen.js.map +1 -1
  13. package/lib/cjs/watsonx/index.js +10 -10
  14. package/lib/cjs/watsonx/index.js.map +1 -1
  15. package/lib/esm/bedrock/converse.js +180 -122
  16. package/lib/esm/bedrock/converse.js.map +1 -1
  17. package/lib/esm/bedrock/index.js +153 -71
  18. package/lib/esm/bedrock/index.js.map +1 -1
  19. package/lib/esm/vertexai/index.js +1 -0
  20. package/lib/esm/vertexai/index.js.map +1 -1
  21. package/lib/esm/vertexai/models/claude.js +228 -119
  22. package/lib/esm/vertexai/models/claude.js.map +1 -1
  23. package/lib/esm/vertexai/models/gemini.js +109 -70
  24. package/lib/esm/vertexai/models/gemini.js.map +1 -1
  25. package/lib/esm/vertexai/models/imagen.js +2 -2
  26. package/lib/esm/vertexai/models/imagen.js.map +1 -1
  27. package/lib/esm/watsonx/index.js +10 -10
  28. package/lib/esm/watsonx/index.js.map +1 -1
  29. package/lib/types/bedrock/converse.d.ts +2 -2
  30. package/lib/types/bedrock/converse.d.ts.map +1 -1
  31. package/lib/types/bedrock/index.d.ts +5 -5
  32. package/lib/types/bedrock/index.d.ts.map +1 -1
  33. package/lib/types/vertexai/index.d.ts +2 -3
  34. package/lib/types/vertexai/index.d.ts.map +1 -1
  35. package/lib/types/vertexai/models/claude.d.ts +5 -7
  36. package/lib/types/vertexai/models/claude.d.ts.map +1 -1
  37. package/lib/types/vertexai/models/gemini.d.ts +4 -2
  38. package/lib/types/vertexai/models/gemini.d.ts.map +1 -1
  39. package/lib/types/vertexai/models.d.ts +2 -2
  40. package/lib/types/vertexai/models.d.ts.map +1 -1
  41. package/package.json +12 -12
  42. package/src/bedrock/converse.ts +194 -129
  43. package/src/bedrock/index.ts +177 -82
  44. package/src/vertexai/index.ts +3 -3
  45. package/src/vertexai/models/claude.ts +268 -138
  46. package/src/vertexai/models/gemini.ts +120 -77
  47. package/src/vertexai/models/imagen.ts +3 -3
  48. package/src/vertexai/models.ts +2 -2
  49. package/src/watsonx/index.ts +12 -12
@@ -44,12 +44,10 @@ const geminiSafetySettings: SafetySetting[] = [
44
44
  ];
45
45
 
46
46
  function getGeminiPayload(options: ExecutionOptions, prompt: GenerateContentPrompt): GenerateContentParameters {
47
- //1.0 Ultra does not support JSON output, 1.0 Pro does.
48
- const useStructuredOutput = supportsStructuredOutput(options);
49
-
50
47
  const model_options = options.model_options as any;
51
48
  const tools = getToolDefinitions(options.tools);
52
- prompt.tools = tools ? [tools] : undefined;
49
+
50
+ const useStructuredOutput = supportsStructuredOutput(options) && !tools;
53
51
 
54
52
  return {
55
53
  model: options.model,
@@ -65,7 +63,7 @@ function getGeminiPayload(options: ExecutionOptions, prompt: GenerateContentProm
65
63
  } : undefined,
66
64
  candidateCount: 1,
67
65
  //JSON/Structured output
68
- responseMimeType: useStructuredOutput ? "application/json" : "text/plain",
66
+ responseMimeType: useStructuredOutput ? "application/json" : undefined,
69
67
  responseSchema: useStructuredOutput ? parseJSONtoSchema(options.result_schema, true) : undefined,
70
68
  //Model options
71
69
  temperature: model_options?.temperature,
@@ -76,10 +74,10 @@ function getGeminiPayload(options: ExecutionOptions, prompt: GenerateContentProm
76
74
  presencePenalty: model_options?.presence_penalty,
77
75
  frequencyPenalty: model_options?.frequency_penalty,
78
76
  seed: model_options?.seed,
79
- thinkingConfig: model_options?.include_thoughts || model_options?.thinking_budget ?
77
+ thinkingConfig: model_options?.include_thoughts || model_options?.thinking_budget_tokens ?
80
78
  {
81
79
  includeThoughts: model_options?.include_thoughts,
82
- thinkingBudget: model_options?.thinking_budget,
80
+ thinkingBudget: model_options?.thinking_budget_tokens,
83
81
  } : undefined,
84
82
  }
85
83
  };
@@ -133,8 +131,8 @@ function convertType(type?: string | string[]): Type | undefined {
133
131
  */
134
132
  function convertSchema(jsSchema?: JSONSchema, depth: number = 0, requiredAll = false): Schema {
135
133
  // Prevent circular references
136
- if (depth > 50) {
137
- throw new Error("Maximum schema depth exceeded. Possible circular reference detected.");
134
+ if (depth > 20) {
135
+ throw new Error("Maximum schema depth (20) exceeded. Possible circular reference detected.");
138
136
  }
139
137
 
140
138
  if (!jsSchema) return {};
@@ -438,6 +436,35 @@ function collectToolUseParts(content: Content): ToolUse[] | undefined {
438
436
  return out.length > 0 ? out : undefined;
439
437
  }
440
438
 
439
+ export function mergeConsecutiveRole(contents: Content[] | undefined): Content[] {
440
+ if (!contents || contents.length === 0) return [];
441
+
442
+ const needsMerging = contents.some((content, i) =>
443
+ i < contents.length - 1 && content.role === contents[i + 1].role
444
+ );
445
+ // If no merging needed, return original array
446
+ if (!needsMerging) {
447
+ return contents;
448
+ }
449
+
450
+ const result: Content[] = [];
451
+ let currentContent = { ...contents[0], parts: [...(contents[0].parts || [])] };
452
+
453
+ for (let i = 1; i < contents.length; i++) {
454
+ if (currentContent.role === contents[i].role) {
455
+ // Same role - concatenate parts (without merging individual parts)
456
+ currentContent.parts = (currentContent.parts || []).concat(...(contents[i].parts || []));
457
+ } else {
458
+ // Different role - push current and start new
459
+ result.push(currentContent);
460
+ currentContent = { ...contents[i], parts: [...(contents[i].parts || [])] };
461
+ }
462
+ }
463
+
464
+ result.push(currentContent);
465
+ return result;
466
+ }
467
+
441
468
  export class GeminiModelDefinition implements ModelDefinition<GenerateContentPrompt> {
442
469
 
443
470
  model: AIModel
@@ -448,13 +475,13 @@ export class GeminiModelDefinition implements ModelDefinition<GenerateContentPro
448
475
  name: modelId,
449
476
  provider: 'vertexai',
450
477
  type: ModelType.Text,
451
- can_stream: true,
478
+ can_stream: true
452
479
  } satisfies AIModel;
453
480
  }
454
481
 
455
482
  preValidationProcessing(result: Completion, options: ExecutionOptions): { result: Completion, options: ExecutionOptions } {
456
- // If there's no schema or result, no processing needed
457
- if (!options.result_schema || !result.result) {
483
+ // Guard clause, if no result_schema, error, or tool use, skip processing
484
+ if (!options.result_schema || !result.result || result.tool_use || result.error) {
458
485
  return { result, options };
459
486
  }
460
487
  try {
@@ -463,106 +490,122 @@ export class GeminiModelDefinition implements ModelDefinition<GenerateContentPro
463
490
  return { result, options };
464
491
  } catch (error) {
465
492
  // Log error during processing but don't fail the completion
466
- console.warn('Error during Gemini JSON pre-validation', error);
493
+ console.warn('Error during Gemini JSON pre-validation: ', error);
467
494
  // Return original result if cleanup fails
468
495
  return { result, options };
469
496
  }
470
497
  }
471
498
 
472
- async createPrompt(_driver: VertexAIDriver, segments: PromptSegment[], options: PromptOptions): Promise<GenerateContentPrompt> {
499
+ async createPrompt(_driver: VertexAIDriver, segments: PromptSegment[], options: ExecutionOptions): Promise<GenerateContentPrompt> {
473
500
  const splits = options.model.split("/");
474
501
  const modelName = splits[splits.length - 1];
475
502
  options = { ...options, model: modelName };
476
503
 
477
504
  const schema = options.result_schema;
478
- const contents: Content[] = [];
479
- const safety: string[] = [];
480
- const system: string[] = [];
481
-
482
- let lastUserContent: Content | undefined = undefined;
483
- const toolParts = [];
505
+ let contents: Content[] = [];
506
+ let system: Content | undefined = { role: "user", parts: [] }; // Single content block for system messages
507
+
508
+ const safety: Content[] = [];
484
509
 
485
510
  for (const msg of segments) {
511
+ // Role specific handling
512
+ if (msg.role === PromptRole.system) {
513
+ // Text only for system messages
514
+ if (msg.files && msg.files.length > 0) {
515
+ throw new Error("Gemini does not support files/images etc. in system messages. Only text content is allowed.");
516
+ }
486
517
 
487
- if (msg.role === PromptRole.safety) {
488
- safety.push(msg.content);
489
- } else if (msg.role === PromptRole.system) {
490
- system.push(msg.content);
491
- } else {
492
- const fileParts: Part[] = [];
518
+ if (msg.content) {
519
+ system.parts?.push({
520
+ text: msg.content
521
+ });
522
+ }
523
+ } else if (msg.role === PromptRole.tool) {
524
+ if (!msg.tool_use_id) {
525
+ throw new Error("Tool response missing tool_use_id");
526
+ }
527
+ contents.push({
528
+ role: 'user',
529
+ parts: [
530
+ {
531
+ functionResponse: {
532
+ name: msg.tool_use_id,
533
+ response: formatFunctionResponse(msg.content || ''),
534
+ }
535
+ }
536
+ ]
537
+ });
538
+ } else { // PromptRole.user, PromptRole.assistant, PromptRole.safety
539
+ const parts: Part[] = [];
540
+ // Text content handling
541
+ if (msg.content) {
542
+ parts.push({
543
+ text: msg.content,
544
+ });
545
+ }
546
+
547
+ // File content handling
493
548
  if (msg.files) {
494
549
  for (const f of msg.files) {
495
550
  const stream = await f.getStream();
496
551
  const data = await readStreamAsBase64(stream);
497
- fileParts.push({
552
+ parts.push({
498
553
  inlineData: {
499
554
  data,
500
- mimeType: f.mime_type!
555
+ mimeType: f.mime_type
501
556
  }
502
557
  });
503
558
  }
504
559
  }
505
560
 
506
- if (msg.role === PromptRole.tool) {
507
- toolParts.push({
508
- functionResponse: {
509
- name: msg.tool_use_id!,
510
- response: formatFunctionResponse(msg.content || ''),
511
- }
512
- });
513
- continue;
514
- }
515
-
516
- const role = msg.role === PromptRole.assistant ? "model" : "user";
517
-
518
- if (lastUserContent && lastUserContent.role === role) {
519
- lastUserContent?.parts?.push({ text: msg.content });
520
- fileParts?.forEach(p => lastUserContent?.parts?.push(p));
521
- } else {
522
- const content: Content = {
523
- role,
524
- parts: [{ text: msg.content }],
525
- }
526
- fileParts?.forEach(p => content?.parts?.push(p));
527
-
528
- if (role === 'user') {
529
- lastUserContent = content;
561
+ if (parts.length > 0) {
562
+ if (msg.role === PromptRole.safety) {
563
+ safety.push({
564
+ role: 'user',
565
+ parts,
566
+ });
567
+ } else {
568
+ contents.push({
569
+ role: msg.role === PromptRole.assistant ? 'model' : 'user',
570
+ parts,
571
+ });
530
572
  }
531
- contents.push(content);
532
573
  }
533
574
  }
534
575
  }
535
576
 
536
- if (schema && !supportsStructuredOutput(options)) {
537
- // Fallback to putting the schema in the prompt, if not using structured output.
538
- safety.push("The answer must be a JSON object using the following JSON Schema:\n" + JSON.stringify(schema));
539
- } else if (schema) {
540
- // Gemini structured output is unnecessarily sparse. Adding encouragement to fill the fields.
541
- // Putting JSON in prompt is not recommended by Google, when using structured output.
542
- safety.push("Fill all appropriate fields in the JSON output.");
543
- }
544
-
545
- if (safety.length > 0) {
546
- const content = safety.join('\n');
547
- if (lastUserContent) {
548
- lastUserContent?.parts?.push({ text: content });
577
+ // Adding JSON Schema to system message
578
+ if (schema) {
579
+ if (supportsStructuredOutput(options) && !options.tools) {
580
+ // Gemini structured output is unnecessarily sparse. Adding encouragement to fill the fields.
581
+ // Putting JSON in prompt is not recommended by Google, when using structured output.
582
+ system.parts?.push({ text: "Fill all appropriate fields in the JSON output." });
549
583
  } else {
550
- contents.push({
551
- role: 'user',
552
- parts: [{ text: content }],
553
- })
584
+ // Fallback to putting the schema in the system instructions, if not using structured output.
585
+ if (options.tools) {
586
+ system.parts?.push({
587
+ text: "When not calling tools, the output must be a JSON object using the following JSON Schema:\n" + JSON.stringify(schema)
588
+ });
589
+ } else {
590
+ system.parts?.push({ text: "The output must be a JSON object using the following JSON Schema:\n" + JSON.stringify(schema) });
591
+ }
554
592
  }
555
593
  }
594
+
595
+ // If no system messages, set system to undefined.
596
+ if (!system.parts || system.parts.length === 0) {
597
+ system = undefined;
598
+ }
556
599
 
557
- if (toolParts.length > 0) {
558
- contents.push({
559
- role: 'user',
560
- parts: toolParts,
561
- });
600
+ // Add safety messages to the end of contents. They are in effect user messages that come at the end.
601
+ if (safety.length > 0) {
602
+ contents = contents.concat(safety);
562
603
  }
563
604
 
564
- // put system messages first and safety last
565
- return { contents, system: system.join('\n'), tools: undefined };
605
+ // Merge consecutive messages with the same role. Note: this may not be necessary, works without it, keeping to match previous behavior.
606
+ contents = mergeConsecutiveRole(contents);
607
+
608
+ return { contents, system };
566
609
  }
567
610
 
568
611
  async requestTextCompletion(driver: VertexAIDriver, prompt: GenerateContentPrompt, options: ExecutionOptions): Promise<Completion> {
@@ -341,13 +341,13 @@ export class ImagenModelDefinition {
341
341
  if (options.model_options?._option_id !== "vertexai-imagen") {
342
342
  driver.logger.warn("Invalid model options", {options: options.model_options });
343
343
  }
344
- options.model_options = options.model_options as ImagenOptions;
344
+ options.model_options = options.model_options as ImagenOptions | undefined;
345
345
 
346
346
  if (options.output_modality !== Modalities.image) {
347
347
  throw new Error(`Image generation requires image output_modality`);
348
348
  }
349
349
 
350
- const taskType: string = options.model_options.edit_mode ?? ImagenTaskType.TEXT_IMAGE;
350
+ const taskType: string = options.model_options?.edit_mode ?? ImagenTaskType.TEXT_IMAGE;
351
351
 
352
352
  driver.logger.info("Task type: " + taskType);
353
353
 
@@ -362,7 +362,7 @@ export class ImagenModelDefinition {
362
362
  }
363
363
  const instances = [instanceValue];
364
364
 
365
- let parameter: any = getImagenParameters(taskType, options.model_options);
365
+ let parameter: any = getImagenParameters(taskType, options.model_options ?? {_option_id: "vertexai-imagen"});
366
366
  parameter.negativePrompt = prompt.negativePrompt ?? undefined;
367
367
 
368
368
  const numberOfImages = options.model_options?.number_of_images ?? 1;
@@ -1,4 +1,4 @@
1
- import { AIModel, Completion, PromptOptions, PromptSegment, ExecutionOptions, CompletionChunk } from "@llumiverse/core";
1
+ import { AIModel, Completion, PromptSegment, ExecutionOptions, CompletionChunk } from "@llumiverse/core";
2
2
  import { VertexAIDriver , trimModelName} from "./index.js";
3
3
  import { GeminiModelDefinition } from "./models/gemini.js";
4
4
  import { ClaudeModelDefinition } from "./models/claude.js";
@@ -7,7 +7,7 @@ import { LLamaModelDefinition } from "./models/llama.js";
7
7
  export interface ModelDefinition<PromptT = any> {
8
8
  model: AIModel;
9
9
  versions?: string[]; // the versions of the model that are available. ex: ['001', '002']
10
- createPrompt: (driver: VertexAIDriver, segments: PromptSegment[], options: PromptOptions) => Promise<PromptT>;
10
+ createPrompt: (driver: VertexAIDriver, segments: PromptSegment[], options: ExecutionOptions) => Promise<PromptT>;
11
11
  requestTextCompletion: (driver: VertexAIDriver, prompt: PromptT, options: ExecutionOptions) => Promise<Completion>;
12
12
  requestTextCompletionStream: (driver: VertexAIDriver, prompt: PromptT, options: ExecutionOptions) => Promise<AsyncIterable<CompletionChunk>>;
13
13
  preValidationProcessing?(result: Completion, options: ExecutionOptions): { result: Completion, options: ExecutionOptions };
@@ -33,17 +33,17 @@ export class WatsonxDriver extends AbstractDriver<WatsonxDriverOptions, string>
33
33
  if (options.model_options?._option_id !== "text-fallback") {
34
34
  this.logger.warn("Invalid model options", {options: options.model_options });
35
35
  }
36
- options.model_options = options.model_options as TextFallbackOptions;
36
+ options.model_options = options.model_options as TextFallbackOptions | undefined;
37
37
 
38
38
  const payload: WatsonxTextGenerationPayload = {
39
39
  model_id: options.model,
40
40
  input: prompt + "\n",
41
41
  parameters: {
42
- max_new_tokens: options.model_options.max_tokens,
43
- temperature: options.model_options.temperature,
44
- top_k: options.model_options.top_k,
45
- top_p: options.model_options.top_p,
46
- stop_sequences: options.model_options.stop_sequence,
42
+ max_new_tokens: options.model_options?.max_tokens,
43
+ temperature: options.model_options?.temperature,
44
+ top_k: options.model_options?.top_k,
45
+ top_p: options.model_options?.top_p,
46
+ stop_sequences: options.model_options?.stop_sequence,
47
47
  },
48
48
  project_id: this.projectId,
49
49
  }
@@ -68,16 +68,16 @@ export class WatsonxDriver extends AbstractDriver<WatsonxDriverOptions, string>
68
68
  if (options.model_options?._option_id !== "text-fallback") {
69
69
  this.logger.warn("Invalid model options", {options: options.model_options });
70
70
  }
71
- options.model_options = options.model_options as TextFallbackOptions;
71
+ options.model_options = options.model_options as TextFallbackOptions | undefined;
72
72
  const payload: WatsonxTextGenerationPayload = {
73
73
  model_id: options.model,
74
74
  input: prompt + "\n",
75
75
  parameters: {
76
- max_new_tokens: options.model_options.max_tokens,
77
- temperature: options.model_options.temperature,
78
- top_k: options.model_options.top_k,
79
- top_p: options.model_options.top_p,
80
- stop_sequences: options.model_options.stop_sequence,
76
+ max_new_tokens: options.model_options?.max_tokens,
77
+ temperature: options.model_options?.temperature,
78
+ top_k: options.model_options?.top_k,
79
+ top_p: options.model_options?.top_p,
80
+ stop_sequences: options.model_options?.stop_sequence,
81
81
  },
82
82
  project_id: this.projectId,
83
83
  }