Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/openai/lib/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
from ._tools import pydantic_function_tool as pydantic_function_tool
from ._parsing import ResponseFormatT as ResponseFormatT
from ._reasoning import get_default_reasoning as get_default_reasoning
from ._reasoning import get_reasoning_effort_from_env as get_reasoning_effort_from_env
145 changes: 145 additions & 0 deletions src/openai/lib/_reasoning.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
"""Utilities for reasoning configuration.

This module provides utilities for configuring reasoning behavior,
including support for the OPENAI_REASONING_EFFORT environment variable.

Example usage:
from openai import OpenAI
from openai.lib import get_default_reasoning

client = OpenAI()

# Uses OPENAI_REASONING_EFFORT env var if set, otherwise returns None
reasoning = get_default_reasoning()

response = client.responses.create(
model="gpt-5",
input="Hello",
reasoning=reasoning,
)
"""

from __future__ import annotations

import os
import warnings
from typing import Optional, Literal

from ..types.shared_params.reasoning import Reasoning
from ..types.shared.reasoning_effort import ReasoningEffort


__all__ = [
"get_default_reasoning",
"get_reasoning_effort_from_env",
]

# Valid reasoning effort values
VALID_REASONING_EFFORTS: tuple[str, ...] = (
"none",
"minimal",
"low",
"medium",
"high",
"xhigh",
)

# Environment variable name
REASONING_EFFORT_ENV_VAR = "OPENAI_REASONING_EFFORT"


def get_reasoning_effort_from_env() -> Optional[ReasoningEffort]:
"""Get reasoning effort from the OPENAI_REASONING_EFFORT environment variable.

Returns:
The reasoning effort value if set and valid, None otherwise.

Valid values are: none, minimal, low, medium, high, xhigh

If an invalid value is set, a warning is emitted and None is returned.

Example:
>>> import os
>>> os.environ["OPENAI_REASONING_EFFORT"] = "low"
>>> get_reasoning_effort_from_env()
'low'
"""
value = os.environ.get(REASONING_EFFORT_ENV_VAR)
if value is None:
return None

# Normalize to lowercase
value_lower = value.lower().strip()

if value_lower not in VALID_REASONING_EFFORTS:
warnings.warn(
f"Invalid {REASONING_EFFORT_ENV_VAR} value: '{value}'. "
f"Valid values are: {', '.join(VALID_REASONING_EFFORTS)}. "
"Ignoring environment variable.",
UserWarning,
stacklevel=2,
)
return None

return value_lower # type: ignore[return-value]


def get_default_reasoning(
effort: Optional[ReasoningEffort] = None,
summary: Optional[Literal["auto", "concise", "detailed"]] = None,
) -> Optional[Reasoning]:
"""Get a Reasoning configuration, using environment variable as default.

This function allows you to easily configure reasoning with support for
the OPENAI_REASONING_EFFORT environment variable.

Args:
effort: Override the reasoning effort. If None, uses OPENAI_REASONING_EFFORT
environment variable if set.
summary: Optional summary configuration for reasoning output.

Returns:
A Reasoning TypedDict if effort is configured (either explicitly or via
environment variable), None otherwise.

Precedence:
1. Explicit `effort` parameter (if provided)
2. OPENAI_REASONING_EFFORT environment variable (if set)
3. None (SDK default behavior)

Example:
>>> # With environment variable set:
>>> import os
>>> os.environ["OPENAI_REASONING_EFFORT"] = "low"
>>> get_default_reasoning()
{'effort': 'low'}

>>> # With explicit override:
>>> get_default_reasoning(effort="high")
{'effort': 'high'}

>>> # With no configuration:
>>> del os.environ["OPENAI_REASONING_EFFORT"]
>>> get_default_reasoning()
None
"""
# Determine the effort value
final_effort: Optional[ReasoningEffort] = None

if effort is not None:
final_effort = effort
else:
final_effort = get_reasoning_effort_from_env()

# If no effort is configured, return None
if final_effort is None and summary is None:
return None

