From 5966abfb1c58b59d048b1916b493b84369d607e5 Mon Sep 17 00:00:00 2001 From: Sebastian Husch Lee Date: Tue, 17 Jun 2025 14:32:17 +0200 Subject: [PATCH 1/4] Update tests --- .../openrouter/chat/chat_generator.py | 2 + .../tests/test_openrouter_chat_generator.py | 441 +++++++++++++++--- 2 files changed, 390 insertions(+), 53 deletions(-) diff --git a/integrations/openrouter/src/haystack_integrations/components/generators/openrouter/chat/chat_generator.py b/integrations/openrouter/src/haystack_integrations/components/generators/openrouter/chat/chat_generator.py index bed6d73373..5b6ddbcda6 100644 --- a/integrations/openrouter/src/haystack_integrations/components/generators/openrouter/chat/chat_generator.py +++ b/integrations/openrouter/src/haystack_integrations/components/generators/openrouter/chat/chat_generator.py @@ -172,6 +172,8 @@ def _prepare_api_call( openai_formatted_messages = [message.to_openai_dict_format() for message in messages] tools = tools or self.tools + if isinstance(tools, Toolset): + tools = list(tools) tools_strict = tools_strict if tools_strict is not None else self.tools_strict _check_duplicate_tool_names(tools) diff --git a/integrations/openrouter/tests/test_openrouter_chat_generator.py b/integrations/openrouter/tests/test_openrouter_chat_generator.py index cbcb03160c..1c831c5379 100644 --- a/integrations/openrouter/tests/test_openrouter_chat_generator.py +++ b/integrations/openrouter/tests/test_openrouter_chat_generator.py @@ -11,12 +11,27 @@ from haystack.tools import Tool from haystack.utils.auth import Secret from openai import OpenAIError -from openai.types.chat import ChatCompletion, ChatCompletionMessage +from openai.types.chat import ChatCompletion, ChatCompletionChunk, ChatCompletionMessage from openai.types.chat.chat_completion import Choice +from openai.types.chat.chat_completion_chunk import Choice as ChoiceChunk +from openai.types.chat.chat_completion_chunk import ChoiceDelta, ChoiceDeltaToolCall, ChoiceDeltaToolCallFunction +from openai.types.completion_usage import CompletionTokensDetails, CompletionUsage, PromptTokensDetails from haystack_integrations.components.generators.openrouter.chat.chat_generator import OpenRouterChatGenerator +class CollectorCallback: + """ + Callback to collect streaming chunks for testing purposes. + """ + + def __init__(self): + self.chunks = [] + + def __call__(self, chunk: StreamingChunk) -> None: + self.chunks.append(chunk) + + @pytest.fixture def chat_messages(): return [ @@ -211,6 +226,340 @@ def test_from_dict_fail_wo_env_var(self, monkeypatch): with pytest.raises(ValueError, match="None of the .* environment variables are set"): OpenRouterChatGenerator.from_dict(data) + def test_handle_stream_response(self): + openrouter_chunks = [ + ChatCompletionChunk( + id="gen-1750162525-tc7ParBHvsqd6rYhCDtK", + choices=[ + ChoiceChunk(delta=ChoiceDelta(content="", role="assistant"), index=0, native_finish_reason=None) + ], + created=1750162525, + model="openai/gpt-4o-mini", + object="chat.completion.chunk", + system_fingerprint="fp_34a54ae93c", + provider="OpenAI", + ), + ChatCompletionChunk( + id="gen-1750162525-tc7ParBHvsqd6rYhCDtK", + choices=[ + ChoiceChunk( + delta=ChoiceDelta( + role="assistant", + tool_calls=[ + ChoiceDeltaToolCall( + index=0, + id="call_zznlVyVfK0GJwY28SShJpDCh", + function=ChoiceDeltaToolCallFunction(arguments="", name="weather"), + type="function", + ) + ], + ), + index=0, + native_finish_reason=None, + ) + ], + created=1750162525, + model="openai/gpt-4o-mini", + object="chat.completion.chunk", + system_fingerprint="fp_34a54ae93c", + provider="OpenAI", + ), + ChatCompletionChunk( + id="gen-1750162525-tc7ParBHvsqd6rYhCDtK", + choices=[ + ChoiceChunk( + delta=ChoiceDelta( + role="assistant", + tool_calls=[ + ChoiceDeltaToolCall( + index=0, + function=ChoiceDeltaToolCallFunction(arguments='{"ci'), + type="function", + ) + ], + ), + index=0, + native_finish_reason=None, + ) + ], + created=1750162525, + model="openai/gpt-4o-mini", + object="chat.completion.chunk", + system_fingerprint="fp_34a54ae93c", + provider="OpenAI", + ), + ChatCompletionChunk( + id="gen-1750162525-tc7ParBHvsqd6rYhCDtK", + choices=[ + ChoiceChunk( + delta=ChoiceDelta( + role="assistant", + tool_calls=[ + ChoiceDeltaToolCall( + index=0, + function=ChoiceDeltaToolCallFunction(arguments='ty": '), + type="function", + ) + ], + ), + index=0, + native_finish_reason=None, + ) + ], + created=1750162525, + model="openai/gpt-4o-mini", + object="chat.completion.chunk", + system_fingerprint="fp_34a54ae93c", + provider="OpenAI", + ), + ChatCompletionChunk( + id="gen-1750162525-tc7ParBHvsqd6rYhCDtK", + choices=[ + ChoiceChunk( + delta=ChoiceDelta( + role="assistant", + tool_calls=[ + ChoiceDeltaToolCall( + index=0, + function=ChoiceDeltaToolCallFunction(arguments='"Paris'), + type="function", + ) + ], + ), + index=0, + native_finish_reason=None, + ) + ], + created=1750162525, + model="openai/gpt-4o-mini", + object="chat.completion.chunk", + system_fingerprint="fp_34a54ae93c", + provider="OpenAI", + ), + ChatCompletionChunk( + id="gen-1750162525-tc7ParBHvsqd6rYhCDtK", + choices=[ + ChoiceChunk( + delta=ChoiceDelta( + role="assistant", + tool_calls=[ + ChoiceDeltaToolCall( + index=0, + function=ChoiceDeltaToolCallFunction(arguments='"}'), + type="function", + ) + ], + ), + index=0, + native_finish_reason=None, + ) + ], + created=1750162525, + model="openai/gpt-4o-mini", + object="chat.completion.chunk", + system_fingerprint="fp_34a54ae93c", + provider="OpenAI", + ), + ChatCompletionChunk( + id="gen-1750162525-tc7ParBHvsqd6rYhCDtK", + choices=[ + ChoiceChunk( + delta=ChoiceDelta( + role="assistant", + tool_calls=[ + ChoiceDeltaToolCall( + index=1, + id="call_Mh1uOyW3Ys4gwydHjNHILHGX", + function=ChoiceDeltaToolCallFunction(arguments="", name="weather"), + type="function", + ) + ], + ), + index=0, + native_finish_reason=None, + ) + ], + created=1750162525, + model="openai/gpt-4o-mini", + object="chat.completion.chunk", + service_tier=None, + system_fingerprint="fp_34a54ae93c", + usage=None, + provider="OpenAI", + ), + ChatCompletionChunk( + id="gen-1750162525-tc7ParBHvsqd6rYhCDtK", + choices=[ + ChoiceChunk( + delta=ChoiceDelta( + role="assistant", + tool_calls=[ + ChoiceDeltaToolCall( + index=1, + id=None, + function=ChoiceDeltaToolCallFunction(arguments='{"ci'), + type="function", + ) + ], + ), + index=0, + native_finish_reason=None, + ) + ], + created=1750162525, + model="openai/gpt-4o-mini", + object="chat.completion.chunk", + system_fingerprint="fp_34a54ae93c", + provider="OpenAI", + ), + ChatCompletionChunk( + id="gen-1750162525-tc7ParBHvsqd6rYhCDtK", + choices=[ + ChoiceChunk( + delta=ChoiceDelta( + role="assistant", + tool_calls=[ + ChoiceDeltaToolCall( + index=1, + function=ChoiceDeltaToolCallFunction(arguments='ty": '), + type="function", + ) + ], + ), + index=0, + native_finish_reason=None, + ) + ], + created=1750162525, + model="openai/gpt-4o-mini", + object="chat.completion.chunk", + system_fingerprint="fp_34a54ae93c", + provider="OpenAI", + ), + ChatCompletionChunk( + id="gen-1750162525-tc7ParBHvsqd6rYhCDtK", + choices=[ + ChoiceChunk( + delta=ChoiceDelta( + role="assistant", + tool_calls=[ + ChoiceDeltaToolCall( + index=1, + function=ChoiceDeltaToolCallFunction(arguments='"Berli'), + type="function", + ) + ], + ), + index=0, + native_finish_reason=None, + ) + ], + created=1750162525, + model="openai/gpt-4o-mini", + object="chat.completion.chunk", + system_fingerprint="fp_34a54ae93c", + provider="OpenAI", + ), + ChatCompletionChunk( + id="gen-1750162525-tc7ParBHvsqd6rYhCDtK", + choices=[ + ChoiceChunk( + delta=ChoiceDelta( + role="assistant", + tool_calls=[ + ChoiceDeltaToolCall( + index=1, + function=ChoiceDeltaToolCallFunction(arguments='n"}'), + type="function", + ) + ], + ), + index=0, + native_finish_reason=None, + ) + ], + created=1750162525, + model="openai/gpt-4o-mini", + object="chat.completion.chunk", + system_fingerprint="fp_34a54ae93c", + provider="OpenAI", + ), + ChatCompletionChunk( + id="gen-1750162525-tc7ParBHvsqd6rYhCDtK", + choices=[ + ChoiceChunk( + delta=ChoiceDelta(content="", role="assistant"), + finish_reason="tool_calls", + index=0, + native_finish_reason="tool_calls", + ) + ], + created=1750162525, + model="openai/gpt-4o-mini", + object="chat.completion.chunk", + system_fingerprint="fp_34a54ae93c", + provider="OpenAI", + ), + ChatCompletionChunk( + id="gen-1750162525-tc7ParBHvsqd6rYhCDtK", + choices=[ + ChoiceChunk( + delta=ChoiceDelta(content="", role="assistant"), + index=0, + native_finish_reason=None, + ) + ], + created=1750162525, + model="openai/gpt-4o-mini", + object="chat.completion.chunk", + usage=CompletionUsage( + completion_tokens=42, + prompt_tokens=55, + total_tokens=97, + completion_tokens_details=CompletionTokensDetails(reasoning_tokens=0), + prompt_tokens_details=PromptTokensDetails(cached_tokens=0), + ), + provider="OpenAI", + ), + ] + + collector_callback = CollectorCallback() + llm = OpenRouterChatGenerator(api_key=Secret.from_token("test-api-key")) + result = llm._handle_stream_response(openrouter_chunks, callback=collector_callback)[0] # type: ignore + + # Assert text is empty + assert result.text is None + + # Verify both tool calls were found and processed + assert len(result.tool_calls) == 2 + assert result.tool_calls[0].id == "call_zznlVyVfK0GJwY28SShJpDCh" + assert result.tool_calls[0].tool_name == "weather" + assert result.tool_calls[0].arguments == {"city": "Paris"} + assert result.tool_calls[1].id == "call_Mh1uOyW3Ys4gwydHjNHILHGX" + assert result.tool_calls[1].tool_name == "weather" + assert result.tool_calls[1].arguments == {"city": "Berlin"} + + # Verify meta information + assert result.meta["model"] == "openai/gpt-4o-mini" + assert result.meta["finish_reason"] == "tool_calls" + assert result.meta["index"] == 0 + assert result.meta["completion_start_time"] is not None + assert result.meta["usage"] == { + "completion_tokens": 42, + "prompt_tokens": 55, + "total_tokens": 97, + "completion_tokens_details": { + "accepted_prediction_tokens": None, + "audio_tokens": None, + "reasoning_tokens": 0, + "rejected_prediction_tokens": None, + }, + "prompt_tokens_details": { + "audio_tokens": None, + "cached_tokens": 0, + }, + } + def test_run(self, chat_messages, mock_chat_completion, monkeypatch): # noqa: ARG002 monkeypatch.setenv("OPENROUTER_API_KEY", "fake-api-key") component = OpenRouterChatGenerator() @@ -323,42 +672,44 @@ def test_live_run_with_tools_and_response(self, tools): """ Integration test that the MistralChatGenerator component can run with tools and get a response. """ - initial_messages = [ChatMessage.from_user("What's the weather like in Paris?")] + initial_messages = [ChatMessage.from_user("What's the weather like in Paris and Berlin?")] component = OpenRouterChatGenerator(tools=tools) results = component.run(messages=initial_messages, generation_kwargs={"tool_choice": "auto"}) - assert len(results["replies"]) > 0, "No replies received" + assert len(results["replies"]) == 1 # Find the message with tool calls - tool_message = None - for message in results["replies"]: - if message.tool_call: - tool_message = message - break - - assert tool_message is not None, "No message with tool call found" - assert isinstance(tool_message, ChatMessage), "Tool message is not a ChatMessage instance" - assert ChatMessage.is_from(tool_message, ChatRole.ASSISTANT), "Tool message is not from the assistant" - - tool_call = tool_message.tool_call - assert tool_call.id, "Tool call does not contain value for 'id' key" - assert tool_call.tool_name == "weather" - assert tool_call.arguments == {"city": "Paris"} + tool_message = results["replies"][0] + + assert isinstance(tool_message, ChatMessage) + tool_calls = tool_message.tool_calls + assert len(tool_calls) == 2 + assert ChatMessage.is_from(tool_message, ChatRole.ASSISTANT) + + for tool_call in tool_calls: + assert tool_call.id is not None + assert isinstance(tool_call, ToolCall) + assert tool_call.tool_name == "weather" + + arguments = [tool_call.arguments for tool_call in tool_calls] + assert sorted(arguments, key=lambda x: x["city"]) == [{"city": "Berlin"}, {"city": "Paris"}] assert tool_message.meta["finish_reason"] == "tool_calls" new_messages = [ initial_messages[0], tool_message, - ChatMessage.from_tool(tool_result="22° C", origin=tool_call), + ChatMessage.from_tool(tool_result="22° C and sunny", origin=tool_calls[0]), + ChatMessage.from_tool(tool_result="16° C and windy", origin=tool_calls[1]), ] # Pass the tool result to the model to get the final response results = component.run(new_messages) assert len(results["replies"]) == 1 final_message = results["replies"][0] - assert not final_message.tool_call + assert final_message.is_from(ChatRole.ASSISTANT) assert len(final_message.text) > 0 assert "paris" in final_message.text.lower() + assert "berlin" in final_message.text.lower() @pytest.mark.skipif( not os.environ.get("OPENROUTER_API_KEY", None), @@ -369,45 +720,29 @@ def test_live_run_with_tools_streaming(self, tools): """ Integration test that the OpenRouterChatGenerator component can run with tools and streaming. """ - - class Callback: - def __init__(self): - self.responses = "" - self.counter = 0 - self.tool_calls = [] - - def __call__(self, chunk: StreamingChunk) -> None: - self.counter += 1 - if chunk.content: - self.responses += chunk.content - if chunk.meta.get("tool_calls"): - self.tool_calls.extend(chunk.meta["tool_calls"]) - - callback = Callback() - component = OpenRouterChatGenerator(tools=tools, streaming_callback=callback) + component = OpenRouterChatGenerator(tools=tools, streaming_callback=print_streaming_chunk) results = component.run( - [ChatMessage.from_user("What's the weather like in Paris?")], generation_kwargs={"tool_choice": "auto"} + [ChatMessage.from_user("What's the weather like in Paris and Berlin?")], + generation_kwargs={"tool_choice": "auto"}, ) - assert len(results["replies"]) > 0, "No replies received" - assert callback.counter > 1, "Streaming callback was not called multiple times" - assert callback.tool_calls, "No tool calls received in streaming" + assert len(results["replies"]) == 1 # Find the message with tool calls - tool_message = None - for message in results["replies"]: - if message.tool_call: - tool_message = message - break - - assert tool_message is not None, "No message with tool call found" - assert isinstance(tool_message, ChatMessage), "Tool message is not a ChatMessage instance" - assert ChatMessage.is_from(tool_message, ChatRole.ASSISTANT), "Tool message is not from the assistant" - - tool_call = tool_message.tool_call - assert tool_call.id, "Tool call does not contain value for 'id' key" - assert tool_call.tool_name == "weather" - assert tool_call.arguments == {"city": "Paris"} + tool_message = results["replies"][0] + + assert isinstance(tool_message, ChatMessage) + tool_calls = tool_message.tool_calls + assert len(tool_calls) == 2 + assert ChatMessage.is_from(tool_message, ChatRole.ASSISTANT) + + for tool_call in tool_calls: + assert tool_call.id is not None + assert isinstance(tool_call, ToolCall) + assert tool_call.tool_name == "weather" + + arguments = [tool_call.arguments for tool_call in tool_calls] + assert sorted(arguments, key=lambda x: x["city"]) == [{"city": "Berlin"}, {"city": "Paris"}] assert tool_message.meta["finish_reason"] == "tool_calls" @pytest.mark.skipif( From 6e70fd6acc32838833f10a3252d78377fd3db24e Mon Sep 17 00:00:00 2001 From: Sebastian Husch Lee Date: Wed, 18 Jun 2025 13:33:15 +0200 Subject: [PATCH 2/4] Refactor tests --- .../tests/test_openrouter_chat_generator.py | 810 +++++++++--------- 1 file changed, 406 insertions(+), 404 deletions(-) diff --git a/integrations/openrouter/tests/test_openrouter_chat_generator.py b/integrations/openrouter/tests/test_openrouter_chat_generator.py index 1c831c5379..76fc0c23e7 100644 --- a/integrations/openrouter/tests/test_openrouter_chat_generator.py +++ b/integrations/openrouter/tests/test_openrouter_chat_generator.py @@ -226,6 +226,321 @@ def test_from_dict_fail_wo_env_var(self, monkeypatch): with pytest.raises(ValueError, match="None of the .* environment variables are set"): OpenRouterChatGenerator.from_dict(data) + def test_run(self, chat_messages, mock_chat_completion, monkeypatch): # noqa: ARG002 + monkeypatch.setenv("OPENROUTER_API_KEY", "fake-api-key") + component = OpenRouterChatGenerator() + response = component.run(chat_messages) + + # check that the component returns the correct ChatMessage response + assert isinstance(response, dict) + assert "replies" in response + assert isinstance(response["replies"], list) + assert len(response["replies"]) == 1 + assert [isinstance(reply, ChatMessage) for reply in response["replies"]] + + def test_run_with_params(self, chat_messages, mock_chat_completion, monkeypatch): + monkeypatch.setenv("OPENROUTER_API_KEY", "fake-api-key") + component = OpenRouterChatGenerator(generation_kwargs={"max_tokens": 10, "temperature": 0.5}) + response = component.run(chat_messages) + + # check that the component calls the OpenAI API with the correct parameters + # for OpenRouter, these are passed in the extra_body parameter + _, kwargs = mock_chat_completion.call_args + assert kwargs["extra_body"]["max_tokens"] == 10 + assert kwargs["extra_body"]["temperature"] == 0.5 + # check that the component returns the correct response + assert isinstance(response, dict) + assert "replies" in response + assert isinstance(response["replies"], list) + assert len(response["replies"]) == 1 + assert [isinstance(reply, ChatMessage) for reply in response["replies"]] + + @pytest.mark.skipif( + not os.environ.get("OPENROUTER_API_KEY", None), + reason="Export an env var called OPENROUTER_API_KEY containing the OpenRouter API key to run this test.", + ) + @pytest.mark.integration + def test_live_run(self): + chat_messages = [ChatMessage.from_user("What's the capital of France")] + component = OpenRouterChatGenerator() + results = component.run(chat_messages) + assert len(results["replies"]) == 1 + message: ChatMessage = results["replies"][0] + assert "Paris" in message.text + assert "openai/gpt-4o-mini" in message.meta["model"] + assert message.meta["finish_reason"] == "stop" + + @pytest.mark.skipif( + not os.environ.get("OPENROUTER_API_KEY", None), + reason="Export an env var called OPENROUTER_API_KEY containing the OpenAI API key to run this test.", + ) + @pytest.mark.integration + def test_live_run_wrong_model(self, chat_messages): + component = OpenRouterChatGenerator(model="something-obviously-wrong") + with pytest.raises(OpenAIError): + component.run(chat_messages) + + @pytest.mark.skipif( + not os.environ.get("OPENROUTER_API_KEY", None), + reason="Export an env var called OPENROUTER_API_KEY containing the OpenAI API key to run this test.", + ) + @pytest.mark.integration + def test_live_run_streaming(self): + class Callback: + def __init__(self): + self.responses = "" + self.counter = 0 + + def __call__(self, chunk: StreamingChunk) -> None: + self.counter += 1 + self.responses += chunk.content if chunk.content else "" + + callback = Callback() + component = OpenRouterChatGenerator(streaming_callback=callback) + results = component.run([ChatMessage.from_user("What's the capital of France?")]) + + assert len(results["replies"]) == 1 + message: ChatMessage = results["replies"][0] + assert "Paris" in message.text + + assert "openai/gpt-4o-mini" in message.meta["model"] + assert message.meta["finish_reason"] == "stop" + + assert callback.counter > 1 + assert "Paris" in callback.responses + + @pytest.mark.skipif( + not os.environ.get("OPENROUTER_API_KEY", None), + reason="Export an env var called OPENROUTER_API_KEY containing the OpenAI API key to run this test.", + ) + @pytest.mark.integration + def test_live_run_with_tools(self, tools): + chat_messages = [ChatMessage.from_user("What's the weather like in Paris?")] + component = OpenRouterChatGenerator(tools=tools) + results = component.run(chat_messages) + assert len(results["replies"]) == 1 + message = results["replies"][0] + assert message.text == "" + + assert message.tool_calls + tool_call = message.tool_call + assert isinstance(tool_call, ToolCall) + assert tool_call.tool_name == "weather" + assert tool_call.arguments == {"city": "Paris"} + assert message.meta["finish_reason"] == "tool_calls" + + @pytest.mark.skipif( + not os.environ.get("OPENROUTER_API_KEY", None), + reason="Export an env var called OPENROUTER_API_KEY containing the OpenAI API key to run this test.", + ) + @pytest.mark.integration + def test_live_run_with_tools_and_response(self, tools): + """ + Integration test that the MistralChatGenerator component can run with tools and get a response. + """ + initial_messages = [ChatMessage.from_user("What's the weather like in Paris and Berlin?")] + component = OpenRouterChatGenerator(tools=tools) + results = component.run(messages=initial_messages, generation_kwargs={"tool_choice": "auto"}) + + assert len(results["replies"]) == 1 + + # Find the message with tool calls + tool_message = results["replies"][0] + + assert isinstance(tool_message, ChatMessage) + tool_calls = tool_message.tool_calls + assert len(tool_calls) == 2 + assert ChatMessage.is_from(tool_message, ChatRole.ASSISTANT) + + for tool_call in tool_calls: + assert tool_call.id is not None + assert isinstance(tool_call, ToolCall) + assert tool_call.tool_name == "weather" + + arguments = [tool_call.arguments for tool_call in tool_calls] + assert sorted(arguments, key=lambda x: x["city"]) == [{"city": "Berlin"}, {"city": "Paris"}] + assert tool_message.meta["finish_reason"] == "tool_calls" + + new_messages = [ + initial_messages[0], + tool_message, + ChatMessage.from_tool(tool_result="22° C and sunny", origin=tool_calls[0]), + ChatMessage.from_tool(tool_result="16° C and windy", origin=tool_calls[1]), + ] + # Pass the tool result to the model to get the final response + results = component.run(new_messages) + + assert len(results["replies"]) == 1 + final_message = results["replies"][0] + assert final_message.is_from(ChatRole.ASSISTANT) + assert len(final_message.text) > 0 + assert "paris" in final_message.text.lower() + assert "berlin" in final_message.text.lower() + + @pytest.mark.skipif( + not os.environ.get("OPENROUTER_API_KEY", None), + reason="Export an env var called OPENROUTER_API_KEY containing the OpenAI API key to run this test.", + ) + @pytest.mark.integration + def test_live_run_with_tools_streaming(self, tools): + """ + Integration test that the OpenRouterChatGenerator component can run with tools and streaming. + """ + component = OpenRouterChatGenerator(tools=tools, streaming_callback=print_streaming_chunk) + results = component.run( + [ChatMessage.from_user("What's the weather like in Paris and Berlin?")], + generation_kwargs={"tool_choice": "auto"}, + ) + + assert len(results["replies"]) == 1 + + # Find the message with tool calls + tool_message = results["replies"][0] + + assert isinstance(tool_message, ChatMessage) + tool_calls = tool_message.tool_calls + assert len(tool_calls) == 2 + assert ChatMessage.is_from(tool_message, ChatRole.ASSISTANT) + + for tool_call in tool_calls: + assert tool_call.id is not None + assert isinstance(tool_call, ToolCall) + assert tool_call.tool_name == "weather" + + arguments = [tool_call.arguments for tool_call in tool_calls] + assert sorted(arguments, key=lambda x: x["city"]) == [{"city": "Berlin"}, {"city": "Paris"}] + assert tool_message.meta["finish_reason"] == "tool_calls" + + @pytest.mark.skipif( + not os.environ.get("OPENROUTER_API_KEY", None), + reason="Export an env var called OPENROUTER_API_KEY containing the OpenAI API key to run this test.", + ) + @pytest.mark.integration + def test_pipeline_with_openrouter_chat_generator(self, tools): + """ + Test that the MistralChatGenerator component can be used in a pipeline + """ + pipeline = Pipeline() + pipeline.add_component("generator", OpenRouterChatGenerator(tools=tools)) + pipeline.add_component("tool_invoker", ToolInvoker(tools=tools)) + + pipeline.connect("generator", "tool_invoker") + + results = pipeline.run( + data={ + "generator": { + "messages": [ChatMessage.from_user("What's the weather like in Paris?")], + "generation_kwargs": {"tool_choice": "auto"}, + } + } + ) + + assert ( + "The weather in Paris is sunny and 32°C" + == results["tool_invoker"]["tool_messages"][0].tool_call_result.result + ) + + def test_serde_in_pipeline(self, monkeypatch): + """ + Test serialization/deserialization of OpenRouterChatGenerator in a Pipeline, + including YAML conversion and detailed dictionary validation + """ + # Set mock API key + monkeypatch.setenv("OPENROUTER_API_KEY", "test-key") + + # Create a test tool + tool = Tool( + name="weather", + description="useful to determine the weather in a given location", + parameters={"city": {"type": "string"}}, + function=weather, + ) + + # Create generator with specific configuration + generator = OpenRouterChatGenerator( + model="openai/gpt-4o-mini", + generation_kwargs={"temperature": 0.7}, + streaming_callback=print_streaming_chunk, + tools=[tool], + ) + + # Create and configure pipeline + pipeline = Pipeline() + pipeline.add_component("generator", generator) + + # Get pipeline dictionary and verify its structure + pipeline_dict = pipeline.to_dict() + expected_dict = { + "metadata": {}, + "max_runs_per_component": 100, + "connection_type_validation": True, + "components": { + "generator": { + "type": "haystack_integrations.components.generators.openrouter.chat.chat_generator.OpenRouterChatGenerator", # noqa: E501 + "init_parameters": { + "api_key": {"type": "env_var", "env_vars": ["OPENROUTER_API_KEY"], "strict": True}, + "model": "openai/gpt-4o-mini", + "streaming_callback": "haystack.components.generators.utils.print_streaming_chunk", + "api_base_url": "https://openrouter.ai/api/v1", + "generation_kwargs": {"temperature": 0.7}, + "tools": [ + { + "type": "haystack.tools.tool.Tool", + "data": { + "name": "weather", + "description": "useful to determine the weather in a given location", + "parameters": {"city": {"type": "string"}}, + "function": "tests.test_openrouter_chat_generator.weather", + }, + } + ], + "http_client_kwargs": None, + "extra_headers": None, + "timeout": None, + "max_retries": None, + }, + } + }, + "connections": [], + } + + if not hasattr(pipeline, "_connection_type_validation"): + expected_dict.pop("connection_type_validation") + + # add outputs_to_string, inputs_from_state and outputs_to_state tool parameters for compatibility with + # haystack-ai>=2.12.0 + if hasattr(tool, "outputs_to_string"): + expected_dict["components"]["generator"]["init_parameters"]["tools"][0]["data"]["outputs_to_string"] = ( + tool.outputs_to_string + ) + if hasattr(tool, "inputs_from_state"): + expected_dict["components"]["generator"]["init_parameters"]["tools"][0]["data"]["inputs_from_state"] = ( + tool.inputs_from_state + ) + if hasattr(tool, "outputs_to_state"): + expected_dict["components"]["generator"]["init_parameters"]["tools"][0]["data"]["outputs_to_state"] = ( + tool.outputs_to_state + ) + + assert pipeline_dict == expected_dict + + # Test YAML serialization/deserialization + pipeline_yaml = pipeline.dumps() + new_pipeline = Pipeline.loads(pipeline_yaml) + assert new_pipeline == pipeline + + # Verify the loaded pipeline's generator has the same configuration + loaded_generator = new_pipeline.get_component("generator") + assert loaded_generator.model == generator.model + assert loaded_generator.generation_kwargs == generator.generation_kwargs + assert loaded_generator.streaming_callback == generator.streaming_callback + assert len(loaded_generator.tools) == len(generator.tools) + assert loaded_generator.tools[0].name == generator.tools[0].name + assert loaded_generator.tools[0].description == generator.tools[0].description + assert loaded_generator.tools[0].parameters == generator.tools[0].parameters + + +class TestChatCompletionChunkConversion: def test_handle_stream_response(self): openrouter_chunks = [ ChatCompletionChunk( @@ -462,413 +777,100 @@ def test_handle_stream_response(self): ), ChatCompletionChunk( id="gen-1750162525-tc7ParBHvsqd6rYhCDtK", - choices=[ - ChoiceChunk( - delta=ChoiceDelta( - role="assistant", - tool_calls=[ - ChoiceDeltaToolCall( - index=1, - function=ChoiceDeltaToolCallFunction(arguments='n"}'), - type="function", - ) - ], - ), - index=0, - native_finish_reason=None, - ) - ], - created=1750162525, - model="openai/gpt-4o-mini", - object="chat.completion.chunk", - system_fingerprint="fp_34a54ae93c", - provider="OpenAI", - ), - ChatCompletionChunk( - id="gen-1750162525-tc7ParBHvsqd6rYhCDtK", - choices=[ - ChoiceChunk( - delta=ChoiceDelta(content="", role="assistant"), - finish_reason="tool_calls", - index=0, - native_finish_reason="tool_calls", - ) - ], - created=1750162525, - model="openai/gpt-4o-mini", - object="chat.completion.chunk", - system_fingerprint="fp_34a54ae93c", - provider="OpenAI", - ), - ChatCompletionChunk( - id="gen-1750162525-tc7ParBHvsqd6rYhCDtK", - choices=[ - ChoiceChunk( - delta=ChoiceDelta(content="", role="assistant"), - index=0, - native_finish_reason=None, - ) - ], - created=1750162525, - model="openai/gpt-4o-mini", - object="chat.completion.chunk", - usage=CompletionUsage( - completion_tokens=42, - prompt_tokens=55, - total_tokens=97, - completion_tokens_details=CompletionTokensDetails(reasoning_tokens=0), - prompt_tokens_details=PromptTokensDetails(cached_tokens=0), - ), - provider="OpenAI", - ), - ] - - collector_callback = CollectorCallback() - llm = OpenRouterChatGenerator(api_key=Secret.from_token("test-api-key")) - result = llm._handle_stream_response(openrouter_chunks, callback=collector_callback)[0] # type: ignore - - # Assert text is empty - assert result.text is None - - # Verify both tool calls were found and processed - assert len(result.tool_calls) == 2 - assert result.tool_calls[0].id == "call_zznlVyVfK0GJwY28SShJpDCh" - assert result.tool_calls[0].tool_name == "weather" - assert result.tool_calls[0].arguments == {"city": "Paris"} - assert result.tool_calls[1].id == "call_Mh1uOyW3Ys4gwydHjNHILHGX" - assert result.tool_calls[1].tool_name == "weather" - assert result.tool_calls[1].arguments == {"city": "Berlin"} - - # Verify meta information - assert result.meta["model"] == "openai/gpt-4o-mini" - assert result.meta["finish_reason"] == "tool_calls" - assert result.meta["index"] == 0 - assert result.meta["completion_start_time"] is not None - assert result.meta["usage"] == { - "completion_tokens": 42, - "prompt_tokens": 55, - "total_tokens": 97, - "completion_tokens_details": { - "accepted_prediction_tokens": None, - "audio_tokens": None, - "reasoning_tokens": 0, - "rejected_prediction_tokens": None, - }, - "prompt_tokens_details": { - "audio_tokens": None, - "cached_tokens": 0, - }, - } - - def test_run(self, chat_messages, mock_chat_completion, monkeypatch): # noqa: ARG002 - monkeypatch.setenv("OPENROUTER_API_KEY", "fake-api-key") - component = OpenRouterChatGenerator() - response = component.run(chat_messages) - - # check that the component returns the correct ChatMessage response - assert isinstance(response, dict) - assert "replies" in response - assert isinstance(response["replies"], list) - assert len(response["replies"]) == 1 - assert [isinstance(reply, ChatMessage) for reply in response["replies"]] - - def test_run_with_params(self, chat_messages, mock_chat_completion, monkeypatch): - monkeypatch.setenv("OPENROUTER_API_KEY", "fake-api-key") - component = OpenRouterChatGenerator(generation_kwargs={"max_tokens": 10, "temperature": 0.5}) - response = component.run(chat_messages) - - # check that the component calls the OpenAI API with the correct parameters - # for OpenRouter, these are passed in the extra_body parameter - _, kwargs = mock_chat_completion.call_args - assert kwargs["extra_body"]["max_tokens"] == 10 - assert kwargs["extra_body"]["temperature"] == 0.5 - # check that the component returns the correct response - assert isinstance(response, dict) - assert "replies" in response - assert isinstance(response["replies"], list) - assert len(response["replies"]) == 1 - assert [isinstance(reply, ChatMessage) for reply in response["replies"]] - - @pytest.mark.skipif( - not os.environ.get("OPENROUTER_API_KEY", None), - reason="Export an env var called OPENROUTER_API_KEY containing the OpenRouter API key to run this test.", - ) - @pytest.mark.integration - def test_live_run(self): - chat_messages = [ChatMessage.from_user("What's the capital of France")] - component = OpenRouterChatGenerator() - results = component.run(chat_messages) - assert len(results["replies"]) == 1 - message: ChatMessage = results["replies"][0] - assert "Paris" in message.text - assert "openai/gpt-4o-mini" in message.meta["model"] - assert message.meta["finish_reason"] == "stop" - - @pytest.mark.skipif( - not os.environ.get("OPENROUTER_API_KEY", None), - reason="Export an env var called OPENROUTER_API_KEY containing the OpenAI API key to run this test.", - ) - @pytest.mark.integration - def test_live_run_wrong_model(self, chat_messages): - component = OpenRouterChatGenerator(model="something-obviously-wrong") - with pytest.raises(OpenAIError): - component.run(chat_messages) - - @pytest.mark.skipif( - not os.environ.get("OPENROUTER_API_KEY", None), - reason="Export an env var called OPENROUTER_API_KEY containing the OpenAI API key to run this test.", - ) - @pytest.mark.integration - def test_live_run_streaming(self): - class Callback: - def __init__(self): - self.responses = "" - self.counter = 0 - - def __call__(self, chunk: StreamingChunk) -> None: - self.counter += 1 - self.responses += chunk.content if chunk.content else "" - - callback = Callback() - component = OpenRouterChatGenerator(streaming_callback=callback) - results = component.run([ChatMessage.from_user("What's the capital of France?")]) - - assert len(results["replies"]) == 1 - message: ChatMessage = results["replies"][0] - assert "Paris" in message.text - - assert "openai/gpt-4o-mini" in message.meta["model"] - assert message.meta["finish_reason"] == "stop" - - assert callback.counter > 1 - assert "Paris" in callback.responses - - @pytest.mark.skipif( - not os.environ.get("OPENROUTER_API_KEY", None), - reason="Export an env var called OPENROUTER_API_KEY containing the OpenAI API key to run this test.", - ) - @pytest.mark.integration - def test_live_run_with_tools(self, tools): - chat_messages = [ChatMessage.from_user("What's the weather like in Paris?")] - component = OpenRouterChatGenerator(tools=tools) - results = component.run(chat_messages) - assert len(results["replies"]) == 1 - message = results["replies"][0] - assert message.text == "" - - assert message.tool_calls - tool_call = message.tool_call - assert isinstance(tool_call, ToolCall) - assert tool_call.tool_name == "weather" - assert tool_call.arguments == {"city": "Paris"} - assert message.meta["finish_reason"] == "tool_calls" - - @pytest.mark.skipif( - not os.environ.get("OPENROUTER_API_KEY", None), - reason="Export an env var called OPENROUTER_API_KEY containing the OpenAI API key to run this test.", - ) - @pytest.mark.integration - def test_live_run_with_tools_and_response(self, tools): - """ - Integration test that the MistralChatGenerator component can run with tools and get a response. - """ - initial_messages = [ChatMessage.from_user("What's the weather like in Paris and Berlin?")] - component = OpenRouterChatGenerator(tools=tools) - results = component.run(messages=initial_messages, generation_kwargs={"tool_choice": "auto"}) - - assert len(results["replies"]) == 1 - - # Find the message with tool calls - tool_message = results["replies"][0] - - assert isinstance(tool_message, ChatMessage) - tool_calls = tool_message.tool_calls - assert len(tool_calls) == 2 - assert ChatMessage.is_from(tool_message, ChatRole.ASSISTANT) - - for tool_call in tool_calls: - assert tool_call.id is not None - assert isinstance(tool_call, ToolCall) - assert tool_call.tool_name == "weather" - - arguments = [tool_call.arguments for tool_call in tool_calls] - assert sorted(arguments, key=lambda x: x["city"]) == [{"city": "Berlin"}, {"city": "Paris"}] - assert tool_message.meta["finish_reason"] == "tool_calls" - - new_messages = [ - initial_messages[0], - tool_message, - ChatMessage.from_tool(tool_result="22° C and sunny", origin=tool_calls[0]), - ChatMessage.from_tool(tool_result="16° C and windy", origin=tool_calls[1]), + choices=[ + ChoiceChunk( + delta=ChoiceDelta( + role="assistant", + tool_calls=[ + ChoiceDeltaToolCall( + index=1, + function=ChoiceDeltaToolCallFunction(arguments='n"}'), + type="function", + ) + ], + ), + index=0, + native_finish_reason=None, + ) + ], + created=1750162525, + model="openai/gpt-4o-mini", + object="chat.completion.chunk", + system_fingerprint="fp_34a54ae93c", + provider="OpenAI", + ), + ChatCompletionChunk( + id="gen-1750162525-tc7ParBHvsqd6rYhCDtK", + choices=[ + ChoiceChunk( + delta=ChoiceDelta(content="", role="assistant"), + finish_reason="tool_calls", + index=0, + native_finish_reason="tool_calls", + ) + ], + created=1750162525, + model="openai/gpt-4o-mini", + object="chat.completion.chunk", + system_fingerprint="fp_34a54ae93c", + provider="OpenAI", + ), + ChatCompletionChunk( + id="gen-1750162525-tc7ParBHvsqd6rYhCDtK", + choices=[ + ChoiceChunk( + delta=ChoiceDelta(content="", role="assistant"), + index=0, + native_finish_reason=None, + ) + ], + created=1750162525, + model="openai/gpt-4o-mini", + object="chat.completion.chunk", + usage=CompletionUsage( + completion_tokens=42, + prompt_tokens=55, + total_tokens=97, + completion_tokens_details=CompletionTokensDetails(reasoning_tokens=0), + prompt_tokens_details=PromptTokensDetails(cached_tokens=0), + ), + provider="OpenAI", + ), ] - # Pass the tool result to the model to get the final response - results = component.run(new_messages) - - assert len(results["replies"]) == 1 - final_message = results["replies"][0] - assert final_message.is_from(ChatRole.ASSISTANT) - assert len(final_message.text) > 0 - assert "paris" in final_message.text.lower() - assert "berlin" in final_message.text.lower() - - @pytest.mark.skipif( - not os.environ.get("OPENROUTER_API_KEY", None), - reason="Export an env var called OPENROUTER_API_KEY containing the OpenAI API key to run this test.", - ) - @pytest.mark.integration - def test_live_run_with_tools_streaming(self, tools): - """ - Integration test that the OpenRouterChatGenerator component can run with tools and streaming. - """ - component = OpenRouterChatGenerator(tools=tools, streaming_callback=print_streaming_chunk) - results = component.run( - [ChatMessage.from_user("What's the weather like in Paris and Berlin?")], - generation_kwargs={"tool_choice": "auto"}, - ) - - assert len(results["replies"]) == 1 - - # Find the message with tool calls - tool_message = results["replies"][0] - - assert isinstance(tool_message, ChatMessage) - tool_calls = tool_message.tool_calls - assert len(tool_calls) == 2 - assert ChatMessage.is_from(tool_message, ChatRole.ASSISTANT) - - for tool_call in tool_calls: - assert tool_call.id is not None - assert isinstance(tool_call, ToolCall) - assert tool_call.tool_name == "weather" - - arguments = [tool_call.arguments for tool_call in tool_calls] - assert sorted(arguments, key=lambda x: x["city"]) == [{"city": "Berlin"}, {"city": "Paris"}] - assert tool_message.meta["finish_reason"] == "tool_calls" - - @pytest.mark.skipif( - not os.environ.get("OPENROUTER_API_KEY", None), - reason="Export an env var called OPENROUTER_API_KEY containing the OpenAI API key to run this test.", - ) - @pytest.mark.integration - def test_pipeline_with_openrouter_chat_generator(self, tools): - """ - Test that the MistralChatGenerator component can be used in a pipeline - """ - pipeline = Pipeline() - pipeline.add_component("generator", OpenRouterChatGenerator(tools=tools)) - pipeline.add_component("tool_invoker", ToolInvoker(tools=tools)) - - pipeline.connect("generator", "tool_invoker") - - results = pipeline.run( - data={ - "generator": { - "messages": [ChatMessage.from_user("What's the weather like in Paris?")], - "generation_kwargs": {"tool_choice": "auto"}, - } - } - ) - - assert ( - "The weather in Paris is sunny and 32°C" - == results["tool_invoker"]["tool_messages"][0].tool_call_result.result - ) - - def test_serde_in_pipeline(self, monkeypatch): - """ - Test serialization/deserialization of OpenRouterChatGenerator in a Pipeline, - including YAML conversion and detailed dictionary validation - """ - # Set mock API key - monkeypatch.setenv("OPENROUTER_API_KEY", "test-key") - # Create a test tool - tool = Tool( - name="weather", - description="useful to determine the weather in a given location", - parameters={"city": {"type": "string"}}, - function=weather, - ) + collector_callback = CollectorCallback() + llm = OpenRouterChatGenerator(api_key=Secret.from_token("test-api-key")) + result = llm._handle_stream_response(openrouter_chunks, callback=collector_callback)[0] # type: ignore - # Create generator with specific configuration - generator = OpenRouterChatGenerator( - model="openai/gpt-4o-mini", - generation_kwargs={"temperature": 0.7}, - streaming_callback=print_streaming_chunk, - tools=[tool], - ) + # Assert text is empty + assert result.text is None - # Create and configure pipeline - pipeline = Pipeline() - pipeline.add_component("generator", generator) + # Verify both tool calls were found and processed + assert len(result.tool_calls) == 2 + assert result.tool_calls[0].id == "call_zznlVyVfK0GJwY28SShJpDCh" + assert result.tool_calls[0].tool_name == "weather" + assert result.tool_calls[0].arguments == {"city": "Paris"} + assert result.tool_calls[1].id == "call_Mh1uOyW3Ys4gwydHjNHILHGX" + assert result.tool_calls[1].tool_name == "weather" + assert result.tool_calls[1].arguments == {"city": "Berlin"} - # Get pipeline dictionary and verify its structure - pipeline_dict = pipeline.to_dict() - expected_dict = { - "metadata": {}, - "max_runs_per_component": 100, - "connection_type_validation": True, - "components": { - "generator": { - "type": "haystack_integrations.components.generators.openrouter.chat.chat_generator.OpenRouterChatGenerator", # noqa: E501 - "init_parameters": { - "api_key": {"type": "env_var", "env_vars": ["OPENROUTER_API_KEY"], "strict": True}, - "model": "openai/gpt-4o-mini", - "streaming_callback": "haystack.components.generators.utils.print_streaming_chunk", - "api_base_url": "https://openrouter.ai/api/v1", - "generation_kwargs": {"temperature": 0.7}, - "tools": [ - { - "type": "haystack.tools.tool.Tool", - "data": { - "name": "weather", - "description": "useful to determine the weather in a given location", - "parameters": {"city": {"type": "string"}}, - "function": "tests.test_openrouter_chat_generator.weather", - }, - } - ], - "http_client_kwargs": None, - "extra_headers": None, - "timeout": None, - "max_retries": None, - }, - } + # Verify meta information + assert result.meta["model"] == "openai/gpt-4o-mini" + assert result.meta["finish_reason"] == "tool_calls" + assert result.meta["index"] == 0 + assert result.meta["completion_start_time"] is not None + assert result.meta["usage"] == { + "completion_tokens": 42, + "prompt_tokens": 55, + "total_tokens": 97, + "completion_tokens_details": { + "accepted_prediction_tokens": None, + "audio_tokens": None, + "reasoning_tokens": 0, + "rejected_prediction_tokens": None, }, - "connections": [], - } - - if not hasattr(pipeline, "_connection_type_validation"): - expected_dict.pop("connection_type_validation") - - # add outputs_to_string, inputs_from_state and outputs_to_state tool parameters for compatibility with - # haystack-ai>=2.12.0 - if hasattr(tool, "outputs_to_string"): - expected_dict["components"]["generator"]["init_parameters"]["tools"][0]["data"]["outputs_to_string"] = ( - tool.outputs_to_string - ) - if hasattr(tool, "inputs_from_state"): - expected_dict["components"]["generator"]["init_parameters"]["tools"][0]["data"]["inputs_from_state"] = ( - tool.inputs_from_state - ) - if hasattr(tool, "outputs_to_state"): - expected_dict["components"]["generator"]["init_parameters"]["tools"][0]["data"]["outputs_to_state"] = ( - tool.outputs_to_state - ) - - assert pipeline_dict == expected_dict - - # Test YAML serialization/deserialization - pipeline_yaml = pipeline.dumps() - new_pipeline = Pipeline.loads(pipeline_yaml) - assert new_pipeline == pipeline - - # Verify the loaded pipeline's generator has the same configuration - loaded_generator = new_pipeline.get_component("generator") - assert loaded_generator.model == generator.model - assert loaded_generator.generation_kwargs == generator.generation_kwargs - assert loaded_generator.streaming_callback == generator.streaming_callback - assert len(loaded_generator.tools) == len(generator.tools) - assert loaded_generator.tools[0].name == generator.tools[0].name - assert loaded_generator.tools[0].description == generator.tools[0].description - assert loaded_generator.tools[0].parameters == generator.tools[0].parameters + "prompt_tokens_details": { + "audio_tokens": None, + "cached_tokens": 0, + }, + } \ No newline at end of file From a96a8d5b92b71bf1509b62714b15a96945ab3851 Mon Sep 17 00:00:00 2001 From: Sebastian Husch Lee Date: Wed, 18 Jun 2025 14:33:57 +0200 Subject: [PATCH 3/4] Formatting --- integrations/openrouter/tests/test_openrouter_chat_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integrations/openrouter/tests/test_openrouter_chat_generator.py b/integrations/openrouter/tests/test_openrouter_chat_generator.py index 76fc0c23e7..646fc6f08f 100644 --- a/integrations/openrouter/tests/test_openrouter_chat_generator.py +++ b/integrations/openrouter/tests/test_openrouter_chat_generator.py @@ -873,4 +873,4 @@ def test_handle_stream_response(self): "audio_tokens": None, "cached_tokens": 0, }, - } \ No newline at end of file + } From 62963dbd2e928646ab86ac65dc9c1f7f57d1725e Mon Sep 17 00:00:00 2001 From: Sebastian Husch Lee Date: Thu, 7 Aug 2025 09:32:10 +0200 Subject: [PATCH 4/4] Update docstrings --- .../openrouter/tests/test_openrouter_chat_generator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integrations/openrouter/tests/test_openrouter_chat_generator.py b/integrations/openrouter/tests/test_openrouter_chat_generator.py index 646fc6f08f..614c448ddd 100644 --- a/integrations/openrouter/tests/test_openrouter_chat_generator.py +++ b/integrations/openrouter/tests/test_openrouter_chat_generator.py @@ -336,7 +336,7 @@ def test_live_run_with_tools(self, tools): @pytest.mark.integration def test_live_run_with_tools_and_response(self, tools): """ - Integration test that the MistralChatGenerator component can run with tools and get a response. + Integration test that the OpenRouterChatGenerator component can run with tools and get a response. """ initial_messages = [ChatMessage.from_user("What's the weather like in Paris and Berlin?")] component = OpenRouterChatGenerator(tools=tools) @@ -418,7 +418,7 @@ def test_live_run_with_tools_streaming(self, tools): @pytest.mark.integration def test_pipeline_with_openrouter_chat_generator(self, tools): """ - Test that the MistralChatGenerator component can be used in a pipeline + Test that the OpenRouterChatGenerator component can be used in a pipeline """ pipeline = Pipeline() pipeline.add_component("generator", OpenRouterChatGenerator(tools=tools))