@langchain/google 0.2.3 → 0.2.4
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.
- package/CHANGELOG.md +18 -0
- package/dist/chat_models/base.cjs +1 -1
- package/dist/chat_models/base.cjs.map +1 -1
- package/dist/chat_models/base.js +1 -1
- package/dist/chat_models/index.cjs.map +1 -1
- package/dist/chat_models/node.cjs +2 -1
- package/dist/chat_models/node.cjs.map +1 -1
- package/dist/chat_models/node.js +2 -1
- package/dist/chat_models/node.js.map +1 -1
- package/dist/clients/index.cjs.map +1 -1
- package/dist/clients/node.cjs.map +1 -1
- package/dist/converters/messages.cjs +75 -29
- package/dist/converters/messages.cjs.map +1 -1
- package/dist/converters/messages.js +75 -29
- package/dist/converters/messages.js.map +1 -1
- package/dist/converters/params.cjs +14 -10
- package/dist/converters/params.cjs.map +1 -1
- package/dist/converters/params.js +14 -10
- package/dist/converters/params.js.map +1 -1
- package/dist/converters/tools.cjs +8 -5
- package/dist/converters/tools.cjs.map +1 -1
- package/dist/converters/tools.js +8 -5
- package/dist/converters/tools.js.map +1 -1
- package/dist/utils/gcp-auth.cjs +13 -11
- package/dist/utils/gcp-auth.cjs.map +1 -1
- package/dist/utils/gcp-auth.js +13 -11
- package/dist/utils/gcp-auth.js.map +1 -1
- package/dist/utils/stream_events.cjs +45 -44
- package/dist/utils/stream_events.cjs.map +1 -1
- package/dist/utils/stream_events.js +45 -44
- package/dist/utils/stream_events.js.map +1 -1
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tools.cjs","names":["InvalidInputError","InvalidToolError"],"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":"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"}
|
|
1
|
+
{"version":3,"file":"tools.cjs","names":["InvalidInputError","isInteropZodSchema","isSerializableSchema","toJsonSchema","isLangChainTool","isOpenAITool","InvalidToolError"],"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,IAAIA,eAAAA,kBACR,0GACF;MACK,IAAI,QAAQ,GAGjB,IAAI,OAAO,KAAK,KAAK;MAChB,IAAI,QAAQ,GAEjB,MAAM,IAAIA,eAAAA,kBACR,2FACF;MAGA,MAAM,IAAIA,eAAAA,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,4BAAA,GACjBC,4BAAAA,mBAAAA,CAAmB,MAAM,MAAA,GAAKC,sCAAAA,qBAAAA,CAAqB,MAAM,KAAA,GACrDC,kCAAAA,aAAAA,CAAa,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,KAAA,GAAIC,uCAAAA,gBAAAA,CAAgB,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,KAAA,GAAIC,qCAAAA,aAAAA,CAAa,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,IAAIC,eAAAA,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"}
|
package/dist/converters/tools.js
CHANGED
|
@@ -257,9 +257,10 @@ const convertToolsToGeminiTools = (tools) => {
|
|
|
257
257
|
if (tools.length === 0) return [];
|
|
258
258
|
const geminiTools = [];
|
|
259
259
|
const functionDeclarations = [];
|
|
260
|
-
for (const tool of tools) if (isGeminiTool(tool))
|
|
261
|
-
|
|
262
|
-
|
|
260
|
+
for (const tool of tools) if (isGeminiTool(tool)) {
|
|
261
|
+
if ("functionDeclarations" in tool) geminiTools.push(tool);
|
|
262
|
+
else geminiTools.push(tool);
|
|
263
|
+
} else if ("googleSearchRetrieval" in tool) geminiTools.push({ googleSearch: {} });
|
|
263
264
|
else {
|
|
264
265
|
const funcDecls = convertToolsToGeminiFunctionDeclarations([tool]);
|
|
265
266
|
functionDeclarations.push(...funcDecls);
|
|
@@ -335,8 +336,10 @@ function convertToolChoiceToGeminiConfig(toolChoice, hasTools) {
|
|
|
335
336
|
mode = "ANY";
|
|
336
337
|
toolChoiceFunction = toolChoiceMode;
|
|
337
338
|
}
|
|
338
|
-
if (toolChoiceFunction)
|
|
339
|
-
|
|
339
|
+
if (toolChoiceFunction) {
|
|
340
|
+
if (Array.isArray(toolChoiceFunction)) allowedFunctionNames = toolChoiceFunction;
|
|
341
|
+
else allowedFunctionNames = [toolChoiceFunction];
|
|
342
|
+
}
|
|
340
343
|
const functionCallingConfig = {};
|
|
341
344
|
if (allowedFunctionNames?.length) functionCallingConfig.allowedFunctionNames = allowedFunctionNames;
|
|
342
345
|
if (mode) functionCallingConfig.mode = mode;
|
|
@@ -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":"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"}
|
|
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":"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"}
|
package/dist/utils/gcp-auth.cjs
CHANGED
|
@@ -146,18 +146,20 @@ async function getGCPCredentialsAccessToken(credentials) {
|
|
|
146
146
|
const cacheKey = cacheKeyUrl.toString();
|
|
147
147
|
const cacheValue = cache.get(cacheKey);
|
|
148
148
|
let now = Math.floor(Date.now() / 1e3);
|
|
149
|
-
if (cacheValue)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
149
|
+
if (cacheValue) {
|
|
150
|
+
if (cacheValue.created > now - CACHE_MAX_AGE) {
|
|
151
|
+
let token = await cacheValue.promise;
|
|
152
|
+
if (token.expires > now) return token.token;
|
|
153
|
+
else {
|
|
154
|
+
const nextValue = cache.get(cacheKey);
|
|
155
|
+
if (nextValue && nextValue !== cacheValue) {
|
|
156
|
+
token = await nextValue.promise;
|
|
157
|
+
if (token.expires > now) return token.token;
|
|
158
|
+
else cache.delete(cacheKey);
|
|
159
|
+
}
|
|
158
160
|
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
+
} else cache.delete(cacheKey);
|
|
162
|
+
}
|
|
161
163
|
const promise = require_misc.iife(async () => {
|
|
162
164
|
now = Math.floor(Date.now() / 1e3);
|
|
163
165
|
const jwt = await getGCPCustomToken(credentials);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gcp-auth.cjs","names":["iife","AuthError"],"sources":["../../src/utils/gcp-auth.ts"],"sourcesContent":["/**\n * GCP authentication utilities.\n *\n * Adapted from: https://github.com/kriasoft/web-auth-library\n * Original source: web-auth-library by Kriasoft\n * License: MIT (https://github.com/kriasoft/web-auth-library/blob/main/LICENSE)\n */\n\nimport { AuthError } from \"../utils/errors.js\";\nimport { iife } from \"../utils/misc.js\";\n\ntype JoseModule = typeof import(\"jose\");\n\nlet joseModulePromise: Promise<JoseModule> | undefined;\n\nasync function getJose(): Promise<JoseModule> {\n joseModulePromise ??= import(\"jose\");\n return joseModulePromise;\n}\n\n/**\n * Google Cloud Platform service account credentials interface.\n *\n * This interface represents the structure of a GCP service account key file,\n * which is used for authenticating with Google Cloud APIs. Service account\n * keys can be downloaded from the Google Cloud Console and contain all the\n * information needed to authenticate as a service account.\n *\n * @see https://cloud.google.com/iam/docs/creating-managing-service-account-keys\n */\nexport interface GCPCredentials {\n /** The type of credential, typically \"service_account\" */\n type: string;\n /** The GCP project ID associated with this service account */\n project_id: string;\n /** The unique identifier for the private key */\n private_key_id: string;\n /** The RSA private key in PEM format used for signing JWTs */\n private_key: string;\n /** The unique client ID for this service account */\n client_id: string;\n /** The email address of the service account */\n client_email: string;\n /** The OAuth2 authorization endpoint URI */\n auth_uri: string;\n /** The OAuth2 token endpoint URI where access tokens are requested */\n token_uri: string;\n /** The URL for the OAuth2 provider's x509 certificate */\n auth_provider_x509_cert_url: string;\n /** The URL for this service account's x509 certificate */\n client_x509_cert_url: string;\n}\n\ninterface CacheValue {\n /** Unix timestamp (in seconds) when this cache entry was created */\n created: number;\n /** Promise that resolves to the token and its expiration time */\n promise: Promise<{ token: string; expires: number }>;\n}\n\nconst cache = new Map<string, CacheValue>();\nconst CACHE_MAX_AGE = 60 * 60; // 1 hour\n\n/**\n * Normalizes GCP credentials to a frozen GCPCredentials object.\n *\n * This function accepts credentials in either string (JSON) or object format\n * and ensures the returned value is a frozen (immutable) GCPCredentials object.\n * Freezing the credentials prevents accidental modification and ensures\n * consistency throughout the application lifecycle.\n *\n * @param credentials - The GCP service account credentials, either as a JSON string\n * or as a GCPCredentials object\n * @returns A frozen GCPCredentials object that cannot be modified\n *\n * @example\n * ```typescript\n * // From JSON string\n * const creds = normalizeGCPCredentials('{\"type\":\"service_account\",...}');\n *\n * // From object\n * const creds = normalizeGCPCredentials({\n * type: \"service_account\",\n * project_id: \"my-project\",\n * // ... other fields\n * });\n * ```\n */\nexport function normalizeGCPCredentials(\n credentials: string | GCPCredentials\n): GCPCredentials {\n return typeof credentials === \"string\"\n ? Object.freeze(JSON.parse(credentials))\n : Object.isFrozen(credentials)\n ? credentials\n : Object.freeze(credentials);\n}\n\n/**\n * Imports and returns the private key from GCP credentials for JWT signing.\n *\n * This function extracts the RSA private key from the service account credentials\n * and imports it using the PKCS#8 format with the RS256 algorithm. The returned\n * key can be used to sign JWTs for authentication with Google Cloud services.\n *\n * @param credentials - The GCP service account credentials containing the private key\n * @returns A Promise that resolves to the imported private key suitable for JWT signing\n *\n * @throws {Error} If the private key format is invalid or cannot be imported\n *\n * @example\n * ```typescript\n * const credentials = normalizeGCPCredentials(credentialsJson);\n * const privateKey = await getGCPPrivateKey(credentials);\n * // Use privateKey for signing operations\n * ```\n */\nexport async function getGCPPrivateKey(credentials: GCPCredentials) {\n const { importPKCS8 } = await getJose();\n return importPKCS8(credentials.private_key, \"RS256\");\n}\n\n/**\n * Generates a custom JWT token for authenticating with Google Cloud services.\n *\n * This function creates a signed JWT (JSON Web Token) using the service account's\n * private key. The token includes standard claims such as issuer, audience, subject,\n * issued-at time, and expiration time. This custom token can be exchanged for an\n * access token or ID token through Google's OAuth2 token endpoint.\n *\n * The generated JWT is valid for 1 hour and follows the OAuth 2.0 JWT Bearer\n * Token flow as specified by RFC 7523.\n *\n * @param credentials - The GCP service account credentials used to sign the JWT\n * @returns A Promise that resolves to the signed JWT string\n *\n * @throws {Error} If the private key cannot be imported or the JWT cannot be signed\n *\n * @see https://datatracker.ietf.org/doc/html/rfc7523\n * @see https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials\n *\n * @example\n * ```typescript\n * const credentials = normalizeGCPCredentials(credentialsJson);\n * const customToken = await getGCPCustomToken(credentials);\n * // Use customToken to request an access token\n * ```\n */\nexport async function getGCPCustomToken(credentials: GCPCredentials) {\n const { SignJWT } = await getJose();\n const privateKey = await getGCPPrivateKey(credentials);\n const customToken = await new SignJWT()\n .setIssuer(credentials.client_email)\n .setAudience(credentials.token_uri)\n .setSubject(credentials.client_email)\n .setProtectedHeader({ alg: \"RS256\" })\n .setIssuedAt()\n .setExpirationTime(\"1h\")\n .sign(privateKey);\n return customToken;\n}\n\n/**\n * Retrieves an access token for authenticating with Google Cloud APIs.\n *\n * - Tokens are cached based on the token URI and private key ID\n * - Cache entries are valid for up to 1 hour\n * - Expired tokens trigger automatic refresh\n * - Multiple concurrent requests for the same token are deduplicated\n *\n * The function handles both access tokens and ID tokens, automatically detecting\n * which type is returned by the token endpoint and extracting the appropriate\n * expiration time.\n *\n * @param credentials - The GCP service account credentials used to request the token\n * @returns A Promise that resolves to the access token string\n *\n * @throws {AuthError} If the token request fails (e.g., invalid credentials,\n * network error, or API error)\n *\n * @see https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials\n * @see https://datatracker.ietf.org/doc/html/rfc7523\n *\n * @example\n * ```typescript\n * const credentials = normalizeGCPCredentials(credentialsJson);\n * const accessToken = await getGCPCredentialsAccessToken(credentials);\n *\n * // Use the access token in API requests\n * const response = await fetch('https://api.google.com/...', {\n * headers: {\n * 'Authorization': `Bearer ${accessToken}`\n * }\n * });\n * ```\n *\n * @remarks\n * The caching mechanism uses a two-level validation:\n * 1. Cache entry age (must be less than CACHE_MAX_AGE)\n * 2. Token expiration time (must not be expired)\n *\n * This ensures that even if a cache entry is fresh, an expired token\n * will trigger a refresh request.\n */\nexport async function getGCPCredentialsAccessToken(\n credentials: GCPCredentials\n): Promise<string> {\n const tokenUrl = credentials.token_uri;\n\n const cacheKeyUrl = new URL(tokenUrl);\n cacheKeyUrl.searchParams.set(\"key\", credentials.private_key_id);\n const cacheKey = cacheKeyUrl.toString();\n\n // Attempt to retrieve the token from the cache\n const cacheValue = cache.get(cacheKey);\n let now = Math.floor(Date.now() / 1000);\n\n if (cacheValue) {\n if (cacheValue.created > now - CACHE_MAX_AGE) {\n let token = await cacheValue.promise;\n\n if (token.expires > now) {\n return token.token;\n } else {\n const nextValue = cache.get(cacheKey);\n\n if (nextValue && nextValue !== cacheValue) {\n token = await nextValue.promise;\n if (token.expires > now) {\n return token.token;\n } else {\n cache.delete(cacheKey);\n }\n }\n }\n } else {\n cache.delete(cacheKey);\n }\n }\n\n const promise = iife(async () => {\n now = Math.floor(Date.now() / 1000);\n\n // Request a new token from the Google Cloud API\n const jwt = await getGCPCustomToken(credentials);\n const body = new URLSearchParams();\n body.append(\"grant_type\", \"urn:ietf:params:oauth:grant-type:jwt-bearer\");\n body.append(\"assertion\", jwt);\n const res = await fetch(tokenUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body,\n });\n\n if (!res.ok) {\n throw await AuthError.fromResponse(res);\n }\n\n const data = await res.json();\n\n if (\"id_token\" in data) {\n const { decodeJwt } = await getJose();\n const claims = decodeJwt(data.id_token);\n return { token: data.id_token, expires: claims.exp as number };\n }\n\n const lastModified = res.headers.get(\"last-modified\");\n const expires = lastModified\n ? Math.floor(new Date(lastModified).valueOf() / 1000) + data.expires_in\n : now + data.expires_in;\n\n return { expires, token: data.access_token };\n });\n\n cache.set(cacheKey, { created: now, promise });\n return await promise.then((data) => data.token);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"gcp-auth.cjs","names":["iife","AuthError"],"sources":["../../src/utils/gcp-auth.ts"],"sourcesContent":["/**\n * GCP authentication utilities.\n *\n * Adapted from: https://github.com/kriasoft/web-auth-library\n * Original source: web-auth-library by Kriasoft\n * License: MIT (https://github.com/kriasoft/web-auth-library/blob/main/LICENSE)\n */\n\nimport { AuthError } from \"../utils/errors.js\";\nimport { iife } from \"../utils/misc.js\";\n\ntype JoseModule = typeof import(\"jose\");\n\nlet joseModulePromise: Promise<JoseModule> | undefined;\n\nasync function getJose(): Promise<JoseModule> {\n joseModulePromise ??= import(\"jose\");\n return joseModulePromise;\n}\n\n/**\n * Google Cloud Platform service account credentials interface.\n *\n * This interface represents the structure of a GCP service account key file,\n * which is used for authenticating with Google Cloud APIs. Service account\n * keys can be downloaded from the Google Cloud Console and contain all the\n * information needed to authenticate as a service account.\n *\n * @see https://cloud.google.com/iam/docs/creating-managing-service-account-keys\n */\nexport interface GCPCredentials {\n /** The type of credential, typically \"service_account\" */\n type: string;\n /** The GCP project ID associated with this service account */\n project_id: string;\n /** The unique identifier for the private key */\n private_key_id: string;\n /** The RSA private key in PEM format used for signing JWTs */\n private_key: string;\n /** The unique client ID for this service account */\n client_id: string;\n /** The email address of the service account */\n client_email: string;\n /** The OAuth2 authorization endpoint URI */\n auth_uri: string;\n /** The OAuth2 token endpoint URI where access tokens are requested */\n token_uri: string;\n /** The URL for the OAuth2 provider's x509 certificate */\n auth_provider_x509_cert_url: string;\n /** The URL for this service account's x509 certificate */\n client_x509_cert_url: string;\n}\n\ninterface CacheValue {\n /** Unix timestamp (in seconds) when this cache entry was created */\n created: number;\n /** Promise that resolves to the token and its expiration time */\n promise: Promise<{ token: string; expires: number }>;\n}\n\nconst cache = new Map<string, CacheValue>();\nconst CACHE_MAX_AGE = 60 * 60; // 1 hour\n\n/**\n * Normalizes GCP credentials to a frozen GCPCredentials object.\n *\n * This function accepts credentials in either string (JSON) or object format\n * and ensures the returned value is a frozen (immutable) GCPCredentials object.\n * Freezing the credentials prevents accidental modification and ensures\n * consistency throughout the application lifecycle.\n *\n * @param credentials - The GCP service account credentials, either as a JSON string\n * or as a GCPCredentials object\n * @returns A frozen GCPCredentials object that cannot be modified\n *\n * @example\n * ```typescript\n * // From JSON string\n * const creds = normalizeGCPCredentials('{\"type\":\"service_account\",...}');\n *\n * // From object\n * const creds = normalizeGCPCredentials({\n * type: \"service_account\",\n * project_id: \"my-project\",\n * // ... other fields\n * });\n * ```\n */\nexport function normalizeGCPCredentials(\n credentials: string | GCPCredentials\n): GCPCredentials {\n return typeof credentials === \"string\"\n ? Object.freeze(JSON.parse(credentials))\n : Object.isFrozen(credentials)\n ? credentials\n : Object.freeze(credentials);\n}\n\n/**\n * Imports and returns the private key from GCP credentials for JWT signing.\n *\n * This function extracts the RSA private key from the service account credentials\n * and imports it using the PKCS#8 format with the RS256 algorithm. The returned\n * key can be used to sign JWTs for authentication with Google Cloud services.\n *\n * @param credentials - The GCP service account credentials containing the private key\n * @returns A Promise that resolves to the imported private key suitable for JWT signing\n *\n * @throws {Error} If the private key format is invalid or cannot be imported\n *\n * @example\n * ```typescript\n * const credentials = normalizeGCPCredentials(credentialsJson);\n * const privateKey = await getGCPPrivateKey(credentials);\n * // Use privateKey for signing operations\n * ```\n */\nexport async function getGCPPrivateKey(credentials: GCPCredentials) {\n const { importPKCS8 } = await getJose();\n return importPKCS8(credentials.private_key, \"RS256\");\n}\n\n/**\n * Generates a custom JWT token for authenticating with Google Cloud services.\n *\n * This function creates a signed JWT (JSON Web Token) using the service account's\n * private key. The token includes standard claims such as issuer, audience, subject,\n * issued-at time, and expiration time. This custom token can be exchanged for an\n * access token or ID token through Google's OAuth2 token endpoint.\n *\n * The generated JWT is valid for 1 hour and follows the OAuth 2.0 JWT Bearer\n * Token flow as specified by RFC 7523.\n *\n * @param credentials - The GCP service account credentials used to sign the JWT\n * @returns A Promise that resolves to the signed JWT string\n *\n * @throws {Error} If the private key cannot be imported or the JWT cannot be signed\n *\n * @see https://datatracker.ietf.org/doc/html/rfc7523\n * @see https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials\n *\n * @example\n * ```typescript\n * const credentials = normalizeGCPCredentials(credentialsJson);\n * const customToken = await getGCPCustomToken(credentials);\n * // Use customToken to request an access token\n * ```\n */\nexport async function getGCPCustomToken(credentials: GCPCredentials) {\n const { SignJWT } = await getJose();\n const privateKey = await getGCPPrivateKey(credentials);\n const customToken = await new SignJWT()\n .setIssuer(credentials.client_email)\n .setAudience(credentials.token_uri)\n .setSubject(credentials.client_email)\n .setProtectedHeader({ alg: \"RS256\" })\n .setIssuedAt()\n .setExpirationTime(\"1h\")\n .sign(privateKey);\n return customToken;\n}\n\n/**\n * Retrieves an access token for authenticating with Google Cloud APIs.\n *\n * - Tokens are cached based on the token URI and private key ID\n * - Cache entries are valid for up to 1 hour\n * - Expired tokens trigger automatic refresh\n * - Multiple concurrent requests for the same token are deduplicated\n *\n * The function handles both access tokens and ID tokens, automatically detecting\n * which type is returned by the token endpoint and extracting the appropriate\n * expiration time.\n *\n * @param credentials - The GCP service account credentials used to request the token\n * @returns A Promise that resolves to the access token string\n *\n * @throws {AuthError} If the token request fails (e.g., invalid credentials,\n * network error, or API error)\n *\n * @see https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials\n * @see https://datatracker.ietf.org/doc/html/rfc7523\n *\n * @example\n * ```typescript\n * const credentials = normalizeGCPCredentials(credentialsJson);\n * const accessToken = await getGCPCredentialsAccessToken(credentials);\n *\n * // Use the access token in API requests\n * const response = await fetch('https://api.google.com/...', {\n * headers: {\n * 'Authorization': `Bearer ${accessToken}`\n * }\n * });\n * ```\n *\n * @remarks\n * The caching mechanism uses a two-level validation:\n * 1. Cache entry age (must be less than CACHE_MAX_AGE)\n * 2. Token expiration time (must not be expired)\n *\n * This ensures that even if a cache entry is fresh, an expired token\n * will trigger a refresh request.\n */\nexport async function getGCPCredentialsAccessToken(\n credentials: GCPCredentials\n): Promise<string> {\n const tokenUrl = credentials.token_uri;\n\n const cacheKeyUrl = new URL(tokenUrl);\n cacheKeyUrl.searchParams.set(\"key\", credentials.private_key_id);\n const cacheKey = cacheKeyUrl.toString();\n\n // Attempt to retrieve the token from the cache\n const cacheValue = cache.get(cacheKey);\n let now = Math.floor(Date.now() / 1000);\n\n if (cacheValue) {\n if (cacheValue.created > now - CACHE_MAX_AGE) {\n let token = await cacheValue.promise;\n\n if (token.expires > now) {\n return token.token;\n } else {\n const nextValue = cache.get(cacheKey);\n\n if (nextValue && nextValue !== cacheValue) {\n token = await nextValue.promise;\n if (token.expires > now) {\n return token.token;\n } else {\n cache.delete(cacheKey);\n }\n }\n }\n } else {\n cache.delete(cacheKey);\n }\n }\n\n const promise = iife(async () => {\n now = Math.floor(Date.now() / 1000);\n\n // Request a new token from the Google Cloud API\n const jwt = await getGCPCustomToken(credentials);\n const body = new URLSearchParams();\n body.append(\"grant_type\", \"urn:ietf:params:oauth:grant-type:jwt-bearer\");\n body.append(\"assertion\", jwt);\n const res = await fetch(tokenUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body,\n });\n\n if (!res.ok) {\n throw await AuthError.fromResponse(res);\n }\n\n const data = await res.json();\n\n if (\"id_token\" in data) {\n const { decodeJwt } = await getJose();\n const claims = decodeJwt(data.id_token);\n return { token: data.id_token, expires: claims.exp as number };\n }\n\n const lastModified = res.headers.get(\"last-modified\");\n const expires = lastModified\n ? Math.floor(new Date(lastModified).valueOf() / 1000) + data.expires_in\n : now + data.expires_in;\n\n return { expires, token: data.access_token };\n });\n\n cache.set(cacheKey, { created: now, promise });\n return await promise.then((data) => data.token);\n}\n"],"mappings":"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"}
|
package/dist/utils/gcp-auth.js
CHANGED
|
@@ -146,18 +146,20 @@ async function getGCPCredentialsAccessToken(credentials) {
|
|
|
146
146
|
const cacheKey = cacheKeyUrl.toString();
|
|
147
147
|
const cacheValue = cache.get(cacheKey);
|
|
148
148
|
let now = Math.floor(Date.now() / 1e3);
|
|
149
|
-
if (cacheValue)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
149
|
+
if (cacheValue) {
|
|
150
|
+
if (cacheValue.created > now - CACHE_MAX_AGE) {
|
|
151
|
+
let token = await cacheValue.promise;
|
|
152
|
+
if (token.expires > now) return token.token;
|
|
153
|
+
else {
|
|
154
|
+
const nextValue = cache.get(cacheKey);
|
|
155
|
+
if (nextValue && nextValue !== cacheValue) {
|
|
156
|
+
token = await nextValue.promise;
|
|
157
|
+
if (token.expires > now) return token.token;
|
|
158
|
+
else cache.delete(cacheKey);
|
|
159
|
+
}
|
|
158
160
|
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
+
} else cache.delete(cacheKey);
|
|
162
|
+
}
|
|
161
163
|
const promise = iife(async () => {
|
|
162
164
|
now = Math.floor(Date.now() / 1e3);
|
|
163
165
|
const jwt = await getGCPCustomToken(credentials);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gcp-auth.js","names":[],"sources":["../../src/utils/gcp-auth.ts"],"sourcesContent":["/**\n * GCP authentication utilities.\n *\n * Adapted from: https://github.com/kriasoft/web-auth-library\n * Original source: web-auth-library by Kriasoft\n * License: MIT (https://github.com/kriasoft/web-auth-library/blob/main/LICENSE)\n */\n\nimport { AuthError } from \"../utils/errors.js\";\nimport { iife } from \"../utils/misc.js\";\n\ntype JoseModule = typeof import(\"jose\");\n\nlet joseModulePromise: Promise<JoseModule> | undefined;\n\nasync function getJose(): Promise<JoseModule> {\n joseModulePromise ??= import(\"jose\");\n return joseModulePromise;\n}\n\n/**\n * Google Cloud Platform service account credentials interface.\n *\n * This interface represents the structure of a GCP service account key file,\n * which is used for authenticating with Google Cloud APIs. Service account\n * keys can be downloaded from the Google Cloud Console and contain all the\n * information needed to authenticate as a service account.\n *\n * @see https://cloud.google.com/iam/docs/creating-managing-service-account-keys\n */\nexport interface GCPCredentials {\n /** The type of credential, typically \"service_account\" */\n type: string;\n /** The GCP project ID associated with this service account */\n project_id: string;\n /** The unique identifier for the private key */\n private_key_id: string;\n /** The RSA private key in PEM format used for signing JWTs */\n private_key: string;\n /** The unique client ID for this service account */\n client_id: string;\n /** The email address of the service account */\n client_email: string;\n /** The OAuth2 authorization endpoint URI */\n auth_uri: string;\n /** The OAuth2 token endpoint URI where access tokens are requested */\n token_uri: string;\n /** The URL for the OAuth2 provider's x509 certificate */\n auth_provider_x509_cert_url: string;\n /** The URL for this service account's x509 certificate */\n client_x509_cert_url: string;\n}\n\ninterface CacheValue {\n /** Unix timestamp (in seconds) when this cache entry was created */\n created: number;\n /** Promise that resolves to the token and its expiration time */\n promise: Promise<{ token: string; expires: number }>;\n}\n\nconst cache = new Map<string, CacheValue>();\nconst CACHE_MAX_AGE = 60 * 60; // 1 hour\n\n/**\n * Normalizes GCP credentials to a frozen GCPCredentials object.\n *\n * This function accepts credentials in either string (JSON) or object format\n * and ensures the returned value is a frozen (immutable) GCPCredentials object.\n * Freezing the credentials prevents accidental modification and ensures\n * consistency throughout the application lifecycle.\n *\n * @param credentials - The GCP service account credentials, either as a JSON string\n * or as a GCPCredentials object\n * @returns A frozen GCPCredentials object that cannot be modified\n *\n * @example\n * ```typescript\n * // From JSON string\n * const creds = normalizeGCPCredentials('{\"type\":\"service_account\",...}');\n *\n * // From object\n * const creds = normalizeGCPCredentials({\n * type: \"service_account\",\n * project_id: \"my-project\",\n * // ... other fields\n * });\n * ```\n */\nexport function normalizeGCPCredentials(\n credentials: string | GCPCredentials\n): GCPCredentials {\n return typeof credentials === \"string\"\n ? Object.freeze(JSON.parse(credentials))\n : Object.isFrozen(credentials)\n ? credentials\n : Object.freeze(credentials);\n}\n\n/**\n * Imports and returns the private key from GCP credentials for JWT signing.\n *\n * This function extracts the RSA private key from the service account credentials\n * and imports it using the PKCS#8 format with the RS256 algorithm. The returned\n * key can be used to sign JWTs for authentication with Google Cloud services.\n *\n * @param credentials - The GCP service account credentials containing the private key\n * @returns A Promise that resolves to the imported private key suitable for JWT signing\n *\n * @throws {Error} If the private key format is invalid or cannot be imported\n *\n * @example\n * ```typescript\n * const credentials = normalizeGCPCredentials(credentialsJson);\n * const privateKey = await getGCPPrivateKey(credentials);\n * // Use privateKey for signing operations\n * ```\n */\nexport async function getGCPPrivateKey(credentials: GCPCredentials) {\n const { importPKCS8 } = await getJose();\n return importPKCS8(credentials.private_key, \"RS256\");\n}\n\n/**\n * Generates a custom JWT token for authenticating with Google Cloud services.\n *\n * This function creates a signed JWT (JSON Web Token) using the service account's\n * private key. The token includes standard claims such as issuer, audience, subject,\n * issued-at time, and expiration time. This custom token can be exchanged for an\n * access token or ID token through Google's OAuth2 token endpoint.\n *\n * The generated JWT is valid for 1 hour and follows the OAuth 2.0 JWT Bearer\n * Token flow as specified by RFC 7523.\n *\n * @param credentials - The GCP service account credentials used to sign the JWT\n * @returns A Promise that resolves to the signed JWT string\n *\n * @throws {Error} If the private key cannot be imported or the JWT cannot be signed\n *\n * @see https://datatracker.ietf.org/doc/html/rfc7523\n * @see https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials\n *\n * @example\n * ```typescript\n * const credentials = normalizeGCPCredentials(credentialsJson);\n * const customToken = await getGCPCustomToken(credentials);\n * // Use customToken to request an access token\n * ```\n */\nexport async function getGCPCustomToken(credentials: GCPCredentials) {\n const { SignJWT } = await getJose();\n const privateKey = await getGCPPrivateKey(credentials);\n const customToken = await new SignJWT()\n .setIssuer(credentials.client_email)\n .setAudience(credentials.token_uri)\n .setSubject(credentials.client_email)\n .setProtectedHeader({ alg: \"RS256\" })\n .setIssuedAt()\n .setExpirationTime(\"1h\")\n .sign(privateKey);\n return customToken;\n}\n\n/**\n * Retrieves an access token for authenticating with Google Cloud APIs.\n *\n * - Tokens are cached based on the token URI and private key ID\n * - Cache entries are valid for up to 1 hour\n * - Expired tokens trigger automatic refresh\n * - Multiple concurrent requests for the same token are deduplicated\n *\n * The function handles both access tokens and ID tokens, automatically detecting\n * which type is returned by the token endpoint and extracting the appropriate\n * expiration time.\n *\n * @param credentials - The GCP service account credentials used to request the token\n * @returns A Promise that resolves to the access token string\n *\n * @throws {AuthError} If the token request fails (e.g., invalid credentials,\n * network error, or API error)\n *\n * @see https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials\n * @see https://datatracker.ietf.org/doc/html/rfc7523\n *\n * @example\n * ```typescript\n * const credentials = normalizeGCPCredentials(credentialsJson);\n * const accessToken = await getGCPCredentialsAccessToken(credentials);\n *\n * // Use the access token in API requests\n * const response = await fetch('https://api.google.com/...', {\n * headers: {\n * 'Authorization': `Bearer ${accessToken}`\n * }\n * });\n * ```\n *\n * @remarks\n * The caching mechanism uses a two-level validation:\n * 1. Cache entry age (must be less than CACHE_MAX_AGE)\n * 2. Token expiration time (must not be expired)\n *\n * This ensures that even if a cache entry is fresh, an expired token\n * will trigger a refresh request.\n */\nexport async function getGCPCredentialsAccessToken(\n credentials: GCPCredentials\n): Promise<string> {\n const tokenUrl = credentials.token_uri;\n\n const cacheKeyUrl = new URL(tokenUrl);\n cacheKeyUrl.searchParams.set(\"key\", credentials.private_key_id);\n const cacheKey = cacheKeyUrl.toString();\n\n // Attempt to retrieve the token from the cache\n const cacheValue = cache.get(cacheKey);\n let now = Math.floor(Date.now() / 1000);\n\n if (cacheValue) {\n if (cacheValue.created > now - CACHE_MAX_AGE) {\n let token = await cacheValue.promise;\n\n if (token.expires > now) {\n return token.token;\n } else {\n const nextValue = cache.get(cacheKey);\n\n if (nextValue && nextValue !== cacheValue) {\n token = await nextValue.promise;\n if (token.expires > now) {\n return token.token;\n } else {\n cache.delete(cacheKey);\n }\n }\n }\n } else {\n cache.delete(cacheKey);\n }\n }\n\n const promise = iife(async () => {\n now = Math.floor(Date.now() / 1000);\n\n // Request a new token from the Google Cloud API\n const jwt = await getGCPCustomToken(credentials);\n const body = new URLSearchParams();\n body.append(\"grant_type\", \"urn:ietf:params:oauth:grant-type:jwt-bearer\");\n body.append(\"assertion\", jwt);\n const res = await fetch(tokenUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body,\n });\n\n if (!res.ok) {\n throw await AuthError.fromResponse(res);\n }\n\n const data = await res.json();\n\n if (\"id_token\" in data) {\n const { decodeJwt } = await getJose();\n const claims = decodeJwt(data.id_token);\n return { token: data.id_token, expires: claims.exp as number };\n }\n\n const lastModified = res.headers.get(\"last-modified\");\n const expires = lastModified\n ? Math.floor(new Date(lastModified).valueOf() / 1000) + data.expires_in\n : now + data.expires_in;\n\n return { expires, token: data.access_token };\n });\n\n cache.set(cacheKey, { created: now, promise });\n return await promise.then((data) => data.token);\n}\n"],"mappings":";;;;;;;;;;AAaA,IAAI;AAEJ,eAAe,UAA+B;CAC5C,sBAAsB,OAAO;CAC7B,OAAO;AACT;AA0CA,MAAM,wBAAQ,IAAI,IAAwB;AAC1C,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BtB,SAAgB,wBACd,aACgB;CAChB,OAAO,OAAO,gBAAgB,WAC1B,OAAO,OAAO,KAAK,MAAM,WAAW,CAAC,IACrC,OAAO,SAAS,WAAW,IACzB,cACA,OAAO,OAAO,WAAW;AACjC;;;;;;;;;;;;;;;;;;;;AAqBA,eAAsB,iBAAiB,aAA6B;CAClE,MAAM,EAAE,gBAAgB,MAAM,QAAQ;CACtC,OAAO,YAAY,YAAY,aAAa,OAAO;AACrD;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,eAAsB,kBAAkB,aAA6B;CACnE,MAAM,EAAE,YAAY,MAAM,QAAQ;CAClC,MAAM,aAAa,MAAM,iBAAiB,WAAW;CASrD,OAAO,MARmB,IAAI,QAAQ,CAAC,CACpC,UAAU,YAAY,YAAY,CAAC,CACnC,YAAY,YAAY,SAAS,CAAC,CAClC,WAAW,YAAY,YAAY,CAAC,CACpC,mBAAmB,EAAE,KAAK,QAAQ,CAAC,CAAC,CACpC,YAAY,CAAC,CACb,kBAAkB,IAAI,CAAC,CACvB,KAAK,UAAU;AAEpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CA,eAAsB,6BACpB,aACiB;CACjB,MAAM,WAAW,YAAY;CAE7B,MAAM,cAAc,IAAI,IAAI,QAAQ;CACpC,YAAY,aAAa,IAAI,OAAO,YAAY,cAAc;CAC9D,MAAM,WAAW,YAAY,SAAS;CAGtC,MAAM,aAAa,MAAM,IAAI,QAAQ;CACrC,IAAI,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;CAEtC,IAAI,YACF,IAAI,WAAW,UAAU,MAAM,eAAe;EAC5C,IAAI,QAAQ,MAAM,WAAW;EAE7B,IAAI,MAAM,UAAU,KAClB,OAAO,MAAM;OACR;GACL,MAAM,YAAY,MAAM,IAAI,QAAQ;GAEpC,IAAI,aAAa,cAAc,YAAY;IACzC,QAAQ,MAAM,UAAU;IACxB,IAAI,MAAM,UAAU,KAClB,OAAO,MAAM;SAEb,MAAM,OAAO,QAAQ;GAEzB;EACF;CACF,OACE,MAAM,OAAO,QAAQ;CAIzB,MAAM,UAAU,KAAK,YAAY;EAC/B,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;EAGlC,MAAM,MAAM,MAAM,kBAAkB,WAAW;EAC/C,MAAM,OAAO,IAAI,gBAAgB;EACjC,KAAK,OAAO,cAAc,6CAA6C;EACvE,KAAK,OAAO,aAAa,GAAG;EAC5B,MAAM,MAAM,MAAM,MAAM,UAAU;GAChC,QAAQ;GACR,SAAS,EAAE,gBAAgB,oCAAoC;GAC/D;EACF,CAAC;EAED,IAAI,CAAC,IAAI,IACP,MAAM,MAAM,UAAU,aAAa,GAAG;EAGxC,MAAM,OAAO,MAAM,IAAI,KAAK;EAE5B,IAAI,cAAc,MAAM;GACtB,MAAM,EAAE,cAAc,MAAM,QAAQ;GACpC,MAAM,SAAS,UAAU,KAAK,QAAQ;GACtC,OAAO;IAAE,OAAO,KAAK;IAAU,SAAS,OAAO;GAAc;EAC/D;EAEA,MAAM,eAAe,IAAI,QAAQ,IAAI,eAAe;EAKpD,OAAO;GAAE,SAJO,eACZ,KAAK,MAAM,IAAI,KAAK,YAAY,CAAC,CAAC,QAAQ,IAAI,GAAI,IAAI,KAAK,aAC3D,MAAM,KAAK;GAEG,OAAO,KAAK;EAAa;CAC7C,CAAC;CAED,MAAM,IAAI,UAAU;EAAE,SAAS;EAAK;CAAQ,CAAC;CAC7C,OAAO,MAAM,QAAQ,MAAM,SAAS,KAAK,KAAK;AAChD"}
|
|
1
|
+
{"version":3,"file":"gcp-auth.js","names":[],"sources":["../../src/utils/gcp-auth.ts"],"sourcesContent":["/**\n * GCP authentication utilities.\n *\n * Adapted from: https://github.com/kriasoft/web-auth-library\n * Original source: web-auth-library by Kriasoft\n * License: MIT (https://github.com/kriasoft/web-auth-library/blob/main/LICENSE)\n */\n\nimport { AuthError } from \"../utils/errors.js\";\nimport { iife } from \"../utils/misc.js\";\n\ntype JoseModule = typeof import(\"jose\");\n\nlet joseModulePromise: Promise<JoseModule> | undefined;\n\nasync function getJose(): Promise<JoseModule> {\n joseModulePromise ??= import(\"jose\");\n return joseModulePromise;\n}\n\n/**\n * Google Cloud Platform service account credentials interface.\n *\n * This interface represents the structure of a GCP service account key file,\n * which is used for authenticating with Google Cloud APIs. Service account\n * keys can be downloaded from the Google Cloud Console and contain all the\n * information needed to authenticate as a service account.\n *\n * @see https://cloud.google.com/iam/docs/creating-managing-service-account-keys\n */\nexport interface GCPCredentials {\n /** The type of credential, typically \"service_account\" */\n type: string;\n /** The GCP project ID associated with this service account */\n project_id: string;\n /** The unique identifier for the private key */\n private_key_id: string;\n /** The RSA private key in PEM format used for signing JWTs */\n private_key: string;\n /** The unique client ID for this service account */\n client_id: string;\n /** The email address of the service account */\n client_email: string;\n /** The OAuth2 authorization endpoint URI */\n auth_uri: string;\n /** The OAuth2 token endpoint URI where access tokens are requested */\n token_uri: string;\n /** The URL for the OAuth2 provider's x509 certificate */\n auth_provider_x509_cert_url: string;\n /** The URL for this service account's x509 certificate */\n client_x509_cert_url: string;\n}\n\ninterface CacheValue {\n /** Unix timestamp (in seconds) when this cache entry was created */\n created: number;\n /** Promise that resolves to the token and its expiration time */\n promise: Promise<{ token: string; expires: number }>;\n}\n\nconst cache = new Map<string, CacheValue>();\nconst CACHE_MAX_AGE = 60 * 60; // 1 hour\n\n/**\n * Normalizes GCP credentials to a frozen GCPCredentials object.\n *\n * This function accepts credentials in either string (JSON) or object format\n * and ensures the returned value is a frozen (immutable) GCPCredentials object.\n * Freezing the credentials prevents accidental modification and ensures\n * consistency throughout the application lifecycle.\n *\n * @param credentials - The GCP service account credentials, either as a JSON string\n * or as a GCPCredentials object\n * @returns A frozen GCPCredentials object that cannot be modified\n *\n * @example\n * ```typescript\n * // From JSON string\n * const creds = normalizeGCPCredentials('{\"type\":\"service_account\",...}');\n *\n * // From object\n * const creds = normalizeGCPCredentials({\n * type: \"service_account\",\n * project_id: \"my-project\",\n * // ... other fields\n * });\n * ```\n */\nexport function normalizeGCPCredentials(\n credentials: string | GCPCredentials\n): GCPCredentials {\n return typeof credentials === \"string\"\n ? Object.freeze(JSON.parse(credentials))\n : Object.isFrozen(credentials)\n ? credentials\n : Object.freeze(credentials);\n}\n\n/**\n * Imports and returns the private key from GCP credentials for JWT signing.\n *\n * This function extracts the RSA private key from the service account credentials\n * and imports it using the PKCS#8 format with the RS256 algorithm. The returned\n * key can be used to sign JWTs for authentication with Google Cloud services.\n *\n * @param credentials - The GCP service account credentials containing the private key\n * @returns A Promise that resolves to the imported private key suitable for JWT signing\n *\n * @throws {Error} If the private key format is invalid or cannot be imported\n *\n * @example\n * ```typescript\n * const credentials = normalizeGCPCredentials(credentialsJson);\n * const privateKey = await getGCPPrivateKey(credentials);\n * // Use privateKey for signing operations\n * ```\n */\nexport async function getGCPPrivateKey(credentials: GCPCredentials) {\n const { importPKCS8 } = await getJose();\n return importPKCS8(credentials.private_key, \"RS256\");\n}\n\n/**\n * Generates a custom JWT token for authenticating with Google Cloud services.\n *\n * This function creates a signed JWT (JSON Web Token) using the service account's\n * private key. The token includes standard claims such as issuer, audience, subject,\n * issued-at time, and expiration time. This custom token can be exchanged for an\n * access token or ID token through Google's OAuth2 token endpoint.\n *\n * The generated JWT is valid for 1 hour and follows the OAuth 2.0 JWT Bearer\n * Token flow as specified by RFC 7523.\n *\n * @param credentials - The GCP service account credentials used to sign the JWT\n * @returns A Promise that resolves to the signed JWT string\n *\n * @throws {Error} If the private key cannot be imported or the JWT cannot be signed\n *\n * @see https://datatracker.ietf.org/doc/html/rfc7523\n * @see https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials\n *\n * @example\n * ```typescript\n * const credentials = normalizeGCPCredentials(credentialsJson);\n * const customToken = await getGCPCustomToken(credentials);\n * // Use customToken to request an access token\n * ```\n */\nexport async function getGCPCustomToken(credentials: GCPCredentials) {\n const { SignJWT } = await getJose();\n const privateKey = await getGCPPrivateKey(credentials);\n const customToken = await new SignJWT()\n .setIssuer(credentials.client_email)\n .setAudience(credentials.token_uri)\n .setSubject(credentials.client_email)\n .setProtectedHeader({ alg: \"RS256\" })\n .setIssuedAt()\n .setExpirationTime(\"1h\")\n .sign(privateKey);\n return customToken;\n}\n\n/**\n * Retrieves an access token for authenticating with Google Cloud APIs.\n *\n * - Tokens are cached based on the token URI and private key ID\n * - Cache entries are valid for up to 1 hour\n * - Expired tokens trigger automatic refresh\n * - Multiple concurrent requests for the same token are deduplicated\n *\n * The function handles both access tokens and ID tokens, automatically detecting\n * which type is returned by the token endpoint and extracting the appropriate\n * expiration time.\n *\n * @param credentials - The GCP service account credentials used to request the token\n * @returns A Promise that resolves to the access token string\n *\n * @throws {AuthError} If the token request fails (e.g., invalid credentials,\n * network error, or API error)\n *\n * @see https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials\n * @see https://datatracker.ietf.org/doc/html/rfc7523\n *\n * @example\n * ```typescript\n * const credentials = normalizeGCPCredentials(credentialsJson);\n * const accessToken = await getGCPCredentialsAccessToken(credentials);\n *\n * // Use the access token in API requests\n * const response = await fetch('https://api.google.com/...', {\n * headers: {\n * 'Authorization': `Bearer ${accessToken}`\n * }\n * });\n * ```\n *\n * @remarks\n * The caching mechanism uses a two-level validation:\n * 1. Cache entry age (must be less than CACHE_MAX_AGE)\n * 2. Token expiration time (must not be expired)\n *\n * This ensures that even if a cache entry is fresh, an expired token\n * will trigger a refresh request.\n */\nexport async function getGCPCredentialsAccessToken(\n credentials: GCPCredentials\n): Promise<string> {\n const tokenUrl = credentials.token_uri;\n\n const cacheKeyUrl = new URL(tokenUrl);\n cacheKeyUrl.searchParams.set(\"key\", credentials.private_key_id);\n const cacheKey = cacheKeyUrl.toString();\n\n // Attempt to retrieve the token from the cache\n const cacheValue = cache.get(cacheKey);\n let now = Math.floor(Date.now() / 1000);\n\n if (cacheValue) {\n if (cacheValue.created > now - CACHE_MAX_AGE) {\n let token = await cacheValue.promise;\n\n if (token.expires > now) {\n return token.token;\n } else {\n const nextValue = cache.get(cacheKey);\n\n if (nextValue && nextValue !== cacheValue) {\n token = await nextValue.promise;\n if (token.expires > now) {\n return token.token;\n } else {\n cache.delete(cacheKey);\n }\n }\n }\n } else {\n cache.delete(cacheKey);\n }\n }\n\n const promise = iife(async () => {\n now = Math.floor(Date.now() / 1000);\n\n // Request a new token from the Google Cloud API\n const jwt = await getGCPCustomToken(credentials);\n const body = new URLSearchParams();\n body.append(\"grant_type\", \"urn:ietf:params:oauth:grant-type:jwt-bearer\");\n body.append(\"assertion\", jwt);\n const res = await fetch(tokenUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body,\n });\n\n if (!res.ok) {\n throw await AuthError.fromResponse(res);\n }\n\n const data = await res.json();\n\n if (\"id_token\" in data) {\n const { decodeJwt } = await getJose();\n const claims = decodeJwt(data.id_token);\n return { token: data.id_token, expires: claims.exp as number };\n }\n\n const lastModified = res.headers.get(\"last-modified\");\n const expires = lastModified\n ? Math.floor(new Date(lastModified).valueOf() / 1000) + data.expires_in\n : now + data.expires_in;\n\n return { expires, token: data.access_token };\n });\n\n cache.set(cacheKey, { created: now, promise });\n return await promise.then((data) => data.token);\n}\n"],"mappings":"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"}
|
|
@@ -51,54 +51,55 @@ async function* convertGoogleGeminiStream(source, options = {}) {
|
|
|
51
51
|
const parts = candidate?.content?.parts;
|
|
52
52
|
if (!parts) continue;
|
|
53
53
|
let toolIdx = 0;
|
|
54
|
-
for (const part of parts) if (part.text)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
reasoning: ""
|
|
58
|
-
});
|
|
59
|
-
if (isNew) yield {
|
|
60
|
-
event: "content-block-start",
|
|
61
|
-
index,
|
|
62
|
-
content: {
|
|
54
|
+
for (const part of parts) if (part.text) {
|
|
55
|
+
if (part.thought) {
|
|
56
|
+
const { index, isNew } = getOrCreateBlockIndex("reasoning", {
|
|
63
57
|
type: "reasoning",
|
|
64
58
|
reasoning: ""
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
59
|
+
});
|
|
60
|
+
if (isNew) yield {
|
|
61
|
+
event: "content-block-start",
|
|
62
|
+
index,
|
|
63
|
+
content: {
|
|
64
|
+
type: "reasoning",
|
|
65
|
+
reasoning: ""
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
const acc = blockAccumulators.get(index);
|
|
69
|
+
acc.reasoning = (acc.reasoning ?? "") + part.text;
|
|
70
|
+
yield {
|
|
71
|
+
event: "content-block-delta",
|
|
72
|
+
index,
|
|
73
|
+
delta: {
|
|
74
|
+
type: "reasoning-delta",
|
|
75
|
+
reasoning: part.text
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
} else {
|
|
79
|
+
const { index, isNew } = getOrCreateBlockIndex("text", {
|
|
86
80
|
type: "text",
|
|
87
81
|
text: ""
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
82
|
+
});
|
|
83
|
+
if (isNew) yield {
|
|
84
|
+
event: "content-block-start",
|
|
85
|
+
index,
|
|
86
|
+
content: {
|
|
87
|
+
type: "text",
|
|
88
|
+
text: ""
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const acc = blockAccumulators.get(index);
|
|
92
|
+
acc.text = (acc.text ?? "") + part.text;
|
|
93
|
+
yield {
|
|
94
|
+
event: "content-block-delta",
|
|
95
|
+
index,
|
|
96
|
+
delta: {
|
|
97
|
+
type: "text-delta",
|
|
98
|
+
text: part.text
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
} else if (part.functionCall) {
|
|
102
103
|
const key = `tool:${toolIdx}`;
|
|
103
104
|
const args = JSON.stringify(part.functionCall.args ?? {});
|
|
104
105
|
const { index, isNew } = getOrCreateBlockIndex(key, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stream_events.cjs","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":";;;;;;;AAsBA,gBAAuB,0BACrB,QACA,UAA4C,CAAC,GACP;CACtC,MAAM,oBAAoB,QAAQ,eAAe;CACjD,MAAM,oCAAoB,IAAI,IAI5B;CACF,MAAM,kCAAkB,IAAI,IAAsB;CAClD,IAAI,iBAAiB;CACrB,IAAI,iBAAiB;CACrB,IAAI;CACJ,IAAI,eAA6B;CAEjC,MAAM,yBACJ,KACA,YACsC;EACtC,MAAM,WAAW,gBAAgB,IAAI,GAAG;EACxC,IAAI,aAAa,KAAA,GACf,OAAO;GAAE,OAAO;GAAU,OAAO;EAAM;EAEzC,MAAM,QAAQ;EACd,gBAAgB,IAAI,KAAK,KAAK;EAC9B,kBAAkB,IAAI,OAAO,EAAE,GAAG,QAAQ,CAAC;EAC3C,OAAO;GAAE;GAAO,OAAO;EAAK;CAC9B;CAEA,WAAW,MAAM,YAAY,QAAQ;EACnC,IAAI,CAAC,gBAAgB;GACnB,iBAAiB;GACjB,MAAM,EAAE,OAAO,gBAAyB;EAC1C;EAEA,MAAM,gBAAgB,SAAS,iBAAiB,SAAS;EACzD,IAAI,qBAAqB,eAAe;GACtC,MAAM,QAAQ,cAAc,oBAAoB;GAChD,MAAM,SAAS,cAAc,wBAAwB;GACrD,gBAAgB;IACd,cAAc;IACd,eAAe;IACf,cAAc,cAAc,mBAAmB,QAAQ;GACzD;GACA,MAAM;IAAE,OAAO;IAAkB,OAAO;GAAc;EACxD;EAEA,MAAM,YAAY,SAAS,aAAa;EACxC,IAAI,WAAW,cACb,eAAe,sBAAsB,UAAU,YAAY;EAG7D,MAAM,QAAQ,WAAW,SAAS;EAClC,IAAI,CAAC,OAAO;EAEZ,IAAI,UAAU;EACd,KAAK,MAAM,QAAQ,OACjB,IAAI,KAAK,
|
|
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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"}
|