evidor 1.10.7.dev1__tar.gz → 1.11.0.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 (103) hide show
  1. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/CHANGELOG.md +11 -0
  2. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/PKG-INFO +52 -32
  3. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/README.md +51 -31
  4. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/pyproject.toml +1 -1
  5. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/__init__.py +2 -6
  6. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/agent/providers/anthropic/builder.py +24 -14
  7. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/agent/providers/anthropic/provider.py +8 -15
  8. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/agent/providers/gemini/builder.py +19 -16
  9. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/agent/providers/openai/builder.py +21 -10
  10. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/agent/providers/openai/provider.py +1 -13
  11. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/models/__init__.py +2 -6
  12. evidor-1.11.0.dev1/src/evidor/models/thinking.py +51 -0
  13. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/tests/test_providers.py +18 -22
  14. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/tests/test_thinking.py +57 -108
  15. evidor-1.10.7.dev1/src/evidor/models/thinking.py +0 -102
  16. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/.github/workflows/release.yml +0 -0
  17. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/.gitignore +0 -0
  18. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/AGENTS.md +0 -0
  19. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/CODE_OF_CONDUCT.md +0 -0
  20. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/LICENSE +0 -0
  21. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/requirements.txt +0 -0
  22. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/agent/__init__.py +0 -0
  23. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/agent/core/__init__.py +0 -0
  24. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/agent/core/agent.py +0 -0
  25. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/agent/core/context.py +0 -0
  26. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/agent/core/runner.py +0 -0
  27. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/agent/core/telemetry_bridge.py +0 -0
  28. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/agent/providers/__init__.py +0 -0
  29. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/agent/providers/anthropic/__init__.py +0 -0
  30. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/agent/providers/anthropic/parser.py +0 -0
  31. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/agent/providers/anthropic/stream.py +0 -0
  32. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/agent/providers/base.py +0 -0
  33. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/agent/providers/client_pool.py +0 -0
  34. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/agent/providers/gemini/__init__.py +0 -0
  35. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/agent/providers/gemini/parser.py +0 -0
  36. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/agent/providers/gemini/provider.py +0 -0
  37. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/agent/providers/gemini/stream.py +0 -0
  38. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/agent/providers/openai/__init__.py +0 -0
  39. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/agent/providers/openai/parser.py +0 -0
  40. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/agent/providers/openai/stream.py +0 -0
  41. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/constants.py +0 -0
  42. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/errors.py +0 -0
  43. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/mcp/__init__.py +0 -0
  44. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/mcp/client.py +0 -0
  45. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/mcp/config.py +0 -0
  46. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/mcp/runner.py +0 -0
  47. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/mcp/session.py +0 -0
  48. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/models/generation.py +0 -0
  49. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/models/streaming.py +0 -0
  50. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/telemetry/__init__.py +0 -0
  51. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/telemetry/adapters/__init__.py +0 -0
  52. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/telemetry/adapters/langfuse.py +0 -0
  53. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/telemetry/adapters/opentelemetry.py +0 -0
  54. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/telemetry/adapters/phoenix.py +0 -0
  55. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/telemetry/adapters/prometheus.py +0 -0
  56. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/telemetry/context.py +0 -0
  57. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/telemetry/events.py +0 -0
  58. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/telemetry/runtime.py +0 -0
  59. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/telemetry/semconv.py +0 -0
  60. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/telemetry/sink.py +0 -0
  61. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/tools/__init__.py +0 -0
  62. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/tools/core/__init__.py +0 -0
  63. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/tools/core/tool_executor.py +0 -0
  64. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/tools/core/tools.py +0 -0
  65. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/tools/core/utils.py +0 -0
  66. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/tools/native/__init__.py +0 -0
  67. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/tools/native/calculator.py +0 -0
  68. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/tools/native/evidence_store.py +0 -0
  69. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/tools/native/filesystem/__init__.py +0 -0
  70. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/tools/native/filesystem/_common.py +0 -0
  71. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/tools/native/filesystem/list_files.py +0 -0
  72. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/tools/native/filesystem/mutate_files.py +0 -0
  73. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/tools/native/filesystem/read_file.py +0 -0
  74. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/tools/native/filesystem/search_files.py +0 -0
  75. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/tools/native/get_current_time.py +0 -0
  76. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/tools/native/read_webpage.py +0 -0
  77. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/tools/native/web_search/__init__.py +0 -0
  78. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/tools/native/web_search/_base.py +0 -0
  79. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/tools/native/web_search/http.py +0 -0
  80. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/tools/native/web_search/providers/__init__.py +0 -0
  81. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/tools/native/web_search/providers/brave.py +0 -0
  82. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/tools/native/web_search/providers/exa.py +0 -0
  83. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/tools/native/web_search/providers/tavily.py +0 -0
  84. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/tools/native/web_search/tool.py +0 -0
  85. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/utils/__init__.py +0 -0
  86. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/utils/execution.py +0 -0
  87. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/src/evidor/utils/schema.py +0 -0
  88. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/tests/test_agent.py +0 -0
  89. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/tests/test_builtin_tools.py +0 -0
  90. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/tests/test_conversation.py +0 -0
  91. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/tests/test_filesystem_tools.py +0 -0
  92. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/tests/test_langfuse_adapter.py +0 -0
  93. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/tests/test_mcp.py +0 -0
  94. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/tests/test_models.py +0 -0
  95. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/tests/test_native_tools.py +0 -0
  96. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/tests/test_opentelemetry_adapter.py +0 -0
  97. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/tests/test_phoenix_adapter.py +0 -0
  98. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/tests/test_prometheus_adapter.py +0 -0
  99. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/tests/test_streaming.py +0 -0
  100. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/tests/test_telemetry_events.py +0 -0
  101. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/tests/test_telemetry_integration.py +0 -0
  102. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/tests/test_tools.py +0 -0
  103. {evidor-1.10.7.dev1 → evidor-1.11.0.dev1}/tests/test_web_search.py +0 -0
