@layer-ai/core 2.0.20 → 2.0.22

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
@@ -3,6 +3,7 @@ export { default as gatesRouter } from './routes/v1/gates.js';
3
3
  export { default as keysRouter } from './routes/v1/keys.js';
4
4
  export { default as logsRouter } from './routes/v1/logs.js';
5
5
  export { default as chatCompletionsRouter } from './routes/v1/chat-completions.js';
6
+ export { default as spendingRouter } from './routes/v1/spending.js';
6
7
  export { default as completeRouter } from './routes/v2/complete.js';
7
8
  export { default as chatRouter } from './routes/v3/chat.js';
8
9
  export { default as imageRouter } from './routes/v3/image.js';
@@ -20,4 +21,6 @@ export declare const createSessionKey: (userId: string) => Promise<string>;
20
21
  export declare const deleteSessionKeysForUser: (userId: string) => Promise<void>;
21
22
  export * from './services/task-analysis.js';
22
23
  export { PROVIDER, PROVIDERS, type Provider, callAdapter, normalizeModelId, getProviderForModel } from './lib/provider-factory.js';
24
+ export { spendingTracker } from './lib/spending-tracker.js';
25
+ export { spendingJobs } from './lib/spending-jobs.js';
23
26
  //# sourceMappingURL=index.d.ts.map
@@ -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;AAC5D,OAAO,EAAE,OAAO,IAAI,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AAGnF,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"}
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;AAC5D,OAAO,EAAE,OAAO,IAAI,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AACnF,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAGpE,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;AAGnI,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC"}
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ export { default as gatesRouter } from './routes/v1/gates.js';
4
4
  export { default as keysRouter } from './routes/v1/keys.js';
5
5
  export { default as logsRouter } from './routes/v1/logs.js';
6
6
  export { default as chatCompletionsRouter } from './routes/v1/chat-completions.js';
7
+ export { default as spendingRouter } from './routes/v1/spending.js';
7
8
  // v2 routes
8
9
  export { default as completeRouter } from './routes/v2/complete.js';
9
10
  // v3 routes
@@ -33,3 +34,6 @@ export const deleteSessionKeysForUser = async (userId) => {
33
34
  export * from './services/task-analysis.js';
34
35
  // Provider Factory
35
36
  export { PROVIDER, PROVIDERS, callAdapter, normalizeModelId, getProviderForModel } from './lib/provider-factory.js';
37
+ // Spending Management
38
+ export { spendingTracker } from './lib/spending-tracker.js';
39
+ export { spendingJobs } from './lib/spending-jobs.js';
@@ -0,0 +1,41 @@
1
+ -- Migration: Add spending controls to users table
2
+ -- This enables monthly spending limits and budget tracking
3
+
4
+ -- Add spending control fields to users table
5
+ ALTER TABLE users ADD COLUMN IF NOT EXISTS status VARCHAR(20) DEFAULT 'active' NOT NULL;
6
+ ALTER TABLE users ADD COLUMN IF NOT EXISTS monthly_spending_limit DECIMAL(10,2); -- NULL means unlimited
7
+ ALTER TABLE users ADD COLUMN IF NOT EXISTS current_month_spending DECIMAL(10,2) DEFAULT 0 NOT NULL;
8
+ ALTER TABLE users ADD COLUMN IF NOT EXISTS spending_period_start TIMESTAMP DEFAULT NOW() NOT NULL;
9
+ ALTER TABLE users ADD COLUMN IF NOT EXISTS alert_threshold_percentage INTEGER DEFAULT 80 NOT NULL;
10
+ ALTER TABLE users ADD COLUMN IF NOT EXISTS limit_enforcement_type VARCHAR(20) DEFAULT 'alert_only' NOT NULL;
11
+ ALTER TABLE users ADD COLUMN IF NOT EXISTS last_spending_alert_sent_at TIMESTAMP;
12
+
13
+ -- Add check constraint for valid status values
14
+ ALTER TABLE users ADD CONSTRAINT users_status_check
15
+ CHECK (status IN ('active', 'over_limit', 'suspended', 'banned'));
16
+
17
+ -- Add check constraint for valid enforcement types
18
+ ALTER TABLE users ADD CONSTRAINT users_enforcement_type_check
19
+ CHECK (limit_enforcement_type IN ('alert_only', 'block'));
20
+
21
+ -- Add check constraint for valid alert threshold
22
+ ALTER TABLE users ADD CONSTRAINT users_alert_threshold_check
23
+ CHECK (alert_threshold_percentage >= 0 AND alert_threshold_percentage <= 100);
24
+
25
+ -- Add check constraint for non-negative spending
26
+ ALTER TABLE users ADD CONSTRAINT users_spending_check
27
+ CHECK (current_month_spending >= 0);
28
+
29
+ -- Create indexes for performance
30
+ CREATE INDEX IF NOT EXISTS idx_users_status ON users(status);
31
+ CREATE INDEX IF NOT EXISTS idx_users_spending_period ON users(spending_period_start);
32
+ CREATE INDEX IF NOT EXISTS idx_users_over_limit ON users(status) WHERE status = 'over_limit';
33
+
34
+ -- Add comment for documentation
35
+ COMMENT ON COLUMN users.status IS 'User account status: active, over_limit, suspended, banned';
36
+ COMMENT ON COLUMN users.monthly_spending_limit IS 'Maximum spending per 30-day period in USD. NULL = unlimited';
37
+ COMMENT ON COLUMN users.current_month_spending IS 'Accumulated spending for current 30-day period in USD';
38
+ COMMENT ON COLUMN users.spending_period_start IS 'Start of current 30-day billing period';
39
+ COMMENT ON COLUMN users.alert_threshold_percentage IS 'Percentage of limit to trigger spending alerts (default 80%)';
40
+ COMMENT ON COLUMN users.last_spending_alert_sent_at IS 'Timestamp of last spending alert sent to prevent spam';
41
+ COMMENT ON COLUMN users.limit_enforcement_type IS 'How to enforce spending limits: alert_only (warn but allow) or block (prevent requests)';
@@ -7,6 +7,25 @@ export declare const db: {
7
7
  getUserById(id: string): Promise<User | null>;
8
8
  createUser(email: string, passwordHash: string): Promise<User>;
9
9
  getUserStatus(userId: string): Promise<string | null>;
10
+ getUserSpending(userId: string): Promise<{
11
+ currentSpending: number;
12
+ limit: number | null;
13
+ periodStart: Date;
14
+ status: string;
15
+ limitEnforcementType: string;
16
+ } | null>;
17
+ updateUserSpending(userId: string, newSpending: number): Promise<void>;
18
+ incrementUserSpending(userId: string, cost: number): Promise<{
19
+ newSpending: number;
20
+ limit: number | null;
21
+ exceeded: boolean;
22
+ }>;
23
+ setUserStatus(userId: string, status: string): Promise<void>;
24
+ setUserSpendingLimit(userId: string, limit: number | null): Promise<void>;
25
+ setUserEnforcementType(userId: string, enforcementType: string): Promise<void>;
26
+ resetUserSpending(userId: string): Promise<void>;
27
+ getUsersToResetSpending(): Promise<string[]>;
28
+ recordSpendingAlert(userId: string): Promise<void>;
10
29
  getApiKeyByHash(keyHash: string): Promise<ApiKey | null>;
11
30
  createApiKey(userId: string, keyHash: string, keyPrefix: string, name: string): Promise<ApiKey>;
12
31
  updateApiKeyLastUsed(keyHash: string): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"postgres.d.ts","sourceRoot":"","sources":["../../../src/lib/db/postgres.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"postgres.d.ts","sourceRoot":"","sources":["../../../src/lib/db/postgres.ts"],"names":[],"mappings":"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"}
@@ -68,6 +68,59 @@ export const db = {
68
68
  const result = await getPool().query('SELECT status FROM users WHERE id = $1', [userId]);
69
69
  return result.rows[0]?.status || null;
70
70
  },
71
+ // ===== SPENDING MANAGEMENT =====
72
+ async getUserSpending(userId) {
73
+ const result = await getPool().query('SELECT current_month_spending, monthly_spending_limit, spending_period_start, status, limit_enforcement_type FROM users WHERE id = $1', [userId]);
74
+ if (!result.rows[0])
75
+ return null;
76
+ return {
77
+ currentSpending: parseFloat(result.rows[0].current_month_spending) || 0,
78
+ limit: result.rows[0].monthly_spending_limit ? parseFloat(result.rows[0].monthly_spending_limit) : null,
79
+ periodStart: result.rows[0].spending_period_start,
80
+ status: result.rows[0].status,
81
+ limitEnforcementType: result.rows[0].limit_enforcement_type,
82
+ };
83
+ },
84
+ async updateUserSpending(userId, newSpending) {
85
+ await getPool().query('UPDATE users SET current_month_spending = $1, updated_at = NOW() WHERE id = $2', [newSpending, userId]);
86
+ },
87
+ async incrementUserSpending(userId, cost) {
88
+ const result = await getPool().query(`UPDATE users
89
+ SET current_month_spending = current_month_spending + $1, updated_at = NOW()
90
+ WHERE id = $2
91
+ RETURNING current_month_spending, monthly_spending_limit`, [cost, userId]);
92
+ const row = result.rows[0];
93
+ const newSpending = parseFloat(row.current_month_spending);
94
+ const limit = row.monthly_spending_limit ? parseFloat(row.monthly_spending_limit) : null;
95
+ const exceeded = limit !== null && newSpending > limit;
96
+ return { newSpending, limit, exceeded };
97
+ },
98
+ async setUserStatus(userId, status) {
99
+ await getPool().query('UPDATE users SET status = $1, updated_at = NOW() WHERE id = $2', [status, userId]);
100
+ },
101
+ async setUserSpendingLimit(userId, limit) {
102
+ await getPool().query('UPDATE users SET monthly_spending_limit = $1, updated_at = NOW() WHERE id = $2', [limit, userId]);
103
+ },
104
+ async setUserEnforcementType(userId, enforcementType) {
105
+ await getPool().query('UPDATE users SET limit_enforcement_type = $1, updated_at = NOW() WHERE id = $2', [enforcementType, userId]);
106
+ },
107
+ async resetUserSpending(userId) {
108
+ await getPool().query(`UPDATE users
109
+ SET current_month_spending = 0,
110
+ spending_period_start = NOW(),
111
+ status = CASE WHEN status = 'over_limit' THEN 'active' ELSE status END,
112
+ updated_at = NOW()
113
+ WHERE id = $1`, [userId]);
114
+ },
115
+ async getUsersToResetSpending() {
116
+ const result = await getPool().query(`SELECT id FROM users
117
+ WHERE spending_period_start < NOW() - INTERVAL '30 days'
118
+ AND status IN ('active', 'over_limit')`);
119
+ return result.rows.map(row => row.id);
120
+ },
121
+ async recordSpendingAlert(userId) {
122
+ await getPool().query('UPDATE users SET last_spending_alert_sent_at = NOW(), updated_at = NOW() WHERE id = $1', [userId]);
123
+ },
71
124
  // API Keys
72
125
  async getApiKeyByHash(keyHash) {
73
126
  const result = await getPool().query('SELECT * FROM api_keys WHERE key_hash = $1 AND is_active = true', [keyHash]);
@@ -8,6 +8,11 @@ export declare const cache: {
8
8
  invalidateGate(userId: string, gateName: string): Promise<void>;
9
9
  invalidateUserGates(userId: string): Promise<void>;
10
10
  ping(): Promise<boolean>;
11
+ getUserSpending(userId: string): Promise<number | null>;
12
+ incrementUserSpending(userId: string, cost: number): Promise<number>;
13
+ setUserSpending(userId: string, spending: number): Promise<void>;
14
+ invalidateUserSpending(userId: string): Promise<void>;
15
+ getAllCachedSpendingUsers(): Promise<string[]>;
11
16
  };
12
17
  export default redis;
13
18
  //# sourceMappingURL=redis.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"redis.d.ts","sourceRoot":"","sources":["../../../src/lib/db/redis.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,SAAS,CAAC;AAC5B,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAG1C,QAAA,MAAM,KAAK,OAcT,CAAC;AAuBH,eAAO,MAAM,KAAK;oBAEM,MAAM,YAAY,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;wBAqB3C,MAAM,UAAU,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;oBAqBjD,MAAM,YAAY,MAAM,QAAQ,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;2BAe7C,MAAM,YAAY,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;gCAUnC,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAc1C,OAAO,CAAC,OAAO,CAAC;CAQ/B,CAAC;AAEF,eAAe,KAAK,CAAC"}
1
+ {"version":3,"file":"redis.d.ts","sourceRoot":"","sources":["../../../src/lib/db/redis.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,SAAS,CAAC;AAC5B,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAG1C,QAAA,MAAM,KAAK,OAcT,CAAC;AAuBH,eAAO,MAAM,KAAK;oBAEM,MAAM,YAAY,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;wBAqB3C,MAAM,UAAU,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;oBAqBjD,MAAM,YAAY,MAAM,QAAQ,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;2BAe7C,MAAM,YAAY,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;gCAUnC,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAa1C,OAAO,CAAC,OAAO,CAAC;4BAWA,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;kCAWzB,MAAM,QAAQ,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;4BAY5C,MAAM,YAAY,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;mCAUjC,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;iCASxB,OAAO,CAAC,MAAM,EAAE,CAAC;CAUrD,CAAC;AAEF,eAAe,KAAK,CAAC"}
@@ -105,7 +105,6 @@ export const cache = {
105
105
  console.error('Redis bulk delete error:', error);
106
106
  }
107
107
  },
108
- // health check
109
108
  async ping() {
110
109
  try {
111
110
  const result = await redis.ping();
@@ -115,5 +114,59 @@ export const cache = {
115
114
  return false;
116
115
  }
117
116
  },
117
+ // ===== SPENDING CACHE =====
118
+ async getUserSpending(userId) {
119
+ try {
120
+ const key = `spending:${userId}`;
121
+ const spending = await redis.get(key);
122
+ return spending ? parseFloat(spending) : null;
123
+ }
124
+ catch (error) {
125
+ console.error('Redis getUserSpending error:', error);
126
+ return null;
127
+ }
128
+ },
129
+ async incrementUserSpending(userId, cost) {
130
+ try {
131
+ const key = `spending:${userId}`;
132
+ const newSpending = await redis.incrbyfloat(key, cost);
133
+ await redis.expire(key, 3600);
134
+ return parseFloat(newSpending);
135
+ }
136
+ catch (error) {
137
+ console.error('Redis incrementUserSpending error:', error);
138
+ throw error;
139
+ }
140
+ },
141
+ async setUserSpending(userId, spending) {
142
+ try {
143
+ const key = `spending:${userId}`;
144
+ await redis.set(key, spending.toString());
145
+ await redis.expire(key, 3600);
146
+ }
147
+ catch (error) {
148
+ console.error('Redis setUserSpending error:', error);
149
+ }
150
+ },
151
+ async invalidateUserSpending(userId) {
152
+ try {
153
+ const key = `spending:${userId}`;
154
+ await redis.del(key);
155
+ }
156
+ catch (error) {
157
+ console.error('Redis invalidateUserSpending error:', error);
158
+ }
159
+ },
160
+ async getAllCachedSpendingUsers() {
161
+ try {
162
+ const pattern = 'spending:*';
163
+ const keys = await redis.keys(pattern);
164
+ return keys.map(key => key.replace('spending:', ''));
165
+ }
166
+ catch (error) {
167
+ console.error('Redis getAllCachedSpendingUsers error:', error);
168
+ return [];
169
+ }
170
+ },
118
171
  };
119
172
  export default redis;
@@ -0,0 +1,6 @@
1
+ export declare const spendingJobs: {
2
+ syncSpendingJob(): Promise<void>;
3
+ resetSpendingPeriodsJob(): Promise<void>;
4
+ startScheduledJobs(): void;
5
+ };
6
+ //# sourceMappingURL=spending-jobs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spending-jobs.d.ts","sourceRoot":"","sources":["../../src/lib/spending-jobs.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,YAAY;uBACE,OAAO,CAAC,IAAI,CAAC;+BASL,OAAO,CAAC,IAAI,CAAC;0BAwBxB,IAAI;CAyB3B,CAAC"}
@@ -0,0 +1,56 @@
1
+ import { db } from './db/postgres.js';
2
+ import { cache } from './db/redis.js';
3
+ import { spendingTracker } from './spending-tracker.js';
4
+ export const spendingJobs = {
5
+ async syncSpendingJob() {
6
+ console.log('[Spending Job] Starting periodic sync...');
7
+ try {
8
+ await spendingTracker.syncAllSpending();
9
+ }
10
+ catch (error) {
11
+ console.error('[Spending Job] Sync failed:', error);
12
+ }
13
+ },
14
+ async resetSpendingPeriodsJob() {
15
+ console.log('[Spending Job] Checking for billing periods to reset...');
16
+ try {
17
+ const usersToReset = await db.getUsersToResetSpending();
18
+ if (usersToReset.length === 0) {
19
+ console.log('[Spending Job] No users need reset');
20
+ return;
21
+ }
22
+ console.log(`[Spending Job] Resetting ${usersToReset.length} users`);
23
+ for (const userId of usersToReset) {
24
+ await db.resetUserSpending(userId);
25
+ await cache.invalidateUserSpending(userId);
26
+ console.log(`[Spending Job] Reset user ${userId}`);
27
+ }
28
+ console.log('[Spending Job] Reset complete');
29
+ }
30
+ catch (error) {
31
+ console.error('[Spending Job] Reset failed:', error);
32
+ }
33
+ },
34
+ startScheduledJobs() {
35
+ // Sync Redis to DB every 5 minutes
36
+ setInterval(() => {
37
+ this.syncSpendingJob().catch(err => {
38
+ console.error('[Spending Job] Sync interval error:', err);
39
+ });
40
+ }, 5 * 60 * 1000);
41
+ // Check for billing period resets every hour
42
+ setInterval(() => {
43
+ this.resetSpendingPeriodsJob().catch(err => {
44
+ console.error('[Spending Job] Reset interval error:', err);
45
+ });
46
+ }, 60 * 60 * 1000);
47
+ // Run once on startup
48
+ this.syncSpendingJob().catch(err => {
49
+ console.error('[Spending Job] Initial sync error:', err);
50
+ });
51
+ this.resetSpendingPeriodsJob().catch(err => {
52
+ console.error('[Spending Job] Initial reset error:', err);
53
+ });
54
+ console.log('[Spending Job] Scheduled jobs started');
55
+ },
56
+ };
@@ -0,0 +1,17 @@
1
+ interface SpendingUpdate {
2
+ userId: string;
3
+ cost: number;
4
+ exceeded?: boolean;
5
+ newSpending?: number;
6
+ }
7
+ export declare const spendingTracker: {
8
+ trackSpending(userId: string, cost: number): Promise<SpendingUpdate>;
9
+ trackSpendingDB(userId: string, cost: number): Promise<SpendingUpdate>;
10
+ checkAlertThresholds(userId: string, currentSpending: number, limit: number | null): Promise<void>;
11
+ sendAlertIfNeeded(userId: string, threshold: number, currentSpending: number, limit: number): Promise<void>;
12
+ syncSpendingToDB(userId: string): Promise<void>;
13
+ syncAllSpending(): Promise<void>;
14
+ warmCache(userId: string): Promise<void>;
15
+ };
16
+ export {};
17
+ //# sourceMappingURL=spending-tracker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spending-tracker.d.ts","sourceRoot":"","sources":["../../src/lib/spending-tracker.ts"],"names":[],"mappings":"AAGA,UAAU,cAAc;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,eAAO,MAAM,eAAe;0BACE,MAAM,QAAQ,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;4BA0B5C,MAAM,QAAQ,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;iCAkBzC,MAAM,mBAAmB,MAAM,SAAS,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;8BAcxE,MAAM,aAAa,MAAM,mBAAmB,MAAM,SAAS,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;6BAKlF,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;uBAY5B,OAAO,CAAC,IAAI,CAAC;sBAad,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAU/C,CAAC"}
@@ -0,0 +1,89 @@
1
+ import { db } from './db/postgres.js';
2
+ import { cache } from './db/redis.js';
3
+ export const spendingTracker = {
4
+ async trackSpending(userId, cost) {
5
+ try {
6
+ const newSpending = await cache.incrementUserSpending(userId, cost);
7
+ const spendingInfo = await db.getUserSpending(userId);
8
+ if (!spendingInfo) {
9
+ return { userId, cost, newSpending };
10
+ }
11
+ const { limit, status } = spendingInfo;
12
+ const exceeded = limit !== null && newSpending > limit;
13
+ if (exceeded && status === 'active') {
14
+ await db.setUserStatus(userId, 'over_limit');
15
+ console.log(`[Spending] User ${userId} exceeded limit: ${newSpending} > ${limit}`);
16
+ }
17
+ await this.checkAlertThresholds(userId, newSpending, limit);
18
+ return { userId, cost, exceeded, newSpending };
19
+ }
20
+ catch (error) {
21
+ console.error('[Spending] Redis error, falling back to DB:', error);
22
+ return await this.trackSpendingDB(userId, cost);
23
+ }
24
+ },
25
+ async trackSpendingDB(userId, cost) {
26
+ const result = await db.incrementUserSpending(userId, cost);
27
+ if (result.exceeded) {
28
+ await db.setUserStatus(userId, 'over_limit');
29
+ console.log(`[Spending] User ${userId} exceeded limit: ${result.newSpending} > ${result.limit}`);
30
+ }
31
+ await this.checkAlertThresholds(userId, result.newSpending, result.limit);
32
+ return {
33
+ userId,
34
+ cost,
35
+ exceeded: result.exceeded,
36
+ newSpending: result.newSpending,
37
+ };
38
+ },
39
+ async checkAlertThresholds(userId, currentSpending, limit) {
40
+ if (!limit || limit === 0)
41
+ return;
42
+ const percentage = (currentSpending / limit) * 100;
43
+ const thresholds = [50, 80, 95, 100];
44
+ for (const threshold of thresholds) {
45
+ if (percentage >= threshold) {
46
+ await this.sendAlertIfNeeded(userId, threshold, currentSpending, limit);
47
+ break;
48
+ }
49
+ }
50
+ },
51
+ async sendAlertIfNeeded(userId, threshold, currentSpending, limit) {
52
+ console.log(`[Spending] Alert: User ${userId} at ${threshold}% of limit ($${currentSpending}/$${limit})`);
53
+ await db.recordSpendingAlert(userId);
54
+ },
55
+ async syncSpendingToDB(userId) {
56
+ try {
57
+ const cachedSpending = await cache.getUserSpending(userId);
58
+ if (cachedSpending !== null) {
59
+ await db.updateUserSpending(userId, cachedSpending);
60
+ console.log(`[Spending] Synced user ${userId}: $${cachedSpending}`);
61
+ }
62
+ }
63
+ catch (error) {
64
+ console.error(`[Spending] Sync error for user ${userId}:`, error);
65
+ }
66
+ },
67
+ async syncAllSpending() {
68
+ try {
69
+ const userIds = await cache.getAllCachedSpendingUsers();
70
+ console.log(`[Spending] Syncing ${userIds.length} users to DB`);
71
+ await Promise.all(userIds.map(userId => this.syncSpendingToDB(userId)));
72
+ console.log('[Spending] Sync complete');
73
+ }
74
+ catch (error) {
75
+ console.error('[Spending] Bulk sync error:', error);
76
+ }
77
+ },
78
+ async warmCache(userId) {
79
+ try {
80
+ const spendingInfo = await db.getUserSpending(userId);
81
+ if (spendingInfo) {
82
+ await cache.setUserSpending(userId, spendingInfo.currentSpending);
83
+ }
84
+ }
85
+ catch (error) {
86
+ console.error(`[Spending] Cache warm error for user ${userId}:`, error);
87
+ }
88
+ },
89
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAK1D,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,OAAO,CAAC;QAChB,UAAU,OAAO;YACf,MAAM,CAAC,EAAE,MAAM,CAAC;YAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;YAClB,UAAU,CAAC,EAAE,MAAM,CAAC;SACrB;KACF;CACF;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,YAAY,GACjB,OAAO,CAAC,IAAI,CAAC,CAyHf;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,YAAY,GACjB,IAAI,CAWN"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAK1D,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,OAAO,CAAC;QAChB,UAAU,OAAO;YACf,MAAM,CAAC,EAAE,MAAM,CAAC;YAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;YAClB,UAAU,CAAC,EAAE,MAAM,CAAC;SACrB;KACF;CACF;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,YAAY,GACjB,OAAO,CAAC,IAAI,CAAC,CAiJf;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,YAAY,GACjB,IAAI,CAWN"}
@@ -64,6 +64,17 @@ export async function authenticate(req, res, next) {
64
64
  });
65
65
  return;
66
66
  }
67
+ // Check spending limits if user has exceeded and enforcement is set to block
68
+ if (userStatus === 'over_limit') {
69
+ const spendingInfo = await db.getUserSpending(apiKeyRecord.userId);
70
+ if (spendingInfo?.limitEnforcementType === 'block') {
71
+ res.status(403).json({
72
+ error: 'spending_limit_exceeded',
73
+ message: 'You have exceeded your spending limit. Requests are blocked until your next billing period or until you increase your limit.',
74
+ });
75
+ return;
76
+ }
77
+ }
67
78
  // Attach userId to request for downstream handlers
68
79
  req.userId = apiKeyRecord.userId;
69
80
  req.apiKeyId = apiKeyRecord.id;
@@ -94,6 +105,17 @@ export async function authenticate(req, res, next) {
94
105
  });
95
106
  return;
96
107
  }
108
+ // Check spending limits if user has exceeded and enforcement is set to block
109
+ if (userStatus === 'over_limit') {
110
+ const spendingInfo = await db.getUserSpending(sessionKey.userId);
111
+ if (spendingInfo?.limitEnforcementType === 'block') {
112
+ res.status(403).json({
113
+ error: 'spending_limit_exceeded',
114
+ message: 'You have exceeded your spending limit. Requests are blocked until your next billing period or until you increase your limit.',
115
+ });
116
+ return;
117
+ }
118
+ }
97
119
  req.userId = sessionKey.userId;
98
120
  next();
99
121
  return;
@@ -1 +1 @@
1
- {"version":3,"file":"chat-completions.d.ts","sourceRoot":"","sources":["../../../src/routes/v1/chat-completions.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,SAAS,CAAC;AAapD,QAAA,MAAM,MAAM,EAAE,UAAqB,CAAC;AA6RpC,eAAe,MAAM,CAAC"}
1
+ {"version":3,"file":"chat-completions.d.ts","sourceRoot":"","sources":["../../../src/routes/v1/chat-completions.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,SAAS,CAAC;AAcpD,QAAA,MAAM,MAAM,EAAE,UAAqB,CAAC;AAqSpC,eAAe,MAAM,CAAC"}
@@ -2,6 +2,7 @@ import { Router } from 'express';
2
2
  import { nanoid } from 'nanoid';
3
3
  import { db } from '../../lib/db/postgres.js';
4
4
  import { authenticate } from '../../middleware/auth.js';
5
+ import { spendingTracker } from '../../lib/spending-tracker.js';
5
6
  import { convertOpenAIRequestToLayer, convertLayerResponseToOpenAI, convertLayerChunkToOpenAI, } from '../../lib/openai-conversion.js';
6
7
  import { resolveFinalRequest } from '../v3/chat.js';
7
8
  import { callAdapter, callAdapterStream } from '../../lib/provider-factory.js';
@@ -147,6 +148,9 @@ router.post('/', authenticate, async (req, res) => {
147
148
  cost: totalCost,
148
149
  },
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
+ });
150
154
  }
151
155
  catch (streamError) {
152
156
  const errorMessage = streamError instanceof Error ? streamError.message : 'Unknown streaming error';
@@ -218,6 +222,9 @@ router.post('/', authenticate, async (req, res) => {
218
222
  finishReason: result.finishReason,
219
223
  },
220
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
+ });
221
228
  const openaiResponse = convertLayerResponseToOpenAI(result);
222
229
  res.json(openaiResponse);
223
230
  }
@@ -1 +1 @@
1
- {"version":3,"file":"gates.d.ts","sourceRoot":"","sources":["../../../src/routes/v1/gates.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,SAAS,CAAC;AASpD,QAAA,MAAM,MAAM,EAAE,UAAqB,CAAC;AAuhBpC,eAAe,MAAM,CAAC"}
1
+ {"version":3,"file":"gates.d.ts","sourceRoot":"","sources":["../../../src/routes/v1/gates.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,SAAS,CAAC;AASpD,QAAA,MAAM,MAAM,EAAE,UAAqB,CAAC;AAwhBpC,eAAe,MAAM,CAAC"}
@@ -416,7 +416,7 @@ router.post('/suggestions', async (req, res) => {
416
416
  return;
417
417
  }
418
418
  try {
419
- const { description, costWeight, latencyWeight, qualityWeight } = req.body;
419
+ const { description, costWeight, latencyWeight, qualityWeight, availableProviders } = req.body;
420
420
  if (!description) {
421
421
  res.status(400).json({ error: 'bad_request', message: 'Gate must have a description for AI recommendations' });
422
422
  return;
@@ -425,6 +425,7 @@ router.post('/suggestions', async (req, res) => {
425
425
  costWeight: parseFloat(costWeight ?? '0.33'),
426
426
  latencyWeight: parseFloat(latencyWeight ?? '0.33'),
427
427
  qualityWeight: parseFloat(qualityWeight ?? '0.34'),
428
+ availableProviders: Array.isArray(availableProviders) ? availableProviders : undefined,
428
429
  };
429
430
  const { analyzeTask } = await import('../../services/task-analysis.js');
430
431
  const suggestions = await analyzeTask(description, userPreferences);
@@ -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,
@@ -3,5 +3,6 @@ export declare function analyzeTask(description: string, userPreferences?: {
3
3
  costWeight?: number;
4
4
  latencyWeight?: number;
5
5
  qualityWeight?: number;
6
+ availableProviders?: string[];
6
7
  }): Promise<TaskAnalysis>;
7
8
  //# sourceMappingURL=task-analysis.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"task-analysis.d.ts","sourceRoot":"","sources":["../../src/services/task-analysis.ts"],"names":[],"mappings":"AACA,OAAO,EAAkB,YAAY,EAAmC,MAAM,eAAe,CAAC;AA2D9F,wBAAsB,WAAW,CAC/B,WAAW,EAAE,MAAM,EACnB,eAAe,CAAC,EAAE;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,GACA,OAAO,CAAC,YAAY,CAAC,CAkHvB"}
1
+ {"version":3,"file":"task-analysis.d.ts","sourceRoot":"","sources":["../../src/services/task-analysis.ts"],"names":[],"mappings":"AACA,OAAO,EAAkB,YAAY,EAAmC,MAAM,eAAe,CAAC;AA2D9F,wBAAsB,WAAW,CAC/B,WAAW,EAAE,MAAM,EACnB,eAAe,CAAC,EAAE;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC/B,GACA,OAAO,CAAC,YAAY,CAAC,CA6HvB"}
@@ -57,6 +57,7 @@ export async function analyzeTask(description, userPreferences) {
57
57
  const costWeight = userPreferences?.costWeight ?? 0.33;
58
58
  const latencyWeight = userPreferences?.latencyWeight ?? 0.33;
59
59
  const qualityWeight = userPreferences?.qualityWeight ?? 0.33;
60
+ const availableProviders = userPreferences?.availableProviders;
60
61
  let taskType = 'chat';
61
62
  try {
62
63
  taskType = await detectTaskType(description, anthropic);
@@ -66,9 +67,17 @@ export async function analyzeTask(description, userPreferences) {
66
67
  }
67
68
  const filteredRegistry = {};
68
69
  for (const [key, model] of Object.entries(MODEL_REGISTRY)) {
69
- if (model.type === taskType) {
70
- filteredRegistry[key] = model;
70
+ // Filter by task type
71
+ if (model.type !== taskType) {
72
+ continue;
71
73
  }
74
+ // Filter by BYOK providers if specified
75
+ if (availableProviders && availableProviders.length > 0) {
76
+ if (!availableProviders.includes(model.provider)) {
77
+ continue;
78
+ }
79
+ }
80
+ filteredRegistry[key] = model;
72
81
  }
73
82
  const registryContext = JSON.stringify(filteredRegistry, null, 2);
74
83
  const prompt = `You are analyzing a task to recommend the best AI models from our registry.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@layer-ai/core",
3
- "version": "2.0.20",
3
+ "version": "2.0.22",
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.7"
39
+ "@layer-ai/sdk": "^2.5.8"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/bcryptjs": "^2.4.6",