Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions models/openai/models/common_openai.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from collections.abc import Generator, Iterable, Mapping
import hashlib
import re
from typing import TypeVar

import openai
Expand All @@ -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:
Expand Down
65 changes: 65 additions & 0 deletions models/openai/tests/test_routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import hashlib

import httpx
import openai
import pytest

from dify_plugin.entities.model.message import PromptMessageTool
Expand All @@ -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"},
Expand Down Expand Up @@ -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)
Loading