fix(managed-models): map DeepSeek "minimal" effort to "low" (#6717)

The official DeepSeek Chat Completions endpoint rejects the generic
"minimal" effort token (issue #4514). The declared effort contract for
DeepSeek models now advertises the provider's real vocabulary
(low/medium/high/max/xhigh), defaults to "low", and aliases the generic
"minimal" preset onto "low" so the "flash" dialogue mode serializes a
value the endpoint accepts.

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
yangfucheng123 2026-10-19 15:45:56 +08:00 committed by GitHub
parent 5a9a8042b4
commit 3aa144a377
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 32 additions and 2 deletions

View File

@ -95,7 +95,7 @@ def _capabilities(provider: str, model: str) -> dict[str, Any]:
when_thinking_enabled={"extra_body": {"thinking": {"type": "enabled"}}},
when_thinking_disabled={"extra_body": {"thinking": {"type": "disabled"}}},
)
result["reasoning"]["effort"] = {"values": ["high", "max"], "default": "high", "aliases": {"minimal": "high", "low": "high", "medium": "high"}}
result["reasoning"]["effort"] = {"values": ["low", "medium", "high", "max", "xhigh"], "default": "low", "aliases": {"minimal": "low"}}
elif provider in {"zai", "zhipu-cn"} and model in {"glm-4.5", "glm-4.6", "glm-4.7", "glm-5.3-flash"}:
result.update(
reasoning={"thinking": "optional", "dialect": "openai_extra_body", "history": "preserve"},

View File

@ -218,7 +218,7 @@ Direct `AppConfig.from_file()` continues to read only operator configuration.
`config/managed_model_providers.py` is the sole registry for provider IDs, labels,
endpoints, protocols, authentication parameter mappings, adapters and request
defaults. Capability rules match exact verified model IDs; unknown preset IDs
have an unsupported-thinking contract, no vision and no inferred context window.
have an unsupported-thinking contract, no vision and no inferred context window. Verified DeepSeek presets declare an effort vocabulary of `low`/`medium`/`high`/`max`/`xhigh` (default `low`) and alias the generic `minimal` preset onto `low`, so a saved per-agent `minimal` default serializes as a value the official endpoint accepts (issue #4514).
Preset drafts cannot supply endpoints or adapter paths. Legacy custom
`openai-compatible` records retain endpoint-based official DeepSeek detection;
never infer that adapter from a model-name substring. Reads do not migrate the

View File

@ -212,6 +212,36 @@ async def test_saved_model_streaming_replays_reasoning_and_honors_token_limit(st
assert model_config.supports_reasoning_effort is True
@pytest.mark.asyncio
@pytest.mark.parametrize(
"effort,expected",
[("minimal", "low"), ("low", "low"), ("medium", "medium"), ("high", "high"), ("max", "max"), ("xhigh", "xhigh")],
)
async def test_deepseek_effort_contract_maps_generic_effort_values(store, provider, effort, expected):
saved = store.save(ManagedModel(provider="deepseek", model="deepseek-flash", api_key=_KEY), expected_revision=None)
config = merge_managed_models(_base_config())
model_config = config.get_model_config(saved.name)
# The official endpoint rejects the generic "minimal" token (issue #4514),
# so the declared contract advertises the provider's real vocabulary and
# aliases "minimal" onto "low". Parametrized over every accepted value so
# a future rename/drop fails here, not at request time.
assert model_config.reasoning is not None
assert model_config.reasoning.effort is not None
assert model_config.reasoning.effort.values == ["low", "medium", "high", "max", "xhigh"]
assert model_config.reasoning.effort.default == "low"
assert model_config.reasoning.effort.aliases == {"minimal": "low"}
assert model_config.supports_reasoning_effort is True
model = create_chat_model(saved.name, thinking_enabled=False, reasoning_effort=effort, app_config=config, attach_tracing=False)
await _collect(model, [HumanMessage(content="Hello")])
assert len(provider.requests) == 1
body = provider.requests[0][1]
assert body["reasoning_effort"] == expected
assert body["thinking"] == {"type": "disabled"}
@pytest.mark.parametrize(
"base_url",
[