@layer-ai/core 2.0.19 → 2.0.21

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.
Files changed (35) hide show
  1. package/dist/index.d.ts +4 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +5 -0
  4. package/dist/lib/db/migrations/007_add_spending_controls.sql +41 -0
  5. package/dist/lib/db/postgres.d.ts +19 -0
  6. package/dist/lib/db/postgres.d.ts.map +1 -1
  7. package/dist/lib/db/postgres.js +53 -0
  8. package/dist/lib/db/redis.d.ts +5 -0
  9. package/dist/lib/db/redis.d.ts.map +1 -1
  10. package/dist/lib/db/redis.js +54 -1
  11. package/dist/lib/openai-conversion.d.ts +6 -0
  12. package/dist/lib/openai-conversion.d.ts.map +1 -0
  13. package/dist/lib/openai-conversion.js +215 -0
  14. package/dist/lib/spending-jobs.d.ts +6 -0
  15. package/dist/lib/spending-jobs.d.ts.map +1 -0
  16. package/dist/lib/spending-jobs.js +56 -0
  17. package/dist/lib/spending-tracker.d.ts +17 -0
  18. package/dist/lib/spending-tracker.d.ts.map +1 -0
  19. package/dist/lib/spending-tracker.js +89 -0
  20. package/dist/middleware/auth.d.ts.map +1 -1
  21. package/dist/middleware/auth.js +22 -0
  22. package/dist/routes/tests/test-openai-endpoint.d.ts +3 -0
  23. package/dist/routes/tests/test-openai-endpoint.d.ts.map +1 -0
  24. package/dist/routes/tests/test-openai-endpoint.js +292 -0
  25. package/dist/routes/v1/chat-completions.d.ts +4 -0
  26. package/dist/routes/v1/chat-completions.d.ts.map +1 -0
  27. package/dist/routes/v1/chat-completions.js +269 -0
  28. package/dist/routes/v1/spending.d.ts +4 -0
  29. package/dist/routes/v1/spending.d.ts.map +1 -0
  30. package/dist/routes/v1/spending.js +94 -0
  31. package/dist/routes/v2/complete.d.ts.map +1 -1
  32. package/dist/routes/v2/complete.js +4 -0
  33. package/dist/routes/v3/chat.d.ts.map +1 -1
  34. package/dist/routes/v3/chat.js +7 -0
  35. package/package.json +2 -2
