mirror of
https://github.com/smkrv/ha-text-ai.git
synced 2026-07-21 22:54:00 +08:00
fix: Pinned session TypeError, dead default models, get_history compat, i18n sync
- create_pinned_session built directly on aiohttp: the HA helper injects its own connector and raised TypeError on every setup and config flow - session lifecycle: closed in APIClient.shutdown, on failed setup, and in config flow validators; allow_redirects=False everywhere so 3xx cannot route past the pinned resolver - defaults: gemini-2.0-flash (shut down 2026-06-01) -> gemini-3.5-flash, deepseek-chat (retires 2026-07-24) -> deepseek-v4-flash - DeepSeek V4 disable_thinking via thinking request parameter - Gemini 3.x disable_thinking via thinking_level (MINIMAL, LOW for Pro) - get_history limit: no forced default/max in schema, storage layer clamps - translations: 6 locales caught up with strings.json, 2 orphan keys dropped - README: manual install path matches zip layout, model sections updated
This commit is contained in:
@@ -142,12 +142,16 @@ Transform your smart home experience with powerful AI assistance powered by mult
|
||||
- **Claude Haiku 4.5** - The fastest and most economical option in the series
|
||||
|
||||
#### DeepSeek Models
|
||||
- **DeepSeek-V3** - A general-purpose model for a wide range of tasks
|
||||
- **DeepSeek-R1** - A specialized model focused on reasoning and coding
|
||||
- **deepseek-v4-flash** - A fast general-purpose model for a wide range of tasks (default)
|
||||
- **deepseek-v4-pro** - A more capable model for reasoning and coding
|
||||
|
||||
> The legacy model names `deepseek-chat` and `deepseek-reasoner` stop working on 2026-07-24. If your instance still uses one of them, switch the model in the integration options.
|
||||
|
||||
#### Google Gemini Models
|
||||
- **Gemini 3.1 Pro** - The newest and most advanced model available
|
||||
- **Gemini 3.1 Flash Lite** - Fastest and most cost-efficient model for high-volume workloads
|
||||
- **gemini-3.5-flash** - Fast and cost-efficient, suitable for most tasks (default)
|
||||
- **Gemini 3.1 Pro** - The most advanced Gemini model available
|
||||
|
||||
> Google shut down `gemini-2.0-flash` on 2026-06-01 and retires the 2.5 family on 2026-10-16. If your instance uses one of those, switch the model in the integration options.
|
||||
|
||||
<details>
|
||||
<summary>🌐 Potentially Compatible Providers</summary>
|
||||
@@ -199,8 +203,8 @@ If the integration is not found in the default repository:
|
||||
5. Click "Download"
|
||||
|
||||
### Manual Installation
|
||||
1. Download the latest release
|
||||
2. Extract and copy `custom_components/ha_text_ai` to your `custom_components` directory
|
||||
1. Download `ha_text_ai.zip` from the latest release
|
||||
2. Extract the archive and copy the `ha_text_ai` folder into your `custom_components` directory
|
||||
3. Restart Home Assistant
|
||||
4. Add configuration via UI (Settings → Devices & Services → Add Integration)
|
||||
|
||||
|
||||
@@ -80,8 +80,11 @@ SERVICE_SCHEMA_SET_SYSTEM_PROMPT = vol.Schema({
|
||||
|
||||
SERVICE_SCHEMA_GET_HISTORY = vol.Schema({
|
||||
vol.Required("instance"): cv.string,
|
||||
# Cap limit to protect automations from huge response payloads.
|
||||
vol.Optional("limit", default=10): vol.All(cv.positive_int, vol.Range(min=1, max=100)),
|
||||
# No default and no schema max: omitting limit returns the full history
|
||||
# (pre-2.5.0 behavior) and oversized values are clamped to
|
||||
# ABSOLUTE_MAX_HISTORY_SIZE in history.async_get_history instead of
|
||||
# failing the whole service call.
|
||||
vol.Optional("limit"): vol.All(cv.positive_int, vol.Range(min=1)),
|
||||
vol.Optional("filter_model"): cv.string,
|
||||
vol.Optional("start_date"): cv.string,
|
||||
vol.Optional("include_metadata"): cv.boolean,
|
||||
@@ -244,7 +247,9 @@ async def async_check_api(session, endpoint: str, headers: dict, provider: str,
|
||||
check_url = f"{endpoint}{check_path}"
|
||||
|
||||
async with asyncio.timeout(api_timeout):
|
||||
async with session.get(check_url, headers=headers) as response:
|
||||
async with session.get(
|
||||
check_url, headers=headers, allow_redirects=False
|
||||
) as response:
|
||||
if response.status == 200:
|
||||
return True
|
||||
elif response.status == 401:
|
||||
@@ -264,6 +269,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up HA Text AI from a config entry."""
|
||||
_LOGGER.debug("Setting up HA Text AI entry: %s", safe_log_data(dict(entry.data)))
|
||||
|
||||
session = None
|
||||
try:
|
||||
# Get provider from data or options (options takes precedence)
|
||||
config = {**entry.data, **entry.options}
|
||||
@@ -292,7 +298,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
# Pinned session closes DNS-rebinding TOCTOU and isolates cookies
|
||||
# from other integrations sharing the same endpoint hostname.
|
||||
session = create_pinned_session(hass, endpoint, resolved_ips)
|
||||
# The integration owns this session: APIClient.shutdown() closes it
|
||||
# on unload, the except handler below closes it on failed setup.
|
||||
session = create_pinned_session(endpoint, resolved_ips)
|
||||
# API key can now be updated via options
|
||||
api_key = config.get(CONF_API_KEY, entry.data.get(CONF_API_KEY))
|
||||
instance_name = entry.data.get(CONF_NAME, entry.entry_id)
|
||||
@@ -359,6 +367,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
except Exception as err:
|
||||
_LOGGER.exception("Error setting up HA Text AI: %s", err)
|
||||
if session is not None and not session.closed:
|
||||
await session.close()
|
||||
raise
|
||||
|
||||
async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
|
||||
@@ -113,6 +113,9 @@ class APIClient:
|
||||
json=payload,
|
||||
headers=self.headers,
|
||||
timeout=self.timeout,
|
||||
# The session pins DNS to validated IPs; following a
|
||||
# redirect would resolve a new host past that pin.
|
||||
allow_redirects=False,
|
||||
) as response:
|
||||
_LOGGER.debug("Response status: %s", response.status)
|
||||
if response.status == 200:
|
||||
@@ -358,12 +361,18 @@ class APIClient:
|
||||
separate field alongside content. We skip the no_think append for
|
||||
this model and preserve reasoning_content in the response payload
|
||||
so it's available for logging/debug.
|
||||
|
||||
DeepSeek V4+ (deepseek-v4-flash/-pro) selects thinking mode via a
|
||||
top-level "thinking" request parameter instead of the model name,
|
||||
so /no_think does not apply there.
|
||||
"""
|
||||
url = f"{self.endpoint}/chat/completions"
|
||||
is_reasoner = "reasoner" in model.lower()
|
||||
m_lower = model.lower()
|
||||
is_reasoner = "reasoner" in m_lower
|
||||
is_v4plus = re.search(r"deepseek-v[4-9]", m_lower) is not None
|
||||
final_messages = (
|
||||
self._apply_no_think_tag(messages)
|
||||
if (disable_thinking and not is_reasoner)
|
||||
if (disable_thinking and not is_reasoner and not is_v4plus)
|
||||
else messages
|
||||
)
|
||||
payload = {
|
||||
@@ -373,6 +382,8 @@ class APIClient:
|
||||
"max_tokens": max_tokens,
|
||||
"stream": False,
|
||||
}
|
||||
if disable_thinking and is_v4plus:
|
||||
payload["thinking"] = {"type": "disabled"}
|
||||
self._apply_structured_output(payload, structured_output, json_schema)
|
||||
|
||||
data = await self._make_request(url, payload)
|
||||
@@ -627,15 +638,26 @@ class APIClient:
|
||||
config.response_mime_type = "application/json"
|
||||
config.response_schema = parsed_schema
|
||||
|
||||
# Disable thinking for Gemini 2.5+ models. Flash variants accept
|
||||
# thinking_budget=0 (fully off); Pro variants reject 0 and require
|
||||
# at least 128 tokens of thinking. 2.0 and earlier ignore the field.
|
||||
# Disable thinking. Gemini 3.x+ replaced the numeric
|
||||
# thinking_budget with a semantic thinking_level; Pro
|
||||
# variants do not accept MINIMAL, their floor is LOW.
|
||||
# Gemini 2.5: Flash accepts thinking_budget=0 (fully off),
|
||||
# Pro rejects 0 and requires at least 128 tokens.
|
||||
# 2.0 and earlier ignore the field.
|
||||
if disable_thinking:
|
||||
m_lower = model.lower()
|
||||
budget = 128 if "2.5-pro" in m_lower else 0
|
||||
try:
|
||||
config.thinking_config = types.ThinkingConfig(thinking_budget=budget)
|
||||
except (AttributeError, TypeError) as err:
|
||||
if re.search(r"gemini-[3-9]", m_lower):
|
||||
level = "LOW" if "pro" in m_lower else "MINIMAL"
|
||||
config.thinking_config = types.ThinkingConfig(
|
||||
thinking_level=level
|
||||
)
|
||||
else:
|
||||
budget = 128 if "2.5-pro" in m_lower else 0
|
||||
config.thinking_config = types.ThinkingConfig(
|
||||
thinking_budget=budget
|
||||
)
|
||||
except (AttributeError, TypeError, ValueError) as err:
|
||||
_LOGGER.debug(
|
||||
"ThinkingConfig not supported by this google-genai version: %s", err
|
||||
)
|
||||
@@ -735,7 +757,11 @@ class APIClient:
|
||||
raise HomeAssistantError(f"Gemini API request failed: {e}") from e
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
"""Shutdown API client."""
|
||||
"""Shutdown API client and close its dedicated session."""
|
||||
_LOGGER.debug("Shutting down API client")
|
||||
self._closed = True
|
||||
# Do NOT close the shared Home Assistant session
|
||||
# The session is dedicated to this config entry (pinned resolver,
|
||||
# isolated cookie jar), so it must be closed here to release the
|
||||
# connector; nothing else owns it.
|
||||
if self.session is not None and not self.session.closed:
|
||||
await self.session.close()
|
||||
|
||||
@@ -255,23 +255,28 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
return False
|
||||
return True
|
||||
|
||||
# Pinned session ensures the reachability check goes to the same
|
||||
# IP that will later be used by api_client (no DNS rebinding).
|
||||
session = create_pinned_session(self.hass, endpoint, resolved_ips)
|
||||
headers = build_auth_headers(self._provider, user_input[CONF_API_KEY])
|
||||
|
||||
from .providers import get_provider_config
|
||||
check_path = get_provider_config(self._provider).get("check_path", "/models")
|
||||
check_url = f"{endpoint}{check_path}"
|
||||
|
||||
async with session.get(check_url, headers=headers) as response:
|
||||
if response.status == 401:
|
||||
self._errors["base"] = "invalid_auth"
|
||||
return False
|
||||
elif response.status != 200:
|
||||
self._errors["base"] = "cannot_connect"
|
||||
return False
|
||||
return True
|
||||
# Pinned session ensures the reachability check goes to the same
|
||||
# IP that will later be used by api_client (no DNS rebinding).
|
||||
session = create_pinned_session(endpoint, resolved_ips)
|
||||
try:
|
||||
async with session.get(
|
||||
check_url, headers=headers, allow_redirects=False
|
||||
) as response:
|
||||
if response.status == 401:
|
||||
self._errors["base"] = "invalid_auth"
|
||||
return False
|
||||
elif response.status != 200:
|
||||
self._errors["base"] = "cannot_connect"
|
||||
return False
|
||||
return True
|
||||
finally:
|
||||
await session.close()
|
||||
|
||||
except Exception as err:
|
||||
_LOGGER.error("API validation error: %s", str(err))
|
||||
@@ -340,21 +345,26 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
if provider == API_PROVIDER_GEMINI:
|
||||
return True
|
||||
|
||||
session = create_pinned_session(self.hass, endpoint, resolved_ips)
|
||||
headers = build_auth_headers(provider, api_key)
|
||||
|
||||
from .providers import get_provider_config
|
||||
check_path = get_provider_config(provider).get("check_path", "/models")
|
||||
check_url = f"{endpoint}{check_path}"
|
||||
|
||||
async with session.get(check_url, headers=headers) as response:
|
||||
if response.status == 401:
|
||||
self._errors["base"] = "invalid_auth"
|
||||
return False
|
||||
elif response.status != 200:
|
||||
self._errors["base"] = "cannot_connect"
|
||||
return False
|
||||
return True
|
||||
session = create_pinned_session(endpoint, resolved_ips)
|
||||
try:
|
||||
async with session.get(
|
||||
check_url, headers=headers, allow_redirects=False
|
||||
) as response:
|
||||
if response.status == 401:
|
||||
self._errors["base"] = "invalid_auth"
|
||||
return False
|
||||
elif response.status != 200:
|
||||
self._errors["base"] = "cannot_connect"
|
||||
return False
|
||||
return True
|
||||
finally:
|
||||
await session.close()
|
||||
|
||||
except Exception as err:
|
||||
_LOGGER.error("API validation error: %s", str(err))
|
||||
|
||||
@@ -58,8 +58,11 @@ MAX_HISTORY_FILE_SIZE = 1 * 1024 * 1024
|
||||
# Default values
|
||||
DEFAULT_MODEL: Final = "gpt-4o-mini"
|
||||
DEFAULT_ANTHROPIC_MODEL: Final = "claude-sonnet-4-6"
|
||||
DEFAULT_DEEPSEEK_MODEL: Final = "deepseek-chat"
|
||||
DEFAULT_GEMINI_MODEL: Final = "gemini-2.0-flash"
|
||||
# deepseek-chat/deepseek-reasoner are discontinued 2026-07-24; V4 models
|
||||
# select thinking mode via a request parameter instead of the model name.
|
||||
DEFAULT_DEEPSEEK_MODEL: Final = "deepseek-v4-flash"
|
||||
# gemini-2.0-flash was shut down 2026-06-01; 2.5-flash follows 2026-10-16.
|
||||
DEFAULT_GEMINI_MODEL: Final = "gemini-3.5-flash"
|
||||
DEFAULT_TEMPERATURE: Final = 0.1
|
||||
DEFAULT_MAX_TOKENS: Final = 1000
|
||||
DEFAULT_REQUEST_INTERVAL: Final = 1.0
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"name_exists": "Eine Instanz mit diesem Namen existiert bereits",
|
||||
"invalid_name": "Ungültiger Instanzname",
|
||||
"invalid_auth": "Authentifizierung fehlgeschlagen - überprüfen Sie Ihren API-Schlüssel",
|
||||
"api_key_required": "API-Schlüssel ist erforderlich, wenn Anbieter oder Endpunkt geändert wird",
|
||||
"invalid_api_key": "Ungültiger API-Schlüssel - bitte überprüfen Sie Ihre Anmeldeinformationen",
|
||||
"cannot_connect": "Verbindung zum API-Dienst fehlgeschlagen",
|
||||
"invalid_model": "Ausgewähltes Modell ist nicht verfügbar",
|
||||
@@ -59,7 +60,6 @@
|
||||
"invalid_instance": "Ungültige Instanz angegeben",
|
||||
"unknown": "Unerwarteter Fehler aufgetreten",
|
||||
"empty": "Name darf nicht leer sein",
|
||||
"invalid_characters": "Name darf nur Buchstaben, Zahlen, Leerzeichen, Unterstriche und Bindestriche enthalten",
|
||||
"name_too_long": "Name darf höchstens 50 Zeichen lang sein"
|
||||
},
|
||||
"abort": {
|
||||
@@ -218,8 +218,7 @@
|
||||
"rate_limited": "Rate limitiert",
|
||||
"maintenance": "Wartung",
|
||||
"initializing": "Initialisierung",
|
||||
"retrying": "Wiederholen",
|
||||
"queued": "In der Warteschlange"
|
||||
"retrying": "Wiederholen"
|
||||
},
|
||||
"state_attributes": {
|
||||
"question": {
|
||||
@@ -311,6 +310,21 @@
|
||||
},
|
||||
"min_latency": {
|
||||
"name": "Minimale Latenz"
|
||||
},
|
||||
"last_model": {
|
||||
"name": "Zuletzt verwendetes Modell"
|
||||
},
|
||||
"last_timestamp": {
|
||||
"name": "Zeitpunkt der letzten Antwort"
|
||||
},
|
||||
"instance_name": {
|
||||
"name": "Instanzname"
|
||||
},
|
||||
"normalized_name": {
|
||||
"name": "Normalisierter Name"
|
||||
},
|
||||
"conversation_history": {
|
||||
"name": "Konversationsverlauf"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"name_exists": "Ya existe una instancia con este nombre",
|
||||
"invalid_name": "Nombre de instancia no válido",
|
||||
"invalid_auth": "La autenticación falló - verifica tu clave API",
|
||||
"api_key_required": "Se requiere la clave API al cambiar de proveedor o endpoint",
|
||||
"invalid_api_key": "Clave API no válida - verifica tus credenciales",
|
||||
"cannot_connect": "Error al conectar con el servicio de API",
|
||||
"invalid_model": "El modelo seleccionado no está disponible",
|
||||
@@ -59,7 +60,6 @@
|
||||
"invalid_instance": "Instancia no válida especificada",
|
||||
"unknown": "Ocurrió un error inesperado",
|
||||
"empty": "El nombre no puede estar vacío",
|
||||
"invalid_characters": "El nombre solo puede contener letras, números, espacios, guiones bajos y guiones",
|
||||
"name_too_long": "El nombre debe tener 50 caracteres o menos"
|
||||
},
|
||||
"abort": {
|
||||
@@ -218,8 +218,7 @@
|
||||
"rate_limited": "Limitado por tasa",
|
||||
"maintenance": "Mantenimiento",
|
||||
"initializing": "Inicializando",
|
||||
"retrying": "Reintentando",
|
||||
"queued": "En cola"
|
||||
"retrying": "Reintentando"
|
||||
},
|
||||
"state_attributes": {
|
||||
"question": {
|
||||
@@ -311,6 +310,21 @@
|
||||
},
|
||||
"min_latency": {
|
||||
"name": "Latencia Mínima"
|
||||
},
|
||||
"last_model": {
|
||||
"name": "Último modelo utilizado"
|
||||
},
|
||||
"last_timestamp": {
|
||||
"name": "Hora de la última respuesta"
|
||||
},
|
||||
"instance_name": {
|
||||
"name": "Nombre de instancia"
|
||||
},
|
||||
"normalized_name": {
|
||||
"name": "Nombre normalizado"
|
||||
},
|
||||
"conversation_history": {
|
||||
"name": "Historial de conversación"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"name_exists": "इस नाम के साथ एक उदाहरण पहले से मौजूद है",
|
||||
"invalid_name": "अमान्य उदाहरण नाम",
|
||||
"invalid_auth": "प्रमाणीकरण विफल - अपनी एपीआई कुंजी की जांच करें",
|
||||
"api_key_required": "प्रदाता या endpoint बदलते समय API कुंजी आवश्यक है",
|
||||
"invalid_api_key": "अमान्य एपीआई कुंजी - कृपया अपनी क्रेडेंशियल्स की पुष्टि करें",
|
||||
"cannot_connect": "एपीआई सेवा से कनेक्ट करने में विफल",
|
||||
"invalid_model": "चुना हुआ मॉडल उपलब्ध नहीं है",
|
||||
@@ -59,7 +60,6 @@
|
||||
"invalid_instance": "अमान्य उदाहरण निर्दिष्ट किया गया",
|
||||
"unknown": "अप्रत्याशित त्रुटि हुई",
|
||||
"empty": "नाम खाली नहीं हो सकता",
|
||||
"invalid_characters": "नाम में केवल अक्षर, अंक, रिक्त स्थान, अंडरस्कोर और हाइफन हो सकते हैं",
|
||||
"name_too_long": "नाम 50 अक्षरों या उससे कम होना चाहिए"
|
||||
},
|
||||
"abort": {
|
||||
@@ -218,8 +218,7 @@
|
||||
"rate_limited": "रेट सीमित",
|
||||
"maintenance": "रखरखाव",
|
||||
"initializing": "प्रारंभिककरण",
|
||||
"retrying": "पुनः प्रयास कर रहा है",
|
||||
"queued": "क्यू में"
|
||||
"retrying": "पुनः प्रयास कर रहा है"
|
||||
},
|
||||
"state_attributes": {
|
||||
"question": {
|
||||
@@ -311,6 +310,21 @@
|
||||
},
|
||||
"min_latency": {
|
||||
"name": "न्यूनतम विलंबता"
|
||||
},
|
||||
"last_model": {
|
||||
"name": "अंतिम उपयोग किया गया मॉडल"
|
||||
},
|
||||
"last_timestamp": {
|
||||
"name": "अंतिम प्रतिक्रिया समय"
|
||||
},
|
||||
"instance_name": {
|
||||
"name": "इंस्टेंस नाम"
|
||||
},
|
||||
"normalized_name": {
|
||||
"name": "सामान्यीकृत नाम"
|
||||
},
|
||||
"conversation_history": {
|
||||
"name": "वार्तालाप इतिहास"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"name_exists": "Esiste già un'istanza con questo nome",
|
||||
"invalid_name": "Nome dell'istanza non valido",
|
||||
"invalid_auth": "Autenticazione fallita - controlla la tua chiave API",
|
||||
"api_key_required": "La chiave API è obbligatoria quando si cambia provider o endpoint",
|
||||
"invalid_api_key": "Chiave API non valida - verifica le tue credenziali",
|
||||
"cannot_connect": "Impossibile connettersi al servizio API",
|
||||
"invalid_model": "Il modello selezionato non è disponibile",
|
||||
@@ -59,7 +60,6 @@
|
||||
"invalid_instance": "Istanze specificata non valida",
|
||||
"unknown": "Si è verificato un errore imprevisto",
|
||||
"empty": "Il nome non può essere vuoto",
|
||||
"invalid_characters": "Il nome può contenere solo lettere, numeri, spazi, trattini bassi e trattini",
|
||||
"name_too_long": "Il nome deve essere lungo 50 caratteri o meno"
|
||||
},
|
||||
"abort": {
|
||||
@@ -218,8 +218,7 @@
|
||||
"rate_limited": "Limite di frequenza",
|
||||
"maintenance": "Manutenzione",
|
||||
"initializing": "Inizializzazione",
|
||||
"retrying": "Riprova",
|
||||
"queued": "In coda"
|
||||
"retrying": "Riprova"
|
||||
},
|
||||
"state_attributes": {
|
||||
"question": {
|
||||
@@ -311,6 +310,21 @@
|
||||
},
|
||||
"min_latency": {
|
||||
"name": "Latenza minima"
|
||||
},
|
||||
"last_model": {
|
||||
"name": "Ultimo modello utilizzato"
|
||||
},
|
||||
"last_timestamp": {
|
||||
"name": "Ora dell'ultima risposta"
|
||||
},
|
||||
"instance_name": {
|
||||
"name": "Nome istanza"
|
||||
},
|
||||
"normalized_name": {
|
||||
"name": "Nome normalizzato"
|
||||
},
|
||||
"conversation_history": {
|
||||
"name": "Cronologia conversazione"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"name_exists": "Инстанца са овим именом већ постоји",
|
||||
"invalid_name": "Неважеће име инстанце",
|
||||
"invalid_auth": "Аутентификација није успела - проверите ваш API кључ",
|
||||
"api_key_required": "API кључ је обавезан при промени провајдера или endpoint-а",
|
||||
"invalid_api_key": "Неважећи API кључ - молимо проверите ваше акредитиве",
|
||||
"cannot_connect": "Неуспело повезивање са API сервисом",
|
||||
"invalid_model": "Изабрани модел није доступан",
|
||||
@@ -59,7 +60,6 @@
|
||||
"invalid_instance": "Неважећа инстанца је назначена",
|
||||
"unknown": "Дошло је до неочекиване грешке",
|
||||
"empty": "Име не може бити празно",
|
||||
"invalid_characters": "Име може садржавати само слова, цифре, размаке, подцрта и цртице",
|
||||
"name_too_long": "Име мора бити 50 знакова или мање"
|
||||
},
|
||||
"abort": {
|
||||
@@ -218,8 +218,7 @@
|
||||
"rate_limited": "Ограничење захтева",
|
||||
"maintenance": "Одржавање",
|
||||
"initializing": "Инициализује се",
|
||||
"retrying": "Покушава поново",
|
||||
"queued": "У реду"
|
||||
"retrying": "Покушава поново"
|
||||
},
|
||||
"state_attributes": {
|
||||
"question": {
|
||||
@@ -311,6 +310,21 @@
|
||||
},
|
||||
"min_latency": {
|
||||
"name": "Минимална латенција"
|
||||
},
|
||||
"last_model": {
|
||||
"name": "Последњи коришћени модел"
|
||||
},
|
||||
"last_timestamp": {
|
||||
"name": "Време последњег одговора"
|
||||
},
|
||||
"instance_name": {
|
||||
"name": "Назив инстанце"
|
||||
},
|
||||
"normalized_name": {
|
||||
"name": "Нормализовани назив"
|
||||
},
|
||||
"conversation_history": {
|
||||
"name": "Историја разговора"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"name_exists": "具有此名称的实例已存在",
|
||||
"invalid_name": "无效的实例名称",
|
||||
"invalid_auth": "身份验证失败 - 检查您的API密钥",
|
||||
"api_key_required": "更改提供商或端点时需要输入 API 密钥",
|
||||
"invalid_api_key": "无效的API密钥 - 请验证您的凭据",
|
||||
"cannot_connect": "无法连接到API服务",
|
||||
"invalid_model": "所选模型不可用",
|
||||
@@ -59,7 +60,6 @@
|
||||
"invalid_instance": "指定的实例无效",
|
||||
"unknown": "发生意外错误",
|
||||
"empty": "名称不能为空",
|
||||
"invalid_characters": "名称只能包含字母、数字、空格、下划线和连字符",
|
||||
"name_too_long": "名称必须少于50个字符"
|
||||
},
|
||||
"abort": {
|
||||
@@ -218,8 +218,7 @@
|
||||
"rate_limited": "速率限制",
|
||||
"maintenance": "维护中",
|
||||
"initializing": "初始化中",
|
||||
"retrying": "重试中",
|
||||
"queued": "排队中"
|
||||
"retrying": "重试中"
|
||||
},
|
||||
"state_attributes": {
|
||||
"question": {
|
||||
@@ -311,6 +310,21 @@
|
||||
},
|
||||
"min_latency": {
|
||||
"name": "最小延迟"
|
||||
},
|
||||
"last_model": {
|
||||
"name": "最近使用的模型"
|
||||
},
|
||||
"last_timestamp": {
|
||||
"name": "最近响应时间"
|
||||
},
|
||||
"instance_name": {
|
||||
"name": "实例名称"
|
||||
},
|
||||
"normalized_name": {
|
||||
"name": "规范化名称"
|
||||
},
|
||||
"conversation_history": {
|
||||
"name": "对话历史"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ from aiohttp.resolver import DefaultResolver
|
||||
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -150,7 +149,6 @@ async def resolve_hostname_ips(
|
||||
return ips
|
||||
|
||||
def create_pinned_session(
|
||||
hass: HomeAssistant,
|
||||
endpoint: str,
|
||||
resolved_ips: list[str],
|
||||
) -> aiohttp.ClientSession:
|
||||
@@ -161,6 +159,13 @@ def create_pinned_session(
|
||||
rather than re-resolving the hostname on each request.
|
||||
- Cookie pollution: DummyCookieJar prevents cookies from leaking between
|
||||
this integration and other HA components sharing the same domain.
|
||||
|
||||
Built directly on aiohttp: HA's async_create_clientsession always
|
||||
injects its own pooled connector and rejects a caller-supplied one,
|
||||
so a custom resolver cannot go through the helper. The caller owns
|
||||
the session and must close it. Requests must pass
|
||||
allow_redirects=False so a 3xx response cannot route past the pinned
|
||||
resolver to an unvalidated host.
|
||||
"""
|
||||
parsed = urlparse(endpoint)
|
||||
hostname = (parsed.hostname or "").lower()
|
||||
@@ -169,10 +174,9 @@ def create_pinned_session(
|
||||
hostname: [(ip, port) for ip in resolved_ips]
|
||||
}
|
||||
connector = aiohttp.TCPConnector(resolver=_PinnedResolver(pinned))
|
||||
return async_create_clientsession(
|
||||
hass,
|
||||
cookie_jar=aiohttp.DummyCookieJar(),
|
||||
return aiohttp.ClientSession(
|
||||
connector=connector,
|
||||
cookie_jar=aiohttp.DummyCookieJar(),
|
||||
)
|
||||
|
||||
async def validate_endpoint(
|
||||
|
||||
Reference in New Issue
Block a user