Skip to content

Commit 4a0f4bd

Browse files
MateoTTRclaude
andcommitted
Add is_reasoning_model config flag for non-OpenAI thinking models
Extract OPENAI_REASONING_MODEL_PREFIXES to module-level constant and is_reasoning_model() to a standalone function. Add is_reasoning_model field to LLMModelConfig with 3-state logic: True (force reasoning), False (force standard), None (auto-detect via OpenAI prefixes). This allows users of non-OpenAI providers (Gemini, DeepSeek, etc.) to explicitly mark models as reasoning models via config, without relying on fragile prefix-based detection. Closes #2 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 80945ed commit 4a0f4bd

3 files changed

Lines changed: 126 additions & 23 deletions

File tree

openevolve/config.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ class LLMModelConfig:
7979
# Reasoning parameters
8080
reasoning_effort: Optional[str] = None
8181

82+
# Reasoning model override: True forces reasoning-model parameter conventions
83+
# (max_completion_tokens, no temperature/top_p), False forces standard conventions,
84+
# None (default) auto-detects based on known OpenAI reasoning model prefixes.
85+
is_reasoning_model: Optional[bool] = None
86+
8287
# Manual mode (human-in-the-loop)
8388
manual_mode: Optional[bool] = None
8489
_manual_queue_dir: Optional[str] = None

openevolve/llm/openai.py

Lines changed: 42 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,43 @@
2020

2121
logger = logging.getLogger(__name__)
2222

23+
# OpenAI reasoning models that require max_completion_tokens instead of max_tokens.
24+
# These models don't support temperature/top_p and use different parameters.
25+
OPENAI_REASONING_MODEL_PREFIXES = (
26+
# O-series reasoning models
27+
"o1-",
28+
"o1", # o1, o1-mini, o1-preview
29+
"o3-",
30+
"o3", # o3, o3-mini, o3-pro
31+
"o4-", # o4-mini
32+
# GPT-5 series are also reasoning models
33+
"gpt-5-",
34+
"gpt-5", # gpt-5, gpt-5-mini, gpt-5-nano
35+
# The GPT OSS series are also reasoning models
36+
"gpt-oss-120b",
37+
"gpt-oss-20b",
38+
)
39+
40+
41+
def is_reasoning_model(
42+
model_name: str,
43+
config_flag: Optional[bool] = None,
44+
) -> bool:
45+
"""Detect if a model should be treated as a reasoning model.
46+
47+
Args:
48+
model_name: The model name/identifier.
49+
config_flag: Explicit override from config. If True/False, returns that
50+
value directly. If None (default), auto-detects based on known
51+
OpenAI reasoning model prefixes.
52+
53+
Returns:
54+
True if the model should be treated as a reasoning model.
55+
"""
56+
if config_flag is not None:
57+
return config_flag
58+
return model_name.lower().startswith(OPENAI_REASONING_MODEL_PREFIXES)
59+
2360

