@layer-ai/core 2.0.19 → 2.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/index.d.ts +4 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +5 -0
  4. package/dist/lib/db/migrations/007_add_spending_controls.sql +41 -0
  5. package/dist/lib/db/postgres.d.ts +19 -0
  6. package/dist/lib/db/postgres.d.ts.map +1 -1
  7. package/dist/lib/db/postgres.js +53 -0
  8. package/dist/lib/db/redis.d.ts +5 -0
  9. package/dist/lib/db/redis.d.ts.map +1 -1
  10. package/dist/lib/db/redis.js +54 -1
  11. package/dist/lib/openai-conversion.d.ts +6 -0
  12. package/dist/lib/openai-conversion.d.ts.map +1 -0
  13. package/dist/lib/openai-conversion.js +215 -0
  14. package/dist/lib/spending-jobs.d.ts +6 -0
  15. package/dist/lib/spending-jobs.d.ts.map +1 -0
  16. package/dist/lib/spending-jobs.js +56 -0
  17. package/dist/lib/spending-tracker.d.ts +17 -0
  18. package/dist/lib/spending-tracker.d.ts.map +1 -0
  19. package/dist/lib/spending-tracker.js +89 -0
  20. package/dist/middleware/auth.d.ts.map +1 -1
  21. package/dist/middleware/auth.js +22 -0
  22. package/dist/routes/tests/test-openai-endpoint.d.ts +3 -0
  23. package/dist/routes/tests/test-openai-endpoint.d.ts.map +1 -0
  24. package/dist/routes/tests/test-openai-endpoint.js +292 -0
  25. package/dist/routes/v1/chat-completions.d.ts +4 -0
  26. package/dist/routes/v1/chat-completions.d.ts.map +1 -0
  27. package/dist/routes/v1/chat-completions.js +269 -0
  28. package/dist/routes/v1/spending.d.ts +4 -0
  29. package/dist/routes/v1/spending.d.ts.map +1 -0
  30. package/dist/routes/v1/spending.js +94 -0
  31. package/dist/routes/v2/complete.d.ts.map +1 -1
  32. package/dist/routes/v2/complete.js +4 -0
  33. package/dist/routes/v3/chat.d.ts.map +1 -1
  34. package/dist/routes/v3/chat.js +7 -0
  35. package/package.json +2 -2
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
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAG5D,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAGpE,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;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
@@ -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":"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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
+ 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,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"}