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,111 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ipaddress import IPv4Network, IPv6Network
|
|
4
|
+
from typing import cast
|
|
5
|
+
|
|
6
|
+
from fastapi import FastAPI, Request
|
|
7
|
+
from fastapi.responses import JSONResponse
|
|
8
|
+
from starlette.datastructures import Headers
|
|
9
|
+
from starlette.requests import HTTPConnection
|
|
10
|
+
from starlette.types import ASGIApp, Receive, Scope, Send
|
|
11
|
+
|
|
12
|
+
from app.core.config.settings import get_settings
|
|
13
|
+
from app.core.errors import openai_error
|
|
14
|
+
from app.core.middleware.firewall_cache import FirewallIPCache, get_firewall_ip_cache
|
|
15
|
+
from app.core.request_locality import (
|
|
16
|
+
FORWARDED_CHAIN_HEADER_NAMES,
|
|
17
|
+
parse_trusted_proxy_networks,
|
|
18
|
+
resolve_connection_client_ip,
|
|
19
|
+
)
|
|
20
|
+
from app.core.socket_peer import raw_socket_peer_host
|
|
21
|
+
from app.db.session import get_background_session
|
|
22
|
+
from app.modules.firewall.repository import FirewallRepository
|
|
23
|
+
from app.modules.firewall.service import FirewallRepositoryPort, FirewallService
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ApiFirewallMiddleware:
|
|
27
|
+
"""IP allowlist for ``/v1/*`` and ``/backend-api/codex/*`` HTTP requests.
|
|
28
|
+
|
|
29
|
+
Pure ASGI: unprotected paths pass through with a single prefix check, and
|
|
30
|
+
allow decisions are answered from the in-memory TTL cache; the database is
|
|
31
|
+
only consulted on a cache miss.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
app: ASGIApp,
|
|
37
|
+
*,
|
|
38
|
+
trust_proxy_headers: bool,
|
|
39
|
+
trusted_proxy_networks: tuple[IPv4Network | IPv6Network, ...],
|
|
40
|
+
firewall_cache: FirewallIPCache,
|
|
41
|
+
) -> None:
|
|
42
|
+
self.app = app
|
|
43
|
+
self._trust_proxy_headers = trust_proxy_headers
|
|
44
|
+
self._trusted_proxy_networks = trusted_proxy_networks
|
|
45
|
+
self._firewall_cache = firewall_cache
|
|
46
|
+
|
|
47
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
48
|
+
if scope["type"] != "http" or not _is_protected_api_path(scope["path"]):
|
|
49
|
+
await self.app(scope, receive, send)
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
client_ip = resolve_connection_client_ip(
|
|
53
|
+
Headers(scope=scope),
|
|
54
|
+
raw_socket_peer_host(HTTPConnection(scope)),
|
|
55
|
+
trust_proxy_headers=self._trust_proxy_headers,
|
|
56
|
+
trusted_proxy_networks=self._trusted_proxy_networks,
|
|
57
|
+
allowed_proxy_header_names=FORWARDED_CHAIN_HEADER_NAMES,
|
|
58
|
+
)
|
|
59
|
+
firewall_cache = self._firewall_cache
|
|
60
|
+
cached_decision = await firewall_cache.is_allowed(client_ip) if client_ip is not None else None
|
|
61
|
+
if cached_decision is not None:
|
|
62
|
+
is_allowed = cached_decision
|
|
63
|
+
else:
|
|
64
|
+
version_before_read = firewall_cache.version
|
|
65
|
+
async with get_background_session() as session:
|
|
66
|
+
repository = cast(FirewallRepositoryPort, FirewallRepository(session))
|
|
67
|
+
service = FirewallService(repository)
|
|
68
|
+
is_allowed = await service.is_ip_allowed(client_ip)
|
|
69
|
+
if client_ip is not None:
|
|
70
|
+
await firewall_cache.set(client_ip, is_allowed, if_version=version_before_read)
|
|
71
|
+
|
|
72
|
+
if is_allowed:
|
|
73
|
+
await self.app(scope, receive, send)
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
response = JSONResponse(
|
|
77
|
+
status_code=403,
|
|
78
|
+
content=openai_error("ip_forbidden", "Access denied for client IP", error_type="access_error"),
|
|
79
|
+
)
|
|
80
|
+
await response(scope, receive, send)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def add_api_firewall_middleware(app: FastAPI) -> None:
|
|
84
|
+
settings = get_settings()
|
|
85
|
+
app.add_middleware(
|
|
86
|
+
ApiFirewallMiddleware,
|
|
87
|
+
trust_proxy_headers=settings.firewall_trust_proxy_headers,
|
|
88
|
+
trusted_proxy_networks=parse_trusted_proxy_networks(settings.firewall_trusted_proxy_cidrs),
|
|
89
|
+
firewall_cache=get_firewall_ip_cache(),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _is_protected_api_path(path: str) -> bool:
|
|
94
|
+
if path == "/backend-api/codex" or path.startswith("/backend-api/codex/"):
|
|
95
|
+
return True
|
|
96
|
+
return path == "/v1" or path.startswith("/v1/")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _resolve_client_ip(
|
|
100
|
+
request: Request,
|
|
101
|
+
*,
|
|
102
|
+
trust_proxy_headers: bool,
|
|
103
|
+
trusted_proxy_networks: tuple[IPv4Network | IPv6Network, ...] = (),
|
|
104
|
+
) -> str | None:
|
|
105
|
+
return resolve_connection_client_ip(
|
|
106
|
+
request.headers,
|
|
107
|
+
raw_socket_peer_host(request),
|
|
108
|
+
trust_proxy_headers=trust_proxy_headers,
|
|
109
|
+
trusted_proxy_networks=trusted_proxy_networks,
|
|
110
|
+
allowed_proxy_header_names=FORWARDED_CHAIN_HEADER_NAMES,
|
|
111
|
+
)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from fastapi import FastAPI
|
|
4
|
+
from starlette.datastructures import MutableHeaders
|
|
5
|
+
from starlette.types import ASGIApp, Message, Receive, Scope, Send
|
|
6
|
+
|
|
7
|
+
from app import __version__
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AppVersionMiddleware:
|
|
11
|
+
"""Append ``X-App-Version`` to every 200-499 HTTP response.
|
|
12
|
+
|
|
13
|
+
Pure ASGI (no ``BaseHTTPMiddleware``) so streaming responses relay chunks
|
|
14
|
+
without an extra task and memory-stream hop. 5xx responses and WebSocket
|
|
15
|
+
scopes never carry the header, and a route-owned ``X-App-Version`` value is
|
|
16
|
+
preserved (see ``openspec/specs/api-response-metadata/spec.md``).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, app: ASGIApp) -> None:
|
|
20
|
+
self.app = app
|
|
21
|
+
|
|
22
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
23
|
+
if scope["type"] != "http":
|
|
24
|
+
await self.app(scope, receive, send)
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
async def send_with_app_version(message: Message) -> None:
|
|
28
|
+
if message["type"] == "http.response.start" and 200 <= message["status"] < 500:
|
|
29
|
+
headers = MutableHeaders(raw=message.setdefault("headers", []))
|
|
30
|
+
headers.setdefault("X-App-Version", __version__)
|
|
31
|
+
await send(message)
|
|
32
|
+
|
|
33
|
+
await self.app(scope, receive, send_with_app_version)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def add_app_version_middleware(app: FastAPI) -> None:
|
|
37
|
+
app.add_middleware(AppVersionMiddleware)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, cast
|
|
4
|
+
|
|
5
|
+
from fastapi import FastAPI
|
|
6
|
+
from starlette.requests import HTTPConnection
|
|
7
|
+
from starlette.types import ASGIApp, Receive, Scope, Send
|
|
8
|
+
|
|
9
|
+
from app.core.auth.dashboard_mode import DashboardAuthMode
|
|
10
|
+
from app.core.config.settings import get_settings
|
|
11
|
+
from app.core.request_locality import is_trusted_proxy_source, parse_trusted_proxy_networks
|
|
12
|
+
from app.core.socket_peer import raw_socket_peer_host
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DashboardAuthProxyHeaderSanitizerMiddleware:
|
|
16
|
+
def __init__(self, app: ASGIApp) -> None:
|
|
17
|
+
self.app = app
|
|
18
|
+
settings = get_settings()
|
|
19
|
+
self._enabled = settings.dashboard_auth_mode == DashboardAuthMode.TRUSTED_HEADER
|
|
20
|
+
self._trust_proxy_headers = settings.firewall_trust_proxy_headers
|
|
21
|
+
self._trusted_proxy_networks = parse_trusted_proxy_networks(settings.firewall_trusted_proxy_cidrs)
|
|
22
|
+
self._trusted_header_name = settings.dashboard_auth_proxy_header.lower().encode("latin-1")
|
|
23
|
+
|
|
24
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
25
|
+
if not self._enabled or scope["type"] != "http":
|
|
26
|
+
await self.app(scope, receive, send)
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
client_host = raw_socket_peer_host(HTTPConnection(scope))
|
|
30
|
+
if (
|
|
31
|
+
self._trust_proxy_headers
|
|
32
|
+
and client_host
|
|
33
|
+
and is_trusted_proxy_source(client_host, self._trusted_proxy_networks)
|
|
34
|
+
):
|
|
35
|
+
await self.app(scope, receive, send)
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
headers = cast(list[tuple[bytes, bytes]], scope.get("headers", []))
|
|
39
|
+
if not any(name.lower() == self._trusted_header_name for name, _ in headers):
|
|
40
|
+
await self.app(scope, receive, send)
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
scrubbed_scope = {**scope, "headers": _filter_headers(headers, self._trusted_header_name)}
|
|
44
|
+
await self.app(scrubbed_scope, receive, send)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def add_dashboard_auth_proxy_middleware(app: FastAPI) -> None:
|
|
48
|
+
app.add_middleware(cast(Any, DashboardAuthProxyHeaderSanitizerMiddleware))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _filter_headers(headers: list[tuple[bytes, bytes]], target: bytes) -> list[tuple[bytes, bytes]]:
|
|
52
|
+
return [(name, value) for name, value in headers if name.lower() != target]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
__all__ = ["add_dashboard_auth_proxy_middleware", "DashboardAuthProxyHeaderSanitizerMiddleware"]
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Reject cross-site browser requests that would mutate dashboard state.
|
|
2
|
+
|
|
3
|
+
The dashboard authenticates with a ``SameSite=Lax`` session cookie or, on
|
|
4
|
+
loopback without a password, with no credential at all. Neither protects a
|
|
5
|
+
mutation that a foreign page triggers from the operator's browser, so this
|
|
6
|
+
middleware inspects the browser-controlled ``Sec-Fetch-Site`` and ``Origin``
|
|
7
|
+
headers before routing and answers ``403 cross_site_request_rejected`` when
|
|
8
|
+
the request did not come from the dashboard's own origin.
|
|
9
|
+
|
|
10
|
+
Only unsafe methods under ``/api/`` are inspected. Bearer-authenticated
|
|
11
|
+
data-plane routes (``/api/fleet/``, ``/api/codex/``) and any request carrying
|
|
12
|
+
an ``Authorization: Bearer`` header are exempt because a foreign page cannot
|
|
13
|
+
attach that header without CORS consent. Requests without either header come
|
|
14
|
+
from non-browser clients and pass through unchanged.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
from typing import Any, cast
|
|
21
|
+
from urllib.parse import urlsplit
|
|
22
|
+
|
|
23
|
+
from fastapi import FastAPI
|
|
24
|
+
from starlette._utils import get_route_path
|
|
25
|
+
from starlette.responses import JSONResponse
|
|
26
|
+
from starlette.types import ASGIApp, Receive, Scope, Send
|
|
27
|
+
|
|
28
|
+
from app.core.errors import dashboard_error
|
|
29
|
+
|
|
30
|
+
CROSS_SITE_REQUEST_REJECTED_CODE = "cross_site_request_rejected"
|
|
31
|
+
CROSS_SITE_REQUEST_REJECTED_MESSAGE = "Cross-site dashboard requests are not allowed"
|
|
32
|
+
|
|
33
|
+
_SAFE_METHODS = frozenset({"GET", "HEAD", "OPTIONS"})
|
|
34
|
+
_PROTECTED_PATH_PREFIX = "/api/"
|
|
35
|
+
_BEARER_AUTHENTICATED_PATH_PREFIXES = ("/api/fleet/", "/api/codex/")
|
|
36
|
+
_ALLOWED_FETCH_SITES = frozenset({"same-origin", "none"})
|
|
37
|
+
_DEFAULT_PORTS = {"http": 80, "https": 443}
|
|
38
|
+
|
|
39
|
+
logger = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _header(headers: list[tuple[bytes, bytes]], name: bytes) -> str | None:
|
|
43
|
+
for key, value in headers:
|
|
44
|
+
if key.lower() == name:
|
|
45
|
+
return value.decode("latin-1")
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _has_bearer_authorization(headers: list[tuple[bytes, bytes]]) -> bool:
|
|
50
|
+
authorization = _header(headers, b"authorization")
|
|
51
|
+
if authorization is None:
|
|
52
|
+
return False
|
|
53
|
+
scheme, _, credentials = authorization.strip().partition(" ")
|
|
54
|
+
return scheme.lower() == "bearer" and credentials.strip() != ""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _origin_tuple(scheme: str, netloc: str) -> tuple[str, str, int] | None:
|
|
58
|
+
scheme = scheme.lower()
|
|
59
|
+
default_port = _DEFAULT_PORTS.get(scheme)
|
|
60
|
+
if default_port is None:
|
|
61
|
+
return None
|
|
62
|
+
try:
|
|
63
|
+
parsed = urlsplit(f"//{netloc}")
|
|
64
|
+
hostname = parsed.hostname
|
|
65
|
+
port = parsed.port
|
|
66
|
+
except ValueError:
|
|
67
|
+
return None
|
|
68
|
+
if not hostname:
|
|
69
|
+
return None
|
|
70
|
+
return scheme, hostname.lower(), port if port is not None else default_port
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _parse_origin_header(value: str) -> tuple[str, str, int] | None:
|
|
74
|
+
try:
|
|
75
|
+
parsed = urlsplit(value.strip())
|
|
76
|
+
except ValueError:
|
|
77
|
+
return None
|
|
78
|
+
if not parsed.scheme or not parsed.netloc:
|
|
79
|
+
return None
|
|
80
|
+
return _origin_tuple(parsed.scheme, parsed.netloc)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _request_origin(scope: Scope, headers: list[tuple[bytes, bytes]]) -> tuple[str, str, int] | None:
|
|
84
|
+
host = _header(headers, b"host")
|
|
85
|
+
if host is None:
|
|
86
|
+
return None
|
|
87
|
+
return _origin_tuple(str(scope.get("scheme", "http")), host.strip())
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _is_protected(scope: Scope, headers: list[tuple[bytes, bytes]]) -> bool:
|
|
91
|
+
if scope["type"] != "http":
|
|
92
|
+
return False
|
|
93
|
+
if str(scope.get("method", "GET")).upper() in _SAFE_METHODS:
|
|
94
|
+
return False
|
|
95
|
+
path = get_route_path(scope)
|
|
96
|
+
if not path.startswith(_PROTECTED_PATH_PREFIX) or path.startswith(_BEARER_AUTHENTICATED_PATH_PREFIXES):
|
|
97
|
+
return False
|
|
98
|
+
return not _has_bearer_authorization(headers)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _format_origin(origin: tuple[str, str, int] | None) -> str:
|
|
102
|
+
if origin is None:
|
|
103
|
+
return "-"
|
|
104
|
+
scheme, host, port = origin
|
|
105
|
+
host_text = f"[{host}]" if ":" in host else host
|
|
106
|
+
return f"{scheme}://{host_text}:{port}"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _rejection_reason(scope: Scope, headers: list[tuple[bytes, bytes]]) -> str | None:
|
|
110
|
+
"""Return a log-safe reason when the request must be rejected, else ``None``.
|
|
111
|
+
|
|
112
|
+
The reason names only the ``Sec-Fetch-Site`` value or the presented ``Origin``
|
|
113
|
+
(scheme, host, port; path and query stripped) next to the expected origin
|
|
114
|
+
derived from the request scheme and ``Host`` header. No other header values
|
|
115
|
+
are included.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
fetch_site = _header(headers, b"sec-fetch-site")
|
|
119
|
+
if fetch_site is not None:
|
|
120
|
+
site = fetch_site.strip().lower()
|
|
121
|
+
return None if site in _ALLOWED_FETCH_SITES else f"sec_fetch_site={site or '-'}"
|
|
122
|
+
|
|
123
|
+
origin = _header(headers, b"origin")
|
|
124
|
+
if origin is None:
|
|
125
|
+
return None
|
|
126
|
+
expected = _request_origin(scope, headers)
|
|
127
|
+
if origin.strip().lower() == "null":
|
|
128
|
+
return f"origin=null expected={_format_origin(expected)}"
|
|
129
|
+
presented = _parse_origin_header(origin)
|
|
130
|
+
if presented is None or expected is None or presented != expected:
|
|
131
|
+
return f"origin={_format_origin(presented)} expected={_format_origin(expected)}"
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class DashboardCsrfMiddleware:
|
|
136
|
+
def __init__(self, app: ASGIApp) -> None:
|
|
137
|
+
self.app = app
|
|
138
|
+
|
|
139
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
140
|
+
if scope["type"] != "http":
|
|
141
|
+
await self.app(scope, receive, send)
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
headers = cast(list[tuple[bytes, bytes]], scope.get("headers", []))
|
|
145
|
+
if not _is_protected(scope, headers):
|
|
146
|
+
await self.app(scope, receive, send)
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
reason = _rejection_reason(scope, headers)
|
|
150
|
+
if reason is None:
|
|
151
|
+
await self.app(scope, receive, send)
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
logger.warning(
|
|
155
|
+
"dashboard_error_response method=%s path=%s status=403 code=%s %s",
|
|
156
|
+
scope.get("method"),
|
|
157
|
+
get_route_path(scope),
|
|
158
|
+
CROSS_SITE_REQUEST_REJECTED_CODE,
|
|
159
|
+
reason,
|
|
160
|
+
)
|
|
161
|
+
response = JSONResponse(
|
|
162
|
+
status_code=403,
|
|
163
|
+
content=dashboard_error(CROSS_SITE_REQUEST_REJECTED_CODE, CROSS_SITE_REQUEST_REJECTED_MESSAGE),
|
|
164
|
+
)
|
|
165
|
+
await response(scope, receive, send)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def add_dashboard_csrf_middleware(app: FastAPI) -> None:
|
|
169
|
+
app.add_middleware(cast(Any, DashboardCsrfMiddleware))
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
__all__ = [
|
|
173
|
+
"CROSS_SITE_REQUEST_REJECTED_CODE",
|
|
174
|
+
"CROSS_SITE_REQUEST_REJECTED_MESSAGE",
|
|
175
|
+
"DashboardCsrfMiddleware",
|
|
176
|
+
"add_dashboard_csrf_middleware",
|
|
177
|
+
]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from starlette.middleware.gzip import GZipMiddleware
|
|
4
|
+
from starlette.types import ASGIApp, Receive, Scope, Send
|
|
5
|
+
|
|
6
|
+
# Only dashboard-facing paths are compressed. Proxy paths (/backend-api,
|
|
7
|
+
# /v1, websockets) stream SSE and must never pass through a compressing
|
|
8
|
+
# wrapper; dashboard API responses are JSON and the built SPA assets are
|
|
9
|
+
# text-heavy (the uncompressed JS bundle alone is ~1.7 MB).
|
|
10
|
+
_COMPRESSED_PATH_PREFIXES = ("/api/", "/assets/")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DashboardGZipMiddleware:
|
|
14
|
+
"""Apply gzip to dashboard API and static-asset responses only."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, app: ASGIApp, minimum_size: int = 1024) -> None:
|
|
17
|
+
self._plain = app
|
|
18
|
+
self._gzip = GZipMiddleware(app, minimum_size=minimum_size)
|
|
19
|
+
|
|
20
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
21
|
+
if (
|
|
22
|
+
scope["type"] == "http"
|
|
23
|
+
and scope.get("path", "").startswith(_COMPRESSED_PATH_PREFIXES)
|
|
24
|
+
and not _has_range_header(scope)
|
|
25
|
+
):
|
|
26
|
+
await self._gzip(scope, receive, send)
|
|
27
|
+
return
|
|
28
|
+
await self._plain(scope, receive, send)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _has_range_header(scope: Scope) -> bool:
|
|
32
|
+
"""Ranged requests bypass gzip: FileResponse builds the 206 and its
|
|
33
|
+
Content-Range against the uncompressed file, so compressing the body
|
|
34
|
+
afterwards would describe offsets the encoded bytes no longer match.
|
|
35
|
+
Header-name casing is normalized because not every ASGI server
|
|
36
|
+
lowercases request header names."""
|
|
37
|
+
return any(name.lower() == b"range" for name, _ in scope.get("headers", ()))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def add_dashboard_gzip_middleware(app) -> None:
|
|
41
|
+
app.add_middleware(DashboardGZipMiddleware)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
from starlette.types import ASGIApp, Receive, Scope, Send
|
|
7
|
+
|
|
8
|
+
from app.core.config.dashboard_overrides import bind_dashboard_overrides, reset_dashboard_overrides
|
|
9
|
+
from app.core.config.settings_cache import get_settings_cache
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
_SNAPSHOT_FAILURE_LOG_INTERVAL_SECONDS = 60.0
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DashboardOverridesMiddleware:
|
|
17
|
+
"""Bind the dashboard-managed ``Settings`` overrides for the lifetime of one request or socket.
|
|
18
|
+
|
|
19
|
+
Reads the ``SettingsCache`` snapshot (TTL 5 s, cross-replica invalidated) once
|
|
20
|
+
per HTTP request or WebSocket connection and exposes the non-NULL dashboard
|
|
21
|
+
columns through ``with_dashboard_overrides`` to every consumer downstream,
|
|
22
|
+
including tasks the request spawns (they inherit the context). When the
|
|
23
|
+
snapshot cannot be refreshed, the last row the cache loaded is used; only
|
|
24
|
+
before the first successful load does the environment fallback apply.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, app: ASGIApp) -> None:
|
|
28
|
+
self.app = app
|
|
29
|
+
self._last_failure_logged_at = 0.0
|
|
30
|
+
|
|
31
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
32
|
+
if scope["type"] not in {"http", "websocket"}:
|
|
33
|
+
await self.app(scope, receive, send)
|
|
34
|
+
return
|
|
35
|
+
cache = get_settings_cache()
|
|
36
|
+
try:
|
|
37
|
+
row = await cache.get()
|
|
38
|
+
except Exception: # noqa: BLE001 - never fail a request over an unreadable snapshot
|
|
39
|
+
row = cache.cached_row()
|
|
40
|
+
self._log_snapshot_failure(stale_row_available=row is not None)
|
|
41
|
+
if row is None:
|
|
42
|
+
await self.app(scope, receive, send)
|
|
43
|
+
return
|
|
44
|
+
token = bind_dashboard_overrides(row)
|
|
45
|
+
try:
|
|
46
|
+
await self.app(scope, receive, send)
|
|
47
|
+
finally:
|
|
48
|
+
reset_dashboard_overrides(token)
|
|
49
|
+
|
|
50
|
+
def _log_snapshot_failure(self, *, stale_row_available: bool) -> None:
|
|
51
|
+
now = time.monotonic()
|
|
52
|
+
if now - self._last_failure_logged_at < _SNAPSHOT_FAILURE_LOG_INTERVAL_SECONDS:
|
|
53
|
+
return
|
|
54
|
+
self._last_failure_logged_at = now
|
|
55
|
+
logger.warning(
|
|
56
|
+
"dashboard settings snapshot unavailable; %s",
|
|
57
|
+
"using the last loaded dashboard values" if stale_row_available else "using environment values",
|
|
58
|
+
exc_info=True,
|
|
59
|
+
)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
import anyio
|
|
7
|
+
|
|
8
|
+
# Default TTL when the firewall cache is constructed before settings are
|
|
9
|
+
# resolved. The application normally tunes this via
|
|
10
|
+
# ``firewall_ip_cache_ttl_seconds`` (env ``CODEX_LB_FIREWALL_IP_CACHE_TTL_SECONDS``),
|
|
11
|
+
# read lazily on first access in :func:`get_firewall_ip_cache`. The default is
|
|
12
|
+
# intentionally well above the worst-case request-handling latency so the cache
|
|
13
|
+
# provides real relief on hot paths under load.
|
|
14
|
+
DEFAULT_FIREWALL_IP_CACHE_TTL_SECONDS = 30
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(slots=True)
|
|
18
|
+
class _CachedFirewallDecision:
|
|
19
|
+
allowed: bool
|
|
20
|
+
expires_at: float
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FirewallIPCache:
|
|
24
|
+
def __init__(self, ttl_seconds: int = DEFAULT_FIREWALL_IP_CACHE_TTL_SECONDS) -> None:
|
|
25
|
+
if ttl_seconds <= 0:
|
|
26
|
+
raise ValueError("ttl_seconds must be positive")
|
|
27
|
+
self._ttl_seconds = ttl_seconds
|
|
28
|
+
self._cache: dict[str, _CachedFirewallDecision] = {}
|
|
29
|
+
self._lock = anyio.Lock()
|
|
30
|
+
self._version = 0
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def ttl_seconds(self) -> int:
|
|
34
|
+
return self._ttl_seconds
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def version(self) -> int:
|
|
38
|
+
return self._version
|
|
39
|
+
|
|
40
|
+
async def is_allowed(self, ip: str) -> bool | None:
|
|
41
|
+
entry = self._cache.get(ip)
|
|
42
|
+
if entry is None:
|
|
43
|
+
return None
|
|
44
|
+
if time.monotonic() >= entry.expires_at:
|
|
45
|
+
return None
|
|
46
|
+
return entry.allowed
|
|
47
|
+
|
|
48
|
+
async def set(self, ip: str, allowed: bool, *, if_version: int | None = None) -> None:
|
|
49
|
+
async with self._lock:
|
|
50
|
+
if if_version is not None and if_version != self._version:
|
|
51
|
+
return
|
|
52
|
+
self._cache[ip] = _CachedFirewallDecision(
|
|
53
|
+
allowed=allowed,
|
|
54
|
+
expires_at=time.monotonic() + self._ttl_seconds,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def invalidate_all(self) -> None:
|
|
58
|
+
self._cache.clear()
|
|
59
|
+
self._version += 1
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
_firewall_ip_cache: FirewallIPCache | None = None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _resolve_configured_ttl() -> int:
|
|
66
|
+
"""Read the configured TTL from settings, falling back to the default.
|
|
67
|
+
|
|
68
|
+
Settings access is wrapped because the cache may be constructed before the
|
|
69
|
+
settings module is fully initialised (e.g. during early module-import in
|
|
70
|
+
tests).
|
|
71
|
+
"""
|
|
72
|
+
try:
|
|
73
|
+
from app.core.config.settings import get_settings
|
|
74
|
+
|
|
75
|
+
return int(get_settings().firewall_ip_cache_ttl_seconds)
|
|
76
|
+
except Exception: # pragma: no cover — defensive only
|
|
77
|
+
return DEFAULT_FIREWALL_IP_CACHE_TTL_SECONDS
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def get_firewall_ip_cache() -> FirewallIPCache:
|
|
81
|
+
global _firewall_ip_cache
|
|
82
|
+
if _firewall_ip_cache is None:
|
|
83
|
+
_firewall_ip_cache = FirewallIPCache(ttl_seconds=_resolve_configured_ttl())
|
|
84
|
+
return _firewall_ip_cache
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def reset_firewall_ip_cache_for_testing() -> None:
|
|
88
|
+
"""Drop the cached singleton so the next ``get_firewall_ip_cache()`` call
|
|
89
|
+
re-reads settings. Test-only helper."""
|
|
90
|
+
global _firewall_ip_cache
|
|
91
|
+
_firewall_ip_cache = None
|