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,1980 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ from collections.abc import Iterable, Mapping
6
+ from dataclasses import dataclass
7
+ from typing import cast
8
+
9
+ from pydantic import (
10
+ BaseModel,
11
+ ConfigDict,
12
+ Field,
13
+ PrivateAttr,
14
+ SerializeAsAny,
15
+ SkipValidation,
16
+ field_validator,
17
+ model_validator,
18
+ )
19
+
20
+ from app.core.openai.exceptions import ClientPayloadError
21
+ from app.core.openai.tool_call_safety import is_downstream_side_effect_tool_call_item
22
+ from app.core.types import JsonObject, JsonValue
23
+ from app.core.utils.json_guards import is_json_list, is_json_mapping
24
+
25
+ type MutableJsonObject = dict[str, JsonValue]
26
+
27
+ # Passthrough request fields (``input``, ``tools``, ``messages``, ``schema``)
28
+ # arrive from ``json.loads`` and are forwarded upstream as-is, so validating
29
+ # every node against the recursive ``JsonValue`` union only burns CPU (a
30
+ # Python-level ``Mapping`` ABC ``isinstance`` per dict node) and copies the
31
+ # tree. ``SkipValidation`` keeps the client's objects; ``SerializeAsAny``
32
+ # (inner) serializes them by runtime type instead of through a wrap
33
+ # serializer, which keeps ``model_dump(mode="json")`` byte-identical and
34
+ # faster. Field validators still run, so shape checks live there. The
35
+ # subscript form expands to ``Annotated[T, SerializeAsAny(), SkipValidation()]``
36
+ # (``SerializeAsAny`` inner, ``SkipValidation`` outermost) and type-checks as ``T``.
37
+ type PassthroughJsonValue = SkipValidation[SerializeAsAny[JsonValue]]
38
+ type PassthroughJsonList = SkipValidation[SerializeAsAny[list[JsonValue]]]
39
+
40
+ # pydantic-core's serializer refuses container nesting deeper than ~250 levels
41
+ # (a 500 from ``model_dump`` long after validation); deep validation used to
42
+ # reject such bodies first (``recursion_loop`` -> 400), so the passthrough
43
+ # validators keep that contract with an explicit, cheaper depth check.
44
+ PASSTHROUGH_MAX_DEPTH = 200
45
+ _JSON_CONTAINER_TYPES = (dict, list)
46
+
47
+ _RESPONSES_INCLUDE_ALLOWLIST = {
48
+ "code_interpreter_call.outputs",
49
+ "computer_call_output.output.image_url",
50
+ "file_search_call.results",
51
+ "message.input_image.image_url",
52
+ "message.output_text.logprobs",
53
+ "output.encrypted_content",
54
+ "reasoning.encrypted_content",
55
+ "web_search_call.action.sources",
56
+ }
57
+
58
+ UNSUPPORTED_TOOL_TYPES = {
59
+ "file_search",
60
+ "code_interpreter",
61
+ "computer_use",
62
+ "computer_use_preview",
63
+ "image_generation",
64
+ }
65
+
66
+ _TOOL_TYPE_ALIASES = {
67
+ "web_search_preview": "web_search",
68
+ }
69
+
70
+ _INTERLEAVED_REASONING_KEYS = frozenset({"reasoning_content", "reasoning_details", "tool_calls", "function_call"})
71
+ _INTERLEAVED_REASONING_PART_TYPES = frozenset({"reasoning", "reasoning_content", "reasoning_details"})
72
+ _ASSISTANT_TEXT_PART_TYPES = frozenset({"text", "input_text", "output_text"})
73
+ _TOOL_TEXT_PART_TYPES = frozenset({"text", "input_text", "output_text", "refusal"})
74
+ _COMPACT_STATE_TOOL_NAMES = frozenset({"create_goal", "get_goal", "update_goal", "update_plan"})
75
+ # A call_id can be reused by different item protocols. Compact pairing must
76
+ # therefore keep each protocol's occurrence stream separate.
77
+ _COMPACT_TOOL_CALL_TYPE_BY_OUTPUT_TYPE: dict[str, str] = {
78
+ "function_call_output": "function_call",
79
+ "custom_tool_call_output": "custom_tool_call",
80
+ "apply_patch_call_output": "apply_patch_call",
81
+ }
82
+ _COMPACT_TOOL_CALL_ITEM_TYPES = frozenset(_COMPACT_TOOL_CALL_TYPE_BY_OUTPUT_TYPE.values())
83
+ _COMPACT_TOOL_CALL_OUTPUT_ITEM_TYPES = frozenset(_COMPACT_TOOL_CALL_TYPE_BY_OUTPUT_TYPE)
84
+ _COMPACT_SIDE_EFFECT_TOOL_ITEM_TYPES = frozenset({"apply_patch_call", "apply_patch_call_output"})
85
+ _TOOL_CALL_ITEM_TYPES = frozenset(_COMPACT_TOOL_CALL_TYPE_BY_OUTPUT_TYPE.values())
86
+ _COMPACT_INLINE_IMAGE_DATA_URL_RE = re.compile(r"""data:image/[^,\s]+,[^\s"'<>]+""")
87
+ _GOAL_CONTINUATION_CONTEXT_PREFIX = '<codex_internal_context source="goal">'
88
+ _PLAN_MODE_CONTEXT_PREFIX = "<collaboration_mode># Plan Mode"
89
+ _EXPLICIT_PROMPT_CACHE_CONTENT_TYPES = frozenset({"input_text", "input_image", "input_file"})
90
+
91
+
92
+ def _json_mapping_or_none(value: JsonValue) -> Mapping[str, JsonValue] | None:
93
+ if not is_json_mapping(value):
94
+ return None
95
+ return value
96
+
97
+
98
+ def _json_parts(value: JsonValue) -> list[JsonValue]:
99
+ if is_json_list(value):
100
+ return value
101
+ return [value]
102
+
103
+
104
+ def normalize_tool_type(tool_type: str) -> str:
105
+ return _TOOL_TYPE_ALIASES.get(tool_type, tool_type)
106
+
107
+
108
+ def normalize_tool_choice(choice: JsonValue | None) -> JsonValue | None:
109
+ if not is_json_mapping(choice):
110
+ return choice
111
+ choice_mapping = choice
112
+ tool_type = choice_mapping.get("type")
113
+ if isinstance(tool_type, str):
114
+ normalized_type = normalize_tool_type(tool_type)
115
+ if normalized_type != tool_type:
116
+ updated = dict(choice_mapping)
117
+ updated["type"] = normalized_type
118
+ return updated
119
+ return choice
120
+
121
+
122
+ def validate_passthrough_depth(value: object, *, limit: int = PASSTHROUGH_MAX_DEPTH) -> None:
123
+ """Reject JSON nested deeper than ``limit`` container levels (iterative walk over
124
+ ``dict``/``list`` nodes; ``isinstance`` because the WebSocket parser yields ``dict`` subclasses)."""
125
+ if not isinstance(value, _JSON_CONTAINER_TYPES):
126
+ return
127
+ frontier: list[object] = [value]
128
+ depth = 0
129
+ while frontier:
130
+ depth += 1
131
+ if depth > limit:
132
+ raise ValueError(f"nesting exceeds {limit} levels")
133
+ next_frontier: list[object] = []
134
+ for node in frontier:
135
+ children: Iterable[object] = node.values() if isinstance(node, dict) else cast(list[object], node)
136
+ next_frontier.extend([child for child in children if isinstance(child, _JSON_CONTAINER_TYPES)])
137
+ frontier = next_frontier
138
+
139
+
140
+ def validate_tool_types(tools: list[JsonValue], *, allow_builtin_tools: bool = False) -> list[JsonValue]:
141
+ # ``tools`` skips pydantic's ``list`` validation, so the array check that
142
+ # used to yield ``list_type`` lives here (same ``param``: ``tools``).
143
+ if not isinstance(tools, list):
144
+ raise ValueError("tools must be an array")
145
+ normalized_tools: list[JsonValue] = []
146
+ for tool in tools:
147
+ if not is_json_mapping(tool):
148
+ normalized_tools.append(tool)
149
+ continue
150
+ tool_mapping = tool
151
+ tool_type = tool_mapping.get("type")
152
+ if isinstance(tool_type, str):
153
+ normalized_type = normalize_tool_type(tool_type)
154
+ if normalized_type != tool_type:
155
+ tool = dict(tool_mapping)
156
+ tool["type"] = normalized_type
157
+ tool_type = normalized_type
158
+ if not allow_builtin_tools and tool_type in UNSUPPORTED_TOOL_TYPES:
159
+ raise ValueError(f"Unsupported tool type: {tool_type}")
160
+ normalized_tools.append(tool)
161
+ return normalized_tools
162
+
163
+
164
+ def _is_input_file_with_id(item: Mapping[str, JsonValue]) -> bool:
165
+ if item.get("type") != "input_file":
166
+ return False
167
+ file_id = item.get("file_id")
168
+ return isinstance(file_id, str) and bool(file_id)
169
+
170
+
171
+ @dataclass(frozen=True, slots=True)
172
+ class InputImageFileReference:
173
+ item_index: int
174
+ content_index: int | None
175
+ file_id: str
176
+
177
+
178
+ def _input_image_file_reference(item: Mapping[str, JsonValue]) -> str | None:
179
+ if item.get("type") != "input_image":
180
+ return None
181
+ file_id = item.get("file_id")
182
+ if isinstance(file_id, str) and file_id:
183
+ return file_id
184
+ image_url = item.get("image_url")
185
+ if not isinstance(image_url, str) or not image_url.startswith("sediment://"):
186
+ return None
187
+ resolved = image_url.removeprefix("sediment://").strip()
188
+ return resolved or None
189
+
190
+
191
+ def extract_input_file_ids(input_value: JsonValue) -> set[str]:
192
+ """Return all ``file_id`` strings referenced by ``input_file`` / ``input_image`` items.
193
+
194
+ Walks top-level input items, role-message ``content`` parts, and retained
195
+ tool-output content, matching the actual reference shapes accepted by
196
+ ``ResponsesRequest.input`` / ``ResponsesCompactRequest.input``. Tool
197
+ metadata and arbitrary nested objects are deliberately not references.
198
+ Returns an empty set when the input is a plain string or has no
199
+ ``input_file`` parts. Used by the
200
+ ``/responses`` flow to look up account pins recorded by
201
+ ``POST /backend-api/files`` so the response request lands on the
202
+ upstream account that registered the file (the upstream contract is
203
+ account-scoped via ``chatgpt-account-id``).
204
+ """
205
+ if not is_json_list(input_value):
206
+ return set()
207
+ file_ids: set[str] = set()
208
+
209
+ def collect_part(part: JsonValue) -> None:
210
+ if not is_json_mapping(part):
211
+ return
212
+ if _is_input_file_with_id(part):
213
+ file_id = part.get("file_id")
214
+ if isinstance(file_id, str) and file_id:
215
+ file_ids.add(file_id)
216
+ image_file_id = _input_image_file_reference(part)
217
+ if image_file_id is not None:
218
+ file_ids.add(image_file_id)
219
+
220
+ for item in input_value:
221
+ if not is_json_mapping(item):
222
+ continue
223
+ collect_part(item)
224
+ for part in _json_parts(item.get("content")):
225
+ collect_part(part)
226
+ if item.get("type") in _COMPACT_TOOL_CALL_OUTPUT_ITEM_TYPES:
227
+ for part in _json_parts(item.get("output")):
228
+ collect_part(part)
229
+ return file_ids
230
+
231
+
232
+ def _append_input_image_file_references(
233
+ references: list[InputImageFileReference],
234
+ value: JsonValue,
235
+ *,
236
+ item_index: int,
237
+ content_index: int | None,
238
+ ) -> None:
239
+ if is_json_mapping(value):
240
+ file_id = _input_image_file_reference(value)
241
+ if file_id is not None:
242
+ references.append(
243
+ InputImageFileReference(
244
+ item_index=item_index,
245
+ content_index=content_index,
246
+ file_id=file_id,
247
+ )
248
+ )
249
+ for child in value.values():
250
+ _append_input_image_file_references(
251
+ references,
252
+ child,
253
+ item_index=item_index,
254
+ content_index=content_index,
255
+ )
256
+ return
257
+ if is_json_list(value):
258
+ for child in value:
259
+ _append_input_image_file_references(
260
+ references,
261
+ child,
262
+ item_index=item_index,
263
+ content_index=content_index,
264
+ )
265
+
266
+
267
+ def extract_input_image_file_references(input_value: JsonValue) -> list[InputImageFileReference]:
268
+ if not is_json_list(input_value):
269
+ return []
270
+ references: list[InputImageFileReference] = []
271
+ for item_index, item in enumerate(input_value):
272
+ if not is_json_mapping(item):
273
+ continue
274
+ item_mapping = item
275
+ top_level_file_id = _input_image_file_reference(item_mapping)
276
+ if top_level_file_id is not None:
277
+ references.append(
278
+ InputImageFileReference(
279
+ item_index=item_index,
280
+ content_index=None,
281
+ file_id=top_level_file_id,
282
+ )
283
+ )
284
+ content = item_mapping.get("content")
285
+ if is_json_list(content):
286
+ parts: list[JsonValue] = content
287
+ elif is_json_mapping(content):
288
+ parts = [content]
289
+ else:
290
+ parts = []
291
+ for content_index, part in enumerate(parts):
292
+ _append_input_image_file_references(
293
+ references,
294
+ part,
295
+ item_index=item_index,
296
+ content_index=content_index,
297
+ )
298
+ output = item_mapping.get("output")
299
+ _append_input_image_file_references(
300
+ references,
301
+ output,
302
+ item_index=item_index,
303
+ content_index=None,
304
+ )
305
+ return references
306
+
307
+
308
+ def _is_preserved_non_message_directive(item: Mapping[str, JsonValue]) -> bool:
309
+ # Shared preservation rule: any system/developer-role input item whose type
310
+ # is present and not "message" must reach upstream byte-identical. It gates
311
+ # instruction hoisting, input sanitization, and compact trim anchoring.
312
+ if item.get("role") not in ("system", "developer"):
313
+ return False
314
+ item_type = item.get("type")
315
+ return item_type is not None and item_type != "message"
316
+
317
+
318
+ def _sanitize_input_items(input_items: list[JsonValue]) -> list[JsonValue]:
319
+ sanitized_input: list[JsonValue] = []
320
+ for item in input_items:
321
+ item_mapping = _json_mapping_or_none(item)
322
+ if item_mapping is not None and _is_preserved_non_message_directive(item_mapping):
323
+ # Preserved directives are forwarded unchanged; the interleaved
324
+ # reasoning sanitizer targets assistant-message echo fields and
325
+ # must not strip keys such as tool_calls or reasoning_content
326
+ # from a typed directive.
327
+ sanitized_input.append(item)
328
+ continue
329
+ sanitized_item = _sanitize_interleaved_reasoning_input_item(item)
330
+ if sanitized_item is None:
331
+ continue
332
+ sanitized_input.append(_normalize_role_input_item(sanitized_item))
333
+ return sanitized_input
334
+
335
+
336
+ def _normalize_responses_input_instructions(data: JsonValue) -> JsonValue:
337
+ if not is_json_mapping(data):
338
+ return data
339
+ input_value = data.get("input")
340
+ if not is_json_list(input_value):
341
+ return data
342
+ # Codex deliberately places the Lite tool bundle and base instructions in
343
+ # the input prefix. Keep that wire shape intact instead of lifting its
344
+ # developer message into the top-level ``instructions`` field.
345
+ if responses_input_uses_lite_tools(input_value):
346
+ return data
347
+
348
+ instruction_parts: list[str] = []
349
+ input_items: list[JsonValue] = []
350
+ changed = False
351
+ for item in input_value:
352
+ item_mapping = _json_mapping_or_none(item)
353
+ if item_mapping is None:
354
+ input_items.append(item)
355
+ continue
356
+ # Only hoist actual message items (type omitted or "message"). Non-message
357
+ # typed system/developer items (e.g. the Codex responses-lite
358
+ # {"type": "additional_tools", "role": "developer", "tools": [...]} bundle, or
359
+ # any future typed item) carry no instruction content; hoisting used to drop
360
+ # them entirely, so pass them through untouched regardless of role.
361
+ if _is_preserved_non_message_directive(item_mapping):
362
+ input_items.append(item)
363
+ # Still counts as a normalization outcome: directive-only inputs
364
+ # must default top-level ``instructions`` (to "") so the request
365
+ # validates instead of failing on the required field.
366
+ changed = True
367
+ continue
368
+ role = item_mapping.get("role")
369
+ if role not in ("system", "developer"):
370
+ input_items.append(item)
371
+ continue
372
+ instruction_text, preserved_content = _split_responses_instruction_item_content(item_mapping)
373
+ if instruction_text:
374
+ instruction_parts.append(instruction_text)
375
+ if preserved_content is not None:
376
+ preserved_item = dict(item_mapping)
377
+ preserved_item["role"] = "user"
378
+ preserved_item["content"] = preserved_content
379
+ input_items.append(preserved_item)
380
+ changed = True
381
+
382
+ if not changed:
383
+ return data
384
+
385
+ normalized: MutableJsonObject = dict(data)
386
+ existing_instructions = normalized.get("instructions")
387
+ merged_instructions = _merge_responses_instructions(
388
+ existing_instructions if isinstance(existing_instructions, str) else "",
389
+ instruction_parts,
390
+ )
391
+ normalized["instructions"] = merged_instructions
392
+ normalized["input"] = input_items
393
+ return normalized
394
+
395
+
396
+ def _is_responses_lite_input(input_value: list[JsonValue]) -> bool:
397
+ # Responses Lite requests carry their tool bundle as an input item with
398
+ # type=additional_tools and deliberately place base instructions as a
399
+ # developer message in input (with empty top-level instructions). That
400
+ # shape is exactly what the lite upstream expects, so the instruction
401
+ # lift must leave the whole request untouched.
402
+ return any(
403
+ (mapping := _json_mapping_or_none(item)) is not None and mapping.get("type") == "additional_tools"
404
+ for item in input_value
405
+ )
406
+
407
+
408
+ def responses_input_uses_lite_tools(input_value: JsonValue) -> bool:
409
+ return is_json_list(input_value) and _is_responses_lite_input(input_value)
410
+
411
+
412
+ def _merge_responses_instructions(existing: str, extra_parts: list[str]) -> str:
413
+ extra = "\n".join(part for part in extra_parts if part)
414
+ if not extra:
415
+ return existing
416
+ if existing:
417
+ return f"{existing}\n{extra}"
418
+ return extra
419
+
420
+
421
+ def _split_responses_instruction_item_content(item: Mapping[str, JsonValue]) -> tuple[str, JsonValue | None]:
422
+ content = item.get("content")
423
+ if content is None:
424
+ return "", None
425
+ if isinstance(content, str):
426
+ return content, None
427
+ if is_json_list(content):
428
+ instruction_parts: list[str] = []
429
+ preserved_parts: list[JsonValue] = []
430
+ for part in _json_parts(content):
431
+ text = _responses_instruction_content_text(part)
432
+ if text is not None:
433
+ if text:
434
+ instruction_parts.append(text)
435
+ continue
436
+ preserved_parts.append(part)
437
+ preserved_content: JsonValue | None = preserved_parts if preserved_parts else None
438
+ return "\n".join(instruction_parts), preserved_content
439
+ text = _responses_instruction_content_text(content)
440
+ if text is not None:
441
+ return text, None
442
+ return "", content
443
+
444
+
445
+ def _responses_instruction_content_text(content: JsonValue) -> str | None:
446
+ if isinstance(content, str):
447
+ return content
448
+ content_mapping = _json_mapping_or_none(content)
449
+ if content_mapping is None:
450
+ return None
451
+ text = content_mapping.get("text")
452
+ return text if isinstance(text, str) else None
453
+
454
+
455
+ def _sanitize_interleaved_reasoning_input_item(item: JsonValue) -> JsonValue | None:
456
+ item_mapping = _json_mapping_or_none(item)
457
+ if item_mapping is None:
458
+ return item
459
+
460
+ sanitized_item: MutableJsonObject = {}
461
+ for key, value in item_mapping.items():
462
+ if key in _INTERLEAVED_REASONING_KEYS:
463
+ continue
464
+ if key == "content":
465
+ sanitized_content = _sanitize_interleaved_reasoning_content(value)
466
+ if sanitized_content is None:
467
+ continue
468
+ sanitized_item[key] = sanitized_content
469
+ continue
470
+ sanitized_item[key] = value
471
+ return sanitized_item
472
+
473
+
474
+ def _sanitize_interleaved_reasoning_content(content: JsonValue) -> JsonValue | None:
475
+ if is_json_list(content):
476
+ sanitized_parts: list[JsonValue] = []
477
+ for part in _json_parts(content):
478
+ sanitized_part = _sanitize_interleaved_reasoning_content_part(part)
479
+ if sanitized_part is None:
480
+ continue
481
+ sanitized_parts.append(sanitized_part)
482
+ return sanitized_parts
483
+ content_mapping = _json_mapping_or_none(content)
484
+ if content_mapping is not None:
485
+ return _sanitize_interleaved_reasoning_content_part(content_mapping)
486
+ return content
487
+
488
+
489
+ def _sanitize_interleaved_reasoning_content_part(part: JsonValue) -> JsonValue | None:
490
+ part_mapping = _json_mapping_or_none(part)
491
+ if part_mapping is None:
492
+ return part
493
+
494
+ part_type = part_mapping.get("type")
495
+ if isinstance(part_type, str) and part_type in _INTERLEAVED_REASONING_PART_TYPES:
496
+ return None
497
+
498
+ sanitized_part = dict(part_mapping)
499
+ for key in _INTERLEAVED_REASONING_KEYS:
500
+ sanitized_part.pop(key, None)
501
+ return sanitized_part
502
+
503
+
504
+ def _normalize_role_input_item(value: JsonValue) -> JsonValue:
505
+ value_mapping = _json_mapping_or_none(value)
506
+ if value_mapping is None:
507
+ return value
508
+ role = value_mapping.get("role")
509
+ if role == "assistant":
510
+ return _normalize_assistant_input_item(value_mapping)
511
+ if role == "tool":
512
+ return _normalize_tool_input_item(value_mapping)
513
+ return value
514
+
515
+
516
+ def _normalize_tool_input_item(value: Mapping[str, JsonValue]) -> JsonValue:
517
+ tool_call_id = value.get("tool_call_id")
518
+ tool_call_id_camel = value.get("toolCallId")
519
+ call_id = value.get("call_id")
520
+ resolved_call_id = tool_call_id if isinstance(tool_call_id, str) and tool_call_id else None
521
+ if resolved_call_id is None and isinstance(tool_call_id_camel, str) and tool_call_id_camel:
522
+ resolved_call_id = tool_call_id_camel
523
+ if resolved_call_id is None and isinstance(call_id, str) and call_id:
524
+ resolved_call_id = call_id
525
+ if not isinstance(resolved_call_id, str) or not resolved_call_id:
526
+ raise ValueError("tool input items must include 'tool_call_id'")
527
+ output = value.get("output")
528
+ output_value = output if output is not None else value.get("content")
529
+ return {
530
+ "type": "function_call_output",
531
+ "call_id": resolved_call_id,
532
+ "output": _normalize_tool_output_value(output_value),
533
+ }
534
+
535
+
536
+ def _normalize_tool_output_value(content: JsonValue) -> str:
537
+ if content is None:
538
+ return ""
539
+ if isinstance(content, str):
540
+ return content
541
+ if is_json_list(content):
542
+ parts: list[str] = []
543
+ for part in _json_parts(content):
544
+ if isinstance(part, str):
545
+ parts.append(part)
546
+ continue
547
+ extracted = _extract_text_content_part(part, _TOOL_TEXT_PART_TYPES)
548
+ if extracted is not None:
549
+ parts.append(extracted)
550
+ if parts:
551
+ return "".join(parts)
552
+ return json.dumps(content, ensure_ascii=False, separators=(",", ":"))
553
+ if is_json_mapping(content):
554
+ extracted = _extract_text_content_part(content, _TOOL_TEXT_PART_TYPES)
555
+ if extracted is not None:
556
+ return extracted
557
+ return json.dumps(content, ensure_ascii=False, separators=(",", ":"))
558
+ return str(content)
559
+
560
+
561
+ def _normalize_assistant_input_item(value: Mapping[str, JsonValue]) -> JsonValue:
562
+ content = value.get("content")
563
+ normalized_content = _normalize_assistant_content(content)
564
+ if normalized_content == content:
565
+ return value
566
+ updated = dict(value)
567
+ updated["content"] = normalized_content
568
+ return updated
569
+
570
+
571
+ def _normalize_assistant_content(content: JsonValue) -> JsonValue:
572
+ if content is None:
573
+ return None
574
+ if isinstance(content, str):
575
+ return cast(JsonValue, [{"type": "output_text", "text": content}])
576
+ if is_json_list(content):
577
+ return cast(JsonValue, [_normalize_assistant_content_part(part) for part in _json_parts(content)])
578
+ content_mapping = _json_mapping_or_none(content)
579
+ if content_mapping is not None:
580
+ return [_normalize_assistant_content_part(content_mapping)]
581
+ return content
582
+
583
+
584
+ def _normalize_assistant_content_part(part: JsonValue) -> JsonValue:
585
+ if isinstance(part, str):
586
+ return {"type": "output_text", "text": part}
587
+ if not is_json_mapping(part):
588
+ return part
589
+ text = _extract_text_content_part(part, _ASSISTANT_TEXT_PART_TYPES)
590
+ if text is not None:
591
+ return {"type": "output_text", "text": text}
592
+ return part
593
+
594
+
595
+ def _extract_text_content_part(part: JsonValue, allowed_types: frozenset[str]) -> str | None:
596
+ part_mapping = _json_mapping_or_none(part)
597
+ if part_mapping is None:
598
+ return None
599
+ part_type = part_mapping.get("type")
600
+ text = part_mapping.get("text")
601
+ if ((isinstance(part_type, str) and part_type in allowed_types) or part_type is None) and isinstance(text, str):
602
+ return text
603
+ refusal = part_mapping.get("refusal")
604
+ if isinstance(part_type, str) and part_type == "refusal" and isinstance(refusal, str):
605
+ return refusal
606
+ return None
607
+
608
+
609
+ def _json_list_or_none(value: JsonValue) -> list[JsonValue] | None:
610
+ if not is_json_list(value):
611
+ return None
612
+ return value
613
+
614
+
615
+ class ResponsesReasoning(BaseModel):
616
+ model_config = ConfigDict(extra="allow")
617
+
618
+ effort: str | None = None
619
+ summary: str | None = None
620
+
621
+
622
+ class ResponsesTextFormat(BaseModel):
623
+ model_config = ConfigDict(extra="allow", populate_by_name=True, serialize_by_alias=True)
624
+
625
+ type: str | None = None
626
+ strict: bool | None = None
627
+ schema_: PassthroughJsonValue = Field(default=None, alias="schema")
628
+ name: str | None = None
629
+
630
+ @field_validator("schema_")
631
+ @classmethod
632
+ def _validate_schema_depth(cls, value: JsonValue) -> JsonValue:
633
+ validate_passthrough_depth(value)
634
+ return value
635
+
636
+
637
+ class ResponsesTextControls(BaseModel):
638
+ model_config = ConfigDict(extra="allow")
639
+
640
+ verbosity: str | None = None
641
+ format: ResponsesTextFormat | None = None
642
+
643
+
644
+ class ResponsesRequest(BaseModel):
645
+ model_config = ConfigDict(extra="allow")
646
+ _codex_lb_client_reasoning_effort: str | None = PrivateAttr(default=None)
647
+ _codex_lb_provider_reasoning_effort_materialized: bool = PrivateAttr(default=False)
648
+
649
+ @model_validator(mode="before")
650
+ @classmethod
651
+ def _move_input_instruction_messages(cls, data: JsonValue) -> JsonValue:
652
+ return _normalize_responses_input_instructions(data)
653
+
654
+ model: str = Field(min_length=1)
655
+ instructions: str
656
+ input: PassthroughJsonValue
657
+ tools: PassthroughJsonList = Field(default_factory=list)
658
+ tool_choice: str | JsonObject | None = None
659
+ parallel_tool_calls: bool | None = None
660
+ reasoning: ResponsesReasoning | None = None
661
+ store: bool = False
662
+ stream: bool | None = None
663
+ include: list[str] = Field(default_factory=list)
664
+ service_tier: str | None = None
665
+ conversation: str | None = None
666
+ previous_response_id: str | None = None
667
+ truncation: str | None = None
668
+ prompt_cache_key: str | None = None
669
+ text: ResponsesTextControls | None = None
670
+
671
+ @field_validator("input")
672
+ @classmethod
673
+ def _validate_input_type(cls, value: JsonValue) -> JsonValue:
674
+ # ``input_file`` content items with a ``file_id`` are now allowed
675
+ # and forwarded verbatim. They reference uploads registered via
676
+ # ``POST /backend-api/files`` (see the file upload protocol),
677
+ # which lets large attachments bypass the 16 MiB websocket
678
+ # ceiling on `/responses`.
679
+ if isinstance(value, str):
680
+ normalized = _normalize_input_text(value)
681
+ return _sanitize_input_items(normalized)
682
+ if is_json_list(value):
683
+ validate_passthrough_depth(value)
684
+ return _sanitize_input_items(value)
685
+ raise ValueError("input must be a string or array")
686
+
687
+ @field_validator("include")
688
+ @classmethod
689
+ def _validate_include(cls, value: list[str]) -> list[str]:
690
+ for entry in value:
691
+ if entry not in _RESPONSES_INCLUDE_ALLOWLIST:
692
+ raise ValueError(f"Unsupported include value: {entry}")
693
+ return value
694
+
695
+ @field_validator("truncation")
696
+ @classmethod
697
+ def _validate_truncation(cls, value: str | None) -> str | None:
698
+ if value is None:
699
+ return value
700
+ if value not in {"auto", "disabled"}:
701
+ raise ValueError("truncation must be 'auto' or 'disabled'")
702
+ return value
703
+
704
+ @field_validator("store")
705
+ @classmethod
706
+ def _ensure_store_false(cls, value: bool | None) -> bool:
707
+ return False
708
+
709
+ @field_validator("previous_response_id")
710
+ @classmethod
711
+ def _normalize_previous_response_id(cls, value: str | None) -> str | None:
712
+ if value is None:
713
+ return value
714
+ stripped = value.strip()
715
+ return stripped or None
716
+
717
+ @field_validator("tools")
718
+ @classmethod
719
+ def _validate_tools(cls, value: list[JsonValue]) -> list[JsonValue]:
720
+ validated = validate_tool_types(value, allow_builtin_tools=True)
721
+ validate_passthrough_depth(validated)
722
+ return validated
723
+
724
+ @field_validator("tool_choice")
725
+ @classmethod
726
+ def _normalize_tool_choice_field(cls, value: JsonValue | None) -> JsonValue | None:
727
+ return normalize_tool_choice(value)
728
+
729
+ @field_validator("service_tier")
730
+ @classmethod
731
+ def _normalize_service_tier_field(cls, value: str | None) -> str | None:
732
+ if value is None:
733
+ return None
734
+ normalized = _normalize_service_tier_alias_value(value)
735
+ return normalized if isinstance(normalized, str) else value
736
+
737
+ @model_validator(mode="after")
738
+ def _validate_conversation(self) -> "ResponsesRequest":
739
+ if self.conversation and self.previous_response_id:
740
+ raise ValueError("Provide either 'conversation' or 'previous_response_id', not both.")
741
+ return self
742
+
743
+ def model_dump_for_forwarding(self) -> MutableJsonObject:
744
+ """Dump the request for re-serialization onto another wire.
745
+
746
+ Like ``model_dump(mode="json", exclude_none=True)`` but without
747
+ synthesizing fields the client never sent. Used by every path that
748
+ forwards this request as a JSON body — the multi-instance owner
749
+ forward (``HTTPBridgeOwnerClient``) and model-source Responses
750
+ egress — so that field omission survives the hop and the receiving
751
+ side does not re-mark ``tools`` as explicitly set.
752
+ """
753
+ payload: MutableJsonObject = self.model_dump(mode="json", exclude_none=True)
754
+ if "tools" not in self.model_fields_set:
755
+ # ``tools`` is declared with ``default_factory=list``, so
756
+ # ``model_dump(exclude_none=True)`` synthesizes an explicit
757
+ # ``"tools": []`` even when the client omitted the field. Codex
758
+ # Responses-Lite clients omit top-level ``tools`` entirely (the
759
+ # bundle rides in the ``additional_tools`` input item), and models
760
+ # with reserved model tools (e.g. ``collaboration.spawn_agent`` on
761
+ # gpt-5.6 ``multi_agent_version: v2``) reject any explicit
762
+ # ``tools`` param that cannot match the reserved schema. Only
763
+ # forward the field when the client actually sent it — including
764
+ # an explicit client-sent ``[]``. See issue #1184.
765
+ payload.pop("tools", None)
766
+ return payload
767
+
768
+ def to_payload(self) -> JsonObject:
769
+ payload = _strip_unsupported_fields(self.model_dump_for_forwarding())
770
+ _normalize_compaction_trigger_singleton(payload)
771
+ return payload
772
+
773
+ def to_replay_safety_payload(self) -> JsonObject:
774
+ return _strip_unsupported_fields(self.model_dump_for_forwarding(), strip_replayed_tool_call_namespaces=False)
775
+
776
+
777
+ class ResponsesCompactRequest(BaseModel):
778
+ model_config = ConfigDict(extra="allow")
779
+ _codex_lb_client_reasoning_effort: str | None = PrivateAttr(default=None)
780
+
781
+ @model_validator(mode="before")
782
+ @classmethod
783
+ def _move_input_instruction_messages(cls, data: JsonValue) -> JsonValue:
784
+ return _normalize_responses_input_instructions(data)
785
+
786
+ model: str = Field(min_length=1)
787
+ instructions: str
788
+ input: PassthroughJsonValue
789
+ reasoning: ResponsesReasoning | None = None
790
+ store: bool = False
791
+ service_tier: str | None = None
792
+ prompt_cache_key: str | None = None
793
+
794
+ @field_validator("input")
795
+ @classmethod
796
+ def _validate_input_type(cls, value: JsonValue) -> JsonValue:
797
+ # ``input_file`` content items with a ``file_id`` are forwarded
798
+ # verbatim; see ``ResponsesRequest._validate_input_type``.
799
+ if isinstance(value, str):
800
+ normalized = _normalize_input_text(value)
801
+ return _sanitize_input_items(normalized)
802
+ if is_json_list(value):
803
+ validate_passthrough_depth(value)
804
+ return _sanitize_input_items(value)
805
+ raise ValueError("input must be a string or array")
806
+
807
+ @model_validator(mode="before")
808
+ @classmethod
809
+ def _normalize_service_tier_aliases_before_validation(cls, data: JsonValue) -> JsonValue:
810
+ if not is_json_mapping(data):
811
+ return data
812
+ normalized = dict(data)
813
+ service_tier = normalized.get("service_tier")
814
+ normalized_service_tier = _normalize_service_tier_alias_value(service_tier)
815
+ if isinstance(normalized_service_tier, str):
816
+ normalized["service_tier"] = normalized_service_tier
817
+ return normalized
818
+
819
+ @field_validator("store")
820
+ @classmethod
821
+ def _ensure_store_false(cls, value: bool) -> bool:
822
+ return False
823
+
824
+ def to_payload(self) -> JsonObject:
825
+ payload: MutableJsonObject = self.model_dump(mode="json", exclude_none=True)
826
+ return _strip_compact_unsupported_fields(payload)
827
+
828
+
829
+ _UNSUPPORTED_UPSTREAM_FIELDS = {
830
+ "max_output_tokens",
831
+ "metadata",
832
+ "prompt_cache_retention",
833
+ "safety_identifier",
834
+ "temperature",
835
+ "top_p",
836
+ "truncation",
837
+ "user",
838
+ }
839
+
840
+
841
+ def responses_request_has_explicit_prompt_cache_controls(payload: ResponsesRequest) -> bool:
842
+ extra = payload.model_extra
843
+ if isinstance(extra, dict) and "prompt_cache_options" in extra:
844
+ return True
845
+ return _contains_explicit_prompt_cache_breakpoint(payload.input)
846
+
847
+
848
+ def _contains_explicit_prompt_cache_breakpoint(value: JsonValue) -> bool:
849
+ if isinstance(value, list):
850
+ return any(_contains_explicit_prompt_cache_breakpoint(item) for item in value)
851
+ if not isinstance(value, dict):
852
+ return False
853
+ value_type = value.get("type")
854
+ if (
855
+ isinstance(value_type, str)
856
+ and value_type in _EXPLICIT_PROMPT_CACHE_CONTENT_TYPES
857
+ and "prompt_cache_breakpoint" in value
858
+ ):
859
+ return True
860
+ return any(_contains_explicit_prompt_cache_breakpoint(child) for child in value.values())
861
+
862
+
863
+ def _strip_subscription_prompt_cache_controls(payload: MutableJsonObject) -> None:
864
+ payload.pop("prompt_cache_options", None)
865
+ _strip_subscription_prompt_cache_breakpoints(payload.get("input"))
866
+
867
+
868
+ def _strip_subscription_prompt_cache_breakpoints(value: JsonValue | None) -> None:
869
+ if isinstance(value, list):
870
+ for item in value:
871
+ _strip_subscription_prompt_cache_breakpoints(item)
872
+ return
873
+ if not isinstance(value, dict):
874
+ return
875
+ value_type = value.get("type")
876
+ if isinstance(value_type, str) and value_type in _EXPLICIT_PROMPT_CACHE_CONTENT_TYPES:
877
+ value.pop("prompt_cache_breakpoint", None)
878
+ for child in value.values():
879
+ _strip_subscription_prompt_cache_breakpoints(child)
880
+
881
+
882
+ def strip_replayed_tool_call_namespaces_from_payload(payload: MutableJsonObject) -> None:
883
+ input_value = payload.get("input")
884
+ if not is_json_list(input_value):
885
+ return
886
+ normalized_items: list[JsonValue] = []
887
+ changed = False
888
+ for item in input_value:
889
+ if not is_json_mapping(item):
890
+ normalized_items.append(item)
891
+ continue
892
+ item_type = item.get("type")
893
+ if isinstance(item_type, str) and item_type in _TOOL_CALL_ITEM_TYPES and "namespace" in item:
894
+ normalized_item = dict(item)
895
+ normalized_item.pop("namespace")
896
+ normalized_items.append(normalized_item)
897
+ changed = True
898
+ else:
899
+ normalized_items.append(item)
900
+ if changed:
901
+ payload["input"] = normalized_items
902
+
903
+
904
+ _POISONED_LOCAL_COMPACT_FALLBACK_TEXT = "Local compact fallback preserved the latest encrypted reasoning state."
905
+ _MAX_COMPACT_UPSTREAM_ESTIMATED_TOKENS = 100_000
906
+ _COMPACT_UPSTREAM_HEAD_ESTIMATED_TOKENS = 12_000
907
+ _ESTIMATED_CHARS_PER_TOKEN = 4
908
+ _COMPACT_OMITTED_INLINE_IMAGE_TEXT = (
909
+ "[compact trim] Omitted inline image bytes that were already observed before compaction"
910
+ )
911
+
912
+
913
+ def _strip_unsupported_fields(
914
+ payload: MutableJsonObject,
915
+ *,
916
+ strip_replayed_tool_call_namespaces: bool = True,
917
+ ) -> MutableJsonObject:
918
+ _normalize_openai_compatible_aliases(payload)
919
+ _normalize_service_tier_aliases(payload)
920
+ _strip_subscription_prompt_cache_controls(payload)
921
+ _sanitize_interleaved_reasoning_input(payload)
922
+ _strip_poisoned_local_compact_fallback_items(payload)
923
+ if strip_replayed_tool_call_namespaces:
924
+ strip_replayed_tool_call_namespaces_from_payload(payload)
925
+ # ``tools`` is deliberately NOT canonicalized here: the wire payload must
926
+ # forward client tool entries byte-preserved (array order, key order, and
927
+ # unknown keys untouched) so reserved model tools survive upstream
928
+ # byte/structural-equality checks. Order-insensitive canonicalization is
929
+ # cache-affinity/observability-only; see ``canonicalized_tools``.
930
+ for key in _UNSUPPORTED_UPSTREAM_FIELDS:
931
+ payload.pop(key, None)
932
+ return payload
933
+
934
+
935
+ def _strip_poisoned_local_compact_fallback_items(payload: MutableJsonObject) -> None:
936
+ input_value = payload.get("input")
937
+ if not is_json_list(input_value):
938
+ return
939
+
940
+ input_items = input_value
941
+ kept: list[JsonValue] = []
942
+ skip_next_poison_compaction = False
943
+ changed = False
944
+ for item in input_items:
945
+ if skip_next_poison_compaction and is_json_mapping(item) and item.get("type") == "compaction":
946
+ encrypted_content = item.get("encrypted_content")
947
+ if isinstance(encrypted_content, str) and encrypted_content:
948
+ skip_next_poison_compaction = False
949
+ changed = True
950
+ continue
951
+ skip_next_poison_compaction = False
952
+
953
+ if _is_poisoned_local_compact_fallback_message(item):
954
+ skip_next_poison_compaction = True
955
+ changed = True
956
+ continue
957
+
958
+ kept.append(item)
959
+
960
+ if changed:
961
+ payload["input"] = kept
962
+
963
+
964
+ def _is_poisoned_local_compact_fallback_message(item: JsonValue) -> bool:
965
+ if not is_json_mapping(item):
966
+ return False
967
+ if item.get("type") != "message" or item.get("role") != "assistant":
968
+ return False
969
+ content = item.get("content")
970
+ if not is_json_list(content):
971
+ return False
972
+ for part in content:
973
+ if not is_json_mapping(part):
974
+ continue
975
+ if part.get("text") == _POISONED_LOCAL_COMPACT_FALLBACK_TEXT:
976
+ return True
977
+ return False
978
+
979
+
980
+ def canonicalized_tools(tools: list[JsonValue]) -> list[JsonValue]:
981
+ """Return an order- and key-order-insensitive canonical form of ``tools``.
982
+
983
+ Used only for prompt-cache affinity/observability hashing (change #228):
984
+ two requests that differ solely in tool array order or object key order
985
+ hash identically. The result MUST NOT feed the upstream wire payload —
986
+ outgoing requests forward the client's tool entries byte-preserved (see
987
+ issue #1184). Array values (e.g. ``parameters.required``) are never
988
+ reordered; only mapping keys are sorted.
989
+ """
990
+ sorted_tools = sorted(tools, key=_tool_sort_key)
991
+ return [_sort_keys_recursive(t) for t in sorted_tools]
992
+
993
+
994
+ def _tool_sort_key(tool: JsonValue) -> str:
995
+ if not is_json_mapping(tool):
996
+ return ""
997
+ tool_map = tool
998
+ name = tool_map.get("name")
999
+ if isinstance(name, str):
1000
+ return name
1001
+ func = tool_map.get("function")
1002
+ if is_json_mapping(func):
1003
+ func_name = func.get("name")
1004
+ if isinstance(func_name, str):
1005
+ return func_name
1006
+ return ""
1007
+
1008
+
1009
+ def _sort_keys_recursive(value: JsonValue) -> JsonValue:
1010
+ if is_json_mapping(value):
1011
+ mapping = value
1012
+ return {k: _sort_keys_recursive(v) for k, v in sorted(mapping.items())}
1013
+ if is_json_list(value):
1014
+ return [_sort_keys_recursive(item) for item in value]
1015
+ return value
1016
+
1017
+
1018
+ def _strip_compact_unsupported_fields(payload: MutableJsonObject) -> MutableJsonObject:
1019
+ payload = _strip_unsupported_fields(payload)
1020
+ normalized_payload = _normalize_responses_input_instructions(payload)
1021
+ if is_json_mapping(normalized_payload):
1022
+ payload = dict(normalized_payload)
1023
+ _trim_compact_input_for_upstream(payload)
1024
+ _normalize_compaction_trigger_singleton(payload)
1025
+ payload.pop("store", None)
1026
+ payload.pop("text", None)
1027
+ payload.pop("tools", None)
1028
+ payload.pop("tool_choice", None)
1029
+ payload.pop("client_metadata", None)
1030
+ payload["parallel_tool_calls"] = False
1031
+ return payload
1032
+
1033
+
1034
+ def _normalize_compaction_trigger_singleton(payload: MutableJsonObject) -> None:
1035
+ input_value = payload.get("input")
1036
+ if not is_json_list(input_value) or not input_value:
1037
+ return
1038
+
1039
+ trigger_items = [item for item in input_value if is_json_mapping(item) and item.get("type") == "compaction_trigger"]
1040
+ if not trigger_items:
1041
+ return
1042
+
1043
+ payload["input"] = [
1044
+ item for item in input_value if not (is_json_mapping(item) and item.get("type") == "compaction_trigger")
1045
+ ]
1046
+ cast(list[JsonValue], payload["input"]).append({"type": "compaction_trigger"})
1047
+
1048
+
1049
+ def _trim_compact_input_for_upstream(payload: MutableJsonObject) -> None:
1050
+ input_value = payload.get("input")
1051
+ if not is_json_list(input_value):
1052
+ return
1053
+ total_tokens = _estimated_json_tokens(input_value)
1054
+ if total_tokens <= _MAX_COMPACT_UPSTREAM_ESTIMATED_TOKENS:
1055
+ return
1056
+
1057
+ has_continuity_anchor = _compact_has_continuity_anchor(payload)
1058
+ losslessly_trimmed_input = _compact_losslessly_trim_input(
1059
+ input_value,
1060
+ has_continuity_anchor=has_continuity_anchor,
1061
+ )
1062
+ if losslessly_trimmed_input is not None:
1063
+ payload["input"] = losslessly_trimmed_input
1064
+ return
1065
+
1066
+ token_counts = [_estimated_json_array_item_tokens(item) for item in input_value]
1067
+ required_indices = _compact_required_indices(
1068
+ input_value,
1069
+ token_counts,
1070
+ has_continuity_anchor=has_continuity_anchor,
1071
+ )
1072
+ required_input = _compact_trimmed_input_with_markers(input_value, token_counts, required_indices)
1073
+ if _estimated_json_tokens(required_input) <= _MAX_COMPACT_UPSTREAM_ESTIMATED_TOKENS:
1074
+ payload["input"] = required_input
1075
+ return
1076
+ rewritten_input, images_elided = _compact_elide_required_tool_output_images(
1077
+ input_value,
1078
+ required_indices=required_indices,
1079
+ )
1080
+ if images_elided:
1081
+ input_value = rewritten_input
1082
+ payload["input"] = input_value
1083
+ if _estimated_json_tokens(input_value) <= _MAX_COMPACT_UPSTREAM_ESTIMATED_TOKENS:
1084
+ return
1085
+ losslessly_trimmed_input = _compact_losslessly_trim_input(
1086
+ input_value,
1087
+ has_continuity_anchor=has_continuity_anchor,
1088
+ )
1089
+ if losslessly_trimmed_input is not None:
1090
+ payload["input"] = losslessly_trimmed_input
1091
+ return
1092
+ raise ClientPayloadError(
1093
+ "Compact input exceeds the upstream size limit and cannot be trimmed without removing required state anchors.",
1094
+ param="input",
1095
+ code="responses_compact_input_too_large",
1096
+ )
1097
+
1098
+
1099
+ def _compact_losslessly_trim_input(
1100
+ input_value: list[JsonValue],
1101
+ *,
1102
+ has_continuity_anchor: bool = False,
1103
+ ) -> list[JsonValue] | None:
1104
+ """Return a budget-fitting context selection without changing any retained bytes."""
1105
+
1106
+ token_counts = [_estimated_json_array_item_tokens(item) for item in input_value]
1107
+ head_count = _compact_trim_prefix_count(token_counts)
1108
+ state_anchor_indices = _compact_state_anchor_indices(input_value)
1109
+ marker_tokens = _estimated_json_array_item_tokens(_compact_trim_marker(omitted_items=0, omitted_tokens=0))
1110
+ side_effect_indices = _compact_side_effect_anchor_indices(input_value)
1111
+ unusable_side_effect_indices = {
1112
+ index
1113
+ for index, item in enumerate(input_value)
1114
+ if is_json_mapping(item)
1115
+ and _compact_item_is_side_effect_anchor(item)
1116
+ and (not isinstance(item.get("call_id"), str) or not item["call_id"])
1117
+ }
1118
+ side_effect_indices = _compact_reconciled_tool_call_indices(
1119
+ input_value,
1120
+ side_effect_indices,
1121
+ token_counts=token_counts,
1122
+ token_budget=sum(token_counts),
1123
+ )
1124
+ required_indices = _compact_required_indices(
1125
+ input_value,
1126
+ token_counts,
1127
+ preserved_indices=state_anchor_indices,
1128
+ has_continuity_anchor=has_continuity_anchor,
1129
+ )
1130
+ if required_indices & unusable_side_effect_indices:
1131
+ raise ClientPayloadError(
1132
+ "Compact input cannot retain a required side-effect call without a usable call_id.",
1133
+ param="input",
1134
+ code="responses_compact_input_too_large",
1135
+ )
1136
+ required_input = _compact_trimmed_input_with_markers(input_value, token_counts, required_indices)
1137
+ if _estimated_json_tokens(required_input) > _MAX_COMPACT_UPSTREAM_ESTIMATED_TOKENS:
1138
+ return None
1139
+ wire_budget = max(0, _MAX_COMPACT_UPSTREAM_ESTIMATED_TOKENS - marker_tokens)
1140
+ side_effect_indices &= _compact_reconciled_tool_call_indices(
1141
+ input_value,
1142
+ required_indices | side_effect_indices,
1143
+ token_counts=token_counts,
1144
+ token_budget=wire_budget,
1145
+ required_indices=required_indices,
1146
+ )
1147
+ selected_indices = set(state_anchor_indices)
1148
+ selected_indices.update(side_effect_indices)
1149
+ selected_indices.update(range(head_count))
1150
+ selected_tokens = sum(token_counts[index] for index in selected_indices)
1151
+ tail_budget = max(0, _MAX_COMPACT_UPSTREAM_ESTIMATED_TOKENS - selected_tokens - marker_tokens)
1152
+ selected_indices.update(
1153
+ _compact_trim_suffix_indices(
1154
+ token_counts,
1155
+ selected_indices=selected_indices,
1156
+ start_index=head_count,
1157
+ token_budget=tail_budget,
1158
+ )
1159
+ )
1160
+ selected_indices.update(required_indices)
1161
+ if has_continuity_anchor:
1162
+ _compact_discard_consumed_continuity_output_pairs(
1163
+ input_value,
1164
+ selected_indices=selected_indices,
1165
+ required_indices=required_indices,
1166
+ )
1167
+ selected_indices.difference_update(unusable_side_effect_indices)
1168
+ selected_indices = _compact_reconciled_tool_call_indices(
1169
+ input_value,
1170
+ selected_indices,
1171
+ token_counts=token_counts,
1172
+ token_budget=max(0, _MAX_COMPACT_UPSTREAM_ESTIMATED_TOKENS - marker_tokens),
1173
+ required_indices=required_indices | side_effect_indices,
1174
+ )
1175
+ selected_indices = _compact_fit_selected_indices_to_wire_budget(
1176
+ input_value,
1177
+ token_counts,
1178
+ selected_indices=selected_indices,
1179
+ required_indices=required_indices,
1180
+ priority_indices=side_effect_indices,
1181
+ )
1182
+ trimmed_input = (
1183
+ input_value
1184
+ if len(selected_indices) == len(input_value)
1185
+ else _compact_trimmed_input_with_markers(input_value, token_counts, selected_indices)
1186
+ )
1187
+ trimmed_tokens = _estimated_json_tokens(trimmed_input)
1188
+ if trimmed_tokens > _MAX_COMPACT_UPSTREAM_ESTIMATED_TOKENS:
1189
+ return None
1190
+ return trimmed_input
1191
+
1192
+
1193
+ def _compact_required_indices(
1194
+ input_value: list[JsonValue],
1195
+ token_counts: list[int],
1196
+ *,
1197
+ preserved_indices: set[int] | None = None,
1198
+ has_continuity_anchor: bool = False,
1199
+ ) -> set[int]:
1200
+ if preserved_indices is None:
1201
+ preserved_indices = _compact_state_anchor_indices(input_value)
1202
+ required_indices = _compact_reconciled_tool_call_indices(
1203
+ input_value,
1204
+ preserved_indices,
1205
+ token_counts=token_counts,
1206
+ token_budget=sum(token_counts),
1207
+ required_indices=preserved_indices,
1208
+ )
1209
+ terminal_indices, terminal_is_required, reconcile_terminal_pairs = _compact_terminal_required_indices(
1210
+ input_value,
1211
+ token_counts=token_counts,
1212
+ has_continuity_anchor=has_continuity_anchor,
1213
+ )
1214
+ if terminal_indices:
1215
+ prospective_required_indices = required_indices | terminal_indices
1216
+ if reconcile_terminal_pairs:
1217
+ prospective_required_indices = _compact_reconciled_tool_call_indices(
1218
+ input_value,
1219
+ prospective_required_indices,
1220
+ token_counts=token_counts,
1221
+ token_budget=sum(token_counts),
1222
+ required_indices=prospective_required_indices,
1223
+ )
1224
+ prospective_required_input = _compact_trimmed_input_with_markers(
1225
+ input_value,
1226
+ token_counts,
1227
+ prospective_required_indices,
1228
+ )
1229
+ if terminal_is_required or (
1230
+ _estimated_json_tokens(prospective_required_input) <= _MAX_COMPACT_UPSTREAM_ESTIMATED_TOKENS
1231
+ ):
1232
+ required_indices = prospective_required_indices
1233
+ return required_indices
1234
+
1235
+
1236
+ def _compact_has_continuity_anchor(payload: Mapping[str, JsonValue]) -> bool:
1237
+ return any(
1238
+ isinstance(payload.get(field), str) and bool(cast(str, payload[field]).strip())
1239
+ for field in ("previous_response_id", "conversation")
1240
+ )
1241
+
1242
+
1243
+ def _compact_discard_consumed_continuity_output_pairs(
1244
+ input_value: list[JsonValue],
1245
+ *,
1246
+ selected_indices: set[int],
1247
+ required_indices: set[int],
1248
+ ) -> None:
1249
+ if not input_value:
1250
+ return
1251
+ latest_index = len(input_value) - 1
1252
+ latest_mapping = _json_mapping_or_none(input_value[latest_index])
1253
+ latest_type = latest_mapping.get("type") if latest_mapping is not None else None
1254
+ if latest_type == "compaction_trigger":
1255
+ if latest_index == 0:
1256
+ return
1257
+ latest_index -= 1
1258
+ latest_mapping = _json_mapping_or_none(input_value[latest_index])
1259
+ latest_type = latest_mapping.get("type") if latest_mapping is not None else None
1260
+ if (
1261
+ latest_mapping is None
1262
+ or not isinstance(latest_type, str)
1263
+ or latest_type not in _COMPACT_TOOL_CALL_OUTPUT_ITEM_TYPES
1264
+ or _compact_matching_tool_call_index(input_value, latest_index) is not None
1265
+ ):
1266
+ return
1267
+ latest_call_id = latest_mapping.get("call_id")
1268
+ matching_call_type = _COMPACT_TOOL_CALL_TYPE_BY_OUTPUT_TYPE.get(latest_type)
1269
+ selected_indices.difference_update(
1270
+ index
1271
+ for index, item in enumerate(input_value[:latest_index])
1272
+ if is_json_mapping(item)
1273
+ and item.get("call_id") == latest_call_id
1274
+ and item.get("type") in {latest_type, matching_call_type}
1275
+ and index not in required_indices
1276
+ )
1277
+
1278
+
1279
+ def _compact_terminal_required_indices(
1280
+ input_value: list[JsonValue],
1281
+ *,
1282
+ token_counts: list[int],
1283
+ has_continuity_anchor: bool,
1284
+ ) -> tuple[set[int], bool, bool]:
1285
+ """Return terminal context and whether it must remain even when oversized."""
1286
+
1287
+ if not input_value:
1288
+ return set(), False, False
1289
+
1290
+ latest_index = len(input_value) - 1
1291
+ latest_mapping = _json_mapping_or_none(input_value[latest_index])
1292
+ latest_type = latest_mapping.get("type") if latest_mapping is not None else None
1293
+ terminal_trigger_indices: set[int] = set()
1294
+ if latest_type == "compaction_trigger":
1295
+ terminal_trigger_indices.add(latest_index)
1296
+ if latest_index == 0:
1297
+ return terminal_trigger_indices, True, False
1298
+ latest_index -= 1
1299
+ latest_mapping = _json_mapping_or_none(input_value[latest_index])
1300
+ latest_type = latest_mapping.get("type") if latest_mapping is not None else None
1301
+
1302
+ def with_terminal_trigger(indices: set[int]) -> set[int]:
1303
+ return indices | terminal_trigger_indices
1304
+
1305
+ if latest_type not in _COMPACT_TOOL_CALL_ITEM_TYPES | _COMPACT_TOOL_CALL_OUTPUT_ITEM_TYPES:
1306
+ return with_terminal_trigger({latest_index}), True, False
1307
+ if latest_mapping is not None and _compact_item_is_state_anchor(latest_mapping):
1308
+ terminal_indices = _compact_required_terminal_indices(input_value, latest_index, token_counts)
1309
+ return with_terminal_trigger(terminal_indices), True, True
1310
+ if latest_mapping is not None and _compact_item_has_elidable_inline_image(latest_mapping):
1311
+ terminal_indices = _compact_required_terminal_indices(input_value, latest_index, token_counts)
1312
+ return with_terminal_trigger(terminal_indices), True, True
1313
+ matching_call_index = _compact_matching_tool_call_index(input_value, latest_index)
1314
+ if latest_mapping is not None and _compact_terminal_item_is_side_effect(
1315
+ input_value,
1316
+ latest_index,
1317
+ matching_call_index=matching_call_index,
1318
+ ):
1319
+ terminal_indices = _compact_required_terminal_indices(input_value, latest_index, token_counts)
1320
+ return with_terminal_trigger(terminal_indices), True, True
1321
+ if latest_type in _COMPACT_TOOL_CALL_OUTPUT_ITEM_TYPES and has_continuity_anchor and matching_call_index is None:
1322
+ return with_terminal_trigger({latest_index}), True, False
1323
+ if latest_type in _COMPACT_TOOL_CALL_ITEM_TYPES:
1324
+ terminal_indices = _compact_required_terminal_indices(input_value, latest_index, token_counts)
1325
+ return with_terminal_trigger(terminal_indices), True, True
1326
+
1327
+ if terminal_trigger_indices:
1328
+ # The trigger is the only mandatory suffix sentinel. An ordinary
1329
+ # matched tool pair immediately before it remains best-effort context
1330
+ # and may be dropped when other anchors consume the wire budget.
1331
+ return terminal_trigger_indices, True, False
1332
+
1333
+ paired_tail = _compact_reconciled_tool_call_indices(
1334
+ input_value,
1335
+ with_terminal_trigger({latest_index}),
1336
+ token_counts=token_counts,
1337
+ token_budget=_MAX_COMPACT_UPSTREAM_ESTIMATED_TOKENS,
1338
+ )
1339
+ if latest_index in paired_tail:
1340
+ return paired_tail, False, False
1341
+ return terminal_trigger_indices, bool(terminal_trigger_indices), False
1342
+
1343
+
1344
+ def _compact_item_has_elidable_inline_image(item: JsonValue) -> bool:
1345
+ _, changed = _compact_elide_inline_images(item)
1346
+ return changed
1347
+
1348
+
1349
+ def _compact_matching_tool_call_index(input_value: list[JsonValue], output_index: int) -> int | None:
1350
+ output = _json_mapping_or_none(input_value[output_index])
1351
+ if output is None:
1352
+ return None
1353
+ call_id = output.get("call_id")
1354
+ if not isinstance(call_id, str) or not call_id:
1355
+ return None
1356
+ output_type = output.get("type")
1357
+ expected_call_type = (
1358
+ _COMPACT_TOOL_CALL_TYPE_BY_OUTPUT_TYPE.get(output_type) if isinstance(output_type, str) else None
1359
+ )
1360
+ if expected_call_type is None:
1361
+ return None
1362
+ unmatched_call_indices: list[int] = []
1363
+ for index in range(output_index):
1364
+ item = _json_mapping_or_none(input_value[index])
1365
+ if item is None or item.get("call_id") != call_id:
1366
+ continue
1367
+ if item.get("type") == expected_call_type:
1368
+ unmatched_call_indices.append(index)
1369
+ elif item.get("type") == output_type and unmatched_call_indices:
1370
+ unmatched_call_indices.pop()
1371
+ return unmatched_call_indices[-1] if unmatched_call_indices else None
1372
+
1373
+
1374
+ def _compact_terminal_item_is_side_effect(
1375
+ input_value: list[JsonValue],
1376
+ index: int,
1377
+ *,
1378
+ matching_call_index: int | None,
1379
+ ) -> bool:
1380
+ item = _json_mapping_or_none(input_value[index])
1381
+ if item is None:
1382
+ return False
1383
+ if item.get("type") in _COMPACT_SIDE_EFFECT_TOOL_ITEM_TYPES or _compact_item_is_side_effect_anchor(item):
1384
+ return True
1385
+ if matching_call_index is None:
1386
+ return False
1387
+ matching_call = _json_mapping_or_none(input_value[matching_call_index])
1388
+ return matching_call is not None and (
1389
+ matching_call.get("type") in _COMPACT_SIDE_EFFECT_TOOL_ITEM_TYPES
1390
+ or _compact_item_is_side_effect_anchor(matching_call)
1391
+ )
1392
+
1393
+
1394
+ def _compact_required_terminal_indices(
1395
+ input_value: list[JsonValue],
1396
+ latest_index: int,
1397
+ token_counts: list[int],
1398
+ ) -> set[int]:
1399
+ return _compact_reconciled_tool_call_indices(
1400
+ input_value,
1401
+ {latest_index},
1402
+ token_counts=token_counts,
1403
+ token_budget=sum(token_counts),
1404
+ required_indices={latest_index},
1405
+ )
1406
+
1407
+
1408
+ def _compact_elide_required_tool_output_images(
1409
+ input_value: list[JsonValue],
1410
+ *,
1411
+ required_indices: set[int],
1412
+ ) -> tuple[list[JsonValue], bool]:
1413
+ rewritten: list[JsonValue] = []
1414
+ changed = False
1415
+ for index, item in enumerate(input_value):
1416
+ if (
1417
+ index in required_indices
1418
+ and is_json_mapping(item)
1419
+ and item.get("type") in _COMPACT_TOOL_CALL_OUTPUT_ITEM_TYPES
1420
+ ):
1421
+ rewritten_item, item_changed = _compact_elide_inline_images(item)
1422
+ rewritten.append(rewritten_item)
1423
+ changed = changed or item_changed
1424
+ else:
1425
+ rewritten.append(item)
1426
+ return rewritten, changed
1427
+
1428
+
1429
+ def _compact_elide_inline_images(value: JsonValue) -> tuple[JsonValue, bool]:
1430
+ """Replace inline image bytes with an explicit compact-only text marker."""
1431
+
1432
+ if is_json_mapping(value):
1433
+ if value.get("type") == "input_image":
1434
+ image_url = value.get("image_url")
1435
+ if isinstance(image_url, str) and image_url.startswith("data:image/"):
1436
+ return (
1437
+ {
1438
+ "type": "input_text",
1439
+ "text": f"{_COMPACT_OMITTED_INLINE_IMAGE_TEXT} ({len(image_url)} encoded characters).",
1440
+ },
1441
+ True,
1442
+ )
1443
+ if value.get("type") == "image_url":
1444
+ image_url = value.get("image_url")
1445
+ url = image_url.get("url") if is_json_mapping(image_url) else image_url
1446
+ if isinstance(url, str) and url.startswith("data:image/"):
1447
+ return (
1448
+ {
1449
+ "type": "text",
1450
+ "text": f"{_COMPACT_OMITTED_INLINE_IMAGE_TEXT} ({len(url)} encoded characters).",
1451
+ },
1452
+ True,
1453
+ )
1454
+ rewritten_mapping: JsonObject = {}
1455
+ changed = False
1456
+ for key, item in value.items():
1457
+ rewritten_item, item_changed = _compact_elide_inline_images(item)
1458
+ rewritten_mapping[key] = rewritten_item
1459
+ changed = changed or item_changed
1460
+ return rewritten_mapping, changed
1461
+ if is_json_list(value):
1462
+ rewritten: list[JsonValue] = []
1463
+ changed = False
1464
+ for item in value:
1465
+ rewritten_item, item_changed = _compact_elide_inline_images(item)
1466
+ rewritten.append(rewritten_item)
1467
+ changed = changed or item_changed
1468
+ return rewritten, changed
1469
+ if isinstance(value, str) and "data:image/" in value:
1470
+ rewritten_value, replacements = _COMPACT_INLINE_IMAGE_DATA_URL_RE.subn(
1471
+ lambda match: f"{_COMPACT_OMITTED_INLINE_IMAGE_TEXT} ({len(match.group(0))} encoded characters).",
1472
+ value,
1473
+ )
1474
+ if replacements:
1475
+ return rewritten_value, True
1476
+ return value, False
1477
+
1478
+
1479
+ def _compact_fit_selected_indices_to_wire_budget(
1480
+ input_value: list[JsonValue],
1481
+ token_counts: list[int],
1482
+ *,
1483
+ selected_indices: set[int],
1484
+ required_indices: set[int],
1485
+ priority_indices: set[int] | None = None,
1486
+ ) -> set[int]:
1487
+ """Drop best-effort middle context until the exact serialized input fits."""
1488
+
1489
+ selected = set(selected_indices)
1490
+ prioritized = priority_indices or set()
1491
+
1492
+ def optional_drop_key(index: int) -> tuple[int, int, int]:
1493
+ if index in prioritized:
1494
+ return (0, 0, -index)
1495
+ return (1, min(index, len(input_value) - 1 - index), index)
1496
+
1497
+ optional_indices = sorted(
1498
+ selected - required_indices,
1499
+ key=optional_drop_key,
1500
+ reverse=True,
1501
+ )
1502
+ marker_budget = max(
1503
+ 0,
1504
+ _MAX_COMPACT_UPSTREAM_ESTIMATED_TOKENS
1505
+ - _estimated_json_array_item_tokens(_compact_trim_marker(omitted_items=0, omitted_tokens=0)),
1506
+ )
1507
+ for index in optional_indices:
1508
+ candidate_input = _compact_trimmed_input_with_markers(input_value, token_counts, selected)
1509
+ if _estimated_json_tokens(candidate_input) <= _MAX_COMPACT_UPSTREAM_ESTIMATED_TOKENS:
1510
+ break
1511
+ if index not in selected:
1512
+ continue
1513
+ trial = set(selected)
1514
+ trial.remove(index)
1515
+ trial = _compact_reconciled_tool_call_indices(
1516
+ input_value,
1517
+ trial,
1518
+ token_counts=token_counts,
1519
+ token_budget=marker_budget,
1520
+ required_indices=required_indices,
1521
+ allow_pair_additions=False,
1522
+ )
1523
+ if required_indices <= trial and trial != selected:
1524
+ selected = trial
1525
+ return selected
1526
+
1527
+
1528
+ def validate_compact_input_wire_budget(payload: Mapping[str, JsonValue]) -> None:
1529
+ """Reject compact input that exceeds the upstream budget after transformations."""
1530
+
1531
+ input_value = payload.get("input")
1532
+ if not is_json_list(input_value):
1533
+ return
1534
+ if _estimated_json_tokens(input_value) <= _MAX_COMPACT_UPSTREAM_ESTIMATED_TOKENS:
1535
+ return
1536
+ raise ClientPayloadError(
1537
+ "Compact input exceeds the upstream size limit after preparing the final wire payload.",
1538
+ param="input",
1539
+ code="responses_compact_input_too_large",
1540
+ error_type="invalid_request_error",
1541
+ )
1542
+
1543
+
1544
+ def _compact_state_anchor_indices(input_value: list[JsonValue]) -> set[int]:
1545
+ preserved_indices: set[int] = set()
1546
+ for index, item in enumerate(input_value):
1547
+ if not is_json_mapping(item):
1548
+ continue
1549
+ item_mapping = item
1550
+ if item_mapping.get("type") == "additional_tools":
1551
+ preserved_indices.add(index)
1552
+ developer_index = index + 1
1553
+ if developer_index < len(input_value):
1554
+ developer_item = input_value[developer_index]
1555
+ if is_json_mapping(developer_item) and developer_item.get("role") == "developer":
1556
+ developer_type = developer_item.get("type")
1557
+ if developer_type is None or developer_type == "message":
1558
+ preserved_indices.add(developer_index)
1559
+ if _is_preserved_non_message_directive(item_mapping):
1560
+ preserved_indices.add(index)
1561
+ if _compact_item_is_state_anchor(item_mapping):
1562
+ preserved_indices.add(index)
1563
+ return preserved_indices
1564
+
1565
+
1566
+ def _compact_side_effect_anchor_indices(input_value: list[JsonValue]) -> set[int]:
1567
+ preserved_indices: set[int] = set()
1568
+ for index, item in enumerate(input_value):
1569
+ if (
1570
+ is_json_mapping(item)
1571
+ and _compact_item_is_side_effect_anchor(item)
1572
+ and isinstance(item.get("call_id"), str)
1573
+ and item["call_id"]
1574
+ ):
1575
+ preserved_indices.add(index)
1576
+ return preserved_indices
1577
+
1578
+
1579
+ def _compact_reconciled_tool_call_indices(
1580
+ input_value: list[JsonValue],
1581
+ selected_indices: set[int],
1582
+ *,
1583
+ token_counts: list[int],
1584
+ token_budget: int,
1585
+ required_indices: set[int] | None = None,
1586
+ allow_pair_additions: bool = True,
1587
+ ) -> set[int]:
1588
+ call_indices_by_key: dict[tuple[str, str], list[int]] = {}
1589
+ output_indices_by_key: dict[tuple[str, str], list[int]] = {}
1590
+ for index, item in enumerate(input_value):
1591
+ if not is_json_mapping(item):
1592
+ continue
1593
+ call_id = item.get("call_id")
1594
+ if not isinstance(call_id, str) or not call_id:
1595
+ continue
1596
+ item_type = item.get("type")
1597
+ if isinstance(item_type, str) and item_type in _COMPACT_TOOL_CALL_ITEM_TYPES:
1598
+ call_indices_by_key.setdefault((call_id, item_type), []).append(index)
1599
+ elif isinstance(item_type, str):
1600
+ matching_call_type = _COMPACT_TOOL_CALL_TYPE_BY_OUTPUT_TYPE.get(item_type)
1601
+ if matching_call_type is not None:
1602
+ output_indices_by_key.setdefault((call_id, matching_call_type), []).append(index)
1603
+
1604
+ reconciled = set(selected_indices)
1605
+ protected_indices = required_indices or set()
1606
+ selected_tokens = sum(token_counts[index] for index in reconciled)
1607
+
1608
+ def add_indices(indices: Iterable[int]) -> bool:
1609
+ nonlocal selected_tokens
1610
+ missing_indices = [index for index in indices if index not in reconciled]
1611
+ missing_tokens = sum(token_counts[index] for index in missing_indices)
1612
+ if selected_tokens + missing_tokens > token_budget:
1613
+ return False
1614
+ reconciled.update(missing_indices)
1615
+ selected_tokens += missing_tokens
1616
+ return True
1617
+
1618
+ def remove_indices(indices: Iterable[int]) -> None:
1619
+ nonlocal selected_tokens
1620
+ for index in indices:
1621
+ if index in reconciled and index not in protected_indices:
1622
+ reconciled.remove(index)
1623
+ selected_tokens -= token_counts[index]
1624
+
1625
+ def matching_call_index(call_indices: list[int], output_indices: list[int], output_index: int) -> int | None:
1626
+ unmatched_calls: list[int] = []
1627
+ call_index_set = set(call_indices)
1628
+ output_index_set = set(output_indices)
1629
+ for index in range(output_index):
1630
+ if index in call_index_set:
1631
+ unmatched_calls.append(index)
1632
+ elif index in output_index_set and unmatched_calls:
1633
+ unmatched_calls.pop()
1634
+ return unmatched_calls[-1] if unmatched_calls else None
1635
+
1636
+ def matching_output_indices(call_indices: list[int], call_index: int, output_indices: list[int]) -> list[int]:
1637
+ next_call_indices = [next_call_index for next_call_index in call_indices if next_call_index > call_index]
1638
+ next_call_index = next_call_indices[0] if next_call_indices else None
1639
+ return [
1640
+ output_index
1641
+ for output_index in output_indices
1642
+ if output_index > call_index and (next_call_index is None or output_index < next_call_index)
1643
+ ][:1]
1644
+
1645
+ for pair_key, output_indices in output_indices_by_key.items():
1646
+ selected_outputs = [index for index in output_indices if index in reconciled]
1647
+ if not selected_outputs:
1648
+ continue
1649
+ call_indices = call_indices_by_key.get(pair_key, [])
1650
+ for output_index in selected_outputs:
1651
+ call_index = matching_call_index(call_indices, output_indices, output_index)
1652
+ if call_index is None:
1653
+ remove_indices([output_index])
1654
+ elif not allow_pair_additions and call_index not in reconciled:
1655
+ remove_indices([output_index])
1656
+ elif not add_indices([call_index]):
1657
+ remove_indices([output_index])
1658
+ for pair_key, call_indices in call_indices_by_key.items():
1659
+ output_indices = output_indices_by_key.get(pair_key, [])
1660
+ for call_index in call_indices:
1661
+ if call_index not in reconciled:
1662
+ continue
1663
+ matched_output_indices = matching_output_indices(call_indices, call_index, output_indices)
1664
+ if not matched_output_indices:
1665
+ remove_indices([call_index])
1666
+ continue
1667
+ if not allow_pair_additions and any(index not in reconciled for index in matched_output_indices):
1668
+ remove_indices([call_index, *matched_output_indices])
1669
+ continue
1670
+ if not add_indices(matched_output_indices):
1671
+ remove_indices([call_index, *matched_output_indices])
1672
+ return reconciled
1673
+
1674
+
1675
+ def _compact_item_is_state_anchor(item: Mapping[str, JsonValue]) -> bool:
1676
+ item_type = item.get("type")
1677
+ if item_type == "additional_tools":
1678
+ return True
1679
+ if item_type in {None, "message"} and item.get("role") in {"system", "developer"}:
1680
+ return True
1681
+ if item_type == "function_call":
1682
+ name = item.get("name")
1683
+ if isinstance(name, str) and name in _COMPACT_STATE_TOOL_NAMES:
1684
+ return True
1685
+ function = item.get("function")
1686
+ if is_json_mapping(function):
1687
+ function_name = function.get("name")
1688
+ if isinstance(function_name, str) and function_name in _COMPACT_STATE_TOOL_NAMES:
1689
+ return True
1690
+ for text in _compact_item_texts(item):
1691
+ stripped = text.lstrip()
1692
+ if stripped.startswith(_GOAL_CONTINUATION_CONTEXT_PREFIX):
1693
+ return True
1694
+ if stripped.startswith(_PLAN_MODE_CONTEXT_PREFIX):
1695
+ return True
1696
+ return False
1697
+
1698
+
1699
+ def _compact_item_is_side_effect_anchor(item: Mapping[str, JsonValue]) -> bool:
1700
+ return is_downstream_side_effect_tool_call_item(item)
1701
+
1702
+
1703
+ def _compact_item_texts(item: Mapping[str, JsonValue]) -> list[str]:
1704
+ content = item.get("content")
1705
+ if isinstance(content, str):
1706
+ return [content]
1707
+ if is_json_mapping(content):
1708
+ content_parts: list[JsonValue] = [content]
1709
+ elif is_json_list(content):
1710
+ content_parts = content
1711
+ else:
1712
+ return []
1713
+
1714
+ texts: list[str] = []
1715
+ for part in content_parts:
1716
+ if isinstance(part, str):
1717
+ texts.append(part)
1718
+ continue
1719
+ if not is_json_mapping(part):
1720
+ continue
1721
+ text = part.get("text")
1722
+ if isinstance(text, str):
1723
+ texts.append(text)
1724
+ return texts
1725
+
1726
+
1727
+ def responses_input_contains_goal_continuation_context(input_value: JsonValue) -> bool:
1728
+ """Return whether Responses input carries Codex's goal-continuation marker."""
1729
+
1730
+ if not is_json_list(input_value):
1731
+ return False
1732
+ for item in input_value:
1733
+ if not is_json_mapping(item):
1734
+ continue
1735
+ for text in _compact_item_texts(item):
1736
+ if text.lstrip().startswith(_GOAL_CONTINUATION_CONTEXT_PREFIX):
1737
+ return True
1738
+ return False
1739
+
1740
+
1741
+ def responses_request_contains_goal_continuation_context(payload: ResponsesRequest) -> bool:
1742
+ """Return whether a normalized request carries Codex's goal restart marker."""
1743
+
1744
+ # ResponsesRequest normalization lifts developer/system input messages into
1745
+ # ``instructions``. The marker can therefore disappear from ``input`` and
1746
+ # follow pre-existing instruction text by the time affinity is classified.
1747
+ # Keep both locations in this check or a harmless parser refactor can
1748
+ # silently break restart recovery while marker-preservation tests still pass.
1749
+ instructions = payload.instructions
1750
+ if isinstance(instructions, str) and any(
1751
+ line.lstrip().startswith(_GOAL_CONTINUATION_CONTEXT_PREFIX) for line in instructions.splitlines()
1752
+ ):
1753
+ return True
1754
+ return responses_input_contains_goal_continuation_context(payload.input)
1755
+
1756
+
1757
+ def _compact_trimmed_input_with_markers(
1758
+ input_value: list[JsonValue], token_counts: list[int], selected_indices: set[int]
1759
+ ) -> list[JsonValue]:
1760
+ trimmed: list[JsonValue] = []
1761
+ omitted_items = 0
1762
+ omitted_tokens = 0
1763
+ for index, item in enumerate(input_value):
1764
+ if index in selected_indices:
1765
+ if omitted_items:
1766
+ trimmed.append(_compact_trim_marker(omitted_items=omitted_items, omitted_tokens=omitted_tokens))
1767
+ omitted_items = 0
1768
+ omitted_tokens = 0
1769
+ trimmed.append(item)
1770
+ else:
1771
+ omitted_items += 1
1772
+ omitted_tokens += token_counts[index]
1773
+ if omitted_items:
1774
+ trimmed.append(_compact_trim_marker(omitted_items=omitted_items, omitted_tokens=omitted_tokens))
1775
+ return trimmed
1776
+
1777
+
1778
+ def _compact_trim_prefix_count(token_counts: list[int]) -> int:
1779
+ used = 0
1780
+ count = 0
1781
+ for token_count in token_counts:
1782
+ if used + token_count > _COMPACT_UPSTREAM_HEAD_ESTIMATED_TOKENS:
1783
+ break
1784
+ used += token_count
1785
+ count += 1
1786
+ return count
1787
+
1788
+
1789
+ def _compact_trim_suffix_indices(
1790
+ token_counts: list[int], *, selected_indices: set[int], start_index: int, token_budget: int
1791
+ ) -> set[int]:
1792
+ used = 0
1793
+ indices: set[int] = set()
1794
+ for index in range(len(token_counts) - 1, start_index - 1, -1):
1795
+ if index in selected_indices:
1796
+ continue
1797
+ token_count = token_counts[index]
1798
+ if indices and used + token_count > token_budget:
1799
+ break
1800
+ if not indices and token_count > token_budget:
1801
+ indices.add(index)
1802
+ break
1803
+ used += token_count
1804
+ indices.add(index)
1805
+ return indices
1806
+
1807
+
1808
+ def _compact_trim_marker(*, omitted_items: int, omitted_tokens: int) -> JsonObject:
1809
+ return {
1810
+ "type": "message",
1811
+ "role": "user",
1812
+ "content": [
1813
+ {
1814
+ "type": "input_text",
1815
+ "text": (
1816
+ "[compact trim] Omitted "
1817
+ f"{omitted_items} input items (~{omitted_tokens} estimated tokens) "
1818
+ "before forwarding this oversized compact request upstream. Required compact "
1819
+ "state anchors and retained input items remain in their original order; "
1820
+ "omitted items may include terminal context."
1821
+ ),
1822
+ }
1823
+ ],
1824
+ }
1825
+
1826
+
1827
+ def _estimated_json_tokens(value: JsonValue) -> int:
1828
+ # Match stdlib/aiohttp's default JSON wire escaping and separators. Calling
1829
+ # this on the whole input list also counts brackets, commas, and inter-item
1830
+ # whitespace, so many individually small items cannot bypass the cap.
1831
+ serialized = json.dumps(value, ensure_ascii=True, sort_keys=True)
1832
+ return max(1, (len(serialized) + _ESTIMATED_CHARS_PER_TOKEN - 1) // _ESTIMATED_CHARS_PER_TOKEN)
1833
+
1834
+
1835
+ def _estimated_json_array_item_tokens(value: JsonValue) -> int:
1836
+ serialized = json.dumps(value, ensure_ascii=True, sort_keys=True)
1837
+ # Every retained item also needs the array's comma-space delimiter. The
1838
+ # final whole-array validation below accounts exactly for brackets and the
1839
+ # missing delimiter after the last item.
1840
+ wire_chars = len(serialized) + 2
1841
+ return max(1, (wire_chars + _ESTIMATED_CHARS_PER_TOKEN - 1) // _ESTIMATED_CHARS_PER_TOKEN)
1842
+
1843
+
1844
+ def _sanitize_interleaved_reasoning_input(payload: MutableJsonObject) -> None:
1845
+ input_value = payload.get("input")
1846
+ input_items = _json_list_or_none(input_value)
1847
+ if input_items is None:
1848
+ return
1849
+ payload["input"] = _sanitize_input_items(input_items)
1850
+
1851
+
1852
+ def normalize_reasoning_aliases(payload: MutableJsonObject) -> None:
1853
+ reasoning_effort = payload.pop("reasoningEffort", None)
1854
+ snake_case_reasoning_effort = payload.pop("reasoning_effort", None)
1855
+ reasoning_summary = payload.pop("reasoningSummary", None)
1856
+ provider_thinking = payload.pop("thinking", None)
1857
+ provider_enable_thinking = payload.pop("enable_thinking", None)
1858
+
1859
+ reasoning_payload = _json_mapping_or_none(payload.get("reasoning"))
1860
+ if reasoning_payload is not None:
1861
+ reasoning_map: MutableJsonObject = dict(reasoning_payload.items())
1862
+ else:
1863
+ reasoning_map = {}
1864
+
1865
+ existing_effort = reasoning_map.get("effort")
1866
+ if isinstance(existing_effort, str) and not existing_effort.strip():
1867
+ reasoning_map.pop("effort")
1868
+
1869
+ alias_effort = next(
1870
+ (
1871
+ candidate.strip()
1872
+ for candidate in (reasoning_effort, snake_case_reasoning_effort)
1873
+ if isinstance(candidate, str) and candidate.strip()
1874
+ ),
1875
+ None,
1876
+ )
1877
+ if alias_effort is not None and "effort" not in reasoning_map:
1878
+ reasoning_map["effort"] = alias_effort
1879
+ if isinstance(reasoning_summary, str) and "summary" not in reasoning_map:
1880
+ reasoning_map["summary"] = reasoning_summary
1881
+
1882
+ provider_reasoning = _normalize_thinking_alias(
1883
+ provider_thinking,
1884
+ enable_thinking=provider_enable_thinking,
1885
+ )
1886
+ if provider_reasoning is not None:
1887
+ if "effort" not in reasoning_map and "effort" in provider_reasoning:
1888
+ reasoning_map["effort"] = provider_reasoning["effort"]
1889
+ if "summary" not in reasoning_map and "summary" in provider_reasoning:
1890
+ reasoning_map["summary"] = provider_reasoning["summary"]
1891
+
1892
+ if reasoning_map:
1893
+ payload["reasoning"] = reasoning_map
1894
+
1895
+
1896
+ def _normalize_thinking_alias(
1897
+ thinking: JsonValue,
1898
+ *,
1899
+ enable_thinking: JsonValue,
1900
+ ) -> MutableJsonObject | None:
1901
+ if isinstance(thinking, bool):
1902
+ if thinking:
1903
+ return {"effort": "medium"}
1904
+ if isinstance(thinking, str):
1905
+ normalized = thinking.strip().lower()
1906
+ if normalized in {"minimal", "low", "medium", "high", "xhigh", "max", "ultra"}:
1907
+ return {"effort": normalized}
1908
+ if normalized in {"enabled", "true", "on"}:
1909
+ return {"effort": "medium"}
1910
+ thinking_mapping = _json_mapping_or_none(thinking)
1911
+ if thinking_mapping is not None:
1912
+ normalized: MutableJsonObject = {}
1913
+ effort = thinking_mapping.get("effort")
1914
+ summary = thinking_mapping.get("summary")
1915
+ if isinstance(effort, str) and effort.strip():
1916
+ normalized["effort"] = effort.strip().lower()
1917
+ if isinstance(summary, str) and summary.strip():
1918
+ normalized["summary"] = summary.strip()
1919
+ thinking_type = thinking_mapping.get("type")
1920
+ if "effort" not in normalized and isinstance(thinking_type, str) and thinking_type.strip().lower() == "enabled":
1921
+ normalized["effort"] = "medium"
1922
+ enabled = thinking_mapping.get("enabled")
1923
+ if "effort" not in normalized and enabled is True:
1924
+ normalized["effort"] = "medium"
1925
+ if "effort" not in normalized and enable_thinking is True:
1926
+ normalized["effort"] = "medium"
1927
+ if normalized:
1928
+ return normalized
1929
+
1930
+ # Disabled `thinking` spellings are inactive, not authoritative: a
1931
+ # separate enabled alias must still participate in policy evaluation.
1932
+ if isinstance(enable_thinking, bool):
1933
+ return {"effort": "medium"} if enable_thinking else None
1934
+ return None
1935
+
1936
+
1937
+ def _normalize_openai_compatible_aliases(payload: MutableJsonObject) -> None:
1938
+ text_verbosity = payload.pop("textVerbosity", None)
1939
+ top_level_verbosity = payload.pop("verbosity", None)
1940
+ prompt_cache_key = payload.pop("promptCacheKey", None)
1941
+ prompt_cache_retention = payload.pop("promptCacheRetention", None)
1942
+
1943
+ if isinstance(prompt_cache_key, str) and "prompt_cache_key" not in payload:
1944
+ payload["prompt_cache_key"] = prompt_cache_key
1945
+ if isinstance(prompt_cache_retention, str) and "prompt_cache_retention" not in payload:
1946
+ payload["prompt_cache_retention"] = prompt_cache_retention
1947
+
1948
+ normalize_reasoning_aliases(payload)
1949
+
1950
+ text_payload = _json_mapping_or_none(payload.get("text"))
1951
+ if text_payload is not None:
1952
+ text_map: MutableJsonObject = dict(text_payload.items())
1953
+ else:
1954
+ text_map = {}
1955
+
1956
+ if isinstance(text_verbosity, str) and "verbosity" not in text_map:
1957
+ text_map["verbosity"] = text_verbosity
1958
+ if isinstance(top_level_verbosity, str) and "verbosity" not in text_map:
1959
+ text_map["verbosity"] = top_level_verbosity
1960
+ if text_map:
1961
+ payload["text"] = text_map
1962
+
1963
+
1964
+ def _normalize_service_tier_aliases(payload: MutableJsonObject) -> None:
1965
+ service_tier = payload.get("service_tier")
1966
+ normalized = _normalize_service_tier_alias_value(service_tier)
1967
+ if isinstance(normalized, str):
1968
+ payload["service_tier"] = normalized
1969
+
1970
+
1971
+ def _normalize_service_tier_alias_value(value: JsonValue) -> JsonValue:
1972
+ if not isinstance(value, str):
1973
+ return value
1974
+ if value.strip().lower() == "fast":
1975
+ return "priority"
1976
+ return value
1977
+
1978
+
1979
+ def _normalize_input_text(text: str) -> list[JsonValue]:
1980
+ return [{"role": "user", "content": [{"type": "input_text", "text": text}]}]