@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.
@@ -41,14 +41,16 @@ const FILE_FAMILY_TYPES = new Set(['file', 'input_file', 'document', 'media', 'f
41
41
  const KNOWN_AUDIO_FORMATS = new Set(['wav', 'mp3', 'ogg', 'flac', 'm4a', 'aac', 'webm']);
42
42
  class MediaTypeContext {
43
43
  static EMPTY = new MediaTypeContext(undefined, undefined);
44
- constructor(parent, key) {
44
+ constructor(parent, key, explicitMediaType) {
45
45
  this.parent = parent;
46
46
  this.key = key;
47
+ this.explicitMediaType = explicitMediaType;
47
48
  }
48
49
  inferMediaType() {
49
50
  return this.inferFromSiblingMime() ?? this.inferFromSiblingFormat() ?? this.inferFromParentType() ?? this.inferFromKey();
50
51
  }
51
52
  inferFromSiblingMime() {
53
+ if (this.explicitMediaType) return this.explicitMediaType;
52
54
  if (!this.parent) return undefined;
53
55
  for (const hint of MIME_HINT_KEYS) {
54
56
  const v = this.parent[hint];
@@ -83,7 +85,13 @@ class MediaTypeContext {
83
85
  if (key.includes('file') || key.includes('document')) return 'application/octet-stream';
84
86
  return undefined;
85
87
  }
88
+ hasExplicitBinaryMediaType() {
89
+ if (!this.explicitMediaType && (!this.parent || !this.key || !STRONG_CONTEXT_KEYS.has(this.key))) return false;
90
+ const mediaType = this.inferFromSiblingMime();
91
+ return mediaType !== undefined && !mediaType.toLowerCase().startsWith('text/');
92
+ }
86
93
  signalsBinary() {
94
+ if (this.explicitMediaType) return true;
87
95
  if (this.parent) {
88
96
  for (const hint of MIME_HINT_KEYS) {
89
97
  if (typeof this.parent[hint] === 'string') return true;
@@ -105,10 +113,10 @@ class BinaryContentRedactor {
105
113
  constructor(recognizer = new Base64Recognizer()) {
106
114
  this.recognizer = recognizer;
107
115
  }
108
- redact(value) {
116
+ redact(value, mediaType) {
109
117
  if (this.isMultimodalEnabled()) return value;
110
118
  this.visited = new WeakSet();
111
- return this.walk(value, MediaTypeContext.EMPTY);
119
+ return this.walk(value, mediaType ? new MediaTypeContext(undefined, undefined, mediaType) : MediaTypeContext.EMPTY);
112
120
  }
113
121
  walk(value, ctx) {
114
122
  if (value === null || value === undefined) return value;
@@ -132,8 +140,10 @@ class BinaryContentRedactor {
132
140
  return out;
133
141
  }
134
142
  redactString(value, ctx) {
135
- const minLength = ctx.signalsBinary() ? STRONG_CONTEXT_MIN_LENGTH : WEAK_CONTEXT_MIN_LENGTH;
136
- const recognition = this.recognizer.recognize(value, minLength);
143
+ const hasExplicitBinaryMediaType = ctx.hasExplicitBinaryMediaType();
144
+ const recognitionValue = hasExplicitBinaryMediaType ? value.replace(/[\r\n]/g, '') : value;
145
+ const minLength = hasExplicitBinaryMediaType ? Math.min(recognitionValue.length, STRONG_CONTEXT_MIN_LENGTH) : ctx.signalsBinary() ? STRONG_CONTEXT_MIN_LENGTH : WEAK_CONTEXT_MIN_LENGTH;
146
+ const recognition = this.recognizer.recognize(recognitionValue, minLength);
137
147
  switch (recognition.kind) {
138
148
  case 'data-url':
139
149
  return this.placeholderFor(recognition.mediaType);
@@ -534,7 +544,7 @@ function formatOpenAIResponsesInput(input, instructions) {
534
544
  return messages;
535
545
  }
536
546
 
537
- var version = "8.6.4";
547
+ var version = "8.6.6";
538
548
 
539
549
  const DEFAULT_MAX_DEPTH = 3;
540
550
  const MAX_STACK_LINES = 20;
@@ -732,7 +742,9 @@ const captureAiGeneration$1 = async (client, options) => {
732
742
  $ai_output_tokens: usage.outputTokens
733
743
  } : {}),
734
744
  ...additionalTokenValues,
735
- $ai_latency: options.latency ?? 0,
745
+ ...(options.latency !== undefined ? {
746
+ $ai_latency: options.latency
747
+ } : {}),
736
748
  ...(options.timeToFirstToken !== undefined ? {
737
749
  $ai_time_to_first_token: options.timeToFirstToken
738
750
  } : {}),
@@ -834,7 +846,7 @@ function extractRequestId(result) {
834
846
  * Assembles the `$ai_provider_metadata` blob for OpenAI / Azure OpenAI events.
835
847
  * Provider-specific fields (system fingerprint, request id) live here rather
836
848
  * than in the shared, provider-agnostic `$ai_*` namespace. Only keys with a
837
- * truthy value are included, and `undefined` is returned when there is nothing
849
+ * meaningful value are included, and `undefined` is returned when there is nothing
838
850
  * to report so the property can be omitted from the event entirely.
839
851
  */
840
852
  function buildProviderMetadata(fields) {
@@ -845,8 +857,104 @@ function buildProviderMetadata(fields) {
845
857
  if (fields.requestId) {
846
858
  metadata.request_id = fields.requestId;
847
859
  }
860
+ if (fields.incompleteDetails != null) {
861
+ metadata.incomplete_details = fields.incompleteDetails;
862
+ }
848
863
  return Object.keys(metadata).length > 0 ? metadata : undefined;
849
864
  }
865
+ const TERMINAL_RESPONSE_STATUSES = new Set(['completed', 'failed', 'cancelled', 'incomplete']);
866
+
867
+ /**
868
+ * Checks whether a Responses API response has reached a status that should
869
+ * produce a final `$ai_generation` event.
870
+ */
871
+ function isTerminalResponse(response) {
872
+ return !!response?.status && TERMINAL_RESPONSE_STATUSES.has(response.status);
873
+ }
874
+
875
+ /**
876
+ * Returns an isolated copy of a failed Responses API error for `$ai_error`, or
877
+ * creates a fallback error when the provider omitted failure details.
878
+ */
879
+ function getResponseFailure(response) {
880
+ if (response?.status !== 'failed') {
881
+ return undefined;
882
+ }
883
+ return response.error ? {
884
+ ...response.error
885
+ } : new Error(`OpenAI response ${response.id} failed without error details`);
886
+ }
887
+
888
+ function isPendingBackgroundResponse(params, response) {
889
+ return params.background === true && !!response.status && !isTerminalResponse(response);
890
+ }
891
+
892
+ /**
893
+ * Uses provider timestamps so background polling cadence does not inflate
894
+ * generation latency. Non-completed responses do not expose a terminal time.
895
+ */
896
+ function getBackgroundResponseLatency(response) {
897
+ if (typeof response.created_at !== 'number' || typeof response.completed_at !== 'number') {
898
+ return undefined;
899
+ }
900
+ return Math.max(0, response.completed_at - response.created_at);
901
+ }
902
+
903
+ /**
904
+ * Keeps the original create context available while a background response is
905
+ * polled. Entries are insertion ordered, so the oldest context is discarded
906
+ * when the bound is reached.
907
+ */
908
+ class BackgroundResponseTracker {
909
+ contexts = new Map();
910
+ constructor(maxEntries = 1000) {
911
+ this.maxEntries = maxEntries;
912
+ }
913
+ set(responseID, context) {
914
+ // Refresh an existing response's insertion order.
915
+ this.contexts.delete(responseID);
916
+ this.contexts.set(responseID, context);
917
+ while (this.contexts.size > this.maxEntries) {
918
+ const oldestResponseID = this.contexts.keys().next().value;
919
+ if (oldestResponseID === undefined) {
920
+ break;
921
+ }
922
+ this.contexts.delete(oldestResponseID);
923
+ }
924
+ }
925
+ get(responseID) {
926
+ return this.contexts.get(responseID);
927
+ }
928
+ take(responseID) {
929
+ const context = this.contexts.get(responseID);
930
+ if (context !== undefined) {
931
+ this.contexts.delete(responseID);
932
+ }
933
+ return context;
934
+ }
935
+ }
936
+
937
+ /**
938
+ * Inspects a streamed background retrieval without consuming it on the
939
+ * caller's behalf. The stored create context is consumed only by a terminal
940
+ * response; an interrupted or nonterminal stream may be followed by another
941
+ * retrieval while the background job continues.
942
+ */
943
+ function wrapBackgroundResponseStream(stream, responseID, tracker, captureTerminalResponse) {
944
+ async function* inspectStream() {
945
+ for await (const event of stream) {
946
+ if ('response' in event && isTerminalResponse(event.response)) {
947
+ const context = tracker.take(responseID);
948
+ if (context) {
949
+ // Monitoring must not delay or disrupt delivery of the provider stream.
950
+ void captureTerminalResponse(event.response, context).catch(() => undefined);
951
+ }
952
+ }
953
+ yield event;
954
+ }
955
+ }
956
+ return new Stream(() => inspectStream(), stream.controller);
957
+ }
850
958
 
851
959
  function addRequestId(result, response, requestIdHeader) {
852
960
  if (!result || typeof result !== 'object' || Array.isArray(result)) {
@@ -1326,7 +1434,7 @@ let WrappedCompletions$1 = class WrappedCompletions extends AzureOpenAI.Chat.Com
1326
1434
  model: openAIParams.model ?? modelFromResponse,
1327
1435
  provider: 'azure',
1328
1436
  input: sanitizeOpenAI(openAIParams.messages),
1329
- output: formattedOutput,
1437
+ output: sanitizeOpenAIResponse(formattedOutput),
1330
1438
  latency,
1331
1439
  timeToFirstToken,
1332
1440
  baseURL: this.baseURL,
@@ -1382,8 +1490,8 @@ let WrappedCompletions$1 = class WrappedCompletions extends AzureOpenAI.Chat.Com
1382
1490
  ...posthogParams,
1383
1491
  model: openAIParams.model ?? result.model,
1384
1492
  provider: 'azure',
1385
- input: openAIParams.messages,
1386
- output: formatResponseOpenAI(result),
1493
+ input: sanitizeOpenAI(openAIParams.messages),
1494
+ output: sanitizeOpenAIResponse(formatResponseOpenAI(result)),
1387
1495
  latency,
1388
1496
  baseURL: this.baseURL,
1389
1497
  modelParameters: getModelParams(body, result.service_tier),
@@ -1408,7 +1516,7 @@ let WrappedCompletions$1 = class WrappedCompletions extends AzureOpenAI.Chat.Com
1408
1516
  ...posthogParams,
1409
1517
  model: openAIParams.model,
1410
1518
  provider: 'azure',
1411
- input: openAIParams.messages,
1519
+ input: sanitizeOpenAI(openAIParams.messages),
1412
1520
  output: [],
1413
1521
  latency: 0,
1414
1522
  baseURL: this.baseURL,
@@ -1427,11 +1535,43 @@ let WrappedCompletions$1 = class WrappedCompletions extends AzureOpenAI.Chat.Com
1427
1535
  }
1428
1536
  };
1429
1537
  let WrappedResponses$1 = class WrappedResponses extends AzureOpenAI.Responses {
1538
+ backgroundResponses = new BackgroundResponseTracker();
1430
1539
  constructor(client, phClient) {
1431
1540
  super(client);
1432
1541
  this.phClient = phClient;
1433
1542
  this.baseURL = client.baseURL;
1434
1543
  }
1544
+ async captureBackgroundResponse(result, context) {
1545
+ const {
1546
+ openAIParams,
1547
+ posthogParams
1548
+ } = context;
1549
+ await captureAiGeneration(this.phClient, {
1550
+ ...posthogParams,
1551
+ model: openAIParams.model ?? result.model,
1552
+ provider: 'azure',
1553
+ input: formatOpenAIResponsesInput(openAIParams.input, openAIParams.instructions),
1554
+ output: result.output,
1555
+ latency: getBackgroundResponseLatency(result),
1556
+ baseURL: this.baseURL,
1557
+ modelParameters: getModelParams(openAIParams, result.service_tier),
1558
+ httpStatus: 200,
1559
+ usage: {
1560
+ inputTokens: result.usage?.input_tokens ?? 0,
1561
+ outputTokens: result.usage?.output_tokens ?? 0,
1562
+ reasoningTokens: result.usage?.output_tokens_details?.reasoning_tokens ?? 0,
1563
+ cacheReadInputTokens: result.usage?.input_tokens_details?.cached_tokens ?? 0,
1564
+ rawUsage: result.usage
1565
+ },
1566
+ stopReason: result.status ?? undefined,
1567
+ completionId: result.id,
1568
+ providerMetadata: buildProviderMetadata({
1569
+ requestId: extractRequestId(result),
1570
+ incompleteDetails: result.incomplete_details
1571
+ }),
1572
+ error: getResponseFailure(result)
1573
+ });
1574
+ }
1435
1575
 
1436
1576
  // --- Overload #1: Non-streaming
1437
1577
 
@@ -1464,6 +1604,7 @@ let WrappedResponses$1 = class WrappedResponses extends AzureOpenAI.Responses {
1464
1604
  inputTokens: 0,
1465
1605
  outputTokens: 0
1466
1606
  };
1607
+ let terminalResponse;
1467
1608
  for await (const chunk of stream1) {
1468
1609
  // Track first token time on content delta events
1469
1610
  if (firstTokenTime === undefined && isResponseTokenChunk(chunk)) {
@@ -1477,12 +1618,19 @@ let WrappedResponses$1 = class WrappedResponses extends AzureOpenAI.Responses {
1477
1618
  if (!completionIdFromResponse && chunk.response.id) {
1478
1619
  completionIdFromResponse = chunk.response.id;
1479
1620
  }
1621
+ if (openAIParams.background === true && !this.backgroundResponses.get(chunk.response.id)) {
1622
+ this.backgroundResponses.set(chunk.response.id, {
1623
+ openAIParams,
1624
+ posthogParams
1625
+ });
1626
+ }
1480
1627
  if (chunk.response.service_tier != null) {
1481
1628
  serviceTierFromResponse = chunk.response.service_tier;
1482
1629
  }
1483
- }
1484
- if (chunk.type === 'response.completed' && 'response' in chunk && chunk.response?.output && chunk.response.output.length > 0) {
1485
- finalContent = chunk.response.output;
1630
+ if (isTerminalResponse(chunk.response)) {
1631
+ terminalResponse = chunk.response;
1632
+ finalContent = chunk.response.output ?? [];
1633
+ }
1486
1634
  }
1487
1635
  if ('response' in chunk && chunk.response?.usage) {
1488
1636
  usage = {
@@ -1493,28 +1641,45 @@ let WrappedResponses$1 = class WrappedResponses extends AzureOpenAI.Responses {
1493
1641
  };
1494
1642
  }
1495
1643
  }
1644
+ if (openAIParams.background === true) {
1645
+ if (terminalResponse) {
1646
+ const context = this.backgroundResponses.take(terminalResponse.id);
1647
+ if (context) {
1648
+ await this.captureBackgroundResponse(terminalResponse, context).catch(() => undefined);
1649
+ }
1650
+ }
1651
+ return;
1652
+ }
1496
1653
  const latency = (Date.now() - startTime) / 1000;
1497
1654
  const timeToFirstToken = firstTokenTime !== undefined ? (firstTokenTime - startTime) / 1000 : undefined;
1498
1655
  await captureAiGeneration(this.phClient, {
1499
1656
  ...posthogParams,
1500
1657
  model: openAIParams.model ?? modelFromResponse,
1501
1658
  provider: 'azure',
1502
- input: formatOpenAIResponsesInput(openAIParams.input, openAIParams.instructions),
1503
- output: finalContent,
1659
+ input: formatOpenAIResponsesInput(sanitizeOpenAIResponse(openAIParams.input), openAIParams.instructions),
1660
+ output: sanitizeOpenAIResponse(finalContent),
1504
1661
  latency,
1505
1662
  timeToFirstToken,
1506
1663
  baseURL: this.baseURL,
1507
1664
  modelParameters: getModelParams(body, serviceTierFromResponse),
1508
1665
  httpStatus: 200,
1509
1666
  usage,
1510
- completionId: completionIdFromResponse
1667
+ stopReason: terminalResponse?.status ?? undefined,
1668
+ completionId: completionIdFromResponse,
1669
+ providerMetadata: buildProviderMetadata({
1670
+ incompleteDetails: terminalResponse?.incomplete_details
1671
+ }),
1672
+ error: getResponseFailure(terminalResponse)
1511
1673
  });
1512
1674
  } catch (error) {
1675
+ if (openAIParams.background === true && completionIdFromResponse && this.backgroundResponses.get(completionIdFromResponse)) {
1676
+ throw error;
1677
+ }
1513
1678
  await captureAiGeneration(this.phClient, {
1514
1679
  ...posthogParams,
1515
1680
  model: openAIParams.model,
1516
1681
  provider: 'azure',
1517
- input: formatOpenAIResponsesInput(openAIParams.input, openAIParams.instructions),
1682
+ input: formatOpenAIResponsesInput(sanitizeOpenAIResponse(openAIParams.input), openAIParams.instructions),
1518
1683
  output: [],
1519
1684
  latency: 0,
1520
1685
  baseURL: this.baseURL,
@@ -1542,13 +1707,20 @@ let WrappedResponses$1 = class WrappedResponses extends AzureOpenAI.Responses {
1542
1707
  } else {
1543
1708
  const wrappedPromise = parentPromise.then(async result => {
1544
1709
  if ('output' in result) {
1710
+ if (isPendingBackgroundResponse(openAIParams, result)) {
1711
+ this.backgroundResponses.set(result.id, {
1712
+ openAIParams,
1713
+ posthogParams
1714
+ });
1715
+ return result;
1716
+ }
1545
1717
  const latency = (Date.now() - startTime) / 1000;
1546
1718
  await captureAiGeneration(this.phClient, {
1547
1719
  ...posthogParams,
1548
1720
  model: openAIParams.model ?? result.model,
1549
1721
  provider: 'azure',
1550
- input: formatOpenAIResponsesInput(openAIParams.input, openAIParams.instructions),
1551
- output: result.output,
1722
+ input: formatOpenAIResponsesInput(sanitizeOpenAIResponse(openAIParams.input), openAIParams.instructions),
1723
+ output: sanitizeOpenAIResponse(result.output),
1552
1724
  latency,
1553
1725
  baseURL: this.baseURL,
1554
1726
  modelParameters: getModelParams(body, result.service_tier),
@@ -1557,12 +1729,16 @@ let WrappedResponses$1 = class WrappedResponses extends AzureOpenAI.Responses {
1557
1729
  inputTokens: result.usage?.input_tokens ?? 0,
1558
1730
  outputTokens: result.usage?.output_tokens ?? 0,
1559
1731
  reasoningTokens: result.usage?.output_tokens_details?.reasoning_tokens ?? 0,
1560
- cacheReadInputTokens: result.usage?.input_tokens_details?.cached_tokens ?? 0
1732
+ cacheReadInputTokens: result.usage?.input_tokens_details?.cached_tokens ?? 0,
1733
+ rawUsage: result.usage
1561
1734
  },
1735
+ stopReason: result.status ?? undefined,
1562
1736
  completionId: result.id,
1563
1737
  providerMetadata: buildProviderMetadata({
1564
- requestId: extractRequestId(result)
1565
- })
1738
+ requestId: extractRequestId(result),
1739
+ incompleteDetails: result.incomplete_details
1740
+ }),
1741
+ error: getResponseFailure(result)
1566
1742
  });
1567
1743
  }
1568
1744
  return result;
@@ -1572,7 +1748,7 @@ let WrappedResponses$1 = class WrappedResponses extends AzureOpenAI.Responses {
1572
1748
  ...posthogParams,
1573
1749
  model: openAIParams.model,
1574
1750
  provider: 'azure',
1575
- input: formatOpenAIResponsesInput(openAIParams.input, openAIParams.instructions),
1751
+ input: formatOpenAIResponsesInput(sanitizeOpenAIResponse(openAIParams.input), openAIParams.instructions),
1576
1752
  output: [],
1577
1753
  latency: 0,
1578
1754
  baseURL: this.baseURL,
@@ -1589,6 +1765,55 @@ let WrappedResponses$1 = class WrappedResponses extends AzureOpenAI.Responses {
1589
1765
  return preserveProviderPromise(parentPromise, wrappedPromise);
1590
1766
  }
1591
1767
  }
1768
+ retrieve(responseID, query = {}, options) {
1769
+ const parentPromise = super.retrieve(responseID, query, options);
1770
+
1771
+ // Preserve the upstream promise and stream unchanged for responses that
1772
+ // were not created through this client.
1773
+ if (!this.backgroundResponses.get(responseID)) {
1774
+ return parentPromise;
1775
+ }
1776
+ if (query.stream) {
1777
+ return parentPromise._thenUnwrap(result => {
1778
+ if ('controller' in result) {
1779
+ return wrapBackgroundResponseStream(result, responseID, this.backgroundResponses, (response, context) => this.captureBackgroundResponse(response, context));
1780
+ }
1781
+ return result;
1782
+ });
1783
+ }
1784
+ return parentPromise._thenUnwrap(async result => {
1785
+ if (!('output' in result) || !isTerminalResponse(result)) {
1786
+ return result;
1787
+ }
1788
+
1789
+ // Removing the context before capture makes concurrent or repeated
1790
+ // terminal polls idempotent.
1791
+ const context = this.backgroundResponses.take(responseID);
1792
+ if (context) {
1793
+ await this.captureBackgroundResponse(result, context).catch(() => undefined);
1794
+ }
1795
+ return result;
1796
+ });
1797
+ }
1798
+ cancel(responseID, options) {
1799
+ const parentPromise = super.cancel(responseID, options);
1800
+
1801
+ // Avoid wrapping calls that do not belong to a background response created
1802
+ // through this client, preserving the upstream APIPromise unchanged.
1803
+ if (!this.backgroundResponses.get(responseID)) {
1804
+ return parentPromise;
1805
+ }
1806
+ return parentPromise._thenUnwrap(async result => {
1807
+ if (!isTerminalResponse(result)) {
1808
+ return result;
1809
+ }
1810
+ const context = this.backgroundResponses.take(responseID);
1811
+ if (context) {
1812
+ await this.captureBackgroundResponse(result, context).catch(() => undefined);
1813
+ }
1814
+ return result;
1815
+ });
1816
+ }
1592
1817
  parse(body, options) {
1593
1818
  const {
1594
1819
  providerParams: openAIParams,
@@ -1597,13 +1822,20 @@ let WrappedResponses$1 = class WrappedResponses extends AzureOpenAI.Responses {
1597
1822
  const startTime = Date.now();
1598
1823
  const parentPromise = callWithOriginalCreate(this, super.create.bind(this), () => super.parse(openAIParams, options));
1599
1824
  const wrappedPromise = parentPromise.then(async result => {
1825
+ if (isPendingBackgroundResponse(openAIParams, result)) {
1826
+ this.backgroundResponses.set(result.id, {
1827
+ openAIParams,
1828
+ posthogParams
1829
+ });
1830
+ return result;
1831
+ }
1600
1832
  const latency = (Date.now() - startTime) / 1000;
1601
1833
  await captureAiGeneration(this.phClient, {
1602
1834
  ...posthogParams,
1603
1835
  model: openAIParams.model ?? result.model,
1604
1836
  provider: 'azure',
1605
- input: formatOpenAIResponsesInput(openAIParams.input, openAIParams.instructions),
1606
- output: result.output,
1837
+ input: formatOpenAIResponsesInput(sanitizeOpenAIResponse(openAIParams.input), openAIParams.instructions),
1838
+ output: sanitizeOpenAIResponse(result.output),
1607
1839
  latency,
1608
1840
  baseURL: this.baseURL,
1609
1841
  modelParameters: getModelParams(body, result.service_tier),
@@ -1612,12 +1844,16 @@ let WrappedResponses$1 = class WrappedResponses extends AzureOpenAI.Responses {
1612
1844
  inputTokens: result.usage?.input_tokens ?? 0,
1613
1845
  outputTokens: result.usage?.output_tokens ?? 0,
1614
1846
  reasoningTokens: result.usage?.output_tokens_details?.reasoning_tokens ?? 0,
1615
- cacheReadInputTokens: result.usage?.input_tokens_details?.cached_tokens ?? 0
1847
+ cacheReadInputTokens: result.usage?.input_tokens_details?.cached_tokens ?? 0,
1848
+ rawUsage: result.usage
1616
1849
  },
1850
+ stopReason: result.status ?? undefined,
1617
1851
  completionId: result.id,
1618
1852
  providerMetadata: buildProviderMetadata({
1619
- requestId: extractRequestId(result)
1620
- })
1853
+ requestId: extractRequestId(result),
1854
+ incompleteDetails: result.incomplete_details
1855
+ }),
1856
+ error: getResponseFailure(result)
1621
1857
  });
1622
1858
  return result;
1623
1859
  }, async error => {
@@ -1625,7 +1861,7 @@ let WrappedResponses$1 = class WrappedResponses extends AzureOpenAI.Responses {
1625
1861
  ...posthogParams,
1626
1862
  model: openAIParams.model,
1627
1863
  provider: 'azure',
1628
- input: formatOpenAIResponsesInput(openAIParams.input, openAIParams.instructions),
1864
+ input: formatOpenAIResponsesInput(sanitizeOpenAIResponse(openAIParams.input), openAIParams.instructions),
1629
1865
  output: [],
1630
1866
  latency: 0,
1631
1867
  baseURL: this.baseURL,
@@ -1715,10 +1951,6 @@ async function captureAiGenerationAfterSuccess(...args) {
1715
1951
  captureAiGenerationInBackground(...args);
1716
1952
  }
1717
1953
  }
1718
- const TERMINAL_RESPONSE_STATUSES = new Set(['completed', 'failed', 'cancelled', 'incomplete']);
1719
- function isPendingBackgroundResponse(params, response) {
1720
- return params.background === true && !response.usage && !!response.status && !TERMINAL_RESPONSE_STATUSES.has(response.status);
1721
- }
1722
1954
  class PostHogOpenAI extends OpenAI {
1723
1955
  constructor(config) {
1724
1956
  const {
@@ -1905,7 +2137,7 @@ class WrappedCompletions extends Completions {
1905
2137
  model: openAIParams.model ?? modelFromResponse,
1906
2138
  provider: 'openai',
1907
2139
  input: sanitizeOpenAI(openAIParams.messages),
1908
- output: formattedOutput,
2140
+ output: sanitizeOpenAIResponse(formattedOutput),
1909
2141
  latency,
1910
2142
  timeToFirstToken,
1911
2143
  baseURL: this.baseURL,
@@ -1973,7 +2205,7 @@ class WrappedCompletions extends Completions {
1973
2205
  model: openAIParams.model ?? result.model,
1974
2206
  provider: 'openai',
1975
2207
  input: sanitizeOpenAI(openAIParams.messages),
1976
- output: formattedOutput,
2208
+ output: sanitizeOpenAIResponse(formattedOutput),
1977
2209
  latency,
1978
2210
  baseURL: this.baseURL,
1979
2211
  modelParameters: getModelParams(body, result.service_tier),
@@ -2021,11 +2253,47 @@ class WrappedCompletions extends Completions {
2021
2253
  }
2022
2254
  }
2023
2255
  class WrappedResponses extends Responses {
2256
+ backgroundResponses = new BackgroundResponseTracker();
2024
2257
  constructor(client, phClient) {
2025
2258
  super(client);
2026
2259
  this.phClient = phClient;
2027
2260
  this.baseURL = client.baseURL;
2028
2261
  }
2262
+ async captureBackgroundResponse(result, context) {
2263
+ const {
2264
+ openAIParams,
2265
+ posthogParams
2266
+ } = context;
2267
+ await captureAiGenerationAfterSuccess(this.phClient, {
2268
+ ...posthogParams,
2269
+ model: openAIParams.model ?? result.model,
2270
+ provider: 'openai',
2271
+ input: formatOpenAIResponsesInput(sanitizeOpenAIResponse(openAIParams.input), openAIParams.instructions),
2272
+ output: formatResponseOpenAI({
2273
+ output: result.output
2274
+ }),
2275
+ latency: getBackgroundResponseLatency(result),
2276
+ baseURL: this.baseURL,
2277
+ modelParameters: getModelParams(openAIParams, result.service_tier),
2278
+ httpStatus: 200,
2279
+ usage: {
2280
+ inputTokens: result.usage?.input_tokens ?? 0,
2281
+ outputTokens: result.usage?.output_tokens ?? 0,
2282
+ reasoningTokens: result.usage?.output_tokens_details?.reasoning_tokens ?? 0,
2283
+ cacheReadInputTokens: result.usage?.input_tokens_details?.cached_tokens ?? 0,
2284
+ webSearchCount: calculateWebSearchCount(result),
2285
+ rawUsage: result.usage
2286
+ },
2287
+ stopReason: result.status ?? undefined,
2288
+ tools: extractAvailableToolCalls('openai', openAIParams),
2289
+ completionId: result.id,
2290
+ providerMetadata: buildProviderMetadata({
2291
+ requestId: extractRequestId(result),
2292
+ incompleteDetails: result.incomplete_details
2293
+ }),
2294
+ error: getResponseFailure(result)
2295
+ });
2296
+ }
2029
2297
 
2030
2298
  // --- Overload #1: Non-streaming
2031
2299
 
@@ -2061,6 +2329,7 @@ class WrappedResponses extends Responses {
2061
2329
  webSearchCount: 0
2062
2330
  };
2063
2331
  let rawUsageData;
2332
+ let terminalResponse;
2064
2333
  for await (const chunk of stream1) {
2065
2334
  // Track first token time on content delta events
2066
2335
  if (firstTokenTime === undefined && isResponseTokenChunk(chunk)) {
@@ -2074,6 +2343,12 @@ class WrappedResponses extends Responses {
2074
2343
  if (!completionIdFromResponse && chunk.response.id) {
2075
2344
  completionIdFromResponse = chunk.response.id;
2076
2345
  }
2346
+ if (openAIParams.background === true && !this.backgroundResponses.get(chunk.response.id)) {
2347
+ this.backgroundResponses.set(chunk.response.id, {
2348
+ openAIParams,
2349
+ posthogParams
2350
+ });
2351
+ }
2077
2352
  if (chunk.response.service_tier != null) {
2078
2353
  serviceTierFromResponse = chunk.response.service_tier;
2079
2354
  }
@@ -2081,10 +2356,9 @@ class WrappedResponses extends Responses {
2081
2356
  if (chunkWebSearchCount > 0 && chunkWebSearchCount > (usage.webSearchCount ?? 0)) {
2082
2357
  usage.webSearchCount = chunkWebSearchCount;
2083
2358
  }
2084
- }
2085
- if (chunk.type === 'response.completed' && 'response' in chunk && chunk.response?.output && chunk.response.output.length > 0) {
2086
- finalContent = chunk.response.output;
2087
- if (chunk.response.status) {
2359
+ if (isTerminalResponse(chunk.response)) {
2360
+ terminalResponse = chunk.response;
2361
+ finalContent = chunk.response.output ?? [];
2088
2362
  stopReason = chunk.response.status;
2089
2363
  }
2090
2364
  }
@@ -2099,6 +2373,15 @@ class WrappedResponses extends Responses {
2099
2373
  };
2100
2374
  }
2101
2375
  }
2376
+ if (openAIParams.background === true) {
2377
+ if (terminalResponse) {
2378
+ const context = this.backgroundResponses.take(terminalResponse.id);
2379
+ if (context) {
2380
+ await this.captureBackgroundResponse(terminalResponse, context).catch(() => undefined);
2381
+ }
2382
+ }
2383
+ return;
2384
+ }
2102
2385
  const latency = (Date.now() - startTime) / 1000;
2103
2386
  const timeToFirstToken = firstTokenTime !== undefined ? (firstTokenTime - startTime) / 1000 : undefined;
2104
2387
  const availableTools = extractAvailableToolCalls('openai', openAIParams);
@@ -2107,7 +2390,7 @@ class WrappedResponses extends Responses {
2107
2390
  model: openAIParams.model ?? modelFromResponse,
2108
2391
  provider: 'openai',
2109
2392
  input: formatOpenAIResponsesInput(sanitizeOpenAIResponse(openAIParams.input), openAIParams.instructions),
2110
- output: finalContent,
2393
+ output: sanitizeOpenAIResponse(finalContent),
2111
2394
  latency,
2112
2395
  timeToFirstToken,
2113
2396
  baseURL: this.baseURL,
@@ -2123,9 +2406,16 @@ class WrappedResponses extends Responses {
2123
2406
  },
2124
2407
  stopReason,
2125
2408
  tools: availableTools,
2126
- completionId: completionIdFromResponse
2409
+ completionId: completionIdFromResponse,
2410
+ providerMetadata: buildProviderMetadata({
2411
+ incompleteDetails: terminalResponse?.incomplete_details
2412
+ }),
2413
+ error: getResponseFailure(terminalResponse)
2127
2414
  });
2128
2415
  } catch (error) {
2416
+ if (openAIParams.background === true && completionIdFromResponse && this.backgroundResponses.get(completionIdFromResponse)) {
2417
+ throw error;
2418
+ }
2129
2419
  await captureAiGeneration(this.phClient, {
2130
2420
  ...posthogParams,
2131
2421
  model: openAIParams.model,
@@ -2159,6 +2449,10 @@ class WrappedResponses extends Responses {
2159
2449
  const wrappedPromise = parentPromise.then(async result => {
2160
2450
  if ('output' in result) {
2161
2451
  if (isPendingBackgroundResponse(openAIParams, result)) {
2452
+ this.backgroundResponses.set(result.id, {
2453
+ openAIParams,
2454
+ posthogParams
2455
+ });
2162
2456
  return result;
2163
2457
  }
2164
2458
  const latency = (Date.now() - startTime) / 1000;
@@ -2171,7 +2465,7 @@ class WrappedResponses extends Responses {
2171
2465
  model: openAIParams.model ?? result.model,
2172
2466
  provider: 'openai',
2173
2467
  input: formatOpenAIResponsesInput(sanitizeOpenAIResponse(openAIParams.input), openAIParams.instructions),
2174
- output: formattedOutput,
2468
+ output: sanitizeOpenAIResponse(formattedOutput),
2175
2469
  latency,
2176
2470
  baseURL: this.baseURL,
2177
2471
  modelParameters: getModelParams(body, result.service_tier),
@@ -2188,8 +2482,10 @@ class WrappedResponses extends Responses {
2188
2482
  tools: availableTools,
2189
2483
  completionId: result.id,
2190
2484
  providerMetadata: buildProviderMetadata({
2191
- requestId: extractRequestId(result)
2192
- })
2485
+ requestId: extractRequestId(result),
2486
+ incompleteDetails: result.incomplete_details
2487
+ }),
2488
+ error: getResponseFailure(result)
2193
2489
  });
2194
2490
  }
2195
2491
  return result;
@@ -2216,6 +2512,55 @@ class WrappedResponses extends Responses {
2216
2512
  return preserveProviderPromise(parentPromise, wrappedPromise);
2217
2513
  }
2218
2514
  }
2515
+ retrieve(responseID, query = {}, options) {
2516
+ const parentPromise = super.retrieve(responseID, query, options);
2517
+
2518
+ // Preserve the upstream promise and stream unchanged for responses that
2519
+ // were not created through this client.
2520
+ if (!this.backgroundResponses.get(responseID)) {
2521
+ return parentPromise;
2522
+ }
2523
+ if (query.stream) {
2524
+ return parentPromise._thenUnwrap(result => {
2525
+ if ('controller' in result) {
2526
+ return wrapBackgroundResponseStream(result, responseID, this.backgroundResponses, (response, context) => this.captureBackgroundResponse(response, context));
2527
+ }
2528
+ return result;
2529
+ });
2530
+ }
2531
+ return parentPromise._thenUnwrap(async result => {
2532
+ if (!('output' in result) || !isTerminalResponse(result)) {
2533
+ return result;
2534
+ }
2535
+
2536
+ // Removing the context before capture makes concurrent or repeated
2537
+ // terminal polls idempotent.
2538
+ const context = this.backgroundResponses.take(responseID);
2539
+ if (context) {
2540
+ await this.captureBackgroundResponse(result, context).catch(() => undefined);
2541
+ }
2542
+ return result;
2543
+ });
2544
+ }
2545
+ cancel(responseID, options) {
2546
+ const parentPromise = super.cancel(responseID, options);
2547
+
2548
+ // Avoid wrapping calls that do not belong to a background response created
2549
+ // through this client, preserving the upstream APIPromise unchanged.
2550
+ if (!this.backgroundResponses.get(responseID)) {
2551
+ return parentPromise;
2552
+ }
2553
+ return parentPromise._thenUnwrap(async result => {
2554
+ if (!isTerminalResponse(result)) {
2555
+ return result;
2556
+ }
2557
+ const context = this.backgroundResponses.take(responseID);
2558
+ if (context) {
2559
+ await this.captureBackgroundResponse(result, context).catch(() => undefined);
2560
+ }
2561
+ return result;
2562
+ });
2563
+ }
2219
2564
  parse(body, options) {
2220
2565
  const {
2221
2566
  providerParams: openAIParams,
@@ -2225,6 +2570,10 @@ class WrappedResponses extends Responses {
2225
2570
  const parentPromise = callWithOriginalCreate(this, super.create.bind(this), () => super.parse(openAIParams, options));
2226
2571
  const wrappedPromise = parentPromise.then(async result => {
2227
2572
  if (isPendingBackgroundResponse(openAIParams, result)) {
2573
+ this.backgroundResponses.set(result.id, {
2574
+ openAIParams,
2575
+ posthogParams
2576
+ });
2228
2577
  return result;
2229
2578
  }
2230
2579
  const latency = (Date.now() - startTime) / 1000;
@@ -2233,7 +2582,7 @@ class WrappedResponses extends Responses {
2233
2582
  model: openAIParams.model ?? result.model,
2234
2583
  provider: 'openai',
2235
2584
  input: formatOpenAIResponsesInput(sanitizeOpenAIResponse(openAIParams.input), openAIParams.instructions),
2236
- output: result.output,
2585
+ output: sanitizeOpenAIResponse(result.output),
2237
2586
  latency,
2238
2587
  baseURL: this.baseURL,
2239
2588
  modelParameters: getModelParams(body, result.service_tier),
@@ -2248,8 +2597,10 @@ class WrappedResponses extends Responses {
2248
2597
  stopReason: result.status ?? undefined,
2249
2598
  completionId: result.id,
2250
2599
  providerMetadata: buildProviderMetadata({
2251
- requestId: extractRequestId(result)
2252
- })
2600
+ requestId: extractRequestId(result),
2601
+ incompleteDetails: result.incomplete_details
2602
+ }),
2603
+ error: getResponseFailure(result)
2253
2604
  });
2254
2605
  return result;
2255
2606
  }, async error => {
@@ -2402,7 +2753,7 @@ class WrappedTranscriptions extends Transcriptions {
2402
2753
  model: openAIParams.model,
2403
2754
  provider: 'openai',
2404
2755
  input: openAIParams.prompt,
2405
- output: finalContent,
2756
+ output: sanitizeOpenAIResponse(finalContent),
2406
2757
  latency,
2407
2758
  timeToFirstToken,
2408
2759
  baseURL: this.baseURL,
@@ -2447,7 +2798,7 @@ class WrappedTranscriptions extends Transcriptions {
2447
2798
  model: openAIParams.model,
2448
2799
  provider: 'openai',
2449
2800
  input: openAIParams.prompt,
2450
- output: result.text,
2801
+ output: sanitizeOpenAIResponse(result.text),
2451
2802
  latency,
2452
2803
  baseURL: this.baseURL,
2453
2804
  modelParameters: getModelParams(body),