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,1890 @@
1
+ from __future__ import annotations
2
+
3
+ import sqlite3
4
+ from collections.abc import Collection, Mapping
5
+ from contextlib import closing
6
+ from dataclasses import dataclass
7
+ from datetime import datetime
8
+ from functools import partial
9
+ from hashlib import sha256
10
+ from threading import RLock
11
+ from typing import Any, Callable, Literal, cast
12
+
13
+ from anyio import to_thread
14
+ from sqlalchemy import (
15
+ Integer,
16
+ String,
17
+ and_,
18
+ case,
19
+ column,
20
+ delete,
21
+ func,
22
+ literal_column,
23
+ or_,
24
+ select,
25
+ text,
26
+ true,
27
+ tuple_,
28
+ union_all,
29
+ values,
30
+ )
31
+ from sqlalchemy import cast as sqlalchemy_cast
32
+ from sqlalchemy.ext.asyncio import AsyncSession
33
+
34
+ from app.core.config.settings import get_settings
35
+ from app.core.usage.types import UsageAggregateRow, UsageTrendBucket
36
+ from app.core.utils.time import utcnow
37
+ from app.db.account_identity_lock import lock_postgresql_account_identities
38
+ from app.db.models import Account, AdditionalUsageHistory, UsageHistory
39
+ from app.db.session import relax_commit_durability, sqlite_writer_section
40
+ from app.db.sqlite_utils import sqlite_db_path_from_url
41
+ from app.modules.usage.additional_quota_keys import (
42
+ AdditionalQuotaQueryScope,
43
+ canonicalize_additional_quota_key,
44
+ get_additional_quota_query_scope,
45
+ )
46
+
47
+ _PRIMARY_WINDOW_LITERAL = literal_column("'primary'")
48
+ NormalizedUsageWindow = Literal["primary", "secondary"]
49
+
50
+
51
+ @dataclass(frozen=True, slots=True)
52
+ class UsageHistorySnapshot:
53
+ id: int
54
+ account_id: str
55
+ used_percent: float
56
+ recorded_at: datetime
57
+ reset_at: float | None
58
+ window_minutes: int | None
59
+
60
+
61
+ @dataclass(frozen=True, slots=True)
62
+ class UsageWindowWrite:
63
+ window: str
64
+ used_percent: float
65
+ reset_at: int | None = None
66
+ window_minutes: int | None = None
67
+ credits_has: bool | None = None
68
+ credits_unlimited: bool | None = None
69
+ credits_balance: float | None = None
70
+
71
+
72
+ class LiveSnapshotOwnerIdentityRelockError(RuntimeError):
73
+ """The selected live-snapshot owner's identity changed twice."""
74
+
75
+
76
+ def _account_snapshot_entries(
77
+ account_id: str,
78
+ windows: Collection[UsageWindowWrite],
79
+ *,
80
+ recorded_at: datetime | None = None,
81
+ ) -> list[UsageHistory]:
82
+ captured_at = recorded_at or utcnow()
83
+ return [
84
+ UsageHistory(
85
+ account_id=account_id,
86
+ used_percent=window.used_percent,
87
+ input_tokens=None,
88
+ output_tokens=None,
89
+ window=window.window,
90
+ reset_at=window.reset_at,
91
+ window_minutes=window.window_minutes,
92
+ credits_has=window.credits_has,
93
+ credits_unlimited=window.credits_unlimited,
94
+ credits_balance=window.credits_balance,
95
+ recorded_at=captured_at,
96
+ )
97
+ for window in windows
98
+ ]
99
+
100
+
101
+ @dataclass(frozen=True, slots=True)
102
+ class _BulkHistoryCacheMetadata:
103
+ row_count: int
104
+ max_id: int
105
+ content_digest: str
106
+
107
+
108
+ @dataclass(slots=True)
109
+ class _BulkHistoryCacheEntry:
110
+ since: datetime
111
+ max_id: int
112
+ metadata: _BulkHistoryCacheMetadata
113
+ rows_by_account: dict[str, list[UsageHistorySnapshot]]
114
+
115
+
116
+ _BULK_HISTORY_SQLITE_CACHE: dict[tuple[str, tuple[str, ...], str], _BulkHistoryCacheEntry] = {}
117
+ _BULK_HISTORY_SQLITE_CACHE_LOCK = RLock()
118
+ _EMPTY_BULK_HISTORY_DIGEST = sha256().hexdigest()
119
+
120
+
121
+ def _normalized_sqlite_datetime_text(value) -> str:
122
+ return str(_parse_sqlite_datetime(value))
123
+
124
+
125
+ class _BulkHistoryDigestAggregate:
126
+ def __init__(self) -> None:
127
+ self._digest = sha256()
128
+
129
+ def step(
130
+ self,
131
+ row_id: int,
132
+ account_id: str,
133
+ used_percent: float,
134
+ recorded_at: str,
135
+ reset_at: float | None,
136
+ window_minutes: int | None,
137
+ ) -> None:
138
+ self._digest.update(str(int(row_id)).encode("utf-8"))
139
+ self._digest.update(b"\x1f")
140
+ account_bytes = str(account_id).encode("utf-8")
141
+ self._digest.update(str(len(account_bytes)).encode("ascii"))
142
+ self._digest.update(b":")
143
+ self._digest.update(account_bytes)
144
+ self._digest.update(b"\x1f")
145
+ self._digest.update(float(used_percent).hex().encode("ascii"))
146
+ self._digest.update(b"\x1f")
147
+ recorded_at_bytes = _normalized_sqlite_datetime_text(recorded_at).encode("utf-8")
148
+ self._digest.update(str(len(recorded_at_bytes)).encode("ascii"))
149
+ self._digest.update(b":")
150
+ self._digest.update(recorded_at_bytes)
151
+ self._digest.update(b"\x1f")
152
+ self._digest.update(b"NULL" if reset_at is None else float(reset_at).hex().encode("ascii"))
153
+ self._digest.update(b"\x1f")
154
+ self._digest.update(b"NULL" if window_minutes is None else str(int(window_minutes)).encode("ascii"))
155
+ self._digest.update(b"\x1e")
156
+
157
+ def finalize(self) -> str:
158
+ return self._digest.hexdigest()
159
+
160
+
161
+ def _clear_bulk_history_since_sqlite_cache() -> None:
162
+ with _BULK_HISTORY_SQLITE_CACHE_LOCK:
163
+ _BULK_HISTORY_SQLITE_CACHE.clear()
164
+
165
+
166
+ def _bulk_history_cache_key(
167
+ db_path: str,
168
+ account_ids: list[str],
169
+ window: str,
170
+ ) -> tuple[str, tuple[str, ...], str]:
171
+ return (db_path, tuple(sorted(account_ids)), window)
172
+
173
+
174
+ def _clone_filtered_history(
175
+ grouped: dict[str, list[UsageHistorySnapshot]],
176
+ since: datetime,
177
+ ) -> dict[str, list[UsageHistorySnapshot]]:
178
+ filtered_grouped: dict[str, list[UsageHistorySnapshot]] = {}
179
+ for account_id, rows in grouped.items():
180
+ filtered = [row for row in rows if row.recorded_at >= since]
181
+ if filtered:
182
+ filtered_grouped[account_id] = filtered
183
+ return filtered_grouped
184
+
185
+
186
+ def _bulk_history_metadata_from_grouped(
187
+ grouped: dict[str, list[UsageHistorySnapshot]],
188
+ ) -> _BulkHistoryCacheMetadata:
189
+ digest = sha256()
190
+ rows = sorted((row for rows in grouped.values() for row in rows), key=lambda row: (row.id, row.account_id))
191
+ for row in rows:
192
+ digest.update(str(int(row.id)).encode("utf-8"))
193
+ digest.update(b"\x1f")
194
+ account_bytes = str(row.account_id).encode("utf-8")
195
+ digest.update(str(len(account_bytes)).encode("ascii"))
196
+ digest.update(b":")
197
+ digest.update(account_bytes)
198
+ digest.update(b"\x1f")
199
+ digest.update(float(row.used_percent).hex().encode("ascii"))
200
+ digest.update(b"\x1f")
201
+ recorded_at_bytes = str(row.recorded_at).encode("utf-8")
202
+ digest.update(str(len(recorded_at_bytes)).encode("ascii"))
203
+ digest.update(b":")
204
+ digest.update(recorded_at_bytes)
205
+ digest.update(b"\x1f")
206
+ digest.update(b"NULL" if row.reset_at is None else float(row.reset_at).hex().encode("ascii"))
207
+ digest.update(b"\x1f")
208
+ digest.update(b"NULL" if row.window_minutes is None else str(int(row.window_minutes)).encode("ascii"))
209
+ digest.update(b"\x1e")
210
+ return _BulkHistoryCacheMetadata(
211
+ row_count=len(rows),
212
+ max_id=max((row.id for row in rows), default=0),
213
+ content_digest=digest.hexdigest(),
214
+ )
215
+
216
+
217
+ def _append_grouped_history(
218
+ target: dict[str, list[UsageHistorySnapshot]],
219
+ source: dict[str, list[UsageHistorySnapshot]],
220
+ ) -> None:
221
+ for account_id, rows in source.items():
222
+ bucket = target.setdefault(account_id, [])
223
+ bucket.extend(rows)
224
+ bucket.sort(key=lambda row: (row.recorded_at, row.id))
225
+
226
+
227
+ def _query_bulk_history_since_sqlite(
228
+ conn: sqlite3.Connection,
229
+ account_ids: list[str],
230
+ window: str,
231
+ since: datetime,
232
+ *,
233
+ after_id: int | None = None,
234
+ ) -> dict[str, list[UsageHistorySnapshot]]:
235
+ placeholders = ",".join("?" for _ in account_ids)
236
+ since_param = since.isoformat(sep=" ")
237
+ id_clause = ""
238
+ params: list[object]
239
+ if window == "primary":
240
+ window_clause = "coalesce(window, 'primary') = 'primary'"
241
+ params = [*account_ids, since_param]
242
+ else:
243
+ window_clause = "window = ?"
244
+ params = [*account_ids, window, since_param]
245
+ if after_id is not None:
246
+ id_clause = "and id > ?"
247
+ params.append(after_id)
248
+ sql = f"""
249
+ select id, account_id, used_percent, recorded_at, reset_at, window_minutes
250
+ from usage_history
251
+ where account_id in ({placeholders})
252
+ and {window_clause}
253
+ and recorded_at >= ?
254
+ {id_clause}
255
+ order by account_id, recorded_at asc
256
+ """
257
+ grouped: dict[str, list[UsageHistorySnapshot]] = {}
258
+ rows = conn.execute(sql, params)
259
+ for row in rows:
260
+ snapshot = UsageHistorySnapshot(
261
+ id=int(row[0]),
262
+ account_id=str(row[1]),
263
+ used_percent=float(row[2]),
264
+ recorded_at=_parse_sqlite_datetime(row[3]),
265
+ reset_at=float(row[4]) if row[4] is not None else None,
266
+ window_minutes=int(row[5]) if row[5] is not None else None,
267
+ )
268
+ grouped.setdefault(snapshot.account_id, []).append(snapshot)
269
+ return grouped
270
+
271
+
272
+ def _query_bulk_history_metadata_sqlite(
273
+ conn: sqlite3.Connection,
274
+ account_ids: list[str],
275
+ window: str,
276
+ since: datetime,
277
+ *,
278
+ max_id: int | None = None,
279
+ ) -> _BulkHistoryCacheMetadata:
280
+ placeholders = ",".join("?" for _ in account_ids)
281
+ since_param = since.isoformat(sep=" ")
282
+ id_clause = ""
283
+ params: list[object]
284
+ if window == "primary":
285
+ window_clause = "coalesce(window, 'primary') = 'primary'"
286
+ params = [*account_ids, since_param]
287
+ else:
288
+ window_clause = "window = ?"
289
+ params = [*account_ids, window, since_param]
290
+ if max_id is not None:
291
+ id_clause = "and id <= ?"
292
+ params.append(max_id)
293
+ conn.create_aggregate("clb_bulk_history_digest", 6, cast(Any, _BulkHistoryDigestAggregate))
294
+ sql = f"""
295
+ select count(*),
296
+ coalesce(max(id), 0),
297
+ coalesce(
298
+ clb_bulk_history_digest(id, account_id, used_percent, recorded_at, reset_at, window_minutes),
299
+ '{_EMPTY_BULK_HISTORY_DIGEST}'
300
+ )
301
+ from (
302
+ select id, account_id, used_percent, recorded_at, reset_at, window_minutes
303
+ from usage_history
304
+ where account_id in ({placeholders})
305
+ and {window_clause}
306
+ and recorded_at >= ?
307
+ {id_clause}
308
+ order by id asc, account_id asc
309
+ )
310
+ """
311
+ row = conn.execute(sql, params).fetchone()
312
+ return _BulkHistoryCacheMetadata(
313
+ row_count=int(row[0]),
314
+ max_id=int(row[1]),
315
+ content_digest=str(row[2]),
316
+ )
317
+
318
+
319
+ def _normalized_window_expr():
320
+ return func.coalesce(UsageHistory.window, _PRIMARY_WINDOW_LITERAL)
321
+
322
+
323
+ def _window_clause(window: str | None):
324
+ if not window or window == "primary":
325
+ return _normalized_window_expr() == "primary"
326
+ return UsageHistory.window == window
327
+
328
+
329
+ def _sqlite_path_from_bind(bind) -> object | None:
330
+ bind_url = getattr(bind, "url", None)
331
+ if bind_url is not None:
332
+ return sqlite_db_path_from_url(str(bind_url))
333
+ return sqlite_db_path_from_url(get_settings().database_url)
334
+
335
+
336
+ def _parse_sqlite_datetime(value) -> datetime:
337
+ if isinstance(value, datetime):
338
+ return value
339
+ return datetime.fromisoformat(str(value))
340
+
341
+
342
+ def _usage_history_from_sqlite_row(row) -> UsageHistory:
343
+ return UsageHistory(
344
+ id=int(row[0]),
345
+ account_id=str(row[1]),
346
+ recorded_at=_parse_sqlite_datetime(row[2]),
347
+ window=row[3],
348
+ used_percent=float(row[4]),
349
+ input_tokens=int(row[5]) if row[5] is not None else None,
350
+ output_tokens=int(row[6]) if row[6] is not None else None,
351
+ reset_at=int(row[7]) if row[7] is not None else None,
352
+ window_minutes=int(row[8]) if row[8] is not None else None,
353
+ credits_has=bool(row[9]) if row[9] is not None else None,
354
+ credits_unlimited=bool(row[10]) if row[10] is not None else None,
355
+ credits_balance=float(row[11]) if row[11] is not None else None,
356
+ )
357
+
358
+
359
+ def _additional_usage_history_from_sqlite_row(row) -> AdditionalUsageHistory:
360
+ return AdditionalUsageHistory(
361
+ id=int(row[0]),
362
+ account_id=str(row[1]),
363
+ quota_key=str(row[2]),
364
+ limit_name=str(row[3]),
365
+ metered_feature=str(row[4]),
366
+ window=str(row[5]),
367
+ used_percent=float(row[6]),
368
+ reset_at=int(row[7]) if row[7] is not None else None,
369
+ window_minutes=int(row[8]) if row[8] is not None else None,
370
+ recorded_at=_parse_sqlite_datetime(row[9]),
371
+ )
372
+
373
+
374
+ def _latest_by_account_sqlite(
375
+ db_path: str,
376
+ window: str | None,
377
+ account_ids: list[str] | None,
378
+ ) -> dict[str, UsageHistory]:
379
+ if account_ids is None:
380
+ account_sql = "select id from accounts"
381
+ account_params: list[object] = []
382
+ elif not account_ids:
383
+ return {}
384
+ else:
385
+ placeholders = ",".join("?" for _ in account_ids)
386
+ account_sql = f"select id from accounts where id in ({placeholders})"
387
+ account_params = list(account_ids)
388
+
389
+ if not window or window == "primary":
390
+ window_clause = "coalesce(window, 'primary') = 'primary'"
391
+ window_params: list[object] = []
392
+ else:
393
+ window_clause = "window = ?"
394
+ window_params = [window]
395
+ latest_sql = f"""
396
+ select id, account_id, recorded_at, window, used_percent,
397
+ input_tokens, output_tokens, reset_at, window_minutes,
398
+ credits_has, credits_unlimited, credits_balance
399
+ from usage_history
400
+ where account_id = ?
401
+ and {window_clause}
402
+ order by recorded_at desc, id desc
403
+ limit 1
404
+ """
405
+
406
+ latest: dict[str, UsageHistory] = {}
407
+ with closing(sqlite3.connect(db_path, detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES)) as conn:
408
+ conn.execute("PRAGMA query_only=ON")
409
+ conn.execute("PRAGMA busy_timeout=30000")
410
+ accounts = [str(row[0]) for row in conn.execute(account_sql, account_params)]
411
+ for account_id in accounts:
412
+ row = conn.execute(latest_sql, [account_id, *window_params]).fetchone()
413
+ if row is not None:
414
+ entry = _usage_history_from_sqlite_row(row)
415
+ latest[entry.account_id] = entry
416
+ return latest
417
+
418
+
419
+ def _additional_scope_sqlite_clause(scope: AdditionalQuotaQueryScope) -> tuple[str, list[object]]:
420
+ quota_values = tuple(scope.quota_key_match_values or {scope.quota_key})
421
+ clauses = [f"quota_key in ({','.join('?' for _ in quota_values)})"]
422
+ params: list[object] = list(quota_values)
423
+ if scope.limit_name_match_values:
424
+ clauses.append(f"lower(limit_name) in ({','.join('?' for _ in scope.limit_name_match_values)})")
425
+ params.extend(scope.limit_name_match_values)
426
+ if scope.metered_feature_match_values:
427
+ clauses.append(f"lower(metered_feature) in ({','.join('?' for _ in scope.metered_feature_match_values)})")
428
+ params.extend(scope.metered_feature_match_values)
429
+ return f"({' or '.join(clauses)})", params
430
+
431
+
432
+ def _bulk_history_since_sqlite(
433
+ db_path: str,
434
+ account_ids: list[str],
435
+ window: str,
436
+ since: datetime,
437
+ ) -> dict[str, list[UsageHistorySnapshot]]:
438
+ cache_key = _bulk_history_cache_key(db_path, account_ids, window)
439
+ with closing(sqlite3.connect(db_path, detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES)) as conn:
440
+ conn.execute("PRAGMA query_only=ON")
441
+ conn.execute("PRAGMA busy_timeout=30000")
442
+ with _BULK_HISTORY_SQLITE_CACHE_LOCK:
443
+ cached = _BULK_HISTORY_SQLITE_CACHE.get(cache_key)
444
+ if cached is not None and cached.since <= since:
445
+ metadata = _query_bulk_history_metadata_sqlite(
446
+ conn,
447
+ account_ids,
448
+ window,
449
+ cached.since,
450
+ max_id=cached.max_id,
451
+ )
452
+ if metadata != cached.metadata:
453
+ grouped = _query_bulk_history_since_sqlite(conn, account_ids, window, cached.since)
454
+ cached.metadata = _bulk_history_metadata_from_grouped(grouped)
455
+ cached.max_id = cached.metadata.max_id
456
+ cached.rows_by_account = grouped
457
+ return _clone_filtered_history(grouped, since)
458
+
459
+ new_rows = _query_bulk_history_since_sqlite(
460
+ conn,
461
+ account_ids,
462
+ window,
463
+ cached.since,
464
+ after_id=cached.max_id,
465
+ )
466
+ if new_rows:
467
+ _append_grouped_history(cached.rows_by_account, new_rows)
468
+ cached.metadata = _bulk_history_metadata_from_grouped(cached.rows_by_account)
469
+ cached.max_id = cached.metadata.max_id
470
+ return _clone_filtered_history(cached.rows_by_account, since)
471
+
472
+ grouped = _query_bulk_history_since_sqlite(conn, account_ids, window, since)
473
+ metadata = _bulk_history_metadata_from_grouped(grouped)
474
+ _BULK_HISTORY_SQLITE_CACHE[cache_key] = _BulkHistoryCacheEntry(
475
+ since=since,
476
+ max_id=metadata.max_id,
477
+ metadata=metadata,
478
+ rows_by_account=grouped,
479
+ )
480
+ return _clone_filtered_history(grouped, since)
481
+
482
+
483
+ def _bulk_history_since_capped_sqlite(
484
+ db_path: str,
485
+ account_ids: list[str],
486
+ window: str,
487
+ since: datetime,
488
+ *,
489
+ cutoffs: dict[str, datetime] | None = None,
490
+ per_account_row_cap: int,
491
+ uncapped_recent_floor: datetime | None = None,
492
+ ) -> dict[str, list[UsageHistorySnapshot]]:
493
+ """Per-account newest-first capped history fetch for SQLite.
494
+
495
+ Mirrors _bulk_history_since_capped_postgresql using SQLite's composite
496
+ indexes idx_usage_window_account_latest and idx_usage_window_account_time_covering.
497
+
498
+ For each account, executes targeted index seeks:
499
+ - If uncapped_recent_floor is None: probes at most per_account_row_cap newest
500
+ rows >= cutoff.
501
+ - If uncapped_recent_floor is provided: unions the capped tail [cutoff, uncapped_floor)
502
+ bounded by per_account_row_cap with all uncapped rows >= uncapped_floor.
503
+ """
504
+ capped_tail_limit = max(0, per_account_row_cap)
505
+ if not account_ids:
506
+ return {}
507
+
508
+ if window == "primary":
509
+ window_clause = "coalesce(window, 'primary') = 'primary'"
510
+ window_params: list[object] = []
511
+ else:
512
+ window_clause = "window = ?"
513
+ window_params = [window]
514
+
515
+ capped_only_sql = f"""
516
+ select id, account_id, used_percent, recorded_at, reset_at, window_minutes
517
+ from usage_history
518
+ where account_id = ?
519
+ and {window_clause}
520
+ and recorded_at >= ?
521
+ order by recorded_at desc, id desc
522
+ limit ?
523
+ """
524
+
525
+ compound_floor_sql = f"""
526
+ select id, account_id, used_percent, recorded_at, reset_at, window_minutes
527
+ from (
528
+ select id, account_id, used_percent, recorded_at, reset_at, window_minutes
529
+ from usage_history
530
+ where account_id = ?
531
+ and {window_clause}
532
+ and recorded_at >= ?
533
+ and recorded_at < ?
534
+ order by recorded_at desc, id desc
535
+ limit ?
536
+ )
537
+ union all
538
+ select id, account_id, used_percent, recorded_at, reset_at, window_minutes
539
+ from usage_history
540
+ where account_id = ?
541
+ and {window_clause}
542
+ and recorded_at >= ?
543
+ """
544
+
545
+ all_recent_sql = f"""
546
+ select id, account_id, used_percent, recorded_at, reset_at, window_minutes
547
+ from usage_history
548
+ where account_id = ?
549
+ and {window_clause}
550
+ and recorded_at >= ?
551
+ order by recorded_at asc, id asc
552
+ """
553
+
554
+ grouped: dict[str, list[UsageHistorySnapshot]] = {}
555
+
556
+ with closing(sqlite3.connect(db_path, detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES)) as conn:
557
+ conn.execute("PRAGMA query_only=ON")
558
+ conn.execute("PRAGMA busy_timeout=30000")
559
+
560
+ for account_id in account_ids:
561
+ cutoff = max(cutoffs.get(account_id, since), since) if cutoffs else since
562
+ cutoff_param = cutoff.isoformat(sep=" ")
563
+
564
+ if uncapped_recent_floor is None:
565
+ query = capped_only_sql
566
+ params: list[object] = [account_id, *window_params, cutoff_param, capped_tail_limit]
567
+ else:
568
+ uncapped_floor = max(cutoff, uncapped_recent_floor)
569
+ if uncapped_floor <= cutoff:
570
+ # Floor covers the entire lookback; run uncapped query
571
+ query = all_recent_sql
572
+ params = [account_id, *window_params, cutoff_param]
573
+ else:
574
+ floor_param = uncapped_floor.isoformat(sep=" ")
575
+ query = compound_floor_sql
576
+ params = [
577
+ account_id,
578
+ *window_params,
579
+ cutoff_param,
580
+ floor_param,
581
+ capped_tail_limit,
582
+ account_id,
583
+ *window_params,
584
+ floor_param,
585
+ ]
586
+
587
+ rows = conn.execute(query, params).fetchall()
588
+ if not rows:
589
+ continue
590
+
591
+ snapshots = [
592
+ UsageHistorySnapshot(
593
+ id=int(r[0]),
594
+ account_id=str(r[1]),
595
+ used_percent=float(r[2]),
596
+ recorded_at=_parse_sqlite_datetime(r[3]),
597
+ reset_at=float(r[4]) if r[4] is not None else None,
598
+ window_minutes=int(r[5]) if r[5] is not None else None,
599
+ )
600
+ for r in rows
601
+ ]
602
+ # Order oldest-first by (recorded_at, id) matching PostgreSQL contract
603
+ snapshots.sort(key=lambda s: (s.recorded_at, s.id))
604
+ grouped[account_id] = snapshots
605
+
606
+ return grouped
607
+
608
+
609
+ def _resolve_additional_quota_key(
610
+ *,
611
+ quota_key: str | None = None,
612
+ limit_name: str | None = None,
613
+ metered_feature: str | None = None,
614
+ ) -> str | None:
615
+ candidate_limit_name = quota_key if quota_key is not None else limit_name
616
+ if candidate_limit_name is None and metered_feature is None:
617
+ return None
618
+ return canonicalize_additional_quota_key(
619
+ quota_key=quota_key,
620
+ limit_name=candidate_limit_name,
621
+ metered_feature=metered_feature,
622
+ )
623
+
624
+
625
+ def _resolve_additional_quota_query_scope(
626
+ *,
627
+ quota_key: str | None = None,
628
+ limit_name: str | None = None,
629
+ metered_feature: str | None = None,
630
+ ) -> AdditionalQuotaQueryScope | None:
631
+ return get_additional_quota_query_scope(
632
+ quota_key=quota_key,
633
+ limit_name=limit_name,
634
+ metered_feature=metered_feature,
635
+ )
636
+
637
+
638
+ def _additional_quota_match_clause(scope: AdditionalQuotaQueryScope, *, canonical_only: bool = False):
639
+ clauses = [AdditionalUsageHistory.quota_key.in_(tuple(scope.quota_key_match_values or {scope.quota_key}))]
640
+ if canonical_only:
641
+ return clauses[0]
642
+ alias_clause = _additional_quota_alias_match_clause(scope)
643
+ if alias_clause is not None:
644
+ clauses.append(alias_clause)
645
+ return or_(*clauses)
646
+
647
+
648
+ def _additional_quota_alias_match_clause(scope: AdditionalQuotaQueryScope):
649
+ clauses = []
650
+ if scope.limit_name_match_values:
651
+ clauses.append(func.lower(AdditionalUsageHistory.limit_name).in_(tuple(scope.limit_name_match_values)))
652
+ if scope.metered_feature_match_values:
653
+ clauses.append(
654
+ func.lower(AdditionalUsageHistory.metered_feature).in_(tuple(scope.metered_feature_match_values))
655
+ )
656
+ if not clauses:
657
+ return None
658
+ return or_(*clauses)
659
+
660
+
661
+ def _newer_additional_usage_entry(
662
+ current: AdditionalUsageHistory | None,
663
+ candidate: AdditionalUsageHistory,
664
+ ) -> AdditionalUsageHistory:
665
+ if current is None:
666
+ return candidate
667
+ current_key = (
668
+ current.recorded_at,
669
+ current.used_percent,
670
+ current.id,
671
+ )
672
+ candidate_key = (
673
+ candidate.recorded_at,
674
+ candidate.used_percent,
675
+ candidate.id,
676
+ )
677
+ return candidate if candidate_key > current_key else current
678
+
679
+
680
+ def _merge_latest_additional_usage_entries(
681
+ entries: dict[str, AdditionalUsageHistory],
682
+ candidates: Collection[AdditionalUsageHistory],
683
+ ) -> None:
684
+ for candidate in candidates:
685
+ entries[candidate.account_id] = _newer_additional_usage_entry(
686
+ entries.get(candidate.account_id),
687
+ candidate,
688
+ )
689
+
690
+
691
+ class UsageRepository:
692
+ def __init__(self, session: AsyncSession) -> None:
693
+ self._session = session
694
+
695
+ async def latest_entry_for_account(
696
+ self,
697
+ account_id: str,
698
+ *,
699
+ window: str | None = None,
700
+ ) -> UsageHistory | None:
701
+ stmt = (
702
+ select(UsageHistory)
703
+ .where(UsageHistory.account_id == account_id)
704
+ .where(_window_clause(window))
705
+ .order_by(UsageHistory.recorded_at.desc(), UsageHistory.id.desc())
706
+ .limit(1)
707
+ )
708
+ result = await self._session.execute(stmt)
709
+ return result.scalar_one_or_none()
710
+
711
+ async def add_entry(
712
+ self,
713
+ account_id: str,
714
+ used_percent: float,
715
+ input_tokens: int | None = None,
716
+ output_tokens: int | None = None,
717
+ recorded_at: datetime | None = None,
718
+ window: str | None = None,
719
+ reset_at: int | None = None,
720
+ window_minutes: int | None = None,
721
+ credits_has: bool | None = None,
722
+ credits_unlimited: bool | None = None,
723
+ credits_balance: float | None = None,
724
+ ) -> UsageHistory:
725
+ entry = UsageHistory(
726
+ account_id=account_id,
727
+ used_percent=used_percent,
728
+ input_tokens=input_tokens,
729
+ output_tokens=output_tokens,
730
+ window=window,
731
+ reset_at=reset_at,
732
+ window_minutes=window_minutes,
733
+ credits_has=credits_has,
734
+ credits_unlimited=credits_unlimited,
735
+ credits_balance=credits_balance,
736
+ recorded_at=recorded_at or utcnow(),
737
+ )
738
+ async with sqlite_writer_section():
739
+ # Telemetry write: this transaction only appends usage-history
740
+ # rows, so its commit may skip the synchronous WAL flush.
741
+ await relax_commit_durability(self._session)
742
+ self._session.add(entry)
743
+ await self._session.commit()
744
+ await self._session.refresh(entry)
745
+ return entry
746
+
747
+ async def add_account_snapshot(
748
+ self,
749
+ account_id: str,
750
+ windows: Collection[UsageWindowWrite],
751
+ *,
752
+ recorded_at: datetime | None = None,
753
+ ) -> list[UsageHistory]:
754
+ """Persist one account's standard usage windows atomically."""
755
+ if not windows:
756
+ return []
757
+ entries = _account_snapshot_entries(account_id, windows, recorded_at=recorded_at)
758
+ try:
759
+ async with sqlite_writer_section():
760
+ # Telemetry write: this transaction only appends usage-history
761
+ # rows, so its commit may skip the synchronous WAL flush.
762
+ await relax_commit_durability(self._session)
763
+ self._session.add_all(entries)
764
+ await self._session.commit()
765
+ except BaseException:
766
+ await self._session.rollback()
767
+ raise
768
+ return entries
769
+
770
+ async def _resolve_postgresql_live_snapshot_owner(
771
+ self,
772
+ account_id: str | None,
773
+ chatgpt_account_id: str | None,
774
+ ) -> str | None:
775
+ locked_identities = (chatgpt_account_id,)
776
+ fallback_identity = chatgpt_account_id
777
+ relocked = False
778
+
779
+ while True:
780
+ await lock_postgresql_account_identities(self._session, locked_identities)
781
+ locked_identity_values = frozenset(identity for identity in locked_identities if identity)
782
+ identity_to_relock: str | None = None
783
+
784
+ if account_id is not None:
785
+ # Read before taking the row lock so MVCC preserves the
786
+ # current recovery identity even when its writer has already
787
+ # deleted the local row but not committed yet.
788
+ observed = (
789
+ await self._session.execute(
790
+ select(Account.id, Account.chatgpt_account_id).where(Account.id == account_id)
791
+ )
792
+ ).one_or_none()
793
+ if observed is not None:
794
+ observed_identity = observed.chatgpt_account_id
795
+ if observed_identity and observed_identity not in locked_identity_values:
796
+ identity_to_relock = observed_identity
797
+ else:
798
+ locked = (
799
+ await self._session.execute(
800
+ select(Account.id, Account.chatgpt_account_id)
801
+ .where(Account.id == account_id)
802
+ .with_for_update(key_share=True)
803
+ )
804
+ ).one_or_none()
805
+ if locked is not None:
806
+ if locked.chatgpt_account_id and locked.chatgpt_account_id not in locked_identity_values:
807
+ identity_to_relock = locked.chatgpt_account_id
808
+ else:
809
+ return locked.id
810
+
811
+ if identity_to_relock is not None:
812
+ if relocked:
813
+ raise LiveSnapshotOwnerIdentityRelockError(
814
+ "Live snapshot owner identity changed during PostgreSQL relock"
815
+ )
816
+ # Release the first lock before adding another identity; the
817
+ # shared helper can then reacquire the full set in canonical
818
+ # order without inverting an account writer's lock order.
819
+ await self._session.rollback()
820
+ fallback_identity = identity_to_relock
821
+ locked_identities = (chatgpt_account_id, identity_to_relock)
822
+ relocked = True
823
+ continue
824
+
825
+ if fallback_identity:
826
+ upstream_stmt = (
827
+ select(Account.id)
828
+ .where(Account.chatgpt_account_id == fallback_identity)
829
+ .with_for_update(key_share=True)
830
+ )
831
+ matches = list((await self._session.execute(upstream_stmt)).scalars().all())
832
+ if len(matches) == 1:
833
+ return matches[0]
834
+ return None
835
+
836
+ async def settle_live_account_snapshot(
837
+ self,
838
+ *,
839
+ account_id: str | None,
840
+ chatgpt_account_id: str | None,
841
+ windows: Collection[UsageWindowWrite],
842
+ should_skip: Callable[[str], bool],
843
+ ) -> str | None:
844
+ """Resolve a live snapshot owner and atomically persist its windows."""
845
+ if not windows:
846
+ return None
847
+
848
+ try:
849
+ async with sqlite_writer_section():
850
+ bind = self._session.get_bind()
851
+ dialect_name = bind.dialect.name if bind is not None else "sqlite"
852
+ if dialect_name == "sqlite":
853
+ # Acquire SQLite's database-wide writer slot before owner
854
+ # lookup. Consolidation then commits before this lookup or
855
+ # waits until the snapshot commit, so the chosen FK owner
856
+ # cannot disappear between SELECT and INSERT.
857
+ await self._session.execute(text("BEGIN IMMEDIATE"))
858
+ resolved_account_id = None
859
+ if account_id is not None:
860
+ resolved_account_id = await self._session.scalar(
861
+ select(Account.id).where(Account.id == account_id)
862
+ )
863
+ if resolved_account_id is None and chatgpt_account_id:
864
+ matches = list(
865
+ (
866
+ await self._session.execute(
867
+ select(Account.id).where(Account.chatgpt_account_id == chatgpt_account_id)
868
+ )
869
+ )
870
+ .scalars()
871
+ .all()
872
+ )
873
+ if len(matches) == 1:
874
+ resolved_account_id = matches[0]
875
+ else:
876
+ resolved_account_id = await self._resolve_postgresql_live_snapshot_owner(
877
+ account_id,
878
+ chatgpt_account_id,
879
+ )
880
+
881
+ if resolved_account_id is None or should_skip(resolved_account_id):
882
+ await self._session.rollback()
883
+ return None
884
+
885
+ entries = _account_snapshot_entries(resolved_account_id, windows)
886
+ # Telemetry write: this transaction only locks the owner and
887
+ # appends usage-history rows, so it may skip synchronous WAL
888
+ # flush just like add_account_snapshot().
889
+ await relax_commit_durability(self._session)
890
+ self._session.add_all(entries)
891
+ await self._session.commit()
892
+ except BaseException:
893
+ await self._session.rollback()
894
+ raise
895
+ return resolved_account_id
896
+
897
+ async def aggregate_since(
898
+ self,
899
+ since: datetime,
900
+ window: str | None = None,
901
+ ) -> list[UsageAggregateRow]:
902
+ conditions = [UsageHistory.recorded_at >= since]
903
+ if window:
904
+ conditions.append(_window_clause(window))
905
+ stmt = (
906
+ select(
907
+ UsageHistory.account_id,
908
+ func.avg(UsageHistory.used_percent).label("used_percent_avg"),
909
+ func.sum(UsageHistory.input_tokens).label("input_tokens_sum"),
910
+ func.sum(UsageHistory.output_tokens).label("output_tokens_sum"),
911
+ func.count(UsageHistory.id).label("samples"),
912
+ func.max(UsageHistory.recorded_at).label("last_recorded_at"),
913
+ func.max(UsageHistory.reset_at).label("reset_at_max"),
914
+ func.max(UsageHistory.window_minutes).label("window_minutes_max"),
915
+ )
916
+ .where(*conditions)
917
+ .group_by(UsageHistory.account_id)
918
+ )
919
+ result = await self._session.execute(stmt)
920
+ rows = result.all()
921
+ return [
922
+ UsageAggregateRow(
923
+ account_id=row.account_id,
924
+ used_percent_avg=float(row.used_percent_avg) if row.used_percent_avg is not None else None,
925
+ input_tokens_sum=int(row.input_tokens_sum) if row.input_tokens_sum is not None else None,
926
+ output_tokens_sum=int(row.output_tokens_sum) if row.output_tokens_sum is not None else None,
927
+ samples=int(row.samples),
928
+ last_recorded_at=row.last_recorded_at,
929
+ reset_at_max=int(row.reset_at_max) if row.reset_at_max is not None else None,
930
+ window_minutes_max=int(row.window_minutes_max) if row.window_minutes_max is not None else None,
931
+ )
932
+ for row in rows
933
+ ]
934
+
935
+ async def positive_used_percent_deltas_by_account(
936
+ self,
937
+ account_windows: Mapping[str, NormalizedUsageWindow],
938
+ *,
939
+ since: datetime,
940
+ until: datetime,
941
+ ) -> dict[str, float]:
942
+ if not account_windows:
943
+ return {}
944
+ account_window_pairs = tuple(account_windows.items())
945
+ samples = (
946
+ select(
947
+ UsageHistory.account_id.label("account_id"),
948
+ UsageHistory.used_percent.label("used_percent"),
949
+ func.lag(UsageHistory.used_percent)
950
+ .over(
951
+ partition_by=UsageHistory.account_id,
952
+ order_by=(UsageHistory.recorded_at, UsageHistory.id),
953
+ )
954
+ .label("previous_used_percent"),
955
+ )
956
+ .where(
957
+ tuple_(UsageHistory.account_id, _normalized_window_expr()).in_(account_window_pairs),
958
+ UsageHistory.recorded_at >= since,
959
+ UsageHistory.recorded_at <= until,
960
+ )
961
+ .subquery("weekly_demand_samples")
962
+ )
963
+ delta = samples.c.used_percent - samples.c.previous_used_percent
964
+ statement = select(
965
+ samples.c.account_id,
966
+ func.coalesce(
967
+ func.sum(case((delta > 0, delta), else_=0.0)),
968
+ 0.0,
969
+ ).label("positive_delta"),
970
+ ).group_by(samples.c.account_id)
971
+ rows = (await self._session.execute(statement)).all()
972
+ return {str(row.account_id): float(row.positive_delta) for row in rows}
973
+
974
+ async def latest_by_account(
975
+ self,
976
+ window: str | None = None,
977
+ *,
978
+ account_ids: Collection[str] | None = None,
979
+ ) -> dict[str, UsageHistory]:
980
+ conditions = _window_clause(window)
981
+ if account_ids is not None and not account_ids:
982
+ return {}
983
+ if account_ids is not None:
984
+ conditions = and_(conditions, UsageHistory.account_id.in_(account_ids))
985
+ bind = self._session.get_bind()
986
+ dialect = bind.dialect.name if bind else "sqlite"
987
+ sqlite_path = _sqlite_path_from_bind(bind) if dialect == "sqlite" else None
988
+ if sqlite_path is not None:
989
+ return await to_thread.run_sync(
990
+ _latest_by_account_sqlite,
991
+ str(sqlite_path),
992
+ window,
993
+ list(account_ids) if account_ids is not None else None,
994
+ )
995
+ if dialect == "postgresql":
996
+ acct_stmt = select(Account.id)
997
+ if account_ids is not None:
998
+ acct_stmt = acct_stmt.where(Account.id.in_(account_ids))
999
+ acct_subq = acct_stmt.subquery("accts")
1000
+ lateral = (
1001
+ select(UsageHistory.id)
1002
+ .where(
1003
+ conditions,
1004
+ UsageHistory.account_id == acct_subq.c.id,
1005
+ )
1006
+ .order_by(UsageHistory.recorded_at.desc(), UsageHistory.id.desc())
1007
+ .limit(1)
1008
+ .correlate(acct_subq)
1009
+ .lateral("latest")
1010
+ )
1011
+ id_query = (
1012
+ select(lateral.c.id).select_from(acct_subq.outerjoin(lateral, true())).where(lateral.c.id.is_not(None))
1013
+ )
1014
+ stmt = select(UsageHistory).where(UsageHistory.id.in_(id_query))
1015
+ result = await self._session.execute(stmt)
1016
+ return {entry.account_id: entry for entry in result.scalars().all()}
1017
+
1018
+ acct_stmt = select(Account.id)
1019
+ if account_ids is not None:
1020
+ acct_stmt = acct_stmt.where(Account.id.in_(account_ids))
1021
+ acct_subq = acct_stmt.subquery("accts")
1022
+ latest_id = (
1023
+ select(UsageHistory.id)
1024
+ .where(
1025
+ conditions,
1026
+ UsageHistory.account_id == acct_subq.c.id,
1027
+ )
1028
+ .order_by(UsageHistory.recorded_at.desc(), UsageHistory.id.desc())
1029
+ .limit(1)
1030
+ .correlate(acct_subq)
1031
+ .scalar_subquery()
1032
+ )
1033
+ id_rows = select(latest_id.label("usage_id")).select_from(acct_subq).subquery("latest_ids")
1034
+ stmt = select(UsageHistory).join(id_rows, UsageHistory.id == id_rows.c.usage_id)
1035
+ result = await self._session.execute(stmt)
1036
+ return {entry.account_id: entry for entry in result.scalars().all()}
1037
+
1038
+ async def history_since(
1039
+ self,
1040
+ account_id: str,
1041
+ window: str,
1042
+ since: datetime,
1043
+ ) -> list[UsageHistory]:
1044
+ stmt = (
1045
+ select(UsageHistory)
1046
+ .where(
1047
+ UsageHistory.account_id == account_id,
1048
+ _window_clause(window),
1049
+ UsageHistory.recorded_at >= since,
1050
+ )
1051
+ .order_by(UsageHistory.recorded_at.asc(), UsageHistory.id.asc())
1052
+ )
1053
+ result = await self._session.execute(stmt)
1054
+ return list(result.scalars().all())
1055
+
1056
+ async def bulk_history_since(
1057
+ self,
1058
+ account_ids: list[str],
1059
+ window: str,
1060
+ since: datetime,
1061
+ *,
1062
+ cutoffs: dict[str, datetime] | None = None,
1063
+ per_account_row_cap: int | None = None,
1064
+ uncapped_recent_floor: datetime | None = None,
1065
+ ) -> dict[str, list[UsageHistorySnapshot]]:
1066
+ """Fetch minimal usage history fields for multiple accounts in a single query.
1067
+
1068
+ ``since`` is the global floor. ``cutoffs`` optionally tightens the
1069
+ lookback per account: callers whose accounts have different window
1070
+ lengths would otherwise widen the fetch to the longest window for
1071
+ every account and discard the surplus in Python. When
1072
+ ``per_account_row_cap`` is set on SQLite, ``_bulk_history_since_capped_sqlite``
1073
+ honors ``cutoffs``; the uncapped SQLite snapshot-cache path ignores
1074
+ ``cutoffs`` (its cache is keyed on the shared floor) where callers keep
1075
+ their own per-account trimming.
1076
+
1077
+ ``per_account_row_cap`` additionally bounds each account's slice to
1078
+ its newest rows inside the cutoff (supported on both PostgreSQL and
1079
+ SQLite). Live snapshot ingestion appends usage rows per proxied request,
1080
+ so a busy account's 7-day window can hold tens of thousands of rows
1081
+ while the projection consumers (EWMA depletion, weekly-pace burn/smoothing)
1082
+ only read the recent tail. Each capped slice keeps oldest-first ordering.
1083
+
1084
+ ``uncapped_recent_floor`` exempts rows at or after the given time
1085
+ from the row cap: every in-cutoff row newer than the floor is always
1086
+ returned, and the cap bounds only the older remainder. Consumers
1087
+ whose math weighs every sample in a fixed time window equally (the
1088
+ weekly-pace smoothing mean) pass their window start here so a
1089
+ write-rate burst can never silently truncate that window, while
1090
+ tail-weighted consumers (EWMA) stay covered by the cap alone.
1091
+ Honored whenever ``per_account_row_cap`` is set on PostgreSQL or SQLite.
1092
+ """
1093
+ if not account_ids:
1094
+ return {}
1095
+ bind = self._session.get_bind()
1096
+ dialect = bind.dialect.name if bind else "sqlite"
1097
+ sqlite_path = _sqlite_path_from_bind(bind) if dialect == "sqlite" else None
1098
+ if sqlite_path is not None:
1099
+ if per_account_row_cap is not None:
1100
+ return await to_thread.run_sync(
1101
+ partial(
1102
+ _bulk_history_since_capped_sqlite,
1103
+ str(sqlite_path),
1104
+ list(account_ids),
1105
+ window,
1106
+ since,
1107
+ cutoffs=cutoffs,
1108
+ per_account_row_cap=per_account_row_cap,
1109
+ uncapped_recent_floor=uncapped_recent_floor,
1110
+ )
1111
+ )
1112
+ return await to_thread.run_sync(
1113
+ _bulk_history_since_sqlite,
1114
+ str(sqlite_path),
1115
+ list(account_ids),
1116
+ window,
1117
+ since,
1118
+ )
1119
+
1120
+ if per_account_row_cap is not None and dialect == "postgresql":
1121
+ return await self._bulk_history_since_capped_postgresql(
1122
+ account_ids,
1123
+ window,
1124
+ since,
1125
+ cutoffs=cutoffs,
1126
+ per_account_row_cap=per_account_row_cap,
1127
+ uncapped_recent_floor=uncapped_recent_floor,
1128
+ )
1129
+
1130
+ if cutoffs:
1131
+ recency_clause = or_(
1132
+ *(
1133
+ and_(
1134
+ UsageHistory.account_id == account_id,
1135
+ UsageHistory.recorded_at >= max(cutoffs.get(account_id, since), since),
1136
+ )
1137
+ for account_id in account_ids
1138
+ )
1139
+ )
1140
+ else:
1141
+ recency_clause = and_(
1142
+ UsageHistory.account_id.in_(account_ids),
1143
+ UsageHistory.recorded_at >= since,
1144
+ )
1145
+ stmt = (
1146
+ select(
1147
+ UsageHistory.id,
1148
+ UsageHistory.account_id,
1149
+ UsageHistory.used_percent,
1150
+ UsageHistory.recorded_at,
1151
+ UsageHistory.reset_at,
1152
+ UsageHistory.window_minutes,
1153
+ )
1154
+ .where(
1155
+ recency_clause,
1156
+ _window_clause(window),
1157
+ )
1158
+ .order_by(UsageHistory.account_id, UsageHistory.recorded_at.asc(), UsageHistory.id.asc())
1159
+ )
1160
+ result = await self._session.execute(stmt)
1161
+ grouped: dict[str, list[UsageHistorySnapshot]] = {}
1162
+ for id_, account_id, used_percent, recorded_at, reset_at, window_minutes in result.tuples():
1163
+ snapshot = UsageHistorySnapshot(
1164
+ id=int(id_),
1165
+ account_id=account_id,
1166
+ used_percent=float(used_percent),
1167
+ recorded_at=recorded_at,
1168
+ reset_at=float(reset_at) if reset_at is not None else None,
1169
+ window_minutes=int(window_minutes) if window_minutes is not None else None,
1170
+ )
1171
+ grouped.setdefault(account_id, []).append(snapshot)
1172
+
1173
+ if per_account_row_cap is not None:
1174
+ capped_grouped: dict[str, list[UsageHistorySnapshot]] = {}
1175
+ for account_id, snapshots in grouped.items():
1176
+ if uncapped_recent_floor is None:
1177
+ capped_grouped[account_id] = snapshots[-per_account_row_cap:] if per_account_row_cap > 0 else []
1178
+ else:
1179
+ cutoff = max(cutoffs.get(account_id, since), since) if cutoffs else since
1180
+ eff_floor = max(cutoff, uncapped_recent_floor)
1181
+ recent = [s for s in snapshots if s.recorded_at >= eff_floor]
1182
+ tail = [s for s in snapshots if s.recorded_at < eff_floor]
1183
+ capped_tail = tail[-per_account_row_cap:] if per_account_row_cap > 0 else []
1184
+ capped_grouped[account_id] = capped_tail + recent
1185
+ return capped_grouped
1186
+
1187
+ return grouped
1188
+
1189
+ async def _bulk_history_since_capped_postgresql(
1190
+ self,
1191
+ account_ids: list[str],
1192
+ window: str,
1193
+ since: datetime,
1194
+ *,
1195
+ cutoffs: dict[str, datetime] | None,
1196
+ per_account_row_cap: int,
1197
+ uncapped_recent_floor: datetime | None,
1198
+ ) -> dict[str, list[UsageHistorySnapshot]]:
1199
+ """Per-account newest-first capped fetch (PostgreSQL).
1200
+
1201
+ One lateral top-N probe per account instead of one shared range scan:
1202
+ the probe descends idx_usage_window_account_time_covering (or its
1203
+ raw-window twin) backward and stops at the cap or the account's
1204
+ cutoff, whichever comes first, so the read never touches the bulk of
1205
+ a dense account's window. The OR-of-cutoffs shape this replaces
1206
+ returned every in-window row (hundreds of thousands on dense
1207
+ deployments) to Python only for the projection consumers to use the
1208
+ recent tail.
1209
+
1210
+ With ``uncapped_recent_floor`` the probe splits into two disjoint
1211
+ branches over the same covering index: rows at or after the floor are
1212
+ returned in full (time-bounded, so still cheap), and the top-N cap
1213
+ applies only to rows between the cutoff and the floor. Snapshot
1214
+ ingestion writes per proxied request whenever the usage fingerprint
1215
+ moves, so a fixed row cap alone cannot guarantee it out-lasts a
1216
+ burst inside an equal-weight consumer window.
1217
+ """
1218
+ value_columns = [
1219
+ column("account_id", String()),
1220
+ column("cutoff", UsageHistory.recorded_at.type),
1221
+ ]
1222
+ if uncapped_recent_floor is not None:
1223
+ value_columns.append(column("uncapped_floor", UsageHistory.recorded_at.type))
1224
+ value_rows: list[tuple] = []
1225
+ for account_id in account_ids:
1226
+ cutoff = max(cutoffs.get(account_id, since), since) if cutoffs else since
1227
+ if uncapped_recent_floor is None:
1228
+ value_rows.append((account_id, cutoff))
1229
+ else:
1230
+ value_rows.append((account_id, cutoff, max(cutoff, uncapped_recent_floor)))
1231
+ account_cutoffs = values(*value_columns, name="account_cutoffs").data(value_rows)
1232
+ snapshot_columns = (
1233
+ UsageHistory.id,
1234
+ UsageHistory.account_id,
1235
+ UsageHistory.used_percent,
1236
+ UsageHistory.recorded_at,
1237
+ UsageHistory.reset_at,
1238
+ UsageHistory.window_minutes,
1239
+ )
1240
+ capped_tail = (
1241
+ select(*snapshot_columns)
1242
+ .where(
1243
+ UsageHistory.account_id == account_cutoffs.c.account_id,
1244
+ UsageHistory.recorded_at >= account_cutoffs.c.cutoff,
1245
+ *(
1246
+ (UsageHistory.recorded_at < account_cutoffs.c.uncapped_floor,)
1247
+ if uncapped_recent_floor is not None
1248
+ else ()
1249
+ ),
1250
+ _window_clause(window),
1251
+ )
1252
+ .order_by(UsageHistory.recorded_at.desc(), UsageHistory.id.desc())
1253
+ .limit(per_account_row_cap)
1254
+ .correlate(account_cutoffs)
1255
+ )
1256
+ if uncapped_recent_floor is not None:
1257
+ uncapped_recent = (
1258
+ select(*snapshot_columns)
1259
+ .where(
1260
+ UsageHistory.account_id == account_cutoffs.c.account_id,
1261
+ UsageHistory.recorded_at >= account_cutoffs.c.uncapped_floor,
1262
+ _window_clause(window),
1263
+ )
1264
+ .correlate(account_cutoffs)
1265
+ )
1266
+ recent = union_all(uncapped_recent, capped_tail).lateral("recent")
1267
+ else:
1268
+ recent = capped_tail.lateral("recent")
1269
+ stmt = select(recent).select_from(account_cutoffs.join(recent, true()))
1270
+ result = await self._session.execute(stmt)
1271
+ grouped: dict[str, list[UsageHistorySnapshot]] = {}
1272
+ # Positional unpacking: Row attribute lookups dominated the per-row
1273
+ # cost of this loop on dense deployments (dashboard polls hydrate
1274
+ # thousands of rows per call). Column order follows snapshot_columns.
1275
+ for id_, account_id, used_percent, recorded_at, reset_at, window_minutes in result.tuples():
1276
+ snapshot = UsageHistorySnapshot(
1277
+ id=int(id_),
1278
+ account_id=account_id,
1279
+ used_percent=float(used_percent),
1280
+ recorded_at=recorded_at,
1281
+ reset_at=float(reset_at) if reset_at is not None else None,
1282
+ window_minutes=int(window_minutes) if window_minutes is not None else None,
1283
+ )
1284
+ grouped.setdefault(account_id, []).append(snapshot)
1285
+ for snapshots in grouped.values():
1286
+ snapshots.sort(key=lambda snapshot: (snapshot.recorded_at, snapshot.id))
1287
+ return grouped
1288
+
1289
+ async def trends_by_bucket(
1290
+ self,
1291
+ since: datetime,
1292
+ bucket_seconds: int = 21600,
1293
+ window: str | None = None,
1294
+ account_id: str | None = None,
1295
+ ) -> list[UsageTrendBucket]:
1296
+ bind = self._session.get_bind()
1297
+ dialect = bind.dialect.name if bind else "sqlite"
1298
+ if dialect == "postgresql":
1299
+ bucket_expr = func.floor(func.extract("epoch", UsageHistory.recorded_at) / bucket_seconds) * bucket_seconds
1300
+ else:
1301
+ epoch_col = sqlalchemy_cast(func.strftime("%s", UsageHistory.recorded_at), Integer)
1302
+ bucket_expr = sqlalchemy_cast(epoch_col / bucket_seconds, Integer) * bucket_seconds
1303
+ bucket_col = bucket_expr.label("bucket_epoch")
1304
+
1305
+ conditions: list = [UsageHistory.recorded_at >= since]
1306
+ if window:
1307
+ conditions.append(_window_clause(window))
1308
+ if account_id:
1309
+ conditions.append(UsageHistory.account_id == account_id)
1310
+
1311
+ window_expr = _normalized_window_expr()
1312
+ if dialect == "sqlite":
1313
+ base_rows = (
1314
+ select(
1315
+ bucket_col,
1316
+ UsageHistory.id.label("usage_id"),
1317
+ UsageHistory.account_id.label("account_id"),
1318
+ window_expr.label("window"),
1319
+ UsageHistory.used_percent.label("used_percent"),
1320
+ UsageHistory.recorded_at.label("recorded_at"),
1321
+ )
1322
+ .where(*conditions)
1323
+ .subquery()
1324
+ )
1325
+
1326
+ aggregate_rows = (
1327
+ select(
1328
+ base_rows.c.bucket_epoch,
1329
+ base_rows.c.account_id,
1330
+ base_rows.c.window,
1331
+ func.avg(base_rows.c.used_percent).label("avg_used_percent"),
1332
+ func.count(base_rows.c.usage_id).label("samples"),
1333
+ func.max(base_rows.c.recorded_at).label("max_recorded_at"),
1334
+ )
1335
+ .group_by(
1336
+ base_rows.c.bucket_epoch,
1337
+ base_rows.c.account_id,
1338
+ base_rows.c.window,
1339
+ )
1340
+ .subquery()
1341
+ )
1342
+
1343
+ latest_ids = (
1344
+ select(
1345
+ aggregate_rows.c.bucket_epoch,
1346
+ aggregate_rows.c.account_id,
1347
+ aggregate_rows.c.window,
1348
+ func.max(base_rows.c.usage_id).label("usage_id"),
1349
+ )
1350
+ .join(
1351
+ base_rows,
1352
+ and_(
1353
+ base_rows.c.bucket_epoch == aggregate_rows.c.bucket_epoch,
1354
+ base_rows.c.account_id == aggregate_rows.c.account_id,
1355
+ base_rows.c.window == aggregate_rows.c.window,
1356
+ base_rows.c.recorded_at == aggregate_rows.c.max_recorded_at,
1357
+ ),
1358
+ )
1359
+ .group_by(
1360
+ aggregate_rows.c.bucket_epoch,
1361
+ aggregate_rows.c.account_id,
1362
+ aggregate_rows.c.window,
1363
+ )
1364
+ .subquery()
1365
+ )
1366
+
1367
+ stmt = (
1368
+ select(
1369
+ aggregate_rows.c.bucket_epoch,
1370
+ aggregate_rows.c.account_id,
1371
+ aggregate_rows.c.window,
1372
+ aggregate_rows.c.avg_used_percent,
1373
+ aggregate_rows.c.samples,
1374
+ UsageHistory.reset_at,
1375
+ UsageHistory.window_minutes,
1376
+ UsageHistory.recorded_at,
1377
+ )
1378
+ .join(
1379
+ latest_ids,
1380
+ and_(
1381
+ latest_ids.c.bucket_epoch == aggregate_rows.c.bucket_epoch,
1382
+ latest_ids.c.account_id == aggregate_rows.c.account_id,
1383
+ latest_ids.c.window == aggregate_rows.c.window,
1384
+ ),
1385
+ )
1386
+ .join(UsageHistory, UsageHistory.id == latest_ids.c.usage_id)
1387
+ .order_by(aggregate_rows.c.bucket_epoch)
1388
+ )
1389
+ else:
1390
+ base_rows = (
1391
+ select(
1392
+ bucket_col,
1393
+ UsageHistory.id.label("usage_id"),
1394
+ UsageHistory.account_id.label("account_id"),
1395
+ window_expr.label("window"),
1396
+ UsageHistory.used_percent.label("used_percent"),
1397
+ UsageHistory.reset_at.label("reset_at"),
1398
+ UsageHistory.window_minutes.label("window_minutes"),
1399
+ UsageHistory.recorded_at.label("recorded_at"),
1400
+ )
1401
+ .where(*conditions)
1402
+ .subquery()
1403
+ )
1404
+
1405
+ aggregate_rows = (
1406
+ select(
1407
+ base_rows.c.bucket_epoch,
1408
+ base_rows.c.account_id,
1409
+ base_rows.c.window,
1410
+ func.avg(base_rows.c.used_percent).label("avg_used_percent"),
1411
+ func.count(base_rows.c.usage_id).label("samples"),
1412
+ )
1413
+ .group_by(
1414
+ base_rows.c.bucket_epoch,
1415
+ base_rows.c.account_id,
1416
+ base_rows.c.window,
1417
+ )
1418
+ .subquery()
1419
+ )
1420
+
1421
+ latest_rows = select(
1422
+ base_rows.c.bucket_epoch,
1423
+ base_rows.c.account_id,
1424
+ base_rows.c.window,
1425
+ base_rows.c.reset_at,
1426
+ base_rows.c.window_minutes,
1427
+ base_rows.c.recorded_at,
1428
+ func.row_number()
1429
+ .over(
1430
+ partition_by=(base_rows.c.bucket_epoch, base_rows.c.account_id, base_rows.c.window),
1431
+ order_by=(base_rows.c.recorded_at.desc(), base_rows.c.usage_id.desc()),
1432
+ )
1433
+ .label("row_number"),
1434
+ ).subquery()
1435
+
1436
+ stmt = (
1437
+ select(
1438
+ aggregate_rows.c.bucket_epoch,
1439
+ aggregate_rows.c.account_id,
1440
+ aggregate_rows.c.window,
1441
+ aggregate_rows.c.avg_used_percent,
1442
+ aggregate_rows.c.samples,
1443
+ latest_rows.c.reset_at,
1444
+ latest_rows.c.window_minutes,
1445
+ latest_rows.c.recorded_at,
1446
+ )
1447
+ .join(
1448
+ latest_rows,
1449
+ and_(
1450
+ latest_rows.c.bucket_epoch == aggregate_rows.c.bucket_epoch,
1451
+ latest_rows.c.account_id == aggregate_rows.c.account_id,
1452
+ latest_rows.c.window == aggregate_rows.c.window,
1453
+ latest_rows.c.row_number == 1,
1454
+ ),
1455
+ )
1456
+ .order_by(aggregate_rows.c.bucket_epoch)
1457
+ )
1458
+
1459
+ result = await self._session.execute(stmt)
1460
+ return [
1461
+ UsageTrendBucket(
1462
+ bucket_epoch=int(row.bucket_epoch),
1463
+ account_id=row.account_id,
1464
+ window=row.window,
1465
+ avg_used_percent=float(row.avg_used_percent) if row.avg_used_percent is not None else 0.0,
1466
+ samples=int(row.samples),
1467
+ reset_at=int(row.reset_at) if row.reset_at is not None else None,
1468
+ window_minutes=int(row.window_minutes) if row.window_minutes is not None else None,
1469
+ recorded_at=row.recorded_at,
1470
+ )
1471
+ for row in result.all()
1472
+ ]
1473
+
1474
+ async def latest_window_minutes(self, window: str) -> int | None:
1475
+ conditions = _window_clause(window)
1476
+ result = await self._session.execute(select(func.max(UsageHistory.window_minutes)).where(conditions))
1477
+ value = result.scalar_one_or_none()
1478
+ return int(value) if value is not None else None
1479
+
1480
+
1481
+ class AdditionalUsageRepository:
1482
+ def __init__(self, session: AsyncSession) -> None:
1483
+ self._session = session
1484
+
1485
+ async def add_entry(
1486
+ self,
1487
+ account_id: str,
1488
+ limit_name: str,
1489
+ metered_feature: str,
1490
+ window: str,
1491
+ used_percent: float,
1492
+ reset_at: int | None = None,
1493
+ window_minutes: int | None = None,
1494
+ recorded_at: datetime | None = None,
1495
+ quota_key: str | None = None,
1496
+ ) -> None:
1497
+ effective_quota_key = _resolve_additional_quota_key(
1498
+ quota_key=quota_key,
1499
+ limit_name=limit_name,
1500
+ metered_feature=metered_feature,
1501
+ )
1502
+ if effective_quota_key is None:
1503
+ raise ValueError("additional usage quota_key could not be determined")
1504
+ entry = AdditionalUsageHistory(
1505
+ account_id=account_id,
1506
+ quota_key=effective_quota_key,
1507
+ limit_name=limit_name,
1508
+ metered_feature=metered_feature,
1509
+ window=window,
1510
+ used_percent=used_percent,
1511
+ reset_at=reset_at,
1512
+ window_minutes=window_minutes,
1513
+ recorded_at=recorded_at or utcnow(),
1514
+ )
1515
+ async with sqlite_writer_section():
1516
+ # Telemetry write: this transaction only appends one
1517
+ # additional-usage-history row, so its commit may skip the
1518
+ # synchronous WAL flush.
1519
+ await relax_commit_durability(self._session)
1520
+ self._session.add(entry)
1521
+ await self._session.commit()
1522
+
1523
+ async def delete_for_account(self, account_id: str) -> None:
1524
+ stmt = delete(AdditionalUsageHistory).where(AdditionalUsageHistory.account_id == account_id)
1525
+ async with sqlite_writer_section():
1526
+ await self._session.execute(stmt)
1527
+ await self._session.commit()
1528
+
1529
+ async def delete_for_account_and_quota_key(self, account_id: str, quota_key: str) -> None:
1530
+ scope = _resolve_additional_quota_query_scope(quota_key=quota_key)
1531
+ if scope is None:
1532
+ raise ValueError("additional usage quota_key could not be determined")
1533
+ stmt = delete(AdditionalUsageHistory).where(
1534
+ AdditionalUsageHistory.account_id == account_id,
1535
+ _additional_quota_match_clause(scope),
1536
+ )
1537
+ async with sqlite_writer_section():
1538
+ await self._session.execute(stmt)
1539
+ await self._session.commit()
1540
+
1541
+ async def delete_for_account_and_limit(self, account_id: str, limit_name: str) -> None:
1542
+ await self.delete_for_account_and_quota_key(account_id, limit_name)
1543
+
1544
+ async def delete_for_account_quota_key_window(
1545
+ self,
1546
+ account_id: str,
1547
+ quota_key: str,
1548
+ window: str,
1549
+ ) -> None:
1550
+ scope = _resolve_additional_quota_query_scope(quota_key=quota_key)
1551
+ if scope is None:
1552
+ raise ValueError("additional usage quota_key could not be determined")
1553
+ stmt = delete(AdditionalUsageHistory).where(
1554
+ AdditionalUsageHistory.account_id == account_id,
1555
+ _additional_quota_match_clause(scope),
1556
+ AdditionalUsageHistory.window == window,
1557
+ )
1558
+ async with sqlite_writer_section():
1559
+ await self._session.execute(stmt)
1560
+ await self._session.commit()
1561
+
1562
+ async def delete_for_account_limit_window(
1563
+ self,
1564
+ account_id: str,
1565
+ limit_name: str,
1566
+ window: str,
1567
+ ) -> None:
1568
+ await self.delete_for_account_quota_key_window(account_id, limit_name, window)
1569
+
1570
+ async def latest_by_account(
1571
+ self,
1572
+ quota_key: str | None = None,
1573
+ window: str | None = None,
1574
+ *,
1575
+ limit_name: str | None = None,
1576
+ account_ids: Collection[str] | None = None,
1577
+ since: datetime | None = None,
1578
+ ) -> dict[str, AdditionalUsageHistory]:
1579
+ """Returns the latest effective entry per account for a canonical quota key + window."""
1580
+ scope = _resolve_additional_quota_query_scope(
1581
+ quota_key=quota_key,
1582
+ limit_name=limit_name,
1583
+ )
1584
+ if scope is None or window is None:
1585
+ raise ValueError("quota_key/limit_name and window are required")
1586
+ bind = self._session.get_bind()
1587
+ dialect = bind.dialect.name if bind else "sqlite"
1588
+ if account_ids is not None:
1589
+ account_ids = list(account_ids)
1590
+ if not account_ids:
1591
+ return {}
1592
+ if dialect == "postgresql":
1593
+ # Correlated top-1 probes per (account × match value) instead of
1594
+ # DISTINCT ON: the scan shape costs one btree descent per probe
1595
+ # (ix_additional_usage_quota_window_latest for canonical values,
1596
+ # the lower(...) alias twins for registry aliases), so the read
1597
+ # scales with the candidate account count rather than with how
1598
+ # many history rows the quota key has accumulated. Merging the
1599
+ # per-value winners under the same (recorded_at, used_percent,
1600
+ # id) ordering reproduces the DISTINCT ON result exactly.
1601
+ probe_targets: list[tuple[Any, str]] = [
1602
+ (AdditionalUsageHistory.quota_key, value)
1603
+ for value in sorted(scope.quota_key_match_values or {scope.quota_key})
1604
+ ]
1605
+ probe_targets.extend(
1606
+ (func.lower(AdditionalUsageHistory.limit_name), value)
1607
+ for value in sorted(scope.limit_name_match_values)
1608
+ )
1609
+ probe_targets.extend(
1610
+ (func.lower(AdditionalUsageHistory.metered_feature), value)
1611
+ for value in sorted(scope.metered_feature_match_values)
1612
+ )
1613
+ acct_stmt = select(Account.id)
1614
+ if account_ids is not None:
1615
+ acct_stmt = acct_stmt.where(Account.id.in_(account_ids))
1616
+ acct_subq = acct_stmt.subquery("accts")
1617
+ entries: dict[str, AdditionalUsageHistory] = {}
1618
+ for column_expr, value in probe_targets:
1619
+ lateral_conditions = [
1620
+ AdditionalUsageHistory.account_id == acct_subq.c.id,
1621
+ AdditionalUsageHistory.window == window,
1622
+ column_expr == value,
1623
+ ]
1624
+ if since is not None:
1625
+ lateral_conditions.append(AdditionalUsageHistory.recorded_at >= since)
1626
+ lateral = (
1627
+ select(AdditionalUsageHistory.id)
1628
+ .where(*lateral_conditions)
1629
+ .order_by(
1630
+ AdditionalUsageHistory.recorded_at.desc(),
1631
+ AdditionalUsageHistory.used_percent.desc(),
1632
+ AdditionalUsageHistory.id.desc(),
1633
+ )
1634
+ .limit(1)
1635
+ .correlate(acct_subq)
1636
+ .lateral("latest")
1637
+ )
1638
+ id_query = (
1639
+ select(lateral.c.id)
1640
+ .select_from(acct_subq.outerjoin(lateral, true()))
1641
+ .where(lateral.c.id.is_not(None))
1642
+ )
1643
+ stmt = select(AdditionalUsageHistory).where(AdditionalUsageHistory.id.in_(id_query))
1644
+ result = await self._session.execute(stmt)
1645
+ _merge_latest_additional_usage_entries(entries, result.scalars().all())
1646
+ # Probe/plan order is not deterministic; callers pick response
1647
+ # metadata from the first entry, so restore the account order
1648
+ # the DISTINCT ON shape used to guarantee.
1649
+ return {account_id: entries[account_id] for account_id in sorted(entries)}
1650
+
1651
+ if dialect == "sqlite":
1652
+ return await self._latest_by_scope_sqlite_probes(
1653
+ scope,
1654
+ window,
1655
+ account_ids=account_ids,
1656
+ since=since,
1657
+ )
1658
+
1659
+ conditions = [
1660
+ _additional_quota_match_clause(scope),
1661
+ AdditionalUsageHistory.window == window,
1662
+ ]
1663
+ if account_ids is not None:
1664
+ conditions.append(AdditionalUsageHistory.account_id.in_(account_ids))
1665
+ if since is not None:
1666
+ conditions.append(AdditionalUsageHistory.recorded_at >= since)
1667
+ subq = (
1668
+ select(
1669
+ AdditionalUsageHistory.id.label("usage_id"),
1670
+ func.row_number()
1671
+ .over(
1672
+ partition_by=AdditionalUsageHistory.account_id,
1673
+ order_by=(
1674
+ AdditionalUsageHistory.recorded_at.desc(),
1675
+ AdditionalUsageHistory.used_percent.desc(),
1676
+ AdditionalUsageHistory.id.desc(),
1677
+ ),
1678
+ )
1679
+ .label("row_number"),
1680
+ )
1681
+ .where(*conditions)
1682
+ .subquery()
1683
+ )
1684
+ stmt = (
1685
+ select(AdditionalUsageHistory)
1686
+ .join(subq, AdditionalUsageHistory.id == subq.c.usage_id)
1687
+ .where(subq.c.row_number == 1)
1688
+ )
1689
+ result = await self._session.execute(stmt)
1690
+ return {entry.account_id: entry for entry in result.scalars().all()}
1691
+
1692
+ async def _latest_by_scope_sqlite_probes(
1693
+ self,
1694
+ scope: AdditionalQuotaQueryScope,
1695
+ window: str,
1696
+ *,
1697
+ account_ids: Collection[str] | None = None,
1698
+ since: datetime | None = None,
1699
+ ) -> dict[str, AdditionalUsageHistory]:
1700
+ account_values = list(account_ids) if account_ids is not None else None
1701
+ if account_values is not None and not account_values:
1702
+ return {}
1703
+ account_stmt = select(AdditionalUsageHistory.account_id).where(
1704
+ _additional_quota_match_clause(scope),
1705
+ AdditionalUsageHistory.window == window,
1706
+ )
1707
+ if account_values is not None:
1708
+ account_stmt = account_stmt.where(AdditionalUsageHistory.account_id.in_(account_values))
1709
+ if since is not None:
1710
+ account_stmt = account_stmt.where(AdditionalUsageHistory.recorded_at >= since)
1711
+ account_result = await self._session.execute(account_stmt.distinct())
1712
+ latest: dict[str, AdditionalUsageHistory] = {}
1713
+ for account_id in account_result.scalars().all():
1714
+ latest_stmt = (
1715
+ select(AdditionalUsageHistory)
1716
+ .where(
1717
+ AdditionalUsageHistory.account_id == account_id,
1718
+ _additional_quota_match_clause(scope),
1719
+ AdditionalUsageHistory.window == window,
1720
+ )
1721
+ .order_by(
1722
+ AdditionalUsageHistory.recorded_at.desc(),
1723
+ AdditionalUsageHistory.used_percent.desc(),
1724
+ AdditionalUsageHistory.id.desc(),
1725
+ )
1726
+ .limit(1)
1727
+ )
1728
+ if since is not None:
1729
+ latest_stmt = latest_stmt.where(AdditionalUsageHistory.recorded_at >= since)
1730
+ row_result = await self._session.execute(latest_stmt)
1731
+ entry = row_result.scalar_one_or_none()
1732
+ if entry is not None:
1733
+ latest[entry.account_id] = entry
1734
+ return latest
1735
+
1736
+ async def latest_by_quota_key(
1737
+ self,
1738
+ quota_key: str,
1739
+ window: str,
1740
+ *,
1741
+ account_ids: Collection[str] | None = None,
1742
+ since: datetime | None = None,
1743
+ ) -> dict[str, AdditionalUsageHistory]:
1744
+ bind = self._session.get_bind()
1745
+ dialect = bind.dialect.name if bind else "sqlite"
1746
+ if dialect == "sqlite":
1747
+ scope = _resolve_additional_quota_query_scope(quota_key=quota_key)
1748
+ if scope is None:
1749
+ raise ValueError("quota_key and window are required")
1750
+ return await self._latest_by_scope_sqlite_probes(
1751
+ scope,
1752
+ window,
1753
+ account_ids=account_ids,
1754
+ since=since,
1755
+ )
1756
+ return await self.latest_by_account(
1757
+ quota_key=quota_key,
1758
+ window=window,
1759
+ account_ids=account_ids,
1760
+ since=since,
1761
+ )
1762
+
1763
+ async def list_quota_keys(
1764
+ self,
1765
+ *,
1766
+ account_ids: Collection[str] | None = None,
1767
+ since: datetime | None = None,
1768
+ ) -> list[str]:
1769
+ bind = self._session.get_bind()
1770
+ dialect = bind.dialect.name if bind else "sqlite"
1771
+ if dialect == "postgresql" and since is None:
1772
+ label_rows = await self._distinct_label_tuples_postgres(account_ids=account_ids)
1773
+ else:
1774
+ stmt = select(
1775
+ AdditionalUsageHistory.quota_key,
1776
+ AdditionalUsageHistory.limit_name,
1777
+ AdditionalUsageHistory.metered_feature,
1778
+ ).distinct()
1779
+ if account_ids is not None:
1780
+ stmt = stmt.where(AdditionalUsageHistory.account_id.in_(account_ids))
1781
+ if since is not None:
1782
+ stmt = stmt.where(AdditionalUsageHistory.recorded_at >= since)
1783
+ result = await self._session.execute(stmt)
1784
+ label_rows = [(row[0], row[1], row[2]) for row in result.all()]
1785
+ resolved_keys = {
1786
+ resolved_key
1787
+ for quota_key_value, limit_name_value, metered_feature_value in label_rows
1788
+ if (
1789
+ resolved_key := canonicalize_additional_quota_key(
1790
+ quota_key=quota_key_value,
1791
+ limit_name=limit_name_value,
1792
+ metered_feature=metered_feature_value,
1793
+ )
1794
+ )
1795
+ is not None
1796
+ }
1797
+ return sorted(resolved_keys)
1798
+
1799
+ async def _distinct_label_tuples_postgres(
1800
+ self,
1801
+ *,
1802
+ account_ids: Collection[str] | None = None,
1803
+ ) -> list[tuple[str, str, str]]:
1804
+ """Loose-index-scan emulation for the distinct label listing.
1805
+
1806
+ PostgreSQL has no native loose index scan, so a plain ``DISTINCT``
1807
+ over the label columns reads every history row. Row-value comparison
1808
+ probes over ``ix_additional_usage_distinct_labels`` instead step
1809
+ through the distinct ``(account_id, quota_key, limit_name,
1810
+ metered_feature)`` tuples — one btree descent per distinct tuple
1811
+ (the request-log facet listing emulates the same skip scan). Each
1812
+ probe is strictly ascending, so the walk terminates after the last
1813
+ distinct tuple.
1814
+ """
1815
+ columns = (
1816
+ AdditionalUsageHistory.account_id,
1817
+ AdditionalUsageHistory.quota_key,
1818
+ AdditionalUsageHistory.limit_name,
1819
+ AdditionalUsageHistory.metered_feature,
1820
+ )
1821
+ ordered = tuple_(*columns)
1822
+ labels: list[tuple[str, str, str]] = []
1823
+ seen: set[tuple[str, str, str]] = set()
1824
+ cursor: tuple[str, str, str, str] | None = None
1825
+ while True:
1826
+ stmt = select(*columns)
1827
+ if account_ids is not None:
1828
+ stmt = stmt.where(AdditionalUsageHistory.account_id.in_(account_ids))
1829
+ if cursor is not None:
1830
+ stmt = stmt.where(ordered > cursor)
1831
+ stmt = stmt.order_by(*(column.asc() for column in columns)).limit(1)
1832
+ row = (await self._session.execute(stmt)).first()
1833
+ if row is None:
1834
+ return labels
1835
+ cursor = (row[0], row[1], row[2], row[3])
1836
+ label = (row[1], row[2], row[3])
1837
+ if label not in seen:
1838
+ seen.add(label)
1839
+ labels.append(label)
1840
+
1841
+ async def list_limit_names(
1842
+ self,
1843
+ *,
1844
+ account_ids: Collection[str] | None = None,
1845
+ since: datetime | None = None,
1846
+ ) -> list[str]:
1847
+ return await self.list_quota_keys(account_ids=account_ids, since=since)
1848
+
1849
+ async def history_since(
1850
+ self,
1851
+ account_id: str,
1852
+ quota_key: str | None = None,
1853
+ window: str | None = None,
1854
+ since: datetime | None = None,
1855
+ *,
1856
+ limit_name: str | None = None,
1857
+ ) -> list[AdditionalUsageHistory]:
1858
+ """Returns time-series entries for EWMA computation."""
1859
+ scope = _resolve_additional_quota_query_scope(
1860
+ quota_key=quota_key,
1861
+ limit_name=limit_name,
1862
+ )
1863
+ if scope is None or window is None or since is None:
1864
+ raise ValueError("account_id, quota_key/limit_name, window, and since are required")
1865
+ stmt = (
1866
+ select(AdditionalUsageHistory)
1867
+ .where(
1868
+ AdditionalUsageHistory.account_id == account_id,
1869
+ _additional_quota_match_clause(scope),
1870
+ AdditionalUsageHistory.window == window,
1871
+ AdditionalUsageHistory.recorded_at >= since,
1872
+ )
1873
+ .order_by(AdditionalUsageHistory.recorded_at.asc())
1874
+ )
1875
+ result = await self._session.execute(stmt)
1876
+ return list(result.scalars().all())
1877
+
1878
+ async def latest_recorded_at_for_account(self, account_id: str) -> datetime | None:
1879
+ """Return the most recent recorded_at for any additional usage entry of this account."""
1880
+ stmt = select(func.max(AdditionalUsageHistory.recorded_at)).where(
1881
+ AdditionalUsageHistory.account_id == account_id
1882
+ )
1883
+ result = await self._session.execute(stmt)
1884
+ return result.scalar_one_or_none()
1885
+
1886
+ async def latest_recorded_at(self) -> datetime | None:
1887
+ """Return the most recent recorded_at across all additional usage entries."""
1888
+ stmt = select(func.max(AdditionalUsageHistory.recorded_at))
1889
+ result = await self._session.execute(stmt)
1890
+ return result.scalar_one_or_none()