@llumiverse/drivers 0.23.0 → 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 (78) hide show
  1. package/README.md +141 -218
  2. package/lib/cjs/azure/azure_foundry.js +46 -2
  3. package/lib/cjs/azure/azure_foundry.js.map +1 -1
  4. package/lib/cjs/bedrock/index.js +236 -16
  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 +165 -0
  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 +59 -20
  21. package/lib/cjs/vertexai/models/gemini.js.map +1 -1
  22. package/lib/cjs/xai/index.js +10 -16
  23. package/lib/cjs/xai/index.js.map +1 -1
  24. package/lib/esm/azure/azure_foundry.js +46 -2
  25. package/lib/esm/azure/azure_foundry.js.map +1 -1
  26. package/lib/esm/bedrock/index.js +236 -17
  27. package/lib/esm/bedrock/index.js.map +1 -1
  28. package/lib/esm/groq/index.js +115 -85
  29. package/lib/esm/groq/index.js.map +1 -1
  30. package/lib/esm/index.js +1 -0
  31. package/lib/esm/index.js.map +1 -1
  32. package/lib/esm/openai/index.js +311 -115
  33. package/lib/esm/openai/index.js.map +1 -1
  34. package/lib/esm/openai/openai_compatible.js +55 -0
  35. package/lib/esm/openai/openai_compatible.js.map +1 -0
  36. package/lib/esm/openai/openai_format.js +32 -39
  37. package/lib/esm/openai/openai_format.js.map +1 -1
  38. package/lib/esm/vertexai/index.js +166 -1
  39. package/lib/esm/vertexai/index.js.map +1 -1
  40. package/lib/esm/vertexai/models/claude.js +199 -3
  41. package/lib/esm/vertexai/models/claude.js.map +1 -1
  42. package/lib/esm/vertexai/models/gemini.js +60 -21
  43. package/lib/esm/vertexai/models/gemini.js.map +1 -1
  44. package/lib/esm/xai/index.js +10 -16
  45. package/lib/esm/xai/index.js.map +1 -1
  46. package/lib/types/azure/azure_foundry.d.ts +7 -5
  47. package/lib/types/azure/azure_foundry.d.ts.map +1 -1
  48. package/lib/types/bedrock/index.d.ts +21 -1
  49. package/lib/types/bedrock/index.d.ts.map +1 -1
  50. package/lib/types/groq/index.d.ts.map +1 -1
  51. package/lib/types/index.d.ts +1 -0
  52. package/lib/types/index.d.ts.map +1 -1
  53. package/lib/types/openai/index.d.ts +13 -7
  54. package/lib/types/openai/index.d.ts.map +1 -1
  55. package/lib/types/openai/openai_compatible.d.ts +26 -0
  56. package/lib/types/openai/openai_compatible.d.ts.map +1 -0
  57. package/lib/types/openai/openai_format.d.ts +4 -2
  58. package/lib/types/openai/openai_format.d.ts.map +1 -1
  59. package/lib/types/vertexai/index.d.ts +15 -0
  60. package/lib/types/vertexai/index.d.ts.map +1 -1
  61. package/lib/types/vertexai/models/claude.d.ts +20 -0
  62. package/lib/types/vertexai/models/claude.d.ts.map +1 -1
  63. package/lib/types/vertexai/models/gemini.d.ts +1 -1
  64. package/lib/types/vertexai/models/gemini.d.ts.map +1 -1
  65. package/lib/types/xai/index.d.ts +2 -3
  66. package/lib/types/xai/index.d.ts.map +1 -1
  67. package/package.json +12 -12
  68. package/src/azure/azure_foundry.ts +56 -7
  69. package/src/bedrock/index.ts +297 -26
  70. package/src/groq/index.ts +120 -94
  71. package/src/index.ts +1 -0
  72. package/src/openai/index.ts +363 -136
  73. package/src/openai/openai_compatible.ts +74 -0
  74. package/src/openai/openai_format.ts +44 -54
  75. package/src/vertexai/index.ts +205 -0
  76. package/src/vertexai/models/claude.ts +233 -3
  77. package/src/vertexai/models/gemini.ts +78 -27
  78. package/src/xai/index.ts +10 -17
