@tanstack/ai-gemini 0.17.3 → 0.18.2

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.
@@ -1,5 +1,6 @@
1
1
  import { EventType } from "@tanstack/ai";
2
2
  import { BaseTextAdapter } from "@tanstack/ai/adapters";
3
+ import { parse } from "partial-json";
3
4
  import { createGeminiClient, generateId, getGeminiApiKeyFromEnv } from "../../utils/client.js";
4
5
  class GeminiTextInteractionsAdapter extends BaseTextAdapter {
5
6
  kind = "text";
@@ -130,8 +131,11 @@ class GeminiTextInteractionsAdapter extends BaseTextAdapter {
130
131
  });
131
132
  const request = {
132
133
  ...baseRequest,
133
- response_mime_type: "application/json",
134
- response_format: outputSchema
134
+ response_format: {
135
+ type: "text",
136
+ mime_type: "application/json",
137
+ schema: outputSchema
138
+ }
135
139
  };
136
140
  try {
137
141
  logger.request(
@@ -201,8 +205,7 @@ function buildInteractionsRequest(options) {
201
205
  store: modelOpts?.store,
202
206
  background: modelOpts?.background,
203
207
  response_modalities: modelOpts?.response_modalities,
204
- response_format: modelOpts?.response_format,
205
- response_mime_type: modelOpts?.response_mime_type
208
+ response_format: modelOpts?.response_format
206
209
  };
207
210
  }
208
211
  function convertMessagesToInteractionsInput(messages, hasPreviousInteraction) {
@@ -297,21 +300,13 @@ function messagesAfterLastAssistant(messages) {
297
300
  }
298
301
  return messages;
299
302
  }
300
- function safeParseToolArguments(raw, logger) {
301
- if (!raw) return {};
303
+ function parsePartialToolArguments(raw) {
304
+ if (!raw) return void 0;
302
305
  try {
303
- const parsed = JSON.parse(raw);
304
- return parsed && typeof parsed === "object" ? parsed : {};
305
- } catch (error) {
306
- logger.errors(
307
- "gemini-text-interactions.safeParseToolArguments parse failed",
308
- {
309
- error,
310
- raw,
311
- source: "gemini-text-interactions.chatStream"
312
- }
313
- );
314
- return {};
306
+ const parsed = parse(raw);
307
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
308
+ } catch {
309
+ return void 0;
315
310
  }
316
311
  }
317
312
  const IMAGE_MIME_TYPES = [
@@ -492,6 +487,8 @@ async function* translateInteractionEvents(stream, model, runId, threadId, paren
492
487
  let thinkingAccumulated = "";
493
488
  let reasoningMessageId = null;
494
489
  let hasClosedReasoning = false;
490
+ const indexToToolCallId = /* @__PURE__ */ new Map();
491
+ const argStringByToolCallId = /* @__PURE__ */ new Map();
495
492
  const closeReasoningIfNeeded = function* () {
496
493
  if (reasoningMessageId && !hasClosedReasoning) {
497
494
  hasClosedReasoning = true;
@@ -552,82 +549,132 @@ async function* translateInteractionEvents(stream, model, runId, threadId, paren
552
549
  for await (const event of stream) {
553
550
  logger.provider(`provider=gemini-text-interactions`, { event });
554
551
  switch (event.event_type) {
555
- case "interaction.start": {
552
+ case "interaction.created": {
556
553
  interactionId = event.interaction.id;
557
554
  yield* emitRunStartedIfNeeded();
558
555
  break;
559
556
  }
560
- case "content.start": {
557
+ case "step.start": {
561
558
  yield* emitRunStartedIfNeeded();
562
- break;
563
- }
564
- case "content.delta": {
565
- yield* emitRunStartedIfNeeded();
566
- const delta = event.delta;
567
- switch (delta.type) {
568
- case "text": {
559
+ const step = event.step;
560
+ const index = event.index;
561
+ switch (step.type) {
562
+ case "function_call": {
569
563
  yield* closeReasoningIfNeeded();
570
- if (!hasEmittedTextMessageStart) {
571
- hasEmittedTextMessageStart = true;
564
+ sawFunctionCall = true;
565
+ const toolCallId = step.id;
566
+ indexToToolCallId.set(index, toolCallId);
567
+ const initialArgs = step.arguments;
568
+ const state = {
569
+ name: step.name,
570
+ args: { ...initialArgs },
571
+ index: nextToolIndex++,
572
+ started: true,
573
+ ended: false
574
+ };
575
+ toolCalls.set(toolCallId, state);
576
+ argStringByToolCallId.set(
577
+ toolCallId,
578
+ Object.keys(initialArgs).length > 0 ? JSON.stringify(initialArgs) : ""
579
+ );
580
+ yield {
581
+ type: EventType.TOOL_CALL_START,
582
+ toolCallId,
583
+ toolCallName: state.name,
584
+ toolName: state.name,
585
+ // Bind the tool call to the same assistant message id the
586
+ // eventual TEXT_MESSAGE_START uses so the message id stays
587
+ // stable when a function_call arrives before any text (#477).
588
+ parentMessageId: messageId,
589
+ model,
590
+ timestamp,
591
+ index: state.index
592
+ };
593
+ if (Object.keys(initialArgs).length > 0) {
594
+ const argsJson = JSON.stringify(initialArgs);
572
595
  yield {
573
- type: EventType.TEXT_MESSAGE_START,
574
- messageId,
596
+ type: EventType.TOOL_CALL_ARGS,
597
+ toolCallId,
575
598
  model,
576
599
  timestamp,
577
- role: "assistant"
600
+ delta: argsJson,
601
+ args: argsJson
578
602
  };
579
603
  }
580
- textAccumulated += delta.text;
581
- yield {
582
- type: EventType.TEXT_MESSAGE_CONTENT,
583
- messageId,
584
- model,
585
- timestamp,
586
- delta: delta.text,
587
- content: textAccumulated
588
- };
589
604
  break;
590
605
  }
591
- case "function_call": {
592
- yield* closeReasoningIfNeeded();
593
- sawFunctionCall = true;
594
- const toolCallId = delta.id;
595
- const deltaArgs = typeof delta.arguments === "string" ? safeParseToolArguments(delta.arguments, logger) : delta.arguments;
596
- let state = toolCalls.get(toolCallId);
597
- if (!state) {
598
- state = {
599
- name: delta.name,
600
- args: { ...deltaArgs },
601
- index: nextToolIndex++,
602
- started: false,
603
- ended: false
606
+ case "thought": {
607
+ if (thinkingStepId === null || reasoningMessageId === null) {
608
+ thinkingStepId = generateId(adapterName);
609
+ reasoningMessageId = generateId(adapterName);
610
+ yield {
611
+ type: EventType.REASONING_START,
612
+ messageId: reasoningMessageId,
613
+ model,
614
+ timestamp
615
+ };
616
+ yield {
617
+ type: EventType.REASONING_MESSAGE_START,
618
+ messageId: reasoningMessageId,
619
+ role: "reasoning",
620
+ model,
621
+ timestamp
622
+ };
623
+ yield {
624
+ type: EventType.STEP_STARTED,
625
+ stepName: thinkingStepId,
626
+ stepId: thinkingStepId,
627
+ model,
628
+ timestamp,
629
+ stepType: "thinking"
604
630
  };
605
- toolCalls.set(toolCallId, state);
606
- } else {
607
- state.args = { ...state.args, ...deltaArgs };
608
- if (delta.name) state.name = delta.name;
609
631
  }
610
- if (!state.started) {
611
- state.started = true;
632
+ for (const part of step.summary ?? []) {
633
+ if (part.type !== "text" || !part.text) continue;
634
+ thinkingAccumulated += part.text;
612
635
  yield {
613
- type: EventType.TOOL_CALL_START,
614
- toolCallId,
615
- toolCallName: state.name,
616
- toolName: state.name,
617
- parentMessageId: messageId,
636
+ type: EventType.REASONING_MESSAGE_CONTENT,
637
+ messageId: reasoningMessageId,
638
+ delta: part.text,
639
+ model,
640
+ timestamp
641
+ };
642
+ yield {
643
+ type: EventType.STEP_FINISHED,
644
+ stepName: thinkingStepId,
645
+ stepId: thinkingStepId,
618
646
  model,
619
647
  timestamp,
620
- index: state.index
648
+ delta: part.text,
649
+ content: thinkingAccumulated
650
+ };
651
+ }
652
+ break;
653
+ }
654
+ case "model_output": {
655
+ yield* closeReasoningIfNeeded();
656
+ for (const part of step.content ?? []) {
657
+ if (part.type !== "text" || !part.text) continue;
658
+ if (!hasEmittedTextMessageStart) {
659
+ hasEmittedTextMessageStart = true;
660
+ yield {
661
+ type: EventType.TEXT_MESSAGE_START,
662
+ messageId,
663
+ model,
664
+ timestamp,
665
+ role: "assistant"
666
+ };
667
+ }
668
+ textAccumulated += part.text;
669
+ yield {
670
+ type: EventType.TEXT_MESSAGE_CONTENT,
671
+ messageId,
672
+ model,
673
+ timestamp,
674
+ delta: part.text,
675
+ content: textAccumulated
621
676
  };
622
677
  }
623
- yield {
624
- type: EventType.TOOL_CALL_ARGS,
625
- toolCallId,
626
- model,
627
- timestamp,
628
- delta: JSON.stringify(deltaArgs),
629
- args: JSON.stringify(state.args)
630
- };
631
678
  break;
632
679
  }
633
680
  case "google_search_call": {
@@ -635,7 +682,7 @@ async function* translateInteractionEvents(stream, model, runId, threadId, paren
635
682
  yield {
636
683
  type: EventType.CUSTOM,
637
684
  name: "gemini.googleSearchCall",
638
- value: delta,
685
+ value: step,
639
686
  model,
640
687
  timestamp
641
688
  };
@@ -646,7 +693,7 @@ async function* translateInteractionEvents(stream, model, runId, threadId, paren
646
693
  yield {
647
694
  type: EventType.CUSTOM,
648
695
  name: "gemini.googleSearchResult",
649
- value: delta,
696
+ value: step,
650
697
  model,
651
698
  timestamp
652
699
  };
@@ -657,7 +704,7 @@ async function* translateInteractionEvents(stream, model, runId, threadId, paren
657
704
  yield {
658
705
  type: EventType.CUSTOM,
659
706
  name: "gemini.codeExecutionCall",
660
- value: delta,
707
+ value: step,
661
708
  model,
662
709
  timestamp
663
710
  };
@@ -668,7 +715,7 @@ async function* translateInteractionEvents(stream, model, runId, threadId, paren
668
715
  yield {
669
716
  type: EventType.CUSTOM,
670
717
  name: "gemini.codeExecutionResult",
671
- value: delta,
718
+ value: step,
672
719
  model,
673
720
  timestamp
674
721
  };
@@ -679,7 +726,7 @@ async function* translateInteractionEvents(stream, model, runId, threadId, paren
679
726
  yield {
680
727
  type: EventType.CUSTOM,
681
728
  name: "gemini.urlContextCall",
682
- value: delta,
729
+ value: step,
683
730
  model,
684
731
  timestamp
685
732
  };
@@ -690,7 +737,7 @@ async function* translateInteractionEvents(stream, model, runId, threadId, paren
690
737
  yield {
691
738
  type: EventType.CUSTOM,
692
739
  name: "gemini.urlContextResult",
693
- value: delta,
740
+ value: step,
694
741
  model,
695
742
  timestamp
696
743
  };
@@ -701,7 +748,7 @@ async function* translateInteractionEvents(stream, model, runId, threadId, paren
701
748
  yield {
702
749
  type: EventType.CUSTOM,
703
750
  name: "gemini.fileSearchCall",
704
- value: delta,
751
+ value: step,
705
752
  model,
706
753
  timestamp
707
754
  };
@@ -712,12 +759,83 @@ async function* translateInteractionEvents(stream, model, runId, threadId, paren
712
759
  yield {
713
760
  type: EventType.CUSTOM,
714
761
  name: "gemini.fileSearchResult",
715
- value: delta,
762
+ value: step,
716
763
  model,
717
764
  timestamp
718
765
  };
719
766
  break;
720
767
  }
768
+ // Unhandled step types (user_input on GET timelines,
769
+ // mcp_server_*, google_maps_*, function_result) fall through
770
+ // to the observability default so SDK drift is visible.
771
+ case "user_input":
772
+ case "mcp_server_tool_call":
773
+ case "mcp_server_tool_result":
774
+ case "google_maps_call":
775
+ case "google_maps_result":
776
+ case "function_result":
777
+ default:
778
+ logger.provider(`gemini-text-interactions unhandled step.start`, {
779
+ step
780
+ });
781
+ break;
782
+ }
783
+ break;
784
+ }
785
+ case "step.delta": {
786
+ yield* emitRunStartedIfNeeded();
787
+ const delta = event.delta;
788
+ const index = event.index;
789
+ switch (delta.type) {
790
+ case "text": {
791
+ yield* closeReasoningIfNeeded();
792
+ if (!hasEmittedTextMessageStart) {
793
+ hasEmittedTextMessageStart = true;
794
+ yield {
795
+ type: EventType.TEXT_MESSAGE_START,
796
+ messageId,
797
+ model,
798
+ timestamp,
799
+ role: "assistant"
800
+ };
801
+ }
802
+ textAccumulated += delta.text;
803
+ yield {
804
+ type: EventType.TEXT_MESSAGE_CONTENT,
805
+ messageId,
806
+ model,
807
+ timestamp,
808
+ delta: delta.text,
809
+ content: textAccumulated
810
+ };
811
+ break;
812
+ }
813
+ case "arguments_delta": {
814
+ const toolCallId = indexToToolCallId.get(index);
815
+ if (!toolCallId) {
816
+ logger.provider(
817
+ `gemini-text-interactions arguments_delta for unknown step index`,
818
+ { index, delta }
819
+ );
820
+ break;
821
+ }
822
+ const state = toolCalls.get(toolCallId);
823
+ if (!state) break;
824
+ const fragment = delta.arguments ?? "";
825
+ const buffer = (argStringByToolCallId.get(toolCallId) ?? "") + fragment;
826
+ argStringByToolCallId.set(toolCallId, buffer);
827
+ const parsed = parsePartialToolArguments(buffer);
828
+ if (parsed) state.args = parsed;
829
+ yield {
830
+ type: EventType.TOOL_CALL_ARGS,
831
+ toolCallId,
832
+ model,
833
+ timestamp,
834
+ delta: fragment,
835
+ args: buffer
836
+ };
837
+ break;
838
+ }
721
839
  case "thought_summary": {
722
840
  const thoughtText = delta.content && "text" in delta.content ? delta.content.text : "";
723
841
  if (!thoughtText) break;
@@ -765,33 +883,62 @@ async function* translateInteractionEvents(stream, model, runId, threadId, paren
765
883
  };
766
884
  break;
767
885
  }
768
- // The following delta types are valid per the SDK type union
769
- // but aren't yet translated by this adapter (output modalities
770
- // text-only adapter shouldn't see, response-side function_result
771
- // / mcp_server_*, thought_signature). Falling through to the
772
- // observability default so SDK drift is visible.
886
+ // The remaining StepDelta variants (image/audio/video/document
887
+ // for output modalities a text adapter shouldn't see, tool
888
+ // call/result deltas which are surfaced via step.start in this
889
+ // adapter, thought_signature, annotation deltas, mcp/google
890
+ // maps variants) fall through to the observability default.
773
891
  case "image":
774
892
  case "audio":
775
893
  case "video":
776
894
  case "document":
777
- case "function_result":
895
+ case "thought_signature":
896
+ case "text_annotation_delta":
897
+ case "code_execution_call":
898
+ case "code_execution_result":
899
+ case "url_context_call":
900
+ case "url_context_result":
901
+ case "google_search_call":
902
+ case "google_search_result":
903
+ case "file_search_call":
904
+ case "file_search_result":
778
905
  case "mcp_server_tool_call":
779
906
  case "mcp_server_tool_result":
780
- case "thought_signature":
907
+ case "google_maps_call":
908
+ case "google_maps_result":
909
+ case "function_result":
781
910
  default:
782
911
  logger.provider(
783
- `gemini-text-interactions unhandled content.delta type`,
912
+ `gemini-text-interactions unhandled step.delta type`,
784
913
  { delta }
785
914
  );
786
915
  break;
787
916
  }
788
917
  break;
789
918
  }
790
- case "content.stop":
919
+ case "step.stop": {
920
+ const toolCallId = indexToToolCallId.get(event.index);
921
+ if (toolCallId) {
922
+ const state = toolCalls.get(toolCallId);
923
+ if (state && !state.ended) {
924
+ state.ended = true;
925
+ yield {
926
+ type: EventType.TOOL_CALL_END,
927
+ toolCallId,
928
+ toolName: state.name,
929
+ model,
930
+ timestamp,
931
+ input: state.args
932
+ };
933
+ }
934
+ indexToToolCallId.delete(event.index);
935
+ }
936
+ break;
937
+ }
791
938
  case "interaction.status_update": {
792
939
  break;
793
940
  }
794
- case "interaction.complete": {
941
+ case "interaction.completed": {
795
942
  if (event.interaction.id) {
796
943
  interactionId = event.interaction.id;
797
944
  }
@@ -871,10 +1018,16 @@ async function* translateInteractionEvents(stream, model, runId, threadId, paren
871
1018
  yield* closeOpenState();
872
1019
  }
873
1020
  function extractTextFromInteraction(interaction) {
1021
+ if (typeof interaction.output_text === "string" && interaction.output_text) {
1022
+ return interaction.output_text;
1023
+ }
874
1024
  let text = "";
875
- for (const output of interaction.outputs ?? []) {
876
- if (output.type === "text") {
877
- text += output.text;
1025
+ for (const step of interaction.steps) {
1026
+ if (step.type !== "model_output" || !step.content) continue;
1027
+ for (const part of step.content) {
1028
+ if (part.type === "text") {
1029
+ text += part.text;
1030
+ }
878
1031
  }
879
1032
  }
880
1033
  return text;