@@ -0,0 +1,269 @@
1
+ import { Router } from 'express';
2
+ import { nanoid } from 'nanoid';
3
+ import { db } from '../../lib/db/postgres.js';
4
+ import { authenticate } from '../../middleware/auth.js';
5
+ import { spendingTracker } from '../../lib/spending-tracker.js';
6
+ import { convertOpenAIRequestToLayer, convertLayerResponseToOpenAI, convertLayerChunkToOpenAI, } from '../../lib/openai-conversion.js';
7
+ import { resolveFinalRequest } from '../v3/chat.js';
8
+ import { callAdapter, callAdapterStream } from '../../lib/provider-factory.js';
9
+ const router = Router();
10
+ async function* executeWithRoutingStream(gateConfig, request, userId) {
11
+ yield* callAdapterStream(request, userId);
12
+ }
13
+ async function executeWithRouting(gateConfig, request, userId) {
14
+ const result = await callAdapter(request, userId);
15
+ return { result, modelUsed: request.model };
16
+ }
17
+ router.post('/', authenticate, async (req, res) => {
18
+ const startTime = Date.now();
19
+ if (!req.userId) {
20
+ const error = {
21
+ error: {
22
+ message: 'Missing user ID',
23
+ type: 'authentication_error',
24
+ code: 'unauthorized',
25
+ },
26
+ };
27
+ res.status(401).json(error);
28
+ return;
29
+ }
30
+ const userId = req.userId;
31
+ let gateConfig = null;
32
+ let layerRequest = null;
33
+ try {
34
+ const openaiReq = req.body;
35
+ const gateId = openaiReq.gateId || req.headers['x-layer-gate-id'];
36
+ if (!gateId) {
37
+ const error = {
38
+ error: {
39
+ message: 'Missing required field: gateId (provide in request body or X-Layer-Gate-Id header)',
40
+ type: 'invalid_request_error',
41
+ param: 'gateId',
42
+ code: 'missing_field',
43
+ },
44
+ };
45
+ res.status(400).json(error);
46
+ return;
47
+ }
48
+ const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(gateId);
49
+ if (!isUUID) {
50
+ const error = {
51
+ error: {
52
+ message: 'gateId must be a valid UUID',
53
+ type: 'invalid_request_error',
54
+ param: 'gateId',
55
+ code: 'invalid_format',
56
+ },
57
+ };
58
+ res.status(400).json(error);
59
+ return;
60
+ }
61
+ gateConfig = await db.getGateByUserAndId(userId, gateId);
62
+ if (!gateConfig) {
63
+ const error = {
64
+ error: {
65
+ message: `Gate with ID "${gateId}" not found`,
66
+ type: 'invalid_request_error',
67
+ param: 'gateId',
68
+ code: 'not_found',
69
+ },
70
+ };
71
+ res.status(404).json(error);
72
+ return;
73
+ }
74
+ if (!openaiReq.messages || !Array.isArray(openaiReq.messages) || openaiReq.messages.length === 0) {
75
+ const error = {
76
+ error: {
77
+ message: 'Missing required field: messages (must be a non-empty array)',
78
+ type: 'invalid_request_error',
79
+ param: 'messages',
80
+ code: 'missing_field',
81
+ },
82
+ };
83
+ res.status(400).json(error);
84
+ return;
85
+ }
86
+ if (gateConfig.taskType && gateConfig.taskType !== 'chat') {
87
+ console.warn(`[Type Mismatch] Gate "${gateConfig.name}" (${gateConfig.id}) configured for taskType="${gateConfig.taskType}" ` +
88
+ `but received request to /v1/chat/completions endpoint. Processing as chat request.`);
89
+ }
90
+ layerRequest = convertOpenAIRequestToLayer(openaiReq, gateId);
91
+ const finalRequest = resolveFinalRequest(gateConfig, layerRequest);
92
+ const isStreaming = finalRequest.data && 'stream' in finalRequest.data && finalRequest.data.stream === true;
93
+ if (isStreaming) {
94
+ res.setHeader('Content-Type', 'text/event-stream');
95
+ res.setHeader('Cache-Control', 'no-cache');
96
+ res.setHeader('Connection', 'keep-alive');
97
+ res.setHeader('X-Accel-Buffering', 'no');
98
+ const requestId = `chatcmpl-${nanoid()}`;
99
+ const created = Math.floor(Date.now() / 1000);
100
+ let promptTokens = 0;
101
+ let completionTokens = 0;
102
+ let totalCost = 0;
103
+ let modelUsed = finalRequest.model;
104
+ try {
105
+ for await (const layerChunk of executeWithRoutingStream(gateConfig, finalRequest, userId)) {
106
+ if (layerChunk.usage) {
107
+ promptTokens = layerChunk.usage.promptTokens || 0;
108
+ completionTokens = layerChunk.usage.completionTokens || 0;
109
+ }
110
+ if (layerChunk.cost) {
111
+ totalCost = layerChunk.cost;
112
+ }
113
+ if (layerChunk.model) {
114
+ modelUsed = layerChunk.model;
115
+ }
116
+ const openaiChunk = convertLayerChunkToOpenAI(layerChunk, requestId, created);
117
+ res.write(`data: ${JSON.stringify(openaiChunk)}\n\n`);
118
+ }
119
+ res.write(`data: [DONE]\n\n`);
120
+ res.end();
121
+ const latencyMs = Date.now() - startTime;
122
+ db.logRequest({
123
+ userId,
124
+ gateId: gateConfig.id,
125
+ gateName: gateConfig.name,
126
+ modelRequested: layerRequest.model || gateConfig.model,
127
+ modelUsed: modelUsed,
128
+ promptTokens,
129
+ completionTokens,
130
+ totalTokens: promptTokens + completionTokens,
131
+ costUsd: totalCost,
132
+ latencyMs,
133
+ success: true,
134
+ errorMessage: null,
135
+ userAgent: req.headers['user-agent'] || null,
136
+ ipAddress: req.ip || null,
137
+ requestPayload: {
138
+ gateId: layerRequest.gateId,
139
+ type: layerRequest.type,
140
+ model: layerRequest.model,
141
+ data: layerRequest.data,
142
+ metadata: layerRequest.metadata,
143
+ },
144
+ responsePayload: {
145
+ streamed: true,
146
+ model: modelUsed,
147
+ usage: { promptTokens, completionTokens, totalTokens: promptTokens + completionTokens },
148
+ cost: totalCost,
149
+ },
150
+ }).catch(err => console.error('Failed to log request:', err));
151
+ spendingTracker.trackSpending(userId, totalCost).catch(err => {
152
+ console.error('Failed to track spending:', err);
153
+ });
154
+ }
155
+ catch (streamError) {
156
+ const errorMessage = streamError instanceof Error ? streamError.message : 'Unknown streaming error';
157
+ const openaiError = {
158
+ error: {
159
+ message: errorMessage,
160
+ type: 'server_error',
161
+ code: 'stream_error',
162
+ },
163
+ };
164
+ res.write(`data: ${JSON.stringify(openaiError)}\n\n`);
165
+ res.end();
166
+ db.logRequest({
167
+ userId,
168
+ gateId: gateConfig.id,
169
+ gateName: gateConfig.name,
170
+ modelRequested: layerRequest.model || gateConfig.model,
171
+ modelUsed: null,
172
+ promptTokens: 0,
173
+ completionTokens: 0,
174
+ totalTokens: 0,
175
+ costUsd: 0,
176
+ latencyMs: Date.now() - startTime,
177
+ success: false,
178
+ errorMessage,
179
+ userAgent: req.headers['user-agent'] || null,
180
+ ipAddress: req.ip || null,
181
+ requestPayload: {
182
+ gateId: layerRequest.gateId,
183
+ type: layerRequest.type,
184
+ model: layerRequest.model,
185
+ data: layerRequest.data,
186
+ metadata: layerRequest.metadata,
187
+ },
188
+ responsePayload: null,
189
+ }).catch(err => console.error('Failed to log request:', err));
190
+ }
191
+ return;
192
+ }
193
+ const { result, modelUsed } = await executeWithRouting(gateConfig, finalRequest, userId);
194
+ const latencyMs = Date.now() - startTime;
195
+ db.logRequest({
196
+ userId,
197
+ gateId: gateConfig.id,
198
+ gateName: gateConfig.name,
199
+ modelRequested: layerRequest.model || gateConfig.model,
200
+ modelUsed: modelUsed,
201
+ promptTokens: result.usage?.promptTokens || 0,
202
+ completionTokens: result.usage?.completionTokens || 0,
203
+ totalTokens: result.usage?.totalTokens || 0,
204
+ costUsd: result.cost || 0,
205
+ latencyMs,
206
+ success: true,
207
+ errorMessage: null,
208
+ userAgent: req.headers['user-agent'] || null,
209
+ ipAddress: req.ip || null,
210
+ requestPayload: {
211
+ gateId: layerRequest.gateId,
212
+ type: layerRequest.type,
213
+ model: layerRequest.model,
214
+ data: layerRequest.data,
215
+ metadata: layerRequest.metadata,
216
+ },
217
+ responsePayload: {
218
+ content: result.content,
219
+ model: result.model,
220
+ usage: result.usage,
221
+ cost: result.cost,
222
+ finishReason: result.finishReason,
223
+ },
224
+ }).catch(err => console.error('Failed to log request:', err));
225
+ spendingTracker.trackSpending(userId, result.cost || 0).catch(err => {
226
+ console.error('Failed to track spending:', err);
227
+ });
228
+ const openaiResponse = convertLayerResponseToOpenAI(result);
229
+ res.json(openaiResponse);
230
+ }
231
+ catch (error) {
232
+ const latencyMs = Date.now() - startTime;
233
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
234
+ db.logRequest({
235
+ userId,
236
+ gateId: gateConfig?.id || null,
237
+ gateName: gateConfig?.name || null,
238
+ modelRequested: (layerRequest?.model || gateConfig?.model) || 'unknown',
239
+ modelUsed: null,
240
+ promptTokens: 0,
241
+ completionTokens: 0,
242
+ totalTokens: 0,
243
+ costUsd: 0,
244
+ latencyMs,
245
+ success: false,
246
+ errorMessage,
247
+ userAgent: req.headers['user-agent'] || null,
248
+ ipAddress: req.ip || null,
249
+ requestPayload: layerRequest ? {
250
+ gateId: layerRequest.gateId,
251
+ type: layerRequest.type,
252
+ model: layerRequest.model,
253
+ data: layerRequest.data,
254
+ metadata: layerRequest.metadata,
255
+ } : null,
256
+ responsePayload: null,
257
+ }).catch(err => console.error('Failed to log request:', err));
258
+ console.error('OpenAI chat completion error:', error);
259
+ const openaiError = {
260
+ error: {
261
+ message: errorMessage,
262
+ type: 'server_error',
263
+ code: 'internal_error',
264
+ },
265
+ };
266
+ res.status(500).json(openaiError);
267
+ }
268
+ });
269
+ 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=spending.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spending.d.ts","sourceRoot":"","sources":["../../../src/routes/v1/spending.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,SAAS,CAAC;AAKpD,QAAA,MAAM,MAAM,EAAE,UAAqB,CAAC;AAyGpC,eAAe,MAAM,CAAC"}
@@ -0,0 +1,94 @@
1
+ import { Router } from 'express';
2
+ import { db } from '../../lib/db/postgres.js';
3
+ import { cache } from '../../lib/db/redis.js';
4
+ import { authenticate } from '../../middleware/auth.js';
5
+ const router = Router();
6
+ router.use(authenticate);
7
+ // GET /spending - Get current spending information
8
+ router.get('/', async (req, res) => {
9
+ if (!req.userId) {
10
+ res.status(401).json({ error: 'unauthorized', message: 'Missing user ID' });
11
+ return;
12
+ }
13
+ try {
14
+ const spendingInfo = await db.getUserSpending(req.userId);
15
+ if (!spendingInfo) {
16
+ res.status(404).json({ error: 'not_found', message: 'User spending data not found' });
17
+ return;
18
+ }
19
+ const { currentSpending, limit, periodStart, status, limitEnforcementType } = spendingInfo;
20
+ res.json({
21
+ currentSpending,
22
+ limit,
23
+ periodStart,
24
+ status,
25
+ limitEnforcementType,
26
+ percentUsed: limit ? Math.round((currentSpending / limit) * 100) : null,
27
+ });
28
+ }
29
+ catch (error) {
30
+ console.error('Get spending error:', error);
31
+ res.status(500).json({ error: 'internal_error', message: 'Failed to get spending data' });
32
+ }
33
+ });
34
+ // PUT /spending/limit - Update spending limit
35
+ router.put('/limit', async (req, res) => {
36
+ if (!req.userId) {
37
+ res.status(401).json({ error: 'unauthorized', message: 'Missing user ID' });
38
+ return;
39
+ }
40
+ try {
41
+ const { limit } = req.body;
42
+ if (limit !== null && (typeof limit !== 'number' || limit < 0)) {
43
+ res.status(400).json({ error: 'bad_request', message: 'Limit must be a positive number or null' });
44
+ return;
45
+ }
46
+ await db.setUserSpendingLimit(req.userId, limit);
47
+ await cache.invalidateUserSpending(req.userId);
48
+ res.json({ success: true, limit });
49
+ }
50
+ catch (error) {
51
+ console.error('Update spending limit error:', error);
52
+ res.status(500).json({ error: 'internal_error', message: 'Failed to update spending limit' });
53
+ }
54
+ });
55
+ // PUT /spending/enforcement - Update limit enforcement type
56
+ router.put('/enforcement', async (req, res) => {
57
+ if (!req.userId) {
58
+ res.status(401).json({ error: 'unauthorized', message: 'Missing user ID' });
59
+ return;
60
+ }
61
+ try {
62
+ const { enforcementType } = req.body;
63
+ if (!['alert_only', 'block'].includes(enforcementType)) {
64
+ res.status(400).json({
65
+ error: 'bad_request',
66
+ message: 'Enforcement type must be "alert_only" or "block"'
67
+ });
68
+ return;
69
+ }
70
+ await db.setUserEnforcementType(req.userId, enforcementType);
71
+ res.json({ success: true, enforcementType });
72
+ }
73
+ catch (error) {
74
+ console.error('Update enforcement type error:', error);
75
+ res.status(500).json({ error: 'internal_error', message: 'Failed to update enforcement type' });
76
+ }
77
+ });
78
+ // POST /spending/reset - Manually reset spending period
79
+ router.post('/reset', async (req, res) => {
80
+ if (!req.userId) {
81
+ res.status(401).json({ error: 'unauthorized', message: 'Missing user ID' });
82
+ return;
83
+ }
84
+ try {
85
+ await db.resetUserSpending(req.userId);
86
+ await cache.invalidateUserSpending(req.userId);
87
+ res.json({ success: true, message: 'Spending period reset successfully' });
88
+ }
89
+ catch (error) {
90
+ console.error('Reset spending error:', error);
91
+ res.status(500).json({ error: 'internal_error', message: 'Failed to reset spending' });
92
+ }
93
+ });
94
+ export default router;
@@ -1 +1 @@
1
- {"version":3,"file":"complete.d.ts","sourceRoot":"","sources":["../../../src/routes/v2/complete.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,SAAS,CAAC;AASpD,QAAA,MAAM,MAAM,EAAE,UAAqB,CAAC;AAkVpC,eAAe,MAAM,CAAC"}
1
+ {"version":3,"file":"complete.d.ts","sourceRoot":"","sources":["../../../src/routes/v2/complete.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,SAAS,CAAC;AAUpD,QAAA,MAAM,MAAM,EAAE,UAAqB,CAAC;AAsVpC,eAAe,MAAM,CAAC"}
@@ -3,6 +3,7 @@ import { db } from '../../lib/db/postgres.js';
3
3
  import { authenticate } from '../../middleware/auth.js';
4
4
  import { callAdapter, normalizeModelId } from '../../lib/provider-factory.js';
5
5
  import { OverrideField } from '@layer-ai/sdk';
6
+ import { spendingTracker } from '../../lib/spending-tracker.js';
6
7
  const router = Router();
7
8
  // MARK:- Helper Functions
8
9
  function isOverrideAllowed(allowOverrides, field) {
@@ -255,6 +256,9 @@ router.post('/', authenticate, async (req, res) => {
255
256
  userAgent: req.headers['user-agent'] || null,
256
257
  ipAddress: req.ip || null,
257
258
  }).catch(err => console.error('Failed to log request:', err));
259
+ spendingTracker.trackSpending(userId, result.cost || 0).catch(err => {
260
+ console.error('Failed to track spending:', err);
261
+ });
258
262
  // Return LayerResponse with additional metadata
259
263
  const response = {
260
264
  ...result,
@@ -1 +1 @@
1
- {"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../../../src/routes/v3/chat.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,SAAS,CAAC;AAIpD,OAAO,KAAK,EAAE,YAAY,EAAiB,IAAI,EAA+C,MAAM,eAAe,CAAC;AAGpH,QAAA,MAAM,MAAM,EAAE,UAAqB,CAAC;AAiBpC,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,IAAI,EAChB,OAAO,EAAE,YAAY,GACpB,YAAY,CAiFd;AAgWD,eAAe,MAAM,CAAC"}
1
+ {"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../../../src/routes/v3/chat.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,SAAS,CAAC;AAIpD,OAAO,KAAK,EAAE,YAAY,EAAiB,IAAI,EAA+C,MAAM,eAAe,CAAC;AAIpH,QAAA,MAAM,MAAM,EAAE,UAAqB,CAAC;AAiBpC,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,IAAI,EAChB,OAAO,EAAE,YAAY,GACpB,YAAY,CAiFd;AAwWD,eAAe,MAAM,CAAC"}
@@ -3,6 +3,7 @@ import { db } from '../../lib/db/postgres.js';
3
3
  import { authenticate } from '../../middleware/auth.js';
4
4
  import { callAdapter, callAdapterStream, normalizeModelId, getProviderForModel, PROVIDER } from '../../lib/provider-factory.js';
5
5
  import { OverrideField } from '@layer-ai/sdk';
6
+ import { spendingTracker } from '../../lib/spending-tracker.js';
6
7
  const router = Router();
7
8
  // MARK:- Helper Functions
8
9
  function isOverrideAllowed(allowOverrides, field) {
@@ -282,6 +283,9 @@ router.post('/', authenticate, async (req, res) => {
282
283
  cost: totalCost,
283
284
  },
284
285
  }).catch(err => console.error('Failed to log request:', err));
286
+ spendingTracker.trackSpending(userId, totalCost).catch(err => {
287
+ console.error('Failed to track spending:', err);
288
+ });
285
289
  }
286
290
  catch (streamError) {
287
291
  const errorMessage = streamError instanceof Error ? streamError.message : 'Unknown streaming error';
@@ -346,6 +350,9 @@ router.post('/', authenticate, async (req, res) => {
346
350
  finishReason: result.finishReason,
347
351
  },
348
352
  }).catch(err => console.error('Failed to log request:', err));
353
+ spendingTracker.trackSpending(userId, result.cost || 0).catch(err => {
354
+ console.error('Failed to track spending:', err);
355
+ });
349
356
  const response = {
350
357
  ...result,
351
358
  model: modelUsed,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@layer-ai/core",
3
- "version": "2.0.19",
3
+ "version": "2.0.21",
4
4
  "description": "Core API routes and services for Layer AI",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -36,7 +36,7 @@
36
36
  "nanoid": "^5.0.4",
37
37
  "openai": "^4.24.0",
38
38
  "pg": "^8.11.3",
39
- "@layer-ai/sdk": "^2.5.6"
39
+ "@layer-ai/sdk": "^2.5.8"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/bcryptjs": "^2.4.6",