@tanstack/openai-base 0.9.9 → 0.9.11

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.
Files changed (47) hide show
  1. package/dist/esm/adapters/chat-completions-text.js +844 -962
  2. package/dist/esm/adapters/chat-completions-text.js.map +1 -1
  3. package/dist/esm/adapters/chat-completions-tool-converter.js +54 -38
  4. package/dist/esm/adapters/chat-completions-tool-converter.js.map +1 -1
  5. package/dist/esm/adapters/responses-text.d.ts +22 -8
  6. package/dist/esm/adapters/responses-text.js +1193 -1319
  7. package/dist/esm/adapters/responses-text.js.map +1 -1
  8. package/dist/esm/adapters/responses-tool-converter.js +50 -34
  9. package/dist/esm/adapters/responses-tool-converter.js.map +1 -1
  10. package/dist/esm/index.d.ts +1 -1
  11. package/dist/esm/index.js +4 -43
  12. package/dist/esm/tools/apply-patch-tool.js +20 -13
  13. package/dist/esm/tools/apply-patch-tool.js.map +1 -1
  14. package/dist/esm/tools/code-interpreter-tool.js +26 -18
  15. package/dist/esm/tools/code-interpreter-tool.js.map +1 -1
  16. package/dist/esm/tools/computer-use-tool.js +26 -19
  17. package/dist/esm/tools/computer-use-tool.js.map +1 -1
  18. package/dist/esm/tools/custom-tool.js +23 -21
  19. package/dist/esm/tools/custom-tool.js.map +1 -1
  20. package/dist/esm/tools/file-search-tool.js +30 -30
  21. package/dist/esm/tools/file-search-tool.js.map +1 -1
  22. package/dist/esm/tools/function-tool.js +44 -31
  23. package/dist/esm/tools/function-tool.js.map +1 -1
  24. package/dist/esm/tools/image-generation-tool.js +30 -23
  25. package/dist/esm/tools/image-generation-tool.js.map +1 -1
  26. package/dist/esm/tools/local-shell-tool.js +20 -13
  27. package/dist/esm/tools/local-shell-tool.js.map +1 -1
  28. package/dist/esm/tools/mcp-tool.js +31 -25
  29. package/dist/esm/tools/mcp-tool.js.map +1 -1
  30. package/dist/esm/tools/shell-tool.js +25 -21
  31. package/dist/esm/tools/shell-tool.js.map +1 -1
  32. package/dist/esm/tools/tool-converter.js +37 -46
  33. package/dist/esm/tools/tool-converter.js.map +1 -1
  34. package/dist/esm/tools/web-search-preview-tool.js +26 -15
  35. package/dist/esm/tools/web-search-preview-tool.js.map +1 -1
  36. package/dist/esm/tools/web-search-tool.js +27 -15
  37. package/dist/esm/tools/web-search-tool.js.map +1 -1
  38. package/dist/esm/usage.js +88 -76
  39. package/dist/esm/usage.js.map +1 -1
  40. package/dist/esm/utils/request-options.js +20 -9
  41. package/dist/esm/utils/request-options.js.map +1 -1
  42. package/dist/esm/utils/schema-converter.js +193 -146
  43. package/dist/esm/utils/schema-converter.js.map +1 -1
  44. package/package.json +7 -7
  45. package/src/adapters/responses-text.ts +84 -57
  46. package/src/index.ts +4 -1
  47. package/dist/esm/index.js.map +0 -1
@@ -31,6 +31,27 @@ import type {
31
31
  TextOptions,
32
32
  } from '@tanstack/ai'
33
33
 
