@tanstack/ai-gemini 0.23.0 → 0.24.1

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 (61) hide show
  1. package/README.md +10 -1
  2. package/dist/esm/adapters/image.d.ts +14 -7
  3. package/dist/esm/adapters/image.js +27 -56
  4. package/dist/esm/adapters/image.js.map +1 -1
  5. package/dist/esm/adapters/text.js.map +1 -1
  6. package/dist/esm/adapters/video.js +2 -1
  7. package/dist/esm/adapters/video.js.map +1 -1
  8. package/dist/esm/experimental/text-interactions/adapter.d.ts +8 -5
  9. package/dist/esm/experimental/text-interactions/adapter.js +20 -18
  10. package/dist/esm/experimental/text-interactions/adapter.js.map +1 -1
  11. package/dist/esm/image/image-provider-options.d.ts +167 -20
  12. package/dist/esm/image/image-provider-options.js +52 -5
  13. package/dist/esm/image/image-provider-options.js.map +1 -1
  14. package/dist/esm/index.d.ts +3 -1
  15. package/dist/esm/index.js +3 -1
  16. package/dist/esm/model-meta.d.ts +15 -2
  17. package/dist/esm/model-meta.js +82 -1
  18. package/dist/esm/model-meta.js.map +1 -1
  19. package/dist/esm/realtime/adapter.js +1 -5
  20. package/dist/esm/realtime/adapter.js.map +1 -1
  21. package/dist/esm/realtime/client.js.map +1 -1
  22. package/dist/esm/realtime/token.js +3 -2
  23. package/dist/esm/realtime/token.js.map +1 -1
  24. package/dist/esm/realtime/utils.js +4 -1
  25. package/dist/esm/realtime/utils.js.map +1 -1
  26. package/dist/esm/tools/code-execution-tool.js +3 -3
  27. package/dist/esm/tools/code-execution-tool.js.map +1 -1
  28. package/dist/esm/tools/computer-use-tool.js +4 -4
  29. package/dist/esm/tools/computer-use-tool.js.map +1 -1
  30. package/dist/esm/tools/file-search-tool.js +4 -4
  31. package/dist/esm/tools/file-search-tool.js.map +1 -1
  32. package/dist/esm/tools/gemini-provider-tool.d.ts +16 -0
  33. package/dist/esm/tools/gemini-provider-tool.js +42 -0
  34. package/dist/esm/tools/gemini-provider-tool.js.map +1 -0
  35. package/dist/esm/tools/google-maps-tool.js +4 -4
  36. package/dist/esm/tools/google-maps-tool.js.map +1 -1
  37. package/dist/esm/tools/google-search-retriveal-tool.js +4 -4
  38. package/dist/esm/tools/google-search-retriveal-tool.js.map +1 -1
  39. package/dist/esm/tools/google-search-tool.js +4 -4
  40. package/dist/esm/tools/google-search-tool.js.map +1 -1
  41. package/dist/esm/tools/tool-converter.js +5 -3
  42. package/dist/esm/tools/tool-converter.js.map +1 -1
  43. package/dist/esm/tools/url-context-tool.js +3 -3
  44. package/dist/esm/tools/url-context-tool.js.map +1 -1
  45. package/dist/esm/usage.js +1 -3
  46. package/dist/esm/usage.js.map +1 -1
  47. package/package.json +5 -5
  48. package/src/adapters/image.ts +123 -36
  49. package/src/experimental/text-interactions/adapter.ts +31 -18
  50. package/src/image/image-provider-options.ts +226 -25
  51. package/src/index.ts +28 -0
  52. package/src/model-meta.ts +118 -1
  53. package/src/tools/code-execution-tool.ts +9 -6
  54. package/src/tools/computer-use-tool.ts +19 -13
  55. package/src/tools/file-search-tool.ts +13 -7
  56. package/src/tools/gemini-provider-tool.ts +60 -0
  57. package/src/tools/google-maps-tool.ts +13 -7
  58. package/src/tools/google-search-retriveal-tool.ts +15 -7
  59. package/src/tools/google-search-tool.ts +13 -7
  60. package/src/tools/tool-converter.ts +5 -4
  61. package/src/tools/url-context-tool.ts +9 -6
@@ -1,3 +1,4 @@
1
+ import { getGeminiProviderToolKind } from "./gemini-provider-tool.js";
1
2
  import { convertCodeExecutionToolToAdapterFormat } from "./code-execution-tool.js";
2
3
  import { convertComputerUseToolToAdapterFormat } from "./computer-use-tool.js";
3
4
  import { convertFileSearchToolToAdapterFormat } from "./file-search-tool.js";
@@ -5,6 +6,7 @@ import { convertGoogleMapsToolToAdapterFormat } from "./google-maps-tool.js";
5
6
  import { convertGoogleSearchRetrievalToolToAdapterFormat } from "./google-search-retriveal-tool.js";
6
7
  import { convertGoogleSearchToolToAdapterFormat } from "./google-search-tool.js";
7
8
  import { convertUrlContextToolToAdapterFormat } from "./url-context-tool.js";
9
+ import { assertUniqueToolNames } from "@tanstack/ai/adapter-internals";
8
10
  //#region src/tools/tool-converter.ts
9
11
  /**
10
12
  * Converts standard Tool format to Gemini-specific tool format
@@ -27,9 +29,10 @@ import { convertUrlContextToolToAdapterFormat } from "./url-context-tool.js";
27
29
  */
