@tanstack/openai-base 0.11.1 → 0.12.2

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,4 +1,4 @@
1
- import { makeStructuredOutputCompatibleWithMap } from "../utils/schema-converter.js";
1
+ import { makeStructuredOutputCompatibleWithMap, warnStrictFallback } from "../utils/schema-converter.js";
2
2
  import { buildResponsesUsage } from "../usage.js";
3
3
  import { extractRequestOptions } from "../utils/request-options.js";
4
4
  import { createToolInputNormalizer } from "../utils/tool-input-normalizer.js";
@@ -13,6 +13,40 @@ var PDF_BASE64_MAGIC = "JVBERi";
13
13
  function isRecord(value) {
14
14
  return typeof value === "object" && value !== null;
15
15
  }
16
+ function readURLCitation(value) {
17
+ if (!isRecord(value) || value.type !== "url_citation") return void 0;
18
+ if (typeof value.url !== "string" || typeof value.title !== "string" || typeof value.start_index !== "number" || typeof value.end_index !== "number") return;
19
+ return {
20
+ type: "url_citation",
21
+ url: value.url,
22
+ title: value.title,
23
+ start_index: value.start_index,
24
+ end_index: value.end_index
25
+ };
26
+ }
27
+ function readWebSearchCall(value) {
28
+ if (!isRecord(value) || value.type !== "web_search_call") return void 0;
29
+ if (typeof value.id !== "string" || typeof value.status !== "string" || !isRecord(value.action) || typeof value.action.type !== "string") return;
30
+ return value;
31
+ }
32
+ function collectWebSearchSources(item, citations) {
33
+ const sources = /* @__PURE__ */ new Map();
34
+ const add = (url, title) => {
35
+ if (typeof url !== "string" || url.length === 0) return;
36
+ const existing = sources.get(url);
37
+ if (existing) {
38
+ if (!existing.title && typeof title === "string" && title.length > 0) existing.title = title;
39
+ return;
40
+ }
41
+ sources.set(url, {
42
+ url,
43
+ ...typeof title === "string" && title.length > 0 ? { title } : {}
44
+ });
45
+ };
46
+ if (item.action.type === "search") for (const source of item.action.sources ?? []) add(source.url);
47
+ for (const citation of citations) add(citation.url, citation.title);
48
+ return [...sources.values()];
49
+ }
16
50
  function packResponsesReasoningSignature(id, encryptedContent) {
17
51
  if (!id && !encryptedContent) return void 0;
18
52
  return JSON.stringify({
@@ -55,10 +89,13 @@ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
55
89
  kind = "text";
56
90
  name;
57
91
  client;
58
- constructor(model, name, client) {
92
+ /** See {@link OpenAIBaseTextAdapterOptions.strictFallbackWarning}. */
93
+ strictFallbackWarning;
94
+ constructor(model, name, client, options = {}) {
59
95
  super({}, model);
60
96
  this.name = name;
61
97
  this.client = client;
98
+ this.strictFallbackWarning = options.strictFallbackWarning ?? true;
62
99
  }
63
100
  async *chatStream(options) {
64
101
  const toolCallMetadata = /* @__PURE__ */ new Map();
@@ -198,6 +235,7 @@ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
198
235
  let hasClosedReasoning = false;
199
236
  let model = chatOptions.model;
200
237
  let usage;
238
+ let responseCompleted = false;
201
239
  const closeReasoning = function* () {
202
240
  if (reasoningMessageId && !hasClosedReasoning) {
203
241
  hasClosedReasoning = true;
@@ -348,10 +386,11 @@ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
348
386
  continue;
349
387
  }
350
388
  if (chunk.type === "response.completed") {
389
+ responseCompleted = true;
351
390
  const response = chunk.response;
352
391
  if (response.usage) usage = response.usage;
353
392
  if (response.model) model = response.model;
354
- continue;
393
+ break;
355
394
  }
356
395
  if (chunk.type === "response.failed") {
357
396
  const response = chunk.response;
@@ -379,6 +418,22 @@ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
379
418
  model,
380
419
  timestamp: Date.now()
381
420
  };
421
+ if (!responseCompleted) {
422
+ const message = "Response stream ended before response.completed";
423
+ yield {
424
+ type: EventType.RUN_ERROR,
425
+ runId: aguiState.runId,
426
+ model,
427
+ timestamp: Date.now(),
428
+ message,
429
+ code: "incomplete-stream",
430
+ error: {
431
+ message,
432
+ code: "incomplete-stream"
433
+ }
434
+ };
435
+ return;
436
+ }
382
437
  if (accumulatedContent.length === 0) {
383
438
  yield {
384
439
  type: EventType.RUN_ERROR,
@@ -567,8 +622,61 @@ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
567
622
  let closedReasoningStepId;
568
623
  let hasClosedReasoning = false;
569
624
  let runFinishedEmitted = false;
625
+ const providerWebSearchCalls = /* @__PURE__ */ new Map();
626
+ const webSearchCitations = [];
570
627
  const adapterName = this.name;
571
628
  const emitModel = () => model || options.model;
629
+ const recordProviderWebSearchCall = (value, index) => {
630
+ const item = readWebSearchCall(value);
631
+ if (!item) return;
632
+ const existing = providerWebSearchCalls.get(item.id);
633
+ if (existing) {
634
+ existing.item = item;
635
+ existing.index = index;
636
+ } else providerWebSearchCalls.set(item.id, {
637
+ item,
638
+ index,
639
+ started: false
640
+ });
641
+ };
642
+ const emitProviderWebSearchCalls = function* (assistantMessage, completedOnly = false) {
643
+ for (const entry of providerWebSearchCalls.values()) {
644
+ if (entry.started || completedOnly && entry.item.status !== "completed") continue;
645
+ const callUrls = new Set(entry.item.action.type === "search" ? (entry.item.action.sources ?? []).map((source) => source.url) : []);
646
+ const citations = webSearchCitations.filter((citation) => callUrls.has(citation.url));
647
+ const metadata = {
648
+ itemId: entry.item.id,
649
+ providerExecuted: true,
650
+ sources: collectWebSearchSources(entry.item, citations),
651
+ openai: {
652
+ webSearchCall: entry.item,
653
+ urlCitations: citations,
654
+ ...assistantMessage ? { assistantMessage } : {}
655
+ }
656
+ };
657
+ entry.started = true;
658
+ yield {
659
+ type: EventType.TOOL_CALL_START,
660
+ toolCallId: entry.item.id,
661
+ toolCallName: "web_search",
662
+ toolName: "web_search",
663
+ parentMessageId: aguiState.messageId,
664
+ model: emitModel(),
665
+ timestamp: Date.now(),
666
+ index: entry.index,
667
+ metadata
668
+ };
669
+ yield {
670
+ type: EventType.TOOL_CALL_END,
671
+ toolCallId: entry.item.id,
672
+ toolCallName: "web_search",
673
+ toolName: "web_search",
674
+ model: emitModel(),
675
+ timestamp: Date.now(),
676
+ input: entry.item.action
677
+ };
678
+ }
679
+ };
572
680
  const openReasoning = function* () {
573
681
  if (reasoningMessageId) return;
574
682
  reasoningMessageId = generateId(adapterName);
@@ -750,6 +858,10 @@ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
750
858
  accumulatedReasoning = "";
751
859
  }
752
860
  if (chunk.type === "response.failed" || chunk.type === "response.incomplete") {
861
+ if (chunk.type === "response.incomplete") {
862
+ for (const [index, item] of (chunk.response.output ?? []).entries()) recordProviderWebSearchCall(item, index);
863
+ yield* emitProviderWebSearchCalls(void 0, true);
864
+ }
753
865
  yield* closeReasoning();
754
866
  if (hasEmittedTextMessageStart) {
755
867
  yield {
@@ -858,6 +970,7 @@ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
858
970
  }
859
971
  if (chunk.type === "response.output_item.added") {
860
972
  const item = chunk.item;
973
+ if (item.type === "web_search_call") recordProviderWebSearchCall(item, chunk.output_index);
861
974
  if (item.type === "reasoning") {
862
975
  captureReasoningItem(item);
863
976
  yield* openReasoning();
@@ -954,6 +1067,7 @@ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
954
1067
  }
955
1068
  if (chunk.type === "response.output_item.done") {
956
1069
  const item = chunk.item;
1070
+ if (item.type === "web_search_call") recordProviderWebSearchCall(item, chunk.output_index);
957
1071
  if (item.type === "reasoning") {
958
1072
  captureReasoningItem(item);
959
1073
  yield* openReasoning();
@@ -1017,8 +1131,15 @@ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
1017
1131
  }
1018
1132
  yield* userToolChunks(item, chunk.output_index, false);
1019
1133
  }
1134
+ if (chunk.type === "response.output_text.annotation.added") {
1135
+ const citation = readURLCitation(chunk.annotation);
1136
+ if (citation) webSearchCitations.push(citation);
1137
+ }
1020
1138
  if (chunk.type === "response.completed") {
1021
- const completedText = chunk.response.output.flatMap((item) => item.type === "message" ? item.content : []).filter((part) => part.type === "output_text").map((part) => part.text).join("");
1139
+ const responseOutput = Array.isArray(chunk.response.output) ? chunk.response.output : [];
1140
+ const assistantMessage = responseOutput.find((item) => item.type === "message");
1141
+ for (const [index, item] of responseOutput.entries()) if (item.type === "web_search_call") recordProviderWebSearchCall(item, index);
1142
+ const completedText = responseOutput.flatMap((item) => item.type === "message" ? item.content : []).filter((part) => part.type === "output_text").map((part) => part.text).join("");
1022
1143
  if (accumulatedContent.length === 0 && completedText.length > 0) {
1023
1144
  if (!hasEmittedTextMessageStart) {
1024
1145
  hasEmittedTextMessageStart = true;
@@ -1041,7 +1162,7 @@ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
1041
1162
  content: accumulatedContent
1042
1163
  };
1043
1164
  }
1044
- if (Array.isArray(chunk.response.output)) for (const item of chunk.response.output) captureReasoningItem(item);
1165
+ for (const item of responseOutput) captureReasoningItem(item);
1045
1166
  if (!reasoningMessageId && (reasoningItemId || reasoningEncryptedContent)) {
1046
1167
  const signature = packResponsesReasoningSignature(reasoningItemId, reasoningEncryptedContent);
1047
1168
  if (closedReasoningStepId && signature) yield {
@@ -1055,7 +1176,7 @@ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
1055
1176
  };
1056
1177
  else if (!closedReasoningStepId) yield* openReasoning();
1057
1178
  }
1058
- for (const [outputIndex, item] of chunk.response.output.entries()) {
1179
+ for (const [outputIndex, item] of responseOutput.entries()) {
1059
1180
  if (item.type !== "function_call" || !item.id) continue;
1060
1181
  const metadata = toolCallMetadata.get(item.id) ?? {
1061
1182
  callId: item.call_id || item.id,
@@ -1113,8 +1234,9 @@ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
1113
1234
  metadata.pendingArguments = void 0;
1114
1235
  }
1115
1236
  }
1116
- const shellOutputs = hostedShellCallIds(chunk.response.output);
1117
- for (const [outputIndex, item] of chunk.response.output.entries()) {
1237
+ yield* emitProviderWebSearchCalls(assistantMessage);
1238
+ const shellOutputs = hostedShellCallIds(responseOutput);
1239
+ for (const [outputIndex, item] of responseOutput.entries()) {
1118
1240
  if (isRecord(item) && item.type === "shell_call" && typeof item.call_id === "string" && shellOutputs.has(item.call_id)) continue;
1119
1241
  yield* userToolChunks(item, outputIndex, true);
1120
1242
  }
@@ -1128,7 +1250,7 @@ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
1128
1250
  };
1129
1251
  hasEmittedTextMessageStart = false;
1130
1252
  }
1131
- const hasFunctionCalls = chunk.response.output.some((item) => {
1253
+ const hasFunctionCalls = responseOutput.some((item) => {
1132
1254
  if (!isRecord(item)) return false;
1133
1255
  if (item.type === "function_call") return true;
1134
1256
  if (item.type === "shell_call" && typeof item.call_id === "string" && shellOutputs.has(item.call_id)) return false;
@@ -1146,6 +1268,7 @@ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
1146
1268
  finishReason
1147
1269
  };
1148
1270
  runFinishedEmitted = true;
1271
+ return;
1149
1272
  }
1150
1273
  if (chunk.type === "error") {
1151
1274
  const code = chunk.code ?? void 0;
@@ -1165,6 +1288,7 @@ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
1165
1288
  }
1166
1289
  }
1167
1290
  if (!runFinishedEmitted && aguiState.hasEmittedRunStarted) {
1291
+ yield* emitProviderWebSearchCalls(void 0, true);
1168
1292
  yield* closeReasoning();
1169
1293
  if (hasEmittedTextMessageStart) yield {
1170
1294
  type: EventType.TEXT_MESSAGE_END,
@@ -1172,13 +1296,17 @@ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
1172
1296
  model: model || options.model,
1173
1297
  timestamp: Date.now()
1174
1298
  };
1299
+ const message = "Response stream ended before response.completed";
1175
1300
  yield {
1176
- type: EventType.RUN_FINISHED,
1177
- runId: aguiState.runId,
1178
- threadId: aguiState.threadId,
1301
+ type: EventType.RUN_ERROR,
1179
1302
  model: model || options.model,
1180
1303
  timestamp: Date.now(),
1181
- finishReason: toolCallMetadata.size > 0 ? "tool_calls" : "stop"
1304
+ message,
1305
+ code: "incomplete-stream",
1306
+ error: {
1307
+ message,
1308
+ code: "incomplete-stream"
1309
+ }
1182
1310
  };
1183
1311
  }
1184
1312
  } catch (error) {
@@ -1208,6 +1336,7 @@ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
1208
1336
  */
1209
1337
  mapOptionsToRequest(options) {
1210
1338
  const input = this.convertMessagesToInput(options.messages);
1339
+ if (this.strictFallbackWarning) warnStrictFallback(options.tools, options.logger);
1211
1340
  const tools = options.tools ? convertToolsToResponsesFormat(options.tools, this.makeStructuredOutputCompatible.bind(this)) : void 0;
1212
1341
  const modelOptions = options.modelOptions;
1213
1342
  const combinedSchema = options.outputSchema;
@@ -1292,7 +1421,17 @@ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
1292
1421
  });
1293
1422
  }
1294
1423
  const canPairReasoning = reasoningCandidates === 0 || reasoningCandidates === 1 && emittedReasoning === 1;
1424
+ let rawAssistantMessage;
1295
1425
  if (message.toolCalls && message.toolCalls.length > 0) for (const toolCall of message.toolCalls) {
1426
+ const metadata = toolCall.metadata;
1427
+ if (metadata?.providerExecuted) {
1428
+ const webSearchCall = metadata.openai?.webSearchCall;
1429
+ if (webSearchCall && canPairReasoning) {
1430
+ result.push(webSearchCall);
1431
+ rawAssistantMessage ??= metadata.openai?.assistantMessage;
1432
+ }
1433
+ continue;
1434
+ }
1296
1435
  const argumentsString = typeof toolCall.function.arguments === "string" ? toolCall.function.arguments : JSON.stringify(toolCall.function.arguments);
1297
1436
  const replayCall = {
1298
1437
  id: toolCall.id,
@@ -1308,7 +1447,7 @@ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
1308
1447
  result.push(userItem);
1309
1448
  continue;
1310
1449
  }
1311
- const itemId = toolCall.metadata?.itemId;
1450
+ const itemId = metadata?.itemId;
1312
1451
  result.push({
1313
1452
  type: "function_call",
1314
1453
  call_id: toolCall.id,
@@ -1317,7 +1456,8 @@ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
1317
1456
  arguments: argumentsString
1318
1457
  });
1319
1458
  }
1320
- if (message.content) {
1459
+ if (rawAssistantMessage) result.push(rawAssistantMessage);
1460
+ else if (message.content) {
1321
1461
  const contentStr = this.extractTextContent(message.content);
1322
1462
  if (contentStr) result.push({
1323
1463
  type: "message",