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,830 @@
1
+ """Sticky-affinity and prompt-cache key helpers for proxy routing.
2
+
3
+ This module owns the pure request/header policy used by ``ProxyService`` to
4
+ choose a sticky session family. Keeping it outside ``service.py`` makes the
5
+ routing decisions testable without adding more responsibility to the proxy
6
+ orchestration class.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ from collections.abc import Mapping
13
+ from dataclasses import dataclass, replace
14
+ from hashlib import sha256
15
+ from typing import Literal, TypedDict
16
+ from uuid import uuid4
17
+
18
+ from app.core.metrics.prometheus import (
19
+ PROMETHEUS_AVAILABLE,
20
+ prompt_cache_key_derivation_total,
21
+ )
22
+ from app.core.openai.requests import (
23
+ ResponsesCompactRequest,
24
+ ResponsesRequest,
25
+ extract_input_file_ids,
26
+ responses_request_contains_goal_continuation_context,
27
+ )
28
+ from app.db.models import StickySessionKind
29
+ from app.modules.api_keys.service import ApiKeyData
30
+ from app.modules.proxy.replay_safety import responses_payload_is_account_neutral_fresh_replay
31
+ from app.modules.proxy.thread_anchors import (
32
+ build_thread_window,
33
+ get_thread_anchor_index,
34
+ thread_anchor_domain,
35
+ )
36
+
37
+ # This typed provenance is a routing capability: callers must never recover it
38
+ # from key text, because a client-controlled turn state can mimic any prefix.
39
+ _CodexSessionSource = Literal["session_header", "thread_header", "turn_state"]
40
+ # Request headers are stripped and HTTP forbids CR/LF, while PostgreSQL/SQLite
41
+ # text keys can safely retain LF. This sentinel makes the internal namespace
42
+ # structurally unreachable by every legacy raw header, even if its digest is
43
+ # disclosed.
44
+ _CODEX_SELECTION_KEY_PREFIX = "\ncodex-lb-affinity-v1"
45
+
46
+
47
+ class _AffinitySelectionKwargs(TypedDict):
48
+ sticky_key: str | None
49
+ sticky_kind: StickySessionKind | None
50
+ reallocate_sticky: bool
51
+ sticky_source: _CodexSessionSource | None
52
+ legacy_sticky_key: str | None
53
+ legacy_continuity_source: _CodexSessionSource | None
54
+ sticky_seed_key: str | None
55
+ sticky_seed_kind: StickySessionKind | None
56
+ spill_bare_session_on_account_cap: bool
57
+ abandon_unavailable_legacy_owner: bool
58
+ require_unambiguous_account: bool
59
+ sticky_max_age_seconds: int | None
60
+
61
+
62
+ @dataclass(frozen=True, slots=True)
63
+ class _AffinityPolicy:
64
+ key: str | None = None
65
+ kind: StickySessionKind | None = None
66
+ reallocate_sticky: bool = False
67
+ # Source capability only. Shared selection still revokes spillover for a
68
+ # required owner or any stage that may carry account-local state.
69
+ spill_on_account_cap: bool = False
70
+ # An explicit, self-contained Codex goal restart may retire only a raw
71
+ # compatibility owner whose durable account status is unavailable.
72
+ abandon_unavailable_legacy_owner: bool = False
73
+ max_age_seconds: int | None = None
74
+ codex_session_source: _CodexSessionSource | None = None
75
+ # A thread row is soft locality, but old replicas may have persisted the
76
+ # raw process/session value as hard CODEX_SESSION ownership. Keep that
77
+ # compatibility lookup explicit instead of trying to reconstruct it from
78
+ # the new opaque thread key.
79
+ legacy_codex_session_key: str | None = None
80
+ # Interpretation used when consulting that raw key. Process-session text
81
+ # is session_header even on a thread-scoped request; a thread-only raw
82
+ # key stays thread_header so a session_header tombstone cannot hide it.
83
+ legacy_continuity_source: _CodexSessionSource | None = None
84
+ # A previously unseen thread should inherit the healthy process preference
85
+ # once, then persist its own bounded row. This is never ownership: a
86
+ # missing process default may be initialized once by insert-if-absent, but
87
+ # no thread request may update or delete an established process row.
88
+ seed_selection_key: str | None = None
89
+ seed_selection_kind: StickySessionKind | None = None
90
+ # ``conversation`` has no dedicated owner index. Preserve that provenance
91
+ # until selection can prove one hard owner or a one-account pool.
92
+ require_unambiguous_account: bool = False
93
+ # For a policy whose key IS the prompt cache key: whether the client sent
94
+ # that key (``payload``) or the proxy derived it (``derived``). Recorded by
95
+ # the resolver rather than re-derived by callers, because
96
+ # ``_resolve_prompt_cache_key`` writes the derived key back onto the
97
+ # payload — so a later reader of the payload cannot tell the two apart, and
98
+ # a blank client hint the resolver rejected still looks present. Routing
99
+ # never reads this; it exists so the request log can describe the decision.
100
+ prompt_cache_key_source: str | None = None
101
+ # How the forwarded ``prompt_cache_key`` was obtained this turn. Diagnostic
102
+ # only: it never participates in routing, but it is the signal that tells
103
+ # an operator whether unanchored threads are being held or are churning.
104
+ prompt_cache_derivation_outcome: str | None = None
105
+
106
+ @property
107
+ def selection_key(self) -> str | None:
108
+ if self.key is None or self.codex_session_source != "session_header":
109
+ return self.key
110
+ # CODEX_SESSION historically mixed raw session and turn-state values.
111
+ # Namespace only the newly soft source; raw legacy rows stay hard so a
112
+ # rolling upgrade cannot reinterpret existing continuity ownership.
113
+ return _codex_session_selection_key(self.key)
114
+
115
+ @property
116
+ def legacy_selection_key(self) -> str | None:
117
+ # Old replicas persisted bare session headers as raw CODEX_SESSION
118
+ # keys. Always consult this alongside the soft row: any raw hit may be
119
+ # hard turn-state ownership and therefore takes precedence.
120
+ if self.legacy_codex_session_key is not None:
121
+ return self.legacy_codex_session_key
122
+ return self.key if self.codex_session_source == "session_header" else None
123
+
124
+ def selection_kwargs(self) -> _AffinitySelectionKwargs:
125
+ """Expand routing policy once at the account-selection boundary."""
126
+
127
+ # Keep the compatibility edge from silently omitting new policy
128
+ # fields. In particular, thread locality is incomplete if callers pass
129
+ # its row but forget the process seed or legacy hard-owner lookup.
130
+ return {
131
+ "sticky_key": self.selection_key,
132
+ "sticky_kind": self.kind,
133
+ "reallocate_sticky": self.reallocate_sticky,
134
+ "sticky_source": self.codex_session_source,
135
+ "legacy_sticky_key": self.legacy_selection_key,
136
+ "legacy_continuity_source": (
137
+ None if self.legacy_selection_key is None else (self.legacy_continuity_source or "session_header")
138
+ ),
139
+ "sticky_seed_key": self.seed_selection_key,
140
+ "sticky_seed_kind": self.seed_selection_kind,
141
+ "spill_bare_session_on_account_cap": self.spill_on_account_cap,
142
+ "abandon_unavailable_legacy_owner": self.abandon_unavailable_legacy_owner,
143
+ "require_unambiguous_account": self.require_unambiguous_account,
144
+ "sticky_max_age_seconds": self.max_age_seconds,
145
+ }
146
+
147
+ @staticmethod
148
+ def cap_spillover_allowed(
149
+ capability: bool,
150
+ preferred_account_id: str | None,
151
+ request_stage: str,
152
+ ) -> bool:
153
+ """Keep soft cap spillover strictly before account-owned transport state."""
154
+ return capability and preferred_account_id is None and request_stage in ("first_turn", "follow_up")
155
+
156
+ @staticmethod
157
+ def preferred_owner_sticky_inputs(
158
+ sticky_key: str | None,
159
+ sticky_kind: StickySessionKind | None,
160
+ reallocate_sticky: bool,
161
+ sticky_max_age_seconds: int | None,
162
+ sticky_source: _CodexSessionSource | None,
163
+ legacy_sticky_key: str | None,
164
+ ) -> tuple[
165
+ str | None,
166
+ StickySessionKind | None,
167
+ bool,
168
+ int | None,
169
+ _CodexSessionSource | None,
170
+ str | None,
171
+ ]:
172
+ if sticky_source not in {"session_header", "thread_header"}:
173
+ return (
174
+ sticky_key,
175
+ sticky_kind,
176
+ reallocate_sticky,
177
+ sticky_max_age_seconds,
178
+ sticky_source,
179
+ legacy_sticky_key,
180
+ )
181
+ # A resolved response/file/bridge owner bypasses the current-Codex
182
+ # soft row (process-session or thread PROMPT_CACHE). The raw
183
+ # compatibility row still has to be checked for conflicting legacy
184
+ # hard ownership. Selection receives no writable sticky key, so a
185
+ # raw miss cannot manufacture or rebind a mapping. The caller also
186
+ # deliberately omits any broader process seed in this exact-owner path.
187
+ return None, StickySessionKind.CODEX_SESSION, False, sticky_max_age_seconds, sticky_source, legacy_sticky_key
188
+
189
+
190
+ def _codex_session_selection_key(key: str) -> str:
191
+ # The digest avoids storing client values, while the header-impossible
192
+ # sentinel above—not secrecy—provides source separation from raw rows.
193
+ digest = sha256(key.encode()).hexdigest()
194
+ return f"{_CODEX_SELECTION_KEY_PREFIX}:session_header:{digest}"
195
+
196
+
197
+ @dataclass(frozen=True, slots=True)
198
+ class _CodexBackendIdentity:
199
+ """Independently parsed process-tree and logical-thread identities."""
200
+
201
+ process_session: str | None
202
+ thread_id: str | None
203
+
204
+ @property
205
+ def thread_selection_key(self) -> str | None:
206
+ if self.thread_id is None:
207
+ return None
208
+ # The explicit scope tag prevents the thread-only compatibility form
209
+ # from colliding with (process, thread). Length framing keeps distinct
210
+ # client tuples distinct even if a future non-HTTP caller admits NULs
211
+ # or other delimiters. The LF namespace remains unreachable by headers.
212
+ if self.process_session is None:
213
+ parts = ("thread-only", self.thread_id)
214
+ scope = "thread_only"
215
+ else:
216
+ parts = ("process-thread", self.process_session, self.thread_id)
217
+ scope = "process_thread"
218
+ encoded_parts = (part.encode() for part in parts)
219
+ framed = b"".join(len(part).to_bytes(8, "big") + part for part in encoded_parts)
220
+ digest = sha256(framed).hexdigest()
221
+ return f"{_CODEX_SELECTION_KEY_PREFIX}:thread_header:{scope}:{digest}"
222
+
223
+
224
+ _CODEX_PROCESS_SESSION_HEADERS = (
225
+ "session_id",
226
+ "session-id",
227
+ "x-codex-session-id",
228
+ "x-codex-conversation-id",
229
+ )
230
+
231
+
232
+ def _normalized_header_value(headers: Mapping[str, str], names: tuple[str, ...]) -> str | None:
233
+ normalized = {key.lower(): value for key, value in headers.items()}
234
+ for name in names:
235
+ value = normalized.get(name)
236
+ if not isinstance(value, str):
237
+ continue
238
+ stripped = value.strip()
239
+ if stripped:
240
+ return stripped
241
+ return None
242
+
243
+
244
+ def _process_session_key_from_headers(headers: Mapping[str, str]) -> str | None:
245
+ return _normalized_header_value(headers, _CODEX_PROCESS_SESSION_HEADERS)
246
+
247
+
248
+ def _thread_id_from_headers(headers: Mapping[str, str]) -> str | None:
249
+ return _normalized_header_value(headers, ("thread-id",))
250
+
251
+
252
+ def _codex_backend_identity(
253
+ headers: Mapping[str, str],
254
+ *,
255
+ thread_id: str | None = None,
256
+ ) -> _CodexBackendIdentity:
257
+ normalized_thread_id = thread_id.strip() if isinstance(thread_id, str) and thread_id.strip() else None
258
+ return _CodexBackendIdentity(
259
+ process_session=_process_session_key_from_headers(headers),
260
+ thread_id=normalized_thread_id if thread_id is not None else _thread_id_from_headers(headers),
261
+ )
262
+
263
+
264
+ def _prompt_cache_key_from_request_model(payload: ResponsesRequest | ResponsesCompactRequest) -> str | None:
265
+ typed_value = getattr(payload, "prompt_cache_key", None)
266
+ if isinstance(typed_value, str) and typed_value:
267
+ return typed_value
268
+ if not payload.model_extra:
269
+ return None
270
+ extra_value = payload.model_extra.get("prompt_cache_key")
271
+ if isinstance(extra_value, str) and extra_value:
272
+ return extra_value
273
+ camel_value = payload.model_extra.get("promptCacheKey")
274
+ if isinstance(camel_value, str) and camel_value:
275
+ return camel_value
276
+ return None
277
+
278
+
279
+ def _extract_model_class(model: str) -> str:
280
+ """Extract model class from model name for cache key prefix.
281
+
282
+ Classification:
283
+ - "mini" for gpt-5.4-mini
284
+ - "codex" for gpt-5.3-codex* (any variant)
285
+ - "std" for all others
286
+ """
287
+ if "codex" in model:
288
+ return "codex"
289
+ if "mini" in model:
290
+ return "mini"
291
+ return "std"
292
+
293
+
294
+ # Version marker for keys minted by the thread-anchor derivation. It also
295
+ # separates the new shape from the legacy content-hash shape
296
+ # (``{model_class}-{api_key_id[:12]}-{hash}...``) so the stale legacy rows can
297
+ # be swept by key prefix. The readable model class and API-key prefix are kept
298
+ # because operators search ``sticky_sessions`` by key text.
299
+ _ANCHORED_KEY_VERSION = "v2t"
300
+ # Fallback when no caller supplies the dashboard freshness window. Matches the
301
+ # ``openai_cache_affinity_max_age_seconds`` column default.
302
+ _DEFAULT_ANCHOR_TTL_SECONDS = 1800
303
+
304
+ DERIVATION_OUTCOME_PAYLOAD = "payload"
305
+ DERIVATION_OUTCOME_DISABLED = "disabled"
306
+ DERIVATION_OUTCOME_ANCHOR_HIT = "anchor_hit"
307
+ DERIVATION_OUTCOME_ANCHOR_NEW = "anchor_new"
308
+ DERIVATION_OUTCOME_ANCHOR_RESET = "anchor_reset"
309
+ DERIVATION_OUTCOME_UNANCHORABLE = "unanchorable"
310
+
311
+
312
+ @dataclass(frozen=True, slots=True)
313
+ class _PromptCacheAnchor:
314
+ """Result of anchoring one turn.
315
+
316
+ ``sticky_key`` is both the value forwarded upstream and the sticky routing
317
+ key -- deliberately the same string, so there is no proxy-minted value that
318
+ is forwarded but must not be routed on. It is ``None`` when the turn is
319
+ unanchorable: there is no transcript to hold, so there is nothing to name,
320
+ selection takes the unbound path, and **nothing is written onto the
321
+ payload**.
322
+
323
+ That last part is load-bearing. The same payload object is resolved a
324
+ second time on the real bridge -> ``_stream_with_retry`` fallback, and a
325
+ written placeholder comes back through the client-supplied branch: a
326
+ constant ``<class>-<apikey12>`` string promoted to a PROMPT_CACHE sticky
327
+ key, collapsing every unanchorable thread of one API key onto one row and
328
+ one account, reported as ``source=payload``.
329
+ """
330
+
331
+ sticky_key: str | None
332
+ outcome: str
333
+
334
+
335
+ def _input_contains_prior_assistant_turn(payload: ResponsesRequest | ResponsesCompactRequest) -> bool:
336
+ """Whether this body already carries model output from an earlier turn."""
337
+
338
+ input_value = getattr(payload, "input", None)
339
+ if not isinstance(input_value, list):
340
+ return False
341
+ for item in input_value:
342
+ if not isinstance(item, dict):
343
+ continue
344
+ if item.get("role") == "assistant" or item.get("type") == "reasoning":
345
+ return True
346
+ return False
347
+
348
+
349
+ def _derive_prompt_cache_anchor(
350
+ payload: ResponsesRequest | ResponsesCompactRequest,
351
+ api_key: ApiKeyData | None,
352
+ *,
353
+ max_age_seconds: int = _DEFAULT_ANCHOR_TTL_SECONDS,
354
+ ) -> _PromptCacheAnchor:
355
+ """Anchor a turn with no client-supplied ``prompt_cache_key`` to its thread.
356
+
357
+ The key is minted once per thread and then reused for every turn whose
358
+ transcript verifiably extends the previous one (see
359
+ ``thread_anchors``). It is therefore stable across appends, across a
360
+ client trimming its leading history, and across an identical re-derivation
361
+ of the same body (bridge-to-HTTP fallback, cross-transport replay). It
362
+ deliberately does **not** survive compaction: a compacted turn does not
363
+ extend the stored transcript, the upstream prefix cache is cold, and the
364
+ only way to bridge it would be fuzzy matching that merges distinct threads.
365
+
366
+ Unlike the previous content-hash derivation, nothing here is a truncated
367
+ digest of client text, so two threads that share their first 512
368
+ characters -- one `<environment_context>` block for one cwd, for
369
+ ``codex_cli_rs`` -- no longer collapse onto one key.
370
+ """
371
+
372
+ model = getattr(payload, "model", None)
373
+ model_class = _extract_model_class(model) if isinstance(model, str) and model else None
374
+ api_key_id = api_key.id if api_key is not None else ""
375
+ instructions = getattr(payload, "instructions", None)
376
+ instructions_text = instructions if isinstance(instructions, str) else ""
377
+ readable_parts = [part for part in (model_class, api_key_id[:12] or None) if part]
378
+
379
+ window = build_thread_window(
380
+ getattr(payload, "input", None),
381
+ domain=thread_anchor_domain(
382
+ owner_row_id=api_key_id,
383
+ model_class=model_class or "",
384
+ instructions=instructions_text,
385
+ ),
386
+ )
387
+ if window is None:
388
+ return _PromptCacheAnchor(sticky_key=None, outcome=DERIVATION_OUTCOME_UNANCHORABLE)
389
+
390
+ index = get_thread_anchor_index()
391
+ anchored_key = index.lookup(window, ttl_seconds=max_age_seconds)
392
+ if anchored_key is not None:
393
+ index.register(anchored_key, window)
394
+ return _PromptCacheAnchor(anchored_key, DERIVATION_OUTCOME_ANCHOR_HIT)
395
+
396
+ minted_key = "-".join([_ANCHORED_KEY_VERSION, *readable_parts, uuid4().hex[:16]])
397
+ index.register(minted_key, window)
398
+ # A body that already carries model output belonged to a thread we could
399
+ # not match: compaction, a restart, or an anchor eviction. Separating it
400
+ # from a genuinely first turn is what makes the counter actionable.
401
+ outcome = (
402
+ DERIVATION_OUTCOME_ANCHOR_RESET
403
+ if _input_contains_prior_assistant_turn(payload)
404
+ else DERIVATION_OUTCOME_ANCHOR_NEW
405
+ )
406
+ return _PromptCacheAnchor(minted_key, outcome)
407
+
408
+
409
+ def _derive_prompt_cache_key(
410
+ payload: ResponsesRequest | ResponsesCompactRequest,
411
+ api_key: ApiKeyData | None,
412
+ *,
413
+ max_age_seconds: int = _DEFAULT_ANCHOR_TTL_SECONDS,
414
+ ) -> str | None:
415
+ """Key attached to the payload and forwarded upstream, or ``None``.
416
+
417
+ ``None`` means the turn is unanchorable and nothing is attached; see
418
+ ``_PromptCacheAnchor``.
419
+ """
420
+
421
+ return _derive_prompt_cache_anchor(payload, api_key, max_age_seconds=max_age_seconds).sticky_key
422
+
423
+
424
+ def _sticky_key_from_session_header(headers: Mapping[str, str]) -> str | None:
425
+ # Legacy owner/request-log callers still need the historical alias order.
426
+ # New account, bridge, and replay locality MUST use the typed process/thread
427
+ # helpers above; otherwise a shared process id silently hides thread-id.
428
+ return _process_session_key_from_headers(headers) or _thread_id_from_headers(headers)
429
+
430
+
431
+ def _sticky_key_from_turn_state_header(headers: Mapping[str, str]) -> str | None:
432
+ normalized = {key.lower(): value for key, value in headers.items()}
433
+ value = normalized.get("x-codex-turn-state")
434
+ if not isinstance(value, str):
435
+ return None
436
+ stripped = value.strip()
437
+ return stripped or None
438
+
439
+
440
+ def _bare_codex_session_affinity(
441
+ headers: Mapping[str, str],
442
+ *,
443
+ enabled: bool,
444
+ allow_cap_spillover: bool,
445
+ ) -> _AffinityPolicy | None:
446
+ if not enabled:
447
+ return None
448
+ session_key = _sticky_key_from_session_header(headers)
449
+ if session_key is None:
450
+ return None
451
+ return _AffinityPolicy(
452
+ key=session_key,
453
+ kind=StickySessionKind.CODEX_SESSION,
454
+ spill_on_account_cap=allow_cap_spillover,
455
+ codex_session_source="session_header",
456
+ )
457
+
458
+
459
+ def _thread_codex_session_affinity(
460
+ headers: Mapping[str, str],
461
+ *,
462
+ enabled: bool,
463
+ max_age_seconds: int,
464
+ thread_id: str | None = None,
465
+ ) -> _AffinityPolicy | None:
466
+ if not enabled:
467
+ return None
468
+ identity = _codex_backend_identity(headers, thread_id=thread_id)
469
+ thread_key = identity.thread_selection_key
470
+ if thread_key is None:
471
+ return None
472
+ # Current Codex shares process session and prompt_cache_key across a root
473
+ # tree. Thread locality therefore reuses the bounded PROMPT_CACHE lifecycle
474
+ # but does not rewrite the upstream cache hint or create durable child rows.
475
+ legacy_key = identity.process_session or identity.thread_id
476
+ return _AffinityPolicy(
477
+ key=thread_key,
478
+ kind=StickySessionKind.PROMPT_CACHE,
479
+ max_age_seconds=max_age_seconds,
480
+ codex_session_source="thread_header",
481
+ legacy_codex_session_key=legacy_key,
482
+ legacy_continuity_source=("session_header" if identity.process_session is not None else "thread_header"),
483
+ seed_selection_key=(
484
+ _codex_session_selection_key(identity.process_session) if identity.process_session is not None else None
485
+ ),
486
+ seed_selection_kind=(StickySessionKind.CODEX_SESSION if identity.process_session is not None else None),
487
+ )
488
+
489
+
490
+ def _request_allows_bare_session_cap_spillover(
491
+ payload: ResponsesRequest | ResponsesCompactRequest,
492
+ ) -> bool:
493
+ if isinstance(payload, ResponsesRequest):
494
+ previous_response_id = payload.previous_response_id
495
+ conversation = payload.conversation
496
+ else:
497
+ extra = payload.model_extra or {}
498
+ previous_response_id = extra.get("previous_response_id")
499
+ conversation = extra.get("conversation")
500
+ # A lookup miss does not make an upstream-stored object portable. Selection
501
+ # must remain fail-closed for every owner-bearing payload shape.
502
+ return not (
503
+ (previous_response_id is not None and not isinstance(previous_response_id, str))
504
+ or (isinstance(previous_response_id, str) and bool(previous_response_id.strip()))
505
+ or (conversation is not None and not isinstance(conversation, str))
506
+ or (isinstance(conversation, str) and bool(conversation.strip()))
507
+ or extract_input_file_ids(payload.input)
508
+ )
509
+
510
+
511
+ def _request_allows_unavailable_legacy_owner_abandonment(payload: ResponsesRequest) -> bool:
512
+ # Intent and safety are separate proofs. The internal goal marker says the
513
+ # client deliberately restarted, while the replay classifier proves that
514
+ # this particular body carries no account-scoped state. Never collapse this
515
+ # into a marker-only or missing-previous-response shortcut.
516
+ if not responses_request_contains_goal_continuation_context(payload):
517
+ return False
518
+ # Classify the same canonical body that subscription egress would send.
519
+ # Raw model dumps retain accepted compatibility-only controls, which are
520
+ # not account state and must not make equivalent request forms disagree.
521
+ replay_payload = dict(payload.to_replay_safety_payload())
522
+ # ``type=response.create`` belongs to the direct-WebSocket envelope, not
523
+ # the HTTP Responses body. Remove only that exact discriminator after
524
+ # canonicalization; unknown envelope values remain fail-closed below.
525
+ if replay_payload.get("type") == "response.create":
526
+ replay_payload.pop("type")
527
+ return responses_payload_is_account_neutral_fresh_replay(replay_payload)
528
+
529
+
530
+ def _affinity_with_payload_continuity(
531
+ policy: _AffinityPolicy,
532
+ payload: ResponsesRequest | ResponsesCompactRequest,
533
+ ) -> _AffinityPolicy:
534
+ if isinstance(payload, ResponsesRequest):
535
+ conversation = payload.conversation
536
+ else:
537
+ conversation = (payload.model_extra or {}).get("conversation")
538
+ if conversation is None or (isinstance(conversation, str) and not conversation.strip()):
539
+ return policy
540
+ return replace(policy, require_unambiguous_account=True)
541
+
542
+
543
+ def _sticky_key_for_codex_control_request(
544
+ headers: Mapping[str, str],
545
+ *,
546
+ codex_session_affinity: bool,
547
+ ) -> _AffinityPolicy:
548
+ turn_state_key = _sticky_key_from_turn_state_header(headers)
549
+ if turn_state_key:
550
+ return _AffinityPolicy(
551
+ key=turn_state_key,
552
+ kind=StickySessionKind.CODEX_SESSION,
553
+ codex_session_source="turn_state",
554
+ )
555
+ session_affinity = _bare_codex_session_affinity(
556
+ headers,
557
+ enabled=codex_session_affinity,
558
+ allow_cap_spillover=False,
559
+ )
560
+ if session_affinity is not None:
561
+ return session_affinity
562
+ return _AffinityPolicy()
563
+
564
+
565
+ def _sticky_key_for_thread_goal_request(
566
+ payload: Mapping[str, object],
567
+ headers: Mapping[str, str],
568
+ codex_session_affinity: bool,
569
+ max_age_seconds: int,
570
+ ) -> _AffinityPolicy:
571
+ turn_state_key = _sticky_key_from_turn_state_header(headers)
572
+ if turn_state_key is not None:
573
+ return _AffinityPolicy(
574
+ key=turn_state_key,
575
+ kind=StickySessionKind.CODEX_SESSION,
576
+ codex_session_source="turn_state",
577
+ )
578
+ payload_thread_id = payload.get("threadId")
579
+ if isinstance(payload_thread_id, str) and payload_thread_id.strip():
580
+ thread_affinity = _thread_codex_session_affinity(
581
+ headers,
582
+ enabled=codex_session_affinity,
583
+ max_age_seconds=max_age_seconds,
584
+ thread_id=payload_thread_id,
585
+ )
586
+ if thread_affinity is not None:
587
+ return thread_affinity
588
+ # Routing only consumes a valid nonblank identity. The upstream thread-goal
589
+ # protocol remains authoritative for payload validation and error shape.
590
+ return _sticky_key_for_codex_control_request(
591
+ headers,
592
+ codex_session_affinity=codex_session_affinity,
593
+ )
594
+
595
+
596
+ def _owner_lookup_session_id_from_headers(
597
+ headers: Mapping[str, str],
598
+ *,
599
+ synthesized_turn_state: str | None = None,
600
+ ) -> str | None:
601
+ # `x-codex-turn-state` is per conversation turn/thread and is more specific
602
+ # than `session_id`, which may be shared across multiple terminals. A turn
603
+ # state generated for the current downstream connection is only an
604
+ # upstream-forwarding placeholder, however; it must not hide a durable
605
+ # client session on reconnect.
606
+ turn_state = _sticky_key_from_turn_state_header(headers)
607
+ if turn_state is not None and turn_state != synthesized_turn_state:
608
+ return turn_state
609
+ return _sticky_key_from_session_header(headers)
610
+
611
+
612
+ def _websocket_continuity_key_from_headers(
613
+ headers: Mapping[str, str],
614
+ *,
615
+ synthesized_turn_state: str | None = None,
616
+ ) -> str | None:
617
+ """Return the primary count-bounded direct-WebSocket continuity key."""
618
+
619
+ explicit_turn_state = _sticky_key_from_turn_state_header(headers)
620
+ if explicit_turn_state is not None and explicit_turn_state != synthesized_turn_state:
621
+ # Exact client continuation must outrank broader thread locality. A
622
+ # synthesized handshake placeholder is only an alias for the current
623
+ # connection and therefore does not gain this hard precedence.
624
+ return explicit_turn_state
625
+ identity = _codex_backend_identity(headers)
626
+ if identity.thread_selection_key is not None:
627
+ return identity.thread_selection_key
628
+ return _owner_lookup_session_id_from_headers(
629
+ headers,
630
+ synthesized_turn_state=synthesized_turn_state,
631
+ )
632
+
633
+
634
+ def _websocket_continuity_aliases_from_headers(
635
+ headers: Mapping[str, str],
636
+ *,
637
+ synthesized_turn_state: str | None = None,
638
+ ) -> tuple[str, ...]:
639
+ """Keep exact turn aliases without restoring process-wide thread state."""
640
+
641
+ aliases: list[str] = []
642
+ primary = _websocket_continuity_key_from_headers(
643
+ headers,
644
+ synthesized_turn_state=synthesized_turn_state,
645
+ )
646
+ if primary is not None:
647
+ aliases.append(primary)
648
+ thread_key = _codex_backend_identity(headers).thread_selection_key
649
+ if thread_key is not None:
650
+ # When an exact turn resolved first, refresh the thread alias to that
651
+ # same state so a later unanchored reconnect remains thread-local.
652
+ aliases.append(thread_key)
653
+ explicit_turn_state = _sticky_key_from_turn_state_header(headers)
654
+ if explicit_turn_state is not None and explicit_turn_state != synthesized_turn_state:
655
+ aliases.append(explicit_turn_state)
656
+ if synthesized_turn_state is not None:
657
+ aliases.append(synthesized_turn_state)
658
+ return tuple(dict.fromkeys(aliases))
659
+
660
+
661
+ # Pattern matching turn-state values synthesized by the helpers below.
662
+ # A 32-char lowercase hex (uuid4().hex) suffix follows the prefix.
663
+ _SYNTHESIZED_TURN_STATE_PATTERN = re.compile(r"^(?:http_)?turn_[0-9a-f]{32}$")
664
+
665
+
666
+ def _is_synthesized_turn_state(value: str) -> bool:
667
+ """True when ``value`` matches a turn-state synthesized by codex-lb itself.
668
+
669
+ Used by the file-pin resolver to distinguish a client-supplied
670
+ continuation marker from a synthesizer-generated placeholder so
671
+ first-turn upload-then-converse requests still benefit from
672
+ file_id pin routing on the websocket / HTTP entry points.
673
+ """
674
+ return bool(_SYNTHESIZED_TURN_STATE_PATTERN.match(value))
675
+
676
+
677
+ def ensure_downstream_turn_state(headers: Mapping[str, str]) -> str:
678
+ existing = _sticky_key_from_turn_state_header(headers)
679
+ if existing is not None:
680
+ return existing
681
+ return f"turn_{uuid4().hex}"
682
+
683
+
684
+ def ensure_http_downstream_turn_state(headers: Mapping[str, str]) -> str:
685
+ existing = _sticky_key_from_turn_state_header(headers)
686
+ if existing is not None:
687
+ return existing
688
+ return f"http_turn_{uuid4().hex}"
689
+
690
+
691
+ def build_downstream_turn_state_accept_headers(turn_state: str) -> list[tuple[bytes, bytes]]:
692
+ return [(b"x-codex-turn-state", turn_state.encode("utf-8"))]
693
+
694
+
695
+ def build_downstream_turn_state_response_headers(turn_state: str) -> dict[str, str]:
696
+ return {"x-codex-turn-state": turn_state}
697
+
698
+
699
+ @dataclass(frozen=True, slots=True)
700
+ class _PromptCacheResolution:
701
+ """Routing key plus the provenance operators need to read a shape log."""
702
+
703
+ sticky_key: str | None
704
+ source: str
705
+ outcome: str
706
+
707
+
708
+ def _resolve_prompt_cache_key(
709
+ payload: ResponsesRequest | ResponsesCompactRequest,
710
+ *,
711
+ openai_cache_affinity: bool,
712
+ api_key: ApiKeyData | None,
713
+ max_age_seconds: int = _DEFAULT_ANCHOR_TTL_SECONDS,
714
+ ) -> _PromptCacheResolution:
715
+ cache_key = _prompt_cache_key_from_request_model(payload)
716
+ if isinstance(cache_key, str):
717
+ stripped = cache_key.strip()
718
+ if stripped:
719
+ if stripped != cache_key:
720
+ payload.prompt_cache_key = stripped
721
+ return _record_prompt_cache_resolution(
722
+ _PromptCacheResolution(stripped, "payload", DERIVATION_OUTCOME_PAYLOAD)
723
+ )
724
+ if not openai_cache_affinity:
725
+ return _record_prompt_cache_resolution(_PromptCacheResolution(None, "none", DERIVATION_OUTCOME_DISABLED))
726
+ anchor = _derive_prompt_cache_anchor(payload, api_key, max_age_seconds=max_age_seconds)
727
+ if anchor.sticky_key is None:
728
+ # Unanchorable: attach nothing. A value written here is indistinguishable
729
+ # from a client-supplied one when this same payload object is resolved
730
+ # again (bridge -> `_stream_with_retry` fallback), which would promote a
731
+ # constant per-API-key string into a real sticky key. There is also
732
+ # nothing for upstream to cache: the body carried no digestible
733
+ # transcript. `http_continuation_signal` falls through to
734
+ # `http_history_signal`, which still classifies a real transcript.
735
+ return _record_prompt_cache_resolution(_PromptCacheResolution(None, "none", anchor.outcome))
736
+ # Attached so that a re-resolution of this same body -- the bridge fallback,
737
+ # or another replica after ring forwarding -- reuses the identical key
738
+ # through the client-supplied branch instead of re-deriving it.
739
+ payload.prompt_cache_key = anchor.sticky_key
740
+ return _record_prompt_cache_resolution(_PromptCacheResolution(anchor.sticky_key, "derived", anchor.outcome))
741
+
742
+
743
+ def _record_prompt_cache_resolution(resolution: _PromptCacheResolution) -> _PromptCacheResolution:
744
+ if PROMETHEUS_AVAILABLE and prompt_cache_key_derivation_total is not None:
745
+ prompt_cache_key_derivation_total.labels(outcome=resolution.outcome).inc()
746
+ return resolution
747
+
748
+
749
+ def _sticky_key_for_responses_request(
750
+ payload: ResponsesRequest,
751
+ headers: Mapping[str, str],
752
+ *,
753
+ codex_session_affinity: bool,
754
+ openai_cache_affinity: bool,
755
+ openai_cache_affinity_max_age_seconds: int,
756
+ sticky_threads_enabled: bool,
757
+ api_key: ApiKeyData | None = None,
758
+ synthesized_turn_state: str | None = None,
759
+ ) -> _AffinityPolicy:
760
+ # This helper only classifies locality keys. Stored-object continuity such
761
+ # as `previous_response_id` is resolved later by ProxyService and must stay
762
+ # hard owner-bound even if this returns a prompt-cache affinity policy.
763
+ resolution = _resolve_prompt_cache_key(
764
+ payload,
765
+ openai_cache_affinity=openai_cache_affinity,
766
+ api_key=api_key,
767
+ max_age_seconds=openai_cache_affinity_max_age_seconds,
768
+ )
769
+ cache_key = resolution.sticky_key
770
+ cache_key_source = resolution.source
771
+ turn_state_key = _sticky_key_from_turn_state_header(headers)
772
+ if turn_state_key and turn_state_key != synthesized_turn_state:
773
+ policy = _AffinityPolicy(
774
+ key=turn_state_key,
775
+ kind=StickySessionKind.CODEX_SESSION,
776
+ codex_session_source="turn_state",
777
+ )
778
+ elif (
779
+ thread_affinity := _thread_codex_session_affinity(
780
+ headers,
781
+ enabled=codex_session_affinity,
782
+ max_age_seconds=openai_cache_affinity_max_age_seconds,
783
+ )
784
+ ) is not None:
785
+ policy = thread_affinity
786
+ elif (
787
+ session_affinity := _bare_codex_session_affinity(
788
+ headers,
789
+ enabled=codex_session_affinity,
790
+ allow_cap_spillover=_request_allows_bare_session_cap_spillover(payload),
791
+ )
792
+ ) is not None:
793
+ policy = session_affinity
794
+ elif openai_cache_affinity:
795
+ policy = _AffinityPolicy(
796
+ key=cache_key,
797
+ kind=StickySessionKind.PROMPT_CACHE,
798
+ max_age_seconds=openai_cache_affinity_max_age_seconds,
799
+ prompt_cache_key_source=cache_key_source,
800
+ )
801
+ elif sticky_threads_enabled:
802
+ policy = _AffinityPolicy(
803
+ key=cache_key,
804
+ kind=StickySessionKind.STICKY_THREAD,
805
+ reallocate_sticky=True,
806
+ prompt_cache_key_source=cache_key_source,
807
+ )
808
+ elif turn_state_key is not None and turn_state_key == synthesized_turn_state:
809
+ policy = _AffinityPolicy(
810
+ key=turn_state_key,
811
+ kind=StickySessionKind.CODEX_SESSION,
812
+ codex_session_source="turn_state",
813
+ )
814
+ else:
815
+ policy = _AffinityPolicy()
816
+ if (
817
+ # The raw row this escape hatch retires is the process-session key.
818
+ # Current Codex also sends thread-id, so locality source is often
819
+ # thread_header; that must not hide the process-session exception.
820
+ # An explicit turn-state header stays hard even with the same marker.
821
+ policy.codex_session_source in {"session_header", "thread_header"}
822
+ and (
823
+ policy.codex_session_source == "session_header"
824
+ or _codex_backend_identity(headers).process_session is not None
825
+ )
826
+ and _request_allows_unavailable_legacy_owner_abandonment(payload)
827
+ ):
828
+ policy = replace(policy, abandon_unavailable_legacy_owner=True)
829
+ policy = replace(policy, prompt_cache_derivation_outcome=resolution.outcome)
830
+ return _affinity_with_payload_continuity(policy, payload)