openrouter-haystack 0.2.2__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (17) hide show
  1. {openrouter_haystack-0.2.2 → openrouter_haystack-0.4.0}/CHANGELOG.md +24 -0
  2. {openrouter_haystack-0.2.2 → openrouter_haystack-0.4.0}/PKG-INFO +2 -2
  3. openrouter_haystack-0.4.0/examples/openrouter_with_structured_outputs.py +35 -0
  4. openrouter_haystack-0.4.0/pydoc/config_docusaurus.yml +28 -0
  5. {openrouter_haystack-0.2.2 → openrouter_haystack-0.4.0}/pyproject.toml +4 -4
  6. {openrouter_haystack-0.2.2 → openrouter_haystack-0.4.0}/src/haystack_integrations/components/generators/openrouter/chat/chat_generator.py +61 -35
  7. {openrouter_haystack-0.2.2 → openrouter_haystack-0.4.0}/tests/test_openrouter_chat_generator.py +156 -29
  8. {openrouter_haystack-0.2.2 → openrouter_haystack-0.4.0}/tests/test_openrouter_chat_generator_async.py +67 -4
  9. {openrouter_haystack-0.2.2 → openrouter_haystack-0.4.0}/.gitignore +0 -0
  10. {openrouter_haystack-0.2.2 → openrouter_haystack-0.4.0}/LICENSE.txt +0 -0
  11. {openrouter_haystack-0.2.2 → openrouter_haystack-0.4.0}/README.md +0 -0
  12. {openrouter_haystack-0.2.2 → openrouter_haystack-0.4.0}/examples/openrouter_with_tools_example.py +0 -0
  13. {openrouter_haystack-0.2.2 → openrouter_haystack-0.4.0}/pydoc/config.yml +0 -0
  14. {openrouter_haystack-0.2.2 → openrouter_haystack-0.4.0}/src/haystack_integrations/components/generators/openrouter/__init__.py +0 -0
  15. {openrouter_haystack-0.2.2 → openrouter_haystack-0.4.0}/src/haystack_integrations/components/generators/openrouter/chat/__init__.py +0 -0
  16. {openrouter_haystack-0.2.2 → openrouter_haystack-0.4.0}/src/haystack_integrations/components/generators/py.typed +0 -0
  17. {openrouter_haystack-0.2.2 → openrouter_haystack-0.4.0}/tests/__init__.py +0 -0
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## [integrations/openrouter-v0.3.0] - 2025-10-23
4
+
5
+ ### 🚀 Features
6
+
7
+ - Add support for structured outputs in OpenRouterChatGenerator (#2406)
8
+ - `OpenRouterChatGenerator` add integration tests for mixing Tool/Toolset (#2421)
9
+
10
+ ### 📚 Documentation
11
+
12
+ - Add pydoc configurations for Docusaurus (#2411)
13
+
14
+
15
+ ## [integrations/openrouter-v0.2.2] - 2025-09-23
16
+
17
+ ### 🐛 Bug Fixes
18
+
19
+ - Chore: Fix linting in tests for Openrouter integration (#2261)
20
+ - Update OpenRouterChatGenerator to work with `haystack-ai>=2.18.0` (#2295)
21
+
22
+ ### 🧹 Chores
23
+
24
+ - Standardize readmes - part 2 (#2205)
25
+
26
+
3
27
  ## [integrations/openrouter-v0.2.1] - 2025-08-07
4
28
 
5
29
  ### 🚀 Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openrouter-haystack
3
- Version: 0.2.2
3
+ Version: 0.4.0
4
4
  Project-URL: Documentation, https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/openrouter#readme
5
5
  Project-URL: Issues, https://github.com/deepset-ai/haystack-core-integrations/issues
6
6
  Project-URL: Source, https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/openrouter
@@ -18,7 +18,7 @@ Classifier: Programming Language :: Python :: 3.13
18
18
  Classifier: Programming Language :: Python :: Implementation :: CPython
19
19
  Classifier: Programming Language :: Python :: Implementation :: PyPy
20
20
  Requires-Python: >=3.9
21
- Requires-Dist: haystack-ai>=2.13.1
21
+ Requires-Dist: haystack-ai>=2.19.0
22
22
  Description-Content-Type: text/markdown
23
23
 
24
24
  # openrouter-haystack
@@ -0,0 +1,35 @@
1
+ # SPDX-FileCopyrightText: 2024-present deepset GmbH <info@deepset.ai>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+
6
+ # This example demonstrates how to use the OpenRouterChatGenerator component
7
+ # with structured outputs.
8
+ # To run this example, you will need to
9
+ # set `OPENROUTER_API_KEY` environment variable
10
+
11
+ from haystack.dataclasses import ChatMessage
12
+ from pydantic import BaseModel
13
+
14
+ from haystack_integrations.components.generators.openrouter import OpenRouterChatGenerator
15
+
16
+
17
+ class NobelPrizeInfo(BaseModel):
18
+ recipient_name: str
19
+ award_year: int
20
+ category: str
21
+ achievement_description: str
22
+ nationality: str
23
+
24
+
25
+ chat_messages = [
26
+ ChatMessage.from_user(
27
+ "In 2021, American scientist David Julius received the Nobel Prize in"
28
+ " Physiology or Medicine for his groundbreaking discoveries on how the human body"
29
+ " senses temperature and touch."
30
+ )
31
+ ]
32
+ component = OpenRouterChatGenerator(generation_kwargs={"response_format": NobelPrizeInfo})
33
+ results = component.run(chat_messages)
34
+
35
+ # print(results)
@@ -0,0 +1,28 @@
1
+ loaders:
2
+ - ignore_when_discovered:
3
+ - __init__
4
+ modules:
5
+ - haystack_integrations.components.generators.openrouter.chat.chat_generator
6
+ search_path:
7
+ - ../src
8
+ type: haystack_pydoc_tools.loaders.CustomPythonLoader
9
+ processors:
10
+ - do_not_filter_modules: false
11
+ documented_only: true
12
+ expression: null
13
+ skip_empty_modules: true
14
+ type: filter
15
+ - type: smart
16
+ - type: crossref
17
+ renderer:
18
+ description: OpenRouter integration for Haystack
19
+ id: integrations-openrouter
20
+ markdown:
21
+ add_member_class_prefix: false
22
+ add_method_class_prefix: true
23
+ classdef_code_block: false
24
+ descriptive_class_title: false
25
+ descriptive_module_title: true
26
+ filename: openrouter.md
27
+ title: OpenRouter
28
+ type: haystack_pydoc_tools.renderers.DocusaurusRenderer
@@ -23,7 +23,7 @@ classifiers = [
23
23
  "Programming Language :: Python :: Implementation :: CPython",
24
24
  "Programming Language :: Python :: Implementation :: PyPy",
25
25
  ]
26
- dependencies = ["haystack-ai>=2.13.1"]
26
+ dependencies = ["haystack-ai>=2.19.0"]
27
27
 
28
28
  [project.urls]
29
29
  Documentation = "https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/openrouter#readme"
@@ -64,7 +64,7 @@ dependencies = [
64
64
  unit = 'pytest -m "not integration" {args:tests}'
65
65
  integration = 'pytest -m "integration" {args:tests}'
66
66
  all = 'pytest {args:tests}'
67
- cov-retry = 'all --cov=haystack_integrations --reruns 3 --reruns-delay 30 -x'
67
+ cov-retry = 'pytest --cov=haystack_integrations --reruns 3 --reruns-delay 30 -x {args:tests}'
68
68
 
69
69
  types = "mypy -p haystack_integrations.components.generators.openrouter {args}"
70
70
 
@@ -76,7 +76,7 @@ disallow_incomplete_defs = true
76
76
 
77
77
 
78
78
  [tool.ruff]
79
- target-version = "py38"
79
+ target-version = "py39"
80
80
  line-length = 120
81
81
 
82
82
  [tool.ruff.lint]
@@ -154,4 +154,4 @@ addopts = "--strict-markers"
154
154
  markers = [
155
155
  "integration: integration tests",
156
156
  ]
157
- log_cli = true
157
+ log_cli = true
@@ -2,12 +2,12 @@
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
- from typing import Any, Dict, List, Optional, Union
5
+ from typing import Any, Optional
6
6
 
7
7
  from haystack import component, default_to_dict, logging
8
8
  from haystack.components.generators.chat import OpenAIChatGenerator
9
9
  from haystack.dataclasses import ChatMessage, StreamingCallbackT
10
- from haystack.tools import Tool, Toolset, _check_duplicate_tool_names
10
+ from haystack.tools import ToolsType, _check_duplicate_tool_names, flatten_tools_or_toolsets, serialize_tools_or_toolset
11
11
  from haystack.utils import serialize_callable
12
12
  from haystack.utils.auth import Secret
13
13
 
@@ -51,7 +51,7 @@ class OpenRouterChatGenerator(OpenAIChatGenerator):
51
51
  >>{'replies': [ChatMessage(_content='Natural Language Processing (NLP) is a branch of artificial intelligence
52
52
  >>that focuses on enabling computers to understand, interpret, and generate human language in a way that is
53
53
  >>meaningful and useful.', _role=<ChatRole.ASSISTANT: 'assistant'>, _name=None,
54
- >>_meta={'model': 'openai/gpt-4o-mini', 'index': 0, 'finish_reason': 'stop',
54
+ >>_meta={'model': 'openai/gpt-5-mini', 'index': 0, 'finish_reason': 'stop',
55
55
  >>'usage': {'prompt_tokens': 15, 'completion_tokens': 36, 'total_tokens': 51}})]}
56
56
  ```
57
57
  """
@@ -60,19 +60,19 @@ class OpenRouterChatGenerator(OpenAIChatGenerator):
60
60
  self,
61
61
  *,
62
62
  api_key: Secret = Secret.from_env_var("OPENROUTER_API_KEY"),
63
- model: str = "openai/gpt-4o-mini",
63
+ model: str = "openai/gpt-5-mini",
64
64
  streaming_callback: Optional[StreamingCallbackT] = None,
65
65
  api_base_url: Optional[str] = "https://openrouter.ai/api/v1",
66
- generation_kwargs: Optional[Dict[str, Any]] = None,
67
- tools: Optional[Union[List[Tool], Toolset]] = None,
66
+ generation_kwargs: Optional[dict[str, Any]] = None,
67
+ tools: Optional[ToolsType] = None,
68
68
  timeout: Optional[float] = None,
69
- extra_headers: Optional[Dict[str, Any]] = None,
69
+ extra_headers: Optional[dict[str, Any]] = None,
70
70
  max_retries: Optional[int] = None,
71
- http_client_kwargs: Optional[Dict[str, Any]] = None,
71
+ http_client_kwargs: Optional[dict[str, Any]] = None,
72
72
  ):
73
73
  """
74
74
  Creates an instance of OpenRouterChatGenerator. Unless specified otherwise,
75
- the default model is `openai/gpt-4o-mini`.
75
+ the default model is `openai/gpt-5-mini`.
76
76
 
77
77
  :param api_key:
78
78
  The OpenRouter API key.
@@ -98,6 +98,14 @@ class OpenRouterChatGenerator(OpenAIChatGenerator):
98
98
  events as they become available, with the stream terminated by a data: [DONE] message.
99
99
  - `safe_prompt`: Whether to inject a safety prompt before all conversations.
100
100
  - `random_seed`: The seed to use for random sampling.
101
+ - `response_format`: A JSON schema or a Pydantic model that enforces the structure of the model's response.
102
+ If provided, the output will always be validated against this
103
+ format (unless the model returns a tool call).
104
+ For details, see the [OpenAI Structured Outputs documentation](https://platform.openai.com/docs/guides/structured-outputs).
105
+ Notes:
106
+ - This parameter accepts Pydantic models and JSON schemas for latest models starting from GPT-4o.
107
+ - For structured outputs with streaming,
108
+ the `response_format` must be a JSON schema and not a Pydantic model.
101
109
  :param tools:
102
110
  A list of tools or a Toolset for which the model can prepare calls. This parameter can accept either a
103
111
  list of `Tool` objects or a `Toolset` instance.
@@ -128,7 +136,7 @@ class OpenRouterChatGenerator(OpenAIChatGenerator):
128
136
  )
129
137
  self.extra_headers = extra_headers
130
138
 
131
- def to_dict(self) -> Dict[str, Any]:
139
+ def to_dict(self) -> dict[str, Any]:
132
140
  """
133
141
  Serialize this component to a dictionary.
134
142
 
@@ -148,7 +156,7 @@ class OpenRouterChatGenerator(OpenAIChatGenerator):
148
156
  api_base_url=self.api_base_url,
149
157
  generation_kwargs=self.generation_kwargs,
150
158
  api_key=self.api_key.to_dict(),
151
- tools=[tool.to_dict() for tool in self.tools] if self.tools else None,
159
+ tools=serialize_tools_or_toolset(self.tools),
152
160
  extra_headers=self.extra_headers,
153
161
  timeout=self.timeout,
154
162
  max_retries=self.max_retries,
@@ -158,46 +166,64 @@ class OpenRouterChatGenerator(OpenAIChatGenerator):
158
166
  def _prepare_api_call(
159
167
  self,
160
168
  *,
161
- messages: List[ChatMessage],
169
+ messages: list[ChatMessage],
162
170
  streaming_callback: Optional[StreamingCallbackT] = None,
163
- generation_kwargs: Optional[Dict[str, Any]] = None,
164
- tools: Optional[Union[List[Tool], Toolset]] = None,
171
+ generation_kwargs: Optional[dict[str, Any]] = None,
172
+ tools: Optional[ToolsType] = None,
165
173
  tools_strict: Optional[bool] = None,
166
- ) -> Dict[str, Any]:
174
+ ) -> dict[str, Any]:
167
175
  # update generation kwargs by merging with the generation kwargs passed to the run method
168
176
  generation_kwargs = {**self.generation_kwargs, **(generation_kwargs or {})}
169
177
  extra_headers = {**(self.extra_headers or {})}
170
178
 
179
+ is_streaming = streaming_callback is not None
180
+ num_responses = generation_kwargs.pop("n", 1)
181
+
182
+ if is_streaming and num_responses > 1:
183
+ msg = "Cannot stream multiple responses, please set n=1."
184
+ raise ValueError(msg)
185
+ response_format = generation_kwargs.pop("response_format", None)
186
+
171
187
  # adapt ChatMessage(s) to the format expected by the OpenAI API
172
188
  openai_formatted_messages = [message.to_openai_dict_format() for message in messages]
173
189
 
174
- tools = tools or self.tools
175
- if isinstance(tools, Toolset):
176
- tools = list(tools)
190
+ flattened_tools = flatten_tools_or_toolsets(tools or self.tools)
177
191
  tools_strict = tools_strict if tools_strict is not None else self.tools_strict
178
- _check_duplicate_tool_names(list(tools or []))
192
+ _check_duplicate_tool_names(flattened_tools)
179
193
 
180
194
  openai_tools = {}
181
- if tools:
182
- tool_definitions = [
183
- {"type": "function", "function": {**t.tool_spec, **({"strict": tools_strict} if tools_strict else {})}}
184
- for t in tools
185
- ]
195
+ if flattened_tools:
196
+ tool_definitions = []
197
+ for t in flattened_tools:
198
+ function_spec = {**t.tool_spec}
199
+ if tools_strict:
200
+ function_spec["strict"] = True
201
+ function_spec["parameters"]["additionalProperties"] = False
202
+ tool_definitions.append({"type": "function", "function": function_spec})
186
203
  openai_tools = {"tools": tool_definitions}
187
204
 
188
- is_streaming = streaming_callback is not None
189
- num_responses = generation_kwargs.pop("n", 1)
190
- if is_streaming and num_responses > 1:
191
- msg = "Cannot stream multiple responses, please set n=1."
192
- raise ValueError(msg)
193
-
194
- return {
205
+ base_args = {
195
206
  "model": self.model,
196
- "messages": openai_formatted_messages, # type: ignore[arg-type] # openai expects list of specific message types
197
- "stream": streaming_callback is not None,
207
+ "messages": openai_formatted_messages,
198
208
  "n": num_responses,
199
209
  **openai_tools,
200
- "extra_body": {**generation_kwargs},
201
210
  "extra_headers": {**extra_headers},
202
- "openai_endpoint": "create",
211
+ "extra_body": {**generation_kwargs},
203
212
  }
213
+
214
+ if response_format and not is_streaming:
215
+ # for structured outputs without streaming, we use openai's parse endpoint
216
+ # Note: `stream` cannot be passed to chat.completions.parse
217
+ # we pass a key `openai_endpoint` as a hint to the run method to use the parse endpoint
218
+ # this key will be removed before the API call is made
219
+ return {**base_args, "response_format": response_format, "openai_endpoint": "parse"}
220
+
221
+ # for structured outputs with streaming, we use openai's create endpoint
222
+ # we pass a key `openai_endpoint` as a hint to the run method to use the create endpoint
223
+ # this key will be removed before the API call is made
224
+ final_args = {**base_args, "stream": is_streaming, "openai_endpoint": "create"}
225
+
226
+ # We only set the response_format parameter if it's not None since None is not a valid value in the API.
227
+ if response_format:
228
+ final_args["response_format"] = response_format
229
+ return final_args
@@ -1,3 +1,4 @@
1
+ import json
1
2
  import os
2
3
  from datetime import datetime
3
4
  from unittest.mock import patch
@@ -8,7 +9,7 @@ from haystack import Pipeline
8
9
  from haystack.components.generators.utils import print_streaming_chunk
9
10
  from haystack.components.tools import ToolInvoker
10
11
  from haystack.dataclasses import ChatMessage, ChatRole, StreamingChunk, ToolCall
11
- from haystack.tools import Tool
12
+ from haystack.tools import Tool, Toolset
12
13
  from haystack.utils.auth import Secret
13
14
  from openai import OpenAIError
14
15
  from openai.types.chat import ChatCompletion, ChatCompletionChunk, ChatCompletionMessage
@@ -16,10 +17,22 @@ from openai.types.chat.chat_completion import Choice
16
17
  from openai.types.chat.chat_completion_chunk import Choice as ChoiceChunk
17
18
  from openai.types.chat.chat_completion_chunk import ChoiceDelta, ChoiceDeltaToolCall, ChoiceDeltaToolCallFunction
18
19
  from openai.types.completion_usage import CompletionTokensDetails, CompletionUsage, PromptTokensDetails
20
+ from pydantic import BaseModel
19
21
 
20
22
  from haystack_integrations.components.generators.openrouter.chat.chat_generator import OpenRouterChatGenerator
21
23
 
22
24
 
25
+ class CalendarEvent(BaseModel):
26
+ event_name: str
27
+ event_date: str
28
+ event_location: str
29
+
30
+
31
+ @pytest.fixture
32
+ def calendar_event_model():
33
+ return CalendarEvent
34
+
35
+
23
36
  class CollectorCallback:
24
37
  """
25
38
  Callback to collect streaming chunks for testing purposes.
@@ -66,7 +79,7 @@ def mock_chat_completion():
66
79
  with patch("openai.resources.chat.completions.Completions.create") as mock_chat_completion_create:
67
80
  completion = ChatCompletion(
68
81
  id="foo",
69
- model="openai/gpt-4o-mini",
82
+ model="openai/gpt-5-mini",
70
83
  object="chat.completion",
71
84
  choices=[
72
85
  Choice(
@@ -89,7 +102,7 @@ class TestOpenRouterChatGenerator:
89
102
  monkeypatch.setenv("OPENROUTER_API_KEY", "test-api-key")
90
103
  component = OpenRouterChatGenerator()
91
104
  assert component.client.api_key == "test-api-key"
92
- assert component.model == "openai/gpt-4o-mini"
105
+ assert component.model == "openai/gpt-5-mini"
93
106
  assert component.api_base_url == "https://openrouter.ai/api/v1"
94
107
  assert component.streaming_callback is None
95
108
  assert not component.generation_kwargs
@@ -102,13 +115,13 @@ class TestOpenRouterChatGenerator:
102
115
  def test_init_with_parameters(self):
103
116
  component = OpenRouterChatGenerator(
104
117
  api_key=Secret.from_token("test-api-key"),
105
- model="openai/gpt-4o-mini",
118
+ model="openai/gpt-5",
106
119
  streaming_callback=print_streaming_chunk,
107
120
  api_base_url="test-base-url",
108
121
  generation_kwargs={"max_tokens": 10, "some_test_param": "test-params"},
109
122
  )
110
123
  assert component.client.api_key == "test-api-key"
111
- assert component.model == "openai/gpt-4o-mini"
124
+ assert component.model == "openai/gpt-5"
112
125
  assert component.streaming_callback is print_streaming_chunk
113
126
  assert component.generation_kwargs == {"max_tokens": 10, "some_test_param": "test-params"}
114
127
 
@@ -124,7 +137,7 @@ class TestOpenRouterChatGenerator:
124
137
 
125
138
  expected_params = {
126
139
  "api_key": {"env_vars": ["OPENROUTER_API_KEY"], "strict": True, "type": "env_var"},
127
- "model": "openai/gpt-4o-mini",
140
+ "model": "openai/gpt-5-mini",
128
141
  "streaming_callback": None,
129
142
  "api_base_url": "https://openrouter.ai/api/v1",
130
143
  "generation_kwargs": {},
@@ -142,7 +155,7 @@ class TestOpenRouterChatGenerator:
142
155
  monkeypatch.setenv("ENV_VAR", "test-api-key")
143
156
  component = OpenRouterChatGenerator(
144
157
  api_key=Secret.from_env_var("ENV_VAR"),
145
- model="openai/gpt-4o-mini",
158
+ model="openai/gpt-5",
146
159
  streaming_callback=print_streaming_chunk,
147
160
  api_base_url="test-base-url",
148
161
  generation_kwargs={"max_tokens": 10, "some_test_param": "test-params"},
@@ -161,7 +174,7 @@ class TestOpenRouterChatGenerator:
161
174
 
162
175
  expected_params = {
163
176
  "api_key": {"env_vars": ["ENV_VAR"], "strict": True, "type": "env_var"},
164
- "model": "openai/gpt-4o-mini",
177
+ "model": "openai/gpt-5",
165
178
  "api_base_url": "test-base-url",
166
179
  "streaming_callback": "haystack.components.generators.utils.print_streaming_chunk",
167
180
  "generation_kwargs": {"max_tokens": 10, "some_test_param": "test-params"},
@@ -183,7 +196,7 @@ class TestOpenRouterChatGenerator:
183
196
  ),
184
197
  "init_parameters": {
185
198
  "api_key": {"env_vars": ["OPENROUTER_API_KEY"], "strict": True, "type": "env_var"},
186
- "model": "openai/gpt-4o-mini",
199
+ "model": "openai/gpt-5-mini",
187
200
  "api_base_url": "test-base-url",
188
201
  "streaming_callback": "haystack.components.generators.utils.print_streaming_chunk",
189
202
  "generation_kwargs": {"max_tokens": 10, "some_test_param": "test-params"},
@@ -195,7 +208,7 @@ class TestOpenRouterChatGenerator:
195
208
  },
196
209
  }
197
210
  component = OpenRouterChatGenerator.from_dict(data)
198
- assert component.model == "openai/gpt-4o-mini"
211
+ assert component.model == "openai/gpt-5-mini"
199
212
  assert component.streaming_callback is print_streaming_chunk
200
213
  assert component.api_base_url == "test-base-url"
201
214
  assert component.generation_kwargs == {"max_tokens": 10, "some_test_param": "test-params"}
@@ -214,7 +227,7 @@ class TestOpenRouterChatGenerator:
214
227
  ),
215
228
  "init_parameters": {
216
229
  "api_key": {"env_vars": ["OPENROUTER_API_KEY"], "strict": True, "type": "env_var"},
217
- "model": "openai/gpt-4o-mini",
230
+ "model": "openai/gpt-5-mini",
218
231
  "api_base_url": "test-base-url",
219
232
  "streaming_callback": "haystack.components.generators.utils.print_streaming_chunk",
220
233
  "generation_kwargs": {"max_tokens": 10, "some_test_param": "test-params"},
@@ -267,7 +280,7 @@ class TestOpenRouterChatGenerator:
267
280
  assert len(results["replies"]) == 1
268
281
  message: ChatMessage = results["replies"][0]
269
282
  assert "Paris" in message.text
270
- assert "openai/gpt-4o-mini" in message.meta["model"]
283
+ assert "openai/gpt-5-mini" in message.meta["model"]
271
284
  assert message.meta["finish_reason"] == "stop"
272
285
 
273
286
  @pytest.mark.skipif(
@@ -303,7 +316,7 @@ class TestOpenRouterChatGenerator:
303
316
  message: ChatMessage = results["replies"][0]
304
317
  assert "Paris" in message.text
305
318
 
306
- assert "openai/gpt-4o-mini" in message.meta["model"]
319
+ assert "openai/gpt-5-mini" in message.meta["model"]
307
320
  assert message.meta["finish_reason"] == "stop"
308
321
 
309
322
  assert callback.counter > 1
@@ -440,6 +453,41 @@ class TestOpenRouterChatGenerator:
440
453
  == results["tool_invoker"]["tool_messages"][0].tool_call_result.result
441
454
  )
442
455
 
456
+ @pytest.mark.skipif(
457
+ not os.environ.get("OPENROUTER_API_KEY", None),
458
+ reason="Export an env var called OPENROUTER_API_KEY containing the OpenRouter API key to run this test.",
459
+ )
460
+ @pytest.mark.integration
461
+ def test_live_run_with_response_format_json_schema(self):
462
+ response_schema = {
463
+ "type": "json_schema",
464
+ "json_schema": {
465
+ "name": "CapitalCity",
466
+ "strict": True,
467
+ "schema": {
468
+ "title": "CapitalCity",
469
+ "type": "object",
470
+ "properties": {
471
+ "city": {"title": "City", "type": "string"},
472
+ "country": {"title": "Country", "type": "string"},
473
+ },
474
+ "required": ["city", "country"],
475
+ "additionalProperties": False,
476
+ },
477
+ },
478
+ }
479
+
480
+ chat_messages = [ChatMessage.from_user("What's the capital of France?")]
481
+ comp = OpenRouterChatGenerator(generation_kwargs={"response_format": response_schema})
482
+ results = comp.run(chat_messages)
483
+ assert len(results["replies"]) == 1
484
+ message: ChatMessage = results["replies"][0]
485
+ msg = json.loads(message.text)
486
+ assert "Paris" in msg["city"]
487
+ assert isinstance(msg["country"], str)
488
+ assert "France" in msg["country"]
489
+ assert message.meta["finish_reason"] == "stop"
490
+
443
491
  def test_serde_in_pipeline(self, monkeypatch):
444
492
  """
445
493
  Test serialization/deserialization of OpenRouterChatGenerator in a Pipeline,
@@ -458,7 +506,6 @@ class TestOpenRouterChatGenerator:
458
506
 
459
507
  # Create generator with specific configuration
460
508
  generator = OpenRouterChatGenerator(
461
- model="openai/gpt-4o-mini",
462
509
  generation_kwargs={"temperature": 0.7},
463
510
  streaming_callback=print_streaming_chunk,
464
511
  tools=[tool],
@@ -479,7 +526,7 @@ class TestOpenRouterChatGenerator:
479
526
  "type": "haystack_integrations.components.generators.openrouter.chat.chat_generator.OpenRouterChatGenerator", # noqa: E501
480
527
  "init_parameters": {
481
528
  "api_key": {"type": "env_var", "env_vars": ["OPENROUTER_API_KEY"], "strict": True},
482
- "model": "openai/gpt-4o-mini",
529
+ "model": "openai/gpt-5-mini",
483
530
  "streaming_callback": "haystack.components.generators.utils.print_streaming_chunk",
484
531
  "api_base_url": "https://openrouter.ai/api/v1",
485
532
  "generation_kwargs": {"temperature": 0.7},
@@ -539,6 +586,86 @@ class TestOpenRouterChatGenerator:
539
586
  assert loaded_generator.tools[0].description == generator.tools[0].description
540
587
  assert loaded_generator.tools[0].parameters == generator.tools[0].parameters
541
588
 
589
+ @pytest.mark.skipif(
590
+ not os.environ.get("OPENROUTER_API_KEY", None),
591
+ reason="Export an env var called OPENROUTER_API_KEY containing the OpenRouter API key to run this test.",
592
+ )
593
+ @pytest.mark.integration
594
+ def test_live_run_with_response_format_pydantic_model(self, calendar_event_model):
595
+ chat_messages = [
596
+ ChatMessage.from_user("The marketing summit takes place on October12th at the Hilton Hotel downtown.")
597
+ ]
598
+ component = OpenRouterChatGenerator(generation_kwargs={"response_format": calendar_event_model})
599
+ results = component.run(chat_messages)
600
+ assert len(results["replies"]) == 1
601
+ message: ChatMessage = results["replies"][0]
602
+ msg = json.loads(message.text)
603
+ assert "Marketing Summit" in msg["event_name"]
604
+ assert isinstance(msg["event_date"], str)
605
+ assert isinstance(msg["event_location"], str)
606
+
607
+ @pytest.mark.skipif(
608
+ not os.environ.get("OPENROUTER_API_KEY", None),
609
+ reason="Export an env var called OPENROUTER_API_KEY containing the OpenRouter API key to run this test.",
610
+ )
611
+ @pytest.mark.integration
612
+ def test_integration_mixing_tools_and_toolset(self):
613
+ """Test mixing Tool list and Toolset at runtime."""
614
+
615
+ def weather_function(city: str) -> str:
616
+ """Get weather information for a city."""
617
+ return f"Weather in {city}: 22°C, sunny"
618
+
619
+ def time_function(city: str) -> str:
620
+ """Get current time in a city."""
621
+ return f"Current time in {city}: 14:30"
622
+
623
+ def echo_function(text: str) -> str:
624
+ """Echo a text."""
625
+ return text
626
+
627
+ # Create tools
628
+ weather_tool = Tool(
629
+ name="weather",
630
+ description="Get weather information for a city",
631
+ parameters={"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]},
632
+ function=weather_function,
633
+ )
634
+
635
+ time_tool = Tool(
636
+ name="time",
637
+ description="Get current time in a city",
638
+ parameters={"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]},
639
+ function=time_function,
640
+ )
641
+
642
+ echo_tool = Tool(
643
+ name="echo",
644
+ description="Echo a text",
645
+ parameters={"type": "object", "properties": {"text": {"type": "string"}}, "required": ["text"]},
646
+ function=echo_function,
647
+ )
648
+
649
+ # Create Toolset with weather and time tools
650
+ toolset = Toolset([weather_tool, time_tool])
651
+
652
+ # Initialize with no tools, we'll pass them at runtime
653
+ component = OpenRouterChatGenerator()
654
+
655
+ # Pass mixed list: echo_tool (individual) and toolset (weather + time) at runtime
656
+ # This tests that both individual tools and toolsets can be combined
657
+ messages = [ChatMessage.from_user("Echo this: Hello World")]
658
+ results = component.run(messages, tools=[echo_tool, toolset])
659
+
660
+ assert len(results["replies"]) == 1
661
+ message = results["replies"][0]
662
+
663
+ # Should be able to use echo_tool from the runtime mixed list
664
+ assert message.tool_calls is not None
665
+ tool_call = message.tool_calls[0]
666
+ assert tool_call.tool_name == "echo"
667
+ assert tool_call.arguments == {"text": "Hello World"}
668
+
542
669
 
543
670
  class TestChatCompletionChunkConversion:
544
671
  def test_handle_stream_response(self):
@@ -549,7 +676,7 @@ class TestChatCompletionChunkConversion:
549
676
  ChoiceChunk(delta=ChoiceDelta(content="", role="assistant"), index=0, native_finish_reason=None)
550
677
  ],
551
678
  created=1750162525,
552
- model="openai/gpt-4o-mini",
679
+ model="openai/gpt-5-mini",
553
680
  object="chat.completion.chunk",
554
681
  system_fingerprint="fp_34a54ae93c",
555
682
  provider="OpenAI",
@@ -574,7 +701,7 @@ class TestChatCompletionChunkConversion:
574
701
  )
575
702
  ],
576
703
  created=1750162525,
577
- model="openai/gpt-4o-mini",
704
+ model="openai/gpt-5-mini",
578
705
  object="chat.completion.chunk",
579
706
  system_fingerprint="fp_34a54ae93c",
580
707
  provider="OpenAI",
@@ -598,7 +725,7 @@ class TestChatCompletionChunkConversion:
598
725
  )
599
726
  ],
600
727
  created=1750162525,
601
- model="openai/gpt-4o-mini",
728
+ model="openai/gpt-5-mini",
602
729
  object="chat.completion.chunk",
603
730
  system_fingerprint="fp_34a54ae93c",
604
731
  provider="OpenAI",
@@ -622,7 +749,7 @@ class TestChatCompletionChunkConversion:
622
749
  )
623
750
  ],
624
751
  created=1750162525,
625
- model="openai/gpt-4o-mini",
752
+ model="openai/gpt-5-mini",
626
753
  object="chat.completion.chunk",
627
754
  system_fingerprint="fp_34a54ae93c",
628
755
  provider="OpenAI",
@@ -646,7 +773,7 @@ class TestChatCompletionChunkConversion:
646
773
  )
647
774
  ],
648
775
  created=1750162525,
649
- model="openai/gpt-4o-mini",
776
+ model="openai/gpt-5-mini",
650
777
  object="chat.completion.chunk",
651
778
  system_fingerprint="fp_34a54ae93c",
652
779
  provider="OpenAI",
@@ -670,7 +797,7 @@ class TestChatCompletionChunkConversion:
670
797
  )
671
798
  ],
672
799
  created=1750162525,
673
- model="openai/gpt-4o-mini",
800
+ model="openai/gpt-5-mini",
674
801
  object="chat.completion.chunk",
675
802
  system_fingerprint="fp_34a54ae93c",
676
803
  provider="OpenAI",
@@ -695,7 +822,7 @@ class TestChatCompletionChunkConversion:
695
822
  )
696
823
  ],
697
824
  created=1750162525,
698
- model="openai/gpt-4o-mini",
825
+ model="openai/gpt-5-mini",
699
826
  object="chat.completion.chunk",
700
827
  service_tier=None,
701
828
  system_fingerprint="fp_34a54ae93c",
@@ -722,7 +849,7 @@ class TestChatCompletionChunkConversion:
722
849
  )
723
850
  ],
724
851
  created=1750162525,
725
- model="openai/gpt-4o-mini",
852
+ model="openai/gpt-5-mini",
726
853
  object="chat.completion.chunk",
727
854
  system_fingerprint="fp_34a54ae93c",
728
855
  provider="OpenAI",
@@ -746,7 +873,7 @@ class TestChatCompletionChunkConversion:
746
873
  )
747
874
  ],
748
875
  created=1750162525,
749
- model="openai/gpt-4o-mini",
876
+ model="openai/gpt-5-mini",
750
877
  object="chat.completion.chunk",
751
878
  system_fingerprint="fp_34a54ae93c",
752
879
  provider="OpenAI",
@@ -770,7 +897,7 @@ class TestChatCompletionChunkConversion:
770
897
  )
771
898
  ],
772
899
  created=1750162525,
773
- model="openai/gpt-4o-mini",
900
+ model="openai/gpt-5-mini",
774
901
  object="chat.completion.chunk",
775
902
  system_fingerprint="fp_34a54ae93c",
776
903
  provider="OpenAI",
@@ -794,7 +921,7 @@ class TestChatCompletionChunkConversion:
794
921
  )
795
922
  ],
796
923
  created=1750162525,
797
- model="openai/gpt-4o-mini",
924
+ model="openai/gpt-5-mini",
798
925
  object="chat.completion.chunk",
799
926
  system_fingerprint="fp_34a54ae93c",
800
927
  provider="OpenAI",
@@ -810,7 +937,7 @@ class TestChatCompletionChunkConversion:
810
937
  )
811
938
  ],
812
939
  created=1750162525,
813
- model="openai/gpt-4o-mini",
940
+ model="openai/gpt-5-mini",
814
941
  object="chat.completion.chunk",
815
942
  system_fingerprint="fp_34a54ae93c",
816
943
  provider="OpenAI",
@@ -825,7 +952,7 @@ class TestChatCompletionChunkConversion:
825
952
  )
826
953
  ],
827
954
  created=1750162525,
828
- model="openai/gpt-4o-mini",
955
+ model="openai/gpt-5-mini",
829
956
  object="chat.completion.chunk",
830
957
  usage=CompletionUsage(
831
958
  completion_tokens=42,
@@ -855,7 +982,7 @@ class TestChatCompletionChunkConversion:
855
982
  assert result.tool_calls[1].arguments == {"city": "Berlin"}
856
983
 
857
984
  # Verify meta information
858
- assert result.meta["model"] == "openai/gpt-4o-mini"
985
+ assert result.meta["model"] == "openai/gpt-5-mini"
859
986
  assert result.meta["finish_reason"] == "tool_calls"
860
987
  assert result.meta["index"] == 0
861
988
  assert result.meta["completion_start_time"] is not None
@@ -9,7 +9,7 @@ from haystack.dataclasses import (
9
9
  ChatRole,
10
10
  StreamingChunk,
11
11
  )
12
- from haystack.tools import Tool
12
+ from haystack.tools import Tool, Toolset
13
13
  from openai import AsyncOpenAI
14
14
  from openai.types.chat import ChatCompletion, ChatCompletionMessage
15
15
  from openai.types.chat.chat_completion import Choice
@@ -60,7 +60,7 @@ def mock_async_chat_completion():
60
60
  ) as mock_chat_completion_create:
61
61
  completion = ChatCompletion(
62
62
  id="foo",
63
- model="openai/gpt-4o-mini",
63
+ model="openai/gpt-5-mini",
64
64
  object="chat.completion",
65
65
  choices=[
66
66
  Choice(
@@ -136,7 +136,7 @@ class TestOpenRouterChatGeneratorAsync:
136
136
  assert len(results["replies"]) == 1
137
137
  message: ChatMessage = results["replies"][0]
138
138
  assert "Paris" in message.text
139
- assert "openai/gpt-4o-mini" in message.meta["model"]
139
+ assert "openai/gpt-5-mini" in message.meta["model"]
140
140
  assert message.meta["finish_reason"] == "stop"
141
141
 
142
142
  @pytest.mark.skipif(
@@ -162,7 +162,7 @@ class TestOpenRouterChatGeneratorAsync:
162
162
  message: ChatMessage = results["replies"][0]
163
163
  assert "Paris" in message.text
164
164
 
165
- assert "openai/gpt-4o-mini" in message.meta["model"]
165
+ assert "openai/gpt-5-mini" in message.meta["model"]
166
166
  assert message.meta["finish_reason"] == "stop"
167
167
 
168
168
  assert counter > 1
@@ -262,3 +262,66 @@ class TestOpenRouterChatGeneratorAsync:
262
262
  assert tool_call.tool_name == "weather"
263
263
  assert tool_call.arguments == {"city": "Paris"}
264
264
  assert tool_message.meta["finish_reason"] == "tool_calls"
265
+
266
+ @pytest.mark.skipif(
267
+ not os.environ.get("OPENROUTER_API_KEY", None),
268
+ reason="Export an env var called OPENROUTER_API_KEY containing the OpenRouter API key to run this test.",
269
+ )
270
+ @pytest.mark.integration
271
+ @pytest.mark.asyncio
272
+ async def test_integration_mixing_tools_and_toolset_async(self):
273
+ """Test mixing Tool list and Toolset at runtime in async mode."""
274
+
275
+ def weather_function(city: str) -> str:
276
+ """Get weather information for a city."""
277
+ return f"Weather in {city}: 22°C, sunny"
278
+
279
+ def time_function(city: str) -> str:
280
+ """Get current time in a city."""
281
+ return f"Current time in {city}: 14:30"
282
+
283
+ def echo_function(text: str) -> str:
284
+ """Echo a text."""
285
+ return text
286
+
287
+ # Create tools
288
+ weather_tool = Tool(
289
+ name="weather",
290
+ description="Get weather information for a city",
291
+ parameters={"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]},
292
+ function=weather_function,
293
+ )
294
+
295
+ time_tool = Tool(
296
+ name="time",
297
+ description="Get current time in a city",
298
+ parameters={"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]},
299
+ function=time_function,
300
+ )
301
+
302
+ echo_tool = Tool(
303
+ name="echo",
304
+ description="Echo a text",
305
+ parameters={"type": "object", "properties": {"text": {"type": "string"}}, "required": ["text"]},
306
+ function=echo_function,
307
+ )
308
+
309
+ # Create Toolset with weather and time tools
310
+ toolset = Toolset([weather_tool, time_tool])
311
+
312
+ # Initialize with no tools, we'll pass them at runtime
313
+ component = OpenRouterChatGenerator()
314
+
315
+ # Pass mixed list: echo_tool (individual) and toolset (weather + time) at runtime
316
+ # This tests that both individual tools and toolsets can be combined
317
+ messages = [ChatMessage.from_user("Echo this: Hello World")]
318
+ results = await component.run_async(messages, tools=[echo_tool, toolset])
319
+
320
+ assert len(results["replies"]) == 1
321
+ message = results["replies"][0]
322
+
323
+ # Should be able to use echo_tool from the runtime mixed list
324
+ assert message.tool_calls is not None
325
+ tool_call = message.tool_calls[0]
326
+ assert tool_call.tool_name == "echo"
327
+ assert tool_call.arguments == {"text": "Hello World"}