@tanstack/ai-gemini 0.10.7 → 0.10.8

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 (45) hide show
  1. package/dist/esm/adapters/audio.d.ts +1 -1
  2. package/dist/esm/adapters/audio.js +3 -2
  3. package/dist/esm/adapters/audio.js.map +1 -1
  4. package/dist/esm/adapters/image.d.ts +1 -1
  5. package/dist/esm/adapters/image.js +13 -8
  6. package/dist/esm/adapters/image.js.map +1 -1
  7. package/dist/esm/adapters/text.d.ts +1 -1
  8. package/dist/esm/adapters/text.js +45 -25
  9. package/dist/esm/adapters/text.js.map +1 -1
  10. package/dist/esm/adapters/tts.d.ts +1 -1
  11. package/dist/esm/adapters/tts.js +6 -5
  12. package/dist/esm/adapters/tts.js.map +1 -1
  13. package/dist/esm/image/image-provider-options.js +3 -2
  14. package/dist/esm/image/image-provider-options.js.map +1 -1
  15. package/dist/esm/tools/code-execution-tool.js +3 -2
  16. package/dist/esm/tools/code-execution-tool.js.map +1 -1
  17. package/dist/esm/tools/computer-use-tool.d.ts +2 -2
  18. package/dist/esm/tools/computer-use-tool.js +15 -6
  19. package/dist/esm/tools/computer-use-tool.js.map +1 -1
  20. package/dist/esm/tools/file-search-tool.js +3 -2
  21. package/dist/esm/tools/file-search-tool.js.map +1 -1
  22. package/dist/esm/tools/function-declaration-tool.js +1 -2
  23. package/dist/esm/tools/function-declaration-tool.js.map +1 -1
  24. package/dist/esm/tools/google-maps-tool.js +3 -2
  25. package/dist/esm/tools/google-maps-tool.js.map +1 -1
  26. package/dist/esm/tools/google-search-retriveal-tool.js +3 -2
  27. package/dist/esm/tools/google-search-retriveal-tool.js.map +1 -1
  28. package/dist/esm/tools/google-search-tool.js +3 -2
  29. package/dist/esm/tools/google-search-tool.js.map +1 -1
  30. package/dist/esm/tools/url-context-tool.js +3 -2
  31. package/dist/esm/tools/url-context-tool.js.map +1 -1
  32. package/package.json +3 -3
  33. package/src/adapters/audio.ts +2 -2
  34. package/src/adapters/image.ts +13 -6
  35. package/src/adapters/text.ts +69 -36
  36. package/src/adapters/tts.ts +7 -6
  37. package/src/image/image-provider-options.ts +3 -2
  38. package/src/tools/code-execution-tool.ts +3 -3
  39. package/src/tools/computer-use-tool.ts +18 -7
  40. package/src/tools/file-search-tool.ts +3 -3
  41. package/src/tools/function-declaration-tool.ts +1 -2
  42. package/src/tools/google-maps-tool.ts +3 -3
  43. package/src/tools/google-search-retriveal-tool.ts +3 -3
  44. package/src/tools/google-search-tool.ts +3 -3
  45. package/src/tools/url-context-tool.ts +3 -3
@@ -106,10 +106,10 @@ export class GeminiTextAdapter<
106
106
  TToolCapabilities,
107
107
  GeminiToolCallMetadata
