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,831 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Sequence
4
+ from dataclasses import dataclass
5
+ from datetime import datetime, timedelta
6
+ from typing import Literal
7
+
8
+ from sqlalchemy import and_, delete, func, or_, select, update
9
+ from sqlalchemy.dialects.postgresql import insert as pg_insert
10
+ from sqlalchemy.dialects.sqlite import insert as sqlite_insert
11
+ from sqlalchemy.ext.asyncio import AsyncSession
12
+ from sqlalchemy.sql import Insert
13
+
14
+ from app.core.utils.time import naive_utc_to_epoch, to_utc_naive, utcnow
15
+ from app.db.models import Account, AccountStatus, StickySession, StickySessionKind
16
+ from app.db.session import sqlite_writer_section
17
+ from app.modules.sticky_sessions.schemas import StickySessionSortBy, StickySessionSortDir
18
+
19
+ # Each (key, kind) pair in delete_entries contributes 2 bind parameters to
20
+ # the underlying DELETE...OR (key=:k AND kind=:t)... statement. SQLite's
21
+ # default SQLITE_LIMIT_VARIABLE_NUMBER is 999 on builds older than 3.32
22
+ # and 32766 on newer builds, so chunking conservatively at 250 pairs
23
+ # (500 bind parameters) keeps delete-filtered safe on any libsqlite that
24
+ # ships with current Python interpreters. Postgres allows up to 65535
25
+ # bind parameters, which this chunk size also respects.
26
+ _DELETE_ENTRIES_CHUNK_SIZE = 250
27
+
28
+ _ContinuitySource = Literal["session_header", "thread_header", "turn_state"]
29
+ _SESSION_HEADER_ABANDONMENT_SCOPE = "session_header"
30
+
31
+ # A same-owner TTL refresh upsert only rewrites ``updated_at``. On hot
32
+ # (key, kind) rows, concurrent requests serialize on that row lock, so the
33
+ # selection path may skip the rewrite while the row is younger than this
34
+ # window, revalidating the observed deadline at write time. The window is
35
+ # bounded to at most 1% of the mapping TTL (so expiry moves by at most 1% of
36
+ # the window it protects) and to a small absolute ceiling; a rebind to a
37
+ # different owner, a row carrying any abandonment marker, or a row stamped in
38
+ # the future is never skippable because those writes change state beyond
39
+ # freshness (or the observation itself is untrustworthy).
40
+ _REFRESH_SKIP_TTL_FRACTION = 0.01
41
+ _REFRESH_SKIP_MAX_SECONDS = 15.0
42
+
43
+ # Only the Live-call ownership namespace is reserved. Other LF-prefixed keys
44
+ # (e.g. the pre-existing "\ncodex-lb-affinity-v1" selection affinities) remain
45
+ # ordinary operator-manageable sessions.
46
+ RESERVED_STICKY_SESSION_KEY_PREFIX = "\ncodex_live_call:"
47
+
48
+
49
+ def is_reserved_sticky_session_key(key: str) -> bool:
50
+ return key.startswith(RESERVED_STICKY_SESSION_KEY_PREFIX)
51
+
52
+
53
+ @dataclass(frozen=True, slots=True)
54
+ class StickySessionListEntryRecord:
55
+ sticky_session: StickySession
56
+ display_name: str
57
+
58
+
59
+ @dataclass(frozen=True, slots=True)
60
+ class StickyOwnerLookup:
61
+ """Result of resolving a mapping's owner, accessed by attribute (never
62
+ unpacked) so an un-configured test double for ``sticky_sessions`` degrades
63
+ to its existing safe defaults (no owner, not abandoned) instead of a hard
64
+ crash on tuple destructuring."""
65
+
66
+ account_id: str | None
67
+ continuity_abandoned: bool
68
+ # Source-qualified abandonment makes account_id ownerless only for the
69
+ # matching source, but selection must still remember which durable owner
70
+ # was retired. Global stale-hard tombstones leave this unset because their
71
+ # established recovery path may legitimately reselect a recovered owner.
72
+ abandoned_account_id: str | None = None
73
+ # Set only when the row was observed in this lookup with a fresh
74
+ # ``updated_at`` (within the refresh-skip window derived from
75
+ # ``max_age_seconds``) and no abandonment marker, so a same-owner TTL
76
+ # refresh upsert would be a pure ``updated_at`` rewrite. The value is the
77
+ # naive-UTC instant (``observed_updated_at`` + skip window) after which
78
+ # the skip is no longer valid; consumers must isinstance-check
79
+ # ``datetime`` (test doubles may auto-vivify attributes), must revalidate
80
+ # the deadline against the clock immediately before omitting the write,
81
+ # and must never skip a write that changes the owner account.
82
+ refresh_skip_deadline: datetime | None = None
83
+
84
+
85
+ def _continuity_is_abandoned_for_source(
86
+ abandoned_at: datetime | None,
87
+ abandonment_scope: str | None,
88
+ continuity_source: _ContinuitySource | None,
89
+ ) -> bool:
90
+ if abandonment_scope is not None:
91
+ # A scope is itself the source-qualified marker. Goal-restart writers
92
+ # deliberately leave the legacy timestamp NULL so pre-scope binaries
93
+ # keep treating account_id as hard ownership during rollout/rollback.
94
+ # Unknown and nonmatching typed callers likewise fail closed.
95
+ return abandonment_scope == continuity_source
96
+ # Historical stale-hard tombstones have a timestamp and NULL scope, and
97
+ # therefore continue to abandon ownership globally for every source.
98
+ return abandoned_at is not None
99
+
100
+
101
+ def _source_scoped_abandoned_account_id(
102
+ account_id: str,
103
+ abandonment_scope: str | None,
104
+ continuity_source: _ContinuitySource | None,
105
+ ) -> str | None:
106
+ if abandonment_scope is not None and abandonment_scope == continuity_source:
107
+ return account_id
108
+ return None
109
+
110
+
111
+ def _owner_lookup_from_row(
112
+ row: StickySession,
113
+ *,
114
+ continuity_source: _ContinuitySource | None,
115
+ refresh_skip_deadline: datetime | None = None,
116
+ ) -> StickyOwnerLookup:
117
+ if _continuity_is_abandoned_for_source(
118
+ row.continuity_abandoned_at,
119
+ row.continuity_abandonment_scope,
120
+ continuity_source,
121
+ ):
122
+ return StickyOwnerLookup(
123
+ account_id=None,
124
+ continuity_abandoned=True,
125
+ abandoned_account_id=_source_scoped_abandoned_account_id(
126
+ row.account_id,
127
+ row.continuity_abandonment_scope,
128
+ continuity_source,
129
+ ),
130
+ )
131
+ return StickyOwnerLookup(
132
+ account_id=row.account_id,
133
+ continuity_abandoned=False,
134
+ refresh_skip_deadline=refresh_skip_deadline,
135
+ )
136
+
137
+
138
+ def _same_owner_refresh_skip_deadline(
139
+ row: StickySession,
140
+ *,
141
+ observed_updated_at: datetime,
142
+ now: datetime,
143
+ max_age_seconds: int,
144
+ ) -> datetime | None:
145
+ """Deadline until which a same-owner upsert of this row stays skippable.
146
+
147
+ Any abandonment marker disqualifies the skip: an upsert re-establishes
148
+ ownership by clearing both marker columns, so that write is semantic even
149
+ when the owner account is unchanged. A row whose ``updated_at`` sits in
150
+ the future (database clock ahead of this process, or a restored row) is
151
+ also never skippable: an upper-bound-only age comparison would let such a
152
+ row satisfy the window for longer than the documented bound.
153
+ """
154
+
155
+ if row.continuity_abandoned_at is not None or row.continuity_abandonment_scope is not None:
156
+ return None
157
+ age_seconds = (now - observed_updated_at).total_seconds()
158
+ if age_seconds < 0:
159
+ return None
160
+ skip_window_seconds = min(_REFRESH_SKIP_MAX_SECONDS, max_age_seconds * _REFRESH_SKIP_TTL_FRACTION)
161
+ if age_seconds > skip_window_seconds:
162
+ return None
163
+ return observed_updated_at + timedelta(seconds=skip_window_seconds)
164
+
165
+
166
+ class StickySessionsRepository:
167
+ def __init__(self, session: AsyncSession) -> None:
168
+ self._session = session
169
+
170
+ async def get_account_id(
171
+ self,
172
+ key: str,
173
+ *,
174
+ kind: StickySessionKind,
175
+ max_age_seconds: int | None = None,
176
+ continuity_source: _ContinuitySource | None = None,
177
+ ) -> str | None:
178
+ lookup = await self.get_account_id_and_abandonment(
179
+ key,
180
+ kind=kind,
181
+ max_age_seconds=max_age_seconds,
182
+ continuity_source=continuity_source,
183
+ )
184
+ return lookup.account_id
185
+
186
+ async def get_account_id_and_abandonment(
187
+ self,
188
+ key: str,
189
+ *,
190
+ kind: StickySessionKind,
191
+ max_age_seconds: int | None = None,
192
+ continuity_source: _ContinuitySource | None = None,
193
+ ) -> StickyOwnerLookup:
194
+ """Resolve a mapping's owner and applicable abandonment marker.
195
+
196
+ Global stale-hard tombstones remain ownerless for every source. A
197
+ source-scoped marker is ownerless only for its matching typed source;
198
+ explicit turn-state and unknown callers retain the stored owner when a
199
+ goal restart abandoned only session-header interpretation.
200
+ """
201
+ if not key:
202
+ return StickyOwnerLookup(account_id=None, continuity_abandoned=False)
203
+ row = await self.get_entry(key, kind=kind)
204
+ if row is None:
205
+ return StickyOwnerLookup(account_id=None, continuity_abandoned=False)
206
+ if max_age_seconds is None:
207
+ return _owner_lookup_from_row(row, continuity_source=continuity_source)
208
+ now = utcnow()
209
+ cutoff = now - timedelta(seconds=max_age_seconds)
210
+ observed_updated_at = to_utc_naive(row.updated_at)
211
+ if observed_updated_at >= cutoff:
212
+ return _owner_lookup_from_row(
213
+ row,
214
+ continuity_source=continuity_source,
215
+ refresh_skip_deadline=_same_owner_refresh_skip_deadline(
216
+ row,
217
+ observed_updated_at=observed_updated_at,
218
+ now=now,
219
+ max_age_seconds=max_age_seconds,
220
+ ),
221
+ )
222
+
223
+ # Release the read snapshot before attempting a SQLite write upgrade.
224
+ # The DELETE remains safe because every value observed above participates
225
+ # in the predicate; a concurrent rebind therefore wins the comparison.
226
+ await self._session.commit()
227
+ statement = (
228
+ delete(StickySession)
229
+ .where(
230
+ StickySession.key == key,
231
+ StickySession.kind == kind,
232
+ StickySession.account_id == row.account_id,
233
+ StickySession.updated_at == observed_updated_at,
234
+ StickySession.updated_at < cutoff,
235
+ )
236
+ .returning(StickySession.key)
237
+ )
238
+ current: tuple[str, datetime, datetime | None, str | None] | None = None
239
+ async with sqlite_writer_section():
240
+ deleted_key = (await self._session.execute(statement)).scalar_one_or_none()
241
+ if deleted_key is None:
242
+ current = (
243
+ (
244
+ await self._session.execute(
245
+ select(
246
+ StickySession.account_id,
247
+ StickySession.updated_at,
248
+ StickySession.continuity_abandoned_at,
249
+ StickySession.continuity_abandonment_scope,
250
+ ).where(
251
+ StickySession.key == key,
252
+ StickySession.kind == kind,
253
+ )
254
+ )
255
+ )
256
+ .tuples()
257
+ .one_or_none()
258
+ )
259
+ await self._session.commit()
260
+
261
+ if deleted_key is not None or current is None:
262
+ return StickyOwnerLookup(account_id=None, continuity_abandoned=False)
263
+ (
264
+ current_account_id,
265
+ current_updated_at,
266
+ current_continuity_abandoned_at,
267
+ current_continuity_abandonment_scope,
268
+ ) = current
269
+ if to_utc_naive(current_updated_at) < cutoff:
270
+ return StickyOwnerLookup(account_id=None, continuity_abandoned=False)
271
+ if _continuity_is_abandoned_for_source(
272
+ current_continuity_abandoned_at,
273
+ current_continuity_abandonment_scope,
274
+ continuity_source,
275
+ ):
276
+ return StickyOwnerLookup(
277
+ account_id=None,
278
+ continuity_abandoned=True,
279
+ abandoned_account_id=_source_scoped_abandoned_account_id(
280
+ current_account_id,
281
+ current_continuity_abandonment_scope,
282
+ continuity_source,
283
+ ),
284
+ )
285
+ return StickyOwnerLookup(account_id=current_account_id, continuity_abandoned=False)
286
+
287
+ async def release_read_snapshot(self) -> None:
288
+ """End the session's current read transaction.
289
+
290
+ On the default SQLite/WAL configuration one transaction pins one read
291
+ snapshot at its first SELECT, so a session shared across successive
292
+ ownership lookups would leave every later lookup blind to owners
293
+ committed concurrently after the first read. Committing ends that
294
+ snapshot so the next SELECT begins a fresh transaction; on PostgreSQL
295
+ READ COMMITTED each statement already reads fresh committed state, so
296
+ this is a near-free no-op. COMMIT (not rollback) on purpose: rollback
297
+ expires all tracked ORM state regardless of ``expire_on_commit``,
298
+ while commit under the session factory's ``expire_on_commit=False``
299
+ keeps rows loaded by earlier lookups readable.
300
+ """
301
+ await self._session.commit()
302
+
303
+ async def get_entry(self, key: str, *, kind: StickySessionKind) -> StickySession | None:
304
+ if not key:
305
+ return None
306
+ statement = select(StickySession).where(
307
+ StickySession.key == key,
308
+ StickySession.kind == kind,
309
+ )
310
+ result = await self._session.execute(statement)
311
+ return result.scalar_one_or_none()
312
+
313
+ async def upsert(self, key: str, account_id: str, *, kind: StickySessionKind) -> StickySession:
314
+ # RETURNING collapses the previous upsert + re-select + refresh
315
+ # (4 round trips) into one statement; this runs inline before the
316
+ # first upstream byte on sticky requests, so round trips are TTFT.
317
+ # populate_existing forces the returned row to overwrite any stale
318
+ # identity-map instance the session may already hold for this key.
319
+ statement = self._build_upsert_statement(key, account_id, kind).returning(StickySession)
320
+ async with sqlite_writer_section():
321
+ result = await self._session.execute(statement, execution_options={"populate_existing": True})
322
+ row = result.scalar_one_or_none()
323
+ await self._session.commit()
324
+ if row is None:
325
+ raise RuntimeError(f"StickySession upsert failed for key={key!r} kind={kind.value!r}")
326
+ return row
327
+
328
+ async def insert_if_absent(self, key: str, account_id: str, kind: StickySessionKind) -> str:
329
+ """Insert immutable ownership and return the persisted owner."""
330
+
331
+ statement = self._build_insert_do_nothing_statement(key, account_id, kind).returning(StickySession.account_id)
332
+ async with sqlite_writer_section():
333
+ result = await self._session.execute(statement)
334
+ owner_id = result.scalar_one_or_none()
335
+ if owner_id is None:
336
+ owner_id = await self._session.scalar(
337
+ select(StickySession.account_id).where(
338
+ StickySession.key == key,
339
+ StickySession.kind == kind,
340
+ )
341
+ )
342
+ await self._session.commit()
343
+ if owner_id is None:
344
+ raise RuntimeError("StickySession immutable insert did not resolve an owner")
345
+ return owner_id
346
+
347
+ async def upsert_with_seed_if_absent(
348
+ self,
349
+ key: str,
350
+ account_id: str,
351
+ *,
352
+ kind: StickySessionKind,
353
+ seed_key: str,
354
+ seed_kind: StickySessionKind,
355
+ ) -> StickySession:
356
+ """Upsert one mapping and initialize its immutable seed atomically."""
357
+
358
+ # Keep these writes in one transaction. A process seed without the
359
+ # initiating thread row is false placement evidence, while a thread
360
+ # row without its seed makes the first admitted thread invisible to
361
+ # later siblings. Do not replace the seed's DO NOTHING with an upsert:
362
+ # another thread may have won first-writer initialization already.
363
+ seed_statement = self._build_insert_do_nothing_statement(seed_key, account_id, seed_kind)
364
+ mapping_statement = self._build_upsert_statement(key, account_id, kind).returning(StickySession)
365
+ async with sqlite_writer_section():
366
+ try:
367
+ await self._session.execute(seed_statement)
368
+ result = await self._session.execute(
369
+ mapping_statement,
370
+ execution_options={"populate_existing": True},
371
+ )
372
+ row = result.scalar_one_or_none()
373
+ if row is None:
374
+ raise RuntimeError(f"StickySession seeded upsert failed for key={key!r} kind={kind.value!r}")
375
+ await self._session.commit()
376
+ except BaseException:
377
+ # This method owns both writes as one unit even when a caller
378
+ # catches the error and keeps using the same session.
379
+ await self._session.rollback()
380
+ raise
381
+ return row
382
+
383
+ async def delete(self, key: str, *, kind: StickySessionKind) -> bool:
384
+ if not key:
385
+ return False
386
+ statement = delete(StickySession).where(
387
+ StickySession.key == key,
388
+ StickySession.kind == kind,
389
+ )
390
+ async with sqlite_writer_section():
391
+ result = await self._session.execute(statement.returning(StickySession.key))
392
+ await self._session.commit()
393
+ return result.scalar_one_or_none() is not None
394
+
395
+ async def abandon_legacy_session_header_owner_if_unavailable(
396
+ self,
397
+ key: str,
398
+ *,
399
+ kind: StickySessionKind,
400
+ expected_account_id: str,
401
+ ) -> bool:
402
+ """Abandon only session-header interpretation of an unavailable raw owner."""
403
+
404
+ if not key or not expected_account_id:
405
+ return False
406
+ unavailable_statuses = (
407
+ AccountStatus.PAUSED,
408
+ AccountStatus.RATE_LIMITED,
409
+ AccountStatus.QUOTA_EXCEEDED,
410
+ )
411
+ # PostgreSQL evaluates the status subquery from the UPDATE statement's
412
+ # snapshot. Without first locking the Account row, a concurrent status
413
+ # recovery can commit while that statement waits for the StickySession
414
+ # row and the stale snapshot can still authorize a tombstone. Locking
415
+ # the status owner makes recovery and retirement serialize; the sticky
416
+ # owner predicate below independently keeps concurrent rebinds safe.
417
+ owner_status_lock = select(Account.status).where(Account.id == expected_account_id).with_for_update()
418
+ # Retain account status inside the UPDATE as a second, database-level
419
+ # invariant. The lock is the concurrency guarantee; this predicate
420
+ # prevents future refactors from turning a prior status observation
421
+ # into unconditional retirement.
422
+ unavailable_owner = select(Account.id).where(
423
+ Account.id == expected_account_id,
424
+ Account.status.in_(unavailable_statuses),
425
+ )
426
+ statement = (
427
+ update(StickySession)
428
+ .where(
429
+ StickySession.key == key,
430
+ StickySession.kind == kind,
431
+ StickySession.account_id == expected_account_id,
432
+ StickySession.continuity_abandoned_at.is_(None),
433
+ StickySession.continuity_abandonment_scope.is_(None),
434
+ StickySession.account_id.in_(unavailable_owner),
435
+ )
436
+ # The scope column is the new reader's marker. Keep the legacy
437
+ # timestamp NULL: older replicas know only that timestamp, so they
438
+ # continue to treat account_id as hard ownership instead of
439
+ # globally abandoning and rebinding a colliding explicit turn
440
+ # state. New readers use typed scope, never key shape, to decide
441
+ # which source may ignore the retained owner.
442
+ .values(
443
+ updated_at=func.now(),
444
+ continuity_abandoned_at=None,
445
+ continuity_abandonment_scope=_SESSION_HEADER_ABANDONMENT_SCOPE,
446
+ )
447
+ .returning(StickySession.key)
448
+ )
449
+ async with sqlite_writer_section():
450
+ owner_status = await self._session.scalar(owner_status_lock)
451
+ if owner_status not in unavailable_statuses:
452
+ await self._session.commit()
453
+ return False
454
+ result = await self._session.execute(statement)
455
+ await self._session.commit()
456
+ return result.scalar_one_or_none() is not None
457
+
458
+ async def restore_if_current(
459
+ self,
460
+ key: str,
461
+ *,
462
+ kind: StickySessionKind,
463
+ expected_account_id: str | None,
464
+ restore_account_id: str | None,
465
+ ) -> bool:
466
+ """Restore a sticky owner only if the provisional owner is still current."""
467
+
468
+ if not key:
469
+ return False
470
+ if expected_account_id is None:
471
+ if restore_account_id is None:
472
+ return True
473
+ statement = self._build_insert_do_nothing_statement(key, restore_account_id, kind).returning(
474
+ StickySession.key
475
+ )
476
+ elif restore_account_id is None:
477
+ statement = (
478
+ delete(StickySession)
479
+ .where(
480
+ StickySession.key == key,
481
+ StickySession.kind == kind,
482
+ StickySession.account_id == expected_account_id,
483
+ )
484
+ .returning(StickySession.key)
485
+ )
486
+ else:
487
+ statement = (
488
+ update(StickySession)
489
+ .where(
490
+ StickySession.key == key,
491
+ StickySession.kind == kind,
492
+ StickySession.account_id == expected_account_id,
493
+ )
494
+ .values(
495
+ account_id=restore_account_id,
496
+ updated_at=func.now(),
497
+ continuity_abandoned_at=None,
498
+ continuity_abandonment_scope=None,
499
+ )
500
+ .returning(StickySession.key)
501
+ )
502
+
503
+ async with sqlite_writer_section():
504
+ result = await self._session.execute(statement)
505
+ await self._session.commit()
506
+ return result.scalar_one_or_none() is not None
507
+
508
+ async def delete_entries(
509
+ self,
510
+ entries: Sequence[tuple[str, StickySessionKind]],
511
+ ) -> list[tuple[str, StickySessionKind]]:
512
+ targets = {(key, kind) for key, kind in entries if key}
513
+ if not targets:
514
+ return []
515
+
516
+ deleted: list[tuple[str, StickySessionKind]] = []
517
+ targets_list = list(targets)
518
+ for offset in range(0, len(targets_list), _DELETE_ENTRIES_CHUNK_SIZE):
519
+ chunk = targets_list[offset : offset + _DELETE_ENTRIES_CHUNK_SIZE]
520
+ statement = delete(StickySession).where(
521
+ or_(*(and_(StickySession.key == key, StickySession.kind == kind) for key, kind in chunk))
522
+ )
523
+ async with sqlite_writer_section():
524
+ result = await self._session.execute(statement.returning(StickySession.key, StickySession.kind))
525
+ await self._session.commit()
526
+ deleted.extend((key, kind) for key, kind in result.all())
527
+ return deleted
528
+
529
+ async def list_entry_identifiers(
530
+ self,
531
+ *,
532
+ kind: StickySessionKind | None = None,
533
+ updated_before: datetime | None = None,
534
+ account_query: str | None = None,
535
+ key_query: str | None = None,
536
+ ) -> list[tuple[str, StickySessionKind]]:
537
+ statement = (
538
+ self._apply_filters(
539
+ select(StickySession.key, StickySession.kind),
540
+ kind=kind,
541
+ updated_before=updated_before,
542
+ account_query=account_query,
543
+ key_query=key_query,
544
+ )
545
+ .join(Account, Account.id == StickySession.account_id)
546
+ .order_by(
547
+ StickySession.updated_at.desc(),
548
+ StickySession.created_at.desc(),
549
+ StickySession.key.asc(),
550
+ )
551
+ )
552
+ result = await self._session.execute(statement)
553
+ return [(key, kind) for key, kind in result.all()]
554
+
555
+ async def list_entries(
556
+ self,
557
+ *,
558
+ kind: StickySessionKind | None = None,
559
+ updated_before: datetime | None = None,
560
+ account_query: str | None = None,
561
+ key_query: str | None = None,
562
+ sort_by: StickySessionSortBy = "updated_at",
563
+ sort_dir: StickySessionSortDir = "desc",
564
+ offset: int = 0,
565
+ limit: int | None = None,
566
+ ) -> Sequence[StickySessionListEntryRecord]:
567
+ order_by = self._build_order_by(sort_by=sort_by, sort_dir=sort_dir)
568
+ statement = (
569
+ self._apply_filters(
570
+ select(StickySession, Account.email),
571
+ kind=kind,
572
+ updated_before=updated_before,
573
+ account_query=account_query,
574
+ key_query=key_query,
575
+ )
576
+ .join(Account, Account.id == StickySession.account_id)
577
+ .order_by(*order_by)
578
+ )
579
+ if offset > 0:
580
+ statement = statement.offset(offset)
581
+ if limit is not None:
582
+ statement = statement.limit(limit)
583
+ result = await self._session.execute(statement)
584
+ return [
585
+ StickySessionListEntryRecord(sticky_session=sticky_session, display_name=display_name)
586
+ for sticky_session, display_name in result.all()
587
+ ]
588
+
589
+ async def count_entries(
590
+ self,
591
+ *,
592
+ kind: StickySessionKind | None = None,
593
+ updated_before: datetime | None = None,
594
+ account_query: str | None = None,
595
+ key_query: str | None = None,
596
+ ) -> int:
597
+ statement = self._apply_filters(
598
+ select(func.count()).select_from(StickySession).join(Account, Account.id == StickySession.account_id),
599
+ kind=kind,
600
+ updated_before=updated_before,
601
+ account_query=account_query,
602
+ key_query=key_query,
603
+ )
604
+ result = await self._session.execute(statement)
605
+ return int(result.scalar_one())
606
+
607
+ async def purge_prompt_cache_before(self, cutoff: datetime) -> int:
608
+ return await self.purge_before(cutoff, kind=StickySessionKind.PROMPT_CACHE)
609
+
610
+ async def purge_before(self, cutoff: datetime, *, kind: StickySessionKind | None = None) -> int:
611
+ stmt = delete(StickySession).where(StickySession.updated_at < to_utc_naive(cutoff))
612
+ if kind is not None:
613
+ stmt = stmt.where(StickySession.kind == kind)
614
+ async with sqlite_writer_section():
615
+ result = await self._session.execute(stmt.returning(StickySession.key))
616
+ deleted = len(result.scalars().all())
617
+ await self._session.commit()
618
+ return deleted
619
+
620
+ async def purge_before_for_key_prefix(
621
+ self,
622
+ cutoff: datetime,
623
+ *,
624
+ kind: StickySessionKind,
625
+ key_prefix: str,
626
+ limit: int = _DELETE_ENTRIES_CHUNK_SIZE,
627
+ ) -> int:
628
+ """Delete one bounded batch from a reserved key namespace."""
629
+
630
+ if limit <= 0:
631
+ return 0
632
+ target_keys = (
633
+ select(StickySession.key)
634
+ .where(
635
+ StickySession.kind == kind,
636
+ StickySession.key.startswith(key_prefix, autoescape=True),
637
+ StickySession.updated_at < to_utc_naive(cutoff),
638
+ )
639
+ .order_by(StickySession.updated_at.asc(), StickySession.key.asc())
640
+ .limit(limit)
641
+ )
642
+ stmt = delete(StickySession).where(
643
+ StickySession.kind == kind,
644
+ StickySession.key.in_(target_keys),
645
+ )
646
+ async with sqlite_writer_section():
647
+ result = await self._session.execute(stmt.returning(StickySession.key))
648
+ deleted = len(result.scalars().all())
649
+ await self._session.commit()
650
+ return deleted
651
+
652
+ async def purge_stale_hard_codex_session_mappings(self, cutoff: datetime, *, now: datetime) -> int:
653
+ """Retire CODEX_SESSION mappings pinned to a durably unusable owner.
654
+
655
+ A hard `codex_session` mapping is never rebound by ordinary selection
656
+ (see load_balancer.py's hard_sticky branch) even once its owner is
657
+ rate-limited/quota-exceeded/paused, because that pin can represent
658
+ live, unverifiable session state that isn't safe to move mid-flight.
659
+ That correctly protects a transient blip, but leaves the mapping
660
+ stuck forever if the owner never recovers.
661
+
662
+ ``Account.reset_at`` is frequently absent, while ``blocked_at`` is
663
+ cleared when an account is paused, so neither field provides one
664
+ durable outage clock for every unavailable status. Instead,
665
+ ``AccountsRepository`` refreshes the mapping timestamp exactly when
666
+ its owner transitions from an available status into one of the
667
+ unavailable statuses below. ``StickySession.updated_at`` therefore
668
+ records the later of the mapping's last use and the outage start.
669
+ Only once BOTH the owner is still non-active AND that conservative
670
+ timestamp is before ``cutoff`` do we give up on the mapping.
671
+
672
+ When ``Account.reset_at`` is known and still in the future (e.g. a
673
+ multi-day quota window), the owner's own stated recovery point takes
674
+ priority over the flat cutoff: the mapping survives until after
675
+ ``reset_at`` even if it has long since gone stale by the cutoff
676
+ alone, since purging before an account's own known recovery time
677
+ would contradict "well past its own recovery point". ``reset_at``
678
+ only ever narrows eligibility (delays a purge); it never widens it
679
+ when unset, which is why the fixed cutoff remains the fallback.
680
+
681
+ Giving up is done in two phases, never by an outright delete on the
682
+ first pass:
683
+
684
+ 1. Tombstone: set ``continuity_abandoned_at`` instead of deleting.
685
+ A `conversation`-continuity request has no owner index besides
686
+ this row (see affinity.py's ``require_unambiguous_account``), so
687
+ an outright delete would be indistinguishable from "this key was
688
+ never seen" — and with more than one account in the pool, that
689
+ makes ``run_sticky_selection_path`` fail closed forever, even
690
+ after the original owner recovers, because nothing on that path
691
+ can ever re-create the very row it needs to stop failing closed.
692
+ A tombstone instead lets selection recognize "this key's
693
+ continuity was deliberately abandoned, picking a fresh owner is
694
+ authorized", so a subsequent request can escape the stuck state.
695
+ 2. Delete: once a tombstone has sat for a further ``cutoff`` window
696
+ with nobody claiming it (i.e. it's still a tombstone, so no
697
+ request re-pinned it), it's dropped outright. A fresh request for
698
+ that key then falls back to the same conservative fail-closed
699
+ default as a key that was never seen, which is fine this long
700
+ after the fact.
701
+ """
702
+ now_epoch = naive_utc_to_epoch(to_utc_naive(now))
703
+ unavailable_account_ids = select(Account.id).where(
704
+ Account.status.in_((AccountStatus.PAUSED, AccountStatus.RATE_LIMITED, AccountStatus.QUOTA_EXCEEDED)),
705
+ or_(Account.reset_at.is_(None), Account.reset_at < now_epoch),
706
+ )
707
+ cutoff_naive = to_utc_naive(cutoff)
708
+ tombstone_stmt = (
709
+ update(StickySession)
710
+ .where(
711
+ StickySession.kind == StickySessionKind.CODEX_SESSION,
712
+ or_(
713
+ StickySession.continuity_abandoned_at.is_(None),
714
+ StickySession.continuity_abandonment_scope.is_not(None),
715
+ ),
716
+ StickySession.updated_at < cutoff_naive,
717
+ StickySession.account_id.in_(unavailable_account_ids),
718
+ )
719
+ # Stale-hard cleanup is global. It may promote a younger
720
+ # session-header-only marker once the original row itself crosses
721
+ # the normal stale-hard threshold.
722
+ .values(
723
+ continuity_abandoned_at=to_utc_naive(now),
724
+ continuity_abandonment_scope=None,
725
+ )
726
+ )
727
+ delete_stmt = delete(StickySession).where(
728
+ StickySession.kind == StickySessionKind.CODEX_SESSION,
729
+ StickySession.continuity_abandoned_at.is_not(None),
730
+ StickySession.continuity_abandonment_scope.is_(None),
731
+ StickySession.continuity_abandoned_at < cutoff_naive,
732
+ )
733
+ async with sqlite_writer_section():
734
+ tombstoned_result = await self._session.execute(tombstone_stmt.returning(StickySession.key))
735
+ tombstoned = len(tombstoned_result.scalars().all())
736
+ deleted_result = await self._session.execute(delete_stmt.returning(StickySession.key))
737
+ deleted = len(deleted_result.scalars().all())
738
+ await self._session.commit()
739
+ return tombstoned + deleted
740
+
741
+ def _build_upsert_statement(self, key: str, account_id: str, kind: StickySessionKind) -> Insert:
742
+ dialect = self._session.get_bind().dialect.name
743
+ if dialect == "postgresql":
744
+ insert_fn = pg_insert
745
+ elif dialect == "sqlite":
746
+ insert_fn = sqlite_insert
747
+ else:
748
+ raise RuntimeError(f"StickySession upsert unsupported for dialect={dialect!r}")
749
+ statement = insert_fn(StickySession).values(key=key, account_id=account_id, kind=kind)
750
+ return statement.on_conflict_do_update(
751
+ index_elements=[StickySession.key, StickySession.kind],
752
+ set_={
753
+ "account_id": account_id,
754
+ "updated_at": func.now(),
755
+ # A fresh pin fully re-establishes ownership, so any earlier
756
+ # purge tombstone (see purge_stale_hard_codex_session_mappings)
757
+ # no longer applies — otherwise this row would keep reporting
758
+ # itself as abandoned even though it now has a live owner.
759
+ "continuity_abandoned_at": None,
760
+ "continuity_abandonment_scope": None,
761
+ },
762
+ )
763
+
764
+ def _build_insert_do_nothing_statement(self, key: str, account_id: str, kind: StickySessionKind) -> Insert:
765
+ dialect = self._session.get_bind().dialect.name
766
+ if dialect == "postgresql":
767
+ insert_fn = pg_insert
768
+ elif dialect == "sqlite":
769
+ insert_fn = sqlite_insert
770
+ else:
771
+ raise RuntimeError(f"StickySession insert unsupported for dialect={dialect!r}")
772
+ statement = insert_fn(StickySession).values(key=key, account_id=account_id, kind=kind)
773
+ return statement.on_conflict_do_nothing(index_elements=[StickySession.key, StickySession.kind])
774
+
775
+ @staticmethod
776
+ def _apply_filters(
777
+ statement,
778
+ *,
779
+ kind: StickySessionKind | None,
780
+ updated_before: datetime | None,
781
+ account_query: str | None,
782
+ key_query: str | None,
783
+ ):
784
+ statement = statement.where(~StickySession.key.startswith(RESERVED_STICKY_SESSION_KEY_PREFIX, autoescape=True))
785
+ if kind is not None:
786
+ statement = statement.where(StickySession.kind == kind)
787
+ if updated_before is not None:
788
+ statement = statement.where(StickySession.updated_at < to_utc_naive(updated_before))
789
+ if account_query:
790
+ statement = statement.where(func.lower(Account.email).contains(account_query.lower()))
791
+ if key_query:
792
+ statement = statement.where(func.lower(StickySession.key).contains(key_query.lower()))
793
+ return statement
794
+
795
+ @staticmethod
796
+ def _build_order_by(
797
+ *,
798
+ sort_by: StickySessionSortBy,
799
+ sort_dir: StickySessionSortDir,
800
+ ):
801
+ sort_column_map = {
802
+ "updated_at": StickySession.updated_at,
803
+ "created_at": StickySession.created_at,
804
+ "account": Account.email,
805
+ "key": StickySession.key,
806
+ }
807
+ primary = sort_column_map[sort_by]
808
+ primary_order = primary.asc() if sort_dir == "asc" else primary.desc()
809
+ if sort_by == "updated_at":
810
+ return (
811
+ primary_order,
812
+ StickySession.created_at.desc(),
813
+ StickySession.key.asc(),
814
+ )
815
+ if sort_by == "created_at":
816
+ return (
817
+ primary_order,
818
+ StickySession.updated_at.desc(),
819
+ StickySession.key.asc(),
820
+ )
821
+ if sort_by == "account":
822
+ return (
823
+ primary_order,
824
+ StickySession.updated_at.desc(),
825
+ StickySession.key.asc(),
826
+ )
827
+ return (
828
+ primary_order,
829
+ StickySession.updated_at.desc(),
830
+ StickySession.created_at.desc(),
831
+ )