@oneuptime/common 12.0.24 → 12.0.25

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.
Files changed (43) hide show
  1. package/Models/AnalyticsModels/Span.ts +101 -0
  2. package/Server/API/BaseAPI.ts +0 -24
  3. package/Server/API/SlackAPI.ts +0 -2
  4. package/Server/Middleware/SlackAuthorization.ts +96 -18
  5. package/Server/Utils/Telemetry/LlmMetricSpend.ts +56 -5
  6. package/Server/Utils/Telemetry/LlmSpan.ts +46 -0
  7. package/Server/Utils/Workspace/Slack/Actions/Auth.ts +0 -12
  8. package/Tests/App/Dashboard/LlmCallsTableIdentity.test.tsx +322 -0
  9. package/Tests/App/Dashboard/LlmOverview.test.tsx +335 -0
  10. package/Tests/App/Dashboard/LlmSpanDisplay.test.ts +391 -0
  11. package/Tests/App/Dashboard/LlmUsageBreakdown.test.tsx +1007 -0
  12. package/Tests/Server/API/BaseAPI.test.ts +41 -0
  13. package/Tests/Server/API/BaseAPIUpdatePayloadValidation.test.ts +9 -16
  14. package/Tests/Server/Middleware/SlackAuthorization.test.ts +262 -5
  15. package/Tests/Server/Utils/Telemetry/LlmCostBudgetEvaluator.test.ts +37 -18
  16. package/Tests/Server/Utils/Telemetry/LlmMetricSpend.test.ts +143 -2
  17. package/Tests/Server/Utils/Telemetry/LlmSpan.test.ts +804 -0
  18. package/Tests/Types/Telemetry/LlmMetricConventions.test.ts +391 -0
  19. package/Tests/Utils/Telemetry/LlmMetricQuery.test.ts +298 -0
  20. package/Types/Telemetry/LlmConventions.ts +255 -0
  21. package/Types/Telemetry/LlmMetricConventions.ts +212 -7
  22. package/Utils/Telemetry/LlmMetricQuery.ts +83 -0
  23. package/build/dist/Models/AnalyticsModels/Span.js +89 -0
  24. package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
  25. package/build/dist/Server/API/BaseAPI.js +3 -19
  26. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  27. package/build/dist/Server/API/SlackAPI.js +0 -2
  28. package/build/dist/Server/API/SlackAPI.js.map +1 -1
  29. package/build/dist/Server/Middleware/SlackAuthorization.js +58 -7
  30. package/build/dist/Server/Middleware/SlackAuthorization.js.map +1 -1
  31. package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js +52 -3
  32. package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js.map +1 -1
  33. package/build/dist/Server/Utils/Telemetry/LlmSpan.js +24 -1
  34. package/build/dist/Server/Utils/Telemetry/LlmSpan.js.map +1 -1
  35. package/build/dist/Server/Utils/Workspace/Slack/Actions/Auth.js +0 -10
  36. package/build/dist/Server/Utils/Workspace/Slack/Actions/Auth.js.map +1 -1
  37. package/build/dist/Types/Telemetry/LlmConventions.js +236 -0
  38. package/build/dist/Types/Telemetry/LlmConventions.js.map +1 -1
  39. package/build/dist/Types/Telemetry/LlmMetricConventions.js +197 -5
  40. package/build/dist/Types/Telemetry/LlmMetricConventions.js.map +1 -1
  41. package/build/dist/Utils/Telemetry/LlmMetricQuery.js +57 -1
  42. package/build/dist/Utils/Telemetry/LlmMetricQuery.js.map +1 -1
  43. package/package.json +1 -1
@@ -5,6 +5,7 @@ import Includes from "../../../../Types/BaseDatabase/Includes";
5
5
  import ObjectID from "../../../../Types/ObjectID";
6
6
  import {
7
7
  LlmCostMetricNames,
8
+ LlmMicroUsdCostMetricNames,
8
9
  LlmTokenTypeAttributeKeys,
9
10
  LlmTokenUsageMetricNames,
10
11
  } from "../../../../Types/Telemetry/LlmMetricConventions";
@@ -111,6 +112,65 @@ describe("LlmMetricSpend", () => {
111
112
  });
112
113
  });
113
114
 
