@posthog/ai 8.6.5 → 8.6.7

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.
@@ -548,7 +548,7 @@ function formatOpenAIResponsesInput(input, instructions) {
548
548
  return messages;
549
549
  }
550
550
 
551
- var version = "8.6.5";
551
+ var version = "8.6.7";
552
552
 
553
553
  const DEFAULT_MAX_DEPTH = 3;
554
554
  const MAX_STACK_LINES = 20;
@@ -746,7 +746,9 @@ const captureAiGeneration$1 = async (client, options) => {
746
746
  $ai_output_tokens: usage.outputTokens
747
747
  } : {}),
748
748
  ...additionalTokenValues,
749
- $ai_latency: options.latency ?? 0,
749
+ ...(options.latency !== undefined ? {
750
+ $ai_latency: options.latency
751
+ } : {}),
750
752
  ...(options.timeToFirstToken !== undefined ? {
751
753
  $ai_time_to_first_token: options.timeToFirstToken
752
754
  } : {}),
@@ -887,6 +889,77 @@ function getResponseFailure(response) {
887
889
  } : new Error(`OpenAI response ${response.id} failed without error details`);
888
890
  }
889
891
 
892
+ function isPendingBackgroundResponse(params, response) {
893
+ return params.background === true && !!response.status && !isTerminalResponse(response);
894
+ }
895
+
896
+ /**
897
+ * Uses provider timestamps so background polling cadence does not inflate
898
+ * generation latency. Non-completed responses do not expose a terminal time.
899
+ */
900
+ function getBackgroundResponseLatency(response) {
901
+ if (typeof response.created_at !== 'number' || typeof response.completed_at !== 'number') {
902
+ return undefined;
903
+ }
904
+ return Math.max(0, response.completed_at - response.created_at);
905
+ }
906
+
907
+ /**
908
+ * Keeps the original create context available while a background response is
909
+ * polled. Entries are insertion ordered, so the oldest context is discarded
910
+ * when the bound is reached.
911
+ */
912
+ class BackgroundResponseTracker {
913
+ contexts = new Map();
914
+ constructor(maxEntries = 1000) {
915
+ this.maxEntries = maxEntries;
916
+ }
917
+ set(responseID, context) {
918
+ // Refresh an existing response's insertion order.
919
+ this.contexts.delete(responseID);
920
+ this.contexts.set(responseID, context);
921
+ while (this.contexts.size > this.maxEntries) {
922
+ const oldestResponseID = this.contexts.keys().next().value;
923
+ if (oldestResponseID === undefined) {
924
+ break;
925
+ }
926
+ this.contexts.delete(oldestResponseID);
927
+ }
928
+ }
929
+ get(responseID) {
930
+ return this.contexts.get(responseID);
931
+ }
932
+ take(responseID) {
933
+ const context = this.contexts.get(responseID);
934
+ if (context !== undefined) {
935
+ this.contexts.delete(responseID);
936
+ }
937
+ return context;
938
+ }
939
+ }
940
+
941
+ /**
942
+ * Inspects a streamed background retrieval without consuming it on the
943
+ * caller's behalf. The stored create context is consumed only by a terminal
944
+ * response; an interrupted or nonterminal stream may be followed by another
945
+ * retrieval while the background job continues.
946
+ */
947
+ function wrapBackgroundResponseStream(stream, responseID, tracker, captureTerminalResponse) {
948
+ async function* inspectStream() {
949
+ for await (const event of stream) {
950
+ if ('response' in event && isTerminalResponse(event.response)) {
951
+ const context = tracker.take(responseID);
952
+ if (context) {
953
+ // Monitoring must not delay or disrupt delivery of the provider stream.
954
+ void captureTerminalResponse(event.response, context).catch(() => undefined);
955
+ }
956
+ }
957
+ yield event;
958
+ }
959
+ }
960
+ return new streaming.Stream(() => inspectStream(), stream.controller);
961
+ }
962
+
890
963
  function addRequestId(result, response, requestIdHeader) {
891
964
  if (!result || typeof result !== 'object' || Array.isArray(result)) {
892
965
  return result;
@@ -1466,11 +1539,43 @@ let WrappedCompletions$1 = class WrappedCompletions extends openai.AzureOpenAI.C
1466
1539
  }
1467
1540
  };
1468
1541
  let WrappedResponses$1 = class WrappedResponses extends openai.AzureOpenAI.Responses {
1542
+ backgroundResponses = new BackgroundResponseTracker();
1469
1543
  constructor(client, phClient) {
1470
1544
  super(client);
1471
1545
  this.phClient = phClient;
1472
1546
  this.baseURL = client.baseURL;
1473
1547
  }
1548
+ async captureBackgroundResponse(result, context) {
1549
+ const {
1550
+ openAIParams,
1551
+ posthogParams
1552
+ } = context;
1553
+ await captureAiGeneration(this.phClient, {
1554
+ ...posthogParams,
1555
+ model: openAIParams.model ?? result.model,
1556
+ provider: 'azure',
1557
+ input: formatOpenAIResponsesInput(openAIParams.input, openAIParams.instructions),
1558
+ output: result.output,
1559
+ latency: getBackgroundResponseLatency(result),
1560
+ baseURL: this.baseURL,
1561
+ modelParameters: getModelParams(openAIParams, result.service_tier),
1562
+ httpStatus: 200,
1563
+ usage: {
1564
+ inputTokens: result.usage?.input_tokens ?? 0,
1565
+ outputTokens: result.usage?.output_tokens ?? 0,
1566
+ reasoningTokens: result.usage?.output_tokens_details?.reasoning_tokens ?? 0,
1567
+ cacheReadInputTokens: result.usage?.input_tokens_details?.cached_tokens ?? 0,
1568
+ rawUsage: result.usage
1569
+ },
1570
+ stopReason: result.status ?? undefined,
1571
+ completionId: result.id,
1572
+ providerMetadata: buildProviderMetadata({
1573
+ requestId: extractRequestId(result),
1574
+ incompleteDetails: result.incomplete_details
1575
+ }),
1576
+ error: getResponseFailure(result)
1577
+ });
1578
+ }
1474
1579
 
1475
1580
  // --- Overload #1: Non-streaming
1476
1581
 
@@ -1517,6 +1622,12 @@ let WrappedResponses$1 = class WrappedResponses extends openai.AzureOpenAI.Respo
1517
1622
  if (!completionIdFromResponse && chunk.response.id) {
1518
1623
  completionIdFromResponse = chunk.response.id;
1519
1624
  }
1625
+ if (openAIParams.background === true && !this.backgroundResponses.get(chunk.response.id)) {
1626
+ this.backgroundResponses.set(chunk.response.id, {
1627
+ openAIParams,
1628
+ posthogParams
1629
+ });
1630
+ }
1520
1631
  if (chunk.response.service_tier != null) {
1521
1632
  serviceTierFromResponse = chunk.response.service_tier;
1522
1633
  }
@@ -1534,6 +1645,15 @@ let WrappedResponses$1 = class WrappedResponses extends openai.AzureOpenAI.Respo
1534
1645
  };
1535
1646
  }
1536
1647
  }
1648
+ if (openAIParams.background === true) {
1649
+ if (terminalResponse) {
1650
+ const context = this.backgroundResponses.take(terminalResponse.id);
1651
+ if (context) {
1652
+ await this.captureBackgroundResponse(terminalResponse, context).catch(() => undefined);
1653
+ }
1654
+ }
1655
+ return;
1656
+ }
1537
1657
  const latency = (Date.now() - startTime) / 1000;
1538
1658
  const timeToFirstToken = firstTokenTime !== undefined ? (firstTokenTime - startTime) / 1000 : undefined;
1539
1659
  await captureAiGeneration(this.phClient, {
@@ -1556,6 +1676,9 @@ let WrappedResponses$1 = class WrappedResponses extends openai.AzureOpenAI.Respo
1556
1676
  error: getResponseFailure(terminalResponse)
1557
1677
  });
