openrouter-haystack 0.2.0__tar.gz → 0.2.2__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/CHANGELOG.md +30 -0
- {openrouter_haystack-0.2.0 → openrouter_haystack-0.2.2}/PKG-INFO +7 -13
- openrouter_haystack-0.2.2/README.md +15 -0
- {openrouter_haystack-0.2.0 → openrouter_haystack-0.2.2}/src/haystack_integrations/components/generators/openrouter/chat/chat_generator.py +3 -0
- {openrouter_haystack-0.2.0 → openrouter_haystack-0.2.2}/tests/test_openrouter_chat_generator.py +394 -57
- openrouter_haystack-0.2.0/CHANGELOG.md +0 -9
- openrouter_haystack-0.2.0/README.md +0 -21
- {openrouter_haystack-0.2.0 → openrouter_haystack-0.2.2}/.gitignore +0 -0
- {openrouter_haystack-0.2.0 → openrouter_haystack-0.2.2}/LICENSE.txt +0 -0
- {openrouter_haystack-0.2.0 → openrouter_haystack-0.2.2}/examples/openrouter_with_tools_example.py +0 -0
- {openrouter_haystack-0.2.0 → openrouter_haystack-0.2.2}/pydoc/config.yml +0 -0
- {openrouter_haystack-0.2.0 → openrouter_haystack-0.2.2}/pyproject.toml +0 -0
- {openrouter_haystack-0.2.0 → openrouter_haystack-0.2.2}/src/haystack_integrations/components/generators/openrouter/__init__.py +0 -0
- {openrouter_haystack-0.2.0 → openrouter_haystack-0.2.2}/src/haystack_integrations/components/generators/openrouter/chat/__init__.py +0 -0
- {openrouter_haystack-0.2.0 → openrouter_haystack-0.2.2}/src/haystack_integrations/components/generators/py.typed +0 -0
- {openrouter_haystack-0.2.0 → openrouter_haystack-0.2.2}/tests/__init__.py +0 -0
- {openrouter_haystack-0.2.0 → openrouter_haystack-0.2.2}/tests/test_openrouter_chat_generator_async.py +0 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [integrations/openrouter-v0.2.1] - 2025-08-07
|
|
4
|
+
|
|
5
|
+
### 🚀 Features
|
|
6
|
+
|
|
7
|
+
- Add ToolSet support and check streaming chunk conversion works as expected in OpenRouterChatGenerator (#1965)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## [integrations/openrouter-v0.2.0] - 2025-07-01
|
|
11
|
+
|
|
12
|
+
### 🚀 Features
|
|
13
|
+
|
|
14
|
+
- Add OpenRouter integration (#1723)
|
|
15
|
+
|
|
16
|
+
### 🐛 Bug Fixes
|
|
17
|
+
|
|
18
|
+
- Fix openrouter types + add py.typed (#2029)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### 🧹 Chores
|
|
22
|
+
|
|
23
|
+
- Align core-integrations Hatch scripts (#1898)
|
|
24
|
+
- Remove black (#1985)
|
|
25
|
+
|
|
26
|
+
### 🌀 Miscellaneous
|
|
27
|
+
|
|
28
|
+
- Test: Remove `test_check_abnormal_completions` - already tested in Haystack (#1842)
|
|
29
|
+
|
|
30
|
+
<!-- generated by git-cliff -->
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openrouter-haystack
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
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
|
|
@@ -26,19 +26,13 @@ Description-Content-Type: text/markdown
|
|
|
26
26
|
[](https://pypi.org/project/openrouter-haystack)
|
|
27
27
|
[](https://pypi.org/project/openrouterhaystack)
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
- [Integration page](https://haystack.deepset.ai/integrations/openrouter)
|
|
30
|
+
- [Changelog](https://github.com/deepset-ai/haystack-core-integrations/blob/main/integrations/openrouter/CHANGELOG.md)
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
---
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
- [License](#license)
|
|
34
|
+
## Contributing
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
Refer to the general [Contribution Guidelines](https://github.com/deepset-ai/haystack-core-integrations/blob/main/CONTRIBUTING.md).
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
pip install openrouter-haystack
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
## License
|
|
43
|
-
|
|
44
|
-
`openrouter-haystack` is distributed under the terms of the [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html) license.
|
|
38
|
+
To run integration tests locally, you need to export the `OPENROUTER_API_KEY` environment variable.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# openrouter-haystack
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/openrouter-haystack)
|
|
4
|
+
[](https://pypi.org/project/openrouterhaystack)
|
|
5
|
+
|
|
6
|
+
- [Integration page](https://haystack.deepset.ai/integrations/openrouter)
|
|
7
|
+
- [Changelog](https://github.com/deepset-ai/haystack-core-integrations/blob/main/integrations/openrouter/CHANGELOG.md)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Contributing
|
|
12
|
+
|
|
13
|
+
Refer to the general [Contribution Guidelines](https://github.com/deepset-ai/haystack-core-integrations/blob/main/CONTRIBUTING.md).
|
|
14
|
+
|
|
15
|
+
To run integration tests locally, you need to export the `OPENROUTER_API_KEY` environment variable.
|
|
@@ -172,6 +172,8 @@ class OpenRouterChatGenerator(OpenAIChatGenerator):
|
|
|
172
172
|
openai_formatted_messages = [message.to_openai_dict_format() for message in messages]
|
|
173
173
|
|
|
174
174
|
tools = tools or self.tools
|
|
175
|
+
if isinstance(tools, Toolset):
|
|
176
|
+
tools = list(tools)
|
|
175
177
|
tools_strict = tools_strict if tools_strict is not None else self.tools_strict
|
|
176
178
|
_check_duplicate_tool_names(list(tools or []))
|
|
177
179
|
|
|
@@ -197,4 +199,5 @@ class OpenRouterChatGenerator(OpenAIChatGenerator):
|
|
|
197
199
|
**openai_tools,
|
|
198
200
|
"extra_body": {**generation_kwargs},
|
|
199
201
|
"extra_headers": {**extra_headers},
|
|
202
|
+
"openai_endpoint": "create",
|
|
200
203
|
}
|
{openrouter_haystack-0.2.0 → openrouter_haystack-0.2.2}/tests/test_openrouter_chat_generator.py
RENAMED
|
@@ -11,12 +11,27 @@ from haystack.dataclasses import ChatMessage, ChatRole, StreamingChunk, ToolCall
|
|
|
11
11
|
from haystack.tools import Tool
|
|
12
12
|
from haystack.utils.auth import Secret
|
|
13
13
|
from openai import OpenAIError
|
|
14
|
-
from openai.types.chat import ChatCompletion, ChatCompletionMessage
|
|
14
|
+
from openai.types.chat import ChatCompletion, ChatCompletionChunk, ChatCompletionMessage
|
|
15
15
|
from openai.types.chat.chat_completion import Choice
|
|
16
|
+
from openai.types.chat.chat_completion_chunk import Choice as ChoiceChunk
|
|
17
|
+
from openai.types.chat.chat_completion_chunk import ChoiceDelta, ChoiceDeltaToolCall, ChoiceDeltaToolCallFunction
|
|
18
|
+
from openai.types.completion_usage import CompletionTokensDetails, CompletionUsage, PromptTokensDetails
|
|
16
19
|
|
|
17
20
|
from haystack_integrations.components.generators.openrouter.chat.chat_generator import OpenRouterChatGenerator
|
|
18
21
|
|
|
19
22
|
|
|
23
|
+
class CollectorCallback:
|
|
24
|
+
"""
|
|
25
|
+
Callback to collect streaming chunks for testing purposes.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self):
|
|
29
|
+
self.chunks = []
|
|
30
|
+
|
|
31
|
+
def __call__(self, chunk: StreamingChunk) -> None:
|
|
32
|
+
self.chunks.append(chunk)
|
|
33
|
+
|
|
34
|
+
|
|
20
35
|
@pytest.fixture
|
|
21
36
|
def chat_messages():
|
|
22
37
|
return [
|
|
@@ -81,7 +96,7 @@ class TestOpenRouterChatGenerator:
|
|
|
81
96
|
|
|
82
97
|
def test_init_fail_wo_api_key(self, monkeypatch):
|
|
83
98
|
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
|
|
84
|
-
with pytest.raises(ValueError, match="None of the .* environment variables are set"):
|
|
99
|
+
with pytest.raises(ValueError, match=r"None of the .* environment variables are set"):
|
|
85
100
|
OpenRouterChatGenerator()
|
|
86
101
|
|
|
87
102
|
def test_init_with_parameters(self):
|
|
@@ -208,7 +223,7 @@ class TestOpenRouterChatGenerator:
|
|
|
208
223
|
"max_retries": 10,
|
|
209
224
|
},
|
|
210
225
|
}
|
|
211
|
-
with pytest.raises(ValueError, match="None of the .* environment variables are set"):
|
|
226
|
+
with pytest.raises(ValueError, match=r"None of the .* environment variables are set"):
|
|
212
227
|
OpenRouterChatGenerator.from_dict(data)
|
|
213
228
|
|
|
214
229
|
def test_run(self, chat_messages, mock_chat_completion, monkeypatch): # noqa: ARG002
|
|
@@ -321,44 +336,46 @@ class TestOpenRouterChatGenerator:
|
|
|
321
336
|
@pytest.mark.integration
|
|
322
337
|
def test_live_run_with_tools_and_response(self, tools):
|
|
323
338
|
"""
|
|
324
|
-
Integration test that the
|
|
339
|
+
Integration test that the OpenRouterChatGenerator component can run with tools and get a response.
|
|
325
340
|
"""
|
|
326
|
-
initial_messages = [ChatMessage.from_user("What's the weather like in Paris?")]
|
|
341
|
+
initial_messages = [ChatMessage.from_user("What's the weather like in Paris and Berlin?")]
|
|
327
342
|
component = OpenRouterChatGenerator(tools=tools)
|
|
328
343
|
results = component.run(messages=initial_messages, generation_kwargs={"tool_choice": "auto"})
|
|
329
344
|
|
|
330
|
-
assert len(results["replies"])
|
|
345
|
+
assert len(results["replies"]) == 1
|
|
331
346
|
|
|
332
347
|
# Find the message with tool calls
|
|
333
|
-
tool_message =
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
assert
|
|
348
|
+
tool_message = results["replies"][0]
|
|
349
|
+
|
|
350
|
+
assert isinstance(tool_message, ChatMessage)
|
|
351
|
+
tool_calls = tool_message.tool_calls
|
|
352
|
+
assert len(tool_calls) == 2
|
|
353
|
+
assert ChatMessage.is_from(tool_message, ChatRole.ASSISTANT)
|
|
354
|
+
|
|
355
|
+
for tool_call in tool_calls:
|
|
356
|
+
assert tool_call.id is not None
|
|
357
|
+
assert isinstance(tool_call, ToolCall)
|
|
358
|
+
assert tool_call.tool_name == "weather"
|
|
359
|
+
|
|
360
|
+
arguments = [tool_call.arguments for tool_call in tool_calls]
|
|
361
|
+
assert sorted(arguments, key=lambda x: x["city"]) == [{"city": "Berlin"}, {"city": "Paris"}]
|
|
347
362
|
assert tool_message.meta["finish_reason"] == "tool_calls"
|
|
348
363
|
|
|
349
364
|
new_messages = [
|
|
350
365
|
initial_messages[0],
|
|
351
366
|
tool_message,
|
|
352
|
-
ChatMessage.from_tool(tool_result="22° C", origin=
|
|
367
|
+
ChatMessage.from_tool(tool_result="22° C and sunny", origin=tool_calls[0]),
|
|
368
|
+
ChatMessage.from_tool(tool_result="16° C and windy", origin=tool_calls[1]),
|
|
353
369
|
]
|
|
354
370
|
# Pass the tool result to the model to get the final response
|
|
355
371
|
results = component.run(new_messages)
|
|
356
372
|
|
|
357
373
|
assert len(results["replies"]) == 1
|
|
358
374
|
final_message = results["replies"][0]
|
|
359
|
-
assert
|
|
375
|
+
assert final_message.is_from(ChatRole.ASSISTANT)
|
|
360
376
|
assert len(final_message.text) > 0
|
|
361
377
|
assert "paris" in final_message.text.lower()
|
|
378
|
+
assert "berlin" in final_message.text.lower()
|
|
362
379
|
|
|
363
380
|
@pytest.mark.skipif(
|
|
364
381
|
not os.environ.get("OPENROUTER_API_KEY", None),
|
|
@@ -369,45 +386,29 @@ class TestOpenRouterChatGenerator:
|
|
|
369
386
|
"""
|
|
370
387
|
Integration test that the OpenRouterChatGenerator component can run with tools and streaming.
|
|
371
388
|
"""
|
|
372
|
-
|
|
373
|
-
class Callback:
|
|
374
|
-
def __init__(self):
|
|
375
|
-
self.responses = ""
|
|
376
|
-
self.counter = 0
|
|
377
|
-
self.tool_calls = []
|
|
378
|
-
|
|
379
|
-
def __call__(self, chunk: StreamingChunk) -> None:
|
|
380
|
-
self.counter += 1
|
|
381
|
-
if chunk.content:
|
|
382
|
-
self.responses += chunk.content
|
|
383
|
-
if chunk.meta.get("tool_calls"):
|
|
384
|
-
self.tool_calls.extend(chunk.meta["tool_calls"])
|
|
385
|
-
|
|
386
|
-
callback = Callback()
|
|
387
|
-
component = OpenRouterChatGenerator(tools=tools, streaming_callback=callback)
|
|
389
|
+
component = OpenRouterChatGenerator(tools=tools, streaming_callback=print_streaming_chunk)
|
|
388
390
|
results = component.run(
|
|
389
|
-
[ChatMessage.from_user("What's the weather like in Paris?")],
|
|
391
|
+
[ChatMessage.from_user("What's the weather like in Paris and Berlin?")],
|
|
392
|
+
generation_kwargs={"tool_choice": "auto"},
|
|
390
393
|
)
|
|
391
394
|
|
|
392
|
-
assert len(results["replies"])
|
|
393
|
-
assert callback.counter > 1, "Streaming callback was not called multiple times"
|
|
394
|
-
assert callback.tool_calls, "No tool calls received in streaming"
|
|
395
|
+
assert len(results["replies"]) == 1
|
|
395
396
|
|
|
396
397
|
# Find the message with tool calls
|
|
397
|
-
tool_message =
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
assert
|
|
398
|
+
tool_message = results["replies"][0]
|
|
399
|
+
|
|
400
|
+
assert isinstance(tool_message, ChatMessage)
|
|
401
|
+
tool_calls = tool_message.tool_calls
|
|
402
|
+
assert len(tool_calls) == 2
|
|
403
|
+
assert ChatMessage.is_from(tool_message, ChatRole.ASSISTANT)
|
|
404
|
+
|
|
405
|
+
for tool_call in tool_calls:
|
|
406
|
+
assert tool_call.id is not None
|
|
407
|
+
assert isinstance(tool_call, ToolCall)
|
|
408
|
+
assert tool_call.tool_name == "weather"
|
|
409
|
+
|
|
410
|
+
arguments = [tool_call.arguments for tool_call in tool_calls]
|
|
411
|
+
assert sorted(arguments, key=lambda x: x["city"]) == [{"city": "Berlin"}, {"city": "Paris"}]
|
|
411
412
|
assert tool_message.meta["finish_reason"] == "tool_calls"
|
|
412
413
|
|
|
413
414
|
@pytest.mark.skipif(
|
|
@@ -417,7 +418,7 @@ class TestOpenRouterChatGenerator:
|
|
|
417
418
|
@pytest.mark.integration
|
|
418
419
|
def test_pipeline_with_openrouter_chat_generator(self, tools):
|
|
419
420
|
"""
|
|
420
|
-
Test that the
|
|
421
|
+
Test that the OpenRouterChatGenerator component can be used in a pipeline
|
|
421
422
|
"""
|
|
422
423
|
pipeline = Pipeline()
|
|
423
424
|
pipeline.add_component("generator", OpenRouterChatGenerator(tools=tools))
|
|
@@ -537,3 +538,339 @@ class TestOpenRouterChatGenerator:
|
|
|
537
538
|
assert loaded_generator.tools[0].name == generator.tools[0].name
|
|
538
539
|
assert loaded_generator.tools[0].description == generator.tools[0].description
|
|
539
540
|
assert loaded_generator.tools[0].parameters == generator.tools[0].parameters
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
class TestChatCompletionChunkConversion:
|
|
544
|
+
def test_handle_stream_response(self):
|
|
545
|
+
openrouter_chunks = [
|
|
546
|
+
ChatCompletionChunk(
|
|
547
|
+
id="gen-1750162525-tc7ParBHvsqd6rYhCDtK",
|
|
548
|
+
choices=[
|
|
549
|
+
ChoiceChunk(delta=ChoiceDelta(content="", role="assistant"), index=0, native_finish_reason=None)
|
|
550
|
+
],
|
|
551
|
+
created=1750162525,
|
|
552
|
+
model="openai/gpt-4o-mini",
|
|
553
|
+
object="chat.completion.chunk",
|
|
554
|
+
system_fingerprint="fp_34a54ae93c",
|
|
555
|
+
provider="OpenAI",
|
|
556
|
+
),
|
|
557
|
+
ChatCompletionChunk(
|
|
558
|
+
id="gen-1750162525-tc7ParBHvsqd6rYhCDtK",
|
|
559
|
+
choices=[
|
|
560
|
+
ChoiceChunk(
|
|
561
|
+
delta=ChoiceDelta(
|
|
562
|
+
role="assistant",
|
|
563
|
+
tool_calls=[
|
|
564
|
+
ChoiceDeltaToolCall(
|
|
565
|
+
index=0,
|
|
566
|
+
id="call_zznlVyVfK0GJwY28SShJpDCh",
|
|
567
|
+
function=ChoiceDeltaToolCallFunction(arguments="", name="weather"),
|
|
568
|
+
type="function",
|
|
569
|
+
)
|
|
570
|
+
],
|
|
571
|
+
),
|
|
572
|
+
index=0,
|
|
573
|
+
native_finish_reason=None,
|
|
574
|
+
)
|
|
575
|
+
],
|
|
576
|
+
created=1750162525,
|
|
577
|
+
model="openai/gpt-4o-mini",
|
|
578
|
+
object="chat.completion.chunk",
|
|
579
|
+
system_fingerprint="fp_34a54ae93c",
|
|
580
|
+
provider="OpenAI",
|
|
581
|
+
),
|
|
582
|
+
ChatCompletionChunk(
|
|
583
|
+
id="gen-1750162525-tc7ParBHvsqd6rYhCDtK",
|
|
584
|
+
choices=[
|
|
585
|
+
ChoiceChunk(
|
|
586
|
+
delta=ChoiceDelta(
|
|
587
|
+
role="assistant",
|
|
588
|
+
tool_calls=[
|
|
589
|
+
ChoiceDeltaToolCall(
|
|
590
|
+
index=0,
|
|
591
|
+
function=ChoiceDeltaToolCallFunction(arguments='{"ci'),
|
|
592
|
+
type="function",
|
|
593
|
+
)
|
|
594
|
+
],
|
|
595
|
+
),
|
|
596
|
+
index=0,
|
|
597
|
+
native_finish_reason=None,
|
|
598
|
+
)
|
|
599
|
+
],
|
|
600
|
+
created=1750162525,
|
|
601
|
+
model="openai/gpt-4o-mini",
|
|
602
|
+
object="chat.completion.chunk",
|
|
603
|
+
system_fingerprint="fp_34a54ae93c",
|
|
604
|
+
provider="OpenAI",
|
|
605
|
+
),
|
|
606
|
+
ChatCompletionChunk(
|
|
607
|
+
id="gen-1750162525-tc7ParBHvsqd6rYhCDtK",
|
|
608
|
+
choices=[
|
|
609
|
+
ChoiceChunk(
|
|
610
|
+
delta=ChoiceDelta(
|
|
611
|
+
role="assistant",
|
|
612
|
+
tool_calls=[
|
|
613
|
+
ChoiceDeltaToolCall(
|
|
614
|
+
index=0,
|
|
615
|
+
function=ChoiceDeltaToolCallFunction(arguments='ty": '),
|
|
616
|
+
type="function",
|
|
617
|
+
)
|
|
618
|
+
],
|
|
619
|
+
),
|
|
620
|
+
index=0,
|
|
621
|
+
native_finish_reason=None,
|
|
622
|
+
)
|
|
623
|
+
],
|
|
624
|
+
created=1750162525,
|
|
625
|
+
model="openai/gpt-4o-mini",
|
|
626
|
+
object="chat.completion.chunk",
|
|
627
|
+
system_fingerprint="fp_34a54ae93c",
|
|
628
|
+
provider="OpenAI",
|
|
629
|
+
),
|
|
630
|
+
ChatCompletionChunk(
|
|
631
|
+
id="gen-1750162525-tc7ParBHvsqd6rYhCDtK",
|
|
632
|
+
choices=[
|
|
633
|
+
ChoiceChunk(
|
|
634
|
+
delta=ChoiceDelta(
|
|
635
|
+
role="assistant",
|
|
636
|
+
tool_calls=[
|
|
637
|
+
ChoiceDeltaToolCall(
|
|
638
|
+
index=0,
|
|
639
|
+
function=ChoiceDeltaToolCallFunction(arguments='"Paris'),
|
|
640
|
+
type="function",
|
|
641
|
+
)
|
|
642
|
+
],
|
|
643
|
+
),
|
|
644
|
+
index=0,
|
|
645
|
+
native_finish_reason=None,
|
|
646
|
+
)
|
|
647
|
+
],
|
|
648
|
+
created=1750162525,
|
|
649
|
+
model="openai/gpt-4o-mini",
|
|
650
|
+
object="chat.completion.chunk",
|
|
651
|
+
system_fingerprint="fp_34a54ae93c",
|
|
652
|
+
provider="OpenAI",
|
|
653
|
+
),
|
|
654
|
+
ChatCompletionChunk(
|
|
655
|
+
id="gen-1750162525-tc7ParBHvsqd6rYhCDtK",
|
|
656
|
+
choices=[
|
|
657
|
+
ChoiceChunk(
|
|
658
|
+
delta=ChoiceDelta(
|
|
659
|
+
role="assistant",
|
|
660
|
+
tool_calls=[
|
|
661
|
+
ChoiceDeltaToolCall(
|
|
662
|
+
index=0,
|
|
663
|
+
function=ChoiceDeltaToolCallFunction(arguments='"}'),
|
|
664
|
+
type="function",
|
|
665
|
+
)
|
|
666
|
+
],
|
|
667
|
+
),
|
|
668
|
+
index=0,
|
|
669
|
+
native_finish_reason=None,
|
|
670
|
+
)
|
|
671
|
+
],
|
|
672
|
+
created=1750162525,
|
|
673
|
+
model="openai/gpt-4o-mini",
|
|
674
|
+
object="chat.completion.chunk",
|
|
675
|
+
system_fingerprint="fp_34a54ae93c",
|
|
676
|
+
provider="OpenAI",
|
|
677
|
+
),
|
|
678
|
+
ChatCompletionChunk(
|
|
679
|
+
id="gen-1750162525-tc7ParBHvsqd6rYhCDtK",
|
|
680
|
+
choices=[
|
|
681
|
+
ChoiceChunk(
|
|
682
|
+
delta=ChoiceDelta(
|
|
683
|
+
role="assistant",
|
|
684
|
+
tool_calls=[
|
|
685
|
+
ChoiceDeltaToolCall(
|
|
686
|
+
index=1,
|
|
687
|
+
id="call_Mh1uOyW3Ys4gwydHjNHILHGX",
|
|
688
|
+
function=ChoiceDeltaToolCallFunction(arguments="", name="weather"),
|
|
689
|
+
type="function",
|
|
690
|
+
)
|
|
691
|
+
],
|
|
692
|
+
),
|
|
693
|
+
index=0,
|
|
694
|
+
native_finish_reason=None,
|
|
695
|
+
)
|
|
696
|
+
],
|
|
697
|
+
created=1750162525,
|
|
698
|
+
model="openai/gpt-4o-mini",
|
|
699
|
+
object="chat.completion.chunk",
|
|
700
|
+
service_tier=None,
|
|
701
|
+
system_fingerprint="fp_34a54ae93c",
|
|
702
|
+
usage=None,
|
|
703
|
+
provider="OpenAI",
|
|
704
|
+
),
|
|
705
|
+
ChatCompletionChunk(
|
|
706
|
+
id="gen-1750162525-tc7ParBHvsqd6rYhCDtK",
|
|
707
|
+
choices=[
|
|
708
|
+
ChoiceChunk(
|
|
709
|
+
delta=ChoiceDelta(
|
|
710
|
+
role="assistant",
|
|
711
|
+
tool_calls=[
|
|
712
|
+
ChoiceDeltaToolCall(
|
|
713
|
+
index=1,
|
|
714
|
+
id=None,
|
|
715
|
+
function=ChoiceDeltaToolCallFunction(arguments='{"ci'),
|
|
716
|
+
type="function",
|
|
717
|
+
)
|
|
718
|
+
],
|
|
719
|
+
),
|
|
720
|
+
index=0,
|
|
721
|
+
native_finish_reason=None,
|
|
722
|
+
)
|
|
723
|
+
],
|
|
724
|
+
created=1750162525,
|
|
725
|
+
model="openai/gpt-4o-mini",
|
|
726
|
+
object="chat.completion.chunk",
|
|
727
|
+
system_fingerprint="fp_34a54ae93c",
|
|
728
|
+
provider="OpenAI",
|
|
729
|
+
),
|
|
730
|
+
ChatCompletionChunk(
|
|
731
|
+
id="gen-1750162525-tc7ParBHvsqd6rYhCDtK",
|
|
732
|
+
choices=[
|
|
733
|
+
ChoiceChunk(
|
|
734
|
+
delta=ChoiceDelta(
|
|
735
|
+
role="assistant",
|
|
736
|
+
tool_calls=[
|
|
737
|
+
ChoiceDeltaToolCall(
|
|
738
|
+
index=1,
|
|
739
|
+
function=ChoiceDeltaToolCallFunction(arguments='ty": '),
|
|
740
|
+
type="function",
|
|
741
|
+
)
|
|
742
|
+
],
|
|
743
|
+
),
|
|
744
|
+
index=0,
|
|
745
|
+
native_finish_reason=None,
|
|
746
|
+
)
|
|
747
|
+
],
|
|
748
|
+
created=1750162525,
|
|
749
|
+
model="openai/gpt-4o-mini",
|
|
750
|
+
object="chat.completion.chunk",
|
|
751
|
+
system_fingerprint="fp_34a54ae93c",
|
|
752
|
+
provider="OpenAI",
|
|
753
|
+
),
|
|
754
|
+
ChatCompletionChunk(
|
|
755
|
+
id="gen-1750162525-tc7ParBHvsqd6rYhCDtK",
|
|
756
|
+
choices=[
|
|
757
|
+
ChoiceChunk(
|
|
758
|
+
delta=ChoiceDelta(
|
|
759
|
+
role="assistant",
|
|
760
|
+
tool_calls=[
|
|
761
|
+
ChoiceDeltaToolCall(
|
|
762
|
+
index=1,
|
|
763
|
+
function=ChoiceDeltaToolCallFunction(arguments='"Berli'),
|
|
764
|
+
type="function",
|
|
765
|
+
)
|
|
766
|
+
],
|
|
767
|
+
),
|
|
768
|
+
index=0,
|
|
769
|
+
native_finish_reason=None,
|
|
770
|
+
)
|
|
771
|
+
],
|
|
772
|
+
created=1750162525,
|
|
773
|
+
model="openai/gpt-4o-mini",
|
|
774
|
+
object="chat.completion.chunk",
|
|
775
|
+
system_fingerprint="fp_34a54ae93c",
|
|
776
|
+
provider="OpenAI",
|
|
777
|
+
),
|
|
778
|
+
ChatCompletionChunk(
|
|
779
|
+
id="gen-1750162525-tc7ParBHvsqd6rYhCDtK",
|
|
780
|
+
choices=[
|
|
781
|
+
ChoiceChunk(
|
|
782
|
+
delta=ChoiceDelta(
|
|
783
|
+
role="assistant",
|
|
784
|
+
tool_calls=[
|
|
785
|
+
ChoiceDeltaToolCall(
|
|
786
|
+
index=1,
|
|
787
|
+
function=ChoiceDeltaToolCallFunction(arguments='n"}'),
|
|
788
|
+
type="function",
|
|
789
|
+
)
|
|
790
|
+
],
|
|
791
|
+
),
|
|
792
|
+
index=0,
|
|
793
|
+
native_finish_reason=None,
|
|
794
|
+
)
|
|
795
|
+
],
|
|
796
|
+
created=1750162525,
|
|
797
|
+
model="openai/gpt-4o-mini",
|
|
798
|
+
object="chat.completion.chunk",
|
|
799
|
+
system_fingerprint="fp_34a54ae93c",
|
|
800
|
+
provider="OpenAI",
|
|
801
|
+
),
|
|
802
|
+
ChatCompletionChunk(
|
|
803
|
+
id="gen-1750162525-tc7ParBHvsqd6rYhCDtK",
|
|
804
|
+
choices=[
|
|
805
|
+
ChoiceChunk(
|
|
806
|
+
delta=ChoiceDelta(content="", role="assistant"),
|
|
807
|
+
finish_reason="tool_calls",
|
|
808
|
+
index=0,
|
|
809
|
+
native_finish_reason="tool_calls",
|
|
810
|
+
)
|
|
811
|
+
],
|
|
812
|
+
created=1750162525,
|
|
813
|
+
model="openai/gpt-4o-mini",
|
|
814
|
+
object="chat.completion.chunk",
|
|
815
|
+
system_fingerprint="fp_34a54ae93c",
|
|
816
|
+
provider="OpenAI",
|
|
817
|
+
),
|
|
818
|
+
ChatCompletionChunk(
|
|
819
|
+
id="gen-1750162525-tc7ParBHvsqd6rYhCDtK",
|
|
820
|
+
choices=[
|
|
821
|
+
ChoiceChunk(
|
|
822
|
+
delta=ChoiceDelta(content="", role="assistant"),
|
|
823
|
+
index=0,
|
|
824
|
+
native_finish_reason=None,
|
|
825
|
+
)
|
|
826
|
+
],
|
|
827
|
+
created=1750162525,
|
|
828
|
+
model="openai/gpt-4o-mini",
|
|
829
|
+
object="chat.completion.chunk",
|
|
830
|
+
usage=CompletionUsage(
|
|
831
|
+
completion_tokens=42,
|
|
832
|
+
prompt_tokens=55,
|
|
833
|
+
total_tokens=97,
|
|
834
|
+
completion_tokens_details=CompletionTokensDetails(reasoning_tokens=0),
|
|
835
|
+
prompt_tokens_details=PromptTokensDetails(cached_tokens=0),
|
|
836
|
+
),
|
|
837
|
+
provider="OpenAI",
|
|
838
|
+
),
|
|
839
|
+
]
|
|
840
|
+
|
|
841
|
+
collector_callback = CollectorCallback()
|
|
842
|
+
llm = OpenRouterChatGenerator(api_key=Secret.from_token("test-api-key"))
|
|
843
|
+
result = llm._handle_stream_response(openrouter_chunks, callback=collector_callback)[0] # type: ignore
|
|
844
|
+
|
|
845
|
+
# Assert text is empty
|
|
846
|
+
assert result.text is None
|
|
847
|
+
|
|
848
|
+
# Verify both tool calls were found and processed
|
|
849
|
+
assert len(result.tool_calls) == 2
|
|
850
|
+
assert result.tool_calls[0].id == "call_zznlVyVfK0GJwY28SShJpDCh"
|
|
851
|
+
assert result.tool_calls[0].tool_name == "weather"
|
|
852
|
+
assert result.tool_calls[0].arguments == {"city": "Paris"}
|
|
853
|
+
assert result.tool_calls[1].id == "call_Mh1uOyW3Ys4gwydHjNHILHGX"
|
|
854
|
+
assert result.tool_calls[1].tool_name == "weather"
|
|
855
|
+
assert result.tool_calls[1].arguments == {"city": "Berlin"}
|
|
856
|
+
|
|
857
|
+
# Verify meta information
|
|
858
|
+
assert result.meta["model"] == "openai/gpt-4o-mini"
|
|
859
|
+
assert result.meta["finish_reason"] == "tool_calls"
|
|
860
|
+
assert result.meta["index"] == 0
|
|
861
|
+
assert result.meta["completion_start_time"] is not None
|
|
862
|
+
assert result.meta["usage"] == {
|
|
863
|
+
"completion_tokens": 42,
|
|
864
|
+
"prompt_tokens": 55,
|
|
865
|
+
"total_tokens": 97,
|
|
866
|
+
"completion_tokens_details": {
|
|
867
|
+
"accepted_prediction_tokens": None,
|
|
868
|
+
"audio_tokens": None,
|
|
869
|
+
"reasoning_tokens": 0,
|
|
870
|
+
"rejected_prediction_tokens": None,
|
|
871
|
+
},
|
|
872
|
+
"prompt_tokens_details": {
|
|
873
|
+
"audio_tokens": None,
|
|
874
|
+
"cached_tokens": 0,
|
|
875
|
+
},
|
|
876
|
+
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
# openrouter-haystack
|
|
2
|
-
|
|
3
|
-
[](https://pypi.org/project/openrouter-haystack)
|
|
4
|
-
[](https://pypi.org/project/openrouterhaystack)
|
|
5
|
-
|
|
6
|
-
-----
|
|
7
|
-
|
|
8
|
-
**Table of Contents**
|
|
9
|
-
|
|
10
|
-
- [Installation](#installation)
|
|
11
|
-
- [License](#license)
|
|
12
|
-
|
|
13
|
-
## Installation
|
|
14
|
-
|
|
15
|
-
```console
|
|
16
|
-
pip install openrouter-haystack
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## License
|
|
20
|
-
|
|
21
|
-
`openrouter-haystack` is distributed under the terms of the [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html) license.
|
|
File without changes
|
|
File without changes
|
{openrouter_haystack-0.2.0 → openrouter_haystack-0.2.2}/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
|
|
File without changes
|
|
File without changes
|