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,872 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import socket
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from functools import cached_property, lru_cache
|
|
9
|
+
from ipaddress import ip_address, ip_network
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Annotated, Literal
|
|
12
|
+
from urllib.parse import urlparse
|
|
13
|
+
|
|
14
|
+
from dotenv import dotenv_values
|
|
15
|
+
from pydantic import AliasChoices, Field, field_validator, model_validator
|
|
16
|
+
from pydantic_settings import BaseSettings, NoDecode, SettingsConfigDict
|
|
17
|
+
|
|
18
|
+
from app.core.auth.dashboard_mode import DashboardAuthMode, normalize_dashboard_auth_proxy_header
|
|
19
|
+
from app.core.clients.codex_version_snapshot import CODEX_VERSION
|
|
20
|
+
from app.core.clients.thread_cache_identity import (
|
|
21
|
+
THREAD_CACHE_IDENTITY_MODE_DEFAULT,
|
|
22
|
+
normalize_thread_cache_identity_mode,
|
|
23
|
+
)
|
|
24
|
+
from app.core.utils.proxy_env import outbound_proxy_env_configured
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
BASE_DIR = Path(__file__).resolve().parents[3]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _resolve_env_files() -> tuple[Path, ...]:
|
|
32
|
+
"""Resolve the env files read at settings load.
|
|
33
|
+
|
|
34
|
+
Default: ``.env`` / ``.env.local`` next to the module root (the repository
|
|
35
|
+
checkout). ``CODEX_LB_ENV_FILE`` — an ``os.pathsep``-separated list of
|
|
36
|
+
paths — overrides that discovery for installs whose module root cannot
|
|
37
|
+
contain env files: the Nix package's module root lives in the read-only
|
|
38
|
+
store, so its wrapper points this at the launch directory. It is a
|
|
39
|
+
bootstrap environment variable rather than a ``Settings`` field because
|
|
40
|
+
the env-file locations must be known before Settings can read env files.
|
|
41
|
+
Unset (every non-Nix launch path) preserves module-root discovery
|
|
42
|
+
unchanged; launch-directory env files are never loaded implicitly.
|
|
43
|
+
"""
|
|
44
|
+
raw = os.getenv("CODEX_LB_ENV_FILE", "")
|
|
45
|
+
files = tuple(Path(entry.strip()).expanduser() for entry in raw.split(os.pathsep) if entry.strip())
|
|
46
|
+
if files:
|
|
47
|
+
return files
|
|
48
|
+
return (BASE_DIR / ".env", BASE_DIR / ".env.local")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
ENV_FILES = _resolve_env_files()
|
|
52
|
+
|
|
53
|
+
# OAuth protocol constants. These values identify codex-lb to OpenAI's OAuth
|
|
54
|
+
# endpoints exactly like the Codex CLI; they are protocol constants, not
|
|
55
|
+
# deployment tunables, and changing any of them breaks login
|
|
56
|
+
# (PRINCIPLES.md P2, issue #1340).
|
|
57
|
+
AUTH_BASE_URL = "https://auth.openai.com"
|
|
58
|
+
OAUTH_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann"
|
|
59
|
+
OAUTH_ORIGINATOR = "codex_chatgpt_desktop"
|
|
60
|
+
OAUTH_SCOPE = "openid profile email"
|
|
61
|
+
OAUTH_REDIRECT_URI = "http://localhost:1455/auth/callback"
|
|
62
|
+
OAUTH_CALLBACK_PORT = 1455 # Do not change the port. OpenAI dislikes changes.
|
|
63
|
+
|
|
64
|
+
# Env names of settings removed from the Settings surface (issue #1340,
|
|
65
|
+
# PRINCIPLES.md P2). ``extra="ignore"`` already makes them harmless; startup
|
|
66
|
+
# emits one WARN for one release as a courtesy to operators who still set them.
|
|
67
|
+
# Names whose warning release has shipped are pruned from this tuple (the
|
|
68
|
+
# July 2026 phases 1-4 shipped in v1.22-v1.24 and were dropped by
|
|
69
|
+
# ``remove-dead-env-settings``, the first release after v1.24.0).
|
|
70
|
+
_REMOVED_SETTINGS: tuple[str, ...] = (
|
|
71
|
+
# remove-dead-env-settings (first release after v1.24.0): retention is a
|
|
72
|
+
# dashboard runtime setting (Settings -> Advanced -> Data retention);
|
|
73
|
+
# NULL now means disabled.
|
|
74
|
+
"CODEX_LB_REQUEST_LOG_RETENTION_DAYS",
|
|
75
|
+
"CODEX_LB_USAGE_HISTORY_RETENTION_DAYS",
|
|
76
|
+
# remove-dead-env-settings (first release after v1.24.0): dashboard-owned
|
|
77
|
+
# columns that the env value only seeded on first boot (or never read).
|
|
78
|
+
"CODEX_LB_HTTP_DOWNSTREAM_TRANSPORT_POLICY",
|
|
79
|
+
"CODEX_LB_OPENAI_CACHE_AFFINITY_MAX_AGE_SECONDS",
|
|
80
|
+
"CODEX_LB_WARMUP_MODEL",
|
|
81
|
+
"CODEX_LB_HTTP_RESPONSES_SESSION_BRIDGE_GATEWAY_SAFE_MODE",
|
|
82
|
+
# Dashboard-authoritative settings (remove-upstream-stream-transport-env):
|
|
83
|
+
# the dashboard row is the only source of the upstream stream transport.
|
|
84
|
+
"CODEX_LB_UPSTREAM_STREAM_TRANSPORT",
|
|
85
|
+
# constantize-core-tunables (first release after v1.25.0-beta.5): never-tuned
|
|
86
|
+
# core tunables became fixed constants (see the openspec change context).
|
|
87
|
+
"CODEX_LB_UPSTREAM_COMPACT_TIMEOUT_SECONDS",
|
|
88
|
+
"CODEX_LB_MAX_SSE_EVENT_BYTES",
|
|
89
|
+
"CODEX_LB_UPSTREAM_RESPONSE_CREATE_MAX_BYTES",
|
|
90
|
+
"CODEX_LB_OAUTH_TIMEOUT_SECONDS",
|
|
91
|
+
"CODEX_LB_TOKEN_REFRESH_TIMEOUT_SECONDS",
|
|
92
|
+
"CODEX_LB_TOKEN_REFRESH_CLAIM_TTL_SECONDS",
|
|
93
|
+
"CODEX_LB_PROXY_REFRESH_FAILURE_COOLDOWN_SECONDS",
|
|
94
|
+
"CODEX_LB_PROXY_ADMISSION_WAIT_TIMEOUT_SECONDS",
|
|
95
|
+
"CODEX_LB_USAGE_FETCH_TIMEOUT_SECONDS",
|
|
96
|
+
"CODEX_LB_USAGE_FETCH_MAX_RETRIES",
|
|
97
|
+
"CODEX_LB_USAGE_REFRESH_ENABLED",
|
|
98
|
+
"CODEX_LB_USAGE_REFRESH_INTERVAL_SECONDS",
|
|
99
|
+
"CODEX_LB_USAGE_REFRESH_AUTH_FAILURE_COOLDOWN_SECONDS",
|
|
100
|
+
"CODEX_LB_LIVE_USAGE_INGESTION_ENABLED",
|
|
101
|
+
"CODEX_LB_RATE_LIMIT_RESET_CREDITS_REFRESH_INTERVAL_SECONDS",
|
|
102
|
+
"CODEX_LB_STICKY_SESSION_CLEANUP_ENABLED",
|
|
103
|
+
"CODEX_LB_QUOTA_PLANNER_SCHEDULER_ENABLED",
|
|
104
|
+
"CODEX_LB_MODEL_REGISTRY_ENABLED",
|
|
105
|
+
"CODEX_LB_MAX_DECOMPRESSED_BODY_BYTES",
|
|
106
|
+
"CODEX_LB_MAX_DECOMPRESSED_RESPONSES_BODY_BYTES",
|
|
107
|
+
"CODEX_LB_IMAGE_INLINE_FETCH_ENABLED",
|
|
108
|
+
"CODEX_LB_IMAGE_INLINE_ALLOWED_HOSTS",
|
|
109
|
+
"CODEX_LB_IMAGES_DEFAULT_MODEL",
|
|
110
|
+
"CODEX_LB_OPENAI_PROMPT_CACHE_KEY_DERIVATION_ENABLED",
|
|
111
|
+
"CODEX_LB_PROXY_TOKEN_REFRESH_LIMIT",
|
|
112
|
+
"CODEX_LB_PROXY_UPSTREAM_WEBSOCKET_CONNECT_LIMIT",
|
|
113
|
+
"CODEX_LB_PROXY_COMPACT_RESPONSE_CREATE_LIMIT",
|
|
114
|
+
# K2 bridge (constantize-session-bridge-tunables): never-tuned HTTP session
|
|
115
|
+
# bridge tunables are fixed module constants now (see
|
|
116
|
+
# app/modules/proxy/_service/http_bridge/helpers.py, retry_circuit.py,
|
|
117
|
+
# request_submit.py and app/modules/proxy/api.py).
|
|
118
|
+
"CODEX_LB_HTTP_RESPONSES_SESSION_BRIDGE_IDLE_TTL_SECONDS",
|
|
119
|
+
"CODEX_LB_HTTP_RESPONSES_SESSION_BRIDGE_CODEX_IDLE_TTL_SECONDS",
|
|
120
|
+
"CODEX_LB_HTTP_RESPONSES_SESSION_BRIDGE_STUCK_GATE_RETIRE_AFTER_SECONDS",
|
|
121
|
+
"CODEX_LB_HTTP_RESPONSES_SESSION_BRIDGE_ANCHOR_POISON_FAILURE_THRESHOLD",
|
|
122
|
+
"CODEX_LB_HTTP_RESPONSES_SESSION_BRIDGE_SERVER_RECOVERY_MAX_ATTEMPTS",
|
|
123
|
+
"CODEX_LB_HTTP_RESPONSES_SESSION_BRIDGE_CLEAN_CLOSE_RETRY_JITTER_MAX_SECONDS",
|
|
124
|
+
"CODEX_LB_HTTP_RESPONSES_SESSION_BRIDGE_OPERATION_LEDGER_ENABLED",
|
|
125
|
+
# end K2 bridge
|
|
126
|
+
# drop-bridge-recovery-modes (first release after v1.25.0-beta.7): the
|
|
127
|
+
# three non-default ambiguous-continuation recovery modes were deleted and
|
|
128
|
+
# the shipped ``fail_closed`` behaviour is now the only one.
|
|
129
|
+
"CODEX_LB_HTTP_RESPONSES_SESSION_BRIDGE_AMBIGUOUS_CONTINUATION_RECOVERY_MODE",
|
|
130
|
+
# constantize-token-refresh-interval (first release after v1.25.0-beta.7):
|
|
131
|
+
# the proactive refresh window is the fixed eight-day
|
|
132
|
+
# ``TOKEN_REFRESH_INTERVAL_DAYS`` in ``app/core/auth/refresh.py``. Its only
|
|
133
|
+
# live consumer, the traffic-parity canary, now suppresses proactive
|
|
134
|
+
# refresh by stamping its isolated ``auth.json`` inside the window instead
|
|
135
|
+
# of widening the window for the whole process.
|
|
136
|
+
"CODEX_LB_TOKEN_REFRESH_INTERVAL_DAYS",
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def warn_removed_settings(environ: Mapping[str, str] | None = None) -> list[str]:
|
|
141
|
+
"""Log one WARN listing removed ``CODEX_LB_*`` env vars still set.
|
|
142
|
+
|
|
143
|
+
Scans the process environment plus the same env files Settings loads
|
|
144
|
+
(``ENV_FILES``), so removed names lingering in ``.env``/``.env.local``
|
|
145
|
+
are reported too. Returns the removed names found so the startup caller
|
|
146
|
+
and tests share one source of truth. Values are never logged.
|
|
147
|
+
"""
|
|
148
|
+
if environ is None:
|
|
149
|
+
source: Mapping[str, str | None] = _effective_environ()
|
|
150
|
+
else:
|
|
151
|
+
source = environ
|
|
152
|
+
found = [name for name in _REMOVED_SETTINGS if _env_key(source, name) is not None]
|
|
153
|
+
if found:
|
|
154
|
+
logger.warning(
|
|
155
|
+
"removed setting(s) ignored: %s — each is now a fixed default or a dashboard runtime setting; "
|
|
156
|
+
"see PRINCIPLES.md P2 / issue #1340",
|
|
157
|
+
", ".join(found),
|
|
158
|
+
)
|
|
159
|
+
return found
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
DOCKER_DATA_DIR = Path("/var/lib/codex-lb")
|
|
163
|
+
DOCKER_CALLBACK_HOST = "0.0.0.0"
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _in_container() -> bool:
|
|
167
|
+
return Path("/.dockerenv").exists() or Path("/run/.containerenv").exists()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _default_home_dir() -> Path:
|
|
171
|
+
env_dir = os.getenv("CODEX_LB_DATA_DIR")
|
|
172
|
+
if env_dir and env_dir.strip():
|
|
173
|
+
return Path(env_dir.strip())
|
|
174
|
+
home_dir = Path.home() / ".codex-lb"
|
|
175
|
+
if home_dir.exists():
|
|
176
|
+
return home_dir
|
|
177
|
+
if _in_container():
|
|
178
|
+
return DOCKER_DATA_DIR
|
|
179
|
+
return home_dir
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _default_oauth_callback_host() -> str:
|
|
183
|
+
if _in_container():
|
|
184
|
+
return DOCKER_CALLBACK_HOST
|
|
185
|
+
return "127.0.0.1"
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _default_http_bridge_instance_id() -> str:
|
|
189
|
+
hostname = socket.gethostname().strip()
|
|
190
|
+
return hostname or "codex-lb"
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _default_upstream_websocket_trust_env() -> bool:
|
|
194
|
+
return outbound_proxy_env_configured(_effective_environ())
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# Startup guard, not a setting: the only supported value is 1 (see
|
|
198
|
+
# ``Settings._reject_multiple_workers_per_instance``).
|
|
199
|
+
_WORKERS_PER_INSTANCE_ENV = "CODEX_LB_WORKERS_PER_INSTANCE"
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _effective_environ() -> dict[str, str | None]:
|
|
203
|
+
environ: dict[str, str | None] = {}
|
|
204
|
+
for env_file in ENV_FILES:
|
|
205
|
+
environ.update(dotenv_values(env_file))
|
|
206
|
+
environ.update(os.environ)
|
|
207
|
+
return environ
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _env_key(environ: Mapping[str, str | None], name: str) -> str | None:
|
|
211
|
+
"""Return the key under which ``name`` is declared, matching case-insensitively.
|
|
212
|
+
|
|
213
|
+
pydantic-settings resolves ``CODEX_LB_*`` names case-insensitively
|
|
214
|
+
(``case_sensitive=False`` is the ``BaseSettings`` default), so the env
|
|
215
|
+
guards that replaced former fields must accept the same spellings a field
|
|
216
|
+
did. An exact-case declaration wins over other casings.
|
|
217
|
+
"""
|
|
218
|
+
if name in environ:
|
|
219
|
+
return name
|
|
220
|
+
upper = name.upper()
|
|
221
|
+
for key in environ:
|
|
222
|
+
if key.upper() == upper:
|
|
223
|
+
return key
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
DEFAULT_HOME_DIR = _default_home_dir()
|
|
228
|
+
DEFAULT_DB_PATH = DEFAULT_HOME_DIR / "store.db"
|
|
229
|
+
DEFAULT_ENCRYPTION_KEY_FILE = DEFAULT_HOME_DIR / "encryption.key"
|
|
230
|
+
DEFAULT_CONVERSATION_ARCHIVE_DIR = DEFAULT_HOME_DIR / "conversation-archive"
|
|
231
|
+
DEFAULT_DATABASE_URL = f"sqlite+aiosqlite:///{DEFAULT_DB_PATH}"
|
|
232
|
+
type StringListInput = str | list[str] | None
|
|
233
|
+
type OptionalStringInput = str | None
|
|
234
|
+
type ModelContextWindowOverridesInput = str | dict[str, int] | None
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _validate_context_window_entries(data: Mapping[str, object]) -> dict[str, int]:
|
|
238
|
+
result: dict[str, int] = {}
|
|
239
|
+
for k, v in data.items():
|
|
240
|
+
if isinstance(v, bool):
|
|
241
|
+
raise TypeError(f"model_context_window_overrides value for '{k}' must be a positive integer, got bool")
|
|
242
|
+
if not isinstance(v, int):
|
|
243
|
+
raise TypeError(
|
|
244
|
+
f"model_context_window_overrides value for '{k}' must be a positive integer, got {type(v).__name__}"
|
|
245
|
+
)
|
|
246
|
+
if v <= 0:
|
|
247
|
+
raise ValueError(f"model_context_window_overrides value for '{k}' must be a positive integer, got {v}")
|
|
248
|
+
result[str(k)] = v
|
|
249
|
+
return result
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _parse_port_value(raw: str) -> int | None:
|
|
253
|
+
try:
|
|
254
|
+
port = int(raw)
|
|
255
|
+
except ValueError:
|
|
256
|
+
return None
|
|
257
|
+
if port <= 0:
|
|
258
|
+
return None
|
|
259
|
+
return port
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _configured_http_port() -> int:
|
|
263
|
+
raw_env_port = os.getenv("PORT")
|
|
264
|
+
if raw_env_port is not None:
|
|
265
|
+
parsed_env_port = _parse_port_value(raw_env_port.strip())
|
|
266
|
+
if parsed_env_port is not None:
|
|
267
|
+
return parsed_env_port
|
|
268
|
+
return 2455
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _normalize_cidr_list(value: StringListInput, *, field_name: str, invalid_label: str) -> list[str]:
|
|
272
|
+
if value is None:
|
|
273
|
+
return []
|
|
274
|
+
|
|
275
|
+
cidrs: list[str] = []
|
|
276
|
+
if isinstance(value, str):
|
|
277
|
+
entries = [entry.strip() for entry in value.split(",")]
|
|
278
|
+
cidrs = [entry for entry in entries if entry]
|
|
279
|
+
elif isinstance(value, list):
|
|
280
|
+
for entry in value:
|
|
281
|
+
if isinstance(entry, str):
|
|
282
|
+
cidr = entry.strip()
|
|
283
|
+
if cidr:
|
|
284
|
+
cidrs.append(cidr)
|
|
285
|
+
else:
|
|
286
|
+
raise TypeError(f"{field_name} must be a list or comma-separated string")
|
|
287
|
+
|
|
288
|
+
for cidr in cidrs:
|
|
289
|
+
try:
|
|
290
|
+
ip_network(cidr, strict=False)
|
|
291
|
+
except ValueError as exc:
|
|
292
|
+
raise ValueError(f"Invalid {invalid_label}: {cidr}") from exc
|
|
293
|
+
return cidrs
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class Settings(BaseSettings):
|
|
297
|
+
model_config = SettingsConfigDict(
|
|
298
|
+
env_prefix="CODEX_LB_",
|
|
299
|
+
env_file=ENV_FILES,
|
|
300
|
+
env_file_encoding="utf-8",
|
|
301
|
+
extra="ignore",
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
data_dir: Path = Field(default_factory=_default_home_dir)
|
|
305
|
+
database_url: str = DEFAULT_DATABASE_URL
|
|
306
|
+
# Pool timeout and recycle are fixed constants in ``app/db/session.py``;
|
|
307
|
+
# the background-task engine always derives its pool sizing from the two
|
|
308
|
+
# settings below. Defaults are sized so one replica's two pooled engines
|
|
309
|
+
# cap at (25 + 15) * 2 = 80 PostgreSQL connections, preserving >= 20 raw
|
|
310
|
+
# server slots on PostgreSQL's default max_connections=100 for reserved
|
|
311
|
+
# connections, the migration path's two-connection peak, and operations.
|
|
312
|
+
database_pool_size: int = Field(default=25, gt=0)
|
|
313
|
+
database_max_overflow: int = Field(default=15, ge=0)
|
|
314
|
+
database_migrate_on_startup: bool = True
|
|
315
|
+
database_sqlite_pre_migrate_backup_enabled: bool = True
|
|
316
|
+
database_sqlite_pre_migrate_backup_max_files: int = Field(default=5, ge=1)
|
|
317
|
+
database_sqlite_startup_check_mode: Literal["quick", "full", "off"] = "quick"
|
|
318
|
+
database_alembic_auto_remap_enabled: bool = True
|
|
319
|
+
database_migration_lock_timeout_seconds: float = Field(default=300.0, gt=0)
|
|
320
|
+
upstream_base_url: str = "https://chatgpt.com/backend-api"
|
|
321
|
+
# T3 → dashboard (deprecated env alias, remove next minor)
|
|
322
|
+
upstream_connect_timeout_seconds: float = 8.0
|
|
323
|
+
upstream_websocket_trust_env: bool = Field(default_factory=_default_upstream_websocket_trust_env)
|
|
324
|
+
# T3 → dashboard (deprecated env alias, remove next minor)
|
|
325
|
+
proxy_request_budget_seconds: float = Field(default=600.0, gt=0)
|
|
326
|
+
# T3 → dashboard (deprecated env alias, remove next minor)
|
|
327
|
+
http_responses_stream_request_budget_seconds: float = Field(default=7200.0, gt=0)
|
|
328
|
+
# T3 → dashboard (deprecated env alias, remove next minor)
|
|
329
|
+
compact_request_budget_seconds: float = Field(default=180.0, gt=0)
|
|
330
|
+
# T3 → dashboard (deprecated env alias, remove next minor)
|
|
331
|
+
stream_idle_timeout_seconds: float = Field(default=7200.0, gt=0)
|
|
332
|
+
# T3 → dashboard (deprecated env alias, remove next minor)
|
|
333
|
+
sse_keepalive_interval_seconds: float = Field(default=10.0, ge=0)
|
|
334
|
+
# T3 → dashboard (deprecated env alias, remove next minor)
|
|
335
|
+
proxy_downstream_websocket_idle_timeout_seconds: float = Field(default=120.0, gt=0)
|
|
336
|
+
oauth_callback_host: str = _default_oauth_callback_host()
|
|
337
|
+
# T3 → dashboard (deprecated env alias, remove next minor)
|
|
338
|
+
auth_guardian_enabled: bool = True
|
|
339
|
+
# T3 → dashboard (deprecated env alias, remove next minor)
|
|
340
|
+
transcription_request_budget_seconds: float = Field(default=120.0, gt=0)
|
|
341
|
+
# T1 (topology). Path to a JSON registry of additional usage quota keys
|
|
342
|
+
# that replaces the bundled ``config/additional_quota_registry.json``.
|
|
343
|
+
# Unset (or blank) keeps the bundled registry. The Alembic backfill
|
|
344
|
+
# migration ``20260312_000000`` reads the same env name directly because
|
|
345
|
+
# migrations must not depend on ``Settings``.
|
|
346
|
+
additional_quota_registry_file: Path | None = None
|
|
347
|
+
# T3 → dashboard (deprecated env alias, remove next minor)
|
|
348
|
+
rate_limit_reset_credits_refresh_enabled: bool = True
|
|
349
|
+
http_responses_session_bridge_enabled: bool = True
|
|
350
|
+
# T3 → dashboard (deprecated env alias, remove next minor)
|
|
351
|
+
http_responses_session_bridge_request_budget_seconds: float = Field(default=7200.0, gt=0)
|
|
352
|
+
# T3 → dashboard (deprecated env alias, remove next minor): the same-name
|
|
353
|
+
# ``dashboard_settings`` column wins when set (M3 codex prewarm).
|
|
354
|
+
http_responses_session_bridge_codex_prewarm_enabled: bool = False
|
|
355
|
+
http_responses_session_bridge_max_sessions: int = Field(default=256, gt=0)
|
|
356
|
+
http_responses_session_bridge_queue_limit: int = Field(default=8, gt=0)
|
|
357
|
+
# Bound durable replay storage per operation so a long response cannot
|
|
358
|
+
# exhaust the database. An incomplete spool is never replayed.
|
|
359
|
+
http_responses_session_bridge_operation_event_spool_max_bytes: int = Field(default=2 * 1024 * 1024, gt=0)
|
|
360
|
+
# Rollout fence: chunks_v2 must be enabled only after every serving replica
|
|
361
|
+
# runs the dual-reader schema expansion.
|
|
362
|
+
http_responses_session_bridge_operation_spool_format: Literal["rows_v1", "chunks_v2"] = "rows_v1"
|
|
363
|
+
http_responses_session_bridge_operation_event_spool_batch_size: int = Field(default=32, gt=0, le=256)
|
|
364
|
+
http_responses_session_bridge_operation_event_spool_flush_interval_seconds: float = Field(
|
|
365
|
+
default=0.1,
|
|
366
|
+
ge=0.01,
|
|
367
|
+
le=5.0,
|
|
368
|
+
)
|
|
369
|
+
http_responses_session_bridge_operation_event_spool_max_pending_events: int = Field(default=2048, gt=0)
|
|
370
|
+
http_responses_session_bridge_operation_event_spool_max_pending_bytes: int = Field(
|
|
371
|
+
default=32 * 1024 * 1024,
|
|
372
|
+
gt=0,
|
|
373
|
+
)
|
|
374
|
+
# Keep durable transcript material short-lived by default. The transcript
|
|
375
|
+
# is sensitive prompt/output data and is only a recovery aid.
|
|
376
|
+
# T3 → dashboard (deprecated env alias, remove next minor)
|
|
377
|
+
http_responses_session_bridge_operation_spool_retention_seconds: float = Field(
|
|
378
|
+
default=7 * 24 * 60 * 60,
|
|
379
|
+
gt=0,
|
|
380
|
+
)
|
|
381
|
+
http_responses_session_bridge_instance_id: str = Field(default_factory=_default_http_bridge_instance_id)
|
|
382
|
+
http_responses_session_bridge_instance_ring: Annotated[list[str], NoDecode] = Field(default_factory=list)
|
|
383
|
+
http_responses_session_bridge_advertise_base_url: str | None = None
|
|
384
|
+
# TTL backstop for the per-account upstream-route resolution cache; 0
|
|
385
|
+
# disables caching. Admin mutations invalidate durably through the
|
|
386
|
+
# cache-invalidation bus, so this only bounds out-of-band database edits.
|
|
387
|
+
upstream_route_cache_ttl_seconds: float = Field(default=60.0, ge=0)
|
|
388
|
+
# T3 → dashboard (deprecated env alias, remove next minor)
|
|
389
|
+
automations_scheduler_enabled: bool = True
|
|
390
|
+
# T3 (dashboard home: dashboard_settings.telemetry_consent). Headless
|
|
391
|
+
# first-boot opt-out fallback; a persisted dashboard decision always wins.
|
|
392
|
+
telemetry_enabled: bool | None = None
|
|
393
|
+
telemetry_endpoint: str = "https://telemetry.tokmaxxing.com"
|
|
394
|
+
encryption_key_file: Path = DEFAULT_ENCRYPTION_KEY_FILE
|
|
395
|
+
# Startup cross-replica encryption-key consistency check against the shared
|
|
396
|
+
# database sentinel: "enforce" refuses startup on mismatch, "warn" logs an
|
|
397
|
+
# ERROR and continues, "off" disables the check.
|
|
398
|
+
encryption_key_fingerprint_mode: Literal["enforce", "warn", "off"] = "enforce"
|
|
399
|
+
database_migrations_fail_fast: bool = True
|
|
400
|
+
# Incident-debugging trace channels (env ``CODEX_LB_TRACE``), a
|
|
401
|
+
# comma-separated list. Empty (the default) disables all trace logging.
|
|
402
|
+
# Channels: ``shape`` (request shape), ``shape_raw_cache_key`` (include the
|
|
403
|
+
# raw prompt cache key in shape logs), ``payload`` (downstream request
|
|
404
|
+
# payload), ``service_tier`` (service-tier trace), ``upstream_summary``
|
|
405
|
+
# (upstream request summary/completion), ``upstream_payload`` (upstream
|
|
406
|
+
# request payload). Interactive incident use only, not steady-state config.
|
|
407
|
+
trace: str = ""
|
|
408
|
+
# T3 → dashboard (deprecated env alias, remove next minor): the
|
|
409
|
+
# ``dashboard_settings.conversation_archive_enabled`` column wins when set;
|
|
410
|
+
# the archive writer resolves it from the settings-cache snapshot.
|
|
411
|
+
conversation_archive_enabled: bool = False
|
|
412
|
+
conversation_archive_dir: Path = DEFAULT_CONVERSATION_ARCHIVE_DIR
|
|
413
|
+
conversation_archive_queue_max_bytes: int = Field(default=256 * 1024 * 1024, gt=0)
|
|
414
|
+
# OpenAI Images API compatibility (POST /v1/images/{generations,edits}):
|
|
415
|
+
# the public default model (``gpt-image-2``) and the internal Responses
|
|
416
|
+
# host model are fixed constants (``app/core/openai/images.py`` /
|
|
417
|
+
# ``app/modules/proxy/api.py``). There is intentionally no ``images_max_n``
|
|
418
|
+
# setting: the upstream ``image_generation`` tool path accepts only a
|
|
419
|
+
# single image per call and codex-lb does not yet implement client-side
|
|
420
|
+
# fan-out, so ``n > 1`` is hard-rejected at the API boundary. The cap is
|
|
421
|
+
# lifted in the same change that introduces fan-out.
|
|
422
|
+
# Fallback Codex client version used when the live release lookup fails.
|
|
423
|
+
# Must stay >= the highest ``minimal_client_version`` in the bootstrap
|
|
424
|
+
# catalog (GPT-5.6 requires 0.144.0) or a degraded-startup refresh would
|
|
425
|
+
# receive an upstream catalog without those models.
|
|
426
|
+
model_registry_client_version: str = CODEX_VERSION
|
|
427
|
+
# Persisted registry snapshots older than this are ignored at load time
|
|
428
|
+
# (bootstrap catalog remains the floor until the next leader refresh).
|
|
429
|
+
model_registry_snapshot_max_age_seconds: int = Field(default=86400, gt=0)
|
|
430
|
+
# T3 → dashboard (deprecated env alias, remove next minor). Per-slug fallback:
|
|
431
|
+
# a ``model_context_window_overrides`` dashboard row wins for its slug; slugs
|
|
432
|
+
# without a row still read this dict.
|
|
433
|
+
model_context_window_overrides: Annotated[dict[str, int], NoDecode] = Field(default_factory=dict)
|
|
434
|
+
# T1 (topology). Raw socket-peer CIDRs allowed to call the proxy without an
|
|
435
|
+
# API key: a fact of this replica's network namespace (sidecar, pod CIDR),
|
|
436
|
+
# like the trusted-proxy CIDRs below.
|
|
437
|
+
proxy_unauthenticated_client_cidrs: Annotated[list[str], NoDecode] = Field(default_factory=list)
|
|
438
|
+
firewall_trust_proxy_headers: bool = False
|
|
439
|
+
firewall_trusted_proxy_cidrs: Annotated[list[str], NoDecode] = Field(
|
|
440
|
+
default_factory=lambda: ["127.0.0.1/32", "::1/128"]
|
|
441
|
+
)
|
|
442
|
+
firewall_ip_cache_ttl_seconds: int = Field(default=30, gt=0)
|
|
443
|
+
# T1 (topology). Uvicorn-compatible proxy-projection trust list consumed by
|
|
444
|
+
# ``TrustedProxyHeadersMiddleware``. Semantics are Uvicorn's, unchanged:
|
|
445
|
+
# unset trusts ``127.0.0.1``, empty trusts no peer, ``*`` trusts every
|
|
446
|
+
# peer, anything else is a comma-separated host/network list. The bare
|
|
447
|
+
# ``FORWARDED_ALLOW_IPS`` env name stays authoritative for compatibility
|
|
448
|
+
# with Uvicorn deployments; ``CODEX_LB_FORWARDED_ALLOW_IPS`` is the
|
|
449
|
+
# prefixed alias. This governs ``scope["client"]``/scheme projection only;
|
|
450
|
+
# ``firewall_trusted_proxy_cidrs`` governs firewall and auth client
|
|
451
|
+
# resolution from the raw socket peer.
|
|
452
|
+
forwarded_allow_ips: str | None = Field(
|
|
453
|
+
default=None,
|
|
454
|
+
validation_alias=AliasChoices("FORWARDED_ALLOW_IPS", "CODEX_LB_FORWARDED_ALLOW_IPS"),
|
|
455
|
+
)
|
|
456
|
+
dashboard_auth_mode: DashboardAuthMode = DashboardAuthMode.STANDARD
|
|
457
|
+
# T1 (topology). Last link of the ``dashboard_auth_mode`` trust chain:
|
|
458
|
+
# whether a loopback ``Host`` header may unlock a >30d session TTL depends
|
|
459
|
+
# on how this deployment's reverse proxy rewrites it (policy D2).
|
|
460
|
+
dashboard_trust_loopback_host_header_for_long_sessions: bool = False
|
|
461
|
+
|
|
462
|
+
def upstream_websocket_proxy_env(self) -> Mapping[str, str | None]:
|
|
463
|
+
return _effective_environ()
|
|
464
|
+
|
|
465
|
+
dashboard_auth_proxy_header: str = "Remote-User"
|
|
466
|
+
# T1 (topology). The header the reverse proxy puts the caller's groups in;
|
|
467
|
+
# it must match that proxy's configuration, so it cannot live in the
|
|
468
|
+
# dashboard (policy D2). Optional: an install whose proxy sends no such
|
|
469
|
+
# header simply has no groups and matches no group rule.
|
|
470
|
+
dashboard_auth_proxy_groups_header: str = "Remote-Groups"
|
|
471
|
+
|
|
472
|
+
# --- Multi-replica & production settings ---
|
|
473
|
+
# Prometheus metrics
|
|
474
|
+
metrics_enabled: bool = False
|
|
475
|
+
metrics_port: int = Field(default=9090, ge=1, le=65535)
|
|
476
|
+
|
|
477
|
+
# Logging
|
|
478
|
+
log_format: Literal["text", "json"] = "text"
|
|
479
|
+
|
|
480
|
+
# Leader election
|
|
481
|
+
leader_election_enabled: bool = True
|
|
482
|
+
leader_election_ttl_seconds: int = Field(default=60, ge=5)
|
|
483
|
+
|
|
484
|
+
# Circuit breaker (failure threshold and recovery timeout are fixed
|
|
485
|
+
# constants in ``app/core/resilience/circuit_breaker.py``).
|
|
486
|
+
# T3 → dashboard (deprecated env alias, remove next minor): the
|
|
487
|
+
# ``dashboard_settings.circuit_breaker_enabled`` column wins when set.
|
|
488
|
+
circuit_breaker_enabled: bool = False
|
|
489
|
+
|
|
490
|
+
# Soft drain & deterministic failover (drain/probe thresholds are fixed
|
|
491
|
+
# constants in ``app/core/balancer/logic.py``).
|
|
492
|
+
# T3 → dashboard (deprecated env alias, remove next minor): the same-name
|
|
493
|
+
# ``dashboard_settings`` columns win when set.
|
|
494
|
+
soft_drain_enabled: bool = True
|
|
495
|
+
deterministic_failover_enabled: bool = True
|
|
496
|
+
|
|
497
|
+
# Thread cache identity mode: "shared" (default) or "isolated".
|
|
498
|
+
# T3 -> dashboard: the same-name nullable ``dashboard_settings`` column
|
|
499
|
+
# wins when it is non-NULL; this field is the environment fallback only.
|
|
500
|
+
# ``shared`` is byte-for-byte the pre-existing outbound request.
|
|
501
|
+
thread_cache_identity_mode: str = THREAD_CACHE_IDENTITY_MODE_DEFAULT
|
|
502
|
+
|
|
503
|
+
# Backpressure
|
|
504
|
+
backpressure_max_concurrent_requests: int = 0 # 0 = unlimited
|
|
505
|
+
|
|
506
|
+
# Per-class proxy bulkhead limits (http/websocket/compact) always derive
|
|
507
|
+
# from this single limit; see ``BulkheadSemaphore`` for the derivation.
|
|
508
|
+
bulkhead_proxy_limit: int = Field(default=512, ge=0)
|
|
509
|
+
bulkhead_dashboard_limit: int = Field(default=50, ge=0)
|
|
510
|
+
dashboard_bootstrap_token: str | None = None
|
|
511
|
+
# T1 (topology). Address the dashboard tells operators to point clients at
|
|
512
|
+
# (``GET /api/settings/runtime/connect-address``). Unset derives it from
|
|
513
|
+
# the request host; a blank value collapses to unset.
|
|
514
|
+
connect_address: str | None = None
|
|
515
|
+
# T1 (topology). Capacity of a per-process asyncio.Semaphore, sized with
|
|
516
|
+
# the replica's resources like ``bulkhead_proxy_limit``.
|
|
517
|
+
proxy_response_create_limit: int = Field(default=256, ge=0)
|
|
518
|
+
proxy_account_response_create_limit: int = Field(default=4, ge=0)
|
|
519
|
+
proxy_account_stream_limit: int = Field(default=8, ge=0)
|
|
520
|
+
proxy_account_stream_recovery_reserve: int = Field(default=1, ge=0)
|
|
521
|
+
# Pool-congestion utilization percentage at which per-API-key stream
|
|
522
|
+
# fair-share throttling engages; 0 disables the gate entirely.
|
|
523
|
+
proxy_api_key_fair_share_congestion_threshold_pct: int = Field(default=0, ge=0, le=100)
|
|
524
|
+
# C2-2 routing/overload: the five fields below have a same-name
|
|
525
|
+
# ``dashboard_settings`` column; the environment is only the fallback the
|
|
526
|
+
# dashboard inherits while its column is NULL (``RoutingTunables``).
|
|
527
|
+
# T3 → dashboard (deprecated env alias, remove next minor)
|
|
528
|
+
proxy_account_inflight_penalty_pct: float = Field(default=2.5, ge=0)
|
|
529
|
+
# Upstream overload (``server_is_overloaded``) handling. Soft backoff and
|
|
530
|
+
# the isolation trip level are fixed constants in
|
|
531
|
+
# ``app/modules/proxy/_load_balancer/overload_backoff.py``; this is how long
|
|
532
|
+
# a sustained-overload account is isolated (fresh selection avoids it and
|
|
533
|
+
# soft sticky owners are rerouted while another candidate exists). ``0``
|
|
534
|
+
# disables the isolation stage and keeps the soft backoff only.
|
|
535
|
+
# T3 → dashboard (deprecated env alias, remove next minor)
|
|
536
|
+
proxy_overload_isolation_seconds: int = Field(default=1800, ge=0)
|
|
537
|
+
# Weighted routing strategies (``capacity_weighted``, ``relative_availability``)
|
|
538
|
+
# discount each candidate's draw weight by its recent upstream error rate
|
|
539
|
+
# (window, sample floor and weight floor are fixed constants in
|
|
540
|
+
# ``app/modules/proxy/_load_balancer/error_rate.py``).
|
|
541
|
+
# T3 → dashboard (deprecated env alias, remove next minor)
|
|
542
|
+
proxy_account_error_rate_weighting_enabled: bool = True
|
|
543
|
+
# T3 → dashboard (deprecated env alias, remove next minor)
|
|
544
|
+
proxy_account_lease_token_weight: float = Field(default=1.0, ge=0)
|
|
545
|
+
# T3 → dashboard (deprecated env alias, remove next minor)
|
|
546
|
+
proxy_account_lease_ttl_seconds: float = Field(default=900.0, gt=0)
|
|
547
|
+
proxy_account_caps_scope: Literal["partitioned", "replica"] = "partitioned"
|
|
548
|
+
proxy_account_cap_partition_scale_down_seconds: int = Field(default=60, ge=30)
|
|
549
|
+
timeout_invariant_validation_strict: bool = False
|
|
550
|
+
|
|
551
|
+
# Local memory-pressure guard (0 = disabled). Requests are rejected with
|
|
552
|
+
# 503 once RSS reaches the threshold; a warning is logged from 80% of it
|
|
553
|
+
# (``app/core/resilience/memory_monitor.py`` derives the warning level).
|
|
554
|
+
memory_reject_threshold_mb: int = 0
|
|
555
|
+
|
|
556
|
+
# Event-loop lag watchdog (0 = disabled). Samples asyncio.sleep drift once
|
|
557
|
+
# per second; lag at or above the threshold emits a rate-limited warning
|
|
558
|
+
# and Prometheus signals (``app/core/resilience/loop_lag_monitor.py``).
|
|
559
|
+
# Default 0.5s: an order of magnitude above healthy scheduling jitter,
|
|
560
|
+
# well below the lag that fails 10s-budget health checks.
|
|
561
|
+
event_loop_lag_warn_threshold_seconds: float = Field(default=0.5, ge=0.0)
|
|
562
|
+
|
|
563
|
+
# OpenTelemetry
|
|
564
|
+
otel_enabled: bool = False
|
|
565
|
+
otel_exporter_endpoint: str = ""
|
|
566
|
+
|
|
567
|
+
# Shutdown drain
|
|
568
|
+
shutdown_drain_timeout_seconds: int = Field(default=30, gt=0, le=300)
|
|
569
|
+
|
|
570
|
+
# HTTP connector limits
|
|
571
|
+
http_connector_limit: int = 100
|
|
572
|
+
http_connector_limit_per_host: int = 50
|
|
573
|
+
|
|
574
|
+
@field_validator("data_dir", mode="before")
|
|
575
|
+
@classmethod
|
|
576
|
+
def _expand_data_dir(cls, value: str | Path) -> Path:
|
|
577
|
+
if isinstance(value, Path):
|
|
578
|
+
return value.expanduser()
|
|
579
|
+
if isinstance(value, str):
|
|
580
|
+
stripped = value.strip()
|
|
581
|
+
if not stripped:
|
|
582
|
+
return _default_home_dir()
|
|
583
|
+
return Path(stripped).expanduser()
|
|
584
|
+
raise TypeError("data_dir must be a path")
|
|
585
|
+
|
|
586
|
+
@field_validator("thread_cache_identity_mode", mode="before")
|
|
587
|
+
@classmethod
|
|
588
|
+
def _normalize_thread_cache_identity_mode(cls, value: object) -> str:
|
|
589
|
+
"""Coerce an unrecognised value to ``shared`` instead of letting it escape.
|
|
590
|
+
|
|
591
|
+
The settings API constrains this field to ``shared``/``isolated``, so a
|
|
592
|
+
typo in the environment variable would otherwise reach the response
|
|
593
|
+
model and fail ``GET /api/settings`` for every setting at once. A
|
|
594
|
+
behaviour toggle is also the wrong thing to make a boot failure, so the
|
|
595
|
+
unrecognised value degrades to the safe default and says so.
|
|
596
|
+
"""
|
|
597
|
+
if value is None:
|
|
598
|
+
return THREAD_CACHE_IDENTITY_MODE_DEFAULT
|
|
599
|
+
normalized = normalize_thread_cache_identity_mode(value)
|
|
600
|
+
if normalized is not None:
|
|
601
|
+
return normalized
|
|
602
|
+
logger.warning(
|
|
603
|
+
"CODEX_LB_THREAD_CACHE_IDENTITY_MODE=%r is not a known mode; using %r",
|
|
604
|
+
value,
|
|
605
|
+
THREAD_CACHE_IDENTITY_MODE_DEFAULT,
|
|
606
|
+
)
|
|
607
|
+
return THREAD_CACHE_IDENTITY_MODE_DEFAULT
|
|
608
|
+
|
|
609
|
+
@field_validator("database_url")
|
|
610
|
+
@classmethod
|
|
611
|
+
def _expand_database_url(cls, value: str) -> str:
|
|
612
|
+
for prefix in ("sqlite+aiosqlite:///", "sqlite:///"):
|
|
613
|
+
if value.startswith(prefix):
|
|
614
|
+
path = value[len(prefix) :]
|
|
615
|
+
if path.startswith("~"):
|
|
616
|
+
return f"{prefix}{Path(path).expanduser()}"
|
|
617
|
+
return value
|
|
618
|
+
|
|
619
|
+
@field_validator("encryption_key_file", mode="before")
|
|
620
|
+
@classmethod
|
|
621
|
+
def _expand_encryption_key_file(cls, value: str | Path) -> Path:
|
|
622
|
+
if isinstance(value, Path):
|
|
623
|
+
return value.expanduser()
|
|
624
|
+
if isinstance(value, str):
|
|
625
|
+
return Path(value).expanduser()
|
|
626
|
+
raise TypeError("encryption_key_file must be a path")
|
|
627
|
+
|
|
628
|
+
@field_validator("conversation_archive_dir", mode="before")
|
|
629
|
+
@classmethod
|
|
630
|
+
def _expand_conversation_archive_dir(cls, value: str | Path) -> Path:
|
|
631
|
+
if isinstance(value, Path):
|
|
632
|
+
return value.expanduser()
|
|
633
|
+
if isinstance(value, str):
|
|
634
|
+
return Path(value).expanduser()
|
|
635
|
+
raise TypeError("conversation_archive_dir must be a path")
|
|
636
|
+
|
|
637
|
+
@field_validator("connect_address", "additional_quota_registry_file", mode="before")
|
|
638
|
+
@classmethod
|
|
639
|
+
def _blank_optional_to_none(cls, value: object) -> object:
|
|
640
|
+
if isinstance(value, str):
|
|
641
|
+
stripped = value.strip()
|
|
642
|
+
return stripped or None
|
|
643
|
+
return value
|
|
644
|
+
|
|
645
|
+
@field_validator("firewall_trusted_proxy_cidrs", mode="before")
|
|
646
|
+
@classmethod
|
|
647
|
+
def _normalize_firewall_trusted_proxy_cidrs(cls, value: StringListInput) -> list[str]:
|
|
648
|
+
return _normalize_cidr_list(
|
|
649
|
+
value,
|
|
650
|
+
field_name="firewall_trusted_proxy_cidrs",
|
|
651
|
+
invalid_label="firewall trusted proxy CIDR",
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
@field_validator("proxy_unauthenticated_client_cidrs", mode="before")
|
|
655
|
+
@classmethod
|
|
656
|
+
def _normalize_proxy_unauthenticated_client_cidrs(cls, value: StringListInput) -> list[str]:
|
|
657
|
+
return _normalize_cidr_list(
|
|
658
|
+
value,
|
|
659
|
+
field_name="proxy_unauthenticated_client_cidrs",
|
|
660
|
+
invalid_label="proxy unauthenticated client CIDR",
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
@field_validator("dashboard_auth_proxy_header", mode="before")
|
|
664
|
+
@classmethod
|
|
665
|
+
def _normalize_dashboard_auth_proxy_header(cls, value: object) -> str:
|
|
666
|
+
if not isinstance(value, str):
|
|
667
|
+
raise TypeError("dashboard_auth_proxy_header must be a string")
|
|
668
|
+
return normalize_dashboard_auth_proxy_header(value)
|
|
669
|
+
|
|
670
|
+
@field_validator("dashboard_auth_proxy_groups_header", mode="before")
|
|
671
|
+
@classmethod
|
|
672
|
+
def _normalize_dashboard_auth_proxy_groups_header(cls, value: object) -> str:
|
|
673
|
+
if not isinstance(value, str):
|
|
674
|
+
raise TypeError("dashboard_auth_proxy_groups_header must be a string")
|
|
675
|
+
return normalize_dashboard_auth_proxy_header(value, "dashboard_auth_proxy_groups_header")
|
|
676
|
+
|
|
677
|
+
@field_validator("http_responses_session_bridge_instance_ring", mode="before")
|
|
678
|
+
@classmethod
|
|
679
|
+
def _normalize_http_bridge_instance_ring(cls, value: StringListInput) -> list[str]:
|
|
680
|
+
if value is None:
|
|
681
|
+
return []
|
|
682
|
+
if isinstance(value, str):
|
|
683
|
+
entries = [entry.strip() for entry in value.split(",")]
|
|
684
|
+
return [entry for entry in entries if entry]
|
|
685
|
+
if isinstance(value, list):
|
|
686
|
+
normalized: list[str] = []
|
|
687
|
+
for entry in value:
|
|
688
|
+
if isinstance(entry, str):
|
|
689
|
+
instance_id = entry.strip()
|
|
690
|
+
if instance_id:
|
|
691
|
+
normalized.append(instance_id)
|
|
692
|
+
return normalized
|
|
693
|
+
raise TypeError("http_responses_session_bridge_instance_ring must be a list or comma-separated string")
|
|
694
|
+
|
|
695
|
+
@field_validator("http_responses_session_bridge_advertise_base_url", mode="before")
|
|
696
|
+
@classmethod
|
|
697
|
+
def _normalize_http_bridge_advertise_base_url(cls, value: OptionalStringInput) -> str | None:
|
|
698
|
+
if value is None:
|
|
699
|
+
return None
|
|
700
|
+
if isinstance(value, str):
|
|
701
|
+
stripped = value.strip().rstrip("/")
|
|
702
|
+
return stripped or None
|
|
703
|
+
raise TypeError("http_responses_session_bridge_advertise_base_url must be a string")
|
|
704
|
+
|
|
705
|
+
@field_validator("model_context_window_overrides", mode="before")
|
|
706
|
+
@classmethod
|
|
707
|
+
def _parse_model_context_window_overrides(cls, value: ModelContextWindowOverridesInput) -> dict[str, int]:
|
|
708
|
+
if value is None:
|
|
709
|
+
return {}
|
|
710
|
+
if isinstance(value, str):
|
|
711
|
+
value = value.strip()
|
|
712
|
+
if not value:
|
|
713
|
+
return {}
|
|
714
|
+
parsed = json.loads(value)
|
|
715
|
+
if not isinstance(parsed, dict):
|
|
716
|
+
raise TypeError("model_context_window_overrides must be a JSON object")
|
|
717
|
+
return _validate_context_window_entries(parsed)
|
|
718
|
+
if isinstance(value, dict):
|
|
719
|
+
return _validate_context_window_entries(value)
|
|
720
|
+
raise TypeError("model_context_window_overrides must be a JSON object string or dict")
|
|
721
|
+
|
|
722
|
+
@model_validator(mode="after")
|
|
723
|
+
def _apply_data_dir_defaults(self) -> "Settings":
|
|
724
|
+
if self.data_dir == DEFAULT_HOME_DIR:
|
|
725
|
+
return self
|
|
726
|
+
explicitly_set = self.model_fields_set
|
|
727
|
+
if "database_url" not in explicitly_set and self.database_url == DEFAULT_DATABASE_URL:
|
|
728
|
+
self.database_url = f"sqlite+aiosqlite:///{self.data_dir / 'store.db'}"
|
|
729
|
+
if "encryption_key_file" not in explicitly_set and self.encryption_key_file == DEFAULT_ENCRYPTION_KEY_FILE:
|
|
730
|
+
self.encryption_key_file = self.data_dir / "encryption.key"
|
|
731
|
+
if (
|
|
732
|
+
"conversation_archive_dir" not in explicitly_set
|
|
733
|
+
and self.conversation_archive_dir == DEFAULT_CONVERSATION_ARCHIVE_DIR
|
|
734
|
+
):
|
|
735
|
+
self.conversation_archive_dir = self.data_dir / "conversation-archive"
|
|
736
|
+
return self
|
|
737
|
+
|
|
738
|
+
@model_validator(mode="after")
|
|
739
|
+
def _validate_http_bridge_instance_configuration(self) -> "Settings":
|
|
740
|
+
ring = self.http_responses_session_bridge_instance_ring
|
|
741
|
+
if ring and self.http_responses_session_bridge_instance_id not in ring:
|
|
742
|
+
raise ValueError(
|
|
743
|
+
"http_responses_session_bridge_instance_id must be explicitly present in "
|
|
744
|
+
"http_responses_session_bridge_instance_ring"
|
|
745
|
+
)
|
|
746
|
+
advertise_base_url = self.http_responses_session_bridge_advertise_base_url
|
|
747
|
+
if advertise_base_url is not None:
|
|
748
|
+
hostname = urlparse(advertise_base_url).hostname
|
|
749
|
+
if hostname is None:
|
|
750
|
+
raise ValueError("http_responses_session_bridge_advertise_base_url must include a valid hostname")
|
|
751
|
+
if not _bridge_advertise_hostname_is_replica_specific(
|
|
752
|
+
hostname,
|
|
753
|
+
instance_id=self.http_responses_session_bridge_instance_id,
|
|
754
|
+
multi_replica_intent=len(ring) > 1,
|
|
755
|
+
):
|
|
756
|
+
raise ValueError(
|
|
757
|
+
"http_responses_session_bridge_advertise_base_url must be replica-specific for bridge routing"
|
|
758
|
+
)
|
|
759
|
+
return self
|
|
760
|
+
|
|
761
|
+
@cached_property
|
|
762
|
+
def trace_channels(self) -> frozenset[str]:
|
|
763
|
+
"""Parsed ``trace`` channels; empty set (the default) disables all."""
|
|
764
|
+
return frozenset(entry.strip().lower() for entry in self.trace.split(",") if entry.strip())
|
|
765
|
+
|
|
766
|
+
@model_validator(mode="after")
|
|
767
|
+
def _validate_metrics_port(self) -> "Settings":
|
|
768
|
+
http_port = _configured_http_port()
|
|
769
|
+
if self.metrics_port == http_port:
|
|
770
|
+
raise ValueError(f"metrics_port must not match the main application port ({http_port})")
|
|
771
|
+
return self
|
|
772
|
+
|
|
773
|
+
@model_validator(mode="after")
|
|
774
|
+
def _validate_firewall_proxy_trust(self) -> "Settings":
|
|
775
|
+
if self.firewall_trust_proxy_headers and not self.firewall_trusted_proxy_cidrs:
|
|
776
|
+
raise ValueError(
|
|
777
|
+
"firewall_trust_proxy_headers=true requires at least one entry in "
|
|
778
|
+
"firewall_trusted_proxy_cidrs; configure a trusted proxy CIDR or disable proxy header trust"
|
|
779
|
+
)
|
|
780
|
+
return self
|
|
781
|
+
|
|
782
|
+
@model_validator(mode="after")
|
|
783
|
+
def _validate_dashboard_auth_mode(self) -> "Settings":
|
|
784
|
+
if self.dashboard_auth_mode != DashboardAuthMode.TRUSTED_HEADER:
|
|
785
|
+
return self
|
|
786
|
+
if not self.firewall_trust_proxy_headers:
|
|
787
|
+
raise ValueError("dashboard_auth_mode=trusted_header requires firewall_trust_proxy_headers=true")
|
|
788
|
+
return self
|
|
789
|
+
|
|
790
|
+
@model_validator(mode="after")
|
|
791
|
+
def _validate_dashboard_auth_proxy_headers_differ(self) -> "Settings":
|
|
792
|
+
# One header cannot be both the identity and the group claim: that
|
|
793
|
+
# would turn the username into a group and hand out roles by name.
|
|
794
|
+
if self.dashboard_auth_proxy_groups_header.lower() == self.dashboard_auth_proxy_header.lower():
|
|
795
|
+
raise ValueError(
|
|
796
|
+
"dashboard_auth_proxy_groups_header must not equal dashboard_auth_proxy_header "
|
|
797
|
+
f"('{self.dashboard_auth_proxy_header}')"
|
|
798
|
+
)
|
|
799
|
+
return self
|
|
800
|
+
|
|
801
|
+
@model_validator(mode="after")
|
|
802
|
+
def _reject_multiple_workers_per_instance(self) -> "Settings":
|
|
803
|
+
# Only one worker process per instance is supported. Per-account
|
|
804
|
+
# concurrency caps are partitioned per REPLICA via the bridge ring, which
|
|
805
|
+
# is correct only when a single process runs behind each ring instance
|
|
806
|
+
# id. Running multiple worker processes per instance cannot be made
|
|
807
|
+
# reliable for shared caps: there is no portable per-worker index, and a
|
|
808
|
+
# standard uvicorn/gunicorn multi-worker launch inherits the SAME
|
|
809
|
+
# environment into every child, so the workers cannot self-partition.
|
|
810
|
+
# ``CODEX_LB_WORKERS_PER_INSTANCE`` is therefore not a setting (the only
|
|
811
|
+
# accepted value is the default, 1) but a startup guard: an explicit
|
|
812
|
+
# declaration of anything else fails fast rather than silently
|
|
813
|
+
# over-admitting.
|
|
814
|
+
environ = _effective_environ()
|
|
815
|
+
key = _env_key(environ, _WORKERS_PER_INSTANCE_ENV)
|
|
816
|
+
raw = environ.get(key) if key is not None else None
|
|
817
|
+
if raw is None or not raw.strip():
|
|
818
|
+
return self
|
|
819
|
+
try:
|
|
820
|
+
declared = int(raw.strip())
|
|
821
|
+
except ValueError:
|
|
822
|
+
declared = 0
|
|
823
|
+
if declared < 1:
|
|
824
|
+
raise ValueError(
|
|
825
|
+
f"{_WORKERS_PER_INSTANCE_ENV}={raw.strip()!r} is not a positive integer; "
|
|
826
|
+
f"only {_WORKERS_PER_INSTANCE_ENV}=1 (the default) is supported."
|
|
827
|
+
)
|
|
828
|
+
if declared > 1:
|
|
829
|
+
raise ValueError(
|
|
830
|
+
f"workers_per_instance ({_WORKERS_PER_INSTANCE_ENV}="
|
|
831
|
+
f"{declared}) is not supported: running more than one worker "
|
|
832
|
+
"process per instance would multiply per-account concurrency caps, because those "
|
|
833
|
+
"caps are partitioned per replica via the bridge ring and intra-pod worker "
|
|
834
|
+
"partitioning cannot be made reliable. Run ONE worker per pod/container and scale "
|
|
835
|
+
"horizontally via replicas (the bridge ring partitions caps per replica); set "
|
|
836
|
+
f"{_WORKERS_PER_INSTANCE_ENV}=1 (the default)."
|
|
837
|
+
)
|
|
838
|
+
return self
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
@lru_cache(maxsize=1)
|
|
842
|
+
def get_settings() -> Settings:
|
|
843
|
+
return Settings()
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
def _bridge_advertise_hostname_is_replica_specific(
|
|
847
|
+
hostname: str,
|
|
848
|
+
*,
|
|
849
|
+
instance_id: str,
|
|
850
|
+
multi_replica_intent: bool = False,
|
|
851
|
+
) -> bool:
|
|
852
|
+
pod_ip = os.getenv("POD_IP")
|
|
853
|
+
if pod_ip and hostname == pod_ip:
|
|
854
|
+
return True
|
|
855
|
+
try:
|
|
856
|
+
parsed_ip = ip_address(hostname)
|
|
857
|
+
except ValueError:
|
|
858
|
+
labels = set(hostname.split("."))
|
|
859
|
+
pod_name = os.getenv("POD_NAME", "").strip()
|
|
860
|
+
host_name = os.getenv("HOSTNAME", "").strip()
|
|
861
|
+
allowed_labels = {
|
|
862
|
+
label
|
|
863
|
+
for label in {
|
|
864
|
+
instance_id.strip(),
|
|
865
|
+
pod_name,
|
|
866
|
+
host_name,
|
|
867
|
+
socket.gethostname().strip(),
|
|
868
|
+
}
|
|
869
|
+
if label
|
|
870
|
+
}
|
|
871
|
+
return bool(labels & allowed_labels)
|
|
872
|
+
return parsed_ip.is_loopback and not multi_replica_intent
|