Skip to content

Commit 353480c

Browse files
fix(llm): add static provider capability lists and reasoning_effort fallback tests
Signed-off-by: Hasnaat Hussain <hasnaat.hussain.2@gmail.com>
1 parent 13e298a commit 353480c

2 files changed

Lines changed: 113 additions & 25 deletions

File tree

lib/crewai/src/crewai/llm.py

Lines changed: 66 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,42 @@ def _ensure_litellm() -> bool:
165165
MAX_CONTEXT: Final[int] = 2097152 # Current max from gemini-1.5-pro
166166
ANTHROPIC_PREFIXES: Final[tuple[str, str, str]] = ("anthropic/", "claude-", "claude/")
167167

168+
# Static provider capability lists. These are checked *before* LiteLLM
169+
# introspection so that well-known providers always receive correct capability
170+
# detection even when LiteLLM's model-mapping table is unavailable or raises.
171+
RESPONSE_FORMAT_SUPPORTED_PROVIDERS: Final[frozenset[str]] = frozenset(
172+
{
173+
"openai",
174+
"azure",
175+
"azure_openai",
176+
"google",
177+
"gemini",
178+
"anthropic",
179+
"bedrock",
180+
"deepseek",
181+
"openrouter",
182+
"mistral",
183+
"groq",
184+
"fireworks_ai",
185+
"together_ai",
186+
"anyscale",
187+
"cerebras",
188+
"dashscope",
189+
}
190+
)
191+
192+
REASONING_EFFORT_SUPPORTED_PROVIDERS: Final[frozenset[str]] = frozenset(
193+
{
194+
"openai",
195+
"azure",
196+
"azure_openai",
197+
"anthropic",
198+
"bedrock",
199+
"deepseek",
200+
"openrouter",
201+
}
202+
)
203+
168204
LLM_CONTEXT_WINDOW_SIZES: Final[dict[str, int]] = {
169205
"gpt-4": 8192,
170206
"gpt-4o": 128000,
@@ -2382,29 +2418,47 @@ def _get_custom_llm_provider(self) -> str | None:
23822418
def _validate_call_params(self) -> None:
23832419
"""Validate call parameters before executing a completion request.
23842420
2385-
Checks whether the requested ``response_format`` is supported by the
2386-
target model/provider via LiteLLM introspection. When introspection
2387-
raises an exception (e.g. an unmapped custom model ID or proxy
2388-
endpoint), the failure is logged at DEBUG level and validation is
2389-
skipped so the request can proceed — this mirrors the permissive
2390-
fallback used in :meth:`supports_function_calling`.
2421+
Performs two kinds of checks in order:
2422+
2423+
1. **Static capability checks** (always run): ``response_format`` and
2424+
``reasoning_effort`` are tested against
2425+
:data:`RESPONSE_FORMAT_SUPPORTED_PROVIDERS` and
2426+
:data:`REASONING_EFFORT_SUPPORTED_PROVIDERS` respectively. These
2427+
module-level frozensets encode well-known provider support and run
2428+
unconditionally — regardless of whether LiteLLM is available.
23912429
2392-
When no ``response_format`` is configured (including when only
2393-
``reasoning_effort`` or other parameters are set) this method returns
2394-
immediately without performing any checks.
2430+
2. **LiteLLM introspection** (best-effort): When the provider is *not*
2431+
in the static lists, ``supports_response_schema`` is called for
2432+
additional coverage. If introspection raises (e.g. an unmapped
2433+
custom model ID or proxy endpoint), the failure is logged at DEBUG
2434+
level and validation is skipped — permitting the request to proceed.
23952435
23962436
Note: This validation only applies to the litellm fallback path.
23972437
Native providers perform their own parameter validation.
23982438
"""
2439+
provider = self._get_custom_llm_provider() or "openai"
2440+
2441+
# --- Static reasoning_effort check (always runs) ---
2442+
if self.reasoning_effort is not None:
2443+
if provider not in REASONING_EFFORT_SUPPORTED_PROVIDERS:
2444+
logger.debug(
2445+
f"Provider '{provider}' does not appear in the static reasoning_effort "
2446+
"support list; the parameter will be forwarded and may be ignored."
2447+
)
2448+
2449+
# --- response_format checks ---
23992450
if self.response_format is None:
24002451
return
24012452

2453+
# 1. Static check: known-supported providers skip LiteLLM introspection.
2454+
if provider in RESPONSE_FORMAT_SUPPORTED_PROVIDERS:
2455+
return
2456+
2457+
# 2. LiteLLM introspection for providers not in the static list.
24022458
if not _ensure_litellm() or supports_response_schema is None:
2403-
# When litellm is not available, skip validation
2404-
# (this path should only be reached for litellm fallback models)
2459+
# LiteLLM unavailable; static list was already consulted — allow.
24052460
return
24062461

2407-
provider = self._get_custom_llm_provider()
24082462
try:
24092463
is_supported = supports_response_schema(
24102464
model=self.model,

lib/crewai/tests/test_llm.py

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -233,10 +233,12 @@ def test_validate_call_params_not_supported():
233233
class DummyResponse(BaseModel):
234234
a: int
235235

236-
# Patch supports_response_schema to simulate an unsupported model.
236+
# Use an unknown provider (not in RESPONSE_FORMAT_SUPPORTED_PROVIDERS) so the
237+
# LiteLLM introspection path is exercised and supports_response_schema can
238+
# report False, which should raise ValueError.
237239
with patch("crewai.llm.supports_response_schema", return_value=False):
238240
llm = LLM(
239-
model="gemini/gemini-1.5-pro",
241+
model="unknown-llm-provider/some-model",
240242
response_format=DummyResponse,
241243
is_litellm=True,
242244
)
@@ -1211,19 +1213,24 @@ async def _ret(*args, **kwargs):
12111213

12121214

12131215
def test_validate_call_params_handles_introspection_error():
1214-
"""Verify that _validate_call_params gracefully handles LiteLLM introspection errors without crashing."""
1216+
"""Verify _validate_call_params gracefully handles LiteLLM introspection errors without crashing.
1217+
1218+
When the provider is not in the static list, introspection is attempted.
1219+
An exception during introspection must be caught and treated as supported
1220+
so the request can proceed with custom/proxy models.
1221+
"""
12151222
llm = LLM(model="custom/unsupported-model", is_litellm=True, response_format={"type": "json_object"})
12161223

12171224
with patch("crewai.llm.supports_response_schema", side_effect=Exception("Introspection error")):
1218-
# Should not raise exception permissive fallback applies
1225+
# Should not raise exception -- permissive fallback applies
12191226
llm._validate_call_params()
12201227

12211228

12221229
def test_validate_call_params_raises_for_confirmed_unsupported_model():
12231230
"""Verify _validate_call_params still raises ValueError when introspection confirms no support.
12241231
1225-
This ensures the validation path is not silently skipped only introspection
1226-
*errors* are swallowed; a clear ``False`` return still produces a ValueError.
1232+
This ensures the validation path is not silently skipped -- only introspection
1233+
errors are swallowed; a clear False return still produces a ValueError.
12271234
"""
12281235
llm = LLM(model="my-provider/my-model", is_litellm=True, response_format={"type": "json_object"})
12291236

@@ -1233,17 +1240,44 @@ def test_validate_call_params_raises_for_confirmed_unsupported_model():
12331240

12341241

12351242
def test_validate_call_params_skips_check_when_no_response_format():
1236-
"""Verify _validate_call_params is a no-op when response_format is not set.
1243+
"""Verify _validate_call_params is a no-op for params that require no validation.
1244+
1245+
When no response_format is set, the method must return without ever
1246+
calling the LiteLLM introspection function.
1247+
"""
1248+
llm = LLM(model="gpt-4o", is_litellm=True, temperature=0.5)
1249+
1250+
with patch("crewai.llm.supports_response_schema", side_effect=Exception("Should not be called")):
1251+
# No exception -- response_format is None so introspection is never invoked
1252+
llm._validate_call_params()
1253+
12371254

1238-
This covers scenarios where only ``reasoning_effort`` or other parameters
1239-
are configured: the method must return immediately so those parameters are
1240-
forwarded to the provider without interference.
1255+
def test_validate_call_params_reasoning_effort_supported_provider():
1256+
"""Verify _validate_call_params passes without error for reasoning_effort on a supported provider.
1257+
1258+
openai is in REASONING_EFFORT_SUPPORTED_PROVIDERS, so no warning or
1259+
error should be raised and introspection must not be triggered.
12411260
"""
1242-
llm = LLM(model="gpt-4o", is_litellm=True, reasoning_effort="medium")
1261+
llm = LLM(model="openai/o3-mini", is_litellm=True, reasoning_effort="medium")
12431262

1244-
# If supports_response_schema were called, patching it to raise would surface the bug
12451263
with patch("crewai.llm.supports_response_schema", side_effect=Exception("Should not be called")):
1246-
# No exception — response_format is None so introspection is never invoked
1264+
# No response_format set -- introspection is never invoked
1265+
llm._validate_call_params()
1266+
1267+
1268+
def test_validate_call_params_reasoning_effort_unknown_provider_logs_debug(caplog):
1269+
"""Verify _validate_call_params logs a debug message for unknown providers with reasoning_effort.
1270+
1271+
Unknown providers are not rejected outright: parameters are forwarded and
1272+
may be silently ignored by the underlying provider. The debug log signals
1273+
this to developers without breaking the call.
1274+
"""
1275+
import logging
1276+
1277+
llm = LLM(model="unknown-provider/some-model", is_litellm=True, reasoning_effort="high")
1278+
1279+
with caplog.at_level(logging.DEBUG, logger="crewai.llm"):
12471280
llm._validate_call_params()
12481281

1282+
assert any("reasoning_effort" in record.message for record in caplog.records)
12491283

0 commit comments

Comments
 (0)