mirror of
https://github.com/smkrv/ha-text-ai.git
synced 2026-07-22 07:03:58 +08:00
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:
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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": [],
|
||||
|
||||
Reference in New Issue
Block a user