Skip to content

add ovhcloud_ai_endpoints integration#171402

Open
Crocmagnon wants to merge 8 commits into
home-assistant:devfrom
Crocmagnon:devel/ovh-conversation
Open

add ovhcloud_ai_endpoints integration#171402
Crocmagnon wants to merge 8 commits into
home-assistant:devfrom
Crocmagnon:devel/ovh-conversation

Conversation

@Crocmagnon
Copy link
Copy Markdown
Contributor

@Crocmagnon Crocmagnon commented May 19, 2026

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:

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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?

Copy link
Copy Markdown
Contributor Author

@Crocmagnon Crocmagnon May 21, 2026

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.

from homeassistant.const import CONF_LLM_HASS_API, CONF_PROMPT
from homeassistant.helpers import llm

DOMAIN = "ovhcloud_ai_endpoints"
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.

why do we call this _endpoints?

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.

That’s the name of the product, see https://www.ovhcloud.com/fr/public-cloud/ai-endpoints/

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 on lines +44 to +55
async def test_dynamic_subentry_creates_entity_and_device(
hass: HomeAssistant,
mock_openai_client: AsyncMock,
entity_registry: er.EntityRegistry,
device_registry: dr.DeviceRegistry,
) -> None:
"""A subentry added after setup must spawn its conversation entity and device."""
entry = MockConfigEntry(
title="OVHcloud AI Endpoints",
domain=DOMAIN,
data={CONF_API_KEY: "bla"},
)
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.

dynamic subentry? What does that mean?

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.

like an entry added after the initial setup
maybe new_subentry?

@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
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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):
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants