@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.
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -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/openai-conversion.d.ts +6 -0
- package/dist/lib/openai-conversion.d.ts.map +1 -0
- package/dist/lib/openai-conversion.js +215 -0
- 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/tests/test-openai-endpoint.d.ts +3 -0
- package/dist/routes/tests/test-openai-endpoint.d.ts.map +1 -0
- package/dist/routes/tests/test-openai-endpoint.js +292 -0
- package/dist/routes/v1/chat-completions.d.ts +4 -0
- package/dist/routes/v1/chat-completions.d.ts.map +1 -0
- package/dist/routes/v1/chat-completions.js +269 -0
- 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/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ export { default as authRouter } from './routes/v1/auth.js';
|
|
|
2
2
|
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
|
+
export { default as chatCompletionsRouter } from './routes/v1/chat-completions.js';
|
|
6
|
+
export { default as spendingRouter } from './routes/v1/spending.js';
|
|
5
7
|
export { default as completeRouter } from './routes/v2/complete.js';
|
|
6
8
|
export { default as chatRouter } from './routes/v3/chat.js';
|
|
7
9
|
export { default as imageRouter } from './routes/v3/image.js';
|
|
@@ -19,4 +21,6 @@ export declare const createSessionKey: (userId: string) => Promise<string>;
|
|
|
19
21
|
export declare const deleteSessionKeysForUser: (userId: string) => Promise<void>;
|
|
20
22
|
export * from './services/task-analysis.js';
|
|
21
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';
|
|
22
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;
|
|
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
|
@@ -3,6 +3,8 @@ export { default as authRouter } from './routes/v1/auth.js';
|
|
|
3
3
|
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
|
+
export { default as chatCompletionsRouter } from './routes/v1/chat-completions.js';
|
|
7
|
+
export { default as spendingRouter } from './routes/v1/spending.js';
|
|
6
8
|
// v2 routes
|
|
7
9
|
export { default as completeRouter } from './routes/v2/complete.js';
|
|
8
10
|
// v3 routes
|
|
@@ -32,3 +34,6 @@ export const deleteSessionKeysForUser = async (userId) => {
|
|
|
32
34
|
export * from './services/task-analysis.js';
|
|
33
35
|
// Provider Factory
|
|
34
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":"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;4BAU7B,MAAM,GAAG,OAAO,CAAC;QAAE,eAAe,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,WAAW,EAAE,IAAI,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,oBAAoB,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;+BAexI,MAAM,eAAe,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;kCAOxC,MAAM,QAAQ,MAAM,GAAG,OAAO,CAAC;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAA;KAAE,CAAC;0BAexG,MAAM,UAAU,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;iCAO/B,MAAM,SAAS,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;mCAO1C,MAAM,mBAAmB,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;8BAOpD,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;+BAYrB,OAAO,CAAC,MAAM,EAAE,CAAC;gCAShB,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;6BAQzB,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;yBAQnC,MAAM,WAAW,MAAM,aAAa,MAAM,QAAQ,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;kCAQjE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;8BAO1B,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;qBAQnC,MAAM,UAAU,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;iCAS7B,MAAM,YAAY,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;+BAQjD,MAAM,UAAU,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;4BAQhD,MAAM,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;uBAQ7B,MAAM,QAAQ,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;oBAiCpC,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;mBAQ9B,MAAM,QAAQ,GAAG,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;mBAwDxC,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;qBAUvB,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;2BAkBhC,MAAM,YACJ;QACR,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,SAAS,CAAC,EAAE,IAAI,CAAC;QACjB,OAAO,CAAC,EAAE,IAAI,CAAC;QACf,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,GACA,OAAO,CAAC,GAAG,EAAE,CAAC;iCAuCkB,MAAM,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;6BAQhE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;qCAehB,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;2BAQhC,MAAM,YAAY,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;4BAQrD,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;8BASnD,MAAM,YACJ,MAAM,gBACF;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,aACrD,MAAM,GAChB,OAAO,CAAC,WAAW,CAAC;8BAWb,MAAM,YACJ,MAAM,gBACF;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,aACrD,MAAM,GAChB,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;8BAWE,MAAM,YAAY,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;oCAQrC,MAAM,YAAY,MAAM,YAAY,OAAO,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;kCAW3E,MAAM,YAAY,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;qCAQzC,MAAM,GAAQ,OAAO,CAAC,WAAW,EAAE,CAAC;8BAahE,MAAM,QACR,OAAO,CAAC,IAAI,CAAC,aACR,MAAM,GAAG,MAAM,kBACV,MAAM,EAAE,GACvB,OAAO,CAAC,IAAI,CAAC;2BA8Ca,MAAM,UAAS,MAAM,GAAQ,OAAO,CAAC,GAAG,EAAE,CAAC;2BAW3C,MAAM,GAAG,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC;+BAQxB,MAAM,UAAS,MAAM,GAAS,OAAO,CAAC,GAAG,EAAE,CAAC;8BAcnE,MAAM,UACN,MAAM,GAAG,IAAI,UACb,eAAe,GAAG,aAAa,GAAG,YAAY,GAAG,UAAU,WAC1D,GAAG,GACX,OAAO,CAAC,IAAI,CAAC;2BAQa,MAAM,UAAS,MAAM,GAAQ,OAAO,CAAC,GAAG,EAAE,CAAC;gCAWtC,MAAM,UAAS,MAAM,GAAS,OAAO,CAAC,GAAG,EAAE,CAAC;4BAchD,MAAM,UAAS,MAAM,GAAS,OAAO,CAAC,GAAG,EAAE,CAAC;yBAa/C,MAAM,aAAa,MAAM,UAAU,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;CA+E5F,CAAC;AAEF,eAAe,OAAO,CAAC"}
|
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,6 @@
|
|
|
1
|
+
import type { OpenAIChatCompletionRequest, OpenAIChatCompletionResponse, OpenAIChatCompletionChunk } from '@layer-ai/sdk';
|
|
2
|
+
import type { LayerRequest, LayerResponse } from '@layer-ai/sdk';
|
|
3
|
+
export declare function convertOpenAIRequestToLayer(openaiReq: OpenAIChatCompletionRequest, gateId: string): LayerRequest;
|
|
4
|
+
export declare function convertLayerResponseToOpenAI(layerResp: LayerResponse, requestId?: string): OpenAIChatCompletionResponse;
|
|
5
|
+
export declare function convertLayerChunkToOpenAI(layerChunk: LayerResponse, requestId: string, created: number): OpenAIChatCompletionChunk;
|
|
6
|
+
//# sourceMappingURL=openai-conversion.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openai-conversion.d.ts","sourceRoot":"","sources":["../../src/lib/openai-conversion.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,2BAA2B,EAK3B,4BAA4B,EAC5B,yBAAyB,EAE1B,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EACV,YAAY,EACZ,aAAa,EAOd,MAAM,eAAe,CAAC;AAqFvB,wBAAgB,2BAA2B,CACzC,SAAS,EAAE,2BAA2B,EACtC,MAAM,EAAE,MAAM,GACb,YAAY,CA2Cd;AAwCD,wBAAgB,4BAA4B,CAC1C,SAAS,EAAE,aAAa,EACxB,SAAS,CAAC,EAAE,MAAM,GACjB,4BAA4B,CA+B9B;AAED,wBAAgB,yBAAyB,CACvC,UAAU,EAAE,aAAa,EACzB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,GACd,yBAAyB,CA2C3B"}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { nanoid } from 'nanoid';
|
|
2
|
+
function convertMessage(openaiMsg) {
|
|
3
|
+
const layerMsg = {
|
|
4
|
+
role: openaiMsg.role,
|
|
5
|
+
};
|
|
6
|
+
if (typeof openaiMsg.content === 'string') {
|
|
7
|
+
layerMsg.content = openaiMsg.content;
|
|
8
|
+
}
|
|
9
|
+
else if (Array.isArray(openaiMsg.content)) {
|
|
10
|
+
const textParts = [];
|
|
11
|
+
const imageParts = [];
|
|
12
|
+
for (const part of openaiMsg.content) {
|
|
13
|
+
if (part.type === 'text') {
|
|
14
|
+
textParts.push(part.text);
|
|
15
|
+
}
|
|
16
|
+
else if (part.type === 'image_url') {
|
|
17
|
+
imageParts.push({
|
|
18
|
+
url: part.image_url.url,
|
|
19
|
+
detail: part.image_url.detail,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (textParts.length > 0) {
|
|
24
|
+
layerMsg.content = textParts.join('\n');
|
|
25
|
+
}
|
|
26
|
+
if (imageParts.length > 0) {
|
|
27
|
+
layerMsg.images = imageParts;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (openaiMsg.tool_calls) {
|
|
31
|
+
layerMsg.toolCalls = openaiMsg.tool_calls.map(tc => ({
|
|
32
|
+
id: tc.id,
|
|
33
|
+
type: 'function',
|
|
34
|
+
function: {
|
|
35
|
+
name: tc.function.name,
|
|
36
|
+
arguments: tc.function.arguments,
|
|
37
|
+
},
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
if (openaiMsg.tool_call_id) {
|
|
41
|
+
layerMsg.toolCallId = openaiMsg.tool_call_id;
|
|
42
|
+
}
|
|
43
|
+
if (openaiMsg.name) {
|
|
44
|
+
layerMsg.name = openaiMsg.name;
|
|
45
|
+
}
|
|
46
|
+
return layerMsg;
|
|
47
|
+
}
|
|
48
|
+
function convertTool(openaiTool) {
|
|
49
|
+
return {
|
|
50
|
+
type: 'function',
|
|
51
|
+
function: {
|
|
52
|
+
name: openaiTool.function.name,
|
|
53
|
+
description: openaiTool.function.description,
|
|
54
|
+
parameters: openaiTool.function.parameters,
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function convertToolChoice(openaiToolChoice) {
|
|
59
|
+
if (!openaiToolChoice)
|
|
60
|
+
return undefined;
|
|
61
|
+
if (typeof openaiToolChoice === 'string')
|
|
62
|
+
return openaiToolChoice;
|
|
63
|
+
return openaiToolChoice;
|
|
64
|
+
}
|
|
65
|
+
function convertResponseFormat(openaiFormat) {
|
|
66
|
+
if (!openaiFormat)
|
|
67
|
+
return undefined;
|
|
68
|
+
if (openaiFormat.type === 'json_schema' && openaiFormat.json_schema) {
|
|
69
|
+
return {
|
|
70
|
+
type: 'json_schema',
|
|
71
|
+
json_schema: openaiFormat.json_schema,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return openaiFormat.type;
|
|
75
|
+
}
|
|
76
|
+
export function convertOpenAIRequestToLayer(openaiReq, gateId) {
|
|
77
|
+
let systemPrompt;
|
|
78
|
+
const messages = [];
|
|
79
|
+
for (const msg of openaiReq.messages) {
|
|
80
|
+
if (msg.role === 'system' && typeof msg.content === 'string') {
|
|
81
|
+
systemPrompt = msg.content;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
messages.push(convertMessage(msg));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const layerRequest = {
|
|
88
|
+
gateId,
|
|
89
|
+
type: 'chat',
|
|
90
|
+
model: openaiReq.model,
|
|
91
|
+
data: {
|
|
92
|
+
messages,
|
|
93
|
+
systemPrompt,
|
|
94
|
+
temperature: openaiReq.temperature,
|
|
95
|
+
maxTokens: openaiReq.max_tokens || openaiReq.max_completion_tokens,
|
|
96
|
+
topP: openaiReq.top_p,
|
|
97
|
+
stream: openaiReq.stream,
|
|
98
|
+
stopSequences: typeof openaiReq.stop === 'string' ? [openaiReq.stop] : openaiReq.stop,
|
|
99
|
+
frequencyPenalty: openaiReq.frequency_penalty,
|
|
100
|
+
presencePenalty: openaiReq.presence_penalty,
|
|
101
|
+
seed: openaiReq.seed,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
if (openaiReq.tools && openaiReq.tools.length > 0) {
|
|
105
|
+
layerRequest.data.tools = openaiReq.tools.map(convertTool);
|
|
106
|
+
}
|
|
107
|
+
if (openaiReq.tool_choice) {
|
|
108
|
+
layerRequest.data.toolChoice = convertToolChoice(openaiReq.tool_choice);
|
|
109
|
+
}
|
|
110
|
+
if (openaiReq.response_format) {
|
|
111
|
+
layerRequest.data.responseFormat = convertResponseFormat(openaiReq.response_format);
|
|
112
|
+
}
|
|
113
|
+
return layerRequest;
|
|
114
|
+
}
|
|
115
|
+
function convertFinishReason(layerReason) {
|
|
116
|
+
if (!layerReason)
|
|
117
|
+
return null;
|
|
118
|
+
switch (layerReason) {
|
|
119
|
+
case 'completed':
|
|
120
|
+
return 'stop';
|
|
121
|
+
case 'length_limit':
|
|
122
|
+
return 'length';
|
|
123
|
+
case 'tool_call':
|
|
124
|
+
return 'tool_calls';
|
|
125
|
+
case 'filtered':
|
|
126
|
+
return 'content_filter';
|
|
127
|
+
default:
|
|
128
|
+
return 'stop';
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function convertToolCallsToOpenAI(layerToolCalls) {
|
|
132
|
+
if (!layerToolCalls || layerToolCalls.length === 0)
|
|
133
|
+
return undefined;
|
|
134
|
+
return layerToolCalls.map(tc => ({
|
|
135
|
+
id: tc.id,
|
|
136
|
+
type: 'function',
|
|
137
|
+
function: {
|
|
138
|
+
name: tc.function.name,
|
|
139
|
+
arguments: tc.function.arguments,
|
|
140
|
+
},
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
143
|
+
function convertUsage(layerUsage) {
|
|
144
|
+
return {
|
|
145
|
+
prompt_tokens: layerUsage?.promptTokens || 0,
|
|
146
|
+
completion_tokens: layerUsage?.completionTokens || 0,
|
|
147
|
+
total_tokens: layerUsage?.totalTokens || 0,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
export function convertLayerResponseToOpenAI(layerResp, requestId) {
|
|
151
|
+
const id = requestId || layerResp.id || `chatcmpl-${nanoid()}`;
|
|
152
|
+
const created = layerResp.created || Math.floor(Date.now() / 1000);
|
|
153
|
+
const message = {
|
|
154
|
+
role: 'assistant',
|
|
155
|
+
content: layerResp.content || undefined,
|
|
156
|
+
};
|
|
157
|
+
const toolCalls = convertToolCallsToOpenAI(layerResp.toolCalls);
|
|
158
|
+
if (toolCalls) {
|
|
159
|
+
message.tool_calls = toolCalls;
|
|
160
|
+
}
|
|
161
|
+
const response = {
|
|
162
|
+
id,
|
|
163
|
+
object: 'chat.completion',
|
|
164
|
+
created,
|
|
165
|
+
model: layerResp.model || 'unknown',
|
|
166
|
+
choices: [
|
|
167
|
+
{
|
|
168
|
+
index: 0,
|
|
169
|
+
message,
|
|
170
|
+
finish_reason: convertFinishReason(layerResp.finishReason),
|
|
171
|
+
logprobs: null,
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
usage: convertUsage(layerResp.usage),
|
|
175
|
+
};
|
|
176
|
+
return response;
|
|
177
|
+
}
|
|
178
|
+
export function convertLayerChunkToOpenAI(layerChunk, requestId, created) {
|
|
179
|
+
const delta = {};
|
|
180
|
+
if (layerChunk.content && !layerChunk.finishReason) {
|
|
181
|
+
delta.role = 'assistant';
|
|
182
|
+
}
|
|
183
|
+
if (layerChunk.content) {
|
|
184
|
+
delta.content = layerChunk.content;
|
|
185
|
+
}
|
|
186
|
+
if (layerChunk.toolCalls && layerChunk.toolCalls.length > 0) {
|
|
187
|
+
delta.tool_calls = layerChunk.toolCalls.map((tc, index) => ({
|
|
188
|
+
index,
|
|
189
|
+
id: tc.id,
|
|
190
|
+
type: 'function',
|
|
191
|
+
function: {
|
|
192
|
+
name: tc.function.name,
|
|
193
|
+
arguments: tc.function.arguments,
|
|
194
|
+
},
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
const chunk = {
|
|
198
|
+
id: requestId,
|
|
199
|
+
object: 'chat.completion.chunk',
|
|
200
|
+
created,
|
|
201
|
+
model: layerChunk.model || 'unknown',
|
|
202
|
+
choices: [
|
|
203
|
+
{
|
|
204
|
+
index: 0,
|
|
205
|
+
delta,
|
|
206
|
+
finish_reason: convertFinishReason(layerChunk.finishReason),
|
|
207
|
+
logprobs: null,
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
};
|
|
211
|
+
if (layerChunk.usage && layerChunk.finishReason) {
|
|
212
|
+
chunk.usage = convertUsage(layerChunk.usage);
|
|
213
|
+
}
|
|
214
|
+
return chunk;
|
|
215
|
+
}
|
|
@@ -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"}
|