@oneuptime/common 11.5.2 → 11.5.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.
Files changed (25) hide show
  1. package/Models/DatabaseModels/Dashboard.ts +3 -3
  2. package/Server/Infrastructure/Postgres/SchemaMigrations/1783937343400-MigrationName.ts +1123 -380
  3. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +1 -1
  4. package/Server/Services/DashboardService.ts +11 -2
  5. package/Server/Utils/Execute.ts +33 -13
  6. package/Server/Utils/Telemetry/LlmSpan.ts +38 -64
  7. package/Tests/Server/API/DashboardMasterPasswordAPI.test.ts +225 -0
  8. package/Types/Telemetry/LlmConventions.ts +182 -0
  9. package/UI/Components/Detail/PlaceholderText.tsx +1 -14
  10. package/build/dist/Models/DatabaseModels/Dashboard.js +3 -3
  11. package/build/dist/Models/DatabaseModels/Dashboard.js.map +1 -1
  12. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783937343400-MigrationName.js +1 -1
  13. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783937343400-MigrationName.js.map +1 -1
  14. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +1 -1
  15. package/build/dist/Server/Services/DashboardService.js +8 -2
  16. package/build/dist/Server/Services/DashboardService.js.map +1 -1
  17. package/build/dist/Server/Utils/Execute.js +21 -11
  18. package/build/dist/Server/Utils/Execute.js.map +1 -1
  19. package/build/dist/Server/Utils/Telemetry/LlmSpan.js +14 -54
  20. package/build/dist/Server/Utils/Telemetry/LlmSpan.js.map +1 -1
  21. package/build/dist/Types/Telemetry/LlmConventions.js +149 -0
  22. package/build/dist/Types/Telemetry/LlmConventions.js.map +1 -0
  23. package/build/dist/UI/Components/Detail/PlaceholderText.js +1 -4
  24. package/build/dist/UI/Components/Detail/PlaceholderText.js.map +1 -1
  25. package/package.json +1 -1
@@ -871,5 +871,5 @@ export default [
871
871
  AddIoTDeviceCredentialTable1783780000000,
872
872
  AddSnmpV3AuthColumnsToNetworkDevice1783790000000,
873
873
  AddAdditionalParamsToLlmProvider1783650000000,
874
- MigrationName1783937343400
874
+ MigrationName1783937343400,
875
875
  ];
