From a94b52ee144e6e1f5a54ebc85d548bf5f860626b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 22:12:08 +0000 Subject: [PATCH] feat(ai): handle temperature incompatibility for gpt-5 and o-series models, add model picker UI Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> --- app/utils/ai_provider.py | 140 ++++++++++++++++++++----------- app/utils/settings_service.py | 50 ++++++++++- frontend/templates/settings.html | 23 +++++ tests/test_ai_provider.py | 72 ++++++++++++++++ 4 files changed, 234 insertions(+), 51 deletions(-) diff --git a/app/utils/ai_provider.py b/app/utils/ai_provider.py index fa9353b0..78644809 100644 --- a/app/utils/ai_provider.py +++ b/app/utils/ai_provider.py @@ -11,6 +11,7 @@ See the Configuration Guide for full details on each provider's settings. """ import logging +import re from abc import ABC, abstractmethod from typing import Any, Dict, List, Optional @@ -19,6 +20,50 @@ from app.config import settings logger = logging.getLogger(__name__) +def _resolve_temperature(model: str, requested: float) -> Optional[float]: + """Return a temperature value compatible with the given model, or ``None`` to omit it. + + Certain model families have restrictions on the ``temperature`` parameter: + + * **o-series reasoning models** (``o1``, ``o3``, ``o4``, …) – do not accept + a ``temperature`` argument at all. Return ``None`` so callers can skip the + parameter entirely. + * **gpt-5 family** (``gpt-5``, ``gpt-5-nano``, ``gpt-5-codex``, …) – only + ``temperature=1`` is accepted; passing ``0`` raises a 400 error. Return + ``1`` and emit a debug log so the caller is aware of the coercion. + * All other models – return the requested value unchanged. + + The model string may include a provider prefix (e.g. ``openai/gpt-4o``); + only the part after the last ``/`` is examined. + + Args: + model: Model identifier (may include a provider prefix). + requested: The temperature the caller wants to use. + + Returns: + A compatible temperature float, or ``None`` if temperature should be + omitted from the API call. + """ + bare = model.lower().split("/")[-1] + + # o-series reasoning models (o1, o3, o4 …) do not support temperature + if re.match(r"^o\d+(-|$)", bare): + logger.debug("Dropping temperature parameter for reasoning model '%s' (not supported)", model) + return None + + # gpt-5 family only supports temperature=1 + if bare.startswith("gpt-5"): + if requested != 1.0: + logger.debug( + "Coercing temperature from %s to 1 for model '%s' (only temperature=1 is supported)", + requested, + model, + ) + return 1.0 + + return requested + + def _require_text_content(content: Optional[str]) -> str: """Raise a clear error if the AI response contains no text content. @@ -101,12 +146,12 @@ class OpenAIProvider(AIProvider): temperature: float = 0, **kwargs: Any, ) -> str: - completion = self._client.chat.completions.create( - model=model, - messages=messages, - temperature=temperature, - **kwargs, - ) + call_kwargs: Dict[str, Any] = {"model": model, "messages": messages} + safe_temp = _resolve_temperature(model, temperature) + if safe_temp is not None: + call_kwargs["temperature"] = safe_temp + call_kwargs.update(kwargs) + completion = self._client.chat.completions.create(**call_kwargs) _content = completion.choices[0].message.content return _require_text_content(_content) @@ -130,12 +175,12 @@ class AzureOpenAIProvider(AIProvider): temperature: float = 0, **kwargs: Any, ) -> str: - completion = self._client.chat.completions.create( - model=model, - messages=messages, - temperature=temperature, - **kwargs, - ) + call_kwargs: Dict[str, Any] = {"model": model, "messages": messages} + safe_temp = _resolve_temperature(model, temperature) + if safe_temp is not None: + call_kwargs["temperature"] = safe_temp + call_kwargs.update(kwargs) + completion = self._client.chat.completions.create(**call_kwargs) _content = completion.choices[0].message.content return _require_text_content(_content) @@ -161,13 +206,12 @@ class AnthropicProvider(AIProvider): import litellm model_name = model if model.startswith("anthropic/") else f"anthropic/{model}" - response = litellm.completion( - model=model_name, - messages=messages, - temperature=temperature, - api_key=self._api_key, - **kwargs, - ) + call_kwargs: Dict[str, Any] = {"model": model_name, "messages": messages, "api_key": self._api_key} + safe_temp = _resolve_temperature(model, temperature) + if safe_temp is not None: + call_kwargs["temperature"] = safe_temp + call_kwargs.update(kwargs) + response = litellm.completion(**call_kwargs) _content = response.choices[0].message.content return _require_text_content(_content) @@ -193,13 +237,12 @@ class GeminiProvider(AIProvider): import litellm model_name = model if model.startswith("gemini/") else f"gemini/{model}" - response = litellm.completion( - model=model_name, - messages=messages, - temperature=temperature, - api_key=self._api_key, - **kwargs, - ) + call_kwargs: Dict[str, Any] = {"model": model_name, "messages": messages, "api_key": self._api_key} + safe_temp = _resolve_temperature(model, temperature) + if safe_temp is not None: + call_kwargs["temperature"] = safe_temp + call_kwargs.update(kwargs) + response = litellm.completion(**call_kwargs) _content = response.choices[0].message.content return _require_text_content(_content) @@ -235,12 +278,12 @@ class OllamaProvider(AIProvider): temperature: float = 0, **kwargs: Any, ) -> str: - completion = self._client.chat.completions.create( - model=model, - messages=messages, - temperature=temperature, - **kwargs, - ) + call_kwargs: Dict[str, Any] = {"model": model, "messages": messages} + safe_temp = _resolve_temperature(model, temperature) + if safe_temp is not None: + call_kwargs["temperature"] = safe_temp + call_kwargs.update(kwargs) + completion = self._client.chat.completions.create(**call_kwargs) _content = completion.choices[0].message.content return _require_text_content(_content) @@ -269,12 +312,12 @@ class OpenRouterProvider(AIProvider): temperature: float = 0, **kwargs: Any, ) -> str: - completion = self._client.chat.completions.create( - model=model, - messages=messages, - temperature=temperature, - **kwargs, - ) + call_kwargs: Dict[str, Any] = {"model": model, "messages": messages} + safe_temp = _resolve_temperature(model, temperature) + if safe_temp is not None: + call_kwargs["temperature"] = safe_temp + call_kwargs.update(kwargs) + completion = self._client.chat.completions.create(**call_kwargs) _content = completion.choices[0].message.content return _require_text_content(_content) @@ -335,12 +378,12 @@ class PortkeyProvider(AIProvider): temperature: float = 0, **kwargs: Any, ) -> str: - completion = self._client.chat.completions.create( - model=model, - messages=messages, - temperature=temperature, - **kwargs, - ) + call_kwargs: Dict[str, Any] = {"model": model, "messages": messages} + safe_temp = _resolve_temperature(model, temperature) + if safe_temp is not None: + call_kwargs["temperature"] = safe_temp + call_kwargs.update(kwargs) + completion = self._client.chat.completions.create(**call_kwargs) _content = completion.choices[0].message.content return _require_text_content(_content) @@ -371,11 +414,10 @@ class LiteLLMProvider(AIProvider): ) -> str: import litellm - completion_kwargs: Dict[str, Any] = { - "model": model, - "messages": messages, - "temperature": temperature, - } + completion_kwargs: Dict[str, Any] = {"model": model, "messages": messages} + safe_temp = _resolve_temperature(model, temperature) + if safe_temp is not None: + completion_kwargs["temperature"] = safe_temp if self._api_key: completion_kwargs["api_key"] = self._api_key if self._api_base: diff --git a/app/utils/settings_service.py b/app/utils/settings_service.py index 5365d7d7..49c8b14b 100644 --- a/app/utils/settings_service.py +++ b/app/utils/settings_service.py @@ -153,10 +153,33 @@ SETTING_METADATA = { "openai_model": { "category": "AI Services", "description": "Fallback model name used when AI_MODEL is not set (e.g. gpt-4o-mini)", - "type": "string", + "type": "model_picker", "sensitive": False, "required": False, "restart_required": False, + "suggested_models": [ + "gpt-4o", + "gpt-4o-mini", + "gpt-4-turbo", + "gpt-4", + "gpt-3.5-turbo", + "o1", + "o1-mini", + "o3", + "o3-mini", + "gpt-5", + "gpt-5-nano", + "claude-3-5-sonnet-20241022", + "claude-3-5-haiku-20241022", + "claude-3-opus-20240229", + "gemini-1.5-pro", + "gemini-1.5-flash", + "gemini-2.0-flash-exp", + "llama3.2", + "qwen2.5:7b", + "phi3", + "mistral", + ], }, "ai_provider": { "category": "AI Services", @@ -170,10 +193,33 @@ SETTING_METADATA = { "ai_model": { "category": "AI Services", "description": "Model name for the selected provider (overrides OPENAI_MODEL). E.g. gpt-4o, claude-3-5-sonnet-20241022, gemini-1.5-pro, llama3.2", - "type": "string", + "type": "model_picker", "sensitive": False, "required": False, "restart_required": False, + "suggested_models": [ + "gpt-4o", + "gpt-4o-mini", + "gpt-4-turbo", + "gpt-4", + "gpt-3.5-turbo", + "o1", + "o1-mini", + "o3", + "o3-mini", + "gpt-5", + "gpt-5-nano", + "claude-3-5-sonnet-20241022", + "claude-3-5-haiku-20241022", + "claude-3-opus-20240229", + "gemini-1.5-pro", + "gemini-1.5-flash", + "gemini-2.0-flash-exp", + "llama3.2", + "qwen2.5:7b", + "phi3", + "mistral", + ], }, "anthropic_api_key": { "category": "AI Services", diff --git a/frontend/templates/settings.html b/frontend/templates/settings.html index 1b132911..cf2335e2 100644 --- a/frontend/templates/settings.html +++ b/frontend/templates/settings.html @@ -139,6 +139,29 @@ Enable {{ setting.key.replace('_', ' ').title() }} + {% elif setting.metadata.type == 'model_picker' %} + +
+ + + {% for m in setting.metadata.suggested_models %} + + {% endfor %} + +

+ + Pick from the list or type any model name supported by your provider. +

+
{% elif setting.metadata.options %}