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,1217 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import AsyncIterator, Callable, Collection, Mapping, Sequence
4
+ from contextlib import asynccontextmanager
5
+ from dataclasses import dataclass
6
+ from datetime import datetime
7
+
8
+ from sqlalchemy.ext.asyncio import AsyncSession
9
+
10
+ from app.core.clients.proxy import ProxyResponseError
11
+ from app.core.config.settings import get_settings
12
+ from app.core.errors import openai_error
13
+ from app.core.utils.time import to_utc_naive
14
+ from app.db.models import HttpBridgeSessionState
15
+ from app.db.session import close_session
16
+ from app.modules.proxy.continuity import is_http_bridge_account_neutral_replay
17
+ from app.modules.proxy.durable_bridge_repository import (
18
+ DURABLE_BRIDGE_OPERATION_SPOOL_PURGE_BATCH_SIZE,
19
+ REBIND_ANCHOR_UNFENCED,
20
+ DurableBridgeAliasRegistration,
21
+ DurableBridgeAliasRegistrationReceipt,
22
+ DurableBridgeOperationAbandonment,
23
+ DurableBridgeOperationAbandonmentScanCursor,
24
+ DurableBridgeOperationEventInput,
25
+ DurableBridgeOperationPurgeBatchResult,
26
+ DurableBridgeOperationSnapshot,
27
+ DurableBridgeRecoveryAttemptSnapshot,
28
+ DurableBridgeRepository,
29
+ DurableBridgeRetryCircuitSnapshot,
30
+ DurableBridgeSessionSnapshot,
31
+ DurableBridgeTranscriptTurn,
32
+ durable_bridge_api_key_scope,
33
+ )
34
+
35
+ _DURABLE_TURN_STATE_ALIAS = "turn_state"
36
+ _DURABLE_PREVIOUS_RESPONSE_ALIAS = "previous_response_id"
37
+ _DURABLE_SESSION_HEADER_ALIAS = "session_header"
38
+
39
+
40
+ @dataclass(frozen=True, slots=True)
41
+ class DurableBridgeLookup:
42
+ session_id: str
43
+ canonical_kind: str
44
+ canonical_key: str
45
+ api_key_scope: str
46
+ account_id: str | None
47
+ owner_instance_id: str | None
48
+ owner_epoch: int
49
+ lease_expires_at: datetime | None
50
+ state: HttpBridgeSessionState
51
+ latest_turn_state: str | None
52
+ latest_response_id: str | None
53
+ latest_input_item_count: int | None = None
54
+ latest_input_full_fingerprint: str | None = None
55
+ model: str | None = None
56
+ latest_pending_tool_calls: dict[str, str] | None = None
57
+ owner_process_epoch: str | None = None
58
+ # True when a writer retired this row's continuity owner. The lookup then
59
+ # carries no owner and no anchor, and the request proceeds as if this
60
+ # thread had never been pinned — which is different from "no row exists",
61
+ # because callers must not fail closed on the missing owner.
62
+ continuity_abandoned: bool = False
63
+ retired_account_id: str | None = None
64
+
65
+ def lease_is_active(self, *, now: datetime) -> bool:
66
+ if self.owner_instance_id is None:
67
+ return False
68
+ if self.lease_expires_at is None:
69
+ return False
70
+ # lease_expires_at is a timestamptz column: PostgreSQL yields it
71
+ # offset-aware while the app clock (utcnow) and SQLite yield naive
72
+ # UTC. Normalize both sides — comparing them raw raises TypeError
73
+ # on the anchored-lookup hot path.
74
+ return to_utc_naive(self.lease_expires_at) > to_utc_naive(now)
75
+
76
+
77
+ def durable_bridge_snapshot_is_detached(snapshot: DurableBridgeSessionSnapshot) -> bool:
78
+ """Return True for a row whose owner account was invalidated.
79
+
80
+ Account deactivation, re-authentication demands, proxy-binding changes and
81
+ deletion detach every durable bridge row of that account: the row is
82
+ CLOSED, its owner account, lease and every continuity anchor are cleared,
83
+ and its aliases are deleted. Such a row proves only that a conversation
84
+ once existed; it names no account that could still preserve continuity,
85
+ so it MUST NOT be treated as durable owner evidence. Reporting it as a
86
+ lookup hit made every hard-affinity (thread/session header) continuation
87
+ fail closed forever with ``previous_response_owner_unavailable`` even
88
+ after the account was reactivated, while a fresh thread on the same
89
+ client worked.
90
+
91
+ A CLOSED row that still names its account (ordinary release) keeps its
92
+ continuity value and is intentionally not covered here.
93
+ """
94
+
95
+ return (
96
+ snapshot.account_id is None
97
+ and snapshot.state == HttpBridgeSessionState.CLOSED
98
+ and snapshot.owner_instance_id is None
99
+ and snapshot.latest_turn_state is None
100
+ and snapshot.latest_response_id is None
101
+ )
102
+
103
+
104
+ class DurableBridgeSessionCoordinator:
105
+ def __init__(self, session_factory: Callable[[], AsyncSession]) -> None:
106
+ self._session_factory = session_factory
107
+ self._operation_abandonment_scan_cursor: DurableBridgeOperationAbandonmentScanCursor | None = None
108
+
109
+ async def lookup_request_targets(
110
+ self,
111
+ *,
112
+ session_key_kind: str,
113
+ session_key_value: str,
114
+ api_key_id: str | None,
115
+ turn_state: str | None,
116
+ session_header: str | None,
117
+ previous_response_id: str | None,
118
+ ) -> DurableBridgeLookup | None:
119
+ api_key_scope = durable_bridge_api_key_scope(api_key_id)
120
+ async with self._session() as session:
121
+ repository = DurableBridgeRepository(session)
122
+ resolved_aliases: list[tuple[str, DurableBridgeSessionSnapshot]] = []
123
+ for alias_kind, alias_value in (
124
+ (_DURABLE_TURN_STATE_ALIAS, turn_state),
125
+ (_DURABLE_PREVIOUS_RESPONSE_ALIAS, previous_response_id),
126
+ (_DURABLE_SESSION_HEADER_ALIAS, session_header),
127
+ ):
128
+ if alias_value is None:
129
+ continue
130
+ snapshot = await repository.resolve_alias(
131
+ alias_kind=alias_kind,
132
+ alias_value=alias_value,
133
+ api_key_scope=api_key_scope,
134
+ )
135
+ if snapshot is not None and not durable_bridge_snapshot_is_detached(snapshot):
136
+ resolved_aliases.append((alias_kind, snapshot))
137
+ resolved_identities = {(snapshot.id, snapshot.account_id) for _alias_kind, snapshot in resolved_aliases}
138
+ resolved_account_ids = {
139
+ snapshot.account_id for _alias_kind, snapshot in resolved_aliases if snapshot.account_id is not None
140
+ }
141
+ has_ownerless_snapshot = any(snapshot.account_id is None for _alias_kind, snapshot in resolved_aliases)
142
+ if len(resolved_identities) > 1:
143
+ same_account_handoff = len(resolved_account_ids) == 1 and not has_ownerless_snapshot
144
+ if same_account_handoff:
145
+ account_id = next(iter(resolved_account_ids))
146
+ same_account_snapshots = [
147
+ snapshot for _alias_kind, snapshot in resolved_aliases if snapshot.account_id == account_id
148
+ ]
149
+ requested_response_snapshot = next(
150
+ (
151
+ snapshot
152
+ for alias_kind, snapshot in resolved_aliases
153
+ if alias_kind == _DURABLE_PREVIOUS_RESPONSE_ALIAS and snapshot.account_id == account_id
154
+ ),
155
+ None,
156
+ )
157
+ account_snapshot = requested_response_snapshot or max(
158
+ same_account_snapshots,
159
+ key=lambda snapshot: (
160
+ snapshot.latest_response_id is not None,
161
+ snapshot.last_seen_at,
162
+ ),
163
+ )
164
+ # Same-account aliases may point at different durable rows
165
+ # during a handoff. Preserve an explicitly requested
166
+ # response anchor; otherwise prefer the newest persisted
167
+ # response anchor rather than alias-resolution order.
168
+ return _to_lookup(account_snapshot)
169
+ specific_aliases = [
170
+ (alias_kind, snapshot)
171
+ for alias_kind, snapshot in resolved_aliases
172
+ if alias_kind != _DURABLE_SESSION_HEADER_ALIAS
173
+ ]
174
+ specific_identities = {(snapshot.id, snapshot.account_id) for _alias_kind, snapshot in specific_aliases}
175
+ if len(specific_identities) == 1:
176
+ specific_snapshot = specific_aliases[0][1]
177
+ specific_identity = (specific_snapshot.id, specific_snapshot.account_id)
178
+ conflicting_alias_kinds = {
179
+ alias_kind
180
+ for alias_kind, snapshot in resolved_aliases
181
+ if (snapshot.id, snapshot.account_id) != specific_identity
182
+ }
183
+ if is_http_bridge_account_neutral_replay(
184
+ kind=specific_snapshot.session_key_kind,
185
+ key=specific_snapshot.session_key_value,
186
+ ) and conflicting_alias_kinds == {_DURABLE_SESSION_HEADER_ALIAS}:
187
+ return _to_lookup(specific_snapshot)
188
+ # Turn-state/response/session aliases are independent hard
189
+ # evidence. Returning the first match would silently discard a
190
+ # conflicting durable owner based on source ordering.
191
+ raise ProxyResponseError(
192
+ 502,
193
+ openai_error(
194
+ "continuity_owner_conflict",
195
+ "Durable continuity aliases resolve to conflicting upstream owners.",
196
+ error_type="server_error",
197
+ ),
198
+ )
199
+ if resolved_aliases:
200
+ return _to_lookup(resolved_aliases[0][1])
201
+ snapshot = await repository.get_session(
202
+ session_key_kind=session_key_kind,
203
+ session_key_value=session_key_value,
204
+ api_key_scope=api_key_scope,
205
+ )
206
+ if snapshot is not None and durable_bridge_snapshot_is_detached(snapshot):
207
+ # The canonical key still maps to a row the account
208
+ # invalidation path detached. Let the request start fresh on
209
+ # a selectable account (the later claim re-owns this row)
210
+ # instead of failing closed on an owner that no longer exists.
211
+ snapshot = None
212
+ if snapshot is None:
213
+ if turn_state is not None:
214
+ snapshot = await repository.find_session_by_latest_turn_state(
215
+ turn_state=turn_state,
216
+ api_key_scope=api_key_scope,
217
+ )
218
+ if snapshot is None and previous_response_id is not None:
219
+ snapshot = await repository.find_session_by_latest_response_id(
220
+ response_id=previous_response_id,
221
+ api_key_scope=api_key_scope,
222
+ )
223
+ if snapshot is None:
224
+ return None
225
+ return _to_lookup(snapshot)
226
+
227
+ async def lookup_turn_state_target(
228
+ self,
229
+ *,
230
+ turn_state: str,
231
+ api_key_id: str | None,
232
+ ) -> DurableBridgeLookup | None:
233
+ """Resolve only a previously registered turn-state continuity anchor."""
234
+
235
+ api_key_scope = durable_bridge_api_key_scope(api_key_id)
236
+ async with self._session() as session:
237
+ repository = DurableBridgeRepository(session)
238
+ snapshot = await repository.resolve_alias(
239
+ alias_kind=_DURABLE_TURN_STATE_ALIAS,
240
+ alias_value=turn_state,
241
+ api_key_scope=api_key_scope,
242
+ )
243
+ return _to_lookup(snapshot) if snapshot is not None else None
244
+
245
+ async def lookup_sessions(self, *, session_ids: Sequence[str]) -> list[DurableBridgeLookup]:
246
+ """Batch-load durable session snapshots for ownership reconciliation."""
247
+
248
+ if not session_ids:
249
+ return []
250
+ async with self._session() as session:
251
+ snapshots = await DurableBridgeRepository(session).get_sessions_by_ids(session_ids)
252
+ return [_to_lookup(snapshot) for snapshot in snapshots]
253
+
254
+ async def lookup_retry_circuit(
255
+ self,
256
+ *,
257
+ session_key_kind: str,
258
+ session_key_value: str,
259
+ api_key_id: str | None,
260
+ ) -> DurableBridgeRetryCircuitSnapshot | None:
261
+ async with self._session() as session:
262
+ return await DurableBridgeRepository(session).get_retry_circuit(
263
+ session_key_kind=session_key_kind,
264
+ session_key_value=session_key_value,
265
+ api_key_scope=durable_bridge_api_key_scope(api_key_id),
266
+ )
267
+
268
+ async def persist_retry_circuit(
269
+ self,
270
+ *,
271
+ session_key_kind: str,
272
+ session_key_value: str,
273
+ api_key_id: str | None,
274
+ consecutive_failures: int,
275
+ cooldown_until_epoch: float,
276
+ last_detail: str | None,
277
+ updated_at_epoch: float,
278
+ base_updated_at_epoch: float = 0.0,
279
+ failure_threshold: int = 1,
280
+ poison_sticky_threshold: int | None = None,
281
+ conflict_cooldown_until_epoch: float | None = None,
282
+ base_backoff_seconds: float = 60.0,
283
+ max_backoff_seconds: float = 600.0,
284
+ clean_close_max_backoff_seconds: float = 30.0,
285
+ ) -> DurableBridgeRetryCircuitSnapshot | None:
286
+ async with self._session() as session:
287
+ repository = DurableBridgeRepository(session)
288
+ await repository.upsert_retry_circuit(
289
+ session_key_kind=session_key_kind,
290
+ session_key_value=session_key_value,
291
+ api_key_scope=durable_bridge_api_key_scope(api_key_id),
292
+ consecutive_failures=consecutive_failures,
293
+ cooldown_until_epoch=cooldown_until_epoch,
294
+ last_detail=last_detail,
295
+ updated_at_epoch=updated_at_epoch,
296
+ base_updated_at_epoch=base_updated_at_epoch,
297
+ failure_threshold=failure_threshold,
298
+ poison_sticky_threshold=poison_sticky_threshold,
299
+ conflict_cooldown_until_epoch=conflict_cooldown_until_epoch,
300
+ base_backoff_seconds=base_backoff_seconds,
301
+ max_backoff_seconds=max_backoff_seconds,
302
+ clean_close_max_backoff_seconds=clean_close_max_backoff_seconds,
303
+ )
304
+ return await repository.get_retry_circuit(
305
+ session_key_kind=session_key_kind,
306
+ session_key_value=session_key_value,
307
+ api_key_scope=durable_bridge_api_key_scope(api_key_id),
308
+ )
309
+
310
+ async def clear_retry_circuit(
311
+ self,
312
+ *,
313
+ session_key_kind: str,
314
+ session_key_value: str,
315
+ api_key_id: str | None,
316
+ expected_updated_at_epoch: float | None = None,
317
+ expected_admission_generation: int | None = None,
318
+ expected_consecutive_failures: int | None = None,
319
+ reset_detail: str | None = None,
320
+ ) -> bool:
321
+ async with self._session() as session:
322
+ return await DurableBridgeRepository(session).delete_retry_circuit(
323
+ session_key_kind=session_key_kind,
324
+ session_key_value=session_key_value,
325
+ api_key_scope=durable_bridge_api_key_scope(api_key_id),
326
+ expected_consecutive_failures=expected_consecutive_failures,
327
+ expected_updated_at_epoch=expected_updated_at_epoch,
328
+ expected_admission_generation=expected_admission_generation,
329
+ reset_detail=reset_detail,
330
+ )
331
+
332
+ async def supersede_retry_circuit_detail(
333
+ self,
334
+ *,
335
+ session_key_kind: str,
336
+ session_key_value: str,
337
+ api_key_id: str | None,
338
+ expected_updated_at_epoch: float,
339
+ expected_consecutive_failures: int,
340
+ expected_last_detail: str | None,
341
+ last_detail: str | None,
342
+ ) -> bool:
343
+ async with self._session() as session:
344
+ return await DurableBridgeRepository(session).supersede_retry_circuit_detail(
345
+ session_key_kind=session_key_kind,
346
+ session_key_value=session_key_value,
347
+ api_key_scope=durable_bridge_api_key_scope(api_key_id),
348
+ expected_updated_at_epoch=expected_updated_at_epoch,
349
+ expected_consecutive_failures=expected_consecutive_failures,
350
+ expected_last_detail=expected_last_detail,
351
+ last_detail=last_detail,
352
+ )
353
+
354
+ async def claim_retry_circuit_generation(
355
+ self,
356
+ *,
357
+ session_key_kind: str,
358
+ session_key_value: str,
359
+ api_key_id: str | None,
360
+ expected_updated_at_epoch: float | None,
361
+ expected_admission_generation: int,
362
+ expected_consecutive_failures: int,
363
+ expected_cooldown_until_epoch: float,
364
+ ) -> DurableBridgeRetryCircuitSnapshot | None:
365
+ async with self._session() as session:
366
+ return await DurableBridgeRepository(session).claim_retry_circuit_generation(
367
+ session_key_kind=session_key_kind,
368
+ session_key_value=session_key_value,
369
+ api_key_scope=durable_bridge_api_key_scope(api_key_id),
370
+ expected_updated_at_epoch=expected_updated_at_epoch,
371
+ expected_admission_generation=expected_admission_generation,
372
+ expected_consecutive_failures=expected_consecutive_failures,
373
+ expected_cooldown_until_epoch=expected_cooldown_until_epoch,
374
+ )
375
+
376
+ async def purge_retry_circuit(
377
+ self,
378
+ *,
379
+ session_key_kind: str,
380
+ session_key_value: str,
381
+ api_key_id: str | None,
382
+ expected_updated_at_epoch: float | None = None,
383
+ expected_admission_generation: int | None = None,
384
+ expected_consecutive_failures: int | None = None,
385
+ fence_last_detail: bool = False,
386
+ expected_last_detail: str | None = None,
387
+ ) -> bool:
388
+ async with self._session() as session:
389
+ return await DurableBridgeRepository(session).purge_retry_circuit(
390
+ session_key_kind=session_key_kind,
391
+ session_key_value=session_key_value,
392
+ api_key_scope=durable_bridge_api_key_scope(api_key_id),
393
+ expected_updated_at_epoch=expected_updated_at_epoch,
394
+ expected_admission_generation=expected_admission_generation,
395
+ expected_consecutive_failures=expected_consecutive_failures,
396
+ fence_last_detail=fence_last_detail,
397
+ expected_last_detail=expected_last_detail,
398
+ )
399
+
400
+ async def claim_live_session(
401
+ self,
402
+ *,
403
+ session_key_kind: str,
404
+ session_key_value: str,
405
+ api_key_id: str | None,
406
+ instance_id: str,
407
+ lease_ttl_seconds: float,
408
+ account_id: str | None,
409
+ model: str | None,
410
+ service_tier: str | None,
411
+ latest_turn_state: str | None,
412
+ latest_response_id: str | None,
413
+ allow_takeover: bool,
414
+ owner_process_epoch: str,
415
+ force_owner_epoch_advance: bool = False,
416
+ ) -> DurableBridgeLookup:
417
+ api_key_scope = durable_bridge_api_key_scope(api_key_id)
418
+ async with self._session() as session:
419
+ snapshot = await DurableBridgeRepository(session).claim_session(
420
+ session_key_kind=session_key_kind,
421
+ session_key_value=session_key_value,
422
+ api_key_scope=api_key_scope,
423
+ instance_id=instance_id,
424
+ lease_ttl_seconds=lease_ttl_seconds,
425
+ account_id=account_id,
426
+ model=model,
427
+ service_tier=service_tier,
428
+ latest_turn_state=latest_turn_state,
429
+ latest_response_id=latest_response_id,
430
+ allow_takeover=allow_takeover,
431
+ owner_process_epoch=owner_process_epoch,
432
+ force_owner_epoch_advance=force_owner_epoch_advance,
433
+ )
434
+ return _to_lookup(snapshot)
435
+
436
+ async def renew_live_session(
437
+ self,
438
+ *,
439
+ session_id: str,
440
+ api_key_id: str | None,
441
+ instance_id: str,
442
+ owner_epoch: int,
443
+ lease_ttl_seconds: float,
444
+ latest_turn_state: str | None = None,
445
+ latest_response_id: str | None = None,
446
+ latest_input_item_count: int | None = None,
447
+ latest_input_full_fingerprint: str | None = None,
448
+ latest_pending_tool_calls: Mapping[str, str] | None = None,
449
+ state: HttpBridgeSessionState | None = None,
450
+ ) -> DurableBridgeLookup | None:
451
+ del api_key_id
452
+ async with self._session() as session:
453
+ snapshot = await DurableBridgeRepository(session).renew_session(
454
+ session_id=session_id,
455
+ instance_id=instance_id,
456
+ owner_epoch=owner_epoch,
457
+ lease_ttl_seconds=lease_ttl_seconds,
458
+ latest_turn_state=latest_turn_state,
459
+ latest_response_id=latest_response_id,
460
+ latest_input_item_count=latest_input_item_count,
461
+ latest_input_full_fingerprint=latest_input_full_fingerprint,
462
+ latest_pending_tool_calls=latest_pending_tool_calls,
463
+ state=state,
464
+ )
465
+ if snapshot is None:
466
+ return None
467
+ return _to_lookup(snapshot)
468
+
469
+ async def rebind_session_account(
470
+ self,
471
+ *,
472
+ session_id: str,
473
+ api_key_id: str | None,
474
+ instance_id: str,
475
+ owner_epoch: int,
476
+ account_id: str,
477
+ clear_continuity: bool = False,
478
+ expected_latest_response_id: object = REBIND_ANCHOR_UNFENCED,
479
+ expected_latest_turn_state: object = REBIND_ANCHOR_UNFENCED,
480
+ ) -> bool:
481
+ del api_key_id
482
+ async with self._session() as session:
483
+ return await DurableBridgeRepository(session).rebind_session_account(
484
+ session_id=session_id,
485
+ instance_id=instance_id,
486
+ owner_epoch=owner_epoch,
487
+ account_id=account_id,
488
+ clear_continuity=clear_continuity,
489
+ expected_latest_response_id=expected_latest_response_id,
490
+ expected_latest_turn_state=expected_latest_turn_state,
491
+ )
492
+
493
+ async def session_latest_continuity(self, *, session_id: str) -> tuple[str | None, str | None] | None:
494
+ """Read the session's current continuity anchors for a fenced clear."""
495
+ async with self._session() as session:
496
+ return await DurableBridgeRepository(session).latest_session_continuity(session_id=session_id)
497
+
498
+ async def release_live_session(
499
+ self,
500
+ *,
501
+ session_id: str,
502
+ instance_id: str,
503
+ owner_epoch: int,
504
+ draining: bool,
505
+ ) -> DurableBridgeLookup | None:
506
+ async with self._session() as session:
507
+ snapshot = await DurableBridgeRepository(session).release_session(
508
+ session_id=session_id,
509
+ instance_id=instance_id,
510
+ owner_epoch=owner_epoch,
511
+ draining=draining,
512
+ )
513
+ if snapshot is None:
514
+ return None
515
+ return _to_lookup(snapshot)
516
+
517
+ async def clear_live_session_response_anchor(
518
+ self,
519
+ *,
520
+ session_id: str,
521
+ instance_id: str,
522
+ owner_epoch: int,
523
+ ) -> DurableBridgeLookup | None:
524
+ async with self._session() as session:
525
+ snapshot = await DurableBridgeRepository(session).clear_latest_response_anchor(
526
+ session_id=session_id,
527
+ instance_id=instance_id,
528
+ owner_epoch=owner_epoch,
529
+ )
530
+ if snapshot is None:
531
+ return None
532
+ return _to_lookup(snapshot)
533
+
534
+ async def clear_live_session_response_anchor_if_matches(
535
+ self,
536
+ *,
537
+ session_id: str,
538
+ api_key_id: str | None,
539
+ instance_id: str,
540
+ owner_epoch: int,
541
+ response_id: str,
542
+ ) -> DurableBridgeLookup | None:
543
+ async with self._session() as session:
544
+ snapshot = await DurableBridgeRepository(session).clear_latest_response_anchor_if_matches(
545
+ session_id=session_id,
546
+ api_key_scope=durable_bridge_api_key_scope(api_key_id),
547
+ instance_id=instance_id,
548
+ owner_epoch=owner_epoch,
549
+ response_id=response_id,
550
+ )
551
+ if snapshot is None:
552
+ return None
553
+ return _to_lookup(snapshot)
554
+
555
+ async def record_recovery_attempt(
556
+ self,
557
+ *,
558
+ session_id: str,
559
+ api_key_id: str | None,
560
+ instance_id: str,
561
+ owner_epoch: int,
562
+ request_fingerprint: str,
563
+ request_id: str,
564
+ account_id: str | None,
565
+ model: str | None,
566
+ replay_safe: bool,
567
+ ) -> DurableBridgeRecoveryAttemptSnapshot | None:
568
+ del api_key_id
569
+ async with self._session() as session:
570
+ return await DurableBridgeRepository(session).record_recovery_attempt(
571
+ session_id=session_id,
572
+ instance_id=instance_id,
573
+ owner_epoch=owner_epoch,
574
+ request_fingerprint=request_fingerprint,
575
+ request_id=request_id,
576
+ account_id=account_id,
577
+ model=model,
578
+ replay_safe=replay_safe,
579
+ )
580
+
581
+ async def lookup_recovery_attempt(
582
+ self,
583
+ *,
584
+ session_id: str,
585
+ request_fingerprint: str,
586
+ ) -> DurableBridgeRecoveryAttemptSnapshot | None:
587
+ async with self._session() as session:
588
+ return await DurableBridgeRepository(session).lookup_recovery_attempt(
589
+ session_id=session_id,
590
+ request_fingerprint=request_fingerprint,
591
+ )
592
+
593
+ async def mark_recovery_attempt_replayed(
594
+ self,
595
+ *,
596
+ session_id: str,
597
+ api_key_id: str | None,
598
+ instance_id: str,
599
+ owner_epoch: int,
600
+ request_fingerprint: str,
601
+ response_id: str | None = None,
602
+ ) -> bool:
603
+ del api_key_id
604
+ async with self._session() as session:
605
+ return await DurableBridgeRepository(session).mark_recovery_attempt_replayed(
606
+ session_id=session_id,
607
+ instance_id=instance_id,
608
+ owner_epoch=owner_epoch,
609
+ request_fingerprint=request_fingerprint,
610
+ response_id=response_id,
611
+ )
612
+
613
+ async def rollback_recovery_attempt_replayed(
614
+ self,
615
+ *,
616
+ session_id: str,
617
+ api_key_id: str | None,
618
+ instance_id: str,
619
+ owner_epoch: int,
620
+ request_fingerprint: str,
621
+ ) -> bool:
622
+ del api_key_id
623
+ async with self._session() as session:
624
+ return await DurableBridgeRepository(session).rollback_recovery_attempt_replayed(
625
+ session_id=session_id,
626
+ instance_id=instance_id,
627
+ owner_epoch=owner_epoch,
628
+ request_fingerprint=request_fingerprint,
629
+ )
630
+
631
+ async def rollback_recovery_attempt_before_dispatch(
632
+ self,
633
+ *,
634
+ session_id: str,
635
+ api_key_id: str | None,
636
+ instance_id: str,
637
+ owner_epoch: int,
638
+ request_fingerprint: str,
639
+ ) -> bool:
640
+ del api_key_id
641
+ async with self._session() as session:
642
+ return await DurableBridgeRepository(session).rollback_recovery_attempt_before_dispatch(
643
+ session_id=session_id,
644
+ instance_id=instance_id,
645
+ owner_epoch=owner_epoch,
646
+ request_fingerprint=request_fingerprint,
647
+ )
648
+
649
+ async def record_operation(
650
+ self,
651
+ *,
652
+ operation_id: str,
653
+ session_id: str,
654
+ instance_id: str,
655
+ owner_epoch: int,
656
+ request_fingerprint: str,
657
+ account_id: str | None,
658
+ model: str | None,
659
+ parent_response_id: str | None,
660
+ api_key_scope: str | None = None,
661
+ request_text: str | None = None,
662
+ recovery_attempt_session_id: str | None = None,
663
+ recovery_attempt_owner_epoch: int | None = None,
664
+ recovery_attempt_fingerprint: str | None = None,
665
+ recovery_attempt_consumed: bool = False,
666
+ ) -> DurableBridgeOperationSnapshot | None:
667
+ async with self._session() as session:
668
+ return await DurableBridgeRepository(session).record_operation(
669
+ operation_id=operation_id,
670
+ session_id=session_id,
671
+ instance_id=instance_id,
672
+ owner_epoch=owner_epoch,
673
+ request_fingerprint=request_fingerprint,
674
+ api_key_scope=api_key_scope,
675
+ account_id=account_id,
676
+ model=model,
677
+ parent_response_id=parent_response_id,
678
+ request_text=request_text,
679
+ recovery_attempt_session_id=recovery_attempt_session_id,
680
+ recovery_attempt_owner_epoch=recovery_attempt_owner_epoch,
681
+ recovery_attempt_fingerprint=recovery_attempt_fingerprint,
682
+ recovery_attempt_consumed=recovery_attempt_consumed,
683
+ )
684
+
685
+ async def get_operation_events(self, *, operation_id: str) -> list[str]:
686
+ async with self._session() as session:
687
+ return await DurableBridgeRepository(session).get_operation_events(
688
+ operation_id=operation_id,
689
+ max_bytes=int(
690
+ getattr(
691
+ get_settings(), "http_responses_session_bridge_operation_event_spool_max_bytes", 2 * 1024 * 1024
692
+ )
693
+ ),
694
+ )
695
+
696
+ async def abandon_stale_operations(
697
+ self,
698
+ *,
699
+ cutoff: datetime,
700
+ lease_expired_before: datetime,
701
+ protected_operation_ids: Collection[str] = (),
702
+ batch_size: int = 500,
703
+ ) -> list[DurableBridgeOperationAbandonment]:
704
+ async with self._session() as session:
705
+ sweep = await DurableBridgeRepository(session).abandon_stale_operations(
706
+ cutoff=cutoff,
707
+ lease_expired_before=lease_expired_before,
708
+ protected_operation_ids=protected_operation_ids,
709
+ batch_size=batch_size,
710
+ scan_cursor=self._operation_abandonment_scan_cursor,
711
+ )
712
+ self._operation_abandonment_scan_cursor = sweep.next_cursor
713
+ return list(sweep.abandonments)
714
+
715
+ async def get_replayable_transcript(
716
+ self,
717
+ *,
718
+ response_id: str,
719
+ max_turns: int = 128,
720
+ max_bytes: int = 8 * 1024 * 1024,
721
+ ) -> list[DurableBridgeTranscriptTurn] | None:
722
+ async with self._session() as session:
723
+ return await DurableBridgeRepository(session).get_replayable_transcript(
724
+ response_id=response_id,
725
+ max_turns=max_turns,
726
+ max_bytes=max_bytes,
727
+ )
728
+
729
+ async def purge_operation_spool(
730
+ self,
731
+ *,
732
+ cutoff: datetime,
733
+ batch_size: int = DURABLE_BRIDGE_OPERATION_SPOOL_PURGE_BATCH_SIZE,
734
+ ) -> int:
735
+ return (await self.purge_operation_spool_batch(cutoff=cutoff, batch_size=batch_size)).deleted_operations
736
+
737
+ async def purge_operation_spool_batch(
738
+ self,
739
+ *,
740
+ cutoff: datetime,
741
+ batch_size: int = DURABLE_BRIDGE_OPERATION_SPOOL_PURGE_BATCH_SIZE,
742
+ ) -> DurableBridgeOperationPurgeBatchResult:
743
+ async with self._session() as session:
744
+ return await DurableBridgeRepository(session).purge_operation_spool_batch(
745
+ cutoff=cutoff,
746
+ batch_size=batch_size,
747
+ )
748
+
749
+ async def append_operation_event(
750
+ self,
751
+ *,
752
+ operation_id: str,
753
+ session_id: str,
754
+ instance_id: str,
755
+ owner_epoch: int,
756
+ event_text: str,
757
+ max_bytes: int,
758
+ ) -> bool:
759
+ async with self._session() as session:
760
+ return await DurableBridgeRepository(session).append_operation_event(
761
+ operation_id=operation_id,
762
+ session_id=session_id,
763
+ instance_id=instance_id,
764
+ owner_epoch=owner_epoch,
765
+ event_text=event_text,
766
+ max_bytes=max_bytes,
767
+ )
768
+
769
+ async def append_terminal_operation_event(
770
+ self,
771
+ *,
772
+ operation_id: str,
773
+ session_id: str,
774
+ instance_id: str,
775
+ owner_epoch: int,
776
+ event_text: str,
777
+ max_bytes: int,
778
+ state: str,
779
+ expected_recovery_dispatch_count: int | None = None,
780
+ response_id: str | None = None,
781
+ complete_spool: bool = True,
782
+ ) -> bool:
783
+ async with self._session() as session:
784
+ return await DurableBridgeRepository(session).append_terminal_operation_event(
785
+ operation_id=operation_id,
786
+ session_id=session_id,
787
+ instance_id=instance_id,
788
+ owner_epoch=owner_epoch,
789
+ event_text=event_text,
790
+ max_bytes=max_bytes,
791
+ state=state,
792
+ expected_recovery_dispatch_count=expected_recovery_dispatch_count,
793
+ response_id=response_id,
794
+ complete_spool=complete_spool,
795
+ )
796
+
797
+ async def append_operation_events(
798
+ self,
799
+ *,
800
+ events: Sequence[DurableBridgeOperationEventInput],
801
+ max_bytes: int,
802
+ ) -> bool:
803
+ async with self._session() as session:
804
+ return await DurableBridgeRepository(session).append_operation_events(
805
+ events=events,
806
+ max_bytes=max_bytes,
807
+ )
808
+
809
+ async def append_operation_event_chunk(
810
+ self,
811
+ *,
812
+ events: Sequence[DurableBridgeOperationEventInput],
813
+ max_bytes: int,
814
+ ) -> bool:
815
+ async with self._session() as session:
816
+ return await DurableBridgeRepository(session).append_operation_event_chunk(
817
+ events=events,
818
+ max_bytes=max_bytes,
819
+ )
820
+
821
+ async def append_terminal_operation_chunk(
822
+ self,
823
+ *,
824
+ operation_id: str,
825
+ session_id: str,
826
+ instance_id: str,
827
+ owner_epoch: int,
828
+ event_text: str,
829
+ max_bytes: int,
830
+ state: str,
831
+ expected_recovery_dispatch_count: int | None = None,
832
+ response_id: str | None = None,
833
+ complete_spool: bool = True,
834
+ ) -> bool:
835
+ async with self._session() as session:
836
+ return await DurableBridgeRepository(session).append_terminal_operation_chunk(
837
+ operation_id=operation_id,
838
+ session_id=session_id,
839
+ instance_id=instance_id,
840
+ owner_epoch=owner_epoch,
841
+ event_text=event_text,
842
+ max_bytes=max_bytes,
843
+ state=state,
844
+ expected_recovery_dispatch_count=expected_recovery_dispatch_count,
845
+ response_id=response_id,
846
+ complete_spool=complete_spool,
847
+ )
848
+
849
+ async def finalize_operation_event_spool(
850
+ self,
851
+ *,
852
+ operation_id: str,
853
+ session_id: str,
854
+ instance_id: str,
855
+ owner_epoch: int,
856
+ expected_state: str | None = None,
857
+ ) -> bool:
858
+ async with self._session() as session:
859
+ return await DurableBridgeRepository(session).finalize_operation_event_spool(
860
+ operation_id=operation_id,
861
+ session_id=session_id,
862
+ instance_id=instance_id,
863
+ owner_epoch=owner_epoch,
864
+ expected_state=expected_state,
865
+ )
866
+
867
+ async def settle_terminal_append_failure(
868
+ self,
869
+ *,
870
+ operation_id: str,
871
+ session_id: str,
872
+ instance_id: str,
873
+ owner_epoch: int,
874
+ state: str,
875
+ expected_response_id: str | None,
876
+ expected_recovery_dispatch_count: int | None = None,
877
+ alternate_expected_response_id: str | None = None,
878
+ response_id: str | None = None,
879
+ ) -> bool:
880
+ async with self._session() as session:
881
+ return await DurableBridgeRepository(session).settle_terminal_append_failure(
882
+ operation_id=operation_id,
883
+ session_id=session_id,
884
+ instance_id=instance_id,
885
+ owner_epoch=owner_epoch,
886
+ state=state,
887
+ expected_response_id=expected_response_id,
888
+ expected_recovery_dispatch_count=expected_recovery_dispatch_count,
889
+ alternate_expected_response_id=alternate_expected_response_id,
890
+ response_id=response_id,
891
+ )
892
+
893
+ async def update_operation(
894
+ self,
895
+ *,
896
+ operation_id: str,
897
+ session_id: str,
898
+ instance_id: str,
899
+ owner_epoch: int,
900
+ state: str,
901
+ response_id: str | None = None,
902
+ ) -> bool:
903
+ async with self._session() as session:
904
+ return await DurableBridgeRepository(session).update_operation(
905
+ operation_id=operation_id,
906
+ session_id=session_id,
907
+ instance_id=instance_id,
908
+ owner_epoch=owner_epoch,
909
+ state=state,
910
+ response_id=response_id,
911
+ )
912
+
913
+ async def get_operation(self, *, operation_id: str) -> DurableBridgeOperationSnapshot | None:
914
+ async with self._session() as session:
915
+ return await DurableBridgeRepository(session).get_operation(operation_id=operation_id)
916
+
917
+ async def reset_operation_event_spool(
918
+ self,
919
+ *,
920
+ operation_id: str,
921
+ session_id: str,
922
+ instance_id: str,
923
+ owner_epoch: int,
924
+ ) -> bool:
925
+ async with self._session() as session:
926
+ return await DurableBridgeRepository(session).reset_operation_event_spool(
927
+ operation_id=operation_id,
928
+ session_id=session_id,
929
+ instance_id=instance_id,
930
+ owner_epoch=owner_epoch,
931
+ )
932
+
933
+ async def claim_unknown_operation_for_recovery(
934
+ self,
935
+ *,
936
+ operation_id: str,
937
+ session_id: str,
938
+ instance_id: str,
939
+ owner_epoch: int,
940
+ max_recovery_dispatches: int | None = None,
941
+ ) -> bool:
942
+ async with self._session() as session:
943
+ return await DurableBridgeRepository(session).claim_unknown_operation_for_recovery(
944
+ operation_id=operation_id,
945
+ session_id=session_id,
946
+ instance_id=instance_id,
947
+ owner_epoch=owner_epoch,
948
+ max_recovery_dispatches=max_recovery_dispatches,
949
+ )
950
+
951
+ async def mark_operation_unknown(
952
+ self,
953
+ *,
954
+ operation_id: str,
955
+ session_id: str,
956
+ instance_id: str,
957
+ owner_epoch: int,
958
+ restore_recovery_dispatch_claim: bool = False,
959
+ ) -> bool:
960
+ async with self._session() as session:
961
+ return await DurableBridgeRepository(session).mark_operation_unknown(
962
+ operation_id=operation_id,
963
+ session_id=session_id,
964
+ instance_id=instance_id,
965
+ owner_epoch=owner_epoch,
966
+ restore_recovery_dispatch_claim=restore_recovery_dispatch_claim,
967
+ )
968
+
969
+ async def rollback_operation_before_dispatch(
970
+ self,
971
+ *,
972
+ operation_id: str,
973
+ session_id: str,
974
+ instance_id: str,
975
+ owner_epoch: int,
976
+ restore_rebound: bool = False,
977
+ rebound_from_session_id: str | None = None,
978
+ rebound_from_account_id: str | None = None,
979
+ rebound_from_model: str | None = None,
980
+ rebound_from_parent_response_id: str | None = None,
981
+ ) -> bool:
982
+ async with self._session() as session:
983
+ return await DurableBridgeRepository(session).rollback_operation_before_dispatch(
984
+ operation_id=operation_id,
985
+ session_id=session_id,
986
+ instance_id=instance_id,
987
+ owner_epoch=owner_epoch,
988
+ restore_rebound=restore_rebound,
989
+ rebound_from_session_id=rebound_from_session_id,
990
+ rebound_from_account_id=rebound_from_account_id,
991
+ rebound_from_model=rebound_from_model,
992
+ rebound_from_parent_response_id=rebound_from_parent_response_id,
993
+ )
994
+
995
+ async def get_operation_by_fingerprint(
996
+ self,
997
+ *,
998
+ request_fingerprint: str,
999
+ api_key_scope: str | None = None,
1000
+ ) -> DurableBridgeOperationSnapshot | None:
1001
+ async with self._session() as session:
1002
+ return await DurableBridgeRepository(session).get_operation_by_fingerprint(
1003
+ request_fingerprint=request_fingerprint,
1004
+ api_key_scope=api_key_scope,
1005
+ )
1006
+
1007
+ async def get_latest_completed_operation(
1008
+ self,
1009
+ *,
1010
+ session_id: str,
1011
+ parent_response_id: str,
1012
+ request_fingerprint: str | None = None,
1013
+ ) -> DurableBridgeOperationSnapshot | None:
1014
+ async with self._session() as session:
1015
+ return await DurableBridgeRepository(session).get_latest_completed_operation(
1016
+ session_id=session_id,
1017
+ parent_response_id=parent_response_id,
1018
+ request_fingerprint=request_fingerprint,
1019
+ )
1020
+
1021
+ async def get_latest_completed_operation_any_session(
1022
+ self,
1023
+ *,
1024
+ parent_response_id: str,
1025
+ api_key_scope: str | None = None,
1026
+ request_fingerprint: str | None = None,
1027
+ ) -> DurableBridgeOperationSnapshot | None:
1028
+ async with self._session() as session:
1029
+ return await DurableBridgeRepository(session).get_latest_completed_operation_any_session(
1030
+ parent_response_id=parent_response_id,
1031
+ api_key_scope=api_key_scope,
1032
+ request_fingerprint=request_fingerprint,
1033
+ )
1034
+
1035
+ async def retire_continuity_owner_if_unavailable(
1036
+ self,
1037
+ *,
1038
+ session_id: str,
1039
+ expected_account_id: str,
1040
+ recovery_deadline_epoch: int,
1041
+ ) -> bool:
1042
+ """Retire a row's owner now when it cannot return before the deadline."""
1043
+ async with self._session() as session:
1044
+ return await DurableBridgeRepository(session).retire_continuity_owner_if_unavailable(
1045
+ session_id,
1046
+ expected_account_id=expected_account_id,
1047
+ recovery_deadline_epoch=recovery_deadline_epoch,
1048
+ )
1049
+
1050
+ async def mark_instance_draining(self, *, instance_id: str) -> int:
1051
+ async with self._session() as session:
1052
+ return await DurableBridgeRepository(session).mark_owner_draining(instance_id=instance_id)
1053
+
1054
+ async def purge_owned_sessions_on_startup(
1055
+ self,
1056
+ *,
1057
+ instance_id: str,
1058
+ owner_process_epoch: str | None = None,
1059
+ ownerless_cutoff: datetime | None = None,
1060
+ ) -> int:
1061
+ async with self._session() as session:
1062
+ return await DurableBridgeRepository(session).purge_owned_sessions_on_startup(
1063
+ instance_id=instance_id,
1064
+ owner_process_epoch=owner_process_epoch,
1065
+ ownerless_cutoff=ownerless_cutoff,
1066
+ )
1067
+
1068
+ async def register_turn_state(
1069
+ self,
1070
+ *,
1071
+ session_id: str,
1072
+ api_key_id: str | None,
1073
+ instance_id: str,
1074
+ owner_epoch: int,
1075
+ turn_state: str,
1076
+ lease_ttl_seconds: float,
1077
+ ) -> DurableBridgeAliasRegistration:
1078
+ api_key_scope = durable_bridge_api_key_scope(api_key_id)
1079
+ async with self._session() as session:
1080
+ return await DurableBridgeRepository(session).register_owned_alias(
1081
+ session_id=session_id,
1082
+ api_key_scope=api_key_scope,
1083
+ instance_id=instance_id,
1084
+ owner_epoch=owner_epoch,
1085
+ alias_kind=_DURABLE_TURN_STATE_ALIAS,
1086
+ alias_value=turn_state,
1087
+ lease_ttl_seconds=lease_ttl_seconds,
1088
+ latest_turn_state=turn_state,
1089
+ )
1090
+
1091
+ async def register_recovery_turn_state(
1092
+ self,
1093
+ *,
1094
+ session_id: str,
1095
+ api_key_id: str | None,
1096
+ instance_id: str,
1097
+ owner_epoch: int,
1098
+ turn_state: str,
1099
+ lease_ttl_seconds: float,
1100
+ ) -> DurableBridgeAliasRegistrationReceipt:
1101
+ api_key_scope = durable_bridge_api_key_scope(api_key_id)
1102
+ async with self._session() as session:
1103
+ return await DurableBridgeRepository(session).register_reversible_turn_state_alias(
1104
+ session_id=session_id,
1105
+ api_key_scope=api_key_scope,
1106
+ instance_id=instance_id,
1107
+ owner_epoch=owner_epoch,
1108
+ turn_state=turn_state,
1109
+ lease_ttl_seconds=lease_ttl_seconds,
1110
+ )
1111
+
1112
+ async def rollback_recovery_turn_state_registration(
1113
+ self,
1114
+ *,
1115
+ receipt: DurableBridgeAliasRegistrationReceipt,
1116
+ ) -> bool:
1117
+ async with self._session() as session:
1118
+ return await DurableBridgeRepository(session).rollback_reversible_turn_state_alias(
1119
+ receipt=receipt,
1120
+ )
1121
+
1122
+ async def register_previous_response_id(
1123
+ self,
1124
+ *,
1125
+ session_id: str,
1126
+ api_key_id: str | None,
1127
+ instance_id: str,
1128
+ owner_epoch: int,
1129
+ response_id: str,
1130
+ lease_ttl_seconds: float,
1131
+ input_item_count: int | None = None,
1132
+ input_full_fingerprint: str | None = None,
1133
+ pending_tool_calls: Mapping[str, str] | None = None,
1134
+ ) -> DurableBridgeAliasRegistration:
1135
+ api_key_scope = durable_bridge_api_key_scope(api_key_id)
1136
+ async with self._session() as session:
1137
+ return await DurableBridgeRepository(session).register_owned_alias(
1138
+ session_id=session_id,
1139
+ api_key_scope=api_key_scope,
1140
+ instance_id=instance_id,
1141
+ owner_epoch=owner_epoch,
1142
+ alias_kind=_DURABLE_PREVIOUS_RESPONSE_ALIAS,
1143
+ alias_value=response_id,
1144
+ lease_ttl_seconds=lease_ttl_seconds,
1145
+ latest_response_id=response_id,
1146
+ latest_input_item_count=input_item_count,
1147
+ latest_input_full_fingerprint=input_full_fingerprint,
1148
+ latest_pending_tool_calls=pending_tool_calls,
1149
+ )
1150
+
1151
+ async def register_session_header(
1152
+ self,
1153
+ *,
1154
+ session_id: str,
1155
+ api_key_id: str | None,
1156
+ session_header: str,
1157
+ ) -> None:
1158
+ api_key_scope = durable_bridge_api_key_scope(api_key_id)
1159
+ async with self._session() as session:
1160
+ await DurableBridgeRepository(session).upsert_alias(
1161
+ session_id=session_id,
1162
+ alias_kind=_DURABLE_SESSION_HEADER_ALIAS,
1163
+ alias_value=session_header,
1164
+ api_key_scope=api_key_scope,
1165
+ )
1166
+
1167
+ @asynccontextmanager
1168
+ async def _session(self) -> AsyncIterator[AsyncSession]:
1169
+ session = self._session_factory()
1170
+ try:
1171
+ yield session
1172
+ finally:
1173
+ await close_session(session)
1174
+
1175
+
1176
+ def _to_lookup(snapshot: DurableBridgeSessionSnapshot) -> DurableBridgeLookup:
1177
+ # Every lookup path funnels through here, so retirement is applied once
1178
+ # rather than at each call site — the detached-row filter above had to be
1179
+ # repeated and ended up covering only two of the four paths.
1180
+ #
1181
+ # A retired row keeps its identity (callers still need the session id and
1182
+ # canonical key to claim it) but surrenders every piece of continuity
1183
+ # evidence: the owner, the response anchor and the turn state. Dropping
1184
+ # the anchors alongside the owner is what makes the retirement coherent —
1185
+ # an anchor without its account is upstream state no replacement account
1186
+ # can read, and leaving it would have the next request inject a pointer
1187
+ # that only the retired owner could resolve.
1188
+ retired = snapshot.continuity_abandoned
1189
+ return DurableBridgeLookup(
1190
+ session_id=snapshot.id,
1191
+ canonical_kind=snapshot.session_key_kind,
1192
+ canonical_key=snapshot.session_key_value,
1193
+ api_key_scope=snapshot.api_key_scope,
1194
+ account_id=None if retired else snapshot.account_id,
1195
+ # The owning replica goes with the owner. ``_durable_bridge_lookup_active_owner``
1196
+ # would otherwise still name it and forward the request there, and the
1197
+ # takeover gates would treat the row as live. The sweep only retires
1198
+ # rows that have been idle for the whole grace window, so no lease can
1199
+ # realistically still be running — but leaving these set makes the
1200
+ # invariant depend on the lease TTL staying below the grace window,
1201
+ # which is a setting nobody would think to check before changing.
1202
+ owner_instance_id=None if retired else snapshot.owner_instance_id,
1203
+ owner_process_epoch=None if retired else snapshot.owner_process_epoch,
1204
+ # The epoch is fencing state, not continuity evidence: a later claim
1205
+ # must still advance past it, so it is preserved.
1206
+ owner_epoch=snapshot.owner_epoch,
1207
+ lease_expires_at=None if retired else snapshot.lease_expires_at,
1208
+ state=snapshot.state,
1209
+ latest_turn_state=None if retired else snapshot.latest_turn_state,
1210
+ latest_response_id=None if retired else snapshot.latest_response_id,
1211
+ latest_input_item_count=None if retired else snapshot.latest_input_item_count,
1212
+ latest_input_full_fingerprint=None if retired else snapshot.latest_input_full_fingerprint,
1213
+ model=snapshot.model,
1214
+ latest_pending_tool_calls=None if retired else snapshot.latest_pending_tool_calls,
1215
+ continuity_abandoned=retired,
1216
+ retired_account_id=snapshot.abandoned_account_id,
1217
+ )