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,930 @@
1
+ from __future__ import annotations
2
+
3
+ import gzip
4
+ import json
5
+ import logging
6
+ import re
7
+ import sys
8
+ from collections.abc import Mapping, Sequence
9
+ from copy import deepcopy
10
+ from datetime import datetime, timezone
11
+ from hashlib import sha256
12
+ from pathlib import Path
13
+ from typing import Any, TypeVar, cast
14
+ from uuid import uuid4
15
+
16
+ from app.core.clients.proxy import (
17
+ _AGENT_CONTROL_OUTPUT_ITEM_TYPES,
18
+ _RESPONSE_CREATE_TOOL_OUTPUT_OMISSION_NOTICE,
19
+ CODEX_INSTALLATION_ID_HEADER,
20
+ UPSTREAM_RESPONSE_CREATE_MAX_BYTES,
21
+ ImageFetchSession,
22
+ ProxyResponseError,
23
+ _agent_control_tool_output_occurrences,
24
+ _finalize_responses_lite_reasoning_context,
25
+ _historical_agent_control_output_occurrences,
26
+ _inline_content_images,
27
+ _is_inline_image_reference,
28
+ _normalize_responses_lite_websocket_client_metadata,
29
+ _payload_has_responses_lite_websocket_marker,
30
+ _payload_uses_responses_lite,
31
+ _response_create_inline_image_notice_item,
32
+ _response_create_recent_suffix_start,
33
+ _response_create_too_large_error_envelope,
34
+ _should_slim_historical_tool_output,
35
+ _slim_historical_response_content,
36
+ _ws_transport_payload_budget_bytes,
37
+ apply_codex_installation_metadata,
38
+ )
39
+ from app.core.config.settings import DEFAULT_HOME_DIR, get_settings
40
+ from app.core.openai.requests import ResponsesRequest
41
+ from app.core.types import JsonValue
42
+ from app.core.utils.json_guards import is_json_mapping
43
+ from app.modules.proxy._service.support import (
44
+ _PENDING_TOOL_CALL_ITEM_TYPES,
45
+ _PENDING_TOOL_CALL_OUTPUT_ITEM_TYPE_BY_CALL_TYPE,
46
+ _PENDING_TOOL_CALL_OUTPUT_ITEM_TYPES,
47
+ _WebSocketRequestState,
48
+ )
49
+
50
+ logger = logging.getLogger("app.modules.proxy.service")
51
+ T = TypeVar("T")
52
+
53
+ _UPSTREAM_RESPONSE_CREATE_MAX_BYTES = UPSTREAM_RESPONSE_CREATE_MAX_BYTES
54
+ _UPSTREAM_RESPONSE_CREATE_WARN_BYTES = int(_UPSTREAM_RESPONSE_CREATE_MAX_BYTES * 0.8)
55
+ _OVERSIZED_RESPONSE_CREATE_LARGEST_ITEMS = 10
56
+ _RESPONSE_CREATE_HISTORY_OMISSION_NOTICE = (
57
+ "[codex-lb omitted {count} historical input items to fit upstream websocket budget]"
58
+ )
59
+ _OVERSIZED_RESPONSE_CREATE_DUMP_DIR: Path | None = None
60
+ _RESPONSE_CREATE_DUMP_SUFFIX = ".response-create.json.gz"
61
+ _RESPONSE_CREATE_META_SUFFIX = ".meta.json"
62
+ _RESPONSE_CREATE_DUMP_SHA_SLUG_LEN = 16
63
+ _RESPONSE_CREATE_DUMP_MAX_PAIRS = 20
64
+ _RESPONSE_CREATE_COMPATIBILITY_METADATA_HEADERS = (
65
+ "x-codex-turn-metadata",
66
+ "x-openai-subagent",
67
+ "x-codex-parent-thread-id",
68
+ "x-codex-window-id",
69
+ )
70
+
71
+
72
+ def _service_module() -> Any | None:
73
+ return sys.modules.get("app.modules.proxy.service")
74
+
75
+
76
+ def _service_global_or(name: str, fallback: T) -> T:
77
+ service_module = _service_module()
78
+ if service_module is None:
79
+ return fallback
80
+ return cast(T, getattr(service_module, name, fallback))
81
+
82
+
83
+ def _upstream_response_create_max_bytes() -> int:
84
+ return int(_service_global_or("_UPSTREAM_RESPONSE_CREATE_MAX_BYTES", _UPSTREAM_RESPONSE_CREATE_MAX_BYTES))
85
+
86
+
87
+ def _upstream_response_create_warn_bytes() -> int:
88
+ fallback = int(_upstream_response_create_max_bytes() * 0.8)
89
+ return int(_service_global_or("_UPSTREAM_RESPONSE_CREATE_WARN_BYTES", fallback))
90
+
91
+
92
+ def _oversized_response_create_largest_items() -> int:
93
+ return int(
94
+ _service_global_or(
95
+ "_OVERSIZED_RESPONSE_CREATE_LARGEST_ITEMS",
96
+ _OVERSIZED_RESPONSE_CREATE_LARGEST_ITEMS,
97
+ )
98
+ )
99
+
100
+
101
+ def _oversized_response_create_dump_dir() -> Path:
102
+ configured_dir = _service_global_or("_OVERSIZED_RESPONSE_CREATE_DUMP_DIR", _OVERSIZED_RESPONSE_CREATE_DUMP_DIR)
103
+ if configured_dir is not None:
104
+ return configured_dir
105
+ settings_factory = _service_global_or("get_settings", get_settings)
106
+ data_dir = getattr(settings_factory(), "data_dir", DEFAULT_HOME_DIR)
107
+ return data_dir / "debug" / "response-create-dumps"
108
+
109
+
110
+ def _response_create_dump_max_pairs() -> int:
111
+ return int(_service_global_or("_RESPONSE_CREATE_DUMP_MAX_PAIRS", _RESPONSE_CREATE_DUMP_MAX_PAIRS))
112
+
113
+
114
+ def _existing_response_create_dump(dump_dir: Path, sha_slug: str) -> Path | None:
115
+ """Return an existing *complete* dump for the same payload fingerprint.
116
+
117
+ A dump only counts as existing when both the payload and its ``.meta.json``
118
+ sibling are present. A lone payload file (e.g. a crash or disk-full failure
119
+ between the two writes) is treated as absent so a retry can recreate the
120
+ missing meta instead of permanently suppressing the capture operators are
121
+ trying to diagnose.
122
+ """
123
+ try:
124
+ matches = sorted(dump_dir.glob(f"*-{sha_slug}{_RESPONSE_CREATE_DUMP_SUFFIX}"))
125
+ except OSError:
126
+ return None
127
+ for dump_path in matches:
128
+ dump_id = dump_path.name[: -len(_RESPONSE_CREATE_DUMP_SUFFIX)]
129
+ meta_path = dump_dir / f"{dump_id}{_RESPONSE_CREATE_META_SUFFIX}"
130
+ if meta_path.exists():
131
+ return dump_path
132
+ return None
133
+
134
+
135
+ def _prune_response_create_dumps(dump_dir: Path, *, max_pairs: int) -> None:
136
+ """Drop the oldest dump pairs so at most ``max_pairs`` remain.
137
+
138
+ Dump ids are timestamp-prefixed, so lexicographic order is chronological.
139
+ """
140
+ try:
141
+ dump_paths = sorted(dump_dir.glob(f"*{_RESPONSE_CREATE_DUMP_SUFFIX}"))
142
+ except OSError:
143
+ return
144
+ excess = len(dump_paths) - max_pairs
145
+ if excess <= 0:
146
+ return
147
+ for dump_path in dump_paths[:excess]:
148
+ dump_id = dump_path.name[: -len(_RESPONSE_CREATE_DUMP_SUFFIX)]
149
+ for stale_path in (dump_path, dump_dir / f"{dump_id}{_RESPONSE_CREATE_META_SUFFIX}"):
150
+ try:
151
+ stale_path.unlink(missing_ok=True)
152
+ except OSError:
153
+ logger.warning("Failed to prune response.create dump path=%s", stale_path, exc_info=True)
154
+
155
+
156
+ def _fingerprint_input_items(items: Sequence[JsonValue]) -> str:
157
+ """Return stable SHA-256 fingerprint for input list canonical JSON."""
158
+ canonical = json.dumps(list(items), ensure_ascii=True, separators=(",", ":"), sort_keys=True)
159
+ return sha256(canonical.encode("utf-8")).hexdigest()
160
+
161
+
162
+ def _input_part_is_image(part: JsonValue) -> bool:
163
+ return is_json_mapping(part) and part.get("type") == "input_image"
164
+
165
+
166
+ def _json_value_contains_input_image_part(value: JsonValue) -> bool:
167
+ if _input_part_is_image(value):
168
+ return True
169
+ if isinstance(value, list):
170
+ return any(_json_value_contains_input_image_part(item) for item in value)
171
+ if is_json_mapping(value):
172
+ return any(_json_value_contains_input_image_part(child) for child in value.values())
173
+ return False
174
+
175
+
176
+ def _responses_request_contains_input_image(payload: ResponsesRequest) -> bool:
177
+ """Return whether a Responses request carries any ``input_image`` part."""
178
+ input_value = payload.input
179
+ if not isinstance(input_value, list):
180
+ return False
181
+ return any(_json_value_contains_input_image_part(item) for item in input_value)
182
+
183
+
184
+ def _json_value_contains_external_input_image(value: JsonValue) -> bool:
185
+ """Whether ``value`` holds an ``input_image`` part that still names an external URL.
186
+
187
+ Recurses the same way :func:`_json_value_contains_input_image_part` does,
188
+ rather than walking the two shapes ``_count_external_image_urls`` knows
189
+ (a top-level item and its ``content`` array). The transport decision has to
190
+ see every external URL the payload carries, including one nested in a
191
+ ``function_call_output`` output array, because an image the URL inliner
192
+ never visits is precisely the one that is still external when the request
193
+ reaches the upstream websocket.
194
+ """
195
+ if _input_part_is_image(value):
196
+ image_url = value.get("image_url") if is_json_mapping(value) else None
197
+ # URL schemes are case-insensitive, and this decision is the fail-safe
198
+ # direction: missing one sends a raw external URL to a websocket that
199
+ # only accepts ``data:``. ``_count_external_image_urls`` still matches
200
+ # case-sensitively; that is the bridge's own guard and out of scope here.
201
+ return isinstance(image_url, str) and image_url.lower().startswith(("http://", "https://"))
202
+ if isinstance(value, list):
203
+ return any(_json_value_contains_external_input_image(item) for item in value)
204
+ if is_json_mapping(value):
205
+ return any(_json_value_contains_external_input_image(child) for child in value.values())
206
+ return False
207
+
208
+
209
+ def _input_image_request_requires_http_upstream(
210
+ payload: ResponsesRequest,
211
+ *,
212
+ payload_size_estimate_bytes: int,
213
+ ) -> bool:
214
+ """Return whether an ``input_image`` request must stay on the upstream HTTP transport.
215
+
216
+ Inline ``data:`` images ride the upstream websocket unchanged, so carrying one
217
+ is not by itself a reason to pin upstream HTTP; the bridge bypass exists to
218
+ free bridge pending slots (#903), not to avoid the websocket. Two
219
+ websocket-specific hazards survive, and only those keep the pin (#2363). A
220
+ payload over the websocket frame budget would reach
221
+ ``_prepare_websocket_response_create_payload``, which replaces every
222
+ historical inline image with an omission notice. An external ``http(s)``
223
+ image URL may still be there after ``_inline_content_images`` gives up on a
224
+ failed fetch, and the upstream websocket does not accept one.
225
+
226
+ The external-URL check recurses the whole input rather than reusing
227
+ ``_count_external_image_urls``, whose traversal stops at an item's
228
+ ``content`` array. An ``input_image`` nested deeper — in a
229
+ ``function_call_output`` output array, a routine Codex tool-result shape —
230
+ is exactly the one the URL inliner also never visits, so it is still
231
+ external at the upstream and must keep the pin.
232
+ """
233
+ input_value = payload.input
234
+ if not isinstance(input_value, list):
235
+ return False
236
+ if not any(_json_value_contains_input_image_part(item) for item in input_value):
237
+ return False
238
+ if payload_size_estimate_bytes > _ws_transport_payload_budget_bytes():
239
+ return True
240
+ return any(_json_value_contains_external_input_image(item) for item in input_value)
241
+
242
+
243
+ def _responses_request_uses_image_generation(payload: ResponsesRequest) -> bool:
244
+ tools = payload.tools
245
+ if not isinstance(tools, list):
246
+ return False
247
+ return any(is_json_mapping(tool) and tool.get("type") == "image_generation" for tool in tools)
248
+
249
+
250
+ def _response_create_text(
251
+ payload: ResponsesRequest,
252
+ *,
253
+ include_type_field: bool,
254
+ client_metadata: Mapping[str, JsonValue] | None,
255
+ ) -> str:
256
+ upstream_payload = dict(payload.to_payload())
257
+ upstream_payload.pop("stream", None)
258
+ upstream_payload.pop("background", None)
259
+ if include_type_field:
260
+ upstream_payload["type"] = "response.create"
261
+ if client_metadata:
262
+ upstream_payload["client_metadata"] = client_metadata
263
+ _finalize_responses_lite_reasoning_context(
264
+ upstream_payload,
265
+ responses_lite=(
266
+ _payload_uses_responses_lite(upstream_payload)
267
+ or _payload_has_responses_lite_websocket_marker(upstream_payload)
268
+ ),
269
+ )
270
+ return json.dumps(upstream_payload, ensure_ascii=True, separators=(",", ":"))
271
+
272
+
273
+ def _response_create_text_with_size_guard(
274
+ payload: ResponsesRequest,
275
+ *,
276
+ include_type_field: bool,
277
+ client_metadata: Mapping[str, JsonValue] | None,
278
+ request_state: _WebSocketRequestState,
279
+ transport: str,
280
+ ) -> str | None:
281
+ protected_agent_control_output_occurrences = (
282
+ _historical_agent_control_output_occurrences(cast(list[JsonValue], payload.input))
283
+ if isinstance(payload.input, list)
284
+ else {}
285
+ )
286
+ upstream_payload = dict(payload.to_payload())
287
+ upstream_payload.pop("stream", None)
288
+ upstream_payload.pop("background", None)
289
+ if include_type_field:
290
+ upstream_payload["type"] = "response.create"
291
+ if client_metadata:
292
+ upstream_payload["client_metadata"] = client_metadata
293
+ _finalize_responses_lite_reasoning_context(
294
+ upstream_payload,
295
+ responses_lite=(
296
+ _payload_uses_responses_lite(upstream_payload)
297
+ or _payload_has_responses_lite_websocket_marker(upstream_payload)
298
+ ),
299
+ )
300
+ text_data = json.dumps(upstream_payload, ensure_ascii=True, separators=(",", ":"))
301
+ payload_size = len(text_data.encode("utf-8"))
302
+ max_bytes = _upstream_response_create_max_bytes()
303
+ if payload_size > max_bytes:
304
+ original_payload_size = payload_size
305
+ slim_payload_for_upstream = _service_global_or(
306
+ "_slim_response_create_payload_for_upstream",
307
+ _slim_response_create_payload_for_upstream,
308
+ )
309
+ slimmed_payload, slim_summary = slim_payload_for_upstream(
310
+ upstream_payload,
311
+ max_bytes=max_bytes,
312
+ protected_agent_control_output_occurrences=protected_agent_control_output_occurrences,
313
+ )
314
+ if slim_summary is not None:
315
+ upstream_payload = slimmed_payload
316
+ text_data = json.dumps(upstream_payload, ensure_ascii=True, separators=(",", ":"))
317
+ payload_size = len(text_data.encode("utf-8"))
318
+ logger.warning(
319
+ (
320
+ "Slimmed response.create request_id=%s request_log_id=%s transport=%s "
321
+ "original_bytes=%s slimmed_bytes=%s "
322
+ "historical_tool_outputs_slimmed=%s historical_images_slimmed=%s"
323
+ ),
324
+ request_state.request_id,
325
+ request_state.request_log_id,
326
+ transport,
327
+ original_payload_size,
328
+ payload_size,
329
+ slim_summary["historical_tool_outputs_slimmed"],
330
+ slim_summary["historical_images_slimmed"],
331
+ )
332
+ if payload_size > max_bytes:
333
+ logger.warning(
334
+ (
335
+ "Skipping oversized response.create retry body request_id=%s request_log_id=%s "
336
+ "transport=%s bytes=%s max_bytes=%s"
337
+ ),
338
+ request_state.request_id,
339
+ request_state.request_log_id,
340
+ transport,
341
+ payload_size,
342
+ max_bytes,
343
+ )
344
+ return None
345
+ return text_data
346
+
347
+
348
+ def _response_create_text_with_account_installation_id(
349
+ text_data: str,
350
+ *,
351
+ account: Any,
352
+ ) -> str:
353
+ installation_id = getattr(account, "codex_installation_id", None)
354
+ if not isinstance(installation_id, str) or not installation_id.strip():
355
+ installation_id = str(uuid4())
356
+ try:
357
+ setattr(account, "codex_installation_id", installation_id)
358
+ except Exception:
359
+ pass
360
+
361
+ try:
362
+ raw_payload = json.loads(text_data)
363
+ except json.JSONDecodeError:
364
+ return text_data
365
+ if not isinstance(raw_payload, dict) or raw_payload.get("type") != "response.create":
366
+ return text_data
367
+ upstream_payload = cast(dict[str, JsonValue], raw_payload)
368
+
369
+ apply_codex_installation_metadata(upstream_payload, installation_id)
370
+ return json.dumps(upstream_payload, ensure_ascii=True, separators=(",", ":"))
371
+
372
+
373
+ def _slim_response_create_payload_for_upstream(
374
+ payload: dict[str, JsonValue],
375
+ *,
376
+ max_bytes: int,
377
+ protected_agent_control_output_occurrences: Mapping[tuple[str, str], tuple[bool, ...]] | None = None,
378
+ ) -> tuple[dict[str, JsonValue], dict[str, int] | None]:
379
+ input_value = payload.get("input")
380
+ if not isinstance(input_value, list) or not input_value:
381
+ return payload, None
382
+
383
+ input_items = cast(list[JsonValue], deepcopy(input_value))
384
+ preserve_from = _response_create_recent_suffix_start(input_items)
385
+ historical = input_items[:preserve_from]
386
+ recent = input_items[preserve_from:]
387
+
388
+ tool_outputs_slimmed = 0
389
+ images_slimmed = 0
390
+ if protected_agent_control_output_occurrences is None:
391
+ protected_agent_control_output_occurrences = _agent_control_tool_output_occurrences(historical)
392
+ agent_control_output_counts: dict[tuple[str, str], int] = {}
393
+
394
+ slimmed_historical: list[JsonValue] = []
395
+ for item in historical:
396
+ (
397
+ slimmed_item,
398
+ item_tool_outputs_slimmed,
399
+ item_images_slimmed,
400
+ ) = _slim_historical_response_input_item(
401
+ item,
402
+ protected_agent_control_output_occurrences=protected_agent_control_output_occurrences,
403
+ agent_control_output_counts=agent_control_output_counts,
404
+ )
405
+ tool_outputs_slimmed += item_tool_outputs_slimmed
406
+ images_slimmed += item_images_slimmed
407
+ slimmed_historical.append(slimmed_item)
408
+
409
+ candidate_payload = dict(payload)
410
+ candidate_payload["input"] = slimmed_historical + recent
411
+
412
+ if tool_outputs_slimmed == 0 and images_slimmed == 0:
413
+ return payload, None
414
+
415
+ return candidate_payload, {
416
+ "historical_tool_outputs_slimmed": tool_outputs_slimmed,
417
+ "historical_images_slimmed": images_slimmed,
418
+ }
419
+
420
+
421
+ def _function_call_output_call_ids(input_items: list[JsonValue]) -> set[str]:
422
+ call_ids: set[str] = set()
423
+ for item in input_items:
424
+ if not isinstance(item, dict) or item.get("type") not in _PENDING_TOOL_CALL_OUTPUT_ITEM_TYPES:
425
+ continue
426
+ call_id = item.get("call_id")
427
+ if isinstance(call_id, str) and call_id:
428
+ call_ids.add(call_id)
429
+ return call_ids
430
+
431
+
432
+ def _missing_function_call_outputs_for_previous_response(
433
+ input_items: list[JsonValue],
434
+ *,
435
+ pending_call_ids: list[str],
436
+ ) -> list[str]:
437
+ if not pending_call_ids:
438
+ return []
439
+ present_call_ids = _function_call_output_call_ids(input_items)
440
+ return [call_id for call_id in pending_call_ids if call_id not in present_call_ids]
441
+
442
+
443
+ def _synthetic_interrupted_function_call_output(
444
+ call_id: str,
445
+ *,
446
+ call_type: str = "function_call",
447
+ ) -> dict[str, JsonValue]:
448
+ output_type = _PENDING_TOOL_CALL_OUTPUT_ITEM_TYPE_BY_CALL_TYPE.get(call_type, "function_call_output")
449
+ item: dict[str, JsonValue] = {
450
+ "type": output_type,
451
+ "call_id": call_id,
452
+ "output": (
453
+ "Tool call was not executed because the previous turn was interrupted before tool output was available."
454
+ ),
455
+ }
456
+ if output_type == "apply_patch_call_output":
457
+ item["status"] = "failed"
458
+ return item
459
+
460
+
461
+ def _inject_missing_interrupted_function_call_outputs(
462
+ input_items: list[JsonValue],
463
+ *,
464
+ missing_call_ids: list[str],
465
+ pending_call_types: Mapping[str, str] | None = None,
466
+ ) -> list[JsonValue]:
467
+ if not missing_call_ids:
468
+ return input_items
469
+ call_types = pending_call_types or {}
470
+ return [
471
+ *[
472
+ _synthetic_interrupted_function_call_output(
473
+ call_id,
474
+ call_type=call_types.get(call_id, "function_call"),
475
+ )
476
+ for call_id in missing_call_ids
477
+ ],
478
+ *input_items,
479
+ ]
480
+
481
+
482
+ def _response_output_item_done_tool_call(payload: dict[str, JsonValue] | None) -> tuple[str, str] | None:
483
+ """Return ``(call_id, item_type)`` for a completed tool-call output item.
484
+
485
+ Covers every tool-call item type that upstream requires a matching output
486
+ item for on the next anchored request: ``function_call``,
487
+ ``custom_tool_call``, and ``apply_patch_call``.
488
+ """
489
+ if not isinstance(payload, dict) or payload.get("type") != "response.output_item.done":
490
+ return None
491
+ item = payload.get("item")
492
+ if not isinstance(item, dict):
493
+ return None
494
+ item_type = item.get("type")
495
+ if not isinstance(item_type, str) or item_type not in _PENDING_TOOL_CALL_ITEM_TYPES:
496
+ return None
497
+ call_id = item.get("call_id")
498
+ if not isinstance(call_id, str) or not call_id:
499
+ return None
500
+ return call_id, item_type
501
+
502
+
503
+ def _slim_historical_response_input_item(
504
+ item: JsonValue,
505
+ *,
506
+ protected_agent_control_output_occurrences: Mapping[tuple[str, str], tuple[bool, ...]],
507
+ agent_control_output_counts: dict[tuple[str, str], int],
508
+ ) -> tuple[JsonValue, int, int]:
509
+ if not is_json_mapping(item):
510
+ return item, 0, 0
511
+
512
+ item_mapping = dict(cast(dict[str, JsonValue], deepcopy(item)))
513
+ tool_outputs_slimmed = 0
514
+ images_slimmed = 0
515
+
516
+ item_type = item_mapping.get("type")
517
+ if isinstance(item_type, str) and item_type in _AGENT_CONTROL_OUTPUT_ITEM_TYPES:
518
+ call_id = item_mapping.get("call_id")
519
+ if isinstance(call_id, str) and call_id:
520
+ key = (item_type, call_id)
521
+ occurrence = agent_control_output_counts.get(key, 0)
522
+ agent_control_output_counts[key] = occurrence + 1
523
+ namespaced_flags = protected_agent_control_output_occurrences.get(key, ())
524
+ if occurrence < len(namespaced_flags) and namespaced_flags[occurrence]:
525
+ return item_mapping, tool_outputs_slimmed, images_slimmed
526
+ if isinstance(item_type, str) and item_type in _PENDING_TOOL_CALL_OUTPUT_ITEM_TYPES:
527
+ output = item_mapping.get("output")
528
+ if isinstance(output, str):
529
+ if _should_slim_historical_tool_output(output):
530
+ item_mapping["output"] = _RESPONSE_CREATE_TOOL_OUTPUT_OMISSION_NOTICE.format(
531
+ bytes=len(output.encode("utf-8"))
532
+ )
533
+ tool_outputs_slimmed += 1
534
+ else:
535
+ slimmed_output, output_images_slimmed = _slim_historical_response_content(output)
536
+ if output_images_slimmed > 0:
537
+ item_mapping["output"] = slimmed_output
538
+ images_slimmed += output_images_slimmed
539
+
540
+ content = item_mapping.get("content")
541
+ slimmed_content, content_images_slimmed = _slim_historical_response_content(content)
542
+ if content_images_slimmed > 0:
543
+ item_mapping["content"] = slimmed_content
544
+ images_slimmed += content_images_slimmed
545
+
546
+ if item_type == "input_image" and _is_inline_image_reference(item_mapping.get("image_url")):
547
+ return _response_create_inline_image_notice_item(), tool_outputs_slimmed, images_slimmed + 1
548
+
549
+ return item_mapping, tool_outputs_slimmed, images_slimmed
550
+
551
+
552
+ def _response_create_history_omission_notice_item(count: int) -> dict[str, JsonValue]:
553
+ return {
554
+ "role": "assistant",
555
+ "content": [
556
+ {
557
+ "type": "output_text",
558
+ "text": _RESPONSE_CREATE_HISTORY_OMISSION_NOTICE.format(count=count),
559
+ }
560
+ ],
561
+ }
562
+
563
+
564
+ async def _inline_top_level_input_image_urls(
565
+ payload: dict[str, JsonValue],
566
+ session: ImageFetchSession,
567
+ connect_timeout: float,
568
+ ) -> dict[str, JsonValue]:
569
+ input_value = payload.get("input")
570
+ if not isinstance(input_value, list):
571
+ return payload
572
+
573
+ updated_input: list[JsonValue] = []
574
+ changed = False
575
+ for item in input_value:
576
+ if not isinstance(item, dict) or item.get("type") != "input_image":
577
+ updated_input.append(item)
578
+ continue
579
+ inline_content_images = _service_global_or("_inline_content_images", _inline_content_images)
580
+ updated_item, item_changed = await inline_content_images(item, session, connect_timeout)
581
+ updated_input.append(updated_item)
582
+ changed = changed or item_changed
583
+ if not changed:
584
+ return payload
585
+
586
+ updated_payload = dict(payload)
587
+ updated_payload["input"] = updated_input
588
+ return updated_payload
589
+
590
+
591
+ def _count_external_image_urls(payload: dict[str, JsonValue]) -> int:
592
+ """Count input_image items that still reference an external (non data:) URL."""
593
+ input_value = payload.get("input")
594
+ if not isinstance(input_value, list):
595
+ return 0
596
+ count = 0
597
+ for item in input_value:
598
+ if not isinstance(item, dict):
599
+ continue
600
+ content_value = item.get("content")
601
+ content_parts = content_value if isinstance(content_value, list) else [content_value]
602
+ if item.get("type") == "input_image":
603
+ content_parts = [item, *content_parts]
604
+ for part in content_parts:
605
+ if not isinstance(part, dict):
606
+ continue
607
+ if part.get("type") != "input_image":
608
+ continue
609
+ image_url = part.get("image_url")
610
+ if isinstance(image_url, str) and image_url.startswith(("http://", "https://")):
611
+ count += 1
612
+ return count
613
+
614
+
615
+ def _enforce_response_create_size_limit(request_state: _WebSocketRequestState) -> None:
616
+ request_text = request_state.request_text
617
+ if not request_text:
618
+ return
619
+
620
+ payload_bytes = request_text.encode("utf-8")
621
+ payload_size = len(payload_bytes)
622
+ warn_bytes = _upstream_response_create_warn_bytes()
623
+ max_bytes = _upstream_response_create_max_bytes()
624
+ if payload_size > warn_bytes:
625
+ logger.warning(
626
+ (
627
+ "Large response.create prepared request_id=%s request_log_id=%s "
628
+ "transport=%s bytes=%s previous_response_id=%s"
629
+ ),
630
+ request_state.request_id,
631
+ request_state.request_log_id,
632
+ request_state.transport,
633
+ payload_size,
634
+ request_state.previous_response_id,
635
+ )
636
+ if payload_size <= max_bytes:
637
+ return
638
+
639
+ payload = _response_create_too_large_error_envelope(payload_size, max_bytes)
640
+ error = payload["error"]
641
+ write_response_create_dump = _service_global_or("_write_response_create_dump", _write_response_create_dump)
642
+ write_response_create_dump(
643
+ request_state,
644
+ account_id_value=None,
645
+ error_code=cast(str, error.get("code") or "payload_too_large"),
646
+ error_message=error.get("message"),
647
+ log_prefix="guarded",
648
+ )
649
+ # 400, not 413: the Codex client surfaces 400 immediately as a non-retryable
650
+ # invalid request, while 413 burns five full-payload retries and then pins the
651
+ # session to HTTP transport.
652
+ raise ProxyResponseError(
653
+ 400,
654
+ payload,
655
+ failure_phase="validation",
656
+ failure_detail=f"response.create_bytes={payload_size}",
657
+ )
658
+
659
+
660
+ def _maybe_dump_oversized_response_create_request(
661
+ request_state: _WebSocketRequestState,
662
+ *,
663
+ account_id_value: str | None,
664
+ error_code: str,
665
+ error_message: str | None,
666
+ ) -> None:
667
+ should_dump_oversized_response_create = _service_global_or(
668
+ "_should_dump_oversized_response_create",
669
+ _should_dump_oversized_response_create,
670
+ )
671
+ if not should_dump_oversized_response_create(error_code, error_message):
672
+ return
673
+ write_response_create_dump = _service_global_or("_write_response_create_dump", _write_response_create_dump)
674
+ write_response_create_dump(
675
+ request_state,
676
+ account_id_value=account_id_value,
677
+ error_code=error_code,
678
+ error_message=error_message,
679
+ log_prefix="oversized",
680
+ )
681
+
682
+
683
+ def _write_response_create_dump(
684
+ request_state: _WebSocketRequestState,
685
+ *,
686
+ account_id_value: str | None,
687
+ error_code: str,
688
+ error_message: str | None,
689
+ log_prefix: str,
690
+ ) -> bool:
691
+ request_text = request_state.request_text
692
+ if not request_text:
693
+ return False
694
+
695
+ payload_bytes = request_text.encode("utf-8")
696
+ request_sha = sha256(payload_bytes).hexdigest()
697
+ sha_slug = request_sha[:_RESPONSE_CREATE_DUMP_SHA_SLUG_LEN]
698
+ dump_dir = _oversized_response_create_dump_dir()
699
+
700
+ existing_dump_path = _existing_response_create_dump(dump_dir, sha_slug)
701
+ if existing_dump_path is not None:
702
+ logger.warning(
703
+ (
704
+ "Skipped duplicate %s response.create dump request_id=%s request_log_id=%s "
705
+ "request_text_sha256=%s existing_dump_path=%s bytes=%s"
706
+ ),
707
+ log_prefix,
708
+ request_state.request_id,
709
+ request_state.request_log_id,
710
+ request_sha,
711
+ existing_dump_path,
712
+ len(payload_bytes),
713
+ )
714
+ return False
715
+
716
+ timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S.%fZ")
717
+ dump_id = "-".join(
718
+ (
719
+ timestamp,
720
+ _safe_dump_slug(request_state.transport, fallback="transport"),
721
+ _safe_dump_slug(request_state.model, fallback="model"),
722
+ _safe_dump_slug(
723
+ request_state.request_log_id or request_state.response_id or request_state.request_id,
724
+ fallback="request",
725
+ ),
726
+ sha_slug,
727
+ )
728
+ )
729
+ dump_path = dump_dir / f"{dump_id}{_RESPONSE_CREATE_DUMP_SUFFIX}"
730
+ meta_path = dump_dir / f"{dump_id}{_RESPONSE_CREATE_META_SUFFIX}"
731
+
732
+ meta: dict[str, JsonValue] = {
733
+ "dump_id": dump_id,
734
+ "captured_at": datetime.now(timezone.utc).isoformat(),
735
+ "reason": {
736
+ "error_code": error_code,
737
+ "error_message": error_message,
738
+ },
739
+ "request": {
740
+ "account_id": account_id_value,
741
+ "request_id": request_state.request_id,
742
+ "request_log_id": request_state.request_log_id,
743
+ "response_id": request_state.response_id,
744
+ "transport": request_state.transport,
745
+ "model": request_state.model,
746
+ "reasoning_effort": request_state.reasoning_effort,
747
+ "service_tier": request_state.service_tier,
748
+ "requested_service_tier": request_state.requested_service_tier,
749
+ "actual_service_tier": request_state.actual_service_tier,
750
+ "previous_response_id": request_state.previous_response_id,
751
+ "awaiting_response_created": request_state.awaiting_response_created,
752
+ "replay_count": request_state.replay_count,
753
+ "request_text_bytes": len(payload_bytes),
754
+ "request_text_chars": len(request_text),
755
+ "request_text_sha256": request_sha,
756
+ },
757
+ "paths": {
758
+ "dump_path": str(dump_path),
759
+ "meta_path": str(meta_path),
760
+ },
761
+ }
762
+
763
+ try:
764
+ parsed_payload = json.loads(request_text)
765
+ except json.JSONDecodeError as exc:
766
+ meta["parse_error"] = str(exc)
767
+ else:
768
+ if isinstance(parsed_payload, dict):
769
+ meta["summary"] = _summarize_response_create_payload(parsed_payload)
770
+ else:
771
+ meta["summary"] = {"payload_type": type(parsed_payload).__name__}
772
+
773
+ max_pairs = _response_create_dump_max_pairs()
774
+ # Trim any pre-existing over-cap backlog before the new write. If the volume
775
+ # is already full, the write below raises and returns without pruning, so a
776
+ # directory left above the bound (e.g. after an upgrade that lowers the cap)
777
+ # would otherwise stay full on the exact failure the cap is meant to bound.
778
+ _prune_response_create_dumps(dump_dir, max_pairs=max_pairs)
779
+
780
+ try:
781
+ dump_dir.mkdir(parents=True, exist_ok=True)
782
+ with gzip.open(dump_path, "wt", encoding="utf-8") as handle:
783
+ handle.write(request_text)
784
+ meta_path.write_text(
785
+ json.dumps(meta, ensure_ascii=True, indent=2) + "\n",
786
+ encoding="utf-8",
787
+ )
788
+ except Exception:
789
+ logger.exception(
790
+ "Failed to dump %s response.create payload request_id=%s request_log_id=%s",
791
+ log_prefix,
792
+ request_state.request_id,
793
+ request_state.request_log_id,
794
+ )
795
+ return False
796
+
797
+ _prune_response_create_dumps(dump_dir, max_pairs=max_pairs)
798
+
799
+ logger.warning(
800
+ "Saved %s response.create dump request_id=%s request_log_id=%s dump_path=%s meta_path=%s bytes=%s",
801
+ log_prefix,
802
+ request_state.request_id,
803
+ request_state.request_log_id,
804
+ dump_path,
805
+ meta_path,
806
+ len(payload_bytes),
807
+ )
808
+ return True
809
+
810
+
811
+ def _should_dump_oversized_response_create(error_code: str, error_message: str | None) -> bool:
812
+ if error_code != "stream_incomplete" or not error_message:
813
+ return False
814
+ normalized = error_message.lower()
815
+ return "1009" in normalized or "message too big" in normalized
816
+
817
+
818
+ def _safe_dump_slug(value: str | None, *, fallback: str) -> str:
819
+ if not value:
820
+ return fallback
821
+ normalized = re.sub(r"[^A-Za-z0-9._-]+", "-", value).strip("-._")
822
+ if not normalized:
823
+ return fallback
824
+ return normalized[:80]
825
+
826
+
827
+ def _summarize_response_create_payload(payload: dict[str, JsonValue]) -> dict[str, JsonValue]:
828
+ field_sizes = sorted(
829
+ (
830
+ {
831
+ "key": key,
832
+ "size_bytes": _json_size_bytes(value),
833
+ }
834
+ for key, value in payload.items()
835
+ ),
836
+ key=lambda item: int(item["size_bytes"]),
837
+ reverse=True,
838
+ )
839
+ summary: dict[str, JsonValue] = {
840
+ "top_level_keys": list(payload.keys()),
841
+ "top_level_field_sizes": cast(JsonValue, field_sizes),
842
+ }
843
+ input_summary = _summarize_response_create_input(payload.get("input"))
844
+ if input_summary is not None:
845
+ summary["input"] = input_summary
846
+ return summary
847
+
848
+
849
+ def _summarize_response_create_input(input_value: JsonValue) -> dict[str, JsonValue] | None:
850
+ if not isinstance(input_value, list):
851
+ return None
852
+
853
+ input_items = cast(list[JsonValue], input_value)
854
+ role_counts: dict[str, int] = {}
855
+ item_type_counts: dict[str, int] = {}
856
+ content_part_type_counts: dict[str, int] = {}
857
+ largest_items: list[dict[str, JsonValue]] = []
858
+
859
+ for index, item in enumerate(input_items):
860
+ item_summary: dict[str, JsonValue] = {
861
+ "index": index,
862
+ "size_bytes": _json_size_bytes(item),
863
+ }
864
+ if isinstance(item, dict):
865
+ item_object = item
866
+ role = item_object.get("role")
867
+ if isinstance(role, str):
868
+ item_summary["role"] = role
869
+ role_counts[role] = role_counts.get(role, 0) + 1
870
+ item_type = item_object.get("type")
871
+ if isinstance(item_type, str):
872
+ item_summary["type"] = item_type
873
+ item_type_counts[item_type] = item_type_counts.get(item_type, 0) + 1
874
+ content = item_object.get("content")
875
+ if isinstance(content, list):
876
+ item_summary["content_parts"] = len(content)
877
+ for part in content:
878
+ if not isinstance(part, dict):
879
+ continue
880
+ part_object = part
881
+ part_type = part_object.get("type")
882
+ if isinstance(part_type, str):
883
+ content_part_type_counts[part_type] = content_part_type_counts.get(part_type, 0) + 1
884
+ largest_items.append(item_summary)
885
+
886
+ largest_items.sort(key=lambda item: cast(int, item["size_bytes"]), reverse=True)
887
+ summary: dict[str, JsonValue] = {
888
+ "count": len(input_value),
889
+ "role_counts": cast(JsonValue, role_counts),
890
+ "item_type_counts": cast(JsonValue, item_type_counts),
891
+ "content_part_type_counts": cast(JsonValue, content_part_type_counts),
892
+ "largest_items": cast(JsonValue, largest_items[: _oversized_response_create_largest_items()]),
893
+ }
894
+ return summary
895
+
896
+
897
+ def _json_size_bytes(value: JsonValue) -> int:
898
+ return len(json.dumps(value, ensure_ascii=True, separators=(",", ":")).encode("utf-8"))
899
+
900
+
901
+ def _response_create_client_metadata(
902
+ payload: Mapping[str, JsonValue],
903
+ *,
904
+ headers: Mapping[str, str],
905
+ codex_installation_id: str | None = None,
906
+ preserve_existing_responses_lite: bool = False,
907
+ ) -> Mapping[str, JsonValue] | None:
908
+ raw_value = payload.get("client_metadata")
909
+ client_metadata: dict[str, JsonValue] = {}
910
+ if is_json_mapping(raw_value):
911
+ for key, value in raw_value.items():
912
+ if isinstance(key, str) and key.lower() != CODEX_INSTALLATION_ID_HEADER:
913
+ client_metadata[key] = value
914
+
915
+ normalized_headers = {key.lower(): value for key, value in headers.items()}
916
+ for header_name in _RESPONSE_CREATE_COMPATIBILITY_METADATA_HEADERS:
917
+ metadata_value = normalized_headers.get(header_name)
918
+ if isinstance(metadata_value, str) and metadata_value.strip():
919
+ client_metadata.setdefault(header_name, metadata_value)
920
+
921
+ metadata_container: dict[str, JsonValue] = {"client_metadata": client_metadata}
922
+ apply_codex_installation_metadata(metadata_container, codex_installation_id)
923
+ normalized_metadata = metadata_container.get("client_metadata")
924
+ client_metadata = dict(normalized_metadata) if is_json_mapping(normalized_metadata) else {}
925
+ client_metadata = _normalize_responses_lite_websocket_client_metadata(
926
+ payload,
927
+ client_metadata,
928
+ preserve_existing=preserve_existing_responses_lite,
929
+ )
930
+ return client_metadata or None