28
30
  function convertToolsToProviderFormat(tools) {
29
31
  if (!tools || tools.length === 0) return [];
32
+ assertUniqueToolNames(tools);
30
33
  const result = [];
31
34
  const functionDeclarations = [];
32
- for (const tool of tools) switch (tool.name) {
35
+ for (const tool of tools) switch (getGeminiProviderToolKind(tool)) {
33
36
  case "code_execution":
34
37
  result.push(convertCodeExecutionToolToAdapterFormat(tool));
35
38
  break;
@@ -51,7 +54,7 @@ function convertToolsToProviderFormat(tools) {
51
54
  case "url_context":
52
55
  result.push(convertUrlContextToolToAdapterFormat(tool));
53
56
  break;
54
- default:
57
+ case void 0:
55
58
  if (!tool.description) throw new Error(`Tool ${tool.name} requires a description for Gemini adapter`);
56
59
  functionDeclarations.push({
57
60
  name: tool.name,
@@ -62,7 +65,6 @@ function convertToolsToProviderFormat(tools) {
62
65
  required: []
63
66
  }
64
67
  });
65
- break;
66
68
  }
67
69
  if (functionDeclarations.length > 0) result.push({ functionDeclarations });
68
70
  return result;
@@ -1 +1 @@
1
- {"version":3,"file":"tool-converter.js","names":[],"sources":["../../../src/tools/tool-converter.ts"],"sourcesContent":["import { convertCodeExecutionToolToAdapterFormat } from './code-execution-tool'\nimport { convertComputerUseToolToAdapterFormat } from './computer-use-tool'\nimport { convertFileSearchToolToAdapterFormat } from './file-search-tool'\nimport { convertGoogleMapsToolToAdapterFormat } from './google-maps-tool'\nimport { convertGoogleSearchRetrievalToolToAdapterFormat } from './google-search-retriveal-tool'\nimport { convertGoogleSearchToolToAdapterFormat } from './google-search-tool'\nimport { convertUrlContextToolToAdapterFormat } from './url-context-tool'\nimport type { Tool } from '@tanstack/ai'\nimport type { ToolUnion } from '@google/genai'\n\n/**\n * Converts standard Tool format to Gemini-specific tool format\n *\n * @param tools - Array of standard Tool objects\n * @returns Array of Gemini-specific tool definitions\n *\n * @example\n * ```typescript\n * const tools: Tool[] = [{\n * name: \"get_weather\",\n * description: \"Get weather for a location\",\n * inputSchema: z.object({\n * location: z.string()\n * })\n * }];\n *\n * const geminiTools = convertToolsToProviderFormat(tools);\n * ```\n */\nexport function convertToolsToProviderFormat<TTool extends Tool>(\n tools: Array<TTool> | undefined,\n): Array<ToolUnion> {\n if (!tools || tools.length === 0) {\n return []\n }\n const result: Array<ToolUnion> = []\n const functionDeclarations: Array<{\n name: string\n description?: string\n parameters?: any\n }> = []\n\n // Process each tool and group function declarations together\n for (const tool of tools) {\n const name = tool.name\n\n switch (name) {\n case 'code_execution':\n result.push(convertCodeExecutionToolToAdapterFormat(tool))\n break\n case 'computer_use':\n result.push(convertComputerUseToolToAdapterFormat(tool))\n break\n case 'file_search':\n result.push(convertFileSearchToolToAdapterFormat(tool))\n break\n case 'google_maps':\n result.push(convertGoogleMapsToolToAdapterFormat(tool))\n break\n case 'google_search_retrieval':\n result.push(convertGoogleSearchRetrievalToolToAdapterFormat(tool))\n break\n case 'google_search':\n result.push(convertGoogleSearchToolToAdapterFormat(tool))\n break\n case 'url_context':\n result.push(convertUrlContextToolToAdapterFormat(tool))\n break\n default:\n // Collect function declarations to group together\n // Description is required for Gemini function declarations\n if (!tool.description) {\n throw new Error(\n `Tool ${tool.name} requires a description for Gemini adapter`,\n )\n }\n\n // Tool schemas are already converted to JSON Schema in the ai layer\n functionDeclarations.push({\n name: tool.name,\n description: tool.description,\n parameters: tool.inputSchema ?? {\n type: 'object',\n properties: {},\n required: [],\n },\n })\n break\n }\n }\n\n // If we have function declarations, add them as a single tool\n if (functionDeclarations.length > 0) {\n result.push({\n functionDeclarations: functionDeclarations,\n })\n }\n\n return result\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,SAAgB,6BACd,OACkB;CAClB,IAAI,CAAC,SAAS,MAAM,WAAW,GAC7B,OAAO,CAAC;CAEV,MAAM,SAA2B,CAAC;CAClC,MAAM,uBAID,CAAC;CAGN,KAAK,MAAM,QAAQ,OAGjB,QAFa,KAAK,MAElB;EACE,KAAK;GACH,OAAO,KAAK,wCAAwC,IAAI,CAAC;GACzD;EACF,KAAK;GACH,OAAO,KAAK,sCAAsC,IAAI,CAAC;GACvD;EACF,KAAK;GACH,OAAO,KAAK,qCAAqC,IAAI,CAAC;GACtD;EACF,KAAK;GACH,OAAO,KAAK,qCAAqC,IAAI,CAAC;GACtD;EACF,KAAK;GACH,OAAO,KAAK,gDAAgD,IAAI,CAAC;GACjE;EACF,KAAK;GACH,OAAO,KAAK,uCAAuC,IAAI,CAAC;GACxD;EACF,KAAK;GACH,OAAO,KAAK,qCAAqC,IAAI,CAAC;GACtD;EACF;GAGE,IAAI,CAAC,KAAK,aACR,MAAM,IAAI,MACR,QAAQ,KAAK,KAAK,2CACpB;GAIF,qBAAqB,KAAK;IACxB,MAAM,KAAK;IACX,aAAa,KAAK;IAClB,YAAY,KAAK,eAAe;KAC9B,MAAM;KACN,YAAY,CAAC;KACb,UAAU,CAAC;IACb;GACF,CAAC;GACD;CACJ;CAIF,IAAI,qBAAqB,SAAS,GAChC,OAAO,KAAK,EACY,qBACxB,CAAC;CAGH,OAAO;AACT"}
1
+ {"version":3,"file":"tool-converter.js","names":[],"sources":["../../../src/tools/tool-converter.ts"],"sourcesContent":["import { assertUniqueToolNames } from '@tanstack/ai/adapter-internals'\nimport { convertCodeExecutionToolToAdapterFormat } from './code-execution-tool'\nimport { convertComputerUseToolToAdapterFormat } from './computer-use-tool'\nimport { convertFileSearchToolToAdapterFormat } from './file-search-tool'\nimport { getGeminiProviderToolKind } from './gemini-provider-tool'\nimport { convertGoogleMapsToolToAdapterFormat } from './google-maps-tool'\nimport { convertGoogleSearchRetrievalToolToAdapterFormat } from './google-search-retriveal-tool'\nimport { convertGoogleSearchToolToAdapterFormat } from './google-search-tool'\nimport { convertUrlContextToolToAdapterFormat } from './url-context-tool'\nimport type { Tool } from '@tanstack/ai'\nimport type { ToolUnion } from '@google/genai'\n\n/**\n * Converts standard Tool format to Gemini-specific tool format\n *\n * @param tools - Array of standard Tool objects\n * @returns Array of Gemini-specific tool definitions\n *\n * @example\n * ```typescript\n * const tools: Tool[] = [{\n * name: \"get_weather\",\n * description: \"Get weather for a location\",\n * inputSchema: z.object({\n * location: z.string()\n * })\n * }];\n *\n * const geminiTools = convertToolsToProviderFormat(tools);\n * ```\n */\nexport function convertToolsToProviderFormat<TTool extends Tool>(\n tools: Array<TTool> | undefined,\n): Array<ToolUnion> {\n if (!tools || tools.length === 0) {\n return []\n }\n assertUniqueToolNames(tools)\n const result: Array<ToolUnion> = []\n const functionDeclarations: Array<{\n name: string\n description?: string\n parameters?: any\n }> = []\n\n // Process each tool and group function declarations together\n for (const tool of tools) {\n switch (getGeminiProviderToolKind(tool)) {\n case 'code_execution':\n result.push(convertCodeExecutionToolToAdapterFormat(tool))\n break\n case 'computer_use':\n result.push(convertComputerUseToolToAdapterFormat(tool))\n break\n case 'file_search':\n result.push(convertFileSearchToolToAdapterFormat(tool))\n break\n case 'google_maps':\n result.push(convertGoogleMapsToolToAdapterFormat(tool))\n break\n case 'google_search_retrieval':\n result.push(convertGoogleSearchRetrievalToolToAdapterFormat(tool))\n break\n case 'google_search':\n result.push(convertGoogleSearchToolToAdapterFormat(tool))\n break\n case 'url_context':\n result.push(convertUrlContextToolToAdapterFormat(tool))\n break\n case undefined:\n // Collect function declarations to group together\n // Description is required for Gemini function declarations\n if (!tool.description) {\n throw new Error(\n `Tool ${tool.name} requires a description for Gemini adapter`,\n )\n }\n\n // Tool schemas are already converted to JSON Schema in the ai layer\n functionDeclarations.push({\n name: tool.name,\n description: tool.description,\n parameters: tool.inputSchema ?? {\n type: 'object',\n properties: {},\n required: [],\n },\n })\n break\n }\n }\n\n // If we have function declarations, add them as a single tool\n if (functionDeclarations.length > 0) {\n result.push({\n functionDeclarations: functionDeclarations,\n })\n }\n\n return result\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,SAAgB,6BACd,OACkB;CAClB,IAAI,CAAC,SAAS,MAAM,WAAW,GAC7B,OAAO,CAAC;CAEV,sBAAsB,KAAK;CAC3B,MAAM,SAA2B,CAAC;CAClC,MAAM,uBAID,CAAC;CAGN,KAAK,MAAM,QAAQ,OACjB,QAAQ,0BAA0B,IAAI,GAAtC;EACE,KAAK;GACH,OAAO,KAAK,wCAAwC,IAAI,CAAC;GACzD;EACF,KAAK;GACH,OAAO,KAAK,sCAAsC,IAAI,CAAC;GACvD;EACF,KAAK;GACH,OAAO,KAAK,qCAAqC,IAAI,CAAC;GACtD;EACF,KAAK;GACH,OAAO,KAAK,qCAAqC,IAAI,CAAC;GACtD;EACF,KAAK;GACH,OAAO,KAAK,gDAAgD,IAAI,CAAC;GACjE;EACF,KAAK;GACH,OAAO,KAAK,uCAAuC,IAAI,CAAC;GACxD;EACF,KAAK;GACH,OAAO,KAAK,qCAAqC,IAAI,CAAC;GACtD;EACF,KAAK,KAAA;GAGH,IAAI,CAAC,KAAK,aACR,MAAM,IAAI,MACR,QAAQ,KAAK,KAAK,2CACpB;GAIF,qBAAqB,KAAK;IACxB,MAAM,KAAK;IACX,aAAa,KAAK;IAClB,YAAY,KAAK,eAAe;KAC9B,MAAM;KACN,YAAY,CAAC;KACb,UAAU,CAAC;IACb;GACF,CAAC;CAEL;CAIF,IAAI,qBAAqB,SAAS,GAChC,OAAO,KAAK,EACY,qBACxB,CAAC;CAGH,OAAO;AACT"}
@@ -1,14 +1,14 @@
1
- import { brandProviderTool } from "@tanstack/ai";
1
+ import { brandGeminiProviderTool } from "./gemini-provider-tool.js";
2
2
  //#region src/tools/url-context-tool.ts
3
3
  function convertUrlContextToolToAdapterFormat(_tool) {
4
4
  return { urlContext: {} };
5
5
  }
6
6
  function urlContextTool() {
7
- return brandProviderTool({
7
+ return brandGeminiProviderTool({
8
8
  name: "url_context",
9
9
  description: "",
10
10
  metadata: {}
11
- });
11
+ }, "url_context");
12
12
  }
13
13
  //#endregion
14
14
  export { convertUrlContextToolToAdapterFormat, urlContextTool };
@@ -1 +1 @@
1
- {"version":3,"file":"url-context-tool.js","names":[],"sources":["../../../src/tools/url-context-tool.ts"],"sourcesContent":["import { brandProviderTool } from '@tanstack/ai'\nimport type { ProviderTool, Tool } from '@tanstack/ai'\n\nexport interface UrlContextToolConfig {}\n\n/** @deprecated Renamed to `UrlContextToolConfig`. Will be removed in a future release. */\nexport type UrlContextTool = UrlContextToolConfig\n\nexport type GeminiUrlContextTool = ProviderTool<'gemini', 'url_context'>\n\nexport function convertUrlContextToolToAdapterFormat(_tool: Tool) {\n return {\n urlContext: {},\n }\n}\n\nexport function urlContextTool(): GeminiUrlContextTool {\n return brandProviderTool<GeminiUrlContextTool>({\n name: 'url_context',\n description: '',\n metadata: {},\n })\n}\n"],"mappings":";;AAUA,SAAgB,qCAAqC,OAAa;CAChE,OAAO,EACL,YAAY,CAAC,EACf;AACF;AAEA,SAAgB,iBAAuC;CACrD,OAAO,kBAAwC;EAC7C,MAAM;EACN,aAAa;EACb,UAAU,CAAC;CACb,CAAC;AACH"}
1
+ {"version":3,"file":"url-context-tool.js","names":[],"sources":["../../../src/tools/url-context-tool.ts"],"sourcesContent":["import { brandGeminiProviderTool } from './gemini-provider-tool'\nimport type { ProviderTool, Tool } from '@tanstack/ai'\n\nexport interface UrlContextToolConfig {}\n\n/** @deprecated Renamed to `UrlContextToolConfig`. Will be removed in a future release. */\nexport type UrlContextTool = UrlContextToolConfig\n\nexport type GeminiUrlContextTool = ProviderTool<'gemini', 'url_context'>\n\nexport function convertUrlContextToolToAdapterFormat(_tool: Tool) {\n return {\n urlContext: {},\n }\n}\n\nexport function urlContextTool(): GeminiUrlContextTool {\n return brandGeminiProviderTool<GeminiUrlContextTool>(\n {\n name: 'url_context',\n description: '',\n metadata: {},\n },\n 'url_context',\n )\n}\n"],"mappings":";;AAUA,SAAgB,qCAAqC,OAAa;CAChE,OAAO,EACL,YAAY,CAAC,EACf;AACF;AAEA,SAAgB,iBAAuC;CACrD,OAAO,wBACL;EACE,MAAM;EACN,aAAa;EACb,UAAU,CAAC;CACb,GACA,aACF;AACF"}
package/dist/esm/usage.js CHANGED
@@ -25,9 +25,7 @@ function flattenModalityTokenCounts(modalities) {
25
25
  case "VIDEO":
26
26
  result.videoTokens = (result.videoTokens ?? 0) + count;
27
27
  break;
28
- case "DOCUMENT":
29
- result.documentTokens = (result.documentTokens ?? 0) + count;
30
- break;
28
+ case "DOCUMENT": result.documentTokens = (result.documentTokens ?? 0) + count;
31
29
  }
32
30
  }
33
31
  return result;
@@ -1 +1 @@
1
- {"version":3,"file":"usage.js","names":[],"sources":["../../src/usage.ts"],"sourcesContent":["import { buildBaseUsage } from '@tanstack/ai'\nimport type { TokenUsage } from '@tanstack/ai'\nimport type {\n GenerateContentResponseUsageMetadata,\n ModalityTokenCount,\n} from '@google/genai'\n\n/**\n * Flattened modality token counts for normalized usage reporting.\n * Maps Gemini's ModalityTokenCount array to individual fields.\n */\nexport interface FlattenedModalityTokens {\n /** Text tokens */\n textTokens?: number\n /** Image tokens */\n imageTokens?: number\n /** Audio tokens */\n audioTokens?: number\n /** Video tokens */\n videoTokens?: number\n /** Document tokens (e.g. PDF inputs) */\n documentTokens?: number\n}\n\n/**\n * Flattens Gemini's ModalityTokenCount array into individual token fields.\n * Extracts TEXT, IMAGE, AUDIO, VIDEO, DOCUMENT modality counts into a\n * normalized structure.\n */\nexport function flattenModalityTokenCounts(\n modalities?: Array<ModalityTokenCount>,\n): FlattenedModalityTokens {\n if (!modalities || modalities.length === 0) {\n return {}\n }\n\n const result: FlattenedModalityTokens = {}\n\n for (const item of modalities) {\n if (!item.modality || item.tokenCount === undefined) {\n continue\n }\n\n const modality = item.modality.toUpperCase()\n const count = item.tokenCount\n\n switch (modality) {\n case 'TEXT':\n result.textTokens = (result.textTokens ?? 0) + count\n break\n case 'IMAGE':\n result.imageTokens = (result.imageTokens ?? 0) + count\n break\n case 'AUDIO':\n result.audioTokens = (result.audioTokens ?? 0) + count\n break\n case 'VIDEO':\n result.videoTokens = (result.videoTokens ?? 0) + count\n break\n case 'DOCUMENT':\n result.documentTokens = (result.documentTokens ?? 0) + count\n break\n }\n }\n\n return result\n}\n\n/**\n * Checks if a FlattenedModalityTokens object has any values set.\n */\nexport function hasModalityTokens(tokens: FlattenedModalityTokens): boolean {\n return (\n tokens.textTokens !== undefined ||\n tokens.imageTokens !== undefined ||\n tokens.audioTokens !== undefined ||\n tokens.videoTokens !== undefined ||\n tokens.documentTokens !== undefined\n )\n}\n\n/**\n * Gemini-specific provider usage details.\n * These fields are unique to Gemini and placed in providerUsageDetails.\n */\nexport type GeminiProviderUsageDetails = {\n /**\n * The traffic type for this request.\n * Can indicate whether request was handled by different service tiers.\n */\n trafficType?: string\n /**\n * Number of tokens in the results from tool executions,\n * which are provided back to the model as input.\n */\n toolUsePromptTokenCount?: number\n /**\n * Detailed breakdown by modality of the token counts from\n * the results of tool executions.\n */\n toolUsePromptTokensDetails?: Array<{\n modality: string\n tokenCount: number\n }>\n /**\n * Detailed breakdown of cache tokens by modality.\n * More granular than the normalized cachedTokens field.\n */\n cacheTokensDetails?: Array<{\n modality: string\n tokenCount: number\n }>\n}\n\n/**\n * Build normalized TokenUsage from Gemini's usageMetadata.\n * Handles modality breakdowns and thinking tokens. Returns `undefined` when the\n * provider reported no usage metadata, so callers omit the field rather than\n * fabricating zeroed totals.\n */\nexport function buildGeminiUsage(\n usageMetadata: GenerateContentResponseUsageMetadata | undefined | null,\n): TokenUsage<GeminiProviderUsageDetails> | undefined {\n if (!usageMetadata) return undefined\n\n const promptTokens = usageMetadata.promptTokenCount ?? 0\n const completionTokens = usageMetadata.candidatesTokenCount ?? 0\n\n const result = buildBaseUsage<GeminiProviderUsageDetails>({\n promptTokens: promptTokens,\n completionTokens: completionTokens,\n totalTokens:\n usageMetadata.totalTokenCount ?? promptTokens + completionTokens,\n })\n\n // Add prompt token details\n // Flatten modality breakdown for prompt\n const promptModalities = flattenModalityTokenCounts(\n usageMetadata.promptTokensDetails,\n )\n const cachedTokens = usageMetadata.cachedContentTokenCount\n\n const promptTokensDetails = {\n ...(hasModalityTokens(promptModalities) ? promptModalities : {}),\n ...(cachedTokens !== undefined && cachedTokens > 0 ? { cachedTokens } : {}),\n }\n\n // Add completion token details\n // Flatten modality breakdown for candidates (output)\n const completionModalities = flattenModalityTokenCounts(\n usageMetadata.candidatesTokensDetails,\n )\n const thoughtsTokens = usageMetadata.thoughtsTokenCount\n\n const completionTokensDetails = {\n ...(hasModalityTokens(completionModalities) ? completionModalities : {}),\n // Map thoughtsTokenCount to reasoningTokens for consistency with OpenAI\n ...(thoughtsTokens !== undefined && thoughtsTokens > 0\n ? { reasoningTokens: thoughtsTokens }\n : {}),\n }\n\n // Add provider-specific details\n const providerDetails: GeminiProviderUsageDetails = {\n ...(usageMetadata.trafficType\n ? { trafficType: usageMetadata.trafficType }\n : {}),\n ...(usageMetadata.toolUsePromptTokenCount !== undefined &&\n usageMetadata.toolUsePromptTokenCount > 0\n ? { toolUsePromptTokenCount: usageMetadata.toolUsePromptTokenCount }\n : {}),\n ...(usageMetadata.toolUsePromptTokensDetails &&\n usageMetadata.toolUsePromptTokensDetails.length > 0\n ? {\n toolUsePromptTokensDetails:\n usageMetadata.toolUsePromptTokensDetails.map((item) => ({\n modality: item.modality || 'UNKNOWN',\n tokenCount: item.tokenCount ?? 0,\n })),\n }\n : {}),\n ...(usageMetadata.cacheTokensDetails &&\n usageMetadata.cacheTokensDetails.length > 0\n ? {\n cacheTokensDetails: usageMetadata.cacheTokensDetails.map((item) => ({\n modality: item.modality || 'UNKNOWN',\n tokenCount: item.tokenCount ?? 0,\n })),\n }\n : {}),\n }\n\n // Add prompt token details if available\n if (Object.keys(promptTokensDetails).length > 0) {\n result.promptTokensDetails = promptTokensDetails\n }\n // Add provider details if available\n if (Object.keys(providerDetails).length > 0) {\n result.providerUsageDetails = providerDetails\n }\n // Add completion token details if available\n if (Object.keys(completionTokensDetails).length > 0) {\n result.completionTokensDetails = completionTokensDetails\n }\n\n return result\n}\n"],"mappings":";;;;;;;AA6BA,SAAgB,2BACd,YACyB;CACzB,IAAI,CAAC,cAAc,WAAW,WAAW,GACvC,OAAO,CAAC;CAGV,MAAM,SAAkC,CAAC;CAEzC,KAAK,MAAM,QAAQ,YAAY;EAC7B,IAAI,CAAC,KAAK,YAAY,KAAK,eAAe,KAAA,GACxC;EAGF,MAAM,WAAW,KAAK,SAAS,YAAY;EAC3C,MAAM,QAAQ,KAAK;EAEnB,QAAQ,UAAR;GACE,KAAK;IACH,OAAO,cAAc,OAAO,cAAc,KAAK;IAC/C;GACF,KAAK;IACH,OAAO,eAAe,OAAO,eAAe,KAAK;IACjD;GACF,KAAK;IACH,OAAO,eAAe,OAAO,eAAe,KAAK;IACjD;GACF,KAAK;IACH,OAAO,eAAe,OAAO,eAAe,KAAK;IACjD;GACF,KAAK;IACH,OAAO,kBAAkB,OAAO,kBAAkB,KAAK;IACvD;EACJ;CACF;CAEA,OAAO;AACT;;;;AAKA,SAAgB,kBAAkB,QAA0C;CAC1E,OACE,OAAO,eAAe,KAAA,KACtB,OAAO,gBAAgB,KAAA,KACvB,OAAO,gBAAgB,KAAA,KACvB,OAAO,gBAAgB,KAAA,KACvB,OAAO,mBAAmB,KAAA;AAE9B;;;;;;;AAyCA,SAAgB,iBACd,eACoD;CACpD,IAAI,CAAC,eAAe,OAAO,KAAA;CAE3B,MAAM,eAAe,cAAc,oBAAoB;CACvD,MAAM,mBAAmB,cAAc,wBAAwB;CAE/D,MAAM,SAAS,eAA2C;EAC1C;EACI;EAClB,aACE,cAAc,mBAAmB,eAAe;CACpD,CAAC;CAID,MAAM,mBAAmB,2BACvB,cAAc,mBAChB;CACA,MAAM,eAAe,cAAc;CAEnC,MAAM,sBAAsB;EAC1B,GAAI,kBAAkB,gBAAgB,IAAI,mBAAmB,CAAC;EAC9D,GAAI,iBAAiB,KAAA,KAAa,eAAe,IAAI,EAAE,aAAa,IAAI,CAAC;CAC3E;CAIA,MAAM,uBAAuB,2BAC3B,cAAc,uBAChB;CACA,MAAM,iBAAiB,cAAc;CAErC,MAAM,0BAA0B;EAC9B,GAAI,kBAAkB,oBAAoB,IAAI,uBAAuB,CAAC;EAEtE,GAAI,mBAAmB,KAAA,KAAa,iBAAiB,IACjD,EAAE,iBAAiB,eAAe,IAClC,CAAC;CACP;CAGA,MAAM,kBAA8C;EAClD,GAAI,cAAc,cACd,EAAE,aAAa,cAAc,YAAY,IACzC,CAAC;EACL,GAAI,cAAc,4BAA4B,KAAA,KAC9C,cAAc,0BAA0B,IACpC,EAAE,yBAAyB,cAAc,wBAAwB,IACjE,CAAC;EACL,GAAI,cAAc,8BAClB,cAAc,2BAA2B,SAAS,IAC9C,EACE,4BACE,cAAc,2BAA2B,KAAK,UAAU;GACtD,UAAU,KAAK,YAAY;GAC3B,YAAY,KAAK,cAAc;EACjC,EAAE,EACN,IACA,CAAC;EACL,GAAI,cAAc,sBAClB,cAAc,mBAAmB,SAAS,IACtC,EACE,oBAAoB,cAAc,mBAAmB,KAAK,UAAU;GAClE,UAAU,KAAK,YAAY;GAC3B,YAAY,KAAK,cAAc;EACjC,EAAE,EACJ,IACA,CAAC;CACP;CAGA,IAAI,OAAO,KAAK,mBAAmB,CAAC,CAAC,SAAS,GAC5C,OAAO,sBAAsB;CAG/B,IAAI,OAAO,KAAK,eAAe,CAAC,CAAC,SAAS,GACxC,OAAO,uBAAuB;CAGhC,IAAI,OAAO,KAAK,uBAAuB,CAAC,CAAC,SAAS,GAChD,OAAO,0BAA0B;CAGnC,OAAO;AACT"}
1
+ {"version":3,"file":"usage.js","names":[],"sources":["../../src/usage.ts"],"sourcesContent":["import { buildBaseUsage } from '@tanstack/ai'\nimport type { TokenUsage } from '@tanstack/ai'\nimport type {\n GenerateContentResponseUsageMetadata,\n ModalityTokenCount,\n} from '@google/genai'\n\n/**\n * Flattened modality token counts for normalized usage reporting.\n * Maps Gemini's ModalityTokenCount array to individual fields.\n */\nexport interface FlattenedModalityTokens {\n /** Text tokens */\n textTokens?: number\n /** Image tokens */\n imageTokens?: number\n /** Audio tokens */\n audioTokens?: number\n /** Video tokens */\n videoTokens?: number\n /** Document tokens (e.g. PDF inputs) */\n documentTokens?: number\n}\n\n/**\n * Flattens Gemini's ModalityTokenCount array into individual token fields.\n * Extracts TEXT, IMAGE, AUDIO, VIDEO, DOCUMENT modality counts into a\n * normalized structure.\n */\nexport function flattenModalityTokenCounts(\n modalities?: Array<ModalityTokenCount>,\n): FlattenedModalityTokens {\n if (!modalities || modalities.length === 0) {\n return {}\n }\n\n const result: FlattenedModalityTokens = {}\n\n for (const item of modalities) {\n if (!item.modality || item.tokenCount === undefined) {\n continue\n }\n\n const modality = item.modality.toUpperCase()\n const count = item.tokenCount\n\n switch (modality) {\n case 'TEXT':\n result.textTokens = (result.textTokens ?? 0) + count\n break\n case 'IMAGE':\n result.imageTokens = (result.imageTokens ?? 0) + count\n break\n case 'AUDIO':\n result.audioTokens = (result.audioTokens ?? 0) + count\n break\n case 'VIDEO':\n result.videoTokens = (result.videoTokens ?? 0) + count\n break\n case 'DOCUMENT':\n result.documentTokens = (result.documentTokens ?? 0) + count\n break\n }\n }\n\n return result\n}\n\n/**\n * Checks if a FlattenedModalityTokens object has any values set.\n */\nexport function hasModalityTokens(tokens: FlattenedModalityTokens): boolean {\n return (\n tokens.textTokens !== undefined ||\n tokens.imageTokens !== undefined ||\n tokens.audioTokens !== undefined ||\n tokens.videoTokens !== undefined ||\n tokens.documentTokens !== undefined\n )\n}\n\n/**\n * Gemini-specific provider usage details.\n * These fields are unique to Gemini and placed in providerUsageDetails.\n */\nexport type GeminiProviderUsageDetails = {\n /**\n * The traffic type for this request.\n * Can indicate whether request was handled by different service tiers.\n */\n trafficType?: string\n /**\n * Number of tokens in the results from tool executions,\n * which are provided back to the model as input.\n */\n toolUsePromptTokenCount?: number\n /**\n * Detailed breakdown by modality of the token counts from\n * the results of tool executions.\n */\n toolUsePromptTokensDetails?: Array<{\n modality: string\n tokenCount: number\n }>\n /**\n * Detailed breakdown of cache tokens by modality.\n * More granular than the normalized cachedTokens field.\n */\n cacheTokensDetails?: Array<{\n modality: string\n tokenCount: number\n }>\n}\n\n/**\n * Build normalized TokenUsage from Gemini's usageMetadata.\n * Handles modality breakdowns and thinking tokens. Returns `undefined` when the\n * provider reported no usage metadata, so callers omit the field rather than\n * fabricating zeroed totals.\n */\nexport function buildGeminiUsage(\n usageMetadata: GenerateContentResponseUsageMetadata | undefined | null,\n): TokenUsage<GeminiProviderUsageDetails> | undefined {\n if (!usageMetadata) return undefined\n\n const promptTokens = usageMetadata.promptTokenCount ?? 0\n const completionTokens = usageMetadata.candidatesTokenCount ?? 0\n\n const result = buildBaseUsage<GeminiProviderUsageDetails>({\n promptTokens: promptTokens,\n completionTokens: completionTokens,\n totalTokens:\n usageMetadata.totalTokenCount ?? promptTokens + completionTokens,\n })\n\n // Add prompt token details\n // Flatten modality breakdown for prompt\n const promptModalities = flattenModalityTokenCounts(\n usageMetadata.promptTokensDetails,\n )\n const cachedTokens = usageMetadata.cachedContentTokenCount\n\n const promptTokensDetails = {\n ...(hasModalityTokens(promptModalities) ? promptModalities : {}),\n ...(cachedTokens !== undefined && cachedTokens > 0 ? { cachedTokens } : {}),\n }\n\n // Add completion token details\n // Flatten modality breakdown for candidates (output)\n const completionModalities = flattenModalityTokenCounts(\n usageMetadata.candidatesTokensDetails,\n )\n const thoughtsTokens = usageMetadata.thoughtsTokenCount\n\n const completionTokensDetails = {\n ...(hasModalityTokens(completionModalities) ? completionModalities : {}),\n // Map thoughtsTokenCount to reasoningTokens for consistency with OpenAI\n ...(thoughtsTokens !== undefined && thoughtsTokens > 0\n ? { reasoningTokens: thoughtsTokens }\n : {}),\n }\n\n // Add provider-specific details\n const providerDetails: GeminiProviderUsageDetails = {\n ...(usageMetadata.trafficType\n ? { trafficType: usageMetadata.trafficType }\n : {}),\n ...(usageMetadata.toolUsePromptTokenCount !== undefined &&\n usageMetadata.toolUsePromptTokenCount > 0\n ? { toolUsePromptTokenCount: usageMetadata.toolUsePromptTokenCount }\n : {}),\n ...(usageMetadata.toolUsePromptTokensDetails &&\n usageMetadata.toolUsePromptTokensDetails.length > 0\n ? {\n toolUsePromptTokensDetails:\n usageMetadata.toolUsePromptTokensDetails.map((item) => ({\n modality: item.modality || 'UNKNOWN',\n tokenCount: item.tokenCount ?? 0,\n })),\n }\n : {}),\n ...(usageMetadata.cacheTokensDetails &&\n usageMetadata.cacheTokensDetails.length > 0\n ? {\n cacheTokensDetails: usageMetadata.cacheTokensDetails.map((item) => ({\n modality: item.modality || 'UNKNOWN',\n tokenCount: item.tokenCount ?? 0,\n })),\n }\n : {}),\n }\n\n // Add prompt token details if available\n if (Object.keys(promptTokensDetails).length > 0) {\n result.promptTokensDetails = promptTokensDetails\n }\n // Add provider details if available\n if (Object.keys(providerDetails).length > 0) {\n result.providerUsageDetails = providerDetails\n }\n // Add completion token details if available\n if (Object.keys(completionTokensDetails).length > 0) {\n result.completionTokensDetails = completionTokensDetails\n }\n\n return result\n}\n"],"mappings":";;;;;;;AA6BA,SAAgB,2BACd,YACyB;CACzB,IAAI,CAAC,cAAc,WAAW,WAAW,GACvC,OAAO,CAAC;CAGV,MAAM,SAAkC,CAAC;CAEzC,KAAK,MAAM,QAAQ,YAAY;EAC7B,IAAI,CAAC,KAAK,YAAY,KAAK,eAAe,KAAA,GACxC;EAGF,MAAM,WAAW,KAAK,SAAS,YAAY;EAC3C,MAAM,QAAQ,KAAK;EAEnB,QAAQ,UAAR;GACE,KAAK;IACH,OAAO,cAAc,OAAO,cAAc,KAAK;IAC/C;GACF,KAAK;IACH,OAAO,eAAe,OAAO,eAAe,KAAK;IACjD;GACF,KAAK;IACH,OAAO,eAAe,OAAO,eAAe,KAAK;IACjD;GACF,KAAK;IACH,OAAO,eAAe,OAAO,eAAe,KAAK;IACjD;GACF,KAAK,YACH,OAAO,kBAAkB,OAAO,kBAAkB,KAAK;EAE3D;CACF;CAEA,OAAO;AACT;;;;AAKA,SAAgB,kBAAkB,QAA0C;CAC1E,OACE,OAAO,eAAe,KAAA,KACtB,OAAO,gBAAgB,KAAA,KACvB,OAAO,gBAAgB,KAAA,KACvB,OAAO,gBAAgB,KAAA,KACvB,OAAO,mBAAmB,KAAA;AAE9B;;;;;;;AAyCA,SAAgB,iBACd,eACoD;CACpD,IAAI,CAAC,eAAe,OAAO,KAAA;CAE3B,MAAM,eAAe,cAAc,oBAAoB;CACvD,MAAM,mBAAmB,cAAc,wBAAwB;CAE/D,MAAM,SAAS,eAA2C;EAC1C;EACI;EAClB,aACE,cAAc,mBAAmB,eAAe;CACpD,CAAC;CAID,MAAM,mBAAmB,2BACvB,cAAc,mBAChB;CACA,MAAM,eAAe,cAAc;CAEnC,MAAM,sBAAsB;EAC1B,GAAI,kBAAkB,gBAAgB,IAAI,mBAAmB,CAAC;EAC9D,GAAI,iBAAiB,KAAA,KAAa,eAAe,IAAI,EAAE,aAAa,IAAI,CAAC;CAC3E;CAIA,MAAM,uBAAuB,2BAC3B,cAAc,uBAChB;CACA,MAAM,iBAAiB,cAAc;CAErC,MAAM,0BAA0B;EAC9B,GAAI,kBAAkB,oBAAoB,IAAI,uBAAuB,CAAC;EAEtE,GAAI,mBAAmB,KAAA,KAAa,iBAAiB,IACjD,EAAE,iBAAiB,eAAe,IAClC,CAAC;CACP;CAGA,MAAM,kBAA8C;EAClD,GAAI,cAAc,cACd,EAAE,aAAa,cAAc,YAAY,IACzC,CAAC;EACL,GAAI,cAAc,4BAA4B,KAAA,KAC9C,cAAc,0BAA0B,IACpC,EAAE,yBAAyB,cAAc,wBAAwB,IACjE,CAAC;EACL,GAAI,cAAc,8BAClB,cAAc,2BAA2B,SAAS,IAC9C,EACE,4BACE,cAAc,2BAA2B,KAAK,UAAU;GACtD,UAAU,KAAK,YAAY;GAC3B,YAAY,KAAK,cAAc;EACjC,EAAE,EACN,IACA,CAAC;EACL,GAAI,cAAc,sBAClB,cAAc,mBAAmB,SAAS,IACtC,EACE,oBAAoB,cAAc,mBAAmB,KAAK,UAAU;GAClE,UAAU,KAAK,YAAY;GAC3B,YAAY,KAAK,cAAc;EACjC,EAAE,EACJ,IACA,CAAC;CACP;CAGA,IAAI,OAAO,KAAK,mBAAmB,CAAC,CAAC,SAAS,GAC5C,OAAO,sBAAsB;CAG/B,IAAI,OAAO,KAAK,eAAe,CAAC,CAAC,SAAS,GACxC,OAAO,uBAAuB;CAGhC,IAAI,OAAO,KAAK,uBAAuB,CAAC,CAAC,SAAS,GAChD,OAAO,0BAA0B;CAGnC,OAAO;AACT"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai-gemini",
3
- "version": "0.23.0",
3
+ "version": "0.24.1",
4
4
  "description": "Google Gemini adapter for TanStack AI chat, images, speech, audio generation, and structured outputs.",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -59,13 +59,13 @@
59
59
  "@tanstack/ai-utils": "^0.4.0"
60
60
  },
61
61
  "peerDependencies": {
62
- "@tanstack/ai": "^0.44.1"
62
+ "@tanstack/ai": "^0.46.0"
63
63
  },
64
64
  "devDependencies": {
65
- "@vitest/coverage-v8": "4.0.14",
66
- "vite": "^8.1.4",
65
+ "@vitest/coverage-v8": "4.1.10",
66
+ "vite": "^8.2.1",
67
67
  "zod": "^4.2.0",
68
- "@tanstack/ai": "0.44.1"
68
+ "@tanstack/ai": "0.46.0"
69
69
  },
70
70
  "scripts": {
71
71
  "build": "vite build",
@@ -7,18 +7,20 @@ import {
7
7
  } from '../utils'
8
8
  import { buildGeminiUsage } from '../usage'
9
9
  import {
10
+ isGeminiNativeImageModel,
10
11
  parseNativeImageSize,
11
12
  sizeToAspectRatio,
12
13
  validateImageSize,
13
14
  validateNumberOfImages,
14
15
  validatePrompt,
15
16
  } from '../image/image-provider-options'
16
- import type { GEMINI_IMAGE_MODELS } from '../model-meta'
17
+ import type { GeminiImageModels } from '../model-meta'
17
18
  import type {
19
+ GeminiAnyImageProviderOptions,
18
20
  GeminiImageModelInputModalitiesByName,
19
21
  GeminiImageModelProviderOptionsByName,
20
22
  GeminiImageModelSizeByName,
21
- GeminiImageProviderOptions,
23
+ GeminiNativeImageProviderOptions,
22
24
  } from '../image/image-provider-options'
23
25
  import type {
24
26
  GeneratedImage,
@@ -35,6 +37,7 @@ import type {
35
37
  GenerateImagesConfig,
36
38
  GenerateImagesResponse,
37
39
  GoogleGenAI,
40
+ ImageConfig,
38
41
  Part,
39
42
  } from '@google/genai'
40
43
  import type { GeminiClientConfig } from '../utils/client'
@@ -45,7 +48,7 @@ import type { GeminiClientConfig } from '../utils/client'
45
48
  export interface GeminiImageConfig extends GeminiClientConfig {}
46
49
 
47
50
  /** Model type for Gemini Image */
48
- export type GeminiImageModel = (typeof GEMINI_IMAGE_MODELS)[number]
51
+ export type GeminiImageModel = GeminiImageModels
49
52
 
50
53
  /**
51
54
  * Gemini Image Generation Adapter
@@ -65,7 +68,7 @@ export class GeminiImageAdapter<
65
68
  TModel extends GeminiImageModel,
66
69
  > extends BaseImageAdapter<
67
70
  TModel,
68
- GeminiImageProviderOptions,
71
+ GeminiAnyImageProviderOptions,
69
72
  GeminiImageModelProviderOptionsByName,
70
73
  GeminiImageModelSizeByName,
71
74
  GeminiImageModelInputModalitiesByName
@@ -75,7 +78,7 @@ export class GeminiImageAdapter<
75
78
 
76
79
  // Type-only property - never assigned at runtime
77
80
  declare '~types': {
78
- providerOptions: GeminiImageProviderOptions
81
+ providerOptions: GeminiAnyImageProviderOptions
79
82
  modelProviderOptionsByName: GeminiImageModelProviderOptionsByName
80
83
  modelSizeByName: GeminiImageModelSizeByName
81
84
  modelInputModalitiesByName: GeminiImageModelInputModalitiesByName
@@ -89,7 +92,7 @@ export class GeminiImageAdapter<
89
92
  }
90
93
 
91
94
  async generateImages(
92
- options: ImageGenerationOptions<GeminiImageProviderOptions>,
95
+ options: ImageGenerationOptions<GeminiAnyImageProviderOptions>,
93
96
  ): Promise<ImageGenerationResult> {
94
97
  const { model, logger } = options
95
98
 
@@ -121,7 +124,7 @@ export class GeminiImageAdapter<
121
124
  )
122
125
  }
123
126
 
124
- if (this.isGeminiImageModel(model)) {
127
+ if (isGeminiNativeImageModel(model)) {
125
128
  return await this.generateWithGeminiApi(options, resolved)
126
129
  }
127
130
 
@@ -155,29 +158,40 @@ export class GeminiImageAdapter<
155
158
  }
156
159
  }
157
160
 
158
- private isGeminiImageModel(model: string): boolean {
159
- return model.startsWith('gemini-')
160
- }
161
-
162
161
  private async generateWithGeminiApi(
163
- options: ImageGenerationOptions<GeminiImageProviderOptions>,
162
+ options: ImageGenerationOptions<GeminiNativeImageProviderOptions>,
164
163
  resolved: ResolvedMediaPrompt,
165
164
  ): Promise<ImageGenerationResult> {
166
165
  const { model, size, numberOfImages, modelOptions } = options
167
166
 
168
167
  const parsedSize = size ? parseNativeImageSize(size) : undefined
169
168
 
170
- // GeminiImageProviderOptions is Imagen-shaped — most fields
171
- // (personGeneration, safetyFilterLevel, addWatermark, outputMimeType,
172
- // outputCompressionQuality, guidanceScale, enhancePrompt,
173
- // includeSafetyAttributes, includeRaiReason, outputGcsUri, labels,
174
- // negativePrompt, language) are only valid on GenerateImagesConfig and
175
- // would be rejected by the Gemini-native generateContent path. Pick only
176
- // the fields that are valid on GenerateContentConfig instead of spreading
177
- // the whole options object.
178
- const nativeConfig: GenerateContentConfig = {}
179
- if (modelOptions?.seed !== undefined) {
180
- nativeConfig.seed = modelOptions.seed
169
+ // The portable `size` option is the baseline; modelOptions.imageConfig is
170
+ // the provider escape hatch and wins per field, so a caller passing only
171
+ // `imageConfig.imageSize` keeps the aspectRatio derived from `size`.
172
+ const imageConfig: ImageConfig = {
173
+ ...(parsedSize?.aspectRatio && { aspectRatio: parsedSize.aspectRatio }),
174
+ ...(parsedSize?.resolution && { imageSize: parsedSize.resolution }),
175
+ ...modelOptions?.imageConfig,
176
+ }
177
+
178
+ // Named picks, never a wholesale spread: the Imagen-shaped fields of
179
+ // GeminiImageProviderOptions (personGeneration, safetyFilterLevel,
180
+ // addWatermark, outputMimeType, …) are only valid on GenerateImagesConfig
181
+ // and would be rejected by generateContent. Picking by name means no
182
+ // Imagen field can reach this path even if one slips past the per-model
183
+ // provider-options map.
184
+ const nativeConfig: GenerateContentConfig = {
185
+ ...(modelOptions?.seed !== undefined && { seed: modelOptions.seed }),
186
+ ...(modelOptions?.safetySettings !== undefined && {
187
+ safetySettings: modelOptions.safetySettings,
188
+ }),
189
+ ...(modelOptions?.thinkingConfig !== undefined && {
190
+ thinkingConfig: modelOptions.thinkingConfig,
191
+ }),
192
+ ...(modelOptions?.systemInstruction !== undefined && {
193
+ systemInstruction: modelOptions.systemInstruction,
194
+ }),
181
195
  }
182
196
 
183
197
  const config: GenerateContentConfig = {
@@ -186,16 +200,7 @@ export class GeminiImageAdapter<
186
200
  // IMPORTANT: responseModalities is a protected default — set it AFTER
187
201
  // nativeConfig so nothing can silently disable image output.
188
202
  responseModalities: ['TEXT', 'IMAGE'],
189
- ...(parsedSize && {
190
- imageConfig: {
191
- ...(parsedSize.aspectRatio && {
192
- aspectRatio: parsedSize.aspectRatio,
193
- }),
194
- ...(parsedSize.resolution && {
195
- imageSize: parsedSize.resolution,
196
- }),
197
- },
198
- }),
203
+ ...(Object.keys(imageConfig).length > 0 && { imageConfig }),
199
204
  }
200
205
 
201
206
  const contents = this.buildContents(resolved, numberOfImages)
@@ -321,7 +326,7 @@ export class GeminiImageAdapter<
321
326
  }
322
327
 
323
328
  private buildImagenConfig(
324
- options: ImageGenerationOptions<GeminiImageProviderOptions>,
329
+ options: ImageGenerationOptions<GeminiAnyImageProviderOptions>,
325
330
  ): GenerateImagesConfig {
326
331
  const { size, numberOfImages, modelOptions } = options
327
332
 
@@ -329,11 +334,62 @@ export class GeminiImageAdapter<
329
334
  // vendor `GenerateImagesConfig` fields are `field?: T` (no `| undefined`),
330
335
  // so we can only assign the property when we actually have a value.
331
336
  const sizeAspectRatio = size ? sizeToAspectRatio(size) : undefined
337
+
338
+ // Named picks, never a wholesale spread — the mirror image of the native
339
+ // path below. A native-only field (safetySettings, thinkingConfig,
340
+ // imageConfig, systemInstruction) belongs to GenerateContentConfig and is
341
+ // rejected by generateImages with 400 INVALID_ARGUMENT, so it must not be
342
+ // able to reach here even when the caller's `modelOptions` was typed
343
+ // against both shapes at once (e.g. an adapter inferred from a union of
344
+ // model names).
332
345
  return {
333
346
  numberOfImages: numberOfImages ?? 1,
334
- // Map size to aspect ratio if provided (modelOptions.aspectRatio will override)
347
+ // Map size to aspect ratio if provided; modelOptions.aspectRatio,
348
+ // picked after it, overrides.
335
349
  ...(sizeAspectRatio !== undefined && { aspectRatio: sizeAspectRatio }),
336
- ...modelOptions,
350
+ ...(modelOptions?.aspectRatio !== undefined && {
351
+ aspectRatio: modelOptions.aspectRatio,
352
+ }),
353
+ ...(modelOptions?.personGeneration !== undefined && {
354
+ personGeneration: modelOptions.personGeneration,
355
+ }),
356
+ ...(modelOptions?.safetyFilterLevel !== undefined && {
357
+ safetyFilterLevel: modelOptions.safetyFilterLevel,
358
+ }),
359
+ ...(modelOptions?.seed !== undefined && { seed: modelOptions.seed }),
360
+ ...(modelOptions?.addWatermark !== undefined && {
361
+ addWatermark: modelOptions.addWatermark,
362
+ }),
363
+ ...(modelOptions?.language !== undefined && {
364
+ language: modelOptions.language,
365
+ }),
366
+ ...(modelOptions?.negativePrompt !== undefined && {
367
+ negativePrompt: modelOptions.negativePrompt,
368
+ }),
369
+ ...(modelOptions?.outputMimeType !== undefined && {
370
+ outputMimeType: modelOptions.outputMimeType,
371
+ }),
372
+ ...(modelOptions?.outputCompressionQuality !== undefined && {
373
+ outputCompressionQuality: modelOptions.outputCompressionQuality,
374
+ }),
375
+ ...(modelOptions?.guidanceScale !== undefined && {
376
+ guidanceScale: modelOptions.guidanceScale,
377
+ }),
378
+ ...(modelOptions?.enhancePrompt !== undefined && {
379
+ enhancePrompt: modelOptions.enhancePrompt,
380
+ }),
381
+ ...(modelOptions?.includeSafetyAttributes !== undefined && {
382
+ includeSafetyAttributes: modelOptions.includeSafetyAttributes,
383
+ }),
384
+ ...(modelOptions?.includeRaiReason !== undefined && {
385
+ includeRaiReason: modelOptions.includeRaiReason,
386
+ }),
387
+ ...(modelOptions?.outputGcsUri !== undefined && {
388
+ outputGcsUri: modelOptions.outputGcsUri,
389
+ }),
390
+ ...(modelOptions?.labels !== undefined && {
391
+ labels: modelOptions.labels,
392
+ }),
337
393
  }
338
394
  }
339
395
 
@@ -392,6 +448,18 @@ export class GeminiImageAdapter<
392
448
  }
393
449
  }
394
450
 
451
+ /** @deprecated Shut down 2026-06-25. Use `gemini-3.1-flash-image`. */
452
+ export function createGeminiImage(
453
+ model: 'gemini-3.1-flash-image-preview',
454
+ apiKey: string,
455
+ config?: Omit<GeminiImageConfig, 'apiKey'>,
456
+ ): GeminiImageAdapter<'gemini-3.1-flash-image-preview'>
457
+ /** @deprecated Shut down 2026-06-25. Use `gemini-3-pro-image`. */
458
+ export function createGeminiImage(
459
+ model: 'gemini-3-pro-image-preview',
460
+ apiKey: string,
461
+ config?: Omit<GeminiImageConfig, 'apiKey'>,
462
+ ): GeminiImageAdapter<'gemini-3-pro-image-preview'>
395
463
  /**
396
464
  * Creates a Gemini image adapter with explicit API key.
397
465
  * Type resolution happens here at the call site.
@@ -411,6 +479,11 @@ export class GeminiImageAdapter<
411
479
  * });
412
480
  * ```
413
481
  */
482
+ export function createGeminiImage<TModel extends GeminiImageModel>(
483
+ model: TModel,
484
+ apiKey: string,
485
+ config?: Omit<GeminiImageConfig, 'apiKey'>,
486
+ ): GeminiImageAdapter<TModel>
414
487
  export function createGeminiImage<TModel extends GeminiImageModel>(
415
488
  model: TModel,
416
489
  apiKey: string,
@@ -419,6 +492,16 @@ export function createGeminiImage<TModel extends GeminiImageModel>(
419
492
  return new GeminiImageAdapter({ apiKey, ...config }, model)
420
493
  }
421
494
 
495
+ /** @deprecated Shut down 2026-06-25. Use `gemini-3.1-flash-image`. */
496
+ export function geminiImage(
497
+ model: 'gemini-3.1-flash-image-preview',
498
+ config?: Omit<GeminiImageConfig, 'apiKey'>,
499
+ ): GeminiImageAdapter<'gemini-3.1-flash-image-preview'>
500
+ /** @deprecated Shut down 2026-06-25. Use `gemini-3-pro-image`. */
501
+ export function geminiImage(
502
+ model: 'gemini-3-pro-image-preview',
503
+ config?: Omit<GeminiImageConfig, 'apiKey'>,
504
+ ): GeminiImageAdapter<'gemini-3-pro-image-preview'>
422
505
  /**
423
506
  * Creates a Gemini image adapter with automatic API key detection from environment variables.
424
507
  * Type resolution happens here at the call site.
@@ -443,6 +526,10 @@ export function createGeminiImage<TModel extends GeminiImageModel>(
443
526
  * });
444
527
  * ```
445
528
  */
529
+ export function geminiImage<TModel extends GeminiImageModel>(
530
+ model: TModel,
531
+ config?: Omit<GeminiImageConfig, 'apiKey'>,
532
+ ): GeminiImageAdapter<TModel>
446
533
  export function geminiImage<TModel extends GeminiImageModel>(
447
534
  model: TModel,
448
535
  config?: Omit<GeminiImageConfig, 'apiKey'>,
@@ -6,6 +6,11 @@ import {
6
6
  generateId,
7
7
  getGeminiApiKeyFromEnv,
8
8
  } from '../../utils/client'
9
+ import {
10
+ getGeminiProviderToolKind,
11
+ getGeminiProviderToolMetadata,
12
+ } from '../../tools/gemini-provider-tool'
13
+ import { assertUniqueToolNames } from '@tanstack/ai/adapter-internals'
9
14
  import type { InternalLogger } from '@tanstack/ai/adapter-internals'
10
15
  import type {
11
16
  GeminiChatModelToolCapabilitiesByName,
@@ -122,9 +127,9 @@ type ResolveInputModalities<TModel extends string> =
122
127
 
123
128
  /**
124
129
  * Resolve tool capabilities for a specific model. Reuses the chat-model
125
- * capability map: `google_maps` / `google_search_retrieval` /
126
- * `mcp_server` are rejected at runtime by `convertToolsToInteractionsFormat`,
127
- * but per-model gating happens here at compile time.
130
+ * capability map: `google_maps` / `google_search_retrieval` are rejected at
131
+ * runtime by `convertToolsToInteractionsFormat`, but per-model gating happens
132
+ * here at compile time.
128
133
  */
129
134
  type ResolveToolCapabilities<TModel extends string> =
130
135
  TModel extends keyof GeminiChatModelToolCapabilitiesByName
@@ -153,8 +158,11 @@ type ResolveToolCapabilities<TModel extends string> =
153
158
  * corresponding per-tool variants) carrying the raw Interactions delta;
154
159
  * see {@link GeminiInteractionsCustomEvent}. `computer_use` is accepted
155
160
  * in the request but the Interactions API does not currently stream
156
- * per-delta CUSTOM events for it. `google_search_retrieval`,
157
- * `google_maps`, and `mcp_server` are not supported on this adapter.
161
+ * per-delta CUSTOM events for it. The `google_search_retrieval` and
162
+ * `google_maps` provider-tool factories are not supported on this adapter and
163
+ * throw a targeted error. There is no Gemini `mcp_server` factory, so a tool
164
+ * merely *named* `mcp_server` is an ordinary function and is sent as a function
165
+ * declaration like any other.
158
166
  *
159
167
  * @experimental Interactions API is in Beta per Google; shapes may change.
160
168
  * @see https://ai.google.dev/gemini-api/docs/interactions
@@ -781,20 +789,29 @@ function convertToolsToInteractionsFormat<TTool extends Tool>(
781
789
  tools: Array<TTool> | undefined,
782
790
  ): Array<InteractionsTool> | undefined {
783
791
  if (!tools || tools.length === 0) return undefined
792
+ assertUniqueToolNames(tools)
784
793
 
785
794
  const result: Array<InteractionsTool> = []
786
795
 
787
796
  for (const tool of tools) {
788
- switch (tool.name) {
797
+ switch (getGeminiProviderToolKind(tool)) {
789
798
  case 'google_search': {
790
- const metadata = (tool.metadata ?? {}) as {
791
- search_types?: Array<'web_search' | 'image_search'>
799
+ const metadata = (getGeminiProviderToolMetadata(tool) ?? {}) as {
800
+ searchTypes?: {
801
+ webSearch?: unknown
802
+ imageSearch?: unknown
803
+ }
804
+ }
805
+ const searchTypes: Array<'web_search' | 'image_search'> = []
806
+ if (metadata.searchTypes?.webSearch !== undefined) {
807
+ searchTypes.push('web_search')
808
+ }
809
+ if (metadata.searchTypes?.imageSearch !== undefined) {
810
+ searchTypes.push('image_search')
792
811
  }
793
812
  result.push({
794
813
  type: 'google_search',
795
- ...(metadata.search_types
796
- ? { search_types: metadata.search_types }
797
- : {}),
814
+ ...(searchTypes.length > 0 ? { search_types: searchTypes } : {}),
798
815
  })
799
816
  break
800
817
  }
@@ -807,7 +824,7 @@ function convertToolsToInteractionsFormat<TTool extends Tool>(
807
824
  break
808
825
  }
809
826
  case 'file_search': {
810
- const metadata = (tool.metadata ?? {}) as {
827
+ const metadata = (getGeminiProviderToolMetadata(tool) ?? {}) as {
811
828
  fileSearchStoreNames?: Array<string>
812
829
  topK?: number
813
830
  metadataFilter?: string
@@ -825,7 +842,7 @@ function convertToolsToInteractionsFormat<TTool extends Tool>(
825
842
  break
826
843
  }
827
844
  case 'computer_use': {
828
- const metadata = (tool.metadata ?? {}) as {
845
+ const metadata = (getGeminiProviderToolMetadata(tool) ?? {}) as {
829
846
  environment?: string
830
847
  excludedPredefinedFunctions?: Array<string>
831
848
  }
@@ -856,11 +873,7 @@ function convertToolsToInteractionsFormat<TTool extends Tool>(
856
873
  throw new Error(
857
874
  '`google_maps` is not yet supported on the Gemini Interactions API. Use `geminiText()` for Google Maps grounding.',
858
875
  )
859
- case 'mcp_server':
860
- throw new Error(
861
- '`mcp_server` is not yet supported on the `geminiTextInteractions()` adapter.',
862
- )
863
- default: {
876
+ case undefined: {
864
877
  if (!tool.description) {
865
878
  throw new Error(
866
879
  `Tool ${tool.name} requires a description for the Gemini Interactions adapter`,