Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions custom_components/configurable_llm/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion custom_components/configurable_llm/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@
"openai>=2.45.0",
"voluptuous-openapi>=0.1.0,<0.5"
],
"version": "1.3.1"
"version": "1.3.2"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions tests/providers/test_anthropic_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
22 changes: 22 additions & 0 deletions tests/providers/test_openai_chat_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions tests/test_coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading