@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.
@@ -27,8 +27,7 @@ import type {
27
27
  DefaultMessageMetadataByModality,
28
28
  Modality,
29
29
  ModelMessage,
30
- RunFinishedEvent,
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<StreamChunk> {
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<StreamChunk> {
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<StreamChunk> {
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<StreamChunk> {
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<StreamChunk> {
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<RunFinishedEvent['finishReason']> =
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
- if (content === null) {
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
- StreamChunk,
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<StreamChunk> {
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`, `response.refusal.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<StreamChunk> {
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<StreamChunk> {
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<StreamChunk> {
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 = await this.client.responses.create(
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<StreamChunk> {
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 hasEmittedStepStarted = false
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
- }): StreamChunk => {
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
- hasEmittedStepStarted = false
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 (chunk.type === 'response.reasoning_text.delta' && chunk.delta) {
1014
- // Delta can be an array of strings or a single string
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
- // Emit TEXT_MESSAGE_START if this is text content
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') {