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,710 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import logging
5
+ from dataclasses import dataclass
6
+ from functools import lru_cache
7
+ from ipaddress import ip_address, ip_network
8
+ from typing import cast
9
+
10
+ from fastapi import Request, Security
11
+ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
12
+ from starlette.requests import HTTPConnection
13
+
14
+ from app.core.auth import generate_unique_account_id
15
+ from app.core.auth.api_key_cache import get_api_key_cache
16
+ from app.core.auth.dashboard_access import (
17
+ STEP_UP_PERMISSIONS,
18
+ DashboardPermission,
19
+ DashboardPrincipal,
20
+ Permission,
21
+ Scope,
22
+ admin_principal,
23
+ guest_principal,
24
+ scope_satisfies,
25
+ totp_policy_applies,
26
+ user_principal,
27
+ )
28
+ from app.core.auth.dashboard_mode import DashboardAuthMode, DashboardRequestAuth, get_dashboard_request_auth
29
+ from app.core.auth.dashboard_users_cache import DashboardUsersCache, get_dashboard_users_cache
30
+ from app.core.auth.external_identity import resolve_trusted_header_request
31
+ from app.core.auth.step_up import (
32
+ STEP_UP_COOKIE,
33
+ STEP_UP_UNAVAILABLE_MESSAGE,
34
+ account_step_up_methods,
35
+ is_step_up_fresh,
36
+ )
37
+ from app.core.clients.proxy import CODEX_LB_REQUIRED_CAPABILITY_HEADER
38
+ from app.core.clients.usage import UsageFetchError, fetch_usage
39
+ from app.core.config.settings import get_settings
40
+ from app.core.config.settings_cache import get_settings_cache
41
+ from app.core.crypto import TokenEncryptor
42
+ from app.core.exceptions import DashboardAuthError, DashboardPermissionError, ProxyAuthError, ProxyUpstreamError
43
+ from app.core.request_locality import is_local_request
44
+ from app.core.socket_peer import raw_socket_peer_host
45
+ from app.core.upstream_proxy import UpstreamProxyRouteError, resolve_upstream_route
46
+ from app.core.utils.time import utcnow
47
+ from app.db.models import AccountStatus, DashboardSettings, DashboardUser, DashboardUserStatus
48
+ from app.db.session import get_background_session
49
+ from app.modules.accounts.repository import AccountsRepository
50
+ from app.modules.api_keys.repository import ApiKeysRepository
51
+ from app.modules.api_keys.service import ApiKeyData, ApiKeyInvalidError, ApiKeysService
52
+ from app.modules.dashboard_auth.service import (
53
+ DASHBOARD_SESSION_COOKIE,
54
+ DashboardSessionState,
55
+ get_dashboard_session_store,
56
+ get_step_up_cookie_store,
57
+ is_local_password_session,
58
+ session_clock,
59
+ )
60
+ from app.modules.dashboard_roles.service import resolve_role_grants
61
+ from app.modules.dashboard_users.break_glass import break_glass_second_factor_required, local_login_admits
62
+
63
+ logger = logging.getLogger(__name__)
64
+
65
+ _bearer = HTTPBearer(description="API key (e.g. sk-clb-…)", auto_error=False)
66
+ #: The only routes an account that still has to enrol a TOTP secret may reach
67
+ #: (an explicit allow-list, not the module prefix: guest-password management,
68
+ #: password removal and TOTP disable live under the same prefix and stay closed).
69
+ TOTP_ENROLLMENT_ALLOWED_PATHS: frozenset[str] = frozenset(
70
+ {
71
+ "/api/dashboard-auth/session",
72
+ "/api/dashboard-auth/totp/setup/start",
73
+ "/api/dashboard-auth/totp/setup/confirm",
74
+ "/api/dashboard-auth/totp/verify",
75
+ "/api/dashboard-auth/logout",
76
+ "/api/dashboard-auth/logout-all",
77
+ "/api/dashboard-auth/me",
78
+ "/api/dashboard-auth/password/change",
79
+ }
80
+ )
81
+ #: ``auth_method`` of the implicit admin on a local install without a password.
82
+ AUTH_METHOD_LOCAL_BOOTSTRAP = "local_bootstrap"
83
+ _CODEX_USAGE_IDENTITY_INACTIVE_WORKSPACE_STATUSES = {
84
+ AccountStatus.PAUSED,
85
+ AccountStatus.DEACTIVATED,
86
+ }
87
+
88
+
89
+ # --- Error format markers ---
90
+
91
+
92
+ def set_openai_error_format(request: Request) -> None:
93
+ request.state.error_format = "openai"
94
+
95
+
96
+ def set_dashboard_error_format(request: Request) -> None:
97
+ request.state.error_format = "dashboard"
98
+
99
+
100
+ def set_scim_error_format(request: Request) -> None:
101
+ """Mark the request so every refusal answers RFC 7644's envelope.
102
+
103
+ A router-level marker, exactly like the two above; unmatched ``/scim``
104
+ paths fall back on the path prefix in ``_error_format`` instead.
105
+ """
106
+
107
+ request.state.error_format = "scim"
108
+
109
+
110
+ # --- Proxy API key auth ---
111
+
112
+
113
+ async def validate_proxy_api_key(
114
+ request: Request,
115
+ credentials: HTTPAuthorizationCredentials | None = Security(_bearer),
116
+ ) -> ApiKeyData | None:
117
+ """A required-capability header authenticates even when global proxy API-key auth is disabled."""
118
+
119
+ authorization = None if credentials is None else f"Bearer {credentials.credentials}"
120
+ if request.headers.getlist(CODEX_LB_REQUIRED_CAPABILITY_HEADER):
121
+ return await validate_required_proxy_api_key_authorization(authorization)
122
+ return await validate_proxy_api_key_authorization(authorization, request=request)
123
+
124
+
125
+ async def validate_proxy_api_key_authorization(
126
+ authorization: str | None,
127
+ *,
128
+ request: HTTPConnection | None = None,
129
+ ) -> ApiKeyData | None:
130
+ settings = await get_settings_cache().get()
131
+ if not settings.api_key_auth_enabled:
132
+ if request is not None and not is_local_request(request):
133
+ if not _is_proxy_unauthenticated_socket_peer_allowed(request):
134
+ raise ProxyAuthError("Proxy authentication must be configured before remote access is allowed")
135
+ return None
136
+
137
+ token = _extract_bearer_token(authorization)
138
+ if not token:
139
+ raise ProxyAuthError("Missing API key in Authorization header")
140
+
141
+ return await _validate_api_key_token(token)
142
+
143
+
144
+ async def validate_required_proxy_api_key_authorization(authorization: str | None) -> ApiKeyData:
145
+ """Validate a proxy API key even when global proxy auth is disabled."""
146
+
147
+ token = _extract_bearer_token(authorization)
148
+ if not token:
149
+ raise ProxyAuthError("Missing API key in Authorization header")
150
+ return await _validate_api_key_token(token)
151
+
152
+
153
+ async def _validate_api_key_token(token: str) -> ApiKeyData:
154
+ """Validate a plain API key token and return the typed key data."""
155
+
156
+ token_hash = hashlib.sha256(token.encode("utf-8")).hexdigest()
157
+ cache = get_api_key_cache()
158
+ cached = cast(ApiKeyData | None, await cache.get(token_hash))
159
+ if cached is not None:
160
+ if cached.expires_at is not None and cached.expires_at <= utcnow():
161
+ await cache.invalidate(token_hash)
162
+ else:
163
+ return cached
164
+
165
+ version_before_read = cache.version
166
+ async with get_background_session() as session:
167
+ service = ApiKeysService(ApiKeysRepository(session))
168
+ try:
169
+ validated = await service.validate_key(token)
170
+ await cache.set(token_hash, validated, if_version=version_before_read)
171
+ return validated
172
+ except ApiKeyInvalidError as exc:
173
+ raise ProxyAuthError(str(exc)) from exc
174
+
175
+
176
+ async def validate_required_proxy_api_key(
177
+ request: Request,
178
+ credentials: HTTPAuthorizationCredentials | None = Security(_bearer),
179
+ ) -> ApiKeyData:
180
+ """Require a valid proxy API key regardless of the global auth setting."""
181
+
182
+ authorization = None if credentials is None else f"Bearer {credentials.credentials}"
183
+ return await validate_required_proxy_api_key_authorization(authorization)
184
+
185
+
186
+ # --- Self-service usage endpoint auth (always requires valid key) ---
187
+
188
+
189
+ async def validate_usage_api_key(
190
+ request: Request,
191
+ credentials: HTTPAuthorizationCredentials | None = Security(_bearer),
192
+ ) -> ApiKeyData:
193
+ """Validate API key for self-service usage endpoint.
194
+
195
+ Unlike ``validate_proxy_api_key``, this dependency always requires a valid
196
+ Bearer API key, regardless of the global ``api_key_auth_enabled`` setting.
197
+ Raises ProxyAuthError when the key is missing or invalid.
198
+ """
199
+ authorization = None if credentials is None else f"Bearer {credentials.credentials}"
200
+ return await validate_required_proxy_api_key_authorization(authorization)
201
+
202
+
203
+ # --- Dashboard session auth ---
204
+
205
+
206
+ def _set_dashboard_principal(request: Request, principal: DashboardPrincipal) -> DashboardPrincipal:
207
+ request.state.dashboard_principal = principal
208
+ return principal
209
+
210
+
211
+ def _get_cached_dashboard_principal(request: Request) -> DashboardPrincipal | None:
212
+ principal = getattr(request.state, "dashboard_principal", None)
213
+ return principal if isinstance(principal, DashboardPrincipal) else None
214
+
215
+
216
+ async def _resolve_session_user(
217
+ state: DashboardSessionState | None, users_cache: DashboardUsersCache
218
+ ) -> DashboardUser | None:
219
+ """The account behind a user cookie, or ``None`` when the cookie no longer binds to one.
220
+
221
+ A missing, disabled, or invited account and a stale ``session_generation``
222
+ all fail the same way; the caller answers ``authentication_required``.
223
+ """
224
+
225
+ if state is None or not state.is_user or state.user_id is None:
226
+ return None
227
+ user = await users_cache.get_user(state.user_id)
228
+ if user is None or user.status != DashboardUserStatus.ACTIVE.value:
229
+ return None
230
+ if user.session_generation != state.session_generation:
231
+ return None
232
+ return user
233
+
234
+
235
+ def _user_session_principal(
236
+ request: Request,
237
+ user: DashboardUser,
238
+ state: DashboardSessionState,
239
+ *,
240
+ settings: DashboardSettings,
241
+ ) -> DashboardPrincipal:
242
+ grants = resolve_role_grants(user.role)
243
+ # An emergency account that holds a secret always presents it, whatever
244
+ # the two toggles say: the account a tightened policy relies on must never
245
+ # be reachable on a password alone.
246
+ totp_required = totp_policy_applies(
247
+ required_on_login=settings.totp_required_on_login,
248
+ required_for_admin_role=settings.totp_required_for_admin_role,
249
+ grants=grants,
250
+ ) or break_glass_second_factor_required(user)
251
+ totp_configured = user.totp_secret_encrypted is not None
252
+ if totp_required and totp_configured and not state.totp_verified:
253
+ raise DashboardAuthError("TOTP verification is required for dashboard access", code="totp_required")
254
+ totp_enrollment_required = totp_required and not totp_configured
255
+ # Routes guarded only by this dependency (account inventory, request logs,
256
+ # ...) assume a reader who may see everything. Until the self-service phase
257
+ # makes own-scoped routes declare their own requirement (this guard then
258
+ # moves to route level), a role without dashboard:read at scope `all`
259
+ # (e.g. member) is refused outright rather than admitted to all-scope views.
260
+ if not scope_satisfies(grants.get(Permission.DASHBOARD_READ), Scope.ALL):
261
+ raise DashboardPermissionError(
262
+ f"Dashboard permission '{Permission.DASHBOARD_READ.value}' is required",
263
+ code="permission_required",
264
+ param=Permission.DASHBOARD_READ.value,
265
+ )
266
+ principal = user_principal(
267
+ user,
268
+ grants,
269
+ auth_method=state.auth_method,
270
+ totp_enrollment_required=totp_enrollment_required,
271
+ step_up_verified_at=state.step_up_verified_at,
272
+ )
273
+ # An account that still has to enrol may only reach the self-service routes
274
+ # listed above; those routes gate themselves, so every other route that
275
+ # reaches this dependency is refused here.
276
+ if totp_enrollment_required and request.url.path not in TOTP_ENROLLMENT_ALLOWED_PATHS:
277
+ raise DashboardPermissionError(
278
+ "TOTP enrollment is required before dashboard access",
279
+ code="totp_enrollment_required",
280
+ )
281
+ return _set_dashboard_principal(request, principal)
282
+
283
+
284
+ async def _password_fallback_principal(request: Request) -> DashboardPrincipal | None:
285
+ """A password-verified cookie the local login policy admits, so an emergency
286
+ account stays reachable while the proxy asserts an identity the resolver refuses.
287
+
288
+ ``local_login_policy`` decides which cookie counts: ``enabled`` (the
289
+ default) admits every active account, which is what shipped before this
290
+ change; ``admins_only`` admits the admin preset; ``break_glass_only``
291
+ admits only a designated emergency account. One function, so the gate and
292
+ the session response that advertises the fallback cannot disagree.
293
+ """
294
+
295
+ users_cache = get_dashboard_users_cache()
296
+ state = get_dashboard_session_store().get(request.cookies.get(DASHBOARD_SESSION_COOKIE))
297
+ session_user = await _resolve_session_user(state, users_cache)
298
+ if state is None or session_user is None or not is_local_password_session(state):
299
+ return None
300
+ settings = await get_settings_cache().get()
301
+ if not local_login_admits(session_user, settings.local_login_policy):
302
+ return None
303
+ return _user_session_principal(request, session_user, state, settings=settings)
304
+
305
+
306
+ async def _trusted_header_principal(request: Request, request_auth: DashboardRequestAuth) -> DashboardPrincipal:
307
+ """The account behind the proxy-asserted identity, or a 401 naming why there is none.
308
+
309
+ Unknown identities become accounts through the provider's
310
+ ``unknown_identity_role_id`` (admin by default, D10). A refused identity
311
+ answers ``identity_not_provisioned`` and a disabled account
312
+ ``account_disabled`` -- unless a password-verified cookie rides along, in
313
+ which case that account is served (a resolved header account always wins
314
+ over the cookie). The TOTP policy is not applied to header sessions yet:
315
+ there is no cookie to carry a verified step, so applying it would lock
316
+ every header user out (the step-up change wires it, honouring the
317
+ provider's ``idp_mfa_enforced``).
318
+ """
319
+
320
+ resolution = await resolve_trusted_header_request(request, request_auth)
321
+ if resolution is None:
322
+ raise DashboardAuthError("Reverse proxy authentication is required", code="proxy_auth_required")
323
+ if resolution.user is None:
324
+ fallback = await _password_fallback_principal(request)
325
+ if fallback is not None:
326
+ return fallback
327
+ if resolution.denial == "account_disabled":
328
+ raise DashboardAuthError("This account is disabled", code="account_disabled")
329
+ raise DashboardAuthError(
330
+ "Your account is not ready yet; ask an administrator to add you", code="identity_not_provisioned"
331
+ )
332
+ grants = resolve_role_grants(resolution.user.role)
333
+ if not scope_satisfies(grants.get(Permission.DASHBOARD_READ), Scope.ALL):
334
+ raise DashboardPermissionError(
335
+ f"Dashboard permission '{Permission.DASHBOARD_READ.value}' is required",
336
+ code="permission_required",
337
+ param=Permission.DASHBOARD_READ.value,
338
+ )
339
+ return user_principal(
340
+ resolution.user, grants, auth_method=request_auth.mode.value, auth_mode=DashboardAuthMode.TRUSTED_HEADER
341
+ )
342
+
343
+
344
+ async def validate_dashboard_session(request: Request) -> DashboardPrincipal:
345
+ cached = _get_cached_dashboard_principal(request)
346
+ if cached is not None:
347
+ return cached
348
+
349
+ request_auth = get_dashboard_request_auth(request)
350
+ if request_auth is not None and request_auth.mode == DashboardAuthMode.TRUSTED_HEADER:
351
+ return _set_dashboard_principal(request, await _trusted_header_principal(request, request_auth))
352
+ if request_auth is not None:
353
+ return _set_dashboard_principal(
354
+ request,
355
+ admin_principal(auth_mode=request_auth.mode, actor=request_auth.actor, auth_method=request_auth.mode.value),
356
+ )
357
+
358
+ settings = await get_settings_cache().get()
359
+ users_cache = get_dashboard_users_cache()
360
+ auth_state = await users_cache.local_auth_state()
361
+ password_required = auth_state.requires_auth
362
+ # The admin-role requirement binds accounts only, so on its own it never
363
+ # turns a passwordless install into one that demands a login.
364
+ requires_auth = password_required or settings.totp_required_on_login
365
+ guest_access_enabled = settings.guest_access_enabled
366
+ guest_password_required = guest_access_enabled and settings.guest_password_hash is not None
367
+ passwordless_guest_fallback_allowed = not (
368
+ get_dashboard_request_auth_mode() == DashboardAuthMode.TRUSTED_HEADER
369
+ and requires_auth
370
+ and guest_access_enabled
371
+ and not guest_password_required
372
+ )
373
+ session_id = request.cookies.get(DASHBOARD_SESSION_COOKIE)
374
+ state = get_dashboard_session_store().get(session_id)
375
+ session_user = await _resolve_session_user(state, users_cache)
376
+
377
+ # Behind a reverse proxy a header-less request only gets in on a password
378
+ # cookie the local login policy still admits (the same decision
379
+ # ``_password_fallback_principal`` makes for a refused identity).
380
+ has_password_fallback_session = (
381
+ state is not None
382
+ and session_user is not None
383
+ and is_local_password_session(state)
384
+ and local_login_admits(session_user, settings.local_login_policy)
385
+ )
386
+ if get_dashboard_request_auth_mode() == DashboardAuthMode.TRUSTED_HEADER and not has_password_fallback_session:
387
+ raise DashboardAuthError("Reverse proxy authentication is required", code="proxy_auth_required")
388
+ # A guest cookie is only ever minted under the current guest generation, so
389
+ # a matching generation proves it passed whatever guest credential applied.
390
+ if (
391
+ state is not None
392
+ and state.is_guest
393
+ and guest_access_enabled
394
+ and state.guest_session_generation == settings.guest_session_generation
395
+ and (guest_password_required or passwordless_guest_fallback_allowed)
396
+ ):
397
+ return _set_dashboard_principal(request, guest_principal())
398
+ if state is not None and session_user is not None and state.password_verified:
399
+ return _user_session_principal(request, session_user, state, settings=settings)
400
+
401
+ if not requires_auth:
402
+ if not is_local_request(request):
403
+ if guest_access_enabled:
404
+ if not guest_password_required:
405
+ return _set_dashboard_principal(request, guest_principal())
406
+ raise DashboardAuthError("Authentication is required")
407
+ raise DashboardAuthError(
408
+ "Remote bootstrap is required before dashboard access is allowed",
409
+ code="bootstrap_required",
410
+ )
411
+ return _set_dashboard_principal(
412
+ request,
413
+ admin_principal(auth_mode=DashboardAuthMode.STANDARD, auth_method=AUTH_METHOD_LOCAL_BOOTSTRAP),
414
+ )
415
+
416
+ if guest_access_enabled and not guest_password_required and passwordless_guest_fallback_allowed:
417
+ return _set_dashboard_principal(request, guest_principal())
418
+
419
+ if auth_state.active_local_password_users == 0 and settings.totp_required_on_login:
420
+ logger.warning(
421
+ "dashboard_auth_migration_inconsistency no active local password user"
422
+ " while totp_required_on_login=true metric=dashboard_auth_migration_inconsistency"
423
+ )
424
+
425
+ raise DashboardAuthError("Authentication is required")
426
+
427
+
428
+ async def require_dashboard_write_access(request: Request) -> DashboardPrincipal:
429
+ principal = await validate_dashboard_session(request)
430
+ if not principal.can(DashboardPermission.WRITE):
431
+ raise DashboardPermissionError(
432
+ "Read-only dashboard access cannot modify dashboard state",
433
+ code="read_only_access",
434
+ )
435
+ return principal
436
+
437
+
438
+ @dataclass(frozen=True, slots=True)
439
+ class PermissionRequirement:
440
+ """Declares the permission a route dependency enforces.
441
+
442
+ Attached to the dependency callables produced by
443
+ :func:`require_dashboard_permission` so route-authorization audits can read
444
+ the requirement without invoking the dependency.
445
+ """
446
+
447
+ permission: Permission
448
+ minimum_scope: Scope = Scope.ALL
449
+
450
+
451
+ def ensure_dashboard_permission(
452
+ principal: DashboardPrincipal,
453
+ permission: Permission,
454
+ *,
455
+ minimum_scope: Scope = Scope.ALL,
456
+ ) -> None:
457
+ if principal.has(permission, minimum_scope=minimum_scope):
458
+ return
459
+ raise DashboardPermissionError(
460
+ f"Dashboard permission '{permission.value}' is required",
461
+ code="permission_required",
462
+ param=permission.value,
463
+ )
464
+
465
+
466
+ _SAFE_METHODS: frozenset[str] = frozenset({"GET", "HEAD", "OPTIONS"})
467
+
468
+
469
+ def recorded_step_up(request: Request, user: DashboardUser) -> int | None:
470
+ """The most recent step-up this request carries for ``user``, wherever it rides.
471
+
472
+ A trusted-header account has no session cookie, so its step-up rides in
473
+ the separate step-up cookie — or in a fallback password session of the
474
+ same account that happens to accompany the request. Both are bound to the
475
+ account's ``session_generation``, so revoking its sessions or resetting
476
+ its TOTP voids the proof instead of leaving it usable for the rest of the
477
+ window. Enforcement and the session response read the same sources, so
478
+ what the dashboard reports is what the gate accepts.
479
+ """
480
+
481
+ from_cookie = get_step_up_cookie_store().get(
482
+ request.cookies.get(STEP_UP_COOKIE),
483
+ user_id=user.id,
484
+ session_generation=user.session_generation,
485
+ )
486
+ state = get_dashboard_session_store().get(request.cookies.get(DASHBOARD_SESSION_COOKIE))
487
+ from_session = (
488
+ state.step_up_verified_at
489
+ if state is not None
490
+ and state.is_user
491
+ and state.user_id == user.id
492
+ and state.password_verified
493
+ and state.session_generation == user.session_generation
494
+ else None
495
+ )
496
+ recorded = [value for value in (from_cookie, from_session) if value is not None]
497
+ return max(recorded) if recorded else None
498
+
499
+
500
+ async def ensure_step_up(request: Request, principal: DashboardPrincipal, permission: Permission) -> None:
501
+ """Require a fresh step-up (PLAN §5 H5) before a mutation guarded by ``permission``.
502
+
503
+ Reads are never gated. Principals without an account (the implicit local
504
+ admin, the disabled-auth principal) have no credential to re-verify and are
505
+ exempt; every account is held to it, whatever provider signed it in. A
506
+ stale or missing step-up answers ``403 step_up_required`` naming the
507
+ factors the account can present — including its identity provider when
508
+ that is the only thing it has; an account with no factor at all answers
509
+ ``403 step_up_unavailable``.
510
+ """
511
+
512
+ if request.method in _SAFE_METHODS or principal.user_id is None:
513
+ return
514
+ user = await get_dashboard_users_cache().get_user(principal.user_id)
515
+ # The principal's own claim came from a cookie this request already
516
+ # validated against the account's generation; the other sources are
517
+ # checked against it inside recorded_step_up.
518
+ recorded = [principal.step_up_verified_at] if principal.step_up_verified_at is not None else []
519
+ if user is not None:
520
+ recorded += [value for value in (recorded_step_up(request, user),) if value is not None]
521
+ if recorded and is_step_up_fresh(max(recorded), now=session_clock()):
522
+ return
523
+ methods = await account_step_up_methods(user) if user is not None else []
524
+ if not methods:
525
+ raise DashboardPermissionError(STEP_UP_UNAVAILABLE_MESSAGE, code="step_up_unavailable", param=permission.value)
526
+ raise DashboardPermissionError(
527
+ "Confirm your identity to continue",
528
+ code="step_up_required",
529
+ param=permission.value,
530
+ details={"methods": list(methods)},
531
+ )
532
+
533
+
534
+ class DashboardPermissionDependency:
535
+ """Route dependency: validate the dashboard session, then enforce one permission.
536
+
537
+ Mutations guarded by a :data:`STEP_UP_PERMISSIONS` member also require a
538
+ recent step-up. Instances are callable so FastAPI can resolve them with
539
+ ``Depends`` while route-authorization audits read :attr:`requirement`
540
+ without invoking them.
541
+ """
542
+
543
+ __slots__ = ("requirement",)
544
+
545
+ def __init__(self, requirement: PermissionRequirement) -> None:
546
+ self.requirement = requirement
547
+
548
+ async def __call__(self, request: Request) -> DashboardPrincipal:
549
+ principal = await validate_dashboard_session(request)
550
+ ensure_dashboard_permission(
551
+ principal,
552
+ self.requirement.permission,
553
+ minimum_scope=self.requirement.minimum_scope,
554
+ )
555
+ if self.requirement.permission in STEP_UP_PERMISSIONS:
556
+ await ensure_step_up(request, principal, self.requirement.permission)
557
+ return principal
558
+
559
+
560
+ @lru_cache(maxsize=None)
561
+ def _dependency_for(requirement: PermissionRequirement) -> DashboardPermissionDependency:
562
+ return DashboardPermissionDependency(requirement)
563
+
564
+
565
+ def require_dashboard_permission(
566
+ permission: Permission,
567
+ *,
568
+ minimum_scope: Scope = Scope.ALL,
569
+ ) -> DashboardPermissionDependency:
570
+ """Return the shared dependency enforcing ``permission`` at ``minimum_scope``.
571
+
572
+ Cached per :class:`PermissionRequirement` (not per call spelling) so every
573
+ router shares one callable per requirement, keeping dependency overrides
574
+ and OpenAPI stable.
575
+ """
576
+
577
+ return _dependency_for(PermissionRequirement(permission=permission, minimum_scope=minimum_scope))
578
+
579
+
580
+ def ensure_dashboard_admin_access(principal: DashboardPrincipal) -> None:
581
+ """Backward-compatible alias for the ``conversations:read`` requirement."""
582
+
583
+ ensure_dashboard_permission(principal, Permission.CONVERSATIONS_READ)
584
+
585
+
586
+ async def require_dashboard_admin_access(request: Request) -> DashboardPrincipal:
587
+ """Backward-compatible alias for ``require_dashboard_permission(CONVERSATIONS_READ)``."""
588
+
589
+ return await require_dashboard_permission(Permission.CONVERSATIONS_READ)(request)
590
+
591
+
592
+ def get_dashboard_request_auth_mode() -> DashboardAuthMode:
593
+ from app.core.config.settings import get_settings
594
+
595
+ return get_settings().dashboard_auth_mode
596
+
597
+
598
+ def _is_proxy_unauthenticated_socket_peer_allowed(request: HTTPConnection) -> bool:
599
+ socket_host = raw_socket_peer_host(request)
600
+ if socket_host is None:
601
+ return False
602
+
603
+ try:
604
+ socket_ip = ip_address(socket_host)
605
+ except ValueError:
606
+ return False
607
+
608
+ configured_cidrs = get_settings().proxy_unauthenticated_client_cidrs
609
+ return any(socket_ip in ip_network(cidr, strict=False) for cidr in configured_cidrs)
610
+
611
+
612
+ # --- Codex usage caller identity auth ---
613
+
614
+
615
+ async def validate_codex_usage_identity(request: Request) -> ApiKeyData | None:
616
+ token = _extract_bearer_token(request.headers.get("Authorization"))
617
+ if not token:
618
+ raise ProxyAuthError("Missing ChatGPT token in Authorization header")
619
+
620
+ raw_account_id = request.headers.get("chatgpt-account-id")
621
+ account_id = raw_account_id.strip() if raw_account_id else ""
622
+ if not account_id:
623
+ if token.startswith("sk-clb-"):
624
+ return await _validate_api_key_token(token)
625
+ raise ProxyAuthError("Missing chatgpt-account-id header")
626
+
627
+ async with get_background_session() as session:
628
+ accounts_repo = AccountsRepository(session)
629
+ account = await accounts_repo.get_active_by_chatgpt_account_id(account_id)
630
+ if account is None:
631
+ raise ProxyAuthError("Unknown or inactive chatgpt-account-id")
632
+ local_account_id = account.id
633
+ local_account_email = account.email
634
+ try:
635
+ route = await resolve_upstream_route(
636
+ session,
637
+ account_id=local_account_id,
638
+ operation="usage_identity",
639
+ scope="account",
640
+ encryptor=TokenEncryptor(),
641
+ )
642
+ except UpstreamProxyRouteError as exc:
643
+ raise ProxyUpstreamError("Unable to resolve upstream proxy route for ChatGPT credentials") from exc
644
+
645
+ try:
646
+ usage_payload = await fetch_usage(
647
+ access_token=token,
648
+ account_id=account_id,
649
+ route=route,
650
+ allow_direct_egress=route is None,
651
+ )
652
+ except UsageFetchError as exc:
653
+ if exc.status_code == 429:
654
+ from app.core.exceptions import ProxyRateLimitError
655
+
656
+ raise ProxyRateLimitError(exc.message) from exc
657
+ if exc.status_code in (401, 403):
658
+ raise ProxyAuthError("Invalid ChatGPT token or chatgpt-account-id") from exc
659
+ raise ProxyUpstreamError("Unable to validate ChatGPT credentials at this time") from exc
660
+ if usage_payload is not None and (usage_payload.workspace_id or usage_payload.workspace_label):
661
+ expected_account_id = generate_unique_account_id(
662
+ account_id,
663
+ local_account_email,
664
+ usage_payload.workspace_id,
665
+ usage_payload.workspace_label,
666
+ )
667
+ async with get_background_session() as session:
668
+ accounts_repo = AccountsRepository(session)
669
+ workspace_account = await accounts_repo.get_by_id(expected_account_id)
670
+ if workspace_account is not None and workspace_account.chatgpt_account_id == account_id:
671
+ if workspace_account.status in _CODEX_USAGE_IDENTITY_INACTIVE_WORKSPACE_STATUSES:
672
+ raise ProxyAuthError("Unknown or inactive chatgpt-account-id")
673
+ local_account_id = workspace_account.id
674
+ try:
675
+ route = await resolve_upstream_route(
676
+ session,
677
+ account_id=local_account_id,
678
+ operation="usage_identity",
679
+ scope="account",
680
+ encryptor=TokenEncryptor(),
681
+ )
682
+ except UpstreamProxyRouteError as exc:
683
+ raise ProxyUpstreamError("Unable to resolve upstream proxy route for ChatGPT credentials") from exc
684
+ request.state.codex_usage_identity_access_token = token
685
+ request.state.codex_usage_identity_chatgpt_account_id = account_id
686
+ request.state.codex_usage_identity_account_id = local_account_id
687
+ request.state.codex_usage_identity_route = route
688
+ request.state.codex_usage_identity_payload = usage_payload
689
+ return None
690
+
691
+
692
+ async def validate_codex_provider_usage_identity(request: Request) -> ApiKeyData | None:
693
+ """Bind provider capability intent to a proxy API-key principal before usage I/O."""
694
+
695
+ if request.headers.getlist(CODEX_LB_REQUIRED_CAPABILITY_HEADER):
696
+ return await validate_required_proxy_api_key_authorization(request.headers.get("authorization"))
697
+ return await validate_codex_usage_identity(request)
698
+
699
+
700
+ def _extract_bearer_token(authorization: str | None) -> str | None:
701
+ if authorization is None:
702
+ return None
703
+ prefix = "bearer "
704
+ value = authorization.strip()
705
+ if not value.lower().startswith(prefix):
706
+ return None
707
+ token = value[len(prefix) :].strip()
708
+ if not token:
709
+ return None
710
+ return token