Skip to content
Closed
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 app/core/balancer/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from app.core.balancer.logic import (
ERROR_BACKOFF_THRESHOLD,
HEALTH_TIER_DRAINING,
HEALTH_TIER_HEALTHY,
HEALTH_TIER_PROBING,
Expand Down Expand Up @@ -36,6 +37,7 @@
"HEALTH_TIER_DRAINING",
"HEALTH_TIER_HEALTHY",
"HEALTH_TIER_PROBING",
"ERROR_BACKOFF_THRESHOLD",
"REAUTH_REQUIRED_FAILURE_CODES",
"AccountState",
"RoutingCost",
Expand Down
7 changes: 4 additions & 3 deletions app/core/balancer/logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
DRAIN_SECONDARY_THRESHOLD_PCT = 90.0
DRAIN_ERROR_WINDOW_SECONDS = 60.0
DRAIN_ERROR_COUNT_THRESHOLD = 2
ERROR_BACKOFF_THRESHOLD = 3
PROBE_QUIET_SECONDS = 60.0
PROBE_SUCCESS_STREAK_REQUIRED = 3
ROUTING_POLICY_NORMAL = "normal"
Expand Down Expand Up @@ -481,8 +482,8 @@ def select_account(
state.error_count = 0
if state.cooldown_until and current < state.cooldown_until:
continue
if state.error_count >= 3:
backoff = min(300, 30 * (2 ** (state.error_count - 3)))
if state.error_count >= ERROR_BACKOFF_THRESHOLD:
backoff = min(300, 30 * (2 ** (state.error_count - ERROR_BACKOFF_THRESHOLD)))
if state.last_error_at and current - state.last_error_at < backoff:
in_error_backoff.append(state)
continue
Expand Down Expand Up @@ -519,7 +520,7 @@ def select_account(
if allow_backoff_fallback and (len(in_error_backoff) > 1 or (in_error_backoff and hard_blocked_exists)):

def _backoff_expires_at(s: AccountState) -> float:
backoff = min(300, 30 * (2 ** (s.error_count - 3)))
backoff = min(300, 30 * (2 ** (s.error_count - ERROR_BACKOFF_THRESHOLD)))
return (s.last_error_at or 0.0) + backoff

available.append(min(in_error_backoff, key=_backoff_expires_at))
Expand Down
15 changes: 12 additions & 3 deletions app/core/clients/codex.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,11 +212,20 @@ async def request_with_route_metadata(
retryable_same_contract=False,
) from None
return CodexRequestResult(response, candidate, index > 0)
except CodexTransportError:
if index == len(endpoints) - 1 or not allow_fallback:
except CodexTransportError as exc:
# A confirmed pre-dispatch connect failure proves the request
# never left for upstream, so trying the next endpoint in the
# same resolved pool is safe even for a non-idempotent POST.
# TLS verification failures are stable endpoint configuration
# errors rather than transient connect losses; they keep the
# idempotent-only rule.
if index == len(endpoints) - 1 or not (
allow_fallback or (exc.retryable_same_contract and not exc.is_tls_verification_failure)
):
raise
except Exception as exc:
if index == len(endpoints) - 1 or not allow_fallback:
pre_dispatch = is_pre_dispatch_connection_failure(exc) and not isinstance(exc, aiohttp.ClientSSLError)
if index == len(endpoints) - 1 or not (allow_fallback or pre_dispatch):
raise _transport_error(
"request",
endpoint.id,
Expand Down
20 changes: 20 additions & 0 deletions app/core/clients/proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@

CODEX_INSTALLATION_ID_HEADER = "x-codex-installation-id"
CODEX_TURN_METADATA_HEADER = "x-codex-turn-metadata"
CODEX_LB_REQUIRED_CAPABILITY_HEADER = "x-codex-lb-required-capability"
CODEX_RESPONSES_LITE_HEADER = "x-openai-internal-codex-responses-lite"
CODEX_RESPONSES_LITE_WEBSOCKET_METADATA_KEY = "ws_request_header_x_openai_internal_codex_responses_lite"

Expand All @@ -103,6 +104,7 @@
"forwarded",
"x-real-ip",
CODEX_INSTALLATION_ID_HEADER,
CODEX_LB_REQUIRED_CAPABILITY_HEADER,
"true-client-ip",
}
INTERNAL_OPENAI_UPSTREAM_HEADERS = frozenset(
Expand Down Expand Up @@ -488,6 +490,24 @@ def __init__(
self.failed_session = failed_session


def is_confirmed_pre_dispatch_transport_error(exc: ProxyResponseError) -> bool:
"""Return whether the transport proved the upstream request never dispatched.

Only this provenance authorizes replaying a movable request on another
account: a typed connector failure while reaching the account's routed
proxy endpoint, before any request bytes could leave for upstream.
Host-wide network loss (``proxy_network_unavailable``) stays on its
account-neutral process recovery path instead of penalizing the selected
account, and ambiguous dispatch outcomes remain non-replayable.
"""

if not (exc.retryable_same_contract and exc.failure_phase == "connect"):
return False
error = exc.payload.get("error")
error_code = error.get("code") if isinstance(error, dict) else None
return error_code != PROCESS_NETWORK_UNAVAILABLE_CODE


def _process_network_failure_error(
message: str,
exc: Exception,
Expand Down
15 changes: 14 additions & 1 deletion app/core/clients/proxy_websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -769,9 +769,22 @@ async def _connect_upstream_websocket(
status_code if policy.preserve_handshake_status else 502,
openai_error(error_code, message, error_type="server_error"),
failure_phase="connect",
# Carry the client's dispatch provenance across the sanitizing
# boundary: a typed connector failure against the routed proxy
# proves no ``response.create`` frame could have reached
# upstream, so service-level failover may replay the request
# on another account. TLS verification failures are stable
# endpoint configuration errors and stay non-replayable.
retryable_same_contract=(
policy.retry_routed_network_errors and error_code == PROCESS_NETWORK_UNAVAILABLE_CODE
(policy.retry_routed_network_errors and error_code == PROCESS_NETWORK_UNAVAILABLE_CODE)
or (exc.retryable_same_contract and not exc.is_tls_verification_failure)
),
failure_detail=(
"proxy_connect_pre_dispatch"
if exc.retryable_same_contract and not exc.is_tls_verification_failure
else "transport_error"
),
failure_exception_type=type(exc).__name__,
) from exc
except Exception:
if owns_codex_client:
Expand Down
48 changes: 24 additions & 24 deletions app/core/usage/pricing.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,34 +104,34 @@ def _normalize_usage(usage: UsageTokens | ResponseUsage | None) -> UsageTokens |
long_context_output_per_1m=45.0,
),
"gpt-5.6-terra": ModelPrice(
input_per_1m=2.5,
cached_input_per_1m=0.25,
output_per_1m=15.0,
priority_input_per_1m=5.0,
priority_cached_input_per_1m=0.5,
priority_output_per_1m=30.0,
flex_input_per_1m=1.25,
flex_cached_input_per_1m=0.125,
flex_output_per_1m=7.5,
input_per_1m=2.0,
cached_input_per_1m=0.2,
output_per_1m=12.0,
priority_input_per_1m=4.0,
priority_cached_input_per_1m=0.4,
priority_output_per_1m=24.0,
flex_input_per_1m=1.0,
flex_cached_input_per_1m=0.1,
flex_output_per_1m=6.0,
long_context_threshold_tokens=272_000,
long_context_input_per_1m=5.0,
long_context_cached_input_per_1m=0.5,
long_context_output_per_1m=22.5,
long_context_input_per_1m=4.0,
long_context_cached_input_per_1m=0.4,
long_context_output_per_1m=18.0,
),
"gpt-5.6-luna": ModelPrice(
input_per_1m=1.0,
cached_input_per_1m=0.1,
output_per_1m=6.0,
priority_input_per_1m=2.0,
priority_cached_input_per_1m=0.2,
priority_output_per_1m=12.0,
flex_input_per_1m=0.5,
flex_cached_input_per_1m=0.05,
flex_output_per_1m=3.0,
input_per_1m=0.2,
cached_input_per_1m=0.02,
output_per_1m=1.2,
priority_input_per_1m=0.4,
priority_cached_input_per_1m=0.04,
priority_output_per_1m=2.4,
flex_input_per_1m=0.1,
flex_cached_input_per_1m=0.01,
flex_output_per_1m=0.6,
long_context_threshold_tokens=272_000,
long_context_input_per_1m=2.0,
long_context_cached_input_per_1m=0.2,
long_context_output_per_1m=9.0,
long_context_input_per_1m=0.4,
long_context_cached_input_per_1m=0.04,
long_context_output_per_1m=1.8,
),
"gpt-5.5": ModelPrice(
input_per_1m=5.0,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""add durable capability lineage markers

Revision ID: 20260731_000000_add_capability_lineage_markers
Revises: 20260725_000000_add_http_bridge_pending_tool_calls
Create Date: 2026-07-31
"""

from __future__ import annotations

import sqlalchemy as sa
from alembic import op

revision = "20260731_000000_add_capability_lineage_markers"
down_revision = "20260725_000000_add_http_bridge_pending_tool_calls"
branch_labels = None
depends_on = None

_TABLE = "capability_lineage_markers"


def upgrade() -> None:
bind = op.get_bind()
if sa.inspect(bind).has_table(_TABLE):
return
op.create_table(
_TABLE,
sa.Column("marker_hash", sa.String(length=64), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.Column("last_seen_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.PrimaryKeyConstraint("marker_hash"),
)


def downgrade() -> None:
bind = op.get_bind()
if not sa.inspect(bind).has_table(_TABLE):
return
op.drop_table(_TABLE)
17 changes: 17 additions & 0 deletions app/db/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,23 @@ class StickySession(Base):
)


class CapabilityLineageMarker(Base):
__tablename__ = "capability_lineage_markers"

marker_hash: Mapped[str] = mapped_column(String(64), primary_key=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
nullable=False,
)
last_seen_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now(),
nullable=False,
)


class DashboardSettings(Base):
__tablename__ = "dashboard_settings"

Expand Down
2 changes: 2 additions & 0 deletions app/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from app.modules.model_sources.repository import ModelSourcesRepository
from app.modules.model_sources.service import ModelSourcesService
from app.modules.oauth.service import OauthService
from app.modules.proxy.capability_lineage_repository import CapabilityLineageRepository
from app.modules.proxy.repo_bundle import ProxyRepositories
from app.modules.proxy.service import ProxyService
from app.modules.proxy.sticky_repository import StickySessionsRepository
Expand Down Expand Up @@ -220,6 +221,7 @@ async def _proxy_repo_context() -> AsyncIterator[ProxyRepositories]:
api_keys=ApiKeysRepository(session),
additional_usage=AdditionalUsageRepository(session),
quota_planner=QuotaPlannerRepository(session),
capability_lineage=CapabilityLineageRepository(session),
)


Expand Down
1 change: 1 addition & 0 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,7 @@ def create_app() -> FastAPI:
app.include_router(dashboard_api.router)
app.include_router(usage_api.router)
app.include_router(request_logs_api.router)
app.include_router(request_logs_api.conversations_router)
app.include_router(quota_planner_api.router)
app.include_router(reports_api.router)
app.include_router(conversation_archive_api.router)
Expand Down
15 changes: 15 additions & 0 deletions app/modules/dashboard/timeframes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from datetime import datetime, timedelta
from typing import cast

from app.core.utils.time import utcnow
from app.modules.dashboard.builders import _OVERVIEW_TIMEFRAME_CONFIGS
from app.modules.dashboard.schemas import DashboardOverviewTimeframeKey

CONVERSATION_TIMEFRAME_KEYS: frozenset[str] = frozenset({"1d", "7d", "30d"})


def resolve_conversation_timeframe(key: str) -> tuple[int, datetime]:
"""Return the window duration and rolling start for a conversation timeframe."""
_key = cast(DashboardOverviewTimeframeKey, key)
timeframe = _OVERVIEW_TIMEFRAME_CONFIGS[_key]
return timeframe.window_minutes, utcnow() - timedelta(minutes=timeframe.window_minutes)
Loading
Loading