anthropic-haystack 6.0.1__tar.gz → 6.2.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.0.1 → anthropic_haystack-6.2.0}/CHANGELOG.md +19 -0
  2. {anthropic_haystack-6.0.1 → anthropic_haystack-6.2.0}/PKG-INFO +1 -1
  3. {anthropic_haystack-6.0.1 → anthropic_haystack-6.2.0}/src/haystack_integrations/components/generators/anthropic/chat/chat_generator.py +31 -10
  4. {anthropic_haystack-6.0.1 → anthropic_haystack-6.2.0}/src/haystack_integrations/components/generators/anthropic/chat/foundry_chat_generator.py +12 -3
  5. {anthropic_haystack-6.0.1 → anthropic_haystack-6.2.0}/src/haystack_integrations/components/generators/anthropic/chat/utils.py +38 -20
  6. {anthropic_haystack-6.0.1 → anthropic_haystack-6.2.0}/src/haystack_integrations/components/generators/anthropic/chat/vertex_chat_generator.py +15 -8
  7. {anthropic_haystack-6.0.1 → anthropic_haystack-6.2.0}/tests/test_chat_generator.py +91 -0
  8. {anthropic_haystack-6.0.1 → anthropic_haystack-6.2.0}/tests/test_utils.py +129 -0
  9. {anthropic_haystack-6.0.1 → anthropic_haystack-6.2.0}/tests/test_vertex_chat_generator.py +9 -9
  10. {anthropic_haystack-6.0.1 → anthropic_haystack-6.2.0}/.gitignore +0 -0
  11. {anthropic_haystack-6.0.1 → anthropic_haystack-6.2.0}/LICENSE.txt +0 -0
  12. {anthropic_haystack-6.0.1 → anthropic_haystack-6.2.0}/README.md +0 -0
  13. {anthropic_haystack-6.0.1 → anthropic_haystack-6.2.0}/example/documentation_rag_with_claude.py +0 -0
  14. {anthropic_haystack-6.0.1 → anthropic_haystack-6.2.0}/example/prompt_caching.py +0 -0
  15. {anthropic_haystack-6.0.1 → anthropic_haystack-6.2.0}/pydoc/config_docusaurus.yml +0 -0
  16. {anthropic_haystack-6.0.1 → anthropic_haystack-6.2.0}/pyproject.toml +0 -0
  17. {anthropic_haystack-6.0.1 → anthropic_haystack-6.2.0}/src/haystack_integrations/components/generators/anthropic/__init__.py +0 -0
  18. {anthropic_haystack-6.0.1 → anthropic_haystack-6.2.0}/src/haystack_integrations/components/generators/anthropic/chat/__init__.py +0 -0
  19. {anthropic_haystack-6.0.1 → anthropic_haystack-6.2.0}/src/haystack_integrations/components/generators/py.typed +0 -0
  20. {anthropic_haystack-6.0.1 → anthropic_haystack-6.2.0}/src/haystack_integrations/token_counters/anthropic/__init__.py +0 -0
  21. {anthropic_haystack-6.0.1 → anthropic_haystack-6.2.0}/src/haystack_integrations/token_counters/anthropic/token_counter.py +0 -0
  22. {anthropic_haystack-6.0.1 → anthropic_haystack-6.2.0}/src/haystack_integrations/token_counters/py.typed +0 -0
  23. {anthropic_haystack-6.0.1 → anthropic_haystack-6.2.0}/tests/__init__.py +0 -0
  24. {anthropic_haystack-6.0.1 → anthropic_haystack-6.2.0}/tests/conftest.py +0 -0
  25. {anthropic_haystack-6.0.1 → anthropic_haystack-6.2.0}/tests/test_chat_generator_async.py +0 -0
  26. {anthropic_haystack-6.0.1 → anthropic_haystack-6.2.0}/tests/test_files/apple.jpg +0 -0
  27. {anthropic_haystack-6.0.1 → anthropic_haystack-6.2.0}/tests/test_files/sample_pdf_3.pdf +0 -0
  28. {anthropic_haystack-6.0.1 → anthropic_haystack-6.2.0}/tests/test_foundry_chat_generator.py +0 -0
  29. {anthropic_haystack-6.0.1 → anthropic_haystack-6.2.0}/tests/test_token_counter.py +0 -0
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## [integrations/anthropic-v6.1.0] - 2026-09-22
4
+
5
+ ### 🐛 Bug Fixes
6
+
7
+ - Anthropic - respect an empty runtime tools list (#3977)
8
+ - Drop Anthropic tool calls truncated at max_tokens and raise the default limit (#3982)
9
+
10
+ ### 🧹 Chores
11
+
12
+ - Refresh Anthropic model lists and expose service_tierupdates (#3983)
13
+
14
+
15
+ ## [integrations/anthropic-v6.0.1] - 2026-09-14
16
+
17
+ ### 🐛 Bug Fixes
18
+
19
+ - Anthropic - allow empty assistant messages (#3952)
20
+
21
+
3
22
  ## [integrations/anthropic-v6.0.0] - 2026-09-03
4
23
 
5
24
  ### 🚀 Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: anthropic-haystack
3
- Version: 6.0.1
3
+ Version: 6.2.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
@@ -42,6 +42,8 @@ from .utils import (
42
42
 
43
43
  logger = logging.getLogger(__name__)
44
44
 
45
+ _DEFAULT_MAX_TOKENS = 8192
46
+
45
47
 
46
48
  @component
47
49
  class AnthropicChatGenerator:
@@ -104,6 +106,7 @@ class AnthropicChatGenerator:
104
106
  "tool_choice",
105
107
  "max_tokens",
106
108
  "metadata",
109
+ "service_tier",
107
110
  "stop_sequences",
108
111
  "temperature",
109
112
  "top_p",
@@ -114,15 +117,17 @@ class AnthropicChatGenerator:
114
117
  ]
115
118
 
116
119
  SUPPORTED_MODELS: ClassVar[list[str]] = [
120
+ "claude-fable-5-1",
121
+ "claude-fable-5",
122
+ "claude-opus-5",
123
+ "claude-opus-4-8",
124
+ "claude-opus-4-7",
117
125
  "claude-opus-4-6",
126
+ "claude-opus-4-5-20251101",
127
+ "claude-sonnet-5",
118
128
  "claude-sonnet-4-6",
119
- "claude-haiku-4-5-20251001",
120
129
  "claude-sonnet-4-5-20250929",
121
- "claude-opus-4-5-20251101",
122
- "claude-opus-4-1-20250805",
123
- "claude-sonnet-4-20250514",
124
- "claude-opus-4-20250514",
125
- "claude-3-haiku-20240307",
130
+ "claude-haiku-4-5-20251001",
126
131
  ]
127
132
  """A non-exhaustive list of chat models supported by this component. See
128
133
  https://platform.claude.com/docs/en/about-claude/models/overview for the full list."""
@@ -153,8 +158,12 @@ class AnthropicChatGenerator:
153
158
 
154
159
  Supported generation_kwargs parameters are:
155
160
  - `system`: The system message to be passed to the model.
156
- - `max_tokens`: The maximum number of tokens to generate.
161
+ - `max_tokens`: The maximum number of tokens to generate. Defaults to 8192. A response that hits
162
+ this limit is cut off; if the model was writing a tool call at the time, that call is dropped
163
+ and the reply carries a `length` finish reason.
157
164
  - `metadata`: A dictionary of metadata to be passed to the model.
165
+ - `service_tier`: Whether the request may use priority capacity (`auto`) or standard capacity only
166
+ (`standard_only`). See [service tiers](https://platform.claude.com/docs/en/api/service-tiers).
158
167
  - `stop_sequences`: A list of strings that the model should stop generating at.
159
168
  - `temperature`: The temperature to use for sampling.
160
169
  - `top_p`: The top_p value to use for nucleus sampling.
@@ -316,7 +325,7 @@ class AnthropicChatGenerator:
316
325
  # prompt caching
317
326
 
318
327
  # tools management
319
- tools = tools or self.tools
328
+ tools = tools if tools is not None else self.tools
320
329
  flattened_tools = flatten_tools_or_toolsets(tools)
321
330
  _check_duplicate_tool_names(flattened_tools)
322
331
 
@@ -340,6 +349,11 @@ class AnthropicChatGenerator:
340
349
 
341
350
  def _resolve_flattened_generation_kwargs(self, generation_kwargs: dict[str, Any]) -> dict[str, Any]:
342
351
  generation_kwargs = generation_kwargs.copy()
352
+ # Copy the nested dicts the flattened kwargs write into; a shallow copy would share them with the
353
+ # component's init-time generation_kwargs, so a per-run flattened kwarg would persist across runs.
354
+ for key in ("tool_choice", "thinking", "output_config"):
355
+ if isinstance(generation_kwargs.get(key), dict):
356
+ generation_kwargs[key] = dict(generation_kwargs[key])
343
357
 
344
358
  disable_parallel_tool_use = generation_kwargs.pop("disable_parallel_tool_use", None)
345
359
  parallel_tool_use = generation_kwargs.pop("parallel_tool_use", None)
@@ -376,6 +390,13 @@ class AnthropicChatGenerator:
376
390
  output_config = generation_kwargs.setdefault("output_config", {})
377
391
  output_config["effort"] = adaptive_thinking_effort
378
392
 
393
+ thinking_display = generation_kwargs.pop("thinking_display", None)
394
+ if thinking_display is not None:
395
+ thinking = generation_kwargs.setdefault("thinking", {})
396
+ # `display` is only accepted with enabled or adaptive thinking, so it's dropped when thinking is disabled
397
+ if thinking.get("type") != "disabled":
398
+ thinking["display"] = thinking_display
399
+
379
400
  return generation_kwargs
380
401
 
381
402
  def _process_response(
@@ -587,7 +608,7 @@ class AnthropicChatGenerator:
587
608
  system=system_messages,
588
609
  tools=anthropic_tools,
589
610
  stream=streaming_callback is not None,
590
- max_tokens=generation_kwargs.pop("max_tokens", 1024),
611
+ max_tokens=generation_kwargs.pop("max_tokens", _DEFAULT_MAX_TOKENS),
591
612
  **generation_kwargs,
592
613
  )
593
614
 
@@ -636,7 +657,7 @@ class AnthropicChatGenerator:
636
657
  system=system_messages,
637
658
  tools=anthropic_tools,
638
659
  stream=streaming_callback is not None,
639
- max_tokens=generation_kwargs.pop("max_tokens", 1024),
660
+ max_tokens=generation_kwargs.pop("max_tokens", _DEFAULT_MAX_TOKENS),
640
661
  **generation_kwargs,
641
662
  )
642
663
 
@@ -77,11 +77,16 @@ class AnthropicFoundryChatGenerator(AnthropicChatGenerator):
77
77
  """
78
78
 
79
79
  SUPPORTED_MODELS: ClassVar[list[str]] = [
80
+ "claude-fable-5-1",
81
+ "claude-fable-5",
82
+ "claude-opus-5",
83
+ "claude-opus-4-8",
84
+ "claude-opus-4-7",
80
85
  "claude-opus-4-6",
86
+ "claude-opus-4-5",
87
+ "claude-sonnet-5",
81
88
  "claude-sonnet-4-6",
82
89
  "claude-sonnet-4-5",
83
- "claude-opus-4-5",
84
- "claude-opus-4-1",
85
90
  "claude-haiku-4-5",
86
91
  ]
87
92
  """A non-exhaustive list of chat models supported by this component.
@@ -122,8 +127,12 @@ class AnthropicFoundryChatGenerator(AnthropicChatGenerator):
122
127
  for more details.
123
128
  Supported generation_kwargs parameters are:
124
129
  - `system`: The system message to be passed to the model.
125
- - `max_tokens`: The maximum number of tokens to generate.
130
+ - `max_tokens`: The maximum number of tokens to generate. Defaults to 8192. A response that hits
131
+ this limit is cut off; if the model was writing a tool call at the time, that call is dropped
132
+ and the reply carries a `length` finish reason.
126
133
  - `metadata`: A dictionary of metadata to be passed to the model.
134
+ - `service_tier`: Whether the request may use priority capacity (`auto`) or standard capacity only
135
+ (`standard_only`). See [service tiers](https://platform.claude.com/docs/en/api/service-tiers).
127
136
  - `stop_sequences`: A list of strings that the model should stop generating at.
128
137
  - `temperature`: The temperature to use for sampling.
129
138
  - `top_p`: The top_p value to use for nucleus sampling.
@@ -2,6 +2,7 @@ import json
2
2
  from copy import deepcopy
3
3
  from typing import Any, Literal, TypeAlias, cast, get_args
4
4
 
5
+ from haystack import logging
5
6
  from haystack.dataclasses.chat_message import (
6
7
  ChatMessage,
7
8
  ChatRole,
@@ -32,6 +33,8 @@ from anthropic.types import (
32
33
  ToolUseBlockParam,
33
34
  )
34
35
 
36
+ logger = logging.getLogger(__name__)
37
+
35
38
  # See https://docs.anthropic.com/en/api/messages for supported formats
36
39
  ImageFormat = Literal["image/jpeg", "image/png", "image/gif", "image/webp"]
37
40
  IMAGE_SUPPORTED_FORMATS: list[ImageFormat] = list(get_args(ImageFormat))
@@ -392,9 +395,29 @@ def _convert_chat_completion_to_chat_message(
392
395
  """
393
396
  tool_calls = []
394
397
  reasoning_contents = []
395
- reasoning_text = ""
396
398
 
397
- for block in anthropic_response.content:
399
+ # A `max_tokens` stop reason cuts the response off mid-block. If that block was a tool call, its arguments are
400
+ # incomplete: Anthropic still returns a `tool_use` block, but only the keys it managed to finish. Running the
401
+ # tool with those arguments would act on the wrong input, so the call is dropped here. Instead, the reply carries
402
+ # the `length` finish reason which mirrors the streaming path, where the truncated JSON fails to parse and the tool
403
+ # call is dropped.
404
+ blocks = anthropic_response.content
405
+ truncated_tool_call = (
406
+ anthropic_response.stop_reason == "max_tokens" and bool(blocks) and blocks[-1].type == "tool_use"
407
+ )
408
+ if truncated_tool_call:
409
+ truncated_block = blocks[-1]
410
+ logger.warning(
411
+ "The model hit the `max_tokens` limit while writing the arguments of tool call `{tool_name}`, so they "
412
+ "are incomplete and the tool call will be skipped. Raise `max_tokens` in `generation_kwargs` to give "
413
+ "the model room to finish. Tool call ID: {tool_call_id}, partial arguments: {arguments}",
414
+ tool_name=truncated_block.name,
415
+ tool_call_id=truncated_block.id,
416
+ arguments=truncated_block.input,
417
+ )
418
+ blocks = blocks[:-1]
419
+
420
+ for block in blocks:
398
421
  reasoning_content: dict[str, Any] = {}
399
422
  if block.type == "tool_use":
400
423
  tool_calls.append(ToolCall(tool_name=block.name, arguments=block.input, id=block.id))
@@ -420,30 +443,25 @@ def _convert_chat_completion_to_chat_message(
420
443
  text = ""
421
444
 
422
445
  if not (ignore_tools_thinking_messages and tool_calls):
423
- text = " ".join(block.text for block in anthropic_response.content if block.type == "text")
424
-
425
- message = ChatMessage.from_assistant(text=text, tool_calls=tool_calls, reasoning=reasoning)
446
+ text = " ".join(block.text for block in blocks if block.type == "text")
426
447
 
427
448
  # Dump the chat completion to a dict
428
449
  response_dict = anthropic_response.model_dump()
429
- usage = _get_openai_compatible_usage(response_dict)
430
- message._meta.update(
431
- {
432
- "model": response_dict.get("model", None),
433
- "index": 0,
434
- "finish_reason": FINISH_REASON_MAPPING.get(response_dict.get("stop_reason" or "")),
435
- "usage": usage,
436
- }
437
- )
450
+ meta: dict[str, Any] = {
451
+ "model": response_dict.get("model", None),
452
+ "index": 0,
453
+ "finish_reason": FINISH_REASON_MAPPING.get(response_dict.get("stop_reason")),
454
+ "usage": _get_openai_compatible_usage(response_dict),
455
+ }
438
456
 
439
457
  # keep raw content so server-tool blocks can be replayed on later turns
440
- content_blocks = response_dict.get("content") or []
441
- if _has_server_tool_blocks(content_blocks):
442
- message._meta["raw_content_for_server_tools"] = content_blocks
443
- if citations := _extract_citations(content_blocks):
444
- message._meta["citations"] = citations
458
+ raw_content_blocks = response_dict.get("content") or []
459
+ if _has_server_tool_blocks(raw_content_blocks):
460
+ meta["raw_content_for_server_tools"] = raw_content_blocks
461
+ if citations := _extract_citations(raw_content_blocks):
462
+ meta["citations"] = citations
445
463
 
446
- return message
464
+ return ChatMessage.from_assistant(text=text, tool_calls=tool_calls, reasoning=reasoning, meta=meta)
447
465
 
448
466
 
449
467
  def _convert_anthropic_chunk_to_streaming_chunk(
@@ -46,7 +46,7 @@ class AnthropicVertexChatGenerator(AnthropicChatGenerator):
46
46
 
47
47
  messages = [ChatMessage.from_user("What's Natural Language Processing?")]
48
48
  client = AnthropicVertexChatGenerator(
49
- model="claude-sonnet-4@20250514",
49
+ model="claude-sonnet-4-5@20250929",
50
50
  project_id="your-project-id", region="your-region"
51
51
  )
52
52
  response = client.run(messages)
@@ -57,7 +57,7 @@ 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@20250514', 'index': 0, 'finish_reason': 'end_turn',
60
+ >> _name=None, _meta={'model': 'claude-sonnet-4-5@20250929', 'index': 0, 'finish_reason': 'end_turn',
61
61
  >> 'usage': {'input_tokens': 15, 'output_tokens': 64}})]}
62
62
  ```
63
63
 
@@ -69,13 +69,16 @@ class AnthropicVertexChatGenerator(AnthropicChatGenerator):
69
69
  """
70
70
 
71
71
  SUPPORTED_MODELS: ClassVar[list[str]] = [
72
+ "claude-fable-5-1",
73
+ "claude-fable-5",
74
+ "claude-opus-5",
75
+ "claude-opus-4-8",
76
+ "claude-opus-4-7",
72
77
  "claude-opus-4-6",
78
+ "claude-opus-4-5@20251101",
79
+ "claude-sonnet-5",
73
80
  "claude-sonnet-4-6",
74
81
  "claude-sonnet-4-5@20250929",
75
- "claude-sonnet-4@20250514",
76
- "claude-opus-4-5@20251101",
77
- "claude-opus-4-1@20250805",
78
- "claude-opus-4@20250514",
79
82
  "claude-haiku-4-5@20251001",
80
83
  ]
81
84
  """A non-exhaustive list of chat models supported by this component. See
@@ -85,7 +88,7 @@ class AnthropicVertexChatGenerator(AnthropicChatGenerator):
85
88
  self,
86
89
  region: str,
87
90
  project_id: str,
88
- model: str = "claude-sonnet-4@20250514",
91
+ model: str = "claude-sonnet-4-5@20250929",
89
92
  streaming_callback: Callable[[StreamingChunk], None] | None = None,
90
93
  generation_kwargs: dict[str, Any] | None = None,
91
94
  ignore_tools_thinking_messages: bool = True,
@@ -109,8 +112,12 @@ class AnthropicVertexChatGenerator(AnthropicChatGenerator):
109
112
 
110
113
  Supported generation_kwargs parameters are:
111
114
  - `system`: The system message to be passed to the model.
112
- - `max_tokens`: The maximum number of tokens to generate.
115
+ - `max_tokens`: The maximum number of tokens to generate. Defaults to 8192. A response that hits
116
+ this limit is cut off; if the model was writing a tool call at the time, that call is dropped
117
+ and the reply carries a `length` finish reason.
113
118
  - `metadata`: A dictionary of metadata to be passed to the model.
119
+ - `service_tier`: Whether the request may use priority capacity (`auto`) or standard capacity only
120
+ (`standard_only`). See [service tiers](https://platform.claude.com/docs/en/api/service-tiers).
114
121
  - `stop_sequences`: A list of strings that the model should stop generating at.
115
122
  - `temperature`: The temperature to use for sampling.
116
123
  - `top_p`: The top_p value to use for nucleus sampling.
@@ -481,6 +481,7 @@ class TestRun:
481
481
  "parallel_tool_use": None,
482
482
  "tool_choice_type": None,
483
483
  "adaptive_thinking_effort": None,
484
+ "thinking_display": None,
484
485
  },
485
486
  {},
486
487
  ),
@@ -509,6 +510,54 @@ class TestRun:
509
510
  "thinking": {"type": "disabled"},
510
511
  },
511
512
  ),
513
+ (
514
+ {
515
+ "service_tier": "standard_only",
516
+ },
517
+ {
518
+ "service_tier": "standard_only",
519
+ },
520
+ ),
521
+ (
522
+ {
523
+ "adaptive_thinking_effort": "high",
524
+ "thinking_display": "summarized",
525
+ },
526
+ {
527
+ "thinking": {"type": "adaptive", "display": "summarized"},
528
+ "output_config": {"effort": "high"},
529
+ },
530
+ ),
531
+ (
532
+ # thinking_display is merged into an explicitly passed thinking dict
533
+ {
534
+ "thinking": {"type": "adaptive"},
535
+ "thinking_display": "omitted",
536
+ },
537
+ {
538
+ "thinking": {"type": "adaptive", "display": "omitted"},
539
+ },
540
+ ),
541
+ (
542
+ # thinking_display is dropped when thinking is disabled through a flattened kwarg
543
+ {
544
+ "adaptive_thinking_effort": "none",
545
+ "thinking_display": "summarized",
546
+ },
547
+ {
548
+ "thinking": {"type": "disabled"},
549
+ },
550
+ ),
551
+ (
552
+ # thinking_display is dropped when thinking is disabled through an explicit thinking dict
553
+ {
554
+ "thinking": {"type": "disabled"},
555
+ "thinking_display": "summarized",
556
+ },
557
+ {
558
+ "thinking": {"type": "disabled"},
559
+ },
560
+ ),
512
561
  ],
513
562
  )
514
563
  def test_run_with_flattened_generation_kwargs(
@@ -528,6 +577,42 @@ class TestRun:
528
577
  assert actual_kwargs.get("tool_choice") == expected_kwargs.get("tool_choice")
529
578
  assert actual_kwargs.get("thinking") == expected_kwargs.get("thinking")
530
579
  assert actual_kwargs.get("output_config") == expected_kwargs.get("output_config")
580
+ assert actual_kwargs.get("service_tier") == expected_kwargs.get("service_tier")
581
+
582
+ def test_run_with_flattened_generation_kwargs_does_not_leak_across_runs(self, chat_messages, mock_chat_completion):
583
+ init_generation_kwargs = {
584
+ "tool_choice": {"type": "auto"},
585
+ "thinking": {"type": "adaptive"},
586
+ "output_config": {"effort": "low"},
587
+ }
588
+ component = AnthropicChatGenerator(
589
+ api_key=Secret.from_token("test-api-key"),
590
+ generation_kwargs=init_generation_kwargs,
591
+ )
592
+
593
+ component.run(
594
+ chat_messages,
595
+ generation_kwargs={
596
+ "parallel_tool_use": False,
597
+ "thinking_display": "summarized",
598
+ "adaptive_thinking_effort": "high",
599
+ },
600
+ )
601
+ first_kwargs = mock_chat_completion.call_args.kwargs
602
+ assert first_kwargs["tool_choice"] == {"type": "auto", "disable_parallel_tool_use": True}
603
+ assert first_kwargs["thinking"] == {"type": "adaptive", "display": "summarized"}
604
+ assert first_kwargs["output_config"] == {"effort": "high"}
605
+
606
+ component.run(chat_messages)
607
+ second_kwargs = mock_chat_completion.call_args.kwargs
608
+ assert second_kwargs["tool_choice"] == {"type": "auto"}
609
+ assert second_kwargs["thinking"] == {"type": "adaptive"}
610
+ assert second_kwargs["output_config"] == {"effort": "low"}
611
+ assert component.generation_kwargs == {
612
+ "tool_choice": {"type": "auto"},
613
+ "thinking": {"type": "adaptive"},
614
+ "output_config": {"effort": "low"},
615
+ }
531
616
 
532
617
 
533
618
  class TestAnthropicServerTools:
@@ -569,6 +654,12 @@ class TestAnthropicServerTools:
569
654
  assert len(kwargs["tools"]) == 2
570
655
  assert kwargs["tools"][-1] == {"type": "web_search_20250305"}
571
656
 
657
+ def test_run_with_empty_tools_override(self, chat_messages, mock_chat_completion, tools):
658
+ component = AnthropicChatGenerator(api_key=Secret.from_token("test-api-key"), tools=tools)
659
+ component.run(messages=chat_messages, tools=[])
660
+ _, kwargs = mock_chat_completion.call_args
661
+ assert kwargs["tools"] == []
662
+
572
663
 
573
664
  class TestMixedToolsAndToolsets:
574
665
  def test_init_with_mixed_tools_and_toolsets(self, monkeypatch):
@@ -182,6 +182,135 @@ class TestConvertChatCompletionToChatMessage:
182
182
  assert message.meta["raw_content_for_server_tools"][2]["citations"][0]["encrypted_index"] == "ENCRYPTED_INDEX"
183
183
 
184
184
 
185
+ class TestTruncatedToolCall:
186
+ def test_truncated_tool_call_is_dropped(self, caplog):
187
+ chat_completion = Message(
188
+ id="msg_01MZF",
189
+ content=[
190
+ ToolUseBlock(
191
+ type="tool_use",
192
+ id="toolu_01",
193
+ name="update_description",
194
+ # the model never got to write `description`
195
+ input={"issue_id": "AGE-144"},
196
+ )
197
+ ],
198
+ model="claude-sonnet-4-5",
199
+ role="assistant",
200
+ stop_reason="max_tokens",
201
+ type="message",
202
+ usage=Usage(input_tokens=57, output_tokens=8192),
203
+ )
204
+
205
+ chat_message = _convert_chat_completion_to_chat_message(chat_completion, ignore_tools_thinking_messages=True)
206
+
207
+ assert chat_message.tool_calls == []
208
+ assert chat_message.meta["finish_reason"] == "length"
209
+ assert "max_tokens" in caplog.text
210
+ assert "update_description" in caplog.text
211
+
212
+ def test_completed_tool_calls_before_the_truncated_one_are_kept(self):
213
+ """Only the final block is cut off, so earlier tool calls in the same reply are still usable."""
214
+ chat_completion = Message(
215
+ id="msg_01MZF",
216
+ content=[
217
+ ToolUseBlock(type="tool_use", id="toolu_01", name="weather", input={"city": "Paris"}),
218
+ ToolUseBlock(type="tool_use", id="toolu_02", name="update_description", input={}),
219
+ ],
220
+ model="claude-sonnet-4-5",
221
+ role="assistant",
222
+ stop_reason="max_tokens",
223
+ type="message",
224
+ usage=Usage(input_tokens=57, output_tokens=8192),
225
+ )
226
+
227
+ chat_message = _convert_chat_completion_to_chat_message(chat_completion, ignore_tools_thinking_messages=True)
228
+
229
+ assert [tool_call.tool_name for tool_call in chat_message.tool_calls] == ["weather"]
230
+
231
+ def test_tool_call_is_kept_when_the_response_ended_normally(self):
232
+ chat_completion = Message(
233
+ id="msg_01MZF",
234
+ content=[ToolUseBlock(type="tool_use", id="toolu_01", name="weather", input={"city": "Paris"})],
235
+ model="claude-sonnet-4-5",
236
+ role="assistant",
237
+ stop_reason="tool_use",
238
+ type="message",
239
+ usage=Usage(input_tokens=57, output_tokens=40),
240
+ )
241
+
242
+ chat_message = _convert_chat_completion_to_chat_message(chat_completion, ignore_tools_thinking_messages=True)
243
+
244
+ assert [tool_call.tool_name for tool_call in chat_message.tool_calls] == ["weather"]
245
+ assert chat_message.meta["finish_reason"] == "tool_calls"
246
+
247
+ def test_text_truncated_at_max_tokens_is_kept(self):
248
+ """Only tool calls are dropped: truncated text is still the model's answer, just a shorter one."""
249
+ chat_completion = Message(
250
+ id="msg_01MZF",
251
+ content=[TextBlock(type="text", text="The capital of France is", citations=None)],
252
+ model="claude-sonnet-4-5",
253
+ role="assistant",
254
+ stop_reason="max_tokens",
255
+ type="message",
256
+ usage=Usage(input_tokens=57, output_tokens=8192),
257
+ )
258
+
259
+ chat_message = _convert_chat_completion_to_chat_message(chat_completion, ignore_tools_thinking_messages=True)
260
+
261
+ assert chat_message.text == "The capital of France is"
262
+ assert chat_message.meta["finish_reason"] == "length"
263
+
264
+ @pytest.mark.parametrize("is_async", [False, True])
265
+ async def test_streaming_truncated_tool_call_is_dropped(self, is_async):
266
+ generator = AnthropicChatGenerator(api_key=Secret.from_token("test-api-key"))
267
+ raw_chunks = [
268
+ RawMessageStartEvent(
269
+ message=Message(
270
+ id="msg_123",
271
+ content=[],
272
+ model="claude-sonnet-4-5",
273
+ role="assistant",
274
+ stop_reason=None,
275
+ stop_sequence=None,
276
+ type="message",
277
+ usage=Usage(input_tokens=100, output_tokens=1),
278
+ ),
279
+ type="message_start",
280
+ ),
281
+ RawContentBlockStartEvent(
282
+ content_block=ToolUseBlock(id="toolu_01", input={}, name="update_description", type="tool_use"),
283
+ index=0,
284
+ type="content_block_start",
285
+ ),
286
+ RawContentBlockDeltaEvent(
287
+ delta=InputJSONDelta(
288
+ partial_json='{"issue_id": "AGE-144", "description": "Lorem ip', type="input_json_delta"
289
+ ),
290
+ index=0,
291
+ type="content_block_delta",
292
+ ),
293
+ RawMessageDeltaEvent(
294
+ delta=Delta(stop_reason="max_tokens", stop_sequence=None),
295
+ type="message_delta",
296
+ usage=MessageDeltaUsage(output_tokens=8192),
297
+ ),
298
+ ]
299
+
300
+ if is_async:
301
+
302
+ async def _astream():
303
+ for chunk in raw_chunks:
304
+ yield chunk
305
+
306
+ message = (await generator._process_response_async(_astream()))["replies"][0]
307
+ else:
308
+ message = generator._process_response(raw_chunks)["replies"][0]
309
+
310
+ assert message.tool_calls == []
311
+ assert message.meta["finish_reason"] == "length"
312
+
313
+
185
314
  class TestConvertAnthropicChunkToStreamingChunk:
186
315
  def test_convert_anthropic_completion_chunks_with_multiple_tool_calls_and_reasoning_to_streaming_chunks(self):
187
316
  """
@@ -20,7 +20,7 @@ class TestUnit:
20
20
  component = AnthropicVertexChatGenerator(region="us-central1", project_id="test-project-id")
21
21
  assert component.region == "us-central1"
22
22
  assert component.project_id == "test-project-id"
23
- assert component.model == "claude-sonnet-4@20250514"
23
+ assert component.model == "claude-sonnet-4-5@20250929"
24
24
  assert component.streaming_callback is None
25
25
  assert not component.generation_kwargs
26
26
  assert component.ignore_tools_thinking_messages
@@ -31,14 +31,14 @@ class TestUnit:
31
31
  component = AnthropicVertexChatGenerator(
32
32
  region="us-central1",
33
33
  project_id="test-project-id",
34
- model="claude-sonnet-4@20250514",
34
+ model="claude-sonnet-4-5@20250929",
35
35
  streaming_callback=print_streaming_chunk,
36
36
  generation_kwargs={"max_tokens": 10, "some_test_param": "test-params"},
37
37
  ignore_tools_thinking_messages=False,
38
38
  )
39
39
  assert component.region == "us-central1"
40
40
  assert component.project_id == "test-project-id"
41
- assert component.model == "claude-sonnet-4@20250514"
41
+ assert component.model == "claude-sonnet-4-5@20250929"
42
42
  assert component.streaming_callback is print_streaming_chunk
43
43
  assert component.generation_kwargs == {"max_tokens": 10, "some_test_param": "test-params"}
44
44
  assert component.ignore_tools_thinking_messages is False
@@ -54,7 +54,7 @@ class TestUnit:
54
54
  "init_parameters": {
55
55
  "region": "us-central1",
56
56
  "project_id": "test-project-id",
57
- "model": "claude-sonnet-4@20250514",
57
+ "model": "claude-sonnet-4-5@20250929",
58
58
  "streaming_callback": None,
59
59
  "generation_kwargs": {},
60
60
  "ignore_tools_thinking_messages": True,
@@ -85,7 +85,7 @@ class TestUnit:
85
85
  "init_parameters": {
86
86
  "region": "us-central1",
87
87
  "project_id": "test-project-id",
88
- "model": "claude-sonnet-4@20250514",
88
+ "model": "claude-sonnet-4-5@20250929",
89
89
  "streaming_callback": "haystack.components.generators.utils.print_streaming_chunk",
90
90
  "generation_kwargs": {"max_tokens": 10, "some_test_param": "test-params"},
91
91
  "ignore_tools_thinking_messages": False,
@@ -105,7 +105,7 @@ class TestUnit:
105
105
  "init_parameters": {
106
106
  "region": "us-central1",
107
107
  "project_id": "test-project-id",
108
- "model": "claude-sonnet-4@20250514",
108
+ "model": "claude-sonnet-4-5@20250929",
109
109
  "streaming_callback": "haystack.components.generators.utils.print_streaming_chunk",
110
110
  "generation_kwargs": {"max_tokens": 10, "some_test_param": "test-params"},
111
111
  "ignore_tools_thinking_messages": True,
@@ -116,7 +116,7 @@ class TestUnit:
116
116
  },
117
117
  }
118
118
  component = AnthropicVertexChatGenerator.from_dict(data)
119
- assert component.model == "claude-sonnet-4@20250514"
119
+ assert component.model == "claude-sonnet-4-5@20250929"
120
120
  assert component.region == "us-central1"
121
121
  assert component.project_id == "test-project-id"
122
122
  assert component.streaming_callback is print_streaming_chunk
@@ -220,7 +220,7 @@ class TestIntegration:
220
220
 
221
221
  def test_default_inference_params(self, chat_messages):
222
222
  client = AnthropicVertexChatGenerator(
223
- region=os.environ.get("REGION"), project_id=os.environ.get("PROJECT_ID"), model="claude-sonnet-4@20250514"
223
+ region=os.environ.get("REGION"), project_id=os.environ.get("PROJECT_ID"), model="claude-sonnet-4-5@20250929"
224
224
  )
225
225
  response = client.run(chat_messages)
226
226
 
@@ -243,7 +243,7 @@ class TestIntegration:
243
243
  component = AnthropicVertexChatGenerator(
244
244
  region=os.environ.get("REGION"),
245
245
  project_id=os.environ.get("PROJECT_ID"),
246
- model="claude-sonnet-4@20250514",
246
+ model="claude-sonnet-4-5@20250929",
247
247
  )
248
248
  results = await component.run_async(messages=[ChatMessage.from_user("What's the capital of France?")])
249
249
  assert len(results["replies"]) == 1