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,601 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import copy
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import re
|
|
8
|
+
import time
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from datetime import UTC, datetime
|
|
11
|
+
from typing import Any, cast
|
|
12
|
+
from urllib.parse import unquote_plus
|
|
13
|
+
|
|
14
|
+
from fastapi import Request
|
|
15
|
+
from uvicorn.config import LOGGING_CONFIG
|
|
16
|
+
from uvicorn.logging import AccessFormatter, DefaultFormatter
|
|
17
|
+
|
|
18
|
+
from app.core.types import JsonValue
|
|
19
|
+
from app.core.utils.request_id import get_request_id
|
|
20
|
+
|
|
21
|
+
_SENSITIVE_LOG_VALUE_PATTERNS = (
|
|
22
|
+
re.compile(r"(?i)(password|passwd|pwd|token|secret|api[_-]?key)(\s*[=:]\s*)([^\s,&]+)"),
|
|
23
|
+
re.compile(r"(?i)(bearer\s+)[A-Za-z0-9._~+/=:-]+"),
|
|
24
|
+
re.compile(r"(?i)(authorization\s*[=:]\s*)(?!\s*bearer\b)([^,&]+)"),
|
|
25
|
+
)
|
|
26
|
+
_LINE_BREAKS = re.compile(r"(\r\n|\n|\r)")
|
|
27
|
+
# RFC 7617 ``Basic <base64>`` token: a reversible encoding of ``user:password``
|
|
28
|
+
# that aiohttp reprs verbatim (``ClientHttpProxyError.request_info.headers``
|
|
29
|
+
# carries the CONNECT ``Proxy-Authorization`` header). Applied to every record
|
|
30
|
+
# behind cheap substring scans for the canonical, lowercase and uppercase
|
|
31
|
+
# scheme spellings (the auth-scheme is case-insensitive per RFC 7235); the
|
|
32
|
+
# regex itself is case-insensitive once a precheck hits.
|
|
33
|
+
_BASIC_TOKEN_PATTERN = re.compile(r"(?i)(basic\s+)[A-Za-z0-9+/=]+")
|
|
34
|
+
_BASIC_TOKEN_PRECHECKS = ("Basic ", "basic ", "BASIC ")
|
|
35
|
+
# Fail-closed rendering when a redaction pass itself raises: the record is
|
|
36
|
+
# still emitted (timestamp/level/logger intact) but never with the original,
|
|
37
|
+
# possibly credential-bearing text.
|
|
38
|
+
_REDACTION_FAILED_PLACEHOLDER = "[REDACTED: log redaction failed]"
|
|
39
|
+
_JSON_SENSITIVE_LOG_VALUE_PATTERN = re.compile(
|
|
40
|
+
r'(?i)("(?:password|passwd|pwd|token|secret|api[_-]?key|authorization)"\s*:\s*")'
|
|
41
|
+
r'(?:\\.|[^"\\])*(?:\\(?=\Z))?("|(?=\Z))'
|
|
42
|
+
)
|
|
43
|
+
# ``scheme://user:pass@`` userinfo, e.g. aiohttp ConnectionKey proxy URL reprs.
|
|
44
|
+
# RFC 3986 userinfo never contains ``/``, ``?`` or ``#``, so a bare host
|
|
45
|
+
# followed by a query/fragment holding an ``@`` is left alone. ``'`` is an
|
|
46
|
+
# RFC 3986 sub-delim that yarl leaves unencoded in userinfo (``URL('http://u:p'w@h')``),
|
|
47
|
+
# so it must stay matchable; ``"`` is not valid unencoded userinfo (yarl emits
|
|
48
|
+
# ``%22``) and stays excluded so a compact JSON document holding a URL and an
|
|
49
|
+
# e-mail address is not over-matched.
|
|
50
|
+
_USERINFO_PATTERN = re.compile(r"(?i)\b([a-z][a-z0-9+.-]*://)([^/?#\s@\"]+)@")
|
|
51
|
+
# Python ``repr()`` of a secret-keyed mapping, ``{'password': 'x', 'token': 1}``:
|
|
52
|
+
# the quote between key and colon defeats the ``key=value`` pattern above and
|
|
53
|
+
# the JSON pattern below is double-quote only. Reached by ``%r``-logged
|
|
54
|
+
# mappings and by the JSON formatter whenever a structured extra has to be
|
|
55
|
+
# rendered as text (depth limit, exploding iteration, unserializable rebuild).
|
|
56
|
+
# The key matches on suffix like ``_SENSITIVE_LOG_KEY_PATTERN``; the value is
|
|
57
|
+
# a quoted string (quotes kept), bytes, a flat container, or a bare token up to
|
|
58
|
+
# the next separator. Containers nested under a secret key are masked only to
|
|
59
|
+
# their first level: a best-effort text backstop, not the structural pass.
|
|
60
|
+
_PYTHON_REPR_SENSITIVE_LOG_VALUE_PATTERN = re.compile(
|
|
61
|
+
r"(?i)('[^'\\]*(?:password|passwd|pwd|token|secret|api[_-]?key|authorization)'\s*:\s*)"
|
|
62
|
+
r"(b?'(?:\\.|[^'\\])*'|b?\"(?:\\.|[^\"\\])*\"|\[[^\[\]]*\]|\([^()]*\)|\{[^{}]*\}|[^,}\]\)\s]+)"
|
|
63
|
+
)
|
|
64
|
+
# Structured (JSON extra) keys whose string values are secrets by name.
|
|
65
|
+
_SENSITIVE_LOG_KEY_PATTERN = re.compile(r"(?i)(password|passwd|pwd|token|secret|api[_-]?key|authorization)$")
|
|
66
|
+
# Case-folded substrings that must be present before the keyed/bearer/
|
|
67
|
+
# authorization/JSON patterns above can match; keeps the per-record cost of
|
|
68
|
+
# credential-free lines to a casefold plus substring scans.
|
|
69
|
+
# Invite tokens travel in the URL path (``GET /api/dashboard-auth/invite/<token>``)
|
|
70
|
+
# and would otherwise land in access logs and 404/429 error lines. ``/invite/accept``
|
|
71
|
+
# is a route name, not a token, and stays readable.
|
|
72
|
+
_INVITE_PATH_TOKEN_PATTERN = re.compile(r"(/api/dashboard-auth/invite/)(?!accept(?:[/?#\s]|$))[A-Za-z0-9_-]{20,}")
|
|
73
|
+
_INVITE_PATH_PRECHECK = "/api/dashboard-auth/invite/"
|
|
74
|
+
# The OIDC callback carries its credentials in the query string
|
|
75
|
+
# (``?code=...&state=...``), which is the one part of a request the access
|
|
76
|
+
# logger prints verbatim: ``AccessFormatter`` renders the whole request line, so
|
|
77
|
+
# a handler that carefully logs nothing still leaves the authorization code and
|
|
78
|
+
# the flow's ``state`` in the access log of every replica. A refused callback is
|
|
79
|
+
# the worse case, because its code is still unconsumed at the identity provider.
|
|
80
|
+
# Anchored to the one path, so no other route's ``code``/``state`` parameter is
|
|
81
|
+
# touched, and applied at every level because access logs are INFO.
|
|
82
|
+
#
|
|
83
|
+
# The key is matched *decoded*, because that is the only spelling that decides
|
|
84
|
+
# anything: Starlette builds ``request.query_params`` with ``parse_qsl``, which
|
|
85
|
+
# percent-decodes names, so ``?c%6Fde=`` binds to the handler's ``code`` exactly
|
|
86
|
+
# as ``?code=`` does -- while Uvicorn renders the request line from the raw
|
|
87
|
+
# ``query_string`` bytes, encoding intact. A literal-key pattern therefore masks
|
|
88
|
+
# one spelling of a parameter the route accepts in several. Values are never
|
|
89
|
+
# decoded: they are replaced whole, so nothing decoded is ever written back out.
|
|
90
|
+
_OIDC_CALLBACK_PRECHECK = "/api/dashboard-auth/oidc/callback"
|
|
91
|
+
_OIDC_CALLBACK_QUERY_PATTERN = re.compile(re.escape(_OIDC_CALLBACK_PRECHECK) + r"\?([^\s\"'<>]*)")
|
|
92
|
+
#: Kept as groups so the separators survive the rebuild verbatim.
|
|
93
|
+
_OIDC_CALLBACK_PARAMETER_SPLIT = re.compile(r"([&;])")
|
|
94
|
+
_OIDC_CALLBACK_CREDENTIAL_KEYS = frozenset({"code", "state"})
|
|
95
|
+
_SECRET_HINTS = (
|
|
96
|
+
_INVITE_PATH_PRECHECK,
|
|
97
|
+
_OIDC_CALLBACK_PRECHECK,
|
|
98
|
+
"password",
|
|
99
|
+
"passwd",
|
|
100
|
+
"pwd",
|
|
101
|
+
"token",
|
|
102
|
+
"secret",
|
|
103
|
+
"api_key",
|
|
104
|
+
"api-key",
|
|
105
|
+
"apikey",
|
|
106
|
+
"bearer",
|
|
107
|
+
"basic",
|
|
108
|
+
"authorization",
|
|
109
|
+
)
|
|
110
|
+
_LOG_REDACTION = "[REDACTED]"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _redact_log_value(value: str | None) -> str | None:
|
|
114
|
+
collapsed = _collapse_log_value(value)
|
|
115
|
+
if collapsed is None:
|
|
116
|
+
return None
|
|
117
|
+
return _redact_secret_patterns(_USERINFO_PATTERN.sub(_redact_userinfo, collapsed))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _redact_invite_path_tokens_on_line(text: str) -> str:
|
|
121
|
+
return _INVITE_PATH_TOKEN_PATTERN.sub(_redact_path_secret, text)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _is_oidc_credential_key(raw_key: str) -> bool:
|
|
125
|
+
"""Whether ``raw_key`` is *any* spelling of a key the callback reads as a credential.
|
|
126
|
+
|
|
127
|
+
``unquote_plus`` is the decoding ``parse_qsl`` applies to names, so this asks
|
|
128
|
+
the same question the router does. It never raises: a stray ``%`` or an
|
|
129
|
+
invalid escape comes back unchanged and simply does not match.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
return unquote_plus(raw_key).casefold() in _OIDC_CALLBACK_CREDENTIAL_KEYS
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _redact_oidc_callback_parameter(parameter: str) -> str:
|
|
136
|
+
key, separator, _ = parameter.partition("=")
|
|
137
|
+
if not separator or not _is_oidc_credential_key(key):
|
|
138
|
+
return parameter
|
|
139
|
+
# The key is written back as it arrived; only the value goes.
|
|
140
|
+
return f"{key}={_LOG_REDACTION}"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _redact_oidc_callback_query(match: re.Match[str]) -> str:
|
|
144
|
+
"""Mask ``code`` and ``state`` inside one callback query, keeping the rest readable."""
|
|
145
|
+
|
|
146
|
+
parts = _OIDC_CALLBACK_PARAMETER_SPLIT.split(match.group(1))
|
|
147
|
+
query = "".join(part if index % 2 else _redact_oidc_callback_parameter(part) for index, part in enumerate(parts))
|
|
148
|
+
return f"{_OIDC_CALLBACK_PRECHECK}?{query}"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _redact_oidc_callback_on_line(text: str) -> str:
|
|
152
|
+
return _OIDC_CALLBACK_QUERY_PATTERN.sub(_redact_oidc_callback_query, text)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _redact_secret_patterns_on_line(text: str) -> str:
|
|
156
|
+
redacted = _redact_oidc_callback_on_line(_redact_invite_path_tokens_on_line(text))
|
|
157
|
+
redacted = _JSON_SENSITIVE_LOG_VALUE_PATTERN.sub(_redact_json_secret, redacted)
|
|
158
|
+
redacted = _SENSITIVE_LOG_VALUE_PATTERNS[0].sub(_redact_keyed_secret, redacted)
|
|
159
|
+
redacted = _SENSITIVE_LOG_VALUE_PATTERNS[1].sub(_redact_bearer_token, redacted)
|
|
160
|
+
redacted = _BASIC_TOKEN_PATTERN.sub(_redact_bearer_token, redacted)
|
|
161
|
+
redacted = _SENSITIVE_LOG_VALUE_PATTERNS[2].sub(_redact_authorization_value, redacted)
|
|
162
|
+
# Last, so ``'Proxy-Authorization': 'Basic [REDACTED]'`` keeps the scheme
|
|
163
|
+
# the token passes above already exposed.
|
|
164
|
+
return _PYTHON_REPR_SENSITIVE_LOG_VALUE_PATTERN.sub(_redact_python_repr_secret, redacted)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _map_log_lines(text: str, transform: Callable[[str], str]) -> str:
|
|
168
|
+
if "\n" not in text and "\r" not in text:
|
|
169
|
+
return transform(text)
|
|
170
|
+
return "".join(transform(part) if index % 2 == 0 else part for index, part in enumerate(_LINE_BREAKS.split(text)))
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _redact_secret_patterns(text: str) -> str:
|
|
174
|
+
return _map_log_lines(text, _redact_secret_patterns_on_line)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _redact_basic_tokens_on_line(text: str) -> str:
|
|
178
|
+
return _BASIC_TOKEN_PATTERN.sub(_redact_bearer_token, text)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def redact_rendered_log_text(text: str, *, keyed_secrets: bool = True) -> str:
|
|
182
|
+
"""Mask URL userinfo, Basic tokens and, optionally, keyed secrets in a rendered log string.
|
|
183
|
+
|
|
184
|
+
Applied to every rendered record regardless of the originating logger
|
|
185
|
+
(asyncio, aiohttp, uvicorn, tracebacks). ``keyed_secrets=False`` limits the
|
|
186
|
+
pass to the substring-precheck patterns (URL userinfo and ``Basic <token>``
|
|
187
|
+
in its ``Basic``/``basic``/``BASIC`` spellings); formatters use it for INFO and lower records because the
|
|
188
|
+
keyed patterns cost tens of microseconds on long hot-path lines. Never
|
|
189
|
+
raises: if a pass fails the text is replaced with a fail-closed placeholder
|
|
190
|
+
so logging itself cannot break and the original text is never emitted.
|
|
191
|
+
"""
|
|
192
|
+
try:
|
|
193
|
+
redacted = text
|
|
194
|
+
if "@" in text and "://" in text:
|
|
195
|
+
redacted = _USERINFO_PATTERN.sub(_redact_userinfo, redacted)
|
|
196
|
+
if any(precheck in text for precheck in _BASIC_TOKEN_PRECHECKS):
|
|
197
|
+
redacted = _map_log_lines(redacted, _redact_basic_tokens_on_line)
|
|
198
|
+
if _INVITE_PATH_PRECHECK in text:
|
|
199
|
+
# Access logs are INFO: the path secret must be masked even when the
|
|
200
|
+
# keyed pass below is skipped for cost.
|
|
201
|
+
redacted = _map_log_lines(redacted, _redact_invite_path_tokens_on_line)
|
|
202
|
+
if _OIDC_CALLBACK_PRECHECK in text:
|
|
203
|
+
# Same reason, for the query string: the access log renders the
|
|
204
|
+
# whole request line, and this route's is an authorization code.
|
|
205
|
+
redacted = _map_log_lines(redacted, _redact_oidc_callback_on_line)
|
|
206
|
+
if not keyed_secrets:
|
|
207
|
+
return redacted
|
|
208
|
+
folded = text.casefold()
|
|
209
|
+
for hint in _SECRET_HINTS:
|
|
210
|
+
if hint in folded:
|
|
211
|
+
return _redact_secret_patterns(redacted)
|
|
212
|
+
return redacted
|
|
213
|
+
except Exception:
|
|
214
|
+
return _REDACTION_FAILED_PLACEHOLDER
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _redact_record_text(record: logging.LogRecord, text: str) -> str:
|
|
218
|
+
return redact_rendered_log_text(text, keyed_secrets=record.levelno >= logging.WARNING)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _redact_path_secret(match: re.Match[str]) -> str:
|
|
222
|
+
return f"{match.group(1)}{_LOG_REDACTION}"
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _redact_userinfo(match: re.Match[str]) -> str:
|
|
226
|
+
return f"{match.group(1)}{_LOG_REDACTION}@"
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _redact_keyed_secret(match: re.Match[str]) -> str:
|
|
230
|
+
return f"{match.group(1)}{match.group(2)}{_LOG_REDACTION}"
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _redact_json_secret(match: re.Match[str]) -> str:
|
|
234
|
+
closer = match.group(2) or ""
|
|
235
|
+
return f"{match.group(1)}{_LOG_REDACTION}{closer}"
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _redact_python_repr_secret(match: re.Match[str]) -> str:
|
|
239
|
+
value = match.group(2)
|
|
240
|
+
if _LOG_REDACTION in value:
|
|
241
|
+
return match.group(0)
|
|
242
|
+
quote = value[0] if value[0] in "'\"" else ""
|
|
243
|
+
return f"{match.group(1)}{quote}{_LOG_REDACTION}{quote}"
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _redact_bearer_token(match: re.Match[str]) -> str:
|
|
247
|
+
return f"{match.group(1)}{_LOG_REDACTION}"
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _redact_authorization_value(match: re.Match[str]) -> str:
|
|
251
|
+
return f"{match.group(1)}{_LOG_REDACTION}"
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _utc_converter(seconds: float | None) -> time.struct_time:
|
|
255
|
+
return time.gmtime(seconds)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class _RedactingFormatterMixin(logging.Formatter):
|
|
259
|
+
"""Redact the fully rendered record (message, exception text, stack info)."""
|
|
260
|
+
|
|
261
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
262
|
+
rendered = _redact_record_text(record, super().format(record))
|
|
263
|
+
if rendered is _REDACTION_FAILED_PLACEHOLDER:
|
|
264
|
+
# Fail closed but keep a secret-free prefix (time, level, logger)
|
|
265
|
+
# so the line still says when and where it came from.
|
|
266
|
+
return f"{self.formatTime(record, self.datefmt)} {record.levelname} {record.name} {rendered}"
|
|
267
|
+
return rendered
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class UtcDefaultFormatter(_RedactingFormatterMixin, DefaultFormatter):
|
|
271
|
+
converter: Callable[[float | None], time.struct_time] = staticmethod(_utc_converter)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class UtcAccessFormatter(_RedactingFormatterMixin, AccessFormatter):
|
|
275
|
+
converter: Callable[[float | None], time.struct_time] = staticmethod(_utc_converter)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
# Containers nested deeper than this are rendered as redacted text instead of
|
|
279
|
+
# being rebuilt, so the key-aware pass can never recurse without bound.
|
|
280
|
+
_MAX_JSON_LOG_DEPTH = 32
|
|
281
|
+
# Placeholders for a container that refers back to an ancestor, mirroring the
|
|
282
|
+
# ``{...}`` / ``[...]`` markers Python's own repr emits for recursive objects.
|
|
283
|
+
_CYCLIC_JSON_LOG_PLACEHOLDERS: dict[type, str] = {dict: "{...}", list: "[...]", tuple: "(...)"}
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _redact_json_log_value(
|
|
287
|
+
record: logging.LogRecord,
|
|
288
|
+
value: object,
|
|
289
|
+
*,
|
|
290
|
+
_ancestors: frozenset[int] = frozenset(),
|
|
291
|
+
_depth: int = 0,
|
|
292
|
+
) -> object:
|
|
293
|
+
"""Rebuild ``value`` with redaction applied to every reachable string (and secret-keyed field).
|
|
294
|
+
|
|
295
|
+
Cycles (a container that refers back to an ancestor) collapse to a repr
|
|
296
|
+
placeholder and nesting beyond ``_MAX_JSON_LOG_DEPTH`` is rendered as
|
|
297
|
+
redacted text, so redaction still applies wherever the structure is finite
|
|
298
|
+
and the rebuild itself cannot raise ``RecursionError``.
|
|
299
|
+
"""
|
|
300
|
+
if isinstance(value, str):
|
|
301
|
+
return _redact_record_text(record, value)
|
|
302
|
+
if not isinstance(value, (dict, list, tuple)):
|
|
303
|
+
return value
|
|
304
|
+
if id(value) in _ancestors:
|
|
305
|
+
return next(marker for kind, marker in _CYCLIC_JSON_LOG_PLACEHOLDERS.items() if isinstance(value, kind))
|
|
306
|
+
if _depth >= _MAX_JSON_LOG_DEPTH:
|
|
307
|
+
return _redact_record_text(record, _safe_str(value))
|
|
308
|
+
ancestors = _ancestors | {id(value)}
|
|
309
|
+
depth = _depth + 1
|
|
310
|
+
if isinstance(value, dict):
|
|
311
|
+
return {
|
|
312
|
+
_redact_json_log_key(record, key): _redact_json_log_item(record, key, item, ancestors, depth)
|
|
313
|
+
for key, item in value.items()
|
|
314
|
+
}
|
|
315
|
+
return [_redact_json_log_value(record, item, _ancestors=ancestors, _depth=depth) for item in value]
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _safe_str(value: object) -> str:
|
|
319
|
+
# ``str()`` of an extra can itself raise (exploding ``__repr__``, repr of a
|
|
320
|
+
# pathologically deep container); logging must still emit the record.
|
|
321
|
+
try:
|
|
322
|
+
return str(value)
|
|
323
|
+
except Exception:
|
|
324
|
+
return f"<unprintable {type(value).__name__}>"
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _redact_json_log_key(record: logging.LogRecord, key: object) -> object:
|
|
328
|
+
# Keys are rendered too (``{"https://u:pw@proxy": "failed"}``).
|
|
329
|
+
return _redact_record_text(record, key) if isinstance(key, str) else key
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _is_secret_json_log_key(record: logging.LogRecord, key: object) -> bool:
|
|
333
|
+
# Key-aware for WARNING+ like the keyed text patterns: ``{"password": x}``
|
|
334
|
+
# carries the secret in a value the value-only pass cannot recognise,
|
|
335
|
+
# whatever its type (string, list, number, bytes).
|
|
336
|
+
return record.levelno >= logging.WARNING and isinstance(key, str) and bool(_SENSITIVE_LOG_KEY_PATTERN.search(key))
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _redact_json_log_item(
|
|
340
|
+
record: logging.LogRecord, key: object, value: object, ancestors: frozenset[int], depth: int
|
|
341
|
+
) -> object:
|
|
342
|
+
if value is not None and _is_secret_json_log_key(record, key):
|
|
343
|
+
return _LOG_REDACTION
|
|
344
|
+
return _redact_json_log_value(record, value, _ancestors=ancestors, _depth=depth)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class JsonFormatter(logging.Formatter):
|
|
348
|
+
def __init__(self) -> None:
|
|
349
|
+
super().__init__()
|
|
350
|
+
|
|
351
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
352
|
+
log_entry: dict[str, object] = {
|
|
353
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
|
354
|
+
"level": record.levelname,
|
|
355
|
+
"logger": record.name,
|
|
356
|
+
"message": _redact_record_text(record, record.getMessage()),
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
try:
|
|
360
|
+
from app.core.tracing.otel import get_current_span_id, get_current_trace_id
|
|
361
|
+
|
|
362
|
+
trace_id = get_current_trace_id()
|
|
363
|
+
span_id = get_current_span_id()
|
|
364
|
+
if trace_id:
|
|
365
|
+
log_entry["trace_id"] = trace_id
|
|
366
|
+
if span_id:
|
|
367
|
+
log_entry["span_id"] = span_id
|
|
368
|
+
except Exception:
|
|
369
|
+
pass
|
|
370
|
+
|
|
371
|
+
excluded_keys = {
|
|
372
|
+
"name",
|
|
373
|
+
"msg",
|
|
374
|
+
"args",
|
|
375
|
+
"levelname",
|
|
376
|
+
"levelno",
|
|
377
|
+
"pathname",
|
|
378
|
+
"filename",
|
|
379
|
+
"module",
|
|
380
|
+
"exc_info",
|
|
381
|
+
"exc_text",
|
|
382
|
+
"stack_info",
|
|
383
|
+
"lineno",
|
|
384
|
+
"funcName",
|
|
385
|
+
"created",
|
|
386
|
+
"msecs",
|
|
387
|
+
"relativeCreated",
|
|
388
|
+
"thread",
|
|
389
|
+
"threadName",
|
|
390
|
+
"processName",
|
|
391
|
+
"process",
|
|
392
|
+
"message",
|
|
393
|
+
"taskName",
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
for key, value in record.__dict__.items():
|
|
397
|
+
if key in excluded_keys:
|
|
398
|
+
continue
|
|
399
|
+
safe_key = str(_redact_json_log_key(record, key))
|
|
400
|
+
if value is not None and _is_secret_json_log_key(record, key):
|
|
401
|
+
log_entry[safe_key] = _LOG_REDACTION
|
|
402
|
+
continue
|
|
403
|
+
# Rebuild containers key-aware first so a non-serializable leaf
|
|
404
|
+
# (bytes) cannot drag secret-keyed siblings into the str() fallback.
|
|
405
|
+
# Redaction never raises: if the rebuild fails anyway (a container
|
|
406
|
+
# whose iteration explodes), fall back to the legacy rendering of
|
|
407
|
+
# the original value rather than dropping the record.
|
|
408
|
+
try:
|
|
409
|
+
redacted = _redact_json_log_value(record, value)
|
|
410
|
+
except Exception:
|
|
411
|
+
redacted = value
|
|
412
|
+
try:
|
|
413
|
+
json.dumps(redacted)
|
|
414
|
+
log_entry[safe_key] = redacted
|
|
415
|
+
except Exception:
|
|
416
|
+
# TypeError/ValueError for unserializable leaves and cycles;
|
|
417
|
+
# anything else (RecursionError, exploding iteration) too.
|
|
418
|
+
log_entry[safe_key] = _redact_record_text(record, _safe_str(redacted))
|
|
419
|
+
|
|
420
|
+
if record.exc_info:
|
|
421
|
+
log_entry["exception"] = _redact_record_text(record, self.formatException(record.exc_info))
|
|
422
|
+
|
|
423
|
+
return json.dumps(log_entry, default=str)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
class JsonAccessFormatter(logging.Formatter):
|
|
427
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
428
|
+
log_entry: dict[str, JsonValue] = {
|
|
429
|
+
"timestamp": datetime.fromtimestamp(record.created, tz=UTC).strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
|
|
430
|
+
"level": record.levelname,
|
|
431
|
+
"logger": record.name,
|
|
432
|
+
"type": "access",
|
|
433
|
+
"client": getattr(record, "client_addr", None),
|
|
434
|
+
"request": cast(JsonValue, _redact_json_log_value(record, getattr(record, "request_line", None))),
|
|
435
|
+
"status": getattr(record, "status_code", None),
|
|
436
|
+
}
|
|
437
|
+
return json.dumps(log_entry, default=str)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
type LogConfigValue = str | bool | None | dict[str, "LogConfigValue"]
|
|
441
|
+
type LogConfig = dict[str, LogConfigValue]
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def build_log_config() -> LogConfig:
|
|
445
|
+
from app.core.config.settings import get_settings
|
|
446
|
+
|
|
447
|
+
config = copy.deepcopy(LOGGING_CONFIG)
|
|
448
|
+
formatters = config.setdefault("formatters", {})
|
|
449
|
+
handlers = config.setdefault("handlers", {})
|
|
450
|
+
settings = get_settings()
|
|
451
|
+
|
|
452
|
+
if settings.log_format == "json":
|
|
453
|
+
formatters["default"] = {
|
|
454
|
+
"()": "app.core.runtime_logging.JsonFormatter",
|
|
455
|
+
}
|
|
456
|
+
else:
|
|
457
|
+
formatters["default"] = {
|
|
458
|
+
"()": "app.core.runtime_logging.UtcDefaultFormatter",
|
|
459
|
+
"fmt": "%(asctime)s %(levelprefix)s %(name)s %(message)s",
|
|
460
|
+
"datefmt": "%Y-%m-%dT%H:%M:%SZ",
|
|
461
|
+
"use_colors": None,
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if settings.log_format == "json":
|
|
465
|
+
formatters["access"] = {
|
|
466
|
+
"()": "app.core.runtime_logging.JsonAccessFormatter",
|
|
467
|
+
}
|
|
468
|
+
else:
|
|
469
|
+
formatters["access"] = {
|
|
470
|
+
"()": "app.core.runtime_logging.UtcAccessFormatter",
|
|
471
|
+
"fmt": '%(asctime)s %(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s',
|
|
472
|
+
"datefmt": "%Y-%m-%dT%H:%M:%SZ",
|
|
473
|
+
"use_colors": None,
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
# Uvicorn's stock config only wires uvicorn.* loggers. Attach the same
|
|
477
|
+
# default handler to the root logger so application loggers such as
|
|
478
|
+
# app.core.balancer.logic surface in docker logs at INFO.
|
|
479
|
+
handlers.setdefault(
|
|
480
|
+
"default", {"class": "logging.StreamHandler", "formatter": "default", "stream": "ext://sys.stderr"}
|
|
481
|
+
)
|
|
482
|
+
config["root"] = {
|
|
483
|
+
"handlers": ["default"],
|
|
484
|
+
"level": "INFO",
|
|
485
|
+
}
|
|
486
|
+
return cast(LogConfig, config)
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
class _RedactedRepr:
|
|
490
|
+
"""Stand-in whose repr is the redacted rendering of the original object."""
|
|
491
|
+
|
|
492
|
+
__slots__ = ("_text",)
|
|
493
|
+
|
|
494
|
+
def __init__(self, text: str) -> None:
|
|
495
|
+
self._text = text
|
|
496
|
+
|
|
497
|
+
def __repr__(self) -> str:
|
|
498
|
+
return self._text
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
# Context values the default handler renders as text rather than repr().
|
|
502
|
+
_UNREDACTED_LOOP_CONTEXT_KEYS = frozenset({"message", "exception", "source_traceback", "handle_traceback"})
|
|
503
|
+
_REDACTING_LOOP_HANDLER_MARKER = "_codex_lb_redacting_loop_handler"
|
|
504
|
+
_LOOP_CONTEXT_REDACTION_FAILED = "[REDACTED: loop context redaction failed]"
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _fail_closed_loop_context(context: dict[str, Any]) -> dict[str, Any]:
|
|
508
|
+
"""Keep the textual entries and replace every object value with an opaque stand-in."""
|
|
509
|
+
try:
|
|
510
|
+
return {
|
|
511
|
+
key: value if key in _UNREDACTED_LOOP_CONTEXT_KEYS else _RedactedRepr(_LOOP_CONTEXT_REDACTION_FAILED)
|
|
512
|
+
for key, value in context.items()
|
|
513
|
+
}
|
|
514
|
+
except Exception:
|
|
515
|
+
return {"message": _LOOP_CONTEXT_REDACTION_FAILED}
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def install_redacting_loop_exception_handler(loop: asyncio.AbstractEventLoop) -> None:
|
|
519
|
+
"""Redact credential-bearing object reprs before the loop's default handler logs them.
|
|
520
|
+
|
|
521
|
+
The default asyncio/uvloop handler renders every context value with
|
|
522
|
+
``repr()`` (aiohttp ``Connection<ConnectionKey(... proxy=URL('http://u:pw@host'))>``,
|
|
523
|
+
``BasicAuth(... password='pw')``) into the ``asyncio`` logger before any
|
|
524
|
+
formatter runs. Idempotent; delegates to the previously installed handler
|
|
525
|
+
(or the default one) so formatting stays byte-identical for contexts that
|
|
526
|
+
contain no secrets. Fails closed: a value whose ``repr()`` raises is
|
|
527
|
+
replaced by an opaque stand-in, and any other failure delegates a context
|
|
528
|
+
whose object values are all stand-ins, so the report is still emitted but
|
|
529
|
+
no unredacted value ever reaches the delegate.
|
|
530
|
+
"""
|
|
531
|
+
previous = loop.get_exception_handler()
|
|
532
|
+
if previous is not None and getattr(previous, _REDACTING_LOOP_HANDLER_MARKER, False):
|
|
533
|
+
return
|
|
534
|
+
|
|
535
|
+
def _delegate(target_loop: asyncio.AbstractEventLoop, context: dict[str, Any]) -> None:
|
|
536
|
+
if previous is None:
|
|
537
|
+
target_loop.default_exception_handler(context)
|
|
538
|
+
else:
|
|
539
|
+
previous(target_loop, context)
|
|
540
|
+
|
|
541
|
+
def _handler(target_loop: asyncio.AbstractEventLoop, context: dict[str, Any]) -> None:
|
|
542
|
+
try:
|
|
543
|
+
safe_context = dict(context)
|
|
544
|
+
for key, value in context.items():
|
|
545
|
+
if key in _UNREDACTED_LOOP_CONTEXT_KEYS:
|
|
546
|
+
continue
|
|
547
|
+
try:
|
|
548
|
+
rendered = repr(value)
|
|
549
|
+
except Exception as exc:
|
|
550
|
+
# The default handler would call repr() again and lose the
|
|
551
|
+
# whole report; an opaque stand-in keeps the message line.
|
|
552
|
+
safe_context[key] = _RedactedRepr(f"<{type(value).__name__} repr failed: {type(exc).__name__}>")
|
|
553
|
+
continue
|
|
554
|
+
redacted = redact_rendered_log_text(rendered)
|
|
555
|
+
if redacted != rendered:
|
|
556
|
+
safe_context[key] = _RedactedRepr(redacted)
|
|
557
|
+
except Exception:
|
|
558
|
+
safe_context = _fail_closed_loop_context(context)
|
|
559
|
+
_delegate(target_loop, safe_context)
|
|
560
|
+
|
|
561
|
+
setattr(_handler, _REDACTING_LOOP_HANDLER_MARKER, True)
|
|
562
|
+
loop.set_exception_handler(_handler)
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def log_error_response(
|
|
566
|
+
logger: logging.Logger,
|
|
567
|
+
request: Request,
|
|
568
|
+
status_code: int,
|
|
569
|
+
code: str | None,
|
|
570
|
+
message: str | None,
|
|
571
|
+
*,
|
|
572
|
+
category: str,
|
|
573
|
+
exc_info: bool = False,
|
|
574
|
+
) -> None:
|
|
575
|
+
level = logging.ERROR if status_code >= 500 else logging.WARNING
|
|
576
|
+
logger.log(
|
|
577
|
+
level,
|
|
578
|
+
"%s request_id=%s method=%s path=%s status=%s code=%s message=%s",
|
|
579
|
+
category,
|
|
580
|
+
get_request_id(),
|
|
581
|
+
request.method,
|
|
582
|
+
request.url.path,
|
|
583
|
+
status_code,
|
|
584
|
+
_error_log_field(code),
|
|
585
|
+
_error_log_field(message),
|
|
586
|
+
exc_info=exc_info,
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def _error_log_field(value: str | None) -> str:
|
|
591
|
+
redacted = _redact_log_value(value)
|
|
592
|
+
if redacted is None:
|
|
593
|
+
return "-"
|
|
594
|
+
return json.dumps(redacted)
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def _collapse_log_value(value: str | None) -> str | None:
|
|
598
|
+
if value is None:
|
|
599
|
+
return None
|
|
600
|
+
collapsed = " ".join(value.split())
|
|
601
|
+
return collapsed or None
|
|
File without changes
|