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
This commit is contained in:
SMKRV
2024-11-20 12:07:30 +03:00
parent 8f796ad9dc
commit 58a7ae4229
4 changed files with 108 additions and 55 deletions
+61 -23
View File
@@ -86,38 +86,69 @@ async def validate_api_connection(
) -> Tuple[bool, str, list]: ) -> Tuple[bool, str, list]:
"""Validate API connection with retry logic.""" """Validate API connection with retry logic."""
session = async_get_clientsession(hass) session = async_get_clientsession(hass)
# Determine API type and configure headers
headers = { headers = {
"Authorization": f"Bearer {api_key}", "Content-Type": "application/json",
"Content-Type": "application/json; charset=utf-8", "Accept": "application/json"
"Accept": "application/json; charset=utf-8",
"Accept-Charset": "utf-8"
} }
base_url = endpoint.rstrip('/') # Configure endpoint and headers based on service type
if not base_url.endswith(f"/{API_VERSION}"): if "vsegpt" in endpoint.lower():
base_url = f"{base_url}/{API_VERSION}" headers["Authorization"] = f"Bearer {api_key}"
base_url = endpoint.rstrip('/')
models_url = f"{base_url}/{API_MODELS_PATH}" models_url = f"{base_url}/models"
_LOGGER.debug("Attempting to connect to: %s", models_url) _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): for attempt in range(retry_count):
try: try:
async with timeout(10): async with timeout(10):
async with session.get(models_url, headers=headers) as response: async with session.get(models_url, headers=headers) as response:
_LOGGER.debug("API response status: %s", response.status)
if response.status == 200: if response.status == 200:
data = await response.json() 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: elif any(m in model.lower() for m in ["claude", "anthropic"]):
_LOGGER.warning( model_ids = [m["id"] for m in data.get("models", [])]
"Model %s not found in available models: %s", # Support for custom Anthropic model names
model, if model.startswith("anthropic/"):
", ".join(model_ids) model = model.split("/")[1]
) if model in model_ids or any(m.endswith(model) for m in model_ids):
return False, ERROR_INVALID_MODEL, model_ids return True, "", 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: elif response.status == 401:
_LOGGER.error("Authentication failed") _LOGGER.error("Authentication failed")
@@ -125,6 +156,9 @@ async def validate_api_connection(
elif response.status == 429: elif response.status == 429:
_LOGGER.error("Rate limit exceeded") _LOGGER.error("Rate limit exceeded")
if attempt < retry_count - 1:
await asyncio.sleep(retry_delay * (2 ** attempt))
continue
return False, ERROR_RATE_LIMIT, [] return False, ERROR_RATE_LIMIT, []
else: else:
@@ -134,6 +168,9 @@ async def validate_api_connection(
response.status, response.status,
response_text response_text
) )
if attempt < retry_count - 1:
await asyncio.sleep(retry_delay)
continue
return False, ERROR_API_ERROR, [] return False, ERROR_API_ERROR, []
except asyncio.TimeoutError: except asyncio.TimeoutError:
@@ -142,9 +179,10 @@ async def validate_api_connection(
attempt + 1, attempt + 1,
retry_count retry_count
) )
if attempt == retry_count - 1: if attempt < retry_count - 1:
return False, ERROR_TIMEOUT, [] await asyncio.sleep(retry_delay)
await asyncio.sleep(retry_delay) continue
return False, ERROR_TIMEOUT, []
except aiohttp.ClientError as err: except aiohttp.ClientError as err:
_LOGGER.error("Connection error: %s", str(err)) _LOGGER.error("Connection error: %s", str(err))
+11 -9
View File
@@ -15,17 +15,19 @@ CONF_REQUEST_INTERVAL: Final = "request_interval"
# Model constants # Model constants
SUPPORTED_MODELS: Final = [ SUPPORTED_MODELS: Final = [
"gpt-3.5-turbo", "o1-preview",
"gpt-3.5-turbo-16k", "o1-mini",
"gpt-4", "gpt-4o-mini",
"gpt-4-32k", "gpt-4o",
"gpt-4-1106-preview", "claude-3-5-haiku",
"claude-3-sonnet", "claude-3.5-sonnet",
"claude-3-opus" "claude-3-haiku",
"anthropic/claude-3-5-haiku",
"anthropic/claude-3.5-sonnet",
] ]
# Default values # Default values
DEFAULT_MODEL: Final = "gpt-3.5-turbo" DEFAULT_MODEL: Final = "gpt-4o-mini"
DEFAULT_TEMPERATURE: Final = 0.1 DEFAULT_TEMPERATURE: Final = 0.1
DEFAULT_MAX_TOKENS: Final = 1000 DEFAULT_MAX_TOKENS: Final = 1000
DEFAULT_API_ENDPOINT: Final = "https://api.openai.com" DEFAULT_API_ENDPOINT: Final = "https://api.openai.com"
@@ -170,7 +172,7 @@ LOG_LEVEL_DEFAULT: Final = "INFO"
QUEUE_TIMEOUT: Final = 5 QUEUE_TIMEOUT: Final = 5
QUEUE_MAX_SIZE: Final = 100 QUEUE_MAX_SIZE: Final = 100
# Retry constants # Retry constants
MAX_RETRIES: Final = 3 MAX_RETRIES: Final = 3
RETRY_DELAY: Final = 1.0 RETRY_DELAY: Final = 1.0
+34 -22
View File
@@ -4,6 +4,9 @@ import logging
from datetime import timedelta from datetime import timedelta
from typing import Any, Dict, Optional, List from typing import Any, Dict, Optional, List
import time import time
import ssl
import certifi
import aiohttp
from openai import AsyncOpenAI, APIError, AuthenticationError, RateLimitError from openai import AsyncOpenAI, APIError, AuthenticationError, RateLimitError
from anthropic import AsyncAnthropic from anthropic import AsyncAnthropic
@@ -38,19 +41,7 @@ class HATextAICoordinator(DataUpdateCoordinator):
session: Optional[Any] = None, session: Optional[Any] = None,
is_anthropic: bool = False, is_anthropic: bool = False,
) -> None: ) -> None:
"""Initialize coordinator. """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
"""
super().__init__( super().__init__(
hass, hass,
_LOGGER, _LOGGER,
@@ -86,15 +77,36 @@ class HATextAICoordinator(DataUpdateCoordinator):
} }
self._last_request_time = 0 self._last_request_time = 0
self._is_anthropic = is_anthropic self._is_anthropic = is_anthropic
self._session = session or aiohttp_client.async_get_clientsession(hass)
if is_anthropic: self._init_client()
self.client = AsyncAnthropic(api_key=self.api_key)
else: async def _create_ssl_context(self):
self.client = AsyncOpenAI( """Create an async SSL context."""
api_key=self.api_key, ssl_context = ssl.create_default_context(cafile=certifi.where())
base_url=self.endpoint, return ssl_context
http_client=session,
) 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: def _validate_params(self, api_key: str, temperature: float, max_tokens: int) -> None:
"""Validate initialization parameters.""" """Validate initialization parameters."""
@@ -600,7 +612,7 @@ class HATextAICoordinator(DataUpdateCoordinator):
return return
seen_questions = set() 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(): while not self._question_queue.empty():
try: try:
+2 -1
View File
@@ -17,7 +17,8 @@
"openai>=1.12.0", "openai>=1.12.0",
"anthropic>=0.8.0", "anthropic>=0.8.0",
"aiohttp>=3.8.0", "aiohttp>=3.8.0",
"async-timeout>=4.0.0" "async-timeout>=4.0.0",
"certifi>=2024.2.2"
], ],
"ssdp": [], "ssdp": [],
"usb": [], "usb": [],