fix: Comprehensive v2.4.0 quality pass — 24 review findings + review agent fixes

Phase A — Critical:
- Remove dual timeout stacking in coordinator._send_to_api
- Add Gemini-specific asyncio.timeout (sync SDK via to_thread)
- Store full text in history for context; cap per-field at 32KB on disk
- Fix instance lookup to match by normalized_name
- Add Bearer/sk-/x-api-key credential sanitization patterns

Phase B — Dead code removal:
- Merge async_ask_question/async_process_question into single method
- Remove dead is_anthropic flag from coordinator and __init__
- Remove unused DEFAULT_TIMEOUT and API_TIMEOUT constants
- Remove redundant _create_history_dir calls
- Consolidate async_check_api to use provider registry

Phase C — Config flow correctness:
- Truncate name before uniqueness check (prevent post-truncation collisions)
- Add async_set_unique_id + _abort_if_unique_id_configured
- Extract shared _build_parameter_schema for ConfigFlow/OptionsFlow dedup

Phase D — UX improvements:
- Optimize history write: serialize from memory, single file write
- Show last 5 history entries in sensor attributes (was 1)
- Return actual error type in ask_question service response
- Add dedicated api_key_required error for provider/endpoint changes
- Pass config_entry to DataUpdateCoordinator (HA 2024.8+)

Phase E — Cleanup:
- Extract _apply_structured_output for OpenAI/DeepSeek dedup
- Reduce ABSOLUTE_MAX_HISTORY_SIZE to 200 with Final annotation
- Remove dead translation keys (queued, invalid_characters)
- Migrate to _attr_has_entity_name = True
- Add from __future__ import annotations to all modules
- Remove redundant api_status sensor attribute
- Add missing translation keys (last_model, last_timestamp, etc.)

Review agent fixes:
- Add archive file cleanup (max 3 archives) to prevent disk exhaustion
- Per-entry storage cap (32KB per field) for history on disk
This commit is contained in:
SMKRV
2026-03-12 02:24:09 +03:00
parent 9cdeb9f417
commit a8a91972ba
13 changed files with 272 additions and 275 deletions
+33 -38
View File
@@ -6,6 +6,8 @@ API Client for HA Text AI.
@github: https://github.com/smkrv/ha-text-ai
@source: https://github.com/smkrv/ha-text-ai
"""
from __future__ import annotations
import logging
import asyncio
from typing import Any, Dict, List, Optional
@@ -182,6 +184,30 @@ class APIClient:
_LOGGER.error("API request failed: %s", str(e))
raise HomeAssistantError(f"API request failed: {str(e)}")
@staticmethod
def _apply_structured_output(
payload: Dict[str, Any],
structured_output: bool,
json_schema: Optional[str],
) -> None:
"""Apply OpenAI-compatible structured output to payload in-place."""
if not (structured_output and json_schema):
return
import json
try:
schema = json.loads(json_schema)
payload["response_format"] = {
"type": "json_schema",
"json_schema": {
"name": "structured_response",
"strict": True,
"schema": schema,
},
}
except json.JSONDecodeError as e:
_LOGGER.warning("Invalid JSON schema: %s. Falling back to json_object.", e)
payload["response_format"] = {"type": "json_object"}
async def _create_deepseek_completion(
self,
model: str,
@@ -198,26 +224,9 @@ class APIClient:
"messages": messages,
"temperature": temperature,
"max_tokens": max_tokens,
"stream": False
"stream": False,
}
# Add structured output format if enabled (DeepSeek is OpenAI-compatible)
if structured_output and json_schema:
try:
import json
schema = json.loads(json_schema)
payload["response_format"] = {
"type": "json_schema",
"json_schema": {
"name": "structured_response",
"strict": True,
"schema": schema
}
}
_LOGGER.debug("DeepSeek structured output enabled with schema")
except json.JSONDecodeError as e:
_LOGGER.warning("Invalid JSON schema provided: %s. Falling back to json_object mode.", e)
payload["response_format"] = {"type": "json_object"}
self._apply_structured_output(payload, structured_output, json_schema)
data = await self._make_request(url, payload)
return {
@@ -250,24 +259,7 @@ class APIClient:
"temperature": temperature,
"max_tokens": max_tokens,
}
# Add structured output format if enabled
if structured_output and json_schema:
try:
import json
schema = json.loads(json_schema)
payload["response_format"] = {
"type": "json_schema",
"json_schema": {
"name": "structured_response",
"strict": True,
"schema": schema
}
}
_LOGGER.debug("OpenAI structured output enabled with schema")
except json.JSONDecodeError as e:
_LOGGER.warning("Invalid JSON schema provided: %s. Falling back to json_object mode.", e)
payload["response_format"] = {"type": "json_object"}
self._apply_structured_output(payload, structured_output, json_schema)
data = await self._make_request(url, payload)
return {
@@ -469,7 +461,10 @@ class APIClient:
)
return chat.send_message(last_user_msg)
response = await asyncio.to_thread(generate_content)
# Gemini uses sync SDK via to_thread, so needs its own timeout
# (aiohttp ClientTimeout doesn't apply here)
async with asyncio.timeout(self.api_timeout):
response = await asyncio.to_thread(generate_content)
# Extract response text
def extract_response():