diff --git a/CHANGELOG.md b/CHANGELOG.md index 589baf7..ee3c295 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to this project are documented here. This project follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) conventions and [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 1.3.2 + +Compatibility fix for Home Assistant 2026.11's anthropic SDK 2.x pairing: constructed model descriptors were missing the now-required `lifecycle` field (issue #13). + +### Fixed + +- **`ModelInfo` lifecycle validation error on model refresh (issue #13)** — the anthropic SDK 2.x line (which HA 2026.11 pairs at `2.3.0`) made `lifecycle` a required field on `anthropic.types.ModelInfo`, so every descriptor the integration constructs without it fails with `pydantic ValidationError: lifecycle Field required`. The reporter hit it on OpenRouter model-list refreshes (`normalize_model()` in the OpenAI-compatible provider); the same construction shape existed in four more places, all fixed: both `fetch_model` fallbacks in the OpenAI-compatible provider (a `models.retrieve` failure, and non-`Model` response bodies such as z.ai's empty-200), the not-found fallback in `coordinator.get_model_info()`, and the empty-response fallback in the Anthropic provider. All five sites now construct with `lifecycle="active"`. SDKs elsewhere in the manifest pin range (0.108.0+) declare the field as an allowed extra rather than a schema field, so passing it unconditionally is safe across the entire range — the pinned test tree (anthropic 0.125.0) exercises exactly that path, with regression assertions on every constructed descriptor. + +### Validation + +- Full suite on the pinned test tree (HA 2025.8.1, anthropic 0.125.0): **146 passed, 2 skipped** — the 145/2 baseline plus a new `models.retrieve`-failure regression test, and lifecycle assertions added to the normalize-model, both fetch-degrade, and coordinator not-found tests +- `ModelInfo(lifecycle="active")` construction verified against `anthropic==2.3.0` on Python 3.13: clean + ## 1.3.1 Community contribution: richer reasoning-effort control for OpenAI-compatible providers. diff --git a/custom_components/configurable_llm/coordinator.py b/custom_components/configurable_llm/coordinator.py index 6b83ad8..24b8b83 100644 --- a/custom_components/configurable_llm/coordinator.py +++ b/custom_components/configurable_llm/coordinator.py @@ -94,6 +94,9 @@ def get_model_info(self, model_id: str) -> tuple[ModelInfo, bool]: id=model_id, created_at=datetime.datetime(1970, 1, 1, tzinfo=datetime.UTC), display_name=alias, + # Required by the anthropic SDK 2.x line (HA 2026.11); allowed + # extra on older SDKs in our pin range. + lifecycle="active", ), False @callback diff --git a/custom_components/configurable_llm/manifest.json b/custom_components/configurable_llm/manifest.json index 44a2942..3abf765 100644 --- a/custom_components/configurable_llm/manifest.json +++ b/custom_components/configurable_llm/manifest.json @@ -21,5 +21,5 @@ "openai>=2.45.0", "voluptuous-openapi>=0.1.0,<0.5" ], - "version": "1.3.1" + "version": "1.3.2" } diff --git a/custom_components/configurable_llm/providers/anthropic_provider.py b/custom_components/configurable_llm/providers/anthropic_provider.py index e844931..9447d46 100644 --- a/custom_components/configurable_llm/providers/anthropic_provider.py +++ b/custom_components/configurable_llm/providers/anthropic_provider.py @@ -1096,6 +1096,7 @@ async def fetch_model( id=model_id, created_at=datetime(1970, 1, 1, tzinfo=UTC), display_name=self.model_alias(model_id), + lifecycle="active", ), None, None, diff --git a/custom_components/configurable_llm/providers/openai_chat_provider.py b/custom_components/configurable_llm/providers/openai_chat_provider.py index efc1664..7d513bf 100644 --- a/custom_components/configurable_llm/providers/openai_chat_provider.py +++ b/custom_components/configurable_llm/providers/openai_chat_provider.py @@ -319,6 +319,10 @@ def normalize_model(self, raw: Any) -> ModelInfo: else datetime(1970, 1, 1, tzinfo=UTC) ), display_name=raw.id, + # anthropic SDK 2.x (paired with HA 2026.11) made ``lifecycle`` a + # required field; older SDKs in our pin range treat it as an + # allowed extra, so passing it unconditionally is safe. + lifecycle="active", ) async def async_list_models(self, client: openai.AsyncOpenAI) -> list[ModelInfo]: @@ -384,6 +388,7 @@ async def fetch_model( id=model_id, created_at=datetime(1970, 1, 1, tzinfo=UTC), display_name=model_id, + lifecycle="active", ), None, None, @@ -400,6 +405,7 @@ async def fetch_model( id=model_id, created_at=datetime(1970, 1, 1, tzinfo=UTC), display_name=model_id, + lifecycle="active", ), None, None, diff --git a/tests/providers/test_anthropic_provider.py b/tests/providers/test_anthropic_provider.py index a29f369..c61ef04 100644 --- a/tests/providers/test_anthropic_provider.py +++ b/tests/providers/test_anthropic_provider.py @@ -485,6 +485,7 @@ async def test_fetch_model_degrades_on_non_model_result(hass: HomeAssistant) -> assert info.display_name == "glm-4.7-flash" assert info.capabilities is None assert info.max_tokens is None + assert info.lifecycle == "active" async def test_fetch_model_passthrough_on_real_result(hass: HomeAssistant) -> None: diff --git a/tests/providers/test_openai_chat_provider.py b/tests/providers/test_openai_chat_provider.py index c559a1f..dfb644c 100644 --- a/tests/providers/test_openai_chat_provider.py +++ b/tests/providers/test_openai_chat_provider.py @@ -534,6 +534,7 @@ def test_normalize_model() -> None: assert info.display_name == "llama-3-70b" assert info.capabilities is None assert info.created_at == datetime.fromtimestamp(1700000000, UTC) + assert info.lifecycle == "active" async def test_async_list_models_success(hass: HomeAssistant) -> None: @@ -682,6 +683,27 @@ async def test_fetch_model_degrades_on_non_model_result(hass: HomeAssistant) -> assert info.id == "glm-4.7-flash" assert info.display_name == "glm-4.7-flash" assert info.capabilities is None + assert info.lifecycle == "active" + + +async def test_fetch_model_degrades_on_retrieve_error(hass: HomeAssistant) -> None: + """A models.retrieve failure degrades to a safe descriptor (issue #13). + + Every descriptor this provider constructs must carry ``lifecycle`` so the + anthropic SDK 2.x line (where the field is required) accepts it; older + SDKs in the pin range keep it as an allowed extra. + """ + coordinator = MagicMock() + coordinator.client.with_options.return_value.models.retrieve = AsyncMock( + side_effect=RuntimeError("endpoint not implemented") + ) + + info, err_key, _ = await provider.fetch_model(coordinator, "llama-3-70b") + + assert err_key is None + assert isinstance(info, ModelInfo) + assert info.id == "llama-3-70b" + assert info.lifecycle == "active" async def test_fetch_model_passthrough_on_real_model(hass: HomeAssistant) -> None: diff --git a/tests/test_coordinator.py b/tests/test_coordinator.py index d9cac61..ce7e68c 100644 --- a/tests/test_coordinator.py +++ b/tests/test_coordinator.py @@ -178,6 +178,8 @@ async def test_get_model_info_not_found( assert model_info.id == "unknown-model" assert model_info.display_name == "unknown-model" assert model_info.created_at == datetime(1970, 1, 1, tzinfo=UTC) + # anthropic SDK 2.x (HA 2026.11) requires lifecycle on ModelInfo. + assert model_info.lifecycle == "active" async def test_get_default_model_with_data(