diff --git a/haystack/components/generators/chat/openai.py b/haystack/components/generators/chat/openai.py index 3547f672d04..7b03e3745f9 100644 --- a/haystack/components/generators/chat/openai.py +++ b/haystack/components/generators/chat/openai.py @@ -97,6 +97,8 @@ class OpenAIChatGenerator: ``` """ + _HAYSTACK_TO_PROVIDER_GENERATION_KWARGS: ClassVar[dict[str, str]] = {"max_output_tokens": "max_completion_tokens"} + 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..da6e4da040d 100644 --- a/haystack/components/generators/chat/openai_responses.py +++ b/haystack/components/generators/chat/openai_responses.py @@ -74,6 +74,8 @@ class OpenAIResponsesChatGenerator: ``` """ + _HAYSTACK_TO_PROVIDER_GENERATION_KWARGS: ClassVar[dict[str, str]] = {"max_output_tokens": "max_output_tokens"} + 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..937e44978c4 --- /dev/null +++ b/haystack/components/generators/chat/utils.py @@ -0,0 +1,41 @@ +# 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. +# The chosen name is based on OpenAI's Responses API. +_HAYSTACK_GENERATION_PARAMETERS = frozenset({"max_output_tokens"}) + + +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", {}) + + 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_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 new file mode 100644 index 00000000000..1661a4860d6 --- /dev/null +++ b/test/components/generators/chat/test_utils.py @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: 2022-present deepset GmbH +# +# SPDX-License-Identifier: Apache-2.0 + +from typing import ClassVar + +import pytest + +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"} == _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}) == {} + + 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})