@lunardinos/server 0.2.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/dist/index.js ADDED
@@ -0,0 +1,212 @@
1
+ // src/init.ts
2
+ import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
3
+
4
+ // src/costs.ts
5
+ var MODEL_PRICES = {
6
+ "gpt-4o": { input: 250, output: 1000 },
7
+ "gpt-4o-mini": { input: 15, output: 60 },
8
+ "gpt-4-turbo": { input: 1000, output: 3000 },
9
+ "gpt-4.1": { input: 200, output: 800 },
10
+ "gpt-4.1-mini": { input: 40, output: 160 },
11
+ "gpt-4.1-nano": { input: 10, output: 40 },
12
+ o3: { input: 1000, output: 4000 },
13
+ "o3-mini": { input: 110, output: 440 },
14
+ "o4-mini": { input: 110, output: 440 },
15
+ "claude-opus-4-6": { input: 1500, output: 7500 },
16
+ "claude-sonnet-4-6": { input: 300, output: 1500 },
17
+ "claude-haiku-4-5-20251001": { input: 80, output: 400 },
18
+ "gemini-2.5-pro": { input: 125, output: 1000 },
19
+ "gemini-2.5-flash": { input: 15, output: 60 }
20
+ };
21
+ function estimateCost(model, inputTokens, outputTokens) {
22
+ const prices = MODEL_PRICES[model] ?? Object.entries(MODEL_PRICES).find(([key]) => model.startsWith(key))?.[1];
23
+ if (!prices)
24
+ return null;
25
+ return inputTokens / 1e6 * prices.input + outputTokens / 1e6 * prices.output;
26
+ }
27
+
28
+ // src/processor.ts
29
+ var ROOT_OPERATIONS = new Set([
30
+ "ai.generateText",
31
+ "ai.streamText",
32
+ "ai.generateObject",
33
+ "ai.streamObject",
34
+ "ai.embed",
35
+ "ai.embedMany"
36
+ ]);
37
+ var METADATA_PREFIX = "ai.telemetry.metadata.";
38
+ var NS = "lunardinos.";
39
+ function attr(span, key) {
40
+ return span.attributes[key];
41
+ }
42
+ function numAttr(span, key) {
43
+ const v = attr(span, key);
44
+ return typeof v === "number" ? v : 0;
45
+ }
46
+ function strAttr(span, key) {
47
+ const v = attr(span, key);
48
+ return typeof v === "string" ? v : null;
49
+ }
50
+ function metaStr(span, key) {
51
+ return strAttr(span, `${METADATA_PREFIX}${NS}${key}`);
52
+ }
53
+ function metaJson(span, key) {
54
+ const v = metaStr(span, key);
55
+ if (!v)
56
+ return null;
57
+ try {
58
+ return JSON.parse(v);
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
63
+ function jsonAttr(span, key) {
64
+ const v = attr(span, key);
65
+ if (typeof v !== "string")
66
+ return null;
67
+ try {
68
+ return JSON.parse(v);
69
+ } catch {
70
+ return v;
71
+ }
72
+ }
73
+
74
+ class LunarDinosSpanProcessor {
75
+ config;
76
+ buffer = [];
77
+ timer = null;
78
+ endpoint;
79
+ maxBatchSize;
80
+ toolCallSpans = new Map;
81
+ constructor(config) {
82
+ this.config = config;
83
+ this.endpoint = config.endpoint ?? "https://ingest.lunardinos.com/api/ai-events";
84
+ this.maxBatchSize = config.maxBatchSize ?? 25;
85
+ this.timer = setInterval(() => {
86
+ this.flush();
87
+ }, config.flushIntervalMs ?? 5000);
88
+ }
89
+ onStart(_span, _context) {}
90
+ onEnd(span) {
91
+ const scope = span.instrumentationLibrary?.name;
92
+ if (scope !== "ai")
93
+ return;
94
+ if (span.name.startsWith("ai.toolCall")) {
95
+ const traceId2 = span.spanContext().traceId;
96
+ const existing = this.toolCallSpans.get(traceId2) ?? [];
97
+ existing.push(span);
98
+ this.toolCallSpans.set(traceId2, existing);
99
+ return;
100
+ }
101
+ if (!ROOT_OPERATIONS.has(span.name))
102
+ return;
103
+ const traceId = span.spanContext().traceId;
104
+ const model = strAttr(span, "ai.model.id") ?? strAttr(span, "gen_ai.request.model");
105
+ const inputTokens = numAttr(span, "gen_ai.usage.input_tokens") || numAttr(span, "ai.usage.promptTokens");
106
+ const outputTokens = numAttr(span, "gen_ai.usage.output_tokens") || numAttr(span, "ai.usage.completionTokens");
107
+ const startMs = hrtimeToMs(span.startTime);
108
+ const endMs = hrtimeToMs(span.endTime);
109
+ const childToolCalls = this.toolCallSpans.get(traceId);
110
+ this.toolCallSpans.delete(traceId);
111
+ let toolCalls = null;
112
+ if (childToolCalls?.length) {
113
+ toolCalls = childToolCalls.map((tc) => ({
114
+ name: strAttr(tc, "ai.toolCall.name") ?? span.name,
115
+ args: this.config.recordPrompts ? jsonAttr(tc, "ai.toolCall.args") : undefined,
116
+ result: this.config.recordPrompts ? jsonAttr(tc, "ai.toolCall.result") : undefined
117
+ }));
118
+ }
119
+ const payload = {
120
+ model,
121
+ inputTokens,
122
+ outputTokens,
123
+ costCents: model ? estimateCost(model, inputTokens, outputTokens) : null,
124
+ durationMs: Math.round(endMs - startMs),
125
+ msToFirstChunk: numAttr(span, "ai.response.msToFirstChunk") || null,
126
+ operation: span.name.replace("ai.", ""),
127
+ finishReason: strAttr(span, "ai.finishReason") ?? strAttr(span, "gen_ai.response.finish_reasons"),
128
+ error: span.status?.code === 2,
129
+ toolCalls,
130
+ prompt: this.config.recordPrompts ? jsonAttr(span, "ai.prompt.messages") : null,
131
+ response: this.config.recordPrompts ? strAttr(span, "ai.response.text") : null
132
+ };
133
+ const sessionId = metaStr(span, "sessionId");
134
+ const visitorId = metaStr(span, "visitorId");
135
+ const source = metaStr(span, "source");
136
+ const user = metaJson(span, "user");
137
+ const account = metaJson(span, "account");
138
+ const plan = metaJson(span, "plan");
139
+ if (!sessionId && !user && !account && !source)
140
+ return;
141
+ this.buffer.push({
142
+ timestamp: Date.now(),
143
+ sessionId,
144
+ visitorId,
145
+ source,
146
+ user,
147
+ account,
148
+ plan,
149
+ payload
150
+ });
151
+ if (this.buffer.length >= this.maxBatchSize) {
152
+ this.flush();
153
+ }
154
+ }
155
+ async forceFlush() {
156
+ await this.flush();
157
+ }
158
+ async shutdown() {
159
+ if (this.timer) {
160
+ clearInterval(this.timer);
161
+ this.timer = null;
162
+ }
163
+ await this.flush();
164
+ }
165
+ async flush() {
166
+ if (this.buffer.length === 0)
167
+ return;
168
+ const events = this.buffer.splice(0);
169
+ fetch(this.endpoint, {
170
+ method: "POST",
171
+ headers: {
172
+ "Content-Type": "application/json",
173
+ Authorization: `Bearer ${this.config.apiKey}`
174
+ },
175
+ body: JSON.stringify({ events })
176
+ }).catch(() => {});
177
+ }
178
+ }
179
+ function hrtimeToMs(hrtime) {
180
+ return hrtime[0] * 1000 + hrtime[1] / 1e6;
181
+ }
182
+
183
+ // src/init.ts
184
+ function initLunarDinos(config) {
185
+ const provider = new NodeTracerProvider({
186
+ spanProcessors: [new LunarDinosSpanProcessor(config)]
187
+ });
188
+ provider.register();
189
+ return provider;
190
+ }
191
+ // src/telemetry.ts
192
+ function lunarTelemetry(input = {}) {
193
+ const metadata = {};
194
+ if (input.sessionId)
195
+ metadata["lunardinos.sessionId"] = input.sessionId;
196
+ if (input.visitorId)
197
+ metadata["lunardinos.visitorId"] = input.visitorId;
198
+ if (input.source)
199
+ metadata["lunardinos.source"] = input.source;
200
+ if (input.user)
201
+ metadata["lunardinos.user"] = JSON.stringify(input.user);
202
+ if (input.account)
203
+ metadata["lunardinos.account"] = JSON.stringify(input.account);
204
+ if (input.plan)
205
+ metadata["lunardinos.plan"] = JSON.stringify(input.plan);
206
+ return { isEnabled: true, metadata };
207
+ }
208
+ export {
209
+ lunarTelemetry,
210
+ initLunarDinos,
211
+ LunarDinosSpanProcessor
212
+ };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Estimate cost in cents from model ID and token counts.
3
+ * Returns null for unknown models.
4
+ */
5
+ export declare function estimateCost(model: string, inputTokens: number, outputTokens: number): number | null;
6
+ //# sourceMappingURL=costs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"costs.d.ts","sourceRoot":"","sources":["../../src/costs.ts"],"names":[],"mappings":"AAqBA;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,GACnB,MAAM,GAAG,IAAI,CAQf"}
@@ -0,0 +1,31 @@
1
+ /** Per-1M-token prices in cents for common models. */
2
+ const MODEL_PRICES = {
3
+ // OpenAI
4
+ "gpt-4o": { input: 250, output: 1000 },
5
+ "gpt-4o-mini": { input: 15, output: 60 },
6
+ "gpt-4-turbo": { input: 1000, output: 3000 },
7
+ "gpt-4.1": { input: 200, output: 800 },
8
+ "gpt-4.1-mini": { input: 40, output: 160 },
9
+ "gpt-4.1-nano": { input: 10, output: 40 },
10
+ o3: { input: 1000, output: 4000 },
11
+ "o3-mini": { input: 110, output: 440 },
12
+ "o4-mini": { input: 110, output: 440 },
13
+ // Anthropic
14
+ "claude-opus-4-6": { input: 1500, output: 7500 },
15
+ "claude-sonnet-4-6": { input: 300, output: 1500 },
16
+ "claude-haiku-4-5-20251001": { input: 80, output: 400 },
17
+ // Google
18
+ "gemini-2.5-pro": { input: 125, output: 1000 },
19
+ "gemini-2.5-flash": { input: 15, output: 60 },
20
+ };
21
+ /**
22
+ * Estimate cost in cents from model ID and token counts.
23
+ * Returns null for unknown models.
24
+ */
25
+ export function estimateCost(model, inputTokens, outputTokens) {
26
+ // Try exact match first, then prefix match (e.g. "gpt-4o-2024-08-06" -> "gpt-4o")
27
+ const prices = MODEL_PRICES[model] ?? Object.entries(MODEL_PRICES).find(([key]) => model.startsWith(key))?.[1];
28
+ if (!prices)
29
+ return null;
30
+ return (inputTokens / 1_000_000) * prices.input + (outputTokens / 1_000_000) * prices.output;
31
+ }
@@ -0,0 +1,5 @@
1
+ export { initLunarDinos } from "./init";
2
+ export type { LunarDinosConfig } from "./processor";
3
+ export { LunarDinosSpanProcessor } from "./processor";
4
+ export { type LunarTelemetryInput, lunarTelemetry } from "./telemetry";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAA;AACvC,YAAY,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AACnD,OAAO,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAA;AACrD,OAAO,EAAE,KAAK,mBAAmB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA"}
@@ -0,0 +1,3 @@
1
+ export { initLunarDinos } from "./init";
2
+ export { LunarDinosSpanProcessor } from "./processor";
3
+ export { lunarTelemetry } from "./telemetry";
@@ -0,0 +1,12 @@
1
+ import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
2
+ import { type LunarDinosConfig } from "./processor";
3
+ /**
4
+ * One-call setup: creates a NodeTracerProvider, attaches the Lunar Dinos span
5
+ * processor, and registers it globally.
6
+ *
7
+ * Use this when you don't already have OpenTelemetry wired up. If you do (e.g.
8
+ * Sentry, Datadog), add `LunarDinosSpanProcessor` to your existing provider
9
+ * instead of calling this.
10
+ */
11
+ export declare function initLunarDinos(config: LunarDinosConfig): NodeTracerProvider;
12
+ //# sourceMappingURL=init.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAA;AAClE,OAAO,EAAE,KAAK,gBAAgB,EAA2B,MAAM,aAAa,CAAA;AAE5E;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,gBAAgB,GAAG,kBAAkB,CAM3E"}
@@ -0,0 +1,17 @@
1
+ import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
2
+ import { LunarDinosSpanProcessor } from "./processor";
3
+ /**
4
+ * One-call setup: creates a NodeTracerProvider, attaches the Lunar Dinos span
5
+ * processor, and registers it globally.
6
+ *
7
+ * Use this when you don't already have OpenTelemetry wired up. If you do (e.g.
8
+ * Sentry, Datadog), add `LunarDinosSpanProcessor` to your existing provider
9
+ * instead of calling this.
10
+ */
11
+ export function initLunarDinos(config) {
12
+ const provider = new NodeTracerProvider({
13
+ spanProcessors: [new LunarDinosSpanProcessor(config)],
14
+ });
15
+ provider.register();
16
+ return provider;
17
+ }
@@ -0,0 +1,29 @@
1
+ import type { Context } from "@opentelemetry/api";
2
+ import type { ReadableSpan, SpanProcessor } from "@opentelemetry/sdk-trace-base";
3
+ export interface LunarDinosConfig {
4
+ /** Ingest key (ik_...) */
5
+ apiKey: string;
6
+ /** Ingestion endpoint. Defaults to https://ingest.lunardinos.com/api/ai-events */
7
+ endpoint?: string;
8
+ /** Capture prompt messages and response text. Off by default (PII risk). */
9
+ recordPrompts?: boolean;
10
+ /** Flush interval in ms. Default 5000. */
11
+ flushIntervalMs?: number;
12
+ /** Max buffered events before force flush. Default 25. */
13
+ maxBatchSize?: number;
14
+ }
15
+ export declare class LunarDinosSpanProcessor implements SpanProcessor {
16
+ private config;
17
+ private buffer;
18
+ private timer;
19
+ private endpoint;
20
+ private maxBatchSize;
21
+ private toolCallSpans;
22
+ constructor(config: LunarDinosConfig);
23
+ onStart(_span: unknown, _context: Context): void;
24
+ onEnd(span: ReadableSpan): void;
25
+ forceFlush(): Promise<void>;
26
+ shutdown(): Promise<void>;
27
+ private flush;
28
+ }
29
+ //# sourceMappingURL=processor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"processor.d.ts","sourceRoot":"","sources":["../../src/processor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAA;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAA;AAGhF,MAAM,WAAW,gBAAgB;IAC/B,0BAA0B;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,kFAAkF;IAClF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,4EAA4E;IAC5E,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,0CAA0C;IAC1C,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,0DAA0D;IAC1D,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AA8FD,qBAAa,uBAAwB,YAAW,aAAa;IAO/C,OAAO,CAAC,MAAM;IAN1B,OAAO,CAAC,MAAM,CAAsB;IACpC,OAAO,CAAC,KAAK,CAA8C;IAC3D,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,YAAY,CAAQ;IAC5B,OAAO,CAAC,aAAa,CAAoC;gBAErC,MAAM,EAAE,gBAAgB;IAS5C,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,GAAG,IAAI;IAEhD,KAAK,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI;IAgFzB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAI3B,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;YAQjB,KAAK;CAepB"}
@@ -0,0 +1,163 @@
1
+ import { estimateCost } from "./costs";
2
+ /** Root-level AI SDK span names we care about. */
3
+ const ROOT_OPERATIONS = new Set([
4
+ "ai.generateText",
5
+ "ai.streamText",
6
+ "ai.generateObject",
7
+ "ai.streamObject",
8
+ "ai.embed",
9
+ "ai.embedMany",
10
+ ]);
11
+ /** Prefix the AI SDK applies to `metadata` values passed in `experimental_telemetry`. */
12
+ const METADATA_PREFIX = "ai.telemetry.metadata.";
13
+ /** Namespace inside metadata — lunarTelemetry() sets keys like `lunardinos.sessionId`. */
14
+ const NS = "lunardinos.";
15
+ function attr(span, key) {
16
+ return span.attributes[key];
17
+ }
18
+ function numAttr(span, key) {
19
+ const v = attr(span, key);
20
+ return typeof v === "number" ? v : 0;
21
+ }
22
+ function strAttr(span, key) {
23
+ const v = attr(span, key);
24
+ return typeof v === "string" ? v : null;
25
+ }
26
+ function metaStr(span, key) {
27
+ return strAttr(span, `${METADATA_PREFIX}${NS}${key}`);
28
+ }
29
+ function metaJson(span, key) {
30
+ const v = metaStr(span, key);
31
+ if (!v)
32
+ return null;
33
+ try {
34
+ return JSON.parse(v);
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ }
40
+ function jsonAttr(span, key) {
41
+ const v = attr(span, key);
42
+ if (typeof v !== "string")
43
+ return null;
44
+ try {
45
+ return JSON.parse(v);
46
+ }
47
+ catch {
48
+ return v;
49
+ }
50
+ }
51
+ export class LunarDinosSpanProcessor {
52
+ config;
53
+ buffer = [];
54
+ timer = null;
55
+ endpoint;
56
+ maxBatchSize;
57
+ toolCallSpans = new Map();
58
+ constructor(config) {
59
+ this.config = config;
60
+ this.endpoint = config.endpoint ?? "https://ingest.lunardinos.com/api/ai-events";
61
+ this.maxBatchSize = config.maxBatchSize ?? 25;
62
+ this.timer = setInterval(() => {
63
+ this.flush();
64
+ }, config.flushIntervalMs ?? 5000);
65
+ }
66
+ onStart(_span, _context) { }
67
+ onEnd(span) {
68
+ const scope = span.instrumentationLibrary?.name;
69
+ if (scope !== "ai")
70
+ return;
71
+ if (span.name.startsWith("ai.toolCall")) {
72
+ const traceId = span.spanContext().traceId;
73
+ const existing = this.toolCallSpans.get(traceId) ?? [];
74
+ existing.push(span);
75
+ this.toolCallSpans.set(traceId, existing);
76
+ return;
77
+ }
78
+ if (!ROOT_OPERATIONS.has(span.name))
79
+ return;
80
+ const traceId = span.spanContext().traceId;
81
+ const model = strAttr(span, "ai.model.id") ?? strAttr(span, "gen_ai.request.model");
82
+ const inputTokens = numAttr(span, "gen_ai.usage.input_tokens") || numAttr(span, "ai.usage.promptTokens");
83
+ const outputTokens = numAttr(span, "gen_ai.usage.output_tokens") || numAttr(span, "ai.usage.completionTokens");
84
+ const startMs = hrtimeToMs(span.startTime);
85
+ const endMs = hrtimeToMs(span.endTime);
86
+ const childToolCalls = this.toolCallSpans.get(traceId);
87
+ this.toolCallSpans.delete(traceId);
88
+ let toolCalls = null;
89
+ if (childToolCalls?.length) {
90
+ toolCalls = childToolCalls.map((tc) => ({
91
+ name: strAttr(tc, "ai.toolCall.name") ?? span.name,
92
+ args: this.config.recordPrompts ? jsonAttr(tc, "ai.toolCall.args") : undefined,
93
+ result: this.config.recordPrompts ? jsonAttr(tc, "ai.toolCall.result") : undefined,
94
+ }));
95
+ }
96
+ const payload = {
97
+ model,
98
+ inputTokens,
99
+ outputTokens,
100
+ costCents: model ? estimateCost(model, inputTokens, outputTokens) : null,
101
+ durationMs: Math.round(endMs - startMs),
102
+ msToFirstChunk: numAttr(span, "ai.response.msToFirstChunk") || null,
103
+ operation: span.name.replace("ai.", ""),
104
+ finishReason: strAttr(span, "ai.finishReason") ?? strAttr(span, "gen_ai.response.finish_reasons"),
105
+ error: span.status?.code === 2,
106
+ toolCalls,
107
+ prompt: this.config.recordPrompts ? jsonAttr(span, "ai.prompt.messages") : null,
108
+ response: this.config.recordPrompts ? strAttr(span, "ai.response.text") : null,
109
+ };
110
+ const sessionId = metaStr(span, "sessionId");
111
+ const visitorId = metaStr(span, "visitorId");
112
+ const source = metaStr(span, "source");
113
+ const user = metaJson(span, "user");
114
+ const account = metaJson(span, "account");
115
+ const plan = metaJson(span, "plan");
116
+ // Drop events with no identifying context at all — nothing for the
117
+ // backend to attribute them to. At least one of sessionId, user, account,
118
+ // or source must be present.
119
+ if (!sessionId && !user && !account && !source)
120
+ return;
121
+ this.buffer.push({
122
+ timestamp: Date.now(),
123
+ sessionId,
124
+ visitorId,
125
+ source,
126
+ user,
127
+ account,
128
+ plan,
129
+ payload,
130
+ });
131
+ if (this.buffer.length >= this.maxBatchSize) {
132
+ this.flush();
133
+ }
134
+ }
135
+ async forceFlush() {
136
+ await this.flush();
137
+ }
138
+ async shutdown() {
139
+ if (this.timer) {
140
+ clearInterval(this.timer);
141
+ this.timer = null;
142
+ }
143
+ await this.flush();
144
+ }
145
+ async flush() {
146
+ if (this.buffer.length === 0)
147
+ return;
148
+ const events = this.buffer.splice(0);
149
+ fetch(this.endpoint, {
150
+ method: "POST",
151
+ headers: {
152
+ "Content-Type": "application/json",
153
+ Authorization: `Bearer ${this.config.apiKey}`,
154
+ },
155
+ body: JSON.stringify({ events }),
156
+ }).catch(() => {
157
+ // fire-and-forget
158
+ });
159
+ }
160
+ }
161
+ function hrtimeToMs(hrtime) {
162
+ return hrtime[0] * 1000 + hrtime[1] / 1_000_000;
163
+ }
@@ -0,0 +1,65 @@
1
+ interface TelemetryUser {
2
+ id: string;
3
+ email?: string;
4
+ name?: string;
5
+ role?: string;
6
+ [k: string]: unknown;
7
+ }
8
+ interface TelemetryAccount {
9
+ id: string;
10
+ name?: string;
11
+ [k: string]: unknown;
12
+ }
13
+ interface TelemetryPlan {
14
+ id: string;
15
+ name?: string;
16
+ [k: string]: unknown;
17
+ }
18
+ export interface LunarTelemetryInput {
19
+ /** Browser session to link this AI call to (for replay correlation). */
20
+ sessionId?: string;
21
+ /** Anonymous visitor ID (for cross-session linkage). */
22
+ visitorId?: string;
23
+ /**
24
+ * Free-text label for background work (e.g. "slack-bot", "cron:digest").
25
+ * Shown in the AI usage dashboard so session-less events can be grouped.
26
+ */
27
+ source?: string;
28
+ /** User identity — same shape as recorder.identify(). */
29
+ user?: TelemetryUser;
30
+ /** Account identity — same shape as recorder.identify(). */
31
+ account?: TelemetryAccount;
32
+ /** Plan identity — same shape as recorder.identify(). */
33
+ plan?: TelemetryPlan;
34
+ }
35
+ /**
36
+ * Build the `experimental_telemetry` option for Vercel AI SDK calls. Tags the
37
+ * AI call so the Lunar Dinos span processor can attribute it.
38
+ *
39
+ * @example Session-linked:
40
+ * ```ts
41
+ * await generateText({
42
+ * model: openai("gpt-4o"),
43
+ * prompt: "Hello",
44
+ * experimental_telemetry: lunarTelemetry({ sessionId }),
45
+ * })
46
+ * ```
47
+ *
48
+ * @example Background (no session):
49
+ * ```ts
50
+ * await generateText({
51
+ * model: openai("gpt-4o"),
52
+ * experimental_telemetry: lunarTelemetry({
53
+ * source: "slack-bot",
54
+ * user: { id: "u_123", email: "jane@acme.com" },
55
+ * account: { id: "acme", name: "Acme Inc" },
56
+ * }),
57
+ * })
58
+ * ```
59
+ */
60
+ export declare function lunarTelemetry(input?: LunarTelemetryInput): {
61
+ isEnabled: true;
62
+ metadata: Record<string, string>;
63
+ };
64
+ export {};
65
+ //# sourceMappingURL=telemetry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"telemetry.d.ts","sourceRoot":"","sources":["../../src/telemetry.ts"],"names":[],"mappings":"AAAA,UAAU,aAAa;IACrB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CACrB;AACD,UAAU,gBAAgB;IACxB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CACrB;AACD,UAAU,aAAa;IACrB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CACrB;AAED,MAAM,WAAW,mBAAmB;IAClC,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,wDAAwD;IACxD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,yDAAyD;IACzD,IAAI,CAAC,EAAE,aAAa,CAAA;IACpB,4DAA4D;IAC5D,OAAO,CAAC,EAAE,gBAAgB,CAAA;IAC1B,yDAAyD;IACzD,IAAI,CAAC,EAAE,aAAa,CAAA;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,cAAc,CAAC,KAAK,GAAE,mBAAwB,GAAG;IAC/D,SAAS,EAAE,IAAI,CAAA;IACf,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACjC,CASA"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Build the `experimental_telemetry` option for Vercel AI SDK calls. Tags the
3
+ * AI call so the Lunar Dinos span processor can attribute it.
4
+ *
5
+ * @example Session-linked:
6
+ * ```ts
7
+ * await generateText({
8
+ * model: openai("gpt-4o"),
9
+ * prompt: "Hello",
10
+ * experimental_telemetry: lunarTelemetry({ sessionId }),
11
+ * })
12
+ * ```
13
+ *
14
+ * @example Background (no session):
15
+ * ```ts
16
+ * await generateText({
17
+ * model: openai("gpt-4o"),
18
+ * experimental_telemetry: lunarTelemetry({
19
+ * source: "slack-bot",
20
+ * user: { id: "u_123", email: "jane@acme.com" },
21
+ * account: { id: "acme", name: "Acme Inc" },
22
+ * }),
23
+ * })
24
+ * ```
25
+ */
26
+ export function lunarTelemetry(input = {}) {
27
+ const metadata = {};
28
+ if (input.sessionId)
29
+ metadata["lunardinos.sessionId"] = input.sessionId;
30
+ if (input.visitorId)
31
+ metadata["lunardinos.visitorId"] = input.visitorId;
32
+ if (input.source)
33
+ metadata["lunardinos.source"] = input.source;
34
+ if (input.user)
35
+ metadata["lunardinos.user"] = JSON.stringify(input.user);
36
+ if (input.account)
37
+ metadata["lunardinos.account"] = JSON.stringify(input.account);
38
+ if (input.plan)
39
+ metadata["lunardinos.plan"] = JSON.stringify(input.plan);
40
+ return { isEnabled: true, metadata };
41
+ }
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@lunardinos/server",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "import": "./dist/index.js",
8
+ "types": "./dist/src/index.d.ts"
9
+ }
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "!dist/*.tsbuildinfo"
14
+ ],
15
+ "scripts": {
16
+ "build": "rm -rf dist && bun run build.ts"
17
+ },
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "dependencies": {
22
+ "@opentelemetry/api": "^1.9",
23
+ "@opentelemetry/sdk-trace-base": "^1.30",
24
+ "@opentelemetry/sdk-trace-node": "^1.30"
25
+ }
26
+ }