Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/util/db-schema/purchase-quotas.ts
6030 views
1
import { CREATED_BY, ID } from "./crm";
2
import { SCHEMA as schema } from "./index";
3
import { LLM_USERNAMES } from "./llm-utils";
4
import type { Service } from "./purchases";
5
import { Table } from "./types";
6
7
export type { Service };
8
9
// Users will set their spend limits for these broad categories.
10
// TODO: right now there is a separate limit for each quota spec,
11
// which has got ridiculous.
12
const SERVICE_CATEGORIES = ["money", "compute", "license", "ai"];
13
type ServiceCategory = (typeof SERVICE_CATEGORIES)[number];
14
15
export interface Spec {
16
display: string; // what to show user to describe this service
17
noSet?: boolean; // if true, then no spend limits are set for this.
18
color: string;
19
category: ServiceCategory;
20
// tooltip more detailed description
21
description?: string;
22
}
23
24
export type QuotaSpec = Record<Service, Spec>;
25
26
// for each category of service, this says whether or not it is a pay as you go service,
27
// which can impact how spend options are determined.
28
const IS_PAYG: { [name: ServiceCategory]: boolean } = {
29
money: false,
30
compute: true,
31
license: false,
32
ai: true,
33
} as const;
34
35
export function isPaygService(service: Service): boolean {
36
const category = QUOTA_SPEC[service]?.category;
37
return IS_PAYG[category ?? ""] ?? false;
38
}
39
40
const GOOGLE_AI_COLOR = "#ff4d4f";
41
const ANTHROPIC_COLOR = "#181818";
42
const OPENAI_COLOR = "#10a37f";
43
const MISTRALAI_COLOR = "#ff7000";
44
const XAI_COLOR = "#000000";
45
46
const GPT_TURBO_128k: Spec = {
47
display: "OpenAI GPT-4 Turbo 128k",
48
color: OPENAI_COLOR,
49
category: "ai",
50
} as const;
51
52
const GPT_TURBO_8K: Spec = {
53
...GPT_TURBO_128k,
54
display: "OpenAI GPT-4 Turbo",
55
} as const;
56
57
const GPT_OMNI_128k: Spec = {
58
display: "OpenAI GPT-4o 128k",
59
color: OPENAI_COLOR,
60
category: "ai",
61
} as const;
62
63
const GPT_OMNI_8K: Spec = {
64
...GPT_OMNI_128k,
65
display: "OpenAI GPT-4o",
66
} as const;
67
68
const GPT_OMNI_MINI_128k: Spec = {
69
...GPT_OMNI_128k,
70
display: "OpenAI GPT-4o Mini 128k",
71
} as const;
72
73
const GPT_OMNI_MINI_8K: Spec = {
74
...GPT_OMNI_MINI_128k,
75
display: "OpenAI GPT-4o Mini",
76
} as const;
77
78
const GPT_41_8K: Spec = {
79
display: "OpenAI GPT-4.1",
80
color: OPENAI_COLOR,
81
category: "ai",
82
} as const;
83
84
const GPT_41_MINI_8K: Spec = {
85
...GPT_41_8K,
86
display: "OpenAI GPT-4.1 Mini",
87
} as const;
88
89
const GPT_O1_8K: Spec = {
90
...GPT_OMNI_128k,
91
display: "OpenAI o1",
92
} as const;
93
94
const GPT_O1_MINI_8K: Spec = {
95
...GPT_O1_8K,
96
display: "OpenAI o1 mini",
97
} as const;
98
99
const GPT_O3_8K: Spec = {
100
display: "OpenAI o3",
101
color: OPENAI_COLOR,
102
category: "ai",
103
} as const;
104
105
const GPT_O3_128k: Spec = {
106
...GPT_O3_8K,
107
display: "OpenAI o3 128k",
108
} as const;
109
110
const GPT_O4_MINI_8K: Spec = {
111
display: "OpenAI o4-mini",
112
color: OPENAI_COLOR,
113
category: "ai",
114
} as const;
115
116
const GPT_O4_MINI_128k: Spec = {
117
...GPT_O4_MINI_8K,
118
display: "OpenAI o4-mini 128k",
119
} as const;
120
121
const GPT_5_8K: Spec = {
122
display: "OpenAI GPT-5",
123
color: OPENAI_COLOR,
124
category: "ai",
125
} as const;
126
127
const GPT_5_128k: Spec = {
128
...GPT_5_8K,
129
display: "OpenAI GPT-5 128k",
130
} as const;
131
132
const GPT_5_2_8K: Spec = {
133
display: "OpenAI GPT-5.2",
134
color: OPENAI_COLOR,
135
category: "ai",
136
} as const;
137
138
const GPT_5_2_128k: Spec = {
139
...GPT_5_2_8K,
140
display: "OpenAI GPT-5.2 128k",
141
} as const;
142
143
const GPT_5_MINI_8K: Spec = {
144
display: "OpenAI GPT-5 Mini",
145
color: OPENAI_COLOR,
146
category: "ai",
147
} as const;
148
149
const GPT_5_MINI_128k: Spec = {
150
...GPT_5_MINI_8K,
151
display: "OpenAI GPT-5 Mini 128k",
152
} as const;
153
154
// NOTE: all-quotas-config.tsx will automatically filter out those, which are free or not selectable by the user
155
export const QUOTA_SPEC: QuotaSpec = {
156
credit: {
157
display: "Credit",
158
noSet: true,
159
color: "green",
160
category: "money",
161
description:
162
"Credit that was added to your account as a result of a manual or subscription payment (e.g., from a credit card)",
163
},
164
"auto-credit": {
165
display: "Automatic Credit",
166
noSet: true,
167
color: "green",
168
category: "money",
169
description:
170
"Credited that was automatically added to your account as a result of a payment because of your balance became low.",
171
},
172
refund: {
173
display: "Refund",
174
noSet: true,
175
color: "red",
176
category: "money",
177
description:
178
"Money that was refunded to your account as a result of a support request.",
179
},
180
"compute-server": {
181
display: "Compute Server",
182
color: "#2196f3",
183
category: "compute",
184
description: "Charge for creating or using a compute server.",
185
},
186
"compute-server-network-usage": {
187
display: "Network Data",
188
color: "#2196f3",
189
category: "compute",
190
description: "Charge due to network traffic out of a compute server.",
191
},
192
"compute-server-storage": {
193
display: "Cloud Storage",
194
color: "#fbbd05",
195
category: "compute",
196
description: "Charge due to storage of data on a cloud filesystem.",
197
},
198
license: {
199
display: "License",
200
color: "cyan",
201
noSet: true,
202
category: "license",
203
description: "Purchase of a license from the store.",
204
},
205
"edit-license": {
206
display: "Edit License",
207
color: "gold",
208
noSet: true,
209
category: "license",
210
description:
211
"Charge or credit resulting from changing a license, which includes you manually editing the license, or the license being edited to extend the validity date on subscription renewal.",
212
},
213
voucher: {
214
display: "Voucher",
215
color: "#00238b",
216
noSet: true,
217
category: "money",
218
description: "Charge for purchasing a voucher.",
219
},
220
// ATTN: LLMs comes below this line, the quotas above are the important ones to show first!
221
"openai-gpt-4": {
222
display: "OpenAI GPT-4",
223
color: OPENAI_COLOR,
224
category: "ai",
225
},
226
"openai-gpt-3.5-turbo": {
227
display: "OpenAI GPT-3.5",
228
color: OPENAI_COLOR,
229
category: "ai",
230
},
231
"openai-gpt-3.5-turbo-16k": {
232
display: "OpenAI GPT-3.5 16k",
233
color: OPENAI_COLOR,
234
category: "ai",
235
},
236
"openai-text-embedding-ada-002": {
237
display: "OpenAI Text Embedding Ada 002",
238
color: OPENAI_COLOR,
239
noSet: true, // because this model is not user visible yet
240
category: "ai",
241
},
242
"openai-gpt-4-32k": {
243
display: "OpenAI GPT-4 32k",
244
color: OPENAI_COLOR,
245
category: "ai",
246
},
247
"openai-gpt-4-turbo-preview": GPT_TURBO_128k, // the "preview" is over
248
"openai-gpt-4-turbo-preview-8k": GPT_TURBO_8K, // the "preview" is over
249
"openai-gpt-4-turbo": GPT_TURBO_128k,
250
"openai-gpt-4-turbo-8k": GPT_TURBO_8K,
251
"openai-gpt-4o": GPT_OMNI_128k,
252
"openai-gpt-4o-8k": GPT_OMNI_8K,
253
"openai-gpt-4o-mini": GPT_OMNI_MINI_128k,
254
"openai-gpt-4o-mini-8k": GPT_OMNI_MINI_8K,
255
"openai-gpt-4.1": GPT_41_8K,
256
"openai-gpt-4.1-mini": GPT_41_MINI_8K,
257
"openai-o1-mini-8k": GPT_O1_8K,
258
"openai-o1-8k": GPT_O1_MINI_8K,
259
"openai-o1-mini": GPT_O1_8K,
260
"openai-o1": GPT_O1_MINI_8K,
261
"openai-o3-8k": GPT_O3_8K,
262
"openai-o3": GPT_O3_128k,
263
"openai-o4-mini-8k": GPT_O4_MINI_8K,
264
"openai-o4-mini": GPT_O4_MINI_128k,
265
"openai-gpt-5-8k": GPT_5_8K,
266
"openai-gpt-5": GPT_5_128k,
267
"openai-gpt-5.2-8k": GPT_5_2_8K,
268
"openai-gpt-5.2": GPT_5_2_128k,
269
"openai-gpt-5-mini-8k": GPT_5_MINI_8K,
270
"openai-gpt-5-mini": GPT_5_MINI_128k,
271
"google-text-bison-001": {
272
display: "Google Palm 2 (Text)",
273
color: GOOGLE_AI_COLOR,
274
noSet: true, // deprecated, will be removed
275
category: "ai",
276
},
277
"google-chat-bison-001": {
278
display: "Google Palm 2 (Chat)",
279
color: GOOGLE_AI_COLOR,
280
noSet: true, // deprecated, will be removed
281
category: "ai",
282
},
283
"google-embedding-gecko-001": {
284
display: "Google Gecko (Embedding)",
285
color: GOOGLE_AI_COLOR,
286
noSet: true, // deprecated, will be removed
287
category: "ai",
288
},
289
"google-gemini-1.5-flash": {
290
display: "Google Gemini 1.5 Flash",
291
color: GOOGLE_AI_COLOR,
292
category: "ai",
293
},
294
"google-gemini-1.5-flash-8k": {
295
display: "Google Gemini 1.5 Flash",
296
color: GOOGLE_AI_COLOR,
297
category: "ai",
298
},
299
"google-gemini-pro": {
300
display: "Google Gemini 1.0 Pro",
301
color: GOOGLE_AI_COLOR,
302
category: "ai",
303
},
304
"google-gemini-1.0-ultra": {
305
display: "Google Gemini 1.0 Ultra",
306
color: GOOGLE_AI_COLOR,
307
category: "ai",
308
},
309
"google-gemini-1.5-pro-8k": {
310
display: LLM_USERNAMES["gemini-1.5-pro-8k"],
311
color: GOOGLE_AI_COLOR,
312
category: "ai",
313
},
314
"google-gemini-1.5-pro": {
315
display: LLM_USERNAMES["gemini-1.5-pro"],
316
color: GOOGLE_AI_COLOR,
317
category: "ai",
318
},
319
"google-gemini-2.0-flash-8k": {
320
display: LLM_USERNAMES["gemini-2.0-flash-8k"],
321
color: GOOGLE_AI_COLOR,
322
category: "ai",
323
},
324
"google-gemini-2.0-flash-lite-8k": {
325
display: LLM_USERNAMES["gemini-2.0-flash-lite-8k"],
326
color: GOOGLE_AI_COLOR,
327
category: "ai",
328
},
329
"google-gemini-2.5-flash-8k": {
330
display: LLM_USERNAMES["gemini-2.5-flash-8k"],
331
color: GOOGLE_AI_COLOR,
332
category: "ai",
333
},
334
"google-gemini-2.5-pro-8k": {
335
display: LLM_USERNAMES["gemini-2.5-pro-8k"],
336
color: GOOGLE_AI_COLOR,
337
category: "ai",
338
},
339
"google-gemini-3-flash-preview-16k": {
340
display: LLM_USERNAMES["gemini-3-flash-preview-16k"],
341
color: GOOGLE_AI_COLOR,
342
category: "ai",
343
},
344
"google-gemini-3-pro-preview-8k": {
345
display: LLM_USERNAMES["gemini-3-pro-preview-8k"],
346
color: GOOGLE_AI_COLOR,
347
category: "ai",
348
},
349
"xai-grok-4-1-fast-non-reasoning-16k": {
350
display: LLM_USERNAMES["grok-4-1-fast-non-reasoning-16k"],
351
color: XAI_COLOR,
352
category: "ai",
353
},
354
"xai-grok-4-1-fast-reasoning-16k": {
355
display: LLM_USERNAMES["grok-4-1-fast-reasoning-16k"],
356
color: XAI_COLOR,
357
category: "ai",
358
},
359
"xai-grok-code-fast-1-16k": {
360
display: LLM_USERNAMES["grok-code-fast-1-16k"],
361
color: XAI_COLOR,
362
category: "ai",
363
},
364
"anthropic-claude-3-opus": {
365
display: LLM_USERNAMES["claude-3-opus"],
366
color: ANTHROPIC_COLOR,
367
category: "ai",
368
},
369
"anthropic-claude-3-opus-8k": {
370
display: LLM_USERNAMES["claude-3-opus-8k"],
371
color: ANTHROPIC_COLOR,
372
category: "ai",
373
},
374
"anthropic-claude-3-sonnet": {
375
display: LLM_USERNAMES["claude-3-sonnet"],
376
color: ANTHROPIC_COLOR,
377
category: "ai",
378
},
379
"anthropic-claude-3-sonnet-4k": {
380
display: LLM_USERNAMES["claude-3-sonnet-4k"],
381
color: ANTHROPIC_COLOR,
382
category: "ai",
383
},
384
"anthropic-claude-3-5-sonnet": {
385
display: LLM_USERNAMES["claude-3-5-sonnet"],
386
color: ANTHROPIC_COLOR,
387
category: "ai",
388
},
389
"anthropic-claude-3-5-sonnet-4k": {
390
display: LLM_USERNAMES["claude-3-5-sonnet-4k"],
391
color: ANTHROPIC_COLOR,
392
category: "ai",
393
},
394
"anthropic-claude-3-haiku": {
395
display: LLM_USERNAMES["claude-3-haiku"],
396
color: ANTHROPIC_COLOR,
397
category: "ai",
398
},
399
"anthropic-claude-3-haiku-8k": {
400
display: LLM_USERNAMES["claude-3-haiku-8k"],
401
color: ANTHROPIC_COLOR,
402
category: "ai",
403
},
404
"anthropic-claude-3-5-haiku-8k": {
405
display: LLM_USERNAMES["claude-3-5-haiku-8k"],
406
color: ANTHROPIC_COLOR,
407
category: "ai",
408
},
409
"anthropic-claude-4-sonnet-8k": {
410
display: LLM_USERNAMES["claude-4-sonnet-8k"],
411
color: ANTHROPIC_COLOR,
412
category: "ai",
413
},
414
"anthropic-claude-4-opus-8k": {
415
display: LLM_USERNAMES["claude-4-opus-8k"],
416
color: ANTHROPIC_COLOR,
417
category: "ai",
418
},
419
"anthropic-claude-4-5-sonnet-8k": {
420
display: LLM_USERNAMES["claude-4-5-sonnet-8k"],
421
color: ANTHROPIC_COLOR,
422
category: "ai",
423
},
424
"anthropic-claude-4-6-sonnet-8k": {
425
display: LLM_USERNAMES["claude-4-6-sonnet-8k"],
426
color: ANTHROPIC_COLOR,
427
category: "ai",
428
},
429
"anthropic-claude-4-5-opus-8k": {
430
display: LLM_USERNAMES["claude-4-5-opus-8k"],
431
color: ANTHROPIC_COLOR,
432
category: "ai",
433
},
434
"anthropic-claude-4-6-opus-8k": {
435
display: LLM_USERNAMES["claude-4-6-opus-8k"],
436
color: ANTHROPIC_COLOR,
437
category: "ai",
438
},
439
"anthropic-claude-4-5-haiku-8k": {
440
display: LLM_USERNAMES["claude-4-5-haiku-8k"],
441
color: ANTHROPIC_COLOR,
442
category: "ai",
443
},
444
"mistralai-mistral-small-latest": {
445
display: LLM_USERNAMES["mistral-small-latest"],
446
color: MISTRALAI_COLOR, // the orange from their website
447
category: "ai",
448
},
449
"mistralai-mistral-medium-latest": {
450
display: LLM_USERNAMES["mistral-medium-latest"],
451
color: MISTRALAI_COLOR, // the orange from their website
452
category: "ai",
453
},
454
"mistralai-mistral-large-latest": {
455
display: LLM_USERNAMES["mistral-large-latest"],
456
color: MISTRALAI_COLOR, // the orange from their website
457
category: "ai",
458
},
459
"mistralai-devstral-medium-2507": {
460
display: LLM_USERNAMES["devstral-medium-2507"],
461
color: MISTRALAI_COLOR, // the orange from their website
462
category: "ai",
463
},
464
// "mistralai-magistral-medium-latest": {
465
// display: LLM_USERNAMES["magistral-medium-latest"],
466
// color: MISTRALAI_COLOR, // the orange from their website
467
// category: "ai",
468
// },
469
"project-upgrade": {
470
display: "Project Upgrade",
471
color: "#5bc0de",
472
category: "compute",
473
description:
474
"Charge resulting from using pay as you go upgrades to a project.",
475
},
476
} as const;
477
478
// For pay-as-you-go project quota upgrades
479
export interface ProjectQuota {
480
cost?: number; // dollars per hour
481
enabled?: number;
482
cores?: number;
483
disk_quota?: number;
484
memory?: number;
485
mintime?: number;
486
network?: number;
487
member_host?: number;
488
always_running?: number;
489
}
490
491
export const PROJECT_QUOTA_KEYS = new Set<string>([
492
"enabled",
493
"cost",
494
"cores",
495
"disk_quota",
496
"memory",
497
"mintime",
498
"network",
499
"member_host",
500
"always_running",
501
]);
502
503
export function serviceToDisplay(service: Service): string {
504
return QUOTA_SPEC[service]?.display ?? service;
505
}
506
507
Table({
508
name: "purchase_quotas",
509
fields: {
510
id: ID,
511
account_id: CREATED_BY,
512
service: {
513
title: "Service Category",
514
desc: "The service being charged for, e.g., openai-gpt-4, project-upgrade, etc.",
515
type: "string",
516
pg_type: "varchar(127)",
517
},
518
value: {
519
title: "Value",
520
desc: "The maximum amount that user can be charged for this service during one month billing period, in US dollars.",
521
type: "number", // actually comes back as string in queries.
522
pg_type: "REAL",
523
},
524
},
525
rules: {
526
desc: "Purchase Quotas",
527
primary_key: "id",
528
// make it fast to find all quotas for a given account
529
pg_indexes: ["account_id"],
530
// enforce that there is only one quota for each service for a given account
531
pg_unique_indexes: ["(account_id,service)"],
532
user_query: {
533
// set happens though v2 api only to enforce global quota
534
get: {
535
pg_where: [{ "account_id = $::UUID": "account_id" }],
536
fields: {
537
id: null,
538
account_id: null,
539
service: null,
540
value: null,
541
},
542
},
543
},
544
},
545
});
546
547
Table({
548
name: "crm_purchase_quotas",
549
rules: {
550
virtual: "purchase_quotas",
551
primary_key: "id",
552
user_query: {
553
get: {
554
pg_where: [],
555
admin: true,
556
fields: {
557
id: null,
558
account_id: null,
559
service: null,
560
value: null,
561
},
562
},
563
set: {
564
admin: true,
565
fields: {
566
id: true,
567
account_id: true,
568
service: true,
569
value: true,
570
},
571
},
572
},
573
},
574
fields: schema.purchase_quotas.fields,
575
});
576
577