mirror of
https://github.com/smkrv/ha-text-ai.git
synced 2026-07-29 11:53:55 +08:00
feat: v2.5.0 - disable_thinking, reasoning models, MIT license, security hardening
Features: - disable_thinking toggle (issue #11) with per-provider semantics: OpenAI classic gets /no_think soft-switch, OpenAI reasoning gets reasoning_effort=low, DeepSeek gets both, Anthropic no-op (thinking opt-in), Gemini 2.5+ gets thinking_budget=0 - OpenAI reasoning model support (o1/o3/o4-mini/gpt-5 family): max_completion_tokens, developer role, reasoning_effort - Per-request disable_thinking override in ask_question service - Provider-specific temperature clip (Anthropic 0-1, others 0-2) Security: - Require API key re-entry on provider change (OptionsFlow) - Validate Anthropic json_schema before system-prompt concatenation - Symlink protection in history file operations - Hardened secret redaction regexes (sk-*, AIza*, x-api-key) - get_history service: hard cap on limit (default 10, max 100) Reliability: - Retry on 502/503/504 in addition to 429/timeout - Honor Retry-After header on 429 - Exception chaining (raise ... from err) - _strip_think_blocks handles nested/dangling tags - normalize_name sha256 fallback for empty-collapse inputs UI/housekeeping: - api_key uses TextSelector(type=PASSWORD) - DeviceInfo entry_type=SERVICE - Services unregister on last entry unload - Dependabot for GitHub Actions - persist-credentials=false in checkout steps - manifest requirements pinned with upper bounds - Ignore docs/plans/ (working artifacts) License: PolyForm Noncommercial 1.0.0 -> MIT No breaking changes: service response shape, sensor attributes, entity IDs and on-disk formats unchanged.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Config flow for HA text AI integration.
|
||||
|
||||
@license: PolyForm Noncommercial 1.0.0 (https://polyformproject.org/licenses/noncommercial/1.0.0)
|
||||
@license: MIT (https://opensource.org/licenses/MIT)
|
||||
@author: SMKRV
|
||||
@github: https://github.com/smkrv/ha-text-ai
|
||||
@source: https://github.com/smkrv/ha-text-ai
|
||||
@@ -56,6 +56,8 @@ from .const import (
|
||||
MAX_HISTORY_SIZE,
|
||||
CONF_ALLOW_LOCAL_NETWORK,
|
||||
DEFAULT_ALLOW_LOCAL_NETWORK,
|
||||
CONF_DISABLE_THINKING,
|
||||
DEFAULT_DISABLE_THINKING,
|
||||
)
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
@@ -92,6 +94,10 @@ def _build_parameter_schema(data: Dict[str, Any]) -> dict:
|
||||
CONF_MAX_HISTORY_SIZE,
|
||||
default=data.get(CONF_MAX_HISTORY_SIZE, DEFAULT_MAX_HISTORY),
|
||||
): vol.All(vol.Coerce(int), vol.Range(min=MIN_HISTORY_SIZE, max=MAX_HISTORY_SIZE)),
|
||||
vol.Optional(
|
||||
CONF_DISABLE_THINKING,
|
||||
default=data.get(CONF_DISABLE_THINKING, DEFAULT_DISABLE_THINKING),
|
||||
): bool,
|
||||
}
|
||||
|
||||
|
||||
@@ -131,7 +137,9 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
defaults = data or {}
|
||||
schema_dict = {
|
||||
vol.Required(CONF_NAME, default=defaults.get(CONF_NAME, DEFAULT_INSTANCE_NAME)): str,
|
||||
vol.Required(CONF_API_KEY): str,
|
||||
vol.Required(CONF_API_KEY): selector.TextSelector(
|
||||
selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD)
|
||||
),
|
||||
vol.Required(CONF_MODEL, default=defaults.get(CONF_MODEL, get_default_model(self._provider))): str,
|
||||
vol.Required(CONF_API_ENDPOINT, default=defaults.get(CONF_API_ENDPOINT, get_default_endpoint(self._provider))): str,
|
||||
vol.Optional(
|
||||
@@ -290,6 +298,7 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
CONF_CONTEXT_MESSAGES: user_input.get(CONF_CONTEXT_MESSAGES, DEFAULT_CONTEXT_MESSAGES),
|
||||
CONF_MAX_HISTORY_SIZE: user_input.get(CONF_MAX_HISTORY_SIZE, DEFAULT_MAX_HISTORY),
|
||||
CONF_ALLOW_LOCAL_NETWORK: user_input.get(CONF_ALLOW_LOCAL_NETWORK, DEFAULT_ALLOW_LOCAL_NETWORK),
|
||||
CONF_DISABLE_THINKING: user_input.get(CONF_DISABLE_THINKING, DEFAULT_DISABLE_THINKING),
|
||||
}
|
||||
|
||||
_LOGGER.debug("Creating config entry with data: %s", safe_log_data(entry_data))
|
||||
@@ -398,7 +407,10 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
api_key = user_input.get(CONF_API_KEY, "").strip()
|
||||
endpoint = user_input.get(CONF_API_ENDPOINT, default_endpoint)
|
||||
|
||||
# Require API key re-entry when endpoint or provider changed
|
||||
# Require API key re-entry when endpoint or provider changed.
|
||||
# Why: reusing a stored key after provider/endpoint change could
|
||||
# ship credentials to a different service (e.g. OpenAI key to
|
||||
# api.anthropic.com). Always force explicit re-entry.
|
||||
stored_endpoint = current_data.get(CONF_API_ENDPOINT, "")
|
||||
endpoint_changed = endpoint != stored_endpoint
|
||||
if not api_key and (provider_changed or endpoint_changed):
|
||||
@@ -418,8 +430,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
}
|
||||
)
|
||||
|
||||
# Fall back to stored key if not re-entered and endpoint unchanged
|
||||
if not api_key:
|
||||
# Fall back to stored key only when neither provider nor endpoint changed.
|
||||
# Defensive: never silently reuse stored key across providers.
|
||||
if not api_key and not provider_changed and not endpoint_changed:
|
||||
api_key = current_data.get(CONF_API_KEY, "")
|
||||
|
||||
allow_local = user_input.get(CONF_ALLOW_LOCAL_NETWORK, DEFAULT_ALLOW_LOCAL_NETWORK)
|
||||
@@ -473,7 +486,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
data = user_input or current_data
|
||||
|
||||
schema_dict = {
|
||||
vol.Optional(CONF_API_KEY, default=""): str,
|
||||
vol.Optional(CONF_API_KEY, default=""): selector.TextSelector(
|
||||
selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD)
|
||||
),
|
||||
vol.Required(
|
||||
CONF_API_ENDPOINT,
|
||||
default=data.get(CONF_API_ENDPOINT, default_endpoint),
|
||||
|
||||
Reference in New Issue
Block a user