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,831 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import and_, delete, func, or_, select, update
|
|
9
|
+
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
|
10
|
+
from sqlalchemy.dialects.sqlite import insert as sqlite_insert
|
|
11
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
12
|
+
from sqlalchemy.sql import Insert
|
|
13
|
+
|
|
14
|
+
from app.core.utils.time import naive_utc_to_epoch, to_utc_naive, utcnow
|
|
15
|
+
from app.db.models import Account, AccountStatus, StickySession, StickySessionKind
|
|
16
|
+
from app.db.session import sqlite_writer_section
|
|
17
|
+
from app.modules.sticky_sessions.schemas import StickySessionSortBy, StickySessionSortDir
|
|
18
|
+
|
|
19
|
+
# Each (key, kind) pair in delete_entries contributes 2 bind parameters to
|
|
20
|
+
# the underlying DELETE...OR (key=:k AND kind=:t)... statement. SQLite's
|
|
21
|
+
# default SQLITE_LIMIT_VARIABLE_NUMBER is 999 on builds older than 3.32
|
|
22
|
+
# and 32766 on newer builds, so chunking conservatively at 250 pairs
|
|
23
|
+
# (500 bind parameters) keeps delete-filtered safe on any libsqlite that
|
|
24
|
+
# ships with current Python interpreters. Postgres allows up to 65535
|
|
25
|
+
# bind parameters, which this chunk size also respects.
|
|
26
|
+
_DELETE_ENTRIES_CHUNK_SIZE = 250
|
|
27
|
+
|
|
28
|
+
_ContinuitySource = Literal["session_header", "thread_header", "turn_state"]
|
|
29
|
+
_SESSION_HEADER_ABANDONMENT_SCOPE = "session_header"
|
|
30
|
+
|
|
31
|
+
# A same-owner TTL refresh upsert only rewrites ``updated_at``. On hot
|
|
32
|
+
# (key, kind) rows, concurrent requests serialize on that row lock, so the
|
|
33
|
+
# selection path may skip the rewrite while the row is younger than this
|
|
34
|
+
# window, revalidating the observed deadline at write time. The window is
|
|
35
|
+
# bounded to at most 1% of the mapping TTL (so expiry moves by at most 1% of
|
|
36
|
+
# the window it protects) and to a small absolute ceiling; a rebind to a
|
|
37
|
+
# different owner, a row carrying any abandonment marker, or a row stamped in
|
|
38
|
+
# the future is never skippable because those writes change state beyond
|
|
39
|
+
# freshness (or the observation itself is untrustworthy).
|
|
40
|
+
_REFRESH_SKIP_TTL_FRACTION = 0.01
|
|
41
|
+
_REFRESH_SKIP_MAX_SECONDS = 15.0
|
|
42
|
+
|
|
43
|
+
# Only the Live-call ownership namespace is reserved. Other LF-prefixed keys
|
|
44
|
+
# (e.g. the pre-existing "\ncodex-lb-affinity-v1" selection affinities) remain
|
|
45
|
+
# ordinary operator-manageable sessions.
|
|
46
|
+
RESERVED_STICKY_SESSION_KEY_PREFIX = "\ncodex_live_call:"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def is_reserved_sticky_session_key(key: str) -> bool:
|
|
50
|
+
return key.startswith(RESERVED_STICKY_SESSION_KEY_PREFIX)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True, slots=True)
|
|
54
|
+
class StickySessionListEntryRecord:
|
|
55
|
+
sticky_session: StickySession
|
|
56
|
+
display_name: str
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True, slots=True)
|
|
60
|
+
class StickyOwnerLookup:
|
|
61
|
+
"""Result of resolving a mapping's owner, accessed by attribute (never
|
|
62
|
+
unpacked) so an un-configured test double for ``sticky_sessions`` degrades
|
|
63
|
+
to its existing safe defaults (no owner, not abandoned) instead of a hard
|
|
64
|
+
crash on tuple destructuring."""
|
|
65
|
+
|
|
66
|
+
account_id: str | None
|
|
67
|
+
continuity_abandoned: bool
|
|
68
|
+
# Source-qualified abandonment makes account_id ownerless only for the
|
|
69
|
+
# matching source, but selection must still remember which durable owner
|
|
70
|
+
# was retired. Global stale-hard tombstones leave this unset because their
|
|
71
|
+
# established recovery path may legitimately reselect a recovered owner.
|
|
72
|
+
abandoned_account_id: str | None = None
|
|
73
|
+
# Set only when the row was observed in this lookup with a fresh
|
|
74
|
+
# ``updated_at`` (within the refresh-skip window derived from
|
|
75
|
+
# ``max_age_seconds``) and no abandonment marker, so a same-owner TTL
|
|
76
|
+
# refresh upsert would be a pure ``updated_at`` rewrite. The value is the
|
|
77
|
+
# naive-UTC instant (``observed_updated_at`` + skip window) after which
|
|
78
|
+
# the skip is no longer valid; consumers must isinstance-check
|
|
79
|
+
# ``datetime`` (test doubles may auto-vivify attributes), must revalidate
|
|
80
|
+
# the deadline against the clock immediately before omitting the write,
|
|
81
|
+
# and must never skip a write that changes the owner account.
|
|
82
|
+
refresh_skip_deadline: datetime | None = None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _continuity_is_abandoned_for_source(
|
|
86
|
+
abandoned_at: datetime | None,
|
|
87
|
+
abandonment_scope: str | None,
|
|
88
|
+
continuity_source: _ContinuitySource | None,
|
|
89
|
+
) -> bool:
|
|
90
|
+
if abandonment_scope is not None:
|
|
91
|
+
# A scope is itself the source-qualified marker. Goal-restart writers
|
|
92
|
+
# deliberately leave the legacy timestamp NULL so pre-scope binaries
|
|
93
|
+
# keep treating account_id as hard ownership during rollout/rollback.
|
|
94
|
+
# Unknown and nonmatching typed callers likewise fail closed.
|
|
95
|
+
return abandonment_scope == continuity_source
|
|
96
|
+
# Historical stale-hard tombstones have a timestamp and NULL scope, and
|
|
97
|
+
# therefore continue to abandon ownership globally for every source.
|
|
98
|
+
return abandoned_at is not None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _source_scoped_abandoned_account_id(
|
|
102
|
+
account_id: str,
|
|
103
|
+
abandonment_scope: str | None,
|
|
104
|
+
continuity_source: _ContinuitySource | None,
|
|
105
|
+
) -> str | None:
|
|
106
|
+
if abandonment_scope is not None and abandonment_scope == continuity_source:
|
|
107
|
+
return account_id
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _owner_lookup_from_row(
|
|
112
|
+
row: StickySession,
|
|
113
|
+
*,
|
|
114
|
+
continuity_source: _ContinuitySource | None,
|
|
115
|
+
refresh_skip_deadline: datetime | None = None,
|
|
116
|
+
) -> StickyOwnerLookup:
|
|
117
|
+
if _continuity_is_abandoned_for_source(
|
|
118
|
+
row.continuity_abandoned_at,
|
|
119
|
+
row.continuity_abandonment_scope,
|
|
120
|
+
continuity_source,
|
|
121
|
+
):
|
|
122
|
+
return StickyOwnerLookup(
|
|
123
|
+
account_id=None,
|
|
124
|
+
continuity_abandoned=True,
|
|
125
|
+
abandoned_account_id=_source_scoped_abandoned_account_id(
|
|
126
|
+
row.account_id,
|
|
127
|
+
row.continuity_abandonment_scope,
|
|
128
|
+
continuity_source,
|
|
129
|
+
),
|
|
130
|
+
)
|
|
131
|
+
return StickyOwnerLookup(
|
|
132
|
+
account_id=row.account_id,
|
|
133
|
+
continuity_abandoned=False,
|
|
134
|
+
refresh_skip_deadline=refresh_skip_deadline,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _same_owner_refresh_skip_deadline(
|
|
139
|
+
row: StickySession,
|
|
140
|
+
*,
|
|
141
|
+
observed_updated_at: datetime,
|
|
142
|
+
now: datetime,
|
|
143
|
+
max_age_seconds: int,
|
|
144
|
+
) -> datetime | None:
|
|
145
|
+
"""Deadline until which a same-owner upsert of this row stays skippable.
|
|
146
|
+
|
|
147
|
+
Any abandonment marker disqualifies the skip: an upsert re-establishes
|
|
148
|
+
ownership by clearing both marker columns, so that write is semantic even
|
|
149
|
+
when the owner account is unchanged. A row whose ``updated_at`` sits in
|
|
150
|
+
the future (database clock ahead of this process, or a restored row) is
|
|
151
|
+
also never skippable: an upper-bound-only age comparison would let such a
|
|
152
|
+
row satisfy the window for longer than the documented bound.
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
if row.continuity_abandoned_at is not None or row.continuity_abandonment_scope is not None:
|
|
156
|
+
return None
|
|
157
|
+
age_seconds = (now - observed_updated_at).total_seconds()
|
|
158
|
+
if age_seconds < 0:
|
|
159
|
+
return None
|
|
160
|
+
skip_window_seconds = min(_REFRESH_SKIP_MAX_SECONDS, max_age_seconds * _REFRESH_SKIP_TTL_FRACTION)
|
|
161
|
+
if age_seconds > skip_window_seconds:
|
|
162
|
+
return None
|
|
163
|
+
return observed_updated_at + timedelta(seconds=skip_window_seconds)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class StickySessionsRepository:
|
|
167
|
+
def __init__(self, session: AsyncSession) -> None:
|
|
168
|
+
self._session = session
|
|
169
|
+
|
|
170
|
+
async def get_account_id(
|
|
171
|
+
self,
|
|
172
|
+
key: str,
|
|
173
|
+
*,
|
|
174
|
+
kind: StickySessionKind,
|
|
175
|
+
max_age_seconds: int | None = None,
|
|
176
|
+
continuity_source: _ContinuitySource | None = None,
|
|
177
|
+
) -> str | None:
|
|
178
|
+
lookup = await self.get_account_id_and_abandonment(
|
|
179
|
+
key,
|
|
180
|
+
kind=kind,
|
|
181
|
+
max_age_seconds=max_age_seconds,
|
|
182
|
+
continuity_source=continuity_source,
|
|
183
|
+
)
|
|
184
|
+
return lookup.account_id
|
|
185
|
+
|
|
186
|
+
async def get_account_id_and_abandonment(
|
|
187
|
+
self,
|
|
188
|
+
key: str,
|
|
189
|
+
*,
|
|
190
|
+
kind: StickySessionKind,
|
|
191
|
+
max_age_seconds: int | None = None,
|
|
192
|
+
continuity_source: _ContinuitySource | None = None,
|
|
193
|
+
) -> StickyOwnerLookup:
|
|
194
|
+
"""Resolve a mapping's owner and applicable abandonment marker.
|
|
195
|
+
|
|
196
|
+
Global stale-hard tombstones remain ownerless for every source. A
|
|
197
|
+
source-scoped marker is ownerless only for its matching typed source;
|
|
198
|
+
explicit turn-state and unknown callers retain the stored owner when a
|
|
199
|
+
goal restart abandoned only session-header interpretation.
|
|
200
|
+
"""
|
|
201
|
+
if not key:
|
|
202
|
+
return StickyOwnerLookup(account_id=None, continuity_abandoned=False)
|
|
203
|
+
row = await self.get_entry(key, kind=kind)
|
|
204
|
+
if row is None:
|
|
205
|
+
return StickyOwnerLookup(account_id=None, continuity_abandoned=False)
|
|
206
|
+
if max_age_seconds is None:
|
|
207
|
+
return _owner_lookup_from_row(row, continuity_source=continuity_source)
|
|
208
|
+
now = utcnow()
|
|
209
|
+
cutoff = now - timedelta(seconds=max_age_seconds)
|
|
210
|
+
observed_updated_at = to_utc_naive(row.updated_at)
|
|
211
|
+
if observed_updated_at >= cutoff:
|
|
212
|
+
return _owner_lookup_from_row(
|
|
213
|
+
row,
|
|
214
|
+
continuity_source=continuity_source,
|
|
215
|
+
refresh_skip_deadline=_same_owner_refresh_skip_deadline(
|
|
216
|
+
row,
|
|
217
|
+
observed_updated_at=observed_updated_at,
|
|
218
|
+
now=now,
|
|
219
|
+
max_age_seconds=max_age_seconds,
|
|
220
|
+
),
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Release the read snapshot before attempting a SQLite write upgrade.
|
|
224
|
+
# The DELETE remains safe because every value observed above participates
|
|
225
|
+
# in the predicate; a concurrent rebind therefore wins the comparison.
|
|
226
|
+
await self._session.commit()
|
|
227
|
+
statement = (
|
|
228
|
+
delete(StickySession)
|
|
229
|
+
.where(
|
|
230
|
+
StickySession.key == key,
|
|
231
|
+
StickySession.kind == kind,
|
|
232
|
+
StickySession.account_id == row.account_id,
|
|
233
|
+
StickySession.updated_at == observed_updated_at,
|
|
234
|
+
StickySession.updated_at < cutoff,
|
|
235
|
+
)
|
|
236
|
+
.returning(StickySession.key)
|
|
237
|
+
)
|
|
238
|
+
current: tuple[str, datetime, datetime | None, str | None] | None = None
|
|
239
|
+
async with sqlite_writer_section():
|
|
240
|
+
deleted_key = (await self._session.execute(statement)).scalar_one_or_none()
|
|
241
|
+
if deleted_key is None:
|
|
242
|
+
current = (
|
|
243
|
+
(
|
|
244
|
+
await self._session.execute(
|
|
245
|
+
select(
|
|
246
|
+
StickySession.account_id,
|
|
247
|
+
StickySession.updated_at,
|
|
248
|
+
StickySession.continuity_abandoned_at,
|
|
249
|
+
StickySession.continuity_abandonment_scope,
|
|
250
|
+
).where(
|
|
251
|
+
StickySession.key == key,
|
|
252
|
+
StickySession.kind == kind,
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
)
|
|
256
|
+
.tuples()
|
|
257
|
+
.one_or_none()
|
|
258
|
+
)
|
|
259
|
+
await self._session.commit()
|
|
260
|
+
|
|
261
|
+
if deleted_key is not None or current is None:
|
|
262
|
+
return StickyOwnerLookup(account_id=None, continuity_abandoned=False)
|
|
263
|
+
(
|
|
264
|
+
current_account_id,
|
|
265
|
+
current_updated_at,
|
|
266
|
+
current_continuity_abandoned_at,
|
|
267
|
+
current_continuity_abandonment_scope,
|
|
268
|
+
) = current
|
|
269
|
+
if to_utc_naive(current_updated_at) < cutoff:
|
|
270
|
+
return StickyOwnerLookup(account_id=None, continuity_abandoned=False)
|
|
271
|
+
if _continuity_is_abandoned_for_source(
|
|
272
|
+
current_continuity_abandoned_at,
|
|
273
|
+
current_continuity_abandonment_scope,
|
|
274
|
+
continuity_source,
|
|
275
|
+
):
|
|
276
|
+
return StickyOwnerLookup(
|
|
277
|
+
account_id=None,
|
|
278
|
+
continuity_abandoned=True,
|
|
279
|
+
abandoned_account_id=_source_scoped_abandoned_account_id(
|
|
280
|
+
current_account_id,
|
|
281
|
+
current_continuity_abandonment_scope,
|
|
282
|
+
continuity_source,
|
|
283
|
+
),
|
|
284
|
+
)
|
|
285
|
+
return StickyOwnerLookup(account_id=current_account_id, continuity_abandoned=False)
|
|
286
|
+
|
|
287
|
+
async def release_read_snapshot(self) -> None:
|
|
288
|
+
"""End the session's current read transaction.
|
|
289
|
+
|
|
290
|
+
On the default SQLite/WAL configuration one transaction pins one read
|
|
291
|
+
snapshot at its first SELECT, so a session shared across successive
|
|
292
|
+
ownership lookups would leave every later lookup blind to owners
|
|
293
|
+
committed concurrently after the first read. Committing ends that
|
|
294
|
+
snapshot so the next SELECT begins a fresh transaction; on PostgreSQL
|
|
295
|
+
READ COMMITTED each statement already reads fresh committed state, so
|
|
296
|
+
this is a near-free no-op. COMMIT (not rollback) on purpose: rollback
|
|
297
|
+
expires all tracked ORM state regardless of ``expire_on_commit``,
|
|
298
|
+
while commit under the session factory's ``expire_on_commit=False``
|
|
299
|
+
keeps rows loaded by earlier lookups readable.
|
|
300
|
+
"""
|
|
301
|
+
await self._session.commit()
|
|
302
|
+
|
|
303
|
+
async def get_entry(self, key: str, *, kind: StickySessionKind) -> StickySession | None:
|
|
304
|
+
if not key:
|
|
305
|
+
return None
|
|
306
|
+
statement = select(StickySession).where(
|
|
307
|
+
StickySession.key == key,
|
|
308
|
+
StickySession.kind == kind,
|
|
309
|
+
)
|
|
310
|
+
result = await self._session.execute(statement)
|
|
311
|
+
return result.scalar_one_or_none()
|
|
312
|
+
|
|
313
|
+
async def upsert(self, key: str, account_id: str, *, kind: StickySessionKind) -> StickySession:
|
|
314
|
+
# RETURNING collapses the previous upsert + re-select + refresh
|
|
315
|
+
# (4 round trips) into one statement; this runs inline before the
|
|
316
|
+
# first upstream byte on sticky requests, so round trips are TTFT.
|
|
317
|
+
# populate_existing forces the returned row to overwrite any stale
|
|
318
|
+
# identity-map instance the session may already hold for this key.
|
|
319
|
+
statement = self._build_upsert_statement(key, account_id, kind).returning(StickySession)
|
|
320
|
+
async with sqlite_writer_section():
|
|
321
|
+
result = await self._session.execute(statement, execution_options={"populate_existing": True})
|
|
322
|
+
row = result.scalar_one_or_none()
|
|
323
|
+
await self._session.commit()
|
|
324
|
+
if row is None:
|
|
325
|
+
raise RuntimeError(f"StickySession upsert failed for key={key!r} kind={kind.value!r}")
|
|
326
|
+
return row
|
|
327
|
+
|
|
328
|
+
async def insert_if_absent(self, key: str, account_id: str, kind: StickySessionKind) -> str:
|
|
329
|
+
"""Insert immutable ownership and return the persisted owner."""
|
|
330
|
+
|
|
331
|
+
statement = self._build_insert_do_nothing_statement(key, account_id, kind).returning(StickySession.account_id)
|
|
332
|
+
async with sqlite_writer_section():
|
|
333
|
+
result = await self._session.execute(statement)
|
|
334
|
+
owner_id = result.scalar_one_or_none()
|
|
335
|
+
if owner_id is None:
|
|
336
|
+
owner_id = await self._session.scalar(
|
|
337
|
+
select(StickySession.account_id).where(
|
|
338
|
+
StickySession.key == key,
|
|
339
|
+
StickySession.kind == kind,
|
|
340
|
+
)
|
|
341
|
+
)
|
|
342
|
+
await self._session.commit()
|
|
343
|
+
if owner_id is None:
|
|
344
|
+
raise RuntimeError("StickySession immutable insert did not resolve an owner")
|
|
345
|
+
return owner_id
|
|
346
|
+
|
|
347
|
+
async def upsert_with_seed_if_absent(
|
|
348
|
+
self,
|
|
349
|
+
key: str,
|
|
350
|
+
account_id: str,
|
|
351
|
+
*,
|
|
352
|
+
kind: StickySessionKind,
|
|
353
|
+
seed_key: str,
|
|
354
|
+
seed_kind: StickySessionKind,
|
|
355
|
+
) -> StickySession:
|
|
356
|
+
"""Upsert one mapping and initialize its immutable seed atomically."""
|
|
357
|
+
|
|
358
|
+
# Keep these writes in one transaction. A process seed without the
|
|
359
|
+
# initiating thread row is false placement evidence, while a thread
|
|
360
|
+
# row without its seed makes the first admitted thread invisible to
|
|
361
|
+
# later siblings. Do not replace the seed's DO NOTHING with an upsert:
|
|
362
|
+
# another thread may have won first-writer initialization already.
|
|
363
|
+
seed_statement = self._build_insert_do_nothing_statement(seed_key, account_id, seed_kind)
|
|
364
|
+
mapping_statement = self._build_upsert_statement(key, account_id, kind).returning(StickySession)
|
|
365
|
+
async with sqlite_writer_section():
|
|
366
|
+
try:
|
|
367
|
+
await self._session.execute(seed_statement)
|
|
368
|
+
result = await self._session.execute(
|
|
369
|
+
mapping_statement,
|
|
370
|
+
execution_options={"populate_existing": True},
|
|
371
|
+
)
|
|
372
|
+
row = result.scalar_one_or_none()
|
|
373
|
+
if row is None:
|
|
374
|
+
raise RuntimeError(f"StickySession seeded upsert failed for key={key!r} kind={kind.value!r}")
|
|
375
|
+
await self._session.commit()
|
|
376
|
+
except BaseException:
|
|
377
|
+
# This method owns both writes as one unit even when a caller
|
|
378
|
+
# catches the error and keeps using the same session.
|
|
379
|
+
await self._session.rollback()
|
|
380
|
+
raise
|
|
381
|
+
return row
|
|
382
|
+
|
|
383
|
+
async def delete(self, key: str, *, kind: StickySessionKind) -> bool:
|
|
384
|
+
if not key:
|
|
385
|
+
return False
|
|
386
|
+
statement = delete(StickySession).where(
|
|
387
|
+
StickySession.key == key,
|
|
388
|
+
StickySession.kind == kind,
|
|
389
|
+
)
|
|
390
|
+
async with sqlite_writer_section():
|
|
391
|
+
result = await self._session.execute(statement.returning(StickySession.key))
|
|
392
|
+
await self._session.commit()
|
|
393
|
+
return result.scalar_one_or_none() is not None
|
|
394
|
+
|
|
395
|
+
async def abandon_legacy_session_header_owner_if_unavailable(
|
|
396
|
+
self,
|
|
397
|
+
key: str,
|
|
398
|
+
*,
|
|
399
|
+
kind: StickySessionKind,
|
|
400
|
+
expected_account_id: str,
|
|
401
|
+
) -> bool:
|
|
402
|
+
"""Abandon only session-header interpretation of an unavailable raw owner."""
|
|
403
|
+
|
|
404
|
+
if not key or not expected_account_id:
|
|
405
|
+
return False
|
|
406
|
+
unavailable_statuses = (
|
|
407
|
+
AccountStatus.PAUSED,
|
|
408
|
+
AccountStatus.RATE_LIMITED,
|
|
409
|
+
AccountStatus.QUOTA_EXCEEDED,
|
|
410
|
+
)
|
|
411
|
+
# PostgreSQL evaluates the status subquery from the UPDATE statement's
|
|
412
|
+
# snapshot. Without first locking the Account row, a concurrent status
|
|
413
|
+
# recovery can commit while that statement waits for the StickySession
|
|
414
|
+
# row and the stale snapshot can still authorize a tombstone. Locking
|
|
415
|
+
# the status owner makes recovery and retirement serialize; the sticky
|
|
416
|
+
# owner predicate below independently keeps concurrent rebinds safe.
|
|
417
|
+
owner_status_lock = select(Account.status).where(Account.id == expected_account_id).with_for_update()
|
|
418
|
+
# Retain account status inside the UPDATE as a second, database-level
|
|
419
|
+
# invariant. The lock is the concurrency guarantee; this predicate
|
|
420
|
+
# prevents future refactors from turning a prior status observation
|
|
421
|
+
# into unconditional retirement.
|
|
422
|
+
unavailable_owner = select(Account.id).where(
|
|
423
|
+
Account.id == expected_account_id,
|
|
424
|
+
Account.status.in_(unavailable_statuses),
|
|
425
|
+
)
|
|
426
|
+
statement = (
|
|
427
|
+
update(StickySession)
|
|
428
|
+
.where(
|
|
429
|
+
StickySession.key == key,
|
|
430
|
+
StickySession.kind == kind,
|
|
431
|
+
StickySession.account_id == expected_account_id,
|
|
432
|
+
StickySession.continuity_abandoned_at.is_(None),
|
|
433
|
+
StickySession.continuity_abandonment_scope.is_(None),
|
|
434
|
+
StickySession.account_id.in_(unavailable_owner),
|
|
435
|
+
)
|
|
436
|
+
# The scope column is the new reader's marker. Keep the legacy
|
|
437
|
+
# timestamp NULL: older replicas know only that timestamp, so they
|
|
438
|
+
# continue to treat account_id as hard ownership instead of
|
|
439
|
+
# globally abandoning and rebinding a colliding explicit turn
|
|
440
|
+
# state. New readers use typed scope, never key shape, to decide
|
|
441
|
+
# which source may ignore the retained owner.
|
|
442
|
+
.values(
|
|
443
|
+
updated_at=func.now(),
|
|
444
|
+
continuity_abandoned_at=None,
|
|
445
|
+
continuity_abandonment_scope=_SESSION_HEADER_ABANDONMENT_SCOPE,
|
|
446
|
+
)
|
|
447
|
+
.returning(StickySession.key)
|
|
448
|
+
)
|
|
449
|
+
async with sqlite_writer_section():
|
|
450
|
+
owner_status = await self._session.scalar(owner_status_lock)
|
|
451
|
+
if owner_status not in unavailable_statuses:
|
|
452
|
+
await self._session.commit()
|
|
453
|
+
return False
|
|
454
|
+
result = await self._session.execute(statement)
|
|
455
|
+
await self._session.commit()
|
|
456
|
+
return result.scalar_one_or_none() is not None
|
|
457
|
+
|
|
458
|
+
async def restore_if_current(
|
|
459
|
+
self,
|
|
460
|
+
key: str,
|
|
461
|
+
*,
|
|
462
|
+
kind: StickySessionKind,
|
|
463
|
+
expected_account_id: str | None,
|
|
464
|
+
restore_account_id: str | None,
|
|
465
|
+
) -> bool:
|
|
466
|
+
"""Restore a sticky owner only if the provisional owner is still current."""
|
|
467
|
+
|
|
468
|
+
if not key:
|
|
469
|
+
return False
|
|
470
|
+
if expected_account_id is None:
|
|
471
|
+
if restore_account_id is None:
|
|
472
|
+
return True
|
|
473
|
+
statement = self._build_insert_do_nothing_statement(key, restore_account_id, kind).returning(
|
|
474
|
+
StickySession.key
|
|
475
|
+
)
|
|
476
|
+
elif restore_account_id is None:
|
|
477
|
+
statement = (
|
|
478
|
+
delete(StickySession)
|
|
479
|
+
.where(
|
|
480
|
+
StickySession.key == key,
|
|
481
|
+
StickySession.kind == kind,
|
|
482
|
+
StickySession.account_id == expected_account_id,
|
|
483
|
+
)
|
|
484
|
+
.returning(StickySession.key)
|
|
485
|
+
)
|
|
486
|
+
else:
|
|
487
|
+
statement = (
|
|
488
|
+
update(StickySession)
|
|
489
|
+
.where(
|
|
490
|
+
StickySession.key == key,
|
|
491
|
+
StickySession.kind == kind,
|
|
492
|
+
StickySession.account_id == expected_account_id,
|
|
493
|
+
)
|
|
494
|
+
.values(
|
|
495
|
+
account_id=restore_account_id,
|
|
496
|
+
updated_at=func.now(),
|
|
497
|
+
continuity_abandoned_at=None,
|
|
498
|
+
continuity_abandonment_scope=None,
|
|
499
|
+
)
|
|
500
|
+
.returning(StickySession.key)
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
async with sqlite_writer_section():
|
|
504
|
+
result = await self._session.execute(statement)
|
|
505
|
+
await self._session.commit()
|
|
506
|
+
return result.scalar_one_or_none() is not None
|
|
507
|
+
|
|
508
|
+
async def delete_entries(
|
|
509
|
+
self,
|
|
510
|
+
entries: Sequence[tuple[str, StickySessionKind]],
|
|
511
|
+
) -> list[tuple[str, StickySessionKind]]:
|
|
512
|
+
targets = {(key, kind) for key, kind in entries if key}
|
|
513
|
+
if not targets:
|
|
514
|
+
return []
|
|
515
|
+
|
|
516
|
+
deleted: list[tuple[str, StickySessionKind]] = []
|
|
517
|
+
targets_list = list(targets)
|
|
518
|
+
for offset in range(0, len(targets_list), _DELETE_ENTRIES_CHUNK_SIZE):
|
|
519
|
+
chunk = targets_list[offset : offset + _DELETE_ENTRIES_CHUNK_SIZE]
|
|
520
|
+
statement = delete(StickySession).where(
|
|
521
|
+
or_(*(and_(StickySession.key == key, StickySession.kind == kind) for key, kind in chunk))
|
|
522
|
+
)
|
|
523
|
+
async with sqlite_writer_section():
|
|
524
|
+
result = await self._session.execute(statement.returning(StickySession.key, StickySession.kind))
|
|
525
|
+
await self._session.commit()
|
|
526
|
+
deleted.extend((key, kind) for key, kind in result.all())
|
|
527
|
+
return deleted
|
|
528
|
+
|
|
529
|
+
async def list_entry_identifiers(
|
|
530
|
+
self,
|
|
531
|
+
*,
|
|
532
|
+
kind: StickySessionKind | None = None,
|
|
533
|
+
updated_before: datetime | None = None,
|
|
534
|
+
account_query: str | None = None,
|
|
535
|
+
key_query: str | None = None,
|
|
536
|
+
) -> list[tuple[str, StickySessionKind]]:
|
|
537
|
+
statement = (
|
|
538
|
+
self._apply_filters(
|
|
539
|
+
select(StickySession.key, StickySession.kind),
|
|
540
|
+
kind=kind,
|
|
541
|
+
updated_before=updated_before,
|
|
542
|
+
account_query=account_query,
|
|
543
|
+
key_query=key_query,
|
|
544
|
+
)
|
|
545
|
+
.join(Account, Account.id == StickySession.account_id)
|
|
546
|
+
.order_by(
|
|
547
|
+
StickySession.updated_at.desc(),
|
|
548
|
+
StickySession.created_at.desc(),
|
|
549
|
+
StickySession.key.asc(),
|
|
550
|
+
)
|
|
551
|
+
)
|
|
552
|
+
result = await self._session.execute(statement)
|
|
553
|
+
return [(key, kind) for key, kind in result.all()]
|
|
554
|
+
|
|
555
|
+
async def list_entries(
|
|
556
|
+
self,
|
|
557
|
+
*,
|
|
558
|
+
kind: StickySessionKind | None = None,
|
|
559
|
+
updated_before: datetime | None = None,
|
|
560
|
+
account_query: str | None = None,
|
|
561
|
+
key_query: str | None = None,
|
|
562
|
+
sort_by: StickySessionSortBy = "updated_at",
|
|
563
|
+
sort_dir: StickySessionSortDir = "desc",
|
|
564
|
+
offset: int = 0,
|
|
565
|
+
limit: int | None = None,
|
|
566
|
+
) -> Sequence[StickySessionListEntryRecord]:
|
|
567
|
+
order_by = self._build_order_by(sort_by=sort_by, sort_dir=sort_dir)
|
|
568
|
+
statement = (
|
|
569
|
+
self._apply_filters(
|
|
570
|
+
select(StickySession, Account.email),
|
|
571
|
+
kind=kind,
|
|
572
|
+
updated_before=updated_before,
|
|
573
|
+
account_query=account_query,
|
|
574
|
+
key_query=key_query,
|
|
575
|
+
)
|
|
576
|
+
.join(Account, Account.id == StickySession.account_id)
|
|
577
|
+
.order_by(*order_by)
|
|
578
|
+
)
|
|
579
|
+
if offset > 0:
|
|
580
|
+
statement = statement.offset(offset)
|
|
581
|
+
if limit is not None:
|
|
582
|
+
statement = statement.limit(limit)
|
|
583
|
+
result = await self._session.execute(statement)
|
|
584
|
+
return [
|
|
585
|
+
StickySessionListEntryRecord(sticky_session=sticky_session, display_name=display_name)
|
|
586
|
+
for sticky_session, display_name in result.all()
|
|
587
|
+
]
|
|
588
|
+
|
|
589
|
+
async def count_entries(
|
|
590
|
+
self,
|
|
591
|
+
*,
|
|
592
|
+
kind: StickySessionKind | None = None,
|
|
593
|
+
updated_before: datetime | None = None,
|
|
594
|
+
account_query: str | None = None,
|
|
595
|
+
key_query: str | None = None,
|
|
596
|
+
) -> int:
|
|
597
|
+
statement = self._apply_filters(
|
|
598
|
+
select(func.count()).select_from(StickySession).join(Account, Account.id == StickySession.account_id),
|
|
599
|
+
kind=kind,
|
|
600
|
+
updated_before=updated_before,
|
|
601
|
+
account_query=account_query,
|
|
602
|
+
key_query=key_query,
|
|
603
|
+
)
|
|
604
|
+
result = await self._session.execute(statement)
|
|
605
|
+
return int(result.scalar_one())
|
|
606
|
+
|
|
607
|
+
async def purge_prompt_cache_before(self, cutoff: datetime) -> int:
|
|
608
|
+
return await self.purge_before(cutoff, kind=StickySessionKind.PROMPT_CACHE)
|
|
609
|
+
|
|
610
|
+
async def purge_before(self, cutoff: datetime, *, kind: StickySessionKind | None = None) -> int:
|
|
611
|
+
stmt = delete(StickySession).where(StickySession.updated_at < to_utc_naive(cutoff))
|
|
612
|
+
if kind is not None:
|
|
613
|
+
stmt = stmt.where(StickySession.kind == kind)
|
|
614
|
+
async with sqlite_writer_section():
|
|
615
|
+
result = await self._session.execute(stmt.returning(StickySession.key))
|
|
616
|
+
deleted = len(result.scalars().all())
|
|
617
|
+
await self._session.commit()
|
|
618
|
+
return deleted
|
|
619
|
+
|
|
620
|
+
async def purge_before_for_key_prefix(
|
|
621
|
+
self,
|
|
622
|
+
cutoff: datetime,
|
|
623
|
+
*,
|
|
624
|
+
kind: StickySessionKind,
|
|
625
|
+
key_prefix: str,
|
|
626
|
+
limit: int = _DELETE_ENTRIES_CHUNK_SIZE,
|
|
627
|
+
) -> int:
|
|
628
|
+
"""Delete one bounded batch from a reserved key namespace."""
|
|
629
|
+
|
|
630
|
+
if limit <= 0:
|
|
631
|
+
return 0
|
|
632
|
+
target_keys = (
|
|
633
|
+
select(StickySession.key)
|
|
634
|
+
.where(
|
|
635
|
+
StickySession.kind == kind,
|
|
636
|
+
StickySession.key.startswith(key_prefix, autoescape=True),
|
|
637
|
+
StickySession.updated_at < to_utc_naive(cutoff),
|
|
638
|
+
)
|
|
639
|
+
.order_by(StickySession.updated_at.asc(), StickySession.key.asc())
|
|
640
|
+
.limit(limit)
|
|
641
|
+
)
|
|
642
|
+
stmt = delete(StickySession).where(
|
|
643
|
+
StickySession.kind == kind,
|
|
644
|
+
StickySession.key.in_(target_keys),
|
|
645
|
+
)
|
|
646
|
+
async with sqlite_writer_section():
|
|
647
|
+
result = await self._session.execute(stmt.returning(StickySession.key))
|
|
648
|
+
deleted = len(result.scalars().all())
|
|
649
|
+
await self._session.commit()
|
|
650
|
+
return deleted
|
|
651
|
+
|
|
652
|
+
async def purge_stale_hard_codex_session_mappings(self, cutoff: datetime, *, now: datetime) -> int:
|
|
653
|
+
"""Retire CODEX_SESSION mappings pinned to a durably unusable owner.
|
|
654
|
+
|
|
655
|
+
A hard `codex_session` mapping is never rebound by ordinary selection
|
|
656
|
+
(see load_balancer.py's hard_sticky branch) even once its owner is
|
|
657
|
+
rate-limited/quota-exceeded/paused, because that pin can represent
|
|
658
|
+
live, unverifiable session state that isn't safe to move mid-flight.
|
|
659
|
+
That correctly protects a transient blip, but leaves the mapping
|
|
660
|
+
stuck forever if the owner never recovers.
|
|
661
|
+
|
|
662
|
+
``Account.reset_at`` is frequently absent, while ``blocked_at`` is
|
|
663
|
+
cleared when an account is paused, so neither field provides one
|
|
664
|
+
durable outage clock for every unavailable status. Instead,
|
|
665
|
+
``AccountsRepository`` refreshes the mapping timestamp exactly when
|
|
666
|
+
its owner transitions from an available status into one of the
|
|
667
|
+
unavailable statuses below. ``StickySession.updated_at`` therefore
|
|
668
|
+
records the later of the mapping's last use and the outage start.
|
|
669
|
+
Only once BOTH the owner is still non-active AND that conservative
|
|
670
|
+
timestamp is before ``cutoff`` do we give up on the mapping.
|
|
671
|
+
|
|
672
|
+
When ``Account.reset_at`` is known and still in the future (e.g. a
|
|
673
|
+
multi-day quota window), the owner's own stated recovery point takes
|
|
674
|
+
priority over the flat cutoff: the mapping survives until after
|
|
675
|
+
``reset_at`` even if it has long since gone stale by the cutoff
|
|
676
|
+
alone, since purging before an account's own known recovery time
|
|
677
|
+
would contradict "well past its own recovery point". ``reset_at``
|
|
678
|
+
only ever narrows eligibility (delays a purge); it never widens it
|
|
679
|
+
when unset, which is why the fixed cutoff remains the fallback.
|
|
680
|
+
|
|
681
|
+
Giving up is done in two phases, never by an outright delete on the
|
|
682
|
+
first pass:
|
|
683
|
+
|
|
684
|
+
1. Tombstone: set ``continuity_abandoned_at`` instead of deleting.
|
|
685
|
+
A `conversation`-continuity request has no owner index besides
|
|
686
|
+
this row (see affinity.py's ``require_unambiguous_account``), so
|
|
687
|
+
an outright delete would be indistinguishable from "this key was
|
|
688
|
+
never seen" — and with more than one account in the pool, that
|
|
689
|
+
makes ``run_sticky_selection_path`` fail closed forever, even
|
|
690
|
+
after the original owner recovers, because nothing on that path
|
|
691
|
+
can ever re-create the very row it needs to stop failing closed.
|
|
692
|
+
A tombstone instead lets selection recognize "this key's
|
|
693
|
+
continuity was deliberately abandoned, picking a fresh owner is
|
|
694
|
+
authorized", so a subsequent request can escape the stuck state.
|
|
695
|
+
2. Delete: once a tombstone has sat for a further ``cutoff`` window
|
|
696
|
+
with nobody claiming it (i.e. it's still a tombstone, so no
|
|
697
|
+
request re-pinned it), it's dropped outright. A fresh request for
|
|
698
|
+
that key then falls back to the same conservative fail-closed
|
|
699
|
+
default as a key that was never seen, which is fine this long
|
|
700
|
+
after the fact.
|
|
701
|
+
"""
|
|
702
|
+
now_epoch = naive_utc_to_epoch(to_utc_naive(now))
|
|
703
|
+
unavailable_account_ids = select(Account.id).where(
|
|
704
|
+
Account.status.in_((AccountStatus.PAUSED, AccountStatus.RATE_LIMITED, AccountStatus.QUOTA_EXCEEDED)),
|
|
705
|
+
or_(Account.reset_at.is_(None), Account.reset_at < now_epoch),
|
|
706
|
+
)
|
|
707
|
+
cutoff_naive = to_utc_naive(cutoff)
|
|
708
|
+
tombstone_stmt = (
|
|
709
|
+
update(StickySession)
|
|
710
|
+
.where(
|
|
711
|
+
StickySession.kind == StickySessionKind.CODEX_SESSION,
|
|
712
|
+
or_(
|
|
713
|
+
StickySession.continuity_abandoned_at.is_(None),
|
|
714
|
+
StickySession.continuity_abandonment_scope.is_not(None),
|
|
715
|
+
),
|
|
716
|
+
StickySession.updated_at < cutoff_naive,
|
|
717
|
+
StickySession.account_id.in_(unavailable_account_ids),
|
|
718
|
+
)
|
|
719
|
+
# Stale-hard cleanup is global. It may promote a younger
|
|
720
|
+
# session-header-only marker once the original row itself crosses
|
|
721
|
+
# the normal stale-hard threshold.
|
|
722
|
+
.values(
|
|
723
|
+
continuity_abandoned_at=to_utc_naive(now),
|
|
724
|
+
continuity_abandonment_scope=None,
|
|
725
|
+
)
|
|
726
|
+
)
|
|
727
|
+
delete_stmt = delete(StickySession).where(
|
|
728
|
+
StickySession.kind == StickySessionKind.CODEX_SESSION,
|
|
729
|
+
StickySession.continuity_abandoned_at.is_not(None),
|
|
730
|
+
StickySession.continuity_abandonment_scope.is_(None),
|
|
731
|
+
StickySession.continuity_abandoned_at < cutoff_naive,
|
|
732
|
+
)
|
|
733
|
+
async with sqlite_writer_section():
|
|
734
|
+
tombstoned_result = await self._session.execute(tombstone_stmt.returning(StickySession.key))
|
|
735
|
+
tombstoned = len(tombstoned_result.scalars().all())
|
|
736
|
+
deleted_result = await self._session.execute(delete_stmt.returning(StickySession.key))
|
|
737
|
+
deleted = len(deleted_result.scalars().all())
|
|
738
|
+
await self._session.commit()
|
|
739
|
+
return tombstoned + deleted
|
|
740
|
+
|
|
741
|
+
def _build_upsert_statement(self, key: str, account_id: str, kind: StickySessionKind) -> Insert:
|
|
742
|
+
dialect = self._session.get_bind().dialect.name
|
|
743
|
+
if dialect == "postgresql":
|
|
744
|
+
insert_fn = pg_insert
|
|
745
|
+
elif dialect == "sqlite":
|
|
746
|
+
insert_fn = sqlite_insert
|
|
747
|
+
else:
|
|
748
|
+
raise RuntimeError(f"StickySession upsert unsupported for dialect={dialect!r}")
|
|
749
|
+
statement = insert_fn(StickySession).values(key=key, account_id=account_id, kind=kind)
|
|
750
|
+
return statement.on_conflict_do_update(
|
|
751
|
+
index_elements=[StickySession.key, StickySession.kind],
|
|
752
|
+
set_={
|
|
753
|
+
"account_id": account_id,
|
|
754
|
+
"updated_at": func.now(),
|
|
755
|
+
# A fresh pin fully re-establishes ownership, so any earlier
|
|
756
|
+
# purge tombstone (see purge_stale_hard_codex_session_mappings)
|
|
757
|
+
# no longer applies — otherwise this row would keep reporting
|
|
758
|
+
# itself as abandoned even though it now has a live owner.
|
|
759
|
+
"continuity_abandoned_at": None,
|
|
760
|
+
"continuity_abandonment_scope": None,
|
|
761
|
+
},
|
|
762
|
+
)
|
|
763
|
+
|
|
764
|
+
def _build_insert_do_nothing_statement(self, key: str, account_id: str, kind: StickySessionKind) -> Insert:
|
|
765
|
+
dialect = self._session.get_bind().dialect.name
|
|
766
|
+
if dialect == "postgresql":
|
|
767
|
+
insert_fn = pg_insert
|
|
768
|
+
elif dialect == "sqlite":
|
|
769
|
+
insert_fn = sqlite_insert
|
|
770
|
+
else:
|
|
771
|
+
raise RuntimeError(f"StickySession insert unsupported for dialect={dialect!r}")
|
|
772
|
+
statement = insert_fn(StickySession).values(key=key, account_id=account_id, kind=kind)
|
|
773
|
+
return statement.on_conflict_do_nothing(index_elements=[StickySession.key, StickySession.kind])
|
|
774
|
+
|
|
775
|
+
@staticmethod
|
|
776
|
+
def _apply_filters(
|
|
777
|
+
statement,
|
|
778
|
+
*,
|
|
779
|
+
kind: StickySessionKind | None,
|
|
780
|
+
updated_before: datetime | None,
|
|
781
|
+
account_query: str | None,
|
|
782
|
+
key_query: str | None,
|
|
783
|
+
):
|
|
784
|
+
statement = statement.where(~StickySession.key.startswith(RESERVED_STICKY_SESSION_KEY_PREFIX, autoescape=True))
|
|
785
|
+
if kind is not None:
|
|
786
|
+
statement = statement.where(StickySession.kind == kind)
|
|
787
|
+
if updated_before is not None:
|
|
788
|
+
statement = statement.where(StickySession.updated_at < to_utc_naive(updated_before))
|
|
789
|
+
if account_query:
|
|
790
|
+
statement = statement.where(func.lower(Account.email).contains(account_query.lower()))
|
|
791
|
+
if key_query:
|
|
792
|
+
statement = statement.where(func.lower(StickySession.key).contains(key_query.lower()))
|
|
793
|
+
return statement
|
|
794
|
+
|
|
795
|
+
@staticmethod
|
|
796
|
+
def _build_order_by(
|
|
797
|
+
*,
|
|
798
|
+
sort_by: StickySessionSortBy,
|
|
799
|
+
sort_dir: StickySessionSortDir,
|
|
800
|
+
):
|
|
801
|
+
sort_column_map = {
|
|
802
|
+
"updated_at": StickySession.updated_at,
|
|
803
|
+
"created_at": StickySession.created_at,
|
|
804
|
+
"account": Account.email,
|
|
805
|
+
"key": StickySession.key,
|
|
806
|
+
}
|
|
807
|
+
primary = sort_column_map[sort_by]
|
|
808
|
+
primary_order = primary.asc() if sort_dir == "asc" else primary.desc()
|
|
809
|
+
if sort_by == "updated_at":
|
|
810
|
+
return (
|
|
811
|
+
primary_order,
|
|
812
|
+
StickySession.created_at.desc(),
|
|
813
|
+
StickySession.key.asc(),
|
|
814
|
+
)
|
|
815
|
+
if sort_by == "created_at":
|
|
816
|
+
return (
|
|
817
|
+
primary_order,
|
|
818
|
+
StickySession.updated_at.desc(),
|
|
819
|
+
StickySession.key.asc(),
|
|
820
|
+
)
|
|
821
|
+
if sort_by == "account":
|
|
822
|
+
return (
|
|
823
|
+
primary_order,
|
|
824
|
+
StickySession.updated_at.desc(),
|
|
825
|
+
StickySession.key.asc(),
|
|
826
|
+
)
|
|
827
|
+
return (
|
|
828
|
+
primary_order,
|
|
829
|
+
StickySession.updated_at.desc(),
|
|
830
|
+
StickySession.created_at.desc(),
|
|
831
|
+
)
|