34
+ /**
35
+ * Provider-specific metadata that preserves the Responses API output item ID.
36
+ *
37
+ * Responses function calls have two identifiers: `call_id` correlates the
38
+ * function output with the call, while `id` identifies the output item itself.
39
+ * TanStack AI uses `call_id` as the canonical tool-call ID and carries the
40
+ * item ID here so stateless follow-up requests can replay both values.
41
+ */
42
+ export interface OpenAIResponsesToolCallMetadata {
43
+ itemId: string
44
+ }
45
+
46
+ interface StreamedFunctionCallMetadata {
47
+ callId: string
48
+ index: number
49
+ name: string
50
+ started: boolean
51
+ ended?: boolean
52
+ pendingArguments?: string | undefined
53
+ }
54
+
34
55
  /**
35
56
  * Shared implementation of the OpenAI Responses API. Holds the stream-event
36
57
  * accumulator + AG-UI lifecycle and calls the OpenAI SDK directly. Subclasses
@@ -49,7 +70,8 @@ export abstract class OpenAIBaseResponsesTextAdapter<
49
70
  TProviderOptions,
50
71
  TInputModalities,
51
72
  TMessageMetadata,
52
- TToolCapabilities
73
+ TToolCapabilities,
74
+ OpenAIResponsesToolCallMetadata
53
75
  > {
54
76
  override readonly kind = 'text' as const
55
77
  readonly name: string
@@ -64,30 +86,17 @@ export abstract class OpenAIBaseResponsesTextAdapter<
64
86
  async *chatStream(
65
87
  options: TextOptions<TProviderOptions>,
66
88
  ): AsyncIterable<StreamChunk> {
67
- // Track tool call metadata by unique ID
68
- // Responses API streams tool calls with deltas — first chunk has ID/name,
69
- // subsequent chunks only have args.
70
- // We assign our own indices as we encounter unique tool call IDs.
71
- const toolCallMetadata = new Map<
72
- string,
73
- {
74
- index: number
75
- name: string
76
- started: boolean
77
- // Set once TOOL_CALL_END has been emitted (via args.done or the
78
- // output_item.done backfill) so the two paths don't double-emit.
79
- ended?: boolean
80
- // Set when args.done arrives before TOOL_CALL_START could fire
81
- // (output_item.added lacked a name). output_item.done picks these
82
- // up to emit the missing END. Allow explicit `undefined` so the
83
- // emission paths can re-clear the slot after handing it off.
84
- pendingArguments?: string
85
- }
86
- >()
87
-
88
- // AG-UI lifecycle tracking
89
+ // Key streamed state by output item ID because argument deltas reference
90
+ // `item_id`. The state separately retains `call_id`, which is the public
91
+ // tool-call ID and the correlation key for function_call_output.
92
+ const toolCallMetadata = new Map<string, StreamedFunctionCallMetadata>()
93
+
94
+ // AG-UI lifecycle tracking. Honor a caller-supplied `runId` (as `threadId`
95
+ // already does) so the emitted RUN_STARTED matches the id the caller keys
96
+ // durability by — e.g. a summarize run threading the client's runId through
97
+ // for mid-run reload resumability. Falls back to a generated id.
89
98
  const aguiState = {
90
- runId: generateId(this.name),
99
+ runId: options.runId ?? generateId(this.name),
91
100
  threadId: options.threadId ?? generateId(this.name),
92
101
  messageId: generateId(this.name),
93
102
  hasEmittedRunStarted: false,
@@ -759,16 +768,7 @@ export abstract class OpenAIBaseResponsesTextAdapter<
759
768
  */
760
769
  protected async *processStreamChunks(
761
770
  stream: AsyncIterable<ResponseStreamEvent>,
762
- toolCallMetadata: Map<
763
- string,
764
- {
765
- index: number
766
- name: string
767
- started: boolean
768
- ended?: boolean
769
- pendingArguments?: string | undefined
770
- }
771
- >,
771
+ toolCallMetadata: Map<string, StreamedFunctionCallMetadata>,
772
772
  options: TextOptions<TProviderOptions>,
773
773
  aguiState: {
774
774
  runId: string
@@ -1184,26 +1184,33 @@ export abstract class OpenAIBaseResponsesTextAdapter<
1184
1184
  let metadata = toolCallMetadata.get(item.id)
1185
1185
  if (!metadata) {
1186
1186
  metadata = {
1187
+ callId: item.call_id || item.id,
1187
1188
  index: chunk.output_index,
1188
1189
  name: item.name || '',
1189
1190
  started: false,
1190
1191
  }
1191
1192
  toolCallMetadata.set(item.id, metadata)
1192
- } else if (!metadata.name && item.name) {
1193
- // A later output_item.added for the same id finally carries
1194
- // the name. Update so the gated emission below can fire.
1195
- metadata.name = item.name
1193
+ } else {
1194
+ if (item.call_id) metadata.callId = item.call_id
1195
+ if (!metadata.name && item.name) {
1196
+ // A later output_item.added for the same id finally carries
1197
+ // the name. Update so the gated emission below can fire.
1198
+ metadata.name = item.name
1199
+ }
1196
1200
  }
1197
1201
  if (!metadata.started && metadata.name) {
1198
1202
  yield {
1199
1203
  type: EventType.TOOL_CALL_START,
1200
- toolCallId: item.id,
1204
+ toolCallId: metadata.callId,
1201
1205
  toolCallName: metadata.name,
1202
1206
  toolName: metadata.name,
1203
1207
  parentMessageId: aguiState.messageId,
1204
1208
  model: model || options.model,
1205
1209
  timestamp: Date.now(),
1206
1210
  index: chunk.output_index,
1211
+ metadata: {
1212
+ itemId: item.id,
1213
+ } satisfies OpenAIResponsesToolCallMetadata,
1207
1214
  }
1208
1215
  metadata.started = true
1209
1216
  }
@@ -1232,7 +1239,9 @@ export abstract class OpenAIBaseResponsesTextAdapter<
1232
1239
  `${this.name}.processStreamChunks orphan function_call_arguments.delta`,
1233
1240
  {
1234
1241
  source: `${this.name}.processStreamChunks`,
1235
- toolCallId: chunk.item_id,
1242
+ // No metadata yet, so the `call_id` is unknown here — only the
1243
+ // output item id the delta referenced.
1244
+ itemId: chunk.item_id,
1236
1245
  rawDelta: chunk.delta,
1237
1246
  },
1238
1247
  )
@@ -1240,7 +1249,7 @@ export abstract class OpenAIBaseResponsesTextAdapter<
1240
1249
  }
1241
1250
  yield {
1242
1251
  type: EventType.TOOL_CALL_ARGS,
1243
- toolCallId: chunk.item_id,
1252
+ toolCallId: metadata.callId,
1244
1253
  model: model || options.model,
1245
1254
  timestamp: Date.now(),
1246
1255
  delta: chunk.delta,
@@ -1267,7 +1276,8 @@ export abstract class OpenAIBaseResponsesTextAdapter<
1267
1276
  `${this.name}.processStreamChunks deferring function_call_arguments.done — TOOL_CALL_START not yet emitted (waiting for name)`,
1268
1277
  {
1269
1278
  source: `${this.name}.processStreamChunks`,
1270
- toolCallId: item_id,
1279
+ ...(metadata && { toolCallId: metadata.callId }),
1280
+ itemId: item_id,
1271
1281
  rawArguments: chunk.arguments,
1272
1282
  },
1273
1283
  )
@@ -1294,10 +1304,11 @@ export abstract class OpenAIBaseResponsesTextAdapter<
1294
1304
  {
1295
1305
  error: toRunErrorPayload(
1296
1306
  parseError,
1297
- `tool ${name} (${item_id}) returned malformed JSON arguments`,
1307
+ `tool ${name} (${metadata.callId}) returned malformed JSON arguments`,
1298
1308
  ),
1299
1309
  source: `${this.name}.processStreamChunks`,
1300
- toolCallId: item_id,
1310
+ toolCallId: metadata.callId,
1311
+ itemId: item_id,
1301
1312
  toolName: name,
1302
1313
  rawArguments: chunk.arguments,
1303
1314
  },
@@ -1308,7 +1319,7 @@ export abstract class OpenAIBaseResponsesTextAdapter<
1308
1319
 
