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,725 @@
1
+ """Queries and writes over ``dashboard_users`` and ``dashboard_user_invites``.
2
+
3
+ The read side is shared with the users cache and the auth module. Writes here
4
+ never touch credentials of the compat ``admin`` (those go through
5
+ ``DashboardAuthRepository`` so the legacy mirror stays consistent); they cover
6
+ account lifecycle (invite, edit, disable, delete), the invite rows, and the
7
+ owner-driven API-key cascade, all inside the caller's session.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import re
13
+ from collections.abc import Sequence
14
+ from dataclasses import dataclass
15
+ from datetime import UTC, datetime
16
+
17
+ from sqlalchemy import ColumnElement, and_, delete, func, or_, select, text, update
18
+ from sqlalchemy.exc import OperationalError
19
+ from sqlalchemy.ext.asyncio import AsyncSession
20
+ from sqlalchemy.orm import aliased, selectinload
21
+
22
+ from app.core.auth.dashboard_access import PRESET_ROLE_IDS, PresetRoleSlug
23
+ from app.db.models import (
24
+ ApiKey,
25
+ ApiKeyDeactivatedReason,
26
+ DashboardIdentity,
27
+ DashboardRoleRecord,
28
+ DashboardUser,
29
+ DashboardUserInvite,
30
+ DashboardUserStatus,
31
+ )
32
+
33
+ _USERNAME_PATTERN = re.compile(r"^[a-z0-9._-]{1,64}$")
34
+ _EMAIL_PATTERN = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
35
+
36
+
37
+ def utc_now() -> datetime:
38
+ """The clock every invite-liveness decision reads (one seam for tests)."""
39
+
40
+ return datetime.now(UTC)
41
+
42
+
43
+ def normalize_username(value: str) -> str:
44
+ """Usernames are compared case-insensitively and stored normalized."""
45
+
46
+ return value.strip().casefold()
47
+
48
+
49
+ def is_valid_username(value: str) -> bool:
50
+ return _USERNAME_PATTERN.fullmatch(value) is not None
51
+
52
+
53
+ def normalize_email(value: str | None) -> str | None:
54
+ """E-mails are stored lower-cased; blank means "no e-mail"."""
55
+
56
+ normalized = (value or "").strip().casefold()
57
+ return normalized or None
58
+
59
+
60
+ def is_valid_email(value: str) -> bool:
61
+ return len(value) <= 320 and _EMAIL_PATTERN.fullmatch(value) is not None
62
+
63
+
64
+ @dataclass(frozen=True, slots=True)
65
+ class LocalAuthState:
66
+ """What the users table says about whether this install requires sign-in.
67
+
68
+ ``requires_auth`` is true when an active user can sign in at all (holds a
69
+ password or an external identity). ``sole_local_password_user_id`` is set
70
+ only when exactly one active user holds a password, which lets the login
71
+ form omit the username field on single-user installs.
72
+ """
73
+
74
+ any_user: bool
75
+ active_users: int
76
+ active_local_password_users: int
77
+ requires_auth: bool
78
+ sole_local_password_user_id: str | None
79
+
80
+
81
+ @dataclass(frozen=True, slots=True)
82
+ class DashboardUserCounts:
83
+ total: int
84
+ active: int
85
+ invited: int
86
+ disabled: int
87
+ non_admin: int
88
+ pending_invites: int
89
+
90
+
91
+ def _user_query():
92
+ return select(DashboardUser).options(
93
+ selectinload(DashboardUser.role).selectinload(DashboardRoleRecord.grants),
94
+ )
95
+
96
+
97
+ def _invite_query():
98
+ return select(DashboardUserInvite).options(
99
+ selectinload(DashboardUserInvite.user).selectinload(DashboardUser.role).selectinload(DashboardRoleRecord.grants)
100
+ )
101
+
102
+
103
+ def live_invite_filter(now: datetime) -> ColumnElement[bool]:
104
+ """An invite that can still be accepted: not consumed, not revoked, and not expired.
105
+
106
+ An SSO-only invite has no link to expire: the account waits for its first
107
+ provider sign-in for as long as the administrator leaves it in place.
108
+ """
109
+
110
+ return and_(
111
+ DashboardUserInvite.consumed_at.is_(None),
112
+ DashboardUserInvite.revoked_at.is_(None),
113
+ or_(DashboardUserInvite.sso_only.is_(True), DashboardUserInvite.expires_at > now),
114
+ )
115
+
116
+
117
+ class DashboardUsersRepository:
118
+ def __init__(self, session: AsyncSession) -> None:
119
+ self._session = session
120
+
121
+ # --- reads ---
122
+
123
+ async def get_by_id(self, user_id: str) -> DashboardUser | None:
124
+ stmt = _user_query().where(DashboardUser.id == user_id)
125
+ return (await self._session.execute(stmt)).scalar_one_or_none()
126
+
127
+ async def get_by_username(self, normalized_username: str) -> DashboardUser | None:
128
+ stmt = _user_query().where(DashboardUser.username == normalized_username)
129
+ return (await self._session.execute(stmt)).scalar_one_or_none()
130
+
131
+ async def get_by_email(self, normalized_email: str) -> DashboardUser | None:
132
+ stmt = _user_query().where(DashboardUser.email == normalized_email)
133
+ return (await self._session.execute(stmt)).scalar_one_or_none()
134
+
135
+ async def get_identity(self, provider: str, provider_key: str, subject: str) -> DashboardIdentity | None:
136
+ """The identity row for a provider triple, with its account and role loaded."""
137
+
138
+ stmt = (
139
+ select(DashboardIdentity)
140
+ .options(
141
+ selectinload(DashboardIdentity.user)
142
+ .selectinload(DashboardUser.role)
143
+ .selectinload(DashboardRoleRecord.grants)
144
+ )
145
+ .where(DashboardIdentity.provider == provider)
146
+ .where(DashboardIdentity.provider_key == provider_key)
147
+ .where(DashboardIdentity.subject == subject)
148
+ )
149
+ return (await self._session.execute(stmt)).scalar_one_or_none()
150
+
151
+ async def has_identity(self, user_id: str, *, provider: str, provider_key: str) -> bool:
152
+ """Whether this account holds any identity on that provider.
153
+
154
+ The other direction of :meth:`get_identity`, and deliberately a bare
155
+ existence check: the caller (step-up availability) needs to know that
156
+ the provider can vouch for the account, not which subject it uses.
157
+ """
158
+
159
+ stmt = (
160
+ select(DashboardIdentity.id)
161
+ .where(DashboardIdentity.user_id == user_id)
162
+ .where(DashboardIdentity.provider == provider)
163
+ .where(DashboardIdentity.provider_key == provider_key)
164
+ .limit(1)
165
+ )
166
+ return (await self._session.execute(stmt)).first() is not None
167
+
168
+ async def list_users(self) -> Sequence[DashboardUser]:
169
+ stmt = _user_query().order_by(DashboardUser.created_at.asc(), DashboardUser.id.asc())
170
+ return (await self._session.execute(stmt)).scalars().all()
171
+
172
+ async def list_active_local_password_users(self) -> Sequence[DashboardUser]:
173
+ stmt = (
174
+ _user_query()
175
+ .where(DashboardUser.status == DashboardUserStatus.ACTIVE.value)
176
+ .where(DashboardUser.password_hash.is_not(None))
177
+ .order_by(DashboardUser.created_at.asc(), DashboardUser.id.asc())
178
+ )
179
+ return (await self._session.execute(stmt)).scalars().all()
180
+
181
+ async def count_identities(self, user_id: str) -> int:
182
+ stmt = select(func.count()).select_from(DashboardIdentity).where(DashboardIdentity.user_id == user_id)
183
+ return int((await self._session.execute(stmt)).scalar_one())
184
+
185
+ async def primary_identity_provider(self, user_id: str) -> str | None:
186
+ """The provider of the account's oldest identity (which sign-in method manages it)."""
187
+
188
+ stmt = (
189
+ select(DashboardIdentity.provider)
190
+ .where(DashboardIdentity.user_id == user_id)
191
+ .order_by(DashboardIdentity.created_at.asc(), DashboardIdentity.id.asc())
192
+ .limit(1)
193
+ )
194
+ return (await self._session.execute(stmt)).scalars().first()
195
+
196
+ async def count_active_admins(self, *, exclude_user_id: str | None = None) -> int:
197
+ """Active accounts holding the admin *preset* (custom roles never count, however wide)."""
198
+
199
+ stmt = (
200
+ select(func.count())
201
+ .select_from(DashboardUser)
202
+ .where(DashboardUser.status == DashboardUserStatus.ACTIVE.value)
203
+ .where(DashboardUser.role_id == PRESET_ROLE_IDS[PresetRoleSlug.ADMIN])
204
+ )
205
+ if exclude_user_id is not None:
206
+ stmt = stmt.where(DashboardUser.id != exclude_user_id)
207
+ return int((await self._session.execute(stmt)).scalar_one())
208
+
209
+ def _qualifying_break_glass_filter(self) -> ColumnElement[bool]:
210
+ """The five facts of a qualifying break-glass account, as a WHERE clause.
211
+
212
+ Kept in step with :func:`app.modules.dashboard_users.break_glass.qualifies`;
213
+ the password term is what stops a proxy-provisioned admin with no local
214
+ credential from counting as a way back in.
215
+ """
216
+
217
+ return and_(
218
+ DashboardUser.is_break_glass.is_(True),
219
+ DashboardUser.status == DashboardUserStatus.ACTIVE.value,
220
+ DashboardUser.role_id == PRESET_ROLE_IDS[PresetRoleSlug.ADMIN],
221
+ DashboardUser.totp_secret_encrypted.is_not(None),
222
+ DashboardUser.password_hash.is_not(None),
223
+ )
224
+
225
+ async def count_qualifying_break_glass(self, *, exclude_user_id: str | None = None) -> int:
226
+ """Accounts that could still open the door while local sign-in is restricted."""
227
+
228
+ stmt = select(func.count()).select_from(DashboardUser).where(self._qualifying_break_glass_filter())
229
+ if exclude_user_id is not None:
230
+ stmt = stmt.where(DashboardUser.id != exclude_user_id)
231
+ return int((await self._session.execute(stmt)).scalar_one())
232
+
233
+ async def list_break_glass_designations(self) -> Sequence[DashboardUser]:
234
+ """Every designated account, qualifying or not, so a refusal can name the one that would fix it."""
235
+
236
+ stmt = (
237
+ _user_query()
238
+ .where(DashboardUser.is_break_glass.is_(True))
239
+ .order_by(DashboardUser.created_at.asc(), DashboardUser.id.asc())
240
+ )
241
+ return (await self._session.execute(stmt)).scalars().all()
242
+
243
+ async def local_auth_state(self) -> LocalAuthState:
244
+ active = DashboardUser.status == DashboardUserStatus.ACTIVE.value
245
+ totals = (
246
+ await self._session.execute(
247
+ select(
248
+ func.count(DashboardUser.id),
249
+ func.count(DashboardUser.id).filter(active),
250
+ )
251
+ )
252
+ ).one()
253
+ total_users, active_users = int(totals[0]), int(totals[1])
254
+ password_user_ids = (
255
+ (
256
+ await self._session.execute(
257
+ select(DashboardUser.id).where(active).where(DashboardUser.password_hash.is_not(None))
258
+ )
259
+ )
260
+ .scalars()
261
+ .all()
262
+ )
263
+ identity_user_exists = (
264
+ await self._session.execute(
265
+ select(DashboardIdentity.id)
266
+ .join(DashboardUser, DashboardUser.id == DashboardIdentity.user_id)
267
+ .where(active)
268
+ .limit(1)
269
+ )
270
+ ).first() is not None
271
+ return LocalAuthState(
272
+ any_user=total_users > 0,
273
+ active_users=active_users,
274
+ active_local_password_users=len(password_user_ids),
275
+ requires_auth=bool(password_user_ids) or identity_user_exists,
276
+ sole_local_password_user_id=password_user_ids[0] if len(password_user_ids) == 1 else None,
277
+ )
278
+
279
+ async def counts(self, *, now: datetime | None = None) -> DashboardUserCounts:
280
+ """Team-size facts. Invited accounts whose invite is no longer live are
281
+ zombies awaiting the lazy purge and are not counted anywhere."""
282
+
283
+ status = DashboardUser.status
284
+ live = self._live_account_filter(now or utc_now())
285
+ row = (
286
+ await self._session.execute(
287
+ select(
288
+ func.count(DashboardUser.id),
289
+ func.count(DashboardUser.id).filter(status == DashboardUserStatus.ACTIVE.value),
290
+ func.count(DashboardUser.id).filter(status == DashboardUserStatus.INVITED.value),
291
+ func.count(DashboardUser.id).filter(status == DashboardUserStatus.DISABLED.value),
292
+ func.count(DashboardUser.id).filter(DashboardUser.role_id != PRESET_ROLE_IDS[PresetRoleSlug.ADMIN]),
293
+ ).where(live)
294
+ )
295
+ ).one()
296
+ return DashboardUserCounts(
297
+ total=int(row[0]),
298
+ active=int(row[1]),
299
+ invited=int(row[2]),
300
+ disabled=int(row[3]),
301
+ non_admin=int(row[4]),
302
+ pending_invites=int(row[2]),
303
+ )
304
+
305
+ @staticmethod
306
+ def _live_account_filter(now: datetime) -> ColumnElement[bool]:
307
+ live_invite = (
308
+ select(DashboardUserInvite.id)
309
+ .where(DashboardUserInvite.user_id == DashboardUser.id)
310
+ .where(live_invite_filter(now))
311
+ .exists()
312
+ )
313
+ return or_(DashboardUser.status != DashboardUserStatus.INVITED.value, live_invite)
314
+
315
+ # --- write serialisation and guarded writes ---
316
+
317
+ async def acquire_write_intent(self) -> None:
318
+ """Serialise concurrent account mutations before any invariant is read.
319
+
320
+ SQLite: ``BEGIN IMMEDIATE`` takes the database write lock up front (the
321
+ same pattern the accounts repository uses), so two admins disabling
322
+ each other queue instead of both passing the last-admin check.
323
+ PostgreSQL: lock the active admin rows ``FOR UPDATE``; every mutation
324
+ that could change who counts as an admin must go through them.
325
+
326
+ ``BEGIN IMMEDIATE`` cannot run inside an open transaction, so a caller
327
+ that has already read something falls back to a row-less ``UPDATE``.
328
+ That statement is not a no-op for locking: SQLite opens the table for
329
+ writing and takes the RESERVED lock before evaluating the predicate,
330
+ which is the whole point of the fallback. Callers should still acquire
331
+ before their first read, so the two dialects take the lock at the same
332
+ moment and neither depends on that subtlety.
333
+ """
334
+
335
+ if self._session.get_bind().dialect.name == "sqlite":
336
+ try:
337
+ await self._session.execute(text("BEGIN IMMEDIATE"))
338
+ except OperationalError as exc:
339
+ if "within a transaction" not in str(exc).lower():
340
+ raise
341
+ await self._session.execute(text("UPDATE dashboard_users SET id = id WHERE 1 = 0"))
342
+ return
343
+ await self._session.execute(
344
+ select(DashboardUser.id)
345
+ .where(DashboardUser.status == DashboardUserStatus.ACTIVE.value)
346
+ .where(DashboardUser.role_id == PRESET_ROLE_IDS[PresetRoleSlug.ADMIN])
347
+ .with_for_update()
348
+ )
349
+
350
+ @staticmethod
351
+ def _other_active_admin_exists(user_id: str) -> ColumnElement[bool]:
352
+ other = aliased(DashboardUser)
353
+ return (
354
+ select(other.id)
355
+ .where(other.id != user_id)
356
+ .where(other.status == DashboardUserStatus.ACTIVE.value)
357
+ .where(other.role_id == PRESET_ROLE_IDS[PresetRoleSlug.ADMIN])
358
+ .exists()
359
+ )
360
+
361
+ def _other_qualifying_break_glass_exists(self, user_id: str) -> ColumnElement[bool]:
362
+ other = aliased(DashboardUser)
363
+ return (
364
+ select(other.id)
365
+ .where(other.id != user_id)
366
+ .where(other.is_break_glass.is_(True))
367
+ .where(other.status == DashboardUserStatus.ACTIVE.value)
368
+ .where(other.role_id == PRESET_ROLE_IDS[PresetRoleSlug.ADMIN])
369
+ .where(other.totp_secret_encrypted.is_not(None))
370
+ .where(other.password_hash.is_not(None))
371
+ .exists()
372
+ )
373
+
374
+ async def update_role_status_guarded(
375
+ self,
376
+ user_id: str,
377
+ *,
378
+ role_id: str,
379
+ status: str,
380
+ is_break_glass: bool | None = None,
381
+ require_other_admin: bool = True,
382
+ require_other_break_glass: bool = False,
383
+ ) -> bool:
384
+ """Write role/status (and the designation) only while the invariants still hold (no commit).
385
+
386
+ ``False`` means the conditional UPDATE matched no row: another writer
387
+ removed the last other admin, or the last other qualifying break-glass
388
+ account, between the read and this statement.
389
+ """
390
+
391
+ stmt = update(DashboardUser).where(DashboardUser.id == user_id)
392
+ if require_other_admin:
393
+ stmt = stmt.where(self._other_active_admin_exists(user_id))
394
+ if require_other_break_glass:
395
+ stmt = stmt.where(self._other_qualifying_break_glass_exists(user_id))
396
+ values: dict[str, object] = {"role_id": role_id, "status": status}
397
+ if is_break_glass is not None:
398
+ values["is_break_glass"] = is_break_glass
399
+ result = await self._session.execute(
400
+ stmt.values(**values).returning(DashboardUser.id).execution_options(synchronize_session=False)
401
+ )
402
+ return result.scalar_one_or_none() is not None
403
+
404
+ # --- invites ---
405
+
406
+ async def list_live_invites(self, now: datetime) -> Sequence[DashboardUserInvite]:
407
+ stmt = (
408
+ _invite_query()
409
+ .join(DashboardUser, DashboardUser.id == DashboardUserInvite.user_id)
410
+ .where(live_invite_filter(now))
411
+ .where(DashboardUser.status == DashboardUserStatus.INVITED.value)
412
+ .order_by(DashboardUserInvite.created_at.asc(), DashboardUserInvite.id.asc())
413
+ )
414
+ return (await self._session.execute(stmt)).scalars().all()
415
+
416
+ async def get_invite_by_token_hash(self, token_hash: bytes) -> DashboardUserInvite | None:
417
+ stmt = _invite_query().where(DashboardUserInvite.token_hash == token_hash)
418
+ return (await self._session.execute(stmt)).scalar_one_or_none()
419
+
420
+ async def get_invite_for_user(self, user_id: str) -> DashboardUserInvite | None:
421
+ stmt = _invite_query().where(DashboardUserInvite.user_id == user_id)
422
+ return (await self._session.execute(stmt)).scalar_one_or_none()
423
+
424
+ async def consume_invite(self, invite_id: str, *, token_hash: bytes, now: datetime) -> bool:
425
+ """Compare-and-set the invite as consumed; ``False`` when it is no longer live (no commit).
426
+
427
+ The presented token hash is part of the predicate so a resend that
428
+ rotated the token between lookup and consume fails the accept.
429
+ """
430
+
431
+ # The invite row is already in the identity map (loaded with a naive
432
+ # SQLite timestamp), so the ORM must not try to evaluate the aware
433
+ # ``expires_at > now`` criterion in Python; the database decides.
434
+ result = await self._session.execute(
435
+ update(DashboardUserInvite)
436
+ .where(DashboardUserInvite.id == invite_id)
437
+ .where(DashboardUserInvite.token_hash == token_hash)
438
+ .where(live_invite_filter(now))
439
+ .values(consumed_at=now)
440
+ .returning(DashboardUserInvite.id)
441
+ .execution_options(synchronize_session=False)
442
+ )
443
+ return result.scalar_one_or_none() is not None
444
+
445
+ async def find_invite_expecting_identity(
446
+ self, provider: str, provider_key: str, subject: str, now: datetime
447
+ ) -> DashboardUserInvite | None:
448
+ """The live invite of an ``invited`` account pre-created for exactly this identity triple."""
449
+
450
+ stmt = (
451
+ _invite_query()
452
+ .join(DashboardUser, DashboardUser.id == DashboardUserInvite.user_id)
453
+ .where(DashboardUser.status == DashboardUserStatus.INVITED.value)
454
+ .where(live_invite_filter(now))
455
+ .where(DashboardUserInvite.expected_provider == provider)
456
+ .where(DashboardUserInvite.expected_provider_key == provider_key)
457
+ .where(DashboardUserInvite.expected_subject == subject)
458
+ .order_by(DashboardUserInvite.created_at.asc(), DashboardUserInvite.id.asc())
459
+ )
460
+ return (await self._session.execute(stmt)).scalars().first()
461
+
462
+ async def consume_invite_by_identity(self, invite_id: str, *, now: datetime) -> bool:
463
+ """Consume the invite via its expected identity (compare-and-set); ``False`` = no longer live (no commit)."""
464
+
465
+ result = await self._session.execute(
466
+ update(DashboardUserInvite)
467
+ .where(DashboardUserInvite.id == invite_id)
468
+ .where(live_invite_filter(now))
469
+ .values(consumed_at=now)
470
+ .returning(DashboardUserInvite.id)
471
+ .execution_options(synchronize_session=False)
472
+ )
473
+ return result.scalar_one_or_none() is not None
474
+
475
+ async def live_invite_for_user(self, user_id: str, now: datetime) -> DashboardUserInvite | None:
476
+ """The live invite of a still-``invited`` account, or ``None``."""
477
+
478
+ stmt = (
479
+ _invite_query()
480
+ .join(DashboardUser, DashboardUser.id == DashboardUserInvite.user_id)
481
+ .where(DashboardUserInvite.user_id == user_id)
482
+ .where(DashboardUser.status == DashboardUserStatus.INVITED.value)
483
+ .where(live_invite_filter(now))
484
+ )
485
+ return (await self._session.execute(stmt)).scalar_one_or_none()
486
+
487
+ async def conflicting_field(self, username: str, email: str | None) -> str | None:
488
+ """After a unique violation: which of the two candidate values already exists."""
489
+
490
+ taken = select(DashboardUser.id).where(DashboardUser.username == username)
491
+ if (await self._session.execute(taken)).first() is not None:
492
+ return "username"
493
+ if email is not None:
494
+ taken = select(DashboardUser.id).where(DashboardUser.email == email)
495
+ if (await self._session.execute(taken)).first() is not None:
496
+ return "email"
497
+ return None
498
+
499
+ async def delete_invite(self, user_id: str) -> bool:
500
+ """Delete the account's invite row and keep the account (no commit); ``False`` = there was none.
501
+
502
+ The one primitive nothing else offers. A human revoke on an account
503
+ that never accepted deletes the account too, and the lazy purge does
504
+ the same; a back-channel deactivation must leave the row behind,
505
+ because the caller has to keep finding that person afterwards. The
506
+ DELETE is not conditional on the invite still being live: an account
507
+ moving out of ``invited`` has no use for a spent one either, and
508
+ leaving it would strand a row the purge — which only looks at
509
+ ``invited`` accounts — would never reach again.
510
+ """
511
+
512
+ deleted = await self._session.execute(
513
+ delete(DashboardUserInvite)
514
+ .where(DashboardUserInvite.user_id == user_id)
515
+ .returning(DashboardUserInvite.id)
516
+ .execution_options(synchronize_session=False)
517
+ )
518
+ return deleted.scalar_one_or_none() is not None
519
+
520
+ async def purge_expired_invited_users(self, now: datetime) -> int:
521
+ """Delete ``invited`` accounts whose invite expired (row + invite); returns how many.
522
+
523
+ One conditional DELETE evaluates ``status = 'invited'`` and "no live
524
+ invite" in the deleting transaction itself, so an acceptance that
525
+ committed in the meantime (status now ``active``) is never swept up.
526
+ """
527
+
528
+ expired_invite = (
529
+ select(DashboardUserInvite.id)
530
+ .where(DashboardUserInvite.user_id == DashboardUser.id)
531
+ .where(DashboardUserInvite.consumed_at.is_(None))
532
+ .where(DashboardUserInvite.sso_only.is_(False))
533
+ .where(DashboardUserInvite.expires_at <= now)
534
+ .exists()
535
+ )
536
+ live_invite = (
537
+ select(DashboardUserInvite.id)
538
+ .where(DashboardUserInvite.user_id == DashboardUser.id)
539
+ .where(live_invite_filter(now))
540
+ .exists()
541
+ )
542
+ deleted = (
543
+ await self._session.execute(
544
+ delete(DashboardUser)
545
+ .where(DashboardUser.status == DashboardUserStatus.INVITED.value)
546
+ .where(expired_invite)
547
+ .where(~live_invite)
548
+ .returning(DashboardUser.id)
549
+ .execution_options(synchronize_session=False)
550
+ )
551
+ ).scalars()
552
+ purged = list(deleted.all())
553
+ if not purged:
554
+ await self._session.rollback()
555
+ return 0
556
+ # The FK cascades on both dialects; delete explicitly so no ORM state lingers.
557
+ await self._session.execute(delete(DashboardUserInvite).where(DashboardUserInvite.user_id.in_(purged)))
558
+ await self._session.commit()
559
+ return len(purged)
560
+
561
+ async def rotate_invite(self, user_id: str, *, token_hash: bytes, expires_at: datetime) -> bool:
562
+ """Rotate the invite only while the account is still ``invited`` (no commit); ``False`` = refused."""
563
+
564
+ still_invited = (
565
+ select(DashboardUser.id)
566
+ .where(DashboardUser.id == DashboardUserInvite.user_id)
567
+ .where(DashboardUser.status == DashboardUserStatus.INVITED.value)
568
+ .exists()
569
+ )
570
+ result = await self._session.execute(
571
+ update(DashboardUserInvite)
572
+ .where(DashboardUserInvite.user_id == user_id)
573
+ .where(still_invited)
574
+ .values(token_hash=token_hash, expires_at=expires_at, consumed_at=None, revoked_at=None)
575
+ .returning(DashboardUserInvite.id)
576
+ .execution_options(synchronize_session=False)
577
+ )
578
+ return result.scalar_one_or_none() is not None
579
+
580
+ # --- writes ---
581
+
582
+ def add(self, *rows: DashboardUser | DashboardUserInvite | DashboardIdentity) -> None:
583
+ self._session.add_all(rows)
584
+
585
+ async def commit_user(self, user_id: str, *, bump_generation: bool = False) -> DashboardUser:
586
+ """Commit pending changes, optionally bumping ``session_generation`` atomically
587
+ (``SET x = x + 1`` in the same transaction, never from a stale ORM value),
588
+ and return the freshly loaded account. Any failure rolls the whole write back."""
589
+
590
+ try:
591
+ await self._session.flush()
592
+ if bump_generation:
593
+ await self._session.execute(
594
+ update(DashboardUser)
595
+ .where(DashboardUser.id == user_id)
596
+ .values(session_generation=DashboardUser.session_generation + 1)
597
+ )
598
+ await self._session.commit()
599
+ except Exception:
600
+ await self._session.rollback()
601
+ raise
602
+ self._session.expire_all()
603
+ user = await self.get_by_id(user_id)
604
+ if user is None: # pragma: no cover - the row was committed a statement ago
605
+ raise RuntimeError(f"dashboard user {user_id} vanished after commit")
606
+ return user
607
+
608
+ async def rollback(self) -> None:
609
+ await self._session.rollback()
610
+
611
+ async def _lock_owner(self, user_id: str) -> None:
612
+ """Take the owner's row before reading which of its keys the cascade must move.
613
+
614
+ Both directions of the cascade read ``api_keys`` and then write it,
615
+ while the API-key page's ``isActive: true`` reads the owner's status
616
+ (``FOR UPDATE`` on this same row) and then writes the key. Under
617
+ PostgreSQL's READ COMMITTED neither read is repeated, so with the two
618
+ rows taken in opposite orders a key switched on after the cascade's
619
+ scan — but committed before the disable — survives active on a
620
+ disabled owner. Taking the owner row first puts both sides in one
621
+ order: whoever loses the row reads the winner's committed state.
622
+
623
+ SQLite ignores ``FOR UPDATE`` and needs nothing here: its writers are
624
+ serialised by the database write lock the callers already hold.
625
+ """
626
+
627
+ await self._session.execute(select(DashboardUser.id).where(DashboardUser.id == user_id).with_for_update())
628
+
629
+ async def deactivate_owned_keys(self, user_id: str) -> list[str]:
630
+ """Turn off every active key the account owns, recording why (no commit); returns their hashes.
631
+
632
+ The owner's row is taken first so "which keys are active" is read under
633
+ the lock the API-key page holds while it re-enables one.
634
+ """
635
+
636
+ await self._lock_owner(user_id)
637
+ result = await self._session.execute(
638
+ update(ApiKey)
639
+ .where(ApiKey.owner_user_id == user_id)
640
+ .where(ApiKey.is_active.is_(True))
641
+ .values(is_active=False, deactivated_reason=ApiKeyDeactivatedReason.OWNER_DISABLED.value)
642
+ .returning(ApiKey.key_hash)
643
+ )
644
+ return list(result.scalars().all())
645
+
646
+ async def reactivate_owner_disabled_keys(self, user_id: str) -> list[str] | None:
647
+ """Restore only the keys the owner cascade turned off; manual blocks stay off.
648
+
649
+ The owner's row is taken first (see :meth:`_lock_owner`) and the UPDATE
650
+ is conditional on the owner being active at that moment; a disable that
651
+ is already in flight therefore either commits first — and this reads
652
+ its ``disabled`` — or waits for this and then finds the restored keys
653
+ active. ``None`` means the owner is not active (nothing was written).
654
+ """
655
+
656
+ await self._lock_owner(user_id)
657
+ owner_active = (
658
+ select(DashboardUser.id)
659
+ .where(DashboardUser.id == ApiKey.owner_user_id)
660
+ .where(DashboardUser.status == DashboardUserStatus.ACTIVE.value)
661
+ .exists()
662
+ )
663
+ result = await self._session.execute(
664
+ update(ApiKey)
665
+ .where(ApiKey.owner_user_id == user_id)
666
+ .where(ApiKey.is_active.is_(False))
667
+ .where(ApiKey.deactivated_reason == ApiKeyDeactivatedReason.OWNER_DISABLED.value)
668
+ .where(owner_active)
669
+ .values(is_active=True, deactivated_reason=None)
670
+ .returning(ApiKey.key_hash)
671
+ .execution_options(synchronize_session=False)
672
+ )
673
+ hashes = list(result.scalars().all())
674
+ if not hashes:
675
+ status = (
676
+ await self._session.execute(select(DashboardUser.status).where(DashboardUser.id == user_id))
677
+ ).scalar_one_or_none()
678
+ if status != DashboardUserStatus.ACTIVE.value:
679
+ await self._session.rollback()
680
+ return None
681
+ await self._session.commit()
682
+ return hashes
683
+
684
+ async def delete_user(
685
+ self,
686
+ user: DashboardUser,
687
+ *,
688
+ require_other_admin: bool = False,
689
+ require_other_break_glass: bool = False,
690
+ only_while_invited: bool = False,
691
+ ) -> list[str] | None:
692
+ """Delete the account; its keys stay (owner cleared) but are inactive with the owner reason.
693
+
694
+ With ``require_other_admin`` the DELETE only applies while another
695
+ active admin exists; with ``only_while_invited`` only while the account
696
+ is still ``invited`` (an acceptance that committed meanwhile survives).
697
+ ``None`` means it was refused and everything (including the key
698
+ cascade) was rolled back.
699
+ """
700
+
701
+ try:
702
+ hashes = await self.deactivate_owned_keys(user.id)
703
+ await self._session.execute(
704
+ update(ApiKey).where(ApiKey.owner_user_id == user.id).values(owner_user_id=None)
705
+ )
706
+ await self._session.execute(delete(DashboardUserInvite).where(DashboardUserInvite.user_id == user.id))
707
+ await self._session.execute(delete(DashboardIdentity).where(DashboardIdentity.user_id == user.id))
708
+ stmt = delete(DashboardUser).where(DashboardUser.id == user.id)
709
+ if require_other_admin:
710
+ stmt = stmt.where(self._other_active_admin_exists(user.id))
711
+ if require_other_break_glass:
712
+ stmt = stmt.where(self._other_qualifying_break_glass_exists(user.id))
713
+ if only_while_invited:
714
+ stmt = stmt.where(DashboardUser.status == DashboardUserStatus.INVITED.value)
715
+ deleted = await self._session.execute(
716
+ stmt.returning(DashboardUser.id).execution_options(synchronize_session=False)
717
+ )
718
+ if deleted.scalar_one_or_none() is None:
719
+ await self._session.rollback()
720
+ return None
721
+ await self._session.commit()
722
+ except Exception:
723
+ await self._session.rollback()
724
+ raise
725
+ return hashes