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,1304 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import base64
5
+ import contextlib
6
+ import json
7
+ import logging
8
+ import math
9
+ import os
10
+ import shutil
11
+ from collections.abc import AsyncGenerator, AsyncIterator, Mapping
12
+ from dataclasses import dataclass
13
+ from functools import lru_cache
14
+ from pathlib import Path
15
+ from typing import Protocol, cast
16
+
17
+ from multidict import CIMultiDict
18
+
19
+ from app.core.clients.stream_errors import StreamEventTooLargeError, StreamIdleTimeoutError
20
+ from app.core.types import JsonValue
21
+ from app.core.utils.shared_future import _await_cleanup_deferring_cancellation
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ _NATIVE_EGRESS_EXECUTABLE = "codex-lb-native-egress"
26
+ _NATIVE_PROTOCOL_VERSION = 1
27
+ _NATIVE_PROTOCOL_HANDSHAKE_TIMEOUT_SECONDS = 2.0
28
+ _REQUIRED_NATIVE_CAPABILITIES = frozenset(
29
+ {
30
+ "failure_provenance_v1",
31
+ "http",
32
+ "http2_profile_v1",
33
+ "http_compact_collect_v1",
34
+ "http_compact_sse_v1",
35
+ "http_sse_v1",
36
+ "http_responses_events_v1",
37
+ "http_responses_completion_v1",
38
+ "websocket",
39
+ "websocket_responses_events_v1",
40
+ "websocket_responses_routing_v1",
41
+ "websocket_send_ack",
42
+ }
43
+ )
44
+ _NATIVE_EVENT_LINE_LIMIT = 24 * 1024 * 1024
45
+ # Per-request bound between the helper reader (one task draining the helper's
46
+ # stdout for every in-flight request) and each request's consumer. The bound
47
+ # is a *byte budget* with a generous event cap: framed SSE deltas are tens of
48
+ # bytes each and arrive in bursts of hundreds when upstream flushes reasoning
49
+ # output, while a non-SSE body (an image generation JSON) is a handful of
50
+ # large chunks. Either shape must fit while a healthy consumer is merely late
51
+ # for a scheduling turn on a saturated event loop; only a consumer that truly
52
+ # stops draining (dead client) trips the bound and is failed so it cannot hold
53
+ # the shared reader hostage.
54
+ _NATIVE_STREAM_QUEUE_LIMIT = 4096
55
+ _NATIVE_STREAM_QUEUE_BYTES_LIMIT = 32 * 1024 * 1024
56
+ _NATIVE_WEBSOCKET_MESSAGE_QUEUE_LIMIT = 64
57
+ _NATIVE_CANCEL_TIMEOUT_SECONDS = 2.0
58
+ _NATIVE_WEBSOCKET_COMMAND_TIMEOUT_SECONDS = 30.0
59
+
60
+
61
+ def _event_payload_size(item: object) -> int:
62
+ """Queued payload bytes of one helper event: its base64 ``data`` (ASCII, so
63
+ the string length is the byte length) or UTF-8 SSE text and type metadata."""
64
+ if not isinstance(item, dict):
65
+ return 0
66
+ data = item.get("data")
67
+ if isinstance(data, str):
68
+ return len(data)
69
+ text = item.get("text")
70
+ if isinstance(text, str):
71
+ kind = item.get("event_type")
72
+ metadata_size = len(kind.encode("utf-8")) if isinstance(kind, str) else 0
73
+ response_id = item.get("payload_response_id")
74
+ if isinstance(response_id, str):
75
+ metadata_size += len(response_id.encode("utf-8", errors="surrogatepass"))
76
+ sequence = item.get("sequence_number")
77
+ if isinstance(sequence, int):
78
+ metadata_size += len(str(sequence))
79
+ # Responses WebSocket IPC embeds the original JSON a second time as
80
+ # payload. Charge both copies without reserializing the decoded object.
81
+ copies = 2 if item.get("type") == "websocket_responses_text" else 1
82
+ return copies * len(text.encode("utf-8")) + metadata_size
83
+ return 0
84
+
85
+
86
+ class _BoundedEventQueue(asyncio.Queue[dict[str, object] | BaseException]):
87
+ """``asyncio.Queue`` whose ``full()`` also trips on a queued-bytes budget.
88
+
89
+ ``put_nowait`` consults ``full()`` before enqueueing, so the reader's
90
+ existing ``QueueFull`` handling covers both the event cap and the byte
91
+ budget without any change to the put/get call sites.
92
+ """
93
+
94
+ def __init__(self, *, max_events: int, max_bytes: int) -> None:
95
+ super().__init__(maxsize=max_events)
96
+ self._max_bytes = max_bytes
97
+ self.queued_bytes = 0
98
+
99
+ def put_nowait(self, item: dict[str, object] | BaseException) -> None:
100
+ # The byte budget is enforced against the projected total, so a queue
101
+ # just under budget rejects an event that would carry it past, while a
102
+ # zero-byte event (``end``/``error``/``cancelled`` or a failure object)
103
+ # is always accepted as long as the event cap has room so a complete
104
+ # response is never discarded at the boundary. An event arriving at an
105
+ # empty queue is always accepted (the SSE event size cap bounds it
106
+ # separately) so a lone large chunk is never a failure.
107
+ size = _event_payload_size(item)
108
+ if size and not self.empty() and self.queued_bytes + size > self._max_bytes:
109
+ raise asyncio.QueueFull
110
+ super().put_nowait(item)
111
+
112
+ def _put(self, item: dict[str, object] | BaseException) -> None:
113
+ super()._put(item)
114
+ self.queued_bytes += _event_payload_size(item)
115
+
116
+ def _get(self) -> dict[str, object] | BaseException:
117
+ item = super()._get()
118
+ self.queued_bytes -= _event_payload_size(item)
119
+ return item
120
+
121
+
122
+ def _new_stream_queue() -> _BoundedEventQueue:
123
+ return _BoundedEventQueue(max_events=_NATIVE_STREAM_QUEUE_LIMIT, max_bytes=_NATIVE_STREAM_QUEUE_BYTES_LIMIT)
124
+
125
+
126
+ class NativeEgressError(Exception):
127
+ """Base error for the optional native Codex egress boundary."""
128
+
129
+
130
+ class NativeEgressUnavailable(NativeEgressError):
131
+ """Raised when the optional native helper is not installed."""
132
+
133
+
134
+ class NativeEgressProtocolError(NativeEgressError):
135
+ """Raised when the helper violates the framed stream contract."""
136
+
137
+
138
+ class NativeEgressTransportError(NativeEgressError):
139
+ """Raised when the helper cannot complete the upstream request."""
140
+
141
+ def __init__(
142
+ self,
143
+ message: str,
144
+ *,
145
+ failure_phase: str = "request",
146
+ retryable_same_contract: bool = False,
147
+ is_tls_verification_failure: bool = False,
148
+ status_code: int | None = None,
149
+ headers: tuple[tuple[str, str], ...] = (),
150
+ body: bytes | None = None,
151
+ ) -> None:
152
+ super().__init__(message)
153
+ self.failure_phase = failure_phase
154
+ self.retryable_same_contract = retryable_same_contract
155
+ self.is_tls_verification_failure = is_tls_verification_failure
156
+ self.status_code = status_code
157
+ self.headers = headers
158
+ self.body = body
159
+
160
+
161
+ @dataclass(frozen=True, slots=True)
162
+ class NativeSseOptions:
163
+ idle_timeout_seconds: float
164
+ max_event_bytes: int
165
+ content_type_aware: bool = False
166
+ collect_compact: bool = False
167
+ interpret_responses: bool = False
168
+
169
+
170
+ @dataclass(frozen=True, slots=True)
171
+ class NativeEgressRequest:
172
+ method: str
173
+ url: str
174
+ headers: Mapping[str, str]
175
+ body: bytes | None = None
176
+ timeout_seconds: float | None = 60.0
177
+ connect_timeout_seconds: float | None = None
178
+ response_head_timeout_seconds: float | None = None
179
+ proxy_url: str | None = None
180
+ sse: NativeSseOptions | None = None
181
+
182
+
183
+ @dataclass(frozen=True, slots=True)
184
+ class NativeWebSocketRequest:
185
+ url: str
186
+ headers: Mapping[str, str]
187
+ connect_timeout_seconds: float
188
+ max_message_bytes: int
189
+ ping_interval_seconds: float | None = 20.0
190
+ ping_timeout_seconds: float | None = None
191
+ proxy_url: str | None = None
192
+ interpret_responses: bool = False
193
+
194
+
195
+ @dataclass(frozen=True, slots=True)
196
+ class NativeWebSocketRoutingMetadata:
197
+ payload_response_id: str | None
198
+ sequence_number: int | None
199
+
200
+
201
+ @dataclass(frozen=True, slots=True)
202
+ class NativeWebSocketMessage:
203
+ kind: str
204
+ text: str | None = None
205
+ data: bytes | None = None
206
+ close_code: int | None = None
207
+ close_reason: str | None = None
208
+ responses_interpreted: bool = False
209
+ event_type: str | None = None
210
+ payload: dict[str, JsonValue] | None = None
211
+ routing: NativeWebSocketRoutingMetadata | None = None
212
+
213
+
214
+ class NativeEgressClient(Protocol):
215
+ async def request(self, request: NativeEgressRequest) -> NativeEgressResponse: ...
216
+
217
+ async def websocket(self, request: NativeWebSocketRequest) -> NativeEgressWebSocket: ...
218
+
219
+
220
+ class NativeResponsesEvent(str):
221
+ """A complete SSE block with interpretation metadata from the Rust owner."""
222
+
223
+ event_type: str | None
224
+ python_normalization: bool
225
+
226
+ def __new__(cls, text: str, event_type: str | None, python_normalization: bool) -> NativeResponsesEvent:
227
+ block = super().__new__(cls, text)
228
+ block.event_type = event_type
229
+ block.python_normalization = python_normalization
230
+ return block
231
+
232
+
233
+ class NativeEgressResponse:
234
+ """One response stream multiplexed through a persistent native helper."""
235
+
236
+ reason: str | None = None
237
+
238
+ def __init__(
239
+ self,
240
+ *,
241
+ status: int,
242
+ http_version: str,
243
+ headers: tuple[tuple[str, str], ...],
244
+ client: SubprocessNativeEgressClient,
245
+ request_id: str,
246
+ generation: int,
247
+ events: asyncio.Queue[dict[str, object] | BaseException],
248
+ sse_framed: bool = False,
249
+ compact_collected: bool = False,
250
+ responses_interpreted: bool = False,
251
+ ) -> None:
252
+ self.status = status
253
+ self.http_version = http_version
254
+ self.raw_headers = headers
255
+ self.headers: CIMultiDict[str] = CIMultiDict(headers)
256
+ self.content = _NativeEgressContent(self)
257
+ self.sse_framed = sse_framed
258
+ self.compact_collected = compact_collected
259
+ self.responses_interpreted = responses_interpreted
260
+ self._client = client
261
+ self._request_id = request_id
262
+ self._generation = generation
263
+ self._events = events
264
+ self._iterated = False
265
+ self._completed = False
266
+ self._body_cache: bytes | None = None
267
+
268
+ async def iter_bytes(self) -> AsyncIterator[bytes]:
269
+ if self.sse_framed:
270
+ raise NativeEgressProtocolError("native SSE response requires framed consumption")
271
+ async with contextlib.aclosing(self._iter_body_events("chunk")) as events:
272
+ async for event in events:
273
+ encoded = event.get("data")
274
+ if not isinstance(encoded, str):
275
+ raise NativeEgressProtocolError("native chunk is missing base64 data")
276
+ try:
277
+ yield base64.b64decode(encoded, validate=True)
278
+ except ValueError as exc:
279
+ raise NativeEgressProtocolError("native chunk contains invalid base64 data") from exc
280
+
281
+ async def iter_sse_events(self) -> AsyncGenerator[str, None]:
282
+ if not self.sse_framed or self.compact_collected:
283
+ raise NativeEgressProtocolError("native response did not request SSE framing")
284
+ event_kind = "responses_event" if self.responses_interpreted else "sse"
285
+ async with contextlib.aclosing(self._iter_text_results(event_kind)) as results:
286
+ async for text in results:
287
+ yield text
288
+
289
+ async def compact_result(self) -> object:
290
+ if not self.compact_collected:
291
+ raise NativeEgressProtocolError("native response did not request compact collection")
292
+ result: object = None
293
+ received = False
294
+ async with contextlib.aclosing(self._iter_text_results("compact")) as results:
295
+ async for text in results:
296
+ if received:
297
+ raise NativeEgressProtocolError("native compact response contains multiple results")
298
+ try:
299
+ result = json.loads(text)
300
+ except ValueError as exc:
301
+ raise NativeEgressProtocolError("native compact result is invalid JSON") from exc
302
+ received = True
303
+ if not received:
304
+ raise NativeEgressProtocolError("native compact response ended without a result")
305
+ return result
306
+
307
+ async def _iter_text_results(self, event_type: str) -> AsyncGenerator[str, None]:
308
+ fragments: list[str] = []
309
+ async with contextlib.aclosing(self._iter_body_events(event_type)) as events:
310
+ async for event in events:
311
+ text = event.get("text")
312
+ more = event.get("more")
313
+ if not isinstance(text, str) or type(more) is not bool:
314
+ raise NativeEgressProtocolError(f"native {event_type} fragment has an invalid shape")
315
+ if event_type in {"compact", "responses_event"} and len(text.encode("utf-8")) > 16 * 1024:
316
+ raise NativeEgressProtocolError(f"native {event_type} fragment exceeds the text limit")
317
+ if event_type == "responses_event":
318
+ kind = event.get("event_type")
319
+ python_normalization = event.get("python_normalization")
320
+ stream_complete = event.get("stream_complete", False)
321
+ if (
322
+ "event_type" not in event
323
+ or (kind is not None and not isinstance(kind, str))
324
+ or (isinstance(kind, str) and len(kind.encode("utf-8")) > 16 * 1024)
325
+ or type(python_normalization) is not bool
326
+ or type(stream_complete) is not bool
327
+ or (more and (kind is not None or python_normalization))
328
+ or (
329
+ stream_complete
330
+ and (more or kind not in {"response.completed", "response.failed", "response.incomplete"})
331
+ )
332
+ ):
333
+ raise NativeEgressProtocolError("native Responses event has invalid metadata")
334
+ if not more:
335
+ text = "".join(fragments) + text
336
+ fragments.clear()
337
+ if stream_complete:
338
+ self._completed = True
339
+ self._client._finish_request(self._request_id, self._generation, self._events)
340
+ yield NativeResponsesEvent(text, kind, python_normalization)
341
+ if stream_complete:
342
+ return
343
+ continue
344
+ if more:
345
+ fragments.append(text)
346
+ elif fragments:
347
+ fragments.append(text)
348
+ block = "".join(fragments)
349
+ fragments.clear()
350
+ yield block
351
+ else:
352
+ yield text
353
+ if fragments:
354
+ raise NativeEgressProtocolError(f"native {event_type} response ended during an event")
355
+
356
+ async def _iter_body_events(self, expected_type: str) -> AsyncGenerator[dict[str, object], None]:
357
+ if self._iterated:
358
+ raise NativeEgressProtocolError("native response body can only be consumed once")
359
+ self._iterated = True
360
+ try:
361
+ while True:
362
+ item = await self._events.get()
363
+ if isinstance(item, BaseException):
364
+ self._completed = True
365
+ self._client._finish_request(self._request_id, self._generation, self._events)
366
+ raise item
367
+ event = item
368
+ event_type = event.get("type")
369
+ if event_type == expected_type:
370
+ yield event
371
+ continue
372
+ if event_type == "end":
373
+ self._completed = True
374
+ self._client._finish_request(self._request_id, self._generation, self._events)
375
+ return
376
+ if event_type == "error":
377
+ self._completed = True
378
+ self._client._finish_request(self._request_id, self._generation, self._events)
379
+ if self.sse_framed and event.get("failure_phase") == "stream_idle_timeout":
380
+ raise StreamIdleTimeoutError()
381
+ raise _transport_error_from_event(event)
382
+ if self.sse_framed and event_type == "sse_event_too_large":
383
+ size = event.get("size_bytes")
384
+ limit = event.get("limit_bytes")
385
+ if type(size) is not int or type(limit) is not int or not 0 < limit < size:
386
+ raise NativeEgressProtocolError("native SSE size failure has an invalid shape")
387
+ self._completed = True
388
+ self._client._finish_request(self._request_id, self._generation, self._events)
389
+ raise StreamEventTooLargeError(size, limit)
390
+ if event_type == "cancelled":
391
+ self._completed = True
392
+ self._client._finish_request(self._request_id, self._generation, self._events)
393
+ raise NativeEgressTransportError(
394
+ "native request was cancelled",
395
+ failure_phase="cancelled",
396
+ )
397
+ raise NativeEgressProtocolError(f"unexpected native stream event: {event_type!r}")
398
+ finally:
399
+ if not self._completed:
400
+ await self.aclose()
401
+
402
+ async def read(self) -> bytes:
403
+ if self._body_cache is None:
404
+ self._body_cache = b"".join([chunk async for chunk in self.iter_bytes()])
405
+ return self._body_cache
406
+
407
+ async def text(self, *, encoding: str = "utf-8", errors: str = "replace") -> str:
408
+ return (await self.read()).decode(encoding, errors=errors)
409
+
410
+ async def json(self, *, content_type: str | None = None) -> object:
411
+ del content_type
412
+ return json.loads(await self.read())
413
+
414
+ async def aclose(self) -> None:
415
+ if self._completed:
416
+ return
417
+ self._completed = True
418
+ cancellation = await _await_cleanup_deferring_cancellation(
419
+ self._client._cancel_request(self._request_id, self._generation, self._events)
420
+ )
421
+ if cancellation is not None:
422
+ raise cancellation
423
+
424
+ async def __aenter__(self) -> NativeEgressResponse:
425
+ return self
426
+
427
+ async def __aexit__(self, *_exc_info: object) -> None:
428
+ await self.aclose()
429
+
430
+
431
+ class NativeEgressWebSocket:
432
+ """Bidirectional WebSocket multiplexed through one native helper stream."""
433
+
434
+ def __init__(
435
+ self,
436
+ *,
437
+ status: int,
438
+ headers: tuple[tuple[str, str], ...],
439
+ client: SubprocessNativeEgressClient,
440
+ process: asyncio.subprocess.Process,
441
+ request_id: str,
442
+ generation: int,
443
+ events: asyncio.Queue[dict[str, object] | BaseException],
444
+ ) -> None:
445
+ self.status = status
446
+ self.raw_headers = headers
447
+ self.headers: CIMultiDict[str] = CIMultiDict(headers)
448
+ self._client = client
449
+ self._process = process
450
+ self._request_id = request_id
451
+ self._generation = generation
452
+ self._events = events
453
+ self._messages: asyncio.Queue[NativeWebSocketMessage | BaseException] = asyncio.Queue(
454
+ maxsize=_NATIVE_WEBSOCKET_MESSAGE_QUEUE_LIMIT
455
+ )
456
+ self._pending: dict[str, asyncio.Future[None]] = {}
457
+ self._command_sequence = 0
458
+ self._completed = False
459
+ self._closing = False
460
+ self._remote_closed = False
461
+ self._terminal_failure: BaseException | None = None
462
+ self._pump_task = asyncio.create_task(
463
+ self._pump(),
464
+ name=f"native-websocket-pump-{request_id}",
465
+ )
466
+
467
+ async def send_text(self, text: str) -> None:
468
+ await self._send("websocket_send_text", text=text)
469
+
470
+ async def send_bytes(self, data: bytes) -> None:
471
+ await self._send(
472
+ "websocket_send_binary",
473
+ data=base64.b64encode(data).decode("ascii"),
474
+ )
475
+
476
+ async def receive(self) -> NativeWebSocketMessage:
477
+ if self._completed and self._messages.empty():
478
+ raise self._terminal_failure or NativeEgressTransportError(
479
+ "native websocket is closed",
480
+ failure_phase="websocket_receive",
481
+ )
482
+ item = await self._messages.get()
483
+ if isinstance(item, BaseException):
484
+ raise item
485
+ return item
486
+
487
+ async def close(self, code: int = 1000, reason: str = "") -> None:
488
+ if self._completed or self._closing:
489
+ return
490
+ self._closing = True
491
+ try:
492
+ try:
493
+ await self._send(
494
+ "websocket_close",
495
+ allow_closing=True,
496
+ code=code,
497
+ reason=reason,
498
+ )
499
+ await asyncio.wait_for(
500
+ asyncio.shield(self._pump_task),
501
+ timeout=_NATIVE_CANCEL_TIMEOUT_SECONDS,
502
+ )
503
+ except NativeEgressTransportError:
504
+ # A peer close can race with a local close command. The helper
505
+ # removes the socket immediately after forwarding the peer's
506
+ # close frame, so the command may receive "not active" even
507
+ # though the connection already ended cleanly.
508
+ if self._remote_closed:
509
+ return
510
+ if not self._pump_task.done():
511
+ self._pump_task.cancel()
512
+ with contextlib.suppress(asyncio.CancelledError):
513
+ await self._pump_task
514
+ await self._client._cancel_request(
515
+ self._request_id,
516
+ self._generation,
517
+ self._events,
518
+ )
519
+ raise
520
+ except BaseException:
521
+ if not self._pump_task.done():
522
+ self._pump_task.cancel()
523
+ with contextlib.suppress(asyncio.CancelledError):
524
+ await self._pump_task
525
+ await self._client._cancel_request(
526
+ self._request_id,
527
+ self._generation,
528
+ self._events,
529
+ )
530
+ raise
531
+ finally:
532
+ if not self._pump_task.done():
533
+ self._pump_task.cancel()
534
+ with contextlib.suppress(asyncio.CancelledError):
535
+ await self._pump_task
536
+ self._finish()
537
+
538
+ def response_header(self, name: str) -> str | None:
539
+ return self.headers.get(name)
540
+
541
+ async def _send(self, event_type: str, *, allow_closing: bool = False, **payload: object) -> None:
542
+ if self._completed or (self._closing and not allow_closing):
543
+ raise NativeEgressTransportError(
544
+ "native websocket is closed",
545
+ failure_phase="websocket_send",
546
+ )
547
+ self._command_sequence += 1
548
+ command_id = f"{self._request_id}:{self._command_sequence}"
549
+ future = asyncio.get_running_loop().create_future()
550
+ self._pending[command_id] = future
551
+ try:
552
+ await self._client._send_command(
553
+ self._process,
554
+ self._generation,
555
+ {
556
+ "type": event_type,
557
+ "request_id": self._request_id,
558
+ "command_id": command_id,
559
+ **payload,
560
+ },
561
+ )
562
+ await asyncio.wait_for(
563
+ asyncio.shield(future),
564
+ timeout=_NATIVE_WEBSOCKET_COMMAND_TIMEOUT_SECONDS,
565
+ )
566
+ except TimeoutError as exc:
567
+ raise NativeEgressTransportError(
568
+ "native websocket send acknowledgement timed out",
569
+ failure_phase="websocket_send",
570
+ ) from exc
571
+ finally:
572
+ self._pending.pop(command_id, None)
573
+
574
+ async def _pump(self) -> None:
575
+ terminal_failure: BaseException | None = None
576
+ try:
577
+ while True:
578
+ item = await self._events.get()
579
+ if isinstance(item, BaseException):
580
+ terminal_failure = item
581
+ return
582
+ event_type = item.get("type")
583
+ if event_type == "websocket_sent":
584
+ command_id = item.get("command_id")
585
+ if not isinstance(command_id, str):
586
+ raise NativeEgressProtocolError("native websocket acknowledgement is missing command_id")
587
+ future = self._pending.get(command_id)
588
+ if future is not None and not future.done():
589
+ future.set_result(None)
590
+ continue
591
+ if event_type == "websocket_text":
592
+ text = item.get("text")
593
+ if not isinstance(text, str):
594
+ raise NativeEgressProtocolError("native websocket text event is invalid")
595
+ self._queue_message(NativeWebSocketMessage(kind="text", text=text))
596
+ continue
597
+ if event_type == "websocket_responses_text":
598
+ text = item.get("text")
599
+ kind = item.get("event_type")
600
+ payload = item.get("payload")
601
+ response_id = item.get("payload_response_id")
602
+ sequence = item.get("sequence_number")
603
+ if (
604
+ not isinstance(text, str)
605
+ or "event_type" not in item
606
+ or (kind is not None and not isinstance(kind, str))
607
+ or not isinstance(payload, dict)
608
+ or "payload_response_id" not in item
609
+ or (response_id is not None and not isinstance(response_id, str))
610
+ or "sequence_number" not in item
611
+ or (sequence is not None and type(sequence) is not int)
612
+ ):
613
+ raise NativeEgressProtocolError("native Responses websocket event is invalid")
614
+ self._queue_message(
615
+ NativeWebSocketMessage(
616
+ kind="text",
617
+ text=text,
618
+ responses_interpreted=True,
619
+ event_type=kind,
620
+ payload=cast(dict[str, JsonValue], payload),
621
+ routing=NativeWebSocketRoutingMetadata(response_id, sequence),
622
+ )
623
+ )
624
+ continue
625
+ if event_type == "websocket_binary":
626
+ encoded = item.get("data")
627
+ if not isinstance(encoded, str):
628
+ raise NativeEgressProtocolError("native websocket binary event is invalid")
629
+ try:
630
+ data = base64.b64decode(encoded, validate=True)
631
+ except ValueError as exc:
632
+ raise NativeEgressProtocolError("native websocket binary event is not base64") from exc
633
+ self._queue_message(NativeWebSocketMessage(kind="binary", data=data))
634
+ continue
635
+ if event_type == "websocket_close":
636
+ code = item.get("code")
637
+ reason = item.get("reason")
638
+ if code is not None and not isinstance(code, int):
639
+ raise NativeEgressProtocolError("native websocket close code is invalid")
640
+ if reason is not None and not isinstance(reason, str):
641
+ raise NativeEgressProtocolError("native websocket close reason is invalid")
642
+ self._remote_closed = True
643
+ self._queue_message(
644
+ NativeWebSocketMessage(
645
+ kind="close",
646
+ close_code=code,
647
+ close_reason=reason,
648
+ )
649
+ )
650
+ terminal_failure = NativeEgressTransportError(
651
+ "native websocket closed",
652
+ failure_phase="websocket_receive",
653
+ )
654
+ return
655
+ if event_type == "websocket_error":
656
+ terminal_failure = _websocket_error_from_event(item)
657
+ return
658
+ if event_type == "cancelled":
659
+ terminal_failure = NativeEgressTransportError(
660
+ "native websocket was cancelled",
661
+ failure_phase="cancelled",
662
+ )
663
+ return
664
+ raise NativeEgressProtocolError(f"unexpected native websocket event: {event_type!r}")
665
+ except asyncio.CancelledError:
666
+ terminal_failure = NativeEgressTransportError(
667
+ "native websocket was closed locally",
668
+ failure_phase="cancelled",
669
+ )
670
+ raise
671
+ except BaseException as exc:
672
+ terminal_failure = exc
673
+ await self._client._abort_request(
674
+ self._request_id,
675
+ self._generation,
676
+ self._events,
677
+ )
678
+ finally:
679
+ if terminal_failure is None and not self._closing:
680
+ terminal_failure = NativeEgressTransportError(
681
+ "native websocket closed",
682
+ failure_phase="websocket_receive",
683
+ )
684
+ if terminal_failure is not None:
685
+ self._fail_pending(terminal_failure)
686
+ self._queue_terminal(terminal_failure)
687
+ self._finish()
688
+
689
+ def _queue_message(self, message: NativeWebSocketMessage) -> None:
690
+ try:
691
+ self._messages.put_nowait(message)
692
+ except asyncio.QueueFull as exc:
693
+ while not self._messages.empty():
694
+ with contextlib.suppress(asyncio.QueueEmpty):
695
+ self._messages.get_nowait()
696
+ raise NativeEgressTransportError(
697
+ "native websocket consumer exceeded the bounded message queue",
698
+ failure_phase="consumer_backpressure",
699
+ ) from exc
700
+
701
+ def _queue_terminal(self, failure: BaseException) -> None:
702
+ self._terminal_failure = failure
703
+ with contextlib.suppress(asyncio.QueueFull):
704
+ self._messages.put_nowait(failure)
705
+
706
+ def _fail_pending(self, failure: BaseException) -> None:
707
+ for future in tuple(self._pending.values()):
708
+ if not future.done():
709
+ future.set_exception(failure)
710
+
711
+ def _finish(self) -> None:
712
+ if self._completed:
713
+ return
714
+ self._completed = True
715
+ self._client._finish_request(
716
+ self._request_id,
717
+ self._generation,
718
+ self._events,
719
+ )
720
+
721
+
722
+ class _NativeEgressContent:
723
+ def __init__(self, response: NativeEgressResponse) -> None:
724
+ self._response = response
725
+
726
+ def iter_chunked(self, size: int) -> AsyncIterator[bytes]:
727
+ del size
728
+ return self._response.iter_bytes()
729
+
730
+
731
+ class SubprocessNativeEgressClient:
732
+ """Persistent multiplexed adapter for the pinned Rust Codex helper.
733
+
734
+ The helper is deliberately discovered by an explicit executable path. Merely
735
+ importing codex-lb never builds, installs, or requires a Rust component.
736
+ """
737
+
738
+ def __init__(self, executable: str | os.PathLike[str]) -> None:
739
+ self.executable = Path(executable)
740
+ self._process: asyncio.subprocess.Process | None = None
741
+ self._reader_task: asyncio.Task[None] | None = None
742
+ self._generation = 0
743
+ self._request_sequence = 0
744
+ self._streams: dict[
745
+ str,
746
+ tuple[int, asyncio.Queue[dict[str, object] | BaseException]],
747
+ ] = {}
748
+ self._start_lock = asyncio.Lock()
749
+ self._write_lock = asyncio.Lock()
750
+ self._closed = False
751
+
752
+ @property
753
+ def available(self) -> bool:
754
+ return self.executable.is_file() and os.access(self.executable, os.X_OK)
755
+
756
+ async def request(self, request: NativeEgressRequest) -> NativeEgressResponse:
757
+ if not self.available:
758
+ raise NativeEgressUnavailable(f"native egress helper is unavailable: {self.executable}")
759
+ if request.timeout_seconds is not None and request.timeout_seconds <= 0:
760
+ raise ValueError("native egress timeout_seconds must be positive")
761
+ if request.connect_timeout_seconds is not None and request.connect_timeout_seconds <= 0:
762
+ raise ValueError("native egress connect_timeout_seconds must be positive")
763
+ if request.response_head_timeout_seconds is not None and request.response_head_timeout_seconds <= 0:
764
+ raise ValueError("native egress response_head_timeout_seconds must be positive")
765
+ if request.sse is not None:
766
+ if request.sse.collect_compact and request.sse.interpret_responses:
767
+ raise ValueError("native compact collection cannot interpret streaming Responses")
768
+ if request.sse.collect_compact and not request.sse.content_type_aware:
769
+ raise ValueError("native compact collection requires content-type-aware framing")
770
+ if not math.isfinite(request.sse.idle_timeout_seconds) or request.sse.idle_timeout_seconds <= 0:
771
+ raise ValueError("native SSE idle_timeout_seconds must be finite and positive")
772
+ if type(request.sse.max_event_bytes) is not int or request.sse.max_event_bytes <= 0:
773
+ raise ValueError("native SSE max_event_bytes must be a positive integer")
774
+
775
+ process, generation = await self._ensure_process()
776
+ self._request_sequence += 1
777
+ request_id = f"{generation}:{self._request_sequence}"
778
+ events: asyncio.Queue[dict[str, object] | BaseException] = _new_stream_queue()
779
+ self._streams[request_id] = (generation, events)
780
+ request_event = {
781
+ "type": "request",
782
+ "request_id": request_id,
783
+ "method": request.method,
784
+ "url": request.url,
785
+ "headers": list(request.headers.items()),
786
+ "body": base64.b64encode(request.body).decode("ascii") if request.body is not None else None,
787
+ "timeout_ms": (
788
+ max(1, round(request.timeout_seconds * 1000)) if request.timeout_seconds is not None else None
789
+ ),
790
+ "connect_timeout_ms": (
791
+ max(1, round(request.connect_timeout_seconds * 1000))
792
+ if request.connect_timeout_seconds is not None
793
+ else None
794
+ ),
795
+ "proxy_url": request.proxy_url,
796
+ "sse": (
797
+ {
798
+ "idle_timeout_ms": max(1, round(request.sse.idle_timeout_seconds * 1000)),
799
+ "max_event_bytes": request.sse.max_event_bytes,
800
+ "content_type_aware": request.sse.content_type_aware,
801
+ "collect_compact": request.sse.collect_compact,
802
+ "interpret_responses": request.sse.interpret_responses,
803
+ }
804
+ if request.sse is not None
805
+ else None
806
+ ),
807
+ }
808
+ try:
809
+ await self._send_command(process, generation, request_event)
810
+ head_timeout = request.response_head_timeout_seconds or request.timeout_seconds
811
+ if request.timeout_seconds is not None and head_timeout is not None:
812
+ head_timeout = min(head_timeout, request.timeout_seconds)
813
+ item = await asyncio.wait_for(events.get(), timeout=head_timeout)
814
+ if isinstance(item, BaseException):
815
+ self._finish_request(request_id, generation, events)
816
+ raise item
817
+ head = item
818
+ if head.get("type") == "error":
819
+ self._finish_request(request_id, generation, events)
820
+ raise _transport_error_from_event(head)
821
+ if head.get("type") != "head":
822
+ raise NativeEgressProtocolError("native response did not begin with a head event")
823
+ status = head.get("status")
824
+ http_version = head.get("http_version")
825
+ raw_headers = head.get("headers")
826
+ if not isinstance(status, int) or not isinstance(http_version, str) or not isinstance(raw_headers, list):
827
+ raise NativeEgressProtocolError("native response head has an invalid shape")
828
+ headers = tuple(_decode_header_pair(pair) for pair in raw_headers)
829
+ content_type = next((value for name, value in headers if name.lower() == "content-type"), "")
830
+ sse_framed = (
831
+ request.sse is not None
832
+ and status < 400
833
+ and (
834
+ not request.sse.content_type_aware
835
+ or not content_type
836
+ or content_type.partition(";")[0].strip().lower() == "text/event-stream"
837
+ )
838
+ )
839
+ except TimeoutError as exc:
840
+ cancellation = await _await_cleanup_deferring_cancellation(
841
+ self._cancel_request(request_id, generation, events)
842
+ )
843
+ if cancellation is not None:
844
+ raise cancellation
845
+ raise NativeEgressTransportError(
846
+ "native upstream response head timed out",
847
+ failure_phase="timeout",
848
+ ) from exc
849
+ except BaseException:
850
+ cancellation = await _await_cleanup_deferring_cancellation(
851
+ self._cancel_request(request_id, generation, events)
852
+ )
853
+ if cancellation is not None:
854
+ raise cancellation
855
+ raise
856
+
857
+ return NativeEgressResponse(
858
+ status=status,
859
+ http_version=http_version,
860
+ headers=headers,
861
+ client=self,
862
+ request_id=request_id,
863
+ generation=generation,
864
+ events=events,
865
+ sse_framed=sse_framed,
866
+ compact_collected=sse_framed and request.sse is not None and request.sse.collect_compact,
867
+ responses_interpreted=sse_framed and request.sse is not None and request.sse.interpret_responses,
868
+ )
869
+
870
+ async def websocket(self, request: NativeWebSocketRequest) -> NativeEgressWebSocket:
871
+ if not self.available:
872
+ raise NativeEgressUnavailable(f"native egress helper is unavailable: {self.executable}")
873
+ if request.connect_timeout_seconds <= 0:
874
+ raise ValueError("native websocket connect_timeout_seconds must be positive")
875
+ if request.max_message_bytes <= 0:
876
+ raise ValueError("native websocket max_message_bytes must be positive")
877
+ if request.ping_interval_seconds is not None and request.ping_interval_seconds <= 0:
878
+ raise ValueError("native websocket ping_interval_seconds must be positive")
879
+ if request.ping_timeout_seconds is not None and request.ping_timeout_seconds <= 0:
880
+ raise ValueError("native websocket ping_timeout_seconds must be positive")
881
+
882
+ process, generation = await self._ensure_process()
883
+ self._request_sequence += 1
884
+ request_id = f"{generation}:{self._request_sequence}"
885
+ events: asyncio.Queue[dict[str, object] | BaseException] = _new_stream_queue()
886
+ self._streams[request_id] = (generation, events)
887
+ try:
888
+ await self._send_command(
889
+ process,
890
+ generation,
891
+ {
892
+ "type": "websocket_connect",
893
+ "request_id": request_id,
894
+ "url": request.url,
895
+ "headers": list(request.headers.items()),
896
+ "connect_timeout_ms": max(1, round(request.connect_timeout_seconds * 1000)),
897
+ "max_message_bytes": request.max_message_bytes,
898
+ "ping_interval_ms": (
899
+ max(1, round(request.ping_interval_seconds * 1000))
900
+ if request.ping_interval_seconds is not None
901
+ else None
902
+ ),
903
+ "ping_timeout_ms": (
904
+ max(1, round(request.ping_timeout_seconds * 1000))
905
+ if request.ping_timeout_seconds is not None
906
+ else None
907
+ ),
908
+ "proxy_url": request.proxy_url,
909
+ "interpret_responses": request.interpret_responses,
910
+ },
911
+ )
912
+ item = await asyncio.wait_for(events.get(), timeout=request.connect_timeout_seconds)
913
+ if isinstance(item, BaseException):
914
+ raise item
915
+ if item.get("type") == "websocket_error":
916
+ raise _websocket_error_from_event(item)
917
+ if item.get("type") != "websocket_open":
918
+ raise NativeEgressProtocolError("native websocket did not begin with an open event")
919
+ status = item.get("status")
920
+ raw_headers = item.get("headers")
921
+ if not isinstance(status, int) or not isinstance(raw_headers, list):
922
+ raise NativeEgressProtocolError("native websocket open event has an invalid shape")
923
+ headers = tuple(_decode_header_pair(pair) for pair in raw_headers)
924
+ except TimeoutError as exc:
925
+ await self._cancel_request(request_id, generation, events)
926
+ raise NativeEgressTransportError(
927
+ "native websocket handshake timed out",
928
+ failure_phase="connect",
929
+ retryable_same_contract=True,
930
+ ) from exc
931
+ except BaseException:
932
+ await self._cancel_request(request_id, generation, events)
933
+ raise
934
+
935
+ return NativeEgressWebSocket(
936
+ status=status,
937
+ headers=headers,
938
+ client=self,
939
+ process=process,
940
+ request_id=request_id,
941
+ generation=generation,
942
+ events=events,
943
+ )
944
+
945
+ async def aclose(self) -> None:
946
+ """Stop the current helper generation and fail any remaining streams."""
947
+
948
+ async with self._start_lock:
949
+ if self._closed:
950
+ return
951
+ self._closed = True
952
+ process = self._process
953
+ reader_task = self._reader_task
954
+ generation = self._generation
955
+ if process is not None and process.stdin is not None:
956
+ process.stdin.close()
957
+ with contextlib.suppress(BrokenPipeError, ConnectionResetError):
958
+ await process.stdin.wait_closed()
959
+ if process is not None:
960
+ try:
961
+ await asyncio.wait_for(process.wait(), timeout=_NATIVE_CANCEL_TIMEOUT_SECONDS)
962
+ except TimeoutError:
963
+ await _stop_process(process)
964
+ if reader_task is not None and reader_task is not asyncio.current_task():
965
+ if not reader_task.done():
966
+ reader_task.cancel()
967
+ with contextlib.suppress(asyncio.CancelledError, NativeEgressError):
968
+ await reader_task
969
+ self._fail_generation(
970
+ generation,
971
+ NativeEgressTransportError("native helper closed", failure_phase="shutdown"),
972
+ )
973
+ self._process = None
974
+ self._reader_task = None
975
+
976
+ async def _ensure_process(self) -> tuple[asyncio.subprocess.Process, int]:
977
+ async with self._start_lock:
978
+ if self._closed:
979
+ raise NativeEgressUnavailable("native egress client is closed")
980
+ if self._process is not None and self._process.returncode is None:
981
+ return self._process, self._generation
982
+ try:
983
+ process = await asyncio.create_subprocess_exec(
984
+ str(self.executable),
985
+ stdin=asyncio.subprocess.PIPE,
986
+ stdout=asyncio.subprocess.PIPE,
987
+ stderr=asyncio.subprocess.DEVNULL,
988
+ limit=_NATIVE_EVENT_LINE_LIMIT,
989
+ )
990
+ except OSError as exc:
991
+ raise NativeEgressUnavailable(f"native egress helper could not start: {self.executable}") from exc
992
+ if process.stdin is None or process.stdout is None:
993
+ await _stop_process(process)
994
+ raise NativeEgressUnavailable("native helper pipes are unavailable")
995
+ try:
996
+ await self._negotiate_process(process)
997
+ except BaseException:
998
+ await _stop_process(process)
999
+ raise
1000
+ self._generation += 1
1001
+ generation = self._generation
1002
+ self._process = process
1003
+ self._reader_task = asyncio.create_task(
1004
+ self._read_process(process, generation),
1005
+ name=f"native-egress-reader-{generation}",
1006
+ )
1007
+ return process, generation
1008
+
1009
+ async def _negotiate_process(self, process: asyncio.subprocess.Process) -> None:
1010
+ """Fail closed before dispatch when the installed worker is incompatible."""
1011
+
1012
+ stdin = process.stdin
1013
+ stdout = process.stdout
1014
+ if stdin is None or stdout is None:
1015
+ raise NativeEgressProtocolError("native helper handshake pipes are unavailable")
1016
+ hello = {
1017
+ "type": "client_hello",
1018
+ "min_protocol_version": _NATIVE_PROTOCOL_VERSION,
1019
+ "max_protocol_version": _NATIVE_PROTOCOL_VERSION,
1020
+ }
1021
+ try:
1022
+ stdin.write(json.dumps(hello, separators=(",", ":")).encode("utf-8") + b"\n")
1023
+ await stdin.drain()
1024
+ event = await asyncio.wait_for(
1025
+ _read_event(stdout),
1026
+ timeout=_NATIVE_PROTOCOL_HANDSHAKE_TIMEOUT_SECONDS,
1027
+ )
1028
+ except TimeoutError as exc:
1029
+ raise NativeEgressProtocolError("native helper protocol handshake timed out") from exc
1030
+ except (BrokenPipeError, ConnectionResetError) as exc:
1031
+ raise NativeEgressProtocolError("native helper closed during protocol handshake") from exc
1032
+
1033
+ if event.get("type") != "server_hello":
1034
+ raise NativeEgressProtocolError("native helper did not acknowledge the protocol handshake")
1035
+ version = event.get("protocol_version")
1036
+ if version != _NATIVE_PROTOCOL_VERSION:
1037
+ raise NativeEgressProtocolError(f"native helper selected unsupported protocol version: {version!r}")
1038
+ capabilities = event.get("capabilities")
1039
+ if not isinstance(capabilities, list) or not all(isinstance(value, str) for value in capabilities):
1040
+ raise NativeEgressProtocolError("native helper capabilities have an invalid shape")
1041
+ missing = sorted(_REQUIRED_NATIVE_CAPABILITIES.difference(capabilities))
1042
+ if missing:
1043
+ raise NativeEgressProtocolError(f"native helper is missing required capabilities: {', '.join(missing)}")
1044
+
1045
+ async def _send_command(
1046
+ self,
1047
+ process: asyncio.subprocess.Process,
1048
+ generation: int,
1049
+ command: Mapping[str, object],
1050
+ ) -> None:
1051
+ async with self._write_lock:
1052
+ if process is not self._process or generation != self._generation or process.returncode is not None:
1053
+ raise NativeEgressTransportError("native helper exited before command dispatch")
1054
+ stdin = process.stdin
1055
+ if stdin is None:
1056
+ raise NativeEgressTransportError("native helper stdin is unavailable")
1057
+ try:
1058
+ stdin.write(json.dumps(command, separators=(",", ":")).encode("utf-8") + b"\n")
1059
+ await stdin.drain()
1060
+ except (BrokenPipeError, ConnectionResetError) as exc:
1061
+ raise NativeEgressTransportError("native helper closed during command dispatch") from exc
1062
+
1063
+ async def _read_process(self, process: asyncio.subprocess.Process, generation: int) -> None:
1064
+ stdout = process.stdout
1065
+ if stdout is None:
1066
+ self._fail_generation(generation, NativeEgressProtocolError("native helper stdout is unavailable"))
1067
+ return
1068
+ failure: NativeEgressError = NativeEgressTransportError(
1069
+ "native helper exited with active requests",
1070
+ failure_phase="helper_exit",
1071
+ )
1072
+ try:
1073
+ while True:
1074
+ event = await _read_event(stdout)
1075
+ request_id = event.get("request_id")
1076
+ if not isinstance(request_id, str):
1077
+ raise NativeEgressProtocolError("native helper event is missing request_id")
1078
+ state = self._streams.get(request_id)
1079
+ if state is None or state[0] != generation:
1080
+ continue
1081
+ events = state[1]
1082
+ try:
1083
+ events.put_nowait(event)
1084
+ # readline() need not suspend for buffered helper output.
1085
+ # Let ready consumers drain before dispatching another event;
1086
+ # a stalled consumer still hits the bounded queue.
1087
+ await asyncio.sleep(0)
1088
+ except asyncio.QueueFull:
1089
+ overflow_failure = NativeEgressTransportError(
1090
+ "native stream consumer exceeded the bounded event queue",
1091
+ failure_phase="consumer_backpressure",
1092
+ )
1093
+ self._finish_request(request_id, generation, events)
1094
+ while not events.empty():
1095
+ with contextlib.suppress(asyncio.QueueEmpty):
1096
+ events.get_nowait()
1097
+ events.put_nowait(overflow_failure)
1098
+ asyncio.create_task(
1099
+ self._cancel_orphaned_request(process, generation, request_id),
1100
+ name=f"native-egress-overflow-cancel-{request_id}",
1101
+ )
1102
+ except NativeEgressProtocolError as exc:
1103
+ failure = exc
1104
+ except BaseException as exc:
1105
+ if isinstance(exc, asyncio.CancelledError):
1106
+ raise
1107
+ failure = NativeEgressTransportError(
1108
+ "native helper reader failed",
1109
+ failure_phase="helper_read",
1110
+ )
1111
+ finally:
1112
+ self._fail_generation(generation, failure)
1113
+ if process.returncode is None:
1114
+ await _stop_process(process)
1115
+
1116
+ async def _cancel_request(
1117
+ self,
1118
+ request_id: str,
1119
+ generation: int,
1120
+ events: asyncio.Queue[dict[str, object] | BaseException],
1121
+ ) -> None:
1122
+ state = self._streams.get(request_id)
1123
+ if state != (generation, events):
1124
+ return
1125
+ process = self._process
1126
+ if process is not None and generation == self._generation and process.returncode is None:
1127
+ try:
1128
+ await self._send_command(
1129
+ process,
1130
+ generation,
1131
+ {"type": "cancel", "request_id": request_id},
1132
+ )
1133
+ while True:
1134
+ item = await asyncio.wait_for(events.get(), timeout=_NATIVE_CANCEL_TIMEOUT_SECONDS)
1135
+ if isinstance(item, BaseException) or item.get("type") in {
1136
+ "cancelled",
1137
+ "end",
1138
+ "error",
1139
+ "sse_event_too_large",
1140
+ }:
1141
+ break
1142
+ except (TimeoutError, NativeEgressError):
1143
+ pass
1144
+ self._finish_request(request_id, generation, events)
1145
+
1146
+ async def _cancel_orphaned_request(
1147
+ self,
1148
+ process: asyncio.subprocess.Process,
1149
+ generation: int,
1150
+ request_id: str,
1151
+ ) -> None:
1152
+ with contextlib.suppress(NativeEgressError):
1153
+ await self._send_command(
1154
+ process,
1155
+ generation,
1156
+ {"type": "cancel", "request_id": request_id},
1157
+ )
1158
+
1159
+ async def _abort_request(
1160
+ self,
1161
+ request_id: str,
1162
+ generation: int,
1163
+ events: asyncio.Queue[dict[str, object] | BaseException],
1164
+ ) -> None:
1165
+ """Best-effort cancel when the stream protocol itself is unusable."""
1166
+
1167
+ state = self._streams.get(request_id)
1168
+ if state != (generation, events):
1169
+ return
1170
+ process = self._process
1171
+ if process is not None and generation == self._generation and process.returncode is None:
1172
+ with contextlib.suppress(NativeEgressError):
1173
+ await self._send_command(
1174
+ process,
1175
+ generation,
1176
+ {"type": "cancel", "request_id": request_id},
1177
+ )
1178
+ self._finish_request(request_id, generation, events)
1179
+
1180
+ def _finish_request(
1181
+ self,
1182
+ request_id: str,
1183
+ generation: int,
1184
+ events: asyncio.Queue[dict[str, object] | BaseException],
1185
+ ) -> None:
1186
+ if self._streams.get(request_id) == (generation, events):
1187
+ self._streams.pop(request_id, None)
1188
+
1189
+ def _fail_generation(self, generation: int, failure: BaseException) -> None:
1190
+ if self._generation == generation:
1191
+ self._process = None
1192
+ self._reader_task = None
1193
+ failed = [
1194
+ (request_id, events)
1195
+ for request_id, (stream_generation, events) in self._streams.items()
1196
+ if stream_generation == generation
1197
+ ]
1198
+ for request_id, events in failed:
1199
+ self._streams.pop(request_id, None)
1200
+ while not events.empty():
1201
+ with contextlib.suppress(asyncio.QueueEmpty):
1202
+ events.get_nowait()
1203
+ with contextlib.suppress(asyncio.QueueFull):
1204
+ events.put_nowait(failure)
1205
+
1206
+
1207
+ @lru_cache(maxsize=1)
1208
+ def discover_native_egress_client() -> SubprocessNativeEgressClient | None:
1209
+ """Resolve only the fixed packaged helper from the runtime PATH."""
1210
+
1211
+ executable = shutil.which(_NATIVE_EGRESS_EXECUTABLE)
1212
+ if executable is None:
1213
+ return None
1214
+ client = SubprocessNativeEgressClient(executable)
1215
+ if not client.available:
1216
+ return None
1217
+ logger.info("Native Codex HTTP egress enabled executable=%s", executable)
1218
+ return client
1219
+
1220
+
1221
+ async def close_discovered_native_egress_client() -> None:
1222
+ """Close the cached helper generation, then allow clean rediscovery."""
1223
+
1224
+ client = discover_native_egress_client()
1225
+ if client is not None:
1226
+ await client.aclose()
1227
+ discover_native_egress_client.cache_clear()
1228
+
1229
+
1230
+ def _decode_header_pair(value: object) -> tuple[str, str]:
1231
+ if not isinstance(value, list) or len(value) != 2 or not all(isinstance(item, str) for item in value):
1232
+ raise NativeEgressProtocolError("native response contains an invalid header pair")
1233
+ pair = cast(list[str], value)
1234
+ return pair[0], pair[1]
1235
+
1236
+
1237
+ def _transport_error_from_event(event: Mapping[str, object]) -> NativeEgressTransportError:
1238
+ message = event.get("message")
1239
+ failure_phase = event.get("failure_phase")
1240
+ retryable_same_contract = event.get("retryable_same_contract")
1241
+ is_tls_verification_failure = event.get("is_tls_verification_failure")
1242
+ return NativeEgressTransportError(
1243
+ message if isinstance(message, str) else "native request failed",
1244
+ failure_phase=failure_phase if isinstance(failure_phase, str) else "request",
1245
+ retryable_same_contract=retryable_same_contract if isinstance(retryable_same_contract, bool) else False,
1246
+ is_tls_verification_failure=(
1247
+ is_tls_verification_failure if isinstance(is_tls_verification_failure, bool) else False
1248
+ ),
1249
+ )
1250
+
1251
+
1252
+ def _websocket_error_from_event(event: Mapping[str, object]) -> NativeEgressTransportError:
1253
+ message = event.get("message")
1254
+ failure_phase = event.get("failure_phase")
1255
+ retryable_same_contract = event.get("retryable_same_contract")
1256
+ is_tls_verification_failure = event.get("is_tls_verification_failure")
1257
+ status = event.get("status")
1258
+ raw_headers = event.get("headers")
1259
+ encoded_body = event.get("body")
1260
+ headers: tuple[tuple[str, str], ...] = ()
1261
+ if isinstance(raw_headers, list):
1262
+ headers = tuple(_decode_header_pair(pair) for pair in raw_headers)
1263
+ body: bytes | None = None
1264
+ if isinstance(encoded_body, str):
1265
+ try:
1266
+ body = base64.b64decode(encoded_body, validate=True)
1267
+ except ValueError as exc:
1268
+ raise NativeEgressProtocolError("native websocket error body is not base64") from exc
1269
+ return NativeEgressTransportError(
1270
+ message if isinstance(message, str) else "native websocket failed",
1271
+ failure_phase=failure_phase if isinstance(failure_phase, str) else "websocket",
1272
+ retryable_same_contract=retryable_same_contract if isinstance(retryable_same_contract, bool) else False,
1273
+ is_tls_verification_failure=(
1274
+ is_tls_verification_failure if isinstance(is_tls_verification_failure, bool) else False
1275
+ ),
1276
+ status_code=status if isinstance(status, int) else None,
1277
+ headers=headers,
1278
+ body=body,
1279
+ )
1280
+
1281
+
1282
+ async def _read_event(stdout: asyncio.StreamReader) -> dict[str, object]:
1283
+ line = await stdout.readline()
1284
+ if not line:
1285
+ raise NativeEgressProtocolError("native helper closed its stream unexpectedly")
1286
+ try:
1287
+ value = json.loads(line)
1288
+ except (UnicodeDecodeError, json.JSONDecodeError) as exc:
1289
+ raise NativeEgressProtocolError("native helper emitted invalid JSON") from exc
1290
+ if not isinstance(value, dict):
1291
+ raise NativeEgressProtocolError("native helper event must be a JSON object")
1292
+ return cast(dict[str, object], value)
1293
+
1294
+
1295
+ async def _stop_process(process: asyncio.subprocess.Process) -> None:
1296
+ if process.returncode is not None:
1297
+ return
1298
+ with contextlib.suppress(ProcessLookupError):
1299
+ process.terminate()
1300
+ try:
1301
+ await asyncio.wait_for(process.wait(), timeout=1.0)
1302
+ except TimeoutError:
1303
+ process.kill()
1304
+ await process.wait()