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.
Files changed (827) hide show
  1. app/__init__.py +10 -0
  2. app/admin_cli.py +494 -0
  3. app/cli.py +339 -0
  4. app/codex_sessions_retag.py +487 -0
  5. app/core/__init__.py +0 -0
  6. app/core/audit/__init__.py +0 -0
  7. app/core/audit/service.py +216 -0
  8. app/core/audit/types.py +94 -0
  9. app/core/auth/__init__.py +236 -0
  10. app/core/auth/api_key_cache.py +65 -0
  11. app/core/auth/dashboard_access.py +429 -0
  12. app/core/auth/dashboard_mode.py +171 -0
  13. app/core/auth/dashboard_session_ttl.py +74 -0
  14. app/core/auth/dashboard_users_cache.py +120 -0
  15. app/core/auth/dependencies.py +710 -0
  16. app/core/auth/external_identity.py +78 -0
  17. app/core/auth/guardian.py +374 -0
  18. app/core/auth/models.py +49 -0
  19. app/core/auth/providers/__init__.py +129 -0
  20. app/core/auth/providers/oidc.py +838 -0
  21. app/core/auth/providers/registry.py +118 -0
  22. app/core/auth/refresh.py +394 -0
  23. app/core/auth/step_up.py +117 -0
  24. app/core/auth/totp.py +81 -0
  25. app/core/balancer/__init__.py +71 -0
  26. app/core/balancer/logic.py +1612 -0
  27. app/core/balancer/rendezvous_hash.py +21 -0
  28. app/core/balancer/types.py +21 -0
  29. app/core/bootstrap.py +172 -0
  30. app/core/cache/invalidation.py +426 -0
  31. app/core/clients/__init__.py +0 -0
  32. app/core/clients/codex.py +996 -0
  33. app/core/clients/codex_version.py +187 -0
  34. app/core/clients/codex_version_snapshot.py +2 -0
  35. app/core/clients/files.py +534 -0
  36. app/core/clients/headers.py +29 -0
  37. app/core/clients/http.py +511 -0
  38. app/core/clients/model_fetcher.py +276 -0
  39. app/core/clients/native_egress.py +1304 -0
  40. app/core/clients/oauth.py +477 -0
  41. app/core/clients/proxy.py +6089 -0
  42. app/core/clients/proxy_websocket.py +1411 -0
  43. app/core/clients/rate_limit_reset_credits.py +412 -0
  44. app/core/clients/stream_errors.py +12 -0
  45. app/core/clients/thread_cache_identity.py +347 -0
  46. app/core/clients/usage.py +467 -0
  47. app/core/clock.py +170 -0
  48. app/core/config/__init__.py +0 -0
  49. app/core/config/background_jobs.py +73 -0
  50. app/core/config/context_window_overrides.py +166 -0
  51. app/core/config/dashboard_overrides.py +145 -0
  52. app/core/config/inheritable.py +47 -0
  53. app/core/config/key_fingerprint.py +115 -0
  54. app/core/config/settings.py +872 -0
  55. app/core/config/settings_cache.py +100 -0
  56. app/core/config/spool_retention.py +217 -0
  57. app/core/config/tiers.py +174 -0
  58. app/core/conversation_archive.py +568 -0
  59. app/core/crypto.py +37 -0
  60. app/core/draining.py +11 -0
  61. app/core/errors.py +365 -0
  62. app/core/exceptions.py +155 -0
  63. app/core/handlers/__init__.py +3 -0
  64. app/core/handlers/exceptions.py +478 -0
  65. app/core/http_protocol.py +176 -0
  66. app/core/http_protocol_httptools.py +204 -0
  67. app/core/ingress_limits.py +17 -0
  68. app/core/metadata_store.py +18 -0
  69. app/core/metrics/__init__.py +0 -0
  70. app/core/metrics/middleware.py +74 -0
  71. app/core/metrics/prometheus.py +604 -0
  72. app/core/middleware/__init__.py +25 -0
  73. app/core/middleware/api_firewall.py +111 -0
  74. app/core/middleware/app_version.py +37 -0
  75. app/core/middleware/dashboard_auth_proxy.py +55 -0
  76. app/core/middleware/dashboard_csrf.py +177 -0
  77. app/core/middleware/dashboard_gzip.py +41 -0
  78. app/core/middleware/dashboard_overrides.py +59 -0
  79. app/core/middleware/firewall_cache.py +91 -0
  80. app/core/middleware/inflight.py +126 -0
  81. app/core/middleware/multipart_content_encoding.py +129 -0
  82. app/core/middleware/path_rewrite.py +148 -0
  83. app/core/middleware/request_body_limit.py +196 -0
  84. app/core/middleware/request_decompression.py +209 -0
  85. app/core/middleware/request_id.py +72 -0
  86. app/core/middleware/required_capability_http.py +116 -0
  87. app/core/middleware/trusted_proxy_headers.py +36 -0
  88. app/core/multipart.py +237 -0
  89. app/core/multipart_fields.py +91 -0
  90. app/core/openai/__init__.py +0 -0
  91. app/core/openai/chat_requests.py +493 -0
  92. app/core/openai/chat_responses.py +931 -0
  93. app/core/openai/contracts.py +46 -0
  94. app/core/openai/exceptions.py +24 -0
  95. app/core/openai/host_models.py +14 -0
  96. app/core/openai/images.py +413 -0
  97. app/core/openai/message_coercion.py +376 -0
  98. app/core/openai/model_refresh_scheduler.py +423 -0
  99. app/core/openai/model_registry.py +997 -0
  100. app/core/openai/model_registry_store.py +548 -0
  101. app/core/openai/models.py +172 -0
  102. app/core/openai/parsing.py +97 -0
  103. app/core/openai/requests.py +1980 -0
  104. app/core/openai/strict_schema.py +199 -0
  105. app/core/openai/tool_call_safety.py +99 -0
  106. app/core/openai/v1_requests.py +187 -0
  107. app/core/plan_types.py +90 -0
  108. app/core/prestop.py +202 -0
  109. app/core/rate_limiter/__init__.py +0 -0
  110. app/core/rate_limiter/db_rate_limiter.py +215 -0
  111. app/core/request_locality.py +473 -0
  112. app/core/resilience/__init__.py +0 -0
  113. app/core/resilience/backpressure.py +57 -0
  114. app/core/resilience/bulkhead.py +202 -0
  115. app/core/resilience/circuit_breaker.py +150 -0
  116. app/core/resilience/degradation.py +55 -0
  117. app/core/resilience/loop_lag_monitor.py +60 -0
  118. app/core/resilience/memory_monitor.py +137 -0
  119. app/core/resilience/network_recovery.py +256 -0
  120. app/core/resilience/overload.py +98 -0
  121. app/core/resilience/toggles.py +114 -0
  122. app/core/retention/__init__.py +0 -0
  123. app/core/retention/job.py +240 -0
  124. app/core/retention/scheduler.py +69 -0
  125. app/core/runtime_logging.py +601 -0
  126. app/core/scheduling/__init__.py +0 -0
  127. app/core/scheduling/leader_election.py +993 -0
  128. app/core/scheduling/leader_election_handle.py +25 -0
  129. app/core/server.py +140 -0
  130. app/core/shutdown.py +326 -0
  131. app/core/socket_peer.py +22 -0
  132. app/core/startup.py +41 -0
  133. app/core/timeout_invariants.py +347 -0
  134. app/core/tracing/__init__.py +0 -0
  135. app/core/tracing/otel.py +207 -0
  136. app/core/types.py +6 -0
  137. app/core/upstream_proxy/__init__.py +13 -0
  138. app/core/upstream_proxy/cache.py +130 -0
  139. app/core/upstream_proxy/resolver.py +189 -0
  140. app/core/upstream_proxy/types.py +104 -0
  141. app/core/usage/__init__.py +407 -0
  142. app/core/usage/depletion.py +109 -0
  143. app/core/usage/live_hub.py +41 -0
  144. app/core/usage/live_snapshots.py +174 -0
  145. app/core/usage/logs.py +212 -0
  146. app/core/usage/metadata_scheduler.py +118 -0
  147. app/core/usage/models.py +54 -0
  148. app/core/usage/pricing.py +576 -0
  149. app/core/usage/pricing_catalog.py +241 -0
  150. app/core/usage/pricing_snapshot.json +821 -0
  151. app/core/usage/quota.py +133 -0
  152. app/core/usage/refresh_policy.py +21 -0
  153. app/core/usage/refresh_scheduler.py +713 -0
  154. app/core/usage/reset_credits_refresh_scheduler.py +435 -0
  155. app/core/usage/types.py +168 -0
  156. app/core/utils/__init__.py +0 -0
  157. app/core/utils/json_guards.py +18 -0
  158. app/core/utils/locks.py +33 -0
  159. app/core/utils/masking.py +20 -0
  160. app/core/utils/proxy_env.py +104 -0
  161. app/core/utils/request_id.py +65 -0
  162. app/core/utils/retry.py +77 -0
  163. app/core/utils/shared_future.py +172 -0
  164. app/core/utils/sse.py +286 -0
  165. app/core/utils/time.py +24 -0
  166. app/db/__init__.py +0 -0
  167. app/db/account_identity_lock.py +56 -0
  168. app/db/alembic/__init__.py +1 -0
  169. app/db/alembic/env.py +70 -0
  170. app/db/alembic/revision_ids.py +43 -0
  171. app/db/alembic/versions/20260213_000000_base_schema.py +248 -0
  172. app/db/alembic/versions/20260213_000100_normalize_account_plan_types.py +45 -0
  173. app/db/alembic/versions/20260213_000200_add_request_logs_reasoning_effort.py +45 -0
  174. app/db/alembic/versions/20260213_000300_add_accounts_reset_at.py +45 -0
  175. app/db/alembic/versions/20260213_000400_add_accounts_chatgpt_account_id.py +45 -0
  176. app/db/alembic/versions/20260213_000500_add_dashboard_settings.py +70 -0
  177. app/db/alembic/versions/20260213_000600_add_dashboard_settings_totp.py +64 -0
  178. app/db/alembic/versions/20260213_000700_add_dashboard_settings_password.py +59 -0
  179. app/db/alembic/versions/20260213_000800_add_api_keys.py +87 -0
  180. app/db/alembic/versions/20260214_000000_add_api_key_limits.py +143 -0
  181. app/db/alembic/versions/20260215_000000_add_idx_logs_requested_at.py +38 -0
  182. app/db/alembic/versions/20260218_000000_add_api_key_usage_reservations.py +117 -0
  183. app/db/alembic/versions/20260218_000100_add_import_without_overwrite_and_drop_accounts_email_unique.py +198 -0
  184. app/db/alembic/versions/20260225_000000_add_dashboard_settings_routing_strategy.py +53 -0
  185. app/db/alembic/versions/20260228_020000_align_api_key_limit_enum_types.py +104 -0
  186. app/db/alembic/versions/20260228_030000_add_api_firewall_allowlist.py +42 -0
  187. app/db/alembic/versions/20260306_000000_add_request_logs_service_tier.py +45 -0
  188. app/db/alembic/versions/20260307_000000_add_api_key_enforcement_fields.py +56 -0
  189. app/db/alembic/versions/20260308_000000_add_sqlite_performance_indexes.py +41 -0
  190. app/db/alembic/versions/20260309_000000_add_additional_usage_history.py +71 -0
  191. app/db/alembic/versions/20260309_000000_request_logs_nullable_account_id.py +28 -0
  192. app/db/alembic/versions/20260310_000000_add_request_logs_transport.py +45 -0
  193. app/db/alembic/versions/20260310_000000_fix_postgresql_enum_value_casing.py +79 -0
  194. app/db/alembic/versions/20260310_120000_add_sticky_session_kinds_and_affinity_ttl.py +110 -0
  195. app/db/alembic/versions/20260312_000000_add_additional_usage_quota_key.py +193 -0
  196. app/db/alembic/versions/20260312_000000_split_sticky_sessions_primary_key_by_kind.py +96 -0
  197. app/db/alembic/versions/20260312_010000_merge_additional_usage_and_sticky_session_heads.py +25 -0
  198. app/db/alembic/versions/20260312_020000_merge_request_logs_transport_and_feature_heads.py +25 -0
  199. app/db/alembic/versions/20260312_020000_merge_request_logs_transport_and_sticky_heads.py +25 -0
  200. app/db/alembic/versions/20260312_120000_add_dashboard_upstream_stream_transport.py +62 -0
  201. app/db/alembic/versions/20260313_024500_merge_request_log_transport_heads.py +27 -0
  202. app/db/alembic/versions/20260319_100937_increase_prompt_cache_ttl_to_1800s.py +73 -0
  203. app/db/alembic/versions/20260319_111500_merge_prompt_cache_ttl_and_upstream_transport_heads.py +25 -0
  204. app/db/alembic/versions/20260319_183000_normalize_sqlite_account_status_casing.py +48 -0
  205. app/db/alembic/versions/20260320_000000_add_request_log_requested_actual_tiers.py +51 -0
  206. app/db/alembic/versions/20260321_190000_tighten_dashboard_db_indexes.py +64 -0
  207. app/db/alembic/versions/20260321_210000_merge_request_log_tiers_and_dashboard_index_heads.py +25 -0
  208. app/db/alembic/versions/20260325_000000_add_request_log_cost.py +193 -0
  209. app/db/alembic/versions/20260328_000000_add_rate_limit_attempts.py +48 -0
  210. app/db/alembic/versions/20260328_120000_add_request_log_ttft.py +37 -0
  211. app/db/alembic/versions/20260328_130000_add_audit_logs_table.py +45 -0
  212. app/db/alembic/versions/20260328_140000_add_scheduler_leader_table.py +39 -0
  213. app/db/alembic/versions/20260330_000000_add_cache_locality_settings.py +69 -0
  214. app/db/alembic/versions/20260330_010000_merge_scheduler_leader_and_cache_locality_heads.py +25 -0
  215. app/db/alembic/versions/20260330_020000_add_bridge_ring_members.py +46 -0
  216. app/db/alembic/versions/20260401_000000_add_api_key_enforced_service_tier.py +52 -0
  217. app/db/alembic/versions/20260401_000000_add_cache_invalidation.py +41 -0
  218. app/db/alembic/versions/20260401_020000_merge_cache_invalidation_and_api_key_service_tier_heads.py +22 -0
  219. app/db/alembic/versions/20260402_000000_switch_dashboard_routing_default_to_capacity_weighted.py +66 -0
  220. app/db/alembic/versions/20260403_000000_add_credit_api_key_limit_values.py +44 -0
  221. app/db/alembic/versions/20260406_000000_add_api_key_account_assignments.py +57 -0
  222. app/db/alembic/versions/20260406_010000_add_api_key_assignment_scope_flag.py +75 -0
  223. app/db/alembic/versions/20260407_000000_add_bridge_gateway_safe_mode.py +51 -0
  224. app/db/alembic/versions/20260407_010000_merge_api_key_assignment_and_bridge_gateway_heads.py +25 -0
  225. app/db/alembic/versions/20260408_000000_switch_import_without_overwrite_default_to_true.py +73 -0
  226. app/db/alembic/versions/20260408_010000_merge_import_without_overwrite_and_assignment_heads.py +25 -0
  227. app/db/alembic/versions/20260409_000000_add_http_bridge_sessions.py +130 -0
  228. app/db/alembic/versions/20260409_000000_switch_sticky_threads_and_prefer_earlier_reset_defaults_to_true.py +89 -0
  229. app/db/alembic/versions/20260409_010000_add_dashboard_settings_bootstrap_token.py +57 -0
  230. app/db/alembic/versions/20260409_010000_merge_http_bridge_and_import_without_overwrite_heads.py +25 -0
  231. app/db/alembic/versions/20260409_020000_fix_http_bridge_last_seen_index.py +46 -0
  232. app/db/alembic/versions/20260410_000000_backfill_pristine_dashboard_settings_defaults.py +77 -0
  233. app/db/alembic/versions/20260410_030000_restore_import_without_overwrite_default_true.py +73 -0
  234. app/db/alembic/versions/20260410_040000_merge_dashboard_defaults_and_import_default_heads.py +25 -0
  235. app/db/alembic/versions/20260413_000000_add_accounts_blocked_at.py +44 -0
  236. app/db/alembic/versions/20260415_160000_add_automations_tables.py +189 -0
  237. app/db/alembic/versions/20260415_160000_add_request_logs_response_lookup_index.py +67 -0
  238. app/db/alembic/versions/20260417_000000_add_request_log_plan_type.py +44 -0
  239. app/db/alembic/versions/20260418_190000_add_automation_runs_cycle_key.py +87 -0
  240. app/db/alembic/versions/20260419_000000_add_automation_run_cycle_metadata.py +56 -0
  241. app/db/alembic/versions/20260419_020000_add_automation_run_cycles_snapshot_tables.py +342 -0
  242. app/db/alembic/versions/20260421_000000_add_dashboard_session_ttl_setting.py +51 -0
  243. app/db/alembic/versions/20260421_120000_merge_request_log_lookup_and_plan_type_heads.py +25 -0
  244. app/db/alembic/versions/20260421_130000_merge_automation_and_request_log_heads.py +25 -0
  245. app/db/alembic/versions/20260422_103000_normalize_legacy_automation_run_cycle_keys.py +648 -0
  246. app/db/alembic/versions/20260423_120000_add_api_key_limit_reset_at_index.py +43 -0
  247. app/db/alembic/versions/20260424_000000_merge_dashboard_session_ttl_and_request_log_heads.py +25 -0
  248. app/db/alembic/versions/20260426_000000_add_dashboard_relative_availability_settings.py +68 -0
  249. app/db/alembic/versions/20260427_130000_add_warmup_model_and_request_kind.py +75 -0
  250. app/db/alembic/versions/20260506_000000_add_account_routing_policy.py +37 -0
  251. app/db/alembic/versions/20260509_000000_add_api_key_traffic_class.py +37 -0
  252. app/db/alembic/versions/20260509_010000_add_additional_quota_routing_policies.py +37 -0
  253. app/db/alembic/versions/20260513_000000_add_accounts_alias.py +44 -0
  254. app/db/alembic/versions/20260513_000000_add_api_key_apply_to_codex_model.py +59 -0
  255. app/db/alembic/versions/20260514_000000_add_request_logs_api_key_time_index.py +43 -0
  256. app/db/alembic/versions/20260515_000000_add_prefer_earlier_reset_window.py +47 -0
  257. app/db/alembic/versions/20260515_000000_soft_delete_request_logs_on_account_delete.py +83 -0
  258. app/db/alembic/versions/20260515_020000_add_split_sticky_budget_thresholds.py +62 -0
  259. app/db/alembic/versions/20260516_000000_add_sqlite_hot_path_indexes.py +75 -0
  260. app/db/alembic/versions/20260517_000000_merge_sqlite_hot_path_and_request_log_delete_heads.py +24 -0
  261. app/db/alembic/versions/20260518_000000_add_http_bridge_durable_input_prefix.py +44 -0
  262. app/db/alembic/versions/20260518_010000_merge_http_bridge_and_sqlite_recovery_heads.py +25 -0
  263. app/db/alembic/versions/20260518_120000_add_dashboard_guest_access.py +57 -0
  264. app/db/alembic/versions/20260520_000000_merge_api_key_and_http_bridge_heads.py +26 -0
  265. app/db/alembic/versions/20260520_010000_add_request_logs_api_key_account_index.py +45 -0
  266. app/db/alembic/versions/20260520_010000_merge_automation_and_api_key_heads.py +26 -0
  267. app/db/alembic/versions/20260520_030000_add_quota_planner.py +194 -0
  268. app/db/alembic/versions/20260520_040000_merge_quota_planner_and_request_log_index_heads.py +24 -0
  269. app/db/alembic/versions/20260521_000000_add_account_security_work_authorized.py +46 -0
  270. app/db/alembic/versions/20260521_000000_merge_dashboard_guest_and_api_key_heads.py +26 -0
  271. app/db/alembic/versions/20260521_000000_merge_prefer_reset_window_head.py +26 -0
  272. app/db/alembic/versions/20260521_000000_merge_warmup_and_request_log_index_heads.py +25 -0
  273. app/db/alembic/versions/20260522_000000_add_limit_warmup_trigger.py +195 -0
  274. app/db/alembic/versions/20260524_000000_merge_accounts_alias_and_reset_window_heads.py +24 -0
  275. app/db/alembic/versions/20260525_000000_add_usage_raw_window_latest_index.py +31 -0
  276. app/db/alembic/versions/20260526_000000_add_request_log_failure_metadata.py +100 -0
  277. app/db/alembic/versions/20260531_000000_add_account_workspace_identity.py +39 -0
  278. app/db/alembic/versions/20260531_000000_add_single_account_routing.py +42 -0
  279. app/db/alembic/versions/20260601_000000_merge_relative_availability_and_usage_raw_heads.py +24 -0
  280. app/db/alembic/versions/20260601_010000_add_api_key_usage_sections.py +58 -0
  281. app/db/alembic/versions/20260601_010000_add_routing_policy_persistence.py +67 -0
  282. app/db/alembic/versions/20260601_010000_merge_security_work_and_relative_availability_heads.py +25 -0
  283. app/db/alembic/versions/20260601_010000_merge_split_sticky_and_relative_usage_heads.py +25 -0
  284. app/db/alembic/versions/20260601_010000_merge_warmup_and_relative_availability_heads.py +25 -0
  285. app/db/alembic/versions/20260601_020000_merge_additional_quota_routing_and_relative_availability_heads.py +25 -0
  286. app/db/alembic/versions/20260601_020000_merge_quota_planner_and_relative_availability_heads.py +25 -0
  287. app/db/alembic/versions/20260601_020000_merge_reset_window_and_relative_availability_heads.py +25 -0
  288. app/db/alembic/versions/20260601_020000_merge_routing_policy_and_split_threshold_heads.py +24 -0
  289. app/db/alembic/versions/20260601_020000_merge_warmup_and_request_log_failure_heads.py +25 -0
  290. app/db/alembic/versions/20260601_030000_merge_single_account_and_routing_quota_heads.py +29 -0
  291. app/db/alembic/versions/20260601_040000_merge_security_work_and_routing_consolidation_heads.py +25 -0
  292. app/db/alembic/versions/20260601_050000_merge_quota_planner_and_routing_consolidation_heads.py +25 -0
  293. app/db/alembic/versions/20260602_050000_add_upstream_proxy_routing.py +164 -0
  294. app/db/alembic/versions/20260602_060000_merge_account_workspace_and_failure_heads.py +25 -0
  295. app/db/alembic/versions/20260602_070000_add_request_log_useragent_fields.py +86 -0
  296. app/db/alembic/versions/20260602_070000_merge_quota_planner_and_account_workspace_heads.py +25 -0
  297. app/db/alembic/versions/20260602_080000_merge_upstream_proxy_and_quota_planner_heads.py +25 -0
  298. app/db/alembic/versions/20260602_080000_merge_useragent_and_upstream_proxy_heads.py +24 -0
  299. app/db/alembic/versions/20260603_000000_add_weekly_pace_working_days.py +52 -0
  300. app/db/alembic/versions/20260603_000000_free_account_monthly_window.py +60 -0
  301. app/db/alembic/versions/20260604_000000_add_reauth_required_account_status.py +159 -0
  302. app/db/alembic/versions/20260604_010000_merge_automations_and_reauth_status_heads.py +25 -0
  303. app/db/alembic/versions/20260604_010000_merge_dashboard_guest_and_reauth_status_heads.py +25 -0
  304. app/db/alembic/versions/20260604_010000_merge_useragent_and_reauth_heads.py +24 -0
  305. app/db/alembic/versions/20260607_000000_merge_weekly_monthly_useragent_heads.py +28 -0
  306. app/db/alembic/versions/20260608_000000_add_hide_upstream_quota_from_api_keys.py +50 -0
  307. app/db/alembic/versions/20260611_000000_add_request_log_archive_request_id.py +47 -0
  308. app/db/alembic/versions/20260611_000000_merge_automations_and_weekly_monthly_useragent_heads.py +26 -0
  309. app/db/alembic/versions/20260611_000000_merge_dashboard_guest_and_weekly_useragent_heads.py +26 -0
  310. app/db/alembic/versions/20260611_010000_add_request_log_client_ip.py +57 -0
  311. app/db/alembic/versions/20260611_020000_merge_request_log_archive_and_client_ip_heads.py +26 -0
  312. app/db/alembic/versions/20260611_030000_merge_dashboard_guest_and_request_log_client_ip_heads.py +26 -0
  313. app/db/alembic/versions/20260613_000000_add_accounts_codex_installation_id.py +57 -0
  314. app/db/alembic/versions/20260613_010000_merge_installation_id_and_dashboard_weekly_heads.py +26 -0
  315. app/db/alembic/versions/20260615_000000_merge_automations_and_dashboard_guest_heads.py +26 -0
  316. app/db/alembic/versions/20260623_000000_add_limit_warmup_exhausted_threshold.py +51 -0
  317. app/db/alembic/versions/20260626_000000_add_http_downstream_transport_policy.py +46 -0
  318. app/db/alembic/versions/20260626_010000_add_request_logs_upstream_transport.py +31 -0
  319. app/db/alembic/versions/20260629_000000_add_dashboard_query_hot_path_indexes.py +53 -0
  320. app/db/alembic/versions/20260630_000000_add_limit_warmup_staggered_idle.py +47 -0
  321. app/db/alembic/versions/20260630_000000_merge_request_log_client_ip_and_dashboard_hot_path_heads.py +26 -0
  322. app/db/alembic/versions/20260630_010000_merge_warmup_and_request_log_dashboard_heads.py +26 -0
  323. app/db/alembic/versions/20260630_020000_merge_automations_and_main_heads.py +26 -0
  324. app/db/alembic/versions/20260630_020000_merge_warmup_threshold_and_main_heads.py +26 -0
  325. app/db/alembic/versions/20260630_030000_add_automation_run_model_snapshot.py +59 -0
  326. app/db/alembic/versions/20260630_040000_merge_automation_snapshot_and_warmup_threshold_heads.py +26 -0
  327. app/db/alembic/versions/20260630_050000_add_automation_run_prompt_snapshot.py +52 -0
  328. app/db/alembic/versions/20260701_000000_add_weekly_pace_smoothing_minutes.py +51 -0
  329. app/db/alembic/versions/20260702_000000_add_model_sources.py +176 -0
  330. app/db/alembic/versions/20260705_000000_add_model_source_audio_transcriptions.py +50 -0
  331. app/db/alembic/versions/20260705_000000_harden_dashboard_session_ttl.py +62 -0
  332. app/db/alembic/versions/20260706_000000_add_model_source_audio_per_minute.py +43 -0
  333. app/db/alembic/versions/20260707_000000_merge_model_sources_and_session_ttl.py +24 -0
  334. app/db/alembic/versions/20260707_010000_merge_automations_and_model_sources_heads.py +24 -0
  335. app/db/alembic/versions/20260707_020000_add_automation_job_account_scope.py +125 -0
  336. app/db/alembic/versions/20260709_000000_add_ttft_phase_observability.py +66 -0
  337. app/db/alembic/versions/20260711_000000_add_dashboard_prohibit_fast_mode.py +42 -0
  338. app/db/alembic/versions/20260711_010000_add_account_chatgpt_user_id.py +40 -0
  339. app/db/alembic/versions/20260711_020000_add_dashboard_account_concurrency_caps.py +71 -0
  340. app/db/alembic/versions/20260711_030000_add_limit_warmup_idle_threshold.py +51 -0
  341. app/db/alembic/versions/20260712_010000_add_account_usage_rollups.py +56 -0
  342. app/db/alembic/versions/20260712_020000_add_api_key_usage_rollups.py +53 -0
  343. app/db/alembic/versions/20260713_020000_add_model_registry_snapshot.py +42 -0
  344. app/db/alembic/versions/20260713_040000_add_account_refresh_claims.py +42 -0
  345. app/db/alembic/versions/20260713_040000_add_replica_guardrails.py +64 -0
  346. app/db/alembic/versions/20260713_070000_add_reset_credit_redeem_tables.py +58 -0
  347. app/db/alembic/versions/20260714_000000_add_oauth_flow_states.py +63 -0
  348. app/db/alembic/versions/20260715_000000_add_request_log_queue_latency.py +44 -0
  349. app/db/alembic/versions/20260716_000000_add_oauth_device_flow_slots.py +41 -0
  350. app/db/alembic/versions/20260716_010000_add_dashboard_retention_settings.py +48 -0
  351. app/db/alembic/versions/20260716_010000_add_reset_credit_display_settings.py +73 -0
  352. app/db/alembic/versions/20260717_000000_add_http_bridge_retry_circuits.py +47 -0
  353. app/db/alembic/versions/20260717_000000_merge_retention_and_reset_credit_display_heads.py +26 -0
  354. app/db/alembic/versions/20260717_000000_optimize_dashboard_hot_path_indexes.py +162 -0
  355. app/db/alembic/versions/20260717_000001_merge_retry_circuits_and_dashboard_indexes.py +22 -0
  356. app/db/alembic/versions/20260720_000000_add_request_log_conversation_id.py +57 -0
  357. app/db/alembic/versions/20260722_000000_backfill_request_log_useragent_families.py +40 -0
  358. app/db/alembic/versions/20260724_000000_add_request_usage_time_rollups.py +176 -0
  359. app/db/alembic/versions/20260724_000000_merge_request_log_schema_heads.py +33 -0
  360. app/db/alembic/versions/20260725_000000_add_http_bridge_pending_tool_calls.py +43 -0
  361. app/db/alembic/versions/20260726_000000_add_account_plan_downgrade_observations.py +44 -0
  362. app/db/alembic/versions/20260726_000000_repair_request_usage_rollups_after_merge.py +37 -0
  363. app/db/alembic/versions/20260727_000000_add_sticky_session_continuity_abandoned_at.py +45 -0
  364. app/db/alembic/versions/20260728_000000_merge_pending_tool_calls_and_rollup_repair_heads.py +31 -0
  365. app/db/alembic/versions/20260730_000000_add_api_key_fair_share_threshold.py +44 -0
  366. app/db/alembic/versions/20260730_000000_add_http_bridge_recovery_attempts.py +60 -0
  367. app/db/alembic/versions/20260731_000000_add_capability_lineage_markers.py +38 -0
  368. app/db/alembic/versions/20260803_000000_merge_http_bridge_recovery_and_capability_lineage_heads.py +30 -0
  369. app/db/alembic/versions/20260804_000000_add_http_bridge_operations.py +63 -0
  370. app/db/alembic/versions/20260804_000001_add_global_http_bridge_operation_fingerprint.py +53 -0
  371. app/db/alembic/versions/20260804_230000_add_request_log_connection_request_kind.py +43 -0
  372. app/db/alembic/versions/20260805_000000_add_http_bridge_operation_spool.py +75 -0
  373. app/db/alembic/versions/20260805_000001_finalize_http_bridge_operation_spool.py +65 -0
  374. app/db/alembic/versions/20260806_000000_add_additional_usage_alias_probe_indexes.py +84 -0
  375. app/db/alembic/versions/20260806_000000_add_anonymous_telemetry.py +49 -0
  376. app/db/alembic/versions/20260806_010000_add_conversation_presence_rollup.py +72 -0
  377. app/db/alembic/versions/20260806_020000_add_usage_history_bulk_covering_indexes.py +99 -0
  378. app/db/alembic/versions/20260806_030000_add_api_key_allowed_reasoning_efforts.py +65 -0
  379. app/db/alembic/versions/20260806_120000_add_http_bridge_owner_process_epoch.py +57 -0
  380. app/db/alembic/versions/20260807_000000_merge_http_bridge_operation_spool_and_latest_main.py +24 -0
  381. app/db/alembic/versions/20260808_000000_tune_usage_history_autovacuum.py +54 -0
  382. app/db/alembic/versions/20260810_000000_add_http_bridge_recovery_dispatch_count.py +44 -0
  383. app/db/alembic/versions/20260811_000000_add_hourly_rollup_cancelled_count.py +92 -0
  384. app/db/alembic/versions/20260812_000000_merge_recovery_dispatch_and_hourly_cancelled_heads.py +25 -0
  385. app/db/alembic/versions/20260812_120000_add_sticky_abandonment_scope.py +43 -0
  386. app/db/alembic/versions/20260813_000000_add_file_account_pins.py +40 -0
  387. app/db/alembic/versions/20260816_000000_add_account_pending_deletion.py +81 -0
  388. app/db/alembic/versions/20260816_000000_add_model_source_embeddings.py +50 -0
  389. app/db/alembic/versions/20260821_000000_add_retry_circuit_admission_generation.py +49 -0
  390. app/db/alembic/versions/20260826_000000_add_http_bridge_event_chunks.py +107 -0
  391. app/db/alembic/versions/20260828_000000_add_accounts_chatgpt_identity_index.py +55 -0
  392. app/db/alembic/versions/20260830_000000_add_quota_warmup_claim_expiry.py +83 -0
  393. app/db/alembic/versions/20260908_000000_add_guest_session_generation.py +37 -0
  394. app/db/alembic/versions/20260908_000000_add_subscription_overflow.py +140 -0
  395. app/db/alembic/versions/20260908_000000_replace_upstream_stream_transport_default_sentinel.py +50 -0
  396. app/db/alembic/versions/20260908_020000_merge_overflow_transport_heads.py +27 -0
  397. app/db/alembic/versions/20260909_000000_add_dashboard_roles.py +72 -0
  398. app/db/alembic/versions/20260909_010000_add_dashboard_users.py +183 -0
  399. app/db/alembic/versions/20260909_020000_reproject_compat_admin_credentials.py +105 -0
  400. app/db/alembic/versions/20260909_030000_add_audit_actor_columns.py +103 -0
  401. app/db/alembic/versions/20260909_030000_dashboard_resilience_toggle_settings.py +63 -0
  402. app/db/alembic/versions/20260909_040000_add_dashboard_user_invites.py +54 -0
  403. app/db/alembic/versions/20260909_040000_dashboard_timeout_settings.py +68 -0
  404. app/db/alembic/versions/20260909_050000_dashboard_routing_overload_settings.py +65 -0
  405. app/db/alembic/versions/20260909_060000_add_report_rollup.py +74 -0
  406. app/db/alembic/versions/20260909_070000_automation_run_claim_budget.py +50 -0
  407. app/db/alembic/versions/20260909_080000_dashboard_stream_bridge_budgets.py +62 -0
  408. app/db/alembic/versions/20260909_090000_dashboard_background_job_toggles.py +63 -0
  409. app/db/alembic/versions/20260909_100000_dashboard_codex_prewarm.py +49 -0
  410. app/db/alembic/versions/20260909_110000_model_context_window_overrides.py +45 -0
  411. app/db/alembic/versions/20260909_120000_dashboard_conversation_archive.py +48 -0
  412. app/db/alembic/versions/20260909_130000_add_request_logs_live_facet_indexes.py +96 -0
  413. app/db/alembic/versions/20260910_000000_add_totp_required_for_admin_role.py +44 -0
  414. app/db/alembic/versions/20260910_000000_request_logs_missing_cost_index.py +52 -0
  415. app/db/alembic/versions/20260910_010000_add_dashboard_auth_providers.py +134 -0
  416. app/db/alembic/versions/20260910_010000_dashboard_spool_retention.py +52 -0
  417. app/db/alembic/versions/20260910_020000_add_dashboard_role_mappings.py +74 -0
  418. app/db/alembic/versions/20260910_143000_request_log_affinity.py +28 -0
  419. app/db/alembic/versions/20260910_180000_merge_affinity_guest_heads.py +17 -0
  420. app/db/alembic/versions/20260910_200000_merge_affinity_identity_heads.py +17 -0
  421. app/db/alembic/versions/20260910_220000_merge_affinity_invite_heads.py +17 -0
  422. app/db/alembic/versions/20260911_000000_model_source_pins_kind_expires_index.py +43 -0
  423. app/db/alembic/versions/20260911_010000_merge_pin_index_and_affinity_heads.py +17 -0
  424. app/db/alembic/versions/20260911_020000_add_http_bridge_terminal_append_phase.py +54 -0
  425. app/db/alembic/versions/20260911_030000_add_local_login_policy.py +48 -0
  426. app/db/alembic/versions/20260911_040000_add_thread_cache_identity_mode.py +48 -0
  427. app/db/alembic/versions/20260911_060000_add_bridge_session_continuity_abandonment.py +40 -0
  428. app/db/alembic/versions/20260912_000000_merge_thread_cache_and_bridge_retirement_heads.py +22 -0
  429. app/db/alembic/versions/20260912_010000_drop_legacy_dashboard_credentials.py +140 -0
  430. app/db/alembic/versions/20260913_000000_add_oidc_provider_flow.py +134 -0
  431. app/db/alembic/versions/20260914_000000_add_scim_tokens.py +78 -0
  432. app/db/alembic/versions/20260914_000000_drop_subscription_overflow_schema.py +132 -0
  433. app/db/alembic/versions/20260918_000000_merge_scim_and_overflow_heads.py +17 -0
  434. app/db/alembic/versions/__init__.py +1 -0
  435. app/db/backup.py +64 -0
  436. app/db/migrate.py +1249 -0
  437. app/db/migration_lock.py +160 -0
  438. app/db/migration_url.py +18 -0
  439. app/db/models.py +3100 -0
  440. app/db/recover.py +244 -0
  441. app/db/session.py +1295 -0
  442. app/db/snapshot.py +46 -0
  443. app/db/sqlite_lock_retry.py +190 -0
  444. app/db/sqlite_utils.py +549 -0
  445. app/dependencies.py +462 -0
  446. app/main.py +1240 -0
  447. app/modules/__init__.py +0 -0
  448. app/modules/accounts/__init__.py +0 -0
  449. app/modules/accounts/api.py +426 -0
  450. app/modules/accounts/auth_manager.py +1439 -0
  451. app/modules/accounts/background_repository.py +160 -0
  452. app/modules/accounts/deletion.py +540 -0
  453. app/modules/accounts/mappers.py +654 -0
  454. app/modules/accounts/refresh_claims.py +352 -0
  455. app/modules/accounts/repository.py +1679 -0
  456. app/modules/accounts/schemas.py +308 -0
  457. app/modules/accounts/service.py +809 -0
  458. app/modules/accounts/usage_rollup.py +486 -0
  459. app/modules/accounts/usage_rollup_scheduler.py +75 -0
  460. app/modules/accounts/usage_time_rollup.py +1037 -0
  461. app/modules/accounts/usage_time_rollup_read.py +522 -0
  462. app/modules/api_keys/__init__.py +1 -0
  463. app/modules/api_keys/api.py +344 -0
  464. app/modules/api_keys/last_used_coalescer.py +216 -0
  465. app/modules/api_keys/limit_windows.py +41 -0
  466. app/modules/api_keys/repository.py +1200 -0
  467. app/modules/api_keys/reset_scheduler.py +85 -0
  468. app/modules/api_keys/schemas.py +135 -0
  469. app/modules/api_keys/service.py +2123 -0
  470. app/modules/audit/__init__.py +0 -0
  471. app/modules/audit/api.py +88 -0
  472. app/modules/audit/repository.py +74 -0
  473. app/modules/audit/schemas.py +33 -0
  474. app/modules/audit/service.py +71 -0
  475. app/modules/auth_providers/__init__.py +0 -0
  476. app/modules/auth_providers/api.py +127 -0
  477. app/modules/auth_providers/config.py +343 -0
  478. app/modules/auth_providers/repository.py +101 -0
  479. app/modules/auth_providers/schemas.py +88 -0
  480. app/modules/auth_providers/seed.py +105 -0
  481. app/modules/auth_providers/service.py +289 -0
  482. app/modules/automations/__init__.py +1 -0
  483. app/modules/automations/api.py +369 -0
  484. app/modules/automations/repository.py +1896 -0
  485. app/modules/automations/scheduler.py +107 -0
  486. app/modules/automations/schemas.py +158 -0
  487. app/modules/automations/service.py +2548 -0
  488. app/modules/cache_isolation_probe/__init__.py +0 -0
  489. app/modules/cache_isolation_probe/api.py +188 -0
  490. app/modules/cache_isolation_probe/prefix.py +144 -0
  491. app/modules/cache_isolation_probe/schemas.py +94 -0
  492. app/modules/cache_isolation_probe/sender.py +229 -0
  493. app/modules/cache_isolation_probe/service.py +525 -0
  494. app/modules/conversation_archive/__init__.py +1 -0
  495. app/modules/conversation_archive/api.py +101 -0
  496. app/modules/conversation_archive/schemas.py +28 -0
  497. app/modules/conversation_archive/service.py +165 -0
  498. app/modules/dashboard/__init__.py +1 -0
  499. app/modules/dashboard/api.py +106 -0
  500. app/modules/dashboard/builders.py +107 -0
  501. app/modules/dashboard/repository.py +304 -0
  502. app/modules/dashboard/schemas.py +148 -0
  503. app/modules/dashboard/service.py +534 -0
  504. app/modules/dashboard/timeframes.py +19 -0
  505. app/modules/dashboard/weekly_pace.py +617 -0
  506. app/modules/dashboard_auth/__init__.py +1 -0
  507. app/modules/dashboard_auth/api.py +1344 -0
  508. app/modules/dashboard_auth/oidc_api.py +753 -0
  509. app/modules/dashboard_auth/oidc_flows.py +330 -0
  510. app/modules/dashboard_auth/repository.py +452 -0
  511. app/modules/dashboard_auth/schemas.py +232 -0
  512. app/modules/dashboard_auth/service.py +1384 -0
  513. app/modules/dashboard_roles/__init__.py +0 -0
  514. app/modules/dashboard_roles/api.py +64 -0
  515. app/modules/dashboard_roles/repository.py +54 -0
  516. app/modules/dashboard_roles/schemas.py +29 -0
  517. app/modules/dashboard_roles/seed.py +80 -0
  518. app/modules/dashboard_roles/service.py +142 -0
  519. app/modules/dashboard_users/__init__.py +0 -0
  520. app/modules/dashboard_users/api.py +278 -0
  521. app/modules/dashboard_users/break_glass.py +169 -0
  522. app/modules/dashboard_users/credentials.py +18 -0
  523. app/modules/dashboard_users/identity_resolver.py +665 -0
  524. app/modules/dashboard_users/repository.py +725 -0
  525. app/modules/dashboard_users/schemas.py +117 -0
  526. app/modules/dashboard_users/service.py +1054 -0
  527. app/modules/firewall/__init__.py +1 -0
  528. app/modules/firewall/api.py +78 -0
  529. app/modules/firewall/repository.py +51 -0
  530. app/modules/firewall/schemas.py +28 -0
  531. app/modules/firewall/service.py +93 -0
  532. app/modules/fleet/__init__.py +1 -0
  533. app/modules/fleet/api.py +177 -0
  534. app/modules/fleet/mappers.py +56 -0
  535. app/modules/fleet/observability.py +372 -0
  536. app/modules/fleet/schemas.py +119 -0
  537. app/modules/health/__init__.py +0 -0
  538. app/modules/health/api.py +246 -0
  539. app/modules/health/schemas.py +27 -0
  540. app/modules/limit_warmup/__init__.py +1 -0
  541. app/modules/limit_warmup/repository.py +159 -0
  542. app/modules/limit_warmup/service.py +1001 -0
  543. app/modules/model_sources/__init__.py +1 -0
  544. app/modules/model_sources/api.py +99 -0
  545. app/modules/model_sources/catalog.py +307 -0
  546. app/modules/model_sources/forwarding.py +1435 -0
  547. app/modules/model_sources/projection.py +55 -0
  548. app/modules/model_sources/repository.py +201 -0
  549. app/modules/model_sources/schemas.py +79 -0
  550. app/modules/model_sources/selection.py +166 -0
  551. app/modules/model_sources/service.py +236 -0
  552. app/modules/oauth/__init__.py +0 -0
  553. app/modules/oauth/api.py +101 -0
  554. app/modules/oauth/repository.py +238 -0
  555. app/modules/oauth/schemas.py +49 -0
  556. app/modules/oauth/service.py +1196 -0
  557. app/modules/oauth/templates/oauth_success.html +122 -0
  558. app/modules/proxy/__init__.py +0 -0
  559. app/modules/proxy/_load_balancer/__init__.py +1 -0
  560. app/modules/proxy/_load_balancer/error_rate.py +116 -0
  561. app/modules/proxy/_load_balancer/latency_cohort.py +154 -0
  562. app/modules/proxy/_load_balancer/model_eligibility.py +250 -0
  563. app/modules/proxy/_load_balancer/opportunistic_admission.py +350 -0
  564. app/modules/proxy/_load_balancer/overload_backoff.py +394 -0
  565. app/modules/proxy/_load_balancer/sticky_selection.py +2417 -0
  566. app/modules/proxy/_load_balancer/throughput_cohort.py +233 -0
  567. app/modules/proxy/_load_balancer/ttft_cohort.py +214 -0
  568. app/modules/proxy/_load_balancer/tunables.py +97 -0
  569. app/modules/proxy/_load_balancer/types.py +96 -0
  570. app/modules/proxy/_load_balancer/unbound_selection.py +534 -0
  571. app/modules/proxy/_service/__init__.py +6 -0
  572. app/modules/proxy/_service/api_key_usage.py +745 -0
  573. app/modules/proxy/_service/clock_budget.py +38 -0
  574. app/modules/proxy/_service/codex_control.py +611 -0
  575. app/modules/proxy/_service/compact.py +2211 -0
  576. app/modules/proxy/_service/file_ops.py +749 -0
  577. app/modules/proxy/_service/http_bridge/__init__.py +3 -0
  578. app/modules/proxy/_service/http_bridge/accepted_replay.py +331 -0
  579. app/modules/proxy/_service/http_bridge/account_sessions.py +57 -0
  580. app/modules/proxy/_service/http_bridge/activity.py +178 -0
  581. app/modules/proxy/_service/http_bridge/helpers.py +3959 -0
  582. app/modules/proxy/_service/http_bridge/mixin.py +2435 -0
  583. app/modules/proxy/_service/http_bridge/owner_forwarding.py +609 -0
  584. app/modules/proxy/_service/http_bridge/protocol.py +167 -0
  585. app/modules/proxy/_service/http_bridge/proxy_failover.py +58 -0
  586. app/modules/proxy/_service/http_bridge/quarantine.py +443 -0
  587. app/modules/proxy/_service/http_bridge/request_submit.py +4582 -0
  588. app/modules/proxy/_service/http_bridge/retry_circuit.py +2442 -0
  589. app/modules/proxy/_service/http_bridge/service_stubs.py +492 -0
  590. app/modules/proxy/_service/http_bridge/session_registry.py +778 -0
  591. app/modules/proxy/_service/http_bridge/streaming.py +5328 -0
  592. app/modules/proxy/_service/http_bridge/upstream_events.py +4538 -0
  593. app/modules/proxy/_service/observability.py +410 -0
  594. app/modules/proxy/_service/rate_limit.py +361 -0
  595. app/modules/proxy/_service/realtime_live.py +655 -0
  596. app/modules/proxy/_service/refresh.py +154 -0
  597. app/modules/proxy/_service/request_log.py +585 -0
  598. app/modules/proxy/_service/response_create.py +930 -0
  599. app/modules/proxy/_service/streaming/__init__.py +3 -0
  600. app/modules/proxy/_service/streaming/helpers.py +1317 -0
  601. app/modules/proxy/_service/streaming/mixin.py +1100 -0
  602. app/modules/proxy/_service/streaming/protocol.py +32 -0
  603. app/modules/proxy/_service/streaming/retry.py +3762 -0
  604. app/modules/proxy/_service/support.py +2122 -0
  605. app/modules/proxy/_service/transcribe.py +434 -0
  606. app/modules/proxy/_service/warmup.py +483 -0
  607. app/modules/proxy/_service/websocket/__init__.py +3 -0
  608. app/modules/proxy/_service/websocket/helpers.py +2360 -0
  609. app/modules/proxy/_service/websocket/mixin.py +7285 -0
  610. app/modules/proxy/_service/websocket/protocol.py +74 -0
  611. app/modules/proxy/account_cache.py +229 -0
  612. app/modules/proxy/account_eligibility.py +87 -0
  613. app/modules/proxy/additional_model_limits.py +40 -0
  614. app/modules/proxy/affinity.py +830 -0
  615. app/modules/proxy/affinity_observation.py +101 -0
  616. app/modules/proxy/api.py +10254 -0
  617. app/modules/proxy/api_key_usage.py +113 -0
  618. app/modules/proxy/cap_partitioning.py +280 -0
  619. app/modules/proxy/capability_lineage.py +65 -0
  620. app/modules/proxy/capability_lineage_repository.py +82 -0
  621. app/modules/proxy/capability_routing.py +253 -0
  622. app/modules/proxy/continuity.py +112 -0
  623. app/modules/proxy/downstream_delivery.py +277 -0
  624. app/modules/proxy/durable_bridge_coordinator.py +1217 -0
  625. app/modules/proxy/durable_bridge_repository.py +4516 -0
  626. app/modules/proxy/durable_bridge_runtime.py +9 -0
  627. app/modules/proxy/durable_bridge_transcript_codec.py +122 -0
  628. app/modules/proxy/fair_share.py +131 -0
  629. app/modules/proxy/file_pin_repository.py +210 -0
  630. app/modules/proxy/helpers.py +439 -0
  631. app/modules/proxy/http_bridge_event_batcher.py +613 -0
  632. app/modules/proxy/http_bridge_forwarding.py +797 -0
  633. app/modules/proxy/http_continuation.py +76 -0
  634. app/modules/proxy/images_observability.py +62 -0
  635. app/modules/proxy/images_service.py +1054 -0
  636. app/modules/proxy/load_balancer.py +3016 -0
  637. app/modules/proxy/rate_limit_cache.py +46 -0
  638. app/modules/proxy/replay_safety.py +1063 -0
  639. app/modules/proxy/repo_bundle.py +31 -0
  640. app/modules/proxy/request_policy.py +1054 -0
  641. app/modules/proxy/ring_membership.py +251 -0
  642. app/modules/proxy/schemas.py +345 -0
  643. app/modules/proxy/selection_errors.py +41 -0
  644. app/modules/proxy/service.py +2600 -0
  645. app/modules/proxy/source_admission.py +125 -0
  646. app/modules/proxy/source_dispatch.py +999 -0
  647. app/modules/proxy/sticky_repository.py +831 -0
  648. app/modules/proxy/thread_anchors.py +491 -0
  649. app/modules/proxy/tool_call_dedupe.py +802 -0
  650. app/modules/proxy/types.py +54 -0
  651. app/modules/proxy/work_admission.py +121 -0
  652. app/modules/quota_planner/__init__.py +1 -0
  653. app/modules/quota_planner/api.py +276 -0
  654. app/modules/quota_planner/logic.py +743 -0
  655. app/modules/quota_planner/repository.py +722 -0
  656. app/modules/quota_planner/scheduler.py +212 -0
  657. app/modules/quota_planner/schemas.py +102 -0
  658. app/modules/quota_planner/warmup.py +693 -0
  659. app/modules/rate_limit_reset_credits/__init__.py +0 -0
  660. app/modules/rate_limit_reset_credits/api.py +601 -0
  661. app/modules/rate_limit_reset_credits/redeem_coordination.py +274 -0
  662. app/modules/rate_limit_reset_credits/store.py +114 -0
  663. app/modules/reports/__init__.py +1 -0
  664. app/modules/reports/api.py +134 -0
  665. app/modules/reports/cache.py +74 -0
  666. app/modules/reports/filters.py +27 -0
  667. app/modules/reports/repository.py +525 -0
  668. app/modules/reports/rollup.py +150 -0
  669. app/modules/reports/rollup_read.py +148 -0
  670. app/modules/reports/schemas.py +123 -0
  671. app/modules/reports/service.py +287 -0
  672. app/modules/reports/thread_identity.py +256 -0
  673. app/modules/request_logs/__init__.py +0 -0
  674. app/modules/request_logs/api.py +224 -0
  675. app/modules/request_logs/cost_backfill.py +154 -0
  676. app/modules/request_logs/mappers.py +97 -0
  677. app/modules/request_logs/repository.py +1765 -0
  678. app/modules/request_logs/schemas.py +145 -0
  679. app/modules/request_logs/service.py +348 -0
  680. app/modules/role_mappings/__init__.py +0 -0
  681. app/modules/role_mappings/api.py +206 -0
  682. app/modules/role_mappings/matching.py +55 -0
  683. app/modules/role_mappings/repository.py +106 -0
  684. app/modules/role_mappings/schemas.py +67 -0
  685. app/modules/role_mappings/service.py +334 -0
  686. app/modules/runtime/__init__.py +1 -0
  687. app/modules/runtime/api.py +18 -0
  688. app/modules/runtime/schemas.py +14 -0
  689. app/modules/runtime/service.py +218 -0
  690. app/modules/scim/__init__.py +1 -0
  691. app/modules/scim/api.py +194 -0
  692. app/modules/scim/dependencies.py +112 -0
  693. app/modules/scim/errors.py +77 -0
  694. app/modules/scim/management_api.py +156 -0
  695. app/modules/scim/repository.py +212 -0
  696. app/modules/scim/schemas.py +145 -0
  697. app/modules/scim/service.py +786 -0
  698. app/modules/scim/tokens.py +43 -0
  699. app/modules/settings/__init__.py +1 -0
  700. app/modules/settings/api.py +1677 -0
  701. app/modules/settings/repository.py +606 -0
  702. app/modules/settings/schemas.py +496 -0
  703. app/modules/settings/service.py +769 -0
  704. app/modules/shared/__init__.py +0 -0
  705. app/modules/shared/schemas.py +22 -0
  706. app/modules/sticky_sessions/__init__.py +1 -0
  707. app/modules/sticky_sessions/api.py +118 -0
  708. app/modules/sticky_sessions/cleanup_scheduler.py +487 -0
  709. app/modules/sticky_sessions/schemas.py +78 -0
  710. app/modules/sticky_sessions/service.py +196 -0
  711. app/modules/telemetry/__init__.py +5 -0
  712. app/modules/telemetry/api.py +115 -0
  713. app/modules/telemetry/clients.py +62 -0
  714. app/modules/telemetry/consent.py +115 -0
  715. app/modules/telemetry/scheduler.py +85 -0
  716. app/modules/telemetry/schemas.py +163 -0
  717. app/modules/telemetry/sender.py +221 -0
  718. app/modules/telemetry/snapshot.py +490 -0
  719. app/modules/usage/__init__.py +0 -0
  720. app/modules/usage/additional_quota_keys.py +379 -0
  721. app/modules/usage/api.py +45 -0
  722. app/modules/usage/background_repository.py +147 -0
  723. app/modules/usage/builders.py +559 -0
  724. app/modules/usage/depletion_service.py +277 -0
  725. app/modules/usage/live_ingest.py +356 -0
  726. app/modules/usage/mappers.py +29 -0
  727. app/modules/usage/plan_downgrade_observations.py +488 -0
  728. app/modules/usage/repository.py +1890 -0
  729. app/modules/usage/schemas.py +68 -0
  730. app/modules/usage/service.py +102 -0
  731. app/modules/usage/updater.py +1488 -0
  732. app/static/assets/Trans-Cj-J9mVA.js +1 -0
  733. app/static/assets/access-page-DHzZiXZi.js +1 -0
  734. app/static/assets/account-cost-donut-DGhfA4U8.js +1 -0
  735. app/static/assets/account-identifiers-CxbdLshZ.js +1 -0
  736. app/static/assets/account-status-DIsU5sUS.js +1 -0
  737. app/static/assets/account-trend-chart-BptlfwwQ.js +1 -0
  738. app/static/assets/accounts-page-Bbai8ift.js +3 -0
  739. app/static/assets/api-Dwa7XxPe.js +1 -0
  740. app/static/assets/api-client-dugX1Pti.js +55 -0
  741. app/static/assets/api-key-create-dialog-DnRm0oYN.js +1 -0
  742. app/static/assets/api-key-created-dialog-B6jkDG9f.js +1 -0
  743. app/static/assets/api-key-edit-dialog-Ck3Bnc3V.js +1 -0
  744. app/static/assets/api-trend-chart-BchCw7Hj.js +1 -0
  745. app/static/assets/apis-page-BvXz9c6i.js +2 -0
  746. app/static/assets/arrow-up-Dqh_KzJo.js +1 -0
  747. app/static/assets/arrow-up-down-LBRgVDGY.js +1 -0
  748. app/static/assets/automations-page-DQgNAJOR.js +3 -0
  749. app/static/assets/badge-DqMb9es0.js +1 -0
  750. app/static/assets/blur-email-HEvKagG2.js +1 -0
  751. app/static/assets/button-BjVmmiGW.js +1 -0
  752. app/static/assets/checkbox-apsl9vzk.js +1 -0
  753. app/static/assets/chevron-left-1d_pHVa5.js +1 -0
  754. app/static/assets/circle-alert-BS0xd_o1.js +1 -0
  755. app/static/assets/clsx-DB0hHKMi.js +1 -0
  756. app/static/assets/colors-BBy8GEtr.js +1 -0
  757. app/static/assets/copy-button-D7nAvOKR.js +1 -0
  758. app/static/assets/cost-per-day-chart-YdqlkZVM.js +1 -0
  759. app/static/assets/createLucideIcon-DBLzLNh2.js +1 -0
  760. app/static/assets/dashboard-page-CSdhBy0f.js +2 -0
  761. app/static/assets/distribution-metric-format-yZTy1ZEu.js +1 -0
  762. app/static/assets/donut-chart-Bxc69cra.js +1 -0
  763. app/static/assets/download-kQteIcsB.js +1 -0
  764. app/static/assets/ellipsis-B_5OgIre.js +1 -0
  765. app/static/assets/empty-state-JcA7tc8m.js +1 -0
  766. app/static/assets/es2015-bMH8YKZT.js +41 -0
  767. app/static/assets/es6-B264ni6W.js +69 -0
  768. app/static/assets/external-link-UVAOgMQ0.js +1 -0
  769. app/static/assets/i18n-BWqZ9Ua2.js +1 -0
  770. app/static/assets/inbox-B_RM2s1J.js +1 -0
  771. app/static/assets/index-DIpd264Z.js +23 -0
  772. app/static/assets/index-DMeLKRJx.css +2 -0
  773. app/static/assets/inherit-badge-QhTYVaU6.js +1 -0
  774. app/static/assets/invite-dialog-0CYh0GiQ.js +1 -0
  775. app/static/assets/iso-Bpe5Ihbb.js +1 -0
  776. app/static/assets/label-CGaLmLNf.js +1 -0
  777. app/static/assets/lazy-recharts-Pv8p9ArM.js +2 -0
  778. app/static/assets/loading-overlay-CzKQfHLI.js +1 -0
  779. app/static/assets/mini-quota-bar-cGT_t8Ae.js +1 -0
  780. app/static/assets/model-distribution-donut-BKjAfT48.js +1 -0
  781. app/static/assets/model-source-multi-select-D2d06Vc7.js +1 -0
  782. app/static/assets/multi-select-filter-KIA5qU6Z.js +1 -0
  783. app/static/assets/oauth-dialog-XeaGGDjS.js +1 -0
  784. app/static/assets/pagination-controls-D1Ypd0RU.js +1 -0
  785. app/static/assets/payload-CIYnGZz9.js +1 -0
  786. app/static/assets/queue-wait-chart-VoV7Yg9L.js +1 -0
  787. app/static/assets/report-chart-card-BXDliwcc.js +1 -0
  788. app/static/assets/reports-page-CGCJeMHG.js +3 -0
  789. app/static/assets/reset-credit-confirm-dialog-QOifTnVB.js +1 -0
  790. app/static/assets/rolldown-runtime-hePW80VL.js +1 -0
  791. app/static/assets/search-HfhqwERt.js +1 -0
  792. app/static/assets/select-Cl33bz-g.js +1 -0
  793. app/static/assets/settings-page-mZj01HYl.js +2 -0
  794. app/static/assets/shim-CZwyM4Q7.js +1 -0
  795. app/static/assets/skeleton-C_EUc4Rp.js +1 -0
  796. app/static/assets/sparkline-chart-DdNNsddM.js +1 -0
  797. app/static/assets/status-badge-CHHA5X6c.js +1 -0
  798. app/static/assets/switch-CzxgokcM.js +1 -0
  799. app/static/assets/table-DXaNSQ0G.js +1 -0
  800. app/static/assets/time-to-first-token-chart--68L0u_v.js +1 -0
  801. app/static/assets/tokens-per-day-chart-Cs82BFuE.js +1 -0
  802. app/static/assets/tokens-per-second-chart-AD2dfOn6.js +1 -0
  803. app/static/assets/totp-settings-CRHtxQ7j.js +1 -0
  804. app/static/assets/trash-D3KyWr1h.js +1 -0
  805. app/static/assets/use-account-quota-display-BPmrTBTC.js +1 -0
  806. app/static/assets/use-accounts-PWlVGw3z.js +1 -0
  807. app/static/assets/use-auth-CuDlW3iC.js +1 -0
  808. app/static/assets/use-chart-colors-DIu6tClI.js +1 -0
  809. app/static/assets/use-model-sources-Bul24JBu.js +1 -0
  810. app/static/assets/use-models-DUCPUuZg.js +1 -0
  811. app/static/assets/use-reduced-motion-hAm5r6_d.js +1 -0
  812. app/static/assets/useTranslation--56nHhlH.js +1 -0
  813. app/static/assets/useragent-distribution-donut-Dup6sf_n.js +1 -0
  814. app/static/assets/users-uIC3TIlE.js +1 -0
  815. app/static/assets/vendor-query-_bHZuxEx.js +1 -0
  816. app/static/assets/vendor-react-BVL0YZPW.js +9 -0
  817. app/static/favicon.svg +10 -0
  818. app/static/fonts/GeistSans-Variable.woff2 +0 -0
  819. app/static/fonts/jetbrains-mono-latin-ext.woff2 +0 -0
  820. app/static/fonts/jetbrains-mono-latin.woff2 +0 -0
  821. app/static/index.html +29 -0
  822. codexlb_up-1.25.0b10.dist-info/METADATA +124 -0
  823. codexlb_up-1.25.0b10.dist-info/RECORD +827 -0
  824. codexlb_up-1.25.0b10.dist-info/WHEEL +4 -0
  825. codexlb_up-1.25.0b10.dist-info/entry_points.txt +3 -0
  826. codexlb_up-1.25.0b10.dist-info/licenses/LICENSE +21 -0
  827. config/additional_quota_registry.json +11 -0
