2026-04-16 00:11:43 -04:00
|
|
|
"""Tests for Responses API circuit breaker in OpenAICompatProvider."""
|
|
|
|
|
|
|
|
|
|
import time
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
from nanobot.providers.openai_compat_provider import (
|
|
|
|
|
_RESPONSES_FAILURE_THRESHOLD,
|
|
|
|
|
_RESPONSES_PROBE_INTERVAL_S,
|
2026-07-15 00:13:09 +08:00
|
|
|
OpenAICompatProvider,
|
2026-04-16 00:11:43 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture()
|
|
|
|
|
def provider():
|
|
|
|
|
"""A direct-OpenAI provider with Responses API support."""
|
|
|
|
|
p = OpenAICompatProvider.__new__(OpenAICompatProvider)
|
|
|
|
|
p.default_model = "gpt-5"
|
|
|
|
|
p._spec = type("Spec", (), {"name": "openai"})()
|
|
|
|
|
p._effective_base = "https://api.openai.com/v1"
|
2026-05-23 18:04:40 +08:00
|
|
|
p._api_type = "auto"
|
2026-04-16 00:11:43 -04:00
|
|
|
p._responses_failures = {}
|
|
|
|
|
p._responses_tripped_at = {}
|
|
|
|
|
return p
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_responses_api_available_by_default(provider):
|
|
|
|
|
assert provider._should_use_responses_api("gpt-5", None) is True
|
2026-05-23 18:04:40 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_api_type_chat_completions_disables_responses(provider):
|
|
|
|
|
provider._api_type = "chat_completions"
|
|
|
|
|
assert provider._should_use_responses_api("gpt-5", None) is False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_api_type_responses_forces_responses_for_openai(provider):
|
|
|
|
|
provider.default_model = "gpt-4o"
|
|
|
|
|
provider._api_type = "responses"
|
|
|
|
|
assert provider._should_use_responses_api("gpt-4o", None) is True
|
2026-04-16 00:11:43 -04:00
|
|
|
|
|
|
|
|
|
2026-05-23 19:34:02 +08:00
|
|
|
def test_api_type_responses_ignores_circuit_breaker(provider):
|
|
|
|
|
provider.default_model = "gpt-4o"
|
|
|
|
|
provider._api_type = "responses"
|
|
|
|
|
provider._responses_failures = {"gpt-4o|gpt-4o|": _RESPONSES_FAILURE_THRESHOLD}
|
|
|
|
|
provider._responses_tripped_at = {"gpt-4o|gpt-4o|": 0.0}
|
|
|
|
|
|
|
|
|
|
assert provider._should_use_responses_api("gpt-4o", None) is True
|
|
|
|
|
|
|
|
|
|
|
2026-05-23 19:13:07 +08:00
|
|
|
def test_api_type_responses_does_not_force_non_openai(provider):
|
|
|
|
|
provider._spec = type("Spec", (), {"name": "custom"})()
|
|
|
|
|
provider._api_type = "responses"
|
|
|
|
|
|
|
|
|
|
assert provider._should_use_responses_api("gpt-4o", None) is False
|
|
|
|
|
|
|
|
|
|
|
2026-04-16 00:11:43 -04:00
|
|
|
def test_circuit_opens_after_threshold(provider):
|
|
|
|
|
for _ in range(_RESPONSES_FAILURE_THRESHOLD):
|
|
|
|
|
provider._record_responses_failure("gpt-5", None)
|
|
|
|
|
assert provider._should_use_responses_api("gpt-5", None) is False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_circuit_does_not_affect_other_models(provider):
|
|
|
|
|
for _ in range(_RESPONSES_FAILURE_THRESHOLD):
|
|
|
|
|
provider._record_responses_failure("gpt-5", None)
|
|
|
|
|
assert provider._should_use_responses_api("o4-mini", None) is True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_success_resets_circuit(provider):
|
|
|
|
|
for _ in range(_RESPONSES_FAILURE_THRESHOLD):
|
|
|
|
|
provider._record_responses_failure("gpt-5", None)
|
|
|
|
|
assert provider._should_use_responses_api("gpt-5", None) is False
|
|
|
|
|
provider._record_responses_success("gpt-5", None)
|
|
|
|
|
assert provider._should_use_responses_api("gpt-5", None) is True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_probe_after_interval(provider, monkeypatch):
|
|
|
|
|
for _ in range(_RESPONSES_FAILURE_THRESHOLD):
|
|
|
|
|
provider._record_responses_failure("gpt-5", None)
|
|
|
|
|
assert provider._should_use_responses_api("gpt-5", None) is False
|
|
|
|
|
|
|
|
|
|
# Fast-forward past the probe interval
|
|
|
|
|
key = "gpt-5:"
|
|
|
|
|
provider._responses_tripped_at[key] = time.monotonic() - _RESPONSES_PROBE_INTERVAL_S - 1
|
|
|
|
|
assert provider._should_use_responses_api("gpt-5", None) is True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_below_threshold_still_allows(provider):
|
|
|
|
|
provider._record_responses_failure("gpt-5", None)
|
|
|
|
|
provider._record_responses_failure("gpt-5", None)
|
|
|
|
|
assert provider._should_use_responses_api("gpt-5", None) is True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_reasoning_effort_keyed_separately(provider):
|
|
|
|
|
for _ in range(_RESPONSES_FAILURE_THRESHOLD):
|
|
|
|
|
provider._record_responses_failure("o3", "high")
|
|
|
|
|
assert provider._should_use_responses_api("o3", "high") is False
|
|
|
|
|
assert provider._should_use_responses_api("o3", "low") is True
|
2026-04-19 12:15:21 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_reasoning_effort_key_is_case_insensitive(provider):
|
|
|
|
|
for _ in range(_RESPONSES_FAILURE_THRESHOLD):
|
|
|
|
|
provider._record_responses_failure("o3", "High")
|
|
|
|
|
assert provider._should_use_responses_api("o3", "high") is False
|