Skip to content

add ovhcloud_ai_endpoints integration - #171402

Merged
joostlek merged 10 commits into
home-assistant:devfrom
Crocmagnon:devel/ovh-conversation
May 26, 2026
Merged

add ovhcloud_ai_endpoints integration#171402
joostlek merged 10 commits into
home-assistant:devfrom
Crocmagnon:devel/ovh-conversation

Conversation

@Crocmagnon

@Crocmagnon Crocmagnon commented May 19, 2026

Copy link
Copy Markdown
Contributor

Proposed change

Add integration for OVH AI endpoints, starting with conversation only.

Type of change

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New integration (thank you!)
  • New feature (which adds functionality to an existing integration)
  • Deprecation (breaking change to happen in the future)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests

Additional information

Checklist

  • I understand the code I am submitting and can explain how it works.
  • The code change is tested and works locally.
  • Local tests pass. Your PR cannot be merged unless tests pass
  • There is no commented out code in this PR.
  • I have followed the development checklist
  • I have followed the perfect PR recommendations
  • The code has been formatted using Ruff (ruff format homeassistant tests)
  • Tests have been added to verify that the new code works.
  • Any generated code has been carefully reviewed for correctness and compliance with project standards.

If user exposed functionality or configuration variables are added/changed:

If the code communicates with devices, web services, or third-party tools:

  • The manifest file has all fields filled out correctly.
    Updated and included derived files by running: python3 -m script.hassfest.
  • New or updated dependencies have been added to requirements_all.txt.
    Updated by running python3 -m script.gen_requirements_all.
  • For the updated dependencies a diff between library versions and ideally a link to the changelog/release notes is added to the PR description.

To help with the load of incoming pull requests:

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds a new Home Assistant integration for OVHcloud AI Endpoints, providing a conversation agent backed by OVHcloud's OpenAI-compatible API. Includes config flow with conversation subentries, model selection, and support for both <think> tag and reasoning/reasoning_content field extraction.

Changes:

  • New ovhcloud_ai_endpoints integration with config flow, conversation entity, and subentry-based model configuration
  • Reasoning/thinking content extraction supporting OpenRouter, vLLM and DeepSeek-style fields plus inline <think> tags
  • Comprehensive tests for setup, config flow, and conversation behavior

Reviewed changes

