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,2360 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ import sys
6
+ import time
7
+ from collections import deque
8
+ from collections.abc import Mapping, Sequence
9
+ from dataclasses import dataclass
10
+ from typing import Any, cast
11
+
12
+ import anyio
13
+
14
+ from app.core.balancer.types import UpstreamError
15
+ from app.core.clients.files import create_file as core_create_file # noqa: F401
16
+ from app.core.clients.files import finalize_file as core_finalize_file # noqa: F401
17
+ from app.core.clients.http import lease_http_session as lease_http_session # noqa: F401
18
+ from app.core.clients.native_egress import NativeWebSocketRoutingMetadata
19
+ from app.core.clients.proxy import ( # noqa: F401 # noqa: F401
20
+ CODEX_LB_REQUIRED_CAPABILITY_HEADER,
21
+ ImageFetchSession,
22
+ ProxyResponseError,
23
+ UpstreamProxyRouteTrace,
24
+ _as_image_fetch_session,
25
+ _inline_content_images,
26
+ _inline_input_image_urls,
27
+ _ws_transport_payload_budget_bytes,
28
+ filter_inbound_headers,
29
+ pop_compact_timeout_overrides,
30
+ pop_stream_timeout_overrides,
31
+ pop_transcribe_timeout_overrides,
32
+ push_compact_timeout_overrides,
33
+ push_stream_timeout_overrides,
34
+ push_transcribe_timeout_overrides,
35
+ )
36
+ from app.core.clients.proxy import CodexControlResponse as CodexControlResponse
37
+ from app.core.clients.proxy import codex_control_request as core_codex_control_request # noqa: F401
38
+ from app.core.clients.proxy import compact_responses as core_compact_responses # noqa: F401
39
+ from app.core.clients.proxy import transcribe_audio as core_transcribe_audio # noqa: F401
40
+ from app.core.clients.proxy_websocket import (
41
+ UpstreamWebSocketMessage,
42
+ )
43
+ from app.core.clock import Scheduler
44
+ from app.core.errors import (
45
+ PREVIOUS_RESPONSE_MALFORMED_PARAM_REASON,
46
+ PREVIOUS_RESPONSE_NOT_FOUND_CODE,
47
+ PREVIOUS_RESPONSE_NOT_FOUND_MESSAGE,
48
+ PREVIOUS_RESPONSE_STREAM_INCOMPLETE_MESSAGE,
49
+ OpenAIErrorEnvelope,
50
+ OpenAIErrorParam,
51
+ coerce_error_param,
52
+ normalize_public_error_param,
53
+ openai_error,
54
+ previous_response_stream_incomplete_error,
55
+ response_failed_event,
56
+ sanitize_public_error_detail,
57
+ )
58
+ from app.core.exceptions import AppError
59
+ from app.core.openai.models import OpenAIEvent
60
+ from app.core.openai.parsing import parse_sse_event
61
+ from app.core.openai.requests import (
62
+ ResponsesRequest,
63
+ extract_input_file_ids,
64
+ )
65
+ from app.core.types import JsonValue
66
+ from app.core.utils.shared_future import (
67
+ _await_cleanup_deferring_cancellation as _await_cleanup_deferring_cancellation,
68
+ )
69
+ from app.core.utils.sse import CODEX_KEEPALIVE_FRAME as CODEX_KEEPALIVE_FRAME # noqa: F401
70
+ from app.core.utils.sse import format_sse_event, parse_sse_data_json
71
+ from app.core.utils.time import to_utc_naive, utcnow
72
+ from app.db.models import (
73
+ Account,
74
+ AccountStatus, # noqa: F401
75
+ )
76
+ from app.modules.proxy._service.api_key_usage import (
77
+ _API_KEY_RESERVATION_HEARTBEAT_SECONDS as _API_KEY_RESERVATION_HEARTBEAT_SECONDS,
78
+ )
79
+ from app.modules.proxy._service.compact import (
80
+ _service_tier_from_compact_payload as _service_tier_from_compact_payload,
81
+ )
82
+ from app.modules.proxy._service.compact import (
83
+ _sticky_key_for_compact_request as _sticky_key_for_compact_request,
84
+ )
85
+ from app.modules.proxy._service.compact import (
86
+ _sticky_key_from_compact_payload as _sticky_key_from_compact_payload,
87
+ )
88
+ from app.modules.proxy._service.http_bridge.accepted_replay import (
89
+ _websocket_accepted_capacity_retry_error_code,
90
+ )
91
+ from app.modules.proxy._service.http_bridge.helpers import (
92
+ _active_http_bridge_instance_ring as _active_http_bridge_instance_ring,
93
+ )
94
+ from app.modules.proxy._service.http_bridge.helpers import (
95
+ _build_http_bridge_prewarm_text as _build_http_bridge_prewarm_text,
96
+ )
97
+ from app.modules.proxy._service.http_bridge.helpers import (
98
+ _durable_bridge_lookup_active_owner as _durable_bridge_lookup_active_owner,
99
+ )
100
+ from app.modules.proxy._service.http_bridge.helpers import (
101
+ _durable_bridge_lookup_allows_local_reuse as _durable_bridge_lookup_allows_local_reuse,
102
+ )
103
+ from app.modules.proxy._service.http_bridge.helpers import (
104
+ _effective_http_bridge_idle_ttl_seconds as _effective_http_bridge_idle_ttl_seconds,
105
+ )
106
+ from app.modules.proxy._service.http_bridge.helpers import (
107
+ _forwarded_http_bridge_session_key as _forwarded_http_bridge_session_key,
108
+ )
109
+ from app.modules.proxy._service.http_bridge.helpers import (
110
+ _has_http_bridge_response_output_marker as _has_http_bridge_response_output_marker,
111
+ )
112
+ from app.modules.proxy._service.http_bridge.helpers import (
113
+ _http_bridge_allow_durable_takeover as _http_bridge_allow_durable_takeover,
114
+ )
115
+ from app.modules.proxy._service.http_bridge.helpers import (
116
+ _http_bridge_can_local_recover_without_ring as _http_bridge_can_local_recover_without_ring,
117
+ )
118
+ from app.modules.proxy._service.http_bridge.helpers import (
119
+ _http_bridge_can_recover_during_drain as _http_bridge_can_recover_during_drain,
120
+ )
121
+ from app.modules.proxy._service.http_bridge.helpers import (
122
+ _http_bridge_continuity_lost_error_envelope as _http_bridge_continuity_lost_error_envelope,
123
+ )
124
+ from app.modules.proxy._service.http_bridge.helpers import (
125
+ _http_bridge_durable_lease_ttl_seconds as _http_bridge_durable_lease_ttl_seconds,
126
+ )
127
+ from app.modules.proxy._service.http_bridge.helpers import (
128
+ _http_bridge_endpoint_matches_current_instance as _http_bridge_endpoint_matches_current_instance,
129
+ )
130
+ from app.modules.proxy._service.http_bridge.helpers import (
131
+ _http_bridge_eviction_priority as _http_bridge_eviction_priority,
132
+ )
133
+ from app.modules.proxy._service.http_bridge.helpers import (
134
+ _http_bridge_has_durable_recovery_anchor as _http_bridge_has_durable_recovery_anchor,
135
+ )
136
+ from app.modules.proxy._service.http_bridge.helpers import (
137
+ _http_bridge_input_item_type as _http_bridge_input_item_type,
138
+ )
139
+ from app.modules.proxy._service.http_bridge.helpers import (
140
+ _http_bridge_is_context_overflow_error as _http_bridge_is_context_overflow_error,
141
+ )
142
+ from app.modules.proxy._service.http_bridge.helpers import (
143
+ _http_bridge_is_previous_response_owner_unavailable as _http_bridge_is_previous_response_owner_unavailable,
144
+ )
145
+ from app.modules.proxy._service.http_bridge.helpers import (
146
+ _http_bridge_key_strength as _http_bridge_key_strength,
147
+ )
148
+ from app.modules.proxy._service.http_bridge.helpers import (
149
+ _http_bridge_owner_check_required as _http_bridge_owner_check_required,
150
+ )
151
+ from app.modules.proxy._service.http_bridge.helpers import (
152
+ _http_bridge_owner_instance as _http_bridge_owner_instance,
153
+ )
154
+ from app.modules.proxy._service.http_bridge.helpers import (
155
+ _http_bridge_owner_lookup_unavailable_error_envelope as _http_bridge_owner_lookup_unavailable_error_envelope,
156
+ )
157
+ from app.modules.proxy._service.http_bridge.helpers import (
158
+ _http_bridge_payload_looks_like_full_resend as _http_bridge_payload_looks_like_full_resend,
159
+ )
160
+ from app.modules.proxy._service.http_bridge.helpers import (
161
+ _http_bridge_payload_without_previous_response_id as _http_bridge_payload_without_previous_response_id,
162
+ )
163
+ from app.modules.proxy._service.http_bridge.helpers import (
164
+ _http_bridge_precreated_retry_failure_error as _http_bridge_precreated_retry_failure_error,
165
+ )
166
+ from app.modules.proxy._service.http_bridge.helpers import (
167
+ _http_bridge_previous_response_alias_key as _http_bridge_previous_response_alias_key,
168
+ )
169
+ from app.modules.proxy._service.http_bridge.helpers import (
170
+ _http_bridge_previous_response_error_envelope as _http_bridge_previous_response_error_envelope,
171
+ )
172
+ from app.modules.proxy._service.http_bridge.helpers import (
173
+ _http_bridge_request_counts_against_queue as _http_bridge_request_counts_against_queue,
174
+ )
175
+ from app.modules.proxy._service.http_bridge.helpers import (
176
+ _http_bridge_request_stage as _http_bridge_request_stage,
177
+ )
178
+ from app.modules.proxy._service.http_bridge.helpers import (
179
+ _http_bridge_requires_cluster_registration as _http_bridge_requires_cluster_registration,
180
+ )
181
+ from app.modules.proxy._service.http_bridge.helpers import (
182
+ _http_bridge_runtime_config as _http_bridge_runtime_config,
183
+ )
184
+ from app.modules.proxy._service.http_bridge.helpers import (
185
+ _http_bridge_session_allows_api_key as _http_bridge_session_allows_api_key,
186
+ )
187
+ from app.modules.proxy._service.http_bridge.helpers import (
188
+ _http_bridge_session_has_visible_requests as _http_bridge_session_has_visible_requests,
189
+ )
190
+ from app.modules.proxy._service.http_bridge.helpers import (
191
+ _http_bridge_session_matches_preferred_account as _http_bridge_session_matches_preferred_account,
192
+ )
193
+ from app.modules.proxy._service.http_bridge.helpers import (
194
+ _http_bridge_session_retiring_with_visible_requests as _http_bridge_session_retiring_with_visible_requests,
195
+ )
196
+ from app.modules.proxy._service.http_bridge.helpers import (
197
+ _http_bridge_session_reusable_for_request as _http_bridge_session_reusable_for_request,
198
+ )
199
+ from app.modules.proxy._service.http_bridge.helpers import (
200
+ _http_bridge_should_attempt_local_bootstrap_rebind as _http_bridge_should_attempt_local_bootstrap_rebind,
201
+ )
202
+ from app.modules.proxy._service.http_bridge.helpers import (
203
+ _http_bridge_should_attempt_local_previous_response_recovery, # noqa: F401
204
+ )
205
+ from app.modules.proxy._service.http_bridge.helpers import (
206
+ _http_bridge_should_attempt_soft_affinity_reroute as _http_bridge_should_attempt_soft_affinity_reroute,
207
+ )
208
+ from app.modules.proxy._service.http_bridge.helpers import (
209
+ _http_bridge_should_rollover_after_context_overflow as _http_bridge_should_rollover_after_context_overflow,
210
+ )
211
+ from app.modules.proxy._service.http_bridge.helpers import (
212
+ _http_bridge_should_wait_for_registration as _http_bridge_should_wait_for_registration,
213
+ )
214
+ from app.modules.proxy._service.http_bridge.helpers import (
215
+ _http_bridge_startup_wait_timeout_error as _http_bridge_startup_wait_timeout_error,
216
+ )
217
+ from app.modules.proxy._service.http_bridge.helpers import (
218
+ _http_bridge_turn_state_alias_key as _http_bridge_turn_state_alias_key,
219
+ )
220
+ from app.modules.proxy._service.http_bridge.helpers import (
221
+ _is_http_bridge_previous_response_output_item as _is_http_bridge_previous_response_output_item,
222
+ )
223
+ from app.modules.proxy._service.http_bridge.helpers import (
224
+ _is_missing_durable_bridge_table_error as _is_missing_durable_bridge_table_error,
225
+ )
226
+ from app.modules.proxy._service.http_bridge.helpers import (
227
+ _log_http_bridge_event as _log_http_bridge_event,
228
+ )
229
+ from app.modules.proxy._service.http_bridge.helpers import (
230
+ _log_http_bridge_startup_wait_timeout as _log_http_bridge_startup_wait_timeout,
231
+ )
232
+ from app.modules.proxy._service.http_bridge.helpers import (
233
+ _make_http_bridge_session_key as _make_http_bridge_session_key,
234
+ )
235
+ from app.modules.proxy._service.http_bridge.helpers import (
236
+ _normalize_http_bridge_error_event as _normalize_http_bridge_error_event,
237
+ )
238
+ from app.modules.proxy._service.http_bridge.helpers import (
239
+ _normalized_http_bridge_instance_ring as _normalized_http_bridge_instance_ring,
240
+ )
241
+ from app.modules.proxy._service.http_bridge.helpers import (
242
+ _preferred_http_bridge_reconnect_turn_state as _preferred_http_bridge_reconnect_turn_state,
243
+ )
244
+ from app.modules.proxy._service.http_bridge.helpers import (
245
+ _record_bridge_drain_recovery_allowed as _record_bridge_drain_recovery_allowed,
246
+ )
247
+ from app.modules.proxy._service.http_bridge.helpers import (
248
+ _record_bridge_first_turn_timeout as _record_bridge_first_turn_timeout,
249
+ )
250
+ from app.modules.proxy._service.http_bridge.helpers import (
251
+ _record_bridge_reattach as _record_bridge_reattach,
252
+ )
253
+ from app.modules.proxy._service.http_bridge.helpers import (
254
+ _trim_http_bridge_previous_response_input_items as _trim_http_bridge_previous_response_input_items,
255
+ )
256
+ from app.modules.proxy._service.observability import (
257
+ _hash_identifier as _hash_identifier,
258
+ )
259
+ from app.modules.proxy._service.observability import (
260
+ _hash_identifier_or_none as _hash_identifier_or_none,
261
+ )
262
+ from app.modules.proxy._service.observability import (
263
+ _interesting_header_keys as _interesting_header_keys,
264
+ )
265
+ from app.modules.proxy._service.observability import (
266
+ _maybe_log_proxy_request_payload as _maybe_log_proxy_request_payload,
267
+ )
268
+ from app.modules.proxy._service.observability import (
269
+ _maybe_log_proxy_request_shape as _maybe_log_proxy_request_shape,
270
+ )
271
+ from app.modules.proxy._service.observability import (
272
+ _maybe_log_proxy_service_tier_trace as _maybe_log_proxy_service_tier_trace,
273
+ )
274
+ from app.modules.proxy._service.observability import (
275
+ _record_continuity_fail_closed as _record_continuity_fail_closed,
276
+ )
277
+ from app.modules.proxy._service.observability import (
278
+ _record_continuity_owner_resolution as _record_continuity_owner_resolution,
279
+ )
280
+ from app.modules.proxy._service.observability import (
281
+ _summarize_input as _summarize_input,
282
+ )
283
+ from app.modules.proxy._service.observability import (
284
+ _tools_hash as _tools_hash,
285
+ )
286
+ from app.modules.proxy._service.observability import (
287
+ _truncate_identifier as _truncate_identifier,
288
+ )
289
+ from app.modules.proxy._service.support import (
290
+ _ACCOUNT_MODEL_UNSUPPORTED_ERROR_CODE,
291
+ _HARD_HTTP_BRIDGE_AFFINITY_KINDS, # noqa: F401
292
+ _WEBSOCKET_FULL_REPLAY_WAIT_MIN_ITEMS,
293
+ _WEBSOCKET_FULL_REPLAY_WAIT_POLL_SECONDS, # noqa: F401
294
+ _affinity_may_resolve_hard_owner,
295
+ _clear_websocket_request_error_overrides,
296
+ _DeferredKeyedStreamHealthPenalty,
297
+ _event_type_from_payload,
298
+ _websocket_request_can_replay_before_visible_output,
299
+ _WebSocketContinuityAnchor,
300
+ _WebSocketContinuityState,
301
+ _WebSocketReceiveTimeout,
302
+ _WebSocketRequestState,
303
+ _WebSocketUpstreamControl,
304
+ )
305
+ from app.modules.proxy._service.support import (
306
+ _HTTPBridgeOwnerForward as _HTTPBridgeOwnerForward,
307
+ )
308
+ from app.modules.proxy._service.support import (
309
+ _websocket_route_log_kwargs as _websocket_route_log_kwargs,
310
+ )
311
+ from app.modules.proxy._service.warmup import (
312
+ WarmupExecutionData as WarmupExecutionData,
313
+ )
314
+ from app.modules.proxy._service.warmup import (
315
+ WarmupFailedAccountData as WarmupFailedAccountData,
316
+ )
317
+ from app.modules.proxy._service.warmup import (
318
+ WarmupSkippedAccountData as WarmupSkippedAccountData,
319
+ )
320
+ from app.modules.proxy._service.warmup import (
321
+ WarmupSubmittedAccountData as WarmupSubmittedAccountData,
322
+ )
323
+ from app.modules.proxy._service.warmup import (
324
+ _is_warmup_usage_eligible as _is_warmup_usage_eligible,
325
+ )
326
+ from app.modules.proxy._service.warmup import (
327
+ _materialize_warmup_account as _materialize_warmup_account,
328
+ )
329
+ from app.modules.proxy._service.warmup import (
330
+ _snapshot_warmup_account as _snapshot_warmup_account,
331
+ )
332
+ from app.modules.proxy._service.warmup import (
333
+ _WarmupAccountSnapshot as _WarmupAccountSnapshot,
334
+ )
335
+ from app.modules.proxy._service.warmup import (
336
+ _WarmupSubmitResult as _WarmupSubmitResult,
337
+ )
338
+ from app.modules.proxy._service.warmup import (
339
+ _WarmupUsageSnapshot as _WarmupUsageSnapshot,
340
+ )
341
+ from app.modules.proxy.affinity import (
342
+ _AffinityPolicy,
343
+ _sticky_key_from_session_header, # noqa: F401
344
+ )
345
+ from app.modules.proxy.durable_bridge_coordinator import (
346
+ DurableBridgeLookup as DurableBridgeLookup,
347
+ )
348
+ from app.modules.proxy.helpers import (
349
+ _is_account_model_unsupported_error,
350
+ _normalize_error_code,
351
+ _parse_openai_error,
352
+ is_upstream_model_capacity_error,
353
+ is_upstream_usage_limit_rejection,
354
+ )
355
+ from app.modules.proxy.http_bridge_forwarding import (
356
+ HTTPBridgeForwardContext as HTTPBridgeForwardContext,
357
+ )
358
+ from app.modules.proxy.http_bridge_forwarding import (
359
+ OwnerForwardRelayFailure as OwnerForwardRelayFailure,
360
+ )
361
+ from app.modules.proxy.replay_safety import responses_payload_is_account_neutral_fresh_replay
362
+ from app.modules.proxy.selection_errors import USAGE_LIMIT_REACHED
363
+
364
+
365
+ def _facade() -> Any:
366
+ return sys.modules["app.modules.proxy.service"]
367
+
368
+
369
+ # A confirmed stale previous-response anchor can otherwise cause every client
370
+ # reconnect to repeat the same owner lookup and doomed upstream connection.
371
+ # Keep this local and short-lived: an owner record may still be committed by a
372
+ # concurrent request, so discovery always invalidates the negative entry.
373
+ _WEBSOCKET_STALE_PREVIOUS_RESPONSE_CACHE_TTL_SECONDS = 60.0
374
+ _WEBSOCKET_STALE_PREVIOUS_RESPONSE_CACHE_LIMIT = 4096
375
+ # The index is process-global and shared by every turn, so its TTL stamps stay
376
+ # on the real monotonic clock instead of a per-turn injected clock; the
377
+ # simulation harness never reaches it (its fake turns carry no stale anchor).
378
+ _websocket_stale_previous_response_index: dict[tuple[str, str | None], float] = {}
379
+
380
+
381
+ def _clear_websocket_stale_previous_response_cache() -> None:
382
+ """Drop process-local negative entries when a proxy service is created.
383
+
384
+ The cache intentionally is not durable: it only suppresses repeated
385
+ lookups during a short recovery window. Clearing it with the service
386
+ lifecycle prevents entries from one app/test instance from affecting a
387
+ later instance that happens to receive the same synthetic response id.
388
+ """
389
+ _websocket_stale_previous_response_index.clear()
390
+
391
+
392
+ def _prune_websocket_stale_previous_response_cache(now: float | None = None) -> None:
393
+ current_time = time.monotonic() if now is None else now
394
+ for cache_key, expires_at in tuple(_websocket_stale_previous_response_index.items()):
395
+ if expires_at <= current_time:
396
+ _websocket_stale_previous_response_index.pop(cache_key, None)
397
+ while len(_websocket_stale_previous_response_index) > _WEBSOCKET_STALE_PREVIOUS_RESPONSE_CACHE_LIMIT:
398
+ _websocket_stale_previous_response_index.pop(next(iter(_websocket_stale_previous_response_index)))
399
+
400
+
401
+ def _remember_websocket_stale_previous_response(
402
+ *,
403
+ previous_response_id: str | None,
404
+ api_key_id: str | None,
405
+ ) -> None:
406
+ if previous_response_id is None:
407
+ return
408
+ response_id = previous_response_id.strip()
409
+ if not response_id:
410
+ return
411
+ now = time.monotonic()
412
+ _prune_websocket_stale_previous_response_cache(now)
413
+ cache_key = (response_id, api_key_id)
414
+ _websocket_stale_previous_response_index.pop(cache_key, None)
415
+ _websocket_stale_previous_response_index[cache_key] = now + _WEBSOCKET_STALE_PREVIOUS_RESPONSE_CACHE_TTL_SECONDS
416
+ _prune_websocket_stale_previous_response_cache(now)
417
+
418
+
419
+ def _forget_websocket_stale_previous_response(
420
+ *,
421
+ previous_response_id: str | None,
422
+ api_key_id: str | None,
423
+ ) -> None:
424
+ if previous_response_id is None:
425
+ return
426
+ response_id = previous_response_id.strip()
427
+ if not response_id:
428
+ return
429
+ _websocket_stale_previous_response_index.pop((response_id, api_key_id), None)
430
+
431
+
432
+ def _is_websocket_stale_previous_response(
433
+ *,
434
+ previous_response_id: str | None,
435
+ api_key_id: str | None,
436
+ ) -> bool:
437
+ if previous_response_id is None:
438
+ return False
439
+ response_id = previous_response_id.strip()
440
+ if not response_id:
441
+ return False
442
+ now = time.monotonic()
443
+ _prune_websocket_stale_previous_response_cache(now)
444
+ expires_at = _websocket_stale_previous_response_index.get((response_id, api_key_id))
445
+ if expires_at is None:
446
+ return False
447
+ if expires_at <= now:
448
+ _websocket_stale_previous_response_index.pop((response_id, api_key_id), None)
449
+ return False
450
+ return True
451
+
452
+
453
+ def _prepare_websocket_request_state_for_visible_output_replay(
454
+ request_state: "_WebSocketRequestState",
455
+ ) -> str | None:
456
+ # An identity captured by an earlier replay (or staged by the terminal
457
+ # capacity path before ``response_id`` was cleared) outlives this call, so
458
+ # a bounded extra replay keeps rewriting to the id the client is reading.
459
+ downstream_response_id = request_state.replay_downstream_response_id
460
+ if (
461
+ downstream_response_id is None
462
+ and request_state.response_id is not None
463
+ and not request_state.awaiting_response_created
464
+ ):
465
+ downstream_response_id = request_state.response_id
466
+ suppress_in_progress = request_state.suppress_next_in_progress_downstream or (
467
+ downstream_response_id is not None and request_state.response_event_count >= 2
468
+ )
469
+ fresh_request_text = request_state.fresh_upstream_request_text
470
+ if request_state.fresh_upstream_request_is_retry_safe and fresh_request_text:
471
+ # Only a proxy-injected anchor is the proxy's to release: its pin was
472
+ # derived from an anchor the client never asked for. A client-supplied
473
+ # anchor is the client's own continuation, so its fresh body keeps the
474
+ # owner pin and reconnects there (the capacity path re-sends such a
475
+ # turn to its owner; ``_websocket_auth_request_can_switch_account``
476
+ # treats it as account-bound).
477
+ _install_fresh_replay_body(
478
+ request_state,
479
+ fresh_request_text,
480
+ account_neutral=_websocket_request_text_is_account_neutral_fresh_replay(fresh_request_text),
481
+ release_owner_pin=(
482
+ request_state.previous_response_id is None or request_state.proxy_injected_previous_response_id
483
+ ),
484
+ )
485
+ request_text = request_state.request_text
486
+ if not isinstance(request_text, str):
487
+ return None
488
+ request_state.replay_count += 1
489
+ request_state.awaiting_response_created = True
490
+ request_state.response_id = None
491
+ request_state.response_event_count = 0
492
+ request_state.replay_downstream_response_id = downstream_response_id
493
+ request_state.suppress_next_created_downstream = downstream_response_id is not None
494
+ request_state.suppress_next_in_progress_downstream = suppress_in_progress
495
+ _clear_websocket_request_error_overrides(request_state)
496
+ return request_text
497
+
498
+
499
+ def _websocket_owner_switch_has_other_pending_requests(
500
+ request_state: "_WebSocketRequestState",
501
+ pending_requests: deque["_WebSocketRequestState"],
502
+ ) -> bool:
503
+ return any(pending is not request_state for pending in pending_requests)
504
+
505
+
506
+ def _websocket_request_text_is_account_neutral_fresh_replay(request_text: str | None) -> bool:
507
+ if not isinstance(request_text, str):
508
+ return False
509
+ try:
510
+ payload = json.loads(request_text)
511
+ except json.JSONDecodeError:
512
+ return False
513
+ if not isinstance(payload, dict):
514
+ return False
515
+ event_type = payload.get("type")
516
+ if event_type is not None and event_type != "response.create":
517
+ return False
518
+ payload.pop("type", None)
519
+ return responses_payload_is_account_neutral_fresh_replay(cast(dict[str, JsonValue], payload))
520
+
521
+
522
+ def _bind_websocket_request_dispatch_owner(
523
+ request_state: "_WebSocketRequestState",
524
+ *,
525
+ account_id: str,
526
+ exact_request_text: str,
527
+ ) -> bool:
528
+ required_account_id = request_state.replay_required_account_id
529
+ if _websocket_request_text_is_account_neutral_fresh_replay(exact_request_text):
530
+ return required_account_id is None or required_account_id == account_id
531
+ if required_account_id is not None and required_account_id != account_id:
532
+ return False
533
+ request_state.preferred_account_id = account_id
534
+ request_state.replay_required_account_id = account_id
535
+ return True
536
+
537
+
538
+ def _install_verified_fresh_replay(
539
+ request_state: "_WebSocketRequestState",
540
+ *,
541
+ require_proxy_injected_previous_response_id: bool = True,
542
+ require_account_neutral: bool = True,
543
+ ) -> str | None:
544
+ if not (request_state.fresh_upstream_request_is_retry_safe and request_state.fresh_upstream_request_text):
545
+ return None
546
+ if require_proxy_injected_previous_response_id and not request_state.proxy_injected_previous_response_id:
547
+ return None
548
+ fresh_request_text = request_state.fresh_upstream_request_text
549
+ account_neutral = _websocket_request_text_is_account_neutral_fresh_replay(fresh_request_text)
550
+ if require_account_neutral and not account_neutral:
551
+ return None
552
+ if not account_neutral and (request_state.replay_required_account_id or request_state.preferred_account_id) is None:
553
+ return None
554
+ return _install_fresh_replay_body(request_state, fresh_request_text, account_neutral=account_neutral)
555
+
556
+
557
+ def _install_fresh_replay_body(
558
+ request_state: "_WebSocketRequestState",
559
+ fresh_request_text: str,
560
+ *,
561
+ account_neutral: bool,
562
+ release_owner_pin: bool = True,
563
+ ) -> str:
564
+ """Swap the retained fresh body in and re-derive the owner requirement from it.
565
+
566
+ The anchored body pinned the request to the anchor's owner
567
+ (``replay_required_account_id`` / ``preferred_account_id``, bound at
568
+ dispatch and on connect). The fresh body carries no anchor: an
569
+ account-neutral one is free to move, a non-neutral one stays with the
570
+ owner that was already required or preferred. Every path that installs
571
+ the fresh body must reconcile the pin with it; a stale pin under a body
572
+ that turned neutral is what let an accepted transport-close replay exclude
573
+ the very account its reconnect still required.
574
+
575
+ A turn-state owner is a session pin, not a body pin. The session loop
576
+ re-resolves it before every reconnect and ``_connect_proxy_websocket``
577
+ hard-requires it (``turn_state_owner_required``) whenever the session
578
+ carries an owner, so the fresh body cannot release it: clearing it here
579
+ only hid the requirement from the exclusion decision, and the replay then
580
+ excluded the account its reconnect was about to require.
581
+
582
+ ``release_owner_pin=False`` keeps both pins untouched for a replay whose
583
+ anchor the proxy is not entitled to release (a client-supplied
584
+ ``previous_response_id``): the fresh body is still what goes upstream,
585
+ but it stays with the owner the anchored body was bound to.
586
+ """
587
+ replay_required_account_id = request_state.replay_required_account_id or request_state.preferred_account_id
588
+ turn_state_owner_account_id = (
589
+ request_state.preferred_account_id
590
+ if request_state.affinity_policy.codex_session_source == "turn_state"
591
+ else None
592
+ )
593
+ request_state.request_text = fresh_request_text
594
+ request_state.previous_response_id = None
595
+ if release_owner_pin:
596
+ request_state.preferred_account_id = turn_state_owner_account_id
597
+ request_state.replay_required_account_id = None if account_neutral else replay_required_account_id
598
+ request_state.proxy_injected_previous_response_id = False
599
+ request_state.fresh_upstream_request_is_retry_safe = False
600
+ request_state.responses_lite_model = request_state.fresh_upstream_request_responses_lite_model
601
+ _refresh_websocket_request_input_fingerprint_from_text(request_state)
602
+ return fresh_request_text
603
+
604
+
605
+ def _websocket_accepted_replay_can_switch_account(request_state: "_WebSocketRequestState") -> bool:
606
+ """Return whether an accepted replay may leave the account that accepted it.
607
+
608
+ Mirrors the owner requirements ``_connect_proxy_websocket`` enforces on
609
+ the reconnect: a bound replay owner, a file pin, an anchored owner, or a
610
+ turn-state owner keeps the replay on that account, so excluding it would
611
+ leave the reconnect no eligible account. Evaluate this after the replay
612
+ body has been prepared, once the anchor is stripped and the owner pin
613
+ reconciled with the body that will actually be sent; the turn-state owner
614
+ survives that reconciliation (``_install_fresh_replay_body``) because the
615
+ connect requires it regardless of the body. A turn-state session without
616
+ a resolved owner is free to move.
617
+ """
618
+ if request_state.replay_required_account_id is not None:
619
+ return False
620
+ if request_state.preferred_account_id is not None and (
621
+ request_state.previous_response_id is not None
622
+ or request_state.affinity_policy.codex_session_source == "turn_state"
623
+ ):
624
+ return False
625
+ return _websocket_auth_request_can_switch_account(request_state)
626
+
627
+
628
+ def _websocket_affinity_may_resolve_hard_owner(affinity_policy: _AffinityPolicy) -> bool:
629
+ """Return whether sticky selection may bind this request to one owner account.
630
+
631
+ The predicate is shared with the HTTP bridge accepted replay
632
+ (``_affinity_may_resolve_hard_owner``): a resolved hard ``CODEX_SESSION``
633
+ row -- turn-state ownership or the raw compatibility row consulted through
634
+ ``legacy_selection_key`` -- narrows selection to an owner the request state
635
+ never carries, so excluding that owner would leave every re-selection at
636
+ ``hard_affinity_saturated`` until the connect budget runs out.
637
+ """
638
+ return _affinity_may_resolve_hard_owner(affinity_policy)
639
+
640
+
641
+ def _websocket_accepted_replay_may_exclude_account(request_state: "_WebSocketRequestState") -> bool:
642
+ """Return whether an accepted replay may exclude the account that accepted it.
643
+
644
+ The exclusion is what moves an account-neutral accepted replay to another
645
+ account (bridge parity). It is refused when a request-state pin requires
646
+ the owner (``_websocket_accepted_replay_can_switch_account``) and when the
647
+ request's affinity may resolve to a hard owner the state does not carry
648
+ (``_websocket_affinity_may_resolve_hard_owner``). In both cases the replay
649
+ reconnects without an exclusion, as the created-only transport-close replay
650
+ did before accepted replays existed: sticky selection then re-resolves the
651
+ owner, or moves a soft row through the health penalty the failing account
652
+ receives. Only accepted lifecycles decide with this predicate; the
653
+ pre-created owner-switch branch keeps the pin-only predicate it already
654
+ used.
655
+ """
656
+ if _websocket_affinity_may_resolve_hard_owner(request_state.affinity_policy):
657
+ return False
658
+ return _websocket_accepted_replay_can_switch_account(request_state)
659
+
660
+
661
+ async def _record_or_defer_websocket_accepted_replay_health(
662
+ proxy: Any,
663
+ request_state: "_WebSocketRequestState",
664
+ *,
665
+ account: Account,
666
+ error_message: str | None,
667
+ error_code: str,
668
+ ) -> None:
669
+ """Penalize the account an accepted replay leaves, now or after settlement.
670
+
671
+ The accepted request keeps its API-key reservation open across the
672
+ re-send, and account health must not be written while a reservation is
673
+ unsettled (api-keys spec settlement-ordering invariant; bridge parity with
674
+ ``_handle_or_defer_precreated_stream_health``). A keyed request queues the
675
+ classified penalty on its state; ``_finalize_websocket_request_state`` and
676
+ ``_release_websocket_request_state_reservation`` drain it once the
677
+ reservation settles or is released. Unkeyed requests write immediately.
678
+ ``account_health_error_handled`` is deliberately not set: the staged
679
+ terminal is never finalized on this surface, so a later terminal belongs to
680
+ the replacement attempt and earns its own penalty.
681
+ """
682
+ error: UpstreamError = {"message": error_message or "Upstream error"}
683
+ if request_state.api_key_reservation is not None:
684
+ request_state.deferred_keyed_stream_health.append(
685
+ _DeferredKeyedStreamHealthPenalty(account=account, error=error, code=error_code)
686
+ )
687
+ return
688
+ await proxy._handle_stream_error(account, error, error_code)
689
+
690
+
691
+ def _prepare_websocket_request_state_for_account_switch(
692
+ request_state: "_WebSocketRequestState",
693
+ ) -> str | None:
694
+ """Return an unsent request body only when moving accounts is proven safe."""
695
+ if request_state.previous_response_id is None:
696
+ if not _websocket_request_text_is_account_neutral_fresh_replay(request_state.request_text):
697
+ return None
698
+ return request_state.request_text
699
+ return _install_verified_fresh_replay(request_state)
700
+
701
+
702
+ def _retire_websocket_continuity_anchor(continuity_state: _WebSocketContinuityState) -> None:
703
+ """Drop the completed-response anchor and the state that only exists for it."""
704
+ continuity_state.last_completed_response_id = None
705
+ continuity_state.last_completed_input_count = 0
706
+ continuity_state.last_completed_input_prefix_fingerprint = None
707
+ continuity_state.last_pending_function_call_ids = []
708
+ continuity_state.last_pending_tool_call_types = {}
709
+
710
+
711
+ def _websocket_continuity_anchor_for_payload(
712
+ continuity_state: _WebSocketContinuityState | None,
713
+ *,
714
+ responses_payload: ResponsesRequest,
715
+ codex_session_affinity: bool,
716
+ api_key_id: str | None = None,
717
+ ) -> _WebSocketContinuityAnchor | None:
718
+ """Select a matching session anchor, retiring any known upstream rejection."""
719
+ if continuity_state is None or not codex_session_affinity:
720
+ return None
721
+ if responses_payload.previous_response_id is not None:
722
+ return None
723
+ previous_response_id = continuity_state.last_completed_response_id
724
+ if previous_response_id is None:
725
+ return None
726
+ if _is_websocket_stale_previous_response(previous_response_id=previous_response_id, api_key_id=api_key_id):
727
+ # Upstream already denied this anchor (``previous_response_not_found``)
728
+ # and the fail-closed path remembered it. Injecting it again would
729
+ # fail the client's retry identically (#1921): retire it from session
730
+ # continuity so the full-context resend goes unanchored, and so the
731
+ # same id cannot return once the negative cache entry expires.
732
+ _retire_websocket_continuity_anchor(continuity_state)
733
+ _facade().logger.info(
734
+ "websocket_session_anchor_retired response_id=%s reason=stale_previous_response",
735
+ previous_response_id,
736
+ )
737
+ return None
738
+ stored_count = continuity_state.last_completed_input_count
739
+ if not _facade()._input_prefix_matches_stored_context(
740
+ responses_payload.input,
741
+ stored_count=stored_count,
742
+ stored_fingerprint=continuity_state.last_completed_input_prefix_fingerprint,
743
+ ):
744
+ return None
745
+ return _WebSocketContinuityAnchor(
746
+ previous_response_id=previous_response_id,
747
+ stored_input_item_count=stored_count,
748
+ )
749
+
750
+
751
+ _WEBSOCKET_TOOL_CALL_ITEM_TYPES_BY_OUTPUT_TYPE = {
752
+ "function_call_output": "function_call",
753
+ "custom_tool_call_output": "custom_tool_call",
754
+ "apply_patch_call_output": "apply_patch_call",
755
+ }
756
+ _WEBSOCKET_TOOL_CALL_ITEM_TYPES = frozenset(_WEBSOCKET_TOOL_CALL_ITEM_TYPES_BY_OUTPUT_TYPE.values())
757
+
758
+
759
+ def _websocket_input_items_are_self_contained_fresh_replay(input_items: list[JsonValue]) -> bool:
760
+ seen_call_ids_by_type: dict[str, set[str]] = {item_type: set() for item_type in _WEBSOCKET_TOOL_CALL_ITEM_TYPES}
761
+ for item in input_items:
762
+ if not isinstance(item, dict):
763
+ continue
764
+ item_type = _websocket_input_item_type(item)
765
+ call_id_value = item.get("call_id")
766
+ call_id = call_id_value if isinstance(call_id_value, str) and call_id_value else None
767
+ if item_type in _WEBSOCKET_TOOL_CALL_ITEM_TYPES:
768
+ if call_id is not None:
769
+ seen_call_ids_by_type[item_type].add(call_id)
770
+ continue
771
+ call_item_type = _WEBSOCKET_TOOL_CALL_ITEM_TYPES_BY_OUTPUT_TYPE.get(item_type or "")
772
+ if call_item_type is None:
773
+ continue
774
+ if call_id is None or call_id not in seen_call_ids_by_type[call_item_type]:
775
+ return False
776
+ return True
777
+
778
+
779
+ def _websocket_client_previous_response_full_resend_is_retry_safe(
780
+ *,
781
+ previous_response_id: str | None,
782
+ input_value: JsonValue,
783
+ continuity_state: _WebSocketContinuityState | None,
784
+ ) -> bool:
785
+ if previous_response_id is None or not isinstance(input_value, list):
786
+ return False
787
+ input_items = cast(list[JsonValue], input_value)
788
+ if len(input_items) <= 1:
789
+ return False
790
+ if not _websocket_input_items_are_self_contained_fresh_replay(input_items):
791
+ return False
792
+ if (
793
+ continuity_state is not None
794
+ and continuity_state.last_completed_response_id == previous_response_id
795
+ and (
796
+ continuity_state.last_completed_input_count > 0
797
+ or continuity_state.last_completed_input_prefix_fingerprint is not None
798
+ )
799
+ ):
800
+ return _facade()._input_prefix_matches_stored_context(
801
+ input_value,
802
+ stored_count=continuity_state.last_completed_input_count,
803
+ stored_fingerprint=continuity_state.last_completed_input_prefix_fingerprint,
804
+ )
805
+ return True
806
+
807
+
808
+ def _record_websocket_continuity_completion(
809
+ continuity_state: _WebSocketContinuityState,
810
+ *,
811
+ request_state: _WebSocketRequestState,
812
+ response_id: str | None,
813
+ ) -> None:
814
+ """Record completed context and pending tools, or clear an absent response anchor."""
815
+ if response_id is None:
816
+ _retire_websocket_continuity_anchor(continuity_state)
817
+ return
818
+ # Record the completed response id and pending tool-call metadata
819
+ # regardless of input shape (string inputs leave ``input_item_count`` at
820
+ # 0), so an anchored follow-up can still match continuity and receive
821
+ # synthetic interrupted tool outputs. Prefix anchoring/trimming is only
822
+ # meaningful for fingerprinted list inputs, so the count/fingerprint pair
823
+ # is cleared rather than left stale when the completed turn cannot
824
+ # provide one.
825
+ continuity_state.last_completed_response_id = response_id
826
+ if request_state.input_item_count > 0 and request_state.input_full_fingerprint is not None:
827
+ continuity_state.last_completed_input_count = request_state.input_item_count
828
+ continuity_state.last_completed_input_prefix_fingerprint = request_state.input_full_fingerprint
829
+ else:
830
+ continuity_state.last_completed_input_count = 0
831
+ continuity_state.last_completed_input_prefix_fingerprint = None
832
+ continuity_state.last_pending_function_call_ids = list(request_state.pending_function_call_ids)
833
+ continuity_state.last_pending_tool_call_types = dict(request_state.pending_tool_call_types)
834
+
835
+
836
+ def _record_websocket_responses_lite_acceptance(
837
+ continuity_state: _WebSocketContinuityState,
838
+ *,
839
+ request_state: _WebSocketRequestState,
840
+ ) -> None:
841
+ # Codex can reuse an accepted ``generate=false`` prewarm response and send
842
+ # only an empty or user-only input delta next. That frame no longer carries
843
+ # ``additional_tools``, so the accepted prewarm must seed Lite continuity.
844
+ # Only Lite acceptances may update the single-slot state: a non-Lite
845
+ # acceptance must not clobber a previously accepted Lite continuity with
846
+ # ``None``. The accepted response id is recorded so trusted incremental
847
+ # continuity can require ``previous_response_id`` to reference it.
848
+ if request_state.responses_lite_model is None:
849
+ return
850
+ continuity_state.responses_lite_model = request_state.responses_lite_model
851
+ # Prefer the downstream-visible id: a suppressed-created replay keeps
852
+ # exposing the original response id to the client (every downstream event
853
+ # is rewritten to it), so the next incremental frame can only reference
854
+ # that visible id, never the hidden upstream replay id.
855
+ continuity_state.responses_lite_response_id = (
856
+ request_state.replay_downstream_response_id or request_state.response_id
857
+ )
858
+
859
+
860
+ def _websocket_response_id(
861
+ event: OpenAIEvent | None,
862
+ payload: dict[str, JsonValue] | None,
863
+ *,
864
+ routing: NativeWebSocketRoutingMetadata | None = None,
865
+ ) -> str | None:
866
+ if event is not None and event.response is not None and event.response.id:
867
+ return event.response.id
868
+ if routing is not None:
869
+ return routing.payload_response_id
870
+ if not isinstance(payload, dict):
871
+ return None
872
+ direct_response_id = payload.get("response_id")
873
+ if isinstance(direct_response_id, str):
874
+ stripped_direct_response_id = direct_response_id.strip()
875
+ if stripped_direct_response_id:
876
+ return stripped_direct_response_id
877
+ response = payload.get("response")
878
+ if not isinstance(response, dict):
879
+ return None
880
+ response_id = response.get("id")
881
+ if not isinstance(response_id, str):
882
+ return None
883
+ stripped = response_id.strip()
884
+ return stripped or None
885
+
886
+
887
+ def _websocket_downstream_response_id(request_state: "_WebSocketRequestState") -> str:
888
+ return request_state.replay_downstream_response_id or request_state.response_id or request_state.request_id
889
+
890
+
891
+ def _websocket_continuity_response_ids(
892
+ request_state: "_WebSocketRequestState",
893
+ upstream_response_id: str | None,
894
+ ) -> tuple[str, ...]:
895
+ response_ids: list[str] = []
896
+ for response_id in (upstream_response_id, request_state.replay_downstream_response_id):
897
+ if not isinstance(response_id, str):
898
+ continue
899
+ stripped = response_id.strip()
900
+ if stripped and stripped not in response_ids:
901
+ response_ids.append(stripped)
902
+ return tuple(response_ids)
903
+
904
+
905
+ def _rewrite_websocket_downstream_response_id(
906
+ payload: dict[str, JsonValue],
907
+ request_state: "_WebSocketRequestState",
908
+ ) -> dict[str, JsonValue]:
909
+ downstream_response_id = request_state.replay_downstream_response_id
910
+ if downstream_response_id is None:
911
+ return payload
912
+
913
+ direct_response_id = payload.get("response_id")
914
+ rewrite_direct = isinstance(direct_response_id, str) and direct_response_id != downstream_response_id
915
+ response = payload.get("response")
916
+ nested_response_id = response.get("id") if isinstance(response, dict) else None
917
+ rewrite_nested = isinstance(nested_response_id, str) and nested_response_id != downstream_response_id
918
+ if not rewrite_direct and not rewrite_nested:
919
+ # Identity fast-path contract: callers skip re-serialization when the
920
+ # original payload object comes back, so an already-aligned frame must
921
+ # not be copied into an equal-but-new dict.
922
+ return payload
923
+
924
+ rewritten = dict(payload)
925
+ if rewrite_direct:
926
+ rewritten["response_id"] = downstream_response_id
927
+ if rewrite_nested and isinstance(response, dict):
928
+ rewritten["response"] = {**response, "id": downstream_response_id}
929
+ return rewritten
930
+
931
+
932
+ def _websocket_event_error_code(event_type: str | None, payload: dict[str, JsonValue] | None) -> str | None:
933
+ error = _websocket_event_error_payload(event_type, payload)
934
+ if not isinstance(error, dict):
935
+ return None
936
+ code_value = error.get("code")
937
+ if not isinstance(code_value, str):
938
+ return None
939
+ stripped = code_value.strip()
940
+ return stripped or None
941
+
942
+
943
+ def _websocket_event_error_type(event_type: str | None, payload: dict[str, JsonValue] | None) -> str | None:
944
+ error = _websocket_event_error_payload(event_type, payload)
945
+ if not isinstance(error, dict):
946
+ return None
947
+ type_value = error.get("type")
948
+ if not isinstance(type_value, str):
949
+ return None
950
+ stripped = type_value.strip()
951
+ return stripped or None
952
+
953
+
954
+ def _websocket_event_error_param(
955
+ event_type: str | None,
956
+ payload: dict[str, JsonValue] | None,
957
+ ) -> OpenAIErrorParam | None:
958
+ error = _websocket_event_error_payload(event_type, payload)
959
+ if not isinstance(error, dict):
960
+ return None
961
+ return OpenAIErrorParam.from_mapping(cast(Mapping[str, JsonValue], error))
962
+
963
+
964
+ def _websocket_event_error_message(event_type: str | None, payload: dict[str, JsonValue] | None) -> str | None:
965
+ error = _websocket_event_error_payload(event_type, payload)
966
+ if not isinstance(error, dict):
967
+ return None
968
+ message_value = error.get("message")
969
+ if not isinstance(message_value, str):
970
+ return None
971
+ stripped = message_value.strip()
972
+ return stripped or None
973
+
974
+
975
+ def _websocket_precreated_retry_error_code(
976
+ request_state: _WebSocketRequestState | None,
977
+ *,
978
+ event_type: str | None,
979
+ payload: dict[str, JsonValue] | None,
980
+ has_other_pending_requests: bool,
981
+ ) -> str | None:
982
+ if request_state is None:
983
+ return None
984
+ if request_state.response_id is not None and not request_state.awaiting_response_created:
985
+ # An accepted response is only replayable under the output-free
986
+ # capacity rule; every pre-created classifier below refuses it.
987
+ return _websocket_accepted_capacity_retry_error_code(
988
+ request_state,
989
+ event_type=event_type,
990
+ error_code=_normalize_error_code(
991
+ _websocket_event_error_code(event_type, payload),
992
+ _websocket_event_error_type(event_type, payload),
993
+ ),
994
+ error_message=_websocket_event_error_message(event_type, payload),
995
+ payload_response_id=_websocket_response_id(None, payload),
996
+ payload=payload,
997
+ has_other_pending_requests=has_other_pending_requests,
998
+ )
999
+ if request_state.last_downstream_sequence_number is not None:
1000
+ return None
1001
+ if request_state.downstream_visible:
1002
+ return None
1003
+ if has_other_pending_requests:
1004
+ return None
1005
+ if request_state.response_id is not None:
1006
+ return None
1007
+ if request_state.response_event_count > 0:
1008
+ return None
1009
+ if not request_state.awaiting_response_created:
1010
+ return None
1011
+ if not request_state.request_text:
1012
+ return None
1013
+ if request_state.replay_count >= 1:
1014
+ return None
1015
+ if event_type not in {"error", "response.failed"}:
1016
+ return None
1017
+
1018
+ error_code = _normalize_error_code(
1019
+ _websocket_event_error_code(event_type, payload),
1020
+ _websocket_event_error_type(event_type, payload),
1021
+ )
1022
+ error_param = _websocket_event_error_param(event_type, payload)
1023
+ error_message = _websocket_event_error_message(event_type, payload)
1024
+ if _facade()._is_previous_response_not_found_error(
1025
+ code=error_code,
1026
+ param=error_param,
1027
+ message=error_message,
1028
+ ):
1029
+ return "stream_incomplete"
1030
+ if _facade()._is_missing_tool_output_error(
1031
+ code=error_code,
1032
+ param=error_param,
1033
+ message=error_message,
1034
+ ):
1035
+ return None
1036
+ if _is_account_model_unsupported_error(
1037
+ code=error_code,
1038
+ message=error_message,
1039
+ model=request_state.model,
1040
+ ):
1041
+ # Any recognized response id means upstream has accepted this request,
1042
+ # even when response.created was not observed on this socket. Do not
1043
+ # account-switch an error event that carries either response.id or a
1044
+ # top-level response_id as though it were still pre-created.
1045
+ if _websocket_response_id(None, payload) is not None:
1046
+ return None
1047
+ return _ACCOUNT_MODEL_UNSUPPORTED_ERROR_CODE
1048
+ if is_upstream_model_capacity_error(error_message):
1049
+ if error_code in {
1050
+ "rate_limit_exceeded",
1051
+ "usage_limit_reached",
1052
+ "insufficient_quota",
1053
+ "usage_not_included",
1054
+ "quota_exceeded",
1055
+ }:
1056
+ return error_code
1057
+ if _websocket_response_id(None, payload) is not None:
1058
+ return None
1059
+ return "server_is_overloaded"
1060
+ return _websocket_transparent_replay_error_code(error_code, error_message)
1061
+
1062
+
1063
+ def _websocket_precreated_replay_fallback_error(
1064
+ request_state: _WebSocketRequestState,
1065
+ ) -> tuple[int, OpenAIErrorEnvelope, str, str, str | None] | None:
1066
+ if request_state.precreated_replay_reason != _ACCOUNT_MODEL_UNSUPPORTED_ERROR_CODE:
1067
+ return None
1068
+ error_code = request_state.error_code_override or "invalid_request_error"
1069
+ error_message = request_state.error_message_override or "Upstream rejected the requested model for this account"
1070
+ error_type = request_state.error_type_override or "invalid_request_error"
1071
+ payload = openai_error(error_code, error_message, error_type=error_type)
1072
+ error_param_override = coerce_error_param(request_state.error_param_override)
1073
+ if error_param_override.present:
1074
+ public_param = normalize_public_error_param(error_param_override)
1075
+ if public_param is not None:
1076
+ payload["error"]["param"] = public_param
1077
+ return (
1078
+ request_state.error_http_status_override or 400,
1079
+ payload,
1080
+ error_code,
1081
+ error_message,
1082
+ request_state.precreated_replay_account_id,
1083
+ )
1084
+
1085
+
1086
+ def _websocket_precreated_auth_error_code(
1087
+ request_state: _WebSocketRequestState | None,
1088
+ *,
1089
+ event_type: str | None,
1090
+ payload: dict[str, JsonValue] | None,
1091
+ has_other_pending_requests: bool,
1092
+ ) -> str | None:
1093
+ if request_state is None:
1094
+ return None
1095
+ if request_state.last_downstream_sequence_number is not None:
1096
+ return None
1097
+ if has_other_pending_requests:
1098
+ return None
1099
+ if request_state.response_id is not None:
1100
+ return None
1101
+ if request_state.response_event_count > 0:
1102
+ return None
1103
+ if not request_state.awaiting_response_created:
1104
+ return None
1105
+ if not request_state.request_text:
1106
+ return None
1107
+ if request_state.downstream_visible:
1108
+ return None
1109
+ if event_type not in {"error", "response.failed"}:
1110
+ return None
1111
+ if _websocket_response_id(None, payload) is not None:
1112
+ return None
1113
+
1114
+ error_code = _normalize_error_code(
1115
+ _websocket_event_error_code(event_type, payload),
1116
+ _websocket_event_error_type(event_type, payload),
1117
+ )
1118
+ error_type = _websocket_event_error_type(event_type, payload)
1119
+ if error_code in _facade()._WEBSOCKET_AUTH_FAILURE_CODES or error_type == "authentication_error":
1120
+ return "invalid_api_key"
1121
+ return None
1122
+
1123
+
1124
+ def _websocket_auth_failure_requires_reauth(message: str | None) -> bool:
1125
+ if not isinstance(message, str):
1126
+ return False
1127
+ lowered = message.lower()
1128
+ return any(marker in lowered for marker in _facade()._WEBSOCKET_REAUTH_REQUIRED_MESSAGE_MARKERS)
1129
+
1130
+
1131
+ def _websocket_auth_failure_permanent_code(message: str | None) -> str:
1132
+ if _websocket_auth_failure_requires_reauth(message):
1133
+ return _facade()._WEBSOCKET_SESSION_EXPIRED_FAILURE_CODE
1134
+ return _facade()._WEBSOCKET_AUTH_INVALIDATED_FAILURE_CODE
1135
+
1136
+
1137
+ def _websocket_fresh_request_blocks_account_switch(request_state: _WebSocketRequestState) -> bool:
1138
+ try:
1139
+ fresh_payload = json.loads(request_state.fresh_upstream_request_text or "null")
1140
+ except (TypeError, json.JSONDecodeError):
1141
+ return True
1142
+ if not isinstance(fresh_payload, dict):
1143
+ return True
1144
+ fresh_input = fresh_payload.get("input") if isinstance(fresh_payload, dict) else None
1145
+ return bool(extract_input_file_ids(fresh_input))
1146
+
1147
+
1148
+ def _websocket_auth_request_can_switch_account(request_state: _WebSocketRequestState) -> bool:
1149
+ if request_state.file_required_preferred_account:
1150
+ return False
1151
+ if request_state.previous_response_id is None:
1152
+ return request_state.request_text is None or _websocket_request_text_is_account_neutral_fresh_replay(
1153
+ request_state.request_text
1154
+ )
1155
+ if not (
1156
+ request_state.proxy_injected_previous_response_id
1157
+ and request_state.fresh_upstream_request_is_retry_safe
1158
+ and request_state.fresh_upstream_request_text
1159
+ ):
1160
+ return False
1161
+ return _websocket_request_text_is_account_neutral_fresh_replay(
1162
+ request_state.fresh_upstream_request_text
1163
+ ) and not _websocket_fresh_request_blocks_account_switch(request_state)
1164
+
1165
+
1166
+ def _prepare_websocket_request_state_for_auth_replay(
1167
+ request_state: _WebSocketRequestState,
1168
+ *,
1169
+ current_account_id: str | None = None,
1170
+ ) -> str | None:
1171
+ if request_state.verified_stale_anchor_replay:
1172
+ return None
1173
+ if request_state.last_downstream_sequence_number is not None:
1174
+ return None
1175
+ can_switch_account = _websocket_auth_request_can_switch_account(request_state)
1176
+ can_retry_bound_owner = (
1177
+ request_state.auth_replay_count == 0
1178
+ and current_account_id is not None
1179
+ and request_state.replay_required_account_id == current_account_id
1180
+ and isinstance(request_state.request_text, str)
1181
+ )
1182
+ if not can_switch_account and not can_retry_bound_owner:
1183
+ return None
1184
+ if can_switch_account and (
1185
+ request_state.proxy_injected_previous_response_id
1186
+ and request_state.fresh_upstream_request_is_retry_safe
1187
+ and request_state.fresh_upstream_request_text
1188
+ ):
1189
+ if _install_verified_fresh_replay(request_state) is None:
1190
+ return None
1191
+ request_text = request_state.request_text
1192
+ if not isinstance(request_text, str):
1193
+ return None
1194
+ request_state.replay_count += 1
1195
+ request_state.auth_replay_count += 1
1196
+ request_state.awaiting_response_created = True
1197
+ request_state.response_id = None
1198
+ request_state.response_event_count = 0
1199
+ _clear_websocket_request_error_overrides(request_state)
1200
+ return request_text
1201
+
1202
+
1203
+ def _websocket_owner_pinned_quota_error_code(
1204
+ request_state: _WebSocketRequestState | None,
1205
+ *,
1206
+ event_type: str | None,
1207
+ payload: dict[str, JsonValue] | None,
1208
+ ) -> str | None:
1209
+ if request_state is None:
1210
+ return None
1211
+ if request_state.previous_response_id is None or request_state.preferred_account_id is None:
1212
+ return None
1213
+ if request_state.response_id is not None:
1214
+ return None
1215
+ if not request_state.awaiting_response_created:
1216
+ return None
1217
+ if not request_state.request_text:
1218
+ return None
1219
+ if event_type not in {"error", "response.failed"}:
1220
+ return None
1221
+
1222
+ error_code = _normalize_error_code(
1223
+ _websocket_event_error_code(event_type, payload),
1224
+ _websocket_event_error_type(event_type, payload),
1225
+ )
1226
+ error_message = _websocket_event_error_message(event_type, payload)
1227
+ if is_upstream_model_capacity_error(error_message):
1228
+ if error_code in {
1229
+ "rate_limit_exceeded",
1230
+ "usage_limit_reached",
1231
+ "insufficient_quota",
1232
+ "usage_not_included",
1233
+ "quota_exceeded",
1234
+ }:
1235
+ return error_code
1236
+ if _websocket_response_id(None, payload) is not None:
1237
+ return None
1238
+ return "server_is_overloaded"
1239
+ return _websocket_transparent_replay_error_code(error_code, error_message)
1240
+
1241
+
1242
+ def _websocket_transparent_replay_error_code(error_code: str, error_message: str | None) -> str | None:
1243
+ """The code this terminal frame may be replayed under, or ``None`` to surface it.
1244
+
1245
+ The code set cannot answer for the serialized usage-limit rejection, which
1246
+ upstream sends with no error code at all: that frame normalizes to
1247
+ ``upstream_error``, which the set does not contain, so the turn is surfaced
1248
+ on a spent account while the identical coded frame is replayed elsewhere.
1249
+ A frame whose sentence proves the usage limit answers under the usage-limit
1250
+ code, so every later question -- whether an owner-pinned turn may move, what
1251
+ the account's health write records -- gets the same answer for both forms
1252
+ upstream chooses between.
1253
+ """
1254
+ if error_code in _facade()._WEBSOCKET_TRANSPARENT_REPLAY_ERROR_CODES:
1255
+ return error_code
1256
+ if is_upstream_usage_limit_rejection(error_code=error_code, message=error_message):
1257
+ return USAGE_LIMIT_REACHED
1258
+ return None
1259
+
1260
+
1261
+ async def _pop_replayable_precreated_websocket_request_state(
1262
+ pending_requests: deque[_WebSocketRequestState],
1263
+ *,
1264
+ pending_lock: anyio.Lock,
1265
+ replay_refusal_reasons: list[str] | None = None,
1266
+ ) -> _WebSocketRequestState | None:
1267
+ async with pending_lock:
1268
+ pending_count = len(pending_requests)
1269
+ if pending_count != 1:
1270
+ if replay_refusal_reasons is not None:
1271
+ replay_refusal_reasons.append(
1272
+ "multiple_pending_requests" if pending_count > 1 else "no_pending_requests"
1273
+ )
1274
+ if any(request_state.last_downstream_sequence_number is not None for request_state in pending_requests):
1275
+ replay_refusal_reasons.append("sequenced_downstream_frame")
1276
+ return None
1277
+ request_state = pending_requests[0]
1278
+ if not _websocket_request_can_replay_before_visible_output(request_state):
1279
+ if replay_refusal_reasons is not None and request_state.last_downstream_sequence_number is not None:
1280
+ replay_refusal_reasons.append("sequenced_downstream_frame")
1281
+ return None
1282
+ pending_requests.popleft()
1283
+ if _prepare_websocket_request_state_for_visible_output_replay(request_state) is None:
1284
+ return None
1285
+ return request_state
1286
+
1287
+
1288
+ async def _websocket_full_resend_conflicts_with_visible_pending(
1289
+ request_state: _WebSocketRequestState,
1290
+ pending_requests: deque[_WebSocketRequestState],
1291
+ *,
1292
+ pending_lock: anyio.Lock,
1293
+ codex_session_affinity: bool,
1294
+ ) -> bool:
1295
+ if (
1296
+ not codex_session_affinity
1297
+ or request_state.previous_response_id is not None
1298
+ or request_state.input_item_count < _WEBSOCKET_FULL_REPLAY_WAIT_MIN_ITEMS
1299
+ ):
1300
+ return False
1301
+ async with pending_lock:
1302
+ return any(pending is not request_state and pending.downstream_visible for pending in pending_requests)
1303
+
1304
+
1305
+ def _refresh_websocket_request_input_fingerprint_from_text(request_state: _WebSocketRequestState) -> None:
1306
+ if not request_state.request_text:
1307
+ request_state.input_item_count = 0
1308
+ request_state.input_full_fingerprint = None
1309
+ return
1310
+ try:
1311
+ payload = json.loads(request_state.request_text)
1312
+ except json.JSONDecodeError:
1313
+ request_state.input_item_count = 0
1314
+ request_state.input_full_fingerprint = None
1315
+ return
1316
+ if not isinstance(payload, dict):
1317
+ request_state.input_item_count = 0
1318
+ request_state.input_full_fingerprint = None
1319
+ return
1320
+ input_items = payload.get("input")
1321
+ if not isinstance(input_items, list):
1322
+ request_state.input_item_count = 0
1323
+ request_state.input_full_fingerprint = None
1324
+ return
1325
+ request_state.input_item_count = len(input_items)
1326
+ request_state.input_full_fingerprint = _facade()._fingerprint_input_items(cast(list[JsonValue], input_items))
1327
+
1328
+
1329
+ def _websocket_top_level_error_payload(payload: dict[str, JsonValue]) -> dict[str, JsonValue] | None:
1330
+ if payload.get("type") != "error":
1331
+ return None
1332
+ error: dict[str, JsonValue] = {}
1333
+ for error_field in ("code", "message"):
1334
+ value = payload.get(error_field)
1335
+ if isinstance(value, str) and value.strip():
1336
+ error[error_field] = value.strip()
1337
+ if "param" in payload:
1338
+ error["param"] = payload["param"]
1339
+ error_type = payload.get("error_type")
1340
+ if isinstance(error_type, str) and error_type.strip():
1341
+ error["type"] = error_type.strip()
1342
+ for metadata_field in ("plan_type", "resets_at", "resets_in_seconds"):
1343
+ value = payload.get(metadata_field)
1344
+ if isinstance(value, str | int | float) and not isinstance(value, bool):
1345
+ error[metadata_field] = value
1346
+ return error or None
1347
+
1348
+
1349
+ def _websocket_event_error_payload(
1350
+ event_type: str | None,
1351
+ payload: dict[str, JsonValue] | None,
1352
+ ) -> dict[str, JsonValue] | None:
1353
+ if not isinstance(payload, dict):
1354
+ return None
1355
+ if event_type == "error":
1356
+ error = payload.get("error")
1357
+ if isinstance(error, dict):
1358
+ return error
1359
+ # The ChatGPT-backed Codex websocket can emit OpenAI-style error
1360
+ # details directly on the event frame instead of under an ``error``
1361
+ # envelope. Normalize those fields into an error-detail object so the
1362
+ # event discriminator ``type: error`` is not mistaken for the upstream
1363
+ # error type.
1364
+ return _websocket_top_level_error_payload(payload)
1365
+ if event_type == "response.failed":
1366
+ response = payload.get("response")
1367
+ error = response.get("error") if isinstance(response, dict) else None
1368
+ return error if isinstance(error, dict) else None
1369
+ return None
1370
+
1371
+
1372
+ def _sanitize_public_websocket_event_payload(
1373
+ payload: dict[str, JsonValue],
1374
+ *,
1375
+ event_type: str | None,
1376
+ ) -> dict[str, JsonValue]:
1377
+ """Copy a terminal event with only a client-safe ``error.param``."""
1378
+
1379
+ normalized = payload
1380
+ if event_type in {"error", "response.failed"}:
1381
+ error_value = payload.get("error")
1382
+ if isinstance(error_value, dict):
1383
+ sanitized_error = sanitize_public_error_detail(error_value)
1384
+ if sanitized_error != error_value:
1385
+ normalized = dict(normalized)
1386
+ normalized["error"] = sanitized_error
1387
+ if "param" in payload:
1388
+ sanitized = dict(normalized)
1389
+ public_param = normalize_public_error_param(OpenAIErrorParam(True, payload["param"]))
1390
+ if public_param is None:
1391
+ sanitized.pop("param", None)
1392
+ else:
1393
+ sanitized["param"] = public_param
1394
+ if sanitized != normalized:
1395
+ normalized = sanitized
1396
+
1397
+ response_value = payload.get("response")
1398
+ if isinstance(response_value, dict):
1399
+ error_value = response_value.get("error")
1400
+ if isinstance(error_value, dict):
1401
+ sanitized_error = sanitize_public_error_detail(error_value)
1402
+ if sanitized_error != error_value:
1403
+ normalized_response = dict(response_value)
1404
+ normalized_response["error"] = sanitized_error
1405
+ normalized = dict(normalized)
1406
+ normalized["response"] = normalized_response
1407
+ return normalized
1408
+
1409
+
1410
+ def _websocket_event_incomplete_reason(
1411
+ event_type: str | None,
1412
+ payload: dict[str, JsonValue] | None,
1413
+ ) -> str | None:
1414
+ if event_type != "response.incomplete" or not isinstance(payload, dict):
1415
+ return None
1416
+ response = payload.get("response")
1417
+ if not isinstance(response, dict):
1418
+ return None
1419
+ incomplete_details = response.get("incomplete_details")
1420
+ if not isinstance(incomplete_details, dict):
1421
+ return None
1422
+ reason = incomplete_details.get("reason")
1423
+ if not isinstance(reason, str):
1424
+ return None
1425
+ stripped = reason.strip()
1426
+ return stripped or None
1427
+
1428
+
1429
+ def _maybe_rewrite_websocket_previous_response_not_found_event(
1430
+ *,
1431
+ request_state: _WebSocketRequestState,
1432
+ event: OpenAIEvent | None,
1433
+ payload: dict[str, JsonValue] | None,
1434
+ event_type: str | None,
1435
+ upstream_control: _WebSocketUpstreamControl,
1436
+ original_text: str,
1437
+ ) -> tuple[OpenAIEvent | None, dict[str, JsonValue] | None, str | None, str]:
1438
+ error_code = _normalize_error_code(
1439
+ _websocket_event_error_code(event_type, payload),
1440
+ _websocket_event_error_type(event_type, payload),
1441
+ )
1442
+ error_param = _websocket_event_error_param(event_type, payload)
1443
+ error_message = _websocket_event_error_message(event_type, payload)
1444
+ should_rewrite = _facade()._is_previous_response_not_found_error(
1445
+ code=error_code,
1446
+ param=error_param,
1447
+ message=error_message,
1448
+ )
1449
+ reason = "previous_response_not_found"
1450
+ if not should_rewrite and _facade()._is_previous_response_not_found_public_shape(
1451
+ code=error_code,
1452
+ param=error_param,
1453
+ message=error_message,
1454
+ ):
1455
+ # Mask malformed stale-anchor metadata without allowing replay.
1456
+ should_rewrite = True
1457
+ reason = PREVIOUS_RESPONSE_MALFORMED_PARAM_REASON
1458
+ request_state.previous_response_not_found_recovery_blocked = True
1459
+ if not should_rewrite:
1460
+ if request_state.previous_response_id is None:
1461
+ return event, payload, event_type, original_text
1462
+ should_rewrite = _facade()._is_missing_tool_output_error(
1463
+ code=error_code,
1464
+ param=error_param,
1465
+ message=error_message,
1466
+ )
1467
+ reason = "missing_tool_output"
1468
+ if not should_rewrite:
1469
+ return event, payload, event_type, original_text
1470
+
1471
+ reconnect_requested = reason != PREVIOUS_RESPONSE_MALFORMED_PARAM_REASON and (
1472
+ reason == "missing_tool_output" or request_state.preferred_account_id is not None
1473
+ )
1474
+ return _rewrite_websocket_continuity_corruption_event(
1475
+ request_state=request_state,
1476
+ upstream_control=upstream_control,
1477
+ reason=reason,
1478
+ reconnect_requested=reconnect_requested,
1479
+ original_text=original_text,
1480
+ )
1481
+
1482
+
1483
+ def _websocket_continuity_error_fields(
1484
+ *,
1485
+ reason: str,
1486
+ expose_stale_previous_response_classifier: bool,
1487
+ ) -> tuple[str, str]:
1488
+ if reason == "previous_response_not_found" and expose_stale_previous_response_classifier:
1489
+ return PREVIOUS_RESPONSE_NOT_FOUND_CODE, PREVIOUS_RESPONSE_NOT_FOUND_MESSAGE
1490
+ return "stream_incomplete", PREVIOUS_RESPONSE_STREAM_INCOMPLETE_MESSAGE
1491
+
1492
+
1493
+ @dataclass(frozen=True, slots=True)
1494
+ class _WebSocketStaleAnchorDiagnostics:
1495
+ previous_response_source: str
1496
+ fresh_replay_available: bool
1497
+ owner_lookup_source: str | None
1498
+ owner_lookup_outcome: str | None
1499
+ previous_response_age_seconds: int | None
1500
+ same_session: bool | None
1501
+
1502
+
1503
+ def _websocket_previous_response_source(request_state: _WebSocketRequestState) -> str:
1504
+ if request_state.previous_response_id is None:
1505
+ return "unknown"
1506
+ if request_state.proxy_injected_previous_response_id:
1507
+ return "proxy_injected"
1508
+ return "client_supplied"
1509
+
1510
+
1511
+ def _websocket_same_session_for_previous_response(
1512
+ request_state: _WebSocketRequestState,
1513
+ ) -> bool | None:
1514
+ request_session_id = request_state.session_id.strip() if isinstance(request_state.session_id, str) else ""
1515
+ owner_session_id = (
1516
+ request_state.previous_response_owner_session_id.strip()
1517
+ if isinstance(request_state.previous_response_owner_session_id, str)
1518
+ else ""
1519
+ )
1520
+ if not request_session_id or not owner_session_id:
1521
+ return None
1522
+ return request_session_id == owner_session_id
1523
+
1524
+
1525
+ def _websocket_previous_response_age_seconds(request_state: _WebSocketRequestState) -> int | None:
1526
+ requested_at = request_state.previous_response_owner_requested_at
1527
+ if requested_at is None:
1528
+ return None
1529
+ return max(0, int((utcnow() - to_utc_naive(requested_at)).total_seconds()))
1530
+
1531
+
1532
+ def _websocket_stale_anchor_diagnostics(
1533
+ request_state: _WebSocketRequestState,
1534
+ ) -> _WebSocketStaleAnchorDiagnostics:
1535
+ return _WebSocketStaleAnchorDiagnostics(
1536
+ previous_response_source=_websocket_previous_response_source(request_state),
1537
+ fresh_replay_available=bool(
1538
+ request_state.fresh_upstream_request_is_retry_safe and request_state.fresh_upstream_request_text is not None
1539
+ ),
1540
+ owner_lookup_source=request_state.previous_response_owner_lookup_source,
1541
+ owner_lookup_outcome=request_state.previous_response_owner_lookup_outcome,
1542
+ previous_response_age_seconds=_websocket_previous_response_age_seconds(request_state),
1543
+ same_session=_websocket_same_session_for_previous_response(request_state),
1544
+ )
1545
+
1546
+
1547
+ def _websocket_stale_anchor_failure_detail(
1548
+ diagnostics: _WebSocketStaleAnchorDiagnostics,
1549
+ ) -> str:
1550
+ owner_lookup_source = diagnostics.owner_lookup_source or "unknown"
1551
+ owner_lookup_outcome = diagnostics.owner_lookup_outcome or "unknown"
1552
+ previous_response_age_seconds = (
1553
+ str(diagnostics.previous_response_age_seconds)
1554
+ if diagnostics.previous_response_age_seconds is not None
1555
+ else "unknown"
1556
+ )
1557
+ same_session = str(diagnostics.same_session).lower() if diagnostics.same_session is not None else "unknown"
1558
+ return (
1559
+ "previous_response_not_found "
1560
+ f"previous_response_source={diagnostics.previous_response_source} "
1561
+ f"fresh_replay_available={str(diagnostics.fresh_replay_available).lower()} "
1562
+ f"owner_lookup_source={owner_lookup_source} "
1563
+ f"owner_lookup_outcome={owner_lookup_outcome} "
1564
+ f"previous_response_age_seconds={previous_response_age_seconds} "
1565
+ f"same_session={same_session}"
1566
+ )
1567
+
1568
+
1569
+ def _record_websocket_stale_anchor_failure(
1570
+ request_state: _WebSocketRequestState,
1571
+ *,
1572
+ surface: str,
1573
+ upstream_error_code: str,
1574
+ ) -> None:
1575
+ diagnostics = _websocket_stale_anchor_diagnostics(request_state)
1576
+ request_state.failure_phase_override = "upstream"
1577
+ request_state.failure_detail_override = _websocket_stale_anchor_failure_detail(diagnostics)
1578
+ request_state.upstream_error_code_override = upstream_error_code
1579
+ if not diagnostics.fresh_replay_available:
1580
+ _remember_websocket_stale_previous_response(
1581
+ previous_response_id=request_state.previous_response_id,
1582
+ api_key_id=request_state.api_key.id if request_state.api_key is not None else None,
1583
+ )
1584
+ _record_continuity_fail_closed(
1585
+ surface=surface,
1586
+ reason="previous_response_not_found",
1587
+ previous_response_id=request_state.previous_response_id,
1588
+ session_id=request_state.session_id,
1589
+ upstream_error_code=upstream_error_code,
1590
+ previous_response_source=diagnostics.previous_response_source,
1591
+ fresh_replay_available=diagnostics.fresh_replay_available,
1592
+ owner_lookup_source=diagnostics.owner_lookup_source,
1593
+ owner_lookup_outcome=diagnostics.owner_lookup_outcome,
1594
+ previous_response_age_seconds=diagnostics.previous_response_age_seconds,
1595
+ same_session=diagnostics.same_session,
1596
+ )
1597
+
1598
+
1599
+ def _rewrite_websocket_continuity_corruption_event(
1600
+ *,
1601
+ request_state: _WebSocketRequestState,
1602
+ upstream_control: _WebSocketUpstreamControl,
1603
+ reason: str,
1604
+ reconnect_requested: bool,
1605
+ original_text: str,
1606
+ ) -> tuple[OpenAIEvent | None, dict[str, JsonValue] | None, str | None, str]:
1607
+ del original_text
1608
+ if reconnect_requested:
1609
+ upstream_control.reconnect_requested = True
1610
+ if reason == "previous_response_not_found":
1611
+ _record_websocket_stale_anchor_failure(
1612
+ request_state,
1613
+ surface="websocket_stream",
1614
+ upstream_error_code="previous_response_not_found",
1615
+ )
1616
+ else:
1617
+ _record_continuity_fail_closed(
1618
+ surface="websocket_stream",
1619
+ reason=reason,
1620
+ previous_response_id=request_state.previous_response_id,
1621
+ session_id=request_state.session_id,
1622
+ upstream_error_code=(
1623
+ PREVIOUS_RESPONSE_NOT_FOUND_CODE if reason == PREVIOUS_RESPONSE_MALFORMED_PARAM_REASON else None
1624
+ ),
1625
+ )
1626
+ rewritten_code, rewritten_message = _websocket_continuity_error_fields(
1627
+ reason=reason,
1628
+ expose_stale_previous_response_classifier=request_state.expose_stale_previous_response_classifier,
1629
+ )
1630
+ rewritten_event_payload = response_failed_event(
1631
+ rewritten_code,
1632
+ rewritten_message,
1633
+ error_type="server_error",
1634
+ response_id=_websocket_downstream_response_id(request_state),
1635
+ )
1636
+ rewritten_text = json.dumps(rewritten_event_payload, ensure_ascii=True, separators=(",", ":"))
1637
+ rewritten_event_block = format_sse_event(rewritten_event_payload)
1638
+ rewritten_payload = parse_sse_data_json(rewritten_event_block)
1639
+ rewritten_event = parse_sse_event(rewritten_event_block)
1640
+ rewritten_event_type = _event_type_from_payload(rewritten_event, rewritten_payload)
1641
+ return rewritten_event, rewritten_payload, rewritten_event_type, rewritten_text
1642
+
1643
+
1644
+ def _rewrite_websocket_previous_response_owner_unavailable_event(
1645
+ *,
1646
+ request_state: _WebSocketRequestState,
1647
+ ) -> tuple[OpenAIEvent | None, dict[str, JsonValue] | None, str | None, str]:
1648
+ _record_continuity_fail_closed(
1649
+ surface="websocket_stream",
1650
+ reason="owner_account_unavailable",
1651
+ previous_response_id=request_state.previous_response_id,
1652
+ session_id=request_state.session_id,
1653
+ )
1654
+ rewritten_event_payload = response_failed_event(
1655
+ "upstream_unavailable",
1656
+ "Previous response owner account is unavailable; retry later.",
1657
+ error_type="server_error",
1658
+ response_id=_websocket_downstream_response_id(request_state),
1659
+ )
1660
+ rewritten_text = json.dumps(rewritten_event_payload, ensure_ascii=True, separators=(",", ":"))
1661
+ rewritten_event_block = format_sse_event(rewritten_event_payload)
1662
+ rewritten_payload = parse_sse_data_json(rewritten_event_block)
1663
+ rewritten_event = parse_sse_event(rewritten_event_block)
1664
+ rewritten_event_type = _event_type_from_payload(rewritten_event, rewritten_payload)
1665
+ return rewritten_event, rewritten_payload, rewritten_event_type, rewritten_text
1666
+
1667
+
1668
+ def _rewrite_websocket_suppressed_duplicate_tool_call_completion_event(
1669
+ *,
1670
+ request_state: _WebSocketRequestState,
1671
+ ) -> tuple[OpenAIEvent | None, dict[str, JsonValue] | None, str | None, str]:
1672
+ rewritten_event_payload = response_failed_event(
1673
+ _facade()._SUPPRESSED_DUPLICATE_TOOL_CALL_ERROR_CODE,
1674
+ _facade()._SUPPRESSED_DUPLICATE_TOOL_CALL_MESSAGE,
1675
+ error_type="server_error",
1676
+ response_id=_websocket_downstream_response_id(request_state),
1677
+ )
1678
+ rewritten_text = json.dumps(rewritten_event_payload, ensure_ascii=True, separators=(",", ":"))
1679
+ rewritten_event_block = format_sse_event(rewritten_event_payload)
1680
+ rewritten_payload = parse_sse_data_json(rewritten_event_block)
1681
+ rewritten_event = parse_sse_event(rewritten_event_block)
1682
+ rewritten_event_type = _event_type_from_payload(rewritten_event, rewritten_payload)
1683
+ return rewritten_event, rewritten_payload, rewritten_event_type, rewritten_text
1684
+
1685
+
1686
+ def _sanitize_websocket_connect_failure(
1687
+ *,
1688
+ request_state: _WebSocketRequestState,
1689
+ status_code: int,
1690
+ payload: OpenAIErrorEnvelope,
1691
+ error_code: str,
1692
+ error_message: str,
1693
+ ) -> tuple[int, OpenAIErrorEnvelope, str, str]:
1694
+ return _sanitize_websocket_previous_response_error(
1695
+ previous_response_id=request_state.previous_response_id,
1696
+ session_id=request_state.session_id,
1697
+ status_code=status_code,
1698
+ payload=payload,
1699
+ error_code=error_code,
1700
+ error_message=error_message,
1701
+ surface="websocket_connect",
1702
+ expose_stale_previous_response_classifier=request_state.expose_stale_previous_response_classifier,
1703
+ request_state=request_state,
1704
+ )
1705
+
1706
+
1707
+ def _sanitize_websocket_previous_response_error(
1708
+ *,
1709
+ previous_response_id: str | None,
1710
+ session_id: str | None,
1711
+ status_code: int,
1712
+ payload: OpenAIErrorEnvelope,
1713
+ error_code: str,
1714
+ error_message: str,
1715
+ surface: str,
1716
+ expose_stale_previous_response_classifier: bool = False,
1717
+ request_state: _WebSocketRequestState | None = None,
1718
+ ) -> tuple[int, OpenAIErrorEnvelope, str, str]:
1719
+ if previous_response_id is None:
1720
+ return status_code, payload, error_code, error_message
1721
+ parsed_error = _parse_openai_error(payload)
1722
+ normalized_code = _normalize_error_code(
1723
+ parsed_error.code if parsed_error else error_code,
1724
+ parsed_error.type if parsed_error else None,
1725
+ )
1726
+ normalized_message = parsed_error.message if parsed_error and parsed_error.message else error_message
1727
+ reason = "previous_response_not_found"
1728
+ should_rewrite = _facade()._is_previous_response_not_found_error(
1729
+ code=normalized_code,
1730
+ param=parsed_error.param_state if parsed_error else OpenAIErrorParam.absent(),
1731
+ message=normalized_message,
1732
+ )
1733
+ if not should_rewrite:
1734
+ should_rewrite = _facade()._is_missing_tool_output_error(
1735
+ code=normalized_code,
1736
+ param=parsed_error.param_state if parsed_error else OpenAIErrorParam.absent(),
1737
+ message=normalized_message,
1738
+ )
1739
+ reason = "missing_tool_output"
1740
+ if not should_rewrite and _facade()._is_previous_response_not_found_public_shape(
1741
+ code=normalized_code,
1742
+ param=parsed_error.param_state if parsed_error else OpenAIErrorParam.absent(),
1743
+ message=normalized_message,
1744
+ ):
1745
+ should_rewrite = True
1746
+ reason = PREVIOUS_RESPONSE_MALFORMED_PARAM_REASON
1747
+ if request_state is not None:
1748
+ request_state.previous_response_not_found_recovery_blocked = True
1749
+ if not should_rewrite:
1750
+ return status_code, payload, error_code, error_message
1751
+
1752
+ rewritten_code, rewritten_message = _websocket_continuity_error_fields(
1753
+ reason=reason,
1754
+ expose_stale_previous_response_classifier=expose_stale_previous_response_classifier,
1755
+ )
1756
+ if request_state is not None and reason == "previous_response_not_found":
1757
+ _record_websocket_stale_anchor_failure(
1758
+ request_state,
1759
+ surface=surface,
1760
+ upstream_error_code=normalized_code,
1761
+ )
1762
+ else:
1763
+ _record_continuity_fail_closed(
1764
+ surface=surface,
1765
+ reason=reason,
1766
+ previous_response_id=previous_response_id,
1767
+ session_id=session_id,
1768
+ upstream_error_code=normalized_code,
1769
+ )
1770
+ return (
1771
+ 502,
1772
+ openai_error(
1773
+ rewritten_code,
1774
+ rewritten_message,
1775
+ error_type="server_error",
1776
+ ),
1777
+ rewritten_code,
1778
+ rewritten_message,
1779
+ )
1780
+
1781
+
1782
+ def _sanitize_websocket_terminal_error_fields(
1783
+ *,
1784
+ request_state: _WebSocketRequestState,
1785
+ error_code: str,
1786
+ error_message: str,
1787
+ error_type: str,
1788
+ error_param: OpenAIErrorParam | JsonValue | None,
1789
+ ) -> tuple[str, str, str, OpenAIErrorParam | None]:
1790
+ error_param_state = coerce_error_param(error_param)
1791
+ if request_state.websocket_terminal_error_fields_sanitized:
1792
+ return (
1793
+ error_code,
1794
+ error_message,
1795
+ error_type,
1796
+ error_param_state if error_param_state.present else None,
1797
+ )
1798
+ normalized_code = _normalize_error_code(error_code, error_type)
1799
+ recoverable = _facade()._is_previous_response_not_found_error(
1800
+ code=normalized_code,
1801
+ param=error_param_state,
1802
+ message=error_message,
1803
+ )
1804
+ if not recoverable and not _facade()._is_previous_response_not_found_public_shape(
1805
+ code=normalized_code,
1806
+ param=error_param_state,
1807
+ message=error_message,
1808
+ ):
1809
+ return error_code, error_message, error_type, error_param_state
1810
+ if recoverable:
1811
+ _record_websocket_stale_anchor_failure(
1812
+ request_state,
1813
+ surface="websocket_terminal",
1814
+ upstream_error_code=normalized_code,
1815
+ )
1816
+ else:
1817
+ request_state.previous_response_not_found_recovery_blocked = True
1818
+ _record_continuity_fail_closed(
1819
+ surface="websocket_terminal",
1820
+ reason=PREVIOUS_RESPONSE_MALFORMED_PARAM_REASON,
1821
+ previous_response_id=request_state.previous_response_id,
1822
+ session_id=request_state.session_id,
1823
+ upstream_error_code=normalized_code,
1824
+ )
1825
+ rewritten_code, rewritten_message = _websocket_continuity_error_fields(
1826
+ reason="previous_response_not_found" if recoverable else PREVIOUS_RESPONSE_MALFORMED_PARAM_REASON,
1827
+ expose_stale_previous_response_classifier=request_state.expose_stale_previous_response_classifier,
1828
+ )
1829
+ request_state.websocket_terminal_error_fields_sanitized = True
1830
+ return (
1831
+ rewritten_code,
1832
+ rewritten_message,
1833
+ "server_error",
1834
+ None,
1835
+ )
1836
+
1837
+
1838
+ def _find_websocket_request_state_by_response_id(
1839
+ pending_requests: deque[_WebSocketRequestState],
1840
+ response_id: str,
1841
+ ) -> _WebSocketRequestState | None:
1842
+ for request_state in pending_requests:
1843
+ if request_state.response_id == response_id:
1844
+ return request_state
1845
+ return None
1846
+
1847
+
1848
+ def _assign_websocket_response_id(
1849
+ pending_requests: deque[_WebSocketRequestState],
1850
+ response_id: str | None,
1851
+ ) -> _WebSocketRequestState | None:
1852
+ if response_id is None:
1853
+ return None
1854
+ existing = _find_websocket_request_state_by_response_id(pending_requests, response_id)
1855
+ if existing is not None:
1856
+ return existing
1857
+ for request_state in pending_requests:
1858
+ if request_state.response_id is None and _http_bridge_request_counts_against_queue(request_state):
1859
+ request_state.response_id = response_id
1860
+ return request_state
1861
+ for request_state in pending_requests:
1862
+ if request_state.response_id is None and request_state.draining_until_terminal:
1863
+ request_state.response_id = response_id
1864
+ return request_state
1865
+ for request_state in pending_requests:
1866
+ if request_state.response_id is None:
1867
+ request_state.response_id = response_id
1868
+ return request_state
1869
+ return None
1870
+
1871
+
1872
+ def _draining_websocket_request_states(
1873
+ pending_requests: deque[_WebSocketRequestState],
1874
+ ) -> list[_WebSocketRequestState]:
1875
+ return [request_state for request_state in pending_requests if request_state.draining_until_terminal]
1876
+
1877
+
1878
+ def _is_response_output_event(event_type: str | None) -> bool:
1879
+ """A ``response.*`` frame that is neither a terminal nor an error carries response output."""
1880
+ return (
1881
+ isinstance(event_type, str)
1882
+ and event_type.startswith("response.")
1883
+ and event_type not in {"response.completed", "response.failed", "response.incomplete"}
1884
+ )
1885
+
1886
+
1887
+ def _match_websocket_request_state_for_anonymous_event(
1888
+ pending_requests: deque[_WebSocketRequestState],
1889
+ *,
1890
+ prefer_previous_response_not_found: bool,
1891
+ previous_response_id_hint: str | None = None,
1892
+ error_message: str | None = None,
1893
+ allow_unanchored_previous_response_error: bool = False,
1894
+ prefer_draining_requests: bool = True,
1895
+ event_type: str | None = None,
1896
+ ) -> _WebSocketRequestState | None:
1897
+ if prefer_previous_response_not_found:
1898
+ return _match_websocket_request_state_for_previous_response_error(
1899
+ pending_requests,
1900
+ previous_response_id_hint=previous_response_id_hint,
1901
+ error_message=error_message,
1902
+ allow_unanchored_previous_response_error=allow_unanchored_previous_response_error,
1903
+ )
1904
+
1905
+ if _is_response_output_event(event_type):
1906
+ # Output frames carry no response id. On a pipelined socket they belong
1907
+ # to the one response upstream has already created, whether that request
1908
+ # is still visible or draining, never to a sibling still waiting for its
1909
+ # own response.created (issue #2350). Vendor telemetry such as
1910
+ # ``codex.rate_limits`` keeps the pre-created ownership below.
1911
+ created_requests = [
1912
+ request_state for request_state in pending_requests if request_state.response_id is not None
1913
+ ]
1914
+ if len(created_requests) == 1:
1915
+ return created_requests[0]
1916
+
1917
+ visible_requests = [
1918
+ request_state for request_state in pending_requests if _http_bridge_request_counts_against_queue(request_state)
1919
+ ]
1920
+ draining_requests = _draining_websocket_request_states(pending_requests)
1921
+ if prefer_draining_requests and draining_requests:
1922
+ unresolved_draining_requests = [
1923
+ request_state for request_state in draining_requests if request_state.response_id is None
1924
+ ]
1925
+ if len(unresolved_draining_requests) == 1:
1926
+ return unresolved_draining_requests[0]
1927
+ if not visible_requests:
1928
+ return draining_requests[0]
1929
+
1930
+ if len(visible_requests) == 1:
1931
+ return visible_requests[0]
1932
+
1933
+ unresolved_visible_requests = [
1934
+ request_state for request_state in visible_requests if request_state.response_id is None
1935
+ ]
1936
+ if len(unresolved_visible_requests) == 1:
1937
+ return unresolved_visible_requests[0]
1938
+
1939
+ if not visible_requests and draining_requests:
1940
+ unresolved_draining_requests = [
1941
+ request_state for request_state in draining_requests if request_state.response_id is None
1942
+ ]
1943
+ if len(unresolved_draining_requests) == 1:
1944
+ return unresolved_draining_requests[0]
1945
+ return draining_requests[0]
1946
+
1947
+ return None
1948
+
1949
+
1950
+ def _match_websocket_request_state_for_precreated_terminal_event(
1951
+ pending_requests: deque[_WebSocketRequestState],
1952
+ ) -> _WebSocketRequestState | None:
1953
+ unresolved_requests = [
1954
+ request_state
1955
+ for request_state in pending_requests
1956
+ if request_state.response_id is None and request_state.awaiting_response_created
1957
+ ]
1958
+ if len(unresolved_requests) == 1:
1959
+ return unresolved_requests[0]
1960
+ return None
1961
+
1962
+
1963
+ def _match_websocket_request_state_for_previous_response_error(
1964
+ pending_requests: deque[_WebSocketRequestState],
1965
+ *,
1966
+ previous_response_id_hint: str | None = None,
1967
+ error_message: str | None = None,
1968
+ allow_unanchored_previous_response_error: bool = False,
1969
+ ) -> _WebSocketRequestState | None:
1970
+ matching_requests = _matching_websocket_request_states_for_previous_response_error(
1971
+ pending_requests,
1972
+ previous_response_id_hint=previous_response_id_hint,
1973
+ error_message=error_message,
1974
+ allow_unanchored_previous_response_error=allow_unanchored_previous_response_error,
1975
+ )
1976
+ if len(matching_requests) == 1:
1977
+ return matching_requests[0]
1978
+ return None
1979
+
1980
+
1981
+ def _is_unanchored_previous_response_error_message(message: str | None) -> bool:
1982
+ if message is None:
1983
+ return False
1984
+ normalized = " ".join(message.casefold().replace("`", "").split()).removesuffix(".").rstrip()
1985
+ return normalized == "invalid previous_response_id"
1986
+
1987
+
1988
+ def _matching_websocket_request_states_for_previous_response_error(
1989
+ pending_requests: deque[_WebSocketRequestState],
1990
+ *,
1991
+ previous_response_id_hint: str | None = None,
1992
+ error_message: str | None = None,
1993
+ allow_unanchored_previous_response_error: bool = False,
1994
+ ) -> list[_WebSocketRequestState]:
1995
+ followup_requests = [
1996
+ request_state for request_state in pending_requests if request_state.previous_response_id is not None
1997
+ ]
1998
+ if not followup_requests:
1999
+ if allow_unanchored_previous_response_error and len(pending_requests) == 1:
2000
+ return [pending_requests[0]]
2001
+ return []
2002
+ if previous_response_id_hint is not None:
2003
+ matching_requests = [
2004
+ request_state
2005
+ for request_state in followup_requests
2006
+ if request_state.previous_response_id == previous_response_id_hint
2007
+ ]
2008
+ if matching_requests:
2009
+ return matching_requests
2010
+ if error_message is not None:
2011
+ matching_requests = [
2012
+ request_state
2013
+ for request_state in followup_requests
2014
+ if _facade()._message_mentions_previous_response_id(error_message, request_state.previous_response_id)
2015
+ ]
2016
+ if matching_requests:
2017
+ return matching_requests
2018
+ unresolved_followups = [request_state for request_state in followup_requests if request_state.response_id is None]
2019
+ if len(unresolved_followups) == 1:
2020
+ return unresolved_followups
2021
+ if len(unresolved_followups) > 1:
2022
+ unique_previous_response_ids = {
2023
+ request_state.previous_response_id
2024
+ for request_state in unresolved_followups
2025
+ if request_state.previous_response_id
2026
+ }
2027
+ if len(unique_previous_response_ids) == 1:
2028
+ return unresolved_followups
2029
+ if (
2030
+ allow_unanchored_previous_response_error
2031
+ and len(followup_requests) == 1
2032
+ and _is_unanchored_previous_response_error_message(error_message)
2033
+ ):
2034
+ # A post-created terminal error may omit both its response id and the
2035
+ # rejected anchor id. When exactly one follow-up is pending, its
2036
+ # previous-response ownership is the only safe correlation seam even
2037
+ # after ``response.created`` assigned a response id.
2038
+ return followup_requests
2039
+ return []
2040
+
2041
+
2042
+ def _matching_websocket_request_states_for_missing_tool_output_error(
2043
+ pending_requests: deque[_WebSocketRequestState],
2044
+ ) -> list[_WebSocketRequestState]:
2045
+ unresolved_followups = [
2046
+ request_state
2047
+ for request_state in pending_requests
2048
+ if request_state.response_id is None and request_state.previous_response_id is not None
2049
+ ]
2050
+ if len(unresolved_followups) <= 1:
2051
+ return unresolved_followups
2052
+ unique_previous_response_ids = {
2053
+ request_state.previous_response_id
2054
+ for request_state in unresolved_followups
2055
+ if request_state.previous_response_id
2056
+ }
2057
+ if len(unique_previous_response_ids) == 1:
2058
+ return unresolved_followups
2059
+ return []
2060
+
2061
+
2062
+ def _pop_matching_websocket_request_states(
2063
+ pending_requests: deque[_WebSocketRequestState],
2064
+ matching_requests: list[_WebSocketRequestState],
2065
+ ) -> list[_WebSocketRequestState]:
2066
+ popped_requests: list[_WebSocketRequestState] = []
2067
+ for request_state in matching_requests:
2068
+ try:
2069
+ pending_requests.remove(request_state)
2070
+ except ValueError:
2071
+ continue
2072
+ popped_requests.append(request_state)
2073
+ return popped_requests
2074
+
2075
+
2076
+ async def _release_websocket_response_create_gate(
2077
+ request_state: _WebSocketRequestState,
2078
+ response_create_gate: asyncio.Semaphore,
2079
+ *,
2080
+ scheduler: Scheduler,
2081
+ ) -> None:
2082
+ """Release every create owner; ``scheduler`` owns the deferred lease-release task."""
2083
+
2084
+ cancellation: asyncio.CancelledError | None = None
2085
+ account_response_create_lease = request_state.account_response_create_lease
2086
+ account_response_create_release = request_state.account_response_create_release
2087
+ request_state.account_response_create_lease = None
2088
+ request_state.account_response_create_release = None
2089
+ if request_state.response_create_admission is not None:
2090
+ request_state.response_create_admission.release()
2091
+ request_state.response_create_admission = None
2092
+ if account_response_create_lease is not None and account_response_create_release is not None:
2093
+ cancellation = await _await_cleanup_deferring_cancellation(
2094
+ account_response_create_release(account_response_create_lease),
2095
+ scheduler=scheduler,
2096
+ )
2097
+ request_state.awaiting_response_created = False
2098
+ request_state.response_create_gate = None
2099
+ if not request_state.response_create_gate_acquired:
2100
+ if cancellation is not None:
2101
+ raise cancellation
2102
+ return
2103
+ request_state.response_create_gate_acquired = False
2104
+ response_create_gate.release()
2105
+ if cancellation is not None:
2106
+ raise cancellation
2107
+
2108
+
2109
+ def _pop_terminal_websocket_request_state(
2110
+ pending_requests: deque[_WebSocketRequestState],
2111
+ *,
2112
+ response_id: str | None,
2113
+ fallback_request_state: _WebSocketRequestState | None,
2114
+ prefer_previous_response_not_found: bool = False,
2115
+ previous_response_id_hint: str | None = None,
2116
+ error_message: str | None = None,
2117
+ allow_unanchored_previous_response_error: bool = False,
2118
+ allow_precreated_terminal_fallback: bool = False,
2119
+ prefer_draining_requests: bool = True,
2120
+ ) -> _WebSocketRequestState | None:
2121
+ if response_id is not None:
2122
+ request_state = _find_websocket_request_state_by_response_id(pending_requests, response_id)
2123
+ if request_state is not None:
2124
+ pending_requests.remove(request_state)
2125
+ return request_state
2126
+ if fallback_request_state is not None and fallback_request_state in pending_requests:
2127
+ pending_requests.remove(fallback_request_state)
2128
+ return fallback_request_state
2129
+ if response_id is not None and allow_precreated_terminal_fallback:
2130
+ request_state = _match_websocket_request_state_for_precreated_terminal_event(pending_requests)
2131
+ if request_state is not None and request_state in pending_requests:
2132
+ pending_requests.remove(request_state)
2133
+ return request_state
2134
+ if response_id is not None and prefer_previous_response_not_found:
2135
+ request_state = _match_websocket_request_state_for_previous_response_error(
2136
+ pending_requests,
2137
+ previous_response_id_hint=previous_response_id_hint,
2138
+ error_message=error_message,
2139
+ allow_unanchored_previous_response_error=allow_unanchored_previous_response_error,
2140
+ )
2141
+ if request_state is not None and request_state in pending_requests:
2142
+ pending_requests.remove(request_state)
2143
+ return request_state
2144
+ if response_id is None:
2145
+ request_state = _match_websocket_request_state_for_anonymous_event(
2146
+ pending_requests,
2147
+ prefer_previous_response_not_found=prefer_previous_response_not_found,
2148
+ previous_response_id_hint=previous_response_id_hint,
2149
+ error_message=error_message,
2150
+ allow_unanchored_previous_response_error=allow_unanchored_previous_response_error,
2151
+ prefer_draining_requests=prefer_draining_requests,
2152
+ )
2153
+ if request_state is not None and request_state in pending_requests:
2154
+ pending_requests.remove(request_state)
2155
+ return request_state
2156
+ return None
2157
+
2158
+
2159
+ def _upstream_websocket_disconnect_message(message: UpstreamWebSocketMessage) -> str:
2160
+ if message.kind == "error" and message.error:
2161
+ return f"Upstream websocket closed before response.completed: {message.error}"
2162
+ if message.close_code is not None:
2163
+ return f"Upstream websocket closed before response.completed (close_code={message.close_code})"
2164
+ return "Upstream websocket closed before response.completed"
2165
+
2166
+
2167
+ def _websocket_receive_timeout_for_pending_requests(
2168
+ started_ats: Sequence[float],
2169
+ *,
2170
+ proxy_request_budget_seconds: float,
2171
+ stream_idle_timeout_seconds: float,
2172
+ now: float,
2173
+ ) -> _WebSocketReceiveTimeout | None:
2174
+ if not started_ats:
2175
+ return None
2176
+
2177
+ idle_timeout_seconds = max(0.001, stream_idle_timeout_seconds)
2178
+ oldest_started_at = min(started_ats)
2179
+ budget_deadline = oldest_started_at + proxy_request_budget_seconds
2180
+ # ``started_ats`` come from the owner's clock; ``now`` must too.
2181
+ remaining_budget = max(0.0, budget_deadline - now)
2182
+ idle_timeout_matches_request_budget = idle_timeout_seconds == max(0.001, proxy_request_budget_seconds)
2183
+
2184
+ if remaining_budget <= 0 and idle_timeout_matches_request_budget:
2185
+ return _WebSocketReceiveTimeout(
2186
+ timeout_seconds=0.0,
2187
+ error_code="stream_idle_timeout",
2188
+ error_message="Upstream stream idle timeout",
2189
+ )
2190
+ if idle_timeout_matches_request_budget and remaining_budget >= idle_timeout_seconds:
2191
+ return _WebSocketReceiveTimeout(
2192
+ timeout_seconds=remaining_budget,
2193
+ error_code="stream_idle_timeout",
2194
+ error_message="Upstream stream idle timeout",
2195
+ )
2196
+ if remaining_budget <= 0:
2197
+ return _WebSocketReceiveTimeout(
2198
+ timeout_seconds=0.0,
2199
+ error_code="upstream_request_timeout",
2200
+ error_message="Proxy request budget exhausted",
2201
+ )
2202
+ if idle_timeout_seconds < remaining_budget:
2203
+ return _WebSocketReceiveTimeout(
2204
+ timeout_seconds=idle_timeout_seconds,
2205
+ error_code="stream_idle_timeout",
2206
+ error_message="Upstream stream idle timeout",
2207
+ fail_all_pending=True,
2208
+ )
2209
+ return _WebSocketReceiveTimeout(
2210
+ timeout_seconds=remaining_budget,
2211
+ error_code="upstream_request_timeout",
2212
+ error_message="Proxy request budget exhausted",
2213
+ )
2214
+
2215
+
2216
+ class _WebSocketJsonObject(dict[str, JsonValue]):
2217
+ def __init__(self, pairs: list[tuple[str, JsonValue]]) -> None:
2218
+ super().__init__(pairs)
2219
+ self.raw_pairs = tuple(pairs)
2220
+
2221
+
2222
+ def _parse_websocket_payload(text: str) -> dict[str, JsonValue] | None:
2223
+ try:
2224
+ payload = json.loads(text, object_pairs_hook=_WebSocketJsonObject)
2225
+ except json.JSONDecodeError:
2226
+ return None
2227
+ if not isinstance(payload, dict):
2228
+ return None
2229
+ return payload
2230
+
2231
+
2232
+ def _websocket_capability_metadata_values(payload: dict[str, JsonValue]) -> tuple[JsonValue, ...] | None:
2233
+ normalized_name = CODEX_LB_REQUIRED_CAPABILITY_HEADER.lower()
2234
+ payload_pairs = payload.raw_pairs if isinstance(payload, _WebSocketJsonObject) else tuple(payload.items())
2235
+ misplaced_values = [value for key, value in payload_pairs if key.lower() == normalized_name]
2236
+ if misplaced_values:
2237
+ # The reserved per-frame carrier is valid only inside
2238
+ # ``client_metadata``. Surface a deliberately ambiguous carrier set so
2239
+ # the shared parser rejects any top-level placement before selection.
2240
+ return (misplaced_values[0], misplaced_values[0])
2241
+ if not isinstance(payload, _WebSocketJsonObject):
2242
+ return None
2243
+ metadata_objects = [value for key, value in payload.raw_pairs if key == "client_metadata"]
2244
+ values: list[JsonValue] = []
2245
+ for metadata in metadata_objects:
2246
+ if not isinstance(metadata, _WebSocketJsonObject):
2247
+ continue
2248
+ values.extend(value for key, value in metadata.raw_pairs if key.lower() == normalized_name)
2249
+ if len(metadata_objects) > 1 and values:
2250
+ # A duplicate top-level metadata container can otherwise erase the
2251
+ # sole marker through last-key-wins JSON decoding. Treat the carrier
2252
+ # as ambiguous so routing fails before selection.
2253
+ values.append(values[0])
2254
+ return tuple(values)
2255
+
2256
+
2257
+ def _is_websocket_response_create(payload: dict[str, JsonValue]) -> bool:
2258
+ payload_type = payload.get("type")
2259
+ return isinstance(payload_type, str) and payload_type == "response.create"
2260
+
2261
+
2262
+ def _app_error_to_websocket_event(exc: AppError) -> dict[str, JsonValue]:
2263
+ payload = openai_error(exc.code, exc.message, error_type=getattr(exc, "error_type", "server_error"))
2264
+ if exc.param is not None:
2265
+ payload["error"]["param"] = exc.param
2266
+ return _wrapped_websocket_error_event(
2267
+ exc.status_code,
2268
+ payload,
2269
+ )
2270
+
2271
+
2272
+ def _wrapped_websocket_error_event(
2273
+ status_code: int,
2274
+ payload: OpenAIErrorEnvelope,
2275
+ *,
2276
+ expose_stale_previous_response_classifier: bool = False,
2277
+ ) -> dict[str, JsonValue]:
2278
+ error = payload["error"]
2279
+ error_code = _normalize_error_code(
2280
+ error.get("code"),
2281
+ error.get("type"),
2282
+ )
2283
+ error_param = OpenAIErrorParam.from_mapping(cast(Mapping[str, JsonValue], error))
2284
+ error_message = error.get("message")
2285
+ if _facade()._is_previous_response_not_found_public_shape(
2286
+ code=error_code,
2287
+ param=error_param,
2288
+ message=error_message,
2289
+ ):
2290
+ status_code = 502
2291
+ # On the Codex-native route, the caller has already sanitized this to
2292
+ # the canonical code (see _sanitize_websocket_previous_response_error);
2293
+ # do not re-mask it back to stream_incomplete. Every other caller
2294
+ # (public /v1, or a raw error this function is seeing for the first
2295
+ # time) keeps the existing stream_incomplete safety net.
2296
+ expose = expose_stale_previous_response_classifier and _facade()._is_previous_response_not_found_error(
2297
+ code=error_code,
2298
+ param=error_param,
2299
+ message=error_message,
2300
+ )
2301
+ if not expose:
2302
+ payload = previous_response_stream_incomplete_error()
2303
+ error_payload = cast(
2304
+ JsonValue,
2305
+ sanitize_public_error_detail(cast(Mapping[str, JsonValue], payload["error"])),
2306
+ )
2307
+ event: dict[str, JsonValue] = {
2308
+ "type": "error",
2309
+ "status": status_code,
2310
+ "error": error_payload,
2311
+ }
2312
+ return event
2313
+
2314
+
2315
+ def _serialize_websocket_error_event(payload: dict[str, JsonValue]) -> str:
2316
+ return json.dumps(payload, ensure_ascii=True, separators=(",", ":"))
2317
+
2318
+
2319
+ def _trim_websocket_previous_response_input_items(input_items: list[JsonValue]) -> list[JsonValue]:
2320
+ first_output_index = next(
2321
+ (
2322
+ index
2323
+ for index, item in enumerate(input_items)
2324
+ if _websocket_input_item_type(item)
2325
+ in {"function_call_output", "custom_tool_call_output", "apply_patch_call_output"}
2326
+ ),
2327
+ None,
2328
+ )
2329
+ if first_output_index is None or first_output_index == 0:
2330
+ return input_items
2331
+ prefix = input_items[:first_output_index]
2332
+ if not all(_is_websocket_previous_response_output_item(item) for item in prefix):
2333
+ return input_items
2334
+ return input_items[first_output_index:]
2335
+
2336
+
2337
+ def _is_websocket_previous_response_output_item(item: JsonValue) -> bool:
2338
+ if isinstance(item, dict) and _websocket_input_item_type(item) is None and item.get("role") == "assistant":
2339
+ return True
2340
+ item_type = _websocket_input_item_type(item)
2341
+ if item_type in {"reasoning", "function_call", "custom_tool_call", "apply_patch_call"}:
2342
+ return True
2343
+ if item_type != "message" or not isinstance(item, dict):
2344
+ return False
2345
+ return item.get("role") == "assistant"
2346
+
2347
+
2348
+ def _websocket_input_item_type(item: JsonValue) -> str | None:
2349
+ if not isinstance(item, dict):
2350
+ return None
2351
+ item_type = item.get("type")
2352
+ return item_type if isinstance(item_type, str) else None
2353
+
2354
+
2355
+ def _websocket_connect_deadline(request_state: _WebSocketRequestState, budget_seconds: float, *, now: float) -> float:
2356
+ # ``now`` is the owner's clock sample: the fallback for a request state
2357
+ # without a start stamp must live in the same time domain as the budget
2358
+ # checks that later compare against this deadline.
2359
+ started_at = request_state.started_at if request_state.started_at > 0 else now
2360
+ return started_at + budget_seconds