@@ -384,6 +384,104 @@ class Article(BaseModel):
384384 assert article_instance .title is None
385385
386386
387+ class TestOpenAIServiceReasoningModelFiltering :
388+ """Test that UiPathOpenAIService correctly filters out temperature for reasoning models."""
389+
390+ @pytest .fixture
391+ def config (self ):
392+ return UiPathApiConfig (base_url = "https://example.com" , secret = "test_secret" )
393+
394+ @pytest .fixture
395+ def execution_context (self ):
396+ return UiPathExecutionContext ()
397+
398+ @pytest .fixture
399+ def openai_service (self , config , execution_context ):
400+ return UiPathOpenAIService (config = config , execution_context = execution_context )
401+
402+ @pytest .mark .parametrize (
403+ "model" ,
404+ [
405+ "o3-mini-2025-01-31" ,
406+ "o4-mini-2025-04-16" ,
407+ "o1-2024-12-17" ,
408+ "o1-mini-2024-09-12" ,
409+ "o3-2025-04-16" ,
410+ ],
411+ )
412+ @patch .object (UiPathOpenAIService , "request_async" )
413+ @pytest .mark .asyncio
414+ async def test_reasoning_model_excludes_temperature (
415+ self , mock_request , openai_service , model
416+ ):
417+ """Test that reasoning models do not include temperature in the request body."""
418+ mock_response = MagicMock ()
419+ mock_response .json .return_value = {
420+ "id" : "chatcmpl-test" ,
421+ "object" : "chat.completion" ,
422+ "created" : 1234567890 ,
423+ "model" : model ,
424+ "choices" : [
425+ {
426+ "index" : 0 ,
427+ "message" : {"role" : "assistant" , "content" : "Hello" },
428+ "finish_reason" : "stop" ,
429+ }
430+ ],
431+ "usage" : {"prompt_tokens" : 10 , "completion_tokens" : 5 , "total_tokens" : 15 },
432+ }
433+ mock_request .return_value = mock_response
434+
435+ await openai_service .chat_completions (
436+ messages = [{"role" : "user" , "content" : "Hello" }],
437+ model = model ,
438+ max_tokens = 1000 ,
439+ )
440+
441+ call_kwargs = mock_request .call_args [1 ]
442+ request_body = call_kwargs ["json" ]
443+
444+ assert "temperature" not in request_body , (
445+ f"Reasoning model { model } request must not include 'temperature'"
446+ )
447+ assert request_body ["max_tokens" ] == 1000
448+
449+ @patch .object (UiPathOpenAIService , "request_async" )
450+ @pytest .mark .asyncio
451+ async def test_non_reasoning_model_includes_temperature (
452+ self , mock_request , openai_service
453+ ):
454+ """Test that non-reasoning models still include temperature."""
455+ mock_response = MagicMock ()
456+ mock_response .json .return_value = {
457+ "id" : "chatcmpl-test" ,
458+ "object" : "chat.completion" ,
459+ "created" : 1234567890 ,
460+ "model" : "gpt-4.1-mini-2025-04-14" ,
461+ "choices" : [
462+ {
463+ "index" : 0 ,
464+ "message" : {"role" : "assistant" , "content" : "Hello" },
465+ "finish_reason" : "stop" ,
466+ }
467+ ],
468+ "usage" : {"prompt_tokens" : 10 , "completion_tokens" : 5 , "total_tokens" : 15 },
469+ }
470+ mock_request .return_value = mock_response
471+
472+ await openai_service .chat_completions (
473+ messages = [{"role" : "user" , "content" : "Hello" }],
474+ model = "gpt-4.1-mini-2025-04-14" ,
475+ max_tokens = 1000 ,
476+ temperature = 0.7 ,
477+ )
478+
479+ call_kwargs = mock_request .call_args [1 ]
480+ request_body = call_kwargs ["json" ]
481+
482+ assert request_body ["temperature" ] == 0.7
483+
484+
387485class TestNormalizedLlmServiceClaudeFiltering :
388486 """Test that Claude models correctly filter out OpenAI-specific parameters.
389487
@@ -543,3 +641,128 @@ async def test_claude_sonnet_45_excluded_params(self, mock_request, llm_service)
543641 assert "presence_penalty" not in request_body
544642 assert "top_p" not in request_body
545643 assert request_body ["max_tokens" ] == 8000
644+
645+
646+ class TestNormalizedLlmServiceReasoningModelFiltering :
647+ """Test that reasoning models (o1, o3, o4) correctly filter out unsupported sampling parameters.
648+
649+ OpenAI reasoning models do NOT support temperature, top_p, frequency_penalty,
650+ presence_penalty, or n, and sending them causes 400 errors.
651+ """
652+
653+ @pytest .fixture
654+ def config (self ):
655+ return UiPathApiConfig (base_url = "https://example.com" , secret = "test_secret" )
656+
657+ @pytest .fixture
658+ def execution_context (self ):
659+ return UiPathExecutionContext ()
660+
661+ @pytest .fixture
662+ def llm_service (self , config , execution_context ):
663+ from uipath .platform .chat ._llm_gateway_service import UiPathLlmChatService
664+
665+ return UiPathLlmChatService (config = config , execution_context = execution_context )
666+
667+ @pytest .mark .parametrize (
668+ "model" ,
669+ [
670+ "o3-mini-2025-01-31" ,
671+ "o4-mini-2025-04-16" ,
672+ "o1-2024-12-17" ,
673+ "o1-mini-2024-09-12" ,
674+ "o3-2025-04-16" ,
675+ ],
676+ )
677+ @patch (
678+ "uipath.platform.chat._llm_gateway_service.UiPathLlmChatService.request_async"
679+ )
680+ @pytest .mark .asyncio
681+ async def test_reasoning_model_excludes_sampling_params (
682+ self , mock_request , llm_service , model
683+ ):
684+ """Test that reasoning models do not include temperature or other sampling params."""
685+ mock_response = MagicMock ()
686+ mock_response .status_code = 200
687+ mock_response .json .return_value = {
688+ "id" : "chatcmpl-test" ,
689+ "object" : "chat.completion" ,
690+ "created" : 1234567890 ,
691+ "model" : model ,
692+ "choices" : [
693+ {
694+ "index" : 0 ,
695+ "message" : {"role" : "assistant" , "content" : "Hello" },
696+ "finish_reason" : "stop" ,
697+ }
698+ ],
699+ "usage" : {"prompt_tokens" : 10 , "completion_tokens" : 5 , "total_tokens" : 15 },
700+ }
701+ mock_request .return_value = mock_response
702+
703+ await llm_service .chat_completions (
704+ messages = [{"role" : "user" , "content" : "Hello" }],
705+ model = model ,
706+ max_tokens = 1000 ,
707+ )
708+
709+ call_kwargs = mock_request .call_args [1 ]
710+ request_body = call_kwargs ["json" ]
711+
712+ assert "temperature" not in request_body , (
713+ f"Reasoning model { model } request must not include 'temperature'"
714+ )
715+ assert "n" not in request_body , (
716+ f"Reasoning model { model } request must not include 'n'"
717+ )
718+ assert "frequency_penalty" not in request_body , (
719+ f"Reasoning model { model } request must not include 'frequency_penalty'"
720+ )
721+ assert "presence_penalty" not in request_body , (
722+ f"Reasoning model { model } request must not include 'presence_penalty'"
723+ )
724+ assert "top_p" not in request_body , (
725+ f"Reasoning model { model } request must not include 'top_p'"
726+ )
727+ assert request_body ["max_tokens" ] == 1000
728+
729+ @patch (
730+ "uipath.platform.chat._llm_gateway_service.UiPathLlmChatService.request_async"
731+ )
732+ @pytest .mark .asyncio
733+ async def test_non_reasoning_model_includes_temperature (
734+ self , mock_request , llm_service
735+ ):
736+ """Test that non-reasoning models still include temperature and sampling params."""
737+ mock_response = MagicMock ()
738+ mock_response .status_code = 200
739+ mock_response .json .return_value = {
740+ "id" : "chatcmpl-test" ,
741+ "object" : "chat.completion" ,
742+ "created" : 1234567890 ,
743+ "model" : "gpt-4.1-mini-2025-04-14" ,
744+ "choices" : [
745+ {
746+ "index" : 0 ,
747+ "message" : {"role" : "assistant" , "content" : "Hello" },
748+ "finish_reason" : "stop" ,
749+ }
750+ ],
751+ "usage" : {"prompt_tokens" : 10 , "completion_tokens" : 5 , "total_tokens" : 15 },
752+ }
753+ mock_request .return_value = mock_response
754+
755+ await llm_service .chat_completions (
756+ messages = [{"role" : "user" , "content" : "Hello" }],
757+ model = "gpt-4.1-mini-2025-04-14" ,
758+ max_tokens = 1000 ,
759+ temperature = 0.5 ,
760+ )
761+
762+ call_kwargs = mock_request .call_args [1 ]
763+ request_body = call_kwargs ["json" ]
764+
765+ assert request_body ["temperature" ] == 0.5
766+ assert "n" in request_body
767+ assert "frequency_penalty" in request_body
768+ assert "presence_penalty" in request_body
0 commit comments