@tanstack/openai-base 0.10.1 → 0.10.3

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 (29) hide show
  1. package/README.md +2 -2
  2. package/dist/esm/adapters/chat-completions-text.d.ts +15 -7
  3. package/dist/esm/adapters/chat-completions-text.js +23 -7
  4. package/dist/esm/adapters/chat-completions-text.js.map +1 -1
  5. package/dist/esm/adapters/chat-completions-tool-converter.test.d.ts +1 -0
  6. package/dist/esm/adapters/responses-text.d.ts +15 -7
  7. package/dist/esm/adapters/responses-text.js +141 -105
  8. package/dist/esm/adapters/responses-text.js.map +1 -1
  9. package/dist/esm/adapters/responses-tool-converter.test.d.ts +1 -0
  10. package/dist/esm/index.d.ts +1 -1
  11. package/dist/esm/index.js +2 -2
  12. package/dist/esm/tools/function-tool.test.d.ts +1 -0
  13. package/dist/esm/utils/schema-converter.d.ts +14 -0
  14. package/dist/esm/utils/schema-converter.js +106 -18
  15. package/dist/esm/utils/schema-converter.js.map +1 -1
  16. package/dist/esm/utils/tool-input-normalizer.d.ts +12 -0
  17. package/dist/esm/utils/tool-input-normalizer.js +36 -0
  18. package/dist/esm/utils/tool-input-normalizer.js.map +1 -0
  19. package/dist/esm/utils/tool-input-normalizer.test.d.ts +1 -0
  20. package/package.json +3 -3
  21. package/src/adapters/chat-completions-text.ts +51 -18
  22. package/src/adapters/chat-completions-tool-converter.test.ts +64 -0
  23. package/src/adapters/responses-text.ts +198 -130
  24. package/src/adapters/responses-tool-converter.test.ts +69 -0
  25. package/src/index.ts +4 -1
  26. package/src/tools/function-tool.test.ts +62 -0
  27. package/src/utils/schema-converter.ts +165 -20
  28. package/src/utils/tool-input-normalizer.test.ts +146 -0
  29. package/src/utils/tool-input-normalizer.ts +55 -0
@@ -1,6 +1,7 @@
1
- import { makeStructuredOutputCompatible } from "../utils/schema-converter.js";
1
+ import { makeStructuredOutputCompatibleWithMap } from "../utils/schema-converter.js";
2
2
  import { buildResponsesUsage } from "../usage.js";
3
3
  import { extractRequestOptions } from "../utils/request-options.js";
4
+ import { createToolInputNormalizer } from "../utils/tool-input-normalizer.js";
4
5
  import { convertToolsToResponsesFormat } from "./responses-tool-converter.js";
5
6
  import { EventType, normalizeSystemPrompts } from "@tanstack/ai";
6
7
  import { toRunErrorPayload, toRunErrorRawEvent } from "@tanstack/ai/adapter-internals";
@@ -183,6 +184,9 @@ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
183
184
  timestamp: Date.now(),
184
185
  content: accumulatedReasoning
185
186
  };
187
+ reasoningMessageId = void 0;
188
+ stepId = void 0;
189
+ hasClosedReasoning = false;
186
190
  }
187
191
  }.bind(this);
188
192
  const openReasoning = function* () {
@@ -437,11 +441,20 @@ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
437
441
  return e.name === "APIUserAbortError" || e.name === "AbortError" || e.code === "ERR_CANCELED";
438
442
  }
439
443
  /**
444
+ * Strict conversion plus the inverse null-widening map for this request.
445
+ * Override this when schema conversion changes, so tool-input undo matches
446
+ * the wire schema.
447
+ */
448
+ makeStructuredOutputCompatibleWithMap(schema, originalRequired) {
449
+ return makeStructuredOutputCompatibleWithMap(schema, originalRequired);
450
+ }
451
+ /**
440
452
  * Applies provider-specific transformations for structured output compatibility.
441
- * Override this in subclasses to handle provider-specific quirks.
453
+ * Override `makeStructuredOutputCompatibleWithMap` when you need the inverse map
454
+ * to match the wire schema.
442
455
  */
