@layer-ai/core 2.0.3 → 2.0.5

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 CHANGED
@@ -5,6 +5,10 @@ export { default as logsRouter } from './routes/v1/logs.js';
5
5
  export { default as completeRouter } from './routes/v2/complete.js';
6
6
  export { default as chatRouter } from './routes/v3/chat.js';
7
7
  export { default as imageRouter } from './routes/v3/image.js';
8
+ export { default as videoRouter } from './routes/v3/video.js';
9
+ export { default as embeddingsRouter } from './routes/v3/embeddings.js';
10
+ export { default as ttsRouter } from './routes/v3/tts.js';
11
+ export { default as ocrRouter } from './routes/v3/ocr.js';
8
12
  export { authenticate } from './middleware/auth.js';
9
13
  export type {} from './middleware/auth.js';
10
14
  export { db } from './lib/db/postgres.js';
@@ -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;AAG5D,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;AAG9D,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"}
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;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
@@ -8,6 +8,10 @@ export { default as completeRouter } from './routes/v2/complete.js';
8
8
  // v3 routes
9
9
  export { default as chatRouter } from './routes/v3/chat.js';
10
10
  export { default as imageRouter } from './routes/v3/image.js';
11
+ export { default as videoRouter } from './routes/v3/video.js';
12
+ export { default as embeddingsRouter } from './routes/v3/embeddings.js';
13
+ export { default as ttsRouter } from './routes/v3/tts.js';
14
+ export { default as ocrRouter } from './routes/v3/ocr.js';
11
15
  // Middleware
12
16
  export { authenticate } from './middleware/auth.js';
13
17
  // Database
