anthropic-haystack 6.3.0__tar.gz → 7.0.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 (29) hide show
  1. {anthropic_haystack-6.3.0 → anthropic_haystack-7.0.0}/CHANGELOG.md +7 -0
  2. {anthropic_haystack-6.3.0 → anthropic_haystack-7.0.0}/PKG-INFO +1 -1
  3. {anthropic_haystack-6.3.0 → anthropic_haystack-7.0.0}/src/haystack_integrations/components/generators/anthropic/chat/chat_generator.py +14 -18
  4. {anthropic_haystack-6.3.0 → anthropic_haystack-7.0.0}/src/haystack_integrations/components/generators/anthropic/chat/foundry_chat_generator.py +3 -4
  5. {anthropic_haystack-6.3.0 → anthropic_haystack-7.0.0}/src/haystack_integrations/components/generators/anthropic/chat/utils.py +16 -0
  6. {anthropic_haystack-6.3.0 → anthropic_haystack-7.0.0}/src/haystack_integrations/components/generators/anthropic/chat/vertex_chat_generator.py +3 -2
  7. {anthropic_haystack-6.3.0 → anthropic_haystack-7.0.0}/src/haystack_integrations/token_counters/anthropic/token_counter.py +1 -2
  8. {anthropic_haystack-6.3.0 → anthropic_haystack-7.0.0}/tests/test_chat_generator.py +17 -12
  9. {anthropic_haystack-6.3.0 → anthropic_haystack-7.0.0}/tests/test_chat_generator_async.py +4 -2
  10. {anthropic_haystack-6.3.0 → anthropic_haystack-7.0.0}/tests/test_utils.py +47 -4
  11. {anthropic_haystack-6.3.0 → anthropic_haystack-7.0.0}/.gitignore +0 -0
  12. {anthropic_haystack-6.3.0 → anthropic_haystack-7.0.0}/LICENSE.txt +0 -0
  13. {anthropic_haystack-6.3.0 → anthropic_haystack-7.0.0}/README.md +0 -0
  14. {anthropic_haystack-6.3.0 → anthropic_haystack-7.0.0}/example/documentation_rag_with_claude.py +0 -0
  15. {anthropic_haystack-6.3.0 → anthropic_haystack-7.0.0}/example/prompt_caching.py +0 -0
  16. {anthropic_haystack-6.3.0 → anthropic_haystack-7.0.0}/pydoc/config_docusaurus.yml +0 -0
  17. {anthropic_haystack-6.3.0 → anthropic_haystack-7.0.0}/pyproject.toml +0 -0
  18. {anthropic_haystack-6.3.0 → anthropic_haystack-7.0.0}/src/haystack_integrations/components/generators/anthropic/__init__.py +0 -0
  19. {anthropic_haystack-6.3.0 → anthropic_haystack-7.0.0}/src/haystack_integrations/components/generators/anthropic/chat/__init__.py +0 -0
  20. {anthropic_haystack-6.3.0 → anthropic_haystack-7.0.0}/src/haystack_integrations/components/generators/py.typed +0 -0
  21. {anthropic_haystack-6.3.0 → anthropic_haystack-7.0.0}/src/haystack_integrations/token_counters/anthropic/__init__.py +0 -0
  22. {anthropic_haystack-6.3.0 → anthropic_haystack-7.0.0}/src/haystack_integrations/token_counters/py.typed +0 -0
  23. {anthropic_haystack-6.3.0 → anthropic_haystack-7.0.0}/tests/__init__.py +0 -0
  24. {anthropic_haystack-6.3.0 → anthropic_haystack-7.0.0}/tests/conftest.py +0 -0
  25. {anthropic_haystack-6.3.0 → anthropic_haystack-7.0.0}/tests/test_files/apple.jpg +0 -0
  26. {anthropic_haystack-6.3.0 → anthropic_haystack-7.0.0}/tests/test_files/sample_pdf_3.pdf +0 -0
  27. {anthropic_haystack-6.3.0 → anthropic_haystack-7.0.0}/tests/test_foundry_chat_generator.py +0 -0
  28. {anthropic_haystack-6.3.0 → anthropic_haystack-7.0.0}/tests/test_token_counter.py +0 -0
  29. {anthropic_haystack-6.3.0 → anthropic_haystack-7.0.0}/tests/test_vertex_chat_generator.py +0 -0
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [integrations/anthropic-v6.3.0] - 2026-10-01
4
+
5
+ ### 🚜 Refactor
6
+
7
+ - Generalize flattened generation_kwargs for tool_choice/thinking/output_config (#4028)
8
+
9
+
3
10
  ## [integrations/anthropic-v6.2.0] - 2026-09-24
4
11
 
5
12
  ### 🚀 Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: anthropic-haystack
3
- Version: 6.3.0
3
+ Version: 7.0.0
4
4
  Summary: An integration of Anthropic Claude models into the Haystack framework.
5
5
  Project-URL: Documentation, https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/anthropic#readme
6
6
  Project-URL: Issues, https://github.com/deepset-ai/haystack-core-integrations/issues
@@ -19,7 +19,7 @@ from haystack.tools import (
19
19
  flatten_tools_or_toolsets,
20
20
  serialize_tools_or_toolset,
21
21
  )
22
- from haystack.utils.auth import Secret, deserialize_secrets_inplace
22
+ from haystack.utils.auth import Secret
23
23
  from haystack.utils.callable_serialization import deserialize_callable, serialize_callable
24
24
 
25
25
  from anthropic import Anthropic, AsyncAnthropic
@@ -37,6 +37,7 @@ from .utils import (
37
37
  _convert_messages_to_anthropic_format,
38
38
  _extract_citations,
39
39
  _has_server_tool_blocks,
40
+ _merge_streaming_usage,
40
41
  _process_reasoning_contents,
41
42
  )
42
43
 
@@ -278,7 +279,6 @@ class AnthropicChatGenerator:
278
279
  :returns:
279
280
  The deserialized component instance.
280
281
  """
281
- deserialize_secrets_inplace(data["init_parameters"], keys=["api_key"])
282
282
  deserialize_tools_or_toolset_inplace(data["init_parameters"], key="tools")
283
283
  init_params = data.get("init_parameters", {})
284
284
  serialized_callback_handler = init_params.get("streaming_callback")
@@ -415,15 +415,14 @@ class AnthropicChatGenerator:
415
415
  model: str | None = None
416
416
  tool_call_index = -1
417
417
  in_server_tool_block = False
418
- input_tokens = None
418
+ start_usage: dict[str, Any] = {}
419
419
  component_info = ComponentInfo.from_component(self)
420
420
  for chunk in response:
421
421
  if chunk.type in ["message_start", "content_block_start", "content_block_delta", "message_delta"]:
422
- # Extract model from message_start chunks
422
+ # Extract model and full usage from message_start chunks
423
423
  if chunk.type == "message_start":
424
424
  model = chunk.message.model
425
- if chunk.message.usage.input_tokens is not None:
426
- input_tokens = chunk.message.usage.input_tokens
425
+ start_usage = chunk.message.usage.model_dump()
427
426
 
428
427
  if chunk.type == "content_block_start":
429
428
  if chunk.content_block.type == "tool_use":
@@ -445,10 +444,9 @@ class AnthropicChatGenerator:
445
444
  {"received_at": datetime.now(timezone.utc).isoformat(), "model": model},
446
445
  )
447
446
 
448
- if input_tokens is not None:
449
- if "usage" not in completion.meta:
450
- completion.meta["usage"] = {}
451
- completion.meta["usage"]["input_tokens"] = input_tokens
447
+ # `completion.meta["usage"]` is the usage of the last message_delta chunk
448
+ if usage := _merge_streaming_usage(start_usage=start_usage, delta_usage=completion.meta.get("usage")):
449
+ completion.meta["usage"] = usage
452
450
 
453
451
  # keep the raw content so server-tool blocks can be replayed on later turns
454
452
  if self.anthropic_server_tools:
@@ -495,7 +493,7 @@ class AnthropicChatGenerator:
495
493
  model: str | None = None
496
494
  tool_call_index = -1
497
495
  in_server_tool_block = False
498
- input_tokens = None
496
+ start_usage: dict[str, Any] = {}
499
497
  component_info = ComponentInfo.from_component(self)
500
498
  async for chunk in response:
501
499
  if chunk.type in [
@@ -504,11 +502,10 @@ class AnthropicChatGenerator:
504
502
  "content_block_delta",
505
503
  "message_delta",
506
504
  ]:
507
- # Extract model from message_start chunks
505
+ # Extract model and full usage from message_start chunks
508
506
  if chunk.type == "message_start":
509
507
  model = chunk.message.model
510
- if chunk.message.usage.input_tokens is not None:
511
- input_tokens = chunk.message.usage.input_tokens
508
+ start_usage = chunk.message.usage.model_dump()
512
509
 
513
510
  if chunk.type == "content_block_start":
514
511
  if chunk.content_block.type == "tool_use":
@@ -533,10 +530,9 @@ class AnthropicChatGenerator:
533
530
  {"received_at": datetime.now(timezone.utc).isoformat(), "model": model},
534
531
  )
535
532
 
536
- if input_tokens is not None:
537
- if "usage" not in completion.meta:
538
- completion.meta["usage"] = {}
539
- completion.meta["usage"]["input_tokens"] = input_tokens
533
+ # `completion.meta["usage"]` is the usage of the last message_delta chunk
534
+ if usage := _merge_streaming_usage(start_usage=start_usage, delta_usage=completion.meta.get("usage")):
535
+ completion.meta["usage"] = usage
540
536
 
541
537
  # keep the raw content so server-tool blocks can be replayed on later turns
542
538
  if self.anthropic_server_tools:
@@ -18,7 +18,6 @@ from haystack.tools import (
18
18
  from haystack.utils import (
19
19
  Secret,
20
20
  deserialize_callable,
21
- deserialize_secrets_inplace,
22
21
  serialize_callable,
23
22
  )
24
23
 
@@ -68,8 +67,9 @@ class AnthropicFoundryChatGenerator(AnthropicChatGenerator):
68
67
  >> focuses on enabling computers to understand, interpret, and generate human language. It involves developing
69
68
  >> techniques and algorithms to analyze and process text or speech data, allowing machines to comprehend and
70
69
  >> communicate in natural languages like English, Spanish, or Chinese.")],
71
- >> _name=None, _meta={'model': 'claude-sonnet-4-5', 'index': 0, 'finish_reason': 'end_turn',
72
- >> 'usage': {'input_tokens': 15, 'output_tokens': 64}})]}
70
+ >> _name=None, _meta={'model': 'claude-sonnet-4-5', 'index': 0, 'finish_reason': 'stop',
71
+ >> 'usage': {'prompt_tokens': 15, 'completion_tokens': 64, 'cache_read_input_tokens': 0,
72
+ >> 'cache_creation_input_tokens': 0, ...}})]}
73
73
  ```
74
74
 
75
75
  For more details on supported models and their capabilities, refer to the Anthropic
@@ -258,7 +258,6 @@ class AnthropicFoundryChatGenerator(AnthropicChatGenerator):
258
258
  The deserialized component instance.
259
259
  """
260
260
  deserialize_tools_or_toolset_inplace(data["init_parameters"], key="tools")
261
- deserialize_secrets_inplace(data["init_parameters"], keys=["api_key"])
262
261
 
263
262
  init_params = data.get("init_parameters", {})
264
263
  if serialized_callback := init_params.get("streaming_callback"):
@@ -387,6 +387,22 @@ def _get_openai_compatible_usage(response_dict: dict) -> dict:
387
387
  return usage
388
388
 
389
389
 
390
+ def _merge_streaming_usage(start_usage: dict[str, Any], delta_usage: dict[str, Any] | None) -> dict[str, Any]:
391
+ """
392
+ Combines the usage of a streamed response into the format a non-streamed response reports.
393
+
394
+ The `message_start` event carries the full `Usage`, including the `cache_creation` split by TTL and the
395
+ `service_tier`. The `message_delta` event carries the final cumulative counts, but only a subset of the fields.
396
+
397
+ :param start_usage: The usage of the `message_start` event.
398
+ :param delta_usage: The usage of the last `message_delta` event, or `None` if the stream had none.
399
+ :returns: The `message_start` usage with every non-null `message_delta` count written over it, converted with
400
+ `_get_openai_compatible_usage`.
401
+ """
402
+ usage = {**start_usage, **{key: value for key, value in (delta_usage or {}).items() if value is not None}}
403
+ return _get_openai_compatible_usage({"usage": usage})
404
+
405
+
390
406
  def _convert_chat_completion_to_chat_message(
391
407
  anthropic_response: Any, ignore_tools_thinking_messages: bool
392
408
  ) -> ChatMessage:
@@ -57,8 +57,9 @@ class AnthropicVertexChatGenerator(AnthropicChatGenerator):
57
57
  >> focuses on enabling computers to understand, interpret, and generate human language. It involves developing
58
58
  >> techniques and algorithms to analyze and process text or speech data, allowing machines to comprehend and
59
59
  >> communicate in natural languages like English, Spanish, or Chinese.")],
60
- >> _name=None, _meta={'model': 'claude-sonnet-4-5@20250929', 'index': 0, 'finish_reason': 'end_turn',
61
- >> 'usage': {'input_tokens': 15, 'output_tokens': 64}})]}
60
+ >> _name=None, _meta={'model': 'claude-sonnet-4-5@20250929', 'index': 0, 'finish_reason': 'stop',
61
+ >> 'usage': {'prompt_tokens': 15, 'completion_tokens': 64, 'cache_read_input_tokens': 0,
62
+ >> 'cache_creation_input_tokens': 0, ...}})]}
62
63
  ```
63
64
 
64
65
  For more details on supported models and their capabilities, refer to the Anthropic
@@ -7,7 +7,7 @@ from typing import Any
7
7
  from haystack import default_from_dict, default_to_dict
8
8
  from haystack.dataclasses import ChatMessage
9
9
  from haystack.tools import ToolsType, flatten_tools_or_toolsets
10
- from haystack.utils.auth import Secret, deserialize_secrets_inplace
10
+ from haystack.utils.auth import Secret
11
11
 
12
12
  from anthropic import Anthropic
13
13
  from anthropic.types import ToolParam
@@ -132,5 +132,4 @@ class AnthropicTokenCounter:
132
132
  :param data: The dictionary to deserialize from.
133
133
  :returns: The deserialized token counter.
134
134
  """
135
- deserialize_secrets_inplace(data["init_parameters"], keys=["api_key"])
136
135
  return default_from_dict(cls, data)
@@ -901,12 +901,14 @@ class TestIntegration:
901
901
  assert "Paris" in message.text
902
902
  assert "claude-sonnet-4-5" in message.meta["model"]
903
903
  assert message.meta["finish_reason"] == "stop"
904
+ assert "prompt_tokens" in message.meta["usage"]
905
+ assert "completion_tokens" in message.meta["usage"]
906
+ # only the message_start event carries `service_tier` when streaming
907
+ assert message.meta["usage"]["service_tier"] is not None
904
908
 
905
909
  if streaming:
906
910
  assert callback.counter > 1
907
911
  assert "Paris" in callback.responses
908
- assert "input_tokens" in message.meta["usage"]
909
- assert "output_tokens" in message.meta["usage"]
910
912
 
911
913
  def test_live_run_wrong_model(self, chat_messages):
912
914
  component = AnthropicChatGenerator(model="something-obviously-wrong")
@@ -938,11 +940,8 @@ class TestIntegration:
938
940
  assert tool_call.tool_name == "weather"
939
941
  assert tool_call.arguments == {"city": "Paris"}
940
942
  assert message.meta["finish_reason"] == "tool_calls"
941
- if streaming:
942
- assert "output_tokens" in message.meta["usage"]
943
- assert "input_tokens" in message.meta["usage"]
944
- else:
945
- assert "completion_tokens" in message.meta["usage"]
943
+ assert "prompt_tokens" in message.meta["usage"]
944
+ assert "completion_tokens" in message.meta["usage"]
946
945
 
947
946
  new_messages = [
948
947
  *initial_messages,
@@ -1195,8 +1194,8 @@ class TestIntegration:
1195
1194
  assert tool_call.tool_name == "weather"
1196
1195
  assert tool_call.arguments == {"city": "Paris"}
1197
1196
  assert message.meta["finish_reason"] == "tool_calls"
1198
- assert "output_tokens" in message.meta["usage"]
1199
- assert "input_tokens" in message.meta["usage"]
1197
+ assert "completion_tokens" in message.meta["usage"]
1198
+ assert "prompt_tokens" in message.meta["usage"]
1200
1199
 
1201
1200
  new_messages = [
1202
1201
  *initial_messages,
@@ -1210,16 +1209,20 @@ class TestIntegration:
1210
1209
  assert len(final_message.text) > 0
1211
1210
  assert "paris" in final_message.text.lower()
1212
1211
 
1212
+ @pytest.mark.parametrize("streaming", [False, True])
1213
1213
  @pytest.mark.parametrize("cache_enabled", [True, False])
1214
1214
  @pytest.mark.parametrize("cache_location", ["system", "user"])
1215
- def test_prompt_caching_live_run(self, cache_enabled, cache_location):
1215
+ def test_prompt_caching_live_run(self, cache_enabled, cache_location, streaming):
1216
1216
  """
1217
- Prompt caching works whether the cache_control marker sits on the system or the user message.
1217
+ Prompt caching works whether the cache_control marker sits on the system or the user message, with and
1218
+ without streaming.
1218
1219
  """
1219
1220
  generation_kwargs = {"extra_headers": {"anthropic-beta": "prompt-caching-2024-07-31"}} if cache_enabled else {}
1220
1221
 
1221
1222
  claude_llm = AnthropicChatGenerator(
1222
- api_key=Secret.from_env_var("ANTHROPIC_API_KEY"), generation_kwargs=generation_kwargs
1223
+ api_key=Secret.from_env_var("ANTHROPIC_API_KEY"),
1224
+ generation_kwargs=generation_kwargs,
1225
+ streaming_callback=print_streaming_chunk if streaming else None,
1223
1226
  )
1224
1227
 
1225
1228
  # see https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching#cache-limitations
@@ -1247,6 +1250,8 @@ class TestIntegration:
1247
1250
  token_usage.get("cache_creation_input_tokens", 0) > 1024
1248
1251
  or token_usage.get("cache_read_input_tokens", 0) > 1024
1249
1252
  ), f"Unexpected usage stats: {token_usage}"
1253
+ # the split of cache writes by TTL, which only the message_start event carries when streaming
1254
+ assert token_usage["cache_creation"] is not None
1250
1255
  else:
1251
1256
  assert token_usage.get("cache_creation_input_tokens", 0) == 0
1252
1257
  assert token_usage.get("cache_read_input_tokens", 0) == 0
@@ -132,8 +132,10 @@ class TestIntegration:
132
132
  assert "paris" in message.text.lower()
133
133
  assert "claude-sonnet-4-5" in message.meta["model"]
134
134
  assert message.meta["finish_reason"] == "stop"
135
- assert "input_tokens" in message.meta["usage"]
136
- assert "output_tokens" in message.meta["usage"]
135
+ assert "prompt_tokens" in message.meta["usage"]
136
+ assert "completion_tokens" in message.meta["usage"]
137
+ # only the message_start event carries `service_tier` when streaming
138
+ assert message.meta["usage"]["service_tier"] is not None
137
139
 
138
140
  # Verify streaming behavior
139
141
  assert counter > 1 # Should have received multiple chunks
@@ -4,6 +4,7 @@ from dataclasses import replace
4
4
 
5
5
  import pytest
6
6
  from anthropic.types import (
7
+ CacheCreation,
7
8
  InputJSONDelta,
8
9
  Message,
9
10
  MessageDeltaUsage,
@@ -52,6 +53,7 @@ from haystack_integrations.components.generators.anthropic.chat.utils import (
52
53
  _finalize_reasoning_group,
53
54
  _get_openai_compatible_usage,
54
55
  _has_server_tool_blocks,
56
+ _merge_streaming_usage,
55
57
  )
56
58
 
57
59
  CITATION = {
@@ -627,11 +629,14 @@ class TestConvertAnthropicChunkToStreamingChunk:
627
629
  message = generator._process_response(raw_chunks)
628
630
  usage = message["replies"][0].meta["usage"]
629
631
  assert isinstance(usage, dict)
630
- assert usage["cache_creation_input_tokens"] is None
631
- assert usage["cache_read_input_tokens"] is None
632
- assert usage["input_tokens"] == 393
633
- assert usage["output_tokens"] == 77
632
+ assert usage["cache_creation_input_tokens"] == 0
633
+ assert usage["cache_read_input_tokens"] == 0
634
+ assert usage["prompt_tokens"] == 393
635
+ assert usage["completion_tokens"] == 77
634
636
  assert usage["server_tool_use"] is None
637
+ assert usage["service_tier"] == "standard"
638
+ assert "input_tokens" not in usage
639
+ assert "output_tokens" not in usage
635
640
 
636
641
  def test_accumulate_raw_content_blocks_from_stream(self):
637
642
  component_info = ComponentInfo(name="test", type="test")
@@ -1568,3 +1573,41 @@ class TestOpenAICompatibleUsage:
1568
1573
  @pytest.mark.parametrize("response_dict", [{}, {"usage": {}}], ids=["no_usage_key", "empty_usage"])
1569
1574
  def test_empty_usage_is_unchanged(self, response_dict):
1570
1575
  assert _get_openai_compatible_usage(response_dict) == {}
1576
+
1577
+
1578
+ class TestMergeStreamingUsage:
1579
+ def test_cache_creation_split_is_kept(self):
1580
+ """`message_delta` usage has no `cache_creation`, so the split must come from `message_start`."""
1581
+ start_usage = Usage(
1582
+ input_tokens=10,
1583
+ output_tokens=1,
1584
+ cache_read_input_tokens=2000,
1585
+ cache_creation_input_tokens=1500,
1586
+ cache_creation=CacheCreation(ephemeral_5m_input_tokens=500, ephemeral_1h_input_tokens=1000),
1587
+ service_tier="standard",
1588
+ ).model_dump()
1589
+ delta_usage = MessageDeltaUsage(
1590
+ input_tokens=10, output_tokens=77, cache_read_input_tokens=2000, cache_creation_input_tokens=1500
1591
+ ).model_dump()
1592
+
1593
+ usage = _merge_streaming_usage(start_usage=start_usage, delta_usage=delta_usage)
1594
+
1595
+ assert usage["prompt_tokens"] == 3510
1596
+ assert usage["completion_tokens"] == 77
1597
+ assert usage["cache_read_input_tokens"] == 2000
1598
+ assert usage["cache_creation_input_tokens"] == 1500
1599
+ assert usage["cache_creation"] == {"ephemeral_5m_input_tokens": 500, "ephemeral_1h_input_tokens": 1000}
1600
+ assert usage["service_tier"] == "standard"
1601
+ assert "input_tokens" not in usage
1602
+ assert "output_tokens" not in usage
1603
+
1604
+ def test_null_delta_counts_keep_start_counts(self):
1605
+ start_usage = {"input_tokens": 393, "output_tokens": 3, "cache_read_input_tokens": 0}
1606
+ delta_usage = {"input_tokens": None, "output_tokens": 77, "cache_read_input_tokens": None}
1607
+
1608
+ usage = _merge_streaming_usage(start_usage=start_usage, delta_usage=delta_usage)
1609
+
1610
+ assert usage == {"prompt_tokens": 393, "completion_tokens": 77, "cache_read_input_tokens": 0}
1611
+
1612
+ def test_no_usage_returns_empty_dict(self):
1613
+ assert _merge_streaming_usage(start_usage={}, delta_usage=None) == {}