Skip to content

Commit 6d12fff

Browse files
authored
Merge branch 'main' into fix/skill-registry-deployment-auth
2 parents 5439601 + bd2cb0f commit 6d12fff

2 files changed

Lines changed: 420 additions & 15 deletions

File tree

lib/crewai/src/crewai/llms/providers/openai/completion.py

Lines changed: 92 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,14 @@
88
from typing import TYPE_CHECKING, Any, ClassVar, Literal, TypedDict
99

1010
import httpx
11-
from openai import APIConnectionError, AsyncOpenAI, NotFoundError, OpenAI, Stream
11+
from openai import (
12+
APIConnectionError,
13+
AsyncOpenAI,
14+
BadRequestError,
15+
NotFoundError,
16+
OpenAI,
17+
Stream,
18+
)
1219
from openai.lib.streaming.chat import ChatCompletionStream
1320
from openai.types.chat import (
1421
ChatCompletion,
@@ -506,27 +513,40 @@ def _call_completions(
506513
messages=messages, tools=tools
507514
)
508515

509-
try:
516+
def dispatch(params: dict[str, Any]) -> str | Any:
510517
if self._effective_stream():
511518
return self._handle_streaming_completion(
512-
params=completion_params,
519+
params=params,
513520
available_functions=available_functions,
514521
from_task=from_task,
515522
from_agent=from_agent,
516523
response_model=response_model,
517524
)
518-
519525
return self._handle_completion(
520-
params=completion_params,
526+
params=params,
521527
available_functions=available_functions,
522528
from_task=from_task,
523529
from_agent=from_agent,
524530
response_model=response_model,
525531
)
532+
533+
try:
534+
return dispatch(completion_params)
526535
except Exception as e:
527-
if self.custom_openai or not self._is_responses_only_error(
528-
e.__cause__ or e
529-
):
536+
cause = e.__cause__ or e
537+
538+
if self._rejects_reasoning_effort_with_tools(cause):
539+
retry_params = self._reasoning_effort_none_params(completion_params)
540+
if retry_params is not None:
541+
logging.debug(
542+
'Retrying %r with reasoning_effort="none": function tools '
543+
"and reasoning effort cannot be combined on "
544+
'/v1/chat/completions. Use api="responses" to keep both.',
545+
self.model,
546+
)
547+
return dispatch(retry_params)
548+
549+
if self.custom_openai or not self._is_responses_only_error(cause):
530550
raise
531551
self._remember_responses_only_model()
532552
logging.debug(
@@ -625,27 +645,34 @@ async def _acall_completions(
625645
messages=messages, tools=tools
626646
)
627647

628-
try:
648+
async def dispatch(params: dict[str, Any]) -> str | Any:
629649
if self._effective_stream():
630650
return await self._ahandle_streaming_completion(
631-
params=completion_params,
651+
params=params,
632652
available_functions=available_functions,
633653
from_task=from_task,
634654
from_agent=from_agent,
635655
response_model=response_model,
636656
)
637-
638657
return await self._ahandle_completion(
639-
params=completion_params,
658+
params=params,
640659
available_functions=available_functions,
641660
from_task=from_task,
642661
from_agent=from_agent,
643662
response_model=response_model,
644663
)
664+
665+
try:
666+
return await dispatch(completion_params)
645667
except Exception as e:
646-
if self.custom_openai or not self._is_responses_only_error(
647-
e.__cause__ or e
648-
):
668+
cause = e.__cause__ or e
669+
670+
if self._rejects_reasoning_effort_with_tools(cause):
671+
retry_params = self._reasoning_effort_none_params(completion_params)
672+
if retry_params is not None:
673+
return await dispatch(retry_params)
674+
675+
if self.custom_openai or not self._is_responses_only_error(cause):
649676
raise
650677
self._remember_responses_only_model()
651678
return await self._acall_responses(
@@ -1694,6 +1721,46 @@ def _model_not_found_message(self, error: Exception) -> str:
16941721
)
16951722
return f"Model {self.model} not found: {error}"
16961723

1724+
@staticmethod
1725+
def _rejects_reasoning_effort_with_tools(error: BaseException) -> bool:
1726+
"""Whether a 400 is OpenAI refusing `reasoning_effort` alongside tools.
1727+
1728+
GPT-5.6 applies a server-side `reasoning_effort` default and then rejects
1729+
it when function tools are present, so a payload carrying no
1730+
`reasoning_effort` at all still fails:
1731+
1732+
"Function tools with reasoning_effort are not supported for
1733+
gpt-5.6-sol in /v1/chat/completions. To use function tools, use
1734+
/v1/responses or set reasoning_effort to 'none'."
1735+
1736+
Matched on the structured `param` field plus the message so the unrelated
1737+
"Unsupported value" 400 that o1/o3 return for `reasoning_effort="none"`
1738+
doesn't look recoverable.
1739+
"""
1740+
if not isinstance(error, BadRequestError):
1741+
return False
1742+
body = getattr(error, "body", None)
1743+
source = None
1744+
if isinstance(body, dict):
1745+
inner = body.get("error")
1746+
source = inner if isinstance(inner, dict) else body
1747+
if not source or source.get("param") != "reasoning_effort":
1748+
return False
1749+
message = str(source.get("message") or "").lower()
1750+
return "function tools" in message and "reasoning_effort" in message
1751+
1752+
def _reasoning_effort_none_params(
1753+
self, params: dict[str, Any]
1754+
) -> dict[str, Any] | None:
1755+
"""Params with an explicit `reasoning_effort="none"`, or None if already set.
1756+
1757+
Removing the key is not enough: absence means "use the server default",
1758+
which is what the request was rejected for in the first place.
1759+
"""
1760+
if params.get("reasoning_effort") == "none":
1761+
return None
1762+
return {**params, "reasoning_effort": "none"}
1763+
16971764
def _effective_api(self) -> str:
16981765
"""Which OpenAI API to actually use for this model.
16991766
@@ -1960,6 +2027,11 @@ def _handle_completion(
19602027
logging.error(f"Context window exceeded: {e}")
19612028
raise LLMContextLengthExceededError(str(e)) from e
19622029

2030+
# `_call_completions` retries this one, so reporting a failed call
2031+
# here would surface an error the caller never experiences.
2032+
if self._rejects_reasoning_effort_with_tools(e):
2033+
raise
2034+
19632035
error_msg = f"OpenAI API call failed: {e!s}"
19642036
logging.error(error_msg)
19652037
self._emit_call_failed_event(
@@ -2383,6 +2455,11 @@ async def _ahandle_completion(
23832455
logging.error(f"Context window exceeded: {e}")
23842456
raise LLMContextLengthExceededError(str(e)) from e
23852457

2458+
# `_call_completions` retries this one, so reporting a failed call
2459+
# here would surface an error the caller never experiences.
2460+
if self._rejects_reasoning_effort_with_tools(e):
2461+
raise
2462+
23862463
error_msg = f"OpenAI API call failed: {e!s}"
23872464
logging.error(error_msg)
23882465
self._emit_call_failed_event(

0 commit comments

Comments
 (0)