@@ -214,11 +214,20 @@ export class Service extends DatabaseService<Model> {
214
214
  const shouldEnforceMasterPassword: boolean = Boolean(
215
215
  dashboard &&
216
216
  dashboard.isPublicDashboard &&
217
- dashboard.enableMasterPassword &&
218
- dashboard.masterPassword,
217
+ dashboard.enableMasterPassword,
219
218
  );
220
219
 
221
220
  if (shouldEnforceMasterPassword) {
221
+ // Fail closed if protection was enabled before a password was set.
222
+ if (!dashboard?.masterPassword) {
223
+ return {
224
+ hasReadAccess: false,
225
+ error: new MasterPasswordRequiredException(
226
+ DASHBOARD_MASTER_PASSWORD_REQUIRED_MESSAGE,
227
+ ),
228
+ };
229
+ }
230
+
222
231
  const hasValidMasterPassword: boolean =
223
232
  this.hasValidMasterPasswordCookie({
224
233
  req,
@@ -27,22 +27,30 @@ export default class Execute {
27
27
  {
28
28
  ...options,
29
29
  },
30
- (err: ExecException | null, stdout: string, stderr: string) => {
30
+ (
31
+ err: ExecException | null,
32
+ stdout: string | Buffer,
33
+ stderr: string | Buffer,
34
+ ) => {
35
+ // See executeCommandFile: string | Buffer for @types/node drift.
36
+ const stdoutText: string = stdout.toString();
37
+ const stderrText: string = stderr.toString();
38
+
31
39
  if (err) {
32
40
  logger.error(`Error executing command: ${command}`);
33
41
  logger.error(err);
34
- logger.error(stdout);
35
- if (stderr) {
36
- logger.error(stderr);
42
+ logger.error(stdoutText);
43
+ if (stderrText) {
44
+ logger.error(stderrText);
37
45
  }
38
46
  return reject(err);
39
47
  }
40
48
 
41
- if (stderr) {
42
- logger.debug(stderr);
49
+ if (stderrText) {
50
+ logger.debug(stderrText);
43
51
  }
44
52
 
45
- return resolve(stdout);
53
+ return resolve(stdoutText);
46
54
  },
47
55
  );
48
56
  },
@@ -81,22 +89,34 @@ export default class Execute {
81
89
  ? { timeout: data.timeoutInMS, killSignal: "SIGKILL" }
82
90
  : {}),
83
91
  },
84
- (err: ExecException | null, stdout: string, stderr: string) => {
92
+ (
93
+ err: ExecException | null,
94
+ stdout: string | Buffer,
95
+ stderr: string | Buffer,
96
+ ) => {
97
+ /*
98
+ * Newer @types/node type execFile's callback output as
99
+ * string | Buffer — coerce so this compiles on both the pinned
100
+ * and freshly-resolved typings (caret ranges drift in CI).
101
+ */
102
+ const stdoutText: string = stdout.toString();
103
+ const stderrText: string = stderr.toString();
104
+
85
105
  if (err) {
86
106
  logger.error(
87
107
  `Error executing command: ${data.command} ${data.args.join(" ")}`,
88
108
  );
89
109
  logger.error(err);
90
- logger.error(stdout);
91
- logger.error(stderr);
110
+ logger.error(stdoutText);
111
+ logger.error(stderrText);
92
112
  return reject(err);
93
113
  }
94
114
 
95
- if (stderr) {
96
- logger.debug(stderr);
115
+ if (stderrText) {
116
+ logger.debug(stderrText);
97
117
  }
98
118
 
99
- return resolve(stdout);
119
+ return resolve(stdoutText);
100
120
  },
101
121
  );
102
122
  },
@@ -1,4 +1,17 @@
1
1
  import Dictionary from "../../../Types/Dictionary";
2
+ import {
3
+ LlmAgentNameAttributeKeys,
4
+ LlmAttributeNamespacePrefixes,
5
+ LlmCostAttributeKeys,
6
+ LlmInputTokenAttributeKeys,
7
+ LlmOperationAttributeKeys,
8
+ LlmOutputTokenAttributeKeys,
9
+ LlmRequestModelAttributeKeys,
10
+ LlmResponseModelAttributeKeys,
11
+ LlmSystemAttributeKeys,
12
+ LlmToolNameAttributeKeys,
13
+ LlmTotalTokenAttributeKeys,
14
+ } from "../../../Types/Telemetry/LlmConventions";
2
15
  import { AttributeType } from "./Telemetry";
3
16
 
4
17
  /*
@@ -7,11 +20,9 @@ import { AttributeType } from "./Telemetry";
7
20
  * OneUptime ingests OpenTelemetry spans generically. To make LLM and agent
8
21
  * telemetry a first-class signal (filterable lists, token/cost/latency
9
22
  * rollups) we denormalize a small set of values out of the span attributes at
10
- * ingest time. We recognize the OpenTelemetry GenAI semantic conventions
11
- * (gen_ai.*) as primary, with cheap fallbacks for the two dominant
12
- * instrumentation libraries:
13
- * - OpenLLMetry / Traceloop (gen_ai.* + traceloop.*)
14
- * - OpenInference / Arize (llm.* + openinference.span.kind)
23
+ * ingest time. The set of recognized attribute keys lives in the shared
24
+ * Common/Types/Telemetry/LlmConventions module so this server-side extractor
25
+ * and the client-side display parser cannot drift out of sync.
15
26
  *
16
27
  * Prompt/completion CONTENT is intentionally NOT denormalized here — it stays
17
28
  * in the span's attributes/events map (already captured + scrubbed) and is
@@ -79,29 +90,19 @@ export default class LlmSpanUtil {
79
90
  return fields;
80
91
  }
81
92
 
82
- fields.llmSystem = this.getString(attributes, [
83
- "gen_ai.system",
84
- "gen_ai.provider.name",
85
- "llm.system",
86
- "llm.provider",
87
- ]);
88
-
89
- fields.llmOperation = this.getString(attributes, [
90
- "gen_ai.operation.name",
91
- "llm.request.type",
92
- "openinference.span.kind",
93
- ]);
94
-
95
- fields.llmRequestModel = this.getString(attributes, [
96
- "gen_ai.request.model",
97
- "llm.model_name",
98
- "llm.request.model",
99
- ]);
100
-
101
- fields.llmResponseModel = this.getString(attributes, [
102
- "gen_ai.response.model",
103
- "llm.response.model",
104
- ]);
93
+ fields.llmSystem = this.getString(attributes, LlmSystemAttributeKeys);
94
+
95
+ fields.llmOperation = this.getString(attributes, LlmOperationAttributeKeys);
96
+
97
+ fields.llmRequestModel = this.getString(
98
+ attributes,
99
+ LlmRequestModelAttributeKeys,
100
+ );
101
+
102
+ fields.llmResponseModel = this.getString(
103
+ attributes,
104
+ LlmResponseModelAttributeKeys,
105
+ );
105
106
 
106
107
  // Fall back to the response model when no request model was reported.
107
108
  if (!fields.llmRequestModel && fields.llmResponseModel) {
@@ -114,29 +115,15 @@ export default class LlmSpanUtil {
114
115
  * reject the row and fail the whole span batch.
115
116
  */
116
117
  fields.llmInputTokens = Math.trunc(
117
- this.getNumber(attributes, [
118
- "gen_ai.usage.input_tokens",
119
- "gen_ai.usage.prompt_tokens",
120
- "llm.token_count.prompt",
121
- "llm.usage.prompt_tokens",
122
- ]),
118
+ this.getNumber(attributes, LlmInputTokenAttributeKeys),
123
119
  );
124
120
 
125
121
  fields.llmOutputTokens = Math.trunc(
126
- this.getNumber(attributes, [
127
- "gen_ai.usage.output_tokens",
128
- "gen_ai.usage.completion_tokens",
129
- "llm.token_count.completion",
130
- "llm.usage.completion_tokens",
131
- ]),
122
+ this.getNumber(attributes, LlmOutputTokenAttributeKeys),
132
123
  );
133
124
 
134
125
  fields.llmTotalTokens = Math.trunc(
135
- this.getNumber(attributes, [
136
- "gen_ai.usage.total_tokens",
137
- "llm.token_count.total",
138
- "llm.usage.total_tokens",
139
- ]),
126
+ this.getNumber(attributes, LlmTotalTokenAttributeKeys),
140
127
  );
141
128
 
142
129
  // Derive total when only the parts were reported.
@@ -147,22 +134,11 @@ export default class LlmSpanUtil {
147
134
  fields.llmTotalTokens = fields.llmInputTokens + fields.llmOutputTokens;
148
135
  }
149
136
 
150
- fields.llmCost = this.getNumber(attributes, [
151
- "gen_ai.usage.cost",
152
- "gen_ai.usage.cost_usd",
153
- "gen_ai.usage.total_cost",
154
- "llm.usage.total_cost",
155
- ]);
137
+ fields.llmCost = this.getNumber(attributes, LlmCostAttributeKeys);
156
138
 
157
- fields.llmAgentName = this.getString(attributes, [
158
- "gen_ai.agent.name",
159
- "agent.name",
160
- ]);
139
+ fields.llmAgentName = this.getString(attributes, LlmAgentNameAttributeKeys);
161
140
 
162
- fields.llmToolName = this.getString(attributes, [
163
- "gen_ai.tool.name",
164
- "tool.name",
165
- ]);
141
+ fields.llmToolName = this.getString(attributes, LlmToolNameAttributeKeys);
166
142
 
167
143
  fields.isLlmSpan = this.detectIsLlmSpan(keys, fields);
168
144
 
@@ -187,11 +163,9 @@ export default class LlmSpanUtil {
187
163
 
188
164
  // Last-resort: any GenAI/LLM-namespaced attribute at all.
189
165
  return keys.some((key: string) => {
190
- return (
191
- key.startsWith("gen_ai.") ||
192
- key.startsWith("llm.") ||
193
- key.startsWith("traceloop.")
194
- );
166
+ return LlmAttributeNamespacePrefixes.some((prefix: string) => {
167
+ return key.startsWith(prefix);
168
+ });
195
169
  });
196
170
  }
197
171
 
@@ -0,0 +1,225 @@
1
+ import Dashboard from "../../../Models/DatabaseModels/Dashboard";
2
+ import DashboardAPI from "../../../Server/API/DashboardAPI";
3
+ import { EncryptionSecret } from "../../../Server/EnvironmentConfig";
4
+ import DashboardService from "../../../Server/Services/DashboardService";
5
+ import CookieUtil from "../../../Server/Utils/Cookie";
6
+ import {
7
+ ExpressRequest,
8
+ ExpressResponse,
9
+ NextFunction,
10
+ } from "../../../Server/Utils/Express";
11
+ import Response from "../../../Server/Utils/Response";
12
+ import {
13
+ DASHBOARD_MASTER_PASSWORD_INVALID_MESSAGE,
14
+ DASHBOARD_MASTER_PASSWORD_REQUIRED_MESSAGE,
15
+ } from "../../../Types/Dashboard/MasterPassword";
16
+ import BadDataException from "../../../Types/Exception/BadDataException";
17
+ import MasterPasswordRequiredException from "../../../Types/Exception/MasterPasswordRequiredException";
18
+ import HashedString from "../../../Types/HashedString";
19
+ import ObjectID from "../../../Types/ObjectID";
20
+ import { mockRouter } from "./Helpers";
21
+ import {
22
+ afterEach,
23
+ beforeAll,
24
+ beforeEach,
25
+ describe,
26
+ expect,
27
+ it,
28
+ } from "@jest/globals";
29
+
30
+ jest.mock("../../../Server/Utils/Express", () => {
31
+ return {
32
+ getRouter: () => {
33
+ return mockRouter;
34
+ },
35
+ };
36
+ });
37
+
38
+ jest.mock("../../../Server/Utils/Response", () => {
39
+ return {
40
+ sendEntityArrayResponse: jest.fn().mockImplementation((...args: []) => {
41
+ return args;
42
+ }),
43
+ sendJsonObjectResponse: jest.fn().mockImplementation((...args: []) => {
44
+ return args;
45
+ }),
46
+ sendEmptySuccessResponse: jest.fn(),
47
+ sendEntityResponse: jest.fn().mockImplementation((...args: []) => {
48
+ return args;
49
+ }),
50
+ sendErrorResponse: jest.fn().mockImplementation((...args: []) => {
51
+ return args;
52
+ }),
53
+ };
54
+ });
55
+
56
+ describe("DashboardAPI master password", () => {
57
+ const password: string = "correct horse battery staple";
58
+
59
+ let dashboardId: ObjectID;
60
+ let dashboard: Dashboard;
61
+ let mockRequest: ExpressRequest;
62
+ let mockResponse: ExpressResponse;
63
+ let nextFunction: NextFunction;
64
+
65
+ beforeAll(() => {
66
+ mockRouter.routes.length = 0;
67
+ new DashboardAPI();
68
+ });
69
+
70
+ beforeEach(async () => {
71
+ jest.clearAllMocks();
72
+
73
+ dashboardId = ObjectID.generate();
74
+ dashboard = new Dashboard();
75
+ dashboard.id = dashboardId;
76
+ dashboard.isPublicDashboard = true;
77
+ dashboard.enableMasterPassword = true;
78
+ dashboard.masterPassword = new HashedString(
79
+ await HashedString.hashValue(password, EncryptionSecret),
80
+ true,
81
+ );
82
+
83
+ jest.spyOn(DashboardService, "findOneById").mockResolvedValue(dashboard);
84
+
85
+ mockRequest = {
86
+ params: {
87
+ dashboardId: dashboardId.toString(),
88
+ },
89
+ body: {
90
+ password,
91
+ },
92
+ cookies: {},
93
+ headers: {},
94
+ socket: {},
95
+ ips: [],
96
+ } as unknown as ExpressRequest;
97
+
98
+ mockResponse = {
99
+ cookie: jest.fn(),
100
+ send: jest.fn(),
101
+ json: jest.fn(),
102
+ status: jest.fn().mockReturnThis(),
103
+ } as unknown as ExpressResponse;
104
+
105
+ nextFunction = jest.fn();
106
+ });
107
+
108
+ afterEach(() => {
109
+ jest.restoreAllMocks();
110
+ });
111
+
112
+ it("denies a protected public dashboard when no master-password cookie is present", async () => {
113
+ const result: Awaited<ReturnType<typeof DashboardService.hasReadAccess>> =
114
+ await DashboardService.hasReadAccess({
115
+ dashboardId,
116
+ req: mockRequest,
117
+ });
118
+
119
+ expect(result.hasReadAccess).toBe(false);
120
+ expect(result.error).toBeInstanceOf(MasterPasswordRequiredException);
121
+ expect(result.error?.message).toBe(
122
+ DASHBOARD_MASTER_PASSWORD_REQUIRED_MESSAGE,
123
+ );
124
+ });
125
+
126
+ it("fails closed when master-password protection is enabled without a stored password", async () => {
127
+ delete dashboard.masterPassword;
128
+
129
+ const result: Awaited<ReturnType<typeof DashboardService.hasReadAccess>> =
130
+ await DashboardService.hasReadAccess({
131
+ dashboardId,
132
+ req: mockRequest,
133
+ });
134
+
135
+ expect(result.hasReadAccess).toBe(false);
136
+ expect(result.error).toBeInstanceOf(MasterPasswordRequiredException);
137
+ expect(result.error?.message).toBe(
138
+ DASHBOARD_MASTER_PASSWORD_REQUIRED_MESSAGE,
139
+ );
140
+ });
141
+
142
+ it("issues a dashboard-scoped cookie for the correct password and unlocks only that dashboard", async () => {
143
+ await mockRouter
144
+ .match("post", "/dashboard/master-password/:dashboardId")
145
+ .handlerFunction(mockRequest, mockResponse, nextFunction);
146
+
147
+ expect(nextFunction).not.toHaveBeenCalled();
148
+ expect(Response.sendEmptySuccessResponse).toHaveBeenCalledWith(
149
+ mockRequest,
150
+ mockResponse,
151
+ );
152
+ expect(mockResponse.cookie).toHaveBeenCalledTimes(1);
153
+
154
+ const cookieCall: Array<unknown> = (mockResponse.cookie as jest.Mock).mock
155
+ .calls[0] as Array<unknown>;
156
+ const cookieName: string = cookieCall[0] as string;
157
+ const cookieToken: string = cookieCall[1] as string;
158
+
159
+ expect(cookieName).toBe(
160
+ CookieUtil.getDashboardMasterPasswordKey(dashboardId),
161
+ );
162
+ expect(cookieToken).toEqual(expect.any(String));
163
+
164
+ const unlockedRequest: ExpressRequest = {
165
+ cookies: {
166
+ [cookieName]: cookieToken,
167
+ },
168
+ headers: {},
169
+ socket: {},
170
+ ips: [],
171
+ } as unknown as ExpressRequest;
172
+
173
+ const unlockedResult: Awaited<
174
+ ReturnType<typeof DashboardService.hasReadAccess>
175
+ > = await DashboardService.hasReadAccess({
176
+ dashboardId,
177
+ req: unlockedRequest,
178
+ });
179
+
180
+ expect(unlockedResult.hasReadAccess).toBe(true);
181
+ expect(unlockedResult.error).toBeUndefined();
182
+
183
+ const otherDashboardId: ObjectID = ObjectID.generate();
184
+ const copiedCookieRequest: ExpressRequest = {
185
+ cookies: {
186
+ [CookieUtil.getDashboardMasterPasswordKey(otherDashboardId)]:
187
+ cookieToken,
188
+ },
189
+ headers: {},
190
+ socket: {},
191
+ ips: [],
192
+ } as unknown as ExpressRequest;
193
+
194
+ const isolatedResult: Awaited<
195
+ ReturnType<typeof DashboardService.hasReadAccess>
196
+ > = await DashboardService.hasReadAccess({
197
+ dashboardId: otherDashboardId,
198
+ req: copiedCookieRequest,
199
+ });
200
+
201
+ expect(isolatedResult.hasReadAccess).toBe(false);
202
+ expect(isolatedResult.error).toBeInstanceOf(
203
+ MasterPasswordRequiredException,
204
+ );
205
+ });
206
+
207
+ it("rejects an incorrect password without issuing a cookie", async () => {
208
+ mockRequest.body["password"] = "incorrect password";
209
+
210
+ await mockRouter
211
+ .match("post", "/dashboard/master-password/:dashboardId")
212
+ .handlerFunction(mockRequest, mockResponse, nextFunction);
213
+
214
+ expect(nextFunction).toHaveBeenCalledTimes(1);
215
+
216
+ const error: unknown = (nextFunction as jest.Mock).mock.calls[0]?.[0];
217
+
218
+ expect(error).toBeInstanceOf(BadDataException);
219
+ expect((error as BadDataException).message).toBe(
220
+ DASHBOARD_MASTER_PASSWORD_INVALID_MESSAGE,
221
+ );
222
+ expect(mockResponse.cookie).not.toHaveBeenCalled();
223
+ expect(Response.sendEmptySuccessResponse).not.toHaveBeenCalled();
224
+ });
225
+ });
@@ -0,0 +1,182 @@
1
+ /*
2
+ * Single source of truth for the OpenTelemetry GenAI (gen_ai.*) semantic
3
+ * convention attribute keys OneUptime recognizes when it detects and
4
+ * denormalizes LLM / GenAI / agent telemetry, plus cheap fallbacks for the two
5
+ * dominant instrumentation libraries:
6
+ * - OpenLLMetry / Traceloop (gen_ai.* + traceloop.*)
7
+ * - OpenInference / Arize (llm.* + openinference.span.kind)
8
+ *
9
+ * Both the server-side ingest extractor
10
+ * (Common/Server/Utils/Telemetry/LlmSpan.ts) and the client-side display parser
11
+ * (App/FeatureSet/Dashboard/src/Utils/LlmSpanDisplay.ts) import these lists so
12
+ * the two cannot silently drift apart when a new attribute is added — add a
13
+ * newly recognized key HERE, once.
14
+ *
15
+ * Order matters: within each list the preferred convention comes first and the
16
+ * lookup helpers return the first key that is present.
17
+ */
18
+
19
+ // Provider / system, e.g. "openai", "anthropic", "aws.bedrock".
20
+ export const LlmSystemAttributeKeys: Array<string> = [
21
+ "gen_ai.system",
22
+ "gen_ai.provider.name",
23
+ "llm.system",
24
+ "llm.provider",
25
+ ];
26
+
27
+ // Operation, e.g. "chat", "embeddings", "execute_tool", "invoke_agent".
28
+ export const LlmOperationAttributeKeys: Array<string> = [
29
+ "gen_ai.operation.name",
30
+ "llm.request.type",
31
+ "openinference.span.kind",
32
+ ];
33
+
34
+ // Model requested by the caller.
35
+ export const LlmRequestModelAttributeKeys: Array<string> = [
36
+ "gen_ai.request.model",
37
+ "llm.model_name",
38
+ "llm.request.model",
39
+ ];
40
+
41
+ // Model the provider actually served (often the resolved/pinned model).
42
+ export const LlmResponseModelAttributeKeys: Array<string> = [
43
+ "gen_ai.response.model",
44
+ "llm.response.model",
45
+ ];
46
+
47
+ export const LlmInputTokenAttributeKeys: Array<string> = [
48
+ "gen_ai.usage.input_tokens",
49
+ "gen_ai.usage.prompt_tokens",
50
+ "llm.token_count.prompt",
51
+ "llm.usage.prompt_tokens",
52
+ ];
53
+
54
+ export const LlmOutputTokenAttributeKeys: Array<string> = [
55
+ "gen_ai.usage.output_tokens",
56
+ "gen_ai.usage.completion_tokens",
57
+ "llm.token_count.completion",
58
+ "llm.usage.completion_tokens",
59
+ ];
60
+
61
+ export const LlmTotalTokenAttributeKeys: Array<string> = [
62
+ "gen_ai.usage.total_tokens",
63
+ "llm.token_count.total",
64
+ "llm.usage.total_tokens",
65
+ ];
66
+
67
+ // Cost in USD. Only populated when the SDK reports it (no built-in pricing).
68
+ export const LlmCostAttributeKeys: Array<string> = [
69
+ "gen_ai.usage.cost",
70
+ "gen_ai.usage.cost_usd",
71
+ "gen_ai.usage.total_cost",
72
+ "llm.usage.total_cost",
73
+ ];
74
+
75
+ export const LlmAgentNameAttributeKeys: Array<string> = [
76
+ "gen_ai.agent.name",
77
+ "agent.name",
78
+ ];
79
+
80
+ export const LlmToolNameAttributeKeys: Array<string> = [
81
+ "gen_ai.tool.name",
82
+ "tool.name",
83
+ ];
84
+
85
+ /*
86
+ * Request-parameter keys — surfaced only in the display panel, never
87
+ * denormalized to DB columns.
88
+ */
89
+ export const LlmTemperatureAttributeKeys: Array<string> = [
90
+ "gen_ai.request.temperature",
91
+ "llm.request.temperature",
92
+ ];
93
+
94
+ export const LlmMaxTokensAttributeKeys: Array<string> = [
95
+ "gen_ai.request.max_tokens",
96
+ "llm.request.max_tokens",
97
+ ];
98
+
99
+ export const LlmTopPAttributeKeys: Array<string> = [
100
+ "gen_ai.request.top_p",
101
+ "llm.request.top_p",
102
+ ];
103
+
104
+ export const LlmFinishReasonAttributeKeys: Array<string> = [
105
+ "gen_ai.response.finish_reasons",
106
+ "gen_ai.response.finish_reason",
107
+ "llm.response.finish_reason",
108
+ ];
109
+
110
+ /*
111
+ * Attribute-key namespace prefixes. Any span carrying an attribute in one of
112
+ * these namespaces is treated as an LLM/GenAI span as a last resort.
113
+ */
114
+ export const LlmAttributeNamespacePrefixes: Array<string> = [
115
+ "gen_ai.",
116
+ "llm.",
117
+ "traceloop.",
118
+ ];
119
+
120
+ /*
121
+ * Indexed prompt/completion message conventions of the shape
122
+ * `${prefix}.${i}.${contentSuffix}` / `${prefix}.${i}.${roleSuffix}`. Used only
123
+ * by the display parser to reconstruct message content for rendering.
124
+ */
125
+ export interface LlmIndexedMessageConvention {
126
+ prefix: string;
127
+ contentSuffix: string;
128
+ roleSuffix: string;
129
+ }
130
+
131
+ export const LlmPromptIndexedMessageConventions: Array<LlmIndexedMessageConvention> =
132
+ [
133
+ // OpenLLMetry indexed prompts.
134
+ { prefix: "gen_ai.prompt", contentSuffix: "content", roleSuffix: "role" },
135
+ // OpenInference indexed input messages.
136
+ {
137
+ prefix: "llm.input_messages",
138
+ contentSuffix: "message.content",
139
+ roleSuffix: "message.role",
140
+ },
141
+ ];
142
+
143
+ export const LlmCompletionIndexedMessageConventions: Array<LlmIndexedMessageConvention> =
144
+ [
145
+ // OpenLLMetry indexed completions.
146
+ {
147
+ prefix: "gen_ai.completion",
148
+ contentSuffix: "content",
149
+ roleSuffix: "role",
150
+ },
151
+ // OpenInference indexed output messages.
152
+ {
153
+ prefix: "llm.output_messages",
154
+ contentSuffix: "message.content",
155
+ roleSuffix: "message.role",
156
+ },
157
+ ];
158
+
159
+ // JSON-encoded message-array attribute keys (checked in order).
160
+ export const LlmPromptJsonAttributeKeys: Array<string> = [
161
+ "gen_ai.input.messages",
162
+ "gen_ai.prompt",
163
+ "input.value",
164
+ ];
165
+
166
+ export const LlmCompletionJsonAttributeKeys: Array<string> = [
167
+ "gen_ai.output.messages",
168
+ "gen_ai.completion",
169
+ "output.value",
170
+ ];
171
+
172
+ // Span-event names carrying prompt/completion content.
173
+ export const LlmPromptEventNames: Array<string> = [
174
+ "gen_ai.system.message",
175
+ "gen_ai.user.message",
176
+ "gen_ai.tool.message",
177
+ ];
178
+
179
+ export const LlmCompletionEventNames: Array<string> = [
180
+ "gen_ai.assistant.message",
181
+ "gen_ai.choice",
182
+ ];
@@ -11,20 +11,7 @@ const PlaceholderText: FunctionComponent<ComponentProps> = (
11
11
  const { translateString } = useTranslateValue();
12
12
  const translatedText: string = translateString(props.text) ?? props.text;
13
13
  return (
14
- <span className="inline-flex items-center gap-1.5 text-gray-400 italic text-sm select-none px-2 py-0.5 rounded-md bg-gray-50 border border-dashed border-gray-200">
15
- <svg
16
- className="w-3.5 h-3.5 text-gray-300"
17
- fill="none"
18
- viewBox="0 0 24 24"
19
- stroke="currentColor"
20
- >
21
- <path
22
- strokeLinecap="round"
23
- strokeLinejoin="round"
24
- strokeWidth={1.5}
25
- d="M20 12H4"
26
- />
27
- </svg>
14
+ <span className="inline-flex items-center whitespace-nowrap rounded-md border border-dashed border-gray-300 bg-gray-50 px-2 py-0.5 align-middle text-sm font-normal text-gray-500 select-none">
28
15
  {translatedText}
29
16
  </span>
30
17
  );