@@ -0,0 +1,74 @@
1
+ import { AIModel, DriverOptions, ModelType, Providers, getModelCapabilities, modelModalitiesToArray } from "@llumiverse/core";
2
+ import OpenAI from "openai";
3
+ import { BaseOpenAIDriver } from "./index.js";
4
+
5
+ export interface OpenAICompatibleDriverOptions extends DriverOptions {
6
+ /**
7
+ * The API key for the OpenAI-compatible service
8
+ */
9
+ apiKey: string;
10
+
11
+ /**
12
+ * The base URL of the OpenAI-compatible API endpoint
13
+ * Example: https://api.example.com/v1
14
+ */
15
+ endpoint: string;
16
+ }
17
+
18
+ /**
19
+ * A generic driver for OpenAI-compatible APIs.
20
+ * This can be used with any service that implements the OpenAI API spec,
21
+ * such as xAI (Grok), LM Studio, Ollama, vLLM, LocalAI, etc.
22
+ */
23
+ export class OpenAICompatibleDriver extends BaseOpenAIDriver {
24
+ service: OpenAI;
25
+ readonly provider = Providers.openai_compatible;
26
+
27
+ constructor(opts: OpenAICompatibleDriverOptions) {
28
+ super(opts);
29
+
30
+ if (!opts.apiKey) {
31
+ throw new Error("apiKey is required");
32
+ }
33
+
34
+ if (!opts.endpoint) {
35
+ throw new Error("endpoint is required for OpenAI-compatible driver");
36
+ }
37
+
38
+ this.service = new OpenAI({
39
+ apiKey: opts.apiKey,
40
+ baseURL: opts.endpoint,
41
+ });
42
+ }
43
+
44
+ async listModels(): Promise<AIModel[]> {
45
+ try {
46
+ const result = (await this.service.models.list()).data;
47
+
48
+ const models = result.map((m) => {
49
+ const modelCapability = getModelCapabilities(m.id, "openai");
50
+ let owner = m.owned_by;
51
+ if (owner === "system") {
52
+ owner = "unknown";
53
+ }
54
+ return {
55
+ id: m.id,
56
+ name: m.id,
57
+ provider: this.provider,
58
+ owner: owner,
59
+ type: ModelType.Text,
60
+ can_stream: true,
61
+ is_multimodal: false,
62
+ input_modalities: modelModalitiesToArray(modelCapability.input),
63
+ output_modalities: modelModalitiesToArray(modelCapability.output),
64
+ tool_support: modelCapability.tool_support,
65
+ } satisfies AIModel<string>;
66
+ }).sort((a, b) => a.id.localeCompare(b.id));
67
+
68
+ return models;
69
+ } catch (error) {
70
+ this.logger.warn({ error }, "[OpenAICompatible] Failed to list models, returning empty list");
71
+ return [];
72
+ }
73
+ }
74
+ }
@@ -3,16 +3,12 @@
3
3
 
4
4
  import { PromptRole, PromptOptions, PromptSegment } from "@llumiverse/common";
5
5
  import { readStreamAsBase64 } from "@llumiverse/core";
6
+ import type OpenAI from "openai";
6
7
 
7
- import type {
8
- ChatCompletionMessageParam,
9
- ChatCompletionContentPartText,
10
- ChatCompletionContentPartImage,
11
- ChatCompletionUserMessageParam,
12
- ChatCompletionSystemMessageParam,
13
- ChatCompletionAssistantMessageParam,
14
- ChatCompletionToolMessageParam
15
- } from 'openai/resources/chat/completions';
8
+ // Types for Response API
9
+ type ResponseInputItem = OpenAI.Responses.ResponseInputItem;
10
+ type ResponseInputContent = OpenAI.Responses.ResponseInputContent;
11
+ type EasyInputMessage = OpenAI.Responses.EasyInputMessage;
16
12
 
