anthropic-haystack 6.2.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.
- {anthropic_haystack-6.2.0 → anthropic_haystack-7.0.0}/CHANGELOG.md +14 -0
- {anthropic_haystack-6.2.0 → anthropic_haystack-7.0.0}/PKG-INFO +1 -1
- {anthropic_haystack-6.2.0 → anthropic_haystack-7.0.0}/src/haystack_integrations/components/generators/anthropic/chat/chat_generator.py +22 -29
- {anthropic_haystack-6.2.0 → anthropic_haystack-7.0.0}/src/haystack_integrations/components/generators/anthropic/chat/foundry_chat_generator.py +3 -4
- {anthropic_haystack-6.2.0 → anthropic_haystack-7.0.0}/src/haystack_integrations/components/generators/anthropic/chat/utils.py +16 -0
- {anthropic_haystack-6.2.0 → anthropic_haystack-7.0.0}/src/haystack_integrations/components/generators/anthropic/chat/vertex_chat_generator.py +3 -2
- {anthropic_haystack-6.2.0 → anthropic_haystack-7.0.0}/src/haystack_integrations/token_counters/anthropic/token_counter.py +1 -2
- {anthropic_haystack-6.2.0 → anthropic_haystack-7.0.0}/tests/test_chat_generator.py +27 -18
- {anthropic_haystack-6.2.0 → anthropic_haystack-7.0.0}/tests/test_chat_generator_async.py +4 -2
- {anthropic_haystack-6.2.0 → anthropic_haystack-7.0.0}/tests/test_utils.py +47 -4
- {anthropic_haystack-6.2.0 → anthropic_haystack-7.0.0}/.gitignore +0 -0
- {anthropic_haystack-6.2.0 → anthropic_haystack-7.0.0}/LICENSE.txt +0 -0
- {anthropic_haystack-6.2.0 → anthropic_haystack-7.0.0}/README.md +0 -0
- {anthropic_haystack-6.2.0 → anthropic_haystack-7.0.0}/example/documentation_rag_with_claude.py +0 -0
- {anthropic_haystack-6.2.0 → anthropic_haystack-7.0.0}/example/prompt_caching.py +0 -0
- {anthropic_haystack-6.2.0 → anthropic_haystack-7.0.0}/pydoc/config_docusaurus.yml +0 -0
- {anthropic_haystack-6.2.0 → anthropic_haystack-7.0.0}/pyproject.toml +0 -0
- {anthropic_haystack-6.2.0 → anthropic_haystack-7.0.0}/src/haystack_integrations/components/generators/anthropic/__init__.py +0 -0
- {anthropic_haystack-6.2.0 → anthropic_haystack-7.0.0}/src/haystack_integrations/components/generators/anthropic/chat/__init__.py +0 -0
- {anthropic_haystack-6.2.0 → anthropic_haystack-7.0.0}/src/haystack_integrations/components/generators/py.typed +0 -0
- {anthropic_haystack-6.2.0 → anthropic_haystack-7.0.0}/src/haystack_integrations/token_counters/anthropic/__init__.py +0 -0
- {anthropic_haystack-6.2.0 → anthropic_haystack-7.0.0}/src/haystack_integrations/token_counters/py.typed +0 -0
- {anthropic_haystack-6.2.0 → anthropic_haystack-7.0.0}/tests/__init__.py +0 -0
- {anthropic_haystack-6.2.0 → anthropic_haystack-7.0.0}/tests/conftest.py +0 -0
- {anthropic_haystack-6.2.0 → anthropic_haystack-7.0.0}/tests/test_files/apple.jpg +0 -0
- {anthropic_haystack-6.2.0 → anthropic_haystack-7.0.0}/tests/test_files/sample_pdf_3.pdf +0 -0
- {anthropic_haystack-6.2.0 → anthropic_haystack-7.0.0}/tests/test_foundry_chat_generator.py +0 -0
- {anthropic_haystack-6.2.0 → anthropic_haystack-7.0.0}/tests/test_token_counter.py +0 -0
- {anthropic_haystack-6.2.0 → anthropic_haystack-7.0.0}/tests/test_vertex_chat_generator.py +0 -0
|
@@ -1,5 +1,19 @@
|
|
|
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
|
+
|
|
10
|
+
## [integrations/anthropic-v6.2.0] - 2026-09-24
|
|
11
|
+
|
|
12
|
+
### 🚀 Features
|
|
13
|
+
|
|
14
|
+
- Add thinking_display flattened generation_kwarg to Anthropic and Bedrock chat generators (#3993)
|
|
15
|
+
|
|
16
|
+
|
|
3
17
|
## [integrations/anthropic-v6.1.0] - 2026-09-22
|
|
4
18
|
|
|
5
19
|
### 🐛 Bug Fixes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: anthropic-haystack
|
|
3
|
-
Version:
|
|
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
|
|
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")
|
|
@@ -369,11 +369,6 @@ class AnthropicChatGenerator:
|
|
|
369
369
|
tool_choice["disable_parallel_tool_use"] = disable_parallel_tool_use
|
|
370
370
|
tool_choice.setdefault("type", "auto") # default value
|
|
371
371
|
|
|
372
|
-
tool_choice_type = generation_kwargs.pop("tool_choice_type", None)
|
|
373
|
-
if tool_choice_type is not None:
|
|
374
|
-
tool_choice = generation_kwargs.setdefault("tool_choice", {})
|
|
375
|
-
tool_choice["type"] = tool_choice_type
|
|
376
|
-
|
|
377
372
|
thinking_budget_tokens = generation_kwargs.pop("thinking_budget_tokens", None)
|
|
378
373
|
if thinking_budget_tokens is not None:
|
|
379
374
|
thinking = generation_kwargs.setdefault("thinking", {})
|
|
@@ -390,12 +385,14 @@ class AnthropicChatGenerator:
|
|
|
390
385
|
output_config = generation_kwargs.setdefault("output_config", {})
|
|
391
386
|
output_config["effort"] = adaptive_thinking_effort
|
|
392
387
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
388
|
+
for nested_name in ("tool_choice", "thinking", "output_config"):
|
|
389
|
+
prefix = f"{nested_name}_"
|
|
390
|
+
for key in [k for k in generation_kwargs if k.startswith(prefix)]:
|
|
391
|
+
value = generation_kwargs.pop(key)
|
|
392
|
+
if value is not None:
|
|
393
|
+
nested_key = key[len(prefix) :]
|
|
394
|
+
nested_dict = generation_kwargs.setdefault(nested_name, {})
|
|
395
|
+
nested_dict[nested_key] = value
|
|
399
396
|
|
|
400
397
|
return generation_kwargs
|
|
401
398
|
|
|
@@ -418,15 +415,14 @@ class AnthropicChatGenerator:
|
|
|
418
415
|
model: str | None = None
|
|
419
416
|
tool_call_index = -1
|
|
420
417
|
in_server_tool_block = False
|
|
421
|
-
|
|
418
|
+
start_usage: dict[str, Any] = {}
|
|
422
419
|
component_info = ComponentInfo.from_component(self)
|
|
423
420
|
for chunk in response:
|
|
424
421
|
if chunk.type in ["message_start", "content_block_start", "content_block_delta", "message_delta"]:
|
|
425
|
-
# Extract model from message_start chunks
|
|
422
|
+
# Extract model and full usage from message_start chunks
|
|
426
423
|
if chunk.type == "message_start":
|
|
427
424
|
model = chunk.message.model
|
|
428
|
-
|
|
429
|
-
input_tokens = chunk.message.usage.input_tokens
|
|
425
|
+
start_usage = chunk.message.usage.model_dump()
|
|
430
426
|
|
|
431
427
|
if chunk.type == "content_block_start":
|
|
432
428
|
if chunk.content_block.type == "tool_use":
|
|
@@ -448,10 +444,9 @@ class AnthropicChatGenerator:
|
|
|
448
444
|
{"received_at": datetime.now(timezone.utc).isoformat(), "model": model},
|
|
449
445
|
)
|
|
450
446
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
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
|
|
455
450
|
|
|
456
451
|
# keep the raw content so server-tool blocks can be replayed on later turns
|
|
457
452
|
if self.anthropic_server_tools:
|
|
@@ -498,7 +493,7 @@ class AnthropicChatGenerator:
|
|
|
498
493
|
model: str | None = None
|
|
499
494
|
tool_call_index = -1
|
|
500
495
|
in_server_tool_block = False
|
|
501
|
-
|
|
496
|
+
start_usage: dict[str, Any] = {}
|
|
502
497
|
component_info = ComponentInfo.from_component(self)
|
|
503
498
|
async for chunk in response:
|
|
504
499
|
if chunk.type in [
|
|
@@ -507,11 +502,10 @@ class AnthropicChatGenerator:
|
|
|
507
502
|
"content_block_delta",
|
|
508
503
|
"message_delta",
|
|
509
504
|
]:
|
|
510
|
-
# Extract model from message_start chunks
|
|
505
|
+
# Extract model and full usage from message_start chunks
|
|
511
506
|
if chunk.type == "message_start":
|
|
512
507
|
model = chunk.message.model
|
|
513
|
-
|
|
514
|
-
input_tokens = chunk.message.usage.input_tokens
|
|
508
|
+
start_usage = chunk.message.usage.model_dump()
|
|
515
509
|
|
|
516
510
|
if chunk.type == "content_block_start":
|
|
517
511
|
if chunk.content_block.type == "tool_use":
|
|
@@ -536,10 +530,9 @@ class AnthropicChatGenerator:
|
|
|
536
530
|
{"received_at": datetime.now(timezone.utc).isoformat(), "model": model},
|
|
537
531
|
)
|
|
538
532
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
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
|
|
543
536
|
|
|
544
537
|
# keep the raw content so server-tool blocks can be replayed on later turns
|
|
545
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': '
|
|
72
|
-
>> 'usage': {'
|
|
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': '
|
|
61
|
-
>> 'usage': {'
|
|
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
|
|
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)
|
|
@@ -539,23 +539,27 @@ class TestRun:
|
|
|
539
539
|
},
|
|
540
540
|
),
|
|
541
541
|
(
|
|
542
|
-
#
|
|
542
|
+
# thinking type in_between with effort
|
|
543
543
|
{
|
|
544
|
-
"
|
|
544
|
+
"thinking_type": "in_between",
|
|
545
545
|
"thinking_display": "summarized",
|
|
546
|
+
"output_config_effort": "high",
|
|
546
547
|
},
|
|
547
548
|
{
|
|
548
|
-
"thinking": {"type": "
|
|
549
|
+
"thinking": {"type": "in_between", "display": "summarized"},
|
|
550
|
+
"output_config": {"effort": "high"},
|
|
549
551
|
},
|
|
550
552
|
),
|
|
551
553
|
(
|
|
552
|
-
#
|
|
554
|
+
# thinking type adaptive with effort
|
|
553
555
|
{
|
|
554
|
-
"
|
|
556
|
+
"thinking_type": "adaptive",
|
|
555
557
|
"thinking_display": "summarized",
|
|
558
|
+
"output_config_effort": "high",
|
|
556
559
|
},
|
|
557
560
|
{
|
|
558
|
-
"thinking": {"type": "
|
|
561
|
+
"thinking": {"type": "adaptive", "display": "summarized"},
|
|
562
|
+
"output_config": {"effort": "high"},
|
|
559
563
|
},
|
|
560
564
|
),
|
|
561
565
|
],
|
|
@@ -897,12 +901,14 @@ class TestIntegration:
|
|
|
897
901
|
assert "Paris" in message.text
|
|
898
902
|
assert "claude-sonnet-4-5" in message.meta["model"]
|
|
899
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
|
|
900
908
|
|
|
901
909
|
if streaming:
|
|
902
910
|
assert callback.counter > 1
|
|
903
911
|
assert "Paris" in callback.responses
|
|
904
|
-
assert "input_tokens" in message.meta["usage"]
|
|
905
|
-
assert "output_tokens" in message.meta["usage"]
|
|
906
912
|
|
|
907
913
|
def test_live_run_wrong_model(self, chat_messages):
|
|
908
914
|
component = AnthropicChatGenerator(model="something-obviously-wrong")
|
|
@@ -934,11 +940,8 @@ class TestIntegration:
|
|
|
934
940
|
assert tool_call.tool_name == "weather"
|
|
935
941
|
assert tool_call.arguments == {"city": "Paris"}
|
|
936
942
|
assert message.meta["finish_reason"] == "tool_calls"
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
assert "input_tokens" in message.meta["usage"]
|
|
940
|
-
else:
|
|
941
|
-
assert "completion_tokens" in message.meta["usage"]
|
|
943
|
+
assert "prompt_tokens" in message.meta["usage"]
|
|
944
|
+
assert "completion_tokens" in message.meta["usage"]
|
|
942
945
|
|
|
943
946
|
new_messages = [
|
|
944
947
|
*initial_messages,
|
|
@@ -1191,8 +1194,8 @@ class TestIntegration:
|
|
|
1191
1194
|
assert tool_call.tool_name == "weather"
|
|
1192
1195
|
assert tool_call.arguments == {"city": "Paris"}
|
|
1193
1196
|
assert message.meta["finish_reason"] == "tool_calls"
|
|
1194
|
-
assert "
|
|
1195
|
-
assert "
|
|
1197
|
+
assert "completion_tokens" in message.meta["usage"]
|
|
1198
|
+
assert "prompt_tokens" in message.meta["usage"]
|
|
1196
1199
|
|
|
1197
1200
|
new_messages = [
|
|
1198
1201
|
*initial_messages,
|
|
@@ -1206,16 +1209,20 @@ class TestIntegration:
|
|
|
1206
1209
|
assert len(final_message.text) > 0
|
|
1207
1210
|
assert "paris" in final_message.text.lower()
|
|
1208
1211
|
|
|
1212
|
+
@pytest.mark.parametrize("streaming", [False, True])
|
|
1209
1213
|
@pytest.mark.parametrize("cache_enabled", [True, False])
|
|
1210
1214
|
@pytest.mark.parametrize("cache_location", ["system", "user"])
|
|
1211
|
-
def test_prompt_caching_live_run(self, cache_enabled, cache_location):
|
|
1215
|
+
def test_prompt_caching_live_run(self, cache_enabled, cache_location, streaming):
|
|
1212
1216
|
"""
|
|
1213
|
-
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.
|
|
1214
1219
|
"""
|
|
1215
1220
|
generation_kwargs = {"extra_headers": {"anthropic-beta": "prompt-caching-2024-07-31"}} if cache_enabled else {}
|
|
1216
1221
|
|
|
1217
1222
|
claude_llm = AnthropicChatGenerator(
|
|
1218
|
-
api_key=Secret.from_env_var("ANTHROPIC_API_KEY"),
|
|
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,
|
|
1219
1226
|
)
|
|
1220
1227
|
|
|
1221
1228
|
# see https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching#cache-limitations
|
|
@@ -1243,6 +1250,8 @@ class TestIntegration:
|
|
|
1243
1250
|
token_usage.get("cache_creation_input_tokens", 0) > 1024
|
|
1244
1251
|
or token_usage.get("cache_read_input_tokens", 0) > 1024
|
|
1245
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
|
|
1246
1255
|
else:
|
|
1247
1256
|
assert token_usage.get("cache_creation_input_tokens", 0) == 0
|
|
1248
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 "
|
|
136
|
-
assert "
|
|
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"]
|
|
631
|
-
assert usage["cache_read_input_tokens"]
|
|
632
|
-
assert usage["
|
|
633
|
-
assert usage["
|
|
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) == {}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{anthropic_haystack-6.2.0 → anthropic_haystack-7.0.0}/example/documentation_rag_with_claude.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|