app/main.py ADDED
@@ -0,0 +1,1240 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import logging
5
+ import mimetypes
6
+ import os
7
+ import stat
8
+ import sys
9
+ import time
10
+ from collections.abc import Awaitable
11
+ from contextlib import asynccontextmanager
12
+ from datetime import timedelta
13
+ from functools import lru_cache
14
+ from importlib import import_module
15
+ from ipaddress import ip_address
16
+ from pathlib import Path, PurePosixPath
17
+ from typing import Any, Protocol, cast
18
+ from urllib.parse import urlparse
19
+
20
+ import aiohttp
21
+ from fastapi import FastAPI, HTTPException
22
+ from fastapi.responses import FileResponse
23
+ from starlette.staticfiles import StaticFiles
24
+
25
+ from app.core.audit.service import drain_audit_log_tasks
26
+ from app.core.auth.dashboard_users_cache import get_dashboard_users_cache
27
+ from app.core.auth.guardian import build_auth_guardian_scheduler
28
+ from app.core.auth.providers.registry import get_auth_provider_registry
29
+ from app.core.balancer import configure_replica_salt
30
+ from app.core.bootstrap import ensure_auto_bootstrap_token, log_bootstrap_token
31
+ from app.core.clients.http import close_http_client, init_http_client
32
+ from app.core.clients.native_egress import close_discovered_native_egress_client
33
+ from app.core.config.dashboard_overrides import effective_settings
34
+ from app.core.config.key_fingerprint import verify_encryption_key_fingerprint
35
+ from app.core.config.settings import (
36
+ Settings,
37
+ _bridge_advertise_hostname_is_replica_specific,
38
+ _parse_port_value,
39
+ get_settings,
40
+ warn_removed_settings,
41
+ )
42
+ from app.core.config.settings_cache import get_settings_cache
43
+ from app.core.config.spool_retention import (
44
+ resolve_operation_spool_retention_seconds,
45
+ warn_spool_retention_below_floor,
46
+ )
47
+ from app.core.handlers import add_exception_handlers
48
+ from app.core.metrics.middleware import MetricsMiddleware
49
+ from app.core.metrics.prometheus import MULTIPROCESS_MODE, PROMETHEUS_AVAILABLE, make_scrape_registry, mark_process_dead
50
+ from app.core.middleware import (
51
+ add_api_firewall_middleware,
52
+ add_app_version_middleware,
53
+ add_backend_api_codex_v1_alias_middleware,
54
+ add_dashboard_auth_proxy_middleware,
55
+ add_dashboard_csrf_middleware,
56
+ add_multipart_content_encoding_middleware,
57
+ add_request_body_limit_middleware,
58
+ add_request_decompression_middleware,
59
+ add_request_id_middleware,
60
+ add_required_capability_http_middleware,
61
+ add_trusted_proxy_headers_middleware,
62
+ )
63
+ from app.core.middleware.dashboard_gzip import add_dashboard_gzip_middleware
64
+ from app.core.middleware.dashboard_overrides import DashboardOverridesMiddleware
65
+ from app.core.middleware.inflight import InFlightMiddleware
66
+ from app.core.openai.model_refresh_scheduler import build_model_refresh_scheduler
67
+ from app.core.resilience.backpressure import BackpressureMiddleware
68
+ from app.core.resilience.bulkhead import BulkheadMiddleware, get_bulkhead
69
+ from app.core.resilience.loop_lag_monitor import run_event_loop_lag_monitor
70
+ from app.core.resilience.memory_monitor import configure as configure_memory_monitor
71
+ from app.core.retention.scheduler import build_data_retention_scheduler
72
+ from app.core.runtime_logging import install_redacting_loop_exception_handler
73
+ from app.core.scheduling.leader_election import get_leader_election
74
+ from app.core.shutdown import close_control_plane_task_admission
75
+ from app.core.timeout_invariants import validate_runtime_timeout_invariants, validate_timeout_invariants
76
+ from app.core.usage.metadata_scheduler import build_metadata_refresh_scheduler
77
+ from app.core.usage.refresh_scheduler import build_usage_refresh_scheduler
78
+ from app.core.usage.reset_credits_refresh_scheduler import build_rate_limit_reset_credits_scheduler
79
+ from app.core.utils.time import utcnow
80
+ from app.db.session import (
81
+ SessionLocal,
82
+ close_db,
83
+ close_session,
84
+ init_background_db,
85
+ init_db,
86
+ mark_sqlite_shutdown_clean,
87
+ )
88
+ from app.modules.accounts import api as accounts_api
89
+ from app.modules.accounts.deletion import build_account_deletion_scheduler
90
+ from app.modules.accounts.repository import AccountsRepository
91
+ from app.modules.accounts.usage_rollup_scheduler import build_account_usage_rollup_scheduler
92
+ from app.modules.api_keys import api as api_keys_api
93
+ from app.modules.api_keys.last_used_coalescer import build_api_key_last_used_flush_scheduler
94
+ from app.modules.api_keys.reset_scheduler import build_api_key_limit_reset_scheduler
95
+ from app.modules.audit import api as audit_api
96
+ from app.modules.auth_providers import api as auth_providers_api
97
+ from app.modules.automations import api as automations_api
98
+ from app.modules.automations.scheduler import build_automations_scheduler
99
+ from app.modules.cache_isolation_probe import api as cache_isolation_probe_api
100
+ from app.modules.conversation_archive import api as conversation_archive_api
101
+ from app.modules.dashboard import api as dashboard_api
102
+ from app.modules.dashboard_auth import api as dashboard_auth_api
103
+ from app.modules.dashboard_roles import api as dashboard_roles_api
104
+ from app.modules.dashboard_users import api as dashboard_users_api
105
+ from app.modules.dashboard_users.identity_resolver import get_identity_resolution_cache
106
+ from app.modules.firewall import api as firewall_api
107
+ from app.modules.fleet import api as fleet_api
108
+ from app.modules.health import api as health_api
109
+ from app.modules.model_sources import api as model_sources_api
110
+ from app.modules.oauth import api as oauth_api
111
+ from app.modules.proxy import api as proxy_api
112
+ from app.modules.proxy.cap_partitioning import refresh_cap_partition
113
+ from app.modules.proxy.durable_bridge_coordinator import DurableBridgeSessionCoordinator
114
+ from app.modules.proxy.durable_bridge_repository import (
115
+ DURABLE_BRIDGE_OPERATION_SPOOL_PURGE_BATCH_SIZE,
116
+ missing_durable_bridge_tables,
117
+ )
118
+ from app.modules.proxy.durable_bridge_runtime import http_bridge_owner_process_epoch
119
+ from app.modules.proxy.rate_limit_cache import get_rate_limit_headers_cache
120
+ from app.modules.proxy.ring_membership import (
121
+ RING_HEARTBEAT_INTERVAL_SECONDS,
122
+ RING_STALE_GRACE_SECONDS,
123
+ RING_STALE_THRESHOLD_SECONDS,
124
+ RingMembershipService,
125
+ )
126
+ from app.modules.quota_planner import api as quota_planner_api
127
+ from app.modules.quota_planner.scheduler import build_quota_planner_scheduler
128
+ from app.modules.rate_limit_reset_credits import api as rate_limit_reset_credits_api
129
+ from app.modules.reports import api as reports_api
130
+ from app.modules.reports.cache import ReportsCaches
131
+ from app.modules.request_logs import api as request_logs_api
132
+ from app.modules.role_mappings import api as role_mappings_api
133
+ from app.modules.runtime import api as runtime_api
134
+ from app.modules.scim import api as scim_api
135
+ from app.modules.scim import management_api as scim_tokens_api
136
+ from app.modules.settings import api as settings_api
137
+ from app.modules.settings.service import warn_environment_shadowed_by_dashboard
138
+ from app.modules.sticky_sessions import api as sticky_sessions_api
139
+ from app.modules.sticky_sessions.cleanup_scheduler import (
140
+ OperationRetentionCleanupResult,
141
+ _abandoned_bridge_retention_seconds,
142
+ _record_operation_retention_cleanup,
143
+ build_sticky_session_cleanup_scheduler,
144
+ operation_retention_metrics_enabled,
145
+ )
146
+ from app.modules.telemetry import api as telemetry_api
147
+ from app.modules.telemetry.scheduler import build_telemetry_scheduler
148
+ from app.modules.usage import api as usage_api
149
+ from app.modules.usage.additional_quota_keys import reload_additional_quota_registry
150
+ from app.modules.usage.live_ingest import start_live_usage_ingestor, stop_live_usage_ingestor
151
+
152
+ logger = logging.getLogger(__name__)
153
+
154
+ # On Windows, ``mimetypes`` merges HKCR registry mappings where third-party
155
+ # software commonly remaps web extensions (``.js`` -> ``text/plain``), and
156
+ # browsers enforce strict MIME checking for ES module scripts, so a poisoned
157
+ # mapping renders the dashboard as a blank page (issue #1698). ``FileResponse``
158
+ # resolves ``media_type`` through ``mimetypes.guess_type``, so pin every
159
+ # extension the built dashboard serves; ``add_type`` wins over the merged
160
+ # registry table on all platforms.
161
+ _WEB_ASSET_MIME_TYPES: dict[str, str] = {
162
+ ".js": "text/javascript",
163
+ ".mjs": "text/javascript",
164
+ ".css": "text/css",
165
+ ".svg": "image/svg+xml",
166
+ ".json": "application/json",
167
+ ".woff2": "font/woff2",
168
+ ".woff": "font/woff",
169
+ ".html": "text/html",
170
+ }
171
+
172
+
173
+ def _ensure_web_asset_mime_types() -> None:
174
+ for extension, mime_type in _WEB_ASSET_MIME_TYPES.items():
175
+ mimetypes.add_type(mime_type, extension)
176
+
177
+
178
+ _ensure_web_asset_mime_types()
179
+
180
+
181
+ async def run_http_bridge_heartbeat_maintenance(proxy_service: Any) -> None:
182
+ """Per-replica bridge upkeep driven by the ring heartbeat.
183
+
184
+ All passes are request-independent by design: durable ownership must be
185
+ reconciled even on a replica nothing is routing to, and the idle sweep is
186
+ otherwise only reached from ``_get_or_create_http_bridge_session``, so a
187
+ replica that stops taking bridge requests would keep its idle sessions'
188
+ upstream WebSockets open until restart (issue #1354). Each pass is isolated
189
+ so one failing cannot skip the others or stop the heartbeat.
190
+ """
191
+ if proxy_service is None:
192
+ return
193
+ for attribute, failure_message in (
194
+ ("reconcile_durable_http_bridge_ownership", "HTTP bridge durable ownership reconciliation failed"),
195
+ ("abandon_stale_http_bridge_operations", "HTTP bridge stale operation abandonment failed"),
196
+ ("prune_idle_http_bridge_sessions", "HTTP bridge idle sweep failed"),
197
+ ):
198
+ pass_callable = getattr(proxy_service, attribute, None)
199
+ if pass_callable is None:
200
+ continue
201
+ try:
202
+ await pass_callable()
203
+ except Exception:
204
+ logger.warning(failure_message, exc_info=True)
205
+
206
+
207
+ def _log_abandoned_lease_release(task: asyncio.Task[bool]) -> None:
208
+ if task.cancelled():
209
+ return
210
+ exc = task.exception()
211
+ if exc is not None:
212
+ logger.warning("Abandoned scheduler leader lease release finished with error", exc_info=exc)
213
+
214
+
215
+ async def _release_leader_lease_within(timeout: float) -> bool:
216
+ """Release the scheduler leader lease without ever pinning shutdown.
217
+
218
+ ``release()`` uses a background DB session whose rollback/close shield and
219
+ await their own cleanup, so wrapping it in ``asyncio.wait_for`` would only
220
+ cancel the awaiting wrapper while a wedged database call keeps unwinding —
221
+ shutdown could still hang past the deadline. Run the release as a task and,
222
+ if it does not finish within ``timeout``, abandon it (logging its eventual
223
+ outcome from a done callback) so shutdown always proceeds within the
224
+ deadline; the lease then expires after its TTL, which is acceptable.
225
+ """
226
+ release_task: asyncio.Task[bool] = asyncio.ensure_future(get_leader_election().release())
227
+ done, _ = await asyncio.wait({release_task}, timeout=timeout)
228
+ if release_task not in done:
229
+ logger.warning(
230
+ "Scheduler leader lease release did not finish within %.1fs; abandoning it so "
231
+ "shutdown can proceed (the lease will expire after its TTL)",
232
+ timeout,
233
+ )
234
+ release_task.add_done_callback(_log_abandoned_lease_release)
235
+ return False
236
+ if release_task.cancelled():
237
+ logger.warning("Scheduler leader lease release was cancelled during shutdown")
238
+ return False
239
+ exc = release_task.exception()
240
+ if exc is not None:
241
+ logger.warning("Failed to release scheduler leader lease during shutdown", exc_info=exc)
242
+ return False
243
+ if release_task.result() is False:
244
+ logger.warning("Scheduler leader lease release did not complete; suppressing the SQLite clean marker")
245
+ return False
246
+ return True
247
+
248
+
249
+ async def _drain_proxy_persistence_tasks(
250
+ proxy_service: Any,
251
+ timeout_seconds: float,
252
+ *,
253
+ task_name_prefixes: tuple[str, ...] | None = None,
254
+ failure_message: str,
255
+ ) -> bool:
256
+ """Drain proxy persistence work within the caller's committed deadline."""
257
+ if proxy_service is None or not hasattr(proxy_service, "drain_persistence_tasks"):
258
+ return True
259
+ try:
260
+ kwargs: dict[str, Any] = {"timeout_seconds": timeout_seconds}
261
+ if task_name_prefixes is not None:
262
+ kwargs["task_name_prefixes"] = task_name_prefixes
263
+ return bool(await proxy_service.drain_persistence_tasks(**kwargs))
264
+ except Exception:
265
+ logger.warning(failure_message, exc_info=True)
266
+ return False
267
+
268
+
269
+ async def _close_proxy_http_bridge_sessions_for_shutdown(
270
+ proxy_service: Any,
271
+ *,
272
+ mark_draining: bool,
273
+ ) -> bool:
274
+ """Close bridge resources and report whether that database-owning step completed.
275
+
276
+ Bridge teardown owns durable leases and can enqueue persistence work. A
277
+ failed mark/close must therefore suppress the SQLite ``clean`` marker even
278
+ when the later persistence drain and engine disposal happen to complete.
279
+ ``None`` remains a successful result for older/test service doubles; the
280
+ concrete service returns ``True`` or ``False`` explicitly.
281
+ """
282
+ if proxy_service is None:
283
+ return True
284
+
285
+ bridge_sessions_drained = True
286
+ if mark_draining and hasattr(proxy_service, "mark_http_bridge_draining"):
287
+ try:
288
+ result = await proxy_service.mark_http_bridge_draining()
289
+ if result is False:
290
+ bridge_sessions_drained = False
291
+ except Exception:
292
+ logger.warning("Failed to mark HTTP bridge durable sessions draining during shutdown", exc_info=True)
293
+ bridge_sessions_drained = False
294
+
295
+ if hasattr(proxy_service, "close_all_http_bridge_sessions"):
296
+ try:
297
+ result = await proxy_service.close_all_http_bridge_sessions()
298
+ if result is False:
299
+ bridge_sessions_drained = False
300
+ except Exception:
301
+ logger.warning("Failed to close HTTP bridge sessions during shutdown", exc_info=True)
302
+ bridge_sessions_drained = False
303
+
304
+ return bridge_sessions_drained
305
+
306
+
307
+ async def _drain_detached_control_plane_tasks(timeout_seconds: float) -> bool:
308
+ # Closing admission is synchronous with producer checks on the event loop,
309
+ # so no task can appear after the stable drain passes complete.
310
+ close_control_plane_task_admission()
311
+ loop = asyncio.get_running_loop()
312
+ deadline = loop.time() + timeout_seconds
313
+ clean_passes = 0
314
+
315
+ while clean_passes < 2:
316
+ remaining = max(0.0, deadline - loop.time())
317
+ results = await asyncio.gather(
318
+ drain_audit_log_tasks(remaining),
319
+ fleet_api.drain_background_refresh_tasks(remaining),
320
+ return_exceptions=True,
321
+ )
322
+
323
+ clean_pass = True
324
+ for task_kind, result in zip(("audit log", "fleet refresh"), results, strict=True):
325
+ if isinstance(result, BaseException):
326
+ clean_pass = False
327
+ logger.warning(
328
+ "Failed to drain %s tasks during shutdown",
329
+ task_kind,
330
+ exc_info=(type(result), result, result.__traceback__),
331
+ )
332
+ elif result is False:
333
+ clean_pass = False
334
+
335
+ if not clean_pass:
336
+ return False
337
+
338
+ clean_passes += 1
339
+
340
+ # A done callback may enqueue sibling audit/fleet work after both
341
+ # registries looked empty. One extra stable pass catches that before
342
+ # HTTP clients and DB engines are torn down.
343
+ if clean_passes < 2:
344
+ await asyncio.sleep(0)
345
+ return True
346
+
347
+
348
+ class _MetricsServer(Protocol):
349
+ should_exit: bool
350
+
351
+ async def serve(self) -> None: ...
352
+
353
+
354
+ class _RingMembershipReader(Protocol):
355
+ def list_active(
356
+ self,
357
+ stale_threshold_seconds: int = RING_STALE_THRESHOLD_SECONDS,
358
+ *,
359
+ require_endpoint: bool = False,
360
+ ) -> Awaitable[list[str]]: ...
361
+
362
+
363
+ @lru_cache(maxsize=4)
364
+ def _static_files_for_root(static_root: Path) -> StaticFiles:
365
+ return StaticFiles(directory=static_root, check_dir=False)
366
+
367
+
368
+ def _resolve_static_asset_path(static_root: Path, requested_path: str) -> Path | None:
369
+ """Return a filesystem path for a SPA asset only when it stays under static_root."""
370
+ normalized = PurePosixPath(requested_path)
371
+ if normalized.is_absolute() or ".." in normalized.parts:
372
+ return None
373
+ full_path, stat_result = _static_files_for_root(static_root).lookup_path(normalized.as_posix())
374
+ if stat_result is None or not stat.S_ISREG(stat_result.st_mode):
375
+ return None
376
+ return Path(full_path)
377
+
378
+
379
+ def _is_metrics_bind_conflict(exc: BaseException) -> bool:
380
+ if isinstance(exc, SystemExit):
381
+ return exc.code == 1
382
+ if isinstance(exc, OSError):
383
+ import errno as _errno
384
+
385
+ return exc.errno in (_errno.EADDRINUSE, _errno.EADDRNOTAVAIL)
386
+ return False
387
+
388
+
389
+ def _is_benign_metrics_bind_failure(exc: BaseException) -> bool:
390
+ return MULTIPROCESS_MODE and _is_metrics_bind_conflict(exc)
391
+
392
+
393
+ def _log_non_multiproc_metrics_bind_conflict(port: int) -> None:
394
+ logger.error(
395
+ "Metrics port %d is already bound by another worker process but PROMETHEUS_MULTIPROC_DIR is not set: "
396
+ "/metrics reflects only the winning worker's counters (1/N of traffic). "
397
+ "Set PROMETHEUS_MULTIPROC_DIR to a writable directory shared by all workers to aggregate metrics "
398
+ "across worker processes.",
399
+ port,
400
+ )
401
+
402
+
403
+ async def _close_db_and_record_clean_shutdown(
404
+ *,
405
+ database_tasks_drained: bool,
406
+ leader_lease_release_completed: bool,
407
+ ) -> None:
408
+ """Dispose the database engines, then record the shutdown as clean.
409
+
410
+ The record is only reached once disposal returns and every database-owning
411
+ shutdown drain completed. A cancellation, failed dispose, or abandoned
412
+ drain must leave the run state unclean, because that is exactly the
413
+ incomplete shutdown the next startup's integrity scan is for.
414
+ """
415
+ sqlite_teardown_drained = await close_db()
416
+ if sqlite_teardown_drained and database_tasks_drained and leader_lease_release_completed:
417
+ mark_sqlite_shutdown_clean()
418
+
419
+
420
+ async def _purge_operation_spool_on_startup(*, retention_seconds: float) -> int:
421
+ """Run one bounded transcript purge and record a sanitized aggregate result."""
422
+
423
+ started_at = time.monotonic()
424
+ try:
425
+ operation_purge_result = await DurableBridgeSessionCoordinator(SessionLocal).purge_operation_spool_batch(
426
+ cutoff=utcnow() - timedelta(seconds=retention_seconds),
427
+ )
428
+ except Exception as exc:
429
+ result = OperationRetentionCleanupResult(
430
+ deleted_operations=0,
431
+ batches=0,
432
+ backlog_likely=True,
433
+ outcome="failed",
434
+ duration_seconds=max(time.monotonic() - started_at, 0.0),
435
+ )
436
+ _record_operation_retention_cleanup(result)
437
+ logger.warning(
438
+ "HTTP bridge operation transcript startup retention failed "
439
+ "deleted_operations=0 batches=0 outcome=failed backlog_likely=true "
440
+ "duration_seconds=%.3f error_type=%s",
441
+ result.duration_seconds,
442
+ type(exc).__name__,
443
+ )
444
+ raise RuntimeError("HTTP bridge operation transcript startup retention failed") from None
445
+
446
+ backlog_likely = operation_purge_result.selected_operations >= DURABLE_BRIDGE_OPERATION_SPOOL_PURGE_BATCH_SIZE
447
+ result = OperationRetentionCleanupResult(
448
+ deleted_operations=operation_purge_result.deleted_operations,
449
+ batches=1,
450
+ backlog_likely=backlog_likely,
451
+ outcome="batch_budget_exhausted" if backlog_likely else "completed",
452
+ duration_seconds=max(time.monotonic() - started_at, 0.0),
453
+ )
454
+ _record_operation_retention_cleanup(result)
455
+ if not operation_retention_metrics_enabled():
456
+ logger.info(
457
+ "HTTP bridge operation transcript startup retention "
458
+ "deleted_operations=%s batches=%s outcome=%s "
459
+ "backlog_likely=%s duration_seconds=%.3f",
460
+ result.deleted_operations,
461
+ result.batches,
462
+ result.outcome,
463
+ result.backlog_likely,
464
+ result.duration_seconds,
465
+ )
466
+ return operation_purge_result.deleted_operations
467
+
468
+
469
+ async def _report_dashboard_timeout_overrides(settings: Settings) -> None:
470
+ """Best-effort startup report on the dashboard-managed timeouts (C2-1).
471
+
472
+ Names env aliases the dashboard shadows and logs (never raises) invariant
473
+ violations of the effective values — a stored dashboard value can break a
474
+ combination the environment alone satisfies. A snapshot read failure must
475
+ not abort boot: the environment fallback stays in force until the cache
476
+ recovers.
477
+ """
478
+ try:
479
+ dashboard_settings_row = await get_settings_cache().get()
480
+ except Exception: # noqa: BLE001 - startup must not depend on the snapshot
481
+ logger.debug("dashboard settings snapshot unavailable at startup; environment timeouts apply", exc_info=True)
482
+ return
483
+ warn_environment_shadowed_by_dashboard(dashboard_settings_row, settings)
484
+ validate_timeout_invariants(effective_settings(dashboard_settings_row, settings), strict=False, log=True)
485
+ # R2 spool retention: same warn-only shape. The environment alias alone can
486
+ # sit below the replay floor with the dashboard column NULL, a state the
487
+ # settings API never validated, so say so before an edit is refused.
488
+ warn_spool_retention_below_floor(dashboard_settings_row, settings)
489
+
490
+
491
+ @asynccontextmanager
492
+ async def lifespan(app: FastAPI):
493
+ import app.core.startup as startup_module
494
+
495
+ app.state.reports_caches = ReportsCaches()
496
+ shutdown_state = import_module("app.core.shutdown")
497
+ # First app code on uvicorn's loop: mask credential-bearing object reprs
498
+ # (aiohttp ConnectionKey proxy URLs, BasicAuth) before the default handler
499
+ # renders them into the 'asyncio' logger.
500
+ install_redacting_loop_exception_handler(asyncio.get_running_loop())
501
+ metrics_server = None
502
+ metrics_server_task: asyncio.Task[None] | None = None
503
+ ring_service = None
504
+ heartbeat_task: asyncio.Task[None] | None = None
505
+ instance_id = None
506
+
507
+ shutdown_state.prepare_lifespan_start()
508
+ startup_module._startup_complete = False
509
+ startup_module.reset_bridge_registration()
510
+ await get_settings_cache().invalidate(propagate=False)
511
+ await get_dashboard_users_cache().invalidate(propagate=False)
512
+ await get_rate_limit_headers_cache().invalidate()
513
+ reload_additional_quota_registry()
514
+ settings = get_settings()
515
+ warn_removed_settings()
516
+ validate_runtime_timeout_invariants(settings)
517
+ # Anchor round-robin tie-break decorrelation to this replica's stable bridge
518
+ # instance identity so peer replicas spread exact ties across equally-good
519
+ # accounts instead of all herding onto the lexicographically-first account.
520
+ configure_replica_salt(settings.http_responses_session_bridge_instance_id)
521
+ bridge_endpoint_base_url = settings.http_responses_session_bridge_advertise_base_url
522
+ await init_db()
523
+ init_background_db()
524
+ await verify_encryption_key_fingerprint()
525
+ _auto_bootstrap_token = await ensure_auto_bootstrap_token()
526
+ if _auto_bootstrap_token:
527
+ log_bootstrap_token(logger, _auto_bootstrap_token)
528
+ await init_http_client()
529
+ await _report_dashboard_timeout_overrides(settings)
530
+ bridge_durable_schema_ready = await _ensure_bridge_durable_schema_ready(settings)
531
+ if bridge_durable_schema_ready is True:
532
+ startup_module.mark_bridge_durable_schema_ready()
533
+ dashboard_settings = await get_settings_cache().get()
534
+ ownerless_cutoff = utcnow() - timedelta(seconds=_abandoned_bridge_retention_seconds(dashboard_settings))
535
+ deleted_bridge_rows = await DurableBridgeSessionCoordinator(SessionLocal).purge_owned_sessions_on_startup(
536
+ instance_id=settings.http_responses_session_bridge_instance_id,
537
+ owner_process_epoch=http_bridge_owner_process_epoch(),
538
+ ownerless_cutoff=ownerless_cutoff,
539
+ )
540
+ if deleted_bridge_rows > 0:
541
+ logger.info(
542
+ "Purged durable HTTP bridge rows from previous process instance",
543
+ extra={
544
+ "instance_id": settings.http_responses_session_bridge_instance_id,
545
+ "deleted": deleted_bridge_rows,
546
+ },
547
+ )
548
+ purged_operation_rows = await _purge_operation_spool_on_startup(
549
+ # R2 spool retention: the dashboard column wins over the deprecated
550
+ # env alias; ``dashboard_settings`` is the snapshot this startup
551
+ # step already loaded above.
552
+ retention_seconds=resolve_operation_spool_retention_seconds(dashboard_settings, startup_settings=settings),
553
+ )
554
+ if purged_operation_rows > 0:
555
+ logger.info(
556
+ "Purged expired durable HTTP bridge operation transcript rows",
557
+ extra={"deleted": purged_operation_rows},
558
+ )
559
+ from app.core.auth.api_key_cache import get_api_key_cache
560
+ from app.core.cache.invalidation import (
561
+ NAMESPACE_ACCOUNT_ROUTING,
562
+ NAMESPACE_ACCOUNT_SELECTION,
563
+ NAMESPACE_API_KEY,
564
+ NAMESPACE_DASHBOARD_USERS,
565
+ NAMESPACE_FIREWALL,
566
+ NAMESPACE_MODEL_REGISTRY,
567
+ NAMESPACE_RESET_CREDITS,
568
+ NAMESPACE_SETTINGS,
569
+ NAMESPACE_UPSTREAM_ROUTE,
570
+ CacheInvalidationPoller,
571
+ get_cache_invalidation_poller,
572
+ set_cache_invalidation_poller,
573
+ )
574
+ from app.core.config.context_window_overrides import get_model_context_window_overrides_cache
575
+ from app.core.middleware.firewall_cache import get_firewall_ip_cache
576
+ from app.core.upstream_proxy.cache import get_upstream_route_cache
577
+ from app.modules.proxy.account_cache import get_account_selection_cache, get_routing_availability_cache
578
+ from app.modules.rate_limit_reset_credits.store import get_rate_limit_reset_credits_store
579
+
580
+ # The poller MUST be installed before the model scheduler starts: a first
581
+ # leader tick that persists a changed snapshot bumps the model_registry
582
+ # namespace through the global poller, and a not-yet-installed poller would
583
+ # silently drop that bump (followers would then wait for the refresh-tick
584
+ # backstop instead of the cache poll bound).
585
+ cache_poller = CacheInvalidationPoller(SessionLocal)
586
+ cache_poller.on_invalidation(NAMESPACE_API_KEY, get_api_key_cache().clear)
587
+ cache_poller.on_invalidation(NAMESPACE_FIREWALL, get_firewall_ip_cache().invalidate_all)
588
+ routing_availability_cache = get_routing_availability_cache()
589
+ # Remote-bump callbacks must be non-propagating variants: a propagating callback
590
+ # would re-bump on every observed bump and feedback-loop across replicas.
591
+ cache_poller.on_invalidation(NAMESPACE_ACCOUNT_ROUTING, routing_availability_cache.refresh_from_db)
592
+ cache_poller.on_invalidation(
593
+ NAMESPACE_ACCOUNT_SELECTION,
594
+ lambda: get_account_selection_cache().invalidate(propagate=False),
595
+ )
596
+ cache_poller.on_invalidation(
597
+ NAMESPACE_SETTINGS,
598
+ lambda: get_settings_cache().invalidate(propagate=False),
599
+ )
600
+ # Then pull the new row in. Readers that cannot await the cache (the
601
+ # conversation archive gate runs per archived frame) would otherwise keep
602
+ # the pre-change snapshot on a replica that is only carrying already-open
603
+ # streams; the invalidate above already expired it, so a failed refresh
604
+ # degrades to the ordinary TTL reload instead of serving a stale value.
605
+ cache_poller.on_invalidation(NAMESPACE_SETTINGS, get_settings_cache().refresh)
606
+ cache_poller.on_invalidation(
607
+ NAMESPACE_DASHBOARD_USERS,
608
+ lambda: get_dashboard_users_cache().invalidate(propagate=False),
609
+ )
610
+ # Provider settings and identity resolutions ride the same bus: a PATCH on
611
+ # one replica bumps dashboard_users, every replica drops both caches.
612
+ cache_poller.on_invalidation(NAMESPACE_DASHBOARD_USERS, get_auth_provider_registry().clear)
613
+ cache_poller.on_invalidation(NAMESPACE_DASHBOARD_USERS, get_identity_resolution_cache().clear)
614
+ cache_poller.on_invalidation(NAMESPACE_UPSTREAM_ROUTE, get_upstream_route_cache().clear)
615
+ # The route resolver also reads the dashboard settings row (routing enabled
616
+ # + default pool id), so settings bumps clear resolved routes as well.
617
+ cache_poller.on_invalidation(NAMESPACE_SETTINGS, get_upstream_route_cache().clear)
618
+ # M4 model catalogue: the per-model context window override rows are
619
+ # invalidated through the settings namespace as well.
620
+ cache_poller.on_invalidation(
621
+ NAMESPACE_SETTINGS,
622
+ lambda: get_model_context_window_overrides_cache().invalidate(propagate=False),
623
+ )
624
+ # The bus carries no payload, so a peer redeem clears this replica's whole
625
+ # reset-credits store; the refresh scheduler repopulates it on its next tick.
626
+ cache_poller.on_invalidation(NAMESPACE_RESET_CREDITS, get_rate_limit_reset_credits_store().invalidate)
627
+ from app.core.openai.model_registry_store import reconcile_model_registry_from_store
628
+
629
+ # raise_on_error=True so a transient load failure leaves the
630
+ # model_registry version unacknowledged and is retried on the next poll
631
+ # cycle (matching the account_routing refresh callback) instead of being
632
+ # swallowed, which would strand this replica on the stale catalog until
633
+ # the non-leader scheduler backstop.
634
+ cache_poller.on_invalidation(
635
+ NAMESPACE_MODEL_REGISTRY,
636
+ lambda: reconcile_model_registry_from_store(raise_on_error=True),
637
+ )
638
+ set_cache_invalidation_poller(cache_poller)
639
+
640
+ # Seed the invalidation version baseline BEFORE loading the routing snapshot
641
+ # and BEFORE the one-shot model-registry reconcile below. prime() records the
642
+ # current version of every namespace (and flushes any pending bumps) without
643
+ # firing callbacks, so a peer bump committed after this point is observed as a
644
+ # change on the first background poll instead of being silently acknowledged
645
+ # as pre-existing state. In particular a leader that persists-and-bumps the
646
+ # model_registry namespace in the window between the reconcile's snapshot read
647
+ # and the poller's first background tick would otherwise be absorbed as the
648
+ # initial baseline, leaving this replica on the pre-refresh catalog until the
649
+ # non-leader scheduler backstop (default 300s) instead of the sub-second cache
650
+ # poll bound.
651
+ try:
652
+ await cache_poller.prime()
653
+ except Exception:
654
+ # prime() raises when the baseline version read fails, leaving the poller
655
+ # uninitialized so an explicit retry would remain baseline-only. Startup
656
+ # continues, but start() arms conservative recovery: the first successful
657
+ # background read invokes callbacks for positive versions before
658
+ # acknowledging them, so a peer bump cannot become a callback-less
659
+ # baseline after local caches are warm.
660
+ logger.warning("cache invalidation baseline prime failed", exc_info=True)
661
+ try:
662
+ await routing_availability_cache.refresh_from_db()
663
+ except Exception:
664
+ # Unseeded snapshot degrades to local-mark semantics; the next
665
+ # account_routing bump retries the refresh via the poller callback.
666
+ logger.warning("initial routing availability snapshot refresh failed", exc_info=True)
667
+
668
+ # Warm the in-memory registry from the persisted snapshot before any
669
+ # scheduler starts so a restarted replica serves the refreshed catalog
670
+ # instead of the bootstrap floor. Never fails startup.
671
+ await reconcile_model_registry_from_store()
672
+
673
+ await cache_poller.start()
674
+
675
+ async with SessionLocal() as session:
676
+ seeded_count = await AccountsRepository(session).seed_hard_sticky_outage_grace_on_startup()
677
+ if seeded_count > 0:
678
+ logger.info(
679
+ "Seeded hard codex_session purge grace for already-unavailable accounts seeded_count=%s",
680
+ seeded_count,
681
+ )
682
+
683
+ metadata_scheduler = build_metadata_refresh_scheduler()
684
+ usage_scheduler = build_usage_refresh_scheduler()
685
+ api_key_limit_reset_scheduler = build_api_key_limit_reset_scheduler()
686
+ api_key_last_used_flush_scheduler = build_api_key_last_used_flush_scheduler()
687
+ model_scheduler = build_model_refresh_scheduler()
688
+ sticky_session_cleanup_scheduler = build_sticky_session_cleanup_scheduler()
689
+ quota_planner_scheduler = build_quota_planner_scheduler()
690
+ auth_guardian_scheduler = build_auth_guardian_scheduler()
691
+ automations_scheduler = build_automations_scheduler()
692
+ rate_limit_reset_credits_scheduler = build_rate_limit_reset_credits_scheduler()
693
+ account_usage_rollup_scheduler = build_account_usage_rollup_scheduler()
694
+ account_deletion_scheduler = build_account_deletion_scheduler()
695
+ data_retention_scheduler = build_data_retention_scheduler()
696
+ telemetry_scheduler = build_telemetry_scheduler()
697
+ # Hold the instance: this lifespan owns it (and keeps it strongly rooted)
698
+ # even if a nested lifespan on another loop replaces the module-global
699
+ # singleton in the meantime; shutdown below stops exactly this instance.
700
+ live_usage_ingestor = start_live_usage_ingestor()
701
+ await metadata_scheduler.start()
702
+ await usage_scheduler.start()
703
+ await api_key_limit_reset_scheduler.start()
704
+ await api_key_last_used_flush_scheduler.start()
705
+ await model_scheduler.start()
706
+ await sticky_session_cleanup_scheduler.start()
707
+ await quota_planner_scheduler.start()
708
+ await auth_guardian_scheduler.start()
709
+ await automations_scheduler.start()
710
+ await rate_limit_reset_credits_scheduler.start()
711
+ await account_usage_rollup_scheduler.start()
712
+ await account_deletion_scheduler.start()
713
+ await data_retention_scheduler.start()
714
+ await telemetry_scheduler.start()
715
+ if settings.metrics_enabled and PROMETHEUS_AVAILABLE:
716
+ import uvicorn
717
+
718
+ from app.core.server import SignalNeutralServer
719
+
720
+ scrape_registry = make_scrape_registry()
721
+ prometheus_module = import_module("prometheus_client")
722
+ make_asgi_app = getattr(prometheus_module, "make_asgi_app")
723
+ metrics_app = make_asgi_app(registry=scrape_registry)
724
+ config = uvicorn.Config(metrics_app, host="0.0.0.0", port=settings.metrics_port, log_level="warning")
725
+ metrics_server = SignalNeutralServer(config)
726
+
727
+ async def _serve_metrics(srv: _MetricsServer) -> None:
728
+ try:
729
+ await srv.serve()
730
+ except SystemExit as exc:
731
+ if _is_benign_metrics_bind_failure(exc):
732
+ logger.info(
733
+ "Metrics port %d unavailable (another worker likely serves metrics)",
734
+ settings.metrics_port,
735
+ )
736
+ elif _is_metrics_bind_conflict(exc):
737
+ _log_non_multiproc_metrics_bind_conflict(settings.metrics_port)
738
+ else:
739
+ raise
740
+ except OSError as exc:
741
+ if _is_benign_metrics_bind_failure(exc):
742
+ logger.info(
743
+ "Metrics port %d already bound (another worker serves metrics)",
744
+ settings.metrics_port,
745
+ )
746
+ elif _is_metrics_bind_conflict(exc):
747
+ _log_non_multiproc_metrics_bind_conflict(settings.metrics_port)
748
+ else:
749
+ raise
750
+
751
+ metrics_server_task = asyncio.create_task(_serve_metrics(metrics_server))
752
+ elif settings.metrics_enabled:
753
+ logger.warning("Metrics endpoint enabled but prometheus-client is not installed")
754
+
755
+ async def _complete_bridge_registration(svc: RingMembershipService, iid: str) -> None:
756
+ if bridge_endpoint_base_url is None:
757
+ await _activate_bridge_membership(svc, iid)
758
+ startup_module.mark_bridge_registration_complete()
759
+ return
760
+ await _validate_bridge_advertise_endpoint_for_multi_replica(
761
+ svc=svc,
762
+ settings=settings,
763
+ instance_id=iid,
764
+ endpoint_base_url=bridge_endpoint_base_url,
765
+ )
766
+ await svc.register(iid, endpoint_base_url=None)
767
+ await _wait_for_bridge_advertise_endpoint(
768
+ bridge_endpoint_base_url,
769
+ connect_timeout_seconds=effective_settings(
770
+ await get_settings_cache().get(), settings
771
+ ).upstream_connect_timeout_seconds,
772
+ )
773
+ await svc.heartbeat(iid, endpoint_base_url=bridge_endpoint_base_url)
774
+ startup_module.mark_bridge_registration_complete()
775
+
776
+ async def _heartbeat_only(svc: RingMembershipService, iid: str) -> None:
777
+ while True:
778
+ await asyncio.sleep(RING_HEARTBEAT_INTERVAL_SECONDS)
779
+ try:
780
+ await svc.heartbeat(iid, endpoint_base_url=bridge_endpoint_base_url)
781
+ except Exception:
782
+ logger.warning("Ring heartbeat failed", exc_info=True)
783
+ await run_http_bridge_heartbeat_maintenance(getattr(app.state, "proxy_service", None))
784
+ await refresh_cap_partition(svc.list_active, iid)
785
+
786
+ async def _register_and_heartbeat(svc: RingMembershipService, iid: str) -> None:
787
+ attempt = 0
788
+ while True:
789
+ attempt += 1
790
+ try:
791
+ await _complete_bridge_registration(svc, iid)
792
+ logger.info("Registered in bridge ring", extra={"instance_id": iid, "attempt": attempt})
793
+ break
794
+ except Exception:
795
+ delay = min(5.0 * (2 ** min(attempt - 1, 5)), 60.0)
796
+ logger.warning("Ring registration attempt %d failed, retrying in %.0fs", attempt, delay, exc_info=True)
797
+ await asyncio.sleep(delay)
798
+ await refresh_cap_partition(svc.list_active, iid)
799
+ await _heartbeat_only(svc, iid)
800
+
801
+ async def _activate_bridge_membership(svc: RingMembershipService, iid: str) -> None:
802
+ if bridge_endpoint_base_url is None:
803
+ await svc.register(iid, endpoint_base_url=None)
804
+ return
805
+ await svc.register(iid, endpoint_base_url=bridge_endpoint_base_url)
806
+
807
+ ring_service: RingMembershipService | None = None
808
+ instance_id: str | None = None
809
+ heartbeat_task: asyncio.Task[None] | None = None
810
+ ring_service = RingMembershipService(SessionLocal)
811
+ instance_id = settings.http_responses_session_bridge_instance_id
812
+ heartbeat_task = asyncio.create_task(_register_and_heartbeat(ring_service, instance_id))
813
+ loop_lag_task: asyncio.Task[None] | None = None
814
+ if settings.event_loop_lag_warn_threshold_seconds > 0:
815
+ loop_lag_task = asyncio.create_task(
816
+ run_event_loop_lag_monitor(
817
+ warn_threshold_seconds=settings.event_loop_lag_warn_threshold_seconds,
818
+ )
819
+ )
820
+ startup_module._startup_complete = True
821
+
822
+ try:
823
+ yield
824
+ finally:
825
+ shutdown_state.commit_shutdown(timeout_seconds=settings.shutdown_drain_timeout_seconds)
826
+ remaining_drain_seconds = shutdown_state.remaining_drain_timeout_seconds() or 0.0
827
+ drained = await shutdown_state.wait_for_in_flight_drain(timeout_seconds=remaining_drain_seconds)
828
+ # No await separates the timeout result from this cutoff. A slow
829
+ # request therefore either registered its task before the barrier or
830
+ # is prevented from starting new audit/fleet database work afterward.
831
+ shutdown_state.close_control_plane_task_admission()
832
+ if not drained:
833
+ logger.warning("Drain timeout reached, proceeding with shutdown")
834
+
835
+ proxy_service = getattr(app.state, "proxy_service", None)
836
+ recovery_settlements_drained = True
837
+ # Settle detached recovery journals while their origin leases are
838
+ # still held; bridge teardown below may release those owner fences.
839
+ recovery_settlements_drained = await _drain_proxy_persistence_tasks(
840
+ proxy_service,
841
+ shutdown_state.remaining_post_drain_cleanup_timeout_seconds() or 0.0,
842
+ task_name_prefixes=("http-bridge-recovery-settlement-",),
843
+ failure_message="Failed to pre-drain proxy settlement tasks during shutdown",
844
+ )
845
+ # An in-flight request can still own a database session after the
846
+ # process-wide drain deadline. It is therefore part of the clean proof
847
+ # even though the later detached drains have their own gates.
848
+ database_tasks_drained = drained and recovery_settlements_drained
849
+ bridge_sessions_drained = await _close_proxy_http_bridge_sessions_for_shutdown(
850
+ proxy_service,
851
+ mark_draining=recovery_settlements_drained,
852
+ )
853
+ database_tasks_drained = database_tasks_drained and bridge_sessions_drained
854
+ # Drain AFTER the bridge teardown: failing a bridge's pending
855
+ # requests writes their request logs, which enqueues more
856
+ # persistence tasks that this drain must cover.
857
+ remaining_drain_seconds = shutdown_state.remaining_drain_timeout_seconds() or 0.0
858
+ final_proxy_persistence_drained = await _drain_proxy_persistence_tasks(
859
+ proxy_service,
860
+ remaining_drain_seconds,
861
+ failure_message="Failed to drain proxy persistence tasks during shutdown",
862
+ )
863
+ database_tasks_drained = database_tasks_drained and final_proxy_persistence_drained
864
+
865
+ # Cancel heartbeat and age the shared ring row near expiry.
866
+ if heartbeat_task is not None:
867
+ heartbeat_task.cancel()
868
+ try:
869
+ await asyncio.wait_for(heartbeat_task, timeout=2)
870
+ except (asyncio.CancelledError, TimeoutError):
871
+ pass
872
+
873
+ if loop_lag_task is not None:
874
+ loop_lag_task.cancel()
875
+ try:
876
+ await asyncio.wait_for(loop_lag_task, timeout=2)
877
+ except (asyncio.CancelledError, TimeoutError):
878
+ pass
879
+
880
+ if ring_service is not None and instance_id is not None:
881
+ try:
882
+ await asyncio.wait_for(
883
+ ring_service.mark_stale(
884
+ instance_id,
885
+ stale_threshold_seconds=RING_STALE_THRESHOLD_SECONDS,
886
+ grace_seconds=RING_STALE_GRACE_SECONDS,
887
+ ),
888
+ timeout=3,
889
+ )
890
+ logger.info(
891
+ "Marked bridge ring membership stale for shutdown",
892
+ extra={"instance_id": instance_id},
893
+ )
894
+ except Exception:
895
+ logger.warning("Failed to mark bridge ring membership stale during shutdown", exc_info=True)
896
+
897
+ if metrics_server is not None:
898
+ metrics_server.should_exit = True
899
+
900
+ # Detached control-plane work must finish while usage singleflight,
901
+ # shared HTTP clients, and both database engines are still available.
902
+ # The replica heartbeat is already stopped/staled so this grace period
903
+ # does not extend its active bridge-ring lifetime.
904
+ remaining_drain_seconds = shutdown_state.remaining_drain_timeout_seconds() or 0.0
905
+ control_plane_tasks_drained = await _drain_detached_control_plane_tasks(remaining_drain_seconds)
906
+ database_tasks_drained = database_tasks_drained and control_plane_tasks_drained
907
+
908
+ # Start the single process-level lease-renewal keeper BEFORE stopping any
909
+ # scheduler. Schedulers are stopped one at a time and only the final
910
+ # release() renews the lease while draining detached bodies; an earlier
911
+ # scheduler's stop() can detach a shielded leader-gated body (which stops
912
+ # that scheduler's own heartbeat) while the remaining schedulers are still
913
+ # stopping. If that stop sequence takes >= the (minimum 5s) TTL, the DB
914
+ # lease would otherwise expire while the detached body still runs as
915
+ # leader, letting a follower acquire it and run duplicate singleton work.
916
+ # The keeper renews the lease continuously across the whole stop sequence
917
+ # and is stopped by release(), which then owns renewal for its bounded
918
+ # drain. It is a no-op when leader election is disabled.
919
+ get_leader_election().start_release_keeper()
920
+ await quota_planner_scheduler.stop()
921
+ await auth_guardian_scheduler.stop()
922
+ await automations_scheduler.stop()
923
+ await sticky_session_cleanup_scheduler.stop()
924
+ await metadata_scheduler.stop()
925
+ await model_scheduler.stop()
926
+ # Stop the invalidation poller only after the model scheduler: a final
927
+ # leader tick may still bump through the installed poller.
928
+ await cache_poller.stop()
929
+ if get_cache_invalidation_poller() is cache_poller:
930
+ # A stopped poller must not keep receiving propagation requests.
931
+ set_cache_invalidation_poller(None)
932
+ await api_key_limit_reset_scheduler.stop()
933
+ # Final last_used_at flush (bounded retries). Settlement tasks were
934
+ # drained above; if that drain timed out, a surviving task's later
935
+ # record() write-throughs immediately (stop() switches the coalescer
936
+ # to shutdown write-through mode before the final flush), so a late
937
+ # touch is never parked in a pending map with no remaining flusher.
938
+ await api_key_last_used_flush_scheduler.stop()
939
+ await usage_scheduler.stop()
940
+ await stop_live_usage_ingestor(live_usage_ingestor)
941
+ await rate_limit_reset_credits_scheduler.stop()
942
+ await account_usage_rollup_scheduler.stop()
943
+ await account_deletion_scheduler.stop()
944
+ await data_retention_scheduler.stop()
945
+ await telemetry_scheduler.stop()
946
+ # Release the scheduler leader lease only after every leader-gated
947
+ # scheduler has stopped so no local tick re-acquires it; followers can
948
+ # then take over immediately instead of waiting out the lease TTL.
949
+ # release() itself first drains bodies that were detached still
950
+ # draining shielded work, and skips the early release (letting the
951
+ # lease expire by TTL) if any is still running, so a follower cannot
952
+ # become leader while this process may still act as one. The deadline
953
+ # covers that bounded drain plus the row delete, and — because the
954
+ # release path shields and awaits its own session teardown — is
955
+ # enforced by abandoning the release task rather than awaiting a
956
+ # potentially wedged cancellation, so shutdown always proceeds.
957
+ leader_lease_release_completed = await _release_leader_lease_within(10)
958
+ try:
959
+ try:
960
+ await close_discovered_native_egress_client()
961
+ finally:
962
+ await close_http_client()
963
+ finally:
964
+ try:
965
+ if metrics_server_task is not None:
966
+ await asyncio.wait_for(metrics_server_task, timeout=5)
967
+ except TimeoutError:
968
+ logger.warning("Timed out waiting for metrics server shutdown")
969
+ except Exception:
970
+ logger.exception("Metrics server stopped with an error")
971
+ finally:
972
+ mark_process_dead()
973
+ try:
974
+ await _close_db_and_record_clean_shutdown(
975
+ database_tasks_drained=database_tasks_drained,
976
+ leader_lease_release_completed=leader_lease_release_completed,
977
+ )
978
+ finally:
979
+ shutdown_state.mark_lifespan_completed()
980
+
981
+
982
+ def create_app() -> FastAPI:
983
+ settings = get_settings()
984
+ configure_memory_monitor(reject_threshold_mb=settings.memory_reject_threshold_mb)
985
+ app = FastAPI(
986
+ title="codex-lb",
987
+ version="0.1.0",
988
+ lifespan=lifespan,
989
+ swagger_ui_parameters={"persistAuthorization": True},
990
+ )
991
+ if settings.otel_enabled:
992
+ from app.core.tracing.otel import init_tracing
993
+
994
+ init_tracing(service_name="codex-lb", endpoint=settings.otel_exporter_endpoint, app=app)
995
+
996
+ # Innermost of the app-level middlewares: binds the dashboard-managed
997
+ # settings overrides (C2-1 timeouts) for the request/socket once the
998
+ # admission middlewares below have let it through.
999
+ app.add_middleware(cast(Any, DashboardOverridesMiddleware))
1000
+ app.add_middleware(cast(Any, InFlightMiddleware))
1001
+ add_dashboard_gzip_middleware(app)
1002
+ add_dashboard_auth_proxy_middleware(app)
1003
+ add_dashboard_csrf_middleware(app)
1004
+ add_request_decompression_middleware(app)
1005
+ add_request_body_limit_middleware(app)
1006
+ add_multipart_content_encoding_middleware(app)
1007
+ add_required_capability_http_middleware(app)
1008
+ add_request_id_middleware(app)
1009
+ add_api_firewall_middleware(app)
1010
+ app.add_middleware(cast(Any, MetricsMiddleware), enabled=settings.metrics_enabled)
1011
+ if settings.backpressure_max_concurrent_requests > 0:
1012
+ app.add_middleware(
1013
+ cast(Any, BackpressureMiddleware),
1014
+ max_concurrent=settings.backpressure_max_concurrent_requests,
1015
+ )
1016
+ app.add_middleware(
1017
+ cast(Any, BulkheadMiddleware),
1018
+ bulkhead=get_bulkhead(
1019
+ proxy_http_limit=settings.bulkhead_proxy_limit,
1020
+ proxy_websocket_limit=settings.bulkhead_proxy_limit,
1021
+ # Compact limit is derived by BulkheadSemaphore: min(http, 16),
1022
+ # or 0 when the http class is unlimited-off (0).
1023
+ proxy_compact_limit=None,
1024
+ dashboard_limit=settings.bulkhead_dashboard_limit,
1025
+ ),
1026
+ )
1027
+ add_backend_api_codex_v1_alias_middleware(app)
1028
+ add_app_version_middleware(app)
1029
+ add_exception_handlers(app)
1030
+ add_trusted_proxy_headers_middleware(app)
1031
+
1032
+ app.include_router(proxy_api.realtime_call_router)
1033
+ app.include_router(proxy_api.router)
1034
+ app.include_router(proxy_api.internal_router)
1035
+ app.include_router(proxy_api.ws_router)
1036
+ app.include_router(proxy_api.wham_router)
1037
+ app.include_router(proxy_api.v1_router)
1038
+ app.include_router(proxy_api.v1_ws_router)
1039
+ app.include_router(proxy_api.transcribe_router)
1040
+ app.include_router(proxy_api.files_router)
1041
+ app.include_router(proxy_api.usage_router)
1042
+ app.include_router(audit_api.router)
1043
+ app.include_router(accounts_api.router)
1044
+ app.include_router(rate_limit_reset_credits_api.router)
1045
+ app.include_router(dashboard_api.router)
1046
+ app.include_router(usage_api.router)
1047
+ app.include_router(request_logs_api.router)
1048
+ app.include_router(request_logs_api.conversations_router)
1049
+ app.include_router(quota_planner_api.router)
1050
+ app.include_router(reports_api.router)
1051
+ app.include_router(conversation_archive_api.router)
1052
+ app.include_router(runtime_api.router)
1053
+ app.include_router(oauth_api.router)
1054
+ app.include_router(dashboard_auth_api.router)
1055
+ app.include_router(dashboard_users_api.router)
1056
+ app.include_router(dashboard_roles_api.router)
1057
+ app.include_router(auth_providers_api.router)
1058
+ app.include_router(role_mappings_api.router)
1059
+ app.include_router(scim_api.router)
1060
+ app.include_router(scim_tokens_api.router)
1061
+ app.include_router(settings_api.router)
1062
+ app.include_router(telemetry_api.router)
1063
+ app.include_router(firewall_api.router)
1064
+ app.include_router(fleet_api.router)
1065
+ app.include_router(sticky_sessions_api.router)
1066
+ app.include_router(cache_isolation_probe_api.router)
1067
+ app.include_router(automations_api.router)
1068
+ app.include_router(api_keys_api.router)
1069
+ app.include_router(model_sources_api.router)
1070
+ app.include_router(health_api.router)
1071
+
1072
+ static_dir = Path(__file__).parent / "static"
1073
+ index_html = static_dir / "index.html"
1074
+ static_root = static_dir.resolve()
1075
+ frontend_build_hint = "Frontend assets are missing. Run `cd frontend && bun run build`."
1076
+ # ``scim/`` is here for a reason the others are not: an identity provider
1077
+ # probes for resources this release does not implement (Groups,
1078
+ # ServiceProviderConfig), and answering those with index.html and a 200
1079
+ # tells it they are supported. They must be a SCIM 404.
1080
+ excluded_prefixes = ("api/", "v1/", "backend-api/", "health", "scim/")
1081
+
1082
+ def _is_static_asset_path(path: str) -> bool:
1083
+ if path.startswith("assets/"):
1084
+ return True
1085
+ last_segment = path.rsplit("/", maxsplit=1)[-1]
1086
+ return "." in last_segment
1087
+
1088
+ @app.get("/", include_in_schema=False)
1089
+ @app.get("/{path:path}", include_in_schema=False)
1090
+ async def spa_fallback(path: str = ""):
1091
+ normalized = path.lstrip("/")
1092
+ if normalized and any(
1093
+ normalized == prefix.rstrip("/") or normalized.startswith(prefix) for prefix in excluded_prefixes
1094
+ ):
1095
+ raise HTTPException(status_code=404, detail="Not Found")
1096
+
1097
+ if normalized:
1098
+ candidate = _resolve_static_asset_path(static_root, normalized)
1099
+ if candidate is not None:
1100
+ if normalized.startswith("assets/"):
1101
+ # Vite content-hashes everything under assets/, so the
1102
+ # response for a given URL can never change: immutable
1103
+ # caching makes repeat dashboard loads skip ~1.7 MB of
1104
+ # re-downloads/revalidations. index.html stays no-cache
1105
+ # below, so deploys still pick up new hashes.
1106
+ return FileResponse(candidate, headers={"Cache-Control": "public, max-age=31536000, immutable"})
1107
+ return FileResponse(candidate)
1108
+ if _is_static_asset_path(normalized):
1109
+ raise HTTPException(status_code=404, detail="Not Found")
1110
+
1111
+ if not index_html.is_file():
1112
+ raise HTTPException(status_code=503, detail=frontend_build_hint)
1113
+
1114
+ return FileResponse(index_html, media_type="text/html", headers={"Cache-Control": "no-cache"})
1115
+
1116
+ return app
1117
+
1118
+
1119
+ async def _ensure_bridge_durable_schema_ready(settings) -> bool:
1120
+ if not settings.http_responses_session_bridge_enabled:
1121
+ return False
1122
+ session = SessionLocal()
1123
+ try:
1124
+ missing_tables = await missing_durable_bridge_tables(session)
1125
+ finally:
1126
+ await close_session(session)
1127
+ if not missing_tables:
1128
+ return True
1129
+ missing = ", ".join(missing_tables)
1130
+ if settings.database_migrations_fail_fast:
1131
+ raise RuntimeError(f"HTTP bridge durable schema is missing required tables: {missing}")
1132
+ logger.warning(
1133
+ "HTTP bridge durable schema is missing required tables but startup fail-fast is disabled",
1134
+ extra={"missing_tables": missing_tables},
1135
+ )
1136
+ return False
1137
+
1138
+
1139
+ async def _wait_for_bridge_advertise_endpoint(
1140
+ bridge_endpoint_base_url: str | None,
1141
+ *,
1142
+ connect_timeout_seconds: float,
1143
+ ) -> None:
1144
+ local_port = _local_api_port()
1145
+ if bridge_endpoint_base_url is None and local_port is None:
1146
+ raise RuntimeError(
1147
+ "Cannot determine local bridge listener port for registration probe; "
1148
+ "set PORT or configure http_responses_session_bridge_advertise_base_url"
1149
+ )
1150
+ probe_base_url = bridge_endpoint_base_url or f"http://127.0.0.1:{local_port}"
1151
+ probe_base_url = probe_base_url.rstrip("/")
1152
+ probe_url = f"{probe_base_url}/health/live"
1153
+ timeout = aiohttp.ClientTimeout(
1154
+ total=connect_timeout_seconds,
1155
+ sock_connect=connect_timeout_seconds,
1156
+ sock_read=connect_timeout_seconds,
1157
+ )
1158
+ max_probe_wait_seconds = max(connect_timeout_seconds, 5.0)
1159
+ deadline = time.monotonic() + max_probe_wait_seconds
1160
+ await asyncio.sleep(0)
1161
+ attempt = 0
1162
+ while time.monotonic() < deadline:
1163
+ attempt += 1
1164
+ try:
1165
+ async with aiohttp.ClientSession(timeout=timeout, trust_env=False) as session:
1166
+ async with session.get(probe_url) as response:
1167
+ if response.status == 200:
1168
+ return
1169
+ except Exception:
1170
+ logger.debug(
1171
+ "Bridge advertise endpoint not yet reachable",
1172
+ extra={"probe_url": probe_url, "attempt": attempt},
1173
+ exc_info=True,
1174
+ )
1175
+ delay = min(0.5 * (2 ** min(attempt - 1, 4)), 5.0)
1176
+ remaining = deadline - time.monotonic()
1177
+ if remaining <= 0:
1178
+ break
1179
+ await asyncio.sleep(min(delay, remaining))
1180
+ raise RuntimeError(
1181
+ f"Bridge advertise endpoint did not become reachable before registration probe deadline: {probe_url}"
1182
+ )
1183
+
1184
+
1185
+ def _local_api_port() -> int | None:
1186
+ raw = os.getenv("PORT")
1187
+ port = _parse_port_value(raw.strip()) if raw is not None else None
1188
+ if port is None:
1189
+ port = _port_from_argv()
1190
+ return port
1191
+
1192
+
1193
+ def _port_from_argv() -> int | None:
1194
+ args = tuple(sys.argv[1:])
1195
+ for index, value in enumerate(args):
1196
+ if value == "--port" and index + 1 < len(args):
1197
+ return _parse_port_value(args[index + 1])
1198
+ if value.startswith("--port="):
1199
+ return _parse_port_value(value.split("=", 1)[1])
1200
+ return None
1201
+
1202
+
1203
+ async def _validate_bridge_advertise_endpoint_for_multi_replica(
1204
+ svc: _RingMembershipReader,
1205
+ *,
1206
+ settings,
1207
+ instance_id: str,
1208
+ endpoint_base_url: str | None,
1209
+ ) -> None:
1210
+ if endpoint_base_url is None:
1211
+ return
1212
+ hostname = urlparse(endpoint_base_url).hostname
1213
+ if hostname is None:
1214
+ raise RuntimeError("http_responses_session_bridge_advertise_base_url must include a valid hostname")
1215
+ try:
1216
+ parsed_ip = ip_address(hostname)
1217
+ except ValueError:
1218
+ parsed_ip = None
1219
+ if (parsed_ip is not None and parsed_ip.is_loopback) or hostname == "localhost":
1220
+ configured_multi_replica = len(settings.http_responses_session_bridge_instance_ring) > 1
1221
+ if configured_multi_replica:
1222
+ raise RuntimeError(
1223
+ "http_responses_session_bridge_advertise_base_url must be replica-specific for bridge routing"
1224
+ )
1225
+ try:
1226
+ active_instances = await svc.list_active(stale_threshold_seconds=RING_HEARTBEAT_INTERVAL_SECONDS)
1227
+ except Exception:
1228
+ active_instances = []
1229
+ if any(active_instance != instance_id for active_instance in active_instances):
1230
+ raise RuntimeError(
1231
+ "http_responses_session_bridge_advertise_base_url must be replica-specific for bridge routing"
1232
+ )
1233
+ return
1234
+ if not _bridge_advertise_hostname_is_replica_specific(hostname, instance_id=instance_id):
1235
+ raise RuntimeError(
1236
+ "http_responses_session_bridge_advertise_base_url must be replica-specific for bridge routing"
1237
+ )
1238
+
1239
+
1240
+ app = create_app()