From f29cdd02900d5edec79bd7e36d47cf119b7d359f Mon Sep 17 00:00:00 2001 From: silveroxides Date: Fri, 13 Nov 2026 01:07:19 +0100 Subject: [PATCH] Add Gemini 4 Carbon and Hydrogen support --- gemini_nodes.py | 67 ++++++++++---- interactions_nodes.py | 55 +++++++++--- pyproject.toml | 2 +- requirements.txt | 2 +- tests/test_gemini_nodes.py | 144 +++++++++++++++++++++++++++++++ tests/test_interactions_nodes.py | 69 +++++++++++++++ 6 files changed, 309 insertions(+), 30 deletions(-) diff --git a/gemini_nodes.py b/gemini_nodes.py index fc85524..adf0857 100644 --- a/gemini_nodes.py +++ b/gemini_nodes.py @@ -249,7 +249,16 @@ class SSL_GeminiTextPrompt(IO.ComfyNode): GEMINI_3_8_FLASH = "gemini-3.8-flash" GEMINI_4_ARGON = "gemini-4-argon" GEMINI_4_ARGON_CYBER_PERMISSIVE = "gemini-4-argon-cyber-permissive" + GEMINI_4_CARBON = "gemini-4-carbon" + GEMINI_4_CARBON_CYBER_PERMISSIVE = "gemini-4-carbon-cyber-permissive" + GEMINI_4_HYDROGEN = "gemini-4-hydrogen" ARGON_MODELS = (GEMINI_4_ARGON, GEMINI_4_ARGON_CYBER_PERMISSIVE) + GEMINI_4_MODELS = ( + *ARGON_MODELS, + GEMINI_4_CARBON, + GEMINI_4_CARBON_CYBER_PERMISSIVE, + GEMINI_4_HYDROGEN, + ) VIDEO_MIME_TYPE = "video/mp4" VIDEO_INLINE_LIMIT_BYTES = 100 * 1024 * 1024 VERTEX_CACHE_INLINE_LIMIT_BYTES = 10 * 1024 * 1024 @@ -263,11 +272,11 @@ class SSL_GeminiTextPrompt(IO.ComfyNode): THINKING_MODELS = [ "gemini-1.5-pro-002", "gemini-2.0-flash-thinking-exp", "gemini-2.0-flash-thinking-exp-01-21", "gemini-2.0-flash-thinking-exp-1219", "gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.5-flash-preview-04-17", "gemini-2.5-pro-exp-03-25", - "gemini-3-flash-preview", "gemini-3.1-pro-preview", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.6-flash", GEMINI_3_7_FLASH, GEMINI_3_8_FLASH, GEMINI_4_ARGON, GEMINI_4_ARGON_CYBER_PERMISSIVE, GEMINI_4_FLASH_PREVIEW, "gemini-pro-latest", "gemini-flash-latest", "gemini-flash-lite-latest" + "gemini-3-flash-preview", "gemini-3.1-pro-preview", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.6-flash", GEMINI_3_7_FLASH, GEMINI_3_8_FLASH, GEMINI_4_ARGON, GEMINI_4_ARGON_CYBER_PERMISSIVE, GEMINI_4_CARBON, GEMINI_4_CARBON_CYBER_PERMISSIVE, GEMINI_4_HYDROGEN, GEMINI_4_FLASH_PREVIEW, "gemini-pro-latest", "gemini-flash-latest", "gemini-flash-lite-latest" ] GEN3_THINKING_MODELS = [ "gemini-pro-latest", "gemini-flash-latest", "gemini-3.1-pro-preview", - "gemini-3-flash-preview", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.6-flash", GEMINI_3_7_FLASH, GEMINI_3_8_FLASH, GEMINI_4_ARGON, GEMINI_4_ARGON_CYBER_PERMISSIVE, GEMINI_4_FLASH_PREVIEW + "gemini-3-flash-preview", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.6-flash", GEMINI_3_7_FLASH, GEMINI_3_8_FLASH, GEMINI_4_ARGON, GEMINI_4_ARGON_CYBER_PERMISSIVE, GEMINI_4_CARBON, GEMINI_4_CARBON_CYBER_PERMISSIVE, GEMINI_4_HYDROGEN, GEMINI_4_FLASH_PREVIEW ] IMAGE_MODELS = [ "gemini-2.5-flash-image", @@ -281,7 +290,7 @@ class SSL_GeminiTextPrompt(IO.ComfyNode): } MEDIA_RES_MODELS = [ "gemini-3.1-flash-lite", "gemini-3-flash-preview", "gemini-3.1-pro-preview", - "gemini-3.5-flash", "gemini-pro-latest", "gemini-flash-latest", "gemini-flash-lite-latest" + "gemini-3.5-flash", GEMINI_4_CARBON, GEMINI_4_CARBON_CYBER_PERMISSIVE, GEMINI_4_HYDROGEN, "gemini-pro-latest", "gemini-flash-latest", "gemini-flash-lite-latest" ] @classmethod @@ -301,11 +310,11 @@ class SSL_GeminiTextPrompt(IO.ComfyNode): cls.GemConfig.Input("config"), IO.String.Input("prompt", multiline=True), IO.String.Input("system_instruction", default="You are a helpful AI assistant.", multiline=True), - IO.Combo.Input("model", options=["gemini-1.5-pro-002", "gemini-2.0-flash", "gemini-2.0-flash-lite", "gemini-2.5-flash-preview-04-17", "gemini-2.5-pro-exp-03-25", "gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.5-flash-lite", "gemini-3-flash-preview", "gemini-3.1-flash-lite", "gemini-3.1-pro-preview", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.6-flash", cls.GEMINI_3_7_FLASH, cls.GEMINI_3_8_FLASH, cls.GEMINI_4_ARGON, cls.GEMINI_4_ARGON_CYBER_PERMISSIVE, "gemini-3.1-flash-image", "gemini-3.1-flash-lite-image", "gemini-3-pro-image", "gemini-pro-latest", "gemini-flash-latest", "gemini-flash-lite-latest"], default="gemini-2.5-flash"), + IO.Combo.Input("model", options=["gemini-1.5-pro-002", "gemini-2.0-flash", "gemini-2.0-flash-lite", "gemini-2.5-flash-preview-04-17", "gemini-2.5-pro-exp-03-25", "gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.5-flash-lite", "gemini-3-flash-preview", "gemini-3.1-flash-lite", "gemini-3.1-pro-preview", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.6-flash", cls.GEMINI_3_7_FLASH, cls.GEMINI_3_8_FLASH, cls.GEMINI_4_ARGON, cls.GEMINI_4_ARGON_CYBER_PERMISSIVE, cls.GEMINI_4_CARBON, cls.GEMINI_4_CARBON_CYBER_PERMISSIVE, cls.GEMINI_4_HYDROGEN, "gemini-3.1-flash-image", "gemini-3.1-flash-lite-image", "gemini-3-pro-image", "gemini-pro-latest", "gemini-flash-latest", "gemini-flash-lite-latest"], default="gemini-2.5-flash"), IO.Float.Input("temperature", default=1.0, min=0.0, max=1.0, step=0.01), IO.Float.Input("top_p", default=0.95, min=0.0, max=1.0, step=0.01), IO.Int.Input("top_k", default=40, min=1, max=100, step=1), - IO.Int.Input("max_output_tokens", default=8192, min=1, max=65536, step=1), + IO.Int.Input("max_output_tokens", default=8192, min=1, max=1048576, step=1), IO.Boolean.Input("include_images", default=False), IO.Combo.Input("aspect_ratio", options=["None", "1:1", "9:16", "16:9", "3:4", "4:3", "3:2", "2:3", "5:4", "4:5", "21:9"], default="None"), IO.Combo.Input("bypass_mode", options=["None", "system_instruction", "prompt", "both"], default="None"), @@ -315,9 +324,9 @@ class SSL_GeminiTextPrompt(IO.ComfyNode): IO.Int.Input("proxy_port", default=7890, min=1, max=65535), IO.Boolean.Input("use_seed", default=True), IO.Int.Input("seed", default=0, min=0, max=2147483647), - IO.Int.Input("timeout", default=30, min=15, max=300, step=15), + IO.Int.Input("timeout", default=30, min=15, max=1200, step=15), IO.Boolean.Input("include_thoughts", default=False), - IO.Combo.Input("thinking_level", options=["None", "low", "medium", "high"], default="None", tooltip="Does not work at the same time as 'thinking_budget'. If this is set, then thinking budget is ignored. Gemini 3.7/3.8 Flash always uses this control, defaults to medium, and ignores thinking_budget."), + IO.Combo.Input("thinking_level", options=["None", "low", "medium", "high", "xhigh"], default="None", tooltip="Does not work at the same time as 'thinking_budget'. If this is set, then thinking budget is ignored. Gemini 3.7/3.8 Flash and Gemini 4 models always use this control and ignore thinking_budget. 'xhigh' is supported only by Gemini 4 Hydrogen."), IO.Combo.Input("media_resolution", options=["unspecified", "low", "medium", "high"], default="unspecified", tooltip="Set input media resolution for image, video and pdf. This changes tokens consumed."), IO.String.Input("retry_pattern", default="", optional=True, multiline=False, tooltip="Regex pattern to match in response text. If matched, retry with new seed. Leave empty to disable."), IO.Int.Input("max_retries", default=3, min=0, max=10, step=1, tooltip="Maximum number of retry attempts when pattern matches. 0 disables retry."), @@ -549,19 +558,34 @@ class SSL_GeminiTextPrompt(IO.ComfyNode): @classmethod def _resolve_gemini_3_7_thinking_level(cls, thinking_level, model_name=None): if thinking_level is None or thinking_level == "None": - if model_name == cls.GEMINI_4_ARGON_CYBER_PERMISSIVE: + if model_name in ( + cls.GEMINI_4_ARGON_CYBER_PERMISSIVE, + cls.GEMINI_4_CARBON_CYBER_PERMISSIVE, + cls.GEMINI_4_HYDROGEN, + ): return "high" return "medium" if thinking_level == "minimal": - if model_name in (cls.GEMINI_3_8_FLASH, cls.GEMINI_4_ARGON, cls.GEMINI_4_ARGON_CYBER_PERMISSIVE): + if model_name in (cls.GEMINI_3_8_FLASH, *cls.GEMINI_4_MODELS): + allowed = ( + "low, medium, high, or xhigh" + if model_name == cls.GEMINI_4_HYDROGEN + else "low, medium, or high" + ) raise ValueError( - f"{model_name} thinking_level must be low, medium, or high." + f"{model_name} thinking_level must be {allowed}." ) print( f"[WARNING] {model_name or 'Gemini 3.7 Flash'} does not support minimal " "thinking; using low instead." ) return "low" + if thinking_level == "xhigh": + if model_name != cls.GEMINI_4_HYDROGEN: + raise ValueError( + f"{model_name or 'This model'} does not support xhigh thinking_level." + ) + return "xhigh" if thinking_level not in {"low", "medium", "high"}: raise ValueError( f"{model_name or 'Gemini 3.7 Flash'} thinking_level must be low, medium, or high." @@ -660,9 +684,9 @@ class SSL_GeminiTextPrompt(IO.ComfyNode): eff_thinking_level = "None" eff_thinking_budget = -1 eff_include_thoughts = False - eff_temperature = None if model in (cls.GEMINI_3_7_FLASH, cls.GEMINI_3_8_FLASH, cls.GEMINI_4_ARGON, cls.GEMINI_4_ARGON_CYBER_PERMISSIVE) else float(temperature) - eff_top_p = None if model in (cls.GEMINI_3_7_FLASH, cls.GEMINI_3_8_FLASH, cls.GEMINI_4_ARGON, cls.GEMINI_4_ARGON_CYBER_PERMISSIVE) else float(top_p) - eff_top_k = None if model in (cls.GEMINI_3_7_FLASH, cls.GEMINI_3_8_FLASH, cls.GEMINI_4_ARGON, cls.GEMINI_4_ARGON_CYBER_PERMISSIVE) else int(top_k) + eff_temperature = None if model in (cls.GEMINI_3_7_FLASH, cls.GEMINI_3_8_FLASH, *cls.GEMINI_4_MODELS) else float(temperature) + eff_top_p = None if model in (cls.GEMINI_3_7_FLASH, cls.GEMINI_3_8_FLASH, *cls.GEMINI_4_MODELS) else float(top_p) + eff_top_k = None if model in (cls.GEMINI_3_7_FLASH, cls.GEMINI_3_8_FLASH, *cls.GEMINI_4_MODELS) else int(top_k) # Logic mirroring _build_generate_content_config exactly if include_images and model in cls.IMAGE_MODELS: @@ -671,11 +695,13 @@ class SSL_GeminiTextPrompt(IO.ComfyNode): eff_image_size = "None" if image_size in (None, "None") else str(image_size) # When generating images, thinking params are ignored - elif model in (cls.GEMINI_3_7_FLASH, cls.GEMINI_3_8_FLASH, cls.GEMINI_4_ARGON, cls.GEMINI_4_ARGON_CYBER_PERMISSIVE): + elif model in (cls.GEMINI_3_7_FLASH, cls.GEMINI_3_8_FLASH, *cls.GEMINI_4_MODELS): eff_thinking_level = cls._resolve_gemini_3_7_thinking_level(thinking_level, model) eff_include_thoughts = include_thoughts elif model in cls.GEN3_THINKING_MODELS and thinking_level is not None and thinking_level != "None": + if thinking_level == "xhigh": + raise ValueError(f"{model} does not support xhigh thinking_level.") eff_thinking_level = thinking_level eff_include_thoughts = include_thoughts # Budget is ignored in this specific branch @@ -922,7 +948,7 @@ class SSL_GeminiTextPrompt(IO.ComfyNode): system_instruction=[types.Part.from_text(text=padded_system_instruction)], ) - if model in (cls.GEMINI_3_7_FLASH, cls.GEMINI_3_8_FLASH, cls.GEMINI_4_ARGON, cls.GEMINI_4_ARGON_CYBER_PERMISSIVE): + if model in (cls.GEMINI_3_7_FLASH, cls.GEMINI_3_8_FLASH, *cls.GEMINI_4_MODELS): return types.GenerateContentConfig( max_output_tokens=max_output_tokens, safety_settings=safety, @@ -937,6 +963,8 @@ class SSL_GeminiTextPrompt(IO.ComfyNode): G3Pro = ["gemini-3.1-pro-preview"] # Modified: Added check for thinking_level != "None" if model in G3Pro and thinking_level is not None and thinking_level != "None": + if thinking_level == "xhigh": + raise ValueError(f"{model} does not support xhigh thinking_level.") return types.GenerateContentConfig( temperature=temperature, top_p=top_p, @@ -1335,10 +1363,10 @@ class SSL_GeminiTextPrompt(IO.ComfyNode): and (config.get("use_vertexai_env", False) or config.get("vertexai_express", False)) and context_inline_bytes > cls.VERTEX_CACHE_INLINE_LIMIT_BYTES ) - or (use_cache and model in cls.ARGON_MODELS) + or (use_cache and model in cls.GEMINI_4_MODELS) ) if skip_context_cache: - if use_cache and model in cls.ARGON_MODELS: + if use_cache and model in cls.GEMINI_4_MODELS: print(f"[WARNING] Explicit context cache skipped for {model} (implicit caching only); using full request") else: print("[WARNING] Vertex context cache skipped because inline cached media exceeds 10 MB; using full request") @@ -1365,6 +1393,11 @@ class SSL_GeminiTextPrompt(IO.ComfyNode): safety_settings=resolved_safety_settings, ) + if generate_content_config.http_options is None: + generate_content_config.http_options = types.HttpOptions( + timeout=max(1, int(float(timeout) * 1000)) + ) + if schema_snapshot is not None: generate_content_config.response_mime_type = "application/json" generate_content_config.response_json_schema = schema_snapshot diff --git a/interactions_nodes.py b/interactions_nodes.py index 5cad565..798bab9 100644 --- a/interactions_nodes.py +++ b/interactions_nodes.py @@ -265,7 +265,16 @@ class SSL_GeminiInteractionsTextPrompt(IO.ComfyNode): GEMINI_3_8_FLASH = "gemini-3.8-flash" GEMINI_4_ARGON = "gemini-4-argon" GEMINI_4_ARGON_CYBER_PERMISSIVE = "gemini-4-argon-cyber-permissive" + GEMINI_4_CARBON = "gemini-4-carbon" + GEMINI_4_CARBON_CYBER_PERMISSIVE = "gemini-4-carbon-cyber-permissive" + GEMINI_4_HYDROGEN = "gemini-4-hydrogen" ARGON_MODELS = (GEMINI_4_ARGON, GEMINI_4_ARGON_CYBER_PERMISSIVE) + GEMINI_4_MODELS = ( + *ARGON_MODELS, + GEMINI_4_CARBON, + GEMINI_4_CARBON_CYBER_PERMISSIVE, + GEMINI_4_HYDROGEN, + ) VIDEO_MIME_TYPE = "video/mp4" VIDEO_INLINE_LIMIT_BYTES = 100 * 1024 * 1024 @@ -287,11 +296,11 @@ class SSL_GeminiInteractionsTextPrompt(IO.ComfyNode): THINKING_MODELS = [ "gemini-1.5-pro-002", "gemini-2.0-flash-thinking-exp", "gemini-2.0-flash-thinking-exp-01-21", "gemini-2.0-flash-thinking-exp-1219", "gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.5-flash-preview-04-17", "gemini-2.5-pro-exp-03-25", - "gemini-3-flash-preview", "gemini-3.1-pro-preview", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.6-flash", GEMINI_3_7_FLASH, GEMINI_3_8_FLASH, GEMINI_4_ARGON, GEMINI_4_ARGON_CYBER_PERMISSIVE, GEMINI_4_FLASH_PREVIEW, "gemini-pro-latest", "gemini-flash-latest", "gemini-flash-lite-latest" + "gemini-3-flash-preview", "gemini-3.1-pro-preview", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.6-flash", GEMINI_3_7_FLASH, GEMINI_3_8_FLASH, GEMINI_4_ARGON, GEMINI_4_ARGON_CYBER_PERMISSIVE, GEMINI_4_CARBON, GEMINI_4_CARBON_CYBER_PERMISSIVE, GEMINI_4_HYDROGEN, GEMINI_4_FLASH_PREVIEW, "gemini-pro-latest", "gemini-flash-latest", "gemini-flash-lite-latest" ] GEN3_THINKING_MODELS = [ "gemini-pro-latest", "gemini-flash-latest", "gemini-3.1-pro-preview", - "gemini-3-flash-preview", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.6-flash", GEMINI_3_7_FLASH, GEMINI_3_8_FLASH, GEMINI_4_ARGON, GEMINI_4_ARGON_CYBER_PERMISSIVE, GEMINI_4_FLASH_PREVIEW + "gemini-3-flash-preview", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.6-flash", GEMINI_3_7_FLASH, GEMINI_3_8_FLASH, GEMINI_4_ARGON, GEMINI_4_ARGON_CYBER_PERMISSIVE, GEMINI_4_CARBON, GEMINI_4_CARBON_CYBER_PERMISSIVE, GEMINI_4_HYDROGEN, GEMINI_4_FLASH_PREVIEW ] IMAGE_MODELS = [ "gemini-2.5-flash-image", @@ -325,11 +334,11 @@ class SSL_GeminiInteractionsTextPrompt(IO.ComfyNode): cls.GemConfig.Input("config"), IO.String.Input("prompt", multiline=True), IO.String.Input("system_instruction", default="You are a helpful AI assistant.", multiline=True), - IO.Combo.Input("model", options=["gemini-1.5-pro-002", "gemini-2.0-flash", "gemini-2.0-flash-lite", "gemini-2.5-flash-preview-04-17", "gemini-2.5-pro-exp-03-25", "gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.5-flash-lite", "gemini-3-flash-preview", "gemini-3.1-flash-lite", "gemini-3.1-pro-preview", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.6-flash", cls.GEMINI_3_7_FLASH, cls.GEMINI_3_8_FLASH, cls.GEMINI_4_ARGON, cls.GEMINI_4_ARGON_CYBER_PERMISSIVE, "gemini-3.1-flash-image", "gemini-3.1-flash-lite-image", "gemini-3-pro-image", "gemini-pro-latest", "gemini-flash-latest", "gemini-flash-lite-latest"], default="gemini-2.5-flash"), + IO.Combo.Input("model", options=["gemini-1.5-pro-002", "gemini-2.0-flash", "gemini-2.0-flash-lite", "gemini-2.5-flash-preview-04-17", "gemini-2.5-pro-exp-03-25", "gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.5-flash-lite", "gemini-3-flash-preview", "gemini-3.1-flash-lite", "gemini-3.1-pro-preview", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.6-flash", cls.GEMINI_3_7_FLASH, cls.GEMINI_3_8_FLASH, cls.GEMINI_4_ARGON, cls.GEMINI_4_ARGON_CYBER_PERMISSIVE, cls.GEMINI_4_CARBON, cls.GEMINI_4_CARBON_CYBER_PERMISSIVE, cls.GEMINI_4_HYDROGEN, "gemini-3.1-flash-image", "gemini-3.1-flash-lite-image", "gemini-3-pro-image", "gemini-pro-latest", "gemini-flash-latest", "gemini-flash-lite-latest"], default="gemini-2.5-flash"), IO.Float.Input("temperature", default=1.0, min=0.0, max=1.0, step=0.01, tooltip="Ignored in Interactions API"), IO.Float.Input("top_p", default=0.95, min=0.0, max=1.0, step=0.01, tooltip="Ignored in Interactions API"), IO.Int.Input("top_k", default=40, min=1, max=100, step=1, tooltip="Ignored in Interactions API"), - IO.Int.Input("max_output_tokens", default=8192, min=1, max=65536, step=1), + IO.Int.Input("max_output_tokens", default=8192, min=1, max=1048576, step=1), IO.Boolean.Input("include_images", default=False), IO.Combo.Input("aspect_ratio", options=["None", "1:1", "9:16", "16:9", "3:4", "4:3", "3:2", "2:3", "5:4", "4:5", "21:9", "1:8", "8:1", "1:4", "4:1"], default="None"), IO.Combo.Input("bypass_mode", options=["None", "system_instruction", "prompt", "both"], default="None"), @@ -339,9 +348,9 @@ class SSL_GeminiInteractionsTextPrompt(IO.ComfyNode): IO.Int.Input("proxy_port", default=7890, min=1, max=65535), IO.Boolean.Input("use_seed", default=True), IO.Int.Input("seed", default=0, min=0, max=2147483647), - IO.Int.Input("timeout", default=30, min=15, max=300, step=15), + IO.Int.Input("timeout", default=30, min=15, max=1200, step=15), IO.Boolean.Input("include_thoughts", default=False), - IO.Combo.Input("thinking_level", options=["None", "minimal", "low", "medium", "high"], default="low", tooltip="Thinking level for reasoning models."), + IO.Combo.Input("thinking_level", options=["None", "minimal", "low", "medium", "high", "xhigh"], default="low", tooltip="Thinking level for reasoning models. 'xhigh' is supported only by Gemini 4 Hydrogen."), IO.Combo.Input("media_resolution", options=["unspecified", "low", "medium", "high", "ultra_high"], default="unspecified", tooltip="Set input media resolution for image, video and pdf."), IO.String.Input("retry_pattern", default="(PROHIBITED_CONTENT|UNAVAILABLE|request/processing|RESOURCE_EXHAUSTED)", optional=True, multiline=False, tooltip="Regex pattern to match in response text. If matched, retry with new seed. Leave empty to disable."), IO.Int.Input("max_retries", default=3, min=0, max=10, step=1, tooltip="Maximum number of retry attempts when pattern matches. 0 disables retry."), @@ -569,19 +578,34 @@ class SSL_GeminiInteractionsTextPrompt(IO.ComfyNode): @classmethod def _resolve_gemini_3_7_thinking_level(cls, thinking_level, model_name=None): if thinking_level is None or thinking_level == "None": - if model_name == cls.GEMINI_4_ARGON_CYBER_PERMISSIVE: + if model_name in ( + cls.GEMINI_4_ARGON_CYBER_PERMISSIVE, + cls.GEMINI_4_CARBON_CYBER_PERMISSIVE, + cls.GEMINI_4_HYDROGEN, + ): return "high" return "medium" if thinking_level == "minimal": - if model_name in (cls.GEMINI_3_8_FLASH, cls.GEMINI_4_ARGON, cls.GEMINI_4_ARGON_CYBER_PERMISSIVE): + if model_name in (cls.GEMINI_3_8_FLASH, *cls.GEMINI_4_MODELS): + allowed = ( + "low, medium, high, or xhigh" + if model_name == cls.GEMINI_4_HYDROGEN + else "low, medium, or high" + ) raise ValueError( - f"{model_name} thinking_level must be low, medium, or high." + f"{model_name} thinking_level must be {allowed}." ) print( f"[WARNING] {model_name or 'Gemini 3.7 Flash'} does not support minimal " "thinking; using low instead." ) return "low" + if thinking_level == "xhigh": + if model_name != cls.GEMINI_4_HYDROGEN: + raise ValueError( + f"{model_name or 'This model'} does not support xhigh thinking_level." + ) + return "xhigh" if thinking_level not in {"low", "medium", "high"}: raise ValueError( f"{model_name or 'Gemini 3.7 Flash'} thinking_level must be low, medium, or high." @@ -672,15 +696,19 @@ class SSL_GeminiInteractionsTextPrompt(IO.ComfyNode): eff_aspect_ratio = str(aspect_ratio) eff_image_size = "None" if image_size in (None, "None") else str(image_size) - elif model in (cls.GEMINI_3_7_FLASH, cls.GEMINI_3_8_FLASH, cls.GEMINI_4_ARGON, cls.GEMINI_4_ARGON_CYBER_PERMISSIVE): + elif model in (cls.GEMINI_3_7_FLASH, cls.GEMINI_3_8_FLASH, *cls.GEMINI_4_MODELS): eff_thinking_level = cls._resolve_gemini_3_7_thinking_level(thinking_level, model) eff_include_thoughts = include_thoughts elif model in cls.GEN3_THINKING_MODELS and thinking_level not in (None, "None"): + if thinking_level == "xhigh": + raise ValueError(f"{model} does not support xhigh thinking_level.") eff_thinking_level = thinking_level eff_include_thoughts = include_thoughts elif thinking_level not in (None, "None"): + if thinking_level == "xhigh": + raise ValueError(f"{model} does not support xhigh thinking_level.") eff_thinking_level = thinking_level eff_include_thoughts = include_thoughts @@ -1219,11 +1247,15 @@ class SSL_GeminiInteractionsTextPrompt(IO.ComfyNode): # Assemble generation_config effective_thinking_level = None - if model in (cls.GEMINI_3_7_FLASH, cls.GEMINI_3_8_FLASH, cls.GEMINI_4_ARGON, cls.GEMINI_4_ARGON_CYBER_PERMISSIVE): + if model in (cls.GEMINI_3_7_FLASH, cls.GEMINI_3_8_FLASH, *cls.GEMINI_4_MODELS): effective_thinking_level = cls._resolve_gemini_3_7_thinking_level(thinking_level, model) elif model in cls.GEN3_THINKING_MODELS and thinking_level not in (None, "None"): + if thinking_level == "xhigh": + raise ValueError(f"{model} does not support xhigh thinking_level.") effective_thinking_level = thinking_level elif thinking_level not in (None, "None"): + if thinking_level == "xhigh": + raise ValueError(f"{model} does not support xhigh thinking_level.") effective_thinking_level = thinking_level generation_config: dict[str, Any] = {} @@ -1252,6 +1284,7 @@ class SSL_GeminiInteractionsTextPrompt(IO.ComfyNode): kwargs: dict[str, Any] = { "model": model, "input": input_contents, + "timeout": float(timeout), } if resolved_safety_settings is not None: kwargs["safety_settings"] = list(resolved_safety_settings) diff --git a/pyproject.toml b/pyproject.toml index 54c8af5..a606917 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "ComfyUI_Gemini_Expanded_API" description = "Node for usage of Gemini API" version = "1.6.2" license = {file = "LICENSE"} -dependencies = ["google-genai>=2.17.0", "numpy", "torch"] +dependencies = ["google-genai>=2.34.0", "numpy", "torch"] [project.urls] Repository = "https://github.com/silveroxides/ComfyUI_Gemini_Expanded_API" diff --git a/requirements.txt b/requirements.txt index cbb7460..fee149f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -google-genai>=2.17.0 +google-genai>=2.34.0 numpy torch Pillow diff --git a/tests/test_gemini_nodes.py b/tests/test_gemini_nodes.py index e4d6a5d..910138f 100644 --- a/tests/test_gemini_nodes.py +++ b/tests/test_gemini_nodes.py @@ -58,6 +58,9 @@ def structured_client(monkeypatch): (gemini_nodes.SSL_GeminiTextPrompt.GEMINI_3_8_FLASH, {"thinking_level": "medium"}), (gemini_nodes.SSL_GeminiTextPrompt.GEMINI_4_ARGON, {"thinking_level": "medium"}), (gemini_nodes.SSL_GeminiTextPrompt.GEMINI_4_ARGON_CYBER_PERMISSIVE, {"thinking_level": "high"}), + (gemini_nodes.SSL_GeminiTextPrompt.GEMINI_4_CARBON, {"thinking_level": "medium"}), + (gemini_nodes.SSL_GeminiTextPrompt.GEMINI_4_CARBON_CYBER_PERMISSIVE, {"thinking_level": "high"}), + (gemini_nodes.SSL_GeminiTextPrompt.GEMINI_4_HYDROGEN, {"thinking_level": "high"}), ]) @pytest.mark.parametrize("client_config", [{}, {"vertexai_express": True}, {"use_vertexai_env": True, "vertexai_project": "test", "vertexai_location": "global"}]) def test_builder_schema_reaches_generate_content(structured_client, monkeypatch, model, settings, client_config): @@ -1809,3 +1812,144 @@ def test_gemini_batch_images_list(structured_client): assert isinstance(output[0], list) and len(output[0]) == 2 assert output[0] == ["Img Resp 1", "Img Resp 2"] assert len(structured_client.calls) == 2 + + +def test_gemini_4_carbon_and_hydrogen_schema_and_limits(): + schema = gemini_nodes.SSL_GeminiTextPrompt.define_schema() + inputs = {inp.id: inp for inp in schema.inputs} + + for model_id in ( + "gemini-4-carbon", + "gemini-4-carbon-cyber-permissive", + "gemini-4-hydrogen", + ): + assert model_id in inputs["model"].options + assert model_id in gemini_nodes.SSL_GeminiTextPrompt.THINKING_MODELS + assert model_id in gemini_nodes.SSL_GeminiTextPrompt.GEN3_THINKING_MODELS + assert model_id in gemini_nodes.SSL_GeminiTextPrompt.MEDIA_RES_MODELS + + assert "xhigh" in inputs["thinking_level"].options + assert inputs["max_output_tokens"].max == 1048576 + assert inputs["timeout"].max == 1200 + + +@pytest.mark.parametrize( + ("model_id", "selected", "expected"), + [ + ("gemini-4-carbon", "None", "MEDIUM"), + ("gemini-4-carbon-cyber-permissive", "None", "HIGH"), + ("gemini-4-hydrogen", "None", "HIGH"), + ("gemini-4-hydrogen", "xhigh", "xhigh"), + ("gemini-4-hydrogen", "low", "LOW"), + ], +) +def test_gemini_4_carbon_and_hydrogen_thinking_config(model_id, selected, expected): + config = gemini_nodes.SSL_GeminiTextPrompt._build_generate_content_config( + model=model_id, + temperature=0.4, + top_p=0.8, + top_k=20, + max_output_tokens=131072, + seed=7, + include_images=False, + response_modalities=["TEXT"], + aspect_ratio="None", + padded_system_instruction="system", + thinking_level=selected, + thinking_budget=4096, + include_thoughts=True, + media_resolution="unspecified", + ) + + assert config.thinking_config.thinking_level.value == expected + assert config.thinking_config.thinking_budget is None + assert config.max_output_tokens == 131072 + assert config.temperature is None + assert config.top_p is None + assert config.top_k is None + + +@pytest.mark.parametrize( + ("model_id", "bad_level"), + [ + ("gemini-4-carbon", "minimal"), + ("gemini-4-carbon-cyber-permissive", "minimal"), + ("gemini-4-hydrogen", "minimal"), + ("gemini-4-carbon", "xhigh"), + ("gemini-4-carbon-cyber-permissive", "xhigh"), + ("gemini-4-argon", "xhigh"), + ("gemini-3.8-flash", "xhigh"), + ("gemini-3.1-pro-preview", "xhigh"), + ], +) +def test_gemini_4_invalid_thinking_levels_rejected(model_id, bad_level): + with pytest.raises(ValueError): + gemini_nodes.SSL_GeminiTextPrompt._build_generate_content_config( + model=model_id, + temperature=1.0, + top_p=0.95, + top_k=40, + max_output_tokens=8192, + seed=7, + include_images=False, + response_modalities=["TEXT"], + aspect_ratio="None", + padded_system_instruction="system", + thinking_level=bad_level, + thinking_budget=0, + include_thoughts=False, + media_resolution="unspecified", + ) + + +@pytest.mark.parametrize("model_id", ["gemini-4-carbon", "gemini-4-carbon-cyber-permissive", "gemini-4-hydrogen"]) +def test_gemini_4_carbon_and_hydrogen_skip_explicit_context_cache(structured_client, model_id): + structured_client.responses = [[("final", "gemini4 response")]] + args = _execute_kwargs(_config(use_cache=True, cache_seed=42)) + args["model"] = model_id + args["max_output_tokens"] = 131072 + args["timeout"] = 1200 + + output = gemini_nodes.SSL_GeminiTextPrompt.execute(**args) + + assert output[0] == "gemini4 response" + assert len(structured_client.calls) == 1 + sent_config = structured_client.calls[0]["config"] + assert sent_config.cached_content is None + assert sent_config.max_output_tokens == 131072 + assert sent_config.http_options.timeout == 1200000 + + +@pytest.mark.parametrize("model_id", ["gemini-4-carbon", "gemini-4-hydrogen"]) +def test_gemini_4_carbon_and_hydrogen_fingerprint_omits_sampling_params(model_id): + common = { + "config": {"api_key": "key", "api_version": "v1"}, + "prompt": "prompt", + "system_instruction": "system", + "model": model_id, + "max_output_tokens": 131072, + "include_images": False, + "aspect_ratio": "None", + "bypass_mode": "None", + "use_seed": False, + "seed": 0, + } + + fp1, _ = gemini_nodes.SSL_GeminiTextPrompt._compute_fingerprint_and_check_cache( + **common, + temperature=0.1, + top_p=0.5, + top_k=10, + thinking_budget=0, + thinking_level="None", + ) + fp2, _ = gemini_nodes.SSL_GeminiTextPrompt._compute_fingerprint_and_check_cache( + **common, + temperature=0.9, + top_p=0.99, + top_k=90, + thinking_budget=1000, + thinking_level="high" if model_id == "gemini-4-hydrogen" else "medium", + ) + assert fp1 == fp2 + diff --git a/tests/test_interactions_nodes.py b/tests/test_interactions_nodes.py index 4e3deb6..ce78b8a 100644 --- a/tests/test_interactions_nodes.py +++ b/tests/test_interactions_nodes.py @@ -828,3 +828,72 @@ def test_interactions_batch_videos(mock_interactions_client): assert len(mock_interactions_client.calls) == 2 +def test_interactions_gemini_4_carbon_and_hydrogen_schema_and_limits(): + schema = interactions_nodes.SSL_GeminiInteractionsTextPrompt.define_schema() + inputs = {inp.id: inp for inp in schema.inputs} + + for model_id in ( + "gemini-4-carbon", + "gemini-4-carbon-cyber-permissive", + "gemini-4-hydrogen", + ): + assert model_id in inputs["model"].options + assert model_id in interactions_nodes.SSL_GeminiInteractionsTextPrompt.THINKING_MODELS + assert model_id in interactions_nodes.SSL_GeminiInteractionsTextPrompt.GEN3_THINKING_MODELS + + assert "xhigh" in inputs["thinking_level"].options + assert inputs["max_output_tokens"].max == 1048576 + assert inputs["timeout"].max == 1200 + + +def test_interactions_gemini_4_carbon_and_hydrogen_thinking(mock_interactions_client): + mock_interactions_client.responses = [ + {"text": "carbon", "thoughts": "t1", "id": "int_c1"}, + {"text": "carbon cyber", "thoughts": "t2", "id": "int_c2"}, + {"text": "hydrogen", "thoughts": "t3", "id": "int_h1"}, + {"text": "hydrogen xhigh", "thoughts": "t4", "id": "int_h2"}, + ] + + for idx, (model_id, selected, expected) in enumerate([ + ("gemini-4-carbon", "None", "medium"), + ("gemini-4-carbon-cyber-permissive", "None", "high"), + ("gemini-4-hydrogen", "None", "high"), + ("gemini-4-hydrogen", "xhigh", "xhigh"), + ]): + args = _execute_kwargs( + model=model_id, + thinking_level=selected, + include_thoughts=True, + max_output_tokens=131072, + timeout=1200, + ) + interactions_nodes.SSL_GeminiInteractionsTextPrompt.execute(**args) + call = mock_interactions_client.calls[idx] + assert call["model"] == model_id + assert call["generation_config"]["thinking_level"] == expected + assert call["generation_config"]["thinking_summaries"] == "auto" + assert call["generation_config"]["max_output_tokens"] == 131072 + assert call["timeout"] == 1200.0 + + +@pytest.mark.parametrize( + ("model_id", "bad_level"), + [ + ("gemini-4-carbon", "minimal"), + ("gemini-4-carbon-cyber-permissive", "minimal"), + ("gemini-4-hydrogen", "minimal"), + ("gemini-4-carbon", "xhigh"), + ("gemini-4-carbon-cyber-permissive", "xhigh"), + ("gemini-4-argon", "xhigh"), + ("gemini-3.8-flash", "xhigh"), + ("gemini-3.1-pro-preview", "xhigh"), + ], +) +def test_interactions_gemini_4_rejects_invalid_thinking_levels(mock_interactions_client, model_id, bad_level): + args = _execute_kwargs(model=model_id, thinking_level=bad_level) + with pytest.raises(ValueError): + interactions_nodes.SSL_GeminiInteractionsTextPrompt.execute(**args) + assert len(mock_interactions_client.calls) == 0 + + +