diff --git a/src/clean_agent_preview.py b/src/clean_agent_preview.py index a645a7f23..47b443ac4 100644 --- a/src/clean_agent_preview.py +++ b/src/clean_agent_preview.py @@ -184,7 +184,8 @@ def search_tool_choice_request(request): def provider_compatible_tool_choice_request(request, model): """Keep tools but avoid forced choice unsupported by thinking providers.""" - if canonical(model) == 'deepseek-flash' and 'tool_choice' in request: + model_name = canonical(str(model or '')).casefold() + if model_name.startswith('deepseek') and 'tool_choice' in request: compatible = dict(request) choice = compatible.get('tool_choice') selected_name = ( @@ -200,6 +201,23 @@ def provider_compatible_tool_choice_request(request, model): compatible['tools'] = selected compatible.pop('tool_choice', None) return compatible + if ( + request.get('tool_choice') == 'required' + and len(request.get('tools') or []) == 1 + and ('qwen' in model_name or model_name.startswith('odysseus-')) + ): + # Raw-policy capture accepts a named tool constraint (and records that + # turn as excluded from policy loss), but deliberately rejects the + # distribution-wide ``required`` mode. Search recovery narrows the + # schema to one tool before reaching this boundary, so preserving that + # exact name has the same runtime intent without a transport failure. + compatible = dict(request) + name = request['tools'][0]['function']['name'] + compatible['tool_choice'] = { + 'type': 'function', + 'function': {'name': name}, + } + return compatible return request diff --git a/tests/test_clean_agent_preview.py b/tests/test_clean_agent_preview.py index ad58af1d4..d2b0defa0 100644 --- a/tests/test_clean_agent_preview.py +++ b/tests/test_clean_agent_preview.py @@ -114,6 +114,29 @@ def test_deepseek_flash_keeps_tools_but_drops_unsupported_forced_choice(): assert provider_compatible_tool_choice_request(request, 'qwen3.5-9b') is request +def test_deepseek_v4_flash_drops_named_choice_for_thinking_mode(): + request = { + 'tools': [{'type': 'function', 'function': {'name': 'write_file'}}], + 'tool_choice': {'type': 'function', 'function': {'name': 'write_file'}}, + } + compatible = provider_compatible_tool_choice_request(request, 'deepseek-v4-flash') + assert [tool['function']['name'] for tool in compatible['tools']] == ['write_file'] + assert 'tool_choice' not in compatible + + +def test_qwen_capture_converts_single_required_tool_to_named_constraint(): + request = { + 'tools': [{'type': 'function', 'function': {'name': 'web_search'}}], + 'tool_choice': 'required', + } + compatible = provider_compatible_tool_choice_request( + request, 'odysseus-qwen3.5-tools-pre-heretic' + ) + assert compatible['tool_choice'] == { + 'type': 'function', 'function': {'name': 'web_search'}, + } + + def test_private_browser_observations_do_not_advance_page_revision(): assert private_browser_state_transition({'action': 'snapshot'}, 'https://example.org') == ( False, 'https://example.org')