@llmops/core 0.1.5-beta.1 → 0.1.5

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
@@ -1,5 +1,6 @@
1
- import { A as record, C as _enum, D as literal, E as boolean, M as union, N as unknown, O as number, S as zod_default, T as array, _ as schemas, a as matchType, b as variantsSchema, c as parsePartialTableData, d as validateTableData, f as SCHEMA_METADATA, g as environmentsSchema, h as environmentSecretsSchema, i as getMigrations, j as string, k as object, 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 targetingRulesSchema, w as any, x as workspaceSettingsSchema, y as variantVersionsSchema } from "./db-CGY-vZ3u.mjs";
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";
3
4
  import * as fs from "node:fs/promises";
4
5
  import * as path from "node:path";
5
6
  import { createRandomStringGenerator } from "@better-auth/utils/random";
@@ -1279,7 +1280,11 @@ const createConfigVariantDataLayer = (db) => {
1279
1280
  "version"
1280
1281
  ]).where("variantId", "=", configVariant.variantId).orderBy("version", "desc").limit(1).executeTakeFirst();
1281
1282
  if (!versionData) throw new LLMOpsError(`No variant version found for variant ${configVariant.variantId}`);
1282
- return versionData;
1283
+ return {
1284
+ ...versionData,
1285
+ configId: resolvedConfigId,
1286
+ variantId: configVariant.variantId
1287
+ };
1283
1288
  }
1284
1289
  };
1285
1290
  };
@@ -1439,6 +1444,287 @@ const createEnvironmentSecretDataLayer = (db) => {
1439
1444
  };
1440
1445
  };
1441
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
+
1442
1728
  //#endregion
1443
1729
  //#region src/datalayer/targetingRules.ts
1444
1730
  const createTargetingRule = zod_default.object({
@@ -1865,6 +2151,7 @@ const createDataLayer = async (db) => {
1865
2151
  ...createConfigVariantDataLayer(db),
1866
2152
  ...createEnvironmentDataLayer(db),
1867
2153
  ...createEnvironmentSecretDataLayer(db),
2154
+ ...createLLMRequestsDataLayer(db),
1868
2155
  ...createTargetingRulesDataLayer(db),
1869
2156
  ...createVariantDataLayer(db),
1870
2157
  ...createVariantVersionsDataLayer(db),
@@ -1873,4 +2160,207 @@ const createDataLayer = async (db) => {
1873
2160
  };
1874
2161
 
1875
2162
  //#endregion
1876
- export { CacheService, FileCacheBackend, MS, MemoryCacheBackend, SCHEMA_METADATA, SupportedProviders, chatCompletionCreateParamsBaseSchema, configVariantsSchema, configsSchema, createDataLayer, createDatabase, createDatabaseFromConnection, detectDatabaseType, environmentSecretsSchema, environmentsSchema, gateway, generateId, getMigrations, llmopsConfigSchema, logger, matchType, parsePartialTableData, parseTableData, runAutoMigrations, schemas, targetingRulesSchema, validateLLMOpsConfig, validatePartialTableData, validateTableData, variantJsonDataSchema, variantVersionsSchema, variantsSchema, workspaceSettingsSchema };
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 };
@@ -1,4 +1,4 @@
1
- const require_db = require('./db-C9-M-kdS.cjs');
1
+ const require_db = require('./db-eEfIe5dO.cjs');
2
2
  let kysely = require("kysely");
3
3
 
4
4
  //#region src/db/node-sqlite-dialect.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llmops/core",
3
- "version": "0.1.5-beta.1",
3
+ "version": "0.1.5",
4
4
  "description": "Core LLMOps functionality and utilities",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -51,7 +51,7 @@
51
51
  "hono": "^4.10.7",
52
52
  "kysely": "^0.28.8",
53
53
  "pino": "^10.1.0",
54
- "@llmops/gateway": "^0.1.5-beta.1"
54
+ "@llmops/gateway": "^0.1.5"
55
55
  },
56
56
  "scripts": {
57
57
  "build": "tsdown",