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,118 @@
|
|
|
1
|
+
"""Process cache over ``dashboard_auth_providers`` joined with the auth mode.
|
|
2
|
+
|
|
3
|
+
Same shape as :class:`app.core.auth.dashboard_users_cache.DashboardUsersCache`:
|
|
4
|
+
a 5 s TTL bounds staleness inside one replica, and a provider write bumps the
|
|
5
|
+
``dashboard_users`` invalidation namespace (providers govern how identities
|
|
6
|
+
become users, so the two caches share one bus and one callback).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import time
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
import anyio
|
|
15
|
+
|
|
16
|
+
from app.core.auth.dashboard_mode import DashboardAuthMode
|
|
17
|
+
from app.core.auth.providers import AuthProvider, PasswordProvider, TrustedHeaderProvider
|
|
18
|
+
from app.core.auth.providers.oidc import get_oidc_provider
|
|
19
|
+
from app.core.cache.invalidation import NAMESPACE_DASHBOARD_USERS, get_cache_invalidation_poller
|
|
20
|
+
from app.db.models import AuthProviderKind, DashboardAuthProvider
|
|
21
|
+
from app.db.session import SessionLocal
|
|
22
|
+
from app.modules.auth_providers.repository import AuthProvidersRepository
|
|
23
|
+
|
|
24
|
+
_IMPLEMENTATIONS: dict[AuthProviderKind, AuthProvider] = {
|
|
25
|
+
AuthProviderKind.PASSWORD: PasswordProvider(),
|
|
26
|
+
AuthProviderKind.TRUSTED_HEADER: TrustedHeaderProvider(),
|
|
27
|
+
AuthProviderKind.OIDC: get_oidc_provider(),
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def provider_active(row: DashboardAuthProvider, mode: DashboardAuthMode) -> bool:
|
|
32
|
+
"""A provider serves sign-ins when its row is enabled and the auth mode admits its kind.
|
|
33
|
+
|
|
34
|
+
``password`` follows the mode's existing rules (management is refused in
|
|
35
|
+
``disabled`` mode elsewhere), ``trusted_header`` needs the matching mode,
|
|
36
|
+
``oidc`` serves ``standard`` and ``trusted_header`` but never ``disabled``
|
|
37
|
+
(an install that has turned dashboard authentication off must not grow a
|
|
38
|
+
sign-in flow that mints sessions and provisions accounts), and a kind with
|
|
39
|
+
no implementation is never active.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
if not row.enabled:
|
|
43
|
+
return False
|
|
44
|
+
try:
|
|
45
|
+
kind = AuthProviderKind(row.kind)
|
|
46
|
+
except ValueError:
|
|
47
|
+
return False
|
|
48
|
+
if kind not in _IMPLEMENTATIONS:
|
|
49
|
+
return False
|
|
50
|
+
if kind is AuthProviderKind.TRUSTED_HEADER:
|
|
51
|
+
return mode == DashboardAuthMode.TRUSTED_HEADER
|
|
52
|
+
if kind is AuthProviderKind.OIDC:
|
|
53
|
+
return mode != DashboardAuthMode.DISABLED
|
|
54
|
+
return True
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True, slots=True)
|
|
58
|
+
class ActiveProvider:
|
|
59
|
+
row: DashboardAuthProvider
|
|
60
|
+
provider: AuthProvider
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class AuthProviderRegistry:
|
|
64
|
+
def __init__(self, *, ttl_seconds: float = 5.0) -> None:
|
|
65
|
+
if ttl_seconds <= 0:
|
|
66
|
+
raise ValueError("ttl_seconds must be positive")
|
|
67
|
+
self._ttl_seconds = ttl_seconds
|
|
68
|
+
self._rows: tuple[tuple[DashboardAuthProvider, ...], float] | None = None
|
|
69
|
+
self._lock = anyio.Lock()
|
|
70
|
+
|
|
71
|
+
async def rows(self) -> tuple[DashboardAuthProvider, ...]:
|
|
72
|
+
now = time.monotonic()
|
|
73
|
+
if self._rows is not None and now - self._rows[1] < self._ttl_seconds:
|
|
74
|
+
return self._rows[0]
|
|
75
|
+
async with self._lock:
|
|
76
|
+
now = time.monotonic()
|
|
77
|
+
if self._rows is not None and now - self._rows[1] < self._ttl_seconds:
|
|
78
|
+
return self._rows[0]
|
|
79
|
+
async with SessionLocal() as session:
|
|
80
|
+
rows = tuple(await AuthProvidersRepository(session).list_providers())
|
|
81
|
+
self._rows = (rows, now)
|
|
82
|
+
return rows
|
|
83
|
+
|
|
84
|
+
async def get_active_providers(self, mode: DashboardAuthMode) -> list[ActiveProvider]:
|
|
85
|
+
"""Active providers in table order; the password provider always lists first."""
|
|
86
|
+
|
|
87
|
+
active = [
|
|
88
|
+
ActiveProvider(row=row, provider=_IMPLEMENTATIONS[AuthProviderKind(row.kind)])
|
|
89
|
+
for row in await self.rows()
|
|
90
|
+
if provider_active(row, mode)
|
|
91
|
+
]
|
|
92
|
+
return sorted(active, key=lambda item: item.row.kind != AuthProviderKind.PASSWORD.value)
|
|
93
|
+
|
|
94
|
+
async def get_active(
|
|
95
|
+
self, kind: AuthProviderKind, provider_key: str, mode: DashboardAuthMode
|
|
96
|
+
) -> ActiveProvider | None:
|
|
97
|
+
for item in await self.get_active_providers(mode):
|
|
98
|
+
if item.row.kind == kind.value and item.row.provider_key == provider_key:
|
|
99
|
+
return item
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
def clear(self) -> None:
|
|
103
|
+
self._rows = None
|
|
104
|
+
|
|
105
|
+
async def invalidate(self, *, propagate: bool = True) -> None:
|
|
106
|
+
async with self._lock:
|
|
107
|
+
self.clear()
|
|
108
|
+
if propagate:
|
|
109
|
+
poller = get_cache_invalidation_poller()
|
|
110
|
+
if poller is not None:
|
|
111
|
+
await poller.bump(NAMESPACE_DASHBOARD_USERS)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
_registry = AuthProviderRegistry()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def get_auth_provider_registry() -> AuthProviderRegistry:
|
|
118
|
+
return _registry
|
app/core/auth/refresh.py
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import contextvars
|
|
5
|
+
import logging
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import datetime, timedelta
|
|
8
|
+
from typing import Final
|
|
9
|
+
|
|
10
|
+
import aiohttp
|
|
11
|
+
from pydantic import ValidationError
|
|
12
|
+
|
|
13
|
+
from app.core.auth import (
|
|
14
|
+
OpenAIAuthClaims,
|
|
15
|
+
clean_account_identity_part,
|
|
16
|
+
extract_id_token_claims,
|
|
17
|
+
normalize_seat_type,
|
|
18
|
+
resolve_seat_identity,
|
|
19
|
+
)
|
|
20
|
+
from app.core.auth.models import OAuthTokenPayload
|
|
21
|
+
from app.core.balancer import PERMANENT_FAILURE_CODES
|
|
22
|
+
from app.core.clients.codex import (
|
|
23
|
+
CodexClient,
|
|
24
|
+
CodexTransportError,
|
|
25
|
+
create_codex_session,
|
|
26
|
+
require_route_or_direct_egress_opt_in,
|
|
27
|
+
)
|
|
28
|
+
from app.core.clients.http import _safe_json, lease_http_session
|
|
29
|
+
from app.core.clients.oauth import _extract_error_code, _extract_error_message
|
|
30
|
+
from app.core.config.settings import AUTH_BASE_URL, OAUTH_CLIENT_ID, OAUTH_SCOPE
|
|
31
|
+
from app.core.resilience.network_recovery import (
|
|
32
|
+
PROCESS_NETWORK_UNAVAILABLE_CODE,
|
|
33
|
+
is_pre_dispatch_connection_failure,
|
|
34
|
+
is_proxy_endpoint_failure,
|
|
35
|
+
process_network_error_code,
|
|
36
|
+
)
|
|
37
|
+
from app.core.types import JsonObject
|
|
38
|
+
from app.core.upstream_proxy import ResolvedUpstreamRoute
|
|
39
|
+
from app.core.utils.request_id import get_request_id
|
|
40
|
+
from app.core.utils.time import to_utc_naive, utcnow
|
|
41
|
+
|
|
42
|
+
# Maximum age of an account's last successful refresh before the next request
|
|
43
|
+
# proactively exchanges its refresh token (fixed; issue #1340 / PRINCIPLES.md
|
|
44
|
+
# P2). This module attribute is the single source of the window: tests
|
|
45
|
+
# monkeypatch it, and the traffic-parity canary suppresses proactive refresh by
|
|
46
|
+
# stamping its isolated credential inside the window rather than widening it.
|
|
47
|
+
TOKEN_REFRESH_INTERVAL_DAYS: Final[int] = 8
|
|
48
|
+
# Total timeout of one refresh-token exchange (fixed; issue #1340 / PRINCIPLES.md
|
|
49
|
+
# P2). Per-request budgets may only clamp it lower through the override below.
|
|
50
|
+
TOKEN_REFRESH_TIMEOUT_SECONDS: Final[float] = 8.0
|
|
51
|
+
|
|
52
|
+
logger = logging.getLogger(__name__)
|
|
53
|
+
_TOKEN_REFRESH_TIMEOUT_OVERRIDE: contextvars.ContextVar[float | None] = contextvars.ContextVar(
|
|
54
|
+
"token_refresh_timeout_override",
|
|
55
|
+
default=None,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class TokenRefreshResult:
|
|
61
|
+
access_token: str
|
|
62
|
+
refresh_token: str
|
|
63
|
+
id_token: str
|
|
64
|
+
account_id: str | None
|
|
65
|
+
plan_type: str | None
|
|
66
|
+
email: str | None
|
|
67
|
+
workspace_id: str | None = None
|
|
68
|
+
workspace_label: str | None = None
|
|
69
|
+
seat_type: str | None = None
|
|
70
|
+
chatgpt_user_id: str | None = None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class RefreshError(Exception):
|
|
74
|
+
def __init__(
|
|
75
|
+
self,
|
|
76
|
+
code: str,
|
|
77
|
+
message: str,
|
|
78
|
+
is_permanent: bool,
|
|
79
|
+
*,
|
|
80
|
+
transport_error: bool = False,
|
|
81
|
+
transport_error_code: str | None = None,
|
|
82
|
+
retryable_same_contract: bool = False,
|
|
83
|
+
failed_session: aiohttp.ClientSession | None = None,
|
|
84
|
+
upstream_proxy_fail_closed_reason: str | None = None,
|
|
85
|
+
) -> None:
|
|
86
|
+
super().__init__(message)
|
|
87
|
+
self.code = code
|
|
88
|
+
self.message = message
|
|
89
|
+
self.is_permanent = is_permanent
|
|
90
|
+
self.transport_error = transport_error
|
|
91
|
+
self.transport_error_code = transport_error_code
|
|
92
|
+
self.retryable_same_contract = retryable_same_contract
|
|
93
|
+
self.failed_session = failed_session
|
|
94
|
+
self.upstream_proxy_fail_closed_reason = upstream_proxy_fail_closed_reason
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# Transient cross-replica refresh-contention codes fall into TWO semantically
|
|
98
|
+
# distinct categories that share the SAME external outcome (retryable, never
|
|
99
|
+
# cached, no account-health penalty, failover where applicable) but are NOT the
|
|
100
|
+
# same internal condition. Conflating them would mask a genuinely degraded state
|
|
101
|
+
# behind benign contention, so they are classified separately.
|
|
102
|
+
#
|
|
103
|
+
# (1) BENIGN CLAIM CONTENTION -- ``refresh_claim_timeout``: a peer replica holds
|
|
104
|
+
# the account's refresh claim (past the wait budget, or admission/budget was
|
|
105
|
+
# exhausted before the exchange could even start). THIS caller NEVER
|
|
106
|
+
# exchanged the token; the account's OAuth credentials are entirely healthy;
|
|
107
|
+
# only its refresh claim is contended. Pure contention -> retry, no penalty.
|
|
108
|
+
REFRESH_CLAIM_CONTENTION_CODES: frozenset[str] = frozenset({"refresh_claim_timeout"})
|
|
109
|
+
|
|
110
|
+
# (2) POST-EXCHANGE PERSIST/STATUS CAS CONFLICT -- ``token_persist_conflict`` /
|
|
111
|
+
# ``status_downgrade_conflict``: raised AFTER the upstream OAuth exchange has
|
|
112
|
+
# already run. ``token_persist_conflict`` in particular means the single-use
|
|
113
|
+
# refresh token was already CONSUMED upstream but the guarded writes could
|
|
114
|
+
# not persist the rotated token (a same-plaintext re-encryption storm the
|
|
115
|
+
# coordinator could not win an atomic compare-and-set window against), so the
|
|
116
|
+
# database may still hold the just-consumed token; ``status_downgrade_conflict``
|
|
117
|
+
# follows a PERMANENT refresh failure whose guarded REAUTH status write lost a
|
|
118
|
+
# compare-and-set. These signal a rare, more-serious internal race than benign
|
|
119
|
+
# contention and are logged/observed DISTINCTLY. They remain transient (a
|
|
120
|
+
# plain retry re-runs the WHOLE refresh -- a fresh upstream re-exchange, never
|
|
121
|
+
# a reuse of the possibly-consumed stored token; see ``refresh_account``), so
|
|
122
|
+
# their external outcome matches benign contention.
|
|
123
|
+
REFRESH_PERSIST_CONFLICT_CODES: frozenset[str] = frozenset(
|
|
124
|
+
{
|
|
125
|
+
"token_persist_conflict",
|
|
126
|
+
"status_downgrade_conflict",
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Union of both categories. Every code here carries ``transport_error=True`` (it
|
|
131
|
+
# is transient and retryable), but crucially the account's OAuth credentials are
|
|
132
|
+
# healthy. This union is deliberately DISJOINT from ``code == "transport_error"``,
|
|
133
|
+
# which ``refresh_access_token`` raises for a GENUINE OAuth transport failure (the
|
|
134
|
+
# OAuth request itself timing out / the upstream connection failing).
|
|
135
|
+
TRANSIENT_REFRESH_CONTENTION_CODES: frozenset[str] = REFRESH_CLAIM_CONTENTION_CODES | REFRESH_PERSIST_CONFLICT_CODES
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def is_refresh_claim_contention(exc: RefreshError) -> bool:
|
|
139
|
+
"""True ONLY for benign cross-replica refresh-CLAIM contention.
|
|
140
|
+
|
|
141
|
+
Narrow predicate: matches ``refresh_claim_timeout`` (a peer replica holds the
|
|
142
|
+
account's refresh claim and THIS caller never exchanged the token). Use this
|
|
143
|
+
where the code specifically means "a peer holds the claim, we did not
|
|
144
|
+
exchange". For the failover / skip-penalty EXTERNAL outcome (which treats
|
|
145
|
+
benign contention and post-exchange persist conflicts identically) gate on
|
|
146
|
+
``is_transient_refresh_contention`` instead.
|
|
147
|
+
"""
|
|
148
|
+
return exc.transport_error and exc.code in REFRESH_CLAIM_CONTENTION_CODES
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def is_refresh_persist_conflict(exc: RefreshError) -> bool:
|
|
152
|
+
"""True for a POST-EXCHANGE guarded-write compare-and-set conflict.
|
|
153
|
+
|
|
154
|
+
Matches ``token_persist_conflict`` / ``status_downgrade_conflict`` -- raised
|
|
155
|
+
after the OAuth exchange when the rotated-token or REAUTH-status guarded write
|
|
156
|
+
lost a compare-and-set. Distinct from benign claim contention: for
|
|
157
|
+
``token_persist_conflict`` the single-use token was already consumed upstream
|
|
158
|
+
but its rotation could not be persisted, so the database may still hold the
|
|
159
|
+
consumed token. This is a rarer, more-serious internal race worth surfacing
|
|
160
|
+
distinctly in logs/metrics, though its external (retryable, unpenalized)
|
|
161
|
+
outcome matches benign contention.
|
|
162
|
+
"""
|
|
163
|
+
return exc.transport_error and exc.code in REFRESH_PERSIST_CONFLICT_CODES
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def is_transient_refresh_contention(exc: RefreshError) -> bool:
|
|
167
|
+
"""True for EITHER benign claim contention OR a post-exchange persist conflict.
|
|
168
|
+
|
|
169
|
+
Proxy failover paths gate their "skip the account-health penalty" behavior on
|
|
170
|
+
THIS predicate rather than on the broad ``transport_error`` flag. A GENUINE
|
|
171
|
+
OAuth transport failure (``code == "transport_error"`` -- the OAuth request
|
|
172
|
+
itself timing out or the upstream connection failing) is transient too but IS
|
|
173
|
+
the account/route's fault, so it MUST retain its normal health accounting
|
|
174
|
+
(``record_error`` / ``_handle_stream_error``) and push the broken account into
|
|
175
|
+
transient backoff instead of being reselected immediately. Only the
|
|
176
|
+
claim/persist-CAS codes -- where the account's credentials are healthy -- skip
|
|
177
|
+
the penalty. The two categories are separated by ``is_refresh_claim_contention``
|
|
178
|
+
(benign) and ``is_refresh_persist_conflict`` (post-exchange) for observability;
|
|
179
|
+
both take the same unpenalized retryable failover path here.
|
|
180
|
+
"""
|
|
181
|
+
return exc.transport_error and exc.code in TRANSIENT_REFRESH_CONTENTION_CODES
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def refresh_contention_kind(exc: RefreshError) -> str | None:
|
|
185
|
+
"""Classify a transient refresh contention for DISTINCT observability.
|
|
186
|
+
|
|
187
|
+
Returns ``"claim_contention"`` for benign peer-holds-claim contention,
|
|
188
|
+
``"persist_conflict"`` for a post-exchange guarded-write CAS conflict, or
|
|
189
|
+
``None`` when ``exc`` is not transient refresh contention. Log/metric call
|
|
190
|
+
sites use this so a rare, more-serious post-exchange persist conflict is
|
|
191
|
+
surfaced distinctly rather than lumped with benign claim contention.
|
|
192
|
+
"""
|
|
193
|
+
if not exc.transport_error:
|
|
194
|
+
return None
|
|
195
|
+
if exc.code in REFRESH_CLAIM_CONTENTION_CODES:
|
|
196
|
+
return "claim_contention"
|
|
197
|
+
if exc.code in REFRESH_PERSIST_CONFLICT_CODES:
|
|
198
|
+
return "persist_conflict"
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def should_refresh(last_refresh: datetime, now: datetime | None = None) -> bool:
|
|
203
|
+
current = to_utc_naive(now) if now is not None else utcnow()
|
|
204
|
+
last = to_utc_naive(last_refresh)
|
|
205
|
+
return current - last > timedelta(days=TOKEN_REFRESH_INTERVAL_DAYS)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def classify_refresh_error(code: str | None) -> bool:
|
|
209
|
+
if not code:
|
|
210
|
+
return False
|
|
211
|
+
return code in PERMANENT_FAILURE_CODES
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
async def refresh_access_token(
|
|
215
|
+
refresh_token: str,
|
|
216
|
+
*,
|
|
217
|
+
session: aiohttp.ClientSession | None = None,
|
|
218
|
+
route: ResolvedUpstreamRoute | None = None,
|
|
219
|
+
codex_client: CodexClient | None = None,
|
|
220
|
+
allow_direct_egress: bool = False,
|
|
221
|
+
) -> TokenRefreshResult:
|
|
222
|
+
url = f"{AUTH_BASE_URL}/oauth/token"
|
|
223
|
+
payload = {
|
|
224
|
+
"grant_type": "refresh_token",
|
|
225
|
+
"client_id": OAUTH_CLIENT_ID,
|
|
226
|
+
"refresh_token": refresh_token,
|
|
227
|
+
"scope": OAUTH_SCOPE,
|
|
228
|
+
}
|
|
229
|
+
timeout = aiohttp.ClientTimeout(total=_effective_token_refresh_timeout())
|
|
230
|
+
|
|
231
|
+
headers: dict[str, str] = {}
|
|
232
|
+
request_id = get_request_id()
|
|
233
|
+
if request_id:
|
|
234
|
+
headers["x-request-id"] = request_id
|
|
235
|
+
require_route_or_direct_egress_opt_in(
|
|
236
|
+
route=route,
|
|
237
|
+
allow_direct_egress=allow_direct_egress,
|
|
238
|
+
operation="token refresh",
|
|
239
|
+
)
|
|
240
|
+
failed_session: aiohttp.ClientSession | None = None
|
|
241
|
+
try:
|
|
242
|
+
if route is not None:
|
|
243
|
+
owns_codex_client = codex_client is None
|
|
244
|
+
active_codex_client = codex_client or CodexClient(create_codex_session())
|
|
245
|
+
try:
|
|
246
|
+
resp = await active_codex_client.request(
|
|
247
|
+
"POST",
|
|
248
|
+
url,
|
|
249
|
+
route=route,
|
|
250
|
+
json=payload,
|
|
251
|
+
headers=headers,
|
|
252
|
+
timeout=_effective_token_refresh_timeout(),
|
|
253
|
+
)
|
|
254
|
+
data = await _safe_codex_json(resp)
|
|
255
|
+
status = int(getattr(resp, "status_code", getattr(resp, "status", 0)))
|
|
256
|
+
payload_data = _validate_token_payload(data)
|
|
257
|
+
if status >= 400:
|
|
258
|
+
logger.warning("Token refresh failed request_id=%s status=%s", get_request_id(), status)
|
|
259
|
+
raise _refresh_error_from_payload(payload_data, status)
|
|
260
|
+
finally:
|
|
261
|
+
if owns_codex_client:
|
|
262
|
+
await active_codex_client.close()
|
|
263
|
+
else:
|
|
264
|
+
async with lease_http_session(session) as client_session:
|
|
265
|
+
failed_session = client_session
|
|
266
|
+
async with client_session.post(url, json=payload, headers=headers, timeout=timeout) as resp:
|
|
267
|
+
data = await _safe_json(resp)
|
|
268
|
+
payload_data = _validate_token_payload(data)
|
|
269
|
+
if resp.status >= 400:
|
|
270
|
+
logger.warning(
|
|
271
|
+
"Token refresh failed request_id=%s status=%s",
|
|
272
|
+
get_request_id(),
|
|
273
|
+
resp.status,
|
|
274
|
+
)
|
|
275
|
+
raise _refresh_error_from_payload(payload_data, resp.status)
|
|
276
|
+
except RefreshError:
|
|
277
|
+
raise
|
|
278
|
+
except (aiohttp.ClientError, asyncio.TimeoutError, OSError, CodexTransportError) as exc:
|
|
279
|
+
message = str(exc) or exc.__class__.__name__
|
|
280
|
+
transport_error_code = (
|
|
281
|
+
exc.error_code
|
|
282
|
+
if isinstance(exc, CodexTransportError) and exc.error_code is not None
|
|
283
|
+
else process_network_error_code(
|
|
284
|
+
exc,
|
|
285
|
+
fallback="transport_error",
|
|
286
|
+
include_permanent_dns=route is None and not is_proxy_endpoint_failure(exc),
|
|
287
|
+
)
|
|
288
|
+
)
|
|
289
|
+
retryable_same_contract = (
|
|
290
|
+
exc.retryable_same_contract
|
|
291
|
+
if isinstance(exc, CodexTransportError)
|
|
292
|
+
else is_pre_dispatch_connection_failure(exc)
|
|
293
|
+
)
|
|
294
|
+
raise RefreshError(
|
|
295
|
+
"transport_error",
|
|
296
|
+
f"Transport error during token refresh: {message}",
|
|
297
|
+
False,
|
|
298
|
+
transport_error=True,
|
|
299
|
+
transport_error_code=(
|
|
300
|
+
transport_error_code if transport_error_code == PROCESS_NETWORK_UNAVAILABLE_CODE else None
|
|
301
|
+
),
|
|
302
|
+
# A rotating refresh token may already be consumed once response
|
|
303
|
+
# headers/body reads begin. Only connector-proven pre-dispatch
|
|
304
|
+
# failures can retry the same refresh contract.
|
|
305
|
+
retryable_same_contract=retryable_same_contract,
|
|
306
|
+
failed_session=failed_session if route is None else None,
|
|
307
|
+
) from exc
|
|
308
|
+
|
|
309
|
+
if not payload_data.access_token or not payload_data.refresh_token or not payload_data.id_token:
|
|
310
|
+
raise RefreshError("invalid_response", "Refresh response missing tokens", False)
|
|
311
|
+
|
|
312
|
+
claims = extract_id_token_claims(payload_data.id_token)
|
|
313
|
+
auth_claims = claims.auth or OpenAIAuthClaims()
|
|
314
|
+
account_id = auth_claims.chatgpt_account_id or claims.chatgpt_account_id
|
|
315
|
+
plan_type = auth_claims.chatgpt_plan_type or claims.chatgpt_plan_type
|
|
316
|
+
email = claims.email
|
|
317
|
+
workspace_id = clean_account_identity_part(auth_claims.workspace_id or claims.workspace_id)
|
|
318
|
+
workspace_label = clean_account_identity_part(auth_claims.workspace_label or claims.workspace_label)
|
|
319
|
+
seat_type = normalize_seat_type(auth_claims.seat_type or claims.seat_type)
|
|
320
|
+
chatgpt_user_id = resolve_seat_identity(claims, auth_claims)
|
|
321
|
+
|
|
322
|
+
return TokenRefreshResult(
|
|
323
|
+
access_token=payload_data.access_token,
|
|
324
|
+
refresh_token=payload_data.refresh_token,
|
|
325
|
+
id_token=payload_data.id_token,
|
|
326
|
+
account_id=account_id,
|
|
327
|
+
plan_type=plan_type,
|
|
328
|
+
email=email,
|
|
329
|
+
workspace_id=workspace_id,
|
|
330
|
+
workspace_label=workspace_label,
|
|
331
|
+
seat_type=seat_type,
|
|
332
|
+
chatgpt_user_id=chatgpt_user_id,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def push_token_refresh_timeout_override(timeout_seconds: float | None) -> contextvars.Token[float | None]:
|
|
337
|
+
return _TOKEN_REFRESH_TIMEOUT_OVERRIDE.set(timeout_seconds)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def pop_token_refresh_timeout_override(token: contextvars.Token[float | None]) -> None:
|
|
341
|
+
_TOKEN_REFRESH_TIMEOUT_OVERRIDE.reset(token)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def get_token_refresh_timeout_override() -> float | None:
|
|
345
|
+
"""Caller-scoped refresh budget (seconds); ``None`` when no caller set one.
|
|
346
|
+
|
|
347
|
+
Set by the proxy request path around ``AuthManager.ensure_fresh`` so that
|
|
348
|
+
everything the (shielded, caller-outliving) refresh task does — including
|
|
349
|
+
waiting on a foreign cross-replica refresh claim — stays bounded by the
|
|
350
|
+
budget of the request that started it.
|
|
351
|
+
"""
|
|
352
|
+
return _TOKEN_REFRESH_TIMEOUT_OVERRIDE.get()
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
async def _safe_codex_json(resp: object) -> JsonObject:
|
|
356
|
+
json_method = getattr(resp, "json", None)
|
|
357
|
+
try:
|
|
358
|
+
if callable(json_method):
|
|
359
|
+
data = json_method()
|
|
360
|
+
if asyncio.iscoroutine(data):
|
|
361
|
+
data = await data
|
|
362
|
+
else:
|
|
363
|
+
text_value = getattr(resp, "text", None)
|
|
364
|
+
if not isinstance(text_value, str):
|
|
365
|
+
content = getattr(resp, "content", b"")
|
|
366
|
+
text_value = content.decode("utf-8", errors="replace") if isinstance(content, bytes) else str(content)
|
|
367
|
+
import json
|
|
368
|
+
|
|
369
|
+
data = json.loads(text_value)
|
|
370
|
+
except Exception:
|
|
371
|
+
text_value = getattr(resp, "text", "")
|
|
372
|
+
return {"error": {"message": str(text_value).strip()}}
|
|
373
|
+
return data if isinstance(data, dict) else {"error": {"message": str(data)}}
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _validate_token_payload(data: JsonObject) -> OAuthTokenPayload:
|
|
377
|
+
try:
|
|
378
|
+
return OAuthTokenPayload.model_validate(data)
|
|
379
|
+
except ValidationError as exc:
|
|
380
|
+
logger.warning("Token refresh response invalid request_id=%s", get_request_id())
|
|
381
|
+
raise RefreshError("invalid_response", "Refresh response invalid", False) from exc
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _refresh_error_from_payload(payload: OAuthTokenPayload, status_code: int) -> RefreshError:
|
|
385
|
+
code = _extract_error_code(payload) or f"http_{status_code}"
|
|
386
|
+
message = _extract_error_message(payload) or f"Token refresh failed ({status_code})"
|
|
387
|
+
return RefreshError(code, message, classify_refresh_error(code))
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _effective_token_refresh_timeout() -> float:
|
|
391
|
+
override = _TOKEN_REFRESH_TIMEOUT_OVERRIDE.get()
|
|
392
|
+
if override is None:
|
|
393
|
+
return TOKEN_REFRESH_TIMEOUT_SECONDS
|
|
394
|
+
return max(0.001, min(TOKEN_REFRESH_TIMEOUT_SECONDS, override))
|
app/core/auth/step_up.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Step-up re-verification for sensitive dashboard mutations (PLAN §5 H5).
|
|
2
|
+
|
|
3
|
+
A signed-in account changing who may sign in (``security:write``,
|
|
4
|
+
``users:manage``, ``roles:manage``) or exporting account credentials
|
|
5
|
+
(``accounts:export``) must have re-proven a credential within the last five
|
|
6
|
+
minutes. What counts as re-proving depends on what the account holds: its
|
|
7
|
+
password (plus its TOTP code when it has a secret), or — for accounts that
|
|
8
|
+
sign in through a provider and hold no password — its own TOTP code. An
|
|
9
|
+
account holding neither a password nor a secret has one last option, and only
|
|
10
|
+
that one: re-authenticating at the identity provider it signs in with
|
|
11
|
+
(``prompt=login``). An account with none of the three cannot step up at all
|
|
12
|
+
and is told so (``step_up_unavailable``); there is no silent exemption, and a
|
|
13
|
+
provider's ``idp_mfa_enforced`` flag does not waive step-up.
|
|
14
|
+
|
|
15
|
+
The identity provider is an alternative to *nothing*, never to a factor the
|
|
16
|
+
account already holds: offering it to a password account would let a stolen
|
|
17
|
+
session cookie plus a still-live provider session change security settings
|
|
18
|
+
while presenting nothing the attacker does not already have.
|
|
19
|
+
|
|
20
|
+
This module holds the pure pieces (window, cookie name, availability,
|
|
21
|
+
freshness) plus the one lookup that turns "holds no credential" into "can
|
|
22
|
+
re-authenticate at its identity provider"; the cookie stores live next to the
|
|
23
|
+
session store in ``dashboard_auth.service`` and enforcement in
|
|
24
|
+
``auth.dependencies``.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from typing import Final, Literal
|
|
30
|
+
|
|
31
|
+
from app.core.auth.dashboard_mode import DashboardAuthMode
|
|
32
|
+
from app.db.models import AuthProviderKind, DashboardUser
|
|
33
|
+
|
|
34
|
+
#: How long a step-up verification is honoured, in seconds.
|
|
35
|
+
STEP_UP_MAX_AGE_SECONDS: Final = 300
|
|
36
|
+
#: Short-lived cookie carrying a step-up for principals that have no session
|
|
37
|
+
#: cookie to carry the claim (trusted-header accounts).
|
|
38
|
+
STEP_UP_COOKIE: Final = "codex_lb_step_up"
|
|
39
|
+
STEP_UP_UNAVAILABLE_MESSAGE: Final = "Set up two-factor authentication or a local password to change security settings"
|
|
40
|
+
|
|
41
|
+
StepUpMethod = Literal["password", "totp", "oidc"]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def step_up_methods(user: DashboardUser, *, oidc_identity: bool = False) -> list[StepUpMethod]:
|
|
45
|
+
"""The factors ``user`` must present to step up, in the order they are asked.
|
|
46
|
+
|
|
47
|
+
Every listed factor is required: a password account with a TOTP secret
|
|
48
|
+
presents both. ``oidc_identity`` says whether the account holds an identity
|
|
49
|
+
on an active OIDC provider, and is consulted **only** when the account
|
|
50
|
+
holds no credential of its own — the identity provider is the last resort,
|
|
51
|
+
not a shortcut past a factor the account already has. Empty means the
|
|
52
|
+
account cannot step up.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
methods: list[StepUpMethod] = []
|
|
56
|
+
if user.password_hash is not None:
|
|
57
|
+
methods.append("password")
|
|
58
|
+
if user.totp_secret_encrypted is not None:
|
|
59
|
+
methods.append("totp")
|
|
60
|
+
if methods:
|
|
61
|
+
return methods
|
|
62
|
+
return ["oidc"] if oidc_identity else []
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
async def account_step_up_methods(user: DashboardUser) -> list[StepUpMethod]:
|
|
66
|
+
""":func:`step_up_methods` for ``user``, asking the database only when it must.
|
|
67
|
+
|
|
68
|
+
An account holding a password or a TOTP secret is answered from the row
|
|
69
|
+
alone, exactly as before. Only an account holding neither — the shape that
|
|
70
|
+
is refused outright today — costs a lookup, and that lookup is what decides
|
|
71
|
+
between "re-authenticate at your identity provider" and "you cannot step up
|
|
72
|
+
at all". The gate, the ``403`` body and the session response all call this,
|
|
73
|
+
so they cannot disagree about what the account may present.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
methods = step_up_methods(user)
|
|
77
|
+
if methods:
|
|
78
|
+
return methods
|
|
79
|
+
return step_up_methods(user, oidc_identity=await _has_active_oidc_identity(user))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
async def _has_active_oidc_identity(user: DashboardUser) -> bool:
|
|
83
|
+
"""Whether ``user`` can actually reach an identity provider that would vouch for it.
|
|
84
|
+
|
|
85
|
+
Both halves matter: an identity row on a provider that is disabled (or on
|
|
86
|
+
an install whose auth mode does not admit it) is not a way to re-verify,
|
|
87
|
+
and an active provider the account has never signed in through cannot
|
|
88
|
+
vouch for it either.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
# Imported here rather than at module scope: the registry reaches the
|
|
92
|
+
# provider and user repositories, which sit above this module in the
|
|
93
|
+
# layering, and everything else here is pure.
|
|
94
|
+
from app.core.auth.providers import DEFAULT_PROVIDER_KEY
|
|
95
|
+
from app.core.auth.providers.registry import get_auth_provider_registry
|
|
96
|
+
from app.core.config.settings import get_settings
|
|
97
|
+
from app.db.session import SessionLocal
|
|
98
|
+
from app.modules.dashboard_users.repository import DashboardUsersRepository
|
|
99
|
+
|
|
100
|
+
mode: DashboardAuthMode = get_settings().dashboard_auth_mode
|
|
101
|
+
active = await get_auth_provider_registry().get_active(AuthProviderKind.OIDC, DEFAULT_PROVIDER_KEY, mode)
|
|
102
|
+
if active is None:
|
|
103
|
+
return False
|
|
104
|
+
async with SessionLocal() as session:
|
|
105
|
+
return await DashboardUsersRepository(session).has_identity(
|
|
106
|
+
user.id, provider=AuthProviderKind.OIDC.value, provider_key=active.row.provider_key
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def is_step_up_fresh(verified_at: int | None, *, now: int) -> bool:
|
|
111
|
+
"""Whether a step-up recorded at ``verified_at`` still covers a request at ``now``."""
|
|
112
|
+
|
|
113
|
+
return verified_at is not None and now - verified_at <= STEP_UP_MAX_AGE_SECONDS
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def step_up_expires_at(verified_at: int) -> int:
|
|
117
|
+
return verified_at + STEP_UP_MAX_AGE_SECONDS
|