@kernl-sdk/lmnr 0.1.0
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/.turbo/turbo-build.log +5 -0
- package/dist/convert.d.ts +8 -0
- package/dist/convert.d.ts.map +1 -0
- package/dist/convert.js +62 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/subscriber.d.ts +87 -0
- package/dist/subscriber.d.ts.map +1 -0
- package/dist/subscriber.js +183 -0
- package/dist/utils.d.ts +18 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +43 -0
- package/package.json +55 -0
- package/src/convert.ts +74 -0
- package/src/index.ts +1 -0
- package/src/subscriber.ts +264 -0
- package/src/utils.ts +57 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Codec } from "@kernl-sdk/shared/lib";
|
|
2
|
+
import type { SpanData, EventData } from "kernl/tracing";
|
|
3
|
+
export type SpanType = "DEFAULT" | "LLM" | "TOOL";
|
|
4
|
+
export declare const SPAN_NAME: Codec<SpanData, string>;
|
|
5
|
+
export declare const SPAN_TYPE: Codec<SpanData["kind"], SpanType>;
|
|
6
|
+
export declare const SPAN_INPUT: Codec<SpanData, Record<string, unknown>>;
|
|
7
|
+
export declare const EVENT_ATTRIBUTES: Codec<EventData, Record<string, string | number | boolean>>;
|
|
8
|
+
//# sourceMappingURL=convert.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"convert.d.ts","sourceRoot":"","sources":["../src/convert.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAEzD,MAAM,MAAM,QAAQ,GAAG,SAAS,GAAG,KAAK,GAAG,MAAM,CAAC;AAElD,eAAO,MAAM,SAAS,EAAE,KAAK,CAAC,QAAQ,EAAE,MAAM,CAgB7C,CAAC;AAEF,eAAO,MAAM,SAAS,EAAE,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,CAgBvD,CAAC;AAEF,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAQ/D,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,KAAK,CAClC,SAAS,EACT,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAoB1C,CAAC"}
|
package/dist/convert.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export const SPAN_NAME = {
|
|
2
|
+
encode: (data) => {
|
|
3
|
+
switch (data.kind) {
|
|
4
|
+
case "thread":
|
|
5
|
+
return `thread.${data.agentId}`;
|
|
6
|
+
case "model.call":
|
|
7
|
+
return `model.${data.provider}.${data.modelId}`;
|
|
8
|
+
case "tool.call":
|
|
9
|
+
return `tool.${data.toolId}`;
|
|
10
|
+
default:
|
|
11
|
+
return `kernl.unknown`;
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
decode: () => {
|
|
15
|
+
throw new Error("SPAN_NAME.decode: unimplemented");
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
export const SPAN_TYPE = {
|
|
19
|
+
encode: (kind) => {
|
|
20
|
+
switch (kind) {
|
|
21
|
+
case "thread":
|
|
22
|
+
return "DEFAULT";
|
|
23
|
+
case "model.call":
|
|
24
|
+
return "LLM";
|
|
25
|
+
case "tool.call":
|
|
26
|
+
return "TOOL";
|
|
27
|
+
default:
|
|
28
|
+
return "DEFAULT";
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
decode: () => {
|
|
32
|
+
throw new Error("SPAN_TYPE.decode: unimplemented");
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
export const SPAN_INPUT = {
|
|
36
|
+
encode: (data) => {
|
|
37
|
+
const { kind, ...rest } = data;
|
|
38
|
+
return rest;
|
|
39
|
+
},
|
|
40
|
+
decode: () => {
|
|
41
|
+
throw new Error("SPAN_INPUT.decode: unimplemented");
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
export const EVENT_ATTRIBUTES = {
|
|
45
|
+
encode: (data) => {
|
|
46
|
+
const result = {};
|
|
47
|
+
for (const [key, value] of Object.entries(data)) {
|
|
48
|
+
if (typeof value === "string" ||
|
|
49
|
+
typeof value === "number" ||
|
|
50
|
+
typeof value === "boolean") {
|
|
51
|
+
result[key] = value;
|
|
52
|
+
}
|
|
53
|
+
else if (value !== undefined) {
|
|
54
|
+
result[key] = JSON.stringify(value);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
},
|
|
59
|
+
decode: () => {
|
|
60
|
+
throw new Error("EVENT_ATTRIBUTES.decode: unimplemented");
|
|
61
|
+
},
|
|
62
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,KAAK,oBAAoB,EAAE,MAAM,cAAc,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { LaminarTracer } from "./subscriber.js";
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { Subscriber, SpanId, SpanData, EventData } from "kernl/tracing";
|
|
2
|
+
export interface LaminarTracerOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Laminar project API key.
|
|
5
|
+
* If not provided, uses the LMNR_PROJECT_API_KEY environment variable.
|
|
6
|
+
*/
|
|
7
|
+
apiKey?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Base URL for the Laminar API.
|
|
10
|
+
* Defaults to https://api.lmnr.ai
|
|
11
|
+
*/
|
|
12
|
+
baseUrl?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Tags to apply to all spans.
|
|
15
|
+
*/
|
|
16
|
+
tags?: string[];
|
|
17
|
+
/**
|
|
18
|
+
* Metadata to apply to all spans.
|
|
19
|
+
*/
|
|
20
|
+
metadata?: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A Laminar tracer that exports spans and events to Laminar.
|
|
24
|
+
*
|
|
25
|
+
* Usage:
|
|
26
|
+
* ```ts
|
|
27
|
+
* import { Kernl } from 'kernl';
|
|
28
|
+
* import { LaminarTracer } from '@kernl-sdk/lmnr';
|
|
29
|
+
*
|
|
30
|
+
* const kernl = new Kernl({
|
|
31
|
+
* tracer: new LaminarTracer({
|
|
32
|
+
* apiKey: process.env.LMNR_PROJECT_API_KEY,
|
|
33
|
+
* }),
|
|
34
|
+
* });
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare class LaminarTracer implements Subscriber {
|
|
38
|
+
private spans;
|
|
39
|
+
private nextId;
|
|
40
|
+
private options;
|
|
41
|
+
constructor(options?: LaminarTracerOptions);
|
|
42
|
+
/**
|
|
43
|
+
* Check if a span should be recorded. Always returns true.
|
|
44
|
+
*/
|
|
45
|
+
enabled(_data: SpanData): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Create a new span and send it to Laminar.
|
|
48
|
+
* Sets gen_ai semantic convention attributes for LLM spans.
|
|
49
|
+
*/
|
|
50
|
+
span(data: SpanData, parent: SpanId | null): SpanId;
|
|
51
|
+
/**
|
|
52
|
+
* Called when entering a span's execution context.
|
|
53
|
+
* Laminar tracks timing from startSpan() to end(), so this is a no-op.
|
|
54
|
+
*/
|
|
55
|
+
enter(_spanId: SpanId): void;
|
|
56
|
+
/**
|
|
57
|
+
* Called when exiting a span's execution context.
|
|
58
|
+
* Laminar tracks timing from startSpan() to end(), so this is a no-op.
|
|
59
|
+
*/
|
|
60
|
+
exit(_spanId: SpanId): void;
|
|
61
|
+
/**
|
|
62
|
+
* Record additional data on an active span.
|
|
63
|
+
* Updates gen_ai semantic convention attributes for LLM responses.
|
|
64
|
+
*/
|
|
65
|
+
record(spanId: SpanId, delta: Partial<SpanData>): void;
|
|
66
|
+
/**
|
|
67
|
+
* Record an error on a span.
|
|
68
|
+
*/
|
|
69
|
+
error(spanId: SpanId, error: Error): void;
|
|
70
|
+
/**
|
|
71
|
+
* Close a span, ending its duration and sending it to Laminar.
|
|
72
|
+
*/
|
|
73
|
+
close(spanId: SpanId): void;
|
|
74
|
+
/**
|
|
75
|
+
* Emit a standalone event to Laminar.
|
|
76
|
+
*/
|
|
77
|
+
event(data: EventData, _parent: SpanId | null): void;
|
|
78
|
+
/**
|
|
79
|
+
* Flush pending spans to Laminar.
|
|
80
|
+
*/
|
|
81
|
+
flush(): Promise<void>;
|
|
82
|
+
/**
|
|
83
|
+
* Shutdown the tracer and flush remaining data.
|
|
84
|
+
*/
|
|
85
|
+
shutdown(_timeout?: number): Promise<void>;
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=subscriber.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subscriber.d.ts","sourceRoot":"","sources":["../src/subscriber.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,UAAU,EACV,MAAM,EACN,QAAQ,EACR,SAAS,EAIV,MAAM,eAAe,CAAC;AAgBvB,MAAM,WAAW,oBAAoB;IACnC;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAEhB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED;;;;;;;;;;;;;;GAcG;AACH,qBAAa,aAAc,YAAW,UAAU;IAC9C,OAAO,CAAC,KAAK,CAAiC;IAC9C,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,OAAO,CAAuB;gBAE1B,OAAO,GAAE,oBAAyB;IAqB9C;;OAEG;IACH,OAAO,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO;IAIjC;;;OAGG;IACH,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM;IAyCnD;;;OAGG;IACH,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAI5B;;;OAGG;IACH,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAE3B;;;OAGG;IACH,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,QAAQ,CAAC,GAAG,IAAI;IA6DtD;;OAEG;IACH,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,GAAG,IAAI;IAMzC;;OAEG;IACH,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAO3B;;OAEG;IACH,KAAK,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAOpD;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAI5B;;OAEG;IACG,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGjD"}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { Laminar } from "@lmnr-ai/lmnr";
|
|
2
|
+
import { SPAN_NAME, SPAN_TYPE, SPAN_INPUT, EVENT_ATTRIBUTES } from "./convert.js";
|
|
3
|
+
import { extractSessionId, extractUserId, setPromptAttributes, setCompletionAttributes, } from "./utils.js";
|
|
4
|
+
/**
|
|
5
|
+
* A Laminar tracer that exports spans and events to Laminar.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { Kernl } from 'kernl';
|
|
10
|
+
* import { LaminarTracer } from '@kernl-sdk/lmnr';
|
|
11
|
+
*
|
|
12
|
+
* const kernl = new Kernl({
|
|
13
|
+
* tracer: new LaminarTracer({
|
|
14
|
+
* apiKey: process.env.LMNR_PROJECT_API_KEY,
|
|
15
|
+
* }),
|
|
16
|
+
* });
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export class LaminarTracer {
|
|
20
|
+
spans = new Map();
|
|
21
|
+
nextId = 0;
|
|
22
|
+
options;
|
|
23
|
+
constructor(options = {}) {
|
|
24
|
+
this.options = options;
|
|
25
|
+
const apiKey = options.apiKey ?? process.env.LMNR_PROJECT_API_KEY;
|
|
26
|
+
if (!apiKey) {
|
|
27
|
+
throw new Error("LaminarTracer requires an API key. " +
|
|
28
|
+
"Pass { apiKey: '...' } or set LMNR_PROJECT_API_KEY environment variable.");
|
|
29
|
+
}
|
|
30
|
+
// Initialize Laminar if not already initialized
|
|
31
|
+
if (!Laminar.initialized()) {
|
|
32
|
+
Laminar.initialize({
|
|
33
|
+
projectApiKey: apiKey,
|
|
34
|
+
baseUrl: options.baseUrl,
|
|
35
|
+
instrumentModules: {}, // disable auto-instrumentation
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Check if a span should be recorded. Always returns true.
|
|
41
|
+
*/
|
|
42
|
+
enabled(_data) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Create a new span and send it to Laminar.
|
|
47
|
+
* Sets gen_ai semantic convention attributes for LLM spans.
|
|
48
|
+
*/
|
|
49
|
+
span(data, parent) {
|
|
50
|
+
const id = `span_${this.nextId++}`;
|
|
51
|
+
// get parent context for linking
|
|
52
|
+
const parentRecord = parent ? this.spans.get(parent) : null;
|
|
53
|
+
const parentSpanContext = parentRecord?.context ?? undefined;
|
|
54
|
+
// extract session/user from thread context
|
|
55
|
+
const sessionId = extractSessionId(data);
|
|
56
|
+
const userId = extractUserId(data);
|
|
57
|
+
// create the Laminar span
|
|
58
|
+
const s = Laminar.startSpan({
|
|
59
|
+
name: SPAN_NAME.encode(data),
|
|
60
|
+
spanType: SPAN_TYPE.encode(data.kind),
|
|
61
|
+
input: SPAN_INPUT.encode(data),
|
|
62
|
+
parentSpanContext,
|
|
63
|
+
tags: this.options.tags,
|
|
64
|
+
sessionId,
|
|
65
|
+
userId,
|
|
66
|
+
metadata: this.options.metadata,
|
|
67
|
+
});
|
|
68
|
+
// set gen_ai semantic convention attributes for LLM spans
|
|
69
|
+
if (data.kind === "model.call") {
|
|
70
|
+
s.setAttribute("gen_ai.system", data.provider);
|
|
71
|
+
s.setAttribute("gen_ai.request.model", data.modelId);
|
|
72
|
+
// Format input messages for native UI
|
|
73
|
+
if (data.request?.input) {
|
|
74
|
+
setPromptAttributes(s, data.request.input);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// get context for future children
|
|
78
|
+
const context = Laminar.getLaminarSpanContext(s);
|
|
79
|
+
this.spans.set(id, { span: s, data, context });
|
|
80
|
+
return id;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Called when entering a span's execution context.
|
|
84
|
+
* Laminar tracks timing from startSpan() to end(), so this is a no-op.
|
|
85
|
+
*/
|
|
86
|
+
enter(_spanId) {
|
|
87
|
+
// Laminar tracks timing from startSpan() to end()
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Called when exiting a span's execution context.
|
|
91
|
+
* Laminar tracks timing from startSpan() to end(), so this is a no-op.
|
|
92
|
+
*/
|
|
93
|
+
exit(_spanId) { }
|
|
94
|
+
/**
|
|
95
|
+
* Record additional data on an active span.
|
|
96
|
+
* Updates gen_ai semantic convention attributes for LLM responses.
|
|
97
|
+
*/
|
|
98
|
+
record(spanId, delta) {
|
|
99
|
+
const record = this.spans.get(spanId);
|
|
100
|
+
if (!record)
|
|
101
|
+
return;
|
|
102
|
+
Object.assign(record.data, delta);
|
|
103
|
+
switch (record.data.kind) {
|
|
104
|
+
case "thread": {
|
|
105
|
+
const d = delta;
|
|
106
|
+
if (d.state) {
|
|
107
|
+
record.span.setAttribute("thread.state", JSON.stringify(d.state));
|
|
108
|
+
}
|
|
109
|
+
if (d.result !== undefined) {
|
|
110
|
+
record.span.setAttribute("lmnr.span.output", JSON.stringify(d.result));
|
|
111
|
+
}
|
|
112
|
+
if (d.error) {
|
|
113
|
+
record.span.setAttribute("thread.error", d.error);
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
case "model.call": {
|
|
118
|
+
const d = delta;
|
|
119
|
+
if (!d.response)
|
|
120
|
+
break;
|
|
121
|
+
if (d.response.usage) {
|
|
122
|
+
record.span.setAttribute("gen_ai.usage.input_tokens", d.response.usage.inputTokens?.total ?? 0);
|
|
123
|
+
record.span.setAttribute("gen_ai.usage.output_tokens", d.response.usage.outputTokens?.total ?? 0);
|
|
124
|
+
}
|
|
125
|
+
if (d.response.content) {
|
|
126
|
+
setCompletionAttributes(record.span, d.response.content);
|
|
127
|
+
record.span.setAttribute("lmnr.span.output", JSON.stringify(d.response.content));
|
|
128
|
+
}
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
case "tool.call": {
|
|
132
|
+
const d = delta;
|
|
133
|
+
if (d.result !== undefined) {
|
|
134
|
+
record.span.setAttribute("lmnr.span.output", JSON.stringify(d.result));
|
|
135
|
+
}
|
|
136
|
+
if (d.error) {
|
|
137
|
+
record.span.setAttribute("tool.call.error", d.error);
|
|
138
|
+
}
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Record an error on a span.
|
|
145
|
+
*/
|
|
146
|
+
error(spanId, error) {
|
|
147
|
+
const record = this.spans.get(spanId);
|
|
148
|
+
if (!record)
|
|
149
|
+
return;
|
|
150
|
+
record.span.recordException(error);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Close a span, ending its duration and sending it to Laminar.
|
|
154
|
+
*/
|
|
155
|
+
close(spanId) {
|
|
156
|
+
const record = this.spans.get(spanId);
|
|
157
|
+
if (!record)
|
|
158
|
+
return;
|
|
159
|
+
record.span.end();
|
|
160
|
+
this.spans.delete(spanId);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Emit a standalone event to Laminar.
|
|
164
|
+
*/
|
|
165
|
+
event(data, _parent) {
|
|
166
|
+
Laminar.event({
|
|
167
|
+
name: data.kind,
|
|
168
|
+
attributes: EVENT_ATTRIBUTES.encode(data),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Flush pending spans to Laminar.
|
|
173
|
+
*/
|
|
174
|
+
async flush() {
|
|
175
|
+
await Laminar.flush();
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Shutdown the tracer and flush remaining data.
|
|
179
|
+
*/
|
|
180
|
+
async shutdown(_timeout) {
|
|
181
|
+
await Laminar.shutdown();
|
|
182
|
+
}
|
|
183
|
+
}
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Span } from "@lmnr-ai/lmnr";
|
|
2
|
+
import type { SpanData } from "kernl/tracing";
|
|
3
|
+
export declare function extractSessionId(data: SpanData): string | undefined;
|
|
4
|
+
export declare function extractUserId(data: SpanData): string | undefined;
|
|
5
|
+
export declare function setPromptAttributes(span: Span, items: Array<{
|
|
6
|
+
kind: string;
|
|
7
|
+
role?: string;
|
|
8
|
+
content?: Array<{
|
|
9
|
+
kind: string;
|
|
10
|
+
text?: string;
|
|
11
|
+
}>;
|
|
12
|
+
}>): void;
|
|
13
|
+
export declare function setCompletionAttributes(span: Span, items: Array<{
|
|
14
|
+
kind: string;
|
|
15
|
+
role?: string;
|
|
16
|
+
text?: string;
|
|
17
|
+
}>): void;
|
|
18
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAC1C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,GAAG,SAAS,CAMnE;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,GAAG,SAAS,CAMhE;AAED,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,KAAK,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAClD,CAAC,GACD,IAAI,CAaN;AAED,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GAC3D,IAAI,CAYN"}
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export function extractSessionId(data) {
|
|
2
|
+
if (data.kind === "thread" && data.context) {
|
|
3
|
+
const ctx = data.context;
|
|
4
|
+
if (typeof ctx.sessionId === "string")
|
|
5
|
+
return ctx.sessionId;
|
|
6
|
+
}
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
export function extractUserId(data) {
|
|
10
|
+
if (data.kind === "thread" && data.context) {
|
|
11
|
+
const ctx = data.context;
|
|
12
|
+
if (typeof ctx.userId === "string")
|
|
13
|
+
return ctx.userId;
|
|
14
|
+
}
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
export function setPromptAttributes(span, items) {
|
|
18
|
+
let idx = 0;
|
|
19
|
+
for (const item of items) {
|
|
20
|
+
if (item.kind === "message" && item.role && item.content) {
|
|
21
|
+
span.setAttribute(`gen_ai.prompt.${idx}.role`, item.role);
|
|
22
|
+
const content = item.content
|
|
23
|
+
.filter((p) => p.kind === "text")
|
|
24
|
+
.map((p) => p.text)
|
|
25
|
+
.join("");
|
|
26
|
+
span.setAttribute(`gen_ai.prompt.${idx}.content`, content);
|
|
27
|
+
idx++;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function setCompletionAttributes(span, items) {
|
|
32
|
+
let idx = 0;
|
|
33
|
+
for (const item of items) {
|
|
34
|
+
if (item.kind === "message" && item.role) {
|
|
35
|
+
span.setAttribute(`gen_ai.completion.${idx}.role`, item.role);
|
|
36
|
+
idx++;
|
|
37
|
+
}
|
|
38
|
+
if ((item.kind === "text" || item.kind === "reasoning") && item.text) {
|
|
39
|
+
span.setAttribute(`gen_ai.completion.${idx}.content`, item.text);
|
|
40
|
+
idx++;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kernl-sdk/lmnr",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Laminar observability integration for kernl",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"kernl",
|
|
7
|
+
"laminar",
|
|
8
|
+
"tracing",
|
|
9
|
+
"observability",
|
|
10
|
+
"llm"
|
|
11
|
+
],
|
|
12
|
+
"author": "dremnik",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/kernl-sdk/kernl.git",
|
|
17
|
+
"directory": "packages/observability/laminar"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/kernl-sdk/kernl#readme",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/kernl-sdk/kernl/issues"
|
|
22
|
+
},
|
|
23
|
+
"type": "module",
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"import": "./dist/index.js"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsc && tsc-alias --resolve-full-paths",
|
|
35
|
+
"dev": "tsc --watch",
|
|
36
|
+
"check-types": "tsc --noEmit",
|
|
37
|
+
"test": "vitest",
|
|
38
|
+
"test:watch": "vitest --watch",
|
|
39
|
+
"test:run": "vitest run"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@kernl-sdk/shared": "workspace:*",
|
|
43
|
+
"kernl": "workspace:*"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"@lmnr-ai/lmnr": ">=0.8.0"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@lmnr-ai/lmnr": "^0.8.6",
|
|
50
|
+
"@types/node": "^24.10.0",
|
|
51
|
+
"tsc-alias": "^1.8.10",
|
|
52
|
+
"typescript": "5.9.2",
|
|
53
|
+
"vitest": "^4.0.8"
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/convert.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { Codec } from "@kernl-sdk/shared/lib";
|
|
2
|
+
import type { SpanData, EventData } from "kernl/tracing";
|
|
3
|
+
|
|
4
|
+
export type SpanType = "DEFAULT" | "LLM" | "TOOL";
|
|
5
|
+
|
|
6
|
+
export const SPAN_NAME: Codec<SpanData, string> = {
|
|
7
|
+
encode: (data) => {
|
|
8
|
+
switch (data.kind) {
|
|
9
|
+
case "thread":
|
|
10
|
+
return `thread.${data.agentId}`;
|
|
11
|
+
case "model.call":
|
|
12
|
+
return `model.${data.provider}.${data.modelId}`;
|
|
13
|
+
case "tool.call":
|
|
14
|
+
return `tool.${data.toolId}`;
|
|
15
|
+
default:
|
|
16
|
+
return `kernl.unknown`;
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
decode: () => {
|
|
20
|
+
throw new Error("SPAN_NAME.decode: unimplemented");
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const SPAN_TYPE: Codec<SpanData["kind"], SpanType> = {
|
|
25
|
+
encode: (kind) => {
|
|
26
|
+
switch (kind) {
|
|
27
|
+
case "thread":
|
|
28
|
+
return "DEFAULT";
|
|
29
|
+
case "model.call":
|
|
30
|
+
return "LLM";
|
|
31
|
+
case "tool.call":
|
|
32
|
+
return "TOOL";
|
|
33
|
+
default:
|
|
34
|
+
return "DEFAULT";
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
decode: () => {
|
|
38
|
+
throw new Error("SPAN_TYPE.decode: unimplemented");
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const SPAN_INPUT: Codec<SpanData, Record<string, unknown>> = {
|
|
43
|
+
encode: (data) => {
|
|
44
|
+
const { kind, ...rest } = data;
|
|
45
|
+
return rest;
|
|
46
|
+
},
|
|
47
|
+
decode: () => {
|
|
48
|
+
throw new Error("SPAN_INPUT.decode: unimplemented");
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const EVENT_ATTRIBUTES: Codec<
|
|
53
|
+
EventData,
|
|
54
|
+
Record<string, string | number | boolean>
|
|
55
|
+
> = {
|
|
56
|
+
encode: (data) => {
|
|
57
|
+
const result: Record<string, string | number | boolean> = {};
|
|
58
|
+
for (const [key, value] of Object.entries(data)) {
|
|
59
|
+
if (
|
|
60
|
+
typeof value === "string" ||
|
|
61
|
+
typeof value === "number" ||
|
|
62
|
+
typeof value === "boolean"
|
|
63
|
+
) {
|
|
64
|
+
result[key] = value;
|
|
65
|
+
} else if (value !== undefined) {
|
|
66
|
+
result[key] = JSON.stringify(value);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
},
|
|
71
|
+
decode: () => {
|
|
72
|
+
throw new Error("EVENT_ATTRIBUTES.decode: unimplemented");
|
|
73
|
+
},
|
|
74
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { LaminarTracer, type LaminarTracerOptions } from "./subscriber";
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { Laminar, type LaminarSpanContext, type Span } from "@lmnr-ai/lmnr";
|
|
2
|
+
import type {
|
|
3
|
+
Subscriber,
|
|
4
|
+
SpanId,
|
|
5
|
+
SpanData,
|
|
6
|
+
EventData,
|
|
7
|
+
ModelCallSpan,
|
|
8
|
+
ToolCallSpan,
|
|
9
|
+
ThreadSpan,
|
|
10
|
+
} from "kernl/tracing";
|
|
11
|
+
|
|
12
|
+
import { SPAN_NAME, SPAN_TYPE, SPAN_INPUT, EVENT_ATTRIBUTES } from "./convert";
|
|
13
|
+
import {
|
|
14
|
+
extractSessionId,
|
|
15
|
+
extractUserId,
|
|
16
|
+
setPromptAttributes,
|
|
17
|
+
setCompletionAttributes,
|
|
18
|
+
} from "./utils";
|
|
19
|
+
|
|
20
|
+
interface SpanRecord {
|
|
21
|
+
span: Span;
|
|
22
|
+
data: SpanData;
|
|
23
|
+
context: LaminarSpanContext | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface LaminarTracerOptions {
|
|
27
|
+
/**
|
|
28
|
+
* Laminar project API key.
|
|
29
|
+
* If not provided, uses the LMNR_PROJECT_API_KEY environment variable.
|
|
30
|
+
*/
|
|
31
|
+
apiKey?: string;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Base URL for the Laminar API.
|
|
35
|
+
* Defaults to https://api.lmnr.ai
|
|
36
|
+
*/
|
|
37
|
+
baseUrl?: string;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Tags to apply to all spans.
|
|
41
|
+
*/
|
|
42
|
+
tags?: string[];
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Metadata to apply to all spans.
|
|
46
|
+
*/
|
|
47
|
+
metadata?: Record<string, unknown>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A Laminar tracer that exports spans and events to Laminar.
|
|
52
|
+
*
|
|
53
|
+
* Usage:
|
|
54
|
+
* ```ts
|
|
55
|
+
* import { Kernl } from 'kernl';
|
|
56
|
+
* import { LaminarTracer } from '@kernl-sdk/lmnr';
|
|
57
|
+
*
|
|
58
|
+
* const kernl = new Kernl({
|
|
59
|
+
* tracer: new LaminarTracer({
|
|
60
|
+
* apiKey: process.env.LMNR_PROJECT_API_KEY,
|
|
61
|
+
* }),
|
|
62
|
+
* });
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export class LaminarTracer implements Subscriber {
|
|
66
|
+
private spans = new Map<SpanId, SpanRecord>();
|
|
67
|
+
private nextId = 0;
|
|
68
|
+
private options: LaminarTracerOptions;
|
|
69
|
+
|
|
70
|
+
constructor(options: LaminarTracerOptions = {}) {
|
|
71
|
+
this.options = options;
|
|
72
|
+
|
|
73
|
+
const apiKey = options.apiKey ?? process.env.LMNR_PROJECT_API_KEY;
|
|
74
|
+
if (!apiKey) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
"LaminarTracer requires an API key. " +
|
|
77
|
+
"Pass { apiKey: '...' } or set LMNR_PROJECT_API_KEY environment variable.",
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Initialize Laminar if not already initialized
|
|
82
|
+
if (!Laminar.initialized()) {
|
|
83
|
+
Laminar.initialize({
|
|
84
|
+
projectApiKey: apiKey,
|
|
85
|
+
baseUrl: options.baseUrl,
|
|
86
|
+
instrumentModules: {}, // disable auto-instrumentation
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Check if a span should be recorded. Always returns true.
|
|
93
|
+
*/
|
|
94
|
+
enabled(_data: SpanData): boolean {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Create a new span and send it to Laminar.
|
|
100
|
+
* Sets gen_ai semantic convention attributes for LLM spans.
|
|
101
|
+
*/
|
|
102
|
+
span(data: SpanData, parent: SpanId | null): SpanId {
|
|
103
|
+
const id = `span_${this.nextId++}`;
|
|
104
|
+
|
|
105
|
+
// get parent context for linking
|
|
106
|
+
const parentRecord = parent ? this.spans.get(parent) : null;
|
|
107
|
+
const parentSpanContext = parentRecord?.context ?? undefined;
|
|
108
|
+
|
|
109
|
+
// extract session/user from thread context
|
|
110
|
+
const sessionId = extractSessionId(data);
|
|
111
|
+
const userId = extractUserId(data);
|
|
112
|
+
|
|
113
|
+
// create the Laminar span
|
|
114
|
+
const s = Laminar.startSpan({
|
|
115
|
+
name: SPAN_NAME.encode(data),
|
|
116
|
+
spanType: SPAN_TYPE.encode(data.kind),
|
|
117
|
+
input: SPAN_INPUT.encode(data),
|
|
118
|
+
parentSpanContext,
|
|
119
|
+
tags: this.options.tags,
|
|
120
|
+
sessionId,
|
|
121
|
+
userId,
|
|
122
|
+
metadata: this.options.metadata as Record<string, any>,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// set gen_ai semantic convention attributes for LLM spans
|
|
126
|
+
if (data.kind === "model.call") {
|
|
127
|
+
s.setAttribute("gen_ai.system", data.provider);
|
|
128
|
+
s.setAttribute("gen_ai.request.model", data.modelId);
|
|
129
|
+
|
|
130
|
+
// Format input messages for native UI
|
|
131
|
+
if (data.request?.input) {
|
|
132
|
+
setPromptAttributes(s, data.request.input);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// get context for future children
|
|
137
|
+
const context = Laminar.getLaminarSpanContext(s);
|
|
138
|
+
|
|
139
|
+
this.spans.set(id, { span: s, data, context });
|
|
140
|
+
return id;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Called when entering a span's execution context.
|
|
145
|
+
* Laminar tracks timing from startSpan() to end(), so this is a no-op.
|
|
146
|
+
*/
|
|
147
|
+
enter(_spanId: SpanId): void {
|
|
148
|
+
// Laminar tracks timing from startSpan() to end()
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Called when exiting a span's execution context.
|
|
153
|
+
* Laminar tracks timing from startSpan() to end(), so this is a no-op.
|
|
154
|
+
*/
|
|
155
|
+
exit(_spanId: SpanId): void {}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Record additional data on an active span.
|
|
159
|
+
* Updates gen_ai semantic convention attributes for LLM responses.
|
|
160
|
+
*/
|
|
161
|
+
record(spanId: SpanId, delta: Partial<SpanData>): void {
|
|
162
|
+
const record = this.spans.get(spanId);
|
|
163
|
+
if (!record) return;
|
|
164
|
+
|
|
165
|
+
Object.assign(record.data, delta);
|
|
166
|
+
|
|
167
|
+
switch (record.data.kind) {
|
|
168
|
+
case "thread": {
|
|
169
|
+
const d = delta as Partial<ThreadSpan>;
|
|
170
|
+
if (d.state) {
|
|
171
|
+
record.span.setAttribute("thread.state", JSON.stringify(d.state));
|
|
172
|
+
}
|
|
173
|
+
if (d.result !== undefined) {
|
|
174
|
+
record.span.setAttribute(
|
|
175
|
+
"lmnr.span.output",
|
|
176
|
+
JSON.stringify(d.result),
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
if (d.error) {
|
|
180
|
+
record.span.setAttribute("thread.error", d.error);
|
|
181
|
+
}
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
case "model.call": {
|
|
185
|
+
const d = delta as Partial<ModelCallSpan>;
|
|
186
|
+
if (!d.response) break;
|
|
187
|
+
if (d.response.usage) {
|
|
188
|
+
record.span.setAttribute(
|
|
189
|
+
"gen_ai.usage.input_tokens",
|
|
190
|
+
d.response.usage.inputTokens?.total ?? 0,
|
|
191
|
+
);
|
|
192
|
+
record.span.setAttribute(
|
|
193
|
+
"gen_ai.usage.output_tokens",
|
|
194
|
+
d.response.usage.outputTokens?.total ?? 0,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
if (d.response.content) {
|
|
198
|
+
setCompletionAttributes(record.span, d.response.content);
|
|
199
|
+
record.span.setAttribute(
|
|
200
|
+
"lmnr.span.output",
|
|
201
|
+
JSON.stringify(d.response.content),
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
case "tool.call": {
|
|
207
|
+
const d = delta as Partial<ToolCallSpan>;
|
|
208
|
+
if (d.result !== undefined) {
|
|
209
|
+
record.span.setAttribute(
|
|
210
|
+
"lmnr.span.output",
|
|
211
|
+
JSON.stringify(d.result),
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
if (d.error) {
|
|
215
|
+
record.span.setAttribute("tool.call.error", d.error);
|
|
216
|
+
}
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Record an error on a span.
|
|
224
|
+
*/
|
|
225
|
+
error(spanId: SpanId, error: Error): void {
|
|
226
|
+
const record = this.spans.get(spanId);
|
|
227
|
+
if (!record) return;
|
|
228
|
+
record.span.recordException(error);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Close a span, ending its duration and sending it to Laminar.
|
|
233
|
+
*/
|
|
234
|
+
close(spanId: SpanId): void {
|
|
235
|
+
const record = this.spans.get(spanId);
|
|
236
|
+
if (!record) return;
|
|
237
|
+
record.span.end();
|
|
238
|
+
this.spans.delete(spanId);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Emit a standalone event to Laminar.
|
|
243
|
+
*/
|
|
244
|
+
event(data: EventData, _parent: SpanId | null): void {
|
|
245
|
+
Laminar.event({
|
|
246
|
+
name: data.kind,
|
|
247
|
+
attributes: EVENT_ATTRIBUTES.encode(data),
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Flush pending spans to Laminar.
|
|
253
|
+
*/
|
|
254
|
+
async flush(): Promise<void> {
|
|
255
|
+
await Laminar.flush();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Shutdown the tracer and flush remaining data.
|
|
260
|
+
*/
|
|
261
|
+
async shutdown(_timeout?: number): Promise<void> {
|
|
262
|
+
await Laminar.shutdown();
|
|
263
|
+
}
|
|
264
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Span } from "@lmnr-ai/lmnr";
|
|
2
|
+
import type { SpanData } from "kernl/tracing";
|
|
3
|
+
|
|
4
|
+
export function extractSessionId(data: SpanData): string | undefined {
|
|
5
|
+
if (data.kind === "thread" && data.context) {
|
|
6
|
+
const ctx = data.context as Record<string, unknown>;
|
|
7
|
+
if (typeof ctx.sessionId === "string") return ctx.sessionId;
|
|
8
|
+
}
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function extractUserId(data: SpanData): string | undefined {
|
|
13
|
+
if (data.kind === "thread" && data.context) {
|
|
14
|
+
const ctx = data.context as Record<string, unknown>;
|
|
15
|
+
if (typeof ctx.userId === "string") return ctx.userId;
|
|
16
|
+
}
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function setPromptAttributes(
|
|
21
|
+
span: Span,
|
|
22
|
+
items: Array<{
|
|
23
|
+
kind: string;
|
|
24
|
+
role?: string;
|
|
25
|
+
content?: Array<{ kind: string; text?: string }>;
|
|
26
|
+
}>,
|
|
27
|
+
): void {
|
|
28
|
+
let idx = 0;
|
|
29
|
+
for (const item of items) {
|
|
30
|
+
if (item.kind === "message" && item.role && item.content) {
|
|
31
|
+
span.setAttribute(`gen_ai.prompt.${idx}.role`, item.role);
|
|
32
|
+
const content = item.content
|
|
33
|
+
.filter((p): p is { kind: "text"; text: string } => p.kind === "text")
|
|
34
|
+
.map((p) => p.text)
|
|
35
|
+
.join("");
|
|
36
|
+
span.setAttribute(`gen_ai.prompt.${idx}.content`, content);
|
|
37
|
+
idx++;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function setCompletionAttributes(
|
|
43
|
+
span: Span,
|
|
44
|
+
items: Array<{ kind: string; role?: string; text?: string }>,
|
|
45
|
+
): void {
|
|
46
|
+
let idx = 0;
|
|
47
|
+
for (const item of items) {
|
|
48
|
+
if (item.kind === "message" && item.role) {
|
|
49
|
+
span.setAttribute(`gen_ai.completion.${idx}.role`, item.role);
|
|
50
|
+
idx++;
|
|
51
|
+
}
|
|
52
|
+
if ((item.kind === "text" || item.kind === "reasoning") && item.text) {
|
|
53
|
+
span.setAttribute(`gen_ai.completion.${idx}.content`, item.text);
|
|
54
|
+
idx++;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
package/tsconfig.json
ADDED