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>
This commit is contained in:
copilot-swe-agent[bot]
2026-02-23 22:12:08 +00:00
parent 2eecfd0d4b
commit a94b52ee14
4 changed files with 234 additions and 51 deletions
+91 -49
View File
@@ -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:
+48 -2
View File
@@ -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",