443
456
  makeStructuredOutputCompatible(schema, originalRequired) {
444
- return makeStructuredOutputCompatible(schema, originalRequired);
457
+ return this.makeStructuredOutputCompatibleWithMap(schema, originalRequired).schema;
445
458
  }
446
459
  /**
447
460
  * Final shaping pass applied to parsed structured-output JSON before it is
@@ -502,6 +515,7 @@ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
502
515
  * - error
503
516
  */
504
517
  async *processStreamChunks(stream, toolCallMetadata, options, aguiState) {
518
+ const normalizeToolInput = createToolInputNormalizer(options.tools, (schema, required) => this.makeStructuredOutputCompatibleWithMap(schema, required));
505
519
  let accumulatedContent = "";
506
520
  let accumulatedReasoning = "";
507
521
  let hasStreamedContentDeltas = false;
@@ -509,8 +523,83 @@ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
509
523
  let model = options.model;
510
524
  let stepId = null;
511
525
  let hasEmittedTextMessageStart = false;
512
- let hasEmittedStepStarted = false;
526
+ let reasoningMessageId;
527
+ let hasClosedReasoning = false;
513
528
  let runFinishedEmitted = false;
529
+ const adapterName = this.name;
530
+ const emitModel = () => model || options.model;
531
+ const openReasoning = function* () {
532
+ if (reasoningMessageId) return;
533
+ reasoningMessageId = generateId(adapterName);
534
+ stepId = generateId(adapterName);
535
+ const timestamp = Date.now();
536
+ const currentModel = emitModel();
537
+ yield {
538
+ type: EventType.REASONING_START,
539
+ messageId: reasoningMessageId,
540
+ model: currentModel,
541
+ timestamp
542
+ };
543
+ yield {
544
+ type: EventType.REASONING_MESSAGE_START,
545
+ messageId: reasoningMessageId,
546
+ role: "reasoning",
547
+ model: currentModel,
548
+ timestamp
549
+ };
550
+ yield {
551
+ type: EventType.STEP_STARTED,
552
+ stepName: stepId,
553
+ stepId,
554
+ model: currentModel,
555
+ timestamp,
556
+ stepType: "thinking"
557
+ };
558
+ };
559
+ const closeReasoning = function* () {
560
+ if (!reasoningMessageId || hasClosedReasoning) return;
561
+ hasClosedReasoning = true;
562
+ const timestamp = Date.now();
563
+ const currentModel = emitModel();
564
+ yield {
565
+ type: EventType.REASONING_MESSAGE_END,
566
+ messageId: reasoningMessageId,
567
+ model: currentModel,
568
+ timestamp
569
+ };
570
+ yield {
571
+ type: EventType.REASONING_END,
572
+ messageId: reasoningMessageId,
573
+ model: currentModel,
574
+ timestamp
575
+ };
576
+ if (stepId) yield {
577
+ type: EventType.STEP_FINISHED,
578
+ stepName: stepId,
579
+ stepId,
580
+ model: currentModel,
581
+ timestamp,
582
+ content: accumulatedReasoning
583
+ };
584
+ reasoningMessageId = void 0;
585
+ stepId = null;
586
+ hasClosedReasoning = false;
587
+ accumulatedReasoning = "";
588
+ };
589
+ const emitReasoningDelta = function* (text) {
590
+ if (!text) return;
591
+ yield* openReasoning();
592
+ if (!reasoningMessageId) return;
593
+ accumulatedReasoning += text;
594
+ hasStreamedReasoningDeltas = true;
595
+ yield {
596
+ type: EventType.REASONING_MESSAGE_CONTENT,
597
+ messageId: reasoningMessageId,
598
+ delta: text,
599
+ model: emitModel(),
600
+ timestamp: Date.now()
601
+ };
602
+ };
514
603
  try {
515
604
  for await (const chunk of stream) {
516
605
  options.logger.provider(`provider=${this.name} type=${chunk.type}`, {
@@ -540,19 +629,6 @@ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
540
629
  content: accumulatedContent
541
630
  };
542
631
  }
543
- if (contentPart.type === "reasoning_text") {
544
- accumulatedReasoning += contentPart.text || "";
545
- if (!stepId) stepId = generateId(this.name);
546
- return {
547
- type: EventType.STEP_FINISHED,
548
- stepName: stepId,
549
- stepId,
550
- model: model || options.model,
551
- timestamp: Date.now(),
552
- delta: contentPart.text || "",
553
- content: accumulatedReasoning
554
- };
555
- }
556
632
  const isRefusal = contentPart.type === "refusal";
557
633
  const message = isRefusal ? contentPart.refusal || "Refused without explanation" : `Unsupported response content_part type: ${contentPart.type}`;
558
634
  const code = isRefusal ? "refusal" : contentPart.type;
@@ -573,11 +649,14 @@ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
573
649
  hasStreamedContentDeltas = false;
574
650
  hasStreamedReasoningDeltas = false;
575
651
  hasEmittedTextMessageStart = false;
576
- hasEmittedStepStarted = false;
652
+ reasoningMessageId = void 0;
653
+ hasClosedReasoning = false;
654
+ stepId = null;
577
655
  accumulatedContent = "";
578
656
  accumulatedReasoning = "";
579
657
  }
580
658
  if (chunk.type === "response.failed" || chunk.type === "response.incomplete") {
659
+ yield* closeReasoning();
581
660
  if (hasEmittedTextMessageStart) {
582
661
  yield {
583
662
  type: EventType.TEXT_MESSAGE_END,
@@ -606,6 +685,7 @@ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
606
685
  if (chunk.type === "response.output_text.delta" && chunk.delta) {
607
686
  const textDelta = Array.isArray(chunk.delta) ? chunk.delta.join("") : typeof chunk.delta === "string" ? chunk.delta : "";
608
687
  if (textDelta) {
688
+ yield* closeReasoning();
609
689
  if (!hasEmittedTextMessageStart) {
610
690
  hasEmittedTextMessageStart = true;
611
691
  yield {
@@ -628,67 +708,17 @@ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
628
708
  };
629
709
  }
630
710
  }
631
- if (chunk.type === "response.reasoning_text.delta" && chunk.delta) {
632
- const reasoningDelta = Array.isArray(chunk.delta) ? chunk.delta.join("") : typeof chunk.delta === "string" ? chunk.delta : "";
633
- if (reasoningDelta) {
634
- if (!hasEmittedStepStarted) {
635
- hasEmittedStepStarted = true;
636
- stepId = generateId(this.name);
637
- yield {
638
- type: EventType.STEP_STARTED,
639
- stepName: stepId,
640
- stepId,
641
- model: model || options.model,
642
- timestamp: Date.now(),
643
- stepType: "thinking"
644
- };
645
- }
646
- accumulatedReasoning += reasoningDelta;
647
- hasStreamedReasoningDeltas = true;
648
- const fallbackStepId = stepId || generateId(this.name);
649
- yield {
650
- type: EventType.STEP_FINISHED,
651
- stepName: fallbackStepId,
652
- stepId: fallbackStepId,
653
- model: model || options.model,
654
- timestamp: Date.now(),
655
- delta: reasoningDelta,
656
- content: accumulatedReasoning
657
- };
658
- }
659
- }
660
- if (chunk.type === "response.reasoning_summary_text.delta" && chunk.delta) {
661
- const summaryDelta = typeof chunk.delta === "string" ? chunk.delta : "";
662
- if (summaryDelta) {
663
- if (!hasEmittedStepStarted) {
664
- hasEmittedStepStarted = true;
665
- stepId = generateId(this.name);
666
- yield {
667
- type: EventType.STEP_STARTED,
668
- stepName: stepId,
669
- stepId,
670
- model: model || options.model,
671
- timestamp: Date.now(),
672
- stepType: "thinking"
673
- };
674
- }
675
- accumulatedReasoning += summaryDelta;
676
- hasStreamedReasoningDeltas = true;
677
- const fallbackStepId = stepId || generateId(this.name);
678
- yield {
679
- type: EventType.STEP_FINISHED,
680
- stepName: fallbackStepId,
681
- stepId: fallbackStepId,
682
- model: model || options.model,
683
- timestamp: Date.now(),
684
- delta: summaryDelta,
685
- content: accumulatedReasoning
686
- };
687
- }
688
- }
711
+ if (chunk.type === "response.reasoning_text.delta" && chunk.delta) yield* emitReasoningDelta(Array.isArray(chunk.delta) ? chunk.delta.join("") : typeof chunk.delta === "string" ? chunk.delta : "");
712
+ if (chunk.type === "response.reasoning_summary_text.delta" && chunk.delta) yield* emitReasoningDelta(typeof chunk.delta === "string" ? chunk.delta : "");
689
713
  if (chunk.type === "response.content_part.added") {
690
714
  const contentPart = chunk.part;
715
+ if ((contentPart.type === "output_text" || contentPart.type === "reasoning_text") && !contentPart.text) continue;
716
+ if (contentPart.type === "reasoning_text") {
717
+ yield* emitReasoningDelta(contentPart.text || "");
718
+ continue;
719
+ }
691
720
  if (contentPart.type === "output_text" && !hasEmittedTextMessageStart) {
721
+ yield* closeReasoning();
692
722
  hasEmittedTextMessageStart = true;
693
723
  yield {
694
724
  type: EventType.TEXT_MESSAGE_START,
@@ -698,20 +728,7 @@ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
698
728
  role: "assistant"
699
729
  };
700
730
  }
701
- if (contentPart.type === "reasoning_text" && !hasEmittedStepStarted) {
702
- hasEmittedStepStarted = true;
703
- stepId = generateId(this.name);
704
- yield {
705
- type: EventType.STEP_STARTED,
706
- stepName: stepId,
707
- stepId,
708
- model: model || options.model,
709
- timestamp: Date.now(),
710
- stepType: "thinking"
711
- };
712
- }
713
731
  if (contentPart.type === "output_text") hasStreamedContentDeltas = true;
714
- else if (contentPart.type === "reasoning_text") hasStreamedReasoningDeltas = true;
715
732
  const partChunk = handleContentPart(contentPart);
716
733
  yield partChunk;
717
734
  if (partChunk.type === "RUN_ERROR") {
@@ -723,7 +740,12 @@ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
723
740
  const contentPart = chunk.part;
724
741
  if (contentPart.type === "output_text" && hasStreamedContentDeltas) continue;
725
742
  if (contentPart.type === "reasoning_text" && hasStreamedReasoningDeltas) continue;
743
+ if (contentPart.type === "reasoning_text") {
744
+ yield* emitReasoningDelta(contentPart.text || "");
745
+ continue;
746
+ }
726
747
  if (contentPart.type === "output_text" && !hasEmittedTextMessageStart) {
748
+ yield* closeReasoning();
727
749
  hasEmittedTextMessageStart = true;
728
750
  yield {
729
751
  type: EventType.TEXT_MESSAGE_START,
@@ -732,17 +754,6 @@ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
732
754
  timestamp: Date.now(),
733
755
  role: "assistant"
734
756
  };
735
- } else if (contentPart.type === "reasoning_text" && !hasEmittedStepStarted) {
736
- hasEmittedStepStarted = true;
737
- stepId = generateId(this.name);
738
- yield {
739
- type: EventType.STEP_STARTED,
740
- stepName: stepId,
741
- stepId,
742
- model: model || options.model,
743
- timestamp: Date.now(),
744
- stepType: "thinking"
745
- };
746
757
  }
747
758
  const doneChunk = handleContentPart(contentPart);
748
759
  yield doneChunk;
@@ -820,7 +831,7 @@ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
820
831
  let parsedInput = {};
821
832
  if (chunk.arguments) try {
822
833
  const parsed = JSON.parse(chunk.arguments);
823
- parsedInput = parsed && typeof parsed === "object" ? parsed : {};
834
+ parsedInput = normalizeToolInput(name, parsed && typeof parsed === "object" ? parsed : {});
824
835
  } catch (parseError) {
825
836
  options.logger.errors(`${this.name}.processStreamChunks tool-args JSON parse failed`, {
826
837
  error: toRunErrorPayload(parseError, `tool ${name} (${metadata.callId}) returned malformed JSON arguments`),
@@ -876,7 +887,7 @@ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
876
887
  let parsedInput = {};
877
888
  if (rawArgs) try {
878
889
  const parsed = JSON.parse(rawArgs);
879
- parsedInput = parsed && typeof parsed === "object" ? parsed : {};
890
+ parsedInput = normalizeToolInput(name, parsed && typeof parsed === "object" ? parsed : {});
880
891
  } catch (parseError) {
881
892
  options.logger.errors(`${this.name}.processStreamChunks tool-args JSON parse failed (output_item.done backfill)`, {
882
893
  error: toRunErrorPayload(parseError, `tool ${name} (${metadata.callId}) returned malformed JSON arguments`),
@@ -903,6 +914,29 @@ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
903
914
  }
904
915
  }
905
916
  if (chunk.type === "response.completed") {
917
+ const completedText = chunk.response.output.flatMap((item) => item.type === "message" ? item.content : []).filter((part) => part.type === "output_text").map((part) => part.text).join("");
918
+ if (accumulatedContent.length === 0 && completedText.length > 0) {
919
+ if (!hasEmittedTextMessageStart) {
920
+ hasEmittedTextMessageStart = true;
921
+ yield {
922
+ type: EventType.TEXT_MESSAGE_START,
923
+ messageId: aguiState.messageId,
924
+ model: model || options.model,
925
+ timestamp: Date.now(),
926
+ role: "assistant"
927
+ };
928
+ }
929
+ accumulatedContent = completedText;
930
+ hasStreamedContentDeltas = true;
931
+ yield {
932
+ type: EventType.TEXT_MESSAGE_CONTENT,
933
+ messageId: aguiState.messageId,
934
+ model: model || options.model,
935
+ timestamp: Date.now(),
936
+ delta: completedText,
937
+ content: accumulatedContent
938
+ };
939
+ }
906
940
  for (const item of chunk.response.output) {
907
941
  if (item.type !== "function_call" || !item.id) continue;
908
942
  const metadata = toolCallMetadata.get(item.id) ?? {
@@ -936,7 +970,7 @@ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
936
970
  let parsedInput = {};
937
971
  if (rawArgs) try {
938
972
  const parsed = JSON.parse(rawArgs);
939
- parsedInput = parsed && typeof parsed === "object" ? parsed : {};
973
+ parsedInput = normalizeToolInput(name, parsed && typeof parsed === "object" ? parsed : {});
940
974
  } catch (parseError) {
941
975
  options.logger.errors(`${this.name}.processStreamChunks tool-args JSON parse failed (response.completed backfill)`, {
942
976
  error: toRunErrorPayload(parseError, `tool ${name} (${metadata.callId}) returned malformed JSON arguments`),
@@ -961,6 +995,7 @@ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
961
995
  metadata.pendingArguments = void 0;
962
996
  }
963
997
  }
998
+ yield* closeReasoning();
964
999
  if (hasEmittedTextMessageStart) {
965
1000
  yield {
966
1001
  type: EventType.TEXT_MESSAGE_END,
@@ -1002,6 +1037,7 @@ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
1002
1037
  }
1003
1038
  }
1004
1039
  if (!runFinishedEmitted && aguiState.hasEmittedRunStarted) {
1040
+ yield* closeReasoning();
1005
1041
  if (hasEmittedTextMessageStart) yield {
1006
1042
  type: EventType.TEXT_MESSAGE_END,
1007
1043
  messageId: aguiState.messageId,
@@ -1200,7 +1236,7 @@ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
1200
1236
  * Handles backward compatibility with string content.
1201
1237
  */
1202
1238
  normalizeContent(content) {
1203
- if (content === null) return [];
1239
+ if (content === null || content === void 0) return [];
1204
1240
  if (typeof content === "string") return [{
1205
1241
  type: "text",
1206
1242
  content
@@ -1211,7 +1247,7 @@ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
1211
1247
  * Extracts text content from a content value that may be string, null, or ContentPart array.
1212
1248
  */
1213
1249
  extractTextContent(content) {
1214
- if (content === null) return "";
1250
+ if (content === null || content === void 0) return "";
1215
1251
  if (typeof content === "string") return content;
1216
1252
  return content.filter((p) => p.type === "text").map((p) => p.content).join("");
1217
1253
  }