mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-09 12:40:59 +00:00
fix(provider): map qwen reasoning levels to budgets
This commit is contained in:
@@ -104,7 +104,12 @@ def validate_reasoning_capabilities(
|
|||||||
available_levels = capabilities.get('levels')
|
available_levels = capabilities.get('levels')
|
||||||
if not isinstance(available_levels, list):
|
if not isinstance(available_levels, list):
|
||||||
available_levels = []
|
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'
|
available_text = ', '.join(str(item) for item in available_levels) or 'provider_default'
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f'Reasoning level "{level}" is not supported by model {model_name}. Available levels: {available_text}'
|
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',
|
'qwen3.7-max-2026-05-17',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
_QWEN_REASONING_BUDGETS = {
|
||||||
|
'low': 1024,
|
||||||
|
'medium': 4096,
|
||||||
|
'high': 8192,
|
||||||
|
}
|
||||||
_INFERRED_EFFORT_PROVIDERS = frozenset(
|
_INFERRED_EFFORT_PROVIDERS = frozenset(
|
||||||
{
|
{
|
||||||
'anthropic',
|
'anthropic',
|
||||||
@@ -448,6 +453,12 @@ class LiteLLMRequester(requester.ProviderAPIRequester):
|
|||||||
or '-thinking' in normalized_name
|
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:
|
def _known_reasoning_levels(self, model_name: str, family: str) -> list[str] | None:
|
||||||
normalized_name = model_name.lower().rsplit('/', 1)[-1]
|
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 family == 'qwen' and normalized_name.startswith(('qwen-', 'qwen3', 'qwq')):
|
||||||
if self._is_dedicated_qwen_thinking_model(normalized_name):
|
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']
|
return ['provider_default']
|
||||||
|
if self._supports_qwen_thinking_budget(normalized_name):
|
||||||
|
return ['provider_default', 'disabled', 'low', 'medium', 'high']
|
||||||
return ['provider_default', 'disabled', 'enabled']
|
return ['provider_default', 'disabled', 'enabled']
|
||||||
|
|
||||||
if family == 'doubao' and normalized_name.startswith(('doubao-', 'seed-')):
|
if family == 'doubao' and normalized_name.startswith(('doubao-', 'seed-')):
|
||||||
@@ -570,11 +585,14 @@ class LiteLLMRequester(requester.ProviderAPIRequester):
|
|||||||
else:
|
else:
|
||||||
levels = ['provider_default']
|
levels = ['provider_default']
|
||||||
|
|
||||||
return {
|
capabilities = {
|
||||||
'supported': True,
|
'supported': True,
|
||||||
'levels': list(dict.fromkeys(levels)),
|
'levels': list(dict.fromkeys(levels)),
|
||||||
'source': 'litellm' if detected else ('provider' if known_levels is not None else 'manual'),
|
'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]:
|
def _build_reasoning_args(self, model: requester.RuntimeLLMModel) -> dict[str, typing.Any]:
|
||||||
level = self._reasoning_level(model)
|
level = self._reasoning_level(model)
|
||||||
@@ -605,6 +623,13 @@ class LiteLLMRequester(requester.ProviderAPIRequester):
|
|||||||
if family == 'qwen':
|
if family == 'qwen':
|
||||||
return {'extra_body': {'enable_thinking': True}}
|
return {'extra_body': {'enable_thinking': True}}
|
||||||
return {'reasoning_effort': 'low'}
|
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':
|
if family == 'deepseek':
|
||||||
return {
|
return {
|
||||||
'extra_body': {
|
'extra_body': {
|
||||||
|
|||||||
@@ -234,7 +234,46 @@ def test_qwen_mixed_and_dedicated_thinking_profiles(monkeypatch):
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert mixed['levels'] == ['provider_default', 'disabled', 'enabled']
|
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'])
|
@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=[]))
|
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(
|
@pytest.mark.parametrize(
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ export interface ReasoningConfig {
|
|||||||
export interface ReasoningCapabilities {
|
export interface ReasoningCapabilities {
|
||||||
supported: boolean;
|
supported: boolean;
|
||||||
levels: ReasoningLevel[];
|
levels: ReasoningLevel[];
|
||||||
|
legacy_levels?: ReasoningLevel[];
|
||||||
source: 'litellm' | 'provider' | 'manual' | 'unknown';
|
source: 'litellm' | 'provider' | 'manual' | 'unknown';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user