Skip to content

fix(openai): preserve reasoning_effort summary field for Responses API - #23151

Merged
Sameerlite merged 2 commits into
mainfrom
litellm_preserve-reasoning-summary-for-responses-api
Mar 10, 2026
Merged

fix(openai): preserve reasoning_effort summary field for Responses API#23151
Sameerlite merged 2 commits into
mainfrom
litellm_preserve-reasoning-summary-for-responses-api

Conversation

@Sameerlite

Copy link
Copy Markdown
Contributor

When reasoning_effort is passed as a dict with additional fields like 'summary' or 'generate_summary', preserve the full dict format instead of normalizing it to a string. This ensures that when requests are routed to the OpenAI Responses API, all reasoning parameters are correctly included.

The normalization to string format now only happens for simple dicts with just the 'effort' key, which is appropriate for the Chat Completions API.

Fixes issue where summary field was being dropped when routing gpt-5.4+ requests with tools + reasoning to Responses API.

Made-with: Cursor

Relevant issues

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have Added testing in the tests/test_litellm/ directory, Adding at least 1 test is a hard requirement - see details
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible, it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

CI (LiteLLM team)

CI status guideline:

  • 50-55 passing tests: main is stable with minor issues.
  • 45-49 passing tests: acceptable but needs attention
  • <= 40 passing tests: unstable; be careful with your merges and assess the risk.
  • Branch creation CI run
    Link:

  • CI run for the last commit
    Link:

  • Merge / cherry-pick CI run
    Links:

Type

🆕 New Feature
🐛 Bug Fix
🧹 Refactoring
📖 Documentation
🚄 Infrastructure
✅ Test

Changes

When reasoning_effort is passed as a dict with additional fields like 'summary' or 'generate_summary', preserve the full dict format instead of normalizing it to a string. This ensures that when requests are routed to the OpenAI Responses API, all reasoning parameters are correctly included.

The normalization to string format now only happens for simple dicts with just the 'effort' key, which is appropriate for the Chat Completions API.

Fixes issue where summary field was being dropped when routing gpt-5.4+ requests with tools + reasoning to Responses API.

Made-with: Cursor
@vercel

vercel Bot commented Mar 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
litellm Error Error Mar 9, 2026 1:16pm

Request Review

@greptile-apps

greptile-apps Bot commented Mar 9, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a bug where reasoning_effort dict fields beyond "effort" (e.g. "summary", "generate_summary") were being stripped before the Responses API transformation could consume them, and also adds proper xhigh/none guard handling for dict-shaped inputs.

Key changes:

  • _get_effort_level helper is introduced to extract the scalar effort string from either a string or dict input, used in all downstream guards
  • Normalization to a plain string now only happens when the dict has only the "effort" key; richer dicts are preserved so the Responses API transformation receives them intact
  • A new is_model_gpt_5_4_plus_model helper gates the tool+reasoning drop logic; Azure always drops, OpenAI preserves (Responses API routing)
  • Test coverage is good — all new tests are mock-only

Issues found:

  • _get_effort_level (new) and _normalize_reasoning_effort_for_chat_completion (existing) have identical implementations, creating a maintenance risk of future divergence
  • is_model_gpt_5_4_plus_model hardcodes model-version parsing via string arithmetic (int(major) >= 4), violating the project rule to store model capability flags in model_prices_and_context_window.json and read them via get_model_info/_supports_factory
  • The tool+reasoning guard block in the OpenAI path (lines 228–240) is dead code: because is_model_gpt_5_4_model is True only when is_model_gpt_5_4_plus_model is also True, the inner not is_model_gpt_5_4_plus_model condition is always False, and the drop path is never executed. The follow-up reasoning_effort = None assignment is also dead since reasoning_effort is not read after that point.

Confidence Score: 3/5

  • PR is safe to merge for the stated fix, but two structural issues need addressing before broader confidence can be given.
  • The core fix (preserving the reasoning_effort dict for Responses API) is correct and well-tested. However, the new is_model_gpt_5_4_plus_model method hardcodes model-version detection in violation of the project's established pattern, meaning future models will require a code upgrade to benefit. Additionally, the tool+reasoning guard in the OpenAI path is logically dead (the inner branch can never be reached), making the codebase harder to reason about. These issues reduce confidence below a clean merge.
  • litellm/llms/openai/chat/gpt_5_transformation.py — dead code block (lines 228–240) and new hardcoded model-detection method need attention before merge.

Important Files Changed

Filename Overview
litellm/llms/openai/chat/gpt_5_transformation.py Core change: preserves reasoning_effort dict (with summary/generate_summary fields) for Responses API routing. Adds _get_effort_level (duplicate of existing _normalize_reasoning_effort_for_chat_completion) and is_model_gpt_5_4_plus_model (hardcoded model-version parsing violating the no-hardcoded-model-flags rule). Tool+reasoning guard (lines 228-240) is dead code — the inner condition is never True.
litellm/llms/azure/chat/gpt_5_transformation.py Azure-specific handling correctly uses _get_effort_level for all effort-level guards and adds a new block to drop reasoning_effort when tools are present (Azure doesn't route to Responses API). The is_model_gpt_5_4_plus_model call here inherits the same hardcoded-model-flag issue from the parent class.
tests/test_litellm/llms/openai/chat/test_openai_gpt_transformation.py New TestGPT5ReasoningEffortPreservation class provides good coverage of dict preservation, xhigh validation, none-as-effort for tools/sampling/temperature guards. Tests are mock-only and well-structured.
tests/test_litellm/llms/openai/test_gpt5_transformation.py Renamed and expanded existing tests to match the new preservation behavior; adds regression tests for xhigh-dict, none-dict with sampling, etc. All mock-only.
tests/test_litellm/llms/azure/chat/test_azure_gpt5_transformation.py Adds test_azure_gpt5_4_drops_reasoning_effort_when_tools_present validating the new Azure-specific drop path. Mock-only, clear and correct.
tests/test_litellm/completion_extras/litellm_responses_transformation/test_completion_extras_litellm_responses_transformation_transformation.py Adds test_map_optional_params_preserves_reasoning_summary verifying the end-to-end Responses API transformation preserves the dict. No real network calls.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["map_openai_params called\nwith reasoning_effort"] --> B{Is reasoning_effort a dict?}
    B -->|"Yes: {effort, summary, ...}"| C{Keys == just 'effort'?}
    B -->|"No: string"| D["Use as-is\neffective_effort = string"]
    C -->|"Yes (only 'effort' key)"| E["Normalize to string\nfor Chat Completions API"]
    C -->|"No (has 'summary', etc.)"| F["Preserve full dict\nfor Responses API routing"]
    D --> G["effective_effort = _get_effort_level(value)"]
    E --> G
    F --> G
    G --> H{effective_effort == 'xhigh'?}
    H -->|"Yes + model unsupported"| I["drop or raise UnsupportedParamsError"]
    H -->|"No"| J{is gpt-5.4 model + tools present?}
    I --> J
    J -->|"Yes (OpenAI)"| K{"is_model_gpt_5_4_plus_model?\n(always True for gpt-5.4)"}
    J -->|"No"| L["Continue to sampling/temperature guards"]
    K -->|"True → inner condition False"| M["Dead code: reasoning_effort NOT dropped\n(Responses API will handle it)"]
    M --> L
    L --> N{Azure path?}
    N -->|"Yes (gpt-5.4+)"| O["Drop reasoning_effort\n(Azure has no Responses API routing)"]
    N -->|"No (OpenAI)"| P["Pass through to Responses API transformation"]
Loading

Comments Outside Diff (1)

  1. litellm/llms/openai/chat/gpt_5_transformation.py, line 228-240 (link)

    Dead code block — inner condition is always False when outer condition is True

    is_model_gpt_5_4_model returns True only for model names starting with gpt-5.4. is_model_gpt_5_4_plus_model also returns True for any model starting with gpt-5.4 (since int("4") >= 4). Therefore the inner guard not self.is_model_gpt_5_4_plus_model(model) is always False when the outer if self.is_model_gpt_5_4_model(model) is True.

    Consequence: the three-line drop path (lines 238–240) is never executedreasoning_effort is never removed here. While this may be intentional for OpenAI (Responses API routing), the structure is misleading and the comment ("otherwise drop it for chat completions API") implies there are cases where the drop path runs, which is untrue.

    Additionally, reasoning_effort = None on line 240 sets a variable that is no longer read after this point (all subsequent guards use effective_effort), making it doubly dead.

Last reviewed commit: ee3ecb5

Comment on lines 183 to 184
reasoning_effort = normalized or raw_reasoning_effort
if reasoning_effort is not None and reasoning_effort == "xhigh":

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

xhigh validation silently bypassed when reasoning_effort is a dict

After this PR, reasoning_effort can be left as a dict (e.g. {"effort": "xhigh", "summary": "detailed"}). On line 184 the guard:

if reasoning_effort is not None and reasoning_effort == "xhigh":

will always be False when reasoning_effort is a dict, regardless of whether effort is "xhigh". This means the model-support validation for xhigh is completely skipped for dict-format inputs, so unsupported models will silently receive "xhigh" effort instead of raising UnsupportedParamsError or dropping the param.

The fix needs to extract the effort string before doing these comparisons, for example:

effort_level = (
    reasoning_effort.get("effort")
    if isinstance(reasoning_effort, dict)
    else reasoning_effort
)
if effort_level is not None and effort_level == "xhigh":

And use effort_level for all subsequent string comparisons on lines 211 and 221.

…s for dict inputs

- Add _get_effort_level() to extract effective effort from string or dict
- Use effective_effort for xhigh validation, tool-drop, sampling, temperature guards
- Preserve dict format when it has summary/generate_summary for Responses API
- Add tests: xhigh-dict validation, none-dict for tools/sampling/temperature
- Update tests: dict-with-summary now preserved (not normalized)

Made-with: Cursor
Comment on lines +28 to +41
def _get_effort_level(value: Union[str, dict, None]) -> Optional[str]:
"""Extract the effective effort level from reasoning_effort (string or dict).

Use this for guards that compare effort level (e.g. xhigh validation, "none" checks).
Ensures dict inputs like {"effort": "none", "summary": "detailed"} are correctly
treated as effort="none" for validation purposes.
"""
if value is None:
return None
if isinstance(value, str):
return value
if isinstance(value, dict) and "effort" in value:
return value["effort"]
return None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate implementation of _normalize_reasoning_effort_for_chat_completion

_get_effort_level (lines 28–41) and _normalize_reasoning_effort_for_chat_completion (lines 11–25) have byte-for-byte identical implementations. Both functions:

  • return None for None input
  • return the string directly for string input
  • return value["effort"] for a dict that contains "effort"
  • return None as a fallback

Having two functions with the same logic but different names makes it easy for them to diverge in the future. Consider removing _normalize_reasoning_effort_for_chat_completion and calling _get_effort_level everywhere, or at least making one delegate to the other (e.g. _normalize_reasoning_effort_for_chat_completion = _get_effort_level).

Comment on lines +89 to +100
@classmethod
def is_model_gpt_5_4_plus_model(cls, model: str) -> bool:
"""Check if the model is gpt-5.4 or newer (5.4, 5.5, 5.6, etc., including pro)."""
model_name = model.split("/")[-1]
if not model_name.startswith("gpt-5."):
return False
try:
version_str = model_name.replace("gpt-5.", "").split("-")[0]
major = version_str.split(".")[0]
return int(major) >= 4
except (ValueError, IndexError):
return False

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded model-version parsing violates the "no model flags in code" rule

is_model_gpt_5_4_plus_model determines model capability by string-parsing the version number directly from the model name (int(major) >= 4). This is the pattern the codebase rule explicitly forbids: every time a new model crosses the 5.4+ threshold (e.g. gpt-5.5, gpt-5.6), users must upgrade LiteLLM to get the correct behavior rather than just getting an update via model_prices_and_context_window.json.

Per the custom rule, the capability flag (routes_to_responses_api_with_tools, or similar) should be stored in model_prices_and_context_window.json and read via get_model_info/_supports_factory, exactly as _supports_reasoning_effort_level already does for effort-level capabilities.

Rule Used: What: Do not hardcode model-specific flags in the ... (source)

@Sameerlite
Sameerlite merged commit cd80213 into main Mar 10, 2026
76 of 102 checks passed
Chesars added a commit that referenced this pull request Mar 12, 2026
…3151)

Independent fix (base: main) collaterally removed by PR #23276.
Restores:
- _get_effort_level() for extracting effort from string or dict
- is_model_gpt_5_4_plus_model() classmethod
- effective_effort usage in xhigh/tool-drop/sampling/temperature guards
- Azure: _get_effort_level import and usage for dict reasoning_effort
- Azure: gpt-5.4+ tool+reasoning drop logic
@ishaan-berri
ishaan-berri deleted the litellm_preserve-reasoning-summary-for-responses-api branch March 26, 2026 22:29
fzowl pushed a commit to fzowl/litellm that referenced this pull request Jun 24, 2026
…ing-summary-for-responses-api

fix(openai): preserve reasoning_effort summary field for Responses API
fzowl pushed a commit to fzowl/litellm that referenced this pull request Jun 24, 2026
…rriAI#23151)

Independent fix (base: main) collaterally removed by PR BerriAI#23276.
Restores:
- _get_effort_level() for extracting effort from string or dict
- is_model_gpt_5_4_plus_model() classmethod
- effective_effort usage in xhigh/tool-drop/sampling/temperature guards
- Azure: _get_effort_level import and usage for dict reasoning_effort
- Azure: gpt-5.4+ tool+reasoning drop logic
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant