@tanstack/openai-base 0.9.10 → 0.9.12
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.
- package/dist/esm/adapters/chat-completions-text.js +8 -4
- package/dist/esm/adapters/chat-completions-text.js.map +1 -1
- package/dist/esm/adapters/responses-text.d.ts +22 -8
- package/dist/esm/adapters/responses-text.js +49 -24
- package/dist/esm/adapters/responses-text.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/package.json +4 -4
- package/src/adapters/chat-completions-text.ts +4 -0
- package/src/adapters/responses-text.ts +82 -54
- package/src/index.ts +4 -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,26 +86,10 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
64
86
|
async *chatStream(
|
|
65
87
|
options: TextOptions<TProviderOptions>,
|
|
66
88
|
): AsyncIterable<StreamChunk> {
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
|
|
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
|
-
>()
|
|
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>()
|
|
87
93
|
|
|
88
94
|
// AG-UI lifecycle tracking. Honor a caller-supplied `runId` (as `threadId`
|
|
89
95
|
// already does) so the emitted RUN_STARTED matches the id the caller keys
|
|
@@ -254,9 +260,13 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
254
260
|
// from strict mode is undone by the engine, not here.
|
|
255
261
|
const transformed = this.transformStructuredOutput(parsed)
|
|
256
262
|
|
|
263
|
+
// Surface usage so non-stream structured paths (and
|
|
264
|
+
// fallbackStructuredOutputStream) can forward tokens to middleware.
|
|
265
|
+
const usage = buildResponsesUsage(response.usage)
|
|
257
266
|
return {
|
|
258
267
|
data: transformed,
|
|
259
268
|
rawText,
|
|
269
|
+
...(usage && { usage }),
|
|
260
270
|
}
|
|
261
271
|
} catch (error: unknown) {
|
|
262
272
|
// Narrow before logging: raw SDK errors can carry request metadata
|
|
@@ -762,16 +772,7 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
762
772
|
*/
|
|
763
773
|
protected async *processStreamChunks(
|
|
764
774
|
stream: AsyncIterable<ResponseStreamEvent>,
|
|
765
|
-
toolCallMetadata: Map<
|
|
766
|
-
string,
|
|
767
|
-
{
|
|
768
|
-
index: number
|
|
769
|
-
name: string
|
|
770
|
-
started: boolean
|
|
771
|
-
ended?: boolean
|
|
772
|
-
pendingArguments?: string | undefined
|
|
773
|
-
}
|
|
774
|
-
>,
|
|
775
|
+
toolCallMetadata: Map<string, StreamedFunctionCallMetadata>,
|
|
775
776
|
options: TextOptions<TProviderOptions>,
|
|
776
777
|
aguiState: {
|
|
777
778
|
runId: string
|
|
@@ -1187,26 +1188,33 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
1187
1188
|
let metadata = toolCallMetadata.get(item.id)
|
|
1188
1189
|
if (!metadata) {
|
|
1189
1190
|
metadata = {
|
|
1191
|
+
callId: item.call_id || item.id,
|
|
1190
1192
|
index: chunk.output_index,
|
|
1191
1193
|
name: item.name || '',
|
|
1192
1194
|
started: false,
|
|
1193
1195
|
}
|
|
1194
1196
|
toolCallMetadata.set(item.id, metadata)
|
|
1195
|
-
} else
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1197
|
+
} else {
|
|
1198
|
+
if (item.call_id) metadata.callId = item.call_id
|
|
1199
|
+
if (!metadata.name && item.name) {
|
|
1200
|
+
// A later output_item.added for the same id finally carries
|
|
1201
|
+
// the name. Update so the gated emission below can fire.
|
|
1202
|
+
metadata.name = item.name
|
|
1203
|
+
}
|
|
1199
1204
|
}
|
|
1200
1205
|
if (!metadata.started && metadata.name) {
|
|
1201
1206
|
yield {
|
|
1202
1207
|
type: EventType.TOOL_CALL_START,
|
|
1203
|
-
toolCallId:
|
|
1208
|
+
toolCallId: metadata.callId,
|
|
1204
1209
|
toolCallName: metadata.name,
|
|
1205
1210
|
toolName: metadata.name,
|
|
1206
1211
|
parentMessageId: aguiState.messageId,
|
|
1207
1212
|
model: model || options.model,
|
|
1208
1213
|
timestamp: Date.now(),
|
|
1209
1214
|
index: chunk.output_index,
|
|
1215
|
+
metadata: {
|
|
1216
|
+
itemId: item.id,
|
|
1217
|
+
} satisfies OpenAIResponsesToolCallMetadata,
|
|
1210
1218
|
}
|
|
1211
1219
|
metadata.started = true
|
|
1212
1220
|
}
|
|
@@ -1235,7 +1243,9 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
1235
1243
|
`${this.name}.processStreamChunks orphan function_call_arguments.delta`,
|
|
1236
1244
|
{
|
|
1237
1245
|
source: `${this.name}.processStreamChunks`,
|
|
1238
|
-
|
|
1246
|
+
// No metadata yet, so the `call_id` is unknown here — only the
|
|
1247
|
+
// output item id the delta referenced.
|
|
1248
|
+
itemId: chunk.item_id,
|
|
1239
1249
|
rawDelta: chunk.delta,
|
|
1240
1250
|
},
|
|
1241
1251
|
)
|
|
@@ -1243,7 +1253,7 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
1243
1253
|
}
|
|
1244
1254
|
yield {
|
|
1245
1255
|
type: EventType.TOOL_CALL_ARGS,
|
|
1246
|
-
toolCallId:
|
|
1256
|
+
toolCallId: metadata.callId,
|
|
1247
1257
|
model: model || options.model,
|
|
1248
1258
|
timestamp: Date.now(),
|
|
1249
1259
|
delta: chunk.delta,
|
|
@@ -1270,7 +1280,8 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
1270
1280
|
`${this.name}.processStreamChunks deferring function_call_arguments.done — TOOL_CALL_START not yet emitted (waiting for name)`,
|
|
1271
1281
|
{
|
|
1272
1282
|
source: `${this.name}.processStreamChunks`,
|
|
1273
|
-
toolCallId:
|
|
1283
|
+
...(metadata && { toolCallId: metadata.callId }),
|
|
1284
|
+
itemId: item_id,
|
|
1274
1285
|
rawArguments: chunk.arguments,
|
|
1275
1286
|
},
|
|
1276
1287
|
)
|
|
@@ -1297,10 +1308,11 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
1297
1308
|
{
|
|
1298
1309
|
error: toRunErrorPayload(
|
|
1299
1310
|
parseError,
|
|
1300
|
-
`tool ${name} (${
|
|
1311
|
+
`tool ${name} (${metadata.callId}) returned malformed JSON arguments`,
|
|
1301
1312
|
),
|
|
1302
1313
|
source: `${this.name}.processStreamChunks`,
|
|
1303
|
-
toolCallId:
|
|
1314
|
+
toolCallId: metadata.callId,
|
|
1315
|
+
itemId: item_id,
|
|
1304
1316
|
toolName: name,
|
|
1305
1317
|
rawArguments: chunk.arguments,
|
|
1306
1318
|
},
|
|
@@ -1311,7 +1323,7 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
1311
1323
|
|
|
1312
1324
|
yield {
|
|
1313
1325
|
type: EventType.TOOL_CALL_END,
|
|
1314
|
-
toolCallId:
|
|
1326
|
+
toolCallId: metadata.callId,
|
|
1315
1327
|
toolCallName: name,
|
|
1316
1328
|
toolName: name,
|
|
1317
1329
|
model: model || options.model,
|
|
@@ -1329,26 +1341,31 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
1329
1341
|
const item = chunk.item
|
|
1330
1342
|
if (item.type === 'function_call' && item.id) {
|
|
1331
1343
|
const metadata = toolCallMetadata.get(item.id) ?? {
|
|
1344
|
+
callId: item.call_id || item.id,
|
|
1332
1345
|
index: chunk.output_index,
|
|
1333
1346
|
name: item.name || '',
|
|
1334
1347
|
started: false,
|
|
1335
1348
|
}
|
|
1336
1349
|
if (!toolCallMetadata.has(item.id)) {
|
|
1337
1350
|
toolCallMetadata.set(item.id, metadata)
|
|
1338
|
-
} else
|
|
1339
|
-
metadata.
|
|
1351
|
+
} else {
|
|
1352
|
+
if (item.call_id) metadata.callId = item.call_id
|
|
1353
|
+
if (!metadata.name && item.name) metadata.name = item.name
|
|
1340
1354
|
}
|
|
1341
1355
|
// Emit gated START if we now have a name and never started.
|
|
1342
1356
|
if (!metadata.started && metadata.name) {
|
|
1343
1357
|
yield {
|
|
1344
1358
|
type: EventType.TOOL_CALL_START,
|
|
1345
|
-
toolCallId:
|
|
1359
|
+
toolCallId: metadata.callId,
|
|
1346
1360
|
toolCallName: metadata.name,
|
|
1347
1361
|
toolName: metadata.name,
|
|
1348
1362
|
parentMessageId: aguiState.messageId,
|
|
1349
1363
|
model: model || options.model,
|
|
1350
1364
|
timestamp: Date.now(),
|
|
1351
1365
|
index: metadata.index,
|
|
1366
|
+
metadata: {
|
|
1367
|
+
itemId: item.id,
|
|
1368
|
+
} satisfies OpenAIResponsesToolCallMetadata,
|
|
1352
1369
|
}
|
|
1353
1370
|
metadata.started = true
|
|
1354
1371
|
}
|
|
@@ -1372,10 +1389,11 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
1372
1389
|
{
|
|
1373
1390
|
error: toRunErrorPayload(
|
|
1374
1391
|
parseError,
|
|
1375
|
-
`tool ${name} (${
|
|
1392
|
+
`tool ${name} (${metadata.callId}) returned malformed JSON arguments`,
|
|
1376
1393
|
),
|
|
1377
1394
|
source: `${this.name}.processStreamChunks`,
|
|
1378
|
-
toolCallId:
|
|
1395
|
+
toolCallId: metadata.callId,
|
|
1396
|
+
itemId: item.id,
|
|
1379
1397
|
toolName: name,
|
|
1380
1398
|
rawArguments: rawArgs,
|
|
1381
1399
|
},
|
|
@@ -1385,7 +1403,7 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
1385
1403
|
}
|
|
1386
1404
|
yield {
|
|
1387
1405
|
type: EventType.TOOL_CALL_END,
|
|
1388
|
-
toolCallId:
|
|
1406
|
+
toolCallId: metadata.callId,
|
|
1389
1407
|
toolCallName: name,
|
|
1390
1408
|
toolName: name,
|
|
1391
1409
|
model: model || options.model,
|
|
@@ -1409,25 +1427,30 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
1409
1427
|
for (const item of chunk.response.output) {
|
|
1410
1428
|
if (item.type !== 'function_call' || !item.id) continue
|
|
1411
1429
|
const metadata = toolCallMetadata.get(item.id) ?? {
|
|
1430
|
+
callId: item.call_id || item.id,
|
|
1412
1431
|
index: 0,
|
|
1413
1432
|
name: item.name || '',
|
|
1414
1433
|
started: false,
|
|
1415
1434
|
}
|
|
1416
1435
|
if (!toolCallMetadata.has(item.id)) {
|
|
1417
1436
|
toolCallMetadata.set(item.id, metadata)
|
|
1418
|
-
} else
|
|
1419
|
-
metadata.
|
|
1437
|
+
} else {
|
|
1438
|
+
if (item.call_id) metadata.callId = item.call_id
|
|
1439
|
+
if (!metadata.name && item.name) metadata.name = item.name
|
|
1420
1440
|
}
|
|
1421
1441
|
if (!metadata.started && metadata.name) {
|
|
1422
1442
|
yield {
|
|
1423
1443
|
type: EventType.TOOL_CALL_START,
|
|
1424
|
-
toolCallId:
|
|
1444
|
+
toolCallId: metadata.callId,
|
|
1425
1445
|
toolCallName: metadata.name,
|
|
1426
1446
|
toolName: metadata.name,
|
|
1427
1447
|
parentMessageId: aguiState.messageId,
|
|
1428
1448
|
model: model || options.model,
|
|
1429
1449
|
timestamp: Date.now(),
|
|
1430
1450
|
index: metadata.index,
|
|
1451
|
+
metadata: {
|
|
1452
|
+
itemId: item.id,
|
|
1453
|
+
} satisfies OpenAIResponsesToolCallMetadata,
|
|
1431
1454
|
}
|
|
1432
1455
|
metadata.started = true
|
|
1433
1456
|
}
|
|
@@ -1449,10 +1472,11 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
1449
1472
|
{
|
|
1450
1473
|
error: toRunErrorPayload(
|
|
1451
1474
|
parseError,
|
|
1452
|
-
`tool ${name} (${
|
|
1475
|
+
`tool ${name} (${metadata.callId}) returned malformed JSON arguments`,
|
|
1453
1476
|
),
|
|
1454
1477
|
source: `${this.name}.processStreamChunks`,
|
|
1455
|
-
toolCallId:
|
|
1478
|
+
toolCallId: metadata.callId,
|
|
1479
|
+
itemId: item.id,
|
|
1456
1480
|
toolName: name,
|
|
1457
1481
|
rawArguments: rawArgs,
|
|
1458
1482
|
},
|
|
@@ -1462,7 +1486,7 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
1462
1486
|
}
|
|
1463
1487
|
yield {
|
|
1464
1488
|
type: EventType.TOOL_CALL_END,
|
|
1465
|
-
toolCallId:
|
|
1489
|
+
toolCallId: metadata.callId,
|
|
1466
1490
|
toolCallName: name,
|
|
1467
1491
|
toolName: name,
|
|
1468
1492
|
model: model || options.model,
|
|
@@ -1728,10 +1752,14 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
1728
1752
|
typeof toolCall.function.arguments === 'string'
|
|
1729
1753
|
? toolCall.function.arguments
|
|
1730
1754
|
: JSON.stringify(toolCall.function.arguments)
|
|
1755
|
+
const itemId = (
|
|
1756
|
+
toolCall.metadata as OpenAIResponsesToolCallMetadata | undefined
|
|
1757
|
+
)?.itemId
|
|
1731
1758
|
|
|
1732
1759
|
result.push({
|
|
1733
1760
|
type: 'function_call',
|
|
1734
1761
|
call_id: toolCall.id,
|
|
1762
|
+
...(itemId && { id: itemId }),
|
|
1735
1763
|
name: toolCall.function.name,
|
|
1736
1764
|
arguments: argumentsString,
|
|
1737
1765
|
})
|
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 {
|
|
14
|
+
export {
|
|
15
|
+
OpenAIBaseResponsesTextAdapter,
|
|
16
|
+
type OpenAIResponsesToolCallMetadata,
|
|
17
|
+
} from './adapters/responses-text'
|
|
15
18
|
export {
|
|
16
19
|
convertFunctionToolToResponsesFormat,
|
|
17
20
|
convertToolsToResponsesFormat,
|