@llmops/core 0.1.4 → 0.1.5-beta.2
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/{bun-sqlite-dialect-zL8xmYst.cjs → bun-sqlite-dialect-Bp2qbl5F.cjs} +1 -1
- package/dist/db/index.cjs +2 -1
- package/dist/db/index.d.cts +2 -2
- package/dist/db/index.d.mts +2 -2
- package/dist/db/index.mjs +2 -2
- package/dist/{db-CGY-vZ3u.mjs → db-DSzwrW4p.mjs} +108 -2
- package/dist/{db-C9-M-kdS.cjs → db-eEfIe5dO.cjs} +115 -3
- package/dist/{index-DTHo2J3v.d.cts → index-BO7DYWFs.d.cts} +338 -22
- package/dist/{index-D3ncxgf2.d.mts → index-mUSLoeGU.d.mts} +338 -22
- package/dist/index.cjs +993 -2
- package/dist/index.d.cts +782 -3
- package/dist/index.d.mts +782 -3
- package/dist/index.mjs +981 -4
- package/dist/{node-sqlite-dialect-CQlHW438.cjs → node-sqlite-dialect-b2V910TJ.cjs} +1 -1
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as object, C as zod_default, D as boolean, E as array, M as string, N as union, O as literal, P as unknown, S as workspaceSettingsSchema, T as any, _ as llmRequestsSchema, a as matchType, b as variantVersionsSchema, c as parsePartialTableData, d as validateTableData, f as SCHEMA_METADATA, g as environmentsSchema, h as environmentSecretsSchema, i as getMigrations, j as record, k as number, l as parseTableData, m as configsSchema, n as createDatabaseFromConnection, o as runAutoMigrations, p as configVariantsSchema, r as detectDatabaseType, s as logger, t as createDatabase, u as validatePartialTableData, v as schemas, w as _enum, x as variantsSchema, y as targetingRulesSchema } from "./db-DSzwrW4p.mjs";
|
|
2
2
|
import gateway from "@llmops/gateway";
|
|
3
|
+
import { sql } from "kysely";
|
|
4
|
+
import * as fs from "node:fs/promises";
|
|
5
|
+
import * as path from "node:path";
|
|
3
6
|
import { createRandomStringGenerator } from "@better-auth/utils/random";
|
|
4
7
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
5
8
|
|
|
@@ -450,7 +453,7 @@ const authSchema = object({ type: string().min(1, "Auth type is required") }).pa
|
|
|
450
453
|
const llmopsConfigSchema = object({
|
|
451
454
|
database: any(),
|
|
452
455
|
auth: authSchema,
|
|
453
|
-
basePath: string().min(1, "Base path is required and cannot be empty").refine((path) => path.startsWith("/"), "Base path must start with a forward slash"),
|
|
456
|
+
basePath: string().min(1, "Base path is required and cannot be empty").refine((path$1) => path$1.startsWith("/"), "Base path must start with a forward slash"),
|
|
454
457
|
providers: providersSchema,
|
|
455
458
|
autoMigrate: union([boolean(), literal("development")]).optional().default(false),
|
|
456
459
|
schema: string().optional().default("llmops")
|
|
@@ -464,6 +467,491 @@ function validateLLMOpsConfig(config) {
|
|
|
464
467
|
return result.data;
|
|
465
468
|
}
|
|
466
469
|
|
|
470
|
+
//#endregion
|
|
471
|
+
//#region src/cache/types.ts
|
|
472
|
+
/** Time constants in milliseconds for convenience */
|
|
473
|
+
const MS = {
|
|
474
|
+
"1_MINUTE": 60 * 1e3,
|
|
475
|
+
"5_MINUTES": 300 * 1e3,
|
|
476
|
+
"10_MINUTES": 600 * 1e3,
|
|
477
|
+
"30_MINUTES": 1800 * 1e3,
|
|
478
|
+
"1_HOUR": 3600 * 1e3,
|
|
479
|
+
"6_HOURS": 360 * 60 * 1e3,
|
|
480
|
+
"12_HOURS": 720 * 60 * 1e3,
|
|
481
|
+
"1_DAY": 1440 * 60 * 1e3,
|
|
482
|
+
"7_DAYS": 10080 * 60 * 1e3,
|
|
483
|
+
"30_DAYS": 720 * 60 * 60 * 1e3
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
//#endregion
|
|
487
|
+
//#region src/cache/backends/memory.ts
|
|
488
|
+
var MemoryCacheBackend = class {
|
|
489
|
+
cache = /* @__PURE__ */ new Map();
|
|
490
|
+
stats = {
|
|
491
|
+
hits: 0,
|
|
492
|
+
misses: 0,
|
|
493
|
+
sets: 0,
|
|
494
|
+
deletes: 0,
|
|
495
|
+
size: 0,
|
|
496
|
+
expired: 0
|
|
497
|
+
};
|
|
498
|
+
cleanupInterval;
|
|
499
|
+
maxSize;
|
|
500
|
+
constructor(maxSize = 1e4, cleanupIntervalMs = 6e4) {
|
|
501
|
+
this.maxSize = maxSize;
|
|
502
|
+
this.startCleanup(cleanupIntervalMs);
|
|
503
|
+
}
|
|
504
|
+
startCleanup(intervalMs) {
|
|
505
|
+
this.cleanupInterval = setInterval(() => {
|
|
506
|
+
this.cleanup();
|
|
507
|
+
}, intervalMs);
|
|
508
|
+
}
|
|
509
|
+
getFullKey(key, namespace) {
|
|
510
|
+
return namespace ? `${namespace}:${key}` : key;
|
|
511
|
+
}
|
|
512
|
+
isExpired(entry) {
|
|
513
|
+
return entry.expiresAt !== void 0 && entry.expiresAt <= Date.now();
|
|
514
|
+
}
|
|
515
|
+
evictIfNeeded() {
|
|
516
|
+
if (this.cache.size >= this.maxSize) {
|
|
517
|
+
const entries = Array.from(this.cache.entries());
|
|
518
|
+
entries.sort((a, b) => a[1].createdAt - b[1].createdAt);
|
|
519
|
+
const toRemove = Math.floor(this.maxSize * .1);
|
|
520
|
+
for (let i = 0; i < toRemove && i < entries.length; i++) this.cache.delete(entries[i][0]);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
async get(key, namespace) {
|
|
524
|
+
const fullKey = this.getFullKey(key, namespace);
|
|
525
|
+
const entry = this.cache.get(fullKey);
|
|
526
|
+
if (!entry) {
|
|
527
|
+
this.stats.misses++;
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
if (this.isExpired(entry)) {
|
|
531
|
+
this.cache.delete(fullKey);
|
|
532
|
+
this.stats.expired++;
|
|
533
|
+
this.stats.misses++;
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
this.stats.hits++;
|
|
537
|
+
return entry;
|
|
538
|
+
}
|
|
539
|
+
async set(key, value, options = {}) {
|
|
540
|
+
const fullKey = this.getFullKey(key, options.namespace);
|
|
541
|
+
const now = Date.now();
|
|
542
|
+
const entry = {
|
|
543
|
+
value,
|
|
544
|
+
createdAt: now,
|
|
545
|
+
expiresAt: options.ttl ? now + options.ttl : void 0,
|
|
546
|
+
metadata: options.metadata
|
|
547
|
+
};
|
|
548
|
+
this.evictIfNeeded();
|
|
549
|
+
this.cache.set(fullKey, entry);
|
|
550
|
+
this.stats.sets++;
|
|
551
|
+
this.stats.size = this.cache.size;
|
|
552
|
+
}
|
|
553
|
+
async delete(key, namespace) {
|
|
554
|
+
const fullKey = this.getFullKey(key, namespace);
|
|
555
|
+
const deleted = this.cache.delete(fullKey);
|
|
556
|
+
if (deleted) {
|
|
557
|
+
this.stats.deletes++;
|
|
558
|
+
this.stats.size = this.cache.size;
|
|
559
|
+
}
|
|
560
|
+
return deleted;
|
|
561
|
+
}
|
|
562
|
+
async clear(namespace) {
|
|
563
|
+
if (namespace) {
|
|
564
|
+
const prefix = `${namespace}:`;
|
|
565
|
+
const keysToDelete = Array.from(this.cache.keys()).filter((key) => key.startsWith(prefix));
|
|
566
|
+
for (const key of keysToDelete) this.cache.delete(key);
|
|
567
|
+
this.stats.deletes += keysToDelete.length;
|
|
568
|
+
} else {
|
|
569
|
+
this.stats.deletes += this.cache.size;
|
|
570
|
+
this.cache.clear();
|
|
571
|
+
}
|
|
572
|
+
this.stats.size = this.cache.size;
|
|
573
|
+
}
|
|
574
|
+
async has(key, namespace) {
|
|
575
|
+
const fullKey = this.getFullKey(key, namespace);
|
|
576
|
+
const entry = this.cache.get(fullKey);
|
|
577
|
+
if (!entry) return false;
|
|
578
|
+
if (this.isExpired(entry)) {
|
|
579
|
+
this.cache.delete(fullKey);
|
|
580
|
+
this.stats.expired++;
|
|
581
|
+
return false;
|
|
582
|
+
}
|
|
583
|
+
return true;
|
|
584
|
+
}
|
|
585
|
+
async keys(namespace) {
|
|
586
|
+
const allKeys = Array.from(this.cache.keys());
|
|
587
|
+
if (namespace) {
|
|
588
|
+
const prefix = `${namespace}:`;
|
|
589
|
+
return allKeys.filter((key) => key.startsWith(prefix)).map((key) => key.substring(prefix.length));
|
|
590
|
+
}
|
|
591
|
+
return allKeys;
|
|
592
|
+
}
|
|
593
|
+
async getStats(namespace) {
|
|
594
|
+
if (namespace) {
|
|
595
|
+
const prefix = `${namespace}:`;
|
|
596
|
+
const namespaceKeys = Array.from(this.cache.keys()).filter((key) => key.startsWith(prefix));
|
|
597
|
+
let expired = 0;
|
|
598
|
+
for (const key of namespaceKeys) {
|
|
599
|
+
const entry = this.cache.get(key);
|
|
600
|
+
if (entry && this.isExpired(entry)) expired++;
|
|
601
|
+
}
|
|
602
|
+
return {
|
|
603
|
+
...this.stats,
|
|
604
|
+
size: namespaceKeys.length,
|
|
605
|
+
expired
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
return { ...this.stats };
|
|
609
|
+
}
|
|
610
|
+
async cleanup() {
|
|
611
|
+
let expiredCount = 0;
|
|
612
|
+
for (const [key, entry] of this.cache.entries()) if (this.isExpired(entry)) {
|
|
613
|
+
this.cache.delete(key);
|
|
614
|
+
expiredCount++;
|
|
615
|
+
}
|
|
616
|
+
if (expiredCount > 0) {
|
|
617
|
+
this.stats.expired += expiredCount;
|
|
618
|
+
this.stats.size = this.cache.size;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
async close() {
|
|
622
|
+
if (this.cleanupInterval) {
|
|
623
|
+
clearInterval(this.cleanupInterval);
|
|
624
|
+
this.cleanupInterval = void 0;
|
|
625
|
+
}
|
|
626
|
+
this.cache.clear();
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
//#endregion
|
|
631
|
+
//#region src/cache/backends/file.ts
|
|
632
|
+
/**
|
|
633
|
+
* @file src/cache/backends/file.ts
|
|
634
|
+
* File-based cache backend implementation
|
|
635
|
+
*/
|
|
636
|
+
var FileCacheBackend = class {
|
|
637
|
+
cacheFile;
|
|
638
|
+
data = {};
|
|
639
|
+
saveTimer;
|
|
640
|
+
cleanupInterval;
|
|
641
|
+
loaded = false;
|
|
642
|
+
loadPromise;
|
|
643
|
+
stats = {
|
|
644
|
+
hits: 0,
|
|
645
|
+
misses: 0,
|
|
646
|
+
sets: 0,
|
|
647
|
+
deletes: 0,
|
|
648
|
+
size: 0,
|
|
649
|
+
expired: 0
|
|
650
|
+
};
|
|
651
|
+
saveInterval;
|
|
652
|
+
constructor(dataDir = "data", fileName = "cache.json", saveIntervalMs = 1e3, cleanupIntervalMs = 6e4) {
|
|
653
|
+
this.cacheFile = path.join(process.cwd(), dataDir, fileName);
|
|
654
|
+
this.saveInterval = saveIntervalMs;
|
|
655
|
+
this.loadPromise = this.loadCache();
|
|
656
|
+
this.loadPromise.then(() => {
|
|
657
|
+
this.startCleanup(cleanupIntervalMs);
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
/** Ensure cache is loaded before any operation */
|
|
661
|
+
async ensureLoaded() {
|
|
662
|
+
if (!this.loaded) await this.loadPromise;
|
|
663
|
+
}
|
|
664
|
+
async ensureDataDir() {
|
|
665
|
+
const dir = path.dirname(this.cacheFile);
|
|
666
|
+
try {
|
|
667
|
+
await fs.mkdir(dir, { recursive: true });
|
|
668
|
+
} catch {}
|
|
669
|
+
}
|
|
670
|
+
async loadCache() {
|
|
671
|
+
try {
|
|
672
|
+
const content = await fs.readFile(this.cacheFile, "utf-8");
|
|
673
|
+
this.data = JSON.parse(content);
|
|
674
|
+
this.updateStats();
|
|
675
|
+
this.loaded = true;
|
|
676
|
+
} catch {
|
|
677
|
+
this.data = {};
|
|
678
|
+
this.loaded = true;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
async saveCache() {
|
|
682
|
+
try {
|
|
683
|
+
await this.ensureDataDir();
|
|
684
|
+
await fs.writeFile(this.cacheFile, JSON.stringify(this.data, null, 2));
|
|
685
|
+
} catch {}
|
|
686
|
+
}
|
|
687
|
+
scheduleSave() {
|
|
688
|
+
if (this.saveTimer) clearTimeout(this.saveTimer);
|
|
689
|
+
this.saveTimer = setTimeout(() => {
|
|
690
|
+
this.saveCache();
|
|
691
|
+
this.saveTimer = void 0;
|
|
692
|
+
}, this.saveInterval);
|
|
693
|
+
}
|
|
694
|
+
startCleanup(intervalMs) {
|
|
695
|
+
this.cleanupInterval = setInterval(() => {
|
|
696
|
+
this.cleanup();
|
|
697
|
+
}, intervalMs);
|
|
698
|
+
}
|
|
699
|
+
isExpired(entry) {
|
|
700
|
+
return entry.expiresAt !== void 0 && entry.expiresAt <= Date.now();
|
|
701
|
+
}
|
|
702
|
+
updateStats() {
|
|
703
|
+
let totalSize = 0;
|
|
704
|
+
let totalExpired = 0;
|
|
705
|
+
for (const namespace of Object.values(this.data)) for (const entry of Object.values(namespace)) {
|
|
706
|
+
totalSize++;
|
|
707
|
+
if (this.isExpired(entry)) totalExpired++;
|
|
708
|
+
}
|
|
709
|
+
this.stats.size = totalSize;
|
|
710
|
+
this.stats.expired = totalExpired;
|
|
711
|
+
}
|
|
712
|
+
getNamespaceData(namespace = "default") {
|
|
713
|
+
if (!this.data[namespace]) this.data[namespace] = {};
|
|
714
|
+
return this.data[namespace];
|
|
715
|
+
}
|
|
716
|
+
async get(key, namespace) {
|
|
717
|
+
await this.ensureLoaded();
|
|
718
|
+
const namespaceData = this.getNamespaceData(namespace);
|
|
719
|
+
const entry = namespaceData[key];
|
|
720
|
+
if (!entry) {
|
|
721
|
+
this.stats.misses++;
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
724
|
+
if (this.isExpired(entry)) {
|
|
725
|
+
delete namespaceData[key];
|
|
726
|
+
this.stats.expired++;
|
|
727
|
+
this.stats.misses++;
|
|
728
|
+
this.scheduleSave();
|
|
729
|
+
return null;
|
|
730
|
+
}
|
|
731
|
+
this.stats.hits++;
|
|
732
|
+
return entry;
|
|
733
|
+
}
|
|
734
|
+
async set(key, value, options = {}) {
|
|
735
|
+
await this.ensureLoaded();
|
|
736
|
+
const namespace = options.namespace || "default";
|
|
737
|
+
const namespaceData = this.getNamespaceData(namespace);
|
|
738
|
+
const now = Date.now();
|
|
739
|
+
namespaceData[key] = {
|
|
740
|
+
value,
|
|
741
|
+
createdAt: now,
|
|
742
|
+
expiresAt: options.ttl ? now + options.ttl : void 0,
|
|
743
|
+
metadata: options.metadata
|
|
744
|
+
};
|
|
745
|
+
this.stats.sets++;
|
|
746
|
+
this.updateStats();
|
|
747
|
+
this.scheduleSave();
|
|
748
|
+
}
|
|
749
|
+
async delete(key, namespace) {
|
|
750
|
+
const namespaceData = this.getNamespaceData(namespace);
|
|
751
|
+
const existed = key in namespaceData;
|
|
752
|
+
if (existed) {
|
|
753
|
+
delete namespaceData[key];
|
|
754
|
+
this.stats.deletes++;
|
|
755
|
+
this.updateStats();
|
|
756
|
+
this.scheduleSave();
|
|
757
|
+
}
|
|
758
|
+
return existed;
|
|
759
|
+
}
|
|
760
|
+
async clear(namespace) {
|
|
761
|
+
if (namespace) {
|
|
762
|
+
const namespaceData = this.getNamespaceData(namespace);
|
|
763
|
+
const count = Object.keys(namespaceData).length;
|
|
764
|
+
this.data[namespace] = {};
|
|
765
|
+
this.stats.deletes += count;
|
|
766
|
+
} else {
|
|
767
|
+
const totalCount = Object.values(this.data).reduce((sum, ns) => sum + Object.keys(ns).length, 0);
|
|
768
|
+
this.data = {};
|
|
769
|
+
this.stats.deletes += totalCount;
|
|
770
|
+
}
|
|
771
|
+
this.updateStats();
|
|
772
|
+
this.scheduleSave();
|
|
773
|
+
}
|
|
774
|
+
async has(key, namespace) {
|
|
775
|
+
const namespaceData = this.getNamespaceData(namespace);
|
|
776
|
+
const entry = namespaceData[key];
|
|
777
|
+
if (!entry) return false;
|
|
778
|
+
if (this.isExpired(entry)) {
|
|
779
|
+
delete namespaceData[key];
|
|
780
|
+
this.stats.expired++;
|
|
781
|
+
this.scheduleSave();
|
|
782
|
+
return false;
|
|
783
|
+
}
|
|
784
|
+
return true;
|
|
785
|
+
}
|
|
786
|
+
async keys(namespace) {
|
|
787
|
+
if (namespace) {
|
|
788
|
+
const namespaceData = this.getNamespaceData(namespace);
|
|
789
|
+
return Object.keys(namespaceData);
|
|
790
|
+
}
|
|
791
|
+
const allKeys = [];
|
|
792
|
+
for (const namespaceData of Object.values(this.data)) allKeys.push(...Object.keys(namespaceData));
|
|
793
|
+
return allKeys;
|
|
794
|
+
}
|
|
795
|
+
async getStats(namespace) {
|
|
796
|
+
if (namespace) {
|
|
797
|
+
const namespaceData = this.getNamespaceData(namespace);
|
|
798
|
+
const keys = Object.keys(namespaceData);
|
|
799
|
+
let expired = 0;
|
|
800
|
+
for (const key of keys) {
|
|
801
|
+
const entry = namespaceData[key];
|
|
802
|
+
if (this.isExpired(entry)) expired++;
|
|
803
|
+
}
|
|
804
|
+
return {
|
|
805
|
+
...this.stats,
|
|
806
|
+
size: keys.length,
|
|
807
|
+
expired
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
this.updateStats();
|
|
811
|
+
return { ...this.stats };
|
|
812
|
+
}
|
|
813
|
+
async cleanup() {
|
|
814
|
+
let expiredCount = 0;
|
|
815
|
+
let hasChanges = false;
|
|
816
|
+
for (const [, namespaceData] of Object.entries(this.data)) for (const [key, entry] of Object.entries(namespaceData)) if (this.isExpired(entry)) {
|
|
817
|
+
delete namespaceData[key];
|
|
818
|
+
expiredCount++;
|
|
819
|
+
hasChanges = true;
|
|
820
|
+
}
|
|
821
|
+
if (hasChanges) {
|
|
822
|
+
this.stats.expired += expiredCount;
|
|
823
|
+
this.updateStats();
|
|
824
|
+
this.scheduleSave();
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
/** Wait for the cache to be ready (file loaded) */
|
|
828
|
+
async waitForReady() {
|
|
829
|
+
await this.loadPromise;
|
|
830
|
+
}
|
|
831
|
+
async close() {
|
|
832
|
+
if (this.saveTimer) {
|
|
833
|
+
clearTimeout(this.saveTimer);
|
|
834
|
+
await this.saveCache();
|
|
835
|
+
}
|
|
836
|
+
if (this.cleanupInterval) {
|
|
837
|
+
clearInterval(this.cleanupInterval);
|
|
838
|
+
this.cleanupInterval = void 0;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
};
|
|
842
|
+
|
|
843
|
+
//#endregion
|
|
844
|
+
//#region src/cache/service.ts
|
|
845
|
+
/**
|
|
846
|
+
* @file src/cache/service.ts
|
|
847
|
+
* Unified cache service with pluggable backends
|
|
848
|
+
*/
|
|
849
|
+
var CacheService = class {
|
|
850
|
+
backend;
|
|
851
|
+
defaultTtl;
|
|
852
|
+
constructor(config) {
|
|
853
|
+
this.defaultTtl = config.defaultTtl;
|
|
854
|
+
this.backend = this.createBackend(config);
|
|
855
|
+
}
|
|
856
|
+
createBackend(config) {
|
|
857
|
+
switch (config.backend) {
|
|
858
|
+
case "memory": return new MemoryCacheBackend(config.maxSize, config.cleanupInterval);
|
|
859
|
+
case "file": return new FileCacheBackend(config.dataDir, config.fileName, config.saveInterval, config.cleanupInterval);
|
|
860
|
+
default: throw new Error(`Unsupported cache backend: ${config.backend}`);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
/** Get a value from the cache */
|
|
864
|
+
async get(key, namespace) {
|
|
865
|
+
const entry = await this.backend.get(key, namespace);
|
|
866
|
+
return entry ? entry.value : null;
|
|
867
|
+
}
|
|
868
|
+
/** Get the full cache entry (with metadata) */
|
|
869
|
+
async getEntry(key, namespace) {
|
|
870
|
+
return this.backend.get(key, namespace);
|
|
871
|
+
}
|
|
872
|
+
/** Set a value in the cache */
|
|
873
|
+
async set(key, value, options = {}) {
|
|
874
|
+
const finalOptions = {
|
|
875
|
+
...options,
|
|
876
|
+
ttl: options.ttl ?? this.defaultTtl
|
|
877
|
+
};
|
|
878
|
+
await this.backend.set(key, value, finalOptions);
|
|
879
|
+
}
|
|
880
|
+
/** Set a value with TTL in seconds (convenience method) */
|
|
881
|
+
async setWithTtl(key, value, ttlSeconds, namespace) {
|
|
882
|
+
await this.set(key, value, {
|
|
883
|
+
ttl: ttlSeconds * 1e3,
|
|
884
|
+
namespace
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
/** Delete a value from the cache */
|
|
888
|
+
async delete(key, namespace) {
|
|
889
|
+
return this.backend.delete(key, namespace);
|
|
890
|
+
}
|
|
891
|
+
/** Check if a key exists in the cache */
|
|
892
|
+
async has(key, namespace) {
|
|
893
|
+
return this.backend.has(key, namespace);
|
|
894
|
+
}
|
|
895
|
+
/** Get all keys in a namespace */
|
|
896
|
+
async keys(namespace) {
|
|
897
|
+
return this.backend.keys(namespace);
|
|
898
|
+
}
|
|
899
|
+
/** Clear all entries in a namespace (or all entries if no namespace) */
|
|
900
|
+
async clear(namespace) {
|
|
901
|
+
await this.backend.clear(namespace);
|
|
902
|
+
}
|
|
903
|
+
/** Get cache statistics */
|
|
904
|
+
async getStats(namespace) {
|
|
905
|
+
return this.backend.getStats(namespace);
|
|
906
|
+
}
|
|
907
|
+
/** Manually trigger cleanup of expired entries */
|
|
908
|
+
async cleanup() {
|
|
909
|
+
await this.backend.cleanup();
|
|
910
|
+
}
|
|
911
|
+
/** Wait for the backend to be ready */
|
|
912
|
+
async waitForReady() {
|
|
913
|
+
if ("waitForReady" in this.backend) await this.backend.waitForReady();
|
|
914
|
+
}
|
|
915
|
+
/** Close the cache and cleanup resources */
|
|
916
|
+
async close() {
|
|
917
|
+
await this.backend.close();
|
|
918
|
+
}
|
|
919
|
+
/** Get or set pattern - get value, or compute and cache it if not found */
|
|
920
|
+
async getOrSet(key, factory, options = {}) {
|
|
921
|
+
const existing = await this.get(key, options.namespace);
|
|
922
|
+
if (existing !== null) return existing;
|
|
923
|
+
const value = await factory();
|
|
924
|
+
await this.set(key, value, options);
|
|
925
|
+
return value;
|
|
926
|
+
}
|
|
927
|
+
/** Increment a numeric value (simulated atomic operation) */
|
|
928
|
+
async increment(key, delta = 1, options = {}) {
|
|
929
|
+
const newValue = (await this.get(key, options.namespace) || 0) + delta;
|
|
930
|
+
await this.set(key, newValue, options);
|
|
931
|
+
return newValue;
|
|
932
|
+
}
|
|
933
|
+
/** Set multiple values at once */
|
|
934
|
+
async setMany(entries, defaultOptions = {}) {
|
|
935
|
+
const promises = entries.map(({ key, value, options }) => this.set(key, value, {
|
|
936
|
+
...defaultOptions,
|
|
937
|
+
...options
|
|
938
|
+
}));
|
|
939
|
+
await Promise.all(promises);
|
|
940
|
+
}
|
|
941
|
+
/** Get multiple values at once */
|
|
942
|
+
async getMany(keys, namespace) {
|
|
943
|
+
const promises = keys.map(async (key) => ({
|
|
944
|
+
key,
|
|
945
|
+
value: await this.get(key, namespace)
|
|
946
|
+
}));
|
|
947
|
+
return Promise.all(promises);
|
|
948
|
+
}
|
|
949
|
+
/** Get the underlying backend (for advanced use cases) */
|
|
950
|
+
getBackend() {
|
|
951
|
+
return this.backend;
|
|
952
|
+
}
|
|
953
|
+
};
|
|
954
|
+
|
|
467
955
|
//#endregion
|
|
468
956
|
//#region src/utils/id.ts
|
|
469
957
|
const generateId = (size) => {
|
|
@@ -792,7 +1280,11 @@ const createConfigVariantDataLayer = (db) => {
|
|
|
792
1280
|
"version"
|
|
793
1281
|
]).where("variantId", "=", configVariant.variantId).orderBy("version", "desc").limit(1).executeTakeFirst();
|
|
794
1282
|
if (!versionData) throw new LLMOpsError(`No variant version found for variant ${configVariant.variantId}`);
|
|
795
|
-
return
|
|
1283
|
+
return {
|
|
1284
|
+
...versionData,
|
|
1285
|
+
configId: resolvedConfigId,
|
|
1286
|
+
variantId: configVariant.variantId
|
|
1287
|
+
};
|
|
796
1288
|
}
|
|
797
1289
|
};
|
|
798
1290
|
};
|
|
@@ -952,6 +1444,287 @@ const createEnvironmentSecretDataLayer = (db) => {
|
|
|
952
1444
|
};
|
|
953
1445
|
};
|
|
954
1446
|
|
|
1447
|
+
//#endregion
|
|
1448
|
+
//#region src/datalayer/llmRequests.ts
|
|
1449
|
+
/**
|
|
1450
|
+
* Schema for inserting a new LLM request log
|
|
1451
|
+
*/
|
|
1452
|
+
const insertLLMRequestSchema = zod_default.object({
|
|
1453
|
+
requestId: zod_default.string().uuid(),
|
|
1454
|
+
configId: zod_default.string().uuid().nullable().optional(),
|
|
1455
|
+
variantId: zod_default.string().uuid().nullable().optional(),
|
|
1456
|
+
provider: zod_default.string(),
|
|
1457
|
+
model: zod_default.string(),
|
|
1458
|
+
promptTokens: zod_default.number().int().default(0),
|
|
1459
|
+
completionTokens: zod_default.number().int().default(0),
|
|
1460
|
+
totalTokens: zod_default.number().int().default(0),
|
|
1461
|
+
cachedTokens: zod_default.number().int().default(0),
|
|
1462
|
+
cost: zod_default.number().int().default(0),
|
|
1463
|
+
inputCost: zod_default.number().int().default(0),
|
|
1464
|
+
outputCost: zod_default.number().int().default(0),
|
|
1465
|
+
endpoint: zod_default.string(),
|
|
1466
|
+
statusCode: zod_default.number().int(),
|
|
1467
|
+
latencyMs: zod_default.number().int().default(0),
|
|
1468
|
+
isStreaming: zod_default.boolean().default(false),
|
|
1469
|
+
userId: zod_default.string().nullable().optional(),
|
|
1470
|
+
tags: zod_default.record(zod_default.string(), zod_default.string()).default({})
|
|
1471
|
+
});
|
|
1472
|
+
/**
|
|
1473
|
+
* Schema for listing LLM requests
|
|
1474
|
+
*/
|
|
1475
|
+
const listRequestsSchema = zod_default.object({
|
|
1476
|
+
limit: zod_default.number().int().positive().max(1e3).default(100),
|
|
1477
|
+
offset: zod_default.number().int().nonnegative().default(0),
|
|
1478
|
+
configId: zod_default.string().uuid().optional(),
|
|
1479
|
+
provider: zod_default.string().optional(),
|
|
1480
|
+
model: zod_default.string().optional(),
|
|
1481
|
+
startDate: zod_default.date().optional(),
|
|
1482
|
+
endDate: zod_default.date().optional()
|
|
1483
|
+
});
|
|
1484
|
+
/**
|
|
1485
|
+
* Schema for date range queries
|
|
1486
|
+
*/
|
|
1487
|
+
const dateRangeSchema = zod_default.object({
|
|
1488
|
+
startDate: zod_default.date(),
|
|
1489
|
+
endDate: zod_default.date()
|
|
1490
|
+
});
|
|
1491
|
+
/**
|
|
1492
|
+
* Schema for cost summary with grouping
|
|
1493
|
+
*/
|
|
1494
|
+
const costSummarySchema = zod_default.object({
|
|
1495
|
+
startDate: zod_default.date(),
|
|
1496
|
+
endDate: zod_default.date(),
|
|
1497
|
+
groupBy: zod_default.enum([
|
|
1498
|
+
"day",
|
|
1499
|
+
"hour",
|
|
1500
|
+
"model",
|
|
1501
|
+
"provider",
|
|
1502
|
+
"config"
|
|
1503
|
+
]).optional()
|
|
1504
|
+
});
|
|
1505
|
+
/**
|
|
1506
|
+
* Helper to create column reference for SQL
|
|
1507
|
+
* Uses sql.ref() to properly quote column names for the database
|
|
1508
|
+
*/
|
|
1509
|
+
const col = (name) => sql.ref(name);
|
|
1510
|
+
const tableCol = (table, name) => sql.ref(`${table}.${name}`);
|
|
1511
|
+
const createLLMRequestsDataLayer = (db) => {
|
|
1512
|
+
return {
|
|
1513
|
+
batchInsertRequests: async (requests) => {
|
|
1514
|
+
if (requests.length === 0) return { count: 0 };
|
|
1515
|
+
const validatedRequests = await Promise.all(requests.map(async (req) => {
|
|
1516
|
+
const result = await insertLLMRequestSchema.safeParseAsync(req);
|
|
1517
|
+
if (!result.success) throw new LLMOpsError(`Invalid request data: ${result.error.message}`);
|
|
1518
|
+
return result.data;
|
|
1519
|
+
}));
|
|
1520
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1521
|
+
const values = validatedRequests.map((req) => ({
|
|
1522
|
+
id: randomUUID(),
|
|
1523
|
+
requestId: req.requestId,
|
|
1524
|
+
configId: req.configId ?? null,
|
|
1525
|
+
variantId: req.variantId ?? null,
|
|
1526
|
+
provider: req.provider,
|
|
1527
|
+
model: req.model,
|
|
1528
|
+
promptTokens: req.promptTokens,
|
|
1529
|
+
completionTokens: req.completionTokens,
|
|
1530
|
+
totalTokens: req.totalTokens,
|
|
1531
|
+
cachedTokens: req.cachedTokens,
|
|
1532
|
+
cost: req.cost,
|
|
1533
|
+
inputCost: req.inputCost,
|
|
1534
|
+
outputCost: req.outputCost,
|
|
1535
|
+
endpoint: req.endpoint,
|
|
1536
|
+
statusCode: req.statusCode,
|
|
1537
|
+
latencyMs: req.latencyMs,
|
|
1538
|
+
isStreaming: req.isStreaming,
|
|
1539
|
+
userId: req.userId ?? null,
|
|
1540
|
+
tags: JSON.stringify(req.tags),
|
|
1541
|
+
createdAt: now,
|
|
1542
|
+
updatedAt: now
|
|
1543
|
+
}));
|
|
1544
|
+
await db.insertInto("llm_requests").values(values).execute();
|
|
1545
|
+
return { count: values.length };
|
|
1546
|
+
},
|
|
1547
|
+
insertRequest: async (request) => {
|
|
1548
|
+
const result = await insertLLMRequestSchema.safeParseAsync(request);
|
|
1549
|
+
if (!result.success) throw new LLMOpsError(`Invalid request data: ${result.error.message}`);
|
|
1550
|
+
const req = result.data;
|
|
1551
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1552
|
+
return db.insertInto("llm_requests").values({
|
|
1553
|
+
id: randomUUID(),
|
|
1554
|
+
requestId: req.requestId,
|
|
1555
|
+
configId: req.configId ?? null,
|
|
1556
|
+
variantId: req.variantId ?? null,
|
|
1557
|
+
provider: req.provider,
|
|
1558
|
+
model: req.model,
|
|
1559
|
+
promptTokens: req.promptTokens,
|
|
1560
|
+
completionTokens: req.completionTokens,
|
|
1561
|
+
totalTokens: req.totalTokens,
|
|
1562
|
+
cachedTokens: req.cachedTokens,
|
|
1563
|
+
cost: req.cost,
|
|
1564
|
+
inputCost: req.inputCost,
|
|
1565
|
+
outputCost: req.outputCost,
|
|
1566
|
+
endpoint: req.endpoint,
|
|
1567
|
+
statusCode: req.statusCode,
|
|
1568
|
+
latencyMs: req.latencyMs,
|
|
1569
|
+
isStreaming: req.isStreaming,
|
|
1570
|
+
userId: req.userId ?? null,
|
|
1571
|
+
tags: JSON.stringify(req.tags),
|
|
1572
|
+
createdAt: now,
|
|
1573
|
+
updatedAt: now
|
|
1574
|
+
}).returningAll().executeTakeFirst();
|
|
1575
|
+
},
|
|
1576
|
+
listRequests: async (params) => {
|
|
1577
|
+
const result = await listRequestsSchema.safeParseAsync(params || {});
|
|
1578
|
+
if (!result.success) throw new LLMOpsError(`Invalid parameters: ${result.error.message}`);
|
|
1579
|
+
const { limit, offset, configId, provider, model, startDate, endDate } = result.data;
|
|
1580
|
+
let baseQuery = db.selectFrom("llm_requests");
|
|
1581
|
+
if (configId) baseQuery = baseQuery.where("configId", "=", configId);
|
|
1582
|
+
if (provider) baseQuery = baseQuery.where("provider", "=", provider);
|
|
1583
|
+
if (model) baseQuery = baseQuery.where("model", "=", model);
|
|
1584
|
+
if (startDate) baseQuery = baseQuery.where(sql`${col("createdAt")} >= ${startDate.toISOString()}`);
|
|
1585
|
+
if (endDate) baseQuery = baseQuery.where(sql`${col("createdAt")} <= ${endDate.toISOString()}`);
|
|
1586
|
+
const countResult = await baseQuery.select(sql`COUNT(*)`.as("total")).executeTakeFirst();
|
|
1587
|
+
const total = Number(countResult?.total ?? 0);
|
|
1588
|
+
return {
|
|
1589
|
+
data: await baseQuery.selectAll().orderBy("createdAt", "desc").limit(limit).offset(offset).execute(),
|
|
1590
|
+
total,
|
|
1591
|
+
limit,
|
|
1592
|
+
offset
|
|
1593
|
+
};
|
|
1594
|
+
},
|
|
1595
|
+
getRequestByRequestId: async (requestId) => {
|
|
1596
|
+
return db.selectFrom("llm_requests").selectAll().where("requestId", "=", requestId).executeTakeFirst();
|
|
1597
|
+
},
|
|
1598
|
+
getTotalCost: async (params) => {
|
|
1599
|
+
const result = await dateRangeSchema.safeParseAsync(params);
|
|
1600
|
+
if (!result.success) throw new LLMOpsError(`Invalid parameters: ${result.error.message}`);
|
|
1601
|
+
const { startDate, endDate } = result.data;
|
|
1602
|
+
return await db.selectFrom("llm_requests").select([
|
|
1603
|
+
sql`COALESCE(SUM(${col("cost")}), 0)`.as("totalCost"),
|
|
1604
|
+
sql`COALESCE(SUM(${col("inputCost")}), 0)`.as("totalInputCost"),
|
|
1605
|
+
sql`COALESCE(SUM(${col("outputCost")}), 0)`.as("totalOutputCost"),
|
|
1606
|
+
sql`COALESCE(SUM(${col("promptTokens")}), 0)`.as("totalPromptTokens"),
|
|
1607
|
+
sql`COALESCE(SUM(${col("completionTokens")}), 0)`.as("totalCompletionTokens"),
|
|
1608
|
+
sql`COALESCE(SUM(${col("totalTokens")}), 0)`.as("totalTokens"),
|
|
1609
|
+
sql`COUNT(*)`.as("requestCount")
|
|
1610
|
+
]).where(sql`${col("createdAt")} >= ${startDate.toISOString()}`).where(sql`${col("createdAt")} <= ${endDate.toISOString()}`).executeTakeFirst();
|
|
1611
|
+
},
|
|
1612
|
+
getCostByModel: async (params) => {
|
|
1613
|
+
const result = await dateRangeSchema.safeParseAsync(params);
|
|
1614
|
+
if (!result.success) throw new LLMOpsError(`Invalid parameters: ${result.error.message}`);
|
|
1615
|
+
const { startDate, endDate } = result.data;
|
|
1616
|
+
return db.selectFrom("llm_requests").select([
|
|
1617
|
+
"provider",
|
|
1618
|
+
"model",
|
|
1619
|
+
sql`COALESCE(SUM(${col("cost")}), 0)`.as("totalCost"),
|
|
1620
|
+
sql`COALESCE(SUM(${col("inputCost")}), 0)`.as("totalInputCost"),
|
|
1621
|
+
sql`COALESCE(SUM(${col("outputCost")}), 0)`.as("totalOutputCost"),
|
|
1622
|
+
sql`COALESCE(SUM(${col("totalTokens")}), 0)`.as("totalTokens"),
|
|
1623
|
+
sql`COUNT(*)`.as("requestCount"),
|
|
1624
|
+
sql`AVG(${col("latencyMs")})`.as("avgLatencyMs")
|
|
1625
|
+
]).where(sql`${col("createdAt")} >= ${startDate.toISOString()}`).where(sql`${col("createdAt")} <= ${endDate.toISOString()}`).groupBy(["provider", "model"]).orderBy(sql`SUM(${col("cost")})`, "desc").execute();
|
|
1626
|
+
},
|
|
1627
|
+
getCostByProvider: async (params) => {
|
|
1628
|
+
const result = await dateRangeSchema.safeParseAsync(params);
|
|
1629
|
+
if (!result.success) throw new LLMOpsError(`Invalid parameters: ${result.error.message}`);
|
|
1630
|
+
const { startDate, endDate } = result.data;
|
|
1631
|
+
return db.selectFrom("llm_requests").select([
|
|
1632
|
+
"provider",
|
|
1633
|
+
sql`COALESCE(SUM(${col("cost")}), 0)`.as("totalCost"),
|
|
1634
|
+
sql`COALESCE(SUM(${col("inputCost")}), 0)`.as("totalInputCost"),
|
|
1635
|
+
sql`COALESCE(SUM(${col("outputCost")}), 0)`.as("totalOutputCost"),
|
|
1636
|
+
sql`COALESCE(SUM(${col("totalTokens")}), 0)`.as("totalTokens"),
|
|
1637
|
+
sql`COUNT(*)`.as("requestCount"),
|
|
1638
|
+
sql`AVG(${col("latencyMs")})`.as("avgLatencyMs")
|
|
1639
|
+
]).where(sql`${col("createdAt")} >= ${startDate.toISOString()}`).where(sql`${col("createdAt")} <= ${endDate.toISOString()}`).groupBy("provider").orderBy(sql`SUM(${col("cost")})`, "desc").execute();
|
|
1640
|
+
},
|
|
1641
|
+
getCostByConfig: async (params) => {
|
|
1642
|
+
const result = await dateRangeSchema.safeParseAsync(params);
|
|
1643
|
+
if (!result.success) throw new LLMOpsError(`Invalid parameters: ${result.error.message}`);
|
|
1644
|
+
const { startDate, endDate } = result.data;
|
|
1645
|
+
return db.selectFrom("llm_requests").leftJoin("configs", "llm_requests.configId", "configs.id").select([
|
|
1646
|
+
"llm_requests.configId",
|
|
1647
|
+
"configs.name as configName",
|
|
1648
|
+
"configs.slug as configSlug",
|
|
1649
|
+
sql`COALESCE(SUM(${tableCol("llm_requests", "cost")}), 0)`.as("totalCost"),
|
|
1650
|
+
sql`COALESCE(SUM(${tableCol("llm_requests", "inputCost")}), 0)`.as("totalInputCost"),
|
|
1651
|
+
sql`COALESCE(SUM(${tableCol("llm_requests", "outputCost")}), 0)`.as("totalOutputCost"),
|
|
1652
|
+
sql`COALESCE(SUM(${tableCol("llm_requests", "totalTokens")}), 0)`.as("totalTokens"),
|
|
1653
|
+
sql`COUNT(*)`.as("requestCount")
|
|
1654
|
+
]).where(sql`${tableCol("llm_requests", "createdAt")} >= ${startDate.toISOString()}`).where(sql`${tableCol("llm_requests", "createdAt")} <= ${endDate.toISOString()}`).groupBy([
|
|
1655
|
+
"llm_requests.configId",
|
|
1656
|
+
"configs.name",
|
|
1657
|
+
"configs.slug"
|
|
1658
|
+
]).orderBy(sql`SUM(${tableCol("llm_requests", "cost")})`, "desc").execute();
|
|
1659
|
+
},
|
|
1660
|
+
getDailyCosts: async (params) => {
|
|
1661
|
+
const result = await dateRangeSchema.safeParseAsync(params);
|
|
1662
|
+
if (!result.success) throw new LLMOpsError(`Invalid parameters: ${result.error.message}`);
|
|
1663
|
+
const { startDate, endDate } = result.data;
|
|
1664
|
+
return db.selectFrom("llm_requests").select([
|
|
1665
|
+
sql`DATE(${col("createdAt")})`.as("date"),
|
|
1666
|
+
sql`COALESCE(SUM(${col("cost")}), 0)`.as("totalCost"),
|
|
1667
|
+
sql`COALESCE(SUM(${col("inputCost")}), 0)`.as("totalInputCost"),
|
|
1668
|
+
sql`COALESCE(SUM(${col("outputCost")}), 0)`.as("totalOutputCost"),
|
|
1669
|
+
sql`COALESCE(SUM(${col("totalTokens")}), 0)`.as("totalTokens"),
|
|
1670
|
+
sql`COUNT(*)`.as("requestCount")
|
|
1671
|
+
]).where(sql`${col("createdAt")} >= ${startDate.toISOString()}`).where(sql`${col("createdAt")} <= ${endDate.toISOString()}`).groupBy(sql`DATE(${col("createdAt")})`).orderBy(sql`DATE(${col("createdAt")})`, "asc").execute();
|
|
1672
|
+
},
|
|
1673
|
+
getCostSummary: async (params) => {
|
|
1674
|
+
const result = await costSummarySchema.safeParseAsync(params);
|
|
1675
|
+
if (!result.success) throw new LLMOpsError(`Invalid parameters: ${result.error.message}`);
|
|
1676
|
+
const { startDate, endDate, groupBy } = result.data;
|
|
1677
|
+
const baseQuery = db.selectFrom("llm_requests").where(sql`${col("createdAt")} >= ${startDate.toISOString()}`).where(sql`${col("createdAt")} <= ${endDate.toISOString()}`);
|
|
1678
|
+
switch (groupBy) {
|
|
1679
|
+
case "day": return baseQuery.select([
|
|
1680
|
+
sql`DATE(${col("createdAt")})`.as("groupKey"),
|
|
1681
|
+
sql`COALESCE(SUM(${col("cost")}), 0)`.as("totalCost"),
|
|
1682
|
+
sql`COUNT(*)`.as("requestCount")
|
|
1683
|
+
]).groupBy(sql`DATE(${col("createdAt")})`).orderBy(sql`DATE(${col("createdAt")})`, "asc").execute();
|
|
1684
|
+
case "hour": return baseQuery.select([
|
|
1685
|
+
sql`DATE_TRUNC('hour', ${col("createdAt")})`.as("groupKey"),
|
|
1686
|
+
sql`COALESCE(SUM(${col("cost")}), 0)`.as("totalCost"),
|
|
1687
|
+
sql`COUNT(*)`.as("requestCount")
|
|
1688
|
+
]).groupBy(sql`DATE_TRUNC('hour', ${col("createdAt")})`).orderBy(sql`DATE_TRUNC('hour', ${col("createdAt")})`, "asc").execute();
|
|
1689
|
+
case "model": return baseQuery.select([
|
|
1690
|
+
sql`${col("provider")} || '/' || ${col("model")}`.as("groupKey"),
|
|
1691
|
+
sql`COALESCE(SUM(${col("cost")}), 0)`.as("totalCost"),
|
|
1692
|
+
sql`COUNT(*)`.as("requestCount")
|
|
1693
|
+
]).groupBy(["provider", "model"]).orderBy(sql`SUM(${col("cost")})`, "desc").execute();
|
|
1694
|
+
case "provider": return baseQuery.select([
|
|
1695
|
+
sql`${col("provider")}`.as("groupKey"),
|
|
1696
|
+
sql`COALESCE(SUM(${col("cost")}), 0)`.as("totalCost"),
|
|
1697
|
+
sql`COUNT(*)`.as("requestCount")
|
|
1698
|
+
]).groupBy("provider").orderBy(sql`SUM(${col("cost")})`, "desc").execute();
|
|
1699
|
+
case "config": return baseQuery.select([
|
|
1700
|
+
sql`COALESCE(${col("configId")}::text, 'no-config')`.as("groupKey"),
|
|
1701
|
+
sql`COALESCE(SUM(${col("cost")}), 0)`.as("totalCost"),
|
|
1702
|
+
sql`COUNT(*)`.as("requestCount")
|
|
1703
|
+
]).groupBy("configId").orderBy(sql`SUM(${col("cost")})`, "desc").execute();
|
|
1704
|
+
default: return baseQuery.select([
|
|
1705
|
+
sql`'total'`.as("groupKey"),
|
|
1706
|
+
sql`COALESCE(SUM(${col("cost")}), 0)`.as("totalCost"),
|
|
1707
|
+
sql`COUNT(*)`.as("requestCount")
|
|
1708
|
+
]).execute();
|
|
1709
|
+
}
|
|
1710
|
+
},
|
|
1711
|
+
getRequestStats: async (params) => {
|
|
1712
|
+
const result = await dateRangeSchema.safeParseAsync(params);
|
|
1713
|
+
if (!result.success) throw new LLMOpsError(`Invalid parameters: ${result.error.message}`);
|
|
1714
|
+
const { startDate, endDate } = result.data;
|
|
1715
|
+
return await db.selectFrom("llm_requests").select([
|
|
1716
|
+
sql`COUNT(*)`.as("totalRequests"),
|
|
1717
|
+
sql`COUNT(CASE WHEN ${col("statusCode")} >= 200 AND ${col("statusCode")} < 300 THEN 1 END)`.as("successfulRequests"),
|
|
1718
|
+
sql`COUNT(CASE WHEN ${col("statusCode")} >= 400 THEN 1 END)`.as("failedRequests"),
|
|
1719
|
+
sql`COUNT(CASE WHEN ${col("isStreaming")} = true THEN 1 END)`.as("streamingRequests"),
|
|
1720
|
+
sql`AVG(${col("latencyMs")})`.as("avgLatencyMs"),
|
|
1721
|
+
sql`MAX(${col("latencyMs")})`.as("maxLatencyMs"),
|
|
1722
|
+
sql`MIN(${col("latencyMs")})`.as("minLatencyMs")
|
|
1723
|
+
]).where(sql`${col("createdAt")} >= ${startDate.toISOString()}`).where(sql`${col("createdAt")} <= ${endDate.toISOString()}`).executeTakeFirst();
|
|
1724
|
+
}
|
|
1725
|
+
};
|
|
1726
|
+
};
|
|
1727
|
+
|
|
955
1728
|
//#endregion
|
|
956
1729
|
//#region src/datalayer/targetingRules.ts
|
|
957
1730
|
const createTargetingRule = zod_default.object({
|
|
@@ -1378,6 +2151,7 @@ const createDataLayer = async (db) => {
|
|
|
1378
2151
|
...createConfigVariantDataLayer(db),
|
|
1379
2152
|
...createEnvironmentDataLayer(db),
|
|
1380
2153
|
...createEnvironmentSecretDataLayer(db),
|
|
2154
|
+
...createLLMRequestsDataLayer(db),
|
|
1381
2155
|
...createTargetingRulesDataLayer(db),
|
|
1382
2156
|
...createVariantDataLayer(db),
|
|
1383
2157
|
...createVariantVersionsDataLayer(db),
|
|
@@ -1386,4 +2160,207 @@ const createDataLayer = async (db) => {
|
|
|
1386
2160
|
};
|
|
1387
2161
|
|
|
1388
2162
|
//#endregion
|
|
1389
|
-
|
|
2163
|
+
//#region src/pricing/calculator.ts
|
|
2164
|
+
/**
|
|
2165
|
+
* Calculate the cost of an LLM request in micro-dollars
|
|
2166
|
+
*
|
|
2167
|
+
* Micro-dollars are used to avoid floating-point precision issues:
|
|
2168
|
+
* - 1 dollar = 1,000,000 micro-dollars
|
|
2169
|
+
* - $0.001 = 1,000 micro-dollars
|
|
2170
|
+
* - $0.000001 = 1 micro-dollar
|
|
2171
|
+
*
|
|
2172
|
+
* @param usage - Token usage data from the LLM response
|
|
2173
|
+
* @param pricing - Model pricing information
|
|
2174
|
+
* @returns Cost breakdown in micro-dollars
|
|
2175
|
+
*
|
|
2176
|
+
* @example
|
|
2177
|
+
* ```typescript
|
|
2178
|
+
* const usage = { promptTokens: 1000, completionTokens: 500 };
|
|
2179
|
+
* const pricing = { inputCostPer1M: 2.5, outputCostPer1M: 10.0 };
|
|
2180
|
+
* const cost = calculateCost(usage, pricing);
|
|
2181
|
+
* // cost = { inputCost: 2500, outputCost: 5000, totalCost: 7500 }
|
|
2182
|
+
* // In dollars: $0.0025 input + $0.005 output = $0.0075 total
|
|
2183
|
+
* ```
|
|
2184
|
+
*/
|
|
2185
|
+
function calculateCost(usage, pricing) {
|
|
2186
|
+
const inputCost = Math.round(usage.promptTokens * pricing.inputCostPer1M);
|
|
2187
|
+
const outputCost = Math.round(usage.completionTokens * pricing.outputCostPer1M);
|
|
2188
|
+
return {
|
|
2189
|
+
inputCost,
|
|
2190
|
+
outputCost,
|
|
2191
|
+
totalCost: inputCost + outputCost
|
|
2192
|
+
};
|
|
2193
|
+
}
|
|
2194
|
+
/**
|
|
2195
|
+
* Convert micro-dollars to dollars
|
|
2196
|
+
*
|
|
2197
|
+
* @param microDollars - Amount in micro-dollars
|
|
2198
|
+
* @returns Amount in dollars
|
|
2199
|
+
*
|
|
2200
|
+
* @example
|
|
2201
|
+
* ```typescript
|
|
2202
|
+
* microDollarsToDollars(7500); // 0.0075
|
|
2203
|
+
* microDollarsToDollars(1000000); // 1.0
|
|
2204
|
+
* ```
|
|
2205
|
+
*/
|
|
2206
|
+
function microDollarsToDollars(microDollars) {
|
|
2207
|
+
return microDollars / 1e6;
|
|
2208
|
+
}
|
|
2209
|
+
/**
|
|
2210
|
+
* Convert dollars to micro-dollars
|
|
2211
|
+
*
|
|
2212
|
+
* @param dollars - Amount in dollars
|
|
2213
|
+
* @returns Amount in micro-dollars (rounded to nearest integer)
|
|
2214
|
+
*
|
|
2215
|
+
* @example
|
|
2216
|
+
* ```typescript
|
|
2217
|
+
* dollarsToMicroDollars(0.0075); // 7500
|
|
2218
|
+
* dollarsToMicroDollars(1.0); // 1000000
|
|
2219
|
+
* ```
|
|
2220
|
+
*/
|
|
2221
|
+
function dollarsToMicroDollars(dollars) {
|
|
2222
|
+
return Math.round(dollars * 1e6);
|
|
2223
|
+
}
|
|
2224
|
+
/**
|
|
2225
|
+
* Format micro-dollars as a human-readable dollar string
|
|
2226
|
+
*
|
|
2227
|
+
* @param microDollars - Amount in micro-dollars
|
|
2228
|
+
* @param decimals - Number of decimal places (default: 6)
|
|
2229
|
+
* @returns Formatted dollar string
|
|
2230
|
+
*
|
|
2231
|
+
* @example
|
|
2232
|
+
* ```typescript
|
|
2233
|
+
* formatCost(7500); // "$0.007500"
|
|
2234
|
+
* formatCost(1234567, 2); // "$1.23"
|
|
2235
|
+
* ```
|
|
2236
|
+
*/
|
|
2237
|
+
function formatCost(microDollars, decimals = 6) {
|
|
2238
|
+
return `$${microDollarsToDollars(microDollars).toFixed(decimals)}`;
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
//#endregion
|
|
2242
|
+
//#region src/pricing/provider.ts
|
|
2243
|
+
const MODELS_DEV_API = "https://models.dev/api.json";
|
|
2244
|
+
/**
|
|
2245
|
+
* Pricing provider that fetches data from models.dev API
|
|
2246
|
+
*
|
|
2247
|
+
* Features:
|
|
2248
|
+
* - Caches pricing data with configurable TTL (default 5 minutes)
|
|
2249
|
+
* - Supports fallback to local cache on fetch failure
|
|
2250
|
+
* - Thread-safe cache refresh
|
|
2251
|
+
*/
|
|
2252
|
+
var ModelsDevPricingProvider = class {
|
|
2253
|
+
cache = /* @__PURE__ */ new Map();
|
|
2254
|
+
lastFetch = 0;
|
|
2255
|
+
cacheTTL;
|
|
2256
|
+
fetchPromise = null;
|
|
2257
|
+
ready = false;
|
|
2258
|
+
/**
|
|
2259
|
+
* Create a new ModelsDevPricingProvider
|
|
2260
|
+
*
|
|
2261
|
+
* @param cacheTTL - Cache TTL in milliseconds (default: 5 minutes)
|
|
2262
|
+
*/
|
|
2263
|
+
constructor(cacheTTL = 300 * 1e3) {
|
|
2264
|
+
this.cacheTTL = cacheTTL;
|
|
2265
|
+
}
|
|
2266
|
+
/**
|
|
2267
|
+
* Generate a cache key for a provider/model combination
|
|
2268
|
+
*/
|
|
2269
|
+
getCacheKey(provider, model) {
|
|
2270
|
+
return `${provider.toLowerCase()}:${model.toLowerCase()}`;
|
|
2271
|
+
}
|
|
2272
|
+
/**
|
|
2273
|
+
* Fetch pricing data from models.dev API
|
|
2274
|
+
*/
|
|
2275
|
+
async fetchPricingData() {
|
|
2276
|
+
try {
|
|
2277
|
+
logger.debug("[Pricing] Fetching pricing data from models.dev");
|
|
2278
|
+
const response = await fetch(MODELS_DEV_API);
|
|
2279
|
+
if (!response.ok) throw new Error(`Failed to fetch models.dev API: ${response.status}`);
|
|
2280
|
+
const data = await response.json();
|
|
2281
|
+
this.cache.clear();
|
|
2282
|
+
for (const [providerId, provider] of Object.entries(data)) {
|
|
2283
|
+
if (!provider.models) continue;
|
|
2284
|
+
for (const [_modelId, model] of Object.entries(provider.models)) {
|
|
2285
|
+
if (!model.cost) continue;
|
|
2286
|
+
const cacheKey = this.getCacheKey(providerId, model.id);
|
|
2287
|
+
this.cache.set(cacheKey, {
|
|
2288
|
+
inputCostPer1M: model.cost.input ?? 0,
|
|
2289
|
+
outputCostPer1M: model.cost.output ?? 0,
|
|
2290
|
+
cacheReadCostPer1M: model.cost.cache_read,
|
|
2291
|
+
cacheWriteCostPer1M: model.cost.cache_write,
|
|
2292
|
+
reasoningCostPer1M: model.cost.reasoning
|
|
2293
|
+
});
|
|
2294
|
+
const nameKey = this.getCacheKey(providerId, model.name);
|
|
2295
|
+
if (nameKey !== cacheKey) this.cache.set(nameKey, this.cache.get(cacheKey));
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
this.lastFetch = Date.now();
|
|
2299
|
+
this.ready = true;
|
|
2300
|
+
logger.debug(`[Pricing] Cached pricing for ${this.cache.size} models from models.dev`);
|
|
2301
|
+
} catch (error) {
|
|
2302
|
+
logger.error(`[Pricing] Failed to fetch pricing data: ${error instanceof Error ? error.message : String(error)}`);
|
|
2303
|
+
if (this.cache.size === 0) throw error;
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
/**
|
|
2307
|
+
* Ensure cache is fresh, fetching if necessary
|
|
2308
|
+
*/
|
|
2309
|
+
async ensureFreshCache() {
|
|
2310
|
+
if (!(Date.now() - this.lastFetch > this.cacheTTL) && this.cache.size > 0) return;
|
|
2311
|
+
if (!this.fetchPromise) this.fetchPromise = this.fetchPricingData().finally(() => {
|
|
2312
|
+
this.fetchPromise = null;
|
|
2313
|
+
});
|
|
2314
|
+
await this.fetchPromise;
|
|
2315
|
+
}
|
|
2316
|
+
/**
|
|
2317
|
+
* Get pricing for a specific model
|
|
2318
|
+
*/
|
|
2319
|
+
async getModelPricing(provider, model) {
|
|
2320
|
+
await this.ensureFreshCache();
|
|
2321
|
+
const cacheKey = this.getCacheKey(provider, model);
|
|
2322
|
+
const pricing = this.cache.get(cacheKey);
|
|
2323
|
+
if (!pricing) {
|
|
2324
|
+
logger.debug(`[Pricing] No pricing found for ${provider}/${model}, trying partial match`);
|
|
2325
|
+
for (const [key, value] of this.cache.entries()) if (key.startsWith(`${provider.toLowerCase()}:`)) {
|
|
2326
|
+
const modelPart = key.split(":")[1];
|
|
2327
|
+
if (model.toLowerCase().includes(modelPart)) {
|
|
2328
|
+
logger.debug(`[Pricing] Found partial match: ${key}`);
|
|
2329
|
+
return value;
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
return null;
|
|
2333
|
+
}
|
|
2334
|
+
return pricing;
|
|
2335
|
+
}
|
|
2336
|
+
/**
|
|
2337
|
+
* Force refresh the pricing cache
|
|
2338
|
+
*/
|
|
2339
|
+
async refreshCache() {
|
|
2340
|
+
this.lastFetch = 0;
|
|
2341
|
+
await this.ensureFreshCache();
|
|
2342
|
+
}
|
|
2343
|
+
/**
|
|
2344
|
+
* Check if the provider is ready
|
|
2345
|
+
*/
|
|
2346
|
+
isReady() {
|
|
2347
|
+
return this.ready;
|
|
2348
|
+
}
|
|
2349
|
+
/**
|
|
2350
|
+
* Get the number of cached models (for debugging)
|
|
2351
|
+
*/
|
|
2352
|
+
getCacheSize() {
|
|
2353
|
+
return this.cache.size;
|
|
2354
|
+
}
|
|
2355
|
+
};
|
|
2356
|
+
let defaultProvider = null;
|
|
2357
|
+
/**
|
|
2358
|
+
* Get the default pricing provider instance
|
|
2359
|
+
*/
|
|
2360
|
+
function getDefaultPricingProvider() {
|
|
2361
|
+
if (!defaultProvider) defaultProvider = new ModelsDevPricingProvider();
|
|
2362
|
+
return defaultProvider;
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
//#endregion
|
|
2366
|
+
export { CacheService, FileCacheBackend, MS, MemoryCacheBackend, ModelsDevPricingProvider, SCHEMA_METADATA, SupportedProviders, calculateCost, chatCompletionCreateParamsBaseSchema, configVariantsSchema, configsSchema, createDataLayer, createDatabase, createDatabaseFromConnection, createLLMRequestsDataLayer, detectDatabaseType, dollarsToMicroDollars, environmentSecretsSchema, environmentsSchema, formatCost, gateway, generateId, getDefaultPricingProvider, getMigrations, llmRequestsSchema, llmopsConfigSchema, logger, matchType, microDollarsToDollars, parsePartialTableData, parseTableData, runAutoMigrations, schemas, targetingRulesSchema, validateLLMOpsConfig, validatePartialTableData, validateTableData, variantJsonDataSchema, variantVersionsSchema, variantsSchema, workspaceSettingsSchema };
|