Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
da18b51
fix(proxy): retry output-free HTTP bridge overloads
Komzpa Jul 16, 2026
d614ba8
fix(proxy): retry selected-model capacity bridge errors
Komzpa Jul 16, 2026
0ca2566
fix(proxy): retry nonstream overload status errors
Komzpa Jul 16, 2026
13e9022
fix(proxy): retry initial stream eof before surfacing
Komzpa Jul 16, 2026
8a223ae
fix(proxy): preserve stream eof error after retries
Komzpa Jul 16, 2026
c6b8a5a
fix(proxy): retry accepted bridge eof
Komzpa Jul 16, 2026
57a9720
fix(proxy): address accepted retry review
Komzpa Jul 16, 2026
954f377
fix(proxy): keep bridge stream closes account-neutral
Komzpa Jul 19, 2026
6bf1e42
fix(proxy): retain pending close failure
Komzpa Jul 22, 2026
ce99ca1
fix(proxy): bound capacity retries by output and deadline
Komzpa Jul 22, 2026
e087436
fix(http-bridge): respect queued admission waiters
Komzpa Jul 22, 2026
7024fe7
refactor(ci): split metadata fetch changes from retry PR
Komzpa Jul 23, 2026
c5674e1
fix(http-bridge): preserve detached retry cleanup
Komzpa Jul 23, 2026
8ba36f4
fix(streaming): preserve exhausted upstream error
Komzpa Jul 23, 2026
104263f
test(streaming): scope empty EOF no-replay to anchored turns
Komzpa Jul 28, 2026
18cbd12
fix: bound bridge replay response-create retries
Komzpa Aug 6, 2026
efe2eb6
fix(proxy): restore bounded capacity retry ownership
Komzpa Aug 6, 2026
2379ee2
fix(http-bridge): preserve retry ownership and event state
Komzpa Aug 7, 2026
b3efd94
style: format api key usage update
Komzpa Aug 7, 2026
f526e28
fix: retry accepted terminal capacity errors
Komzpa Aug 7, 2026
580585f
fix(proxy): preserve retry failure precedence
Komzpa Aug 7, 2026
337e9de
test(http-bridge): cover neutral untyped closes
Komzpa Aug 7, 2026
21358f9
style(http-bridge): fix regression indentation
Komzpa Aug 7, 2026
374dc4e
fix(http-bridge): keep blocked untyped closes account-neutral
Komzpa Aug 7, 2026
b4d20d3
docs(http-bridge): clarify neutral blocked close classification
Komzpa Aug 7, 2026
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
5 changes: 5 additions & 0 deletions app/modules/api_keys/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@ class ApiKeysRepository:
def __init__(self, session: AsyncSession) -> None:
self._session = session

async def update_last_used(self, key_id: str, *, commit: bool = True) -> None:
await self._session.execute(update(ApiKey).where(ApiKey.id == key_id).values(last_used_at=utcnow()))
if commit:
await self._session.commit()

@staticmethod
def _build_account_costs(rows: Sequence[object]) -> list[ApiKeyAccountCost]:
account_costs: list[ApiKeyAccountCost] = []
Expand Down
1 change: 1 addition & 0 deletions app/modules/proxy/_service/http_bridge/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ def _schedule_http_bridge_session_closes(
async def _open_upstream_websocket_with_budget(self, *args: Any, **kwargs: Any) -> Any: ...
async def _resolve_websocket_previous_response_owner(self, *args: Any, **kwargs: Any) -> Any: ...
async def _acquire_request_state_response_create_admission(self, *args: Any, **kwargs: Any) -> None: ...
async def _reconnect_http_bridge_session(self, *args: Any, **kwargs: Any) -> Any: ...
async def _handle_proxy_error(self, account: Account, exc: ProxyResponseError) -> None: ...
async def _handle_stream_error(
self, account: Account, error: Any, code: str, http_status: int | None = None
Expand Down
156 changes: 144 additions & 12 deletions app/modules/proxy/_service/http_bridge/request_submit.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
reset_request_id,
set_request_id,
)
from app.core.utils.retry import backoff_seconds
from app.core.utils.sse import format_sse_event, parse_sse_data_json
from app.modules.api_keys.service import (
ApiKeyData,
Expand Down Expand Up @@ -247,6 +248,24 @@ async def _send_http_bridge_request_text_with_archive_id(
reset_request_id(token)


def _prepare_http_bridge_terminal_capacity_replay(request_state: _WebSocketRequestState) -> str | None:
request_text = request_state.request_text
if not isinstance(request_text, str) or not request_text:
return None
if request_state.response_id is None or request_state.replay_count >= 1:
return None
if request_state.upstream_model_output_seen:
return None
request_state.replay_count += 1
request_state.awaiting_response_created = True
request_state.response_id = None
request_state.response_event_count = 0
Comment thread
Komzpa marked this conversation as resolved.
request_state.latency_response_created_ms = None
request_state.upstream_model_output_seen = False
Comment thread
Komzpa marked this conversation as resolved.
_clear_websocket_request_error_overrides(request_state)
return request_text


def _text_with_account_installation_id(text_data: str, codex_installation_id: str | None) -> str:
payload = json.loads(text_data)
if not isinstance(payload, dict):
Expand Down Expand Up @@ -1688,18 +1707,18 @@ async def _detach_http_bridge_request(
request_state: _WebSocketRequestState,
) -> bool:
detached = False
async with session.pending_lock:
if request_state in session.pending_requests and not request_state.draining_until_terminal:
request_state.draining_until_terminal = True
request_state.downstream_visible = False
session.queued_request_count = max(0, session.queued_request_count - 1)
session.upstream_control.reconnect_requested = True
session.upstream_control.retire_after_drain = True
detached = True
# Queue revocation and pending ownership use the same lock. A
# completed handler that wins first keeps its local queue reference;
# a detach that wins first leaves no queue for that handler to claim.
request_state.event_queue = None
# Revoke downstream delivery immediately; lifecycle ownership below
# may legitimately be held by a reconnect owner for an arbitrary wait.
request_state.event_queue = None
async with session.lifecycle_lock:
async with session.pending_lock:
if request_state in session.pending_requests and not request_state.draining_until_terminal:
request_state.draining_until_terminal = True
request_state.downstream_visible = False
session.queued_request_count = max(0, session.queued_request_count - 1)
session.upstream_control.reconnect_requested = True
session.upstream_control.retire_after_drain = True
detached = True
await _release_websocket_response_create_gate(request_state, session.response_create_gate)
if not detached:
if request_state.terminal_settlement_phase == "abandoned":
Expand Down Expand Up @@ -1921,6 +1940,119 @@ async def _retry_http_bridge_request_on_fresh_upstream(
logger.warning("HTTP bridge retry on fresh upstream failed", exc_info=True)
return False

async def _retry_http_bridge_terminal_capacity_request(
self: Any,
session: "_HTTPBridgeSession",
request_state: _WebSocketRequestState,
*,
error_code: str,
preserve_for_reader_failure: bool = False,
) -> bool:
original_account_id = session.account.id
original_response_id = request_state.response_id
original_response_event_count = request_state.response_event_count
original_replay_count = request_state.replay_count
original_output_seen = request_state.upstream_model_output_seen
original_preferred_account_id = request_state.preferred_account_id
original_error_overrides = (
request_state.error_code_override,
request_state.error_message_override,
request_state.error_type_override,
request_state.error_param_override,
request_state.error_http_status_override,
)
async with session.pending_lock:
if (
(session.pending_requests and session.pending_requests != deque([request_state]))
or request_state.replay_count >= 1
or session.admission_waiter_count
):
return False
if not session.pending_requests:
session.pending_requests.appendleft(request_state)
session.queued_request_count += 1

async def owns_request() -> bool:
if session.closed or session.upstream_control.retire_after_drain:
return False
async with session.pending_lock:
return len(session.pending_requests) == 1 and session.pending_requests[0] is request_state
Comment on lines +1975 to +1979

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Abort replay after the downstream queue is revoked

Fresh evidence on the current head is the opposite side of the detach race: when the downstream disconnects while this retry is awaiting _reconnect_http_bridge_session under lifecycle_lock, _detach_http_bridge_request immediately sets event_queue=None and then blocks on that same lock, but owns_request() still returns true because it checks only session flags and pending-deque membership. The retry can therefore send a continuation after its consumer has gone away; detach retires it only after the send, potentially wasting billing or duplicating work/tool effects on an accepted abrupt-close request. Treat a revoked queue or draining request as lost ownership before reconnect/send.

AGENTS.md reference: AGENTS.md:L111-L115

Useful? React with 👍 / 👎.


retry_sent = False
try:
await self._acquire_request_state_response_create_admission(
request_state,
response_create_gate=session.response_create_gate,
account_id=original_account_id,
surface="http_bridge_capacity_retry",
bridge_session=session,
)
if not await owns_request():
return False
delay = backoff_seconds(original_replay_count + 1)
if request_state.bridge_request_deadline is not None:
remaining = max(0.0, request_state.bridge_request_deadline - _service_time().monotonic())
if remaining <= 0:
return False
delay = min(delay, remaining)
await asyncio.sleep(delay)
Comment thread
Komzpa marked this conversation as resolved.
if (
request_state.bridge_request_deadline is not None
and _service_time().monotonic() >= request_state.bridge_request_deadline
):
return False
async with session.lifecycle_lock:
if not await owns_request():
return False
request_state.preferred_account_id = original_account_id
await self._reconnect_http_bridge_session(
session,
request_state=request_state,
require_preferred_account=True,
)
if session.account.id != original_account_id or not await owns_request():
return False
request_text = _prepare_http_bridge_terminal_capacity_replay(request_state)
if request_text is None:
return False
request_text = self._http_bridge_text_with_account_installation_id(session, request_state, request_text)
await _send_http_bridge_request_text_with_archive_id(session, request_state, request_text)
session.last_used_at = _service_time().monotonic()
retry_sent = True
return True
except UpstreamWebSocketTransportError:
raise
except Exception:
logger.warning("HTTP bridge terminal capacity retry failed", exc_info=True)
return False
finally:
if not retry_sent and not preserve_for_reader_failure:
async with session.pending_lock:
if request_state in session.pending_requests:
session.pending_requests.remove(request_state)
Comment thread
Komzpa marked this conversation as resolved.
session.queued_request_count = sum(
1
for pending in session.pending_requests
if _http_bridge_request_counts_against_queue(pending)
)
if (
request_state.response_create_gate_acquired
or request_state.account_response_create_lease is not None
):
await _release_websocket_response_create_gate(request_state, session.response_create_gate)
request_state.response_id = original_response_id
request_state.response_event_count = original_response_event_count
request_state.replay_count = original_replay_count
request_state.upstream_model_output_seen = original_output_seen
request_state.preferred_account_id = original_preferred_account_id
(
request_state.error_code_override,
request_state.error_message_override,
request_state.error_type_override,
request_state.error_param_override,
request_state.error_http_status_override,
) = original_error_overrides

async def _retry_http_bridge_precreated_request(
self: Any,
session: "_HTTPBridgeSession",
Expand Down
Loading
Loading