@layer-ai/core 2.0.4 → 2.0.6
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 +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/routes/v3/embeddings.d.ts +4 -0
- package/dist/routes/v3/embeddings.d.ts.map +1 -0
- package/dist/routes/v3/embeddings.js +196 -0
- package/dist/routes/v3/image.d.ts.map +1 -1
- package/dist/routes/v3/image.js +11 -2
- package/dist/routes/v3/ocr.d.ts +4 -0
- package/dist/routes/v3/ocr.d.ts.map +1 -0
- package/dist/routes/v3/ocr.js +184 -0
- package/dist/routes/v3/tts.d.ts +4 -0
- package/dist/routes/v3/tts.d.ts.map +1 -0
- package/dist/routes/v3/tts.js +180 -0
- package/dist/routes/v3/video.d.ts.map +1 -1
- package/dist/routes/v3/video.js +11 -2
- package/dist/services/providers/base-adapter.d.ts +2 -0
- package/dist/services/providers/base-adapter.d.ts.map +1 -1
- package/dist/services/providers/base-adapter.js +29 -0
- package/dist/services/providers/openai-adapter.d.ts.map +1 -1
- package/dist/services/providers/openai-adapter.js +3 -0
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,9 @@ 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
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';
|
|
9
12
|
export { authenticate } from './middleware/auth.js';
|
|
10
13
|
export type {} from './middleware/auth.js';
|
|
11
14
|
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;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;
|
|
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
|
@@ -9,6 +9,9 @@ export { default as completeRouter } from './routes/v2/complete.js';
|
|
|
9
9
|
export { default as chatRouter } from './routes/v3/chat.js';
|
|
10
10
|
export { default as imageRouter } from './routes/v3/image.js';
|
|
11
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';
|
|
12
15
|
// Middleware
|
|
13
16
|
export { authenticate } from './middleware/auth.js';
|
|
14
17
|
// Database
|
|
@@ -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;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../../../src/routes/v3/image.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,SAAS,CAAC;AAOpD,QAAA,MAAM,MAAM,EAAE,UAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../../../src/routes/v3/image.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,SAAS,CAAC;AAOpD,QAAA,MAAM,MAAM,EAAE,UAAqB,CAAC;AAuOpC,eAAe,MAAM,CAAC"}
|
package/dist/routes/v3/image.js
CHANGED
|
@@ -22,12 +22,21 @@ function resolveFinalRequest(gateConfig, request) {
|
|
|
22
22
|
finalModel = gateConfig.model;
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
-
//
|
|
26
|
-
//
|
|
25
|
+
// Apply gate config temperature if not provided in request
|
|
26
|
+
// While most image models don't support temperature, some providers/future models might
|
|
27
|
+
const imageData = { ...request.data };
|
|
28
|
+
// Apply temperature from gate config if available
|
|
29
|
+
if (imageData.temperature === undefined && gateConfig.temperature !== undefined) {
|
|
30
|
+
imageData.temperature = gateConfig.temperature;
|
|
31
|
+
}
|
|
32
|
+
else if (imageData.temperature !== undefined && !isOverrideAllowed(gateConfig.allowOverrides, OverrideField.Temperature)) {
|
|
33
|
+
imageData.temperature = gateConfig.temperature;
|
|
34
|
+
}
|
|
27
35
|
return {
|
|
28
36
|
...request,
|
|
29
37
|
type: 'image',
|
|
30
38
|
model: normalizeModelId(finalModel),
|
|
39
|
+
data: imageData,
|
|
31
40
|
};
|
|
32
41
|
}
|
|
33
42
|
function getModelsToTry(gateConfig, primaryModel) {
|
|
@@ -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 @@
|
|
|
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;
|
|
@@ -1 +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;
|
|
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;AAuOpC,eAAe,MAAM,CAAC"}
|
package/dist/routes/v3/video.js
CHANGED
|
@@ -22,12 +22,21 @@ function resolveFinalRequest(gateConfig, request) {
|
|
|
22
22
|
finalModel = gateConfig.model;
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
-
//
|
|
26
|
-
//
|
|
25
|
+
// Apply gate config temperature if not provided in request
|
|
26
|
+
// While most video models don't support temperature, some providers/future models might
|
|
27
|
+
const videoData = { ...request.data };
|
|
28
|
+
// Apply temperature from gate config if available
|
|
29
|
+
if (videoData.temperature === undefined && gateConfig.temperature !== undefined) {
|
|
30
|
+
videoData.temperature = gateConfig.temperature;
|
|
31
|
+
}
|
|
32
|
+
else if (videoData.temperature !== undefined && !isOverrideAllowed(gateConfig.allowOverrides, OverrideField.Temperature)) {
|
|
33
|
+
videoData.temperature = gateConfig.temperature;
|
|
34
|
+
}
|
|
27
35
|
return {
|
|
28
36
|
...request,
|
|
29
37
|
type: 'video',
|
|
30
38
|
model: normalizeModelId(finalModel),
|
|
39
|
+
data: videoData,
|
|
31
40
|
};
|
|
32
41
|
}
|
|
33
42
|
function getModelsToTry(gateConfig, primaryModel) {
|
|
@@ -30,5 +30,7 @@ export declare abstract class BaseProviderAdapter {
|
|
|
30
30
|
protected mapFinishReason(providerFinishReason: string): FinishReason;
|
|
31
31
|
protected mapToolChoice(choice: ToolChoice): string | object | undefined;
|
|
32
32
|
protected calculateCost(model: string, promptTokens: number, completionTokens: number): number;
|
|
33
|
+
protected calculateImageCost(model: string, quality?: string, size?: string, count?: number): number;
|
|
34
|
+
protected calculateVideoCost(model: string, duration?: number, count?: number): number;
|
|
33
35
|
}
|
|
34
36
|
//# sourceMappingURL=base-adapter.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"base-adapter.d.ts","sourceRoot":"","sources":["../../../src/services/providers/base-adapter.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"base-adapter.d.ts","sourceRoot":"","sources":["../../../src/services/providers/base-adapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -90,4 +90,33 @@ export class BaseProviderAdapter {
|
|
|
90
90
|
const outputCost = ('output' in pricing && pricing.output) ? (completionTokens / 1000000 * pricing.output) : 0;
|
|
91
91
|
return inputCost + outputCost;
|
|
92
92
|
}
|
|
93
|
+
calculateImageCost(model, quality, size, count = 1) {
|
|
94
|
+
const modelInfo = MODEL_REGISTRY[model];
|
|
95
|
+
if (!modelInfo || !('imagePricing' in modelInfo) || !modelInfo.imagePricing) {
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
const imagePricing = modelInfo.imagePricing;
|
|
99
|
+
// Build pricing key from quality and size (e.g., 'hd-1024x1024' or 'standard-1024x1024')
|
|
100
|
+
const pricingKey = quality && size ? `${quality}-${size}` : size || 'standard-1024x1024';
|
|
101
|
+
const pricePerImage = imagePricing[pricingKey];
|
|
102
|
+
if (!pricePerImage) {
|
|
103
|
+
// If exact match not found, try without quality prefix
|
|
104
|
+
const fallbackPrice = imagePricing[size || '1024x1024'];
|
|
105
|
+
return (fallbackPrice || 0) * count;
|
|
106
|
+
}
|
|
107
|
+
return pricePerImage * count;
|
|
108
|
+
}
|
|
109
|
+
calculateVideoCost(model, duration, count = 1) {
|
|
110
|
+
const modelInfo = MODEL_REGISTRY[model];
|
|
111
|
+
if (!modelInfo || !('videoPricing' in modelInfo) || !modelInfo.videoPricing) {
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
const videoPricing = modelInfo.videoPricing;
|
|
115
|
+
// Video pricing might be per-second or per-video
|
|
116
|
+
const pricePerUnit = videoPricing.perVideo || videoPricing.perSecond || 0;
|
|
117
|
+
if (videoPricing.perSecond && duration) {
|
|
118
|
+
return pricePerUnit * duration * count;
|
|
119
|
+
}
|
|
120
|
+
return pricePerUnit * count;
|
|
121
|
+
}
|
|
93
122
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openai-adapter.d.ts","sourceRoot":"","sources":["../../../src/services/providers/openai-adapter.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EACL,YAAY,EACZ,aAAa,EACb,IAAI,EACJ,WAAW,EACX,SAAS,EACT,YAAY,EACZ,UAAU,EACV,SAAS,EACT,WAAW,EACX,YAAY,EACb,MAAM,eAAe,CAAC;AACvB,OAAO,EAAY,KAAK,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAoB1E,qBAAa,aAAc,SAAQ,mBAAmB;IACpD,SAAS,CAAC,QAAQ,EAAE,QAAQ,CAAmB;IAE/C,SAAS,CAAC,YAAY,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAQ1C;IAEF,SAAS,CAAC,mBAAmB,EAAE,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAIxD;IAEF,SAAS,CAAC,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAK1D;IAEF,SAAS,CAAC,iBAAiB,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAQpD;IAEF,SAAS,CAAC,oBAAoB,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAG1D;IAEF,SAAS,CAAC,kBAAkB,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAGtD;IAEF,SAAS,CAAC,iBAAiB,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAKpD;IAEF,SAAS,CAAC,mBAAmB,EAAE,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAOxD;IAEI,IAAI,CAAC,OAAO,EAAE,YAAY,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;YAoB5D,UAAU;YA4GV,qBAAqB;
|
|
1
|
+
{"version":3,"file":"openai-adapter.d.ts","sourceRoot":"","sources":["../../../src/services/providers/openai-adapter.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EACL,YAAY,EACZ,aAAa,EACb,IAAI,EACJ,WAAW,EACX,SAAS,EACT,YAAY,EACZ,UAAU,EACV,SAAS,EACT,WAAW,EACX,YAAY,EACb,MAAM,eAAe,CAAC;AACvB,OAAO,EAAY,KAAK,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAoB1E,qBAAa,aAAc,SAAQ,mBAAmB;IACpD,SAAS,CAAC,QAAQ,EAAE,QAAQ,CAAmB;IAE/C,SAAS,CAAC,YAAY,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAQ1C;IAEF,SAAS,CAAC,mBAAmB,EAAE,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAIxD;IAEF,SAAS,CAAC,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAK1D;IAEF,SAAS,CAAC,iBAAiB,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAQpD;IAEF,SAAS,CAAC,oBAAoB,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAG1D;IAEF,SAAS,CAAC,kBAAkB,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAGtD;IAEF,SAAS,CAAC,iBAAiB,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAKpD;IAEF,SAAS,CAAC,mBAAmB,EAAE,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAOxD;IAEI,IAAI,CAAC,OAAO,EAAE,YAAY,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;YAoB5D,UAAU;YA4GV,qBAAqB;YAuCrB,gBAAgB;YAkChB,kBAAkB;CA+BjC"}
|
|
@@ -200,12 +200,15 @@ export class OpenAIAdapter extends BaseProviderAdapter {
|
|
|
200
200
|
...(image.count && { n: image.count }),
|
|
201
201
|
...(image.style && { style: this.mapImageStyle(image.style) }),
|
|
202
202
|
});
|
|
203
|
+
// Calculate cost based on quality, size, and count
|
|
204
|
+
const cost = this.calculateImageCost(model, image.quality || 'standard', image.size || '1024x1024', image.count || 1);
|
|
203
205
|
return {
|
|
204
206
|
images: (response.data || []).map(img => ({
|
|
205
207
|
url: img.url,
|
|
206
208
|
revisedPrompt: img.revised_prompt,
|
|
207
209
|
})),
|
|
208
210
|
model: model,
|
|
211
|
+
cost,
|
|
209
212
|
latencyMs: Date.now() - startTime,
|
|
210
213
|
usedPlatformKey,
|
|
211
214
|
raw: response,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@layer-ai/core",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.6",
|
|
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.
|
|
39
|
+
"@layer-ai/sdk": "^2.5.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/bcryptjs": "^2.4.6",
|