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,797 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import hashlib
5
+ import hmac
6
+ import json
7
+ from collections.abc import AsyncIterator, Callable, Mapping
8
+ from dataclasses import dataclass
9
+ from typing import cast
10
+
11
+ import aiohttp
12
+
13
+ from app.core.clients.proxy import ProxyResponseError, filter_inbound_headers
14
+ from app.core.clock import REAL_CLOCK, REAL_SCHEDULER, Clock, Scheduler
15
+ from app.core.config.dashboard_overrides import with_dashboard_overrides
16
+ from app.core.config.settings import get_settings
17
+ from app.core.crypto import get_or_create_key
18
+ from app.core.errors import OpenAIErrorEnvelope, openai_error, response_failed_event
19
+ from app.core.openai.requests import ResponsesRequest, extract_input_file_ids
20
+ from app.core.types import JsonObject
21
+ from app.core.utils.json_guards import is_json_mapping
22
+ from app.core.utils.request_id import get_request_id
23
+ from app.core.utils.sse import format_sse_event
24
+ from app.modules.api_keys.service import ApiKeyUsageReservationData
25
+ from app.modules.proxy._service.http_bridge.helpers import _http_bridge_request_budget_seconds
26
+
27
+ # HTTP-only and hop-by-hop headers that must not be forwarded through the
28
+ # internal bridge. These headers are either illegal in WebSocket handshakes or
29
+ # carry HTTP framing semantics that the aiohttp upstream session manages itself.
30
+ # Applies on top of filter_inbound_headers (which already strips authorization,
31
+ # host, content-length, and x-forwarded-* / cf-* headers).
32
+ _BRIDGE_UNSAFE_HEADER_NAMES = frozenset(
33
+ {
34
+ "accept",
35
+ "accept-encoding",
36
+ "connection",
37
+ "content-type",
38
+ "cookie",
39
+ "keep-alive",
40
+ "te",
41
+ "trailer",
42
+ "transfer-encoding",
43
+ "upgrade",
44
+ }
45
+ )
46
+ _OWNER_FORWARD_SKIP_AUTO_HEADERS = frozenset({aiohttp.hdrs.ACCEPT, aiohttp.hdrs.ACCEPT_ENCODING})
47
+ _LEGACY_SIGNATURE_DELIMITER = "|"
48
+
49
+ HTTP_BRIDGE_INTERNAL_FORWARD_PATH = "/internal/bridge/responses"
50
+ HTTP_BRIDGE_FORWARDED_HEADER = "x-codex-bridge-forwarded"
51
+ HTTP_BRIDGE_ORIGIN_INSTANCE_HEADER = "x-codex-bridge-origin-instance"
52
+ HTTP_BRIDGE_TARGET_INSTANCE_HEADER = "x-codex-bridge-target-instance"
53
+ HTTP_BRIDGE_CODEX_AFFINITY_HEADER = "x-codex-bridge-codex-session-affinity"
54
+ HTTP_BRIDGE_RESERVATION_ID_HEADER = "x-codex-bridge-reservation-id"
55
+ HTTP_BRIDGE_RESERVATION_KEY_ID_HEADER = "x-codex-bridge-reservation-key-id"
56
+ HTTP_BRIDGE_RESERVATION_MODEL_HEADER = "x-codex-bridge-reservation-model"
57
+ HTTP_BRIDGE_AFFINITY_KIND_HEADER = "x-codex-bridge-affinity-kind"
58
+ HTTP_BRIDGE_AFFINITY_KEY_HEADER = "x-codex-bridge-affinity-key"
59
+ HTTP_BRIDGE_FILE_OWNER_HEADER = "x-codex-bridge-file-owner"
60
+ HTTP_BRIDGE_ORIGINAL_UNANCHORED_HEADER = "x-codex-bridge-original-unanchored"
61
+ HTTP_BRIDGE_SIGNATURE_VERSION_HEADER = "x-codex-bridge-signature-version"
62
+ HTTP_BRIDGE_CLIENT_IP_HEADER = "x-codex-bridge-client-ip"
63
+ HTTP_BRIDGE_CLIENT_IP_SIGNATURE_HEADER = "x-codex-bridge-client-ip-signature"
64
+ HTTP_BRIDGE_SIGNATURE_HEADER = "x-codex-bridge-signature"
65
+ # Additive tamper-proofing header (#1203): a second signature bound to the
66
+ # exact forwarding body (``model_dump_for_forwarding``) that is posted, so an
67
+ # in-transit rewrite injecting ``"tools": []`` is detected even though the
68
+ # primary signature hashes a plain ``model_dump`` that synthesizes the same
69
+ # empty list. Orthogonal to ``x-codex-bridge-signature-version`` below (which
70
+ # domain-separates the *primary* signature for unanchored parallel requests,
71
+ # #1169): this header carries its own full-context structured signature. Kept
72
+ # as a one-release rolling-upgrade shim alongside the legacy primary
73
+ # signature; see the ROLLOUT SHIM notes in ``build_owner_forward_headers`` and
74
+ # ``parse_forwarded_request``.
75
+ HTTP_BRIDGE_SIGNATURE_V2_HEADER = "x-codex-bridge-signature-v2"
76
+ _HTTP_BRIDGE_SIGNATURE_VERSION_V2 = "2"
77
+ # Response header (owner -> origin) carrying ``ProxyResponseError``
78
+ # provenance back across the forward hop. The owner's error body is an
79
+ # ordinary OpenAI envelope, so a refusal the owner raised before it sent any
80
+ # upstream frame is indistinguishable by code from a transport failure it
81
+ # observed: both end as ``stream_incomplete``. Without this marker the origin
82
+ # rebuilds the error without the provenance and its own native Codex
83
+ # transport-failure lifecycle aborts the committed body, moving issue #2364's
84
+ # empty 200 from the owner onto the origin.
85
+ HTTP_BRIDGE_LOCAL_PRE_DISPATCH_REFUSAL_HEADER = "x-codex-bridge-local-pre-dispatch-refusal"
86
+
87
+
88
+ @dataclass(frozen=True, slots=True)
89
+ class HTTPBridgeForwardContext:
90
+ origin_instance: str
91
+ target_instance: str
92
+ codex_session_affinity: bool
93
+ downstream_turn_state: str | None
94
+ original_request_unanchored: bool = False
95
+ original_affinity_kind: str | None = None
96
+ original_affinity_key: str | None = None
97
+ file_owner_account_id: str | None = None
98
+ client_ip: str | None = None
99
+ reservation: ApiKeyUsageReservationData | None = None
100
+ signature_version: str | None = None
101
+
102
+
103
+ @dataclass(frozen=True, slots=True)
104
+ class HTTPBridgeForwardedRequest:
105
+ context: HTTPBridgeForwardContext
106
+
107
+
108
+ @dataclass(frozen=True, slots=True)
109
+ class _OwnerForwardReceiveTimeout:
110
+ timeout_seconds: float
111
+ error_code: str
112
+ error_message: str
113
+
114
+
115
+ class _OwnerForwardStreamTimeoutError(Exception):
116
+ def __init__(self, *, error_code: str, error_message: str) -> None:
117
+ super().__init__(error_message)
118
+ self.error_code = error_code
119
+ self.error_message = error_message
120
+
121
+
122
+ @dataclass(frozen=True, slots=True)
123
+ class OwnerForwardRelayFailure(Exception):
124
+ event_block: str
125
+
126
+
127
+ def _bridge_header_name_has_illegal_control_char(name: str) -> bool:
128
+ return any(ord(char) < 32 or ord(char) == 127 for char in name)
129
+
130
+
131
+ def _bridge_header_value_has_illegal_control_char(value: str) -> bool:
132
+ return any((ord(char) < 32 and char != "\t") or ord(char) == 127 for char in value)
133
+
134
+
135
+ def _bridge_header_has_illegal_control_char(name: str, value: str) -> bool:
136
+ """Return whether reconstructed metadata is unsafe as an HTTP header."""
137
+
138
+ return _bridge_header_name_has_illegal_control_char(name) or _bridge_header_value_has_illegal_control_char(value)
139
+
140
+
141
+ def _reject_illegal_bridge_header_value(name: str, value: str | None) -> None:
142
+ if value is None or not _bridge_header_has_illegal_control_char(name, value):
143
+ return
144
+ raise ProxyResponseError(
145
+ 400,
146
+ openai_error(
147
+ "bridge_forward_invalid",
148
+ "Internal bridge forward metadata is not safe to forward",
149
+ error_type="invalid_request_error",
150
+ ),
151
+ )
152
+
153
+
154
+ def _validate_bridge_forward_context_headers(context: HTTPBridgeForwardContext) -> None:
155
+ for name, value in (
156
+ (HTTP_BRIDGE_ORIGIN_INSTANCE_HEADER, context.origin_instance),
157
+ (HTTP_BRIDGE_TARGET_INSTANCE_HEADER, context.target_instance),
158
+ ("x-codex-turn-state", context.downstream_turn_state),
159
+ (HTTP_BRIDGE_AFFINITY_KIND_HEADER, context.original_affinity_kind),
160
+ (HTTP_BRIDGE_AFFINITY_KEY_HEADER, context.original_affinity_key),
161
+ (HTTP_BRIDGE_FILE_OWNER_HEADER, context.file_owner_account_id),
162
+ (HTTP_BRIDGE_CLIENT_IP_HEADER, context.client_ip),
163
+ ):
164
+ _reject_illegal_bridge_header_value(name, value)
165
+ if context.reservation is None:
166
+ return
167
+ for name, value in (
168
+ (HTTP_BRIDGE_RESERVATION_ID_HEADER, context.reservation.reservation_id),
169
+ (HTTP_BRIDGE_RESERVATION_KEY_ID_HEADER, context.reservation.key_id),
170
+ (HTTP_BRIDGE_RESERVATION_MODEL_HEADER, context.reservation.model),
171
+ ):
172
+ _reject_illegal_bridge_header_value(name, value)
173
+
174
+
175
+ class HTTPBridgeOwnerClient:
176
+ async def stream_responses(
177
+ self,
178
+ *,
179
+ owner_endpoint: str,
180
+ payload: ResponsesRequest,
181
+ headers: Mapping[str, str],
182
+ context: HTTPBridgeForwardContext,
183
+ request_started_at: float,
184
+ on_request_dispatched: Callable[[], None] | None = None,
185
+ on_response_rejected: Callable[[], None] | None = None,
186
+ on_response_wait: Callable[[], None] | None = None,
187
+ on_response_ready: Callable[[], None] | None = None,
188
+ scheduler: Scheduler = REAL_SCHEDULER,
189
+ clock: Clock = REAL_CLOCK,
190
+ ) -> AsyncIterator[str]:
191
+ settings = with_dashboard_overrides(get_settings())
192
+ timeout = _owner_forward_timeout(
193
+ connect_timeout_seconds=settings.upstream_connect_timeout_seconds,
194
+ idle_timeout_seconds=settings.stream_idle_timeout_seconds,
195
+ )
196
+ if on_response_wait is not None:
197
+ on_response_wait()
198
+ async with aiohttp.ClientSession(timeout=timeout, trust_env=False) as session:
199
+ request_url = f"{owner_endpoint}{HTTP_BRIDGE_INTERNAL_FORWARD_PATH}"
200
+ request_payload = payload.model_dump_for_forwarding()
201
+ request_headers = build_owner_forward_headers(headers=headers, payload=payload, context=context)
202
+ request_context = session.post(
203
+ request_url,
204
+ json=request_payload,
205
+ headers=request_headers,
206
+ skip_auto_headers=_OWNER_FORWARD_SKIP_AUTO_HEADERS,
207
+ )
208
+ # I/O begins when __aenter__ is awaited. Cancellation after that
209
+ # point can leave the receiver settling the reservation.
210
+ transport_started = False
211
+ observed_status = False
212
+ try:
213
+ transport_started = True
214
+ async with request_context as response:
215
+ observed_status = True
216
+ if response.status != 200:
217
+ if on_response_rejected is not None:
218
+ # The receiver contract never transfers cleanup on a
219
+ # non-200 response, so the origin may safely release.
220
+ on_response_rejected()
221
+ payload_text = await response.text()
222
+ raise ProxyResponseError(
223
+ response.status,
224
+ _owner_forward_error_payload(status_code=response.status, payload_text=payload_text),
225
+ failure_phase="owner_forward_status",
226
+ failure_detail="owner_forward_non_200",
227
+ upstream_status_code=response.status,
228
+ # Rebuilding the error from the owner's body alone
229
+ # would drop the provenance the owner attached, so
230
+ # carry it over the hop: a non-200 by itself does
231
+ # not prove the owner sent no upstream frame, and
232
+ # only the owner knows which of its failures was a
233
+ # local pre-dispatch refusal (issue #2364).
234
+ local_pre_dispatch_refusal=_bool_header(
235
+ response.headers.get(HTTP_BRIDGE_LOCAL_PRE_DISPATCH_REFUSAL_HEADER)
236
+ ),
237
+ )
238
+ if on_response_ready is not None:
239
+ on_response_ready()
240
+ yielded_event = False
241
+ try:
242
+ async for event_block in _iter_sse_event_blocks(
243
+ response,
244
+ request_started_at=request_started_at,
245
+ proxy_request_budget_seconds=_http_bridge_request_budget_seconds(settings),
246
+ stream_idle_timeout_seconds=settings.stream_idle_timeout_seconds,
247
+ scheduler=scheduler,
248
+ clock=clock,
249
+ ):
250
+ yielded_event = True
251
+ yield event_block
252
+ except _OwnerForwardStreamTimeoutError as exc:
253
+ raise OwnerForwardRelayFailure(
254
+ format_sse_event(
255
+ response_failed_event(
256
+ exc.error_code,
257
+ exc.error_message,
258
+ response_id=get_request_id(),
259
+ )
260
+ )
261
+ )
262
+ if not yielded_event:
263
+ yield format_sse_event(
264
+ response_failed_event(
265
+ "stream_incomplete",
266
+ "Upstream websocket closed before response.completed",
267
+ response_id=get_request_id(),
268
+ )
269
+ )
270
+ except aiohttp.ClientConnectorError:
271
+ # DNS/connect refusal never delivered the reservation.
272
+ raise
273
+ except asyncio.CancelledError:
274
+ if transport_started and not observed_status and on_request_dispatched is not None:
275
+ on_request_dispatched()
276
+ raise
277
+ except (aiohttp.ClientError, asyncio.TimeoutError):
278
+ if not observed_status and on_request_dispatched is not None:
279
+ # The request left local construction and may have reached
280
+ # the owner; origin must not release or replay.
281
+ on_request_dispatched()
282
+ raise
283
+
284
+
285
+ def build_owner_forward_headers(
286
+ *,
287
+ headers: Mapping[str, str],
288
+ payload: ResponsesRequest,
289
+ context: HTTPBridgeForwardContext,
290
+ ) -> dict[str, str]:
291
+ # WebSocket client metadata is reconstructed as a header mapping without
292
+ # passing through an HTTP parser. Validate it before signing so aiohttp is
293
+ # never the first component to discover illegal wire bytes.
294
+ _validate_bridge_forward_context_headers(context)
295
+ filtered = filter_inbound_headers(headers)
296
+ # Per the hop-by-hop contract, also drop any header named by the inbound
297
+ # Connection header in addition to the fixed unsafe set.
298
+ connection_value = next(
299
+ (value for key, value in headers.items() if key.lower() == "connection"),
300
+ "",
301
+ )
302
+ connection_named = {token.strip().lower() for token in connection_value.split(",") if token.strip()}
303
+ drop = _BRIDGE_UNSAFE_HEADER_NAMES | connection_named
304
+ # Drop any client-supplied ``x-codex-bridge-*`` header: those names are
305
+ # reserved for the internal forward contract and are set below from the
306
+ # trusted context. Relaying unknown bridge headers verbatim would let an
307
+ # external client plant a spoofed ``x-codex-bridge-signature-v2`` (or any
308
+ # other bridge header) on an honestly signed forward; upgraded origins
309
+ # must never relay externally injected bridge headers.
310
+ forwarded = {
311
+ key: value
312
+ for key, value in filtered.items()
313
+ if key.lower() not in drop
314
+ and not key.lower().startswith("x-codex-bridge-")
315
+ and not _bridge_header_has_illegal_control_char(key, value)
316
+ }
317
+ # filter_inbound_headers strips Authorization, but the owner instance
318
+ # re-validates the client API key from this header (see
319
+ # _validate_internal_bridge_api_key) before swapping in its own upstream
320
+ # access token. Preserve it so api_key_auth_enabled deployments still
321
+ # authenticate forwarded bridge requests.
322
+ authorization = next(
323
+ (value for key, value in headers.items() if key.lower() == "authorization"),
324
+ None,
325
+ )
326
+ if authorization is not None and not _bridge_header_has_illegal_control_char("authorization", authorization):
327
+ forwarded["authorization"] = authorization
328
+ forwarded[HTTP_BRIDGE_FORWARDED_HEADER] = "1"
329
+ forwarded[HTTP_BRIDGE_ORIGIN_INSTANCE_HEADER] = context.origin_instance
330
+ forwarded[HTTP_BRIDGE_TARGET_INSTANCE_HEADER] = context.target_instance
331
+ forwarded[HTTP_BRIDGE_CODEX_AFFINITY_HEADER] = "1" if context.codex_session_affinity else "0"
332
+ signature_version = _HTTP_BRIDGE_SIGNATURE_VERSION_V2 if context.original_request_unanchored else None
333
+ if signature_version is not None:
334
+ forwarded[HTTP_BRIDGE_SIGNATURE_VERSION_HEADER] = signature_version
335
+ forwarded[HTTP_BRIDGE_ORIGINAL_UNANCHORED_HEADER] = "1"
336
+ if context.original_affinity_kind and context.original_affinity_key:
337
+ forwarded[HTTP_BRIDGE_AFFINITY_KIND_HEADER] = context.original_affinity_kind
338
+ forwarded[HTTP_BRIDGE_AFFINITY_KEY_HEADER] = context.original_affinity_key
339
+ if context.file_owner_account_id:
340
+ # This proof is accepted only with the full-context signature below;
341
+ # the legacy primary signature intentionally remains byte-compatible.
342
+ forwarded[HTTP_BRIDGE_FILE_OWNER_HEADER] = context.file_owner_account_id
343
+ if context.client_ip:
344
+ forwarded[HTTP_BRIDGE_CLIENT_IP_HEADER] = context.client_ip
345
+ forwarded[HTTP_BRIDGE_CLIENT_IP_SIGNATURE_HEADER] = _bridge_forward_signature(
346
+ payload=payload,
347
+ context=context,
348
+ include_client_ip=True,
349
+ signature_version=signature_version,
350
+ )
351
+ if context.downstream_turn_state:
352
+ forwarded["x-codex-turn-state"] = context.downstream_turn_state
353
+ if context.reservation is not None:
354
+ forwarded[HTTP_BRIDGE_RESERVATION_ID_HEADER] = context.reservation.reservation_id
355
+ forwarded[HTTP_BRIDGE_RESERVATION_KEY_ID_HEADER] = context.reservation.key_id
356
+ forwarded[HTTP_BRIDGE_RESERVATION_MODEL_HEADER] = context.reservation.model
357
+ # ROLLOUT SHIM (#1203, remove with HTTP_BRIDGE_SIGNATURE_V2_HEADER
358
+ # follow-up): keep sending the primary signature computed over the plain
359
+ # ``model_dump`` (with synthesized ``"tools": []``) so owners running code
360
+ # that predates the tamper-proofing header can still verify requests from
361
+ # updated origins during a rolling upgrade. New-code receivers verify the
362
+ # tamper-proofing header below first and fall back to this primary
363
+ # signature only when the tamper-proofing header does not validate.
364
+ forwarded[HTTP_BRIDGE_SIGNATURE_HEADER] = _bridge_forward_signature(
365
+ payload=payload,
366
+ context=context,
367
+ include_client_ip=False,
368
+ signature_version=signature_version,
369
+ )
370
+ # Additive tamper-proofing signature bound to the exact posted forwarding
371
+ # body; covers the full authenticated context (including the unanchored /
372
+ # signature-version domain) so it cannot be replayed against a different
373
+ # forward.
374
+ forwarded[HTTP_BRIDGE_SIGNATURE_V2_HEADER] = _bridge_forward_tools_bound_signature(
375
+ payload=payload,
376
+ context=context,
377
+ signature_version=signature_version,
378
+ )
379
+ return forwarded
380
+
381
+
382
+ def parse_forwarded_request(
383
+ headers: Mapping[str, str],
384
+ *,
385
+ payload: ResponsesRequest,
386
+ current_instance: str,
387
+ ) -> tuple[HTTPBridgeForwardedRequest | None, ProxyResponseError | None]:
388
+ if headers.get(HTTP_BRIDGE_FORWARDED_HEADER) != "1":
389
+ return None, ProxyResponseError(
390
+ 400,
391
+ openai_error(
392
+ "bridge_forward_invalid",
393
+ "Internal bridge forward marker is required",
394
+ error_type="invalid_request_error",
395
+ ),
396
+ )
397
+ target_instance = headers.get(HTTP_BRIDGE_TARGET_INSTANCE_HEADER, "").strip()
398
+ if not target_instance or target_instance != current_instance:
399
+ return None, ProxyResponseError(
400
+ 503,
401
+ openai_error(
402
+ "bridge_owner_forward_failed",
403
+ "Internal bridge forward reached a non-target instance",
404
+ error_type="server_error",
405
+ ),
406
+ )
407
+ client_ip = _optional_header(headers.get(HTTP_BRIDGE_CLIENT_IP_HEADER))
408
+ signature_version = _optional_header(headers.get(HTTP_BRIDGE_SIGNATURE_VERSION_HEADER))
409
+ original_unanchored_value = _optional_header(headers.get(HTTP_BRIDGE_ORIGINAL_UNANCHORED_HEADER))
410
+ if signature_version == _HTTP_BRIDGE_SIGNATURE_VERSION_V2:
411
+ if original_unanchored_value not in {"0", "1"}:
412
+ return None, _invalid_bridge_forward_signature_error()
413
+ original_request_unanchored = original_unanchored_value == "1"
414
+ elif signature_version is None:
415
+ original_request_unanchored = False
416
+ else:
417
+ return None, _invalid_bridge_forward_signature_error()
418
+ context = HTTPBridgeForwardContext(
419
+ origin_instance=headers.get(HTTP_BRIDGE_ORIGIN_INSTANCE_HEADER, "").strip() or "unknown",
420
+ target_instance=target_instance,
421
+ codex_session_affinity=_bool_header(headers.get(HTTP_BRIDGE_CODEX_AFFINITY_HEADER)),
422
+ downstream_turn_state=_optional_header(headers.get("x-codex-turn-state")),
423
+ original_request_unanchored=original_request_unanchored,
424
+ original_affinity_kind=_optional_header(headers.get(HTTP_BRIDGE_AFFINITY_KIND_HEADER)),
425
+ original_affinity_key=_optional_header(headers.get(HTTP_BRIDGE_AFFINITY_KEY_HEADER)),
426
+ file_owner_account_id=_optional_header(headers.get(HTTP_BRIDGE_FILE_OWNER_HEADER)),
427
+ client_ip=client_ip,
428
+ reservation=_reservation_from_headers(headers),
429
+ signature_version=signature_version,
430
+ )
431
+ # Tamper-proofing fast path (#1203): a VALIDATING tamper-proofing
432
+ # signature proves the received body was not rewritten in transit —
433
+ # including an injected ``"tools": []`` that the primary plain-dump
434
+ # signature cannot distinguish from an omitted field — and it also
435
+ # authenticates the full forward context (structured, delimiter-safe), so
436
+ # it accepts even when the legacy delimiter check below would bail. It is
437
+ # authoritative only when it validates: mere header presence is not a
438
+ # trustworthy signal, because an external client could plant a garbage
439
+ # value on an honestly primary-signed forward, so a present-but-invalid
440
+ # header simply falls through to the primary verification.
441
+ tools_bound_signature = _optional_header(headers.get(HTTP_BRIDGE_SIGNATURE_V2_HEADER))
442
+ tools_bound_valid = tools_bound_signature is not None and hmac.compare_digest(
443
+ tools_bound_signature,
444
+ _bridge_forward_tools_bound_signature(
445
+ payload=payload,
446
+ context=context,
447
+ signature_version=signature_version,
448
+ ),
449
+ )
450
+ if tools_bound_valid:
451
+ return HTTPBridgeForwardedRequest(context=context), None
452
+ if context.file_owner_account_id is not None or extract_input_file_ids(payload.input):
453
+ # The rolling-upgrade primary signature does not bind the additive
454
+ # file-owner proof. Never allow a stripped/forged proof to downgrade to
455
+ # it, and never allow payloads with file references to fall back after a
456
+ # stripped proof made the owner value look absent.
457
+ return None, _invalid_bridge_forward_signature_error()
458
+ # ROLLOUT SHIM (#1203, remove with HTTP_BRIDGE_SIGNATURE_V2_HEADER
459
+ # follow-up): fall back to the primary signature (#1169's versioned /
460
+ # legacy scheme) so owners predating the tamper-proofing header — and
461
+ # genuinely old origins that only send the primary signature — keep
462
+ # verifying during a rolling upgrade. Known residual until the shim is
463
+ # removed: this fallback is exactly as strong as the pre-#1203 scheme, so
464
+ # a body-only rewrite that injects ``"tools": []`` downgrades to the
465
+ # plain-dump digest (which is insensitive to synthesized-vs-injected empty
466
+ # tools) and verifies. Dropping the shim restores strict tamper-proof
467
+ # rejection.
468
+ if signature_version is None and _legacy_signature_context_has_ambiguous_delimiter(context):
469
+ return None, _invalid_bridge_forward_signature_error()
470
+ signature = _optional_header(headers.get(HTTP_BRIDGE_SIGNATURE_HEADER))
471
+ client_ip_signature = _optional_header(headers.get(HTTP_BRIDGE_CLIENT_IP_SIGNATURE_HEADER))
472
+ expected_signature = _bridge_forward_signature(
473
+ payload=payload,
474
+ context=context,
475
+ signature_version=signature_version,
476
+ )
477
+ signature_without_client_ip = _bridge_forward_signature(
478
+ payload=payload,
479
+ context=context,
480
+ include_client_ip=False,
481
+ signature_version=signature_version,
482
+ )
483
+ primary_signature_valid = signature is not None and hmac.compare_digest(signature, expected_signature)
484
+ signature_without_client_ip_valid = signature is not None and hmac.compare_digest(
485
+ signature,
486
+ signature_without_client_ip,
487
+ )
488
+ client_ip_signature_valid = client_ip_signature is not None and hmac.compare_digest(
489
+ client_ip_signature,
490
+ expected_signature,
491
+ )
492
+ signature_valid = primary_signature_valid or (
493
+ signature_without_client_ip_valid and (client_ip is None or client_ip_signature_valid)
494
+ )
495
+ if not signature_valid:
496
+ return None, _invalid_bridge_forward_signature_error()
497
+ return HTTPBridgeForwardedRequest(context=context), None
498
+
499
+
500
+ def _invalid_bridge_forward_signature_error() -> ProxyResponseError:
501
+ return ProxyResponseError(
502
+ 400,
503
+ openai_error(
504
+ "bridge_forward_invalid",
505
+ "Internal bridge forward signature is invalid",
506
+ error_type="invalid_request_error",
507
+ ),
508
+ )
509
+
510
+
511
+ def _legacy_signature_context_has_ambiguous_delimiter(context: HTTPBridgeForwardContext) -> bool:
512
+ """Reject legacy fields whose boundaries cannot be authenticated safely."""
513
+
514
+ values = [
515
+ context.origin_instance,
516
+ context.target_instance,
517
+ context.downstream_turn_state,
518
+ context.original_affinity_kind,
519
+ context.original_affinity_key,
520
+ context.client_ip,
521
+ ]
522
+ if context.reservation is not None:
523
+ values.extend(
524
+ (
525
+ context.reservation.reservation_id,
526
+ context.reservation.key_id,
527
+ context.reservation.model,
528
+ )
529
+ )
530
+ return any(_LEGACY_SIGNATURE_DELIMITER in value for value in values if value is not None)
531
+
532
+
533
+ def _owner_forward_timeout(*, connect_timeout_seconds: float, idle_timeout_seconds: float) -> aiohttp.ClientTimeout:
534
+ return aiohttp.ClientTimeout(
535
+ total=None,
536
+ sock_connect=connect_timeout_seconds,
537
+ sock_read=max(0.001, idle_timeout_seconds),
538
+ )
539
+
540
+
541
+ def _reservation_from_headers(headers: Mapping[str, str]) -> ApiKeyUsageReservationData | None:
542
+ reservation_id = _optional_header(headers.get(HTTP_BRIDGE_RESERVATION_ID_HEADER))
543
+ key_id = _optional_header(headers.get(HTTP_BRIDGE_RESERVATION_KEY_ID_HEADER))
544
+ model = _optional_header(headers.get(HTTP_BRIDGE_RESERVATION_MODEL_HEADER))
545
+ if reservation_id is None or key_id is None or model is None:
546
+ return None
547
+ return ApiKeyUsageReservationData(
548
+ reservation_id=reservation_id,
549
+ key_id=key_id,
550
+ model=model,
551
+ )
552
+
553
+
554
+ def _bool_header(value: str | None) -> bool:
555
+ if value is None:
556
+ return False
557
+ return value.strip().lower() in {"1", "true", "yes", "on"}
558
+
559
+
560
+ def _optional_header(value: str | None) -> str | None:
561
+ if value is None:
562
+ return None
563
+ stripped = value.strip()
564
+ return stripped or None
565
+
566
+
567
+ def _bridge_forward_signature(
568
+ *,
569
+ payload: ResponsesRequest,
570
+ context: HTTPBridgeForwardContext,
571
+ include_client_ip: bool = True,
572
+ signature_version: str | None = None,
573
+ ) -> str:
574
+ """Primary forward signature (#1169), computed over a plain ``model_dump``.
575
+
576
+ Because the plain dump synthesizes ``"tools": []`` for clients that
577
+ omitted the field, this digest cannot distinguish an omitted-tools body
578
+ from one with an injected explicit empty list; the tamper-proof binding
579
+ lives in ``_bridge_forward_tools_bound_signature`` (#1203). This signature
580
+ is retained for the unanchored/legacy wire contract and rolling-upgrade
581
+ fallback. ``signature_version is None`` uses the deployed legacy
582
+ delimiter-joined format; the versioned path uses a canonical structured
583
+ encoding whose object boundaries make field re-packing impossible.
584
+ """
585
+ body_digest = _bridge_forward_body_digest(payload.model_dump(mode="json", exclude_none=True))
586
+ if signature_version is None:
587
+ # Preserve the deployed legacy wire format for anchored requests during
588
+ # rolling upgrades. Its delimiter-based encoding is intentionally not
589
+ # reused by the versioned path because attacker-controlled affinity
590
+ # values can contain the delimiter and make different field layouts
591
+ # authenticate alike.
592
+ fields = [
593
+ context.origin_instance,
594
+ context.target_instance,
595
+ "1" if context.codex_session_affinity else "0",
596
+ context.downstream_turn_state or "",
597
+ context.original_affinity_kind or "",
598
+ context.original_affinity_key or "",
599
+ ]
600
+ if include_client_ip:
601
+ fields.append(context.client_ip or "")
602
+ fields.extend(
603
+ (
604
+ context.reservation.reservation_id if context.reservation is not None else "",
605
+ context.reservation.key_id if context.reservation is not None else "",
606
+ context.reservation.model if context.reservation is not None else "",
607
+ body_digest,
608
+ )
609
+ )
610
+ signing_payload = _LEGACY_SIGNATURE_DELIMITER.join(fields)
611
+ else:
612
+ signing_payload = _structured_bridge_signing_payload(
613
+ body_digest=body_digest,
614
+ context=context,
615
+ include_client_ip=include_client_ip,
616
+ signature_version=signature_version,
617
+ protocol="codex-lb-http-bridge-forward",
618
+ )
619
+ return _sign_bridge_payload(signing_payload)
620
+
621
+
622
+ def _bridge_forward_tools_bound_signature(
623
+ *,
624
+ payload: ResponsesRequest,
625
+ context: HTTPBridgeForwardContext,
626
+ signature_version: str | None = None,
627
+ ) -> str:
628
+ """Tamper-proofing signature bound to the exact posted forwarding body.
629
+
630
+ Signs the same forwarding dump that is actually posted
631
+ (``model_dump_for_forwarding``), not a plain ``model_dump`` that
632
+ synthesizes ``"tools": []`` for clients that omitted the field. A plain
633
+ dump would make the omitted-tools and explicit-``tools: []`` bodies sign
634
+ identically, so a body rewritten in transit to inject ``"tools": []``
635
+ would still verify on the owner instance and re-mark ``tools`` as
636
+ explicitly set (issue #1184). Uses a distinct protocol domain so it can
637
+ never be confused with the primary signature, and reuses the same
638
+ canonical structured encoding (covering the full authenticated context,
639
+ including the unanchored / signature-version domain) so the binding also
640
+ carries #1169's isolation guarantees. Always authenticates ``client_ip``.
641
+ """
642
+ body_digest = _bridge_forward_body_digest(payload.model_dump_for_forwarding())
643
+ signing_payload = _structured_bridge_signing_payload(
644
+ body_digest=body_digest,
645
+ context=context,
646
+ include_client_ip=True,
647
+ signature_version=signature_version,
648
+ protocol="codex-lb-http-bridge-forward-tools-bound",
649
+ )
650
+ return _sign_bridge_payload(signing_payload)
651
+
652
+
653
+ def _bridge_forward_body_digest(payload_dump: JsonObject) -> str:
654
+ payload_json = json.dumps(payload_dump, ensure_ascii=True, sort_keys=True, separators=(",", ":"))
655
+ return hashlib.sha256(payload_json.encode("utf-8")).hexdigest()
656
+
657
+
658
+ def _structured_bridge_signing_payload(
659
+ *,
660
+ body_digest: str,
661
+ context: HTTPBridgeForwardContext,
662
+ include_client_ip: bool,
663
+ signature_version: str | None,
664
+ protocol: str,
665
+ ) -> str:
666
+ # Canonical structured encoding: object boundaries make field re-packing
667
+ # impossible, the client-IP mode is itself authenticated, and ``protocol``
668
+ # domain-separates the primary and tamper-proofing signatures.
669
+ return json.dumps(
670
+ {
671
+ "body_digest": body_digest,
672
+ "client_ip": context.client_ip if include_client_ip else None,
673
+ "client_ip_present": context.client_ip is not None,
674
+ "codex_session_affinity": context.codex_session_affinity,
675
+ "downstream_turn_state": context.downstream_turn_state,
676
+ "file_owner_account_id": context.file_owner_account_id,
677
+ "include_client_ip": include_client_ip,
678
+ "origin_instance": context.origin_instance,
679
+ "original_affinity_key": context.original_affinity_key,
680
+ "original_affinity_kind": context.original_affinity_kind,
681
+ "original_request_unanchored": context.original_request_unanchored,
682
+ "protocol": protocol,
683
+ "reservation": (
684
+ {
685
+ "id": context.reservation.reservation_id,
686
+ "key_id": context.reservation.key_id,
687
+ "model": context.reservation.model,
688
+ }
689
+ if context.reservation is not None
690
+ else None
691
+ ),
692
+ "signature_version": signature_version,
693
+ "target_instance": context.target_instance,
694
+ },
695
+ ensure_ascii=True,
696
+ sort_keys=True,
697
+ separators=(",", ":"),
698
+ )
699
+
700
+
701
+ def _sign_bridge_payload(signing_payload: str) -> str:
702
+ secret = get_or_create_key(get_settings().encryption_key_file)
703
+ return hmac.new(secret, signing_payload.encode("utf-8"), hashlib.sha256).hexdigest()
704
+
705
+
706
+ async def _iter_sse_event_blocks(
707
+ response: aiohttp.ClientResponse,
708
+ *,
709
+ request_started_at: float,
710
+ proxy_request_budget_seconds: float,
711
+ stream_idle_timeout_seconds: float,
712
+ scheduler: Scheduler,
713
+ clock: Clock,
714
+ ) -> AsyncIterator[str]:
715
+ buffer = b""
716
+ chunks = response.content.iter_chunked(65536)
717
+ while True:
718
+ receive_timeout = _owner_forward_receive_timeout(
719
+ request_started_at=request_started_at,
720
+ proxy_request_budget_seconds=proxy_request_budget_seconds,
721
+ stream_idle_timeout_seconds=stream_idle_timeout_seconds,
722
+ now=clock.monotonic(),
723
+ )
724
+ try:
725
+ chunk = await scheduler.wait_for(chunks.__anext__(), timeout=receive_timeout.timeout_seconds)
726
+ except StopAsyncIteration:
727
+ break
728
+ except asyncio.TimeoutError as exc:
729
+ raise _OwnerForwardStreamTimeoutError(
730
+ error_code=receive_timeout.error_code,
731
+ error_message=receive_timeout.error_message,
732
+ ) from exc
733
+ if not chunk:
734
+ continue
735
+ buffer += chunk
736
+ while b"\n\n" in buffer:
737
+ raw_block, buffer = buffer.split(b"\n\n", 1)
738
+ text = raw_block.decode("utf-8")
739
+ if text:
740
+ yield f"{text}\n\n"
741
+ if buffer.strip():
742
+ yield buffer.decode("utf-8")
743
+
744
+
745
+ def _owner_forward_receive_timeout(
746
+ *,
747
+ request_started_at: float,
748
+ proxy_request_budget_seconds: float,
749
+ stream_idle_timeout_seconds: float,
750
+ now: float,
751
+ ) -> _OwnerForwardReceiveTimeout:
752
+ idle_timeout_seconds = max(0.001, stream_idle_timeout_seconds)
753
+ remaining_budget = max(0.0, request_started_at + proxy_request_budget_seconds - now)
754
+ idle_timeout_matches_request_budget = idle_timeout_seconds == max(0.001, proxy_request_budget_seconds)
755
+ if remaining_budget <= 0 and idle_timeout_matches_request_budget:
756
+ return _OwnerForwardReceiveTimeout(
757
+ timeout_seconds=0.0,
758
+ error_code="stream_idle_timeout",
759
+ error_message="Upstream stream idle timeout",
760
+ )
761
+ if idle_timeout_matches_request_budget and remaining_budget >= idle_timeout_seconds:
762
+ return _OwnerForwardReceiveTimeout(
763
+ timeout_seconds=remaining_budget,
764
+ error_code="stream_idle_timeout",
765
+ error_message="Upstream stream idle timeout",
766
+ )
767
+ if remaining_budget <= 0:
768
+ return _OwnerForwardReceiveTimeout(
769
+ timeout_seconds=0.0,
770
+ error_code="upstream_request_timeout",
771
+ error_message="Proxy request budget exhausted",
772
+ )
773
+ if idle_timeout_seconds <= remaining_budget:
774
+ return _OwnerForwardReceiveTimeout(
775
+ timeout_seconds=idle_timeout_seconds,
776
+ error_code="stream_idle_timeout",
777
+ error_message="Upstream stream idle timeout",
778
+ )
779
+ return _OwnerForwardReceiveTimeout(
780
+ timeout_seconds=remaining_budget,
781
+ error_code="upstream_request_timeout",
782
+ error_message="Proxy request budget exhausted",
783
+ )
784
+
785
+
786
+ def _owner_forward_error_payload(*, status_code: int, payload_text: str) -> OpenAIErrorEnvelope:
787
+ try:
788
+ payload = json.loads(payload_text)
789
+ except json.JSONDecodeError:
790
+ payload = None
791
+ if is_json_mapping(payload) and is_json_mapping(payload.get("error")):
792
+ return cast(OpenAIErrorEnvelope, payload)
793
+ return openai_error(
794
+ "bridge_owner_forward_failed",
795
+ payload_text or f"HTTP bridge owner request failed with status {status_code}",
796
+ error_type="server_error",
797
+ )