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,2211 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ import logging
6
+ import sys
7
+ import time
8
+ from collections.abc import Awaitable, Callable, Mapping
9
+ from dataclasses import replace
10
+ from typing import Any, NoReturn, Protocol, TypeVar, cast
11
+
12
+ import aiohttp
13
+ import anyio
14
+ from anyio.lowlevel import checkpoint_if_cancelled
15
+ from pydantic import ValidationError
16
+
17
+ from app.core.auth.refresh import RefreshError, is_transient_refresh_contention, refresh_contention_kind
18
+ from app.core.balancer import ResetPreferenceWindow, RoutingStrategy, failover_decision
19
+ from app.core.clients.proxy import (
20
+ ProxyResponseError,
21
+ UpstreamProxyRouteTrace,
22
+ filter_inbound_headers,
23
+ pop_compact_timeout_overrides,
24
+ push_compact_timeout_overrides,
25
+ )
26
+ from app.core.clients.proxy import compact_responses as core_compact_responses
27
+ from app.core.clients.thread_cache_identity import (
28
+ ThreadCacheIdentity,
29
+ )
30
+ from app.core.clients.thread_cache_identity import (
31
+ effective_thread_cache_identity_mode as _effective_thread_cache_identity_mode,
32
+ )
33
+ from app.core.config.dashboard_overrides import with_dashboard_overrides
34
+ from app.core.config.settings import get_settings
35
+ from app.core.config.settings_cache import get_settings_cache
36
+ from app.core.errors import openai_error
37
+ from app.core.openai.exceptions import ClientPayloadError
38
+ from app.core.openai.models import CompactResponsePayload
39
+ from app.core.openai.requests import ResponsesCompactRequest
40
+ from app.core.resilience.network_recovery import ProcessNetworkRecovery
41
+ from app.core.resilience.toggles import bind_resilience_toggles
42
+ from app.core.types import JsonValue
43
+ from app.core.upstream_proxy import ResolvedUpstreamRoute, UpstreamProxyRouteError
44
+ from app.core.utils.request_id import ensure_request_id, get_request_id
45
+ from app.core.utils.retry import backoff_seconds
46
+ from app.core.utils.shared_future import wait_on_shared_future
47
+ from app.db.models import Account, AccountStatus, DashboardSettings, StickySessionKind
48
+ from app.modules.api_keys.service import (
49
+ ApiKeyData,
50
+ ApiKeyRequestUsageBudget,
51
+ ApiKeyUsageReservationData,
52
+ )
53
+ from app.modules.proxy._service.support import _request_log_client_fields, _RequestLogFailureMetadata
54
+ from app.modules.proxy.affinity import (
55
+ _affinity_with_payload_continuity,
56
+ _AffinityPolicy,
57
+ _bare_codex_session_affinity,
58
+ _is_synthesized_turn_state,
59
+ _owner_lookup_session_id_from_headers,
60
+ _prompt_cache_key_from_request_model,
61
+ _request_allows_bare_session_cap_spillover,
62
+ _resolve_prompt_cache_key,
63
+ _sticky_key_from_turn_state_header,
64
+ _thread_codex_session_affinity,
65
+ )
66
+ from app.modules.proxy.affinity_observation import AffinityObservation
67
+ from app.modules.proxy.api_key_usage import estimate_api_key_request_usage
68
+ from app.modules.proxy.continuity import (
69
+ resolve_required_account_id,
70
+ without_http_bridge_session_affinity_headers,
71
+ )
72
+ from app.modules.proxy.helpers import (
73
+ _header_account_id,
74
+ _normalize_error_code,
75
+ _parse_openai_error,
76
+ classify_upstream_failure,
77
+ )
78
+ from app.modules.proxy.load_balancer import (
79
+ AccountConcurrencyCaps,
80
+ AccountLease,
81
+ AccountSelection,
82
+ effective_account_concurrency_caps,
83
+ )
84
+ from app.modules.proxy.replay_safety import (
85
+ project_responses_input_for_account_neutral_fresh_replay,
86
+ responses_input_suffix_retains_prior_output,
87
+ responses_payload_is_account_neutral_fresh_replay,
88
+ )
89
+ from app.modules.proxy.selection_errors import selection_failure_response
90
+ from app.modules.proxy.work_admission import AdmissionLease, WorkAdmissionController
91
+
92
+ logger = logging.getLogger("app.modules.proxy.service")
93
+ T = TypeVar("T")
94
+
95
+ _REQUEST_TRANSPORT_HTTP = "http"
96
+ _CompactResponses = Callable[
97
+ [ResponsesCompactRequest, Mapping[str, str], str, str | None],
98
+ Awaitable[CompactResponsePayload],
99
+ ]
100
+
101
+
102
+ def _compact_turn_state_session_identity(session_key: object | None, session: object | None) -> str | None:
103
+ durable_session_id = getattr(session, "durable_session_id", None)
104
+ if isinstance(durable_session_id, str) and durable_session_id.strip():
105
+ return f"durable:{durable_session_id.strip()}"
106
+ if session_key is None:
107
+ return None
108
+ return f"live:{session_key!r}"
109
+
110
+
111
+ class _CompactServiceProtocol(Protocol):
112
+ _encryptor: Any
113
+ _load_balancer: Any
114
+ _repo_factory: Any
115
+ _http_bridge_lock: Any
116
+ _http_bridge_sessions: Any
117
+ _http_bridge_turn_state_index: Any
118
+ _durable_bridge: Any
119
+
120
+ def _get_work_admission(self) -> WorkAdmissionController: ...
121
+
122
+ def _raise_for_unsupported_input_image_references(self, payload: ResponsesCompactRequest) -> None: ...
123
+
124
+ async def _resolve_file_account_for_responses(
125
+ self, payload: ResponsesCompactRequest, headers: Mapping[str, str]
126
+ ) -> str | None: ...
127
+
128
+ async def _resolve_forwarded_file_account_for_responses(
129
+ self,
130
+ payload: ResponsesCompactRequest,
131
+ headers: Mapping[str, str],
132
+ *,
133
+ forwarded_file_owner_account_id: str | None,
134
+ require_forwarded_file_owner: bool = False,
135
+ ) -> str | None: ...
136
+
137
+ async def _acquire_account_response_create_lease_or_overload(
138
+ self, *, account_id: str, request_id: str, surface: str, concurrency_caps: AccountConcurrencyCaps
139
+ ) -> AccountLease: ...
140
+
141
+ async def _resolve_upstream_route_for_account(
142
+ self, account: Account, *, operation: str
143
+ ) -> ResolvedUpstreamRoute | None: ...
144
+
145
+ async def _select_account_with_budget_compatible(self, deadline: float, **kwargs: object) -> AccountSelection: ...
146
+
147
+ async def _resolve_websocket_previous_response_owner(
148
+ self,
149
+ *,
150
+ previous_response_id: str | None,
151
+ api_key: ApiKeyData | None,
152
+ session_id: str | None = None,
153
+ surface: str,
154
+ ) -> str | None: ...
155
+
156
+ async def _resolve_compact_turn_state_owner(
157
+ self,
158
+ *,
159
+ turn_state: str,
160
+ api_key: ApiKeyData | None,
161
+ fail_on_missing: bool = True,
162
+ ) -> str | None: ...
163
+
164
+ async def _compact_owner_selection_loss_is_quota_caused(self, account_id: str) -> bool: ...
165
+
166
+ async def _ensure_fresh_with_budget(
167
+ self, account: Account, *, force: bool = False, timeout_seconds: float | None = None
168
+ ) -> Account: ...
169
+
170
+ async def _handle_stream_error(
171
+ self,
172
+ account: Account,
173
+ error: Any,
174
+ code: str,
175
+ http_status: int | None = None,
176
+ ) -> Any: ...
177
+
178
+ async def _handle_proxy_error(self, account: Account, exc: ProxyResponseError) -> None: ...
179
+
180
+ async def _settle_compact_api_key_usage(
181
+ self,
182
+ *,
183
+ api_key: ApiKeyData | None,
184
+ api_key_reservation: ApiKeyUsageReservationData | None,
185
+ response: CompactResponsePayload | None,
186
+ request_service_tier: str | None,
187
+ ) -> None: ...
188
+
189
+ async def _write_request_log(self, **kwargs: Any) -> None: ...
190
+
191
+
192
+ def _service_module() -> Any:
193
+ service_module = sys.modules.get("app.modules.proxy.service")
194
+ if service_module is None:
195
+ raise RuntimeError("app.modules.proxy.service is not loaded")
196
+ return service_module
197
+
198
+
199
+ def _service_global(name: str) -> Any:
200
+ return getattr(_service_module(), name)
201
+
202
+
203
+ def _service_global_or(name: str, fallback: T) -> T:
204
+ service_module = sys.modules.get("app.modules.proxy.service")
205
+ if service_module is None:
206
+ return fallback
207
+ return cast(T, getattr(service_module, name, fallback))
208
+
209
+
210
+ def _service_get_settings() -> Any:
211
+ return _service_global_or("get_settings", get_settings)()
212
+
213
+
214
+ def _service_get_settings_cache() -> Any:
215
+ return _service_global_or("get_settings_cache", get_settings_cache)()
216
+
217
+
218
+ def _service_time() -> Any:
219
+ return _service_global_or("time", time)
220
+
221
+
222
+ def _service_core_compact_responses() -> _CompactResponses:
223
+ return _service_global_or("core_compact_responses", core_compact_responses)
224
+
225
+
226
+ def _service_push_compact_timeout_overrides(**kwargs: float) -> object:
227
+ service_module = sys.modules.get("app.modules.proxy.service")
228
+ if service_module is not None:
229
+ func = getattr(service_module, "push_compact_timeout_overrides", push_compact_timeout_overrides)
230
+ return cast(Callable[..., object], func)(**kwargs)
231
+ return push_compact_timeout_overrides(**kwargs)
232
+
233
+
234
+ def _service_pop_compact_timeout_overrides(token: object) -> None:
235
+ service_module = sys.modules.get("app.modules.proxy.service")
236
+ if service_module is not None:
237
+ func = getattr(service_module, "pop_compact_timeout_overrides", pop_compact_timeout_overrides)
238
+ cast(Callable[[object], None], func)(token)
239
+ return
240
+ pop_compact_timeout_overrides(cast(Any, token))
241
+
242
+
243
+ def _request_kind_from_headers(headers: Mapping[str, str]) -> str:
244
+ raw_metadata = headers.get("x-codex-turn-metadata") or headers.get("X-Codex-Turn-Metadata")
245
+ if not raw_metadata:
246
+ return "normal"
247
+ try:
248
+ metadata = json.loads(raw_metadata)
249
+ except json.JSONDecodeError:
250
+ return "normal"
251
+ if not isinstance(metadata, dict):
252
+ return "normal"
253
+ raw_request_kind = metadata.get("request_kind")
254
+ if not isinstance(raw_request_kind, str):
255
+ return "normal"
256
+ request_kind = raw_request_kind.strip()
257
+ if request_kind == "compaction":
258
+ return request_kind
259
+ return "normal"
260
+
261
+
262
+ def _remaining_budget_seconds(deadline: float) -> float:
263
+ return cast(Callable[[float], float], _service_global("_remaining_budget_seconds"))(deadline)
264
+
265
+
266
+ def _compact_upstream_call_budget_reserve_seconds(remaining_budget: float) -> float:
267
+ if remaining_budget <= 0:
268
+ return 0.0
269
+ return min(30.0, max(1.0, remaining_budget * 0.2), remaining_budget * 0.5)
270
+
271
+
272
+ def _compact_freshness_budget_seconds(remaining_budget: float) -> float:
273
+ reserve = _compact_upstream_call_budget_reserve_seconds(remaining_budget)
274
+ return min(20.0, max(0.0, remaining_budget - reserve))
275
+
276
+
277
+ def _compact_upstream_budget_seconds(remaining_budget: float) -> float:
278
+ if remaining_budget <= 0:
279
+ return 0.0
280
+ reserve = _compact_upstream_call_budget_reserve_seconds(remaining_budget)
281
+ return max(0.0, remaining_budget - reserve)
282
+
283
+
284
+ def _raise_proxy_budget_exhausted() -> NoReturn:
285
+ cast(Callable[[], NoReturn], _service_global("_raise_proxy_budget_exhausted"))()
286
+
287
+
288
+ def _raise_proxy_unavailable(message: str) -> NoReturn:
289
+ cast(Callable[[str], NoReturn], _service_global("_raise_proxy_unavailable"))(message)
290
+
291
+
292
+ def _request_log_failure_metadata(exc: ProxyResponseError) -> _RequestLogFailureMetadata:
293
+ return cast(
294
+ Callable[[ProxyResponseError], _RequestLogFailureMetadata], _service_global("_request_log_failure_metadata")
295
+ )(exc)
296
+
297
+
298
+ def _prefer_earlier_reset_window(settings: DashboardSettings) -> ResetPreferenceWindow:
299
+ return cast(Callable[[DashboardSettings], ResetPreferenceWindow], _service_global("_prefer_earlier_reset_window"))(
300
+ settings
301
+ )
302
+
303
+
304
+ def _routing_strategy(settings: DashboardSettings) -> RoutingStrategy:
305
+ return cast(Callable[[DashboardSettings], RoutingStrategy], _service_global("_routing_strategy"))(settings)
306
+
307
+
308
+ def _call_with_supported_optional_kwargs(
309
+ func: Callable[..., Awaitable[CompactResponsePayload]],
310
+ *args: object,
311
+ optional_kwargs: Mapping[str, object],
312
+ ) -> Awaitable[CompactResponsePayload]:
313
+ return cast(
314
+ Callable[..., Awaitable[CompactResponsePayload]], _service_global("_call_with_supported_optional_kwargs")
315
+ )(func, *args, optional_kwargs=optional_kwargs)
316
+
317
+
318
+ def _maybe_log_proxy_request_payload(
319
+ kind: str,
320
+ payload: ResponsesCompactRequest,
321
+ headers: Mapping[str, str],
322
+ ) -> None:
323
+ cast(Callable[..., None], _service_global("_maybe_log_proxy_request_payload"))(kind, payload, headers)
324
+
325
+
326
+ def _maybe_log_proxy_request_shape(
327
+ kind: str,
328
+ payload: ResponsesCompactRequest,
329
+ headers: Mapping[str, str],
330
+ **kwargs: object,
331
+ ) -> None:
332
+ cast(Callable[..., None], _service_global("_maybe_log_proxy_request_shape"))(kind, payload, headers, **kwargs)
333
+
334
+
335
+ def _maybe_log_proxy_service_tier_trace(
336
+ kind: str,
337
+ *,
338
+ requested_service_tier: str | None,
339
+ actual_service_tier: str | None,
340
+ ) -> None:
341
+ cast(Callable[..., None], _service_global("_maybe_log_proxy_service_tier_trace"))(
342
+ kind,
343
+ requested_service_tier=requested_service_tier,
344
+ actual_service_tier=actual_service_tier,
345
+ )
346
+
347
+
348
+ def _should_retry_transient_stream_error(code: str | None, message: str | None) -> bool:
349
+ return cast(Callable[[str | None, str | None], bool], _service_global("_should_retry_transient_stream_error"))(
350
+ code, message
351
+ )
352
+
353
+
354
+ def _compact_previous_response_not_found_error(exc: ProxyResponseError) -> ProxyResponseError | None:
355
+ return cast(
356
+ Callable[[ProxyResponseError], ProxyResponseError | None],
357
+ _service_global("_compact_previous_response_not_found_error"),
358
+ )(exc)
359
+
360
+
361
+ def _proxy_response_error_code(exc: ProxyResponseError) -> str | None:
362
+ return cast(Callable[[ProxyResponseError], str | None], _service_global("_proxy_response_error_code"))(exc)
363
+
364
+
365
+ def _record_continuity_fail_closed(
366
+ *,
367
+ surface: str,
368
+ reason: str,
369
+ previous_response_id: str | None,
370
+ session_id: str | None,
371
+ upstream_error_code: str | None,
372
+ ) -> None:
373
+ cast(Callable[..., None], _service_global("_record_continuity_fail_closed"))(
374
+ surface=surface,
375
+ reason=reason,
376
+ previous_response_id=previous_response_id,
377
+ session_id=session_id,
378
+ upstream_error_code=upstream_error_code,
379
+ )
380
+
381
+
382
+ def _is_security_work_authorization_required_error(code: str | None, message: str | None) -> bool:
383
+ return cast(
384
+ Callable[[str | None, str | None], bool],
385
+ _service_global("_is_security_work_authorization_required_error"),
386
+ )(code, message)
387
+
388
+
389
+ def _is_account_neutral_error_code(code: str | None) -> bool:
390
+ return cast(Callable[[str | None], bool], _service_global("_is_account_neutral_error_code"))(code)
391
+
392
+
393
+ def _upstream_error_from_openai(error: Any) -> Any:
394
+ return cast(Callable[[Any], Any], _service_global("_upstream_error_from_openai"))(error)
395
+
396
+
397
+ def _estimated_lease_tokens_from_request_usage_budget(budget: ApiKeyRequestUsageBudget | None) -> float:
398
+ return cast(
399
+ Callable[[ApiKeyRequestUsageBudget | None], float],
400
+ _service_global("_estimated_lease_tokens_from_request_usage_budget"),
401
+ )(budget)
402
+
403
+
404
+ def _service_tier_from_response(response: CompactResponsePayload | None) -> str | None:
405
+ return cast(Callable[[CompactResponsePayload | None], str | None], _service_global("_service_tier_from_response"))(
406
+ response
407
+ )
408
+
409
+
410
+ def _effective_service_tier(requested_service_tier: str | None, actual_service_tier: str | None) -> str | None:
411
+ return cast(
412
+ Callable[[str | None, str | None], str | None],
413
+ _service_global("_effective_service_tier"),
414
+ )(requested_service_tier, actual_service_tier)
415
+
416
+
417
+ def _compact_same_contract_retry_budget() -> int:
418
+ return cast(int, _service_global("_COMPACT_SAME_CONTRACT_RETRY_BUDGET"))
419
+
420
+
421
+ def _compact_max_account_attempts() -> int:
422
+ return cast(int, _service_global("_COMPACT_MAX_ACCOUNT_ATTEMPTS"))
423
+
424
+
425
+ def _max_transient_same_account_retries() -> int:
426
+ return cast(int, _service_global("_MAX_TRANSIENT_SAME_ACCOUNT_RETRIES"))
427
+
428
+
429
+ def _no_security_work_authorized_accounts_code() -> str:
430
+ return cast(str, _service_global("_NO_SECURITY_WORK_AUTHORIZED_ACCOUNTS_CODE"))
431
+
432
+
433
+ def _sticky_key_from_compact_payload(payload: ResponsesCompactRequest) -> str | None:
434
+ value = _prompt_cache_key_from_request_model(payload)
435
+ if not isinstance(value, str):
436
+ return None
437
+ stripped = value.strip()
438
+ return stripped or None
439
+
440
+
441
+ def _sticky_key_for_compact_request(
442
+ payload: ResponsesCompactRequest,
443
+ headers: Mapping[str, str],
444
+ *,
445
+ codex_session_affinity: bool,
446
+ openai_cache_affinity: bool,
447
+ openai_cache_affinity_max_age_seconds: int,
448
+ sticky_threads_enabled: bool,
449
+ api_key: ApiKeyData | None = None,
450
+ ) -> _AffinityPolicy:
451
+ # A compact body is already trimmed, so it rarely extends the ordinary
452
+ # turns' transcript and will usually mint a new anchor. That is the same
453
+ # answer the ordinary path gives for a compacted turn, and it is correct:
454
+ # the upstream prefix cache is cold after compaction.
455
+ resolution = _resolve_prompt_cache_key(
456
+ payload,
457
+ openai_cache_affinity=openai_cache_affinity,
458
+ api_key=api_key,
459
+ max_age_seconds=openai_cache_affinity_max_age_seconds,
460
+ )
461
+ cache_key = resolution.sticky_key
462
+ cache_key_source = resolution.source
463
+ turn_state_key = _sticky_key_from_turn_state_header(headers)
464
+ if turn_state_key:
465
+ policy = _AffinityPolicy(
466
+ key=turn_state_key,
467
+ kind=StickySessionKind.CODEX_SESSION,
468
+ codex_session_source="turn_state",
469
+ )
470
+ elif (
471
+ thread_affinity := _thread_codex_session_affinity(
472
+ headers,
473
+ enabled=codex_session_affinity,
474
+ max_age_seconds=openai_cache_affinity_max_age_seconds,
475
+ )
476
+ ) is not None:
477
+ policy = thread_affinity
478
+ elif (
479
+ session_affinity := _bare_codex_session_affinity(
480
+ headers,
481
+ enabled=codex_session_affinity,
482
+ allow_cap_spillover=_request_allows_bare_session_cap_spillover(payload),
483
+ )
484
+ ) is not None:
485
+ policy = session_affinity
486
+ elif openai_cache_affinity:
487
+ policy = _AffinityPolicy(
488
+ key=cache_key,
489
+ kind=StickySessionKind.PROMPT_CACHE,
490
+ max_age_seconds=openai_cache_affinity_max_age_seconds,
491
+ prompt_cache_key_source=cache_key_source,
492
+ )
493
+ elif sticky_threads_enabled:
494
+ policy = _AffinityPolicy(
495
+ key=cache_key,
496
+ kind=StickySessionKind.STICKY_THREAD,
497
+ reallocate_sticky=True,
498
+ prompt_cache_key_source=cache_key_source,
499
+ )
500
+ else:
501
+ policy = _AffinityPolicy()
502
+ policy = replace(policy, prompt_cache_derivation_outcome=resolution.outcome)
503
+ return _affinity_with_payload_continuity(policy, payload)
504
+
505
+
506
+ def _service_tier_from_compact_payload(payload: ResponsesCompactRequest) -> str | None:
507
+ normalize = cast(Callable[[JsonValue], str | None], _service_global("_normalize_service_tier_value"))
508
+ return normalize(payload.service_tier)
509
+
510
+
511
+ # Account statuses that prove the pinned owner's selection-time loss is caused
512
+ # by upstream quota/rate-limit state rather than authentication, deactivation,
513
+ # or an operator pause. Only these authorize account-neutral replay recovery.
514
+ _COMPACT_OWNER_QUOTA_UNAVAILABLE_STATUSES = (
515
+ AccountStatus.RATE_LIMITED,
516
+ AccountStatus.QUOTA_EXCEEDED,
517
+ )
518
+
519
+
520
+ def _compact_replay_history_retains_prior_output(input_items: list[JsonValue]) -> bool:
521
+ """Prove the carried history retains prior assistant output before new input.
522
+
523
+ A self-contained account-neutral ``input`` is not by itself a full resend:
524
+ a client could send only the turns after ``previous_response_id`` (for
525
+ example two fresh user messages) and rely on the owner account to hold the
526
+ earlier conversation, so replaying without the anchor would compact a
527
+ truncated history. Without durable prefix metadata for the compact surface,
528
+ the strongest client-side evidence of a full resend is the same
529
+ retained-prior-output shape the HTTP bridge replay path trusts: the input
530
+ must parse as a clean transcript whose final segment is the previous
531
+ response's completed assistant output followed only by fresh client input.
532
+ The split is anchored at the last assistant message so the shared suffix
533
+ walk proves exactly that segment; anything it cannot prove stays
534
+ owner-bound.
535
+
536
+ This is the evidence ceiling of the #1490 rescope: completeness relative to
537
+ the anchored conversation is not provable from the payload alone, and the
538
+ durable prefix metadata that could prove it is deliberately not consulted
539
+ here. A delta resend that itself carries a completed assistant exchange
540
+ ahead of the fresh input is indistinguishable from a full resend and is
541
+ recovered as the client's authoritative local history — the same trust the
542
+ shared account-neutral fresh-replay rules already grant a normal turn that
543
+ abandons an unavailable owner. The rejected shapes below are the ones the
544
+ transcript walk can actually refute.
545
+ """
546
+
547
+ last_assistant_index: int | None = None
548
+ for index in range(len(input_items) - 1, -1, -1):
549
+ item = input_items[index]
550
+ if isinstance(item, dict) and item.get("type") in (None, "message") and item.get("role") == "assistant":
551
+ last_assistant_index = index
552
+ break
553
+ # ``responses_input_suffix_retains_prior_output`` requires a non-empty
554
+ # stored prefix, so a history that opens with (or lacks) assistant output
555
+ # cannot be proven and stays owner-bound.
556
+ if last_assistant_index is None or last_assistant_index == 0:
557
+ return False
558
+ # The projection is an identity transform for input that already passed
559
+ # the account-neutral fresh-replay gate (no server-assigned ids, no
560
+ # reasoning or omitted bookkeeping types survive that gate), but it is the
561
+ # shared authority for recognizing the canonical Responses-Lite developer
562
+ # instruction behind an ``additional_tools`` bundle — without that index
563
+ # the suffix walk would reject every Lite full resend.
564
+ projection = project_responses_input_for_account_neutral_fresh_replay(
565
+ input_items,
566
+ stored_count=last_assistant_index,
567
+ )
568
+ if projection is None:
569
+ return False
570
+ return responses_input_suffix_retains_prior_output(
571
+ projection.input_items,
572
+ stored_count=projection.stored_prefix_count,
573
+ canonical_lite_developer_index=projection.canonical_lite_developer_index,
574
+ )
575
+
576
+
577
+ def _compact_account_neutral_replay_payload(
578
+ payload: ResponsesCompactRequest,
579
+ ) -> ResponsesCompactRequest | None:
580
+ """Return the anchor-free replay payload for a verified full resend.
581
+
582
+ A compact request pinned only by ``previous_response_id`` may move off an
583
+ unselectable owner account when the history it carries is provably
584
+ account-neutral: the upstream-bound payload without the anchor must pass
585
+ the shared fresh-replay validation, so no encrypted or compaction state,
586
+ server-assigned item ids, account-scoped file/container handles,
587
+ conversation/prompt handles, or hosted/MCP state can reach the replacement
588
+ account.
589
+
590
+ Neutrality is checked on the serialized upstream-bound payload
591
+ (``to_payload``), never on the request model, matching how the HTTP bridge
592
+ replay paths apply the shared gate to pre-transport serializations. The
593
+ compact transport applies two further mutations after this serialization —
594
+ the Responses-Lite ``reasoning.context`` control and inline image
595
+ fetching — both proxy-injected, account-agnostic, and applied identically
596
+ to the owner send and the replay send, so they are not part of the client
597
+ payload being proven. The serialized history must additionally be a
598
+ complete resend. ``to_payload`` can still drop history on the wire: it
599
+ strips poisoned local-compact fallback messages together with their
600
+ trailing encrypted compaction item, and it trims oversized inputs down to a
601
+ head, a trim marker, and a tail. Both remain multi-item account-neutral
602
+ lists. Sending either to a replacement account without the anchor would
603
+ compact an incomplete conversation, because only the owner can resolve the
604
+ omitted context from the dropped anchor. So the wire input must be
605
+ item-for-item identical to the validated request input, must still carry
606
+ more than one item, and must retain prior assistant output ahead of the new
607
+ client input (see ``_compact_replay_history_retains_prior_output``).
608
+ """
609
+
610
+ previous_response_id = getattr(payload, "previous_response_id", None)
611
+ if not isinstance(previous_response_id, str) or not previous_response_id.strip():
612
+ return None
613
+ if not isinstance(payload.input, list):
614
+ return None
615
+ replay_source = payload.model_dump(mode="json", exclude_none=True)
616
+ replay_source.pop("previous_response_id", None)
617
+ request_input = replay_source.get("input")
618
+ if not isinstance(request_input, list) or len(request_input) <= 1:
619
+ return None
620
+ try:
621
+ replay_payload = ResponsesCompactRequest.model_validate(replay_source)
622
+ replay_wire_payload = replay_payload.to_payload()
623
+ except (ValidationError, ClientPayloadError):
624
+ return None
625
+ replay_wire_input = replay_wire_payload.get("input")
626
+ if not isinstance(replay_wire_input, list) or len(replay_wire_input) <= 1:
627
+ return None
628
+ if replay_wire_input != request_input:
629
+ return None
630
+ if not responses_payload_is_account_neutral_fresh_replay(replay_wire_payload):
631
+ return None
632
+ if not _compact_replay_history_retains_prior_output(cast(list[JsonValue], replay_wire_input)):
633
+ return None
634
+ return replay_payload
635
+
636
+
637
+ class _CompactMixin:
638
+ async def _compact_owner_selection_loss_is_quota_caused(self, account_id: str) -> bool:
639
+ """Return whether the pinned owner is unselectable because of quota state.
640
+
641
+ Account-neutral replay off a pinned previous-response owner is legal
642
+ only for owner loss the owner's quota state caused. At selection time
643
+ that evidence is the owner's own persisted status: ``RATE_LIMITED`` or
644
+ ``QUOTA_EXCEEDED`` is the same upstream usage-exhaustion state the
645
+ selector consulted. Hard authentication loss (``DEACTIVATED``),
646
+ operator pauses, local capacity caps on a request-routable account, and
647
+ a failed lookup all stay owner-bound. ``REAUTH_REQUIRED`` remains
648
+ selectable and therefore does not itself cause owner loss.
649
+ """
650
+
651
+ proxy = cast(_CompactServiceProtocol, self)
652
+ try:
653
+ async with proxy._repo_factory() as repos:
654
+ account = await repos.accounts.get_by_id_fresh(account_id)
655
+ # Read inside the repository scope: the session expires ORM
656
+ # attributes when it closes.
657
+ status = account.status if account is not None else None
658
+ except Exception:
659
+ logger.warning(
660
+ "Compact previous-response owner status lookup failed; keeping the request owner-bound",
661
+ exc_info=True,
662
+ )
663
+ return False
664
+ return status in _COMPACT_OWNER_QUOTA_UNAVAILABLE_STATUSES
665
+
666
+ async def _resolve_compact_turn_state_owner(
667
+ self,
668
+ *,
669
+ turn_state: str,
670
+ api_key: ApiKeyData | None,
671
+ fail_on_missing: bool = True,
672
+ ) -> str | None:
673
+ """Resolve a turn-state token to its API-key-scoped HTTP bridge owner.
674
+
675
+ A compact request cannot safely fall back to generic sticky routing: an
676
+ opaque turn-state is valid only on the account that created it. The
677
+ local alias index is the fast path; the durable lookup covers a request
678
+ that arrives on another replica. Both lookup surfaces are keyed by the
679
+ exact API key id, so a token observed under one key cannot select an
680
+ account for another key.
681
+
682
+ Synthetic-shaped ``turn_*`` / ``http_turn_*`` values can be real
683
+ registered bridge aliases on later turns. Callers may pass
684
+ ``fail_on_missing=False`` only for those synthetic placeholders so an
685
+ unregistered first-turn placeholder still allows weaker routing signals
686
+ such as file ownership to run.
687
+ """
688
+ proxy = cast(_CompactServiceProtocol, self)
689
+ normalized_turn_state = turn_state.strip()
690
+ if not normalized_turn_state:
691
+ raise ProxyResponseError(
692
+ 502,
693
+ openai_error(
694
+ "turn_state_owner_unavailable",
695
+ "Turn-state owner account is unavailable; retry the logical turn.",
696
+ error_type="server_error",
697
+ ),
698
+ )
699
+ api_key_id = api_key.id if api_key is not None else None
700
+ owner_refs: list[tuple[str, str, str | None]] = []
701
+ async with proxy._http_bridge_lock:
702
+ session_key = proxy._http_bridge_turn_state_index.get((normalized_turn_state, api_key_id))
703
+ session = proxy._http_bridge_sessions.get(session_key) if session_key is not None else None
704
+ account = getattr(session, "account", None)
705
+ account_id = getattr(account, "id", None)
706
+ if isinstance(account_id, str) and account_id.strip():
707
+ owner_refs.append(
708
+ (
709
+ "turn-state live index",
710
+ account_id,
711
+ _compact_turn_state_session_identity(session_key, session),
712
+ )
713
+ )
714
+
715
+ try:
716
+ durable_lookup = await proxy._durable_bridge.lookup_turn_state_target(
717
+ turn_state=normalized_turn_state,
718
+ api_key_id=api_key_id,
719
+ )
720
+ except Exception as exc:
721
+ raise ProxyResponseError(
722
+ 502,
723
+ openai_error(
724
+ "turn_state_owner_unavailable",
725
+ "Turn-state owner account is unavailable; retry the logical turn.",
726
+ error_type="server_error",
727
+ ),
728
+ ) from exc
729
+ account_id = getattr(durable_lookup, "account_id", None)
730
+ if isinstance(account_id, str) and account_id.strip():
731
+ durable_session_id = getattr(durable_lookup, "session_id", None)
732
+ owner_refs.append(
733
+ (
734
+ "turn-state durable alias",
735
+ account_id,
736
+ f"durable:{durable_session_id.strip()}"
737
+ if isinstance(durable_session_id, str) and durable_session_id.strip()
738
+ else None,
739
+ )
740
+ )
741
+ resolved_owner = resolve_required_account_id(
742
+ *((source, owner_account_id) for source, owner_account_id, _session_id in owner_refs)
743
+ )
744
+ if resolved_owner is not None:
745
+ session_identities = {
746
+ session_identity
747
+ for _source, owner_account_id, session_identity in owner_refs
748
+ if owner_account_id == resolved_owner and session_identity is not None
749
+ }
750
+ if len(session_identities) > 1:
751
+ raise ProxyResponseError(
752
+ 502,
753
+ openai_error(
754
+ "continuity_owner_conflict",
755
+ "Turn-state owner sessions conflict; retry the logical turn.",
756
+ error_type="server_error",
757
+ ),
758
+ )
759
+ return resolved_owner
760
+ if not fail_on_missing:
761
+ return None
762
+ raise ProxyResponseError(
763
+ 502,
764
+ openai_error(
765
+ "turn_state_owner_unavailable",
766
+ "Turn-state owner account is unavailable; retry the logical turn.",
767
+ error_type="server_error",
768
+ ),
769
+ )
770
+
771
+ async def compact_responses(
772
+ self,
773
+ payload: ResponsesCompactRequest,
774
+ headers: Mapping[str, str],
775
+ *,
776
+ codex_session_affinity: bool = False,
777
+ openai_cache_affinity: bool = False,
778
+ api_key: ApiKeyData | None = None,
779
+ api_key_reservation: ApiKeyUsageReservationData | None = None,
780
+ client_ip: str | None = None,
781
+ forwarded_request: bool = False,
782
+ forwarded_file_owner_account_id: str | None = None,
783
+ ) -> CompactResponsePayload:
784
+ proxy = cast(_CompactServiceProtocol, self)
785
+ _maybe_log_proxy_request_payload("compact", payload, headers)
786
+ filtered = filter_inbound_headers(headers)
787
+ useragent, useragent_group, conversation_id = _request_log_client_fields(headers)
788
+ request_kind = _request_kind_from_headers(headers)
789
+ request_id = get_request_id() or ensure_request_id(None)
790
+ start = _service_time().monotonic()
791
+ base_settings = _service_get_settings()
792
+ deadline = start + base_settings.compact_request_budget_seconds
793
+ account_id_value: str | None = None
794
+ log_status = "error"
795
+ log_error_code: str | None = None
796
+ log_error_message: str | None = None
797
+ failure_metadata = _RequestLogFailureMetadata()
798
+ response: CompactResponsePayload | None = None
799
+ request_service_tier: str | None = None
800
+ actual_service_tier: str | None = None
801
+ route_mode: str | None = None
802
+ route_pool_id: str | None = None
803
+ route_endpoint_id: str | None = None
804
+ route_fallback_used: bool | None = None
805
+ route_fail_closed_reason: str | None = None
806
+ settlement_attempted = False
807
+
808
+ async def settle_compact_usage(
809
+ *,
810
+ api_key: ApiKeyData | None,
811
+ api_key_reservation: ApiKeyUsageReservationData | None,
812
+ response: CompactResponsePayload | None,
813
+ request_service_tier: str | None,
814
+ ) -> None:
815
+ nonlocal settlement_attempted
816
+ if settlement_attempted:
817
+ return
818
+ if forwarded_request and response is None:
819
+ # A forwarded receiver has not transferred cleanup ownership
820
+ # until its successful HTTP 200. Every error before that
821
+ # acknowledgement remains the origin's single release path.
822
+ return
823
+ settlement_attempted = True
824
+ await proxy._settle_compact_api_key_usage(
825
+ api_key=api_key,
826
+ api_key_reservation=api_key_reservation,
827
+ response=response,
828
+ request_service_tier=request_service_tier,
829
+ )
830
+
831
+ proxy._raise_for_unsupported_input_image_references(payload)
832
+ try:
833
+ rewritten_file_account_id = await proxy._resolve_forwarded_file_account_for_responses(
834
+ payload,
835
+ headers,
836
+ forwarded_file_owner_account_id=forwarded_file_owner_account_id,
837
+ require_forwarded_file_owner=forwarded_request,
838
+ )
839
+ except ProxyResponseError:
840
+ if not forwarded_request and api_key is not None and api_key_reservation is not None:
841
+ try:
842
+ await settle_compact_usage(
843
+ api_key=api_key,
844
+ api_key_reservation=api_key_reservation,
845
+ response=None,
846
+ request_service_tier=_service_tier_from_compact_payload(payload),
847
+ )
848
+ except Exception:
849
+ logger.warning(
850
+ "Failed to settle compact API key reservation after owner lookup failure",
851
+ exc_info=True,
852
+ )
853
+ raise
854
+ except asyncio.CancelledError:
855
+ if not forwarded_request and api_key is not None and api_key_reservation is not None:
856
+ try:
857
+ await settle_compact_usage(
858
+ api_key=api_key,
859
+ api_key_reservation=api_key_reservation,
860
+ response=None,
861
+ request_service_tier=_service_tier_from_compact_payload(payload),
862
+ )
863
+ except Exception:
864
+ logger.warning(
865
+ "Failed to settle compact API key reservation after cancelled owner lookup",
866
+ exc_info=True,
867
+ )
868
+ raise
869
+ settings = await _service_get_settings_cache().get()
870
+ # C2-3 resilience toggles: this request's snapshot, bound for the client.
871
+ resilience = bind_resilience_toggles(settings, startup_settings=base_settings)
872
+ concurrency_caps = effective_account_concurrency_caps(settings)
873
+ prefer_earlier_reset = settings.prefer_earlier_reset_accounts
874
+ # Resolved once per request: API-key override, then the fleet value read
875
+ # off the *overlaid* settings (dashboard over environment over default),
876
+ # never the raw dashboard row whose column is NULL until an operator sets
877
+ # it. See the matching comment in ``streaming/retry.py``.
878
+ thread_cache_identity_mode, thread_cache_identity_from_key = _effective_thread_cache_identity_mode(
879
+ api_key, with_dashboard_overrides(base_settings)
880
+ )
881
+ affinity = _sticky_key_for_compact_request(
882
+ payload,
883
+ headers,
884
+ codex_session_affinity=codex_session_affinity,
885
+ openai_cache_affinity=openai_cache_affinity,
886
+ openai_cache_affinity_max_age_seconds=settings.openai_cache_affinity_max_age_seconds,
887
+ sticky_threads_enabled=settings.sticky_threads_enabled,
888
+ api_key=api_key,
889
+ )
890
+ affinity_observation = AffinityObservation.from_policy(affinity)
891
+ _maybe_log_proxy_request_shape(
892
+ "compact",
893
+ payload,
894
+ headers,
895
+ sticky_kind=affinity_observation.kind,
896
+ sticky_key_source=affinity_observation.source,
897
+ derivation_outcome=affinity.prompt_cache_derivation_outcome,
898
+ prompt_cache_key_set=_prompt_cache_key_from_request_model(payload) is not None,
899
+ thread_cache_identity_mode=thread_cache_identity_mode,
900
+ thread_cache_identity_from_key=thread_cache_identity_from_key,
901
+ )
902
+ routing_strategy = _routing_strategy(settings)
903
+ turn_state_owner_account_id: str | None = None
904
+ turn_state = _sticky_key_from_turn_state_header(headers)
905
+ if turn_state is not None:
906
+ turn_state_owner_account_id = await proxy._resolve_compact_turn_state_owner(
907
+ turn_state=turn_state,
908
+ api_key=api_key,
909
+ fail_on_missing=not _is_synthesized_turn_state(turn_state),
910
+ )
911
+ previous_response_id = getattr(payload, "previous_response_id", None)
912
+ previous_response_preferred_account_id: str | None = None
913
+ previous_response_lookup_session_id: str | None = None
914
+ if isinstance(previous_response_id, str) and previous_response_id.strip():
915
+ previous_response_id = previous_response_id.strip()
916
+ previous_response_lookup_session_id = _owner_lookup_session_id_from_headers(headers)
917
+ previous_response_preferred_account_id = await proxy._resolve_websocket_previous_response_owner(
918
+ previous_response_id=previous_response_id,
919
+ api_key=api_key,
920
+ session_id=previous_response_lookup_session_id,
921
+ surface="compact",
922
+ )
923
+ if previous_response_preferred_account_id is None:
924
+ selection_inputs = await proxy._load_balancer._load_selection_inputs(
925
+ model=payload.model,
926
+ additional_limit_name=None,
927
+ account_ids=api_key.assigned_account_ids
928
+ if api_key is not None and api_key.account_assignment_scope_enabled
929
+ else None,
930
+ )
931
+ if len(selection_inputs.accounts) != 1:
932
+ message = "Previous response owner account is unavailable; retry later."
933
+ _record_continuity_fail_closed(
934
+ surface="compact",
935
+ reason="owner_account_unavailable",
936
+ previous_response_id=previous_response_id,
937
+ session_id=previous_response_lookup_session_id,
938
+ upstream_error_code="owner_lookup_miss",
939
+ )
940
+ raise ProxyResponseError(
941
+ 502,
942
+ openai_error(
943
+ "previous_response_owner_unavailable",
944
+ message,
945
+ error_type="server_error",
946
+ ),
947
+ )
948
+
949
+ # File pins are account ownership, not locality. Resolved turn-state or
950
+ # previous-response owners above still take precedence (and conflicts
951
+ # fail closed), while process-session/prompt-cache hints never hide a
952
+ # known file owner.
953
+ preferred_account_id = resolve_required_account_id(
954
+ ("turn state", turn_state_owner_account_id),
955
+ ("previous response", previous_response_preferred_account_id),
956
+ ("input file", rewritten_file_account_id),
957
+ )
958
+ deferred_stream_health: list[tuple[Account, Any, str, int | None]] = []
959
+ deferred_http_500_health: list[tuple[Account, ProxyResponseError, int]] = []
960
+ deferred_proxy_health: list[tuple[Account, ProxyResponseError]] = []
961
+ deferred_permanent_health: list[tuple[Account, str]] = []
962
+ settlement_attempted = False
963
+
964
+ async def flush_deferred_health() -> None:
965
+ stream_pending = list(deferred_stream_health)
966
+ deferred_stream_health.clear()
967
+ http_500_pending = list(deferred_http_500_health)
968
+ deferred_http_500_health.clear()
969
+ proxy_pending = list(deferred_proxy_health)
970
+ deferred_proxy_health.clear()
971
+ permanent_pending = list(deferred_permanent_health)
972
+ deferred_permanent_health.clear()
973
+ for failed_account, failed_error, failed_code, failed_status in stream_pending:
974
+ try:
975
+ await proxy._handle_stream_error(
976
+ failed_account,
977
+ failed_error,
978
+ failed_code,
979
+ http_status=failed_status,
980
+ )
981
+ except Exception:
982
+ logger.warning(
983
+ "Failed to flush deferred compact stream health account_id=%s request_id=%s",
984
+ failed_account.id,
985
+ request_id,
986
+ exc_info=True,
987
+ )
988
+ for failed_account, failed_exc, extra_error_count in http_500_pending:
989
+ try:
990
+ await proxy._handle_proxy_error(failed_account, failed_exc)
991
+ await proxy._load_balancer.record_errors(failed_account, extra_error_count)
992
+ except Exception:
993
+ logger.warning(
994
+ "Failed to flush deferred compact HTTP 500 health account_id=%s request_id=%s",
995
+ failed_account.id,
996
+ request_id,
997
+ exc_info=True,
998
+ )
999
+ for failed_account, failed_exc in proxy_pending:
1000
+ try:
1001
+ await proxy._handle_proxy_error(failed_account, failed_exc)
1002
+ except Exception:
1003
+ logger.warning(
1004
+ "Failed to flush deferred compact proxy health account_id=%s request_id=%s",
1005
+ failed_account.id,
1006
+ request_id,
1007
+ exc_info=True,
1008
+ )
1009
+ for failed_account, failed_code in permanent_pending:
1010
+ try:
1011
+ await proxy._load_balancer.mark_permanent_failure(failed_account, failed_code)
1012
+ except Exception:
1013
+ logger.warning(
1014
+ "Failed to flush deferred compact permanent health account_id=%s request_id=%s",
1015
+ failed_account.id,
1016
+ request_id,
1017
+ exc_info=True,
1018
+ )
1019
+
1020
+ async def settle_compact_usage(
1021
+ *,
1022
+ api_key: ApiKeyData | None,
1023
+ api_key_reservation: ApiKeyUsageReservationData | None,
1024
+ response: CompactResponsePayload | None,
1025
+ request_service_tier: str | None,
1026
+ ) -> None:
1027
+ nonlocal settlement_attempted
1028
+ settlement_attempted = True
1029
+ settlement_error: ProxyResponseError | None = None
1030
+ try:
1031
+ await proxy._settle_compact_api_key_usage(
1032
+ api_key=api_key,
1033
+ api_key_reservation=api_key_reservation,
1034
+ response=response,
1035
+ request_service_tier=request_service_tier,
1036
+ )
1037
+ except ProxyResponseError as exc:
1038
+ if exc.failure_phase != "usage_settlement" or not exc.reservation_released:
1039
+ raise
1040
+ settlement_error = exc
1041
+ flush_task = asyncio.create_task(
1042
+ flush_deferred_health(),
1043
+ name=f"compact-deferred-health-{request_id}",
1044
+ )
1045
+ cancellation_pending = False
1046
+ # The anyio shield keeps a level-cancelled Starlette scope from
1047
+ # re-raising into every ``await`` (busy-spin), and
1048
+ # ``wait_on_shared_future`` keeps waits off the task's
1049
+ # done-callback list (3.14 shield leaks one per cancelled wait).
1050
+ with anyio.CancelScope(shield=True):
1051
+ while not flush_task.done():
1052
+ try:
1053
+ await wait_on_shared_future(flush_task)
1054
+ except asyncio.CancelledError:
1055
+ cancellation_pending = True
1056
+ except Exception:
1057
+ break
1058
+ if not cancellation_pending:
1059
+ # The shield also blocks the level cancellation this block
1060
+ # promises to re-raise after the flush. Probe without
1061
+ # suspending so a disconnected compact request still cancels.
1062
+ try:
1063
+ await checkpoint_if_cancelled()
1064
+ except asyncio.CancelledError:
1065
+ cancellation_pending = True
1066
+ try:
1067
+ flush_task.result()
1068
+ except Exception:
1069
+ logger.warning(
1070
+ "Failed to flush deferred compact account health request_id=%s",
1071
+ request_id,
1072
+ exc_info=True,
1073
+ )
1074
+ if cancellation_pending:
1075
+ raise asyncio.CancelledError()
1076
+ if settlement_error is not None:
1077
+ raise settlement_error
1078
+
1079
+ async def settle_on_terminal_exit() -> None:
1080
+ if settlement_attempted:
1081
+ return
1082
+ try:
1083
+ await settle_compact_usage(
1084
+ api_key=api_key,
1085
+ api_key_reservation=api_key_reservation,
1086
+ response=None,
1087
+ request_service_tier=request_service_tier,
1088
+ )
1089
+ except Exception:
1090
+ logger.warning(
1091
+ "Failed to settle compact reservation after unexpected exit request_id=%s",
1092
+ request_id,
1093
+ exc_info=True,
1094
+ )
1095
+
1096
+ async def record_or_defer_proxy_health(
1097
+ failed_account: Account,
1098
+ failed_exc: ProxyResponseError,
1099
+ ) -> None:
1100
+ if api_key is not None and api_key_reservation is not None:
1101
+ deferred_proxy_health.append((failed_account, failed_exc))
1102
+ return
1103
+ await proxy._handle_proxy_error(failed_account, failed_exc)
1104
+
1105
+ async def record_or_defer_permanent_health(
1106
+ failed_account: Account,
1107
+ failed_code: str,
1108
+ ) -> None:
1109
+ if api_key is not None and api_key_reservation is not None:
1110
+ deferred_permanent_health.append((failed_account, failed_code))
1111
+ return
1112
+ await proxy._load_balancer.mark_permanent_failure(failed_account, failed_code)
1113
+
1114
+ async def record_or_defer_stream_health(
1115
+ failed_account: Account,
1116
+ failed_error: Any,
1117
+ failed_code: str,
1118
+ failed_status: int | None = None,
1119
+ ) -> None:
1120
+ if api_key is not None and api_key_reservation is not None:
1121
+ deferred_stream_health.append((failed_account, failed_error, failed_code, failed_status))
1122
+ return
1123
+ await proxy._handle_stream_error(
1124
+ failed_account,
1125
+ failed_error,
1126
+ failed_code,
1127
+ http_status=failed_status,
1128
+ )
1129
+
1130
+ try:
1131
+
1132
+ async def _call_compact(
1133
+ target: Account,
1134
+ account_response_create_lease: AccountLease | None = None,
1135
+ ) -> CompactResponsePayload:
1136
+ nonlocal route_fallback_used, route_mode, route_pool_id, route_endpoint_id
1137
+ access_token = proxy._encryptor.decrypt(target.access_token_encrypted)
1138
+ account_id = _header_account_id(target.chatgpt_account_id)
1139
+ remaining_budget = _remaining_budget_seconds(deadline)
1140
+ if remaining_budget <= 0:
1141
+ logger.warning(
1142
+ "Compact request budget exhausted before upstream call request_id=%s account_id=%s",
1143
+ request_id,
1144
+ target.id,
1145
+ )
1146
+ _raise_proxy_budget_exhausted()
1147
+ create_lease: AdmissionLease | None = None
1148
+ timeout_tokens: Any | None = None
1149
+ try:
1150
+ if account_response_create_lease is None:
1151
+ account_response_create_lease = await proxy._acquire_account_response_create_lease_or_overload(
1152
+ account_id=target.id,
1153
+ request_id=request_id,
1154
+ surface="compact",
1155
+ concurrency_caps=concurrency_caps,
1156
+ )
1157
+ create_lease = await proxy._get_work_admission().acquire_response_create(compact=True)
1158
+ route = await proxy._resolve_upstream_route_for_account(target, operation="compact")
1159
+ remaining_budget = _remaining_budget_seconds(deadline)
1160
+ if remaining_budget <= 0:
1161
+ logger.warning(
1162
+ "Compact request budget exhausted after admission waits request_id=%s account_id=%s",
1163
+ request_id,
1164
+ target.id,
1165
+ )
1166
+ _raise_proxy_budget_exhausted()
1167
+ upstream_budget = _compact_upstream_budget_seconds(remaining_budget)
1168
+ if upstream_budget <= 0:
1169
+ logger.warning(
1170
+ "Compact request budget exhausted before upstream call cap request_id=%s account_id=%s",
1171
+ request_id,
1172
+ target.id,
1173
+ )
1174
+ _raise_proxy_budget_exhausted()
1175
+ timeout_tokens = _service_push_compact_timeout_overrides(
1176
+ connect_timeout_seconds=upstream_budget,
1177
+ total_timeout_seconds=upstream_budget,
1178
+ )
1179
+ if route is not None:
1180
+ route_mode = route.mode
1181
+ route_pool_id = route.pool_id
1182
+ route_endpoint_id = route.endpoint_id
1183
+ route_trace = UpstreamProxyRouteTrace()
1184
+ upstream_started_at = time.monotonic()
1185
+ try:
1186
+ logger.info(
1187
+ "Compact upstream call start request_id=%s account_id=%s timeout_seconds=%.2f "
1188
+ "remaining_budget=%.2f",
1189
+ request_id,
1190
+ target.id,
1191
+ upstream_budget,
1192
+ remaining_budget,
1193
+ )
1194
+ response = await asyncio.wait_for(
1195
+ _call_with_supported_optional_kwargs(
1196
+ _service_core_compact_responses(),
1197
+ payload,
1198
+ filtered,
1199
+ access_token,
1200
+ account_id,
1201
+ optional_kwargs={
1202
+ "route": route,
1203
+ "allow_direct_egress": route is None,
1204
+ "route_trace": route_trace,
1205
+ "chatgpt_account_id": account_id,
1206
+ "synthesize_routing_hint": True,
1207
+ # ``target.id`` is the load-balancer account
1208
+ # id; ``account_id`` above is the wire one.
1209
+ # The scope token hashes the LB id in both
1210
+ # the stream and compact paths.
1211
+ "thread_cache_identity": ThreadCacheIdentity(
1212
+ mode=thread_cache_identity_mode,
1213
+ account_id=target.id,
1214
+ ),
1215
+ },
1216
+ ),
1217
+ timeout=upstream_budget,
1218
+ )
1219
+ logger.info(
1220
+ "Compact upstream call complete request_id=%s account_id=%s elapsed_seconds=%.2f "
1221
+ "timeout_seconds=%.2f",
1222
+ request_id,
1223
+ target.id,
1224
+ time.monotonic() - upstream_started_at,
1225
+ upstream_budget,
1226
+ )
1227
+ return response
1228
+ except ProxyResponseError as exc:
1229
+ error = _parse_openai_error(exc.payload)
1230
+ code = _normalize_error_code(
1231
+ error.code if error else None,
1232
+ error.type if error else None,
1233
+ )
1234
+ error_message = error.message if error and error.message is not None else ""
1235
+ if (
1236
+ code == "upstream_unavailable"
1237
+ and exc.retryable_same_contract
1238
+ and (
1239
+ exc.failure_exception_type in {"TimeoutError", "ServerTimeoutError"}
1240
+ or "timed out" in error_message.lower()
1241
+ or "timeout" in error_message.lower()
1242
+ )
1243
+ ):
1244
+ logger.warning(
1245
+ "Compact inner upstream timeout surfaced request_id=%s account_id=%s "
1246
+ "elapsed_seconds=%.2f timeout_seconds=%.2f",
1247
+ request_id,
1248
+ target.id,
1249
+ time.monotonic() - upstream_started_at,
1250
+ upstream_budget,
1251
+ )
1252
+ raise ProxyResponseError(
1253
+ 502,
1254
+ openai_error("upstream_request_timeout", "Compact upstream call timed out"),
1255
+ ) from exc
1256
+ raise
1257
+ except asyncio.TimeoutError as exc:
1258
+ logger.warning(
1259
+ "Compact upstream call timed out request_id=%s account_id=%s elapsed_seconds=%.2f "
1260
+ "timeout_seconds=%.2f",
1261
+ request_id,
1262
+ target.id,
1263
+ time.monotonic() - upstream_started_at,
1264
+ upstream_budget,
1265
+ )
1266
+ raise ProxyResponseError(
1267
+ 502,
1268
+ openai_error("upstream_request_timeout", "Compact upstream call timed out"),
1269
+ ) from exc
1270
+ finally:
1271
+ if route_trace.mode is not None:
1272
+ route_mode = route_trace.mode
1273
+ route_pool_id = route_trace.pool_id
1274
+ route_endpoint_id = route_trace.endpoint_id
1275
+ route_fallback_used = route_trace.fallback_used
1276
+ finally:
1277
+ if create_lease is not None:
1278
+ create_lease.release()
1279
+ await proxy._load_balancer.release_account_lease(account_response_create_lease)
1280
+ if timeout_tokens is not None:
1281
+ _service_pop_compact_timeout_overrides(timeout_tokens)
1282
+
1283
+ last_exc: ProxyResponseError | None = None
1284
+ network_recovery = ProcessNetworkRecovery(transport="compact", request_id=request_id)
1285
+ excluded_account_ids: set[str] = set()
1286
+ # Account-neutral replay off a pinned previous-response owner is only
1287
+ # legal for owner loss the owner's quota state caused: either the
1288
+ # owner was never usable at selection time, or it was excluded
1289
+ # mid-request by a pre-visible quota / rate-limit failure. Post-
1290
+ # selection authentication, refresh, transport, and transient
1291
+ # failures also exclude the owner, and those keep their existing
1292
+ # owner-bound handling instead of moving the history to another
1293
+ # account.
1294
+ owner_quota_failover_eligible = False
1295
+ require_security_work_authorized = False
1296
+ estimated_lease_tokens = _estimated_lease_tokens_from_request_usage_budget(
1297
+ estimate_api_key_request_usage(payload)
1298
+ )
1299
+ for _account_attempt in range(_compact_max_account_attempts()):
1300
+ selection = await proxy._select_account_with_budget_compatible(
1301
+ deadline,
1302
+ request_id=request_id,
1303
+ kind="compact",
1304
+ api_key=api_key,
1305
+ affinity_policy=affinity,
1306
+ prefer_earlier_reset_accounts=prefer_earlier_reset,
1307
+ prefer_earlier_reset_window=_prefer_earlier_reset_window(settings),
1308
+ routing_strategy=routing_strategy,
1309
+ model=payload.model,
1310
+ service_tier=payload.service_tier,
1311
+ exclude_account_ids=excluded_account_ids,
1312
+ preferred_account_id=preferred_account_id,
1313
+ require_security_work_authorized=require_security_work_authorized,
1314
+ lease_kind="response_create",
1315
+ estimated_lease_tokens=estimated_lease_tokens,
1316
+ fallback_on_preferred_account_unavailable=preferred_account_id is None,
1317
+ )
1318
+ account = selection.account
1319
+ if not account:
1320
+ if (
1321
+ require_security_work_authorized
1322
+ and selection.error_code == _no_security_work_authorized_accounts_code()
1323
+ and last_exc is not None
1324
+ ):
1325
+ logger.info(
1326
+ "No security-work-authorized account available for compact retry; "
1327
+ "continuing normal account failover request_id=%s",
1328
+ request_id,
1329
+ )
1330
+ require_security_work_authorized = False
1331
+ selection = await proxy._select_account_with_budget_compatible(
1332
+ deadline,
1333
+ request_id=request_id,
1334
+ kind="compact",
1335
+ api_key=api_key,
1336
+ affinity_policy=affinity,
1337
+ prefer_earlier_reset_accounts=prefer_earlier_reset,
1338
+ prefer_earlier_reset_window=_prefer_earlier_reset_window(settings),
1339
+ routing_strategy=routing_strategy,
1340
+ model=payload.model,
1341
+ service_tier=payload.service_tier,
1342
+ exclude_account_ids=excluded_account_ids,
1343
+ preferred_account_id=preferred_account_id,
1344
+ require_security_work_authorized=False,
1345
+ lease_kind="response_create",
1346
+ estimated_lease_tokens=estimated_lease_tokens,
1347
+ fallback_on_preferred_account_unavailable=preferred_account_id is None,
1348
+ )
1349
+ account = selection.account
1350
+ if (
1351
+ account is None
1352
+ and previous_response_preferred_account_id is not None
1353
+ and preferred_account_id == previous_response_preferred_account_id
1354
+ ):
1355
+ # Narrowed alias: the structural gate above proves the
1356
+ # selection pin names the previous-response owner.
1357
+ unavailable_owner_account_id = previous_response_preferred_account_id
1358
+ # The pinned previous-response owner cannot be selected.
1359
+ # A full resend that is provably account-neutral on the
1360
+ # wire needs nothing from the owner, so the stale anchor
1361
+ # can be dropped and the compact can move to a healthy
1362
+ # account instead of wedging the session until the
1363
+ # owner's quota window resets — the same selection-time
1364
+ # escape normal turns already have. Turn-state and file
1365
+ # pins keep the request owner-bound (the first blocked
1366
+ # reason below), but their selection failure still
1367
+ # records the fail-closed outcome on the common path.
1368
+ recovery_blocked_reason: str | None = None
1369
+ if turn_state_owner_account_id is not None or rewritten_file_account_id is not None:
1370
+ # The previous-response owner is also pinned by a
1371
+ # turn-state or input-file owner. Those pins are
1372
+ # account ownership this recovery must never move,
1373
+ # so the request stays owner-bound regardless of
1374
+ # the owner's quota state — but the unavailable
1375
+ # owner still fails closed and must be recorded.
1376
+ recovery_blocked_reason = "additional_owner_pins"
1377
+ elif previous_response_lookup_session_id is not None:
1378
+ # A session/turn-state identity on the request can
1379
+ # bind live or durable HTTP-bridge continuity rows
1380
+ # that still name the lost owner. Without the
1381
+ # rebinding machinery this recovery deliberately
1382
+ # avoids, moving the history would strand that
1383
+ # continuity, so session-scoped requests stay
1384
+ # owner-bound.
1385
+ recovery_blocked_reason = "session_scoped_continuity"
1386
+ elif (
1387
+ affinity.kind == StickySessionKind.CODEX_SESSION
1388
+ or affinity.legacy_selection_key is not None
1389
+ or affinity.require_unambiguous_account
1390
+ ):
1391
+ # CODEX_SESSION affinity (turn-state, thread, or
1392
+ # session-header keys, including raw legacy rows)
1393
+ # is session ownership this recovery would have to
1394
+ # rebind, so those requests stay owner-bound.
1395
+ # PROMPT_CACHE / STICKY_THREAD keys are soft cache
1396
+ # locality the sticky selection path already falls
1397
+ # back from on an unavailable account — the compact
1398
+ # routes derive one unconditionally — so they gate
1399
+ # nothing here and the recovery reselection flows
1400
+ # through that same existing sticky handling.
1401
+ recovery_blocked_reason = "session_affinity"
1402
+ elif (
1403
+ not owner_quota_failover_eligible
1404
+ and selection.error_code == "preferred_account_unavailable"
1405
+ ):
1406
+ # The selector skipped the owner before evaluating
1407
+ # its availability (API-key assignment scope,
1408
+ # single-account routing, or an in-request
1409
+ # exclusion that was not a pre-visible quota
1410
+ # failover). Policy-caused loss must not become
1411
+ # replay-eligible just because the owner's
1412
+ # persisted status happens to be quota-exhausted.
1413
+ recovery_blocked_reason = "owner_skipped_by_policy"
1414
+ elif not (
1415
+ owner_quota_failover_eligible
1416
+ or (
1417
+ unavailable_owner_account_id not in excluded_account_ids
1418
+ and await proxy._compact_owner_selection_loss_is_quota_caused(
1419
+ unavailable_owner_account_id
1420
+ )
1421
+ )
1422
+ ):
1423
+ recovery_blocked_reason = "non_quota_owner_loss"
1424
+ replay_payload: ResponsesCompactRequest | None = None
1425
+ if recovery_blocked_reason is None:
1426
+ replay_payload = _compact_account_neutral_replay_payload(payload)
1427
+ if replay_payload is None:
1428
+ recovery_blocked_reason = "history_not_account_neutral"
1429
+ if replay_payload is None:
1430
+ logger.info(
1431
+ "Compact previous-response owner unavailable; staying owner-bound "
1432
+ "request_id=%s owner_account_id=%s blocked_reason=%s selection_error_code=%s",
1433
+ request_id,
1434
+ preferred_account_id,
1435
+ recovery_blocked_reason,
1436
+ selection.error_code,
1437
+ )
1438
+ _record_continuity_fail_closed(
1439
+ surface="compact",
1440
+ reason="owner_account_unavailable",
1441
+ previous_response_id=previous_response_id
1442
+ if isinstance(previous_response_id, str)
1443
+ else None,
1444
+ session_id=previous_response_lookup_session_id,
1445
+ upstream_error_code=selection.error_code,
1446
+ )
1447
+ else:
1448
+ logger.warning(
1449
+ "Compact previous-response owner unavailable; replaying verified "
1450
+ "account-neutral full resend request_id=%s owner_account_id=%s "
1451
+ "selection_error_code=%s",
1452
+ request_id,
1453
+ preferred_account_id,
1454
+ selection.error_code,
1455
+ )
1456
+ excluded_account_ids.add(unavailable_owner_account_id)
1457
+ payload = replay_payload
1458
+ filtered = without_http_bridge_session_affinity_headers(filtered)
1459
+ preferred_account_id = None
1460
+ previous_response_preferred_account_id = None
1461
+ selection = await proxy._select_account_with_budget_compatible(
1462
+ deadline,
1463
+ request_id=request_id,
1464
+ kind="compact",
1465
+ api_key=api_key,
1466
+ affinity_policy=affinity,
1467
+ prefer_earlier_reset_accounts=prefer_earlier_reset,
1468
+ prefer_earlier_reset_window=_prefer_earlier_reset_window(settings),
1469
+ routing_strategy=routing_strategy,
1470
+ model=payload.model,
1471
+ service_tier=payload.service_tier,
1472
+ exclude_account_ids=excluded_account_ids,
1473
+ preferred_account_id=None,
1474
+ require_security_work_authorized=require_security_work_authorized,
1475
+ lease_kind="response_create",
1476
+ estimated_lease_tokens=estimated_lease_tokens,
1477
+ fallback_on_preferred_account_unavailable=True,
1478
+ )
1479
+ account = selection.account
1480
+ if account is not None:
1481
+ pass
1482
+ elif last_exc is not None:
1483
+ break
1484
+ else:
1485
+ log_error_code = selection.error_code or "no_accounts"
1486
+ log_error_message = selection.error_message or "No active accounts available"
1487
+ status_code, error_payload = selection_failure_response(selection)
1488
+ raise ProxyResponseError(
1489
+ status_code,
1490
+ error_payload,
1491
+ )
1492
+ assert account is not None
1493
+ account_id_value = account.id
1494
+ selected_account_response_create_lease = selection.lease
1495
+ remaining_budget = _remaining_budget_seconds(deadline)
1496
+ if remaining_budget <= 0:
1497
+ logger.warning("Compact request budget exhausted before freshness check request_id=%s", request_id)
1498
+ await proxy._load_balancer.release_account_lease(selected_account_response_create_lease)
1499
+ # This budget-exhausted terminal exits compact_responses before
1500
+ # reaching the retry loop's settle sites, so on the HTTP bridge /
1501
+ # forwarded path (``owns_reservation`` false, ``compact_responses``
1502
+ # is the sole settler) the API-key reservation would leak held
1503
+ # quota. Settle BEFORE raising, mirroring the transport/permanent
1504
+ # preflight branches above.
1505
+ await settle_compact_usage(
1506
+ api_key=api_key,
1507
+ api_key_reservation=api_key_reservation,
1508
+ response=None,
1509
+ request_service_tier=request_service_tier,
1510
+ )
1511
+ _raise_proxy_budget_exhausted()
1512
+ freshness_budget = _compact_freshness_budget_seconds(remaining_budget)
1513
+ if freshness_budget <= 0:
1514
+ logger.warning(
1515
+ "Compact request budget exhausted before freshness check reserve request_id=%s "
1516
+ "remaining_budget=%.2f",
1517
+ request_id,
1518
+ remaining_budget,
1519
+ )
1520
+ await proxy._load_balancer.release_account_lease(selected_account_response_create_lease)
1521
+ # Sole-settler leak guard (see above): settle the reservation
1522
+ # before this budget-exhausted terminal raise.
1523
+ await settle_compact_usage(
1524
+ api_key=api_key,
1525
+ api_key_reservation=api_key_reservation,
1526
+ response=None,
1527
+ request_service_tier=request_service_tier,
1528
+ )
1529
+ _raise_proxy_budget_exhausted()
1530
+ try:
1531
+ logger.info(
1532
+ "Compact freshness start request_id=%s account_id=%s timeout_seconds=%.2f "
1533
+ "remaining_budget=%.2f",
1534
+ request_id,
1535
+ account.id,
1536
+ freshness_budget,
1537
+ remaining_budget,
1538
+ )
1539
+ account = await proxy._ensure_fresh_with_budget(account, timeout_seconds=freshness_budget)
1540
+ logger.info(
1541
+ "Compact freshness complete request_id=%s account_id=%s",
1542
+ request_id,
1543
+ account.id,
1544
+ )
1545
+ except ProxyResponseError:
1546
+ await proxy._load_balancer.release_account_lease(selected_account_response_create_lease)
1547
+ selected_account_response_create_lease = None
1548
+ # ensure_fresh_with_budget translates terminal process-network
1549
+ # recovery outcomes before the compact upstream settlement
1550
+ # branches run, so this boundary owns reservation cleanup.
1551
+ await settle_compact_usage(
1552
+ api_key=api_key,
1553
+ api_key_reservation=api_key_reservation,
1554
+ response=None,
1555
+ request_service_tier=request_service_tier,
1556
+ )
1557
+ raise
1558
+ except (RefreshError, aiohttp.ClientError, asyncio.TimeoutError) as exc:
1559
+ await proxy._load_balancer.release_account_lease(selected_account_response_create_lease)
1560
+ selected_account_response_create_lease = None
1561
+ if isinstance(exc, RefreshError):
1562
+ if exc.is_permanent:
1563
+ # Permanent refresh failures keep their prior
1564
+ # escalation (they propagate to the caller). On the
1565
+ # HTTP bridge / forwarded path the caller passes an
1566
+ # ``api_key_reservation_override`` with
1567
+ # ``owns_reservation`` false, so ``compact_responses``
1568
+ # is the sole settler; settle BEFORE raising so the
1569
+ # reservation is finalized instead of leaking held
1570
+ # API-key quota (matching the post-401 permanent
1571
+ # branch, which settles before re-raising).
1572
+ await settle_compact_usage(
1573
+ api_key=api_key,
1574
+ api_key_reservation=api_key_reservation,
1575
+ response=None,
1576
+ request_service_tier=request_service_tier,
1577
+ )
1578
+ raise
1579
+ if is_transient_refresh_contention(exc):
1580
+ # Transient CROSS-REPLICA refresh contention: benign
1581
+ # claim contention (``refresh_claim_timeout``: the
1582
+ # account's refresh claim is held by another replica) OR
1583
+ # a post-exchange persist/status CAS conflict
1584
+ # (``token_persist_conflict`` / ``status_downgrade_conflict``).
1585
+ # This is NOT a genuine ``transport_error`` OAuth failure
1586
+ # — the account's credentials are healthy — so it fails
1587
+ # over WITHOUT an account-health penalty. Unlike the
1588
+ # genuine transport-level failure handled below, do NOT
1589
+ # record ``_handle_stream_error`` here: that would push an
1590
+ # otherwise-healthy account into backoff for normal
1591
+ # cross-replica contention. Surface a retryable
1592
+ # ``upstream_unavailable`` on exhaustion (``last_exc``).
1593
+ message = exc.message or str(exc) or "Request to upstream timed out"
1594
+ if refresh_contention_kind(exc) == "persist_conflict":
1595
+ logger.warning(
1596
+ "Compact refresh post-exchange persist conflict code=%s "
1597
+ "request_id=%s account_id=%s",
1598
+ exc.code,
1599
+ request_id,
1600
+ account.id,
1601
+ exc_info=True,
1602
+ )
1603
+ else:
1604
+ logger.warning(
1605
+ "Compact refresh claim contention request_id=%s account_id=%s",
1606
+ request_id,
1607
+ account.id,
1608
+ exc_info=True,
1609
+ )
1610
+ if preferred_account_id is not None:
1611
+ # File/previous-response-pinned requests cannot
1612
+ # fail over. On the HTTP bridge / forwarded path
1613
+ # the caller passes an ``api_key_reservation_override``
1614
+ # with ``owns_reservation`` false, making
1615
+ # ``compact_responses`` responsible for settling the
1616
+ # reservation. Settle it BEFORE raising so the
1617
+ # API-key reservation is finalized instead of leaking
1618
+ # held quota when the pinned refresh claim times out.
1619
+ await settle_compact_usage(
1620
+ api_key=api_key,
1621
+ api_key_reservation=api_key_reservation,
1622
+ response=None,
1623
+ request_service_tier=request_service_tier,
1624
+ )
1625
+ _raise_proxy_unavailable(message)
1626
+ last_exc = ProxyResponseError(502, openai_error("upstream_unavailable", message))
1627
+ excluded_account_ids.add(account.id)
1628
+ continue
1629
+ # A GENUINE OAuth transport failure (``code == "transport_error"``:
1630
+ # the refresh request itself timed out / its upstream
1631
+ # connection failed). This IS the account/route's fault, so
1632
+ # it falls through to the shared transport-failure handling
1633
+ # below — identical to a raw aiohttp/connect failure — which
1634
+ # records the account-health penalty (``_handle_stream_error``)
1635
+ # so a persistently broken account backs off instead of
1636
+ # being kept healthy and reselected on the next request.
1637
+ message = getattr(exc, "message", None) or str(exc) or "Request to upstream timed out"
1638
+ logger.warning(
1639
+ "Compact refresh/connect failed request_id=%s account_id=%s",
1640
+ request_id,
1641
+ account.id,
1642
+ exc_info=True,
1643
+ )
1644
+ # Both terminal (non-failover) transport-failure raises below
1645
+ # exit compact_responses without reaching the retry loop's
1646
+ # settle sites, so on the HTTP bridge / forwarded path
1647
+ # (owns_reservation false, compact_responses is the sole
1648
+ # settler) the API-key reservation would leak held quota.
1649
+ # Settle BEFORE raising, mirroring the claim-contention and
1650
+ # post-401 transport branches.
1651
+ if not _should_retry_transient_stream_error("upstream_unavailable", message):
1652
+ await settle_compact_usage(
1653
+ api_key=api_key,
1654
+ api_key_reservation=api_key_reservation,
1655
+ response=None,
1656
+ request_service_tier=request_service_tier,
1657
+ )
1658
+ _raise_proxy_unavailable(message)
1659
+ if preferred_account_id is not None:
1660
+ await settle_compact_usage(
1661
+ api_key=api_key,
1662
+ api_key_reservation=api_key_reservation,
1663
+ response=None,
1664
+ request_service_tier=request_service_tier,
1665
+ )
1666
+ _raise_proxy_unavailable(message)
1667
+ await record_or_defer_stream_health(
1668
+ account,
1669
+ {"message": message},
1670
+ "upstream_unavailable",
1671
+ )
1672
+ last_exc = ProxyResponseError(502, openai_error("upstream_unavailable", message))
1673
+ excluded_account_ids.add(account.id)
1674
+ continue
1675
+ except BaseException:
1676
+ await proxy._load_balancer.release_account_lease(selected_account_response_create_lease)
1677
+ selected_account_response_create_lease = None
1678
+ raise
1679
+ remaining_budget = _remaining_budget_seconds(deadline)
1680
+ if remaining_budget <= 0:
1681
+ logger.warning(
1682
+ "Compact request budget exhausted after freshness check request_id=%s account_id=%s",
1683
+ request_id,
1684
+ account.id,
1685
+ )
1686
+ await proxy._load_balancer.release_account_lease(selected_account_response_create_lease)
1687
+ # Sole-settler leak guard (see above): settle the reservation
1688
+ # before this budget-exhausted terminal raise.
1689
+ await settle_compact_usage(
1690
+ api_key=api_key,
1691
+ api_key_reservation=api_key_reservation,
1692
+ response=None,
1693
+ request_service_tier=request_service_tier,
1694
+ )
1695
+ _raise_proxy_budget_exhausted()
1696
+ request_service_tier = _service_tier_from_compact_payload(payload)
1697
+
1698
+ safe_retry_budget = _compact_same_contract_retry_budget()
1699
+ transient_retries = 0
1700
+ refresh_retry_used = False
1701
+ transient_exhausted = False
1702
+ while True:
1703
+ try:
1704
+ account_response_create_lease = selected_account_response_create_lease
1705
+ selected_account_response_create_lease = None
1706
+ response = await _call_compact(account, account_response_create_lease)
1707
+ network_recovery.log_recovered()
1708
+ actual_service_tier = _service_tier_from_response(response)
1709
+ await proxy._load_balancer.record_success(account)
1710
+ await settle_compact_usage(
1711
+ api_key=api_key,
1712
+ api_key_reservation=api_key_reservation,
1713
+ response=response,
1714
+ request_service_tier=request_service_tier,
1715
+ )
1716
+ log_status = "success"
1717
+ return response
1718
+ except ProxyResponseError as exc:
1719
+ if exc.failure_phase == "usage_settlement":
1720
+ raise
1721
+ compact_continuity_error = _compact_previous_response_not_found_error(exc)
1722
+ if compact_continuity_error is not None:
1723
+ await settle_compact_usage(
1724
+ api_key=api_key,
1725
+ api_key_reservation=api_key_reservation,
1726
+ response=None,
1727
+ request_service_tier=request_service_tier,
1728
+ )
1729
+ _record_continuity_fail_closed(
1730
+ surface="compact",
1731
+ reason="previous_response_not_found",
1732
+ previous_response_id=None,
1733
+ session_id=_owner_lookup_session_id_from_headers(headers),
1734
+ upstream_error_code=_proxy_response_error_code(exc),
1735
+ )
1736
+ raise compact_continuity_error from exc
1737
+ if exc.status_code == 401:
1738
+ if refresh_retry_used:
1739
+ try:
1740
+ await record_or_defer_proxy_health(account, exc)
1741
+ except Exception:
1742
+ await settle_compact_usage(
1743
+ api_key=api_key,
1744
+ api_key_reservation=api_key_reservation,
1745
+ response=None,
1746
+ request_service_tier=request_service_tier,
1747
+ )
1748
+ raise
1749
+ last_exc = exc
1750
+ excluded_account_ids.add(account.id)
1751
+ transient_exhausted = True
1752
+ break
1753
+ try:
1754
+ remaining_budget = _remaining_budget_seconds(deadline)
1755
+ if remaining_budget <= 0:
1756
+ logger.warning(
1757
+ "Compact request budget exhausted before forced refresh retry request_id=%s "
1758
+ "account_id=%s",
1759
+ request_id,
1760
+ account.id,
1761
+ )
1762
+ # Sole-settler leak guard (see above): this
1763
+ # budget-exhausted terminal exits the retry loop
1764
+ # to the outer handler without settling, so on
1765
+ # the bridge/forwarded path (``owns_reservation``
1766
+ # false) the reservation would leak held quota.
1767
+ # Settle BEFORE raising.
1768
+ await settle_compact_usage(
1769
+ api_key=api_key,
1770
+ api_key_reservation=api_key_reservation,
1771
+ response=None,
1772
+ request_service_tier=request_service_tier,
1773
+ )
1774
+ _raise_proxy_budget_exhausted()
1775
+ account = await proxy._ensure_fresh_with_budget(
1776
+ account,
1777
+ force=True,
1778
+ timeout_seconds=_compact_freshness_budget_seconds(remaining_budget),
1779
+ )
1780
+ except ProxyResponseError:
1781
+ # A translated refresh-recovery error escapes the
1782
+ # current upstream-error handler, so settle before
1783
+ # handing it to the request-level error boundary.
1784
+ await settle_compact_usage(
1785
+ api_key=api_key,
1786
+ api_key_reservation=api_key_reservation,
1787
+ response=None,
1788
+ request_service_tier=request_service_tier,
1789
+ )
1790
+ raise
1791
+ except (RefreshError, aiohttp.ClientError, asyncio.TimeoutError) as refresh_exc:
1792
+ if isinstance(refresh_exc, RefreshError):
1793
+ if refresh_exc.is_permanent:
1794
+ if preferred_account_id is None:
1795
+ # This compact request is not bound to an
1796
+ # account-owned response, turn state, or
1797
+ # file. Retire the revoked account and
1798
+ # continue the same pre-visible request on
1799
+ # another healthy account. For API-key
1800
+ # requests the health write is deferred
1801
+ # until after usage settlement.
1802
+ await record_or_defer_permanent_health(
1803
+ account,
1804
+ refresh_exc.code,
1805
+ )
1806
+ last_exc = exc
1807
+ excluded_account_ids.add(account.id)
1808
+ transient_exhausted = True
1809
+ break
1810
+ await settle_compact_usage(
1811
+ api_key=api_key,
1812
+ api_key_reservation=api_key_reservation,
1813
+ response=None,
1814
+ request_service_tier=request_service_tier,
1815
+ )
1816
+ await proxy._load_balancer.mark_permanent_failure(account, refresh_exc.code)
1817
+ raise exc
1818
+ if is_transient_refresh_contention(refresh_exc):
1819
+ # Transient CROSS-REPLICA refresh contention
1820
+ # on the post-401 forced refresh: benign
1821
+ # claim contention (``refresh_claim_timeout``)
1822
+ # OR a post-exchange persist/status CAS
1823
+ # conflict (``token_persist_conflict`` /
1824
+ # ``status_downgrade_conflict``). This is NOT
1825
+ # a genuine ``transport_error`` OAuth failure
1826
+ # — the account's credentials are healthy — so
1827
+ # fail over WITHOUT an account-health penalty
1828
+ # and surface a retryable
1829
+ # ``upstream_unavailable`` on exhaustion
1830
+ # instead of the misleading original 401. Do
1831
+ # NOT record ``_handle_stream_error`` here
1832
+ # (that is reserved for the genuine transport
1833
+ # failure handled below).
1834
+ message = (
1835
+ refresh_exc.message or str(refresh_exc) or "Request to upstream timed out"
1836
+ )
1837
+ if refresh_contention_kind(refresh_exc) == "persist_conflict":
1838
+ logger.warning(
1839
+ "Compact forced refresh post-exchange persist conflict code=%s "
1840
+ "request_id=%s account_id=%s",
1841
+ refresh_exc.code,
1842
+ request_id,
1843
+ account.id,
1844
+ exc_info=True,
1845
+ )
1846
+ else:
1847
+ logger.warning(
1848
+ "Compact forced refresh claim contention request_id=%s account_id=%s",
1849
+ request_id,
1850
+ account.id,
1851
+ exc_info=True,
1852
+ )
1853
+ if preferred_account_id is not None:
1854
+ await settle_compact_usage(
1855
+ api_key=api_key,
1856
+ api_key_reservation=api_key_reservation,
1857
+ response=None,
1858
+ request_service_tier=request_service_tier,
1859
+ )
1860
+ _raise_proxy_unavailable(message)
1861
+ last_exc = ProxyResponseError(
1862
+ 502, openai_error("upstream_unavailable", message)
1863
+ )
1864
+ excluded_account_ids.add(account.id)
1865
+ transient_exhausted = True
1866
+ break
1867
+ if not refresh_exc.transport_error:
1868
+ # Non-transport, non-permanent RefreshError
1869
+ # keeps its prior escalation: re-raise the
1870
+ # original 401 to the caller.
1871
+ await settle_compact_usage(
1872
+ api_key=api_key,
1873
+ api_key_reservation=api_key_reservation,
1874
+ response=None,
1875
+ request_service_tier=request_service_tier,
1876
+ )
1877
+ raise exc
1878
+ # A GENUINE OAuth transport failure
1879
+ # (``code == "transport_error"``): the account/
1880
+ # route is at fault, so it falls through to the
1881
+ # shared transport-failure handling below —
1882
+ # identical to a raw aiohttp/connect failure —
1883
+ # which records the account-health penalty
1884
+ # (``_handle_stream_error``) so the broken
1885
+ # account backs off instead of being reselected.
1886
+ message = getattr(refresh_exc, "message", None) or str(refresh_exc)
1887
+ message = message or "Request to upstream timed out"
1888
+ logger.warning(
1889
+ "Compact forced refresh/connect failed request_id=%s account_id=%s",
1890
+ request_id,
1891
+ account.id,
1892
+ exc_info=True,
1893
+ )
1894
+ if not _should_retry_transient_stream_error("upstream_unavailable", message):
1895
+ await settle_compact_usage(
1896
+ api_key=api_key,
1897
+ api_key_reservation=api_key_reservation,
1898
+ response=None,
1899
+ request_service_tier=request_service_tier,
1900
+ )
1901
+ _raise_proxy_unavailable(message)
1902
+ if preferred_account_id is not None:
1903
+ await settle_compact_usage(
1904
+ api_key=api_key,
1905
+ api_key_reservation=api_key_reservation,
1906
+ response=None,
1907
+ request_service_tier=request_service_tier,
1908
+ )
1909
+ _raise_proxy_unavailable(message)
1910
+ await record_or_defer_stream_health(
1911
+ account,
1912
+ {"message": message},
1913
+ "upstream_unavailable",
1914
+ )
1915
+ last_exc = ProxyResponseError(502, openai_error("upstream_unavailable", message))
1916
+ excluded_account_ids.add(account.id)
1917
+ transient_exhausted = True
1918
+ break
1919
+ refresh_retry_used = True
1920
+ continue
1921
+ if exc.status_code == 500:
1922
+ transient_retries += 1
1923
+ if (
1924
+ transient_retries < _max_transient_same_account_retries()
1925
+ and _remaining_budget_seconds(deadline) > 0
1926
+ ):
1927
+ delay = backoff_seconds(transient_retries)
1928
+ logger.info(
1929
+ "Transient compact error, retrying same account "
1930
+ "request_id=%s account_id=%s retry=%s/%s delay=%.2fs",
1931
+ request_id,
1932
+ account.id,
1933
+ transient_retries,
1934
+ _max_transient_same_account_retries(),
1935
+ delay,
1936
+ )
1937
+ await asyncio.sleep(delay)
1938
+ continue
1939
+ # Exhausted same-account transient retries — penalize and failover
1940
+ logger.warning(
1941
+ "Compact transient retries exhausted for account "
1942
+ "request_id=%s account_id=%s retries=%s code=server_error",
1943
+ request_id,
1944
+ account.id,
1945
+ transient_retries,
1946
+ )
1947
+ if api_key is not None and api_key_reservation is not None:
1948
+ deferred_http_500_health.append((account, exc, transient_retries - 1))
1949
+ else:
1950
+ await proxy._handle_proxy_error(account, exc)
1951
+ # Record remaining errors so total equals transient_retries,
1952
+ # meeting the load balancer backoff threshold (error_count >= 3).
1953
+ await proxy._load_balancer.record_errors(account, transient_retries - 1)
1954
+ last_exc = exc
1955
+ excluded_account_ids.add(account.id)
1956
+ transient_exhausted = True
1957
+ break # break inner loop → outer loop tries different account
1958
+ error = _parse_openai_error(exc.payload)
1959
+ code = _normalize_error_code(
1960
+ error.code if error else None,
1961
+ error.type if error else None,
1962
+ )
1963
+ error_message = error.message if error else None
1964
+ network_recovery.account_id = account.id
1965
+ recovery_decision = await network_recovery.wait(
1966
+ error_code=code,
1967
+ retryable_same_contract=exc.retryable_same_contract,
1968
+ deadline=deadline,
1969
+ rotate_shared_client=True,
1970
+ failed_session=exc.failed_session,
1971
+ )
1972
+ if recovery_decision == "retry":
1973
+ continue
1974
+ if recovery_decision == "exhausted":
1975
+ await settle_compact_usage(
1976
+ api_key=api_key,
1977
+ api_key_reservation=api_key_reservation,
1978
+ response=None,
1979
+ request_service_tier=request_service_tier,
1980
+ )
1981
+ _raise_proxy_budget_exhausted()
1982
+ if exc.retryable_same_contract and safe_retry_budget > 0:
1983
+ safe_retry_budget -= 1
1984
+ continue
1985
+ if _is_security_work_authorization_required_error(code, error_message):
1986
+ if (
1987
+ not account.security_work_authorized
1988
+ and account.id != preferred_account_id
1989
+ and _account_attempt < _compact_max_account_attempts() - 1
1990
+ ):
1991
+ last_exc = exc
1992
+ excluded_account_ids.add(account.id)
1993
+ require_security_work_authorized = True
1994
+ transient_exhausted = True
1995
+ break
1996
+ await settle_compact_usage(
1997
+ api_key=api_key,
1998
+ api_key_reservation=api_key_reservation,
1999
+ response=None,
2000
+ request_service_tier=request_service_tier,
2001
+ )
2002
+ raise
2003
+ if code == "account_response_create_cap":
2004
+ last_exc = exc
2005
+ excluded_account_ids.add(account.id)
2006
+ transient_exhausted = True
2007
+ break
2008
+ if _is_account_neutral_error_code(code):
2009
+ await settle_compact_usage(
2010
+ api_key=api_key,
2011
+ api_key_reservation=api_key_reservation,
2012
+ response=None,
2013
+ request_service_tier=request_service_tier,
2014
+ )
2015
+ raise
2016
+ if code == "upstream_request_timeout":
2017
+ await settle_compact_usage(
2018
+ api_key=api_key,
2019
+ api_key_reservation=api_key_reservation,
2020
+ response=None,
2021
+ request_service_tier=request_service_tier,
2022
+ )
2023
+ classified = await proxy._handle_stream_error(
2024
+ account,
2025
+ _upstream_error_from_openai(error),
2026
+ code,
2027
+ http_status=exc.status_code,
2028
+ )
2029
+ if (
2030
+ affinity.selection_key is not None
2031
+ and affinity.kind is not None
2032
+ and affinity.kind != StickySessionKind.CODEX_SESSION
2033
+ ):
2034
+ # A timeout cannot prove that durable Codex
2035
+ # ownership is invalid. Preserve both raw hard
2036
+ # turn-state rows and namespaced process locality;
2037
+ # runtime health already drives safe fallback.
2038
+ try:
2039
+ async with proxy._repo_factory() as repos:
2040
+ await repos.sticky_sessions.delete(
2041
+ affinity.selection_key,
2042
+ kind=affinity.kind,
2043
+ )
2044
+ logger.info(
2045
+ "Compact sticky mapping cleared after upstream timeout request_id=%s "
2046
+ "sticky_kind=%s",
2047
+ request_id,
2048
+ affinity.kind.value,
2049
+ )
2050
+ except Exception:
2051
+ logger.warning(
2052
+ "Failed to clear compact sticky mapping after upstream timeout "
2053
+ "request_id=%s sticky_kind=%s",
2054
+ request_id,
2055
+ affinity.kind.value,
2056
+ exc_info=True,
2057
+ )
2058
+ logger.info(
2059
+ "Failover decision request_id=%s transport=compact account_id=%s "
2060
+ "attempt=%d failure_class=%s action=surface",
2061
+ request_id,
2062
+ account.id,
2063
+ _account_attempt + 1,
2064
+ classified["failure_class"],
2065
+ )
2066
+ raise
2067
+ classified = classify_upstream_failure(
2068
+ error_code=code,
2069
+ error=_upstream_error_from_openai(error),
2070
+ http_status=exc.status_code,
2071
+ phase="first_event",
2072
+ )
2073
+ if resilience.deterministic_failover_enabled:
2074
+ action = failover_decision(
2075
+ failure_class=classified["failure_class"],
2076
+ downstream_visible=False,
2077
+ candidates_remaining=_compact_max_account_attempts() - _account_attempt - 1,
2078
+ )
2079
+ else:
2080
+ action = "surface"
2081
+ logger.info(
2082
+ "Failover decision request_id=%s transport=compact account_id=%s "
2083
+ "attempt=%d failure_class=%s action=%s",
2084
+ request_id,
2085
+ account.id,
2086
+ _account_attempt + 1,
2087
+ classified["failure_class"],
2088
+ action,
2089
+ )
2090
+ if action == "failover_next":
2091
+ if account.id == preferred_account_id and classified["failure_class"] in (
2092
+ "rate_limit",
2093
+ "quota",
2094
+ ):
2095
+ # Only a pre-visible quota / rate-limit exclusion
2096
+ # of the pinned owner makes account-neutral replay
2097
+ # recovery eligible for the remaining attempts.
2098
+ owner_quota_failover_eligible = True
2099
+ last_exc = exc
2100
+ excluded_account_ids.add(account.id)
2101
+ await record_or_defer_stream_health(
2102
+ account,
2103
+ _upstream_error_from_openai(error),
2104
+ code,
2105
+ exc.status_code,
2106
+ )
2107
+ transient_exhausted = True
2108
+ break
2109
+ await settle_compact_usage(
2110
+ api_key=api_key,
2111
+ api_key_reservation=api_key_reservation,
2112
+ response=None,
2113
+ request_service_tier=request_service_tier,
2114
+ )
2115
+ await proxy._handle_stream_error(
2116
+ account,
2117
+ _upstream_error_from_openai(error),
2118
+ code,
2119
+ http_status=exc.status_code,
2120
+ )
2121
+ raise
2122
+ if transient_exhausted:
2123
+ continue # outer loop: try different account
2124
+ # All account attempts exhausted — raise last error
2125
+ await settle_compact_usage(
2126
+ api_key=api_key,
2127
+ api_key_reservation=api_key_reservation,
2128
+ response=None,
2129
+ request_service_tier=request_service_tier,
2130
+ )
2131
+ if last_exc is not None:
2132
+ raise last_exc
2133
+ raise ProxyResponseError(
2134
+ 502,
2135
+ openai_error("upstream_unavailable", "All account attempts exhausted"),
2136
+ )
2137
+ except ProxyResponseError as exc:
2138
+ await settle_on_terminal_exit()
2139
+ failure_metadata = _request_log_failure_metadata(exc)
2140
+ error = _parse_openai_error(exc.payload)
2141
+ log_error_code = log_error_code or _normalize_error_code(
2142
+ error.code if error else None,
2143
+ error.type if error else None,
2144
+ )
2145
+ log_error_message = log_error_message or (error.message if error else None)
2146
+ raise
2147
+ except UpstreamProxyRouteError as exc:
2148
+ route_fail_closed_reason = exc.reason
2149
+ log_error_code = "upstream_proxy_unavailable"
2150
+ log_error_message = exc.reason
2151
+ await settle_compact_usage(
2152
+ api_key=api_key,
2153
+ api_key_reservation=api_key_reservation,
2154
+ response=None,
2155
+ request_service_tier=request_service_tier,
2156
+ )
2157
+ raise ProxyResponseError(
2158
+ 502,
2159
+ openai_error("upstream_proxy_unavailable", f"Upstream proxy route unavailable: {exc.reason}"),
2160
+ ) from exc
2161
+ except BaseException:
2162
+ await settle_on_terminal_exit()
2163
+ raise
2164
+ finally:
2165
+ usage = response.usage if response else None
2166
+ reasoning_effort = payload.reasoning.effort if payload.reasoning else None
2167
+ await proxy._write_request_log(
2168
+ affinity_observation=affinity_observation,
2169
+ account_id=account_id_value,
2170
+ api_key=api_key,
2171
+ request_id=request_id,
2172
+ model=payload.model,
2173
+ latency_ms=int((_service_time().monotonic() - start) * 1000),
2174
+ status=log_status,
2175
+ error_code=log_error_code,
2176
+ error_message=log_error_message,
2177
+ input_tokens=usage.input_tokens if usage else None,
2178
+ output_tokens=usage.output_tokens if usage else None,
2179
+ cached_input_tokens=(
2180
+ usage.input_tokens_details.cached_tokens if usage and usage.input_tokens_details else None
2181
+ ),
2182
+ reasoning_tokens=(
2183
+ usage.output_tokens_details.reasoning_tokens if usage and usage.output_tokens_details else None
2184
+ ),
2185
+ reasoning_effort=reasoning_effort,
2186
+ transport=_REQUEST_TRANSPORT_HTTP,
2187
+ service_tier=_effective_service_tier(request_service_tier, actual_service_tier),
2188
+ requested_service_tier=request_service_tier,
2189
+ actual_service_tier=actual_service_tier,
2190
+ failure_phase=failure_metadata.failure_phase,
2191
+ failure_detail=failure_metadata.failure_detail,
2192
+ failure_exception_type=failure_metadata.failure_exception_type,
2193
+ upstream_status_code=failure_metadata.upstream_status_code,
2194
+ upstream_error_code=failure_metadata.upstream_error_code,
2195
+ bridge_stage=failure_metadata.bridge_stage,
2196
+ upstream_proxy_route_mode=route_mode,
2197
+ upstream_proxy_pool_id=route_pool_id,
2198
+ upstream_proxy_endpoint_id=route_endpoint_id,
2199
+ upstream_proxy_fallback_used=route_fallback_used if route_endpoint_id else None,
2200
+ upstream_proxy_fail_closed_reason=route_fail_closed_reason,
2201
+ useragent=useragent,
2202
+ useragent_group=useragent_group,
2203
+ conversation_id=conversation_id,
2204
+ client_ip=client_ip,
2205
+ request_kind=request_kind,
2206
+ )
2207
+ _maybe_log_proxy_service_tier_trace(
2208
+ "compact",
2209
+ requested_service_tier=request_service_tier,
2210
+ actual_service_tier=actual_service_tier,
2211
+ )