-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
fix: first-class reasoning effort — provider-native translation + session persistence #4473
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -492,6 +492,14 @@ def __init__( | |
| self.max_reflect = extra_settings.get('max_reflect', 3) | ||
| self.min_reflect = extra_settings.get('min_reflect', 1) | ||
| self.reasoning_steps = extra_settings.get('reasoning_steps', False) | ||
| # Unified, provider-portable reasoning-effort control (Issue #4452). | ||
| # Accepts a graded level (off|minimal|low|medium|high) or a legacy | ||
| # ``thinking_budget`` int; both normalise to one internal value that is | ||
| # translated to each provider's native parameter in | ||
| # ``_build_completion_params``. ``None``/``off`` is a zero-overhead no-op. | ||
| self.reasoning_effort = extra_settings.get( | ||
| 'reasoning_effort', extra_settings.get('thinking_budget') | ||
| ) | ||
| self.metrics = extra_settings.get('metrics', False) | ||
| # Auto-detect XML tool format for known models, or allow manual override | ||
| self.xml_tool_format = extra_settings.get('xml_tool_format', 'auto') | ||
|
|
@@ -5776,10 +5784,24 @@ def _build_completion_params(self, **override_params) -> Dict[str, Any]: | |
| 'max_tool_calls_per_turn', 'parallel_tool_calls', # Tool execution settings | ||
| 'in_loop_compaction', 'clear_threshold_pct', 'compact_threshold_pct', # In-loop context management | ||
| 'keep_recent_tool_results', # In-loop context management | ||
| 'reasoning_effort', 'thinking_budget', # Reasoning-effort (translated below, Issue #4452) | ||
| ] | ||
| for param in internal_params: | ||
| params.pop(param, None) | ||
|
|
||
| # Translate the unified reasoning-effort level to the target provider's | ||
| # native request parameter (Issue #4452): OpenAI/xAI reasoning models get | ||
| # ``reasoning_effort``, Anthropic/Gemini get an extended-thinking budget, | ||
| # and models with no reasoning control are left untouched. A per-call | ||
| # override wins over the instance-level setting; ``off``/unset is a no-op. | ||
| effort = override_params.get('reasoning_effort', self.reasoning_effort) | ||
| if effort is not None: | ||
| from ..thinking.effort import resolve_reasoning_params | ||
| reasoning_params = resolve_reasoning_params(effort, self.model) | ||
| # Don't clobber an explicit native param the caller already set. | ||
| for key, value in reasoning_params.items(): | ||
| params.setdefault(key, value) | ||
|
|
||
|
Comment on lines
+5787
to
+5804
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Propagate reasoning effort through the Responses API path and add routed-model coverage. Only 📍 Affects 1 file
🤖 Prompt for AI Agents
Comment on lines
+5792
to
+5804
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- repository conventions ---'
head -5 /tmp/coderabbit-repo-knowledge/mervinpraison-praisonai-dcd467e7/*/*.md 2>/dev/null || true
printf '%s\n' '--- target outline ---'
ast-grep outline src/praisonai-agents/praisonaiagents/llm/llm.py --match 'class LLM' --view expanded 2>/dev/null | head -120 || true
printf '%s\n' '--- target implementation ---'
sed -n '5750,5835p' src/praisonai-agents/praisonaiagents/llm/llm.py
printf '%s\n' '--- response routing and params ---'
sed -n '5940,6170p' src/praisonai-agents/praisonaiagents/llm/llm.py
printf '%s\n' '--- effort resolver ---'
effort_file=$(fd -t f -p 'effort.py' . | head -1)
printf 'effort_file=%s\n' "$effort_file"
sed -n '1,260p' "$effort_file"
printf '%s\n' '--- litellm references and version pins ---'
rg -n -i 'litellm|modify_params|reasoning_effort|thinking_budget|resolve_reasoning_params' --glob '!*.lock' --glob '!node_modules/**' . | head -220Repository: MervinPraison/PraisonAI Length of output: 50379 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- repository conventions ---'
head -5 /tmp/coderabbit-repo-knowledge/mervinpraison-praisonai-dcd467e7/*/*.md 2>/dev/null || true
printf '%s\n' '--- target implementation ---'
sed -n '5750,5835p' src/praisonai-agents/praisonaiagents/llm/llm.py
printf '%s\n' '--- response routing and params ---'
sed -n '5940,6170p' src/praisonai-agents/praisonaiagents/llm/llm.py
printf '%s\n' '--- effort resolver ---'
effort_file=$(fd -t f -p 'effort.py' . | head -1)
printf 'effort_file=%s\n' "$effort_file"
sed -n '1,260p' "$effort_file"
printf '%s\n' '--- litellm references and version pins ---'
rg -n -i 'litellm|modify_params|reasoning_effort|thinking_budget|resolve_reasoning_params' --glob '!*.lock' --glob '!node_modules/**' . | head -220Repository: MervinPraison/PraisonAI Length of output: 50379 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- completion call path ---'
sed -n '2520,2725p' src/praisonai-agents/praisonaiagents/llm/llm.py
printf '%s\n' '--- async completion call path ---'
sed -n '4550,4715p' src/praisonai-agents/praisonaiagents/llm/llm.py
printf '%s\n' '--- initialization of temperature and reasoning settings ---'
sed -n '410,525p' src/praisonai-agents/praisonaiagents/llm/llm.py
printf '%s\n' '--- logging configuration and modify_params references ---'
sed -n '270,335p' src/praisonai-agents/praisonaiagents/llm/llm.py
rg -n -C 3 'modify_params|_build_completion_params\(|_build_responses_params\(' src/praisonai-agents/praisonaiagents/llm/llm.pyRepository: MervinPraison/PraisonAI Length of output: 43725 🌐 Web query:
💡 Result: When using Anthropic's "thinking" (extended thinking) mode, Anthropic requires the temperature to be set to 1 [1][2][3]. If a request is sent with thinking enabled and a temperature other than 1, Anthropic will return an error: "temperature may only be set to 1 when thinking is enabled" [1][2]. While Citations:
🌐 Web query:
💡 Result: The _drop_incompatible_temperature_for_thinking method is a utility function implemented within LiteLLM’s Anthropic message transformation layer (specifically located in litellm/llms/anthropic/experimental_pass_through/messages/transformation.py) [1][2][3]. It was introduced to address Anthropic API errors occurring when "extended thinking" features are used alongside non-default temperature settings [1][4][5]. Key details regarding this transformation include: Purpose: Anthropic requires temperature to be set to 1 when extended thinking is enabled [1][2][3]. When clients (such as Claude Code) send requests with extended thinking (e.g., via adaptive thinking or legacy thinking parameters) combined with a pinned temperature other than 1, the Anthropic API rejects the request with a 400 Invalid Request Error [1][4][5]. This method silently removes the incompatible temperature parameter from the request, allowing Anthropic to apply its default temperature of 1 [1][2]. Scope of Application: The method specifically targets non-adaptive (pre-4.6) models [1][2][3]. Adaptive models (claude-3-7-sonnet and newer, or 4.6+ versions) handle this natively and are left untouched by the logic [1][2][3]. The check runs after other thinking-related transformations in the Anthropic /v1/messages passthrough path [1][4][5]. Context: This was introduced as part of fix Citations:
🌐 Web query:
💡 Result: The premise that "extended thinking temperature must be 1" is incorrect. In fact, for many current Claude models—including Claude Sonnet 5, Claude Opus 5, and others—setting the temperature parameter to any non-default value will return a 400 error when thinking is enabled or used [1][2][3]. On models where extended thinking is used, the following rules generally apply regarding temperature and sampling parameters: 1. Incompatible with Thinking: On newer models (e.g., Claude Sonnet 5, Claude Opus 5, Claude Fable 5, Claude Mythos 5), setting non-default values for temperature, top_p, or top_k returns a 400 error regardless of whether thinking is active [1][2]. 2. Restricted Usage: On older models that support manual extended thinking, temperature and top_k are typically incompatible with the thinking feature [1]. If you attempt to use them while thinking is enabled, the API will return an error [1]. 3. Default Behavior: To ensure successful API requests, you should omit temperature, top_p, and top_k parameters when using models that support or require extended/adaptive thinking [2][3]. The model will then use its default behavior [2]. In summary, you cannot set the temperature to 1 (or any other value) if it differs from the model's default when using thinking features; doing so will likely cause your request to fail [1][2]. Citations:
🏁 Script executed: #!/bin/bash
set -eu
api='https://api.github.com/repos/BerriAI/litellm'
tag='v1.83.14'
printf '%s\n' '--- tag object ---'
curl -fsSL "$api/git/ref/tags/$tag" | jq '{ref, object}'
printf '%s\n' '--- candidate transformation files at tag ---'
curl -fsSL "$api/git/trees/$tag?recursive=1" |
jq -r '.tree[] | select(.path|test("anthropic.*transformation|transformation.*anthropic";"i")) | .path' |
head -40
printf '%s\n' '--- version metadata ---'
curl -fsSL "$api/contents/pyproject.toml?ref=$tag" |
jq -r '.content' | base64 -d | rg -n 'version\s*=|__version__' | head -10Repository: MervinPraison/PraisonAI Length of output: 398 🌐 Web query:
💡 Result: The release tag for LiteLLM version 1.83.14 is v1.83.14-stable [1][2]. This release was published on May 2, 2026 [1][3] and featured updates such as support for GPT-5.5, prompt compression, and a Memory API [4]. You can view the full changelog and associated commits on the official BerriAI/litellm GitHub repository [5]. Citations:
🌐 Web query:
💡 Result: In LiteLLM, setting Citations:
🏁 Script executed: #!/bin/bash
set -eu
api='https://api.github.com/repos/BerriAI/litellm'
tag='v1.83.14-stable'
printf '%s\n' '--- tag object ---'
curl -fsSL "$api/git/ref/tags/$tag" | jq '{ref, object}'
printf '%s\n' '--- candidate transformation files at tag ---'
curl -fsSL "$api/git/trees/$tag?recursive=1" |
jq -r '.tree[] | select(.path|test("anthropic.*transformation|transformation.*anthropic";"i")) | .path' |
head -60
printf '%s\n' '--- temperature/thinking handling in tagged sources ---'
for path in \
litellm/llms/anthropic/chat/transformation.py \
litellm/llms/anthropic/experimental_pass_through/messages/transformation.py; do
url="$api/contents/$path?ref=$tag"
body=$(curl -fsSL "$url" || true)
if [ -n "$body" ]; then
echo "--- $path ---"
echo "$body" | jq -r '.content' | base64 -d |
rg -n -C 5 'temperature|thinking|modify_params|drop_incompatible' | head -220
fi
doneRepository: MervinPraison/PraisonAI Length of output: 14655 Drop 🤖 Prompt for AI Agents |
||
| # Reasoning models (o1/o3/gpt-5.x) require max_completion_tokens and | ||
| # reject the legacy max_tokens parameter plus several sampling params. | ||
| # Normalize here (after override_params merge) so per-call overrides | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -242,7 +242,8 @@ def to_dict(self) -> Dict[str, Any]: | |
| } | ||
| if self.last_compaction is not None: | ||
| data["last_compaction"] = self.last_compaction.to_dict() | ||
| for key in ("model", "llm", "total_tokens", "token_count", "cost", "source"): | ||
| for key in ("model", "llm", "total_tokens", "token_count", "cost", "source", | ||
| "reasoning_effort"): | ||
| if key in self.metadata: | ||
| data[key] = self.metadata[key] | ||
| return data | ||
|
|
@@ -270,7 +271,8 @@ def from_dict(cls, data: Dict[str, Any]) -> "SessionData": | |
| # resume can recover the recorded model instead of silently reverting to | ||
| # the current default (Issue #3685). Existing metadata always wins. | ||
| metadata = dict(data.get("metadata") or {}) | ||
| for key in ("model", "llm", "total_tokens", "token_count", "cost", "source"): | ||
| for key in ("model", "llm", "total_tokens", "token_count", "cost", "source", | ||
| "reasoning_effort"): | ||
| if key not in metadata and data.get(key) is not None: | ||
| metadata[key] = data[key] | ||
| return cls( | ||
|
|
@@ -1449,6 +1451,28 @@ def get_session_model(self, session_id: str) -> Optional[str]: | |
| return recorded | ||
| return None | ||
|
|
||
| def get_session_reasoning_effort(self, session_id: str) -> Optional[str]: | ||
| """Return the reasoning effort a session was last run with (Issue #4452). | ||
|
|
||
| Mirrors :meth:`get_session_model`: resolves the session-level | ||
| ``reasoning_effort`` recorded in metadata so a resume (``--continue`` / | ||
| ``--session``) can restore the graded effort alongside the model. Returns | ||
| ``None`` when none was recorded, so a caller falls back to the default / | ||
| per-invocation value. | ||
| """ | ||
| try: | ||
| session = self._read_session_fresh(session_id) | ||
| except Exception: | ||
| return None | ||
| effort = session.metadata.get("reasoning_effort") | ||
| if isinstance(effort, str) and effort: | ||
| return effort | ||
| for message in reversed(session.messages): | ||
| recorded = (message.metadata or {}).get("reasoning_effort") | ||
| if isinstance(recorded, str) and recorded: | ||
| return recorded | ||
| return None | ||
|
Comment on lines
+1454
to
+1474
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When a user resumes a session through Knowledge Base Used: |
||
|
|
||
| def set_agent_info( | ||
| self, | ||
| session_id: str, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| """ | ||
| Reasoning-effort translation for PraisonAI Agents. | ||
|
|
||
| A single, graded, provider-portable reasoning-effort control that resolves to | ||
| each provider's native request parameter: | ||
|
|
||
| - OpenAI o-series / GPT-5 / xAI reasoning models -> native ``reasoning_effort`` | ||
| (``minimal|low|medium|high``). | ||
| - Anthropic / Gemini extended-thinking models -> a ``thinking`` token budget. | ||
| - Models without a reasoning control -> nothing (silently ignored, | ||
| backward-compatible). | ||
|
|
||
| Zero overhead when unused: ``off``/``None`` resolves to an empty dict and the | ||
| helper is only imported on the request path when an effort is actually set. | ||
| """ | ||
|
|
||
| from typing import Any, Dict, Optional | ||
|
|
||
| # Canonical graded levels shared with the CLI surface | ||
| # (``praisonai_code.cli.features.thinking.THINKING_LEVELS``). | ||
| EFFORT_LEVELS = ("off", "minimal", "low", "medium", "high") | ||
|
|
||
| # Native ``reasoning_effort`` only accepts these; ``off`` is a no-op. | ||
| _NATIVE_EFFORT = {"minimal", "low", "medium", "high"} | ||
|
|
||
| # Extended-thinking token budgets for Anthropic/Gemini, mirroring the CLI's | ||
| # ``THINKING_BUDGET_MAP`` so a level means the same thing on every surface. | ||
| _EFFORT_BUDGET_MAP: Dict[str, Optional[int]] = { | ||
| "off": None, | ||
| "minimal": 2000, | ||
| "low": 4000, | ||
| "medium": 8000, | ||
| "high": 16000, | ||
| } | ||
|
|
||
| # Inverse of the budget map: lets a legacy ``thinking_budget`` int be normalised | ||
| # back to the nearest graded level so both surfaces share one internal value. | ||
| _BUDGET_EFFORT_PAIRS = sorted( | ||
| ((tokens, level) for level, tokens in _EFFORT_BUDGET_MAP.items() if tokens), | ||
| key=lambda pair: pair[0], | ||
| ) | ||
|
|
||
|
|
||
| def normalize_effort(value: Any) -> Optional[str]: | ||
| """Normalise a reasoning-effort value to a canonical level or ``None``. | ||
|
|
||
| Accepts a graded string (``off|minimal|low|medium|high``, case-insensitive) | ||
| or a legacy ``thinking_budget`` int (mapped to the nearest level). Unknown | ||
| or unset values return ``None`` (treated as "no reasoning control"). | ||
| """ | ||
| if value is None: | ||
| return None | ||
| if isinstance(value, bool): | ||
| # Guard against ``True``/``False`` sneaking in via ``int`` handling. | ||
| return "medium" if value else None | ||
| if isinstance(value, str): | ||
| level = value.strip().lower() | ||
| return level if level in EFFORT_LEVELS else None | ||
| if isinstance(value, int): | ||
| if value <= 0: | ||
| return None | ||
| # Map a token budget to the smallest level whose budget covers it. | ||
| for tokens, level in _BUDGET_EFFORT_PAIRS: | ||
| if value <= tokens: | ||
| return level | ||
| return "high" | ||
| return None | ||
|
|
||
|
|
||
| def _is_native_effort_model(model: str) -> bool: | ||
| """OpenAI o-series / GPT-5 / xAI reasoning models take native effort.""" | ||
| from ..llm.model_capabilities import is_reasoning_model | ||
|
|
||
| name = (model or "").lower() | ||
| if name.startswith("xai/") or "grok" in name: | ||
| return True | ||
| return is_reasoning_model(model) | ||
|
|
||
|
|
||
| def _is_extended_thinking_model(model: str) -> bool: | ||
| """Anthropic / Gemini models expose an extended-thinking token budget.""" | ||
| name = (model or "").lower() | ||
| return ( | ||
| "claude" in name | ||
| or "anthropic" in name | ||
| or "gemini" in name | ||
| ) | ||
|
|
||
|
Comment on lines
+80
to
+88
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: printf '%s\n' '--- repository conventions ---'
head -5 /tmp/coderabbit-repo-knowledge/mervinpraison-praisonai-dcd467e7/*/*.md 2>/dev/null
printf '%s\n' '--- target file ---'
sed -n '1,150p' src/praisonai-agents/praisonaiagents/thinking/effort.py
printf '%s\n' '--- relevant learnings ---'
cat /tmp/coderabbit-repo-knowledge/mervinpraison-praisonai-dcd467e7/learnings/*.md 2>/dev/nullRepository: MervinPraison/PraisonAI Length of output: 11842 🌐 Web query:
💡 Result: In LiteLLM version 1.83.14, the handling of Gemini 3 models differs significantly from legacy models due to the shift from a token-based Citations:
🏁 Script executed: printf '%s\n' '--- LiteLLM dependency declarations ---'
rg -n -i --glob 'pyproject.toml' --glob 'requirements*.txt' --glob 'setup.cfg' --glob 'setup.py' 'litellm|praisonaiagents' .
printf '%s\n' '--- resolve_reasoning_params callers ---'
rg -n -C 5 'resolve_reasoning_params|reasoning_params' src/praisonai-agents/praisonaiagentsRepository: MervinPraison/PraisonAI Length of output: 8854 Use Gemini 3's
🤖 Prompt for AI Agents |
||
|
|
||
| def resolve_reasoning_params(effort: Any, model: str) -> Dict[str, Any]: | ||
| """Translate a unified reasoning-effort level to provider-native kwargs. | ||
|
|
||
| Args: | ||
| effort: A graded level (``off|minimal|low|medium|high``) or a legacy | ||
| ``thinking_budget`` int; anything else is treated as unset. | ||
| model: The target model name (with or without provider prefix). | ||
|
|
||
| Returns: | ||
| A dict of native request params to merge into the completion call: | ||
| ``{"reasoning_effort": <level>}`` for OpenAI/xAI reasoning models, | ||
| ``{"thinking": {"type": "enabled", "budget_tokens": <int>}}`` for | ||
| Anthropic/Gemini extended-thinking models, or ``{}`` when the effort is | ||
| off/unset or the model has no reasoning control. | ||
| """ | ||
| level = normalize_effort(effort) | ||
| if level is None or level == "off": | ||
| return {} | ||
|
|
||
| # Anthropic / Gemini expose an extended-thinking token budget. Checked first | ||
| # because these families can also match the generic reasoning-model | ||
| # classifier, but their native control is the thinking budget, not | ||
| # ``reasoning_effort``. | ||
| if _is_extended_thinking_model(model): | ||
| budget = _EFFORT_BUDGET_MAP.get(level) | ||
| if budget: | ||
| return {"thinking": {"type": "enabled", "budget_tokens": budget}} | ||
| return {} | ||
|
|
||
| # OpenAI o-series / GPT-5 / xAI reasoning models take native reasoning_effort. | ||
| if _is_native_effort_model(model): | ||
| if level in _NATIVE_EFFORT: | ||
| return {"reasoning_effort": level} | ||
| return {} | ||
|
|
||
| # No known reasoning control for this model: silently ignore. | ||
| return {} | ||
Uh oh!
There was an error while loading. Please reload this page.