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,78 @@
|
|
|
1
|
+
"""Request-time resolution of a provider-asserted identity to an account.
|
|
2
|
+
|
|
3
|
+
Shared by the session dependency (which turns the outcome into a principal or
|
|
4
|
+
a 401) and the session endpoint (which turns it into the session response, so
|
|
5
|
+
a refused identity can render the "account not ready" screen). The outcome is
|
|
6
|
+
memoised on the request so both read the same answer.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
from fastapi import Request
|
|
14
|
+
|
|
15
|
+
from app.core.auth.dashboard_mode import DashboardAuthMode, DashboardRequestAuth
|
|
16
|
+
from app.core.auth.dashboard_users_cache import get_dashboard_users_cache
|
|
17
|
+
from app.core.auth.providers import TrustedHeaderProvider
|
|
18
|
+
from app.core.auth.providers.registry import get_auth_provider_registry
|
|
19
|
+
from app.db.models import AuthProviderKind, DashboardAuthProvider, DashboardUser, DashboardUserStatus
|
|
20
|
+
from app.modules.dashboard_users.identity_resolver import (
|
|
21
|
+
DenialCode,
|
|
22
|
+
Denied,
|
|
23
|
+
Resolution,
|
|
24
|
+
get_identity_resolution_cache,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True, slots=True)
|
|
29
|
+
class ExternalResolution:
|
|
30
|
+
"""Who the provider-asserted identity is on this install.
|
|
31
|
+
|
|
32
|
+
``user`` is the active account (``denial`` is ``None``); otherwise
|
|
33
|
+
``denial`` names why the request is refused: ``identity_not_provisioned``
|
|
34
|
+
(no account, none created) or ``account_disabled``.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
provider: DashboardAuthProvider
|
|
38
|
+
user: DashboardUser | None
|
|
39
|
+
denial: DenialCode | None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def resolve_trusted_header_request(
|
|
43
|
+
request: Request, request_auth: DashboardRequestAuth
|
|
44
|
+
) -> ExternalResolution | None:
|
|
45
|
+
"""Resolve the trusted-header identity of ``request``; ``None`` when the provider is not active.
|
|
46
|
+
|
|
47
|
+
Cached on ``request.state`` so the dependency and the session endpoint agree.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
cached = getattr(request.state, "external_resolution", None)
|
|
51
|
+
if isinstance(cached, ExternalResolution):
|
|
52
|
+
return cached
|
|
53
|
+
if request_auth.mode != DashboardAuthMode.TRUSTED_HEADER or not request_auth.actor:
|
|
54
|
+
return None
|
|
55
|
+
active = await get_auth_provider_registry().get_active(
|
|
56
|
+
AuthProviderKind.TRUSTED_HEADER, TrustedHeaderProvider.provider_key, request_auth.mode
|
|
57
|
+
)
|
|
58
|
+
if active is None:
|
|
59
|
+
return None
|
|
60
|
+
identity = TrustedHeaderProvider().identity_from_subject(request_auth.actor, groups=request_auth.groups)
|
|
61
|
+
if identity is None:
|
|
62
|
+
return None
|
|
63
|
+
actor_ip = request.client.host if request.client else None
|
|
64
|
+
result = await get_identity_resolution_cache().resolve(identity, active.row, actor_ip=actor_ip)
|
|
65
|
+
resolution = await _from_result(active.row, result)
|
|
66
|
+
request.state.external_resolution = resolution
|
|
67
|
+
return resolution
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
async def _from_result(provider: DashboardAuthProvider, result: Resolution) -> ExternalResolution:
|
|
71
|
+
if isinstance(result, Denied):
|
|
72
|
+
return ExternalResolution(provider=provider, user=None, denial=result.code)
|
|
73
|
+
user = await get_dashboard_users_cache().get_user(result.user_id)
|
|
74
|
+
if user is None:
|
|
75
|
+
return ExternalResolution(provider=provider, user=None, denial="identity_not_provisioned")
|
|
76
|
+
if user.status != DashboardUserStatus.ACTIVE.value:
|
|
77
|
+
return ExternalResolution(provider=provider, user=None, denial="account_disabled")
|
|
78
|
+
return ExternalResolution(provider=provider, user=user, denial=None)
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import contextlib
|
|
5
|
+
import logging
|
|
6
|
+
import random
|
|
7
|
+
import time
|
|
8
|
+
from collections.abc import AsyncIterator, Awaitable, Callable
|
|
9
|
+
from contextlib import AbstractAsyncContextManager, asynccontextmanager
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from datetime import datetime, timedelta
|
|
12
|
+
from typing import Protocol, cast
|
|
13
|
+
|
|
14
|
+
from app.core.auth.refresh import RefreshError
|
|
15
|
+
from app.core.config.background_jobs import auth_guardian_blocked_by_topology, background_job_enabled
|
|
16
|
+
from app.core.scheduling.leader_election_handle import (
|
|
17
|
+
LeaderElectionLike as _LeaderElectionLike,
|
|
18
|
+
)
|
|
19
|
+
from app.core.scheduling.leader_election_handle import (
|
|
20
|
+
get_leader_election as _get_leader_election,
|
|
21
|
+
)
|
|
22
|
+
from app.core.utils.time import to_utc_naive, utcnow
|
|
23
|
+
from app.db.models import Account, AccountStatus
|
|
24
|
+
from app.db.session import get_background_session
|
|
25
|
+
from app.modules.accounts.auth_manager import AuthManager
|
|
26
|
+
from app.modules.accounts.repository import AccountsRepository
|
|
27
|
+
from app.modules.proxy.account_cache import get_account_selection_cache
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
# Guardian cadence and backoff tuning (fixed; issue #1340 / PRINCIPLES.md P2).
|
|
32
|
+
# The guardian is a background refresher; these values are implementation
|
|
33
|
+
# details, not operator contract. ``AuthGuardianScheduler`` keeps them as
|
|
34
|
+
# constructor fields so tests can exercise the behavior.
|
|
35
|
+
_INTERVAL_SECONDS = 21600
|
|
36
|
+
_MAX_REFRESH_AGE_SECONDS = 43200
|
|
37
|
+
_BATCH_SIZE = 100
|
|
38
|
+
_CONCURRENCY = 3
|
|
39
|
+
_JITTER_SECONDS = 300.0
|
|
40
|
+
_FAILURE_BACKOFF_BASE_SECONDS = 300.0
|
|
41
|
+
_FAILURE_BACKOFF_MAX_SECONDS = 3600.0
|
|
42
|
+
|
|
43
|
+
# RATE_LIMITED and QUOTA_EXCEEDED recover to ACTIVE through usage-refresh
|
|
44
|
+
# reconciliation, then become guardian-eligible within the max-age window.
|
|
45
|
+
_AUTH_GUARDIAN_ELIGIBLE_STATUSES = frozenset({AccountStatus.ACTIVE, AccountStatus.PAUSED})
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class _AccountsRepositoryLike(Protocol):
|
|
49
|
+
async def list_accounts(self, *, refresh_existing: bool = False) -> list[Account]: ...
|
|
50
|
+
|
|
51
|
+
async def get_by_id(self, account_id: str) -> Account | None: ...
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class _AuthManagerLike(Protocol):
|
|
55
|
+
async def ensure_fresh(self, account: Account, *, force: bool = False) -> Account: ...
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
_RepoFactory = Callable[[], AbstractAsyncContextManager[_AccountsRepositoryLike]]
|
|
59
|
+
_AuthManagerFactory = Callable[[_AccountsRepositoryLike], _AuthManagerLike]
|
|
60
|
+
_LeaderElectionFactory = Callable[[], _LeaderElectionLike]
|
|
61
|
+
_Sleep = Callable[[float], Awaitable[None]]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(slots=True)
|
|
65
|
+
class _FailureBackoff:
|
|
66
|
+
attempts: int
|
|
67
|
+
retry_after_monotonic: float
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(slots=True)
|
|
71
|
+
class AuthGuardianScheduler:
|
|
72
|
+
interval_seconds: int
|
|
73
|
+
enabled: bool
|
|
74
|
+
max_age_seconds: int
|
|
75
|
+
batch_size: int
|
|
76
|
+
concurrency: int
|
|
77
|
+
jitter_seconds: float
|
|
78
|
+
failure_backoff_base_seconds: float = 300.0
|
|
79
|
+
failure_backoff_max_seconds: float = 3600.0
|
|
80
|
+
# When leader election is disabled, each refresh pass first counts live
|
|
81
|
+
# bridge-ring replicas and skips the pass if more than one is registered
|
|
82
|
+
# (the static instance ring is empty in Helm/compose deployments, so the
|
|
83
|
+
# build-time guard alone cannot see dynamically registered replicas).
|
|
84
|
+
# Defaults to True so directly constructed schedulers skip the dynamic
|
|
85
|
+
# check; build_auth_guardian_scheduler wires the real setting.
|
|
86
|
+
leader_election_enabled: bool = True
|
|
87
|
+
# M2 background jobs: the loop always runs; each pass reads the effective
|
|
88
|
+
# ``auth_guardian_enabled`` toggle (dashboard column, else the deprecated
|
|
89
|
+
# env alias, else the default) from the settings cache and skips while it is
|
|
90
|
+
# False, so a dashboard change applies on the next tick without a restart.
|
|
91
|
+
# ``topology_blocked`` is the static multi-replica-without-election gate
|
|
92
|
+
# computed at build time; the dashboard toggle cannot override it.
|
|
93
|
+
dashboard_enabled: Callable[[], Awaitable[bool]] = field(default_factory=lambda: _dashboard_guardian_enabled)
|
|
94
|
+
topology_blocked: bool = False
|
|
95
|
+
live_replica_count: Callable[[], Awaitable[int]] = field(default_factory=lambda: _count_live_bridge_ring_members)
|
|
96
|
+
leader_election_factory: _LeaderElectionFactory = field(default_factory=lambda: _get_leader_election)
|
|
97
|
+
repo_factory: _RepoFactory = field(default_factory=lambda: _default_accounts_repo_factory)
|
|
98
|
+
auth_manager_factory: _AuthManagerFactory = field(default_factory=lambda: _default_auth_manager_factory)
|
|
99
|
+
sleep: _Sleep = field(default_factory=lambda: asyncio.sleep)
|
|
100
|
+
now: Callable[[], datetime] = field(default_factory=lambda: utcnow)
|
|
101
|
+
_task: asyncio.Task[None] | None = None
|
|
102
|
+
_stop: asyncio.Event = field(default_factory=asyncio.Event)
|
|
103
|
+
_lock: asyncio.Lock = field(default_factory=asyncio.Lock)
|
|
104
|
+
_failures: dict[str, _FailureBackoff] = field(default_factory=dict)
|
|
105
|
+
|
|
106
|
+
async def start(self) -> None:
|
|
107
|
+
if not self.enabled:
|
|
108
|
+
return
|
|
109
|
+
if self._task and not self._task.done():
|
|
110
|
+
return
|
|
111
|
+
self._stop.clear()
|
|
112
|
+
self._task = asyncio.create_task(self._run_loop())
|
|
113
|
+
|
|
114
|
+
async def stop(self) -> None:
|
|
115
|
+
self._stop.set()
|
|
116
|
+
if self._task is not None:
|
|
117
|
+
self._task.cancel()
|
|
118
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
119
|
+
await self._task
|
|
120
|
+
self._task = None
|
|
121
|
+
|
|
122
|
+
async def _run_loop(self) -> None:
|
|
123
|
+
while not self._stop.is_set():
|
|
124
|
+
jitter = _jitter_delay(self.jitter_seconds)
|
|
125
|
+
if jitter > 0:
|
|
126
|
+
try:
|
|
127
|
+
await asyncio.wait_for(self._stop.wait(), timeout=jitter)
|
|
128
|
+
break
|
|
129
|
+
except asyncio.TimeoutError:
|
|
130
|
+
pass
|
|
131
|
+
try:
|
|
132
|
+
await self._refresh_once()
|
|
133
|
+
except Exception:
|
|
134
|
+
logger.exception("Auth Guardian refresh pass failed")
|
|
135
|
+
try:
|
|
136
|
+
await asyncio.wait_for(self._stop.wait(), timeout=self.interval_seconds)
|
|
137
|
+
except asyncio.TimeoutError:
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
async def _refresh_once(self) -> None:
|
|
141
|
+
if not await self.dashboard_enabled():
|
|
142
|
+
logger.debug("Auth Guardian skipped refresh pass: disabled in the dashboard settings")
|
|
143
|
+
return
|
|
144
|
+
if self.topology_blocked:
|
|
145
|
+
# The builder already logged this once at WARNING; repeating it
|
|
146
|
+
# every pass for a static condition would only be noise.
|
|
147
|
+
logger.debug(
|
|
148
|
+
"Auth Guardian skipped refresh pass: multi-replica deployment without leader election; "
|
|
149
|
+
"set CODEX_LB_LEADER_ELECTION_ENABLED=true to run it leader-gated"
|
|
150
|
+
)
|
|
151
|
+
return
|
|
152
|
+
if not self.leader_election_enabled:
|
|
153
|
+
live_replicas = await self.live_replica_count()
|
|
154
|
+
if live_replicas > 1:
|
|
155
|
+
logger.warning(
|
|
156
|
+
"Auth Guardian skipped refresh pass: %d live replicas registered in the bridge ring "
|
|
157
|
+
"while leader election is disabled; set CODEX_LB_LEADER_ELECTION_ENABLED=true so a "
|
|
158
|
+
"single elected replica performs proactive refresh work",
|
|
159
|
+
live_replicas,
|
|
160
|
+
)
|
|
161
|
+
return
|
|
162
|
+
await self.leader_election_factory().run_if_leader(self._refresh_as_leader)
|
|
163
|
+
|
|
164
|
+
async def _refresh_as_leader(self) -> None:
|
|
165
|
+
async with self._lock:
|
|
166
|
+
async with self.repo_factory() as repo:
|
|
167
|
+
accounts = await repo.list_accounts(refresh_existing=True)
|
|
168
|
+
candidates = select_auth_guardian_candidates(
|
|
169
|
+
accounts,
|
|
170
|
+
now=self.now(),
|
|
171
|
+
max_age_seconds=self.max_age_seconds,
|
|
172
|
+
limit=len(accounts),
|
|
173
|
+
)
|
|
174
|
+
candidate_ids = [account.id for account in candidates if not self._in_backoff(account.id)]
|
|
175
|
+
candidate_ids = candidate_ids[: max(0, self.batch_size)]
|
|
176
|
+
if not candidate_ids:
|
|
177
|
+
return
|
|
178
|
+
semaphore = asyncio.Semaphore(max(1, self.concurrency))
|
|
179
|
+
await asyncio.gather(*(self._refresh_candidate(account_id, semaphore) for account_id in candidate_ids))
|
|
180
|
+
|
|
181
|
+
async def _refresh_candidate(self, account_id: str, semaphore: asyncio.Semaphore) -> None:
|
|
182
|
+
if self._in_backoff(account_id):
|
|
183
|
+
return
|
|
184
|
+
async with semaphore:
|
|
185
|
+
async with self.repo_factory() as repo:
|
|
186
|
+
account = await repo.get_by_id(account_id)
|
|
187
|
+
if account is None:
|
|
188
|
+
self._failures.pop(account_id, None)
|
|
189
|
+
return
|
|
190
|
+
source_status = account.status.value
|
|
191
|
+
if not _auth_guardian_account_is_stale_eligible(
|
|
192
|
+
account,
|
|
193
|
+
now=self.now(),
|
|
194
|
+
max_age_seconds=self.max_age_seconds,
|
|
195
|
+
):
|
|
196
|
+
return
|
|
197
|
+
manager = self.auth_manager_factory(repo)
|
|
198
|
+
try:
|
|
199
|
+
refresh_task = asyncio.create_task(manager.ensure_fresh(account, force=True))
|
|
200
|
+
try:
|
|
201
|
+
await asyncio.shield(refresh_task)
|
|
202
|
+
except asyncio.CancelledError:
|
|
203
|
+
with contextlib.suppress(Exception):
|
|
204
|
+
await refresh_task
|
|
205
|
+
raise
|
|
206
|
+
except RefreshError as exc:
|
|
207
|
+
self._record_failure(account_id)
|
|
208
|
+
if exc.is_permanent:
|
|
209
|
+
get_account_selection_cache().invalidate()
|
|
210
|
+
logger.warning(
|
|
211
|
+
"Auth Guardian refresh failed account_id=%s account_alias=%s status=%s code=%s permanent=%s "
|
|
212
|
+
"transport=%s",
|
|
213
|
+
account.id,
|
|
214
|
+
_safe_account_alias(account),
|
|
215
|
+
source_status,
|
|
216
|
+
exc.code,
|
|
217
|
+
exc.is_permanent,
|
|
218
|
+
exc.transport_error,
|
|
219
|
+
)
|
|
220
|
+
return
|
|
221
|
+
except Exception as exc:
|
|
222
|
+
self._record_failure(account_id)
|
|
223
|
+
logger.warning(
|
|
224
|
+
"Auth Guardian refresh failed account_id=%s account_alias=%s status=%s error_type=%s",
|
|
225
|
+
account.id,
|
|
226
|
+
_safe_account_alias(account),
|
|
227
|
+
source_status,
|
|
228
|
+
exc.__class__.__name__,
|
|
229
|
+
exc_info=True,
|
|
230
|
+
)
|
|
231
|
+
return
|
|
232
|
+
self._failures.pop(account_id, None)
|
|
233
|
+
get_account_selection_cache().invalidate()
|
|
234
|
+
logger.info(
|
|
235
|
+
"Auth Guardian refreshed account_id=%s account_alias=%s status=%s",
|
|
236
|
+
account.id,
|
|
237
|
+
_safe_account_alias(account),
|
|
238
|
+
source_status,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
def _in_backoff(self, account_id: str) -> bool:
|
|
242
|
+
failure = self._failures.get(account_id)
|
|
243
|
+
if failure is None:
|
|
244
|
+
return False
|
|
245
|
+
if failure.retry_after_monotonic > time.monotonic():
|
|
246
|
+
return True
|
|
247
|
+
return False
|
|
248
|
+
|
|
249
|
+
def _record_failure(self, account_id: str) -> None:
|
|
250
|
+
previous = self._failures.get(account_id)
|
|
251
|
+
attempts = 1 if previous is None else previous.attempts + 1
|
|
252
|
+
base = max(0.0, float(self.failure_backoff_base_seconds))
|
|
253
|
+
cap = max(base, float(self.failure_backoff_max_seconds))
|
|
254
|
+
delay = min(cap, base * (2 ** min(attempts - 1, 6)))
|
|
255
|
+
delay += _jitter_delay(self.jitter_seconds)
|
|
256
|
+
self._failures[account_id] = _FailureBackoff(
|
|
257
|
+
attempts=attempts,
|
|
258
|
+
retry_after_monotonic=time.monotonic() + delay,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def select_auth_guardian_candidates(
|
|
263
|
+
accounts: list[Account],
|
|
264
|
+
*,
|
|
265
|
+
now: datetime,
|
|
266
|
+
max_age_seconds: int,
|
|
267
|
+
limit: int,
|
|
268
|
+
) -> list[Account]:
|
|
269
|
+
candidates = [
|
|
270
|
+
account
|
|
271
|
+
for account in accounts
|
|
272
|
+
if _auth_guardian_account_is_stale_eligible(
|
|
273
|
+
account,
|
|
274
|
+
now=now,
|
|
275
|
+
max_age_seconds=max_age_seconds,
|
|
276
|
+
)
|
|
277
|
+
]
|
|
278
|
+
candidates.sort(key=lambda account: to_utc_naive(account.last_refresh))
|
|
279
|
+
return candidates[: max(0, limit)]
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def build_auth_guardian_scheduler() -> AuthGuardianScheduler:
|
|
283
|
+
from app.core.config.settings import get_settings
|
|
284
|
+
|
|
285
|
+
settings = get_settings()
|
|
286
|
+
# Deliberate exception to the "disabled election means every replica is
|
|
287
|
+
# leader" escape hatch: concurrent force token refreshes across replicas
|
|
288
|
+
# can invalidate rotated refresh tokens, so without election the guardian
|
|
289
|
+
# must not run in a multi-replica ring at all. The loop still starts (M2:
|
|
290
|
+
# the dashboard toggle is read at every pass), but every pass skips.
|
|
291
|
+
topology_blocked = auth_guardian_blocked_by_topology(settings)
|
|
292
|
+
if topology_blocked:
|
|
293
|
+
logger.warning(
|
|
294
|
+
"Auth Guardian disabled: multi-replica deployment without leader election; "
|
|
295
|
+
"set CODEX_LB_LEADER_ELECTION_ENABLED=true to run it leader-gated"
|
|
296
|
+
)
|
|
297
|
+
return AuthGuardianScheduler(
|
|
298
|
+
interval_seconds=_INTERVAL_SECONDS,
|
|
299
|
+
enabled=True,
|
|
300
|
+
topology_blocked=topology_blocked,
|
|
301
|
+
max_age_seconds=_MAX_REFRESH_AGE_SECONDS,
|
|
302
|
+
batch_size=_BATCH_SIZE,
|
|
303
|
+
concurrency=_CONCURRENCY,
|
|
304
|
+
jitter_seconds=_JITTER_SECONDS,
|
|
305
|
+
failure_backoff_base_seconds=_FAILURE_BACKOFF_BASE_SECONDS,
|
|
306
|
+
failure_backoff_max_seconds=_FAILURE_BACKOFF_MAX_SECONDS,
|
|
307
|
+
leader_election_enabled=settings.leader_election_enabled,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _auth_guardian_account_is_stale_eligible(
|
|
312
|
+
account: Account,
|
|
313
|
+
*,
|
|
314
|
+
now: datetime,
|
|
315
|
+
max_age_seconds: int,
|
|
316
|
+
) -> bool:
|
|
317
|
+
if account.status not in _AUTH_GUARDIAN_ELIGIBLE_STATUSES:
|
|
318
|
+
return False
|
|
319
|
+
age = to_utc_naive(now) - to_utc_naive(account.last_refresh)
|
|
320
|
+
return age > timedelta(seconds=max_age_seconds)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
async def _dashboard_guardian_enabled() -> bool:
|
|
324
|
+
return await background_job_enabled("auth_guardian_enabled")
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
async def _count_live_bridge_ring_members() -> int:
|
|
328
|
+
from sqlalchemy import func, select
|
|
329
|
+
|
|
330
|
+
from app.db.models import BridgeRingMember
|
|
331
|
+
from app.modules.proxy.ring_membership import RING_STALE_THRESHOLD_SECONDS
|
|
332
|
+
|
|
333
|
+
cutoff = utcnow() - timedelta(seconds=RING_STALE_THRESHOLD_SECONDS)
|
|
334
|
+
async with get_background_session() as session:
|
|
335
|
+
result = await session.execute(
|
|
336
|
+
select(func.count()).select_from(BridgeRingMember).where(BridgeRingMember.last_heartbeat_at >= cutoff)
|
|
337
|
+
)
|
|
338
|
+
return int(result.scalar_one() or 0)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@asynccontextmanager
|
|
342
|
+
async def _default_accounts_repo_factory() -> AsyncIterator[AccountsRepository]:
|
|
343
|
+
async with get_background_session() as session:
|
|
344
|
+
yield AccountsRepository(session)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _default_auth_manager_factory(repo: _AccountsRepositoryLike) -> _AuthManagerLike:
|
|
348
|
+
return AuthManager(cast(AccountsRepository, repo), refresh_repo_factory=_default_accounts_repo_factory)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _jitter_delay(max_seconds: float) -> float:
|
|
352
|
+
if max_seconds <= 0:
|
|
353
|
+
return 0.0
|
|
354
|
+
return random.uniform(0.0, max_seconds)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _safe_account_alias(account: Account) -> str:
|
|
358
|
+
alias = (account.alias or "").strip()
|
|
359
|
+
if alias:
|
|
360
|
+
return alias[:64]
|
|
361
|
+
return _mask_email(account.email)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _mask_email(email: str) -> str:
|
|
365
|
+
if "@" not in email:
|
|
366
|
+
return email[:2] + "***" if email else ""
|
|
367
|
+
local, domain = email.split("@", 1)
|
|
368
|
+
if not local:
|
|
369
|
+
return f"***@{domain}"
|
|
370
|
+
if len(local) == 1:
|
|
371
|
+
masked_local = f"{local}***"
|
|
372
|
+
else:
|
|
373
|
+
masked_local = f"{local[0]}***{local[-1]}"
|
|
374
|
+
return f"{masked_local}@{domain}"
|
app/core/auth/models.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, StrictStr, field_validator
|
|
4
|
+
|
|
5
|
+
from app.core.types import JsonObject
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class OAuthTokenPayload(BaseModel):
|
|
9
|
+
model_config = ConfigDict(extra="ignore")
|
|
10
|
+
|
|
11
|
+
access_token: StrictStr | None = None
|
|
12
|
+
refresh_token: StrictStr | None = None
|
|
13
|
+
id_token: StrictStr | None = None
|
|
14
|
+
authorization_code: StrictStr | None = None
|
|
15
|
+
code_verifier: StrictStr | None = None
|
|
16
|
+
error: JsonObject | StrictStr | None = None
|
|
17
|
+
error_description: StrictStr | None = None
|
|
18
|
+
message: StrictStr | None = None
|
|
19
|
+
error_code: StrictStr | None = None
|
|
20
|
+
code: StrictStr | None = None
|
|
21
|
+
status: StrictStr | None = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DeviceCodePayload(BaseModel):
|
|
25
|
+
model_config = ConfigDict(extra="ignore")
|
|
26
|
+
|
|
27
|
+
device_auth_id: StrictStr | None = None
|
|
28
|
+
user_code: StrictStr | None = Field(
|
|
29
|
+
default=None,
|
|
30
|
+
validation_alias=AliasChoices("user_code", "usercode"),
|
|
31
|
+
)
|
|
32
|
+
interval: int | None = None
|
|
33
|
+
expires_in: int | None = None
|
|
34
|
+
expires_at: StrictStr | None = None
|
|
35
|
+
|
|
36
|
+
@field_validator("interval", mode="before")
|
|
37
|
+
@classmethod
|
|
38
|
+
def _parse_interval(cls, value: int | str | None) -> int | None:
|
|
39
|
+
if value is None:
|
|
40
|
+
return None
|
|
41
|
+
if isinstance(value, int):
|
|
42
|
+
return value
|
|
43
|
+
if isinstance(value, str):
|
|
44
|
+
stripped = value.strip()
|
|
45
|
+
if not stripped:
|
|
46
|
+
return None
|
|
47
|
+
if stripped.isdigit():
|
|
48
|
+
return int(stripped)
|
|
49
|
+
raise ValueError("Invalid interval")
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Sign-in providers: how a request proves who is calling.
|
|
2
|
+
|
|
3
|
+
A provider either recognises the caller on every request from evidence the
|
|
4
|
+
request carries (``resolve_identity``: the reverse-proxy header today, a
|
|
5
|
+
signed assertion tomorrow) or runs a redirect dance (``begin_login`` /
|
|
6
|
+
``complete_login``: OIDC, Phase 3). Every provider hands the same
|
|
7
|
+
:class:`ExternalIdentity` to the shared identity resolver, which maps it to a
|
|
8
|
+
dashboard account; providers never touch accounts themselves.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Protocol, runtime_checkable
|
|
15
|
+
|
|
16
|
+
from fastapi import Request
|
|
17
|
+
|
|
18
|
+
from app.core.auth.dashboard_mode import DashboardAuthMode, get_dashboard_request_auth
|
|
19
|
+
from app.db.models import AuthProviderKind
|
|
20
|
+
from app.modules.auth_providers.seed import DEFAULT_PROVIDER_KEY
|
|
21
|
+
from app.modules.dashboard_users.repository import is_valid_email, normalize_email
|
|
22
|
+
|
|
23
|
+
#: Column widths of ``dashboard_identities.subject`` / ``display_name``; the API validates the same bounds.
|
|
24
|
+
MAX_SUBJECT_LENGTH = 512
|
|
25
|
+
MAX_DISPLAY_NAME_LENGTH = 128
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"DEFAULT_PROVIDER_KEY",
|
|
29
|
+
"MAX_DISPLAY_NAME_LENGTH",
|
|
30
|
+
"MAX_SUBJECT_LENGTH",
|
|
31
|
+
"AuthProvider",
|
|
32
|
+
"ExternalIdentity",
|
|
33
|
+
"PasswordProvider",
|
|
34
|
+
"TrustedHeaderProvider",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True, slots=True)
|
|
39
|
+
class ExternalIdentity:
|
|
40
|
+
"""What a provider learned about the caller, before any account lookup.
|
|
41
|
+
|
|
42
|
+
``subject`` is the stable identifier the provider guarantees (the header
|
|
43
|
+
value, an OIDC ``sub``); ``email`` and ``display_name`` are display and
|
|
44
|
+
linking hints; ``groups`` is the IdP's group snapshot (case-folded), which
|
|
45
|
+
role mappings match on.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
provider: str
|
|
49
|
+
provider_key: str
|
|
50
|
+
subject: str
|
|
51
|
+
email: str | None = None
|
|
52
|
+
display_name: str | None = None
|
|
53
|
+
groups: tuple[str, ...] = ()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@runtime_checkable
|
|
57
|
+
class AuthProvider(Protocol):
|
|
58
|
+
"""One way of signing in.
|
|
59
|
+
|
|
60
|
+
Header-style providers implement :meth:`resolve_identity`. Redirect-style
|
|
61
|
+
providers (OIDC) add ``begin_login(request) -> RedirectResponse`` and
|
|
62
|
+
``complete_login(request) -> ExternalIdentity`` when they arrive; both
|
|
63
|
+
still end in the shared identity resolver. ``kind`` matches
|
|
64
|
+
``dashboard_auth_providers.kind`` and ``provider_key`` the row's key.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def kind(self) -> AuthProviderKind: ...
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def provider_key(self) -> str: ...
|
|
72
|
+
|
|
73
|
+
def resolve_identity(self, request: Request) -> ExternalIdentity | None:
|
|
74
|
+
"""The caller's identity when the request carries the provider's evidence, else ``None``."""
|
|
75
|
+
...
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class PasswordProvider:
|
|
79
|
+
"""Local username/password sign-in. The login flow itself lives in
|
|
80
|
+
``dashboard_auth``; this marker exists so the provider list and the
|
|
81
|
+
providers table have one entry per way of signing in."""
|
|
82
|
+
|
|
83
|
+
kind = AuthProviderKind.PASSWORD
|
|
84
|
+
provider_key = DEFAULT_PROVIDER_KEY
|
|
85
|
+
|
|
86
|
+
def resolve_identity(self, request: Request) -> ExternalIdentity | None:
|
|
87
|
+
del request
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class TrustedHeaderProvider:
|
|
92
|
+
"""The reverse-proxy identity header (``CODEX_LB_DASHBOARD_AUTH_PROXY_HEADER``).
|
|
93
|
+
|
|
94
|
+
Reuses the request-auth resolution that already checks the trusted-proxy
|
|
95
|
+
peer and the singular header. The subject is case-folded: two proxies
|
|
96
|
+
spelling the same person differently must not become two accounts. The
|
|
97
|
+
raw value is kept as the display name and, when it parses as an e-mail,
|
|
98
|
+
as the identity's e-mail. Groups arrive on the same request-auth value,
|
|
99
|
+
read from ``CODEX_LB_DASHBOARD_AUTH_PROXY_GROUPS_HEADER``.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
kind = AuthProviderKind.TRUSTED_HEADER
|
|
103
|
+
provider_key = DEFAULT_PROVIDER_KEY
|
|
104
|
+
|
|
105
|
+
def resolve_identity(self, request: Request) -> ExternalIdentity | None:
|
|
106
|
+
request_auth = get_dashboard_request_auth(request)
|
|
107
|
+
if request_auth is None or request_auth.mode != DashboardAuthMode.TRUSTED_HEADER or not request_auth.actor:
|
|
108
|
+
return None
|
|
109
|
+
return self.identity_from_subject(request_auth.actor, groups=request_auth.groups)
|
|
110
|
+
|
|
111
|
+
def identity_from_subject(self, raw_subject: str, *, groups: tuple[str, ...] = ()) -> ExternalIdentity | None:
|
|
112
|
+
"""``None`` when the value cannot be an identity here (empty or wider than the subject column).
|
|
113
|
+
|
|
114
|
+
``groups`` defaults to none so the paths without a request (an invite's
|
|
115
|
+
expected identity) can still name an identity.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
subject = raw_subject.strip()
|
|
119
|
+
if not subject or len(subject) > MAX_SUBJECT_LENGTH:
|
|
120
|
+
return None
|
|
121
|
+
email = normalize_email(subject)
|
|
122
|
+
return ExternalIdentity(
|
|
123
|
+
provider=self.kind.value,
|
|
124
|
+
provider_key=self.provider_key,
|
|
125
|
+
subject=subject.casefold(),
|
|
126
|
+
email=email if email is not None and is_valid_email(email) else None,
|
|
127
|
+
display_name=subject[:MAX_DISPLAY_NAME_LENGTH],
|
|
128
|
+
groups=groups,
|
|
129
|
+
)
|