@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 +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/lib/db/migrations/007_add_spending_controls.sql +41 -0
- package/dist/lib/db/postgres.d.ts +19 -0
- package/dist/lib/db/postgres.d.ts.map +1 -1
- package/dist/lib/db/postgres.js +53 -0
- package/dist/lib/db/redis.d.ts +5 -0
- package/dist/lib/db/redis.d.ts.map +1 -1
- package/dist/lib/db/redis.js +54 -1
- package/dist/lib/spending-jobs.d.ts +6 -0
- package/dist/lib/spending-jobs.d.ts.map +1 -0
- package/dist/lib/spending-jobs.js +56 -0
- package/dist/lib/spending-tracker.d.ts +17 -0
- package/dist/lib/spending-tracker.d.ts.map +1 -0
- package/dist/lib/spending-tracker.js +89 -0
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +22 -0
- package/dist/routes/v1/chat-completions.d.ts.map +1 -1
- package/dist/routes/v1/chat-completions.js +7 -0
- package/dist/routes/v1/gates.d.ts.map +1 -1
- package/dist/routes/v1/gates.js +2 -1
- package/dist/routes/v1/spending.d.ts +4 -0
- package/dist/routes/v1/spending.d.ts.map +1 -0
- package/dist/routes/v1/spending.js +94 -0
- package/dist/routes/v2/complete.d.ts.map +1 -1
- package/dist/routes/v2/complete.js +4 -0
- package/dist/routes/v3/chat.d.ts.map +1 -1
- package/dist/routes/v3/chat.js +7 -0
- package/dist/services/task-analysis.d.ts +1 -0
- package/dist/services/task-analysis.d.ts.map +1 -1
- package/dist/services/task-analysis.js +11 -2
- package/package.json +2 -2
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
|
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;AAC5D,OAAO,EAAE,OAAO,IAAI,qBAAqB,EAAE,MAAM,iCAAiC,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":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAyB,WAAW,EAAE,MAAM,eAAe,CAAC;AAO5F,iBAAS,OAAO,IAAI,EAAE,CAAC,IAAI,CAqB1B;AA0BD,eAAO,MAAM,EAAE;gBAEK,MAAM,WAAW,GAAG,EAAE;0BASZ,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;oBAQnC,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;sBAQ3B,MAAM,gBAAgB,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;0BAQxC,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"postgres.d.ts","sourceRoot":"","sources":["../../../src/lib/db/postgres.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/db/postgres.js
CHANGED
|
@@ -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]);
|
package/dist/lib/db/redis.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/lib/db/redis.js
CHANGED
|
@@ -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 @@
|
|
|
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,
|
|
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"}
|
package/dist/middleware/auth.js
CHANGED
|
@@ -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;
|
|
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;
|
|
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"}
|
package/dist/routes/v1/gates.js
CHANGED
|
@@ -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 @@
|
|
|
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;
|
|
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;
|
|
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"}
|
package/dist/routes/v3/chat.js
CHANGED
|
@@ -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,
|
|
@@ -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;
|
|
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
|
-
|
|
70
|
-
|
|
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.
|
|
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.
|
|
39
|
+
"@layer-ai/sdk": "^2.5.8"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/bcryptjs": "^2.4.6",
|