@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.
- package/README.md +141 -218
- package/lib/cjs/azure/azure_foundry.js +46 -2
- package/lib/cjs/azure/azure_foundry.js.map +1 -1
- package/lib/cjs/bedrock/index.js +236 -16
- package/lib/cjs/bedrock/index.js.map +1 -1
- package/lib/cjs/groq/index.js +115 -85
- package/lib/cjs/groq/index.js.map +1 -1
- package/lib/cjs/index.js +1 -0
- package/lib/cjs/index.js.map +1 -1
- package/lib/cjs/openai/index.js +310 -114
- package/lib/cjs/openai/index.js.map +1 -1
- package/lib/cjs/openai/openai_compatible.js +62 -0
- package/lib/cjs/openai/openai_compatible.js.map +1 -0
- package/lib/cjs/openai/openai_format.js +32 -39
- package/lib/cjs/openai/openai_format.js.map +1 -1
- package/lib/cjs/vertexai/index.js +165 -0
- package/lib/cjs/vertexai/index.js.map +1 -1
- package/lib/cjs/vertexai/models/claude.js +201 -3
- package/lib/cjs/vertexai/models/claude.js.map +1 -1
- package/lib/cjs/vertexai/models/gemini.js +59 -20
- package/lib/cjs/vertexai/models/gemini.js.map +1 -1
- package/lib/cjs/xai/index.js +10 -16
- package/lib/cjs/xai/index.js.map +1 -1
- package/lib/esm/azure/azure_foundry.js +46 -2
- package/lib/esm/azure/azure_foundry.js.map +1 -1
- package/lib/esm/bedrock/index.js +236 -17
- package/lib/esm/bedrock/index.js.map +1 -1
- package/lib/esm/groq/index.js +115 -85
- package/lib/esm/groq/index.js.map +1 -1
- package/lib/esm/index.js +1 -0
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/openai/index.js +311 -115
- package/lib/esm/openai/index.js.map +1 -1
- package/lib/esm/openai/openai_compatible.js +55 -0
- package/lib/esm/openai/openai_compatible.js.map +1 -0
- package/lib/esm/openai/openai_format.js +32 -39
- package/lib/esm/openai/openai_format.js.map +1 -1
- package/lib/esm/vertexai/index.js +166 -1
- package/lib/esm/vertexai/index.js.map +1 -1
- package/lib/esm/vertexai/models/claude.js +199 -3
- package/lib/esm/vertexai/models/claude.js.map +1 -1
- package/lib/esm/vertexai/models/gemini.js +60 -21
- package/lib/esm/vertexai/models/gemini.js.map +1 -1
- package/lib/esm/xai/index.js +10 -16
- package/lib/esm/xai/index.js.map +1 -1
- package/lib/types/azure/azure_foundry.d.ts +7 -5
- package/lib/types/azure/azure_foundry.d.ts.map +1 -1
- package/lib/types/bedrock/index.d.ts +21 -1
- package/lib/types/bedrock/index.d.ts.map +1 -1
- package/lib/types/groq/index.d.ts.map +1 -1
- package/lib/types/index.d.ts +1 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/openai/index.d.ts +13 -7
- package/lib/types/openai/index.d.ts.map +1 -1
- package/lib/types/openai/openai_compatible.d.ts +26 -0
- package/lib/types/openai/openai_compatible.d.ts.map +1 -0
- package/lib/types/openai/openai_format.d.ts +4 -2
- package/lib/types/openai/openai_format.d.ts.map +1 -1
- package/lib/types/vertexai/index.d.ts +15 -0
- package/lib/types/vertexai/index.d.ts.map +1 -1
- package/lib/types/vertexai/models/claude.d.ts +20 -0
- package/lib/types/vertexai/models/claude.d.ts.map +1 -1
- package/lib/types/vertexai/models/gemini.d.ts +1 -1
- package/lib/types/vertexai/models/gemini.d.ts.map +1 -1
- package/lib/types/xai/index.d.ts +2 -3
- package/lib/types/xai/index.d.ts.map +1 -1
- package/package.json +12 -12
- package/src/azure/azure_foundry.ts +56 -7
- package/src/bedrock/index.ts +297 -26
- package/src/groq/index.ts +120 -94
- package/src/index.ts +1 -0
- package/src/openai/index.ts +363 -136
- package/src/openai/openai_compatible.ts +74 -0
- package/src/openai/openai_format.ts +44 -54
- package/src/vertexai/index.ts +205 -0
- package/src/vertexai/models/claude.ts +233 -3
- package/src/vertexai/models/gemini.ts +78 -27
- 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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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<
|
|
51
|
-
const system:
|
|
52
|
-
const safety:
|
|
53
|
-
const others:
|
|
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:
|
|
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: "
|
|
66
|
-
image_url: {
|
|
67
|
-
|
|
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
|
|
85
|
-
const
|
|
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:
|
|
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 (
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
105
|
-
const safetyMsg:
|
|
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 === "
|
|
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
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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(
|
|
124
|
+
others.push(toolOutputMsg);
|
|
127
125
|
} else if (msg.role !== PromptRole.negative && msg.role !== PromptRole.mask) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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:
|
|
136
|
+
const schemaMsg: EasyInputMessage = {
|
|
147
137
|
role: "system",
|
|
148
138
|
content: [{
|
|
149
|
-
type: "
|
|
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
|
|
147
|
+
return ([] as ResponseInputItem[]).concat(system).concat(others).concat(safety);
|
|
158
148
|
|
|
159
149
|
}
|
|
160
150
|
|
package/src/vertexai/index.ts
CHANGED
|
@@ -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',
|