Skip to content

fix(llm): prevent infinite recursion and params overwrite in call()/acall()#4503

Open
eren-karakus0 wants to merge 1 commit intocrewAIInc:mainfrom
eren-karakus0:fix/llm-call-infinite-recursion-and-params-overwrite
Open

fix(llm): prevent infinite recursion and params overwrite in call()/acall()#4503
eren-karakus0 wants to merge 1 commit intocrewAIInc:mainfrom
eren-karakus0:fix/llm-call-infinite-recursion-and-params-overwrite

Conversation

@eren-karakus0
Copy link

@eren-karakus0 eren-karakus0 commented Feb 17, 2026

Summary

Fixes two critical bugs in LLM.call() and LLM.acall() that occur when a provider permanently rejects the stop parameter with an Unsupported parameter: 'stop' error.

Bug 1: Infinite Recursion (RecursionError)

When the stop parameter is unsupported, the error handler adds "stop" to additional_drop_params and recursively calls self.call() / self.acall(). However, if the provider still rejects the parameter (e.g., due to a provider-side bug, or if the drop_params mechanism fails to strip it), there is no guard to prevent re-entry. The method keeps calling itself until Python hits RecursionError after ~1000 frames.

Before (crashes):

# No check if we already tried dropping 'stop'
except Exception as e:
    if "Unsupported parameter" in str(e) and "'stop'" in str(e):
        self.additional_params = {"additional_drop_params": ["stop"]}
        return self.call(...)  # infinite loop if error persists

After (fails cleanly after one retry):

except Exception as e:
    if "Unsupported parameter" in str(e) and "'stop'" in str(e):
        existing = self.additional_params.get("additional_drop_params", [])
        if isinstance(existing, list) and "stop" in existing:
            raise  # already tried, don't recurse again
        # ... append "stop" and retry once

Bug 2: Additional Parameters Overwritten

The else branch replaces self.additional_params entirely:

self.additional_params = {"additional_drop_params": ["stop"]}

This destroys any existing keys (extra_headers, seed, top_k, etc.) that were previously configured. The fix appends to the existing dict instead:

self.additional_params["additional_drop_params"] = ["stop"]

Scope

Both bugs exist in the sync call() method and the async acall() method. Both are fixed with identical logic.

Test Plan

Added 7 tests in TestUnsupportedStopRetryGuard:

  • test_retries_once_then_raises_on_persistent_stop_error - Verifies exactly 2 calls (not infinite), then raises
  • test_preserves_existing_additional_params - Confirms extra_headers and seed survive the retry
  • test_appends_to_existing_drop_params - Verifies ["another_param"] becomes ["another_param", "stop"]
  • test_non_stop_exceptions_are_not_retried - Non-stop errors propagate immediately (1 call only)
  • test_acall_retries_once_then_raises_on_persistent_stop_error - Async version of recursion guard
  • test_acall_preserves_existing_additional_params - Async version of params preservation

Reproduction

from crewai import LLM

llm = LLM(model="some-provider/model", extra_headers={"X-Key": "val"}, seed=42)

# Before fix: RecursionError after ~1000 frames
# Before fix: extra_headers and seed are lost
# After fix: clean exception after 1 retry, params preserved
llm.call(messages=[{"role": "user", "content": "hello"}])

Note

Medium Risk
Touches core LLM invocation error-handling and retry logic in both sync and async codepaths; incorrect conditions could change retry behavior or error propagation across providers.

Overview
Fixes LLM.call() and LLM.acall() retry behavior when a provider errors on the stop parameter by retrying at most once (detecting when stop is already in additional_drop_params) and by adding stop to additional_drop_params without clobbering other additional_params.

Adds a focused test suite (TestUnsupportedStopRetryGuard) validating single-retry semantics, non-stop errors not being retried, appending to existing drop params, and preservation of pre-existing additional_params for both sync and async paths.

Written by Cursor Bugbot for commit 03890d7. This will update automatically on new commits. Configure here.

…call()

When a provider permanently rejects the 'stop' parameter, the retry
logic in both call() and acall() recursively calls itself without
checking whether 'stop' was already added to drop_params. This causes
a RecursionError instead of a clean failure.

Additionally, the else branch overwrites self.additional_params with a
new dict containing only additional_drop_params, destroying any
existing parameters (extra_headers, seed, etc.).

Changes:
- Add recursion guard: check if 'stop' is already in drop_params
  before retrying, raise immediately if retry already attempted
- Preserve existing additional_params by appending to the list
  instead of replacing the entire dict
- Apply identical fix to both sync call() and async acall()
- Add 7 comprehensive tests covering recursion guard, params
  preservation, and async paths
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

Comments