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,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from hashlib import sha256
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def select_node(key: str, nodes: Sequence[str]) -> str | None:
|
|
8
|
+
"""Rendezvous (HRW) hashing: returns node with highest sha256(key + node) hash.
|
|
9
|
+
|
|
10
|
+
Property: adding/removing 1 node remaps only 1/N keys (vs N-1/N for modulo).
|
|
11
|
+
Time complexity: O(N) where N = number of nodes.
|
|
12
|
+
"""
|
|
13
|
+
if not nodes:
|
|
14
|
+
return None
|
|
15
|
+
if len(nodes) == 1:
|
|
16
|
+
return nodes[0]
|
|
17
|
+
|
|
18
|
+
def _score(node: str) -> bytes:
|
|
19
|
+
return sha256(f"{key}:{node}".encode()).digest()
|
|
20
|
+
|
|
21
|
+
return max(nodes, key=_score)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Literal, TypedDict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class UpstreamError(TypedDict, total=False):
|
|
7
|
+
message: str
|
|
8
|
+
resets_at: int | float
|
|
9
|
+
resets_in_seconds: int | float
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
FailureClass = Literal["rate_limit", "quota", "retryable_transient", "account_unavailable", "non_retryable"]
|
|
13
|
+
FailurePhase = Literal["connect", "first_event", "mid_stream"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ClassifiedFailure(TypedDict):
|
|
17
|
+
failure_class: FailureClass
|
|
18
|
+
phase: FailurePhase
|
|
19
|
+
error_code: str
|
|
20
|
+
error: UpstreamError
|
|
21
|
+
http_status: int | None
|
app/core/bootstrap.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import logging
|
|
5
|
+
import secrets
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from secrets import compare_digest
|
|
8
|
+
|
|
9
|
+
from app.core.auth.dashboard_users_cache import get_dashboard_users_cache
|
|
10
|
+
from app.core.config.settings import get_settings
|
|
11
|
+
from app.core.config.settings_cache import get_settings_cache
|
|
12
|
+
from app.core.crypto import TokenEncryptor
|
|
13
|
+
from app.db.session import SessionLocal
|
|
14
|
+
from app.modules.dashboard_auth.repository import DashboardAuthRepository
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
_encryptor: TokenEncryptor | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class SharedBootstrapState:
|
|
22
|
+
"""The cross-replica facts the bootstrap token depends on, read uncached.
|
|
23
|
+
|
|
24
|
+
``local_password_configured`` is derived from ``dashboard_users`` (an active
|
|
25
|
+
account holding a password), never from the legacy settings column.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
local_password_configured: bool
|
|
29
|
+
bootstrap_token_encrypted: bytes | None
|
|
30
|
+
bootstrap_token_hash: bytes | None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _get_manual_bootstrap_token() -> str | None:
|
|
34
|
+
manual = (get_settings().dashboard_bootstrap_token or "").strip()
|
|
35
|
+
return manual or None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _hash_bootstrap_token(token: str) -> bytes:
|
|
39
|
+
return hashlib.sha256(token.encode("utf-8")).digest()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _get_encryptor() -> TokenEncryptor:
|
|
43
|
+
global _encryptor
|
|
44
|
+
if _encryptor is None:
|
|
45
|
+
_encryptor = TokenEncryptor()
|
|
46
|
+
return _encryptor
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def log_bootstrap_token(logger: logging.Logger, token: str, *, reason: str = "first-run") -> None:
|
|
50
|
+
# Emit at WARNING so the one-time token is surfaced regardless of the
|
|
51
|
+
# container/root logger level (which defaults to WARNING in most docker
|
|
52
|
+
# setups, silently dropping an INFO log and leaving operators unable to
|
|
53
|
+
# find the token). See #458.
|
|
54
|
+
logger.warning(
|
|
55
|
+
"\n"
|
|
56
|
+
"============================================\n"
|
|
57
|
+
" Dashboard bootstrap token (%s):\n"
|
|
58
|
+
" %s\n"
|
|
59
|
+
"\n"
|
|
60
|
+
" Use this token for initial remote setup.\n"
|
|
61
|
+
" It is shared across replicas and stays\n"
|
|
62
|
+
" valid until a password is set.\n"
|
|
63
|
+
"============================================",
|
|
64
|
+
reason,
|
|
65
|
+
token,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
async def _get_shared_bootstrap_state() -> SharedBootstrapState:
|
|
70
|
+
async with SessionLocal() as session:
|
|
71
|
+
repository = DashboardAuthRepository(session)
|
|
72
|
+
settings = await repository.get_settings()
|
|
73
|
+
auth_state = await repository.get_local_auth_state()
|
|
74
|
+
return SharedBootstrapState(
|
|
75
|
+
local_password_configured=auth_state.active_local_password_users > 0,
|
|
76
|
+
bootstrap_token_encrypted=settings.bootstrap_token_encrypted,
|
|
77
|
+
bootstrap_token_hash=settings.bootstrap_token_hash,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async def has_active_bootstrap_token() -> bool:
|
|
82
|
+
manual = _get_manual_bootstrap_token()
|
|
83
|
+
if manual:
|
|
84
|
+
return True
|
|
85
|
+
|
|
86
|
+
state = await _get_shared_bootstrap_state()
|
|
87
|
+
return not state.local_password_configured and state.bootstrap_token_hash is not None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
async def validate_bootstrap_token(submitted_token: str) -> bool:
|
|
91
|
+
manual = _get_manual_bootstrap_token()
|
|
92
|
+
if manual is not None:
|
|
93
|
+
return compare_digest(submitted_token.encode("utf-8"), manual.encode("utf-8"))
|
|
94
|
+
|
|
95
|
+
state = await _get_shared_bootstrap_state()
|
|
96
|
+
if state.local_password_configured or state.bootstrap_token_hash is None:
|
|
97
|
+
return False
|
|
98
|
+
return compare_digest(_hash_bootstrap_token(submitted_token), state.bootstrap_token_hash)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
async def get_bootstrap_validation_status(submitted_token: str) -> str:
|
|
102
|
+
manual = _get_manual_bootstrap_token()
|
|
103
|
+
if manual is not None:
|
|
104
|
+
if compare_digest(submitted_token.encode("utf-8"), manual.encode("utf-8")):
|
|
105
|
+
return "valid"
|
|
106
|
+
return "invalid"
|
|
107
|
+
|
|
108
|
+
state = await _get_shared_bootstrap_state()
|
|
109
|
+
if state.bootstrap_token_hash is None:
|
|
110
|
+
return "password_already_configured" if state.local_password_configured else "unavailable"
|
|
111
|
+
if compare_digest(_hash_bootstrap_token(submitted_token), state.bootstrap_token_hash):
|
|
112
|
+
return "valid"
|
|
113
|
+
if state.local_password_configured:
|
|
114
|
+
return "password_already_configured"
|
|
115
|
+
return "invalid"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
async def _invalidate_bootstrap_caches() -> None:
|
|
119
|
+
await get_settings_cache().invalidate()
|
|
120
|
+
await get_dashboard_users_cache().invalidate()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
async def ensure_auto_bootstrap_token() -> str | None:
|
|
124
|
+
manual = _get_manual_bootstrap_token()
|
|
125
|
+
|
|
126
|
+
async with SessionLocal() as session:
|
|
127
|
+
repository = DashboardAuthRepository(session)
|
|
128
|
+
settings = await repository.get_settings()
|
|
129
|
+
local_password_configured = (await repository.get_local_auth_state()).active_local_password_users > 0
|
|
130
|
+
|
|
131
|
+
if manual or local_password_configured:
|
|
132
|
+
if settings.bootstrap_token_hash is not None:
|
|
133
|
+
await repository.clear_bootstrap_token()
|
|
134
|
+
await _invalidate_bootstrap_caches()
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
if settings.bootstrap_token_hash is not None:
|
|
138
|
+
encrypted = settings.bootstrap_token_encrypted
|
|
139
|
+
if encrypted is None:
|
|
140
|
+
logger.warning(
|
|
141
|
+
"Stored bootstrap token hash exists without encrypted token; keeping existing hash untouched"
|
|
142
|
+
)
|
|
143
|
+
return None
|
|
144
|
+
try:
|
|
145
|
+
return _get_encryptor().decrypt(encrypted)
|
|
146
|
+
except Exception:
|
|
147
|
+
logger.warning(
|
|
148
|
+
"Stored bootstrap token could not be decrypted; leaving existing token valid. "
|
|
149
|
+
"Configure CODEX_LB_DASHBOARD_BOOTSTRAP_TOKEN or restore a shared encryption key "
|
|
150
|
+
"to recover it.",
|
|
151
|
+
exc_info=True,
|
|
152
|
+
)
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
token = secrets.token_urlsafe(32)
|
|
156
|
+
stored = await repository.store_bootstrap_token_if_absent(
|
|
157
|
+
_get_encryptor().encrypt(token),
|
|
158
|
+
_hash_bootstrap_token(token),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
await _invalidate_bootstrap_caches()
|
|
162
|
+
if stored:
|
|
163
|
+
return token
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
async def clear_auto_generated_token() -> None:
|
|
168
|
+
async with SessionLocal() as session:
|
|
169
|
+
repository = DashboardAuthRepository(session)
|
|
170
|
+
cleared = await repository.clear_bootstrap_token()
|
|
171
|
+
if cleared:
|
|
172
|
+
await _invalidate_bootstrap_caches()
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from collections.abc import Awaitable, Callable
|
|
6
|
+
from inspect import isawaitable
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import 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.exc import OperationalError
|
|
12
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
13
|
+
|
|
14
|
+
from app.core.metrics.prometheus import (
|
|
15
|
+
PROMETHEUS_AVAILABLE,
|
|
16
|
+
cache_invalidation_bump_failures_total,
|
|
17
|
+
cache_invalidation_poll_failures_total,
|
|
18
|
+
)
|
|
19
|
+
from app.db.models import CacheInvalidation
|
|
20
|
+
from app.db.session import close_session
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
NAMESPACE_API_KEY = "api_key"
|
|
25
|
+
NAMESPACE_FIREWALL = "firewall"
|
|
26
|
+
NAMESPACE_ACCOUNT_ROUTING = "account_routing"
|
|
27
|
+
NAMESPACE_ACCOUNT_SELECTION = "account_selection"
|
|
28
|
+
NAMESPACE_SETTINGS = "settings"
|
|
29
|
+
NAMESPACE_RESET_CREDITS = "reset_credits"
|
|
30
|
+
NAMESPACE_MODEL_REGISTRY = "model_registry"
|
|
31
|
+
NAMESPACE_UPSTREAM_ROUTE = "upstream_route"
|
|
32
|
+
NAMESPACE_DASHBOARD_USERS = "dashboard_users"
|
|
33
|
+
# Callback return values are ignored; awaitables are awaited for their side
|
|
34
|
+
# effects only, so callbacks may return a status (e.g. bool) for other callers.
|
|
35
|
+
type InvalidationCallback = Callable[[], object | Awaitable[object]]
|
|
36
|
+
|
|
37
|
+
# Log-safe labels for namespace values. Static analyzers (CodeQL) classify the
|
|
38
|
+
# NAMESPACE_API_KEY constant as credential-like from its name alone, so log
|
|
39
|
+
# statements must not take dataflow from the constants. These literals mirror
|
|
40
|
+
# the namespace values; test_namespace_log_labels_cover_all_namespaces keeps
|
|
41
|
+
# them in sync.
|
|
42
|
+
_NAMESPACE_LOG_LABELS: dict[str, str] = {
|
|
43
|
+
"api_key": "api_key",
|
|
44
|
+
"firewall": "firewall",
|
|
45
|
+
"account_routing": "account_routing",
|
|
46
|
+
"account_selection": "account_selection",
|
|
47
|
+
"settings": "settings",
|
|
48
|
+
"reset_credits": "reset_credits",
|
|
49
|
+
"model_registry": "model_registry",
|
|
50
|
+
"upstream_route": "upstream_route",
|
|
51
|
+
"dashboard_users": "dashboard_users",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
_BUMP_RETRY_ATTEMPTS = 3
|
|
55
|
+
_BUMP_RETRY_BASE_SECONDS = 0.05
|
|
56
|
+
_POLL_FAILURES_WARNING_THRESHOLD = 3
|
|
57
|
+
_POLL_FAILURES_ERROR_THRESHOLD = 10
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class CacheInvalidationPoller:
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
session_factory: Callable[[], AsyncSession],
|
|
64
|
+
poll_interval_seconds: float = 0.5,
|
|
65
|
+
) -> None:
|
|
66
|
+
self._session_factory = session_factory
|
|
67
|
+
self._poll_interval = poll_interval_seconds
|
|
68
|
+
self._known_versions: dict[str, int] = {}
|
|
69
|
+
self._callbacks: dict[str, list[InvalidationCallback]] = {}
|
|
70
|
+
self._pending_bumps: set[str] = set()
|
|
71
|
+
self._consecutive_poll_failures = 0
|
|
72
|
+
self._poll_initialized = False
|
|
73
|
+
self._task: asyncio.Task[None] | None = None
|
|
74
|
+
self._stop = asyncio.Event()
|
|
75
|
+
|
|
76
|
+
def on_invalidation(self, namespace: str, callback: InvalidationCallback) -> None:
|
|
77
|
+
self._callbacks.setdefault(namespace, []).append(callback)
|
|
78
|
+
|
|
79
|
+
async def initialize(self) -> None:
|
|
80
|
+
"""Seed baseline namespace versions before the process serves traffic.
|
|
81
|
+
|
|
82
|
+
MUST run at lifespan startup before local caches / routing snapshots are
|
|
83
|
+
loaded and before the first poll cycle. It records the current version of
|
|
84
|
+
every ``cache_invalidation`` row as the baseline and marks the poller
|
|
85
|
+
initialized, so that any bump a peer commits after this point is observed
|
|
86
|
+
as a change (fires callbacks) rather than acknowledged as pre-existing
|
|
87
|
+
state. Without this seed the first poll would treat every existing version
|
|
88
|
+
row as a baseline and silently drop a peer bump that landed between local
|
|
89
|
+
cache load and the first poll, leaving security/selection/routing state
|
|
90
|
+
stale until the fallback TTL or a later bump.
|
|
91
|
+
|
|
92
|
+
On success ``_poll_initialized`` is set to ``True``. If the read fails the
|
|
93
|
+
method raises with state unchanged (baseline empty, ``_poll_initialized``
|
|
94
|
+
still ``False``), so the caller can retry before background polling. If
|
|
95
|
+
the caller continues, ``start()`` arms conservative callback delivery for
|
|
96
|
+
the first successfully observed versions instead of accepting them as a
|
|
97
|
+
callback-less baseline.
|
|
98
|
+
"""
|
|
99
|
+
session = self._session_factory()
|
|
100
|
+
try:
|
|
101
|
+
result = await session.execute(select(CacheInvalidation.namespace, CacheInvalidation.version))
|
|
102
|
+
rows = result.all()
|
|
103
|
+
finally:
|
|
104
|
+
await close_session(session)
|
|
105
|
+
for namespace, version in rows:
|
|
106
|
+
self._known_versions[namespace] = version
|
|
107
|
+
self._poll_initialized = True
|
|
108
|
+
|
|
109
|
+
async def prime(self) -> None:
|
|
110
|
+
"""Record the current version baseline without firing callbacks.
|
|
111
|
+
|
|
112
|
+
Runs a single poll so that any bump landing *after* this call is
|
|
113
|
+
delivered as an invalidation callback rather than absorbed as the
|
|
114
|
+
poller's initial (callback-less) baseline. Call before a one-shot
|
|
115
|
+
startup reconcile: a leader that persists-and-bumps in the window
|
|
116
|
+
between that reconcile's read and the poller's first background tick
|
|
117
|
+
would otherwise be silently missed until the next full backstop. The
|
|
118
|
+
first poll only records versions (``_poll_initialized`` is still
|
|
119
|
+
``False``), so priming never invokes a callback. Unlike ``initialize``
|
|
120
|
+
this also flushes any pending bumps and seeds baselines for every
|
|
121
|
+
namespace observed in the ``cache_invalidation`` table.
|
|
122
|
+
|
|
123
|
+
Mirrors ``initialize``'s error contract: if the baseline read fails the
|
|
124
|
+
poller stays uninitialized (``_poll_initialized`` still ``False``) and
|
|
125
|
+
this method raises so the caller can retry before background polling.
|
|
126
|
+
Starting without a successful retry makes the first recovered poll
|
|
127
|
+
reconcile positive versions through their callbacks before acknowledging
|
|
128
|
+
them.
|
|
129
|
+
``_poll_once`` swallows the read error, so a silent success here would
|
|
130
|
+
let the first *background* poll absorb a peer bump as the initial
|
|
131
|
+
baseline, voiding the delivery guarantee priming exists to provide.
|
|
132
|
+
"""
|
|
133
|
+
if not await self._poll_once():
|
|
134
|
+
raise RuntimeError("cache invalidation prime failed: baseline version read did not complete")
|
|
135
|
+
|
|
136
|
+
async def start(self) -> None:
|
|
137
|
+
if self._task and not self._task.done():
|
|
138
|
+
return
|
|
139
|
+
# Callback-less baseline acquisition is safe only before process-local
|
|
140
|
+
# state can be served. Once background polling starts, a missing baseline
|
|
141
|
+
# means the observed versions are uncertain: treat each positive first
|
|
142
|
+
# observation as a change and reconcile it through the normal callback /
|
|
143
|
+
# acknowledgement path. A successful prime already set this flag after
|
|
144
|
+
# recording exact versions, so normal startup remains unchanged.
|
|
145
|
+
self._poll_initialized = True
|
|
146
|
+
self._stop.clear()
|
|
147
|
+
self._task = asyncio.create_task(self._run())
|
|
148
|
+
|
|
149
|
+
async def stop(self) -> None:
|
|
150
|
+
if not self._task:
|
|
151
|
+
return
|
|
152
|
+
self._stop.set()
|
|
153
|
+
self._task.cancel()
|
|
154
|
+
try:
|
|
155
|
+
await self._task
|
|
156
|
+
except asyncio.CancelledError:
|
|
157
|
+
pass
|
|
158
|
+
self._task = None
|
|
159
|
+
|
|
160
|
+
def request_bump(self, namespace: str) -> None:
|
|
161
|
+
"""Enqueue a coalesced bump flushed at the start of the next poll cycle.
|
|
162
|
+
|
|
163
|
+
Multiple requests for the same namespace within one cycle collapse into a
|
|
164
|
+
single version bump. A failed flush keeps the namespace pending so it is
|
|
165
|
+
retried on subsequent cycles. A request arriving while a flush is already
|
|
166
|
+
awaiting the bump for this namespace re-queues it for the next cycle.
|
|
167
|
+
"""
|
|
168
|
+
self._pending_bumps.add(namespace)
|
|
169
|
+
|
|
170
|
+
async def bump(self, namespace: str) -> bool:
|
|
171
|
+
"""Publish now, retaining unfinished work for a later poll cycle.
|
|
172
|
+
|
|
173
|
+
Consume only markers queued before this attempt. A request arriving
|
|
174
|
+
during the write re-adds the marker and must survive completion. An
|
|
175
|
+
interrupted write may already have committed; a redundant retry is
|
|
176
|
+
safe, while losing its invalidation is not.
|
|
177
|
+
"""
|
|
178
|
+
self._pending_bumps.discard(namespace)
|
|
179
|
+
succeeded = False
|
|
180
|
+
try:
|
|
181
|
+
for attempt in range(_BUMP_RETRY_ATTEMPTS):
|
|
182
|
+
try:
|
|
183
|
+
await self._bump_once(namespace)
|
|
184
|
+
succeeded = True
|
|
185
|
+
return True
|
|
186
|
+
except OperationalError:
|
|
187
|
+
if attempt == _BUMP_RETRY_ATTEMPTS - 1:
|
|
188
|
+
self._record_bump_failure(namespace)
|
|
189
|
+
return False
|
|
190
|
+
await asyncio.sleep(_BUMP_RETRY_BASE_SECONDS * (2**attempt))
|
|
191
|
+
except Exception:
|
|
192
|
+
self._record_bump_failure(namespace)
|
|
193
|
+
return False
|
|
194
|
+
return False
|
|
195
|
+
finally:
|
|
196
|
+
# Synchronous cleanup also covers cancellation during retry backoff
|
|
197
|
+
# and session cleanup without swallowing the cancellation.
|
|
198
|
+
if not succeeded:
|
|
199
|
+
self.request_bump(namespace)
|
|
200
|
+
|
|
201
|
+
async def bump_local(self, namespace: str) -> bool:
|
|
202
|
+
"""Bump a namespace this replica has ALREADY invalidated locally.
|
|
203
|
+
|
|
204
|
+
For callers that both mutate their own in-memory state AND want peers
|
|
205
|
+
to react (e.g. the reset-credit redeem path evicts the affected
|
|
206
|
+
account's snapshot locally, then bumps so peers clear their stores).
|
|
207
|
+
After the shared bump succeeds this records the resulting version as
|
|
208
|
+
already-observed on THIS poller, so the originating replica does NOT
|
|
209
|
+
re-run its (possibly whole-store) callback for a bump it already
|
|
210
|
+
accounted for locally. Peer replicas still observe the bump and fire.
|
|
211
|
+
|
|
212
|
+
A peer bump that lands between our commit and the acknowledging read is
|
|
213
|
+
acknowledged here without firing on this replica; that degrades to the
|
|
214
|
+
per-replica refresh fallback the reset-credits design already documents
|
|
215
|
+
for a lost bump, and never affects peers. ``_known_versions`` is only
|
|
216
|
+
advanced (``max``), never rewound, so a concurrent poll cannot be forced
|
|
217
|
+
to re-fire by this method.
|
|
218
|
+
"""
|
|
219
|
+
if not await self.bump(namespace):
|
|
220
|
+
return False
|
|
221
|
+
session = self._session_factory()
|
|
222
|
+
try:
|
|
223
|
+
version = await session.scalar(
|
|
224
|
+
select(CacheInvalidation.version).where(CacheInvalidation.namespace == namespace)
|
|
225
|
+
)
|
|
226
|
+
except Exception:
|
|
227
|
+
# Failing to acknowledge only risks one redundant self-invalidation
|
|
228
|
+
# on the next poll; the bump itself already succeeded for peers.
|
|
229
|
+
logger.debug("cache_invalidation bump_local acknowledge read failed", exc_info=True)
|
|
230
|
+
return True
|
|
231
|
+
finally:
|
|
232
|
+
await close_session(session)
|
|
233
|
+
if version is not None:
|
|
234
|
+
self._known_versions[namespace] = max(self._known_versions.get(namespace, 0), version)
|
|
235
|
+
return True
|
|
236
|
+
|
|
237
|
+
async def _bump_once(self, namespace: str) -> None:
|
|
238
|
+
session = self._session_factory()
|
|
239
|
+
try:
|
|
240
|
+
dialect = session.get_bind().dialect.name
|
|
241
|
+
if dialect == "postgresql":
|
|
242
|
+
stmt = (
|
|
243
|
+
pg_insert(CacheInvalidation)
|
|
244
|
+
.values(namespace=namespace, version=1)
|
|
245
|
+
.on_conflict_do_update(
|
|
246
|
+
index_elements=[CacheInvalidation.namespace],
|
|
247
|
+
set_={"version": CacheInvalidation.version + 1},
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
await session.execute(stmt)
|
|
251
|
+
elif dialect == "sqlite":
|
|
252
|
+
stmt = (
|
|
253
|
+
sqlite_insert(CacheInvalidation)
|
|
254
|
+
.values(namespace=namespace, version=1)
|
|
255
|
+
.on_conflict_do_update(
|
|
256
|
+
index_elements=[CacheInvalidation.namespace],
|
|
257
|
+
set_={"version": CacheInvalidation.version + 1},
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
await session.execute(stmt)
|
|
261
|
+
else:
|
|
262
|
+
existing = await session.scalar(
|
|
263
|
+
select(CacheInvalidation).where(CacheInvalidation.namespace == namespace)
|
|
264
|
+
)
|
|
265
|
+
if existing is None:
|
|
266
|
+
session.add(CacheInvalidation(namespace=namespace, version=1))
|
|
267
|
+
else:
|
|
268
|
+
await session.execute(
|
|
269
|
+
update(CacheInvalidation)
|
|
270
|
+
.where(CacheInvalidation.namespace == namespace)
|
|
271
|
+
.values(version=CacheInvalidation.version + 1)
|
|
272
|
+
)
|
|
273
|
+
await session.commit()
|
|
274
|
+
finally:
|
|
275
|
+
await close_session(session)
|
|
276
|
+
|
|
277
|
+
def _record_bump_failure(self, namespace: str) -> None:
|
|
278
|
+
logger.error(
|
|
279
|
+
"cache_invalidation bump failed for namespace %s",
|
|
280
|
+
_NAMESPACE_LOG_LABELS.get(namespace, "unknown"),
|
|
281
|
+
exc_info=True,
|
|
282
|
+
)
|
|
283
|
+
if PROMETHEUS_AVAILABLE and cache_invalidation_bump_failures_total is not None:
|
|
284
|
+
cache_invalidation_bump_failures_total.labels(namespace=namespace).inc()
|
|
285
|
+
|
|
286
|
+
async def _run(self) -> None:
|
|
287
|
+
while not self._stop.is_set():
|
|
288
|
+
try:
|
|
289
|
+
await self._poll_once()
|
|
290
|
+
except Exception:
|
|
291
|
+
logger.debug("cache_invalidation poll failed", exc_info=True)
|
|
292
|
+
try:
|
|
293
|
+
await asyncio.wait_for(self._stop.wait(), timeout=self._poll_interval)
|
|
294
|
+
except asyncio.TimeoutError:
|
|
295
|
+
continue
|
|
296
|
+
|
|
297
|
+
async def _flush_pending_bumps(self) -> None:
|
|
298
|
+
for namespace in sorted(self._pending_bumps):
|
|
299
|
+
try:
|
|
300
|
+
await self.bump(namespace)
|
|
301
|
+
except asyncio.CancelledError:
|
|
302
|
+
# Defensive if bump() is replaced: cancellation still retains
|
|
303
|
+
# the current namespace and propagates to the flush owner.
|
|
304
|
+
self.request_bump(namespace)
|
|
305
|
+
raise
|
|
306
|
+
except Exception:
|
|
307
|
+
# An abnormal raise must not starve later namespaces.
|
|
308
|
+
self.request_bump(namespace)
|
|
309
|
+
logger.warning(
|
|
310
|
+
"cache_invalidation flush bump raised for namespace %s; kept pending",
|
|
311
|
+
_NAMESPACE_LOG_LABELS.get(namespace, "unknown"),
|
|
312
|
+
exc_info=True,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
async def _poll_once(self) -> bool:
|
|
316
|
+
"""Flush pending bumps and reconcile observed versions once.
|
|
317
|
+
|
|
318
|
+
Returns ``True`` when the baseline/version read completed (callbacks may
|
|
319
|
+
still have failed individually and left their namespace unacknowledged)
|
|
320
|
+
and ``False`` when the read itself failed, so ``prime`` can surface a
|
|
321
|
+
failed startup seed instead of silently leaving the poller uninitialized.
|
|
322
|
+
"""
|
|
323
|
+
await self._flush_pending_bumps()
|
|
324
|
+
session: AsyncSession | None = None
|
|
325
|
+
try:
|
|
326
|
+
session = self._session_factory()
|
|
327
|
+
result = await session.execute(select(CacheInvalidation.namespace, CacheInvalidation.version))
|
|
328
|
+
rows = result.all()
|
|
329
|
+
except Exception:
|
|
330
|
+
self._record_poll_failure()
|
|
331
|
+
return False
|
|
332
|
+
finally:
|
|
333
|
+
if session is not None:
|
|
334
|
+
await close_session(session)
|
|
335
|
+
self._consecutive_poll_failures = 0
|
|
336
|
+
|
|
337
|
+
for namespace, version in rows:
|
|
338
|
+
# Re-read the acknowledged version for THIS namespace immediately
|
|
339
|
+
# before comparing: a concurrent bump_local() may have advanced it
|
|
340
|
+
# (recording a local self-suppression) after this cycle's snapshot
|
|
341
|
+
# read but before we reach this row. Using the freshly-read value
|
|
342
|
+
# keeps the local ack authoritative.
|
|
343
|
+
prev = self._known_versions.get(namespace)
|
|
344
|
+
# Only a STRICTLY newer version is a change. A stale snapshot whose
|
|
345
|
+
# version is <= the acknowledged version (e.g. read before a
|
|
346
|
+
# concurrent local bump advanced _known_versions) must neither fire
|
|
347
|
+
# the callback nor rewind the acknowledged version.
|
|
348
|
+
changed = (prev is not None and version > prev) or (prev is None and self._poll_initialized and version > 0)
|
|
349
|
+
if changed and not await self._run_callbacks(namespace):
|
|
350
|
+
# Do not acknowledge the observed version: a failed callback
|
|
351
|
+
# (e.g. a transient DB error during a snapshot refresh) must be
|
|
352
|
+
# retried on the next cycle, or a replica would permanently
|
|
353
|
+
# miss the invalidation.
|
|
354
|
+
continue
|
|
355
|
+
# Never lower an acknowledged version: a concurrent bump_local()
|
|
356
|
+
# advance must survive an older in-flight poll observation.
|
|
357
|
+
self._known_versions[namespace] = max(self._known_versions.get(namespace, 0), version)
|
|
358
|
+
self._poll_initialized = True
|
|
359
|
+
return True
|
|
360
|
+
|
|
361
|
+
async def _run_callbacks(self, namespace: str) -> bool:
|
|
362
|
+
succeeded = True
|
|
363
|
+
for cb in self._callbacks.get(namespace, []):
|
|
364
|
+
try:
|
|
365
|
+
result = cb()
|
|
366
|
+
if isawaitable(result):
|
|
367
|
+
await result
|
|
368
|
+
except Exception:
|
|
369
|
+
succeeded = False
|
|
370
|
+
logger.warning(
|
|
371
|
+
"cache_invalidation callback failed for namespace %s; retrying next poll cycle",
|
|
372
|
+
_NAMESPACE_LOG_LABELS.get(namespace, "unknown"),
|
|
373
|
+
exc_info=True,
|
|
374
|
+
)
|
|
375
|
+
return succeeded
|
|
376
|
+
|
|
377
|
+
def _record_poll_failure(self) -> None:
|
|
378
|
+
self._consecutive_poll_failures += 1
|
|
379
|
+
if PROMETHEUS_AVAILABLE and cache_invalidation_poll_failures_total is not None:
|
|
380
|
+
cache_invalidation_poll_failures_total.inc()
|
|
381
|
+
if self._consecutive_poll_failures >= _POLL_FAILURES_ERROR_THRESHOLD:
|
|
382
|
+
logger.error(
|
|
383
|
+
"cache_invalidation poll failed %d consecutive times",
|
|
384
|
+
self._consecutive_poll_failures,
|
|
385
|
+
exc_info=True,
|
|
386
|
+
)
|
|
387
|
+
elif self._consecutive_poll_failures >= _POLL_FAILURES_WARNING_THRESHOLD:
|
|
388
|
+
logger.warning(
|
|
389
|
+
"cache_invalidation poll failed %d consecutive times",
|
|
390
|
+
self._consecutive_poll_failures,
|
|
391
|
+
exc_info=True,
|
|
392
|
+
)
|
|
393
|
+
else:
|
|
394
|
+
logger.debug("cache_invalidation poll failed", exc_info=True)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
_poller: CacheInvalidationPoller | None = None
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def get_cache_invalidation_poller() -> CacheInvalidationPoller | None:
|
|
401
|
+
return _poller
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def set_cache_invalidation_poller(poller: CacheInvalidationPoller | None) -> None:
|
|
405
|
+
global _poller
|
|
406
|
+
_poller = poller
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
async def bump_cache_invalidation(namespace: str) -> None:
|
|
410
|
+
"""Publish now or queue a retry; a no-op outside the poller's lifetime."""
|
|
411
|
+
poller = _poller
|
|
412
|
+
if poller is None:
|
|
413
|
+
return
|
|
414
|
+
await poller.bump(namespace)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
async def bump_cache_invalidation_local(namespace: str) -> None:
|
|
418
|
+
"""Best-effort bump for a namespace already invalidated locally by the caller.
|
|
419
|
+
|
|
420
|
+
Notifies peers while suppressing this replica's own re-invalidation for the
|
|
421
|
+
bump it just issued. A no-op outside the lifespan poller's lifetime.
|
|
422
|
+
"""
|
|
423
|
+
poller = _poller
|
|
424
|
+
if poller is None:
|
|
425
|
+
return
|
|
426
|
+
await poller.bump_local(namespace)
|
|
File without changes
|