@@ -0,0 +1,4 @@
1
+ import type { Router as RouterType } from 'express';
2
+ declare const router: RouterType;
3
+ export default router;
4
+ //# sourceMappingURL=embeddings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embeddings.d.ts","sourceRoot":"","sources":["../../../src/routes/v3/embeddings.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,SAAS,CAAC;AAOpD,QAAA,MAAM,MAAM,EAAE,UAAqB,CAAC;AA8OpC,eAAe,MAAM,CAAC"}
@@ -0,0 +1,196 @@
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
+ // For embeddings, we don't merge gate config like we do for chat
26
+ return {
27
+ ...request,
28
+ type: 'embeddings',
29
+ model: normalizeModelId(finalModel),
30
+ };
31
+ }
32
+ function getModelsToTry(gateConfig, primaryModel) {
33
+ const modelsToTry = [primaryModel];
34
+ if (gateConfig.routingStrategy === 'fallback' && gateConfig.fallbackModels?.length) {
35
+ modelsToTry.push(...gateConfig.fallbackModels);
36
+ }
37
+ return modelsToTry;
38
+ }
39
+ async function executeWithFallback(request, modelsToTry, userId) {
40
+ let result = null;
41
+ let lastError = null;
42
+ let modelUsed = request.model;
43
+ for (const modelToTry of modelsToTry) {
44
+ try {
45
+ const modelRequest = { ...request, model: modelToTry };
46
+ result = await callAdapter(modelRequest, userId);
47
+ modelUsed = modelToTry;
48
+ break;
49
+ }
50
+ catch (error) {
51
+ lastError = error;
52
+ console.log(`Model ${modelToTry} failed, trying next fallback...`, error instanceof Error ? error.message : error);
53
+ continue;
54
+ }
55
+ }
56
+ if (!result) {
57
+ throw lastError || new Error('All models failed');
58
+ }
59
+ return { result, modelUsed };
60
+ }
61
+ async function executeWithRoundRobin(gateConfig, request, userId) {
62
+ if (!gateConfig.fallbackModels?.length) {
63
+ const result = await callAdapter(request, userId);
64
+ return { result, modelUsed: request.model };
65
+ }
66
+ const allModels = [gateConfig.model, ...gateConfig.fallbackModels];
67
+ const modelIndex = Math.floor(Math.random() * allModels.length);
68
+ const selectedModel = allModels[modelIndex];
69
+ const modelRequest = { ...request, model: selectedModel };
70
+ const result = await callAdapter(modelRequest, userId);
71
+ return { result, modelUsed: selectedModel };
72
+ }
73
+ async function executeWithRouting(gateConfig, request, userId) {
74
+ const modelsToTry = getModelsToTry(gateConfig, request.model);
75
+ switch (gateConfig.routingStrategy) {
76
+ case 'fallback':
77
+ return await executeWithFallback(request, modelsToTry, userId);
78
+ case 'round-robin':
79
+ return await executeWithRoundRobin(gateConfig, request, userId);
80
+ case 'single':
81
+ default:
82
+ const result = await callAdapter(request, userId);
83
+ return { result, modelUsed: request.model };
84
+ }
85
+ }
86
+ // MARK:- Route Handler
87
+ router.post('/', authenticate, async (req, res) => {
88
+ const startTime = Date.now();
89
+ if (!req.userId) {
90
+ res.status(401).json({ error: 'unauthorized', message: 'Missing user ID' });
91
+ return;
92
+ }
93
+ const userId = req.userId;
94
+ let gateConfig = null;
95
+ let request = null;
96
+ try {
97
+ const rawRequest = req.body;
98
+ if (!rawRequest.gateId) {
99
+ res.status(400).json({ error: 'bad_request', message: 'Missing required field: gateId' });
100
+ return;
101
+ }
102
+ 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);
103
+ if (!isUUID) {
104
+ res.status(400).json({ error: 'bad_request', message: 'gateId must be a valid UUID' });
105
+ return;
106
+ }
107
+ gateConfig = await db.getGateByUserAndId(userId, rawRequest.gateId);
108
+ if (!gateConfig) {
109
+ res.status(404).json({ error: 'not_found', message: `Gate with ID "${rawRequest.gateId}" not found` });
110
+ return;
111
+ }
112
+ // Validate embeddings-specific fields
113
+ if (!rawRequest.data?.input) {
114
+ res.status(400).json({ error: 'bad_request', message: 'Missing required field: data.input' });
115
+ return;
116
+ }
117
+ // Validate input is string or array of strings
118
+ const input = rawRequest.data.input;
119
+ if (typeof input !== 'string' && !Array.isArray(input)) {
120
+ res.status(400).json({ error: 'bad_request', message: 'data.input must be a string or array of strings' });
121
+ return;
122
+ }
123
+ if (Array.isArray(input)) {
124
+ if (input.length === 0) {
125
+ res.status(400).json({ error: 'bad_request', message: 'data.input array must not be empty' });
126
+ return;
127
+ }
128
+ if (!input.every(item => typeof item === 'string')) {
129
+ res.status(400).json({ error: 'bad_request', message: 'data.input array must contain only strings' });
130
+ return;
131
+ }
132
+ }
133
+ // Warn if gate is configured for a different task type
134
+ if (gateConfig.taskType && gateConfig.taskType !== 'embeddings') {
135
+ console.warn(`[Type Mismatch] Gate "${gateConfig.name}" (${gateConfig.id}) configured for taskType="${gateConfig.taskType}" ` +
136
+ `but received request to /v3/embeddings endpoint. Processing as embeddings request.`);
137
+ }
138
+ request = {
139
+ gateId: rawRequest.gateId,
140
+ type: 'embeddings',
141
+ data: rawRequest.data,
142
+ model: rawRequest.model,
143
+ metadata: rawRequest.metadata
144
+ };
145
+ const finalRequest = resolveFinalRequest(gateConfig, request);
146
+ const { result, modelUsed } = await executeWithRouting(gateConfig, finalRequest, userId);
147
+ const latencyMs = Date.now() - startTime;
148
+ // Log request to database
149
+ db.logRequest({
150
+ userId,
151
+ gateId: gateConfig.id,
152
+ gateName: gateConfig.name,
153
+ modelRequested: request.model || gateConfig.model,
154
+ modelUsed: modelUsed,
155
+ promptTokens: result.usage?.promptTokens || 0,
156
+ completionTokens: result.usage?.completionTokens || 0,
157
+ totalTokens: result.usage?.totalTokens || 0,
158
+ costUsd: result.cost || 0,
159
+ latencyMs,
160
+ success: true,
161
+ errorMessage: null,
162
+ userAgent: req.headers['user-agent'] || null,
163
+ ipAddress: req.ip || null,
164
+ }).catch(err => console.error('Failed to log request:', err));
165
+ // Return LayerResponse with additional metadata
166
+ const response = {
167
+ ...result,
168
+ model: modelUsed,
169
+ latencyMs,
170
+ };
171
+ res.json(response);
172
+ }
173
+ catch (error) {
174
+ const latencyMs = Date.now() - startTime;
175
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
176
+ db.logRequest({
177
+ userId,
178
+ gateId: gateConfig?.id || null,
179
+ gateName: req.body?.gate || null,
180
+ modelRequested: (request?.model || gateConfig?.model) || 'unknown',
181
+ modelUsed: null,
182
+ promptTokens: 0,
183
+ completionTokens: 0,
184
+ totalTokens: 0,
185
+ costUsd: 0,
186
+ latencyMs,
187
+ success: false,
188
+ errorMessage,
189
+ userAgent: req.headers['user-agent'] || null,
190
+ ipAddress: req.ip || null,
191
+ }).catch(err => console.error('Failed to log request:', err));
192
+ console.error('Embeddings error:', error);
193
+ res.status(500).json({ error: 'internal_error', message: errorMessage });
194
+ }
195
+ });
196
+ export default router;
@@ -0,0 +1,4 @@
1
+ import type { Router as RouterType } from 'express';
2
+ declare const router: RouterType;
3
+ export default router;
4
+ //# sourceMappingURL=ocr.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ocr.d.ts","sourceRoot":"","sources":["../../../src/routes/v3/ocr.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,SAAS,CAAC;AAOpD,QAAA,MAAM,MAAM,EAAE,UAAqB,CAAC;AAgOpC,eAAe,MAAM,CAAC"}
@@ -0,0 +1,184 @@
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
+ // For OCR, we don't merge gate config like we do for chat
26
+ return {
27
+ ...request,
28
+ type: 'ocr',
29
+ model: normalizeModelId(finalModel),
30
+ };
31
+ }
32
+ function getModelsToTry(gateConfig, primaryModel) {
33
+ const modelsToTry = [primaryModel];
34
+ if (gateConfig.routingStrategy === 'fallback' && gateConfig.fallbackModels?.length) {
35
+ modelsToTry.push(...gateConfig.fallbackModels);
36
+ }
37
+ return modelsToTry;
38
+ }
39
+ async function executeWithFallback(request, modelsToTry, userId) {
40
+ let result = null;
41
+ let lastError = null;
42
+ let modelUsed = request.model;
43
+ for (const modelToTry of modelsToTry) {
44
+ try {
45
+ const modelRequest = { ...request, model: modelToTry };
46
+ result = await callAdapter(modelRequest, userId);
47
+ modelUsed = modelToTry;
48
+ break;
49
+ }
50
+ catch (error) {
51
+ lastError = error;
52
+ console.log(`Model ${modelToTry} failed, trying next fallback...`, error instanceof Error ? error.message : error);
53
+ continue;
54
+ }
55
+ }
56
+ if (!result) {
57
+ throw lastError || new Error('All models failed');
58
+ }
59
+ return { result, modelUsed };
60
+ }
61
+ async function executeWithRoundRobin(gateConfig, request, userId) {
62
+ if (!gateConfig.fallbackModels?.length) {
63
+ const result = await callAdapter(request, userId);
64
+ return { result, modelUsed: request.model };
65
+ }
66
+ const allModels = [gateConfig.model, ...gateConfig.fallbackModels];
67
+ const modelIndex = Math.floor(Math.random() * allModels.length);
68
+ const selectedModel = allModels[modelIndex];
69
+ const modelRequest = { ...request, model: selectedModel };
70
+ const result = await callAdapter(modelRequest, userId);
71
+ return { result, modelUsed: selectedModel };
72
+ }
73
+ async function executeWithRouting(gateConfig, request, userId) {
74
+ const modelsToTry = getModelsToTry(gateConfig, request.model);
75
+ switch (gateConfig.routingStrategy) {
76
+ case 'fallback':
77
+ return await executeWithFallback(request, modelsToTry, userId);
78
+ case 'round-robin':
79
+ return await executeWithRoundRobin(gateConfig, request, userId);
80
+ case 'single':
81
+ default:
82
+ const result = await callAdapter(request, userId);
83
+ return { result, modelUsed: request.model };
84
+ }
85
+ }
86
+ // MARK:- Route Handler
87
+ router.post('/', authenticate, async (req, res) => {
88
+ const startTime = Date.now();
89
+ if (!req.userId) {
90
+ res.status(401).json({ error: 'unauthorized', message: 'Missing user ID' });
91
+ return;
92
+ }
93
+ const userId = req.userId;
94
+ let gateConfig = null;
95
+ let request = null;
96
+ try {
97
+ const rawRequest = req.body;
98
+ if (!rawRequest.gateId) {
99
+ res.status(400).json({ error: 'bad_request', message: 'Missing required field: gateId' });
100
+ return;
101
+ }
102
+ 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);
103
+ if (!isUUID) {
104
+ res.status(400).json({ error: 'bad_request', message: 'gateId must be a valid UUID' });
105
+ return;
106
+ }
107
+ gateConfig = await db.getGateByUserAndId(userId, rawRequest.gateId);
108
+ if (!gateConfig) {
109
+ res.status(404).json({ error: 'not_found', message: `Gate with ID "${rawRequest.gateId}" not found` });
110
+ return;
111
+ }
112
+ // Validate OCR-specific fields - must have one of documentUrl, imageUrl, or base64
113
+ const data = rawRequest.data;
114
+ if (!data || (!data.documentUrl && !data.imageUrl && !data.base64)) {
115
+ res.status(400).json({
116
+ error: 'bad_request',
117
+ message: 'Missing required field: data must contain one of documentUrl, imageUrl, or base64'
118
+ });
119
+ return;
120
+ }
121
+ // Warn if gate is configured for a different task type
122
+ if (gateConfig.taskType && gateConfig.taskType !== 'document') {
123
+ console.warn(`[Type Mismatch] Gate "${gateConfig.name}" (${gateConfig.id}) configured for taskType="${gateConfig.taskType}" ` +
124
+ `but received request to /v3/ocr endpoint. Processing as OCR request.`);
125
+ }
126
+ request = {
127
+ gateId: rawRequest.gateId,
128
+ type: 'ocr',
129
+ data: rawRequest.data,
130
+ model: rawRequest.model,
131
+ metadata: rawRequest.metadata
132
+ };
133
+ const finalRequest = resolveFinalRequest(gateConfig, request);
134
+ const { result, modelUsed } = await executeWithRouting(gateConfig, finalRequest, userId);
135
+ const latencyMs = Date.now() - startTime;
136
+ // Log request to database
137
+ db.logRequest({
138
+ userId,
139
+ gateId: gateConfig.id,
140
+ gateName: gateConfig.name,
141
+ modelRequested: request.model || gateConfig.model,
142
+ modelUsed: modelUsed,
143
+ promptTokens: result.usage?.promptTokens || 0,
144
+ completionTokens: result.usage?.completionTokens || 0,
145
+ totalTokens: result.usage?.totalTokens || 0,
146
+ costUsd: result.cost || 0,
147
+ latencyMs,
148
+ success: true,
149
+ errorMessage: null,
150
+ userAgent: req.headers['user-agent'] || null,
151
+ ipAddress: req.ip || null,
152
+ }).catch(err => console.error('Failed to log request:', err));
153
+ // Return LayerResponse with additional metadata
154
+ const response = {
155
+ ...result,
156
+ model: modelUsed,
157
+ latencyMs,
158
+ };
159
+ res.json(response);
160
+ }
161
+ catch (error) {
162
+ const latencyMs = Date.now() - startTime;
163
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
164
+ db.logRequest({
165
+ userId,
166
+ gateId: gateConfig?.id || null,
167
+ gateName: req.body?.gate || null,
168
+ modelRequested: (request?.model || gateConfig?.model) || 'unknown',
169
+ modelUsed: null,
170
+ promptTokens: 0,
171
+ completionTokens: 0,
172
+ totalTokens: 0,
173
+ costUsd: 0,
174
+ latencyMs,
175
+ success: false,
176
+ errorMessage,
177
+ userAgent: req.headers['user-agent'] || null,
178
+ ipAddress: req.ip || null,
179
+ }).catch(err => console.error('Failed to log request:', err));
180
+ console.error('OCR error:', error);
181
+ res.status(500).json({ error: 'internal_error', message: errorMessage });
182
+ }
183
+ });
184
+ export default router;
@@ -0,0 +1,4 @@
1
+ import type { Router as RouterType } from 'express';
2
+ declare const router: RouterType;
3
+ export default router;
4
+ //# sourceMappingURL=tts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tts.d.ts","sourceRoot":"","sources":["../../../src/routes/v3/tts.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,SAAS,CAAC;AAOpD,QAAA,MAAM,MAAM,EAAE,UAAqB,CAAC;AA4NpC,eAAe,MAAM,CAAC"}
@@ -0,0 +1,180 @@
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
+ // For TTS, we don't merge gate config like we do for chat
26
+ return {
27
+ ...request,
28
+ type: 'tts',
29
+ model: normalizeModelId(finalModel),
30
+ };
31
+ }
32
+ function getModelsToTry(gateConfig, primaryModel) {
33
+ const modelsToTry = [primaryModel];
34
+ if (gateConfig.routingStrategy === 'fallback' && gateConfig.fallbackModels?.length) {
35
+ modelsToTry.push(...gateConfig.fallbackModels);
36
+ }
37
+ return modelsToTry;
38
+ }
39
+ async function executeWithFallback(request, modelsToTry, userId) {
40
+ let result = null;
41
+ let lastError = null;
42
+ let modelUsed = request.model;
43
+ for (const modelToTry of modelsToTry) {
44
+ try {
45
+ const modelRequest = { ...request, model: modelToTry };
46
+ result = await callAdapter(modelRequest, userId);
47
+ modelUsed = modelToTry;
48
+ break;
49
+ }
50
+ catch (error) {
51
+ lastError = error;
52
+ console.log(`Model ${modelToTry} failed, trying next fallback...`, error instanceof Error ? error.message : error);
53
+ continue;
54
+ }
55
+ }
56
+ if (!result) {
57
+ throw lastError || new Error('All models failed');
58
+ }
59
+ return { result, modelUsed };
60
+ }
61
+ async function executeWithRoundRobin(gateConfig, request, userId) {
62
+ if (!gateConfig.fallbackModels?.length) {
63
+ const result = await callAdapter(request, userId);
64
+ return { result, modelUsed: request.model };
65
+ }
66
+ const allModels = [gateConfig.model, ...gateConfig.fallbackModels];
67
+ const modelIndex = Math.floor(Math.random() * allModels.length);
68
+ const selectedModel = allModels[modelIndex];
69
+ const modelRequest = { ...request, model: selectedModel };
70
+ const result = await callAdapter(modelRequest, userId);
71
+ return { result, modelUsed: selectedModel };
72
+ }
73
+ async function executeWithRouting(gateConfig, request, userId) {
74
+ const modelsToTry = getModelsToTry(gateConfig, request.model);
75
+ switch (gateConfig.routingStrategy) {
76
+ case 'fallback':
77
+ return await executeWithFallback(request, modelsToTry, userId);
78
+ case 'round-robin':
79
+ return await executeWithRoundRobin(gateConfig, request, userId);
80
+ case 'single':
81
+ default:
82
+ const result = await callAdapter(request, userId);
83
+ return { result, modelUsed: request.model };
84
+ }
85
+ }
86
+ // MARK:- Route Handler
87
+ router.post('/', authenticate, async (req, res) => {
88
+ const startTime = Date.now();
89
+ if (!req.userId) {
90
+ res.status(401).json({ error: 'unauthorized', message: 'Missing user ID' });
91
+ return;
92
+ }
93
+ const userId = req.userId;
94
+ let gateConfig = null;
95
+ let request = null;
96
+ try {
97
+ const rawRequest = req.body;
98
+ if (!rawRequest.gateId) {
99
+ res.status(400).json({ error: 'bad_request', message: 'Missing required field: gateId' });
100
+ return;
101
+ }
102
+ 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);
103
+ if (!isUUID) {
104
+ res.status(400).json({ error: 'bad_request', message: 'gateId must be a valid UUID' });
105
+ return;
106
+ }
107
+ gateConfig = await db.getGateByUserAndId(userId, rawRequest.gateId);
108
+ if (!gateConfig) {
109
+ res.status(404).json({ error: 'not_found', message: `Gate with ID "${rawRequest.gateId}" not found` });
110
+ return;
111
+ }
112
+ // Validate TTS-specific fields
113
+ if (!rawRequest.data?.input || typeof rawRequest.data.input !== 'string' || rawRequest.data.input.trim().length === 0) {
114
+ res.status(400).json({ error: 'bad_request', message: 'Missing required field: data.input (must be a non-empty string)' });
115
+ return;
116
+ }
117
+ // Warn if gate is configured for a different task type
118
+ if (gateConfig.taskType && gateConfig.taskType !== 'tts') {
119
+ console.warn(`[Type Mismatch] Gate "${gateConfig.name}" (${gateConfig.id}) configured for taskType="${gateConfig.taskType}" ` +
120
+ `but received request to /v3/tts endpoint. Processing as TTS request.`);
121
+ }
122
+ request = {
123
+ gateId: rawRequest.gateId,
124
+ type: 'tts',
125
+ data: rawRequest.data,
126
+ model: rawRequest.model,
127
+ metadata: rawRequest.metadata
128
+ };
129
+ const finalRequest = resolveFinalRequest(gateConfig, request);
130
+ const { result, modelUsed } = await executeWithRouting(gateConfig, finalRequest, userId);
131
+ const latencyMs = Date.now() - startTime;
132
+ // Log request to database
133
+ db.logRequest({
134
+ userId,
135
+ gateId: gateConfig.id,
136
+ gateName: gateConfig.name,
137
+ modelRequested: request.model || gateConfig.model,
138
+ modelUsed: modelUsed,
139
+ promptTokens: result.usage?.promptTokens || 0,
140
+ completionTokens: result.usage?.completionTokens || 0,
141
+ totalTokens: result.usage?.totalTokens || 0,
142
+ costUsd: result.cost || 0,
143
+ latencyMs,
144
+ success: true,
145
+ errorMessage: null,
146
+ userAgent: req.headers['user-agent'] || null,
147
+ ipAddress: req.ip || null,
148
+ }).catch(err => console.error('Failed to log request:', err));
149
+ // Return LayerResponse with additional metadata
150
+ const response = {
151
+ ...result,
152
+ model: modelUsed,
153
+ latencyMs,
154
+ };
155
+ res.json(response);
156
+ }
157
+ catch (error) {
158
+ const latencyMs = Date.now() - startTime;
159
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
160
+ db.logRequest({
161
+ userId,
162
+ gateId: gateConfig?.id || null,
163
+ gateName: req.body?.gate || null,
164
+ modelRequested: (request?.model || gateConfig?.model) || 'unknown',
165
+ modelUsed: null,
166
+ promptTokens: 0,
167
+ completionTokens: 0,
168
+ totalTokens: 0,
169
+ costUsd: 0,
170
+ latencyMs,
171
+ success: false,
172
+ errorMessage,
173
+ userAgent: req.headers['user-agent'] || null,
174
+ ipAddress: req.ip || null,
175
+ }).catch(err => console.error('Failed to log request:', err));
176
+ console.error('TTS error:', error);
177
+ res.status(500).json({ error: 'internal_error', message: errorMessage });
178
+ }
179
+ });
180
+ export default router;
@@ -0,0 +1,4 @@
1
+ import type { Router as RouterType } from 'express';
2
+ declare const router: RouterType;
3
+ export default router;
4
+ //# sourceMappingURL=video.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"video.d.ts","sourceRoot":"","sources":["../../../src/routes/v3/video.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,SAAS,CAAC;AAOpD,QAAA,MAAM,MAAM,EAAE,UAAqB,CAAC;AA6NpC,eAAe,MAAM,CAAC"}
@@ -0,0 +1,181 @@
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
+ // For image generation, we don't merge gate config like we do for chat
26
+ // The prompt and parameters are specific to the request
27
+ return {
28
+ ...request,
29
+ type: 'video',
30
+ model: normalizeModelId(finalModel),
31
+ };
32
+ }
33
+ function getModelsToTry(gateConfig, primaryModel) {
34
+ const modelsToTry = [primaryModel];
35
+ if (gateConfig.routingStrategy === 'fallback' && gateConfig.fallbackModels?.length) {
36
+ modelsToTry.push(...gateConfig.fallbackModels);
37
+ }
38
+ return modelsToTry;
39
+ }
40
+ async function executeWithFallback(request, modelsToTry, userId) {
41
+ let result = null;
42
+ let lastError = null;
43
+ let modelUsed = request.model;
44
+ for (const modelToTry of modelsToTry) {
45
+ try {
46
+ const modelRequest = { ...request, model: modelToTry };
47
+ result = await callAdapter(modelRequest, userId);
48
+ modelUsed = modelToTry;
49
+ break;
50
+ }
51
+ catch (error) {
52
+ lastError = error;
53
+ console.log(`Model ${modelToTry} failed, trying next fallback...`, error instanceof Error ? error.message : error);
54
+ continue;
55
+ }
56
+ }
57
+ if (!result) {
58
+ throw lastError || new Error('All models failed');
59
+ }
60
+ return { result, modelUsed };
61
+ }
62
+ async function executeWithRoundRobin(gateConfig, request, userId) {
63
+ if (!gateConfig.fallbackModels?.length) {
64
+ const result = await callAdapter(request, userId);
65
+ return { result, modelUsed: request.model };
66
+ }
67
+ const allModels = [gateConfig.model, ...gateConfig.fallbackModels];
68
+ const modelIndex = Math.floor(Math.random() * allModels.length);
69
+ const selectedModel = allModels[modelIndex];
70
+ const modelRequest = { ...request, model: selectedModel };
71
+ const result = await callAdapter(modelRequest, userId);
72
+ return { result, modelUsed: selectedModel };
73
+ }
74
+ async function executeWithRouting(gateConfig, request, userId) {
75
+ const modelsToTry = getModelsToTry(gateConfig, request.model);
76
+ switch (gateConfig.routingStrategy) {
77
+ case 'fallback':
78
+ return await executeWithFallback(request, modelsToTry, userId);
79
+ case 'round-robin':
80
+ return await executeWithRoundRobin(gateConfig, request, userId);
81
+ case 'single':
82
+ default:
83
+ const result = await callAdapter(request, userId);
84
+ return { result, modelUsed: request.model };
85
+ }
86
+ }
87
+ // MARK:- Route Handler
88
+ router.post('/', authenticate, async (req, res) => {
89
+ const startTime = Date.now();
90
+ if (!req.userId) {
91
+ res.status(401).json({ error: 'unauthorized', message: 'Missing user ID' });
92
+ return;
93
+ }
94
+ const userId = req.userId;
95
+ let gateConfig = null;
96
+ let request = null;
97
+ try {
98
+ const rawRequest = req.body;
99
+ if (!rawRequest.gateId) {
100
+ res.status(400).json({ error: 'bad_request', message: 'Missing required field: gateId' });
101
+ return;
102
+ }
103
+ 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);
104
+ if (!isUUID) {
105
+ res.status(400).json({ error: 'bad_request', message: 'gateId must be a valid UUID' });
106
+ return;
107
+ }
108
+ gateConfig = await db.getGateByUserAndId(userId, rawRequest.gateId);
109
+ if (!gateConfig) {
110
+ res.status(404).json({ error: 'not_found', message: `Gate with ID "${rawRequest.gateId}" not found` });
111
+ return;
112
+ }
113
+ // Validate video-specific fields
114
+ if (!rawRequest.data?.prompt || typeof rawRequest.data.prompt !== 'string' || rawRequest.data.prompt.trim().length === 0) {
115
+ res.status(400).json({ error: 'bad_request', message: 'Missing required field: data.prompt (must be a non-empty string)' });
116
+ return;
117
+ }
118
+ // Warn if gate is configured for a different task type
119
+ if (gateConfig.taskType && gateConfig.taskType !== 'video') {
120
+ console.warn(`[Type Mismatch] Gate "${gateConfig.name}" (${gateConfig.id}) configured for taskType="${gateConfig.taskType}" ` +
121
+ `but received request to /v3/video endpoint. Processing as video request.`);
122
+ }
123
+ request = {
124
+ gateId: rawRequest.gateId,
125
+ type: 'video',
126
+ data: rawRequest.data,
127
+ model: rawRequest.model,
128
+ metadata: rawRequest.metadata
129
+ };
130
+ const finalRequest = resolveFinalRequest(gateConfig, request);
131
+ const { result, modelUsed } = await executeWithRouting(gateConfig, finalRequest, userId);
132
+ const latencyMs = Date.now() - startTime;
133
+ // Log request to database
134
+ db.logRequest({
135
+ userId,
136
+ gateId: gateConfig.id,
137
+ gateName: gateConfig.name,
138
+ modelRequested: request.model || gateConfig.model,
139
+ modelUsed: modelUsed,
140
+ promptTokens: result.usage?.promptTokens || 0,
141
+ completionTokens: result.usage?.completionTokens || 0,
142
+ totalTokens: result.usage?.totalTokens || 0,
143
+ costUsd: result.cost || 0,
144
+ latencyMs,
145
+ success: true,
146
+ errorMessage: null,
147
+ userAgent: req.headers['user-agent'] || null,
148
+ ipAddress: req.ip || null,
149
+ }).catch(err => console.error('Failed to log request:', err));
150
+ // Return LayerResponse with additional metadata
151
+ const response = {
152
+ ...result,
153
+ model: modelUsed,
154
+ latencyMs,
155
+ };
156
+ res.json(response);
157
+ }
158
+ catch (error) {
159
+ const latencyMs = Date.now() - startTime;
160
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
161
+ db.logRequest({
162
+ userId,
163
+ gateId: gateConfig?.id || null,
164
+ gateName: req.body?.gate || null,
165
+ modelRequested: (request?.model || gateConfig?.model) || 'unknown',
166
+ modelUsed: null,
167
+ promptTokens: 0,
168
+ completionTokens: 0,
169
+ totalTokens: 0,
170
+ costUsd: 0,
171
+ latencyMs,
172
+ success: false,
173
+ errorMessage,
174
+ userAgent: req.headers['user-agent'] || null,
175
+ ipAddress: req.ip || null,
176
+ }).catch(err => console.error('Failed to log request:', err));
177
+ console.error('Video generation error:', error);
178
+ res.status(500).json({ error: 'internal_error', message: errorMessage });
179
+ }
180
+ });
181
+ export default router;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@layer-ai/core",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
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.3.0"
39
+ "@layer-ai/sdk": "^2.5.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/bcryptjs": "^2.4.6",