From 39baf35af3357446b01e5a819b8c55b1b38b45a8 Mon Sep 17 00:00:00 2001 From: Sebastian Husch Lee Date: Thu, 13 Aug 2026 08:47:06 +0200 Subject: [PATCH 1/2] Add new class variable for generation kwarg conversion and utils to use it --- haystack/components/generators/chat/openai.py | 6 +++ .../generators/chat/openai_responses.py | 6 +++ haystack/components/generators/chat/utils.py | 45 +++++++++++++++++++ test/components/generators/chat/test_azure.py | 8 +++- .../generators/chat/test_azure_responses.py | 8 +++- test/components/generators/chat/test_utils.py | 37 +++++++++++++++ 6 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 haystack/components/generators/chat/utils.py create mode 100644 test/components/generators/chat/test_utils.py diff --git a/haystack/components/generators/chat/openai.py b/haystack/components/generators/chat/openai.py index 3547f672d04..389cd8e0e83 100644 --- a/haystack/components/generators/chat/openai.py +++ b/haystack/components/generators/chat/openai.py @@ -97,6 +97,12 @@ class OpenAIChatGenerator: ``` """ + _HAYSTACK_TO_PROVIDER_GENERATION_KWARGS: ClassVar[dict[str, str]] = { + "max_output_tokens": "max_completion_tokens", + "temperature": "temperature", + "top_p": "top_p", + } + SUPPORTED_MODELS: ClassVar[list[str]] = [ "gpt-5-mini", "gpt-5-nano", diff --git a/haystack/components/generators/chat/openai_responses.py b/haystack/components/generators/chat/openai_responses.py index d0a804610bc..01058450bfa 100644 --- a/haystack/components/generators/chat/openai_responses.py +++ b/haystack/components/generators/chat/openai_responses.py @@ -74,6 +74,12 @@ class OpenAIResponsesChatGenerator: ``` """ + _HAYSTACK_TO_PROVIDER_GENERATION_KWARGS: ClassVar[dict[str, str]] = { + "max_output_tokens": "max_output_tokens", + "temperature": "temperature", + "top_p": "top_p", + } + SUPPORTED_MODELS: ClassVar[list[str]] = [ "gpt-5-mini", "gpt-5-nano", diff --git a/haystack/components/generators/chat/utils.py b/haystack/components/generators/chat/utils.py new file mode 100644 index 00000000000..00939b9cb55 --- /dev/null +++ b/haystack/components/generators/chat/utils.py @@ -0,0 +1,45 @@ +# SPDX-FileCopyrightText: 2022-present deepset GmbH +# +# SPDX-License-Identifier: Apache-2.0 + +from typing import Any + +from haystack.components.generators.chat.types import ChatGenerator + +# The provider-neutral generation parameters that Haystack components can request from Chat Generators. These names +# follow the OpenAI Responses API. +_HAYSTACK_GENERATION_PARAMETERS = frozenset({"max_output_tokens", "temperature", "top_p"}) + +_HAYSTACK_TO_PROVIDER_GENERATION_KWARGS = "_HAYSTACK_TO_PROVIDER_GENERATION_KWARGS" + + +def _convert_haystack_generation_kwargs( + chat_generator: ChatGenerator, haystack_generation_kwargs: dict[str, Any] +) -> dict[str, Any]: + """ + Convert provider-neutral Haystack generation parameters for a Chat Generator. + + Chat Generators advertise supported parameters through a private class-level mapping from the canonical Haystack + name to the provider-specific name. Parameters not advertised by the generator are omitted, allowing callers to + provide a fallback for generators that do not expose this optional capability. + + :param chat_generator: The Chat Generator that will receive the converted parameters. + :param haystack_generation_kwargs: Generation parameters using Haystack's canonical names. + :returns: The supported parameters converted to their provider-specific names. + :raises ValueError: If a parameter is not part of Haystack's canonical vocabulary. + """ + unknown_parameters = haystack_generation_kwargs.keys() - _HAYSTACK_GENERATION_PARAMETERS + if unknown_parameters: + unknown = ", ".join(sorted(unknown_parameters)) + msg = f"Unknown Haystack generation parameter(s): {unknown}" + raise ValueError(msg) + + parameter_mapping = getattr(chat_generator, _HAYSTACK_TO_PROVIDER_GENERATION_KWARGS, {}) + if not isinstance(parameter_mapping, dict): + return {} + + return { + provider_name: haystack_generation_kwargs[haystack_name] + for haystack_name, provider_name in parameter_mapping.items() + if haystack_name in haystack_generation_kwargs + } diff --git a/test/components/generators/chat/test_azure.py b/test/components/generators/chat/test_azure.py index 8ad94029c6d..7b0c1ad0193 100644 --- a/test/components/generators/chat/test_azure.py +++ b/test/components/generators/chat/test_azure.py @@ -14,7 +14,7 @@ import haystack.components.generators.chat.azure as azure_chat_module from haystack import Pipeline, component -from haystack.components.generators.chat import AzureOpenAIChatGenerator +from haystack.components.generators.chat import AzureOpenAIChatGenerator, OpenAIChatGenerator from haystack.components.generators.utils import print_streaming_chunk from haystack.dataclasses import ChatMessage, ToolCall from haystack.tools import ComponentTool, Tool @@ -78,6 +78,12 @@ def tools(): class TestAzureOpenAIChatGenerator: + def test_haystack_to_provider_generation_kwargs(self) -> None: + assert ( + AzureOpenAIChatGenerator._HAYSTACK_TO_PROVIDER_GENERATION_KWARGS + is OpenAIChatGenerator._HAYSTACK_TO_PROVIDER_GENERATION_KWARGS + ) + def test_supported_models(self) -> None: """SUPPORTED_MODELS is a non-empty list of strings.""" models = AzureOpenAIChatGenerator.SUPPORTED_MODELS diff --git a/test/components/generators/chat/test_azure_responses.py b/test/components/generators/chat/test_azure_responses.py index 5692954dd92..d0202b51477 100644 --- a/test/components/generators/chat/test_azure_responses.py +++ b/test/components/generators/chat/test_azure_responses.py @@ -11,7 +11,7 @@ from pydantic import BaseModel from haystack import Pipeline, component -from haystack.components.generators.chat import AzureOpenAIResponsesChatGenerator +from haystack.components.generators.chat import AzureOpenAIResponsesChatGenerator, OpenAIResponsesChatGenerator from haystack.components.generators.utils import print_streaming_chunk from haystack.dataclasses import ChatMessage, ToolCall from haystack.tools import ComponentTool, Tool @@ -75,6 +75,12 @@ def tools(): class TestInitialization: + def test_haystack_to_provider_generation_kwargs(self) -> None: + assert ( + AzureOpenAIResponsesChatGenerator._HAYSTACK_TO_PROVIDER_GENERATION_KWARGS + is OpenAIResponsesChatGenerator._HAYSTACK_TO_PROVIDER_GENERATION_KWARGS + ) + def test_supported_models(self) -> None: """SUPPORTED_MODELS is a non-empty list of strings.""" models = AzureOpenAIResponsesChatGenerator.SUPPORTED_MODELS diff --git a/test/components/generators/chat/test_utils.py b/test/components/generators/chat/test_utils.py new file mode 100644 index 00000000000..a53eb8dd6c7 --- /dev/null +++ b/test/components/generators/chat/test_utils.py @@ -0,0 +1,37 @@ +# SPDX-FileCopyrightText: 2022-present deepset GmbH +# +# SPDX-License-Identifier: Apache-2.0 + +import pytest + +from haystack.components.generators.chat import MockChatGenerator, OpenAIChatGenerator, OpenAIResponsesChatGenerator +from haystack.components.generators.chat.utils import ( + _HAYSTACK_GENERATION_PARAMETERS, + _convert_haystack_generation_kwargs, +) + + +class TestConvertHaystackGenerationKwargs: + def test_haystack_generation_parameters(self) -> None: + assert {"max_output_tokens", "temperature", "top_p"} == _HAYSTACK_GENERATION_PARAMETERS + + def test_openai_kwargs(self) -> None: + converted = _convert_haystack_generation_kwargs( + OpenAIChatGenerator.__new__(OpenAIChatGenerator), + {"max_output_tokens": 100, "temperature": 0.2, "top_p": 0.9}, + ) + assert converted == {"max_completion_tokens": 100, "temperature": 0.2, "top_p": 0.9} + + def test_openai_responses_kwargs(self) -> None: + converted = _convert_haystack_generation_kwargs( + OpenAIResponsesChatGenerator.__new__(OpenAIResponsesChatGenerator), + {"max_output_tokens": 100, "temperature": 0.2, "top_p": 0.9}, + ) + assert converted == {"max_output_tokens": 100, "temperature": 0.2, "top_p": 0.9} + + def test_no_mapping(self) -> None: + assert _convert_haystack_generation_kwargs(MockChatGenerator(), {"max_output_tokens": 100}) == {} + + def test_invalid_parameter(self) -> None: + with pytest.raises(ValueError, match="Unknown Haystack generation parameter\\(s\\): max_tokens"): + _convert_haystack_generation_kwargs(MockChatGenerator(), {"max_tokens": 100}) From d92b9823b5573bd37f949cb81a3fbb2ef08395ad Mon Sep 17 00:00:00 2001 From: Sebastian Husch Lee Date: Thu, 13 Aug 2026 11:16:58 +0200 Subject: [PATCH 2/2] PR comments --- haystack/components/generators/chat/openai.py | 6 +--- .../generators/chat/openai_responses.py | 6 +--- haystack/components/generators/chat/utils.py | 12 +++----- .../components/generators/chat/test_openai.py | 5 ++++ .../generators/chat/test_openai_responses.py | 5 ++++ test/components/generators/chat/test_utils.py | 28 ++++++++----------- 6 files changed, 28 insertions(+), 34 deletions(-) diff --git a/haystack/components/generators/chat/openai.py b/haystack/components/generators/chat/openai.py index 389cd8e0e83..7b03e3745f9 100644 --- a/haystack/components/generators/chat/openai.py +++ b/haystack/components/generators/chat/openai.py @@ -97,11 +97,7 @@ class OpenAIChatGenerator: ``` """ - _HAYSTACK_TO_PROVIDER_GENERATION_KWARGS: ClassVar[dict[str, str]] = { - "max_output_tokens": "max_completion_tokens", - "temperature": "temperature", - "top_p": "top_p", - } + _HAYSTACK_TO_PROVIDER_GENERATION_KWARGS: ClassVar[dict[str, str]] = {"max_output_tokens": "max_completion_tokens"} SUPPORTED_MODELS: ClassVar[list[str]] = [ "gpt-5-mini", diff --git a/haystack/components/generators/chat/openai_responses.py b/haystack/components/generators/chat/openai_responses.py index 01058450bfa..da6e4da040d 100644 --- a/haystack/components/generators/chat/openai_responses.py +++ b/haystack/components/generators/chat/openai_responses.py @@ -74,11 +74,7 @@ class OpenAIResponsesChatGenerator: ``` """ - _HAYSTACK_TO_PROVIDER_GENERATION_KWARGS: ClassVar[dict[str, str]] = { - "max_output_tokens": "max_output_tokens", - "temperature": "temperature", - "top_p": "top_p", - } + _HAYSTACK_TO_PROVIDER_GENERATION_KWARGS: ClassVar[dict[str, str]] = {"max_output_tokens": "max_output_tokens"} SUPPORTED_MODELS: ClassVar[list[str]] = [ "gpt-5-mini", diff --git a/haystack/components/generators/chat/utils.py b/haystack/components/generators/chat/utils.py index 00939b9cb55..937e44978c4 100644 --- a/haystack/components/generators/chat/utils.py +++ b/haystack/components/generators/chat/utils.py @@ -6,11 +6,9 @@ from haystack.components.generators.chat.types import ChatGenerator -# The provider-neutral generation parameters that Haystack components can request from Chat Generators. These names -# follow the OpenAI Responses API. -_HAYSTACK_GENERATION_PARAMETERS = frozenset({"max_output_tokens", "temperature", "top_p"}) - -_HAYSTACK_TO_PROVIDER_GENERATION_KWARGS = "_HAYSTACK_TO_PROVIDER_GENERATION_KWARGS" +# The provider-neutral generation parameters that Haystack components can request from Chat Generators. +# The chosen name is based on OpenAI's Responses API. +_HAYSTACK_GENERATION_PARAMETERS = frozenset({"max_output_tokens"}) def _convert_haystack_generation_kwargs( @@ -34,9 +32,7 @@ def _convert_haystack_generation_kwargs( msg = f"Unknown Haystack generation parameter(s): {unknown}" raise ValueError(msg) - parameter_mapping = getattr(chat_generator, _HAYSTACK_TO_PROVIDER_GENERATION_KWARGS, {}) - if not isinstance(parameter_mapping, dict): - return {} + parameter_mapping = getattr(chat_generator, "_HAYSTACK_TO_PROVIDER_GENERATION_KWARGS", {}) return { provider_name: haystack_generation_kwargs[haystack_name] diff --git a/test/components/generators/chat/test_openai.py b/test/components/generators/chat/test_openai.py index cd5c63d95a8..772826d9c6b 100644 --- a/test/components/generators/chat/test_openai.py +++ b/test/components/generators/chat/test_openai.py @@ -189,6 +189,11 @@ def tools(): class TestOpenAIChatGenerator: + def test_haystack_to_provider_generation_kwargs(self) -> None: + assert OpenAIChatGenerator._HAYSTACK_TO_PROVIDER_GENERATION_KWARGS == { + "max_output_tokens": "max_completion_tokens" + } + def test_supported_models(self) -> None: """SUPPORTED_MODELS is a non-empty list of strings.""" models = OpenAIChatGenerator.SUPPORTED_MODELS diff --git a/test/components/generators/chat/test_openai_responses.py b/test/components/generators/chat/test_openai_responses.py index 91a7c102221..acde77b4b6e 100644 --- a/test/components/generators/chat/test_openai_responses.py +++ b/test/components/generators/chat/test_openai_responses.py @@ -104,6 +104,11 @@ def __call__(self, chunk: StreamingChunk) -> None: class TestInitialization: + def test_haystack_to_provider_generation_kwargs(self) -> None: + assert OpenAIResponsesChatGenerator._HAYSTACK_TO_PROVIDER_GENERATION_KWARGS == { + "max_output_tokens": "max_output_tokens" + } + def test_supported_models(self) -> None: """SUPPORTED_MODELS is a non-empty list of strings.""" models = OpenAIResponsesChatGenerator.SUPPORTED_MODELS diff --git a/test/components/generators/chat/test_utils.py b/test/components/generators/chat/test_utils.py index a53eb8dd6c7..1661a4860d6 100644 --- a/test/components/generators/chat/test_utils.py +++ b/test/components/generators/chat/test_utils.py @@ -2,32 +2,28 @@ # # SPDX-License-Identifier: Apache-2.0 +from typing import ClassVar + import pytest -from haystack.components.generators.chat import MockChatGenerator, OpenAIChatGenerator, OpenAIResponsesChatGenerator +from haystack.components.generators.chat import MockChatGenerator from haystack.components.generators.chat.utils import ( _HAYSTACK_GENERATION_PARAMETERS, _convert_haystack_generation_kwargs, ) +class MappedMockChatGenerator(MockChatGenerator): + _HAYSTACK_TO_PROVIDER_GENERATION_KWARGS: ClassVar[dict[str, str]] = {"max_output_tokens": "provider_max_tokens"} + + class TestConvertHaystackGenerationKwargs: def test_haystack_generation_parameters(self) -> None: - assert {"max_output_tokens", "temperature", "top_p"} == _HAYSTACK_GENERATION_PARAMETERS - - def test_openai_kwargs(self) -> None: - converted = _convert_haystack_generation_kwargs( - OpenAIChatGenerator.__new__(OpenAIChatGenerator), - {"max_output_tokens": 100, "temperature": 0.2, "top_p": 0.9}, - ) - assert converted == {"max_completion_tokens": 100, "temperature": 0.2, "top_p": 0.9} - - def test_openai_responses_kwargs(self) -> None: - converted = _convert_haystack_generation_kwargs( - OpenAIResponsesChatGenerator.__new__(OpenAIResponsesChatGenerator), - {"max_output_tokens": 100, "temperature": 0.2, "top_p": 0.9}, - ) - assert converted == {"max_output_tokens": 100, "temperature": 0.2, "top_p": 0.9} + assert {"max_output_tokens"} == _HAYSTACK_GENERATION_PARAMETERS + + def test_conversion(self) -> None: + converted = _convert_haystack_generation_kwargs(MappedMockChatGenerator(), {"max_output_tokens": 100}) + assert converted == {"provider_max_tokens": 100} def test_no_mapping(self) -> None: assert _convert_haystack_generation_kwargs(MockChatGenerator(), {"max_output_tokens": 100}) == {}