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,2435 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import inspect
5
+ import logging
6
+ from collections import deque
7
+ from collections.abc import Collection
8
+ from dataclasses import replace
9
+ from typing import Any, Literal, TypeVar, overload
10
+ from uuid import uuid4
11
+
12
+ import aiohttp
13
+
14
+ from app.core import shutdown as shutdown_state
15
+ from app.core.auth.refresh import RefreshError
16
+ from app.core.clients.files import create_file as core_create_file # noqa: F401
17
+ from app.core.clients.files import finalize_file as core_finalize_file # noqa: F401
18
+ from app.core.clients.proxy import CodexControlResponse as CodexControlResponse
19
+ from app.core.clients.proxy import ( # noqa: F401 # noqa: F401
20
+ ImageFetchSession,
21
+ ProxyResponseError,
22
+ UpstreamProxyRouteTrace,
23
+ _as_image_fetch_session,
24
+ _inline_content_images,
25
+ _inline_input_image_urls,
26
+ _ws_transport_payload_budget_bytes,
27
+ filter_inbound_headers,
28
+ pop_compact_timeout_overrides,
29
+ pop_stream_timeout_overrides,
30
+ pop_transcribe_timeout_overrides,
31
+ push_compact_timeout_overrides,
32
+ push_stream_timeout_overrides,
33
+ push_transcribe_timeout_overrides,
34
+ )
35
+ from app.core.clients.proxy import codex_control_request as core_codex_control_request # noqa: F401
36
+ from app.core.clients.proxy import compact_responses as core_compact_responses # noqa: F401
37
+ from app.core.clients.proxy import transcribe_audio as core_transcribe_audio # noqa: F401
38
+ from app.core.clock import clock_for, scheduler_for
39
+ from app.core.errors import openai_error
40
+ from app.core.metrics.prometheus import (
41
+ PROMETHEUS_AVAILABLE,
42
+ bridge_durable_recover_total,
43
+ bridge_local_rebind_total,
44
+ bridge_owner_mismatch_total,
45
+ bridge_prompt_cache_locality_miss_total,
46
+ bridge_soft_local_rebind_total,
47
+ )
48
+ from app.core.utils.locks import fast_lock
49
+ from app.core.utils.request_id import ensure_request_scope_id
50
+ from app.db.models import (
51
+ DashboardSettings,
52
+ StickySessionKind,
53
+ )
54
+ from app.modules.api_keys.service import (
55
+ ApiKeyData,
56
+ ApiKeyRequestUsageBudget,
57
+ )
58
+ from app.modules.proxy._service.api_key_usage import (
59
+ _API_KEY_RESERVATION_HEARTBEAT_SECONDS as _API_KEY_RESERVATION_HEARTBEAT_SECONDS,
60
+ )
61
+ from app.modules.proxy._service.compact import (
62
+ _sticky_key_for_compact_request as _sticky_key_for_compact_request,
63
+ )
64
+ from app.modules.proxy._service.compact import (
65
+ _sticky_key_from_compact_payload as _sticky_key_from_compact_payload,
66
+ )
67
+ from app.modules.proxy._service.http_bridge.account_sessions import _HTTPBridgeAccountSessionsMixin
68
+ from app.modules.proxy._service.http_bridge.activity import _HTTPBridgeActivityMixin
69
+ from app.modules.proxy._service.http_bridge.helpers import (
70
+ _HTTP_BRIDGE_BACKGROUND_CLOSE_TIMEOUT_SECONDS,
71
+ _HTTP_BRIDGE_INFLIGHT_STARTED_AT_ATTR,
72
+ _active_http_bridge_instance_ring,
73
+ _alias_fallback_key,
74
+ _durable_bridge_lookup_active_owner,
75
+ _durable_bridge_lookup_allows_local_reuse,
76
+ _forwarded_http_bridge_session_key,
77
+ _http_bridge_alias_target_is_stale,
78
+ _http_bridge_allow_durable_takeover,
79
+ _http_bridge_can_local_recover_without_ring,
80
+ _http_bridge_can_recover_during_drain,
81
+ _http_bridge_can_single_instance_owner_takeover_without_anchor,
82
+ _http_bridge_can_single_instance_prompt_cache_takeover_without_anchor,
83
+ _http_bridge_capacity_after_planned_closes,
84
+ _http_bridge_claim_allows_takeover,
85
+ _http_bridge_compatible,
86
+ _http_bridge_continuity_lost_error_envelope,
87
+ _http_bridge_endpoint_matches_current_instance,
88
+ _http_bridge_has_durable_recovery_anchor,
89
+ _http_bridge_incompatible_model_fork_key,
90
+ _http_bridge_inflight_creation_count,
91
+ _http_bridge_key_strength,
92
+ _http_bridge_locally_owned_fork_key,
93
+ _http_bridge_models_compatible,
94
+ _http_bridge_owner_check_required,
95
+ _http_bridge_owner_instance,
96
+ _http_bridge_owner_lookup_unavailable_error_envelope,
97
+ _http_bridge_parallel_fork_key,
98
+ _http_bridge_previous_response_alias_key,
99
+ _http_bridge_previous_response_owner_unavailable_error,
100
+ _http_bridge_reconnect_connect_failure,
101
+ _http_bridge_reconnect_selection_failure,
102
+ _http_bridge_reconnect_turn_state,
103
+ _http_bridge_request_budget_seconds,
104
+ _http_bridge_request_needs_unanchored_handoff,
105
+ _http_bridge_session_account_active,
106
+ _http_bridge_session_allows_api_key,
107
+ _http_bridge_session_generation_count,
108
+ _http_bridge_session_has_admission_waiter,
109
+ _http_bridge_session_matches_preferred_account,
110
+ _http_bridge_session_retiring_with_visible_requests,
111
+ _http_bridge_session_reusable_for_lookup,
112
+ _http_bridge_session_reusable_for_request,
113
+ _http_bridge_should_wait_for_registration,
114
+ _http_bridge_startup_wait_timeout_error,
115
+ _http_bridge_turn_state_alias_key,
116
+ _log_http_bridge_event,
117
+ _log_http_bridge_startup_wait_timeout,
118
+ _mark_http_bridge_reader_handoff_reconnect_failed,
119
+ _persist_http_bridge_replacement_account,
120
+ _persistent_http_bridge_affinity,
121
+ _plan_http_bridge_lru_capacity_closes,
122
+ _raise_if_http_bridge_creation_superseded,
123
+ _record_bridge_drain_recovery_allowed,
124
+ _record_bridge_first_turn_timeout,
125
+ _refresh_reused_http_bridge_session_with_handoff,
126
+ _register_http_bridge_turn_state_aliases_locked,
127
+ _require_http_bridge_bound_account_not_excluded,
128
+ _reserve_http_bridge_unanchored_handoff,
129
+ _settle_failed_http_bridge_creation,
130
+ _turn_keys,
131
+ )
132
+ from app.modules.proxy._service.http_bridge.helpers import (
133
+ _close_http_bridge_session as _helpers_close_http_bridge_session,
134
+ )
135
+ from app.modules.proxy._service.http_bridge.owner_forwarding import _HTTPBridgeOwnerForwardingMixin
136
+ from app.modules.proxy._service.http_bridge.protocol import _HTTPBridgeServiceProtocol
137
+ from app.modules.proxy._service.http_bridge.proxy_failover import _HTTPBridgePreDispatchFailover
138
+ from app.modules.proxy._service.http_bridge.quarantine import (
139
+ _http_bridge_session_key_quarantined,
140
+ )
141
+ from app.modules.proxy._service.http_bridge.request_submit import _HTTPBridgeRequestSubmitMixin
142
+ from app.modules.proxy._service.http_bridge.service_stubs import (
143
+ _call_with_supported_optional_kwargs,
144
+ _estimated_lease_tokens_from_request_usage_budget,
145
+ _prefer_earlier_reset_window,
146
+ _proxy_admission_wait_timeout_seconds,
147
+ _raise_proxy_unavailable,
148
+ _record_continuity_fail_closed,
149
+ _record_same_account_takeover,
150
+ _routing_strategy,
151
+ _service_get_settings,
152
+ _service_get_settings_cache,
153
+ _upstream_turn_state_from_socket,
154
+ _websocket_connect_deadline,
155
+ _websocket_safe_headers_with_turn_state,
156
+ )
157
+ from app.modules.proxy._service.http_bridge.session_registry import _HTTPBridgeSessionRegistryMixin
158
+ from app.modules.proxy._service.http_bridge.streaming import _HTTPBridgeStreamingMixin
159
+ from app.modules.proxy._service.http_bridge.upstream_events import _HTTPBridgeUpstreamEventsMixin
160
+ from app.modules.proxy._service.observability import _hash_identifier
161
+ from app.modules.proxy._service.support import (
162
+ _ACCOUNT_MODEL_UNSUPPORTED_ERROR_CODE,
163
+ _HARD_HTTP_BRIDGE_AFFINITY_KINDS, # noqa: F401
164
+ _WEBSOCKET_FULL_REPLAY_WAIT_POLL_SECONDS, # noqa: F401
165
+ _clear_websocket_precreated_replay_fallback,
166
+ _complete_http_bridge_handoff,
167
+ _copy_websocket_route_metadata_to_session,
168
+ _DeferredAccountBackoffLifecycle,
169
+ _HTTPBridgeOwnerForward,
170
+ _HTTPBridgeSession,
171
+ _HTTPBridgeSessionKey,
172
+ _sleep_for_account_selection_recovery,
173
+ _WebSocketRequestState,
174
+ _WebSocketUpstreamControl,
175
+ )
176
+ from app.modules.proxy._service.support import (
177
+ _websocket_route_log_kwargs as _websocket_route_log_kwargs,
178
+ )
179
+ from app.modules.proxy._service.warmup import (
180
+ WarmupExecutionData as WarmupExecutionData,
181
+ )
182
+ from app.modules.proxy._service.warmup import (
183
+ WarmupFailedAccountData as WarmupFailedAccountData,
184
+ )
185
+ from app.modules.proxy._service.warmup import (
186
+ WarmupSkippedAccountData as WarmupSkippedAccountData,
187
+ )
188
+ from app.modules.proxy._service.warmup import (
189
+ WarmupSubmittedAccountData as WarmupSubmittedAccountData,
190
+ )
191
+ from app.modules.proxy._service.warmup import (
192
+ _is_warmup_usage_eligible as _is_warmup_usage_eligible,
193
+ )
194
+ from app.modules.proxy._service.warmup import (
195
+ _materialize_warmup_account as _materialize_warmup_account,
196
+ )
197
+ from app.modules.proxy._service.warmup import (
198
+ _snapshot_warmup_account as _snapshot_warmup_account,
199
+ )
200
+ from app.modules.proxy._service.warmup import (
201
+ _WarmupAccountSnapshot as _WarmupAccountSnapshot,
202
+ )
203
+ from app.modules.proxy._service.warmup import (
204
+ _WarmupSubmitResult as _WarmupSubmitResult,
205
+ )
206
+ from app.modules.proxy._service.warmup import (
207
+ _WarmupUsageSnapshot as _WarmupUsageSnapshot,
208
+ )
209
+ from app.modules.proxy.account_eligibility import ROUTABLE_STATUSES
210
+ from app.modules.proxy.account_eligibility import account_access_token_expires_at as _token_expiry
211
+ from app.modules.proxy.affinity import (
212
+ _AffinityPolicy,
213
+ _extract_model_class,
214
+ _sticky_key_from_turn_state_header,
215
+ )
216
+ from app.modules.proxy.continuity import (
217
+ is_http_bridge_account_neutral_replay,
218
+ resolve_reconnect_preferred_account_id,
219
+ resolve_required_account_id,
220
+ without_http_bridge_session_affinity_headers,
221
+ )
222
+ from app.modules.proxy.durable_bridge_coordinator import DurableBridgeLookup
223
+ from app.modules.proxy.load_balancer import CONTINUITY_OWNER_UNAVAILABLE, AccountLease
224
+ from app.modules.proxy.selection_errors import USAGE_LIMIT_REACHED, selection_failure_response
225
+
226
+ logger = logging.getLogger("app.modules.proxy.service")
227
+ T = TypeVar("T")
228
+ _REQUEST_TRANSPORT_HTTP = "http"
229
+ _UPSTREAM_CLOSE_CODES_SKIP_SAME_ACCOUNT_RETRY = frozenset({1011})
230
+ _HTTP_BRIDGE_BACKGROUND_CLEANUP_WARN_THRESHOLD = 100
231
+
232
+
233
+ class _HTTPBridgeMixin(
234
+ _HTTPBridgeStreamingMixin,
235
+ _HTTPBridgeSessionRegistryMixin,
236
+ _HTTPBridgeAccountSessionsMixin,
237
+ _HTTPBridgeActivityMixin,
238
+ _HTTPBridgeOwnerForwardingMixin,
239
+ _HTTPBridgeRequestSubmitMixin,
240
+ _HTTPBridgeUpstreamEventsMixin,
241
+ _HTTPBridgeServiceProtocol,
242
+ ):
243
+ def _schedule_http_bridge_session_closes(
244
+ self,
245
+ sessions: list["_HTTPBridgeSession"],
246
+ *,
247
+ reason: str,
248
+ ) -> None:
249
+ for session in sessions:
250
+ if len(self._background_cleanup_tasks) >= _HTTP_BRIDGE_BACKGROUND_CLEANUP_WARN_THRESHOLD:
251
+ logger.warning(
252
+ "http_bridge_background_cleanup_backlog action=session_close count=%d threshold=%d reason=%s",
253
+ len(self._background_cleanup_tasks),
254
+ _HTTP_BRIDGE_BACKGROUND_CLEANUP_WARN_THRESHOLD,
255
+ reason,
256
+ )
257
+ self._schedule_cancel_safe_cleanup(
258
+ self._close_http_bridge_session_bounded(session, reason=reason),
259
+ action="http_bridge_session_close",
260
+ request_id=_hash_identifier(session.key.affinity_key),
261
+ )
262
+
263
+ async def _drain_http_bridge_background_cleanup_tasks(self, *, reason: str) -> bool:
264
+ tasks = [
265
+ task
266
+ for task in self._background_cleanup_tasks
267
+ if (
268
+ task.get_name().startswith("proxy-http_bridge_session_close-")
269
+ or task.get_name().startswith("http-bridge-close-")
270
+ or task.get_name().startswith("cancelled-task-cleanup-")
271
+ )
272
+ ]
273
+ if not tasks:
274
+ return not self._http_bridge_background_cleanup_failed
275
+ try:
276
+ results = await asyncio.wait_for(
277
+ asyncio.gather(*(asyncio.shield(task) for task in tasks), return_exceptions=True),
278
+ timeout=_HTTP_BRIDGE_BACKGROUND_CLOSE_TIMEOUT_SECONDS,
279
+ )
280
+ except TimeoutError:
281
+ logger.warning(
282
+ "http_bridge_background_cleanup_drain_timeout reason=%s count=%d timeout_seconds=%.1f",
283
+ reason,
284
+ len(tasks),
285
+ _HTTP_BRIDGE_BACKGROUND_CLOSE_TIMEOUT_SECONDS,
286
+ )
287
+ return False
288
+ self._http_bridge_background_cleanup_failed |= any(isinstance(result, BaseException) for result in results)
289
+ return not self._http_bridge_background_cleanup_failed
290
+
291
+ async def _fail_http_bridge_inflight_session_creation(
292
+ self,
293
+ key: "_HTTPBridgeSessionKey",
294
+ inflight_future: asyncio.Future["_HTTPBridgeSession"] | None,
295
+ exc: BaseException,
296
+ ) -> bool:
297
+ if inflight_future is None:
298
+ return False
299
+ async with self._http_bridge_lock:
300
+ current_future = self._http_bridge_inflight_sessions.get(key)
301
+ if current_future is not inflight_future:
302
+ return False
303
+ if getattr(inflight_future, "_http_bridge_handoff", False):
304
+ return False
305
+ self._http_bridge_inflight_sessions.pop(key, None)
306
+ if inflight_future.done():
307
+ return True
308
+ if isinstance(exc, asyncio.CancelledError):
309
+ inflight_future.cancel()
310
+ else:
311
+ inflight_future.set_exception(exc)
312
+ inflight_future.exception()
313
+ return True
314
+
315
+ async def _evict_http_bridge_inflight_waiter(
316
+ self,
317
+ inflight_future: asyncio.Future["_HTTPBridgeSession"],
318
+ exc: BaseException,
319
+ ) -> "_HTTPBridgeSessionKey | None":
320
+ async with self._http_bridge_lock:
321
+ stale_key = None
322
+ for candidate_key, candidate_future in self._http_bridge_inflight_sessions.items():
323
+ if candidate_future is inflight_future:
324
+ stale_key = candidate_key
325
+ break
326
+ if stale_key is None:
327
+ return None
328
+ if getattr(inflight_future, "_http_bridge_handoff", False):
329
+ return None
330
+ self._http_bridge_inflight_sessions.pop(stale_key, None)
331
+ if not inflight_future.done():
332
+ inflight_future.set_exception(exc)
333
+ inflight_future.exception()
334
+ return stale_key
335
+
336
+ @overload
337
+ async def _get_or_create_http_bridge_session(
338
+ self,
339
+ key: "_HTTPBridgeSessionKey",
340
+ *,
341
+ headers: dict[str, str],
342
+ affinity: _AffinityPolicy,
343
+ api_key: ApiKeyData | None,
344
+ request_model: str | None,
345
+ idle_ttl_seconds: float,
346
+ max_sessions: int,
347
+ request_service_tier: str | None = None,
348
+ previous_response_id: str | None = None,
349
+ gateway_safe_mode: bool = False,
350
+ allow_forward_to_owner: Literal[False] = False,
351
+ forwarded_request: bool = False,
352
+ forwarded_original_request_unanchored: bool = False,
353
+ forwarded_affinity_kind: str | None = None,
354
+ forwarded_affinity_key: str | None = None,
355
+ allow_previous_response_recovery_rebind: bool = False,
356
+ allow_bootstrap_owner_rebind: bool = False,
357
+ durable_lookup: DurableBridgeLookup | None = None,
358
+ request_stage: str = "first_turn",
359
+ preferred_account_id: str | None = None,
360
+ preferred_account_has_continuity_provenance: bool = False,
361
+ fallback_on_preferred_account_unavailable: bool = True,
362
+ request_usage_budget: ApiKeyRequestUsageBudget | None = None,
363
+ request_deadline: float | None = None,
364
+ session_header_fallback_key: "_HTTPBridgeSessionKey | None" = None,
365
+ exclude_account_ids: Collection[str] | None = None,
366
+ deferred_account_backoff_lifecycle: _DeferredAccountBackoffLifecycle | None = None,
367
+ defer_account_health_writes: bool = False,
368
+ ) -> "_HTTPBridgeSession": ...
369
+ @overload
370
+ async def _get_or_create_http_bridge_session(
371
+ self,
372
+ key: "_HTTPBridgeSessionKey",
373
+ *,
374
+ headers: dict[str, str],
375
+ affinity: _AffinityPolicy,
376
+ api_key: ApiKeyData | None,
377
+ request_model: str | None,
378
+ idle_ttl_seconds: float,
379
+ max_sessions: int,
380
+ request_service_tier: str | None = None,
381
+ previous_response_id: str | None = None,
382
+ gateway_safe_mode: bool = False,
383
+ allow_forward_to_owner: Literal[True],
384
+ forwarded_request: bool = False,
385
+ forwarded_original_request_unanchored: bool = False,
386
+ forwarded_affinity_kind: str | None = None,
387
+ forwarded_affinity_key: str | None = None,
388
+ allow_previous_response_recovery_rebind: bool = False,
389
+ allow_bootstrap_owner_rebind: bool = False,
390
+ durable_lookup: DurableBridgeLookup | None = None,
391
+ request_stage: str = "first_turn",
392
+ preferred_account_id: str | None = None,
393
+ preferred_account_has_continuity_provenance: bool = False,
394
+ fallback_on_preferred_account_unavailable: bool = True,
395
+ request_usage_budget: ApiKeyRequestUsageBudget | None = None,
396
+ request_deadline: float | None = None,
397
+ session_header_fallback_key: "_HTTPBridgeSessionKey | None" = None,
398
+ exclude_account_ids: Collection[str] | None = None,
399
+ deferred_account_backoff_lifecycle: _DeferredAccountBackoffLifecycle | None = None,
400
+ defer_account_health_writes: bool = False,
401
+ ) -> "_HTTPBridgeSession | _HTTPBridgeOwnerForward": ...
402
+ async def _get_or_create_http_bridge_session(
403
+ self,
404
+ key: "_HTTPBridgeSessionKey",
405
+ *,
406
+ headers: dict[str, str],
407
+ affinity: _AffinityPolicy,
408
+ api_key: ApiKeyData | None,
409
+ request_model: str | None,
410
+ idle_ttl_seconds: float,
411
+ max_sessions: int,
412
+ request_service_tier: str | None = None,
413
+ previous_response_id: str | None = None,
414
+ gateway_safe_mode: bool = False,
415
+ allow_forward_to_owner: bool = False,
416
+ forwarded_request: bool = False,
417
+ forwarded_original_request_unanchored: bool = False,
418
+ forwarded_affinity_kind: str | None = None,
419
+ forwarded_affinity_key: str | None = None,
420
+ allow_previous_response_recovery_rebind: bool = False,
421
+ allow_bootstrap_owner_rebind: bool = False,
422
+ durable_lookup: DurableBridgeLookup | None = None,
423
+ request_stage: str = "first_turn",
424
+ preferred_account_id: str | None = None,
425
+ preferred_account_has_continuity_provenance: bool = False,
426
+ fallback_on_preferred_account_unavailable: bool = True,
427
+ request_usage_budget: ApiKeyRequestUsageBudget | None = None,
428
+ request_deadline: float | None = None,
429
+ session_header_fallback_key: "_HTTPBridgeSessionKey | None" = None,
430
+ exclude_account_ids: Collection[str] | None = None,
431
+ deferred_account_backoff_lifecycle: _DeferredAccountBackoffLifecycle | None = None,
432
+ defer_account_health_writes: bool = False,
433
+ ) -> "_HTTPBridgeSession | _HTTPBridgeOwnerForward":
434
+ settings = _service_get_settings()
435
+ request_scope_id = ensure_request_scope_id()
436
+ api_key_id = api_key.id if api_key is not None else None
437
+ incoming_turn_state = _sticky_key_from_turn_state_header(headers)
438
+ incoming_session_key, initial_session_key = _turn_keys(headers, api_key, key, session_header_fallback_key)
439
+ original_request_unanchored = _http_bridge_request_needs_unanchored_handoff(
440
+ key, incoming_turn_state, previous_response_id, forwarded_request, forwarded_original_request_unanchored
441
+ )
442
+ # Model-transition isolation intentionally drops the durable lookup as a
443
+ # routing input below. Preserve generation provenance first: the same
444
+ # replica id can still name an older socket/process whose late release
445
+ # must be fenced by a newly advanced owner epoch.
446
+ same_replica_durable_predecessor = bool(
447
+ durable_lookup and durable_lookup.owner_instance_id == settings.http_responses_session_bridge_instance_id
448
+ )
449
+ model_transition_rebind = bool(
450
+ durable_lookup is not None and not _http_bridge_models_compatible(durable_lookup.model, request_model)
451
+ )
452
+ if model_transition_rebind:
453
+ durable_lookup = None
454
+ # Account selection consumes this one-shot capability; canonical creation
455
+ # also forces takeover so a prior-ring durable owner cannot reject it.
456
+ force_goal_restart_account_reselection = affinity.abandon_unavailable_legacy_owner
457
+ if await _http_bridge_should_wait_for_registration(self, key, settings):
458
+ skip_registration_gate = False
459
+ async with self._http_bridge_lock:
460
+ existing = self._http_bridge_sessions.get(key)
461
+ if existing is not None:
462
+ skip_registration_gate = True
463
+ elif incoming_turn_state is not None:
464
+ alias_index_key = _http_bridge_turn_state_alias_key(incoming_turn_state, api_key_id)
465
+ alias_key = self._http_bridge_turn_state_index.get(alias_index_key)
466
+ if alias_key is not None and alias_key in self._http_bridge_sessions:
467
+ skip_registration_gate = True
468
+ if not skip_registration_gate:
469
+ import app.core.startup as startup_module
470
+
471
+ registered = await startup_module.wait_for_bridge_registration(
472
+ timeout_seconds=settings.upstream_connect_timeout_seconds,
473
+ )
474
+ if not registered:
475
+ raise ProxyResponseError(
476
+ 503,
477
+ openai_error(
478
+ "bridge_owner_unreachable",
479
+ "HTTP bridge registration is not ready",
480
+ error_type="server_error",
481
+ ),
482
+ )
483
+ effective_idle_ttl_seconds = idle_ttl_seconds
484
+ forwarded_affinity = (
485
+ _forwarded_http_bridge_session_key(
486
+ headers,
487
+ api_key,
488
+ forwarded_affinity_kind=forwarded_affinity_kind,
489
+ forwarded_affinity_key=forwarded_affinity_key,
490
+ )
491
+ if forwarded_request
492
+ else None
493
+ )
494
+ old_account_id: str | None = None
495
+ force_durable_takeover_after_detach = used_session_header_fallback = False
496
+ locally_owned_fork_key: _HTTPBridgeSessionKey | None = None
497
+ model_transition_parent_key: _HTTPBridgeSessionKey | None = None
498
+
499
+ def bind_account_neutral_recovery_owner(session: _HTTPBridgeSession) -> None:
500
+ nonlocal preferred_account_id
501
+ if not is_http_bridge_account_neutral_replay(
502
+ kind=session.key.affinity_kind,
503
+ key=session.key.affinity_key,
504
+ ):
505
+ return
506
+ preferred_account_id = resolve_required_account_id(
507
+ ("requested continuity owner", preferred_account_id),
508
+ ("local account-neutral recovery", session.account.id),
509
+ )
510
+
511
+ while True:
512
+ account_neutral_recovery = is_http_bridge_account_neutral_replay(
513
+ kind=key.affinity_kind,
514
+ key=key.affinity_key,
515
+ )
516
+ if account_neutral_recovery:
517
+ headers = without_http_bridge_session_affinity_headers(headers)
518
+ affinity = _AffinityPolicy()
519
+ incoming_turn_state = None
520
+ incoming_session_key = None
521
+ session_header_fallback_key = None
522
+ initial_session_key = None
523
+ if durable_lookup is None:
524
+ allow_forward_to_owner = False
525
+ inflight_future: asyncio.Future[_HTTPBridgeSession] | None = None
526
+ capacity_wait_future: asyncio.Future[_HTTPBridgeSession] | None = None
527
+ capacity_error_after_planned_closes: ProxyResponseError | None = None
528
+ owns_creation = False
529
+ continuity_error: ProxyResponseError | None = None
530
+ owner_mismatch_error: ProxyResponseError | None = None
531
+ owner_forward: _HTTPBridgeOwnerForward | None = None
532
+ force_durable_takeover = force_durable_takeover_after_detach or force_goal_restart_account_reselection
533
+ missing_turn_state_alias = False
534
+ sessions_to_close_before_create: list[_HTTPBridgeSession] = []
535
+ session_to_return_after_close: _HTTPBridgeSession | None = None
536
+ preserve_durable_canonical_key = (
537
+ incoming_turn_state is not None
538
+ and forwarded_affinity is None
539
+ and durable_lookup is not None
540
+ and key.affinity_kind == durable_lookup.canonical_kind
541
+ and key.affinity_key == durable_lookup.canonical_key
542
+ and key.affinity_kind != "turn_state_header"
543
+ )
544
+ preserve_internal_fork_key = key.affinity_kind in {
545
+ "internal_unanchored_parallel",
546
+ "internal_model_parallel",
547
+ "internal_request_parallel",
548
+ }
549
+ require_preferred_account = preferred_account_id is not None and (
550
+ previous_response_id is not None
551
+ or preferred_account_has_continuity_provenance
552
+ or key.strength == "hard"
553
+ or not fallback_on_preferred_account_unavailable
554
+ )
555
+ async with self._http_bridge_lock:
556
+ if (
557
+ incoming_turn_state is not None
558
+ and forwarded_affinity is None
559
+ and not preserve_durable_canonical_key
560
+ and not preserve_internal_fork_key
561
+ ):
562
+ alias_index_key = _http_bridge_turn_state_alias_key(incoming_turn_state, api_key_id)
563
+ alias_key = self._http_bridge_turn_state_index.get(alias_index_key)
564
+ if alias_key is not None:
565
+ key = alias_key
566
+ alias_session = self._http_bridge_sessions.get(alias_key)
567
+ if _http_bridge_alias_target_is_stale(alias_session):
568
+ if is_http_bridge_account_neutral_replay(
569
+ kind=alias_key.affinity_kind,
570
+ key=alias_key.affinity_key,
571
+ ):
572
+ if alias_session is None:
573
+ raise ProxyResponseError(502, _http_bridge_continuity_lost_error_envelope())
574
+ bind_account_neutral_recovery_owner(alias_session)
575
+ continue
576
+ self._http_bridge_turn_state_index.pop(alias_index_key, None)
577
+ key = _HTTPBridgeSessionKey("turn_state_header", incoming_turn_state, api_key_id)
578
+ elif not _http_bridge_models_compatible(alias_session.request_model, request_model):
579
+ model_transition_rebind, model_transition_parent_key = True, alias_key
580
+ if is_http_bridge_account_neutral_replay(
581
+ kind=alias_key.affinity_kind,
582
+ key=alias_key.affinity_key,
583
+ ):
584
+ recovery_fork_key = _http_bridge_incompatible_model_fork_key(
585
+ key=alias_key,
586
+ existing_model=alias_session.request_model,
587
+ request_model=request_model,
588
+ request_scope_id=request_scope_id,
589
+ )
590
+ assert recovery_fork_key is not None
591
+ bind_account_neutral_recovery_owner(alias_session)
592
+ key = recovery_fork_key
593
+ continue
594
+ else:
595
+ key = _HTTPBridgeSessionKey("turn_state_header", incoming_turn_state, api_key_id)
596
+ elif not _http_bridge_compatible(
597
+ alias_session, request_model, request_service_tier, True
598
+ ) or not _http_bridge_session_matches_preferred_account(
599
+ session=alias_session,
600
+ previous_response_id=previous_response_id,
601
+ preferred_account_id=preferred_account_id,
602
+ require_preferred_account=require_preferred_account,
603
+ ):
604
+ raise ProxyResponseError(502, _http_bridge_continuity_lost_error_envelope())
605
+ else:
606
+ self._promote_http_bridge_session_to_codex_affinity(
607
+ alias_session,
608
+ turn_state=incoming_turn_state,
609
+ )
610
+ _register_http_bridge_turn_state_aliases_locked(self, alias_session)
611
+ key = alias_session.key
612
+ elif incoming_turn_state.startswith("http_turn_"):
613
+ if previous_response_id is not None:
614
+ previous_alias_key = _http_bridge_previous_response_alias_key(
615
+ previous_response_id,
616
+ api_key_id,
617
+ )
618
+ previous_key = self._http_bridge_previous_response_index.get(previous_alias_key)
619
+ previous_session = None
620
+ if previous_key is not None:
621
+ previous_session = self._http_bridge_sessions.get(previous_key)
622
+ if (
623
+ previous_session is not None
624
+ and (not previous_session.closed or previous_session.handoff_in_progress)
625
+ and _http_bridge_session_account_active(previous_session)
626
+ and _http_bridge_compatible(previous_session, request_model, request_service_tier, True)
627
+ and _http_bridge_session_matches_preferred_account(
628
+ session=previous_session,
629
+ previous_response_id=previous_response_id,
630
+ preferred_account_id=preferred_account_id,
631
+ require_preferred_account=require_preferred_account,
632
+ )
633
+ ):
634
+ key = previous_session.key
635
+ self._promote_http_bridge_session_to_codex_affinity(
636
+ previous_session,
637
+ turn_state=incoming_turn_state,
638
+ )
639
+ previous_session.downstream_turn_state_aliases.add(incoming_turn_state)
640
+ for alias in previous_session.downstream_turn_state_aliases:
641
+ self._http_bridge_turn_state_index[
642
+ _http_bridge_turn_state_alias_key(
643
+ alias,
644
+ previous_session.key.api_key_id,
645
+ )
646
+ ] = previous_session.key
647
+ continue
648
+ if previous_session is not None and not _http_bridge_models_compatible(
649
+ previous_session.request_model, request_model
650
+ ):
651
+ model_transition_rebind = True
652
+ model_transition_parent_key = previous_key
653
+ if previous_key is not None and is_http_bridge_account_neutral_replay(
654
+ kind=previous_key.affinity_kind,
655
+ key=previous_key.affinity_key,
656
+ ):
657
+ recovery_fork_key = _http_bridge_incompatible_model_fork_key(
658
+ key=previous_key,
659
+ existing_model=previous_session.request_model,
660
+ request_model=request_model,
661
+ request_scope_id=request_scope_id,
662
+ )
663
+ assert recovery_fork_key is not None
664
+ bind_account_neutral_recovery_owner(previous_session)
665
+ key = recovery_fork_key
666
+ continue
667
+ elif previous_session is not None and (
668
+ not _http_bridge_alias_target_is_stale(previous_session)
669
+ and not previous_session.handoff_in_progress
670
+ ):
671
+ raise ProxyResponseError(502, _http_bridge_continuity_lost_error_envelope())
672
+ elif previous_key is not None:
673
+ self._http_bridge_previous_response_index.pop(previous_alias_key, None)
674
+ if model_transition_rebind:
675
+ if not is_http_bridge_account_neutral_replay(
676
+ kind=key.affinity_kind,
677
+ key=key.affinity_key,
678
+ ):
679
+ key = _HTTPBridgeSessionKey("turn_state_header", incoming_turn_state, api_key_id)
680
+ elif (
681
+ fallback_key := _alias_fallback_key(incoming_session_key, initial_session_key, api_key_id)
682
+ ) is not None:
683
+ key = fallback_key
684
+ used_session_header_fallback = True
685
+ else:
686
+ key = _HTTPBridgeSessionKey("turn_state_header", incoming_turn_state, api_key_id)
687
+ missing_turn_state_alias = True
688
+ pruned_sessions = self._prune_http_bridge_sessions_locked()
689
+ if pruned_sessions:
690
+ if any(session.key == key for session in pruned_sessions):
691
+ force_durable_takeover = True
692
+ self._schedule_http_bridge_session_closes(pruned_sessions, reason="registry_detach")
693
+ existing = self._http_bridge_sessions.get(key)
694
+ retained_handoff = bool(
695
+ existing and existing.closed and _http_bridge_session_has_admission_waiter(existing)
696
+ )
697
+ reusable = (
698
+ not force_goal_restart_account_reselection
699
+ and existing is not None
700
+ and _http_bridge_session_reusable_for_lookup(
701
+ session=existing,
702
+ key=key,
703
+ api_key=api_key,
704
+ incoming_turn_state=incoming_turn_state,
705
+ previous_response_id=previous_response_id,
706
+ preferred_account_id=preferred_account_id,
707
+ require_preferred_account=require_preferred_account,
708
+ service_tier_supported=_http_bridge_compatible(existing, request_model, request_service_tier),
709
+ allow_closed_admission_handoff=retained_handoff,
710
+ session_key_quarantined=_http_bridge_session_key_quarantined(self, existing.key),
711
+ )
712
+ )
713
+ fork_key = _http_bridge_parallel_fork_key(
714
+ key=key,
715
+ session=existing,
716
+ inflight_creation=key in self._http_bridge_inflight_sessions
717
+ and not bool(existing and existing.handoff_in_progress),
718
+ incoming_turn_state=incoming_turn_state,
719
+ previous_response_id=previous_response_id,
720
+ request_model=request_model,
721
+ request_service_tier=request_service_tier,
722
+ request_scope_id=request_scope_id,
723
+ allow_model_fork=reusable or model_transition_rebind,
724
+ force_canonical_replacement=force_goal_restart_account_reselection,
725
+ )
726
+ if fork_key is not None:
727
+ if existing is not None:
728
+ bind_account_neutral_recovery_owner(existing)
729
+ model_transition_parent_key = key
730
+ key = fork_key
731
+ durable_lookup = None
732
+ force_durable_takeover_after_detach = False
733
+ locally_owned_fork_key = _http_bridge_locally_owned_fork_key(
734
+ fork_key, forwarded_request, forwarded_original_request_unanchored
735
+ )
736
+ continue
737
+ if retained_handoff and not reusable:
738
+ existing, force_durable_takeover = self._recover_http_bridge_incompatible_admission_handoff(
739
+ key,
740
+ existing,
741
+ force_durable_takeover,
742
+ original_request_unanchored,
743
+ request_model,
744
+ api_key,
745
+ incoming_turn_state,
746
+ previous_response_id,
747
+ preferred_account_id,
748
+ require_preferred_account,
749
+ request_service_tier,
750
+ )
751
+ if reusable:
752
+ assert existing is not None
753
+ current_instance = settings.http_responses_session_bridge_instance_id
754
+ if _durable_bridge_lookup_allows_local_reuse(durable_lookup, current_instance=current_instance):
755
+ existing.api_key = api_key
756
+ existing.request_model = request_model
757
+ existing.request_service_tier = request_service_tier
758
+ existing.last_used_at = clock_for(self).monotonic()
759
+ await _refresh_reused_http_bridge_session_with_handoff(
760
+ self,
761
+ existing,
762
+ key=key,
763
+ request_scope_id=request_scope_id,
764
+ reserve_handoff=original_request_unanchored,
765
+ )
766
+ return existing
767
+ old_account_id = existing.account.id
768
+ detached = self._detach_http_bridge_session_locked(key, expected_session=existing)
769
+ if detached is not None:
770
+ force_durable_takeover = True
771
+ self._schedule_http_bridge_session_closes([detached], reason="registry_detach")
772
+ existing = None
773
+ if existing is not None and (
774
+ force_goal_restart_account_reselection
775
+ or (not existing.closed and existing.account.status in ROUTABLE_STATUSES)
776
+ ):
777
+ old_account_id = existing.account.id
778
+ retiring_with_visible_requests = _http_bridge_session_retiring_with_visible_requests(existing)
779
+ detached = self._detach_http_bridge_session_locked(
780
+ key,
781
+ expected_session=existing,
782
+ mark_closed=not retiring_with_visible_requests,
783
+ )
784
+ if detached is not None:
785
+ force_durable_takeover = True
786
+ if not retiring_with_visible_requests:
787
+ if self._http_bridge_forced_close_must_finish_before_create(
788
+ force_goal_restart_account_reselection,
789
+ max_sessions,
790
+ ):
791
+ sessions_to_close_before_create.append(detached)
792
+ else:
793
+ self._schedule_http_bridge_session_closes([detached], reason="registry_detach")
794
+ existing = None
795
+ if shutdown_state.is_bridge_drain_active() and not _http_bridge_can_recover_during_drain(
796
+ key=key,
797
+ headers=headers,
798
+ previous_response_id=previous_response_id,
799
+ durable_lookup=durable_lookup,
800
+ ):
801
+ raise ProxyResponseError(
802
+ 503,
803
+ openai_error(
804
+ "bridge_drain_active",
805
+ "HTTP bridge is draining — new sessions not accepted during shutdown",
806
+ error_type="server_error",
807
+ ),
808
+ )
809
+ if shutdown_state.is_bridge_drain_active():
810
+ _record_bridge_drain_recovery_allowed()
811
+ owner_check_required = _http_bridge_owner_check_required(
812
+ key,
813
+ gateway_safe_mode=gateway_safe_mode,
814
+ )
815
+ if owner_check_required or key.affinity_kind == "prompt_cache":
816
+ owner_instance = _durable_bridge_lookup_active_owner(durable_lookup)
817
+ hard_continuity_lookup = owner_check_required or bool(incoming_turn_state or previous_response_id)
818
+ ring_lookup_failed = False
819
+ if key == locally_owned_fork_key:
820
+ owner_instance = settings.http_responses_session_bridge_instance_id
821
+ if owner_instance is None:
822
+ try:
823
+ owner_instance = await _http_bridge_owner_instance(key, settings, self._ring_membership)
824
+ except Exception as exc:
825
+ ring_lookup_failed = True
826
+ if hard_continuity_lookup:
827
+ _record_continuity_fail_closed(
828
+ surface="http_bridge",
829
+ reason="owner_metadata_unavailable",
830
+ previous_response_id=previous_response_id,
831
+ session_id=incoming_turn_state or incoming_session_key,
832
+ upstream_error_code="owner_lookup_failed",
833
+ )
834
+ raise ProxyResponseError(
835
+ 502,
836
+ _http_bridge_owner_lookup_unavailable_error_envelope(),
837
+ ) from exc
838
+ if _http_bridge_can_local_recover_without_ring(
839
+ key=key,
840
+ headers=headers,
841
+ previous_response_id=previous_response_id,
842
+ durable_lookup=durable_lookup,
843
+ ):
844
+ logger.warning(
845
+ "Bridge owner lookup failed; allowing local recovery path",
846
+ exc_info=True,
847
+ )
848
+ owner_instance = settings.http_responses_session_bridge_instance_id
849
+ else:
850
+ raise
851
+ try:
852
+ current_instance, ring = await _active_http_bridge_instance_ring(
853
+ settings, self._ring_membership
854
+ )
855
+ except Exception as exc:
856
+ if hard_continuity_lookup:
857
+ _record_continuity_fail_closed(
858
+ surface="http_bridge",
859
+ reason="owner_metadata_unavailable",
860
+ previous_response_id=previous_response_id,
861
+ session_id=incoming_turn_state or incoming_session_key,
862
+ upstream_error_code="ring_lookup_failed",
863
+ )
864
+ raise ProxyResponseError(
865
+ 502,
866
+ _http_bridge_owner_lookup_unavailable_error_envelope(),
867
+ ) from exc
868
+ if ring_lookup_failed or _http_bridge_can_local_recover_without_ring(
869
+ key=key,
870
+ headers=headers,
871
+ previous_response_id=previous_response_id,
872
+ durable_lookup=durable_lookup,
873
+ ):
874
+ logger.warning(
875
+ "Bridge ring lookup failed; falling back to local recovery ring", exc_info=True
876
+ )
877
+ current_instance = settings.http_responses_session_bridge_instance_id
878
+ ring = (current_instance,)
879
+ else:
880
+ raise
881
+ owner_mismatch = owner_instance is not None and owner_instance != current_instance
882
+ if owner_mismatch and (len(ring) > 1 or durable_lookup is not None):
883
+ if PROMETHEUS_AVAILABLE and bridge_owner_mismatch_total is not None:
884
+ bridge_owner_mismatch_total.labels(strength=_http_bridge_key_strength(key)).inc()
885
+ if (
886
+ hard_continuity_lookup
887
+ and not (previous_response_id is not None and allow_previous_response_recovery_rebind)
888
+ and not allow_bootstrap_owner_rebind
889
+ ):
890
+ _log_http_bridge_event(
891
+ "owner_mismatch",
892
+ key,
893
+ account_id=None,
894
+ model=request_model,
895
+ detail=(
896
+ "expected_instance="
897
+ f"{owner_instance}, current_instance={current_instance}, outcome=forward"
898
+ ),
899
+ cache_key_family=key.affinity_kind,
900
+ model_class=_extract_model_class(request_model) if request_model else None,
901
+ owner_check_applied=True,
902
+ )
903
+ if allow_forward_to_owner:
904
+ if forwarded_request:
905
+ _log_http_bridge_event(
906
+ "owner_mismatch_forward_loop",
907
+ key,
908
+ account_id=None,
909
+ model=request_model,
910
+ detail=(
911
+ "expected_instance="
912
+ f"{owner_instance}, current_instance={current_instance}, "
913
+ "outcome=forward_loop_prevented"
914
+ ),
915
+ cache_key_family=key.affinity_kind,
916
+ model_class=_extract_model_class(request_model) if request_model else None,
917
+ owner_check_applied=True,
918
+ )
919
+ raise ProxyResponseError(
920
+ 503,
921
+ openai_error(
922
+ "bridge_forward_loop_prevented",
923
+ (
924
+ "HTTP bridge request was forwarded back to a non-owner instance; "
925
+ "refusing takeover to avoid a forward loop"
926
+ ),
927
+ error_type="server_error",
928
+ ),
929
+ )
930
+ elif self._ring_membership is None:
931
+ if _http_bridge_has_durable_recovery_anchor(
932
+ previous_response_id=previous_response_id,
933
+ durable_lookup=durable_lookup,
934
+ ):
935
+ if PROMETHEUS_AVAILABLE and bridge_durable_recover_total is not None:
936
+ bridge_durable_recover_total.labels(path="owner_missing").inc()
937
+ _log_http_bridge_event(
938
+ "owner_mismatch_local_recover",
939
+ key,
940
+ account_id=None,
941
+ model=request_model,
942
+ detail=(
943
+ "expected_instance="
944
+ f"{owner_instance}, current_instance={current_instance}, "
945
+ "outcome=local_recover_no_ring"
946
+ ),
947
+ cache_key_family=key.affinity_kind,
948
+ model_class=_extract_model_class(request_model) if request_model else None,
949
+ owner_check_applied=True,
950
+ )
951
+ force_durable_takeover = True
952
+ elif _http_bridge_can_single_instance_owner_takeover_without_anchor(
953
+ key=key,
954
+ owner_instance=owner_instance,
955
+ current_instance=current_instance,
956
+ ring=ring,
957
+ ):
958
+ if PROMETHEUS_AVAILABLE and bridge_durable_recover_total is not None:
959
+ bridge_durable_recover_total.labels(path="restart_takeover").inc()
960
+ _log_http_bridge_event(
961
+ "owner_mismatch_local_recover",
962
+ key,
963
+ account_id=None,
964
+ model=request_model,
965
+ detail=(
966
+ "expected_instance="
967
+ f"{owner_instance}, current_instance={current_instance}, "
968
+ "outcome=single_instance_takeover_no_anchor"
969
+ ),
970
+ cache_key_family=key.affinity_kind,
971
+ model_class=_extract_model_class(request_model) if request_model else None,
972
+ owner_check_applied=True,
973
+ )
974
+ force_durable_takeover = True
975
+ else:
976
+ _log_http_bridge_event(
977
+ "owner_mismatch_local_recover",
978
+ key,
979
+ account_id=None,
980
+ model=request_model,
981
+ detail=(
982
+ "expected_instance="
983
+ f"{owner_instance}, current_instance={current_instance}, "
984
+ "outcome=local_recover_no_ring"
985
+ ),
986
+ cache_key_family=key.affinity_kind,
987
+ model_class=_extract_model_class(request_model) if request_model else None,
988
+ owner_check_applied=True,
989
+ )
990
+ force_durable_takeover = True
991
+ else:
992
+ assert owner_instance is not None
993
+ owner_endpoint = await self._ring_membership.resolve_endpoint(owner_instance)
994
+ if owner_endpoint is None:
995
+ if _http_bridge_has_durable_recovery_anchor(
996
+ previous_response_id=previous_response_id,
997
+ durable_lookup=durable_lookup,
998
+ ):
999
+ if PROMETHEUS_AVAILABLE and bridge_durable_recover_total is not None:
1000
+ bridge_durable_recover_total.labels(path="owner_missing").inc()
1001
+ _log_http_bridge_event(
1002
+ "owner_endpoint_missing_local_recover",
1003
+ key,
1004
+ account_id=None,
1005
+ model=request_model,
1006
+ detail=(
1007
+ "expected_instance="
1008
+ f"{owner_instance}, current_instance={current_instance}, "
1009
+ "outcome=local_recover"
1010
+ ),
1011
+ cache_key_family=key.affinity_kind,
1012
+ model_class=_extract_model_class(request_model)
1013
+ if request_model
1014
+ else None,
1015
+ owner_check_applied=True,
1016
+ )
1017
+ force_durable_takeover = True
1018
+ else:
1019
+ _log_http_bridge_event(
1020
+ "owner_mismatch_local_recover",
1021
+ key,
1022
+ account_id=None,
1023
+ model=request_model,
1024
+ detail=(
1025
+ "expected_instance="
1026
+ f"{owner_instance}, current_instance={current_instance}, "
1027
+ "outcome=local_recover_no_endpoint"
1028
+ ),
1029
+ cache_key_family=key.affinity_kind,
1030
+ model_class=_extract_model_class(request_model)
1031
+ if request_model
1032
+ else None,
1033
+ owner_check_applied=True,
1034
+ )
1035
+ force_durable_takeover = True
1036
+ elif _http_bridge_endpoint_matches_current_instance(owner_endpoint, settings):
1037
+ if PROMETHEUS_AVAILABLE and bridge_durable_recover_total is not None:
1038
+ bridge_durable_recover_total.labels(path="restart_takeover").inc()
1039
+ _log_http_bridge_event(
1040
+ "owner_mismatch_local_recover",
1041
+ key,
1042
+ account_id=None,
1043
+ model=request_model,
1044
+ detail=(
1045
+ "expected_instance="
1046
+ f"{owner_instance}, current_instance={current_instance}, "
1047
+ "outcome=local_recover_same_endpoint"
1048
+ ),
1049
+ cache_key_family=key.affinity_kind,
1050
+ model_class=_extract_model_class(request_model) if request_model else None,
1051
+ owner_check_applied=True,
1052
+ )
1053
+ force_durable_takeover = True
1054
+ else:
1055
+ owner_forward = _HTTPBridgeOwnerForward(
1056
+ owner_instance=owner_instance,
1057
+ owner_endpoint=owner_endpoint,
1058
+ key=key,
1059
+ )
1060
+ else:
1061
+ if _http_bridge_has_durable_recovery_anchor(
1062
+ previous_response_id=previous_response_id,
1063
+ durable_lookup=durable_lookup,
1064
+ ):
1065
+ if PROMETHEUS_AVAILABLE and bridge_durable_recover_total is not None:
1066
+ bridge_durable_recover_total.labels(path="owner_missing").inc()
1067
+ _log_http_bridge_event(
1068
+ "owner_mismatch_local_recover",
1069
+ key,
1070
+ account_id=None,
1071
+ model=request_model,
1072
+ detail=(
1073
+ "expected_instance="
1074
+ f"{owner_instance}, current_instance={current_instance}, "
1075
+ "outcome=local_recover"
1076
+ ),
1077
+ cache_key_family=key.affinity_kind,
1078
+ model_class=_extract_model_class(request_model) if request_model else None,
1079
+ owner_check_applied=True,
1080
+ )
1081
+ force_durable_takeover = True
1082
+ else:
1083
+ _log_http_bridge_event(
1084
+ "owner_mismatch_local_recover",
1085
+ key,
1086
+ account_id=None,
1087
+ model=request_model,
1088
+ detail=(
1089
+ "expected_instance="
1090
+ f"{owner_instance}, current_instance={current_instance}, "
1091
+ "outcome=local_recover_no_forward"
1092
+ ),
1093
+ cache_key_family=key.affinity_kind,
1094
+ model_class=_extract_model_class(request_model) if request_model else None,
1095
+ owner_check_applied=True,
1096
+ )
1097
+ force_durable_takeover = True
1098
+ else:
1099
+ _log_http_bridge_event(
1100
+ "prompt_cache_locality_miss",
1101
+ key,
1102
+ account_id=None,
1103
+ model=request_model,
1104
+ detail=(
1105
+ "expected_instance="
1106
+ f"{owner_instance}, current_instance={current_instance}, "
1107
+ "outcome=local_rebind"
1108
+ ),
1109
+ cache_key_family=key.affinity_kind,
1110
+ model_class=_extract_model_class(request_model) if request_model else None,
1111
+ owner_check_applied=False,
1112
+ )
1113
+ if _http_bridge_can_single_instance_prompt_cache_takeover_without_anchor(
1114
+ key=key,
1115
+ owner_instance=owner_instance,
1116
+ current_instance=current_instance,
1117
+ ring=ring,
1118
+ ):
1119
+ force_durable_takeover = True
1120
+ elif allow_previous_response_recovery_rebind or allow_bootstrap_owner_rebind:
1121
+ force_durable_takeover = True
1122
+ _log_http_bridge_event(
1123
+ "soft_locality_rebind",
1124
+ key,
1125
+ account_id=None,
1126
+ model=request_model,
1127
+ detail=(
1128
+ "expected_instance="
1129
+ f"{owner_instance}, current_instance={current_instance}, outcome=local_rebind"
1130
+ ),
1131
+ cache_key_family=key.affinity_kind,
1132
+ model_class=_extract_model_class(request_model) if request_model else None,
1133
+ owner_check_applied=False,
1134
+ )
1135
+ if PROMETHEUS_AVAILABLE:
1136
+ if bridge_prompt_cache_locality_miss_total is not None:
1137
+ bridge_prompt_cache_locality_miss_total.inc()
1138
+ if bridge_soft_local_rebind_total is not None:
1139
+ bridge_soft_local_rebind_total.inc()
1140
+ if bridge_local_rebind_total is not None:
1141
+ bridge_local_rebind_total.labels(reason="prompt_cache_locality_miss").inc()
1142
+ if existing is not None and not existing.handoff_in_progress:
1143
+ old_account_id = existing.account.id
1144
+ _log_http_bridge_event(
1145
+ "discard_stale",
1146
+ key,
1147
+ account_id=existing.account.id,
1148
+ model=existing.request_model,
1149
+ cache_key_family=key.affinity_kind,
1150
+ model_class=_extract_model_class(existing.request_model) if existing.request_model else None,
1151
+ )
1152
+ detached = self._detach_http_bridge_session_locked(key, expected_session=existing)
1153
+ if detached is not None:
1154
+ force_durable_takeover = True
1155
+ self._schedule_http_bridge_session_closes([detached], reason="registry_detach")
1156
+ if owner_mismatch_error is None:
1157
+ inflight_future = self._http_bridge_inflight_sessions.get(key)
1158
+ if (
1159
+ previous_response_id is not None
1160
+ and inflight_future is None
1161
+ and (existing is None or existing.closed or not _http_bridge_session_account_active(existing))
1162
+ ):
1163
+ previous_alias_key = _http_bridge_previous_response_alias_key(previous_response_id, api_key_id)
1164
+ previous_key = self._http_bridge_previous_response_index.get(previous_alias_key)
1165
+ if previous_key is not None:
1166
+ previous_session = self._http_bridge_sessions.get(previous_key)
1167
+ if (
1168
+ previous_session is not None
1169
+ and not previous_session.closed
1170
+ and _http_bridge_session_account_active(previous_session)
1171
+ and _http_bridge_compatible(previous_session, request_model, request_service_tier, True)
1172
+ ):
1173
+ key = previous_session.key
1174
+ existing = previous_session
1175
+ inflight_future = self._http_bridge_inflight_sessions.get(previous_key)
1176
+ if incoming_turn_state:
1177
+ self._promote_http_bridge_session_to_codex_affinity(
1178
+ previous_session,
1179
+ turn_state=incoming_turn_state,
1180
+ )
1181
+ previous_session.downstream_turn_state_aliases.add(incoming_turn_state)
1182
+ for alias in previous_session.downstream_turn_state_aliases:
1183
+ self._http_bridge_turn_state_index[
1184
+ _http_bridge_turn_state_alias_key(
1185
+ alias,
1186
+ previous_session.key.api_key_id,
1187
+ )
1188
+ ] = previous_session.key
1189
+ if inflight_future is None:
1190
+ previous_session.request_model = request_model
1191
+ previous_session.request_service_tier = request_service_tier
1192
+ previous_session.last_used_at = clock_for(self).monotonic()
1193
+ await self._refresh_durable_http_bridge_session(previous_session)
1194
+ _log_http_bridge_event(
1195
+ "reuse",
1196
+ key,
1197
+ account_id=previous_session.account.id,
1198
+ model=previous_session.request_model,
1199
+ pending_count=self._http_bridge_pending_count_nowait(
1200
+ previous_session,
1201
+ context="previous_response_reuse_log",
1202
+ ),
1203
+ cache_key_family=key.affinity_kind,
1204
+ model_class=_extract_model_class(previous_session.request_model)
1205
+ if previous_session.request_model
1206
+ else None,
1207
+ )
1208
+ session_to_return_after_close = previous_session
1209
+ elif previous_session is not None and not _http_bridge_models_compatible(
1210
+ previous_session.request_model, request_model
1211
+ ):
1212
+ model_transition_rebind = True
1213
+ model_transition_parent_key = previous_key
1214
+ if is_http_bridge_account_neutral_replay(
1215
+ kind=previous_key.affinity_kind,
1216
+ key=previous_key.affinity_key,
1217
+ ):
1218
+ recovery_fork_key = _http_bridge_incompatible_model_fork_key(
1219
+ key=previous_key,
1220
+ existing_model=previous_session.request_model,
1221
+ request_model=request_model,
1222
+ request_scope_id=request_scope_id,
1223
+ )
1224
+ assert recovery_fork_key is not None
1225
+ bind_account_neutral_recovery_owner(previous_session)
1226
+ if key != recovery_fork_key:
1227
+ key = recovery_fork_key
1228
+ continue
1229
+ elif previous_key is not None and _http_bridge_alias_target_is_stale(previous_session):
1230
+ self._http_bridge_previous_response_index.pop(previous_alias_key, None)
1231
+ if (
1232
+ session_to_return_after_close is None
1233
+ and previous_response_id is not None
1234
+ and not used_session_header_fallback
1235
+ and not allow_previous_response_recovery_rebind
1236
+ and durable_lookup is None
1237
+ and not model_transition_rebind
1238
+ ):
1239
+ _record_continuity_fail_closed(
1240
+ surface="http_bridge",
1241
+ reason="continuity_lost",
1242
+ previous_response_id=previous_response_id,
1243
+ session_id=incoming_turn_state or incoming_session_key,
1244
+ )
1245
+ continuity_error = ProxyResponseError(502, _http_bridge_continuity_lost_error_envelope())
1246
+ elif missing_turn_state_alias and inflight_future is None and durable_lookup is None:
1247
+ turn_state_scope_conflict = incoming_turn_state is not None and any(
1248
+ alias == incoming_turn_state and alias_api_key != api_key_id
1249
+ for alias, alias_api_key in self._http_bridge_turn_state_index
1250
+ )
1251
+ if turn_state_scope_conflict:
1252
+ _record_continuity_fail_closed(
1253
+ surface="http_bridge",
1254
+ reason="turn_state_scope_conflict",
1255
+ previous_response_id=previous_response_id,
1256
+ session_id=incoming_turn_state,
1257
+ )
1258
+ continuity_error = ProxyResponseError(
1259
+ 409,
1260
+ openai_error(
1261
+ "bridge_instance_mismatch",
1262
+ "HTTP bridge turn-state is bound to a different API key scope",
1263
+ error_type="server_error",
1264
+ ),
1265
+ )
1266
+ elif (
1267
+ incoming_turn_state is not None
1268
+ and incoming_turn_state.startswith("http_turn_")
1269
+ and not allow_forward_to_owner
1270
+ ):
1271
+ _record_continuity_fail_closed(
1272
+ surface="http_bridge",
1273
+ reason="generated_turn_state_continuity_lost",
1274
+ previous_response_id=previous_response_id,
1275
+ session_id=incoming_turn_state,
1276
+ )
1277
+ continuity_error = ProxyResponseError(
1278
+ 409,
1279
+ openai_error(
1280
+ "bridge_instance_mismatch",
1281
+ "HTTP bridge continuity was lost for generated turn-state",
1282
+ error_type="server_error",
1283
+ ),
1284
+ )
1285
+ else:
1286
+ _log_http_bridge_event(
1287
+ "turn_state_alias_miss_local_rebind",
1288
+ key,
1289
+ account_id=None,
1290
+ model=request_model,
1291
+ detail="outcome=local_rebind_without_alias",
1292
+ cache_key_family=key.affinity_kind,
1293
+ model_class=_extract_model_class(request_model) if request_model else None,
1294
+ owner_check_applied=owner_check_required,
1295
+ )
1296
+ elif session_to_return_after_close is None and inflight_future is None and owner_forward is None:
1297
+ # Owner forwards never resolve a local inflight reservation; skip admission.
1298
+ # Detached generations remain globally capacity-owned
1299
+ # until close finalization. This request may discount
1300
+ # only the idle generations it has committed to close
1301
+ # synchronously below, before its inflight reservation
1302
+ # can create a replacement socket.
1303
+ _plan_http_bridge_lru_capacity_closes(
1304
+ self,
1305
+ max_sessions=max_sessions,
1306
+ model_transition_parent_key=model_transition_parent_key,
1307
+ sessions_to_close_before_create=sessions_to_close_before_create,
1308
+ )
1309
+ if (
1310
+ _http_bridge_capacity_after_planned_closes(self, sessions_to_close_before_create)
1311
+ >= max_sessions
1312
+ ):
1313
+ if _http_bridge_inflight_creation_count(self):
1314
+ capacity_wait_future = next(
1315
+ future
1316
+ for future in self._http_bridge_inflight_sessions.values()
1317
+ if not getattr(future, "_http_bridge_handoff", False)
1318
+ )
1319
+ else:
1320
+ capacity_error = self._http_bridge_active_capacity_error(
1321
+ key=key,
1322
+ request_model=request_model,
1323
+ )
1324
+ if not sessions_to_close_before_create:
1325
+ raise capacity_error
1326
+ # Detachment already transferred these LRU
1327
+ # generations out of the canonical registry.
1328
+ # Give each one a bounded-close owner before
1329
+ # rejecting admission; otherwise this early 429
1330
+ # leaves its live socket and leases stranded in
1331
+ # the detached registry until unrelated cleanup.
1332
+ capacity_error_after_planned_closes = capacity_error
1333
+ else:
1334
+ inflight_future = asyncio.get_running_loop().create_future()
1335
+ setattr(
1336
+ inflight_future,
1337
+ _HTTP_BRIDGE_INFLIGHT_STARTED_AT_ATTR,
1338
+ clock_for(self).monotonic(),
1339
+ )
1340
+ self._http_bridge_inflight_sessions[key] = inflight_future
1341
+ owns_creation = True
1342
+ try:
1343
+ for session_to_close in sessions_to_close_before_create:
1344
+ await self._close_http_bridge_session_bounded(session_to_close, reason="registry_detach")
1345
+ except BaseException as exc:
1346
+ if owns_creation:
1347
+ await self._fail_http_bridge_inflight_session_creation(key, inflight_future, exc)
1348
+ raise
1349
+ if capacity_error_after_planned_closes is not None:
1350
+ raise capacity_error_after_planned_closes
1351
+ if owns_creation and sessions_to_close_before_create:
1352
+ await self._enforce_http_bridge_capacity_after_planned_closes(
1353
+ key=key,
1354
+ inflight_future=inflight_future,
1355
+ max_sessions=max_sessions,
1356
+ request_model=request_model,
1357
+ )
1358
+ if session_to_return_after_close is not None:
1359
+ return session_to_return_after_close
1360
+ if owner_forward is not None:
1361
+ return owner_forward
1362
+ if owner_mismatch_error is not None:
1363
+ raise owner_mismatch_error
1364
+ if continuity_error is not None:
1365
+ raise continuity_error
1366
+ if capacity_wait_future is not None:
1367
+ wait_timeout_seconds = _proxy_admission_wait_timeout_seconds()
1368
+ try:
1369
+ await self._await_http_bridge_registry_wait(capacity_wait_future, timeout=wait_timeout_seconds)
1370
+ except asyncio.CancelledError:
1371
+ if capacity_wait_future.cancelled():
1372
+ continue
1373
+ raise
1374
+ except TimeoutError as exc:
1375
+ timeout_error = _http_bridge_startup_wait_timeout_error(
1376
+ "http_bridge_capacity",
1377
+ code="capacity_exhausted_active_sessions",
1378
+ )
1379
+ stale_key = await self._evict_http_bridge_inflight_waiter(capacity_wait_future, timeout_error)
1380
+ _log_http_bridge_startup_wait_timeout(
1381
+ stage="capacity",
1382
+ timeout_seconds=wait_timeout_seconds,
1383
+ key=stale_key or key,
1384
+ request_model=request_model,
1385
+ pending_count=_http_bridge_session_generation_count(self),
1386
+ inflight_count=len(self._http_bridge_inflight_sessions),
1387
+ )
1388
+ raise timeout_error from exc
1389
+ except ProxyResponseError:
1390
+ raise
1391
+ except Exception:
1392
+ pass
1393
+ continue
1394
+ if inflight_future is not None and not owns_creation:
1395
+ wait_timeout_seconds = _proxy_admission_wait_timeout_seconds()
1396
+ try:
1397
+ session = await self._await_http_bridge_registry_wait(inflight_future, timeout=wait_timeout_seconds)
1398
+ except asyncio.CancelledError:
1399
+ if inflight_future.cancelled():
1400
+ continue
1401
+ raise
1402
+ except TimeoutError as exc:
1403
+ timeout_error = _http_bridge_startup_wait_timeout_error(
1404
+ "http_bridge_inflight_session",
1405
+ code="capacity_exhausted_active_sessions",
1406
+ )
1407
+ await self._fail_http_bridge_inflight_session_creation(key, inflight_future, timeout_error)
1408
+ _log_http_bridge_startup_wait_timeout(
1409
+ stage="inflight_session",
1410
+ timeout_seconds=wait_timeout_seconds,
1411
+ key=key,
1412
+ request_model=request_model,
1413
+ pending_count=_http_bridge_session_generation_count(self),
1414
+ inflight_count=len(self._http_bridge_inflight_sessions),
1415
+ )
1416
+ raise timeout_error from exc
1417
+ except Exception:
1418
+ raise
1419
+ if session is None:
1420
+ continue
1421
+ fork_key = _http_bridge_parallel_fork_key(
1422
+ key=key,
1423
+ session=session,
1424
+ inflight_creation=False,
1425
+ incoming_turn_state=incoming_turn_state,
1426
+ previous_response_id=previous_response_id,
1427
+ request_model=request_model,
1428
+ request_service_tier=request_service_tier,
1429
+ request_scope_id=request_scope_id,
1430
+ same_model_required=True,
1431
+ force_canonical_replacement=force_goal_restart_account_reselection,
1432
+ )
1433
+ if fork_key is not None:
1434
+ bind_account_neutral_recovery_owner(session)
1435
+ model_transition_parent_key, key = key, fork_key
1436
+ durable_lookup = None
1437
+ force_durable_takeover_after_detach = False
1438
+ locally_owned_fork_key = _http_bridge_locally_owned_fork_key(
1439
+ fork_key, forwarded_request, forwarded_original_request_unanchored
1440
+ )
1441
+ continue
1442
+ if (
1443
+ not force_goal_restart_account_reselection
1444
+ and not session.closed
1445
+ and _http_bridge_session_account_active(session)
1446
+ and _http_bridge_session_allows_api_key(session, api_key)
1447
+ and _http_bridge_compatible(session, request_model, request_service_tier, True)
1448
+ and _http_bridge_session_reusable_for_request(
1449
+ session=session,
1450
+ key=key,
1451
+ incoming_turn_state=incoming_turn_state,
1452
+ previous_response_id=previous_response_id,
1453
+ )
1454
+ and _http_bridge_session_matches_preferred_account(
1455
+ session=session,
1456
+ previous_response_id=previous_response_id,
1457
+ preferred_account_id=preferred_account_id,
1458
+ require_preferred_account=require_preferred_account,
1459
+ )
1460
+ ):
1461
+ current_instance = settings.http_responses_session_bridge_instance_id
1462
+ if _durable_bridge_lookup_allows_local_reuse(durable_lookup, current_instance=current_instance):
1463
+ session.api_key = api_key
1464
+ session.request_model = request_model
1465
+ session.request_service_tier = request_service_tier
1466
+ session.last_used_at = clock_for(self).monotonic()
1467
+ return session
1468
+ if force_goal_restart_account_reselection or (
1469
+ not session.closed and session.account.status in ROUTABLE_STATUSES
1470
+ ):
1471
+ old_account_id = session.account.id
1472
+ retiring_with_visible_requests = _http_bridge_session_retiring_with_visible_requests(session)
1473
+ async with self._http_bridge_lock:
1474
+ detached = self._detach_http_bridge_session_locked(
1475
+ key,
1476
+ expected_session=session,
1477
+ mark_closed=not retiring_with_visible_requests,
1478
+ )
1479
+ if detached is not None:
1480
+ force_durable_takeover_after_detach = True
1481
+ if detached is not None and not retiring_with_visible_requests:
1482
+ self._schedule_http_bridge_session_closes([detached], reason="registry_detach")
1483
+ continue
1484
+ created_session: _HTTPBridgeSession | None = None
1485
+ session_registered = False
1486
+ try:
1487
+ create_session = self._create_http_bridge_session
1488
+ preferred_account_is_continuity_owner = preferred_account_id is not None and (
1489
+ preferred_account_has_continuity_provenance
1490
+ or previous_response_id is not None
1491
+ or is_http_bridge_account_neutral_replay(
1492
+ kind=key.affinity_kind,
1493
+ key=key.affinity_key,
1494
+ )
1495
+ )
1496
+ create_kwargs: dict[str, Any] = {
1497
+ "headers": headers,
1498
+ "affinity": affinity,
1499
+ "api_key": api_key,
1500
+ "request_model": request_model,
1501
+ "request_service_tier": request_service_tier,
1502
+ "idle_ttl_seconds": effective_idle_ttl_seconds,
1503
+ "request_stage": request_stage,
1504
+ "preferred_account_id": preferred_account_id,
1505
+ "require_preferred_account": require_preferred_account,
1506
+ "preferred_account_is_continuity_owner": preferred_account_is_continuity_owner,
1507
+ "fallback_on_preferred_account_unavailable": (
1508
+ fallback_on_preferred_account_unavailable and not require_preferred_account
1509
+ ),
1510
+ "request_usage_budget": request_usage_budget,
1511
+ "request_deadline": request_deadline,
1512
+ "exclude_account_ids": exclude_account_ids,
1513
+ "deferred_account_backoff_lifecycle": deferred_account_backoff_lifecycle,
1514
+ "defer_account_health_writes": defer_account_health_writes,
1515
+ }
1516
+ try:
1517
+ create_signature = inspect.signature(create_session)
1518
+ except (TypeError, ValueError):
1519
+ create_signature = None
1520
+ create_accepts_var_keyword = create_signature is not None and any(
1521
+ parameter.kind is inspect.Parameter.VAR_KEYWORD
1522
+ for parameter in create_signature.parameters.values()
1523
+ )
1524
+ if create_signature is not None and not create_accepts_var_keyword:
1525
+ for optional_kwarg in (
1526
+ "request_service_tier",
1527
+ "request_usage_budget",
1528
+ "request_deadline",
1529
+ "exclude_account_ids",
1530
+ "preferred_account_is_continuity_owner",
1531
+ "deferred_account_backoff_lifecycle",
1532
+ "defer_account_health_writes",
1533
+ ):
1534
+ if optional_kwarg not in create_signature.parameters:
1535
+ create_kwargs.pop(optional_kwarg, None)
1536
+ created_session = await create_session(key, **create_kwargs)
1537
+ await _raise_if_http_bridge_creation_superseded(self, key, inflight_future=inflight_future)
1538
+ claim_kwargs: dict[str, Any] = {
1539
+ "allow_takeover": _http_bridge_claim_allows_takeover(
1540
+ durable_lookup,
1541
+ force=force_durable_takeover,
1542
+ ),
1543
+ "force_owner_epoch_advance": (force_durable_takeover or same_replica_durable_predecessor),
1544
+ }
1545
+ restart_takeover = durable_lookup is not None and _http_bridge_allow_durable_takeover(durable_lookup)
1546
+ if restart_takeover:
1547
+ # restart_takeover means recovering a row whose previous
1548
+ # owner is genuinely gone. Every claim now advances the
1549
+ # epoch, so epoch > 1 alone would also count ordinary
1550
+ # local successor claims (no pre-claim lookup, or a
1551
+ # forced replace of a live local session).
1552
+ claim_kwargs["record_restart_takeover"] = True
1553
+ await self._claim_durable_http_bridge_session(created_session, **claim_kwargs)
1554
+ async with self._http_bridge_lock:
1555
+ current_future = self._http_bridge_inflight_sessions.get(key)
1556
+ if current_future is inflight_future:
1557
+ self._http_bridge_inflight_sessions.pop(key, None)
1558
+ if original_request_unanchored:
1559
+ _reserve_http_bridge_unanchored_handoff(created_session, request_scope_id=request_scope_id)
1560
+ self._http_bridge_sessions[key] = created_session
1561
+ session_registered = True
1562
+ if inflight_future is not None and not inflight_future.done():
1563
+ inflight_future.set_result(created_session)
1564
+ if not session_registered:
1565
+ raise _http_bridge_startup_wait_timeout_error(
1566
+ "http_bridge_session_registration",
1567
+ code="capacity_exhausted_active_sessions",
1568
+ )
1569
+ except BaseException as exc:
1570
+ superseded = await _settle_failed_http_bridge_creation(
1571
+ self,
1572
+ key,
1573
+ inflight_future=inflight_future,
1574
+ created_session=created_session,
1575
+ exc=exc,
1576
+ )
1577
+ if created_session is not None and not session_registered:
1578
+ await self._close_http_bridge_session(
1579
+ created_session,
1580
+ release_durable_session=not superseded,
1581
+ )
1582
+ raise
1583
+ assert created_session is not None
1584
+ _log_http_bridge_event(
1585
+ "create",
1586
+ key,
1587
+ account_id=created_session.account.id,
1588
+ model=created_session.request_model,
1589
+ detail=(
1590
+ f"request_stage={request_stage}, preferred_account_id={preferred_account_id}, "
1591
+ f"selected_account_id={created_session.account.id}, "
1592
+ f"durable_session_id={created_session.durable_session_id}"
1593
+ ),
1594
+ cache_key_family=key.affinity_kind,
1595
+ model_class=_extract_model_class(created_session.request_model)
1596
+ if created_session.request_model
1597
+ else None,
1598
+ )
1599
+ if old_account_id is not None and old_account_id != created_session.account.id:
1600
+ _log_http_bridge_event(
1601
+ "reallocation_orphan",
1602
+ key,
1603
+ account_id=created_session.account.id,
1604
+ model=created_session.request_model,
1605
+ detail=f"old_account={old_account_id}",
1606
+ cache_key_family=key.affinity_kind,
1607
+ model_class=_extract_model_class(created_session.request_model)
1608
+ if created_session.request_model
1609
+ else None,
1610
+ )
1611
+ return created_session
1612
+
1613
+ async def mark_http_bridge_draining(self) -> bool:
1614
+ try:
1615
+ await self._durable_bridge.mark_instance_draining(
1616
+ instance_id=_service_get_settings().http_responses_session_bridge_instance_id,
1617
+ )
1618
+ except Exception:
1619
+ logger.warning("Failed to mark durable HTTP bridge sessions draining", exc_info=True)
1620
+ return False
1621
+ return True
1622
+
1623
+ def _prune_http_bridge_sessions_locked(self) -> list["_HTTPBridgeSession"]:
1624
+ now = clock_for(self).monotonic()
1625
+ stale_keys: list[_HTTPBridgeSessionKey] = []
1626
+ for key, session in self._http_bridge_sessions.items():
1627
+ if _http_bridge_session_has_admission_waiter(session):
1628
+ continue
1629
+ if session.handoff_in_progress:
1630
+ continue
1631
+ if session.closed:
1632
+ stale_keys.append(key)
1633
+ continue
1634
+ if getattr(session, "unanchored_reservation_id", None) is not None:
1635
+ continue
1636
+ pending_count = self._http_bridge_pending_count_nowait(session, context="idle_prune")
1637
+ if pending_count is None:
1638
+ continue
1639
+ if pending_count:
1640
+ continue
1641
+ if now - session.last_used_at < session.idle_ttl_seconds:
1642
+ continue
1643
+ stale_keys.append(key)
1644
+ sessions_to_close: list[_HTTPBridgeSession] = []
1645
+ for key in stale_keys:
1646
+ session = self._detach_http_bridge_session_locked(key)
1647
+ if session is not None:
1648
+ _log_http_bridge_event(
1649
+ "evict_idle",
1650
+ key,
1651
+ account_id=session.account.id,
1652
+ model=session.request_model,
1653
+ cache_key_family=key.affinity_kind,
1654
+ model_class=_extract_model_class(session.request_model) if session.request_model else None,
1655
+ )
1656
+ sessions_to_close.append(session)
1657
+ return sessions_to_close
1658
+
1659
+ _close_http_bridge_session = _helpers_close_http_bridge_session
1660
+
1661
+ async def _create_http_bridge_session(
1662
+ self,
1663
+ key: "_HTTPBridgeSessionKey",
1664
+ *,
1665
+ headers: dict[str, str],
1666
+ affinity: _AffinityPolicy,
1667
+ api_key: ApiKeyData | None,
1668
+ request_model: str | None,
1669
+ idle_ttl_seconds: float,
1670
+ request_service_tier: str | None = None,
1671
+ request_stage: str = "first_turn",
1672
+ preferred_account_id: str | None = None,
1673
+ require_preferred_account: bool = False,
1674
+ preferred_account_is_continuity_owner: bool = False,
1675
+ fallback_on_preferred_account_unavailable: bool = True,
1676
+ request_usage_budget: ApiKeyRequestUsageBudget | None = None,
1677
+ request_deadline: float | None = None,
1678
+ exclude_account_ids: Collection[str] | None = None,
1679
+ deferred_account_backoff_lifecycle: _DeferredAccountBackoffLifecycle | None = None,
1680
+ defer_account_health_writes: bool = False,
1681
+ ) -> "_HTTPBridgeSession":
1682
+ request_state = _WebSocketRequestState(
1683
+ request_id=f"http_bridge_connect_{uuid4().hex}",
1684
+ model=request_model,
1685
+ service_tier=request_service_tier,
1686
+ requested_service_tier=request_service_tier,
1687
+ reasoning_effort=None,
1688
+ api_key_reservation=None,
1689
+ started_at=clock_for(self).monotonic(),
1690
+ thread_affinity_last_touch_at=clock_for(self).monotonic(),
1691
+ api_key_reservation_last_touch_at=clock_for(self).monotonic(),
1692
+ transport=_REQUEST_TRANSPORT_HTTP,
1693
+ )
1694
+ deadline = (
1695
+ request_deadline
1696
+ if request_deadline is not None
1697
+ else _websocket_connect_deadline(
1698
+ request_state,
1699
+ _http_bridge_request_budget_seconds(_service_get_settings()),
1700
+ now=clock_for(self).monotonic(),
1701
+ )
1702
+ )
1703
+ settings = await _service_get_settings_cache().get()
1704
+ excluded_account_ids = set(exclude_account_ids or ())
1705
+ if require_preferred_account:
1706
+ fallback_on_preferred_account_unavailable = False
1707
+ retry_same_account_once = preferred_account_id is not None
1708
+ proxy_connect_failover = _HTTPBridgePreDispatchFailover(
1709
+ excluded_account_ids,
1710
+ preferred_account_id,
1711
+ affinity.reallocate_sticky,
1712
+ )
1713
+ selected_account_lease: AccountLease | None = None
1714
+ while True:
1715
+ select_kwargs = {
1716
+ "request_id": request_state.request_log_id or request_state.request_id,
1717
+ "kind": "http_bridge",
1718
+ "request_stage": request_stage,
1719
+ "api_key": api_key,
1720
+ "affinity_policy": (
1721
+ replace(affinity, reallocate_sticky=True)
1722
+ if proxy_connect_failover.reallocate_sticky and not affinity.reallocate_sticky
1723
+ else affinity
1724
+ ),
1725
+ "prefer_earlier_reset_accounts": settings.prefer_earlier_reset_accounts,
1726
+ "prefer_earlier_reset_window": _prefer_earlier_reset_window(settings),
1727
+ "routing_strategy": _routing_strategy(settings),
1728
+ "model": request_model,
1729
+ "service_tier": request_service_tier,
1730
+ "exclude_account_ids": excluded_account_ids,
1731
+ "preferred_account_id": proxy_connect_failover.preferred_account_id,
1732
+ "preferred_account_is_continuity_owner": preferred_account_is_continuity_owner,
1733
+ "lease_kind": "stream",
1734
+ "estimated_lease_tokens": _estimated_lease_tokens_from_request_usage_budget(request_usage_budget),
1735
+ "fallback_on_preferred_account_unavailable": fallback_on_preferred_account_unavailable,
1736
+ }
1737
+ selection = await self._select_account_with_budget_for_stream(deadline, **select_kwargs)
1738
+ selected_account_lease = selection.lease
1739
+ account = selection.account
1740
+ if account is None:
1741
+ await self._load_balancer.release_account_lease(selected_account_lease)
1742
+ selected_account_lease = None
1743
+ _record_same_account_takeover(
1744
+ preferred_account_id=preferred_account_id,
1745
+ selected_account_id=None,
1746
+ )
1747
+ if proxy_connect_failover.last_error is not None:
1748
+ # Preserve a confirmed pre-dispatch route failure instead
1749
+ # of generating ``no_accounts``.
1750
+ raise proxy_connect_failover.last_error
1751
+ if (
1752
+ require_preferred_account
1753
+ and preferred_account_id is not None
1754
+ and preferred_account_is_continuity_owner
1755
+ and selection.error_code in (CONTINUITY_OWNER_UNAVAILABLE, "hard_affinity_saturated")
1756
+ ):
1757
+ raise _http_bridge_previous_response_owner_unavailable_error()
1758
+ status_code, error_payload = selection_failure_response(selection)
1759
+ raise ProxyResponseError(status_code, error_payload)
1760
+ if require_preferred_account and preferred_account_id is not None and account.id != preferred_account_id:
1761
+ await self._load_balancer.release_account_lease(selected_account_lease)
1762
+ selected_account_lease = None
1763
+ _record_same_account_takeover(
1764
+ preferred_account_id=preferred_account_id,
1765
+ selected_account_id=account.id,
1766
+ )
1767
+ if preferred_account_is_continuity_owner:
1768
+ raise _http_bridge_previous_response_owner_unavailable_error()
1769
+ raise ProxyResponseError(
1770
+ 503,
1771
+ openai_error(
1772
+ "preferred_account_unavailable",
1773
+ "Preferred account is unavailable; retry later.",
1774
+ error_type="server_error",
1775
+ ),
1776
+ )
1777
+ selected_is_preferred = preferred_account_id is not None and account.id == preferred_account_id
1778
+ try:
1779
+ account = await self._ensure_fresh_with_budget(
1780
+ account,
1781
+ timeout_seconds=self._remaining_budget_seconds(deadline),
1782
+ )
1783
+ connect_headers = _websocket_safe_headers_with_turn_state(
1784
+ headers, _sticky_key_from_turn_state_header(headers)
1785
+ )
1786
+ upstream = await _call_with_supported_optional_kwargs(
1787
+ self._open_upstream_websocket_with_budget,
1788
+ account,
1789
+ connect_headers,
1790
+ optional_kwargs={"request_state": request_state},
1791
+ timeout_seconds=self._remaining_budget_seconds(deadline),
1792
+ )
1793
+ _record_same_account_takeover(
1794
+ preferred_account_id=preferred_account_id,
1795
+ selected_account_id=account.id,
1796
+ )
1797
+ break
1798
+ except ProxyResponseError as exc:
1799
+ if await proxy_connect_failover.handle(
1800
+ self,
1801
+ account,
1802
+ selected_account_lease,
1803
+ exc,
1804
+ required_account=require_preferred_account and selected_is_preferred,
1805
+ deferred_account_backoff_lifecycle=deferred_account_backoff_lifecycle,
1806
+ defer_account_health_write=defer_account_health_writes,
1807
+ ):
1808
+ selected_account_lease = None
1809
+ continue
1810
+ if exc.status_code != 401 or self._remaining_budget_seconds(deadline) <= 0:
1811
+ await self._load_balancer.release_account_lease(selected_account_lease)
1812
+ selected_account_lease = None
1813
+ raise
1814
+ try:
1815
+ account = await self._ensure_fresh_with_budget(
1816
+ account,
1817
+ force=True,
1818
+ timeout_seconds=self._remaining_budget_seconds(deadline),
1819
+ )
1820
+ connect_headers = _websocket_safe_headers_with_turn_state(
1821
+ headers, _sticky_key_from_turn_state_header(headers)
1822
+ )
1823
+ upstream = await self._open_upstream_websocket_with_budget(
1824
+ account,
1825
+ connect_headers,
1826
+ timeout_seconds=self._remaining_budget_seconds(deadline),
1827
+ request_state=request_state,
1828
+ )
1829
+ _record_same_account_takeover(
1830
+ preferred_account_id=preferred_account_id,
1831
+ selected_account_id=account.id,
1832
+ )
1833
+ break
1834
+ except ProxyResponseError as retry_exc:
1835
+ if await proxy_connect_failover.handle(
1836
+ self,
1837
+ account,
1838
+ selected_account_lease,
1839
+ retry_exc,
1840
+ required_account=require_preferred_account and selected_is_preferred,
1841
+ deferred_account_backoff_lifecycle=deferred_account_backoff_lifecycle,
1842
+ defer_account_health_write=defer_account_health_writes,
1843
+ ):
1844
+ selected_account_lease = None
1845
+ continue
1846
+ if retry_exc.status_code != 401:
1847
+ await self._load_balancer.release_account_lease(selected_account_lease)
1848
+ selected_account_lease = None
1849
+ raise
1850
+ await self._handle_proxy_error(account, retry_exc)
1851
+ if require_preferred_account and selected_is_preferred:
1852
+ await self._load_balancer.release_account_lease(selected_account_lease)
1853
+ selected_account_lease = None
1854
+ raise
1855
+ excluded_account_ids.add(account.id)
1856
+ proxy_connect_failover.preferred_account_id = None
1857
+ await self._load_balancer.release_account_lease(selected_account_lease)
1858
+ selected_account_lease = None
1859
+ continue
1860
+ except RefreshError as refresh_exc:
1861
+ if refresh_exc.is_permanent:
1862
+ await self._load_balancer.mark_permanent_failure(account, refresh_exc.code)
1863
+ if require_preferred_account and selected_is_preferred:
1864
+ await self._load_balancer.release_account_lease(selected_account_lease)
1865
+ selected_account_lease = None
1866
+ raise
1867
+ excluded_account_ids.add(account.id)
1868
+ proxy_connect_failover.preferred_account_id = None
1869
+ await self._load_balancer.release_account_lease(selected_account_lease)
1870
+ selected_account_lease = None
1871
+ continue
1872
+ except RefreshError as exc:
1873
+ if exc.is_permanent:
1874
+ await self._load_balancer.mark_permanent_failure(account, exc.code)
1875
+ if selected_is_preferred and self._remaining_budget_seconds(deadline) > 0:
1876
+ if retry_same_account_once and not exc.is_permanent:
1877
+ retry_same_account_once = False
1878
+ await self._load_balancer.release_account_lease(selected_account_lease)
1879
+ selected_account_lease = None
1880
+ continue
1881
+ if require_preferred_account:
1882
+ await self._load_balancer.release_account_lease(selected_account_lease)
1883
+ selected_account_lease = None
1884
+ raise ProxyResponseError(
1885
+ 503,
1886
+ openai_error(
1887
+ "no_accounts",
1888
+ "Preferred account is unavailable; retry later.",
1889
+ error_type="server_error",
1890
+ ),
1891
+ ) from exc
1892
+ excluded_account_ids.add(account.id)
1893
+ proxy_connect_failover.preferred_account_id = None
1894
+ await self._load_balancer.release_account_lease(selected_account_lease)
1895
+ selected_account_lease = None
1896
+ continue
1897
+ if exc.is_permanent:
1898
+ await self._load_balancer.release_account_lease(selected_account_lease)
1899
+ selected_account_lease = None
1900
+ raise ProxyResponseError(
1901
+ 401,
1902
+ openai_error(
1903
+ "invalid_api_key",
1904
+ exc.message,
1905
+ error_type="authentication_error",
1906
+ ),
1907
+ ) from exc
1908
+ if request_stage == "first_turn":
1909
+ _record_bridge_first_turn_timeout()
1910
+ await self._load_balancer.release_account_lease(selected_account_lease)
1911
+ selected_account_lease = None
1912
+ _raise_proxy_unavailable(exc.message or "Temporary upstream refresh failure")
1913
+ except (aiohttp.ClientError, asyncio.TimeoutError) as exc:
1914
+ if selected_is_preferred and self._remaining_budget_seconds(deadline) > 0:
1915
+ if retry_same_account_once:
1916
+ retry_same_account_once = False
1917
+ await self._load_balancer.release_account_lease(selected_account_lease)
1918
+ selected_account_lease = None
1919
+ continue
1920
+ if require_preferred_account:
1921
+ await self._load_balancer.release_account_lease(selected_account_lease)
1922
+ selected_account_lease = None
1923
+ raise ProxyResponseError(
1924
+ 503,
1925
+ openai_error(
1926
+ "no_accounts",
1927
+ "Preferred account is unavailable; retry later.",
1928
+ error_type="server_error",
1929
+ ),
1930
+ ) from exc
1931
+ excluded_account_ids.add(account.id)
1932
+ proxy_connect_failover.preferred_account_id = None
1933
+ await self._load_balancer.release_account_lease(selected_account_lease)
1934
+ selected_account_lease = None
1935
+ continue
1936
+ if request_stage == "first_turn":
1937
+ _record_bridge_first_turn_timeout()
1938
+ await self._load_balancer.release_account_lease(selected_account_lease)
1939
+ selected_account_lease = None
1940
+ _raise_proxy_unavailable(str(exc) or "Request to upstream timed out")
1941
+ except BaseException:
1942
+ await self._load_balancer.release_account_lease(selected_account_lease)
1943
+ selected_account_lease = None
1944
+ raise
1945
+ session = _HTTPBridgeSession(
1946
+ key=key,
1947
+ headers=connect_headers,
1948
+ affinity=_persistent_http_bridge_affinity(affinity),
1949
+ api_key=api_key,
1950
+ request_model=request_model,
1951
+ request_service_tier=request_service_tier,
1952
+ account=account,
1953
+ upstream=upstream,
1954
+ upstream_control=_WebSocketUpstreamControl(),
1955
+ pending_requests=deque(),
1956
+ pending_lock=fast_lock(),
1957
+ response_create_gate=asyncio.Semaphore(1),
1958
+ queued_request_count=0,
1959
+ lifecycle_lock=fast_lock(),
1960
+ last_used_at=clock_for(self).monotonic(),
1961
+ idle_ttl_seconds=idle_ttl_seconds,
1962
+ codex_session=(affinity.kind == StickySessionKind.CODEX_SESSION or key.affinity_kind == "thread_header"),
1963
+ access_token_expires_at=_token_expiry(account, self._encryptor),
1964
+ prewarm_lock=fast_lock(),
1965
+ upstream_turn_state=_upstream_turn_state_from_socket(upstream),
1966
+ downstream_turn_state=None,
1967
+ account_lease=selected_account_lease,
1968
+ catalog_omission_quota_admission=selection.catalog_omission_quota_admission,
1969
+ )
1970
+ _copy_websocket_route_metadata_to_session(session, request_state)
1971
+ session.upstream_reader = self._spawn_http_bridge_upstream_reader(session)
1972
+ return session
1973
+
1974
+ async def _reconnect_http_bridge_session(
1975
+ self,
1976
+ session: "_HTTPBridgeSession",
1977
+ *,
1978
+ request_state: _WebSocketRequestState,
1979
+ restart_reader: bool = False,
1980
+ require_security_work_authorized: bool = False,
1981
+ require_same_account: bool = False,
1982
+ require_preferred_account: bool = False,
1983
+ owner_rebind_affinity: _AffinityPolicy | None = None,
1984
+ selection_affinity: _AffinityPolicy | None = None,
1985
+ dashboard_settings: DashboardSettings | None = None,
1986
+ ) -> None:
1987
+ request_state.response_create_sent_at = None
1988
+ goal_restart = request_state.affinity_policy.abandon_unavailable_legacy_owner
1989
+ if selection_affinity is None and goal_restart:
1990
+ selection_affinity = request_state.affinity_policy
1991
+ account_neutral_recovery = is_http_bridge_account_neutral_replay(
1992
+ kind=session.key.affinity_kind, key=session.key.affinity_key
1993
+ )
1994
+ require_same_account = account_neutral_recovery or (require_same_account and not goal_restart)
1995
+ old_upstream = session.upstream
1996
+ old_reader = session.upstream_reader if restart_reader else None
1997
+ session.handoff_in_progress = True
1998
+ inflight_sessions = self._http_bridge_inflight_sessions
1999
+ handoff_future = inflight_sessions.get(session.key) or asyncio.get_running_loop().create_future()
2000
+ inflight_sessions.setdefault(session.key, handoff_future)
2001
+ setattr(handoff_future, "_http_bridge_handoff", True)
2002
+ session.handoff_future = handoff_future
2003
+ session.closed = True
2004
+ if old_reader is not None:
2005
+ if old_reader is not asyncio.current_task():
2006
+ try:
2007
+ cancelled = await self._cancel_http_bridge_previous_reader(old_reader)
2008
+ except BaseException:
2009
+ session.closed = True
2010
+ _complete_http_bridge_handoff(session, self._http_bridge_inflight_sessions)
2011
+ raise
2012
+ if not cancelled:
2013
+ session.closed = True
2014
+ _complete_http_bridge_handoff(session, self._http_bridge_inflight_sessions)
2015
+ raise ProxyResponseError(
2016
+ 502,
2017
+ openai_error(
2018
+ "upstream_unavailable",
2019
+ "HTTP responses session bridge reader did not shut down cleanly",
2020
+ ),
2021
+ )
2022
+ deadline = _websocket_connect_deadline(
2023
+ request_state,
2024
+ _http_bridge_request_budget_seconds(_service_get_settings()),
2025
+ now=clock_for(self).monotonic(),
2026
+ )
2027
+ try:
2028
+ # Callers that reconnect while holding a lock resolve this row
2029
+ # before taking it and pass it in. The prewarm timeout recovery is
2030
+ # one: it reconnects under ``prewarm_lock``, and a refresh behind
2031
+ # this read runs a DB query under a process-global lock, so
2032
+ # awaiting it there would suspend the critical section (issues
2033
+ # #1971/#1972).
2034
+ settings = (
2035
+ dashboard_settings if dashboard_settings is not None else await _service_get_settings_cache().get()
2036
+ )
2037
+ session.api_key = request_state.api_key
2038
+ forced_refresh_account_id = request_state.force_refresh_account_id
2039
+ excluded_account_ids: set[str] = set(request_state.excluded_account_ids)
2040
+ requested_preferred_account_id = resolve_reconnect_preferred_account_id(
2041
+ request_state, session.account.id, require_preferred_account, account_neutral_recovery
2042
+ )
2043
+ required_preferred_account_id = resolve_required_account_id(
2044
+ ("requested reconnect owner", requested_preferred_account_id),
2045
+ ("account-neutral recovery", session.account.id if account_neutral_recovery else None),
2046
+ )
2047
+ close_skips_account = session.last_upstream_close_code in _UPSTREAM_CLOSE_CODES_SKIP_SAME_ACCOUNT_RETRY
2048
+ hard_close_account_bound = session.key.strength == "hard" and (close_skips_account or require_same_account)
2049
+ skip_same_account = (
2050
+ session.key.strength != "hard" and close_skips_account and required_preferred_account_id is None
2051
+ )
2052
+ if required_preferred_account_id is not None and required_preferred_account_id in excluded_account_ids:
2053
+ session.closed = True
2054
+ _complete_http_bridge_handoff(session, self._http_bridge_inflight_sessions)
2055
+ raise _http_bridge_previous_response_owner_unavailable_error()
2056
+ _require_http_bridge_bound_account_not_excluded(
2057
+ hard_close_account_bound, session.account.id, excluded_account_ids
2058
+ )
2059
+ except BaseException:
2060
+ session.closed = True
2061
+ _complete_http_bridge_handoff(session, self._http_bridge_inflight_sessions)
2062
+ raise
2063
+ if skip_same_account:
2064
+ excluded_account_ids.add(session.account.id)
2065
+ retry_same_account_once = not skip_same_account and session.account.id not in excluded_account_ids
2066
+ if skip_same_account:
2067
+ preferred_candidate_id: str | None = None
2068
+ elif hard_close_account_bound and session.account.id not in excluded_account_ids:
2069
+ preferred_candidate_id = session.account.id
2070
+ elif required_preferred_account_id is not None:
2071
+ preferred_candidate_id = required_preferred_account_id
2072
+ elif forced_refresh_account_id is not None:
2073
+ preferred_candidate_id = forced_refresh_account_id
2074
+ elif request_state.preferred_account_id is not None:
2075
+ preferred_candidate_id = request_state.preferred_account_id
2076
+ elif session.account.id not in excluded_account_ids:
2077
+ preferred_candidate_id = session.account.id
2078
+ else:
2079
+ preferred_candidate_id = None
2080
+ selected_account_lease: AccountLease | None = None
2081
+ selected_account_model_replacement = False
2082
+ file_owner = request_state.file_required_preferred_account
2083
+
2084
+ def record_selected_account_takeover(
2085
+ selected_account_id: str | None, preferred_account_id: str | None = session.account.id
2086
+ ) -> None:
2087
+ _record_same_account_takeover(
2088
+ preferred_account_id=preferred_account_id,
2089
+ selected_account_id=selected_account_id,
2090
+ )
2091
+
2092
+ async def release_selected_account_lease() -> None:
2093
+ nonlocal selected_account_lease
2094
+ lease, selected_account_lease = selected_account_lease, None
2095
+ if lease is None:
2096
+ return
2097
+ async with session.pending_lock:
2098
+ if session.account_lease is not None and lease.lease_id == session.account_lease.lease_id:
2099
+ session.account_lease = None
2100
+ try:
2101
+ await self._load_balancer.release_account_lease(lease)
2102
+ except BaseException:
2103
+ complete_failed_handoff()
2104
+ raise
2105
+
2106
+ async def abandon_selected_account_retry(selected_account: Any) -> None:
2107
+ nonlocal preferred_candidate_id
2108
+ if hard_close_account_bound or selected_account_model_replacement:
2109
+ await release_selected_account_lease()
2110
+ complete_failed_handoff()
2111
+ raise
2112
+ excluded_account_ids.add(selected_account.id)
2113
+ preferred_candidate_id = None
2114
+ await release_selected_account_lease()
2115
+
2116
+ async def open_replacement_upstream(selected_account: Any, selected_headers: dict[str, str]) -> Any:
2117
+ try:
2118
+ return await self._open_upstream_websocket_with_budget(
2119
+ selected_account,
2120
+ selected_headers,
2121
+ timeout_seconds=self._remaining_budget_seconds(deadline),
2122
+ request_state=request_state,
2123
+ )
2124
+ except Exception:
2125
+ session.closed = True
2126
+ raise
2127
+
2128
+ def complete_failed_handoff() -> None:
2129
+ session.closed = True
2130
+ _mark_http_bridge_reader_handoff_reconnect_failed(session, old_reader)
2131
+ _complete_http_bridge_handoff(session, self._http_bridge_inflight_sessions)
2132
+
2133
+ def require_bound_account() -> None:
2134
+ try:
2135
+ _require_http_bridge_bound_account_not_excluded(
2136
+ hard_close_account_bound, session.account.id, excluded_account_ids
2137
+ )
2138
+ except BaseException:
2139
+ complete_failed_handoff()
2140
+ raise
2141
+
2142
+ while True:
2143
+ reuse_current_account_lease = preferred_candidate_id == session.account.id and bool(session.account_lease)
2144
+ try:
2145
+ selection = await self._select_account_with_budget_for_stream(
2146
+ deadline,
2147
+ request_id=request_state.request_log_id or request_state.request_id,
2148
+ kind="http_bridge",
2149
+ request_stage="reattach",
2150
+ api_key=session.api_key,
2151
+ affinity_policy=selection_affinity or session.affinity,
2152
+ prefer_earlier_reset_accounts=settings.prefer_earlier_reset_accounts,
2153
+ prefer_earlier_reset_window=_prefer_earlier_reset_window(settings),
2154
+ routing_strategy=_routing_strategy(settings),
2155
+ model=session.request_model,
2156
+ service_tier=session.request_service_tier,
2157
+ exclude_account_ids=excluded_account_ids,
2158
+ preferred_account_id=preferred_candidate_id,
2159
+ preferred_account_is_continuity_owner=account_neutral_recovery or file_owner,
2160
+ preferred_account_overrides_single_account_routing=file_owner,
2161
+ require_security_work_authorized=require_security_work_authorized,
2162
+ lease_kind=None if reuse_current_account_lease else "stream",
2163
+ estimated_lease_tokens=_estimated_lease_tokens_from_request_usage_budget(
2164
+ request_state.request_usage_budget
2165
+ ),
2166
+ fallback_on_preferred_account_unavailable=(
2167
+ not reuse_current_account_lease
2168
+ and not hard_close_account_bound
2169
+ and required_preferred_account_id is None
2170
+ ),
2171
+ )
2172
+ except BaseException:
2173
+ complete_failed_handoff()
2174
+ raise
2175
+ account = selection.account
2176
+ if account is None:
2177
+ try:
2178
+ await release_selected_account_lease()
2179
+ except BaseException:
2180
+ complete_failed_handoff()
2181
+ raise
2182
+ if required_preferred_account_id is not None and selection.continuity_owner_no_longer_exists:
2183
+ complete_failed_handoff()
2184
+ raise _http_bridge_previous_response_owner_unavailable_error()
2185
+ if (
2186
+ reuse_current_account_lease
2187
+ and not hard_close_account_bound
2188
+ and required_preferred_account_id is None
2189
+ and self._remaining_budget_seconds(deadline) > 0
2190
+ ):
2191
+ preferred_candidate_id = None
2192
+ continue
2193
+ if selection.error_code == USAGE_LIMIT_REACHED and (
2194
+ required_preferred_account_id is not None or hard_close_account_bound
2195
+ ):
2196
+ complete_failed_handoff()
2197
+ raise _http_bridge_previous_response_owner_unavailable_error()
2198
+ if selection.error_code == USAGE_LIMIT_REACHED:
2199
+ record_selected_account_takeover(None)
2200
+ status_code, error_payload = selection_failure_response(selection)
2201
+ complete_failed_handoff()
2202
+ raise ProxyResponseError(status_code, error_payload)
2203
+ try:
2204
+ should_retry_selection = await _sleep_for_account_selection_recovery(
2205
+ selection,
2206
+ request_id=request_state.request_log_id or request_state.request_id,
2207
+ kind="http_bridge",
2208
+ request_stage="reattach",
2209
+ model=session.request_model,
2210
+ max_sleep_seconds=self._remaining_budget_seconds(deadline),
2211
+ request_state=request_state,
2212
+ scheduler=scheduler_for(self),
2213
+ clock=clock_for(self),
2214
+ )
2215
+ except BaseException:
2216
+ complete_failed_handoff()
2217
+ raise
2218
+ if should_retry_selection:
2219
+ excluded_account_ids.update(request_state.excluded_account_ids)
2220
+ if required_preferred_account_id in excluded_account_ids:
2221
+ complete_failed_handoff()
2222
+ raise _http_bridge_previous_response_owner_unavailable_error()
2223
+ if skip_same_account:
2224
+ excluded_account_ids.add(session.account.id)
2225
+ require_bound_account()
2226
+ retry_same_account_once = not skip_same_account and session.account.id not in excluded_account_ids
2227
+ if skip_same_account:
2228
+ preferred_candidate_id = None
2229
+ elif hard_close_account_bound and session.account.id not in excluded_account_ids:
2230
+ preferred_candidate_id = session.account.id
2231
+ elif required_preferred_account_id is not None:
2232
+ preferred_candidate_id = required_preferred_account_id
2233
+ elif forced_refresh_account_id is not None:
2234
+ preferred_candidate_id = forced_refresh_account_id
2235
+ elif request_state.preferred_account_id is not None:
2236
+ preferred_candidate_id = request_state.preferred_account_id
2237
+ elif session.account.id not in excluded_account_ids:
2238
+ preferred_candidate_id = session.account.id
2239
+ else:
2240
+ preferred_candidate_id = None
2241
+ continue
2242
+ record_selected_account_takeover(None)
2243
+ complete_failed_handoff()
2244
+ raise _http_bridge_reconnect_selection_failure(selection, required_preferred_account_id)
2245
+ if required_preferred_account_id is not None and account.id != required_preferred_account_id:
2246
+ if selection.lease is not None:
2247
+ selected_account_lease = selection.lease
2248
+ await release_selected_account_lease()
2249
+ record_selected_account_takeover(account.id, required_preferred_account_id)
2250
+ complete_failed_handoff()
2251
+ raise _http_bridge_previous_response_owner_unavailable_error()
2252
+ selected_account_lease = (
2253
+ session.account_lease
2254
+ if reuse_current_account_lease and account.id == session.account.id
2255
+ else selection.lease
2256
+ )
2257
+ selected_account_model_replacement = (
2258
+ request_state.precreated_replay_reason == _ACCOUNT_MODEL_UNSUPPORTED_ERROR_CODE
2259
+ and account.id != request_state.precreated_replay_account_id
2260
+ )
2261
+ if selected_account_model_replacement:
2262
+ _clear_websocket_precreated_replay_fallback(request_state)
2263
+ selected_is_preferred = account.id == session.account.id
2264
+ force_refresh = forced_refresh_account_id == account.id
2265
+ if forced_refresh_account_id is not None and account.id != forced_refresh_account_id:
2266
+ request_state.force_refresh_account_id = None
2267
+ if request_state.preferred_account_id == forced_refresh_account_id:
2268
+ request_state.preferred_account_id = None
2269
+ try:
2270
+ account = await self._ensure_fresh_with_budget(
2271
+ account,
2272
+ force=force_refresh,
2273
+ timeout_seconds=self._remaining_budget_seconds(deadline),
2274
+ )
2275
+ if force_refresh and request_state.force_refresh_account_id == account.id:
2276
+ request_state.force_refresh_account_id = None
2277
+ connect_headers = _websocket_safe_headers_with_turn_state(
2278
+ session.headers, _http_bridge_reconnect_turn_state(session, account.id, owner_rebind_affinity)
2279
+ )
2280
+ upstream = await open_replacement_upstream(account, connect_headers)
2281
+ _copy_websocket_route_metadata_to_session(session, request_state)
2282
+ record_selected_account_takeover(account.id)
2283
+ break
2284
+ except ProxyResponseError as exc:
2285
+ if exc.status_code != 401 or self._remaining_budget_seconds(deadline) <= 0:
2286
+ await release_selected_account_lease()
2287
+ complete_failed_handoff()
2288
+ raise _http_bridge_reconnect_connect_failure(exc, required_preferred_account_id) from exc
2289
+ try:
2290
+ account = await self._ensure_fresh_with_budget(
2291
+ account,
2292
+ force=True,
2293
+ timeout_seconds=self._remaining_budget_seconds(deadline),
2294
+ )
2295
+ connect_headers = _websocket_safe_headers_with_turn_state(
2296
+ session.headers, _http_bridge_reconnect_turn_state(session, account.id, owner_rebind_affinity)
2297
+ )
2298
+ upstream = await open_replacement_upstream(account, connect_headers)
2299
+ _copy_websocket_route_metadata_to_session(session, request_state)
2300
+ record_selected_account_takeover(account.id)
2301
+ break
2302
+ except ProxyResponseError as retry_exc:
2303
+ if retry_exc.status_code != 401:
2304
+ await release_selected_account_lease()
2305
+ complete_failed_handoff()
2306
+ raise _http_bridge_reconnect_connect_failure(retry_exc, required_preferred_account_id)
2307
+ await self._handle_proxy_error(account, retry_exc)
2308
+ await abandon_selected_account_retry(account)
2309
+ continue
2310
+ except RefreshError as refresh_exc:
2311
+ if refresh_exc.is_permanent:
2312
+ await self._load_balancer.mark_permanent_failure(account, refresh_exc.code)
2313
+ await abandon_selected_account_retry(account)
2314
+ continue
2315
+ except RefreshError as exc:
2316
+ if exc.is_permanent:
2317
+ await self._load_balancer.mark_permanent_failure(account, exc.code)
2318
+ if selected_is_preferred and self._remaining_budget_seconds(deadline) > 0:
2319
+ if retry_same_account_once and not exc.is_permanent:
2320
+ retry_same_account_once = False
2321
+ await release_selected_account_lease()
2322
+ continue
2323
+ await abandon_selected_account_retry(account)
2324
+ continue
2325
+ await release_selected_account_lease()
2326
+ complete_failed_handoff()
2327
+ raise _http_bridge_reconnect_connect_failure(exc, required_preferred_account_id)
2328
+ except (aiohttp.ClientError, asyncio.TimeoutError) as transport_exc:
2329
+ if selected_is_preferred and self._remaining_budget_seconds(deadline) > 0:
2330
+ if retry_same_account_once:
2331
+ retry_same_account_once = False
2332
+ await release_selected_account_lease()
2333
+ continue
2334
+ await abandon_selected_account_retry(account)
2335
+ continue
2336
+ await release_selected_account_lease()
2337
+ complete_failed_handoff()
2338
+ raise _http_bridge_reconnect_connect_failure(transport_exc, required_preferred_account_id)
2339
+ except asyncio.CancelledError:
2340
+ session.closed = True
2341
+ await release_selected_account_lease()
2342
+ complete_failed_handoff()
2343
+ raise
2344
+ except BaseException:
2345
+ await release_selected_account_lease()
2346
+ complete_failed_handoff()
2347
+ raise
2348
+
2349
+ async def abort_selected_handoff() -> None:
2350
+ session.closed = True
2351
+ # ``shield(coroutine)`` would ``ensure_future`` an unowned task; the
2352
+ # scheduler spawns it so a simulation owns the outliving cleanup.
2353
+ scheduler = scheduler_for(self)
2354
+ try:
2355
+ await asyncio.shield(scheduler.create_task(upstream.close(), name="http-bridge-handoff-abort-close"))
2356
+ except BaseException:
2357
+ logger.debug("Failed to close HTTP bridge replacement websocket", exc_info=True)
2358
+ selected_lease = selected_account_lease
2359
+ old_lease = session.account_lease
2360
+ try:
2361
+ await asyncio.shield(
2362
+ scheduler.create_task(release_selected_account_lease(), name="http-bridge-handoff-abort-lease")
2363
+ )
2364
+ except BaseException:
2365
+ logger.debug("Failed to release HTTP bridge replacement lease", exc_info=True)
2366
+ if old_lease is not None and old_lease is not selected_lease:
2367
+ session.account_lease = None
2368
+ try:
2369
+ await asyncio.shield(
2370
+ scheduler.create_task(
2371
+ self._load_balancer.release_account_lease(old_lease),
2372
+ name="http-bridge-handoff-abort-old-lease",
2373
+ )
2374
+ )
2375
+ except BaseException:
2376
+ logger.debug("Failed to release HTTP bridge old account lease", exc_info=True)
2377
+ complete_failed_handoff()
2378
+
2379
+ try:
2380
+ if owner_rebind_affinity is not None:
2381
+ await self._claim_http_bridge_replacement_before_swap(
2382
+ session,
2383
+ account_id=account.id,
2384
+ upstream=upstream,
2385
+ release_selected_account_lease=release_selected_account_lease,
2386
+ owner_rebind_affinity=owner_rebind_affinity,
2387
+ )
2388
+ if owner_rebind_affinity is not None or account.id != session.account.id:
2389
+ await self._unregister_http_bridge_turn_states(session)
2390
+ await self._unregister_http_bridge_previous_response_ids(session)
2391
+ session.last_completed_response_id = None
2392
+ session.last_completed_response_account_id = None
2393
+ session.last_completed_input_count = 0
2394
+ session.last_completed_input_prefix_fingerprint = None
2395
+ session.last_pending_tool_calls.clear()
2396
+ session.affinity = _persistent_http_bridge_affinity(selection_affinity or session.affinity)
2397
+ session.codex_session = session.key.affinity_kind == "thread_header"
2398
+ session.upstream_turn_state = None
2399
+ session.downstream_turn_state = None
2400
+ session.headers = {
2401
+ key: value for key, value in session.headers.items() if key.lower() != "x-codex-turn-state"
2402
+ }
2403
+ await _persist_http_bridge_replacement_account(self, session, account.id)
2404
+ try:
2405
+ await old_upstream.close()
2406
+ except Exception:
2407
+ logger.debug("Failed to close HTTP bridge upstream websocket before reconnect", exc_info=True)
2408
+ session.closed = True
2409
+ if selected_account_lease is not session.account_lease:
2410
+ old_lease = session.account_lease
2411
+ if old_lease is not None:
2412
+ await self._load_balancer.release_account_lease(old_lease)
2413
+ session.account_lease = None
2414
+ session.account_lease = selected_account_lease
2415
+ session.replace_connection(account, connect_headers, upstream, _token_expiry(account, self._encryptor))
2416
+ session.catalog_omission_quota_admission = selection.catalog_omission_quota_admission
2417
+ session.upstream_control = _WebSocketUpstreamControl()
2418
+ session.closed = False
2419
+ session.last_upstream_close_code = None
2420
+ session.upstream_turn_state = _upstream_turn_state_from_socket(upstream) or session.upstream_turn_state
2421
+ _complete_http_bridge_handoff(session, self._http_bridge_inflight_sessions)
2422
+ except BaseException:
2423
+ await abort_selected_handoff()
2424
+ raise
2425
+ if restart_reader:
2426
+ session.upstream_reader = self._spawn_http_bridge_upstream_reader(session)
2427
+ _log_http_bridge_event(
2428
+ "reconnect",
2429
+ session.key,
2430
+ account_id=account.id,
2431
+ model=session.request_model,
2432
+ detail=f"selected_account_id={account.id}, durable_session_id={session.durable_session_id}",
2433
+ cache_key_family=session.key.affinity_kind,
2434
+ model_class=_extract_model_class(session.request_model) if session.request_model else None,
2435
+ )