@langchain/google 0.2.7 → 0.2.9

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.
@@ -136,6 +136,15 @@ function getModelSpecialty(model) {
136
136
  else if (model.includes("tts")) return "tts";
137
137
  else return "";
138
138
  }
139
+ const MINIMAL_THINKING_LEVEL_MODELS = [
140
+ "gemini-3-flash",
141
+ "gemini-3.1-flash-lite",
142
+ "gemini-3.5-flash",
143
+ "gemini-3.6-flash"
144
+ ];
145
+ function supportsMinimalThinkingLevel(model) {
146
+ return MINIMAL_THINKING_LEVEL_MODELS.some((prefix) => model.startsWith(prefix));
147
+ }
139
148
  /**
140
149
  * Builds the `thinkingConfig` object for the Gemini `generationConfig`,
141
150
  * translating user-facing reasoning fields into the wire format.
@@ -154,12 +163,8 @@ function convertFieldsToThinkingConfig(model, fields) {
154
163
  }
155
164
  let thinkingLevel = (fields?.reasoningEffort ?? fields?.thinkingLevel ?? convertReasoningTokensToReasoningEffort(model, fields?.maxReasoningTokens ?? fields?.thinkingBudget))?.toUpperCase();
156
165
  if (thinkingLevel === "MINIMAL") includeThoughts = false;
157
- if (model.startsWith("gemini-3-pro")) {
158
- if (thinkingLevel === "MINIMAL") thinkingLevel = "LOW";
159
- else if (thinkingLevel === "MEDIUM") thinkingLevel = "HIGH";
160
- } else if (model.startsWith("gemini-3.1-pro")) {
161
- if (thinkingLevel === "MINIMAL") thinkingLevel = "LOW";
162
- }
166
+ if (thinkingLevel === "MINIMAL" && model.startsWith("gemini-3") && !supportsMinimalThinkingLevel(model)) thinkingLevel = "LOW";
167
+ if (model.startsWith("gemini-3-pro") && thinkingLevel === "MEDIUM") thinkingLevel = "HIGH";
163
168
  const modelVersion = getModelVersion(model);
164
169
  const modelSpecialty = getModelSpecialty(model);
165
170
  if (modelVersion === "2.5" && modelSpecialty === "image" || typeof thinkingBudget === "undefined" || typeof thinkingLevel === "undefined") return;
@@ -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// Gemini 3 models that accept thinkingLevel \"MINIMAL\"; the others reject it with a 400, so they get \"LOW\".\n// TODO: Move this to model profile.\nconst MINIMAL_THINKING_LEVEL_MODELS = [\n \"gemini-3-flash\",\n \"gemini-3.1-flash-lite\",\n \"gemini-3.5-flash\",\n \"gemini-3.6-flash\",\n];\n\nfunction supportsMinimalThinkingLevel(model: string): boolean {\n return MINIMAL_THINKING_LEVEL_MODELS.some((prefix) =>\n model.startsWith(prefix)\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 (\n thinkingLevel === \"MINIMAL\" &&\n model.startsWith(\"gemini-3\") &&\n !supportsMinimalThinkingLevel(model)\n ) {\n thinkingLevel = \"LOW\";\n }\n if (model.startsWith(\"gemini-3-pro\") && thinkingLevel === \"MEDIUM\") {\n // Gemini 3 Pro has only low and high.\n thinkingLevel = \"HIGH\";\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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"}
@@ -136,6 +136,15 @@ function getModelSpecialty(model) {
136
136
  else if (model.includes("tts")) return "tts";
137
137
  else return "";
138
138
  }
139
+ const MINIMAL_THINKING_LEVEL_MODELS = [
140
+ "gemini-3-flash",
141
+ "gemini-3.1-flash-lite",
142
+ "gemini-3.5-flash",
143
+ "gemini-3.6-flash"
144
+ ];
145
+ function supportsMinimalThinkingLevel(model) {
146
+ return MINIMAL_THINKING_LEVEL_MODELS.some((prefix) => model.startsWith(prefix));
147
+ }
139
148
  /**
140
149
  * Builds the `thinkingConfig` object for the Gemini `generationConfig`,
141
150
  * translating user-facing reasoning fields into the wire format.
@@ -154,12 +163,8 @@ function convertFieldsToThinkingConfig(model, fields) {
154
163
  }
155
164
  let thinkingLevel = (fields?.reasoningEffort ?? fields?.thinkingLevel ?? convertReasoningTokensToReasoningEffort(model, fields?.maxReasoningTokens ?? fields?.thinkingBudget))?.toUpperCase();
156
165
  if (thinkingLevel === "MINIMAL") includeThoughts = false;
157
- if (model.startsWith("gemini-3-pro")) {
158
- if (thinkingLevel === "MINIMAL") thinkingLevel = "LOW";
159
- else if (thinkingLevel === "MEDIUM") thinkingLevel = "HIGH";
160
- } else if (model.startsWith("gemini-3.1-pro")) {
161
- if (thinkingLevel === "MINIMAL") thinkingLevel = "LOW";
162
- }
166
+ if (thinkingLevel === "MINIMAL" && model.startsWith("gemini-3") && !supportsMinimalThinkingLevel(model)) thinkingLevel = "LOW";
167
+ if (model.startsWith("gemini-3-pro") && thinkingLevel === "MEDIUM") thinkingLevel = "HIGH";
163
168
  const modelVersion = getModelVersion(model);
164
169
  const modelSpecialty = getModelSpecialty(model);
165
170
  if (modelVersion === "2.5" && modelSpecialty === "image" || typeof thinkingBudget === "undefined" || typeof thinkingLevel === "undefined") return;
@@ -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// Gemini 3 models that accept thinkingLevel \"MINIMAL\"; the others reject it with a 400, so they get \"LOW\".\n// TODO: Move this to model profile.\nconst MINIMAL_THINKING_LEVEL_MODELS = [\n \"gemini-3-flash\",\n \"gemini-3.1-flash-lite\",\n \"gemini-3.5-flash\",\n \"gemini-3.6-flash\",\n];\n\nfunction supportsMinimalThinkingLevel(model: string): boolean {\n return MINIMAL_THINKING_LEVEL_MODELS.some((prefix) =>\n model.startsWith(prefix)\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 (\n thinkingLevel === \"MINIMAL\" &&\n model.startsWith(\"gemini-3\") &&\n !supportsMinimalThinkingLevel(model)\n ) {\n thinkingLevel = \"LOW\";\n }\n if (model.startsWith(\"gemini-3-pro\") && thinkingLevel === \"MEDIUM\") {\n // Gemini 3 Pro has only low and high.\n thinkingLevel = \"HIGH\";\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+CA,SAAgB,wCACd,WACA,QACoB;CACpB,IAAI,WAAW,KAAA,GACb;CAKF,MAAM,YAAoB,WAAW,WAAW,kBAAkB,IAAI,IAAI;CAC1E,MAAM,YAAoB,WAAW,WAAW,kBAAkB,IAC9D,QACA;CAGJ,QADyB,OAAO,YACT,GAAvB;EACE,KAAK;EACL,KAAK,WACH,OAAO;EACT,KAAK,OAEH,OAAO;EACT,KAAK,UAEH,OAAO;EACT,KAAK,QAEH,OAAO;EACT,SACE;CACJ;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCA,SAAgB,wCACd,OACA,iBACkC;CAClC,IAAI,OAAO,oBAAoB,aAC7B;MACK,IAAI,oBAAoB,IAAI;EAGjC,IAAI,SAAS,cAAc,KAAK,MAAM,cACpC,OAAO;EAET,OAAO;CACT,OAAO,IAAI,oBAAoB,GAAG;EAChC,IAAI,OAAO,WAAW,cAAc,GAClC,OAAO;OAEP,OAAO;CAEX,OAAO,IAAI,mBAAmB,MAC5B,OAAO;MACF,IAAI,mBAAmB,MAAM;EAClC,IAAI,OAAO,WAAW,cAAc,GAClC,OAAO;OAEP,OAAO;CAEX,OACE,OAAO;AAEX;;;;;;;;AASA,SAAgB,4BAA4B,QAGT;CACjC,IAAI,OAAO,WAAW,aACpB;CAEF,IAAI,OAAO,OAAO,iBAAiB,aACjC,OAAO,OAAO;CAEhB,IAAI,OAAO,aAAa,MACtB,OAAO;AAGX;;;;;AAcA,SAAgB,gBAAgB,OAAuB;CACrD,OAAO,MAAM,MAAM,GAAG,CAAC,CAAC;AAC1B;;;;;AAMA,SAAgB,cAAc,OAAuB;CACnD,MAAM,QAAQ,MAAM,MAAM,GAAG;CAC7B,IAAI,MAAM,MAAM;CAChB,IAAI,QAAQ,WAAW,MAAM,OAAO,QAClC,MAAM;CAER,OAAO;AACT;;;;;;;AAQA,SAAgB,kBAAkB,OAAuB;CACvD,IAAI,MAAM,SAAS,OAAO,GACxB,OAAO;MACF,IAAI,MAAM,SAAS,KAAK,GAC7B,OAAO;MAEP,OAAO;AAEX;AAIA,MAAM,gCAAgC;CACpC;CACA;CACA;CACA;AACF;AAEA,SAAS,6BAA6B,OAAwB;CAC5D,OAAO,8BAA8B,MAAM,WACzC,MAAM,WAAW,MAAM,CACzB;AACF;;;;;;;;;;AAWA,SAAgB,8BACd,OACA,QACmC;CAEnC,IAAI,kBAAkB;CAEtB,IAAI,iBACF,QAAQ,sBACR,QAAQ,kBACR,wCACE,OACA,QAAQ,mBAAmB,QAAQ,aACrC;CACF,IACE,mBAAmB,KAClB,MAAM,SAAS,KAAK,KAAK,mBAAmB,KAE7C,kBAAkB;CAEpB,IACE,MAAM,WAAW,gBAAgB,KACjC,OAAO,mBAAmB,aAGtB;MAAA,kBAAkB,KAAK,iBAAiB,KAC1C,iBAAiB;CAAA;CAWrB,IAAI,iBANF,QAAQ,mBACR,QAAQ,iBACR,wCACE,OACA,QAAQ,sBAAsB,QAAQ,cACxC,EAAA,EACoC,YAAY;CAClD,IAAI,kBAAkB,WACpB,kBAAkB;CAEpB,IACE,kBAAkB,aAClB,MAAM,WAAW,UAAU,KAC3B,CAAC,6BAA6B,KAAK,GAEnC,gBAAgB;CAElB,IAAI,MAAM,WAAW,cAAc,KAAK,kBAAkB,UAExD,gBAAgB;CAKlB,MAAM,eAAe,gBAAgB,KAAK;CAC1C,MAAM,iBAAiB,kBAAkB,KAAK;CAG9C,IADE,iBAAiB,SAAS,mBAAmB,WAG7C,OAAO,mBAAmB,eAC1B,OAAO,kBAAkB,aAEzB;CAIF,MAAM,iBAAwC,EAC5C,gBACF;CAGA,IAAI,mBAAmB,IAAI;EACzB,IAAI,MAAM,WAAW,YAAY,GAC/B,eAAe,iBAAiB;OAEhC,eAAe,gBAAgB;CAEnC;CAEA,OAAO;AACT;;;;;;;;;AAUA,SAAgB,4BACd,QACiC;CACjC,MAAM,SACJ,OAAO;CACT,IAAI,OAAO,WAAW,aACpB;CAGF,SAAS,eACP,QAC+B;EAC/B,OACE,OAAO,WAAW,aACjB,OAAO,OAAO,QAAQ,aAAa,KAClC,OAAO,OAAO,QAAQ,yBAAyB;CAErD;CAEA,SAAS,YACP,QAC0C;EAC1C,OAAO,OAAO,WAAW,YAAY,OAAO,OAAO,QAAQ,cAAc;CAC3E;CAEA,SAAS,SACP,QAC+B;EAC/B,OAAO,OAAO,OAAO,QAAQ,OAAO;CACtC;CAGA,IAAI,eAAe,MAAM,GACvB,OAAO;CAGT,IAAI;CACJ,IAAI;CACJ,IAAI,YAAY,MAAM,GAAG;EACvB,eAAe,OAAO;EACtB,QAAQ,SAAS,MAAM,IAAI,OAAO,QAAQ,OAAO;CACnD,OAAO;EACL,eAAe,KAAA;EACf,QAAQ;CACV;CAEA,IAAI;CAEJ,IAAI,OAAO,UAAU,UAEnB,MAAM,EACJ,aAAa,EACX,qBAAqB,EACnB,WAAW,MACb,EACF,EACF;MAiBA,MAAM,EACJ,yBAAyB,EACvB,sBAfgC,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK,EAAA,CAET,KAC7D,OAAqD;EACpD,SAAS,EAAE;EACX,aAAa,EACX,qBAAqB,EACnB,WAAW,EAAE,KACf,EACF;CACF,EAKoB,EACpB,EACF;CAGF,IAAI,cACF,IAAI,eAAe;CAGrB,OAAO;AACT;;;;;AAQA,SAAgB,2BACd,UACA,QACqB;CAErB,IAAI,aAAa,OACf,OAAO,CAAC;CAGV,MAAM,cAAc,OAAO;CAC3B,IAAI,CAAC,eAAe,CAAC,oBAAoB,SAAS,WAAW,GAC3D,OAAO,CAAC;CAGV,OAAO,EACQ,YACf;AACF"}
@@ -1,3 +1,4 @@
1
+ const require_const = require("../const.cjs");
1
2
  const require_misc = require("./misc.cjs");
2
3
  const require_errors = require("./errors.cjs");
3
4
  //#region src/utils/gcp-auth.ts
@@ -78,24 +79,33 @@ async function getGCPPrivateKey(credentials) {
78
79
  * Token flow as specified by RFC 7523.
79
80
  *
80
81
  * @param credentials - The GCP service account credentials used to sign the JWT
82
+ * @param scopes - The OAuth scopes the resulting access token should carry.
83
+ * Google requires these on the assertion itself: the claim set
84
+ * for an access token request must include `scope`, "a
85
+ * space-delimited list of the permissions that the application
86
+ * requests". Omitting it makes the exchange fail with
87
+ * `Invalid OAuth scope or ID token audience provided`.
81
88
  * @returns A Promise that resolves to the signed JWT string
82
89
  *
83
90
  * @throws {Error} If the private key cannot be imported or the JWT cannot be signed
84
91
  *
85
92
  * @see https://datatracker.ietf.org/doc/html/rfc7523
93
+ * @see https://developers.google.com/identity/protocols/oauth2/service-account
86
94
  * @see https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials
87
95
  *
88
96
  * @example
89
97
  * ```typescript
90
98
  * const credentials = normalizeGCPCredentials(credentialsJson);
91
- * const customToken = await getGCPCustomToken(credentials);
99
+ * const customToken = await getGCPCustomToken(credentials, [
100
+ * "https://www.googleapis.com/auth/cloud-platform",
101
+ * ]);
92
102
  * // Use customToken to request an access token
93
103
  * ```
94
104
  */
