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,996 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import inspect
5
+ import json
6
+ import secrets
7
+ from dataclasses import dataclass
8
+ from typing import Any, Mapping
9
+ from urllib.parse import urlencode
10
+
11
+ import aiohttp
12
+ from aiohttp_socks import ProxyConnector
13
+ from python_socks import ProxyType
14
+ from yarl import URL
15
+
16
+ from app.core.clients.native_egress import (
17
+ NativeEgressClient,
18
+ NativeEgressError,
19
+ NativeEgressRequest,
20
+ NativeEgressTransportError,
21
+ NativeEgressUnavailable,
22
+ NativeSseOptions,
23
+ NativeWebSocketRequest,
24
+ discover_native_egress_client,
25
+ )
26
+ from app.core.resilience.network_recovery import (
27
+ PROCESS_NETWORK_UNAVAILABLE_CODE,
28
+ is_pre_dispatch_connection_failure,
29
+ is_process_network_failure,
30
+ )
31
+ from app.core.upstream_proxy import ResolvedProxyEndpoint, ResolvedUpstreamRoute
32
+
33
+ _RESERVED = frozenset({"akamai", "extra_fp", "impersonate", "ja3", "proxies", "proxy", "proxy_headers"})
34
+ _TLS_TARGET_SCHEMES = frozenset({"https", "wss"})
35
+ _CODEX_SKIP_AUTO_IDENTITY_HEADERS = frozenset({aiohttp.hdrs.ACCEPT, aiohttp.hdrs.ACCEPT_ENCODING})
36
+
37
+
38
+ class CodexTransportError(RuntimeError):
39
+ """Credential-safe routed transport failure.
40
+
41
+ ``retryable_same_contract`` is provenance, not a synonym for transient:
42
+ callers may replay only when the failing layer proved dispatch never
43
+ happened.
44
+ """
45
+
46
+ def __init__(
47
+ self,
48
+ message: str,
49
+ *,
50
+ status_code: int | None = None,
51
+ error_code: str | None = None,
52
+ retryable_same_contract: bool = False,
53
+ failure_phase: str | None = None,
54
+ is_tls_verification_failure: bool = False,
55
+ handshake_headers: Mapping[str, str] | None = None,
56
+ handshake_message: str | None = None,
57
+ ) -> None:
58
+ super().__init__(message)
59
+ self.status_code = status_code
60
+ self.error_code = error_code
61
+ self.retryable_same_contract = retryable_same_contract
62
+ self.failure_phase = failure_phase
63
+ self.is_tls_verification_failure = is_tls_verification_failure
64
+ # Handshake response metadata preserved across the sanitizing
65
+ # boundary so callers can classify edge-level rejections (for
66
+ # example Cloudflare browser challenges) that the sanitized message
67
+ # and status alone cannot prove. In-process only; never surfaced to
68
+ # clients.
69
+ self.handshake_headers = handshake_headers
70
+ self.handshake_message = handshake_message
71
+
72
+
73
+ def require_route_or_direct_egress_opt_in(
74
+ *,
75
+ route: ResolvedUpstreamRoute | None,
76
+ allow_direct_egress: bool,
77
+ operation: str,
78
+ ) -> None:
79
+ if route is None and not allow_direct_egress:
80
+ raise ValueError(f"Direct Codex upstream egress for {operation} requires allow_direct_egress=True")
81
+
82
+
83
+ @dataclass(frozen=True, slots=True)
84
+ class CodexRequestResult:
85
+ response: Any
86
+ route: ResolvedUpstreamRoute
87
+ fallback_used: bool
88
+
89
+
90
+ @dataclass(frozen=True, slots=True)
91
+ class CodexWebSocketResult:
92
+ websocket: Any
93
+ context: Any | None
94
+ route: ResolvedUpstreamRoute
95
+ fallback_used: bool
96
+ native: bool = False
97
+
98
+
99
+ @dataclass(frozen=True, slots=True)
100
+ class _BufferedResponse:
101
+ status: int
102
+ status_code: int
103
+ headers: Mapping[str, str]
104
+ content: bytes
105
+
106
+ async def read(self) -> bytes:
107
+ return self.content
108
+
109
+ async def text(self) -> str:
110
+ return self.content.decode("utf-8", errors="replace")
111
+
112
+ def json(self) -> Any:
113
+ return json.loads(self.content)
114
+
115
+
116
+ @dataclass(frozen=True, slots=True)
117
+ class _PreparedNativeRequest:
118
+ url: str
119
+ headers: dict[str, str]
120
+ body: bytes | None
121
+ timeout_seconds: float | None
122
+ connect_timeout_seconds: float | None
123
+ response_head_timeout_seconds: float | None
124
+
125
+
126
+ async def release_codex_response(response: Any) -> None:
127
+ """Release an unbuffered upstream response once its consumer stops reading.
128
+
129
+ Streams routinely end before body EOF (terminal SSE event, idle timeout,
130
+ cancellation, downstream disconnect). Closing the owning session alone
131
+ leaves the aiohttp ``Connection`` acquired, so the cyclic GC later finalizes
132
+ it as ``Unclosed connection``. Duck-typed on purpose: aiohttp exposes
133
+ ``release()``, the native egress response exposes ``aclose()``, and buffered
134
+ or test responses expose neither.
135
+ """
136
+ for name in ("release", "close", "aclose"):
137
+ method = getattr(response, name, None)
138
+ if callable(method):
139
+ result = method()
140
+ if inspect.isawaitable(result):
141
+ await result
142
+ return
143
+
144
+
145
+ class _SessionOwnedContent:
146
+ def __init__(self, response: Any, session: aiohttp.ClientSession) -> None:
147
+ self._response = response
148
+ self._session = session
149
+
150
+ def iter_chunked(self, size: int) -> Any:
151
+ return self._iter_and_close(self._response.content.iter_chunked(size))
152
+
153
+ async def _iter_and_close(self, iterator: Any) -> Any:
154
+ try:
155
+ async for chunk in iterator:
156
+ yield chunk
157
+ finally:
158
+ await release_codex_response(self._response)
159
+ await self._session.close()
160
+
161
+
162
+ class _SessionOwnedResponse:
163
+ def __init__(self, response: Any, session: aiohttp.ClientSession) -> None:
164
+ self._response = response
165
+ self._session = session
166
+ self.status = getattr(response, "status", getattr(response, "status_code", 0))
167
+ self.status_code = getattr(response, "status_code", self.status)
168
+ self.headers = getattr(response, "headers", {}) or {}
169
+ self.content = _SessionOwnedContent(response, session)
170
+
171
+ async def release(self) -> None:
172
+ try:
173
+ await release_codex_response(self._response)
174
+ finally:
175
+ await self._session.close()
176
+
177
+ async def read(self) -> bytes:
178
+ try:
179
+ result = self._response.read()
180
+ if asyncio.iscoroutine(result):
181
+ result = await result
182
+ if isinstance(result, bytes):
183
+ return result
184
+ if isinstance(result, str):
185
+ return result.encode()
186
+ return b""
187
+ finally:
188
+ await self.release()
189
+
190
+ async def text(self) -> str:
191
+ return (await self.read()).decode("utf-8", errors="replace")
192
+
193
+ async def json(self, *args: Any, **kwargs: Any) -> Any:
194
+ del args, kwargs
195
+ import json
196
+
197
+ return json.loads(await self.read())
198
+
199
+
200
+ class _SessionOwnedWebSocketContext:
201
+ def __init__(self, context: Any, session: aiohttp.ClientSession) -> None:
202
+ self._context = context
203
+ self._session = session
204
+
205
+ async def __aenter__(self) -> Any:
206
+ return self._context
207
+
208
+ async def __aexit__(self, exc_type: Any, exc: Any, traceback: Any) -> None:
209
+ try:
210
+ if hasattr(self._context, "__aexit__"):
211
+ await self._context.__aexit__(exc_type, exc, traceback)
212
+ else:
213
+ close = getattr(self._context, "close", None)
214
+ if callable(close):
215
+ result = close()
216
+ if asyncio.iscoroutine(result):
217
+ await result
218
+ finally:
219
+ await self._session.close()
220
+
221
+
222
+ class CodexClient:
223
+ def __init__(
224
+ self,
225
+ session: Any,
226
+ *,
227
+ native_egress_client: NativeEgressClient | None = None,
228
+ ) -> None:
229
+ self._session = session
230
+ self._native_egress_client = (
231
+ native_egress_client if native_egress_client is not None else discover_native_egress_client()
232
+ )
233
+
234
+ async def request(self, method: str, url: str, *, route: ResolvedUpstreamRoute, **kwargs: Any) -> Any:
235
+ return (await self.request_with_route_metadata(method, url, route=route, **kwargs)).response
236
+
237
+ async def request_with_route_metadata(
238
+ self,
239
+ method: str,
240
+ url: str,
241
+ *,
242
+ route: ResolvedUpstreamRoute,
243
+ native_sse: NativeSseOptions | None = None,
244
+ **kwargs: Any,
245
+ ) -> CodexRequestResult:
246
+ if route is None:
247
+ raise ValueError("Codex upstream calls require a resolved upstream proxy route")
248
+ buffer_response = bool(kwargs.pop("buffer_response", True))
249
+ if native_sse is not None and buffer_response:
250
+ raise ValueError("Native SSE framing requires buffer_response=False")
251
+ _reject_reserved(kwargs)
252
+ native_request = _prepare_native_request(url, kwargs)
253
+ aiohttp_kwargs = dict(kwargs)
254
+ _normalize_aiohttp_request_kwargs(aiohttp_kwargs)
255
+ endpoints = (route.endpoint, *route.fallbacks)
256
+ _reject_credentialed_plaintext_target(url, endpoints)
257
+ allow_fallback = _is_idempotent_method(method)
258
+ for index, endpoint in enumerate(endpoints):
259
+ candidate = route.with_endpoint(endpoint, tuple(endpoints[index + 1 :]))
260
+ try:
261
+ response: Any | None = None
262
+ if self._native_egress_client is not None and native_request is not None:
263
+ try:
264
+ response = await self._native_egress_client.request(
265
+ NativeEgressRequest(
266
+ method=method,
267
+ url=native_request.url,
268
+ headers=native_request.headers,
269
+ body=native_request.body,
270
+ timeout_seconds=native_request.timeout_seconds,
271
+ connect_timeout_seconds=native_request.connect_timeout_seconds,
272
+ response_head_timeout_seconds=native_request.response_head_timeout_seconds,
273
+ proxy_url=endpoint.proxy_url,
274
+ sse=native_sse,
275
+ )
276
+ )
277
+ if buffer_response:
278
+ response = await _buffer_response(response)
279
+ except NativeEgressUnavailable:
280
+ response = None
281
+ except NativeEgressError as exc:
282
+ raise _native_transport_error("request", endpoint.id, exc) from None
283
+ if response is not None:
284
+ pass
285
+ elif endpoint.scheme.startswith("socks"):
286
+ response = await _request_via_socks_proxy(
287
+ method,
288
+ url,
289
+ endpoint,
290
+ buffer_response=buffer_response,
291
+ **aiohttp_kwargs,
292
+ )
293
+ else:
294
+ try:
295
+ response = await self._session.request(
296
+ method,
297
+ url,
298
+ **endpoint.aiohttp_proxy_kwargs(),
299
+ **aiohttp_kwargs,
300
+ )
301
+ except Exception as exc:
302
+ raise _transport_error(
303
+ "request",
304
+ endpoint.id,
305
+ exc,
306
+ failure_phase="connect" if is_pre_dispatch_connection_failure(exc) else "request",
307
+ retryable_same_contract=is_pre_dispatch_connection_failure(exc),
308
+ ) from None
309
+ if buffer_response:
310
+ try:
311
+ response = await _buffer_response(response)
312
+ except Exception as exc:
313
+ # Headers prove the POST reached the response phase;
314
+ # a failed buffered read is neutral when classified
315
+ # as host-network loss, but it is never replay-safe.
316
+ raise _transport_error(
317
+ "response body",
318
+ endpoint.id,
319
+ exc,
320
+ failure_phase="body_read",
321
+ retryable_same_contract=False,
322
+ ) from None
323
+ return CodexRequestResult(response, candidate, index > 0)
324
+ except CodexTransportError as exc:
325
+ # A confirmed pre-dispatch connect failure proves the request
326
+ # never left for upstream, so trying the next endpoint in the
327
+ # same resolved pool is safe even for a non-idempotent POST.
328
+ # TLS verification failures are stable endpoint configuration
329
+ # errors rather than transient connect losses; they keep the
330
+ # idempotent-only rule.
331
+ if index == len(endpoints) - 1 or not (
332
+ allow_fallback or (exc.retryable_same_contract and not exc.is_tls_verification_failure)
333
+ ):
334
+ raise
335
+ except Exception as exc:
336
+ pre_dispatch = is_pre_dispatch_connection_failure(exc) and not isinstance(exc, aiohttp.ClientSSLError)
337
+ if index == len(endpoints) - 1 or not (allow_fallback or pre_dispatch):
338
+ raise _transport_error(
339
+ "request",
340
+ endpoint.id,
341
+ exc,
342
+ failure_phase="connect" if is_pre_dispatch_connection_failure(exc) else "request",
343
+ retryable_same_contract=is_pre_dispatch_connection_failure(exc),
344
+ ) from None
345
+ raise RuntimeError("unreachable Codex client fallback state")
346
+
347
+ async def ws_connect(self, url: str, *, route: ResolvedUpstreamRoute, **kwargs: Any) -> Any:
348
+ if route is None:
349
+ raise ValueError("Codex upstream calls require a resolved upstream proxy route")
350
+ _reject_reserved(kwargs)
351
+ _reject_credentialed_plaintext_target(url, (route.endpoint,))
352
+ result = self._session.ws_connect(url, **route.endpoint.aiohttp_proxy_kwargs(), **kwargs)
353
+ if asyncio.iscoroutine(result):
354
+ return await result
355
+ return result
356
+
357
+ async def open_ws_with_route_metadata(
358
+ self,
359
+ url: str,
360
+ *,
361
+ route: ResolvedUpstreamRoute,
362
+ retry_handshake_status: bool = True,
363
+ retry_network_errors: bool = True,
364
+ **kwargs: Any,
365
+ ) -> CodexWebSocketResult:
366
+ if route is None:
367
+ raise ValueError("Codex upstream calls require a resolved upstream proxy route")
368
+ interpret_responses = bool(kwargs.pop("native_interpret_responses", False))
369
+ _reject_reserved(kwargs)
370
+ endpoints = (route.endpoint, *route.fallbacks)
371
+ _reject_credentialed_plaintext_target(url, endpoints)
372
+ for index, endpoint in enumerate(endpoints):
373
+ candidate = route.with_endpoint(endpoint, tuple(endpoints[index + 1 :]))
374
+ context: Any | None = None
375
+ entered_context: Any | None = None
376
+ try:
377
+ websocket: Any | None = None
378
+ native_request = _prepare_native_websocket_request(
379
+ url,
380
+ endpoint.proxy_url,
381
+ kwargs,
382
+ interpret_responses=interpret_responses,
383
+ )
384
+ if self._native_egress_client is not None and native_request is not None:
385
+ try:
386
+ websocket = await self._native_egress_client.websocket(native_request)
387
+ except NativeEgressUnavailable:
388
+ websocket = None
389
+ except NativeEgressError as exc:
390
+ raise _native_transport_error("websocket", endpoint.id, exc) from None
391
+ if websocket is not None:
392
+ return CodexWebSocketResult(
393
+ websocket,
394
+ None,
395
+ candidate,
396
+ index > 0,
397
+ native=True,
398
+ )
399
+ elif endpoint.scheme.startswith("socks"):
400
+ websocket, context = await _open_ws_via_socks_proxy(url, endpoint, **kwargs)
401
+ entered_context = context
402
+ else:
403
+ context = self._session.ws_connect(
404
+ url,
405
+ **endpoint.aiohttp_proxy_kwargs(),
406
+ **kwargs,
407
+ )
408
+ if asyncio.iscoroutine(context):
409
+ context = await context
410
+ if hasattr(context, "__aenter__"):
411
+ websocket = await context.__aenter__()
412
+ entered_context = context
413
+ else:
414
+ websocket = context
415
+ return CodexWebSocketResult(
416
+ websocket,
417
+ entered_context,
418
+ candidate,
419
+ index > 0,
420
+ )
421
+ except CodexTransportError as exc:
422
+ if entered_context is not None and hasattr(entered_context, "__aexit__"):
423
+ await entered_context.__aexit__(None, None, None)
424
+ if exc.is_tls_verification_failure:
425
+ raise
426
+ if exc.status_code is not None and not retry_handshake_status:
427
+ raise _without_same_contract_retry(exc) from None
428
+ if exc.status_code is None and not retry_network_errors:
429
+ raise _without_same_contract_retry(exc) from None
430
+ if index == len(endpoints) - 1:
431
+ raise
432
+ except Exception as exc:
433
+ if entered_context is not None and hasattr(entered_context, "__aexit__"):
434
+ await entered_context.__aexit__(None, None, None)
435
+ handshake_status = _transport_error_status_code(exc)
436
+ if handshake_status is not None and not retry_handshake_status:
437
+ raise _transport_error(
438
+ "websocket",
439
+ endpoint.id,
440
+ exc,
441
+ failure_phase="connect",
442
+ retryable_same_contract=False,
443
+ ) from None
444
+ if handshake_status is None and not retry_network_errors:
445
+ raise _transport_error(
446
+ "websocket",
447
+ endpoint.id,
448
+ exc,
449
+ failure_phase="connect",
450
+ retryable_same_contract=False,
451
+ ) from None
452
+ if index == len(endpoints) - 1:
453
+ raise _transport_error(
454
+ "websocket",
455
+ endpoint.id,
456
+ exc,
457
+ failure_phase="connect",
458
+ # No response.create frame can be delivered before the
459
+ # websocket open returns to its caller.
460
+ retryable_same_contract=is_pre_dispatch_connection_failure(exc),
461
+ ) from None
462
+ raise RuntimeError("unreachable Codex client websocket fallback state")
463
+
464
+ async def close(self) -> None:
465
+ close = getattr(self._session, "close", None)
466
+ if not callable(close):
467
+ close = getattr(self._session, "aclose", None)
468
+ if not callable(close):
469
+ return
470
+ result = close()
471
+ if asyncio.iscoroutine(result):
472
+ await result
473
+
474
+
475
+ def create_codex_session(*, max_clients: int = 10) -> Any:
476
+ from app.core.clients.http import _shared_ssl_context
477
+
478
+ connector = aiohttp.TCPConnector(limit=max_clients, ssl=_shared_ssl_context())
479
+ return aiohttp.ClientSession(
480
+ connector=connector,
481
+ timeout=aiohttp.ClientTimeout(total=None),
482
+ trust_env=False,
483
+ skip_auto_headers=_CODEX_SKIP_AUTO_IDENTITY_HEADERS,
484
+ )
485
+
486
+
487
+ async def _request_via_socks_proxy(
488
+ method: str,
489
+ url: str,
490
+ endpoint: ResolvedProxyEndpoint,
491
+ *,
492
+ buffer_response: bool,
493
+ **kwargs: Any,
494
+ ) -> Any:
495
+ connector = _socks_proxy_connector(endpoint)
496
+ session = aiohttp.ClientSession(
497
+ connector=connector,
498
+ timeout=aiohttp.ClientTimeout(total=None),
499
+ trust_env=False,
500
+ skip_auto_headers=_CODEX_SKIP_AUTO_IDENTITY_HEADERS,
501
+ )
502
+ try:
503
+ try:
504
+ response = await session.request(method, url, **kwargs)
505
+ except Exception as exc:
506
+ raise _transport_error(
507
+ "request",
508
+ endpoint.id,
509
+ exc,
510
+ failure_phase="connect" if is_pre_dispatch_connection_failure(exc) else "request",
511
+ retryable_same_contract=is_pre_dispatch_connection_failure(exc),
512
+ ) from None
513
+ if buffer_response:
514
+ try:
515
+ return await _buffer_response(response)
516
+ except Exception as exc:
517
+ raise _transport_error(
518
+ "response body",
519
+ endpoint.id,
520
+ exc,
521
+ failure_phase="body_read",
522
+ retryable_same_contract=False,
523
+ ) from None
524
+ return _SessionOwnedResponse(response, session)
525
+ except Exception:
526
+ await session.close()
527
+ raise
528
+ finally:
529
+ if buffer_response:
530
+ await session.close()
531
+
532
+
533
+ async def _open_ws_via_socks_proxy(url: str, endpoint: ResolvedProxyEndpoint, **kwargs: Any) -> tuple[Any, Any]:
534
+ connector = _socks_proxy_connector(endpoint)
535
+ session = aiohttp.ClientSession(
536
+ connector=connector,
537
+ timeout=aiohttp.ClientTimeout(total=None),
538
+ trust_env=False,
539
+ skip_auto_headers=_CODEX_SKIP_AUTO_IDENTITY_HEADERS,
540
+ )
541
+ try:
542
+ context = session.ws_connect(url, **kwargs)
543
+ if asyncio.iscoroutine(context):
544
+ context = await context
545
+ websocket = await context.__aenter__() if hasattr(context, "__aenter__") else context
546
+ return websocket, _SessionOwnedWebSocketContext(context, session)
547
+ except BaseException:
548
+ await session.close()
549
+ raise
550
+
551
+
552
+ def _socks_proxy_connector(endpoint: ResolvedProxyEndpoint) -> ProxyConnector:
553
+ from app.core.clients.http import _shared_ssl_context
554
+
555
+ proxy_scheme = endpoint.proxy_url.split(":", 1)[0]
556
+ return ProxyConnector(
557
+ host=endpoint.host,
558
+ port=endpoint.port,
559
+ proxy_type=ProxyType.SOCKS5,
560
+ username=endpoint.username,
561
+ password=endpoint.password,
562
+ rdns=proxy_scheme == "socks5h",
563
+ ssl=_shared_ssl_context(),
564
+ )
565
+
566
+
567
+ def _normalize_aiohttp_request_kwargs(kwargs: dict[str, Any]) -> None:
568
+ files = kwargs.pop("files", None)
569
+ if files is None:
570
+ return
571
+ form = aiohttp.FormData()
572
+ data = kwargs.pop("data", None)
573
+ if isinstance(data, Mapping):
574
+ for name, value in data.items():
575
+ form.add_field(str(name), value)
576
+ elif data is not None:
577
+ form.add_field("data", data)
578
+
579
+ file_items = files.items() if isinstance(files, Mapping) else files
580
+ for name, value in file_items:
581
+ _add_form_file(form, str(name), value)
582
+ kwargs["data"] = form
583
+
584
+
585
+ def _prepare_native_request(
586
+ url: str,
587
+ kwargs: Mapping[str, Any],
588
+ ) -> _PreparedNativeRequest | None:
589
+ supported = {
590
+ "data",
591
+ "files",
592
+ "headers",
593
+ "json",
594
+ "params",
595
+ "skip_auto_headers",
596
+ "timeout",
597
+ }
598
+ if set(kwargs).difference(supported):
599
+ return None
600
+
601
+ raw_headers = kwargs.get("headers") or {}
602
+ if not isinstance(raw_headers, Mapping):
603
+ return None
604
+ headers = {str(name): str(value) for name, value in raw_headers.items()}
605
+
606
+ params = kwargs.get("params")
607
+ if params is not None:
608
+ try:
609
+ url = str(URL(url).update_query(params))
610
+ except (TypeError, ValueError):
611
+ return None
612
+
613
+ has_json = "json" in kwargs
614
+ data = kwargs.get("data")
615
+ files = kwargs.get("files")
616
+ if has_json and (data is not None or files is not None):
617
+ return None
618
+
619
+ body: bytes | None
620
+ if has_json:
621
+ try:
622
+ body = json.dumps(
623
+ kwargs.get("json"),
624
+ ensure_ascii=True,
625
+ separators=(",", ":"),
626
+ ).encode("utf-8")
627
+ except (TypeError, ValueError):
628
+ return None
629
+ _set_header_default(headers, "Content-Type", "application/json")
630
+ elif files is not None:
631
+ encoded = _encode_native_multipart(data, files)
632
+ if encoded is None:
633
+ return None
634
+ body, content_type = encoded
635
+ _set_header_default(headers, "Content-Type", content_type)
636
+ elif data is None:
637
+ body = None
638
+ elif isinstance(data, bytes):
639
+ body = data
640
+ elif isinstance(data, (bytearray, memoryview)):
641
+ body = bytes(data)
642
+ elif isinstance(data, str):
643
+ body = data.encode("utf-8")
644
+ _set_header_default(headers, "Content-Type", "text/plain; charset=utf-8")
645
+ elif isinstance(data, Mapping):
646
+ body = urlencode([(str(name), str(value)) for name, value in data.items()], doseq=True).encode()
647
+ _set_header_default(headers, "Content-Type", "application/x-www-form-urlencoded")
648
+ else:
649
+ return None
650
+
651
+ timeouts = _native_timeout_parts(kwargs.get("timeout"))
652
+ if timeouts is None:
653
+ return None
654
+ timeout_seconds, connect_timeout_seconds, response_head_timeout_seconds = timeouts
655
+ return _PreparedNativeRequest(
656
+ url=url,
657
+ headers=headers,
658
+ body=body,
659
+ timeout_seconds=timeout_seconds,
660
+ connect_timeout_seconds=connect_timeout_seconds,
661
+ response_head_timeout_seconds=response_head_timeout_seconds,
662
+ )
663
+
664
+
665
+ def _native_timeout_parts(value: Any) -> tuple[float | None, float | None, float | None] | None:
666
+ if value is None:
667
+ return 60.0, None, None
668
+ if isinstance(value, aiohttp.ClientTimeout):
669
+ total = float(value.total) if value.total is not None else None
670
+ connect = float(value.sock_connect) if value.sock_connect is not None else None
671
+ response_head = float(value.sock_read) if value.sock_read is not None else None
672
+ else:
673
+ try:
674
+ total = float(value)
675
+ except (TypeError, ValueError):
676
+ return None
677
+ connect = None
678
+ response_head = None
679
+ if any(part is not None and part <= 0 for part in (total, connect, response_head)):
680
+ return None
681
+ return total, connect, response_head
682
+
683
+
684
+ def _encode_native_multipart(data: Any, files: Any) -> tuple[bytes, str] | None:
685
+ data_items = _form_items(data)
686
+ file_items = _form_items(files)
687
+ if data_items is None or file_items is None:
688
+ return None
689
+ boundary = f"codex-lb-{secrets.token_hex(16)}"
690
+ chunks: list[bytes] = []
691
+ for name, value in data_items:
692
+ if not isinstance(value, (str, bytes, bytearray, memoryview, int, float, bool)):
693
+ return None
694
+ payload = (
695
+ value.encode()
696
+ if isinstance(value, str)
697
+ else bytes(value)
698
+ if not isinstance(value, (int, float, bool))
699
+ else str(value).encode()
700
+ )
701
+ chunks.extend(
702
+ (
703
+ f"--{boundary}\r\n".encode(),
704
+ f'Content-Disposition: form-data; name="{_multipart_parameter(name)}"\r\n\r\n'.encode(),
705
+ payload,
706
+ b"\r\n",
707
+ )
708
+ )
709
+ for name, value in file_items:
710
+ if not isinstance(value, tuple) or len(value) < 2:
711
+ return None
712
+ filename = str(value[0] or name)
713
+ file_value = value[1]
714
+ content_type = str(value[2]) if len(value) >= 3 and value[2] else "application/octet-stream"
715
+ if isinstance(file_value, str):
716
+ payload = file_value.encode()
717
+ elif isinstance(file_value, (bytes, bytearray, memoryview)):
718
+ payload = bytes(file_value)
719
+ else:
720
+ return None
721
+ chunks.extend(
722
+ (
723
+ f"--{boundary}\r\n".encode(),
724
+ (
725
+ f'Content-Disposition: form-data; name="{_multipart_parameter(name)}"; '
726
+ f'filename="{_multipart_parameter(filename)}"\r\n'
727
+ ).encode(),
728
+ f"Content-Type: {content_type}\r\n\r\n".encode(),
729
+ payload,
730
+ b"\r\n",
731
+ )
732
+ )
733
+ chunks.append(f"--{boundary}--\r\n".encode())
734
+ return b"".join(chunks), f"multipart/form-data; boundary={boundary}"
735
+
736
+
737
+ def _form_items(value: Any) -> list[tuple[str, Any]] | None:
738
+ if value is None:
739
+ return []
740
+ items = value.items() if isinstance(value, Mapping) else value
741
+ try:
742
+ return [(str(name), item) for name, item in items]
743
+ except (TypeError, ValueError):
744
+ return None
745
+
746
+
747
+ def _multipart_parameter(value: str) -> str:
748
+ return value.replace("\\", "\\\\").replace('"', '\\"').replace("\r", "").replace("\n", "")
749
+
750
+
751
+ def _set_header_default(headers: dict[str, str], name: str, value: str) -> None:
752
+ if not any(existing.lower() == name.lower() for existing in headers):
753
+ headers[name] = value
754
+
755
+
756
+ def _prepare_native_websocket_request(
757
+ url: str,
758
+ proxy_url: str,
759
+ kwargs: Mapping[str, Any],
760
+ *,
761
+ interpret_responses: bool = False,
762
+ ) -> NativeWebSocketRequest | None:
763
+ supported = {
764
+ "compress",
765
+ "headers",
766
+ "heartbeat",
767
+ "max_msg_size",
768
+ "protocols",
769
+ "timeout",
770
+ }
771
+ if set(kwargs).difference(supported):
772
+ return None
773
+ raw_headers = kwargs.get("headers") or {}
774
+ if not isinstance(raw_headers, Mapping):
775
+ return None
776
+ headers = {str(name): str(value) for name, value in raw_headers.items()}
777
+ protocols = kwargs.get("protocols")
778
+ if protocols:
779
+ try:
780
+ headers["sec-websocket-protocol"] = ", ".join(str(value) for value in protocols)
781
+ except TypeError:
782
+ return None
783
+ try:
784
+ connect_timeout = float(kwargs.get("timeout", 10.0))
785
+ max_message_bytes = int(kwargs.get("max_msg_size", 4 * 1024 * 1024))
786
+ compress = int(kwargs.get("compress", 0))
787
+ heartbeat = kwargs.get("heartbeat")
788
+ ping_interval_seconds = float(heartbeat) if heartbeat is not None else None
789
+ ping_timeout_seconds = ping_interval_seconds / 2 if ping_interval_seconds is not None else None
790
+ except (TypeError, ValueError):
791
+ return None
792
+ if (
793
+ compress != 15
794
+ or connect_timeout <= 0
795
+ or max_message_bytes <= 0
796
+ or (ping_interval_seconds is not None and ping_interval_seconds <= 0)
797
+ ):
798
+ return None
799
+ return NativeWebSocketRequest(
800
+ url=url,
801
+ headers=headers,
802
+ connect_timeout_seconds=connect_timeout,
803
+ max_message_bytes=max_message_bytes,
804
+ ping_interval_seconds=ping_interval_seconds,
805
+ ping_timeout_seconds=ping_timeout_seconds,
806
+ proxy_url=proxy_url,
807
+ interpret_responses=interpret_responses,
808
+ )
809
+
810
+
811
+ def _add_form_file(form: aiohttp.FormData, name: str, value: Any) -> None:
812
+ if isinstance(value, tuple):
813
+ filename = value[0] if len(value) >= 1 else None
814
+ file_value = value[1] if len(value) >= 2 else b""
815
+ content_type = value[2] if len(value) >= 3 else None
816
+ form.add_field(
817
+ name,
818
+ file_value,
819
+ filename=str(filename) if filename else None,
820
+ content_type=str(content_type) if content_type else None,
821
+ )
822
+ return
823
+ form.add_field(name, value)
824
+
825
+
826
+ async def _buffer_response(response: Any) -> Any:
827
+ body = await _read_response_body(response)
828
+ if body is None:
829
+ return response
830
+ status = _response_status(response)
831
+ headers = getattr(response, "headers", {}) or {}
832
+ return _BufferedResponse(
833
+ status=status,
834
+ status_code=status,
835
+ headers={str(key): str(value) for key, value in headers.items()},
836
+ content=body,
837
+ )
838
+
839
+
840
+ async def _read_response_body(response: Any) -> bytes | None:
841
+ read = getattr(response, "read", None)
842
+ if callable(read):
843
+ result = read()
844
+ if asyncio.iscoroutine(result):
845
+ result = await result
846
+ if isinstance(result, bytes):
847
+ return result
848
+ if isinstance(result, str):
849
+ return result.encode()
850
+ content = getattr(response, "content", None)
851
+ if isinstance(content, bytes):
852
+ return content
853
+ if isinstance(content, str):
854
+ return content.encode()
855
+ return None
856
+
857
+
858
+ def _response_status(response: Any) -> int:
859
+ value = getattr(response, "status", getattr(response, "status_code", 0))
860
+ return int(value or 0)
861
+
862
+
863
+ def _reject_credentialed_plaintext_target(url: str, endpoints: tuple[ResolvedProxyEndpoint, ...]) -> None:
864
+ # Proxy credentials ride in a Proxy-Authorization header (never URL
865
+ # userinfo, which aiohttp reprs into ConnectionKey/ClientHttpProxyError).
866
+ # aiohttp only forwards proxy_headers on the CONNECT tunnel, so a
867
+ # credentialed endpoint requires a TLS target. Checked once for the whole
868
+ # ordered pool, ahead of every transport branch and before any dispatch,
869
+ # so a credential-free fallback cannot quietly absorb a misconfigured
870
+ # primary. Raised as a connect-phase transport error so callers map it to
871
+ # the usual upstream-unavailable response instead of an unhandled failure.
872
+ if any(endpoint.username for endpoint in endpoints) and URL(url).scheme not in _TLS_TARGET_SCHEMES:
873
+ raise CodexTransportError(
874
+ "credentialed aiohttp proxy routes require an https/wss upstream target",
875
+ failure_phase="connect",
876
+ retryable_same_contract=False,
877
+ )
878
+
879
+
880
+ def _reject_reserved(kwargs: Mapping[str, Any]) -> None:
881
+ forbidden = sorted(_RESERVED.intersection(kwargs))
882
+ if forbidden:
883
+ raise ValueError(f"Codex upstream route/TLS kwargs are controlled centrally: {', '.join(forbidden)}")
884
+
885
+
886
+ def _is_idempotent_method(method: str) -> bool:
887
+ return method.upper() in {"GET", "HEAD", "OPTIONS", "TRACE"}
888
+
889
+
890
+ def _transport_error(
891
+ operation: str,
892
+ endpoint_id: str,
893
+ exc: Exception,
894
+ *,
895
+ failure_phase: str,
896
+ retryable_same_contract: bool,
897
+ ) -> CodexTransportError:
898
+ process_network_failure = is_process_network_failure(exc, include_permanent_dns=False)
899
+ return CodexTransportError(
900
+ codex_transport_error_message(operation, endpoint_id, exc),
901
+ status_code=_transport_error_status_code(exc),
902
+ # A missing proxy hostname is endpoint configuration, not evidence
903
+ # that the host lost DNS. Transient DNS and local route loss remain
904
+ # process-wide failures and are safe to classify without exposing the
905
+ # credential-bearing original exception.
906
+ error_code=PROCESS_NETWORK_UNAVAILABLE_CODE if process_network_failure else None,
907
+ retryable_same_contract=retryable_same_contract,
908
+ failure_phase=failure_phase,
909
+ is_tls_verification_failure=isinstance(exc, aiohttp.ClientSSLError),
910
+ handshake_headers=_transport_error_handshake_headers(exc),
911
+ handshake_message=_transport_error_handshake_message(exc),
912
+ )
913
+
914
+
915
+ def _transport_error_handshake_headers(exc: Exception) -> Mapping[str, str] | None:
916
+ """Handshake response headers of a wrapped upgrade rejection, else ``None``.
917
+
918
+ ``aiohttp.WSServerHandshakeError`` carries the response headers directly;
919
+ exceptions wrapping a response object carry them one level down.
920
+ """
921
+
922
+ response = getattr(exc, "response", None)
923
+ for source in (exc, response):
924
+ if source is None:
925
+ continue
926
+ headers = getattr(source, "headers", None)
927
+ if headers is not None:
928
+ return headers
929
+ return None
930
+
931
+
932
+ def _transport_error_handshake_message(exc: Exception) -> str | None:
933
+ message = getattr(exc, "message", None)
934
+ return message if isinstance(message, str) else None
935
+
936
+
937
+ def _native_transport_error(
938
+ operation: str,
939
+ endpoint_id: str,
940
+ exc: NativeEgressError,
941
+ ) -> CodexTransportError:
942
+ if isinstance(exc, NativeEgressTransportError):
943
+ process_failure = exc.failure_phase in {
944
+ "helper_exit",
945
+ "helper_read",
946
+ "helper_write",
947
+ "shutdown",
948
+ }
949
+ return CodexTransportError(
950
+ codex_transport_error_message(operation, endpoint_id, exc),
951
+ status_code=exc.status_code,
952
+ error_code=PROCESS_NETWORK_UNAVAILABLE_CODE if process_failure else None,
953
+ retryable_same_contract=(exc.retryable_same_contract and not exc.is_tls_verification_failure),
954
+ failure_phase=exc.failure_phase,
955
+ is_tls_verification_failure=exc.is_tls_verification_failure,
956
+ handshake_headers=dict(exc.headers) if exc.headers else None,
957
+ handshake_message=(exc.body.decode("utf-8", errors="replace") if exc.body else None),
958
+ )
959
+ return CodexTransportError(
960
+ codex_transport_error_message(operation, endpoint_id, exc),
961
+ retryable_same_contract=False,
962
+ failure_phase="protocol",
963
+ )
964
+
965
+
966
+ def _without_same_contract_retry(exc: CodexTransportError) -> CodexTransportError:
967
+ return CodexTransportError(
968
+ str(exc),
969
+ status_code=exc.status_code,
970
+ error_code=exc.error_code,
971
+ retryable_same_contract=False,
972
+ failure_phase=exc.failure_phase,
973
+ is_tls_verification_failure=exc.is_tls_verification_failure,
974
+ handshake_headers=exc.handshake_headers,
975
+ handshake_message=exc.handshake_message,
976
+ )
977
+
978
+
979
+ def _transport_error_status_code(exc: Exception) -> int | None:
980
+ response = getattr(exc, "response", None)
981
+ for source in (response, exc):
982
+ if source is None:
983
+ continue
984
+ value = getattr(source, "status_code", getattr(source, "status", None))
985
+ if value is None:
986
+ continue
987
+ try:
988
+ return int(value)
989
+ except (TypeError, ValueError):
990
+ continue
991
+ return None
992
+
993
+
994
+ def codex_transport_error_message(operation: str, endpoint_id: str | None, exc: Exception) -> str:
995
+ endpoint = endpoint_id or "unknown"
996
+ return f"Codex upstream {operation} failed via proxy endpoint {endpoint}: {type(exc).__name__}"