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
app/main.py
ADDED
|
@@ -0,0 +1,1240 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import mimetypes
|
|
6
|
+
import os
|
|
7
|
+
import stat
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
from collections.abc import Awaitable
|
|
11
|
+
from contextlib import asynccontextmanager
|
|
12
|
+
from datetime import timedelta
|
|
13
|
+
from functools import lru_cache
|
|
14
|
+
from importlib import import_module
|
|
15
|
+
from ipaddress import ip_address
|
|
16
|
+
from pathlib import Path, PurePosixPath
|
|
17
|
+
from typing import Any, Protocol, cast
|
|
18
|
+
from urllib.parse import urlparse
|
|
19
|
+
|
|
20
|
+
import aiohttp
|
|
21
|
+
from fastapi import FastAPI, HTTPException
|
|
22
|
+
from fastapi.responses import FileResponse
|
|
23
|
+
from starlette.staticfiles import StaticFiles
|
|
24
|
+
|
|
25
|
+
from app.core.audit.service import drain_audit_log_tasks
|
|
26
|
+
from app.core.auth.dashboard_users_cache import get_dashboard_users_cache
|
|
27
|
+
from app.core.auth.guardian import build_auth_guardian_scheduler
|
|
28
|
+
from app.core.auth.providers.registry import get_auth_provider_registry
|
|
29
|
+
from app.core.balancer import configure_replica_salt
|
|
30
|
+
from app.core.bootstrap import ensure_auto_bootstrap_token, log_bootstrap_token
|
|
31
|
+
from app.core.clients.http import close_http_client, init_http_client
|
|
32
|
+
from app.core.clients.native_egress import close_discovered_native_egress_client
|
|
33
|
+
from app.core.config.dashboard_overrides import effective_settings
|
|
34
|
+
from app.core.config.key_fingerprint import verify_encryption_key_fingerprint
|
|
35
|
+
from app.core.config.settings import (
|
|
36
|
+
Settings,
|
|
37
|
+
_bridge_advertise_hostname_is_replica_specific,
|
|
38
|
+
_parse_port_value,
|
|
39
|
+
get_settings,
|
|
40
|
+
warn_removed_settings,
|
|
41
|
+
)
|
|
42
|
+
from app.core.config.settings_cache import get_settings_cache
|
|
43
|
+
from app.core.config.spool_retention import (
|
|
44
|
+
resolve_operation_spool_retention_seconds,
|
|
45
|
+
warn_spool_retention_below_floor,
|
|
46
|
+
)
|
|
47
|
+
from app.core.handlers import add_exception_handlers
|
|
48
|
+
from app.core.metrics.middleware import MetricsMiddleware
|
|
49
|
+
from app.core.metrics.prometheus import MULTIPROCESS_MODE, PROMETHEUS_AVAILABLE, make_scrape_registry, mark_process_dead
|
|
50
|
+
from app.core.middleware import (
|
|
51
|
+
add_api_firewall_middleware,
|
|
52
|
+
add_app_version_middleware,
|
|
53
|
+
add_backend_api_codex_v1_alias_middleware,
|
|
54
|
+
add_dashboard_auth_proxy_middleware,
|
|
55
|
+
add_dashboard_csrf_middleware,
|
|
56
|
+
add_multipart_content_encoding_middleware,
|
|
57
|
+
add_request_body_limit_middleware,
|
|
58
|
+
add_request_decompression_middleware,
|
|
59
|
+
add_request_id_middleware,
|
|
60
|
+
add_required_capability_http_middleware,
|
|
61
|
+
add_trusted_proxy_headers_middleware,
|
|
62
|
+
)
|
|
63
|
+
from app.core.middleware.dashboard_gzip import add_dashboard_gzip_middleware
|
|
64
|
+
from app.core.middleware.dashboard_overrides import DashboardOverridesMiddleware
|
|
65
|
+
from app.core.middleware.inflight import InFlightMiddleware
|
|
66
|
+
from app.core.openai.model_refresh_scheduler import build_model_refresh_scheduler
|
|
67
|
+
from app.core.resilience.backpressure import BackpressureMiddleware
|
|
68
|
+
from app.core.resilience.bulkhead import BulkheadMiddleware, get_bulkhead
|
|
69
|
+
from app.core.resilience.loop_lag_monitor import run_event_loop_lag_monitor
|
|
70
|
+
from app.core.resilience.memory_monitor import configure as configure_memory_monitor
|
|
71
|
+
from app.core.retention.scheduler import build_data_retention_scheduler
|
|
72
|
+
from app.core.runtime_logging import install_redacting_loop_exception_handler
|
|
73
|
+
from app.core.scheduling.leader_election import get_leader_election
|
|
74
|
+
from app.core.shutdown import close_control_plane_task_admission
|
|
75
|
+
from app.core.timeout_invariants import validate_runtime_timeout_invariants, validate_timeout_invariants
|
|
76
|
+
from app.core.usage.metadata_scheduler import build_metadata_refresh_scheduler
|
|
77
|
+
from app.core.usage.refresh_scheduler import build_usage_refresh_scheduler
|
|
78
|
+
from app.core.usage.reset_credits_refresh_scheduler import build_rate_limit_reset_credits_scheduler
|
|
79
|
+
from app.core.utils.time import utcnow
|
|
80
|
+
from app.db.session import (
|
|
81
|
+
SessionLocal,
|
|
82
|
+
close_db,
|
|
83
|
+
close_session,
|
|
84
|
+
init_background_db,
|
|
85
|
+
init_db,
|
|
86
|
+
mark_sqlite_shutdown_clean,
|
|
87
|
+
)
|
|
88
|
+
from app.modules.accounts import api as accounts_api
|
|
89
|
+
from app.modules.accounts.deletion import build_account_deletion_scheduler
|
|
90
|
+
from app.modules.accounts.repository import AccountsRepository
|
|
91
|
+
from app.modules.accounts.usage_rollup_scheduler import build_account_usage_rollup_scheduler
|
|
92
|
+
from app.modules.api_keys import api as api_keys_api
|
|
93
|
+
from app.modules.api_keys.last_used_coalescer import build_api_key_last_used_flush_scheduler
|
|
94
|
+
from app.modules.api_keys.reset_scheduler import build_api_key_limit_reset_scheduler
|
|
95
|
+
from app.modules.audit import api as audit_api
|
|
96
|
+
from app.modules.auth_providers import api as auth_providers_api
|
|
97
|
+
from app.modules.automations import api as automations_api
|
|
98
|
+
from app.modules.automations.scheduler import build_automations_scheduler
|
|
99
|
+
from app.modules.cache_isolation_probe import api as cache_isolation_probe_api
|
|
100
|
+
from app.modules.conversation_archive import api as conversation_archive_api
|
|
101
|
+
from app.modules.dashboard import api as dashboard_api
|
|
102
|
+
from app.modules.dashboard_auth import api as dashboard_auth_api
|
|
103
|
+
from app.modules.dashboard_roles import api as dashboard_roles_api
|
|
104
|
+
from app.modules.dashboard_users import api as dashboard_users_api
|
|
105
|
+
from app.modules.dashboard_users.identity_resolver import get_identity_resolution_cache
|
|
106
|
+
from app.modules.firewall import api as firewall_api
|
|
107
|
+
from app.modules.fleet import api as fleet_api
|
|
108
|
+
from app.modules.health import api as health_api
|
|
109
|
+
from app.modules.model_sources import api as model_sources_api
|
|
110
|
+
from app.modules.oauth import api as oauth_api
|
|
111
|
+
from app.modules.proxy import api as proxy_api
|
|
112
|
+
from app.modules.proxy.cap_partitioning import refresh_cap_partition
|
|
113
|
+
from app.modules.proxy.durable_bridge_coordinator import DurableBridgeSessionCoordinator
|
|
114
|
+
from app.modules.proxy.durable_bridge_repository import (
|
|
115
|
+
DURABLE_BRIDGE_OPERATION_SPOOL_PURGE_BATCH_SIZE,
|
|
116
|
+
missing_durable_bridge_tables,
|
|
117
|
+
)
|
|
118
|
+
from app.modules.proxy.durable_bridge_runtime import http_bridge_owner_process_epoch
|
|
119
|
+
from app.modules.proxy.rate_limit_cache import get_rate_limit_headers_cache
|
|
120
|
+
from app.modules.proxy.ring_membership import (
|
|
121
|
+
RING_HEARTBEAT_INTERVAL_SECONDS,
|
|
122
|
+
RING_STALE_GRACE_SECONDS,
|
|
123
|
+
RING_STALE_THRESHOLD_SECONDS,
|
|
124
|
+
RingMembershipService,
|
|
125
|
+
)
|
|
126
|
+
from app.modules.quota_planner import api as quota_planner_api
|
|
127
|
+
from app.modules.quota_planner.scheduler import build_quota_planner_scheduler
|
|
128
|
+
from app.modules.rate_limit_reset_credits import api as rate_limit_reset_credits_api
|
|
129
|
+
from app.modules.reports import api as reports_api
|
|
130
|
+
from app.modules.reports.cache import ReportsCaches
|
|
131
|
+
from app.modules.request_logs import api as request_logs_api
|
|
132
|
+
from app.modules.role_mappings import api as role_mappings_api
|
|
133
|
+
from app.modules.runtime import api as runtime_api
|
|
134
|
+
from app.modules.scim import api as scim_api
|
|
135
|
+
from app.modules.scim import management_api as scim_tokens_api
|
|
136
|
+
from app.modules.settings import api as settings_api
|
|
137
|
+
from app.modules.settings.service import warn_environment_shadowed_by_dashboard
|
|
138
|
+
from app.modules.sticky_sessions import api as sticky_sessions_api
|
|
139
|
+
from app.modules.sticky_sessions.cleanup_scheduler import (
|
|
140
|
+
OperationRetentionCleanupResult,
|
|
141
|
+
_abandoned_bridge_retention_seconds,
|
|
142
|
+
_record_operation_retention_cleanup,
|
|
143
|
+
build_sticky_session_cleanup_scheduler,
|
|
144
|
+
operation_retention_metrics_enabled,
|
|
145
|
+
)
|
|
146
|
+
from app.modules.telemetry import api as telemetry_api
|
|
147
|
+
from app.modules.telemetry.scheduler import build_telemetry_scheduler
|
|
148
|
+
from app.modules.usage import api as usage_api
|
|
149
|
+
from app.modules.usage.additional_quota_keys import reload_additional_quota_registry
|
|
150
|
+
from app.modules.usage.live_ingest import start_live_usage_ingestor, stop_live_usage_ingestor
|
|
151
|
+
|
|
152
|
+
logger = logging.getLogger(__name__)
|
|
153
|
+
|
|
154
|
+
# On Windows, ``mimetypes`` merges HKCR registry mappings where third-party
|
|
155
|
+
# software commonly remaps web extensions (``.js`` -> ``text/plain``), and
|
|
156
|
+
# browsers enforce strict MIME checking for ES module scripts, so a poisoned
|
|
157
|
+
# mapping renders the dashboard as a blank page (issue #1698). ``FileResponse``
|
|
158
|
+
# resolves ``media_type`` through ``mimetypes.guess_type``, so pin every
|
|
159
|
+
# extension the built dashboard serves; ``add_type`` wins over the merged
|
|
160
|
+
# registry table on all platforms.
|
|
161
|
+
_WEB_ASSET_MIME_TYPES: dict[str, str] = {
|
|
162
|
+
".js": "text/javascript",
|
|
163
|
+
".mjs": "text/javascript",
|
|
164
|
+
".css": "text/css",
|
|
165
|
+
".svg": "image/svg+xml",
|
|
166
|
+
".json": "application/json",
|
|
167
|
+
".woff2": "font/woff2",
|
|
168
|
+
".woff": "font/woff",
|
|
169
|
+
".html": "text/html",
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _ensure_web_asset_mime_types() -> None:
|
|
174
|
+
for extension, mime_type in _WEB_ASSET_MIME_TYPES.items():
|
|
175
|
+
mimetypes.add_type(mime_type, extension)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
_ensure_web_asset_mime_types()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
async def run_http_bridge_heartbeat_maintenance(proxy_service: Any) -> None:
|
|
182
|
+
"""Per-replica bridge upkeep driven by the ring heartbeat.
|
|
183
|
+
|
|
184
|
+
All passes are request-independent by design: durable ownership must be
|
|
185
|
+
reconciled even on a replica nothing is routing to, and the idle sweep is
|
|
186
|
+
otherwise only reached from ``_get_or_create_http_bridge_session``, so a
|
|
187
|
+
replica that stops taking bridge requests would keep its idle sessions'
|
|
188
|
+
upstream WebSockets open until restart (issue #1354). Each pass is isolated
|
|
189
|
+
so one failing cannot skip the others or stop the heartbeat.
|
|
190
|
+
"""
|
|
191
|
+
if proxy_service is None:
|
|
192
|
+
return
|
|
193
|
+
for attribute, failure_message in (
|
|
194
|
+
("reconcile_durable_http_bridge_ownership", "HTTP bridge durable ownership reconciliation failed"),
|
|
195
|
+
("abandon_stale_http_bridge_operations", "HTTP bridge stale operation abandonment failed"),
|
|
196
|
+
("prune_idle_http_bridge_sessions", "HTTP bridge idle sweep failed"),
|
|
197
|
+
):
|
|
198
|
+
pass_callable = getattr(proxy_service, attribute, None)
|
|
199
|
+
if pass_callable is None:
|
|
200
|
+
continue
|
|
201
|
+
try:
|
|
202
|
+
await pass_callable()
|
|
203
|
+
except Exception:
|
|
204
|
+
logger.warning(failure_message, exc_info=True)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _log_abandoned_lease_release(task: asyncio.Task[bool]) -> None:
|
|
208
|
+
if task.cancelled():
|
|
209
|
+
return
|
|
210
|
+
exc = task.exception()
|
|
211
|
+
if exc is not None:
|
|
212
|
+
logger.warning("Abandoned scheduler leader lease release finished with error", exc_info=exc)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
async def _release_leader_lease_within(timeout: float) -> bool:
|
|
216
|
+
"""Release the scheduler leader lease without ever pinning shutdown.
|
|
217
|
+
|
|
218
|
+
``release()`` uses a background DB session whose rollback/close shield and
|
|
219
|
+
await their own cleanup, so wrapping it in ``asyncio.wait_for`` would only
|
|
220
|
+
cancel the awaiting wrapper while a wedged database call keeps unwinding —
|
|
221
|
+
shutdown could still hang past the deadline. Run the release as a task and,
|
|
222
|
+
if it does not finish within ``timeout``, abandon it (logging its eventual
|
|
223
|
+
outcome from a done callback) so shutdown always proceeds within the
|
|
224
|
+
deadline; the lease then expires after its TTL, which is acceptable.
|
|
225
|
+
"""
|
|
226
|
+
release_task: asyncio.Task[bool] = asyncio.ensure_future(get_leader_election().release())
|
|
227
|
+
done, _ = await asyncio.wait({release_task}, timeout=timeout)
|
|
228
|
+
if release_task not in done:
|
|
229
|
+
logger.warning(
|
|
230
|
+
"Scheduler leader lease release did not finish within %.1fs; abandoning it so "
|
|
231
|
+
"shutdown can proceed (the lease will expire after its TTL)",
|
|
232
|
+
timeout,
|
|
233
|
+
)
|
|
234
|
+
release_task.add_done_callback(_log_abandoned_lease_release)
|
|
235
|
+
return False
|
|
236
|
+
if release_task.cancelled():
|
|
237
|
+
logger.warning("Scheduler leader lease release was cancelled during shutdown")
|
|
238
|
+
return False
|
|
239
|
+
exc = release_task.exception()
|
|
240
|
+
if exc is not None:
|
|
241
|
+
logger.warning("Failed to release scheduler leader lease during shutdown", exc_info=exc)
|
|
242
|
+
return False
|
|
243
|
+
if release_task.result() is False:
|
|
244
|
+
logger.warning("Scheduler leader lease release did not complete; suppressing the SQLite clean marker")
|
|
245
|
+
return False
|
|
246
|
+
return True
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
async def _drain_proxy_persistence_tasks(
|
|
250
|
+
proxy_service: Any,
|
|
251
|
+
timeout_seconds: float,
|
|
252
|
+
*,
|
|
253
|
+
task_name_prefixes: tuple[str, ...] | None = None,
|
|
254
|
+
failure_message: str,
|
|
255
|
+
) -> bool:
|
|
256
|
+
"""Drain proxy persistence work within the caller's committed deadline."""
|
|
257
|
+
if proxy_service is None or not hasattr(proxy_service, "drain_persistence_tasks"):
|
|
258
|
+
return True
|
|
259
|
+
try:
|
|
260
|
+
kwargs: dict[str, Any] = {"timeout_seconds": timeout_seconds}
|
|
261
|
+
if task_name_prefixes is not None:
|
|
262
|
+
kwargs["task_name_prefixes"] = task_name_prefixes
|
|
263
|
+
return bool(await proxy_service.drain_persistence_tasks(**kwargs))
|
|
264
|
+
except Exception:
|
|
265
|
+
logger.warning(failure_message, exc_info=True)
|
|
266
|
+
return False
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
async def _close_proxy_http_bridge_sessions_for_shutdown(
|
|
270
|
+
proxy_service: Any,
|
|
271
|
+
*,
|
|
272
|
+
mark_draining: bool,
|
|
273
|
+
) -> bool:
|
|
274
|
+
"""Close bridge resources and report whether that database-owning step completed.
|
|
275
|
+
|
|
276
|
+
Bridge teardown owns durable leases and can enqueue persistence work. A
|
|
277
|
+
failed mark/close must therefore suppress the SQLite ``clean`` marker even
|
|
278
|
+
when the later persistence drain and engine disposal happen to complete.
|
|
279
|
+
``None`` remains a successful result for older/test service doubles; the
|
|
280
|
+
concrete service returns ``True`` or ``False`` explicitly.
|
|
281
|
+
"""
|
|
282
|
+
if proxy_service is None:
|
|
283
|
+
return True
|
|
284
|
+
|
|
285
|
+
bridge_sessions_drained = True
|
|
286
|
+
if mark_draining and hasattr(proxy_service, "mark_http_bridge_draining"):
|
|
287
|
+
try:
|
|
288
|
+
result = await proxy_service.mark_http_bridge_draining()
|
|
289
|
+
if result is False:
|
|
290
|
+
bridge_sessions_drained = False
|
|
291
|
+
except Exception:
|
|
292
|
+
logger.warning("Failed to mark HTTP bridge durable sessions draining during shutdown", exc_info=True)
|
|
293
|
+
bridge_sessions_drained = False
|
|
294
|
+
|
|
295
|
+
if hasattr(proxy_service, "close_all_http_bridge_sessions"):
|
|
296
|
+
try:
|
|
297
|
+
result = await proxy_service.close_all_http_bridge_sessions()
|
|
298
|
+
if result is False:
|
|
299
|
+
bridge_sessions_drained = False
|
|
300
|
+
except Exception:
|
|
301
|
+
logger.warning("Failed to close HTTP bridge sessions during shutdown", exc_info=True)
|
|
302
|
+
bridge_sessions_drained = False
|
|
303
|
+
|
|
304
|
+
return bridge_sessions_drained
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
async def _drain_detached_control_plane_tasks(timeout_seconds: float) -> bool:
|
|
308
|
+
# Closing admission is synchronous with producer checks on the event loop,
|
|
309
|
+
# so no task can appear after the stable drain passes complete.
|
|
310
|
+
close_control_plane_task_admission()
|
|
311
|
+
loop = asyncio.get_running_loop()
|
|
312
|
+
deadline = loop.time() + timeout_seconds
|
|
313
|
+
clean_passes = 0
|
|
314
|
+
|
|
315
|
+
while clean_passes < 2:
|
|
316
|
+
remaining = max(0.0, deadline - loop.time())
|
|
317
|
+
results = await asyncio.gather(
|
|
318
|
+
drain_audit_log_tasks(remaining),
|
|
319
|
+
fleet_api.drain_background_refresh_tasks(remaining),
|
|
320
|
+
return_exceptions=True,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
clean_pass = True
|
|
324
|
+
for task_kind, result in zip(("audit log", "fleet refresh"), results, strict=True):
|
|
325
|
+
if isinstance(result, BaseException):
|
|
326
|
+
clean_pass = False
|
|
327
|
+
logger.warning(
|
|
328
|
+
"Failed to drain %s tasks during shutdown",
|
|
329
|
+
task_kind,
|
|
330
|
+
exc_info=(type(result), result, result.__traceback__),
|
|
331
|
+
)
|
|
332
|
+
elif result is False:
|
|
333
|
+
clean_pass = False
|
|
334
|
+
|
|
335
|
+
if not clean_pass:
|
|
336
|
+
return False
|
|
337
|
+
|
|
338
|
+
clean_passes += 1
|
|
339
|
+
|
|
340
|
+
# A done callback may enqueue sibling audit/fleet work after both
|
|
341
|
+
# registries looked empty. One extra stable pass catches that before
|
|
342
|
+
# HTTP clients and DB engines are torn down.
|
|
343
|
+
if clean_passes < 2:
|
|
344
|
+
await asyncio.sleep(0)
|
|
345
|
+
return True
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
class _MetricsServer(Protocol):
|
|
349
|
+
should_exit: bool
|
|
350
|
+
|
|
351
|
+
async def serve(self) -> None: ...
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
class _RingMembershipReader(Protocol):
|
|
355
|
+
def list_active(
|
|
356
|
+
self,
|
|
357
|
+
stale_threshold_seconds: int = RING_STALE_THRESHOLD_SECONDS,
|
|
358
|
+
*,
|
|
359
|
+
require_endpoint: bool = False,
|
|
360
|
+
) -> Awaitable[list[str]]: ...
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
@lru_cache(maxsize=4)
|
|
364
|
+
def _static_files_for_root(static_root: Path) -> StaticFiles:
|
|
365
|
+
return StaticFiles(directory=static_root, check_dir=False)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _resolve_static_asset_path(static_root: Path, requested_path: str) -> Path | None:
|
|
369
|
+
"""Return a filesystem path for a SPA asset only when it stays under static_root."""
|
|
370
|
+
normalized = PurePosixPath(requested_path)
|
|
371
|
+
if normalized.is_absolute() or ".." in normalized.parts:
|
|
372
|
+
return None
|
|
373
|
+
full_path, stat_result = _static_files_for_root(static_root).lookup_path(normalized.as_posix())
|
|
374
|
+
if stat_result is None or not stat.S_ISREG(stat_result.st_mode):
|
|
375
|
+
return None
|
|
376
|
+
return Path(full_path)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _is_metrics_bind_conflict(exc: BaseException) -> bool:
|
|
380
|
+
if isinstance(exc, SystemExit):
|
|
381
|
+
return exc.code == 1
|
|
382
|
+
if isinstance(exc, OSError):
|
|
383
|
+
import errno as _errno
|
|
384
|
+
|
|
385
|
+
return exc.errno in (_errno.EADDRINUSE, _errno.EADDRNOTAVAIL)
|
|
386
|
+
return False
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _is_benign_metrics_bind_failure(exc: BaseException) -> bool:
|
|
390
|
+
return MULTIPROCESS_MODE and _is_metrics_bind_conflict(exc)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _log_non_multiproc_metrics_bind_conflict(port: int) -> None:
|
|
394
|
+
logger.error(
|
|
395
|
+
"Metrics port %d is already bound by another worker process but PROMETHEUS_MULTIPROC_DIR is not set: "
|
|
396
|
+
"/metrics reflects only the winning worker's counters (1/N of traffic). "
|
|
397
|
+
"Set PROMETHEUS_MULTIPROC_DIR to a writable directory shared by all workers to aggregate metrics "
|
|
398
|
+
"across worker processes.",
|
|
399
|
+
port,
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
async def _close_db_and_record_clean_shutdown(
|
|
404
|
+
*,
|
|
405
|
+
database_tasks_drained: bool,
|
|
406
|
+
leader_lease_release_completed: bool,
|
|
407
|
+
) -> None:
|
|
408
|
+
"""Dispose the database engines, then record the shutdown as clean.
|
|
409
|
+
|
|
410
|
+
The record is only reached once disposal returns and every database-owning
|
|
411
|
+
shutdown drain completed. A cancellation, failed dispose, or abandoned
|
|
412
|
+
drain must leave the run state unclean, because that is exactly the
|
|
413
|
+
incomplete shutdown the next startup's integrity scan is for.
|
|
414
|
+
"""
|
|
415
|
+
sqlite_teardown_drained = await close_db()
|
|
416
|
+
if sqlite_teardown_drained and database_tasks_drained and leader_lease_release_completed:
|
|
417
|
+
mark_sqlite_shutdown_clean()
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
async def _purge_operation_spool_on_startup(*, retention_seconds: float) -> int:
|
|
421
|
+
"""Run one bounded transcript purge and record a sanitized aggregate result."""
|
|
422
|
+
|
|
423
|
+
started_at = time.monotonic()
|
|
424
|
+
try:
|
|
425
|
+
operation_purge_result = await DurableBridgeSessionCoordinator(SessionLocal).purge_operation_spool_batch(
|
|
426
|
+
cutoff=utcnow() - timedelta(seconds=retention_seconds),
|
|
427
|
+
)
|
|
428
|
+
except Exception as exc:
|
|
429
|
+
result = OperationRetentionCleanupResult(
|
|
430
|
+
deleted_operations=0,
|
|
431
|
+
batches=0,
|
|
432
|
+
backlog_likely=True,
|
|
433
|
+
outcome="failed",
|
|
434
|
+
duration_seconds=max(time.monotonic() - started_at, 0.0),
|
|
435
|
+
)
|
|
436
|
+
_record_operation_retention_cleanup(result)
|
|
437
|
+
logger.warning(
|
|
438
|
+
"HTTP bridge operation transcript startup retention failed "
|
|
439
|
+
"deleted_operations=0 batches=0 outcome=failed backlog_likely=true "
|
|
440
|
+
"duration_seconds=%.3f error_type=%s",
|
|
441
|
+
result.duration_seconds,
|
|
442
|
+
type(exc).__name__,
|
|
443
|
+
)
|
|
444
|
+
raise RuntimeError("HTTP bridge operation transcript startup retention failed") from None
|
|
445
|
+
|
|
446
|
+
backlog_likely = operation_purge_result.selected_operations >= DURABLE_BRIDGE_OPERATION_SPOOL_PURGE_BATCH_SIZE
|
|
447
|
+
result = OperationRetentionCleanupResult(
|
|
448
|
+
deleted_operations=operation_purge_result.deleted_operations,
|
|
449
|
+
batches=1,
|
|
450
|
+
backlog_likely=backlog_likely,
|
|
451
|
+
outcome="batch_budget_exhausted" if backlog_likely else "completed",
|
|
452
|
+
duration_seconds=max(time.monotonic() - started_at, 0.0),
|
|
453
|
+
)
|
|
454
|
+
_record_operation_retention_cleanup(result)
|
|
455
|
+
if not operation_retention_metrics_enabled():
|
|
456
|
+
logger.info(
|
|
457
|
+
"HTTP bridge operation transcript startup retention "
|
|
458
|
+
"deleted_operations=%s batches=%s outcome=%s "
|
|
459
|
+
"backlog_likely=%s duration_seconds=%.3f",
|
|
460
|
+
result.deleted_operations,
|
|
461
|
+
result.batches,
|
|
462
|
+
result.outcome,
|
|
463
|
+
result.backlog_likely,
|
|
464
|
+
result.duration_seconds,
|
|
465
|
+
)
|
|
466
|
+
return operation_purge_result.deleted_operations
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
async def _report_dashboard_timeout_overrides(settings: Settings) -> None:
|
|
470
|
+
"""Best-effort startup report on the dashboard-managed timeouts (C2-1).
|
|
471
|
+
|
|
472
|
+
Names env aliases the dashboard shadows and logs (never raises) invariant
|
|
473
|
+
violations of the effective values — a stored dashboard value can break a
|
|
474
|
+
combination the environment alone satisfies. A snapshot read failure must
|
|
475
|
+
not abort boot: the environment fallback stays in force until the cache
|
|
476
|
+
recovers.
|
|
477
|
+
"""
|
|
478
|
+
try:
|
|
479
|
+
dashboard_settings_row = await get_settings_cache().get()
|
|
480
|
+
except Exception: # noqa: BLE001 - startup must not depend on the snapshot
|
|
481
|
+
logger.debug("dashboard settings snapshot unavailable at startup; environment timeouts apply", exc_info=True)
|
|
482
|
+
return
|
|
483
|
+
warn_environment_shadowed_by_dashboard(dashboard_settings_row, settings)
|
|
484
|
+
validate_timeout_invariants(effective_settings(dashboard_settings_row, settings), strict=False, log=True)
|
|
485
|
+
# R2 spool retention: same warn-only shape. The environment alias alone can
|
|
486
|
+
# sit below the replay floor with the dashboard column NULL, a state the
|
|
487
|
+
# settings API never validated, so say so before an edit is refused.
|
|
488
|
+
warn_spool_retention_below_floor(dashboard_settings_row, settings)
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
@asynccontextmanager
|
|
492
|
+
async def lifespan(app: FastAPI):
|
|
493
|
+
import app.core.startup as startup_module
|
|
494
|
+
|
|
495
|
+
app.state.reports_caches = ReportsCaches()
|
|
496
|
+
shutdown_state = import_module("app.core.shutdown")
|
|
497
|
+
# First app code on uvicorn's loop: mask credential-bearing object reprs
|
|
498
|
+
# (aiohttp ConnectionKey proxy URLs, BasicAuth) before the default handler
|
|
499
|
+
# renders them into the 'asyncio' logger.
|
|
500
|
+
install_redacting_loop_exception_handler(asyncio.get_running_loop())
|
|
501
|
+
metrics_server = None
|
|
502
|
+
metrics_server_task: asyncio.Task[None] | None = None
|
|
503
|
+
ring_service = None
|
|
504
|
+
heartbeat_task: asyncio.Task[None] | None = None
|
|
505
|
+
instance_id = None
|
|
506
|
+
|
|
507
|
+
shutdown_state.prepare_lifespan_start()
|
|
508
|
+
startup_module._startup_complete = False
|
|
509
|
+
startup_module.reset_bridge_registration()
|
|
510
|
+
await get_settings_cache().invalidate(propagate=False)
|
|
511
|
+
await get_dashboard_users_cache().invalidate(propagate=False)
|
|
512
|
+
await get_rate_limit_headers_cache().invalidate()
|
|
513
|
+
reload_additional_quota_registry()
|
|
514
|
+
settings = get_settings()
|
|
515
|
+
warn_removed_settings()
|
|
516
|
+
validate_runtime_timeout_invariants(settings)
|
|
517
|
+
# Anchor round-robin tie-break decorrelation to this replica's stable bridge
|
|
518
|
+
# instance identity so peer replicas spread exact ties across equally-good
|
|
519
|
+
# accounts instead of all herding onto the lexicographically-first account.
|
|
520
|
+
configure_replica_salt(settings.http_responses_session_bridge_instance_id)
|
|
521
|
+
bridge_endpoint_base_url = settings.http_responses_session_bridge_advertise_base_url
|
|
522
|
+
await init_db()
|
|
523
|
+
init_background_db()
|
|
524
|
+
await verify_encryption_key_fingerprint()
|
|
525
|
+
_auto_bootstrap_token = await ensure_auto_bootstrap_token()
|
|
526
|
+
if _auto_bootstrap_token:
|
|
527
|
+
log_bootstrap_token(logger, _auto_bootstrap_token)
|
|
528
|
+
await init_http_client()
|
|
529
|
+
await _report_dashboard_timeout_overrides(settings)
|
|
530
|
+
bridge_durable_schema_ready = await _ensure_bridge_durable_schema_ready(settings)
|
|
531
|
+
if bridge_durable_schema_ready is True:
|
|
532
|
+
startup_module.mark_bridge_durable_schema_ready()
|
|
533
|
+
dashboard_settings = await get_settings_cache().get()
|
|
534
|
+
ownerless_cutoff = utcnow() - timedelta(seconds=_abandoned_bridge_retention_seconds(dashboard_settings))
|
|
535
|
+
deleted_bridge_rows = await DurableBridgeSessionCoordinator(SessionLocal).purge_owned_sessions_on_startup(
|
|
536
|
+
instance_id=settings.http_responses_session_bridge_instance_id,
|
|
537
|
+
owner_process_epoch=http_bridge_owner_process_epoch(),
|
|
538
|
+
ownerless_cutoff=ownerless_cutoff,
|
|
539
|
+
)
|
|
540
|
+
if deleted_bridge_rows > 0:
|
|
541
|
+
logger.info(
|
|
542
|
+
"Purged durable HTTP bridge rows from previous process instance",
|
|
543
|
+
extra={
|
|
544
|
+
"instance_id": settings.http_responses_session_bridge_instance_id,
|
|
545
|
+
"deleted": deleted_bridge_rows,
|
|
546
|
+
},
|
|
547
|
+
)
|
|
548
|
+
purged_operation_rows = await _purge_operation_spool_on_startup(
|
|
549
|
+
# R2 spool retention: the dashboard column wins over the deprecated
|
|
550
|
+
# env alias; ``dashboard_settings`` is the snapshot this startup
|
|
551
|
+
# step already loaded above.
|
|
552
|
+
retention_seconds=resolve_operation_spool_retention_seconds(dashboard_settings, startup_settings=settings),
|
|
553
|
+
)
|
|
554
|
+
if purged_operation_rows > 0:
|
|
555
|
+
logger.info(
|
|
556
|
+
"Purged expired durable HTTP bridge operation transcript rows",
|
|
557
|
+
extra={"deleted": purged_operation_rows},
|
|
558
|
+
)
|
|
559
|
+
from app.core.auth.api_key_cache import get_api_key_cache
|
|
560
|
+
from app.core.cache.invalidation import (
|
|
561
|
+
NAMESPACE_ACCOUNT_ROUTING,
|
|
562
|
+
NAMESPACE_ACCOUNT_SELECTION,
|
|
563
|
+
NAMESPACE_API_KEY,
|
|
564
|
+
NAMESPACE_DASHBOARD_USERS,
|
|
565
|
+
NAMESPACE_FIREWALL,
|
|
566
|
+
NAMESPACE_MODEL_REGISTRY,
|
|
567
|
+
NAMESPACE_RESET_CREDITS,
|
|
568
|
+
NAMESPACE_SETTINGS,
|
|
569
|
+
NAMESPACE_UPSTREAM_ROUTE,
|
|
570
|
+
CacheInvalidationPoller,
|
|
571
|
+
get_cache_invalidation_poller,
|
|
572
|
+
set_cache_invalidation_poller,
|
|
573
|
+
)
|
|
574
|
+
from app.core.config.context_window_overrides import get_model_context_window_overrides_cache
|
|
575
|
+
from app.core.middleware.firewall_cache import get_firewall_ip_cache
|
|
576
|
+
from app.core.upstream_proxy.cache import get_upstream_route_cache
|
|
577
|
+
from app.modules.proxy.account_cache import get_account_selection_cache, get_routing_availability_cache
|
|
578
|
+
from app.modules.rate_limit_reset_credits.store import get_rate_limit_reset_credits_store
|
|
579
|
+
|
|
580
|
+
# The poller MUST be installed before the model scheduler starts: a first
|
|
581
|
+
# leader tick that persists a changed snapshot bumps the model_registry
|
|
582
|
+
# namespace through the global poller, and a not-yet-installed poller would
|
|
583
|
+
# silently drop that bump (followers would then wait for the refresh-tick
|
|
584
|
+
# backstop instead of the cache poll bound).
|
|
585
|
+
cache_poller = CacheInvalidationPoller(SessionLocal)
|
|
586
|
+
cache_poller.on_invalidation(NAMESPACE_API_KEY, get_api_key_cache().clear)
|
|
587
|
+
cache_poller.on_invalidation(NAMESPACE_FIREWALL, get_firewall_ip_cache().invalidate_all)
|
|
588
|
+
routing_availability_cache = get_routing_availability_cache()
|
|
589
|
+
# Remote-bump callbacks must be non-propagating variants: a propagating callback
|
|
590
|
+
# would re-bump on every observed bump and feedback-loop across replicas.
|
|
591
|
+
cache_poller.on_invalidation(NAMESPACE_ACCOUNT_ROUTING, routing_availability_cache.refresh_from_db)
|
|
592
|
+
cache_poller.on_invalidation(
|
|
593
|
+
NAMESPACE_ACCOUNT_SELECTION,
|
|
594
|
+
lambda: get_account_selection_cache().invalidate(propagate=False),
|
|
595
|
+
)
|
|
596
|
+
cache_poller.on_invalidation(
|
|
597
|
+
NAMESPACE_SETTINGS,
|
|
598
|
+
lambda: get_settings_cache().invalidate(propagate=False),
|
|
599
|
+
)
|
|
600
|
+
# Then pull the new row in. Readers that cannot await the cache (the
|
|
601
|
+
# conversation archive gate runs per archived frame) would otherwise keep
|
|
602
|
+
# the pre-change snapshot on a replica that is only carrying already-open
|
|
603
|
+
# streams; the invalidate above already expired it, so a failed refresh
|
|
604
|
+
# degrades to the ordinary TTL reload instead of serving a stale value.
|
|
605
|
+
cache_poller.on_invalidation(NAMESPACE_SETTINGS, get_settings_cache().refresh)
|
|
606
|
+
cache_poller.on_invalidation(
|
|
607
|
+
NAMESPACE_DASHBOARD_USERS,
|
|
608
|
+
lambda: get_dashboard_users_cache().invalidate(propagate=False),
|
|
609
|
+
)
|
|
610
|
+
# Provider settings and identity resolutions ride the same bus: a PATCH on
|
|
611
|
+
# one replica bumps dashboard_users, every replica drops both caches.
|
|
612
|
+
cache_poller.on_invalidation(NAMESPACE_DASHBOARD_USERS, get_auth_provider_registry().clear)
|
|
613
|
+
cache_poller.on_invalidation(NAMESPACE_DASHBOARD_USERS, get_identity_resolution_cache().clear)
|
|
614
|
+
cache_poller.on_invalidation(NAMESPACE_UPSTREAM_ROUTE, get_upstream_route_cache().clear)
|
|
615
|
+
# The route resolver also reads the dashboard settings row (routing enabled
|
|
616
|
+
# + default pool id), so settings bumps clear resolved routes as well.
|
|
617
|
+
cache_poller.on_invalidation(NAMESPACE_SETTINGS, get_upstream_route_cache().clear)
|
|
618
|
+
# M4 model catalogue: the per-model context window override rows are
|
|
619
|
+
# invalidated through the settings namespace as well.
|
|
620
|
+
cache_poller.on_invalidation(
|
|
621
|
+
NAMESPACE_SETTINGS,
|
|
622
|
+
lambda: get_model_context_window_overrides_cache().invalidate(propagate=False),
|
|
623
|
+
)
|
|
624
|
+
# The bus carries no payload, so a peer redeem clears this replica's whole
|
|
625
|
+
# reset-credits store; the refresh scheduler repopulates it on its next tick.
|
|
626
|
+
cache_poller.on_invalidation(NAMESPACE_RESET_CREDITS, get_rate_limit_reset_credits_store().invalidate)
|
|
627
|
+
from app.core.openai.model_registry_store import reconcile_model_registry_from_store
|
|
628
|
+
|
|
629
|
+
# raise_on_error=True so a transient load failure leaves the
|
|
630
|
+
# model_registry version unacknowledged and is retried on the next poll
|
|
631
|
+
# cycle (matching the account_routing refresh callback) instead of being
|
|
632
|
+
# swallowed, which would strand this replica on the stale catalog until
|
|
633
|
+
# the non-leader scheduler backstop.
|
|
634
|
+
cache_poller.on_invalidation(
|
|
635
|
+
NAMESPACE_MODEL_REGISTRY,
|
|
636
|
+
lambda: reconcile_model_registry_from_store(raise_on_error=True),
|
|
637
|
+
)
|
|
638
|
+
set_cache_invalidation_poller(cache_poller)
|
|
639
|
+
|
|
640
|
+
# Seed the invalidation version baseline BEFORE loading the routing snapshot
|
|
641
|
+
# and BEFORE the one-shot model-registry reconcile below. prime() records the
|
|
642
|
+
# current version of every namespace (and flushes any pending bumps) without
|
|
643
|
+
# firing callbacks, so a peer bump committed after this point is observed as a
|
|
644
|
+
# change on the first background poll instead of being silently acknowledged
|
|
645
|
+
# as pre-existing state. In particular a leader that persists-and-bumps the
|
|
646
|
+
# model_registry namespace in the window between the reconcile's snapshot read
|
|
647
|
+
# and the poller's first background tick would otherwise be absorbed as the
|
|
648
|
+
# initial baseline, leaving this replica on the pre-refresh catalog until the
|
|
649
|
+
# non-leader scheduler backstop (default 300s) instead of the sub-second cache
|
|
650
|
+
# poll bound.
|
|
651
|
+
try:
|
|
652
|
+
await cache_poller.prime()
|
|
653
|
+
except Exception:
|
|
654
|
+
# prime() raises when the baseline version read fails, leaving the poller
|
|
655
|
+
# uninitialized so an explicit retry would remain baseline-only. Startup
|
|
656
|
+
# continues, but start() arms conservative recovery: the first successful
|
|
657
|
+
# background read invokes callbacks for positive versions before
|
|
658
|
+
# acknowledging them, so a peer bump cannot become a callback-less
|
|
659
|
+
# baseline after local caches are warm.
|
|
660
|
+
logger.warning("cache invalidation baseline prime failed", exc_info=True)
|
|
661
|
+
try:
|
|
662
|
+
await routing_availability_cache.refresh_from_db()
|
|
663
|
+
except Exception:
|
|
664
|
+
# Unseeded snapshot degrades to local-mark semantics; the next
|
|
665
|
+
# account_routing bump retries the refresh via the poller callback.
|
|
666
|
+
logger.warning("initial routing availability snapshot refresh failed", exc_info=True)
|
|
667
|
+
|
|
668
|
+
# Warm the in-memory registry from the persisted snapshot before any
|
|
669
|
+
# scheduler starts so a restarted replica serves the refreshed catalog
|
|
670
|
+
# instead of the bootstrap floor. Never fails startup.
|
|
671
|
+
await reconcile_model_registry_from_store()
|
|
672
|
+
|
|
673
|
+
await cache_poller.start()
|
|
674
|
+
|
|
675
|
+
async with SessionLocal() as session:
|
|
676
|
+
seeded_count = await AccountsRepository(session).seed_hard_sticky_outage_grace_on_startup()
|
|
677
|
+
if seeded_count > 0:
|
|
678
|
+
logger.info(
|
|
679
|
+
"Seeded hard codex_session purge grace for already-unavailable accounts seeded_count=%s",
|
|
680
|
+
seeded_count,
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
metadata_scheduler = build_metadata_refresh_scheduler()
|
|
684
|
+
usage_scheduler = build_usage_refresh_scheduler()
|
|
685
|
+
api_key_limit_reset_scheduler = build_api_key_limit_reset_scheduler()
|
|
686
|
+
api_key_last_used_flush_scheduler = build_api_key_last_used_flush_scheduler()
|
|
687
|
+
model_scheduler = build_model_refresh_scheduler()
|
|
688
|
+
sticky_session_cleanup_scheduler = build_sticky_session_cleanup_scheduler()
|
|
689
|
+
quota_planner_scheduler = build_quota_planner_scheduler()
|
|
690
|
+
auth_guardian_scheduler = build_auth_guardian_scheduler()
|
|
691
|
+
automations_scheduler = build_automations_scheduler()
|
|
692
|
+
rate_limit_reset_credits_scheduler = build_rate_limit_reset_credits_scheduler()
|
|
693
|
+
account_usage_rollup_scheduler = build_account_usage_rollup_scheduler()
|
|
694
|
+
account_deletion_scheduler = build_account_deletion_scheduler()
|
|
695
|
+
data_retention_scheduler = build_data_retention_scheduler()
|
|
696
|
+
telemetry_scheduler = build_telemetry_scheduler()
|
|
697
|
+
# Hold the instance: this lifespan owns it (and keeps it strongly rooted)
|
|
698
|
+
# even if a nested lifespan on another loop replaces the module-global
|
|
699
|
+
# singleton in the meantime; shutdown below stops exactly this instance.
|
|
700
|
+
live_usage_ingestor = start_live_usage_ingestor()
|
|
701
|
+
await metadata_scheduler.start()
|
|
702
|
+
await usage_scheduler.start()
|
|
703
|
+
await api_key_limit_reset_scheduler.start()
|
|
704
|
+
await api_key_last_used_flush_scheduler.start()
|
|
705
|
+
await model_scheduler.start()
|
|
706
|
+
await sticky_session_cleanup_scheduler.start()
|
|
707
|
+
await quota_planner_scheduler.start()
|
|
708
|
+
await auth_guardian_scheduler.start()
|
|
709
|
+
await automations_scheduler.start()
|
|
710
|
+
await rate_limit_reset_credits_scheduler.start()
|
|
711
|
+
await account_usage_rollup_scheduler.start()
|
|
712
|
+
await account_deletion_scheduler.start()
|
|
713
|
+
await data_retention_scheduler.start()
|
|
714
|
+
await telemetry_scheduler.start()
|
|
715
|
+
if settings.metrics_enabled and PROMETHEUS_AVAILABLE:
|
|
716
|
+
import uvicorn
|
|
717
|
+
|
|
718
|
+
from app.core.server import SignalNeutralServer
|
|
719
|
+
|
|
720
|
+
scrape_registry = make_scrape_registry()
|
|
721
|
+
prometheus_module = import_module("prometheus_client")
|
|
722
|
+
make_asgi_app = getattr(prometheus_module, "make_asgi_app")
|
|
723
|
+
metrics_app = make_asgi_app(registry=scrape_registry)
|
|
724
|
+
config = uvicorn.Config(metrics_app, host="0.0.0.0", port=settings.metrics_port, log_level="warning")
|
|
725
|
+
metrics_server = SignalNeutralServer(config)
|
|
726
|
+
|
|
727
|
+
async def _serve_metrics(srv: _MetricsServer) -> None:
|
|
728
|
+
try:
|
|
729
|
+
await srv.serve()
|
|
730
|
+
except SystemExit as exc:
|
|
731
|
+
if _is_benign_metrics_bind_failure(exc):
|
|
732
|
+
logger.info(
|
|
733
|
+
"Metrics port %d unavailable (another worker likely serves metrics)",
|
|
734
|
+
settings.metrics_port,
|
|
735
|
+
)
|
|
736
|
+
elif _is_metrics_bind_conflict(exc):
|
|
737
|
+
_log_non_multiproc_metrics_bind_conflict(settings.metrics_port)
|
|
738
|
+
else:
|
|
739
|
+
raise
|
|
740
|
+
except OSError as exc:
|
|
741
|
+
if _is_benign_metrics_bind_failure(exc):
|
|
742
|
+
logger.info(
|
|
743
|
+
"Metrics port %d already bound (another worker serves metrics)",
|
|
744
|
+
settings.metrics_port,
|
|
745
|
+
)
|
|
746
|
+
elif _is_metrics_bind_conflict(exc):
|
|
747
|
+
_log_non_multiproc_metrics_bind_conflict(settings.metrics_port)
|
|
748
|
+
else:
|
|
749
|
+
raise
|
|
750
|
+
|
|
751
|
+
metrics_server_task = asyncio.create_task(_serve_metrics(metrics_server))
|
|
752
|
+
elif settings.metrics_enabled:
|
|
753
|
+
logger.warning("Metrics endpoint enabled but prometheus-client is not installed")
|
|
754
|
+
|
|
755
|
+
async def _complete_bridge_registration(svc: RingMembershipService, iid: str) -> None:
|
|
756
|
+
if bridge_endpoint_base_url is None:
|
|
757
|
+
await _activate_bridge_membership(svc, iid)
|
|
758
|
+
startup_module.mark_bridge_registration_complete()
|
|
759
|
+
return
|
|
760
|
+
await _validate_bridge_advertise_endpoint_for_multi_replica(
|
|
761
|
+
svc=svc,
|
|
762
|
+
settings=settings,
|
|
763
|
+
instance_id=iid,
|
|
764
|
+
endpoint_base_url=bridge_endpoint_base_url,
|
|
765
|
+
)
|
|
766
|
+
await svc.register(iid, endpoint_base_url=None)
|
|
767
|
+
await _wait_for_bridge_advertise_endpoint(
|
|
768
|
+
bridge_endpoint_base_url,
|
|
769
|
+
connect_timeout_seconds=effective_settings(
|
|
770
|
+
await get_settings_cache().get(), settings
|
|
771
|
+
).upstream_connect_timeout_seconds,
|
|
772
|
+
)
|
|
773
|
+
await svc.heartbeat(iid, endpoint_base_url=bridge_endpoint_base_url)
|
|
774
|
+
startup_module.mark_bridge_registration_complete()
|
|
775
|
+
|
|
776
|
+
async def _heartbeat_only(svc: RingMembershipService, iid: str) -> None:
|
|
777
|
+
while True:
|
|
778
|
+
await asyncio.sleep(RING_HEARTBEAT_INTERVAL_SECONDS)
|
|
779
|
+
try:
|
|
780
|
+
await svc.heartbeat(iid, endpoint_base_url=bridge_endpoint_base_url)
|
|
781
|
+
except Exception:
|
|
782
|
+
logger.warning("Ring heartbeat failed", exc_info=True)
|
|
783
|
+
await run_http_bridge_heartbeat_maintenance(getattr(app.state, "proxy_service", None))
|
|
784
|
+
await refresh_cap_partition(svc.list_active, iid)
|
|
785
|
+
|
|
786
|
+
async def _register_and_heartbeat(svc: RingMembershipService, iid: str) -> None:
|
|
787
|
+
attempt = 0
|
|
788
|
+
while True:
|
|
789
|
+
attempt += 1
|
|
790
|
+
try:
|
|
791
|
+
await _complete_bridge_registration(svc, iid)
|
|
792
|
+
logger.info("Registered in bridge ring", extra={"instance_id": iid, "attempt": attempt})
|
|
793
|
+
break
|
|
794
|
+
except Exception:
|
|
795
|
+
delay = min(5.0 * (2 ** min(attempt - 1, 5)), 60.0)
|
|
796
|
+
logger.warning("Ring registration attempt %d failed, retrying in %.0fs", attempt, delay, exc_info=True)
|
|
797
|
+
await asyncio.sleep(delay)
|
|
798
|
+
await refresh_cap_partition(svc.list_active, iid)
|
|
799
|
+
await _heartbeat_only(svc, iid)
|
|
800
|
+
|
|
801
|
+
async def _activate_bridge_membership(svc: RingMembershipService, iid: str) -> None:
|
|
802
|
+
if bridge_endpoint_base_url is None:
|
|
803
|
+
await svc.register(iid, endpoint_base_url=None)
|
|
804
|
+
return
|
|
805
|
+
await svc.register(iid, endpoint_base_url=bridge_endpoint_base_url)
|
|
806
|
+
|
|
807
|
+
ring_service: RingMembershipService | None = None
|
|
808
|
+
instance_id: str | None = None
|
|
809
|
+
heartbeat_task: asyncio.Task[None] | None = None
|
|
810
|
+
ring_service = RingMembershipService(SessionLocal)
|
|
811
|
+
instance_id = settings.http_responses_session_bridge_instance_id
|
|
812
|
+
heartbeat_task = asyncio.create_task(_register_and_heartbeat(ring_service, instance_id))
|
|
813
|
+
loop_lag_task: asyncio.Task[None] | None = None
|
|
814
|
+
if settings.event_loop_lag_warn_threshold_seconds > 0:
|
|
815
|
+
loop_lag_task = asyncio.create_task(
|
|
816
|
+
run_event_loop_lag_monitor(
|
|
817
|
+
warn_threshold_seconds=settings.event_loop_lag_warn_threshold_seconds,
|
|
818
|
+
)
|
|
819
|
+
)
|
|
820
|
+
startup_module._startup_complete = True
|
|
821
|
+
|
|
822
|
+
try:
|
|
823
|
+
yield
|
|
824
|
+
finally:
|
|
825
|
+
shutdown_state.commit_shutdown(timeout_seconds=settings.shutdown_drain_timeout_seconds)
|
|
826
|
+
remaining_drain_seconds = shutdown_state.remaining_drain_timeout_seconds() or 0.0
|
|
827
|
+
drained = await shutdown_state.wait_for_in_flight_drain(timeout_seconds=remaining_drain_seconds)
|
|
828
|
+
# No await separates the timeout result from this cutoff. A slow
|
|
829
|
+
# request therefore either registered its task before the barrier or
|
|
830
|
+
# is prevented from starting new audit/fleet database work afterward.
|
|
831
|
+
shutdown_state.close_control_plane_task_admission()
|
|
832
|
+
if not drained:
|
|
833
|
+
logger.warning("Drain timeout reached, proceeding with shutdown")
|
|
834
|
+
|
|
835
|
+
proxy_service = getattr(app.state, "proxy_service", None)
|
|
836
|
+
recovery_settlements_drained = True
|
|
837
|
+
# Settle detached recovery journals while their origin leases are
|
|
838
|
+
# still held; bridge teardown below may release those owner fences.
|
|
839
|
+
recovery_settlements_drained = await _drain_proxy_persistence_tasks(
|
|
840
|
+
proxy_service,
|
|
841
|
+
shutdown_state.remaining_post_drain_cleanup_timeout_seconds() or 0.0,
|
|
842
|
+
task_name_prefixes=("http-bridge-recovery-settlement-",),
|
|
843
|
+
failure_message="Failed to pre-drain proxy settlement tasks during shutdown",
|
|
844
|
+
)
|
|
845
|
+
# An in-flight request can still own a database session after the
|
|
846
|
+
# process-wide drain deadline. It is therefore part of the clean proof
|
|
847
|
+
# even though the later detached drains have their own gates.
|
|
848
|
+
database_tasks_drained = drained and recovery_settlements_drained
|
|
849
|
+
bridge_sessions_drained = await _close_proxy_http_bridge_sessions_for_shutdown(
|
|
850
|
+
proxy_service,
|
|
851
|
+
mark_draining=recovery_settlements_drained,
|
|
852
|
+
)
|
|
853
|
+
database_tasks_drained = database_tasks_drained and bridge_sessions_drained
|
|
854
|
+
# Drain AFTER the bridge teardown: failing a bridge's pending
|
|
855
|
+
# requests writes their request logs, which enqueues more
|
|
856
|
+
# persistence tasks that this drain must cover.
|
|
857
|
+
remaining_drain_seconds = shutdown_state.remaining_drain_timeout_seconds() or 0.0
|
|
858
|
+
final_proxy_persistence_drained = await _drain_proxy_persistence_tasks(
|
|
859
|
+
proxy_service,
|
|
860
|
+
remaining_drain_seconds,
|
|
861
|
+
failure_message="Failed to drain proxy persistence tasks during shutdown",
|
|
862
|
+
)
|
|
863
|
+
database_tasks_drained = database_tasks_drained and final_proxy_persistence_drained
|
|
864
|
+
|
|
865
|
+
# Cancel heartbeat and age the shared ring row near expiry.
|
|
866
|
+
if heartbeat_task is not None:
|
|
867
|
+
heartbeat_task.cancel()
|
|
868
|
+
try:
|
|
869
|
+
await asyncio.wait_for(heartbeat_task, timeout=2)
|
|
870
|
+
except (asyncio.CancelledError, TimeoutError):
|
|
871
|
+
pass
|
|
872
|
+
|
|
873
|
+
if loop_lag_task is not None:
|
|
874
|
+
loop_lag_task.cancel()
|
|
875
|
+
try:
|
|
876
|
+
await asyncio.wait_for(loop_lag_task, timeout=2)
|
|
877
|
+
except (asyncio.CancelledError, TimeoutError):
|
|
878
|
+
pass
|
|
879
|
+
|
|
880
|
+
if ring_service is not None and instance_id is not None:
|
|
881
|
+
try:
|
|
882
|
+
await asyncio.wait_for(
|
|
883
|
+
ring_service.mark_stale(
|
|
884
|
+
instance_id,
|
|
885
|
+
stale_threshold_seconds=RING_STALE_THRESHOLD_SECONDS,
|
|
886
|
+
grace_seconds=RING_STALE_GRACE_SECONDS,
|
|
887
|
+
),
|
|
888
|
+
timeout=3,
|
|
889
|
+
)
|
|
890
|
+
logger.info(
|
|
891
|
+
"Marked bridge ring membership stale for shutdown",
|
|
892
|
+
extra={"instance_id": instance_id},
|
|
893
|
+
)
|
|
894
|
+
except Exception:
|
|
895
|
+
logger.warning("Failed to mark bridge ring membership stale during shutdown", exc_info=True)
|
|
896
|
+
|
|
897
|
+
if metrics_server is not None:
|
|
898
|
+
metrics_server.should_exit = True
|
|
899
|
+
|
|
900
|
+
# Detached control-plane work must finish while usage singleflight,
|
|
901
|
+
# shared HTTP clients, and both database engines are still available.
|
|
902
|
+
# The replica heartbeat is already stopped/staled so this grace period
|
|
903
|
+
# does not extend its active bridge-ring lifetime.
|
|
904
|
+
remaining_drain_seconds = shutdown_state.remaining_drain_timeout_seconds() or 0.0
|
|
905
|
+
control_plane_tasks_drained = await _drain_detached_control_plane_tasks(remaining_drain_seconds)
|
|
906
|
+
database_tasks_drained = database_tasks_drained and control_plane_tasks_drained
|
|
907
|
+
|
|
908
|
+
# Start the single process-level lease-renewal keeper BEFORE stopping any
|
|
909
|
+
# scheduler. Schedulers are stopped one at a time and only the final
|
|
910
|
+
# release() renews the lease while draining detached bodies; an earlier
|
|
911
|
+
# scheduler's stop() can detach a shielded leader-gated body (which stops
|
|
912
|
+
# that scheduler's own heartbeat) while the remaining schedulers are still
|
|
913
|
+
# stopping. If that stop sequence takes >= the (minimum 5s) TTL, the DB
|
|
914
|
+
# lease would otherwise expire while the detached body still runs as
|
|
915
|
+
# leader, letting a follower acquire it and run duplicate singleton work.
|
|
916
|
+
# The keeper renews the lease continuously across the whole stop sequence
|
|
917
|
+
# and is stopped by release(), which then owns renewal for its bounded
|
|
918
|
+
# drain. It is a no-op when leader election is disabled.
|
|
919
|
+
get_leader_election().start_release_keeper()
|
|
920
|
+
await quota_planner_scheduler.stop()
|
|
921
|
+
await auth_guardian_scheduler.stop()
|
|
922
|
+
await automations_scheduler.stop()
|
|
923
|
+
await sticky_session_cleanup_scheduler.stop()
|
|
924
|
+
await metadata_scheduler.stop()
|
|
925
|
+
await model_scheduler.stop()
|
|
926
|
+
# Stop the invalidation poller only after the model scheduler: a final
|
|
927
|
+
# leader tick may still bump through the installed poller.
|
|
928
|
+
await cache_poller.stop()
|
|
929
|
+
if get_cache_invalidation_poller() is cache_poller:
|
|
930
|
+
# A stopped poller must not keep receiving propagation requests.
|
|
931
|
+
set_cache_invalidation_poller(None)
|
|
932
|
+
await api_key_limit_reset_scheduler.stop()
|
|
933
|
+
# Final last_used_at flush (bounded retries). Settlement tasks were
|
|
934
|
+
# drained above; if that drain timed out, a surviving task's later
|
|
935
|
+
# record() write-throughs immediately (stop() switches the coalescer
|
|
936
|
+
# to shutdown write-through mode before the final flush), so a late
|
|
937
|
+
# touch is never parked in a pending map with no remaining flusher.
|
|
938
|
+
await api_key_last_used_flush_scheduler.stop()
|
|
939
|
+
await usage_scheduler.stop()
|
|
940
|
+
await stop_live_usage_ingestor(live_usage_ingestor)
|
|
941
|
+
await rate_limit_reset_credits_scheduler.stop()
|
|
942
|
+
await account_usage_rollup_scheduler.stop()
|
|
943
|
+
await account_deletion_scheduler.stop()
|
|
944
|
+
await data_retention_scheduler.stop()
|
|
945
|
+
await telemetry_scheduler.stop()
|
|
946
|
+
# Release the scheduler leader lease only after every leader-gated
|
|
947
|
+
# scheduler has stopped so no local tick re-acquires it; followers can
|
|
948
|
+
# then take over immediately instead of waiting out the lease TTL.
|
|
949
|
+
# release() itself first drains bodies that were detached still
|
|
950
|
+
# draining shielded work, and skips the early release (letting the
|
|
951
|
+
# lease expire by TTL) if any is still running, so a follower cannot
|
|
952
|
+
# become leader while this process may still act as one. The deadline
|
|
953
|
+
# covers that bounded drain plus the row delete, and — because the
|
|
954
|
+
# release path shields and awaits its own session teardown — is
|
|
955
|
+
# enforced by abandoning the release task rather than awaiting a
|
|
956
|
+
# potentially wedged cancellation, so shutdown always proceeds.
|
|
957
|
+
leader_lease_release_completed = await _release_leader_lease_within(10)
|
|
958
|
+
try:
|
|
959
|
+
try:
|
|
960
|
+
await close_discovered_native_egress_client()
|
|
961
|
+
finally:
|
|
962
|
+
await close_http_client()
|
|
963
|
+
finally:
|
|
964
|
+
try:
|
|
965
|
+
if metrics_server_task is not None:
|
|
966
|
+
await asyncio.wait_for(metrics_server_task, timeout=5)
|
|
967
|
+
except TimeoutError:
|
|
968
|
+
logger.warning("Timed out waiting for metrics server shutdown")
|
|
969
|
+
except Exception:
|
|
970
|
+
logger.exception("Metrics server stopped with an error")
|
|
971
|
+
finally:
|
|
972
|
+
mark_process_dead()
|
|
973
|
+
try:
|
|
974
|
+
await _close_db_and_record_clean_shutdown(
|
|
975
|
+
database_tasks_drained=database_tasks_drained,
|
|
976
|
+
leader_lease_release_completed=leader_lease_release_completed,
|
|
977
|
+
)
|
|
978
|
+
finally:
|
|
979
|
+
shutdown_state.mark_lifespan_completed()
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
def create_app() -> FastAPI:
|
|
983
|
+
settings = get_settings()
|
|
984
|
+
configure_memory_monitor(reject_threshold_mb=settings.memory_reject_threshold_mb)
|
|
985
|
+
app = FastAPI(
|
|
986
|
+
title="codex-lb",
|
|
987
|
+
version="0.1.0",
|
|
988
|
+
lifespan=lifespan,
|
|
989
|
+
swagger_ui_parameters={"persistAuthorization": True},
|
|
990
|
+
)
|
|
991
|
+
if settings.otel_enabled:
|
|
992
|
+
from app.core.tracing.otel import init_tracing
|
|
993
|
+
|
|
994
|
+
init_tracing(service_name="codex-lb", endpoint=settings.otel_exporter_endpoint, app=app)
|
|
995
|
+
|
|
996
|
+
# Innermost of the app-level middlewares: binds the dashboard-managed
|
|
997
|
+
# settings overrides (C2-1 timeouts) for the request/socket once the
|
|
998
|
+
# admission middlewares below have let it through.
|
|
999
|
+
app.add_middleware(cast(Any, DashboardOverridesMiddleware))
|
|
1000
|
+
app.add_middleware(cast(Any, InFlightMiddleware))
|
|
1001
|
+
add_dashboard_gzip_middleware(app)
|
|
1002
|
+
add_dashboard_auth_proxy_middleware(app)
|
|
1003
|
+
add_dashboard_csrf_middleware(app)
|
|
1004
|
+
add_request_decompression_middleware(app)
|
|
1005
|
+
add_request_body_limit_middleware(app)
|
|
1006
|
+
add_multipart_content_encoding_middleware(app)
|
|
1007
|
+
add_required_capability_http_middleware(app)
|
|
1008
|
+
add_request_id_middleware(app)
|
|
1009
|
+
add_api_firewall_middleware(app)
|
|
1010
|
+
app.add_middleware(cast(Any, MetricsMiddleware), enabled=settings.metrics_enabled)
|
|
1011
|
+
if settings.backpressure_max_concurrent_requests > 0:
|
|
1012
|
+
app.add_middleware(
|
|
1013
|
+
cast(Any, BackpressureMiddleware),
|
|
1014
|
+
max_concurrent=settings.backpressure_max_concurrent_requests,
|
|
1015
|
+
)
|
|
1016
|
+
app.add_middleware(
|
|
1017
|
+
cast(Any, BulkheadMiddleware),
|
|
1018
|
+
bulkhead=get_bulkhead(
|
|
1019
|
+
proxy_http_limit=settings.bulkhead_proxy_limit,
|
|
1020
|
+
proxy_websocket_limit=settings.bulkhead_proxy_limit,
|
|
1021
|
+
# Compact limit is derived by BulkheadSemaphore: min(http, 16),
|
|
1022
|
+
# or 0 when the http class is unlimited-off (0).
|
|
1023
|
+
proxy_compact_limit=None,
|
|
1024
|
+
dashboard_limit=settings.bulkhead_dashboard_limit,
|
|
1025
|
+
),
|
|
1026
|
+
)
|
|
1027
|
+
add_backend_api_codex_v1_alias_middleware(app)
|
|
1028
|
+
add_app_version_middleware(app)
|
|
1029
|
+
add_exception_handlers(app)
|
|
1030
|
+
add_trusted_proxy_headers_middleware(app)
|
|
1031
|
+
|
|
1032
|
+
app.include_router(proxy_api.realtime_call_router)
|
|
1033
|
+
app.include_router(proxy_api.router)
|
|
1034
|
+
app.include_router(proxy_api.internal_router)
|
|
1035
|
+
app.include_router(proxy_api.ws_router)
|
|
1036
|
+
app.include_router(proxy_api.wham_router)
|
|
1037
|
+
app.include_router(proxy_api.v1_router)
|
|
1038
|
+
app.include_router(proxy_api.v1_ws_router)
|
|
1039
|
+
app.include_router(proxy_api.transcribe_router)
|
|
1040
|
+
app.include_router(proxy_api.files_router)
|
|
1041
|
+
app.include_router(proxy_api.usage_router)
|
|
1042
|
+
app.include_router(audit_api.router)
|
|
1043
|
+
app.include_router(accounts_api.router)
|
|
1044
|
+
app.include_router(rate_limit_reset_credits_api.router)
|
|
1045
|
+
app.include_router(dashboard_api.router)
|
|
1046
|
+
app.include_router(usage_api.router)
|
|
1047
|
+
app.include_router(request_logs_api.router)
|
|
1048
|
+
app.include_router(request_logs_api.conversations_router)
|
|
1049
|
+
app.include_router(quota_planner_api.router)
|
|
1050
|
+
app.include_router(reports_api.router)
|
|
1051
|
+
app.include_router(conversation_archive_api.router)
|
|
1052
|
+
app.include_router(runtime_api.router)
|
|
1053
|
+
app.include_router(oauth_api.router)
|
|
1054
|
+
app.include_router(dashboard_auth_api.router)
|
|
1055
|
+
app.include_router(dashboard_users_api.router)
|
|
1056
|
+
app.include_router(dashboard_roles_api.router)
|
|
1057
|
+
app.include_router(auth_providers_api.router)
|
|
1058
|
+
app.include_router(role_mappings_api.router)
|
|
1059
|
+
app.include_router(scim_api.router)
|
|
1060
|
+
app.include_router(scim_tokens_api.router)
|
|
1061
|
+
app.include_router(settings_api.router)
|
|
1062
|
+
app.include_router(telemetry_api.router)
|
|
1063
|
+
app.include_router(firewall_api.router)
|
|
1064
|
+
app.include_router(fleet_api.router)
|
|
1065
|
+
app.include_router(sticky_sessions_api.router)
|
|
1066
|
+
app.include_router(cache_isolation_probe_api.router)
|
|
1067
|
+
app.include_router(automations_api.router)
|
|
1068
|
+
app.include_router(api_keys_api.router)
|
|
1069
|
+
app.include_router(model_sources_api.router)
|
|
1070
|
+
app.include_router(health_api.router)
|
|
1071
|
+
|
|
1072
|
+
static_dir = Path(__file__).parent / "static"
|
|
1073
|
+
index_html = static_dir / "index.html"
|
|
1074
|
+
static_root = static_dir.resolve()
|
|
1075
|
+
frontend_build_hint = "Frontend assets are missing. Run `cd frontend && bun run build`."
|
|
1076
|
+
# ``scim/`` is here for a reason the others are not: an identity provider
|
|
1077
|
+
# probes for resources this release does not implement (Groups,
|
|
1078
|
+
# ServiceProviderConfig), and answering those with index.html and a 200
|
|
1079
|
+
# tells it they are supported. They must be a SCIM 404.
|
|
1080
|
+
excluded_prefixes = ("api/", "v1/", "backend-api/", "health", "scim/")
|
|
1081
|
+
|
|
1082
|
+
def _is_static_asset_path(path: str) -> bool:
|
|
1083
|
+
if path.startswith("assets/"):
|
|
1084
|
+
return True
|
|
1085
|
+
last_segment = path.rsplit("/", maxsplit=1)[-1]
|
|
1086
|
+
return "." in last_segment
|
|
1087
|
+
|
|
1088
|
+
@app.get("/", include_in_schema=False)
|
|
1089
|
+
@app.get("/{path:path}", include_in_schema=False)
|
|
1090
|
+
async def spa_fallback(path: str = ""):
|
|
1091
|
+
normalized = path.lstrip("/")
|
|
1092
|
+
if normalized and any(
|
|
1093
|
+
normalized == prefix.rstrip("/") or normalized.startswith(prefix) for prefix in excluded_prefixes
|
|
1094
|
+
):
|
|
1095
|
+
raise HTTPException(status_code=404, detail="Not Found")
|
|
1096
|
+
|
|
1097
|
+
if normalized:
|
|
1098
|
+
candidate = _resolve_static_asset_path(static_root, normalized)
|
|
1099
|
+
if candidate is not None:
|
|
1100
|
+
if normalized.startswith("assets/"):
|
|
1101
|
+
# Vite content-hashes everything under assets/, so the
|
|
1102
|
+
# response for a given URL can never change: immutable
|
|
1103
|
+
# caching makes repeat dashboard loads skip ~1.7 MB of
|
|
1104
|
+
# re-downloads/revalidations. index.html stays no-cache
|
|
1105
|
+
# below, so deploys still pick up new hashes.
|
|
1106
|
+
return FileResponse(candidate, headers={"Cache-Control": "public, max-age=31536000, immutable"})
|
|
1107
|
+
return FileResponse(candidate)
|
|
1108
|
+
if _is_static_asset_path(normalized):
|
|
1109
|
+
raise HTTPException(status_code=404, detail="Not Found")
|
|
1110
|
+
|
|
1111
|
+
if not index_html.is_file():
|
|
1112
|
+
raise HTTPException(status_code=503, detail=frontend_build_hint)
|
|
1113
|
+
|
|
1114
|
+
return FileResponse(index_html, media_type="text/html", headers={"Cache-Control": "no-cache"})
|
|
1115
|
+
|
|
1116
|
+
return app
|
|
1117
|
+
|
|
1118
|
+
|
|
1119
|
+
async def _ensure_bridge_durable_schema_ready(settings) -> bool:
|
|
1120
|
+
if not settings.http_responses_session_bridge_enabled:
|
|
1121
|
+
return False
|
|
1122
|
+
session = SessionLocal()
|
|
1123
|
+
try:
|
|
1124
|
+
missing_tables = await missing_durable_bridge_tables(session)
|
|
1125
|
+
finally:
|
|
1126
|
+
await close_session(session)
|
|
1127
|
+
if not missing_tables:
|
|
1128
|
+
return True
|
|
1129
|
+
missing = ", ".join(missing_tables)
|
|
1130
|
+
if settings.database_migrations_fail_fast:
|
|
1131
|
+
raise RuntimeError(f"HTTP bridge durable schema is missing required tables: {missing}")
|
|
1132
|
+
logger.warning(
|
|
1133
|
+
"HTTP bridge durable schema is missing required tables but startup fail-fast is disabled",
|
|
1134
|
+
extra={"missing_tables": missing_tables},
|
|
1135
|
+
)
|
|
1136
|
+
return False
|
|
1137
|
+
|
|
1138
|
+
|
|
1139
|
+
async def _wait_for_bridge_advertise_endpoint(
|
|
1140
|
+
bridge_endpoint_base_url: str | None,
|
|
1141
|
+
*,
|
|
1142
|
+
connect_timeout_seconds: float,
|
|
1143
|
+
) -> None:
|
|
1144
|
+
local_port = _local_api_port()
|
|
1145
|
+
if bridge_endpoint_base_url is None and local_port is None:
|
|
1146
|
+
raise RuntimeError(
|
|
1147
|
+
"Cannot determine local bridge listener port for registration probe; "
|
|
1148
|
+
"set PORT or configure http_responses_session_bridge_advertise_base_url"
|
|
1149
|
+
)
|
|
1150
|
+
probe_base_url = bridge_endpoint_base_url or f"http://127.0.0.1:{local_port}"
|
|
1151
|
+
probe_base_url = probe_base_url.rstrip("/")
|
|
1152
|
+
probe_url = f"{probe_base_url}/health/live"
|
|
1153
|
+
timeout = aiohttp.ClientTimeout(
|
|
1154
|
+
total=connect_timeout_seconds,
|
|
1155
|
+
sock_connect=connect_timeout_seconds,
|
|
1156
|
+
sock_read=connect_timeout_seconds,
|
|
1157
|
+
)
|
|
1158
|
+
max_probe_wait_seconds = max(connect_timeout_seconds, 5.0)
|
|
1159
|
+
deadline = time.monotonic() + max_probe_wait_seconds
|
|
1160
|
+
await asyncio.sleep(0)
|
|
1161
|
+
attempt = 0
|
|
1162
|
+
while time.monotonic() < deadline:
|
|
1163
|
+
attempt += 1
|
|
1164
|
+
try:
|
|
1165
|
+
async with aiohttp.ClientSession(timeout=timeout, trust_env=False) as session:
|
|
1166
|
+
async with session.get(probe_url) as response:
|
|
1167
|
+
if response.status == 200:
|
|
1168
|
+
return
|
|
1169
|
+
except Exception:
|
|
1170
|
+
logger.debug(
|
|
1171
|
+
"Bridge advertise endpoint not yet reachable",
|
|
1172
|
+
extra={"probe_url": probe_url, "attempt": attempt},
|
|
1173
|
+
exc_info=True,
|
|
1174
|
+
)
|
|
1175
|
+
delay = min(0.5 * (2 ** min(attempt - 1, 4)), 5.0)
|
|
1176
|
+
remaining = deadline - time.monotonic()
|
|
1177
|
+
if remaining <= 0:
|
|
1178
|
+
break
|
|
1179
|
+
await asyncio.sleep(min(delay, remaining))
|
|
1180
|
+
raise RuntimeError(
|
|
1181
|
+
f"Bridge advertise endpoint did not become reachable before registration probe deadline: {probe_url}"
|
|
1182
|
+
)
|
|
1183
|
+
|
|
1184
|
+
|
|
1185
|
+
def _local_api_port() -> int | None:
|
|
1186
|
+
raw = os.getenv("PORT")
|
|
1187
|
+
port = _parse_port_value(raw.strip()) if raw is not None else None
|
|
1188
|
+
if port is None:
|
|
1189
|
+
port = _port_from_argv()
|
|
1190
|
+
return port
|
|
1191
|
+
|
|
1192
|
+
|
|
1193
|
+
def _port_from_argv() -> int | None:
|
|
1194
|
+
args = tuple(sys.argv[1:])
|
|
1195
|
+
for index, value in enumerate(args):
|
|
1196
|
+
if value == "--port" and index + 1 < len(args):
|
|
1197
|
+
return _parse_port_value(args[index + 1])
|
|
1198
|
+
if value.startswith("--port="):
|
|
1199
|
+
return _parse_port_value(value.split("=", 1)[1])
|
|
1200
|
+
return None
|
|
1201
|
+
|
|
1202
|
+
|
|
1203
|
+
async def _validate_bridge_advertise_endpoint_for_multi_replica(
|
|
1204
|
+
svc: _RingMembershipReader,
|
|
1205
|
+
*,
|
|
1206
|
+
settings,
|
|
1207
|
+
instance_id: str,
|
|
1208
|
+
endpoint_base_url: str | None,
|
|
1209
|
+
) -> None:
|
|
1210
|
+
if endpoint_base_url is None:
|
|
1211
|
+
return
|
|
1212
|
+
hostname = urlparse(endpoint_base_url).hostname
|
|
1213
|
+
if hostname is None:
|
|
1214
|
+
raise RuntimeError("http_responses_session_bridge_advertise_base_url must include a valid hostname")
|
|
1215
|
+
try:
|
|
1216
|
+
parsed_ip = ip_address(hostname)
|
|
1217
|
+
except ValueError:
|
|
1218
|
+
parsed_ip = None
|
|
1219
|
+
if (parsed_ip is not None and parsed_ip.is_loopback) or hostname == "localhost":
|
|
1220
|
+
configured_multi_replica = len(settings.http_responses_session_bridge_instance_ring) > 1
|
|
1221
|
+
if configured_multi_replica:
|
|
1222
|
+
raise RuntimeError(
|
|
1223
|
+
"http_responses_session_bridge_advertise_base_url must be replica-specific for bridge routing"
|
|
1224
|
+
)
|
|
1225
|
+
try:
|
|
1226
|
+
active_instances = await svc.list_active(stale_threshold_seconds=RING_HEARTBEAT_INTERVAL_SECONDS)
|
|
1227
|
+
except Exception:
|
|
1228
|
+
active_instances = []
|
|
1229
|
+
if any(active_instance != instance_id for active_instance in active_instances):
|
|
1230
|
+
raise RuntimeError(
|
|
1231
|
+
"http_responses_session_bridge_advertise_base_url must be replica-specific for bridge routing"
|
|
1232
|
+
)
|
|
1233
|
+
return
|
|
1234
|
+
if not _bridge_advertise_hostname_is_replica_specific(hostname, instance_id=instance_id):
|
|
1235
|
+
raise RuntimeError(
|
|
1236
|
+
"http_responses_session_bridge_advertise_base_url must be replica-specific for bridge routing"
|
|
1237
|
+
)
|
|
1238
|
+
|
|
1239
|
+
|
|
1240
|
+
app = create_app()
|