108
108
  > {
109
- readonly kind = 'text' as const
109
+ override readonly kind = 'text' as const
110
110
  readonly name = 'gemini' as const
111
111
 
112
- private client: GoogleGenAI
112
+ private readonly client: GoogleGenAI
113
113
 
114
114
  constructor(config: GeminiTextConfig, model: TModel) {
115
115
  super({}, model)
@@ -315,10 +315,12 @@ export class GeminiTextAdapter<
315
315
 
316
316
  accumulatedThinking += part.text
317
317
 
318
- // Spec REASONING content event
318
+ // Spec REASONING content event — reasoningMessageId is set in the
319
+ // hasEmittedStepStarted block above (entered on the same `part.thought` path)
320
+ if (!reasoningMessageId) continue
319
321
  yield {
320
322
  type: EventType.REASONING_MESSAGE_CONTENT,
321
- messageId: reasoningMessageId!,
323
+ messageId: reasoningMessageId,
322
324
  delta: part.text,
323
325
  model,
324
326
  timestamp: Date.now(),
@@ -399,7 +401,11 @@ export class GeminiTextAdapter<
399
401
  : JSON.stringify(functionArgs),
400
402
  index: nextToolIndex++,
401
403
  started: false,
402
- thoughtSignature: partThoughtSignature,
404
+ // Only set thoughtSignature when present — under EOPT, the
405
+ // optional field cannot accept an explicit `undefined`.
406
+ ...(partThoughtSignature !== undefined && {
407
+ thoughtSignature: partThoughtSignature,
408
+ }),
403
409
  }
404
410
  toolCallMap.set(toolCallId, toolCallData)
405
411
  } else {
@@ -617,13 +623,16 @@ export class GeminiTextAdapter<
617
623
  model,
618
624
  timestamp: Date.now(),
619
625
  finishReason: toolCallMap.size > 0 ? 'tool_calls' : 'stop',
620
- usage: chunk.usageMetadata
621
- ? {
622
- promptTokens: chunk.usageMetadata.promptTokenCount ?? 0,
623
- completionTokens: chunk.usageMetadata.candidatesTokenCount ?? 0,
624
- totalTokens: chunk.usageMetadata.totalTokenCount ?? 0,
625
- }
626
- : undefined,
626
+ // RunFinishedEvent.usage is `usage?: {...}` (no `| undefined`) under
627
+ // exactOptionalPropertyTypes; only include it when usageMetadata is
628
+ // present rather than assigning an explicit `undefined`.
629
+ ...(chunk.usageMetadata && {
630
+ usage: {
631
+ promptTokens: chunk.usageMetadata.promptTokenCount ?? 0,
632
+ completionTokens: chunk.usageMetadata.candidatesTokenCount ?? 0,
633
+ totalTokens: chunk.usageMetadata.totalTokenCount ?? 0,
634
+ },
635
+ }),
627
636
  }
628
637
  }
629
638
  }
@@ -707,10 +716,7 @@ export class GeminiTextAdapter<
707
716
  >)
708
717
  : {}
709
718
  } catch {
710
- parsedArgs = toolCall.function.arguments as unknown as Record<
711
- string,
712
- unknown
713
- >
719
+ parsedArgs = {}
714
720
  }
715
721
 
