@layer-ai/core 2.0.18 → 2.0.20
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/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/lib/openai-conversion.d.ts +6 -0
- package/dist/lib/openai-conversion.d.ts.map +1 -0
- package/dist/lib/openai-conversion.js +215 -0
- package/dist/routes/tests/test-chat-streaming.js +80 -0
- package/dist/routes/tests/test-openai-endpoint.d.ts +3 -0
- package/dist/routes/tests/test-openai-endpoint.d.ts.map +1 -0
- package/dist/routes/tests/test-openai-endpoint.js +292 -0
- package/dist/routes/v1/chat-completions.d.ts +4 -0
- package/dist/routes/v1/chat-completions.d.ts.map +1 -0
- package/dist/routes/v1/chat-completions.js +262 -0
- package/dist/services/providers/mistral-adapter.d.ts +2 -0
- package/dist/services/providers/mistral-adapter.d.ts.map +1 -1
- package/dist/services/providers/mistral-adapter.js +205 -0
- package/dist/services/providers/tests/test-mistral-streaming.d.ts +2 -0
- package/dist/services/providers/tests/test-mistral-streaming.d.ts.map +1 -0
- package/dist/services/providers/tests/test-mistral-streaming.js +139 -0
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export { default as authRouter } from './routes/v1/auth.js';
|
|
|
2
2
|
export { default as gatesRouter } from './routes/v1/gates.js';
|
|
3
3
|
export { default as keysRouter } from './routes/v1/keys.js';
|
|
4
4
|
export { default as logsRouter } from './routes/v1/logs.js';
|
|
5
|
+
export { default as chatCompletionsRouter } from './routes/v1/chat-completions.js';
|
|
5
6
|
export { default as completeRouter } from './routes/v2/complete.js';
|
|
6
7
|
export { default as chatRouter } from './routes/v3/chat.js';
|
|
7
8
|
export { default as imageRouter } from './routes/v3/image.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AAGnF,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAGpE,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AACxE,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAG1D,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACpD,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAG3C,OAAO,EAAE,EAAE,EAAE,MAAM,sBAAsB,CAAC;AAC1C,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAGrD,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC9E,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAGnD,eAAO,MAAM,gBAAgB,GAAU,QAAQ,MAAM,KAAG,OAAO,CAAC,MAAM,CAGrE,CAAC;AAEF,eAAO,MAAM,wBAAwB,GAAU,QAAQ,MAAM,KAAG,OAAO,CAAC,IAAI,CAG3E,CAAC;AAGF,cAAc,6BAA6B,CAAC;AAG5C,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,QAAQ,EAAE,WAAW,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ export { default as authRouter } from './routes/v1/auth.js';
|
|
|
3
3
|
export { default as gatesRouter } from './routes/v1/gates.js';
|
|
4
4
|
export { default as keysRouter } from './routes/v1/keys.js';
|
|
5
5
|
export { default as logsRouter } from './routes/v1/logs.js';
|
|
6
|
+
export { default as chatCompletionsRouter } from './routes/v1/chat-completions.js';
|
|
6
7
|
// v2 routes
|
|
7
8
|
export { default as completeRouter } from './routes/v2/complete.js';
|
|
8
9
|
// v3 routes
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { OpenAIChatCompletionRequest, OpenAIChatCompletionResponse, OpenAIChatCompletionChunk } from '@layer-ai/sdk';
|
|
2
|
+
import type { LayerRequest, LayerResponse } from '@layer-ai/sdk';
|
|
3
|
+
export declare function convertOpenAIRequestToLayer(openaiReq: OpenAIChatCompletionRequest, gateId: string): LayerRequest;
|
|
4
|
+
export declare function convertLayerResponseToOpenAI(layerResp: LayerResponse, requestId?: string): OpenAIChatCompletionResponse;
|
|
5
|
+
export declare function convertLayerChunkToOpenAI(layerChunk: LayerResponse, requestId: string, created: number): OpenAIChatCompletionChunk;
|
|
6
|
+
//# sourceMappingURL=openai-conversion.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openai-conversion.d.ts","sourceRoot":"","sources":["../../src/lib/openai-conversion.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,2BAA2B,EAK3B,4BAA4B,EAC5B,yBAAyB,EAE1B,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EACV,YAAY,EACZ,aAAa,EAOd,MAAM,eAAe,CAAC;AAqFvB,wBAAgB,2BAA2B,CACzC,SAAS,EAAE,2BAA2B,EACtC,MAAM,EAAE,MAAM,GACb,YAAY,CA2Cd;AAwCD,wBAAgB,4BAA4B,CAC1C,SAAS,EAAE,aAAa,EACxB,SAAS,CAAC,EAAE,MAAM,GACjB,4BAA4B,CA+B9B;AAED,wBAAgB,yBAAyB,CACvC,UAAU,EAAE,aAAa,EACzB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,GACd,yBAAyB,CA2C3B"}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { nanoid } from 'nanoid';
|
|
2
|
+
function convertMessage(openaiMsg) {
|
|
3
|
+
const layerMsg = {
|
|
4
|
+
role: openaiMsg.role,
|
|
5
|
+
};
|
|
6
|
+
if (typeof openaiMsg.content === 'string') {
|
|
7
|
+
layerMsg.content = openaiMsg.content;
|
|
8
|
+
}
|
|
9
|
+
else if (Array.isArray(openaiMsg.content)) {
|
|
10
|
+
const textParts = [];
|
|
11
|
+
const imageParts = [];
|
|
12
|
+
for (const part of openaiMsg.content) {
|
|
13
|
+
if (part.type === 'text') {
|
|
14
|
+
textParts.push(part.text);
|
|
15
|
+
}
|
|
16
|
+
else if (part.type === 'image_url') {
|
|
17
|
+
imageParts.push({
|
|
18
|
+
url: part.image_url.url,
|
|
19
|
+
detail: part.image_url.detail,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (textParts.length > 0) {
|
|
24
|
+
layerMsg.content = textParts.join('\n');
|
|
25
|
+
}
|
|
26
|
+
if (imageParts.length > 0) {
|
|
27
|
+
layerMsg.images = imageParts;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (openaiMsg.tool_calls) {
|
|
31
|
+
layerMsg.toolCalls = openaiMsg.tool_calls.map(tc => ({
|
|
32
|
+
id: tc.id,
|
|
33
|
+
type: 'function',
|
|
34
|
+
function: {
|
|
35
|
+
name: tc.function.name,
|
|
36
|
+
arguments: tc.function.arguments,
|
|
37
|
+
},
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
if (openaiMsg.tool_call_id) {
|
|
41
|
+
layerMsg.toolCallId = openaiMsg.tool_call_id;
|
|
42
|
+
}
|
|
43
|
+
if (openaiMsg.name) {
|
|
44
|
+
layerMsg.name = openaiMsg.name;
|
|
45
|
+
}
|
|
46
|
+
return layerMsg;
|
|
47
|
+
}
|
|
48
|
+
function convertTool(openaiTool) {
|
|
49
|
+
return {
|
|
50
|
+
type: 'function',
|
|
51
|
+
function: {
|
|
52
|
+
name: openaiTool.function.name,
|
|
53
|
+
description: openaiTool.function.description,
|
|
54
|
+
parameters: openaiTool.function.parameters,
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function convertToolChoice(openaiToolChoice) {
|
|
59
|
+
if (!openaiToolChoice)
|
|
60
|
+
return undefined;
|
|
61
|
+
if (typeof openaiToolChoice === 'string')
|
|
62
|
+
return openaiToolChoice;
|
|
63
|
+
return openaiToolChoice;
|
|
64
|
+
}
|
|
65
|
+
function convertResponseFormat(openaiFormat) {
|
|
66
|
+
if (!openaiFormat)
|
|
67
|
+
return undefined;
|
|
68
|
+
if (openaiFormat.type === 'json_schema' && openaiFormat.json_schema) {
|
|
69
|
+
return {
|
|
70
|
+
type: 'json_schema',
|
|
71
|
+
json_schema: openaiFormat.json_schema,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return openaiFormat.type;
|
|
75
|
+
}
|
|
76
|
+
export function convertOpenAIRequestToLayer(openaiReq, gateId) {
|
|
77
|
+
let systemPrompt;
|
|
78
|
+
const messages = [];
|
|
79
|
+
for (const msg of openaiReq.messages) {
|
|
80
|
+
if (msg.role === 'system' && typeof msg.content === 'string') {
|
|
81
|
+
systemPrompt = msg.content;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
messages.push(convertMessage(msg));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const layerRequest = {
|
|
88
|
+
gateId,
|
|
89
|
+
type: 'chat',
|
|
90
|
+
model: openaiReq.model,
|
|
91
|
+
data: {
|
|
92
|
+
messages,
|
|
93
|
+
systemPrompt,
|
|
94
|
+
temperature: openaiReq.temperature,
|
|
95
|
+
maxTokens: openaiReq.max_tokens || openaiReq.max_completion_tokens,
|
|
96
|
+
topP: openaiReq.top_p,
|
|
97
|
+
stream: openaiReq.stream,
|
|
98
|
+
stopSequences: typeof openaiReq.stop === 'string' ? [openaiReq.stop] : openaiReq.stop,
|
|
99
|
+
frequencyPenalty: openaiReq.frequency_penalty,
|
|
100
|
+
presencePenalty: openaiReq.presence_penalty,
|
|
101
|
+
seed: openaiReq.seed,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
if (openaiReq.tools && openaiReq.tools.length > 0) {
|
|
105
|
+
layerRequest.data.tools = openaiReq.tools.map(convertTool);
|
|
106
|
+
}
|
|
107
|
+
if (openaiReq.tool_choice) {
|
|
108
|
+
layerRequest.data.toolChoice = convertToolChoice(openaiReq.tool_choice);
|
|
109
|
+
}
|
|
110
|
+
if (openaiReq.response_format) {
|
|
111
|
+
layerRequest.data.responseFormat = convertResponseFormat(openaiReq.response_format);
|
|
112
|
+
}
|
|
113
|
+
return layerRequest;
|
|
114
|
+
}
|
|
115
|
+
function convertFinishReason(layerReason) {
|
|
116
|
+
if (!layerReason)
|
|
117
|
+
return null;
|
|
118
|
+
switch (layerReason) {
|
|
119
|
+
case 'completed':
|
|
120
|
+
return 'stop';
|
|
121
|
+
case 'length_limit':
|
|
122
|
+
return 'length';
|
|
123
|
+
case 'tool_call':
|
|
124
|
+
return 'tool_calls';
|
|
125
|
+
case 'filtered':
|
|
126
|
+
return 'content_filter';
|
|
127
|
+
default:
|
|
128
|
+
return 'stop';
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function convertToolCallsToOpenAI(layerToolCalls) {
|
|
132
|
+
if (!layerToolCalls || layerToolCalls.length === 0)
|
|
133
|
+
return undefined;
|
|
134
|
+
return layerToolCalls.map(tc => ({
|
|
135
|
+
id: tc.id,
|
|
136
|
+
type: 'function',
|
|
137
|
+
function: {
|
|
138
|
+
name: tc.function.name,
|
|
139
|
+
arguments: tc.function.arguments,
|
|
140
|
+
},
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
143
|
+
function convertUsage(layerUsage) {
|
|
144
|
+
return {
|
|
145
|
+
prompt_tokens: layerUsage?.promptTokens || 0,
|
|
146
|
+
completion_tokens: layerUsage?.completionTokens || 0,
|
|
147
|
+
total_tokens: layerUsage?.totalTokens || 0,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
export function convertLayerResponseToOpenAI(layerResp, requestId) {
|
|
151
|
+
const id = requestId || layerResp.id || `chatcmpl-${nanoid()}`;
|
|
152
|
+
const created = layerResp.created || Math.floor(Date.now() / 1000);
|
|
153
|
+
const message = {
|
|
154
|
+
role: 'assistant',
|
|
155
|
+
content: layerResp.content || undefined,
|
|
156
|
+
};
|
|
157
|
+
const toolCalls = convertToolCallsToOpenAI(layerResp.toolCalls);
|
|
158
|
+
if (toolCalls) {
|
|
159
|
+
message.tool_calls = toolCalls;
|
|
160
|
+
}
|
|
161
|
+
const response = {
|
|
162
|
+
id,
|
|
163
|
+
object: 'chat.completion',
|
|
164
|
+
created,
|
|
165
|
+
model: layerResp.model || 'unknown',
|
|
166
|
+
choices: [
|
|
167
|
+
{
|
|
168
|
+
index: 0,
|
|
169
|
+
message,
|
|
170
|
+
finish_reason: convertFinishReason(layerResp.finishReason),
|
|
171
|
+
logprobs: null,
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
usage: convertUsage(layerResp.usage),
|
|
175
|
+
};
|
|
176
|
+
return response;
|
|
177
|
+
}
|
|
178
|
+
export function convertLayerChunkToOpenAI(layerChunk, requestId, created) {
|
|
179
|
+
const delta = {};
|
|
180
|
+
if (layerChunk.content && !layerChunk.finishReason) {
|
|
181
|
+
delta.role = 'assistant';
|
|
182
|
+
}
|
|
183
|
+
if (layerChunk.content) {
|
|
184
|
+
delta.content = layerChunk.content;
|
|
185
|
+
}
|
|
186
|
+
if (layerChunk.toolCalls && layerChunk.toolCalls.length > 0) {
|
|
187
|
+
delta.tool_calls = layerChunk.toolCalls.map((tc, index) => ({
|
|
188
|
+
index,
|
|
189
|
+
id: tc.id,
|
|
190
|
+
type: 'function',
|
|
191
|
+
function: {
|
|
192
|
+
name: tc.function.name,
|
|
193
|
+
arguments: tc.function.arguments,
|
|
194
|
+
},
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
const chunk = {
|
|
198
|
+
id: requestId,
|
|
199
|
+
object: 'chat.completion.chunk',
|
|
200
|
+
created,
|
|
201
|
+
model: layerChunk.model || 'unknown',
|
|
202
|
+
choices: [
|
|
203
|
+
{
|
|
204
|
+
index: 0,
|
|
205
|
+
delta,
|
|
206
|
+
finish_reason: convertFinishReason(layerChunk.finishReason),
|
|
207
|
+
logprobs: null,
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
};
|
|
211
|
+
if (layerChunk.usage && layerChunk.finishReason) {
|
|
212
|
+
chunk.usage = convertUsage(layerChunk.usage);
|
|
213
|
+
}
|
|
214
|
+
return chunk;
|
|
215
|
+
}
|
|
@@ -392,6 +392,84 @@ async function testGeminiToolCallsStreaming() {
|
|
|
392
392
|
console.log(' ⚠️ Tool calls may not have been invoked (model chose not to use tools)\n');
|
|
393
393
|
}
|
|
394
394
|
}
|
|
395
|
+
// Test 10: Mistral streaming
|
|
396
|
+
async function testMistralStreaming() {
|
|
397
|
+
console.log('Test 10: Mistral Streaming');
|
|
398
|
+
console.log('-'.repeat(80));
|
|
399
|
+
const request = {
|
|
400
|
+
gateId: 'test-gate',
|
|
401
|
+
model: 'mistral-small-2501',
|
|
402
|
+
type: 'chat',
|
|
403
|
+
data: {
|
|
404
|
+
messages: [
|
|
405
|
+
{ role: 'user', content: 'Say "mistral test passed" and nothing else.' }
|
|
406
|
+
],
|
|
407
|
+
maxTokens: 20,
|
|
408
|
+
stream: true,
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
let chunkCount = 0;
|
|
412
|
+
let fullContent = '';
|
|
413
|
+
for await (const chunk of callAdapterStream(request)) {
|
|
414
|
+
chunkCount++;
|
|
415
|
+
if (chunk.content) {
|
|
416
|
+
fullContent += chunk.content;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
console.log(` Chunks received: ${chunkCount}`);
|
|
420
|
+
console.log(` Content: ${fullContent.trim()}`);
|
|
421
|
+
console.log(' ✅ Mistral streaming test passed\n');
|
|
422
|
+
}
|
|
423
|
+
// Test 11: Mistral with tool calls streaming
|
|
424
|
+
async function testMistralToolCallsStreaming() {
|
|
425
|
+
console.log('Test 11: Mistral Tool Calls Streaming');
|
|
426
|
+
console.log('-'.repeat(80));
|
|
427
|
+
const request = {
|
|
428
|
+
gateId: 'test-gate',
|
|
429
|
+
model: 'mistral-small-2501',
|
|
430
|
+
type: 'chat',
|
|
431
|
+
data: {
|
|
432
|
+
messages: [
|
|
433
|
+
{ role: 'user', content: 'What is the weather in Berlin?' }
|
|
434
|
+
],
|
|
435
|
+
tools: [
|
|
436
|
+
{
|
|
437
|
+
type: 'function',
|
|
438
|
+
function: {
|
|
439
|
+
name: 'get_weather',
|
|
440
|
+
description: 'Get weather for a location',
|
|
441
|
+
parameters: {
|
|
442
|
+
type: 'object',
|
|
443
|
+
properties: {
|
|
444
|
+
location: { type: 'string' },
|
|
445
|
+
},
|
|
446
|
+
required: ['location'],
|
|
447
|
+
},
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
],
|
|
451
|
+
stream: true,
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
let toolCallsFound = false;
|
|
455
|
+
let finishReason = null;
|
|
456
|
+
for await (const chunk of callAdapterStream(request)) {
|
|
457
|
+
if (chunk.toolCalls && chunk.toolCalls.length > 0) {
|
|
458
|
+
toolCallsFound = true;
|
|
459
|
+
}
|
|
460
|
+
if (chunk.finishReason) {
|
|
461
|
+
finishReason = chunk.finishReason;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
console.log(` Tool calls found: ${toolCallsFound}`);
|
|
465
|
+
console.log(` Finish reason: ${finishReason}`);
|
|
466
|
+
if (toolCallsFound) {
|
|
467
|
+
console.log(' ✅ Mistral tool calls streaming test passed\n');
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
console.log(' ⚠️ Tool calls may not have been invoked (model chose not to use tools)\n');
|
|
471
|
+
}
|
|
472
|
+
}
|
|
395
473
|
// Run all tests
|
|
396
474
|
(async () => {
|
|
397
475
|
try {
|
|
@@ -404,6 +482,8 @@ async function testGeminiToolCallsStreaming() {
|
|
|
404
482
|
await testMultiProviderFallback();
|
|
405
483
|
await testGeminiStreaming();
|
|
406
484
|
await testGeminiToolCallsStreaming();
|
|
485
|
+
await testMistralStreaming();
|
|
486
|
+
await testMistralToolCallsStreaming();
|
|
407
487
|
console.log('='.repeat(80));
|
|
408
488
|
console.log('✅ ALL STREAMING ROUTE TESTS PASSED');
|
|
409
489
|
console.log('='.repeat(80));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-openai-endpoint.d.ts","sourceRoot":"","sources":["../../../src/routes/tests/test-openai-endpoint.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
console.log('='.repeat(80));
|
|
3
|
+
console.log('OPENAI-COMPATIBLE ENDPOINT TESTS');
|
|
4
|
+
console.log('='.repeat(80));
|
|
5
|
+
console.log('');
|
|
6
|
+
const BASE_URL = process.env.API_URL || 'http://localhost:3004';
|
|
7
|
+
const API_KEY = process.env.LAYER_API_KEY;
|
|
8
|
+
const GATE_ID = process.env.TEST_GATE_ID;
|
|
9
|
+
if (!API_KEY) {
|
|
10
|
+
console.error('❌ Error: LAYER_API_KEY environment variable not set');
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
if (!GATE_ID) {
|
|
14
|
+
console.error('❌ Error: TEST_GATE_ID environment variable not set');
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
async function testNonStreamingBasic() {
|
|
18
|
+
console.log('Test 1: Non-streaming basic chat completion');
|
|
19
|
+
console.log('-'.repeat(80));
|
|
20
|
+
const request = {
|
|
21
|
+
model: 'gpt-4o',
|
|
22
|
+
messages: [
|
|
23
|
+
{ role: 'user', content: 'Say "test passed" and nothing else.' }
|
|
24
|
+
],
|
|
25
|
+
max_tokens: 10,
|
|
26
|
+
gateId: GATE_ID,
|
|
27
|
+
};
|
|
28
|
+
const response = await fetch(`${BASE_URL}/v1/chat/completions`, {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: {
|
|
31
|
+
'Content-Type': 'application/json',
|
|
32
|
+
'Authorization': `Bearer ${API_KEY}`,
|
|
33
|
+
},
|
|
34
|
+
body: JSON.stringify(request),
|
|
35
|
+
});
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
const error = await response.json();
|
|
38
|
+
throw new Error(`Request failed: ${JSON.stringify(error)}`);
|
|
39
|
+
}
|
|
40
|
+
const data = await response.json();
|
|
41
|
+
console.log(' Response ID:', data.id);
|
|
42
|
+
console.log(' Model:', data.model);
|
|
43
|
+
console.log(' Content:', data.choices[0].message.content);
|
|
44
|
+
console.log(' Finish reason:', data.choices[0].finish_reason);
|
|
45
|
+
console.log(' Usage:', data.usage);
|
|
46
|
+
console.log(' ✅ Non-streaming basic test passed\n');
|
|
47
|
+
}
|
|
48
|
+
async function testNonStreamingWithGateIdInHeader() {
|
|
49
|
+
console.log('Test 2: Non-streaming with gateId in header');
|
|
50
|
+
console.log('-'.repeat(80));
|
|
51
|
+
const request = {
|
|
52
|
+
model: 'gpt-4o',
|
|
53
|
+
messages: [
|
|
54
|
+
{ role: 'user', content: 'Say "header test passed" and nothing else.' }
|
|
55
|
+
],
|
|
56
|
+
max_tokens: 10,
|
|
57
|
+
};
|
|
58
|
+
const response = await fetch(`${BASE_URL}/v1/chat/completions`, {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
headers: {
|
|
61
|
+
'Content-Type': 'application/json',
|
|
62
|
+
'Authorization': `Bearer ${API_KEY}`,
|
|
63
|
+
'X-Layer-Gate-Id': GATE_ID,
|
|
64
|
+
},
|
|
65
|
+
body: JSON.stringify(request),
|
|
66
|
+
});
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
const error = await response.json();
|
|
69
|
+
throw new Error(`Request failed: ${JSON.stringify(error)}`);
|
|
70
|
+
}
|
|
71
|
+
const data = await response.json();
|
|
72
|
+
console.log(' Content:', data.choices[0].message.content);
|
|
73
|
+
console.log(' ✅ Header gateId test passed\n');
|
|
74
|
+
}
|
|
75
|
+
async function testStreamingBasic() {
|
|
76
|
+
console.log('Test 3: Streaming basic chat completion');
|
|
77
|
+
console.log('-'.repeat(80));
|
|
78
|
+
const request = {
|
|
79
|
+
model: 'gpt-4o',
|
|
80
|
+
messages: [
|
|
81
|
+
{ role: 'user', content: 'Count from 1 to 3, one number per line.' }
|
|
82
|
+
],
|
|
83
|
+
max_tokens: 50,
|
|
84
|
+
stream: true,
|
|
85
|
+
gateId: GATE_ID,
|
|
86
|
+
};
|
|
87
|
+
const response = await fetch(`${BASE_URL}/v1/chat/completions`, {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
headers: {
|
|
90
|
+
'Content-Type': 'application/json',
|
|
91
|
+
'Authorization': `Bearer ${API_KEY}`,
|
|
92
|
+
},
|
|
93
|
+
body: JSON.stringify(request),
|
|
94
|
+
});
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
const error = await response.json();
|
|
97
|
+
throw new Error(`Request failed: ${JSON.stringify(error)}`);
|
|
98
|
+
}
|
|
99
|
+
let chunkCount = 0;
|
|
100
|
+
let fullContent = '';
|
|
101
|
+
let finalUsage = null;
|
|
102
|
+
const reader = response.body?.getReader();
|
|
103
|
+
const decoder = new TextDecoder();
|
|
104
|
+
if (!reader) {
|
|
105
|
+
throw new Error('No response body reader');
|
|
106
|
+
}
|
|
107
|
+
while (true) {
|
|
108
|
+
const { done, value } = await reader.read();
|
|
109
|
+
if (done)
|
|
110
|
+
break;
|
|
111
|
+
const text = decoder.decode(value);
|
|
112
|
+
const lines = text.split('\n').filter(line => line.trim().startsWith('data: '));
|
|
113
|
+
for (const line of lines) {
|
|
114
|
+
const data = line.replace('data: ', '').trim();
|
|
115
|
+
if (data === '[DONE]') {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const chunk = JSON.parse(data);
|
|
120
|
+
chunkCount++;
|
|
121
|
+
if (chunk.choices[0].delta.content) {
|
|
122
|
+
fullContent += chunk.choices[0].delta.content;
|
|
123
|
+
}
|
|
124
|
+
if (chunk.usage) {
|
|
125
|
+
finalUsage = chunk.usage;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch (e) {
|
|
129
|
+
// Skip invalid JSON
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
console.log(' Chunks received:', chunkCount);
|
|
134
|
+
console.log(' Full content:', fullContent.trim());
|
|
135
|
+
console.log(' Final usage:', finalUsage);
|
|
136
|
+
console.log(' ✅ Streaming basic test passed\n');
|
|
137
|
+
}
|
|
138
|
+
async function testWithToolCalls() {
|
|
139
|
+
console.log('Test 4: Non-streaming with tool calls');
|
|
140
|
+
console.log('-'.repeat(80));
|
|
141
|
+
const request = {
|
|
142
|
+
model: 'gpt-4o',
|
|
143
|
+
messages: [
|
|
144
|
+
{ role: 'user', content: 'What is the weather in Paris?' }
|
|
145
|
+
],
|
|
146
|
+
tools: [
|
|
147
|
+
{
|
|
148
|
+
type: 'function',
|
|
149
|
+
function: {
|
|
150
|
+
name: 'get_weather',
|
|
151
|
+
description: 'Get the current weather for a location',
|
|
152
|
+
parameters: {
|
|
153
|
+
type: 'object',
|
|
154
|
+
properties: {
|
|
155
|
+
location: {
|
|
156
|
+
type: 'string',
|
|
157
|
+
description: 'The city and state, e.g. Paris, France',
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
required: ['location'],
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
tool_choice: 'auto',
|
|
166
|
+
gateId: GATE_ID,
|
|
167
|
+
};
|
|
168
|
+
const response = await fetch(`${BASE_URL}/v1/chat/completions`, {
|
|
169
|
+
method: 'POST',
|
|
170
|
+
headers: {
|
|
171
|
+
'Content-Type': 'application/json',
|
|
172
|
+
'Authorization': `Bearer ${API_KEY}`,
|
|
173
|
+
},
|
|
174
|
+
body: JSON.stringify(request),
|
|
175
|
+
});
|
|
176
|
+
if (!response.ok) {
|
|
177
|
+
const error = await response.json();
|
|
178
|
+
throw new Error(`Request failed: ${JSON.stringify(error)}`);
|
|
179
|
+
}
|
|
180
|
+
const data = await response.json();
|
|
181
|
+
console.log(' Finish reason:', data.choices[0].finish_reason);
|
|
182
|
+
if (data.choices[0].message.tool_calls && data.choices[0].message.tool_calls.length > 0) {
|
|
183
|
+
console.log(' Tool calls:', JSON.stringify(data.choices[0].message.tool_calls, null, 2));
|
|
184
|
+
console.log(' ✅ Tool calls test passed\n');
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
console.log(' ⚠️ No tool calls received (model may have chosen not to use tools)\n');
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async function testClaudeModel() {
|
|
191
|
+
console.log('Test 5: OpenAI format with Claude model');
|
|
192
|
+
console.log('-'.repeat(80));
|
|
193
|
+
const request = {
|
|
194
|
+
model: 'claude-3-7-sonnet-20250219',
|
|
195
|
+
messages: [
|
|
196
|
+
{ role: 'user', content: 'Say "claude via openai format works" and nothing else.' }
|
|
197
|
+
],
|
|
198
|
+
max_tokens: 20,
|
|
199
|
+
gateId: GATE_ID,
|
|
200
|
+
};
|
|
201
|
+
const response = await fetch(`${BASE_URL}/v1/chat/completions`, {
|
|
202
|
+
method: 'POST',
|
|
203
|
+
headers: {
|
|
204
|
+
'Content-Type': 'application/json',
|
|
205
|
+
'Authorization': `Bearer ${API_KEY}`,
|
|
206
|
+
},
|
|
207
|
+
body: JSON.stringify(request),
|
|
208
|
+
});
|
|
209
|
+
if (!response.ok) {
|
|
210
|
+
const error = await response.json();
|
|
211
|
+
throw new Error(`Request failed: ${JSON.stringify(error)}`);
|
|
212
|
+
}
|
|
213
|
+
const data = await response.json();
|
|
214
|
+
console.log(' Model:', data.model);
|
|
215
|
+
console.log(' Content:', data.choices[0].message.content);
|
|
216
|
+
console.log(' ✅ Claude model test passed\n');
|
|
217
|
+
}
|
|
218
|
+
async function testGeminiModel() {
|
|
219
|
+
console.log('Test 6: OpenAI format with Gemini model');
|
|
220
|
+
console.log('-'.repeat(80));
|
|
221
|
+
const request = {
|
|
222
|
+
model: 'gemini-2.0-flash',
|
|
223
|
+
messages: [
|
|
224
|
+
{ role: 'user', content: 'Say "gemini via openai format works" and nothing else.' }
|
|
225
|
+
],
|
|
226
|
+
max_tokens: 20,
|
|
227
|
+
gateId: GATE_ID,
|
|
228
|
+
};
|
|
229
|
+
const response = await fetch(`${BASE_URL}/v1/chat/completions`, {
|
|
230
|
+
method: 'POST',
|
|
231
|
+
headers: {
|
|
232
|
+
'Content-Type': 'application/json',
|
|
233
|
+
'Authorization': `Bearer ${API_KEY}`,
|
|
234
|
+
},
|
|
235
|
+
body: JSON.stringify(request),
|
|
236
|
+
});
|
|
237
|
+
if (!response.ok) {
|
|
238
|
+
const error = await response.json();
|
|
239
|
+
throw new Error(`Request failed: ${JSON.stringify(error)}`);
|
|
240
|
+
}
|
|
241
|
+
const data = await response.json();
|
|
242
|
+
console.log(' Model:', data.model);
|
|
243
|
+
console.log(' Content:', data.choices[0].message.content);
|
|
244
|
+
console.log(' ✅ Gemini model test passed\n');
|
|
245
|
+
}
|
|
246
|
+
async function testMistralModel() {
|
|
247
|
+
console.log('Test 7: OpenAI format with Mistral model');
|
|
248
|
+
console.log('-'.repeat(80));
|
|
249
|
+
const request = {
|
|
250
|
+
model: 'mistral-small-2501',
|
|
251
|
+
messages: [
|
|
252
|
+
{ role: 'user', content: 'Say "mistral via openai format works" and nothing else.' }
|
|
253
|
+
],
|
|
254
|
+
max_tokens: 20,
|
|
255
|
+
gateId: GATE_ID,
|
|
256
|
+
};
|
|
257
|
+
const response = await fetch(`${BASE_URL}/v1/chat/completions`, {
|
|
258
|
+
method: 'POST',
|
|
259
|
+
headers: {
|
|
260
|
+
'Content-Type': 'application/json',
|
|
261
|
+
'Authorization': `Bearer ${API_KEY}`,
|
|
262
|
+
},
|
|
263
|
+
body: JSON.stringify(request),
|
|
264
|
+
});
|
|
265
|
+
if (!response.ok) {
|
|
266
|
+
const error = await response.json();
|
|
267
|
+
throw new Error(`Request failed: ${JSON.stringify(error)}`);
|
|
268
|
+
}
|
|
269
|
+
const data = await response.json();
|
|
270
|
+
console.log(' Model:', data.model);
|
|
271
|
+
console.log(' Content:', data.choices[0].message.content);
|
|
272
|
+
console.log(' ✅ Mistral model test passed\n');
|
|
273
|
+
}
|
|
274
|
+
(async () => {
|
|
275
|
+
try {
|
|
276
|
+
await testNonStreamingBasic();
|
|
277
|
+
await testNonStreamingWithGateIdInHeader();
|
|
278
|
+
await testStreamingBasic();
|
|
279
|
+
await testWithToolCalls();
|
|
280
|
+
await testClaudeModel();
|
|
281
|
+
await testGeminiModel();
|
|
282
|
+
await testMistralModel();
|
|
283
|
+
console.log('='.repeat(80));
|
|
284
|
+
console.log('✅ ALL OPENAI-COMPATIBLE ENDPOINT TESTS PASSED');
|
|
285
|
+
console.log('='.repeat(80));
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
console.error('❌ Test failed:', error);
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
})();
|
|
292
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat-completions.d.ts","sourceRoot":"","sources":["../../../src/routes/v1/chat-completions.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,SAAS,CAAC;AAapD,QAAA,MAAM,MAAM,EAAE,UAAqB,CAAC;AA6RpC,eAAe,MAAM,CAAC"}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { nanoid } from 'nanoid';
|
|
3
|
+
import { db } from '../../lib/db/postgres.js';
|
|
4
|
+
import { authenticate } from '../../middleware/auth.js';
|
|
5
|
+
import { convertOpenAIRequestToLayer, convertLayerResponseToOpenAI, convertLayerChunkToOpenAI, } from '../../lib/openai-conversion.js';
|
|
6
|
+
import { resolveFinalRequest } from '../v3/chat.js';
|
|
7
|
+
import { callAdapter, callAdapterStream } from '../../lib/provider-factory.js';
|
|
8
|
+
const router = Router();
|
|
9
|
+
async function* executeWithRoutingStream(gateConfig, request, userId) {
|
|
10
|
+
yield* callAdapterStream(request, userId);
|
|
11
|
+
}
|
|
12
|
+
async function executeWithRouting(gateConfig, request, userId) {
|
|
13
|
+
const result = await callAdapter(request, userId);
|
|
14
|
+
return { result, modelUsed: request.model };
|
|
15
|
+
}
|
|
16
|
+
router.post('/', authenticate, async (req, res) => {
|
|
17
|
+
const startTime = Date.now();
|
|
18
|
+
if (!req.userId) {
|
|
19
|
+
const error = {
|
|
20
|
+
error: {
|
|
21
|
+
message: 'Missing user ID',
|
|
22
|
+
type: 'authentication_error',
|
|
23
|
+
code: 'unauthorized',
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
res.status(401).json(error);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const userId = req.userId;
|
|
30
|
+
let gateConfig = null;
|
|
31
|
+
let layerRequest = null;
|
|
32
|
+
try {
|
|
33
|
+
const openaiReq = req.body;
|
|
34
|
+
const gateId = openaiReq.gateId || req.headers['x-layer-gate-id'];
|
|
35
|
+
if (!gateId) {
|
|
36
|
+
const error = {
|
|
37
|
+
error: {
|
|
38
|
+
message: 'Missing required field: gateId (provide in request body or X-Layer-Gate-Id header)',
|
|
39
|
+
type: 'invalid_request_error',
|
|
40
|
+
param: 'gateId',
|
|
41
|
+
code: 'missing_field',
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
res.status(400).json(error);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(gateId);
|
|
48
|
+
if (!isUUID) {
|
|
49
|
+
const error = {
|
|
50
|
+
error: {
|
|
51
|
+
message: 'gateId must be a valid UUID',
|
|
52
|
+
type: 'invalid_request_error',
|
|
53
|
+
param: 'gateId',
|
|
54
|
+
code: 'invalid_format',
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
res.status(400).json(error);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
gateConfig = await db.getGateByUserAndId(userId, gateId);
|
|
61
|
+
if (!gateConfig) {
|
|
62
|
+
const error = {
|
|
63
|
+
error: {
|
|
64
|
+
message: `Gate with ID "${gateId}" not found`,
|
|
65
|
+
type: 'invalid_request_error',
|
|
66
|
+
param: 'gateId',
|
|
67
|
+
code: 'not_found',
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
res.status(404).json(error);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (!openaiReq.messages || !Array.isArray(openaiReq.messages) || openaiReq.messages.length === 0) {
|
|
74
|
+
const error = {
|
|
75
|
+
error: {
|
|
76
|
+
message: 'Missing required field: messages (must be a non-empty array)',
|
|
77
|
+
type: 'invalid_request_error',
|
|
78
|
+
param: 'messages',
|
|
79
|
+
code: 'missing_field',
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
res.status(400).json(error);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (gateConfig.taskType && gateConfig.taskType !== 'chat') {
|
|
86
|
+
console.warn(`[Type Mismatch] Gate "${gateConfig.name}" (${gateConfig.id}) configured for taskType="${gateConfig.taskType}" ` +
|
|
87
|
+
`but received request to /v1/chat/completions endpoint. Processing as chat request.`);
|
|
88
|
+
}
|
|
89
|
+
layerRequest = convertOpenAIRequestToLayer(openaiReq, gateId);
|
|
90
|
+
const finalRequest = resolveFinalRequest(gateConfig, layerRequest);
|
|
91
|
+
const isStreaming = finalRequest.data && 'stream' in finalRequest.data && finalRequest.data.stream === true;
|
|
92
|
+
if (isStreaming) {
|
|
93
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
94
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
95
|
+
res.setHeader('Connection', 'keep-alive');
|
|
96
|
+
res.setHeader('X-Accel-Buffering', 'no');
|
|
97
|
+
const requestId = `chatcmpl-${nanoid()}`;
|
|
98
|
+
const created = Math.floor(Date.now() / 1000);
|
|
99
|
+
let promptTokens = 0;
|
|
100
|
+
let completionTokens = 0;
|
|
101
|
+
let totalCost = 0;
|
|
102
|
+
let modelUsed = finalRequest.model;
|
|
103
|
+
try {
|
|
104
|
+
for await (const layerChunk of executeWithRoutingStream(gateConfig, finalRequest, userId)) {
|
|
105
|
+
if (layerChunk.usage) {
|
|
106
|
+
promptTokens = layerChunk.usage.promptTokens || 0;
|
|
107
|
+
completionTokens = layerChunk.usage.completionTokens || 0;
|
|
108
|
+
}
|
|
109
|
+
if (layerChunk.cost) {
|
|
110
|
+
totalCost = layerChunk.cost;
|
|
111
|
+
}
|
|
112
|
+
if (layerChunk.model) {
|
|
113
|
+
modelUsed = layerChunk.model;
|
|
114
|
+
}
|
|
115
|
+
const openaiChunk = convertLayerChunkToOpenAI(layerChunk, requestId, created);
|
|
116
|
+
res.write(`data: ${JSON.stringify(openaiChunk)}\n\n`);
|
|
117
|
+
}
|
|
118
|
+
res.write(`data: [DONE]\n\n`);
|
|
119
|
+
res.end();
|
|
120
|
+
const latencyMs = Date.now() - startTime;
|
|
121
|
+
db.logRequest({
|
|
122
|
+
userId,
|
|
123
|
+
gateId: gateConfig.id,
|
|
124
|
+
gateName: gateConfig.name,
|
|
125
|
+
modelRequested: layerRequest.model || gateConfig.model,
|
|
126
|
+
modelUsed: modelUsed,
|
|
127
|
+
promptTokens,
|
|
128
|
+
completionTokens,
|
|
129
|
+
totalTokens: promptTokens + completionTokens,
|
|
130
|
+
costUsd: totalCost,
|
|
131
|
+
latencyMs,
|
|
132
|
+
success: true,
|
|
133
|
+
errorMessage: null,
|
|
134
|
+
userAgent: req.headers['user-agent'] || null,
|
|
135
|
+
ipAddress: req.ip || null,
|
|
136
|
+
requestPayload: {
|
|
137
|
+
gateId: layerRequest.gateId,
|
|
138
|
+
type: layerRequest.type,
|
|
139
|
+
model: layerRequest.model,
|
|
140
|
+
data: layerRequest.data,
|
|
141
|
+
metadata: layerRequest.metadata,
|
|
142
|
+
},
|
|
143
|
+
responsePayload: {
|
|
144
|
+
streamed: true,
|
|
145
|
+
model: modelUsed,
|
|
146
|
+
usage: { promptTokens, completionTokens, totalTokens: promptTokens + completionTokens },
|
|
147
|
+
cost: totalCost,
|
|
148
|
+
},
|
|
149
|
+
}).catch(err => console.error('Failed to log request:', err));
|
|
150
|
+
}
|
|
151
|
+
catch (streamError) {
|
|
152
|
+
const errorMessage = streamError instanceof Error ? streamError.message : 'Unknown streaming error';
|
|
153
|
+
const openaiError = {
|
|
154
|
+
error: {
|
|
155
|
+
message: errorMessage,
|
|
156
|
+
type: 'server_error',
|
|
157
|
+
code: 'stream_error',
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
res.write(`data: ${JSON.stringify(openaiError)}\n\n`);
|
|
161
|
+
res.end();
|
|
162
|
+
db.logRequest({
|
|
163
|
+
userId,
|
|
164
|
+
gateId: gateConfig.id,
|
|
165
|
+
gateName: gateConfig.name,
|
|
166
|
+
modelRequested: layerRequest.model || gateConfig.model,
|
|
167
|
+
modelUsed: null,
|
|
168
|
+
promptTokens: 0,
|
|
169
|
+
completionTokens: 0,
|
|
170
|
+
totalTokens: 0,
|
|
171
|
+
costUsd: 0,
|
|
172
|
+
latencyMs: Date.now() - startTime,
|
|
173
|
+
success: false,
|
|
174
|
+
errorMessage,
|
|
175
|
+
userAgent: req.headers['user-agent'] || null,
|
|
176
|
+
ipAddress: req.ip || null,
|
|
177
|
+
requestPayload: {
|
|
178
|
+
gateId: layerRequest.gateId,
|
|
179
|
+
type: layerRequest.type,
|
|
180
|
+
model: layerRequest.model,
|
|
181
|
+
data: layerRequest.data,
|
|
182
|
+
metadata: layerRequest.metadata,
|
|
183
|
+
},
|
|
184
|
+
responsePayload: null,
|
|
185
|
+
}).catch(err => console.error('Failed to log request:', err));
|
|
186
|
+
}
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const { result, modelUsed } = await executeWithRouting(gateConfig, finalRequest, userId);
|
|
190
|
+
const latencyMs = Date.now() - startTime;
|
|
191
|
+
db.logRequest({
|
|
192
|
+
userId,
|
|
193
|
+
gateId: gateConfig.id,
|
|
194
|
+
gateName: gateConfig.name,
|
|
195
|
+
modelRequested: layerRequest.model || gateConfig.model,
|
|
196
|
+
modelUsed: modelUsed,
|
|
197
|
+
promptTokens: result.usage?.promptTokens || 0,
|
|
198
|
+
completionTokens: result.usage?.completionTokens || 0,
|
|
199
|
+
totalTokens: result.usage?.totalTokens || 0,
|
|
200
|
+
costUsd: result.cost || 0,
|
|
201
|
+
latencyMs,
|
|
202
|
+
success: true,
|
|
203
|
+
errorMessage: null,
|
|
204
|
+
userAgent: req.headers['user-agent'] || null,
|
|
205
|
+
ipAddress: req.ip || null,
|
|
206
|
+
requestPayload: {
|
|
207
|
+
gateId: layerRequest.gateId,
|
|
208
|
+
type: layerRequest.type,
|
|
209
|
+
model: layerRequest.model,
|
|
210
|
+
data: layerRequest.data,
|
|
211
|
+
metadata: layerRequest.metadata,
|
|
212
|
+
},
|
|
213
|
+
responsePayload: {
|
|
214
|
+
content: result.content,
|
|
215
|
+
model: result.model,
|
|
216
|
+
usage: result.usage,
|
|
217
|
+
cost: result.cost,
|
|
218
|
+
finishReason: result.finishReason,
|
|
219
|
+
},
|
|
220
|
+
}).catch(err => console.error('Failed to log request:', err));
|
|
221
|
+
const openaiResponse = convertLayerResponseToOpenAI(result);
|
|
222
|
+
res.json(openaiResponse);
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
const latencyMs = Date.now() - startTime;
|
|
226
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
227
|
+
db.logRequest({
|
|
228
|
+
userId,
|
|
229
|
+
gateId: gateConfig?.id || null,
|
|
230
|
+
gateName: gateConfig?.name || null,
|
|
231
|
+
modelRequested: (layerRequest?.model || gateConfig?.model) || 'unknown',
|
|
232
|
+
modelUsed: null,
|
|
233
|
+
promptTokens: 0,
|
|
234
|
+
completionTokens: 0,
|
|
235
|
+
totalTokens: 0,
|
|
236
|
+
costUsd: 0,
|
|
237
|
+
latencyMs,
|
|
238
|
+
success: false,
|
|
239
|
+
errorMessage,
|
|
240
|
+
userAgent: req.headers['user-agent'] || null,
|
|
241
|
+
ipAddress: req.ip || null,
|
|
242
|
+
requestPayload: layerRequest ? {
|
|
243
|
+
gateId: layerRequest.gateId,
|
|
244
|
+
type: layerRequest.type,
|
|
245
|
+
model: layerRequest.model,
|
|
246
|
+
data: layerRequest.data,
|
|
247
|
+
metadata: layerRequest.metadata,
|
|
248
|
+
} : null,
|
|
249
|
+
responsePayload: null,
|
|
250
|
+
}).catch(err => console.error('Failed to log request:', err));
|
|
251
|
+
console.error('OpenAI chat completion error:', error);
|
|
252
|
+
const openaiError = {
|
|
253
|
+
error: {
|
|
254
|
+
message: errorMessage,
|
|
255
|
+
type: 'server_error',
|
|
256
|
+
code: 'internal_error',
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
res.status(500).json(openaiError);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
export default router;
|
|
@@ -7,7 +7,9 @@ export declare class MistralAdapter extends BaseProviderAdapter {
|
|
|
7
7
|
protected finishReasonMappings: Record<string, FinishReason>;
|
|
8
8
|
protected toolChoiceMappings: Record<string, string>;
|
|
9
9
|
call(request: LayerRequest, userId?: string): Promise<LayerResponse>;
|
|
10
|
+
callStream(request: LayerRequest, userId?: string): AsyncIterable<LayerResponse>;
|
|
10
11
|
private handleChat;
|
|
12
|
+
private handleChatStream;
|
|
11
13
|
private handleEmbeddings;
|
|
12
14
|
private handleOCR;
|
|
13
15
|
}
|
|
@@ -1 +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;AACvB,OAAO,EAAY,KAAK,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAoB1E,qBAAa,cAAe,SAAQ,mBAAmB;IACrD,SAAS,CAAC,QAAQ,EAAE,QAAQ,CAAoB;IAEhD,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,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;
|
|
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;AACvB,OAAO,EAAY,KAAK,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAoB1E,qBAAa,cAAe,SAAQ,mBAAmB;IACrD,SAAS,CAAC,QAAQ,EAAE,QAAQ,CAAoB;IAEhD,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,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAsBnE,UAAU,CAAC,OAAO,EAAE,YAAY,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,aAAa,CAAC,aAAa,CAAC;YAYzE,UAAU;YAiMT,gBAAgB;YAgPjB,gBAAgB;YA0ChB,SAAS;CA8ExB"}
|
|
@@ -64,6 +64,16 @@ export class MistralAdapter extends BaseProviderAdapter {
|
|
|
64
64
|
throw new Error(`Unknown modality: ${request.type}`);
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
|
+
async *callStream(request, userId) {
|
|
68
|
+
const resolved = await resolveApiKey(this.provider, userId, process.env.MISTRAL_API_KEY);
|
|
69
|
+
switch (request.type) {
|
|
70
|
+
case 'chat':
|
|
71
|
+
yield* this.handleChatStream(request, resolved.key, resolved.usedPlatformKey);
|
|
72
|
+
break;
|
|
73
|
+
default:
|
|
74
|
+
throw new Error(`Streaming not supported for type: ${request.type}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
67
77
|
async handleChat(request, apiKey, usedPlatformKey) {
|
|
68
78
|
const startTime = Date.now();
|
|
69
79
|
const mistral = getMistralClient(apiKey);
|
|
@@ -218,6 +228,201 @@ export class MistralAdapter extends BaseProviderAdapter {
|
|
|
218
228
|
raw: response,
|
|
219
229
|
};
|
|
220
230
|
}
|
|
231
|
+
async *handleChatStream(request, apiKey, usedPlatformKey) {
|
|
232
|
+
const startTime = Date.now();
|
|
233
|
+
const mistral = getMistralClient(apiKey);
|
|
234
|
+
const { data: chat, model } = request;
|
|
235
|
+
if (!model) {
|
|
236
|
+
throw new Error('Model is required for chat completion');
|
|
237
|
+
}
|
|
238
|
+
// Build messages array (same as non-streaming)
|
|
239
|
+
const messages = [];
|
|
240
|
+
// Handle system prompt
|
|
241
|
+
if (chat.systemPrompt) {
|
|
242
|
+
messages.push({ role: 'system', content: chat.systemPrompt });
|
|
243
|
+
}
|
|
244
|
+
// Convert messages to Mistral format
|
|
245
|
+
for (const msg of chat.messages) {
|
|
246
|
+
const role = this.mapRole(msg.role);
|
|
247
|
+
// Handle vision messages (content + images)
|
|
248
|
+
if (msg.images && msg.images.length > 0 && role === 'user') {
|
|
249
|
+
const content = [];
|
|
250
|
+
if (msg.content) {
|
|
251
|
+
content.push({ type: 'text', text: msg.content });
|
|
252
|
+
}
|
|
253
|
+
for (const image of msg.images) {
|
|
254
|
+
const imageUrl = image.url || `data:${image.mimeType || 'image/jpeg'};base64,${image.base64}`;
|
|
255
|
+
content.push({
|
|
256
|
+
type: 'image_url',
|
|
257
|
+
imageUrl: imageUrl,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
messages.push({ role, content });
|
|
261
|
+
}
|
|
262
|
+
// Handle tool responses
|
|
263
|
+
else if (msg.toolCallId && role === 'tool') {
|
|
264
|
+
messages.push({
|
|
265
|
+
role: 'tool',
|
|
266
|
+
content: msg.content || '',
|
|
267
|
+
toolCallId: msg.toolCallId,
|
|
268
|
+
name: msg.name,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
// Handle assistant messages with tool calls
|
|
272
|
+
else if (msg.toolCalls && msg.toolCalls.length > 0) {
|
|
273
|
+
messages.push({
|
|
274
|
+
role: 'assistant',
|
|
275
|
+
content: msg.content || '',
|
|
276
|
+
toolCalls: msg.toolCalls.map((tc) => ({
|
|
277
|
+
id: tc.id,
|
|
278
|
+
type: 'function',
|
|
279
|
+
function: {
|
|
280
|
+
name: tc.function.name,
|
|
281
|
+
arguments: tc.function.arguments,
|
|
282
|
+
},
|
|
283
|
+
})),
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
// Handle regular text messages
|
|
287
|
+
else {
|
|
288
|
+
messages.push({
|
|
289
|
+
role,
|
|
290
|
+
content: msg.content || '',
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// Convert tools to Mistral format
|
|
295
|
+
let tools;
|
|
296
|
+
if (chat.tools && chat.tools.length > 0) {
|
|
297
|
+
tools = chat.tools.map((tool) => ({
|
|
298
|
+
type: 'function',
|
|
299
|
+
function: {
|
|
300
|
+
name: tool.function.name,
|
|
301
|
+
description: tool.function.description,
|
|
302
|
+
parameters: tool.function.parameters || {},
|
|
303
|
+
},
|
|
304
|
+
}));
|
|
305
|
+
}
|
|
306
|
+
// Map tool choice
|
|
307
|
+
let toolChoice;
|
|
308
|
+
if (chat.toolChoice) {
|
|
309
|
+
if (typeof chat.toolChoice === 'object') {
|
|
310
|
+
toolChoice = chat.toolChoice;
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
const mapped = this.mapToolChoice(chat.toolChoice);
|
|
314
|
+
if (mapped === 'auto' || mapped === 'none' || mapped === 'any' || mapped === 'required') {
|
|
315
|
+
toolChoice = mapped;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
const stream = await mistral.chat.stream({
|
|
320
|
+
model,
|
|
321
|
+
messages: messages,
|
|
322
|
+
...(chat.temperature !== undefined && { temperature: chat.temperature }),
|
|
323
|
+
...(chat.maxTokens !== undefined && { maxTokens: chat.maxTokens }),
|
|
324
|
+
...(chat.topP !== undefined && { topP: chat.topP }),
|
|
325
|
+
...(chat.stopSequences !== undefined && { stop: chat.stopSequences }),
|
|
326
|
+
...(chat.frequencyPenalty !== undefined && { frequencyPenalty: chat.frequencyPenalty }),
|
|
327
|
+
...(chat.presencePenalty !== undefined && { presencePenalty: chat.presencePenalty }),
|
|
328
|
+
...(chat.seed !== undefined && { randomSeed: chat.seed }),
|
|
329
|
+
...(tools && { tools }),
|
|
330
|
+
...(toolChoice && { toolChoice }),
|
|
331
|
+
...(chat.responseFormat && {
|
|
332
|
+
responseFormat: typeof chat.responseFormat === 'string'
|
|
333
|
+
? { type: chat.responseFormat }
|
|
334
|
+
: chat.responseFormat,
|
|
335
|
+
}),
|
|
336
|
+
});
|
|
337
|
+
let promptTokens = 0;
|
|
338
|
+
let completionTokens = 0;
|
|
339
|
+
let totalTokens = 0;
|
|
340
|
+
let fullContent = '';
|
|
341
|
+
let currentToolCalls = [];
|
|
342
|
+
let finishReason = null;
|
|
343
|
+
let modelVersion;
|
|
344
|
+
for await (const chunk of stream) {
|
|
345
|
+
// Mistral CompletionEvent can be of type 'chunk' or 'usage'
|
|
346
|
+
const event = chunk;
|
|
347
|
+
// Handle chunk events with choices
|
|
348
|
+
if (event.data?.choices) {
|
|
349
|
+
const choice = event.data.choices[0];
|
|
350
|
+
const delta = choice?.delta;
|
|
351
|
+
// Handle text content
|
|
352
|
+
if (delta?.content) {
|
|
353
|
+
const contentStr = typeof delta.content === 'string'
|
|
354
|
+
? delta.content
|
|
355
|
+
: Array.isArray(delta.content)
|
|
356
|
+
? delta.content.map((c) => c.text || c.content || '').join('')
|
|
357
|
+
: '';
|
|
358
|
+
if (contentStr) {
|
|
359
|
+
fullContent += contentStr;
|
|
360
|
+
yield {
|
|
361
|
+
content: contentStr,
|
|
362
|
+
model: model,
|
|
363
|
+
stream: true,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
// Handle tool calls
|
|
368
|
+
if (delta?.toolCalls && delta.toolCalls.length > 0) {
|
|
369
|
+
for (const tc of delta.toolCalls) {
|
|
370
|
+
const existingCall = currentToolCalls.find(call => call.id === tc.id);
|
|
371
|
+
if (existingCall) {
|
|
372
|
+
// Append to existing tool call arguments
|
|
373
|
+
if (tc.function?.arguments) {
|
|
374
|
+
existingCall.function.arguments += tc.function.arguments;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
// New tool call
|
|
379
|
+
currentToolCalls.push({
|
|
380
|
+
id: tc.id || `call_${currentToolCalls.length}`,
|
|
381
|
+
type: 'function',
|
|
382
|
+
function: {
|
|
383
|
+
name: tc.function?.name || '',
|
|
384
|
+
arguments: tc.function?.arguments || '',
|
|
385
|
+
},
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
// Handle finish reason
|
|
391
|
+
if (choice?.finishReason) {
|
|
392
|
+
finishReason = choice.finishReason;
|
|
393
|
+
}
|
|
394
|
+
// Handle model version
|
|
395
|
+
if (event.data.model) {
|
|
396
|
+
modelVersion = event.data.model;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
// Handle usage events
|
|
400
|
+
if (event.data?.usage) {
|
|
401
|
+
promptTokens = event.data.usage.promptTokens || 0;
|
|
402
|
+
completionTokens = event.data.usage.completionTokens || 0;
|
|
403
|
+
totalTokens = event.data.usage.totalTokens || 0;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const cost = this.calculateCost(model, promptTokens, completionTokens);
|
|
407
|
+
const latencyMs = Date.now() - startTime;
|
|
408
|
+
// Yield final chunk with metadata
|
|
409
|
+
yield {
|
|
410
|
+
content: '',
|
|
411
|
+
model: modelVersion || model,
|
|
412
|
+
toolCalls: currentToolCalls.length > 0 ? currentToolCalls : undefined,
|
|
413
|
+
usage: {
|
|
414
|
+
promptTokens,
|
|
415
|
+
completionTokens,
|
|
416
|
+
totalTokens,
|
|
417
|
+
},
|
|
418
|
+
cost,
|
|
419
|
+
latencyMs,
|
|
420
|
+
usedPlatformKey,
|
|
421
|
+
stream: true,
|
|
422
|
+
finishReason: this.mapFinishReason(finishReason || 'stop'),
|
|
423
|
+
rawFinishReason: finishReason || undefined,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
221
426
|
async handleEmbeddings(request, apiKey, usedPlatformKey) {
|
|
222
427
|
const startTime = Date.now();
|
|
223
428
|
const mistral = getMistralClient(apiKey);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-mistral-streaming.d.ts","sourceRoot":"","sources":["../../../../src/services/providers/tests/test-mistral-streaming.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { MistralAdapter } from '../mistral-adapter.js';
|
|
2
|
+
const adapter = new MistralAdapter();
|
|
3
|
+
async function testChatStreamingBasic() {
|
|
4
|
+
console.log('Testing basic chat streaming...');
|
|
5
|
+
const request = {
|
|
6
|
+
gateId: 'test-gate',
|
|
7
|
+
model: 'mistral-small-2501',
|
|
8
|
+
type: 'chat',
|
|
9
|
+
data: {
|
|
10
|
+
messages: [
|
|
11
|
+
{ role: 'user', content: 'Count from 1 to 5 slowly, one number per line.' }
|
|
12
|
+
],
|
|
13
|
+
temperature: 0.7,
|
|
14
|
+
maxTokens: 50,
|
|
15
|
+
stream: true,
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
if (!adapter.callStream) {
|
|
19
|
+
throw new Error('callStream method not available');
|
|
20
|
+
}
|
|
21
|
+
let chunkCount = 0;
|
|
22
|
+
let fullContent = '';
|
|
23
|
+
let finalUsage = null;
|
|
24
|
+
let finalCost = null;
|
|
25
|
+
console.log('\nStreaming chunks:');
|
|
26
|
+
console.log('---');
|
|
27
|
+
for await (const chunk of adapter.callStream(request)) {
|
|
28
|
+
chunkCount++;
|
|
29
|
+
if (chunk.content) {
|
|
30
|
+
process.stdout.write(chunk.content);
|
|
31
|
+
fullContent += chunk.content;
|
|
32
|
+
}
|
|
33
|
+
if (chunk.usage) {
|
|
34
|
+
finalUsage = chunk.usage;
|
|
35
|
+
}
|
|
36
|
+
if (chunk.cost) {
|
|
37
|
+
finalCost = chunk.cost;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
console.log('\n---\n');
|
|
41
|
+
console.log('Total chunks received:', chunkCount);
|
|
42
|
+
console.log('Full content:', fullContent);
|
|
43
|
+
console.log('Final usage:', finalUsage);
|
|
44
|
+
console.log('Final cost:', finalCost);
|
|
45
|
+
console.log('✅ Basic streaming test passed\n');
|
|
46
|
+
}
|
|
47
|
+
async function testChatStreamingWithToolCalls() {
|
|
48
|
+
console.log('Testing chat streaming with tool calls...');
|
|
49
|
+
const request = {
|
|
50
|
+
gateId: 'test-gate',
|
|
51
|
+
model: 'mistral-small-2501',
|
|
52
|
+
type: 'chat',
|
|
53
|
+
data: {
|
|
54
|
+
messages: [
|
|
55
|
+
{ role: 'user', content: 'What is the weather in Paris?' }
|
|
56
|
+
],
|
|
57
|
+
tools: [
|
|
58
|
+
{
|
|
59
|
+
type: 'function',
|
|
60
|
+
function: {
|
|
61
|
+
name: 'get_weather',
|
|
62
|
+
description: 'Get the current weather for a location',
|
|
63
|
+
parameters: {
|
|
64
|
+
type: 'object',
|
|
65
|
+
properties: {
|
|
66
|
+
location: {
|
|
67
|
+
type: 'string',
|
|
68
|
+
description: 'The city and state, e.g. Paris, France',
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
required: ['location'],
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
toolChoice: 'auto',
|
|
77
|
+
stream: true,
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
if (!adapter.callStream) {
|
|
81
|
+
throw new Error('callStream method not available');
|
|
82
|
+
}
|
|
83
|
+
let toolCallsReceived = false;
|
|
84
|
+
for await (const chunk of adapter.callStream(request)) {
|
|
85
|
+
if (chunk.toolCalls && chunk.toolCalls.length > 0) {
|
|
86
|
+
console.log('Tool calls received:', JSON.stringify(chunk.toolCalls, null, 2));
|
|
87
|
+
toolCallsReceived = true;
|
|
88
|
+
}
|
|
89
|
+
if (chunk.finishReason === 'tool_call') {
|
|
90
|
+
console.log('Finish reason: tool_call');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (!toolCallsReceived) {
|
|
94
|
+
console.warn('⚠️ No tool calls received (model may have chosen not to use tools)');
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
console.log('✅ Tool calls streaming test passed\n');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function testChatStreamingError() {
|
|
101
|
+
console.log('Testing streaming with invalid model (error handling)...');
|
|
102
|
+
const request = {
|
|
103
|
+
gateId: 'test-gate',
|
|
104
|
+
model: 'invalid-mistral-model-name',
|
|
105
|
+
type: 'chat',
|
|
106
|
+
data: {
|
|
107
|
+
messages: [
|
|
108
|
+
{ role: 'user', content: 'Hello' }
|
|
109
|
+
],
|
|
110
|
+
stream: true,
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
if (!adapter.callStream) {
|
|
114
|
+
throw new Error('callStream method not available');
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
for await (const chunk of adapter.callStream(request)) {
|
|
118
|
+
console.log('Received chunk:', chunk);
|
|
119
|
+
}
|
|
120
|
+
console.error('❌ Should have thrown an error for invalid model');
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
console.log('✅ Correctly threw error:', error instanceof Error ? error.message : error);
|
|
124
|
+
console.log('✅ Error handling test passed\n');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Run all tests
|
|
128
|
+
(async () => {
|
|
129
|
+
try {
|
|
130
|
+
await testChatStreamingBasic();
|
|
131
|
+
await testChatStreamingWithToolCalls();
|
|
132
|
+
await testChatStreamingError();
|
|
133
|
+
console.log('✅ All streaming tests completed successfully!');
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
console.error('❌ Test failed:', error);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@layer-ai/core",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.20",
|
|
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": "^2.5.
|
|
39
|
+
"@layer-ai/sdk": "^2.5.7"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/bcryptjs": "^2.4.6",
|