@posthog/ai 7.11.2 → 7.12.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.
@@ -1,1136 +1,38 @@
1
1
  'use strict';
2
2
 
3
- var uuid = require('uuid');
4
- var core = require('@posthog/core');
5
-
6
- var version = "7.11.2";
7
-
8
- // Type guards for safer type checking
9
-
10
- const isString = value => {
11
- return typeof value === 'string';
12
- };
13
-
14
- const REDACTED_IMAGE_PLACEHOLDER = '[base64 image redacted]';
15
-
16
- // ============================================
17
- // Multimodal Feature Toggle
18
- // ============================================
19
-
20
- const isMultimodalEnabled = () => {
21
- const val = process.env._INTERNAL_LLMA_MULTIMODAL || '';
22
- return val.toLowerCase() === 'true' || val === '1' || val.toLowerCase() === 'yes';
23
- };
24
-
25
- // ============================================
26
- // Base64 Detection Helpers
27
- // ============================================
28
-
29
- const isBase64DataUrl = str => {
30
- return /^data:([^;]+);base64,/.test(str);
31
- };
32
- const isValidUrl = str => {
33
- try {
34
- new URL(str);
35
- return true;
36
- } catch {
37
- // Not an absolute URL, check if it's a relative URL or path
38
- return str.startsWith('/') || str.startsWith('./') || str.startsWith('../');
39
- }
40
- };
41
- const isRawBase64 = str => {
42
- // Skip if it's a valid URL or path
43
- if (isValidUrl(str)) {
44
- return false;
45
- }
46
-
47
- // Check if it's a valid base64 string
48
- // Base64 images are typically at least a few hundred chars, but we'll be conservative
49
- return str.length > 20 && /^[A-Za-z0-9+/]+=*$/.test(str);
50
- };
51
- function redactBase64DataUrl(str) {
52
- if (isMultimodalEnabled()) return str;
53
- if (!isString(str)) return str;
54
-
55
- // Check for data URL format
56
- if (isBase64DataUrl(str)) {
57
- return REDACTED_IMAGE_PLACEHOLDER;
58
- }
59
-
60
- // Check for raw base64 (Vercel sends raw base64 for inline images)
61
- if (isRawBase64(str)) {
62
- return REDACTED_IMAGE_PLACEHOLDER;
63
- }
64
- return str;
65
- }
66
-
67
- const TOKEN_PROPERTY_KEYS = new Set(['$ai_input_tokens', '$ai_output_tokens', '$ai_cache_read_input_tokens', '$ai_cache_creation_input_tokens', '$ai_total_tokens', '$ai_reasoning_tokens']);
68
- function getTokensSource(posthogProperties) {
69
- if (posthogProperties && Object.keys(posthogProperties).some(key => TOKEN_PROPERTY_KEYS.has(key))) {
70
- return 'passthrough';
71
- }
72
- return 'sdk';
73
- }
74
-
75
- // limit large outputs by truncating to 200kb (approx 200k bytes)
76
- const MAX_OUTPUT_SIZE = 200000;
77
- const STRING_FORMAT = 'utf8';
78
- const getModelParams = params => {
79
- if (!params) {
80
- return {};
81
- }
82
- const modelParams = {};
83
- const paramKeys = ['temperature', 'max_tokens', 'max_completion_tokens', 'top_p', 'frequency_penalty', 'presence_penalty', 'n', 'stop', 'stream', 'streaming', 'language', 'response_format', 'timestamp_granularities'];
84
- for (const key of paramKeys) {
85
- if (key in params && params[key] !== undefined) {
86
- modelParams[key] = params[key];
87
- }
88
- }
89
- return modelParams;
90
- };
91
- const withPrivacyMode = (client, privacyMode, input) => {
92
- return client.privacy_mode || privacyMode ? null : input;
93
- };
94
- function toSafeString(input) {
95
- if (input === undefined || input === null) {
96
- return '';
97
- }
98
- if (typeof input === 'string') {
99
- return input;
100
- }
101
- try {
102
- return JSON.stringify(input);
103
- } catch {
104
- console.warn('Failed to stringify input', input);
105
- return '';
106
- }
107
- }
108
- const truncate = input => {
109
- const str = toSafeString(input);
110
- if (str === '') {
111
- return '';
112
- }
113
-
114
- // Check if we need to truncate and ensure STRING_FORMAT is respected
115
- const encoder = new TextEncoder();
116
- const buffer = encoder.encode(str);
117
- if (buffer.length <= MAX_OUTPUT_SIZE) {
118
- // Ensure STRING_FORMAT is respected
119
- return new TextDecoder(STRING_FORMAT).decode(buffer);
120
- }
121
-
122
- // Truncate the buffer and ensure a valid string is returned
123
- const truncatedBuffer = buffer.slice(0, MAX_OUTPUT_SIZE);
124
- // fatal: false means we get U+FFFD at the end if truncation broke the encoding
125
- const decoder = new TextDecoder(STRING_FORMAT, {
126
- fatal: false
127
- });
128
- let truncatedStr = decoder.decode(truncatedBuffer);
129
- if (truncatedStr.endsWith('\uFFFD')) {
130
- truncatedStr = truncatedStr.slice(0, -1);
131
- }
132
- return `${truncatedStr}... [truncated]`;
133
- };
134
- let AIEvent = /*#__PURE__*/function (AIEvent) {
135
- AIEvent["Generation"] = "$ai_generation";
136
- AIEvent["Embedding"] = "$ai_embedding";
137
- return AIEvent;
138
- }({});
139
- function sanitizeValues(obj) {
140
- if (obj === undefined || obj === null) {
141
- return obj;
142
- }
143
- const jsonSafe = JSON.parse(JSON.stringify(obj));
144
- if (typeof jsonSafe === 'string') {
145
- // Sanitize lone surrogates by round-tripping through UTF-8
146
- return new TextDecoder().decode(new TextEncoder().encode(jsonSafe));
147
- } else if (Array.isArray(jsonSafe)) {
148
- return jsonSafe.map(sanitizeValues);
149
- } else if (jsonSafe && typeof jsonSafe === 'object') {
150
- return Object.fromEntries(Object.entries(jsonSafe).map(([k, v]) => [k, sanitizeValues(v)]));
151
- }
152
- return jsonSafe;
153
- }
154
- const sendEventWithErrorToPosthog = async ({
155
- client,
156
- traceId,
157
- error,
158
- ...args
159
- }) => {
160
- const httpStatus = error && typeof error === 'object' && 'status' in error ? error.status ?? 500 : 500;
161
- const properties = {
162
- client,
163
- traceId,
164
- httpStatus,
165
- error: JSON.stringify(error),
166
- ...args
167
- };
168
- const enrichedError = error;
169
- if (client.options?.enableExceptionAutocapture) {
170
- // assign a uuid that can be used to link the trace and exception events
171
- const exceptionId = core.uuidv7();
172
- client.captureException(error, undefined, {
173
- $ai_trace_id: traceId
174
- }, exceptionId);
175
- enrichedError.__posthog_previously_captured_error = true;
176
- properties.exceptionId = exceptionId;
177
- }
178
- await sendEventToPosthog(properties);
179
- return enrichedError;
180
- };
181
- const sendEventToPosthog = async ({
182
- client,
183
- eventType = AIEvent.Generation,
184
- distinctId,
185
- traceId,
186
- model,
187
- provider,
188
- input,
189
- output,
190
- latency,
191
- timeToFirstToken,
192
- baseURL,
193
- params,
194
- httpStatus = 200,
195
- usage = {},
196
- error,
197
- exceptionId,
198
- tools,
199
- captureImmediate = false
200
- }) => {
201
- if (!client.capture) {
202
- return Promise.resolve();
203
- }
204
- // sanitize input and output for UTF-8 validity
205
- const safeInput = sanitizeValues(input);
206
- const safeOutput = sanitizeValues(output);
207
- const safeError = sanitizeValues(error);
208
- let errorData = {};
209
- if (error) {
210
- errorData = {
211
- $ai_is_error: true,
212
- $ai_error: safeError,
213
- $exception_event_id: exceptionId
214
- };
215
- }
216
- let costOverrideData = {};
217
- if (params.posthogCostOverride) {
218
- const inputCostUSD = (params.posthogCostOverride.inputCost ?? 0) * (usage.inputTokens ?? 0);
219
- const outputCostUSD = (params.posthogCostOverride.outputCost ?? 0) * (usage.outputTokens ?? 0);
220
- costOverrideData = {
221
- $ai_input_cost_usd: inputCostUSD,
222
- $ai_output_cost_usd: outputCostUSD,
223
- $ai_total_cost_usd: inputCostUSD + outputCostUSD
224
- };
225
- }
226
- const additionalTokenValues = {
227
- ...(usage.reasoningTokens ? {
228
- $ai_reasoning_tokens: usage.reasoningTokens
229
- } : {}),
230
- ...(usage.cacheReadInputTokens ? {
231
- $ai_cache_read_input_tokens: usage.cacheReadInputTokens
232
- } : {}),
233
- ...(usage.cacheCreationInputTokens ? {
234
- $ai_cache_creation_input_tokens: usage.cacheCreationInputTokens
235
- } : {}),
236
- ...(usage.webSearchCount ? {
237
- $ai_web_search_count: usage.webSearchCount
238
- } : {}),
239
- ...(usage.rawUsage ? {
240
- $ai_usage: usage.rawUsage
241
- } : {})
242
- };
243
- const properties = {
244
- $ai_lib: 'posthog-ai',
245
- $ai_lib_version: version,
246
- $ai_provider: params.posthogProviderOverride ?? provider,
247
- $ai_model: params.posthogModelOverride ?? model,
248
- $ai_model_parameters: getModelParams(params),
249
- $ai_input: withPrivacyMode(client, params.posthogPrivacyMode ?? false, safeInput),
250
- $ai_output_choices: withPrivacyMode(client, params.posthogPrivacyMode ?? false, safeOutput),
251
- $ai_http_status: httpStatus,
252
- $ai_input_tokens: usage.inputTokens ?? 0,
253
- ...(usage.outputTokens !== undefined ? {
254
- $ai_output_tokens: usage.outputTokens
255
- } : {}),
256
- ...additionalTokenValues,
257
- $ai_latency: latency,
258
- ...(timeToFirstToken !== undefined ? {
259
- $ai_time_to_first_token: timeToFirstToken
260
- } : {}),
261
- $ai_trace_id: traceId,
262
- $ai_base_url: baseURL,
263
- ...params.posthogProperties,
264
- $ai_tokens_source: getTokensSource(params.posthogProperties),
265
- ...(distinctId ? {} : {
266
- $process_person_profile: false
267
- }),
268
- ...(tools ? {
269
- $ai_tools: tools
270
- } : {}),
271
- ...errorData,
272
- ...costOverrideData
273
- };
274
- const event = {
275
- distinctId: distinctId ?? traceId,
276
- event: eventType,
277
- properties,
278
- groups: params.posthogGroups
279
- };
280
- if (captureImmediate) {
281
- // await capture promise to send single event in serverless environments
282
- await client.captureImmediate(event);
283
- } else {
284
- client.capture(event);
285
- }
286
- return Promise.resolve();
287
- };
288
-
289
- const OTEL_STATUS_ERROR = 2;
290
- const AI_TELEMETRY_METADATA_PREFIX = 'ai.telemetry.metadata.';
291
- function parseJsonValue(value) {
292
- if (value === undefined || value === null) {
293
- return null;
294
- }
295
- if (typeof value !== 'string') {
296
- return value;
297
- }
298
- try {
299
- return JSON.parse(value);
300
- } catch {
301
- return null;
302
- }
303
- }
304
- function toNumber(value) {
305
- if (typeof value === 'number' && Number.isFinite(value)) {
306
- return value;
307
- }
308
- if (typeof value === 'string') {
309
- const parsed = Number(value);
310
- if (Number.isFinite(parsed)) {
311
- return parsed;
312
- }
313
- }
314
- return undefined;
315
- }
316
- function toStringValue(value) {
317
- return typeof value === 'string' ? value : undefined;
318
- }
319
- function toStringArray(value) {
320
- if (!Array.isArray(value)) {
321
- return [];
322
- }
323
- return value.filter(item => typeof item === 'string');
324
- }
325
- function toSafeBinaryData(value) {
326
- const asString = typeof value === 'string' ? value : JSON.stringify(value ?? '');
327
- return truncate(redactBase64DataUrl(asString));
328
- }
329
- function toMimeType(value) {
330
- return typeof value === 'string' && value.length > 0 ? value : 'application/octet-stream';
331
- }
332
- function getSpanLatencySeconds(span) {
333
- const duration = span.duration;
334
- if (!duration || !Array.isArray(duration) || duration.length !== 2) {
335
- return 0;
336
- }
337
- const seconds = Number(duration[0]) || 0;
338
- const nanos = Number(duration[1]) || 0;
339
- return seconds + nanos / 1_000_000_000;
340
- }
341
- function getOperationId(span) {
342
- const attributes = span.attributes || {};
343
- const operationId = toStringValue(attributes['ai.operationId']);
344
- if (operationId) {
345
- return operationId;
346
- }
347
- return span.name || '';
348
- }
349
- function isDoGenerateSpan(operationId) {
350
- return operationId.endsWith('.doGenerate');
351
- }
352
- function isDoStreamSpan(operationId) {
353
- return operationId.endsWith('.doStream');
354
- }
355
- function isDoEmbedSpan(operationId) {
356
- return operationId.endsWith('.doEmbed');
357
- }
358
- function shouldMapAiSdkSpan(span) {
359
- const operationId = getOperationId(span);
360
- return isDoGenerateSpan(operationId) || isDoStreamSpan(operationId) || isDoEmbedSpan(operationId);
361
- }
362
- function extractAiSdkTelemetryMetadata(attributes) {
363
- const metadata = {};
364
- for (const [key, value] of Object.entries(attributes)) {
365
- if (key.startsWith(AI_TELEMETRY_METADATA_PREFIX)) {
366
- metadata[key.slice(AI_TELEMETRY_METADATA_PREFIX.length)] = value;
367
- }
368
- }
369
- if (metadata.traceId && typeof metadata.traceId === 'string') {
370
- metadata.trace_id = metadata.traceId;
371
- }
372
- return metadata;
373
- }
374
- function mapPromptMessagesInput(attributes) {
375
- const promptMessages = parseJsonValue(attributes['ai.prompt.messages']) || [];
376
- if (!Array.isArray(promptMessages)) {
377
- return [];
378
- }
379
- return promptMessages.map(message => {
380
- const role = typeof message?.role === 'string' ? message.role : 'user';
381
- const content = message?.content;
382
- if (typeof content === 'string') {
383
- return {
384
- role,
385
- content: [{
386
- type: 'text',
387
- text: truncate(content)
388
- }]
389
- };
390
- }
391
- if (Array.isArray(content)) {
392
- return {
393
- role,
394
- content: content.map(part => {
395
- if (part && typeof part === 'object' && 'type' in part) {
396
- const typedPart = part;
397
- if (typedPart.type === 'text' && typeof typedPart.text === 'string') {
398
- return {
399
- type: 'text',
400
- text: truncate(typedPart.text)
401
- };
402
- }
403
- return typedPart;
404
- }
405
- return {
406
- type: 'text',
407
- text: truncate(String(part))
408
- };
409
- })
410
- };
411
- }
412
- return {
413
- role,
414
- content: [{
415
- type: 'text',
416
- text: truncate(content)
417
- }]
418
- };
419
- });
420
- }
421
- function mapPromptInput(attributes, operationId) {
422
- if (isDoEmbedSpan(operationId)) {
423
- if (attributes['ai.values'] !== undefined) {
424
- return attributes['ai.values'];
425
- }
426
- return attributes['ai.value'] ?? null;
427
- }
428
- const promptMessages = mapPromptMessagesInput(attributes);
429
- if (promptMessages.length > 0) {
430
- return promptMessages;
431
- }
432
- if (attributes['ai.prompt'] !== undefined) {
433
- return [{
434
- role: 'user',
435
- content: [{
436
- type: 'text',
437
- text: truncate(attributes['ai.prompt'])
438
- }]
439
- }];
440
- }
441
- return [];
442
- }
443
- function mapOutputPart(part) {
444
- const partType = toStringValue(part.type);
445
- if (partType === 'text' && typeof part.text === 'string') {
446
- return {
447
- type: 'text',
448
- text: truncate(part.text)
449
- };
450
- }
451
- if (partType === 'tool-call') {
452
- const toolName = toStringValue(part.toolName) || toStringValue(part.function?.name) || '';
453
- const toolCallId = toStringValue(part.toolCallId) || toStringValue(part.id) || '';
454
- const input = 'input' in part ? part.input : part.function?.arguments;
455
- if (toolName) {
456
- return {
457
- type: 'tool-call',
458
- id: toolCallId,
459
- function: {
460
- name: toolName,
461
- arguments: typeof input === 'string' ? input : JSON.stringify(input ?? {})
462
- }
463
- };
464
- }
465
- }
466
- if (partType === 'file') {
467
- const mediaType = toMimeType(part.mediaType ?? part.mimeType ?? part.contentType);
468
- const data = part.data ?? part.base64 ?? part.bytes ?? part.url ?? part.uri;
469
- if (data !== undefined) {
470
- return {
471
- type: 'file',
472
- name: 'generated_file',
473
- mediaType,
474
- data: toSafeBinaryData(data)
475
- };
476
- }
477
- }
478
- if (partType === 'image') {
479
- const mediaType = toMimeType(part.mediaType ?? part.mimeType ?? part.contentType ?? 'image/unknown');
480
- const data = part.data ?? part.base64 ?? part.bytes ?? part.url ?? part.uri ?? part.image ?? part.image_url;
481
- if (data !== undefined) {
482
- return {
483
- type: 'file',
484
- name: 'generated_file',
485
- mediaType,
486
- data: toSafeBinaryData(data)
487
- };
488
- }
489
- }
490
- const inlineData = part.inlineData ?? part.inline_data;
491
- if (inlineData && typeof inlineData === 'object' && inlineData.data !== undefined) {
492
- const mediaType = toMimeType(inlineData.mimeType ?? inlineData.mime_type);
493
- return {
494
- type: 'file',
495
- name: 'generated_file',
496
- mediaType,
497
- data: toSafeBinaryData(inlineData.data)
498
- };
499
- }
500
- if (partType === 'object' && part.object !== undefined) {
501
- return {
502
- type: 'object',
503
- object: part.object
504
- };
505
- }
506
- return null;
507
- }
508
- function mapResponseMessagesOutput(attributes) {
509
- const messagesRaw = parseJsonValue(attributes['ai.response.messages']) ?? parseJsonValue(attributes['ai.response.message']);
510
- if (!messagesRaw) {
511
- return [];
512
- }
513
- const messages = Array.isArray(messagesRaw) ? messagesRaw : [messagesRaw];
514
- const mappedMessages = [];
515
- for (const message of messages) {
516
- if (!message || typeof message !== 'object') {
517
- continue;
518
- }
519
- const role = toStringValue(message.role) || 'assistant';
520
- const content = message.content;
521
- if (typeof content === 'string') {
522
- mappedMessages.push({
523
- role,
524
- content: [{
525
- type: 'text',
526
- text: truncate(content)
527
- }]
528
- });
529
- continue;
530
- }
531
- if (Array.isArray(content)) {
532
- const parts = content.map(part => part && typeof part === 'object' ? mapOutputPart(part) : null).filter(part => part !== null);
533
- if (parts.length > 0) {
534
- mappedMessages.push({
535
- role,
536
- content: parts
537
- });
3
+ var exporterTraceOtlpHttp = require('@opentelemetry/exporter-trace-otlp-http');
4
+
5
+ /**
6
+ * An OpenTelemetry SpanExporter that sends traces to PostHog's OTLP
7
+ * ingestion endpoint. PostHog converts `gen_ai.*` spans into
8
+ * `$ai_generation` events server-side.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * import { PostHogTraceExporter } from '@posthog/ai/otel'
13
+ * import { NodeSDK } from '@opentelemetry/sdk-node'
14
+ *
15
+ * const sdk = new NodeSDK({
16
+ * traceExporter: new PostHogTraceExporter({ apiKey: 'phc_...' }),
17
+ * })
18
+ * sdk.start()
19
+ * ```
20
+ */
21
+ class PostHogTraceExporter extends exporterTraceOtlpHttp.OTLPTraceExporter {
22
+ constructor(options) {
23
+ if (!options.apiKey) {
24
+ throw new Error('PostHogTraceExporter requires an apiKey');
25
+ }
26
+ const host = new URL(options.host || 'https://us.i.posthog.com').origin;
27
+ super({
28
+ url: `${host}/i/v0/ai/otel`,
29
+ headers: {
30
+ // The OTLP ingestion endpoint authenticates using the project API key as a Bearer token
31
+ Authorization: `Bearer ${options.apiKey}`
538
32
  }
539
- continue;
540
- }
541
- }
542
- return mappedMessages;
543
- }
544
- function mapTextToolObjectOutputParts(attributes) {
545
- const responseText = toStringValue(attributes['ai.response.text']) || '';
546
- const toolCalls = parseJsonValue(attributes['ai.response.toolCalls']) || [];
547
- const responseObjectRaw = attributes['ai.response.object'];
548
- const responseObject = parseJsonValue(responseObjectRaw);
549
- const contentParts = [];
550
- if (responseText) {
551
- contentParts.push({
552
- type: 'text',
553
- text: truncate(responseText)
554
33
  });
555
34
  }
556
- if (responseObjectRaw !== undefined) {
557
- contentParts.push({
558
- type: 'object',
559
- object: responseObject ?? responseObjectRaw
560
- });
561
- }
562
- if (Array.isArray(toolCalls)) {
563
- for (const toolCall of toolCalls) {
564
- if (!toolCall || typeof toolCall !== 'object') {
565
- continue;
566
- }
567
- const toolName = typeof toolCall.toolName === 'string' ? toolCall.toolName : '';
568
- const toolCallId = typeof toolCall.toolCallId === 'string' ? toolCall.toolCallId : '';
569
- if (!toolName) {
570
- continue;
571
- }
572
- const input = 'input' in toolCall ? toolCall.input : {};
573
- contentParts.push({
574
- type: 'tool-call',
575
- id: toolCallId,
576
- function: {
577
- name: toolName,
578
- arguments: typeof input === 'string' ? input : JSON.stringify(input)
579
- }
580
- });
581
- }
582
- }
583
- return contentParts;
584
- }
585
- function mapResponseFilesOutput(attributes) {
586
- const responseFiles = parseJsonValue(attributes['ai.response.files']) || [];
587
- if (!Array.isArray(responseFiles)) {
588
- return [];
589
- }
590
- const mapped = [];
591
- for (const file of responseFiles) {
592
- if (!file || typeof file !== 'object') {
593
- continue;
594
- }
595
- const mimeType = toMimeType(file.mimeType ?? file.mediaType ?? file.contentType);
596
- const data = file.data ?? file.base64 ?? file.bytes;
597
- const url = typeof file.url === 'string' ? file.url : typeof file.uri === 'string' ? file.uri : undefined;
598
- if (data !== undefined) {
599
- mapped.push({
600
- type: 'file',
601
- name: 'generated_file',
602
- mediaType: mimeType,
603
- data: toSafeBinaryData(data)
604
- });
605
- continue;
606
- }
607
- if (url) {
608
- mapped.push({
609
- type: 'file',
610
- name: 'generated_file',
611
- mediaType: mimeType,
612
- data: truncate(url)
613
- });
614
- }
615
- }
616
- return mapped;
617
- }
618
- function extractGeminiParts(providerMetadata) {
619
- const parts = [];
620
- const visit = node => {
621
- if (!node || typeof node !== 'object') {
622
- return;
623
- }
624
- if (Array.isArray(node)) {
625
- for (const item of node) {
626
- visit(item);
627
- }
628
- return;
629
- }
630
- const objectNode = node;
631
- const maybeParts = objectNode.parts;
632
- if (Array.isArray(maybeParts)) {
633
- for (const part of maybeParts) {
634
- if (part && typeof part === 'object') {
635
- parts.push(part);
636
- }
637
- }
638
- }
639
- for (const value of Object.values(objectNode)) {
640
- visit(value);
641
- }
642
- };
643
- visit(providerMetadata);
644
- return parts;
645
- }
646
- function mapProviderMetadataInlineDataOutput(providerMetadata) {
647
- const parts = extractGeminiParts(providerMetadata);
648
- const mapped = [];
649
- for (const part of parts) {
650
- const inlineData = part.inlineData ?? part.inline_data;
651
- if (!inlineData || typeof inlineData !== 'object') {
652
- continue;
653
- }
654
- const mimeType = toMimeType(inlineData.mimeType ?? inlineData.mime_type);
655
- if (inlineData.data === undefined) {
656
- continue;
657
- }
658
- mapped.push({
659
- type: 'file',
660
- name: 'generated_file',
661
- mediaType: mimeType,
662
- data: toSafeBinaryData(inlineData.data)
663
- });
664
- }
665
- return mapped;
666
- }
667
- function mapProviderMetadataTextOutput(providerMetadata) {
668
- const parts = extractGeminiParts(providerMetadata);
669
- const mapped = [];
670
- for (const part of parts) {
671
- if (typeof part.text === 'string' && part.text.length > 0) {
672
- mapped.push({
673
- type: 'text',
674
- text: truncate(part.text)
675
- });
676
- }
677
- }
678
- return mapped;
679
- }
680
- function extractMediaBlocksFromUnknownNode(node) {
681
- const mapped = [];
682
- const visit = value => {
683
- if (!value || typeof value !== 'object') {
684
- return;
685
- }
686
- if (Array.isArray(value)) {
687
- for (const item of value) {
688
- visit(item);
689
- }
690
- return;
691
- }
692
- const objectValue = value;
693
- const inlineData = objectValue.inlineData ?? objectValue.inline_data;
694
- if (inlineData && typeof inlineData === 'object' && inlineData.data !== undefined) {
695
- const mediaType = toMimeType(inlineData.mimeType ?? inlineData.mime_type);
696
- mapped.push({
697
- type: 'file',
698
- name: 'generated_file',
699
- mediaType,
700
- data: toSafeBinaryData(inlineData.data)
701
- });
702
- }
703
- if ((objectValue.type === 'file' || 'mediaType' in objectValue || 'mimeType' in objectValue) && objectValue.data) {
704
- const mediaType = toMimeType(objectValue.mediaType ?? objectValue.mimeType);
705
- mapped.push({
706
- type: 'file',
707
- name: 'generated_file',
708
- mediaType,
709
- data: toSafeBinaryData(objectValue.data)
710
- });
711
- }
712
- for (const child of Object.values(objectValue)) {
713
- visit(child);
714
- }
715
- };
716
- visit(node);
717
- return mapped;
718
- }
719
- function mapUnknownResponseAttributeMediaOutput(attributes) {
720
- const mapped = [];
721
- for (const [key, value] of Object.entries(attributes)) {
722
- if (!key.startsWith('ai.response.')) {
723
- continue;
724
- }
725
- if (key === 'ai.response.text' || key === 'ai.response.toolCalls' || key === 'ai.response.object' || key === 'ai.response.files' || key === 'ai.response.message' || key === 'ai.response.messages' || key === 'ai.response.providerMetadata') {
726
- continue;
727
- }
728
- const parsed = typeof value === 'string' ? parseJsonValue(value) ?? value : value;
729
- mapped.push(...extractMediaBlocksFromUnknownNode(parsed));
730
- }
731
- return mapped;
732
- }
733
- function mapGenericResponseAttributeMediaOutput(attributes) {
734
- const mapped = [];
735
- for (const [key, value] of Object.entries(attributes)) {
736
- if (!key.includes('response') || key.startsWith('ai.response.') || key === 'ai.response.providerMetadata' || key.startsWith('ai.prompt.') || key.startsWith('gen_ai.request.')) {
737
- continue;
738
- }
739
- const parsed = typeof value === 'string' ? parseJsonValue(value) : value;
740
- if (parsed === null || parsed === undefined) {
741
- continue;
742
- }
743
- mapped.push(...extractMediaBlocksFromUnknownNode(parsed));
744
- }
745
- return mapped;
746
- }
747
- function dedupeContentParts(parts) {
748
- const seen = new Set();
749
- const deduped = [];
750
- for (const part of parts) {
751
- const key = JSON.stringify(part);
752
- if (seen.has(key)) {
753
- continue;
754
- }
755
- seen.add(key);
756
- deduped.push(part);
757
- }
758
- return deduped;
759
- }
760
- function mapOutput(attributes, operationId, providerMetadata) {
761
- if (isDoEmbedSpan(operationId)) {
762
- // Keep embedding behavior aligned with existing provider wrappers.
763
- return null;
764
- }
765
- const responseMessages = mapResponseMessagesOutput(attributes);
766
- if (responseMessages.length > 0) {
767
- return responseMessages;
768
- }
769
- const textToolObjectParts = mapTextToolObjectOutputParts(attributes);
770
- const responseFileParts = mapResponseFilesOutput(attributes);
771
- const unknownMediaParts = mapUnknownResponseAttributeMediaOutput(attributes);
772
- const genericResponseMediaParts = mapGenericResponseAttributeMediaOutput(attributes);
773
- const providerMetadataTextParts = mapProviderMetadataTextOutput(providerMetadata);
774
- const providerMetadataInlineParts = mapProviderMetadataInlineDataOutput(providerMetadata);
775
- const mergedContentParts = dedupeContentParts([...textToolObjectParts, ...responseFileParts, ...unknownMediaParts, ...genericResponseMediaParts, ...providerMetadataTextParts, ...providerMetadataInlineParts]);
776
- const contentParts = mergedContentParts;
777
- if (contentParts.length === 0) {
778
- return [];
779
- }
780
- return [{
781
- role: 'assistant',
782
- content: contentParts
783
- }];
784
- }
785
- function mapModelSettings(attributes, operationId) {
786
- const temperature = toNumber(attributes['ai.settings.temperature']) ?? toNumber(attributes['gen_ai.request.temperature']);
787
- const maxTokens = toNumber(attributes['ai.settings.maxTokens']) ?? toNumber(attributes['gen_ai.request.max_tokens']);
788
- const maxOutputTokens = toNumber(attributes['ai.settings.maxOutputTokens']);
789
- const topP = toNumber(attributes['ai.settings.topP']) ?? toNumber(attributes['gen_ai.request.top_p']);
790
- const frequencyPenalty = toNumber(attributes['ai.settings.frequencyPenalty']) ?? toNumber(attributes['gen_ai.request.frequency_penalty']);
791
- const presencePenalty = toNumber(attributes['ai.settings.presencePenalty']) ?? toNumber(attributes['gen_ai.request.presence_penalty']);
792
- const stopSequences = parseJsonValue(attributes['ai.settings.stopSequences']) ?? parseJsonValue(attributes['gen_ai.request.stop_sequences']);
793
- const stream = isDoStreamSpan(operationId);
794
- return {
795
- ...(temperature !== undefined ? {
796
- temperature
797
- } : {}),
798
- ...(maxTokens !== undefined ? {
799
- max_tokens: maxTokens
800
- } : {}),
801
- ...(maxOutputTokens !== undefined ? {
802
- max_completion_tokens: maxOutputTokens
803
- } : {}),
804
- ...(topP !== undefined ? {
805
- top_p: topP
806
- } : {}),
807
- ...(frequencyPenalty !== undefined ? {
808
- frequency_penalty: frequencyPenalty
809
- } : {}),
810
- ...(presencePenalty !== undefined ? {
811
- presence_penalty: presencePenalty
812
- } : {}),
813
- ...(stopSequences !== null ? {
814
- stop: stopSequences
815
- } : {}),
816
- ...(stream ? {
817
- stream: true
818
- } : {})
819
- };
820
- }
821
- function mapUsage(attributes, providerMetadata, operationId) {
822
- if (isDoEmbedSpan(operationId)) {
823
- const tokens = toNumber(attributes['ai.usage.tokens']) ?? toNumber(attributes['gen_ai.usage.input_tokens']) ?? 0;
824
- return {
825
- inputTokens: tokens,
826
- rawUsage: {
827
- usage: {
828
- tokens
829
- },
830
- providerMetadata
831
- }
832
- };
833
- }
834
- const inputTokens = toNumber(attributes['ai.usage.promptTokens']) ?? toNumber(attributes['gen_ai.usage.input_tokens']) ?? 0;
835
- const outputTokens = toNumber(attributes['ai.usage.completionTokens']) ?? toNumber(attributes['gen_ai.usage.output_tokens']) ?? 0;
836
- const totalTokens = toNumber(attributes['ai.usage.totalTokens']);
837
- const reasoningTokens = toNumber(attributes['ai.usage.reasoningTokens']);
838
- const cachedInputTokens = toNumber(attributes['ai.usage.cachedInputTokens']);
839
- return {
840
- inputTokens,
841
- outputTokens,
842
- ...(reasoningTokens !== undefined ? {
843
- reasoningTokens
844
- } : {}),
845
- ...(cachedInputTokens !== undefined ? {
846
- cacheReadInputTokens: cachedInputTokens
847
- } : {}),
848
- rawUsage: {
849
- usage: {
850
- promptTokens: inputTokens,
851
- completionTokens: outputTokens,
852
- ...(totalTokens !== undefined ? {
853
- totalTokens
854
- } : {})
855
- },
856
- providerMetadata
857
- }
858
- };
859
- }
860
- function parsePromptTools(attributes) {
861
- const rawTools = attributes['ai.prompt.tools'];
862
- if (!Array.isArray(rawTools)) {
863
- return null;
864
- }
865
- const parsedTools = [];
866
- for (const rawTool of rawTools) {
867
- if (typeof rawTool === 'string') {
868
- const parsed = parseJsonValue(rawTool);
869
- if (parsed !== null) {
870
- parsedTools.push(parsed);
871
- }
872
- continue;
873
- }
874
- if (rawTool && typeof rawTool === 'object') {
875
- parsedTools.push(rawTool);
876
- }
877
- }
878
- return parsedTools.length > 0 ? parsedTools : null;
879
- }
880
- function extractProviderMetadata(attributes) {
881
- const rawProviderMetadata = attributes['ai.response.providerMetadata'];
882
- return parseJsonValue(rawProviderMetadata) || {};
883
- }
884
- function getAiSdkFrameworkVersion(span) {
885
- const instrumentedSpan = span;
886
- const attributes = span.attributes || {};
887
- const instrumentationScopeVersion = toStringValue(instrumentedSpan.instrumentationScope?.version) || toStringValue(instrumentedSpan.instrumentationLibrary?.version);
888
- const aiUserAgent = toStringValue(attributes['ai.request.headers.user-agent']);
889
- const userAgentVersionMatch = aiUserAgent?.match(/\bai\/(\d+(?:\.\d+)*)\b/i);
890
- const userAgentVersion = userAgentVersionMatch?.[1];
891
- const rawVersion = instrumentationScopeVersion || userAgentVersion;
892
- if (!rawVersion) {
893
- return undefined;
894
- }
895
- const majorVersionMatch = rawVersion.match(/^v?(\d+)/i);
896
- return majorVersionMatch ? majorVersionMatch[1] : rawVersion;
897
- }
898
- function buildPosthogProperties(attributes, operationId) {
899
- const telemetryMetadata = extractAiSdkTelemetryMetadata(attributes);
900
- const finishReasons = toStringArray(parseJsonValue(attributes['gen_ai.response.finish_reasons']));
901
- const finishReason = toStringValue(attributes['ai.response.finishReason']) || finishReasons[0];
902
- const toolChoice = parseJsonValue(attributes['ai.prompt.toolChoice']) ?? attributes['ai.prompt.toolChoice'];
903
- return {
904
- ...telemetryMetadata,
905
- $ai_framework: 'vercel',
906
- ai_operation_id: operationId,
907
- ...(finishReason ? {
908
- ai_finish_reason: finishReason
909
- } : {}),
910
- ...(toStringValue(attributes['ai.response.model']) ? {
911
- ai_response_model: attributes['ai.response.model']
912
- } : {}),
913
- ...(toStringValue(attributes['gen_ai.response.model']) ? {
914
- ai_response_model: attributes['gen_ai.response.model']
915
- } : {}),
916
- ...(toStringValue(attributes['ai.response.id']) ? {
917
- ai_response_id: attributes['ai.response.id']
918
- } : {}),
919
- ...(toStringValue(attributes['gen_ai.response.id']) ? {
920
- ai_response_id: attributes['gen_ai.response.id']
921
- } : {}),
922
- ...(toStringValue(attributes['ai.response.timestamp']) ? {
923
- ai_response_timestamp: attributes['ai.response.timestamp']
924
- } : {}),
925
- ...(toNumber(attributes['ai.response.msToFinish']) !== undefined ? {
926
- ai_response_ms_to_finish: toNumber(attributes['ai.response.msToFinish'])
927
- } : {}),
928
- ...(toNumber(attributes['ai.response.avgCompletionTokensPerSecond']) !== undefined ? {
929
- ai_response_avg_completion_tokens_per_second: toNumber(attributes['ai.response.avgCompletionTokensPerSecond'])
930
- } : {}),
931
- ...(toStringValue(attributes['ai.telemetry.functionId']) ? {
932
- ai_telemetry_function_id: attributes['ai.telemetry.functionId']
933
- } : {}),
934
- ...(toNumber(attributes['ai.settings.maxRetries']) !== undefined ? {
935
- ai_settings_max_retries: toNumber(attributes['ai.settings.maxRetries'])
936
- } : {}),
937
- ...(toNumber(attributes['gen_ai.request.top_k']) !== undefined ? {
938
- ai_request_top_k: toNumber(attributes['gen_ai.request.top_k'])
939
- } : {}),
940
- ...(attributes['ai.schema.name'] !== undefined ? {
941
- ai_schema_name: attributes['ai.schema.name']
942
- } : {}),
943
- ...(attributes['ai.schema.description'] !== undefined ? {
944
- ai_schema_description: attributes['ai.schema.description']
945
- } : {}),
946
- ...(attributes['ai.settings.output'] !== undefined ? {
947
- ai_settings_output: attributes['ai.settings.output']
948
- } : {}),
949
- ...(toolChoice ? {
950
- ai_prompt_tool_choice: toolChoice
951
- } : {})
952
- };
953
- }
954
- function buildAiSdkMapperResult(span) {
955
- const attributes = span.attributes || {};
956
- const operationId = getOperationId(span);
957
- const providerMetadata = extractProviderMetadata(attributes);
958
- const model = toStringValue(attributes['ai.model.id']) || toStringValue(attributes['gen_ai.request.model']) || 'unknown';
959
- const provider = (toStringValue(attributes['ai.model.provider']) || toStringValue(attributes['gen_ai.system']) || 'unknown').toLowerCase();
960
- const latency = getSpanLatencySeconds(span);
961
- const timeToFirstTokenMs = toNumber(attributes['ai.response.msToFirstChunk']);
962
- const timeToFirstToken = timeToFirstTokenMs !== undefined ? timeToFirstTokenMs / 1000 : undefined;
963
- const input = mapPromptInput(attributes, operationId);
964
- const output = mapOutput(attributes, operationId, providerMetadata);
965
- const usage = mapUsage(attributes, providerMetadata, operationId);
966
- const modelParams = mapModelSettings(attributes, operationId);
967
- const tools = parsePromptTools(attributes);
968
- const httpStatus = toNumber(attributes['http.response.status_code']) || 200;
969
- const eventType = isDoEmbedSpan(operationId) ? AIEvent.Embedding : AIEvent.Generation;
970
- const frameworkVersion = getAiSdkFrameworkVersion(span);
971
- const error = span.status?.code === OTEL_STATUS_ERROR ? span.status.message || 'AI SDK span recorded error status' : undefined;
972
- return {
973
- model,
974
- provider,
975
- input,
976
- output,
977
- latency,
978
- timeToFirstToken,
979
- httpStatus,
980
- eventType,
981
- usage,
982
- tools,
983
- modelParams,
984
- posthogProperties: {
985
- ...buildPosthogProperties(attributes, operationId),
986
- ...(frameworkVersion ? {
987
- $ai_framework_version: frameworkVersion
988
- } : {})
989
- },
990
- error
991
- };
992
- }
993
- const aiSdkSpanMapper = {
994
- name: 'ai-sdk',
995
- canMap: shouldMapAiSdkSpan,
996
- map: span => {
997
- return buildAiSdkMapperResult(span);
998
- }
999
- };
1000
-
1001
- const defaultSpanMappers = [aiSdkSpanMapper];
1002
-
1003
- function pickMapper(span, mappers) {
1004
- return mappers.find(mapper => {
1005
- try {
1006
- return mapper.canMap(span);
1007
- } catch {
1008
- return false;
1009
- }
1010
- });
1011
- }
1012
- function getTraceId(span, options, mapperTraceId) {
1013
- if (mapperTraceId) {
1014
- return mapperTraceId;
1015
- }
1016
- if (options.posthogTraceId) {
1017
- return options.posthogTraceId;
1018
- }
1019
- const spanTraceId = span.spanContext?.().traceId;
1020
- return spanTraceId || uuid.v4();
1021
- }
1022
- function buildPosthogParams(options, traceId, distinctId, modelParams, posthogProperties) {
1023
- return {
1024
- ...modelParams,
1025
- posthogDistinctId: distinctId,
1026
- posthogTraceId: traceId,
1027
- posthogProperties,
1028
- posthogPrivacyMode: options.posthogPrivacyMode,
1029
- posthogGroups: options.posthogGroups,
1030
- posthogModelOverride: options.posthogModelOverride,
1031
- posthogProviderOverride: options.posthogProviderOverride,
1032
- posthogCostOverride: options.posthogCostOverride,
1033
- posthogCaptureImmediate: options.posthogCaptureImmediate
1034
- };
1035
- }
1036
- async function captureSpan(span, phClient, options = {}) {
1037
- if (options.shouldExportSpan && options.shouldExportSpan({
1038
- otelSpan: span
1039
- }) === false) {
1040
- return;
1041
- }
1042
- const mappers = options.mappers ?? defaultSpanMappers;
1043
- const mapper = pickMapper(span, mappers);
1044
- if (!mapper) {
1045
- return;
1046
- }
1047
- const mapped = mapper.map(span, {
1048
- options
1049
- });
1050
- if (!mapped) {
1051
- return;
1052
- }
1053
- const traceId = getTraceId(span, options, mapped.traceId);
1054
- const distinctId = mapped.distinctId ?? options.posthogDistinctId;
1055
- const posthogProperties = {
1056
- ...options.posthogProperties,
1057
- ...mapped.posthogProperties
1058
- };
1059
- const params = buildPosthogParams(options, traceId, distinctId, mapped.modelParams ?? {}, posthogProperties);
1060
- const baseURL = mapped.baseURL ?? '';
1061
- const usage = mapped.usage ?? {};
1062
- if (mapped.error !== undefined) {
1063
- await sendEventWithErrorToPosthog({
1064
- eventType: mapped.eventType,
1065
- client: phClient,
1066
- distinctId,
1067
- traceId,
1068
- model: mapped.model,
1069
- provider: mapped.provider,
1070
- input: mapped.input,
1071
- output: mapped.output,
1072
- latency: mapped.latency,
1073
- baseURL,
1074
- params: params,
1075
- usage,
1076
- tools: mapped.tools,
1077
- error: mapped.error,
1078
- captureImmediate: options.posthogCaptureImmediate
1079
- });
1080
- return;
1081
- }
1082
- await sendEventToPosthog({
1083
- eventType: mapped.eventType,
1084
- client: phClient,
1085
- distinctId,
1086
- traceId,
1087
- model: mapped.model,
1088
- provider: mapped.provider,
1089
- input: mapped.input,
1090
- output: mapped.output,
1091
- latency: mapped.latency,
1092
- timeToFirstToken: mapped.timeToFirstToken,
1093
- baseURL,
1094
- params: params,
1095
- httpStatus: mapped.httpStatus ?? 200,
1096
- usage,
1097
- tools: mapped.tools,
1098
- captureImmediate: options.posthogCaptureImmediate
1099
- });
1100
- }
1101
-
1102
- class PostHogSpanProcessor {
1103
- pendingCaptures = new Set();
1104
- constructor(phClient, options = {}) {
1105
- this.phClient = phClient;
1106
- this.options = options;
1107
- }
1108
- onStart(_span, _parentContext) {
1109
- // no-op
1110
- }
1111
- onEnd(span) {
1112
- const capturePromise = captureSpan(span, this.phClient, this.options).catch(error => {
1113
- console.error('Failed to capture telemetry span', error);
1114
- }).finally(() => {
1115
- this.pendingCaptures.delete(capturePromise);
1116
- });
1117
- this.pendingCaptures.add(capturePromise);
1118
- }
1119
- async shutdown() {
1120
- await this.forceFlush();
1121
- }
1122
- async forceFlush() {
1123
- while (this.pendingCaptures.size > 0) {
1124
- await Promise.allSettled([...this.pendingCaptures]);
1125
- }
1126
- }
1127
- }
1128
- function createPostHogSpanProcessor(phClient, options = {}) {
1129
- return new PostHogSpanProcessor(phClient, options);
1130
35
  }
1131
36
 
1132
- exports.PostHogSpanProcessor = PostHogSpanProcessor;
1133
- exports.aiSdkSpanMapper = aiSdkSpanMapper;
1134
- exports.captureSpan = captureSpan;
1135
- exports.createPostHogSpanProcessor = createPostHogSpanProcessor;
37
+ exports.PostHogTraceExporter = PostHogTraceExporter;
1136
38
  //# sourceMappingURL=index.cjs.map