Skip to content
Open
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
12 changes: 10 additions & 2 deletions backend/app/gateway/routers/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ class ModelResponse(BaseModel):
description: str | None = Field(None, description="Model description")
supports_thinking: bool = Field(default=False, description="Whether model supports thinking mode")
supports_reasoning_effort: bool = Field(default=False, description="Whether model supports reasoning effort")
reasoning_efforts: list[str] | None = Field(
default=None,
description="Provider-specific reasoning_effort values accepted by this model",
)


class TokenUsageResponse(BaseModel):
Expand Down Expand Up @@ -56,15 +60,17 @@ async def list_models(config: AppConfig = Depends(get_config)) -> ModelsListResp
"display_name": "GPT-4",
"description": "OpenAI GPT-4 model",
"supports_thinking": false,
"supports_reasoning_effort": false
"supports_reasoning_effort": false,
"reasoning_efforts": null
},
{
"name": "claude-3-opus",
"model": "claude-3-opus",
"display_name": "Claude 3 Opus",
"description": "Anthropic Claude 3 Opus model",
"supports_thinking": true,
"supports_reasoning_effort": false
"supports_reasoning_effort": false,
"reasoning_efforts": null
}
],
"token_usage": {
Expand All @@ -81,6 +87,7 @@ async def list_models(config: AppConfig = Depends(get_config)) -> ModelsListResp
description=model.description,
supports_thinking=model.supports_thinking,
supports_reasoning_effort=model.supports_reasoning_effort,
reasoning_efforts=model.reasoning_efforts,
)
for model in config.models
]
Expand Down Expand Up @@ -129,4 +136,5 @@ async def get_model(model_name: str, config: AppConfig = Depends(get_config)) ->
description=model.description,
supports_thinking=model.supports_thinking,
supports_reasoning_effort=model.supports_reasoning_effort,
reasoning_efforts=model.reasoning_efforts,
)
8 changes: 8 additions & 0 deletions backend/docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,18 +182,24 @@ GET /api/models
"name": "gpt-4",
"display_name": "GPT-4",
"supports_thinking": false,
"supports_reasoning_effort": false,
"reasoning_efforts": null,
"supports_vision": true
},
{
"name": "claude-3-opus",
"display_name": "Claude 3 Opus",
"supports_thinking": false,
"supports_reasoning_effort": false,
"reasoning_efforts": null,
"supports_vision": true
},
{
"name": "deepseek-v3",
"display_name": "DeepSeek V3",
"supports_thinking": true,
"supports_reasoning_effort": true,
"reasoning_efforts": ["low", "medium", "high", "max", "xhigh"],
"supports_vision": false
}
]
Expand All @@ -214,6 +220,8 @@ GET /api/models/{model_name}
"model": "gpt-4",
"max_tokens": 4096,
"supports_thinking": false,
"supports_reasoning_effort": false,
"reasoning_efforts": null,
"supports_vision": true
}
```
Expand Down
6 changes: 6 additions & 0 deletions backend/docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,18 @@ Some models support "thinking" mode for complex reasoning:
models:
- name: deepseek-v3
supports_thinking: true
supports_reasoning_effort: true
reasoning_efforts: [low, medium, high, max, xhigh]
when_thinking_enabled:
extra_body:
thinking:
type: enabled
```

Use `reasoning_efforts` when a provider supports reasoning effort but only accepts
a subset of DeerFlow's UI values. For example, omit `minimal` for providers that
reject it.

**Gemini with thinking via OpenAI-compatible gateway**:

When routing Gemini through an OpenAI-compatible proxy (Vertex AI OpenAI compat endpoint, AI Studio, or third-party gateways) with thinking enabled, the API attaches a `thought_signature` to each tool-call object returned in the response. Every subsequent request that replays those assistant messages **must** echo those signatures back on the tool-call entries or the API returns:
Expand Down
9 changes: 9 additions & 0 deletions backend/packages/harness/deerflow/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@
logger = logging.getLogger(__name__)


def _get_reasoning_efforts(model: Any) -> list[str] | None:
efforts = getattr(model, "reasoning_efforts", None)
if not isinstance(efforts, (list, tuple)):
return None
return [str(effort) for effort in efforts]


StreamEventType = Literal["values", "messages-tuple", "custom", "end"]


Expand Down Expand Up @@ -851,6 +858,7 @@ def list_models(self) -> dict:
"description": getattr(model, "description", None),
"supports_thinking": getattr(model, "supports_thinking", False),
"supports_reasoning_effort": getattr(model, "supports_reasoning_effort", False),
"reasoning_efforts": _get_reasoning_efforts(model),
}
for model in self._app_config.models
],
Expand Down Expand Up @@ -922,6 +930,7 @@ def get_model(self, name: str) -> dict | None:
"description": getattr(model, "description", None),
"supports_thinking": getattr(model, "supports_thinking", False),
"supports_reasoning_effort": getattr(model, "supports_reasoning_effort", False),
"reasoning_efforts": _get_reasoning_efforts(model),
}

# ------------------------------------------------------------------
Expand Down
4 changes: 4 additions & 0 deletions backend/packages/harness/deerflow/config/model_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ class ModelConfig(BaseModel):
)
supports_thinking: bool = Field(default_factory=lambda: False, description="Whether the model supports thinking")
supports_reasoning_effort: bool = Field(default_factory=lambda: False, description="Whether the model supports reasoning effort")
reasoning_efforts: list[str] | None = Field(
default=None,
description="Provider-specific reasoning_effort values accepted by this model",
)
when_thinking_enabled: dict | None = Field(
default_factory=lambda: None,
description="Extra settings to be passed to the model when thinking is enabled",
Expand Down
16 changes: 16 additions & 0 deletions backend/packages/harness/deerflow/models/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,18 @@ def _enable_stream_usage_by_default(model_use_path: str, model_settings_from_con
model_settings_from_config["stream_usage"] = True


def _configured_reasoning_efforts(model_config) -> set[str] | None:
if not model_config.reasoning_efforts:
return None
return {str(effort) for effort in model_config.reasoning_efforts}


def _remove_unsupported_reasoning_effort(settings: dict, allowed_efforts: set[str]) -> None:
effort = settings.get("reasoning_effort")
if effort is not None and str(effort) not in allowed_efforts:
settings.pop("reasoning_effort", None)


def create_chat_model(name: str | None = None, thinking_enabled: bool = False, *, app_config: AppConfig | None = None, attach_tracing: bool = True, **kwargs) -> BaseChatModel:
"""Create a chat model instance from the config.

Expand Down Expand Up @@ -85,6 +97,7 @@ def create_chat_model(name: str | None = None, thinking_enabled: bool = False, *
"description",
"supports_thinking",
"supports_reasoning_effort",
"reasoning_efforts",
"when_thinking_enabled",
"when_thinking_disabled",
"thinking",
Expand Down Expand Up @@ -126,6 +139,9 @@ def create_chat_model(name: str | None = None, thinking_enabled: bool = False, *
if not model_config.supports_reasoning_effort:
kwargs.pop("reasoning_effort", None)
model_settings_from_config.pop("reasoning_effort", None)
elif allowed_efforts := _configured_reasoning_efforts(model_config):
_remove_unsupported_reasoning_effort(kwargs, allowed_efforts)
_remove_unsupported_reasoning_effort(model_settings_from_config, allowed_efforts)

_enable_stream_usage_by_default(model_config.use, model_settings_from_config)

Expand Down
8 changes: 8 additions & 0 deletions backend/tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def mock_app_config():
model.model = "test-model"
model.supports_thinking = False
model.supports_reasoning_effort = False
model.reasoning_efforts = None
model.model_dump.return_value = {"name": "test-model", "use": "langchain_openai:ChatOpenAI"}

config = MagicMock()
Expand Down Expand Up @@ -130,6 +131,7 @@ def test_list_models(self, client):
assert "model" in result["models"][0]
assert "display_name" in result["models"][0]
assert "supports_thinking" in result["models"][0]
assert "reasoning_efforts" in result["models"][0]

def test_list_skills(self, client):
skill = MagicMock()
Expand Down Expand Up @@ -1015,6 +1017,7 @@ def test_found(self, client):
model_cfg.description = "A test model"
model_cfg.supports_thinking = True
model_cfg.supports_reasoning_effort = True
model_cfg.reasoning_efforts = None
client._app_config.get_model_config.return_value = model_cfg

result = client.get_model("test-model")
Expand All @@ -1025,6 +1028,7 @@ def test_found(self, client):
"description": "A test model",
"supports_thinking": True,
"supports_reasoning_effort": True,
"reasoning_efforts": None,
}

def test_not_found(self, client):
Expand Down Expand Up @@ -2315,6 +2319,7 @@ def test_list_models(self, mock_app_config):
model.description = "A test model"
model.supports_thinking = False
model.supports_reasoning_effort = False
model.reasoning_efforts = None
mock_app_config.models = [model]
mock_app_config.token_usage.enabled = True

Expand All @@ -2335,6 +2340,8 @@ def test_get_model(self, mock_app_config):
model.display_name = "Test Model"
model.description = "A test model"
model.supports_thinking = True
model.supports_reasoning_effort = True
model.reasoning_efforts = ["low", "medium", "high"]
mock_app_config.models = [model]
mock_app_config.get_model_config.return_value = model

Expand All @@ -2346,6 +2353,7 @@ def test_get_model(self, mock_app_config):
parsed = ModelResponse(**result)
assert parsed.name == "test-model"
assert parsed.model == "gpt-test"
assert parsed.reasoning_efforts == ["low", "medium", "high"]

def test_list_skills(self, client):
skill = MagicMock()
Expand Down
65 changes: 65 additions & 0 deletions backend/tests/test_model_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def _make_model(
use: str = "langchain_openai:ChatOpenAI",
supports_thinking: bool = False,
supports_reasoning_effort: bool = False,
reasoning_efforts: list[str] | None = None,
when_thinking_enabled: dict | None = None,
when_thinking_disabled: dict | None = None,
thinking: dict | None = None,
Expand All @@ -43,6 +44,7 @@ def _make_model(
max_tokens=max_tokens,
supports_thinking=supports_thinking,
supports_reasoning_effort=supports_reasoning_effort,
reasoning_efforts=reasoning_efforts,
when_thinking_enabled=when_thinking_enabled,
when_thinking_disabled=when_thinking_disabled,
thinking=thinking,
Expand Down Expand Up @@ -429,6 +431,69 @@ def __init__(self, **kwargs):
assert captured.get("reasoning_effort") == "minimal"


def test_runtime_reasoning_effort_removed_when_not_in_model_allowlist(monkeypatch):
cfg = _make_app_config(
[
_make_model(
"deepseek",
supports_thinking=True,
supports_reasoning_effort=True,
reasoning_efforts=["low", "medium", "high", "max", "xhigh"],
when_thinking_disabled={"extra_body": {"thinking": {"type": "disabled"}}},
)
]
)
_patch_factory(monkeypatch, cfg)

captured: dict = {}

class CapturingModel(FakeChatModel):
def __init__(self, **kwargs):
captured.update(kwargs)
BaseChatModel.__init__(self, **kwargs)

monkeypatch.setattr(factory_module, "resolve_class", lambda path, base: CapturingModel)

factory_module.create_chat_model(
name="deepseek",
thinking_enabled=False,
reasoning_effort="minimal",
)

assert captured.get("extra_body") == {"thinking": {"type": "disabled"}}
assert captured.get("reasoning_effort") is None


def test_runtime_reasoning_effort_preserved_when_in_model_allowlist(monkeypatch):
cfg = _make_app_config(
[
_make_model(
"deepseek",
supports_reasoning_effort=True,
reasoning_efforts=["low", "medium", "high", "max", "xhigh"],
)
]
)
_patch_factory(monkeypatch, cfg)

captured: dict = {}

class CapturingModel(FakeChatModel):
def __init__(self, **kwargs):
captured.update(kwargs)
BaseChatModel.__init__(self, **kwargs)

monkeypatch.setattr(factory_module, "resolve_class", lambda path, base: CapturingModel)

factory_module.create_chat_model(
name="deepseek",
thinking_enabled=True,
reasoning_effort="xhigh",
)

assert captured.get("reasoning_effort") == "xhigh"


# ---------------------------------------------------------------------------
# thinking shortcut field
# ---------------------------------------------------------------------------
Expand Down
3 changes: 3 additions & 0 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ models:
# supports_thinking: true
# supports_vision: true
# supports_reasoning_effort: true
# # Optional: restrict UI/backend reasoning_effort values to those this provider accepts.
# # For providers such as DeepSeek, omit "minimal" if their API rejects it.
# reasoning_efforts: [low, medium, high, max, xhigh]
# when_thinking_enabled:
# extra_body:
# thinking:
Expand Down
Loading
Loading