Skip to content

Commit 1242456

Browse files
Shulyakaballoob
andauthored
Bump openai end switch from dall-e-2 to dall-e-3 (#104998)
* Bump openai * Fix tests * Apply suggestions from code review * Undo conftest changes * Raise repasir issue * Explicitly use async mock for chat.completions.create It is not always detected correctly as async because it uses a decorator * removed duplicated message * ruff * Compatibility with old pydantic versions * Compatibility with old pydantic versions * More tests * Apply suggestions from code review Co-authored-by: Paulus Schoutsen <[email protected]> * Apply suggestions from code review --------- Co-authored-by: Paulus Schoutsen <[email protected]>
1 parent c0314cd commit 1242456

File tree

10 files changed

+266
-68
lines changed

10 files changed

+266
-68
lines changed

Diff for: homeassistant/components/openai_conversation/__init__.py

+48-23
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
"""The OpenAI Conversation integration."""
22
from __future__ import annotations
33

4-
from functools import partial
54
import logging
65
from typing import Literal
76

87
import openai
9-
from openai import error
108
import voluptuous as vol
119

1210
from homeassistant.components import conversation
@@ -23,7 +21,13 @@
2321
HomeAssistantError,
2422
TemplateError,
2523
)
26-
from homeassistant.helpers import config_validation as cv, intent, selector, template
24+
from homeassistant.helpers import (
25+
config_validation as cv,
26+
intent,
27+
issue_registry as ir,
28+
selector,
29+
template,
30+
)
2731
from homeassistant.helpers.typing import ConfigType
2832
from homeassistant.util import ulid
2933

@@ -52,17 +56,38 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
5256

5357
async def render_image(call: ServiceCall) -> ServiceResponse:
5458
"""Render an image with dall-e."""
59+
client = hass.data[DOMAIN][call.data["config_entry"]]
60+
61+
if call.data["size"] in ("256", "512", "1024"):
62+
ir.async_create_issue(
63+
hass,
64+
DOMAIN,
65+
"image_size_deprecated_format",
66+
breaks_in_ha_version="2024.7.0",
67+
is_fixable=False,
68+
is_persistent=True,
69+
learn_more_url="https://www.home-assistant.io/integrations/openai_conversation/",
70+
severity=ir.IssueSeverity.WARNING,
71+
translation_key="image_size_deprecated_format",
72+
)
73+
size = "1024x1024"
74+
else:
75+
size = call.data["size"]
76+
5577
try:
56-
response = await openai.Image.acreate(
57-
api_key=hass.data[DOMAIN][call.data["config_entry"]],
78+
response = await client.images.generate(
79+
model="dall-e-3",
5880
prompt=call.data["prompt"],
81+
size=size,
82+
quality=call.data["quality"],
83+
style=call.data["style"],
84+
response_format="url",
5985
n=1,
60-
size=f'{call.data["size"]}x{call.data["size"]}',
6186
)
62-
except error.OpenAIError as err:
87+
except openai.OpenAIError as err:
6388
raise HomeAssistantError(f"Error generating image: {err}") from err
6489

65-
return response["data"][0]
90+
return response.data[0].model_dump(exclude={"b64_json"})
6691

6792
hass.services.async_register(
6893
DOMAIN,
@@ -76,7 +101,11 @@ async def render_image(call: ServiceCall) -> ServiceResponse:
76101
}
77102
),
78103
vol.Required("prompt"): cv.string,
79-
vol.Optional("size", default="512"): vol.In(("256", "512", "1024")),
104+
vol.Optional("size", default="1024x1024"): vol.In(
105+
("1024x1024", "1024x1792", "1792x1024", "256", "512", "1024")
106+
),
107+
vol.Optional("quality", default="standard"): vol.In(("standard", "hd")),
108+
vol.Optional("style", default="vivid"): vol.In(("vivid", "natural")),
80109
}
81110
),
82111
supports_response=SupportsResponse.ONLY,
@@ -86,21 +115,16 @@ async def render_image(call: ServiceCall) -> ServiceResponse:
86115

