diff --git a/models/openai/models/common_openai.py b/models/openai/models/common_openai.py index cc0ff00e5..26c114888 100644 --- a/models/openai/models/common_openai.py +++ b/models/openai/models/common_openai.py @@ -1,5 +1,6 @@ from collections.abc import Generator, Iterable, Mapping import hashlib +import re from typing import TypeVar import openai @@ -19,10 +20,42 @@ def _user_digest(user: str) -> str: return hashlib.sha256(user.encode()).hexdigest() +def _chat_completions_tools_reasoning_guidance(error: Exception) -> str | None: + if not isinstance(error, openai.BadRequestError): + return None + + body = error.body + if isinstance(body, Mapping): + nested_error = body.get("error") + if isinstance(nested_error, Mapping): + body = nested_error + body_message = body.get("message") + else: + body_message = None + + message = body_message if isinstance(body_message, str) else str(error) + normalized_message = message.lower() + if not all( + token in normalized_message + for token in ("function tools", "reasoning_effort", "/v1/chat/completions") + ): + return None + + model_match = re.search(r"\bgpt-(\d+(?:\.\d+)*)", message, re.IGNORECASE) + model_label = f"GPT-{model_match.group(1)}" if model_match else "The selected model" + return ( + f"{model_label} cannot use tools with reasoning enabled through the Chat Completions API.\n" + "To fix this, open the model's API Key Authorization Configuration, change API Protocol " + "from Chat Completions to Responses, save the configuration, and try again." + ) + + class _CommonOpenAI: def _transform_invoke_error(self, error: Exception) -> InvokeError: if isinstance(error, InvokeError): return error + if guidance := _chat_completions_tools_reasoning_guidance(error): + return InvokeBadRequestError(guidance) return super()._transform_invoke_error(error) def _to_credential_kwargs(self, credentials: Mapping) -> dict: diff --git a/models/openai/tests/test_routing.py b/models/openai/tests/test_routing.py index af6c02734..5d165582c 100644 --- a/models/openai/tests/test_routing.py +++ b/models/openai/tests/test_routing.py @@ -2,6 +2,8 @@ import hashlib +import httpx +import openai import pytest from dify_plugin.entities.model.message import PromptMessageTool @@ -10,6 +12,29 @@ from models.llm.llm import _base_model, _uses_responses +_CHAT_TOOLS_REASONING_ERROR = ( + "Function tools with reasoning_effort are not supported for gpt-5.6-sol in " + "/v1/chat/completions. To use function tools, use /v1/responses or set " + "reasoning_effort to 'none'." +) +_CHAT_TOOLS_REASONING_GUIDANCE = ( + "GPT-5.6 cannot use tools with reasoning enabled through the Chat Completions API.\n" + "To fix this, open the model's API Key Authorization Configuration, change API Protocol " + "from Chat Completions to Responses, save the configuration, and try again." +) + + +def _bad_request_error(body: object) -> openai.BadRequestError: + return openai.BadRequestError( + "Error code: 400", + response=httpx.Response( + 400, + request=httpx.Request("POST", "https://api.openai.com/v1/chat/completions"), + ), + body=body, + ) + + def test_reasoning_parameters_are_merged_without_mutating_the_caller() -> None: source = { "reasoning": {"effort": "low"}, @@ -310,3 +335,43 @@ def broken_stream(): assert caught.value is transformed transform.assert_called_once_with(source) + + +@pytest.mark.parametrize( + "body", + [ + { + "message": _CHAT_TOOLS_REASONING_ERROR, + "type": "invalid_request_error", + "param": "reasoning_effort", + }, + { + "error": { + "message": _CHAT_TOOLS_REASONING_ERROR, + "type": "invalid_request_error", + "param": "reasoning_effort", + } + }, + ], +) +def test_chat_tools_reasoning_error_has_actionable_protocol_guidance(llm, body) -> None: + transformed = llm._transform_invoke_error(_bad_request_error(body)) + + assert isinstance(transformed, InvokeBadRequestError) + assert str(transformed) == _CHAT_TOOLS_REASONING_GUIDANCE + + +def test_unrelated_bad_request_keeps_standard_error_mapping(llm) -> None: + transformed = llm._transform_invoke_error( + _bad_request_error( + { + "message": "Unsupported parameter: temperature", + "type": "invalid_request_error", + "param": "temperature", + } + ) + ) + + assert isinstance(transformed, InvokeBadRequestError) + assert "Bad Request Error" in str(transformed) + assert "API Key Authorization Configuration" not in str(transformed)