@posthog/ai 8.6.4 → 8.6.6

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.
@@ -45,14 +45,16 @@ const FILE_FAMILY_TYPES = new Set(['file', 'input_file', 'document', 'media', 'f
45
45
  const KNOWN_AUDIO_FORMATS = new Set(['wav', 'mp3', 'ogg', 'flac', 'm4a', 'aac', 'webm']);
46
46
  class MediaTypeContext {
47
47
  static EMPTY = new MediaTypeContext(undefined, undefined);
48
- constructor(parent, key) {
48
+ constructor(parent, key, explicitMediaType) {
49
49
  this.parent = parent;
50
50
  this.key = key;
51
+ this.explicitMediaType = explicitMediaType;
51
52
  }
52
53
  inferMediaType() {
53
54
  return this.inferFromSiblingMime() ?? this.inferFromSiblingFormat() ?? this.inferFromParentType() ?? this.inferFromKey();
54
55
  }
55
56
  inferFromSiblingMime() {
57
+ if (this.explicitMediaType) return this.explicitMediaType;
56
58
  if (!this.parent) return undefined;
57
59
  for (const hint of MIME_HINT_KEYS) {
58
60
  const v = this.parent[hint];
@@ -87,7 +89,13 @@ class MediaTypeContext {
87
89
  if (key.includes('file') || key.includes('document')) return 'application/octet-stream';
88
90
  return undefined;
89
91
  }
92
+ hasExplicitBinaryMediaType() {
93
+ if (!this.explicitMediaType && (!this.parent || !this.key || !STRONG_CONTEXT_KEYS.has(this.key))) return false;
94
+ const mediaType = this.inferFromSiblingMime();
95
+ return mediaType !== undefined && !mediaType.toLowerCase().startsWith('text/');
96
+ }
90
97
  signalsBinary() {
98
+ if (this.explicitMediaType) return true;
91
99
  if (this.parent) {
92
100
  for (const hint of MIME_HINT_KEYS) {
93
101
  if (typeof this.parent[hint] === 'string') return true;
@@ -109,10 +117,10 @@ class BinaryContentRedactor {
109
117
  constructor(recognizer = new Base64Recognizer()) {
110
118
  this.recognizer = recognizer;
111
119
  }
112
- redact(value) {
120
+ redact(value, mediaType) {
113
121
  if (this.isMultimodalEnabled()) return value;
114
122
  this.visited = new WeakSet();
115
- return this.walk(value, MediaTypeContext.EMPTY);
123
+ return this.walk(value, mediaType ? new MediaTypeContext(undefined, undefined, mediaType) : MediaTypeContext.EMPTY);
116
124
  }
117
125
  walk(value, ctx) {
118
126
  if (value === null || value === undefined) return value;
@@ -136,8 +144,10 @@ class BinaryContentRedactor {
136
144
  return out;
137
145
  }
138
146
  redactString(value, ctx) {
139
- const minLength = ctx.signalsBinary() ? STRONG_CONTEXT_MIN_LENGTH : WEAK_CONTEXT_MIN_LENGTH;
140
- const recognition = this.recognizer.recognize(value, minLength);
147
+ const hasExplicitBinaryMediaType = ctx.hasExplicitBinaryMediaType();
148
+ const recognitionValue = hasExplicitBinaryMediaType ? value.replace(/[\r\n]/g, '') : value;
149
+ const minLength = hasExplicitBinaryMediaType ? Math.min(recognitionValue.length, STRONG_CONTEXT_MIN_LENGTH) : ctx.signalsBinary() ? STRONG_CONTEXT_MIN_LENGTH : WEAK_CONTEXT_MIN_LENGTH;
150
+ const recognition = this.recognizer.recognize(recognitionValue, minLength);
141
151
  switch (recognition.kind) {
142
152
  case 'data-url':
143
153
  return this.placeholderFor(recognition.mediaType);
@@ -538,7 +548,7 @@ function formatOpenAIResponsesInput(input, instructions) {
538
548
  return messages;
539
549
  }
540
550
 
541
- var version = "8.6.4";
551
+ var version = "8.6.6";
542
552
 
543
553
  const DEFAULT_MAX_DEPTH = 3;
544
554
  const MAX_STACK_LINES = 20;
@@ -736,7 +746,9 @@ const captureAiGeneration$1 = async (client, options) => {
736
746
  $ai_output_tokens: usage.outputTokens
737
747
  } : {}),
738
748
  ...additionalTokenValues,
739
- $ai_latency: options.latency ?? 0,
749
+ ...(options.latency !== undefined ? {
750
+ $ai_latency: options.latency
751
+ } : {}),
740
752
  ...(options.timeToFirstToken !== undefined ? {
741
753
  $ai_time_to_first_token: options.timeToFirstToken
742
754
  } : {}),
@@ -838,7 +850,7 @@ function extractRequestId(result) {
838
850
  * Assembles the `$ai_provider_metadata` blob for OpenAI / Azure OpenAI events.
839
851
  * Provider-specific fields (system fingerprint, request id) live here rather
840
852
  * than in the shared, provider-agnostic `$ai_*` namespace. Only keys with a
841
- * truthy value are included, and `undefined` is returned when there is nothing
853
+ * meaningful value are included, and `undefined` is returned when there is nothing
842
854
  * to report so the property can be omitted from the event entirely.
843
855
  */
844
856
  function buildProviderMetadata(fields) {
@@ -849,8 +861,104 @@ function buildProviderMetadata(fields) {
849
861
  if (fields.requestId) {
850
862
  metadata.request_id = fields.requestId;
851
863
  }
864
+ if (fields.incompleteDetails != null) {
865
+ metadata.incomplete_details = fields.incompleteDetails;
866
+ }
852
867
  return Object.keys(metadata).length > 0 ? metadata : undefined;
853
868
  }
869
+ const TERMINAL_RESPONSE_STATUSES = new Set(['completed', 'failed', 'cancelled', 'incomplete']);
870
+
871
+ /**
872
+ * Checks whether a Responses API response has reached a status that should
873
+ * produce a final `$ai_generation` event.
874
+ */
875
+ function isTerminalResponse(response) {
876
+ return !!response?.status && TERMINAL_RESPONSE_STATUSES.has(response.status);
877
+ }
878
+
879
+ /**
880
+ * Returns an isolated copy of a failed Responses API error for `$ai_error`, or
881
+ * creates a fallback error when the provider omitted failure details.
882
+ */
883
+ function getResponseFailure(response) {
884
+ if (response?.status !== 'failed') {
885
+ return undefined;
886
+ }
887
+ return response.error ? {
888
+ ...response.error
889
+ } : new Error(`OpenAI response ${response.id} failed without error details`);
890
+ }
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
+ }
854
962
 
855
963
  function addRequestId(result, response, requestIdHeader) {
856
964
  if (!result || typeof result !== 'object' || Array.isArray(result)) {
@@ -1330,7 +1438,7 @@ let WrappedCompletions$1 = class WrappedCompletions extends openai.AzureOpenAI.C
1330
1438
  model: openAIParams.model ?? modelFromResponse,
1331
1439
  provider: 'azure',
1332
1440
  input: sanitizeOpenAI(openAIParams.messages),
1333
- output: formattedOutput,
1441
+ output: sanitizeOpenAIResponse(formattedOutput),
1334
1442
  latency,
1335
1443
  timeToFirstToken,
1336
1444
  baseURL: this.baseURL,
@@ -1386,8 +1494,8 @@ let WrappedCompletions$1 = class WrappedCompletions extends openai.AzureOpenAI.C
1386
1494
  ...posthogParams,
1387
1495
  model: openAIParams.model ?? result.model,
1388
1496
  provider: 'azure',
1389
- input: openAIParams.messages,
1390
- output: formatResponseOpenAI(result),
1497
+ input: sanitizeOpenAI(openAIParams.messages),
1498
+ output: sanitizeOpenAIResponse(formatResponseOpenAI(result)),
1391
1499
  latency,
1392
1500
  baseURL: this.baseURL,
1393
1501
  modelParameters: getModelParams(body, result.service_tier),
@@ -1412,7 +1520,7 @@ let WrappedCompletions$1 = class WrappedCompletions extends openai.AzureOpenAI.C
1412
1520
  ...posthogParams,
1413
1521
  model: openAIParams.model,
1414
1522
  provider: 'azure',
1415
- input: openAIParams.messages,
1523
+ input: sanitizeOpenAI(openAIParams.messages),
1416
1524
  output: [],
1417
1525
  latency: 0,
1418
1526
  baseURL: this.baseURL,
@@ -1431,11 +1539,43 @@ let WrappedCompletions$1 = class WrappedCompletions extends openai.AzureOpenAI.C
1431
1539
  }
1432
1540
  };
1433
1541
  let WrappedResponses$1 = class WrappedResponses extends openai.AzureOpenAI.Responses {
1542
+ backgroundResponses = new BackgroundResponseTracker();
1434
1543
  constructor(client, phClient) {
1435
1544
  super(client);
1436
1545
  this.phClient = phClient;
1437
1546
  this.baseURL = client.baseURL;
1438
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
+ }
1439
1579
 
1440
1580
  // --- Overload #1: Non-streaming
1441
1581
 
@@ -1468,6 +1608,7 @@ let WrappedResponses$1 = class WrappedResponses extends openai.AzureOpenAI.Respo
1468
1608
  inputTokens: 0,
1469
1609
  outputTokens: 0
1470
1610
  };
1611
+ let terminalResponse;
1471
1612
  for await (const chunk of stream1) {
1472
1613
  // Track first token time on content delta events
1473
1614
  if (firstTokenTime === undefined && isResponseTokenChunk(chunk)) {
@@ -1481,12 +1622,19 @@ let WrappedResponses$1 = class WrappedResponses extends openai.AzureOpenAI.Respo
1481
1622
  if (!completionIdFromResponse && chunk.response.id) {
1482
1623
  completionIdFromResponse = chunk.response.id;
1483
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
+ }
1484
1631
  if (chunk.response.service_tier != null) {
1485
1632
  serviceTierFromResponse = chunk.response.service_tier;
1486
1633
  }
1487
- }
1488
- if (chunk.type === 'response.completed' && 'response' in chunk && chunk.response?.output && chunk.response.output.length > 0) {
1489
- finalContent = chunk.response.output;
1634
+ if (isTerminalResponse(chunk.response)) {
1635
+ terminalResponse = chunk.response;
1636
+ finalContent = chunk.response.output ?? [];
1637
+ }
1490
1638
  }
1491
1639
  if ('response' in chunk && chunk.response?.usage) {
1492
1640
  usage = {
@@ -1497,28 +1645,45 @@ let WrappedResponses$1 = class WrappedResponses extends openai.AzureOpenAI.Respo
1497
1645
  };
1498
1646
  }
1499
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
+ }
1500
1657
  const latency = (Date.now() - startTime) / 1000;
1501
1658
  const timeToFirstToken = firstTokenTime !== undefined ? (firstTokenTime - startTime) / 1000 : undefined;
1502
1659
  await captureAiGeneration(this.phClient, {
1503
1660
  ...posthogParams,
1504
1661
  model: openAIParams.model ?? modelFromResponse,
1505
1662
  provider: 'azure',
1506
- input: formatOpenAIResponsesInput(openAIParams.input, openAIParams.instructions),
1507
- output: finalContent,
1663
+ input: formatOpenAIResponsesInput(sanitizeOpenAIResponse(openAIParams.input), openAIParams.instructions),
1664
+ output: sanitizeOpenAIResponse(finalContent),
1508
1665
  latency,
1509
1666
  timeToFirstToken,
1510
1667
  baseURL: this.baseURL,
1511
1668
  modelParameters: getModelParams(body, serviceTierFromResponse),
1512
1669
  httpStatus: 200,
1513
1670
  usage,
1514
- completionId: completionIdFromResponse
1671
+ stopReason: terminalResponse?.status ?? undefined,
1672
+ completionId: completionIdFromResponse,
1673
+ providerMetadata: buildProviderMetadata({
1674
+ incompleteDetails: terminalResponse?.incomplete_details
1675
+ }),
1676
+ error: getResponseFailure(terminalResponse)
1515
1677
  });
1516
1678
  } catch (error) {
1679
+ if (openAIParams.background === true && completionIdFromResponse && this.backgroundResponses.get(completionIdFromResponse)) {
1680
+ throw error;
1681
+ }
1517
1682
  await captureAiGeneration(this.phClient, {
1518
1683
  ...posthogParams,
1519
1684
  model: openAIParams.model,
1520
1685
  provider: 'azure',
1521
- input: formatOpenAIResponsesInput(openAIParams.input, openAIParams.instructions),
1686
+ input: formatOpenAIResponsesInput(sanitizeOpenAIResponse(openAIParams.input), openAIParams.instructions),
1522
1687
  output: [],
1523
1688
  latency: 0,
1524
1689
  baseURL: this.baseURL,
@@ -1546,13 +1711,20 @@ let WrappedResponses$1 = class WrappedResponses extends openai.AzureOpenAI.Respo
1546
1711
  } else {
1547
1712
  const wrappedPromise = parentPromise.then(async result => {
1548
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
+ }
1549
1721
  const latency = (Date.now() - startTime) / 1000;
1550
1722
  await captureAiGeneration(this.phClient, {
1551
1723
  ...posthogParams,
1552
1724
  model: openAIParams.model ?? result.model,
1553
1725
  provider: 'azure',
1554
- input: formatOpenAIResponsesInput(openAIParams.input, openAIParams.instructions),
1555
- output: result.output,
1726
+ input: formatOpenAIResponsesInput(sanitizeOpenAIResponse(openAIParams.input), openAIParams.instructions),
1727
+ output: sanitizeOpenAIResponse(result.output),
1556
1728
  latency,
1557
1729
  baseURL: this.baseURL,
1558
1730
  modelParameters: getModelParams(body, result.service_tier),
@@ -1561,12 +1733,16 @@ let WrappedResponses$1 = class WrappedResponses extends openai.AzureOpenAI.Respo
1561
1733
  inputTokens: result.usage?.input_tokens ?? 0,
1562
1734
  outputTokens: result.usage?.output_tokens ?? 0,
1563
1735
  reasoningTokens: result.usage?.output_tokens_details?.reasoning_tokens ?? 0,
1564
- cacheReadInputTokens: result.usage?.input_tokens_details?.cached_tokens ?? 0
1736
+ cacheReadInputTokens: result.usage?.input_tokens_details?.cached_tokens ?? 0,
1737
+ rawUsage: result.usage
1565
1738
  },
1739
+ stopReason: result.status ?? undefined,
1566
1740
  completionId: result.id,
1567
1741
  providerMetadata: buildProviderMetadata({
1568
- requestId: extractRequestId(result)
1569
- })
1742
+ requestId: extractRequestId(result),
1743
+ incompleteDetails: result.incomplete_details
1744
+ }),
1745
+ error: getResponseFailure(result)
1570
1746
  });
1571
1747
  }
1572
1748
  return result;
@@ -1576,7 +1752,7 @@ let WrappedResponses$1 = class WrappedResponses extends openai.AzureOpenAI.Respo
1576
1752
  ...posthogParams,
1577
1753
  model: openAIParams.model,
1578
1754
  provider: 'azure',
1579
- input: formatOpenAIResponsesInput(openAIParams.input, openAIParams.instructions),
1755
+ input: formatOpenAIResponsesInput(sanitizeOpenAIResponse(openAIParams.input), openAIParams.instructions),
1580
1756
  output: [],
1581
1757
  latency: 0,
1582
1758
  baseURL: this.baseURL,
@@ -1593,6 +1769,55 @@ let WrappedResponses$1 = class WrappedResponses extends openai.AzureOpenAI.Respo
1593
1769
  return preserveProviderPromise(parentPromise, wrappedPromise);
1594
1770
  }
1595
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
+ }
1596
1821
  parse(body, options) {
1597
1822
  const {
1598
1823
  providerParams: openAIParams,
@@ -1601,13 +1826,20 @@ let WrappedResponses$1 = class WrappedResponses extends openai.AzureOpenAI.Respo
1601
1826
  const startTime = Date.now();
1602
1827
  const parentPromise = callWithOriginalCreate(this, super.create.bind(this), () => super.parse(openAIParams, options));
1603
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
+ }
1604
1836
  const latency = (Date.now() - startTime) / 1000;
1605
1837
  await captureAiGeneration(this.phClient, {
1606
1838
  ...posthogParams,
1607
1839
  model: openAIParams.model ?? result.model,
1608
1840
  provider: 'azure',
1609
- input: formatOpenAIResponsesInput(openAIParams.input, openAIParams.instructions),
1610
- output: result.output,
1841
+ input: formatOpenAIResponsesInput(sanitizeOpenAIResponse(openAIParams.input), openAIParams.instructions),
1842
+ output: sanitizeOpenAIResponse(result.output),
1611
1843
  latency,
1612
1844
  baseURL: this.baseURL,
1613
1845
  modelParameters: getModelParams(body, result.service_tier),
@@ -1616,12 +1848,16 @@ let WrappedResponses$1 = class WrappedResponses extends openai.AzureOpenAI.Respo
1616
1848
  inputTokens: result.usage?.input_tokens ?? 0,
1617
1849
  outputTokens: result.usage?.output_tokens ?? 0,
1618
1850
  reasoningTokens: result.usage?.output_tokens_details?.reasoning_tokens ?? 0,
1619
- cacheReadInputTokens: result.usage?.input_tokens_details?.cached_tokens ?? 0
1851
+ cacheReadInputTokens: result.usage?.input_tokens_details?.cached_tokens ?? 0,
1852
+ rawUsage: result.usage
1620
1853
  },
1854
+ stopReason: result.status ?? undefined,
1621
1855
  completionId: result.id,
1622
1856
  providerMetadata: buildProviderMetadata({
1623
- requestId: extractRequestId(result)
1624
- })
1857
+ requestId: extractRequestId(result),
1858
+ incompleteDetails: result.incomplete_details
1859
+ }),
1860
+ error: getResponseFailure(result)
1625
1861
  });
1626
1862
  return result;
1627
1863
  }, async error => {
@@ -1629,7 +1865,7 @@ let WrappedResponses$1 = class WrappedResponses extends openai.AzureOpenAI.Respo
1629
1865
  ...posthogParams,
1630
1866
  model: openAIParams.model,
1631
1867
  provider: 'azure',
1632
- input: formatOpenAIResponsesInput(openAIParams.input, openAIParams.instructions),
1868
+ input: formatOpenAIResponsesInput(sanitizeOpenAIResponse(openAIParams.input), openAIParams.instructions),
1633
1869
  output: [],
1634
1870
  latency: 0,
1635
1871
  baseURL: this.baseURL,
@@ -1719,10 +1955,6 @@ async function captureAiGenerationAfterSuccess(...args) {
1719
1955
  captureAiGenerationInBackground(...args);
1720
1956
  }
1721
1957
  }
1722
- const TERMINAL_RESPONSE_STATUSES = new Set(['completed', 'failed', 'cancelled', 'incomplete']);
1723
- function isPendingBackgroundResponse(params, response) {
1724
- return params.background === true && !response.usage && !!response.status && !TERMINAL_RESPONSE_STATUSES.has(response.status);
1725
- }
1726
1958
  class PostHogOpenAI extends openai.OpenAI {
1727
1959
  constructor(config) {
1728
1960
  const {
@@ -1909,7 +2141,7 @@ class WrappedCompletions extends Completions {
1909
2141
  model: openAIParams.model ?? modelFromResponse,
1910
2142
  provider: 'openai',
1911
2143
  input: sanitizeOpenAI(openAIParams.messages),
1912
- output: formattedOutput,
2144
+ output: sanitizeOpenAIResponse(formattedOutput),
1913
2145
  latency,
1914
2146
  timeToFirstToken,
1915
2147
  baseURL: this.baseURL,
@@ -1977,7 +2209,7 @@ class WrappedCompletions extends Completions {
1977
2209
  model: openAIParams.model ?? result.model,
1978
2210
  provider: 'openai',
1979
2211
  input: sanitizeOpenAI(openAIParams.messages),
1980
- output: formattedOutput,
2212
+ output: sanitizeOpenAIResponse(formattedOutput),
1981
2213
  latency,
1982
2214
  baseURL: this.baseURL,
1983
2215
  modelParameters: getModelParams(body, result.service_tier),
@@ -2025,11 +2257,47 @@ class WrappedCompletions extends Completions {
2025
2257
  }
2026
2258
  }
2027
2259
  class WrappedResponses extends Responses {
2260
+ backgroundResponses = new BackgroundResponseTracker();
2028
2261
  constructor(client, phClient) {
2029
2262
  super(client);
2030
2263
  this.phClient = phClient;
2031
2264
  this.baseURL = client.baseURL;
2032
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
+ }
2033
2301
 
2034
2302
  // --- Overload #1: Non-streaming
2035
2303
 
@@ -2065,6 +2333,7 @@ class WrappedResponses extends Responses {
2065
2333
  webSearchCount: 0
2066
2334
  };
2067
2335
  let rawUsageData;
2336
+ let terminalResponse;
2068
2337
  for await (const chunk of stream1) {
2069
2338
  // Track first token time on content delta events
2070
2339
  if (firstTokenTime === undefined && isResponseTokenChunk(chunk)) {
@@ -2078,6 +2347,12 @@ class WrappedResponses extends Responses {
2078
2347
  if (!completionIdFromResponse && chunk.response.id) {
2079
2348
  completionIdFromResponse = chunk.response.id;
2080
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
+ }
2081
2356
  if (chunk.response.service_tier != null) {
2082
2357
  serviceTierFromResponse = chunk.response.service_tier;
2083
2358
  }
@@ -2085,10 +2360,9 @@ class WrappedResponses extends Responses {
2085
2360
  if (chunkWebSearchCount > 0 && chunkWebSearchCount > (usage.webSearchCount ?? 0)) {
2086
2361
  usage.webSearchCount = chunkWebSearchCount;
2087
2362
  }
2088
- }
2089
- if (chunk.type === 'response.completed' && 'response' in chunk && chunk.response?.output && chunk.response.output.length > 0) {
2090
- finalContent = chunk.response.output;
2091
- if (chunk.response.status) {
2363
+ if (isTerminalResponse(chunk.response)) {
2364
+ terminalResponse = chunk.response;
2365
+ finalContent = chunk.response.output ?? [];
2092
2366
  stopReason = chunk.response.status;
2093
2367
  }
2094
2368
  }
@@ -2103,6 +2377,15 @@ class WrappedResponses extends Responses {
2103
2377
  };
2104
2378
  }
2105
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
+ }
2106
2389
  const latency = (Date.now() - startTime) / 1000;
2107
2390
  const timeToFirstToken = firstTokenTime !== undefined ? (firstTokenTime - startTime) / 1000 : undefined;
2108
2391
  const availableTools = extractAvailableToolCalls('openai', openAIParams);
@@ -2111,7 +2394,7 @@ class WrappedResponses extends Responses {
2111
2394
  model: openAIParams.model ?? modelFromResponse,
2112
2395
  provider: 'openai',
2113
2396
  input: formatOpenAIResponsesInput(sanitizeOpenAIResponse(openAIParams.input), openAIParams.instructions),
2114
- output: finalContent,
2397
+ output: sanitizeOpenAIResponse(finalContent),
2115
2398
  latency,
2116
2399
  timeToFirstToken,
2117
2400
  baseURL: this.baseURL,
@@ -2127,9 +2410,16 @@ class WrappedResponses extends Responses {
2127
2410
  },
2128
2411
  stopReason,
2129
2412
  tools: availableTools,
2130
- completionId: completionIdFromResponse
2413
+ completionId: completionIdFromResponse,
2414
+ providerMetadata: buildProviderMetadata({
2415
+ incompleteDetails: terminalResponse?.incomplete_details
2416
+ }),
2417
+ error: getResponseFailure(terminalResponse)
2131
2418
  });
2132
2419
  } catch (error) {
2420
+ if (openAIParams.background === true && completionIdFromResponse && this.backgroundResponses.get(completionIdFromResponse)) {
2421
+ throw error;
2422
+ }
2133
2423
  await captureAiGeneration(this.phClient, {
2134
2424
  ...posthogParams,
2135
2425
  model: openAIParams.model,
@@ -2163,6 +2453,10 @@ class WrappedResponses extends Responses {
2163
2453
  const wrappedPromise = parentPromise.then(async result => {
2164
2454
  if ('output' in result) {
2165
2455
  if (isPendingBackgroundResponse(openAIParams, result)) {
2456
+ this.backgroundResponses.set(result.id, {
2457
+ openAIParams,
2458
+ posthogParams
2459
+ });
2166
2460
  return result;
2167
2461
  }
2168
2462
  const latency = (Date.now() - startTime) / 1000;
@@ -2175,7 +2469,7 @@ class WrappedResponses extends Responses {
2175
2469
  model: openAIParams.model ?? result.model,
2176
2470
  provider: 'openai',
2177
2471
  input: formatOpenAIResponsesInput(sanitizeOpenAIResponse(openAIParams.input), openAIParams.instructions),
2178
- output: formattedOutput,
2472
+ output: sanitizeOpenAIResponse(formattedOutput),
2179
2473
  latency,
2180
2474
  baseURL: this.baseURL,
2181
2475
  modelParameters: getModelParams(body, result.service_tier),
@@ -2192,8 +2486,10 @@ class WrappedResponses extends Responses {
2192
2486
  tools: availableTools,
2193
2487
  completionId: result.id,
2194
2488
  providerMetadata: buildProviderMetadata({
2195
- requestId: extractRequestId(result)
2196
- })
2489
+ requestId: extractRequestId(result),
2490
+ incompleteDetails: result.incomplete_details
2491
+ }),
2492
+ error: getResponseFailure(result)
2197
2493
  });
2198
2494
  }
2199
2495
  return result;
@@ -2220,6 +2516,55 @@ class WrappedResponses extends Responses {
2220
2516
  return preserveProviderPromise(parentPromise, wrappedPromise);
2221
2517
  }
2222
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
+ }
2223
2568
  parse(body, options) {
2224
2569
  const {
2225
2570
  providerParams: openAIParams,
@@ -2229,6 +2574,10 @@ class WrappedResponses extends Responses {
2229
2574
  const parentPromise = callWithOriginalCreate(this, super.create.bind(this), () => super.parse(openAIParams, options));
2230
2575
  const wrappedPromise = parentPromise.then(async result => {
2231
2576
  if (isPendingBackgroundResponse(openAIParams, result)) {
2577
+ this.backgroundResponses.set(result.id, {
2578
+ openAIParams,
2579
+ posthogParams
2580
+ });
2232
2581
  return result;
2233
2582
  }
2234
2583
  const latency = (Date.now() - startTime) / 1000;
@@ -2237,7 +2586,7 @@ class WrappedResponses extends Responses {
2237
2586
  model: openAIParams.model ?? result.model,
2238
2587
  provider: 'openai',
2239
2588
  input: formatOpenAIResponsesInput(sanitizeOpenAIResponse(openAIParams.input), openAIParams.instructions),
2240
- output: result.output,
2589
+ output: sanitizeOpenAIResponse(result.output),
2241
2590
  latency,
2242
2591
  baseURL: this.baseURL,
2243
2592
  modelParameters: getModelParams(body, result.service_tier),
@@ -2252,8 +2601,10 @@ class WrappedResponses extends Responses {
2252
2601
  stopReason: result.status ?? undefined,
2253
2602
  completionId: result.id,
2254
2603
  providerMetadata: buildProviderMetadata({
2255
- requestId: extractRequestId(result)
2256
- })
2604
+ requestId: extractRequestId(result),
2605
+ incompleteDetails: result.incomplete_details
2606
+ }),
2607
+ error: getResponseFailure(result)
2257
2608
  });
2258
2609
  return result;
2259
2610
  }, async error => {
@@ -2406,7 +2757,7 @@ class WrappedTranscriptions extends Transcriptions {
2406
2757
  model: openAIParams.model,
2407
2758
  provider: 'openai',
2408
2759
  input: openAIParams.prompt,
2409
- output: finalContent,
2760
+ output: sanitizeOpenAIResponse(finalContent),
2410
2761
  latency,
2411
2762
  timeToFirstToken,
2412
2763
  baseURL: this.baseURL,
@@ -2451,7 +2802,7 @@ class WrappedTranscriptions extends Transcriptions {
2451
2802
  model: openAIParams.model,
2452
2803
  provider: 'openai',
2453
2804
  input: openAIParams.prompt,
2454
- output: result.text,
2805
+ output: sanitizeOpenAIResponse(result.text),
2455
2806
  latency,
2456
2807
  baseURL: this.baseURL,
2457
2808
  modelParameters: getModelParams(body),