@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/{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 +500 -2
- package/dist/index.d.cts +600 -3
- package/dist/index.d.mts +600 -3
- package/dist/index.mjs +493 -3
- 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,6 @@
|
|
|
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";
|
|
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
|
|
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
|
-
|
|
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 };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llmops/core",
|
|
3
|
-
"version": "0.1.5
|
|
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
|
|
54
|
+
"@llmops/gateway": "^0.1.5"
|
|
55
55
|
},
|
|
56
56
|
"scripts": {
|
|
57
57
|
"build": "tsdown",
|