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,1317 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ import json
5
+ import sys
6
+ from collections.abc import Callable
7
+ from copy import deepcopy
8
+ from typing import Any, AsyncIterator, Literal, Mapping, cast
9
+
10
+ from app.core.balancer import PERMANENT_FAILURE_CODES
11
+ from app.core.balancer.types import ClassifiedFailure, UpstreamError
12
+ from app.core.clients.files import create_file as core_create_file # noqa: F401
13
+ from app.core.clients.files import finalize_file as core_finalize_file # noqa: F401
14
+ from app.core.clients.http import lease_http_session as lease_http_session # noqa: F401
15
+ from app.core.clients.proxy import CodexControlRequestPrivacyPolicy as CodexControlRequestPrivacyPolicy
16
+ from app.core.clients.proxy import CodexControlResponse as CodexControlResponse
17
+ from app.core.clients.proxy import ( # noqa: F401 # noqa: F401
18
+ ImageFetchSession,
19
+ ProxyResponseError,
20
+ UpstreamProxyRouteTrace,
21
+ _agent_control_tool_output_occurrences,
22
+ _as_image_fetch_session,
23
+ _inline_content_images,
24
+ _inline_input_image_urls,
25
+ _ws_transport_payload_budget_bytes,
26
+ filter_inbound_headers,
27
+ pop_compact_timeout_overrides,
28
+ pop_stream_timeout_overrides,
29
+ pop_transcribe_timeout_overrides,
30
+ push_compact_timeout_overrides,
31
+ push_stream_timeout_overrides,
32
+ push_transcribe_timeout_overrides,
33
+ )
34
+ from app.core.clients.proxy import codex_control_request as core_codex_control_request # noqa: F401
35
+ from app.core.clients.proxy import compact_responses as core_compact_responses # noqa: F401
36
+ from app.core.clients.proxy import transcribe_audio as core_transcribe_audio # noqa: F401
37
+ from app.core.clients.proxy_websocket import (
38
+ UpstreamWebSocket,
39
+ )
40
+ from app.core.clock import Clock
41
+ from app.core.errors import (
42
+ PREVIOUS_RESPONSE_MALFORMED_PARAM_REASON,
43
+ PREVIOUS_RESPONSE_STREAM_INCOMPLETE_MESSAGE,
44
+ SYNTHETIC_TRANSPORT_FAILURE_CODES,
45
+ SYNTHETIC_TRANSPORT_FAILURE_MARKER,
46
+ OpenAIErrorParam,
47
+ openai_error,
48
+ response_failed_event,
49
+ synthetic_stream_failure_event,
50
+ synthetic_transport_failure_event,
51
+ )
52
+ from app.core.errors import (
53
+ PREVIOUS_RESPONSE_NOT_FOUND_CODE as PREVIOUS_RESPONSE_NOT_FOUND_CODE,
54
+ )
55
+ from app.core.errors import (
56
+ PREVIOUS_RESPONSE_NOT_FOUND_MESSAGE as PREVIOUS_RESPONSE_NOT_FOUND_MESSAGE,
57
+ )
58
+ from app.core.openai.models import OpenAIError, OpenAIEvent, OpenAIResponsePayload, ResponseUsage
59
+ from app.core.openai.parsing import classify_event_type, parse_sse_event
60
+ from app.core.openai.requests import ResponsesRequest
61
+ from app.core.resilience.network_recovery import (
62
+ PROCESS_NETWORK_UNAVAILABLE_CODE,
63
+ )
64
+ from app.core.types import JsonValue
65
+ from app.core.upstream_proxy import ResolvedUpstreamRoute, UpstreamProxyRouteError
66
+ from app.core.upstream_proxy.cache import get_upstream_route_cache
67
+ from app.core.utils.request_id import get_request_id
68
+ from app.core.utils.sse import CODEX_KEEPALIVE_FRAME as CODEX_KEEPALIVE_FRAME # noqa: F401
69
+ from app.core.utils.sse import ParsedSseBlock, format_sse_event, parse_sse_data_json
70
+ from app.core.utils.time import utcnow as utcnow
71
+ from app.db.models import (
72
+ Account,
73
+ AccountStatus, # noqa: F401
74
+ )
75
+ from app.modules.api_keys.service import ApiKeyData
76
+ from app.modules.proxy._load_balancer.overload_backoff import (
77
+ UPSTREAM_OVERLOAD_CODES,
78
+ UPSTREAM_SOFT_OVERLOAD_CODES,
79
+ record_upstream_burst_rejection,
80
+ record_upstream_overload,
81
+ )
82
+ from app.modules.proxy._service.api_key_usage import (
83
+ _API_KEY_RESERVATION_HEARTBEAT_SECONDS as _API_KEY_RESERVATION_HEARTBEAT_SECONDS,
84
+ )
85
+ from app.modules.proxy._service.compact import (
86
+ _service_tier_from_compact_payload as _service_tier_from_compact_payload,
87
+ )
88
+ from app.modules.proxy._service.compact import (
89
+ _sticky_key_for_compact_request as _sticky_key_for_compact_request,
90
+ )
91
+ from app.modules.proxy._service.compact import (
92
+ _sticky_key_from_compact_payload as _sticky_key_from_compact_payload,
93
+ )
94
+ from app.modules.proxy._service.http_bridge.helpers import (
95
+ _active_http_bridge_instance_ring as _active_http_bridge_instance_ring,
96
+ )
97
+ from app.modules.proxy._service.http_bridge.helpers import (
98
+ _build_http_bridge_prewarm_text as _build_http_bridge_prewarm_text,
99
+ )
100
+ from app.modules.proxy._service.http_bridge.helpers import (
101
+ _durable_bridge_lookup_active_owner as _durable_bridge_lookup_active_owner,
102
+ )
103
+ from app.modules.proxy._service.http_bridge.helpers import (
104
+ _durable_bridge_lookup_allows_local_reuse as _durable_bridge_lookup_allows_local_reuse,
105
+ )
106
+ from app.modules.proxy._service.http_bridge.helpers import (
107
+ _effective_http_bridge_idle_ttl_seconds as _effective_http_bridge_idle_ttl_seconds,
108
+ )
109
+ from app.modules.proxy._service.http_bridge.helpers import (
110
+ _forwarded_http_bridge_session_key as _forwarded_http_bridge_session_key,
111
+ )
112
+ from app.modules.proxy._service.http_bridge.helpers import (
113
+ _has_http_bridge_response_output_marker as _has_http_bridge_response_output_marker,
114
+ )
115
+ from app.modules.proxy._service.http_bridge.helpers import (
116
+ _http_bridge_allow_durable_takeover as _http_bridge_allow_durable_takeover,
117
+ )
118
+ from app.modules.proxy._service.http_bridge.helpers import (
119
+ _http_bridge_can_local_recover_without_ring as _http_bridge_can_local_recover_without_ring,
120
+ )
121
+ from app.modules.proxy._service.http_bridge.helpers import (
122
+ _http_bridge_can_recover_during_drain as _http_bridge_can_recover_during_drain,
123
+ )
124
+ from app.modules.proxy._service.http_bridge.helpers import (
125
+ _http_bridge_continuity_lost_error_envelope as _http_bridge_continuity_lost_error_envelope,
126
+ )
127
+ from app.modules.proxy._service.http_bridge.helpers import (
128
+ _http_bridge_durable_lease_ttl_seconds as _http_bridge_durable_lease_ttl_seconds,
129
+ )
130
+ from app.modules.proxy._service.http_bridge.helpers import (
131
+ _http_bridge_endpoint_matches_current_instance as _http_bridge_endpoint_matches_current_instance,
132
+ )
133
+ from app.modules.proxy._service.http_bridge.helpers import (
134
+ _http_bridge_eviction_priority as _http_bridge_eviction_priority,
135
+ )
136
+ from app.modules.proxy._service.http_bridge.helpers import (
137
+ _http_bridge_has_durable_recovery_anchor as _http_bridge_has_durable_recovery_anchor,
138
+ )
139
+ from app.modules.proxy._service.http_bridge.helpers import (
140
+ _http_bridge_input_item_type as _http_bridge_input_item_type,
141
+ )
142
+ from app.modules.proxy._service.http_bridge.helpers import (
143
+ _http_bridge_is_context_overflow_error as _http_bridge_is_context_overflow_error,
144
+ )
145
+ from app.modules.proxy._service.http_bridge.helpers import (
146
+ _http_bridge_is_previous_response_owner_unavailable as _http_bridge_is_previous_response_owner_unavailable,
147
+ )
148
+ from app.modules.proxy._service.http_bridge.helpers import (
149
+ _http_bridge_key_strength as _http_bridge_key_strength,
150
+ )
151
+ from app.modules.proxy._service.http_bridge.helpers import (
152
+ _http_bridge_owner_check_required as _http_bridge_owner_check_required,
153
+ )
154
+ from app.modules.proxy._service.http_bridge.helpers import (
155
+ _http_bridge_owner_instance as _http_bridge_owner_instance,
156
+ )
157
+ from app.modules.proxy._service.http_bridge.helpers import (
158
+ _http_bridge_owner_lookup_unavailable_error_envelope as _http_bridge_owner_lookup_unavailable_error_envelope,
159
+ )
160
+ from app.modules.proxy._service.http_bridge.helpers import (
161
+ _http_bridge_payload_looks_like_full_resend as _http_bridge_payload_looks_like_full_resend,
162
+ )
163
+ from app.modules.proxy._service.http_bridge.helpers import (
164
+ _http_bridge_payload_without_previous_response_id as _http_bridge_payload_without_previous_response_id,
165
+ )
166
+ from app.modules.proxy._service.http_bridge.helpers import (
167
+ _http_bridge_precreated_retry_failure_error as _http_bridge_precreated_retry_failure_error,
168
+ )
169
+ from app.modules.proxy._service.http_bridge.helpers import (
170
+ _http_bridge_previous_response_alias_key as _http_bridge_previous_response_alias_key,
171
+ )
172
+ from app.modules.proxy._service.http_bridge.helpers import (
173
+ _http_bridge_previous_response_error_envelope as _http_bridge_previous_response_error_envelope,
174
+ )
175
+ from app.modules.proxy._service.http_bridge.helpers import (
176
+ _http_bridge_request_counts_against_queue as _http_bridge_request_counts_against_queue,
177
+ )
178
+ from app.modules.proxy._service.http_bridge.helpers import (
179
+ _http_bridge_request_stage as _http_bridge_request_stage,
180
+ )
181
+ from app.modules.proxy._service.http_bridge.helpers import (
182
+ _http_bridge_requires_cluster_registration as _http_bridge_requires_cluster_registration,
183
+ )
184
+ from app.modules.proxy._service.http_bridge.helpers import (
185
+ _http_bridge_runtime_config as _http_bridge_runtime_config,
186
+ )
187
+ from app.modules.proxy._service.http_bridge.helpers import (
188
+ _http_bridge_session_allows_api_key as _http_bridge_session_allows_api_key,
189
+ )
190
+ from app.modules.proxy._service.http_bridge.helpers import (
191
+ _http_bridge_session_has_visible_requests as _http_bridge_session_has_visible_requests,
192
+ )
193
+ from app.modules.proxy._service.http_bridge.helpers import (
194
+ _http_bridge_session_matches_preferred_account as _http_bridge_session_matches_preferred_account,
195
+ )
196
+ from app.modules.proxy._service.http_bridge.helpers import (
197
+ _http_bridge_session_retiring_with_visible_requests as _http_bridge_session_retiring_with_visible_requests,
198
+ )
199
+ from app.modules.proxy._service.http_bridge.helpers import (
200
+ _http_bridge_session_reusable_for_request as _http_bridge_session_reusable_for_request,
201
+ )
202
+ from app.modules.proxy._service.http_bridge.helpers import (
203
+ _http_bridge_should_attempt_local_bootstrap_rebind as _http_bridge_should_attempt_local_bootstrap_rebind,
204
+ )
205
+ from app.modules.proxy._service.http_bridge.helpers import (
206
+ _http_bridge_should_attempt_local_previous_response_recovery, # noqa: F401
207
+ )
208
+ from app.modules.proxy._service.http_bridge.helpers import (
209
+ _http_bridge_should_attempt_soft_affinity_reroute as _http_bridge_should_attempt_soft_affinity_reroute,
210
+ )
211
+ from app.modules.proxy._service.http_bridge.helpers import (
212
+ _http_bridge_should_rollover_after_context_overflow as _http_bridge_should_rollover_after_context_overflow,
213
+ )
214
+ from app.modules.proxy._service.http_bridge.helpers import (
215
+ _http_bridge_should_wait_for_registration as _http_bridge_should_wait_for_registration,
216
+ )
217
+ from app.modules.proxy._service.http_bridge.helpers import (
218
+ _http_bridge_startup_wait_timeout_error as _http_bridge_startup_wait_timeout_error,
219
+ )
220
+ from app.modules.proxy._service.http_bridge.helpers import (
221
+ _http_bridge_turn_state_alias_key as _http_bridge_turn_state_alias_key,
222
+ )
223
+ from app.modules.proxy._service.http_bridge.helpers import (
224
+ _is_http_bridge_previous_response_output_item as _is_http_bridge_previous_response_output_item,
225
+ )
226
+ from app.modules.proxy._service.http_bridge.helpers import (
227
+ _is_missing_durable_bridge_table_error as _is_missing_durable_bridge_table_error,
228
+ )
229
+ from app.modules.proxy._service.http_bridge.helpers import (
230
+ _log_http_bridge_event as _log_http_bridge_event,
231
+ )
232
+ from app.modules.proxy._service.http_bridge.helpers import (
233
+ _log_http_bridge_startup_wait_timeout as _log_http_bridge_startup_wait_timeout,
234
+ )
235
+ from app.modules.proxy._service.http_bridge.helpers import (
236
+ _make_http_bridge_session_key as _make_http_bridge_session_key,
237
+ )
238
+ from app.modules.proxy._service.http_bridge.helpers import (
239
+ _normalize_http_bridge_error_event as _normalize_http_bridge_error_event,
240
+ )
241
+ from app.modules.proxy._service.http_bridge.helpers import (
242
+ _normalized_http_bridge_instance_ring as _normalized_http_bridge_instance_ring,
243
+ )
244
+ from app.modules.proxy._service.http_bridge.helpers import (
245
+ _preferred_http_bridge_reconnect_turn_state as _preferred_http_bridge_reconnect_turn_state,
246
+ )
247
+ from app.modules.proxy._service.http_bridge.helpers import (
248
+ _record_bridge_drain_recovery_allowed as _record_bridge_drain_recovery_allowed,
249
+ )
250
+ from app.modules.proxy._service.http_bridge.helpers import (
251
+ _record_bridge_first_turn_timeout as _record_bridge_first_turn_timeout,
252
+ )
253
+ from app.modules.proxy._service.http_bridge.helpers import (
254
+ _record_bridge_reattach as _record_bridge_reattach,
255
+ )
256
+ from app.modules.proxy._service.http_bridge.helpers import (
257
+ _trim_http_bridge_previous_response_input_items as _trim_http_bridge_previous_response_input_items,
258
+ )
259
+ from app.modules.proxy._service.observability import (
260
+ _hash_identifier as _hash_identifier,
261
+ )
262
+ from app.modules.proxy._service.observability import (
263
+ _hash_identifier_or_none as _hash_identifier_or_none,
264
+ )
265
+ from app.modules.proxy._service.observability import (
266
+ _interesting_header_keys as _interesting_header_keys,
267
+ )
268
+ from app.modules.proxy._service.observability import (
269
+ _is_reasoning_replay_rejection as _is_reasoning_replay_rejection,
270
+ )
271
+ from app.modules.proxy._service.observability import (
272
+ _maybe_log_proxy_request_payload as _maybe_log_proxy_request_payload,
273
+ )
274
+ from app.modules.proxy._service.observability import (
275
+ _maybe_log_proxy_request_shape as _maybe_log_proxy_request_shape,
276
+ )
277
+ from app.modules.proxy._service.observability import (
278
+ _maybe_log_proxy_service_tier_trace as _maybe_log_proxy_service_tier_trace,
279
+ )
280
+ from app.modules.proxy._service.observability import (
281
+ _observe_terminal_stream_error_frame as _observe_terminal_stream_error_frame,
282
+ )
283
+ from app.modules.proxy._service.observability import (
284
+ _record_continuity_fail_closed as _record_continuity_fail_closed,
285
+ )
286
+ from app.modules.proxy._service.observability import (
287
+ _record_continuity_owner_resolution as _record_continuity_owner_resolution,
288
+ )
289
+ from app.modules.proxy._service.observability import (
290
+ _record_upstream_reasoning_replay_rejection as _record_upstream_reasoning_replay_rejection,
291
+ )
292
+ from app.modules.proxy._service.observability import (
293
+ _summarize_input as _summarize_input,
294
+ )
295
+ from app.modules.proxy._service.observability import (
296
+ _tools_hash as _tools_hash,
297
+ )
298
+ from app.modules.proxy._service.observability import (
299
+ _truncate_identifier as _truncate_identifier,
300
+ )
301
+ from app.modules.proxy._service.streaming.protocol import _StreamingServiceProtocol
302
+ from app.modules.proxy._service.support import (
303
+ _HARD_HTTP_BRIDGE_AFFINITY_KINDS, # noqa: F401
304
+ _REQUEST_TRANSPORT_WEBSOCKET, # noqa: F401
305
+ _WEBSOCKET_FULL_REPLAY_WAIT_MIN_ITEMS, # noqa: F401
306
+ _WEBSOCKET_FULL_REPLAY_WAIT_POLL_SECONDS, # noqa: F401
307
+ _event_type_from_payload,
308
+ _RequestLogFailureMetadata,
309
+ _signal_propagated_capacity_startup_ready,
310
+ _StreamSettlement,
311
+ _WebSocketRequestState,
312
+ )
313
+ from app.modules.proxy._service.support import (
314
+ _HTTPBridgeOwnerForward as _HTTPBridgeOwnerForward,
315
+ )
316
+ from app.modules.proxy._service.support import (
317
+ _record_websocket_route_metadata as _record_websocket_route_metadata,
318
+ )
319
+ from app.modules.proxy._service.support import (
320
+ _websocket_route_log_kwargs as _websocket_route_log_kwargs,
321
+ )
322
+ from app.modules.proxy._service.warmup import (
323
+ WarmupExecutionData as WarmupExecutionData,
324
+ )
325
+ from app.modules.proxy._service.warmup import (
326
+ WarmupFailedAccountData as WarmupFailedAccountData,
327
+ )
328
+ from app.modules.proxy._service.warmup import (
329
+ WarmupSkippedAccountData as WarmupSkippedAccountData,
330
+ )
331
+ from app.modules.proxy._service.warmup import (
332
+ WarmupSubmittedAccountData as WarmupSubmittedAccountData,
333
+ )
334
+ from app.modules.proxy._service.warmup import (
335
+ _is_warmup_usage_eligible as _is_warmup_usage_eligible,
336
+ )
337
+ from app.modules.proxy._service.warmup import (
338
+ _materialize_warmup_account as _materialize_warmup_account,
339
+ )
340
+ from app.modules.proxy._service.warmup import (
341
+ _snapshot_warmup_account as _snapshot_warmup_account,
342
+ )
343
+ from app.modules.proxy._service.warmup import (
344
+ _WarmupAccountSnapshot as _WarmupAccountSnapshot,
345
+ )
346
+ from app.modules.proxy._service.warmup import (
347
+ _WarmupSubmitResult as _WarmupSubmitResult,
348
+ )
349
+ from app.modules.proxy._service.warmup import (
350
+ _WarmupUsageSnapshot as _WarmupUsageSnapshot,
351
+ )
352
+ from app.modules.proxy._service.websocket.helpers import (
353
+ _app_error_to_websocket_event, # noqa: F401
354
+ _assign_websocket_response_id, # noqa: F401
355
+ _draining_websocket_request_states, # noqa: F401
356
+ _find_websocket_request_state_by_response_id, # noqa: F401
357
+ _is_websocket_previous_response_output_item, # noqa: F401
358
+ _is_websocket_response_create, # noqa: F401
359
+ _match_websocket_request_state_for_anonymous_event, # noqa: F401
360
+ _match_websocket_request_state_for_precreated_terminal_event, # noqa: F401
361
+ _match_websocket_request_state_for_previous_response_error, # noqa: F401
362
+ _matching_websocket_request_states_for_missing_tool_output_error, # noqa: F401
363
+ _matching_websocket_request_states_for_previous_response_error, # noqa: F401
364
+ _maybe_rewrite_websocket_previous_response_not_found_event, # noqa: F401
365
+ _parse_websocket_payload, # noqa: F401
366
+ _pop_matching_websocket_request_states, # noqa: F401
367
+ _pop_replayable_precreated_websocket_request_state, # noqa: F401
368
+ _pop_terminal_websocket_request_state, # noqa: F401
369
+ _prepare_websocket_request_state_for_auth_replay, # noqa: F401
370
+ _prepare_websocket_request_state_for_visible_output_replay, # noqa: F401
371
+ _record_websocket_continuity_completion, # noqa: F401
372
+ _refresh_websocket_request_input_fingerprint_from_text, # noqa: F401
373
+ _release_websocket_response_create_gate, # noqa: F401
374
+ _rewrite_websocket_continuity_corruption_event, # noqa: F401
375
+ _rewrite_websocket_downstream_response_id, # noqa: F401
376
+ _rewrite_websocket_previous_response_owner_unavailable_event, # noqa: F401
377
+ _rewrite_websocket_suppressed_duplicate_tool_call_completion_event, # noqa: F401
378
+ _sanitize_websocket_connect_failure, # noqa: F401
379
+ _sanitize_websocket_previous_response_error, # noqa: F401
380
+ _sanitize_websocket_terminal_error_fields, # noqa: F401
381
+ _serialize_websocket_error_event, # noqa: F401
382
+ _trim_websocket_previous_response_input_items, # noqa: F401
383
+ _upstream_websocket_disconnect_message, # noqa: F401
384
+ _websocket_auth_failure_permanent_code, # noqa: F401
385
+ _websocket_auth_failure_requires_reauth, # noqa: F401
386
+ _websocket_auth_request_can_switch_account, # noqa: F401
387
+ _websocket_client_previous_response_full_resend_is_retry_safe, # noqa: F401
388
+ _websocket_connect_deadline, # noqa: F401
389
+ _websocket_continuity_anchor_for_payload, # noqa: F401
390
+ _websocket_continuity_error_fields, # noqa: F401
391
+ _websocket_continuity_response_ids, # noqa: F401
392
+ _websocket_downstream_response_id, # noqa: F401
393
+ _websocket_event_error_code, # noqa: F401
394
+ _websocket_event_error_message, # noqa: F401
395
+ _websocket_event_error_param, # noqa: F401
396
+ _websocket_event_error_payload, # noqa: F401
397
+ _websocket_event_error_type, # noqa: F401
398
+ _websocket_full_resend_conflicts_with_visible_pending, # noqa: F401
399
+ _websocket_input_item_type, # noqa: F401
400
+ _websocket_owner_pinned_quota_error_code, # noqa: F401
401
+ _websocket_precreated_auth_error_code, # noqa: F401
402
+ _websocket_precreated_retry_error_code, # noqa: F401
403
+ _websocket_receive_timeout_for_pending_requests, # noqa: F401
404
+ _websocket_response_id, # noqa: F401
405
+ _websocket_top_level_error_payload, # noqa: F401
406
+ _wrapped_websocket_error_event, # noqa: F401
407
+ )
408
+ from app.modules.proxy.affinity import (
409
+ _sticky_key_from_session_header, # noqa: F401
410
+ )
411
+ from app.modules.proxy.durable_bridge_coordinator import (
412
+ DurableBridgeLookup as DurableBridgeLookup,
413
+ )
414
+ from app.modules.proxy.helpers import (
415
+ _normalize_error_code,
416
+ classify_upstream_failure,
417
+ is_account_neutral_safety_policy_rejection,
418
+ is_model_scoped_upstream_rejection,
419
+ is_upstream_model_capacity_error,
420
+ is_upstream_usage_limit_rejection,
421
+ )
422
+ from app.modules.proxy.http_bridge_forwarding import (
423
+ HTTPBridgeForwardContext as HTTPBridgeForwardContext,
424
+ )
425
+ from app.modules.proxy.http_bridge_forwarding import (
426
+ OwnerForwardRelayFailure as OwnerForwardRelayFailure,
427
+ )
428
+ from app.modules.proxy.load_balancer import AccountSelection
429
+ from app.modules.usage.updater import UsageUpdater
430
+
431
+
432
+ def _facade() -> Any:
433
+ return sys.modules["app.modules.proxy.service"]
434
+
435
+
436
+ def _canonical_background_ack(
437
+ event_payload: dict[str, JsonValue] | None,
438
+ event_type: str | None,
439
+ ) -> tuple[str, OpenAIResponsePayload] | None:
440
+ if event_type not in {"response.queued", "response.in_progress"}:
441
+ return None
442
+ response = event_payload.get("response") if event_payload is not None else None
443
+ response_id = response.get("id") if isinstance(response, dict) else None
444
+ if not (
445
+ isinstance(response, dict)
446
+ and response.get("object") == "response"
447
+ and isinstance(response_id, str)
448
+ and bool(response_id)
449
+ and response_id == response_id.strip()
450
+ and response.get("status") == event_type.removeprefix("response.")
451
+ and response.get("output") == []
452
+ ):
453
+ return None
454
+ # The relayed payload model drops known submodels that fail validation
455
+ # instead of raising, so a malformed `usage` or `error` would otherwise be
456
+ # discarded silently on a response classified as successful.
457
+ payload = OpenAIResponsePayload.model_validate(response)
458
+ if (response.get("usage") is None) != (payload.usage is None):
459
+ return None
460
+ if (response.get("error") is None) != (payload.error is None):
461
+ return None
462
+ return response_id, payload
463
+
464
+
465
+ def _canonical_background_ack_response_id(
466
+ event_payload: dict[str, JsonValue] | None,
467
+ event_type: str | None,
468
+ ) -> str | None:
469
+ canonical_ack = _canonical_background_ack(event_payload, event_type)
470
+ return canonical_ack[0] if canonical_ack is not None else None
471
+
472
+
473
+ def _is_background_json_ack(
474
+ stream: bool | None,
475
+ event_payload: dict[str, JsonValue] | None,
476
+ event_type: str | None,
477
+ ) -> bool:
478
+ return stream is False and _canonical_background_ack_response_id(event_payload, event_type) is not None
479
+
480
+
481
+ def _publish_http_response_owner(
482
+ proxy: _StreamingServiceProtocol,
483
+ event: OpenAIEvent | None,
484
+ event_payload: dict[str, JsonValue] | None,
485
+ block: str,
486
+ account_id: str,
487
+ api_key: ApiKeyData | None,
488
+ session_id: str | None,
489
+ ) -> None:
490
+ if event is None or event.response is None or not event.response.id or event_payload is None:
491
+ return
492
+ if isinstance(block, ParsedSseBlock) and (block.is_local or block.response_id_is_local):
493
+ return
494
+ if event_payload.get(SYNTHETIC_TRANSPORT_FAILURE_MARKER):
495
+ return
496
+ proxy._remember_websocket_previous_response_owner(
497
+ previous_response_id=event.response.id,
498
+ api_key_id=api_key.id if api_key is not None else None,
499
+ account_id=account_id,
500
+ session_id=session_id,
501
+ )
502
+
503
+
504
+ def _settle_background_ack(
505
+ settlement: _StreamSettlement,
506
+ payload: ResponsesRequest,
507
+ event_payload: dict[str, JsonValue] | None,
508
+ response_id: str,
509
+ ) -> tuple[bool, str, ResponseUsage | None]:
510
+ """Treat a canonical background acknowledgement as the terminal event of a `stream: false` request."""
511
+ canonical_ack = (
512
+ _canonical_background_ack(event_payload, classify_event_type(event_payload))
513
+ if payload.stream is False
514
+ else None
515
+ )
516
+ if canonical_ack is None:
517
+ return False, response_id, None
518
+ ack_response_id, ack_payload = canonical_ack
519
+ settlement.response_id = ack_response_id
520
+ return True, ack_response_id, ack_payload.usage
521
+
522
+
523
+ def _stream_iterator_after_capacity_admission(
524
+ stream: AsyncIterator[str],
525
+ ) -> AsyncIterator[str]:
526
+ """Expose a slow stream once local response-create admission has succeeded."""
527
+ _signal_propagated_capacity_startup_ready()
528
+ return stream.__aiter__()
529
+
530
+
531
+ _REQUEST_TRANSPORT_HTTP = "http"
532
+
533
+
534
+ def _should_penalize_stream_error(code: str | None, message: str | None = None) -> bool:
535
+ """Whether this stream failure owes the account a health write.
536
+
537
+ ``message`` is the sentence the failure carried, and every caller that
538
+ reads an upstream-authored one passes it: the code table cannot answer for
539
+ the serialized usage-limit rejection, which upstream sends with no error
540
+ code at all. That frame normalizes to ``upstream_error``, which is in
541
+ neither code set, so an account that just said its subscription window is
542
+ spent would be left ACTIVE and handed the next request -- while the
543
+ identical coded frame benches it.
544
+
545
+ Callers whose message is proxy-authored rather than upstream's own (the
546
+ WebSocket terminal-cleanup path, whose codes and sentences are this
547
+ proxy's: ``client_disconnected``, ``stream_incomplete``, an idle timeout)
548
+ pass no message and keep the pure code answer, because there is nothing of
549
+ upstream's there to read.
550
+ """
551
+ if code is None:
552
+ return False
553
+ should_penalize = (
554
+ code in _facade()._ACCOUNT_RECOVERY_RETRY_CODES
555
+ or code in _facade()._TRANSIENT_RETRY_CODES
556
+ or is_upstream_usage_limit_rejection(error_code=code, message=message)
557
+ )
558
+ if not should_penalize and _is_account_neutral_request_rejection(
559
+ code=code,
560
+ http_status=None,
561
+ message=message,
562
+ ):
563
+ _facade().logger.info(
564
+ "Skipped account error penalty for account-neutral request rejection code=%s request_id=%s",
565
+ code,
566
+ get_request_id(),
567
+ )
568
+ return should_penalize
569
+
570
+
571
+ _MODEL_CAPACITY_LIMIT_CODES = {
572
+ "rate_limit_exceeded",
573
+ "usage_limit_reached",
574
+ "insufficient_quota",
575
+ "usage_not_included",
576
+ "quota_exceeded",
577
+ }
578
+
579
+
580
+ def _should_retry_transient_stream_error(
581
+ code: str | None,
582
+ message: str | None,
583
+ *,
584
+ response_id: str | None = None,
585
+ ) -> bool:
586
+ if code is None or code == "stream_idle_timeout":
587
+ return False
588
+ if code == PROCESS_NETWORK_UNAVAILABLE_CODE:
589
+ # Serialized terminal events arrive only after upstream dispatch. The
590
+ # stable code keeps settlement account-neutral, but cannot prove that
591
+ # replaying the POST is safe.
592
+ return False
593
+ if response_id is not None:
594
+ return False
595
+ if code in _facade()._TRANSIENT_RETRY_CODES:
596
+ return True
597
+ if is_upstream_model_capacity_error(message):
598
+ if code in _MODEL_CAPACITY_LIMIT_CODES:
599
+ return False
600
+ return True
601
+ if code != "upstream_unavailable" or not message:
602
+ return False
603
+ normalized_message = message.lower()
604
+ if any(marker in normalized_message for marker in _facade()._UPSTREAM_UNAVAILABLE_NON_TRANSIENT_MESSAGE_MARKERS):
605
+ return False
606
+ return any(marker in normalized_message for marker in _facade()._UPSTREAM_UNAVAILABLE_TRANSIENT_MESSAGE_MARKERS)
607
+
608
+
609
+ def _classify_upstream_close(
610
+ close_code: int | None,
611
+ *,
612
+ response_events_seen: int,
613
+ ) -> Literal["clean", "transient"]:
614
+ if close_code == 1000 and response_events_seen == 0:
615
+ # A clean websocket close before response.created does not prove that
616
+ # the request was invalid. The upstream can close a socket during a
617
+ # handoff, so the caller may safely recreate the socket and replay the
618
+ # still-unstarted request once.
619
+ return "clean"
620
+ return "transient"
621
+
622
+
623
+ def _is_account_neutral_transport_drop(
624
+ close_code: int | None,
625
+ *,
626
+ response_events_seen: int,
627
+ ) -> bool:
628
+ """Return whether an upstream websocket ending is account-neutral evidence.
629
+
630
+ An abrupt transport drop that carries no close frame and arrived before
631
+ any application-layer response event is the weakest possible evidence of
632
+ account ill-health: the account never spoke at the application layer for
633
+ this request. Charging the account lets a few infrastructure resets push
634
+ it into error backoff and 502 continuity-bound follow-ups while healthy
635
+ pool siblings idle (issue #1754). Any close frame — even a non-clean one —
636
+ is upstream-authored evidence and keeps the existing penalty semantics, as
637
+ does a drop after response events started streaming.
638
+
639
+ Close code 1006 (abnormal closure) is reserved by RFC 6455 and can never
640
+ appear in an actual close frame: adapters synthesize it locally when the
641
+ socket dies without one (aiohttp stores 1006 on ``close_code`` for an
642
+ abnormal CLOSED), so it counts as frame-less here.
643
+ """
644
+
645
+ return close_code in (None, 1006) and response_events_seen == 0
646
+
647
+
648
+ def _should_infer_upstream_status_from_proxy_error(exc: ProxyResponseError, upstream_error_code: str | None) -> bool:
649
+ if exc.failure_phase == "status":
650
+ return True
651
+ if exc.failure_phase is not None:
652
+ return False
653
+ return upstream_error_code not in _facade()._LOCAL_PROXY_ERROR_CODES
654
+
655
+
656
+ def _rewrite_previous_response_stream_error(
657
+ *,
658
+ previous_response_id: str | None,
659
+ preferred_account_id: str | None,
660
+ error_code: str | None,
661
+ error_type: str | None,
662
+ error_message: str | None,
663
+ error_param: OpenAIErrorParam | JsonValue,
664
+ ) -> tuple[str, str, str | None] | None:
665
+ if previous_response_id is None:
666
+ return None
667
+ if _facade()._is_previous_response_not_found_error(
668
+ code=error_code,
669
+ param=error_param,
670
+ message=error_message,
671
+ ):
672
+ _record_continuity_fail_closed(
673
+ surface="http_stream",
674
+ reason="previous_response_not_found",
675
+ previous_response_id=previous_response_id,
676
+ upstream_error_code=error_code,
677
+ )
678
+ return (
679
+ "stream_incomplete",
680
+ PREVIOUS_RESPONSE_STREAM_INCOMPLETE_MESSAGE,
681
+ None,
682
+ )
683
+ if _facade()._is_missing_tool_output_error(
684
+ code=error_code,
685
+ param=error_param,
686
+ message=error_message,
687
+ ):
688
+ _record_continuity_fail_closed(
689
+ surface="http_stream",
690
+ reason="missing_tool_output",
691
+ previous_response_id=previous_response_id,
692
+ upstream_error_code=error_code,
693
+ )
694
+ return (
695
+ "stream_incomplete",
696
+ PREVIOUS_RESPONSE_STREAM_INCOMPLETE_MESSAGE,
697
+ None,
698
+ )
699
+ if _facade()._is_previous_response_not_found_public_shape(
700
+ code=error_code,
701
+ param=error_param,
702
+ message=error_message,
703
+ ):
704
+ # Preserve masking when malformed ``param`` metadata makes the
705
+ # recovery classifier fail closed. This branch never authorizes
706
+ # replay or account switching.
707
+ _record_continuity_fail_closed(
708
+ surface="http_stream",
709
+ reason=PREVIOUS_RESPONSE_MALFORMED_PARAM_REASON,
710
+ previous_response_id=previous_response_id,
711
+ upstream_error_code=error_code,
712
+ )
713
+ return (
714
+ "stream_incomplete",
715
+ PREVIOUS_RESPONSE_STREAM_INCOMPLETE_MESSAGE,
716
+ None,
717
+ )
718
+ normalized_code = _normalize_error_code(error_code, error_type)
719
+ if preferred_account_id is not None and normalized_code in _facade()._ACCOUNT_RECOVERY_RETRY_CODES:
720
+ _record_continuity_fail_closed(
721
+ surface="http_stream",
722
+ reason="owner_account_unavailable",
723
+ previous_response_id=previous_response_id,
724
+ upstream_error_code=normalized_code,
725
+ )
726
+ return (
727
+ "previous_response_owner_unavailable",
728
+ "Previous response owner account is unavailable; retry later.",
729
+ normalized_code,
730
+ )
731
+ return None
732
+
733
+
734
+ def _raw_stream_error_fields(
735
+ event_type: str | None,
736
+ event_payload: dict[str, JsonValue] | None,
737
+ ) -> tuple[str | None, str | None, OpenAIErrorParam | None, str]:
738
+ raw_error_type = _websocket_event_error_type(event_type, event_payload)
739
+ raw_error_message = _websocket_event_error_message(event_type, event_payload)
740
+ raw_error_param = _websocket_event_error_param(event_type, event_payload)
741
+ return (
742
+ raw_error_type,
743
+ raw_error_message,
744
+ raw_error_param,
745
+ _normalize_error_code(_websocket_event_error_code(event_type, event_payload), raw_error_type),
746
+ )
747
+
748
+
749
+ def _openai_error_fields(
750
+ error: OpenAIError | None,
751
+ ) -> tuple[str | None, str | None, OpenAIErrorParam | None]:
752
+ if error is None:
753
+ return None, None, None
754
+ return error.type, error.message, error.param_state
755
+
756
+
757
+ def _raw_stream_error_code_or_upstream(
758
+ event_type: str | None,
759
+ event_payload: dict[str, JsonValue] | None,
760
+ error_code: str,
761
+ ) -> str:
762
+ if (
763
+ event_type == "error"
764
+ and error_code == "error"
765
+ and _websocket_event_error_code(event_type, event_payload) is None
766
+ ):
767
+ return "upstream_error"
768
+ return error_code
769
+
770
+
771
+ def _classify_terminal_stream_error_frame(
772
+ event_type: str | None,
773
+ event_payload: dict[str, JsonValue] | None,
774
+ error_code: str,
775
+ error_message: str | None,
776
+ ) -> str:
777
+ """Resolve a terminal frame's error code and record its observability in one step.
778
+
779
+ ``streaming/mixin.py`` sits at its line ceiling, so the two parsed
780
+ terminal-frame sites resolve the code (``_raw_stream_error_code_or_upstream``)
781
+ and observe the frame (``_observe_terminal_stream_error_frame``) through
782
+ this single call instead of one statement each.
783
+ """
784
+ resolved_code = _raw_stream_error_code_or_upstream(event_type, event_payload, error_code)
785
+ _observe_terminal_stream_error_frame(resolved_code, error_message)
786
+ return resolved_code
787
+
788
+
789
+ def _mark_stream_settlement_interrupted(
790
+ settlement: _StreamSettlement,
791
+ *,
792
+ status: str,
793
+ error_code: str,
794
+ error_message: str,
795
+ failure_phase: Literal["upstream", "downstream"],
796
+ failure_detail: str,
797
+ account_health_error: bool,
798
+ ) -> tuple[str, str, str, _RequestLogFailureMetadata]:
799
+ settlement.record_success = False
800
+ settlement.account_health_error = account_health_error
801
+ settlement.error = {"message": error_message}
802
+ return (
803
+ status,
804
+ error_code,
805
+ error_message,
806
+ _RequestLogFailureMetadata(failure_phase=failure_phase, failure_detail=failure_detail),
807
+ )
808
+
809
+
810
+ def _stamp_terminal(settlement: _StreamSettlement, event_type: str | None, clock: Clock) -> bool:
811
+ """Return whether ``event_type`` is an upstream terminal frame, stamping its parse instant on the settlement.
812
+
813
+ Called at the HTTP stream's terminal-detection sites before the frame is
814
+ yielded downstream, so the throughput cohort sample's span ends when the
815
+ upstream finished generating, not when a slow downstream consumer drained
816
+ the frame or the upstream connection finally closed.
817
+ """
818
+ if event_type not in {"response.completed", "response.failed", "response.incomplete", "error"}:
819
+ return False
820
+ settlement.upstream_terminal_at = clock.monotonic()
821
+ return True
822
+
823
+
824
+ def _upstream_terminal_latency_ms(settlement: _StreamSettlement, started_at: float) -> int | None:
825
+ """Attempt-start-to-upstream-terminal latency for the request-log funnel; ``None`` without a terminal stamp."""
826
+ if settlement.upstream_terminal_at is None:
827
+ return None
828
+ return max(0, int((settlement.upstream_terminal_at - started_at) * 1000))
829
+
830
+
831
+ def _mark_upstream_stream_incomplete(
832
+ settlement: _StreamSettlement,
833
+ ) -> tuple[str, str, str, _RequestLogFailureMetadata]:
834
+ return _mark_stream_settlement_interrupted(
835
+ settlement,
836
+ status="error",
837
+ error_code="stream_incomplete",
838
+ error_message="Upstream stream ended before response.completed",
839
+ failure_phase="upstream",
840
+ failure_detail="upstream_eof_before_terminal_event",
841
+ account_health_error=True,
842
+ )
843
+
844
+
845
+ def _mark_downstream_stream_cancelled(
846
+ settlement: _StreamSettlement,
847
+ ) -> tuple[str, str, str, _RequestLogFailureMetadata]:
848
+ return _mark_stream_settlement_interrupted(
849
+ settlement,
850
+ status="cancelled",
851
+ error_code="client_disconnected",
852
+ error_message="Downstream client disconnected before response.completed",
853
+ failure_phase="downstream",
854
+ failure_detail="client_disconnected_before_terminal_event",
855
+ account_health_error=False,
856
+ )
857
+
858
+
859
+ def _rewrite_malformed_stream_error_event(
860
+ *,
861
+ enforce_openai_sdk_contract: bool,
862
+ event: OpenAIEvent | None,
863
+ event_type: str | None,
864
+ event_payload: dict[str, JsonValue] | None,
865
+ response_id: str,
866
+ ) -> tuple[str, OpenAIEvent | None, dict[str, JsonValue] | None, str | None] | None:
867
+ """Rewrite a schema-less upstream ``error`` frame under the SDK contract.
868
+
869
+ A malformed frame like ``{"type":"error","message":"..."}`` classifies as
870
+ ``error`` but carries no error envelope (``event`` is None or has no
871
+ ``error``), so it must become a terminal ``response.failed`` instead of
872
+ leaking the raw frame with a success settlement. Returns None when the
873
+ frame is not a malformed error (well-formed errors keep their
874
+ envelope-driven handling).
875
+ """
876
+ if not enforce_openai_sdk_contract or event_type != "error":
877
+ return None
878
+ if (event is not None and event.error is not None) or not isinstance(event_payload, dict):
879
+ return None
880
+ message_value = event_payload.get("message")
881
+ message = message_value.strip() if isinstance(message_value, str) and message_value.strip() else "Upstream error"
882
+ return _build_rewritten_stream_response_failed_event(
883
+ response_id=response_id,
884
+ error_code="upstream_error",
885
+ error_message=message,
886
+ )
887
+
888
+
889
+ def _build_rewritten_stream_response_failed_event(
890
+ *,
891
+ response_id: str,
892
+ error_code: str,
893
+ error_message: str,
894
+ ) -> tuple[str, OpenAIEvent | None, dict[str, JsonValue] | None, str | None]:
895
+ rewritten_event_payload = response_failed_event(
896
+ error_code,
897
+ error_message,
898
+ error_type="server_error",
899
+ response_id=response_id,
900
+ )
901
+ if error_code in SYNTHETIC_TRANSPORT_FAILURE_CODES:
902
+ rewritten_event_payload = synthetic_transport_failure_event(rewritten_event_payload)
903
+ rewritten_event_block = format_sse_event(rewritten_event_payload)
904
+ rewritten_payload = parse_sse_data_json(rewritten_event_block)
905
+ rewritten_event = parse_sse_event(rewritten_event_block)
906
+ rewritten_event_type = _event_type_from_payload(rewritten_event, rewritten_payload)
907
+ return rewritten_event_block, rewritten_event, rewritten_payload, rewritten_event_type
908
+
909
+
910
+ def _stream_transport_failure_event_or_raise(
911
+ error_code: str,
912
+ error_message: str,
913
+ *,
914
+ response_id: str,
915
+ preserve_native_failure_lifecycle: bool,
916
+ ) -> str:
917
+ if preserve_native_failure_lifecycle:
918
+ raise ProxyResponseError(
919
+ 502,
920
+ openai_error(error_code, error_message),
921
+ failure_phase="upstream",
922
+ )
923
+ return format_sse_event(synthetic_stream_failure_event(error_code, error_message, response_id=response_id))
924
+
925
+
926
+ def _build_stream_incomplete_terminal_event_for_request(
927
+ request_state: _WebSocketRequestState,
928
+ *,
929
+ reason: str = "stream_incomplete",
930
+ ) -> tuple[str, str, OpenAIEvent | None, dict[str, JsonValue] | None, str | None]:
931
+ error_code, error_message = _websocket_continuity_error_fields(
932
+ reason=reason,
933
+ expose_stale_previous_response_classifier=request_state.expose_stale_previous_response_classifier,
934
+ )
935
+ event_block, event, payload, event_type = _build_rewritten_stream_response_failed_event(
936
+ response_id=_websocket_downstream_response_id(request_state),
937
+ error_code=error_code,
938
+ error_message=error_message,
939
+ )
940
+ if payload is None: # pragma: no cover - formatter/parser contract
941
+ raise RuntimeError("rewritten stream failure event did not contain a JSON payload")
942
+ downstream_text = json.dumps(
943
+ payload,
944
+ ensure_ascii=True,
945
+ separators=(",", ":"),
946
+ )
947
+ return downstream_text, event_block, event, payload, event_type
948
+
949
+
950
+ def _slim_response_create_payload_for_upstream(
951
+ payload: dict[str, JsonValue],
952
+ *,
953
+ max_bytes: int,
954
+ protected_agent_control_output_occurrences: Mapping[tuple[str, str], tuple[bool, ...]] | None = None,
955
+ ) -> tuple[dict[str, JsonValue], dict[str, int] | None]:
956
+ input_value = payload.get("input")
957
+ if not isinstance(input_value, list) or not input_value:
958
+ return payload, None
959
+
960
+ input_items = cast(list[JsonValue], deepcopy(input_value))
961
+ preserve_from = _facade()._response_create_recent_suffix_start(input_items)
962
+ historical = input_items[:preserve_from]
963
+ recent = input_items[preserve_from:]
964
+
965
+ tool_outputs_slimmed = 0
966
+ images_slimmed = 0
967
+ if protected_agent_control_output_occurrences is None:
968
+ protected_agent_control_output_occurrences = _agent_control_tool_output_occurrences(historical)
969
+ agent_control_output_counts: dict[tuple[str, str], int] = {}
970
+
971
+ slimmed_historical: list[JsonValue] = []
972
+ for item in historical:
973
+ (
974
+ slimmed_item,
975
+ item_tool_outputs_slimmed,
976
+ item_images_slimmed,
977
+ ) = _facade()._slim_historical_response_input_item(
978
+ item,
979
+ protected_agent_control_output_occurrences=protected_agent_control_output_occurrences,
980
+ agent_control_output_counts=agent_control_output_counts,
981
+ )
982
+ tool_outputs_slimmed += item_tool_outputs_slimmed
983
+ images_slimmed += item_images_slimmed
984
+ slimmed_historical.append(slimmed_item)
985
+
986
+ candidate_payload = dict(payload)
987
+ candidate_payload["input"] = slimmed_historical + recent
988
+
989
+ if tool_outputs_slimmed == 0 and images_slimmed == 0:
990
+ return payload, None
991
+
992
+ return candidate_payload, {
993
+ "historical_tool_outputs_slimmed": tool_outputs_slimmed,
994
+ "historical_images_slimmed": images_slimmed,
995
+ }
996
+
997
+
998
+ def _call_stream_with_supported_optional_kwargs(
999
+ func: Callable[..., AsyncIterator[str]],
1000
+ /,
1001
+ *args: Any,
1002
+ optional_kwargs: Mapping[str, Any],
1003
+ **required_kwargs: Any,
1004
+ ) -> AsyncIterator[str]:
1005
+ return func(*args, **_facade()._supported_optional_kwargs(func, optional_kwargs, required_kwargs))
1006
+
1007
+
1008
+ def _stream_request_budget_seconds(settings: object, *, request_transport: str) -> float:
1009
+ del request_transport
1010
+ budget = getattr(settings, "http_responses_stream_request_budget_seconds", None)
1011
+ if budget is not None:
1012
+ return float(budget)
1013
+ return float(getattr(settings, "proxy_request_budget_seconds"))
1014
+
1015
+
1016
+ async def _resolve_upstream_route_for_account(
1017
+ proxy: Any,
1018
+ account: Account,
1019
+ *,
1020
+ operation: str,
1021
+ ) -> ResolvedUpstreamRoute | None:
1022
+ # Outcomes (route / direct-egress None / fail-closed error) are cached
1023
+ # verbatim, so a hit can never change the degradation path the resolver
1024
+ # chose; errors re-raise with their original reason.
1025
+ cache = get_upstream_route_cache()
1026
+ cached = cache.get(account.id)
1027
+ if cached is not None:
1028
+ return cached.unwrap(account.id)
1029
+ generation = cache.generation
1030
+ async with _facade().SessionLocal() as session:
1031
+ try:
1032
+ route = await _facade().resolve_upstream_route(
1033
+ session,
1034
+ account_id=account.id,
1035
+ operation=operation,
1036
+ scope="account",
1037
+ encryptor=proxy._encryptor,
1038
+ )
1039
+ except UpstreamProxyRouteError as exc:
1040
+ cache.store_error(account.id, exc, generation=generation)
1041
+ raise
1042
+ cache.store_route(account.id, route, generation=generation)
1043
+ return route
1044
+
1045
+
1046
+ async def _select_account_with_budget_for_stream(proxy: Any, deadline: float, **kwargs: Any) -> AccountSelection:
1047
+ selector = proxy._select_account_with_budget_compatible
1048
+ optional_kwargs = (
1049
+ "require_security_work_authorized",
1050
+ "lease_kind",
1051
+ "estimated_lease_tokens",
1052
+ "fallback_on_preferred_account_unavailable",
1053
+ "preferred_account_is_continuity_owner",
1054
+ "preferred_account_overrides_single_account_routing",
1055
+ "spill_bare_session_on_account_cap",
1056
+ "require_unambiguous_account",
1057
+ )
1058
+ if any(name in kwargs for name in optional_kwargs):
1059
+ try:
1060
+ signature = inspect.signature(selector)
1061
+ except (TypeError, ValueError):
1062
+ signature = None
1063
+ accepts_var_keyword = signature is not None and any(
1064
+ parameter.kind is inspect.Parameter.VAR_KEYWORD for parameter in signature.parameters.values()
1065
+ )
1066
+ if signature is not None and not accepts_var_keyword:
1067
+ kwargs = dict(kwargs)
1068
+ for name in optional_kwargs:
1069
+ if name not in signature.parameters:
1070
+ kwargs.pop(name, None)
1071
+ return await selector(deadline, **kwargs)
1072
+
1073
+
1074
+ def _is_account_neutral_request_rejection(
1075
+ *,
1076
+ code: str,
1077
+ http_status: int | None,
1078
+ message: str | None,
1079
+ ) -> bool:
1080
+ """Return whether upstream rejected the request payload, not the account.
1081
+
1082
+ An account-neutral request rejection reproduces identically on every
1083
+ account, so it must never mutate one account's health: otherwise a single
1084
+ client looping on a rejected conversation drives its serving accounts into
1085
+ ``error_count`` backoff and starves unrelated tenants.
1086
+
1087
+ Keep this set narrow: membership is decided by the specific classified
1088
+ code and message, never by a broad HTTP status alone. The model-entitlement
1089
+ rejection is deliberately not a member -- it is handled by
1090
+ ``_is_model_scoped_rejection`` below, which likewise keeps the account's
1091
+ health untouched but still lets failover try accounts whose entitlements
1092
+ may differ.
1093
+ """
1094
+ if http_status is not None and http_status != 400:
1095
+ return False
1096
+ if is_account_neutral_safety_policy_rejection(code=code, http_status=http_status, message=message):
1097
+ return True
1098
+ return code == "invalid_request_error" and bool(_facade()._is_missing_tool_output_message(message))
1099
+
1100
+
1101
+ def _is_model_scoped_rejection(
1102
+ *,
1103
+ http_status: int | None,
1104
+ message: str | None,
1105
+ ) -> bool:
1106
+ """Return whether upstream rejected the requested model, not the account.
1107
+
1108
+ The ChatGPT model-entitlement rejection is scoped to the model named in the
1109
+ message. It proves nothing about the account's ability to serve the models
1110
+ it *is* entitled to, so it must never move that account's ``error_count``:
1111
+ otherwise one client looping on a model no account can serve drives every
1112
+ serving account into ``ERROR_BACKOFF_THRESHOLD`` backoff and starves all
1113
+ unrelated traffic on those accounts -- the exact failure mode when a model
1114
+ reaches subscription selection because its OpenAI-compatible model source
1115
+ is disabled or unreachable.
1116
+
1117
+ Failover is deliberately untouched: the classified failure is still
1118
+ returned, so the caller keeps trying other accounts, whose entitlements may
1119
+ differ.
1120
+
1121
+ The normalized code is not part of the match. Upstream delivers this
1122
+ rejection with neither ``code`` nor ``type`` on the streaming path, which
1123
+ normalizes to ``upstream_error``; on other paths it arrives as
1124
+ ``invalid_request_error``. Only the exact message shape decides membership.
1125
+ """
1126
+ if http_status is not None and http_status != 400:
1127
+ return False
1128
+ return is_model_scoped_upstream_rejection(message)
1129
+
1130
+
1131
+ def _request_usage_refresh(proxy: Any, account_id: str) -> None:
1132
+ """Schedule a tracked, coalesced usage refresh after a streamed ``usage_limit_reached``.
1133
+
1134
+ ``mark_rate_limit`` persists status only, while the pool-exhaustion
1135
+ predicate also needs a >= 100 % usage row that would otherwise wait for
1136
+ the next scheduler tick. The refresh runs on its own background session
1137
+ and never touches this request's ``Account``.
1138
+ """
1139
+ schedule = getattr(proxy, "_schedule_cancel_safe_cleanup", None)
1140
+ if schedule is None:
1141
+ return
1142
+ refresh = UsageUpdater.request_refresh(account_id)
1143
+ if refresh is None:
1144
+ return
1145
+ schedule(refresh, action="request_usage_refresh", request_id=get_request_id() or "unknown")
1146
+
1147
+
1148
+ async def _handle_stream_error(
1149
+ proxy: Any,
1150
+ account: Account,
1151
+ error: UpstreamError,
1152
+ code: str,
1153
+ http_status: int | None = None,
1154
+ *,
1155
+ privacy_policy: CodexControlRequestPrivacyPolicy = CodexControlRequestPrivacyPolicy.STANDARD,
1156
+ retry_after_seconds: float | None = None,
1157
+ burst_cooldown_recorded: bool = False,
1158
+ upstream_http_status: int | None = None,
1159
+ ) -> ClassifiedFailure:
1160
+ """Write account health for a stream failure and return its classification.
1161
+
1162
+ ``burst_cooldown_recorded`` is set by a keyed stream whose health write was
1163
+ deferred until after usage settlement: the replica-local burst cooldown
1164
+ was already engaged at rejection time, so the deferred write must not
1165
+ re-engage it (that would bench an account that has since succeeded).
1166
+
1167
+ ``upstream_http_status`` is evidence-only: it lets a caller that knows the
1168
+ upstream HTTP status of this failure say so WITHOUT taking any of the
1169
+ ``http_status`` side effects. It is read by the soft-overload gate below
1170
+ and by nothing else -- never by ``classify_upstream_failure``, the
1171
+ reasoning-replay metric, the account-neutral / model-scoped rejection
1172
+ predicates, or the HTTP 429 burst-cooldown branch. Callers that want those
1173
+ behaviours must pass the positional ``http_status`` instead.
1174
+ """
1175
+ classified = classify_upstream_failure(
1176
+ error_code=code,
1177
+ error=error,
1178
+ http_status=http_status,
1179
+ phase="first_event",
1180
+ )
1181
+ # Terminal frames are counted where they are classified
1182
+ # (``_observe_terminal_stream_error_frame``); only HTTP status rejections
1183
+ # reach the counter from here, so a failure is never counted twice.
1184
+ if http_status is not None and _is_reasoning_replay_rejection(
1185
+ code=code, http_status=http_status, message=error.get("message")
1186
+ ):
1187
+ _record_upstream_reasoning_replay_rejection()
1188
+ if _facade()._is_account_neutral_error_code(code):
1189
+ return classified
1190
+ if _is_account_neutral_request_rejection(
1191
+ code=code,
1192
+ http_status=http_status,
1193
+ message=error.get("message"),
1194
+ ):
1195
+ _facade().logger.info(
1196
+ "Skipped account error penalty for account-neutral request rejection account_id=%s request_id=%s code=%s",
1197
+ "<redacted>" if privacy_policy.redacts_sensitive_details else account.id,
1198
+ get_request_id(),
1199
+ code,
1200
+ )
1201
+ return classified
1202
+ if _is_model_scoped_rejection(
1203
+ http_status=http_status,
1204
+ message=error.get("message"),
1205
+ ):
1206
+ _facade().logger.info(
1207
+ "Skipped account error penalty for model-scoped upstream rejection account_id=%s request_id=%s code=%s",
1208
+ "<redacted>" if privacy_policy.redacts_sensitive_details else account.id,
1209
+ get_request_id(),
1210
+ code,
1211
+ )
1212
+ return classified
1213
+ if classified["failure_class"] == "rate_limit":
1214
+ await proxy._load_balancer.mark_rate_limit(account, error)
1215
+ # The refresh is owed to the account whose window is spent, and the
1216
+ # literal code is the one piece of that evidence upstream omits at will:
1217
+ # the serialized terminal frame asserts the same limit in its message
1218
+ # alone. Reading only the code leaves the pool's usage picture stale for
1219
+ # exactly the accounts it most needs to be current about. Plain
1220
+ # throttling still asks for nothing.
1221
+ if is_upstream_usage_limit_rejection(error_code=code, message=error.get("message")):
1222
+ _request_usage_refresh(proxy, account.id)
1223
+ elif classified["failure_class"] == "quota":
1224
+ await proxy._load_balancer.mark_quota_exceeded(account, error)
1225
+ elif code in PERMANENT_FAILURE_CODES:
1226
+ await proxy._load_balancer.mark_permanent_failure(account, code)
1227
+ else:
1228
+ await proxy._load_balancer.record_error(account)
1229
+ _facade().logger.info(
1230
+ "Recorded transient account error account_id=%s request_id=%s code=%s",
1231
+ "<redacted>" if privacy_policy.redacts_sensitive_details else account.id,
1232
+ get_request_id(),
1233
+ code,
1234
+ )
1235
+ hard_overload = code in UPSTREAM_OVERLOAD_CODES
1236
+ # A bare ``server_error`` *stream terminal* is the same observable
1237
+ # condition as an explicit overload: upstream admitted the turn and then
1238
+ # refused to run it. The terminal shape is what makes it an admission
1239
+ # rejection, so it is keyed on the absence of an HTTP status. A coded
1240
+ # HTTP failure carrying the same string is an ordinary transient error
1241
+ # (and an HTTP 429 is a burst rejection with its own cooldown branch
1242
+ # below); neither may deprioritize an otherwise healthy account.
1243
+ #
1244
+ # Two status keywords are checked because several callers KNOW the
1245
+ # upstream status yet deliberately do not forward it as ``http_status``:
1246
+ # doing so would also flip the account-neutral / model-scoped predicates
1247
+ # above from "skip the penalty" to "record it", arm the 429 branch below,
1248
+ # and double-count the reasoning-replay metric for a frame already
1249
+ # counted at ``_observe_terminal_stream_error_frame``. They pass
1250
+ # ``upstream_http_status`` instead, which gates this window only. A
1251
+ # status from EITHER keyword means the failure was not a status-less
1252
+ # terminal and so must stay out of the soft window.
1253
+ soft_overload = code in UPSTREAM_SOFT_OVERLOAD_CODES and http_status is None and upstream_http_status is None
1254
+ if hard_overload or soft_overload:
1255
+ # Overload is an admission rejection that successes on the same
1256
+ # account's warm sessions keep masking from ``error_count``; feed
1257
+ # the dedicated sliding window so fresh selection can deprioritize.
1258
+ # Soft observations count at a fractional weight, so a sustained
1259
+ # refusal still trips the window while a lone fault never does.
1260
+ await record_upstream_overload(
1261
+ proxy._load_balancer,
1262
+ account,
1263
+ redact_account_id=privacy_policy.redacts_sensitive_details,
1264
+ soft=not hard_overload,
1265
+ )
1266
+ elif http_status == 429 and not burst_cooldown_recorded:
1267
+ # A code-less HTTP 429 (rate_limit / quota classes returned above)
1268
+ # is a per-account burst/concurrency rejection: keyed on the status
1269
+ # so ``server_error`` rewrites and ``detail``-parsed bodies are
1270
+ # covered. Short replica-local cooldown only -- never
1271
+ # ``mark_rate_limit`` / persisted RATE_LIMITED.
1272
+ await record_upstream_burst_rejection(
1273
+ proxy._load_balancer,
1274
+ account,
1275
+ retry_after_seconds=retry_after_seconds,
1276
+ redact_account_id=privacy_policy.redacts_sensitive_details,
1277
+ )
1278
+ return classified
1279
+
1280
+
1281
+ def _push_stream_attempt_timeout_overrides(
1282
+ timeout_seconds: float,
1283
+ ) -> tuple[float | None, float | None, float | None]:
1284
+ return _facade().push_stream_timeout_overrides(
1285
+ connect_timeout_seconds=timeout_seconds,
1286
+ idle_timeout_seconds=timeout_seconds,
1287
+ total_timeout_seconds=timeout_seconds,
1288
+ )
1289
+
1290
+
1291
+ def _should_retry_stream_error(code: str, message: str | None = None) -> bool:
1292
+ """Whether a pre-visible terminal frame may be retried on a sibling account.
1293
+
1294
+ The code allowlist cannot answer this for the serialized form of the
1295
+ usage-limit rejection: upstream sends that frame with no error code, which
1296
+ normalizes to ``upstream_error`` and is in no transport retry list -- so a
1297
+ frame saying the account is spent gets surfaced on the first account while
1298
+ the identical HTTP body walks the pool. The frame's sentence is put through
1299
+ the same predicate the health write beside it uses, because that sentence
1300
+ is the only evidence a status-less, code-less frame carries.
1301
+ """
1302
+ return code in _facade()._ACCOUNT_RECOVERY_RETRY_CODES or is_upstream_usage_limit_rejection(
1303
+ error_code=code, message=message
1304
+ )
1305
+
1306
+
1307
+ def _upstream_turn_state_from_socket(upstream: UpstreamWebSocket | None) -> str | None:
1308
+ if upstream is None:
1309
+ return None
1310
+ getter = getattr(upstream, "response_header", None)
1311
+ if not callable(getter):
1312
+ return None
1313
+ value = getter("x-codex-turn-state")
1314
+ if not isinstance(value, str):
1315
+ return None
1316
+ stripped = value.strip()
1317
+ return stripped or None