codexLB-up 1.25.0b10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- app/__init__.py +10 -0
- app/admin_cli.py +494 -0
- app/cli.py +339 -0
- app/codex_sessions_retag.py +487 -0
- app/core/__init__.py +0 -0
- app/core/audit/__init__.py +0 -0
- app/core/audit/service.py +216 -0
- app/core/audit/types.py +94 -0
- app/core/auth/__init__.py +236 -0
- app/core/auth/api_key_cache.py +65 -0
- app/core/auth/dashboard_access.py +429 -0
- app/core/auth/dashboard_mode.py +171 -0
- app/core/auth/dashboard_session_ttl.py +74 -0
- app/core/auth/dashboard_users_cache.py +120 -0
- app/core/auth/dependencies.py +710 -0
- app/core/auth/external_identity.py +78 -0
- app/core/auth/guardian.py +374 -0
- app/core/auth/models.py +49 -0
- app/core/auth/providers/__init__.py +129 -0
- app/core/auth/providers/oidc.py +838 -0
- app/core/auth/providers/registry.py +118 -0
- app/core/auth/refresh.py +394 -0
- app/core/auth/step_up.py +117 -0
- app/core/auth/totp.py +81 -0
- app/core/balancer/__init__.py +71 -0
- app/core/balancer/logic.py +1612 -0
- app/core/balancer/rendezvous_hash.py +21 -0
- app/core/balancer/types.py +21 -0
- app/core/bootstrap.py +172 -0
- app/core/cache/invalidation.py +426 -0
- app/core/clients/__init__.py +0 -0
- app/core/clients/codex.py +996 -0
- app/core/clients/codex_version.py +187 -0
- app/core/clients/codex_version_snapshot.py +2 -0
- app/core/clients/files.py +534 -0
- app/core/clients/headers.py +29 -0
- app/core/clients/http.py +511 -0
- app/core/clients/model_fetcher.py +276 -0
- app/core/clients/native_egress.py +1304 -0
- app/core/clients/oauth.py +477 -0
- app/core/clients/proxy.py +6089 -0
- app/core/clients/proxy_websocket.py +1411 -0
- app/core/clients/rate_limit_reset_credits.py +412 -0
- app/core/clients/stream_errors.py +12 -0
- app/core/clients/thread_cache_identity.py +347 -0
- app/core/clients/usage.py +467 -0
- app/core/clock.py +170 -0
- app/core/config/__init__.py +0 -0
- app/core/config/background_jobs.py +73 -0
- app/core/config/context_window_overrides.py +166 -0
- app/core/config/dashboard_overrides.py +145 -0
- app/core/config/inheritable.py +47 -0
- app/core/config/key_fingerprint.py +115 -0
- app/core/config/settings.py +872 -0
- app/core/config/settings_cache.py +100 -0
- app/core/config/spool_retention.py +217 -0
- app/core/config/tiers.py +174 -0
- app/core/conversation_archive.py +568 -0
- app/core/crypto.py +37 -0
- app/core/draining.py +11 -0
- app/core/errors.py +365 -0
- app/core/exceptions.py +155 -0
- app/core/handlers/__init__.py +3 -0
- app/core/handlers/exceptions.py +478 -0
- app/core/http_protocol.py +176 -0
- app/core/http_protocol_httptools.py +204 -0
- app/core/ingress_limits.py +17 -0
- app/core/metadata_store.py +18 -0
- app/core/metrics/__init__.py +0 -0
- app/core/metrics/middleware.py +74 -0
- app/core/metrics/prometheus.py +604 -0
- app/core/middleware/__init__.py +25 -0
- app/core/middleware/api_firewall.py +111 -0
- app/core/middleware/app_version.py +37 -0
- app/core/middleware/dashboard_auth_proxy.py +55 -0
- app/core/middleware/dashboard_csrf.py +177 -0
- app/core/middleware/dashboard_gzip.py +41 -0
- app/core/middleware/dashboard_overrides.py +59 -0
- app/core/middleware/firewall_cache.py +91 -0
- app/core/middleware/inflight.py +126 -0
- app/core/middleware/multipart_content_encoding.py +129 -0
- app/core/middleware/path_rewrite.py +148 -0
- app/core/middleware/request_body_limit.py +196 -0
- app/core/middleware/request_decompression.py +209 -0
- app/core/middleware/request_id.py +72 -0
- app/core/middleware/required_capability_http.py +116 -0
- app/core/middleware/trusted_proxy_headers.py +36 -0
- app/core/multipart.py +237 -0
- app/core/multipart_fields.py +91 -0
- app/core/openai/__init__.py +0 -0
- app/core/openai/chat_requests.py +493 -0
- app/core/openai/chat_responses.py +931 -0
- app/core/openai/contracts.py +46 -0
- app/core/openai/exceptions.py +24 -0
- app/core/openai/host_models.py +14 -0
- app/core/openai/images.py +413 -0
- app/core/openai/message_coercion.py +376 -0
- app/core/openai/model_refresh_scheduler.py +423 -0
- app/core/openai/model_registry.py +997 -0
- app/core/openai/model_registry_store.py +548 -0
- app/core/openai/models.py +172 -0
- app/core/openai/parsing.py +97 -0
- app/core/openai/requests.py +1980 -0
- app/core/openai/strict_schema.py +199 -0
- app/core/openai/tool_call_safety.py +99 -0
- app/core/openai/v1_requests.py +187 -0
- app/core/plan_types.py +90 -0
- app/core/prestop.py +202 -0
- app/core/rate_limiter/__init__.py +0 -0
- app/core/rate_limiter/db_rate_limiter.py +215 -0
- app/core/request_locality.py +473 -0
- app/core/resilience/__init__.py +0 -0
- app/core/resilience/backpressure.py +57 -0
- app/core/resilience/bulkhead.py +202 -0
- app/core/resilience/circuit_breaker.py +150 -0
- app/core/resilience/degradation.py +55 -0
- app/core/resilience/loop_lag_monitor.py +60 -0
- app/core/resilience/memory_monitor.py +137 -0
- app/core/resilience/network_recovery.py +256 -0
- app/core/resilience/overload.py +98 -0
- app/core/resilience/toggles.py +114 -0
- app/core/retention/__init__.py +0 -0
- app/core/retention/job.py +240 -0
- app/core/retention/scheduler.py +69 -0
- app/core/runtime_logging.py +601 -0
- app/core/scheduling/__init__.py +0 -0
- app/core/scheduling/leader_election.py +993 -0
- app/core/scheduling/leader_election_handle.py +25 -0
- app/core/server.py +140 -0
- app/core/shutdown.py +326 -0
- app/core/socket_peer.py +22 -0
- app/core/startup.py +41 -0
- app/core/timeout_invariants.py +347 -0
- app/core/tracing/__init__.py +0 -0
- app/core/tracing/otel.py +207 -0
- app/core/types.py +6 -0
- app/core/upstream_proxy/__init__.py +13 -0
- app/core/upstream_proxy/cache.py +130 -0
- app/core/upstream_proxy/resolver.py +189 -0
- app/core/upstream_proxy/types.py +104 -0
- app/core/usage/__init__.py +407 -0
- app/core/usage/depletion.py +109 -0
- app/core/usage/live_hub.py +41 -0
- app/core/usage/live_snapshots.py +174 -0
- app/core/usage/logs.py +212 -0
- app/core/usage/metadata_scheduler.py +118 -0
- app/core/usage/models.py +54 -0
- app/core/usage/pricing.py +576 -0
- app/core/usage/pricing_catalog.py +241 -0
- app/core/usage/pricing_snapshot.json +821 -0
- app/core/usage/quota.py +133 -0
- app/core/usage/refresh_policy.py +21 -0
- app/core/usage/refresh_scheduler.py +713 -0
- app/core/usage/reset_credits_refresh_scheduler.py +435 -0
- app/core/usage/types.py +168 -0
- app/core/utils/__init__.py +0 -0
- app/core/utils/json_guards.py +18 -0
- app/core/utils/locks.py +33 -0
- app/core/utils/masking.py +20 -0
- app/core/utils/proxy_env.py +104 -0
- app/core/utils/request_id.py +65 -0
- app/core/utils/retry.py +77 -0
- app/core/utils/shared_future.py +172 -0
- app/core/utils/sse.py +286 -0
- app/core/utils/time.py +24 -0
- app/db/__init__.py +0 -0
- app/db/account_identity_lock.py +56 -0
- app/db/alembic/__init__.py +1 -0
- app/db/alembic/env.py +70 -0
- app/db/alembic/revision_ids.py +43 -0
- app/db/alembic/versions/20260213_000000_base_schema.py +248 -0
- app/db/alembic/versions/20260213_000100_normalize_account_plan_types.py +45 -0
- app/db/alembic/versions/20260213_000200_add_request_logs_reasoning_effort.py +45 -0
- app/db/alembic/versions/20260213_000300_add_accounts_reset_at.py +45 -0
- app/db/alembic/versions/20260213_000400_add_accounts_chatgpt_account_id.py +45 -0
- app/db/alembic/versions/20260213_000500_add_dashboard_settings.py +70 -0
- app/db/alembic/versions/20260213_000600_add_dashboard_settings_totp.py +64 -0
- app/db/alembic/versions/20260213_000700_add_dashboard_settings_password.py +59 -0
- app/db/alembic/versions/20260213_000800_add_api_keys.py +87 -0
- app/db/alembic/versions/20260214_000000_add_api_key_limits.py +143 -0
- app/db/alembic/versions/20260215_000000_add_idx_logs_requested_at.py +38 -0
- app/db/alembic/versions/20260218_000000_add_api_key_usage_reservations.py +117 -0
- app/db/alembic/versions/20260218_000100_add_import_without_overwrite_and_drop_accounts_email_unique.py +198 -0
- app/db/alembic/versions/20260225_000000_add_dashboard_settings_routing_strategy.py +53 -0
- app/db/alembic/versions/20260228_020000_align_api_key_limit_enum_types.py +104 -0
- app/db/alembic/versions/20260228_030000_add_api_firewall_allowlist.py +42 -0
- app/db/alembic/versions/20260306_000000_add_request_logs_service_tier.py +45 -0
- app/db/alembic/versions/20260307_000000_add_api_key_enforcement_fields.py +56 -0
- app/db/alembic/versions/20260308_000000_add_sqlite_performance_indexes.py +41 -0
- app/db/alembic/versions/20260309_000000_add_additional_usage_history.py +71 -0
- app/db/alembic/versions/20260309_000000_request_logs_nullable_account_id.py +28 -0
- app/db/alembic/versions/20260310_000000_add_request_logs_transport.py +45 -0
- app/db/alembic/versions/20260310_000000_fix_postgresql_enum_value_casing.py +79 -0
- app/db/alembic/versions/20260310_120000_add_sticky_session_kinds_and_affinity_ttl.py +110 -0
- app/db/alembic/versions/20260312_000000_add_additional_usage_quota_key.py +193 -0
- app/db/alembic/versions/20260312_000000_split_sticky_sessions_primary_key_by_kind.py +96 -0
- app/db/alembic/versions/20260312_010000_merge_additional_usage_and_sticky_session_heads.py +25 -0
- app/db/alembic/versions/20260312_020000_merge_request_logs_transport_and_feature_heads.py +25 -0
- app/db/alembic/versions/20260312_020000_merge_request_logs_transport_and_sticky_heads.py +25 -0
- app/db/alembic/versions/20260312_120000_add_dashboard_upstream_stream_transport.py +62 -0
- app/db/alembic/versions/20260313_024500_merge_request_log_transport_heads.py +27 -0
- app/db/alembic/versions/20260319_100937_increase_prompt_cache_ttl_to_1800s.py +73 -0
- app/db/alembic/versions/20260319_111500_merge_prompt_cache_ttl_and_upstream_transport_heads.py +25 -0
- app/db/alembic/versions/20260319_183000_normalize_sqlite_account_status_casing.py +48 -0
- app/db/alembic/versions/20260320_000000_add_request_log_requested_actual_tiers.py +51 -0
- app/db/alembic/versions/20260321_190000_tighten_dashboard_db_indexes.py +64 -0
- app/db/alembic/versions/20260321_210000_merge_request_log_tiers_and_dashboard_index_heads.py +25 -0
- app/db/alembic/versions/20260325_000000_add_request_log_cost.py +193 -0
- app/db/alembic/versions/20260328_000000_add_rate_limit_attempts.py +48 -0
- app/db/alembic/versions/20260328_120000_add_request_log_ttft.py +37 -0
- app/db/alembic/versions/20260328_130000_add_audit_logs_table.py +45 -0
- app/db/alembic/versions/20260328_140000_add_scheduler_leader_table.py +39 -0
- app/db/alembic/versions/20260330_000000_add_cache_locality_settings.py +69 -0
- app/db/alembic/versions/20260330_010000_merge_scheduler_leader_and_cache_locality_heads.py +25 -0
- app/db/alembic/versions/20260330_020000_add_bridge_ring_members.py +46 -0
- app/db/alembic/versions/20260401_000000_add_api_key_enforced_service_tier.py +52 -0
- app/db/alembic/versions/20260401_000000_add_cache_invalidation.py +41 -0
- app/db/alembic/versions/20260401_020000_merge_cache_invalidation_and_api_key_service_tier_heads.py +22 -0
- app/db/alembic/versions/20260402_000000_switch_dashboard_routing_default_to_capacity_weighted.py +66 -0
- app/db/alembic/versions/20260403_000000_add_credit_api_key_limit_values.py +44 -0
- app/db/alembic/versions/20260406_000000_add_api_key_account_assignments.py +57 -0
- app/db/alembic/versions/20260406_010000_add_api_key_assignment_scope_flag.py +75 -0
- app/db/alembic/versions/20260407_000000_add_bridge_gateway_safe_mode.py +51 -0
- app/db/alembic/versions/20260407_010000_merge_api_key_assignment_and_bridge_gateway_heads.py +25 -0
- app/db/alembic/versions/20260408_000000_switch_import_without_overwrite_default_to_true.py +73 -0
- app/db/alembic/versions/20260408_010000_merge_import_without_overwrite_and_assignment_heads.py +25 -0
- app/db/alembic/versions/20260409_000000_add_http_bridge_sessions.py +130 -0
- app/db/alembic/versions/20260409_000000_switch_sticky_threads_and_prefer_earlier_reset_defaults_to_true.py +89 -0
- app/db/alembic/versions/20260409_010000_add_dashboard_settings_bootstrap_token.py +57 -0
- app/db/alembic/versions/20260409_010000_merge_http_bridge_and_import_without_overwrite_heads.py +25 -0
- app/db/alembic/versions/20260409_020000_fix_http_bridge_last_seen_index.py +46 -0
- app/db/alembic/versions/20260410_000000_backfill_pristine_dashboard_settings_defaults.py +77 -0
- app/db/alembic/versions/20260410_030000_restore_import_without_overwrite_default_true.py +73 -0
- app/db/alembic/versions/20260410_040000_merge_dashboard_defaults_and_import_default_heads.py +25 -0
- app/db/alembic/versions/20260413_000000_add_accounts_blocked_at.py +44 -0
- app/db/alembic/versions/20260415_160000_add_automations_tables.py +189 -0
- app/db/alembic/versions/20260415_160000_add_request_logs_response_lookup_index.py +67 -0
- app/db/alembic/versions/20260417_000000_add_request_log_plan_type.py +44 -0
- app/db/alembic/versions/20260418_190000_add_automation_runs_cycle_key.py +87 -0
- app/db/alembic/versions/20260419_000000_add_automation_run_cycle_metadata.py +56 -0
- app/db/alembic/versions/20260419_020000_add_automation_run_cycles_snapshot_tables.py +342 -0
- app/db/alembic/versions/20260421_000000_add_dashboard_session_ttl_setting.py +51 -0
- app/db/alembic/versions/20260421_120000_merge_request_log_lookup_and_plan_type_heads.py +25 -0
- app/db/alembic/versions/20260421_130000_merge_automation_and_request_log_heads.py +25 -0
- app/db/alembic/versions/20260422_103000_normalize_legacy_automation_run_cycle_keys.py +648 -0
- app/db/alembic/versions/20260423_120000_add_api_key_limit_reset_at_index.py +43 -0
- app/db/alembic/versions/20260424_000000_merge_dashboard_session_ttl_and_request_log_heads.py +25 -0
- app/db/alembic/versions/20260426_000000_add_dashboard_relative_availability_settings.py +68 -0
- app/db/alembic/versions/20260427_130000_add_warmup_model_and_request_kind.py +75 -0
- app/db/alembic/versions/20260506_000000_add_account_routing_policy.py +37 -0
- app/db/alembic/versions/20260509_000000_add_api_key_traffic_class.py +37 -0
- app/db/alembic/versions/20260509_010000_add_additional_quota_routing_policies.py +37 -0
- app/db/alembic/versions/20260513_000000_add_accounts_alias.py +44 -0
- app/db/alembic/versions/20260513_000000_add_api_key_apply_to_codex_model.py +59 -0
- app/db/alembic/versions/20260514_000000_add_request_logs_api_key_time_index.py +43 -0
- app/db/alembic/versions/20260515_000000_add_prefer_earlier_reset_window.py +47 -0
- app/db/alembic/versions/20260515_000000_soft_delete_request_logs_on_account_delete.py +83 -0
- app/db/alembic/versions/20260515_020000_add_split_sticky_budget_thresholds.py +62 -0
- app/db/alembic/versions/20260516_000000_add_sqlite_hot_path_indexes.py +75 -0
- app/db/alembic/versions/20260517_000000_merge_sqlite_hot_path_and_request_log_delete_heads.py +24 -0
- app/db/alembic/versions/20260518_000000_add_http_bridge_durable_input_prefix.py +44 -0
- app/db/alembic/versions/20260518_010000_merge_http_bridge_and_sqlite_recovery_heads.py +25 -0
- app/db/alembic/versions/20260518_120000_add_dashboard_guest_access.py +57 -0
- app/db/alembic/versions/20260520_000000_merge_api_key_and_http_bridge_heads.py +26 -0
- app/db/alembic/versions/20260520_010000_add_request_logs_api_key_account_index.py +45 -0
- app/db/alembic/versions/20260520_010000_merge_automation_and_api_key_heads.py +26 -0
- app/db/alembic/versions/20260520_030000_add_quota_planner.py +194 -0
- app/db/alembic/versions/20260520_040000_merge_quota_planner_and_request_log_index_heads.py +24 -0
- app/db/alembic/versions/20260521_000000_add_account_security_work_authorized.py +46 -0
- app/db/alembic/versions/20260521_000000_merge_dashboard_guest_and_api_key_heads.py +26 -0
- app/db/alembic/versions/20260521_000000_merge_prefer_reset_window_head.py +26 -0
- app/db/alembic/versions/20260521_000000_merge_warmup_and_request_log_index_heads.py +25 -0
- app/db/alembic/versions/20260522_000000_add_limit_warmup_trigger.py +195 -0
- app/db/alembic/versions/20260524_000000_merge_accounts_alias_and_reset_window_heads.py +24 -0
- app/db/alembic/versions/20260525_000000_add_usage_raw_window_latest_index.py +31 -0
- app/db/alembic/versions/20260526_000000_add_request_log_failure_metadata.py +100 -0
- app/db/alembic/versions/20260531_000000_add_account_workspace_identity.py +39 -0
- app/db/alembic/versions/20260531_000000_add_single_account_routing.py +42 -0
- app/db/alembic/versions/20260601_000000_merge_relative_availability_and_usage_raw_heads.py +24 -0
- app/db/alembic/versions/20260601_010000_add_api_key_usage_sections.py +58 -0
- app/db/alembic/versions/20260601_010000_add_routing_policy_persistence.py +67 -0
- app/db/alembic/versions/20260601_010000_merge_security_work_and_relative_availability_heads.py +25 -0
- app/db/alembic/versions/20260601_010000_merge_split_sticky_and_relative_usage_heads.py +25 -0
- app/db/alembic/versions/20260601_010000_merge_warmup_and_relative_availability_heads.py +25 -0
- app/db/alembic/versions/20260601_020000_merge_additional_quota_routing_and_relative_availability_heads.py +25 -0
- app/db/alembic/versions/20260601_020000_merge_quota_planner_and_relative_availability_heads.py +25 -0
- app/db/alembic/versions/20260601_020000_merge_reset_window_and_relative_availability_heads.py +25 -0
- app/db/alembic/versions/20260601_020000_merge_routing_policy_and_split_threshold_heads.py +24 -0
- app/db/alembic/versions/20260601_020000_merge_warmup_and_request_log_failure_heads.py +25 -0
- app/db/alembic/versions/20260601_030000_merge_single_account_and_routing_quota_heads.py +29 -0
- app/db/alembic/versions/20260601_040000_merge_security_work_and_routing_consolidation_heads.py +25 -0
- app/db/alembic/versions/20260601_050000_merge_quota_planner_and_routing_consolidation_heads.py +25 -0
- app/db/alembic/versions/20260602_050000_add_upstream_proxy_routing.py +164 -0
- app/db/alembic/versions/20260602_060000_merge_account_workspace_and_failure_heads.py +25 -0
- app/db/alembic/versions/20260602_070000_add_request_log_useragent_fields.py +86 -0
- app/db/alembic/versions/20260602_070000_merge_quota_planner_and_account_workspace_heads.py +25 -0
- app/db/alembic/versions/20260602_080000_merge_upstream_proxy_and_quota_planner_heads.py +25 -0
- app/db/alembic/versions/20260602_080000_merge_useragent_and_upstream_proxy_heads.py +24 -0
- app/db/alembic/versions/20260603_000000_add_weekly_pace_working_days.py +52 -0
- app/db/alembic/versions/20260603_000000_free_account_monthly_window.py +60 -0
- app/db/alembic/versions/20260604_000000_add_reauth_required_account_status.py +159 -0
- app/db/alembic/versions/20260604_010000_merge_automations_and_reauth_status_heads.py +25 -0
- app/db/alembic/versions/20260604_010000_merge_dashboard_guest_and_reauth_status_heads.py +25 -0
- app/db/alembic/versions/20260604_010000_merge_useragent_and_reauth_heads.py +24 -0
- app/db/alembic/versions/20260607_000000_merge_weekly_monthly_useragent_heads.py +28 -0
- app/db/alembic/versions/20260608_000000_add_hide_upstream_quota_from_api_keys.py +50 -0
- app/db/alembic/versions/20260611_000000_add_request_log_archive_request_id.py +47 -0
- app/db/alembic/versions/20260611_000000_merge_automations_and_weekly_monthly_useragent_heads.py +26 -0
- app/db/alembic/versions/20260611_000000_merge_dashboard_guest_and_weekly_useragent_heads.py +26 -0
- app/db/alembic/versions/20260611_010000_add_request_log_client_ip.py +57 -0
- app/db/alembic/versions/20260611_020000_merge_request_log_archive_and_client_ip_heads.py +26 -0
- app/db/alembic/versions/20260611_030000_merge_dashboard_guest_and_request_log_client_ip_heads.py +26 -0
- app/db/alembic/versions/20260613_000000_add_accounts_codex_installation_id.py +57 -0
- app/db/alembic/versions/20260613_010000_merge_installation_id_and_dashboard_weekly_heads.py +26 -0
- app/db/alembic/versions/20260615_000000_merge_automations_and_dashboard_guest_heads.py +26 -0
- app/db/alembic/versions/20260623_000000_add_limit_warmup_exhausted_threshold.py +51 -0
- app/db/alembic/versions/20260626_000000_add_http_downstream_transport_policy.py +46 -0
- app/db/alembic/versions/20260626_010000_add_request_logs_upstream_transport.py +31 -0
- app/db/alembic/versions/20260629_000000_add_dashboard_query_hot_path_indexes.py +53 -0
- app/db/alembic/versions/20260630_000000_add_limit_warmup_staggered_idle.py +47 -0
- app/db/alembic/versions/20260630_000000_merge_request_log_client_ip_and_dashboard_hot_path_heads.py +26 -0
- app/db/alembic/versions/20260630_010000_merge_warmup_and_request_log_dashboard_heads.py +26 -0
- app/db/alembic/versions/20260630_020000_merge_automations_and_main_heads.py +26 -0
- app/db/alembic/versions/20260630_020000_merge_warmup_threshold_and_main_heads.py +26 -0
- app/db/alembic/versions/20260630_030000_add_automation_run_model_snapshot.py +59 -0
- app/db/alembic/versions/20260630_040000_merge_automation_snapshot_and_warmup_threshold_heads.py +26 -0
- app/db/alembic/versions/20260630_050000_add_automation_run_prompt_snapshot.py +52 -0
- app/db/alembic/versions/20260701_000000_add_weekly_pace_smoothing_minutes.py +51 -0
- app/db/alembic/versions/20260702_000000_add_model_sources.py +176 -0
- app/db/alembic/versions/20260705_000000_add_model_source_audio_transcriptions.py +50 -0
- app/db/alembic/versions/20260705_000000_harden_dashboard_session_ttl.py +62 -0
- app/db/alembic/versions/20260706_000000_add_model_source_audio_per_minute.py +43 -0
- app/db/alembic/versions/20260707_000000_merge_model_sources_and_session_ttl.py +24 -0
- app/db/alembic/versions/20260707_010000_merge_automations_and_model_sources_heads.py +24 -0
- app/db/alembic/versions/20260707_020000_add_automation_job_account_scope.py +125 -0
- app/db/alembic/versions/20260709_000000_add_ttft_phase_observability.py +66 -0
- app/db/alembic/versions/20260711_000000_add_dashboard_prohibit_fast_mode.py +42 -0
- app/db/alembic/versions/20260711_010000_add_account_chatgpt_user_id.py +40 -0
- app/db/alembic/versions/20260711_020000_add_dashboard_account_concurrency_caps.py +71 -0
- app/db/alembic/versions/20260711_030000_add_limit_warmup_idle_threshold.py +51 -0
- app/db/alembic/versions/20260712_010000_add_account_usage_rollups.py +56 -0
- app/db/alembic/versions/20260712_020000_add_api_key_usage_rollups.py +53 -0
- app/db/alembic/versions/20260713_020000_add_model_registry_snapshot.py +42 -0
- app/db/alembic/versions/20260713_040000_add_account_refresh_claims.py +42 -0
- app/db/alembic/versions/20260713_040000_add_replica_guardrails.py +64 -0
- app/db/alembic/versions/20260713_070000_add_reset_credit_redeem_tables.py +58 -0
- app/db/alembic/versions/20260714_000000_add_oauth_flow_states.py +63 -0
- app/db/alembic/versions/20260715_000000_add_request_log_queue_latency.py +44 -0
- app/db/alembic/versions/20260716_000000_add_oauth_device_flow_slots.py +41 -0
- app/db/alembic/versions/20260716_010000_add_dashboard_retention_settings.py +48 -0
- app/db/alembic/versions/20260716_010000_add_reset_credit_display_settings.py +73 -0
- app/db/alembic/versions/20260717_000000_add_http_bridge_retry_circuits.py +47 -0
- app/db/alembic/versions/20260717_000000_merge_retention_and_reset_credit_display_heads.py +26 -0
- app/db/alembic/versions/20260717_000000_optimize_dashboard_hot_path_indexes.py +162 -0
- app/db/alembic/versions/20260717_000001_merge_retry_circuits_and_dashboard_indexes.py +22 -0
- app/db/alembic/versions/20260720_000000_add_request_log_conversation_id.py +57 -0
- app/db/alembic/versions/20260722_000000_backfill_request_log_useragent_families.py +40 -0
- app/db/alembic/versions/20260724_000000_add_request_usage_time_rollups.py +176 -0
- app/db/alembic/versions/20260724_000000_merge_request_log_schema_heads.py +33 -0
- app/db/alembic/versions/20260725_000000_add_http_bridge_pending_tool_calls.py +43 -0
- app/db/alembic/versions/20260726_000000_add_account_plan_downgrade_observations.py +44 -0
- app/db/alembic/versions/20260726_000000_repair_request_usage_rollups_after_merge.py +37 -0
- app/db/alembic/versions/20260727_000000_add_sticky_session_continuity_abandoned_at.py +45 -0
- app/db/alembic/versions/20260728_000000_merge_pending_tool_calls_and_rollup_repair_heads.py +31 -0
- app/db/alembic/versions/20260730_000000_add_api_key_fair_share_threshold.py +44 -0
- app/db/alembic/versions/20260730_000000_add_http_bridge_recovery_attempts.py +60 -0
- app/db/alembic/versions/20260731_000000_add_capability_lineage_markers.py +38 -0
- app/db/alembic/versions/20260803_000000_merge_http_bridge_recovery_and_capability_lineage_heads.py +30 -0
- app/db/alembic/versions/20260804_000000_add_http_bridge_operations.py +63 -0
- app/db/alembic/versions/20260804_000001_add_global_http_bridge_operation_fingerprint.py +53 -0
- app/db/alembic/versions/20260804_230000_add_request_log_connection_request_kind.py +43 -0
- app/db/alembic/versions/20260805_000000_add_http_bridge_operation_spool.py +75 -0
- app/db/alembic/versions/20260805_000001_finalize_http_bridge_operation_spool.py +65 -0
- app/db/alembic/versions/20260806_000000_add_additional_usage_alias_probe_indexes.py +84 -0
- app/db/alembic/versions/20260806_000000_add_anonymous_telemetry.py +49 -0
- app/db/alembic/versions/20260806_010000_add_conversation_presence_rollup.py +72 -0
- app/db/alembic/versions/20260806_020000_add_usage_history_bulk_covering_indexes.py +99 -0
- app/db/alembic/versions/20260806_030000_add_api_key_allowed_reasoning_efforts.py +65 -0
- app/db/alembic/versions/20260806_120000_add_http_bridge_owner_process_epoch.py +57 -0
- app/db/alembic/versions/20260807_000000_merge_http_bridge_operation_spool_and_latest_main.py +24 -0
- app/db/alembic/versions/20260808_000000_tune_usage_history_autovacuum.py +54 -0
- app/db/alembic/versions/20260810_000000_add_http_bridge_recovery_dispatch_count.py +44 -0
- app/db/alembic/versions/20260811_000000_add_hourly_rollup_cancelled_count.py +92 -0
- app/db/alembic/versions/20260812_000000_merge_recovery_dispatch_and_hourly_cancelled_heads.py +25 -0
- app/db/alembic/versions/20260812_120000_add_sticky_abandonment_scope.py +43 -0
- app/db/alembic/versions/20260813_000000_add_file_account_pins.py +40 -0
- app/db/alembic/versions/20260816_000000_add_account_pending_deletion.py +81 -0
- app/db/alembic/versions/20260816_000000_add_model_source_embeddings.py +50 -0
- app/db/alembic/versions/20260821_000000_add_retry_circuit_admission_generation.py +49 -0
- app/db/alembic/versions/20260826_000000_add_http_bridge_event_chunks.py +107 -0
- app/db/alembic/versions/20260828_000000_add_accounts_chatgpt_identity_index.py +55 -0
- app/db/alembic/versions/20260830_000000_add_quota_warmup_claim_expiry.py +83 -0
- app/db/alembic/versions/20260908_000000_add_guest_session_generation.py +37 -0
- app/db/alembic/versions/20260908_000000_add_subscription_overflow.py +140 -0
- app/db/alembic/versions/20260908_000000_replace_upstream_stream_transport_default_sentinel.py +50 -0
- app/db/alembic/versions/20260908_020000_merge_overflow_transport_heads.py +27 -0
- app/db/alembic/versions/20260909_000000_add_dashboard_roles.py +72 -0
- app/db/alembic/versions/20260909_010000_add_dashboard_users.py +183 -0
- app/db/alembic/versions/20260909_020000_reproject_compat_admin_credentials.py +105 -0
- app/db/alembic/versions/20260909_030000_add_audit_actor_columns.py +103 -0
- app/db/alembic/versions/20260909_030000_dashboard_resilience_toggle_settings.py +63 -0
- app/db/alembic/versions/20260909_040000_add_dashboard_user_invites.py +54 -0
- app/db/alembic/versions/20260909_040000_dashboard_timeout_settings.py +68 -0
- app/db/alembic/versions/20260909_050000_dashboard_routing_overload_settings.py +65 -0
- app/db/alembic/versions/20260909_060000_add_report_rollup.py +74 -0
- app/db/alembic/versions/20260909_070000_automation_run_claim_budget.py +50 -0
- app/db/alembic/versions/20260909_080000_dashboard_stream_bridge_budgets.py +62 -0
- app/db/alembic/versions/20260909_090000_dashboard_background_job_toggles.py +63 -0
- app/db/alembic/versions/20260909_100000_dashboard_codex_prewarm.py +49 -0
- app/db/alembic/versions/20260909_110000_model_context_window_overrides.py +45 -0
- app/db/alembic/versions/20260909_120000_dashboard_conversation_archive.py +48 -0
- app/db/alembic/versions/20260909_130000_add_request_logs_live_facet_indexes.py +96 -0
- app/db/alembic/versions/20260910_000000_add_totp_required_for_admin_role.py +44 -0
- app/db/alembic/versions/20260910_000000_request_logs_missing_cost_index.py +52 -0
- app/db/alembic/versions/20260910_010000_add_dashboard_auth_providers.py +134 -0
- app/db/alembic/versions/20260910_010000_dashboard_spool_retention.py +52 -0
- app/db/alembic/versions/20260910_020000_add_dashboard_role_mappings.py +74 -0
- app/db/alembic/versions/20260910_143000_request_log_affinity.py +28 -0
- app/db/alembic/versions/20260910_180000_merge_affinity_guest_heads.py +17 -0
- app/db/alembic/versions/20260910_200000_merge_affinity_identity_heads.py +17 -0
- app/db/alembic/versions/20260910_220000_merge_affinity_invite_heads.py +17 -0
- app/db/alembic/versions/20260911_000000_model_source_pins_kind_expires_index.py +43 -0
- app/db/alembic/versions/20260911_010000_merge_pin_index_and_affinity_heads.py +17 -0
- app/db/alembic/versions/20260911_020000_add_http_bridge_terminal_append_phase.py +54 -0
- app/db/alembic/versions/20260911_030000_add_local_login_policy.py +48 -0
- app/db/alembic/versions/20260911_040000_add_thread_cache_identity_mode.py +48 -0
- app/db/alembic/versions/20260911_060000_add_bridge_session_continuity_abandonment.py +40 -0
- app/db/alembic/versions/20260912_000000_merge_thread_cache_and_bridge_retirement_heads.py +22 -0
- app/db/alembic/versions/20260912_010000_drop_legacy_dashboard_credentials.py +140 -0
- app/db/alembic/versions/20260913_000000_add_oidc_provider_flow.py +134 -0
- app/db/alembic/versions/20260914_000000_add_scim_tokens.py +78 -0
- app/db/alembic/versions/20260914_000000_drop_subscription_overflow_schema.py +132 -0
- app/db/alembic/versions/20260918_000000_merge_scim_and_overflow_heads.py +17 -0
- app/db/alembic/versions/__init__.py +1 -0
- app/db/backup.py +64 -0
- app/db/migrate.py +1249 -0
- app/db/migration_lock.py +160 -0
- app/db/migration_url.py +18 -0
- app/db/models.py +3100 -0
- app/db/recover.py +244 -0
- app/db/session.py +1295 -0
- app/db/snapshot.py +46 -0
- app/db/sqlite_lock_retry.py +190 -0
- app/db/sqlite_utils.py +549 -0
- app/dependencies.py +462 -0
- app/main.py +1240 -0
- app/modules/__init__.py +0 -0
- app/modules/accounts/__init__.py +0 -0
- app/modules/accounts/api.py +426 -0
- app/modules/accounts/auth_manager.py +1439 -0
- app/modules/accounts/background_repository.py +160 -0
- app/modules/accounts/deletion.py +540 -0
- app/modules/accounts/mappers.py +654 -0
- app/modules/accounts/refresh_claims.py +352 -0
- app/modules/accounts/repository.py +1679 -0
- app/modules/accounts/schemas.py +308 -0
- app/modules/accounts/service.py +809 -0
- app/modules/accounts/usage_rollup.py +486 -0
- app/modules/accounts/usage_rollup_scheduler.py +75 -0
- app/modules/accounts/usage_time_rollup.py +1037 -0
- app/modules/accounts/usage_time_rollup_read.py +522 -0
- app/modules/api_keys/__init__.py +1 -0
- app/modules/api_keys/api.py +344 -0
- app/modules/api_keys/last_used_coalescer.py +216 -0
- app/modules/api_keys/limit_windows.py +41 -0
- app/modules/api_keys/repository.py +1200 -0
- app/modules/api_keys/reset_scheduler.py +85 -0
- app/modules/api_keys/schemas.py +135 -0
- app/modules/api_keys/service.py +2123 -0
- app/modules/audit/__init__.py +0 -0
- app/modules/audit/api.py +88 -0
- app/modules/audit/repository.py +74 -0
- app/modules/audit/schemas.py +33 -0
- app/modules/audit/service.py +71 -0
- app/modules/auth_providers/__init__.py +0 -0
- app/modules/auth_providers/api.py +127 -0
- app/modules/auth_providers/config.py +343 -0
- app/modules/auth_providers/repository.py +101 -0
- app/modules/auth_providers/schemas.py +88 -0
- app/modules/auth_providers/seed.py +105 -0
- app/modules/auth_providers/service.py +289 -0
- app/modules/automations/__init__.py +1 -0
- app/modules/automations/api.py +369 -0
- app/modules/automations/repository.py +1896 -0
- app/modules/automations/scheduler.py +107 -0
- app/modules/automations/schemas.py +158 -0
- app/modules/automations/service.py +2548 -0
- app/modules/cache_isolation_probe/__init__.py +0 -0
- app/modules/cache_isolation_probe/api.py +188 -0
- app/modules/cache_isolation_probe/prefix.py +144 -0
- app/modules/cache_isolation_probe/schemas.py +94 -0
- app/modules/cache_isolation_probe/sender.py +229 -0
- app/modules/cache_isolation_probe/service.py +525 -0
- app/modules/conversation_archive/__init__.py +1 -0
- app/modules/conversation_archive/api.py +101 -0
- app/modules/conversation_archive/schemas.py +28 -0
- app/modules/conversation_archive/service.py +165 -0
- app/modules/dashboard/__init__.py +1 -0
- app/modules/dashboard/api.py +106 -0
- app/modules/dashboard/builders.py +107 -0
- app/modules/dashboard/repository.py +304 -0
- app/modules/dashboard/schemas.py +148 -0
- app/modules/dashboard/service.py +534 -0
- app/modules/dashboard/timeframes.py +19 -0
- app/modules/dashboard/weekly_pace.py +617 -0
- app/modules/dashboard_auth/__init__.py +1 -0
- app/modules/dashboard_auth/api.py +1344 -0
- app/modules/dashboard_auth/oidc_api.py +753 -0
- app/modules/dashboard_auth/oidc_flows.py +330 -0
- app/modules/dashboard_auth/repository.py +452 -0
- app/modules/dashboard_auth/schemas.py +232 -0
- app/modules/dashboard_auth/service.py +1384 -0
- app/modules/dashboard_roles/__init__.py +0 -0
- app/modules/dashboard_roles/api.py +64 -0
- app/modules/dashboard_roles/repository.py +54 -0
- app/modules/dashboard_roles/schemas.py +29 -0
- app/modules/dashboard_roles/seed.py +80 -0
- app/modules/dashboard_roles/service.py +142 -0
- app/modules/dashboard_users/__init__.py +0 -0
- app/modules/dashboard_users/api.py +278 -0
- app/modules/dashboard_users/break_glass.py +169 -0
- app/modules/dashboard_users/credentials.py +18 -0
- app/modules/dashboard_users/identity_resolver.py +665 -0
- app/modules/dashboard_users/repository.py +725 -0
- app/modules/dashboard_users/schemas.py +117 -0
- app/modules/dashboard_users/service.py +1054 -0
- app/modules/firewall/__init__.py +1 -0
- app/modules/firewall/api.py +78 -0
- app/modules/firewall/repository.py +51 -0
- app/modules/firewall/schemas.py +28 -0
- app/modules/firewall/service.py +93 -0
- app/modules/fleet/__init__.py +1 -0
- app/modules/fleet/api.py +177 -0
- app/modules/fleet/mappers.py +56 -0
- app/modules/fleet/observability.py +372 -0
- app/modules/fleet/schemas.py +119 -0
- app/modules/health/__init__.py +0 -0
- app/modules/health/api.py +246 -0
- app/modules/health/schemas.py +27 -0
- app/modules/limit_warmup/__init__.py +1 -0
- app/modules/limit_warmup/repository.py +159 -0
- app/modules/limit_warmup/service.py +1001 -0
- app/modules/model_sources/__init__.py +1 -0
- app/modules/model_sources/api.py +99 -0
- app/modules/model_sources/catalog.py +307 -0
- app/modules/model_sources/forwarding.py +1435 -0
- app/modules/model_sources/projection.py +55 -0
- app/modules/model_sources/repository.py +201 -0
- app/modules/model_sources/schemas.py +79 -0
- app/modules/model_sources/selection.py +166 -0
- app/modules/model_sources/service.py +236 -0
- app/modules/oauth/__init__.py +0 -0
- app/modules/oauth/api.py +101 -0
- app/modules/oauth/repository.py +238 -0
- app/modules/oauth/schemas.py +49 -0
- app/modules/oauth/service.py +1196 -0
- app/modules/oauth/templates/oauth_success.html +122 -0
- app/modules/proxy/__init__.py +0 -0
- app/modules/proxy/_load_balancer/__init__.py +1 -0
- app/modules/proxy/_load_balancer/error_rate.py +116 -0
- app/modules/proxy/_load_balancer/latency_cohort.py +154 -0
- app/modules/proxy/_load_balancer/model_eligibility.py +250 -0
- app/modules/proxy/_load_balancer/opportunistic_admission.py +350 -0
- app/modules/proxy/_load_balancer/overload_backoff.py +394 -0
- app/modules/proxy/_load_balancer/sticky_selection.py +2417 -0
- app/modules/proxy/_load_balancer/throughput_cohort.py +233 -0
- app/modules/proxy/_load_balancer/ttft_cohort.py +214 -0
- app/modules/proxy/_load_balancer/tunables.py +97 -0
- app/modules/proxy/_load_balancer/types.py +96 -0
- app/modules/proxy/_load_balancer/unbound_selection.py +534 -0
- app/modules/proxy/_service/__init__.py +6 -0
- app/modules/proxy/_service/api_key_usage.py +745 -0
- app/modules/proxy/_service/clock_budget.py +38 -0
- app/modules/proxy/_service/codex_control.py +611 -0
- app/modules/proxy/_service/compact.py +2211 -0
- app/modules/proxy/_service/file_ops.py +749 -0
- app/modules/proxy/_service/http_bridge/__init__.py +3 -0
- app/modules/proxy/_service/http_bridge/accepted_replay.py +331 -0
- app/modules/proxy/_service/http_bridge/account_sessions.py +57 -0
- app/modules/proxy/_service/http_bridge/activity.py +178 -0
- app/modules/proxy/_service/http_bridge/helpers.py +3959 -0
- app/modules/proxy/_service/http_bridge/mixin.py +2435 -0
- app/modules/proxy/_service/http_bridge/owner_forwarding.py +609 -0
- app/modules/proxy/_service/http_bridge/protocol.py +167 -0
- app/modules/proxy/_service/http_bridge/proxy_failover.py +58 -0
- app/modules/proxy/_service/http_bridge/quarantine.py +443 -0
- app/modules/proxy/_service/http_bridge/request_submit.py +4582 -0
- app/modules/proxy/_service/http_bridge/retry_circuit.py +2442 -0
- app/modules/proxy/_service/http_bridge/service_stubs.py +492 -0
- app/modules/proxy/_service/http_bridge/session_registry.py +778 -0
- app/modules/proxy/_service/http_bridge/streaming.py +5328 -0
- app/modules/proxy/_service/http_bridge/upstream_events.py +4538 -0
- app/modules/proxy/_service/observability.py +410 -0
- app/modules/proxy/_service/rate_limit.py +361 -0
- app/modules/proxy/_service/realtime_live.py +655 -0
- app/modules/proxy/_service/refresh.py +154 -0
- app/modules/proxy/_service/request_log.py +585 -0
- app/modules/proxy/_service/response_create.py +930 -0
- app/modules/proxy/_service/streaming/__init__.py +3 -0
- app/modules/proxy/_service/streaming/helpers.py +1317 -0
- app/modules/proxy/_service/streaming/mixin.py +1100 -0
- app/modules/proxy/_service/streaming/protocol.py +32 -0
- app/modules/proxy/_service/streaming/retry.py +3762 -0
- app/modules/proxy/_service/support.py +2122 -0
- app/modules/proxy/_service/transcribe.py +434 -0
- app/modules/proxy/_service/warmup.py +483 -0
- app/modules/proxy/_service/websocket/__init__.py +3 -0
- app/modules/proxy/_service/websocket/helpers.py +2360 -0
- app/modules/proxy/_service/websocket/mixin.py +7285 -0
- app/modules/proxy/_service/websocket/protocol.py +74 -0
- app/modules/proxy/account_cache.py +229 -0
- app/modules/proxy/account_eligibility.py +87 -0
- app/modules/proxy/additional_model_limits.py +40 -0
- app/modules/proxy/affinity.py +830 -0
- app/modules/proxy/affinity_observation.py +101 -0
- app/modules/proxy/api.py +10254 -0
- app/modules/proxy/api_key_usage.py +113 -0
- app/modules/proxy/cap_partitioning.py +280 -0
- app/modules/proxy/capability_lineage.py +65 -0
- app/modules/proxy/capability_lineage_repository.py +82 -0
- app/modules/proxy/capability_routing.py +253 -0
- app/modules/proxy/continuity.py +112 -0
- app/modules/proxy/downstream_delivery.py +277 -0
- app/modules/proxy/durable_bridge_coordinator.py +1217 -0
- app/modules/proxy/durable_bridge_repository.py +4516 -0
- app/modules/proxy/durable_bridge_runtime.py +9 -0
- app/modules/proxy/durable_bridge_transcript_codec.py +122 -0
- app/modules/proxy/fair_share.py +131 -0
- app/modules/proxy/file_pin_repository.py +210 -0
- app/modules/proxy/helpers.py +439 -0
- app/modules/proxy/http_bridge_event_batcher.py +613 -0
- app/modules/proxy/http_bridge_forwarding.py +797 -0
- app/modules/proxy/http_continuation.py +76 -0
- app/modules/proxy/images_observability.py +62 -0
- app/modules/proxy/images_service.py +1054 -0
- app/modules/proxy/load_balancer.py +3016 -0
- app/modules/proxy/rate_limit_cache.py +46 -0
- app/modules/proxy/replay_safety.py +1063 -0
- app/modules/proxy/repo_bundle.py +31 -0
- app/modules/proxy/request_policy.py +1054 -0
- app/modules/proxy/ring_membership.py +251 -0
- app/modules/proxy/schemas.py +345 -0
- app/modules/proxy/selection_errors.py +41 -0
- app/modules/proxy/service.py +2600 -0
- app/modules/proxy/source_admission.py +125 -0
- app/modules/proxy/source_dispatch.py +999 -0
- app/modules/proxy/sticky_repository.py +831 -0
- app/modules/proxy/thread_anchors.py +491 -0
- app/modules/proxy/tool_call_dedupe.py +802 -0
- app/modules/proxy/types.py +54 -0
- app/modules/proxy/work_admission.py +121 -0
- app/modules/quota_planner/__init__.py +1 -0
- app/modules/quota_planner/api.py +276 -0
- app/modules/quota_planner/logic.py +743 -0
- app/modules/quota_planner/repository.py +722 -0
- app/modules/quota_planner/scheduler.py +212 -0
- app/modules/quota_planner/schemas.py +102 -0
- app/modules/quota_planner/warmup.py +693 -0
- app/modules/rate_limit_reset_credits/__init__.py +0 -0
- app/modules/rate_limit_reset_credits/api.py +601 -0
- app/modules/rate_limit_reset_credits/redeem_coordination.py +274 -0
- app/modules/rate_limit_reset_credits/store.py +114 -0
- app/modules/reports/__init__.py +1 -0
- app/modules/reports/api.py +134 -0
- app/modules/reports/cache.py +74 -0
- app/modules/reports/filters.py +27 -0
- app/modules/reports/repository.py +525 -0
- app/modules/reports/rollup.py +150 -0
- app/modules/reports/rollup_read.py +148 -0
- app/modules/reports/schemas.py +123 -0
- app/modules/reports/service.py +287 -0
- app/modules/reports/thread_identity.py +256 -0
- app/modules/request_logs/__init__.py +0 -0
- app/modules/request_logs/api.py +224 -0
- app/modules/request_logs/cost_backfill.py +154 -0
- app/modules/request_logs/mappers.py +97 -0
- app/modules/request_logs/repository.py +1765 -0
- app/modules/request_logs/schemas.py +145 -0
- app/modules/request_logs/service.py +348 -0
- app/modules/role_mappings/__init__.py +0 -0
- app/modules/role_mappings/api.py +206 -0
- app/modules/role_mappings/matching.py +55 -0
- app/modules/role_mappings/repository.py +106 -0
- app/modules/role_mappings/schemas.py +67 -0
- app/modules/role_mappings/service.py +334 -0
- app/modules/runtime/__init__.py +1 -0
- app/modules/runtime/api.py +18 -0
- app/modules/runtime/schemas.py +14 -0
- app/modules/runtime/service.py +218 -0
- app/modules/scim/__init__.py +1 -0
- app/modules/scim/api.py +194 -0
- app/modules/scim/dependencies.py +112 -0
- app/modules/scim/errors.py +77 -0
- app/modules/scim/management_api.py +156 -0
- app/modules/scim/repository.py +212 -0
- app/modules/scim/schemas.py +145 -0
- app/modules/scim/service.py +786 -0
- app/modules/scim/tokens.py +43 -0
- app/modules/settings/__init__.py +1 -0
- app/modules/settings/api.py +1677 -0
- app/modules/settings/repository.py +606 -0
- app/modules/settings/schemas.py +496 -0
- app/modules/settings/service.py +769 -0
- app/modules/shared/__init__.py +0 -0
- app/modules/shared/schemas.py +22 -0
- app/modules/sticky_sessions/__init__.py +1 -0
- app/modules/sticky_sessions/api.py +118 -0
- app/modules/sticky_sessions/cleanup_scheduler.py +487 -0
- app/modules/sticky_sessions/schemas.py +78 -0
- app/modules/sticky_sessions/service.py +196 -0
- app/modules/telemetry/__init__.py +5 -0
- app/modules/telemetry/api.py +115 -0
- app/modules/telemetry/clients.py +62 -0
- app/modules/telemetry/consent.py +115 -0
- app/modules/telemetry/scheduler.py +85 -0
- app/modules/telemetry/schemas.py +163 -0
- app/modules/telemetry/sender.py +221 -0
- app/modules/telemetry/snapshot.py +490 -0
- app/modules/usage/__init__.py +0 -0
- app/modules/usage/additional_quota_keys.py +379 -0
- app/modules/usage/api.py +45 -0
- app/modules/usage/background_repository.py +147 -0
- app/modules/usage/builders.py +559 -0
- app/modules/usage/depletion_service.py +277 -0
- app/modules/usage/live_ingest.py +356 -0
- app/modules/usage/mappers.py +29 -0
- app/modules/usage/plan_downgrade_observations.py +488 -0
- app/modules/usage/repository.py +1890 -0
- app/modules/usage/schemas.py +68 -0
- app/modules/usage/service.py +102 -0
- app/modules/usage/updater.py +1488 -0
- app/static/assets/Trans-Cj-J9mVA.js +1 -0
- app/static/assets/access-page-DHzZiXZi.js +1 -0
- app/static/assets/account-cost-donut-DGhfA4U8.js +1 -0
- app/static/assets/account-identifiers-CxbdLshZ.js +1 -0
- app/static/assets/account-status-DIsU5sUS.js +1 -0
- app/static/assets/account-trend-chart-BptlfwwQ.js +1 -0
- app/static/assets/accounts-page-Bbai8ift.js +3 -0
- app/static/assets/api-Dwa7XxPe.js +1 -0
- app/static/assets/api-client-dugX1Pti.js +55 -0
- app/static/assets/api-key-create-dialog-DnRm0oYN.js +1 -0
- app/static/assets/api-key-created-dialog-B6jkDG9f.js +1 -0
- app/static/assets/api-key-edit-dialog-Ck3Bnc3V.js +1 -0
- app/static/assets/api-trend-chart-BchCw7Hj.js +1 -0
- app/static/assets/apis-page-BvXz9c6i.js +2 -0
- app/static/assets/arrow-up-Dqh_KzJo.js +1 -0
- app/static/assets/arrow-up-down-LBRgVDGY.js +1 -0
- app/static/assets/automations-page-DQgNAJOR.js +3 -0
- app/static/assets/badge-DqMb9es0.js +1 -0
- app/static/assets/blur-email-HEvKagG2.js +1 -0
- app/static/assets/button-BjVmmiGW.js +1 -0
- app/static/assets/checkbox-apsl9vzk.js +1 -0
- app/static/assets/chevron-left-1d_pHVa5.js +1 -0
- app/static/assets/circle-alert-BS0xd_o1.js +1 -0
- app/static/assets/clsx-DB0hHKMi.js +1 -0
- app/static/assets/colors-BBy8GEtr.js +1 -0
- app/static/assets/copy-button-D7nAvOKR.js +1 -0
- app/static/assets/cost-per-day-chart-YdqlkZVM.js +1 -0
- app/static/assets/createLucideIcon-DBLzLNh2.js +1 -0
- app/static/assets/dashboard-page-CSdhBy0f.js +2 -0
- app/static/assets/distribution-metric-format-yZTy1ZEu.js +1 -0
- app/static/assets/donut-chart-Bxc69cra.js +1 -0
- app/static/assets/download-kQteIcsB.js +1 -0
- app/static/assets/ellipsis-B_5OgIre.js +1 -0
- app/static/assets/empty-state-JcA7tc8m.js +1 -0
- app/static/assets/es2015-bMH8YKZT.js +41 -0
- app/static/assets/es6-B264ni6W.js +69 -0
- app/static/assets/external-link-UVAOgMQ0.js +1 -0
- app/static/assets/i18n-BWqZ9Ua2.js +1 -0
- app/static/assets/inbox-B_RM2s1J.js +1 -0
- app/static/assets/index-DIpd264Z.js +23 -0
- app/static/assets/index-DMeLKRJx.css +2 -0
- app/static/assets/inherit-badge-QhTYVaU6.js +1 -0
- app/static/assets/invite-dialog-0CYh0GiQ.js +1 -0
- app/static/assets/iso-Bpe5Ihbb.js +1 -0
- app/static/assets/label-CGaLmLNf.js +1 -0
- app/static/assets/lazy-recharts-Pv8p9ArM.js +2 -0
- app/static/assets/loading-overlay-CzKQfHLI.js +1 -0
- app/static/assets/mini-quota-bar-cGT_t8Ae.js +1 -0
- app/static/assets/model-distribution-donut-BKjAfT48.js +1 -0
- app/static/assets/model-source-multi-select-D2d06Vc7.js +1 -0
- app/static/assets/multi-select-filter-KIA5qU6Z.js +1 -0
- app/static/assets/oauth-dialog-XeaGGDjS.js +1 -0
- app/static/assets/pagination-controls-D1Ypd0RU.js +1 -0
- app/static/assets/payload-CIYnGZz9.js +1 -0
- app/static/assets/queue-wait-chart-VoV7Yg9L.js +1 -0
- app/static/assets/report-chart-card-BXDliwcc.js +1 -0
- app/static/assets/reports-page-CGCJeMHG.js +3 -0
- app/static/assets/reset-credit-confirm-dialog-QOifTnVB.js +1 -0
- app/static/assets/rolldown-runtime-hePW80VL.js +1 -0
- app/static/assets/search-HfhqwERt.js +1 -0
- app/static/assets/select-Cl33bz-g.js +1 -0
- app/static/assets/settings-page-mZj01HYl.js +2 -0
- app/static/assets/shim-CZwyM4Q7.js +1 -0
- app/static/assets/skeleton-C_EUc4Rp.js +1 -0
- app/static/assets/sparkline-chart-DdNNsddM.js +1 -0
- app/static/assets/status-badge-CHHA5X6c.js +1 -0
- app/static/assets/switch-CzxgokcM.js +1 -0
- app/static/assets/table-DXaNSQ0G.js +1 -0
- app/static/assets/time-to-first-token-chart--68L0u_v.js +1 -0
- app/static/assets/tokens-per-day-chart-Cs82BFuE.js +1 -0
- app/static/assets/tokens-per-second-chart-AD2dfOn6.js +1 -0
- app/static/assets/totp-settings-CRHtxQ7j.js +1 -0
- app/static/assets/trash-D3KyWr1h.js +1 -0
- app/static/assets/use-account-quota-display-BPmrTBTC.js +1 -0
- app/static/assets/use-accounts-PWlVGw3z.js +1 -0
- app/static/assets/use-auth-CuDlW3iC.js +1 -0
- app/static/assets/use-chart-colors-DIu6tClI.js +1 -0
- app/static/assets/use-model-sources-Bul24JBu.js +1 -0
- app/static/assets/use-models-DUCPUuZg.js +1 -0
- app/static/assets/use-reduced-motion-hAm5r6_d.js +1 -0
- app/static/assets/useTranslation--56nHhlH.js +1 -0
- app/static/assets/useragent-distribution-donut-Dup6sf_n.js +1 -0
- app/static/assets/users-uIC3TIlE.js +1 -0
- app/static/assets/vendor-query-_bHZuxEx.js +1 -0
- app/static/assets/vendor-react-BVL0YZPW.js +9 -0
- app/static/favicon.svg +10 -0
- app/static/fonts/GeistSans-Variable.woff2 +0 -0
- app/static/fonts/jetbrains-mono-latin-ext.woff2 +0 -0
- app/static/fonts/jetbrains-mono-latin.woff2 +0 -0
- app/static/index.html +29 -0
- codexlb_up-1.25.0b10.dist-info/METADATA +124 -0
- codexlb_up-1.25.0b10.dist-info/RECORD +827 -0
- codexlb_up-1.25.0b10.dist-info/WHEEL +4 -0
- codexlb_up-1.25.0b10.dist-info/entry_points.txt +3 -0
- codexlb_up-1.25.0b10.dist-info/licenses/LICENSE +21 -0
- config/additional_quota_registry.json +11 -0
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
"""Background account deletion: fast-marked accounts drained in bounded chunks.
|
|
2
|
+
|
|
3
|
+
``DELETE /api/accounts/{id}`` used to detach (or delete) the account's entire
|
|
4
|
+
raw history in ONE transaction while holding the fold-state lock: for a
|
|
5
|
+
long-lived account that is hundreds of thousands of rows across ``request_logs``
|
|
6
|
+
(18 indexes) and ``usage_history``, minutes of fold blockage, one pinned pool
|
|
7
|
+
connection, and an HTTP client timeout. The API now only stamps the
|
|
8
|
+
pending-deletion marker (``AccountsRepository.begin_delete``); this module
|
|
9
|
+
drains the bulk rows afterwards, ``DELETE_BATCH_SIZE`` rows per transaction,
|
|
10
|
+
and finalizes with the exact transaction shape the synchronous path used.
|
|
11
|
+
|
|
12
|
+
Fold-safety argument — why the chunk transactions do NOT take the fold-state
|
|
13
|
+
lock, yet a deleted account's folded rows can never resurrect:
|
|
14
|
+
|
|
15
|
+
1. Chunk transactions touch only raw rows (``usage_history``,
|
|
16
|
+
``additional_usage_history``, ``request_logs``); they never write a rollup
|
|
17
|
+
table and never move a watermark. ``usage_history`` tables are not
|
|
18
|
+
fold-governed at all.
|
|
19
|
+
2. A fold slice that interleaves between chunks may aggregate rows still
|
|
20
|
+
attributed to the account (adding folded rows under the account dimension)
|
|
21
|
+
or rows a chunk already detached (adding them under the orphaned-deleted
|
|
22
|
+
dimension — exactly the soft-path end state). Both are converged by the
|
|
23
|
+
finalization transaction: it takes ``lock_fold_state()`` and only then
|
|
24
|
+
detaches/deletes the residual raw rows and runs the lifecycle mirrors,
|
|
25
|
+
which move or remove EVERY folded row carrying the account dimension,
|
|
26
|
+
including rows folded mid-drain.
|
|
27
|
+
3. Every fold slice holds the fold-state row lock from before it reads raw
|
|
28
|
+
rows until its commit. A slice therefore commits either before
|
|
29
|
+
finalization (its account-attributed output exists when the mirrors run
|
|
30
|
+
and is moved/removed by them) or after (it observes the post-finalization
|
|
31
|
+
raw state, which carries no attribution to the account). No slice can
|
|
32
|
+
commit pre-deletion attribution after the mirrors ran — the exact
|
|
33
|
+
resurrection the single-transaction path guarded against.
|
|
34
|
+
|
|
35
|
+
Restart safety and idempotency: all progress lives in the database (the
|
|
36
|
+
marker columns plus the shrinking predicate ``WHERE account_id = :id``), so a
|
|
37
|
+
leader restart resumes mid-drain, and re-running any chunk is a no-op.
|
|
38
|
+
A credential replacement (re-import/reauth) clears the marker and supersedes
|
|
39
|
+
the deletion: every chunk transaction re-reads the marker under the account
|
|
40
|
+
row lock (PostgreSQL ``FOR NO KEY UPDATE``; the SQLite writer section
|
|
41
|
+
serializes writers) before touching rows, and finalization re-checks it the
|
|
42
|
+
same way, so no chunk can commit row work after a replacement committed and
|
|
43
|
+
a superseded account is never finalized.
|
|
44
|
+
|
|
45
|
+
Fairness: a deletion pass round-robins one chunk per pending account and
|
|
46
|
+
re-scans for newly marked accounts between rounds, so one account's
|
|
47
|
+
multi-minute drain can neither starve another marked account nor delay a
|
|
48
|
+
delete request that arrives mid-pass.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
from __future__ import annotations
|
|
52
|
+
|
|
53
|
+
import asyncio
|
|
54
|
+
import contextlib
|
|
55
|
+
import logging
|
|
56
|
+
from collections.abc import Awaitable, Callable
|
|
57
|
+
from dataclasses import dataclass, field
|
|
58
|
+
|
|
59
|
+
from sqlalchemy import Select, delete, select, update
|
|
60
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
61
|
+
|
|
62
|
+
from app.core.auth.api_key_cache import get_api_key_cache
|
|
63
|
+
from app.core.cache.invalidation import NAMESPACE_API_KEY, get_cache_invalidation_poller
|
|
64
|
+
from app.core.scheduling.leader_election_handle import get_leader_election as _get_leader_election
|
|
65
|
+
from app.core.upstream_proxy.cache import get_upstream_route_cache
|
|
66
|
+
from app.core.utils.time import utcnow
|
|
67
|
+
from app.db.models import (
|
|
68
|
+
Account,
|
|
69
|
+
AccountStatus,
|
|
70
|
+
AdditionalUsageHistory,
|
|
71
|
+
ApiKeyAccountAssignment,
|
|
72
|
+
RequestLog,
|
|
73
|
+
UsageHistory,
|
|
74
|
+
)
|
|
75
|
+
from app.db.session import get_background_session, sqlite_writer_section
|
|
76
|
+
from app.modules.accounts.repository import (
|
|
77
|
+
ACCOUNT_PENDING_DELETION_REASON,
|
|
78
|
+
AccountsRepository,
|
|
79
|
+
credentials_replaced_since_wipe,
|
|
80
|
+
)
|
|
81
|
+
from app.modules.proxy.account_cache import (
|
|
82
|
+
get_account_selection_cache,
|
|
83
|
+
mark_account_routing_unavailable,
|
|
84
|
+
propagate_account_routing_change,
|
|
85
|
+
)
|
|
86
|
+
from app.modules.usage.repository import _clear_bulk_history_since_sqlite_cache
|
|
87
|
+
|
|
88
|
+
logger = logging.getLogger(__name__)
|
|
89
|
+
|
|
90
|
+
# Worker tick; the fast delete path additionally wakes the local worker, so a
|
|
91
|
+
# single-replica deployment (or a delete that lands on the leader) starts
|
|
92
|
+
# draining immediately and the tick only covers follower-received requests
|
|
93
|
+
# and restart resume.
|
|
94
|
+
DELETION_INTERVAL_SECONDS = 30
|
|
95
|
+
# Rows per chunk transaction. Every chunk holds the account row lock
|
|
96
|
+
# (``FOR NO KEY UPDATE``) for its full duration, so a supersede
|
|
97
|
+
# (``replace_reauthorized``) or a fenced settlement write can wait for at most
|
|
98
|
+
# one chunk. Measured production rates: ~1.2s/10k usage_history deletes and
|
|
99
|
+
# ~23s/10k request_logs detaches (18 indexes, non-HOT updates) — 1k bounds the
|
|
100
|
+
# worst table at ~2.3s per transaction (5k would have been ~11.5s there).
|
|
101
|
+
DELETE_BATCH_SIZE = 1_000
|
|
102
|
+
# Between consecutive row-touching rounds the pass sleeps a fraction of the
|
|
103
|
+
# round's own duration (capped), so a multi-hundred-chunk drain leaves the
|
|
104
|
+
# 2-vCPU database headroom for foreground traffic instead of running chunk
|
|
105
|
+
# transactions back-to-back.
|
|
106
|
+
INTER_ROUND_PAUSE_RATIO = 0.25
|
|
107
|
+
INTER_ROUND_PAUSE_CAP_SECONDS = 2.0
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
async def run_account_deletion_pass(*, batch_size: int = DELETE_BATCH_SIZE) -> dict[str, str]:
|
|
111
|
+
"""Drain and finalize every account marked for deletion.
|
|
112
|
+
|
|
113
|
+
Round-robin fairness: each round advances every pending account by at
|
|
114
|
+
most one nonempty chunk, and the pending set is re-scanned between
|
|
115
|
+
rounds, so a newly marked account starts draining within one chunk
|
|
116
|
+
transaction of its request even while another account's long drain is in
|
|
117
|
+
progress.
|
|
118
|
+
|
|
119
|
+
Returns an outcome per account id: ``finalized`` (rows drained, account
|
|
120
|
+
row removed), ``superseded`` (marker cleared mid-drain by a credential
|
|
121
|
+
replacement — deletion abandoned), or ``error`` (logged; retried on the
|
|
122
|
+
next tick).
|
|
123
|
+
"""
|
|
124
|
+
outcomes: dict[str, str] = {}
|
|
125
|
+
# Tables observed empty for an account earlier in THIS pass are not
|
|
126
|
+
# re-probed on later rounds: a drained table stays drained for the rest of
|
|
127
|
+
# the drain (rows that land afterwards — e.g. a stream settling a log row
|
|
128
|
+
# mid-drain — are swept by finalization's residual pass), and re-probing
|
|
129
|
+
# would cost one account-row-locking transaction per table per round.
|
|
130
|
+
drained: dict[str, set[str]] = {}
|
|
131
|
+
loop = asyncio.get_running_loop()
|
|
132
|
+
while True:
|
|
133
|
+
runnable = [
|
|
134
|
+
account_id for account_id in await _pending_deletion_ids() if outcomes.get(account_id) in (None, "draining")
|
|
135
|
+
]
|
|
136
|
+
if not runnable:
|
|
137
|
+
break
|
|
138
|
+
round_started = loop.time()
|
|
139
|
+
for account_id in runnable:
|
|
140
|
+
try:
|
|
141
|
+
outcomes[account_id] = await _advance_account(
|
|
142
|
+
account_id, batch_size=batch_size, drained=drained.setdefault(account_id, set())
|
|
143
|
+
)
|
|
144
|
+
except Exception:
|
|
145
|
+
logger.exception("Background account deletion failed account_id=%s", account_id)
|
|
146
|
+
outcomes[account_id] = "error"
|
|
147
|
+
if any(outcomes.get(account_id) == "draining" for account_id in runnable):
|
|
148
|
+
elapsed = loop.time() - round_started
|
|
149
|
+
await asyncio.sleep(min(INTER_ROUND_PAUSE_CAP_SECONDS, elapsed * INTER_ROUND_PAUSE_RATIO))
|
|
150
|
+
# ``draining`` cannot survive the loop: an account leaves the runnable
|
|
151
|
+
# set only through a terminal outcome or by vanishing from the pending
|
|
152
|
+
# scan (its marker was cleared — a supersede that raced the scan).
|
|
153
|
+
for account_id, outcome in outcomes.items():
|
|
154
|
+
if outcome == "draining":
|
|
155
|
+
outcomes[account_id] = "superseded"
|
|
156
|
+
if outcomes:
|
|
157
|
+
logger.info("Account deletion pass outcomes=%s", outcomes)
|
|
158
|
+
return outcomes
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
async def _pending_deletion_ids() -> list[str]:
|
|
162
|
+
async with get_background_session() as session:
|
|
163
|
+
rows = await session.execute(
|
|
164
|
+
select(Account.id)
|
|
165
|
+
.where(Account.delete_requested_at.is_not(None))
|
|
166
|
+
.order_by(Account.delete_requested_at.asc(), Account.id.asc())
|
|
167
|
+
)
|
|
168
|
+
return list(rows.scalars().all())
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
async def _advance_account(account_id: str, *, batch_size: int, drained: set[str] | None = None) -> str:
|
|
172
|
+
"""One bounded round of work for one account.
|
|
173
|
+
|
|
174
|
+
Runs the drain tables in order (usage snapshots first — not
|
|
175
|
+
fold-governed — then the raw request logs) but stops after the first
|
|
176
|
+
NONEMPTY chunk so the caller can round-robin other pending accounts —
|
|
177
|
+
each round commits at most one row-touching transaction per account:
|
|
178
|
+
``draining`` means more work may remain. Only tables whose chunk came up
|
|
179
|
+
empty are known drained; when every table is, finalize. ``drained``
|
|
180
|
+
(caller-owned, per pass) records those tables so later rounds skip their
|
|
181
|
+
probes instead of re-running one locking transaction per table per round.
|
|
182
|
+
"""
|
|
183
|
+
if drained is None:
|
|
184
|
+
drained = set()
|
|
185
|
+
for label, chunk_fn in (
|
|
186
|
+
("usage_history", _usage_history_chunk),
|
|
187
|
+
("additional_usage_history", _additional_usage_history_chunk),
|
|
188
|
+
("request_logs", _request_logs_chunk),
|
|
189
|
+
):
|
|
190
|
+
if label in drained:
|
|
191
|
+
continue
|
|
192
|
+
affected = await _run_chunk(chunk_fn, account_id, batch_size=batch_size)
|
|
193
|
+
if affected is None:
|
|
194
|
+
return "superseded"
|
|
195
|
+
if not affected:
|
|
196
|
+
drained.add(label)
|
|
197
|
+
continue
|
|
198
|
+
if label == "usage_history":
|
|
199
|
+
# Same hygiene as retention pruning: bulk usage-history reads are
|
|
200
|
+
# cached on SQLite and must not serve the drained account.
|
|
201
|
+
_clear_bulk_history_since_sqlite_cache()
|
|
202
|
+
return "draining"
|
|
203
|
+
# Finalization: residual rows (streams that settled a log row mid-drain),
|
|
204
|
+
# folded-bucket mirrors, sticky/rollup rows, and the account row itself —
|
|
205
|
+
# one fold-state-locked transaction, identical in shape to the historical
|
|
206
|
+
# synchronous delete but over a residual row set instead of full history.
|
|
207
|
+
async with get_background_session() as session:
|
|
208
|
+
finalized = await AccountsRepository(session).delete(account_id, only_pending=True)
|
|
209
|
+
if not finalized:
|
|
210
|
+
return "superseded"
|
|
211
|
+
# Invalidate immediately (not at end of pass): the account row is gone
|
|
212
|
+
# and ids are deterministic, so cached routing/API-key snapshots must not
|
|
213
|
+
# outlive it while the pass keeps draining other accounts.
|
|
214
|
+
await _invalidate_account_caches()
|
|
215
|
+
return "finalized"
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
_ChunkFn = Callable[..., Awaitable[int]]
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
async def _run_chunk(chunk_fn: _ChunkFn, account_id: str, *, batch_size: int) -> int | None:
|
|
222
|
+
"""Run one chunk transaction; None when the pending marker disappeared
|
|
223
|
+
(deletion superseded)."""
|
|
224
|
+
drift_repaired = False
|
|
225
|
+
async with get_background_session() as session:
|
|
226
|
+
async with sqlite_writer_section():
|
|
227
|
+
state = await _pending_state(session, account_id)
|
|
228
|
+
if state is None:
|
|
229
|
+
await session.rollback()
|
|
230
|
+
return None
|
|
231
|
+
delete_history, drift_repaired = state
|
|
232
|
+
affected = await chunk_fn(session, account_id, delete_history=delete_history, batch_size=batch_size)
|
|
233
|
+
await session.commit()
|
|
234
|
+
if drift_repaired:
|
|
235
|
+
# The drift a pre-upgrade replica wrote may already be cached in
|
|
236
|
+
# selection/API-key snapshots on this or peer replicas; repairing the
|
|
237
|
+
# database alone would leave those caches selecting the wiped account
|
|
238
|
+
# (or honoring a stale assignment) until expiry. Same invalidation
|
|
239
|
+
# fan-out as the delete request itself.
|
|
240
|
+
mark_account_routing_unavailable(account_id)
|
|
241
|
+
await _invalidate_account_caches()
|
|
242
|
+
return affected
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
async def _pending_state(session: AsyncSession, account_id: str) -> tuple[bool, bool] | None:
|
|
246
|
+
"""``(delete_history, drift_repaired)``, or None when no longer pending.
|
|
247
|
+
|
|
248
|
+
On PostgreSQL the read locks the account row (``FOR NO KEY UPDATE``) for
|
|
249
|
+
the rest of the chunk transaction, so a credential replacement cannot
|
|
250
|
+
clear the marker between this read and the chunk's row mutations — the
|
|
251
|
+
replacement blocks until the chunk commits, then the next chunk observes
|
|
252
|
+
the cleared marker and stops. On SQLite the writer section already
|
|
253
|
+
serializes this transaction against every other writer.
|
|
254
|
+
|
|
255
|
+
A replacement handled by a pre-upgrade replica (rolling deploy) writes
|
|
256
|
+
fresh credentials but cannot clear marker columns its ORM does not know;
|
|
257
|
+
fresh non-wiped ciphertext on a marked row is therefore itself the
|
|
258
|
+
supersede signal — the marker is cleared here, under the same lock.
|
|
259
|
+
"""
|
|
260
|
+
stmt = select(
|
|
261
|
+
Account.delete_requested_at,
|
|
262
|
+
Account.delete_history_requested,
|
|
263
|
+
Account.access_token_encrypted,
|
|
264
|
+
Account.refresh_token_encrypted,
|
|
265
|
+
Account.id_token_encrypted,
|
|
266
|
+
Account.status,
|
|
267
|
+
Account.deactivation_reason,
|
|
268
|
+
).where(Account.id == account_id)
|
|
269
|
+
if session.get_bind().dialect.name == "postgresql":
|
|
270
|
+
stmt = stmt.with_for_update(key_share=True)
|
|
271
|
+
row = (await session.execute(stmt)).first()
|
|
272
|
+
if row is None or row[0] is None:
|
|
273
|
+
return None
|
|
274
|
+
if credentials_replaced_since_wipe(row[2], row[3], row[4]):
|
|
275
|
+
await session.execute(
|
|
276
|
+
update(Account)
|
|
277
|
+
.where(Account.id == account_id)
|
|
278
|
+
.values(delete_requested_at=None, delete_history_requested=False)
|
|
279
|
+
)
|
|
280
|
+
await session.commit()
|
|
281
|
+
return None
|
|
282
|
+
# Self-heal drift written by pre-upgrade replicas during a rolling
|
|
283
|
+
# deploy (their writers are unfenced): a late settlement may have
|
|
284
|
+
# replaced the terminal status — making the wiped account selectable
|
|
285
|
+
# again — and an unconditional assignment insert may have recreated an
|
|
286
|
+
# API-key assignment begin_delete removed. Re-fence both under the row
|
|
287
|
+
# lock held above; any drift is bounded by one chunk transaction. The
|
|
288
|
+
# credentials check above already excluded genuine replacements, so a
|
|
289
|
+
# non-DEACTIVATED status here can only be such drift. The caller
|
|
290
|
+
# propagates cache invalidation after commit when drift was repaired.
|
|
291
|
+
drift_repaired = False
|
|
292
|
+
if row[5] is not AccountStatus.DEACTIVATED or row[6] != ACCOUNT_PENDING_DELETION_REASON:
|
|
293
|
+
drift_repaired = True
|
|
294
|
+
await session.execute(
|
|
295
|
+
update(Account)
|
|
296
|
+
.where(Account.id == account_id)
|
|
297
|
+
.values(
|
|
298
|
+
status=AccountStatus.DEACTIVATED,
|
|
299
|
+
deactivation_reason=ACCOUNT_PENDING_DELETION_REASON,
|
|
300
|
+
reset_at=None,
|
|
301
|
+
blocked_at=None,
|
|
302
|
+
)
|
|
303
|
+
)
|
|
304
|
+
assignment_rows = await session.execute(
|
|
305
|
+
delete(ApiKeyAccountAssignment)
|
|
306
|
+
.where(ApiKeyAccountAssignment.account_id == account_id)
|
|
307
|
+
.returning(ApiKeyAccountAssignment.account_id)
|
|
308
|
+
)
|
|
309
|
+
if assignment_rows.scalars().first() is not None:
|
|
310
|
+
drift_repaired = True
|
|
311
|
+
return bool(row[1]), drift_repaired
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
# Chunk batch shape — why a RANGE predicate plus an index-matching ORDER BY
|
|
315
|
+
# instead of plain ``account_id = :id LIMIT n``:
|
|
316
|
+
#
|
|
317
|
+
# With an equality predicate the PostgreSQL planner folds ``account_id`` into
|
|
318
|
+
# a constant, drops it from the sort pathkeys, and — whenever the per-account
|
|
319
|
+
# row estimate is large (exactly the accounts this drain exists for) — plans
|
|
320
|
+
# the LIMIT subquery as an early-terminating Seq Scan (or a scan of an
|
|
321
|
+
# unrelated time index with a filter), betting on uniformly interleaved
|
|
322
|
+
# matches. That bet loses precisely mid-drain: detached/deleted rows no
|
|
323
|
+
# longer match, so each chunk re-scans a growing dead prefix, and once the
|
|
324
|
+
# table is drained but statistics are stale, every empty probe is a FULL heap
|
|
325
|
+
# scan (0 matches → no early termination). Verified against the production
|
|
326
|
+
# planner (606,970-row account): Seq Scans on all three tables.
|
|
327
|
+
#
|
|
328
|
+
# ``account_id >= :id AND account_id <= :id`` selects the same rows but keeps
|
|
329
|
+
# ``account_id`` out of the constant-equivalence class, so it survives as the
|
|
330
|
+
# leading ORDER BY pathkey; the ORDER BY then lists the target index's exact
|
|
331
|
+
# column order, making that account-leading index the only sort-free plan:
|
|
332
|
+
#
|
|
333
|
+
# usage_history → idx_usage_account_time (account_id, recorded_at)
|
|
334
|
+
# additional_usage_history → ix_additional_usage_distinct_labels
|
|
335
|
+
# (account_id, quota_key, limit_name, metered_feature)
|
|
336
|
+
# request_logs → idx_logs_account_kind_deleted_latest
|
|
337
|
+
# (account_id, request_kind, deleted_at,
|
|
338
|
+
# requested_at, id) — covering: Index Only Scan
|
|
339
|
+
#
|
|
340
|
+
# Verified on the production planner: all three plan as index (or index-only)
|
|
341
|
+
# scans with the account range as the Index Cond, and a drained-table probe
|
|
342
|
+
# costs one index descent (~8 cost units) instead of a heap scan. Chunk scan
|
|
343
|
+
# work is therefore bounded by the account's own remaining rows regardless of
|
|
344
|
+
# statistics staleness, and detached rows leave the scanned key range
|
|
345
|
+
# immediately. Row order is irrelevant to correctness (every row is drained);
|
|
346
|
+
# the ORDER BY exists purely to pin the plan.
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _usage_history_batch(account_id: str, batch_size: int) -> Select[tuple[int]]:
|
|
350
|
+
return (
|
|
351
|
+
select(UsageHistory.id)
|
|
352
|
+
.where(UsageHistory.account_id >= account_id, UsageHistory.account_id <= account_id)
|
|
353
|
+
.order_by(UsageHistory.account_id, UsageHistory.recorded_at)
|
|
354
|
+
.limit(batch_size)
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _additional_usage_history_batch(account_id: str, batch_size: int) -> Select[tuple[int]]:
|
|
359
|
+
return (
|
|
360
|
+
select(AdditionalUsageHistory.id)
|
|
361
|
+
.where(
|
|
362
|
+
AdditionalUsageHistory.account_id >= account_id,
|
|
363
|
+
AdditionalUsageHistory.account_id <= account_id,
|
|
364
|
+
)
|
|
365
|
+
.order_by(
|
|
366
|
+
AdditionalUsageHistory.account_id,
|
|
367
|
+
AdditionalUsageHistory.quota_key,
|
|
368
|
+
AdditionalUsageHistory.limit_name,
|
|
369
|
+
AdditionalUsageHistory.metered_feature,
|
|
370
|
+
)
|
|
371
|
+
.limit(batch_size)
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _request_logs_batch(account_id: str, batch_size: int) -> Select[tuple[int]]:
|
|
376
|
+
return (
|
|
377
|
+
select(RequestLog.id)
|
|
378
|
+
.where(RequestLog.account_id >= account_id, RequestLog.account_id <= account_id)
|
|
379
|
+
.order_by(
|
|
380
|
+
RequestLog.account_id,
|
|
381
|
+
RequestLog.request_kind,
|
|
382
|
+
RequestLog.deleted_at,
|
|
383
|
+
RequestLog.requested_at,
|
|
384
|
+
RequestLog.id,
|
|
385
|
+
)
|
|
386
|
+
.limit(batch_size)
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
async def _usage_history_chunk(session: AsyncSession, account_id: str, *, delete_history: bool, batch_size: int) -> int:
|
|
391
|
+
batch = _usage_history_batch(account_id, batch_size).scalar_subquery()
|
|
392
|
+
result = await session.execute(delete(UsageHistory).where(UsageHistory.id.in_(batch)).returning(UsageHistory.id))
|
|
393
|
+
return len(result.scalars().all())
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
async def _additional_usage_history_chunk(
|
|
397
|
+
session: AsyncSession, account_id: str, *, delete_history: bool, batch_size: int
|
|
398
|
+
) -> int:
|
|
399
|
+
batch = _additional_usage_history_batch(account_id, batch_size).scalar_subquery()
|
|
400
|
+
result = await session.execute(
|
|
401
|
+
delete(AdditionalUsageHistory).where(AdditionalUsageHistory.id.in_(batch)).returning(AdditionalUsageHistory.id)
|
|
402
|
+
)
|
|
403
|
+
return len(result.scalars().all())
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
async def _request_logs_chunk(session: AsyncSession, account_id: str, *, delete_history: bool, batch_size: int) -> int:
|
|
407
|
+
"""Detach (soft) or delete (hard) one chunk of the account's raw logs.
|
|
408
|
+
|
|
409
|
+
Deliberately NOT fold-state-locked and NOT mirrored: see the module
|
|
410
|
+
docstring for why interleaved fold slices converge at finalization.
|
|
411
|
+
"""
|
|
412
|
+
batch = _request_logs_batch(account_id, batch_size).scalar_subquery()
|
|
413
|
+
if delete_history:
|
|
414
|
+
result = await session.execute(delete(RequestLog).where(RequestLog.id.in_(batch)).returning(RequestLog.id))
|
|
415
|
+
else:
|
|
416
|
+
result = await session.execute(
|
|
417
|
+
update(RequestLog)
|
|
418
|
+
.where(RequestLog.id.in_(batch))
|
|
419
|
+
.values(account_id=None, deleted_at=utcnow())
|
|
420
|
+
.returning(RequestLog.id)
|
|
421
|
+
)
|
|
422
|
+
return len(result.scalars().all())
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
async def _invalidate_account_caches() -> None:
|
|
426
|
+
"""Post-finalization invalidation, mirroring the synchronous delete path.
|
|
427
|
+
|
|
428
|
+
Account ids are deterministic (delete-then-re-import regenerates the same
|
|
429
|
+
id), so cached route outcomes and API-key assignment snapshots must not
|
|
430
|
+
survive the account row's removal.
|
|
431
|
+
"""
|
|
432
|
+
get_account_selection_cache().invalidate()
|
|
433
|
+
get_api_key_cache().clear()
|
|
434
|
+
await get_upstream_route_cache().invalidate()
|
|
435
|
+
await propagate_account_routing_change()
|
|
436
|
+
poller = get_cache_invalidation_poller()
|
|
437
|
+
if poller is not None:
|
|
438
|
+
await poller.bump(NAMESPACE_API_KEY)
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
@dataclass(slots=True)
|
|
442
|
+
class AccountDeletionScheduler:
|
|
443
|
+
"""Leader-gated worker tick with a local wake signal.
|
|
444
|
+
|
|
445
|
+
Each tick first checks — with one cheap indexed-table read, before any
|
|
446
|
+
leader-election work — whether any account is pending deletion, so the
|
|
447
|
+
steady state (no pending deletions) costs one SELECT per interval.
|
|
448
|
+
"""
|
|
449
|
+
|
|
450
|
+
interval_seconds: int
|
|
451
|
+
_task: asyncio.Task[None] | None = None
|
|
452
|
+
_stop: asyncio.Event = field(default_factory=asyncio.Event)
|
|
453
|
+
_wake: asyncio.Event = field(default_factory=asyncio.Event)
|
|
454
|
+
_lock: asyncio.Lock = field(default_factory=asyncio.Lock)
|
|
455
|
+
|
|
456
|
+
async def start(self) -> None:
|
|
457
|
+
if self._task and not self._task.done():
|
|
458
|
+
return
|
|
459
|
+
self._stop.clear()
|
|
460
|
+
self._task = asyncio.create_task(self._run_loop())
|
|
461
|
+
|
|
462
|
+
async def stop(self) -> None:
|
|
463
|
+
if not self._task:
|
|
464
|
+
return
|
|
465
|
+
self._stop.set()
|
|
466
|
+
# Release the interval wait without depending on cancellation delivery.
|
|
467
|
+
# The loop only clears ``_wake`` AFTER passing the ``_stop`` gate, so a
|
|
468
|
+
# wake set here is sticky: whatever the loop is doing, its next wait
|
|
469
|
+
# returns at once and the loop exits on the gate instead of parking for
|
|
470
|
+
# another ``interval_seconds``. ``cancel()`` alone is not enough — a
|
|
471
|
+
# tick body can absorb it. The tick's own session teardown does: when
|
|
472
|
+
# its ``SELECT`` fails, ``get_background_session`` runs
|
|
473
|
+
# ``_safe_rollback`` on this frame, and that helper discards a
|
|
474
|
+
# ``CancelledError`` landing inside it (dropped outright by the bounded
|
|
475
|
+
# SQLite wait, re-raised into ``except BaseException: return`` on the
|
|
476
|
+
# unbounded one). A swallowed cancel would otherwise hold shutdown for
|
|
477
|
+
# the full interval, well past the drain budget.
|
|
478
|
+
self._wake.set()
|
|
479
|
+
self._task.cancel()
|
|
480
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
481
|
+
await self._task
|
|
482
|
+
self._task = None
|
|
483
|
+
|
|
484
|
+
def wake(self) -> None:
|
|
485
|
+
"""Start the next pass immediately (fast delete path just committed)."""
|
|
486
|
+
self._wake.set()
|
|
487
|
+
|
|
488
|
+
async def _run_loop(self) -> None:
|
|
489
|
+
while not self._stop.is_set():
|
|
490
|
+
# Clear BEFORE running so a wake that lands mid-pass (a second
|
|
491
|
+
# delete request) schedules another pass instead of being lost.
|
|
492
|
+
self._wake.clear()
|
|
493
|
+
await self._run_once()
|
|
494
|
+
try:
|
|
495
|
+
await asyncio.wait_for(self._wake.wait(), timeout=self.interval_seconds)
|
|
496
|
+
except asyncio.TimeoutError:
|
|
497
|
+
continue
|
|
498
|
+
|
|
499
|
+
async def _run_once(self) -> None:
|
|
500
|
+
try:
|
|
501
|
+
if not await _any_pending_deletion():
|
|
502
|
+
return
|
|
503
|
+
except Exception:
|
|
504
|
+
logger.exception("Failed to check for pending account deletions")
|
|
505
|
+
return
|
|
506
|
+
await _get_leader_election().run_if_leader(self._run_as_leader)
|
|
507
|
+
|
|
508
|
+
async def _run_as_leader(self) -> None:
|
|
509
|
+
async with self._lock:
|
|
510
|
+
try:
|
|
511
|
+
await run_account_deletion_pass()
|
|
512
|
+
except Exception:
|
|
513
|
+
logger.exception("Account deletion pass failed")
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
async def _any_pending_deletion() -> bool:
|
|
517
|
+
async with get_background_session() as session:
|
|
518
|
+
row = await session.execute(select(Account.id).where(Account.delete_requested_at.is_not(None)).limit(1))
|
|
519
|
+
return row.scalar_one_or_none() is not None
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
_scheduler: AccountDeletionScheduler | None = None
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def build_account_deletion_scheduler() -> AccountDeletionScheduler:
|
|
526
|
+
global _scheduler
|
|
527
|
+
_scheduler = AccountDeletionScheduler(interval_seconds=DELETION_INTERVAL_SECONDS)
|
|
528
|
+
return _scheduler
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def request_account_deletion_run() -> None:
|
|
532
|
+
"""Nudge the local worker after a delete request commits.
|
|
533
|
+
|
|
534
|
+
A follower's nudge is a no-op (``run_if_leader`` declines) and the
|
|
535
|
+
leader's periodic tick picks the request up within the interval; when the
|
|
536
|
+
receiving replica IS the leader — the common single-replica case — the
|
|
537
|
+
drain starts immediately.
|
|
538
|
+
"""
|
|
539
|
+
if _scheduler is not None:
|
|
540
|
+
_scheduler.wake()
|