@langchain/google 0.2.5 → 0.2.7

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.
@@ -33,32 +33,42 @@ function adjustObjectType(obj) {
33
33
  else throw new InvalidInputError("Gemini does not support union types in function schemas. Use a single type instead.");
34
34
  return obj;
35
35
  }
36
- /**
37
- * Recursively removes unsupported properties from a JSON Schema object to make it
38
- * compatible with Gemini's function schema format.
39
- *
40
- * Gemini's function schema format does not support:
41
- * - `additionalProperties` property
42
- * - Array types (must be converted to string types with nullable flag)
43
- * - Union types
44
- *
45
- * @param obj - The JSON Schema object to clean
46
- * @returns A cleaned GeminiFunctionSchema object
47
- *
48
- * @internal
49
- */
50
- function removeAdditionalProperties(obj) {
51
- if (typeof obj === "object" && obj !== null) {
52
- const newObj = { ...obj };
53
- if ("additionalProperties" in newObj) delete newObj.additionalProperties;
54
- adjustObjectType(newObj);
55
- for (const key in newObj) if (key in newObj) {
56
- if (Array.isArray(newObj[key])) newObj[key] = newObj[key].map(removeAdditionalProperties);
57
- else if (typeof newObj[key] === "object" && newObj[key] !== null) newObj[key] = removeAdditionalProperties(newObj[key]);
58
- }
59
- return newObj;
60
- }
61
- return obj;
36
+ const GEMINI_SCHEMA_KEYS = /* @__PURE__ */ new Set([
37
+ "anyOf",
38
+ "default",
39
+ "description",
40
+ "enum",
41
+ "example",
42
+ "format",
43
+ "items",
44
+ "maxItems",
45
+ "maxLength",
46
+ "maxProperties",
47
+ "maximum",
48
+ "minItems",
49
+ "minLength",
50
+ "minProperties",
51
+ "minimum",
52
+ "nullable",
53
+ "pattern",
54
+ "properties",
55
+ "propertyOrdering",
56
+ "required",
57
+ "title",
58
+ "type"
59
+ ]);
60
+ function sanitizeGeminiSchema(node) {
61
+ if (Array.isArray(node)) return node.map(sanitizeGeminiSchema);
62
+ if (typeof node !== "object" || node === null) return node;
63
+ if ("$ref" in node) throw new InvalidInputError("Gemini does not support recursive or referenced ($ref) schemas in function or response schemas. Inline the schema instead.");
64
+ const source = node;
65
+ const schema = {};
66
+ for (const key of Object.keys(source)) if (GEMINI_SCHEMA_KEYS.has(key)) schema[key] = source[key];
67
+ adjustObjectType(schema);
68
+ if (schema.properties && typeof schema.properties === "object") schema.properties = Object.fromEntries(Object.entries(schema.properties).map(([name, value]) => [name, sanitizeGeminiSchema(value)]));
69
+ if (schema.items !== void 0) schema.items = sanitizeGeminiSchema(schema.items);
70
+ if (Array.isArray(schema.anyOf)) schema.anyOf = schema.anyOf.map(sanitizeGeminiSchema);
71
+ return schema;
62
72
  }
63
73
  /**
64
74
  * Converts a Zod schema or JSON Schema to Gemini function parameters format.
@@ -73,8 +83,7 @@ function removeAdditionalProperties(obj) {
73
83
  * @internal
74
84
  */
75
85
  function schemaToGeminiParameters(schema) {
76
- const { $schema, ...rest } = removeAdditionalProperties(isInteropZodSchema(schema) || isSerializableSchema(schema) ? toJsonSchema(schema) : schema);
77
- return rest;
86
+ return sanitizeGeminiSchema(isInteropZodSchema(schema) || isSerializableSchema(schema) ? toJsonSchema(schema) : schema);
78
87
  }
79
88
  /**
80
89
  * Converts a JSON Schema object to Gemini function parameters format.
@@ -85,8 +94,7 @@ function schemaToGeminiParameters(schema) {
85
94
  * @internal
86
95
  */
87
96
  function jsonSchemaToGeminiParameters(schema) {
88
- const { $schema, ...rest } = removeAdditionalProperties(schema);
89
- return rest;
97
+ return sanitizeGeminiSchema(schema);
90
98
  }
91
99
  /**
92
100
  * Type guard to check if a tool is already in Gemini's native format.
@@ -345,7 +353,19 @@ function convertToolChoiceToGeminiConfig(toolChoice, hasTools) {
345
353
  if (mode) functionCallingConfig.mode = mode;
346
354
  return { functionCallingConfig };
347
355
  }
356
+ /**
357
+ * Checks whether a tools array mixes server-side built-in tools (e.g.
358
+ * `googleSearch`, `codeExecution`) with function-declaration tools.
359
+ *
360
+ * Gemini rejects such a mix with a 400 unless
361
+ * `toolConfig.includeServerSideToolInvocations` is set.
362
+ * See https://ai.google.dev/gemini-api/docs/tool-combination
363
+ */
364
+ function mixesBuiltinAndFunctionTools(tools) {
365
+ const isFunctionDeclarationTool = (tool) => "functionDeclarations" in tool && !!tool.functionDeclarations?.length;
366
+ return tools.some(isFunctionDeclarationTool) && tools.some((tool) => !isFunctionDeclarationTool(tool));
367
+ }
348
368
  //#endregion
349
- export { convertToolChoiceToGeminiConfig, convertToolsToGeminiFunctionDeclarations, convertToolsToGeminiTools, schemaToGeminiParameters };
369
+ export { convertToolChoiceToGeminiConfig, convertToolsToGeminiFunctionDeclarations, convertToolsToGeminiTools, mixesBuiltinAndFunctionTools, schemaToGeminiParameters };
350
370
 
351
371
  //# sourceMappingURL=tools.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"tools.js","names":[],"sources":["../../src/converters/tools.ts"],"sourcesContent":["import { isLangChainTool } from \"@langchain/core/utils/function_calling\";\nimport { isOpenAITool } from \"@langchain/core/language_models/base\";\nimport {\n InteropZodType,\n isInteropZodSchema,\n} from \"@langchain/core/utils/types\";\nimport {\n type JsonSchema7Type,\n toJsonSchema,\n} from \"@langchain/core/utils/json_schema\";\nimport { Converter } from \"@langchain/core/utils/format\";\nimport type {\n BindToolsInput,\n ToolChoice,\n} from \"@langchain/core/language_models/chat_models\";\nimport type { Gemini } from \"../chat_models/types.js\";\nimport { InvalidInputError, InvalidToolError } from \"../utils/errors.js\";\nimport {\n isSerializableSchema,\n SerializableSchema,\n} from \"@langchain/core/utils/standard_schema\";\n\n/**\n * Adjusts a JSON Schema object type to be compatible with Gemini's function schema format.\n *\n * Gemini's function schema format has specific requirements:\n * - Type must be a string, not an array\n * - Nullable types should use the `nullable` property instead of including \"null\" in a type array\n * - Union types are not supported\n *\n * @param obj - The JSON Schema object to adjust\n * @returns The adjusted object with type converted to Gemini-compatible format\n * @throws {Error} If the type is a union type or null-only type, which Gemini cannot handle\n *\n * @internal\n */\nfunction adjustObjectType(\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n obj: Record<string, any>\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n): Record<string, any> {\n if (!Array.isArray(obj.type)) {\n return obj;\n }\n\n const len = obj.type.length;\n const nullIndex = obj.type.indexOf(\"null\");\n if (len === 2 && nullIndex >= 0) {\n // There are only two values set for the type, and one of them is \"null\".\n // Set the type to the other one and set nullable to true.\n const typeIndex = nullIndex === 0 ? 1 : 0;\n obj.type = obj.type[typeIndex];\n obj.nullable = true;\n } else if (len === 1 && nullIndex === 0) {\n // This is nullable only without a type, which doesn't make sense for Gemini\n throw new InvalidInputError(\n \"Gemini does not support null-only types in function schemas. Provide a non-null type alongside nullable.\"\n );\n } else if (len === 1) {\n // Although an array, it has only one value.\n // So set it to the string to match what Gemini expects.\n obj.type = obj?.type[0];\n } else if (len === 0) {\n // Empty type array is invalid\n throw new InvalidInputError(\n \"Gemini does not support empty type arrays in function schemas. Provide at least one type.\"\n );\n } else {\n // Anything else could be a union type, so reject it.\n throw new InvalidInputError(\n \"Gemini does not support union types in function schemas. Use a single type instead.\"\n );\n }\n return obj;\n}\n\n/**\n * Recursively removes unsupported properties from a JSON Schema object to make it\n * compatible with Gemini's function schema format.\n *\n * Gemini's function schema format does not support:\n * - `additionalProperties` property\n * - Array types (must be converted to string types with nullable flag)\n * - Union types\n *\n * @param obj - The JSON Schema object to clean\n * @returns A cleaned GeminiFunctionSchema object\n *\n * @internal\n */\nfunction removeAdditionalProperties(\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n obj: Record<string, any>\n): Gemini.Tools.Schema & { $schema?: string } {\n if (typeof obj === \"object\" && obj !== null) {\n const newObj = { ...obj };\n\n if (\"additionalProperties\" in newObj) {\n delete newObj.additionalProperties;\n }\n\n adjustObjectType(newObj);\n\n for (const key in newObj) {\n if (key in newObj) {\n if (Array.isArray(newObj[key])) {\n newObj[key] = newObj[key].map(removeAdditionalProperties);\n } else if (typeof newObj[key] === \"object\" && newObj[key] !== null) {\n newObj[key] = removeAdditionalProperties(newObj[key]);\n }\n }\n }\n\n return newObj as Gemini.Tools.Schema;\n }\n\n return obj as Gemini.Tools.Schema;\n}\n\n/**\n * Converts a Zod schema or JSON Schema to Gemini function parameters format.\n *\n * This function handles the conversion of schema definitions from various formats\n * (Zod schemas, JSON Schema 7) to Gemini's function schema format. It removes\n * unsupported properties and adjusts type definitions to be compatible with Gemini.\n *\n * @param schema - The schema to convert (Zod schema or JSON Schema 7)\n * @returns A schema object compatible with Gemini's API\n *\n * @internal\n */\nexport function schemaToGeminiParameters<\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n RunOutput extends Record<string, any> = Record<string, any>,\n>(\n schema:\n | SerializableSchema<RunOutput>\n | InteropZodType<RunOutput>\n | JsonSchema7Type\n): Gemini.Tools.Schema {\n // Gemini doesn't accept either the $schema or additionalProperties\n // attributes, so we need to explicitly remove them.\n // Zod sometimes also makes an array of type (because of .nullish()),\n // which needs cleaning up.\n const jsonSchema = removeAdditionalProperties(\n isInteropZodSchema(schema) || isSerializableSchema(schema)\n ? toJsonSchema(schema)\n : schema\n );\n const { $schema, ...rest } = jsonSchema;\n return rest;\n}\n\n/**\n * Converts a JSON Schema object to Gemini function parameters format.\n *\n * @param schema - The JSON Schema object to convert\n * @returns A schema object compatible with Gemini's API\n *\n * @internal\n */\nfunction jsonSchemaToGeminiParameters(\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n schema: Record<string, any>\n): Gemini.Tools.Schema {\n const jsonSchema = removeAdditionalProperties(schema);\n const { $schema, ...rest } = jsonSchema;\n\n return rest;\n}\n\n/**\n * Type guard to check if a tool is already in Gemini's native format.\n *\n * Gemini tools can be:\n * - Function declarations (`functionDeclarations`)\n * - Code execution tools (`codeExecution`)\n * - Google Search retrieval tools (`googleSearchRetrieval` or `googleSearch`)\n *\n * The full list of tool types are those defined by the `Gemini.Tool` type.\n * Updating that list requires a change to the `geminiToolAttributes` below.\n *\n * @param tool - The tool to check\n * @returns `true` if the tool is already in Gemini format, `false` otherwise\n *\n * @internal\n */\nfunction isGeminiTool(tool: BindToolsInput): tool is Gemini.Tool {\n if (typeof tool !== \"object\" || tool === null) {\n return false;\n }\n\n const geminiToolAttributes = [\n \"functionDeclarations\",\n \"codeExecution\",\n \"googleSearch\",\n \"urlContext\",\n \"googleMaps\",\n \"fileSearch\",\n \"computerUse\",\n \"mcpServers\",\n ];\n\n return geminiToolAttributes.some((attr) => attr in tool);\n}\n\n/**\n * Converts an array of LangChain tools to Gemini function declarations.\n *\n * This converter transforms various tool formats into Gemini's function declaration\n * format. It supports:\n * - LangChain structured tools (with Zod schemas)\n * - OpenAI format tools (with JSON Schema)\n * - Gemini-native function declarations (passed through)\n * - Raw function declaration objects\n *\n * @remarks\n * The converter handles each tool type differently:\n * - **LangChain tools**: Extracts name, description, and schema, converting the schema\n * to Gemini-compatible format\n * - **OpenAI tools**: Extracts function name, description, and parameters from the\n * OpenAI tool format\n * - **Gemini tools**: If the tool contains `functionDeclarations`, extracts and returns\n * them directly. Non-function Gemini tools (like `codeExecution`) are skipped.\n * - **Raw objects**: Assumes the object is a function declaration and validates it has\n * a `name` property\n *\n * @example\n * ```typescript\n * import { StructuredTool } from \"@langchain/core/tools\";\n * import { z } from \"zod\";\n *\n * const weatherTool = new StructuredTool({\n * name: \"get_weather\",\n * description: \"Get the weather for a location\",\n * schema: z.object({\n * location: z.string().describe(\"The city name\"),\n * unit: z.enum([\"celsius\", \"fahrenheit\"]).optional()\n * }),\n * func: async (args) => { /* ... *\\/ }\n * });\n *\n * const declarations = convertToolsToGeminiFunctionDeclarations([weatherTool]);\n * // Returns: [{\n * // name: \"get_weather\",\n * // description: \"Get the weather for a location\",\n * // parameters: {\n * // type: \"object\",\n * // properties: {\n * // location: { type: \"string\", description: \"The city name\" },\n * // unit: { type: \"string\", enum: [\"celsius\", \"fahrenheit\"], nullable: true }\n * // },\n * // required: [\"location\"]\n * // }\n * // }]\n * ```\n *\n * @param tools - Array of tools in various formats (LangChain, OpenAI, or Gemini)\n * @returns Array of Gemini function declarations ready for API submission\n * @throws {Error} If a tool cannot be converted (invalid format)\n */\nexport const convertToolsToGeminiFunctionDeclarations: Converter<\n BindToolsInput[],\n Gemini.Tools.FunctionDeclaration[]\n> = (tools) => {\n const functionDeclarations: Gemini.Tools.FunctionDeclaration[] = [];\n\n for (const tool of tools) {\n // Skip if already in Gemini format (non-function tools)\n if (isGeminiTool(tool) && !(\"functionDeclarations\" in tool)) {\n continue;\n }\n\n // Handle Gemini format with functionDeclarations\n if (\n isGeminiTool(tool) &&\n \"functionDeclarations\" in tool &&\n Array.isArray(tool.functionDeclarations)\n ) {\n functionDeclarations.push(...tool.functionDeclarations);\n continue;\n }\n\n // Handle LangChain structured tools\n if (isLangChainTool(tool)) {\n const jsonSchema = schemaToGeminiParameters(tool.schema);\n functionDeclarations.push({\n name: tool.name,\n description: tool.description ?? `A function available to call.`,\n parameters: jsonSchema,\n });\n continue;\n }\n\n // Handle OpenAI format tools\n if (isOpenAITool(tool)) {\n functionDeclarations.push({\n name: tool.function.name,\n description:\n tool.function.description ?? `A function available to call.`,\n parameters: jsonSchemaToGeminiParameters(tool.function.parameters),\n });\n continue;\n }\n\n // Handle raw function declaration objects\n if (\n typeof tool === \"object\" &&\n tool !== null &&\n \"name\" in tool &&\n typeof tool.name === \"string\"\n ) {\n const funcDecl = tool as Partial<Gemini.Tools.FunctionDeclaration>;\n functionDeclarations.push({\n name: funcDecl.name!,\n description: funcDecl.description ?? `A function available to call.`,\n parameters: funcDecl.parameters,\n });\n continue;\n }\n\n throw new InvalidToolError(tool);\n }\n\n return functionDeclarations;\n};\n\n/**\n * Converts an array of tools to Gemini's Tools array format.\n *\n * This converter groups function declarations into a single tool object while\n * preserving Gemini-native tools (like code execution and Google Search retrieval)\n * as separate tool objects. This matches Gemini's API structure where:\n * - Function declarations are grouped together in a single tool\n * - Non-function tools (codeExecution, googleSearchRetrieval) are separate tools\n *\n * @remarks\n * The conversion process:\n * 1. Separates Gemini-native tools from function declarations\n * 2. Converts LangChain/OpenAI tools to function declarations\n * 3. Groups all function declarations into a single Tool object\n * 4. Returns an array with the grouped function declarations tool plus any\n * non-function Gemini tools\n *\n * @example\n * ```typescript\n * import { StructuredTool } from \"@langchain/core/tools\";\n * import { z } from \"zod\";\n *\n * const weatherTool = new StructuredTool({\n * name: \"get_weather\",\n * description: \"Get the weather\",\n * schema: z.object({ location: z.string() }),\n * func: async () => {}\n * });\n *\n * const geminiCodeTool: Gemini.Tool = {\n * codeExecution: { enabled: true }\n * };\n *\n * const tools = convertToolsToGeminiTools([weatherTool, geminiCodeTool]);\n * // Returns: [\n * // { functionDeclarations: [{ name: \"get_weather\", ... }] },\n * // { codeExecution: { enabled: true } }\n * // ]\n * ```\n *\n * @param tools - Array of tools in various formats (LangChain, OpenAI, or Gemini)\n * @returns Array of Tool objects ready for API submission\n */\nexport const convertToolsToGeminiTools: Converter<\n BindToolsInput[],\n Gemini.Tool[]\n> = (tools) => {\n if (tools.length === 0) {\n return [];\n }\n\n const geminiTools: Gemini.Tool[] = [];\n const functionDeclarations: Gemini.Tools.FunctionDeclaration[] = [];\n\n // Separate Gemini-native tools from function declarations\n for (const tool of tools) {\n if (isGeminiTool(tool)) {\n if (\"functionDeclarations\" in tool) {\n // Already a Gemini tool with function declarations\n geminiTools.push(tool);\n } else {\n // Non-function Gemini tool (codeExecution, googleSearch, etc.)\n geminiTools.push(tool);\n }\n } else if (\"googleSearchRetrieval\" in tool) {\n // Convert the obsolete search into the modern one\n const searchTool: Gemini.Tool = {\n googleSearch: {},\n };\n geminiTools.push(searchTool);\n } else {\n // Convert to function declaration\n const funcDecls = convertToolsToGeminiFunctionDeclarations([tool]);\n functionDeclarations.push(...funcDecls);\n }\n }\n\n // Add function declarations as a single tool if we have any\n if (functionDeclarations.length > 0) {\n geminiTools.push({\n functionDeclarations,\n });\n }\n\n return geminiTools;\n};\n\n/**\n * Converts tool choice to Gemini's function calling configuration.\n *\n * This converter transforms LangChain's tool choice format into Gemini's\n * `toolConfig.functionCallingConfig` format. The configuration is only\n * created when tools are present (`hasTools` is true).\n *\n * @remarks\n * The conversion logic:\n * - If `hasTools` is `false`, returns `undefined` (no config needed)\n * - If `toolChoice` is provided, converts it to a Gemini mode:\n * - `\"auto\"` → `\"AUTO\"` - Model decides whether to call functions\n * - `\"any\"` or `\"required\"` → `\"ANY\"` - Model must call at least one function\n * - `\"none\"` → `\"NONE\"` - Model cannot call functions\n * - Function name string → `\"ANY\"` - Forces function use\n * - Object with `mode` → Maps the mode string to Gemini format\n * - Object with `function` → `\"ANY\"` - Forces function use\n * - If `toolChoice` is not provided but tools exist, defaults to `\"AUTO\"`\n *\n * Gemini's function calling modes:\n * - **AUTO**: The model can choose whether to call functions based on the conversation\n * - **ANY**: The model must call at least one function before responding\n * - **NONE**: The model cannot call any functions\n *\n * When a specific function is requested (via function name string or `{ function: { name } }`),\n * the converter uses `\"ANY\"` mode to force function usage, as Gemini doesn't support\n * specifying individual functions in the mode.\n *\n * @example\n * ```typescript\n * // With explicit tool choice\n * convertToolChoiceToGeminiConfig(\"auto\", true);\n * // Returns: { functionCallingConfig: { mode: \"AUTO\" } }\n *\n * // Required mode - must call a function\n * convertToolChoiceToGeminiConfig(\"any\", true);\n * // Returns: { functionCallingConfig: { mode: \"ANY\" } }\n *\n * // Without tool choice (defaults to AUTO when tools exist)\n * convertToolChoiceToGeminiConfig(undefined, true);\n * // Returns: { functionCallingConfig: { mode: \"AUTO\" } }\n *\n * // No tools - returns undefined\n * convertToolChoiceToGeminiConfig(\"auto\", false);\n * // Returns: undefined\n * ```\n *\n * @param toolChoice - The tool choice option from LangChain (string, object, or undefined)\n * @param hasTools - Whether tools are present. If false, returns undefined\n * @returns The Gemini tool configuration object, or undefined if no tools are present\n */\nexport function convertToolChoiceToGeminiConfig(\n toolChoice: ToolChoice | undefined,\n hasTools: boolean\n): Gemini.Tools.ToolConfig | undefined {\n if (!hasTools || toolChoice === undefined) {\n return undefined;\n }\n\n let mode: Gemini.Tools.FunctionCallingConfigMode | undefined;\n let allowedFunctionNames: string[] | undefined;\n\n let toolChoiceMode: string | undefined;\n let toolChoiceFunction: string | string[] | undefined;\n if (typeof toolChoice === \"object\") {\n toolChoiceMode = toolChoice.mode;\n toolChoiceFunction = toolChoice.function?.name;\n } else {\n toolChoiceMode = toolChoice;\n }\n\n if (toolChoiceMode === \"auto\") {\n mode = \"AUTO\";\n } else if (toolChoiceMode === \"any\" || toolChoiceMode === \"required\") {\n mode = \"ANY\";\n } else if (toolChoiceMode === \"none\") {\n mode = \"NONE\";\n } else if (typeof toolChoiceMode === \"string\") {\n mode = \"ANY\";\n toolChoiceFunction = toolChoiceMode;\n }\n\n if (toolChoiceFunction) {\n if (Array.isArray(toolChoiceFunction)) {\n allowedFunctionNames = toolChoiceFunction;\n } else {\n allowedFunctionNames = [toolChoiceFunction];\n }\n }\n\n const functionCallingConfig: Gemini.Tools.FunctionCallingConfig = {};\n if (allowedFunctionNames?.length) {\n functionCallingConfig.allowedFunctionNames = allowedFunctionNames;\n }\n if (mode) {\n functionCallingConfig.mode = mode;\n }\n return {\n functionCallingConfig,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAoCA,SAAS,iBAEP,KAEqB;CACrB,IAAI,CAAC,MAAM,QAAQ,IAAI,IAAI,GACzB,OAAO;CAGT,MAAM,MAAM,IAAI,KAAK;CACrB,MAAM,YAAY,IAAI,KAAK,QAAQ,MAAM;CACzC,IAAI,QAAQ,KAAK,aAAa,GAAG;EAG/B,MAAM,YAAY,cAAc,IAAI,IAAI;EACxC,IAAI,OAAO,IAAI,KAAK;EACpB,IAAI,WAAW;CACjB,OAAO,IAAI,QAAQ,KAAK,cAAc,GAEpC,MAAM,IAAI,kBACR,0GACF;MACK,IAAI,QAAQ,GAGjB,IAAI,OAAO,KAAK,KAAK;MAChB,IAAI,QAAQ,GAEjB,MAAM,IAAI,kBACR,2FACF;MAGA,MAAM,IAAI,kBACR,qFACF;CAEF,OAAO;AACT;;;;;;;;;;;;;;;AAgBA,SAAS,2BAEP,KAC4C;CAC5C,IAAI,OAAO,QAAQ,YAAY,QAAQ,MAAM;EAC3C,MAAM,SAAS,EAAE,GAAG,IAAI;EAExB,IAAI,0BAA0B,QAC5B,OAAO,OAAO;EAGhB,iBAAiB,MAAM;EAEvB,KAAK,MAAM,OAAO,QAChB,IAAI,OAAO,QAAQ;GACjB,IAAI,MAAM,QAAQ,OAAO,IAAI,GAC3B,OAAO,OAAO,OAAO,IAAI,CAAC,IAAI,0BAA0B;QACnD,IAAI,OAAO,OAAO,SAAS,YAAY,OAAO,SAAS,MAC5D,OAAO,OAAO,2BAA2B,OAAO,IAAI;EAExD;EAGF,OAAO;CACT;CAEA,OAAO;AACT;;;;;;;;;;;;;AAcA,SAAgB,yBAId,QAIqB;CAUrB,MAAM,EAAE,SAAS,GAAG,SALD,2BACjB,mBAAmB,MAAM,KAAK,qBAAqB,MAAM,IACrD,aAAa,MAAM,IACnB,MAEgC;CACtC,OAAO;AACT;;;;;;;;;AAUA,SAAS,6BAEP,QACqB;CAErB,MAAM,EAAE,SAAS,GAAG,SADD,2BAA2B,MACR;CAEtC,OAAO;AACT;;;;;;;;;;;;;;;;;AAkBA,SAAS,aAAa,MAA2C;CAC/D,IAAI,OAAO,SAAS,YAAY,SAAS,MACvC,OAAO;CAcT,OAAO;EAVL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CAGwB,CAAC,CAAC,MAAM,SAAS,QAAQ,IAAI;AACzD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyDA,MAAa,4CAGR,UAAU;CACb,MAAM,uBAA2D,CAAC;CAElE,KAAK,MAAM,QAAQ,OAAO;EAExB,IAAI,aAAa,IAAI,KAAK,EAAE,0BAA0B,OACpD;EAIF,IACE,aAAa,IAAI,KACjB,0BAA0B,QAC1B,MAAM,QAAQ,KAAK,oBAAoB,GACvC;GACA,qBAAqB,KAAK,GAAG,KAAK,oBAAoB;GACtD;EACF;EAGA,IAAI,gBAAgB,IAAI,GAAG;GACzB,MAAM,aAAa,yBAAyB,KAAK,MAAM;GACvD,qBAAqB,KAAK;IACxB,MAAM,KAAK;IACX,aAAa,KAAK,eAAe;IACjC,YAAY;GACd,CAAC;GACD;EACF;EAGA,IAAI,aAAa,IAAI,GAAG;GACtB,qBAAqB,KAAK;IACxB,MAAM,KAAK,SAAS;IACpB,aACE,KAAK,SAAS,eAAe;IAC/B,YAAY,6BAA6B,KAAK,SAAS,UAAU;GACnE,CAAC;GACD;EACF;EAGA,IACE,OAAO,SAAS,YAChB,SAAS,QACT,UAAU,QACV,OAAO,KAAK,SAAS,UACrB;GACA,MAAM,WAAW;GACjB,qBAAqB,KAAK;IACxB,MAAM,SAAS;IACf,aAAa,SAAS,eAAe;IACrC,YAAY,SAAS;GACvB,CAAC;GACD;EACF;EAEA,MAAM,IAAI,iBAAiB,IAAI;CACjC;CAEA,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6CA,MAAa,6BAGR,UAAU;CACb,IAAI,MAAM,WAAW,GACnB,OAAO,CAAC;CAGV,MAAM,cAA6B,CAAC;CACpC,MAAM,uBAA2D,CAAC;CAGlE,KAAK,MAAM,QAAQ,OACjB,IAAI,aAAa,IAAI,GAAG;EACtB,IAAI,0BAA0B,MAE5B,YAAY,KAAK,IAAI;OAGrB,YAAY,KAAK,IAAI;CAEzB,OAAO,IAAI,2BAA2B,MAKpC,YAAY,KAAK,EAFf,cAAc,CAAC,EAES,CAAC;MACtB;EAEL,MAAM,YAAY,yCAAyC,CAAC,IAAI,CAAC;EACjE,qBAAqB,KAAK,GAAG,SAAS;CACxC;CAIF,IAAI,qBAAqB,SAAS,GAChC,YAAY,KAAK,EACf,qBACF,CAAC;CAGH,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqDA,SAAgB,gCACd,YACA,UACqC;CACrC,IAAI,CAAC,YAAY,eAAe,KAAA,GAC9B;CAGF,IAAI;CACJ,IAAI;CAEJ,IAAI;CACJ,IAAI;CACJ,IAAI,OAAO,eAAe,UAAU;EAClC,iBAAiB,WAAW;EAC5B,qBAAqB,WAAW,UAAU;CAC5C,OACE,iBAAiB;CAGnB,IAAI,mBAAmB,QACrB,OAAO;MACF,IAAI,mBAAmB,SAAS,mBAAmB,YACxD,OAAO;MACF,IAAI,mBAAmB,QAC5B,OAAO;MACF,IAAI,OAAO,mBAAmB,UAAU;EAC7C,OAAO;EACP,qBAAqB;CACvB;CAEA,IAAI,oBAAoB;EACtB,IAAI,MAAM,QAAQ,kBAAkB,GAClC,uBAAuB;OAEvB,uBAAuB,CAAC,kBAAkB;CAE9C;CAEA,MAAM,wBAA4D,CAAC;CACnE,IAAI,sBAAsB,QACxB,sBAAsB,uBAAuB;CAE/C,IAAI,MACF,sBAAsB,OAAO;CAE/B,OAAO,EACL,sBACF;AACF"}
1
+ {"version":3,"file":"tools.js","names":[],"sources":["../../src/converters/tools.ts"],"sourcesContent":["import { isLangChainTool } from \"@langchain/core/utils/function_calling\";\nimport { isOpenAITool } from \"@langchain/core/language_models/base\";\nimport {\n InteropZodType,\n isInteropZodSchema,\n} from \"@langchain/core/utils/types\";\nimport {\n type JsonSchema7Type,\n toJsonSchema,\n} from \"@langchain/core/utils/json_schema\";\nimport { Converter } from \"@langchain/core/utils/format\";\nimport type {\n BindToolsInput,\n ToolChoice,\n} from \"@langchain/core/language_models/chat_models\";\nimport type { Gemini } from \"../chat_models/types.js\";\nimport { InvalidInputError, InvalidToolError } from \"../utils/errors.js\";\nimport {\n isSerializableSchema,\n SerializableSchema,\n} from \"@langchain/core/utils/standard_schema\";\n\n/**\n * Adjusts a JSON Schema object type to be compatible with Gemini's function schema format.\n *\n * Gemini's function schema format has specific requirements:\n * - Type must be a string, not an array\n * - Nullable types should use the `nullable` property instead of including \"null\" in a type array\n * - Union types are not supported\n *\n * @param obj - The JSON Schema object to adjust\n * @returns The adjusted object with type converted to Gemini-compatible format\n * @throws {Error} If the type is a union type or null-only type, which Gemini cannot handle\n *\n * @internal\n */\nfunction adjustObjectType(\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n obj: Record<string, any>\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n): Record<string, any> {\n if (!Array.isArray(obj.type)) {\n return obj;\n }\n\n const len = obj.type.length;\n const nullIndex = obj.type.indexOf(\"null\");\n if (len === 2 && nullIndex >= 0) {\n // There are only two values set for the type, and one of them is \"null\".\n // Set the type to the other one and set nullable to true.\n const typeIndex = nullIndex === 0 ? 1 : 0;\n obj.type = obj.type[typeIndex];\n obj.nullable = true;\n } else if (len === 1 && nullIndex === 0) {\n // This is nullable only without a type, which doesn't make sense for Gemini\n throw new InvalidInputError(\n \"Gemini does not support null-only types in function schemas. Provide a non-null type alongside nullable.\"\n );\n } else if (len === 1) {\n // Although an array, it has only one value.\n // So set it to the string to match what Gemini expects.\n obj.type = obj?.type[0];\n } else if (len === 0) {\n // Empty type array is invalid\n throw new InvalidInputError(\n \"Gemini does not support empty type arrays in function schemas. Provide at least one type.\"\n );\n } else {\n // Anything else could be a union type, so reject it.\n throw new InvalidInputError(\n \"Gemini does not support union types in function schemas. Use a single type instead.\"\n );\n }\n return obj;\n}\n\n// Keywords Gemini's function schema accepts. Keep in sync with Gemini.Tools.Schema (api-types.ts); anything else 400s (#8584).\nconst GEMINI_SCHEMA_KEYS = new Set([\n \"anyOf\",\n \"default\",\n \"description\",\n \"enum\",\n \"example\",\n \"format\",\n \"items\",\n \"maxItems\",\n \"maxLength\",\n \"maxProperties\",\n \"maximum\",\n \"minItems\",\n \"minLength\",\n \"minProperties\",\n \"minimum\",\n \"nullable\",\n \"pattern\",\n \"properties\",\n \"propertyOrdering\",\n \"required\",\n \"title\",\n \"type\",\n]);\n\n// Recursively allowlist-filters a schema node; `properties`' own keys are user field names, not schema keywords, so only its values recurse.\nfunction sanitizeGeminiSchema(node: unknown): unknown {\n if (Array.isArray(node)) {\n return node.map(sanitizeGeminiSchema);\n }\n if (typeof node !== \"object\" || node === null) {\n return node;\n }\n\n if (\"$ref\" in node) {\n throw new InvalidInputError(\n \"Gemini does not support recursive or referenced ($ref) schemas in function or response schemas. Inline the schema instead.\"\n );\n }\n\n const source = node as Record<string, unknown>;\n const schema: Record<string, unknown> = {};\n for (const key of Object.keys(source)) {\n if (GEMINI_SCHEMA_KEYS.has(key)) {\n schema[key] = source[key];\n }\n }\n\n adjustObjectType(schema);\n\n if (schema.properties && typeof schema.properties === \"object\") {\n // fromEntries defines properties (safe for a field literally named \"__proto__\"), unlike bracket assignment on a plain {}.\n schema.properties = Object.fromEntries(\n Object.entries(schema.properties).map(([name, value]) => [\n name,\n sanitizeGeminiSchema(value),\n ])\n );\n }\n if (schema.items !== undefined) {\n schema.items = sanitizeGeminiSchema(schema.items);\n }\n if (Array.isArray(schema.anyOf)) {\n schema.anyOf = schema.anyOf.map(sanitizeGeminiSchema);\n }\n\n return schema;\n}\n\n/**\n * Converts a Zod schema or JSON Schema to Gemini function parameters format.\n *\n * This function handles the conversion of schema definitions from various formats\n * (Zod schemas, JSON Schema 7) to Gemini's function schema format. It removes\n * unsupported properties and adjusts type definitions to be compatible with Gemini.\n *\n * @param schema - The schema to convert (Zod schema or JSON Schema 7)\n * @returns A schema object compatible with Gemini's API\n *\n * @internal\n */\nexport function schemaToGeminiParameters<\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n RunOutput extends Record<string, any> = Record<string, any>,\n>(\n schema:\n | SerializableSchema<RunOutput>\n | InteropZodType<RunOutput>\n | JsonSchema7Type\n): Gemini.Tools.Schema {\n return sanitizeGeminiSchema(\n isInteropZodSchema(schema) || isSerializableSchema(schema)\n ? toJsonSchema(schema)\n : schema\n ) as Gemini.Tools.Schema;\n}\n\n/**\n * Converts a JSON Schema object to Gemini function parameters format.\n *\n * @param schema - The JSON Schema object to convert\n * @returns A schema object compatible with Gemini's API\n *\n * @internal\n */\nfunction jsonSchemaToGeminiParameters(\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n schema: Record<string, any>\n): Gemini.Tools.Schema {\n return sanitizeGeminiSchema(schema) as Gemini.Tools.Schema;\n}\n\n/**\n * Type guard to check if a tool is already in Gemini's native format.\n *\n * Gemini tools can be:\n * - Function declarations (`functionDeclarations`)\n * - Code execution tools (`codeExecution`)\n * - Google Search retrieval tools (`googleSearchRetrieval` or `googleSearch`)\n *\n * The full list of tool types are those defined by the `Gemini.Tool` type.\n * Updating that list requires a change to the `geminiToolAttributes` below.\n *\n * @param tool - The tool to check\n * @returns `true` if the tool is already in Gemini format, `false` otherwise\n *\n * @internal\n */\nfunction isGeminiTool(tool: BindToolsInput): tool is Gemini.Tool {\n if (typeof tool !== \"object\" || tool === null) {\n return false;\n }\n\n const geminiToolAttributes = [\n \"functionDeclarations\",\n \"codeExecution\",\n \"googleSearch\",\n \"urlContext\",\n \"googleMaps\",\n \"fileSearch\",\n \"computerUse\",\n \"mcpServers\",\n ];\n\n return geminiToolAttributes.some((attr) => attr in tool);\n}\n\n/**\n * Converts an array of LangChain tools to Gemini function declarations.\n *\n * This converter transforms various tool formats into Gemini's function declaration\n * format. It supports:\n * - LangChain structured tools (with Zod schemas)\n * - OpenAI format tools (with JSON Schema)\n * - Gemini-native function declarations (passed through)\n * - Raw function declaration objects\n *\n * @remarks\n * The converter handles each tool type differently:\n * - **LangChain tools**: Extracts name, description, and schema, converting the schema\n * to Gemini-compatible format\n * - **OpenAI tools**: Extracts function name, description, and parameters from the\n * OpenAI tool format\n * - **Gemini tools**: If the tool contains `functionDeclarations`, extracts and returns\n * them directly. Non-function Gemini tools (like `codeExecution`) are skipped.\n * - **Raw objects**: Assumes the object is a function declaration and validates it has\n * a `name` property\n *\n * @example\n * ```typescript\n * import { StructuredTool } from \"@langchain/core/tools\";\n * import { z } from \"zod\";\n *\n * const weatherTool = new StructuredTool({\n * name: \"get_weather\",\n * description: \"Get the weather for a location\",\n * schema: z.object({\n * location: z.string().describe(\"The city name\"),\n * unit: z.enum([\"celsius\", \"fahrenheit\"]).optional()\n * }),\n * func: async (args) => { /* ... *\\/ }\n * });\n *\n * const declarations = convertToolsToGeminiFunctionDeclarations([weatherTool]);\n * // Returns: [{\n * // name: \"get_weather\",\n * // description: \"Get the weather for a location\",\n * // parameters: {\n * // type: \"object\",\n * // properties: {\n * // location: { type: \"string\", description: \"The city name\" },\n * // unit: { type: \"string\", enum: [\"celsius\", \"fahrenheit\"], nullable: true }\n * // },\n * // required: [\"location\"]\n * // }\n * // }]\n * ```\n *\n * @param tools - Array of tools in various formats (LangChain, OpenAI, or Gemini)\n * @returns Array of Gemini function declarations ready for API submission\n * @throws {Error} If a tool cannot be converted (invalid format)\n */\nexport const convertToolsToGeminiFunctionDeclarations: Converter<\n BindToolsInput[],\n Gemini.Tools.FunctionDeclaration[]\n> = (tools) => {\n const functionDeclarations: Gemini.Tools.FunctionDeclaration[] = [];\n\n for (const tool of tools) {\n // Skip if already in Gemini format (non-function tools)\n if (isGeminiTool(tool) && !(\"functionDeclarations\" in tool)) {\n continue;\n }\n\n // Handle Gemini format with functionDeclarations\n if (\n isGeminiTool(tool) &&\n \"functionDeclarations\" in tool &&\n Array.isArray(tool.functionDeclarations)\n ) {\n functionDeclarations.push(...tool.functionDeclarations);\n continue;\n }\n\n // Handle LangChain structured tools\n if (isLangChainTool(tool)) {\n const jsonSchema = schemaToGeminiParameters(tool.schema);\n functionDeclarations.push({\n name: tool.name,\n description: tool.description ?? `A function available to call.`,\n parameters: jsonSchema,\n });\n continue;\n }\n\n // Handle OpenAI format tools\n if (isOpenAITool(tool)) {\n functionDeclarations.push({\n name: tool.function.name,\n description:\n tool.function.description ?? `A function available to call.`,\n parameters: jsonSchemaToGeminiParameters(tool.function.parameters),\n });\n continue;\n }\n\n // Handle raw function declaration objects\n if (\n typeof tool === \"object\" &&\n tool !== null &&\n \"name\" in tool &&\n typeof tool.name === \"string\"\n ) {\n const funcDecl = tool as Partial<Gemini.Tools.FunctionDeclaration>;\n functionDeclarations.push({\n name: funcDecl.name!,\n description: funcDecl.description ?? `A function available to call.`,\n parameters: funcDecl.parameters,\n });\n continue;\n }\n\n throw new InvalidToolError(tool);\n }\n\n return functionDeclarations;\n};\n\n/**\n * Converts an array of tools to Gemini's Tools array format.\n *\n * This converter groups function declarations into a single tool object while\n * preserving Gemini-native tools (like code execution and Google Search retrieval)\n * as separate tool objects. This matches Gemini's API structure where:\n * - Function declarations are grouped together in a single tool\n * - Non-function tools (codeExecution, googleSearchRetrieval) are separate tools\n *\n * @remarks\n * The conversion process:\n * 1. Separates Gemini-native tools from function declarations\n * 2. Converts LangChain/OpenAI tools to function declarations\n * 3. Groups all function declarations into a single Tool object\n * 4. Returns an array with the grouped function declarations tool plus any\n * non-function Gemini tools\n *\n * @example\n * ```typescript\n * import { StructuredTool } from \"@langchain/core/tools\";\n * import { z } from \"zod\";\n *\n * const weatherTool = new StructuredTool({\n * name: \"get_weather\",\n * description: \"Get the weather\",\n * schema: z.object({ location: z.string() }),\n * func: async () => {}\n * });\n *\n * const geminiCodeTool: Gemini.Tool = {\n * codeExecution: { enabled: true }\n * };\n *\n * const tools = convertToolsToGeminiTools([weatherTool, geminiCodeTool]);\n * // Returns: [\n * // { functionDeclarations: [{ name: \"get_weather\", ... }] },\n * // { codeExecution: { enabled: true } }\n * // ]\n * ```\n *\n * @param tools - Array of tools in various formats (LangChain, OpenAI, or Gemini)\n * @returns Array of Tool objects ready for API submission\n */\nexport const convertToolsToGeminiTools: Converter<\n BindToolsInput[],\n Gemini.Tool[]\n> = (tools) => {\n if (tools.length === 0) {\n return [];\n }\n\n const geminiTools: Gemini.Tool[] = [];\n const functionDeclarations: Gemini.Tools.FunctionDeclaration[] = [];\n\n // Separate Gemini-native tools from function declarations\n for (const tool of tools) {\n if (isGeminiTool(tool)) {\n if (\"functionDeclarations\" in tool) {\n // Already a Gemini tool with function declarations\n geminiTools.push(tool);\n } else {\n // Non-function Gemini tool (codeExecution, googleSearch, etc.)\n geminiTools.push(tool);\n }\n } else if (\"googleSearchRetrieval\" in tool) {\n // Convert the obsolete search into the modern one\n const searchTool: Gemini.Tool = {\n googleSearch: {},\n };\n geminiTools.push(searchTool);\n } else {\n // Convert to function declaration\n const funcDecls = convertToolsToGeminiFunctionDeclarations([tool]);\n functionDeclarations.push(...funcDecls);\n }\n }\n\n // Add function declarations as a single tool if we have any\n if (functionDeclarations.length > 0) {\n geminiTools.push({\n functionDeclarations,\n });\n }\n\n return geminiTools;\n};\n\n/**\n * Converts tool choice to Gemini's function calling configuration.\n *\n * This converter transforms LangChain's tool choice format into Gemini's\n * `toolConfig.functionCallingConfig` format. The configuration is only\n * created when tools are present (`hasTools` is true).\n *\n * @remarks\n * The conversion logic:\n * - If `hasTools` is `false`, returns `undefined` (no config needed)\n * - If `toolChoice` is provided, converts it to a Gemini mode:\n * - `\"auto\"` → `\"AUTO\"` - Model decides whether to call functions\n * - `\"any\"` or `\"required\"` → `\"ANY\"` - Model must call at least one function\n * - `\"none\"` → `\"NONE\"` - Model cannot call functions\n * - Function name string → `\"ANY\"` - Forces function use\n * - Object with `mode` → Maps the mode string to Gemini format\n * - Object with `function` → `\"ANY\"` - Forces function use\n * - If `toolChoice` is not provided but tools exist, defaults to `\"AUTO\"`\n *\n * Gemini's function calling modes:\n * - **AUTO**: The model can choose whether to call functions based on the conversation\n * - **ANY**: The model must call at least one function before responding\n * - **NONE**: The model cannot call any functions\n *\n * When a specific function is requested (via function name string or `{ function: { name } }`),\n * the converter uses `\"ANY\"` mode to force function usage, as Gemini doesn't support\n * specifying individual functions in the mode.\n *\n * @example\n * ```typescript\n * // With explicit tool choice\n * convertToolChoiceToGeminiConfig(\"auto\", true);\n * // Returns: { functionCallingConfig: { mode: \"AUTO\" } }\n *\n * // Required mode - must call a function\n * convertToolChoiceToGeminiConfig(\"any\", true);\n * // Returns: { functionCallingConfig: { mode: \"ANY\" } }\n *\n * // Without tool choice (defaults to AUTO when tools exist)\n * convertToolChoiceToGeminiConfig(undefined, true);\n * // Returns: { functionCallingConfig: { mode: \"AUTO\" } }\n *\n * // No tools - returns undefined\n * convertToolChoiceToGeminiConfig(\"auto\", false);\n * // Returns: undefined\n * ```\n *\n * @param toolChoice - The tool choice option from LangChain (string, object, or undefined)\n * @param hasTools - Whether tools are present. If false, returns undefined\n * @returns The Gemini tool configuration object, or undefined if no tools are present\n */\nexport function convertToolChoiceToGeminiConfig(\n toolChoice: ToolChoice | undefined,\n hasTools: boolean\n): Gemini.Tools.ToolConfig | undefined {\n if (!hasTools || toolChoice === undefined) {\n return undefined;\n }\n\n let mode: Gemini.Tools.FunctionCallingConfigMode | undefined;\n let allowedFunctionNames: string[] | undefined;\n\n let toolChoiceMode: string | undefined;\n let toolChoiceFunction: string | string[] | undefined;\n if (typeof toolChoice === \"object\") {\n toolChoiceMode = toolChoice.mode;\n toolChoiceFunction = toolChoice.function?.name;\n } else {\n toolChoiceMode = toolChoice;\n }\n\n if (toolChoiceMode === \"auto\") {\n mode = \"AUTO\";\n } else if (toolChoiceMode === \"any\" || toolChoiceMode === \"required\") {\n mode = \"ANY\";\n } else if (toolChoiceMode === \"none\") {\n mode = \"NONE\";\n } else if (typeof toolChoiceMode === \"string\") {\n mode = \"ANY\";\n toolChoiceFunction = toolChoiceMode;\n }\n\n if (toolChoiceFunction) {\n if (Array.isArray(toolChoiceFunction)) {\n allowedFunctionNames = toolChoiceFunction;\n } else {\n allowedFunctionNames = [toolChoiceFunction];\n }\n }\n\n const functionCallingConfig: Gemini.Tools.FunctionCallingConfig = {};\n if (allowedFunctionNames?.length) {\n functionCallingConfig.allowedFunctionNames = allowedFunctionNames;\n }\n if (mode) {\n functionCallingConfig.mode = mode;\n }\n return {\n functionCallingConfig,\n };\n}\n\n/**\n * Checks whether a tools array mixes server-side built-in tools (e.g.\n * `googleSearch`, `codeExecution`) with function-declaration tools.\n *\n * Gemini rejects such a mix with a 400 unless\n * `toolConfig.includeServerSideToolInvocations` is set.\n * See https://ai.google.dev/gemini-api/docs/tool-combination\n */\nexport function mixesBuiltinAndFunctionTools(tools: Gemini.Tool[]): boolean {\n const isFunctionDeclarationTool = (tool: Gemini.Tool) =>\n \"functionDeclarations\" in tool && !!tool.functionDeclarations?.length;\n return (\n tools.some(isFunctionDeclarationTool) &&\n tools.some((tool) => !isFunctionDeclarationTool(tool))\n );\n}\n"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  let _langchain_core_language_models_compat = require("@langchain/core/language_models/compat");
2
+ let _langchain_core_utils_uuid = require("@langchain/core/utils/uuid");
2
3
  //#region src/utils/stream_events.ts
3
4
  /**
4
5
  * Converts Gemini stream responses into LangChain ChatModelStreamEvents.
@@ -13,6 +14,8 @@ async function* convertGoogleGeminiStream(source, options = {}) {
13
14
  let messageStarted = false;
14
15
  let usageSnapshot;
15
16
  let finishReason = "stop";
17
+ let groundingMetadata;
18
+ let citationMetadata;
16
19
  const getOrCreateBlockIndex = (key, initial) => {
17
20
  const existing = blockKeyToIndex.get(key);
18
21
  if (existing !== void 0) return {
@@ -48,6 +51,8 @@ async function* convertGoogleGeminiStream(source, options = {}) {
48
51
  }
49
52
  const candidate = response.candidates?.[0];
50
53
  if (candidate?.finishReason) finishReason = mapGeminiFinishReason(candidate.finishReason);
54
+ if (candidate?.groundingMetadata) groundingMetadata = candidate.groundingMetadata;
55
+ if (candidate?.citationMetadata) citationMetadata = candidate.citationMetadata;
51
56
  const parts = candidate?.content?.parts;
52
57
  if (!parts) continue;
53
58
  let toolIdx = 0;
@@ -67,6 +72,7 @@ async function* convertGoogleGeminiStream(source, options = {}) {
67
72
  };
68
73
  const acc = blockAccumulators.get(index);
69
74
  acc.reasoning = (acc.reasoning ?? "") + part.text;
75
+ if (part.thoughtSignature) acc.thoughtSignature = part.thoughtSignature;
70
76
  yield {
71
77
  event: "content-block-delta",
72
78
  index,
@@ -104,22 +110,26 @@ async function* convertGoogleGeminiStream(source, options = {}) {
104
110
  const args = JSON.stringify(part.functionCall.args ?? {});
105
111
  const { index, isNew } = getOrCreateBlockIndex(key, {
106
112
  type: "tool_call_chunk",
113
+ id: part.functionCall.id ?? `lc-tool-call-${(0, _langchain_core_utils_uuid.v4)().replace(/-/g, "")}`,
107
114
  name: part.functionCall.name,
108
115
  args: "",
109
116
  index: toolIdx
110
117
  });
118
+ const acc = blockAccumulators.get(index);
119
+ const id = acc.id;
111
120
  if (isNew) yield {
112
121
  event: "content-block-start",
113
122
  index,
114
123
  content: {
115
124
  type: "tool_call_chunk",
125
+ id,
116
126
  name: part.functionCall.name,
117
127
  args: "",
118
128
  index: toolIdx
119
129
  }
120
130
  };
121
- const acc = blockAccumulators.get(index);
122
131
  acc.args = args;
132
+ if (part.thoughtSignature) acc.thoughtSignature = part.thoughtSignature;
123
133
  yield {
124
134
  event: "content-block-delta",
125
135
  index,
@@ -127,6 +137,7 @@ async function* convertGoogleGeminiStream(source, options = {}) {
127
137
  type: "block-delta",
128
138
  fields: {
129
139
  type: "tool_call_chunk",
140
+ id,
130
141
  name: acc.name,
131
142
  args: acc.args
132
143
  }
@@ -135,16 +146,29 @@ async function* convertGoogleGeminiStream(source, options = {}) {
135
146
  toolIdx += 1;
136
147
  }
137
148
  }
138
- for (const [index, acc] of blockAccumulators) yield {
139
- event: "content-block-finish",
140
- index,
141
- content: (0, _langchain_core_language_models_compat.finalizeContentBlock)(acc)
142
- };
149
+ for (const [index, acc] of blockAccumulators) {
150
+ const finalized = (0, _langchain_core_language_models_compat.finalizeContentBlock)(acc);
151
+ yield {
152
+ event: "content-block-finish",
153
+ index,
154
+ content: acc.thoughtSignature ? {
155
+ ...finalized,
156
+ thoughtSignature: acc.thoughtSignature
157
+ } : finalized
158
+ };
159
+ }
143
160
  yield {
144
161
  event: "message-finish",
145
162
  reason: finishReason,
146
163
  ...usageSnapshot ? { usage: usageSnapshot } : {},
147
- responseMetadata: { model_provider: "google" }
164
+ responseMetadata: {
165
+ model_provider: "google",
166
+ ...citationMetadata ? { citationMetadata } : {},
167
+ ...groundingMetadata ? {
168
+ groundingMetadata,
169
+ groundingSupport: groundingMetadata.groundingSupports?.filter((s) => (s?.segment?.partIndex ?? 0) === 0)
170
+ } : {}
171
+ }
148
172
  };
149
173
  }
150
174
  function mapGeminiFinishReason(reason) {
@@ -1 +1 @@
1
- {"version":3,"file":"stream_events.cjs","names":["finalizeContentBlock"],"sources":["../../src/utils/stream_events.ts"],"sourcesContent":["/**\n * Converts Gemini stream responses into LangChain ChatModelStreamEvents.\n *\n * @module\n */\n\nimport { finalizeContentBlock } from \"@langchain/core/language_models/compat\";\nimport type {\n ChatModelStreamEvent,\n FinishReason,\n} from \"@langchain/core/language_models/event\";\nimport type { ContentBlock, UsageMetadata } from \"@langchain/core/messages\";\nimport type { Gemini } from \"../chat_models/api-types.js\";\n\nexport type GeminiStreamResponse = Gemini.GenerateContentResponse;\n\nexport interface ConvertGoogleGeminiStreamOptions {\n streamUsage?: boolean;\n}\n\ntype BlockKey = \"text\" | \"reasoning\" | `tool:${number}`;\n\nexport async function* convertGoogleGeminiStream(\n source: AsyncIterable<GeminiStreamResponse>,\n options: ConvertGoogleGeminiStreamOptions = {}\n): AsyncGenerator<ChatModelStreamEvent> {\n const shouldStreamUsage = options.streamUsage ?? true;\n const blockAccumulators = new Map<\n number,\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n Record<string, any>\n >();\n const blockKeyToIndex = new Map<BlockKey, number>();\n let nextBlockIndex = 0;\n let messageStarted = false;\n let usageSnapshot: UsageMetadata | undefined;\n let finishReason: FinishReason = \"stop\";\n\n const getOrCreateBlockIndex = (\n key: BlockKey,\n initial: Record<string, unknown>\n ): { index: number; isNew: boolean } => {\n const existing = blockKeyToIndex.get(key);\n if (existing !== undefined) {\n return { index: existing, isNew: false };\n }\n const index = nextBlockIndex++;\n blockKeyToIndex.set(key, index);\n blockAccumulators.set(index, { ...initial });\n return { index, isNew: true };\n };\n\n for await (const response of source) {\n if (!messageStarted) {\n messageStarted = true;\n yield { event: \"message-start\" as const };\n }\n\n const usageMetadata = response.usageMetadata ?? response.usage_metadata;\n if (shouldStreamUsage && usageMetadata) {\n const input = usageMetadata.promptTokenCount ?? 0;\n const output = usageMetadata.candidatesTokenCount ?? 0;\n usageSnapshot = {\n input_tokens: input,\n output_tokens: output,\n total_tokens: usageMetadata.totalTokenCount ?? input + output,\n };\n yield { event: \"usage\" as const, usage: usageSnapshot };\n }\n\n const candidate = response.candidates?.[0];\n if (candidate?.finishReason) {\n finishReason = mapGeminiFinishReason(candidate.finishReason);\n }\n\n const parts = candidate?.content?.parts;\n if (!parts) continue;\n\n let toolIdx = 0;\n for (const part of parts) {\n if (part.text) {\n if (part.thought) {\n const key: BlockKey = \"reasoning\";\n const { index, isNew } = getOrCreateBlockIndex(key, {\n type: \"reasoning\",\n reasoning: \"\",\n });\n if (isNew) {\n yield {\n event: \"content-block-start\" as const,\n index,\n content: { type: \"reasoning\", reasoning: \"\" } as ContentBlock,\n };\n }\n const acc = blockAccumulators.get(index)!;\n acc.reasoning = (acc.reasoning ?? \"\") + part.text;\n yield {\n event: \"content-block-delta\" as const,\n index,\n delta: { type: \"reasoning-delta\" as const, reasoning: part.text },\n };\n } else {\n const key: BlockKey = \"text\";\n const { index, isNew } = getOrCreateBlockIndex(key, {\n type: \"text\",\n text: \"\",\n });\n if (isNew) {\n yield {\n event: \"content-block-start\" as const,\n index,\n content: { type: \"text\", text: \"\" } as ContentBlock,\n };\n }\n const acc = blockAccumulators.get(index)!;\n acc.text = (acc.text ?? \"\") + part.text;\n yield {\n event: \"content-block-delta\" as const,\n index,\n delta: { type: \"text-delta\" as const, text: part.text },\n };\n }\n } else if (part.functionCall) {\n const key: BlockKey = `tool:${toolIdx}`;\n const args = JSON.stringify(part.functionCall.args ?? {});\n const { index, isNew } = getOrCreateBlockIndex(key, {\n type: \"tool_call_chunk\",\n name: part.functionCall.name,\n args: \"\",\n index: toolIdx,\n });\n if (isNew) {\n yield {\n event: \"content-block-start\" as const,\n index,\n content: {\n type: \"tool_call_chunk\",\n name: part.functionCall.name,\n args: \"\",\n index: toolIdx,\n } as ContentBlock,\n };\n }\n const acc = blockAccumulators.get(index)!;\n acc.args = args;\n yield {\n event: \"content-block-delta\" as const,\n index,\n delta: {\n type: \"block-delta\" as const,\n fields: {\n type: \"tool_call_chunk\",\n name: acc.name,\n args: acc.args,\n },\n },\n };\n toolIdx += 1;\n }\n }\n }\n\n for (const [index, acc] of blockAccumulators) {\n yield {\n event: \"content-block-finish\" as const,\n index,\n content: finalizeContentBlock(acc as ContentBlock),\n };\n }\n\n yield {\n event: \"message-finish\" as const,\n reason: finishReason,\n ...(usageSnapshot ? { usage: usageSnapshot } : {}),\n responseMetadata: { model_provider: \"google\" },\n };\n}\n\nfunction mapGeminiFinishReason(reason: string): FinishReason {\n switch (reason.toLowerCase()) {\n case \"max_tokens\":\n case \"max-token\":\n case \"max_token\":\n return \"length\";\n case \"safety\":\n case \"recitation\":\n case \"language\":\n case \"blocklist\":\n case \"prohibited_content\":\n case \"prohibited-content\":\n case \"spii\":\n case \"image_safety\":\n case \"image-safety\":\n case \"image_prohibited_content\":\n case \"image-prohibited-content\":\n case \"image_recitation\":\n case \"image-recitation\":\n return \"content_filter\";\n default:\n return \"stop\";\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"stream_events.cjs","names":["uuidv4","finalizeContentBlock"],"sources":["../../src/utils/stream_events.ts"],"sourcesContent":["/**\n * Converts Gemini stream responses into LangChain ChatModelStreamEvents.\n *\n * @module\n */\n\nimport { finalizeContentBlock } from \"@langchain/core/language_models/compat\";\nimport type {\n ChatModelStreamEvent,\n FinishReason,\n} from \"@langchain/core/language_models/event\";\nimport type { ContentBlock, UsageMetadata } from \"@langchain/core/messages\";\nimport { v4 as uuidv4 } from \"@langchain/core/utils/uuid\";\nimport type { Gemini } from \"../chat_models/api-types.js\";\n\nexport type GeminiStreamResponse = Gemini.GenerateContentResponse;\n\nexport interface ConvertGoogleGeminiStreamOptions {\n streamUsage?: boolean;\n}\n\ntype BlockKey = \"text\" | \"reasoning\" | `tool:${number}`;\n\nexport async function* convertGoogleGeminiStream(\n source: AsyncIterable<GeminiStreamResponse>,\n options: ConvertGoogleGeminiStreamOptions = {}\n): AsyncGenerator<ChatModelStreamEvent> {\n const shouldStreamUsage = options.streamUsage ?? true;\n const blockAccumulators = new Map<\n number,\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n Record<string, any>\n >();\n const blockKeyToIndex = new Map<BlockKey, number>();\n let nextBlockIndex = 0;\n let messageStarted = false;\n let usageSnapshot: UsageMetadata | undefined;\n let finishReason: FinishReason = \"stop\";\n let groundingMetadata: Gemini.GroundingMetadata | undefined;\n let citationMetadata: Gemini.CitationMetadata | undefined;\n\n const getOrCreateBlockIndex = (\n key: BlockKey,\n initial: Record<string, unknown>\n ): { index: number; isNew: boolean } => {\n const existing = blockKeyToIndex.get(key);\n if (existing !== undefined) {\n return { index: existing, isNew: false };\n }\n const index = nextBlockIndex++;\n blockKeyToIndex.set(key, index);\n blockAccumulators.set(index, { ...initial });\n return { index, isNew: true };\n };\n\n for await (const response of source) {\n if (!messageStarted) {\n messageStarted = true;\n yield { event: \"message-start\" as const };\n }\n\n const usageMetadata = response.usageMetadata ?? response.usage_metadata;\n if (shouldStreamUsage && usageMetadata) {\n const input = usageMetadata.promptTokenCount ?? 0;\n const output = usageMetadata.candidatesTokenCount ?? 0;\n usageSnapshot = {\n input_tokens: input,\n output_tokens: output,\n total_tokens: usageMetadata.totalTokenCount ?? input + output,\n };\n yield { event: \"usage\" as const, usage: usageSnapshot };\n }\n\n const candidate = response.candidates?.[0];\n if (candidate?.finishReason) {\n finishReason = mapGeminiFinishReason(candidate.finishReason);\n }\n if (candidate?.groundingMetadata) {\n groundingMetadata = candidate.groundingMetadata;\n }\n if (candidate?.citationMetadata) {\n citationMetadata = candidate.citationMetadata;\n }\n\n const parts = candidate?.content?.parts;\n if (!parts) continue;\n\n let toolIdx = 0;\n for (const part of parts) {\n if (part.text) {\n if (part.thought) {\n const key: BlockKey = \"reasoning\";\n const { index, isNew } = getOrCreateBlockIndex(key, {\n type: \"reasoning\",\n reasoning: \"\",\n });\n if (isNew) {\n yield {\n event: \"content-block-start\" as const,\n index,\n content: { type: \"reasoning\", reasoning: \"\" } as ContentBlock,\n };\n }\n const acc = blockAccumulators.get(index)!;\n acc.reasoning = (acc.reasoning ?? \"\") + part.text;\n if (part.thoughtSignature)\n acc.thoughtSignature = part.thoughtSignature;\n yield {\n event: \"content-block-delta\" as const,\n index,\n delta: { type: \"reasoning-delta\" as const, reasoning: part.text },\n };\n } else {\n const key: BlockKey = \"text\";\n const { index, isNew } = getOrCreateBlockIndex(key, {\n type: \"text\",\n text: \"\",\n });\n if (isNew) {\n yield {\n event: \"content-block-start\" as const,\n index,\n content: { type: \"text\", text: \"\" } as ContentBlock,\n };\n }\n const acc = blockAccumulators.get(index)!;\n acc.text = (acc.text ?? \"\") + part.text;\n yield {\n event: \"content-block-delta\" as const,\n index,\n delta: { type: \"text-delta\" as const, text: part.text },\n };\n }\n } else if (part.functionCall) {\n const key: BlockKey = `tool:${toolIdx}`;\n const args = JSON.stringify(part.functionCall.args ?? {});\n // Only used to seed a *new* block; an id already assigned to this\n // block must not change on later chunks.\n const candidateId =\n part.functionCall.id ?? `lc-tool-call-${uuidv4().replace(/-/g, \"\")}`;\n const { index, isNew } = getOrCreateBlockIndex(key, {\n type: \"tool_call_chunk\",\n id: candidateId,\n name: part.functionCall.name,\n args: \"\",\n index: toolIdx,\n });\n const acc = blockAccumulators.get(index)!;\n const id = acc.id as string;\n if (isNew) {\n yield {\n event: \"content-block-start\" as const,\n index,\n content: {\n type: \"tool_call_chunk\",\n id,\n name: part.functionCall.name,\n args: \"\",\n index: toolIdx,\n } as ContentBlock,\n };\n }\n acc.args = args;\n if (part.thoughtSignature) acc.thoughtSignature = part.thoughtSignature;\n yield {\n event: \"content-block-delta\" as const,\n index,\n delta: {\n type: \"block-delta\" as const,\n fields: {\n type: \"tool_call_chunk\",\n id,\n name: acc.name,\n args: acc.args,\n },\n },\n };\n toolIdx += 1;\n }\n }\n }\n\n for (const [index, acc] of blockAccumulators) {\n // finalizeContentBlock rebuilds tool_call_chunk -> tool_call as\n // {type, id, name, args} only, so thoughtSignature has to be re-attached.\n const finalized = finalizeContentBlock(acc as ContentBlock);\n yield {\n event: \"content-block-finish\" as const,\n index,\n content: acc.thoughtSignature\n ? ({\n ...finalized,\n thoughtSignature: acc.thoughtSignature,\n } as ContentBlock)\n : finalized,\n };\n }\n\n yield {\n event: \"message-finish\" as const,\n reason: finishReason,\n ...(usageSnapshot ? { usage: usageSnapshot } : {}),\n responseMetadata: {\n model_provider: \"google\",\n ...(citationMetadata ? { citationMetadata } : {}),\n ...(groundingMetadata\n ? {\n groundingMetadata,\n // Support entries for the first content part only (matches messages.ts).\n groundingSupport: groundingMetadata.groundingSupports?.filter(\n (s) => (s?.segment?.partIndex ?? 0) === 0\n ),\n }\n : {}),\n },\n };\n}\n\nfunction mapGeminiFinishReason(reason: string): FinishReason {\n switch (reason.toLowerCase()) {\n case \"max_tokens\":\n case \"max-token\":\n case \"max_token\":\n return \"length\";\n case \"safety\":\n case \"recitation\":\n case \"language\":\n case \"blocklist\":\n case \"prohibited_content\":\n case \"prohibited-content\":\n case \"spii\":\n case \"image_safety\":\n case \"image-safety\":\n case \"image_prohibited_content\":\n case \"image-prohibited-content\":\n case \"image_recitation\":\n case \"image-recitation\":\n return \"content_filter\";\n default:\n return \"stop\";\n }\n}\n"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { finalizeContentBlock } from "@langchain/core/language_models/compat";
2
+ import { v4 } from "@langchain/core/utils/uuid";
2
3
  //#region src/utils/stream_events.ts
3
4
  /**
4
5
  * Converts Gemini stream responses into LangChain ChatModelStreamEvents.
@@ -13,6 +14,8 @@ async function* convertGoogleGeminiStream(source, options = {}) {
13
14
  let messageStarted = false;
14
15
  let usageSnapshot;
15
16
  let finishReason = "stop";
17
+ let groundingMetadata;
18
+ let citationMetadata;
16
19
  const getOrCreateBlockIndex = (key, initial) => {
17
20
  const existing = blockKeyToIndex.get(key);
18
21
  if (existing !== void 0) return {
@@ -48,6 +51,8 @@ async function* convertGoogleGeminiStream(source, options = {}) {
48
51
  }
49
52
  const candidate = response.candidates?.[0];
50
53
  if (candidate?.finishReason) finishReason = mapGeminiFinishReason(candidate.finishReason);
54
+ if (candidate?.groundingMetadata) groundingMetadata = candidate.groundingMetadata;
55
+ if (candidate?.citationMetadata) citationMetadata = candidate.citationMetadata;
51
56
  const parts = candidate?.content?.parts;
52
57
  if (!parts) continue;
53
58
  let toolIdx = 0;
@@ -67,6 +72,7 @@ async function* convertGoogleGeminiStream(source, options = {}) {
67
72
  };
68
73
  const acc = blockAccumulators.get(index);
69
74
  acc.reasoning = (acc.reasoning ?? "") + part.text;
75
+ if (part.thoughtSignature) acc.thoughtSignature = part.thoughtSignature;
70
76
  yield {
71
77
  event: "content-block-delta",
72
78
  index,
@@ -104,22 +110,26 @@ async function* convertGoogleGeminiStream(source, options = {}) {
104
110
  const args = JSON.stringify(part.functionCall.args ?? {});
105
111
  const { index, isNew } = getOrCreateBlockIndex(key, {
106
112
  type: "tool_call_chunk",
113
+ id: part.functionCall.id ?? `lc-tool-call-${v4().replace(/-/g, "")}`,
107
114
  name: part.functionCall.name,
108
115
  args: "",
109
116
  index: toolIdx
110
117
  });
118
+ const acc = blockAccumulators.get(index);
119
+ const id = acc.id;
111
120
  if (isNew) yield {
112
121
  event: "content-block-start",
113
122
  index,
114
123
  content: {
115
124
  type: "tool_call_chunk",
125
+ id,
116
126
  name: part.functionCall.name,
117
127
  args: "",
118
128
  index: toolIdx
119
129
  }
120
130
  };
121
- const acc = blockAccumulators.get(index);
122
131
  acc.args = args;
132
+ if (part.thoughtSignature) acc.thoughtSignature = part.thoughtSignature;
123
133
  yield {
124
134
  event: "content-block-delta",
125
135
  index,
@@ -127,6 +137,7 @@ async function* convertGoogleGeminiStream(source, options = {}) {
127
137
  type: "block-delta",
128
138
  fields: {
129
139
  type: "tool_call_chunk",
140
+ id,
130
141
  name: acc.name,
131
142
  args: acc.args
132
143
  }
@@ -135,16 +146,29 @@ async function* convertGoogleGeminiStream(source, options = {}) {
135
146
  toolIdx += 1;
136
147
  }
137
148
  }
138
- for (const [index, acc] of blockAccumulators) yield {
139
- event: "content-block-finish",
140
- index,
141
- content: finalizeContentBlock(acc)
142
- };
149
+ for (const [index, acc] of blockAccumulators) {
150
+ const finalized = finalizeContentBlock(acc);
151
+ yield {
152
+ event: "content-block-finish",
153
+ index,
154
+ content: acc.thoughtSignature ? {
155
+ ...finalized,
156
+ thoughtSignature: acc.thoughtSignature
157
+ } : finalized
158
+ };
159
+ }
143
160
  yield {
144
161
  event: "message-finish",
145
162
  reason: finishReason,
146
163
  ...usageSnapshot ? { usage: usageSnapshot } : {},
147
- responseMetadata: { model_provider: "google" }
164
+ responseMetadata: {
165
+ model_provider: "google",
166
+ ...citationMetadata ? { citationMetadata } : {},
167
+ ...groundingMetadata ? {
168
+ groundingMetadata,
169
+ groundingSupport: groundingMetadata.groundingSupports?.filter((s) => (s?.segment?.partIndex ?? 0) === 0)
170
+ } : {}
171
+ }
148
172
  };
149
173
  }
150
174
  function mapGeminiFinishReason(reason) {
@@ -1 +1 @@
1
- {"version":3,"file":"stream_events.js","names":[],"sources":["../../src/utils/stream_events.ts"],"sourcesContent":["/**\n * Converts Gemini stream responses into LangChain ChatModelStreamEvents.\n *\n * @module\n */\n\nimport { finalizeContentBlock } from \"@langchain/core/language_models/compat\";\nimport type {\n ChatModelStreamEvent,\n FinishReason,\n} from \"@langchain/core/language_models/event\";\nimport type { ContentBlock, UsageMetadata } from \"@langchain/core/messages\";\nimport type { Gemini } from \"../chat_models/api-types.js\";\n\nexport type GeminiStreamResponse = Gemini.GenerateContentResponse;\n\nexport interface ConvertGoogleGeminiStreamOptions {\n streamUsage?: boolean;\n}\n\ntype BlockKey = \"text\" | \"reasoning\" | `tool:${number}`;\n\nexport async function* convertGoogleGeminiStream(\n source: AsyncIterable<GeminiStreamResponse>,\n options: ConvertGoogleGeminiStreamOptions = {}\n): AsyncGenerator<ChatModelStreamEvent> {\n const shouldStreamUsage = options.streamUsage ?? true;\n const blockAccumulators = new Map<\n number,\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n Record<string, any>\n >();\n const blockKeyToIndex = new Map<BlockKey, number>();\n let nextBlockIndex = 0;\n let messageStarted = false;\n let usageSnapshot: UsageMetadata | undefined;\n let finishReason: FinishReason = \"stop\";\n\n const getOrCreateBlockIndex = (\n key: BlockKey,\n initial: Record<string, unknown>\n ): { index: number; isNew: boolean } => {\n const existing = blockKeyToIndex.get(key);\n if (existing !== undefined) {\n return { index: existing, isNew: false };\n }\n const index = nextBlockIndex++;\n blockKeyToIndex.set(key, index);\n blockAccumulators.set(index, { ...initial });\n return { index, isNew: true };\n };\n\n for await (const response of source) {\n if (!messageStarted) {\n messageStarted = true;\n yield { event: \"message-start\" as const };\n }\n\n const usageMetadata = response.usageMetadata ?? response.usage_metadata;\n if (shouldStreamUsage && usageMetadata) {\n const input = usageMetadata.promptTokenCount ?? 0;\n const output = usageMetadata.candidatesTokenCount ?? 0;\n usageSnapshot = {\n input_tokens: input,\n output_tokens: output,\n total_tokens: usageMetadata.totalTokenCount ?? input + output,\n };\n yield { event: \"usage\" as const, usage: usageSnapshot };\n }\n\n const candidate = response.candidates?.[0];\n if (candidate?.finishReason) {\n finishReason = mapGeminiFinishReason(candidate.finishReason);\n }\n\n const parts = candidate?.content?.parts;\n if (!parts) continue;\n\n let toolIdx = 0;\n for (const part of parts) {\n if (part.text) {\n if (part.thought) {\n const key: BlockKey = \"reasoning\";\n const { index, isNew } = getOrCreateBlockIndex(key, {\n type: \"reasoning\",\n reasoning: \"\",\n });\n if (isNew) {\n yield {\n event: \"content-block-start\" as const,\n index,\n content: { type: \"reasoning\", reasoning: \"\" } as ContentBlock,\n };\n }\n const acc = blockAccumulators.get(index)!;\n acc.reasoning = (acc.reasoning ?? \"\") + part.text;\n yield {\n event: \"content-block-delta\" as const,\n index,\n delta: { type: \"reasoning-delta\" as const, reasoning: part.text },\n };\n } else {\n const key: BlockKey = \"text\";\n const { index, isNew } = getOrCreateBlockIndex(key, {\n type: \"text\",\n text: \"\",\n });\n if (isNew) {\n yield {\n event: \"content-block-start\" as const,\n index,\n content: { type: \"text\", text: \"\" } as ContentBlock,\n };\n }\n const acc = blockAccumulators.get(index)!;\n acc.text = (acc.text ?? \"\") + part.text;\n yield {\n event: \"content-block-delta\" as const,\n index,\n delta: { type: \"text-delta\" as const, text: part.text },\n };\n }\n } else if (part.functionCall) {\n const key: BlockKey = `tool:${toolIdx}`;\n const args = JSON.stringify(part.functionCall.args ?? {});\n const { index, isNew } = getOrCreateBlockIndex(key, {\n type: \"tool_call_chunk\",\n name: part.functionCall.name,\n args: \"\",\n index: toolIdx,\n });\n if (isNew) {\n yield {\n event: \"content-block-start\" as const,\n index,\n content: {\n type: \"tool_call_chunk\",\n name: part.functionCall.name,\n args: \"\",\n index: toolIdx,\n } as ContentBlock,\n };\n }\n const acc = blockAccumulators.get(index)!;\n acc.args = args;\n yield {\n event: \"content-block-delta\" as const,\n index,\n delta: {\n type: \"block-delta\" as const,\n fields: {\n type: \"tool_call_chunk\",\n name: acc.name,\n args: acc.args,\n },\n },\n };\n toolIdx += 1;\n }\n }\n }\n\n for (const [index, acc] of blockAccumulators) {\n yield {\n event: \"content-block-finish\" as const,\n index,\n content: finalizeContentBlock(acc as ContentBlock),\n };\n }\n\n yield {\n event: \"message-finish\" as const,\n reason: finishReason,\n ...(usageSnapshot ? { usage: usageSnapshot } : {}),\n responseMetadata: { model_provider: \"google\" },\n };\n}\n\nfunction mapGeminiFinishReason(reason: string): FinishReason {\n switch (reason.toLowerCase()) {\n case \"max_tokens\":\n case \"max-token\":\n case \"max_token\":\n return \"length\";\n case \"safety\":\n case \"recitation\":\n case \"language\":\n case \"blocklist\":\n case \"prohibited_content\":\n case \"prohibited-content\":\n case \"spii\":\n case \"image_safety\":\n case \"image-safety\":\n case \"image_prohibited_content\":\n case \"image-prohibited-content\":\n case \"image_recitation\":\n case \"image-recitation\":\n return \"content_filter\";\n default:\n return \"stop\";\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"stream_events.js","names":["uuidv4"],"sources":["../../src/utils/stream_events.ts"],"sourcesContent":["/**\n * Converts Gemini stream responses into LangChain ChatModelStreamEvents.\n *\n * @module\n */\n\nimport { finalizeContentBlock } from \"@langchain/core/language_models/compat\";\nimport type {\n ChatModelStreamEvent,\n FinishReason,\n} from \"@langchain/core/language_models/event\";\nimport type { ContentBlock, UsageMetadata } from \"@langchain/core/messages\";\nimport { v4 as uuidv4 } from \"@langchain/core/utils/uuid\";\nimport type { Gemini } from \"../chat_models/api-types.js\";\n\nexport type GeminiStreamResponse = Gemini.GenerateContentResponse;\n\nexport interface ConvertGoogleGeminiStreamOptions {\n streamUsage?: boolean;\n}\n\ntype BlockKey = \"text\" | \"reasoning\" | `tool:${number}`;\n\nexport async function* convertGoogleGeminiStream(\n source: AsyncIterable<GeminiStreamResponse>,\n options: ConvertGoogleGeminiStreamOptions = {}\n): AsyncGenerator<ChatModelStreamEvent> {\n const shouldStreamUsage = options.streamUsage ?? true;\n const blockAccumulators = new Map<\n number,\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n Record<string, any>\n >();\n const blockKeyToIndex = new Map<BlockKey, number>();\n let nextBlockIndex = 0;\n let messageStarted = false;\n let usageSnapshot: UsageMetadata | undefined;\n let finishReason: FinishReason = \"stop\";\n let groundingMetadata: Gemini.GroundingMetadata | undefined;\n let citationMetadata: Gemini.CitationMetadata | undefined;\n\n const getOrCreateBlockIndex = (\n key: BlockKey,\n initial: Record<string, unknown>\n ): { index: number; isNew: boolean } => {\n const existing = blockKeyToIndex.get(key);\n if (existing !== undefined) {\n return { index: existing, isNew: false };\n }\n const index = nextBlockIndex++;\n blockKeyToIndex.set(key, index);\n blockAccumulators.set(index, { ...initial });\n return { index, isNew: true };\n };\n\n for await (const response of source) {\n if (!messageStarted) {\n messageStarted = true;\n yield { event: \"message-start\" as const };\n }\n\n const usageMetadata = response.usageMetadata ?? response.usage_metadata;\n if (shouldStreamUsage && usageMetadata) {\n const input = usageMetadata.promptTokenCount ?? 0;\n const output = usageMetadata.candidatesTokenCount ?? 0;\n usageSnapshot = {\n input_tokens: input,\n output_tokens: output,\n total_tokens: usageMetadata.totalTokenCount ?? input + output,\n };\n yield { event: \"usage\" as const, usage: usageSnapshot };\n }\n\n const candidate = response.candidates?.[0];\n if (candidate?.finishReason) {\n finishReason = mapGeminiFinishReason(candidate.finishReason);\n }\n if (candidate?.groundingMetadata) {\n groundingMetadata = candidate.groundingMetadata;\n }\n if (candidate?.citationMetadata) {\n citationMetadata = candidate.citationMetadata;\n }\n\n const parts = candidate?.content?.parts;\n if (!parts) continue;\n\n let toolIdx = 0;\n for (const part of parts) {\n if (part.text) {\n if (part.thought) {\n const key: BlockKey = \"reasoning\";\n const { index, isNew } = getOrCreateBlockIndex(key, {\n type: \"reasoning\",\n reasoning: \"\",\n });\n if (isNew) {\n yield {\n event: \"content-block-start\" as const,\n index,\n content: { type: \"reasoning\", reasoning: \"\" } as ContentBlock,\n };\n }\n const acc = blockAccumulators.get(index)!;\n acc.reasoning = (acc.reasoning ?? \"\") + part.text;\n if (part.thoughtSignature)\n acc.thoughtSignature = part.thoughtSignature;\n yield {\n event: \"content-block-delta\" as const,\n index,\n delta: { type: \"reasoning-delta\" as const, reasoning: part.text },\n };\n } else {\n const key: BlockKey = \"text\";\n const { index, isNew } = getOrCreateBlockIndex(key, {\n type: \"text\",\n text: \"\",\n });\n if (isNew) {\n yield {\n event: \"content-block-start\" as const,\n index,\n content: { type: \"text\", text: \"\" } as ContentBlock,\n };\n }\n const acc = blockAccumulators.get(index)!;\n acc.text = (acc.text ?? \"\") + part.text;\n yield {\n event: \"content-block-delta\" as const,\n index,\n delta: { type: \"text-delta\" as const, text: part.text },\n };\n }\n } else if (part.functionCall) {\n const key: BlockKey = `tool:${toolIdx}`;\n const args = JSON.stringify(part.functionCall.args ?? {});\n // Only used to seed a *new* block; an id already assigned to this\n // block must not change on later chunks.\n const candidateId =\n part.functionCall.id ?? `lc-tool-call-${uuidv4().replace(/-/g, \"\")}`;\n const { index, isNew } = getOrCreateBlockIndex(key, {\n type: \"tool_call_chunk\",\n id: candidateId,\n name: part.functionCall.name,\n args: \"\",\n index: toolIdx,\n });\n const acc = blockAccumulators.get(index)!;\n const id = acc.id as string;\n if (isNew) {\n yield {\n event: \"content-block-start\" as const,\n index,\n content: {\n type: \"tool_call_chunk\",\n id,\n name: part.functionCall.name,\n args: \"\",\n index: toolIdx,\n } as ContentBlock,\n };\n }\n acc.args = args;\n if (part.thoughtSignature) acc.thoughtSignature = part.thoughtSignature;\n yield {\n event: \"content-block-delta\" as const,\n index,\n delta: {\n type: \"block-delta\" as const,\n fields: {\n type: \"tool_call_chunk\",\n id,\n name: acc.name,\n args: acc.args,\n },\n },\n };\n toolIdx += 1;\n }\n }\n }\n\n for (const [index, acc] of blockAccumulators) {\n // finalizeContentBlock rebuilds tool_call_chunk -> tool_call as\n // {type, id, name, args} only, so thoughtSignature has to be re-attached.\n const finalized = finalizeContentBlock(acc as ContentBlock);\n yield {\n event: \"content-block-finish\" as const,\n index,\n content: acc.thoughtSignature\n ? ({\n ...finalized,\n thoughtSignature: acc.thoughtSignature,\n } as ContentBlock)\n : finalized,\n };\n }\n\n yield {\n event: \"message-finish\" as const,\n reason: finishReason,\n ...(usageSnapshot ? { usage: usageSnapshot } : {}),\n responseMetadata: {\n model_provider: \"google\",\n ...(citationMetadata ? { citationMetadata } : {}),\n ...(groundingMetadata\n ? {\n groundingMetadata,\n // Support entries for the first content part only (matches messages.ts).\n groundingSupport: groundingMetadata.groundingSupports?.filter(\n (s) => (s?.segment?.partIndex ?? 0) === 0\n ),\n }\n : {}),\n },\n };\n}\n\nfunction mapGeminiFinishReason(reason: string): FinishReason {\n switch (reason.toLowerCase()) {\n case \"max_tokens\":\n case \"max-token\":\n case \"max_token\":\n return \"length\";\n case \"safety\":\n case \"recitation\":\n case \"language\":\n case \"blocklist\":\n case \"prohibited_content\":\n case \"prohibited-content\":\n case \"spii\":\n case \"image_safety\":\n case \"image-safety\":\n case \"image_prohibited_content\":\n case \"image-prohibited-content\":\n case \"image_recitation\":\n case \"image-recitation\":\n return \"content_filter\";\n default:\n return \"stop\";\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@langchain/google",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Google integrations for LangChain.js",
5
5
  "author": "LangChain",
6
6
  "license": "MIT",
@@ -19,7 +19,7 @@
19
19
  "jose": "^6.2.10"
20
20
  },
21
21
  "peerDependencies": {
22
- "@langchain/core": "^1.2.10"
22
+ "@langchain/core": "^1.2.12"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@tsconfig/recommended": "^1.0.10",
@@ -27,10 +27,10 @@
27
27
  "dpdm": "^4.3.0",
28
28
  "sparktype": "^0.1.3",
29
29
  "typescript": "~7.0.2",
30
- "vitest": "^4.1.8",
30
+ "vitest": "^4.1.11",
31
31
  "zod": "^3.25.76 || ^4",
32
32
  "zod-to-json-schema": "^3.25.2",
33
- "@langchain/core": "^1.2.10",
33
+ "@langchain/core": "^1.2.12",
34
34
  "@langchain/standard-tests": "0.0.23"
35
35
  },
36
36
  "publishConfig": {