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,1001 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import logging
5
+ import time
6
+ import uuid
7
+ from dataclasses import dataclass
8
+ from datetime import datetime, timedelta, timezone
9
+ from typing import AsyncContextManager, Callable, Protocol
10
+
11
+ from app.core import usage as usage_core
12
+ from app.core.auth.refresh import RefreshError
13
+ from app.core.clients.proxy import UpstreamProxyRouteTrace, override_stream_timeouts, stream_responses
14
+ from app.core.crypto import TokenEncryptor
15
+ from app.core.openai.model_registry import get_model_registry
16
+ from app.core.openai.models import OpenAIError, ResponseUsage
17
+ from app.core.openai.parsing import parse_sse_event
18
+ from app.core.openai.requests import ResponsesRequest
19
+ from app.core.plan_types import account_plan_matches_allowed, normalize_account_plan_type
20
+ from app.core.resilience.toggles import bind_resilience_toggles
21
+ from app.core.upstream_proxy import ResolvedUpstreamRoute, UpstreamProxyRouteError, resolve_upstream_route
22
+ from app.core.usage.pricing import get_pricing_for_model
23
+ from app.core.utils.time import naive_utc_to_epoch, utcnow
24
+ from app.db.models import Account, AccountLimitWarmup, AccountStatus, DashboardSettings, UsageHistory
25
+ from app.modules.accounts.auth_manager import AuthManager
26
+ from app.modules.accounts.repository import AccountsRepository
27
+ from app.modules.usage.mappers import usage_history_to_window_row
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ LIMIT_WARMUP_SOURCE = "limit_warmup"
32
+ LIMIT_WARMUP_REQUEST_KIND = "warmup"
33
+ LIMIT_WARMUP_HEADER = "x-codex-lb-limit-warmup"
34
+ _DEFAULT_WARMUP_INSTRUCTIONS = "Reply with OK only."
35
+ _TERMINAL_ERROR_EVENTS = {"response.failed", "response.incomplete", "error"}
36
+ _QUOTA_ERROR_CODES = {"insufficient_quota", "quota_exceeded", "rate_limit_exceeded", "usage_limit_reached"}
37
+ _MAX_CONCURRENT_WARMUP_SENDS = 4
38
+ _ROLLING_WINDOW_SECONDS = 300 * 60
39
+ _SHORT_WINDOW_MAX_MINUTES = 24 * 60
40
+ _STAGGER_SLOT_GRACE_SECONDS = 60
41
+ _IDLE_PRIMARY_WINDOW = "primary_idle"
42
+ # Minimum reset_at forward jump (in seconds) to confirm a real quota window reset.
43
+ # Upstream timestamp jitter of ~1 second must not trigger a warm-up.
44
+ _RESET_CONFIRMED_MIN_JUMP_SECONDS = 60
45
+ # Persist the upstream value, but treat nearby values as the same reset. This
46
+ # avoids duplicate attempts when reset_at jitters between refresh cycles.
47
+ _RESET_AT_JITTER_TOLERANCE_SECONDS = 5
48
+
49
+
50
+ @dataclass(frozen=True, slots=True)
51
+ class LimitWarmupSendResult:
52
+ request_id: str
53
+ success: bool
54
+ latency_ms: int
55
+ usage: ResponseUsage | None = None
56
+ error_code: str | None = None
57
+ error_message: str | None = None
58
+ upstream_proxy_route_mode: str | None = None
59
+ upstream_proxy_pool_id: str | None = None
60
+ upstream_proxy_endpoint_id: str | None = None
61
+ upstream_proxy_fallback_used: bool | None = None
62
+ upstream_proxy_fail_closed_reason: str | None = None
63
+
64
+
65
+ @dataclass(frozen=True, slots=True)
66
+ class LimitWarmupSendOutcome:
67
+ attempt: AccountLimitWarmup
68
+ account: Account
69
+ model: str
70
+ result: LimitWarmupSendResult | None
71
+ error_message: str | None = None
72
+
73
+
74
+ class LimitWarmupSender(Protocol):
75
+ async def send(self, account: Account, *, model: str, prompt: str) -> LimitWarmupSendResult: ...
76
+
77
+
78
+ class LimitWarmupAttemptsRepository(Protocol):
79
+ async def latest_by_account(self, account_ids: list[str]) -> dict[str, AccountLimitWarmup]: ...
80
+
81
+ async def try_create_attempt(
82
+ self,
83
+ *,
84
+ account_id: str,
85
+ window: str,
86
+ reset_at: int,
87
+ model: str,
88
+ attempted_at,
89
+ status: str = "pending",
90
+ reset_at_tolerance_seconds: int = 0,
91
+ require_no_prior_attempt: bool = False,
92
+ ) -> AccountLimitWarmup | None: ...
93
+
94
+ async def complete_attempt(
95
+ self,
96
+ attempt_id: int,
97
+ *,
98
+ status: str,
99
+ completed_at,
100
+ error_code: str | None = None,
101
+ error_message: str | None = None,
102
+ ) -> AccountLimitWarmup | None: ...
103
+
104
+
105
+ class LimitWarmupRequestLogRepository(Protocol):
106
+ async def add_log(
107
+ self,
108
+ account_id: str | None,
109
+ request_id: str,
110
+ model: str,
111
+ input_tokens: int | None,
112
+ output_tokens: int | None,
113
+ latency_ms: int | None,
114
+ status: str,
115
+ error_code: str | None,
116
+ latency_first_token_ms: int | None = None,
117
+ error_message: str | None = None,
118
+ requested_at: datetime | None = None,
119
+ cached_input_tokens: int | None = None,
120
+ reasoning_tokens: int | None = None,
121
+ reasoning_effort: str | None = None,
122
+ service_tier: str | None = None,
123
+ requested_service_tier: str | None = None,
124
+ actual_service_tier: str | None = None,
125
+ transport: str | None = None,
126
+ upstream_transport: str | None = None,
127
+ api_key_id: str | None = None,
128
+ session_id: str | None = None,
129
+ plan_type: str | None = None,
130
+ source: str | None = None,
131
+ useragent: str | None = None,
132
+ useragent_group: str | None = None,
133
+ client_ip: str | None = None,
134
+ failure_phase: str | None = None,
135
+ failure_detail: str | None = None,
136
+ failure_exception_type: str | None = None,
137
+ upstream_status_code: int | None = None,
138
+ upstream_error_code: str | None = None,
139
+ bridge_stage: str | None = None,
140
+ request_kind: str = "normal",
141
+ upstream_proxy_route_mode: str | None = None,
142
+ upstream_proxy_pool_id: str | None = None,
143
+ upstream_proxy_endpoint_id: str | None = None,
144
+ upstream_proxy_fallback_used: bool | None = None,
145
+ upstream_proxy_fail_closed_reason: str | None = None,
146
+ ) -> object: ...
147
+
148
+
149
+ class StreamingLimitWarmupSender:
150
+ def __init__(
151
+ self,
152
+ accounts_repo: AccountsRepository,
153
+ *,
154
+ accounts_repo_factory: Callable[[], AsyncContextManager[AccountsRepository]] | None = None,
155
+ ) -> None:
156
+ self._accounts_repo = accounts_repo
157
+ self._accounts_repo_factory = accounts_repo_factory
158
+ self._auth_manager = AuthManager(accounts_repo)
159
+ self._encryptor = TokenEncryptor()
160
+ self._auth_lock = asyncio.Lock()
161
+
162
+ async def send(self, account: Account, *, model: str, prompt: str) -> LimitWarmupSendResult:
163
+ request_id = f"limit-warmup-{uuid.uuid4().hex}"
164
+ started = time.monotonic()
165
+ try:
166
+ async with self._auth_lock:
167
+ fresh_account = await self._ensure_fresh(account)
168
+ if (
169
+ fresh_account is None
170
+ or not _account_is_safe_candidate(fresh_account)
171
+ or not fresh_account.limit_warmup_enabled
172
+ ):
173
+ if fresh_account is None:
174
+ error_message = "Account no longer exists"
175
+ elif not fresh_account.limit_warmup_enabled:
176
+ error_message = "Limit warm-up is disabled for this account"
177
+ else:
178
+ error_message = f"Account status is {fresh_account.status.value}"
179
+ return LimitWarmupSendResult(
180
+ request_id=request_id,
181
+ success=False,
182
+ latency_ms=_elapsed_ms(started),
183
+ error_code="account_not_active",
184
+ error_message=error_message,
185
+ )
186
+ access_token = self._encryptor.decrypt(fresh_account.access_token_encrypted)
187
+ chatgpt_account_id = fresh_account.chatgpt_account_id
188
+ except RefreshError as exc:
189
+ return LimitWarmupSendResult(
190
+ request_id=request_id,
191
+ success=False,
192
+ latency_ms=_elapsed_ms(started),
193
+ error_code=f"auth_refresh_{exc.code}",
194
+ error_message=exc.message,
195
+ )
196
+
197
+ try:
198
+ route = await self._resolve_upstream_route(fresh_account)
199
+ except UpstreamProxyRouteError as exc:
200
+ return LimitWarmupSendResult(
201
+ request_id=request_id,
202
+ success=False,
203
+ latency_ms=_elapsed_ms(started),
204
+ error_code="upstream_proxy_unavailable",
205
+ error_message=f"Upstream proxy route unavailable: {exc.reason}",
206
+ upstream_proxy_fail_closed_reason=exc.reason,
207
+ )
208
+
209
+ payload = ResponsesRequest.model_validate(
210
+ {
211
+ "model": model,
212
+ "instructions": _DEFAULT_WARMUP_INSTRUCTIONS,
213
+ "input": prompt,
214
+ "tools": [],
215
+ "parallel_tool_calls": False,
216
+ "stream": True,
217
+ "store": False,
218
+ "max_output_tokens": 4,
219
+ }
220
+ )
221
+ headers = {
222
+ "x-request-id": request_id,
223
+ LIMIT_WARMUP_HEADER: "1",
224
+ "user-agent": "codex-lb-limit-warmup",
225
+ }
226
+ usage: ResponseUsage | None = None
227
+ route_trace = UpstreamProxyRouteTrace()
228
+ with override_stream_timeouts(
229
+ connect_timeout_seconds=5.0,
230
+ idle_timeout_seconds=10.0,
231
+ total_timeout_seconds=30.0,
232
+ ):
233
+ async for event_block in stream_responses(
234
+ payload,
235
+ headers,
236
+ access_token,
237
+ chatgpt_account_id,
238
+ upstream_stream_transport_override="http",
239
+ route=route,
240
+ route_trace=route_trace,
241
+ allow_direct_egress=route is None,
242
+ codex_lb_account_id=fresh_account.id,
243
+ ):
244
+ event = parse_sse_event(event_block)
245
+ if event is None:
246
+ continue
247
+ if event.response is not None and event.response.usage is not None:
248
+ usage = event.response.usage
249
+ if event.type == "response.completed":
250
+ return LimitWarmupSendResult(
251
+ request_id=request_id,
252
+ success=True,
253
+ latency_ms=_elapsed_ms(started),
254
+ usage=usage,
255
+ upstream_proxy_route_mode=route_trace.mode,
256
+ upstream_proxy_pool_id=route_trace.pool_id,
257
+ upstream_proxy_endpoint_id=route_trace.endpoint_id,
258
+ upstream_proxy_fallback_used=route_trace.fallback_used,
259
+ )
260
+ if event.type in _TERMINAL_ERROR_EVENTS:
261
+ error = _event_error(event.error, event.response.error if event.response is not None else None)
262
+ return LimitWarmupSendResult(
263
+ request_id=request_id,
264
+ success=False,
265
+ latency_ms=_elapsed_ms(started),
266
+ usage=usage,
267
+ error_code=error.code or event.type,
268
+ error_message=error.message or event.type,
269
+ upstream_proxy_route_mode=route_trace.mode,
270
+ upstream_proxy_pool_id=route_trace.pool_id,
271
+ upstream_proxy_endpoint_id=route_trace.endpoint_id,
272
+ upstream_proxy_fallback_used=route_trace.fallback_used,
273
+ )
274
+
275
+ return LimitWarmupSendResult(
276
+ request_id=request_id,
277
+ success=False,
278
+ latency_ms=_elapsed_ms(started),
279
+ usage=usage,
280
+ error_code="stream_incomplete",
281
+ error_message="Warm-up stream ended without a terminal event",
282
+ upstream_proxy_route_mode=route_trace.mode,
283
+ upstream_proxy_pool_id=route_trace.pool_id,
284
+ upstream_proxy_endpoint_id=route_trace.endpoint_id,
285
+ upstream_proxy_fallback_used=route_trace.fallback_used,
286
+ )
287
+
288
+ async def _ensure_fresh(self, account: Account) -> Account | None:
289
+ if self._accounts_repo_factory is None:
290
+ current = await self._accounts_repo.get_by_id_fresh(account.id)
291
+ if current is None or not _account_is_safe_candidate(current) or not current.limit_warmup_enabled:
292
+ return current
293
+ await self._auth_manager.ensure_fresh(current)
294
+ return await self._accounts_repo.get_by_id_fresh(account.id)
295
+ async with self._accounts_repo_factory() as accounts_repo:
296
+ current = await accounts_repo.get_by_id_fresh(account.id)
297
+ if current is None or not _account_is_safe_candidate(current) or not current.limit_warmup_enabled:
298
+ return current
299
+ await AuthManager(
300
+ accounts_repo,
301
+ refresh_repo_factory=self._accounts_repo_factory,
302
+ ).ensure_fresh(current)
303
+ async with self._accounts_repo_factory() as accounts_repo:
304
+ return await accounts_repo.get_by_id_fresh(account.id)
305
+
306
+ async def _resolve_upstream_route(self, account: Account) -> ResolvedUpstreamRoute | None:
307
+ if self._accounts_repo_factory is not None:
308
+ async with self._accounts_repo_factory() as accounts_repo:
309
+ return await resolve_upstream_route(
310
+ accounts_repo.session,
311
+ account_id=account.id,
312
+ operation="limit_warmup",
313
+ scope="account",
314
+ encryptor=self._encryptor,
315
+ )
316
+ return await resolve_upstream_route(
317
+ self._accounts_repo.session,
318
+ account_id=account.id,
319
+ operation="limit_warmup",
320
+ scope="account",
321
+ encryptor=self._encryptor,
322
+ )
323
+
324
+
325
+ class LimitWarmupService:
326
+ def __init__(
327
+ self,
328
+ warmup_repo: LimitWarmupAttemptsRepository,
329
+ request_logs_repo: LimitWarmupRequestLogRepository,
330
+ *,
331
+ sender: LimitWarmupSender | None = None,
332
+ ) -> None:
333
+ self._warmup_repo = warmup_repo
334
+ self._request_logs_repo = request_logs_repo
335
+ self._sender = sender
336
+
337
+ async def run_after_usage_refresh(
338
+ self,
339
+ *,
340
+ accounts: list[Account],
341
+ stagger_accounts: list[Account] | None = None,
342
+ settings: DashboardSettings,
343
+ before_primary: dict[str, UsageHistory],
344
+ before_secondary: dict[str, UsageHistory],
345
+ after_primary: dict[str, UsageHistory],
346
+ after_secondary: dict[str, UsageHistory],
347
+ previous_plan_types: dict[str, str | None] | None = None,
348
+ refresh_started_at: datetime | None = None,
349
+ usage_refresh_interval_seconds: int = _STAGGER_SLOT_GRACE_SECONDS,
350
+ ) -> None:
351
+ if not settings.limit_warmup_enabled:
352
+ return
353
+ # C2-3 resilience toggles: bound before the warmup fan-out so the
354
+ # upstream probes gate their breaker on the dashboard value.
355
+ bind_resilience_toggles(settings)
356
+ selected_windows = _selected_windows(settings.limit_warmup_windows)
357
+ if not selected_windows:
358
+ return
359
+
360
+ account_ids = [account.id for account in accounts]
361
+ latest_attempts = await self._warmup_repo.latest_by_account(account_ids)
362
+ sender = self._sender
363
+ if sender is None:
364
+ raise RuntimeError("LimitWarmupService requires a sender")
365
+ send_tasks: dict[asyncio.Task[LimitWarmupSendOutcome], AccountLimitWarmup] = {}
366
+ send_semaphore = asyncio.Semaphore(_MAX_CONCURRENT_WARMUP_SENDS)
367
+ stagger_source = accounts if stagger_accounts is None else stagger_accounts
368
+ staggered_accounts = [
369
+ account
370
+ for account in stagger_source
371
+ if _account_is_safe_candidate(account) and account.limit_warmup_enabled
372
+ ]
373
+ now = utcnow()
374
+
375
+ for account in accounts:
376
+ if not _account_is_safe_candidate(account):
377
+ continue
378
+ if not account.limit_warmup_enabled:
379
+ continue
380
+ latest_attempt = latest_attempts.get(account.id)
381
+
382
+ windows_to_evaluate = list(selected_windows)
383
+ if settings.limit_warmup_staggered_idle_enabled and "primary" not in windows_to_evaluate:
384
+ windows_to_evaluate.append("primary")
385
+ for window in windows_to_evaluate:
386
+ candidate = None
387
+ if window in selected_windows:
388
+ candidate = _build_candidate(
389
+ account=account,
390
+ window=window,
391
+ before_primary=before_primary,
392
+ before_secondary=before_secondary,
393
+ after_primary=after_primary,
394
+ after_secondary=after_secondary,
395
+ min_available_percent=settings.limit_warmup_min_available_percent,
396
+ )
397
+ if candidate is None and window == "secondary":
398
+ candidate = _build_paid_to_free_transition_candidate(
399
+ account=account,
400
+ previous_plan_type=(previous_plan_types or {}).get(account.id),
401
+ after_secondary=after_secondary,
402
+ refresh_started_at=refresh_started_at,
403
+ min_available_percent=settings.limit_warmup_min_available_percent,
404
+ )
405
+ if candidate is None and window == "secondary":
406
+ candidate = _build_initial_free_quota_candidate(
407
+ account=account,
408
+ previous_plan_type=(previous_plan_types or {}).get(account.id),
409
+ latest_attempt=latest_attempt,
410
+ after_secondary=after_secondary,
411
+ refresh_started_at=refresh_started_at,
412
+ )
413
+ if (
414
+ candidate is None
415
+ and _account_is_safe_candidate(account)
416
+ and settings.limit_warmup_staggered_idle_enabled
417
+ and window == "primary"
418
+ ):
419
+ candidate = _build_staggered_idle_candidate(
420
+ account=account,
421
+ accounts=staggered_accounts,
422
+ now=now,
423
+ after_primary=after_primary,
424
+ refresh_started_at=refresh_started_at,
425
+ usage_refresh_interval_seconds=usage_refresh_interval_seconds,
426
+ idle_threshold_percent=settings.limit_warmup_idle_threshold_percent,
427
+ )
428
+ if candidate is None:
429
+ continue
430
+ if candidate.window == _IDLE_PRIMARY_WINDOW and _in_cooldown(
431
+ latest_attempt,
432
+ cooldown_seconds=settings.limit_warmup_cooldown_seconds,
433
+ ):
434
+ continue
435
+
436
+ model = self._resolve_model(settings.limit_warmup_model, account)
437
+ if model is None:
438
+ skipped = await self._warmup_repo.try_create_attempt(
439
+ account_id=account.id,
440
+ window=candidate.window,
441
+ reset_at=candidate.reset_at,
442
+ model="auto",
443
+ attempted_at=utcnow(),
444
+ reset_at_tolerance_seconds=_attempt_reset_at_tolerance(candidate),
445
+ require_no_prior_attempt=candidate.require_no_prior_attempt,
446
+ )
447
+ if skipped is not None:
448
+ completed = await self._warmup_repo.complete_attempt(
449
+ skipped.id,
450
+ status="skipped",
451
+ completed_at=utcnow(),
452
+ error_code="model_unavailable",
453
+ error_message="No eligible priced text model was available for warm-up",
454
+ )
455
+ latest_attempt = completed or skipped
456
+ latest_attempts[account.id] = latest_attempt
457
+ elif candidate.require_no_prior_attempt:
458
+ break
459
+ continue
460
+
461
+ attempt = await self._warmup_repo.try_create_attempt(
462
+ account_id=account.id,
463
+ window=candidate.window,
464
+ reset_at=candidate.reset_at,
465
+ model=model,
466
+ attempted_at=utcnow(),
467
+ reset_at_tolerance_seconds=_attempt_reset_at_tolerance(candidate),
468
+ require_no_prior_attempt=candidate.require_no_prior_attempt,
469
+ )
470
+ if attempt is None:
471
+ if candidate.require_no_prior_attempt:
472
+ break
473
+ continue
474
+ latest_attempt = attempt
475
+ latest_attempts[account.id] = attempt
476
+
477
+ send_task = asyncio.create_task(
478
+ self._send_warmup(
479
+ attempt,
480
+ account=account,
481
+ model=model,
482
+ prompt=settings.limit_warmup_prompt,
483
+ sender=sender,
484
+ semaphore=send_semaphore,
485
+ ),
486
+ name=f"limit-warmup:{attempt.id}",
487
+ )
488
+ send_tasks[send_task] = attempt
489
+
490
+ pending_send_tasks = set(send_tasks)
491
+ try:
492
+ while pending_send_tasks:
493
+ completed_send_tasks, pending_send_tasks = await asyncio.wait(
494
+ pending_send_tasks,
495
+ return_when=asyncio.FIRST_COMPLETED,
496
+ )
497
+ completion_error: BaseException | None = None
498
+ for send_task in completed_send_tasks:
499
+ outcome = await send_task
500
+ try:
501
+ completed = await self._complete_warmup(outcome)
502
+ except Exception as exc:
503
+ completion_error = completion_error or exc
504
+ await self._mark_aborted_warmup(
505
+ outcome.attempt,
506
+ error_code="warmup_completion_failed",
507
+ error_message="Limit warm-up completion failed",
508
+ )
509
+ continue
510
+ latest_attempts[outcome.account.id] = completed or outcome.attempt
511
+ if completion_error is not None:
512
+ raise completion_error
513
+ finally:
514
+ if pending_send_tasks:
515
+ for send_task in pending_send_tasks:
516
+ send_task.cancel()
517
+ drained_results = await asyncio.gather(*pending_send_tasks, return_exceptions=True)
518
+ for send_task, drained_result in zip(pending_send_tasks, drained_results, strict=True):
519
+ if isinstance(drained_result, LimitWarmupSendOutcome):
520
+ try:
521
+ completed = await self._complete_warmup(drained_result)
522
+ except Exception:
523
+ await self._mark_aborted_warmup(
524
+ drained_result.attempt,
525
+ error_code="warmup_completion_failed",
526
+ error_message="Limit warm-up completion failed",
527
+ )
528
+ continue
529
+ latest_attempts[drained_result.account.id] = completed or drained_result.attempt
530
+ continue
531
+ await self._mark_aborted_warmup(
532
+ send_tasks[send_task],
533
+ error_code=(
534
+ "warmup_cancelled"
535
+ if isinstance(drained_result, asyncio.CancelledError)
536
+ else "warmup_send_failed"
537
+ ),
538
+ error_message=(
539
+ "Limit warm-up cancelled after another warm-up completion failed"
540
+ if isinstance(drained_result, asyncio.CancelledError)
541
+ else (_truncate(str(drained_result)) or "Limit warm-up send failed")
542
+ ),
543
+ )
544
+
545
+ def _resolve_model(self, configured_model: str, account: Account) -> str | None:
546
+ normalized = configured_model.strip()
547
+ if normalized and normalized.lower() != "auto":
548
+ return normalized
549
+
550
+ candidates: list[tuple[float, str]] = []
551
+ for model in get_model_registry().get_models_with_fallback().values():
552
+ if not model.supported_in_api:
553
+ continue
554
+ if model.input_modalities and "text" not in {modality.lower() for modality in model.input_modalities}:
555
+ continue
556
+ if model.available_in_plans and not account_plan_matches_allowed(
557
+ account.plan_type, model.available_in_plans
558
+ ):
559
+ continue
560
+ resolved_price = get_pricing_for_model(model.slug)
561
+ if resolved_price is None:
562
+ continue
563
+ _, price = resolved_price
564
+ candidates.append((price.input_per_1m + price.output_per_1m, model.slug))
565
+ if not candidates:
566
+ return None
567
+ return min(candidates, key=lambda item: (item[0], item[1]))[1]
568
+
569
+ async def _send_warmup(
570
+ self,
571
+ attempt: AccountLimitWarmup,
572
+ *,
573
+ account: Account,
574
+ model: str,
575
+ prompt: str,
576
+ sender: LimitWarmupSender,
577
+ semaphore: asyncio.Semaphore,
578
+ ) -> LimitWarmupSendOutcome:
579
+ try:
580
+ async with semaphore:
581
+ result = await sender.send(account, model=model, prompt=prompt)
582
+ except Exception as exc:
583
+ logger.warning(
584
+ "Limit warm-up send failed account_id=%s window=%s", account.id, attempt.window, exc_info=True
585
+ )
586
+ return LimitWarmupSendOutcome(
587
+ attempt=attempt,
588
+ account=account,
589
+ model=model,
590
+ result=None,
591
+ error_message=str(exc),
592
+ )
593
+
594
+ return LimitWarmupSendOutcome(attempt=attempt, account=account, model=model, result=result)
595
+
596
+ async def _complete_warmup(self, outcome: LimitWarmupSendOutcome) -> AccountLimitWarmup | None:
597
+ if outcome.result is None:
598
+ return await self._warmup_repo.complete_attempt(
599
+ outcome.attempt.id,
600
+ status="failed",
601
+ completed_at=utcnow(),
602
+ error_code="warmup_send_failed",
603
+ error_message=_truncate(outcome.error_message),
604
+ )
605
+
606
+ result = outcome.result
607
+ await self._record_request_log(
608
+ account=outcome.account,
609
+ model=outcome.model,
610
+ result=result,
611
+ )
612
+ status = "succeeded" if result.success else "failed"
613
+ error_code = result.error_code
614
+ if error_code in _QUOTA_ERROR_CODES:
615
+ error_code = "quota_still_exhausted"
616
+ return await self._warmup_repo.complete_attempt(
617
+ outcome.attempt.id,
618
+ status=status,
619
+ completed_at=utcnow(),
620
+ error_code=error_code,
621
+ error_message=_truncate(result.error_message),
622
+ )
623
+
624
+ async def _mark_aborted_warmup(
625
+ self,
626
+ attempt: AccountLimitWarmup,
627
+ *,
628
+ error_code: str,
629
+ error_message: str,
630
+ ) -> None:
631
+ try:
632
+ await self._warmup_repo.complete_attempt(
633
+ attempt.id,
634
+ status="failed",
635
+ completed_at=utcnow(),
636
+ error_code=error_code,
637
+ error_message=error_message,
638
+ )
639
+ except Exception:
640
+ logger.warning(
641
+ "Failed to mark aborted limit warm-up attempt_id=%s error_code=%s",
642
+ attempt.id,
643
+ error_code,
644
+ exc_info=True,
645
+ )
646
+
647
+ async def _record_request_log(
648
+ self,
649
+ *,
650
+ account: Account,
651
+ model: str,
652
+ result: LimitWarmupSendResult,
653
+ ) -> None:
654
+ usage = result.usage
655
+ input_tokens = usage.input_tokens if usage is not None else None
656
+ output_tokens = usage.output_tokens if usage is not None else None
657
+ cached_input_tokens = (
658
+ usage.input_tokens_details.cached_tokens
659
+ if usage is not None and usage.input_tokens_details is not None
660
+ else None
661
+ )
662
+ reasoning_tokens = (
663
+ usage.output_tokens_details.reasoning_tokens
664
+ if usage is not None and usage.output_tokens_details is not None
665
+ else None
666
+ )
667
+ await self._request_logs_repo.add_log(
668
+ account_id=account.id,
669
+ request_id=result.request_id,
670
+ model=model,
671
+ input_tokens=input_tokens,
672
+ output_tokens=output_tokens,
673
+ cached_input_tokens=cached_input_tokens,
674
+ reasoning_tokens=reasoning_tokens,
675
+ latency_ms=result.latency_ms,
676
+ status="success" if result.success else "error",
677
+ error_code=result.error_code,
678
+ error_message=_truncate(result.error_message),
679
+ transport="http",
680
+ plan_type=account.plan_type,
681
+ source=LIMIT_WARMUP_SOURCE,
682
+ request_kind=LIMIT_WARMUP_REQUEST_KIND,
683
+ upstream_proxy_route_mode=result.upstream_proxy_route_mode,
684
+ upstream_proxy_pool_id=result.upstream_proxy_pool_id,
685
+ upstream_proxy_endpoint_id=result.upstream_proxy_endpoint_id,
686
+ upstream_proxy_fallback_used=result.upstream_proxy_fallback_used,
687
+ upstream_proxy_fail_closed_reason=result.upstream_proxy_fail_closed_reason,
688
+ )
689
+
690
+
691
+ @dataclass(frozen=True, slots=True)
692
+ class _WarmupCandidate:
693
+ reset_at: int
694
+ window: str
695
+ require_no_prior_attempt: bool = False
696
+
697
+
698
+ def _selected_windows(value: str) -> tuple[str, ...]:
699
+ normalized = value.strip().lower()
700
+ if normalized == "both":
701
+ return ("primary", "secondary")
702
+ if normalized in {"primary", "secondary"}:
703
+ return (normalized,)
704
+ return ()
705
+
706
+
707
+ def _account_is_safe_candidate(account: Account) -> bool:
708
+ return account.status == AccountStatus.ACTIVE
709
+
710
+
711
+ def _in_cooldown(attempt: AccountLimitWarmup | None, *, cooldown_seconds: int) -> bool:
712
+ if attempt is None:
713
+ return False
714
+ return utcnow() - attempt.attempted_at < timedelta(seconds=cooldown_seconds)
715
+
716
+
717
+ def _build_candidate(
718
+ *,
719
+ account: Account,
720
+ window: str,
721
+ before_primary: dict[str, UsageHistory],
722
+ before_secondary: dict[str, UsageHistory],
723
+ after_primary: dict[str, UsageHistory],
724
+ after_secondary: dict[str, UsageHistory],
725
+ min_available_percent: float,
726
+ ) -> _WarmupCandidate | None:
727
+ before = _effective_usage_entry(
728
+ account.id,
729
+ window=window,
730
+ primary=before_primary,
731
+ secondary=before_secondary,
732
+ )
733
+ after = _effective_usage_entry(
734
+ account.id,
735
+ window=window,
736
+ primary=after_primary,
737
+ secondary=after_secondary,
738
+ )
739
+ if before is None or after is None:
740
+ return None
741
+ available_percent = 100.0 - after.used_percent
742
+ if min_available_percent < 100.0 and available_percent < min_available_percent:
743
+ return None
744
+ if not usage_reset_confirmed(before=before, after=after):
745
+ return None
746
+ assert after.reset_at is not None
747
+ candidate_window = "monthly" if after.window == "monthly" else window
748
+ return _WarmupCandidate(reset_at=after.reset_at, window=candidate_window)
749
+
750
+
751
+ def usage_reset_confirmed(*, before: UsageHistory | None, after: UsageHistory | None) -> bool:
752
+ """Return whether consecutive samples prove a real, newly available quota window."""
753
+ if before is None or after is None:
754
+ return False
755
+ if before.reset_at is None or after.reset_at is None:
756
+ return False
757
+ if (before.window or "primary") != (after.window or "primary"):
758
+ return False
759
+ if after.used_percent >= 100.0:
760
+ return False
761
+ reset_at_jump = after.reset_at - before.reset_at
762
+ if reset_at_jump < _RESET_CONFIRMED_MIN_JUMP_SECONDS:
763
+ return False
764
+ before_observed_at = naive_utc_to_epoch(before.recorded_at)
765
+ observed_at = naive_utc_to_epoch(after.recorded_at)
766
+ window_started_at = after.reset_at - _rolling_window_seconds(after)
767
+ quota_recovered = after.used_percent < before.used_percent
768
+ crossed_previous_reset = before_observed_at <= before.reset_at <= observed_at < after.reset_at
769
+ reanchored_between_samples = (
770
+ quota_recovered and before_observed_at <= window_started_at <= observed_at < after.reset_at
771
+ )
772
+ # Scheduled resets cross the previous boundary. Early resets can happen
773
+ # when upstream restores quota and reanchors a complete window from the
774
+ # sampling interval. A reset_at update outside that interval is not a reset.
775
+ if not crossed_previous_reset and not reanchored_between_samples:
776
+ return False
777
+ return True
778
+
779
+
780
+ def _build_paid_to_free_transition_candidate(
781
+ *,
782
+ account: Account,
783
+ previous_plan_type: str | None,
784
+ after_secondary: dict[str, UsageHistory],
785
+ refresh_started_at: datetime | None,
786
+ min_available_percent: float,
787
+ ) -> _WarmupCandidate | None:
788
+ normalized_previous_plan = normalize_account_plan_type(previous_plan_type)
789
+ if normalized_previous_plan is None or normalized_previous_plan == "free":
790
+ return None
791
+ if normalize_account_plan_type(account.plan_type) != "free":
792
+ return None
793
+ if refresh_started_at is None:
794
+ return None
795
+ after = after_secondary.get(account.id)
796
+ if after is None or after.window != "monthly" or after.reset_at is None:
797
+ return None
798
+ if after.recorded_at < refresh_started_at:
799
+ return None
800
+ if after.used_percent >= 100.0:
801
+ return None
802
+ available_percent = 100.0 - after.used_percent
803
+ if min_available_percent < 100.0 and available_percent < min_available_percent:
804
+ return None
805
+ return _WarmupCandidate(reset_at=after.reset_at, window="monthly")
806
+
807
+
808
+ def _build_initial_free_quota_candidate(
809
+ *,
810
+ account: Account,
811
+ previous_plan_type: str | None,
812
+ latest_attempt: AccountLimitWarmup | None,
813
+ after_secondary: dict[str, UsageHistory],
814
+ refresh_started_at: datetime | None,
815
+ ) -> _WarmupCandidate | None:
816
+ if latest_attempt is not None:
817
+ return None
818
+ if normalize_account_plan_type(account.plan_type) != "free":
819
+ return None
820
+ if normalize_account_plan_type(previous_plan_type) != "free":
821
+ return None
822
+ if refresh_started_at is None:
823
+ return None
824
+ after = after_secondary.get(account.id)
825
+ if after is None or after.window != "monthly" or after.reset_at is None:
826
+ return None
827
+ if after.recorded_at < refresh_started_at:
828
+ return None
829
+ if after.used_percent != 0.0:
830
+ return None
831
+ return _WarmupCandidate(
832
+ reset_at=after.reset_at,
833
+ window="monthly",
834
+ require_no_prior_attempt=True,
835
+ )
836
+
837
+
838
+ def _build_staggered_idle_candidate(
839
+ *,
840
+ account: Account,
841
+ accounts: list[Account],
842
+ now: datetime,
843
+ after_primary: dict[str, UsageHistory],
844
+ refresh_started_at: datetime | None,
845
+ usage_refresh_interval_seconds: int,
846
+ idle_threshold_percent: float = 1.0,
847
+ ) -> _WarmupCandidate | None:
848
+ after = after_primary.get(account.id)
849
+ if after is None:
850
+ return None
851
+ if after.reset_at is None:
852
+ return None
853
+ if after.window_minutes is not None and after.window_minutes > _SHORT_WINDOW_MAX_MINUTES:
854
+ # Any long window (weekly, monthly, or a nonstandard duration over
855
+ # 24h) has no short phase to pre-start.
856
+ return None
857
+ if after.used_percent > idle_threshold_percent:
858
+ return None
859
+
860
+ window_seconds = _rolling_window_seconds(after)
861
+ due = _staggered_idle_due(
862
+ account.id,
863
+ [candidate.id for candidate in accounts],
864
+ now=now,
865
+ reset_at=after.reset_at,
866
+ interval_started_at=refresh_started_at,
867
+ usage_refresh_interval_seconds=usage_refresh_interval_seconds,
868
+ window_seconds=window_seconds,
869
+ )
870
+ if due is None:
871
+ return None
872
+ if not _usage_entry_refreshed_for_cycle(
873
+ after,
874
+ refresh_started_at=refresh_started_at,
875
+ cycle_end=due.cycle_end,
876
+ window_seconds=window_seconds,
877
+ ):
878
+ return None
879
+ return _WarmupCandidate(reset_at=after.reset_at, window=_IDLE_PRIMARY_WINDOW)
880
+
881
+
882
+ @dataclass(frozen=True, slots=True)
883
+ class _StaggeredIdleDue:
884
+ cycle_end: int
885
+ slot_offset_seconds: int
886
+
887
+
888
+ def _rolling_window_seconds(entry: UsageHistory) -> int:
889
+ # Derive the rolling cycle from the observed primary window duration so
890
+ # the stagger tracks whatever short window upstream reports; 300 minutes
891
+ # remains the fallback when duration metadata is missing.
892
+ if entry.window_minutes is not None and entry.window_minutes > 0:
893
+ return int(entry.window_minutes) * 60
894
+ return _ROLLING_WINDOW_SECONDS
895
+
896
+
897
+ def _staggered_idle_due(
898
+ account_id: str,
899
+ account_ids: list[str],
900
+ *,
901
+ now: datetime,
902
+ reset_at: int,
903
+ interval_started_at: datetime | None = None,
904
+ usage_refresh_interval_seconds: int = _STAGGER_SLOT_GRACE_SECONDS,
905
+ window_seconds: int = _ROLLING_WINDOW_SECONDS,
906
+ ) -> _StaggeredIdleDue | None:
907
+ if not account_ids:
908
+ return None
909
+ ordered_account_ids = sorted(set(account_ids))
910
+ try:
911
+ account_index = ordered_account_ids.index(account_id)
912
+ except ValueError:
913
+ return None
914
+
915
+ now_epoch = naive_utc_to_epoch(now)
916
+ cycle_start = reset_at - window_seconds
917
+ if now_epoch < cycle_start or now_epoch >= reset_at:
918
+ return None
919
+ elapsed = now_epoch - cycle_start
920
+ slot_offset = int(account_index * window_seconds / len(ordered_account_ids))
921
+ grace_seconds = max(_STAGGER_SLOT_GRACE_SECONDS, usage_refresh_interval_seconds)
922
+ interval_start_epoch = naive_utc_to_epoch(interval_started_at) if interval_started_at is not None else now_epoch
923
+ interval_start_epoch = min(interval_start_epoch, now_epoch)
924
+ interval_start_elapsed = max(0, interval_start_epoch - cycle_start)
925
+ if not slot_offset <= elapsed:
926
+ return None
927
+ if slot_offset + grace_seconds <= interval_start_elapsed:
928
+ return None
929
+ return _StaggeredIdleDue(
930
+ cycle_end=cycle_start + window_seconds,
931
+ slot_offset_seconds=slot_offset,
932
+ )
933
+
934
+
935
+ def _usage_entry_refreshed_for_cycle(
936
+ entry: UsageHistory,
937
+ *,
938
+ refresh_started_at: datetime | None,
939
+ cycle_end: int | None,
940
+ window_seconds: int = _ROLLING_WINDOW_SECONDS,
941
+ ) -> bool:
942
+ if entry.recorded_at is None:
943
+ return False
944
+ if cycle_end is not None:
945
+ cycle_start = cycle_end - window_seconds
946
+ if entry.recorded_at < datetime.fromtimestamp(cycle_start, tz=timezone.utc).replace(tzinfo=None):
947
+ return False
948
+ if entry.reset_at is not None and entry.reset_at <= cycle_start:
949
+ return False
950
+ if refresh_started_at is None:
951
+ return True
952
+ return entry.recorded_at >= refresh_started_at
953
+
954
+
955
+ def _effective_usage_entry(
956
+ account_id: str,
957
+ *,
958
+ window: str,
959
+ primary: dict[str, UsageHistory],
960
+ secondary: dict[str, UsageHistory],
961
+ ) -> UsageHistory | None:
962
+ if window == "primary":
963
+ primary_entry = primary.get(account_id)
964
+ if primary_entry is None or usage_core.is_weekly_window_minutes(primary_entry.window_minutes):
965
+ return None
966
+ return primary_entry
967
+
968
+ primary_entry = primary.get(account_id)
969
+ secondary_entry = secondary.get(account_id)
970
+ if primary_entry is not None and usage_core.is_weekly_window_minutes(primary_entry.window_minutes):
971
+ if secondary_entry is None:
972
+ return primary_entry
973
+ if usage_core.should_use_weekly_primary(
974
+ usage_history_to_window_row(primary_entry),
975
+ usage_history_to_window_row(secondary_entry),
976
+ ):
977
+ return primary_entry
978
+ return secondary_entry
979
+
980
+
981
+ def _event_error(*errors: OpenAIError | None) -> OpenAIError:
982
+ for error in errors:
983
+ if error is not None:
984
+ return error
985
+ return OpenAIError(message=None, code=None)
986
+
987
+
988
+ def _elapsed_ms(started: float) -> int:
989
+ return max(0, int((time.monotonic() - started) * 1000))
990
+
991
+
992
+ def _truncate(value: str | None, limit: int = 1000) -> str | None:
993
+ if value is None:
994
+ return None
995
+ if len(value) <= limit:
996
+ return value
997
+ return value[: limit - 1] + "..."
998
+
999
+
1000
+ def _attempt_reset_at_tolerance(candidate: _WarmupCandidate) -> int:
1001
+ return _RESET_AT_JITTER_TOLERANCE_SECONDS