@tanstack/openai-base 0.10.2 → 0.10.5
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.d.ts +6 -6
- package/dist/esm/adapters/chat-completions-text.js +7 -2
- package/dist/esm/adapters/chat-completions-text.js.map +1 -1
- package/dist/esm/adapters/responses-text.d.ts +19 -8
- package/dist/esm/adapters/responses-text.js +104 -101
- package/dist/esm/adapters/responses-text.js.map +1 -1
- package/package.json +3 -3
- package/src/adapters/chat-completions-text.ts +19 -12
- package/src/adapters/responses-text.ts +141 -127
|
@@ -27,8 +27,7 @@ import type {
|
|
|
27
27
|
DefaultMessageMetadataByModality,
|
|
28
28
|
Modality,
|
|
29
29
|
ModelMessage,
|
|
30
|
-
|
|
31
|
-
StreamChunk,
|
|
30
|
+
AdapterYieldChunk,
|
|
32
31
|
TextOptions,
|
|
33
32
|
} from '@tanstack/ai'
|
|
34
33
|
|
|
@@ -71,7 +70,7 @@ export abstract class OpenAIBaseChatCompletionsTextAdapter<
|
|
|
71
70
|
|
|
72
71
|
async *chatStream(
|
|
73
72
|
options: TextOptions<TProviderOptions>,
|
|
74
|
-
): AsyncIterable<
|
|
73
|
+
): AsyncIterable<AdapterYieldChunk> {
|
|
75
74
|
// AG-UI lifecycle tracking (mutable state object for ESLint compatibility)
|
|
76
75
|
const aguiState = {
|
|
77
76
|
runId: generateId(this.name),
|
|
@@ -112,7 +111,7 @@ export abstract class OpenAIBaseChatCompletionsTextAdapter<
|
|
|
112
111
|
options: TextOptions,
|
|
113
112
|
aguiState: ChatStreamState,
|
|
114
113
|
source: 'chatStream' | 'processStreamChunks',
|
|
115
|
-
): AsyncIterable<
|
|
114
|
+
): AsyncIterable<AdapterYieldChunk> {
|
|
116
115
|
// Narrow before logging: raw SDK errors can carry request metadata
|
|
117
116
|
// (including auth headers) which we must never surface to user loggers.
|
|
118
117
|
const errorPayload = toRunErrorPayload(
|
|
@@ -187,6 +186,8 @@ export abstract class OpenAIBaseChatCompletionsTextAdapter<
|
|
|
187
186
|
|
|
188
187
|
yield {
|
|
189
188
|
type: EventType.RUN_ERROR,
|
|
189
|
+
runId: aguiState.runId,
|
|
190
|
+
threadId: aguiState.threadId,
|
|
190
191
|
model: options.model,
|
|
191
192
|
timestamp: Date.now(),
|
|
192
193
|
message: errorPayload.message,
|
|
@@ -322,7 +323,7 @@ export abstract class OpenAIBaseChatCompletionsTextAdapter<
|
|
|
322
323
|
*/
|
|
323
324
|
async *structuredOutputStream(
|
|
324
325
|
options: StructuredOutputOptions<TProviderOptions>,
|
|
325
|
-
): AsyncIterable<
|
|
326
|
+
): AsyncIterable<AdapterYieldChunk> {
|
|
326
327
|
const { chatOptions, outputSchema } = options
|
|
327
328
|
const requestParams = this.mapOptionsToRequest(chatOptions)
|
|
328
329
|
|
|
@@ -351,7 +352,7 @@ export abstract class OpenAIBaseChatCompletionsTextAdapter<
|
|
|
351
352
|
|
|
352
353
|
const closeReasoningLifecycle = function* (this: {
|
|
353
354
|
name: string
|
|
354
|
-
}): Generator<
|
|
355
|
+
}): Generator<AdapterYieldChunk> {
|
|
355
356
|
if (reasoningMessageId && !hasClosedReasoning) {
|
|
356
357
|
hasClosedReasoning = true
|
|
357
358
|
yield {
|
|
@@ -376,6 +377,9 @@ export abstract class OpenAIBaseChatCompletionsTextAdapter<
|
|
|
376
377
|
content: accumulatedReasoning,
|
|
377
378
|
}
|
|
378
379
|
}
|
|
380
|
+
reasoningMessageId = undefined
|
|
381
|
+
stepId = undefined
|
|
382
|
+
hasClosedReasoning = false
|
|
379
383
|
}
|
|
380
384
|
}.bind(this)
|
|
381
385
|
|
|
@@ -705,7 +709,7 @@ export abstract class OpenAIBaseChatCompletionsTextAdapter<
|
|
|
705
709
|
stream: AsyncIterable<ChatCompletionChunk>,
|
|
706
710
|
options: TextOptions,
|
|
707
711
|
aguiState: ChatStreamState,
|
|
708
|
-
): AsyncIterable<
|
|
712
|
+
): AsyncIterable<AdapterYieldChunk> {
|
|
709
713
|
const normalizeToolInput = createToolInputNormalizer(
|
|
710
714
|
options.tools,
|
|
711
715
|
(schema, required) =>
|
|
@@ -1140,7 +1144,7 @@ export abstract class OpenAIBaseChatCompletionsTextAdapter<
|
|
|
1140
1144
|
// tool results that would never arrive. OpenAI's legacy
|
|
1141
1145
|
// `function_call` value (from the v1 function-calling API) is
|
|
1142
1146
|
// normalized to `tool_calls` — semantically the same termination.
|
|
1143
|
-
const finishReason: NonNullable<
|
|
1147
|
+
const finishReason: NonNullable<AdapterYieldChunk['finishReason']> =
|
|
1144
1148
|
emittedAnyToolCallEnd
|
|
1145
1149
|
? 'tool_calls'
|
|
1146
1150
|
: pendingFinishReason === 'tool_calls'
|
|
@@ -1427,9 +1431,9 @@ export abstract class OpenAIBaseChatCompletionsTextAdapter<
|
|
|
1427
1431
|
* Handles backward compatibility with string content.
|
|
1428
1432
|
*/
|
|
1429
1433
|
protected normalizeContent(
|
|
1430
|
-
content: string | null | Array<ContentPart>,
|
|
1434
|
+
content: string | null | undefined | Array<ContentPart>,
|
|
1431
1435
|
): Array<ContentPart> {
|
|
1432
|
-
if (content === null) {
|
|
1436
|
+
if (content === null || content === undefined) {
|
|
1433
1437
|
return []
|
|
1434
1438
|
}
|
|
1435
1439
|
if (typeof content === 'string') {
|
|
@@ -1442,9 +1446,12 @@ export abstract class OpenAIBaseChatCompletionsTextAdapter<
|
|
|
1442
1446
|
* Extracts text content from a content value that may be string, null, or ContentPart array.
|
|
1443
1447
|
*/
|
|
1444
1448
|
protected extractTextContent(
|
|
1445
|
-
content: string | null | Array<ContentPart>,
|
|
1449
|
+
content: string | null | undefined | Array<ContentPart>,
|
|
1446
1450
|
): string {
|
|
1447
|
-
|
|
1451
|
+
// Tool-call-only assistant turns (e.g. an approval resume replaying the
|
|
1452
|
+
// pending call) carry no text and arrive as `null` or `undefined`; both
|
|
1453
|
+
// must collapse to '' rather than crash on `.filter`.
|
|
1454
|
+
if (content === null || content === undefined) {
|
|
1448
1455
|
return ''
|
|
1449
1456
|
}
|
|
1450
1457
|
if (typeof content === 'string') {
|
|
@@ -29,7 +29,7 @@ import type {
|
|
|
29
29
|
DefaultMessageMetadataByModality,
|
|
30
30
|
Modality,
|
|
31
31
|
ModelMessage,
|
|
32
|
-
|
|
32
|
+
AdapterYieldChunk,
|
|
33
33
|
TextOptions,
|
|
34
34
|
} from '@tanstack/ai'
|
|
35
35
|
|
|
@@ -58,6 +58,16 @@ interface StreamedFunctionCallMetadata {
|
|
|
58
58
|
pendingArguments?: string | undefined
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* The pre-2025-07 spec name for `response.reasoning_text.delta`. Removed from
|
|
63
|
+
* the openai SDK, but OpenAI-compatible providers frozen on the older spec
|
|
64
|
+
* (e.g. Amazon Bedrock's Mantle endpoint serving Gemma) still emit it.
|
|
65
|
+
*/
|
|
66
|
+
interface LegacyReasoningDeltaEvent {
|
|
67
|
+
type: 'response.reasoning.delta'
|
|
68
|
+
delta?: unknown
|
|
69
|
+
}
|
|
70
|
+
|
|
61
71
|
/**
|
|
62
72
|
* Shared implementation of the OpenAI Responses API. Holds the stream-event
|
|
63
73
|
* accumulator + AG-UI lifecycle and calls the OpenAI SDK directly. Subclasses
|
|
@@ -91,7 +101,7 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
91
101
|
|
|
92
102
|
async *chatStream(
|
|
93
103
|
options: TextOptions<TProviderOptions>,
|
|
94
|
-
): AsyncIterable<
|
|
104
|
+
): AsyncIterable<AdapterYieldChunk> {
|
|
95
105
|
// Key streamed state by output item ID because argument deltas reference
|
|
96
106
|
// `item_id`. The state separately retains `call_id`, which is the public
|
|
97
107
|
// tool-call ID and the correlation key for function_call_output.
|
|
@@ -289,7 +299,8 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
289
299
|
* Stream structured output via the Responses API: single request with
|
|
290
300
|
* `text.format: json_schema` + `stream: true`. Consumes Responses-API
|
|
291
301
|
* events (`response.output_text.delta`, `response.reasoning_text.delta`,
|
|
292
|
-
* `response.reasoning_summary_text.delta`,
|
|
302
|
+
* `response.reasoning_summary_text.delta`, the legacy
|
|
303
|
+
* `response.reasoning.delta`, `response.refusal.delta`,
|
|
293
304
|
* `response.completed`, `response.failed`) and re-emits the standard AG-UI
|
|
294
305
|
* lifecycle ending with `CUSTOM 'structured-output.complete'`.
|
|
295
306
|
*
|
|
@@ -300,7 +311,7 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
300
311
|
*/
|
|
301
312
|
async *structuredOutputStream(
|
|
302
313
|
options: StructuredOutputOptions<TProviderOptions>,
|
|
303
|
-
): AsyncIterable<
|
|
314
|
+
): AsyncIterable<AdapterYieldChunk> {
|
|
304
315
|
const { chatOptions, outputSchema } = options
|
|
305
316
|
const requestParams = this.mapOptionsToRequest(chatOptions)
|
|
306
317
|
|
|
@@ -327,7 +338,7 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
327
338
|
|
|
328
339
|
const closeReasoning = function* (this: {
|
|
329
340
|
name: string
|
|
330
|
-
}): Generator<
|
|
341
|
+
}): Generator<AdapterYieldChunk> {
|
|
331
342
|
if (reasoningMessageId && !hasClosedReasoning) {
|
|
332
343
|
hasClosedReasoning = true
|
|
333
344
|
yield {
|
|
@@ -352,12 +363,15 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
352
363
|
content: accumulatedReasoning,
|
|
353
364
|
}
|
|
354
365
|
}
|
|
366
|
+
reasoningMessageId = undefined
|
|
367
|
+
stepId = undefined
|
|
368
|
+
hasClosedReasoning = false
|
|
355
369
|
}
|
|
356
370
|
}.bind(this)
|
|
357
371
|
|
|
358
372
|
const openReasoning = function* (this: {
|
|
359
373
|
name: string
|
|
360
|
-
}): Generator<
|
|
374
|
+
}): Generator<AdapterYieldChunk> {
|
|
361
375
|
if (reasoningMessageId) return
|
|
362
376
|
reasoningMessageId = generateId(this.name)
|
|
363
377
|
stepId = generateId(this.name)
|
|
@@ -393,7 +407,9 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
393
407
|
{ provider: this.name, model: this.model },
|
|
394
408
|
)
|
|
395
409
|
|
|
396
|
-
const stream
|
|
410
|
+
const stream: AsyncIterable<
|
|
411
|
+
ResponseStreamEvent | LegacyReasoningDeltaEvent
|
|
412
|
+
> = await this.client.responses.create(
|
|
397
413
|
{
|
|
398
414
|
...cleanParams,
|
|
399
415
|
stream: true,
|
|
@@ -456,7 +472,8 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
456
472
|
|
|
457
473
|
if (
|
|
458
474
|
chunk.type === 'response.reasoning_text.delta' ||
|
|
459
|
-
chunk.type === 'response.reasoning_summary_text.delta'
|
|
475
|
+
chunk.type === 'response.reasoning_summary_text.delta' ||
|
|
476
|
+
chunk.type === 'response.reasoning.delta'
|
|
460
477
|
) {
|
|
461
478
|
const raw = (chunk as { delta?: unknown }).delta
|
|
462
479
|
const reasoningDelta = Array.isArray(raw)
|
|
@@ -781,6 +798,7 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
781
798
|
* - response.created / response.incomplete / response.failed
|
|
782
799
|
* - response.output_text.delta
|
|
783
800
|
* - response.reasoning_text.delta
|
|
801
|
+
* - response.reasoning.delta (the legacy type used before response.reasoning_text.delta)
|
|
784
802
|
* - response.reasoning_summary_text.delta
|
|
785
803
|
* - response.content_part.added / response.content_part.done
|
|
786
804
|
* - response.output_item.added
|
|
@@ -789,7 +807,7 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
789
807
|
* - error
|
|
790
808
|
*/
|
|
791
809
|
protected async *processStreamChunks(
|
|
792
|
-
stream: AsyncIterable<ResponseStreamEvent>,
|
|
810
|
+
stream: AsyncIterable<ResponseStreamEvent | LegacyReasoningDeltaEvent>,
|
|
793
811
|
toolCallMetadata: Map<string, StreamedFunctionCallMetadata>,
|
|
794
812
|
options: TextOptions<TProviderOptions>,
|
|
795
813
|
aguiState: {
|
|
@@ -798,7 +816,7 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
798
816
|
messageId: string
|
|
799
817
|
hasEmittedRunStarted: boolean
|
|
800
818
|
},
|
|
801
|
-
): AsyncIterable<
|
|
819
|
+
): AsyncIterable<AdapterYieldChunk> {
|
|
802
820
|
const normalizeToolInput = createToolInputNormalizer(
|
|
803
821
|
options.tools,
|
|
804
822
|
(schema, required) =>
|
|
@@ -817,12 +835,95 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
817
835
|
// AG-UI lifecycle tracking
|
|
818
836
|
let stepId: string | null = null
|
|
819
837
|
let hasEmittedTextMessageStart = false
|
|
820
|
-
let
|
|
838
|
+
let reasoningMessageId: string | undefined
|
|
839
|
+
let hasClosedReasoning = false
|
|
821
840
|
// Track whether we've emitted a terminal RUN_FINISHED so the
|
|
822
841
|
// end-of-stream fallback below knows to synthesise one when the upstream
|
|
823
842
|
// cuts off without a response.completed event.
|
|
824
843
|
let runFinishedEmitted = false
|
|
825
844
|
|
|
845
|
+
const adapterName = this.name
|
|
846
|
+
const emitModel = () => model || options.model
|
|
847
|
+
|
|
848
|
+
const openReasoning = function* (): Generator<AdapterYieldChunk> {
|
|
849
|
+
if (reasoningMessageId) return
|
|
850
|
+
reasoningMessageId = generateId(adapterName)
|
|
851
|
+
stepId = generateId(adapterName)
|
|
852
|
+
const timestamp = Date.now()
|
|
853
|
+
const currentModel = emitModel()
|
|
854
|
+
yield {
|
|
855
|
+
type: EventType.REASONING_START,
|
|
856
|
+
messageId: reasoningMessageId,
|
|
857
|
+
model: currentModel,
|
|
858
|
+
timestamp,
|
|
859
|
+
}
|
|
860
|
+
yield {
|
|
861
|
+
type: EventType.REASONING_MESSAGE_START,
|
|
862
|
+
messageId: reasoningMessageId,
|
|
863
|
+
role: 'reasoning' as const,
|
|
864
|
+
model: currentModel,
|
|
865
|
+
timestamp,
|
|
866
|
+
}
|
|
867
|
+
yield {
|
|
868
|
+
type: EventType.STEP_STARTED,
|
|
869
|
+
stepName: stepId,
|
|
870
|
+
stepId,
|
|
871
|
+
model: currentModel,
|
|
872
|
+
timestamp,
|
|
873
|
+
stepType: 'thinking',
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const closeReasoning = function* (): Generator<AdapterYieldChunk> {
|
|
878
|
+
if (!reasoningMessageId || hasClosedReasoning) return
|
|
879
|
+
hasClosedReasoning = true
|
|
880
|
+
const timestamp = Date.now()
|
|
881
|
+
const currentModel = emitModel()
|
|
882
|
+
yield {
|
|
883
|
+
type: EventType.REASONING_MESSAGE_END,
|
|
884
|
+
messageId: reasoningMessageId,
|
|
885
|
+
model: currentModel,
|
|
886
|
+
timestamp,
|
|
887
|
+
}
|
|
888
|
+
yield {
|
|
889
|
+
type: EventType.REASONING_END,
|
|
890
|
+
messageId: reasoningMessageId,
|
|
891
|
+
model: currentModel,
|
|
892
|
+
timestamp,
|
|
893
|
+
}
|
|
894
|
+
if (stepId) {
|
|
895
|
+
yield {
|
|
896
|
+
type: EventType.STEP_FINISHED,
|
|
897
|
+
stepName: stepId,
|
|
898
|
+
stepId,
|
|
899
|
+
model: currentModel,
|
|
900
|
+
timestamp,
|
|
901
|
+
content: accumulatedReasoning,
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
reasoningMessageId = undefined
|
|
905
|
+
stepId = null
|
|
906
|
+
hasClosedReasoning = false
|
|
907
|
+
accumulatedReasoning = ''
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
const emitReasoningDelta = function* (
|
|
911
|
+
text: string,
|
|
912
|
+
): Generator<AdapterYieldChunk> {
|
|
913
|
+
if (!text) return
|
|
914
|
+
yield* openReasoning()
|
|
915
|
+
if (!reasoningMessageId) return
|
|
916
|
+
accumulatedReasoning += text
|
|
917
|
+
hasStreamedReasoningDeltas = true
|
|
918
|
+
yield {
|
|
919
|
+
type: EventType.REASONING_MESSAGE_CONTENT,
|
|
920
|
+
messageId: reasoningMessageId,
|
|
921
|
+
delta: text,
|
|
922
|
+
model: emitModel(),
|
|
923
|
+
timestamp: Date.now(),
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
826
927
|
try {
|
|
827
928
|
for await (const chunk of stream) {
|
|
828
929
|
options.logger.provider(`provider=${this.name} type=${chunk.type}`, {
|
|
@@ -847,7 +948,7 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
847
948
|
type: string
|
|
848
949
|
text?: string
|
|
849
950
|
refusal?: string
|
|
850
|
-
}):
|
|
951
|
+
}): AdapterYieldChunk => {
|
|
851
952
|
if (contentPart.type === 'output_text') {
|
|
852
953
|
accumulatedContent += contentPart.text || ''
|
|
853
954
|
return {
|
|
@@ -860,25 +961,6 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
860
961
|
}
|
|
861
962
|
}
|
|
862
963
|
|
|
863
|
-
if (contentPart.type === 'reasoning_text') {
|
|
864
|
-
accumulatedReasoning += contentPart.text || ''
|
|
865
|
-
// Cache the fallback stepId rather than generating a fresh one
|
|
866
|
-
// on every call — otherwise multiple reasoning chunks arriving
|
|
867
|
-
// before STEP_STARTED was emitted (e.g. via response.content_part.done
|
|
868
|
-
// alone) would each get a different stepId and break correlation.
|
|
869
|
-
if (!stepId) {
|
|
870
|
-
stepId = generateId(this.name)
|
|
871
|
-
}
|
|
872
|
-
return {
|
|
873
|
-
type: EventType.STEP_FINISHED,
|
|
874
|
-
stepName: stepId,
|
|
875
|
-
stepId,
|
|
876
|
-
model: model || options.model,
|
|
877
|
-
timestamp: Date.now(),
|
|
878
|
-
delta: contentPart.text || '',
|
|
879
|
-
content: accumulatedReasoning,
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
964
|
// Either a real refusal or an unknown content_part type. Surface
|
|
883
965
|
// the part type in the error so unknown parts are debuggable
|
|
884
966
|
// instead of being misreported as "Unknown refusal".
|
|
@@ -913,7 +995,9 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
913
995
|
hasStreamedContentDeltas = false
|
|
914
996
|
hasStreamedReasoningDeltas = false
|
|
915
997
|
hasEmittedTextMessageStart = false
|
|
916
|
-
|
|
998
|
+
reasoningMessageId = undefined
|
|
999
|
+
hasClosedReasoning = false
|
|
1000
|
+
stepId = null
|
|
917
1001
|
accumulatedContent = ''
|
|
918
1002
|
accumulatedReasoning = ''
|
|
919
1003
|
}
|
|
@@ -928,6 +1012,7 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
928
1012
|
chunk.type === 'response.failed' ||
|
|
929
1013
|
chunk.type === 'response.incomplete'
|
|
930
1014
|
) {
|
|
1015
|
+
yield* closeReasoning()
|
|
931
1016
|
if (hasEmittedTextMessageStart) {
|
|
932
1017
|
yield {
|
|
933
1018
|
type: EventType.TEXT_MESSAGE_END,
|
|
@@ -983,6 +1068,7 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
983
1068
|
: ''
|
|
984
1069
|
|
|
985
1070
|
if (textDelta) {
|
|
1071
|
+
yield* closeReasoning()
|
|
986
1072
|
// Emit TEXT_MESSAGE_START on first text content
|
|
987
1073
|
if (!hasEmittedTextMessageStart) {
|
|
988
1074
|
hasEmittedTextMessageStart = true
|
|
@@ -1010,81 +1096,27 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
1010
1096
|
|
|
1011
1097
|
// Handle reasoning deltas (token-by-token thinking/reasoning streaming)
|
|
1012
1098
|
// response.reasoning_text.delta provides incremental reasoning updates
|
|
1013
|
-
if (
|
|
1014
|
-
|
|
1099
|
+
if (
|
|
1100
|
+
(chunk.type === 'response.reasoning_text.delta' ||
|
|
1101
|
+
chunk.type === 'response.reasoning.delta') &&
|
|
1102
|
+
chunk.delta
|
|
1103
|
+
) {
|
|
1015
1104
|
const reasoningDelta = Array.isArray(chunk.delta)
|
|
1016
1105
|
? chunk.delta.join('')
|
|
1017
1106
|
: typeof chunk.delta === 'string'
|
|
1018
1107
|
? chunk.delta
|
|
1019
1108
|
: ''
|
|
1020
|
-
|
|
1021
|
-
if (reasoningDelta) {
|
|
1022
|
-
// Emit STEP_STARTED on first reasoning content
|
|
1023
|
-
if (!hasEmittedStepStarted) {
|
|
1024
|
-
hasEmittedStepStarted = true
|
|
1025
|
-
stepId = generateId(this.name)
|
|
1026
|
-
yield {
|
|
1027
|
-
type: EventType.STEP_STARTED,
|
|
1028
|
-
stepName: stepId,
|
|
1029
|
-
stepId,
|
|
1030
|
-
model: model || options.model,
|
|
1031
|
-
timestamp: Date.now(),
|
|
1032
|
-
stepType: 'thinking',
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
accumulatedReasoning += reasoningDelta
|
|
1037
|
-
hasStreamedReasoningDeltas = true
|
|
1038
|
-
const fallbackStepId = stepId || generateId(this.name)
|
|
1039
|
-
yield {
|
|
1040
|
-
type: EventType.STEP_FINISHED,
|
|
1041
|
-
stepName: fallbackStepId,
|
|
1042
|
-
stepId: fallbackStepId,
|
|
1043
|
-
model: model || options.model,
|
|
1044
|
-
timestamp: Date.now(),
|
|
1045
|
-
delta: reasoningDelta,
|
|
1046
|
-
content: accumulatedReasoning,
|
|
1047
|
-
}
|
|
1048
|
-
}
|
|
1109
|
+
yield* emitReasoningDelta(reasoningDelta)
|
|
1049
1110
|
}
|
|
1050
1111
|
|
|
1051
1112
|
// Handle reasoning summary deltas (when using reasoning.summary option)
|
|
1052
|
-
// response.reasoning_summary_text.delta provides incremental summary updates
|
|
1053
1113
|
if (
|
|
1054
1114
|
chunk.type === 'response.reasoning_summary_text.delta' &&
|
|
1055
1115
|
chunk.delta
|
|
1056
1116
|
) {
|
|
1057
1117
|
const summaryDelta =
|
|
1058
1118
|
typeof chunk.delta === 'string' ? chunk.delta : ''
|
|
1059
|
-
|
|
1060
|
-
if (summaryDelta) {
|
|
1061
|
-
// Emit STEP_STARTED on first reasoning content
|
|
1062
|
-
if (!hasEmittedStepStarted) {
|
|
1063
|
-
hasEmittedStepStarted = true
|
|
1064
|
-
stepId = generateId(this.name)
|
|
1065
|
-
yield {
|
|
1066
|
-
type: EventType.STEP_STARTED,
|
|
1067
|
-
stepName: stepId,
|
|
1068
|
-
stepId,
|
|
1069
|
-
model: model || options.model,
|
|
1070
|
-
timestamp: Date.now(),
|
|
1071
|
-
stepType: 'thinking',
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
accumulatedReasoning += summaryDelta
|
|
1076
|
-
hasStreamedReasoningDeltas = true
|
|
1077
|
-
const fallbackStepId = stepId || generateId(this.name)
|
|
1078
|
-
yield {
|
|
1079
|
-
type: EventType.STEP_FINISHED,
|
|
1080
|
-
stepName: fallbackStepId,
|
|
1081
|
-
stepId: fallbackStepId,
|
|
1082
|
-
model: model || options.model,
|
|
1083
|
-
timestamp: Date.now(),
|
|
1084
|
-
delta: summaryDelta,
|
|
1085
|
-
content: accumulatedReasoning,
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1119
|
+
yield* emitReasoningDelta(summaryDelta)
|
|
1088
1120
|
}
|
|
1089
1121
|
|
|
1090
1122
|
// handle content_part added events for text, reasoning and refusals
|
|
@@ -1101,11 +1133,15 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
1101
1133
|
) {
|
|
1102
1134
|
continue
|
|
1103
1135
|
}
|
|
1104
|
-
|
|
1136
|
+
if (contentPart.type === 'reasoning_text') {
|
|
1137
|
+
yield* emitReasoningDelta(contentPart.text || '')
|
|
1138
|
+
continue
|
|
1139
|
+
}
|
|
1105
1140
|
if (
|
|
1106
1141
|
contentPart.type === 'output_text' &&
|
|
1107
1142
|
!hasEmittedTextMessageStart
|
|
1108
1143
|
) {
|
|
1144
|
+
yield* closeReasoning()
|
|
1109
1145
|
hasEmittedTextMessageStart = true
|
|
1110
1146
|
yield {
|
|
1111
1147
|
type: EventType.TEXT_MESSAGE_START,
|
|
@@ -1115,27 +1151,12 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
1115
1151
|
role: 'assistant',
|
|
1116
1152
|
}
|
|
1117
1153
|
}
|
|
1118
|
-
// Emit STEP_STARTED if this is reasoning content
|
|
1119
|
-
if (contentPart.type === 'reasoning_text' && !hasEmittedStepStarted) {
|
|
1120
|
-
hasEmittedStepStarted = true
|
|
1121
|
-
stepId = generateId(this.name)
|
|
1122
|
-
yield {
|
|
1123
|
-
type: EventType.STEP_STARTED,
|
|
1124
|
-
stepName: stepId,
|
|
1125
|
-
stepId,
|
|
1126
|
-
model: model || options.model,
|
|
1127
|
-
timestamp: Date.now(),
|
|
1128
|
-
stepType: 'thinking',
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
1154
|
// Mark whichever stream we just emitted into so a subsequent
|
|
1132
1155
|
// `content_part.done` doesn't duplicate the same text. Without
|
|
1133
1156
|
// this flag, an `added` event carrying the full text followed by
|
|
1134
1157
|
// a matching `done` event would emit TEXT_MESSAGE_CONTENT twice.
|
|
1135
1158
|
if (contentPart.type === 'output_text') {
|
|
1136
1159
|
hasStreamedContentDeltas = true
|
|
1137
|
-
} else if (contentPart.type === 'reasoning_text') {
|
|
1138
|
-
hasStreamedReasoningDeltas = true
|
|
1139
1160
|
}
|
|
1140
1161
|
const partChunk = handleContentPart(contentPart)
|
|
1141
1162
|
yield partChunk
|
|
@@ -1172,10 +1193,15 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
1172
1193
|
// without a start and never see an end. Emit the lifecycle opener
|
|
1173
1194
|
// for whichever stream this content_part belongs to before yielding
|
|
1174
1195
|
// the CONTENT chunk; the post-loop block emits the matching END.
|
|
1196
|
+
if (contentPart.type === 'reasoning_text') {
|
|
1197
|
+
yield* emitReasoningDelta(contentPart.text || '')
|
|
1198
|
+
continue
|
|
1199
|
+
}
|
|
1175
1200
|
if (
|
|
1176
1201
|
contentPart.type === 'output_text' &&
|
|
1177
1202
|
!hasEmittedTextMessageStart
|
|
1178
1203
|
) {
|
|
1204
|
+
yield* closeReasoning()
|
|
1179
1205
|
hasEmittedTextMessageStart = true
|
|
1180
1206
|
yield {
|
|
1181
1207
|
type: EventType.TEXT_MESSAGE_START,
|
|
@@ -1184,20 +1210,6 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
1184
1210
|
timestamp: Date.now(),
|
|
1185
1211
|
role: 'assistant',
|
|
1186
1212
|
}
|
|
1187
|
-
} else if (
|
|
1188
|
-
contentPart.type === 'reasoning_text' &&
|
|
1189
|
-
!hasEmittedStepStarted
|
|
1190
|
-
) {
|
|
1191
|
-
hasEmittedStepStarted = true
|
|
1192
|
-
stepId = generateId(this.name)
|
|
1193
|
-
yield {
|
|
1194
|
-
type: EventType.STEP_STARTED,
|
|
1195
|
-
stepName: stepId,
|
|
1196
|
-
stepId,
|
|
1197
|
-
model: model || options.model,
|
|
1198
|
-
timestamp: Date.now(),
|
|
1199
|
-
stepType: 'thinking',
|
|
1200
|
-
}
|
|
1201
1213
|
}
|
|
1202
1214
|
|
|
1203
1215
|
// Only emit if we haven't been streaming deltas (e.g., for non-streaming responses)
|
|
@@ -1573,6 +1585,7 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
1573
1585
|
}
|
|
1574
1586
|
}
|
|
1575
1587
|
|
|
1588
|
+
yield* closeReasoning()
|
|
1576
1589
|
// Emit TEXT_MESSAGE_END if we had text content
|
|
1577
1590
|
if (hasEmittedTextMessageStart) {
|
|
1578
1591
|
yield {
|
|
@@ -1655,6 +1668,7 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
1655
1668
|
// mirrors the chat-completions adapter's behavior so consumers always
|
|
1656
1669
|
// see a terminal event for every started run.
|
|
1657
1670
|
if (!runFinishedEmitted && aguiState.hasEmittedRunStarted) {
|
|
1671
|
+
yield* closeReasoning()
|
|
1658
1672
|
if (hasEmittedTextMessageStart) {
|
|
1659
1673
|
yield {
|
|
1660
1674
|
type: EventType.TEXT_MESSAGE_END,
|
|
@@ -2035,9 +2049,9 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
2035
2049
|
* Handles backward compatibility with string content.
|
|
2036
2050
|
*/
|
|
2037
2051
|
protected normalizeContent(
|
|
2038
|
-
content: string | null | Array<ContentPart>,
|
|
2052
|
+
content: string | null | undefined | Array<ContentPart>,
|
|
2039
2053
|
): Array<ContentPart> {
|
|
2040
|
-
if (content === null) {
|
|
2054
|
+
if (content === null || content === undefined) {
|
|
2041
2055
|
return []
|
|
2042
2056
|
}
|
|
2043
2057
|
if (typeof content === 'string') {
|
|
@@ -2050,9 +2064,9 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
2050
2064
|
* Extracts text content from a content value that may be string, null, or ContentPart array.
|
|
2051
2065
|
*/
|
|
2052
2066
|
protected extractTextContent(
|
|
2053
|
-
content: string | null | Array<ContentPart>,
|
|
2067
|
+
content: string | null | undefined | Array<ContentPart>,
|
|
2054
2068
|
): string {
|
|
2055
|
-
if (content === null) {
|
|
2069
|
+
if (content === null || content === undefined) {
|
|
2056
2070
|
return ''
|
|
2057
2071
|
}
|
|
2058
2072
|
if (typeof content === 'string') {
|