@oneuptime/common 13.0.3 → 13.0.4
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/Tests/Server/Utils/LoggerCore.test.ts +597 -0
- package/Tests/Server/Utils/Monitor/MonitorTemplateUtil.test.ts +1109 -0
- package/Tests/Server/Utils/Telemetry/EntityRegistry.test.ts +711 -0
- package/Tests/Server/Utils/Telemetry/ResourceFacetResolver.test.ts +522 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilCorePaths.test.ts +1014 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveRotation.test.ts +9 -6
- package/Tests/Types/Rules/RuleCriteriaFieldRegistry.test.ts +276 -0
- package/Tests/Types/Telemetry/LlmConventions.test.ts +495 -0
- package/Tests/Utils/Rum/SessionReplayHealth.test.ts +884 -0
- package/Types/OnCallDutyPolicy/Layer.ts +12 -0
- package/Types/Rules/RuleCriteriaFieldRegistry.ts +12 -0
- package/build/dist/Types/OnCallDutyPolicy/Layer.js +11 -0
- package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
- package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js +9 -0
- package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
import { describe, expect, test } from "@jest/globals";
|
|
2
|
+
import {
|
|
3
|
+
LlmAgentNameAttributeKeys,
|
|
4
|
+
LlmAttributeNamespacePrefixes,
|
|
5
|
+
LlmCompletionEventNames,
|
|
6
|
+
LlmCompletionIndexedMessageConventions,
|
|
7
|
+
LlmCompletionJsonAttributeKeys,
|
|
8
|
+
LlmConversationIdAttributeKeys,
|
|
9
|
+
LlmCostAttributeKeys,
|
|
10
|
+
LlmEndUserAttributeKeys,
|
|
11
|
+
LlmEndUserBaseAttributeKeys,
|
|
12
|
+
LlmFinishReasonAttributeKeys,
|
|
13
|
+
LlmIndexedMessageConvention,
|
|
14
|
+
LlmInputTokenAttributeKeys,
|
|
15
|
+
LlmMaxTokensAttributeKeys,
|
|
16
|
+
LlmOperationAttributeKeys,
|
|
17
|
+
LlmOutputTokenAttributeKeys,
|
|
18
|
+
LlmPromptEventNames,
|
|
19
|
+
LlmPromptIndexedMessageConventions,
|
|
20
|
+
LlmPromptJsonAttributeKeys,
|
|
21
|
+
LlmRequestModelAttributeKeys,
|
|
22
|
+
LlmResponseModelAttributeKeys,
|
|
23
|
+
LlmSystemAttributeKeys,
|
|
24
|
+
LlmTeamAttributeKeys,
|
|
25
|
+
LlmTeamBaseAttributeKeys,
|
|
26
|
+
LlmTemperatureAttributeKeys,
|
|
27
|
+
LlmToolNameAttributeKeys,
|
|
28
|
+
LlmTopPAttributeKeys,
|
|
29
|
+
LlmTotalTokenAttributeKeys,
|
|
30
|
+
LlmUserEmailAttributeKeys,
|
|
31
|
+
LlmUserEmailBaseAttributeKeys,
|
|
32
|
+
LlmUserIdAttributeKeys,
|
|
33
|
+
LlmUserIdBaseAttributeKeys,
|
|
34
|
+
RESOURCE_ATTRIBUTE_KEY_PREFIX,
|
|
35
|
+
withResourcePrefixedKeys,
|
|
36
|
+
} from "../../../Types/Telemetry/LlmConventions";
|
|
37
|
+
|
|
38
|
+
/*
|
|
39
|
+
* Lists that carry a per-CALL value and must stay span-attribute only.
|
|
40
|
+
* Widening them would double lookups on the hottest ingest path.
|
|
41
|
+
*/
|
|
42
|
+
const PER_CALL_LISTS: Record<string, Array<string>> = {
|
|
43
|
+
LlmSystemAttributeKeys,
|
|
44
|
+
LlmOperationAttributeKeys,
|
|
45
|
+
LlmRequestModelAttributeKeys,
|
|
46
|
+
LlmResponseModelAttributeKeys,
|
|
47
|
+
LlmInputTokenAttributeKeys,
|
|
48
|
+
LlmOutputTokenAttributeKeys,
|
|
49
|
+
LlmTotalTokenAttributeKeys,
|
|
50
|
+
LlmCostAttributeKeys,
|
|
51
|
+
LlmConversationIdAttributeKeys,
|
|
52
|
+
LlmAgentNameAttributeKeys,
|
|
53
|
+
LlmToolNameAttributeKeys,
|
|
54
|
+
LlmTemperatureAttributeKeys,
|
|
55
|
+
LlmMaxTokensAttributeKeys,
|
|
56
|
+
LlmTopPAttributeKeys,
|
|
57
|
+
LlmFinishReasonAttributeKeys,
|
|
58
|
+
LlmPromptJsonAttributeKeys,
|
|
59
|
+
LlmCompletionJsonAttributeKeys,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
interface IdentityTier {
|
|
63
|
+
name: string;
|
|
64
|
+
base: Array<string>;
|
|
65
|
+
widened: Array<string>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const IDENTITY_TIERS: Array<IdentityTier> = [
|
|
69
|
+
{
|
|
70
|
+
name: "user id",
|
|
71
|
+
base: LlmUserIdBaseAttributeKeys,
|
|
72
|
+
widened: LlmUserIdAttributeKeys,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: "user email",
|
|
76
|
+
base: LlmUserEmailBaseAttributeKeys,
|
|
77
|
+
widened: LlmUserEmailAttributeKeys,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "team",
|
|
81
|
+
base: LlmTeamBaseAttributeKeys,
|
|
82
|
+
widened: LlmTeamAttributeKeys,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: "end user",
|
|
86
|
+
base: LlmEndUserBaseAttributeKeys,
|
|
87
|
+
widened: LlmEndUserAttributeKeys,
|
|
88
|
+
},
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
const ALL_STRING_LISTS: Record<string, Array<string>> = {
|
|
92
|
+
...PER_CALL_LISTS,
|
|
93
|
+
LlmUserIdBaseAttributeKeys,
|
|
94
|
+
LlmUserIdAttributeKeys,
|
|
95
|
+
LlmUserEmailBaseAttributeKeys,
|
|
96
|
+
LlmUserEmailAttributeKeys,
|
|
97
|
+
LlmTeamBaseAttributeKeys,
|
|
98
|
+
LlmTeamAttributeKeys,
|
|
99
|
+
LlmEndUserBaseAttributeKeys,
|
|
100
|
+
LlmEndUserAttributeKeys,
|
|
101
|
+
LlmAttributeNamespacePrefixes,
|
|
102
|
+
LlmPromptEventNames,
|
|
103
|
+
LlmCompletionEventNames,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
describe("RESOURCE_ATTRIBUTE_KEY_PREFIX", () => {
|
|
107
|
+
test("is the exact prefix OTLP ingest stamps on resource attributes", () => {
|
|
108
|
+
expect(RESOURCE_ATTRIBUTE_KEY_PREFIX).toBe("resource.");
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("withResourcePrefixedKeys", () => {
|
|
113
|
+
test("returns an empty list for an empty input", () => {
|
|
114
|
+
expect(withResourcePrefixedKeys([])).toEqual([]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("puts the whole bare block first, then the whole resource block, each in input order", () => {
|
|
118
|
+
expect(withResourcePrefixedKeys(["a", "b", "c"])).toEqual([
|
|
119
|
+
"a",
|
|
120
|
+
"b",
|
|
121
|
+
"c",
|
|
122
|
+
"resource.a",
|
|
123
|
+
"resource.b",
|
|
124
|
+
"resource.c",
|
|
125
|
+
]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("does not interleave: the last bare key comes before the first resource key", () => {
|
|
129
|
+
const widened: Array<string> = withResourcePrefixedKeys(["x", "y"]);
|
|
130
|
+
|
|
131
|
+
expect(widened.indexOf("y")).toBeLessThan(widened.indexOf("resource.x"));
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("does not mutate its input", () => {
|
|
135
|
+
const input: Array<string> = ["user.id", "team.id"];
|
|
136
|
+
const snapshot: Array<string> = [...input];
|
|
137
|
+
|
|
138
|
+
const widened: Array<string> = withResourcePrefixedKeys(input);
|
|
139
|
+
|
|
140
|
+
expect(input).toEqual(snapshot);
|
|
141
|
+
expect(widened).not.toBe(input);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("returns a fresh array on every call", () => {
|
|
145
|
+
const input: Array<string> = ["k"];
|
|
146
|
+
|
|
147
|
+
expect(withResourcePrefixedKeys(input)).not.toBe(
|
|
148
|
+
withResourcePrefixedKeys(input),
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("always doubles the length, preserving duplicates as given", () => {
|
|
153
|
+
expect(withResourcePrefixedKeys(["k", "k"])).toEqual([
|
|
154
|
+
"k",
|
|
155
|
+
"k",
|
|
156
|
+
"resource.k",
|
|
157
|
+
"resource.k",
|
|
158
|
+
]);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("prefixes blindly, even a key that is already resource-prefixed or empty", () => {
|
|
162
|
+
expect(withResourcePrefixedKeys(["resource.team"])).toEqual([
|
|
163
|
+
"resource.team",
|
|
164
|
+
"resource.resource.team",
|
|
165
|
+
]);
|
|
166
|
+
expect(withResourcePrefixedKeys([""])).toEqual(["", "resource."]);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe("identity lists are resource-widened", () => {
|
|
171
|
+
test.each(IDENTITY_TIERS)(
|
|
172
|
+
"the $name list is exactly the widened base list",
|
|
173
|
+
(tier: IdentityTier) => {
|
|
174
|
+
expect(tier.widened).toEqual(withResourcePrefixedKeys(tier.base));
|
|
175
|
+
},
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
test.each(IDENTITY_TIERS)(
|
|
179
|
+
"the $name base list carries only span-attribute spellings",
|
|
180
|
+
(tier: IdentityTier) => {
|
|
181
|
+
for (const key of tier.base) {
|
|
182
|
+
expect(key.startsWith(RESOURCE_ATTRIBUTE_KEY_PREFIX)).toBe(false);
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
test.each(IDENTITY_TIERS)(
|
|
188
|
+
"every $name key has a resource-prefixed twin that sorts after every bare key",
|
|
189
|
+
(tier: IdentityTier) => {
|
|
190
|
+
const lastBareIndex: number = tier.base.length - 1;
|
|
191
|
+
|
|
192
|
+
for (const key of tier.base) {
|
|
193
|
+
const twinIndex: number = tier.widened.indexOf(
|
|
194
|
+
`${RESOURCE_ATTRIBUTE_KEY_PREFIX}${key}`,
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
expect(twinIndex).toBeGreaterThan(lastBareIndex);
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
test("OTEL_RESOURCE_ATTRIBUTES=team.id=... is recognised in its ingested spelling", () => {
|
|
203
|
+
expect(LlmTeamAttributeKeys).toContain("resource.team.id");
|
|
204
|
+
expect(LlmTeamAttributeKeys).toContain("resource.cost_center");
|
|
205
|
+
expect(LlmUserIdAttributeKeys).toContain("resource.cursor.user.id");
|
|
206
|
+
expect(LlmTeamAttributeKeys).toContain("resource.cursor.team.id");
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe("per-call lists are NOT resource-widened", () => {
|
|
211
|
+
test.each(Object.keys(PER_CALL_LISTS))(
|
|
212
|
+
"%s has no resource.* key",
|
|
213
|
+
(listName: string) => {
|
|
214
|
+
for (const key of PER_CALL_LISTS[listName]!) {
|
|
215
|
+
expect(key.startsWith(RESOURCE_ATTRIBUTE_KEY_PREFIX)).toBe(false);
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe("list hygiene", () => {
|
|
222
|
+
test.each(Object.keys(ALL_STRING_LISTS))(
|
|
223
|
+
"%s is non-empty, has no duplicates and no blank or padded keys",
|
|
224
|
+
(listName: string) => {
|
|
225
|
+
const list: Array<string> = ALL_STRING_LISTS[listName]!;
|
|
226
|
+
|
|
227
|
+
expect(list.length).toBeGreaterThan(0);
|
|
228
|
+
expect(new Set(list).size).toBe(list.length);
|
|
229
|
+
|
|
230
|
+
for (const key of list) {
|
|
231
|
+
expect(key.trim()).toBe(key);
|
|
232
|
+
expect(key.length).toBeGreaterThan(0);
|
|
233
|
+
expect(key).not.toMatch(/\s/);
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
test("namespace prefixes all end with a dot so 'llm.' cannot match 'llmops.x'", () => {
|
|
239
|
+
for (const prefix of LlmAttributeNamespacePrefixes) {
|
|
240
|
+
expect(prefix.endsWith(".")).toBe(true);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
expect(LlmAttributeNamespacePrefixes).toEqual([
|
|
244
|
+
"gen_ai.",
|
|
245
|
+
"llm.",
|
|
246
|
+
"traceloop.",
|
|
247
|
+
]);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("the preferred (first) key of every core list is in an LLM namespace", () => {
|
|
251
|
+
const coreLists: Array<Array<string>> = [
|
|
252
|
+
LlmSystemAttributeKeys,
|
|
253
|
+
LlmOperationAttributeKeys,
|
|
254
|
+
LlmRequestModelAttributeKeys,
|
|
255
|
+
LlmResponseModelAttributeKeys,
|
|
256
|
+
LlmInputTokenAttributeKeys,
|
|
257
|
+
LlmOutputTokenAttributeKeys,
|
|
258
|
+
LlmTotalTokenAttributeKeys,
|
|
259
|
+
LlmCostAttributeKeys,
|
|
260
|
+
LlmConversationIdAttributeKeys,
|
|
261
|
+
LlmAgentNameAttributeKeys,
|
|
262
|
+
LlmToolNameAttributeKeys,
|
|
263
|
+
LlmTemperatureAttributeKeys,
|
|
264
|
+
LlmMaxTokensAttributeKeys,
|
|
265
|
+
LlmTopPAttributeKeys,
|
|
266
|
+
LlmFinishReasonAttributeKeys,
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
for (const list of coreLists) {
|
|
270
|
+
expect(list[0]!.startsWith("gen_ai.")).toBe(true);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe("preference ordering", () => {
|
|
276
|
+
test("the OTel GenAI semconv spelling comes first", () => {
|
|
277
|
+
expect(LlmSystemAttributeKeys[0]).toBe("gen_ai.system");
|
|
278
|
+
expect(LlmOperationAttributeKeys[0]).toBe("gen_ai.operation.name");
|
|
279
|
+
expect(LlmRequestModelAttributeKeys[0]).toBe("gen_ai.request.model");
|
|
280
|
+
expect(LlmResponseModelAttributeKeys[0]).toBe("gen_ai.response.model");
|
|
281
|
+
expect(LlmInputTokenAttributeKeys[0]).toBe("gen_ai.usage.input_tokens");
|
|
282
|
+
expect(LlmOutputTokenAttributeKeys[0]).toBe("gen_ai.usage.output_tokens");
|
|
283
|
+
expect(LlmConversationIdAttributeKeys[0]).toBe("gen_ai.conversation.id");
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("current token names are preferred over their deprecated prompt/completion spellings", () => {
|
|
287
|
+
expect(
|
|
288
|
+
LlmInputTokenAttributeKeys.indexOf("gen_ai.usage.input_tokens"),
|
|
289
|
+
).toBeLessThan(
|
|
290
|
+
LlmInputTokenAttributeKeys.indexOf("gen_ai.usage.prompt_tokens"),
|
|
291
|
+
);
|
|
292
|
+
expect(
|
|
293
|
+
LlmOutputTokenAttributeKeys.indexOf("gen_ai.usage.output_tokens"),
|
|
294
|
+
).toBeLessThan(
|
|
295
|
+
LlmOutputTokenAttributeKeys.indexOf("gen_ai.usage.completion_tokens"),
|
|
296
|
+
);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("user.id is THE canonical employee key and the opaque cursor id sorts last", () => {
|
|
300
|
+
expect(LlmUserIdBaseAttributeKeys[0]).toBe("user.id");
|
|
301
|
+
expect(
|
|
302
|
+
LlmUserIdBaseAttributeKeys[LlmUserIdBaseAttributeKeys.length - 1],
|
|
303
|
+
).toBe("cursor.user.id");
|
|
304
|
+
expect(LlmUserEmailBaseAttributeKeys[0]).toBe("user.email");
|
|
305
|
+
expect(LlmTeamBaseAttributeKeys[0]).toBe("team.id");
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("the JSON message arrays prefer the current gen_ai.*.messages keys", () => {
|
|
309
|
+
expect(LlmPromptJsonAttributeKeys[0]).toBe("gen_ai.input.messages");
|
|
310
|
+
expect(LlmCompletionJsonAttributeKeys[0]).toBe("gen_ai.output.messages");
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
describe("LiteLLM spellings", () => {
|
|
315
|
+
/*
|
|
316
|
+
* The v1 default otel callback emits a bare "metadata." prefix; the opt-in
|
|
317
|
+
* v2 mode emits "litellm.metadata.". Recognising only one silently loses
|
|
318
|
+
* attribution for every deployment on the other.
|
|
319
|
+
*/
|
|
320
|
+
const V2_PREFIX: string = "litellm.metadata.";
|
|
321
|
+
const V1_PREFIX: string = "metadata.";
|
|
322
|
+
|
|
323
|
+
test.each([
|
|
324
|
+
["user id", LlmUserIdBaseAttributeKeys],
|
|
325
|
+
["user email", LlmUserEmailBaseAttributeKeys],
|
|
326
|
+
["team", LlmTeamBaseAttributeKeys],
|
|
327
|
+
["end user", LlmEndUserBaseAttributeKeys],
|
|
328
|
+
])(
|
|
329
|
+
"every v2 litellm.metadata.* %s key has its v1 metadata.* twin, listed after it",
|
|
330
|
+
(_name: string, list: Array<string>) => {
|
|
331
|
+
const v2Keys: Array<string> = list.filter((key: string) => {
|
|
332
|
+
return key.startsWith(V2_PREFIX);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
expect(v2Keys.length).toBeGreaterThan(0);
|
|
336
|
+
|
|
337
|
+
for (const v2Key of v2Keys) {
|
|
338
|
+
const v1Key: string = `${V1_PREFIX}${v2Key.slice(V2_PREFIX.length)}`;
|
|
339
|
+
|
|
340
|
+
expect(list).toContain(v1Key);
|
|
341
|
+
expect(list.indexOf(v2Key)).toBeLessThan(list.indexOf(v1Key));
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
test("the key-OWNER id is an employee key, and the END-USER id is not", () => {
|
|
347
|
+
expect(LlmUserIdAttributeKeys).toContain("metadata.user_api_key_user_id");
|
|
348
|
+
expect(LlmUserIdAttributeKeys).not.toContain(
|
|
349
|
+
"metadata.user_api_key_end_user_id",
|
|
350
|
+
);
|
|
351
|
+
expect(LlmEndUserAttributeKeys).toContain(
|
|
352
|
+
"metadata.user_api_key_end_user_id",
|
|
353
|
+
);
|
|
354
|
+
expect(LlmEndUserAttributeKeys).toContain("litellm.end_user.id");
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
describe("the downstream customer is never read as the employee", () => {
|
|
359
|
+
const EMPLOYEE_LISTS: Record<string, Array<string>> = {
|
|
360
|
+
LlmUserIdAttributeKeys,
|
|
361
|
+
LlmUserEmailAttributeKeys,
|
|
362
|
+
LlmTeamAttributeKeys,
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
test.each(Object.keys(EMPLOYEE_LISTS))(
|
|
366
|
+
"%s is disjoint from the end-user list in both tiers",
|
|
367
|
+
(listName: string) => {
|
|
368
|
+
const employeeKeys: Set<string> = new Set(EMPLOYEE_LISTS[listName]!);
|
|
369
|
+
|
|
370
|
+
for (const endUserKey of LlmEndUserAttributeKeys) {
|
|
371
|
+
expect(employeeKeys.has(endUserKey)).toBe(false);
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
test("the OpenAI `user` request parameter echoes are excluded in both tiers", () => {
|
|
377
|
+
for (const key of [
|
|
378
|
+
"gen_ai.user",
|
|
379
|
+
"llm.user",
|
|
380
|
+
"resource.gen_ai.user",
|
|
381
|
+
"resource.llm.user",
|
|
382
|
+
]) {
|
|
383
|
+
expect(LlmEndUserAttributeKeys).toContain(key);
|
|
384
|
+
expect(LlmUserIdAttributeKeys).not.toContain(key);
|
|
385
|
+
expect(LlmUserEmailAttributeKeys).not.toContain(key);
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test("id, email and team lists do not share keys with each other", () => {
|
|
390
|
+
const id: Set<string> = new Set(LlmUserIdAttributeKeys);
|
|
391
|
+
const email: Set<string> = new Set(LlmUserEmailAttributeKeys);
|
|
392
|
+
|
|
393
|
+
for (const key of LlmTeamAttributeKeys) {
|
|
394
|
+
expect(id.has(key)).toBe(false);
|
|
395
|
+
expect(email.has(key)).toBe(false);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
for (const key of LlmUserEmailAttributeKeys) {
|
|
399
|
+
expect(id.has(key)).toBe(false);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
describe("indexed message conventions", () => {
|
|
405
|
+
const ALL_CONVENTIONS: Array<LlmIndexedMessageConvention> = [
|
|
406
|
+
...LlmPromptIndexedMessageConventions,
|
|
407
|
+
...LlmCompletionIndexedMessageConventions,
|
|
408
|
+
];
|
|
409
|
+
|
|
410
|
+
test("each convention has non-empty parts with no leading/trailing dots", () => {
|
|
411
|
+
for (const convention of ALL_CONVENTIONS) {
|
|
412
|
+
for (const part of [
|
|
413
|
+
convention.prefix,
|
|
414
|
+
convention.contentSuffix,
|
|
415
|
+
convention.roleSuffix,
|
|
416
|
+
]) {
|
|
417
|
+
expect(part.length).toBeGreaterThan(0);
|
|
418
|
+
expect(part.startsWith(".")).toBe(false);
|
|
419
|
+
expect(part.endsWith(".")).toBe(false);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
expect(convention.contentSuffix).not.toBe(convention.roleSuffix);
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
test("composes into the exact OpenLLMetry and OpenInference attribute keys", () => {
|
|
427
|
+
const compose: (
|
|
428
|
+
convention: LlmIndexedMessageConvention,
|
|
429
|
+
index: number,
|
|
430
|
+
) => { content: string; role: string } = (
|
|
431
|
+
convention: LlmIndexedMessageConvention,
|
|
432
|
+
index: number,
|
|
433
|
+
): { content: string; role: string } => {
|
|
434
|
+
return {
|
|
435
|
+
content: `${convention.prefix}.${index}.${convention.contentSuffix}`,
|
|
436
|
+
role: `${convention.prefix}.${index}.${convention.roleSuffix}`,
|
|
437
|
+
};
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
expect(compose(LlmPromptIndexedMessageConventions[0]!, 0)).toEqual({
|
|
441
|
+
content: "gen_ai.prompt.0.content",
|
|
442
|
+
role: "gen_ai.prompt.0.role",
|
|
443
|
+
});
|
|
444
|
+
expect(compose(LlmPromptIndexedMessageConventions[1]!, 2)).toEqual({
|
|
445
|
+
content: "llm.input_messages.2.message.content",
|
|
446
|
+
role: "llm.input_messages.2.message.role",
|
|
447
|
+
});
|
|
448
|
+
expect(compose(LlmCompletionIndexedMessageConventions[0]!, 1)).toEqual({
|
|
449
|
+
content: "gen_ai.completion.1.content",
|
|
450
|
+
role: "gen_ai.completion.1.role",
|
|
451
|
+
});
|
|
452
|
+
expect(compose(LlmCompletionIndexedMessageConventions[1]!, 0)).toEqual({
|
|
453
|
+
content: "llm.output_messages.0.message.content",
|
|
454
|
+
role: "llm.output_messages.0.message.role",
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test("prompt and completion conventions never share a prefix", () => {
|
|
459
|
+
const promptPrefixes: Set<string> = new Set(
|
|
460
|
+
LlmPromptIndexedMessageConventions.map(
|
|
461
|
+
(convention: LlmIndexedMessageConvention) => {
|
|
462
|
+
return convention.prefix;
|
|
463
|
+
},
|
|
464
|
+
),
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
for (const convention of LlmCompletionIndexedMessageConventions) {
|
|
468
|
+
expect(promptPrefixes.has(convention.prefix)).toBe(false);
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
describe("prompt vs completion content sources", () => {
|
|
474
|
+
test("JSON attribute keys for prompts and completions are disjoint", () => {
|
|
475
|
+
for (const key of LlmPromptJsonAttributeKeys) {
|
|
476
|
+
expect(LlmCompletionJsonAttributeKeys).not.toContain(key);
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
test("span-event names for prompts and completions are disjoint and all gen_ai.*", () => {
|
|
481
|
+
for (const name of LlmPromptEventNames) {
|
|
482
|
+
expect(LlmCompletionEventNames).not.toContain(name);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
for (const name of [...LlmPromptEventNames, ...LlmCompletionEventNames]) {
|
|
486
|
+
expect(name.startsWith("gen_ai.")).toBe(true);
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
test("the assistant's own message is a completion, never a prompt", () => {
|
|
491
|
+
expect(LlmCompletionEventNames).toContain("gen_ai.assistant.message");
|
|
492
|
+
expect(LlmPromptEventNames).not.toContain("gen_ai.assistant.message");
|
|
493
|
+
expect(LlmPromptEventNames).toContain("gen_ai.user.message");
|
|
494
|
+
});
|
|
495
|
+
});
|