diff --git a/README.md b/README.md index ea7afef..a7a9b43 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Special Note: Regarding the error [ERROR]API call error: 'NoneType' object has n ## How to Use -### 1. Configure API Key or Configure Vertex AI +### 1. Configure Gemini API or Enterprise/Vertex AI ### Option A(Gemini API) @@ -41,12 +41,13 @@ First, you need to obtain a Google Gemini API key: 2. Create an API key (In Google Cloud Console it is recommended to set key restriction to the Generative Language API) 3. In ComfyUI, locate and use the `Configure Gemini API Key` node to enter your API key. -### Option B(Vertex AI) but this is a bit complicated and a guide would be too verbose here -1. But start by visiting [Google Cloud Console Vertex AI API](https://console.cloud.google.com/apis/api/aiplatform.googleapis.com/overview) and Enable it. +### Option B (Gemini Enterprise Agent Platform, formerly Vertex AI) +1. Start by visiting [Google Cloud Console Vertex AI API](https://console.cloud.google.com/apis/api/aiplatform.googleapis.com/overview) and enable it. 2. Visit the [Vertex AI Model Garden](https://console.cloud.google.com/vertex-ai/model-garden) and select a Google Gemini model and then "view code" button. -3. Read the instructions for chosen model at "Try (Python)" for the environment variable option or "Try while using express mode (Python)" for API key (Currently unsure if API here is working correctly) -4. Set environment variables before launching: `GOOGLE_CLOUD_PROJECT=your-project-id`, `GOOGLE_CLOUD_LOCATION=global` and `GOOGLE_GENAI_USE_VERTEXAI=True` for variable option. +3. Read the instructions for the chosen model at "Try (Python)" for environment authentication or "Try while using express mode (Python)" for API-key authentication. +4. For environment authentication, set `GOOGLE_CLOUD_PROJECT=your-project-id`, `GOOGLE_CLOUD_LOCATION=global`, and `GOOGLE_GENAI_USE_ENTERPRISE=true` before launching ComfyUI. Existing installations may continue using `GOOGLE_GENAI_USE_VERTEXAI=true`; the node supports both names, with `GOOGLE_GENAI_USE_ENTERPRISE` taking precedence if both are set. 5. In ComfyUI, locate and use the `Configure Gemini API Key` node and set `use_vertexai_env` to `true`. +6. For Enterprise express mode, enter its API key and enable `vertexai_express`; project and location are not required for this mode. ### 2. Text Generation diff --git a/gemini_nodes.py b/gemini_nodes.py index 0322690..0a6a88b 100644 --- a/gemini_nodes.py +++ b/gemini_nodes.py @@ -16,35 +16,12 @@ import traceback import threading import queue import sys -import importlib import subprocess import random import hashlib +import warnings from typing import Any, Tuple -def check_and_install_dependencies(): - required_packages = { - 'requests': 'requests', - 'pysocks': 'PySocks', - } - - missing_packages = [] - for module_name, package_name in required_packages.items(): - try: - importlib.import_module(module_name) - except ImportError: - missing_packages.append(package_name) - - if missing_packages: - print(f"[WARNING] Missing required dependencies: {', '.join(missing_packages)}.") - print(f"[INFO] Please install them using: pip install {' '.join(missing_packages)}") - print(f"[INFO] Alternatively, install all requirements: pip install -r requirements.txt") - -try: - check_and_install_dependencies() -except Exception as e: - print(f"[WARNING] Error checking dependencies: {str(e)}") - class GetKeyAPI(IO.ComfyNode): @classmethod def define_schema(cls) -> IO.Schema: @@ -133,7 +110,7 @@ class SSL_GeminiAPIKeyConfig(IO.ComfyNode): inputs=[ IO.String.Input("api_key", multiline=False, default=""), IO.Combo.Input("api_version", options=["v1", "v1alpha", "v1beta", "v1beta1", "v2beta"], default="v1alpha", tooltip="Select API version to use. v1alpha, v1beta and v2beta are Gemini API specific while v1beta1 is Vertex AI specific. Both can use v1"), - IO.Boolean.Input("use_vertexai_env", default=False, tooltip="Bypasses rest of config and uses Vertex AI environment variables if set"), + IO.Boolean.Input("use_vertexai_env", default=False, tooltip="Uses Gemini Enterprise Agent Platform (formerly Vertex AI) with GOOGLE_GENAI_USE_ENTERPRISE or the legacy GOOGLE_GENAI_USE_VERTEXAI environment variable."), IO.Boolean.Input("vertexai_express", default=False), IO.String.Input("vertexai_project", optional=True), IO.String.Input("vertexai_location", optional=True), @@ -171,13 +148,17 @@ 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-lite", "gemini-3.6-flash", "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-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-lite", "gemini-3.6-flash" + "gemini-3-flash-preview", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.6-flash" + ] + IMAGE_MODELS = [ + "gemini-2.5-flash-image-preview", "gemini-2.5-flash-image", + "gemini-3.1-flash-image", "gemini-3.1-flash-lite-image", + "gemini-3-pro-image-preview", "gemini-3-pro-image", "nano-banana-pro-preview", ] - IMAGE_MODELS = ["gemini-2.5-flash-image-preview", "gemini-2.5-flash-image", "gemini-3-pro-image-preview", "nano-banana-pro-preview"] 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" @@ -199,7 +180,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-lite", "gemini-3.6-flash", "gemini-2.5-flash-image-preview", "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", "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), @@ -431,21 +412,50 @@ class SSL_GeminiTextPrompt(IO.ComfyNode): return actual_seed @classmethod - def _setup_proxy_env(cls, proxy_host, proxy_port): + def _build_proxy_url(cls, proxy_host, proxy_port): if not proxy_host.startswith(('http://', 'https://')): proxy_url = f"http://{proxy_host}:{proxy_port}" else: proxy_url = f"{proxy_host}:{proxy_port}" - os.environ['HTTP_PROXY'] = proxy_url - os.environ['HTTPS_PROXY'] = proxy_url - os.environ['http_proxy'] = proxy_url - os.environ['https_proxy'] = proxy_url - os.environ['REQUESTS_CA_BUNDLE'] = '' - print(f"[INFO] Proxy enabled: {proxy_url}") return proxy_url + @staticmethod + def _parse_boolean_environment_variable(name: str) -> bool | None: + value = os.environ.get(name) + if value is None: + return None + + normalized = value.strip().lower() + if normalized not in {"true", "1", "false", "0"}: + raise ValueError(f"{name} must be true, false, 1, or 0") + return normalized in {"true", "1"} + + @classmethod + def _resolve_enterprise_environment(cls) -> bool: + enterprise = cls._parse_boolean_environment_variable("GOOGLE_GENAI_USE_ENTERPRISE") + legacy_vertex = cls._parse_boolean_environment_variable("GOOGLE_GENAI_USE_VERTEXAI") + + if enterprise is not None and legacy_vertex is not None and enterprise != legacy_vertex: + warnings.warn( + "GOOGLE_GENAI_USE_ENTERPRISE and GOOGLE_GENAI_USE_VERTEXAI conflict; " + "GOOGLE_GENAI_USE_ENTERPRISE takes precedence.", + UserWarning, + stacklevel=2, + ) + + if enterprise is not None: + return enterprise + if legacy_vertex is not None: + return legacy_vertex + return True + + @staticmethod + def _build_http_options(api_version: str, proxy_url: str | None): + client_args = {"proxy": proxy_url} if proxy_url else None + return types.HttpOptions(api_version=api_version, client_args=client_args) + @classmethod def _build_generate_content_config(cls, model, temperature, top_p, top_k, max_output_tokens, seed, include_images, response_modalities, aspect_ratio, padded_system_instruction, @@ -532,12 +542,6 @@ class SSL_GeminiTextPrompt(IO.ComfyNode): print(f"[INFO] Returning cached result for fingerprint {fingerprint}") return IO.NodeOutput(cached_text, cached_image, cached_seed) - # --- keep most of the original implementation but converted to classmethod usage --- - original_http_proxy = os.environ.get('HTTP_PROXY') - original_https_proxy = os.environ.get('HTTPS_PROXY') - original_http_proxy_lower = os.environ.get('http_proxy') - original_https_proxy_lower = os.environ.get('https_proxy') - print(f"[INFO] Starting generation, model: {model}, temperature: {temperature}") padded_prompt = prompt @@ -562,12 +566,6 @@ class SSL_GeminiTextPrompt(IO.ComfyNode): print(f"[INFO] Using cached successful gemini seed {cached_gemini_seed} for input seed {input_seed}") actual_seed = cached_gemini_seed - # Flatten and simplify nested try/except blocks to ensure correct pairing - original_http_proxy = os.environ.get('HTTP_PROXY') - original_https_proxy = os.environ.get('HTTPS_PROXY') - original_http_proxy_lower = os.environ.get('http_proxy') - original_https_proxy_lower = os.environ.get('https_proxy') - text_output = "" image_tensor = cls.generate_empty_image() proxy_url: str | None = None @@ -575,37 +573,7 @@ class SSL_GeminiTextPrompt(IO.ComfyNode): try: if use_proxy: - proxy_url = cls._setup_proxy_env(proxy_host, proxy_port) - - # Initialize Gemini client - client_options = {} - if use_proxy: - try: - import google.api_core.http_client # type: ignore[import] - import google.auth.transport.requests # type: ignore[import] - import requests - from requests.adapters import HTTPAdapter - - class ProxyAdapter(HTTPAdapter): - def __init__(self, proxy_url, **kwargs): - self.proxy_url = proxy_url - super().__init__(**kwargs) - - def add_headers(self, request, **kwargs): - super().add_headers(request, **kwargs) - - session = requests.Session() - proxies = {"http": str(proxy_url), "https": str(proxy_url)} - session.proxies.update(proxies) - adapter = ProxyAdapter(proxy_url, max_retries=1) - session.mount('http://', adapter) - session.mount('https://', adapter) - session.verify = False - http_client = google.api_core.http_client.RequestsHttpClient(session=session) - client_options["http_client"] = http_client - except Exception: - # best-effort proxy HTTP client setup; fall back if imports fail - pass + proxy_url = cls._build_proxy_url(proxy_host, proxy_port) try: vertexai_express = config.get("vertexai_express", False) @@ -614,15 +582,16 @@ class SSL_GeminiTextPrompt(IO.ComfyNode): project = config.get("vertexai_project") location = config.get("vertexai_location") credentials_path = config.get("google_application_credentials") + http_options = cls._build_http_options(api_version, proxy_url) if use_vertexai_env: try: - env_use_value = os.environ.get("GOOGLE_GENAI_USE_VERTEXAI", "true").strip().lower() - if env_use_value not in {"true", "1", "false", "0"}: - raise ValueError("GOOGLE_GENAI_USE_VERTEXAI must be true, false, 1, or 0") - env_use = env_use_value in {"true", "1"} + env_use = cls._resolve_enterprise_environment() if not env_use: - raise ValueError("GOOGLE_GENAI_USE_VERTEXAI must be enabled when use_vertexai_env is true") + raise ValueError( + "GOOGLE_GENAI_USE_ENTERPRISE or GOOGLE_GENAI_USE_VERTEXAI " + "must be enabled when use_vertexai_env is true" + ) credentials = None credentials_cache_key = None @@ -648,12 +617,11 @@ class SSL_GeminiTextPrompt(IO.ComfyNode): client_key = ("vertexai_env", env_use, env_proj, env_loc, api_version, proxy_url, credentials_cache_key) if client_key not in cls._client_cache: cls._client_cache[client_key] = genai.Client( - vertexai=env_use, + enterprise=env_use, credentials=credentials, project=env_proj, location=env_loc, - http_options=types.HttpOptions(api_version=api_version), - **client_options + http_options=http_options, ) print(f"[INFO] Created new genai.Client (vertexai_env)") is_new_client = True @@ -672,39 +640,23 @@ class SSL_GeminiTextPrompt(IO.ComfyNode): except ValueError as e: print(f"Error: {e}") - return IO.NodeOutput(f"Invalid Vertex AI configuration: {e}", cls.generate_empty_image(), actual_seed if actual_seed is not None else 0) + return IO.NodeOutput(f"Invalid Enterprise/Vertex AI configuration: {e}", cls.generate_empty_image(), actual_seed if actual_seed is not None else 0) elif vertexai_express: - - if not project: - try: - project = os.environ["GOOGLE_CLOUD_PROJECT"].strip() - assert project, "GOOGLE_CLOUD_PROJECT is empty" - except KeyError: - print("Missing required environment variable: GOOGLE_CLOUD_PROJECT") - return IO.NodeOutput("Missing environment variable: GOOGLE_CLOUD_PROJECT", cls.generate_empty_image(), actual_seed if actual_seed is not None else 0) - else: - os.environ.setdefault("GOOGLE_CLOUD_PROJECT", project) - - if not location: - try: - location = os.environ["GOOGLE_CLOUD_LOCATION"].strip() - assert location, "GOOGLE_CLOUD_LOCATION is empty" - except KeyError: - print("Missing required environment variable: GOOGLE_CLOUD_LOCATION") - return IO.NodeOutput("Missing environment variable: GOOGLE_CLOUD_LOCATION", cls.generate_empty_image(), actual_seed if actual_seed is not None else 0) - else: - os.environ.setdefault("GOOGLE_CLOUD_LOCATION", location) - api_key = config.get("api_key") + if not api_key: + return IO.NodeOutput( + "Invalid Enterprise express configuration: API key is required", + cls.generate_empty_image(), + actual_seed if actual_seed is not None else 0, + ) api_key_hash = hashlib.sha256(str(api_key).encode("utf-8")).hexdigest() if api_key else None - client_key = ("vertexai_express", api_key_hash, project, location, api_version, proxy_url) + client_key = ("vertexai_express", api_key_hash, api_version, proxy_url) if client_key not in cls._client_cache: cls._client_cache[client_key] = genai.Client( - vertexai=True, + enterprise=True, api_key=api_key, - http_options=types.HttpOptions(api_version=api_version), - **client_options + http_options=http_options, ) print(f"[INFO] Created new genai.Client (vertexai_express)") is_new_client = True @@ -720,8 +672,7 @@ class SSL_GeminiTextPrompt(IO.ComfyNode): if client_key not in cls._client_cache: cls._client_cache[client_key] = genai.Client( api_key=api_key, - http_options=types.HttpOptions(api_version=api_version), - **client_options + http_options=http_options, ) print(f"[INFO] Created new genai.Client (standard)") is_new_client = True @@ -968,43 +919,6 @@ class SSL_GeminiTextPrompt(IO.ComfyNode): print(f"[INFO] Cached successful gemini seed {final_actual_seed} for input seed {input_seed}") - try: - return IO.NodeOutput(text_output, image_tensor, final_actual_seed) - finally: - if original_http_proxy: - os.environ['HTTP_PROXY'] = original_http_proxy - else: - if 'HTTP_PROXY' in os.environ: - os.environ.pop('HTTP_PROXY') - - if original_https_proxy: - os.environ['HTTPS_PROXY'] = original_https_proxy - else: - if 'HTTPS_PROXY' in os.environ: - os.environ.pop('HTTPS_PROXY') - - if original_http_proxy_lower: - os.environ['http_proxy'] = original_http_proxy_lower - else: - if 'http_proxy' in os.environ: - os.environ.pop('http_proxy') - - if original_https_proxy_lower: - os.environ['https_proxy'] = original_https_proxy_lower - else: - if 'https_proxy' in os.environ: - os.environ.pop('https_proxy') - - if 'REQUESTS_CA_BUNDLE' in os.environ: - os.environ.pop('REQUESTS_CA_BUNDLE') - - try: - import requests - if hasattr(requests, 'Session'): - clean_session = requests.Session() - if hasattr(requests, 'session'): - requests.session = lambda: clean_session - except: - pass + return IO.NodeOutput(text_output, image_tensor, final_actual_seed) # V3 uses ComfyExtension entrypoint in __init__.py to expose nodes diff --git a/pyproject.toml b/pyproject.toml index ef100bc..7ff2daf 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.3.8" license = {file = "LICENSE"} -dependencies = ["google-genai", "numpy", "torch", "requests", "PySocks"] +dependencies = ["google-genai>=2.17.0", "numpy", "torch"] [project.urls] Repository = "https://github.com/silveroxides/ComfyUI_Gemini_Expanded_API" diff --git a/requirements.txt b/requirements.txt index 3ee6a39..cbb7460 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,4 @@ -google-genai>=1.72.0 -numpy -torch -Pillow -requests -PySocks +google-genai>=2.17.0 +numpy +torch +Pillow diff --git a/tests/test_gemini_nodes.py b/tests/test_gemini_nodes.py index 24ad0ca..24b34fb 100644 --- a/tests/test_gemini_nodes.py +++ b/tests/test_gemini_nodes.py @@ -4,6 +4,7 @@ from io import BytesIO from types import SimpleNamespace import google.auth +import pytest import torch from PIL import Image @@ -27,12 +28,57 @@ def _execute_kwargs(config): } +def _config(**overrides): + config = { + "api_key": "", + "api_version": "v1", + "use_vertexai_env": False, + "vertexai_express": False, + "vertexai_project": "", + "vertexai_location": "", + "google_application_credentials": "", + } + config.update(overrides) + return config + + +def _install_success_client(monkeypatch, captured): + class FakeModels: + @staticmethod + def generate_content(**kwargs): + part = SimpleNamespace(text="ok", inline_data=None) + content = SimpleNamespace(parts=[part]) + return SimpleNamespace(candidates=[SimpleNamespace(content=content)]) + + class FakeClient: + def __init__(self, **kwargs): + captured.update(kwargs) + self.models = FakeModels() + + monkeypatch.setattr(gemini_nodes.genai, "Client", FakeClient) + gemini_nodes.SSL_GeminiTextPrompt._client_cache.clear() + + def test_timeout_fallback_is_last_input(): schema = gemini_nodes.SSL_GeminiTextPrompt.define_schema() assert schema.inputs[-1].id == "timeout_fallback_text" +def test_vertex_config_socket_ids_remain_stable(): + schema = gemini_nodes.SSL_GeminiAPIKeyConfig.define_schema() + + assert [input_.id for input_ in schema.inputs] == [ + "api_key", + "api_version", + "use_vertexai_env", + "vertexai_express", + "vertexai_project", + "vertexai_location", + "google_application_credentials", + ] + + def test_image_inputs_use_clean_autogrow_socket_ids(): schema = gemini_nodes.SSL_GeminiTextPrompt.define_schema() image_inputs = next(input_ for input_ in schema.inputs if input_.id == "image_inputs") @@ -120,6 +166,7 @@ def test_all_ordered_images_are_sent_to_gemini_2_and_3_before_prompt(monkeypatch assert output[0] == "ok" assert_ordered_parts() + monkeypatch.delenv("GOOGLE_GENAI_USE_ENTERPRISE", raising=False) monkeypatch.setenv("GOOGLE_GENAI_USE_VERTEXAI", "true") monkeypatch.setenv("GOOGLE_CLOUD_PROJECT", "test-project") monkeypatch.setenv("GOOGLE_CLOUD_LOCATION", "global") @@ -191,6 +238,7 @@ def test_api_key_is_hashed_in_fingerprint(): def test_false_vertex_environment_value_is_rejected(monkeypatch): + monkeypatch.delenv("GOOGLE_GENAI_USE_ENTERPRISE", raising=False) monkeypatch.setenv("GOOGLE_GENAI_USE_VERTEXAI", "false") monkeypatch.setenv("GOOGLE_CLOUD_PROJECT", "test-project") monkeypatch.setenv("GOOGLE_CLOUD_LOCATION", "global") @@ -212,8 +260,8 @@ def test_false_vertex_environment_value_is_rejected(monkeypatch): output = gemini_nodes.SSL_GeminiTextPrompt.execute(**_execute_kwargs(config)) assert output[0] == ( - "Invalid Vertex AI configuration: GOOGLE_GENAI_USE_VERTEXAI must be enabled " - "when use_vertexai_env is true" + "Invalid Enterprise/Vertex AI configuration: GOOGLE_GENAI_USE_ENTERPRISE or " + "GOOGLE_GENAI_USE_VERTEXAI must be enabled when use_vertexai_env is true" ) @@ -223,6 +271,7 @@ def test_adc_path_is_loaded_and_passed_to_vertex_client(monkeypatch, tmp_path): credentials = object() captured = {} + monkeypatch.delenv("GOOGLE_GENAI_USE_ENTERPRISE", raising=False) monkeypatch.setenv("GOOGLE_GENAI_USE_VERTEXAI", "true") monkeypatch.delenv("GOOGLE_CLOUD_PROJECT", raising=False) monkeypatch.setenv("GOOGLE_CLOUD_LOCATION", "global") @@ -259,11 +308,115 @@ def test_adc_path_is_loaded_and_passed_to_vertex_client(monkeypatch, tmp_path): output = gemini_nodes.SSL_GeminiTextPrompt.execute(**_execute_kwargs(config)) assert output[0] == "ok" - assert captured["vertexai"] is True + assert captured["enterprise"] is True + assert "vertexai" not in captured assert captured["credentials"] is credentials assert captured["project"] == "credentials-project" +def test_enterprise_environment_takes_precedence_over_legacy_vertex(monkeypatch): + captured = {} + monkeypatch.setenv("GOOGLE_GENAI_USE_ENTERPRISE", "true") + monkeypatch.setenv("GOOGLE_GENAI_USE_VERTEXAI", "false") + monkeypatch.setenv("GOOGLE_CLOUD_PROJECT", "test-project") + monkeypatch.setenv("GOOGLE_CLOUD_LOCATION", "global") + _install_success_client(monkeypatch, captured) + + with pytest.warns(UserWarning, match="GOOGLE_GENAI_USE_ENTERPRISE takes precedence"): + output = gemini_nodes.SSL_GeminiTextPrompt.execute( + **_execute_kwargs(_config(use_vertexai_env=True)) + ) + + assert output[0] == "ok" + assert captured["enterprise"] is True + assert captured["project"] == "test-project" + assert captured["location"] == "global" + + +def test_vertex_environment_mode_defaults_to_enterprise_when_selector_is_absent(monkeypatch): + captured = {} + monkeypatch.delenv("GOOGLE_GENAI_USE_ENTERPRISE", raising=False) + monkeypatch.delenv("GOOGLE_GENAI_USE_VERTEXAI", raising=False) + monkeypatch.setenv("GOOGLE_CLOUD_PROJECT", "test-project") + monkeypatch.setenv("GOOGLE_CLOUD_LOCATION", "global") + _install_success_client(monkeypatch, captured) + + output = gemini_nodes.SSL_GeminiTextPrompt.execute( + **_execute_kwargs(_config(use_vertexai_env=True)) + ) + + assert output[0] == "ok" + assert captured["enterprise"] is True + + +def test_malformed_enterprise_environment_value_is_rejected(monkeypatch): + monkeypatch.setenv("GOOGLE_GENAI_USE_ENTERPRISE", "yes") + monkeypatch.delenv("GOOGLE_GENAI_USE_VERTEXAI", raising=False) + monkeypatch.setenv("GOOGLE_CLOUD_PROJECT", "test-project") + monkeypatch.setenv("GOOGLE_CLOUD_LOCATION", "global") + monkeypatch.setattr( + gemini_nodes.genai, + "Client", + lambda **kwargs: (_ for _ in ()).throw(AssertionError("Client must not be created")), + ) + + output = gemini_nodes.SSL_GeminiTextPrompt.execute( + **_execute_kwargs(_config(use_vertexai_env=True)) + ) + + assert output[0] == ( + "Invalid Enterprise/Vertex AI configuration: " + "GOOGLE_GENAI_USE_ENTERPRISE must be true, false, 1, or 0" + ) + + +def test_enterprise_express_requires_only_api_key(monkeypatch): + captured = {} + monkeypatch.delenv("GOOGLE_CLOUD_PROJECT", raising=False) + monkeypatch.delenv("GOOGLE_CLOUD_LOCATION", raising=False) + _install_success_client(monkeypatch, captured) + + output = gemini_nodes.SSL_GeminiTextPrompt.execute( + **_execute_kwargs(_config(api_key="express-key", vertexai_express=True)) + ) + + assert output[0] == "ok" + assert captured["enterprise"] is True + assert captured["api_key"] == "express-key" + assert "project" not in captured + assert "location" not in captured + + +def test_proxy_uses_sdk_http_options_without_mutating_environment(monkeypatch): + captured = {} + monkeypatch.setenv("HTTP_PROXY", "http://existing-proxy:9000") + _install_success_client(monkeypatch, captured) + + kwargs = _execute_kwargs(_config(api_key="test-key")) + kwargs.update(use_proxy=True, proxy_host="proxy.example", proxy_port=8080) + output = gemini_nodes.SSL_GeminiTextPrompt.execute(**kwargs) + + assert output[0] == "ok" + assert captured["http_options"].client_args == { + "proxy": "http://proxy.example:8080" + } + assert gemini_nodes.os.environ["HTTP_PROXY"] == "http://existing-proxy:9000" + + +def test_confirmed_current_models_are_visible_without_removing_legacy_ids(): + schema = gemini_nodes.SSL_GeminiTextPrompt.define_schema() + model_input = next(input_ for input_ in schema.inputs if input_.id == "model") + + for model in ( + "gemini-3.5-flash", + "gemini-3.1-flash-image", + "gemini-3.1-flash-lite-image", + "gemini-3-pro-image", + "gemini-2.5-flash-image-preview", + ): + assert model in model_input.options + + def test_timeout_returns_custom_fallback_without_caching(monkeypatch): release_request = threading.Event()