@langchain/google 0.1.12 → 0.2.0
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 +12 -0
- package/dist/chat_models/api-types.d.cts +1 -1
- package/dist/chat_models/api-types.d.ts +1 -1
- package/dist/chat_models/base.cjs +43 -2
- package/dist/chat_models/base.cjs.map +1 -1
- package/dist/chat_models/base.d.cts +2 -0
- package/dist/chat_models/base.d.ts +2 -0
- package/dist/chat_models/base.js +43 -2
- package/dist/chat_models/base.js.map +1 -1
- package/dist/chat_models/node.cjs.map +1 -1
- package/dist/chat_models/node.js.map +1 -1
- package/dist/clients/node.cjs.map +1 -1
- package/dist/clients/node.js.map +1 -1
- package/dist/converters/messages.cjs +12 -0
- package/dist/converters/messages.cjs.map +1 -1
- package/dist/converters/messages.js +12 -0
- package/dist/converters/messages.js.map +1 -1
- package/dist/converters/params.cjs.map +1 -1
- package/dist/converters/params.js.map +1 -1
- package/dist/converters/tools.cjs.map +1 -1
- package/dist/converters/tools.js.map +1 -1
- package/dist/utils/gcp-auth.cjs.map +1 -1
- package/dist/utils/gcp-auth.js.map +1 -1
- package/dist/utils/stream_events.cjs +173 -0
- package/dist/utils/stream_events.cjs.map +1 -0
- package/dist/utils/stream_events.js +173 -0
- package/dist/utils/stream_events.js.map +1 -0
- package/package.json +5 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"params.cjs","names":["settableServiceTier"],"sources":["../../src/converters/params.ts"],"sourcesContent":["import {\n ChatGoogleFields,\n GooglePlatformType,\n settableServiceTier,\n SimplifiedSpeechConfig,\n SimplifiedSpeechLanguageConfig,\n SpeechSpeakerName,\n SpeechVoice,\n SpeechVoiceLanguage,\n} from \"../chat_models/types.js\";\nimport type { Gemini } from \"../chat_models/api-types.js\";\n\n/**\n * Converts a reasoning \"effort\" level value (e.g., \"minimal\", \"low\", \"medium\", \"high\")\n * to a corresponding reasoning token budget, depending on the target Gemini model.\n *\n * This mapping is essential when configuring Google Gemini API calls with reasoning\n * effort/level fields (as in the Gemini OpenAI-compatible API) or when translating\n * user-friendly configuration to explicit token budgets.\n *\n * - The mapping is refined to reflect the constraints documented by Google:\n * - For \"gemini-2.5-flash\" models, token budgets may be set as low as 0 and up to 24k.\n * - For other Gemini models, minimum is 128, maximum is 32k.\n * - Standard effort levels and their token mappings:\n * - \"none\" / \"minimal\": lowest allowed (minTokens)\n * - \"low\": 1k tokens\n * - \"medium\": 8k tokens\n * - \"high\": maximum allowed for the model (maxTokens)\n * - See reference:\n * https://ai.google.dev/gemini-api/docs/thinking#thinking-levels\n * https://ai.google.dev/gemini-api/docs/openai#thinking\n *\n * @param {string | undefined} modelName\n * The Gemini model name (e.g., \"gemini-2.5-flash\"). Used to determine token range constraints.\n * @param {string | undefined} effort\n * The reasoning effort level: one of \"none\", \"minimal\", \"low\", \"medium\", \"high\"\n * (case-insensitive). If undefined, the function returns undefined.\n * @returns {number | undefined}\n * The corresponding token count for the given effort and model. Returns undefined if\n * the effort is not recognized or not provided.\n *\n * @example\n * convertReasoningEffortToReasoningTokens(\"gemini-2.5-flash\", \"medium\") // 8192\n * convertReasoningEffortToReasoningTokens(\"gemini-1.5-pro\", \"high\") // 32768\n * convertReasoningEffortToReasoningTokens(\"gemini-2.5-flash\", \"none\") // 0\n * convertReasoningEffortToReasoningTokens(undefined, \"low\") // 1024\n */\nexport function convertReasoningEffortToReasoningTokens(\n modelName?: string,\n effort?: string\n): number | undefined {\n if (effort === undefined) {\n return undefined;\n }\n\n // gemini-2.5-flash and -flash-lite can be disabled. Others can't.\n // https://ai.google.dev/gemini-api/docs/thinking#thinking-levels\n const minTokens: number = modelName?.startsWith(\"gemini-2.5-flash\") ? 0 : 128;\n const maxTokens: number = modelName?.startsWith(\"gemini-2.5-flash\")\n ? 24 * 1024\n : 32 * 1024;\n\n const normalizedEffort = effort.toLowerCase();\n switch (normalizedEffort) {\n case \"none\":\n case \"minimal\":\n return minTokens;\n case \"low\":\n // Defined as 1k by https://ai.google.dev/gemini-api/docs/openai#thinking\n return 1024;\n case \"medium\":\n // Defined as 8k by https://ai.google.dev/gemini-api/docs/openai#thinking\n return 8 * 1024;\n case \"high\":\n // Defined as 24k or 32k (model-dependent) by https://ai.google.dev/gemini-api/docs/openai#thinking\n return maxTokens;\n default:\n return undefined;\n }\n}\n\n/**\n * Converts a reasoning token budget (e.g., 0, 1024, 8192, 32768) to its corresponding\n * reasoning \"effort\" level, as used in the Google Gemini API (\"minimal\", \"low\", \"medium\", \"high\").\n *\n * This is the inverse of {@link convertReasoningEffortToReasoningTokens}. It is useful for\n * translating explicit numeric settings (often surfaced in raw API responses or configs)\n * into user-facing friendly reasoning levels/labels. The mapping relies on Google's\n * documented reasoning levels for different models:\n *\n * - -1 means default effort (per API documentation: this is \"minimal\" for flash-lite models\n * and \"high\" (dynamic) for others).\n * - 0 means \"minimal\" unless the model is \"gemini-3-pro\", for which it maps to \"low\".\n * - <= 1024 tokens: \"low\"\n * - <= 8192 tokens: \"medium\" (unless model is \"gemini-3-pro\", for which it reverts to \"low\")\n * - > 8192: \"high\"\n *\n * Reference:\n * https://ai.google.dev/gemini-api/docs/thinking#thinking-levels\n * https://ai.google.dev/gemini-api/docs/openai#thinking\n *\n * @param {string | undefined} model\n * The Gemini model name (e.g., \"gemini-3-pro\"). Used for model-specific special cases.\n * @param {number | undefined} reasoningTokens\n * The token budget to convert. If undefined, the function returns undefined.\n * @returns {Lowercase<Gemini.ThinkingLevel> | undefined}\n * The reasoning \"effort\" level (\"minimal\", \"low\", \"medium\", \"high\") or undefined if input is not valid.\n *\n * @example\n * convertReasoningTokensToReasoningEffort(\"gemini-3-pro\", 0) // \"low\"\n * convertReasoningTokensToReasoningEffort(\"gemini-1.5-pro\", 0) // \"minimal\"\n * convertReasoningTokensToReasoningEffort(undefined, 1024) // \"low\"\n * convertReasoningTokensToReasoningEffort(undefined, -1) // \"high\"\n * convertReasoningTokensToReasoningEffort(undefined, 9000) // \"high\"\n */\nexport function convertReasoningTokensToReasoningEffort(\n model?: string,\n reasoningTokens?: number\n): Gemini.ThinkingLevel | undefined {\n if (typeof reasoningTokens === \"undefined\") {\n return undefined;\n } else if (reasoningTokens === -1) {\n // -1 means \"minimal\" for flash-lite models and \"high\"/dynamic for others\n // (\"default\" per https://ai.google.dev/gemini-api/docs/thinking#thinking-levels)\n if (model && getModelLevel(model) === \"flash-lite\") {\n return \"minimal\";\n }\n return \"high\";\n } else if (reasoningTokens === 0) {\n if (model?.startsWith(\"gemini-3-pro\")) {\n return \"low\";\n } else {\n return \"minimal\";\n }\n } else if (reasoningTokens <= 1024) {\n return \"low\";\n } else if (reasoningTokens <= 8192) {\n if (model?.startsWith(\"gemini-3-pro\")) {\n return \"low\";\n } else {\n return \"medium\";\n }\n } else {\n return \"high\";\n }\n}\n\n/**\n * Infers and returns the Google platform type from the provided chat model parameters.\n *\n * This function helps determine which Google platform (e.g., Vertex AI on GCP or the public API)\n * should be used for calling Gemini/PaLM endpoints, based on the options provided in\n * {@link BaseChatGoogleParams} (such as model constructors or call options).\n */\nexport function convertParamsToPlatformType(params: {\n platformType?: GooglePlatformType;\n vertexai?: boolean;\n}): GooglePlatformType | undefined {\n if (typeof params === \"undefined\") {\n return undefined;\n }\n if (typeof params.platformType !== \"undefined\") {\n return params.platformType;\n }\n if (params.vertexai === true) {\n return \"gcp\";\n }\n return undefined;\n}\n\n/**\n * Returns the model family from a Gemini model name.\n * e.g. \"gemini-2.5-flash\" → \"gemini\"\n */\nexport function getModelFamily(model: string): string {\n return model.split(\"-\")[0];\n}\n\n/**\n * Returns the model version from a Gemini model name.\n * e.g. \"gemini-2.5-flash\" → \"2.5\"\n */\nexport function getModelVersion(model: string): string {\n return model.split(\"-\")[1];\n}\n\n/**\n * Returns the model level from a Gemini model name.\n * e.g. \"gemini-2.5-flash\" → \"flash\", \"gemini-2.5-flash-lite\" → \"flash-lite\"\n */\nexport function getModelLevel(model: string): string {\n const parts = model.split(\"-\");\n let ret = parts[2];\n if (ret === \"flash\" && parts[3] === \"lite\") {\n ret = \"flash-lite\";\n }\n return ret;\n}\n\n/**\n * Returns the model specialty from a Gemini model name.\n * e.g. \"gemini-2.5-flash-preview-image\" → \"image\",\n * \"gemini-2.5-flash-preview-tts\" → \"tts\",\n * \"gemini-2.5-flash\" → \"\"\n */\nexport function getModelSpecialty(model: string): string {\n if (model.includes(\"image\")) {\n return \"image\";\n } else if (model.includes(\"tts\")) {\n return \"tts\";\n } else {\n return \"\";\n }\n}\n\n/**\n * Builds the `thinkingConfig` object for the Gemini `generationConfig`,\n * translating user-facing reasoning fields into the wire format.\n *\n * @param model The full Gemini model name (e.g. \"gemini-2.5-flash\").\n * @param fields The combined user/call-option fields.\n * @returns A populated {@link Gemini.ThinkingConfig}, or `{}`\n * when thinking should not be explicitly configured.\n */\nexport function convertFieldsToThinkingConfig(\n model: string,\n fields: ChatGoogleFields\n): Gemini.ThinkingConfig | undefined {\n // Thinking / reasoning\n let includeThoughts = true;\n\n let thinkingBudget =\n fields?.maxReasoningTokens ??\n fields?.thinkingBudget ??\n convertReasoningEffortToReasoningTokens(\n model,\n fields?.reasoningEffort ?? fields?.thinkingLevel\n );\n if (\n thinkingBudget === 0 ||\n (model.includes(\"pro\") && thinkingBudget === 128)\n ) {\n includeThoughts = false;\n }\n if (\n model.startsWith(\"gemini-2.5-pro\") &&\n typeof thinkingBudget !== \"undefined\"\n ) {\n // Can't turn off Gemini 2.5 Pro thinking completely\n if (thinkingBudget >= 0 && thinkingBudget < 128) {\n thinkingBudget = 128;\n }\n }\n\n const thinkingLevelRaw =\n fields?.reasoningEffort ??\n fields?.thinkingLevel ??\n convertReasoningTokensToReasoningEffort(\n model,\n fields?.maxReasoningTokens ?? fields?.thinkingBudget\n );\n let thinkingLevel = thinkingLevelRaw?.toUpperCase();\n if (thinkingLevel === \"MINIMAL\") {\n includeThoughts = false;\n }\n if (model.startsWith(\"gemini-3-pro\")) {\n // Gemini 3 Pro has only low and high.\n if (thinkingLevel === \"MINIMAL\") {\n thinkingLevel = \"LOW\";\n } else if (thinkingLevel === \"MEDIUM\") {\n thinkingLevel = \"HIGH\";\n }\n } else if (model.startsWith(\"gemini-3.1-pro\")) {\n // Gemini 3.1 Pro does not have \"minimal\"\n if (thinkingLevel === \"MINIMAL\") {\n thinkingLevel = \"LOW\";\n }\n }\n\n // If we are using a model that doesn't support thinking at all (gemini 2.5 imaging)\n // or we haven't explicitly tried to set a thinking budget/level, then bail out.\n const modelVersion = getModelVersion(model);\n const modelSpecialty = getModelSpecialty(model);\n const thoughtsNotSupported =\n modelVersion === \"2.5\" && modelSpecialty === \"image\";\n if (\n thoughtsNotSupported ||\n typeof thinkingBudget === \"undefined\" ||\n typeof thinkingLevel === \"undefined\"\n ) {\n return undefined;\n }\n\n // If we have gotten this far, then we want to explicitly set if we include thoughts or not.\n const thinkingConfig: Gemini.ThinkingConfig = {\n includeThoughts,\n };\n\n // Explicitly setting the budget/level is only valid (currently) for text models.\n if (modelSpecialty === \"\") {\n if (model.startsWith(\"gemini-2.5\")) {\n thinkingConfig.thinkingBudget = thinkingBudget;\n } else {\n thinkingConfig.thinkingLevel = thinkingLevel as Gemini.ThinkingLevel;\n }\n }\n\n return thinkingConfig;\n}\n\n/**\n * Builds the `speechConfig` object for the Gemini `generationConfig`,\n * normalising the user-friendly simplified forms into the wire format.\n *\n * @param fields The combined user/call-option fields.\n * @returns A populated {@link Gemini.SpeechConfig}, or `{}`\n * when no speech config was provided.\n */\nexport function convertFieldsToSpeechConfig(\n fields: ChatGoogleFields\n): Gemini.SpeechConfig | undefined {\n const config: Gemini.SpeechConfig | SimplifiedSpeechConfig | undefined =\n fields.speechConfig;\n if (typeof config === \"undefined\") {\n return undefined;\n }\n\n function isSpeechConfig(\n config: Gemini.SpeechConfig | SimplifiedSpeechConfig\n ): config is Gemini.SpeechConfig {\n return (\n typeof config === \"object\" &&\n (Object.hasOwn(config, \"voiceConfig\") ||\n Object.hasOwn(config, \"multiSpeakerVoiceConfig\"))\n );\n }\n\n function hasLanguage(\n config: SimplifiedSpeechConfig\n ): config is SimplifiedSpeechLanguageConfig {\n return typeof config === \"object\" && Object.hasOwn(config, \"languageCode\");\n }\n\n function hasVoice(\n config: SimplifiedSpeechLanguageConfig\n ): config is SpeechVoiceLanguage {\n return Object.hasOwn(config, \"voice\");\n }\n\n // If this is already a SpeechConfig, just return it\n if (isSpeechConfig(config)) {\n return config;\n }\n\n let languageCode: string | undefined;\n let voice: SpeechVoice;\n if (hasLanguage(config)) {\n languageCode = config.languageCode;\n voice = hasVoice(config) ? config.voice : config.voices;\n } else {\n languageCode = undefined;\n voice = config;\n }\n\n let ret: Gemini.SpeechConfig;\n\n if (typeof voice === \"string\") {\n // They just provided the prebuilt voice configuration name. Use it.\n ret = {\n voiceConfig: {\n prebuiltVoiceConfig: {\n voiceName: voice,\n },\n },\n };\n } else {\n // This is multi-speaker, so we have speaker/name pairs\n // If we have just one (why?), turn it into an array for the moment\n const voices: SpeechSpeakerName[] = Array.isArray(voice) ? voice : [voice];\n // Go through all the speaker/name pairs and turn this into the voice config array\n const speakerVoiceConfigs: Gemini.SpeakerVoiceConfig[] = voices.map(\n (v: SpeechSpeakerName): Gemini.SpeakerVoiceConfig => ({\n speaker: v.speaker,\n voiceConfig: {\n prebuiltVoiceConfig: {\n voiceName: v.name,\n },\n },\n })\n );\n // Create the multi-speaker voice configuration\n ret = {\n multiSpeakerVoiceConfig: {\n speakerVoiceConfigs,\n },\n };\n }\n\n if (languageCode) {\n ret.languageCode = languageCode;\n }\n\n return ret;\n}\n\ntype ServiceTierResponse = {} | { serviceTier: Gemini.ServiceTier };\n\n/**\n * Builds an object that can be included as part of the request\n * which will add a `speechConfig` attribute if appropriate.\n */\nexport function convertFieldsToServiceTier(\n platform: GooglePlatformType,\n fields: ChatGoogleFields\n): ServiceTierResponse {\n // Service tier is handled in a different way on GCP\n if (platform === \"gcp\") {\n return {};\n }\n\n const serviceTier = fields.serviceTier;\n if (!serviceTier || !settableServiceTier.includes(serviceTier)) {\n return {};\n }\n\n return {\n serviceTier: serviceTier,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"params.cjs","names":["settableServiceTier"],"sources":["../../src/converters/params.ts"],"sourcesContent":["import {\n ChatGoogleFields,\n GooglePlatformType,\n settableServiceTier,\n SimplifiedSpeechConfig,\n SimplifiedSpeechLanguageConfig,\n SpeechSpeakerName,\n SpeechVoice,\n SpeechVoiceLanguage,\n} from \"../chat_models/types.js\";\nimport type { Gemini } from \"../chat_models/api-types.js\";\n\n/**\n * Converts a reasoning \"effort\" level value (e.g., \"minimal\", \"low\", \"medium\", \"high\")\n * to a corresponding reasoning token budget, depending on the target Gemini model.\n *\n * This mapping is essential when configuring Google Gemini API calls with reasoning\n * effort/level fields (as in the Gemini OpenAI-compatible API) or when translating\n * user-friendly configuration to explicit token budgets.\n *\n * - The mapping is refined to reflect the constraints documented by Google:\n * - For \"gemini-2.5-flash\" models, token budgets may be set as low as 0 and up to 24k.\n * - For other Gemini models, minimum is 128, maximum is 32k.\n * - Standard effort levels and their token mappings:\n * - \"none\" / \"minimal\": lowest allowed (minTokens)\n * - \"low\": 1k tokens\n * - \"medium\": 8k tokens\n * - \"high\": maximum allowed for the model (maxTokens)\n * - See reference:\n * https://ai.google.dev/gemini-api/docs/thinking#thinking-levels\n * https://ai.google.dev/gemini-api/docs/openai#thinking\n *\n * @param {string | undefined} modelName\n * The Gemini model name (e.g., \"gemini-2.5-flash\"). Used to determine token range constraints.\n * @param {string | undefined} effort\n * The reasoning effort level: one of \"none\", \"minimal\", \"low\", \"medium\", \"high\"\n * (case-insensitive). If undefined, the function returns undefined.\n * @returns {number | undefined}\n * The corresponding token count for the given effort and model. Returns undefined if\n * the effort is not recognized or not provided.\n *\n * @example\n * convertReasoningEffortToReasoningTokens(\"gemini-2.5-flash\", \"medium\") // 8192\n * convertReasoningEffortToReasoningTokens(\"gemini-1.5-pro\", \"high\") // 32768\n * convertReasoningEffortToReasoningTokens(\"gemini-2.5-flash\", \"none\") // 0\n * convertReasoningEffortToReasoningTokens(undefined, \"low\") // 1024\n */\nexport function convertReasoningEffortToReasoningTokens(\n modelName?: string,\n effort?: string\n): number | undefined {\n if (effort === undefined) {\n return undefined;\n }\n\n // gemini-2.5-flash and -flash-lite can be disabled. Others can't.\n // https://ai.google.dev/gemini-api/docs/thinking#thinking-levels\n const minTokens: number = modelName?.startsWith(\"gemini-2.5-flash\") ? 0 : 128;\n const maxTokens: number = modelName?.startsWith(\"gemini-2.5-flash\")\n ? 24 * 1024\n : 32 * 1024;\n\n const normalizedEffort = effort.toLowerCase();\n switch (normalizedEffort) {\n case \"none\":\n case \"minimal\":\n return minTokens;\n case \"low\":\n // Defined as 1k by https://ai.google.dev/gemini-api/docs/openai#thinking\n return 1024;\n case \"medium\":\n // Defined as 8k by https://ai.google.dev/gemini-api/docs/openai#thinking\n return 8 * 1024;\n case \"high\":\n // Defined as 24k or 32k (model-dependent) by https://ai.google.dev/gemini-api/docs/openai#thinking\n return maxTokens;\n default:\n return undefined;\n }\n}\n\n/**\n * Converts a reasoning token budget (e.g., 0, 1024, 8192, 32768) to its corresponding\n * reasoning \"effort\" level, as used in the Google Gemini API (\"minimal\", \"low\", \"medium\", \"high\").\n *\n * This is the inverse of {@link convertReasoningEffortToReasoningTokens}. It is useful for\n * translating explicit numeric settings (often surfaced in raw API responses or configs)\n * into user-facing friendly reasoning levels/labels. The mapping relies on Google's\n * documented reasoning levels for different models:\n *\n * - -1 means default effort (per API documentation: this is \"minimal\" for flash-lite models\n * and \"high\" (dynamic) for others).\n * - 0 means \"minimal\" unless the model is \"gemini-3-pro\", for which it maps to \"low\".\n * - <= 1024 tokens: \"low\"\n * - <= 8192 tokens: \"medium\" (unless model is \"gemini-3-pro\", for which it reverts to \"low\")\n * - > 8192: \"high\"\n *\n * Reference:\n * https://ai.google.dev/gemini-api/docs/thinking#thinking-levels\n * https://ai.google.dev/gemini-api/docs/openai#thinking\n *\n * @param {string | undefined} model\n * The Gemini model name (e.g., \"gemini-3-pro\"). Used for model-specific special cases.\n * @param {number | undefined} reasoningTokens\n * The token budget to convert. If undefined, the function returns undefined.\n * @returns {Lowercase<Gemini.ThinkingLevel> | undefined}\n * The reasoning \"effort\" level (\"minimal\", \"low\", \"medium\", \"high\") or undefined if input is not valid.\n *\n * @example\n * convertReasoningTokensToReasoningEffort(\"gemini-3-pro\", 0) // \"low\"\n * convertReasoningTokensToReasoningEffort(\"gemini-1.5-pro\", 0) // \"minimal\"\n * convertReasoningTokensToReasoningEffort(undefined, 1024) // \"low\"\n * convertReasoningTokensToReasoningEffort(undefined, -1) // \"high\"\n * convertReasoningTokensToReasoningEffort(undefined, 9000) // \"high\"\n */\nexport function convertReasoningTokensToReasoningEffort(\n model?: string,\n reasoningTokens?: number\n): Gemini.ThinkingLevel | undefined {\n if (typeof reasoningTokens === \"undefined\") {\n return undefined;\n } else if (reasoningTokens === -1) {\n // -1 means \"minimal\" for flash-lite models and \"high\"/dynamic for others\n // (\"default\" per https://ai.google.dev/gemini-api/docs/thinking#thinking-levels)\n if (model && getModelLevel(model) === \"flash-lite\") {\n return \"minimal\";\n }\n return \"high\";\n } else if (reasoningTokens === 0) {\n if (model?.startsWith(\"gemini-3-pro\")) {\n return \"low\";\n } else {\n return \"minimal\";\n }\n } else if (reasoningTokens <= 1024) {\n return \"low\";\n } else if (reasoningTokens <= 8192) {\n if (model?.startsWith(\"gemini-3-pro\")) {\n return \"low\";\n } else {\n return \"medium\";\n }\n } else {\n return \"high\";\n }\n}\n\n/**\n * Infers and returns the Google platform type from the provided chat model parameters.\n *\n * This function helps determine which Google platform (e.g., Vertex AI on GCP or the public API)\n * should be used for calling Gemini/PaLM endpoints, based on the options provided in\n * {@link BaseChatGoogleParams} (such as model constructors or call options).\n */\nexport function convertParamsToPlatformType(params: {\n platformType?: GooglePlatformType;\n vertexai?: boolean;\n}): GooglePlatformType | undefined {\n if (typeof params === \"undefined\") {\n return undefined;\n }\n if (typeof params.platformType !== \"undefined\") {\n return params.platformType;\n }\n if (params.vertexai === true) {\n return \"gcp\";\n }\n return undefined;\n}\n\n/**\n * Returns the model family from a Gemini model name.\n * e.g. \"gemini-2.5-flash\" → \"gemini\"\n */\nexport function getModelFamily(model: string): string {\n return model.split(\"-\")[0];\n}\n\n/**\n * Returns the model version from a Gemini model name.\n * e.g. \"gemini-2.5-flash\" → \"2.5\"\n */\nexport function getModelVersion(model: string): string {\n return model.split(\"-\")[1];\n}\n\n/**\n * Returns the model level from a Gemini model name.\n * e.g. \"gemini-2.5-flash\" → \"flash\", \"gemini-2.5-flash-lite\" → \"flash-lite\"\n */\nexport function getModelLevel(model: string): string {\n const parts = model.split(\"-\");\n let ret = parts[2];\n if (ret === \"flash\" && parts[3] === \"lite\") {\n ret = \"flash-lite\";\n }\n return ret;\n}\n\n/**\n * Returns the model specialty from a Gemini model name.\n * e.g. \"gemini-2.5-flash-preview-image\" → \"image\",\n * \"gemini-2.5-flash-preview-tts\" → \"tts\",\n * \"gemini-2.5-flash\" → \"\"\n */\nexport function getModelSpecialty(model: string): string {\n if (model.includes(\"image\")) {\n return \"image\";\n } else if (model.includes(\"tts\")) {\n return \"tts\";\n } else {\n return \"\";\n }\n}\n\n/**\n * Builds the `thinkingConfig` object for the Gemini `generationConfig`,\n * translating user-facing reasoning fields into the wire format.\n *\n * @param model The full Gemini model name (e.g. \"gemini-2.5-flash\").\n * @param fields The combined user/call-option fields.\n * @returns A populated {@link Gemini.ThinkingConfig}, or `{}`\n * when thinking should not be explicitly configured.\n */\nexport function convertFieldsToThinkingConfig(\n model: string,\n fields: ChatGoogleFields\n): Gemini.ThinkingConfig | undefined {\n // Thinking / reasoning\n let includeThoughts = true;\n\n let thinkingBudget =\n fields?.maxReasoningTokens ??\n fields?.thinkingBudget ??\n convertReasoningEffortToReasoningTokens(\n model,\n fields?.reasoningEffort ?? fields?.thinkingLevel\n );\n if (\n thinkingBudget === 0 ||\n (model.includes(\"pro\") && thinkingBudget === 128)\n ) {\n includeThoughts = false;\n }\n if (\n model.startsWith(\"gemini-2.5-pro\") &&\n typeof thinkingBudget !== \"undefined\"\n ) {\n // Can't turn off Gemini 2.5 Pro thinking completely\n if (thinkingBudget >= 0 && thinkingBudget < 128) {\n thinkingBudget = 128;\n }\n }\n\n const thinkingLevelRaw =\n fields?.reasoningEffort ??\n fields?.thinkingLevel ??\n convertReasoningTokensToReasoningEffort(\n model,\n fields?.maxReasoningTokens ?? fields?.thinkingBudget\n );\n let thinkingLevel = thinkingLevelRaw?.toUpperCase();\n if (thinkingLevel === \"MINIMAL\") {\n includeThoughts = false;\n }\n if (model.startsWith(\"gemini-3-pro\")) {\n // Gemini 3 Pro has only low and high.\n if (thinkingLevel === \"MINIMAL\") {\n thinkingLevel = \"LOW\";\n } else if (thinkingLevel === \"MEDIUM\") {\n thinkingLevel = \"HIGH\";\n }\n } else if (model.startsWith(\"gemini-3.1-pro\")) {\n // Gemini 3.1 Pro does not have \"minimal\"\n if (thinkingLevel === \"MINIMAL\") {\n thinkingLevel = \"LOW\";\n }\n }\n\n // If we are using a model that doesn't support thinking at all (gemini 2.5 imaging)\n // or we haven't explicitly tried to set a thinking budget/level, then bail out.\n const modelVersion = getModelVersion(model);\n const modelSpecialty = getModelSpecialty(model);\n const thoughtsNotSupported =\n modelVersion === \"2.5\" && modelSpecialty === \"image\";\n if (\n thoughtsNotSupported ||\n typeof thinkingBudget === \"undefined\" ||\n typeof thinkingLevel === \"undefined\"\n ) {\n return undefined;\n }\n\n // If we have gotten this far, then we want to explicitly set if we include thoughts or not.\n const thinkingConfig: Gemini.ThinkingConfig = {\n includeThoughts,\n };\n\n // Explicitly setting the budget/level is only valid (currently) for text models.\n if (modelSpecialty === \"\") {\n if (model.startsWith(\"gemini-2.5\")) {\n thinkingConfig.thinkingBudget = thinkingBudget;\n } else {\n thinkingConfig.thinkingLevel = thinkingLevel as Gemini.ThinkingLevel;\n }\n }\n\n return thinkingConfig;\n}\n\n/**\n * Builds the `speechConfig` object for the Gemini `generationConfig`,\n * normalising the user-friendly simplified forms into the wire format.\n *\n * @param fields The combined user/call-option fields.\n * @returns A populated {@link Gemini.SpeechConfig}, or `{}`\n * when no speech config was provided.\n */\nexport function convertFieldsToSpeechConfig(\n fields: ChatGoogleFields\n): Gemini.SpeechConfig | undefined {\n const config: Gemini.SpeechConfig | SimplifiedSpeechConfig | undefined =\n fields.speechConfig;\n if (typeof config === \"undefined\") {\n return undefined;\n }\n\n function isSpeechConfig(\n config: Gemini.SpeechConfig | SimplifiedSpeechConfig\n ): config is Gemini.SpeechConfig {\n return (\n typeof config === \"object\" &&\n (Object.hasOwn(config, \"voiceConfig\") ||\n Object.hasOwn(config, \"multiSpeakerVoiceConfig\"))\n );\n }\n\n function hasLanguage(\n config: SimplifiedSpeechConfig\n ): config is SimplifiedSpeechLanguageConfig {\n return typeof config === \"object\" && Object.hasOwn(config, \"languageCode\");\n }\n\n function hasVoice(\n config: SimplifiedSpeechLanguageConfig\n ): config is SpeechVoiceLanguage {\n return Object.hasOwn(config, \"voice\");\n }\n\n // If this is already a SpeechConfig, just return it\n if (isSpeechConfig(config)) {\n return config;\n }\n\n let languageCode: string | undefined;\n let voice: SpeechVoice;\n if (hasLanguage(config)) {\n languageCode = config.languageCode;\n voice = hasVoice(config) ? config.voice : config.voices;\n } else {\n languageCode = undefined;\n voice = config;\n }\n\n let ret: Gemini.SpeechConfig;\n\n if (typeof voice === \"string\") {\n // They just provided the prebuilt voice configuration name. Use it.\n ret = {\n voiceConfig: {\n prebuiltVoiceConfig: {\n voiceName: voice,\n },\n },\n };\n } else {\n // This is multi-speaker, so we have speaker/name pairs\n // If we have just one (why?), turn it into an array for the moment\n const voices: SpeechSpeakerName[] = Array.isArray(voice) ? voice : [voice];\n // Go through all the speaker/name pairs and turn this into the voice config array\n const speakerVoiceConfigs: Gemini.SpeakerVoiceConfig[] = voices.map(\n (v: SpeechSpeakerName): Gemini.SpeakerVoiceConfig => ({\n speaker: v.speaker,\n voiceConfig: {\n prebuiltVoiceConfig: {\n voiceName: v.name,\n },\n },\n })\n );\n // Create the multi-speaker voice configuration\n ret = {\n multiSpeakerVoiceConfig: {\n speakerVoiceConfigs,\n },\n };\n }\n\n if (languageCode) {\n ret.languageCode = languageCode;\n }\n\n return ret;\n}\n\ntype ServiceTierResponse = {} | { serviceTier: Gemini.ServiceTier };\n\n/**\n * Builds an object that can be included as part of the request\n * which will add a `speechConfig` attribute if appropriate.\n */\nexport function convertFieldsToServiceTier(\n platform: GooglePlatformType,\n fields: ChatGoogleFields\n): ServiceTierResponse {\n // Service tier is handled in a different way on GCP\n if (platform === \"gcp\") {\n return {};\n }\n\n const serviceTier = fields.serviceTier;\n if (!serviceTier || !settableServiceTier.includes(serviceTier)) {\n return {};\n }\n\n return {\n serviceTier: serviceTier,\n };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"params.js","names":[],"sources":["../../src/converters/params.ts"],"sourcesContent":["import {\n ChatGoogleFields,\n GooglePlatformType,\n settableServiceTier,\n SimplifiedSpeechConfig,\n SimplifiedSpeechLanguageConfig,\n SpeechSpeakerName,\n SpeechVoice,\n SpeechVoiceLanguage,\n} from \"../chat_models/types.js\";\nimport type { Gemini } from \"../chat_models/api-types.js\";\n\n/**\n * Converts a reasoning \"effort\" level value (e.g., \"minimal\", \"low\", \"medium\", \"high\")\n * to a corresponding reasoning token budget, depending on the target Gemini model.\n *\n * This mapping is essential when configuring Google Gemini API calls with reasoning\n * effort/level fields (as in the Gemini OpenAI-compatible API) or when translating\n * user-friendly configuration to explicit token budgets.\n *\n * - The mapping is refined to reflect the constraints documented by Google:\n * - For \"gemini-2.5-flash\" models, token budgets may be set as low as 0 and up to 24k.\n * - For other Gemini models, minimum is 128, maximum is 32k.\n * - Standard effort levels and their token mappings:\n * - \"none\" / \"minimal\": lowest allowed (minTokens)\n * - \"low\": 1k tokens\n * - \"medium\": 8k tokens\n * - \"high\": maximum allowed for the model (maxTokens)\n * - See reference:\n * https://ai.google.dev/gemini-api/docs/thinking#thinking-levels\n * https://ai.google.dev/gemini-api/docs/openai#thinking\n *\n * @param {string | undefined} modelName\n * The Gemini model name (e.g., \"gemini-2.5-flash\"). Used to determine token range constraints.\n * @param {string | undefined} effort\n * The reasoning effort level: one of \"none\", \"minimal\", \"low\", \"medium\", \"high\"\n * (case-insensitive). If undefined, the function returns undefined.\n * @returns {number | undefined}\n * The corresponding token count for the given effort and model. Returns undefined if\n * the effort is not recognized or not provided.\n *\n * @example\n * convertReasoningEffortToReasoningTokens(\"gemini-2.5-flash\", \"medium\") // 8192\n * convertReasoningEffortToReasoningTokens(\"gemini-1.5-pro\", \"high\") // 32768\n * convertReasoningEffortToReasoningTokens(\"gemini-2.5-flash\", \"none\") // 0\n * convertReasoningEffortToReasoningTokens(undefined, \"low\") // 1024\n */\nexport function convertReasoningEffortToReasoningTokens(\n modelName?: string,\n effort?: string\n): number | undefined {\n if (effort === undefined) {\n return undefined;\n }\n\n // gemini-2.5-flash and -flash-lite can be disabled. Others can't.\n // https://ai.google.dev/gemini-api/docs/thinking#thinking-levels\n const minTokens: number = modelName?.startsWith(\"gemini-2.5-flash\") ? 0 : 128;\n const maxTokens: number = modelName?.startsWith(\"gemini-2.5-flash\")\n ? 24 * 1024\n : 32 * 1024;\n\n const normalizedEffort = effort.toLowerCase();\n switch (normalizedEffort) {\n case \"none\":\n case \"minimal\":\n return minTokens;\n case \"low\":\n // Defined as 1k by https://ai.google.dev/gemini-api/docs/openai#thinking\n return 1024;\n case \"medium\":\n // Defined as 8k by https://ai.google.dev/gemini-api/docs/openai#thinking\n return 8 * 1024;\n case \"high\":\n // Defined as 24k or 32k (model-dependent) by https://ai.google.dev/gemini-api/docs/openai#thinking\n return maxTokens;\n default:\n return undefined;\n }\n}\n\n/**\n * Converts a reasoning token budget (e.g., 0, 1024, 8192, 32768) to its corresponding\n * reasoning \"effort\" level, as used in the Google Gemini API (\"minimal\", \"low\", \"medium\", \"high\").\n *\n * This is the inverse of {@link convertReasoningEffortToReasoningTokens}. It is useful for\n * translating explicit numeric settings (often surfaced in raw API responses or configs)\n * into user-facing friendly reasoning levels/labels. The mapping relies on Google's\n * documented reasoning levels for different models:\n *\n * - -1 means default effort (per API documentation: this is \"minimal\" for flash-lite models\n * and \"high\" (dynamic) for others).\n * - 0 means \"minimal\" unless the model is \"gemini-3-pro\", for which it maps to \"low\".\n * - <= 1024 tokens: \"low\"\n * - <= 8192 tokens: \"medium\" (unless model is \"gemini-3-pro\", for which it reverts to \"low\")\n * - > 8192: \"high\"\n *\n * Reference:\n * https://ai.google.dev/gemini-api/docs/thinking#thinking-levels\n * https://ai.google.dev/gemini-api/docs/openai#thinking\n *\n * @param {string | undefined} model\n * The Gemini model name (e.g., \"gemini-3-pro\"). Used for model-specific special cases.\n * @param {number | undefined} reasoningTokens\n * The token budget to convert. If undefined, the function returns undefined.\n * @returns {Lowercase<Gemini.ThinkingLevel> | undefined}\n * The reasoning \"effort\" level (\"minimal\", \"low\", \"medium\", \"high\") or undefined if input is not valid.\n *\n * @example\n * convertReasoningTokensToReasoningEffort(\"gemini-3-pro\", 0) // \"low\"\n * convertReasoningTokensToReasoningEffort(\"gemini-1.5-pro\", 0) // \"minimal\"\n * convertReasoningTokensToReasoningEffort(undefined, 1024) // \"low\"\n * convertReasoningTokensToReasoningEffort(undefined, -1) // \"high\"\n * convertReasoningTokensToReasoningEffort(undefined, 9000) // \"high\"\n */\nexport function convertReasoningTokensToReasoningEffort(\n model?: string,\n reasoningTokens?: number\n): Gemini.ThinkingLevel | undefined {\n if (typeof reasoningTokens === \"undefined\") {\n return undefined;\n } else if (reasoningTokens === -1) {\n // -1 means \"minimal\" for flash-lite models and \"high\"/dynamic for others\n // (\"default\" per https://ai.google.dev/gemini-api/docs/thinking#thinking-levels)\n if (model && getModelLevel(model) === \"flash-lite\") {\n return \"minimal\";\n }\n return \"high\";\n } else if (reasoningTokens === 0) {\n if (model?.startsWith(\"gemini-3-pro\")) {\n return \"low\";\n } else {\n return \"minimal\";\n }\n } else if (reasoningTokens <= 1024) {\n return \"low\";\n } else if (reasoningTokens <= 8192) {\n if (model?.startsWith(\"gemini-3-pro\")) {\n return \"low\";\n } else {\n return \"medium\";\n }\n } else {\n return \"high\";\n }\n}\n\n/**\n * Infers and returns the Google platform type from the provided chat model parameters.\n *\n * This function helps determine which Google platform (e.g., Vertex AI on GCP or the public API)\n * should be used for calling Gemini/PaLM endpoints, based on the options provided in\n * {@link BaseChatGoogleParams} (such as model constructors or call options).\n */\nexport function convertParamsToPlatformType(params: {\n platformType?: GooglePlatformType;\n vertexai?: boolean;\n}): GooglePlatformType | undefined {\n if (typeof params === \"undefined\") {\n return undefined;\n }\n if (typeof params.platformType !== \"undefined\") {\n return params.platformType;\n }\n if (params.vertexai === true) {\n return \"gcp\";\n }\n return undefined;\n}\n\n/**\n * Returns the model family from a Gemini model name.\n * e.g. \"gemini-2.5-flash\" → \"gemini\"\n */\nexport function getModelFamily(model: string): string {\n return model.split(\"-\")[0];\n}\n\n/**\n * Returns the model version from a Gemini model name.\n * e.g. \"gemini-2.5-flash\" → \"2.5\"\n */\nexport function getModelVersion(model: string): string {\n return model.split(\"-\")[1];\n}\n\n/**\n * Returns the model level from a Gemini model name.\n * e.g. \"gemini-2.5-flash\" → \"flash\", \"gemini-2.5-flash-lite\" → \"flash-lite\"\n */\nexport function getModelLevel(model: string): string {\n const parts = model.split(\"-\");\n let ret = parts[2];\n if (ret === \"flash\" && parts[3] === \"lite\") {\n ret = \"flash-lite\";\n }\n return ret;\n}\n\n/**\n * Returns the model specialty from a Gemini model name.\n * e.g. \"gemini-2.5-flash-preview-image\" → \"image\",\n * \"gemini-2.5-flash-preview-tts\" → \"tts\",\n * \"gemini-2.5-flash\" → \"\"\n */\nexport function getModelSpecialty(model: string): string {\n if (model.includes(\"image\")) {\n return \"image\";\n } else if (model.includes(\"tts\")) {\n return \"tts\";\n } else {\n return \"\";\n }\n}\n\n/**\n * Builds the `thinkingConfig` object for the Gemini `generationConfig`,\n * translating user-facing reasoning fields into the wire format.\n *\n * @param model The full Gemini model name (e.g. \"gemini-2.5-flash\").\n * @param fields The combined user/call-option fields.\n * @returns A populated {@link Gemini.ThinkingConfig}, or `{}`\n * when thinking should not be explicitly configured.\n */\nexport function convertFieldsToThinkingConfig(\n model: string,\n fields: ChatGoogleFields\n): Gemini.ThinkingConfig | undefined {\n // Thinking / reasoning\n let includeThoughts = true;\n\n let thinkingBudget =\n fields?.maxReasoningTokens ??\n fields?.thinkingBudget ??\n convertReasoningEffortToReasoningTokens(\n model,\n fields?.reasoningEffort ?? fields?.thinkingLevel\n );\n if (\n thinkingBudget === 0 ||\n (model.includes(\"pro\") && thinkingBudget === 128)\n ) {\n includeThoughts = false;\n }\n if (\n model.startsWith(\"gemini-2.5-pro\") &&\n typeof thinkingBudget !== \"undefined\"\n ) {\n // Can't turn off Gemini 2.5 Pro thinking completely\n if (thinkingBudget >= 0 && thinkingBudget < 128) {\n thinkingBudget = 128;\n }\n }\n\n const thinkingLevelRaw =\n fields?.reasoningEffort ??\n fields?.thinkingLevel ??\n convertReasoningTokensToReasoningEffort(\n model,\n fields?.maxReasoningTokens ?? fields?.thinkingBudget\n );\n let thinkingLevel = thinkingLevelRaw?.toUpperCase();\n if (thinkingLevel === \"MINIMAL\") {\n includeThoughts = false;\n }\n if (model.startsWith(\"gemini-3-pro\")) {\n // Gemini 3 Pro has only low and high.\n if (thinkingLevel === \"MINIMAL\") {\n thinkingLevel = \"LOW\";\n } else if (thinkingLevel === \"MEDIUM\") {\n thinkingLevel = \"HIGH\";\n }\n } else if (model.startsWith(\"gemini-3.1-pro\")) {\n // Gemini 3.1 Pro does not have \"minimal\"\n if (thinkingLevel === \"MINIMAL\") {\n thinkingLevel = \"LOW\";\n }\n }\n\n // If we are using a model that doesn't support thinking at all (gemini 2.5 imaging)\n // or we haven't explicitly tried to set a thinking budget/level, then bail out.\n const modelVersion = getModelVersion(model);\n const modelSpecialty = getModelSpecialty(model);\n const thoughtsNotSupported =\n modelVersion === \"2.5\" && modelSpecialty === \"image\";\n if (\n thoughtsNotSupported ||\n typeof thinkingBudget === \"undefined\" ||\n typeof thinkingLevel === \"undefined\"\n ) {\n return undefined;\n }\n\n // If we have gotten this far, then we want to explicitly set if we include thoughts or not.\n const thinkingConfig: Gemini.ThinkingConfig = {\n includeThoughts,\n };\n\n // Explicitly setting the budget/level is only valid (currently) for text models.\n if (modelSpecialty === \"\") {\n if (model.startsWith(\"gemini-2.5\")) {\n thinkingConfig.thinkingBudget = thinkingBudget;\n } else {\n thinkingConfig.thinkingLevel = thinkingLevel as Gemini.ThinkingLevel;\n }\n }\n\n return thinkingConfig;\n}\n\n/**\n * Builds the `speechConfig` object for the Gemini `generationConfig`,\n * normalising the user-friendly simplified forms into the wire format.\n *\n * @param fields The combined user/call-option fields.\n * @returns A populated {@link Gemini.SpeechConfig}, or `{}`\n * when no speech config was provided.\n */\nexport function convertFieldsToSpeechConfig(\n fields: ChatGoogleFields\n): Gemini.SpeechConfig | undefined {\n const config: Gemini.SpeechConfig | SimplifiedSpeechConfig | undefined =\n fields.speechConfig;\n if (typeof config === \"undefined\") {\n return undefined;\n }\n\n function isSpeechConfig(\n config: Gemini.SpeechConfig | SimplifiedSpeechConfig\n ): config is Gemini.SpeechConfig {\n return (\n typeof config === \"object\" &&\n (Object.hasOwn(config, \"voiceConfig\") ||\n Object.hasOwn(config, \"multiSpeakerVoiceConfig\"))\n );\n }\n\n function hasLanguage(\n config: SimplifiedSpeechConfig\n ): config is SimplifiedSpeechLanguageConfig {\n return typeof config === \"object\" && Object.hasOwn(config, \"languageCode\");\n }\n\n function hasVoice(\n config: SimplifiedSpeechLanguageConfig\n ): config is SpeechVoiceLanguage {\n return Object.hasOwn(config, \"voice\");\n }\n\n // If this is already a SpeechConfig, just return it\n if (isSpeechConfig(config)) {\n return config;\n }\n\n let languageCode: string | undefined;\n let voice: SpeechVoice;\n if (hasLanguage(config)) {\n languageCode = config.languageCode;\n voice = hasVoice(config) ? config.voice : config.voices;\n } else {\n languageCode = undefined;\n voice = config;\n }\n\n let ret: Gemini.SpeechConfig;\n\n if (typeof voice === \"string\") {\n // They just provided the prebuilt voice configuration name. Use it.\n ret = {\n voiceConfig: {\n prebuiltVoiceConfig: {\n voiceName: voice,\n },\n },\n };\n } else {\n // This is multi-speaker, so we have speaker/name pairs\n // If we have just one (why?), turn it into an array for the moment\n const voices: SpeechSpeakerName[] = Array.isArray(voice) ? voice : [voice];\n // Go through all the speaker/name pairs and turn this into the voice config array\n const speakerVoiceConfigs: Gemini.SpeakerVoiceConfig[] = voices.map(\n (v: SpeechSpeakerName): Gemini.SpeakerVoiceConfig => ({\n speaker: v.speaker,\n voiceConfig: {\n prebuiltVoiceConfig: {\n voiceName: v.name,\n },\n },\n })\n );\n // Create the multi-speaker voice configuration\n ret = {\n multiSpeakerVoiceConfig: {\n speakerVoiceConfigs,\n },\n };\n }\n\n if (languageCode) {\n ret.languageCode = languageCode;\n }\n\n return ret;\n}\n\ntype ServiceTierResponse = {} | { serviceTier: Gemini.ServiceTier };\n\n/**\n * Builds an object that can be included as part of the request\n * which will add a `speechConfig` attribute if appropriate.\n */\nexport function convertFieldsToServiceTier(\n platform: GooglePlatformType,\n fields: ChatGoogleFields\n): ServiceTierResponse {\n // Service tier is handled in a different way on GCP\n if (platform === \"gcp\") {\n return {};\n }\n\n const serviceTier = fields.serviceTier;\n if (!serviceTier || !settableServiceTier.includes(serviceTier)) {\n return {};\n }\n\n return {\n serviceTier: serviceTier,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"params.js","names":[],"sources":["../../src/converters/params.ts"],"sourcesContent":["import {\n ChatGoogleFields,\n GooglePlatformType,\n settableServiceTier,\n SimplifiedSpeechConfig,\n SimplifiedSpeechLanguageConfig,\n SpeechSpeakerName,\n SpeechVoice,\n SpeechVoiceLanguage,\n} from \"../chat_models/types.js\";\nimport type { Gemini } from \"../chat_models/api-types.js\";\n\n/**\n * Converts a reasoning \"effort\" level value (e.g., \"minimal\", \"low\", \"medium\", \"high\")\n * to a corresponding reasoning token budget, depending on the target Gemini model.\n *\n * This mapping is essential when configuring Google Gemini API calls with reasoning\n * effort/level fields (as in the Gemini OpenAI-compatible API) or when translating\n * user-friendly configuration to explicit token budgets.\n *\n * - The mapping is refined to reflect the constraints documented by Google:\n * - For \"gemini-2.5-flash\" models, token budgets may be set as low as 0 and up to 24k.\n * - For other Gemini models, minimum is 128, maximum is 32k.\n * - Standard effort levels and their token mappings:\n * - \"none\" / \"minimal\": lowest allowed (minTokens)\n * - \"low\": 1k tokens\n * - \"medium\": 8k tokens\n * - \"high\": maximum allowed for the model (maxTokens)\n * - See reference:\n * https://ai.google.dev/gemini-api/docs/thinking#thinking-levels\n * https://ai.google.dev/gemini-api/docs/openai#thinking\n *\n * @param {string | undefined} modelName\n * The Gemini model name (e.g., \"gemini-2.5-flash\"). Used to determine token range constraints.\n * @param {string | undefined} effort\n * The reasoning effort level: one of \"none\", \"minimal\", \"low\", \"medium\", \"high\"\n * (case-insensitive). If undefined, the function returns undefined.\n * @returns {number | undefined}\n * The corresponding token count for the given effort and model. Returns undefined if\n * the effort is not recognized or not provided.\n *\n * @example\n * convertReasoningEffortToReasoningTokens(\"gemini-2.5-flash\", \"medium\") // 8192\n * convertReasoningEffortToReasoningTokens(\"gemini-1.5-pro\", \"high\") // 32768\n * convertReasoningEffortToReasoningTokens(\"gemini-2.5-flash\", \"none\") // 0\n * convertReasoningEffortToReasoningTokens(undefined, \"low\") // 1024\n */\nexport function convertReasoningEffortToReasoningTokens(\n modelName?: string,\n effort?: string\n): number | undefined {\n if (effort === undefined) {\n return undefined;\n }\n\n // gemini-2.5-flash and -flash-lite can be disabled. Others can't.\n // https://ai.google.dev/gemini-api/docs/thinking#thinking-levels\n const minTokens: number = modelName?.startsWith(\"gemini-2.5-flash\") ? 0 : 128;\n const maxTokens: number = modelName?.startsWith(\"gemini-2.5-flash\")\n ? 24 * 1024\n : 32 * 1024;\n\n const normalizedEffort = effort.toLowerCase();\n switch (normalizedEffort) {\n case \"none\":\n case \"minimal\":\n return minTokens;\n case \"low\":\n // Defined as 1k by https://ai.google.dev/gemini-api/docs/openai#thinking\n return 1024;\n case \"medium\":\n // Defined as 8k by https://ai.google.dev/gemini-api/docs/openai#thinking\n return 8 * 1024;\n case \"high\":\n // Defined as 24k or 32k (model-dependent) by https://ai.google.dev/gemini-api/docs/openai#thinking\n return maxTokens;\n default:\n return undefined;\n }\n}\n\n/**\n * Converts a reasoning token budget (e.g., 0, 1024, 8192, 32768) to its corresponding\n * reasoning \"effort\" level, as used in the Google Gemini API (\"minimal\", \"low\", \"medium\", \"high\").\n *\n * This is the inverse of {@link convertReasoningEffortToReasoningTokens}. It is useful for\n * translating explicit numeric settings (often surfaced in raw API responses or configs)\n * into user-facing friendly reasoning levels/labels. The mapping relies on Google's\n * documented reasoning levels for different models:\n *\n * - -1 means default effort (per API documentation: this is \"minimal\" for flash-lite models\n * and \"high\" (dynamic) for others).\n * - 0 means \"minimal\" unless the model is \"gemini-3-pro\", for which it maps to \"low\".\n * - <= 1024 tokens: \"low\"\n * - <= 8192 tokens: \"medium\" (unless model is \"gemini-3-pro\", for which it reverts to \"low\")\n * - > 8192: \"high\"\n *\n * Reference:\n * https://ai.google.dev/gemini-api/docs/thinking#thinking-levels\n * https://ai.google.dev/gemini-api/docs/openai#thinking\n *\n * @param {string | undefined} model\n * The Gemini model name (e.g., \"gemini-3-pro\"). Used for model-specific special cases.\n * @param {number | undefined} reasoningTokens\n * The token budget to convert. If undefined, the function returns undefined.\n * @returns {Lowercase<Gemini.ThinkingLevel> | undefined}\n * The reasoning \"effort\" level (\"minimal\", \"low\", \"medium\", \"high\") or undefined if input is not valid.\n *\n * @example\n * convertReasoningTokensToReasoningEffort(\"gemini-3-pro\", 0) // \"low\"\n * convertReasoningTokensToReasoningEffort(\"gemini-1.5-pro\", 0) // \"minimal\"\n * convertReasoningTokensToReasoningEffort(undefined, 1024) // \"low\"\n * convertReasoningTokensToReasoningEffort(undefined, -1) // \"high\"\n * convertReasoningTokensToReasoningEffort(undefined, 9000) // \"high\"\n */\nexport function convertReasoningTokensToReasoningEffort(\n model?: string,\n reasoningTokens?: number\n): Gemini.ThinkingLevel | undefined {\n if (typeof reasoningTokens === \"undefined\") {\n return undefined;\n } else if (reasoningTokens === -1) {\n // -1 means \"minimal\" for flash-lite models and \"high\"/dynamic for others\n // (\"default\" per https://ai.google.dev/gemini-api/docs/thinking#thinking-levels)\n if (model && getModelLevel(model) === \"flash-lite\") {\n return \"minimal\";\n }\n return \"high\";\n } else if (reasoningTokens === 0) {\n if (model?.startsWith(\"gemini-3-pro\")) {\n return \"low\";\n } else {\n return \"minimal\";\n }\n } else if (reasoningTokens <= 1024) {\n return \"low\";\n } else if (reasoningTokens <= 8192) {\n if (model?.startsWith(\"gemini-3-pro\")) {\n return \"low\";\n } else {\n return \"medium\";\n }\n } else {\n return \"high\";\n }\n}\n\n/**\n * Infers and returns the Google platform type from the provided chat model parameters.\n *\n * This function helps determine which Google platform (e.g., Vertex AI on GCP or the public API)\n * should be used for calling Gemini/PaLM endpoints, based on the options provided in\n * {@link BaseChatGoogleParams} (such as model constructors or call options).\n */\nexport function convertParamsToPlatformType(params: {\n platformType?: GooglePlatformType;\n vertexai?: boolean;\n}): GooglePlatformType | undefined {\n if (typeof params === \"undefined\") {\n return undefined;\n }\n if (typeof params.platformType !== \"undefined\") {\n return params.platformType;\n }\n if (params.vertexai === true) {\n return \"gcp\";\n }\n return undefined;\n}\n\n/**\n * Returns the model family from a Gemini model name.\n * e.g. \"gemini-2.5-flash\" → \"gemini\"\n */\nexport function getModelFamily(model: string): string {\n return model.split(\"-\")[0];\n}\n\n/**\n * Returns the model version from a Gemini model name.\n * e.g. \"gemini-2.5-flash\" → \"2.5\"\n */\nexport function getModelVersion(model: string): string {\n return model.split(\"-\")[1];\n}\n\n/**\n * Returns the model level from a Gemini model name.\n * e.g. \"gemini-2.5-flash\" → \"flash\", \"gemini-2.5-flash-lite\" → \"flash-lite\"\n */\nexport function getModelLevel(model: string): string {\n const parts = model.split(\"-\");\n let ret = parts[2];\n if (ret === \"flash\" && parts[3] === \"lite\") {\n ret = \"flash-lite\";\n }\n return ret;\n}\n\n/**\n * Returns the model specialty from a Gemini model name.\n * e.g. \"gemini-2.5-flash-preview-image\" → \"image\",\n * \"gemini-2.5-flash-preview-tts\" → \"tts\",\n * \"gemini-2.5-flash\" → \"\"\n */\nexport function getModelSpecialty(model: string): string {\n if (model.includes(\"image\")) {\n return \"image\";\n } else if (model.includes(\"tts\")) {\n return \"tts\";\n } else {\n return \"\";\n }\n}\n\n/**\n * Builds the `thinkingConfig` object for the Gemini `generationConfig`,\n * translating user-facing reasoning fields into the wire format.\n *\n * @param model The full Gemini model name (e.g. \"gemini-2.5-flash\").\n * @param fields The combined user/call-option fields.\n * @returns A populated {@link Gemini.ThinkingConfig}, or `{}`\n * when thinking should not be explicitly configured.\n */\nexport function convertFieldsToThinkingConfig(\n model: string,\n fields: ChatGoogleFields\n): Gemini.ThinkingConfig | undefined {\n // Thinking / reasoning\n let includeThoughts = true;\n\n let thinkingBudget =\n fields?.maxReasoningTokens ??\n fields?.thinkingBudget ??\n convertReasoningEffortToReasoningTokens(\n model,\n fields?.reasoningEffort ?? fields?.thinkingLevel\n );\n if (\n thinkingBudget === 0 ||\n (model.includes(\"pro\") && thinkingBudget === 128)\n ) {\n includeThoughts = false;\n }\n if (\n model.startsWith(\"gemini-2.5-pro\") &&\n typeof thinkingBudget !== \"undefined\"\n ) {\n // Can't turn off Gemini 2.5 Pro thinking completely\n if (thinkingBudget >= 0 && thinkingBudget < 128) {\n thinkingBudget = 128;\n }\n }\n\n const thinkingLevelRaw =\n fields?.reasoningEffort ??\n fields?.thinkingLevel ??\n convertReasoningTokensToReasoningEffort(\n model,\n fields?.maxReasoningTokens ?? fields?.thinkingBudget\n );\n let thinkingLevel = thinkingLevelRaw?.toUpperCase();\n if (thinkingLevel === \"MINIMAL\") {\n includeThoughts = false;\n }\n if (model.startsWith(\"gemini-3-pro\")) {\n // Gemini 3 Pro has only low and high.\n if (thinkingLevel === \"MINIMAL\") {\n thinkingLevel = \"LOW\";\n } else if (thinkingLevel === \"MEDIUM\") {\n thinkingLevel = \"HIGH\";\n }\n } else if (model.startsWith(\"gemini-3.1-pro\")) {\n // Gemini 3.1 Pro does not have \"minimal\"\n if (thinkingLevel === \"MINIMAL\") {\n thinkingLevel = \"LOW\";\n }\n }\n\n // If we are using a model that doesn't support thinking at all (gemini 2.5 imaging)\n // or we haven't explicitly tried to set a thinking budget/level, then bail out.\n const modelVersion = getModelVersion(model);\n const modelSpecialty = getModelSpecialty(model);\n const thoughtsNotSupported =\n modelVersion === \"2.5\" && modelSpecialty === \"image\";\n if (\n thoughtsNotSupported ||\n typeof thinkingBudget === \"undefined\" ||\n typeof thinkingLevel === \"undefined\"\n ) {\n return undefined;\n }\n\n // If we have gotten this far, then we want to explicitly set if we include thoughts or not.\n const thinkingConfig: Gemini.ThinkingConfig = {\n includeThoughts,\n };\n\n // Explicitly setting the budget/level is only valid (currently) for text models.\n if (modelSpecialty === \"\") {\n if (model.startsWith(\"gemini-2.5\")) {\n thinkingConfig.thinkingBudget = thinkingBudget;\n } else {\n thinkingConfig.thinkingLevel = thinkingLevel as Gemini.ThinkingLevel;\n }\n }\n\n return thinkingConfig;\n}\n\n/**\n * Builds the `speechConfig` object for the Gemini `generationConfig`,\n * normalising the user-friendly simplified forms into the wire format.\n *\n * @param fields The combined user/call-option fields.\n * @returns A populated {@link Gemini.SpeechConfig}, or `{}`\n * when no speech config was provided.\n */\nexport function convertFieldsToSpeechConfig(\n fields: ChatGoogleFields\n): Gemini.SpeechConfig | undefined {\n const config: Gemini.SpeechConfig | SimplifiedSpeechConfig | undefined =\n fields.speechConfig;\n if (typeof config === \"undefined\") {\n return undefined;\n }\n\n function isSpeechConfig(\n config: Gemini.SpeechConfig | SimplifiedSpeechConfig\n ): config is Gemini.SpeechConfig {\n return (\n typeof config === \"object\" &&\n (Object.hasOwn(config, \"voiceConfig\") ||\n Object.hasOwn(config, \"multiSpeakerVoiceConfig\"))\n );\n }\n\n function hasLanguage(\n config: SimplifiedSpeechConfig\n ): config is SimplifiedSpeechLanguageConfig {\n return typeof config === \"object\" && Object.hasOwn(config, \"languageCode\");\n }\n\n function hasVoice(\n config: SimplifiedSpeechLanguageConfig\n ): config is SpeechVoiceLanguage {\n return Object.hasOwn(config, \"voice\");\n }\n\n // If this is already a SpeechConfig, just return it\n if (isSpeechConfig(config)) {\n return config;\n }\n\n let languageCode: string | undefined;\n let voice: SpeechVoice;\n if (hasLanguage(config)) {\n languageCode = config.languageCode;\n voice = hasVoice(config) ? config.voice : config.voices;\n } else {\n languageCode = undefined;\n voice = config;\n }\n\n let ret: Gemini.SpeechConfig;\n\n if (typeof voice === \"string\") {\n // They just provided the prebuilt voice configuration name. Use it.\n ret = {\n voiceConfig: {\n prebuiltVoiceConfig: {\n voiceName: voice,\n },\n },\n };\n } else {\n // This is multi-speaker, so we have speaker/name pairs\n // If we have just one (why?), turn it into an array for the moment\n const voices: SpeechSpeakerName[] = Array.isArray(voice) ? voice : [voice];\n // Go through all the speaker/name pairs and turn this into the voice config array\n const speakerVoiceConfigs: Gemini.SpeakerVoiceConfig[] = voices.map(\n (v: SpeechSpeakerName): Gemini.SpeakerVoiceConfig => ({\n speaker: v.speaker,\n voiceConfig: {\n prebuiltVoiceConfig: {\n voiceName: v.name,\n },\n },\n })\n );\n // Create the multi-speaker voice configuration\n ret = {\n multiSpeakerVoiceConfig: {\n speakerVoiceConfigs,\n },\n };\n }\n\n if (languageCode) {\n ret.languageCode = languageCode;\n }\n\n return ret;\n}\n\ntype ServiceTierResponse = {} | { serviceTier: Gemini.ServiceTier };\n\n/**\n * Builds an object that can be included as part of the request\n * which will add a `speechConfig` attribute if appropriate.\n */\nexport function convertFieldsToServiceTier(\n platform: GooglePlatformType,\n fields: ChatGoogleFields\n): ServiceTierResponse {\n // Service tier is handled in a different way on GCP\n if (platform === \"gcp\") {\n return {};\n }\n\n const serviceTier = fields.serviceTier;\n if (!serviceTier || !settableServiceTier.includes(serviceTier)) {\n return {};\n }\n\n return {\n serviceTier: serviceTier,\n };\n}\n"],"mappings":"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"}
|
|
@@ -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","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;AACrB,KAAI,CAAC,MAAM,QAAQ,IAAI,KAAK,CAC1B,QAAO;CAGT,MAAM,MAAM,IAAI,KAAK;CACrB,MAAM,YAAY,IAAI,KAAK,QAAQ,OAAO;AAC1C,KAAI,QAAQ,KAAK,aAAa,GAAG;EAG/B,MAAM,YAAY,cAAc,IAAI,IAAI;AACxC,MAAI,OAAO,IAAI,KAAK;AACpB,MAAI,WAAW;YACN,QAAQ,KAAK,cAAc,EAEpC,OAAM,IAAIA,eAAAA,kBACR,2GACD;UACQ,QAAQ,EAGjB,KAAI,OAAO,KAAK,KAAK;UACZ,QAAQ,EAEjB,OAAM,IAAIA,eAAAA,kBACR,4FACD;KAGD,OAAM,IAAIA,eAAAA,kBACR,sFACD;AAEH,QAAO;;;;;;;;;;;;;;;;AAiBT,SAAS,2BAEP,KAC4C;AAC5C,KAAI,OAAO,QAAQ,YAAY,QAAQ,MAAM;EAC3C,MAAM,SAAS,EAAE,GAAG,KAAK;AAEzB,MAAI,0BAA0B,OAC5B,QAAO,OAAO;AAGhB,mBAAiB,OAAO;AAExB,OAAK,MAAM,OAAO,OAChB,KAAI,OAAO;OACL,MAAM,QAAQ,OAAO,KAAK,CAC5B,QAAO,OAAO,OAAO,KAAK,IAAI,2BAA2B;YAChD,OAAO,OAAO,SAAS,YAAY,OAAO,SAAS,KAC5D,QAAO,OAAO,2BAA2B,OAAO,KAAK;;AAK3D,SAAO;;AAGT,QAAO;;;;;;;;;;;;;;AAeT,SAAgB,yBAId,QAIqB;CAUrB,MAAM,EAAE,SAAS,GAAG,SALD,4BAAA,GAAA,4BAAA,oBACE,OAAO,KAAA,GAAA,sCAAA,sBAAyB,OAAO,IAAA,GAAA,kCAAA,cACzC,OAAO,GACpB,OAEiC;AACvC,QAAO;;;;;;;;;;AAWT,SAAS,6BAEP,QACqB;CAErB,MAAM,EAAE,SAAS,GAAG,SADD,2BAA2B,OACP;AAEvC,QAAO;;;;;;;;;;;;;;;;;;AAmBT,SAAS,aAAa,MAA2C;AAC/D,KAAI,OAAO,SAAS,YAAY,SAAS,KACvC,QAAO;AAcT,QAAO;EAVL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAGyB,CAAC,MAAM,SAAS,QAAQ,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0D1D,MAAa,4CAGR,UAAU;CACb,MAAM,uBAA2D,EAAE;AAEnE,MAAK,MAAM,QAAQ,OAAO;AAExB,MAAI,aAAa,KAAK,IAAI,EAAE,0BAA0B,MACpD;AAIF,MACE,aAAa,KAAK,IAClB,0BAA0B,QAC1B,MAAM,QAAQ,KAAK,qBAAqB,EACxC;AACA,wBAAqB,KAAK,GAAG,KAAK,qBAAqB;AACvD;;AAIF,OAAA,GAAA,uCAAA,iBAAoB,KAAK,EAAE;GACzB,MAAM,aAAa,yBAAyB,KAAK,OAAO;AACxD,wBAAqB,KAAK;IACxB,MAAM,KAAK;IACX,aAAa,KAAK,eAAe;IACjC,YAAY;IACb,CAAC;AACF;;AAIF,OAAA,GAAA,qCAAA,cAAiB,KAAK,EAAE;AACtB,wBAAqB,KAAK;IACxB,MAAM,KAAK,SAAS;IACpB,aACE,KAAK,SAAS,eAAe;IAC/B,YAAY,6BAA6B,KAAK,SAAS,WAAW;IACnE,CAAC;AACF;;AAIF,MACE,OAAO,SAAS,YAChB,SAAS,QACT,UAAU,QACV,OAAO,KAAK,SAAS,UACrB;GACA,MAAM,WAAW;AACjB,wBAAqB,KAAK;IACxB,MAAM,SAAS;IACf,aAAa,SAAS,eAAe;IACrC,YAAY,SAAS;IACtB,CAAC;AACF;;AAGF,QAAM,IAAIC,eAAAA,iBAAiB,KAAK;;AAGlC,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8CT,MAAa,6BAGR,UAAU;AACb,KAAI,MAAM,WAAW,EACnB,QAAO,EAAE;CAGX,MAAM,cAA6B,EAAE;CACrC,MAAM,uBAA2D,EAAE;AAGnE,MAAK,MAAM,QAAQ,MACjB,KAAI,aAAa,KAAK,CACpB,KAAI,0BAA0B,KAE5B,aAAY,KAAK,KAAK;KAGtB,aAAY,KAAK,KAAK;UAEf,2BAA2B,KAKpC,aAAY,KAAK,EAFf,cAAc,EAAE,EAES,CAAC;MACvB;EAEL,MAAM,YAAY,yCAAyC,CAAC,KAAK,CAAC;AAClE,uBAAqB,KAAK,GAAG,UAAU;;AAK3C,KAAI,qBAAqB,SAAS,EAChC,aAAY,KAAK,EACf,sBACD,CAAC;AAGJ,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsDT,SAAgB,gCACd,YACA,UACqC;AACrC,KAAI,CAAC,YAAY,eAAe,KAAA,EAC9B;CAGF,IAAI;CACJ,IAAI;CAEJ,IAAI;CACJ,IAAI;AACJ,KAAI,OAAO,eAAe,UAAU;AAClC,mBAAiB,WAAW;AAC5B,uBAAqB,WAAW,UAAU;OAE1C,kBAAiB;AAGnB,KAAI,mBAAmB,OACrB,QAAO;UACE,mBAAmB,SAAS,mBAAmB,WACxD,QAAO;UACE,mBAAmB,OAC5B,QAAO;UACE,OAAO,mBAAmB,UAAU;AAC7C,SAAO;AACP,uBAAqB;;AAGvB,KAAI,mBACF,KAAI,MAAM,QAAQ,mBAAmB,CACnC,wBAAuB;KAEvB,wBAAuB,CAAC,mBAAmB;CAI/C,MAAM,wBAA4D,EAAE;AACpE,KAAI,sBAAsB,OACxB,uBAAsB,uBAAuB;AAE/C,KAAI,KACF,uBAAsB,OAAO;AAE/B,QAAO,EACL,uBACD"}
|
|
@@ -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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"}
|
|
@@ -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":";;;;;;;;;;AAaA,IAAI;AAEJ,eAAe,UAA+B;AAC5C,uBAAsB,OAAO;AAC7B,QAAO;;AA2CT,MAAM,wBAAQ,IAAI,KAAyB;AAC3C,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BtB,SAAgB,wBACd,aACgB;AAChB,QAAO,OAAO,gBAAgB,WAC1B,OAAO,OAAO,KAAK,MAAM,YAAY,CAAC,GACtC,OAAO,SAAS,YAAY,GAC1B,cACA,OAAO,OAAO,YAAY;;;;;;;;;;;;;;;;;;;;;AAsBlC,eAAsB,iBAAiB,aAA6B;CAClE,MAAM,EAAE,gBAAgB,MAAM,SAAS;AACvC,QAAO,YAAY,YAAY,aAAa,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BtD,eAAsB,kBAAkB,aAA6B;CACnE,MAAM,EAAE,YAAY,MAAM,SAAS;CACnC,MAAM,aAAa,MAAM,iBAAiB,YAAY;AAStD,QARoB,MAAM,IAAI,SAAS,CACpC,UAAU,YAAY,aAAa,CACnC,YAAY,YAAY,UAAU,CAClC,WAAW,YAAY,aAAa,CACpC,mBAAmB,EAAE,KAAK,SAAS,CAAC,CACpC,aAAa,CACb,kBAAkB,KAAK,CACvB,KAAK,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8CrB,eAAsB,6BACpB,aACiB;CACjB,MAAM,WAAW,YAAY;CAE7B,MAAM,cAAc,IAAI,IAAI,SAAS;AACrC,aAAY,aAAa,IAAI,OAAO,YAAY,eAAe;CAC/D,MAAM,WAAW,YAAY,UAAU;CAGvC,MAAM,aAAa,MAAM,IAAI,SAAS;CACtC,IAAI,MAAM,KAAK,MAAM,KAAK,KAAK,GAAG,IAAK;AAEvC,KAAI,WACF,KAAI,WAAW,UAAU,MAAM,eAAe;EAC5C,IAAI,QAAQ,MAAM,WAAW;AAE7B,MAAI,MAAM,UAAU,IAClB,QAAO,MAAM;OACR;GACL,MAAM,YAAY,MAAM,IAAI,SAAS;AAErC,OAAI,aAAa,cAAc,YAAY;AACzC,YAAQ,MAAM,UAAU;AACxB,QAAI,MAAM,UAAU,IAClB,QAAO,MAAM;QAEb,OAAM,OAAO,SAAS;;;OAK5B,OAAM,OAAO,SAAS;CAI1B,MAAM,UAAUA,aAAAA,KAAK,YAAY;AAC/B,QAAM,KAAK,MAAM,KAAK,KAAK,GAAG,IAAK;EAGnC,MAAM,MAAM,MAAM,kBAAkB,YAAY;EAChD,MAAM,OAAO,IAAI,iBAAiB;AAClC,OAAK,OAAO,cAAc,8CAA8C;AACxE,OAAK,OAAO,aAAa,IAAI;EAC7B,MAAM,MAAM,MAAM,MAAM,UAAU;GAChC,QAAQ;GACR,SAAS,EAAE,gBAAgB,qCAAqC;GAChE;GACD,CAAC;AAEF,MAAI,CAAC,IAAI,GACP,OAAM,MAAMC,eAAAA,UAAU,aAAa,IAAI;EAGzC,MAAM,OAAO,MAAM,IAAI,MAAM;AAE7B,MAAI,cAAc,MAAM;GACtB,MAAM,EAAE,cAAc,MAAM,SAAS;GACrC,MAAM,SAAS,UAAU,KAAK,SAAS;AACvC,UAAO;IAAE,OAAO,KAAK;IAAU,SAAS,OAAO;IAAe;;EAGhE,MAAM,eAAe,IAAI,QAAQ,IAAI,gBAAgB;AAKrD,SAAO;GAAE,SAJO,eACZ,KAAK,MAAM,IAAI,KAAK,aAAa,CAAC,SAAS,GAAG,IAAK,GAAG,KAAK,aAC3D,MAAM,KAAK;GAEG,OAAO,KAAK;GAAc;GAC5C;AAEF,OAAM,IAAI,UAAU;EAAE,SAAS;EAAK;EAAS,CAAC;AAC9C,QAAO,MAAM,QAAQ,MAAM,SAAS,KAAK,MAAM"}
|
|
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":";;;;;;;;;;AAaA,IAAI;AAEJ,eAAe,UAA+B;AAC5C,uBAAsB,OAAO;AAC7B,QAAO;;AA2CT,MAAM,wBAAQ,IAAI,KAAyB;AAC3C,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BtB,SAAgB,wBACd,aACgB;AAChB,QAAO,OAAO,gBAAgB,WAC1B,OAAO,OAAO,KAAK,MAAM,YAAY,CAAC,GACtC,OAAO,SAAS,YAAY,GAC1B,cACA,OAAO,OAAO,YAAY;;;;;;;;;;;;;;;;;;;;;AAsBlC,eAAsB,iBAAiB,aAA6B;CAClE,MAAM,EAAE,gBAAgB,MAAM,SAAS;AACvC,QAAO,YAAY,YAAY,aAAa,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BtD,eAAsB,kBAAkB,aAA6B;CACnE,MAAM,EAAE,YAAY,MAAM,SAAS;CACnC,MAAM,aAAa,MAAM,iBAAiB,YAAY;AAStD,QAAO,MARmB,IAAI,SAAS,CACpC,UAAU,YAAY,aAAa,CACnC,YAAY,YAAY,UAAU,CAClC,WAAW,YAAY,aAAa,CACpC,mBAAmB,EAAE,KAAK,SAAS,CAAC,CACpC,aAAa,CACb,kBAAkB,KAAK,CACvB,KAAK,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8CrB,eAAsB,6BACpB,aACiB;CACjB,MAAM,WAAW,YAAY;CAE7B,MAAM,cAAc,IAAI,IAAI,SAAS;AACrC,aAAY,aAAa,IAAI,OAAO,YAAY,eAAe;CAC/D,MAAM,WAAW,YAAY,UAAU;CAGvC,MAAM,aAAa,MAAM,IAAI,SAAS;CACtC,IAAI,MAAM,KAAK,MAAM,KAAK,KAAK,GAAG,IAAK;AAEvC,KAAI,WACF,KAAI,WAAW,UAAU,MAAM,eAAe;EAC5C,IAAI,QAAQ,MAAM,WAAW;AAE7B,MAAI,MAAM,UAAU,IAClB,QAAO,MAAM;OACR;GACL,MAAM,YAAY,MAAM,IAAI,SAAS;AAErC,OAAI,aAAa,cAAc,YAAY;AACzC,YAAQ,MAAM,UAAU;AACxB,QAAI,MAAM,UAAU,IAClB,QAAO,MAAM;QAEb,OAAM,OAAO,SAAS;;;OAK5B,OAAM,OAAO,SAAS;CAI1B,MAAM,UAAUA,aAAAA,KAAK,YAAY;AAC/B,QAAM,KAAK,MAAM,KAAK,KAAK,GAAG,IAAK;EAGnC,MAAM,MAAM,MAAM,kBAAkB,YAAY;EAChD,MAAM,OAAO,IAAI,iBAAiB;AAClC,OAAK,OAAO,cAAc,8CAA8C;AACxE,OAAK,OAAO,aAAa,IAAI;EAC7B,MAAM,MAAM,MAAM,MAAM,UAAU;GAChC,QAAQ;GACR,SAAS,EAAE,gBAAgB,qCAAqC;GAChE;GACD,CAAC;AAEF,MAAI,CAAC,IAAI,GACP,OAAM,MAAMC,eAAAA,UAAU,aAAa,IAAI;EAGzC,MAAM,OAAO,MAAM,IAAI,MAAM;AAE7B,MAAI,cAAc,MAAM;GACtB,MAAM,EAAE,cAAc,MAAM,SAAS;GACrC,MAAM,SAAS,UAAU,KAAK,SAAS;AACvC,UAAO;IAAE,OAAO,KAAK;IAAU,SAAS,OAAO;IAAe;;EAGhE,MAAM,eAAe,IAAI,QAAQ,IAAI,gBAAgB;AAKrD,SAAO;GAAE,SAJO,eACZ,KAAK,MAAM,IAAI,KAAK,aAAa,CAAC,SAAS,GAAG,IAAK,GAAG,KAAK,aAC3D,MAAM,KAAK;GAEG,OAAO,KAAK;GAAc;GAC5C;AAEF,OAAM,IAAI,UAAU;EAAE,SAAS;EAAK;EAAS,CAAC;AAC9C,QAAO,MAAM,QAAQ,MAAM,SAAS,KAAK,MAAM"}
|
|
@@ -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;AAC5C,uBAAsB,OAAO;AAC7B,QAAO;;AA2CT,MAAM,wBAAQ,IAAI,KAAyB;AAC3C,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BtB,SAAgB,wBACd,aACgB;AAChB,QAAO,OAAO,gBAAgB,WAC1B,OAAO,OAAO,KAAK,MAAM,YAAY,CAAC,GACtC,OAAO,SAAS,YAAY,GAC1B,cACA,OAAO,OAAO,YAAY;;;;;;;;;;;;;;;;;;;;;AAsBlC,eAAsB,iBAAiB,aAA6B;CAClE,MAAM,EAAE,gBAAgB,MAAM,SAAS;AACvC,QAAO,YAAY,YAAY,aAAa,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BtD,eAAsB,kBAAkB,aAA6B;CACnE,MAAM,EAAE,YAAY,MAAM,SAAS;CACnC,MAAM,aAAa,MAAM,iBAAiB,YAAY;AAStD,QARoB,MAAM,IAAI,SAAS,CACpC,UAAU,YAAY,aAAa,CACnC,YAAY,YAAY,UAAU,CAClC,WAAW,YAAY,aAAa,CACpC,mBAAmB,EAAE,KAAK,SAAS,CAAC,CACpC,aAAa,CACb,kBAAkB,KAAK,CACvB,KAAK,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8CrB,eAAsB,6BACpB,aACiB;CACjB,MAAM,WAAW,YAAY;CAE7B,MAAM,cAAc,IAAI,IAAI,SAAS;AACrC,aAAY,aAAa,IAAI,OAAO,YAAY,eAAe;CAC/D,MAAM,WAAW,YAAY,UAAU;CAGvC,MAAM,aAAa,MAAM,IAAI,SAAS;CACtC,IAAI,MAAM,KAAK,MAAM,KAAK,KAAK,GAAG,IAAK;AAEvC,KAAI,WACF,KAAI,WAAW,UAAU,MAAM,eAAe;EAC5C,IAAI,QAAQ,MAAM,WAAW;AAE7B,MAAI,MAAM,UAAU,IAClB,QAAO,MAAM;OACR;GACL,MAAM,YAAY,MAAM,IAAI,SAAS;AAErC,OAAI,aAAa,cAAc,YAAY;AACzC,YAAQ,MAAM,UAAU;AACxB,QAAI,MAAM,UAAU,IAClB,QAAO,MAAM;QAEb,OAAM,OAAO,SAAS;;;OAK5B,OAAM,OAAO,SAAS;CAI1B,MAAM,UAAU,KAAK,YAAY;AAC/B,QAAM,KAAK,MAAM,KAAK,KAAK,GAAG,IAAK;EAGnC,MAAM,MAAM,MAAM,kBAAkB,YAAY;EAChD,MAAM,OAAO,IAAI,iBAAiB;AAClC,OAAK,OAAO,cAAc,8CAA8C;AACxE,OAAK,OAAO,aAAa,IAAI;EAC7B,MAAM,MAAM,MAAM,MAAM,UAAU;GAChC,QAAQ;GACR,SAAS,EAAE,gBAAgB,qCAAqC;GAChE;GACD,CAAC;AAEF,MAAI,CAAC,IAAI,GACP,OAAM,MAAM,UAAU,aAAa,IAAI;EAGzC,MAAM,OAAO,MAAM,IAAI,MAAM;AAE7B,MAAI,cAAc,MAAM;GACtB,MAAM,EAAE,cAAc,MAAM,SAAS;GACrC,MAAM,SAAS,UAAU,KAAK,SAAS;AACvC,UAAO;IAAE,OAAO,KAAK;IAAU,SAAS,OAAO;IAAe;;EAGhE,MAAM,eAAe,IAAI,QAAQ,IAAI,gBAAgB;AAKrD,SAAO;GAAE,SAJO,eACZ,KAAK,MAAM,IAAI,KAAK,aAAa,CAAC,SAAS,GAAG,IAAK,GAAG,KAAK,aAC3D,MAAM,KAAK;GAEG,OAAO,KAAK;GAAc;GAC5C;AAEF,OAAM,IAAI,UAAU;EAAE,SAAS;EAAK;EAAS,CAAC;AAC9C,QAAO,MAAM,QAAQ,MAAM,SAAS,KAAK,MAAM"}
|
|
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;AAC5C,uBAAsB,OAAO;AAC7B,QAAO;;AA2CT,MAAM,wBAAQ,IAAI,KAAyB;AAC3C,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BtB,SAAgB,wBACd,aACgB;AAChB,QAAO,OAAO,gBAAgB,WAC1B,OAAO,OAAO,KAAK,MAAM,YAAY,CAAC,GACtC,OAAO,SAAS,YAAY,GAC1B,cACA,OAAO,OAAO,YAAY;;;;;;;;;;;;;;;;;;;;;AAsBlC,eAAsB,iBAAiB,aAA6B;CAClE,MAAM,EAAE,gBAAgB,MAAM,SAAS;AACvC,QAAO,YAAY,YAAY,aAAa,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BtD,eAAsB,kBAAkB,aAA6B;CACnE,MAAM,EAAE,YAAY,MAAM,SAAS;CACnC,MAAM,aAAa,MAAM,iBAAiB,YAAY;AAStD,QAAO,MARmB,IAAI,SAAS,CACpC,UAAU,YAAY,aAAa,CACnC,YAAY,YAAY,UAAU,CAClC,WAAW,YAAY,aAAa,CACpC,mBAAmB,EAAE,KAAK,SAAS,CAAC,CACpC,aAAa,CACb,kBAAkB,KAAK,CACvB,KAAK,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8CrB,eAAsB,6BACpB,aACiB;CACjB,MAAM,WAAW,YAAY;CAE7B,MAAM,cAAc,IAAI,IAAI,SAAS;AACrC,aAAY,aAAa,IAAI,OAAO,YAAY,eAAe;CAC/D,MAAM,WAAW,YAAY,UAAU;CAGvC,MAAM,aAAa,MAAM,IAAI,SAAS;CACtC,IAAI,MAAM,KAAK,MAAM,KAAK,KAAK,GAAG,IAAK;AAEvC,KAAI,WACF,KAAI,WAAW,UAAU,MAAM,eAAe;EAC5C,IAAI,QAAQ,MAAM,WAAW;AAE7B,MAAI,MAAM,UAAU,IAClB,QAAO,MAAM;OACR;GACL,MAAM,YAAY,MAAM,IAAI,SAAS;AAErC,OAAI,aAAa,cAAc,YAAY;AACzC,YAAQ,MAAM,UAAU;AACxB,QAAI,MAAM,UAAU,IAClB,QAAO,MAAM;QAEb,OAAM,OAAO,SAAS;;;OAK5B,OAAM,OAAO,SAAS;CAI1B,MAAM,UAAU,KAAK,YAAY;AAC/B,QAAM,KAAK,MAAM,KAAK,KAAK,GAAG,IAAK;EAGnC,MAAM,MAAM,MAAM,kBAAkB,YAAY;EAChD,MAAM,OAAO,IAAI,iBAAiB;AAClC,OAAK,OAAO,cAAc,8CAA8C;AACxE,OAAK,OAAO,aAAa,IAAI;EAC7B,MAAM,MAAM,MAAM,MAAM,UAAU;GAChC,QAAQ;GACR,SAAS,EAAE,gBAAgB,qCAAqC;GAChE;GACD,CAAC;AAEF,MAAI,CAAC,IAAI,GACP,OAAM,MAAM,UAAU,aAAa,IAAI;EAGzC,MAAM,OAAO,MAAM,IAAI,MAAM;AAE7B,MAAI,cAAc,MAAM;GACtB,MAAM,EAAE,cAAc,MAAM,SAAS;GACrC,MAAM,SAAS,UAAU,KAAK,SAAS;AACvC,UAAO;IAAE,OAAO,KAAK;IAAU,SAAS,OAAO;IAAe;;EAGhE,MAAM,eAAe,IAAI,QAAQ,IAAI,gBAAgB;AAKrD,SAAO;GAAE,SAJO,eACZ,KAAK,MAAM,IAAI,KAAK,aAAa,CAAC,SAAS,GAAG,IAAK,GAAG,KAAK,aAC3D,MAAM,KAAK;GAEG,OAAO,KAAK;GAAc;GAC5C;AAEF,OAAM,IAAI,UAAU;EAAE,SAAS;EAAK;EAAS,CAAC;AAC9C,QAAO,MAAM,QAAQ,MAAM,SAAS,KAAK,MAAM"}
|