"""Config flow for AI Agent HA integration.""" from __future__ import annotations import logging import voluptuous as vol from homeassistant import config_entries from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.selector import ( SelectSelector, SelectSelectorConfig, TextSelector, TextSelectorConfig, ) from .agent import ( fetch_gemini_models, fetch_openai_compatible_models, fetch_openai_models, ) from .const import ( CONF_LOCAL_OLLAMA_URL, CONF_OPENAI_BASE_URL, CONF_OPENAI_COMPATIBLE_URL, DOMAIN, ) _LOGGER = logging.getLogger(__name__) PROVIDERS = { "llama": "Llama", "openai": "OpenAI", "gemini": "Google Gemini", "openrouter": "OpenRouter", "anthropic": "Anthropic (Claude)", "alter": "Alter", "zai": "z.ai", "local_ollama": "Local Ollama", "openai_compatible": "Local OpenAI-Compatible (e.g. LM Studio, vLLM)", } TOKEN_FIELD_NAMES = { "llama": "llama_token", "openai": "openai_token", "gemini": "gemini_token", "openrouter": "openrouter_token", "anthropic": "anthropic_token", "alter": "alter_token", "zai": "zai_token", "zai_endpoint": "zai_endpoint", "local_ollama": CONF_LOCAL_OLLAMA_URL, # For local Ollama models, we use URL instead of token "openai_compatible": CONF_OPENAI_COMPATIBLE_URL, # For OpenAI-compatible endpoints } TOKEN_LABELS = { "llama": "Llama API Token", "openai": "OpenAI API Key", "gemini": "Google Gemini API Key", "openrouter": "OpenRouter API Key", "anthropic": "Anthropic API Key", "alter": "Alter API Key", "zai": "z.ai API Key", "zai_endpoint": "z.ai API Endpoint Type", "local_ollama": "Local Ollama API URL (e.g., http://localhost:11434/api/generate)", "openai_compatible": "OpenAI-Compatible URL (e.g., http://example.com/v1/ or http://localhost:8080/v1/). Must end with /v1/", } DEFAULT_MODELS = { "llama": "Llama-4-Maverick-17B-128E-Instruct-FP8", "openai": "gpt-5", "gemini": "gemini-2.5-flash", "openrouter": "openai/gpt-4o", "anthropic": "claude-sonnet-4-5-20250929", "alter": "", # User enters custom model "zai": "glm-4.7", # Z.ai's latest flagship model "local_ollama": "llama3.2", # Updated to use llama3.2 as default for local Ollama "openai_compatible": "", # User enters custom model for OpenAI-compatible endpoint } AVAILABLE_MODELS = { "openai": [ "gpt-5", "gpt-5-mini", "gpt-5-nano", "gpt-4.1", "gpt-4.1-mini", "gpt-4.1-nano", "gpt-4o", "gpt-4o-mini", "o3", "o3-mini", "o4-mini", "o1", "o1-preview", "o1-mini", "gpt-4-turbo", "gpt-4", "gpt-3.5-turbo", ], "gemini": [ "gemini-2.5-flash", "gemini-2.5-pro", "gemini-2.0-flash", "gemini-2.0-flash-exp", "gemini-2.5-flash-preview", "gemini-2.5-pro-preview", ], "openrouter": [ "openai/gpt-4o", "openai/gpt-4-turbo", "openai/gpt-3.5-turbo", "anthropic/claude-3.5-sonnet", "anthropic/claude-3-sonnet", "anthropic/claude-3-haiku", "meta-llama/llama-3.1-70b-instruct", "meta-llama/llama-3.2-90b-instruct", "google/gemini-pro", "mistralai/mixtral-8x7b-instruct", "deepseek/deepseek-r1", ], "anthropic": [ "claude-opus-4-7", "claude-sonnet-4-6", "claude-haiku-4-5", "claude-sonnet-4-5-20250929", "claude-sonnet-4-20250514", "claude-3-5-sonnet-20241022", "claude-3-5-haiku-20241022", "claude-3-opus-20240229", "claude-3-sonnet-20240229", "claude-3-haiku-20240307", ], "llama": [ "Llama-4-Maverick-17B-128E-Instruct-FP8", "Llama-3.1-70B-Instruct", "Llama-3.1-8B-Instruct", "Llama-3.2-90B-Instruct", ], # Alter - user enters custom model name only "alter": [ "Custom...", ], # z.ai - available models "zai": [ "glm-4.7", "glm-4.6", "glm-4.5", "glm-4.5-air", "glm-4.5-x", "glm-4.5-airx", "glm-4.5-flash", "glm-4-32b-0414-128k", "Custom...", ], # For local Ollama models, provide common models with llama3.2 as the default "local_ollama": [ "llama3.2", "llama3", "llama3.1", "mistral", "mixtral", "deepseek-coder", "Custom...", ], # For OpenAI-compatible endpoints, user should specify their model "openai_compatible": [ "Custom...", ], } DEFAULT_PROVIDER = "openai" class AiAgentHaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # type: ignore[call-arg,misc] """Handle a config flow for AI Agent HA.""" VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL @staticmethod @callback def async_get_options_flow(config_entry): """Get the options flow for this handler.""" try: return AiAgentHaOptionsFlowHandler() except Exception as e: _LOGGER.error("Error creating options flow: %s", e) return None async def async_step_user(self, user_input=None): """Handle the initial step.""" errors = {} if user_input is not None: # Check if this provider is already configured await self.async_set_unique_id(f"ai_agent_ha_{user_input['ai_provider']}") self._abort_if_unique_id_configured() self.config_data = {"ai_provider": user_input["ai_provider"]} return await self.async_step_configure() # Show provider selection form return self.async_show_form( step_id="user", data_schema=vol.Schema( { vol.Required("ai_provider"): SelectSelector( SelectSelectorConfig( options=[ {"value": k, "label": v} for k, v in PROVIDERS.items() ] ) ), } ), ) async def async_step_configure(self, user_input=None): """Handle the configuration step for the selected provider.""" errors = {} provider = self.config_data["ai_provider"] token_field = TOKEN_FIELD_NAMES[provider] token_label = TOKEN_LABELS[provider] default_model = DEFAULT_MODELS[provider] # For Alter provider, default to "Custom..." for the dropdown since model is user-provided dropdown_default = "Custom..." if provider == "alter" else default_model available_models = AVAILABLE_MODELS.get(provider, [default_model]) if user_input is not None: try: # Validate the token token_value = user_input.get(token_field) if not token_value: errors[token_field] = "required" raise InvalidApiKey # Store the configuration data self.config_data[token_field] = token_value # For z.ai, store endpoint type if provider == "zai": endpoint_type = user_input.get("zai_endpoint", "general") self.config_data["zai_endpoint"] = endpoint_type # For OpenAI, store Base URL (defaults to official endpoint if unchanged) if provider == "openai": base_url = (user_input.get(CONF_OPENAI_BASE_URL) or "").strip() self.config_data[CONF_OPENAI_BASE_URL] = ( base_url or "https://api.openai.com/v1" ) # For OpenAI, move to next step to select model from dynamic list return await self.async_step_configure_openai_models() # For OpenAI-Compatible, store Base URL + optional API key, then move on if provider == "openai_compatible": base_url = ( user_input.get(CONF_OPENAI_COMPATIBLE_URL) or "" ).strip() self.config_data[CONF_OPENAI_COMPATIBLE_URL] = base_url api_key = ( user_input.get("openai_compatible_api_key") or "" ).strip() self.config_data["openai_compatible_api_key"] = api_key # Move to next step to select model from dynamic list return await self.async_step_configure_openai_compatible_models() # Add model configuration if provided selected_model = user_input.get("model") custom_model = user_input.get("custom_model") _LOGGER.debug( f"Config flow - Provider: {provider}, Selected model: {selected_model}, Custom model: {custom_model}" ) # Initialize models dict if it doesn't exist if "models" not in self.config_data: self.config_data["models"] = {} if custom_model and custom_model.strip(): # Use custom model if provided and not empty self.config_data["models"][provider] = custom_model.strip() elif selected_model and selected_model != "Custom...": # Use selected model if it's not the "Custom..." option self.config_data["models"][provider] = selected_model else: # For local_ollama, openai_compatible, alter, and zai providers, allow empty model name if provider in ( "local_ollama", "openai_compatible", "alter", "zai", ): self.config_data["models"][provider] = "" else: # Fallback to default model for other providers self.config_data["models"][provider] = default_model return self.async_create_entry( title=f"AI Agent HA ({PROVIDERS[provider]})", data=self.config_data, ) except InvalidApiKey: errors["base"] = "invalid_api_key" except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" if provider == "zai": # For z.ai provider, we need token, endpoint type, and optional model name model_options = AVAILABLE_MODELS.get("zai", ["Custom..."]) schema_dict = { vol.Required(token_field): TextSelector( TextSelectorConfig(type="password") ), vol.Optional("zai_endpoint", default="general"): SelectSelector( SelectSelectorConfig( options=[ {"value": "general", "label": "General Purpose"}, {"value": "coding", "label": "Coding (3× usage, 1/7 cost)"}, ] ) ), vol.Optional("model", default="glm-4.7"): SelectSelector( SelectSelectorConfig(options=model_options) ), vol.Optional("custom_model"): TextSelector( TextSelectorConfig(type="text") ), } return self.async_show_form( step_id="configure", data_schema=vol.Schema(schema_dict), errors=errors, description_placeholders={ "token_label": token_label, "provider": PROVIDERS[provider], }, ) if provider == "local_ollama": # For local_ollama provider, we need both URL and optional model name schema_dict = { vol.Required(CONF_LOCAL_OLLAMA_URL): TextSelector( TextSelectorConfig(type="text") ), } # Add model selection model_options = AVAILABLE_MODELS.get("local_ollama", ["Custom..."]) schema_dict[vol.Optional("model", default="Custom...")] = SelectSelector( SelectSelectorConfig(options=model_options) ) schema_dict[vol.Optional("custom_model")] = TextSelector( TextSelectorConfig(type="text") ) return self.async_show_form( step_id="configure", data_schema=vol.Schema(schema_dict), errors=errors, description_placeholders={ "token_label": "Local Ollama API URL", # nosec B105 - UI label string shown next to the URL field, not a credential "provider": PROVIDERS[provider], }, ) if provider == "openai_compatible": # For openai_compatible provider, we need base URL + optional API key. # Many local endpoints (LM Studio, vLLM) need no key; gateways like # Open WebUI or LiteLLM require one. Models are fetched in the next step. schema_dict = { vol.Required(CONF_OPENAI_COMPATIBLE_URL): TextSelector( TextSelectorConfig(type="text") ), vol.Optional("openai_compatible_api_key", default=""): TextSelector( TextSelectorConfig(type="password") ), } return self.async_show_form( step_id="configure", data_schema=vol.Schema(schema_dict), errors=errors, description_placeholders={ "token_label": "Local OpenAI-Compatible URL", # nosec B105 - UI label for config form, not a credential "provider": PROVIDERS[provider], }, ) if provider == "openai": # For OpenAI provider, first step: API Key + Base URL # Model selection happens in the next step after we fetch available models schema_dict = { vol.Required(token_field): TextSelector( TextSelectorConfig(type="password") ), vol.Optional( CONF_OPENAI_BASE_URL, default="https://api.openai.com/v1", ): TextSelector(TextSelectorConfig(type="text")), } return self.async_show_form( step_id="configure", data_schema=vol.Schema(schema_dict), errors=errors, description_placeholders={ "token_label": token_label, "provider": PROVIDERS[provider], }, ) # Build schema for other providers schema_dict = { vol.Required(token_field): TextSelector( TextSelectorConfig(type="password") ), } # Add model selection if available if available_models: # For Gemini, fetch models dynamically if provider == "gemini": token_value = self.config_data.get("gemini_token") model_list = await fetch_gemini_models(token_value) if "Custom..." not in model_list: model_list.insert(0, "Custom...") model_options = model_list else: # Add predefined models + custom option (avoid duplicating "Custom...") if "Custom..." in available_models: model_options = available_models else: model_options = available_models + ["Custom..."] schema_dict[vol.Optional("model", default=dropdown_default)] = ( SelectSelector(SelectSelectorConfig(options=model_options)) ) schema_dict[vol.Optional("custom_model")] = TextSelector( TextSelectorConfig(type="text") ) return self.async_show_form( step_id="configure", data_schema=vol.Schema(schema_dict), errors=errors, description_placeholders={ "token_label": token_label, "provider": PROVIDERS[provider], }, ) async def async_step_configure_openai_models(self, user_input=None): """Handle the OpenAI model selection step with dynamic model list.""" errors = {} provider = "openai" token = self.config_data.get("openai_token") base_url = self.config_data.get( CONF_OPENAI_BASE_URL, "https://api.openai.com/v1" ) # Fetch available models dynamically model_list = await fetch_openai_models(base_url, token) # Ensure "Custom..." is always available if "Custom..." not in model_list: model_list.insert(0, "Custom...") if user_input is not None: try: selected_model = user_input.get("model") custom_model = user_input.get("custom_model") # Initialize models dict if it doesn't exist if "models" not in self.config_data: self.config_data["models"] = {} if custom_model and custom_model.strip(): self.config_data["models"][provider] = custom_model.strip() elif selected_model and selected_model != "Custom...": self.config_data["models"][provider] = selected_model else: self.config_data["models"][provider] = "" return self.async_create_entry( title=f"AI Agent HA ({PROVIDERS[provider]})", data=self.config_data, ) except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception in OpenAI model selection") errors["base"] = "unknown" schema_dict = { vol.Optional("model", default="Custom..."): SelectSelector( SelectSelectorConfig(options=model_list) ), vol.Optional("custom_model"): TextSelector(TextSelectorConfig(type="text")), } return self.async_show_form( step_id="configure_openai_models", data_schema=vol.Schema(schema_dict), errors=errors, description_placeholders={ "provider": PROVIDERS[provider], }, ) async def async_step_configure_openai_compatible_models(self, user_input=None): """Handle the OpenAI-Compatible model selection step with dynamic model list.""" errors = {} provider = "openai_compatible" base_url = self.config_data.get(CONF_OPENAI_COMPATIBLE_URL, "") api_key = self.config_data.get("openai_compatible_api_key") or "" # Fetch available models dynamically if the endpoint supports it model_list = await fetch_openai_compatible_models(base_url, api_key or None) # Ensure "Custom..." is always available if "Custom..." not in model_list: model_list.insert(0, "Custom...") if user_input is not None: try: selected_model = user_input.get("model") custom_model = user_input.get("custom_model") # Initialize models dict if it doesn't exist if "models" not in self.config_data: self.config_data["models"] = {} if custom_model and custom_model.strip(): self.config_data["models"][provider] = custom_model.strip() elif selected_model and selected_model != "Custom...": self.config_data["models"][provider] = selected_model else: self.config_data["models"][provider] = "" return self.async_create_entry( title=f"AI Agent HA ({PROVIDERS[provider]})", data=self.config_data, ) except Exception: # pylint: disable=broad-except _LOGGER.exception( "Unexpected exception in OpenAI-Compatible model selection" ) errors["base"] = "unknown" schema_dict = { vol.Optional("model", default="Custom..."): SelectSelector( SelectSelectorConfig(options=model_list) ), vol.Optional("custom_model"): TextSelector(TextSelectorConfig(type="text")), } return self.async_show_form( step_id="configure_openai_compatible_models", data_schema=vol.Schema(schema_dict), errors=errors, description_placeholders={ "provider": PROVIDERS[provider], }, ) class InvalidApiKey(HomeAssistantError): """Error to indicate there is an invalid API key.""" class AiAgentHaOptionsFlowHandler(config_entries.OptionsFlow): """Handle options flow for AI Agent HA.""" def __init__(self): """Initialize options flow.""" self.options_data = {} async def async_step_init(self, user_input=None): """Handle the initial options step - provider selection.""" current_provider = self.config_entry.data.get("ai_provider", DEFAULT_PROVIDER) if user_input is not None: # Store selected provider and move to configure step self.options_data = { "ai_provider": user_input["ai_provider"], "current_provider": current_provider, } return await self.async_step_configure_options() return self.async_show_form( step_id="init", data_schema=vol.Schema( { vol.Required( "ai_provider", default=current_provider ): SelectSelector( SelectSelectorConfig( options=[ {"value": k, "label": v} for k, v in PROVIDERS.items() ] ) ), } ), description_placeholders={"current_provider": PROVIDERS[current_provider]}, ) async def async_step_configure_options(self, user_input=None): """Handle the configuration step for the selected provider in options.""" errors = {} provider = self.options_data["ai_provider"] current_provider = self.options_data["current_provider"] token_field = TOKEN_FIELD_NAMES[provider] token_label = TOKEN_LABELS[provider] # Get current configuration current_models = self.config_entry.data.get("models", {}) current_model = current_models.get(provider, DEFAULT_MODELS[provider]) # For Alter provider, if model is empty, default to "Custom..." for the dropdown if provider == "alter" and not current_model: current_model = "Custom..." current_token = self.config_entry.data.get(token_field, "") available_models = AVAILABLE_MODELS.get(provider, [DEFAULT_MODELS[provider]]) # Use current token if provider hasn't changed, otherwise empty display_token = current_token if provider == current_provider else "" # Determine if current model is a custom model (not in available models list) # and prepare model dropdown and custom model field defaults model_options = available_models if "Custom..." not in model_options: model_options = model_options + ["Custom..."] # Check if current_model is a custom model (not in the available models) # Remove "Custom..." from the check since it's the selector option, not a real model available_models_without_custom = [ m for m in available_models if m != "Custom..." ] is_custom_model = ( current_model and current_model not in available_models_without_custom and current_model != "Custom..." ) if is_custom_model: # Current model is a custom model - show "Custom..." in dropdown and populate custom field model_default = "Custom..." custom_model_default = current_model else: # Current model is a standard model or empty model_default = current_model if current_model else "Custom..." custom_model_default = "" if user_input is not None: try: token_value = user_input.get(token_field) if not token_value: errors[token_field] = "required" else: # Prepare the updated configuration updated_data = dict(self.config_entry.data) updated_data["ai_provider"] = provider updated_data[token_field] = token_value # Update model configuration selected_model = user_input.get("model") custom_model = user_input.get("custom_model") # For zai, update endpoint type if provider == "zai": endpoint_type = user_input.get("zai_endpoint", "general") updated_data["zai_endpoint"] = endpoint_type # For OpenAI, update Base URL (default to official if blank) if provider == "openai": base_url = (user_input.get(CONF_OPENAI_BASE_URL) or "").strip() updated_data[CONF_OPENAI_BASE_URL] = ( base_url or "https://api.openai.com/v1" ) # For OpenAI-Compatible, update the optional API key if provider == "openai_compatible": updated_data["openai_compatible_api_key"] = ( user_input.get("openai_compatible_api_key") or "" ).strip() # Initialize models dict if it doesn't exist if "models" not in updated_data: updated_data["models"] = {} if custom_model and custom_model.strip(): # Use custom model if provided and not empty updated_data["models"][provider] = custom_model.strip() elif selected_model and selected_model != "Custom...": # Use selected model if it's not the "Custom..." option updated_data["models"][provider] = selected_model else: # For local_ollama, openai_compatible, alter, and zai providers, allow empty model name if provider in ( "local_ollama", "openai_compatible", "alter", "zai", ): updated_data["models"][provider] = "" else: # Ensure we keep the current model or use default for other providers if provider not in updated_data["models"]: updated_data["models"][provider] = DEFAULT_MODELS[ provider ] _LOGGER.debug( f"Options flow - Final model config for {provider}: {updated_data['models'].get(provider)}" ) # Update the config entry self.hass.config_entries.async_update_entry( self.config_entry, data=updated_data ) return self.async_create_entry(title="", data={}) except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception in options flow") errors["base"] = "unknown" # Build schema for the selected provider in options if provider == "zai": current_endpoint = self.config_entry.data.get("zai_endpoint", "general") model_options = AVAILABLE_MODELS.get("zai", ["glm-4.7"]) # Ensure "Custom..." is in model options if "Custom..." not in model_options: model_options = model_options + ["Custom..."] schema_dict = { vol.Required(token_field, default=display_token): TextSelector( TextSelectorConfig(type="password") ), vol.Optional("zai_endpoint", default=current_endpoint): SelectSelector( SelectSelectorConfig( options=[ {"value": "general", "label": "General Purpose"}, {"value": "coding", "label": "Coding (3× usage, 1/7 cost)"}, ] ) ), vol.Optional("model", default=model_default): SelectSelector( SelectSelectorConfig(options=model_options) ), vol.Optional( "custom_model", default=custom_model_default ): TextSelector(TextSelectorConfig(type="text")), } return self.async_show_form( step_id="configure_options", data_schema=vol.Schema(schema_dict), errors=errors, description_placeholders={ "token_label": token_label, "provider": PROVIDERS[provider], }, ) if provider == "local_ollama": # For local_ollama provider, we need both URL and optional model name current_url = self.config_entry.data.get(CONF_LOCAL_OLLAMA_URL, "") schema_dict = { vol.Required(CONF_LOCAL_OLLAMA_URL, default=current_url): TextSelector( TextSelectorConfig(type="text") ), } # Add model selection model_options = AVAILABLE_MODELS.get("local_ollama", ["Custom..."]) # Ensure "Custom..." is in model options if "Custom..." not in model_options: model_options = model_options + ["Custom..."] schema_dict[vol.Optional("model", default=model_default)] = SelectSelector( SelectSelectorConfig(options=model_options) ) schema_dict[vol.Optional("custom_model", default=custom_model_default)] = ( TextSelector(TextSelectorConfig(type="text")) ) return self.async_show_form( step_id="configure_options", data_schema=vol.Schema(schema_dict), errors=errors, description_placeholders={ "token_label": "Local Ollama API URL", # nosec B105 - UI label string shown next to the URL field, not a credential "provider": PROVIDERS[provider], }, ) if provider == "openai_compatible": # For openai_compatible provider, we need URL + optional API key + model current_url = self.config_entry.data.get(CONF_OPENAI_COMPATIBLE_URL, "") current_api_key = ( self.config_entry.data.get("openai_compatible_api_key") or "" ) # Fetch available models dynamically if the endpoint supports it model_list = await fetch_openai_compatible_models( current_url, current_api_key or None ) # Ensure "Custom..." is always available if "Custom..." not in model_list: model_list.insert(0, "Custom...") schema_dict = { vol.Required( CONF_OPENAI_COMPATIBLE_URL, default=current_url ): TextSelector(TextSelectorConfig(type="text")), vol.Optional( "openai_compatible_api_key", default=current_api_key ): TextSelector(TextSelectorConfig(type="password")), } # Add model selection with dynamic list schema_dict[vol.Optional("model", default=model_default)] = SelectSelector( SelectSelectorConfig(options=model_list) ) schema_dict[vol.Optional("custom_model", default=custom_model_default)] = ( TextSelector(TextSelectorConfig(type="text")) ) return self.async_show_form( step_id="configure_options", data_schema=vol.Schema(schema_dict), errors=errors, description_placeholders={ "token_label": "Local OpenAI-Compatible URL", # nosec B105 - UI label for config form, not a credential "provider": PROVIDERS[provider], }, ) if provider == "openai": # For OpenAI provider, we need token and optional Base URL # Pre-fill with official endpoint if not set current_base_url = ( self.config_entry.data.get(CONF_OPENAI_BASE_URL) or "https://api.openai.com/v1" ) # Fetch available models dynamically current_token = self.config_entry.data.get("openai_token", "") model_list = await fetch_openai_models(current_base_url, current_token) # Ensure "Custom..." is always available if "Custom..." not in model_list: model_list.insert(0, "Custom...") schema_dict = { vol.Required(token_field, default=display_token): TextSelector( TextSelectorConfig(type="password") ), vol.Optional( CONF_OPENAI_BASE_URL, default=current_base_url ): TextSelector(TextSelectorConfig(type="text")), } # Add model selection with dynamic list schema_dict[vol.Optional("model", default=model_default)] = SelectSelector( SelectSelectorConfig(options=model_list) ) schema_dict[vol.Optional("custom_model", default=custom_model_default)] = ( TextSelector(TextSelectorConfig(type="text")) ) return self.async_show_form( step_id="configure_options", data_schema=vol.Schema(schema_dict), errors=errors, description_placeholders={ "token_label": token_label, "provider": PROVIDERS[provider], }, ) # Build schema for other providers schema_dict = { vol.Required(token_field, default=display_token): TextSelector( TextSelectorConfig(type="password") ), } # Add model selection if available if available_models: # For Gemini, fetch models dynamically if provider == "gemini": current_token = self.config_entry.data.get("gemini_token", "") model_list = await fetch_gemini_models(current_token) if "Custom..." not in model_list: model_list.insert(0, "Custom...") model_options = model_list # model_options already has "Custom..." added above for other providers schema_dict[vol.Optional("model", default=model_default)] = SelectSelector( SelectSelectorConfig(options=model_options) ) schema_dict[vol.Optional("custom_model", default=custom_model_default)] = ( TextSelector(TextSelectorConfig(type="text")) ) return self.async_show_form( step_id="configure_options", data_schema=vol.Schema(schema_dict), errors=errors, description_placeholders={ "token_label": token_label, "provider": PROVIDERS[provider], }, )