1558
1678
  } catch (error) {
1679
+ if (openAIParams.background === true && completionIdFromResponse && this.backgroundResponses.get(completionIdFromResponse)) {
1680
+ throw error;
1681
+ }
1559
1682
  await captureAiGeneration(this.phClient, {
1560
1683
  ...posthogParams,
1561
1684
  model: openAIParams.model,
@@ -1588,6 +1711,13 @@ let WrappedResponses$1 = class WrappedResponses extends openai.AzureOpenAI.Respo
1588
1711
  } else {
1589
1712
  const wrappedPromise = parentPromise.then(async result => {
1590
1713
  if ('output' in result) {
1714
+ if (isPendingBackgroundResponse(openAIParams, result)) {
1715
+ this.backgroundResponses.set(result.id, {
1716
+ openAIParams,
1717
+ posthogParams
1718
+ });
1719
+ return result;
1720
+ }
1591
1721
  const latency = (Date.now() - startTime) / 1000;
1592
1722
  await captureAiGeneration(this.phClient, {
1593
1723
  ...posthogParams,
@@ -1603,7 +1733,8 @@ let WrappedResponses$1 = class WrappedResponses extends openai.AzureOpenAI.Respo
1603
1733
  inputTokens: result.usage?.input_tokens ?? 0,
1604
1734
  outputTokens: result.usage?.output_tokens ?? 0,
1605
1735
  reasoningTokens: result.usage?.output_tokens_details?.reasoning_tokens ?? 0,
1606
- cacheReadInputTokens: result.usage?.input_tokens_details?.cached_tokens ?? 0
1736
+ cacheReadInputTokens: result.usage?.input_tokens_details?.cached_tokens ?? 0,
1737
+ rawUsage: result.usage
1607
1738
  },
1608
1739
  stopReason: result.status ?? undefined,
1609
1740
  completionId: result.id,
@@ -1638,6 +1769,55 @@ let WrappedResponses$1 = class WrappedResponses extends openai.AzureOpenAI.Respo
1638
1769
  return preserveProviderPromise(parentPromise, wrappedPromise);
1639
1770
  }
1640
1771
  }
1772
+ retrieve(responseID, query = {}, options) {
1773
+ const parentPromise = super.retrieve(responseID, query, options);
1774
+
1775
+ // Preserve the upstream promise and stream unchanged for responses that
1776
+ // were not created through this client.
1777
+ if (!this.backgroundResponses.get(responseID)) {
1778
+ return parentPromise;
1779
+ }
1780
+ if (query.stream) {
1781
+ return parentPromise._thenUnwrap(result => {
1782
+ if ('controller' in result) {
1783
+ return wrapBackgroundResponseStream(result, responseID, this.backgroundResponses, (response, context) => this.captureBackgroundResponse(response, context));
1784
+ }
1785
+ return result;
1786
+ });
1787
+ }
1788
+ return parentPromise._thenUnwrap(async result => {
1789
+ if (!('output' in result) || !isTerminalResponse(result)) {
1790
+ return result;
1791
+ }
1792
+
1793
+ // Removing the context before capture makes concurrent or repeated
1794
+ // terminal polls idempotent.
1795
+ const context = this.backgroundResponses.take(responseID);
1796
+ if (context) {
1797
+ await this.captureBackgroundResponse(result, context).catch(() => undefined);
1798
+ }
1799
+ return result;
1800
+ });
1801
+ }
1802
+ cancel(responseID, options) {
1803
+ const parentPromise = super.cancel(responseID, options);
1804
+
1805
+ // Avoid wrapping calls that do not belong to a background response created
1806
+ // through this client, preserving the upstream APIPromise unchanged.
1807
+ if (!this.backgroundResponses.get(responseID)) {
1808
+ return parentPromise;
1809
+ }
1810
+ return parentPromise._thenUnwrap(async result => {
1811
+ if (!isTerminalResponse(result)) {
1812
+ return result;
1813
+ }
1814
+ const context = this.backgroundResponses.take(responseID);
1815
+ if (context) {
1816
+ await this.captureBackgroundResponse(result, context).catch(() => undefined);
1817
+ }
1818
+ return result;
1819
+ });
1820
+ }
1641
1821
  parse(body, options) {
1642
1822
  const {
1643
1823
  providerParams: openAIParams,
@@ -1646,6 +1826,13 @@ let WrappedResponses$1 = class WrappedResponses extends openai.AzureOpenAI.Respo
1646
1826
  const startTime = Date.now();
1647
1827
  const parentPromise = callWithOriginalCreate(this, super.create.bind(this), () => super.parse(openAIParams, options));
1648
1828
  const wrappedPromise = parentPromise.then(async result => {
1829
+ if (isPendingBackgroundResponse(openAIParams, result)) {
1830
+ this.backgroundResponses.set(result.id, {
1831
+ openAIParams,
1832
+ posthogParams
1833
+ });
1834
+ return result;
1835
+ }
1649
1836
  const latency = (Date.now() - startTime) / 1000;
1650
1837
  await captureAiGeneration(this.phClient, {
1651
1838
  ...posthogParams,
@@ -1661,7 +1848,8 @@ let WrappedResponses$1 = class WrappedResponses extends openai.AzureOpenAI.Respo
1661
1848
  inputTokens: result.usage?.input_tokens ?? 0,
1662
1849
  outputTokens: result.usage?.output_tokens ?? 0,
1663
1850
  reasoningTokens: result.usage?.output_tokens_details?.reasoning_tokens ?? 0,
1664
- cacheReadInputTokens: result.usage?.input_tokens_details?.cached_tokens ?? 0
1851
+ cacheReadInputTokens: result.usage?.input_tokens_details?.cached_tokens ?? 0,
1852
+ rawUsage: result.usage
1665
1853
  },
1666
1854
  stopReason: result.status ?? undefined,
1667
1855
  completionId: result.id,
@@ -1767,9 +1955,6 @@ async function captureAiGenerationAfterSuccess(...args) {
1767
1955
  captureAiGenerationInBackground(...args);
1768
1956
  }
1769
1957
  }
1770
- function isPendingBackgroundResponse(params, response) {
1771
- return params.background === true && !response.usage && !!response.status && !isTerminalResponse(response);
1772
- }
1773
1958
  class PostHogOpenAI extends openai.OpenAI {
1774
1959
  constructor(config) {
1775
1960
  const {
@@ -2072,11 +2257,47 @@ class WrappedCompletions extends Completions {
2072
2257
  }
2073
2258
  }
2074
2259
  class WrappedResponses extends Responses {
2260
+ backgroundResponses = new BackgroundResponseTracker();
2075
2261
  constructor(client, phClient) {
2076
2262
  super(client);
2077
2263
  this.phClient = phClient;
2078
2264
  this.baseURL = client.baseURL;
2079
2265
  }
2266
+ async captureBackgroundResponse(result, context) {
2267
+ const {
2268
+ openAIParams,
2269
+ posthogParams
2270
+ } = context;
2271
+ await captureAiGenerationAfterSuccess(this.phClient, {
2272
+ ...posthogParams,
2273
+ model: openAIParams.model ?? result.model,
2274
+ provider: 'openai',
2275
+ input: formatOpenAIResponsesInput(sanitizeOpenAIResponse(openAIParams.input), openAIParams.instructions),
2276
+ output: formatResponseOpenAI({
2277
+ output: result.output
2278
+ }),
2279
+ latency: getBackgroundResponseLatency(result),
2280
+ baseURL: this.baseURL,
2281
+ modelParameters: getModelParams(openAIParams, result.service_tier),
2282
+ httpStatus: 200,
2283
+ usage: {
2284
+ inputTokens: result.usage?.input_tokens ?? 0,
2285
+ outputTokens: result.usage?.output_tokens ?? 0,
2286
+ reasoningTokens: result.usage?.output_tokens_details?.reasoning_tokens ?? 0,
2287
+ cacheReadInputTokens: result.usage?.input_tokens_details?.cached_tokens ?? 0,
2288
+ webSearchCount: calculateWebSearchCount(result),
2289
+ rawUsage: result.usage
2290
+ },
2291
+ stopReason: result.status ?? undefined,
2292
+ tools: extractAvailableToolCalls('openai', openAIParams),
2293
+ completionId: result.id,
2294
+ providerMetadata: buildProviderMetadata({
2295
+ requestId: extractRequestId(result),
2296
+ incompleteDetails: result.incomplete_details
2297
+ }),
2298
+ error: getResponseFailure(result)
2299
+ });
2300
+ }
2080
2301
 
2081
2302
  // --- Overload #1: Non-streaming
2082
2303
 
@@ -2126,6 +2347,12 @@ class WrappedResponses extends Responses {
2126
2347
  if (!completionIdFromResponse && chunk.response.id) {
2127
2348
  completionIdFromResponse = chunk.response.id;
2128
2349
  }
2350
+ if (openAIParams.background === true && !this.backgroundResponses.get(chunk.response.id)) {
2351
+ this.backgroundResponses.set(chunk.response.id, {
2352
+ openAIParams,
2353
+ posthogParams
2354
+ });
2355
+ }
2129
2356
  if (chunk.response.service_tier != null) {
2130
2357
  serviceTierFromResponse = chunk.response.service_tier;
2131
2358
  }
@@ -2150,6 +2377,15 @@ class WrappedResponses extends Responses {
2150
2377
  };
2151
2378
  }
2152
2379
  }
2380
+ if (openAIParams.background === true) {
2381
+ if (terminalResponse) {
2382
+ const context = this.backgroundResponses.take(terminalResponse.id);
2383
+ if (context) {
2384
+ await this.captureBackgroundResponse(terminalResponse, context).catch(() => undefined);
2385
+ }
2386
+ }
2387
+ return;
2388
+ }
2153
2389
  const latency = (Date.now() - startTime) / 1000;
2154
2390
  const timeToFirstToken = firstTokenTime !== undefined ? (firstTokenTime - startTime) / 1000 : undefined;
2155
2391
  const availableTools = extractAvailableToolCalls('openai', openAIParams);
@@ -2181,6 +2417,9 @@ class WrappedResponses extends Responses {
2181
2417
  error: getResponseFailure(terminalResponse)
2182
2418
  });
2183
2419
  } catch (error) {
2420
+ if (openAIParams.background === true && completionIdFromResponse && this.backgroundResponses.get(completionIdFromResponse)) {
2421
+ throw error;
2422
+ }
2184
2423
  await captureAiGeneration(this.phClient, {
2185
2424
  ...posthogParams,
2186
2425
  model: openAIParams.model,
@@ -2214,6 +2453,10 @@ class WrappedResponses extends Responses {
2214
2453
  const wrappedPromise = parentPromise.then(async result => {
2215
2454
  if ('output' in result) {
2216
2455
  if (isPendingBackgroundResponse(openAIParams, result)) {
2456
+ this.backgroundResponses.set(result.id, {
2457
+ openAIParams,
2458
+ posthogParams
2459
+ });
2217
2460
  return result;
2218
2461
  }
2219
2462
  const latency = (Date.now() - startTime) / 1000;
@@ -2273,6 +2516,55 @@ class WrappedResponses extends Responses {
2273
2516
  return preserveProviderPromise(parentPromise, wrappedPromise);
2274
2517
  }
2275
2518
  }
2519
+ retrieve(responseID, query = {}, options) {
2520
+ const parentPromise = super.retrieve(responseID, query, options);
2521
+
2522
+ // Preserve the upstream promise and stream unchanged for responses that
2523
+ // were not created through this client.
2524
+ if (!this.backgroundResponses.get(responseID)) {
2525
+ return parentPromise;
2526
+ }
2527
+ if (query.stream) {
2528
+ return parentPromise._thenUnwrap(result => {
2529
+ if ('controller' in result) {
2530
+ return wrapBackgroundResponseStream(result, responseID, this.backgroundResponses, (response, context) => this.captureBackgroundResponse(response, context));
2531
+ }
2532
+ return result;
2533
+ });
2534
+ }
2535
+ return parentPromise._thenUnwrap(async result => {
2536
+ if (!('output' in result) || !isTerminalResponse(result)) {
2537
+ return result;
2538
+ }
2539
+
2540
+ // Removing the context before capture makes concurrent or repeated
2541
+ // terminal polls idempotent.
2542
+ const context = this.backgroundResponses.take(responseID);
2543
+ if (context) {
2544
+ await this.captureBackgroundResponse(result, context).catch(() => undefined);
2545
+ }
2546
+ return result;
2547
+ });
2548
+ }
2549
+ cancel(responseID, options) {
2550
+ const parentPromise = super.cancel(responseID, options);
2551
+
2552
+ // Avoid wrapping calls that do not belong to a background response created
2553
+ // through this client, preserving the upstream APIPromise unchanged.
2554
+ if (!this.backgroundResponses.get(responseID)) {
2555
+ return parentPromise;
2556
+ }
2557
+ return parentPromise._thenUnwrap(async result => {
2558
+ if (!isTerminalResponse(result)) {
2559
+ return result;
2560
+ }
2561
+ const context = this.backgroundResponses.take(responseID);
2562
+ if (context) {
2563
+ await this.captureBackgroundResponse(result, context).catch(() => undefined);
2564
+ }
2565
+ return result;
2566
+ });
2567
+ }
2276
2568
  parse(body, options) {
2277
2569
  const {
2278
2570
  providerParams: openAIParams,
@@ -2282,6 +2574,10 @@ class WrappedResponses extends Responses {
2282
2574
  const parentPromise = callWithOriginalCreate(this, super.create.bind(this), () => super.parse(openAIParams, options));
2283
2575
  const wrappedPromise = parentPromise.then(async result => {
2284
2576
  if (isPendingBackgroundResponse(openAIParams, result)) {
2577
+ this.backgroundResponses.set(result.id, {
2578
+ openAIParams,
2579
+ posthogParams
2580
+ });
2285
2581
  return result;
2286
2582
  }
2287
2583
  const latency = (Date.now() - startTime) / 1000;