@@ -2,6 +2,17 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v1.11.0-dev.1 (2026-11-03)
6
+
7
+ ### Features
8
+
9
+ - **agent**: Centralized thinking configs
10
+ ([`4a05c49`](https://github.com/vedangiitb/evidor-core/commit/4a05c49bbae3852a10d9597c6c27c1d9cdd335af))
11
+
12
+
13
+ ## v1.10.7 (2026-11-02)
14
+
15
+
5
16
  ## v1.10.7-dev.1 (2026-11-02)
6
17
 
7
18
  ### Bug Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: evidor
3
- Version: 1.10.7-dev.1
3
+ Version: 1.11.0-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
@@ -443,7 +443,7 @@ For reasoning models (**OpenAI** `gpt-6-astra`, **Anthropic** `claude-3-7-sonnet
443
443
  ```python
444
444
  from evidor import Agent, AnthropicProvider
445
445
 
446
- agent = Agent(AnthropicProvider(model="claude-3-7-sonnet-20250219", thinking=2048))
446
+ agent = Agent(AnthropicProvider(model="claude-3-7-sonnet-20250219", thinking="high"))
447
447
  stream = agent.send("Solve: Which is greater, 9.11 or 9.9?", stream=True)
448
448
 
449
449
  for chunk in stream:
@@ -517,59 +517,79 @@ Evidor delegates retry handling directly to the underlying model provider SDKs (
517
517
 
518
518
  ### Configurable Thinking & Reasoning Mode
519
519
 
520
- Evidor provides first-class support for reasoning models across **OpenAI** (`gpt-6-astra`, `gpt-6.1-sol`, `gpt-6-luna`), **Anthropic** (`claude-3-7-sonnet-20250219`), and **Gemini** (`gemini-3.8-flash`, `gemini-3.8-live-extended-thinking`, `gemini-3.1-pro-preview`). Thinking can be configured either using provider-specific typed configuration classes or natural shorthands.
520
+ Evidor provides a centralized, provider-neutral `ThinkingConfig` for reasoning models across **OpenAI** (`gpt-6-astra`, `gpt-6.1-sol`, `gpt-6-luna`), **Anthropic** (`claude-3-7-sonnet-20250219`), and **Gemini** (`gemini-3.8-flash`, `gemini-3.8-live-extended-thinking`, `gemini-3.1-pro-preview`).
521
+
522
+ Instead of navigating provider-specific configuration differences, you configure thinking through **two simple, unified modes**:
523
+
524
+ - **Mode 1: Shorthand Effort String** (e.g. `thinking="high"`, `thinking="medium"`, `thinking="low"`): Works universally across all providers. The effort level string is passed directly through to the underlying provider without client-side mapping or restrictions.
525
+ - **Mode 2: Centralized `ThinkingConfig` Object**: When you need granular control over reasoning effort, token budget, or summaries.
521
526
 
522
527
  ```python
523
528
  from evidor import (
524
529
  Agent,
525
530
  AnthropicProvider,
526
- AnthropicThinkingConfig,
527
531
  GeminiProvider,
528
- GeminiThinkingConfig,
529
532
  OpenAIProvider,
530
- OpenAIThinkingConfig,
533
+ ThinkingConfig,
531
534
  )
532
535
 
533
- # 1. OpenAI: Configure reasoning effort ('low', 'medium', 'high', 'xhigh', 'max')
534
- # Via shorthand:
535
- provider = OpenAIProvider(model="gpt-6-astra", thinking="high")
536
- # Or via typed config:
537
- provider = OpenAIProvider(model="gpt-6-astra", thinking=OpenAIThinkingConfig(effort="high"))
538
-
539
- # 2. Anthropic: Configure token budget or adaptive effort
540
- # Shorthand int (sets budget_tokens >= 1024; auto-bumps max_tokens if needed):
541
- provider = AnthropicProvider(model="claude-3-7-sonnet-20250219", thinking=4096)
542
- # Shorthand string (adaptive thinking):
543
- provider = AnthropicProvider(model="claude-3-7-sonnet-20250219", thinking="high")
544
- # Or via typed config:
545
- provider = AnthropicProvider(
546
- model="claude-3-7-sonnet-20250219",
547
- thinking=AnthropicThinkingConfig(budget_tokens=4096),
536
+ # ----------------------------------------------------------------------
537
+ # Mode 1: Direct Effort String (universal across providers)
538
+ # ----------------------------------------------------------------------
539
+ # OpenAI (sets reasoning.effort):
540
+ p_openai = OpenAIProvider(model="gpt-6-astra", thinking="high")
541
+
542
+ # Anthropic (sets adaptive thinking with effort):
543
+ p_anthropic = AnthropicProvider(model="claude-3-7-sonnet-20250219", thinking="high")
544
+
545
+ # Gemini (sets generation_config.thinking_level):
546
+ p_gemini = GeminiProvider(model="gemini-3.8-flash", thinking="high")
547
+
548
+ # ----------------------------------------------------------------------
549
+ # Mode 2: Centralized ThinkingConfig Object
550
+ # ----------------------------------------------------------------------
551
+ # 1. OpenAI: Configure effort and reasoning summary
552
+ p_openai_cfg = OpenAIProvider(
553
+ model="gpt-6-astra",
554
+ thinking=ThinkingConfig(effort="high", summary="auto"),
548
555
  )
549
556
 
550
- # 3. Gemini: 2 modes for thinking configuration
551
- # Mode 1: Thinking level string ('minimal', 'low', 'medium', 'high'):
552
- provider = GeminiProvider(model="gemini-3.8-flash", thinking="high")
557
+ # 2. Anthropic: Configure extended budget tokens (budget_tokens >= 1024; auto-bumps max_tokens if needed)
558
+ p_anthropic_cfg = AnthropicProvider(
559
+ model="claude-3-7-sonnet-20250219",
560
+ thinking=ThinkingConfig(budget_tokens=4096),
561
+ )
553
562
 
554
- # Mode 2: Object supporting thinking_level, thinking_summaries ('auto', 'none'), or thinking_budget:
555
- provider = GeminiProvider(
563
+ # 3. Gemini: Configure thinking level and summaries (or thinking budget for earlier models)
564
+ p_gemini_cfg = GeminiProvider(
556
565
  model="gemini-3.8-flash",
557
- thinking=GeminiThinkingConfig(thinking_level="high", thinking_summaries="auto"),
566
+ thinking=ThinkingConfig(effort="medium", summary="auto"),
558
567
  )
559
568
 
560
- # Earlier models supporting numeric thinking_budget can also be configured via GeminiThinkingConfig:
561
- provider = GeminiProvider(
569
+ # Earlier models supporting numeric thinking budget:
570
+ p_gemini_budget = GeminiProvider(
562
571
  model="gemini-2.5-flash",
563
- thinking=GeminiThinkingConfig(thinking_budget=2048),
572
+ thinking=ThinkingConfig(budget_tokens=2048),
564
573
  )
565
574
  ```
566
575
 
576
+ #### Centralized `ThinkingConfig` Reference
577
+
578
+ | Field | Type | Description & Provider Mapping |
579
+ | :--- | :--- | :--- |
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. |
581
+ | `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"`. |
583
+
584
+ > [!NOTE]
585
+ > **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)`.
586
+
567
587
  #### Inspecting Thinking Steps & Reasoning Tokens
568
588
 
569
589
  When thinking is enabled and the provider outputs thought tokens, the response provides both the visible text and the internal reasoning scratchpad:
570
590
 
571
591
  ```python
572
- agent = Agent(AnthropicProvider(model="claude-3-7-sonnet-20250219", thinking=2048))
592
+ agent = Agent(AnthropicProvider(model="claude-3-7-sonnet-20250219", thinking="high"))
573
593
  response = agent.send("Solve this logic riddle step-by-step...")
574
594
 
575
595
  # Final visible response
@@ -1186,7 +1206,7 @@ phoenix_sink = PhoenixSink(capture_content=True, capture_thinking=True)
1186
1206
  langfuse_sink = LangfuseSink(capture_content=True, capture_thinking=True)
1187
1207
 
1188
1208
  agent = Agent(
1189
- AnthropicProvider(model="claude-3-7-sonnet-20250219", thinking=2048),
1209
+ AnthropicProvider(model="claude-3-7-sonnet-20250219", thinking="high"),
1190
1210
  telemetry=[console_sink, phoenix_sink],
1191
1211
  )
1192
1212
  response = agent.send("Analyze system security risks...")
@@ -356,7 +356,7 @@ For reasoning models (**OpenAI** `gpt-6-astra`, **Anthropic** `claude-3-7-sonnet
356
356
  ```python
357
357
  from evidor import Agent, AnthropicProvider
358
358
 
359
- agent = Agent(AnthropicProvider(model="claude-3-7-sonnet-20250219", thinking=2048))
359
+ agent = Agent(AnthropicProvider(model="claude-3-7-sonnet-20250219", thinking="high"))
360
360
  stream = agent.send("Solve: Which is greater, 9.11 or 9.9?", stream=True)
361
361
 
362
362
  for chunk in stream:
@@ -430,59 +430,79 @@ Evidor delegates retry handling directly to the underlying model provider SDKs (
430
430
 
431
431
  ### Configurable Thinking & Reasoning Mode
432
432
 
433
- Evidor provides first-class support for reasoning models across **OpenAI** (`gpt-6-astra`, `gpt-6.1-sol`, `gpt-6-luna`), **Anthropic** (`claude-3-7-sonnet-20250219`), and **Gemini** (`gemini-3.8-flash`, `gemini-3.8-live-extended-thinking`, `gemini-3.1-pro-preview`). Thinking can be configured either using provider-specific typed configuration classes or natural shorthands.
433
+ Evidor provides a centralized, provider-neutral `ThinkingConfig` for reasoning models across **OpenAI** (`gpt-6-astra`, `gpt-6.1-sol`, `gpt-6-luna`), **Anthropic** (`claude-3-7-sonnet-20250219`), and **Gemini** (`gemini-3.8-flash`, `gemini-3.8-live-extended-thinking`, `gemini-3.1-pro-preview`).
434
+
435
+ Instead of navigating provider-specific configuration differences, you configure thinking through **two simple, unified modes**:
436
+
437
+ - **Mode 1: Shorthand Effort String** (e.g. `thinking="high"`, `thinking="medium"`, `thinking="low"`): Works universally across all providers. The effort level string is passed directly through to the underlying provider without client-side mapping or restrictions.
438
+ - **Mode 2: Centralized `ThinkingConfig` Object**: When you need granular control over reasoning effort, token budget, or summaries.
434
439
 
435
440
  ```python
436
441
  from evidor import (
437
442
  Agent,
438
443
  AnthropicProvider,
439
- AnthropicThinkingConfig,
440
444
  GeminiProvider,
441
- GeminiThinkingConfig,
442
445
  OpenAIProvider,
443
- OpenAIThinkingConfig,
446
+ ThinkingConfig,
444
447
  )
445
448
 
446
- # 1. OpenAI: Configure reasoning effort ('low', 'medium', 'high', 'xhigh', 'max')
447
- # Via shorthand:
448
- provider = OpenAIProvider(model="gpt-6-astra", thinking="high")
449
- # Or via typed config:
450
- provider = OpenAIProvider(model="gpt-6-astra", thinking=OpenAIThinkingConfig(effort="high"))
451
-
452
- # 2. Anthropic: Configure token budget or adaptive effort
453
- # Shorthand int (sets budget_tokens >= 1024; auto-bumps max_tokens if needed):
454
- provider = AnthropicProvider(model="claude-3-7-sonnet-20250219", thinking=4096)
455
- # Shorthand string (adaptive thinking):
456
- provider = AnthropicProvider(model="claude-3-7-sonnet-20250219", thinking="high")
457
- # Or via typed config:
458
- provider = AnthropicProvider(
459
- model="claude-3-7-sonnet-20250219",
460
- thinking=AnthropicThinkingConfig(budget_tokens=4096),
449
+ # ----------------------------------------------------------------------
450
+ # Mode 1: Direct Effort String (universal across providers)
451
+ # ----------------------------------------------------------------------
452
+ # OpenAI (sets reasoning.effort):
453
+ p_openai = OpenAIProvider(model="gpt-6-astra", thinking="high")
454
+
455
+ # Anthropic (sets adaptive thinking with effort):
456
+ p_anthropic = AnthropicProvider(model="claude-3-7-sonnet-20250219", thinking="high")
457
+
458
+ # Gemini (sets generation_config.thinking_level):
459
+ p_gemini = GeminiProvider(model="gemini-3.8-flash", thinking="high")
460
+
461
+ # ----------------------------------------------------------------------
462
+ # Mode 2: Centralized ThinkingConfig Object
463
+ # ----------------------------------------------------------------------
464
+ # 1. OpenAI: Configure effort and reasoning summary
465
+ p_openai_cfg = OpenAIProvider(
466
+ model="gpt-6-astra",
467
+ thinking=ThinkingConfig(effort="high", summary="auto"),
461
468
  )
462
469
 
463
- # 3. Gemini: 2 modes for thinking configuration
464
- # Mode 1: Thinking level string ('minimal', 'low', 'medium', 'high'):
465
- provider = GeminiProvider(model="gemini-3.8-flash", thinking="high")
470
+ # 2. Anthropic: Configure extended budget tokens (budget_tokens >= 1024; auto-bumps max_tokens if needed)
471
+ p_anthropic_cfg = AnthropicProvider(
472
+ model="claude-3-7-sonnet-20250219",
473
+ thinking=ThinkingConfig(budget_tokens=4096),
474
+ )
466
475
 
467
- # Mode 2: Object supporting thinking_level, thinking_summaries ('auto', 'none'), or thinking_budget:
468
- provider = GeminiProvider(
476
+ # 3. Gemini: Configure thinking level and summaries (or thinking budget for earlier models)
477
+ p_gemini_cfg = GeminiProvider(
469
478
  model="gemini-3.8-flash",
470
- thinking=GeminiThinkingConfig(thinking_level="high", thinking_summaries="auto"),
479
+ thinking=ThinkingConfig(effort="medium", summary="auto"),
471
480
  )
472
481
 
473
- # Earlier models supporting numeric thinking_budget can also be configured via GeminiThinkingConfig:
474
- provider = GeminiProvider(
482
+ # Earlier models supporting numeric thinking budget:
483
+ p_gemini_budget = GeminiProvider(
475
484
  model="gemini-2.5-flash",
476
- thinking=GeminiThinkingConfig(thinking_budget=2048),
485
+ thinking=ThinkingConfig(budget_tokens=2048),
477
486
  )
478
487
  ```
479
488
 
489
+ #### Centralized `ThinkingConfig` Reference
490
+
491
+ | Field | Type | Description & Provider Mapping |
492
+ | :--- | :--- | :--- |
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. |
494
+ | `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"`. |
496
+
497
+ > [!NOTE]
498
+ > **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)`.
499
+
480
500
  #### Inspecting Thinking Steps & Reasoning Tokens
481
501
 
482
502
  When thinking is enabled and the provider outputs thought tokens, the response provides both the visible text and the internal reasoning scratchpad:
483
503
 
484
504
  ```python
485
- agent = Agent(AnthropicProvider(model="claude-3-7-sonnet-20250219", thinking=2048))
505
+ agent = Agent(AnthropicProvider(model="claude-3-7-sonnet-20250219", thinking="high"))
486
506
  response = agent.send("Solve this logic riddle step-by-step...")
487
507
 
488
508
  # Final visible response
@@ -1099,7 +1119,7 @@ phoenix_sink = PhoenixSink(capture_content=True, capture_thinking=True)
1099
1119
  langfuse_sink = LangfuseSink(capture_content=True, capture_thinking=True)
1100
1120
 
1101
1121
  agent = Agent(
1102
- AnthropicProvider(model="claude-3-7-sonnet-20250219", thinking=2048),
1122
+ AnthropicProvider(model="claude-3-7-sonnet-20250219", thinking="high"),
1103
1123
  telemetry=[console_sink, phoenix_sink],
1104
1124
  )
1105
1125
  response = agent.send("Analyze system security risks...")
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "evidor"
7
- version = "1.10.7-dev.1"
7
+ version = "1.11.0-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"
@@ -13,18 +13,16 @@ from .mcp import (
13
13
  )
14
14
  from .models import (
15
15
  AnyThinkingConfig,
16
- AnthropicThinkingConfig,
17
16
  AsyncStreamResponse,
18
- GeminiThinkingConfig,
19
17
  GenerationRequest,
20
18
  GenerationResponse,
21
19
  Message,
22
- OpenAIThinkingConfig,
23
20
  SearchResult,
24
21
  StreamChunk,
25
22
  StreamResponse,
26
23
  ThinkingConfig,
27
24
  ThinkingEffort,
25
+ ThinkingSummary,
28
26
  ToolCall,
29
27
  )
30
28
 
@@ -77,7 +75,6 @@ from .tools import (
77
75
  __all__ = [
78
76
  "Agent",
79
77
  "AnthropicProvider",
80
- "AnthropicThinkingConfig",
81
78
  "AnyThinkingConfig",
82
79
  "AsyncStreamResponse",
83
80
  "BraveSearchProvider",
@@ -91,7 +88,6 @@ __all__ = [
91
88
  "ExaSearchProvider",
92
89
  "fetch_webpage_content",
93
90
  "GeminiProvider",
94
- "GeminiThinkingConfig",
95
91
  "GenerationRequest",
96
92
  "GenerationResponse",
97
93
  "get_current_time",
@@ -105,7 +101,6 @@ __all__ = [
105
101
  "Message",
106
102
  "ModelProvider",
107
103
  "OpenAIProvider",
108
- "OpenAIThinkingConfig",
109
104
  "OpenTelemetrySink",
110
105
  "PhoenixSink",
111
106
  "PrometheusSink",
@@ -121,6 +116,7 @@ __all__ = [
121
116
  "TelemetrySink",
122
117
  "ThinkingConfig",
123
118
  "ThinkingEffort",
119
+ "ThinkingSummary",
124
120
  "Tool",
125
121
  "ToolCall",
126
122
  "TraceContext",
@@ -27,7 +27,7 @@ class AnthropicPayloadBuilder:
27
27
  self.compact_threshold = compact_threshold
28
28
  self.compact_instructions = compact_instructions
29
29
 
30
- def build(self, request: GenerationRequest) -> dict[str, Any]:
30
+ def build(self, request: GenerationRequest, thinking: Any = None) -> dict[str, Any]:
31
31
  """Construct the full request dictionary for Anthropic Messages API."""
32
32
  system_prompt = extract_system_prompt(request)
33
33
  formatted_messages = self.format_messages(request.messages)
@@ -48,22 +48,32 @@ class AnthropicPayloadBuilder:
48
48
  payload["betas"] = ["compact-2026-01-12"]
49
49
  payload["context_management"] = {"edits": [edit]}
50
50
 
51
- if self.thinking and self.thinking.enabled:
52
- if getattr(self.thinking, "type", "enabled") == "adaptive":
53
- payload["thinking"] = {"type": "adaptive"}
54
- if getattr(self.thinking, "effort", None):
55
- payload["output_config"] = {"effort": self.thinking.effort}
56
- else:
57
- budget = getattr(self.thinking, "budget_tokens", None)
58
- if budget is None or budget < 1024:
59
- budget = 1024
51
+ active_thinking = thinking if thinking is not None else self.thinking
52
+ if active_thinking and isinstance(active_thinking, str):
53
+ payload["thinking"] = {"type": "adaptive"}
54
+ payload["output_config"] = {"effort": active_thinking}
55
+ elif active_thinking and not isinstance(active_thinking, (bool, int)):
56
+ budget = (
57
+ active_thinking.get("budget_tokens")
58
+ if isinstance(active_thinking, dict)
59
+ else getattr(active_thinking, "budget_tokens", None)
60
+ )
61
+ effort = (
62
+ active_thinking.get("effort")
63
+ if isinstance(active_thinking, dict)
64
+ else getattr(active_thinking, "effort", None)
65
+ )
66
+ if budget is not None and not isinstance(budget, bool) and budget > 0:
67
+ actual_budget = max(budget, 1024)
60
68
  payload["thinking"] = {
61
69
  "type": "enabled",
62
- "budget_tokens": budget,
70
+ "budget_tokens": actual_budget,
63
71
  }
64
- # Anthropic Messages API requires max_tokens > budget_tokens
65
- if payload["max_tokens"] <= budget:
66
- payload["max_tokens"] = budget + 1024
72
+ if payload["max_tokens"] <= actual_budget:
73
+ payload["max_tokens"] = actual_budget + 1024
74
+ elif isinstance(effort, str):
75
+ payload["thinking"] = {"type": "adaptive"}
76
+ payload["output_config"] = {"effort": effort}
67
77
 
68
78
  if self.enable_prompt_caching:
69
79
  payload["cache_control"] = {"type": "ephemeral"}
@@ -47,19 +47,7 @@ class AnthropicProvider(BaseProvider):
47
47
  compact_instructions: str | None = None,
48
48
  base_url: str | None = None,
49
49
  ) -> None:
50
- if thinking is not None:
51
- from evidor.models import AnthropicThinkingConfig
52
-
53
- thinking_cfg: AnthropicThinkingConfig | None = (
54
- thinking
55
- if isinstance(thinking, AnthropicThinkingConfig)
56
- else AnthropicThinkingConfig.from_value(thinking)
57
- )
58
- thinking_cfg.validate()
59
- else:
60
- thinking_cfg = None
61
-
62
- super().__init__(model=model, api_key=api_key, thinking=thinking_cfg)
50
+ super().__init__(model=model, api_key=api_key, thinking=thinking)
63
51
  self._enable_prompt_caching = enable_prompt_caching
64
52
  self._compact_threshold = compact_threshold
65
53
  self._compact_instructions = compact_instructions
@@ -69,8 +57,13 @@ class AnthropicProvider(BaseProvider):
69
57
  self._async_clients: dict[asyncio.AbstractEventLoop, Any] = {}
70
58
 
71
59
  if max_tokens is None:
72
- if self._thinking and self._thinking.enabled and self._thinking.budget_tokens:
73
- max_tokens = self._thinking.budget_tokens + 2048
60
+ budget = (
61
+ self._thinking.get("budget_tokens")
62
+ if isinstance(self._thinking, dict)
63
+ else getattr(self._thinking, "budget_tokens", None)
64
+ )
65
+ if budget is not None and not isinstance(budget, bool) and budget > 0:
66
+ max_tokens = budget + 2048
74
67
  else:
75
68
  max_tokens = 1024
76
69
  self._max_tokens = max_tokens
@@ -175,31 +175,34 @@ class GeminiPayloadBuilder:
175
175
  active_thinking = thinking if thinking is not None else self.thinking
176
176
  if active_thinking and isinstance(active_thinking, str):
177
177
  payload["generation_config"] = {"thinking_level": active_thinking}
178
- elif active_thinking and not isinstance(active_thinking, bool):
178
+ elif active_thinking and not isinstance(active_thinking, (bool, int)):
179
179
  gen_config: dict[str, Any] = {}
180
- thinking_level = (
181
- active_thinking.get("thinking_level")
180
+ effort = (
181
+ active_thinking.get("effort") or active_thinking.get("thinking_level")
182
182
  if isinstance(active_thinking, dict)
183
- else getattr(active_thinking, "thinking_level", None)
183
+ else getattr(active_thinking, "effort", None) or getattr(active_thinking, "thinking_level", None)
184
184
  )
185
- if isinstance(thinking_level, str):
186
- gen_config["thinking_level"] = thinking_level
185
+ if isinstance(effort, str):
186
+ gen_config["thinking_level"] = effort
187
187
 
188
- thinking_summaries = (
189
- active_thinking.get("thinking_summaries")
188
+ summary = (
189
+ active_thinking.get("summary") or active_thinking.get("thinking_summaries")
190
190
  if isinstance(active_thinking, dict)
191
- else getattr(active_thinking, "thinking_summaries", None)
191
+ else getattr(active_thinking, "summary", None) or getattr(active_thinking, "thinking_summaries", None)
192
192
  )
193
- if isinstance(thinking_summaries, str):
194
- gen_config["thinking_summaries"] = thinking_summaries
193
+ if isinstance(summary, str):
194
+ if summary.lower() == "none":
195
+ gen_config["thinking_summaries"] = "none"
196
+ else:
197
+ gen_config["thinking_summaries"] = "auto"
195
198
 
196
- thinking_budget = (
197
- active_thinking.get("thinking_budget")
199
+ budget = (
200
+ active_thinking.get("budget_tokens") or active_thinking.get("thinking_budget")
198
201
  if isinstance(active_thinking, dict)
199
- else getattr(active_thinking, "thinking_budget", None)
202
+ else getattr(active_thinking, "budget_tokens", None) or getattr(active_thinking, "thinking_budget", None)
200
203
  )
201
- if thinking_budget is not None and not isinstance(thinking_budget, bool):
202
- gen_config["thinking_budget"] = thinking_budget
204
+ if budget is not None and not isinstance(budget, bool) and budget > 0:
205
+ gen_config["thinking_budget"] = budget
203
206
 
204
207
  if gen_config:
205
208
  payload["generation_config"] = gen_config
@@ -21,7 +21,7 @@ class OpenAIPayloadBuilder:
21
21
  self.thinking = thinking
22
22
  self.compact_threshold = compact_threshold
23
23
 
24
- def build(self, request: GenerationRequest) -> dict[str, Any]:
24
+ def build(self, request: GenerationRequest, thinking: Any = None) -> dict[str, Any]:
25
25
  """Construct the full request dictionary for OpenAI Responses API."""
26
26
  payload: dict[str, Any] = {
27
27
  "model": self.model,
@@ -42,16 +42,27 @@ class OpenAIPayloadBuilder:
42
42
  {"type": "compaction", "compact_threshold": self.compact_threshold}
43
43
  ]
44
44
 
45
- if self.thinking is not None:
45
+ active_thinking = thinking if thinking is not None else self.thinking
46
+ if active_thinking and isinstance(active_thinking, str):
47
+ payload["reasoning"] = {"effort": active_thinking}
48
+ elif active_thinking and not isinstance(active_thinking, (bool, int)):
46
49
  reasoning_config: dict[str, Any] = {}
47
- if self.thinking.enabled:
48
- if self.thinking.effort:
49
- reasoning_config["effort"] = self.thinking.effort
50
- summary_val = getattr(self.thinking, "summary", None)
51
- if summary_val:
52
- reasoning_config["summary"] = summary_val
53
- else:
54
- reasoning_config["effort"] = "none"
50
+ effort = (
51
+ active_thinking.get("effort")
52
+ if isinstance(active_thinking, dict)
53
+ else getattr(active_thinking, "effort", None)
54
+ )
55
+ if isinstance(effort, str):
56
+ reasoning_config["effort"] = effort
57
+
58
+ summary_val = (
59
+ active_thinking.get("summary")
60
+ if isinstance(active_thinking, dict)
61
+ else getattr(active_thinking, "summary", None)
62
+ )
63
+ if isinstance(summary_val, str) and summary_val.lower() != "none":
64
+ reasoning_config["summary"] = summary_val
65
+
55
66
  if reasoning_config:
56
67
  payload["reasoning"] = reasoning_config
57
68
 
@@ -33,19 +33,7 @@ class OpenAIProvider(BaseProvider):
33
33
  compact_threshold: int | None = None,
34
34
  base_url: str | None = None,
35
35
  ) -> None:
36
- if thinking is not None:
37
- from evidor.models import OpenAIThinkingConfig
38
-
39
- thinking_cfg: OpenAIThinkingConfig | None = (
40
- thinking
41
- if isinstance(thinking, OpenAIThinkingConfig)
42
- else OpenAIThinkingConfig.from_value(thinking)
43
- )
44
- thinking_cfg.validate()
45
- else:
46
- thinking_cfg = None
47
-
48
- super().__init__(model=model, api_key=api_key, thinking=thinking_cfg)
36
+ super().__init__(model=model, api_key=api_key, thinking=thinking)
49
37
  self._compact_threshold = compact_threshold
50
38
  self._base_url = base_url
51
39
  self._sync_client: Any = None
@@ -14,20 +14,16 @@ from .streaming import (
14
14
  StreamResponse,
15
15
  )
16
16
  from .thinking import (
17
- AnthropicThinkingConfig,
18
17
  AnyThinkingConfig,
19
- GeminiThinkingConfig,
20
- OpenAIThinkingConfig,
21
18
  ThinkingConfig,
22
19
  ThinkingEffort,
20
+ ThinkingSummary,
23
21
  )
24
22
 
25
23
  __all__ = [
26
24
  "ThinkingConfig",
27
25
  "ThinkingEffort",
28
- "OpenAIThinkingConfig",
29
- "AnthropicThinkingConfig",
30
- "GeminiThinkingConfig",
26
+ "ThinkingSummary",
31
27
  "AnyThinkingConfig",
32
28
  "SearchResult",
33
29
  "ToolCall",
@@ -0,0 +1,51 @@
1
+ """Thinking and reasoning configuration for models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any
7
+
8
+ ThinkingEffort = str
9
+ ThinkingSummary = str
10
+
11
+
12
+ @dataclass(frozen=True, slots=True)
13
+ class ThinkingConfig:
14
+ """Centralized, provider-neutral reasoning and thinking configuration.
15
+
16
+ Attributes:
17
+ effort: Desired thinking depth or reasoning level (e.g. 'minimal', 'low', 'medium', 'high', 'xhigh', 'max').
18
+ Passed directly to the provider without client-side mapping or restriction.
19
+ budget_tokens: Token budget for reasoning where supported (e.g. Anthropic extended thinking or earlier Gemini models).
20
+ summary: Reasoning summary exposure (e.g. 'auto', 'concise', 'detailed', 'none').
21
+ """
22
+
23
+ effort: ThinkingEffort | None = None
24
+ budget_tokens: int | None = None
25
+ summary: ThinkingSummary | None = None
26
+
27
+ def validate(self) -> None:
28
+ """Provider-neutral constraint checks."""
29
+ pass
30
+
31
+ @classmethod
32
+ def from_value(cls, value: Any) -> ThinkingConfig:
33
+ """Coerce a value to ThinkingConfig."""
34
+ if isinstance(value, ThinkingConfig):
35
+ return value
36
+ if isinstance(value, str):
37
+ return cls(effort=value)
38
+ if isinstance(value, dict):
39
+ return cls(
40
+ effort=value.get("effort"),
41
+ budget_tokens=value.get("budget_tokens"),
42
+ summary=value.get("summary"),
43
+ )
44
+ raise TypeError(
45
+ f"Cannot coerce {type(value).__name__} to ThinkingConfig. "
46
+ "Thinking must be configured either as an effort string (e.g. thinking='high') "
47
+ "or via ThinkingConfig(effort=..., budget_tokens=..., summary=...)."
48
+ )
49
+
50
+
51
+ AnyThinkingConfig = ThinkingConfig