# Build the Reasoning config
result: Reasoning = {}
if final_effort is not None:
result["effort"] = final_effort
if summary is not None:
result["summary"] = summary

return result
114 changes: 114 additions & 0 deletions tests/lib/test_reasoning.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
"""Tests for reasoning utilities.

Tests the OPENAI_REASONING_EFFORT environment variable support.
Relates to issue #2686: Allow setting reasoning effort via environment variable
"""

from __future__ import annotations

import os
import warnings

import pytest

from openai.lib import get_default_reasoning, get_reasoning_effort_from_env


class TestGetReasoningEffortFromEnv:
"""Tests for get_reasoning_effort_from_env function."""

def setup_method(self) -> None:
"""Clean up env var before each test."""
if "OPENAI_REASONING_EFFORT" in os.environ:
del os.environ["OPENAI_REASONING_EFFORT"]

def teardown_method(self) -> None:
"""Clean up env var after each test."""
if "OPENAI_REASONING_EFFORT" in os.environ:
del os.environ["OPENAI_REASONING_EFFORT"]

def test_returns_none_when_not_set(self) -> None:
"""Returns None when env var is not set."""
result = get_reasoning_effort_from_env()
assert result is None

@pytest.mark.parametrize(
"value",
["none", "minimal", "low", "medium", "high", "xhigh"],
)
def test_returns_valid_values(self, value: str) -> None:
"""Returns the value when it's valid."""
os.environ["OPENAI_REASONING_EFFORT"] = value
result = get_reasoning_effort_from_env()
assert result == value

def test_case_insensitive(self) -> None:
"""Accepts case-insensitive values."""
os.environ["OPENAI_REASONING_EFFORT"] = "HIGH"
result = get_reasoning_effort_from_env()
assert result == "high"

def test_strips_whitespace(self) -> None:
"""Strips leading/trailing whitespace."""
os.environ["OPENAI_REASONING_EFFORT"] = " low "
result = get_reasoning_effort_from_env()
assert result == "low"

def test_warns_on_invalid_value(self) -> None:
"""Warns and returns None for invalid values."""
os.environ["OPENAI_REASONING_EFFORT"] = "invalid"
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
result = get_reasoning_effort_from_env()
assert result is None
assert len(w) == 1
assert "Invalid" in str(w[0].message)
assert "OPENAI_REASONING_EFFORT" in str(w[0].message)


class TestGetDefaultReasoning:
"""Tests for get_default_reasoning function."""

def setup_method(self) -> None:
"""Clean up env var before each test."""
if "OPENAI_REASONING_EFFORT" in os.environ:
del os.environ["OPENAI_REASONING_EFFORT"]

def teardown_method(self) -> None:
"""Clean up env var after each test."""
if "OPENAI_REASONING_EFFORT" in os.environ:
del os.environ["OPENAI_REASONING_EFFORT"]

def test_returns_none_when_no_config(self) -> None:
"""Returns None when no effort configured."""
result = get_default_reasoning()
assert result is None

def test_uses_env_var(self) -> None:
"""Uses env var when no explicit effort provided."""
os.environ["OPENAI_REASONING_EFFORT"] = "low"
result = get_default_reasoning()
assert result == {"effort": "low"}

def test_explicit_effort_overrides_env(self) -> None:
"""Explicit effort parameter overrides env var."""
os.environ["OPENAI_REASONING_EFFORT"] = "low"
result = get_default_reasoning(effort="high")
assert result == {"effort": "high"}

def test_with_summary(self) -> None:
"""Can configure summary alongside effort."""
os.environ["OPENAI_REASONING_EFFORT"] = "medium"
result = get_default_reasoning(summary="concise")
assert result == {"effort": "medium", "summary": "concise"}

def test_summary_only(self) -> None:
"""Can configure summary without effort."""
result = get_default_reasoning(summary="detailed")
assert result == {"summary": "detailed"}

def test_explicit_none_effort_uses_env(self) -> None:
"""Explicit None for effort still uses env var."""
os.environ["OPENAI_REASONING_EFFORT"] = "high"
result = get_default_reasoning(effort=None)
assert result == {"effort": "high"}