Coverage for haystack/components/generators/chat/azure_responses.py: 96%
46 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 13:53 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 13:53 +0000
1# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
2#
3# SPDX-License-Identifier: Apache-2.0
5import os
6from collections.abc import Awaitable, Callable
7from typing import Any, ClassVar
9from openai.lib._pydantic import to_strict_json_schema
10from pydantic import BaseModel
12from haystack import component, default_from_dict, default_to_dict
13from haystack.components.generators.chat import OpenAIResponsesChatGenerator
14from haystack.dataclasses.streaming_chunk import StreamingCallbackT
15from haystack.tools import ToolsType, deserialize_tools_or_toolset_inplace, serialize_tools_or_toolset
16from haystack.utils import Secret, deserialize_callable, serialize_callable
19@component
20class AzureOpenAIResponsesChatGenerator(OpenAIResponsesChatGenerator):
21 """
22 Completes chats using OpenAI's Responses API on Azure.
24 It works with the gpt-5 and o-series models and supports streaming responses
25 from OpenAI API. It uses [ChatMessage](https://docs.haystack.deepset.ai/docs/chatmessage)
26 format in input and output.
28 You can customize how the text is generated by passing parameters to the
29 OpenAI API. Use the `**generation_kwargs` argument when you initialize
30 the component or when you run it. Any parameter that works with
31 `openai.Responses.create` will work here too.
33 For details on OpenAI API parameters, see
34 [OpenAI documentation](https://platform.openai.com/docs/api-reference/responses).
36 ### Usage example
37 <!-- test-ignore -->
38 ```python
39 from haystack.components.generators.chat import AzureOpenAIResponsesChatGenerator
40 from haystack.dataclasses import ChatMessage
42 messages = [ChatMessage.from_user("What's Natural Language Processing?")]
44 client = AzureOpenAIResponsesChatGenerator(
45 azure_endpoint="https://example-resource.azure.openai.com/",
46 generation_kwargs={"reasoning": {"effort": "low", "summary": "auto"}}
47 )
48 response = client.run(messages)
49 print(response)
50 ```
51 """
53 SUPPORTED_MODELS: ClassVar[list[str]] = [
54 "gpt-5.4-pro",
55 "gpt-5.4",
56 "gpt-5.3-chat",
57 "gpt-5.3-codex",
58 "gpt-5.2-codex",
59 "gpt-5.2",
60 "gpt-5.2-chat",
61 "gpt-5.1-codex-max",
62 "gpt-5.1",
63 "gpt-5.1-chat",
64 "gpt-5.1-codex",
65 "gpt-5.1-codex-mini",
66 "gpt-5-pro",
67 "gpt-5-codex",
68 "gpt-5",
69 "gpt-5-mini",
70 "gpt-5-nano",
71 "gpt-5-chat",
72 "gpt-4o",
73 "gpt-4o-mini",
74 "computer-use-preview",
75 "gpt-4.1",
76 "gpt-4.1-nano",
77 "gpt-4.1-mini",
78 "gpt-image-1",
79 "gpt-image-1-mini",
80 "gpt-image-1.5",
81 "o1",
82 "o3-mini",
83 "o3",
84 "o4-mini",
85 ]
86 """A non-exhaustive list of chat models supported by this component.
87 See https://learn.microsoft.com/en-us/azure/foundry/openai/how-to/responses#model-support for the full list."""
89 # ruff: noqa: PLR0913
90 def __init__(
91 self,
92 *,
93 api_key: Secret | Callable[[], str] | Callable[[], Awaitable[str]] = Secret.from_env_var(
94 "AZURE_OPENAI_API_KEY", strict=False
95 ),
96 azure_endpoint: str | None = None,
97 azure_deployment: str = "gpt-5-mini",
98 streaming_callback: StreamingCallbackT | None = None,
99 organization: str | None = None,
100 generation_kwargs: dict[str, Any] | None = None,
101 timeout: float | None = None,
102 max_retries: int | None = None,
103 tools: ToolsType | None = None,
104 tools_strict: bool = False,
105 http_client_kwargs: dict[str, Any] | None = None,
106 ) -> None:
107 """
108 Initialize the AzureOpenAIResponsesChatGenerator component.
110 :param api_key: The API key to use for authentication. Can be:
111 - A `Secret` object containing the API key.
112 - A `Secret` object containing the [Azure Active Directory token](https://www.microsoft.com/en-us/security/business/identity-access/microsoft-entra-id).
113 - A function that returns an Azure Active Directory token.
114 :param azure_endpoint: The endpoint of the deployed model, for example `"https://example-resource.azure.openai.com/"`.
115 :param azure_deployment: The deployment of the model, usually the model name.
116 :param organization: Your organization ID, defaults to `None`. For help, see
117 [Setting up your organization](https://platform.openai.com/docs/guides/production-best-practices/setting-up-your-organization).
118 :param streaming_callback: A callback function called when a new token is received from the stream.
119 It accepts [StreamingChunk](https://docs.haystack.deepset.ai/docs/data-classes#streamingchunk)
120 as an argument.
121 :param timeout: Timeout for OpenAI client calls. If not set, it defaults to either the
122 `OPENAI_TIMEOUT` environment variable, or 30 seconds.
123 :param max_retries: Maximum number of retries to contact OpenAI after an internal error.
124 If not set, it defaults to either the `OPENAI_MAX_RETRIES` environment variable, or set to 5.
125 :param generation_kwargs: Other parameters to use for the model. These parameters are sent
126 directly to the OpenAI endpoint.
127 See OpenAI [documentation](https://platform.openai.com/docs/api-reference/responses) for
128 more details.
129 Some of the supported parameters:
130 - `temperature`: What sampling temperature to use. Higher values like 0.8 will make the output more random,
131 while lower values like 0.2 will make it more focused and deterministic.
132 - `top_p`: An alternative to sampling with temperature, called nucleus sampling, where the model
133 considers the results of the tokens with top_p probability mass. For example, 0.1 means only the tokens
134 comprising the top 10% probability mass are considered.
135 - `previous_response_id`: The ID of the previous response.
136 Use this to create multi-turn conversations.
137 - `text_format`: A Pydantic model that enforces the structure of the model's response.
138 If provided, the output will always be validated against this
139 format (unless the model returns a tool call).
140 For details, see the [OpenAI Structured Outputs documentation](https://platform.openai.com/docs/guides/structured-outputs).
141 - `text`: A JSON schema that enforces the structure of the model's response.
142 If provided, the output will always be validated against this
143 format (unless the model returns a tool call).
144 Notes:
145 - Both JSON Schema and Pydantic models are supported for latest models starting from GPT-4o.
146 - If both are provided, `text_format` takes precedence and json schema passed to `text` is ignored.
147 - Currently, this component doesn't support streaming for structured outputs.
148 - Older models only support basic version of structured outputs through `{"type": "json_object"}`.
149 For detailed information on JSON mode, see the [OpenAI Structured Outputs documentation](https://platform.openai.com/docs/guides/structured-outputs#json-mode).
150 - `reasoning`: A dictionary of parameters for reasoning. For example:
151 - `summary`: The summary of the reasoning.
152 - `effort`: The level of effort to put into the reasoning. Can be `low`, `medium` or `high`.
153 - `generate_summary`: Whether to generate a summary of the reasoning.
154 Note: OpenAI does not return the reasoning tokens, but we can view summary if its enabled.
155 For details, see the [OpenAI Reasoning documentation](https://platform.openai.com/docs/guides/reasoning).
156 :param tools:
157 A list of Tool and/or Toolset objects, or a single Toolset for which the model can prepare calls.
158 :param tools_strict:
159 Whether to enable strict schema adherence for tool calls. If set to `True`, the model will follow exactly
160 the schema provided in the `parameters` field of the tool definition, but this may increase latency.
161 :param http_client_kwargs:
162 A dictionary of keyword arguments to configure a custom `httpx.Client`or `httpx.AsyncClient`.
163 For more information, see the [HTTPX documentation](https://www.python-httpx.org/api/#client).
164 """
165 azure_endpoint = azure_endpoint or os.getenv("AZURE_OPENAI_ENDPOINT")
166 if azure_endpoint is None:
167 raise ValueError(
168 "You must provide `azure_endpoint` or set the `AZURE_OPENAI_ENDPOINT` environment variable."
169 )
170 self._azure_endpoint = azure_endpoint
171 self._azure_deployment = azure_deployment
172 super(AzureOpenAIResponsesChatGenerator, self).__init__( # noqa: UP008
173 api_key=api_key, # type: ignore[arg-type]
174 model=self._azure_deployment,
175 streaming_callback=streaming_callback,
176 api_base_url=f"{self._azure_endpoint.rstrip('/')}/openai/v1",
177 organization=organization,
178 generation_kwargs=generation_kwargs,
179 timeout=timeout,
180 max_retries=max_retries,
181 tools=tools,
182 tools_strict=tools_strict,
183 http_client_kwargs=http_client_kwargs,
184 )
186 def to_dict(self) -> dict[str, Any]:
187 """
188 Serialize this component to a dictionary.
190 :returns:
191 The serialized component as a dictionary.
192 """
193 callback_name = serialize_callable(self.streaming_callback) if self.streaming_callback else None
195 # API key can be a secret or a callable
196 serialized_api_key = (
197 serialize_callable(self.api_key)
198 if callable(self.api_key)
199 else self.api_key.to_dict()
200 if isinstance(self.api_key, Secret)
201 else None
202 )
204 # If the text format is a Pydantic model, it's converted to openai's json schema format
205 # If it's already a json schema, it's left as is
206 generation_kwargs = self.generation_kwargs.copy()
207 text_format = generation_kwargs.pop("text_format", None)
208 if text_format and isinstance(text_format, type) and issubclass(text_format, BaseModel):
209 json_schema = {
210 "format": {
211 "type": "json_schema",
212 "name": text_format.__name__,
213 "strict": True,
214 "schema": to_strict_json_schema(text_format),
215 }
216 }
217 # json schema needs to be passed to text parameter instead of text_format
218 generation_kwargs["text"] = json_schema
220 # OpenAI/MCP tools are passed as list of dictionaries
221 serialized_tools: dict[str, Any] | list[dict[str, Any]] | None
222 if self.tools and isinstance(self.tools, list) and isinstance(self.tools[0], dict):
223 # mypy can't infer that self.tools is list[dict] here
224 serialized_tools = self.tools
225 else:
226 serialized_tools = serialize_tools_or_toolset(self.tools) # type: ignore[arg-type]
228 return default_to_dict(
229 self,
230 azure_endpoint=self._azure_endpoint,
231 api_key=serialized_api_key,
232 azure_deployment=self._azure_deployment,
233 streaming_callback=callback_name,
234 organization=self.organization,
235 generation_kwargs=generation_kwargs,
236 timeout=self.timeout,
237 max_retries=self.max_retries,
238 tools=serialized_tools,
239 tools_strict=self.tools_strict,
240 http_client_kwargs=self.http_client_kwargs,
241 )
243 @classmethod
244 def from_dict(cls, data: dict[str, Any]) -> "AzureOpenAIResponsesChatGenerator":
245 """
246 Deserialize this component from a dictionary.
248 :param data: The dictionary representation of this component.
249 :returns:
250 The deserialized component instance.
251 """
252 # If api_key is a str, it's a callable (Secrets are handled automatically by default_from_dict)
253 serialized_api_key = data["init_parameters"].get("api_key")
254 if isinstance(serialized_api_key, str):
255 data["init_parameters"]["api_key"] = deserialize_callable(serialized_api_key)
257 # we only deserialize the tools if they are haystack tools
258 # because openai tools are not serialized in the same way
259 tools = data["init_parameters"].get("tools")
260 if tools and (
261 isinstance(tools, dict)
262 and tools.get("type") == "haystack.tools.toolset.Toolset"
263 or isinstance(tools, list)
264 and tools[0].get("type") == "haystack.tools.tool.Tool"
265 ):
266 deserialize_tools_or_toolset_inplace(data["init_parameters"], key="tools")
268 init_params = data.get("init_parameters", {})
269 serialized_callback_handler = init_params.get("streaming_callback")
270 if serialized_callback_handler:
271 data["init_parameters"]["streaming_callback"] = deserialize_callable(serialized_callback_handler)
272 return default_from_dict(cls, data)