-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
fix: translate graded reasoning effort to provider-native controls + persist per session #4479
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
base: main
Are you sure you want to change the base?
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 |
|---|---|---|
|
|
@@ -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. | ||
|
Comment on lines
+1454
to
+1461
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 session using Knowledge Base Used: |
||
| """ | ||
| 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 | ||
|
|
||
| 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 | ||
| ) | ||
|
|
||
|
|
||
| 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.