716
722
  const thoughtSignature = (
@@ -819,31 +825,58 @@ export class GeminiTextAdapter<
819
825
  private mapCommonOptionsToGemini(
820
826
  options: TextOptions<GeminiTextProviderOptions>,
821
827
  ) {
822
- const modelOpts = options.modelOptions
823
- const thinkingConfig = modelOpts?.thinkingConfig
828
+ // Separate `thinkingConfig` from the other model options so the loose
829
+ // local `thinkingLevel?: keyof typeof ThinkingLevel` type doesn't leak
830
+ // into the SDK config object via the `...modelOpts` spread — we re-add a
831
+ // properly-typed `ThinkingConfig` below.
832
+ const { thinkingConfig, ...modelOpts } =
833
+ options.modelOptions ?? ({} as GeminiTextProviderOptions)
834
+ // Build the thinkingConfig payload only when the caller actually supplied
835
+ // one. Our local `thinkingLevel` is typed as `keyof typeof ThinkingLevel`
836
+ // (string union) so users can pass plain strings; the SDK target is the
837
+ // `ThinkingLevel` enum, and every field is `field?: T` under EOPT — so we
838
+ // re-emit fields via conditional spreads.
839
+ const mappedThinkingConfig = thinkingConfig
840
+ ? {
841
+ ...(thinkingConfig.includeThoughts !== undefined && {
842
+ includeThoughts: thinkingConfig.includeThoughts,
843
+ }),
844
+ ...(thinkingConfig.thinkingBudget !== undefined && {
845
+ thinkingBudget: thinkingConfig.thinkingBudget,
846
+ }),
847
+ ...(thinkingConfig.thinkingLevel
848
+ ? {
849
+ thinkingLevel: thinkingConfig.thinkingLevel as ThinkingLevel,
850
+ }
851
+ : {}),
852
+ }
853
+ : undefined
854
+
855
+ const normalizedPrompts = normalizeSystemPrompts(options.systemPrompts)
856
+ const systemInstruction =
857
+ normalizedPrompts.length > 0
858
+ ? normalizedPrompts.map((p) => p.content).join('\n')
859
+ : undefined
860
+
861
+ // Vendor `GenerateContentConfig` fields are `field?: T` (no `| undefined`)
862
+ // under EOPT, so spread each common option only when present rather than
863
+ // emitting `field: undefined`s into the wire payload.
824
864
  const requestOptions: GenerateContentParameters = {
825
865
  model: options.model,
826
866
  contents: this.formatMessages(options.messages),
827
867
  config: {
828
868
  ...modelOpts,
829
- temperature: options.temperature,
830
- topP: options.topP,
831
- maxOutputTokens: options.maxTokens,
832
- thinkingConfig: thinkingConfig
833
- ? {
834
- ...thinkingConfig,
835
- thinkingLevel: thinkingConfig.thinkingLevel
836
- ? // Enum is provided by the SDK, we use it for the type but cast it to string constants, here we just cast them back
837
- (thinkingConfig.thinkingLevel as ThinkingLevel)
838
- : undefined,
839
- }
840
- : undefined,
841
- systemInstruction: (() => {
842
- const prompts = normalizeSystemPrompts(options.systemPrompts)
843
- return prompts.length > 0
844
- ? prompts.map((p) => p.content).join('\n')
845
- : undefined
846
- })(),
869
+ ...(options.temperature !== undefined && {
870
+ temperature: options.temperature,
871
+ }),
872
+ ...(options.topP !== undefined && { topP: options.topP }),
873
+ ...(options.maxTokens !== undefined && {
874
+ maxOutputTokens: options.maxTokens,
875
+ }),
876
+ ...(mappedThinkingConfig !== undefined && {
877
+ thinkingConfig: mappedThinkingConfig,
878
+ }),
879
+ ...(systemInstruction !== undefined && { systemInstruction }),
847
880
  tools: convertToolsToProviderFormat(options.tools),
848
881
  },
849
882
  }
@@ -121,7 +121,7 @@ export class GeminiTTSAdapter<
121
121
  > extends BaseTTSAdapter<TModel, GeminiTTSProviderOptions> {
122
122
  readonly name = 'gemini' as const
123
123
 
124
- private client: GoogleGenAI
124
+ private readonly client: GoogleGenAI
125
125
 
126
126
  constructor(config: GeminiTTSConfig, model: TModel) {
127
127
  super(model, config)
@@ -251,8 +251,9 @@ export class GeminiTTSAdapter<
251
251
  }
252
252
 
253
253
  // Strip any mime parameters (e.g. `audio/ogg;codec=opus`) before pulling
254
- // the subtype out as the file format.
255
- const format = mimeType.split(';')[0]!.split('/')[1] || 'wav'
254
+ // the subtype out as the file format. `String.split` always returns at
255
+ // least one element, so [0] is defined.
256
+ const format = (mimeType.split(';')[0] ?? '').split('/')[1] || 'wav'
256
257
 
257
258
  return {
258
259
  id: generateId(this.name),
@@ -275,7 +276,7 @@ function parsePcmMimeType(
275
276
  mimeType: string,
276
277
  ): { sampleRate: number; channels: number; bitsPerSample: number } | undefined {
277
278
  const normalized = mimeType.toLowerCase()
278
- const subtype = normalized.split(';')[0]!.split('/')[1] ?? ''
279
+ const subtype = (normalized.split(';')[0] ?? '').split('/')[1] ?? ''
279
280
  // Exclude containerized wav (e.g. `audio/wav;codec=pcm`) — those already
280
281
  // carry a RIFF header and must not be re-wrapped.
281
282
  if (subtype.includes('wav')) return undefined
@@ -351,8 +352,8 @@ function wrapPcmBase64AsWav(
351
352
  }
352
353
  let binary = ''
353
354
  const bytes = new Uint8Array(buffer)
354
- for (let i = 0; i < bytes.byteLength; i += 1) {
355
- binary += String.fromCharCode(bytes[i]!)
355
+ for (const byte of bytes) {
356
+ binary += String.fromCharCode(byte)
356
357
  }
357
358
  return btoa(binary)
358
359
  }
@@ -304,6 +304,7 @@ export function parseNativeImageSize(
304
304
  size: string,
305
305
  ): { aspectRatio: string; resolution: string } | undefined {
306
306
  const match = size.match(/^(\d+:\d+)_(.+)$/)
307
- if (!match) return undefined
308
- return { aspectRatio: match[1]!, resolution: match[2]! }
307
+ const [, aspectRatio, resolution] = match ?? []
308
+ if (aspectRatio === undefined || resolution === undefined) return undefined
309
+ return { aspectRatio, resolution }
309
310
  }
@@ -1,3 +1,4 @@
1
+ import { brandProviderTool } from '@tanstack/ai'
1
2
  import type { ProviderTool, Tool } from '@tanstack/ai'
2
3
 
3
4
  export interface CodeExecutionToolConfig {}
@@ -14,10 +15,9 @@ export function convertCodeExecutionToolToAdapterFormat(_tool: Tool) {
14
15
  }
15
16
 
16
17
  export function codeExecutionTool(): GeminiCodeExecutionTool {
17
- // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime.
18
- return {
18
+ return brandProviderTool<GeminiCodeExecutionTool>({
19
19
  name: 'code_execution',
20
20
  description: '',
21
21
  metadata: {},
22
- } as unknown as GeminiCodeExecutionTool
22
+ })
23
23
  }
@@ -1,3 +1,4 @@
1
+ import { brandProviderTool } from '@tanstack/ai'
1
2
  import type { ComputerUse } from '@google/genai'
2
3
  import type { ProviderTool, Tool } from '@tanstack/ai'
3
4
 
@@ -10,10 +11,17 @@ export type GeminiComputerUseTool = ProviderTool<'gemini', 'computer_use'>
10
11
 
11
12
  export function convertComputerUseToolToAdapterFormat(tool: Tool) {
12
13
  const metadata = tool.metadata as ComputerUseToolConfig
14
+ // Vendor `ComputerUse` fields are `field?: T` (no `| undefined`) under EOPT,
15
+ // so spread each field conditionally rather than emitting explicit
16
+ // `undefined`s on the wire payload.
13
17
  return {
14
18
  computerUse: {
15
- environment: metadata.environment,
16
- excludedPredefinedFunctions: metadata.excludedPredefinedFunctions,
19
+ ...(metadata.environment !== undefined && {
20
+ environment: metadata.environment,
21
+ }),
22
+ ...(metadata.excludedPredefinedFunctions !== undefined && {
23
+ excludedPredefinedFunctions: metadata.excludedPredefinedFunctions,
24
+ }),
17
25
  },
18
26
  }
19
27
  }
@@ -21,13 +29,16 @@ export function convertComputerUseToolToAdapterFormat(tool: Tool) {
21
29
  export function computerUseTool(
22
30
  config: ComputerUseToolConfig,
23
31
  ): GeminiComputerUseTool {
24
- // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime.
25
- return {
32
+ return brandProviderTool<GeminiComputerUseTool>({
26
33
  name: 'computer_use',
27
34
  description: '',
28
35
  metadata: {
29
- environment: config.environment,
30
- excludedPredefinedFunctions: config.excludedPredefinedFunctions,
36
+ ...(config.environment !== undefined && {
37
+ environment: config.environment,
38
+ }),
39
+ ...(config.excludedPredefinedFunctions !== undefined && {
40
+ excludedPredefinedFunctions: config.excludedPredefinedFunctions,
41
+ }),
31
42
  },
32
- } as unknown as GeminiComputerUseTool
43
+ })
33
44
  }
@@ -1,3 +1,4 @@
1
+ import { brandProviderTool } from '@tanstack/ai'
1
2
  import type { FileSearch } from '@google/genai'
2
3
  import type { ProviderTool, Tool } from '@tanstack/ai'
3
4
 
@@ -18,10 +19,9 @@ export function convertFileSearchToolToAdapterFormat(tool: Tool) {
18
19
  export function fileSearchTool(
19
20
  config: FileSearchToolConfig,
20
21
  ): GeminiFileSearchTool {
21
- // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime.
22
- return {
22
+ return brandProviderTool<GeminiFileSearchTool>({
23
23
  name: 'file_search',
24
24
  description: '',
25
25
  metadata: config,
26
- } as unknown as GeminiFileSearchTool
26
+ })
27
27
  }
@@ -4,8 +4,7 @@ export type FunctionDeclarationTool = FunctionDeclaration
4
4
 
5
5
  const validateFunctionDeclarationTool = (tool: FunctionDeclarationTool) => {
6
6
  const nameRegex = /^[a-zA-Z0-9_:.-]{1,64}$/
7
- const valid = nameRegex.test(tool.name!)
8
- if (!valid) {
7
+ if (tool.name === undefined || !nameRegex.test(tool.name)) {
9
8
  throw new Error(
10
9
  `Invalid function name: ${tool.name}. Must be 1-64 characters long and contain only a-z, A-Z, 0-9, underscores, colons, dots, and dashes.`,
11
10
  )
@@ -1,3 +1,4 @@
1
+ import { brandProviderTool } from '@tanstack/ai'
1
2
  import type { GoogleMaps } from '@google/genai'
2
3
  import type { ProviderTool, Tool } from '@tanstack/ai'
3
4
 
@@ -18,10 +19,9 @@ export function convertGoogleMapsToolToAdapterFormat(tool: Tool) {
18
19
  export function googleMapsTool(
19
20
  config?: GoogleMapsToolConfig,
20
21
  ): GeminiGoogleMapsTool {
21
- // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime.
22
- return {
22
+ return brandProviderTool<GeminiGoogleMapsTool>({
23
23
  name: 'google_maps',
24
24
  description: '',
25
25
  metadata: config,
26
- } as unknown as GeminiGoogleMapsTool
26
+ })
27
27
  }
@@ -1,3 +1,4 @@
1
+ import { brandProviderTool } from '@tanstack/ai'
1
2
  import type { GoogleSearchRetrieval } from '@google/genai'
2
3
  import type { ProviderTool, Tool } from '@tanstack/ai'
3
4
 
@@ -21,10 +22,9 @@ export function convertGoogleSearchRetrievalToolToAdapterFormat(tool: Tool) {
21
22
  export function googleSearchRetrievalTool(
22
23
  config?: GoogleSearchRetrievalToolConfig,
23
24
  ): GeminiGoogleSearchRetrievalTool {
24
- // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime.
25
- return {
25
+ return brandProviderTool<GeminiGoogleSearchRetrievalTool>({
26
26
  name: 'google_search_retrieval',
27
27
  description: '',
28
28
  metadata: config,
29
- } as unknown as GeminiGoogleSearchRetrievalTool
29
+ })
30
30
  }
@@ -1,3 +1,4 @@
1
+ import { brandProviderTool } from '@tanstack/ai'
1
2
  import type { GoogleSearch } from '@google/genai'
2
3
  import type { ProviderTool, Tool } from '@tanstack/ai'
3
4
 
@@ -18,10 +19,9 @@ export function convertGoogleSearchToolToAdapterFormat(tool: Tool) {
18
19
  export function googleSearchTool(
19
20
  config?: GoogleSearchToolConfig,
20
21
  ): GeminiGoogleSearchTool {
21
- // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime.
22
- return {
22
+ return brandProviderTool<GeminiGoogleSearchTool>({
23
23
  name: 'google_search',
24
24
  description: '',
25
25
  metadata: config,
26
- } as unknown as GeminiGoogleSearchTool
26
+ })
27
27
  }
@@ -1,3 +1,4 @@
1
+ import { brandProviderTool } from '@tanstack/ai'
1
2
  import type { ProviderTool, Tool } from '@tanstack/ai'
2
3
 
3
4
  export interface UrlContextToolConfig {}
@@ -14,10 +15,9 @@ export function convertUrlContextToolToAdapterFormat(_tool: Tool) {
14
15
  }
15
16
 
16
17
  export function urlContextTool(): GeminiUrlContextTool {
17
- // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime.
18
- return {
18
+ return brandProviderTool<GeminiUrlContextTool>({
19
19
  name: 'url_context',
20
20
  description: '',
21
21
  metadata: {},
22
- } as unknown as GeminiUrlContextTool
22
+ })
23
23
  }