From 58a7ae42297edfcff0f3414b9f96ec6760728a33 Mon Sep 17 00:00:00 2001 From: SMKRV Date: Wed, 20 Nov 2024 12:07:30 +0300 Subject: [PATCH] Resoved blocking SSL verification issue in coordinator.py API endpoint handling in config_flow.py changes Added support for the custom models in const.py Requirements in manifest.json updated --- custom_components/ha_text_ai/config_flow.py | 84 +++++++++++++++------ custom_components/ha_text_ai/const.py | 20 ++--- custom_components/ha_text_ai/coordinator.py | 56 ++++++++------ custom_components/ha_text_ai/manifest.json | 3 +- 4 files changed, 108 insertions(+), 55 deletions(-) diff --git a/custom_components/ha_text_ai/config_flow.py b/custom_components/ha_text_ai/config_flow.py index 5a5266c..3bf448f 100644 --- a/custom_components/ha_text_ai/config_flow.py +++ b/custom_components/ha_text_ai/config_flow.py @@ -86,38 +86,69 @@ async def validate_api_connection( ) -> Tuple[bool, str, list]: """Validate API connection with retry logic.""" session = async_get_clientsession(hass) + + # Determine API type and configure headers headers = { - "Authorization": f"Bearer {api_key}", - "Content-Type": "application/json; charset=utf-8", - "Accept": "application/json; charset=utf-8", - "Accept-Charset": "utf-8" + "Content-Type": "application/json", + "Accept": "application/json" } - base_url = endpoint.rstrip('/') - if not base_url.endswith(f"/{API_VERSION}"): - base_url = f"{base_url}/{API_VERSION}" - - models_url = f"{base_url}/{API_MODELS_PATH}" - _LOGGER.debug("Attempting to connect to: %s", models_url) + # Configure endpoint and headers based on service type + if "vsegpt" in endpoint.lower(): + headers["Authorization"] = f"Bearer {api_key}" + base_url = endpoint.rstrip('/') + models_url = f"{base_url}/models" + _LOGGER.debug("Using VSE GPT endpoint: %s", models_url) + elif any(m in model.lower() for m in ["claude", "anthropic"]): + headers["x-api-key"] = api_key + headers["anthropic-version"] = "2023-06-01" + base_url = endpoint.rstrip('/') + if not base_url.endswith("/v1"): + base_url = f"{base_url}/v1" + models_url = f"{base_url}/models" + _LOGGER.debug("Using Anthropic endpoint: %s", models_url) + else: + headers["Authorization"] = f"Bearer {api_key}" + base_url = endpoint.rstrip('/') + if not base_url.endswith(f"/{API_VERSION}"): + base_url = f"{base_url}/{API_VERSION}" + models_url = f"{base_url}/{API_MODELS_PATH}" + _LOGGER.debug("Using OpenAI endpoint: %s", models_url) for attempt in range(retry_count): try: async with timeout(10): async with session.get(models_url, headers=headers) as response: + _LOGGER.debug("API response status: %s", response.status) + if response.status == 200: data = await response.json() - model_ids = [m["id"] for m in data.get("data", [])] - _LOGGER.debug("Available models: %s", ", ".join(model_ids)) + # Handle different API response formats + if "vsegpt" in endpoint.lower(): + model_ids = [m["id"] for m in data.get("data", [])] + # Add VSE GPT specific model handling if needed + return True, "", model_ids - if model not in model_ids: - _LOGGER.warning( - "Model %s not found in available models: %s", - model, - ", ".join(model_ids) - ) - return False, ERROR_INVALID_MODEL, model_ids - return True, "", model_ids + elif any(m in model.lower() for m in ["claude", "anthropic"]): + model_ids = [m["id"] for m in data.get("models", [])] + # Support for custom Anthropic model names + if model.startswith("anthropic/"): + model = model.split("/")[1] + if model in model_ids or any(m.endswith(model) for m in model_ids): + return True, "", model_ids + + else: # OpenAI format + model_ids = [m["id"] for m in data.get("data", [])] + if model in model_ids: + return True, "", model_ids + + _LOGGER.warning( + "Model %s not found in available models: %s", + model, + ", ".join(model_ids) + ) + return False, ERROR_INVALID_MODEL, model_ids elif response.status == 401: _LOGGER.error("Authentication failed") @@ -125,6 +156,9 @@ async def validate_api_connection( elif response.status == 429: _LOGGER.error("Rate limit exceeded") + if attempt < retry_count - 1: + await asyncio.sleep(retry_delay * (2 ** attempt)) + continue return False, ERROR_RATE_LIMIT, [] else: @@ -134,6 +168,9 @@ async def validate_api_connection( response.status, response_text ) + if attempt < retry_count - 1: + await asyncio.sleep(retry_delay) + continue return False, ERROR_API_ERROR, [] except asyncio.TimeoutError: @@ -142,9 +179,10 @@ async def validate_api_connection( attempt + 1, retry_count ) - if attempt == retry_count - 1: - return False, ERROR_TIMEOUT, [] - await asyncio.sleep(retry_delay) + if attempt < retry_count - 1: + await asyncio.sleep(retry_delay) + continue + return False, ERROR_TIMEOUT, [] except aiohttp.ClientError as err: _LOGGER.error("Connection error: %s", str(err)) diff --git a/custom_components/ha_text_ai/const.py b/custom_components/ha_text_ai/const.py index cebbc4c..de2580a 100644 --- a/custom_components/ha_text_ai/const.py +++ b/custom_components/ha_text_ai/const.py @@ -15,17 +15,19 @@ CONF_REQUEST_INTERVAL: Final = "request_interval" # Model constants SUPPORTED_MODELS: Final = [ - "gpt-3.5-turbo", - "gpt-3.5-turbo-16k", - "gpt-4", - "gpt-4-32k", - "gpt-4-1106-preview", - "claude-3-sonnet", - "claude-3-opus" + "o1-preview", + "o1-mini", + "gpt-4o-mini", + "gpt-4o", + "claude-3-5-haiku", + "claude-3.5-sonnet", + "claude-3-haiku", + "anthropic/claude-3-5-haiku", + "anthropic/claude-3.5-sonnet", ] # Default values -DEFAULT_MODEL: Final = "gpt-3.5-turbo" +DEFAULT_MODEL: Final = "gpt-4o-mini" DEFAULT_TEMPERATURE: Final = 0.1 DEFAULT_MAX_TOKENS: Final = 1000 DEFAULT_API_ENDPOINT: Final = "https://api.openai.com" @@ -170,7 +172,7 @@ LOG_LEVEL_DEFAULT: Final = "INFO" QUEUE_TIMEOUT: Final = 5 QUEUE_MAX_SIZE: Final = 100 -# Retry constants +# Retry constants MAX_RETRIES: Final = 3 RETRY_DELAY: Final = 1.0 diff --git a/custom_components/ha_text_ai/coordinator.py b/custom_components/ha_text_ai/coordinator.py index d79e829..201e2a3 100644 --- a/custom_components/ha_text_ai/coordinator.py +++ b/custom_components/ha_text_ai/coordinator.py @@ -4,6 +4,9 @@ import logging from datetime import timedelta from typing import Any, Dict, Optional, List import time +import ssl +import certifi +import aiohttp from openai import AsyncOpenAI, APIError, AuthenticationError, RateLimitError from anthropic import AsyncAnthropic @@ -38,19 +41,7 @@ class HATextAICoordinator(DataUpdateCoordinator): session: Optional[Any] = None, is_anthropic: bool = False, ) -> None: - """Initialize coordinator. - - Args: - hass: HomeAssistant instance - api_key: API key for the service - endpoint: API endpoint URL - model: Model name to use - temperature: Temperature parameter for generation - max_tokens: Maximum tokens to generate - request_interval: Interval between requests - session: Optional session object - is_anthropic: Whether to use Anthropic API - """ + """Initialize coordinator.""" super().__init__( hass, _LOGGER, @@ -86,15 +77,36 @@ class HATextAICoordinator(DataUpdateCoordinator): } self._last_request_time = 0 self._is_anthropic = is_anthropic + self._session = session or aiohttp_client.async_get_clientsession(hass) - if is_anthropic: - self.client = AsyncAnthropic(api_key=self.api_key) - else: - self.client = AsyncOpenAI( - api_key=self.api_key, - base_url=self.endpoint, - http_client=session, - ) + self._init_client() + + async def _create_ssl_context(self): + """Create an async SSL context.""" + ssl_context = ssl.create_default_context(cafile=certifi.where()) + return ssl_context + + async def _init_client(self): + """Initialize API client with proper SSL context.""" + try: + ssl_context = await self._create_ssl_context() + connector = aiohttp.TCPConnector(ssl=ssl_context) + client_session = aiohttp.ClientSession(connector=connector) + + if self._is_anthropic: + self.client = AsyncAnthropic( + api_key=self.api_key, + http_client=client_session + ) + else: + self.client = AsyncOpenAI( + api_key=self.api_key, + base_url=self.endpoint, + http_client=client_session, + ) + except Exception as e: + _LOGGER.error("Error initializing API client: %s", str(e)) + raise def _validate_params(self, api_key: str, temperature: float, max_tokens: int) -> None: """Validate initialization parameters.""" @@ -600,7 +612,7 @@ class HATextAICoordinator(DataUpdateCoordinator): return seen_questions = set() - optimized_queue = asyncio.PriorityQueue(maxsize=QUEUE_MAX_SIZE) + optimized_queue = asyncio.PriorityQueue(maxsize=QUEUE_MAX_SIZE) while not self._question_queue.empty(): try: diff --git a/custom_components/ha_text_ai/manifest.json b/custom_components/ha_text_ai/manifest.json index 71ae378..589c02d 100644 --- a/custom_components/ha_text_ai/manifest.json +++ b/custom_components/ha_text_ai/manifest.json @@ -17,7 +17,8 @@ "openai>=1.12.0", "anthropic>=0.8.0", "aiohttp>=3.8.0", - "async-timeout>=4.0.0" + "async-timeout>=4.0.0", + "certifi>=2024.2.2" ], "ssdp": [], "usb": [],