1309
1320
  yield {
1310
1321
  type: EventType.TOOL_CALL_END,
1311
- toolCallId: item_id,
1322
+ toolCallId: metadata.callId,
1312
1323
  toolCallName: name,
1313
1324
  toolName: name,
1314
1325
  model: model || options.model,
@@ -1326,26 +1337,31 @@ export abstract class OpenAIBaseResponsesTextAdapter<
1326
1337
  const item = chunk.item
1327
1338
  if (item.type === 'function_call' && item.id) {
1328
1339
  const metadata = toolCallMetadata.get(item.id) ?? {
1340
+ callId: item.call_id || item.id,
1329
1341
  index: chunk.output_index,
1330
1342
  name: item.name || '',
1331
1343
  started: false,
1332
1344
  }
1333
1345
  if (!toolCallMetadata.has(item.id)) {
1334
1346
  toolCallMetadata.set(item.id, metadata)
1335
- } else if (!metadata.name && item.name) {
1336
- metadata.name = item.name
1347
+ } else {
1348
+ if (item.call_id) metadata.callId = item.call_id
1349
+ if (!metadata.name && item.name) metadata.name = item.name
1337
1350
  }
1338
1351
  // Emit gated START if we now have a name and never started.
1339
1352
  if (!metadata.started && metadata.name) {
1340
1353
  yield {
1341
1354
  type: EventType.TOOL_CALL_START,
1342
- toolCallId: item.id,
1355
+ toolCallId: metadata.callId,
1343
1356
  toolCallName: metadata.name,
1344
1357
  toolName: metadata.name,
1345
1358
  parentMessageId: aguiState.messageId,
1346
1359
  model: model || options.model,
1347
1360
  timestamp: Date.now(),
1348
1361
  index: metadata.index,
1362
+ metadata: {
1363
+ itemId: item.id,
1364
+ } satisfies OpenAIResponsesToolCallMetadata,
1349
1365
  }
1350
1366
  metadata.started = true
1351
1367
  }
@@ -1369,10 +1385,11 @@ export abstract class OpenAIBaseResponsesTextAdapter<
1369
1385
  {
1370
1386
  error: toRunErrorPayload(
1371
1387
  parseError,
1372
- `tool ${name} (${item.id}) returned malformed JSON arguments`,
1388
+ `tool ${name} (${metadata.callId}) returned malformed JSON arguments`,
1373
1389
  ),
1374
1390
  source: `${this.name}.processStreamChunks`,
1375
- toolCallId: item.id,
1391
+ toolCallId: metadata.callId,
1392
+ itemId: item.id,
1376
1393
  toolName: name,
1377
1394
  rawArguments: rawArgs,
1378
1395
  },
@@ -1382,7 +1399,7 @@ export abstract class OpenAIBaseResponsesTextAdapter<
1382
1399
  }
1383
1400
  yield {
1384
1401
  type: EventType.TOOL_CALL_END,
1385
- toolCallId: item.id,
1402
+ toolCallId: metadata.callId,
1386
1403
  toolCallName: name,
1387
1404
  toolName: name,
1388
1405
  model: model || options.model,
@@ -1406,25 +1423,30 @@ export abstract class OpenAIBaseResponsesTextAdapter<
1406
1423
  for (const item of chunk.response.output) {
1407
1424
  if (item.type !== 'function_call' || !item.id) continue
1408
1425
  const metadata = toolCallMetadata.get(item.id) ?? {
1426
+ callId: item.call_id || item.id,
1409
1427
  index: 0,
1410
1428
  name: item.name || '',
1411
1429
  started: false,
1412
1430
  }
1413
1431
  if (!toolCallMetadata.has(item.id)) {
1414
1432
  toolCallMetadata.set(item.id, metadata)
1415
- } else if (!metadata.name && item.name) {
1416
- metadata.name = item.name
1433
+ } else {
1434
+ if (item.call_id) metadata.callId = item.call_id
1435
+ if (!metadata.name && item.name) metadata.name = item.name
1417
1436
  }
1418
1437
  if (!metadata.started && metadata.name) {
1419
1438
  yield {
1420
1439
  type: EventType.TOOL_CALL_START,
1421
- toolCallId: item.id,
1440
+ toolCallId: metadata.callId,
1422
1441
  toolCallName: metadata.name,
1423
1442
  toolName: metadata.name,
1424
1443
  parentMessageId: aguiState.messageId,
1425
1444
  model: model || options.model,
1426
1445
  timestamp: Date.now(),
1427
1446
  index: metadata.index,
1447
+ metadata: {
1448
+ itemId: item.id,
1449
+ } satisfies OpenAIResponsesToolCallMetadata,
1428
1450
  }
1429
1451
  metadata.started = true
1430
1452
  }
@@ -1446,10 +1468,11 @@ export abstract class OpenAIBaseResponsesTextAdapter<
1446
1468
  {
1447
1469
  error: toRunErrorPayload(
1448
1470
  parseError,
1449
- `tool ${name} (${item.id}) returned malformed JSON arguments`,
1471
+ `tool ${name} (${metadata.callId}) returned malformed JSON arguments`,
1450
1472
  ),
1451
1473
  source: `${this.name}.processStreamChunks`,
1452
- toolCallId: item.id,
1474
+ toolCallId: metadata.callId,
1475
+ itemId: item.id,
1453
1476
  toolName: name,
1454
1477
  rawArguments: rawArgs,
1455
1478
  },
@@ -1459,7 +1482,7 @@ export abstract class OpenAIBaseResponsesTextAdapter<
1459
1482
  }
1460
1483
  yield {
1461
1484
  type: EventType.TOOL_CALL_END,
1462
- toolCallId: item.id,
1485
+ toolCallId: metadata.callId,
1463
1486
  toolCallName: name,
1464
1487
  toolName: name,
1465
1488
  model: model || options.model,
@@ -1725,10 +1748,14 @@ export abstract class OpenAIBaseResponsesTextAdapter<
1725
1748
  typeof toolCall.function.arguments === 'string'
1726
1749
  ? toolCall.function.arguments
1727
1750
  : JSON.stringify(toolCall.function.arguments)
1751
+ const itemId = (
1752
+ toolCall.metadata as OpenAIResponsesToolCallMetadata | undefined
1753
+ )?.itemId
1728
1754
 
1729
1755
  result.push({
1730
1756
  type: 'function_call',
1731
1757
  call_id: toolCall.id,
1758
+ ...(itemId && { id: itemId }),
1732
1759
  name: toolCall.function.name,
1733
1760
  arguments: argumentsString,
1734
1761
  })
package/src/index.ts CHANGED
@@ -11,7 +11,10 @@ export {
11
11
  convertToolsToChatCompletionsFormat,
12
12
  type ChatCompletionFunctionTool,
13
13
  } from './adapters/chat-completions-tool-converter'
14
- export { OpenAIBaseResponsesTextAdapter } from './adapters/responses-text'
14
+ export {
15
+ OpenAIBaseResponsesTextAdapter,
16
+ type OpenAIResponsesToolCallMetadata,
17
+ } from './adapters/responses-text'
15
18
  export {
16
19
  convertFunctionToolToResponsesFormat,
17
20
  convertToolsToResponsesFormat,
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;"}