Copilot reviewed 20 out of 22 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
homeassistant/components/ovhcloud_ai_endpoints/init.py Integration setup, OpenAI client init, API key validation
homeassistant/components/ovhcloud_ai_endpoints/config_flow.py User and conversation subentry config flow
homeassistant/components/ovhcloud_ai_endpoints/conversation.py Conversation entity wiring
homeassistant/components/ovhcloud_ai_endpoints/entity.py Base entity, chat log handling, thinking extraction
homeassistant/components/ovhcloud_ai_endpoints/const.py Constants and recommended defaults
homeassistant/components/ovhcloud_ai_endpoints/strings.json Translation strings
homeassistant/components/ovhcloud_ai_endpoints/manifest.json Integration manifest
homeassistant/components/ovhcloud_ai_endpoints/quality_scale.yaml Quality scale tracking
tests/components/ovhcloud_ai_endpoints/* Test fixtures, conftest, and tests for init/config flow/conversation
CODEOWNERS, mypy.ini, .strict-typing, requirements_*, generated files Registration of the new integration

Comment thread tests/components/ovhcloud_ai_endpoints/test_conversation.py Outdated
Comment thread homeassistant/components/ovhcloud_ai_endpoints/const.py Outdated
Comment thread homeassistant/components/ovhcloud_ai_endpoints/__init__.py Outdated
@Crocmagnon
Crocmagnon force-pushed the devel/ovh-conversation branch from 25faa45 to d1f1c79 Compare May 19, 2026 21:57
Copilot AI review requested due to automatic review settings May 20, 2026 18:27
@Crocmagnon
Crocmagnon force-pushed the devel/ovh-conversation branch from d1f1c79 to da3bf3f Compare May 20, 2026 18:27
@Crocmagnon Crocmagnon changed the title ovhcloud ai endpoints: initial commit add ovhcloud_ai_endpoints integration May 20, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 22 changed files in this pull request and generated 8 comments.

Comment thread tests/components/ovhcloud_ai_endpoints/test_config_flow.py Outdated
Comment on lines +90 to +95
mock_openai_client.models.list.side_effect = None

result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_API_KEY: "bla"},
)
Comment thread homeassistant/components/ovhcloud_ai_endpoints/entity.py
Comment on lines +207 to +216
for _iteration in range(MAX_TOOL_ITERATIONS):
try:
result = await client.chat.completions.create(**model_args)
except openai.OpenAIError as err:
LOGGER.error("Error talking to API: %s", err)
raise HomeAssistantError("Error talking to API") from err

if not result.choices:
LOGGER.error("API returned empty choices")
raise HomeAssistantError("API returned empty response")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same implem as openrouter

]
)
if not chat_log.unresponded_tool_results:
break

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same implem as openrouter

Comment thread homeassistant/components/ovhcloud_ai_endpoints/config_flow.py
Comment thread homeassistant/components/ovhcloud_ai_endpoints/__init__.py
Comment thread homeassistant/components/ovhcloud_ai_endpoints/conversation.py
@Crocmagnon
Crocmagnon force-pushed the devel/ovh-conversation branch from da3bf3f to d8d9f2f Compare May 20, 2026 18:32
Copilot AI review requested due to automatic review settings May 20, 2026 18:58
@Crocmagnon
Crocmagnon force-pushed the devel/ovh-conversation branch from d8d9f2f to ac575e3 Compare May 20, 2026 18:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 22 changed files in this pull request and generated 5 comments.

Comment on lines +31 to +40
client = _create_client(hass, entry.data[CONF_API_KEY])

try:
# Unfortunately I couldn't find an endpoint that would authenticate the key
# without calling an LLM. This always succeeds regardless of auth.
async for _ in client.with_options(timeout=10.0).models.list():
break
except OpenAIError as err:
raise ConfigEntryNotReady(err) from err

Comment on lines +56 to +60
try:
# Unfortunately I couldn't find an endpoint that would authenticate the key
# without calling an LLM. This always succeeds regardless of auth.
async for _ in client.with_options(timeout=10.0).models.list():
break
]
)
if not chat_log.unresponded_tool_results:
break
content: conversation.Content,
) -> ChatCompletionMessageParam | None:
"""Convert chat message for this agent to the native format."""
LOGGER.debug("_convert_content_to_chat_message=%s", content)
Comment on lines +90 to +95
async def _get_models(self) -> None:
"""Fetch models from OVHcloud AI Endpoints."""
client: AsyncOpenAI = self._get_entry().runtime_data
self.models = [
model.id async for model in client.with_options(timeout=10.0).models.list()
]
@Crocmagnon
Crocmagnon force-pushed the devel/ovh-conversation branch from ac575e3 to 4f22457 Compare May 20, 2026 19:13
Copilot AI review requested due to automatic review settings May 20, 2026 19:36
@Crocmagnon
Crocmagnon force-pushed the devel/ovh-conversation branch from 4f22457 to 30e1650 Compare May 20, 2026 19:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 22 changed files in this pull request and generated 5 comments.

Comment thread homeassistant/components/ovhcloud_ai_endpoints/entity.py
Comment thread homeassistant/components/ovhcloud_ai_endpoints/entity.py
Comment thread tests/components/ovhcloud_ai_endpoints/conftest.py
Comment thread homeassistant/components/ovhcloud_ai_endpoints/quality_scale.yaml
Comment thread homeassistant/components/ovhcloud_ai_endpoints/quality_scale.yaml Outdated
@Crocmagnon
Crocmagnon marked this pull request as ready for review May 20, 2026 19:53
@Crocmagnon
Crocmagnon force-pushed the devel/ovh-conversation branch from 30e1650 to cd65fae Compare May 20, 2026 19:58
Copilot AI review requested due to automatic review settings May 21, 2026 09:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 21 changed files in this pull request and generated 6 comments.

Comment on lines +125 to +133
options = [
SelectOptionDict(value=model_id, label=model_id) for model_id in self.models
]

hass_apis: list[SelectOptionDict] = [
SelectOptionDict(
label=api.name,
value=api.id,
)
Comment on lines +92 to +94
self.models = [
model.id async for model in client.with_options(timeout=10.0).models.list()
]

async def _transform_response(
message: ChatCompletionMessage,
) -> AsyncGenerator[conversation.AssistantContentDeltaDict]:
]
)
if not chat_log.unresponded_tool_results:
break
content: conversation.Content,
) -> ChatCompletionMessageParam | None:
"""Convert chat message for this agent to the native format."""
LOGGER.debug("_convert_content_to_chat_message=%s", content)
We send a chat completion request with an unknown ``extra_body`` field
to prevent valid usage and billing.
A valid key triggers a 400 (BadRequestError), which we treat as success.
An invalid key triggers a 401 (AuthenticationError),which propagates
Comment on lines +28 to +44
async def _validate_api_key(client: AsyncOpenAI) -> None:
"""Validate the API key against the chat completions endpoint.

We send a chat completion request with an unknown ``extra_body`` field
to prevent valid usage and billing.
A valid key triggers a 400 (BadRequestError), which we treat as success.
An invalid key triggers a 401 (AuthenticationError),which propagates
along with any other exception.
"""
try:
await client.with_options(timeout=10.0).chat.completions.create(
model="llama@latest",
messages=[ChatCompletionUserMessageParam(role="user", content="ping")],
extra_body={"foo": "bar"},
)
except BadRequestError:
return

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about just fetching the models?

@Crocmagnon Crocmagnon May 21, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Models list is not authenticated. Calling it with an invalid key results in a HTTP 200. I had a chat with the product team and this is what they suggested, for lack of a better option.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool! then this would work

Comment thread homeassistant/components/ovhcloud_ai_endpoints/const.py
Comment thread homeassistant/components/ovhcloud_ai_endpoints/conversation.py Outdated
Comment thread tests/components/ovhcloud_ai_endpoints/__init__.py Outdated
Comment thread tests/components/ovhcloud_ai_endpoints/conftest.py Outdated
Comment thread tests/components/ovhcloud_ai_endpoints/test_init.py Outdated
@home-assistant
home-assistant Bot marked this pull request as draft May 21, 2026 17:19
@home-assistant

Copy link
Copy Markdown
Contributor

Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍

Learn more about our pull request process.

Copilot AI review requested due to automatic review settings May 21, 2026 20:05
@Crocmagnon
Crocmagnon marked this pull request as ready for review May 21, 2026 20:06
@home-assistant
home-assistant Bot requested a review from joostlek May 21, 2026 20:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 21 changed files in this pull request and generated 7 comments.

]
)
if not chat_log.unresponded_tool_results:
break
content: conversation.Content,
) -> ChatCompletionMessageParam | None:
"""Convert chat message for this agent to the native format."""
LOGGER.debug("_convert_content_to_chat_message=%s", content)
Comment on lines +28 to +44
async def _validate_api_key(client: AsyncOpenAI) -> None:
"""Validate the API key against the chat completions endpoint.

We send a chat completion request with an unknown ``extra_body`` field
to prevent valid usage and billing.
A valid key triggers a 400 (BadRequestError), which we treat as success.
An invalid key triggers a 401 (AuthenticationError),which propagates
along with any other exception.
"""
try:
await client.with_options(timeout=10.0).chat.completions.create(
model="llama@latest",
messages=[ChatCompletionUserMessageParam(role="user", content="ping")],
extra_body={"foo": "bar"},
)
except BadRequestError:
return
We send a chat completion request with an unknown ``extra_body`` field
to prevent valid usage and billing.
A valid key triggers a 400 (BadRequestError), which we treat as success.
An invalid key triggers a 401 (AuthenticationError),which propagates
comment: No actions are implemented
appropriate-polling:
status: exempt
comment: the integration does not poll
from . import OVHcloudAIEndpointsConfigEntry
from .const import DOMAIN, LOGGER

MAX_TOOL_ITERATIONS = 10

client = self.entry.runtime_data

for _iteration in range(MAX_TOOL_ITERATIONS):
Comment on lines +28 to +44
async def _validate_api_key(client: AsyncOpenAI) -> None:
"""Validate the API key against the chat completions endpoint.

We send a chat completion request with an unknown ``extra_body`` field
to prevent valid usage and billing.
A valid key triggers a 400 (BadRequestError), which we treat as success.
An invalid key triggers a 401 (AuthenticationError),which propagates
along with any other exception.
"""
try:
await client.with_options(timeout=10.0).chat.completions.create(
model="llama@latest",
messages=[ChatCompletionUserMessageParam(role="user", content="ping")],
extra_body={"foo": "bar"},
)
except BadRequestError:
return

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool! then this would work

Comment on lines +28 to +29
for subentry_id, subentry in mock_config_entry.subentries.items()
if subentry.subentry_type == subentry_type

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have a mock_config_entry helper method for this we should use

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function was not used, remainder of the open_router integration which was used as inspiration/reference for this PR.
I removed it.

Copilot AI review requested due to automatic review settings May 26, 2026 20:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 21 changed files in this pull request and generated 5 comments.

Comment on lines +37 to +43
@pytest.mark.parametrize("enable_assist", [True, False], ids=["assist", "no_assist"])
async def test_all_entities(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
mock_openai_client: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
Comment on lines +320 to +327
@pytest.mark.parametrize("enable_assist", [True])
async def test_function_call(
hass: HomeAssistant,
mock_chat_log: MockChatLog, # noqa: F811
mock_config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
mock_openai_client: AsyncMock,
) -> None:
]
)
if not chat_log.unresponded_tool_results:
break
We send a chat completion request with an unknown ``extra_body`` field
to prevent valid usage and billing.
A valid key triggers a 400 (BadRequestError), which we treat as success.
An invalid key triggers a 401 (AuthenticationError),which propagates
Comment on lines +37 to +44
try:
await client.with_options(timeout=10.0).chat.completions.create(
model="llama@latest",
messages=[ChatCompletionUserMessageParam(role="user", content="ping")],
extra_body={"foo": "bar"},
)
except BadRequestError:
return
@joostlek
joostlek merged commit 64bc689 into home-assistant:dev May 26, 2026
48 checks passed
@Crocmagnon
Crocmagnon deleted the devel/ovh-conversation branch May 26, 2026 20:38
@github-actions github-actions Bot locked and limited conversation to collaborators May 27, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants