fix(provider): map qwen reasoning levels to budgets

This commit is contained in:
fdc310
2026-08-06 01:00:34 +08:00
parent 1f415459d9
commit d6f01adb8a
4 changed files with 74 additions and 4 deletions
@@ -104,7 +104,12 @@ def validate_reasoning_capabilities(
available_levels = capabilities.get('levels')
if not isinstance(available_levels, list):
available_levels = []
if capabilities.get('supported') is not True or level not in available_levels:
legacy_levels = capabilities.get('legacy_levels')
if not isinstance(legacy_levels, list):
legacy_levels = []
if capabilities.get('supported') is not True or (
level not in available_levels and level not in legacy_levels
):
available_text = ', '.join(str(item) for item in available_levels) or 'provider_default'
raise ValueError(
f'Reasoning level "{level}" is not supported by model {model_name}. Available levels: {available_text}'
@@ -170,6 +170,11 @@ class LiteLLMRequester(requester.ProviderAPIRequester):
'qwen3.7-max-2026-05-17',
}
)
_QWEN_REASONING_BUDGETS = {
'low': 1024,
'medium': 4096,
'high': 8192,
}
_INFERRED_EFFORT_PROVIDERS = frozenset(
{
'anthropic',
@@ -448,6 +453,12 @@ class LiteLLMRequester(requester.ProviderAPIRequester):
or '-thinking' in normalized_name
)
@staticmethod
def _supports_qwen_thinking_budget(model_name: str) -> bool:
"""Return whether the documented Qwen3 family supports thinking_budget."""
normalized_name = model_name.lower().rsplit('/', 1)[-1]
return normalized_name.startswith('qwen3')
def _known_reasoning_levels(self, model_name: str, family: str) -> list[str] | None:
normalized_name = model_name.lower().rsplit('/', 1)[-1]
@@ -470,7 +481,11 @@ class LiteLLMRequester(requester.ProviderAPIRequester):
if family == 'qwen' and normalized_name.startswith(('qwen-', 'qwen3', 'qwq')):
if self._is_dedicated_qwen_thinking_model(normalized_name):
if self._supports_qwen_thinking_budget(normalized_name):
return ['provider_default', 'low', 'medium', 'high']
return ['provider_default']
if self._supports_qwen_thinking_budget(normalized_name):
return ['provider_default', 'disabled', 'low', 'medium', 'high']
return ['provider_default', 'disabled', 'enabled']
if family == 'doubao' and normalized_name.startswith(('doubao-', 'seed-')):
@@ -570,11 +585,14 @@ class LiteLLMRequester(requester.ProviderAPIRequester):
else:
levels = ['provider_default']
return {
capabilities = {
'supported': True,
'levels': list(dict.fromkeys(levels)),
'source': 'litellm' if detected else ('provider' if known_levels is not None else 'manual'),
}
if family == 'qwen' and 'disabled' in capabilities['levels'] and 'enabled' not in capabilities['levels']:
capabilities['legacy_levels'] = ['enabled']
return capabilities
def _build_reasoning_args(self, model: requester.RuntimeLLMModel) -> dict[str, typing.Any]:
level = self._reasoning_level(model)
@@ -605,6 +623,13 @@ class LiteLLMRequester(requester.ProviderAPIRequester):
if family == 'qwen':
return {'extra_body': {'enable_thinking': True}}
return {'reasoning_effort': 'low'}
if family == 'qwen' and level in self._QWEN_REASONING_BUDGETS:
return {
'extra_body': {
'enable_thinking': True,
'thinking_budget': self._QWEN_REASONING_BUDGETS[level],
}
}
if family == 'deepseek':
return {
'extra_body': {
@@ -234,7 +234,46 @@ def test_qwen_mixed_and_dedicated_thinking_profiles(monkeypatch):
)
assert mixed['levels'] == ['provider_default', 'disabled', 'enabled']
assert dedicated['levels'] == ['provider_default']
assert dedicated['levels'] == ['provider_default', 'low', 'medium', 'high']
def test_qwen3_exposes_budget_based_reasoning_levels(monkeypatch):
request = _requester('openai', 'bailian-chat-completions')
monkeypatch.setattr(request, '_supports_reasoning', lambda _: False)
mixed = request.get_reasoning_capabilities(_runtime_model(request, name='qwen3.8-max', abilities=[]))
dedicated = request.get_reasoning_capabilities(
_runtime_model(request, name='qwen3.7-max-preview', abilities=[])
)
assert mixed['levels'] == ['provider_default', 'disabled', 'low', 'medium', 'high']
assert mixed['legacy_levels'] == ['enabled']
assert dedicated['levels'] == ['provider_default', 'low', 'medium', 'high']
def test_qwen3_legacy_enabled_config_remains_supported(monkeypatch):
request = _requester('openai', 'bailian-chat-completions')
monkeypatch.setattr(request, '_supports_reasoning', lambda _: False)
assert request._build_reasoning_args(_runtime_model(request, 'enabled', name='qwen3.8-max')) == {
'extra_body': {'enable_thinking': True}
}
@pytest.mark.parametrize(
('level', 'budget'),
[('low', 1024), ('medium', 4096), ('high', 8192)],
)
def test_qwen3_reasoning_levels_translate_to_thinking_budget(level, budget, monkeypatch):
request = _requester('openai', 'bailian-chat-completions')
monkeypatch.setattr(request, '_supports_reasoning', lambda _: False)
assert request._build_reasoning_args(_runtime_model(request, level, name='qwen3.8-max')) == {
'extra_body': {
'enable_thinking': True,
'thinking_budget': budget,
}
}
@pytest.mark.parametrize('model_name', ['qwen3.7-max-preview', 'qwen3.7-max-2026-05-17'])
@@ -244,7 +283,7 @@ def test_qwen_dedicated_thinking_release_models_are_not_toggleable(model_name, m
capabilities = request.get_reasoning_capabilities(_runtime_model(request, name=model_name, abilities=[]))
assert capabilities['levels'] == ['provider_default']
assert capabilities['levels'] == ['provider_default', 'low', 'medium', 'high']
@pytest.mark.parametrize(
+1
View File
@@ -122,6 +122,7 @@ export interface ReasoningConfig {
export interface ReasoningCapabilities {
supported: boolean;
levels: ReasoningLevel[];
legacy_levels?: ReasoningLevel[];
source: 'litellm' | 'provider' | 'manual' | 'unknown';
}