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.
Files changed (17) hide show
  1. openrouter_haystack-0.2.2/CHANGELOG.md +30 -0
  2. {openrouter_haystack-0.2.0 → openrouter_haystack-0.2.2}/PKG-INFO +7 -13
  3. openrouter_haystack-0.2.2/README.md +15 -0
  4. {openrouter_haystack-0.2.0 → openrouter_haystack-0.2.2}/src/haystack_integrations/components/generators/openrouter/chat/chat_generator.py +3 -0
  5. {openrouter_haystack-0.2.0 → openrouter_haystack-0.2.2}/tests/test_openrouter_chat_generator.py +394 -57
  6. openrouter_haystack-0.2.0/CHANGELOG.md +0 -9
  7. openrouter_haystack-0.2.0/README.md +0 -21
  8. {openrouter_haystack-0.2.0 → openrouter_haystack-0.2.2}/.gitignore +0 -0
  9. {openrouter_haystack-0.2.0 → openrouter_haystack-0.2.2}/LICENSE.txt +0 -0
  10. {openrouter_haystack-0.2.0 → openrouter_haystack-0.2.2}/examples/openrouter_with_tools_example.py +0 -0
  11. {openrouter_haystack-0.2.0 → openrouter_haystack-0.2.2}/pydoc/config.yml +0 -0
  12. {openrouter_haystack-0.2.0 → openrouter_haystack-0.2.2}/pyproject.toml +0 -0
  13. {openrouter_haystack-0.2.0 → openrouter_haystack-0.2.2}/src/haystack_integrations/components/generators/openrouter/__init__.py +0 -0
  14. {openrouter_haystack-0.2.0 → openrouter_haystack-0.2.2}/src/haystack_integrations/components/generators/openrouter/chat/__init__.py +0 -0
  15. {openrouter_haystack-0.2.0 → openrouter_haystack-0.2.2}/src/haystack_integrations/components/generators/py.typed +0 -0
  16. {openrouter_haystack-0.2.0 → openrouter_haystack-0.2.2}/tests/__init__.py +0 -0
  17. {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.0
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
  [![PyPI - Version](https://img.shields.io/pypi/v/openrouter-haystack.svg)](https://pypi.org/project/openrouter-haystack)
27
27
  [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/openrouter-haystack.svg)](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
- **Table of Contents**
32
+ ---
32
33
 
33
- - [Installation](#installation)
34
- - [License](#license)
34
+ ## Contributing
35
35
 
36
- ## Installation
36
+ Refer to the general [Contribution Guidelines](https://github.com/deepset-ai/haystack-core-integrations/blob/main/CONTRIBUTING.md).
37
37
 
38
- ```console
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
+ [![PyPI - Version](https://img.shields.io/pypi/v/openrouter-haystack.svg)](https://pypi.org/project/openrouter-haystack)
4
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/openrouter-haystack.svg)](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
  }
@@ -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 MistralChatGenerator component can run with tools and get a response.
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"]) > 0, "No replies received"
345
+ assert len(results["replies"]) == 1
331
346
 
332
347
  # Find the message with tool calls
333
- tool_message = None
334
- for message in results["replies"]:
335
- if message.tool_call:
336
- tool_message = message
337
- break
338
-
339
- assert tool_message is not None, "No message with tool call found"
340
- assert isinstance(tool_message, ChatMessage), "Tool message is not a ChatMessage instance"
341
- assert ChatMessage.is_from(tool_message, ChatRole.ASSISTANT), "Tool message is not from the assistant"
342
-
343
- tool_call = tool_message.tool_call
344
- assert tool_call.id, "Tool call does not contain value for 'id' key"
345
- assert tool_call.tool_name == "weather"
346
- assert tool_call.arguments == {"city": "Paris"}
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=tool_call),
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 not final_message.tool_call
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?")], generation_kwargs={"tool_choice": "auto"}
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"]) > 0, "No replies received"
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 = None
398
- for message in results["replies"]:
399
- if message.tool_call:
400
- tool_message = message
401
- break
402
-
403
- assert tool_message is not None, "No message with tool call found"
404
- assert isinstance(tool_message, ChatMessage), "Tool message is not a ChatMessage instance"
405
- assert ChatMessage.is_from(tool_message, ChatRole.ASSISTANT), "Tool message is not from the assistant"
406
-
407
- tool_call = tool_message.tool_call
408
- assert tool_call.id, "Tool call does not contain value for 'id' key"
409
- assert tool_call.tool_name == "weather"
410
- assert tool_call.arguments == {"city": "Paris"}
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 MistralChatGenerator component can be used in a pipeline
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,9 +0,0 @@
1
- # Changelog
2
-
3
- ## [integrations/openrouter-v1.0.0] - 2025-05-19
4
-
5
- ### 🚀 Features
6
-
7
- - Support OpenRouter API as a Chat Generator (#1723)
8
-
9
- <!-- generated by git-cliff -->
@@ -1,21 +0,0 @@
1
- # openrouter-haystack
2
-
3
- [![PyPI - Version](https://img.shields.io/pypi/v/openrouter-haystack.svg)](https://pypi.org/project/openrouter-haystack)
4
- [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/openrouter-haystack.svg)](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.