87116
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
88117
"""Set up OpenAI Conversation from a config entry."""
118+
client = openai.AsyncOpenAI(api_key=entry.data[CONF_API_KEY])
89119
try:
90-
await hass.async_add_executor_job(
91-
partial(
92-
openai.Model.list,
93-
api_key=entry.data[CONF_API_KEY],
94-
request_timeout=10,
95-
)
96-
)
97-
except error.AuthenticationError as err:
120+
await hass.async_add_executor_job(client.with_options(timeout=10.0).models.list)
121+
except openai.AuthenticationError as err:
98122
_LOGGER.error("Invalid API key: %s", err)
99123
return False
100-
except error.OpenAIError as err:
124+
except openai.OpenAIError as err:
101125
raise ConfigEntryNotReady(err) from err
102126

103-
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = entry.data[CONF_API_KEY]
127+
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = client
104128

105129
conversation.async_set_agent(hass, entry, OpenAIAgent(hass, entry))
106130
return True
@@ -160,17 +184,18 @@ async def async_process(
160184

161185
_LOGGER.debug("Prompt for %s: %s", model, messages)
162186

187+
client = self.hass.data[DOMAIN][self.entry.entry_id]
188+
163189
try:
164-
result = await openai.ChatCompletion.acreate(
165-
api_key=self.entry.data[CONF_API_KEY],
190+
result = await client.chat.completions.create(
166191
model=model,
167192
messages=messages,
168193
max_tokens=max_tokens,
169194
top_p=top_p,
170195
temperature=temperature,
171196
user=conversation_id,
172197
)
173-
except error.OpenAIError as err:
198+
except openai.OpenAIError as err:
174199
intent_response = intent.IntentResponse(language=user_input.language)
175200
intent_response.async_set_error(
176201
intent.IntentResponseErrorCode.UNKNOWN,
@@ -181,7 +206,7 @@ async def async_process(
181206
)
182207

183208
_LOGGER.debug("Response %s", result)
184-
response = result["choices"][0]["message"]
209+
response = result.choices[0].message.model_dump(include={"role", "content"})
185210
messages.append(response)
186211
self.history[conversation_id] = messages
187212

Diff for: homeassistant/components/openai_conversation/config_flow.py

+4-6
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
"""Config flow for OpenAI Conversation integration."""
22
from __future__ import annotations
33

4-
from functools import partial
54
import logging
65
import types
76
from types import MappingProxyType
87
from typing import Any
98

109
import openai
11-
from openai import error
1210
import voluptuous as vol
1311

1412
from homeassistant import config_entries
@@ -59,8 +57,8 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None:
5957
6058
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
6159
"""
62-
openai.api_key = data[CONF_API_KEY]
63-
await hass.async_add_executor_job(partial(openai.Model.list, request_timeout=10))
60+
client = openai.AsyncOpenAI(api_key=data[CONF_API_KEY])
61+
await hass.async_add_executor_job(client.with_options(timeout=10.0).models.list)
6462

6563

6664
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
@@ -81,9 +79,9 @@ async def async_step_user(
8179

8280
try:
8381
await validate_input(self.hass, user_input)
84-
except error.APIConnectionError:
82+
except openai.APIConnectionError:
8583
errors["base"] = "cannot_connect"
86-
except error.AuthenticationError:
84+
except openai.AuthenticationError:
8785
errors["base"] = "invalid_auth"
8886
except Exception: # pylint: disable=broad-except
8987
_LOGGER.exception("Unexpected exception")

Diff for: homeassistant/components/openai_conversation/manifest.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@
77
"documentation": "https://www.home-assistant.io/integrations/openai_conversation",
88
"integration_type": "service",
99
"iot_class": "cloud_polling",
10-
"requirements": ["openai==0.27.2"]
10+
"requirements": ["openai==1.3.8"]
1111
}

Diff for: homeassistant/components/openai_conversation/services.yaml

+24-6
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,30 @@ generate_image:
1111
text:
1212
multiline: true
1313
size:
14-
required: true
15-
example: "512"
16-
default: "512"
14+
required: false
15+
example: "1024x1024"
16+
default: "1024x1024"
17+
selector:
18+
select:
19+
options:
20+
- "1024x1024"
21+
- "1024x1792"
22+
- "1792x1024"
23+
quality:
24+
required: false
25+
example: "standard"
26+
default: "standard"
27+
selector:
28+
select:
29+
options:
30+
- "standard"
31+
- "hd"
32+
style:
33+
required: false
34+
example: "vivid"
35+
default: "vivid"
1736
selector:
1837
select:
1938
options:
20-
- "256"
21-
- "512"
22-
- "1024"
39+
- "vivid"
40+
- "natural"

Diff for: homeassistant/components/openai_conversation/strings.json

+14
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,22 @@
4343
"size": {
4444
"name": "Size",
4545
"description": "The size of the image to generate"
46+
},
47+
"quality": {
48+
"name": "Quality",
49+
"description": "The quality of the image that will be generated"
50+
},
51+
"style": {
52+
"name": "Style",
53+
"description": "The style of the generated image"
4654
}
4755
}
4856
}
57+
},
58+
"issues": {
59+
"image_size_deprecated_format": {
60+
"title": "Deprecated size format for image generation service",
61+
"description": "OpenAI is now using Dall-E 3 to generate images when calling `openai_conversation.generate_image`, which supports different sizes. Valid values are now \"1024x1024\", \"1024x1792\", \"1792x1024\". The old values of \"256\", \"512\", \"1024\" are currently interpreted as \"1024x1024\".\nPlease update your scripts or automations with the new parameters."
62+
}
4963
}
5064
}

Diff for: requirements_all.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1393,7 +1393,7 @@ open-garage==0.2.0
13931393
open-meteo==0.3.1
13941394

13951395
# homeassistant.components.openai_conversation
1396-
openai==0.27.2
1396+
openai==1.3.8
13971397

13981398
# homeassistant.components.opencv
13991399
# opencv-python-headless==4.6.0.66

Diff for: requirements_test_all.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1087,7 +1087,7 @@ open-garage==0.2.0
10871087
open-meteo==0.3.1
10881088

10891089
# homeassistant.components.openai_conversation
1090-
openai==0.27.2
1090+
openai==1.3.8
10911091

10921092
# homeassistant.components.openerz
10931093
openerz-api==0.2.0

Diff for: tests/components/openai_conversation/conftest.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def mock_config_entry(hass):
2525
async def mock_init_component(hass, mock_config_entry):
2626
"""Initialize integration."""
2727
with patch(
28-
"openai.Model.list",
28+
"openai.resources.models.AsyncModels.list",
2929
):
3030
assert await async_setup_component(hass, "openai_conversation", {})
3131
await hass.async_block_till_done()

Diff for: tests/components/openai_conversation/test_config_flow.py

+17-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
"""Test the OpenAI Conversation config flow."""
22
from unittest.mock import patch
33

4-
from openai.error import APIConnectionError, AuthenticationError, InvalidRequestError
4+
from httpx import Response
5+
from openai import APIConnectionError, AuthenticationError, BadRequestError
56
import pytest
67

78
from homeassistant import config_entries
@@ -32,7 +33,7 @@ async def test_form(hass: HomeAssistant) -> None:
3233
assert result["errors"] is None
3334

3435
with patch(
35-
"homeassistant.components.openai_conversation.config_flow.openai.Model.list",
36+
"homeassistant.components.openai_conversation.config_flow.openai.resources.models.AsyncModels.list",
3637
), patch(
3738
"homeassistant.components.openai_conversation.async_setup_entry",
3839
return_value=True,
@@ -76,9 +77,19 @@ async def test_options(
7677
@pytest.mark.parametrize(
7778
("side_effect", "error"),
7879
[
79-
(APIConnectionError(""), "cannot_connect"),
80-
(AuthenticationError, "invalid_auth"),
81-
(InvalidRequestError, "unknown"),
80+
(APIConnectionError(request=None), "cannot_connect"),
81+
(
82+
AuthenticationError(
83+
response=Response(status_code=None, request=""), body=None, message=None
84+
),
85+
"invalid_auth",
86+
),
87+
(
88+
BadRequestError(
89+
response=Response(status_code=None, request=""), body=None, message=None
90+
),
91+
"unknown",
92+
),
8293
],
8394
)
8495
async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> None:
@@ -88,7 +99,7 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non
8899
)
89100

90101
with patch(
91-
"homeassistant.components.openai_conversation.config_flow.openai.Model.list",
102+
"homeassistant.components.openai_conversation.config_flow.openai.resources.models.AsyncModels.list",
92103
side_effect=side_effect,
93104
):
94105
result2 = await hass.config_entries.flow.async_configure(

0 commit comments

Comments
 (0)