@layer-ai/core 0.9.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/routes/v1/gates.js +2 -2
- package/dist/routes/v2/complete.d.ts.map +1 -1
- package/dist/routes/v2/complete.js +127 -56
- package/dist/routes/v2/tests/test-byok-completion.js +1 -1
- package/dist/routes/v2/tests/test-complete-anthropic.js +4 -4
- package/dist/routes/v2/tests/test-complete-openai.js +7 -7
- package/dist/routes/v2/tests/test-complete-routing.js +5 -5
- package/dist/routes/v3/chat.d.ts +4 -0
- package/dist/routes/v3/chat.d.ts.map +1 -0
- package/dist/routes/v3/chat.js +203 -0
- package/dist/routes/v3/completions/chat.d.ts +4 -0
- package/dist/routes/v3/completions/chat.d.ts.map +1 -0
- package/dist/routes/v3/completions/chat.js +178 -0
- package/dist/routes/v3/completions/embed.d.ts +4 -0
- package/dist/routes/v3/completions/embed.d.ts.map +1 -0
- package/dist/routes/v3/completions/embed.js +94 -0
- package/dist/routes/v3/completions/image.d.ts +4 -0
- package/dist/routes/v3/completions/image.d.ts.map +1 -0
- package/dist/routes/v3/completions/image.js +155 -0
- package/dist/routes/v3/completions/ocr.d.ts +4 -0
- package/dist/routes/v3/completions/ocr.d.ts.map +1 -0
- package/dist/routes/v3/completions/ocr.js +94 -0
- package/dist/routes/v3/completions/tts.d.ts +4 -0
- package/dist/routes/v3/completions/tts.d.ts.map +1 -0
- package/dist/routes/v3/completions/tts.js +94 -0
- package/dist/routes/v3/completions/video.d.ts +4 -0
- package/dist/routes/v3/completions/video.d.ts.map +1 -0
- package/dist/routes/v3/completions/video.js +94 -0
- package/dist/services/providers/base-adapter.d.ts.map +1 -1
- package/dist/services/providers/base-adapter.js +5 -2
- package/dist/services/providers/tests/test-anthropic-adapter.js +4 -4
- package/dist/services/providers/tests/test-google-adapter.js +9 -9
- package/dist/services/providers/tests/test-mistral-adapter.js +11 -11
- package/dist/services/providers/tests/test-openai-adapter.js +8 -8
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ 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
5
|
export { default as completeRouter } from './routes/v2/complete.js';
|
|
6
|
+
export { default as chatRouter } from './routes/v3/chat.js';
|
|
6
7
|
export { authenticate } from './middleware/auth.js';
|
|
7
8
|
export type {} from './middleware/auth.js';
|
|
8
9
|
export { db } from './lib/db/postgres.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;AAG5D,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAGpE,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAG5D,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,7 +3,10 @@ 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
|
+
// v2 routes
|
|
6
7
|
export { default as completeRouter } from './routes/v2/complete.js';
|
|
8
|
+
// v3 routes
|
|
9
|
+
export { default as chatRouter } from './routes/v3/chat.js';
|
|
7
10
|
// Middleware
|
|
8
11
|
export { authenticate } from './middleware/auth.js';
|
|
9
12
|
// Database
|
package/dist/routes/v1/gates.js
CHANGED
|
@@ -327,7 +327,7 @@ router.post('/test', async (req, res) => {
|
|
|
327
327
|
try {
|
|
328
328
|
const request = {
|
|
329
329
|
type: 'chat',
|
|
330
|
-
|
|
330
|
+
gateId: finalGate.id,
|
|
331
331
|
model: finalGate.model,
|
|
332
332
|
data: {
|
|
333
333
|
messages,
|
|
@@ -363,7 +363,7 @@ router.post('/test', async (req, res) => {
|
|
|
363
363
|
try {
|
|
364
364
|
const request = {
|
|
365
365
|
type: 'chat',
|
|
366
|
-
|
|
366
|
+
gateId: finalGate.id,
|
|
367
367
|
model: fallbackModel,
|
|
368
368
|
data: {
|
|
369
369
|
messages,
|
|
@@ -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;AASpD,QAAA,MAAM,MAAM,EAAE,UAAqB,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;AASpD,QAAA,MAAM,MAAM,EAAE,UAAqB,CAAC;AAkVpC,eAAe,MAAM,CAAC"}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
2
|
import { db } from '../../lib/db/postgres.js';
|
|
3
|
-
import { cache } from '../../lib/db/redis.js';
|
|
4
3
|
import { authenticate } from '../../middleware/auth.js';
|
|
5
4
|
import { callAdapter, normalizeModelId } from '../../lib/provider-factory.js';
|
|
6
5
|
import { OverrideField } from '@layer-ai/sdk';
|
|
@@ -13,31 +12,7 @@ function isOverrideAllowed(allowOverrides, field) {
|
|
|
13
12
|
return false;
|
|
14
13
|
return allowOverrides[field] ?? false;
|
|
15
14
|
}
|
|
16
|
-
async function getGateConfig(userId, gateIdentifier) {
|
|
17
|
-
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(gateIdentifier);
|
|
18
|
-
let gateConfig = null;
|
|
19
|
-
if (isUUID) {
|
|
20
|
-
gateConfig = await cache.getGateById(userId, gateIdentifier);
|
|
21
|
-
if (!gateConfig) {
|
|
22
|
-
gateConfig = await db.getGateByUserAndId(userId, gateIdentifier);
|
|
23
|
-
if (gateConfig) {
|
|
24
|
-
await cache.setGate(userId, gateConfig.name, gateConfig);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
else {
|
|
29
|
-
gateConfig = await cache.getGate(userId, gateIdentifier);
|
|
30
|
-
if (!gateConfig) {
|
|
31
|
-
gateConfig = await db.getGateByUserAndName(userId, gateIdentifier);
|
|
32
|
-
if (gateConfig) {
|
|
33
|
-
await cache.setGate(userId, gateIdentifier, gateConfig);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return gateConfig;
|
|
38
|
-
}
|
|
39
15
|
function resolveFinalRequest(gateConfig, request) {
|
|
40
|
-
const finalRequest = { ...request };
|
|
41
16
|
let finalModel = gateConfig.model;
|
|
42
17
|
if (request.model && isOverrideAllowed(gateConfig.allowOverrides, OverrideField.Model)) {
|
|
43
18
|
try {
|
|
@@ -47,33 +22,85 @@ function resolveFinalRequest(gateConfig, request) {
|
|
|
47
22
|
finalModel = gateConfig.model;
|
|
48
23
|
}
|
|
49
24
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
chatData.systemPrompt
|
|
25
|
+
// Use discriminated union to handle each request type
|
|
26
|
+
switch (request.type) {
|
|
27
|
+
case 'chat': {
|
|
28
|
+
const chatData = { ...request.data };
|
|
29
|
+
if (!chatData.systemPrompt && gateConfig.systemPrompt) {
|
|
30
|
+
chatData.systemPrompt = gateConfig.systemPrompt;
|
|
31
|
+
}
|
|
32
|
+
if (chatData.temperature === undefined && gateConfig.temperature !== undefined) {
|
|
33
|
+
chatData.temperature = gateConfig.temperature;
|
|
34
|
+
}
|
|
35
|
+
else if (chatData.temperature !== undefined && !isOverrideAllowed(gateConfig.allowOverrides, OverrideField.Temperature)) {
|
|
36
|
+
chatData.temperature = gateConfig.temperature;
|
|
37
|
+
}
|
|
38
|
+
if (chatData.maxTokens === undefined && gateConfig.maxTokens !== undefined) {
|
|
39
|
+
chatData.maxTokens = gateConfig.maxTokens;
|
|
40
|
+
}
|
|
41
|
+
else if (chatData.maxTokens !== undefined && !isOverrideAllowed(gateConfig.allowOverrides, OverrideField.MaxTokens)) {
|
|
42
|
+
chatData.maxTokens = gateConfig.maxTokens;
|
|
43
|
+
}
|
|
44
|
+
if (chatData.topP === undefined && gateConfig.topP !== undefined) {
|
|
45
|
+
chatData.topP = gateConfig.topP;
|
|
46
|
+
}
|
|
47
|
+
else if (chatData.topP !== undefined && !isOverrideAllowed(gateConfig.allowOverrides, OverrideField.TopP)) {
|
|
48
|
+
chatData.topP = gateConfig.topP;
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
...request,
|
|
52
|
+
model: normalizeModelId(finalModel),
|
|
53
|
+
data: chatData,
|
|
54
|
+
};
|
|
55
55
|
}
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
case 'image': {
|
|
57
|
+
// TODO: Future enhancement - intelligently apply gate-level defaults
|
|
58
|
+
// Potential features:
|
|
59
|
+
// - Apply systemPrompt by intelligently merging with user prompt
|
|
60
|
+
// - Support defaults for size, quality, style, count
|
|
61
|
+
// - Make this opt-in with a gate setting (e.g., applySystemPromptToAllTasks)
|
|
62
|
+
return {
|
|
63
|
+
...request,
|
|
64
|
+
model: normalizeModelId(finalModel),
|
|
65
|
+
};
|
|
58
66
|
}
|
|
59
|
-
|
|
60
|
-
|
|
67
|
+
case 'video': {
|
|
68
|
+
// TODO: Future enhancement - intelligently apply gate-level defaults
|
|
69
|
+
// Similar to image generation, could support systemPrompt integration
|
|
70
|
+
// and video-specific defaults (duration, size, fps)
|
|
71
|
+
return {
|
|
72
|
+
...request,
|
|
73
|
+
model: normalizeModelId(finalModel),
|
|
74
|
+
};
|
|
61
75
|
}
|
|
62
|
-
|
|
63
|
-
|
|
76
|
+
case 'embeddings': {
|
|
77
|
+
// Embeddings are deterministic and don't typically need gate-level defaults
|
|
78
|
+
// beyond model selection (already handled)
|
|
79
|
+
return {
|
|
80
|
+
...request,
|
|
81
|
+
model: normalizeModelId(finalModel),
|
|
82
|
+
};
|
|
64
83
|
}
|
|
65
|
-
|
|
66
|
-
|
|
84
|
+
case 'tts': {
|
|
85
|
+
// TODO: Future enhancement - support defaults for voice, speed, format
|
|
86
|
+
return {
|
|
87
|
+
...request,
|
|
88
|
+
model: normalizeModelId(finalModel),
|
|
89
|
+
};
|
|
67
90
|
}
|
|
68
|
-
|
|
69
|
-
|
|
91
|
+
case 'ocr': {
|
|
92
|
+
// TODO: Future enhancement - support defaults for tableFormat, includeImageBase64, etc.
|
|
93
|
+
return {
|
|
94
|
+
...request,
|
|
95
|
+
model: normalizeModelId(finalModel),
|
|
96
|
+
};
|
|
70
97
|
}
|
|
71
|
-
|
|
72
|
-
|
|
98
|
+
default: {
|
|
99
|
+
// This will cause a TypeScript error if we miss a case
|
|
100
|
+
const exhaustiveCheck = request;
|
|
101
|
+
throw new Error(`Unhandled request type: ${exhaustiveCheck.type}`);
|
|
73
102
|
}
|
|
74
|
-
finalRequest.data = chatData;
|
|
75
103
|
}
|
|
76
|
-
return finalRequest;
|
|
77
104
|
}
|
|
78
105
|
function getModelsToTry(gateConfig, primaryModel) {
|
|
79
106
|
const modelsToTry = [primaryModel];
|
|
@@ -141,28 +168,72 @@ router.post('/', authenticate, async (req, res) => {
|
|
|
141
168
|
let request = null;
|
|
142
169
|
try {
|
|
143
170
|
const rawRequest = req.body;
|
|
144
|
-
if (!rawRequest.
|
|
145
|
-
res.status(400).json({ error: 'bad_request', message: 'Missing required field:
|
|
171
|
+
if (!rawRequest.gateId) {
|
|
172
|
+
res.status(400).json({ error: 'bad_request', message: 'Missing required field: gateId' });
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(rawRequest.gateId);
|
|
176
|
+
if (!isUUID) {
|
|
177
|
+
res.status(400).json({ error: 'bad_request', message: 'gateId must be a valid UUID. Gate names are no longer supported.' });
|
|
146
178
|
return;
|
|
147
179
|
}
|
|
148
|
-
gateConfig = await
|
|
180
|
+
gateConfig = await db.getGateByUserAndId(userId, rawRequest.gateId);
|
|
149
181
|
if (!gateConfig) {
|
|
150
|
-
res.status(404).json({ error: 'not_found', message: `Gate "${rawRequest.
|
|
182
|
+
res.status(404).json({ error: 'not_found', message: `Gate with ID "${rawRequest.gateId}" not found` });
|
|
151
183
|
return;
|
|
152
184
|
}
|
|
153
|
-
|
|
185
|
+
// Default to gate's taskType, allow override via request.type
|
|
186
|
+
const requestType = rawRequest.type || gateConfig.taskType;
|
|
187
|
+
// Warn if request type doesn't match gate's taskType (possible misconfiguration)
|
|
188
|
+
if (rawRequest.type && gateConfig.taskType && rawRequest.type !== gateConfig.taskType) {
|
|
189
|
+
console.warn(`[Type Mismatch] Gate "${gateConfig.name}" (${gateConfig.id}) configured for taskType="${gateConfig.taskType}" ` +
|
|
190
|
+
`but received request with type="${rawRequest.type}". Using request type as override.`);
|
|
191
|
+
}
|
|
154
192
|
request = {
|
|
155
|
-
|
|
193
|
+
gateId: rawRequest.gateId,
|
|
156
194
|
type: requestType,
|
|
157
195
|
data: rawRequest.data,
|
|
158
196
|
model: rawRequest.model,
|
|
159
197
|
metadata: rawRequest.metadata
|
|
160
198
|
};
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
199
|
+
// Validate required fields based on request type
|
|
200
|
+
switch (request.type) {
|
|
201
|
+
case 'chat':
|
|
202
|
+
if (!request.data.messages || !Array.isArray(request.data.messages) || request.data.messages.length === 0) {
|
|
203
|
+
res.status(400).json({ error: 'bad_request', message: 'Missing required field: data.messages' });
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
break;
|
|
207
|
+
case 'image':
|
|
208
|
+
if (!request.data.prompt) {
|
|
209
|
+
res.status(400).json({ error: 'bad_request', message: 'Missing required field: data.prompt' });
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
break;
|
|
213
|
+
case 'video':
|
|
214
|
+
if (!request.data.prompt) {
|
|
215
|
+
res.status(400).json({ error: 'bad_request', message: 'Missing required field: data.prompt' });
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
break;
|
|
219
|
+
case 'embeddings':
|
|
220
|
+
if (!request.data.input) {
|
|
221
|
+
res.status(400).json({ error: 'bad_request', message: 'Missing required field: data.input' });
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
break;
|
|
225
|
+
case 'tts':
|
|
226
|
+
if (!request.data.input) {
|
|
227
|
+
res.status(400).json({ error: 'bad_request', message: 'Missing required field: data.input' });
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
break;
|
|
231
|
+
case 'ocr':
|
|
232
|
+
if (!request.data.documentUrl && !request.data.imageUrl && !request.data.base64) {
|
|
233
|
+
res.status(400).json({ error: 'bad_request', message: 'Missing required field: data must contain one of documentUrl, imageUrl, or base64' });
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
break;
|
|
166
237
|
}
|
|
167
238
|
const finalRequest = resolveFinalRequest(gateConfig, request);
|
|
168
239
|
const { result, modelUsed } = await executeWithRouting(gateConfig, finalRequest, userId);
|
|
@@ -171,7 +242,7 @@ router.post('/', authenticate, async (req, res) => {
|
|
|
171
242
|
db.logRequest({
|
|
172
243
|
userId,
|
|
173
244
|
gateId: gateConfig.id,
|
|
174
|
-
gateName:
|
|
245
|
+
gateName: gateConfig.name,
|
|
175
246
|
modelRequested: request.model || gateConfig.model,
|
|
176
247
|
modelUsed: modelUsed,
|
|
177
248
|
promptTokens: result.usage?.promptTokens || 0,
|
|
@@ -12,7 +12,7 @@ import { GoogleAdapter } from '../../../services/providers/google-adapter.js';
|
|
|
12
12
|
// Test user ID from the database
|
|
13
13
|
const TEST_USER_ID = 'ebd64998-465d-4211-ad67-87b4e01ad0da';
|
|
14
14
|
const SIMPLE_REQUEST = {
|
|
15
|
-
|
|
15
|
+
gateId: 'byok-test',
|
|
16
16
|
model: 'gpt-4o-mini',
|
|
17
17
|
type: 'chat',
|
|
18
18
|
data: {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
async function testBasicChat() {
|
|
4
4
|
console.log('Test 1: Basic chat completion with Anthropic\n');
|
|
5
5
|
const request = {
|
|
6
|
-
|
|
6
|
+
gateId: 'test-gate',
|
|
7
7
|
model: 'claude-sonnet-4-5-20250929',
|
|
8
8
|
type: 'chat',
|
|
9
9
|
data: {
|
|
@@ -27,7 +27,7 @@ async function testBasicChat() {
|
|
|
27
27
|
async function testVision() {
|
|
28
28
|
console.log('\n\nTest 2: Vision with Anthropic (not supported in v1)\n');
|
|
29
29
|
const request = {
|
|
30
|
-
|
|
30
|
+
gateId: 'test-gate',
|
|
31
31
|
model: 'claude-sonnet-4-5-20250929',
|
|
32
32
|
type: 'chat',
|
|
33
33
|
data: {
|
|
@@ -49,7 +49,7 @@ async function testVision() {
|
|
|
49
49
|
async function testToolCalls() {
|
|
50
50
|
console.log('\n\nTest 3: Tool calls with Anthropic (not supported in v1)\n');
|
|
51
51
|
const request = {
|
|
52
|
-
|
|
52
|
+
gateId: 'test-gate',
|
|
53
53
|
model: 'claude-sonnet-4-5-20250929',
|
|
54
54
|
type: 'chat',
|
|
55
55
|
data: {
|
|
@@ -86,7 +86,7 @@ async function testToolCalls() {
|
|
|
86
86
|
async function testSystemPrompt() {
|
|
87
87
|
console.log('\n\nTest 4: System prompt and advanced params\n');
|
|
88
88
|
const request = {
|
|
89
|
-
|
|
89
|
+
gateId: 'test-gate',
|
|
90
90
|
model: 'claude-sonnet-4-5-20250929',
|
|
91
91
|
type: 'chat',
|
|
92
92
|
data: {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
async function testBasicChat() {
|
|
4
4
|
console.log('Test 1: Basic chat completion with OpenAI\n');
|
|
5
5
|
const request = {
|
|
6
|
-
|
|
6
|
+
gateId: 'test-gate',
|
|
7
7
|
model: 'gpt-4o-mini',
|
|
8
8
|
type: 'chat',
|
|
9
9
|
data: {
|
|
@@ -26,7 +26,7 @@ async function testBasicChat() {
|
|
|
26
26
|
async function testVision() {
|
|
27
27
|
console.log('\n\nTest 2: Vision with GPT-4o\n');
|
|
28
28
|
const request = {
|
|
29
|
-
|
|
29
|
+
gateId: 'test-gate',
|
|
30
30
|
model: 'gpt-4o',
|
|
31
31
|
type: 'chat',
|
|
32
32
|
data: {
|
|
@@ -49,7 +49,7 @@ async function testVision() {
|
|
|
49
49
|
async function testToolCalls() {
|
|
50
50
|
console.log('\n\nTest 3: Tool calls with GPT-4\n');
|
|
51
51
|
const request = {
|
|
52
|
-
|
|
52
|
+
gateId: 'test-gate',
|
|
53
53
|
model: 'gpt-4o-mini',
|
|
54
54
|
type: 'chat',
|
|
55
55
|
data: {
|
|
@@ -85,7 +85,7 @@ async function testToolCalls() {
|
|
|
85
85
|
async function testImageGeneration() {
|
|
86
86
|
console.log('\n\nTest 4: Image generation with DALL-E\n');
|
|
87
87
|
const request = {
|
|
88
|
-
|
|
88
|
+
gateId: 'test-gate',
|
|
89
89
|
model: 'dall-e-3',
|
|
90
90
|
type: 'image',
|
|
91
91
|
data: {
|
|
@@ -102,7 +102,7 @@ async function testImageGeneration() {
|
|
|
102
102
|
async function testEmbeddings() {
|
|
103
103
|
console.log('\n\nTest 5: Text embeddings\n');
|
|
104
104
|
const request = {
|
|
105
|
-
|
|
105
|
+
gateId: 'test-gate',
|
|
106
106
|
model: 'text-embedding-3-small',
|
|
107
107
|
type: 'embeddings',
|
|
108
108
|
data: {
|
|
@@ -117,7 +117,7 @@ async function testEmbeddings() {
|
|
|
117
117
|
async function testTextToSpeech() {
|
|
118
118
|
console.log('\n\nTest 6: Text-to-speech\n');
|
|
119
119
|
const request = {
|
|
120
|
-
|
|
120
|
+
gateId: 'test-gate',
|
|
121
121
|
model: 'tts-1',
|
|
122
122
|
type: 'tts',
|
|
123
123
|
data: {
|
|
@@ -133,7 +133,7 @@ async function testTextToSpeech() {
|
|
|
133
133
|
async function testResponseFormat() {
|
|
134
134
|
console.log('\n\nTest 7: JSON response format\n');
|
|
135
135
|
const request = {
|
|
136
|
-
|
|
136
|
+
gateId: 'test-gate',
|
|
137
137
|
model: 'gpt-4o-mini',
|
|
138
138
|
type: 'chat',
|
|
139
139
|
data: {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
async function testFallbackRouting() {
|
|
4
4
|
console.log('Test 1: Fallback routing (Anthropic -> OpenAI)\n');
|
|
5
5
|
const request = {
|
|
6
|
-
|
|
6
|
+
gateId: 'test-gate-with-fallback',
|
|
7
7
|
model: 'claude-sonnet-4-5-20250929', // Primary model
|
|
8
8
|
type: 'chat',
|
|
9
9
|
data: {
|
|
@@ -27,7 +27,7 @@ async function testFallbackRouting() {
|
|
|
27
27
|
async function testRoundRobinRouting() {
|
|
28
28
|
console.log('\n\nTest 2: Round-robin routing across providers\n');
|
|
29
29
|
const request = {
|
|
30
|
-
|
|
30
|
+
gateId: 'test-gate-with-round-robin',
|
|
31
31
|
model: 'claude-sonnet-4-5-20250929',
|
|
32
32
|
type: 'chat',
|
|
33
33
|
data: {
|
|
@@ -50,7 +50,7 @@ async function testRoundRobinRouting() {
|
|
|
50
50
|
async function testCrossProviderFallback() {
|
|
51
51
|
console.log('\n\nTest 3: Cross-provider fallback with vision\n');
|
|
52
52
|
const request = {
|
|
53
|
-
|
|
53
|
+
gateId: 'test-gate-vision-fallback',
|
|
54
54
|
model: 'claude-sonnet-4-5-20250929',
|
|
55
55
|
type: 'chat',
|
|
56
56
|
data: {
|
|
@@ -78,7 +78,7 @@ async function testCrossProviderFallback() {
|
|
|
78
78
|
async function testToolCallsFallback() {
|
|
79
79
|
console.log('\n\nTest 4: Fallback routing with tool calls\n');
|
|
80
80
|
const request = {
|
|
81
|
-
|
|
81
|
+
gateId: 'test-gate-tools-fallback',
|
|
82
82
|
model: 'gpt-4o-mini',
|
|
83
83
|
type: 'chat',
|
|
84
84
|
data: {
|
|
@@ -117,7 +117,7 @@ async function testToolCallsFallback() {
|
|
|
117
117
|
async function testCostOptimization() {
|
|
118
118
|
console.log('\n\nTest 5: Cost optimization with round-robin\n');
|
|
119
119
|
const request = {
|
|
120
|
-
|
|
120
|
+
gateId: 'test-gate-cost-optimization',
|
|
121
121
|
model: 'gpt-4o-mini',
|
|
122
122
|
type: 'chat',
|
|
123
123
|
data: {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../../../src/routes/v3/chat.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,SAAS,CAAC;AAOpD,QAAA,MAAM,MAAM,EAAE,UAAqB,CAAC;AAqPpC,eAAe,MAAM,CAAC"}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { db } from '../../lib/db/postgres.js';
|
|
3
|
+
import { authenticate } from '../../middleware/auth.js';
|
|
4
|
+
import { callAdapter, normalizeModelId } from '../../lib/provider-factory.js';
|
|
5
|
+
import { OverrideField } from '@layer-ai/sdk';
|
|
6
|
+
const router = Router();
|
|
7
|
+
// MARK:- Helper Functions
|
|
8
|
+
function isOverrideAllowed(allowOverrides, field) {
|
|
9
|
+
if (allowOverrides === undefined || allowOverrides === null || allowOverrides === true)
|
|
10
|
+
return true;
|
|
11
|
+
if (allowOverrides === false)
|
|
12
|
+
return false;
|
|
13
|
+
return allowOverrides[field] ?? false;
|
|
14
|
+
}
|
|
15
|
+
function resolveFinalRequest(gateConfig, request) {
|
|
16
|
+
let finalModel = gateConfig.model;
|
|
17
|
+
if (request.model && isOverrideAllowed(gateConfig.allowOverrides, OverrideField.Model)) {
|
|
18
|
+
try {
|
|
19
|
+
finalModel = normalizeModelId(request.model);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
finalModel = gateConfig.model;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// Since this is v3/chat endpoint, we know the data is ChatRequest
|
|
26
|
+
const chatData = { ...request.data };
|
|
27
|
+
if (!chatData.systemPrompt && gateConfig.systemPrompt) {
|
|
28
|
+
chatData.systemPrompt = gateConfig.systemPrompt;
|
|
29
|
+
}
|
|
30
|
+
if (chatData.temperature === undefined && gateConfig.temperature !== undefined) {
|
|
31
|
+
chatData.temperature = gateConfig.temperature;
|
|
32
|
+
}
|
|
33
|
+
else if (chatData.temperature !== undefined && !isOverrideAllowed(gateConfig.allowOverrides, OverrideField.Temperature)) {
|
|
34
|
+
chatData.temperature = gateConfig.temperature;
|
|
35
|
+
}
|
|
36
|
+
if (chatData.maxTokens === undefined && gateConfig.maxTokens !== undefined) {
|
|
37
|
+
chatData.maxTokens = gateConfig.maxTokens;
|
|
38
|
+
}
|
|
39
|
+
else if (chatData.maxTokens !== undefined && !isOverrideAllowed(gateConfig.allowOverrides, OverrideField.MaxTokens)) {
|
|
40
|
+
chatData.maxTokens = gateConfig.maxTokens;
|
|
41
|
+
}
|
|
42
|
+
if (chatData.topP === undefined && gateConfig.topP !== undefined) {
|
|
43
|
+
chatData.topP = gateConfig.topP;
|
|
44
|
+
}
|
|
45
|
+
else if (chatData.topP !== undefined && !isOverrideAllowed(gateConfig.allowOverrides, OverrideField.TopP)) {
|
|
46
|
+
chatData.topP = gateConfig.topP;
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
...request,
|
|
50
|
+
type: 'chat',
|
|
51
|
+
model: normalizeModelId(finalModel),
|
|
52
|
+
data: chatData,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function getModelsToTry(gateConfig, primaryModel) {
|
|
56
|
+
const modelsToTry = [primaryModel];
|
|
57
|
+
if (gateConfig.routingStrategy === 'fallback' && gateConfig.fallbackModels?.length) {
|
|
58
|
+
modelsToTry.push(...gateConfig.fallbackModels);
|
|
59
|
+
}
|
|
60
|
+
return modelsToTry;
|
|
61
|
+
}
|
|
62
|
+
async function executeWithFallback(request, modelsToTry, userId) {
|
|
63
|
+
let result = null;
|
|
64
|
+
let lastError = null;
|
|
65
|
+
let modelUsed = request.model;
|
|
66
|
+
for (const modelToTry of modelsToTry) {
|
|
67
|
+
try {
|
|
68
|
+
const modelRequest = { ...request, model: modelToTry };
|
|
69
|
+
result = await callAdapter(modelRequest, userId);
|
|
70
|
+
modelUsed = modelToTry;
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
lastError = error;
|
|
75
|
+
console.log(`Model ${modelToTry} failed, trying next fallback...`, error instanceof Error ? error.message : error);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (!result) {
|
|
80
|
+
throw lastError || new Error('All models failed');
|
|
81
|
+
}
|
|
82
|
+
return { result, modelUsed };
|
|
83
|
+
}
|
|
84
|
+
async function executeWithRoundRobin(gateConfig, request, userId) {
|
|
85
|
+
if (!gateConfig.fallbackModels?.length) {
|
|
86
|
+
const result = await callAdapter(request, userId);
|
|
87
|
+
return { result, modelUsed: request.model };
|
|
88
|
+
}
|
|
89
|
+
const allModels = [gateConfig.model, ...gateConfig.fallbackModels];
|
|
90
|
+
const modelIndex = Math.floor(Math.random() * allModels.length);
|
|
91
|
+
const selectedModel = allModels[modelIndex];
|
|
92
|
+
const modelRequest = { ...request, model: selectedModel };
|
|
93
|
+
const result = await callAdapter(modelRequest, userId);
|
|
94
|
+
return { result, modelUsed: selectedModel };
|
|
95
|
+
}
|
|
96
|
+
async function executeWithRouting(gateConfig, request, userId) {
|
|
97
|
+
const modelsToTry = getModelsToTry(gateConfig, request.model);
|
|
98
|
+
switch (gateConfig.routingStrategy) {
|
|
99
|
+
case 'fallback':
|
|
100
|
+
return await executeWithFallback(request, modelsToTry, userId);
|
|
101
|
+
case 'round-robin':
|
|
102
|
+
return await executeWithRoundRobin(gateConfig, request, userId);
|
|
103
|
+
case 'single':
|
|
104
|
+
default:
|
|
105
|
+
const result = await callAdapter(request, userId);
|
|
106
|
+
return { result, modelUsed: request.model };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// MARK:- Route Handler
|
|
110
|
+
router.post('/', authenticate, async (req, res) => {
|
|
111
|
+
const startTime = Date.now();
|
|
112
|
+
if (!req.userId) {
|
|
113
|
+
res.status(401).json({ error: 'unauthorized', message: 'Missing user ID' });
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const userId = req.userId;
|
|
117
|
+
let gateConfig = null;
|
|
118
|
+
let request = null;
|
|
119
|
+
try {
|
|
120
|
+
const rawRequest = req.body;
|
|
121
|
+
if (!rawRequest.gateId) {
|
|
122
|
+
res.status(400).json({ error: 'bad_request', message: 'Missing required field: gateId' });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(rawRequest.gateId);
|
|
126
|
+
if (!isUUID) {
|
|
127
|
+
res.status(400).json({ error: 'bad_request', message: 'gateId must be a valid UUID' });
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
gateConfig = await db.getGateByUserAndId(userId, rawRequest.gateId);
|
|
131
|
+
if (!gateConfig) {
|
|
132
|
+
res.status(404).json({ error: 'not_found', message: `Gate with ID "${rawRequest.gateId}" not found` });
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
// Validate chat-specific fields
|
|
136
|
+
if (!rawRequest.data?.messages || !Array.isArray(rawRequest.data.messages) || rawRequest.data.messages.length === 0) {
|
|
137
|
+
res.status(400).json({ error: 'bad_request', message: 'Missing required field: data.messages (must be a non-empty array)' });
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
// Warn if gate is configured for a different task type
|
|
141
|
+
if (gateConfig.taskType && gateConfig.taskType !== 'chat') {
|
|
142
|
+
console.warn(`[Type Mismatch] Gate "${gateConfig.name}" (${gateConfig.id}) configured for taskType="${gateConfig.taskType}" ` +
|
|
143
|
+
`but received request to /v3/chat endpoint. Processing as chat request.`);
|
|
144
|
+
}
|
|
145
|
+
request = {
|
|
146
|
+
gateId: rawRequest.gateId,
|
|
147
|
+
type: 'chat',
|
|
148
|
+
data: rawRequest.data,
|
|
149
|
+
model: rawRequest.model,
|
|
150
|
+
metadata: rawRequest.metadata
|
|
151
|
+
};
|
|
152
|
+
const finalRequest = resolveFinalRequest(gateConfig, request);
|
|
153
|
+
const { result, modelUsed } = await executeWithRouting(gateConfig, finalRequest, userId);
|
|
154
|
+
const latencyMs = Date.now() - startTime;
|
|
155
|
+
// Log request to database
|
|
156
|
+
db.logRequest({
|
|
157
|
+
userId,
|
|
158
|
+
gateId: gateConfig.id,
|
|
159
|
+
gateName: gateConfig.name,
|
|
160
|
+
modelRequested: request.model || gateConfig.model,
|
|
161
|
+
modelUsed: modelUsed,
|
|
162
|
+
promptTokens: result.usage?.promptTokens || 0,
|
|
163
|
+
completionTokens: result.usage?.completionTokens || 0,
|
|
164
|
+
totalTokens: result.usage?.totalTokens || 0,
|
|
165
|
+
costUsd: result.cost || 0,
|
|
166
|
+
latencyMs,
|
|
167
|
+
success: true,
|
|
168
|
+
errorMessage: null,
|
|
169
|
+
userAgent: req.headers['user-agent'] || null,
|
|
170
|
+
ipAddress: req.ip || null,
|
|
171
|
+
}).catch(err => console.error('Failed to log request:', err));
|
|
172
|
+
// Return LayerResponse with additional metadata
|
|
173
|
+
const response = {
|
|
174
|
+
...result,
|
|
175
|
+
model: modelUsed,
|
|
176
|
+
latencyMs,
|
|
177
|
+
};
|
|
178
|
+
res.json(response);
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
const latencyMs = Date.now() - startTime;
|
|
182
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
183
|
+
db.logRequest({
|
|
184
|
+
userId,
|
|
185
|
+
gateId: gateConfig?.id || null,
|
|
186
|
+
gateName: req.body?.gate || null,
|
|
187
|
+
modelRequested: (request?.model || gateConfig?.model) || 'unknown',
|
|
188
|
+
modelUsed: null,
|
|
189
|
+
promptTokens: 0,
|
|
190
|
+
completionTokens: 0,
|
|
191
|
+
totalTokens: 0,
|
|
192
|
+
costUsd: 0,
|
|
193
|
+
latencyMs,
|
|
194
|
+
success: false,
|
|
195
|
+
errorMessage,
|
|
196
|
+
userAgent: req.headers['user-agent'] || null,
|
|
197
|
+
ipAddress: req.ip || null,
|
|
198
|
+
}).catch(err => console.error('Failed to log request:', err));
|
|
199
|
+
console.error('Chat completion error:', error);
|
|
200
|
+
res.status(500).json({ error: 'internal_error', message: errorMessage });
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
export default router;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../../../../src/routes/v3/completions/chat.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,SAAS,CAAC;AAOpD,QAAA,MAAM,MAAM,EAAE,UAAqB,CAAC;AAoNpC,eAAe,MAAM,CAAC"}
|