evidor 1.11.0.dev1__tar.gz → 1.11.1.dev1__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 (102) hide show
  1. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/CHANGELOG.md +14 -0
  2. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/PKG-INFO +14 -7
  3. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/README.md +13 -6
  4. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/pyproject.toml +1 -1
  5. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/providers/anthropic/builder.py +115 -28
  6. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/providers/anthropic/parser.py +24 -5
  7. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/providers/anthropic/stream.py +16 -5
  8. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/providers/gemini/parser.py +4 -4
  9. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/tests/test_thinking.py +193 -0
  10. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/.github/workflows/release.yml +0 -0
  11. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/.gitignore +0 -0
  12. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/AGENTS.md +0 -0
  13. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/CODE_OF_CONDUCT.md +0 -0
  14. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/LICENSE +0 -0
  15. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/requirements.txt +0 -0
  16. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/__init__.py +0 -0
  17. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/__init__.py +0 -0
  18. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/core/__init__.py +0 -0
  19. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/core/agent.py +0 -0
  20. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/core/context.py +0 -0
  21. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/core/runner.py +0 -0
  22. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/core/telemetry_bridge.py +0 -0
  23. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/providers/__init__.py +0 -0
  24. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/providers/anthropic/__init__.py +0 -0
  25. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/providers/anthropic/provider.py +0 -0
  26. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/providers/base.py +0 -0
  27. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/providers/client_pool.py +0 -0
  28. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/providers/gemini/__init__.py +0 -0
  29. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/providers/gemini/builder.py +0 -0
  30. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/providers/gemini/provider.py +0 -0
  31. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/providers/gemini/stream.py +0 -0
  32. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/providers/openai/__init__.py +0 -0
  33. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/providers/openai/builder.py +0 -0
  34. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/providers/openai/parser.py +0 -0
  35. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/providers/openai/provider.py +0 -0
  36. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/agent/providers/openai/stream.py +0 -0
  37. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/constants.py +0 -0
  38. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/errors.py +0 -0
  39. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/mcp/__init__.py +0 -0
  40. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/mcp/client.py +0 -0
  41. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/mcp/config.py +0 -0
  42. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/mcp/runner.py +0 -0
  43. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/mcp/session.py +0 -0
  44. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/models/__init__.py +0 -0
  45. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/models/generation.py +0 -0
  46. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/models/streaming.py +0 -0
  47. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/models/thinking.py +0 -0
  48. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/telemetry/__init__.py +0 -0
  49. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/telemetry/adapters/__init__.py +0 -0
  50. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/telemetry/adapters/langfuse.py +0 -0
  51. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/telemetry/adapters/opentelemetry.py +0 -0
  52. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/telemetry/adapters/phoenix.py +0 -0
  53. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/telemetry/adapters/prometheus.py +0 -0
  54. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/telemetry/context.py +0 -0
  55. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/telemetry/events.py +0 -0
  56. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/telemetry/runtime.py +0 -0
  57. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/telemetry/semconv.py +0 -0
  58. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/telemetry/sink.py +0 -0
  59. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/__init__.py +0 -0
  60. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/core/__init__.py +0 -0
  61. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/core/tool_executor.py +0 -0
  62. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/core/tools.py +0 -0
  63. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/core/utils.py +0 -0
  64. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/__init__.py +0 -0
  65. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/calculator.py +0 -0
  66. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/evidence_store.py +0 -0
  67. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/filesystem/__init__.py +0 -0
  68. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/filesystem/_common.py +0 -0
  69. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/filesystem/list_files.py +0 -0
  70. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/filesystem/mutate_files.py +0 -0
  71. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/filesystem/read_file.py +0 -0
  72. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/filesystem/search_files.py +0 -0
  73. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/get_current_time.py +0 -0
  74. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/read_webpage.py +0 -0
  75. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/web_search/__init__.py +0 -0
  76. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/web_search/_base.py +0 -0
  77. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/web_search/http.py +0 -0
  78. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/web_search/providers/__init__.py +0 -0
  79. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/web_search/providers/brave.py +0 -0
  80. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/web_search/providers/exa.py +0 -0
  81. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/web_search/providers/tavily.py +0 -0
  82. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/tools/native/web_search/tool.py +0 -0
  83. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/utils/__init__.py +0 -0
  84. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/utils/execution.py +0 -0
  85. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/src/evidor/utils/schema.py +0 -0
  86. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/tests/test_agent.py +0 -0
  87. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/tests/test_builtin_tools.py +0 -0
  88. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/tests/test_conversation.py +0 -0
  89. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/tests/test_filesystem_tools.py +0 -0
  90. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/tests/test_langfuse_adapter.py +0 -0
  91. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/tests/test_mcp.py +0 -0
  92. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/tests/test_models.py +0 -0
  93. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/tests/test_native_tools.py +0 -0
  94. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/tests/test_opentelemetry_adapter.py +0 -0
  95. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/tests/test_phoenix_adapter.py +0 -0
  96. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/tests/test_prometheus_adapter.py +0 -0
  97. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/tests/test_providers.py +0 -0
  98. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/tests/test_streaming.py +0 -0
  99. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/tests/test_telemetry_events.py +0 -0
  100. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/tests/test_telemetry_integration.py +0 -0
  101. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/tests/test_tools.py +0 -0
  102. {evidor-1.11.0.dev1 → evidor-1.11.1.dev1}/tests/test_web_search.py +0 -0