95
- async function getGCPCustomToken(credentials) {
105
+ async function getGCPCustomToken(credentials, scopes = require_const.VERTEX_AI_AUTH_SCOPES) {
96
106
  const { SignJWT } = await getJose();
97
107
  const privateKey = await getGCPPrivateKey(credentials);
98
- return await new SignJWT().setIssuer(credentials.client_email).setAudience(credentials.token_uri).setSubject(credentials.client_email).setProtectedHeader({ alg: "RS256" }).setIssuedAt().setExpirationTime("1h").sign(privateKey);
108
+ return await new SignJWT({ scope: scopes.join(" ") }).setIssuer(credentials.client_email).setAudience(credentials.token_uri).setSubject(credentials.client_email).setProtectedHeader({ alg: "RS256" }).setIssuedAt().setExpirationTime("1h").sign(privateKey);
99
109
  }
100
110
  /**
101
111
  * Retrieves an access token for authenticating with Google Cloud APIs.
@@ -139,10 +149,11 @@ async function getGCPCustomToken(credentials) {
139
149
  * This ensures that even if a cache entry is fresh, an expired token
140
150
  * will trigger a refresh request.
141
151
  */
142
- async function getGCPCredentialsAccessToken(credentials) {
152
+ async function getGCPCredentialsAccessToken(credentials, scopes = require_const.VERTEX_AI_AUTH_SCOPES) {
143
153
  const tokenUrl = credentials.token_uri;
144
154
  const cacheKeyUrl = new URL(tokenUrl);
145
155
  cacheKeyUrl.searchParams.set("key", credentials.private_key_id);
156
+ cacheKeyUrl.searchParams.set("scope", [...scopes].sort().join(" "));
146
157
  const cacheKey = cacheKeyUrl.toString();
147
158
  const cacheValue = cache.get(cacheKey);
148
159
  let now = Math.floor(Date.now() / 1e3);
@@ -162,7 +173,7 @@ async function getGCPCredentialsAccessToken(credentials) {
162
173
  }
163
174
  const promise = require_misc.iife(async () => {
164
175
  now = Math.floor(Date.now() / 1e3);
165
- const jwt = await getGCPCustomToken(credentials);
176
+ const jwt = await getGCPCustomToken(credentials, scopes);
166
177
  const body = new URLSearchParams();
167
178
  body.append("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer");
168
179
  body.append("assertion", jwt);
@@ -1 +1 @@
1
- {"version":3,"file":"gcp-auth.cjs","names":["iife","AuthError"],"sources":["../../src/utils/gcp-auth.ts"],"sourcesContent":["/**\n * GCP authentication utilities.\n *\n * Adapted from: https://github.com/kriasoft/web-auth-library\n * Original source: web-auth-library by Kriasoft\n * License: MIT (https://github.com/kriasoft/web-auth-library/blob/main/LICENSE)\n */\n\nimport { AuthError } from \"../utils/errors.js\";\nimport { iife } from \"../utils/misc.js\";\n\ntype JoseModule = typeof import(\"jose\");\n\nlet joseModulePromise: Promise<JoseModule> | undefined;\n\nasync function getJose(): Promise<JoseModule> {\n joseModulePromise ??= import(\"jose\");\n return joseModulePromise;\n}\n\n/**\n * Google Cloud Platform service account credentials interface.\n *\n * This interface represents the structure of a GCP service account key file,\n * which is used for authenticating with Google Cloud APIs. Service account\n * keys can be downloaded from the Google Cloud Console and contain all the\n * information needed to authenticate as a service account.\n *\n * @see https://cloud.google.com/iam/docs/creating-managing-service-account-keys\n */\nexport interface GCPCredentials {\n /** The type of credential, typically \"service_account\" */\n type: string;\n /** The GCP project ID associated with this service account */\n project_id: string;\n /** The unique identifier for the private key */\n private_key_id: string;\n /** The RSA private key in PEM format used for signing JWTs */\n private_key: string;\n /** The unique client ID for this service account */\n client_id: string;\n /** The email address of the service account */\n client_email: string;\n /** The OAuth2 authorization endpoint URI */\n auth_uri: string;\n /** The OAuth2 token endpoint URI where access tokens are requested */\n token_uri: string;\n /** The URL for the OAuth2 provider's x509 certificate */\n auth_provider_x509_cert_url: string;\n /** The URL for this service account's x509 certificate */\n client_x509_cert_url: string;\n}\n\ninterface CacheValue {\n /** Unix timestamp (in seconds) when this cache entry was created */\n created: number;\n /** Promise that resolves to the token and its expiration time */\n promise: Promise<{ token: string; expires: number }>;\n}\n\nconst cache = new Map<string, CacheValue>();\nconst CACHE_MAX_AGE = 60 * 60; // 1 hour\n\n/**\n * Normalizes GCP credentials to a frozen GCPCredentials object.\n *\n * This function accepts credentials in either string (JSON) or object format\n * and ensures the returned value is a frozen (immutable) GCPCredentials object.\n * Freezing the credentials prevents accidental modification and ensures\n * consistency throughout the application lifecycle.\n *\n * @param credentials - The GCP service account credentials, either as a JSON string\n * or as a GCPCredentials object\n * @returns A frozen GCPCredentials object that cannot be modified\n *\n * @example\n * ```typescript\n * // From JSON string\n * const creds = normalizeGCPCredentials('{\"type\":\"service_account\",...}');\n *\n * // From object\n * const creds = normalizeGCPCredentials({\n * type: \"service_account\",\n * project_id: \"my-project\",\n * // ... other fields\n * });\n * ```\n */\nexport function normalizeGCPCredentials(\n credentials: string | GCPCredentials\n): GCPCredentials {\n return typeof credentials === \"string\"\n ? Object.freeze(JSON.parse(credentials))\n : Object.isFrozen(credentials)\n ? credentials\n : Object.freeze(credentials);\n}\n\n/**\n * Imports and returns the private key from GCP credentials for JWT signing.\n *\n * This function extracts the RSA private key from the service account credentials\n * and imports it using the PKCS#8 format with the RS256 algorithm. The returned\n * key can be used to sign JWTs for authentication with Google Cloud services.\n *\n * @param credentials - The GCP service account credentials containing the private key\n * @returns A Promise that resolves to the imported private key suitable for JWT signing\n *\n * @throws {Error} If the private key format is invalid or cannot be imported\n *\n * @example\n * ```typescript\n * const credentials = normalizeGCPCredentials(credentialsJson);\n * const privateKey = await getGCPPrivateKey(credentials);\n * // Use privateKey for signing operations\n * ```\n */\nexport async function getGCPPrivateKey(credentials: GCPCredentials) {\n const { importPKCS8 } = await getJose();\n return importPKCS8(credentials.private_key, \"RS256\");\n}\n\n/**\n * Generates a custom JWT token for authenticating with Google Cloud services.\n *\n * This function creates a signed JWT (JSON Web Token) using the service account's\n * private key. The token includes standard claims such as issuer, audience, subject,\n * issued-at time, and expiration time. This custom token can be exchanged for an\n * access token or ID token through Google's OAuth2 token endpoint.\n *\n * The generated JWT is valid for 1 hour and follows the OAuth 2.0 JWT Bearer\n * Token flow as specified by RFC 7523.\n *\n * @param credentials - The GCP service account credentials used to sign the JWT\n * @returns A Promise that resolves to the signed JWT string\n *\n * @throws {Error} If the private key cannot be imported or the JWT cannot be signed\n *\n * @see https://datatracker.ietf.org/doc/html/rfc7523\n * @see https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials\n *\n * @example\n * ```typescript\n * const credentials = normalizeGCPCredentials(credentialsJson);\n * const customToken = await getGCPCustomToken(credentials);\n * // Use customToken to request an access token\n * ```\n */\nexport async function getGCPCustomToken(credentials: GCPCredentials) {\n const { SignJWT } = await getJose();\n const privateKey = await getGCPPrivateKey(credentials);\n const customToken = await new SignJWT()\n .setIssuer(credentials.client_email)\n .setAudience(credentials.token_uri)\n .setSubject(credentials.client_email)\n .setProtectedHeader({ alg: \"RS256\" })\n .setIssuedAt()\n .setExpirationTime(\"1h\")\n .sign(privateKey);\n return customToken;\n}\n\n/**\n * Retrieves an access token for authenticating with Google Cloud APIs.\n *\n * - Tokens are cached based on the token URI and private key ID\n * - Cache entries are valid for up to 1 hour\n * - Expired tokens trigger automatic refresh\n * - Multiple concurrent requests for the same token are deduplicated\n *\n * The function handles both access tokens and ID tokens, automatically detecting\n * which type is returned by the token endpoint and extracting the appropriate\n * expiration time.\n *\n * @param credentials - The GCP service account credentials used to request the token\n * @returns A Promise that resolves to the access token string\n *\n * @throws {AuthError} If the token request fails (e.g., invalid credentials,\n * network error, or API error)\n *\n * @see https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials\n * @see https://datatracker.ietf.org/doc/html/rfc7523\n *\n * @example\n * ```typescript\n * const credentials = normalizeGCPCredentials(credentialsJson);\n * const accessToken = await getGCPCredentialsAccessToken(credentials);\n *\n * // Use the access token in API requests\n * const response = await fetch('https://api.google.com/...', {\n * headers: {\n * 'Authorization': `Bearer ${accessToken}`\n * }\n * });\n * ```\n *\n * @remarks\n * The caching mechanism uses a two-level validation:\n * 1. Cache entry age (must be less than CACHE_MAX_AGE)\n * 2. Token expiration time (must not be expired)\n *\n * This ensures that even if a cache entry is fresh, an expired token\n * will trigger a refresh request.\n */\nexport async function getGCPCredentialsAccessToken(\n credentials: GCPCredentials\n): Promise<string> {\n const tokenUrl = credentials.token_uri;\n\n const cacheKeyUrl = new URL(tokenUrl);\n cacheKeyUrl.searchParams.set(\"key\", credentials.private_key_id);\n const cacheKey = cacheKeyUrl.toString();\n\n // Attempt to retrieve the token from the cache\n const cacheValue = cache.get(cacheKey);\n let now = Math.floor(Date.now() / 1000);\n\n if (cacheValue) {\n if (cacheValue.created > now - CACHE_MAX_AGE) {\n let token = await cacheValue.promise;\n\n if (token.expires > now) {\n return token.token;\n } else {\n const nextValue = cache.get(cacheKey);\n\n if (nextValue && nextValue !== cacheValue) {\n token = await nextValue.promise;\n if (token.expires > now) {\n return token.token;\n } else {\n cache.delete(cacheKey);\n }\n }\n }\n } else {\n cache.delete(cacheKey);\n }\n }\n\n const promise = iife(async () => {\n now = Math.floor(Date.now() / 1000);\n\n // Request a new token from the Google Cloud API\n const jwt = await getGCPCustomToken(credentials);\n const body = new URLSearchParams();\n body.append(\"grant_type\", \"urn:ietf:params:oauth:grant-type:jwt-bearer\");\n body.append(\"assertion\", jwt);\n const res = await fetch(tokenUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body,\n });\n\n if (!res.ok) {\n throw await AuthError.fromResponse(res);\n }\n\n const data = await res.json();\n\n if (\"id_token\" in data) {\n const { decodeJwt } = await getJose();\n const claims = decodeJwt(data.id_token);\n return { token: data.id_token, expires: claims.exp as number };\n }\n\n const lastModified = res.headers.get(\"last-modified\");\n const expires = lastModified\n ? Math.floor(new Date(lastModified).valueOf() / 1000) + data.expires_in\n : now + data.expires_in;\n\n return { expires, token: data.access_token };\n });\n\n cache.set(cacheKey, { created: now, promise });\n return await promise.then((data) => data.token);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"gcp-auth.cjs","names":["VERTEX_AI_AUTH_SCOPES","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\";\nimport { VERTEX_AI_AUTH_SCOPES } from \"../const.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 * @param scopes - The OAuth scopes the resulting access token should carry.\n * Google requires these on the assertion itself: the claim set\n * for an access token request must include `scope`, \"a\n * space-delimited list of the permissions that the application\n * requests\". Omitting it makes the exchange fail with\n * `Invalid OAuth scope or ID token audience provided`.\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://developers.google.com/identity/protocols/oauth2/service-account\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 * \"https://www.googleapis.com/auth/cloud-platform\",\n * ]);\n * // Use customToken to request an access token\n * ```\n */\nexport async function getGCPCustomToken(\n credentials: GCPCredentials,\n scopes: string[] = VERTEX_AI_AUTH_SCOPES\n) {\n const { SignJWT } = await getJose();\n const privateKey = await getGCPPrivateKey(credentials);\n const customToken = await new SignJWT({ scope: scopes.join(\" \") })\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 scopes: string[] = VERTEX_AI_AUTH_SCOPES\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 // The scopes are baked into the token that comes back, so a token minted for\n // one scope set must not be handed to a caller that asked for another.\n cacheKeyUrl.searchParams.set(\"scope\", [...scopes].sort().join(\" \"));\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, scopes);\n const body = new URLSearchParams();\n body.append(\"grant_type\", \"urn:ietf:params:oauth:grant-type:jwt-bearer\");\n body.append(\"assertion\", jwt);\n const res = await fetch(tokenUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body,\n });\n\n if (!res.ok) {\n throw await AuthError.fromResponse(res);\n }\n\n const data = await res.json();\n\n if (\"id_token\" in data) {\n const { decodeJwt } = await getJose();\n const claims = decodeJwt(data.id_token);\n return { token: data.id_token, expires: claims.exp as number };\n }\n\n const lastModified = res.headers.get(\"last-modified\");\n const expires = lastModified\n ? Math.floor(new Date(lastModified).valueOf() / 1000) + data.expires_in\n : now + data.expires_in;\n\n return { expires, token: data.access_token };\n });\n\n cache.set(cacheKey, { created: now, promise });\n return await promise.then((data) => data.token);\n}\n"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { VERTEX_AI_AUTH_SCOPES } from "../const.js";
1
2
  import { iife } from "./misc.js";
2
3
  import { AuthError } from "./errors.js";
3
4
  //#region src/utils/gcp-auth.ts
@@ -78,24 +79,33 @@ async function getGCPPrivateKey(credentials) {
78
79
  * Token flow as specified by RFC 7523.
79
80
  *
80
81
  * @param credentials - The GCP service account credentials used to sign the JWT
82
+ * @param scopes - The OAuth scopes the resulting access token should carry.
83
+ * Google requires these on the assertion itself: the claim set
84
+ * for an access token request must include `scope`, "a
85
+ * space-delimited list of the permissions that the application
86
+ * requests". Omitting it makes the exchange fail with
87
+ * `Invalid OAuth scope or ID token audience provided`.
81
88
  * @returns A Promise that resolves to the signed JWT string
82
89
  *
83
90
  * @throws {Error} If the private key cannot be imported or the JWT cannot be signed
84
91
  *
85
92
  * @see https://datatracker.ietf.org/doc/html/rfc7523
93
+ * @see https://developers.google.com/identity/protocols/oauth2/service-account
86
94
  * @see https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials
87
95
  *
88
96
  * @example
89
97
  * ```typescript
90
98
  * const credentials = normalizeGCPCredentials(credentialsJson);
91
- * const customToken = await getGCPCustomToken(credentials);
99
+ * const customToken = await getGCPCustomToken(credentials, [
100
+ * "https://www.googleapis.com/auth/cloud-platform",
101
+ * ]);
92
102
  * // Use customToken to request an access token
93
103
  * ```
94
104
  */
95
- async function getGCPCustomToken(credentials) {
105
+ async function getGCPCustomToken(credentials, scopes = VERTEX_AI_AUTH_SCOPES) {
96
106
  const { SignJWT } = await getJose();
97
107
  const privateKey = await getGCPPrivateKey(credentials);
98
- return await new SignJWT().setIssuer(credentials.client_email).setAudience(credentials.token_uri).setSubject(credentials.client_email).setProtectedHeader({ alg: "RS256" }).setIssuedAt().setExpirationTime("1h").sign(privateKey);
108
+ return await new SignJWT({ scope: scopes.join(" ") }).setIssuer(credentials.client_email).setAudience(credentials.token_uri).setSubject(credentials.client_email).setProtectedHeader({ alg: "RS256" }).setIssuedAt().setExpirationTime("1h").sign(privateKey);
99
109
  }
100
110
  /**
101
111
  * Retrieves an access token for authenticating with Google Cloud APIs.
@@ -139,10 +149,11 @@ async function getGCPCustomToken(credentials) {
139
149
  * This ensures that even if a cache entry is fresh, an expired token
140
150
  * will trigger a refresh request.
141
151
  */
142
- async function getGCPCredentialsAccessToken(credentials) {
152
+ async function getGCPCredentialsAccessToken(credentials, scopes = VERTEX_AI_AUTH_SCOPES) {
143
153
  const tokenUrl = credentials.token_uri;
144
154
  const cacheKeyUrl = new URL(tokenUrl);
145
155
  cacheKeyUrl.searchParams.set("key", credentials.private_key_id);
156
+ cacheKeyUrl.searchParams.set("scope", [...scopes].sort().join(" "));
146
157
  const cacheKey = cacheKeyUrl.toString();
147
158
  const cacheValue = cache.get(cacheKey);
148
159
  let now = Math.floor(Date.now() / 1e3);
@@ -162,7 +173,7 @@ async function getGCPCredentialsAccessToken(credentials) {
162
173
  }
163
174
  const promise = iife(async () => {
164
175
  now = Math.floor(Date.now() / 1e3);
165
- const jwt = await getGCPCustomToken(credentials);
176
+ const jwt = await getGCPCustomToken(credentials, scopes);
166
177
  const body = new URLSearchParams();
167
178
  body.append("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer");
168
179
  body.append("assertion", jwt);
@@ -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":"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"}
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\";\nimport { VERTEX_AI_AUTH_SCOPES } from \"../const.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 * @param scopes - The OAuth scopes the resulting access token should carry.\n * Google requires these on the assertion itself: the claim set\n * for an access token request must include `scope`, \"a\n * space-delimited list of the permissions that the application\n * requests\". Omitting it makes the exchange fail with\n * `Invalid OAuth scope or ID token audience provided`.\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://developers.google.com/identity/protocols/oauth2/service-account\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 * \"https://www.googleapis.com/auth/cloud-platform\",\n * ]);\n * // Use customToken to request an access token\n * ```\n */\nexport async function getGCPCustomToken(\n credentials: GCPCredentials,\n scopes: string[] = VERTEX_AI_AUTH_SCOPES\n) {\n const { SignJWT } = await getJose();\n const privateKey = await getGCPPrivateKey(credentials);\n const customToken = await new SignJWT({ scope: scopes.join(\" \") })\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 scopes: string[] = VERTEX_AI_AUTH_SCOPES\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 // The scopes are baked into the token that comes back, so a token minted for\n // one scope set must not be handed to a caller that asked for another.\n cacheKeyUrl.searchParams.set(\"scope\", [...scopes].sort().join(\" \"));\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, scopes);\n const body = new URLSearchParams();\n body.append(\"grant_type\", \"urn:ietf:params:oauth:grant-type:jwt-bearer\");\n body.append(\"assertion\", jwt);\n const res = await fetch(tokenUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body,\n });\n\n if (!res.ok) {\n throw await AuthError.fromResponse(res);\n }\n\n const data = await res.json();\n\n if (\"id_token\" in data) {\n const { decodeJwt } = await getJose();\n const claims = decodeJwt(data.id_token);\n return { token: data.id_token, expires: claims.exp as number };\n }\n\n const lastModified = res.headers.get(\"last-modified\");\n const expires = lastModified\n ? Math.floor(new Date(lastModified).valueOf() / 1000) + data.expires_in\n : now + data.expires_in;\n\n return { expires, token: data.access_token };\n });\n\n cache.set(cacheKey, { created: now, promise });\n return await promise.then((data) => data.token);\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@langchain/google",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "Google integrations for LangChain.js",
5
5
  "author": "LangChain",
6
6
  "license": "MIT",
@@ -15,11 +15,11 @@
15
15
  },
16
16
  "dependencies": {
17
17
  "eventsource-parser": "^4.1.0",
18
- "google-auth-library": "^11.0.2",
18
+ "google-auth-library": "^11.1.0",
19
19
  "jose": "^6.2.10"
20
20
  },
21
21
  "peerDependencies": {
22
- "@langchain/core": "^1.2.12"
22
+ "@langchain/core": "^1.2.14"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@tsconfig/recommended": "^1.0.10",
@@ -30,7 +30,7 @@
30
30
  "vitest": "^4.1.11",
31
31
  "zod": "^3.25.76 || ^4",
32
32
  "zod-to-json-schema": "^3.25.2",
33
- "@langchain/core": "^1.2.12",
33
+ "@langchain/core": "^1.2.14",
34
34
  "@langchain/standard-tests": "0.0.23"
35
35
  },
36
36
  "publishConfig": {