From a76f04a05253e9598e30950cbfdec39256f18f45 Mon Sep 17 00:00:00 2001 From: silveroxides Date: Mon, 17 Aug 2026 15:36:02 +0200 Subject: [PATCH] feat(gemini): add 3.7 Flash --- gemini_nodes.py | 50 ++++++++++++++++--- tests/test_gemini_nodes.py | 100 +++++++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+), 7 deletions(-) diff --git a/gemini_nodes.py b/gemini_nodes.py index bda3ab5..9185ed6 100644 --- a/gemini_nodes.py +++ b/gemini_nodes.py @@ -143,6 +143,7 @@ class SSL_GeminiTextPrompt(IO.ComfyNode): _cache: dict = {} _seed_map_cache: dict = {} # Maps (input_seed, fingerprint_without_seed) -> successful_gemini_seed _client_cache: dict = {} # Maps client_key tuple -> genai.Client instance + GEMINI_3_7_FLASH = "gemini-3.7-flash" # Placeholder only. Keep this out of the model combo until Google's hosted-model # documentation confirms the final ID and gemini-3.6-flash-equivalent capabilities @@ -153,11 +154,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_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_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_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_4_FLASH_PREVIEW ] IMAGE_MODELS = [ "gemini-2.5-flash-image-preview", "gemini-2.5-flash-image", @@ -185,7 +186,7 @@ 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", "gemini-2.5-flash-image-preview", "gemini-3.1-flash-image", "gemini-3.1-flash-lite-image", "gemini-3-pro-image", "nano-banana-pro-preview", "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, "gemini-2.5-flash-image-preview", "gemini-3.1-flash-image", "gemini-3.1-flash-lite-image", "gemini-3-pro-image", "nano-banana-pro-preview", "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), @@ -211,7 +212,7 @@ class SSL_GeminiTextPrompt(IO.ComfyNode): IO.Int.Input("seed", default=0, min=0, max=2147483647), IO.Int.Input("timeout", default=30, min=15, max=300, 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."), + 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 Flash always uses this control, defaults to medium, and ignores thinking_budget."), 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."), @@ -310,6 +311,22 @@ class SSL_GeminiTextPrompt(IO.ComfyNode): ) return frames, batch_counts + @classmethod + def _resolve_gemini_3_7_thinking_level(cls, thinking_level): + if thinking_level is None or thinking_level == "None": + return "medium" + if thinking_level == "minimal": + print( + "[WARNING] Gemini 3.7 Flash does not support minimal " + "thinking; using low instead." + ) + return "low" + if thinking_level not in {"low", "medium", "high"}: + raise ValueError( + "Gemini 3.7 Flash thinking_level must be low, medium, or high." + ) + return thinking_level + @classmethod def _compute_fingerprint_and_check_cache(cls, config, prompt, system_instruction, model, temperature, top_p, top_k, max_output_tokens, include_images, aspect_ratio, bypass_mode, thinking_budget, use_seed, seed, @@ -340,6 +357,9 @@ class SSL_GeminiTextPrompt(IO.ComfyNode): eff_thinking_level = "None" eff_thinking_budget = -1 eff_include_thoughts = False + eff_temperature = None if model == cls.GEMINI_3_7_FLASH else float(temperature) + eff_top_p = None if model == cls.GEMINI_3_7_FLASH else float(top_p) + eff_top_k = None if model == cls.GEMINI_3_7_FLASH else int(top_k) # Logic mirroring _build_generate_content_config exactly if include_images and model in cls.IMAGE_MODELS: @@ -347,6 +367,10 @@ class SSL_GeminiTextPrompt(IO.ComfyNode): eff_aspect_ratio = str(aspect_ratio) # When generating images, thinking params are ignored + elif model == cls.GEMINI_3_7_FLASH: + eff_thinking_level = cls._resolve_gemini_3_7_thinking_level(thinking_level) + eff_include_thoughts = include_thoughts + elif model in cls.GEN3_THINKING_MODELS and thinking_level is not None and thinking_level != "None": eff_thinking_level = thinking_level eff_include_thoughts = include_thoughts @@ -369,9 +393,9 @@ class SSL_GeminiTextPrompt(IO.ComfyNode): prompt, system_instruction, model, - float(temperature), - float(top_p), - int(top_k), + eff_temperature, + eff_top_p, + eff_top_k, int(max_output_tokens), eff_include_images, # EFFECTIVE include_images eff_aspect_ratio, # EFFECTIVE aspect_ratio @@ -491,6 +515,18 @@ class SSL_GeminiTextPrompt(IO.ComfyNode): system_instruction=[types.Part.from_text(text=padded_system_instruction)], ) + if model == cls.GEMINI_3_7_FLASH: + return types.GenerateContentConfig( + max_output_tokens=max_output_tokens, + safety_settings=safety, + thinking_config=types.ThinkingConfig( + include_thoughts=include_thoughts, + thinking_level=cls._resolve_gemini_3_7_thinking_level(thinking_level), + ), + response_modalities=response_modalities, + system_instruction=[types.Part.from_text(text=padded_system_instruction)], + ) + 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": diff --git a/tests/test_gemini_nodes.py b/tests/test_gemini_nodes.py index ce0479b..d4947fc 100644 --- a/tests/test_gemini_nodes.py +++ b/tests/test_gemini_nodes.py @@ -443,6 +443,7 @@ def test_confirmed_current_models_are_visible_without_removing_legacy_ids(): model_input = next(input_ for input_ in schema.inputs if input_.id == "model") for model in ( + "gemini-3.7-flash", "gemini-3.5-flash", "gemini-3.1-flash-image", "gemini-3.1-flash-lite-image", @@ -452,6 +453,105 @@ def test_confirmed_current_models_are_visible_without_removing_legacy_ids(): assert model in model_input.options +@pytest.mark.parametrize( + ("selected_level", "expected_level"), + [("None", "medium"), ("low", "low"), ("medium", "medium"), ("high", "high")], +) +def test_gemini_3_7_uses_supported_thinking_levels(selected_level, expected_level): + config = gemini_nodes.SSL_GeminiTextPrompt._build_generate_content_config( + model="gemini-3.7-flash", + temperature=0.4, + top_p=0.8, + top_k=20, + max_output_tokens=1024, + seed=7, + include_images=False, + response_modalities=["TEXT"], + aspect_ratio="None", + padded_system_instruction="system", + thinking_level=selected_level, + thinking_budget=4096, + include_thoughts=True, + media_resolution="unspecified", + ) + + assert config.thinking_config.thinking_level.value == expected_level.upper() + assert config.thinking_config.thinking_budget is None + assert config.temperature is None + assert config.top_p is None + assert config.top_k is None + + +def test_gemini_3_7_fingerprint_matches_effective_request_controls(): + common = { + "config": {"api_key": "key", "api_version": "v1"}, + "prompt": "prompt", + "system_instruction": "system", + "model": "gemini-3.7-flash", + "max_output_tokens": 1024, + "include_images": False, + "aspect_ratio": "None", + "bypass_mode": "None", + "use_seed": False, + "seed": 0, + "include_thoughts": True, + } + + default_fingerprint, _ = gemini_nodes.SSL_GeminiTextPrompt._compute_fingerprint_and_check_cache( + **common, + temperature=0.4, + top_p=0.8, + top_k=20, + thinking_budget=0, + thinking_level="None", + ) + explicit_medium_fingerprint, _ = gemini_nodes.SSL_GeminiTextPrompt._compute_fingerprint_and_check_cache( + **common, + temperature=1.0, + top_p=0.95, + top_k=40, + thinking_budget=8192, + thinking_level="medium", + ) + high_fingerprint, _ = gemini_nodes.SSL_GeminiTextPrompt._compute_fingerprint_and_check_cache( + **common, + temperature=1.0, + top_p=0.95, + top_k=40, + thinking_budget=8192, + thinking_level="high", + ) + minimal_fingerprint, _ = gemini_nodes.SSL_GeminiTextPrompt._compute_fingerprint_and_check_cache( + **common, + temperature=1.0, + top_p=0.95, + top_k=40, + thinking_budget=8192, + thinking_level="minimal", + ) + low_fingerprint, _ = gemini_nodes.SSL_GeminiTextPrompt._compute_fingerprint_and_check_cache( + **common, + temperature=0.2, + top_p=0.7, + top_k=10, + thinking_budget=0, + thinking_level="low", + ) + + assert default_fingerprint == explicit_medium_fingerprint + assert high_fingerprint != default_fingerprint + assert minimal_fingerprint == low_fingerprint + + +def test_gemini_3_7_coerces_minimal_to_low_with_warning(capsys): + resolved = gemini_nodes.SSL_GeminiTextPrompt._resolve_gemini_3_7_thinking_level( + "minimal" + ) + + assert resolved == "low" + assert "does not support minimal thinking; using low instead" in capsys.readouterr().out + + def test_gemini_4_placeholder_matches_3_6_without_ui_exposure(): placeholder = gemini_nodes.SSL_GeminiTextPrompt.GEMINI_4_FLASH_PREVIEW reference = "gemini-3.6-flash"