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
102 changes: 102 additions & 0 deletions tests/integration/test_http_responses_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -8914,6 +8914,108 @@ async def fake_connect_responses_websocket(
assert connect_count == 2


@pytest.mark.asyncio
async def test_v1_responses_http_bridge_retries_when_upstream_never_acknowledges_response_create(
async_client,
monkeypatch,
):
_install_bridge_settings_with_limits(
monkeypatch,
enabled=True,
)
proxy_module.get_settings().http_responses_session_bridge_stuck_gate_retire_after_seconds = 0.01
account_id = await _import_account(
async_client,
"acc_http_bridge_missing_created_retry",
"http-bridge-missing-created-retry@example.com",
)
account = await _get_account(account_id)
silent_upstream = _SilentUpstreamWebSocket()
recovered_upstream = _FakeBridgeUpstreamWebSocket()
upstreams = [silent_upstream, recovered_upstream]
connect_count = 0

async def fake_select_account_with_budget(
self,
deadline,
*,
request_id,
kind,
request_stage="first_turn",
sticky_key,
sticky_kind,
reallocate_sticky,
sticky_max_age_seconds,
prefer_earlier_reset_accounts,
routing_strategy,
model,
exclude_account_ids=None,
additional_limit_name=None,
api_key=None,
preferred_account_id=None,
):
del preferred_account_id
del (
self,
deadline,
request_id,
kind,
request_stage,
sticky_key,
sticky_kind,
reallocate_sticky,
sticky_max_age_seconds,
prefer_earlier_reset_accounts,
routing_strategy,
model,
exclude_account_ids,
additional_limit_name,
api_key,
)
return AccountSelection(account=account, error_message=None, error_code=None)

async def fake_ensure_fresh_with_budget(self, target, *, force=False, timeout_seconds):
del self, force, timeout_seconds
return target

async def fake_connect_responses_websocket(
headers,
access_token,
account_id_header,
*,
base_url=None,
session=None,
):
del headers, access_token, account_id_header, base_url, session
nonlocal connect_count
upstream = upstreams[connect_count]
connect_count += 1
return upstream

monkeypatch.setattr(proxy_module.ProxyService, "_select_account_with_budget", fake_select_account_with_budget)
monkeypatch.setattr(proxy_module.ProxyService, "_ensure_fresh_with_budget", fake_ensure_fresh_with_budget)
monkeypatch.setattr(proxy_module, "connect_responses_websocket", fake_connect_responses_websocket)

response = await asyncio.wait_for(
async_client.post(
"/v1/responses",
json={
"model": "gpt-5.1",
"instructions": "Return exactly OK.",
"input": "retry missing response.created",
"prompt_cache_key": "missing-created-retry-key",
},
),
timeout=_TEST_SYNC_TIMEOUT_SECONDS,
)

assert response.status_code == 200
assert connect_count == 2
assert silent_upstream.closed is True
assert len(silent_upstream.sent_text) == 1
assert len(recovered_upstream.sent_text) == 1


@pytest.mark.asyncio
async def test_backend_responses_http_bridge_retries_precreated_server_overload(async_client, monkeypatch):
_install_bridge_settings(monkeypatch, enabled=True)
Expand Down
42 changes: 42 additions & 0 deletions tests/unit/test_proxy_http_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -828,6 +828,48 @@ def _make_eventless_http_bridge_owner(
)


@pytest.mark.asyncio
@pytest.mark.parametrize("include_sibling", [False, True])
async def test_http_bridge_eventless_anchored_precreated_retry_stays_fail_closed(
monkeypatch: pytest.MonkeyPatch,
include_sibling: bool,
) -> None:
service = proxy_service.ProxyService(cast(Any, nullcontext()))
owner = _make_eventless_http_bridge_owner()
owner.request_text = '{"type":"response.create","input":"continue"}'
owner.previous_response_id = "resp-parent"
pending_requests = deque([owner])
queued_request_count = 1

if include_sibling:
sibling = proxy_service._WebSocketRequestState(
request_id="req-created-sibling",
model="gpt-5.6-sol",
service_tier=None,
reasoning_effort="high",
api_key_reservation=None,
started_at=time.monotonic(),
transport="http",
response_id="resp-created-sibling",
)
pending_requests.append(sibling)
queued_request_count = 2

session = _make_bridge_session(
key=proxy_service._HTTPBridgeSessionKey("session_header", "hard-anchor", None),
pending_requests=pending_requests,
queued_request_count=queued_request_count,
)
session.last_upstream_close_code = 1011
session.upstream = cast(UpstreamWebSocket, SimpleNamespace(send_text=AsyncMock(), close=AsyncMock()))
reconnect = AsyncMock()
monkeypatch.setattr(proxy_service, "get_settings", lambda: _make_app_settings())
monkeypatch.setattr(service, "_reconnect_http_bridge_session", reconnect)

assert await service._retry_http_bridge_precreated_request(session) is False
reconnect.assert_not_awaited()


class _SilentEventlessUpstream:
"""Upstream double that never produces a response event, for eventless-timeout tests."""

Expand Down
Loading