@llmops/core 0.2.14 → 0.3.0-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/index.mjs CHANGED
@@ -6,6 +6,7 @@ import * as fs from "node:fs/promises";
6
6
  import * as path from "node:path";
7
7
  import { createRandomStringGenerator } from "@better-auth/utils/random";
8
8
  import { randomBytes, randomUUID } from "node:crypto";
9
+ import jsonLogic from "json-logic-js";
9
10
 
10
11
  //#region src/providers/supported-providers.ts
11
12
  /**
@@ -837,7 +838,10 @@ var CacheService = class {
837
838
  }
838
839
  createBackend(config) {
839
840
  switch (config.backend) {
840
- case "memory": return new MemoryCacheBackend(config.maxSize, config.cleanupInterval);
841
+ case "memory": return new MemoryCacheBackend({
842
+ maxSize: config.maxSize,
843
+ cleanupIntervalMs: config.cleanupInterval
844
+ });
841
845
  case "file": return new FileCacheBackend(config.dataDir, config.fileName, config.saveInterval, config.cleanupInterval);
842
846
  default: throw new Error(`Unsupported cache backend: ${config.backend}`);
843
847
  }
@@ -1226,9 +1230,9 @@ const createConfigVariantDataLayer = (db) => {
1226
1230
  const value = await getVariantJsonDataForConfig.safeParseAsync(params);
1227
1231
  if (!value.success) throw new LLMOpsError(`Invalid parameters: ${value.error.message}`);
1228
1232
  const { configId: configIdOrSlug, envSecret } = value.data;
1229
- const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1233
+ const UUID_REGEX$1 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1230
1234
  let resolvedConfigId;
1231
- if (UUID_REGEX.test(configIdOrSlug)) resolvedConfigId = configIdOrSlug;
1235
+ if (UUID_REGEX$1.test(configIdOrSlug)) resolvedConfigId = configIdOrSlug;
1232
1236
  else {
1233
1237
  const config = await db.selectFrom("configs").select("id").where("slug", "=", configIdOrSlug).executeTakeFirst();
1234
1238
  if (!config) throw new LLMOpsError(`Config not found: ${configIdOrSlug}`);
@@ -1264,7 +1268,7 @@ const createConfigVariantDataLayer = (db) => {
1264
1268
  if (!versionData) throw new LLMOpsError(`No variant version found for variant ${configVariant.variantId}`);
1265
1269
  let finalProvider = versionData.provider;
1266
1270
  let providerConfigId = null;
1267
- if (UUID_REGEX.test(versionData.provider)) {
1271
+ if (UUID_REGEX$1.test(versionData.provider)) {
1268
1272
  const providerConfig = await db.selectFrom("provider_configs").select(["id", "providerId"]).where("id", "=", versionData.provider).executeTakeFirst();
1269
1273
  if (providerConfig) {
1270
1274
  finalProvider = providerConfig.providerId;
@@ -1828,6 +1832,7 @@ const updateProviderConfig = zod_default.object({
1828
1832
  });
1829
1833
  const getProviderConfigById = zod_default.object({ id: zod_default.uuidv4() });
1830
1834
  const getProviderConfigByProviderId = zod_default.object({ providerId: zod_default.string().min(1) });
1835
+ const getProviderConfigBySlug = zod_default.object({ slug: zod_default.string().min(1) });
1831
1836
  const deleteProviderConfig = zod_default.object({ id: zod_default.uuidv4() });
1832
1837
  const listProviderConfigs = zod_default.object({
1833
1838
  limit: zod_default.number().int().positive().optional(),
@@ -1874,6 +1879,12 @@ const createProviderConfigsDataLayer = (db) => {
1874
1879
  const { providerId } = value.data;
1875
1880
  return db.selectFrom("provider_configs").selectAll().where("providerId", "=", providerId).executeTakeFirst();
1876
1881
  },
1882
+ getProviderConfigBySlug: async (params) => {
1883
+ const value = await getProviderConfigBySlug.safeParseAsync(params);
1884
+ if (!value.success) throw new LLMOpsError(`Invalid parameters: ${value.error.message}`);
1885
+ const { slug } = value.data;
1886
+ return db.selectFrom("provider_configs").selectAll().where("slug", "=", slug).executeTakeFirst();
1887
+ },
1877
1888
  deleteProviderConfig: async (params) => {
1878
1889
  const value = await deleteProviderConfig.safeParseAsync(params);
1879
1890
  if (!value.success) throw new LLMOpsError(`Invalid parameters: ${value.error.message}`);
@@ -2606,4 +2617,332 @@ function getDefaultPricingProvider() {
2606
2617
  }
2607
2618
 
2608
2619
  //#endregion
2609
- export { CacheService, FileCacheBackend, MS, MemoryCacheBackend, ModelsDevPricingProvider, SCHEMA_METADATA, SupportedProviders, calculateCost, chatCompletionCreateParamsBaseSchema, configVariantsSchema, configsSchema, createDataLayer, createDatabase, createDatabaseFromConnection, createLLMRequestsDataLayer, createNeonDialect, createProviderConfigsDataLayer, createWorkspaceSettingsDataLayer, detectDatabaseType, dollarsToMicroDollars, environmentSecretsSchema, environmentsSchema, executeWithSchema, formatCost, gateway, generateId, getAuthClientOptions, getDefaultPricingProvider, getMigrations, llmRequestsSchema, llmopsConfigSchema, logger, matchType, microDollarsToDollars, parsePartialTableData, parseTableData, providerConfigsSchema, runAutoMigrations, schemas, targetingRulesSchema, validateLLMOpsConfig, validatePartialTableData, validateTableData, variantJsonDataSchema, variantVersionsSchema, variantsSchema, workspaceSettingsSchema };
2620
+ //#region src/manifest/builder.ts
2621
+ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2622
+ /**
2623
+ * Builds the gateway routing manifest from database
2624
+ */
2625
+ var ManifestBuilder = class {
2626
+ constructor(db) {
2627
+ this.db = db;
2628
+ }
2629
+ /**
2630
+ * Build the complete routing manifest from database
2631
+ */
2632
+ async build() {
2633
+ const [configs, environments, environmentSecrets, targetingRules, configVariants, variantVersions, providerConfigs] = await Promise.all([
2634
+ this.db.selectFrom("configs").selectAll().execute(),
2635
+ this.db.selectFrom("environments").selectAll().execute(),
2636
+ this.db.selectFrom("environment_secrets").selectAll().execute(),
2637
+ this.db.selectFrom("targeting_rules").where("enabled", "=", true).selectAll().execute(),
2638
+ this.db.selectFrom("config_variants").selectAll().execute(),
2639
+ this.db.selectFrom("variant_versions").selectAll().execute(),
2640
+ this.db.selectFrom("provider_configs").selectAll().execute()
2641
+ ]);
2642
+ const manifestConfigs = {};
2643
+ const configsBySlug = {};
2644
+ for (const config of configs) {
2645
+ manifestConfigs[config.id] = {
2646
+ id: config.id,
2647
+ slug: config.slug,
2648
+ name: config.name ?? null
2649
+ };
2650
+ configsBySlug[config.slug] = config.id;
2651
+ }
2652
+ const manifestEnvironments = {};
2653
+ const environmentsBySlug = {};
2654
+ for (const env of environments) {
2655
+ manifestEnvironments[env.id] = {
2656
+ id: env.id,
2657
+ slug: env.slug,
2658
+ name: env.name,
2659
+ isProd: env.isProd
2660
+ };
2661
+ environmentsBySlug[env.slug] = env.id;
2662
+ }
2663
+ const secretToEnvironment = {};
2664
+ for (const secret of environmentSecrets) secretToEnvironment[secret.keyValue] = secret.environmentId;
2665
+ const configVariantMap = new Map(configVariants.map((cv) => [cv.id, cv]));
2666
+ const providerConfigMap = new Map(providerConfigs.map((pc) => [pc.id, pc]));
2667
+ const versionsByVariant = /* @__PURE__ */ new Map();
2668
+ for (const vv of variantVersions) {
2669
+ const list = versionsByVariant.get(vv.variantId) || [];
2670
+ list.push(vv);
2671
+ versionsByVariant.set(vv.variantId, list);
2672
+ }
2673
+ for (const list of versionsByVariant.values()) list.sort((a, b) => b.version - a.version);
2674
+ const versionById = new Map(variantVersions.map((vv) => [vv.id, vv]));
2675
+ const resolveProvider = (provider) => {
2676
+ if (UUID_REGEX.test(provider)) {
2677
+ const pc = providerConfigMap.get(provider);
2678
+ if (pc) return {
2679
+ providerId: pc.providerId,
2680
+ providerConfigId: pc.id
2681
+ };
2682
+ }
2683
+ return {
2684
+ providerId: provider,
2685
+ providerConfigId: null
2686
+ };
2687
+ };
2688
+ const buildVersion = (vv) => {
2689
+ const { providerId, providerConfigId } = resolveProvider(vv.provider);
2690
+ const jsonData = typeof vv.jsonData === "string" ? JSON.parse(vv.jsonData) : vv.jsonData;
2691
+ return {
2692
+ id: vv.id,
2693
+ variantId: vv.variantId,
2694
+ version: vv.version,
2695
+ provider: providerId,
2696
+ providerConfigId,
2697
+ modelName: vv.modelName,
2698
+ jsonData
2699
+ };
2700
+ };
2701
+ const routingTable = {};
2702
+ for (const rule of targetingRules) {
2703
+ const configVariant = configVariantMap.get(rule.configVariantId);
2704
+ if (!configVariant) continue;
2705
+ const variantId = configVariant.variantId;
2706
+ let resolvedVersionData;
2707
+ if (rule.variantVersionId) resolvedVersionData = versionById.get(rule.variantVersionId);
2708
+ else resolvedVersionData = versionsByVariant.get(variantId)?.[0];
2709
+ if (!resolvedVersionData) continue;
2710
+ let conditions = null;
2711
+ if (rule.conditions) {
2712
+ const conditionsObj = typeof rule.conditions === "string" ? JSON.parse(rule.conditions) : rule.conditions;
2713
+ if (conditionsObj && Object.keys(conditionsObj).length > 0) conditions = conditionsObj;
2714
+ }
2715
+ const manifestRule = {
2716
+ id: rule.id,
2717
+ configVariantId: rule.configVariantId,
2718
+ variantVersionId: rule.variantVersionId,
2719
+ weight: rule.weight,
2720
+ priority: rule.priority,
2721
+ enabled: rule.enabled,
2722
+ conditions,
2723
+ resolvedVersion: buildVersion(resolvedVersionData)
2724
+ };
2725
+ if (!routingTable[rule.configId]) routingTable[rule.configId] = {};
2726
+ if (!routingTable[rule.configId][rule.environmentId]) routingTable[rule.configId][rule.environmentId] = [];
2727
+ routingTable[rule.configId][rule.environmentId].push(manifestRule);
2728
+ }
2729
+ for (const configRules of Object.values(routingTable)) for (const envRules of Object.values(configRules)) envRules.sort((a, b) => {
2730
+ if (b.priority !== a.priority) return b.priority - a.priority;
2731
+ return b.weight - a.weight;
2732
+ });
2733
+ return {
2734
+ version: Date.now(),
2735
+ builtAt: (/* @__PURE__ */ new Date()).toISOString(),
2736
+ configs: manifestConfigs,
2737
+ configsBySlug,
2738
+ environments: manifestEnvironments,
2739
+ environmentsBySlug,
2740
+ routingTable,
2741
+ secretToEnvironment
2742
+ };
2743
+ }
2744
+ };
2745
+
2746
+ //#endregion
2747
+ //#region src/manifest/service.ts
2748
+ const MANIFEST_CACHE_KEY = "manifest";
2749
+ const MANIFEST_NAMESPACE = "gateway";
2750
+ const DEFAULT_TTL_MS = 300 * 1e3;
2751
+ const log = logger.child({ module: "ManifestService" });
2752
+ var ManifestService = class {
2753
+ builder;
2754
+ constructor(cache, db, ttlMs = DEFAULT_TTL_MS) {
2755
+ this.cache = cache;
2756
+ this.ttlMs = ttlMs;
2757
+ this.builder = new ManifestBuilder(db);
2758
+ log.debug({ ttlMs }, "ManifestService initialized");
2759
+ }
2760
+ /**
2761
+ * Get the current manifest, building if necessary
2762
+ */
2763
+ async getManifest() {
2764
+ log.debug("Getting manifest from cache or building");
2765
+ const manifest = await this.cache.getOrSet(MANIFEST_CACHE_KEY, async () => {
2766
+ log.info("Building new manifest");
2767
+ const built = await this.builder.build();
2768
+ log.info({
2769
+ version: built.version,
2770
+ configCount: Object.keys(built.configs).length,
2771
+ environmentCount: Object.keys(built.environments).length
2772
+ }, "Manifest built successfully");
2773
+ return built;
2774
+ }, {
2775
+ namespace: MANIFEST_NAMESPACE,
2776
+ ttl: this.ttlMs
2777
+ });
2778
+ log.debug({ version: manifest.version }, "Manifest retrieved");
2779
+ return manifest;
2780
+ }
2781
+ /**
2782
+ * Force invalidate the manifest (called on mutations)
2783
+ */
2784
+ async invalidate() {
2785
+ log.info("Invalidating manifest cache");
2786
+ await this.cache.delete(MANIFEST_CACHE_KEY, MANIFEST_NAMESPACE);
2787
+ }
2788
+ /**
2789
+ * Invalidate and immediately rebuild (atomic refresh)
2790
+ */
2791
+ async refresh() {
2792
+ log.info("Refreshing manifest (invalidate + rebuild)");
2793
+ await this.invalidate();
2794
+ return this.getManifest();
2795
+ }
2796
+ /**
2797
+ * Get manifest version without fetching full manifest
2798
+ * Useful for checking if manifest is stale
2799
+ */
2800
+ async getVersion() {
2801
+ const version = (await this.cache.get(MANIFEST_CACHE_KEY, MANIFEST_NAMESPACE))?.version ?? null;
2802
+ log.debug({ version }, "Got manifest version");
2803
+ return version;
2804
+ }
2805
+ /**
2806
+ * Check if manifest exists in cache
2807
+ */
2808
+ async hasManifest() {
2809
+ const exists = await this.cache.has(MANIFEST_CACHE_KEY, MANIFEST_NAMESPACE);
2810
+ log.debug({ exists }, "Checked manifest existence");
2811
+ return exists;
2812
+ }
2813
+ };
2814
+
2815
+ //#endregion
2816
+ //#region src/manifest/router.ts
2817
+ /**
2818
+ * Router for evaluating the gateway manifest and selecting variants
2819
+ */
2820
+ var ManifestRouter = class {
2821
+ constructor(manifest) {
2822
+ this.manifest = manifest;
2823
+ }
2824
+ /**
2825
+ * Resolve a config identifier (UUID or slug) to config ID
2826
+ */
2827
+ resolveConfigId(configIdOrSlug) {
2828
+ if (this.manifest.configs[configIdOrSlug]) return configIdOrSlug;
2829
+ return this.manifest.configsBySlug[configIdOrSlug] ?? null;
2830
+ }
2831
+ /**
2832
+ * Resolve environment from secret value
2833
+ */
2834
+ resolveEnvironmentFromSecret(secretValue) {
2835
+ return this.manifest.secretToEnvironment[secretValue] ?? null;
2836
+ }
2837
+ /**
2838
+ * Get production environment ID
2839
+ */
2840
+ getProductionEnvironmentId() {
2841
+ for (const env of Object.values(this.manifest.environments)) if (env.isProd) return env.id;
2842
+ return null;
2843
+ }
2844
+ /**
2845
+ * Get environment by ID
2846
+ */
2847
+ getEnvironment(environmentId) {
2848
+ return this.manifest.environments[environmentId] ?? null;
2849
+ }
2850
+ /**
2851
+ * Get config by ID
2852
+ */
2853
+ getConfig(configId) {
2854
+ return this.manifest.configs[configId] ?? null;
2855
+ }
2856
+ /**
2857
+ * Route a request to the appropriate variant version (first match wins)
2858
+ */
2859
+ route(configIdOrSlug, environmentId, context = {}) {
2860
+ const configId = this.resolveConfigId(configIdOrSlug);
2861
+ if (!configId) return null;
2862
+ const rules = this.manifest.routingTable[configId]?.[environmentId];
2863
+ if (!rules || rules.length === 0) return null;
2864
+ const evalContext = {
2865
+ ...context,
2866
+ timestamp: context.timestamp ?? Date.now()
2867
+ };
2868
+ for (const rule of rules) {
2869
+ if (!rule.enabled) continue;
2870
+ if (rule.conditions) try {
2871
+ if (!jsonLogic.apply(rule.conditions, evalContext)) continue;
2872
+ } catch (error) {
2873
+ console.warn(`JSONLogic evaluation error for rule ${rule.id}:`, error);
2874
+ continue;
2875
+ }
2876
+ return {
2877
+ configId,
2878
+ environmentId,
2879
+ variantId: rule.resolvedVersion.variantId,
2880
+ version: rule.resolvedVersion,
2881
+ rule
2882
+ };
2883
+ }
2884
+ return null;
2885
+ }
2886
+ /**
2887
+ * Route with weighted random selection among matching rules of same priority
2888
+ */
2889
+ routeWithWeights(configIdOrSlug, environmentId, context = {}) {
2890
+ const configId = this.resolveConfigId(configIdOrSlug);
2891
+ if (!configId) return null;
2892
+ const rules = this.manifest.routingTable[configId]?.[environmentId];
2893
+ if (!rules || rules.length === 0) return null;
2894
+ const evalContext = {
2895
+ ...context,
2896
+ timestamp: context.timestamp ?? Date.now()
2897
+ };
2898
+ const rulesByPriority = /* @__PURE__ */ new Map();
2899
+ for (const rule$1 of rules) {
2900
+ if (!rule$1.enabled) continue;
2901
+ if (rule$1.conditions) try {
2902
+ if (!jsonLogic.apply(rule$1.conditions, evalContext)) continue;
2903
+ } catch {
2904
+ continue;
2905
+ }
2906
+ const list = rulesByPriority.get(rule$1.priority) || [];
2907
+ list.push(rule$1);
2908
+ rulesByPriority.set(rule$1.priority, list);
2909
+ }
2910
+ const priorities = Array.from(rulesByPriority.keys()).sort((a, b) => b - a);
2911
+ if (priorities.length === 0) return null;
2912
+ const topPriorityRules = rulesByPriority.get(priorities[0]);
2913
+ if (topPriorityRules.length === 1) {
2914
+ const rule$1 = topPriorityRules[0];
2915
+ return {
2916
+ configId,
2917
+ environmentId,
2918
+ variantId: rule$1.resolvedVersion.variantId,
2919
+ version: rule$1.resolvedVersion,
2920
+ rule: rule$1
2921
+ };
2922
+ }
2923
+ const totalWeight = topPriorityRules.reduce((sum, r) => sum + r.weight, 0);
2924
+ if (totalWeight === 0) return null;
2925
+ let random = Math.random() * totalWeight;
2926
+ for (const rule$1 of topPriorityRules) {
2927
+ random -= rule$1.weight;
2928
+ if (random <= 0) return {
2929
+ configId,
2930
+ environmentId,
2931
+ variantId: rule$1.resolvedVersion.variantId,
2932
+ version: rule$1.resolvedVersion,
2933
+ rule: rule$1
2934
+ };
2935
+ }
2936
+ const rule = topPriorityRules[0];
2937
+ return {
2938
+ configId,
2939
+ environmentId,
2940
+ variantId: rule.resolvedVersion.variantId,
2941
+ version: rule.resolvedVersion,
2942
+ rule
2943
+ };
2944
+ }
2945
+ };
2946
+
2947
+ //#endregion
2948
+ export { CacheService, FileCacheBackend, MS, ManifestBuilder, ManifestRouter, ManifestService, MemoryCacheBackend, ModelsDevPricingProvider, SCHEMA_METADATA, SupportedProviders, calculateCost, chatCompletionCreateParamsBaseSchema, configVariantsSchema, configsSchema, createDataLayer, createDatabase, createDatabaseFromConnection, createLLMRequestsDataLayer, createNeonDialect, createProviderConfigsDataLayer, createWorkspaceSettingsDataLayer, detectDatabaseType, dollarsToMicroDollars, environmentSecretsSchema, environmentsSchema, executeWithSchema, formatCost, gateway, generateId, getAuthClientOptions, getDefaultPricingProvider, getMigrations, llmRequestsSchema, llmopsConfigSchema, logger, matchType, microDollarsToDollars, parsePartialTableData, parseTableData, providerConfigsSchema, runAutoMigrations, schemas, targetingRulesSchema, validateLLMOpsConfig, validatePartialTableData, validateTableData, variantJsonDataSchema, variantVersionsSchema, variantsSchema, workspaceSettingsSchema };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llmops/core",
3
- "version": "0.2.14",
3
+ "version": "0.3.0-beta.2",
4
4
  "description": "Core LLMOps functionality and utilities",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -51,12 +51,14 @@
51
51
  "@neondatabase/serverless": "^1.0.2",
52
52
  "better-auth": "^1.4.10",
53
53
  "hono": "^4.10.7",
54
+ "json-logic-js": "^2.0.5",
54
55
  "kysely": "^0.28.8",
55
56
  "kysely-neon": "^2.0.2",
56
57
  "pino": "^10.1.0",
57
- "@llmops/gateway": "^0.2.14"
58
+ "@llmops/gateway": "^0.3.0-beta.2"
58
59
  },
59
60
  "devDependencies": {
61
+ "@types/json-logic-js": "^2.0.8",
60
62
  "@types/ws": "^8.18.1"
61
63
  },
62
64
  "scripts": {