codexLB-up 1.25.0b10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- app/__init__.py +10 -0
- app/admin_cli.py +494 -0
- app/cli.py +339 -0
- app/codex_sessions_retag.py +487 -0
- app/core/__init__.py +0 -0
- app/core/audit/__init__.py +0 -0
- app/core/audit/service.py +216 -0
- app/core/audit/types.py +94 -0
- app/core/auth/__init__.py +236 -0
- app/core/auth/api_key_cache.py +65 -0
- app/core/auth/dashboard_access.py +429 -0
- app/core/auth/dashboard_mode.py +171 -0
- app/core/auth/dashboard_session_ttl.py +74 -0
- app/core/auth/dashboard_users_cache.py +120 -0
- app/core/auth/dependencies.py +710 -0
- app/core/auth/external_identity.py +78 -0
- app/core/auth/guardian.py +374 -0
- app/core/auth/models.py +49 -0
- app/core/auth/providers/__init__.py +129 -0
- app/core/auth/providers/oidc.py +838 -0
- app/core/auth/providers/registry.py +118 -0
- app/core/auth/refresh.py +394 -0
- app/core/auth/step_up.py +117 -0
- app/core/auth/totp.py +81 -0
- app/core/balancer/__init__.py +71 -0
- app/core/balancer/logic.py +1612 -0
- app/core/balancer/rendezvous_hash.py +21 -0
- app/core/balancer/types.py +21 -0
- app/core/bootstrap.py +172 -0
- app/core/cache/invalidation.py +426 -0
- app/core/clients/__init__.py +0 -0
- app/core/clients/codex.py +996 -0
- app/core/clients/codex_version.py +187 -0
- app/core/clients/codex_version_snapshot.py +2 -0
- app/core/clients/files.py +534 -0
- app/core/clients/headers.py +29 -0
- app/core/clients/http.py +511 -0
- app/core/clients/model_fetcher.py +276 -0
- app/core/clients/native_egress.py +1304 -0
- app/core/clients/oauth.py +477 -0
- app/core/clients/proxy.py +6089 -0
- app/core/clients/proxy_websocket.py +1411 -0
- app/core/clients/rate_limit_reset_credits.py +412 -0
- app/core/clients/stream_errors.py +12 -0
- app/core/clients/thread_cache_identity.py +347 -0
- app/core/clients/usage.py +467 -0
- app/core/clock.py +170 -0
- app/core/config/__init__.py +0 -0
- app/core/config/background_jobs.py +73 -0
- app/core/config/context_window_overrides.py +166 -0
- app/core/config/dashboard_overrides.py +145 -0
- app/core/config/inheritable.py +47 -0
- app/core/config/key_fingerprint.py +115 -0
- app/core/config/settings.py +872 -0
- app/core/config/settings_cache.py +100 -0
- app/core/config/spool_retention.py +217 -0
- app/core/config/tiers.py +174 -0
- app/core/conversation_archive.py +568 -0
- app/core/crypto.py +37 -0
- app/core/draining.py +11 -0
- app/core/errors.py +365 -0
- app/core/exceptions.py +155 -0
- app/core/handlers/__init__.py +3 -0
- app/core/handlers/exceptions.py +478 -0
- app/core/http_protocol.py +176 -0
- app/core/http_protocol_httptools.py +204 -0
- app/core/ingress_limits.py +17 -0
- app/core/metadata_store.py +18 -0
- app/core/metrics/__init__.py +0 -0
- app/core/metrics/middleware.py +74 -0
- app/core/metrics/prometheus.py +604 -0
- app/core/middleware/__init__.py +25 -0
- app/core/middleware/api_firewall.py +111 -0
- app/core/middleware/app_version.py +37 -0
- app/core/middleware/dashboard_auth_proxy.py +55 -0
- app/core/middleware/dashboard_csrf.py +177 -0
- app/core/middleware/dashboard_gzip.py +41 -0
- app/core/middleware/dashboard_overrides.py +59 -0
- app/core/middleware/firewall_cache.py +91 -0
- app/core/middleware/inflight.py +126 -0
- app/core/middleware/multipart_content_encoding.py +129 -0
- app/core/middleware/path_rewrite.py +148 -0
- app/core/middleware/request_body_limit.py +196 -0
- app/core/middleware/request_decompression.py +209 -0
- app/core/middleware/request_id.py +72 -0
- app/core/middleware/required_capability_http.py +116 -0
- app/core/middleware/trusted_proxy_headers.py +36 -0
- app/core/multipart.py +237 -0
- app/core/multipart_fields.py +91 -0
- app/core/openai/__init__.py +0 -0
- app/core/openai/chat_requests.py +493 -0
- app/core/openai/chat_responses.py +931 -0
- app/core/openai/contracts.py +46 -0
- app/core/openai/exceptions.py +24 -0
- app/core/openai/host_models.py +14 -0
- app/core/openai/images.py +413 -0
- app/core/openai/message_coercion.py +376 -0
- app/core/openai/model_refresh_scheduler.py +423 -0
- app/core/openai/model_registry.py +997 -0
- app/core/openai/model_registry_store.py +548 -0
- app/core/openai/models.py +172 -0
- app/core/openai/parsing.py +97 -0
- app/core/openai/requests.py +1980 -0
- app/core/openai/strict_schema.py +199 -0
- app/core/openai/tool_call_safety.py +99 -0
- app/core/openai/v1_requests.py +187 -0
- app/core/plan_types.py +90 -0
- app/core/prestop.py +202 -0
- app/core/rate_limiter/__init__.py +0 -0
- app/core/rate_limiter/db_rate_limiter.py +215 -0
- app/core/request_locality.py +473 -0
- app/core/resilience/__init__.py +0 -0
- app/core/resilience/backpressure.py +57 -0
- app/core/resilience/bulkhead.py +202 -0
- app/core/resilience/circuit_breaker.py +150 -0
- app/core/resilience/degradation.py +55 -0
- app/core/resilience/loop_lag_monitor.py +60 -0
- app/core/resilience/memory_monitor.py +137 -0
- app/core/resilience/network_recovery.py +256 -0
- app/core/resilience/overload.py +98 -0
- app/core/resilience/toggles.py +114 -0
- app/core/retention/__init__.py +0 -0
- app/core/retention/job.py +240 -0
- app/core/retention/scheduler.py +69 -0
- app/core/runtime_logging.py +601 -0
- app/core/scheduling/__init__.py +0 -0
- app/core/scheduling/leader_election.py +993 -0
- app/core/scheduling/leader_election_handle.py +25 -0
- app/core/server.py +140 -0
- app/core/shutdown.py +326 -0
- app/core/socket_peer.py +22 -0
- app/core/startup.py +41 -0
- app/core/timeout_invariants.py +347 -0
- app/core/tracing/__init__.py +0 -0
- app/core/tracing/otel.py +207 -0
- app/core/types.py +6 -0
- app/core/upstream_proxy/__init__.py +13 -0
- app/core/upstream_proxy/cache.py +130 -0
- app/core/upstream_proxy/resolver.py +189 -0
- app/core/upstream_proxy/types.py +104 -0
- app/core/usage/__init__.py +407 -0
- app/core/usage/depletion.py +109 -0
- app/core/usage/live_hub.py +41 -0
- app/core/usage/live_snapshots.py +174 -0
- app/core/usage/logs.py +212 -0
- app/core/usage/metadata_scheduler.py +118 -0
- app/core/usage/models.py +54 -0
- app/core/usage/pricing.py +576 -0
- app/core/usage/pricing_catalog.py +241 -0
- app/core/usage/pricing_snapshot.json +821 -0
- app/core/usage/quota.py +133 -0
- app/core/usage/refresh_policy.py +21 -0
- app/core/usage/refresh_scheduler.py +713 -0
- app/core/usage/reset_credits_refresh_scheduler.py +435 -0
- app/core/usage/types.py +168 -0
- app/core/utils/__init__.py +0 -0
- app/core/utils/json_guards.py +18 -0
- app/core/utils/locks.py +33 -0
- app/core/utils/masking.py +20 -0
- app/core/utils/proxy_env.py +104 -0
- app/core/utils/request_id.py +65 -0
- app/core/utils/retry.py +77 -0
- app/core/utils/shared_future.py +172 -0
- app/core/utils/sse.py +286 -0
- app/core/utils/time.py +24 -0
- app/db/__init__.py +0 -0
- app/db/account_identity_lock.py +56 -0
- app/db/alembic/__init__.py +1 -0
- app/db/alembic/env.py +70 -0
- app/db/alembic/revision_ids.py +43 -0
- app/db/alembic/versions/20260213_000000_base_schema.py +248 -0
- app/db/alembic/versions/20260213_000100_normalize_account_plan_types.py +45 -0
- app/db/alembic/versions/20260213_000200_add_request_logs_reasoning_effort.py +45 -0
- app/db/alembic/versions/20260213_000300_add_accounts_reset_at.py +45 -0
- app/db/alembic/versions/20260213_000400_add_accounts_chatgpt_account_id.py +45 -0
- app/db/alembic/versions/20260213_000500_add_dashboard_settings.py +70 -0
- app/db/alembic/versions/20260213_000600_add_dashboard_settings_totp.py +64 -0
- app/db/alembic/versions/20260213_000700_add_dashboard_settings_password.py +59 -0
- app/db/alembic/versions/20260213_000800_add_api_keys.py +87 -0
- app/db/alembic/versions/20260214_000000_add_api_key_limits.py +143 -0
- app/db/alembic/versions/20260215_000000_add_idx_logs_requested_at.py +38 -0
- app/db/alembic/versions/20260218_000000_add_api_key_usage_reservations.py +117 -0
- app/db/alembic/versions/20260218_000100_add_import_without_overwrite_and_drop_accounts_email_unique.py +198 -0
- app/db/alembic/versions/20260225_000000_add_dashboard_settings_routing_strategy.py +53 -0
- app/db/alembic/versions/20260228_020000_align_api_key_limit_enum_types.py +104 -0
- app/db/alembic/versions/20260228_030000_add_api_firewall_allowlist.py +42 -0
- app/db/alembic/versions/20260306_000000_add_request_logs_service_tier.py +45 -0
- app/db/alembic/versions/20260307_000000_add_api_key_enforcement_fields.py +56 -0
- app/db/alembic/versions/20260308_000000_add_sqlite_performance_indexes.py +41 -0
- app/db/alembic/versions/20260309_000000_add_additional_usage_history.py +71 -0
- app/db/alembic/versions/20260309_000000_request_logs_nullable_account_id.py +28 -0
- app/db/alembic/versions/20260310_000000_add_request_logs_transport.py +45 -0
- app/db/alembic/versions/20260310_000000_fix_postgresql_enum_value_casing.py +79 -0
- app/db/alembic/versions/20260310_120000_add_sticky_session_kinds_and_affinity_ttl.py +110 -0
- app/db/alembic/versions/20260312_000000_add_additional_usage_quota_key.py +193 -0
- app/db/alembic/versions/20260312_000000_split_sticky_sessions_primary_key_by_kind.py +96 -0
- app/db/alembic/versions/20260312_010000_merge_additional_usage_and_sticky_session_heads.py +25 -0
- app/db/alembic/versions/20260312_020000_merge_request_logs_transport_and_feature_heads.py +25 -0
- app/db/alembic/versions/20260312_020000_merge_request_logs_transport_and_sticky_heads.py +25 -0
- app/db/alembic/versions/20260312_120000_add_dashboard_upstream_stream_transport.py +62 -0
- app/db/alembic/versions/20260313_024500_merge_request_log_transport_heads.py +27 -0
- app/db/alembic/versions/20260319_100937_increase_prompt_cache_ttl_to_1800s.py +73 -0
- app/db/alembic/versions/20260319_111500_merge_prompt_cache_ttl_and_upstream_transport_heads.py +25 -0
- app/db/alembic/versions/20260319_183000_normalize_sqlite_account_status_casing.py +48 -0
- app/db/alembic/versions/20260320_000000_add_request_log_requested_actual_tiers.py +51 -0
- app/db/alembic/versions/20260321_190000_tighten_dashboard_db_indexes.py +64 -0
- app/db/alembic/versions/20260321_210000_merge_request_log_tiers_and_dashboard_index_heads.py +25 -0
- app/db/alembic/versions/20260325_000000_add_request_log_cost.py +193 -0
- app/db/alembic/versions/20260328_000000_add_rate_limit_attempts.py +48 -0
- app/db/alembic/versions/20260328_120000_add_request_log_ttft.py +37 -0
- app/db/alembic/versions/20260328_130000_add_audit_logs_table.py +45 -0
- app/db/alembic/versions/20260328_140000_add_scheduler_leader_table.py +39 -0
- app/db/alembic/versions/20260330_000000_add_cache_locality_settings.py +69 -0
- app/db/alembic/versions/20260330_010000_merge_scheduler_leader_and_cache_locality_heads.py +25 -0
- app/db/alembic/versions/20260330_020000_add_bridge_ring_members.py +46 -0
- app/db/alembic/versions/20260401_000000_add_api_key_enforced_service_tier.py +52 -0
- app/db/alembic/versions/20260401_000000_add_cache_invalidation.py +41 -0
- app/db/alembic/versions/20260401_020000_merge_cache_invalidation_and_api_key_service_tier_heads.py +22 -0
- app/db/alembic/versions/20260402_000000_switch_dashboard_routing_default_to_capacity_weighted.py +66 -0
- app/db/alembic/versions/20260403_000000_add_credit_api_key_limit_values.py +44 -0
- app/db/alembic/versions/20260406_000000_add_api_key_account_assignments.py +57 -0
- app/db/alembic/versions/20260406_010000_add_api_key_assignment_scope_flag.py +75 -0
- app/db/alembic/versions/20260407_000000_add_bridge_gateway_safe_mode.py +51 -0
- app/db/alembic/versions/20260407_010000_merge_api_key_assignment_and_bridge_gateway_heads.py +25 -0
- app/db/alembic/versions/20260408_000000_switch_import_without_overwrite_default_to_true.py +73 -0
- app/db/alembic/versions/20260408_010000_merge_import_without_overwrite_and_assignment_heads.py +25 -0
- app/db/alembic/versions/20260409_000000_add_http_bridge_sessions.py +130 -0
- app/db/alembic/versions/20260409_000000_switch_sticky_threads_and_prefer_earlier_reset_defaults_to_true.py +89 -0
- app/db/alembic/versions/20260409_010000_add_dashboard_settings_bootstrap_token.py +57 -0
- app/db/alembic/versions/20260409_010000_merge_http_bridge_and_import_without_overwrite_heads.py +25 -0
- app/db/alembic/versions/20260409_020000_fix_http_bridge_last_seen_index.py +46 -0
- app/db/alembic/versions/20260410_000000_backfill_pristine_dashboard_settings_defaults.py +77 -0
- app/db/alembic/versions/20260410_030000_restore_import_without_overwrite_default_true.py +73 -0
- app/db/alembic/versions/20260410_040000_merge_dashboard_defaults_and_import_default_heads.py +25 -0
- app/db/alembic/versions/20260413_000000_add_accounts_blocked_at.py +44 -0
- app/db/alembic/versions/20260415_160000_add_automations_tables.py +189 -0
- app/db/alembic/versions/20260415_160000_add_request_logs_response_lookup_index.py +67 -0
- app/db/alembic/versions/20260417_000000_add_request_log_plan_type.py +44 -0
- app/db/alembic/versions/20260418_190000_add_automation_runs_cycle_key.py +87 -0
- app/db/alembic/versions/20260419_000000_add_automation_run_cycle_metadata.py +56 -0
- app/db/alembic/versions/20260419_020000_add_automation_run_cycles_snapshot_tables.py +342 -0
- app/db/alembic/versions/20260421_000000_add_dashboard_session_ttl_setting.py +51 -0
- app/db/alembic/versions/20260421_120000_merge_request_log_lookup_and_plan_type_heads.py +25 -0
- app/db/alembic/versions/20260421_130000_merge_automation_and_request_log_heads.py +25 -0
- app/db/alembic/versions/20260422_103000_normalize_legacy_automation_run_cycle_keys.py +648 -0
- app/db/alembic/versions/20260423_120000_add_api_key_limit_reset_at_index.py +43 -0
- app/db/alembic/versions/20260424_000000_merge_dashboard_session_ttl_and_request_log_heads.py +25 -0
- app/db/alembic/versions/20260426_000000_add_dashboard_relative_availability_settings.py +68 -0
- app/db/alembic/versions/20260427_130000_add_warmup_model_and_request_kind.py +75 -0
- app/db/alembic/versions/20260506_000000_add_account_routing_policy.py +37 -0
- app/db/alembic/versions/20260509_000000_add_api_key_traffic_class.py +37 -0
- app/db/alembic/versions/20260509_010000_add_additional_quota_routing_policies.py +37 -0
- app/db/alembic/versions/20260513_000000_add_accounts_alias.py +44 -0
- app/db/alembic/versions/20260513_000000_add_api_key_apply_to_codex_model.py +59 -0
- app/db/alembic/versions/20260514_000000_add_request_logs_api_key_time_index.py +43 -0
- app/db/alembic/versions/20260515_000000_add_prefer_earlier_reset_window.py +47 -0
- app/db/alembic/versions/20260515_000000_soft_delete_request_logs_on_account_delete.py +83 -0
- app/db/alembic/versions/20260515_020000_add_split_sticky_budget_thresholds.py +62 -0
- app/db/alembic/versions/20260516_000000_add_sqlite_hot_path_indexes.py +75 -0
- app/db/alembic/versions/20260517_000000_merge_sqlite_hot_path_and_request_log_delete_heads.py +24 -0
- app/db/alembic/versions/20260518_000000_add_http_bridge_durable_input_prefix.py +44 -0
- app/db/alembic/versions/20260518_010000_merge_http_bridge_and_sqlite_recovery_heads.py +25 -0
- app/db/alembic/versions/20260518_120000_add_dashboard_guest_access.py +57 -0
- app/db/alembic/versions/20260520_000000_merge_api_key_and_http_bridge_heads.py +26 -0
- app/db/alembic/versions/20260520_010000_add_request_logs_api_key_account_index.py +45 -0
- app/db/alembic/versions/20260520_010000_merge_automation_and_api_key_heads.py +26 -0
- app/db/alembic/versions/20260520_030000_add_quota_planner.py +194 -0
- app/db/alembic/versions/20260520_040000_merge_quota_planner_and_request_log_index_heads.py +24 -0
- app/db/alembic/versions/20260521_000000_add_account_security_work_authorized.py +46 -0
- app/db/alembic/versions/20260521_000000_merge_dashboard_guest_and_api_key_heads.py +26 -0
- app/db/alembic/versions/20260521_000000_merge_prefer_reset_window_head.py +26 -0
- app/db/alembic/versions/20260521_000000_merge_warmup_and_request_log_index_heads.py +25 -0
- app/db/alembic/versions/20260522_000000_add_limit_warmup_trigger.py +195 -0
- app/db/alembic/versions/20260524_000000_merge_accounts_alias_and_reset_window_heads.py +24 -0
- app/db/alembic/versions/20260525_000000_add_usage_raw_window_latest_index.py +31 -0
- app/db/alembic/versions/20260526_000000_add_request_log_failure_metadata.py +100 -0
- app/db/alembic/versions/20260531_000000_add_account_workspace_identity.py +39 -0
- app/db/alembic/versions/20260531_000000_add_single_account_routing.py +42 -0
- app/db/alembic/versions/20260601_000000_merge_relative_availability_and_usage_raw_heads.py +24 -0
- app/db/alembic/versions/20260601_010000_add_api_key_usage_sections.py +58 -0
- app/db/alembic/versions/20260601_010000_add_routing_policy_persistence.py +67 -0
- app/db/alembic/versions/20260601_010000_merge_security_work_and_relative_availability_heads.py +25 -0
- app/db/alembic/versions/20260601_010000_merge_split_sticky_and_relative_usage_heads.py +25 -0
- app/db/alembic/versions/20260601_010000_merge_warmup_and_relative_availability_heads.py +25 -0
- app/db/alembic/versions/20260601_020000_merge_additional_quota_routing_and_relative_availability_heads.py +25 -0
- app/db/alembic/versions/20260601_020000_merge_quota_planner_and_relative_availability_heads.py +25 -0
- app/db/alembic/versions/20260601_020000_merge_reset_window_and_relative_availability_heads.py +25 -0
- app/db/alembic/versions/20260601_020000_merge_routing_policy_and_split_threshold_heads.py +24 -0
- app/db/alembic/versions/20260601_020000_merge_warmup_and_request_log_failure_heads.py +25 -0
- app/db/alembic/versions/20260601_030000_merge_single_account_and_routing_quota_heads.py +29 -0
- app/db/alembic/versions/20260601_040000_merge_security_work_and_routing_consolidation_heads.py +25 -0
- app/db/alembic/versions/20260601_050000_merge_quota_planner_and_routing_consolidation_heads.py +25 -0
- app/db/alembic/versions/20260602_050000_add_upstream_proxy_routing.py +164 -0
- app/db/alembic/versions/20260602_060000_merge_account_workspace_and_failure_heads.py +25 -0
- app/db/alembic/versions/20260602_070000_add_request_log_useragent_fields.py +86 -0
- app/db/alembic/versions/20260602_070000_merge_quota_planner_and_account_workspace_heads.py +25 -0
- app/db/alembic/versions/20260602_080000_merge_upstream_proxy_and_quota_planner_heads.py +25 -0
- app/db/alembic/versions/20260602_080000_merge_useragent_and_upstream_proxy_heads.py +24 -0
- app/db/alembic/versions/20260603_000000_add_weekly_pace_working_days.py +52 -0
- app/db/alembic/versions/20260603_000000_free_account_monthly_window.py +60 -0
- app/db/alembic/versions/20260604_000000_add_reauth_required_account_status.py +159 -0
- app/db/alembic/versions/20260604_010000_merge_automations_and_reauth_status_heads.py +25 -0
- app/db/alembic/versions/20260604_010000_merge_dashboard_guest_and_reauth_status_heads.py +25 -0
- app/db/alembic/versions/20260604_010000_merge_useragent_and_reauth_heads.py +24 -0
- app/db/alembic/versions/20260607_000000_merge_weekly_monthly_useragent_heads.py +28 -0
- app/db/alembic/versions/20260608_000000_add_hide_upstream_quota_from_api_keys.py +50 -0
- app/db/alembic/versions/20260611_000000_add_request_log_archive_request_id.py +47 -0
- app/db/alembic/versions/20260611_000000_merge_automations_and_weekly_monthly_useragent_heads.py +26 -0
- app/db/alembic/versions/20260611_000000_merge_dashboard_guest_and_weekly_useragent_heads.py +26 -0
- app/db/alembic/versions/20260611_010000_add_request_log_client_ip.py +57 -0
- app/db/alembic/versions/20260611_020000_merge_request_log_archive_and_client_ip_heads.py +26 -0
- app/db/alembic/versions/20260611_030000_merge_dashboard_guest_and_request_log_client_ip_heads.py +26 -0
- app/db/alembic/versions/20260613_000000_add_accounts_codex_installation_id.py +57 -0
- app/db/alembic/versions/20260613_010000_merge_installation_id_and_dashboard_weekly_heads.py +26 -0
- app/db/alembic/versions/20260615_000000_merge_automations_and_dashboard_guest_heads.py +26 -0
- app/db/alembic/versions/20260623_000000_add_limit_warmup_exhausted_threshold.py +51 -0
- app/db/alembic/versions/20260626_000000_add_http_downstream_transport_policy.py +46 -0
- app/db/alembic/versions/20260626_010000_add_request_logs_upstream_transport.py +31 -0
- app/db/alembic/versions/20260629_000000_add_dashboard_query_hot_path_indexes.py +53 -0
- app/db/alembic/versions/20260630_000000_add_limit_warmup_staggered_idle.py +47 -0
- app/db/alembic/versions/20260630_000000_merge_request_log_client_ip_and_dashboard_hot_path_heads.py +26 -0
- app/db/alembic/versions/20260630_010000_merge_warmup_and_request_log_dashboard_heads.py +26 -0
- app/db/alembic/versions/20260630_020000_merge_automations_and_main_heads.py +26 -0
- app/db/alembic/versions/20260630_020000_merge_warmup_threshold_and_main_heads.py +26 -0
- app/db/alembic/versions/20260630_030000_add_automation_run_model_snapshot.py +59 -0
- app/db/alembic/versions/20260630_040000_merge_automation_snapshot_and_warmup_threshold_heads.py +26 -0
- app/db/alembic/versions/20260630_050000_add_automation_run_prompt_snapshot.py +52 -0
- app/db/alembic/versions/20260701_000000_add_weekly_pace_smoothing_minutes.py +51 -0
- app/db/alembic/versions/20260702_000000_add_model_sources.py +176 -0
- app/db/alembic/versions/20260705_000000_add_model_source_audio_transcriptions.py +50 -0
- app/db/alembic/versions/20260705_000000_harden_dashboard_session_ttl.py +62 -0
- app/db/alembic/versions/20260706_000000_add_model_source_audio_per_minute.py +43 -0
- app/db/alembic/versions/20260707_000000_merge_model_sources_and_session_ttl.py +24 -0
- app/db/alembic/versions/20260707_010000_merge_automations_and_model_sources_heads.py +24 -0
- app/db/alembic/versions/20260707_020000_add_automation_job_account_scope.py +125 -0
- app/db/alembic/versions/20260709_000000_add_ttft_phase_observability.py +66 -0
- app/db/alembic/versions/20260711_000000_add_dashboard_prohibit_fast_mode.py +42 -0
- app/db/alembic/versions/20260711_010000_add_account_chatgpt_user_id.py +40 -0
- app/db/alembic/versions/20260711_020000_add_dashboard_account_concurrency_caps.py +71 -0
- app/db/alembic/versions/20260711_030000_add_limit_warmup_idle_threshold.py +51 -0
- app/db/alembic/versions/20260712_010000_add_account_usage_rollups.py +56 -0
- app/db/alembic/versions/20260712_020000_add_api_key_usage_rollups.py +53 -0
- app/db/alembic/versions/20260713_020000_add_model_registry_snapshot.py +42 -0
- app/db/alembic/versions/20260713_040000_add_account_refresh_claims.py +42 -0
- app/db/alembic/versions/20260713_040000_add_replica_guardrails.py +64 -0
- app/db/alembic/versions/20260713_070000_add_reset_credit_redeem_tables.py +58 -0
- app/db/alembic/versions/20260714_000000_add_oauth_flow_states.py +63 -0
- app/db/alembic/versions/20260715_000000_add_request_log_queue_latency.py +44 -0
- app/db/alembic/versions/20260716_000000_add_oauth_device_flow_slots.py +41 -0
- app/db/alembic/versions/20260716_010000_add_dashboard_retention_settings.py +48 -0
- app/db/alembic/versions/20260716_010000_add_reset_credit_display_settings.py +73 -0
- app/db/alembic/versions/20260717_000000_add_http_bridge_retry_circuits.py +47 -0
- app/db/alembic/versions/20260717_000000_merge_retention_and_reset_credit_display_heads.py +26 -0
- app/db/alembic/versions/20260717_000000_optimize_dashboard_hot_path_indexes.py +162 -0
- app/db/alembic/versions/20260717_000001_merge_retry_circuits_and_dashboard_indexes.py +22 -0
- app/db/alembic/versions/20260720_000000_add_request_log_conversation_id.py +57 -0
- app/db/alembic/versions/20260722_000000_backfill_request_log_useragent_families.py +40 -0
- app/db/alembic/versions/20260724_000000_add_request_usage_time_rollups.py +176 -0
- app/db/alembic/versions/20260724_000000_merge_request_log_schema_heads.py +33 -0
- app/db/alembic/versions/20260725_000000_add_http_bridge_pending_tool_calls.py +43 -0
- app/db/alembic/versions/20260726_000000_add_account_plan_downgrade_observations.py +44 -0
- app/db/alembic/versions/20260726_000000_repair_request_usage_rollups_after_merge.py +37 -0
- app/db/alembic/versions/20260727_000000_add_sticky_session_continuity_abandoned_at.py +45 -0
- app/db/alembic/versions/20260728_000000_merge_pending_tool_calls_and_rollup_repair_heads.py +31 -0
- app/db/alembic/versions/20260730_000000_add_api_key_fair_share_threshold.py +44 -0
- app/db/alembic/versions/20260730_000000_add_http_bridge_recovery_attempts.py +60 -0
- app/db/alembic/versions/20260731_000000_add_capability_lineage_markers.py +38 -0
- app/db/alembic/versions/20260803_000000_merge_http_bridge_recovery_and_capability_lineage_heads.py +30 -0
- app/db/alembic/versions/20260804_000000_add_http_bridge_operations.py +63 -0
- app/db/alembic/versions/20260804_000001_add_global_http_bridge_operation_fingerprint.py +53 -0
- app/db/alembic/versions/20260804_230000_add_request_log_connection_request_kind.py +43 -0
- app/db/alembic/versions/20260805_000000_add_http_bridge_operation_spool.py +75 -0
- app/db/alembic/versions/20260805_000001_finalize_http_bridge_operation_spool.py +65 -0
- app/db/alembic/versions/20260806_000000_add_additional_usage_alias_probe_indexes.py +84 -0
- app/db/alembic/versions/20260806_000000_add_anonymous_telemetry.py +49 -0
- app/db/alembic/versions/20260806_010000_add_conversation_presence_rollup.py +72 -0
- app/db/alembic/versions/20260806_020000_add_usage_history_bulk_covering_indexes.py +99 -0
- app/db/alembic/versions/20260806_030000_add_api_key_allowed_reasoning_efforts.py +65 -0
- app/db/alembic/versions/20260806_120000_add_http_bridge_owner_process_epoch.py +57 -0
- app/db/alembic/versions/20260807_000000_merge_http_bridge_operation_spool_and_latest_main.py +24 -0
- app/db/alembic/versions/20260808_000000_tune_usage_history_autovacuum.py +54 -0
- app/db/alembic/versions/20260810_000000_add_http_bridge_recovery_dispatch_count.py +44 -0
- app/db/alembic/versions/20260811_000000_add_hourly_rollup_cancelled_count.py +92 -0
- app/db/alembic/versions/20260812_000000_merge_recovery_dispatch_and_hourly_cancelled_heads.py +25 -0
- app/db/alembic/versions/20260812_120000_add_sticky_abandonment_scope.py +43 -0
- app/db/alembic/versions/20260813_000000_add_file_account_pins.py +40 -0
- app/db/alembic/versions/20260816_000000_add_account_pending_deletion.py +81 -0
- app/db/alembic/versions/20260816_000000_add_model_source_embeddings.py +50 -0
- app/db/alembic/versions/20260821_000000_add_retry_circuit_admission_generation.py +49 -0
- app/db/alembic/versions/20260826_000000_add_http_bridge_event_chunks.py +107 -0
- app/db/alembic/versions/20260828_000000_add_accounts_chatgpt_identity_index.py +55 -0
- app/db/alembic/versions/20260830_000000_add_quota_warmup_claim_expiry.py +83 -0
- app/db/alembic/versions/20260908_000000_add_guest_session_generation.py +37 -0
- app/db/alembic/versions/20260908_000000_add_subscription_overflow.py +140 -0
- app/db/alembic/versions/20260908_000000_replace_upstream_stream_transport_default_sentinel.py +50 -0
- app/db/alembic/versions/20260908_020000_merge_overflow_transport_heads.py +27 -0
- app/db/alembic/versions/20260909_000000_add_dashboard_roles.py +72 -0
- app/db/alembic/versions/20260909_010000_add_dashboard_users.py +183 -0
- app/db/alembic/versions/20260909_020000_reproject_compat_admin_credentials.py +105 -0
- app/db/alembic/versions/20260909_030000_add_audit_actor_columns.py +103 -0
- app/db/alembic/versions/20260909_030000_dashboard_resilience_toggle_settings.py +63 -0
- app/db/alembic/versions/20260909_040000_add_dashboard_user_invites.py +54 -0
- app/db/alembic/versions/20260909_040000_dashboard_timeout_settings.py +68 -0
- app/db/alembic/versions/20260909_050000_dashboard_routing_overload_settings.py +65 -0
- app/db/alembic/versions/20260909_060000_add_report_rollup.py +74 -0
- app/db/alembic/versions/20260909_070000_automation_run_claim_budget.py +50 -0
- app/db/alembic/versions/20260909_080000_dashboard_stream_bridge_budgets.py +62 -0
- app/db/alembic/versions/20260909_090000_dashboard_background_job_toggles.py +63 -0
- app/db/alembic/versions/20260909_100000_dashboard_codex_prewarm.py +49 -0
- app/db/alembic/versions/20260909_110000_model_context_window_overrides.py +45 -0
- app/db/alembic/versions/20260909_120000_dashboard_conversation_archive.py +48 -0
- app/db/alembic/versions/20260909_130000_add_request_logs_live_facet_indexes.py +96 -0
- app/db/alembic/versions/20260910_000000_add_totp_required_for_admin_role.py +44 -0
- app/db/alembic/versions/20260910_000000_request_logs_missing_cost_index.py +52 -0
- app/db/alembic/versions/20260910_010000_add_dashboard_auth_providers.py +134 -0
- app/db/alembic/versions/20260910_010000_dashboard_spool_retention.py +52 -0
- app/db/alembic/versions/20260910_020000_add_dashboard_role_mappings.py +74 -0
- app/db/alembic/versions/20260910_143000_request_log_affinity.py +28 -0
- app/db/alembic/versions/20260910_180000_merge_affinity_guest_heads.py +17 -0
- app/db/alembic/versions/20260910_200000_merge_affinity_identity_heads.py +17 -0
- app/db/alembic/versions/20260910_220000_merge_affinity_invite_heads.py +17 -0
- app/db/alembic/versions/20260911_000000_model_source_pins_kind_expires_index.py +43 -0
- app/db/alembic/versions/20260911_010000_merge_pin_index_and_affinity_heads.py +17 -0
- app/db/alembic/versions/20260911_020000_add_http_bridge_terminal_append_phase.py +54 -0
- app/db/alembic/versions/20260911_030000_add_local_login_policy.py +48 -0
- app/db/alembic/versions/20260911_040000_add_thread_cache_identity_mode.py +48 -0
- app/db/alembic/versions/20260911_060000_add_bridge_session_continuity_abandonment.py +40 -0
- app/db/alembic/versions/20260912_000000_merge_thread_cache_and_bridge_retirement_heads.py +22 -0
- app/db/alembic/versions/20260912_010000_drop_legacy_dashboard_credentials.py +140 -0
- app/db/alembic/versions/20260913_000000_add_oidc_provider_flow.py +134 -0
- app/db/alembic/versions/20260914_000000_add_scim_tokens.py +78 -0
- app/db/alembic/versions/20260914_000000_drop_subscription_overflow_schema.py +132 -0
- app/db/alembic/versions/20260918_000000_merge_scim_and_overflow_heads.py +17 -0
- app/db/alembic/versions/__init__.py +1 -0
- app/db/backup.py +64 -0
- app/db/migrate.py +1249 -0
- app/db/migration_lock.py +160 -0
- app/db/migration_url.py +18 -0
- app/db/models.py +3100 -0
- app/db/recover.py +244 -0
- app/db/session.py +1295 -0
- app/db/snapshot.py +46 -0
- app/db/sqlite_lock_retry.py +190 -0
- app/db/sqlite_utils.py +549 -0
- app/dependencies.py +462 -0
- app/main.py +1240 -0
- app/modules/__init__.py +0 -0
- app/modules/accounts/__init__.py +0 -0
- app/modules/accounts/api.py +426 -0
- app/modules/accounts/auth_manager.py +1439 -0
- app/modules/accounts/background_repository.py +160 -0
- app/modules/accounts/deletion.py +540 -0
- app/modules/accounts/mappers.py +654 -0
- app/modules/accounts/refresh_claims.py +352 -0
- app/modules/accounts/repository.py +1679 -0
- app/modules/accounts/schemas.py +308 -0
- app/modules/accounts/service.py +809 -0
- app/modules/accounts/usage_rollup.py +486 -0
- app/modules/accounts/usage_rollup_scheduler.py +75 -0
- app/modules/accounts/usage_time_rollup.py +1037 -0
- app/modules/accounts/usage_time_rollup_read.py +522 -0
- app/modules/api_keys/__init__.py +1 -0
- app/modules/api_keys/api.py +344 -0
- app/modules/api_keys/last_used_coalescer.py +216 -0
- app/modules/api_keys/limit_windows.py +41 -0
- app/modules/api_keys/repository.py +1200 -0
- app/modules/api_keys/reset_scheduler.py +85 -0
- app/modules/api_keys/schemas.py +135 -0
- app/modules/api_keys/service.py +2123 -0
- app/modules/audit/__init__.py +0 -0
- app/modules/audit/api.py +88 -0
- app/modules/audit/repository.py +74 -0
- app/modules/audit/schemas.py +33 -0
- app/modules/audit/service.py +71 -0
- app/modules/auth_providers/__init__.py +0 -0
- app/modules/auth_providers/api.py +127 -0
- app/modules/auth_providers/config.py +343 -0
- app/modules/auth_providers/repository.py +101 -0
- app/modules/auth_providers/schemas.py +88 -0
- app/modules/auth_providers/seed.py +105 -0
- app/modules/auth_providers/service.py +289 -0
- app/modules/automations/__init__.py +1 -0
- app/modules/automations/api.py +369 -0
- app/modules/automations/repository.py +1896 -0
- app/modules/automations/scheduler.py +107 -0
- app/modules/automations/schemas.py +158 -0
- app/modules/automations/service.py +2548 -0
- app/modules/cache_isolation_probe/__init__.py +0 -0
- app/modules/cache_isolation_probe/api.py +188 -0
- app/modules/cache_isolation_probe/prefix.py +144 -0
- app/modules/cache_isolation_probe/schemas.py +94 -0
- app/modules/cache_isolation_probe/sender.py +229 -0
- app/modules/cache_isolation_probe/service.py +525 -0
- app/modules/conversation_archive/__init__.py +1 -0
- app/modules/conversation_archive/api.py +101 -0
- app/modules/conversation_archive/schemas.py +28 -0
- app/modules/conversation_archive/service.py +165 -0
- app/modules/dashboard/__init__.py +1 -0
- app/modules/dashboard/api.py +106 -0
- app/modules/dashboard/builders.py +107 -0
- app/modules/dashboard/repository.py +304 -0
- app/modules/dashboard/schemas.py +148 -0
- app/modules/dashboard/service.py +534 -0
- app/modules/dashboard/timeframes.py +19 -0
- app/modules/dashboard/weekly_pace.py +617 -0
- app/modules/dashboard_auth/__init__.py +1 -0
- app/modules/dashboard_auth/api.py +1344 -0
- app/modules/dashboard_auth/oidc_api.py +753 -0
- app/modules/dashboard_auth/oidc_flows.py +330 -0
- app/modules/dashboard_auth/repository.py +452 -0
- app/modules/dashboard_auth/schemas.py +232 -0
- app/modules/dashboard_auth/service.py +1384 -0
- app/modules/dashboard_roles/__init__.py +0 -0
- app/modules/dashboard_roles/api.py +64 -0
- app/modules/dashboard_roles/repository.py +54 -0
- app/modules/dashboard_roles/schemas.py +29 -0
- app/modules/dashboard_roles/seed.py +80 -0
- app/modules/dashboard_roles/service.py +142 -0
- app/modules/dashboard_users/__init__.py +0 -0
- app/modules/dashboard_users/api.py +278 -0
- app/modules/dashboard_users/break_glass.py +169 -0
- app/modules/dashboard_users/credentials.py +18 -0
- app/modules/dashboard_users/identity_resolver.py +665 -0
- app/modules/dashboard_users/repository.py +725 -0
- app/modules/dashboard_users/schemas.py +117 -0
- app/modules/dashboard_users/service.py +1054 -0
- app/modules/firewall/__init__.py +1 -0
- app/modules/firewall/api.py +78 -0
- app/modules/firewall/repository.py +51 -0
- app/modules/firewall/schemas.py +28 -0
- app/modules/firewall/service.py +93 -0
- app/modules/fleet/__init__.py +1 -0
- app/modules/fleet/api.py +177 -0
- app/modules/fleet/mappers.py +56 -0
- app/modules/fleet/observability.py +372 -0
- app/modules/fleet/schemas.py +119 -0
- app/modules/health/__init__.py +0 -0
- app/modules/health/api.py +246 -0
- app/modules/health/schemas.py +27 -0
- app/modules/limit_warmup/__init__.py +1 -0
- app/modules/limit_warmup/repository.py +159 -0
- app/modules/limit_warmup/service.py +1001 -0
- app/modules/model_sources/__init__.py +1 -0
- app/modules/model_sources/api.py +99 -0
- app/modules/model_sources/catalog.py +307 -0
- app/modules/model_sources/forwarding.py +1435 -0
- app/modules/model_sources/projection.py +55 -0
- app/modules/model_sources/repository.py +201 -0
- app/modules/model_sources/schemas.py +79 -0
- app/modules/model_sources/selection.py +166 -0
- app/modules/model_sources/service.py +236 -0
- app/modules/oauth/__init__.py +0 -0
- app/modules/oauth/api.py +101 -0
- app/modules/oauth/repository.py +238 -0
- app/modules/oauth/schemas.py +49 -0
- app/modules/oauth/service.py +1196 -0
- app/modules/oauth/templates/oauth_success.html +122 -0
- app/modules/proxy/__init__.py +0 -0
- app/modules/proxy/_load_balancer/__init__.py +1 -0
- app/modules/proxy/_load_balancer/error_rate.py +116 -0
- app/modules/proxy/_load_balancer/latency_cohort.py +154 -0
- app/modules/proxy/_load_balancer/model_eligibility.py +250 -0
- app/modules/proxy/_load_balancer/opportunistic_admission.py +350 -0
- app/modules/proxy/_load_balancer/overload_backoff.py +394 -0
- app/modules/proxy/_load_balancer/sticky_selection.py +2417 -0
- app/modules/proxy/_load_balancer/throughput_cohort.py +233 -0
- app/modules/proxy/_load_balancer/ttft_cohort.py +214 -0
- app/modules/proxy/_load_balancer/tunables.py +97 -0
- app/modules/proxy/_load_balancer/types.py +96 -0
- app/modules/proxy/_load_balancer/unbound_selection.py +534 -0
- app/modules/proxy/_service/__init__.py +6 -0
- app/modules/proxy/_service/api_key_usage.py +745 -0
- app/modules/proxy/_service/clock_budget.py +38 -0
- app/modules/proxy/_service/codex_control.py +611 -0
- app/modules/proxy/_service/compact.py +2211 -0
- app/modules/proxy/_service/file_ops.py +749 -0
- app/modules/proxy/_service/http_bridge/__init__.py +3 -0
- app/modules/proxy/_service/http_bridge/accepted_replay.py +331 -0
- app/modules/proxy/_service/http_bridge/account_sessions.py +57 -0
- app/modules/proxy/_service/http_bridge/activity.py +178 -0
- app/modules/proxy/_service/http_bridge/helpers.py +3959 -0
- app/modules/proxy/_service/http_bridge/mixin.py +2435 -0
- app/modules/proxy/_service/http_bridge/owner_forwarding.py +609 -0
- app/modules/proxy/_service/http_bridge/protocol.py +167 -0
- app/modules/proxy/_service/http_bridge/proxy_failover.py +58 -0
- app/modules/proxy/_service/http_bridge/quarantine.py +443 -0
- app/modules/proxy/_service/http_bridge/request_submit.py +4582 -0
- app/modules/proxy/_service/http_bridge/retry_circuit.py +2442 -0
- app/modules/proxy/_service/http_bridge/service_stubs.py +492 -0
- app/modules/proxy/_service/http_bridge/session_registry.py +778 -0
- app/modules/proxy/_service/http_bridge/streaming.py +5328 -0
- app/modules/proxy/_service/http_bridge/upstream_events.py +4538 -0
- app/modules/proxy/_service/observability.py +410 -0
- app/modules/proxy/_service/rate_limit.py +361 -0
- app/modules/proxy/_service/realtime_live.py +655 -0
- app/modules/proxy/_service/refresh.py +154 -0
- app/modules/proxy/_service/request_log.py +585 -0
- app/modules/proxy/_service/response_create.py +930 -0
- app/modules/proxy/_service/streaming/__init__.py +3 -0
- app/modules/proxy/_service/streaming/helpers.py +1317 -0
- app/modules/proxy/_service/streaming/mixin.py +1100 -0
- app/modules/proxy/_service/streaming/protocol.py +32 -0
- app/modules/proxy/_service/streaming/retry.py +3762 -0
- app/modules/proxy/_service/support.py +2122 -0
- app/modules/proxy/_service/transcribe.py +434 -0
- app/modules/proxy/_service/warmup.py +483 -0
- app/modules/proxy/_service/websocket/__init__.py +3 -0
- app/modules/proxy/_service/websocket/helpers.py +2360 -0
- app/modules/proxy/_service/websocket/mixin.py +7285 -0
- app/modules/proxy/_service/websocket/protocol.py +74 -0
- app/modules/proxy/account_cache.py +229 -0
- app/modules/proxy/account_eligibility.py +87 -0
- app/modules/proxy/additional_model_limits.py +40 -0
- app/modules/proxy/affinity.py +830 -0
- app/modules/proxy/affinity_observation.py +101 -0
- app/modules/proxy/api.py +10254 -0
- app/modules/proxy/api_key_usage.py +113 -0
- app/modules/proxy/cap_partitioning.py +280 -0
- app/modules/proxy/capability_lineage.py +65 -0
- app/modules/proxy/capability_lineage_repository.py +82 -0
- app/modules/proxy/capability_routing.py +253 -0
- app/modules/proxy/continuity.py +112 -0
- app/modules/proxy/downstream_delivery.py +277 -0
- app/modules/proxy/durable_bridge_coordinator.py +1217 -0
- app/modules/proxy/durable_bridge_repository.py +4516 -0
- app/modules/proxy/durable_bridge_runtime.py +9 -0
- app/modules/proxy/durable_bridge_transcript_codec.py +122 -0
- app/modules/proxy/fair_share.py +131 -0
- app/modules/proxy/file_pin_repository.py +210 -0
- app/modules/proxy/helpers.py +439 -0
- app/modules/proxy/http_bridge_event_batcher.py +613 -0
- app/modules/proxy/http_bridge_forwarding.py +797 -0
- app/modules/proxy/http_continuation.py +76 -0
- app/modules/proxy/images_observability.py +62 -0
- app/modules/proxy/images_service.py +1054 -0
- app/modules/proxy/load_balancer.py +3016 -0
- app/modules/proxy/rate_limit_cache.py +46 -0
- app/modules/proxy/replay_safety.py +1063 -0
- app/modules/proxy/repo_bundle.py +31 -0
- app/modules/proxy/request_policy.py +1054 -0
- app/modules/proxy/ring_membership.py +251 -0
- app/modules/proxy/schemas.py +345 -0
- app/modules/proxy/selection_errors.py +41 -0
- app/modules/proxy/service.py +2600 -0
- app/modules/proxy/source_admission.py +125 -0
- app/modules/proxy/source_dispatch.py +999 -0
- app/modules/proxy/sticky_repository.py +831 -0
- app/modules/proxy/thread_anchors.py +491 -0
- app/modules/proxy/tool_call_dedupe.py +802 -0
- app/modules/proxy/types.py +54 -0
- app/modules/proxy/work_admission.py +121 -0
- app/modules/quota_planner/__init__.py +1 -0
- app/modules/quota_planner/api.py +276 -0
- app/modules/quota_planner/logic.py +743 -0
- app/modules/quota_planner/repository.py +722 -0
- app/modules/quota_planner/scheduler.py +212 -0
- app/modules/quota_planner/schemas.py +102 -0
- app/modules/quota_planner/warmup.py +693 -0
- app/modules/rate_limit_reset_credits/__init__.py +0 -0
- app/modules/rate_limit_reset_credits/api.py +601 -0
- app/modules/rate_limit_reset_credits/redeem_coordination.py +274 -0
- app/modules/rate_limit_reset_credits/store.py +114 -0
- app/modules/reports/__init__.py +1 -0
- app/modules/reports/api.py +134 -0
- app/modules/reports/cache.py +74 -0
- app/modules/reports/filters.py +27 -0
- app/modules/reports/repository.py +525 -0
- app/modules/reports/rollup.py +150 -0
- app/modules/reports/rollup_read.py +148 -0
- app/modules/reports/schemas.py +123 -0
- app/modules/reports/service.py +287 -0
- app/modules/reports/thread_identity.py +256 -0
- app/modules/request_logs/__init__.py +0 -0
- app/modules/request_logs/api.py +224 -0
- app/modules/request_logs/cost_backfill.py +154 -0
- app/modules/request_logs/mappers.py +97 -0
- app/modules/request_logs/repository.py +1765 -0
- app/modules/request_logs/schemas.py +145 -0
- app/modules/request_logs/service.py +348 -0
- app/modules/role_mappings/__init__.py +0 -0
- app/modules/role_mappings/api.py +206 -0
- app/modules/role_mappings/matching.py +55 -0
- app/modules/role_mappings/repository.py +106 -0
- app/modules/role_mappings/schemas.py +67 -0
- app/modules/role_mappings/service.py +334 -0
- app/modules/runtime/__init__.py +1 -0
- app/modules/runtime/api.py +18 -0
- app/modules/runtime/schemas.py +14 -0
- app/modules/runtime/service.py +218 -0
- app/modules/scim/__init__.py +1 -0
- app/modules/scim/api.py +194 -0
- app/modules/scim/dependencies.py +112 -0
- app/modules/scim/errors.py +77 -0
- app/modules/scim/management_api.py +156 -0
- app/modules/scim/repository.py +212 -0
- app/modules/scim/schemas.py +145 -0
- app/modules/scim/service.py +786 -0
- app/modules/scim/tokens.py +43 -0
- app/modules/settings/__init__.py +1 -0
- app/modules/settings/api.py +1677 -0
- app/modules/settings/repository.py +606 -0
- app/modules/settings/schemas.py +496 -0
- app/modules/settings/service.py +769 -0
- app/modules/shared/__init__.py +0 -0
- app/modules/shared/schemas.py +22 -0
- app/modules/sticky_sessions/__init__.py +1 -0
- app/modules/sticky_sessions/api.py +118 -0
- app/modules/sticky_sessions/cleanup_scheduler.py +487 -0
- app/modules/sticky_sessions/schemas.py +78 -0
- app/modules/sticky_sessions/service.py +196 -0
- app/modules/telemetry/__init__.py +5 -0
- app/modules/telemetry/api.py +115 -0
- app/modules/telemetry/clients.py +62 -0
- app/modules/telemetry/consent.py +115 -0
- app/modules/telemetry/scheduler.py +85 -0
- app/modules/telemetry/schemas.py +163 -0
- app/modules/telemetry/sender.py +221 -0
- app/modules/telemetry/snapshot.py +490 -0
- app/modules/usage/__init__.py +0 -0
- app/modules/usage/additional_quota_keys.py +379 -0
- app/modules/usage/api.py +45 -0
- app/modules/usage/background_repository.py +147 -0
- app/modules/usage/builders.py +559 -0
- app/modules/usage/depletion_service.py +277 -0
- app/modules/usage/live_ingest.py +356 -0
- app/modules/usage/mappers.py +29 -0
- app/modules/usage/plan_downgrade_observations.py +488 -0
- app/modules/usage/repository.py +1890 -0
- app/modules/usage/schemas.py +68 -0
- app/modules/usage/service.py +102 -0
- app/modules/usage/updater.py +1488 -0
- app/static/assets/Trans-Cj-J9mVA.js +1 -0
- app/static/assets/access-page-DHzZiXZi.js +1 -0
- app/static/assets/account-cost-donut-DGhfA4U8.js +1 -0
- app/static/assets/account-identifiers-CxbdLshZ.js +1 -0
- app/static/assets/account-status-DIsU5sUS.js +1 -0
- app/static/assets/account-trend-chart-BptlfwwQ.js +1 -0
- app/static/assets/accounts-page-Bbai8ift.js +3 -0
- app/static/assets/api-Dwa7XxPe.js +1 -0
- app/static/assets/api-client-dugX1Pti.js +55 -0
- app/static/assets/api-key-create-dialog-DnRm0oYN.js +1 -0
- app/static/assets/api-key-created-dialog-B6jkDG9f.js +1 -0
- app/static/assets/api-key-edit-dialog-Ck3Bnc3V.js +1 -0
- app/static/assets/api-trend-chart-BchCw7Hj.js +1 -0
- app/static/assets/apis-page-BvXz9c6i.js +2 -0
- app/static/assets/arrow-up-Dqh_KzJo.js +1 -0
- app/static/assets/arrow-up-down-LBRgVDGY.js +1 -0
- app/static/assets/automations-page-DQgNAJOR.js +3 -0
- app/static/assets/badge-DqMb9es0.js +1 -0
- app/static/assets/blur-email-HEvKagG2.js +1 -0
- app/static/assets/button-BjVmmiGW.js +1 -0
- app/static/assets/checkbox-apsl9vzk.js +1 -0
- app/static/assets/chevron-left-1d_pHVa5.js +1 -0
- app/static/assets/circle-alert-BS0xd_o1.js +1 -0
- app/static/assets/clsx-DB0hHKMi.js +1 -0
- app/static/assets/colors-BBy8GEtr.js +1 -0
- app/static/assets/copy-button-D7nAvOKR.js +1 -0
- app/static/assets/cost-per-day-chart-YdqlkZVM.js +1 -0
- app/static/assets/createLucideIcon-DBLzLNh2.js +1 -0
- app/static/assets/dashboard-page-CSdhBy0f.js +2 -0
- app/static/assets/distribution-metric-format-yZTy1ZEu.js +1 -0
- app/static/assets/donut-chart-Bxc69cra.js +1 -0
- app/static/assets/download-kQteIcsB.js +1 -0
- app/static/assets/ellipsis-B_5OgIre.js +1 -0
- app/static/assets/empty-state-JcA7tc8m.js +1 -0
- app/static/assets/es2015-bMH8YKZT.js +41 -0
- app/static/assets/es6-B264ni6W.js +69 -0
- app/static/assets/external-link-UVAOgMQ0.js +1 -0
- app/static/assets/i18n-BWqZ9Ua2.js +1 -0
- app/static/assets/inbox-B_RM2s1J.js +1 -0
- app/static/assets/index-DIpd264Z.js +23 -0
- app/static/assets/index-DMeLKRJx.css +2 -0
- app/static/assets/inherit-badge-QhTYVaU6.js +1 -0
- app/static/assets/invite-dialog-0CYh0GiQ.js +1 -0
- app/static/assets/iso-Bpe5Ihbb.js +1 -0
- app/static/assets/label-CGaLmLNf.js +1 -0
- app/static/assets/lazy-recharts-Pv8p9ArM.js +2 -0
- app/static/assets/loading-overlay-CzKQfHLI.js +1 -0
- app/static/assets/mini-quota-bar-cGT_t8Ae.js +1 -0
- app/static/assets/model-distribution-donut-BKjAfT48.js +1 -0
- app/static/assets/model-source-multi-select-D2d06Vc7.js +1 -0
- app/static/assets/multi-select-filter-KIA5qU6Z.js +1 -0
- app/static/assets/oauth-dialog-XeaGGDjS.js +1 -0
- app/static/assets/pagination-controls-D1Ypd0RU.js +1 -0
- app/static/assets/payload-CIYnGZz9.js +1 -0
- app/static/assets/queue-wait-chart-VoV7Yg9L.js +1 -0
- app/static/assets/report-chart-card-BXDliwcc.js +1 -0
- app/static/assets/reports-page-CGCJeMHG.js +3 -0
- app/static/assets/reset-credit-confirm-dialog-QOifTnVB.js +1 -0
- app/static/assets/rolldown-runtime-hePW80VL.js +1 -0
- app/static/assets/search-HfhqwERt.js +1 -0
- app/static/assets/select-Cl33bz-g.js +1 -0
- app/static/assets/settings-page-mZj01HYl.js +2 -0
- app/static/assets/shim-CZwyM4Q7.js +1 -0
- app/static/assets/skeleton-C_EUc4Rp.js +1 -0
- app/static/assets/sparkline-chart-DdNNsddM.js +1 -0
- app/static/assets/status-badge-CHHA5X6c.js +1 -0
- app/static/assets/switch-CzxgokcM.js +1 -0
- app/static/assets/table-DXaNSQ0G.js +1 -0
- app/static/assets/time-to-first-token-chart--68L0u_v.js +1 -0
- app/static/assets/tokens-per-day-chart-Cs82BFuE.js +1 -0
- app/static/assets/tokens-per-second-chart-AD2dfOn6.js +1 -0
- app/static/assets/totp-settings-CRHtxQ7j.js +1 -0
- app/static/assets/trash-D3KyWr1h.js +1 -0
- app/static/assets/use-account-quota-display-BPmrTBTC.js +1 -0
- app/static/assets/use-accounts-PWlVGw3z.js +1 -0
- app/static/assets/use-auth-CuDlW3iC.js +1 -0
- app/static/assets/use-chart-colors-DIu6tClI.js +1 -0
- app/static/assets/use-model-sources-Bul24JBu.js +1 -0
- app/static/assets/use-models-DUCPUuZg.js +1 -0
- app/static/assets/use-reduced-motion-hAm5r6_d.js +1 -0
- app/static/assets/useTranslation--56nHhlH.js +1 -0
- app/static/assets/useragent-distribution-donut-Dup6sf_n.js +1 -0
- app/static/assets/users-uIC3TIlE.js +1 -0
- app/static/assets/vendor-query-_bHZuxEx.js +1 -0
- app/static/assets/vendor-react-BVL0YZPW.js +9 -0
- app/static/favicon.svg +10 -0
- app/static/fonts/GeistSans-Variable.woff2 +0 -0
- app/static/fonts/jetbrains-mono-latin-ext.woff2 +0 -0
- app/static/fonts/jetbrains-mono-latin.woff2 +0 -0
- app/static/index.html +29 -0
- codexlb_up-1.25.0b10.dist-info/METADATA +124 -0
- codexlb_up-1.25.0b10.dist-info/RECORD +827 -0
- codexlb_up-1.25.0b10.dist-info/WHEEL +4 -0
- codexlb_up-1.25.0b10.dist-info/entry_points.txt +3 -0
- codexlb_up-1.25.0b10.dist-info/licenses/LICENSE +21 -0
- config/additional_quota_registry.json +11 -0
|
@@ -0,0 +1,2435 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import inspect
|
|
5
|
+
import logging
|
|
6
|
+
from collections import deque
|
|
7
|
+
from collections.abc import Collection
|
|
8
|
+
from dataclasses import replace
|
|
9
|
+
from typing import Any, Literal, TypeVar, overload
|
|
10
|
+
from uuid import uuid4
|
|
11
|
+
|
|
12
|
+
import aiohttp
|
|
13
|
+
|
|
14
|
+
from app.core import shutdown as shutdown_state
|
|
15
|
+
from app.core.auth.refresh import RefreshError
|
|
16
|
+
from app.core.clients.files import create_file as core_create_file # noqa: F401
|
|
17
|
+
from app.core.clients.files import finalize_file as core_finalize_file # noqa: F401
|
|
18
|
+
from app.core.clients.proxy import CodexControlResponse as CodexControlResponse
|
|
19
|
+
from app.core.clients.proxy import ( # noqa: F401 # noqa: F401
|
|
20
|
+
ImageFetchSession,
|
|
21
|
+
ProxyResponseError,
|
|
22
|
+
UpstreamProxyRouteTrace,
|
|
23
|
+
_as_image_fetch_session,
|
|
24
|
+
_inline_content_images,
|
|
25
|
+
_inline_input_image_urls,
|
|
26
|
+
_ws_transport_payload_budget_bytes,
|
|
27
|
+
filter_inbound_headers,
|
|
28
|
+
pop_compact_timeout_overrides,
|
|
29
|
+
pop_stream_timeout_overrides,
|
|
30
|
+
pop_transcribe_timeout_overrides,
|
|
31
|
+
push_compact_timeout_overrides,
|
|
32
|
+
push_stream_timeout_overrides,
|
|
33
|
+
push_transcribe_timeout_overrides,
|
|
34
|
+
)
|
|
35
|
+
from app.core.clients.proxy import codex_control_request as core_codex_control_request # noqa: F401
|
|
36
|
+
from app.core.clients.proxy import compact_responses as core_compact_responses # noqa: F401
|
|
37
|
+
from app.core.clients.proxy import transcribe_audio as core_transcribe_audio # noqa: F401
|
|
38
|
+
from app.core.clock import clock_for, scheduler_for
|
|
39
|
+
from app.core.errors import openai_error
|
|
40
|
+
from app.core.metrics.prometheus import (
|
|
41
|
+
PROMETHEUS_AVAILABLE,
|
|
42
|
+
bridge_durable_recover_total,
|
|
43
|
+
bridge_local_rebind_total,
|
|
44
|
+
bridge_owner_mismatch_total,
|
|
45
|
+
bridge_prompt_cache_locality_miss_total,
|
|
46
|
+
bridge_soft_local_rebind_total,
|
|
47
|
+
)
|
|
48
|
+
from app.core.utils.locks import fast_lock
|
|
49
|
+
from app.core.utils.request_id import ensure_request_scope_id
|
|
50
|
+
from app.db.models import (
|
|
51
|
+
DashboardSettings,
|
|
52
|
+
StickySessionKind,
|
|
53
|
+
)
|
|
54
|
+
from app.modules.api_keys.service import (
|
|
55
|
+
ApiKeyData,
|
|
56
|
+
ApiKeyRequestUsageBudget,
|
|
57
|
+
)
|
|
58
|
+
from app.modules.proxy._service.api_key_usage import (
|
|
59
|
+
_API_KEY_RESERVATION_HEARTBEAT_SECONDS as _API_KEY_RESERVATION_HEARTBEAT_SECONDS,
|
|
60
|
+
)
|
|
61
|
+
from app.modules.proxy._service.compact import (
|
|
62
|
+
_sticky_key_for_compact_request as _sticky_key_for_compact_request,
|
|
63
|
+
)
|
|
64
|
+
from app.modules.proxy._service.compact import (
|
|
65
|
+
_sticky_key_from_compact_payload as _sticky_key_from_compact_payload,
|
|
66
|
+
)
|
|
67
|
+
from app.modules.proxy._service.http_bridge.account_sessions import _HTTPBridgeAccountSessionsMixin
|
|
68
|
+
from app.modules.proxy._service.http_bridge.activity import _HTTPBridgeActivityMixin
|
|
69
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
70
|
+
_HTTP_BRIDGE_BACKGROUND_CLOSE_TIMEOUT_SECONDS,
|
|
71
|
+
_HTTP_BRIDGE_INFLIGHT_STARTED_AT_ATTR,
|
|
72
|
+
_active_http_bridge_instance_ring,
|
|
73
|
+
_alias_fallback_key,
|
|
74
|
+
_durable_bridge_lookup_active_owner,
|
|
75
|
+
_durable_bridge_lookup_allows_local_reuse,
|
|
76
|
+
_forwarded_http_bridge_session_key,
|
|
77
|
+
_http_bridge_alias_target_is_stale,
|
|
78
|
+
_http_bridge_allow_durable_takeover,
|
|
79
|
+
_http_bridge_can_local_recover_without_ring,
|
|
80
|
+
_http_bridge_can_recover_during_drain,
|
|
81
|
+
_http_bridge_can_single_instance_owner_takeover_without_anchor,
|
|
82
|
+
_http_bridge_can_single_instance_prompt_cache_takeover_without_anchor,
|
|
83
|
+
_http_bridge_capacity_after_planned_closes,
|
|
84
|
+
_http_bridge_claim_allows_takeover,
|
|
85
|
+
_http_bridge_compatible,
|
|
86
|
+
_http_bridge_continuity_lost_error_envelope,
|
|
87
|
+
_http_bridge_endpoint_matches_current_instance,
|
|
88
|
+
_http_bridge_has_durable_recovery_anchor,
|
|
89
|
+
_http_bridge_incompatible_model_fork_key,
|
|
90
|
+
_http_bridge_inflight_creation_count,
|
|
91
|
+
_http_bridge_key_strength,
|
|
92
|
+
_http_bridge_locally_owned_fork_key,
|
|
93
|
+
_http_bridge_models_compatible,
|
|
94
|
+
_http_bridge_owner_check_required,
|
|
95
|
+
_http_bridge_owner_instance,
|
|
96
|
+
_http_bridge_owner_lookup_unavailable_error_envelope,
|
|
97
|
+
_http_bridge_parallel_fork_key,
|
|
98
|
+
_http_bridge_previous_response_alias_key,
|
|
99
|
+
_http_bridge_previous_response_owner_unavailable_error,
|
|
100
|
+
_http_bridge_reconnect_connect_failure,
|
|
101
|
+
_http_bridge_reconnect_selection_failure,
|
|
102
|
+
_http_bridge_reconnect_turn_state,
|
|
103
|
+
_http_bridge_request_budget_seconds,
|
|
104
|
+
_http_bridge_request_needs_unanchored_handoff,
|
|
105
|
+
_http_bridge_session_account_active,
|
|
106
|
+
_http_bridge_session_allows_api_key,
|
|
107
|
+
_http_bridge_session_generation_count,
|
|
108
|
+
_http_bridge_session_has_admission_waiter,
|
|
109
|
+
_http_bridge_session_matches_preferred_account,
|
|
110
|
+
_http_bridge_session_retiring_with_visible_requests,
|
|
111
|
+
_http_bridge_session_reusable_for_lookup,
|
|
112
|
+
_http_bridge_session_reusable_for_request,
|
|
113
|
+
_http_bridge_should_wait_for_registration,
|
|
114
|
+
_http_bridge_startup_wait_timeout_error,
|
|
115
|
+
_http_bridge_turn_state_alias_key,
|
|
116
|
+
_log_http_bridge_event,
|
|
117
|
+
_log_http_bridge_startup_wait_timeout,
|
|
118
|
+
_mark_http_bridge_reader_handoff_reconnect_failed,
|
|
119
|
+
_persist_http_bridge_replacement_account,
|
|
120
|
+
_persistent_http_bridge_affinity,
|
|
121
|
+
_plan_http_bridge_lru_capacity_closes,
|
|
122
|
+
_raise_if_http_bridge_creation_superseded,
|
|
123
|
+
_record_bridge_drain_recovery_allowed,
|
|
124
|
+
_record_bridge_first_turn_timeout,
|
|
125
|
+
_refresh_reused_http_bridge_session_with_handoff,
|
|
126
|
+
_register_http_bridge_turn_state_aliases_locked,
|
|
127
|
+
_require_http_bridge_bound_account_not_excluded,
|
|
128
|
+
_reserve_http_bridge_unanchored_handoff,
|
|
129
|
+
_settle_failed_http_bridge_creation,
|
|
130
|
+
_turn_keys,
|
|
131
|
+
)
|
|
132
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
133
|
+
_close_http_bridge_session as _helpers_close_http_bridge_session,
|
|
134
|
+
)
|
|
135
|
+
from app.modules.proxy._service.http_bridge.owner_forwarding import _HTTPBridgeOwnerForwardingMixin
|
|
136
|
+
from app.modules.proxy._service.http_bridge.protocol import _HTTPBridgeServiceProtocol
|
|
137
|
+
from app.modules.proxy._service.http_bridge.proxy_failover import _HTTPBridgePreDispatchFailover
|
|
138
|
+
from app.modules.proxy._service.http_bridge.quarantine import (
|
|
139
|
+
_http_bridge_session_key_quarantined,
|
|
140
|
+
)
|
|
141
|
+
from app.modules.proxy._service.http_bridge.request_submit import _HTTPBridgeRequestSubmitMixin
|
|
142
|
+
from app.modules.proxy._service.http_bridge.service_stubs import (
|
|
143
|
+
_call_with_supported_optional_kwargs,
|
|
144
|
+
_estimated_lease_tokens_from_request_usage_budget,
|
|
145
|
+
_prefer_earlier_reset_window,
|
|
146
|
+
_proxy_admission_wait_timeout_seconds,
|
|
147
|
+
_raise_proxy_unavailable,
|
|
148
|
+
_record_continuity_fail_closed,
|
|
149
|
+
_record_same_account_takeover,
|
|
150
|
+
_routing_strategy,
|
|
151
|
+
_service_get_settings,
|
|
152
|
+
_service_get_settings_cache,
|
|
153
|
+
_upstream_turn_state_from_socket,
|
|
154
|
+
_websocket_connect_deadline,
|
|
155
|
+
_websocket_safe_headers_with_turn_state,
|
|
156
|
+
)
|
|
157
|
+
from app.modules.proxy._service.http_bridge.session_registry import _HTTPBridgeSessionRegistryMixin
|
|
158
|
+
from app.modules.proxy._service.http_bridge.streaming import _HTTPBridgeStreamingMixin
|
|
159
|
+
from app.modules.proxy._service.http_bridge.upstream_events import _HTTPBridgeUpstreamEventsMixin
|
|
160
|
+
from app.modules.proxy._service.observability import _hash_identifier
|
|
161
|
+
from app.modules.proxy._service.support import (
|
|
162
|
+
_ACCOUNT_MODEL_UNSUPPORTED_ERROR_CODE,
|
|
163
|
+
_HARD_HTTP_BRIDGE_AFFINITY_KINDS, # noqa: F401
|
|
164
|
+
_WEBSOCKET_FULL_REPLAY_WAIT_POLL_SECONDS, # noqa: F401
|
|
165
|
+
_clear_websocket_precreated_replay_fallback,
|
|
166
|
+
_complete_http_bridge_handoff,
|
|
167
|
+
_copy_websocket_route_metadata_to_session,
|
|
168
|
+
_DeferredAccountBackoffLifecycle,
|
|
169
|
+
_HTTPBridgeOwnerForward,
|
|
170
|
+
_HTTPBridgeSession,
|
|
171
|
+
_HTTPBridgeSessionKey,
|
|
172
|
+
_sleep_for_account_selection_recovery,
|
|
173
|
+
_WebSocketRequestState,
|
|
174
|
+
_WebSocketUpstreamControl,
|
|
175
|
+
)
|
|
176
|
+
from app.modules.proxy._service.support import (
|
|
177
|
+
_websocket_route_log_kwargs as _websocket_route_log_kwargs,
|
|
178
|
+
)
|
|
179
|
+
from app.modules.proxy._service.warmup import (
|
|
180
|
+
WarmupExecutionData as WarmupExecutionData,
|
|
181
|
+
)
|
|
182
|
+
from app.modules.proxy._service.warmup import (
|
|
183
|
+
WarmupFailedAccountData as WarmupFailedAccountData,
|
|
184
|
+
)
|
|
185
|
+
from app.modules.proxy._service.warmup import (
|
|
186
|
+
WarmupSkippedAccountData as WarmupSkippedAccountData,
|
|
187
|
+
)
|
|
188
|
+
from app.modules.proxy._service.warmup import (
|
|
189
|
+
WarmupSubmittedAccountData as WarmupSubmittedAccountData,
|
|
190
|
+
)
|
|
191
|
+
from app.modules.proxy._service.warmup import (
|
|
192
|
+
_is_warmup_usage_eligible as _is_warmup_usage_eligible,
|
|
193
|
+
)
|
|
194
|
+
from app.modules.proxy._service.warmup import (
|
|
195
|
+
_materialize_warmup_account as _materialize_warmup_account,
|
|
196
|
+
)
|
|
197
|
+
from app.modules.proxy._service.warmup import (
|
|
198
|
+
_snapshot_warmup_account as _snapshot_warmup_account,
|
|
199
|
+
)
|
|
200
|
+
from app.modules.proxy._service.warmup import (
|
|
201
|
+
_WarmupAccountSnapshot as _WarmupAccountSnapshot,
|
|
202
|
+
)
|
|
203
|
+
from app.modules.proxy._service.warmup import (
|
|
204
|
+
_WarmupSubmitResult as _WarmupSubmitResult,
|
|
205
|
+
)
|
|
206
|
+
from app.modules.proxy._service.warmup import (
|
|
207
|
+
_WarmupUsageSnapshot as _WarmupUsageSnapshot,
|
|
208
|
+
)
|
|
209
|
+
from app.modules.proxy.account_eligibility import ROUTABLE_STATUSES
|
|
210
|
+
from app.modules.proxy.account_eligibility import account_access_token_expires_at as _token_expiry
|
|
211
|
+
from app.modules.proxy.affinity import (
|
|
212
|
+
_AffinityPolicy,
|
|
213
|
+
_extract_model_class,
|
|
214
|
+
_sticky_key_from_turn_state_header,
|
|
215
|
+
)
|
|
216
|
+
from app.modules.proxy.continuity import (
|
|
217
|
+
is_http_bridge_account_neutral_replay,
|
|
218
|
+
resolve_reconnect_preferred_account_id,
|
|
219
|
+
resolve_required_account_id,
|
|
220
|
+
without_http_bridge_session_affinity_headers,
|
|
221
|
+
)
|
|
222
|
+
from app.modules.proxy.durable_bridge_coordinator import DurableBridgeLookup
|
|
223
|
+
from app.modules.proxy.load_balancer import CONTINUITY_OWNER_UNAVAILABLE, AccountLease
|
|
224
|
+
from app.modules.proxy.selection_errors import USAGE_LIMIT_REACHED, selection_failure_response
|
|
225
|
+
|
|
226
|
+
logger = logging.getLogger("app.modules.proxy.service")
|
|
227
|
+
T = TypeVar("T")
|
|
228
|
+
_REQUEST_TRANSPORT_HTTP = "http"
|
|
229
|
+
_UPSTREAM_CLOSE_CODES_SKIP_SAME_ACCOUNT_RETRY = frozenset({1011})
|
|
230
|
+
_HTTP_BRIDGE_BACKGROUND_CLEANUP_WARN_THRESHOLD = 100
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class _HTTPBridgeMixin(
|
|
234
|
+
_HTTPBridgeStreamingMixin,
|
|
235
|
+
_HTTPBridgeSessionRegistryMixin,
|
|
236
|
+
_HTTPBridgeAccountSessionsMixin,
|
|
237
|
+
_HTTPBridgeActivityMixin,
|
|
238
|
+
_HTTPBridgeOwnerForwardingMixin,
|
|
239
|
+
_HTTPBridgeRequestSubmitMixin,
|
|
240
|
+
_HTTPBridgeUpstreamEventsMixin,
|
|
241
|
+
_HTTPBridgeServiceProtocol,
|
|
242
|
+
):
|
|
243
|
+
def _schedule_http_bridge_session_closes(
|
|
244
|
+
self,
|
|
245
|
+
sessions: list["_HTTPBridgeSession"],
|
|
246
|
+
*,
|
|
247
|
+
reason: str,
|
|
248
|
+
) -> None:
|
|
249
|
+
for session in sessions:
|
|
250
|
+
if len(self._background_cleanup_tasks) >= _HTTP_BRIDGE_BACKGROUND_CLEANUP_WARN_THRESHOLD:
|
|
251
|
+
logger.warning(
|
|
252
|
+
"http_bridge_background_cleanup_backlog action=session_close count=%d threshold=%d reason=%s",
|
|
253
|
+
len(self._background_cleanup_tasks),
|
|
254
|
+
_HTTP_BRIDGE_BACKGROUND_CLEANUP_WARN_THRESHOLD,
|
|
255
|
+
reason,
|
|
256
|
+
)
|
|
257
|
+
self._schedule_cancel_safe_cleanup(
|
|
258
|
+
self._close_http_bridge_session_bounded(session, reason=reason),
|
|
259
|
+
action="http_bridge_session_close",
|
|
260
|
+
request_id=_hash_identifier(session.key.affinity_key),
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
async def _drain_http_bridge_background_cleanup_tasks(self, *, reason: str) -> bool:
|
|
264
|
+
tasks = [
|
|
265
|
+
task
|
|
266
|
+
for task in self._background_cleanup_tasks
|
|
267
|
+
if (
|
|
268
|
+
task.get_name().startswith("proxy-http_bridge_session_close-")
|
|
269
|
+
or task.get_name().startswith("http-bridge-close-")
|
|
270
|
+
or task.get_name().startswith("cancelled-task-cleanup-")
|
|
271
|
+
)
|
|
272
|
+
]
|
|
273
|
+
if not tasks:
|
|
274
|
+
return not self._http_bridge_background_cleanup_failed
|
|
275
|
+
try:
|
|
276
|
+
results = await asyncio.wait_for(
|
|
277
|
+
asyncio.gather(*(asyncio.shield(task) for task in tasks), return_exceptions=True),
|
|
278
|
+
timeout=_HTTP_BRIDGE_BACKGROUND_CLOSE_TIMEOUT_SECONDS,
|
|
279
|
+
)
|
|
280
|
+
except TimeoutError:
|
|
281
|
+
logger.warning(
|
|
282
|
+
"http_bridge_background_cleanup_drain_timeout reason=%s count=%d timeout_seconds=%.1f",
|
|
283
|
+
reason,
|
|
284
|
+
len(tasks),
|
|
285
|
+
_HTTP_BRIDGE_BACKGROUND_CLOSE_TIMEOUT_SECONDS,
|
|
286
|
+
)
|
|
287
|
+
return False
|
|
288
|
+
self._http_bridge_background_cleanup_failed |= any(isinstance(result, BaseException) for result in results)
|
|
289
|
+
return not self._http_bridge_background_cleanup_failed
|
|
290
|
+
|
|
291
|
+
async def _fail_http_bridge_inflight_session_creation(
|
|
292
|
+
self,
|
|
293
|
+
key: "_HTTPBridgeSessionKey",
|
|
294
|
+
inflight_future: asyncio.Future["_HTTPBridgeSession"] | None,
|
|
295
|
+
exc: BaseException,
|
|
296
|
+
) -> bool:
|
|
297
|
+
if inflight_future is None:
|
|
298
|
+
return False
|
|
299
|
+
async with self._http_bridge_lock:
|
|
300
|
+
current_future = self._http_bridge_inflight_sessions.get(key)
|
|
301
|
+
if current_future is not inflight_future:
|
|
302
|
+
return False
|
|
303
|
+
if getattr(inflight_future, "_http_bridge_handoff", False):
|
|
304
|
+
return False
|
|
305
|
+
self._http_bridge_inflight_sessions.pop(key, None)
|
|
306
|
+
if inflight_future.done():
|
|
307
|
+
return True
|
|
308
|
+
if isinstance(exc, asyncio.CancelledError):
|
|
309
|
+
inflight_future.cancel()
|
|
310
|
+
else:
|
|
311
|
+
inflight_future.set_exception(exc)
|
|
312
|
+
inflight_future.exception()
|
|
313
|
+
return True
|
|
314
|
+
|
|
315
|
+
async def _evict_http_bridge_inflight_waiter(
|
|
316
|
+
self,
|
|
317
|
+
inflight_future: asyncio.Future["_HTTPBridgeSession"],
|
|
318
|
+
exc: BaseException,
|
|
319
|
+
) -> "_HTTPBridgeSessionKey | None":
|
|
320
|
+
async with self._http_bridge_lock:
|
|
321
|
+
stale_key = None
|
|
322
|
+
for candidate_key, candidate_future in self._http_bridge_inflight_sessions.items():
|
|
323
|
+
if candidate_future is inflight_future:
|
|
324
|
+
stale_key = candidate_key
|
|
325
|
+
break
|
|
326
|
+
if stale_key is None:
|
|
327
|
+
return None
|
|
328
|
+
if getattr(inflight_future, "_http_bridge_handoff", False):
|
|
329
|
+
return None
|
|
330
|
+
self._http_bridge_inflight_sessions.pop(stale_key, None)
|
|
331
|
+
if not inflight_future.done():
|
|
332
|
+
inflight_future.set_exception(exc)
|
|
333
|
+
inflight_future.exception()
|
|
334
|
+
return stale_key
|
|
335
|
+
|
|
336
|
+
@overload
|
|
337
|
+
async def _get_or_create_http_bridge_session(
|
|
338
|
+
self,
|
|
339
|
+
key: "_HTTPBridgeSessionKey",
|
|
340
|
+
*,
|
|
341
|
+
headers: dict[str, str],
|
|
342
|
+
affinity: _AffinityPolicy,
|
|
343
|
+
api_key: ApiKeyData | None,
|
|
344
|
+
request_model: str | None,
|
|
345
|
+
idle_ttl_seconds: float,
|
|
346
|
+
max_sessions: int,
|
|
347
|
+
request_service_tier: str | None = None,
|
|
348
|
+
previous_response_id: str | None = None,
|
|
349
|
+
gateway_safe_mode: bool = False,
|
|
350
|
+
allow_forward_to_owner: Literal[False] = False,
|
|
351
|
+
forwarded_request: bool = False,
|
|
352
|
+
forwarded_original_request_unanchored: bool = False,
|
|
353
|
+
forwarded_affinity_kind: str | None = None,
|
|
354
|
+
forwarded_affinity_key: str | None = None,
|
|
355
|
+
allow_previous_response_recovery_rebind: bool = False,
|
|
356
|
+
allow_bootstrap_owner_rebind: bool = False,
|
|
357
|
+
durable_lookup: DurableBridgeLookup | None = None,
|
|
358
|
+
request_stage: str = "first_turn",
|
|
359
|
+
preferred_account_id: str | None = None,
|
|
360
|
+
preferred_account_has_continuity_provenance: bool = False,
|
|
361
|
+
fallback_on_preferred_account_unavailable: bool = True,
|
|
362
|
+
request_usage_budget: ApiKeyRequestUsageBudget | None = None,
|
|
363
|
+
request_deadline: float | None = None,
|
|
364
|
+
session_header_fallback_key: "_HTTPBridgeSessionKey | None" = None,
|
|
365
|
+
exclude_account_ids: Collection[str] | None = None,
|
|
366
|
+
deferred_account_backoff_lifecycle: _DeferredAccountBackoffLifecycle | None = None,
|
|
367
|
+
defer_account_health_writes: bool = False,
|
|
368
|
+
) -> "_HTTPBridgeSession": ...
|
|
369
|
+
@overload
|
|
370
|
+
async def _get_or_create_http_bridge_session(
|
|
371
|
+
self,
|
|
372
|
+
key: "_HTTPBridgeSessionKey",
|
|
373
|
+
*,
|
|
374
|
+
headers: dict[str, str],
|
|
375
|
+
affinity: _AffinityPolicy,
|
|
376
|
+
api_key: ApiKeyData | None,
|
|
377
|
+
request_model: str | None,
|
|
378
|
+
idle_ttl_seconds: float,
|
|
379
|
+
max_sessions: int,
|
|
380
|
+
request_service_tier: str | None = None,
|
|
381
|
+
previous_response_id: str | None = None,
|
|
382
|
+
gateway_safe_mode: bool = False,
|
|
383
|
+
allow_forward_to_owner: Literal[True],
|
|
384
|
+
forwarded_request: bool = False,
|
|
385
|
+
forwarded_original_request_unanchored: bool = False,
|
|
386
|
+
forwarded_affinity_kind: str | None = None,
|
|
387
|
+
forwarded_affinity_key: str | None = None,
|
|
388
|
+
allow_previous_response_recovery_rebind: bool = False,
|
|
389
|
+
allow_bootstrap_owner_rebind: bool = False,
|
|
390
|
+
durable_lookup: DurableBridgeLookup | None = None,
|
|
391
|
+
request_stage: str = "first_turn",
|
|
392
|
+
preferred_account_id: str | None = None,
|
|
393
|
+
preferred_account_has_continuity_provenance: bool = False,
|
|
394
|
+
fallback_on_preferred_account_unavailable: bool = True,
|
|
395
|
+
request_usage_budget: ApiKeyRequestUsageBudget | None = None,
|
|
396
|
+
request_deadline: float | None = None,
|
|
397
|
+
session_header_fallback_key: "_HTTPBridgeSessionKey | None" = None,
|
|
398
|
+
exclude_account_ids: Collection[str] | None = None,
|
|
399
|
+
deferred_account_backoff_lifecycle: _DeferredAccountBackoffLifecycle | None = None,
|
|
400
|
+
defer_account_health_writes: bool = False,
|
|
401
|
+
) -> "_HTTPBridgeSession | _HTTPBridgeOwnerForward": ...
|
|
402
|
+
async def _get_or_create_http_bridge_session(
|
|
403
|
+
self,
|
|
404
|
+
key: "_HTTPBridgeSessionKey",
|
|
405
|
+
*,
|
|
406
|
+
headers: dict[str, str],
|
|
407
|
+
affinity: _AffinityPolicy,
|
|
408
|
+
api_key: ApiKeyData | None,
|
|
409
|
+
request_model: str | None,
|
|
410
|
+
idle_ttl_seconds: float,
|
|
411
|
+
max_sessions: int,
|
|
412
|
+
request_service_tier: str | None = None,
|
|
413
|
+
previous_response_id: str | None = None,
|
|
414
|
+
gateway_safe_mode: bool = False,
|
|
415
|
+
allow_forward_to_owner: bool = False,
|
|
416
|
+
forwarded_request: bool = False,
|
|
417
|
+
forwarded_original_request_unanchored: bool = False,
|
|
418
|
+
forwarded_affinity_kind: str | None = None,
|
|
419
|
+
forwarded_affinity_key: str | None = None,
|
|
420
|
+
allow_previous_response_recovery_rebind: bool = False,
|
|
421
|
+
allow_bootstrap_owner_rebind: bool = False,
|
|
422
|
+
durable_lookup: DurableBridgeLookup | None = None,
|
|
423
|
+
request_stage: str = "first_turn",
|
|
424
|
+
preferred_account_id: str | None = None,
|
|
425
|
+
preferred_account_has_continuity_provenance: bool = False,
|
|
426
|
+
fallback_on_preferred_account_unavailable: bool = True,
|
|
427
|
+
request_usage_budget: ApiKeyRequestUsageBudget | None = None,
|
|
428
|
+
request_deadline: float | None = None,
|
|
429
|
+
session_header_fallback_key: "_HTTPBridgeSessionKey | None" = None,
|
|
430
|
+
exclude_account_ids: Collection[str] | None = None,
|
|
431
|
+
deferred_account_backoff_lifecycle: _DeferredAccountBackoffLifecycle | None = None,
|
|
432
|
+
defer_account_health_writes: bool = False,
|
|
433
|
+
) -> "_HTTPBridgeSession | _HTTPBridgeOwnerForward":
|
|
434
|
+
settings = _service_get_settings()
|
|
435
|
+
request_scope_id = ensure_request_scope_id()
|
|
436
|
+
api_key_id = api_key.id if api_key is not None else None
|
|
437
|
+
incoming_turn_state = _sticky_key_from_turn_state_header(headers)
|
|
438
|
+
incoming_session_key, initial_session_key = _turn_keys(headers, api_key, key, session_header_fallback_key)
|
|
439
|
+
original_request_unanchored = _http_bridge_request_needs_unanchored_handoff(
|
|
440
|
+
key, incoming_turn_state, previous_response_id, forwarded_request, forwarded_original_request_unanchored
|
|
441
|
+
)
|
|
442
|
+
# Model-transition isolation intentionally drops the durable lookup as a
|
|
443
|
+
# routing input below. Preserve generation provenance first: the same
|
|
444
|
+
# replica id can still name an older socket/process whose late release
|
|
445
|
+
# must be fenced by a newly advanced owner epoch.
|
|
446
|
+
same_replica_durable_predecessor = bool(
|
|
447
|
+
durable_lookup and durable_lookup.owner_instance_id == settings.http_responses_session_bridge_instance_id
|
|
448
|
+
)
|
|
449
|
+
model_transition_rebind = bool(
|
|
450
|
+
durable_lookup is not None and not _http_bridge_models_compatible(durable_lookup.model, request_model)
|
|
451
|
+
)
|
|
452
|
+
if model_transition_rebind:
|
|
453
|
+
durable_lookup = None
|
|
454
|
+
# Account selection consumes this one-shot capability; canonical creation
|
|
455
|
+
# also forces takeover so a prior-ring durable owner cannot reject it.
|
|
456
|
+
force_goal_restart_account_reselection = affinity.abandon_unavailable_legacy_owner
|
|
457
|
+
if await _http_bridge_should_wait_for_registration(self, key, settings):
|
|
458
|
+
skip_registration_gate = False
|
|
459
|
+
async with self._http_bridge_lock:
|
|
460
|
+
existing = self._http_bridge_sessions.get(key)
|
|
461
|
+
if existing is not None:
|
|
462
|
+
skip_registration_gate = True
|
|
463
|
+
elif incoming_turn_state is not None:
|
|
464
|
+
alias_index_key = _http_bridge_turn_state_alias_key(incoming_turn_state, api_key_id)
|
|
465
|
+
alias_key = self._http_bridge_turn_state_index.get(alias_index_key)
|
|
466
|
+
if alias_key is not None and alias_key in self._http_bridge_sessions:
|
|
467
|
+
skip_registration_gate = True
|
|
468
|
+
if not skip_registration_gate:
|
|
469
|
+
import app.core.startup as startup_module
|
|
470
|
+
|
|
471
|
+
registered = await startup_module.wait_for_bridge_registration(
|
|
472
|
+
timeout_seconds=settings.upstream_connect_timeout_seconds,
|
|
473
|
+
)
|
|
474
|
+
if not registered:
|
|
475
|
+
raise ProxyResponseError(
|
|
476
|
+
503,
|
|
477
|
+
openai_error(
|
|
478
|
+
"bridge_owner_unreachable",
|
|
479
|
+
"HTTP bridge registration is not ready",
|
|
480
|
+
error_type="server_error",
|
|
481
|
+
),
|
|
482
|
+
)
|
|
483
|
+
effective_idle_ttl_seconds = idle_ttl_seconds
|
|
484
|
+
forwarded_affinity = (
|
|
485
|
+
_forwarded_http_bridge_session_key(
|
|
486
|
+
headers,
|
|
487
|
+
api_key,
|
|
488
|
+
forwarded_affinity_kind=forwarded_affinity_kind,
|
|
489
|
+
forwarded_affinity_key=forwarded_affinity_key,
|
|
490
|
+
)
|
|
491
|
+
if forwarded_request
|
|
492
|
+
else None
|
|
493
|
+
)
|
|
494
|
+
old_account_id: str | None = None
|
|
495
|
+
force_durable_takeover_after_detach = used_session_header_fallback = False
|
|
496
|
+
locally_owned_fork_key: _HTTPBridgeSessionKey | None = None
|
|
497
|
+
model_transition_parent_key: _HTTPBridgeSessionKey | None = None
|
|
498
|
+
|
|
499
|
+
def bind_account_neutral_recovery_owner(session: _HTTPBridgeSession) -> None:
|
|
500
|
+
nonlocal preferred_account_id
|
|
501
|
+
if not is_http_bridge_account_neutral_replay(
|
|
502
|
+
kind=session.key.affinity_kind,
|
|
503
|
+
key=session.key.affinity_key,
|
|
504
|
+
):
|
|
505
|
+
return
|
|
506
|
+
preferred_account_id = resolve_required_account_id(
|
|
507
|
+
("requested continuity owner", preferred_account_id),
|
|
508
|
+
("local account-neutral recovery", session.account.id),
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
while True:
|
|
512
|
+
account_neutral_recovery = is_http_bridge_account_neutral_replay(
|
|
513
|
+
kind=key.affinity_kind,
|
|
514
|
+
key=key.affinity_key,
|
|
515
|
+
)
|
|
516
|
+
if account_neutral_recovery:
|
|
517
|
+
headers = without_http_bridge_session_affinity_headers(headers)
|
|
518
|
+
affinity = _AffinityPolicy()
|
|
519
|
+
incoming_turn_state = None
|
|
520
|
+
incoming_session_key = None
|
|
521
|
+
session_header_fallback_key = None
|
|
522
|
+
initial_session_key = None
|
|
523
|
+
if durable_lookup is None:
|
|
524
|
+
allow_forward_to_owner = False
|
|
525
|
+
inflight_future: asyncio.Future[_HTTPBridgeSession] | None = None
|
|
526
|
+
capacity_wait_future: asyncio.Future[_HTTPBridgeSession] | None = None
|
|
527
|
+
capacity_error_after_planned_closes: ProxyResponseError | None = None
|
|
528
|
+
owns_creation = False
|
|
529
|
+
continuity_error: ProxyResponseError | None = None
|
|
530
|
+
owner_mismatch_error: ProxyResponseError | None = None
|
|
531
|
+
owner_forward: _HTTPBridgeOwnerForward | None = None
|
|
532
|
+
force_durable_takeover = force_durable_takeover_after_detach or force_goal_restart_account_reselection
|
|
533
|
+
missing_turn_state_alias = False
|
|
534
|
+
sessions_to_close_before_create: list[_HTTPBridgeSession] = []
|
|
535
|
+
session_to_return_after_close: _HTTPBridgeSession | None = None
|
|
536
|
+
preserve_durable_canonical_key = (
|
|
537
|
+
incoming_turn_state is not None
|
|
538
|
+
and forwarded_affinity is None
|
|
539
|
+
and durable_lookup is not None
|
|
540
|
+
and key.affinity_kind == durable_lookup.canonical_kind
|
|
541
|
+
and key.affinity_key == durable_lookup.canonical_key
|
|
542
|
+
and key.affinity_kind != "turn_state_header"
|
|
543
|
+
)
|
|
544
|
+
preserve_internal_fork_key = key.affinity_kind in {
|
|
545
|
+
"internal_unanchored_parallel",
|
|
546
|
+
"internal_model_parallel",
|
|
547
|
+
"internal_request_parallel",
|
|
548
|
+
}
|
|
549
|
+
require_preferred_account = preferred_account_id is not None and (
|
|
550
|
+
previous_response_id is not None
|
|
551
|
+
or preferred_account_has_continuity_provenance
|
|
552
|
+
or key.strength == "hard"
|
|
553
|
+
or not fallback_on_preferred_account_unavailable
|
|
554
|
+
)
|
|
555
|
+
async with self._http_bridge_lock:
|
|
556
|
+
if (
|
|
557
|
+
incoming_turn_state is not None
|
|
558
|
+
and forwarded_affinity is None
|
|
559
|
+
and not preserve_durable_canonical_key
|
|
560
|
+
and not preserve_internal_fork_key
|
|
561
|
+
):
|
|
562
|
+
alias_index_key = _http_bridge_turn_state_alias_key(incoming_turn_state, api_key_id)
|
|
563
|
+
alias_key = self._http_bridge_turn_state_index.get(alias_index_key)
|
|
564
|
+
if alias_key is not None:
|
|
565
|
+
key = alias_key
|
|
566
|
+
alias_session = self._http_bridge_sessions.get(alias_key)
|
|
567
|
+
if _http_bridge_alias_target_is_stale(alias_session):
|
|
568
|
+
if is_http_bridge_account_neutral_replay(
|
|
569
|
+
kind=alias_key.affinity_kind,
|
|
570
|
+
key=alias_key.affinity_key,
|
|
571
|
+
):
|
|
572
|
+
if alias_session is None:
|
|
573
|
+
raise ProxyResponseError(502, _http_bridge_continuity_lost_error_envelope())
|
|
574
|
+
bind_account_neutral_recovery_owner(alias_session)
|
|
575
|
+
continue
|
|
576
|
+
self._http_bridge_turn_state_index.pop(alias_index_key, None)
|
|
577
|
+
key = _HTTPBridgeSessionKey("turn_state_header", incoming_turn_state, api_key_id)
|
|
578
|
+
elif not _http_bridge_models_compatible(alias_session.request_model, request_model):
|
|
579
|
+
model_transition_rebind, model_transition_parent_key = True, alias_key
|
|
580
|
+
if is_http_bridge_account_neutral_replay(
|
|
581
|
+
kind=alias_key.affinity_kind,
|
|
582
|
+
key=alias_key.affinity_key,
|
|
583
|
+
):
|
|
584
|
+
recovery_fork_key = _http_bridge_incompatible_model_fork_key(
|
|
585
|
+
key=alias_key,
|
|
586
|
+
existing_model=alias_session.request_model,
|
|
587
|
+
request_model=request_model,
|
|
588
|
+
request_scope_id=request_scope_id,
|
|
589
|
+
)
|
|
590
|
+
assert recovery_fork_key is not None
|
|
591
|
+
bind_account_neutral_recovery_owner(alias_session)
|
|
592
|
+
key = recovery_fork_key
|
|
593
|
+
continue
|
|
594
|
+
else:
|
|
595
|
+
key = _HTTPBridgeSessionKey("turn_state_header", incoming_turn_state, api_key_id)
|
|
596
|
+
elif not _http_bridge_compatible(
|
|
597
|
+
alias_session, request_model, request_service_tier, True
|
|
598
|
+
) or not _http_bridge_session_matches_preferred_account(
|
|
599
|
+
session=alias_session,
|
|
600
|
+
previous_response_id=previous_response_id,
|
|
601
|
+
preferred_account_id=preferred_account_id,
|
|
602
|
+
require_preferred_account=require_preferred_account,
|
|
603
|
+
):
|
|
604
|
+
raise ProxyResponseError(502, _http_bridge_continuity_lost_error_envelope())
|
|
605
|
+
else:
|
|
606
|
+
self._promote_http_bridge_session_to_codex_affinity(
|
|
607
|
+
alias_session,
|
|
608
|
+
turn_state=incoming_turn_state,
|
|
609
|
+
)
|
|
610
|
+
_register_http_bridge_turn_state_aliases_locked(self, alias_session)
|
|
611
|
+
key = alias_session.key
|
|
612
|
+
elif incoming_turn_state.startswith("http_turn_"):
|
|
613
|
+
if previous_response_id is not None:
|
|
614
|
+
previous_alias_key = _http_bridge_previous_response_alias_key(
|
|
615
|
+
previous_response_id,
|
|
616
|
+
api_key_id,
|
|
617
|
+
)
|
|
618
|
+
previous_key = self._http_bridge_previous_response_index.get(previous_alias_key)
|
|
619
|
+
previous_session = None
|
|
620
|
+
if previous_key is not None:
|
|
621
|
+
previous_session = self._http_bridge_sessions.get(previous_key)
|
|
622
|
+
if (
|
|
623
|
+
previous_session is not None
|
|
624
|
+
and (not previous_session.closed or previous_session.handoff_in_progress)
|
|
625
|
+
and _http_bridge_session_account_active(previous_session)
|
|
626
|
+
and _http_bridge_compatible(previous_session, request_model, request_service_tier, True)
|
|
627
|
+
and _http_bridge_session_matches_preferred_account(
|
|
628
|
+
session=previous_session,
|
|
629
|
+
previous_response_id=previous_response_id,
|
|
630
|
+
preferred_account_id=preferred_account_id,
|
|
631
|
+
require_preferred_account=require_preferred_account,
|
|
632
|
+
)
|
|
633
|
+
):
|
|
634
|
+
key = previous_session.key
|
|
635
|
+
self._promote_http_bridge_session_to_codex_affinity(
|
|
636
|
+
previous_session,
|
|
637
|
+
turn_state=incoming_turn_state,
|
|
638
|
+
)
|
|
639
|
+
previous_session.downstream_turn_state_aliases.add(incoming_turn_state)
|
|
640
|
+
for alias in previous_session.downstream_turn_state_aliases:
|
|
641
|
+
self._http_bridge_turn_state_index[
|
|
642
|
+
_http_bridge_turn_state_alias_key(
|
|
643
|
+
alias,
|
|
644
|
+
previous_session.key.api_key_id,
|
|
645
|
+
)
|
|
646
|
+
] = previous_session.key
|
|
647
|
+
continue
|
|
648
|
+
if previous_session is not None and not _http_bridge_models_compatible(
|
|
649
|
+
previous_session.request_model, request_model
|
|
650
|
+
):
|
|
651
|
+
model_transition_rebind = True
|
|
652
|
+
model_transition_parent_key = previous_key
|
|
653
|
+
if previous_key is not None and is_http_bridge_account_neutral_replay(
|
|
654
|
+
kind=previous_key.affinity_kind,
|
|
655
|
+
key=previous_key.affinity_key,
|
|
656
|
+
):
|
|
657
|
+
recovery_fork_key = _http_bridge_incompatible_model_fork_key(
|
|
658
|
+
key=previous_key,
|
|
659
|
+
existing_model=previous_session.request_model,
|
|
660
|
+
request_model=request_model,
|
|
661
|
+
request_scope_id=request_scope_id,
|
|
662
|
+
)
|
|
663
|
+
assert recovery_fork_key is not None
|
|
664
|
+
bind_account_neutral_recovery_owner(previous_session)
|
|
665
|
+
key = recovery_fork_key
|
|
666
|
+
continue
|
|
667
|
+
elif previous_session is not None and (
|
|
668
|
+
not _http_bridge_alias_target_is_stale(previous_session)
|
|
669
|
+
and not previous_session.handoff_in_progress
|
|
670
|
+
):
|
|
671
|
+
raise ProxyResponseError(502, _http_bridge_continuity_lost_error_envelope())
|
|
672
|
+
elif previous_key is not None:
|
|
673
|
+
self._http_bridge_previous_response_index.pop(previous_alias_key, None)
|
|
674
|
+
if model_transition_rebind:
|
|
675
|
+
if not is_http_bridge_account_neutral_replay(
|
|
676
|
+
kind=key.affinity_kind,
|
|
677
|
+
key=key.affinity_key,
|
|
678
|
+
):
|
|
679
|
+
key = _HTTPBridgeSessionKey("turn_state_header", incoming_turn_state, api_key_id)
|
|
680
|
+
elif (
|
|
681
|
+
fallback_key := _alias_fallback_key(incoming_session_key, initial_session_key, api_key_id)
|
|
682
|
+
) is not None:
|
|
683
|
+
key = fallback_key
|
|
684
|
+
used_session_header_fallback = True
|
|
685
|
+
else:
|
|
686
|
+
key = _HTTPBridgeSessionKey("turn_state_header", incoming_turn_state, api_key_id)
|
|
687
|
+
missing_turn_state_alias = True
|
|
688
|
+
pruned_sessions = self._prune_http_bridge_sessions_locked()
|
|
689
|
+
if pruned_sessions:
|
|
690
|
+
if any(session.key == key for session in pruned_sessions):
|
|
691
|
+
force_durable_takeover = True
|
|
692
|
+
self._schedule_http_bridge_session_closes(pruned_sessions, reason="registry_detach")
|
|
693
|
+
existing = self._http_bridge_sessions.get(key)
|
|
694
|
+
retained_handoff = bool(
|
|
695
|
+
existing and existing.closed and _http_bridge_session_has_admission_waiter(existing)
|
|
696
|
+
)
|
|
697
|
+
reusable = (
|
|
698
|
+
not force_goal_restart_account_reselection
|
|
699
|
+
and existing is not None
|
|
700
|
+
and _http_bridge_session_reusable_for_lookup(
|
|
701
|
+
session=existing,
|
|
702
|
+
key=key,
|
|
703
|
+
api_key=api_key,
|
|
704
|
+
incoming_turn_state=incoming_turn_state,
|
|
705
|
+
previous_response_id=previous_response_id,
|
|
706
|
+
preferred_account_id=preferred_account_id,
|
|
707
|
+
require_preferred_account=require_preferred_account,
|
|
708
|
+
service_tier_supported=_http_bridge_compatible(existing, request_model, request_service_tier),
|
|
709
|
+
allow_closed_admission_handoff=retained_handoff,
|
|
710
|
+
session_key_quarantined=_http_bridge_session_key_quarantined(self, existing.key),
|
|
711
|
+
)
|
|
712
|
+
)
|
|
713
|
+
fork_key = _http_bridge_parallel_fork_key(
|
|
714
|
+
key=key,
|
|
715
|
+
session=existing,
|
|
716
|
+
inflight_creation=key in self._http_bridge_inflight_sessions
|
|
717
|
+
and not bool(existing and existing.handoff_in_progress),
|
|
718
|
+
incoming_turn_state=incoming_turn_state,
|
|
719
|
+
previous_response_id=previous_response_id,
|
|
720
|
+
request_model=request_model,
|
|
721
|
+
request_service_tier=request_service_tier,
|
|
722
|
+
request_scope_id=request_scope_id,
|
|
723
|
+
allow_model_fork=reusable or model_transition_rebind,
|
|
724
|
+
force_canonical_replacement=force_goal_restart_account_reselection,
|
|
725
|
+
)
|
|
726
|
+
if fork_key is not None:
|
|
727
|
+
if existing is not None:
|
|
728
|
+
bind_account_neutral_recovery_owner(existing)
|
|
729
|
+
model_transition_parent_key = key
|
|
730
|
+
key = fork_key
|
|
731
|
+
durable_lookup = None
|
|
732
|
+
force_durable_takeover_after_detach = False
|
|
733
|
+
locally_owned_fork_key = _http_bridge_locally_owned_fork_key(
|
|
734
|
+
fork_key, forwarded_request, forwarded_original_request_unanchored
|
|
735
|
+
)
|
|
736
|
+
continue
|
|
737
|
+
if retained_handoff and not reusable:
|
|
738
|
+
existing, force_durable_takeover = self._recover_http_bridge_incompatible_admission_handoff(
|
|
739
|
+
key,
|
|
740
|
+
existing,
|
|
741
|
+
force_durable_takeover,
|
|
742
|
+
original_request_unanchored,
|
|
743
|
+
request_model,
|
|
744
|
+
api_key,
|
|
745
|
+
incoming_turn_state,
|
|
746
|
+
previous_response_id,
|
|
747
|
+
preferred_account_id,
|
|
748
|
+
require_preferred_account,
|
|
749
|
+
request_service_tier,
|
|
750
|
+
)
|
|
751
|
+
if reusable:
|
|
752
|
+
assert existing is not None
|
|
753
|
+
current_instance = settings.http_responses_session_bridge_instance_id
|
|
754
|
+
if _durable_bridge_lookup_allows_local_reuse(durable_lookup, current_instance=current_instance):
|
|
755
|
+
existing.api_key = api_key
|
|
756
|
+
existing.request_model = request_model
|
|
757
|
+
existing.request_service_tier = request_service_tier
|
|
758
|
+
existing.last_used_at = clock_for(self).monotonic()
|
|
759
|
+
await _refresh_reused_http_bridge_session_with_handoff(
|
|
760
|
+
self,
|
|
761
|
+
existing,
|
|
762
|
+
key=key,
|
|
763
|
+
request_scope_id=request_scope_id,
|
|
764
|
+
reserve_handoff=original_request_unanchored,
|
|
765
|
+
)
|
|
766
|
+
return existing
|
|
767
|
+
old_account_id = existing.account.id
|
|
768
|
+
detached = self._detach_http_bridge_session_locked(key, expected_session=existing)
|
|
769
|
+
if detached is not None:
|
|
770
|
+
force_durable_takeover = True
|
|
771
|
+
self._schedule_http_bridge_session_closes([detached], reason="registry_detach")
|
|
772
|
+
existing = None
|
|
773
|
+
if existing is not None and (
|
|
774
|
+
force_goal_restart_account_reselection
|
|
775
|
+
or (not existing.closed and existing.account.status in ROUTABLE_STATUSES)
|
|
776
|
+
):
|
|
777
|
+
old_account_id = existing.account.id
|
|
778
|
+
retiring_with_visible_requests = _http_bridge_session_retiring_with_visible_requests(existing)
|
|
779
|
+
detached = self._detach_http_bridge_session_locked(
|
|
780
|
+
key,
|
|
781
|
+
expected_session=existing,
|
|
782
|
+
mark_closed=not retiring_with_visible_requests,
|
|
783
|
+
)
|
|
784
|
+
if detached is not None:
|
|
785
|
+
force_durable_takeover = True
|
|
786
|
+
if not retiring_with_visible_requests:
|
|
787
|
+
if self._http_bridge_forced_close_must_finish_before_create(
|
|
788
|
+
force_goal_restart_account_reselection,
|
|
789
|
+
max_sessions,
|
|
790
|
+
):
|
|
791
|
+
sessions_to_close_before_create.append(detached)
|
|
792
|
+
else:
|
|
793
|
+
self._schedule_http_bridge_session_closes([detached], reason="registry_detach")
|
|
794
|
+
existing = None
|
|
795
|
+
if shutdown_state.is_bridge_drain_active() and not _http_bridge_can_recover_during_drain(
|
|
796
|
+
key=key,
|
|
797
|
+
headers=headers,
|
|
798
|
+
previous_response_id=previous_response_id,
|
|
799
|
+
durable_lookup=durable_lookup,
|
|
800
|
+
):
|
|
801
|
+
raise ProxyResponseError(
|
|
802
|
+
503,
|
|
803
|
+
openai_error(
|
|
804
|
+
"bridge_drain_active",
|
|
805
|
+
"HTTP bridge is draining — new sessions not accepted during shutdown",
|
|
806
|
+
error_type="server_error",
|
|
807
|
+
),
|
|
808
|
+
)
|
|
809
|
+
if shutdown_state.is_bridge_drain_active():
|
|
810
|
+
_record_bridge_drain_recovery_allowed()
|
|
811
|
+
owner_check_required = _http_bridge_owner_check_required(
|
|
812
|
+
key,
|
|
813
|
+
gateway_safe_mode=gateway_safe_mode,
|
|
814
|
+
)
|
|
815
|
+
if owner_check_required or key.affinity_kind == "prompt_cache":
|
|
816
|
+
owner_instance = _durable_bridge_lookup_active_owner(durable_lookup)
|
|
817
|
+
hard_continuity_lookup = owner_check_required or bool(incoming_turn_state or previous_response_id)
|
|
818
|
+
ring_lookup_failed = False
|
|
819
|
+
if key == locally_owned_fork_key:
|
|
820
|
+
owner_instance = settings.http_responses_session_bridge_instance_id
|
|
821
|
+
if owner_instance is None:
|
|
822
|
+
try:
|
|
823
|
+
owner_instance = await _http_bridge_owner_instance(key, settings, self._ring_membership)
|
|
824
|
+
except Exception as exc:
|
|
825
|
+
ring_lookup_failed = True
|
|
826
|
+
if hard_continuity_lookup:
|
|
827
|
+
_record_continuity_fail_closed(
|
|
828
|
+
surface="http_bridge",
|
|
829
|
+
reason="owner_metadata_unavailable",
|
|
830
|
+
previous_response_id=previous_response_id,
|
|
831
|
+
session_id=incoming_turn_state or incoming_session_key,
|
|
832
|
+
upstream_error_code="owner_lookup_failed",
|
|
833
|
+
)
|
|
834
|
+
raise ProxyResponseError(
|
|
835
|
+
502,
|
|
836
|
+
_http_bridge_owner_lookup_unavailable_error_envelope(),
|
|
837
|
+
) from exc
|
|
838
|
+
if _http_bridge_can_local_recover_without_ring(
|
|
839
|
+
key=key,
|
|
840
|
+
headers=headers,
|
|
841
|
+
previous_response_id=previous_response_id,
|
|
842
|
+
durable_lookup=durable_lookup,
|
|
843
|
+
):
|
|
844
|
+
logger.warning(
|
|
845
|
+
"Bridge owner lookup failed; allowing local recovery path",
|
|
846
|
+
exc_info=True,
|
|
847
|
+
)
|
|
848
|
+
owner_instance = settings.http_responses_session_bridge_instance_id
|
|
849
|
+
else:
|
|
850
|
+
raise
|
|
851
|
+
try:
|
|
852
|
+
current_instance, ring = await _active_http_bridge_instance_ring(
|
|
853
|
+
settings, self._ring_membership
|
|
854
|
+
)
|
|
855
|
+
except Exception as exc:
|
|
856
|
+
if hard_continuity_lookup:
|
|
857
|
+
_record_continuity_fail_closed(
|
|
858
|
+
surface="http_bridge",
|
|
859
|
+
reason="owner_metadata_unavailable",
|
|
860
|
+
previous_response_id=previous_response_id,
|
|
861
|
+
session_id=incoming_turn_state or incoming_session_key,
|
|
862
|
+
upstream_error_code="ring_lookup_failed",
|
|
863
|
+
)
|
|
864
|
+
raise ProxyResponseError(
|
|
865
|
+
502,
|
|
866
|
+
_http_bridge_owner_lookup_unavailable_error_envelope(),
|
|
867
|
+
) from exc
|
|
868
|
+
if ring_lookup_failed or _http_bridge_can_local_recover_without_ring(
|
|
869
|
+
key=key,
|
|
870
|
+
headers=headers,
|
|
871
|
+
previous_response_id=previous_response_id,
|
|
872
|
+
durable_lookup=durable_lookup,
|
|
873
|
+
):
|
|
874
|
+
logger.warning(
|
|
875
|
+
"Bridge ring lookup failed; falling back to local recovery ring", exc_info=True
|
|
876
|
+
)
|
|
877
|
+
current_instance = settings.http_responses_session_bridge_instance_id
|
|
878
|
+
ring = (current_instance,)
|
|
879
|
+
else:
|
|
880
|
+
raise
|
|
881
|
+
owner_mismatch = owner_instance is not None and owner_instance != current_instance
|
|
882
|
+
if owner_mismatch and (len(ring) > 1 or durable_lookup is not None):
|
|
883
|
+
if PROMETHEUS_AVAILABLE and bridge_owner_mismatch_total is not None:
|
|
884
|
+
bridge_owner_mismatch_total.labels(strength=_http_bridge_key_strength(key)).inc()
|
|
885
|
+
if (
|
|
886
|
+
hard_continuity_lookup
|
|
887
|
+
and not (previous_response_id is not None and allow_previous_response_recovery_rebind)
|
|
888
|
+
and not allow_bootstrap_owner_rebind
|
|
889
|
+
):
|
|
890
|
+
_log_http_bridge_event(
|
|
891
|
+
"owner_mismatch",
|
|
892
|
+
key,
|
|
893
|
+
account_id=None,
|
|
894
|
+
model=request_model,
|
|
895
|
+
detail=(
|
|
896
|
+
"expected_instance="
|
|
897
|
+
f"{owner_instance}, current_instance={current_instance}, outcome=forward"
|
|
898
|
+
),
|
|
899
|
+
cache_key_family=key.affinity_kind,
|
|
900
|
+
model_class=_extract_model_class(request_model) if request_model else None,
|
|
901
|
+
owner_check_applied=True,
|
|
902
|
+
)
|
|
903
|
+
if allow_forward_to_owner:
|
|
904
|
+
if forwarded_request:
|
|
905
|
+
_log_http_bridge_event(
|
|
906
|
+
"owner_mismatch_forward_loop",
|
|
907
|
+
key,
|
|
908
|
+
account_id=None,
|
|
909
|
+
model=request_model,
|
|
910
|
+
detail=(
|
|
911
|
+
"expected_instance="
|
|
912
|
+
f"{owner_instance}, current_instance={current_instance}, "
|
|
913
|
+
"outcome=forward_loop_prevented"
|
|
914
|
+
),
|
|
915
|
+
cache_key_family=key.affinity_kind,
|
|
916
|
+
model_class=_extract_model_class(request_model) if request_model else None,
|
|
917
|
+
owner_check_applied=True,
|
|
918
|
+
)
|
|
919
|
+
raise ProxyResponseError(
|
|
920
|
+
503,
|
|
921
|
+
openai_error(
|
|
922
|
+
"bridge_forward_loop_prevented",
|
|
923
|
+
(
|
|
924
|
+
"HTTP bridge request was forwarded back to a non-owner instance; "
|
|
925
|
+
"refusing takeover to avoid a forward loop"
|
|
926
|
+
),
|
|
927
|
+
error_type="server_error",
|
|
928
|
+
),
|
|
929
|
+
)
|
|
930
|
+
elif self._ring_membership is None:
|
|
931
|
+
if _http_bridge_has_durable_recovery_anchor(
|
|
932
|
+
previous_response_id=previous_response_id,
|
|
933
|
+
durable_lookup=durable_lookup,
|
|
934
|
+
):
|
|
935
|
+
if PROMETHEUS_AVAILABLE and bridge_durable_recover_total is not None:
|
|
936
|
+
bridge_durable_recover_total.labels(path="owner_missing").inc()
|
|
937
|
+
_log_http_bridge_event(
|
|
938
|
+
"owner_mismatch_local_recover",
|
|
939
|
+
key,
|
|
940
|
+
account_id=None,
|
|
941
|
+
model=request_model,
|
|
942
|
+
detail=(
|
|
943
|
+
"expected_instance="
|
|
944
|
+
f"{owner_instance}, current_instance={current_instance}, "
|
|
945
|
+
"outcome=local_recover_no_ring"
|
|
946
|
+
),
|
|
947
|
+
cache_key_family=key.affinity_kind,
|
|
948
|
+
model_class=_extract_model_class(request_model) if request_model else None,
|
|
949
|
+
owner_check_applied=True,
|
|
950
|
+
)
|
|
951
|
+
force_durable_takeover = True
|
|
952
|
+
elif _http_bridge_can_single_instance_owner_takeover_without_anchor(
|
|
953
|
+
key=key,
|
|
954
|
+
owner_instance=owner_instance,
|
|
955
|
+
current_instance=current_instance,
|
|
956
|
+
ring=ring,
|
|
957
|
+
):
|
|
958
|
+
if PROMETHEUS_AVAILABLE and bridge_durable_recover_total is not None:
|
|
959
|
+
bridge_durable_recover_total.labels(path="restart_takeover").inc()
|
|
960
|
+
_log_http_bridge_event(
|
|
961
|
+
"owner_mismatch_local_recover",
|
|
962
|
+
key,
|
|
963
|
+
account_id=None,
|
|
964
|
+
model=request_model,
|
|
965
|
+
detail=(
|
|
966
|
+
"expected_instance="
|
|
967
|
+
f"{owner_instance}, current_instance={current_instance}, "
|
|
968
|
+
"outcome=single_instance_takeover_no_anchor"
|
|
969
|
+
),
|
|
970
|
+
cache_key_family=key.affinity_kind,
|
|
971
|
+
model_class=_extract_model_class(request_model) if request_model else None,
|
|
972
|
+
owner_check_applied=True,
|
|
973
|
+
)
|
|
974
|
+
force_durable_takeover = True
|
|
975
|
+
else:
|
|
976
|
+
_log_http_bridge_event(
|
|
977
|
+
"owner_mismatch_local_recover",
|
|
978
|
+
key,
|
|
979
|
+
account_id=None,
|
|
980
|
+
model=request_model,
|
|
981
|
+
detail=(
|
|
982
|
+
"expected_instance="
|
|
983
|
+
f"{owner_instance}, current_instance={current_instance}, "
|
|
984
|
+
"outcome=local_recover_no_ring"
|
|
985
|
+
),
|
|
986
|
+
cache_key_family=key.affinity_kind,
|
|
987
|
+
model_class=_extract_model_class(request_model) if request_model else None,
|
|
988
|
+
owner_check_applied=True,
|
|
989
|
+
)
|
|
990
|
+
force_durable_takeover = True
|
|
991
|
+
else:
|
|
992
|
+
assert owner_instance is not None
|
|
993
|
+
owner_endpoint = await self._ring_membership.resolve_endpoint(owner_instance)
|
|
994
|
+
if owner_endpoint is None:
|
|
995
|
+
if _http_bridge_has_durable_recovery_anchor(
|
|
996
|
+
previous_response_id=previous_response_id,
|
|
997
|
+
durable_lookup=durable_lookup,
|
|
998
|
+
):
|
|
999
|
+
if PROMETHEUS_AVAILABLE and bridge_durable_recover_total is not None:
|
|
1000
|
+
bridge_durable_recover_total.labels(path="owner_missing").inc()
|
|
1001
|
+
_log_http_bridge_event(
|
|
1002
|
+
"owner_endpoint_missing_local_recover",
|
|
1003
|
+
key,
|
|
1004
|
+
account_id=None,
|
|
1005
|
+
model=request_model,
|
|
1006
|
+
detail=(
|
|
1007
|
+
"expected_instance="
|
|
1008
|
+
f"{owner_instance}, current_instance={current_instance}, "
|
|
1009
|
+
"outcome=local_recover"
|
|
1010
|
+
),
|
|
1011
|
+
cache_key_family=key.affinity_kind,
|
|
1012
|
+
model_class=_extract_model_class(request_model)
|
|
1013
|
+
if request_model
|
|
1014
|
+
else None,
|
|
1015
|
+
owner_check_applied=True,
|
|
1016
|
+
)
|
|
1017
|
+
force_durable_takeover = True
|
|
1018
|
+
else:
|
|
1019
|
+
_log_http_bridge_event(
|
|
1020
|
+
"owner_mismatch_local_recover",
|
|
1021
|
+
key,
|
|
1022
|
+
account_id=None,
|
|
1023
|
+
model=request_model,
|
|
1024
|
+
detail=(
|
|
1025
|
+
"expected_instance="
|
|
1026
|
+
f"{owner_instance}, current_instance={current_instance}, "
|
|
1027
|
+
"outcome=local_recover_no_endpoint"
|
|
1028
|
+
),
|
|
1029
|
+
cache_key_family=key.affinity_kind,
|
|
1030
|
+
model_class=_extract_model_class(request_model)
|
|
1031
|
+
if request_model
|
|
1032
|
+
else None,
|
|
1033
|
+
owner_check_applied=True,
|
|
1034
|
+
)
|
|
1035
|
+
force_durable_takeover = True
|
|
1036
|
+
elif _http_bridge_endpoint_matches_current_instance(owner_endpoint, settings):
|
|
1037
|
+
if PROMETHEUS_AVAILABLE and bridge_durable_recover_total is not None:
|
|
1038
|
+
bridge_durable_recover_total.labels(path="restart_takeover").inc()
|
|
1039
|
+
_log_http_bridge_event(
|
|
1040
|
+
"owner_mismatch_local_recover",
|
|
1041
|
+
key,
|
|
1042
|
+
account_id=None,
|
|
1043
|
+
model=request_model,
|
|
1044
|
+
detail=(
|
|
1045
|
+
"expected_instance="
|
|
1046
|
+
f"{owner_instance}, current_instance={current_instance}, "
|
|
1047
|
+
"outcome=local_recover_same_endpoint"
|
|
1048
|
+
),
|
|
1049
|
+
cache_key_family=key.affinity_kind,
|
|
1050
|
+
model_class=_extract_model_class(request_model) if request_model else None,
|
|
1051
|
+
owner_check_applied=True,
|
|
1052
|
+
)
|
|
1053
|
+
force_durable_takeover = True
|
|
1054
|
+
else:
|
|
1055
|
+
owner_forward = _HTTPBridgeOwnerForward(
|
|
1056
|
+
owner_instance=owner_instance,
|
|
1057
|
+
owner_endpoint=owner_endpoint,
|
|
1058
|
+
key=key,
|
|
1059
|
+
)
|
|
1060
|
+
else:
|
|
1061
|
+
if _http_bridge_has_durable_recovery_anchor(
|
|
1062
|
+
previous_response_id=previous_response_id,
|
|
1063
|
+
durable_lookup=durable_lookup,
|
|
1064
|
+
):
|
|
1065
|
+
if PROMETHEUS_AVAILABLE and bridge_durable_recover_total is not None:
|
|
1066
|
+
bridge_durable_recover_total.labels(path="owner_missing").inc()
|
|
1067
|
+
_log_http_bridge_event(
|
|
1068
|
+
"owner_mismatch_local_recover",
|
|
1069
|
+
key,
|
|
1070
|
+
account_id=None,
|
|
1071
|
+
model=request_model,
|
|
1072
|
+
detail=(
|
|
1073
|
+
"expected_instance="
|
|
1074
|
+
f"{owner_instance}, current_instance={current_instance}, "
|
|
1075
|
+
"outcome=local_recover"
|
|
1076
|
+
),
|
|
1077
|
+
cache_key_family=key.affinity_kind,
|
|
1078
|
+
model_class=_extract_model_class(request_model) if request_model else None,
|
|
1079
|
+
owner_check_applied=True,
|
|
1080
|
+
)
|
|
1081
|
+
force_durable_takeover = True
|
|
1082
|
+
else:
|
|
1083
|
+
_log_http_bridge_event(
|
|
1084
|
+
"owner_mismatch_local_recover",
|
|
1085
|
+
key,
|
|
1086
|
+
account_id=None,
|
|
1087
|
+
model=request_model,
|
|
1088
|
+
detail=(
|
|
1089
|
+
"expected_instance="
|
|
1090
|
+
f"{owner_instance}, current_instance={current_instance}, "
|
|
1091
|
+
"outcome=local_recover_no_forward"
|
|
1092
|
+
),
|
|
1093
|
+
cache_key_family=key.affinity_kind,
|
|
1094
|
+
model_class=_extract_model_class(request_model) if request_model else None,
|
|
1095
|
+
owner_check_applied=True,
|
|
1096
|
+
)
|
|
1097
|
+
force_durable_takeover = True
|
|
1098
|
+
else:
|
|
1099
|
+
_log_http_bridge_event(
|
|
1100
|
+
"prompt_cache_locality_miss",
|
|
1101
|
+
key,
|
|
1102
|
+
account_id=None,
|
|
1103
|
+
model=request_model,
|
|
1104
|
+
detail=(
|
|
1105
|
+
"expected_instance="
|
|
1106
|
+
f"{owner_instance}, current_instance={current_instance}, "
|
|
1107
|
+
"outcome=local_rebind"
|
|
1108
|
+
),
|
|
1109
|
+
cache_key_family=key.affinity_kind,
|
|
1110
|
+
model_class=_extract_model_class(request_model) if request_model else None,
|
|
1111
|
+
owner_check_applied=False,
|
|
1112
|
+
)
|
|
1113
|
+
if _http_bridge_can_single_instance_prompt_cache_takeover_without_anchor(
|
|
1114
|
+
key=key,
|
|
1115
|
+
owner_instance=owner_instance,
|
|
1116
|
+
current_instance=current_instance,
|
|
1117
|
+
ring=ring,
|
|
1118
|
+
):
|
|
1119
|
+
force_durable_takeover = True
|
|
1120
|
+
elif allow_previous_response_recovery_rebind or allow_bootstrap_owner_rebind:
|
|
1121
|
+
force_durable_takeover = True
|
|
1122
|
+
_log_http_bridge_event(
|
|
1123
|
+
"soft_locality_rebind",
|
|
1124
|
+
key,
|
|
1125
|
+
account_id=None,
|
|
1126
|
+
model=request_model,
|
|
1127
|
+
detail=(
|
|
1128
|
+
"expected_instance="
|
|
1129
|
+
f"{owner_instance}, current_instance={current_instance}, outcome=local_rebind"
|
|
1130
|
+
),
|
|
1131
|
+
cache_key_family=key.affinity_kind,
|
|
1132
|
+
model_class=_extract_model_class(request_model) if request_model else None,
|
|
1133
|
+
owner_check_applied=False,
|
|
1134
|
+
)
|
|
1135
|
+
if PROMETHEUS_AVAILABLE:
|
|
1136
|
+
if bridge_prompt_cache_locality_miss_total is not None:
|
|
1137
|
+
bridge_prompt_cache_locality_miss_total.inc()
|
|
1138
|
+
if bridge_soft_local_rebind_total is not None:
|
|
1139
|
+
bridge_soft_local_rebind_total.inc()
|
|
1140
|
+
if bridge_local_rebind_total is not None:
|
|
1141
|
+
bridge_local_rebind_total.labels(reason="prompt_cache_locality_miss").inc()
|
|
1142
|
+
if existing is not None and not existing.handoff_in_progress:
|
|
1143
|
+
old_account_id = existing.account.id
|
|
1144
|
+
_log_http_bridge_event(
|
|
1145
|
+
"discard_stale",
|
|
1146
|
+
key,
|
|
1147
|
+
account_id=existing.account.id,
|
|
1148
|
+
model=existing.request_model,
|
|
1149
|
+
cache_key_family=key.affinity_kind,
|
|
1150
|
+
model_class=_extract_model_class(existing.request_model) if existing.request_model else None,
|
|
1151
|
+
)
|
|
1152
|
+
detached = self._detach_http_bridge_session_locked(key, expected_session=existing)
|
|
1153
|
+
if detached is not None:
|
|
1154
|
+
force_durable_takeover = True
|
|
1155
|
+
self._schedule_http_bridge_session_closes([detached], reason="registry_detach")
|
|
1156
|
+
if owner_mismatch_error is None:
|
|
1157
|
+
inflight_future = self._http_bridge_inflight_sessions.get(key)
|
|
1158
|
+
if (
|
|
1159
|
+
previous_response_id is not None
|
|
1160
|
+
and inflight_future is None
|
|
1161
|
+
and (existing is None or existing.closed or not _http_bridge_session_account_active(existing))
|
|
1162
|
+
):
|
|
1163
|
+
previous_alias_key = _http_bridge_previous_response_alias_key(previous_response_id, api_key_id)
|
|
1164
|
+
previous_key = self._http_bridge_previous_response_index.get(previous_alias_key)
|
|
1165
|
+
if previous_key is not None:
|
|
1166
|
+
previous_session = self._http_bridge_sessions.get(previous_key)
|
|
1167
|
+
if (
|
|
1168
|
+
previous_session is not None
|
|
1169
|
+
and not previous_session.closed
|
|
1170
|
+
and _http_bridge_session_account_active(previous_session)
|
|
1171
|
+
and _http_bridge_compatible(previous_session, request_model, request_service_tier, True)
|
|
1172
|
+
):
|
|
1173
|
+
key = previous_session.key
|
|
1174
|
+
existing = previous_session
|
|
1175
|
+
inflight_future = self._http_bridge_inflight_sessions.get(previous_key)
|
|
1176
|
+
if incoming_turn_state:
|
|
1177
|
+
self._promote_http_bridge_session_to_codex_affinity(
|
|
1178
|
+
previous_session,
|
|
1179
|
+
turn_state=incoming_turn_state,
|
|
1180
|
+
)
|
|
1181
|
+
previous_session.downstream_turn_state_aliases.add(incoming_turn_state)
|
|
1182
|
+
for alias in previous_session.downstream_turn_state_aliases:
|
|
1183
|
+
self._http_bridge_turn_state_index[
|
|
1184
|
+
_http_bridge_turn_state_alias_key(
|
|
1185
|
+
alias,
|
|
1186
|
+
previous_session.key.api_key_id,
|
|
1187
|
+
)
|
|
1188
|
+
] = previous_session.key
|
|
1189
|
+
if inflight_future is None:
|
|
1190
|
+
previous_session.request_model = request_model
|
|
1191
|
+
previous_session.request_service_tier = request_service_tier
|
|
1192
|
+
previous_session.last_used_at = clock_for(self).monotonic()
|
|
1193
|
+
await self._refresh_durable_http_bridge_session(previous_session)
|
|
1194
|
+
_log_http_bridge_event(
|
|
1195
|
+
"reuse",
|
|
1196
|
+
key,
|
|
1197
|
+
account_id=previous_session.account.id,
|
|
1198
|
+
model=previous_session.request_model,
|
|
1199
|
+
pending_count=self._http_bridge_pending_count_nowait(
|
|
1200
|
+
previous_session,
|
|
1201
|
+
context="previous_response_reuse_log",
|
|
1202
|
+
),
|
|
1203
|
+
cache_key_family=key.affinity_kind,
|
|
1204
|
+
model_class=_extract_model_class(previous_session.request_model)
|
|
1205
|
+
if previous_session.request_model
|
|
1206
|
+
else None,
|
|
1207
|
+
)
|
|
1208
|
+
session_to_return_after_close = previous_session
|
|
1209
|
+
elif previous_session is not None and not _http_bridge_models_compatible(
|
|
1210
|
+
previous_session.request_model, request_model
|
|
1211
|
+
):
|
|
1212
|
+
model_transition_rebind = True
|
|
1213
|
+
model_transition_parent_key = previous_key
|
|
1214
|
+
if is_http_bridge_account_neutral_replay(
|
|
1215
|
+
kind=previous_key.affinity_kind,
|
|
1216
|
+
key=previous_key.affinity_key,
|
|
1217
|
+
):
|
|
1218
|
+
recovery_fork_key = _http_bridge_incompatible_model_fork_key(
|
|
1219
|
+
key=previous_key,
|
|
1220
|
+
existing_model=previous_session.request_model,
|
|
1221
|
+
request_model=request_model,
|
|
1222
|
+
request_scope_id=request_scope_id,
|
|
1223
|
+
)
|
|
1224
|
+
assert recovery_fork_key is not None
|
|
1225
|
+
bind_account_neutral_recovery_owner(previous_session)
|
|
1226
|
+
if key != recovery_fork_key:
|
|
1227
|
+
key = recovery_fork_key
|
|
1228
|
+
continue
|
|
1229
|
+
elif previous_key is not None and _http_bridge_alias_target_is_stale(previous_session):
|
|
1230
|
+
self._http_bridge_previous_response_index.pop(previous_alias_key, None)
|
|
1231
|
+
if (
|
|
1232
|
+
session_to_return_after_close is None
|
|
1233
|
+
and previous_response_id is not None
|
|
1234
|
+
and not used_session_header_fallback
|
|
1235
|
+
and not allow_previous_response_recovery_rebind
|
|
1236
|
+
and durable_lookup is None
|
|
1237
|
+
and not model_transition_rebind
|
|
1238
|
+
):
|
|
1239
|
+
_record_continuity_fail_closed(
|
|
1240
|
+
surface="http_bridge",
|
|
1241
|
+
reason="continuity_lost",
|
|
1242
|
+
previous_response_id=previous_response_id,
|
|
1243
|
+
session_id=incoming_turn_state or incoming_session_key,
|
|
1244
|
+
)
|
|
1245
|
+
continuity_error = ProxyResponseError(502, _http_bridge_continuity_lost_error_envelope())
|
|
1246
|
+
elif missing_turn_state_alias and inflight_future is None and durable_lookup is None:
|
|
1247
|
+
turn_state_scope_conflict = incoming_turn_state is not None and any(
|
|
1248
|
+
alias == incoming_turn_state and alias_api_key != api_key_id
|
|
1249
|
+
for alias, alias_api_key in self._http_bridge_turn_state_index
|
|
1250
|
+
)
|
|
1251
|
+
if turn_state_scope_conflict:
|
|
1252
|
+
_record_continuity_fail_closed(
|
|
1253
|
+
surface="http_bridge",
|
|
1254
|
+
reason="turn_state_scope_conflict",
|
|
1255
|
+
previous_response_id=previous_response_id,
|
|
1256
|
+
session_id=incoming_turn_state,
|
|
1257
|
+
)
|
|
1258
|
+
continuity_error = ProxyResponseError(
|
|
1259
|
+
409,
|
|
1260
|
+
openai_error(
|
|
1261
|
+
"bridge_instance_mismatch",
|
|
1262
|
+
"HTTP bridge turn-state is bound to a different API key scope",
|
|
1263
|
+
error_type="server_error",
|
|
1264
|
+
),
|
|
1265
|
+
)
|
|
1266
|
+
elif (
|
|
1267
|
+
incoming_turn_state is not None
|
|
1268
|
+
and incoming_turn_state.startswith("http_turn_")
|
|
1269
|
+
and not allow_forward_to_owner
|
|
1270
|
+
):
|
|
1271
|
+
_record_continuity_fail_closed(
|
|
1272
|
+
surface="http_bridge",
|
|
1273
|
+
reason="generated_turn_state_continuity_lost",
|
|
1274
|
+
previous_response_id=previous_response_id,
|
|
1275
|
+
session_id=incoming_turn_state,
|
|
1276
|
+
)
|
|
1277
|
+
continuity_error = ProxyResponseError(
|
|
1278
|
+
409,
|
|
1279
|
+
openai_error(
|
|
1280
|
+
"bridge_instance_mismatch",
|
|
1281
|
+
"HTTP bridge continuity was lost for generated turn-state",
|
|
1282
|
+
error_type="server_error",
|
|
1283
|
+
),
|
|
1284
|
+
)
|
|
1285
|
+
else:
|
|
1286
|
+
_log_http_bridge_event(
|
|
1287
|
+
"turn_state_alias_miss_local_rebind",
|
|
1288
|
+
key,
|
|
1289
|
+
account_id=None,
|
|
1290
|
+
model=request_model,
|
|
1291
|
+
detail="outcome=local_rebind_without_alias",
|
|
1292
|
+
cache_key_family=key.affinity_kind,
|
|
1293
|
+
model_class=_extract_model_class(request_model) if request_model else None,
|
|
1294
|
+
owner_check_applied=owner_check_required,
|
|
1295
|
+
)
|
|
1296
|
+
elif session_to_return_after_close is None and inflight_future is None and owner_forward is None:
|
|
1297
|
+
# Owner forwards never resolve a local inflight reservation; skip admission.
|
|
1298
|
+
# Detached generations remain globally capacity-owned
|
|
1299
|
+
# until close finalization. This request may discount
|
|
1300
|
+
# only the idle generations it has committed to close
|
|
1301
|
+
# synchronously below, before its inflight reservation
|
|
1302
|
+
# can create a replacement socket.
|
|
1303
|
+
_plan_http_bridge_lru_capacity_closes(
|
|
1304
|
+
self,
|
|
1305
|
+
max_sessions=max_sessions,
|
|
1306
|
+
model_transition_parent_key=model_transition_parent_key,
|
|
1307
|
+
sessions_to_close_before_create=sessions_to_close_before_create,
|
|
1308
|
+
)
|
|
1309
|
+
if (
|
|
1310
|
+
_http_bridge_capacity_after_planned_closes(self, sessions_to_close_before_create)
|
|
1311
|
+
>= max_sessions
|
|
1312
|
+
):
|
|
1313
|
+
if _http_bridge_inflight_creation_count(self):
|
|
1314
|
+
capacity_wait_future = next(
|
|
1315
|
+
future
|
|
1316
|
+
for future in self._http_bridge_inflight_sessions.values()
|
|
1317
|
+
if not getattr(future, "_http_bridge_handoff", False)
|
|
1318
|
+
)
|
|
1319
|
+
else:
|
|
1320
|
+
capacity_error = self._http_bridge_active_capacity_error(
|
|
1321
|
+
key=key,
|
|
1322
|
+
request_model=request_model,
|
|
1323
|
+
)
|
|
1324
|
+
if not sessions_to_close_before_create:
|
|
1325
|
+
raise capacity_error
|
|
1326
|
+
# Detachment already transferred these LRU
|
|
1327
|
+
# generations out of the canonical registry.
|
|
1328
|
+
# Give each one a bounded-close owner before
|
|
1329
|
+
# rejecting admission; otherwise this early 429
|
|
1330
|
+
# leaves its live socket and leases stranded in
|
|
1331
|
+
# the detached registry until unrelated cleanup.
|
|
1332
|
+
capacity_error_after_planned_closes = capacity_error
|
|
1333
|
+
else:
|
|
1334
|
+
inflight_future = asyncio.get_running_loop().create_future()
|
|
1335
|
+
setattr(
|
|
1336
|
+
inflight_future,
|
|
1337
|
+
_HTTP_BRIDGE_INFLIGHT_STARTED_AT_ATTR,
|
|
1338
|
+
clock_for(self).monotonic(),
|
|
1339
|
+
)
|
|
1340
|
+
self._http_bridge_inflight_sessions[key] = inflight_future
|
|
1341
|
+
owns_creation = True
|
|
1342
|
+
try:
|
|
1343
|
+
for session_to_close in sessions_to_close_before_create:
|
|
1344
|
+
await self._close_http_bridge_session_bounded(session_to_close, reason="registry_detach")
|
|
1345
|
+
except BaseException as exc:
|
|
1346
|
+
if owns_creation:
|
|
1347
|
+
await self._fail_http_bridge_inflight_session_creation(key, inflight_future, exc)
|
|
1348
|
+
raise
|
|
1349
|
+
if capacity_error_after_planned_closes is not None:
|
|
1350
|
+
raise capacity_error_after_planned_closes
|
|
1351
|
+
if owns_creation and sessions_to_close_before_create:
|
|
1352
|
+
await self._enforce_http_bridge_capacity_after_planned_closes(
|
|
1353
|
+
key=key,
|
|
1354
|
+
inflight_future=inflight_future,
|
|
1355
|
+
max_sessions=max_sessions,
|
|
1356
|
+
request_model=request_model,
|
|
1357
|
+
)
|
|
1358
|
+
if session_to_return_after_close is not None:
|
|
1359
|
+
return session_to_return_after_close
|
|
1360
|
+
if owner_forward is not None:
|
|
1361
|
+
return owner_forward
|
|
1362
|
+
if owner_mismatch_error is not None:
|
|
1363
|
+
raise owner_mismatch_error
|
|
1364
|
+
if continuity_error is not None:
|
|
1365
|
+
raise continuity_error
|
|
1366
|
+
if capacity_wait_future is not None:
|
|
1367
|
+
wait_timeout_seconds = _proxy_admission_wait_timeout_seconds()
|
|
1368
|
+
try:
|
|
1369
|
+
await self._await_http_bridge_registry_wait(capacity_wait_future, timeout=wait_timeout_seconds)
|
|
1370
|
+
except asyncio.CancelledError:
|
|
1371
|
+
if capacity_wait_future.cancelled():
|
|
1372
|
+
continue
|
|
1373
|
+
raise
|
|
1374
|
+
except TimeoutError as exc:
|
|
1375
|
+
timeout_error = _http_bridge_startup_wait_timeout_error(
|
|
1376
|
+
"http_bridge_capacity",
|
|
1377
|
+
code="capacity_exhausted_active_sessions",
|
|
1378
|
+
)
|
|
1379
|
+
stale_key = await self._evict_http_bridge_inflight_waiter(capacity_wait_future, timeout_error)
|
|
1380
|
+
_log_http_bridge_startup_wait_timeout(
|
|
1381
|
+
stage="capacity",
|
|
1382
|
+
timeout_seconds=wait_timeout_seconds,
|
|
1383
|
+
key=stale_key or key,
|
|
1384
|
+
request_model=request_model,
|
|
1385
|
+
pending_count=_http_bridge_session_generation_count(self),
|
|
1386
|
+
inflight_count=len(self._http_bridge_inflight_sessions),
|
|
1387
|
+
)
|
|
1388
|
+
raise timeout_error from exc
|
|
1389
|
+
except ProxyResponseError:
|
|
1390
|
+
raise
|
|
1391
|
+
except Exception:
|
|
1392
|
+
pass
|
|
1393
|
+
continue
|
|
1394
|
+
if inflight_future is not None and not owns_creation:
|
|
1395
|
+
wait_timeout_seconds = _proxy_admission_wait_timeout_seconds()
|
|
1396
|
+
try:
|
|
1397
|
+
session = await self._await_http_bridge_registry_wait(inflight_future, timeout=wait_timeout_seconds)
|
|
1398
|
+
except asyncio.CancelledError:
|
|
1399
|
+
if inflight_future.cancelled():
|
|
1400
|
+
continue
|
|
1401
|
+
raise
|
|
1402
|
+
except TimeoutError as exc:
|
|
1403
|
+
timeout_error = _http_bridge_startup_wait_timeout_error(
|
|
1404
|
+
"http_bridge_inflight_session",
|
|
1405
|
+
code="capacity_exhausted_active_sessions",
|
|
1406
|
+
)
|
|
1407
|
+
await self._fail_http_bridge_inflight_session_creation(key, inflight_future, timeout_error)
|
|
1408
|
+
_log_http_bridge_startup_wait_timeout(
|
|
1409
|
+
stage="inflight_session",
|
|
1410
|
+
timeout_seconds=wait_timeout_seconds,
|
|
1411
|
+
key=key,
|
|
1412
|
+
request_model=request_model,
|
|
1413
|
+
pending_count=_http_bridge_session_generation_count(self),
|
|
1414
|
+
inflight_count=len(self._http_bridge_inflight_sessions),
|
|
1415
|
+
)
|
|
1416
|
+
raise timeout_error from exc
|
|
1417
|
+
except Exception:
|
|
1418
|
+
raise
|
|
1419
|
+
if session is None:
|
|
1420
|
+
continue
|
|
1421
|
+
fork_key = _http_bridge_parallel_fork_key(
|
|
1422
|
+
key=key,
|
|
1423
|
+
session=session,
|
|
1424
|
+
inflight_creation=False,
|
|
1425
|
+
incoming_turn_state=incoming_turn_state,
|
|
1426
|
+
previous_response_id=previous_response_id,
|
|
1427
|
+
request_model=request_model,
|
|
1428
|
+
request_service_tier=request_service_tier,
|
|
1429
|
+
request_scope_id=request_scope_id,
|
|
1430
|
+
same_model_required=True,
|
|
1431
|
+
force_canonical_replacement=force_goal_restart_account_reselection,
|
|
1432
|
+
)
|
|
1433
|
+
if fork_key is not None:
|
|
1434
|
+
bind_account_neutral_recovery_owner(session)
|
|
1435
|
+
model_transition_parent_key, key = key, fork_key
|
|
1436
|
+
durable_lookup = None
|
|
1437
|
+
force_durable_takeover_after_detach = False
|
|
1438
|
+
locally_owned_fork_key = _http_bridge_locally_owned_fork_key(
|
|
1439
|
+
fork_key, forwarded_request, forwarded_original_request_unanchored
|
|
1440
|
+
)
|
|
1441
|
+
continue
|
|
1442
|
+
if (
|
|
1443
|
+
not force_goal_restart_account_reselection
|
|
1444
|
+
and not session.closed
|
|
1445
|
+
and _http_bridge_session_account_active(session)
|
|
1446
|
+
and _http_bridge_session_allows_api_key(session, api_key)
|
|
1447
|
+
and _http_bridge_compatible(session, request_model, request_service_tier, True)
|
|
1448
|
+
and _http_bridge_session_reusable_for_request(
|
|
1449
|
+
session=session,
|
|
1450
|
+
key=key,
|
|
1451
|
+
incoming_turn_state=incoming_turn_state,
|
|
1452
|
+
previous_response_id=previous_response_id,
|
|
1453
|
+
)
|
|
1454
|
+
and _http_bridge_session_matches_preferred_account(
|
|
1455
|
+
session=session,
|
|
1456
|
+
previous_response_id=previous_response_id,
|
|
1457
|
+
preferred_account_id=preferred_account_id,
|
|
1458
|
+
require_preferred_account=require_preferred_account,
|
|
1459
|
+
)
|
|
1460
|
+
):
|
|
1461
|
+
current_instance = settings.http_responses_session_bridge_instance_id
|
|
1462
|
+
if _durable_bridge_lookup_allows_local_reuse(durable_lookup, current_instance=current_instance):
|
|
1463
|
+
session.api_key = api_key
|
|
1464
|
+
session.request_model = request_model
|
|
1465
|
+
session.request_service_tier = request_service_tier
|
|
1466
|
+
session.last_used_at = clock_for(self).monotonic()
|
|
1467
|
+
return session
|
|
1468
|
+
if force_goal_restart_account_reselection or (
|
|
1469
|
+
not session.closed and session.account.status in ROUTABLE_STATUSES
|
|
1470
|
+
):
|
|
1471
|
+
old_account_id = session.account.id
|
|
1472
|
+
retiring_with_visible_requests = _http_bridge_session_retiring_with_visible_requests(session)
|
|
1473
|
+
async with self._http_bridge_lock:
|
|
1474
|
+
detached = self._detach_http_bridge_session_locked(
|
|
1475
|
+
key,
|
|
1476
|
+
expected_session=session,
|
|
1477
|
+
mark_closed=not retiring_with_visible_requests,
|
|
1478
|
+
)
|
|
1479
|
+
if detached is not None:
|
|
1480
|
+
force_durable_takeover_after_detach = True
|
|
1481
|
+
if detached is not None and not retiring_with_visible_requests:
|
|
1482
|
+
self._schedule_http_bridge_session_closes([detached], reason="registry_detach")
|
|
1483
|
+
continue
|
|
1484
|
+
created_session: _HTTPBridgeSession | None = None
|
|
1485
|
+
session_registered = False
|
|
1486
|
+
try:
|
|
1487
|
+
create_session = self._create_http_bridge_session
|
|
1488
|
+
preferred_account_is_continuity_owner = preferred_account_id is not None and (
|
|
1489
|
+
preferred_account_has_continuity_provenance
|
|
1490
|
+
or previous_response_id is not None
|
|
1491
|
+
or is_http_bridge_account_neutral_replay(
|
|
1492
|
+
kind=key.affinity_kind,
|
|
1493
|
+
key=key.affinity_key,
|
|
1494
|
+
)
|
|
1495
|
+
)
|
|
1496
|
+
create_kwargs: dict[str, Any] = {
|
|
1497
|
+
"headers": headers,
|
|
1498
|
+
"affinity": affinity,
|
|
1499
|
+
"api_key": api_key,
|
|
1500
|
+
"request_model": request_model,
|
|
1501
|
+
"request_service_tier": request_service_tier,
|
|
1502
|
+
"idle_ttl_seconds": effective_idle_ttl_seconds,
|
|
1503
|
+
"request_stage": request_stage,
|
|
1504
|
+
"preferred_account_id": preferred_account_id,
|
|
1505
|
+
"require_preferred_account": require_preferred_account,
|
|
1506
|
+
"preferred_account_is_continuity_owner": preferred_account_is_continuity_owner,
|
|
1507
|
+
"fallback_on_preferred_account_unavailable": (
|
|
1508
|
+
fallback_on_preferred_account_unavailable and not require_preferred_account
|
|
1509
|
+
),
|
|
1510
|
+
"request_usage_budget": request_usage_budget,
|
|
1511
|
+
"request_deadline": request_deadline,
|
|
1512
|
+
"exclude_account_ids": exclude_account_ids,
|
|
1513
|
+
"deferred_account_backoff_lifecycle": deferred_account_backoff_lifecycle,
|
|
1514
|
+
"defer_account_health_writes": defer_account_health_writes,
|
|
1515
|
+
}
|
|
1516
|
+
try:
|
|
1517
|
+
create_signature = inspect.signature(create_session)
|
|
1518
|
+
except (TypeError, ValueError):
|
|
1519
|
+
create_signature = None
|
|
1520
|
+
create_accepts_var_keyword = create_signature is not None and any(
|
|
1521
|
+
parameter.kind is inspect.Parameter.VAR_KEYWORD
|
|
1522
|
+
for parameter in create_signature.parameters.values()
|
|
1523
|
+
)
|
|
1524
|
+
if create_signature is not None and not create_accepts_var_keyword:
|
|
1525
|
+
for optional_kwarg in (
|
|
1526
|
+
"request_service_tier",
|
|
1527
|
+
"request_usage_budget",
|
|
1528
|
+
"request_deadline",
|
|
1529
|
+
"exclude_account_ids",
|
|
1530
|
+
"preferred_account_is_continuity_owner",
|
|
1531
|
+
"deferred_account_backoff_lifecycle",
|
|
1532
|
+
"defer_account_health_writes",
|
|
1533
|
+
):
|
|
1534
|
+
if optional_kwarg not in create_signature.parameters:
|
|
1535
|
+
create_kwargs.pop(optional_kwarg, None)
|
|
1536
|
+
created_session = await create_session(key, **create_kwargs)
|
|
1537
|
+
await _raise_if_http_bridge_creation_superseded(self, key, inflight_future=inflight_future)
|
|
1538
|
+
claim_kwargs: dict[str, Any] = {
|
|
1539
|
+
"allow_takeover": _http_bridge_claim_allows_takeover(
|
|
1540
|
+
durable_lookup,
|
|
1541
|
+
force=force_durable_takeover,
|
|
1542
|
+
),
|
|
1543
|
+
"force_owner_epoch_advance": (force_durable_takeover or same_replica_durable_predecessor),
|
|
1544
|
+
}
|
|
1545
|
+
restart_takeover = durable_lookup is not None and _http_bridge_allow_durable_takeover(durable_lookup)
|
|
1546
|
+
if restart_takeover:
|
|
1547
|
+
# restart_takeover means recovering a row whose previous
|
|
1548
|
+
# owner is genuinely gone. Every claim now advances the
|
|
1549
|
+
# epoch, so epoch > 1 alone would also count ordinary
|
|
1550
|
+
# local successor claims (no pre-claim lookup, or a
|
|
1551
|
+
# forced replace of a live local session).
|
|
1552
|
+
claim_kwargs["record_restart_takeover"] = True
|
|
1553
|
+
await self._claim_durable_http_bridge_session(created_session, **claim_kwargs)
|
|
1554
|
+
async with self._http_bridge_lock:
|
|
1555
|
+
current_future = self._http_bridge_inflight_sessions.get(key)
|
|
1556
|
+
if current_future is inflight_future:
|
|
1557
|
+
self._http_bridge_inflight_sessions.pop(key, None)
|
|
1558
|
+
if original_request_unanchored:
|
|
1559
|
+
_reserve_http_bridge_unanchored_handoff(created_session, request_scope_id=request_scope_id)
|
|
1560
|
+
self._http_bridge_sessions[key] = created_session
|
|
1561
|
+
session_registered = True
|
|
1562
|
+
if inflight_future is not None and not inflight_future.done():
|
|
1563
|
+
inflight_future.set_result(created_session)
|
|
1564
|
+
if not session_registered:
|
|
1565
|
+
raise _http_bridge_startup_wait_timeout_error(
|
|
1566
|
+
"http_bridge_session_registration",
|
|
1567
|
+
code="capacity_exhausted_active_sessions",
|
|
1568
|
+
)
|
|
1569
|
+
except BaseException as exc:
|
|
1570
|
+
superseded = await _settle_failed_http_bridge_creation(
|
|
1571
|
+
self,
|
|
1572
|
+
key,
|
|
1573
|
+
inflight_future=inflight_future,
|
|
1574
|
+
created_session=created_session,
|
|
1575
|
+
exc=exc,
|
|
1576
|
+
)
|
|
1577
|
+
if created_session is not None and not session_registered:
|
|
1578
|
+
await self._close_http_bridge_session(
|
|
1579
|
+
created_session,
|
|
1580
|
+
release_durable_session=not superseded,
|
|
1581
|
+
)
|
|
1582
|
+
raise
|
|
1583
|
+
assert created_session is not None
|
|
1584
|
+
_log_http_bridge_event(
|
|
1585
|
+
"create",
|
|
1586
|
+
key,
|
|
1587
|
+
account_id=created_session.account.id,
|
|
1588
|
+
model=created_session.request_model,
|
|
1589
|
+
detail=(
|
|
1590
|
+
f"request_stage={request_stage}, preferred_account_id={preferred_account_id}, "
|
|
1591
|
+
f"selected_account_id={created_session.account.id}, "
|
|
1592
|
+
f"durable_session_id={created_session.durable_session_id}"
|
|
1593
|
+
),
|
|
1594
|
+
cache_key_family=key.affinity_kind,
|
|
1595
|
+
model_class=_extract_model_class(created_session.request_model)
|
|
1596
|
+
if created_session.request_model
|
|
1597
|
+
else None,
|
|
1598
|
+
)
|
|
1599
|
+
if old_account_id is not None and old_account_id != created_session.account.id:
|
|
1600
|
+
_log_http_bridge_event(
|
|
1601
|
+
"reallocation_orphan",
|
|
1602
|
+
key,
|
|
1603
|
+
account_id=created_session.account.id,
|
|
1604
|
+
model=created_session.request_model,
|
|
1605
|
+
detail=f"old_account={old_account_id}",
|
|
1606
|
+
cache_key_family=key.affinity_kind,
|
|
1607
|
+
model_class=_extract_model_class(created_session.request_model)
|
|
1608
|
+
if created_session.request_model
|
|
1609
|
+
else None,
|
|
1610
|
+
)
|
|
1611
|
+
return created_session
|
|
1612
|
+
|
|
1613
|
+
async def mark_http_bridge_draining(self) -> bool:
|
|
1614
|
+
try:
|
|
1615
|
+
await self._durable_bridge.mark_instance_draining(
|
|
1616
|
+
instance_id=_service_get_settings().http_responses_session_bridge_instance_id,
|
|
1617
|
+
)
|
|
1618
|
+
except Exception:
|
|
1619
|
+
logger.warning("Failed to mark durable HTTP bridge sessions draining", exc_info=True)
|
|
1620
|
+
return False
|
|
1621
|
+
return True
|
|
1622
|
+
|
|
1623
|
+
def _prune_http_bridge_sessions_locked(self) -> list["_HTTPBridgeSession"]:
|
|
1624
|
+
now = clock_for(self).monotonic()
|
|
1625
|
+
stale_keys: list[_HTTPBridgeSessionKey] = []
|
|
1626
|
+
for key, session in self._http_bridge_sessions.items():
|
|
1627
|
+
if _http_bridge_session_has_admission_waiter(session):
|
|
1628
|
+
continue
|
|
1629
|
+
if session.handoff_in_progress:
|
|
1630
|
+
continue
|
|
1631
|
+
if session.closed:
|
|
1632
|
+
stale_keys.append(key)
|
|
1633
|
+
continue
|
|
1634
|
+
if getattr(session, "unanchored_reservation_id", None) is not None:
|
|
1635
|
+
continue
|
|
1636
|
+
pending_count = self._http_bridge_pending_count_nowait(session, context="idle_prune")
|
|
1637
|
+
if pending_count is None:
|
|
1638
|
+
continue
|
|
1639
|
+
if pending_count:
|
|
1640
|
+
continue
|
|
1641
|
+
if now - session.last_used_at < session.idle_ttl_seconds:
|
|
1642
|
+
continue
|
|
1643
|
+
stale_keys.append(key)
|
|
1644
|
+
sessions_to_close: list[_HTTPBridgeSession] = []
|
|
1645
|
+
for key in stale_keys:
|
|
1646
|
+
session = self._detach_http_bridge_session_locked(key)
|
|
1647
|
+
if session is not None:
|
|
1648
|
+
_log_http_bridge_event(
|
|
1649
|
+
"evict_idle",
|
|
1650
|
+
key,
|
|
1651
|
+
account_id=session.account.id,
|
|
1652
|
+
model=session.request_model,
|
|
1653
|
+
cache_key_family=key.affinity_kind,
|
|
1654
|
+
model_class=_extract_model_class(session.request_model) if session.request_model else None,
|
|
1655
|
+
)
|
|
1656
|
+
sessions_to_close.append(session)
|
|
1657
|
+
return sessions_to_close
|
|
1658
|
+
|
|
1659
|
+
_close_http_bridge_session = _helpers_close_http_bridge_session
|
|
1660
|
+
|
|
1661
|
+
async def _create_http_bridge_session(
|
|
1662
|
+
self,
|
|
1663
|
+
key: "_HTTPBridgeSessionKey",
|
|
1664
|
+
*,
|
|
1665
|
+
headers: dict[str, str],
|
|
1666
|
+
affinity: _AffinityPolicy,
|
|
1667
|
+
api_key: ApiKeyData | None,
|
|
1668
|
+
request_model: str | None,
|
|
1669
|
+
idle_ttl_seconds: float,
|
|
1670
|
+
request_service_tier: str | None = None,
|
|
1671
|
+
request_stage: str = "first_turn",
|
|
1672
|
+
preferred_account_id: str | None = None,
|
|
1673
|
+
require_preferred_account: bool = False,
|
|
1674
|
+
preferred_account_is_continuity_owner: bool = False,
|
|
1675
|
+
fallback_on_preferred_account_unavailable: bool = True,
|
|
1676
|
+
request_usage_budget: ApiKeyRequestUsageBudget | None = None,
|
|
1677
|
+
request_deadline: float | None = None,
|
|
1678
|
+
exclude_account_ids: Collection[str] | None = None,
|
|
1679
|
+
deferred_account_backoff_lifecycle: _DeferredAccountBackoffLifecycle | None = None,
|
|
1680
|
+
defer_account_health_writes: bool = False,
|
|
1681
|
+
) -> "_HTTPBridgeSession":
|
|
1682
|
+
request_state = _WebSocketRequestState(
|
|
1683
|
+
request_id=f"http_bridge_connect_{uuid4().hex}",
|
|
1684
|
+
model=request_model,
|
|
1685
|
+
service_tier=request_service_tier,
|
|
1686
|
+
requested_service_tier=request_service_tier,
|
|
1687
|
+
reasoning_effort=None,
|
|
1688
|
+
api_key_reservation=None,
|
|
1689
|
+
started_at=clock_for(self).monotonic(),
|
|
1690
|
+
thread_affinity_last_touch_at=clock_for(self).monotonic(),
|
|
1691
|
+
api_key_reservation_last_touch_at=clock_for(self).monotonic(),
|
|
1692
|
+
transport=_REQUEST_TRANSPORT_HTTP,
|
|
1693
|
+
)
|
|
1694
|
+
deadline = (
|
|
1695
|
+
request_deadline
|
|
1696
|
+
if request_deadline is not None
|
|
1697
|
+
else _websocket_connect_deadline(
|
|
1698
|
+
request_state,
|
|
1699
|
+
_http_bridge_request_budget_seconds(_service_get_settings()),
|
|
1700
|
+
now=clock_for(self).monotonic(),
|
|
1701
|
+
)
|
|
1702
|
+
)
|
|
1703
|
+
settings = await _service_get_settings_cache().get()
|
|
1704
|
+
excluded_account_ids = set(exclude_account_ids or ())
|
|
1705
|
+
if require_preferred_account:
|
|
1706
|
+
fallback_on_preferred_account_unavailable = False
|
|
1707
|
+
retry_same_account_once = preferred_account_id is not None
|
|
1708
|
+
proxy_connect_failover = _HTTPBridgePreDispatchFailover(
|
|
1709
|
+
excluded_account_ids,
|
|
1710
|
+
preferred_account_id,
|
|
1711
|
+
affinity.reallocate_sticky,
|
|
1712
|
+
)
|
|
1713
|
+
selected_account_lease: AccountLease | None = None
|
|
1714
|
+
while True:
|
|
1715
|
+
select_kwargs = {
|
|
1716
|
+
"request_id": request_state.request_log_id or request_state.request_id,
|
|
1717
|
+
"kind": "http_bridge",
|
|
1718
|
+
"request_stage": request_stage,
|
|
1719
|
+
"api_key": api_key,
|
|
1720
|
+
"affinity_policy": (
|
|
1721
|
+
replace(affinity, reallocate_sticky=True)
|
|
1722
|
+
if proxy_connect_failover.reallocate_sticky and not affinity.reallocate_sticky
|
|
1723
|
+
else affinity
|
|
1724
|
+
),
|
|
1725
|
+
"prefer_earlier_reset_accounts": settings.prefer_earlier_reset_accounts,
|
|
1726
|
+
"prefer_earlier_reset_window": _prefer_earlier_reset_window(settings),
|
|
1727
|
+
"routing_strategy": _routing_strategy(settings),
|
|
1728
|
+
"model": request_model,
|
|
1729
|
+
"service_tier": request_service_tier,
|
|
1730
|
+
"exclude_account_ids": excluded_account_ids,
|
|
1731
|
+
"preferred_account_id": proxy_connect_failover.preferred_account_id,
|
|
1732
|
+
"preferred_account_is_continuity_owner": preferred_account_is_continuity_owner,
|
|
1733
|
+
"lease_kind": "stream",
|
|
1734
|
+
"estimated_lease_tokens": _estimated_lease_tokens_from_request_usage_budget(request_usage_budget),
|
|
1735
|
+
"fallback_on_preferred_account_unavailable": fallback_on_preferred_account_unavailable,
|
|
1736
|
+
}
|
|
1737
|
+
selection = await self._select_account_with_budget_for_stream(deadline, **select_kwargs)
|
|
1738
|
+
selected_account_lease = selection.lease
|
|
1739
|
+
account = selection.account
|
|
1740
|
+
if account is None:
|
|
1741
|
+
await self._load_balancer.release_account_lease(selected_account_lease)
|
|
1742
|
+
selected_account_lease = None
|
|
1743
|
+
_record_same_account_takeover(
|
|
1744
|
+
preferred_account_id=preferred_account_id,
|
|
1745
|
+
selected_account_id=None,
|
|
1746
|
+
)
|
|
1747
|
+
if proxy_connect_failover.last_error is not None:
|
|
1748
|
+
# Preserve a confirmed pre-dispatch route failure instead
|
|
1749
|
+
# of generating ``no_accounts``.
|
|
1750
|
+
raise proxy_connect_failover.last_error
|
|
1751
|
+
if (
|
|
1752
|
+
require_preferred_account
|
|
1753
|
+
and preferred_account_id is not None
|
|
1754
|
+
and preferred_account_is_continuity_owner
|
|
1755
|
+
and selection.error_code in (CONTINUITY_OWNER_UNAVAILABLE, "hard_affinity_saturated")
|
|
1756
|
+
):
|
|
1757
|
+
raise _http_bridge_previous_response_owner_unavailable_error()
|
|
1758
|
+
status_code, error_payload = selection_failure_response(selection)
|
|
1759
|
+
raise ProxyResponseError(status_code, error_payload)
|
|
1760
|
+
if require_preferred_account and preferred_account_id is not None and account.id != preferred_account_id:
|
|
1761
|
+
await self._load_balancer.release_account_lease(selected_account_lease)
|
|
1762
|
+
selected_account_lease = None
|
|
1763
|
+
_record_same_account_takeover(
|
|
1764
|
+
preferred_account_id=preferred_account_id,
|
|
1765
|
+
selected_account_id=account.id,
|
|
1766
|
+
)
|
|
1767
|
+
if preferred_account_is_continuity_owner:
|
|
1768
|
+
raise _http_bridge_previous_response_owner_unavailable_error()
|
|
1769
|
+
raise ProxyResponseError(
|
|
1770
|
+
503,
|
|
1771
|
+
openai_error(
|
|
1772
|
+
"preferred_account_unavailable",
|
|
1773
|
+
"Preferred account is unavailable; retry later.",
|
|
1774
|
+
error_type="server_error",
|
|
1775
|
+
),
|
|
1776
|
+
)
|
|
1777
|
+
selected_is_preferred = preferred_account_id is not None and account.id == preferred_account_id
|
|
1778
|
+
try:
|
|
1779
|
+
account = await self._ensure_fresh_with_budget(
|
|
1780
|
+
account,
|
|
1781
|
+
timeout_seconds=self._remaining_budget_seconds(deadline),
|
|
1782
|
+
)
|
|
1783
|
+
connect_headers = _websocket_safe_headers_with_turn_state(
|
|
1784
|
+
headers, _sticky_key_from_turn_state_header(headers)
|
|
1785
|
+
)
|
|
1786
|
+
upstream = await _call_with_supported_optional_kwargs(
|
|
1787
|
+
self._open_upstream_websocket_with_budget,
|
|
1788
|
+
account,
|
|
1789
|
+
connect_headers,
|
|
1790
|
+
optional_kwargs={"request_state": request_state},
|
|
1791
|
+
timeout_seconds=self._remaining_budget_seconds(deadline),
|
|
1792
|
+
)
|
|
1793
|
+
_record_same_account_takeover(
|
|
1794
|
+
preferred_account_id=preferred_account_id,
|
|
1795
|
+
selected_account_id=account.id,
|
|
1796
|
+
)
|
|
1797
|
+
break
|
|
1798
|
+
except ProxyResponseError as exc:
|
|
1799
|
+
if await proxy_connect_failover.handle(
|
|
1800
|
+
self,
|
|
1801
|
+
account,
|
|
1802
|
+
selected_account_lease,
|
|
1803
|
+
exc,
|
|
1804
|
+
required_account=require_preferred_account and selected_is_preferred,
|
|
1805
|
+
deferred_account_backoff_lifecycle=deferred_account_backoff_lifecycle,
|
|
1806
|
+
defer_account_health_write=defer_account_health_writes,
|
|
1807
|
+
):
|
|
1808
|
+
selected_account_lease = None
|
|
1809
|
+
continue
|
|
1810
|
+
if exc.status_code != 401 or self._remaining_budget_seconds(deadline) <= 0:
|
|
1811
|
+
await self._load_balancer.release_account_lease(selected_account_lease)
|
|
1812
|
+
selected_account_lease = None
|
|
1813
|
+
raise
|
|
1814
|
+
try:
|
|
1815
|
+
account = await self._ensure_fresh_with_budget(
|
|
1816
|
+
account,
|
|
1817
|
+
force=True,
|
|
1818
|
+
timeout_seconds=self._remaining_budget_seconds(deadline),
|
|
1819
|
+
)
|
|
1820
|
+
connect_headers = _websocket_safe_headers_with_turn_state(
|
|
1821
|
+
headers, _sticky_key_from_turn_state_header(headers)
|
|
1822
|
+
)
|
|
1823
|
+
upstream = await self._open_upstream_websocket_with_budget(
|
|
1824
|
+
account,
|
|
1825
|
+
connect_headers,
|
|
1826
|
+
timeout_seconds=self._remaining_budget_seconds(deadline),
|
|
1827
|
+
request_state=request_state,
|
|
1828
|
+
)
|
|
1829
|
+
_record_same_account_takeover(
|
|
1830
|
+
preferred_account_id=preferred_account_id,
|
|
1831
|
+
selected_account_id=account.id,
|
|
1832
|
+
)
|
|
1833
|
+
break
|
|
1834
|
+
except ProxyResponseError as retry_exc:
|
|
1835
|
+
if await proxy_connect_failover.handle(
|
|
1836
|
+
self,
|
|
1837
|
+
account,
|
|
1838
|
+
selected_account_lease,
|
|
1839
|
+
retry_exc,
|
|
1840
|
+
required_account=require_preferred_account and selected_is_preferred,
|
|
1841
|
+
deferred_account_backoff_lifecycle=deferred_account_backoff_lifecycle,
|
|
1842
|
+
defer_account_health_write=defer_account_health_writes,
|
|
1843
|
+
):
|
|
1844
|
+
selected_account_lease = None
|
|
1845
|
+
continue
|
|
1846
|
+
if retry_exc.status_code != 401:
|
|
1847
|
+
await self._load_balancer.release_account_lease(selected_account_lease)
|
|
1848
|
+
selected_account_lease = None
|
|
1849
|
+
raise
|
|
1850
|
+
await self._handle_proxy_error(account, retry_exc)
|
|
1851
|
+
if require_preferred_account and selected_is_preferred:
|
|
1852
|
+
await self._load_balancer.release_account_lease(selected_account_lease)
|
|
1853
|
+
selected_account_lease = None
|
|
1854
|
+
raise
|
|
1855
|
+
excluded_account_ids.add(account.id)
|
|
1856
|
+
proxy_connect_failover.preferred_account_id = None
|
|
1857
|
+
await self._load_balancer.release_account_lease(selected_account_lease)
|
|
1858
|
+
selected_account_lease = None
|
|
1859
|
+
continue
|
|
1860
|
+
except RefreshError as refresh_exc:
|
|
1861
|
+
if refresh_exc.is_permanent:
|
|
1862
|
+
await self._load_balancer.mark_permanent_failure(account, refresh_exc.code)
|
|
1863
|
+
if require_preferred_account and selected_is_preferred:
|
|
1864
|
+
await self._load_balancer.release_account_lease(selected_account_lease)
|
|
1865
|
+
selected_account_lease = None
|
|
1866
|
+
raise
|
|
1867
|
+
excluded_account_ids.add(account.id)
|
|
1868
|
+
proxy_connect_failover.preferred_account_id = None
|
|
1869
|
+
await self._load_balancer.release_account_lease(selected_account_lease)
|
|
1870
|
+
selected_account_lease = None
|
|
1871
|
+
continue
|
|
1872
|
+
except RefreshError as exc:
|
|
1873
|
+
if exc.is_permanent:
|
|
1874
|
+
await self._load_balancer.mark_permanent_failure(account, exc.code)
|
|
1875
|
+
if selected_is_preferred and self._remaining_budget_seconds(deadline) > 0:
|
|
1876
|
+
if retry_same_account_once and not exc.is_permanent:
|
|
1877
|
+
retry_same_account_once = False
|
|
1878
|
+
await self._load_balancer.release_account_lease(selected_account_lease)
|
|
1879
|
+
selected_account_lease = None
|
|
1880
|
+
continue
|
|
1881
|
+
if require_preferred_account:
|
|
1882
|
+
await self._load_balancer.release_account_lease(selected_account_lease)
|
|
1883
|
+
selected_account_lease = None
|
|
1884
|
+
raise ProxyResponseError(
|
|
1885
|
+
503,
|
|
1886
|
+
openai_error(
|
|
1887
|
+
"no_accounts",
|
|
1888
|
+
"Preferred account is unavailable; retry later.",
|
|
1889
|
+
error_type="server_error",
|
|
1890
|
+
),
|
|
1891
|
+
) from exc
|
|
1892
|
+
excluded_account_ids.add(account.id)
|
|
1893
|
+
proxy_connect_failover.preferred_account_id = None
|
|
1894
|
+
await self._load_balancer.release_account_lease(selected_account_lease)
|
|
1895
|
+
selected_account_lease = None
|
|
1896
|
+
continue
|
|
1897
|
+
if exc.is_permanent:
|
|
1898
|
+
await self._load_balancer.release_account_lease(selected_account_lease)
|
|
1899
|
+
selected_account_lease = None
|
|
1900
|
+
raise ProxyResponseError(
|
|
1901
|
+
401,
|
|
1902
|
+
openai_error(
|
|
1903
|
+
"invalid_api_key",
|
|
1904
|
+
exc.message,
|
|
1905
|
+
error_type="authentication_error",
|
|
1906
|
+
),
|
|
1907
|
+
) from exc
|
|
1908
|
+
if request_stage == "first_turn":
|
|
1909
|
+
_record_bridge_first_turn_timeout()
|
|
1910
|
+
await self._load_balancer.release_account_lease(selected_account_lease)
|
|
1911
|
+
selected_account_lease = None
|
|
1912
|
+
_raise_proxy_unavailable(exc.message or "Temporary upstream refresh failure")
|
|
1913
|
+
except (aiohttp.ClientError, asyncio.TimeoutError) as exc:
|
|
1914
|
+
if selected_is_preferred and self._remaining_budget_seconds(deadline) > 0:
|
|
1915
|
+
if retry_same_account_once:
|
|
1916
|
+
retry_same_account_once = False
|
|
1917
|
+
await self._load_balancer.release_account_lease(selected_account_lease)
|
|
1918
|
+
selected_account_lease = None
|
|
1919
|
+
continue
|
|
1920
|
+
if require_preferred_account:
|
|
1921
|
+
await self._load_balancer.release_account_lease(selected_account_lease)
|
|
1922
|
+
selected_account_lease = None
|
|
1923
|
+
raise ProxyResponseError(
|
|
1924
|
+
503,
|
|
1925
|
+
openai_error(
|
|
1926
|
+
"no_accounts",
|
|
1927
|
+
"Preferred account is unavailable; retry later.",
|
|
1928
|
+
error_type="server_error",
|
|
1929
|
+
),
|
|
1930
|
+
) from exc
|
|
1931
|
+
excluded_account_ids.add(account.id)
|
|
1932
|
+
proxy_connect_failover.preferred_account_id = None
|
|
1933
|
+
await self._load_balancer.release_account_lease(selected_account_lease)
|
|
1934
|
+
selected_account_lease = None
|
|
1935
|
+
continue
|
|
1936
|
+
if request_stage == "first_turn":
|
|
1937
|
+
_record_bridge_first_turn_timeout()
|
|
1938
|
+
await self._load_balancer.release_account_lease(selected_account_lease)
|
|
1939
|
+
selected_account_lease = None
|
|
1940
|
+
_raise_proxy_unavailable(str(exc) or "Request to upstream timed out")
|
|
1941
|
+
except BaseException:
|
|
1942
|
+
await self._load_balancer.release_account_lease(selected_account_lease)
|
|
1943
|
+
selected_account_lease = None
|
|
1944
|
+
raise
|
|
1945
|
+
session = _HTTPBridgeSession(
|
|
1946
|
+
key=key,
|
|
1947
|
+
headers=connect_headers,
|
|
1948
|
+
affinity=_persistent_http_bridge_affinity(affinity),
|
|
1949
|
+
api_key=api_key,
|
|
1950
|
+
request_model=request_model,
|
|
1951
|
+
request_service_tier=request_service_tier,
|
|
1952
|
+
account=account,
|
|
1953
|
+
upstream=upstream,
|
|
1954
|
+
upstream_control=_WebSocketUpstreamControl(),
|
|
1955
|
+
pending_requests=deque(),
|
|
1956
|
+
pending_lock=fast_lock(),
|
|
1957
|
+
response_create_gate=asyncio.Semaphore(1),
|
|
1958
|
+
queued_request_count=0,
|
|
1959
|
+
lifecycle_lock=fast_lock(),
|
|
1960
|
+
last_used_at=clock_for(self).monotonic(),
|
|
1961
|
+
idle_ttl_seconds=idle_ttl_seconds,
|
|
1962
|
+
codex_session=(affinity.kind == StickySessionKind.CODEX_SESSION or key.affinity_kind == "thread_header"),
|
|
1963
|
+
access_token_expires_at=_token_expiry(account, self._encryptor),
|
|
1964
|
+
prewarm_lock=fast_lock(),
|
|
1965
|
+
upstream_turn_state=_upstream_turn_state_from_socket(upstream),
|
|
1966
|
+
downstream_turn_state=None,
|
|
1967
|
+
account_lease=selected_account_lease,
|
|
1968
|
+
catalog_omission_quota_admission=selection.catalog_omission_quota_admission,
|
|
1969
|
+
)
|
|
1970
|
+
_copy_websocket_route_metadata_to_session(session, request_state)
|
|
1971
|
+
session.upstream_reader = self._spawn_http_bridge_upstream_reader(session)
|
|
1972
|
+
return session
|
|
1973
|
+
|
|
1974
|
+
async def _reconnect_http_bridge_session(
|
|
1975
|
+
self,
|
|
1976
|
+
session: "_HTTPBridgeSession",
|
|
1977
|
+
*,
|
|
1978
|
+
request_state: _WebSocketRequestState,
|
|
1979
|
+
restart_reader: bool = False,
|
|
1980
|
+
require_security_work_authorized: bool = False,
|
|
1981
|
+
require_same_account: bool = False,
|
|
1982
|
+
require_preferred_account: bool = False,
|
|
1983
|
+
owner_rebind_affinity: _AffinityPolicy | None = None,
|
|
1984
|
+
selection_affinity: _AffinityPolicy | None = None,
|
|
1985
|
+
dashboard_settings: DashboardSettings | None = None,
|
|
1986
|
+
) -> None:
|
|
1987
|
+
request_state.response_create_sent_at = None
|
|
1988
|
+
goal_restart = request_state.affinity_policy.abandon_unavailable_legacy_owner
|
|
1989
|
+
if selection_affinity is None and goal_restart:
|
|
1990
|
+
selection_affinity = request_state.affinity_policy
|
|
1991
|
+
account_neutral_recovery = is_http_bridge_account_neutral_replay(
|
|
1992
|
+
kind=session.key.affinity_kind, key=session.key.affinity_key
|
|
1993
|
+
)
|
|
1994
|
+
require_same_account = account_neutral_recovery or (require_same_account and not goal_restart)
|
|
1995
|
+
old_upstream = session.upstream
|
|
1996
|
+
old_reader = session.upstream_reader if restart_reader else None
|
|
1997
|
+
session.handoff_in_progress = True
|
|
1998
|
+
inflight_sessions = self._http_bridge_inflight_sessions
|
|
1999
|
+
handoff_future = inflight_sessions.get(session.key) or asyncio.get_running_loop().create_future()
|
|
2000
|
+
inflight_sessions.setdefault(session.key, handoff_future)
|
|
2001
|
+
setattr(handoff_future, "_http_bridge_handoff", True)
|
|
2002
|
+
session.handoff_future = handoff_future
|
|
2003
|
+
session.closed = True
|
|
2004
|
+
if old_reader is not None:
|
|
2005
|
+
if old_reader is not asyncio.current_task():
|
|
2006
|
+
try:
|
|
2007
|
+
cancelled = await self._cancel_http_bridge_previous_reader(old_reader)
|
|
2008
|
+
except BaseException:
|
|
2009
|
+
session.closed = True
|
|
2010
|
+
_complete_http_bridge_handoff(session, self._http_bridge_inflight_sessions)
|
|
2011
|
+
raise
|
|
2012
|
+
if not cancelled:
|
|
2013
|
+
session.closed = True
|
|
2014
|
+
_complete_http_bridge_handoff(session, self._http_bridge_inflight_sessions)
|
|
2015
|
+
raise ProxyResponseError(
|
|
2016
|
+
502,
|
|
2017
|
+
openai_error(
|
|
2018
|
+
"upstream_unavailable",
|
|
2019
|
+
"HTTP responses session bridge reader did not shut down cleanly",
|
|
2020
|
+
),
|
|
2021
|
+
)
|
|
2022
|
+
deadline = _websocket_connect_deadline(
|
|
2023
|
+
request_state,
|
|
2024
|
+
_http_bridge_request_budget_seconds(_service_get_settings()),
|
|
2025
|
+
now=clock_for(self).monotonic(),
|
|
2026
|
+
)
|
|
2027
|
+
try:
|
|
2028
|
+
# Callers that reconnect while holding a lock resolve this row
|
|
2029
|
+
# before taking it and pass it in. The prewarm timeout recovery is
|
|
2030
|
+
# one: it reconnects under ``prewarm_lock``, and a refresh behind
|
|
2031
|
+
# this read runs a DB query under a process-global lock, so
|
|
2032
|
+
# awaiting it there would suspend the critical section (issues
|
|
2033
|
+
# #1971/#1972).
|
|
2034
|
+
settings = (
|
|
2035
|
+
dashboard_settings if dashboard_settings is not None else await _service_get_settings_cache().get()
|
|
2036
|
+
)
|
|
2037
|
+
session.api_key = request_state.api_key
|
|
2038
|
+
forced_refresh_account_id = request_state.force_refresh_account_id
|
|
2039
|
+
excluded_account_ids: set[str] = set(request_state.excluded_account_ids)
|
|
2040
|
+
requested_preferred_account_id = resolve_reconnect_preferred_account_id(
|
|
2041
|
+
request_state, session.account.id, require_preferred_account, account_neutral_recovery
|
|
2042
|
+
)
|
|
2043
|
+
required_preferred_account_id = resolve_required_account_id(
|
|
2044
|
+
("requested reconnect owner", requested_preferred_account_id),
|
|
2045
|
+
("account-neutral recovery", session.account.id if account_neutral_recovery else None),
|
|
2046
|
+
)
|
|
2047
|
+
close_skips_account = session.last_upstream_close_code in _UPSTREAM_CLOSE_CODES_SKIP_SAME_ACCOUNT_RETRY
|
|
2048
|
+
hard_close_account_bound = session.key.strength == "hard" and (close_skips_account or require_same_account)
|
|
2049
|
+
skip_same_account = (
|
|
2050
|
+
session.key.strength != "hard" and close_skips_account and required_preferred_account_id is None
|
|
2051
|
+
)
|
|
2052
|
+
if required_preferred_account_id is not None and required_preferred_account_id in excluded_account_ids:
|
|
2053
|
+
session.closed = True
|
|
2054
|
+
_complete_http_bridge_handoff(session, self._http_bridge_inflight_sessions)
|
|
2055
|
+
raise _http_bridge_previous_response_owner_unavailable_error()
|
|
2056
|
+
_require_http_bridge_bound_account_not_excluded(
|
|
2057
|
+
hard_close_account_bound, session.account.id, excluded_account_ids
|
|
2058
|
+
)
|
|
2059
|
+
except BaseException:
|
|
2060
|
+
session.closed = True
|
|
2061
|
+
_complete_http_bridge_handoff(session, self._http_bridge_inflight_sessions)
|
|
2062
|
+
raise
|
|
2063
|
+
if skip_same_account:
|
|
2064
|
+
excluded_account_ids.add(session.account.id)
|
|
2065
|
+
retry_same_account_once = not skip_same_account and session.account.id not in excluded_account_ids
|
|
2066
|
+
if skip_same_account:
|
|
2067
|
+
preferred_candidate_id: str | None = None
|
|
2068
|
+
elif hard_close_account_bound and session.account.id not in excluded_account_ids:
|
|
2069
|
+
preferred_candidate_id = session.account.id
|
|
2070
|
+
elif required_preferred_account_id is not None:
|
|
2071
|
+
preferred_candidate_id = required_preferred_account_id
|
|
2072
|
+
elif forced_refresh_account_id is not None:
|
|
2073
|
+
preferred_candidate_id = forced_refresh_account_id
|
|
2074
|
+
elif request_state.preferred_account_id is not None:
|
|
2075
|
+
preferred_candidate_id = request_state.preferred_account_id
|
|
2076
|
+
elif session.account.id not in excluded_account_ids:
|
|
2077
|
+
preferred_candidate_id = session.account.id
|
|
2078
|
+
else:
|
|
2079
|
+
preferred_candidate_id = None
|
|
2080
|
+
selected_account_lease: AccountLease | None = None
|
|
2081
|
+
selected_account_model_replacement = False
|
|
2082
|
+
file_owner = request_state.file_required_preferred_account
|
|
2083
|
+
|
|
2084
|
+
def record_selected_account_takeover(
|
|
2085
|
+
selected_account_id: str | None, preferred_account_id: str | None = session.account.id
|
|
2086
|
+
) -> None:
|
|
2087
|
+
_record_same_account_takeover(
|
|
2088
|
+
preferred_account_id=preferred_account_id,
|
|
2089
|
+
selected_account_id=selected_account_id,
|
|
2090
|
+
)
|
|
2091
|
+
|
|
2092
|
+
async def release_selected_account_lease() -> None:
|
|
2093
|
+
nonlocal selected_account_lease
|
|
2094
|
+
lease, selected_account_lease = selected_account_lease, None
|
|
2095
|
+
if lease is None:
|
|
2096
|
+
return
|
|
2097
|
+
async with session.pending_lock:
|
|
2098
|
+
if session.account_lease is not None and lease.lease_id == session.account_lease.lease_id:
|
|
2099
|
+
session.account_lease = None
|
|
2100
|
+
try:
|
|
2101
|
+
await self._load_balancer.release_account_lease(lease)
|
|
2102
|
+
except BaseException:
|
|
2103
|
+
complete_failed_handoff()
|
|
2104
|
+
raise
|
|
2105
|
+
|
|
2106
|
+
async def abandon_selected_account_retry(selected_account: Any) -> None:
|
|
2107
|
+
nonlocal preferred_candidate_id
|
|
2108
|
+
if hard_close_account_bound or selected_account_model_replacement:
|
|
2109
|
+
await release_selected_account_lease()
|
|
2110
|
+
complete_failed_handoff()
|
|
2111
|
+
raise
|
|
2112
|
+
excluded_account_ids.add(selected_account.id)
|
|
2113
|
+
preferred_candidate_id = None
|
|
2114
|
+
await release_selected_account_lease()
|
|
2115
|
+
|
|
2116
|
+
async def open_replacement_upstream(selected_account: Any, selected_headers: dict[str, str]) -> Any:
|
|
2117
|
+
try:
|
|
2118
|
+
return await self._open_upstream_websocket_with_budget(
|
|
2119
|
+
selected_account,
|
|
2120
|
+
selected_headers,
|
|
2121
|
+
timeout_seconds=self._remaining_budget_seconds(deadline),
|
|
2122
|
+
request_state=request_state,
|
|
2123
|
+
)
|
|
2124
|
+
except Exception:
|
|
2125
|
+
session.closed = True
|
|
2126
|
+
raise
|
|
2127
|
+
|
|
2128
|
+
def complete_failed_handoff() -> None:
|
|
2129
|
+
session.closed = True
|
|
2130
|
+
_mark_http_bridge_reader_handoff_reconnect_failed(session, old_reader)
|
|
2131
|
+
_complete_http_bridge_handoff(session, self._http_bridge_inflight_sessions)
|
|
2132
|
+
|
|
2133
|
+
def require_bound_account() -> None:
|
|
2134
|
+
try:
|
|
2135
|
+
_require_http_bridge_bound_account_not_excluded(
|
|
2136
|
+
hard_close_account_bound, session.account.id, excluded_account_ids
|
|
2137
|
+
)
|
|
2138
|
+
except BaseException:
|
|
2139
|
+
complete_failed_handoff()
|
|
2140
|
+
raise
|
|
2141
|
+
|
|
2142
|
+
while True:
|
|
2143
|
+
reuse_current_account_lease = preferred_candidate_id == session.account.id and bool(session.account_lease)
|
|
2144
|
+
try:
|
|
2145
|
+
selection = await self._select_account_with_budget_for_stream(
|
|
2146
|
+
deadline,
|
|
2147
|
+
request_id=request_state.request_log_id or request_state.request_id,
|
|
2148
|
+
kind="http_bridge",
|
|
2149
|
+
request_stage="reattach",
|
|
2150
|
+
api_key=session.api_key,
|
|
2151
|
+
affinity_policy=selection_affinity or session.affinity,
|
|
2152
|
+
prefer_earlier_reset_accounts=settings.prefer_earlier_reset_accounts,
|
|
2153
|
+
prefer_earlier_reset_window=_prefer_earlier_reset_window(settings),
|
|
2154
|
+
routing_strategy=_routing_strategy(settings),
|
|
2155
|
+
model=session.request_model,
|
|
2156
|
+
service_tier=session.request_service_tier,
|
|
2157
|
+
exclude_account_ids=excluded_account_ids,
|
|
2158
|
+
preferred_account_id=preferred_candidate_id,
|
|
2159
|
+
preferred_account_is_continuity_owner=account_neutral_recovery or file_owner,
|
|
2160
|
+
preferred_account_overrides_single_account_routing=file_owner,
|
|
2161
|
+
require_security_work_authorized=require_security_work_authorized,
|
|
2162
|
+
lease_kind=None if reuse_current_account_lease else "stream",
|
|
2163
|
+
estimated_lease_tokens=_estimated_lease_tokens_from_request_usage_budget(
|
|
2164
|
+
request_state.request_usage_budget
|
|
2165
|
+
),
|
|
2166
|
+
fallback_on_preferred_account_unavailable=(
|
|
2167
|
+
not reuse_current_account_lease
|
|
2168
|
+
and not hard_close_account_bound
|
|
2169
|
+
and required_preferred_account_id is None
|
|
2170
|
+
),
|
|
2171
|
+
)
|
|
2172
|
+
except BaseException:
|
|
2173
|
+
complete_failed_handoff()
|
|
2174
|
+
raise
|
|
2175
|
+
account = selection.account
|
|
2176
|
+
if account is None:
|
|
2177
|
+
try:
|
|
2178
|
+
await release_selected_account_lease()
|
|
2179
|
+
except BaseException:
|
|
2180
|
+
complete_failed_handoff()
|
|
2181
|
+
raise
|
|
2182
|
+
if required_preferred_account_id is not None and selection.continuity_owner_no_longer_exists:
|
|
2183
|
+
complete_failed_handoff()
|
|
2184
|
+
raise _http_bridge_previous_response_owner_unavailable_error()
|
|
2185
|
+
if (
|
|
2186
|
+
reuse_current_account_lease
|
|
2187
|
+
and not hard_close_account_bound
|
|
2188
|
+
and required_preferred_account_id is None
|
|
2189
|
+
and self._remaining_budget_seconds(deadline) > 0
|
|
2190
|
+
):
|
|
2191
|
+
preferred_candidate_id = None
|
|
2192
|
+
continue
|
|
2193
|
+
if selection.error_code == USAGE_LIMIT_REACHED and (
|
|
2194
|
+
required_preferred_account_id is not None or hard_close_account_bound
|
|
2195
|
+
):
|
|
2196
|
+
complete_failed_handoff()
|
|
2197
|
+
raise _http_bridge_previous_response_owner_unavailable_error()
|
|
2198
|
+
if selection.error_code == USAGE_LIMIT_REACHED:
|
|
2199
|
+
record_selected_account_takeover(None)
|
|
2200
|
+
status_code, error_payload = selection_failure_response(selection)
|
|
2201
|
+
complete_failed_handoff()
|
|
2202
|
+
raise ProxyResponseError(status_code, error_payload)
|
|
2203
|
+
try:
|
|
2204
|
+
should_retry_selection = await _sleep_for_account_selection_recovery(
|
|
2205
|
+
selection,
|
|
2206
|
+
request_id=request_state.request_log_id or request_state.request_id,
|
|
2207
|
+
kind="http_bridge",
|
|
2208
|
+
request_stage="reattach",
|
|
2209
|
+
model=session.request_model,
|
|
2210
|
+
max_sleep_seconds=self._remaining_budget_seconds(deadline),
|
|
2211
|
+
request_state=request_state,
|
|
2212
|
+
scheduler=scheduler_for(self),
|
|
2213
|
+
clock=clock_for(self),
|
|
2214
|
+
)
|
|
2215
|
+
except BaseException:
|
|
2216
|
+
complete_failed_handoff()
|
|
2217
|
+
raise
|
|
2218
|
+
if should_retry_selection:
|
|
2219
|
+
excluded_account_ids.update(request_state.excluded_account_ids)
|
|
2220
|
+
if required_preferred_account_id in excluded_account_ids:
|
|
2221
|
+
complete_failed_handoff()
|
|
2222
|
+
raise _http_bridge_previous_response_owner_unavailable_error()
|
|
2223
|
+
if skip_same_account:
|
|
2224
|
+
excluded_account_ids.add(session.account.id)
|
|
2225
|
+
require_bound_account()
|
|
2226
|
+
retry_same_account_once = not skip_same_account and session.account.id not in excluded_account_ids
|
|
2227
|
+
if skip_same_account:
|
|
2228
|
+
preferred_candidate_id = None
|
|
2229
|
+
elif hard_close_account_bound and session.account.id not in excluded_account_ids:
|
|
2230
|
+
preferred_candidate_id = session.account.id
|
|
2231
|
+
elif required_preferred_account_id is not None:
|
|
2232
|
+
preferred_candidate_id = required_preferred_account_id
|
|
2233
|
+
elif forced_refresh_account_id is not None:
|
|
2234
|
+
preferred_candidate_id = forced_refresh_account_id
|
|
2235
|
+
elif request_state.preferred_account_id is not None:
|
|
2236
|
+
preferred_candidate_id = request_state.preferred_account_id
|
|
2237
|
+
elif session.account.id not in excluded_account_ids:
|
|
2238
|
+
preferred_candidate_id = session.account.id
|
|
2239
|
+
else:
|
|
2240
|
+
preferred_candidate_id = None
|
|
2241
|
+
continue
|
|
2242
|
+
record_selected_account_takeover(None)
|
|
2243
|
+
complete_failed_handoff()
|
|
2244
|
+
raise _http_bridge_reconnect_selection_failure(selection, required_preferred_account_id)
|
|
2245
|
+
if required_preferred_account_id is not None and account.id != required_preferred_account_id:
|
|
2246
|
+
if selection.lease is not None:
|
|
2247
|
+
selected_account_lease = selection.lease
|
|
2248
|
+
await release_selected_account_lease()
|
|
2249
|
+
record_selected_account_takeover(account.id, required_preferred_account_id)
|
|
2250
|
+
complete_failed_handoff()
|
|
2251
|
+
raise _http_bridge_previous_response_owner_unavailable_error()
|
|
2252
|
+
selected_account_lease = (
|
|
2253
|
+
session.account_lease
|
|
2254
|
+
if reuse_current_account_lease and account.id == session.account.id
|
|
2255
|
+
else selection.lease
|
|
2256
|
+
)
|
|
2257
|
+
selected_account_model_replacement = (
|
|
2258
|
+
request_state.precreated_replay_reason == _ACCOUNT_MODEL_UNSUPPORTED_ERROR_CODE
|
|
2259
|
+
and account.id != request_state.precreated_replay_account_id
|
|
2260
|
+
)
|
|
2261
|
+
if selected_account_model_replacement:
|
|
2262
|
+
_clear_websocket_precreated_replay_fallback(request_state)
|
|
2263
|
+
selected_is_preferred = account.id == session.account.id
|
|
2264
|
+
force_refresh = forced_refresh_account_id == account.id
|
|
2265
|
+
if forced_refresh_account_id is not None and account.id != forced_refresh_account_id:
|
|
2266
|
+
request_state.force_refresh_account_id = None
|
|
2267
|
+
if request_state.preferred_account_id == forced_refresh_account_id:
|
|
2268
|
+
request_state.preferred_account_id = None
|
|
2269
|
+
try:
|
|
2270
|
+
account = await self._ensure_fresh_with_budget(
|
|
2271
|
+
account,
|
|
2272
|
+
force=force_refresh,
|
|
2273
|
+
timeout_seconds=self._remaining_budget_seconds(deadline),
|
|
2274
|
+
)
|
|
2275
|
+
if force_refresh and request_state.force_refresh_account_id == account.id:
|
|
2276
|
+
request_state.force_refresh_account_id = None
|
|
2277
|
+
connect_headers = _websocket_safe_headers_with_turn_state(
|
|
2278
|
+
session.headers, _http_bridge_reconnect_turn_state(session, account.id, owner_rebind_affinity)
|
|
2279
|
+
)
|
|
2280
|
+
upstream = await open_replacement_upstream(account, connect_headers)
|
|
2281
|
+
_copy_websocket_route_metadata_to_session(session, request_state)
|
|
2282
|
+
record_selected_account_takeover(account.id)
|
|
2283
|
+
break
|
|
2284
|
+
except ProxyResponseError as exc:
|
|
2285
|
+
if exc.status_code != 401 or self._remaining_budget_seconds(deadline) <= 0:
|
|
2286
|
+
await release_selected_account_lease()
|
|
2287
|
+
complete_failed_handoff()
|
|
2288
|
+
raise _http_bridge_reconnect_connect_failure(exc, required_preferred_account_id) from exc
|
|
2289
|
+
try:
|
|
2290
|
+
account = await self._ensure_fresh_with_budget(
|
|
2291
|
+
account,
|
|
2292
|
+
force=True,
|
|
2293
|
+
timeout_seconds=self._remaining_budget_seconds(deadline),
|
|
2294
|
+
)
|
|
2295
|
+
connect_headers = _websocket_safe_headers_with_turn_state(
|
|
2296
|
+
session.headers, _http_bridge_reconnect_turn_state(session, account.id, owner_rebind_affinity)
|
|
2297
|
+
)
|
|
2298
|
+
upstream = await open_replacement_upstream(account, connect_headers)
|
|
2299
|
+
_copy_websocket_route_metadata_to_session(session, request_state)
|
|
2300
|
+
record_selected_account_takeover(account.id)
|
|
2301
|
+
break
|
|
2302
|
+
except ProxyResponseError as retry_exc:
|
|
2303
|
+
if retry_exc.status_code != 401:
|
|
2304
|
+
await release_selected_account_lease()
|
|
2305
|
+
complete_failed_handoff()
|
|
2306
|
+
raise _http_bridge_reconnect_connect_failure(retry_exc, required_preferred_account_id)
|
|
2307
|
+
await self._handle_proxy_error(account, retry_exc)
|
|
2308
|
+
await abandon_selected_account_retry(account)
|
|
2309
|
+
continue
|
|
2310
|
+
except RefreshError as refresh_exc:
|
|
2311
|
+
if refresh_exc.is_permanent:
|
|
2312
|
+
await self._load_balancer.mark_permanent_failure(account, refresh_exc.code)
|
|
2313
|
+
await abandon_selected_account_retry(account)
|
|
2314
|
+
continue
|
|
2315
|
+
except RefreshError as exc:
|
|
2316
|
+
if exc.is_permanent:
|
|
2317
|
+
await self._load_balancer.mark_permanent_failure(account, exc.code)
|
|
2318
|
+
if selected_is_preferred and self._remaining_budget_seconds(deadline) > 0:
|
|
2319
|
+
if retry_same_account_once and not exc.is_permanent:
|
|
2320
|
+
retry_same_account_once = False
|
|
2321
|
+
await release_selected_account_lease()
|
|
2322
|
+
continue
|
|
2323
|
+
await abandon_selected_account_retry(account)
|
|
2324
|
+
continue
|
|
2325
|
+
await release_selected_account_lease()
|
|
2326
|
+
complete_failed_handoff()
|
|
2327
|
+
raise _http_bridge_reconnect_connect_failure(exc, required_preferred_account_id)
|
|
2328
|
+
except (aiohttp.ClientError, asyncio.TimeoutError) as transport_exc:
|
|
2329
|
+
if selected_is_preferred and self._remaining_budget_seconds(deadline) > 0:
|
|
2330
|
+
if retry_same_account_once:
|
|
2331
|
+
retry_same_account_once = False
|
|
2332
|
+
await release_selected_account_lease()
|
|
2333
|
+
continue
|
|
2334
|
+
await abandon_selected_account_retry(account)
|
|
2335
|
+
continue
|
|
2336
|
+
await release_selected_account_lease()
|
|
2337
|
+
complete_failed_handoff()
|
|
2338
|
+
raise _http_bridge_reconnect_connect_failure(transport_exc, required_preferred_account_id)
|
|
2339
|
+
except asyncio.CancelledError:
|
|
2340
|
+
session.closed = True
|
|
2341
|
+
await release_selected_account_lease()
|
|
2342
|
+
complete_failed_handoff()
|
|
2343
|
+
raise
|
|
2344
|
+
except BaseException:
|
|
2345
|
+
await release_selected_account_lease()
|
|
2346
|
+
complete_failed_handoff()
|
|
2347
|
+
raise
|
|
2348
|
+
|
|
2349
|
+
async def abort_selected_handoff() -> None:
|
|
2350
|
+
session.closed = True
|
|
2351
|
+
# ``shield(coroutine)`` would ``ensure_future`` an unowned task; the
|
|
2352
|
+
# scheduler spawns it so a simulation owns the outliving cleanup.
|
|
2353
|
+
scheduler = scheduler_for(self)
|
|
2354
|
+
try:
|
|
2355
|
+
await asyncio.shield(scheduler.create_task(upstream.close(), name="http-bridge-handoff-abort-close"))
|
|
2356
|
+
except BaseException:
|
|
2357
|
+
logger.debug("Failed to close HTTP bridge replacement websocket", exc_info=True)
|
|
2358
|
+
selected_lease = selected_account_lease
|
|
2359
|
+
old_lease = session.account_lease
|
|
2360
|
+
try:
|
|
2361
|
+
await asyncio.shield(
|
|
2362
|
+
scheduler.create_task(release_selected_account_lease(), name="http-bridge-handoff-abort-lease")
|
|
2363
|
+
)
|
|
2364
|
+
except BaseException:
|
|
2365
|
+
logger.debug("Failed to release HTTP bridge replacement lease", exc_info=True)
|
|
2366
|
+
if old_lease is not None and old_lease is not selected_lease:
|
|
2367
|
+
session.account_lease = None
|
|
2368
|
+
try:
|
|
2369
|
+
await asyncio.shield(
|
|
2370
|
+
scheduler.create_task(
|
|
2371
|
+
self._load_balancer.release_account_lease(old_lease),
|
|
2372
|
+
name="http-bridge-handoff-abort-old-lease",
|
|
2373
|
+
)
|
|
2374
|
+
)
|
|
2375
|
+
except BaseException:
|
|
2376
|
+
logger.debug("Failed to release HTTP bridge old account lease", exc_info=True)
|
|
2377
|
+
complete_failed_handoff()
|
|
2378
|
+
|
|
2379
|
+
try:
|
|
2380
|
+
if owner_rebind_affinity is not None:
|
|
2381
|
+
await self._claim_http_bridge_replacement_before_swap(
|
|
2382
|
+
session,
|
|
2383
|
+
account_id=account.id,
|
|
2384
|
+
upstream=upstream,
|
|
2385
|
+
release_selected_account_lease=release_selected_account_lease,
|
|
2386
|
+
owner_rebind_affinity=owner_rebind_affinity,
|
|
2387
|
+
)
|
|
2388
|
+
if owner_rebind_affinity is not None or account.id != session.account.id:
|
|
2389
|
+
await self._unregister_http_bridge_turn_states(session)
|
|
2390
|
+
await self._unregister_http_bridge_previous_response_ids(session)
|
|
2391
|
+
session.last_completed_response_id = None
|
|
2392
|
+
session.last_completed_response_account_id = None
|
|
2393
|
+
session.last_completed_input_count = 0
|
|
2394
|
+
session.last_completed_input_prefix_fingerprint = None
|
|
2395
|
+
session.last_pending_tool_calls.clear()
|
|
2396
|
+
session.affinity = _persistent_http_bridge_affinity(selection_affinity or session.affinity)
|
|
2397
|
+
session.codex_session = session.key.affinity_kind == "thread_header"
|
|
2398
|
+
session.upstream_turn_state = None
|
|
2399
|
+
session.downstream_turn_state = None
|
|
2400
|
+
session.headers = {
|
|
2401
|
+
key: value for key, value in session.headers.items() if key.lower() != "x-codex-turn-state"
|
|
2402
|
+
}
|
|
2403
|
+
await _persist_http_bridge_replacement_account(self, session, account.id)
|
|
2404
|
+
try:
|
|
2405
|
+
await old_upstream.close()
|
|
2406
|
+
except Exception:
|
|
2407
|
+
logger.debug("Failed to close HTTP bridge upstream websocket before reconnect", exc_info=True)
|
|
2408
|
+
session.closed = True
|
|
2409
|
+
if selected_account_lease is not session.account_lease:
|
|
2410
|
+
old_lease = session.account_lease
|
|
2411
|
+
if old_lease is not None:
|
|
2412
|
+
await self._load_balancer.release_account_lease(old_lease)
|
|
2413
|
+
session.account_lease = None
|
|
2414
|
+
session.account_lease = selected_account_lease
|
|
2415
|
+
session.replace_connection(account, connect_headers, upstream, _token_expiry(account, self._encryptor))
|
|
2416
|
+
session.catalog_omission_quota_admission = selection.catalog_omission_quota_admission
|
|
2417
|
+
session.upstream_control = _WebSocketUpstreamControl()
|
|
2418
|
+
session.closed = False
|
|
2419
|
+
session.last_upstream_close_code = None
|
|
2420
|
+
session.upstream_turn_state = _upstream_turn_state_from_socket(upstream) or session.upstream_turn_state
|
|
2421
|
+
_complete_http_bridge_handoff(session, self._http_bridge_inflight_sessions)
|
|
2422
|
+
except BaseException:
|
|
2423
|
+
await abort_selected_handoff()
|
|
2424
|
+
raise
|
|
2425
|
+
if restart_reader:
|
|
2426
|
+
session.upstream_reader = self._spawn_http_bridge_upstream_reader(session)
|
|
2427
|
+
_log_http_bridge_event(
|
|
2428
|
+
"reconnect",
|
|
2429
|
+
session.key,
|
|
2430
|
+
account_id=account.id,
|
|
2431
|
+
model=session.request_model,
|
|
2432
|
+
detail=f"selected_account_id={account.id}, durable_session_id={session.durable_session_id}",
|
|
2433
|
+
cache_key_family=session.key.affinity_kind,
|
|
2434
|
+
model_class=_extract_model_class(session.request_model) if session.request_model else None,
|
|
2435
|
+
)
|