17
13
  export interface OpenAITextMessage {
18
14
  content: string;
@@ -47,14 +43,14 @@ export function formatOpenAILikeTextPrompt(segments: PromptSegment[]): OpenAITex
47
43
  }
48
44
 
49
45
 
50
- export async function formatOpenAILikeMultimodalPrompt(segments: PromptSegment[], opts: PromptOptions & OpenAIPromptFormatterOptions): Promise<ChatCompletionMessageParam[]> {
51
- const system: ChatCompletionMessageParam[] = [];
52
- const safety: ChatCompletionMessageParam[] = [];
53
- const others: ChatCompletionMessageParam[] = [];
46
+ export async function formatOpenAILikeMultimodalPrompt(segments: PromptSegment[], opts: PromptOptions & OpenAIPromptFormatterOptions): Promise<ResponseInputItem[]> {
47
+ const system: ResponseInputItem[] = [];
48
+ const safety: ResponseInputItem[] = [];
49
+ const others: ResponseInputItem[] = [];
54
50
 
55
51
  for (const msg of segments) {
56
52
 
57
- const parts: (ChatCompletionContentPartImage | ChatCompletionContentPartText)[] = [];
53
+ const parts: ResponseInputContent[] = [];
58
54
 
59
55
  //generate the parts based on PromptSegment
60
56
  if (msg.files) {
@@ -62,54 +58,56 @@ export async function formatOpenAILikeMultimodalPrompt(segments: PromptSegment[]
62
58
  const stream = await file.getStream();
63
59
  const data = await readStreamAsBase64(stream);
64
60
  parts.push({
65
- type: "image_url",
66
- image_url: {
67
- url: `data:${file.mime_type || "image/jpeg"};base64,${data}`,
68
- //detail: "auto" //This is modified just before execution to "low" | "high" | "auto"
69
- },
61
+ type: "input_image",
62
+ image_url: `data:${file.mime_type || "image/jpeg"};base64,${data}`,
63
+ detail: "auto",
70
64
  })
71
65
  }
72
66
  }
73
67
 
74
68
  if (msg.content) {
75
69
  parts.push({
70
+ type: "input_text",
76
71
  text: msg.content,
77
- type: "text"
78
72
  })
79
73
  }
80
74
 
81
75
 
82
76
  if (msg.role === PromptRole.system) {
83
77
  // For system messages, filter to only text parts
84
- const textParts = parts.filter((part): part is ChatCompletionContentPartText => part.type === 'text');
85
- const systemMsg: ChatCompletionSystemMessageParam = {
78
+ const textParts = parts.filter((part): part is OpenAI.Responses.ResponseInputText => part.type === 'input_text');
79
+ const textContent = textParts.length === 1 && !msg.files ? textParts[0].text : textParts;
80
+ const systemMsg: EasyInputMessage = {
86
81
  role: "system",
87
- content: textParts.length === 1 && !msg.files ? textParts[0].text : textParts
82
+ content: textContent,
88
83
  };
89
84
  system.push(systemMsg);
90
85
 
91
86
  if (opts.useToolForFormatting && opts.schema) {
92
87
  system.forEach(s => {
93
- if (typeof s.content === 'string') {
94
- s.content = "TOOL: " + s.content;
95
- } else if (Array.isArray(s.content)) {
96
- s.content.forEach((c: any) => {
97
- if (c.type === "text") c.text = "TOOL: " + c.text;
98
- });
88
+ if ((s as EasyInputMessage).role === 'system') {
89
+ const sysMsg = s as EasyInputMessage;
90
+ if (typeof sysMsg.content === 'string') {
91
+ sysMsg.content = "TOOL: " + sysMsg.content;
92
+ } else if (Array.isArray(sysMsg.content)) {
93
+ sysMsg.content.forEach((c: any) => {
94
+ if (c.type === "input_text") c.text = "TOOL: " + c.text;
95
+ });
96
+ }
99
97
  }
100
98
  });
101
99
  }
102
100
 
103
101
  } else if (msg.role === PromptRole.safety) {
104
- const textParts = parts.filter((part): part is ChatCompletionContentPartText => part.type === 'text');
105
- const safetyMsg: ChatCompletionSystemMessageParam = {
102
+ const textParts = parts.filter((part): part is OpenAI.Responses.ResponseInputText => part.type === 'input_text');
103
+ const safetyMsg: EasyInputMessage = {
106
104
  role: "system",
107
- content: textParts
105
+ content: textParts,
108
106
  };
109
107
 
110
108
  if (Array.isArray(safetyMsg.content)) {
111
109
  safetyMsg.content.forEach((c: any) => {
112
- if (c.type === "text") c.text = "DO NOT IGNORE - IMPORTANT: " + c.text;
110
+ if (c.type === "input_text") c.text = "DO NOT IGNORE - IMPORTANT: " + c.text;
113
111
  });
114
112
  }
115
113
 
@@ -118,35 +116,27 @@ export async function formatOpenAILikeMultimodalPrompt(segments: PromptSegment[]
118
116
  if (!msg.tool_use_id) {
119
117
  throw new Error("Tool use id is required for tool messages")
120
118
  }
121
- const toolMsg: ChatCompletionToolMessageParam = {
122
- role: "tool",
123
- tool_call_id: msg.tool_use_id,
124
- content: msg.content || ""
119
+ const toolOutputMsg: OpenAI.Responses.ResponseInputItem.FunctionCallOutput = {
120
+ type: "function_call_output",
121
+ call_id: msg.tool_use_id,
122
+ output: msg.content || ""
125
123
  };
126
- others.push(toolMsg);
124
+ others.push(toolOutputMsg);
127
125
  } else if (msg.role !== PromptRole.negative && msg.role !== PromptRole.mask) {
128
- if (msg.role === 'assistant') {
129
- const assistantMsg: ChatCompletionAssistantMessageParam = {
130
- role: 'assistant',
131
- content: parts as (ChatCompletionContentPartText)[]
132
- };
133
- others.push(assistantMsg);
134
- } else {
135
- const userMsg: ChatCompletionUserMessageParam = {
136
- role: 'user',
137
- content: parts
138
- };
139
- others.push(userMsg);
140
- }
126
+ const inputMsg: EasyInputMessage = {
127
+ role: msg.role === 'assistant' ? 'assistant' : 'user',
128
+ content: parts,
129
+ };
130
+ others.push(inputMsg);
141
131
  }
142
132
 
143
133
  }
144
134
 
145
135
  if (opts.result_schema && !opts.useToolForFormatting) {
146
- const schemaMsg: ChatCompletionSystemMessageParam = {
136
+ const schemaMsg: EasyInputMessage = {
147
137
  role: "system",
148
138
  content: [{
149
- type: "text",
139
+ type: "input_text",
150
140
  text: "IMPORTANT: only answer using JSON, and respecting the schema included below, between the <response_schema> tags. " + `<response_schema>${JSON.stringify(opts.result_schema)}</response_schema>`
151
141
  }]
152
142
  };
@@ -154,7 +144,7 @@ export async function formatOpenAILikeMultimodalPrompt(segments: PromptSegment[]
154
144
  }
155
145
 
156
146
  // put system messages first and safety last
157
- return ([] as ChatCompletionMessageParam[]).concat(system).concat(others).concat(safety);
147
+ return ([] as ResponseInputItem[]).concat(system).concat(others).concat(safety);
158
148
 
159
149
  }
160
150
 
@@ -6,6 +6,7 @@ import {
6
6
  AbstractDriver,
7
7
  Completion,
8
8
  CompletionChunkObject,
9
+ CompletionResult,
9
10
  DriverOptions,
10
11
  EmbeddingsOptions,
11
12
  EmbeddingsResult,
@@ -14,6 +15,11 @@ import {
14
15
  PromptSegment,
15
16
  getModelCapabilities,
16
17
  modelModalitiesToArray,
18
+ stripBase64ImagesFromConversation,
19
+ truncateLargeTextInConversation,
20
+ getConversationMeta,
21
+ incrementConversationTurn,
22
+ unwrapConversationArray,
17
23
  } from "@llumiverse/core";
18
24
  import { FetchClient } from "@vertesia/api-fetch-client";
19
25
  import { GoogleAuth, GoogleAuthOptions, AuthClient } from "google-auth-library";
@@ -251,6 +257,196 @@ export class VertexAIDriver extends AbstractDriver<VertexAIDriverOptions, Vertex
251
257
  return getModelDefinition(options.model).requestTextCompletionStream(this, prompt, options);
252
258
  }
253
259
 
260
+ /**
261
+ * Build conversation context after streaming completion.
262
+ * Reconstructs the assistant message from accumulated results and applies stripping.
263
+ * Handles both Gemini (Content[]) and Claude (ClaudePrompt) formats.
264
+ */
265
+ buildStreamingConversation(
266
+ prompt: VertexAIPrompt,
267
+ result: unknown[],
268
+ toolUse: unknown[] | undefined,
269
+ options: ExecutionOptions
270
+ ): Content[] | unknown | undefined {
271
+ // Handle Claude-style prompts (has 'messages' array)
272
+ if ('messages' in prompt && Array.isArray((prompt as any).messages)) {
273
+ return this.buildClaudeStreamingConversation(prompt as any, result, toolUse, options);
274
+ }
275
+
276
+ // Only handle Gemini-style prompts with contents array
277
+ if (!('contents' in prompt) || !Array.isArray(prompt.contents)) {
278
+ return undefined;
279
+ }
280
+
281
+ const completionResults = result as CompletionResult[];
282
+
283
+ // Convert accumulated results to text content for assistant message
284
+ const textContent = completionResults
285
+ .map(r => {
286
+ switch (r.type) {
287
+ case 'text':
288
+ return r.value;
289
+ case 'json':
290
+ return typeof r.value === 'string' ? r.value : JSON.stringify(r.value);
291
+ case 'image':
292
+ // Skip images in conversation - they're in the result
293
+ return '';
294
+ default:
295
+ return String((r as any).value || '');
296
+ }
297
+ })
298
+ .join('');
299
+
300
+ // Build parts array for assistant message
301
+ const parts: any[] = [];
302
+ if (textContent) {
303
+ parts.push({ text: textContent });
304
+ }
305
+ // Add function calls if present (Gemini format)
306
+ if (toolUse && toolUse.length > 0) {
307
+ for (const tool of toolUse as any[]) {
308
+ const functionCallPart: any = {
309
+ functionCall: {
310
+ name: tool.tool_name,
311
+ args: tool.tool_input,
312
+ }
313
+ };
314
+ // Include thought_signature for Gemini thinking models (2.5+/3.0+)
315
+ // This must be preserved in the conversation for subsequent API calls
316
+ if (tool.thought_signature) {
317
+ functionCallPart.thoughtSignature = tool.thought_signature;
318
+ }
319
+ parts.push(functionCallPart);
320
+ }
321
+ }
322
+
323
+ // Unwrap array if wrapped, otherwise treat as array
324
+ const unwrapped = unwrapConversationArray<Content>(options.conversation);
325
+ const existingConversation = unwrapped ?? (options.conversation as Content[] || []);
326
+
327
+ // Combine existing conversation + prompt contents
328
+ let conversation: Content[] = [
329
+ ...existingConversation,
330
+ ...prompt.contents,
331
+ ];
332
+
333
+ // Only add assistant message if there's actual content
334
+ // (Empty text parts can cause API errors)
335
+ if (parts.length > 0) {
336
+ conversation.push({
337
+ role: 'model',
338
+ parts: parts
339
+ });
340
+ }
341
+
342
+ // Increment turn counter
343
+ conversation = incrementConversationTurn(conversation) as Content[];
344
+
345
+ // Apply stripping based on options
346
+ const currentTurn = getConversationMeta(conversation).turnNumber;
347
+ const stripOptions = {
348
+ keepForTurns: options.stripImagesAfterTurns ?? Infinity,
349
+ currentTurn,
350
+ textMaxTokens: options.stripTextMaxTokens
351
+ };
352
+ let processedConversation = stripBase64ImagesFromConversation(conversation, stripOptions);
353
+ processedConversation = truncateLargeTextInConversation(processedConversation, stripOptions);
354
+
355
+ return processedConversation as Content[];
356
+ }
357
+
358
+ /**
359
+ * Build conversation for Claude streaming.
360
+ * Creates assistant message with tool_use blocks in Claude's ContentBlock format.
361
+ */
362
+ private buildClaudeStreamingConversation(
363
+ prompt: { messages: unknown[]; system?: unknown[] },
364
+ result: unknown[],
365
+ toolUse: unknown[] | undefined,
366
+ options: ExecutionOptions
367
+ ): unknown {
368
+ const completionResults = result as CompletionResult[];
369
+
370
+ // Convert accumulated results to text content
371
+ const textContent = completionResults
372
+ .map(r => {
373
+ switch (r.type) {
374
+ case 'text':
375
+ return r.value;
376
+ case 'json':
377
+ return typeof r.value === 'string' ? r.value : JSON.stringify(r.value);
378
+ case 'image':
379
+ return '';
380
+ default:
381
+ return String((r as any).value || '');
382
+ }
383
+ })
384
+ .join('');
385
+
386
+ // Build Claude-style ContentBlock array for assistant message
387
+ const content: unknown[] = [];
388
+
389
+ // Add text block if there's text content
390
+ if (textContent) {
391
+ content.push({
392
+ type: 'text',
393
+ text: textContent
394
+ });
395
+ }
396
+
397
+ // Add tool_use blocks in Claude format
398
+ if (toolUse && toolUse.length > 0) {
399
+ for (const tool of toolUse as any[]) {
400
+ content.push({
401
+ type: 'tool_use',
402
+ id: tool.id,
403
+ name: tool.tool_name,
404
+ input: tool.tool_input ?? {}
405
+ });
406
+ }
407
+ }
408
+
409
+ // Get existing conversation or start fresh
410
+ const existingMessages = (options.conversation as any)?.messages ?? [];
411
+ const existingSystem = (options.conversation as any)?.system ?? prompt.system;
412
+
413
+ // Build the new messages array
414
+ const newMessages = [
415
+ ...existingMessages,
416
+ ...prompt.messages,
417
+ ];
418
+
419
+ // Only add assistant message if there's actual content
420
+ // (Claude API rejects empty text content blocks)
421
+ if (content.length > 0) {
422
+ newMessages.push({
423
+ role: 'assistant',
424
+ content: content
425
+ });
426
+ }
427
+
428
+ // Build the new conversation in ClaudePrompt format
429
+ const conversation = {
430
+ messages: newMessages,
431
+ system: existingSystem
432
+ };
433
+
434
+ // Increment turn counter
435
+ const withTurn = incrementConversationTurn(conversation);
436
+
437
+ // Apply stripping based on options
438
+ const currentTurn = getConversationMeta(withTurn).turnNumber;
439
+ const stripOptions = {
440
+ keepForTurns: options.stripImagesAfterTurns ?? Infinity,
441
+ currentTurn,
442
+ textMaxTokens: options.stripTextMaxTokens
443
+ };
444
+ let processedConversation = stripBase64ImagesFromConversation(withTurn, stripOptions);
445
+ processedConversation = truncateLargeTextInConversation(processedConversation, stripOptions);
446
+
447
+ return processedConversation;
448
+ }
449
+
254
450
  async requestImageGeneration(
255
451
  _prompt: ImagenPrompt,
256
452
  _options: ExecutionOptions,
@@ -496,6 +692,15 @@ export class VertexAIDriver extends AbstractDriver<VertexAIDriverOptions, Vertex
496
692
  };
497
693
  return getEmbeddingsForText(this, text_options);
498
694
  }
695
+
696
+ /**
697
+ * Cleanup Google Cloud clients when the driver is evicted from the cache.
698
+ */
699
+ destroy(): void {
700
+ this.aiplatform?.close();
701
+ this.modelGarden?.close();
702
+ this.imagenClient?.close();
703
+ }
499
704
  }
500
705
 
501
706
  //'us-central1-aiplatform.googleapis.com',