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.
- {openrouter_haystack-0.2.2 → openrouter_haystack-0.4.0}/CHANGELOG.md +24 -0
- {openrouter_haystack-0.2.2 → openrouter_haystack-0.4.0}/PKG-INFO +2 -2
- openrouter_haystack-0.4.0/examples/openrouter_with_structured_outputs.py +35 -0
- openrouter_haystack-0.4.0/pydoc/config_docusaurus.yml +28 -0
- {openrouter_haystack-0.2.2 → openrouter_haystack-0.4.0}/pyproject.toml +4 -4
- {openrouter_haystack-0.2.2 → openrouter_haystack-0.4.0}/src/haystack_integrations/components/generators/openrouter/chat/chat_generator.py +61 -35
- {openrouter_haystack-0.2.2 → openrouter_haystack-0.4.0}/tests/test_openrouter_chat_generator.py +156 -29
- {openrouter_haystack-0.2.2 → openrouter_haystack-0.4.0}/tests/test_openrouter_chat_generator_async.py +67 -4
- {openrouter_haystack-0.2.2 → openrouter_haystack-0.4.0}/.gitignore +0 -0
- {openrouter_haystack-0.2.2 → openrouter_haystack-0.4.0}/LICENSE.txt +0 -0
- {openrouter_haystack-0.2.2 → openrouter_haystack-0.4.0}/README.md +0 -0
- {openrouter_haystack-0.2.2 → openrouter_haystack-0.4.0}/examples/openrouter_with_tools_example.py +0 -0
- {openrouter_haystack-0.2.2 → openrouter_haystack-0.4.0}/pydoc/config.yml +0 -0
- {openrouter_haystack-0.2.2 → openrouter_haystack-0.4.0}/src/haystack_integrations/components/generators/openrouter/__init__.py +0 -0
- {openrouter_haystack-0.2.2 → openrouter_haystack-0.4.0}/src/haystack_integrations/components/generators/openrouter/chat/__init__.py +0 -0
- {openrouter_haystack-0.2.2 → openrouter_haystack-0.4.0}/src/haystack_integrations/components/generators/py.typed +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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 = '
|
|
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 = "
|
|
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,
|
|
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
|
|
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-
|
|
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-
|
|
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[
|
|
67
|
-
tools: Optional[
|
|
66
|
+
generation_kwargs: Optional[dict[str, Any]] = None,
|
|
67
|
+
tools: Optional[ToolsType] = None,
|
|
68
68
|
timeout: Optional[float] = None,
|
|
69
|
-
extra_headers: Optional[
|
|
69
|
+
extra_headers: Optional[dict[str, Any]] = None,
|
|
70
70
|
max_retries: Optional[int] = None,
|
|
71
|
-
http_client_kwargs: Optional[
|
|
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-
|
|
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) ->
|
|
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=
|
|
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:
|
|
169
|
+
messages: list[ChatMessage],
|
|
162
170
|
streaming_callback: Optional[StreamingCallbackT] = None,
|
|
163
|
-
generation_kwargs: Optional[
|
|
164
|
-
tools: Optional[
|
|
171
|
+
generation_kwargs: Optional[dict[str, Any]] = None,
|
|
172
|
+
tools: Optional[ToolsType] = None,
|
|
165
173
|
tools_strict: Optional[bool] = None,
|
|
166
|
-
) ->
|
|
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
|
-
|
|
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(
|
|
192
|
+
_check_duplicate_tool_names(flattened_tools)
|
|
179
193
|
|
|
180
194
|
openai_tools = {}
|
|
181
|
-
if
|
|
182
|
-
tool_definitions = [
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
"
|
|
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
|
{openrouter_haystack-0.2.2 → openrouter_haystack-0.4.0}/tests/test_openrouter_chat_generator.py
RENAMED
|
@@ -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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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"}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openrouter_haystack-0.2.2 → openrouter_haystack-0.4.0}/examples/openrouter_with_tools_example.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|