1import * as Cause from "effect/Cause";
2import * as Exit from "effect/Exit";
3
4export type MetricAttributeValue = string;
5export type MetricAttributes = Readonly<Record<string, MetricAttributeValue>>;
6export type ObservabilityOutcome = "success" | "failure" | "interrupt";
7
8export function compactMetricAttributes(
9 attributes: Readonly<Record<string, unknown>>,
10): MetricAttributes {
11 return Object.fromEntries(
12 Object.entries(attributes).flatMap(([key, value]) => {
13 if (value === undefined || value === null) {
14 return [];
15 }
16 if (typeof value === "string") {
17 return [[key, value]];
18 }
19 if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
20 return [[key, String(value)]];
21 }
22 return [];
23 }),
24 );
25}
26
27export function outcomeFromExit(exit: Exit.Exit<unknown, unknown>): ObservabilityOutcome {
28 if (Exit.isSuccess(exit)) {
29 return "success";
30 }
31 return Cause.hasInterruptsOnly(exit.cause) ? "interrupt" : "failure";
32}
33
34export function normalizeModelMetricLabel(model: string | null | undefined): string | undefined {
35 const normalized = model?.trim().toLowerCase();
36 if (!normalized) {
37 return undefined;
38 }
39 if (normalized.includes("gpt")) {
40 return "gpt";
41 }
42 if (normalized.includes("claude")) {
43 return "claude";
44 }
45 if (normalized.includes("gemini")) {
46 return "gemini";
47 }
48 return "other";
49}