@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
|
@@ -0,0 +1,178 @@
|
|
|
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
|
+
function isOverrideAllowed(allowOverrides, field) {
|
|
8
|
+
if (allowOverrides === undefined || allowOverrides === null || allowOverrides === true)
|
|
9
|
+
return true;
|
|
10
|
+
if (allowOverrides === false)
|
|
11
|
+
return false;
|
|
12
|
+
return allowOverrides[field] ?? false;
|
|
13
|
+
}
|
|
14
|
+
function resolveFinalRequest(gateConfig, request) {
|
|
15
|
+
let finalModel = gateConfig.model;
|
|
16
|
+
if (request.model && isOverrideAllowed(gateConfig.allowOverrides, OverrideField.Model)) {
|
|
17
|
+
try {
|
|
18
|
+
finalModel = normalizeModelId(request.model);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
finalModel = gateConfig.model;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const chatData = { ...request.data };
|
|
25
|
+
if (!chatData.systemPrompt && gateConfig.systemPrompt) {
|
|
26
|
+
chatData.systemPrompt = gateConfig.systemPrompt;
|
|
27
|
+
}
|
|
28
|
+
if (chatData.temperature === undefined && gateConfig.temperature !== undefined) {
|
|
29
|
+
chatData.temperature = gateConfig.temperature;
|
|
30
|
+
}
|
|
31
|
+
else if (chatData.temperature !== undefined && !isOverrideAllowed(gateConfig.allowOverrides, OverrideField.Temperature)) {
|
|
32
|
+
chatData.temperature = gateConfig.temperature;
|
|
33
|
+
}
|
|
34
|
+
if (chatData.maxTokens === undefined && gateConfig.maxTokens !== undefined) {
|
|
35
|
+
chatData.maxTokens = gateConfig.maxTokens;
|
|
36
|
+
}
|
|
37
|
+
else if (chatData.maxTokens !== undefined && !isOverrideAllowed(gateConfig.allowOverrides, OverrideField.MaxTokens)) {
|
|
38
|
+
chatData.maxTokens = gateConfig.maxTokens;
|
|
39
|
+
}
|
|
40
|
+
if (chatData.topP === undefined && gateConfig.topP !== undefined) {
|
|
41
|
+
chatData.topP = gateConfig.topP;
|
|
42
|
+
}
|
|
43
|
+
else if (chatData.topP !== undefined && !isOverrideAllowed(gateConfig.allowOverrides, OverrideField.TopP)) {
|
|
44
|
+
chatData.topP = gateConfig.topP;
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
...request,
|
|
48
|
+
model: normalizeModelId(finalModel),
|
|
49
|
+
data: chatData,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function getModelsToTry(gateConfig, primaryModel) {
|
|
53
|
+
const modelsToTry = [primaryModel];
|
|
54
|
+
if (gateConfig.fallback1) {
|
|
55
|
+
try {
|
|
56
|
+
modelsToTry.push(normalizeModelId(gateConfig.fallback1));
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
console.error(`Invalid fallback1 model: ${gateConfig.fallback1}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (gateConfig.fallback2) {
|
|
63
|
+
try {
|
|
64
|
+
modelsToTry.push(normalizeModelId(gateConfig.fallback2));
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
console.error(`Invalid fallback2 model: ${gateConfig.fallback2}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return modelsToTry;
|
|
71
|
+
}
|
|
72
|
+
async function executeWithRouting(gateConfig, request, userId) {
|
|
73
|
+
const modelsToTry = getModelsToTry(gateConfig, request.model);
|
|
74
|
+
let lastError = null;
|
|
75
|
+
for (let i = 0; i < modelsToTry.length; i++) {
|
|
76
|
+
const model = modelsToTry[i];
|
|
77
|
+
const requestWithModel = {
|
|
78
|
+
...request,
|
|
79
|
+
model,
|
|
80
|
+
};
|
|
81
|
+
try {
|
|
82
|
+
const result = await callAdapter(requestWithModel, userId);
|
|
83
|
+
return { result, modelUsed: model };
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
console.error(`Model ${model} failed (attempt ${i + 1}/${modelsToTry.length}):`, error);
|
|
87
|
+
lastError = error;
|
|
88
|
+
if (i < modelsToTry.length - 1) {
|
|
89
|
+
console.log(`Trying fallback model: ${modelsToTry[i + 1]}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
throw lastError || new Error('All models failed');
|
|
94
|
+
}
|
|
95
|
+
router.post('/', authenticate, async (req, res) => {
|
|
96
|
+
const startTime = Date.now();
|
|
97
|
+
if (!req.userId) {
|
|
98
|
+
res.status(401).json({ error: 'unauthorized', message: 'Missing user ID' });
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const userId = req.userId;
|
|
102
|
+
let gateConfig = null;
|
|
103
|
+
try {
|
|
104
|
+
const rawRequest = req.body;
|
|
105
|
+
if (!rawRequest.gateId) {
|
|
106
|
+
res.status(400).json({ error: 'bad_request', message: 'Missing required field: gateId' });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (!rawRequest.data || !rawRequest.data.messages || !Array.isArray(rawRequest.data.messages) || rawRequest.data.messages.length === 0) {
|
|
110
|
+
res.status(400).json({ error: 'bad_request', message: 'Missing required field: data.messages (must be a non-empty array)' });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
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);
|
|
114
|
+
if (!isUUID) {
|
|
115
|
+
res.status(400).json({ error: 'bad_request', message: 'gateId must be a valid UUID' });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
gateConfig = await db.getGateByUserAndId(userId, rawRequest.gateId);
|
|
119
|
+
if (!gateConfig) {
|
|
120
|
+
res.status(404).json({ error: 'not_found', message: `Gate with ID "${rawRequest.gateId}" not found` });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (gateConfig.taskType && gateConfig.taskType !== 'chat') {
|
|
124
|
+
console.warn(`[Type Mismatch] Gate "${gateConfig.name}" (${gateConfig.id}) configured for taskType="${gateConfig.taskType}" ` +
|
|
125
|
+
`but received request to /v3/chat endpoint. Processing as chat request.`);
|
|
126
|
+
}
|
|
127
|
+
const request = {
|
|
128
|
+
gateId: rawRequest.gateId,
|
|
129
|
+
type: 'chat',
|
|
130
|
+
data: rawRequest.data,
|
|
131
|
+
model: rawRequest.model,
|
|
132
|
+
metadata: rawRequest.metadata
|
|
133
|
+
};
|
|
134
|
+
const finalRequest = resolveFinalRequest(gateConfig, request);
|
|
135
|
+
const { result, modelUsed } = await executeWithRouting(gateConfig, finalRequest, userId);
|
|
136
|
+
const latencyMs = Date.now() - startTime;
|
|
137
|
+
db.logRequest({
|
|
138
|
+
userId,
|
|
139
|
+
gateId: gateConfig.id,
|
|
140
|
+
model: modelUsed,
|
|
141
|
+
promptTokens: result.usage?.promptTokens || 0,
|
|
142
|
+
completionTokens: result.usage?.completionTokens || 0,
|
|
143
|
+
totalTokens: result.usage?.totalTokens || 0,
|
|
144
|
+
cost: result.cost || 0,
|
|
145
|
+
latencyMs,
|
|
146
|
+
success: true,
|
|
147
|
+
}).catch(error => {
|
|
148
|
+
console.error('Failed to log request:', error);
|
|
149
|
+
});
|
|
150
|
+
res.status(200).json(result);
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
const latencyMs = Date.now() - startTime;
|
|
154
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
155
|
+
console.error('[v3/chat] Error processing request:', error);
|
|
156
|
+
if (gateConfig) {
|
|
157
|
+
db.logRequest({
|
|
158
|
+
userId,
|
|
159
|
+
gateId: gateConfig.id,
|
|
160
|
+
model: gateConfig.model,
|
|
161
|
+
promptTokens: 0,
|
|
162
|
+
completionTokens: 0,
|
|
163
|
+
totalTokens: 0,
|
|
164
|
+
cost: 0,
|
|
165
|
+
latencyMs,
|
|
166
|
+
success: false,
|
|
167
|
+
errorMessage,
|
|
168
|
+
}).catch(logError => {
|
|
169
|
+
console.error('Failed to log error:', logError);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
res.status(500).json({
|
|
173
|
+
error: 'internal_server_error',
|
|
174
|
+
message: errorMessage
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
export default router;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"embed.d.ts","sourceRoot":"","sources":["../../../../src/routes/v3/completions/embed.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,SAAS,CAAC;AAOpD,QAAA,MAAM,MAAM,EAAE,UAAqB,CAAC;AAgGpC,eAAe,MAAM,CAAC"}
|
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
function isOverrideAllowed(allowOverrides, field) {
|
|
8
|
+
if (allowOverrides === undefined || allowOverrides === null || allowOverrides === true)
|
|
9
|
+
return true;
|
|
10
|
+
if (allowOverrides === false)
|
|
11
|
+
return false;
|
|
12
|
+
return allowOverrides[field] ?? false;
|
|
13
|
+
}
|
|
14
|
+
function resolveFinalRequest(gateConfig, request) {
|
|
15
|
+
let finalModel = gateConfig.model;
|
|
16
|
+
if (request.model && isOverrideAllowed(gateConfig.allowOverrides, OverrideField.Model)) {
|
|
17
|
+
try {
|
|
18
|
+
finalModel = normalizeModelId(request.model);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
finalModel = gateConfig.model;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return { ...request, model: normalizeModelId(finalModel) };
|
|
25
|
+
}
|
|
26
|
+
function getModelsToTry(gateConfig, primaryModel) {
|
|
27
|
+
const modelsToTry = [primaryModel];
|
|
28
|
+
if (gateConfig.fallback1) {
|
|
29
|
+
try {
|
|
30
|
+
modelsToTry.push(normalizeModelId(gateConfig.fallback1));
|
|
31
|
+
}
|
|
32
|
+
catch (error) { }
|
|
33
|
+
}
|
|
34
|
+
if (gateConfig.fallback2) {
|
|
35
|
+
try {
|
|
36
|
+
modelsToTry.push(normalizeModelId(gateConfig.fallback2));
|
|
37
|
+
}
|
|
38
|
+
catch (error) { }
|
|
39
|
+
}
|
|
40
|
+
return modelsToTry;
|
|
41
|
+
}
|
|
42
|
+
async function executeWithRouting(gateConfig, request, userId) {
|
|
43
|
+
const modelsToTry = getModelsToTry(gateConfig, request.model);
|
|
44
|
+
let lastError = null;
|
|
45
|
+
for (let i = 0; i < modelsToTry.length; i++) {
|
|
46
|
+
const model = modelsToTry[i];
|
|
47
|
+
try {
|
|
48
|
+
const result = await callAdapter({ ...request, model }, userId);
|
|
49
|
+
return { result, modelUsed: model };
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
lastError = error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
throw lastError || new Error('All models failed');
|
|
56
|
+
}
|
|
57
|
+
router.post('/', authenticate, async (req, res) => {
|
|
58
|
+
const startTime = Date.now();
|
|
59
|
+
if (!req.userId) {
|
|
60
|
+
res.status(401).json({ error: 'unauthorized', message: 'Missing user ID' });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const userId = req.userId;
|
|
64
|
+
let gateConfig = null;
|
|
65
|
+
try {
|
|
66
|
+
const rawRequest = req.body;
|
|
67
|
+
if (!rawRequest.gateId || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(rawRequest.gateId)) {
|
|
68
|
+
res.status(400).json({ error: 'bad_request', message: 'Valid gateId (UUID) is required' });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (!rawRequest.data?.prompt || typeof rawRequest.data.input !== 'string' || !rawRequest.data.input.trim()) {
|
|
72
|
+
res.status(400).json({ error: 'bad_request', message: 'data.input is required' });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
gateConfig = await db.getGateByUserAndId(userId, rawRequest.gateId);
|
|
76
|
+
if (!gateConfig) {
|
|
77
|
+
res.status(404).json({ error: 'not_found', message: `Gate not found` });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const request = { gateId: rawRequest.gateId, type: 'embeddings', data: rawRequest.data, model: rawRequest.model, metadata: rawRequest.metadata };
|
|
81
|
+
const finalRequest = resolveFinalRequest(gateConfig, request);
|
|
82
|
+
const { result, modelUsed } = await executeWithRouting(gateConfig, finalRequest, userId);
|
|
83
|
+
db.logRequest({ userId, gateId: gateConfig.id, model: modelUsed, promptTokens: result.usage?.promptTokens || 0, completionTokens: result.usage?.completionTokens || 0, totalTokens: result.usage?.totalTokens || 0, cost: result.cost || 0, latencyMs: Date.now() - startTime, success: true }).catch(() => { });
|
|
84
|
+
res.status(200).json(result);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
88
|
+
if (gateConfig) {
|
|
89
|
+
db.logRequest({ userId, gateId: gateConfig.id, model: gateConfig.model, promptTokens: 0, completionTokens: 0, totalTokens: 0, cost: 0, latencyMs: Date.now() - startTime, success: false, errorMessage }).catch(() => { });
|
|
90
|
+
}
|
|
91
|
+
res.status(500).json({ error: 'internal_server_error', message: errorMessage });
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
export default router;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../../../../src/routes/v3/completions/image.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,SAAS,CAAC;AAOpD,QAAA,MAAM,MAAM,EAAE,UAAqB,CAAC;AA2LpC,eAAe,MAAM,CAAC"}
|
|
@@ -0,0 +1,155 @@
|
|
|
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
|
+
function isOverrideAllowed(allowOverrides, field) {
|
|
8
|
+
if (allowOverrides === undefined || allowOverrides === null || allowOverrides === true)
|
|
9
|
+
return true;
|
|
10
|
+
if (allowOverrides === false)
|
|
11
|
+
return false;
|
|
12
|
+
return allowOverrides[field] ?? false;
|
|
13
|
+
}
|
|
14
|
+
function resolveFinalRequest(gateConfig, request) {
|
|
15
|
+
let finalModel = gateConfig.model;
|
|
16
|
+
if (request.model && isOverrideAllowed(gateConfig.allowOverrides, OverrideField.Model)) {
|
|
17
|
+
try {
|
|
18
|
+
finalModel = normalizeModelId(request.model);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
finalModel = gateConfig.model;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
...request,
|
|
26
|
+
model: normalizeModelId(finalModel),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function getModelsToTry(gateConfig, primaryModel) {
|
|
30
|
+
const modelsToTry = [primaryModel];
|
|
31
|
+
if (gateConfig.fallback1) {
|
|
32
|
+
try {
|
|
33
|
+
modelsToTry.push(normalizeModelId(gateConfig.fallback1));
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
console.error(`Invalid fallback1 model: ${gateConfig.fallback1}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (gateConfig.fallback2) {
|
|
40
|
+
try {
|
|
41
|
+
modelsToTry.push(normalizeModelId(gateConfig.fallback2));
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
console.error(`Invalid fallback2 model: ${gateConfig.fallback2}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return modelsToTry;
|
|
48
|
+
}
|
|
49
|
+
async function executeWithRouting(gateConfig, request, userId) {
|
|
50
|
+
const modelsToTry = getModelsToTry(gateConfig, request.model);
|
|
51
|
+
let lastError = null;
|
|
52
|
+
for (let i = 0; i < modelsToTry.length; i++) {
|
|
53
|
+
const model = modelsToTry[i];
|
|
54
|
+
const requestWithModel = {
|
|
55
|
+
...request,
|
|
56
|
+
model,
|
|
57
|
+
};
|
|
58
|
+
try {
|
|
59
|
+
const result = await callAdapter(requestWithModel, userId);
|
|
60
|
+
return { result, modelUsed: model };
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
console.error(`Model ${model} failed (attempt ${i + 1}/${modelsToTry.length}):`, error);
|
|
64
|
+
lastError = error;
|
|
65
|
+
if (i < modelsToTry.length - 1) {
|
|
66
|
+
console.log(`Trying fallback model: ${modelsToTry[i + 1]}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
throw lastError || new Error('All models failed');
|
|
71
|
+
}
|
|
72
|
+
router.post('/', authenticate, async (req, res) => {
|
|
73
|
+
const startTime = Date.now();
|
|
74
|
+
if (!req.userId) {
|
|
75
|
+
res.status(401).json({ error: 'unauthorized', message: 'Missing user ID' });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const userId = req.userId;
|
|
79
|
+
let gateConfig = null;
|
|
80
|
+
try {
|
|
81
|
+
const rawRequest = req.body;
|
|
82
|
+
if (!rawRequest.gateId) {
|
|
83
|
+
res.status(400).json({ error: 'bad_request', message: 'Missing required field: gateId' });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (!rawRequest.data || !rawRequest.data.prompt || typeof rawRequest.data.prompt !== 'string' || rawRequest.data.prompt.trim().length === 0) {
|
|
87
|
+
res.status(400).json({ error: 'bad_request', message: 'Missing required field: data.prompt (must be a non-empty string)' });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
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);
|
|
91
|
+
if (!isUUID) {
|
|
92
|
+
res.status(400).json({ error: 'bad_request', message: 'gateId must be a valid UUID' });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
gateConfig = await db.getGateByUserAndId(userId, rawRequest.gateId);
|
|
96
|
+
if (!gateConfig) {
|
|
97
|
+
res.status(404).json({ error: 'not_found', message: `Gate with ID "${rawRequest.gateId}" not found` });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (gateConfig.taskType && gateConfig.taskType !== 'image') {
|
|
101
|
+
console.warn(`[Type Mismatch] Gate "${gateConfig.name}" (${gateConfig.id}) configured for taskType="${gateConfig.taskType}" ` +
|
|
102
|
+
`but received request to /v3/image endpoint. Processing as image request.`);
|
|
103
|
+
}
|
|
104
|
+
const request = {
|
|
105
|
+
gateId: rawRequest.gateId,
|
|
106
|
+
type: 'image',
|
|
107
|
+
data: rawRequest.data,
|
|
108
|
+
model: rawRequest.model,
|
|
109
|
+
metadata: rawRequest.metadata
|
|
110
|
+
};
|
|
111
|
+
const finalRequest = resolveFinalRequest(gateConfig, request);
|
|
112
|
+
const { result, modelUsed } = await executeWithRouting(gateConfig, finalRequest, userId);
|
|
113
|
+
const latencyMs = Date.now() - startTime;
|
|
114
|
+
db.logRequest({
|
|
115
|
+
userId,
|
|
116
|
+
gateId: gateConfig.id,
|
|
117
|
+
model: modelUsed,
|
|
118
|
+
promptTokens: result.usage?.promptTokens || 0,
|
|
119
|
+
completionTokens: result.usage?.completionTokens || 0,
|
|
120
|
+
totalTokens: result.usage?.totalTokens || 0,
|
|
121
|
+
cost: result.cost || 0,
|
|
122
|
+
latencyMs,
|
|
123
|
+
success: true,
|
|
124
|
+
}).catch(error => {
|
|
125
|
+
console.error('Failed to log request:', error);
|
|
126
|
+
});
|
|
127
|
+
res.status(200).json(result);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
const latencyMs = Date.now() - startTime;
|
|
131
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
132
|
+
console.error('[v3/image] Error processing request:', error);
|
|
133
|
+
if (gateConfig) {
|
|
134
|
+
db.logRequest({
|
|
135
|
+
userId,
|
|
136
|
+
gateId: gateConfig.id,
|
|
137
|
+
model: gateConfig.model,
|
|
138
|
+
promptTokens: 0,
|
|
139
|
+
completionTokens: 0,
|
|
140
|
+
totalTokens: 0,
|
|
141
|
+
cost: 0,
|
|
142
|
+
latencyMs,
|
|
143
|
+
success: false,
|
|
144
|
+
errorMessage,
|
|
145
|
+
}).catch(logError => {
|
|
146
|
+
console.error('Failed to log error:', logError);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
res.status(500).json({
|
|
150
|
+
error: 'internal_server_error',
|
|
151
|
+
message: errorMessage
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
export default router;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ocr.d.ts","sourceRoot":"","sources":["../../../../src/routes/v3/completions/ocr.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,SAAS,CAAC;AAOpD,QAAA,MAAM,MAAM,EAAE,UAAqB,CAAC;AAgGpC,eAAe,MAAM,CAAC"}
|
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
function isOverrideAllowed(allowOverrides, field) {
|
|
8
|
+
if (allowOverrides === undefined || allowOverrides === null || allowOverrides === true)
|
|
9
|
+
return true;
|
|
10
|
+
if (allowOverrides === false)
|
|
11
|
+
return false;
|
|
12
|
+
return allowOverrides[field] ?? false;
|
|
13
|
+
}
|
|
14
|
+
function resolveFinalRequest(gateConfig, request) {
|
|
15
|
+
let finalModel = gateConfig.model;
|
|
16
|
+
if (request.model && isOverrideAllowed(gateConfig.allowOverrides, OverrideField.Model)) {
|
|
17
|
+
try {
|
|
18
|
+
finalModel = normalizeModelId(request.model);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
finalModel = gateConfig.model;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return { ...request, model: normalizeModelId(finalModel) };
|
|
25
|
+
}
|
|
26
|
+
function getModelsToTry(gateConfig, primaryModel) {
|
|
27
|
+
const modelsToTry = [primaryModel];
|
|
28
|
+
if (gateConfig.fallback1) {
|
|
29
|
+
try {
|
|
30
|
+
modelsToTry.push(normalizeModelId(gateConfig.fallback1));
|
|
31
|
+
}
|
|
32
|
+
catch (error) { }
|
|
33
|
+
}
|
|
34
|
+
if (gateConfig.fallback2) {
|
|
35
|
+
try {
|
|
36
|
+
modelsToTry.push(normalizeModelId(gateConfig.fallback2));
|
|
37
|
+
}
|
|
38
|
+
catch (error) { }
|
|
39
|
+
}
|
|
40
|
+
return modelsToTry;
|
|
41
|
+
}
|
|
42
|
+
async function executeWithRouting(gateConfig, request, userId) {
|
|
43
|
+
const modelsToTry = getModelsToTry(gateConfig, request.model);
|
|
44
|
+
let lastError = null;
|
|
45
|
+
for (let i = 0; i < modelsToTry.length; i++) {
|
|
46
|
+
const model = modelsToTry[i];
|
|
47
|
+
try {
|
|
48
|
+
const result = await callAdapter({ ...request, model }, userId);
|
|
49
|
+
return { result, modelUsed: model };
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
lastError = error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
throw lastError || new Error('All models failed');
|
|
56
|
+
}
|
|
57
|
+
router.post('/', authenticate, async (req, res) => {
|
|
58
|
+
const startTime = Date.now();
|
|
59
|
+
if (!req.userId) {
|
|
60
|
+
res.status(401).json({ error: 'unauthorized', message: 'Missing user ID' });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const userId = req.userId;
|
|
64
|
+
let gateConfig = null;
|
|
65
|
+
try {
|
|
66
|
+
const rawRequest = req.body;
|
|
67
|
+
if (!rawRequest.gateId || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(rawRequest.gateId)) {
|
|
68
|
+
res.status(400).json({ error: 'bad_request', message: 'Valid gateId (UUID) is required' });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (!rawRequest.data?.prompt || typeof rawRequest.data.documentUrl !== 'string' || !rawRequest.data.documentUrl.trim()) {
|
|
72
|
+
res.status(400).json({ error: 'bad_request', message: 'data.documentUrl is required' });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
gateConfig = await db.getGateByUserAndId(userId, rawRequest.gateId);
|
|
76
|
+
if (!gateConfig) {
|
|
77
|
+
res.status(404).json({ error: 'not_found', message: `Gate not found` });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const request = { gateId: rawRequest.gateId, type: 'ocr', data: rawRequest.data, model: rawRequest.model, metadata: rawRequest.metadata };
|
|
81
|
+
const finalRequest = resolveFinalRequest(gateConfig, request);
|
|
82
|
+
const { result, modelUsed } = await executeWithRouting(gateConfig, finalRequest, userId);
|
|
83
|
+
db.logRequest({ userId, gateId: gateConfig.id, model: modelUsed, promptTokens: result.usage?.promptTokens || 0, completionTokens: result.usage?.completionTokens || 0, totalTokens: result.usage?.totalTokens || 0, cost: result.cost || 0, latencyMs: Date.now() - startTime, success: true }).catch(() => { });
|
|
84
|
+
res.status(200).json(result);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
88
|
+
if (gateConfig) {
|
|
89
|
+
db.logRequest({ userId, gateId: gateConfig.id, model: gateConfig.model, promptTokens: 0, completionTokens: 0, totalTokens: 0, cost: 0, latencyMs: Date.now() - startTime, success: false, errorMessage }).catch(() => { });
|
|
90
|
+
}
|
|
91
|
+
res.status(500).json({ error: 'internal_server_error', message: errorMessage });
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
export default router;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tts.d.ts","sourceRoot":"","sources":["../../../../src/routes/v3/completions/tts.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,SAAS,CAAC;AAOpD,QAAA,MAAM,MAAM,EAAE,UAAqB,CAAC;AAgGpC,eAAe,MAAM,CAAC"}
|
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
function isOverrideAllowed(allowOverrides, field) {
|
|
8
|
+
if (allowOverrides === undefined || allowOverrides === null || allowOverrides === true)
|
|
9
|
+
return true;
|
|
10
|
+
if (allowOverrides === false)
|
|
11
|
+
return false;
|
|
12
|
+
return allowOverrides[field] ?? false;
|
|
13
|
+
}
|
|
14
|
+
function resolveFinalRequest(gateConfig, request) {
|
|
15
|
+
let finalModel = gateConfig.model;
|
|
16
|
+
if (request.model && isOverrideAllowed(gateConfig.allowOverrides, OverrideField.Model)) {
|
|
17
|
+
try {
|
|
18
|
+
finalModel = normalizeModelId(request.model);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
finalModel = gateConfig.model;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return { ...request, model: normalizeModelId(finalModel) };
|
|
25
|
+
}
|
|
26
|
+
function getModelsToTry(gateConfig, primaryModel) {
|
|
27
|
+
const modelsToTry = [primaryModel];
|
|
28
|
+
if (gateConfig.fallback1) {
|
|
29
|
+
try {
|
|
30
|
+
modelsToTry.push(normalizeModelId(gateConfig.fallback1));
|
|
31
|
+
}
|
|
32
|
+
catch (error) { }
|
|
33
|
+
}
|
|
34
|
+
if (gateConfig.fallback2) {
|
|
35
|
+
try {
|
|
36
|
+
modelsToTry.push(normalizeModelId(gateConfig.fallback2));
|
|
37
|
+
}
|
|
38
|
+
catch (error) { }
|
|
39
|
+
}
|
|
40
|
+
return modelsToTry;
|
|
41
|
+
}
|
|
42
|
+
async function executeWithRouting(gateConfig, request, userId) {
|
|
43
|
+
const modelsToTry = getModelsToTry(gateConfig, request.model);
|
|
44
|
+
let lastError = null;
|
|
45
|
+
for (let i = 0; i < modelsToTry.length; i++) {
|
|
46
|
+
const model = modelsToTry[i];
|
|
47
|
+
try {
|
|
48
|
+
const result = await callAdapter({ ...request, model }, userId);
|
|
49
|
+
return { result, modelUsed: model };
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
lastError = error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
throw lastError || new Error('All models failed');
|
|
56
|
+
}
|
|
57
|
+
router.post('/', authenticate, async (req, res) => {
|
|
58
|
+
const startTime = Date.now();
|
|
59
|
+
if (!req.userId) {
|
|
60
|
+
res.status(401).json({ error: 'unauthorized', message: 'Missing user ID' });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const userId = req.userId;
|
|
64
|
+
let gateConfig = null;
|
|
65
|
+
try {
|
|
66
|
+
const rawRequest = req.body;
|
|
67
|
+
if (!rawRequest.gateId || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(rawRequest.gateId)) {
|
|
68
|
+
res.status(400).json({ error: 'bad_request', message: 'Valid gateId (UUID) is required' });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (!rawRequest.data?.prompt || typeof rawRequest.data.input !== 'string' || !rawRequest.data.input.trim()) {
|
|
72
|
+
res.status(400).json({ error: 'bad_request', message: 'data.input is required' });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
gateConfig = await db.getGateByUserAndId(userId, rawRequest.gateId);
|
|
76
|
+
if (!gateConfig) {
|
|
77
|
+
res.status(404).json({ error: 'not_found', message: `Gate not found` });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const request = { gateId: rawRequest.gateId, type: 'tts', data: rawRequest.data, model: rawRequest.model, metadata: rawRequest.metadata };
|
|
81
|
+
const finalRequest = resolveFinalRequest(gateConfig, request);
|
|
82
|
+
const { result, modelUsed } = await executeWithRouting(gateConfig, finalRequest, userId);
|
|
83
|
+
db.logRequest({ userId, gateId: gateConfig.id, model: modelUsed, promptTokens: result.usage?.promptTokens || 0, completionTokens: result.usage?.completionTokens || 0, totalTokens: result.usage?.totalTokens || 0, cost: result.cost || 0, latencyMs: Date.now() - startTime, success: true }).catch(() => { });
|
|
84
|
+
res.status(200).json(result);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
88
|
+
if (gateConfig) {
|
|
89
|
+
db.logRequest({ userId, gateId: gateConfig.id, model: gateConfig.model, promptTokens: 0, completionTokens: 0, totalTokens: 0, cost: 0, latencyMs: Date.now() - startTime, success: false, errorMessage }).catch(() => { });
|
|
90
|
+
}
|
|
91
|
+
res.status(500).json({ error: 'internal_server_error', message: errorMessage });
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
export default router;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"video.d.ts","sourceRoot":"","sources":["../../../../src/routes/v3/completions/video.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,SAAS,CAAC;AAOpD,QAAA,MAAM,MAAM,EAAE,UAAqB,CAAC;AAgGpC,eAAe,MAAM,CAAC"}
|