@@ -2,6 +2,20 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v1.11.1-dev.1 (2026-11-04)
6
+
7
+ ### Bug Fixes
8
+
9
+ - **anthropic**: Fixing reasoning mode issues
10
+ ([`405c681`](https://github.com/vedangiitb/evidor-core/commit/405c68150ea149f0bb693a1fb23bcd5151267211))
11
+
12
+ - **gemini**: Removing redundant fields in parsing
13
+ ([`555be1d`](https://github.com/vedangiitb/evidor-core/commit/555be1dfd2e0f79cc5c27c3c9c6cf342f5ca1029))
14
+
15
+
16
+ ## v1.11.0 (2026-11-03)
17
+
18
+
5
19
  ## v1.11.0-dev.1 (2026-11-03)
6
20
 
7
21
  ### Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: evidor
3
- Version: 1.11.0-dev.1
3
+ Version: 1.11.1-dev.1
4
4
  Summary: A small, provider-agnostic harness for LLM input and output.
5
5
  Project-URL: Homepage, https://github.com/evidor/evidor-core
6
6
  Project-URL: Repository, https://github.com/evidor/evidor-core
@@ -554,10 +554,17 @@ p_openai_cfg = OpenAIProvider(
554
554
  thinking=ThinkingConfig(effort="high", summary="auto"),
555
555
  )
556
556
 
557
- # 2. Anthropic: Configure extended budget tokens (budget_tokens >= 1024; auto-bumps max_tokens if needed)
558
- p_anthropic_cfg = AnthropicProvider(
557
+ # 2. Anthropic: Configure adaptive thinking with reasoning summary, or manual budget tokens
558
+ # Adaptive thinking with reasoning summary:
559
+ p_anthropic_adaptive = AnthropicProvider(
559
560
  model="claude-3-7-sonnet-20250219",
560
- thinking=ThinkingConfig(budget_tokens=4096),
561
+ thinking=ThinkingConfig(effort="high", summary="auto"),
562
+ )
563
+
564
+ # Extended manual budget tokens (budget_tokens >= 1024; auto-bumps max_tokens if needed):
565
+ p_anthropic_budget = AnthropicProvider(
566
+ model="claude-3-7-sonnet-20250219",
567
+ thinking=ThinkingConfig(budget_tokens=4096, summary="omitted"),
561
568
  )
562
569
 
563
570
  # 3. Gemini: Configure thinking level and summaries (or thinking budget for earlier models)
@@ -577,9 +584,9 @@ p_gemini_budget = GeminiProvider(
577
584
 
578
585
  | Field | Type | Description & Provider Mapping |
579
586
  | :--- | :--- | :--- |
580
- | `effort` | `str \| None` | Reasoning intensity. Passed through directly without client-side mapping or gating to ensure forward compatibility with new provider levels (e.g., `"none"`, `"minimal"`, `"low"`, `"medium"`, `"high"`, `"xhigh"`, `"max"`). Supported by OpenAI (`reasoning.effort`), Gemini (`thinking_level`), and Anthropic (`effort`). If an unexpected effort level is passed, the provider API handles it directly. |
587
+ | `effort` | `str \| None` | Reasoning intensity. Passed through directly without client-side mapping or gating to ensure forward compatibility with new provider levels (e.g., `"none"`, `"minimal"`, `"low"`, `"medium"`, `"high"`, `"xhigh"`, `"max"`). Supported by OpenAI (`reasoning.effort`), Gemini (`thinking_level`), and Anthropic (`output_config.effort` with adaptive thinking). If an unexpected effort level is passed, the provider API handles it directly. |
581
588
  | `budget_tokens` | `int \| None` | Maximum reasoning token budget where supported. Supported by Anthropic (`thinking={"type": "enabled", "budget_tokens": ...}`) and Gemini (`thinking_budget`). |
582
- | `summary` | `str \| None` | Whether and how to expose reasoning summaries. Supported by OpenAI (`reasoning.summary`) and Gemini (`thinking_summaries`).<br>**Note on Gemini**: Gemini natively supports `"auto"` and `"none"`. When targeting Gemini, you should specify `"auto"` or `"none"`. As a fallback, Evidor automatically maps other non-`none` strings (e.g. `"concise"`, `"detailed"`) to `"auto"`. |
589
+ | `summary` | `str \| None` | Whether and how to expose reasoning summaries. Supported across all three providers:<br>• **Anthropic**: Controls reasoning exposure via Claude's `thinking.display` parameter (`"summarized"` vs `"omitted"`). Values such as `"auto"`, `"summarized"`, `"concise"`, or `"detailed"` set `display: "summarized"`, populating `response.thinking` with a human-readable summary. Values like `"none"` or `"omitted"` set `display: "omitted"`, omitting thinking text to minimize time-to-first-token while preserving cryptographic signatures in `thought_blocks` for multi-turn replay.<br>• **OpenAI**: Configures `reasoning.summary`.<br>• **Gemini**: Configures `thinking_summaries` (`"auto"` or `"none"`). As a fallback, Evidor automatically maps other non-`none` strings (e.g. `"concise"`, `"detailed"`) to `"auto"`. |
583
590
 
584
591
  > [!NOTE]
585
592
  > **No Boolean or Standalone Integer Shorthands**: Shorthands like `thinking=False` or `thinking=4096` are intentionally not supported to avoid ambiguity. To run without thinking, simply omit the parameter or pass `thinking=None`. To configure a token budget, use `ThinkingConfig(budget_tokens=4096)`.
@@ -606,7 +613,7 @@ if response.usage and response.usage.reasoning_tokens:
606
613
 
607
614
  #### Multi-Turn Tool Calling & Cryptographic Signatures
608
615
 
609
- Reasoning models from Anthropic and Gemini output thought blocks alongside cryptographic signatures (such as Anthropic's `signature` field or Gemini's thought parts). In multi-turn tool loops, these signatures must be preserved and replayed in subsequent conversation turns to avoid API verification errors. Evidor automatically captures and replays them in `agent.messages` and `thought_blocks` without requiring any manual handling.
616
+ Reasoning models from Anthropic and Gemini output thought blocks alongside cryptographic signatures (such as Anthropic's `signature` field or Gemini's thought parts). In multi-turn tool loops, these signatures must be preserved and replayed in subsequent conversation turns to avoid API verification errors. Evidor automatically captures and replays them in `agent.messages` and `thought_blocks` without requiring any manual handling. Furthermore, Evidor normalizes and sanitizes thought blocks across providers (e.g. converting between Gemini's and Anthropic's signature representations and stripping non-standard fields) so history can be transferred or replayed seamlessly without schema violations.
610
617
 
611
618
  ## Tools & Function Calling
612
619
 
@@ -467,10 +467,17 @@ p_openai_cfg = OpenAIProvider(
467
467
  thinking=ThinkingConfig(effort="high", summary="auto"),
468
468
  )
469
469
 
470
- # 2. Anthropic: Configure extended budget tokens (budget_tokens >= 1024; auto-bumps max_tokens if needed)
471
- p_anthropic_cfg = AnthropicProvider(
470
+ # 2. Anthropic: Configure adaptive thinking with reasoning summary, or manual budget tokens
471
+ # Adaptive thinking with reasoning summary:
472
+ p_anthropic_adaptive = AnthropicProvider(
472
473
  model="claude-3-7-sonnet-20250219",
473
- thinking=ThinkingConfig(budget_tokens=4096),
474
+ thinking=ThinkingConfig(effort="high", summary="auto"),
475
+ )
476
+
477
+ # Extended manual budget tokens (budget_tokens >= 1024; auto-bumps max_tokens if needed):
478
+ p_anthropic_budget = AnthropicProvider(
479
+ model="claude-3-7-sonnet-20250219",
480
+ thinking=ThinkingConfig(budget_tokens=4096, summary="omitted"),
474
481
  )
475
482
 
476
483
  # 3. Gemini: Configure thinking level and summaries (or thinking budget for earlier models)
@@ -490,9 +497,9 @@ p_gemini_budget = GeminiProvider(
490
497
 
491
498
  | Field | Type | Description & Provider Mapping |
492
499
  | :--- | :--- | :--- |
493
- | `effort` | `str \| None` | Reasoning intensity. Passed through directly without client-side mapping or gating to ensure forward compatibility with new provider levels (e.g., `"none"`, `"minimal"`, `"low"`, `"medium"`, `"high"`, `"xhigh"`, `"max"`). Supported by OpenAI (`reasoning.effort`), Gemini (`thinking_level`), and Anthropic (`effort`). If an unexpected effort level is passed, the provider API handles it directly. |
500
+ | `effort` | `str \| None` | Reasoning intensity. Passed through directly without client-side mapping or gating to ensure forward compatibility with new provider levels (e.g., `"none"`, `"minimal"`, `"low"`, `"medium"`, `"high"`, `"xhigh"`, `"max"`). Supported by OpenAI (`reasoning.effort`), Gemini (`thinking_level`), and Anthropic (`output_config.effort` with adaptive thinking). If an unexpected effort level is passed, the provider API handles it directly. |
494
501
  | `budget_tokens` | `int \| None` | Maximum reasoning token budget where supported. Supported by Anthropic (`thinking={"type": "enabled", "budget_tokens": ...}`) and Gemini (`thinking_budget`). |
495
- | `summary` | `str \| None` | Whether and how to expose reasoning summaries. Supported by OpenAI (`reasoning.summary`) and Gemini (`thinking_summaries`).<br>**Note on Gemini**: Gemini natively supports `"auto"` and `"none"`. When targeting Gemini, you should specify `"auto"` or `"none"`. As a fallback, Evidor automatically maps other non-`none` strings (e.g. `"concise"`, `"detailed"`) to `"auto"`. |
502
+ | `summary` | `str \| None` | Whether and how to expose reasoning summaries. Supported across all three providers:<br>• **Anthropic**: Controls reasoning exposure via Claude's `thinking.display` parameter (`"summarized"` vs `"omitted"`). Values such as `"auto"`, `"summarized"`, `"concise"`, or `"detailed"` set `display: "summarized"`, populating `response.thinking` with a human-readable summary. Values like `"none"` or `"omitted"` set `display: "omitted"`, omitting thinking text to minimize time-to-first-token while preserving cryptographic signatures in `thought_blocks` for multi-turn replay.<br>• **OpenAI**: Configures `reasoning.summary`.<br>• **Gemini**: Configures `thinking_summaries` (`"auto"` or `"none"`). As a fallback, Evidor automatically maps other non-`none` strings (e.g. `"concise"`, `"detailed"`) to `"auto"`. |
496
503
 
497
504
  > [!NOTE]
498
505
  > **No Boolean or Standalone Integer Shorthands**: Shorthands like `thinking=False` or `thinking=4096` are intentionally not supported to avoid ambiguity. To run without thinking, simply omit the parameter or pass `thinking=None`. To configure a token budget, use `ThinkingConfig(budget_tokens=4096)`.
@@ -519,7 +526,7 @@ if response.usage and response.usage.reasoning_tokens:
519
526
 
520
527
  #### Multi-Turn Tool Calling & Cryptographic Signatures
521
528
 
522
- Reasoning models from Anthropic and Gemini output thought blocks alongside cryptographic signatures (such as Anthropic's `signature` field or Gemini's thought parts). In multi-turn tool loops, these signatures must be preserved and replayed in subsequent conversation turns to avoid API verification errors. Evidor automatically captures and replays them in `agent.messages` and `thought_blocks` without requiring any manual handling.
529
+ Reasoning models from Anthropic and Gemini output thought blocks alongside cryptographic signatures (such as Anthropic's `signature` field or Gemini's thought parts). In multi-turn tool loops, these signatures must be preserved and replayed in subsequent conversation turns to avoid API verification errors. Evidor automatically captures and replays them in `agent.messages` and `thought_blocks` without requiring any manual handling. Furthermore, Evidor normalizes and sanitizes thought blocks across providers (e.g. converting between Gemini's and Anthropic's signature representations and stripping non-standard fields) so history can be transferred or replayed seamlessly without schema violations.
523
530
 
524
531
  ## Tools & Function Calling
525
532
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "evidor"
7
- version = "1.11.0-dev.1"
7
+ version = "1.11.1-dev.1"
8
8
  description = "A small, provider-agnostic harness for LLM input and output."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -4,8 +4,8 @@ from collections.abc import Sequence
4
4
  import json
5
5
  from typing import Any
6
6
 
7
- from evidor.models import GenerationRequest, Message
8
- from ..base import extract_system_prompt, sort_tools
7
+ from evidor.models import GenerationRequest, Message, ThinkingConfig
8
+ from ..base import extract_system_prompt, safe_parse_tool_arguments, sort_tools
9
9
 
10
10
 
11
11
  class AnthropicPayloadBuilder:
@@ -54,26 +54,61 @@ class AnthropicPayloadBuilder:
54
54
  payload["output_config"] = {"effort": active_thinking}
55
55
  elif active_thinking and not isinstance(active_thinking, (bool, int)):
56
56
  budget = (
57
- active_thinking.get("budget_tokens")
57
+ active_thinking.get("budget_tokens") or active_thinking.get("thinking_budget")
58
58
  if isinstance(active_thinking, dict)
59
- else getattr(active_thinking, "budget_tokens", None)
59
+ else getattr(active_thinking, "budget_tokens", None) or getattr(active_thinking, "thinking_budget", None)
60
60
  )
61
61
  effort = (
62
- active_thinking.get("effort")
62
+ active_thinking.get("effort") or active_thinking.get("thinking_level")
63
63
  if isinstance(active_thinking, dict)
64
- else getattr(active_thinking, "effort", None)
64
+ else getattr(active_thinking, "effort", None) or getattr(active_thinking, "thinking_level", None)
65
65
  )
66
- if budget is not None and not isinstance(budget, bool) and budget > 0:
67
- actual_budget = max(budget, 1024)
68
- payload["thinking"] = {
66
+ raw_summary = (
67
+ active_thinking.get("summary") or active_thinking.get("display") or active_thinking.get("thinking_summaries")
68
+ if isinstance(active_thinking, dict)
69
+ else getattr(active_thinking, "summary", None) or getattr(active_thinking, "display", None) or getattr(active_thinking, "thinking_summaries", None)
70
+ )
71
+ explicit_type = (
72
+ active_thinking.get("type")
73
+ if isinstance(active_thinking, dict)
74
+ else getattr(active_thinking, "type", None)
75
+ )
76
+
77
+ display_val: str | None = None
78
+ if isinstance(raw_summary, bool):
79
+ display_val = "summarized" if raw_summary else "omitted"
80
+ elif isinstance(raw_summary, str) and raw_summary.strip():
81
+ cleaned_summary = raw_summary.strip().lower()
82
+ if cleaned_summary in ("none", "omitted", "off", "hidden", "false"):
83
+ display_val = "omitted"
84
+ elif cleaned_summary in ("summarized", "summary", "auto", "concise", "detailed", "on", "visible", "true"):
85
+ display_val = "summarized"
86
+ else:
87
+ display_val = raw_summary.strip()
88
+
89
+ if (budget is not None and not isinstance(budget, bool) and budget > 0) or explicit_type == "enabled":
90
+ actual_budget = max(budget or 1024, 1024)
91
+ thinking_dict: dict[str, Any] = {
69
92
  "type": "enabled",
70
93
  "budget_tokens": actual_budget,
71
94
  }
95
+ if display_val:
96
+ thinking_dict["display"] = display_val
97
+ payload["thinking"] = thinking_dict
72
98
  if payload["max_tokens"] <= actual_budget:
73
99
  payload["max_tokens"] = actual_budget + 1024
74
- elif isinstance(effort, str):
75
- payload["thinking"] = {"type": "adaptive"}
76
- payload["output_config"] = {"effort": effort}
100
+ elif (
101
+ isinstance(effort, str)
102
+ or display_val is not None
103
+ or explicit_type == "adaptive"
104
+ or isinstance(active_thinking, ThinkingConfig)
105
+ ):
106
+ thinking_dict = {"type": "adaptive"}
107
+ if display_val:
108
+ thinking_dict["display"] = display_val
109
+ payload["thinking"] = thinking_dict
110
+ if isinstance(effort, str) and effort.strip():
111
+ payload["output_config"] = {"effort": effort.strip()}
77
112
 
78
113
  if self.enable_prompt_caching:
79
114
  payload["cache_control"] = {"type": "ephemeral"}
@@ -109,25 +144,65 @@ class AnthropicPayloadBuilder:
109
144
  @staticmethod
110
145
  def _normalize_thought_block(tb: Any) -> dict[str, Any] | None:
111
146
  """Convert a thought block (dict, Pydantic model, SDK object) to an Anthropic block dict."""
147
+ if tb is None:
148
+ return None
149
+
150
+ block_dict: dict[str, Any] | None = None
112
151
  if isinstance(tb, dict):
113
- return dict(tb)
114
- if hasattr(tb, "model_dump") and callable(tb.model_dump):
152
+ block_dict = tb
153
+ elif hasattr(tb, "model_dump") and callable(tb.model_dump):
115
154
  dumped = tb.model_dump()
116
155
  if isinstance(dumped, dict):
117
- return dumped
118
- if hasattr(tb, "to_dict") and callable(tb.to_dict):
156
+ block_dict = dumped
157
+ elif hasattr(tb, "to_dict") and callable(tb.to_dict):
119
158
  dumped = tb.to_dict()
120
159
  if isinstance(dumped, dict):
121
- return dumped
122
- if hasattr(tb, "type"):
123
- block: dict[str, Any] = {"type": getattr(tb, "type")}
124
- if hasattr(tb, "thinking") and getattr(tb, "thinking") is not None:
125
- block["thinking"] = getattr(tb, "thinking")
126
- if hasattr(tb, "signature") and getattr(tb, "signature") is not None:
127
- block["signature"] = getattr(tb, "signature")
128
- if hasattr(tb, "data") and getattr(tb, "data") is not None:
129
- block["data"] = getattr(tb, "data")
130
- return block
160
+ block_dict = dumped
161
+
162
+ def _get(key: str, default: Any = None) -> Any:
163
+ if block_dict is not None:
164
+ return block_dict.get(key, default)
165
+ return getattr(tb, key, default)
166
+
167
+ raw_type = _get("type")
168
+ block_type = str(raw_type).strip().lower() if raw_type is not None else None
169
+
170
+ if block_type == "redacted_thinking":
171
+ data_val = _get("data", "")
172
+ return {
173
+ "type": "redacted_thinking",
174
+ "data": str(data_val or ""),
175
+ }
176
+
177
+ is_thought = (
178
+ block_type in ("thinking", "thought", "reasoning")
179
+ or _get("thinking") is not None
180
+ or _get("thought") is not None
181
+ or _get("thought_signature") is not None
182
+ or _get("thoughtSignature") is not None
183
+ or _get("signature") is not None
184
+ )
185
+
186
+ if is_thought:
187
+ thinking_text = (
188
+ _get("thinking")
189
+ or _get("summary")
190
+ or _get("text")
191
+ or ""
192
+ )
193
+ sig = (
194
+ _get("signature")
195
+ or _get("thought_signature")
196
+ or _get("thoughtSignature")
197
+ )
198
+ normalized: dict[str, Any] = {
199
+ "type": "thinking",
200
+ "thinking": str(thinking_text) if thinking_text is not None else "",
201
+ }
202
+ if sig is not None and str(sig).strip():
203
+ normalized["signature"] = str(sig).strip()
204
+ return normalized
205
+
131
206
  return None
132
207
 
133
208
  def format_messages(self, messages: Sequence[Message]) -> list[dict[str, Any]]:
@@ -184,15 +259,27 @@ class AnthropicPayloadBuilder:
184
259
  content_blocks.append({"type": "text", "text": message.content})
185
260
  if message.tool_calls:
186
261
  for tc in message.tool_calls:
262
+ tc_args = (
263
+ tc.arguments
264
+ if isinstance(tc.arguments, dict)
265
+ else (safe_parse_tool_arguments(tc.arguments) if tc.arguments else {})
266
+ )
187
267
  content_blocks.append({
188
268
  "type": "tool_use",
189
269
  "id": tc.id,
190
270
  "name": tc.name,
191
- "input": tc.arguments,
271
+ "input": tc_args,
192
272
  })
273
+ if not content_blocks:
274
+ fallback_text = message.content if (isinstance(message.content, str) and message.content.strip()) else "Done."
275
+ content_blocks.append({"type": "text", "text": fallback_text})
193
276
  formatted.append({"role": "assistant", "content": content_blocks})
194
277
  else:
195
- formatted.append({"role": "assistant", "content": message.content})
278
+ if isinstance(message.content, list):
279
+ formatted.append({"role": "assistant", "content": message.content})
280
+ else:
281
+ content_val = message.content if (isinstance(message.content, str) and message.content.strip()) else "Done."
282
+ formatted.append({"role": "assistant", "content": content_val})
196
283
 
197
284
  else:
198
285
  # User or other role
@@ -39,9 +39,18 @@ class AnthropicResponseParser:
39
39
 
40
40
  thinking_text = "".join(thought_parts) if thought_parts else None
41
41
  if thinking_text is None:
42
- raw_thinking = _get_field(response_msg, "thinking") or _get_field(response_msg, "reasoning")
42
+ raw_thinking = (
43
+ _get_field(response_msg, "thinking")
44
+ or _get_field(response_msg, "reasoning")
45
+ or _get_field(response_msg, "thinking_summary")
46
+ or _get_field(response_msg, "summary")
47
+ )
43
48
  if isinstance(raw_thinking, str) and raw_thinking.strip():
44
49
  thinking_text = raw_thinking.strip()
50
+ elif isinstance(raw_thinking, dict):
51
+ th = raw_thinking.get("thinking") or raw_thinking.get("summary") or raw_thinking.get("text")
52
+ if isinstance(th, str) and th.strip():
53
+ thinking_text = th.strip()
45
54
 
46
55
  usage = self.parse_usage(_get_field(response_msg, "usage"))
47
56
 
@@ -93,10 +102,20 @@ class AnthropicResponseParser:
93
102
  )
94
103
  if c:
95
104
  compaction_summary = str(c)
96
- elif block_type == "thinking":
97
- th_text = _get_field(block, "thinking") or _get_field(block, "text") or ""
98
- thought_parts.append(str(th_text))
99
- sig = _get_field(block, "signature")
105
+ elif block_type in ("thinking", "thought"):
106
+ th_text = (
107
+ _get_field(block, "thinking")
108
+ or _get_field(block, "summary")
109
+ or _get_field(block, "text")
110
+ or ""
111
+ )
112
+ if str(th_text).strip():
113
+ thought_parts.append(str(th_text))
114
+ sig = (
115
+ _get_field(block, "signature")
116
+ or _get_field(block, "thought_signature")
117
+ or _get_field(block, "thoughtSignature")
118
+ )
100
119
  tb: dict[str, Any] = {"type": "thinking", "thinking": str(th_text)}
101
120
  if isinstance(sig, str) and sig.strip():
102
121
  tb["signature"] = sig.strip()
@@ -53,8 +53,8 @@ class AnthropicStreamHandler:
53
53
  name = _get_field(cb, "name") or "unknown_tool"
54
54
  self.block_id_map[idx] = str(call_id)
55
55
  self.tool_acc.start_call(str(call_id), str(name))
56
- elif cb_type == "thinking":
57
- th_text = _get_field(cb, "thinking") or ""
56
+ elif cb_type in ("thinking", "thought"):
57
+ th_text = _get_field(cb, "thinking") or _get_field(cb, "summary") or ""
58
58
  self.block_thinking_map[idx] = {
59
59
  "type": "thinking",
60
60
  "parts": [str(th_text)] if th_text else [],
@@ -85,8 +85,17 @@ class AnthropicStreamHandler:
85
85
  if txt:
86
86
  self.accumulated_text.append(str(txt))
87
87
  yield StreamChunk(text=str(txt), raw_event=event)
88
- elif delta_type == "thinking_delta" or hasattr(delta, "thinking"):
89
- th = _get_field(delta, "thinking", "") or ""
88
+ elif (
89
+ delta_type in (
90
+ "thinking_delta",
91
+ "thinking_summary_delta",
92
+ "reasoning_delta",
93
+ "reasoning_summary_delta",
94
+ )
95
+ or hasattr(delta, "thinking")
96
+ or hasattr(delta, "summary")
97
+ ):
98
+ th = _get_field(delta, "thinking", "") or _get_field(delta, "summary", "") or ""
90
99
  if th:
91
100
  self.text_thinking_parts.append(str(th))
92
101
  if idx in self.block_thinking_map and "parts" in self.block_thinking_map[idx]:
@@ -101,7 +110,9 @@ class AnthropicStreamHandler:
101
110
  elif delta_type == "signature_delta" or hasattr(delta, "signature"):
102
111
  sig = _get_field(delta, "signature", "") or ""
103
112
  if sig and idx in self.block_thinking_map:
104
- self.block_thinking_map[idx]["signature"] = str(sig)
113
+ self.block_thinking_map[idx]["signature"] = (
114
+ self.block_thinking_map[idx].get("signature", "") + str(sig)
115
+ )
105
116
  elif delta_type == "input_json_delta" or hasattr(delta, "partial_json"):
106
117
  call_id = self.block_id_map.get(idx, f"call_{idx}")
107
118
  pj = _get_field(delta, "partial_json", "") or ""
@@ -152,11 +152,11 @@ class GeminiResponseParser:
152
152
  return val
153
153
  return default
154
154
 
155
- pt = _get_field("total_input_tokens", "input_tokens", "prompt_tokens")
156
- ct = _get_field("total_output_tokens", "output_tokens", "completion_tokens")
155
+ pt = _get_field("total_input_tokens")
156
+ ct = _get_field("total_output_tokens")
157
157
  tt = _get_field("total_tokens")
158
- cached = _get_field("total_cached_tokens", "cached_tokens", default=0) or 0
159
- reasoning = _get_field("total_thought_tokens", "thought_tokens", "reasoning_tokens", default=0) or 0
158
+ cached = _get_field("total_cached_tokens", default=0) or 0
159
+ reasoning = _get_field("total_thought_tokens", default=0) or 0
160
160
 
161
161
  if not (isinstance(pt, int) or isinstance(ct, int) or isinstance(tt, int)):
162
162
  return None
@@ -224,6 +224,199 @@ def test_anthropic_provider_multiturn_thought_signature_replay(monkeypatch: pyte
224
224
  assert content_blocks[1]["id"] == "call_99"
225
225
 
226
226
 
227
+ def test_anthropic_builder_adaptive_effort_and_summary() -> None:
228
+ from evidor.agent.providers.anthropic.builder import AnthropicPayloadBuilder
229
+
230
+ # 1. Adaptive with effort and summary="auto" -> display: "summarized"
231
+ cfg1 = ThinkingConfig(effort="high", summary="auto")
232
+ b1 = AnthropicPayloadBuilder(model="claude-3-7-sonnet", max_tokens=2048, thinking=cfg1)
233
+ p1 = b1.build(GenerationRequest(prompt="Explain quantum mechanics"))
234
+ assert p1["thinking"] == {"type": "adaptive", "display": "summarized"}
235
+ assert p1["output_config"] == {"effort": "high"}
236
+
237
+ # 2. Adaptive with effort and summary="none" -> display: "omitted"
238
+ cfg2 = ThinkingConfig(effort="low", summary="none")
239
+ b2 = AnthropicPayloadBuilder(model="claude-3-7-sonnet", max_tokens=2048, thinking=cfg2)
240
+ p2 = b2.build(GenerationRequest(prompt="Simple math"))
241
+ assert p2["thinking"] == {"type": "adaptive", "display": "omitted"}
242
+ assert p2["output_config"] == {"effort": "low"}
243
+
244
+ # 3. Summary only ("detailed") -> adaptive thinking with display: "summarized"
245
+ cfg3 = ThinkingConfig(summary="detailed")
246
+ b3 = AnthropicPayloadBuilder(model="claude-3-7-sonnet", max_tokens=2048, thinking=cfg3)
247
+ p3 = b3.build(GenerationRequest(prompt="Summarize"))
248
+ assert p3["thinking"] == {"type": "adaptive", "display": "summarized"}
249
+ assert "output_config" not in p3
250
+
251
+ # 4. Summary only ("omitted") -> adaptive thinking with display: "omitted"
252
+ cfg4 = ThinkingConfig(summary="omitted")
253
+ b4 = AnthropicPayloadBuilder(model="claude-3-7-sonnet", max_tokens=2048, thinking=cfg4)
254
+ p4 = b4.build(GenerationRequest(prompt="Code quickly"))
255
+ assert p4["thinking"] == {"type": "adaptive", "display": "omitted"}
256
+
257
+ # 5. Budget tokens with summary="concise" -> display: "summarized"
258
+ cfg5 = ThinkingConfig(budget_tokens=2048, summary="concise")
259
+ b5 = AnthropicPayloadBuilder(model="claude-3-7-sonnet", max_tokens=2048, thinking=cfg5)
260
+ p5 = b5.build(GenerationRequest(prompt="Analyze code"))
261
+ assert p5["thinking"] == {"type": "enabled", "budget_tokens": 2048, "display": "summarized"}
262
+ assert p5["max_tokens"] > 2048
263
+
264
+ # 6. Budget tokens with summary="none" -> display: "omitted"
265
+ cfg6 = ThinkingConfig(budget_tokens=2048, summary="none")
266
+ b6 = AnthropicPayloadBuilder(model="claude-3-7-sonnet", max_tokens=4096, thinking=cfg6)
267
+ p6 = b6.build(GenerationRequest(prompt="Deep benchmark"))
268
+ assert p6["thinking"] == {"type": "enabled", "budget_tokens": 2048, "display": "omitted"}
269
+
270
+
271
+ def test_anthropic_builder_thought_block_normalization_cross_provider() -> None:
272
+ from evidor.agent.providers.anthropic.builder import AnthropicPayloadBuilder
273
+
274
+ builder = AnthropicPayloadBuilder(model="claude-3-7-sonnet", max_tokens=2048)
275
+
276
+ # Gemini-style thought block with thought_signature and boolean thought flag
277
+ gem_block = {
278
+ "type": "thought",
279
+ "text": "Gemini chain-of-thought",
280
+ "thought": True,
281
+ "thought_signature": "gem_sig_789",
282
+ "extra_gemini_field": "ignore_me",
283
+ }
284
+ norm_gem = builder._normalize_thought_block(gem_block)
285
+ assert norm_gem == {
286
+ "type": "thinking",
287
+ "thinking": "Gemini chain-of-thought",
288
+ "signature": "gem_sig_789",
289
+ }
290
+ assert "extra_gemini_field" not in norm_gem
291
+ assert "thought" not in norm_gem
292
+
293
+ # OpenAI-style reasoning block with summary
294
+ oai_block = {
295
+ "type": "reasoning",
296
+ "summary": "OpenAI reasoning summary",
297
+ "signature": "oai_sig_321",
298
+ }
299
+ norm_oai = builder._normalize_thought_block(oai_block)
300
+ assert norm_oai == {
301
+ "type": "thinking",
302
+ "thinking": "OpenAI reasoning summary",
303
+ "signature": "oai_sig_321",
304
+ }
305
+
306
+ # Anthropic omitted block (empty thinking)
307
+ omitted_block = {
308
+ "type": "thinking",
309
+ "thinking": "",
310
+ "signature": "omitted_sig_999",
311
+ }
312
+ norm_omit = builder._normalize_thought_block(omitted_block)
313
+ assert norm_omit == {
314
+ "type": "thinking",
315
+ "thinking": "",
316
+ "signature": "omitted_sig_999",
317
+ }
318
+
319
+ # Redacted thinking block
320
+ redacted_block = {
321
+ "type": "redacted_thinking",
322
+ "data": "encrypted_thought_data",
323
+ }
324
+ norm_redacted = builder._normalize_thought_block(redacted_block)
325
+ assert norm_redacted == {
326
+ "type": "redacted_thinking",
327
+ "data": "encrypted_thought_data",
328
+ }
329
+
330
+ # Replay assistant message with normalized blocks and fallback for empty content
331
+ req = GenerationRequest(
332
+ messages=[
333
+ Message(role="user", content="Test"),
334
+ Message(role="assistant", content="", thought_blocks=(gem_block,)),
335
+ ]
336
+ )
337
+ payload = builder.build(req)
338
+ asst_content = payload["messages"][1]["content"]
339
+ assert len(asst_content) == 1
340
+ assert asst_content[0]["type"] == "thinking"
341
+ assert asst_content[0]["signature"] == "gem_sig_789"
342
+
343
+
344
+ def test_anthropic_parser_reasoning_summary_and_omitted() -> None:
345
+ from evidor.agent.providers.anthropic.parser import AnthropicResponseParser
346
+
347
+ parser = AnthropicResponseParser(model="claude-3-7-sonnet")
348
+
349
+ # 1. Summarized thinking block: thinking has text
350
+ resp_sum = {
351
+ "id": "msg_sum_01",
352
+ "content": [
353
+ {"type": "thinking", "summary": "Core algorithm design steps", "signature": "sig_sum_1"},
354
+ {"type": "text", "text": "Here is the implementation"},
355
+ ],
356
+ }
357
+ res_sum = parser.parse(resp_sum)
358
+ assert res_sum.thinking == "Core algorithm design steps"
359
+ assert len(res_sum.thought_blocks) == 1
360
+ assert res_sum.thought_blocks[0] == {
361
+ "type": "thinking",
362
+ "thinking": "Core algorithm design steps",
363
+ "signature": "sig_sum_1",
364
+ }
365
+
366
+ # 2. Omitted thinking block: thinking is empty string ""
367
+ resp_omit = {
368
+ "id": "msg_omit_02",
369
+ "content": [
370
+ {"type": "thinking", "thinking": "", "signature": "sig_omit_2"},
371
+ {"type": "text", "text": "Fast response"},
372
+ ],
373
+ }
374
+ res_omit = parser.parse(resp_omit)
375
+ assert res_omit.thinking is None # Omitted thinking text should not expose empty string
376
+ assert len(res_omit.thought_blocks) == 1
377
+ assert res_omit.thought_blocks[0] == {
378
+ "type": "thinking",
379
+ "thinking": "",
380
+ "signature": "sig_omit_2",
381
+ }
382
+
383
+
384
+ def test_anthropic_stream_reasoning_summary_and_signature_accumulation() -> None:
385
+ from evidor.agent.providers.anthropic.stream import AnthropicStreamHandler
386
+
387
+ handler = AnthropicStreamHandler(model="claude-3-7-sonnet")
388
+
389
+ events = [
390
+ {"type": "message_start", "message": {"id": "msg_str_01", "model": "claude-3-7-sonnet"}},
391
+ {"type": "content_block_start", "index": 0, "content_block": {"type": "thinking", "thinking": ""}},
392
+ {"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_delta", "thinking": "First reasoning step. "}},
393
+ {"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_summary_delta", "summary": "Second reasoning step."}},
394
+ {"type": "content_block_delta", "index": 0, "delta": {"type": "signature_delta", "signature": "sig_part1_"}},
395
+ {"type": "content_block_delta", "index": 0, "delta": {"type": "signature_delta", "signature": "sig_part2"}},
396
+ {"type": "content_block_stop", "index": 0},
397
+ {"type": "content_block_start", "index": 1, "content_block": {"type": "text", "text": ""}},
398
+ {"type": "content_block_delta", "index": 1, "delta": {"type": "text_delta", "text": "Final answer."}},
399
+ {"type": "content_block_stop", "index": 1},
400
+ {"type": "message_stop"},
401
+ ]
402
+
403
+ chunks = []
404
+ for ev in events:
405
+ chunks.extend(list(handler.process_event(ev)))
406
+
407
+ finished_chunk = next(c for c in chunks if c.is_finished)
408
+ final_resp = finished_chunk.response
409
+ assert final_resp is not None
410
+ assert final_resp.text == "Final answer."
411
+ assert final_resp.thinking == "First reasoning step. Second reasoning step."
412
+ assert len(final_resp.thought_blocks) == 1
413
+ assert final_resp.thought_blocks[0] == {
414
+ "type": "thinking",
415
+ "thinking": "First reasoning step. Second reasoning step.",
416
+ "signature": "sig_part1_sig_part2",
417
+ }
418
+
419
+
227
420
  # ==============================================================================
228
421
  # GeminiProvider Thinking Tests
229
422
  # ==============================================================================
File without changes
File without changes
File without changes