@layer-ai/core 0.5.0 → 0.5.2
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/dist/routes/v2/complete.d.ts.map +1 -1
- package/dist/routes/v2/complete.js +28 -4
- package/dist/services/providers/mistral-adapter.d.ts +13 -0
- package/dist/services/providers/mistral-adapter.d.ts.map +1 -0
- package/dist/services/providers/mistral-adapter.js +310 -0
- package/dist/services/providers/tests/test-mistral-adapter.d.ts +2 -0
- package/dist/services/providers/tests/test-mistral-adapter.d.ts.map +1 -0
- package/dist/services/providers/tests/test-mistral-adapter.js +334 -0
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"complete.d.ts","sourceRoot":"","sources":["../../../src/routes/v2/complete.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"complete.d.ts","sourceRoot":"","sources":["../../../src/routes/v2/complete.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,SAAS,CAAC;AAYpD,QAAA,MAAM,MAAM,EAAE,UAAqB,CAAC;AAyRpC,eAAe,MAAM,CAAC"}
|
|
@@ -5,9 +5,23 @@ import { authenticate } from '../../middleware/auth.js';
|
|
|
5
5
|
import { OpenAIAdapter } from '../../services/providers/openai-adapter.js';
|
|
6
6
|
import { AnthropicAdapter } from '../../services/providers/anthropic-adapter.js';
|
|
7
7
|
import { GoogleAdapter } from '../../services/providers/google-adapter.js';
|
|
8
|
+
import { MistralAdapter } from '../../services/providers/mistral-adapter.js';
|
|
8
9
|
import { MODEL_REGISTRY, OverrideField } from '@layer-ai/sdk';
|
|
9
10
|
const router = Router();
|
|
10
11
|
// MARK:- Helper Functions
|
|
12
|
+
function normalizeModelId(modelId) {
|
|
13
|
+
if (MODEL_REGISTRY[modelId]) {
|
|
14
|
+
return modelId;
|
|
15
|
+
}
|
|
16
|
+
const providers = ['openai', 'anthropic', 'google', 'mistral'];
|
|
17
|
+
for (const provider of providers) {
|
|
18
|
+
const fullId = `${provider}/${modelId}`;
|
|
19
|
+
if (MODEL_REGISTRY[fullId]) {
|
|
20
|
+
return fullId;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
throw new Error(`invalid model ID: "${modelId}" not found in registry`);
|
|
24
|
+
}
|
|
11
25
|
function isOverrideAllowed(allowOverrides, field) {
|
|
12
26
|
if (allowOverrides === undefined || allowOverrides === null || allowOverrides === true)
|
|
13
27
|
return true;
|
|
@@ -28,10 +42,15 @@ async function getGateConfig(userId, gateName) {
|
|
|
28
42
|
function resolveFinalRequest(gateConfig, request) {
|
|
29
43
|
const finalRequest = { ...request };
|
|
30
44
|
let finalModel = gateConfig.model;
|
|
31
|
-
if (request.model && isOverrideAllowed(gateConfig.allowOverrides, OverrideField.Model)
|
|
32
|
-
|
|
45
|
+
if (request.model && isOverrideAllowed(gateConfig.allowOverrides, OverrideField.Model)) {
|
|
46
|
+
try {
|
|
47
|
+
finalModel = normalizeModelId(request.model);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
finalModel = gateConfig.model;
|
|
51
|
+
}
|
|
33
52
|
}
|
|
34
|
-
finalRequest.model = finalModel;
|
|
53
|
+
finalRequest.model = normalizeModelId(finalModel);
|
|
35
54
|
if (request.type === 'chat') {
|
|
36
55
|
const chatData = { ...request.data };
|
|
37
56
|
if (!chatData.systemPrompt && gateConfig.systemPrompt) {
|
|
@@ -60,7 +79,8 @@ function resolveFinalRequest(gateConfig, request) {
|
|
|
60
79
|
return finalRequest;
|
|
61
80
|
}
|
|
62
81
|
async function callProvider(request) {
|
|
63
|
-
const
|
|
82
|
+
const normalizedModel = normalizeModelId(request.model);
|
|
83
|
+
const provider = MODEL_REGISTRY[normalizedModel].provider;
|
|
64
84
|
switch (provider) {
|
|
65
85
|
case 'openai': {
|
|
66
86
|
const adapter = new OpenAIAdapter();
|
|
@@ -73,6 +93,10 @@ async function callProvider(request) {
|
|
|
73
93
|
case 'google':
|
|
74
94
|
const adapter = new GoogleAdapter();
|
|
75
95
|
return await adapter.call(request);
|
|
96
|
+
case 'mistral': {
|
|
97
|
+
const adapter = new MistralAdapter();
|
|
98
|
+
return await adapter.call(request);
|
|
99
|
+
}
|
|
76
100
|
default:
|
|
77
101
|
throw new Error(`Unknown provider: ${provider}`);
|
|
78
102
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { BaseProviderAdapter } from './base-adapter.js';
|
|
2
|
+
import { LayerRequest, LayerResponse, Role, FinishReason } from '@layer-ai/sdk';
|
|
3
|
+
export declare class MistralAdapter extends BaseProviderAdapter {
|
|
4
|
+
protected provider: string;
|
|
5
|
+
protected roleMappings: Record<Role, string>;
|
|
6
|
+
protected finishReasonMappings: Record<string, FinishReason>;
|
|
7
|
+
protected toolChoiceMappings: Record<string, string>;
|
|
8
|
+
call(request: LayerRequest): Promise<LayerResponse>;
|
|
9
|
+
private handleChat;
|
|
10
|
+
private handleEmbeddings;
|
|
11
|
+
private handleOCR;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=mistral-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mistral-adapter.d.ts","sourceRoot":"","sources":["../../../src/services/providers/mistral-adapter.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EACL,YAAY,EACZ,aAAa,EACb,IAAI,EACJ,YAAY,EAEb,MAAM,eAAe,CAAC;AAavB,qBAAa,cAAe,SAAQ,mBAAmB;IACrD,SAAS,CAAC,QAAQ,SAAa;IAE/B,SAAS,CAAC,YAAY,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAQ1C;IAGF,SAAS,CAAC,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAM1D;IAEF,SAAS,CAAC,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAIlD;IAEI,IAAI,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC;YAmB3C,UAAU;YA8LV,gBAAgB;YAuChB,SAAS;CA2ExB"}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { Mistral } from '@mistralai/mistralai';
|
|
2
|
+
import { BaseProviderAdapter } from './base-adapter.js';
|
|
3
|
+
import { ADAPTER_HANDLED, } from '@layer-ai/sdk';
|
|
4
|
+
let client = null;
|
|
5
|
+
function getMistralClient() {
|
|
6
|
+
if (!client) {
|
|
7
|
+
client = new Mistral({
|
|
8
|
+
apiKey: process.env.MISTRAL_API_KEY || '',
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
return client;
|
|
12
|
+
}
|
|
13
|
+
export class MistralAdapter extends BaseProviderAdapter {
|
|
14
|
+
constructor() {
|
|
15
|
+
super(...arguments);
|
|
16
|
+
this.provider = 'mistral';
|
|
17
|
+
this.roleMappings = {
|
|
18
|
+
system: ADAPTER_HANDLED,
|
|
19
|
+
user: 'user',
|
|
20
|
+
assistant: 'assistant',
|
|
21
|
+
tool: 'tool',
|
|
22
|
+
function: 'tool',
|
|
23
|
+
model: 'assistant',
|
|
24
|
+
developer: 'system',
|
|
25
|
+
};
|
|
26
|
+
// Map Mistral finish reasons to Layer finish reasons
|
|
27
|
+
this.finishReasonMappings = {
|
|
28
|
+
stop: 'completed',
|
|
29
|
+
length: 'length_limit',
|
|
30
|
+
tool_calls: 'tool_call',
|
|
31
|
+
model_length: 'length_limit',
|
|
32
|
+
error: 'error',
|
|
33
|
+
};
|
|
34
|
+
this.toolChoiceMappings = {
|
|
35
|
+
auto: 'auto',
|
|
36
|
+
none: 'none',
|
|
37
|
+
required: 'any',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
async call(request) {
|
|
41
|
+
switch (request.type) {
|
|
42
|
+
case 'chat':
|
|
43
|
+
return this.handleChat(request);
|
|
44
|
+
case 'embeddings':
|
|
45
|
+
return this.handleEmbeddings(request);
|
|
46
|
+
case 'ocr':
|
|
47
|
+
return this.handleOCR(request);
|
|
48
|
+
case 'image':
|
|
49
|
+
throw new Error('Image generation not supported by Mistral');
|
|
50
|
+
case 'tts':
|
|
51
|
+
throw new Error('Text-to-speech not supported by Mistral');
|
|
52
|
+
case 'video':
|
|
53
|
+
throw new Error('Video generation not supported by Mistral');
|
|
54
|
+
default:
|
|
55
|
+
throw new Error(`Unknown modality: ${request.type}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async handleChat(request) {
|
|
59
|
+
const startTime = Date.now();
|
|
60
|
+
const mistral = getMistralClient();
|
|
61
|
+
const { data: chat, model } = request;
|
|
62
|
+
if (!model) {
|
|
63
|
+
throw new Error('Model is required for chat completion');
|
|
64
|
+
}
|
|
65
|
+
// Build messages array
|
|
66
|
+
const messages = [];
|
|
67
|
+
// Handle system prompt
|
|
68
|
+
if (chat.systemPrompt) {
|
|
69
|
+
messages.push({ role: 'system', content: chat.systemPrompt });
|
|
70
|
+
}
|
|
71
|
+
// Convert messages to Mistral format
|
|
72
|
+
for (const msg of chat.messages) {
|
|
73
|
+
const role = this.mapRole(msg.role);
|
|
74
|
+
// Handle vision messages (content + images)
|
|
75
|
+
if (msg.images && msg.images.length > 0 && role === 'user') {
|
|
76
|
+
const content = [];
|
|
77
|
+
if (msg.content) {
|
|
78
|
+
content.push({ type: 'text', text: msg.content });
|
|
79
|
+
}
|
|
80
|
+
for (const image of msg.images) {
|
|
81
|
+
const imageUrl = image.url || `data:${image.mimeType || 'image/jpeg'};base64,${image.base64}`;
|
|
82
|
+
content.push({
|
|
83
|
+
type: 'image_url',
|
|
84
|
+
imageUrl: imageUrl,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
messages.push({ role, content });
|
|
88
|
+
}
|
|
89
|
+
// Handle tool responses
|
|
90
|
+
else if (msg.toolCallId && role === 'tool') {
|
|
91
|
+
messages.push({
|
|
92
|
+
role: 'tool',
|
|
93
|
+
content: msg.content || '',
|
|
94
|
+
toolCallId: msg.toolCallId,
|
|
95
|
+
name: msg.name,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
// Handle assistant messages with tool calls
|
|
99
|
+
else if (msg.toolCalls && msg.toolCalls.length > 0) {
|
|
100
|
+
messages.push({
|
|
101
|
+
role: 'assistant',
|
|
102
|
+
content: msg.content || '',
|
|
103
|
+
toolCalls: msg.toolCalls.map((tc) => ({
|
|
104
|
+
id: tc.id,
|
|
105
|
+
type: 'function',
|
|
106
|
+
function: {
|
|
107
|
+
name: tc.function.name,
|
|
108
|
+
arguments: tc.function.arguments,
|
|
109
|
+
},
|
|
110
|
+
})),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
// Handle regular text messages
|
|
114
|
+
else {
|
|
115
|
+
messages.push({
|
|
116
|
+
role,
|
|
117
|
+
content: msg.content || '',
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Convert tools to Mistral format - ensure parameters is always defined
|
|
122
|
+
let tools;
|
|
123
|
+
if (chat.tools && chat.tools.length > 0) {
|
|
124
|
+
tools = chat.tools.map((tool) => ({
|
|
125
|
+
type: 'function',
|
|
126
|
+
function: {
|
|
127
|
+
name: tool.function.name,
|
|
128
|
+
description: tool.function.description,
|
|
129
|
+
parameters: tool.function.parameters || {},
|
|
130
|
+
},
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
133
|
+
// Map tool choice - Mistral uses 'auto', 'none', 'any', 'required' or specific function
|
|
134
|
+
let toolChoice;
|
|
135
|
+
if (chat.toolChoice) {
|
|
136
|
+
if (typeof chat.toolChoice === 'object') {
|
|
137
|
+
toolChoice = chat.toolChoice;
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
const mapped = this.mapToolChoice(chat.toolChoice);
|
|
141
|
+
if (mapped === 'auto' || mapped === 'none' || mapped === 'any' || mapped === 'required') {
|
|
142
|
+
toolChoice = mapped;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const response = await mistral.chat.complete({
|
|
147
|
+
model,
|
|
148
|
+
messages: messages,
|
|
149
|
+
...(chat.temperature !== undefined && { temperature: chat.temperature }),
|
|
150
|
+
...(chat.maxTokens !== undefined && { maxTokens: chat.maxTokens }),
|
|
151
|
+
...(chat.topP !== undefined && { topP: chat.topP }),
|
|
152
|
+
...(chat.stopSequences !== undefined && { stop: chat.stopSequences }),
|
|
153
|
+
...(chat.frequencyPenalty !== undefined && { frequencyPenalty: chat.frequencyPenalty }),
|
|
154
|
+
...(chat.presencePenalty !== undefined && { presencePenalty: chat.presencePenalty }),
|
|
155
|
+
...(chat.seed !== undefined && { randomSeed: chat.seed }),
|
|
156
|
+
...(tools && { tools }),
|
|
157
|
+
...(toolChoice && { toolChoice }),
|
|
158
|
+
...(chat.responseFormat && {
|
|
159
|
+
responseFormat: typeof chat.responseFormat === 'string'
|
|
160
|
+
? { type: chat.responseFormat }
|
|
161
|
+
: chat.responseFormat,
|
|
162
|
+
}),
|
|
163
|
+
});
|
|
164
|
+
const choice = response.choices?.[0];
|
|
165
|
+
const message = choice?.message;
|
|
166
|
+
// Extract tool calls if present - normalize arguments to string
|
|
167
|
+
const toolCalls = message?.toolCalls?.map((tc) => ({
|
|
168
|
+
id: tc.id || '',
|
|
169
|
+
type: 'function',
|
|
170
|
+
function: {
|
|
171
|
+
name: tc.function?.name || '',
|
|
172
|
+
arguments: typeof tc.function?.arguments === 'string'
|
|
173
|
+
? tc.function.arguments
|
|
174
|
+
: JSON.stringify(tc.function?.arguments || {}),
|
|
175
|
+
},
|
|
176
|
+
}));
|
|
177
|
+
const promptTokens = response.usage?.promptTokens || 0;
|
|
178
|
+
const completionTokens = response.usage?.completionTokens || 0;
|
|
179
|
+
const totalTokens = response.usage?.totalTokens || 0;
|
|
180
|
+
const cost = this.calculateCost(model, promptTokens, completionTokens);
|
|
181
|
+
// Extract content as string - handle ContentChunk array
|
|
182
|
+
let contentStr;
|
|
183
|
+
if (message?.content) {
|
|
184
|
+
if (typeof message.content === 'string') {
|
|
185
|
+
contentStr = message.content;
|
|
186
|
+
}
|
|
187
|
+
else if (Array.isArray(message.content)) {
|
|
188
|
+
// ContentChunk array - extract text parts
|
|
189
|
+
contentStr = message.content
|
|
190
|
+
.map((chunk) => chunk.text || chunk.content || '')
|
|
191
|
+
.filter(Boolean)
|
|
192
|
+
.join('');
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
content: contentStr || undefined,
|
|
197
|
+
toolCalls: toolCalls && toolCalls.length > 0 ? toolCalls : undefined,
|
|
198
|
+
model: response.model || model,
|
|
199
|
+
finishReason: this.mapFinishReason(choice?.finishReason || 'stop'),
|
|
200
|
+
rawFinishReason: choice?.finishReason,
|
|
201
|
+
usage: {
|
|
202
|
+
promptTokens,
|
|
203
|
+
completionTokens,
|
|
204
|
+
totalTokens,
|
|
205
|
+
},
|
|
206
|
+
cost,
|
|
207
|
+
latencyMs: Date.now() - startTime,
|
|
208
|
+
raw: response,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
async handleEmbeddings(request) {
|
|
212
|
+
const startTime = Date.now();
|
|
213
|
+
const mistral = getMistralClient();
|
|
214
|
+
const { data: embedding, model } = request;
|
|
215
|
+
if (!model) {
|
|
216
|
+
throw new Error('Model is required for embeddings');
|
|
217
|
+
}
|
|
218
|
+
// Mistral expects 'inputs' as an array of strings
|
|
219
|
+
const inputs = Array.isArray(embedding.input) ? embedding.input : [embedding.input];
|
|
220
|
+
const response = await mistral.embeddings.create({
|
|
221
|
+
model,
|
|
222
|
+
inputs,
|
|
223
|
+
});
|
|
224
|
+
const embeddings = response.data?.map((d) => d.embedding || []) || [];
|
|
225
|
+
const promptTokens = response.usage?.promptTokens || 0;
|
|
226
|
+
const totalTokens = response.usage?.totalTokens || 0;
|
|
227
|
+
const cost = this.calculateCost(model, promptTokens, 0);
|
|
228
|
+
return {
|
|
229
|
+
embeddings,
|
|
230
|
+
model: response.model || model,
|
|
231
|
+
usage: {
|
|
232
|
+
promptTokens,
|
|
233
|
+
completionTokens: 0,
|
|
234
|
+
totalTokens,
|
|
235
|
+
},
|
|
236
|
+
cost,
|
|
237
|
+
latencyMs: Date.now() - startTime,
|
|
238
|
+
raw: response,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
async handleOCR(request) {
|
|
242
|
+
const startTime = Date.now();
|
|
243
|
+
const mistral = getMistralClient();
|
|
244
|
+
const { data: ocr, model } = request;
|
|
245
|
+
const ocrModel = model || 'mistral-ocr-latest';
|
|
246
|
+
let document;
|
|
247
|
+
if (ocr.documentUrl) {
|
|
248
|
+
document = {
|
|
249
|
+
type: 'document_url',
|
|
250
|
+
documentUrl: ocr.documentUrl,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
else if (ocr.imageUrl) {
|
|
254
|
+
document = {
|
|
255
|
+
type: 'image_url',
|
|
256
|
+
imageUrl: ocr.imageUrl,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
else if (ocr.base64) {
|
|
260
|
+
const mimeType = ocr.mimeType || 'application/pdf';
|
|
261
|
+
const isImage = mimeType.startsWith('image/');
|
|
262
|
+
if (isImage) {
|
|
263
|
+
document = {
|
|
264
|
+
type: 'image_url',
|
|
265
|
+
imageUrl: `data:${mimeType};base64,${ocr.base64}`,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
document = {
|
|
270
|
+
type: 'document_url',
|
|
271
|
+
documentUrl: `data:${mimeType};base64,${ocr.base64}`,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
throw new Error('OCR requires either documentUrl, imageUrl, or base64 input');
|
|
277
|
+
}
|
|
278
|
+
const response = await mistral.ocr.process({
|
|
279
|
+
model: ocrModel,
|
|
280
|
+
document,
|
|
281
|
+
...(ocr.tableFormat && { tableFormat: ocr.tableFormat }),
|
|
282
|
+
...(ocr.includeImageBase64 !== undefined && { includeImageBase64: ocr.includeImageBase64 }),
|
|
283
|
+
...(ocr.extractHeader !== undefined && { extractHeader: ocr.extractHeader }),
|
|
284
|
+
...(ocr.extractFooter !== undefined && { extractFooter: ocr.extractFooter }),
|
|
285
|
+
});
|
|
286
|
+
const pages = response.pages?.map((page) => ({
|
|
287
|
+
index: page.index,
|
|
288
|
+
markdown: page.markdown,
|
|
289
|
+
images: page.images,
|
|
290
|
+
tables: page.tables,
|
|
291
|
+
hyperlinks: page.hyperlinks,
|
|
292
|
+
header: page.header,
|
|
293
|
+
footer: page.footer,
|
|
294
|
+
dimensions: page.dimensions,
|
|
295
|
+
})) || [];
|
|
296
|
+
const combinedMarkdown = pages.map((p) => p.markdown).join('\n\n---\n\n');
|
|
297
|
+
return {
|
|
298
|
+
content: combinedMarkdown,
|
|
299
|
+
ocr: {
|
|
300
|
+
pages,
|
|
301
|
+
model: response.model || ocrModel,
|
|
302
|
+
documentAnnotation: response.documentAnnotation,
|
|
303
|
+
usageInfo: response.usageInfo,
|
|
304
|
+
},
|
|
305
|
+
model: response.model || ocrModel,
|
|
306
|
+
latencyMs: Date.now() - startTime,
|
|
307
|
+
raw: response,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-mistral-adapter.d.ts","sourceRoot":"","sources":["../../../../src/services/providers/tests/test-mistral-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,eAAe,CAAC"}
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import { MistralAdapter } from '../mistral-adapter.js';
|
|
3
|
+
const adapter = new MistralAdapter();
|
|
4
|
+
// Test delay to avoid rate limits
|
|
5
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
6
|
+
async function testChatCompletion() {
|
|
7
|
+
console.log('--- Testing Chat Completion ---');
|
|
8
|
+
const request = {
|
|
9
|
+
gate: 'test-gate',
|
|
10
|
+
type: 'chat',
|
|
11
|
+
model: 'mistral-small-latest',
|
|
12
|
+
data: {
|
|
13
|
+
messages: [
|
|
14
|
+
{
|
|
15
|
+
role: 'user',
|
|
16
|
+
content: 'What is the capital of France? Answer in one word.',
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
temperature: 0.7,
|
|
20
|
+
maxTokens: 50,
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
const response = await adapter.call(request);
|
|
24
|
+
console.log('Response:', response.content);
|
|
25
|
+
console.log('Model:', response.model);
|
|
26
|
+
console.log('Finish Reason:', response.finishReason);
|
|
27
|
+
console.log('Usage:', response.usage);
|
|
28
|
+
console.log('Latency:', response.latencyMs, 'ms');
|
|
29
|
+
console.log();
|
|
30
|
+
}
|
|
31
|
+
async function testChatWithSystemPrompt() {
|
|
32
|
+
console.log('--- Testing Chat with System Prompt ---');
|
|
33
|
+
const request = {
|
|
34
|
+
gate: 'test-gate',
|
|
35
|
+
type: 'chat',
|
|
36
|
+
model: 'mistral-small-latest',
|
|
37
|
+
data: {
|
|
38
|
+
systemPrompt: 'You are a helpful assistant that responds in JSON format only.',
|
|
39
|
+
messages: [
|
|
40
|
+
{
|
|
41
|
+
role: 'user',
|
|
42
|
+
content: 'List 3 colors.',
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
temperature: 0.5,
|
|
46
|
+
maxTokens: 100,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
const response = await adapter.call(request);
|
|
50
|
+
console.log('Response:', response.content);
|
|
51
|
+
console.log('Model:', response.model);
|
|
52
|
+
console.log();
|
|
53
|
+
}
|
|
54
|
+
async function testChatWithTools() {
|
|
55
|
+
console.log('--- Testing Chat with Tools ---');
|
|
56
|
+
const request = {
|
|
57
|
+
gate: 'test-gate',
|
|
58
|
+
type: 'chat',
|
|
59
|
+
model: 'mistral-small-latest',
|
|
60
|
+
data: {
|
|
61
|
+
messages: [
|
|
62
|
+
{
|
|
63
|
+
role: 'user',
|
|
64
|
+
content: 'What is the weather in Paris today?',
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
tools: [
|
|
68
|
+
{
|
|
69
|
+
type: 'function',
|
|
70
|
+
function: {
|
|
71
|
+
name: 'get_weather',
|
|
72
|
+
description: 'Get the current weather for a location',
|
|
73
|
+
parameters: {
|
|
74
|
+
type: 'object',
|
|
75
|
+
properties: {
|
|
76
|
+
location: {
|
|
77
|
+
type: 'string',
|
|
78
|
+
description: 'The city name',
|
|
79
|
+
},
|
|
80
|
+
unit: {
|
|
81
|
+
type: 'string',
|
|
82
|
+
enum: ['celsius', 'fahrenheit'],
|
|
83
|
+
description: 'Temperature unit',
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
required: ['location'],
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
toolChoice: 'auto',
|
|
92
|
+
maxTokens: 200,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
const response = await adapter.call(request);
|
|
96
|
+
console.log('Response content:', response.content);
|
|
97
|
+
console.log('Tool calls:', JSON.stringify(response.toolCalls, null, 2));
|
|
98
|
+
console.log('Finish Reason:', response.finishReason);
|
|
99
|
+
console.log();
|
|
100
|
+
}
|
|
101
|
+
async function testToolResponse() {
|
|
102
|
+
console.log('--- Testing Tool Response Flow ---');
|
|
103
|
+
// First, get the tool call
|
|
104
|
+
const initialRequest = {
|
|
105
|
+
gate: 'test-gate',
|
|
106
|
+
type: 'chat',
|
|
107
|
+
model: 'mistral-small-latest',
|
|
108
|
+
data: {
|
|
109
|
+
messages: [
|
|
110
|
+
{
|
|
111
|
+
role: 'user',
|
|
112
|
+
content: 'What is 25 multiplied by 4?',
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
tools: [
|
|
116
|
+
{
|
|
117
|
+
type: 'function',
|
|
118
|
+
function: {
|
|
119
|
+
name: 'calculator',
|
|
120
|
+
description: 'Perform mathematical calculations',
|
|
121
|
+
parameters: {
|
|
122
|
+
type: 'object',
|
|
123
|
+
properties: {
|
|
124
|
+
operation: {
|
|
125
|
+
type: 'string',
|
|
126
|
+
enum: ['add', 'subtract', 'multiply', 'divide'],
|
|
127
|
+
},
|
|
128
|
+
a: { type: 'number' },
|
|
129
|
+
b: { type: 'number' },
|
|
130
|
+
},
|
|
131
|
+
required: ['operation', 'a', 'b'],
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
toolChoice: 'required',
|
|
137
|
+
maxTokens: 200,
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
const initialResponse = await adapter.call(initialRequest);
|
|
141
|
+
console.log('Initial response tool calls:', JSON.stringify(initialResponse.toolCalls, null, 2));
|
|
142
|
+
if (initialResponse.toolCalls && initialResponse.toolCalls.length > 0) {
|
|
143
|
+
const toolCall = initialResponse.toolCalls[0];
|
|
144
|
+
await delay(2000);
|
|
145
|
+
const followUpRequest = {
|
|
146
|
+
gate: 'test-gate',
|
|
147
|
+
type: 'chat',
|
|
148
|
+
model: 'mistral-small-latest',
|
|
149
|
+
data: {
|
|
150
|
+
messages: [
|
|
151
|
+
{
|
|
152
|
+
role: 'user',
|
|
153
|
+
content: 'What is 25 multiplied by 4?',
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
role: 'assistant',
|
|
157
|
+
content: '',
|
|
158
|
+
toolCalls: initialResponse.toolCalls,
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
role: 'tool',
|
|
162
|
+
content: '100',
|
|
163
|
+
toolCallId: toolCall.id,
|
|
164
|
+
name: toolCall.function.name,
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
maxTokens: 200,
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
const followUpResponse = await adapter.call(followUpRequest);
|
|
171
|
+
console.log('Follow-up response:', followUpResponse.content);
|
|
172
|
+
}
|
|
173
|
+
console.log();
|
|
174
|
+
}
|
|
175
|
+
async function testEmbeddings() {
|
|
176
|
+
console.log('--- Testing Embeddings ---');
|
|
177
|
+
const request = {
|
|
178
|
+
gate: 'test-gate',
|
|
179
|
+
type: 'embeddings',
|
|
180
|
+
model: 'mistral-embed',
|
|
181
|
+
data: {
|
|
182
|
+
input: ['Hello, world!', 'How are you?'],
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
const response = await adapter.call(request);
|
|
186
|
+
console.log('Number of embeddings:', response.embeddings?.length);
|
|
187
|
+
console.log('First embedding dimensions:', response.embeddings?.[0]?.length);
|
|
188
|
+
console.log('Usage:', response.usage);
|
|
189
|
+
console.log('Latency:', response.latencyMs, 'ms');
|
|
190
|
+
console.log();
|
|
191
|
+
}
|
|
192
|
+
async function testVisionCapability() {
|
|
193
|
+
console.log('--- Testing Vision Capability (Pixtral) ---');
|
|
194
|
+
// Use a public image URL for testing
|
|
195
|
+
const request = {
|
|
196
|
+
gate: 'test-gate',
|
|
197
|
+
type: 'chat',
|
|
198
|
+
model: 'pixtral-large-2411',
|
|
199
|
+
data: {
|
|
200
|
+
messages: [
|
|
201
|
+
{
|
|
202
|
+
role: 'user',
|
|
203
|
+
content: 'Describe this image briefly.',
|
|
204
|
+
images: [
|
|
205
|
+
{
|
|
206
|
+
url: 'https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/Camponotus_flavomarginatus_ant.jpg/320px-Camponotus_flavomarginatus_ant.jpg',
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
maxTokens: 150,
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
try {
|
|
215
|
+
const response = await adapter.call(request);
|
|
216
|
+
console.log('Vision response:', response.content);
|
|
217
|
+
console.log('Model:', response.model);
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
console.log('Test failed (Mistral could not fetch image URL):', error.message.substring(0, 100));
|
|
221
|
+
}
|
|
222
|
+
console.log();
|
|
223
|
+
}
|
|
224
|
+
async function testResponseFormat() {
|
|
225
|
+
console.log('--- Testing JSON Response Format ---');
|
|
226
|
+
const request = {
|
|
227
|
+
gate: 'test-gate',
|
|
228
|
+
type: 'chat',
|
|
229
|
+
model: 'mistral-small-latest',
|
|
230
|
+
data: {
|
|
231
|
+
messages: [
|
|
232
|
+
{
|
|
233
|
+
role: 'user',
|
|
234
|
+
content: 'List 3 programming languages with their year of creation.',
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
responseFormat: { type: 'json_object' },
|
|
238
|
+
maxTokens: 200,
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
const response = await adapter.call(request);
|
|
242
|
+
console.log('JSON Response:', response.content);
|
|
243
|
+
console.log();
|
|
244
|
+
}
|
|
245
|
+
async function testMultiTurn() {
|
|
246
|
+
console.log('--- Testing Multi-turn Conversation ---');
|
|
247
|
+
const request = {
|
|
248
|
+
gate: 'test-gate',
|
|
249
|
+
type: 'chat',
|
|
250
|
+
model: 'mistral-small-latest',
|
|
251
|
+
data: {
|
|
252
|
+
messages: [
|
|
253
|
+
{
|
|
254
|
+
role: 'user',
|
|
255
|
+
content: 'My name is Alice.',
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
role: 'assistant',
|
|
259
|
+
content: 'Hello Alice! Nice to meet you.',
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
role: 'user',
|
|
263
|
+
content: 'What is my name?',
|
|
264
|
+
},
|
|
265
|
+
],
|
|
266
|
+
maxTokens: 50,
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
const response = await adapter.call(request);
|
|
270
|
+
console.log('Response:', response.content);
|
|
271
|
+
console.log();
|
|
272
|
+
}
|
|
273
|
+
async function testOCR() {
|
|
274
|
+
console.log('--- Testing OCR Capability ---');
|
|
275
|
+
const request = {
|
|
276
|
+
gate: 'test-gate',
|
|
277
|
+
type: 'ocr',
|
|
278
|
+
model: 'mistral-ocr-latest',
|
|
279
|
+
data: {
|
|
280
|
+
documentUrl: 'https://arxiv.org/pdf/2201.04234',
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
try {
|
|
284
|
+
const response = await adapter.call(request);
|
|
285
|
+
console.log('OCR Response (first 500 chars):', response.content?.substring(0, 500));
|
|
286
|
+
console.log('Model:', response.model);
|
|
287
|
+
console.log('Pages extracted:', response.ocr?.pages?.length || 0);
|
|
288
|
+
console.log('Latency:', response.latencyMs, 'ms');
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
console.log('Test failed:', error.message);
|
|
292
|
+
}
|
|
293
|
+
console.log();
|
|
294
|
+
}
|
|
295
|
+
async function testUnsupportedModality() {
|
|
296
|
+
console.log('--- Testing Unsupported Modality (Image Generation) ---');
|
|
297
|
+
const request = {
|
|
298
|
+
gate: 'test-gate',
|
|
299
|
+
type: 'image',
|
|
300
|
+
model: 'mistral-large-latest',
|
|
301
|
+
data: {
|
|
302
|
+
prompt: 'A sunset over the ocean',
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
try {
|
|
306
|
+
await adapter.call(request);
|
|
307
|
+
console.log('ERROR: Should have thrown an error');
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
console.log('Expected error:', error.message);
|
|
311
|
+
}
|
|
312
|
+
console.log();
|
|
313
|
+
}
|
|
314
|
+
async function runAllTests() {
|
|
315
|
+
console.log('=== Mistral Adapter Tests ===\n');
|
|
316
|
+
try {
|
|
317
|
+
await testChatCompletion();
|
|
318
|
+
await testChatWithSystemPrompt();
|
|
319
|
+
await testChatWithTools();
|
|
320
|
+
await testToolResponse();
|
|
321
|
+
await testEmbeddings();
|
|
322
|
+
await testVisionCapability();
|
|
323
|
+
await testOCR();
|
|
324
|
+
await testResponseFormat();
|
|
325
|
+
await testMultiTurn();
|
|
326
|
+
await testUnsupportedModality();
|
|
327
|
+
console.log('=== All tests completed ===');
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
console.error('Test failed:', error);
|
|
331
|
+
process.exit(1);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
runAllTests();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@layer-ai/core",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Core API routes and services for Layer AI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"nanoid": "^5.0.4",
|
|
37
37
|
"openai": "^4.24.0",
|
|
38
38
|
"pg": "^8.11.3",
|
|
39
|
-
"@layer-ai/sdk": "^0.5.
|
|
39
|
+
"@layer-ai/sdk": "^0.5.1"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/bcryptjs": "^2.4.6",
|