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,809 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ import logging
6
+ from datetime import timedelta
7
+ from typing import cast
8
+ from uuid import uuid4
9
+
10
+ import aiohttp
11
+ from pydantic import ValidationError
12
+ from sqlalchemy.exc import OperationalError
13
+
14
+ from app.core.auth import (
15
+ DEFAULT_EMAIL,
16
+ DEFAULT_PLAN,
17
+ claims_from_auth,
18
+ generate_unique_account_id,
19
+ parse_auth_json,
20
+ token_expiry_epoch_ms,
21
+ )
22
+ from app.core.auth.api_key_cache import get_api_key_cache
23
+ from app.core.auth.refresh import RefreshError
24
+ from app.core.cache.invalidation import NAMESPACE_API_KEY, get_cache_invalidation_poller
25
+ from app.core.clients.http import lease_http_session
26
+ from app.core.clients.usage import (
27
+ ConsumeRateLimitResetCreditResponse,
28
+ UsageFetchError,
29
+ consume_rate_limit_reset_credit,
30
+ fetch_usage,
31
+ )
32
+ from app.core.config.settings import get_settings
33
+ from app.core.crypto import TokenEncryptor
34
+ from app.core.openai.host_models import resolve_default_host_model
35
+ from app.core.plan_types import coerce_account_plan_type
36
+ from app.core.upstream_proxy import ResolvedUpstreamRoute, UpstreamProxyRouteError, resolve_upstream_route
37
+ from app.core.upstream_proxy.cache import get_upstream_route_cache
38
+ from app.core.upstream_proxy.resolver import _is_missing_upstream_proxy_schema
39
+ from app.core.usage.models import UsagePayload
40
+ from app.core.utils.time import naive_utc_to_epoch, to_utc_naive, utcnow
41
+ from app.db.models import Account, AccountStatus, DashboardSettings
42
+ from app.db.session import get_background_session
43
+ from app.modules.accounts.auth_manager import AuthManager
44
+ from app.modules.accounts.deletion import request_account_deletion_run
45
+ from app.modules.accounts.mappers import build_account_summaries, build_account_usage_trends
46
+ from app.modules.accounts.repository import AccountsRepository
47
+ from app.modules.accounts.schemas import (
48
+ AccountAdditionalQuota,
49
+ AccountAdditionalWindow,
50
+ AccountAuthExportResponse,
51
+ AccountAuthExportTokens,
52
+ AccountImportResponse,
53
+ AccountOpenCodeAuthExportAccount,
54
+ AccountProbeResponse,
55
+ AccountRequestUsage,
56
+ AccountSummary,
57
+ AccountTrendsResponse,
58
+ AccountUsageResetConsumeResponse,
59
+ AccountUsageResetCredits,
60
+ AccountUsageResetCreditsResponse,
61
+ CodexAuthJson,
62
+ CodexAuthTokens,
63
+ OpenCodeAuthJson,
64
+ OpenCodeOAuthAuth,
65
+ )
66
+ from app.modules.limit_warmup.repository import LimitWarmupRepository
67
+ from app.modules.proxy.account_cache import (
68
+ clear_account_routing_unavailable,
69
+ get_account_selection_cache,
70
+ mark_account_routing_unavailable,
71
+ propagate_account_routing_change,
72
+ )
73
+ from app.modules.rate_limit_reset_credits.store import get_rate_limit_reset_credits_store
74
+ from app.modules.usage.additional_quota_keys import (
75
+ get_additional_display_label_for_quota_key,
76
+ get_additional_quota_routing_policy,
77
+ )
78
+ from app.modules.usage.repository import AdditionalUsageRepository, UsageRepository
79
+ from app.modules.usage.updater import AdditionalUsageRepositoryPort, UsageUpdater
80
+
81
+ logger = logging.getLogger(__name__)
82
+
83
+ _SPARKLINE_DAYS = 7
84
+ _DETAIL_BUCKET_SECONDS = 3600 # 1h → 168 points
85
+
86
+ PROBE_REQUEST_TIMEOUT_SECONDS = 30.0
87
+ PROBE_CONNECT_TIMEOUT_SECONDS = 10.0
88
+ # Network/upstream failure sentinel for ``probe_status_code`` — kept as ``0`` so
89
+ # the value is distinguishable from any real HTTP status the upstream might
90
+ # return.
91
+ PROBE_NETWORK_FAILURE_STATUS = 0
92
+ IMPORT_PROXY_REQUIRED_PAUSE_REASON = "upstream_proxy_required_on_import"
93
+
94
+
95
+ class InvalidAuthJsonError(Exception):
96
+ pass
97
+
98
+
99
+ class AccountNotProbableError(Exception):
100
+ """Raised when an account is in a status that disallows probing."""
101
+
102
+
103
+ class AccountStateTransitionError(Exception):
104
+ """Raised when an operator action is not valid for the account state."""
105
+
106
+
107
+ class AccountUsageResetCreditsUnavailableError(Exception):
108
+ """Raised when a dashboard account cannot read upstream reset credits."""
109
+
110
+
111
+ class AccountUsageResetConsumeUnavailableError(Exception):
112
+ """Raised when a dashboard account cannot consume upstream reset credits."""
113
+
114
+
115
+ class AccountsService:
116
+ def __init__(
117
+ self,
118
+ repo: AccountsRepository,
119
+ usage_repo: UsageRepository | None = None,
120
+ additional_usage_repo: AdditionalUsageRepository | AdditionalUsageRepositoryPort | None = None,
121
+ limit_warmup_repo: LimitWarmupRepository | None = None,
122
+ auth_manager: AuthManager | None = None,
123
+ ) -> None:
124
+ self._repo = repo
125
+ self._usage_repo = usage_repo
126
+ self._additional_usage_repo = additional_usage_repo
127
+ self._limit_warmup_repo = limit_warmup_repo
128
+ self._usage_updater = UsageUpdater(usage_repo, repo, additional_usage_repo) if usage_repo else None
129
+ self._encryptor = TokenEncryptor()
130
+ self._auth_manager = auth_manager
131
+
132
+ async def list_accounts(
133
+ self,
134
+ *,
135
+ account_ids: list[str] | None = None,
136
+ redact_identity: bool = False,
137
+ ) -> list[AccountSummary]:
138
+ accounts = (
139
+ await self._repo.list_accounts_by_ids(account_ids)
140
+ if account_ids is not None
141
+ else await self._repo.list_accounts()
142
+ )
143
+ if not accounts:
144
+ return []
145
+ visible_account_ids = [account.id for account in accounts]
146
+ account_id_set = set(visible_account_ids)
147
+ usage_account_ids = visible_account_ids if account_ids is not None else None
148
+ primary_usage = (
149
+ await self._usage_repo.latest_by_account(window="primary", account_ids=usage_account_ids)
150
+ if self._usage_repo
151
+ else {}
152
+ )
153
+ secondary_usage = (
154
+ await self._usage_repo.latest_by_account(window="secondary", account_ids=usage_account_ids)
155
+ if self._usage_repo
156
+ else {}
157
+ )
158
+ monthly_usage = (
159
+ await self._usage_repo.latest_by_account(window="monthly", account_ids=usage_account_ids)
160
+ if self._usage_repo
161
+ else {}
162
+ )
163
+ request_usage_rows = await self._repo.list_request_usage_summary_by_account(visible_account_ids)
164
+ limit_warmups_by_account = (
165
+ await self._limit_warmup_repo.latest_by_account(visible_account_ids) if self._limit_warmup_repo else {}
166
+ )
167
+ request_usage_by_account = {
168
+ account_id: AccountRequestUsage(
169
+ request_count=row.request_count,
170
+ total_tokens=row.total_tokens,
171
+ cached_input_tokens=row.cached_input_tokens,
172
+ total_cost_usd=row.total_cost_usd,
173
+ )
174
+ for account_id, row in request_usage_rows.items()
175
+ }
176
+ additional_quotas_by_account: dict[str, list[AccountAdditionalQuota]] = {}
177
+ additional_usage_repo = cast(AdditionalUsageRepository | None, self._additional_usage_repo)
178
+ if additional_usage_repo:
179
+ additional_quota_routing_overrides = await self._repo.additional_quota_routing_policy_overrides()
180
+ quota_keys = await additional_usage_repo.list_quota_keys(account_ids=visible_account_ids)
181
+ for quota_key in quota_keys:
182
+ primary_entries = await additional_usage_repo.latest_by_account(
183
+ quota_key,
184
+ "primary",
185
+ account_ids=visible_account_ids,
186
+ )
187
+ secondary_entries = await additional_usage_repo.latest_by_account(
188
+ quota_key,
189
+ "secondary",
190
+ account_ids=visible_account_ids,
191
+ )
192
+ for account_id in (set(primary_entries) | set(secondary_entries)) & account_id_set:
193
+ primary_entry = primary_entries.get(account_id)
194
+ secondary_entry = secondary_entries.get(account_id)
195
+ reference_entry = primary_entry or secondary_entry
196
+ if reference_entry is None:
197
+ continue
198
+ additional_quotas_by_account.setdefault(account_id, []).append(
199
+ AccountAdditionalQuota(
200
+ quota_key=quota_key,
201
+ limit_name=reference_entry.limit_name,
202
+ metered_feature=reference_entry.metered_feature,
203
+ display_label=get_additional_display_label_for_quota_key(quota_key)
204
+ or reference_entry.limit_name,
205
+ routing_policy=get_additional_quota_routing_policy(
206
+ quota_key,
207
+ overrides=additional_quota_routing_overrides,
208
+ ),
209
+ primary_window=AccountAdditionalWindow(
210
+ used_percent=primary_entry.used_percent,
211
+ reset_at=primary_entry.reset_at,
212
+ window_minutes=primary_entry.window_minutes,
213
+ )
214
+ if primary_entry is not None
215
+ else None,
216
+ secondary_window=AccountAdditionalWindow(
217
+ used_percent=secondary_entry.used_percent,
218
+ reset_at=secondary_entry.reset_at,
219
+ window_minutes=secondary_entry.window_minutes,
220
+ )
221
+ if secondary_entry is not None
222
+ else None,
223
+ )
224
+ )
225
+ for account_quota_list in additional_quotas_by_account.values():
226
+ account_quota_list.sort(key=lambda quota: quota.display_label or quota.quota_key or quota.limit_name)
227
+
228
+ return build_account_summaries(
229
+ accounts=accounts,
230
+ primary_usage=primary_usage,
231
+ secondary_usage=secondary_usage,
232
+ monthly_usage=monthly_usage,
233
+ request_usage_by_account=request_usage_by_account,
234
+ additional_quotas_by_account=additional_quotas_by_account,
235
+ limit_warmups_by_account=limit_warmups_by_account,
236
+ encryptor=self._encryptor,
237
+ redact_identity=redact_identity,
238
+ )
239
+
240
+ async def get_account_trends(self, account_id: str) -> AccountTrendsResponse | None:
241
+ account = await self._get_visible_account(account_id)
242
+ if not account or not self._usage_repo:
243
+ return None
244
+ now = utcnow()
245
+ since = now - timedelta(days=_SPARKLINE_DAYS)
246
+ since_epoch = naive_utc_to_epoch(since)
247
+ bucket_count = (_SPARKLINE_DAYS * 24 * 3600) // _DETAIL_BUCKET_SECONDS
248
+ buckets = await self._usage_repo.trends_by_bucket(
249
+ since=since,
250
+ bucket_seconds=_DETAIL_BUCKET_SECONDS,
251
+ account_id=account_id,
252
+ )
253
+ trends = build_account_usage_trends(buckets, since_epoch, _DETAIL_BUCKET_SECONDS, bucket_count)
254
+ trend = trends.get(account_id)
255
+ return AccountTrendsResponse(
256
+ account_id=account_id,
257
+ primary=trend.primary if trend else [],
258
+ secondary=trend.secondary if trend else [],
259
+ secondary_scheduled=trend.secondary_scheduled if trend else [],
260
+ )
261
+
262
+ async def get_usage_reset_credits(self, account_id: str) -> AccountUsageResetCreditsResponse | None:
263
+ account = await self._get_visible_account(account_id)
264
+ if account is None:
265
+ return None
266
+ if account.status in (AccountStatus.PAUSED, AccountStatus.DEACTIVATED):
267
+ raise AccountUsageResetCreditsUnavailableError(
268
+ f"Account is {account.status.value} and cannot fetch usage reset credits",
269
+ )
270
+ if self._auth_manager is not None:
271
+ try:
272
+ account = await self._auth_manager.ensure_fresh(account)
273
+ except RefreshError as exc:
274
+ raise AccountUsageResetCreditsUnavailableError(
275
+ f"Account credentials could not be refreshed: {exc.message}",
276
+ ) from exc
277
+ if not account.chatgpt_account_id:
278
+ raise AccountUsageResetCreditsUnavailableError("Account is missing ChatGPT account identity")
279
+
280
+ payload = await self._fetch_usage_payload_for_reset_credits(account)
281
+ reset_credits = payload.rate_limit_reset_credits
282
+ available_count = reset_credits.available_count if reset_credits is not None else None
283
+ return AccountUsageResetCreditsResponse(
284
+ account_id=account.id,
285
+ rate_limit_reset_credits=AccountUsageResetCredits(
286
+ available_count=max(0, int(available_count or 0)),
287
+ ),
288
+ )
289
+
290
+ async def _fetch_usage_payload_for_reset_credits(self, account: Account) -> UsagePayload:
291
+ access_token = self._encryptor.decrypt(account.access_token_encrypted)
292
+ route = await self._resolve_usage_reset_credit_route(account, operation="usage_reset_credits_read")
293
+ try:
294
+ return await fetch_usage(
295
+ access_token=access_token,
296
+ account_id=account.chatgpt_account_id,
297
+ route=route,
298
+ allow_direct_egress=route is None,
299
+ )
300
+ except UsageFetchError as exc:
301
+ if exc.status_code != 401 or self._auth_manager is None:
302
+ raise
303
+ try:
304
+ account = await self._auth_manager.ensure_fresh(account, force=True)
305
+ except RefreshError as refresh_exc:
306
+ raise AccountUsageResetCreditsUnavailableError(
307
+ f"Account credentials could not be refreshed: {refresh_exc.message}",
308
+ ) from refresh_exc
309
+ if not account.chatgpt_account_id:
310
+ raise AccountUsageResetCreditsUnavailableError("Account is missing ChatGPT account identity") from exc
311
+ access_token = self._encryptor.decrypt(account.access_token_encrypted)
312
+ retry_route = await self._resolve_usage_reset_credit_route(account, operation="usage_reset_credits_read")
313
+ return await fetch_usage(
314
+ access_token=access_token,
315
+ account_id=account.chatgpt_account_id,
316
+ route=retry_route,
317
+ allow_direct_egress=retry_route is None,
318
+ )
319
+
320
+ async def consume_usage_reset_credit(
321
+ self,
322
+ account_id: str,
323
+ *,
324
+ redeem_request_id: str | None = None,
325
+ ) -> AccountUsageResetConsumeResponse | None:
326
+ account = await self._get_visible_account(account_id)
327
+ if account is None:
328
+ return None
329
+ if account.status in (AccountStatus.PAUSED, AccountStatus.DEACTIVATED):
330
+ raise AccountUsageResetConsumeUnavailableError(
331
+ f"Account is {account.status.value} and cannot consume usage reset credits",
332
+ )
333
+ if self._auth_manager is not None:
334
+ try:
335
+ account = await self._auth_manager.ensure_fresh(account)
336
+ except RefreshError as exc:
337
+ raise AccountUsageResetConsumeUnavailableError(
338
+ f"Account credentials could not be refreshed: {exc.message}",
339
+ ) from exc
340
+ if not account.chatgpt_account_id:
341
+ raise AccountUsageResetConsumeUnavailableError("Account is missing ChatGPT account identity")
342
+
343
+ primary_before, secondary_before = await self._latest_usage_percents(account_id)
344
+ status_before = account.status.value
345
+ upstream_response, account = await self._consume_usage_reset_credit(
346
+ account, redeem_request_id=redeem_request_id
347
+ )
348
+ if upstream_response.code in ("reset", "already_redeemed", "no_credit", "nothing_to_reset"):
349
+ await get_rate_limit_reset_credits_store().invalidate(account_id)
350
+
351
+ usage_written = False
352
+ if upstream_response.code in ("reset", "already_redeemed") and self._usage_repo and self._usage_updater:
353
+ usage_written = await self._usage_updater.force_refresh(account)
354
+ get_account_selection_cache().invalidate()
355
+
356
+ refreshed = await self._repo.get_by_id(account_id) or account
357
+ primary_after, secondary_after = await self._latest_usage_percents(account_id)
358
+
359
+ return AccountUsageResetConsumeResponse(
360
+ status="reset",
361
+ account_id=account_id,
362
+ code=upstream_response.code,
363
+ windows_reset=upstream_response.windows_reset,
364
+ usage_written=usage_written,
365
+ primary_used_percent_before=primary_before,
366
+ primary_used_percent_after=primary_after,
367
+ secondary_used_percent_before=secondary_before,
368
+ secondary_used_percent_after=secondary_after,
369
+ account_status_before=status_before,
370
+ account_status_after=refreshed.status.value,
371
+ )
372
+
373
+ async def _consume_usage_reset_credit(
374
+ self,
375
+ account: Account,
376
+ *,
377
+ redeem_request_id: str | None = None,
378
+ ) -> tuple[ConsumeRateLimitResetCreditResponse, Account]:
379
+ chatgpt_account_id = account.chatgpt_account_id
380
+ if not chatgpt_account_id:
381
+ raise AccountUsageResetConsumeUnavailableError("Account is missing ChatGPT account identity")
382
+ effective_redeem_request_id = redeem_request_id or str(uuid4())
383
+ access_token = self._encryptor.decrypt(account.access_token_encrypted)
384
+ route = await self._resolve_usage_reset_credit_route(account, operation="usage_reset_credits_consume")
385
+ try:
386
+ response = await consume_rate_limit_reset_credit(
387
+ access_token=access_token,
388
+ account_id=chatgpt_account_id,
389
+ redeem_request_id=effective_redeem_request_id,
390
+ route=route,
391
+ allow_direct_egress=route is None,
392
+ )
393
+ return response, account
394
+ except UsageFetchError as exc:
395
+ if exc.status_code != 401 or self._auth_manager is None:
396
+ raise
397
+ try:
398
+ account = await self._auth_manager.ensure_fresh(account, force=True)
399
+ except RefreshError as refresh_exc:
400
+ raise AccountUsageResetConsumeUnavailableError(
401
+ f"Account credentials could not be refreshed: {refresh_exc.message}",
402
+ ) from refresh_exc
403
+ if not account.chatgpt_account_id:
404
+ raise AccountUsageResetConsumeUnavailableError("Account is missing ChatGPT account identity") from exc
405
+ access_token = self._encryptor.decrypt(account.access_token_encrypted)
406
+ retry_route = await self._resolve_usage_reset_credit_route(account, operation="usage_reset_credits_consume")
407
+ response = await consume_rate_limit_reset_credit(
408
+ access_token=access_token,
409
+ account_id=account.chatgpt_account_id,
410
+ redeem_request_id=effective_redeem_request_id,
411
+ route=retry_route,
412
+ allow_direct_egress=retry_route is None,
413
+ )
414
+ return response, account
415
+
416
+ async def _resolve_usage_reset_credit_route(
417
+ self,
418
+ account: Account,
419
+ *,
420
+ operation: str,
421
+ ) -> ResolvedUpstreamRoute | None:
422
+ async with get_background_session() as session:
423
+ return await resolve_upstream_route(
424
+ session,
425
+ account_id=account.id,
426
+ operation=operation,
427
+ scope="account",
428
+ encryptor=self._encryptor,
429
+ )
430
+
431
+ async def _get_visible_account(self, account_id: str) -> Account | None:
432
+ """Account fetch for ID-based operator routes; marked-for-deletion
433
+ rows are gone from the operator's perspective (the synchronous delete
434
+ returned 404 on every one of these routes once the row was removed)
435
+ and MUST NOT keep serving reads, mutations, or decrypted tokens
436
+ during the background drain window."""
437
+ account = await self._repo.get_by_id(account_id)
438
+ if account is None or account.delete_requested_at is not None:
439
+ return None
440
+ return account
441
+
442
+ async def export_auth(self, account_id: str) -> AccountAuthExportResponse | None:
443
+ account = await self._get_visible_account(account_id)
444
+ if account is None:
445
+ return None
446
+
447
+ access_token = self._encryptor.decrypt(account.access_token_encrypted)
448
+ refresh_token = self._encryptor.decrypt(account.refresh_token_encrypted)
449
+ id_token = self._encryptor.decrypt(account.id_token_encrypted)
450
+ expires = token_expiry_epoch_ms(access_token) or 0
451
+
452
+ tokens = AccountAuthExportTokens(
453
+ id_token=id_token,
454
+ access_token=access_token,
455
+ refresh_token=refresh_token,
456
+ expires_at_ms=expires,
457
+ )
458
+
459
+ codex_auth_json = CodexAuthJson(
460
+ auth_mode="chatgpt",
461
+ openai_api_key=None,
462
+ tokens=CodexAuthTokens(
463
+ id_token=id_token,
464
+ access_token=access_token,
465
+ refresh_token=refresh_token,
466
+ account_id=account.chatgpt_account_id,
467
+ ),
468
+ last_refresh=account.last_refresh.strftime("%Y-%m-%dT%H:%M:%S.%f") + "Z",
469
+ )
470
+
471
+ opencode_auth_json = OpenCodeAuthJson(
472
+ openai=OpenCodeOAuthAuth(
473
+ refresh=refresh_token,
474
+ access=access_token,
475
+ expires=expires,
476
+ account_id=account.chatgpt_account_id,
477
+ ),
478
+ )
479
+
480
+ return AccountAuthExportResponse(
481
+ filename=_opencode_auth_export_filename(account),
482
+ account=AccountOpenCodeAuthExportAccount(
483
+ account_id=account.id,
484
+ chatgpt_account_id=account.chatgpt_account_id,
485
+ email=account.email,
486
+ ),
487
+ tokens=tokens,
488
+ codex_auth_json=codex_auth_json,
489
+ opencode_auth_json=opencode_auth_json,
490
+ )
491
+
492
+ async def import_account(self, raw: bytes) -> AccountImportResponse:
493
+ try:
494
+ auth = parse_auth_json(raw)
495
+ except (json.JSONDecodeError, ValidationError, UnicodeDecodeError, TypeError) as exc:
496
+ raise InvalidAuthJsonError("Invalid auth.json payload") from exc
497
+ claims = claims_from_auth(auth)
498
+
499
+ email = claims.email or DEFAULT_EMAIL
500
+ raw_account_id = claims.account_id
501
+ account_id = generate_unique_account_id(raw_account_id, email, claims.workspace_id, claims.workspace_label)
502
+ plan_type = coerce_account_plan_type(claims.plan_type, DEFAULT_PLAN)
503
+ last_refresh = to_utc_naive(auth.last_refresh_at) if auth.last_refresh_at else utcnow()
504
+
505
+ account = Account(
506
+ id=account_id,
507
+ chatgpt_account_id=raw_account_id,
508
+ email=email,
509
+ workspace_id=claims.workspace_id,
510
+ workspace_label=claims.workspace_label,
511
+ seat_type=claims.seat_type,
512
+ plan_type=plan_type,
513
+ access_token_encrypted=self._encryptor.encrypt(auth.tokens.access_token),
514
+ refresh_token_encrypted=self._encryptor.encrypt(auth.tokens.refresh_token),
515
+ id_token_encrypted=self._encryptor.encrypt(auth.tokens.id_token),
516
+ last_refresh=last_refresh,
517
+ status=AccountStatus.ACTIVE,
518
+ deactivation_reason=None,
519
+ )
520
+
521
+ saved = await self._repo.upsert_account_slot(account)
522
+ import_usage_refresh_allowed = await self._import_usage_refresh_allowed(saved)
523
+ if not import_usage_refresh_allowed:
524
+ await self._repo.update_status(
525
+ saved.id,
526
+ AccountStatus.PAUSED,
527
+ IMPORT_PROXY_REQUIRED_PAUSE_REASON,
528
+ None,
529
+ blocked_at=None,
530
+ )
531
+ mark_account_routing_unavailable(saved.id)
532
+ saved = await self._repo.get_by_id(saved.id) or saved
533
+ if import_usage_refresh_allowed and self._usage_repo and self._usage_updater:
534
+ latest_usage = await self._usage_repo.latest_by_account(window="primary")
535
+ await self._usage_updater.refresh_accounts([saved], latest_usage)
536
+ if saved.status == AccountStatus.ACTIVE:
537
+ clear_account_routing_unavailable(saved.id)
538
+ get_account_selection_cache().invalidate()
539
+ return AccountImportResponse(
540
+ account_id=saved.id,
541
+ email=saved.email,
542
+ workspace_id=saved.workspace_id,
543
+ workspace_label=saved.workspace_label,
544
+ seat_type=saved.seat_type,
545
+ plan_type=saved.plan_type,
546
+ status=saved.status,
547
+ )
548
+
549
+ async def _import_usage_refresh_allowed(self, account: Account) -> bool:
550
+ try:
551
+ route = await resolve_upstream_route(
552
+ self._repo.session,
553
+ account_id=account.id,
554
+ operation="usage_refresh",
555
+ scope="account",
556
+ encryptor=self._encryptor,
557
+ )
558
+ except UpstreamProxyRouteError as exc:
559
+ logger.info(
560
+ "Pausing imported account until upstream proxy binding is available account_id=%s reason=%s",
561
+ account.id,
562
+ exc.reason,
563
+ )
564
+ return False
565
+ if route is not None:
566
+ return True
567
+
568
+ try:
569
+ settings = await self._repo.session.get(DashboardSettings, 1)
570
+ except OperationalError as exc:
571
+ if not _is_missing_upstream_proxy_schema(exc):
572
+ raise
573
+ return True
574
+ if settings is not None and settings.upstream_proxy_routing_enabled:
575
+ logger.info(
576
+ "Pausing imported account until upstream proxy default pool is configured account_id=%s",
577
+ account.id,
578
+ )
579
+ return False
580
+ return True
581
+
582
+ async def reactivate_account(self, account_id: str) -> bool:
583
+ account = await self._repo.get_by_id(account_id)
584
+ if account is None:
585
+ return False
586
+ if account.delete_requested_at is not None:
587
+ # Marked for background deletion: already invisible in listings
588
+ # and about to be removed — report it as gone rather than racing
589
+ # the deletion worker back to ACTIVE.
590
+ return False
591
+ if account.status == AccountStatus.REAUTH_REQUIRED:
592
+ raise AccountStateTransitionError("Account requires re-authentication and cannot be reactivated directly")
593
+ result = await self._repo.update_status_if_current(
594
+ account_id,
595
+ AccountStatus.ACTIVE,
596
+ None,
597
+ None,
598
+ blocked_at=None,
599
+ expected_status=account.status,
600
+ expected_deactivation_reason=account.deactivation_reason,
601
+ expected_reset_at=account.reset_at,
602
+ expected_blocked_at=account.blocked_at,
603
+ )
604
+ if not result:
605
+ raise AccountStateTransitionError("Account state changed; retry the operation")
606
+ if result:
607
+ clear_account_routing_unavailable(account_id)
608
+ get_account_selection_cache().invalidate()
609
+ await propagate_account_routing_change()
610
+ return result
611
+
612
+ async def pause_account(self, account_id: str) -> bool:
613
+ account = await self._get_visible_account(account_id)
614
+ if account is None:
615
+ return False
616
+ if account.status in (AccountStatus.REAUTH_REQUIRED, AccountStatus.DEACTIVATED):
617
+ raise AccountStateTransitionError(f"Account is {account.status.value} and cannot be paused")
618
+ result = await self._repo.update_status_if_current(
619
+ account_id,
620
+ AccountStatus.PAUSED,
621
+ None,
622
+ None,
623
+ blocked_at=None,
624
+ expected_status=account.status,
625
+ expected_deactivation_reason=account.deactivation_reason,
626
+ expected_reset_at=account.reset_at,
627
+ expected_blocked_at=account.blocked_at,
628
+ )
629
+ if not result:
630
+ raise AccountStateTransitionError("Account state changed; retry the operation")
631
+ if result:
632
+ mark_account_routing_unavailable(account_id)
633
+ get_account_selection_cache().invalidate()
634
+ await propagate_account_routing_change()
635
+ return result
636
+
637
+ async def update_account(self, account_id: str, *, security_work_authorized: bool | None = None) -> bool:
638
+ result = False
639
+ if security_work_authorized is not None:
640
+ result = await self._repo.update_security_work_authorized(account_id, security_work_authorized)
641
+ if result:
642
+ get_account_selection_cache().invalidate()
643
+ return result
644
+
645
+ async def set_limit_warmup_enabled(self, account_id: str, enabled: bool) -> bool:
646
+ result = await self._repo.update_limit_warmup_enabled(account_id, enabled)
647
+ if result:
648
+ get_account_selection_cache().invalidate()
649
+ return result
650
+
651
+ async def set_routing_policy(self, account_id: str, routing_policy: str) -> bool:
652
+ result = await self._repo.update_routing_policy(account_id, routing_policy)
653
+ if result:
654
+ get_account_selection_cache().invalidate()
655
+ return result
656
+
657
+ async def delete_account(self, account_id: str, *, delete_history: bool = False) -> bool:
658
+ # Fast path: stamp the pending-deletion marker (terminal status, hidden
659
+ # from listings, sticky/bridge cleanup) and return in milliseconds; the
660
+ # background deletion worker drains the bulk rows and removes the
661
+ # account row afterwards (see app.modules.accounts.deletion).
662
+ result = await self._repo.begin_delete(account_id, delete_history=delete_history)
663
+ if result:
664
+ mark_account_routing_unavailable(account_id)
665
+ get_account_selection_cache().invalidate()
666
+ get_api_key_cache().clear()
667
+ # Finalization cascades the account_proxy_bindings row away, and
668
+ # account ids are deterministic (delete-then-re-import regenerates
669
+ # the same id), so the cached route outcome must not survive the
670
+ # delete request; the worker invalidates again after finalizing.
671
+ await get_upstream_route_cache().invalidate()
672
+ await propagate_account_routing_change()
673
+ poller = get_cache_invalidation_poller()
674
+ if poller is not None:
675
+ await poller.bump(NAMESPACE_API_KEY)
676
+ request_account_deletion_run()
677
+ return result
678
+
679
+ async def set_account_alias(self, account_id: str, alias: str | None) -> bool:
680
+ normalized = alias.strip() if isinstance(alias, str) else None
681
+ if normalized == "":
682
+ normalized = None
683
+ return await self._repo.update_alias(account_id, normalized)
684
+
685
+ async def probe_account(
686
+ self,
687
+ account_id: str,
688
+ model: str | None = None,
689
+ ) -> AccountProbeResponse | None:
690
+ """Send a minimal upstream ``responses.create`` pinned to one account.
691
+
692
+ Bypasses load-balancer scoring so an operator can wake the upstream
693
+ rate-limiter for a stuck account (see upstream issues #676 / #677).
694
+ Triggers an immediate usage refresh after the probe and returns the
695
+ before/after snapshot so the operator can see whether the upstream
696
+ state changed.
697
+ """
698
+ account = await self._get_visible_account(account_id)
699
+ if account is None:
700
+ return None
701
+ if account.status in (AccountStatus.PAUSED, AccountStatus.DEACTIVATED):
702
+ raise AccountNotProbableError(f"Account is {account.status.value} and cannot be probed")
703
+
704
+ primary_before, secondary_before = await self._latest_usage_percents(account_id)
705
+ status_before = account.status.value
706
+
707
+ probe_account = account
708
+ if self._auth_manager is not None:
709
+ probe_account = await self._auth_manager.ensure_fresh(account, force=False)
710
+
711
+ access_token = self._encryptor.decrypt(probe_account.access_token_encrypted)
712
+ probe_model = model or resolve_default_host_model()
713
+ probe_status = await self._send_probe_request(
714
+ access_token=access_token,
715
+ chatgpt_account_id=probe_account.chatgpt_account_id,
716
+ model=probe_model,
717
+ )
718
+
719
+ usage_refresh_fetch_succeeded: bool | None = None
720
+ if self._usage_repo and self._usage_updater:
721
+ usage_refresh_result = await self._usage_updater.force_refresh_result(probe_account)
722
+ usage_refresh_fetch_succeeded = usage_refresh_result.fetch_succeeded
723
+ # Forced refresh can still persist fresh OAuth credentials before a
724
+ # later upstream usage fetch fails. Selection-cache rows carry
725
+ # cloned encrypted tokens, so every forced attempt must invalidate
726
+ # cached accounts, not only attempts that wrote usage rows.
727
+ get_account_selection_cache().invalidate()
728
+
729
+ refreshed = await self._repo.get_by_id(account_id) or account
730
+ primary_after, secondary_after = await self._latest_usage_percents(account_id)
731
+
732
+ response = AccountProbeResponse(
733
+ status="probed",
734
+ account_id=account_id,
735
+ probe_status_code=probe_status,
736
+ primary_used_percent_before=primary_before,
737
+ primary_used_percent_after=primary_after,
738
+ secondary_used_percent_before=secondary_before,
739
+ secondary_used_percent_after=secondary_after,
740
+ account_status_before=status_before,
741
+ account_status_after=refreshed.status.value,
742
+ )
743
+ response._usage_refresh_fetch_succeeded = usage_refresh_fetch_succeeded
744
+ return response
745
+
746
+ async def _latest_usage_percents(self, account_id: str) -> tuple[float | None, float | None]:
747
+ if self._usage_repo is None:
748
+ return None, None
749
+ primary_entry = await self._usage_repo.latest_entry_for_account(account_id, window="primary")
750
+ secondary_entry = await self._usage_repo.latest_entry_for_account(account_id, window="secondary")
751
+ return (
752
+ primary_entry.used_percent if primary_entry is not None else None,
753
+ secondary_entry.used_percent if secondary_entry is not None else None,
754
+ )
755
+
756
+ async def _send_probe_request(
757
+ self,
758
+ *,
759
+ access_token: str,
760
+ chatgpt_account_id: str | None,
761
+ model: str,
762
+ ) -> int:
763
+ settings = get_settings()
764
+ base = settings.upstream_base_url.rstrip("/")
765
+ if "/backend-api" not in base:
766
+ base = f"{base}/backend-api"
767
+ url = f"{base}/codex/responses"
768
+ headers = {
769
+ "Authorization": f"Bearer {access_token}",
770
+ "Accept": "text/event-stream",
771
+ "Content-Type": "application/json",
772
+ }
773
+ if chatgpt_account_id and not chatgpt_account_id.startswith(("email_", "local_")):
774
+ headers["chatgpt-account-id"] = chatgpt_account_id
775
+ body = {
776
+ "model": model,
777
+ "instructions": "Respond with a single dot.",
778
+ "input": [
779
+ {
780
+ "role": "user",
781
+ "content": [{"type": "input_text", "text": "."}],
782
+ }
783
+ ],
784
+ "stream": True,
785
+ "store": False,
786
+ }
787
+ timeout = aiohttp.ClientTimeout(
788
+ total=PROBE_REQUEST_TIMEOUT_SECONDS,
789
+ sock_connect=PROBE_CONNECT_TIMEOUT_SECONDS,
790
+ )
791
+ try:
792
+ async with lease_http_session() as session:
793
+ async with session.post(url, headers=headers, json=body, timeout=timeout) as resp:
794
+ # Initiating the request is enough to wake the upstream
795
+ # rate-limiter; we do not consume the SSE body.
796
+ return resp.status
797
+ except (aiohttp.ClientError, asyncio.TimeoutError) as exc:
798
+ logger.warning(
799
+ "Probe upstream request failed account=%s error=%s",
800
+ chatgpt_account_id,
801
+ exc,
802
+ )
803
+ return PROBE_NETWORK_FAILURE_STATUS
804
+
805
+
806
+ def _opencode_auth_export_filename(account: Account) -> str:
807
+ source = account.email or account.id
808
+ safe = "".join(char if char.isalnum() or char in "._-" else "-" for char in source).strip("-._")
809
+ return f"opencode-auth-{safe or account.id}.json"