115
+ /*
116
+ * The micro-USD aggregate. It exists as a SECOND query for one reason: the
117
+ * recognized cost metrics come in two units, and a single Sum over both
118
+ * would add millionths of a dollar to dollars with no way to recover the
119
+ * unit afterwards. A $3 Codex turn would reach a budget as $3,000,000.
120
+ */
121
+ describe("buildMicroUsdCostAggregateBy", () => {
122
+ test("selects the micro-USD metric names", () => {
123
+ const query: Record<string, unknown> =
124
+ LlmMetricSpend.buildMicroUsdCostAggregateBy(scope())
125
+ .query as unknown as Record<string, unknown>;
126
+
127
+ expect((query["name"] as Includes).values).toEqual(
128
+ LlmMicroUsdCostMetricNames,
129
+ );
130
+ });
131
+
132
+ test("never selects a USD cost metric name", () => {
133
+ const query: Record<string, unknown> =
134
+ LlmMetricSpend.buildMicroUsdCostAggregateBy(scope())
135
+ .query as unknown as Record<string, unknown>;
136
+
137
+ const names: Array<string> = (query["name"] as Includes)
138
+ .values as Array<string>;
139
+
140
+ for (const name of names) {
141
+ expect(LlmCostMetricNames).not.toContain(name);
142
+ }
143
+ });
144
+
145
+ test("sums the value column over the whole window, like the USD query", () => {
146
+ const aggregate: AggregateBy<Metric> =
147
+ LlmMetricSpend.buildMicroUsdCostAggregateBy(scope());
148
+
149
+ expect(aggregate.aggregationType).toBe(AggregationType.Sum);
150
+ expect(aggregate.aggregateColumnName).toBe("value");
151
+ expect(aggregate.aggregationInterval).toBe(AggregationInterval.Total);
152
+ expect(aggregate.startTimestamp).toBe(START);
153
+ expect(aggregate.endTimestamp).toBe(END);
154
+ });
155
+
156
+ test("fails loud on query timeout", () => {
157
+ expect(
158
+ LlmMetricSpend.buildMicroUsdCostAggregateBy(scope())
159
+ .timeoutOverflowMode,
160
+ ).toBe("throw");
161
+ });
162
+
163
+ test("carries the caller's scoping into the query", () => {
164
+ const query: Record<string, unknown> =
165
+ LlmMetricSpend.buildMicroUsdCostAggregateBy(
166
+ scope({ serviceId: SERVICE_ID, llmSystem: "openai" }),
167
+ ).query as unknown as Record<string, unknown>;
168
+
169
+ expect(query["primaryEntityId"]).toBe(SERVICE_ID);
170
+ expect(query["attributes"]).toEqual({ "gen_ai.system": "openai" });
171
+ });
172
+ });
173
+
114
174
  describe("buildTokenAggregateBy", () => {
115
175
  test("selects the token usage metric names", () => {
116
176
  const query: Record<string, unknown> =
@@ -185,12 +245,93 @@ describe("LlmMetricSpend", () => {
185
245
  );
186
246
  });
187
247
 
188
- test("issues exactly one aggregate", async () => {
248
+ /*
249
+ * TWO aggregates, not one — this count changed deliberately when the
250
+ * micro-USD emitters (the Codex CLI) were recognized. The two name lists
251
+ * carry different UNITS, so they cannot share a Sum; see
252
+ * buildMicroUsdCostAggregateBy above.
253
+ */
254
+ test("issues one aggregate per cost unit", async () => {
189
255
  mockedMetricService.aggregateBy.mockResolvedValue({ data: [] });
190
256
 
191
257
  await LlmMetricSpend.getCostInUSD(scope());
192
258
 
193
- expect(mockedMetricService.aggregateBy).toHaveBeenCalledTimes(1);
259
+ expect(mockedMetricService.aggregateBy).toHaveBeenCalledTimes(2);
260
+ });
261
+
262
+ test("the two cost aggregates select disjoint metric names", async () => {
263
+ mockedMetricService.aggregateBy.mockResolvedValue({ data: [] });
264
+
265
+ await LlmMetricSpend.getCostInUSD(scope());
266
+
267
+ const selected: Array<Array<string>> =
268
+ mockedMetricService.aggregateBy.mock.calls.map(
269
+ (call: Array<unknown>): Array<string> => {
270
+ const aggregate: AggregateBy<Metric> =
271
+ call[0] as AggregateBy<Metric>;
272
+ const query: Record<string, unknown> =
273
+ aggregate.query as unknown as Record<string, unknown>;
274
+
275
+ return (query["name"] as Includes).values as Array<string>;
276
+ },
277
+ );
278
+
279
+ expect(selected).toHaveLength(2);
280
+ expect(selected[0]).toEqual(LlmCostMetricNames);
281
+ expect(selected[1]).toEqual(LlmMicroUsdCostMetricNames);
282
+
283
+ const overlap: Array<string> = selected[0]!.filter((name: string) => {
284
+ return selected[1]!.includes(name);
285
+ });
286
+
287
+ expect(overlap).toEqual([]);
288
+ });
289
+
290
+ /*
291
+ * The million-fold regression, end to end: a Codex turn reported as
292
+ * 1,500,000 micro-USD must reach a budget as $1.50, not $1,500,000.
293
+ */
294
+ test("scales micro-USD spend by 1e-6 before adding it to USD spend", async () => {
295
+ mockedMetricService.aggregateBy
296
+ .mockResolvedValueOnce({ data: [{ timestamp: START, value: 0.5 }] })
297
+ .mockResolvedValueOnce({
298
+ data: [{ timestamp: START, value: 1_500_000 }],
299
+ });
300
+
301
+ await expect(LlmMetricSpend.getCostInUSD(scope())).resolves.toBeCloseTo(
302
+ 2,
303
+ 10,
304
+ );
305
+ });
306
+
307
+ test("micro-USD-only spend is still reported rather than lost", async () => {
308
+ // The metrics-only Codex fleet: no USD cost metric exists at all.
309
+ mockedMetricService.aggregateBy
310
+ .mockResolvedValueOnce({ data: [] })
311
+ .mockResolvedValueOnce({
312
+ data: [{ timestamp: START, value: 2_250_000 }],
313
+ });
314
+
315
+ await expect(LlmMetricSpend.getCostInUSD(scope())).resolves.toBeCloseTo(
316
+ 2.25,
317
+ 10,
318
+ );
319
+ });
320
+
321
+ test("a micro-USD query failure is not swallowed into a low total", async () => {
322
+ /*
323
+ * Both aggregates run in parallel; a failure in EITHER must propagate.
324
+ * Silently treating the failed one as zero would understate spend and
325
+ * suppress a real budget breach — the same reasoning as the
326
+ * timeoutOverflowMode: "throw" above.
327
+ */
328
+ mockedMetricService.aggregateBy
329
+ .mockResolvedValueOnce({ data: [{ timestamp: START, value: 1 }] })
330
+ .mockRejectedValueOnce(new Error("clickhouse timeout"));
331
+
332
+ await expect(LlmMetricSpend.getCostInUSD(scope())).rejects.toThrow(
333
+ "clickhouse timeout",
334
+ );
194
335
  });
195
336
  });
196
337