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,1677 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ipaddress
|
|
4
|
+
import socket
|
|
5
|
+
import time
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from typing import Any, cast
|
|
8
|
+
|
|
9
|
+
import aiohttp
|
|
10
|
+
import httpx
|
|
11
|
+
from aiohttp_socks import ProxyConnector
|
|
12
|
+
from fastapi import APIRouter, Body, Depends, Request
|
|
13
|
+
from python_socks import ProxyType
|
|
14
|
+
from sqlalchemy import select
|
|
15
|
+
from sqlalchemy.exc import IntegrityError
|
|
16
|
+
|
|
17
|
+
from app.core.audit.service import AuditActor, AuditService, AuditSeverity, AuditTarget
|
|
18
|
+
from app.core.auth.dashboard_access import DashboardPrincipal, DashboardRole, Permission
|
|
19
|
+
from app.core.auth.dependencies import (
|
|
20
|
+
ensure_dashboard_permission,
|
|
21
|
+
ensure_step_up,
|
|
22
|
+
require_dashboard_permission,
|
|
23
|
+
require_dashboard_write_access,
|
|
24
|
+
set_dashboard_error_format,
|
|
25
|
+
validate_dashboard_session,
|
|
26
|
+
)
|
|
27
|
+
from app.core.clients.http import _shared_ssl_context
|
|
28
|
+
from app.core.config import settings as settings_module
|
|
29
|
+
from app.core.config.background_jobs import BACKGROUND_JOB_SETTINGS
|
|
30
|
+
from app.core.config.context_window_overrides import (
|
|
31
|
+
get_model_context_window_overrides_cache,
|
|
32
|
+
resolve_context_window_overrides,
|
|
33
|
+
)
|
|
34
|
+
from app.core.config.dashboard_overrides import DASHBOARD_TIMEOUT_SETTINGS
|
|
35
|
+
from app.core.config.inheritable import resolve_inheritable
|
|
36
|
+
from app.core.config.settings import get_settings as get_app_settings
|
|
37
|
+
from app.core.config.settings_cache import get_settings_cache
|
|
38
|
+
from app.core.config.spool_retention import (
|
|
39
|
+
OPERATION_SPOOL_RETENTION_SETTING,
|
|
40
|
+
SPOOL_RETENTION_FLOOR_INPUTS,
|
|
41
|
+
binding_spool_retention_floor_term,
|
|
42
|
+
operation_spool_retention_floor_seconds,
|
|
43
|
+
)
|
|
44
|
+
from app.core.conversation_archive import CONVERSATION_ARCHIVE_SETTING, CONVERSATION_ARCHIVE_TOGGLED_ACTION
|
|
45
|
+
from app.core.crypto import TokenEncryptor
|
|
46
|
+
from app.core.exceptions import (
|
|
47
|
+
DashboardBadRequestError,
|
|
48
|
+
DashboardConflictError,
|
|
49
|
+
DashboardNotFoundError,
|
|
50
|
+
DashboardSettingsConflictError,
|
|
51
|
+
)
|
|
52
|
+
from app.core.resilience.toggles import RESILIENCE_TOGGLE_SETTINGS
|
|
53
|
+
from app.core.timeout_invariants import find_timeout_invariant_violations
|
|
54
|
+
from app.core.upstream_proxy import UpstreamProxyRouteError, resolve_proxy_endpoint, sends_plaintext_credentials
|
|
55
|
+
from app.core.upstream_proxy.cache import get_upstream_route_cache
|
|
56
|
+
from app.db.models import Account, AccountProxyBinding, AccountStatus, ProxyEndpoint, ProxyPool, ProxyPoolMember
|
|
57
|
+
from app.dependencies import SettingsContext, get_proxy_service_for_app, get_settings_context
|
|
58
|
+
from app.modules.dashboard_users.break_glass import BreakGlassRequiresTotpError
|
|
59
|
+
from app.modules.proxy.account_cache import (
|
|
60
|
+
clear_account_routing_unavailable,
|
|
61
|
+
get_account_selection_cache,
|
|
62
|
+
propagate_account_routing_change,
|
|
63
|
+
)
|
|
64
|
+
from app.modules.settings.repository import ModelContextWindowOverridesRepository
|
|
65
|
+
from app.modules.settings.schemas import (
|
|
66
|
+
SECURITY_SETTINGS_FIELDS,
|
|
67
|
+
AccountProxyBindingRequest,
|
|
68
|
+
AccountProxyBindingResponse,
|
|
69
|
+
AdditionalQuotaPolicy,
|
|
70
|
+
DashboardSettingsResponse,
|
|
71
|
+
DashboardSettingsUpdateRequest,
|
|
72
|
+
LocalLoginPolicyLiteral,
|
|
73
|
+
ModelContextWindowOverrideResponse,
|
|
74
|
+
ModelContextWindowOverridesResponse,
|
|
75
|
+
ModelContextWindowOverrideUpsertRequest,
|
|
76
|
+
RuntimeConnectAddressResponse,
|
|
77
|
+
SettingProvenance,
|
|
78
|
+
UpstreamProxyAdminResponse,
|
|
79
|
+
UpstreamProxyEndpointCreateRequest,
|
|
80
|
+
UpstreamProxyEndpointResponse,
|
|
81
|
+
UpstreamProxyEndpointTestResponse,
|
|
82
|
+
UpstreamProxyPoolCreateRequest,
|
|
83
|
+
UpstreamProxyPoolMemberRequest,
|
|
84
|
+
UpstreamProxyPoolResponse,
|
|
85
|
+
)
|
|
86
|
+
from app.modules.settings.service import DashboardSettingsUpdateData
|
|
87
|
+
from app.modules.usage.additional_quota_keys import (
|
|
88
|
+
get_additional_quota_routing_policy,
|
|
89
|
+
list_additional_quota_definitions,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
LOOPBACK_HOSTS = {"localhost", "127.0.0.1", "::1", "[::1]"}
|
|
93
|
+
UPSTREAM_PROXY_TEST_URL = "https://chatgpt.com/cdn-cgi/trace"
|
|
94
|
+
UPSTREAM_PROXY_TEST_TIMEOUT_SECONDS = 8.0
|
|
95
|
+
HTTP_PROXY_AUTHENTICATION_REQUIRED = 407
|
|
96
|
+
IMPORT_PROXY_REQUIRED_PAUSE_REASON = "upstream_proxy_required_on_import"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _is_non_loopback_ipv4(value: str | None) -> bool:
|
|
100
|
+
if not value:
|
|
101
|
+
return False
|
|
102
|
+
try:
|
|
103
|
+
address = ipaddress.ip_address(value)
|
|
104
|
+
except ValueError:
|
|
105
|
+
return False
|
|
106
|
+
return isinstance(address, ipaddress.IPv4Address) and not address.is_loopback and not address.is_unspecified
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _resolve_hostname_ipv4(hostname: str) -> str | None:
|
|
110
|
+
try:
|
|
111
|
+
infos = socket.getaddrinfo(hostname, None, family=socket.AF_INET, type=socket.SOCK_STREAM)
|
|
112
|
+
except OSError:
|
|
113
|
+
return None
|
|
114
|
+
for info in infos:
|
|
115
|
+
candidate = info[4][0]
|
|
116
|
+
if not isinstance(candidate, str):
|
|
117
|
+
continue
|
|
118
|
+
if _is_non_loopback_ipv4(candidate):
|
|
119
|
+
return candidate
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _resolve_runtime_connect_address(request: Request) -> str:
|
|
124
|
+
override = get_app_settings().connect_address
|
|
125
|
+
if override:
|
|
126
|
+
return override
|
|
127
|
+
|
|
128
|
+
request_host = request.url.hostname or ""
|
|
129
|
+
if _is_non_loopback_ipv4(request_host):
|
|
130
|
+
return request_host
|
|
131
|
+
|
|
132
|
+
normalized_host = request_host.strip().lower()
|
|
133
|
+
if normalized_host and normalized_host not in LOOPBACK_HOSTS:
|
|
134
|
+
resolved_host = _resolve_hostname_ipv4(request_host)
|
|
135
|
+
if resolved_host:
|
|
136
|
+
return resolved_host
|
|
137
|
+
return request_host
|
|
138
|
+
return "<codex-lb-ip-or-dns>"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
router = APIRouter(
|
|
142
|
+
prefix="/api/settings",
|
|
143
|
+
tags=["dashboard"],
|
|
144
|
+
dependencies=[Depends(validate_dashboard_session), Depends(set_dashboard_error_format)],
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# C2-2 routing/overload
|
|
149
|
+
_ROUTING_OVERLOAD_SETTINGS: tuple[str, ...] = (
|
|
150
|
+
"proxy_overload_isolation_seconds",
|
|
151
|
+
"proxy_account_error_rate_weighting_enabled",
|
|
152
|
+
"proxy_account_inflight_penalty_pct",
|
|
153
|
+
"proxy_account_lease_token_weight",
|
|
154
|
+
"proxy_account_lease_ttl_seconds",
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _dashboard_value(payload: DashboardSettingsUpdateRequest, name: str) -> Any:
|
|
159
|
+
"""Tri-state read: the value when the field was sent (``None`` for an explicit
|
|
160
|
+
null), ``None`` when it was omitted (unchanged)."""
|
|
161
|
+
return getattr(payload, name) if name in payload.model_fields_set else None
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _clears_dashboard_value(payload: DashboardSettingsUpdateRequest, name: str) -> bool:
|
|
165
|
+
"""True only for an explicit ``null``: the setting returns to inheriting the
|
|
166
|
+
environment value or code default."""
|
|
167
|
+
return name in payload.model_fields_set and getattr(payload, name) is None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# end C2-2 routing/overload
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _dashboard_settings_response(settings, *, principal: DashboardPrincipal) -> DashboardSettingsResponse:
|
|
174
|
+
environment_settings = get_app_settings()
|
|
175
|
+
# M5 conversation archive: the T1 directory is this replica's local shard;
|
|
176
|
+
# a filesystem path is admin-only information. ``getattr``: startup-settings
|
|
177
|
+
# fakes in tests may carry only the fields they exercise.
|
|
178
|
+
archive_dir = getattr(environment_settings, "conversation_archive_dir", None)
|
|
179
|
+
conversation_archive_dir = (
|
|
180
|
+
str(archive_dir) if archive_dir is not None and principal.role == DashboardRole.ADMIN else None
|
|
181
|
+
)
|
|
182
|
+
additional_quota_policies = [
|
|
183
|
+
AdditionalQuotaPolicy(
|
|
184
|
+
quota_key=definition.quota_key,
|
|
185
|
+
display_label=definition.display_label,
|
|
186
|
+
routing_policy=get_additional_quota_routing_policy(
|
|
187
|
+
definition.quota_key,
|
|
188
|
+
overrides=settings.additional_quota_routing_policies,
|
|
189
|
+
),
|
|
190
|
+
model_ids=sorted(definition.model_ids),
|
|
191
|
+
)
|
|
192
|
+
for definition in list_additional_quota_definitions()
|
|
193
|
+
]
|
|
194
|
+
return DashboardSettingsResponse(
|
|
195
|
+
sticky_threads_enabled=settings.sticky_threads_enabled,
|
|
196
|
+
upstream_stream_transport=settings.upstream_stream_transport,
|
|
197
|
+
prohibit_fast_mode=settings.prohibit_fast_mode,
|
|
198
|
+
http_downstream_transport_policy=settings.http_downstream_transport_policy,
|
|
199
|
+
thread_cache_identity_mode=settings.thread_cache_identity_mode,
|
|
200
|
+
thread_cache_identity_mode_override=settings.thread_cache_identity_mode_override,
|
|
201
|
+
proxy_account_response_create_limit=settings.proxy_account_response_create_limit,
|
|
202
|
+
proxy_account_response_create_limit_environment_value=(
|
|
203
|
+
getattr(
|
|
204
|
+
environment_settings,
|
|
205
|
+
"proxy_account_response_create_limit",
|
|
206
|
+
settings.proxy_account_response_create_limit,
|
|
207
|
+
)
|
|
208
|
+
),
|
|
209
|
+
proxy_account_response_create_limit_override=(settings.proxy_account_response_create_limit_override),
|
|
210
|
+
proxy_account_stream_limit=settings.proxy_account_stream_limit,
|
|
211
|
+
proxy_account_stream_limit_environment_value=getattr(
|
|
212
|
+
environment_settings,
|
|
213
|
+
"proxy_account_stream_limit",
|
|
214
|
+
settings.proxy_account_stream_limit,
|
|
215
|
+
),
|
|
216
|
+
proxy_account_stream_limit_override=settings.proxy_account_stream_limit_override,
|
|
217
|
+
proxy_account_stream_recovery_reserve=settings.proxy_account_stream_recovery_reserve,
|
|
218
|
+
proxy_account_stream_recovery_reserve_environment_value=(
|
|
219
|
+
getattr(
|
|
220
|
+
environment_settings,
|
|
221
|
+
"proxy_account_stream_recovery_reserve",
|
|
222
|
+
settings.proxy_account_stream_recovery_reserve,
|
|
223
|
+
)
|
|
224
|
+
),
|
|
225
|
+
proxy_account_stream_recovery_reserve_override=(settings.proxy_account_stream_recovery_reserve_override),
|
|
226
|
+
proxy_api_key_fair_share_congestion_threshold_pct=(settings.proxy_api_key_fair_share_congestion_threshold_pct),
|
|
227
|
+
proxy_api_key_fair_share_congestion_threshold_pct_environment_value=(
|
|
228
|
+
getattr(
|
|
229
|
+
environment_settings,
|
|
230
|
+
"proxy_api_key_fair_share_congestion_threshold_pct",
|
|
231
|
+
settings.proxy_api_key_fair_share_congestion_threshold_pct,
|
|
232
|
+
)
|
|
233
|
+
),
|
|
234
|
+
proxy_api_key_fair_share_congestion_threshold_pct_override=(
|
|
235
|
+
settings.proxy_api_key_fair_share_congestion_threshold_pct_override
|
|
236
|
+
),
|
|
237
|
+
# C2-2 routing/overload
|
|
238
|
+
proxy_overload_isolation_seconds=settings.proxy_overload_isolation_seconds,
|
|
239
|
+
proxy_account_error_rate_weighting_enabled=settings.proxy_account_error_rate_weighting_enabled,
|
|
240
|
+
proxy_account_inflight_penalty_pct=settings.proxy_account_inflight_penalty_pct,
|
|
241
|
+
proxy_account_lease_token_weight=settings.proxy_account_lease_token_weight,
|
|
242
|
+
proxy_account_lease_ttl_seconds=settings.proxy_account_lease_ttl_seconds,
|
|
243
|
+
# end C2-2 routing/overload
|
|
244
|
+
upstream_proxy_routing_enabled=settings.upstream_proxy_routing_enabled,
|
|
245
|
+
upstream_proxy_default_pool_id=settings.upstream_proxy_default_pool_id,
|
|
246
|
+
prefer_earlier_reset_accounts=settings.prefer_earlier_reset_accounts,
|
|
247
|
+
prefer_earlier_reset_window=settings.prefer_earlier_reset_window,
|
|
248
|
+
show_reset_credit_badges=settings.show_reset_credit_badges,
|
|
249
|
+
auto_redeem_reset_credits_before_expiry=settings.auto_redeem_reset_credits_before_expiry,
|
|
250
|
+
show_reset_credit_expiry_badge=settings.show_reset_credit_expiry_badge,
|
|
251
|
+
routing_strategy=settings.routing_strategy,
|
|
252
|
+
relative_availability_power=settings.relative_availability_power,
|
|
253
|
+
relative_availability_top_k=settings.relative_availability_top_k,
|
|
254
|
+
single_account_id=settings.single_account_id,
|
|
255
|
+
openai_cache_affinity_max_age_seconds=settings.openai_cache_affinity_max_age_seconds,
|
|
256
|
+
dashboard_session_ttl_seconds=settings.dashboard_session_ttl_seconds,
|
|
257
|
+
http_responses_session_bridge_prompt_cache_idle_ttl_seconds=settings.http_responses_session_bridge_prompt_cache_idle_ttl_seconds,
|
|
258
|
+
http_responses_session_bridge_gateway_safe_mode=settings.http_responses_session_bridge_gateway_safe_mode,
|
|
259
|
+
# M3 codex prewarm
|
|
260
|
+
http_responses_session_bridge_codex_prewarm_enabled=(
|
|
261
|
+
settings.http_responses_session_bridge_codex_prewarm_enabled
|
|
262
|
+
),
|
|
263
|
+
# end M3 codex prewarm
|
|
264
|
+
sticky_reallocation_budget_threshold_pct=settings.sticky_reallocation_budget_threshold_pct,
|
|
265
|
+
sticky_reallocation_primary_budget_threshold_pct=settings.sticky_reallocation_primary_budget_threshold_pct,
|
|
266
|
+
sticky_reallocation_secondary_budget_threshold_pct=settings.sticky_reallocation_secondary_budget_threshold_pct,
|
|
267
|
+
additional_quota_routing_policies=settings.additional_quota_routing_policies,
|
|
268
|
+
additional_quota_policies=additional_quota_policies,
|
|
269
|
+
warmup_model=settings.warmup_model,
|
|
270
|
+
import_without_overwrite=settings.import_without_overwrite,
|
|
271
|
+
totp_required_on_login=settings.totp_required_on_login,
|
|
272
|
+
totp_required_for_admin_role=settings.totp_required_for_admin_role,
|
|
273
|
+
local_login_policy=cast(LocalLoginPolicyLiteral, settings.local_login_policy),
|
|
274
|
+
users_without_totp_count=settings.users_without_totp_count,
|
|
275
|
+
admins_without_totp_count=settings.admins_without_totp_count,
|
|
276
|
+
api_key_auth_enabled=settings.api_key_auth_enabled,
|
|
277
|
+
hide_upstream_quota_from_api_keys=settings.hide_upstream_quota_from_api_keys,
|
|
278
|
+
limit_warmup_enabled=settings.limit_warmup_enabled,
|
|
279
|
+
limit_warmup_windows=settings.limit_warmup_windows,
|
|
280
|
+
limit_warmup_model=settings.limit_warmup_model,
|
|
281
|
+
limit_warmup_prompt=settings.limit_warmup_prompt,
|
|
282
|
+
limit_warmup_cooldown_seconds=settings.limit_warmup_cooldown_seconds,
|
|
283
|
+
limit_warmup_exhausted_threshold_percent=settings.limit_warmup_exhausted_threshold_percent,
|
|
284
|
+
limit_warmup_idle_threshold_percent=settings.limit_warmup_idle_threshold_percent,
|
|
285
|
+
limit_warmup_min_available_percent=settings.limit_warmup_min_available_percent,
|
|
286
|
+
weekly_pace_working_days=settings.weekly_pace_working_days,
|
|
287
|
+
weekly_pace_smoothing_minutes=settings.weekly_pace_smoothing_minutes,
|
|
288
|
+
guest_access_enabled=settings.guest_access_enabled,
|
|
289
|
+
guest_password_configured=settings.guest_password_configured,
|
|
290
|
+
limit_warmup_staggered_idle_enabled=settings.limit_warmup_staggered_idle_enabled,
|
|
291
|
+
request_log_retention_days=settings.request_log_retention_days,
|
|
292
|
+
usage_history_retention_days=settings.usage_history_retention_days,
|
|
293
|
+
request_log_retention_override_days=settings.request_log_retention_override_days,
|
|
294
|
+
usage_history_retention_override_days=settings.usage_history_retention_override_days,
|
|
295
|
+
# C2-3 resilience toggles
|
|
296
|
+
soft_drain_enabled=settings.soft_drain_enabled,
|
|
297
|
+
deterministic_failover_enabled=settings.deterministic_failover_enabled,
|
|
298
|
+
circuit_breaker_enabled=settings.circuit_breaker_enabled,
|
|
299
|
+
# M2 background jobs
|
|
300
|
+
auth_guardian_enabled=settings.auth_guardian_enabled,
|
|
301
|
+
auth_guardian_blocked_by_topology=settings.auth_guardian_blocked_by_topology,
|
|
302
|
+
automations_scheduler_enabled=settings.automations_scheduler_enabled,
|
|
303
|
+
rate_limit_reset_credits_refresh_enabled=settings.rate_limit_reset_credits_refresh_enabled,
|
|
304
|
+
# end M2 background jobs
|
|
305
|
+
# M5 conversation archive
|
|
306
|
+
conversation_archive_enabled=settings.conversation_archive_enabled,
|
|
307
|
+
conversation_archive_dir=conversation_archive_dir,
|
|
308
|
+
# end M5 conversation archive
|
|
309
|
+
# R2 spool retention: the effective window plus the floor the API would
|
|
310
|
+
# enforce, so the data retention card can mirror the check client-side.
|
|
311
|
+
http_responses_session_bridge_operation_spool_retention_seconds=(
|
|
312
|
+
settings.http_responses_session_bridge_operation_spool_retention_seconds
|
|
313
|
+
),
|
|
314
|
+
http_responses_session_bridge_operation_spool_retention_floor_seconds=(
|
|
315
|
+
operation_spool_retention_floor_seconds(settings, startup_settings=environment_settings)
|
|
316
|
+
),
|
|
317
|
+
# end R2 spool retention
|
|
318
|
+
version=settings.version,
|
|
319
|
+
# C2-1 timeouts
|
|
320
|
+
upstream_connect_timeout_seconds=settings.upstream_connect_timeout_seconds,
|
|
321
|
+
proxy_request_budget_seconds=settings.proxy_request_budget_seconds,
|
|
322
|
+
compact_request_budget_seconds=settings.compact_request_budget_seconds,
|
|
323
|
+
transcription_request_budget_seconds=settings.transcription_request_budget_seconds,
|
|
324
|
+
stream_idle_timeout_seconds=settings.stream_idle_timeout_seconds,
|
|
325
|
+
proxy_downstream_websocket_idle_timeout_seconds=settings.proxy_downstream_websocket_idle_timeout_seconds,
|
|
326
|
+
sse_keepalive_interval_seconds=settings.sse_keepalive_interval_seconds,
|
|
327
|
+
# end C2-1 timeouts
|
|
328
|
+
# M1 stream/bridge budgets
|
|
329
|
+
http_responses_stream_request_budget_seconds=settings.http_responses_stream_request_budget_seconds,
|
|
330
|
+
http_responses_session_bridge_request_budget_seconds=(
|
|
331
|
+
settings.http_responses_session_bridge_request_budget_seconds
|
|
332
|
+
),
|
|
333
|
+
# end M1 stream/bridge budgets
|
|
334
|
+
provenance={
|
|
335
|
+
name: SettingProvenance(source=resolved.source, env_value=resolved.env_value, default=resolved.default)
|
|
336
|
+
for name, resolved in settings.provenance.items()
|
|
337
|
+
},
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@router.get("", response_model=DashboardSettingsResponse)
|
|
342
|
+
async def get_settings(
|
|
343
|
+
principal: DashboardPrincipal = Depends(validate_dashboard_session),
|
|
344
|
+
context: SettingsContext = Depends(get_settings_context),
|
|
345
|
+
) -> DashboardSettingsResponse:
|
|
346
|
+
settings = await context.service.get_settings(actor_user_id=principal.user_id)
|
|
347
|
+
return _dashboard_settings_response(settings, principal=principal)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
@router.get(
|
|
351
|
+
"/runtime/connect-address",
|
|
352
|
+
response_model=RuntimeConnectAddressResponse,
|
|
353
|
+
dependencies=[Depends(require_dashboard_permission(Permission.OPS_WRITE))],
|
|
354
|
+
)
|
|
355
|
+
async def get_runtime_connect_address(request: Request) -> RuntimeConnectAddressResponse:
|
|
356
|
+
return RuntimeConnectAddressResponse(connect_address=_resolve_runtime_connect_address(request))
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
@router.get(
|
|
360
|
+
"/upstream-proxy",
|
|
361
|
+
response_model=UpstreamProxyAdminResponse,
|
|
362
|
+
dependencies=[Depends(require_dashboard_permission(Permission.OPS_WRITE))],
|
|
363
|
+
)
|
|
364
|
+
async def get_upstream_proxy_admin(
|
|
365
|
+
context: SettingsContext = Depends(get_settings_context),
|
|
366
|
+
) -> UpstreamProxyAdminResponse:
|
|
367
|
+
settings = await context.repository.get_or_create()
|
|
368
|
+
endpoint_rows = (await context.session.execute(select(ProxyEndpoint).order_by(ProxyEndpoint.name.asc()))).scalars()
|
|
369
|
+
pool_rows = (await context.session.execute(select(ProxyPool).order_by(ProxyPool.name.asc()))).scalars().all()
|
|
370
|
+
member_rows = (
|
|
371
|
+
await context.session.execute(select(ProxyPoolMember).order_by(ProxyPoolMember.sort_order.asc()))
|
|
372
|
+
).scalars()
|
|
373
|
+
bindings = (
|
|
374
|
+
(await context.session.execute(select(AccountProxyBinding).order_by(AccountProxyBinding.account_id.asc())))
|
|
375
|
+
.scalars()
|
|
376
|
+
.all()
|
|
377
|
+
)
|
|
378
|
+
endpoint_ids_by_pool: dict[str, list[str]] = {}
|
|
379
|
+
for member in member_rows:
|
|
380
|
+
endpoint_ids_by_pool.setdefault(member.pool_id, []).append(member.endpoint_id)
|
|
381
|
+
return UpstreamProxyAdminResponse(
|
|
382
|
+
routing_enabled=settings.upstream_proxy_routing_enabled,
|
|
383
|
+
default_pool_id=settings.upstream_proxy_default_pool_id,
|
|
384
|
+
endpoints=[_proxy_endpoint_response(row) for row in endpoint_rows],
|
|
385
|
+
pools=[
|
|
386
|
+
UpstreamProxyPoolResponse(
|
|
387
|
+
id=row.id,
|
|
388
|
+
name=row.name,
|
|
389
|
+
is_active=row.is_active,
|
|
390
|
+
endpoint_ids=endpoint_ids_by_pool.get(row.id, []),
|
|
391
|
+
)
|
|
392
|
+
for row in pool_rows
|
|
393
|
+
],
|
|
394
|
+
bindings=[
|
|
395
|
+
AccountProxyBindingResponse(account_id=row.account_id, pool_id=row.pool_id, is_active=row.is_active)
|
|
396
|
+
for row in bindings
|
|
397
|
+
],
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
@router.post("/upstream-proxy/endpoints", response_model=UpstreamProxyEndpointResponse)
|
|
402
|
+
async def create_upstream_proxy_endpoint(
|
|
403
|
+
payload: UpstreamProxyEndpointCreateRequest,
|
|
404
|
+
_security_access=Depends(require_dashboard_permission(Permission.SECURITY_WRITE)),
|
|
405
|
+
context: SettingsContext = Depends(get_settings_context),
|
|
406
|
+
) -> UpstreamProxyEndpointResponse:
|
|
407
|
+
if payload.username is not None and ":" in payload.username:
|
|
408
|
+
# Mirrors the resolver: a Basic user-id cannot encode a colon.
|
|
409
|
+
raise DashboardBadRequestError('Proxy usernames cannot contain ":"', code="invalid_proxy_username")
|
|
410
|
+
encryptor = TokenEncryptor()
|
|
411
|
+
row = ProxyEndpoint(
|
|
412
|
+
name=payload.name,
|
|
413
|
+
scheme=payload.scheme,
|
|
414
|
+
host=payload.host,
|
|
415
|
+
port=payload.port,
|
|
416
|
+
username=payload.username,
|
|
417
|
+
password_encrypted=encryptor.encrypt(payload.password) if payload.password else None,
|
|
418
|
+
is_active=payload.is_active,
|
|
419
|
+
)
|
|
420
|
+
context.session.add(row)
|
|
421
|
+
await context.session.commit()
|
|
422
|
+
await context.session.refresh(row)
|
|
423
|
+
return _proxy_endpoint_response(row)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
@router.post("/upstream-proxy/endpoints/{endpoint_id}/test", response_model=UpstreamProxyEndpointTestResponse)
|
|
427
|
+
async def test_upstream_proxy_endpoint(
|
|
428
|
+
endpoint_id: str,
|
|
429
|
+
_write_access=Depends(require_dashboard_write_access),
|
|
430
|
+
context: SettingsContext = Depends(get_settings_context),
|
|
431
|
+
) -> UpstreamProxyEndpointTestResponse:
|
|
432
|
+
row = await context.session.get(ProxyEndpoint, endpoint_id)
|
|
433
|
+
if row is None:
|
|
434
|
+
raise DashboardBadRequestError("Proxy endpoint not found", code="proxy_endpoint_not_found")
|
|
435
|
+
try:
|
|
436
|
+
endpoint = resolve_proxy_endpoint(row, encryptor=TokenEncryptor())
|
|
437
|
+
except UpstreamProxyRouteError as exc:
|
|
438
|
+
# Persisted rows the resolver now rejects report the reason instead of 500.
|
|
439
|
+
return UpstreamProxyEndpointTestResponse(endpoint_id=row.id, ok=False, elapsed_ms=0, error=exc.reason)
|
|
440
|
+
started = time.monotonic()
|
|
441
|
+
try:
|
|
442
|
+
status_code = await _probe_upstream_proxy_endpoint(endpoint)
|
|
443
|
+
except Exception as exc:
|
|
444
|
+
return UpstreamProxyEndpointTestResponse(
|
|
445
|
+
endpoint_id=endpoint.id,
|
|
446
|
+
ok=False,
|
|
447
|
+
elapsed_ms=_elapsed_ms(started),
|
|
448
|
+
error=type(exc).__name__,
|
|
449
|
+
)
|
|
450
|
+
if status_code == HTTP_PROXY_AUTHENTICATION_REQUIRED:
|
|
451
|
+
return UpstreamProxyEndpointTestResponse(
|
|
452
|
+
endpoint_id=endpoint.id,
|
|
453
|
+
ok=False,
|
|
454
|
+
status_code=status_code,
|
|
455
|
+
elapsed_ms=_elapsed_ms(started),
|
|
456
|
+
error="proxy_auth_failed",
|
|
457
|
+
)
|
|
458
|
+
ok = status_code < 500
|
|
459
|
+
return UpstreamProxyEndpointTestResponse(
|
|
460
|
+
endpoint_id=endpoint.id,
|
|
461
|
+
ok=ok,
|
|
462
|
+
status_code=status_code,
|
|
463
|
+
elapsed_ms=_elapsed_ms(started),
|
|
464
|
+
error=None if ok else "upstream_probe_failed",
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
@router.post("/upstream-proxy/pools", response_model=UpstreamProxyPoolResponse)
|
|
469
|
+
async def create_upstream_proxy_pool(
|
|
470
|
+
payload: UpstreamProxyPoolCreateRequest,
|
|
471
|
+
_write_access=Depends(require_dashboard_write_access),
|
|
472
|
+
context: SettingsContext = Depends(get_settings_context),
|
|
473
|
+
) -> UpstreamProxyPoolResponse:
|
|
474
|
+
endpoint_ids = list(dict.fromkeys(payload.endpoint_ids))
|
|
475
|
+
await _validate_proxy_endpoint_ids(context, endpoint_ids)
|
|
476
|
+
pool = ProxyPool(name=payload.name, is_active=payload.is_active)
|
|
477
|
+
context.session.add(pool)
|
|
478
|
+
await context.session.flush()
|
|
479
|
+
for sort_order, endpoint_id in enumerate(endpoint_ids):
|
|
480
|
+
context.session.add(ProxyPoolMember(pool_id=pool.id, endpoint_id=endpoint_id, sort_order=sort_order))
|
|
481
|
+
try:
|
|
482
|
+
await context.session.commit()
|
|
483
|
+
except IntegrityError as exc:
|
|
484
|
+
await context.session.rollback()
|
|
485
|
+
if _is_missing_proxy_endpoint_error(exc):
|
|
486
|
+
raise DashboardBadRequestError("Proxy endpoint not found", code="proxy_endpoint_not_found")
|
|
487
|
+
raise
|
|
488
|
+
await context.session.refresh(pool)
|
|
489
|
+
return UpstreamProxyPoolResponse(
|
|
490
|
+
id=pool.id,
|
|
491
|
+
name=pool.name,
|
|
492
|
+
is_active=pool.is_active,
|
|
493
|
+
endpoint_ids=endpoint_ids,
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
@router.post("/upstream-proxy/pools/{pool_id}/members", response_model=UpstreamProxyPoolResponse)
|
|
498
|
+
async def add_upstream_proxy_pool_member(
|
|
499
|
+
pool_id: str,
|
|
500
|
+
payload: UpstreamProxyPoolMemberRequest,
|
|
501
|
+
_write_access=Depends(require_dashboard_write_access),
|
|
502
|
+
context: SettingsContext = Depends(get_settings_context),
|
|
503
|
+
) -> UpstreamProxyPoolResponse:
|
|
504
|
+
pool = await context.session.get(ProxyPool, pool_id)
|
|
505
|
+
if pool is None:
|
|
506
|
+
raise DashboardBadRequestError("Proxy pool not found", code="proxy_pool_not_found")
|
|
507
|
+
await _validate_proxy_endpoint_ids(context, [payload.endpoint_id])
|
|
508
|
+
await _validate_proxy_pool_member_is_unique(context, pool_id=pool_id, endpoint_id=payload.endpoint_id)
|
|
509
|
+
context.session.add(
|
|
510
|
+
ProxyPoolMember(
|
|
511
|
+
pool_id=pool_id,
|
|
512
|
+
endpoint_id=payload.endpoint_id,
|
|
513
|
+
sort_order=payload.sort_order,
|
|
514
|
+
weight=payload.weight,
|
|
515
|
+
is_active=payload.is_active,
|
|
516
|
+
)
|
|
517
|
+
)
|
|
518
|
+
try:
|
|
519
|
+
await context.session.commit()
|
|
520
|
+
except IntegrityError as exc:
|
|
521
|
+
await context.session.rollback()
|
|
522
|
+
if _is_missing_proxy_endpoint_error(exc):
|
|
523
|
+
raise DashboardBadRequestError("Proxy endpoint not found", code="proxy_endpoint_not_found")
|
|
524
|
+
if _is_duplicate_proxy_pool_member_error(exc):
|
|
525
|
+
raise _duplicate_proxy_pool_member_error()
|
|
526
|
+
raise
|
|
527
|
+
# Pool membership is a route-resolver input: clear + durably bump before
|
|
528
|
+
# responding (same contract as the account-binding upsert).
|
|
529
|
+
await get_upstream_route_cache().invalidate()
|
|
530
|
+
endpoint_ids = (
|
|
531
|
+
(
|
|
532
|
+
await context.session.execute(
|
|
533
|
+
select(ProxyPoolMember.endpoint_id)
|
|
534
|
+
.where(ProxyPoolMember.pool_id == pool_id)
|
|
535
|
+
.order_by(ProxyPoolMember.sort_order.asc())
|
|
536
|
+
)
|
|
537
|
+
)
|
|
538
|
+
.scalars()
|
|
539
|
+
.all()
|
|
540
|
+
)
|
|
541
|
+
return UpstreamProxyPoolResponse(
|
|
542
|
+
id=pool.id,
|
|
543
|
+
name=pool.name,
|
|
544
|
+
is_active=pool.is_active,
|
|
545
|
+
endpoint_ids=list(endpoint_ids),
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
async def _validate_proxy_endpoint_ids(context: SettingsContext, endpoint_ids: list[str]) -> None:
|
|
550
|
+
if not endpoint_ids:
|
|
551
|
+
return
|
|
552
|
+
existing_ids = set(
|
|
553
|
+
(await context.session.execute(select(ProxyEndpoint.id).where(ProxyEndpoint.id.in_(endpoint_ids))))
|
|
554
|
+
.scalars()
|
|
555
|
+
.all()
|
|
556
|
+
)
|
|
557
|
+
missing_ids = [endpoint_id for endpoint_id in endpoint_ids if endpoint_id not in existing_ids]
|
|
558
|
+
if missing_ids:
|
|
559
|
+
raise DashboardBadRequestError(
|
|
560
|
+
f"Proxy endpoint not found: {', '.join(missing_ids)}",
|
|
561
|
+
code="proxy_endpoint_not_found",
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
async def _validate_proxy_pool_member_is_unique(
|
|
566
|
+
context: SettingsContext,
|
|
567
|
+
*,
|
|
568
|
+
pool_id: str,
|
|
569
|
+
endpoint_id: str,
|
|
570
|
+
) -> None:
|
|
571
|
+
existing_id = (
|
|
572
|
+
await context.session.execute(
|
|
573
|
+
select(ProxyPoolMember.id)
|
|
574
|
+
.where(ProxyPoolMember.pool_id == pool_id, ProxyPoolMember.endpoint_id == endpoint_id)
|
|
575
|
+
.limit(1)
|
|
576
|
+
)
|
|
577
|
+
).scalar_one_or_none()
|
|
578
|
+
if existing_id is not None:
|
|
579
|
+
raise _duplicate_proxy_pool_member_error()
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
async def _validate_proxy_pool_id(context: SettingsContext, pool_id: str | None) -> None:
|
|
583
|
+
if pool_id is None:
|
|
584
|
+
return
|
|
585
|
+
if await context.session.get(ProxyPool, pool_id) is None:
|
|
586
|
+
raise DashboardBadRequestError("Proxy pool not found", code="proxy_pool_not_found")
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
async def _get_account_or_error(context: SettingsContext, account_id: str) -> Account:
|
|
590
|
+
account = await context.session.get(Account, account_id)
|
|
591
|
+
# An account marked for background deletion is already deleted from the
|
|
592
|
+
# operator's point of view: binding mutations must report not-found, as
|
|
593
|
+
# the synchronous delete did once the row was removed.
|
|
594
|
+
if account is None or account.delete_requested_at is not None:
|
|
595
|
+
raise DashboardBadRequestError("Account not found", code="account_not_found")
|
|
596
|
+
return account
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def _is_missing_proxy_endpoint_error(exc: Exception) -> bool:
|
|
600
|
+
message = str(exc).lower()
|
|
601
|
+
return "foreign key" in message or "fk constraint" in message or "violates foreign key constraint" in message
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def _is_duplicate_proxy_pool_member_error(exc: Exception) -> bool:
|
|
605
|
+
message = str(exc).lower()
|
|
606
|
+
return (
|
|
607
|
+
"uq_proxy_pool_members_pool_endpoint" in message
|
|
608
|
+
or ("proxy_pool_members" in message and "unique" in message)
|
|
609
|
+
or ("proxy_pool_members.pool_id" in message and "proxy_pool_members.endpoint_id" in message)
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def _duplicate_proxy_pool_member_error() -> DashboardBadRequestError:
|
|
614
|
+
return DashboardBadRequestError(
|
|
615
|
+
"Proxy endpoint is already a member of this pool",
|
|
616
|
+
code="proxy_pool_member_duplicate",
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
@router.put("/upstream-proxy/accounts/{account_id}/binding", response_model=AccountProxyBindingResponse)
|
|
621
|
+
async def put_account_proxy_binding(
|
|
622
|
+
account_id: str,
|
|
623
|
+
payload: AccountProxyBindingRequest,
|
|
624
|
+
request: Request,
|
|
625
|
+
_write_access=Depends(require_dashboard_write_access),
|
|
626
|
+
context: SettingsContext = Depends(get_settings_context),
|
|
627
|
+
) -> AccountProxyBindingResponse:
|
|
628
|
+
account = await _get_account_or_error(context, account_id)
|
|
629
|
+
await _validate_proxy_pool_id(context, payload.pool_id)
|
|
630
|
+
row = (
|
|
631
|
+
(
|
|
632
|
+
await context.session.execute(
|
|
633
|
+
select(AccountProxyBinding).where(AccountProxyBinding.account_id == account_id).limit(1)
|
|
634
|
+
)
|
|
635
|
+
)
|
|
636
|
+
.scalars()
|
|
637
|
+
.one_or_none()
|
|
638
|
+
)
|
|
639
|
+
close_bridge_sessions = payload.is_active
|
|
640
|
+
if row is None:
|
|
641
|
+
row = AccountProxyBinding(account_id=account_id, pool_id=payload.pool_id, is_active=payload.is_active)
|
|
642
|
+
context.session.add(row)
|
|
643
|
+
else:
|
|
644
|
+
close_bridge_sessions = row.pool_id != payload.pool_id or row.is_active != payload.is_active
|
|
645
|
+
row.pool_id = payload.pool_id
|
|
646
|
+
row.is_active = payload.is_active
|
|
647
|
+
reactivated = False
|
|
648
|
+
if payload.is_active and _account_proxy_binding_should_reactivate(account):
|
|
649
|
+
account.status = AccountStatus.ACTIVE
|
|
650
|
+
account.deactivation_reason = None
|
|
651
|
+
account.reset_at = None
|
|
652
|
+
account.blocked_at = None
|
|
653
|
+
reactivated = True
|
|
654
|
+
await context.session.commit()
|
|
655
|
+
# Binding rows are a route-resolver input: clear this replica's route
|
|
656
|
+
# cache and durably bump ``upstream_route`` before responding so no
|
|
657
|
+
# request on the mutating replica resolves the pre-mutation binding.
|
|
658
|
+
await get_upstream_route_cache().invalidate()
|
|
659
|
+
if reactivated:
|
|
660
|
+
# Invalidate + bump only after the status commit so peers (and this
|
|
661
|
+
# replica's poller) never rebuild selection/routing inputs from the
|
|
662
|
+
# pre-commit row. The coalesced ``account_selection`` bump enqueued by
|
|
663
|
+
# ``invalidate()`` and the durable ``account_routing`` bump both fire
|
|
664
|
+
# post-commit, matching AccountService.reactivate_account.
|
|
665
|
+
clear_account_routing_unavailable(account_id)
|
|
666
|
+
get_account_selection_cache().invalidate()
|
|
667
|
+
await propagate_account_routing_change()
|
|
668
|
+
if close_bridge_sessions:
|
|
669
|
+
await get_proxy_service_for_app(request.app).close_http_bridge_sessions_for_account(account_id)
|
|
670
|
+
await context.session.refresh(row)
|
|
671
|
+
return AccountProxyBindingResponse(account_id=row.account_id, pool_id=row.pool_id, is_active=row.is_active)
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
def _proxy_endpoint_response(row: ProxyEndpoint) -> UpstreamProxyEndpointResponse:
|
|
675
|
+
return UpstreamProxyEndpointResponse(
|
|
676
|
+
id=row.id,
|
|
677
|
+
name=row.name,
|
|
678
|
+
scheme=row.scheme,
|
|
679
|
+
host=row.host,
|
|
680
|
+
port=row.port,
|
|
681
|
+
username=row.username,
|
|
682
|
+
is_active=row.is_active,
|
|
683
|
+
plaintext_credentials=sends_plaintext_credentials(
|
|
684
|
+
row.scheme, has_credentials=row.username is not None or row.password_encrypted is not None
|
|
685
|
+
),
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def _account_proxy_binding_should_reactivate(account: Account) -> bool:
|
|
690
|
+
reason = account.deactivation_reason or ""
|
|
691
|
+
return (
|
|
692
|
+
account.status == AccountStatus.PAUSED
|
|
693
|
+
and reason == IMPORT_PROXY_REQUIRED_PAUSE_REASON
|
|
694
|
+
or account.status == AccountStatus.DEACTIVATED
|
|
695
|
+
and (reason == "proxy_unreachable" or reason.startswith("proxy_unreachable:"))
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
async def _probe_upstream_proxy_endpoint(endpoint) -> int:
|
|
700
|
+
if endpoint.scheme.startswith("socks"):
|
|
701
|
+
connector = ProxyConnector(
|
|
702
|
+
host=endpoint.host,
|
|
703
|
+
port=endpoint.port,
|
|
704
|
+
proxy_type=ProxyType.SOCKS5,
|
|
705
|
+
username=endpoint.username,
|
|
706
|
+
password=endpoint.password,
|
|
707
|
+
rdns=endpoint.proxy_url.split(":", 1)[0] == "socks5h",
|
|
708
|
+
ssl=_shared_ssl_context(),
|
|
709
|
+
)
|
|
710
|
+
async with aiohttp.ClientSession(
|
|
711
|
+
connector=connector,
|
|
712
|
+
timeout=aiohttp.ClientTimeout(total=UPSTREAM_PROXY_TEST_TIMEOUT_SECONDS),
|
|
713
|
+
trust_env=False,
|
|
714
|
+
) as client:
|
|
715
|
+
response = await client.get(UPSTREAM_PROXY_TEST_URL, allow_redirects=False)
|
|
716
|
+
return response.status
|
|
717
|
+
async with httpx.AsyncClient(
|
|
718
|
+
proxy=endpoint.proxy_url,
|
|
719
|
+
timeout=httpx.Timeout(UPSTREAM_PROXY_TEST_TIMEOUT_SECONDS),
|
|
720
|
+
follow_redirects=False,
|
|
721
|
+
) as client:
|
|
722
|
+
response = await client.get(UPSTREAM_PROXY_TEST_URL)
|
|
723
|
+
return response.status_code
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
def _elapsed_ms(started: float) -> int:
|
|
727
|
+
return max(0, round((time.monotonic() - started) * 1000))
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
# C2-1 timeouts
|
|
731
|
+
# Dashboard-managed fields the timeout-invariant rules read: the C2-1 timeouts
|
|
732
|
+
# and request budgets plus the C2-2 account lease TTL (``account-lease-ttl-
|
|
733
|
+
# covers-*``), so one PUT-time check sees the merged effective values — a TTL
|
|
734
|
+
# below a dashboard budget and a budget above a dashboard TTL are both caught.
|
|
735
|
+
_TIMEOUT_INVARIANT_DASHBOARD_SETTINGS: tuple[str, ...] = (
|
|
736
|
+
*DASHBOARD_TIMEOUT_SETTINGS,
|
|
737
|
+
"proxy_account_lease_ttl_seconds", # C2-2 routing/overload
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
# M4 model catalogue: per-model context window overrides. One dashboard row per
|
|
742
|
+
# slug (``model_context_window_overrides``); the
|
|
743
|
+
# ``CODEX_LB_MODEL_CONTEXT_WINDOW_OVERRIDES`` entry is the per-slug fallback.
|
|
744
|
+
MODEL_CONTEXT_WINDOW_OVERRIDES_PATH = "/model-context-window-overrides"
|
|
745
|
+
_MODEL_SLUG_MAX_LENGTH = 256
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
def _validate_model_slug(slug: str) -> str:
|
|
749
|
+
# The raw segment is validated, never trimmed: silently storing " gpt-5.4 "
|
|
750
|
+
# as "gpt-5.4" would make the row the operator sees disagree with the slug
|
|
751
|
+
# they wrote, and the requirement rejects any slug containing whitespace.
|
|
752
|
+
if (
|
|
753
|
+
not slug
|
|
754
|
+
or len(slug) > _MODEL_SLUG_MAX_LENGTH
|
|
755
|
+
or any(character.isspace() or not character.isprintable() for character in slug)
|
|
756
|
+
):
|
|
757
|
+
raise DashboardBadRequestError(
|
|
758
|
+
f"Model slug must be 1-{_MODEL_SLUG_MAX_LENGTH} printable characters without whitespace",
|
|
759
|
+
code="invalid_model_slug",
|
|
760
|
+
)
|
|
761
|
+
return slug
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
async def _model_context_window_overrides_response(context: SettingsContext) -> ModelContextWindowOverridesResponse:
|
|
765
|
+
dashboard = await ModelContextWindowOverridesRepository(context.session).by_slug()
|
|
766
|
+
resolved = resolve_context_window_overrides(dashboard, get_app_settings().model_context_window_overrides)
|
|
767
|
+
return ModelContextWindowOverridesResponse(
|
|
768
|
+
overrides=[
|
|
769
|
+
ModelContextWindowOverrideResponse(
|
|
770
|
+
slug=override.slug,
|
|
771
|
+
context_window=override.context_window,
|
|
772
|
+
source=override.source,
|
|
773
|
+
env_value=override.env_value,
|
|
774
|
+
)
|
|
775
|
+
for override in resolved.values()
|
|
776
|
+
]
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
@router.get(MODEL_CONTEXT_WINDOW_OVERRIDES_PATH, response_model=ModelContextWindowOverridesResponse)
|
|
781
|
+
async def get_model_context_window_overrides(
|
|
782
|
+
context: SettingsContext = Depends(get_settings_context),
|
|
783
|
+
) -> ModelContextWindowOverridesResponse:
|
|
784
|
+
return await _model_context_window_overrides_response(context)
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
@router.put(MODEL_CONTEXT_WINDOW_OVERRIDES_PATH + "/{slug:path}", response_model=ModelContextWindowOverridesResponse)
|
|
788
|
+
async def put_model_context_window_override(
|
|
789
|
+
request: Request,
|
|
790
|
+
slug: str,
|
|
791
|
+
payload: ModelContextWindowOverrideUpsertRequest,
|
|
792
|
+
_write_access=Depends(require_dashboard_write_access),
|
|
793
|
+
context: SettingsContext = Depends(get_settings_context),
|
|
794
|
+
) -> ModelContextWindowOverridesResponse:
|
|
795
|
+
normalized = _validate_model_slug(slug)
|
|
796
|
+
await ModelContextWindowOverridesRepository(context.session).upsert(normalized, payload.context_window)
|
|
797
|
+
# The catalog reads a cached snapshot of the rows: clear + durably bump
|
|
798
|
+
# before responding so every replica reports the new window.
|
|
799
|
+
await get_model_context_window_overrides_cache().invalidate()
|
|
800
|
+
# Audited like every other dashboard settings write: this one changes what
|
|
801
|
+
# the model catalog advertises to every client.
|
|
802
|
+
AuditService.log_async(
|
|
803
|
+
"settings_changed",
|
|
804
|
+
actor_ip=request.client.host if request.client else None,
|
|
805
|
+
details={"changed_fields": ["model_context_window_overrides"], "slug": normalized},
|
|
806
|
+
)
|
|
807
|
+
return await _model_context_window_overrides_response(context)
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
@router.delete(MODEL_CONTEXT_WINDOW_OVERRIDES_PATH + "/{slug:path}", response_model=ModelContextWindowOverridesResponse)
|
|
811
|
+
async def delete_model_context_window_override(
|
|
812
|
+
request: Request,
|
|
813
|
+
slug: str,
|
|
814
|
+
_write_access=Depends(require_dashboard_write_access),
|
|
815
|
+
context: SettingsContext = Depends(get_settings_context),
|
|
816
|
+
) -> ModelContextWindowOverridesResponse:
|
|
817
|
+
normalized = _validate_model_slug(slug)
|
|
818
|
+
if not await ModelContextWindowOverridesRepository(context.session).delete(normalized):
|
|
819
|
+
raise DashboardNotFoundError(
|
|
820
|
+
"Model context window override not found", code="model_context_window_override_not_found"
|
|
821
|
+
)
|
|
822
|
+
await get_model_context_window_overrides_cache().invalidate()
|
|
823
|
+
AuditService.log_async(
|
|
824
|
+
"settings_changed",
|
|
825
|
+
actor_ip=request.client.host if request.client else None,
|
|
826
|
+
details={"changed_fields": ["model_context_window_overrides"], "slug": normalized},
|
|
827
|
+
)
|
|
828
|
+
return await _model_context_window_overrides_response(context)
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
# end M4 model catalogue
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
def _proposed_timeout_settings(payload: DashboardSettingsUpdateRequest, current, startup_settings) -> dict[str, float]:
|
|
835
|
+
"""Effective timeout values after ``payload`` is applied: value = store, null = inherit, absent = current."""
|
|
836
|
+
proposed: dict[str, float] = {}
|
|
837
|
+
for name in _TIMEOUT_INVARIANT_DASHBOARD_SETTINGS:
|
|
838
|
+
if name in payload.model_fields_set:
|
|
839
|
+
value = getattr(payload, name)
|
|
840
|
+
if value is None: # inherit: the environment value, or the process default for a partial fake
|
|
841
|
+
value = getattr(startup_settings, name, None)
|
|
842
|
+
if value is None:
|
|
843
|
+
value = getattr(settings_module.get_settings(), name)
|
|
844
|
+
proposed[name] = float(value)
|
|
845
|
+
else:
|
|
846
|
+
proposed[name] = float(getattr(current, name))
|
|
847
|
+
return proposed
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
class _EffectiveTimeoutView:
|
|
851
|
+
"""``TimeoutSettings`` view: effective timeout values over the startup settings.
|
|
852
|
+
|
|
853
|
+
Fields a startup-settings fake does not carry are read from the process
|
|
854
|
+
``Settings`` so constant-only rules keep evaluating.
|
|
855
|
+
"""
|
|
856
|
+
|
|
857
|
+
def __init__(self, base: object, overrides: Mapping[str, float]) -> None:
|
|
858
|
+
self._base = base
|
|
859
|
+
self._overrides = overrides
|
|
860
|
+
|
|
861
|
+
def __getattr__(self, name: str) -> object:
|
|
862
|
+
if name in self._overrides:
|
|
863
|
+
return self._overrides[name]
|
|
864
|
+
if hasattr(self._base, name):
|
|
865
|
+
return getattr(self._base, name)
|
|
866
|
+
return getattr(settings_module.get_settings(), name)
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
def _validate_timeout_invariants(payload: DashboardSettingsUpdateRequest, current, startup_settings) -> None:
|
|
870
|
+
"""Reject a PUT that introduces a timeout-invariant violation against the effective values.
|
|
871
|
+
|
|
872
|
+
The same rules as startup (``app.core.timeout_invariants``) are evaluated on
|
|
873
|
+
the effective settings — dashboard value, else environment, else default —
|
|
874
|
+
before and after the change; only violations the change introduces are
|
|
875
|
+
rejected, so a deployment whose environment already violates a rule can
|
|
876
|
+
still edit unrelated fields. The C2-2 account lease TTL takes part in the
|
|
877
|
+
same evaluation, so lowering a budget below a dashboard TTL, or storing a
|
|
878
|
+
TTL below an effective budget, is rejected on the merged values.
|
|
879
|
+
"""
|
|
880
|
+
if not set(_TIMEOUT_INVARIANT_DASHBOARD_SETTINGS) & payload.model_fields_set:
|
|
881
|
+
return
|
|
882
|
+
before = _EffectiveTimeoutView(
|
|
883
|
+
startup_settings, {name: float(getattr(current, name)) for name in _TIMEOUT_INVARIANT_DASHBOARD_SETTINGS}
|
|
884
|
+
)
|
|
885
|
+
after = _EffectiveTimeoutView(startup_settings, _proposed_timeout_settings(payload, current, startup_settings))
|
|
886
|
+
before_ids = {violation.rule.id for violation in find_timeout_invariant_violations(cast(Any, before))}
|
|
887
|
+
introduced = [
|
|
888
|
+
violation
|
|
889
|
+
for violation in find_timeout_invariant_violations(cast(Any, after))
|
|
890
|
+
if violation.rule.id not in before_ids
|
|
891
|
+
]
|
|
892
|
+
if introduced:
|
|
893
|
+
raise DashboardBadRequestError(
|
|
894
|
+
"; ".join(violation.format() for violation in introduced),
|
|
895
|
+
code="timeout_invariant_violation",
|
|
896
|
+
)
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
# R2 spool retention
|
|
900
|
+
class _ProposedSpoolRetentionInputs:
|
|
901
|
+
"""Effective floor inputs after ``payload`` is applied, over ``current``.
|
|
902
|
+
|
|
903
|
+
The three inputs behave differently in the update request: the two reuse
|
|
904
|
+
windows are non-nullable dashboard columns with no environment layer, so a
|
|
905
|
+
``None`` there only ever means "unchanged"; the bridge request budget is
|
|
906
|
+
tri-state, so an explicit null returns it to the environment value.
|
|
907
|
+
"""
|
|
908
|
+
|
|
909
|
+
__slots__ = ("_values",)
|
|
910
|
+
|
|
911
|
+
def __init__(self, payload: DashboardSettingsUpdateRequest, current, startup_settings) -> None:
|
|
912
|
+
values: dict[str, float] = {}
|
|
913
|
+
for name in SPOOL_RETENTION_FLOOR_INPUTS:
|
|
914
|
+
proposed = getattr(payload, name, None)
|
|
915
|
+
inheritable = name in settings_module.Settings.model_fields
|
|
916
|
+
if proposed is None and inheritable and name in payload.model_fields_set:
|
|
917
|
+
# Explicit null on a tri-state field: inherit the environment
|
|
918
|
+
# value (the process default for a partial startup fake).
|
|
919
|
+
proposed = getattr(startup_settings, name, None)
|
|
920
|
+
if proposed is None:
|
|
921
|
+
proposed = getattr(settings_module.get_settings(), name)
|
|
922
|
+
values[name] = float(proposed if proposed is not None else getattr(current, name))
|
|
923
|
+
self._values = values
|
|
924
|
+
|
|
925
|
+
def __getattr__(self, name: str) -> object:
|
|
926
|
+
try:
|
|
927
|
+
return self._values[name]
|
|
928
|
+
except KeyError as exc: # keep ``getattr(obj, name, default)`` working
|
|
929
|
+
raise AttributeError(name) from exc
|
|
930
|
+
|
|
931
|
+
|
|
932
|
+
def _proposed_spool_retention_seconds(payload: DashboardSettingsUpdateRequest, current, startup_settings) -> float:
|
|
933
|
+
"""Effective spool retention after ``payload``: value = store, null = inherit, absent = current."""
|
|
934
|
+
if OPERATION_SPOOL_RETENTION_SETTING not in payload.model_fields_set:
|
|
935
|
+
return float(getattr(current, OPERATION_SPOOL_RETENTION_SETTING))
|
|
936
|
+
proposed = getattr(payload, OPERATION_SPOOL_RETENTION_SETTING)
|
|
937
|
+
if proposed is not None:
|
|
938
|
+
return float(proposed)
|
|
939
|
+
inherited = current.provenance[OPERATION_SPOOL_RETENTION_SETTING]
|
|
940
|
+
return float(resolve_inheritable(None, inherited.env_value, inherited.default).value)
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
def _validate_spool_retention_floor(payload: DashboardSettingsUpdateRequest, current, startup_settings) -> None:
|
|
944
|
+
"""Reject a PUT that would leave the operation spool shorter than its replay floor.
|
|
945
|
+
|
|
946
|
+
The spool holds the raw request payload and the response events that
|
|
947
|
+
durable bridge recovery replays, so it must outlive every window in which a
|
|
948
|
+
spooled operation may still be read. Both the retention window and the floor
|
|
949
|
+
are evaluated on the effective values (dashboard column, else environment,
|
|
950
|
+
else code default) before and after the change.
|
|
951
|
+
|
|
952
|
+
A configuration can already be below the floor without ever passing through
|
|
953
|
+
here -- the environment alias alone decides it while the column is NULL --
|
|
954
|
+
and that state must not make the dashboard read-only. The rule is therefore
|
|
955
|
+
"no worse", not "bypass": while below the floor an update is accepted only
|
|
956
|
+
when it leaves the retention no shorter and the floor no higher than it
|
|
957
|
+
found them, so unrelated edits go through but deepening the violation does
|
|
958
|
+
not. Startup warns about the state (``_report_dashboard_timeout_overrides``)
|
|
959
|
+
so the first refusal is never a surprise.
|
|
960
|
+
"""
|
|
961
|
+
if not {OPERATION_SPOOL_RETENTION_SETTING, *SPOOL_RETENTION_FLOOR_INPUTS} & payload.model_fields_set:
|
|
962
|
+
return
|
|
963
|
+
before_retention = float(getattr(current, OPERATION_SPOOL_RETENTION_SETTING))
|
|
964
|
+
before_floor = operation_spool_retention_floor_seconds(current, startup_settings=startup_settings)
|
|
965
|
+
after_inputs = _ProposedSpoolRetentionInputs(payload, current, startup_settings)
|
|
966
|
+
after_retention = _proposed_spool_retention_seconds(payload, current, startup_settings)
|
|
967
|
+
term, after_floor = binding_spool_retention_floor_term(after_inputs, startup_settings=startup_settings)
|
|
968
|
+
if after_retention >= after_floor:
|
|
969
|
+
return
|
|
970
|
+
if after_retention >= before_retention and after_floor <= before_floor:
|
|
971
|
+
# Neither side got worse. Only reachable from an already-violating
|
|
972
|
+
# state: a healthy one has before_retention >= before_floor >=
|
|
973
|
+
# after_floor > after_retention >= before_retention, a contradiction.
|
|
974
|
+
return
|
|
975
|
+
if before_retention < before_floor:
|
|
976
|
+
raise DashboardBadRequestError(
|
|
977
|
+
f"{OPERATION_SPOOL_RETENTION_SETTING} is already below its replay floor "
|
|
978
|
+
f"({before_retention:g}s < {before_floor:g}s) and this change would deepen it "
|
|
979
|
+
f"({before_retention:g}s -> {after_retention:g}s against a floor of "
|
|
980
|
+
f"{before_floor:g}s -> {after_floor:g}s, bound by {term}); raise the retention to at least "
|
|
981
|
+
f"{after_floor:g}s, or leave both no worse than they are",
|
|
982
|
+
code="spool_retention_below_floor",
|
|
983
|
+
)
|
|
984
|
+
raise DashboardBadRequestError(
|
|
985
|
+
f"{OPERATION_SPOOL_RETENTION_SETTING} must be at least {after_floor:g}s: the operation spool is replayed "
|
|
986
|
+
f"for as long as {term} ({after_floor:g}s), so a shorter window deletes transcripts a recovery still needs",
|
|
987
|
+
code="spool_retention_below_floor",
|
|
988
|
+
)
|
|
989
|
+
|
|
990
|
+
|
|
991
|
+
# end R2 spool retention
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
def _proposed_reset_credit_polling_enabled(payload: DashboardSettingsUpdateRequest, current) -> bool:
|
|
995
|
+
"""M2 background jobs: the reset-credit polling toggle as it will be after this update.
|
|
996
|
+
|
|
997
|
+
A value in the payload wins; an explicit null returns to the inherited
|
|
998
|
+
environment / default value; an omitted field keeps the current effective
|
|
999
|
+
value (dashboard column, else the deprecated env alias, else the default).
|
|
1000
|
+
"""
|
|
1001
|
+
name = "rate_limit_reset_credits_refresh_enabled"
|
|
1002
|
+
if name not in payload.model_fields_set:
|
|
1003
|
+
return bool(getattr(current, name))
|
|
1004
|
+
proposed = getattr(payload, name)
|
|
1005
|
+
if proposed is not None:
|
|
1006
|
+
return bool(proposed)
|
|
1007
|
+
inherited = current.provenance[name]
|
|
1008
|
+
return bool(resolve_inheritable(None, inherited.env_value, inherited.default).value)
|
|
1009
|
+
|
|
1010
|
+
|
|
1011
|
+
def _proposed_auto_redeem_enabled(payload: DashboardSettingsUpdateRequest, current) -> bool:
|
|
1012
|
+
"""M2 background jobs: the auto-redeem opt-in as it will be after this update."""
|
|
1013
|
+
if payload.auto_redeem_reset_credits_before_expiry is None:
|
|
1014
|
+
return bool(current.auto_redeem_reset_credits_before_expiry)
|
|
1015
|
+
return bool(payload.auto_redeem_reset_credits_before_expiry)
|
|
1016
|
+
|
|
1017
|
+
|
|
1018
|
+
def _timeout_field(payload: DashboardSettingsUpdateRequest, name: str) -> tuple[float | None, bool]:
|
|
1019
|
+
"""(value to store, clear flag) for one tri-state timeout field of ``payload``."""
|
|
1020
|
+
if name not in payload.model_fields_set:
|
|
1021
|
+
return None, False
|
|
1022
|
+
value = getattr(payload, name)
|
|
1023
|
+
return value, value is None
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
# end C2-1 timeouts
|
|
1027
|
+
|
|
1028
|
+
|
|
1029
|
+
@router.put("", response_model=DashboardSettingsResponse)
|
|
1030
|
+
async def update_settings(
|
|
1031
|
+
request: Request,
|
|
1032
|
+
payload: DashboardSettingsUpdateRequest = Body(...),
|
|
1033
|
+
principal: DashboardPrincipal = Depends(require_dashboard_write_access),
|
|
1034
|
+
context: SettingsContext = Depends(get_settings_context),
|
|
1035
|
+
) -> DashboardSettingsResponse:
|
|
1036
|
+
current = await context.service.get_settings(actor_user_id=principal.user_id)
|
|
1037
|
+
# The dashboard client submits the whole form on every save, so a security
|
|
1038
|
+
# field merely being present must not require security:write; only a value
|
|
1039
|
+
# that differs from what is stored does.
|
|
1040
|
+
security_changes = {
|
|
1041
|
+
name
|
|
1042
|
+
for name in payload.model_fields_set & SECURITY_SETTINGS_FIELDS
|
|
1043
|
+
if getattr(payload, name) is not None and getattr(payload, name) != getattr(current, name)
|
|
1044
|
+
}
|
|
1045
|
+
if security_changes:
|
|
1046
|
+
ensure_dashboard_permission(principal, Permission.SECURITY_WRITE)
|
|
1047
|
+
await ensure_step_up(request, principal, Permission.SECURITY_WRITE)
|
|
1048
|
+
if payload.expected_version is not None and payload.expected_version != current.version:
|
|
1049
|
+
raise DashboardSettingsConflictError(
|
|
1050
|
+
"Settings were modified since this form was loaded; reload and retry",
|
|
1051
|
+
)
|
|
1052
|
+
if (
|
|
1053
|
+
"upstream_proxy_default_pool_id" in payload.model_fields_set
|
|
1054
|
+
and payload.upstream_proxy_default_pool_id is not None
|
|
1055
|
+
):
|
|
1056
|
+
await _validate_proxy_pool_id(context, payload.upstream_proxy_default_pool_id)
|
|
1057
|
+
# The reset-credit refresh loop is the sole driver of automatic redemption,
|
|
1058
|
+
# so "auto-redeem on, polling off" is a setting that can never run. The gate
|
|
1059
|
+
# gets the effective (dashboard-aware) values this request would leave
|
|
1060
|
+
# behind and is symmetric: it refuses both the request that turns the
|
|
1061
|
+
# opt-in on and the one that turns polling off. A payload that only
|
|
1062
|
+
# re-saves an already inconsistent pair is still accepted, so unrelated
|
|
1063
|
+
# settings edits are never blocked by pre-existing state.
|
|
1064
|
+
proposed_polling_enabled = _proposed_reset_credit_polling_enabled(payload, current)
|
|
1065
|
+
proposed_auto_redeem_enabled = _proposed_auto_redeem_enabled(payload, current)
|
|
1066
|
+
if proposed_auto_redeem_enabled and not proposed_polling_enabled:
|
|
1067
|
+
enables_auto_redeem = not current.auto_redeem_reset_credits_before_expiry
|
|
1068
|
+
disables_polling = (
|
|
1069
|
+
"rate_limit_reset_credits_refresh_enabled" in payload.model_fields_set
|
|
1070
|
+
and current.rate_limit_reset_credits_refresh_enabled
|
|
1071
|
+
)
|
|
1072
|
+
if enables_auto_redeem or disables_polling:
|
|
1073
|
+
raise DashboardBadRequestError(
|
|
1074
|
+
"autoRedeemResetCreditsBeforeExpiry requires reset-credit polling; "
|
|
1075
|
+
"keep rateLimitResetCreditsRefreshEnabled on (Settings -> Advanced -> Background jobs), "
|
|
1076
|
+
"or turn the opt-in off in the same request",
|
|
1077
|
+
code="reset_credit_polling_disabled",
|
|
1078
|
+
)
|
|
1079
|
+
try:
|
|
1080
|
+
legacy_threshold_provided = payload.sticky_reallocation_budget_threshold_pct is not None
|
|
1081
|
+
primary_threshold_provided = payload.sticky_reallocation_primary_budget_threshold_pct is not None
|
|
1082
|
+
if legacy_threshold_provided and primary_threshold_provided:
|
|
1083
|
+
assert payload.sticky_reallocation_budget_threshold_pct is not None
|
|
1084
|
+
assert payload.sticky_reallocation_primary_budget_threshold_pct is not None
|
|
1085
|
+
if (
|
|
1086
|
+
payload.sticky_reallocation_budget_threshold_pct
|
|
1087
|
+
!= payload.sticky_reallocation_primary_budget_threshold_pct
|
|
1088
|
+
and (
|
|
1089
|
+
payload.sticky_reallocation_budget_threshold_pct != current.sticky_reallocation_budget_threshold_pct
|
|
1090
|
+
or payload.sticky_reallocation_primary_budget_threshold_pct
|
|
1091
|
+
!= current.sticky_reallocation_primary_budget_threshold_pct
|
|
1092
|
+
)
|
|
1093
|
+
):
|
|
1094
|
+
raise DashboardBadRequestError(
|
|
1095
|
+
"stickyReallocationBudgetThresholdPct and "
|
|
1096
|
+
"stickyReallocationPrimaryBudgetThresholdPct must match when both are provided",
|
|
1097
|
+
code="conflicting_sticky_reallocation_thresholds",
|
|
1098
|
+
)
|
|
1099
|
+
|
|
1100
|
+
resolved_primary_threshold = (
|
|
1101
|
+
payload.sticky_reallocation_primary_budget_threshold_pct
|
|
1102
|
+
if payload.sticky_reallocation_primary_budget_threshold_pct is not None
|
|
1103
|
+
else (
|
|
1104
|
+
payload.sticky_reallocation_budget_threshold_pct
|
|
1105
|
+
if payload.sticky_reallocation_budget_threshold_pct is not None
|
|
1106
|
+
else current.sticky_reallocation_primary_budget_threshold_pct
|
|
1107
|
+
)
|
|
1108
|
+
)
|
|
1109
|
+
resolved_legacy_threshold = (
|
|
1110
|
+
payload.sticky_reallocation_budget_threshold_pct
|
|
1111
|
+
if payload.sticky_reallocation_budget_threshold_pct is not None
|
|
1112
|
+
else resolved_primary_threshold
|
|
1113
|
+
)
|
|
1114
|
+
single_account_id = (
|
|
1115
|
+
payload.single_account_id if "single_account_id" in payload.model_fields_set else current.single_account_id
|
|
1116
|
+
)
|
|
1117
|
+
startup_settings = get_app_settings()
|
|
1118
|
+
stream_limit = (
|
|
1119
|
+
payload.proxy_account_stream_limit
|
|
1120
|
+
if payload.proxy_account_stream_limit is not None
|
|
1121
|
+
else startup_settings.proxy_account_stream_limit
|
|
1122
|
+
if "proxy_account_stream_limit" in payload.model_fields_set
|
|
1123
|
+
else current.proxy_account_stream_limit
|
|
1124
|
+
)
|
|
1125
|
+
stream_recovery_reserve = (
|
|
1126
|
+
payload.proxy_account_stream_recovery_reserve
|
|
1127
|
+
if payload.proxy_account_stream_recovery_reserve is not None
|
|
1128
|
+
else startup_settings.proxy_account_stream_recovery_reserve
|
|
1129
|
+
if "proxy_account_stream_recovery_reserve" in payload.model_fields_set
|
|
1130
|
+
else current.proxy_account_stream_recovery_reserve
|
|
1131
|
+
)
|
|
1132
|
+
cap_fields_changed = bool(
|
|
1133
|
+
{
|
|
1134
|
+
"proxy_account_stream_limit",
|
|
1135
|
+
"proxy_account_stream_recovery_reserve",
|
|
1136
|
+
}
|
|
1137
|
+
& payload.model_fields_set
|
|
1138
|
+
)
|
|
1139
|
+
if cap_fields_changed and stream_limit > 0 and stream_recovery_reserve > stream_limit:
|
|
1140
|
+
raise DashboardBadRequestError(
|
|
1141
|
+
"proxyAccountStreamRecoveryReserve must not exceed proxyAccountStreamLimit",
|
|
1142
|
+
code="invalid_proxy_account_stream_recovery_reserve",
|
|
1143
|
+
)
|
|
1144
|
+
_validate_timeout_invariants(payload, current, startup_settings) # C2-1 timeouts + C2-2 lease TTL
|
|
1145
|
+
_validate_spool_retention_floor(payload, current, startup_settings) # R2 spool retention
|
|
1146
|
+
timeout_fields = {name: _timeout_field(payload, name) for name in DASHBOARD_TIMEOUT_SETTINGS}
|
|
1147
|
+
updated = await context.service.update_settings(
|
|
1148
|
+
DashboardSettingsUpdateData(
|
|
1149
|
+
sticky_threads_enabled=(
|
|
1150
|
+
payload.sticky_threads_enabled
|
|
1151
|
+
if payload.sticky_threads_enabled is not None
|
|
1152
|
+
else current.sticky_threads_enabled
|
|
1153
|
+
),
|
|
1154
|
+
upstream_stream_transport=payload.upstream_stream_transport or current.upstream_stream_transport,
|
|
1155
|
+
prohibit_fast_mode=(
|
|
1156
|
+
payload.prohibit_fast_mode if payload.prohibit_fast_mode is not None else current.prohibit_fast_mode
|
|
1157
|
+
),
|
|
1158
|
+
http_downstream_transport_policy=(
|
|
1159
|
+
payload.http_downstream_transport_policy or current.http_downstream_transport_policy
|
|
1160
|
+
),
|
|
1161
|
+
thread_cache_identity_mode=_dashboard_value(payload, "thread_cache_identity_mode"),
|
|
1162
|
+
clear_thread_cache_identity_mode=_clears_dashboard_value(payload, "thread_cache_identity_mode"),
|
|
1163
|
+
proxy_account_response_create_limit=(
|
|
1164
|
+
payload.proxy_account_response_create_limit
|
|
1165
|
+
if "proxy_account_response_create_limit" in payload.model_fields_set
|
|
1166
|
+
else None
|
|
1167
|
+
),
|
|
1168
|
+
clear_proxy_account_response_create_limit=(
|
|
1169
|
+
"proxy_account_response_create_limit" in payload.model_fields_set
|
|
1170
|
+
and payload.proxy_account_response_create_limit is None
|
|
1171
|
+
),
|
|
1172
|
+
proxy_account_stream_limit=(
|
|
1173
|
+
payload.proxy_account_stream_limit
|
|
1174
|
+
if "proxy_account_stream_limit" in payload.model_fields_set
|
|
1175
|
+
else None
|
|
1176
|
+
),
|
|
1177
|
+
clear_proxy_account_stream_limit=(
|
|
1178
|
+
"proxy_account_stream_limit" in payload.model_fields_set
|
|
1179
|
+
and payload.proxy_account_stream_limit is None
|
|
1180
|
+
),
|
|
1181
|
+
proxy_account_stream_recovery_reserve=(
|
|
1182
|
+
payload.proxy_account_stream_recovery_reserve
|
|
1183
|
+
if "proxy_account_stream_recovery_reserve" in payload.model_fields_set
|
|
1184
|
+
else None
|
|
1185
|
+
),
|
|
1186
|
+
clear_proxy_account_stream_recovery_reserve=(
|
|
1187
|
+
"proxy_account_stream_recovery_reserve" in payload.model_fields_set
|
|
1188
|
+
and payload.proxy_account_stream_recovery_reserve is None
|
|
1189
|
+
),
|
|
1190
|
+
proxy_api_key_fair_share_congestion_threshold_pct=(
|
|
1191
|
+
payload.proxy_api_key_fair_share_congestion_threshold_pct
|
|
1192
|
+
if "proxy_api_key_fair_share_congestion_threshold_pct" in payload.model_fields_set
|
|
1193
|
+
else None
|
|
1194
|
+
),
|
|
1195
|
+
clear_proxy_api_key_fair_share_congestion_threshold_pct=(
|
|
1196
|
+
"proxy_api_key_fair_share_congestion_threshold_pct" in payload.model_fields_set
|
|
1197
|
+
and payload.proxy_api_key_fair_share_congestion_threshold_pct is None
|
|
1198
|
+
),
|
|
1199
|
+
# C2-2 routing/overload
|
|
1200
|
+
proxy_overload_isolation_seconds=_dashboard_value(payload, "proxy_overload_isolation_seconds"),
|
|
1201
|
+
clear_proxy_overload_isolation_seconds=_clears_dashboard_value(
|
|
1202
|
+
payload, "proxy_overload_isolation_seconds"
|
|
1203
|
+
),
|
|
1204
|
+
proxy_account_error_rate_weighting_enabled=_dashboard_value(
|
|
1205
|
+
payload, "proxy_account_error_rate_weighting_enabled"
|
|
1206
|
+
),
|
|
1207
|
+
clear_proxy_account_error_rate_weighting_enabled=_clears_dashboard_value(
|
|
1208
|
+
payload, "proxy_account_error_rate_weighting_enabled"
|
|
1209
|
+
),
|
|
1210
|
+
proxy_account_inflight_penalty_pct=_dashboard_value(payload, "proxy_account_inflight_penalty_pct"),
|
|
1211
|
+
clear_proxy_account_inflight_penalty_pct=_clears_dashboard_value(
|
|
1212
|
+
payload, "proxy_account_inflight_penalty_pct"
|
|
1213
|
+
),
|
|
1214
|
+
proxy_account_lease_token_weight=_dashboard_value(payload, "proxy_account_lease_token_weight"),
|
|
1215
|
+
clear_proxy_account_lease_token_weight=_clears_dashboard_value(
|
|
1216
|
+
payload, "proxy_account_lease_token_weight"
|
|
1217
|
+
),
|
|
1218
|
+
proxy_account_lease_ttl_seconds=_dashboard_value(payload, "proxy_account_lease_ttl_seconds"),
|
|
1219
|
+
clear_proxy_account_lease_ttl_seconds=_clears_dashboard_value(
|
|
1220
|
+
payload, "proxy_account_lease_ttl_seconds"
|
|
1221
|
+
),
|
|
1222
|
+
# end C2-2 routing/overload
|
|
1223
|
+
upstream_proxy_routing_enabled=(
|
|
1224
|
+
payload.upstream_proxy_routing_enabled
|
|
1225
|
+
if payload.upstream_proxy_routing_enabled is not None
|
|
1226
|
+
else current.upstream_proxy_routing_enabled
|
|
1227
|
+
),
|
|
1228
|
+
upstream_proxy_default_pool_id=(
|
|
1229
|
+
payload.upstream_proxy_default_pool_id
|
|
1230
|
+
if "upstream_proxy_default_pool_id" in payload.model_fields_set
|
|
1231
|
+
else current.upstream_proxy_default_pool_id
|
|
1232
|
+
),
|
|
1233
|
+
prefer_earlier_reset_accounts=(
|
|
1234
|
+
payload.prefer_earlier_reset_accounts
|
|
1235
|
+
if payload.prefer_earlier_reset_accounts is not None
|
|
1236
|
+
else current.prefer_earlier_reset_accounts
|
|
1237
|
+
),
|
|
1238
|
+
prefer_earlier_reset_window=payload.prefer_earlier_reset_window or current.prefer_earlier_reset_window,
|
|
1239
|
+
show_reset_credit_badges=(
|
|
1240
|
+
payload.show_reset_credit_badges
|
|
1241
|
+
if payload.show_reset_credit_badges is not None
|
|
1242
|
+
else current.show_reset_credit_badges
|
|
1243
|
+
),
|
|
1244
|
+
auto_redeem_reset_credits_before_expiry=(
|
|
1245
|
+
payload.auto_redeem_reset_credits_before_expiry
|
|
1246
|
+
if payload.auto_redeem_reset_credits_before_expiry is not None
|
|
1247
|
+
else current.auto_redeem_reset_credits_before_expiry
|
|
1248
|
+
),
|
|
1249
|
+
show_reset_credit_expiry_badge=(
|
|
1250
|
+
payload.show_reset_credit_expiry_badge
|
|
1251
|
+
if payload.show_reset_credit_expiry_badge is not None
|
|
1252
|
+
else current.show_reset_credit_expiry_badge
|
|
1253
|
+
),
|
|
1254
|
+
routing_strategy=payload.routing_strategy or current.routing_strategy,
|
|
1255
|
+
relative_availability_power=(
|
|
1256
|
+
payload.relative_availability_power
|
|
1257
|
+
if payload.relative_availability_power is not None
|
|
1258
|
+
else current.relative_availability_power
|
|
1259
|
+
),
|
|
1260
|
+
relative_availability_top_k=(
|
|
1261
|
+
payload.relative_availability_top_k
|
|
1262
|
+
if payload.relative_availability_top_k is not None
|
|
1263
|
+
else current.relative_availability_top_k
|
|
1264
|
+
),
|
|
1265
|
+
single_account_id=single_account_id,
|
|
1266
|
+
openai_cache_affinity_max_age_seconds=(
|
|
1267
|
+
payload.openai_cache_affinity_max_age_seconds
|
|
1268
|
+
if payload.openai_cache_affinity_max_age_seconds is not None
|
|
1269
|
+
else current.openai_cache_affinity_max_age_seconds
|
|
1270
|
+
),
|
|
1271
|
+
dashboard_session_ttl_seconds=(
|
|
1272
|
+
payload.dashboard_session_ttl_seconds
|
|
1273
|
+
if payload.dashboard_session_ttl_seconds is not None
|
|
1274
|
+
else current.dashboard_session_ttl_seconds
|
|
1275
|
+
),
|
|
1276
|
+
http_responses_session_bridge_prompt_cache_idle_ttl_seconds=(
|
|
1277
|
+
payload.http_responses_session_bridge_prompt_cache_idle_ttl_seconds
|
|
1278
|
+
if payload.http_responses_session_bridge_prompt_cache_idle_ttl_seconds is not None
|
|
1279
|
+
else current.http_responses_session_bridge_prompt_cache_idle_ttl_seconds
|
|
1280
|
+
),
|
|
1281
|
+
http_responses_session_bridge_gateway_safe_mode=(
|
|
1282
|
+
payload.http_responses_session_bridge_gateway_safe_mode
|
|
1283
|
+
if payload.http_responses_session_bridge_gateway_safe_mode is not None
|
|
1284
|
+
else current.http_responses_session_bridge_gateway_safe_mode
|
|
1285
|
+
),
|
|
1286
|
+
# M3 codex prewarm: tri-state via model_fields_set.
|
|
1287
|
+
http_responses_session_bridge_codex_prewarm_enabled=_dashboard_value(
|
|
1288
|
+
payload, "http_responses_session_bridge_codex_prewarm_enabled"
|
|
1289
|
+
),
|
|
1290
|
+
clear_http_responses_session_bridge_codex_prewarm_enabled=_clears_dashboard_value(
|
|
1291
|
+
payload, "http_responses_session_bridge_codex_prewarm_enabled"
|
|
1292
|
+
),
|
|
1293
|
+
# end M3 codex prewarm
|
|
1294
|
+
sticky_reallocation_budget_threshold_pct=resolved_legacy_threshold,
|
|
1295
|
+
sticky_reallocation_primary_budget_threshold_pct=resolved_primary_threshold,
|
|
1296
|
+
sticky_reallocation_secondary_budget_threshold_pct=(
|
|
1297
|
+
payload.sticky_reallocation_secondary_budget_threshold_pct
|
|
1298
|
+
if payload.sticky_reallocation_secondary_budget_threshold_pct is not None
|
|
1299
|
+
else current.sticky_reallocation_secondary_budget_threshold_pct
|
|
1300
|
+
),
|
|
1301
|
+
additional_quota_routing_policies=(
|
|
1302
|
+
payload.additional_quota_routing_policies
|
|
1303
|
+
if payload.additional_quota_routing_policies is not None
|
|
1304
|
+
else current.additional_quota_routing_policies
|
|
1305
|
+
),
|
|
1306
|
+
warmup_model=(payload.warmup_model if payload.warmup_model is not None else current.warmup_model),
|
|
1307
|
+
import_without_overwrite=(
|
|
1308
|
+
payload.import_without_overwrite
|
|
1309
|
+
if payload.import_without_overwrite is not None
|
|
1310
|
+
else current.import_without_overwrite
|
|
1311
|
+
),
|
|
1312
|
+
totp_required_on_login=(
|
|
1313
|
+
payload.totp_required_on_login
|
|
1314
|
+
if payload.totp_required_on_login is not None
|
|
1315
|
+
else current.totp_required_on_login
|
|
1316
|
+
),
|
|
1317
|
+
totp_required_for_admin_role=(
|
|
1318
|
+
payload.totp_required_for_admin_role
|
|
1319
|
+
if payload.totp_required_for_admin_role is not None
|
|
1320
|
+
else current.totp_required_for_admin_role
|
|
1321
|
+
),
|
|
1322
|
+
local_login_policy=(
|
|
1323
|
+
payload.local_login_policy if payload.local_login_policy is not None else current.local_login_policy
|
|
1324
|
+
),
|
|
1325
|
+
api_key_auth_enabled=(
|
|
1326
|
+
payload.api_key_auth_enabled
|
|
1327
|
+
if payload.api_key_auth_enabled is not None
|
|
1328
|
+
else current.api_key_auth_enabled
|
|
1329
|
+
),
|
|
1330
|
+
hide_upstream_quota_from_api_keys=(
|
|
1331
|
+
payload.hide_upstream_quota_from_api_keys
|
|
1332
|
+
if payload.hide_upstream_quota_from_api_keys is not None
|
|
1333
|
+
else current.hide_upstream_quota_from_api_keys
|
|
1334
|
+
),
|
|
1335
|
+
limit_warmup_enabled=(
|
|
1336
|
+
payload.limit_warmup_enabled
|
|
1337
|
+
if payload.limit_warmup_enabled is not None
|
|
1338
|
+
else current.limit_warmup_enabled
|
|
1339
|
+
),
|
|
1340
|
+
limit_warmup_windows=payload.limit_warmup_windows or current.limit_warmup_windows,
|
|
1341
|
+
limit_warmup_model=payload.limit_warmup_model or current.limit_warmup_model,
|
|
1342
|
+
limit_warmup_prompt=payload.limit_warmup_prompt or current.limit_warmup_prompt,
|
|
1343
|
+
limit_warmup_cooldown_seconds=(
|
|
1344
|
+
payload.limit_warmup_cooldown_seconds
|
|
1345
|
+
if payload.limit_warmup_cooldown_seconds is not None
|
|
1346
|
+
else current.limit_warmup_cooldown_seconds
|
|
1347
|
+
),
|
|
1348
|
+
limit_warmup_exhausted_threshold_percent=(
|
|
1349
|
+
payload.limit_warmup_exhausted_threshold_percent
|
|
1350
|
+
if payload.limit_warmup_exhausted_threshold_percent is not None
|
|
1351
|
+
else current.limit_warmup_exhausted_threshold_percent
|
|
1352
|
+
),
|
|
1353
|
+
limit_warmup_idle_threshold_percent=(
|
|
1354
|
+
payload.limit_warmup_idle_threshold_percent
|
|
1355
|
+
if payload.limit_warmup_idle_threshold_percent is not None
|
|
1356
|
+
else current.limit_warmup_idle_threshold_percent
|
|
1357
|
+
),
|
|
1358
|
+
limit_warmup_min_available_percent=(
|
|
1359
|
+
payload.limit_warmup_min_available_percent
|
|
1360
|
+
if payload.limit_warmup_min_available_percent is not None
|
|
1361
|
+
else current.limit_warmup_min_available_percent
|
|
1362
|
+
),
|
|
1363
|
+
weekly_pace_working_days=(
|
|
1364
|
+
payload.weekly_pace_working_days
|
|
1365
|
+
if payload.weekly_pace_working_days is not None
|
|
1366
|
+
else current.weekly_pace_working_days
|
|
1367
|
+
),
|
|
1368
|
+
weekly_pace_smoothing_minutes=(
|
|
1369
|
+
payload.weekly_pace_smoothing_minutes
|
|
1370
|
+
if payload.weekly_pace_smoothing_minutes is not None
|
|
1371
|
+
else current.weekly_pace_smoothing_minutes
|
|
1372
|
+
),
|
|
1373
|
+
guest_access_enabled=(
|
|
1374
|
+
payload.guest_access_enabled
|
|
1375
|
+
if payload.guest_access_enabled is not None
|
|
1376
|
+
else current.guest_access_enabled
|
|
1377
|
+
),
|
|
1378
|
+
limit_warmup_staggered_idle_enabled=(
|
|
1379
|
+
payload.limit_warmup_staggered_idle_enabled
|
|
1380
|
+
if payload.limit_warmup_staggered_idle_enabled is not None
|
|
1381
|
+
else current.limit_warmup_staggered_idle_enabled
|
|
1382
|
+
),
|
|
1383
|
+
request_log_retention_override_days=(
|
|
1384
|
+
payload.request_log_retention_override_days
|
|
1385
|
+
if "request_log_retention_override_days" in payload.model_fields_set
|
|
1386
|
+
else None
|
|
1387
|
+
),
|
|
1388
|
+
usage_history_retention_override_days=(
|
|
1389
|
+
payload.usage_history_retention_override_days
|
|
1390
|
+
if "usage_history_retention_override_days" in payload.model_fields_set
|
|
1391
|
+
else None
|
|
1392
|
+
),
|
|
1393
|
+
clear_request_log_retention_override=(
|
|
1394
|
+
"request_log_retention_override_days" in payload.model_fields_set
|
|
1395
|
+
and payload.request_log_retention_override_days is None
|
|
1396
|
+
),
|
|
1397
|
+
clear_usage_history_retention_override=(
|
|
1398
|
+
"usage_history_retention_override_days" in payload.model_fields_set
|
|
1399
|
+
and payload.usage_history_retention_override_days is None
|
|
1400
|
+
),
|
|
1401
|
+
# C2-3 resilience toggles: tri-state via model_fields_set.
|
|
1402
|
+
soft_drain_enabled=(
|
|
1403
|
+
payload.soft_drain_enabled if "soft_drain_enabled" in payload.model_fields_set else None
|
|
1404
|
+
),
|
|
1405
|
+
clear_soft_drain_enabled=(
|
|
1406
|
+
"soft_drain_enabled" in payload.model_fields_set and payload.soft_drain_enabled is None
|
|
1407
|
+
),
|
|
1408
|
+
deterministic_failover_enabled=(
|
|
1409
|
+
payload.deterministic_failover_enabled
|
|
1410
|
+
if "deterministic_failover_enabled" in payload.model_fields_set
|
|
1411
|
+
else None
|
|
1412
|
+
),
|
|
1413
|
+
clear_deterministic_failover_enabled=(
|
|
1414
|
+
"deterministic_failover_enabled" in payload.model_fields_set
|
|
1415
|
+
and payload.deterministic_failover_enabled is None
|
|
1416
|
+
),
|
|
1417
|
+
circuit_breaker_enabled=(
|
|
1418
|
+
payload.circuit_breaker_enabled if "circuit_breaker_enabled" in payload.model_fields_set else None
|
|
1419
|
+
),
|
|
1420
|
+
clear_circuit_breaker_enabled=(
|
|
1421
|
+
"circuit_breaker_enabled" in payload.model_fields_set and payload.circuit_breaker_enabled is None
|
|
1422
|
+
),
|
|
1423
|
+
# M2 background jobs: tri-state via model_fields_set.
|
|
1424
|
+
auth_guardian_enabled=_dashboard_value(payload, "auth_guardian_enabled"),
|
|
1425
|
+
clear_auth_guardian_enabled=_clears_dashboard_value(payload, "auth_guardian_enabled"),
|
|
1426
|
+
automations_scheduler_enabled=_dashboard_value(payload, "automations_scheduler_enabled"),
|
|
1427
|
+
clear_automations_scheduler_enabled=_clears_dashboard_value(payload, "automations_scheduler_enabled"),
|
|
1428
|
+
rate_limit_reset_credits_refresh_enabled=_dashboard_value(
|
|
1429
|
+
payload, "rate_limit_reset_credits_refresh_enabled"
|
|
1430
|
+
),
|
|
1431
|
+
clear_rate_limit_reset_credits_refresh_enabled=_clears_dashboard_value(
|
|
1432
|
+
payload, "rate_limit_reset_credits_refresh_enabled"
|
|
1433
|
+
),
|
|
1434
|
+
# end M2 background jobs
|
|
1435
|
+
# M5 conversation archive: tri-state via model_fields_set.
|
|
1436
|
+
conversation_archive_enabled=(
|
|
1437
|
+
payload.conversation_archive_enabled
|
|
1438
|
+
if CONVERSATION_ARCHIVE_SETTING in payload.model_fields_set
|
|
1439
|
+
else None
|
|
1440
|
+
),
|
|
1441
|
+
clear_conversation_archive_enabled=(
|
|
1442
|
+
CONVERSATION_ARCHIVE_SETTING in payload.model_fields_set
|
|
1443
|
+
and payload.conversation_archive_enabled is None
|
|
1444
|
+
),
|
|
1445
|
+
# end M5 conversation archive
|
|
1446
|
+
# R2 spool retention: tri-state via model_fields_set.
|
|
1447
|
+
http_responses_session_bridge_operation_spool_retention_seconds=_dashboard_value(
|
|
1448
|
+
payload, OPERATION_SPOOL_RETENTION_SETTING
|
|
1449
|
+
),
|
|
1450
|
+
clear_http_responses_session_bridge_operation_spool_retention_seconds=_clears_dashboard_value(
|
|
1451
|
+
payload, OPERATION_SPOOL_RETENTION_SETTING
|
|
1452
|
+
),
|
|
1453
|
+
# end R2 spool retention
|
|
1454
|
+
# C2-1 timeouts
|
|
1455
|
+
upstream_connect_timeout_seconds=timeout_fields["upstream_connect_timeout_seconds"][0],
|
|
1456
|
+
clear_upstream_connect_timeout_seconds=timeout_fields["upstream_connect_timeout_seconds"][1],
|
|
1457
|
+
proxy_request_budget_seconds=timeout_fields["proxy_request_budget_seconds"][0],
|
|
1458
|
+
clear_proxy_request_budget_seconds=timeout_fields["proxy_request_budget_seconds"][1],
|
|
1459
|
+
compact_request_budget_seconds=timeout_fields["compact_request_budget_seconds"][0],
|
|
1460
|
+
clear_compact_request_budget_seconds=timeout_fields["compact_request_budget_seconds"][1],
|
|
1461
|
+
transcription_request_budget_seconds=timeout_fields["transcription_request_budget_seconds"][0],
|
|
1462
|
+
clear_transcription_request_budget_seconds=timeout_fields["transcription_request_budget_seconds"][1],
|
|
1463
|
+
stream_idle_timeout_seconds=timeout_fields["stream_idle_timeout_seconds"][0],
|
|
1464
|
+
clear_stream_idle_timeout_seconds=timeout_fields["stream_idle_timeout_seconds"][1],
|
|
1465
|
+
proxy_downstream_websocket_idle_timeout_seconds=timeout_fields[
|
|
1466
|
+
"proxy_downstream_websocket_idle_timeout_seconds"
|
|
1467
|
+
][0],
|
|
1468
|
+
clear_proxy_downstream_websocket_idle_timeout_seconds=timeout_fields[
|
|
1469
|
+
"proxy_downstream_websocket_idle_timeout_seconds"
|
|
1470
|
+
][1],
|
|
1471
|
+
sse_keepalive_interval_seconds=timeout_fields["sse_keepalive_interval_seconds"][0],
|
|
1472
|
+
clear_sse_keepalive_interval_seconds=timeout_fields["sse_keepalive_interval_seconds"][1],
|
|
1473
|
+
# end C2-1 timeouts
|
|
1474
|
+
# M1 stream/bridge budgets (registered in DASHBOARD_TIMEOUT_SETTINGS,
|
|
1475
|
+
# so the PUT-time invariant check and the audit loop cover them).
|
|
1476
|
+
http_responses_stream_request_budget_seconds=timeout_fields[
|
|
1477
|
+
"http_responses_stream_request_budget_seconds"
|
|
1478
|
+
][0],
|
|
1479
|
+
clear_http_responses_stream_request_budget_seconds=timeout_fields[
|
|
1480
|
+
"http_responses_stream_request_budget_seconds"
|
|
1481
|
+
][1],
|
|
1482
|
+
http_responses_session_bridge_request_budget_seconds=timeout_fields[
|
|
1483
|
+
"http_responses_session_bridge_request_budget_seconds"
|
|
1484
|
+
][0],
|
|
1485
|
+
clear_http_responses_session_bridge_request_budget_seconds=timeout_fields[
|
|
1486
|
+
"http_responses_session_bridge_request_budget_seconds"
|
|
1487
|
+
][1],
|
|
1488
|
+
# end M1 stream/bridge budgets
|
|
1489
|
+
),
|
|
1490
|
+
# CAS anchor: omitted fields above were merged from `current`
|
|
1491
|
+
# (version checked against expectedVersion when supplied), so the
|
|
1492
|
+
# repository must apply the UPDATE only if the row still carries
|
|
1493
|
+
# that version; a writer committing in between yields 409 instead
|
|
1494
|
+
# of silently reverting its fields.
|
|
1495
|
+
actor_user_id=principal.user_id,
|
|
1496
|
+
expected_version=current.version,
|
|
1497
|
+
)
|
|
1498
|
+
except BreakGlassRequiresTotpError as exc:
|
|
1499
|
+
# Before the generic ValueError arm: this refusal is a 409 whose body
|
|
1500
|
+
# names the account that would clear it, not a malformed-input 400.
|
|
1501
|
+
raise DashboardConflictError(
|
|
1502
|
+
str(exc),
|
|
1503
|
+
code="break_glass_requires_totp",
|
|
1504
|
+
param=exc.username,
|
|
1505
|
+
details={"username": exc.username},
|
|
1506
|
+
) from exc
|
|
1507
|
+
except ValueError as exc:
|
|
1508
|
+
raise DashboardBadRequestError(str(exc), code="invalid_totp_config") from exc
|
|
1509
|
+
|
|
1510
|
+
upstream_route_inputs_changed = (
|
|
1511
|
+
current.upstream_proxy_routing_enabled != updated.upstream_proxy_routing_enabled
|
|
1512
|
+
or current.upstream_proxy_default_pool_id != updated.upstream_proxy_default_pool_id
|
|
1513
|
+
)
|
|
1514
|
+
# ``SettingsRepository.commit_refresh`` already cleared the route cache
|
|
1515
|
+
# synchronously between the commit and its refresh await (no concurrent
|
|
1516
|
+
# request can see the committed row alongside the stale cache); only the
|
|
1517
|
+
# durable cross-replica signals remain here.
|
|
1518
|
+
await get_settings_cache().invalidate()
|
|
1519
|
+
changed_fields = [
|
|
1520
|
+
field_name
|
|
1521
|
+
for field_name in (
|
|
1522
|
+
"sticky_threads_enabled",
|
|
1523
|
+
"upstream_stream_transport",
|
|
1524
|
+
"prohibit_fast_mode",
|
|
1525
|
+
"http_downstream_transport_policy",
|
|
1526
|
+
"thread_cache_identity_mode",
|
|
1527
|
+
"proxy_account_response_create_limit",
|
|
1528
|
+
"proxy_account_stream_limit",
|
|
1529
|
+
"proxy_account_stream_recovery_reserve",
|
|
1530
|
+
"proxy_api_key_fair_share_congestion_threshold_pct",
|
|
1531
|
+
*_ROUTING_OVERLOAD_SETTINGS, # C2-2 routing/overload
|
|
1532
|
+
"upstream_proxy_routing_enabled",
|
|
1533
|
+
"upstream_proxy_default_pool_id",
|
|
1534
|
+
"prefer_earlier_reset_accounts",
|
|
1535
|
+
"prefer_earlier_reset_window",
|
|
1536
|
+
"show_reset_credit_badges",
|
|
1537
|
+
"auto_redeem_reset_credits_before_expiry",
|
|
1538
|
+
"show_reset_credit_expiry_badge",
|
|
1539
|
+
"routing_strategy",
|
|
1540
|
+
"relative_availability_power",
|
|
1541
|
+
"relative_availability_top_k",
|
|
1542
|
+
"single_account_id",
|
|
1543
|
+
"openai_cache_affinity_max_age_seconds",
|
|
1544
|
+
"dashboard_session_ttl_seconds",
|
|
1545
|
+
"http_responses_session_bridge_prompt_cache_idle_ttl_seconds",
|
|
1546
|
+
"http_responses_session_bridge_gateway_safe_mode",
|
|
1547
|
+
"http_responses_session_bridge_codex_prewarm_enabled", # M3 codex prewarm
|
|
1548
|
+
"sticky_reallocation_budget_threshold_pct",
|
|
1549
|
+
"sticky_reallocation_primary_budget_threshold_pct",
|
|
1550
|
+
"sticky_reallocation_secondary_budget_threshold_pct",
|
|
1551
|
+
"additional_quota_routing_policies",
|
|
1552
|
+
"warmup_model",
|
|
1553
|
+
"import_without_overwrite",
|
|
1554
|
+
"totp_required_on_login",
|
|
1555
|
+
"totp_required_for_admin_role",
|
|
1556
|
+
"local_login_policy",
|
|
1557
|
+
"api_key_auth_enabled",
|
|
1558
|
+
"hide_upstream_quota_from_api_keys",
|
|
1559
|
+
"limit_warmup_enabled",
|
|
1560
|
+
"limit_warmup_windows",
|
|
1561
|
+
"limit_warmup_model",
|
|
1562
|
+
"limit_warmup_prompt",
|
|
1563
|
+
"limit_warmup_cooldown_seconds",
|
|
1564
|
+
"limit_warmup_exhausted_threshold_percent",
|
|
1565
|
+
"limit_warmup_idle_threshold_percent",
|
|
1566
|
+
"limit_warmup_min_available_percent",
|
|
1567
|
+
"weekly_pace_working_days",
|
|
1568
|
+
"weekly_pace_smoothing_minutes",
|
|
1569
|
+
"guest_access_enabled",
|
|
1570
|
+
"limit_warmup_staggered_idle_enabled",
|
|
1571
|
+
"request_log_retention_override_days",
|
|
1572
|
+
"usage_history_retention_override_days",
|
|
1573
|
+
# C2-3 resilience toggles
|
|
1574
|
+
"soft_drain_enabled",
|
|
1575
|
+
"deterministic_failover_enabled",
|
|
1576
|
+
"circuit_breaker_enabled",
|
|
1577
|
+
*BACKGROUND_JOB_SETTINGS, # M2 background jobs
|
|
1578
|
+
CONVERSATION_ARCHIVE_SETTING, # M5 conversation archive
|
|
1579
|
+
OPERATION_SPOOL_RETENTION_SETTING, # R2 spool retention
|
|
1580
|
+
*DASHBOARD_TIMEOUT_SETTINGS, # C2-1 timeouts
|
|
1581
|
+
)
|
|
1582
|
+
if getattr(current, field_name) != getattr(updated, field_name)
|
|
1583
|
+
]
|
|
1584
|
+
# C2-3 resilience toggles / M2 background jobs / M5 conversation archive:
|
|
1585
|
+
# storing the inherited value (or clearing it) changes ownership without
|
|
1586
|
+
# changing the effective value; audit that too. An effective archive flip
|
|
1587
|
+
# additionally gets its own audit line naming the actor (further below).
|
|
1588
|
+
for field_name in (
|
|
1589
|
+
*RESILIENCE_TOGGLE_SETTINGS,
|
|
1590
|
+
*BACKGROUND_JOB_SETTINGS,
|
|
1591
|
+
CONVERSATION_ARCHIVE_SETTING,
|
|
1592
|
+
OPERATION_SPOOL_RETENTION_SETTING, # R2 spool retention
|
|
1593
|
+
):
|
|
1594
|
+
if current.provenance[field_name] != updated.provenance[field_name] and field_name not in changed_fields:
|
|
1595
|
+
changed_fields.append(field_name)
|
|
1596
|
+
# C2-1 timeouts: a dashboard value equal to the inherited one still changes
|
|
1597
|
+
# the setting's owner (provenance source), which the audit log must record.
|
|
1598
|
+
for field_name in DASHBOARD_TIMEOUT_SETTINGS:
|
|
1599
|
+
if (
|
|
1600
|
+
field_name not in changed_fields
|
|
1601
|
+
and current.provenance[field_name].source != updated.provenance[field_name].source
|
|
1602
|
+
):
|
|
1603
|
+
changed_fields.append(field_name)
|
|
1604
|
+
capacity_override_fields = (
|
|
1605
|
+
"proxy_account_response_create_limit",
|
|
1606
|
+
"proxy_account_stream_limit",
|
|
1607
|
+
"proxy_account_stream_recovery_reserve",
|
|
1608
|
+
"proxy_api_key_fair_share_congestion_threshold_pct",
|
|
1609
|
+
)
|
|
1610
|
+
for field_name in capacity_override_fields:
|
|
1611
|
+
override_field_name = f"{field_name}_override"
|
|
1612
|
+
if (
|
|
1613
|
+
getattr(current, field_name) == getattr(updated, field_name)
|
|
1614
|
+
and getattr(current, override_field_name) != getattr(updated, override_field_name)
|
|
1615
|
+
and field_name not in changed_fields
|
|
1616
|
+
):
|
|
1617
|
+
changed_fields.append(field_name)
|
|
1618
|
+
# C2-2 routing/overload: a value that moved between layers without changing
|
|
1619
|
+
# the effective number (dashboard set to the inherited value, or cleared)
|
|
1620
|
+
# is still a change worth auditing.
|
|
1621
|
+
for field_name in _ROUTING_OVERLOAD_SETTINGS:
|
|
1622
|
+
if field_name not in changed_fields and current.provenance.get(field_name) != updated.provenance.get(
|
|
1623
|
+
field_name
|
|
1624
|
+
):
|
|
1625
|
+
changed_fields.append(field_name)
|
|
1626
|
+
# end C2-2 routing/overload
|
|
1627
|
+
# M3 codex prewarm: storing the inherited value (or clearing it) changes
|
|
1628
|
+
# ownership without changing the effective value; audit that too.
|
|
1629
|
+
if "http_responses_session_bridge_codex_prewarm_enabled" not in changed_fields and current.provenance.get(
|
|
1630
|
+
"http_responses_session_bridge_codex_prewarm_enabled"
|
|
1631
|
+
) != updated.provenance.get("http_responses_session_bridge_codex_prewarm_enabled"):
|
|
1632
|
+
changed_fields.append("http_responses_session_bridge_codex_prewarm_enabled")
|
|
1633
|
+
# end M3 codex prewarm
|
|
1634
|
+
if upstream_route_inputs_changed:
|
|
1635
|
+
# Durably bump ``upstream_route`` (with the coalesced retry fallback)
|
|
1636
|
+
# rather than relying solely on the ``settings`` bump issued above:
|
|
1637
|
+
# that bump is non-raising and enqueues no retry, so a transient write
|
|
1638
|
+
# failure would leave peers on the stale route outcome until the TTL
|
|
1639
|
+
# instead of the first recovered poll cycle. The re-clear inside
|
|
1640
|
+
# ``invalidate`` is harmless; the guarding clear already ran pre-await.
|
|
1641
|
+
await get_upstream_route_cache().invalidate()
|
|
1642
|
+
actor_ip = request.client.host if request.client else None
|
|
1643
|
+
AuditService.log_async(
|
|
1644
|
+
"settings_changed",
|
|
1645
|
+
actor_ip=actor_ip,
|
|
1646
|
+
actor=AuditActor.from_principal(principal),
|
|
1647
|
+
target=AuditTarget("settings", "dashboard"),
|
|
1648
|
+
details={"changed_fields": changed_fields},
|
|
1649
|
+
)
|
|
1650
|
+
if current.local_login_policy != updated.local_login_policy:
|
|
1651
|
+
# Who may use the local password form is a security decision with its
|
|
1652
|
+
# own event: ``settings_changed`` lists field names, this names values.
|
|
1653
|
+
AuditService.log_async(
|
|
1654
|
+
"login_policy_changed",
|
|
1655
|
+
actor_ip=actor_ip,
|
|
1656
|
+
actor=AuditActor.from_principal(principal),
|
|
1657
|
+
target=AuditTarget("settings", "local_login_policy"),
|
|
1658
|
+
details={"from": current.local_login_policy, "to": updated.local_login_policy},
|
|
1659
|
+
severity=AuditSeverity.WARNING,
|
|
1660
|
+
)
|
|
1661
|
+
# M5 conversation archive: enabling turns the proxy into a full
|
|
1662
|
+
# prompt/response recorder readable by the same dashboard admin, so every
|
|
1663
|
+
# effective on/off change is a dedicated audit event with the actor, not
|
|
1664
|
+
# just an entry in ``changed_fields``.
|
|
1665
|
+
if current.conversation_archive_enabled != updated.conversation_archive_enabled:
|
|
1666
|
+
AuditService.log_async(
|
|
1667
|
+
CONVERSATION_ARCHIVE_TOGGLED_ACTION,
|
|
1668
|
+
actor_ip=actor_ip,
|
|
1669
|
+
details={
|
|
1670
|
+
"enabled": updated.conversation_archive_enabled,
|
|
1671
|
+
"source": updated.provenance[CONVERSATION_ARCHIVE_SETTING].source,
|
|
1672
|
+
"actor": principal.actor,
|
|
1673
|
+
"actor_role": principal.role.value,
|
|
1674
|
+
},
|
|
1675
|
+
)
|
|
1676
|
+
# end M5 conversation archive
|
|
1677
|
+
return _dashboard_settings_response(updated, principal=principal)
|