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