diff --git a/agent/auxiliary_client.py b/agent/auxiliary_client.py index 5e8a60e7657d..b78a71c595ab 100644 --- a/agent/auxiliary_client.py +++ b/agent/auxiliary_client.py @@ -2796,6 +2796,19 @@ def _build_call_kwargs( return kwargs +def _is_unsupported_temperature_error(exc: Exception) -> bool: + """True when a provider rejects the temperature request parameter. + + Some OpenAI-compatible endpoints (notably Codex Responses-compatible + gateways) reject ``temperature`` with messages like + ``Unsupported parameter: temperature`` instead of the newer + ``unsupported_parameter`` code. Auxiliary callers frequently pass a + conservative temperature for generic models, so retry once without it. + """ + msg = str(exc).lower() + return "temperature" in msg and ("unsupported" in msg or "not support" in msg) + + def _validate_llm_response(response: Any, task: str = None) -> Any: """Validate that an LLM response has the expected .choices[0].message shape. @@ -2958,6 +2971,19 @@ def call_llm( client.chat.completions.create(**kwargs), task) except Exception as first_err: err_str = str(first_err) + if _is_unsupported_temperature_error(first_err) and "temperature" in kwargs: + kwargs.pop("temperature", None) + try: + return _validate_llm_response( + client.chat.completions.create(**kwargs), task) + except Exception as retry_err: + # If the temperature retry also hits a payment or connection + # error, fall through to the fallback chain below. + if not (_is_payment_error(retry_err) or _is_connection_error(retry_err)): + raise + first_err = retry_err + err_str = str(first_err) + if "max_tokens" in err_str or "unsupported_parameter" in err_str: kwargs.pop("max_tokens", None) kwargs["max_completion_tokens"] = max_tokens @@ -3222,6 +3248,19 @@ async def async_call_llm( await client.chat.completions.create(**kwargs), task) except Exception as first_err: err_str = str(first_err) + if _is_unsupported_temperature_error(first_err) and "temperature" in kwargs: + kwargs.pop("temperature", None) + try: + return _validate_llm_response( + await client.chat.completions.create(**kwargs), task) + except Exception as retry_err: + # If the temperature retry also hits a payment or connection + # error, fall through to the fallback chain below. + if not (_is_payment_error(retry_err) or _is_connection_error(retry_err)): + raise + first_err = retry_err + err_str = str(first_err) + if "max_tokens" in err_str or "unsupported_parameter" in err_str: kwargs.pop("max_tokens", None) kwargs["max_completion_tokens"] = max_tokens diff --git a/tests/agent/test_auxiliary_client.py b/tests/agent/test_auxiliary_client.py index 5ee0f1265caa..136cb76e9fa7 100644 --- a/tests/agent/test_auxiliary_client.py +++ b/tests/agent/test_auxiliary_client.py @@ -863,6 +863,79 @@ def test_500_not_connection(self): assert _is_connection_error(err) is False +class TestUnsupportedTemperatureRetry: + """Auxiliary calls retry once without temperature when endpoints reject it.""" + + def test_sync_call_retries_without_temperature(self): + client = MagicMock() + client.base_url = "https://chatgpt.com/backend-api/codex" + response = MagicMock() + client.chat.completions.create.side_effect = [ + Exception("HTTP 400: Error code: 400 - {'detail': 'Unsupported parameter: temperature'}"), + response, + ] + + with patch( + "agent.auxiliary_client._get_cached_client", + return_value=(client, "gpt-5.5"), + ), patch( + "agent.auxiliary_client._resolve_task_provider_model", + return_value=("auto", "gpt-5.5", None, None, None), + ), patch( + "agent.auxiliary_client._validate_llm_response", + side_effect=lambda resp, _task: resp, + ): + result = call_llm( + task="flush_memories", + messages=[{"role": "user", "content": "hello"}], + temperature=0.3, + max_tokens=5120, + ) + + assert result is response + assert client.chat.completions.create.call_count == 2 + first_kwargs = client.chat.completions.create.call_args_list[0].kwargs + second_kwargs = client.chat.completions.create.call_args_list[1].kwargs + assert first_kwargs["temperature"] == 0.3 + assert "temperature" not in second_kwargs + assert second_kwargs["max_tokens"] == 5120 + + @pytest.mark.asyncio + async def test_async_call_retries_without_temperature(self): + client = MagicMock() + client.base_url = "https://chatgpt.com/backend-api/codex" + response = MagicMock() + client.chat.completions.create = AsyncMock(side_effect=[ + Exception("HTTP 400: Error code: 400 - {'detail': 'Unsupported parameter: temperature'}"), + response, + ]) + + with patch( + "agent.auxiliary_client._get_cached_client", + return_value=(client, "gpt-5.5"), + ), patch( + "agent.auxiliary_client._resolve_task_provider_model", + return_value=("auto", "gpt-5.5", None, None, None), + ), patch( + "agent.auxiliary_client._validate_llm_response", + side_effect=lambda resp, _task: resp, + ): + result = await async_call_llm( + task="session_search", + messages=[{"role": "user", "content": "hello"}], + temperature=0.3, + max_tokens=5120, + ) + + assert result is response + assert client.chat.completions.create.await_count == 2 + first_kwargs = client.chat.completions.create.call_args_list[0].kwargs + second_kwargs = client.chat.completions.create.call_args_list[1].kwargs + assert first_kwargs["temperature"] == 0.3 + assert "temperature" not in second_kwargs + assert second_kwargs["max_tokens"] == 5120 + + class TestKimiTemperatureOmitted: """Kimi/Moonshot models should have temperature OMITTED from API kwargs.