2461
def _iso_now() -> str:
2562
return datetime.now(tz=timezone.utc).isoformat()
@@ -63,9 +100,10 @@ def __init__(
63100
self.api_key = model_cfg.api_key
64101
self.random_seed = getattr(model_cfg, "random_seed", None)
65102
self.reasoning_effort = getattr(model_cfg, "reasoning_effort", None)
103+
self.is_reasoning_model_flag = getattr(model_cfg, "is_reasoning_model", None)
66104

67105
# Manual mode: enabled via llm.manual_mode in config.yaml
68-
self.manual_mode = (getattr(model_cfg, "manual_mode", False) is True)
106+
self.manual_mode = getattr(model_cfg, "manual_mode", False) is True
69107
self.manual_queue_dir: Optional[Path] = None
70108

71109
if self.manual_mode:
@@ -114,29 +152,10 @@ async def generate_with_context(
114152
formatted_messages.extend(messages)
115153

116154
# Set up generation parameters
117-
# Define OpenAI reasoning models that require max_completion_tokens
118-
# These models don't support temperature/top_p and use different parameters
119-
OPENAI_REASONING_MODEL_PREFIXES = (
120-
# O-series reasoning models
121-
"o1-",
122-
"o1", # o1, o1-mini, o1-preview
123-
"o3-",
124-
"o3", # o3, o3-mini, o3-pro
125-
"o4-", # o4-mini
126-
# GPT-5 series are also reasoning models
127-
"gpt-5-",
128-
"gpt-5", # gpt-5, gpt-5-mini, gpt-5-nano
129-
# The GPT OSS series are also reasoning models
130-
"gpt-oss-120b",
131-
"gpt-oss-20b",
132-
)
133-
134-
# Check if this is an OpenAI reasoning model based on model name pattern
135-
# This works for all endpoints (OpenAI, Azure, OptiLLM, OpenRouter, etc.)
136-
model_lower = str(self.model).lower()
137-
is_openai_reasoning_model = model_lower.startswith(OPENAI_REASONING_MODEL_PREFIXES)
155+
# Detect whether to use reasoning-model parameter conventions
156+
is_reasoning = is_reasoning_model(self.model, self.is_reasoning_model_flag)
138157

139-
if is_openai_reasoning_model:
158+
if is_reasoning:
140159
# For OpenAI reasoning models
141160
params = {
142161
"model": self.model,
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""Tests for reasoning model detection logic (is_reasoning_model function)"""
2+
3+
import unittest
4+
5+
from openevolve.llm.openai import OPENAI_REASONING_MODEL_PREFIXES, is_reasoning_model
6+
7+
8+
class TestIsReasoningModel(unittest.TestCase):
9+
"""Test the is_reasoning_model() function"""
10+
11+
# Auto-detect (config_flag=None) -- OpenAI models
12+
def test_openai_o_series_auto_detected(self):
13+
for model in ["o1", "o1-mini", "o3", "o3-mini", "o3-pro", "o4-mini"]:
14+
with self.subTest(model=model):
15+
self.assertTrue(is_reasoning_model(model))
16+
17+
def test_openai_gpt5_auto_detected(self):
18+
for model in ["gpt-5", "gpt-5-mini", "gpt-5-nano"]:
19+
with self.subTest(model=model):
20+
self.assertTrue(is_reasoning_model(model))
21+
22+
def test_openai_non_reasoning_not_detected(self):
23+
for model in ["gpt-4o", "gpt-4o-mini", "gpt-4", "gpt-3.5-turbo"]:
24+
with self.subTest(model=model):
25+
self.assertFalse(is_reasoning_model(model))
26+
27+
# Auto-detect -- Non-OpenAI models should NOT be auto-detected
28+
def test_gemini_not_auto_detected(self):
29+
for model in ["gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.5-flash-lite"]:
30+
with self.subTest(model=model):
31+
self.assertFalse(is_reasoning_model(model))
32+
33+
def test_claude_not_auto_detected(self):
34+
for model in ["claude-sonnet-4-5-20250929", "claude-opus-4-5-20251101"]:
35+
with self.subTest(model=model):
36+
self.assertFalse(is_reasoning_model(model))
37+
38+
def test_deepseek_not_auto_detected(self):
39+
self.assertFalse(is_reasoning_model("deepseek-r1"))
40+
41+
# Explicit config_flag=True -- forces reasoning model
42+
def test_explicit_true_overrides_auto_detect(self):
43+
self.assertTrue(is_reasoning_model("gemini-2.5-flash", config_flag=True))
44+
self.assertTrue(is_reasoning_model("deepseek-r1", config_flag=True))
45+
self.assertTrue(is_reasoning_model("any-unknown-model", config_flag=True))
46+
47+
# Explicit config_flag=False -- forces non-reasoning model
48+
def test_explicit_false_overrides_auto_detect(self):
49+
# Even OpenAI reasoning models can be forced to non-reasoning
50+
self.assertFalse(is_reasoning_model("o3-mini", config_flag=False))
51+
self.assertFalse(is_reasoning_model("gpt-5", config_flag=False))
52+
53+
# Case insensitivity
54+
def test_case_insensitive(self):
55+
self.assertTrue(is_reasoning_model("O3-MINI"))
56+
self.assertTrue(is_reasoning_model("GPT-5-MINI"))
57+
58+
# Backward compatibility
59+
def test_none_config_flag_is_default(self):
60+
"""None config_flag should behave exactly like the old hardcoded logic"""
61+
self.assertTrue(is_reasoning_model("o3-mini", config_flag=None))
62+
self.assertFalse(is_reasoning_model("gpt-4o", config_flag=None))
63+
64+
65+
class TestReasoningModelPrefixes(unittest.TestCase):
66+
"""Test that the prefix constant is properly defined"""
67+
68+
def test_prefixes_is_tuple(self):
69+
self.assertIsInstance(OPENAI_REASONING_MODEL_PREFIXES, tuple)
70+
71+
def test_prefixes_contains_o_series(self):
72+
# At minimum, o1 and o3 should be in the prefixes
73+
prefixes_str = " ".join(OPENAI_REASONING_MODEL_PREFIXES)
74+
self.assertIn("o1", prefixes_str)
75+
self.assertIn("o3", prefixes_str)
76+
77+
78+
if __name__ == "__main__":
79+
unittest.main()

0 commit comments

Comments
 (0)