OE-02/03/04: Add is_reasoning_model config flag for non-OpenAI thinking models - #4
Conversation
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>
MateoTTR
left a comment
There was a problem hiding this comment.
Code Review — PR #4
Summary
Extracts the reasoning-model detection logic from a local variable inside generate_with_context() into a module-level constant + public function, then adds an is_reasoning_model: Optional[bool] = None field to LLMModelConfig that allows per-model override. The design is clean and the 3-state logic is correctly implemented.
Verdict: COMMENT (no changes required before merge, but see warnings below)
Upstream-ready: YES — branch contains only openevolve/, tests/, no fork artifacts. One pre-existing test file (test_openai_model_detection.py) needs a follow-up fix.
Stats
- Files reviewed: 3 changed + 1 pre-existing test
- Critical: 0 | Warnings: 3 | Suggestions: 2
CRITICAL — none
WARNINGS
W1 — test_openai_model_detection.py now tests phantom behaviour (pre-existing file, not changed by this PR)
The old test file (tests/test_openai_model_detection.py) defines a local is_reasoning_model() helper that includes an api_base == "https://api.openai.com/v1" guard. The real production code NEVER had that guard — it was always pure prefix matching. test_non_openai_api_base therefore tests an invariant that does not exist in the actual code (the test passes only because it calls its own local stub, not the real function). This was a latent issue before this PR, but it becomes more confusing now that there is a real is_reasoning_model() exported by the module. The test should be rewritten to call the real function and verify that is_reasoning_model("o1-mini", config_flag=None) returns True regardless of api_base (which is no longer a parameter at all), and that is_reasoning_model("o1-mini", config_flag=False) can be used by callers who need the old "non-OpenAI endpoint" behaviour.
W2 — gpt-oss-* entries are specific model names, not a prefix — any future gpt-oss-* variant will silently fall through to non-reasoning
"gpt-oss-120b" and "gpt-oss-20b" are used as prefixes via str.startswith(), which means they match those exact strings (and any longer name that starts with them, e.g. "gpt-oss-120b-2025"). However "gpt-oss-30b" or any other gpt-oss-* variant would not match. If the intention is to cover the whole gpt-oss family, the entry should be "gpt-oss-" instead.
W3 — Redundant prefixes in OPENAI_REASONING_MODEL_PREFIXES
"o1-" is fully redundant: "o1" already matches "o1-mini", "o1-preview", etc. because str.startswith("o1") is true for any string beginning with "o1". Same for "o3-" and "gpt-5-". The redundant entries have no effect on runtime behaviour (confirmed by test), but they add noise and could mislead future maintainers into thinking they are load-bearing. Suggesting removal of "o1-", "o3-", and "gpt-5-".
SUGGESTIONS
S1 — New test (test_reasoning_model_detection.py) does not cover gpt-oss-* models
TestIsReasoningModel tests o1, o3, o4, gpt-5, gemini, claude, and deepseek, but never exercises the two gpt-oss-* entries that were added to OPENAI_REASONING_MODEL_PREFIXES. A subTest loop over ["gpt-oss-120b", "gpt-oss-20b"] should be added.
S2 — is_reasoning_model not propagated through LLMConfig.update_model_params (intentional but undocumented)
The field is correctly omitted from shared_config in LLMConfig.__post_init__() and rebuild_models() — per-model overrides should not be overwritten by the top-level defaults. But there is no comment explaining why. A one-line comment (# is_reasoning_model is intentionally per-model only — not propagated from shared config) would prevent a future contributor from "fixing" the omission.
GOOD
- 3-state
Optional[bool]design is idiomatic and backward compatible: existing YAML configs without the field produceNonewhich falls through to auto-detection, preserving the old behaviour exactly. getattr(model_cfg, "is_reasoning_model", None)inOpenAILLM.__init__is the right defensive pattern for forward/backward compatibility with configs that predate the field.- Test coverage is comprehensive for the happy path: auto-detect (positive + negative), explicit override in both directions, case insensitivity, and
config_flag=Noneequivalence. 12 targeted subtests is appropriate scope for this utility function. - Branch is clean: only
openevolve/config.py,openevolve/llm/openai.py, andtests/test_reasoning_model_detection.py— no fork-internal files. - CI passes (unit + integration).
Automated review by Claude Code
| "gpt-5", # gpt-5, gpt-5-mini, gpt-5-nano | ||
| # The GPT OSS series are also reasoning models | ||
| "gpt-oss-120b", | ||
| "gpt-oss-20b", |
There was a problem hiding this comment.
WARNING W2 — "gpt-oss-120b" and "gpt-oss-20b" are used as startswith prefixes, which means they only match those two exact model families (plus any longer name that starts with them). A future "gpt-oss-30b" or "gpt-oss-mini" would silently fall through to non-reasoning.
If the intention is to cover the entire gpt-oss family, replace both entries with the single prefix "gpt-oss-":
# The GPT OSS series are also reasoning models
"gpt-oss-",If only these two specific variants are known reasoning models, add a comment clarifying that the list is exhaustive by intent, to prevent a future "fix".
| # These models don't support temperature/top_p and use different parameters. | ||
| OPENAI_REASONING_MODEL_PREFIXES = ( | ||
| # O-series reasoning models | ||
| "o1-", |
There was a problem hiding this comment.
WARNING W3 — "o1-" is redundant: since "o1" is already in the tuple, str.startswith("o1") matches "o1-mini", "o1-preview", etc. just as well. Same applies to "o3-" (covered by "o3") and "gpt-5-" (covered by "gpt-5"). Confirmed with tests — removing the three dash-suffixed variants produces identical results for all known model names.
Suggest removing "o1-", "o3-", and "gpt-5-" to keep the constant minimal and avoid misleading future maintainers.
| for model in ["gpt-5", "gpt-5-mini", "gpt-5-nano"]: | ||
| with self.subTest(model=model): | ||
| self.assertTrue(is_reasoning_model(model)) | ||
|
|
There was a problem hiding this comment.
SUGGESTION S1 — The new OPENAI_REASONING_MODEL_PREFIXES constant includes "gpt-oss-120b" and "gpt-oss-20b", but neither is exercised by TestIsReasoningModel. Add a subTest loop:
def test_gpt_oss_auto_detected(self):
for model in ["gpt-oss-120b", "gpt-oss-20b"]:
with self.subTest(model=model):
self.assertTrue(is_reasoning_model(model))This would also catch a future regression if one of the specific-name entries is accidentally removed.
| # Reasoning model override: True forces reasoning-model parameter conventions | ||
| # (max_completion_tokens, no temperature/top_p), False forces standard conventions, | ||
| # None (default) auto-detects based on known OpenAI reasoning model prefixes. | ||
| is_reasoning_model: Optional[bool] = None |
There was a problem hiding this comment.
SUGGESTION S2 — This field is correctly omitted from shared_config inside LLMConfig.__post_init__() and rebuild_models() (lines ~186 and ~240). However there is no comment explaining the omission, which may look like a bug to a future contributor. Suggest adding a short inline comment in shared_config in both places:
shared_config = {
...
"reasoning_effort": self.reasoning_effort,
# is_reasoning_model is intentionally per-model only — not propagated from shared config
"manual_mode": self.manual_mode,
}…t-oss coverage - Remove redundant dash-prefixes (o1-, o3-, gpt-5-) already covered by base prefixes - Replace gpt-oss-120b/gpt-oss-20b full names with gpt-oss- prefix - Rewrite test_openai_model_detection.py to use real is_reasoning_model() function instead of duplicating logic with a non-existent api_base check - Add test coverage for gpt-oss-* model family - Add comment explaining intentional omission of is_reasoning_model from shared_config Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Closes #2
OPENAI_REASONING_MODEL_PREFIXESto a module-level constant and create a standaloneis_reasoning_model()function inopenevolve/llm/openai.pyis_reasoning_model: Optional[bool] = Nonefield toLLMModelConfiginopenevolve/config.pytests/test_reasoning_model_detection.py(12 test cases)Design decision
The
is_reasoning_modelconfig field uses 3-state logic:None(default)Truemax_completion_tokens, notemperature/top_p)FalseThis is fully backward compatible: existing configs without the field behave exactly as before.
Config example
Test plan
is_reasoning_model: true🤖 Generated with Claude Code