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,1054 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from collections.abc import Mapping
5
+ from typing import NamedTuple
6
+
7
+ from pydantic import ValidationError
8
+
9
+ from app.core.errors import OpenAIErrorEnvelope, openai_error
10
+ from app.core.exceptions import ProxyModelNotAllowed, ProxyReasoningEffortNotAllowed
11
+ from app.core.openai.exceptions import ClientPayloadError
12
+ from app.core.openai.model_registry import ModelRegistry, canonical_service_tier_value, get_model_registry
13
+ from app.core.openai.requests import (
14
+ ResponsesCompactRequest,
15
+ ResponsesReasoning,
16
+ ResponsesRequest,
17
+ extract_input_file_ids,
18
+ normalize_reasoning_aliases,
19
+ responses_input_uses_lite_tools,
20
+ )
21
+ from app.core.openai.strict_schema import (
22
+ validate_strict_function_tool_schema,
23
+ validate_strict_json_schema,
24
+ )
25
+ from app.core.openai.v1_requests import V1ResponsesRequest
26
+ from app.core.types import JsonValue
27
+ from app.core.utils.json_guards import is_json_list, is_json_mapping
28
+ from app.core.utils.request_id import get_request_id
29
+ from app.db.models import ModelSource
30
+ from app.modules.api_keys.service import ApiKeyData
31
+ from app.modules.model_sources.catalog import source_model_reasoning_levels
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+ # Reasoning efforts that the upstream ChatGPT/Codex backend silently drops
36
+ # (the WebSocket never produces ``response.completed``). When a client sends
37
+ # one of these we transparently rewrite it to a value the resolved model
38
+ # advertises in its ``supported_reasoning_levels`` so the request does not
39
+ # hang. ``minimal`` is a valid value on the OpenAI Platform Responses API for
40
+ # GPT-5 family models, but the ChatGPT backend codex-lb proxies to does not
41
+ # accept it as of 2026-04. See https://github.com/Soju06/codex-lb/issues/493
42
+ _UNSUPPORTED_UPSTREAM_REASONING_EFFORTS: frozenset[str] = frozenset({"minimal"})
43
+ _DEFAULT_REASONING_EFFORT_FALLBACK = "low"
44
+
45
+ # Client-plane reasoning efforts the reference Codex client never sends on the
46
+ # wire. GPT-5.6 Sol/Terra advertise ``ultra`` in their catalog entries, but the
47
+ # official client rewrites it to ``max`` before building the Responses request
48
+ # (``reasoning_effort_for_request`` in codex-rs ``core/src/client.rs`` at
49
+ # rust-v0.144.1); ``ultra``'s extra effect (proactive multi-agent mode) is
50
+ # purely client-side. Mirror that aliasing for API-key enforcement and raw
51
+ # API callers so the upstream backend only ever sees wire-safe values.
52
+ _REASONING_EFFORT_WIRE_ALIASES: dict[str, str] = {"ultra": "max"}
53
+
54
+ # Cursor exposes GPT-5 family model labels with UI suffixes such as "Extra
55
+ # High Fast". The ChatGPT/Codex upstream accepts the canonical GPT-5-family
56
+ # slug plus request fields, not those synthetic suffixes in the model name.
57
+ # Keep this deliberately narrow: only strip known Cursor-style suffix tokens
58
+ # from known GPT-5 base model slugs, and leave every other model untouched.
59
+ _GPT5_ALIAS_BASE_MODELS: tuple[str, ...] = (
60
+ "gpt-5.6-sol",
61
+ "gpt-5.6-terra",
62
+ "gpt-5.6-luna",
63
+ "gpt-5.4-mini",
64
+ "gpt-5.3-codex",
65
+ "gpt-5.2-codex",
66
+ "gpt-5.1-codex-max",
67
+ "gpt-5.1-codex-mini",
68
+ "gpt-5.1-codex",
69
+ "gpt-5-codex",
70
+ "gpt-5.5",
71
+ "gpt-5.4",
72
+ "gpt-5.3",
73
+ "gpt-5.2",
74
+ "gpt-5.1",
75
+ "gpt-5",
76
+ )
77
+ _MODEL_ALIAS_REASONING_TOKENS: dict[str, str] = {
78
+ "minimal": "minimal",
79
+ "low": "low",
80
+ "medium": "medium",
81
+ "high": "high",
82
+ "xhigh": "high",
83
+ "extra": "high",
84
+ }
85
+ _MODEL_ALIAS_REASONING_RANK: dict[str, int] = {"minimal": 0, "low": 1, "medium": 2, "high": 3}
86
+ _MODEL_ALIAS_SERVICE_TIER_TOKENS: dict[str, str] = {
87
+ "fast": "priority",
88
+ "priority": "priority",
89
+ }
90
+ _MODEL_ALIAS_IGNORED_TOKENS: frozenset[str] = frozenset({"reasoning", "thinking"})
91
+ _MODEL_ALIAS_TOKENS: frozenset[str] = frozenset(
92
+ {
93
+ *tuple(_MODEL_ALIAS_REASONING_TOKENS),
94
+ *tuple(_MODEL_ALIAS_SERVICE_TIER_TOKENS),
95
+ *tuple(_MODEL_ALIAS_IGNORED_TOKENS),
96
+ }
97
+ )
98
+
99
+ # Service tier values codex-lb accepts at the API-key surface but that the
100
+ # ChatGPT/Codex backend rejects with ``Unsupported service_tier: <value>``.
101
+ # Semantically both ``auto`` and ``default`` mean "let upstream pick" -- the
102
+ # same thing as omitting the field entirely -- so when an enforced API-key
103
+ # policy resolves to one of these, we forward the request without a
104
+ # ``service_tier`` instead of sending a literal that fails upstream. See
105
+ # https://github.com/Soju06/codex-lb/issues/546
106
+ _UPSTREAM_OMIT_SERVICE_TIERS: frozenset[str] = frozenset({"auto", "default"})
107
+
108
+
109
+ def resolve_wire_reasoning_effort(effort: str) -> str:
110
+ """Return the wire-safe value for a client-plane reasoning effort.
111
+
112
+ The reference Codex client rewrites client-plane efforts (``ultra`` ->
113
+ ``max``) before building the upstream Responses request; every codex-lb
114
+ code path that builds an upstream payload directly (proxy enforcement,
115
+ automation compact pings) applies the same aliasing so the upstream
116
+ backend never sees a client-plane literal. Unknown values pass through
117
+ unchanged.
118
+ """
119
+ return _REASONING_EFFORT_WIRE_ALIASES.get(effort.strip().lower(), effort)
120
+
121
+
122
+ def validate_model_access(api_key: ApiKeyData | None, model: str | None) -> None:
123
+ if api_key is None:
124
+ return
125
+ if not api_key.allowed_models:
126
+ return
127
+ allowed_models = {resolve_model_alias(allowed_model) for allowed_model in api_key.allowed_models}
128
+ effective_model = resolve_model_alias(model)
129
+ if model is None or effective_model in allowed_models:
130
+ return
131
+ raise ProxyModelNotAllowed(f"This API key does not have access to model '{model}'")
132
+
133
+
134
+ def validate_reasoning_effort_access(api_key: ApiKeyData | None, effort: str | None) -> None:
135
+ if api_key is None:
136
+ return
137
+ allowed_reasoning_efforts = getattr(api_key, "allowed_reasoning_efforts", None)
138
+ if allowed_reasoning_efforts is None or effort is None:
139
+ return
140
+ normalized_effort = effort.strip().lower()
141
+ if normalized_effort in allowed_reasoning_efforts:
142
+ return
143
+ logger.info(
144
+ "api_key_reasoning_effort_not_allowed request_id=%s key_id=%s reasoning_effort=%s",
145
+ get_request_id(),
146
+ api_key.id,
147
+ normalized_effort,
148
+ )
149
+ raise ProxyReasoningEffortNotAllowed(
150
+ f"This API key does not have access to reasoning effort '{normalized_effort}'",
151
+ param="reasoning.effort",
152
+ )
153
+
154
+
155
+ def _client_reasoning_effort(payload: ResponsesRequest | ResponsesCompactRequest) -> str | None:
156
+ """Return the effort selected by the client before wire normalization.
157
+
158
+ Cursor encodes its effort in an accepted model alias, where ``xhigh`` is
159
+ later lowered to the upstream's ``high`` value. API-key policies are an
160
+ operator-facing client-plane control, so they must compare against the
161
+ original selection rather than that wire representation.
162
+ """
163
+ model_effort = _client_reasoning_effort_from_model(payload.model)
164
+ if model_effort is not None:
165
+ return model_effort
166
+
167
+ reasoning = payload.reasoning.model_dump(mode="json", exclude_none=True) if payload.reasoning is not None else None
168
+ if is_json_mapping(reasoning):
169
+ effort = reasoning.get("effort")
170
+ if isinstance(effort, str) and effort.strip():
171
+ return effort.strip().lower()
172
+ return _client_reasoning_effort_from_provider_aliases(payload)
173
+
174
+
175
+ def _client_reasoning_effort_from_provider_aliases(
176
+ payload: ResponsesRequest | ResponsesCompactRequest,
177
+ ) -> str | None:
178
+ reasoning = payload.reasoning.model_dump(mode="json", exclude_none=True) if payload.reasoning is not None else None
179
+ extra = payload.model_extra
180
+ if isinstance(extra, dict):
181
+ alias_payload = dict(extra)
182
+ if reasoning is not None:
183
+ alias_payload["reasoning"] = reasoning
184
+ normalize_reasoning_aliases(alias_payload)
185
+ normalized_reasoning = alias_payload.get("reasoning")
186
+ if is_json_mapping(normalized_reasoning):
187
+ extra_effort = normalized_reasoning.get("effort")
188
+ if isinstance(extra_effort, str) and extra_effort.strip():
189
+ return extra_effort.strip().lower()
190
+
191
+ return None
192
+
193
+
194
+ def _materialize_provider_reasoning_effort(
195
+ payload: ResponsesRequest | ResponsesCompactRequest,
196
+ effort: str | None,
197
+ ) -> None:
198
+ existing_effort = payload.reasoning.effort if payload.reasoning is not None else None
199
+ if effort is None or (isinstance(existing_effort, str) and existing_effort.strip()):
200
+ return
201
+ if payload.reasoning is None:
202
+ payload.reasoning = ResponsesReasoning(effort=effort)
203
+ else:
204
+ payload.reasoning.effort = effort
205
+ if isinstance(payload, ResponsesRequest):
206
+ payload._codex_lb_provider_reasoning_effort_materialized = True
207
+
208
+
209
+ def _client_reasoning_effort_from_model(model: str | None) -> str | None:
210
+ alias = _resolve_model_alias_parts(model)
211
+ if alias is not None:
212
+ normalized_model = model.strip().lower() if isinstance(model, str) else ""
213
+ suffix = normalized_model[len(alias[0]) + 1 :]
214
+ tokens = {token for token in suffix.split("-") if token}
215
+ if "xhigh" in tokens or "extra" in tokens:
216
+ return "xhigh"
217
+ if alias[1] is not None:
218
+ return alias[1]
219
+ return None
220
+
221
+
222
+ def normalize_source_reasoning_aliases(payload: dict[str, JsonValue]) -> None:
223
+ """Align effort-bearing aliases while preserving unrelated source controls."""
224
+ provider_thinking = payload.get("thinking")
225
+ preserve_provider_thinking = False
226
+ if "thinking" in payload:
227
+ probe: dict[str, JsonValue] = {"thinking": provider_thinking}
228
+ normalize_reasoning_aliases(probe)
229
+ normalized_reasoning = probe.get("reasoning")
230
+ normalized_effort = normalized_reasoning.get("effort") if is_json_mapping(normalized_reasoning) else None
231
+ thinking_mapping = provider_thinking if is_json_mapping(provider_thinking) else None
232
+ thinking_type = thinking_mapping.get("type") if thinking_mapping is not None else None
233
+ is_inactive = thinking_mapping is not None and (
234
+ thinking_mapping.get("enabled") is False
235
+ or (isinstance(thinking_type, str) and thinking_type.strip().lower() == "disabled")
236
+ )
237
+ preserve_provider_thinking = (
238
+ thinking_mapping is not None
239
+ and not is_inactive
240
+ and not (isinstance(normalized_effort, str) and bool(normalized_effort.strip()))
241
+ )
242
+ if preserve_provider_thinking:
243
+ payload.pop("thinking", None)
244
+ normalize_reasoning_aliases(payload)
245
+ if preserve_provider_thinking and thinking_mapping is not None:
246
+ preserved_thinking = dict(thinking_mapping.items())
247
+ preserved_effort = preserved_thinking.get("effort")
248
+ if isinstance(preserved_effort, str) and not preserved_effort.strip():
249
+ preserved_thinking.pop("effort")
250
+ payload["thinking"] = preserved_thinking
251
+
252
+
253
+ class ApiKeyEnforcementResult(NamedTuple):
254
+ """What :func:`apply_api_key_enforcement` observed while mutating the payload.
255
+
256
+ ``pre_normalization_reasoning_effort`` carries the effort that
257
+ :func:`normalize_unsupported_reasoning_effort` replaced, so a caller that
258
+ later routes the request to an OpenAI-compatible model source can restore
259
+ it. It is the post-enforcement value: restoring it cannot resurrect an
260
+ effort an API key overrode.
261
+ """
262
+
263
+ service_tier_was_enforced: bool
264
+ pre_normalization_reasoning_effort: str | None
265
+
266
+
267
+ def apply_api_key_enforcement(
268
+ payload: ResponsesRequest | ResponsesCompactRequest,
269
+ api_key: ApiKeyData | None,
270
+ *,
271
+ registry: ModelRegistry | None = None,
272
+ prohibit_fast_mode: bool = False,
273
+ ) -> ApiKeyEnforcementResult:
274
+ """Apply API-key policy and report whether it supplied the service tier.
275
+
276
+ The returned provenance is captured before mutating ``payload``. Callers
277
+ must not infer it later by comparing values: an explicit client request can
278
+ equal the enforced value (including after ``fast`` canonicalizes to
279
+ ``priority``).
280
+ """
281
+ client_reasoning_effort = payload._codex_lb_client_reasoning_effort or _client_reasoning_effort(payload)
282
+ payload._codex_lb_client_reasoning_effort = client_reasoning_effort
283
+ provider_reasoning_effort = _client_reasoning_effort_from_provider_aliases(payload)
284
+ normalize_upstream_model_alias(payload)
285
+
286
+ if api_key is None:
287
+ _materialize_provider_reasoning_effort(payload, provider_reasoning_effort)
288
+ pre_normalization_effort = normalize_unsupported_reasoning_effort(payload, registry=registry)
289
+ apply_prohibit_fast_mode(payload, prohibit_fast_mode=prohibit_fast_mode)
290
+ return ApiKeyEnforcementResult(False, pre_normalization_effort)
291
+
292
+ if api_key.enforced_model:
293
+ enforced_model_reasoning_effort = _client_reasoning_effort_from_model(api_key.enforced_model)
294
+ requested_model = payload.model
295
+ if requested_model != api_key.enforced_model:
296
+ logger.info(
297
+ "api_key_model_enforced request_id=%s key_id=%s requested_model=%s enforced_model=%s",
298
+ get_request_id(),
299
+ api_key.id,
300
+ requested_model,
301
+ api_key.enforced_model,
302
+ )
303
+ payload.model = api_key.enforced_model
304
+ if enforced_model_reasoning_effort is not None:
305
+ client_reasoning_effort = enforced_model_reasoning_effort
306
+ payload._codex_lb_client_reasoning_effort = client_reasoning_effort
307
+ normalize_upstream_model_alias(payload)
308
+ if (
309
+ responses_input_uses_lite_tools(payload.input)
310
+ and _model_responses_lite_capability(
311
+ payload.model,
312
+ registry=registry or get_model_registry(),
313
+ )
314
+ is False
315
+ ):
316
+ raise ProxyModelNotAllowed(
317
+ f"API key enforced model '{payload.model}' does not support Responses Lite",
318
+ code="responses_lite_model_mismatch",
319
+ )
320
+
321
+ if api_key.enforced_reasoning_effort is not None:
322
+ requested_effort = payload.reasoning.effort if payload.reasoning else None
323
+ if payload.reasoning is None:
324
+ payload.reasoning = ResponsesReasoning(effort=api_key.enforced_reasoning_effort)
325
+ else:
326
+ payload.reasoning.effort = api_key.enforced_reasoning_effort
327
+ if requested_effort != api_key.enforced_reasoning_effort:
328
+ logger.info(
329
+ "api_key_reasoning_enforced request_id=%s key_id=%s requested_effort=%s enforced_effort=%s",
330
+ get_request_id(),
331
+ api_key.id,
332
+ requested_effort,
333
+ api_key.enforced_reasoning_effort,
334
+ )
335
+
336
+ _materialize_provider_reasoning_effort(payload, provider_reasoning_effort)
337
+ if client_reasoning_effort is not None:
338
+ validate_reasoning_effort_access(api_key, client_reasoning_effort)
339
+ if (
340
+ payload.reasoning is not None
341
+ and isinstance(payload.reasoning.effort, str)
342
+ and payload.reasoning.effort.strip().lower() == client_reasoning_effort
343
+ ):
344
+ payload.reasoning.effort = client_reasoning_effort
345
+ pre_normalization_effort = normalize_unsupported_reasoning_effort(payload, registry=registry)
346
+
347
+ service_tier_was_enforced = False
348
+ if api_key.enforced_service_tier is not None:
349
+ requested_service_tier = getattr(payload, "service_tier", None)
350
+ service_tier_was_enforced = requested_service_tier is None or (
351
+ requested_service_tier.strip().lower() in _UPSTREAM_OMIT_SERVICE_TIERS
352
+ )
353
+ # ``auto``/``default`` are accepted at the API-key surface but
354
+ # the ChatGPT/Codex backend rejects them as literal values. Map
355
+ # them onto the wire-level absence of ``service_tier`` (which
356
+ # already means "use upstream default") so the enforcement
357
+ # actually reaches upstream instead of failing with
358
+ # ``Unsupported service_tier``. See issue #546.
359
+ if api_key.enforced_service_tier in _UPSTREAM_OMIT_SERVICE_TIERS:
360
+ effective_service_tier: str | None = None
361
+ else:
362
+ effective_service_tier = api_key.enforced_service_tier
363
+ setattr(payload, "service_tier", effective_service_tier)
364
+ if requested_service_tier != api_key.enforced_service_tier:
365
+ logger.info(
366
+ "api_key_service_tier_enforced request_id=%s key_id=%s "
367
+ "requested_service_tier=%s enforced_service_tier=%s "
368
+ "outbound_service_tier=%s",
369
+ get_request_id(),
370
+ api_key.id,
371
+ requested_service_tier,
372
+ api_key.enforced_service_tier,
373
+ effective_service_tier,
374
+ )
375
+ apply_prohibit_fast_mode(payload, prohibit_fast_mode=prohibit_fast_mode)
376
+ return ApiKeyEnforcementResult(service_tier_was_enforced, pre_normalization_effort)
377
+
378
+
379
+ def apply_prohibit_fast_mode(
380
+ payload: ResponsesRequest | ResponsesCompactRequest | dict[str, JsonValue],
381
+ *,
382
+ prohibit_fast_mode: bool,
383
+ request_id: str | None = None,
384
+ ) -> bool:
385
+ """Remove a resolved priority tier when the global policy prohibits it."""
386
+ raw_service_tier = payload.get("service_tier") if isinstance(payload, dict) else payload.service_tier
387
+ service_tier = raw_service_tier if isinstance(raw_service_tier, str) else None
388
+ if not prohibit_fast_mode or service_tier is None or canonical_service_tier_value(service_tier) != "priority":
389
+ return False
390
+
391
+ logger.info(
392
+ "fast_mode_service_tier_prohibited request_id=%s stripped_service_tier=%s",
393
+ request_id or get_request_id(),
394
+ service_tier,
395
+ )
396
+ if isinstance(payload, dict):
397
+ payload.pop("service_tier", None)
398
+ else:
399
+ payload.service_tier = None
400
+ return True
401
+
402
+
403
+ def apply_enforced_service_tier_model_fallback(
404
+ payload: ResponsesRequest | ResponsesCompactRequest,
405
+ *,
406
+ service_tier_was_enforced: bool,
407
+ registry: ModelRegistry | None = None,
408
+ ) -> bool:
409
+ """Drop an enforced tier only when an account model truly lacks it.
410
+
411
+ This runs after model-source selection, and only on subscription-account
412
+ routes. Mutating the request at that boundary makes account selection,
413
+ bridge compatibility, API-key accounting, request logs, and the upstream
414
+ wire payload agree on the same effective tier.
415
+ """
416
+ service_tier = payload.service_tier
417
+ if not service_tier_was_enforced or service_tier is None:
418
+ return False
419
+
420
+ model_registry = registry or get_model_registry()
421
+ advertises_service_tier = getattr(model_registry, "model_advertises_service_tier", None)
422
+ if not callable(advertises_service_tier) or advertises_service_tier(payload.model, service_tier):
423
+ return False
424
+
425
+ logger.info(
426
+ "api_key_enforced_service_tier_model_fallback request_id=%s model=%s enforced_service_tier=%s",
427
+ get_request_id(),
428
+ payload.model,
429
+ service_tier,
430
+ )
431
+ payload.service_tier = None
432
+ return True
433
+
434
+
435
+ def _model_responses_lite_capability(
436
+ model: str,
437
+ *,
438
+ registry: ModelRegistry,
439
+ ) -> bool | None:
440
+ normalized_model = model.strip().lower()
441
+ models = registry.get_models_for_metadata()
442
+ model_entry = models.get(model) or models.get(normalized_model)
443
+ if model_entry is None:
444
+ return None
445
+ capability = model_entry.raw.get("use_responses_lite")
446
+ return capability if isinstance(capability, bool) else None
447
+
448
+
449
+ # Non-standard reasoning toggles some OpenAI-compatible clients attach
450
+ # (OpenRouter/SGLang style). Forwarding them to a source whose model does not
451
+ # support reasoning flips the upstream into reasoning-extraction mode: the
452
+ # answer lands in ``message.reasoning`` instead of ``message.content``.
453
+ _SOURCE_REASONING_TOGGLE_KEYS: tuple[str, ...] = (
454
+ "include_reasoning",
455
+ "separate_reasoning",
456
+ "stream_reasoning",
457
+ "reasoning",
458
+ "reasoning_effort",
459
+ )
460
+
461
+
462
+ def sanitize_source_chat_payload(
463
+ payload: dict[str, JsonValue],
464
+ *,
465
+ allow_reasoning: bool,
466
+ ) -> None:
467
+ """Clean a chat-completions wire payload before forwarding to a source.
468
+
469
+ ``ChatCompletionsRequest`` allows extra fields and defaults ``tools`` to an
470
+ empty list, so an unfiltered ``model_dump`` forwards ``"tools": []`` and any
471
+ client-side reasoning toggles verbatim.
472
+ """
473
+ tools = payload.get("tools")
474
+ if isinstance(tools, list) and not tools:
475
+ payload.pop("tools", None)
476
+ payload.pop("tool_choice", None)
477
+ payload.pop("parallel_tool_calls", None)
478
+ if not allow_reasoning:
479
+ for key in _SOURCE_REASONING_TOGGLE_KEYS:
480
+ payload.pop(key, None)
481
+
482
+
483
+ def apply_api_key_enforcement_to_chat_payload(
484
+ payload: dict[str, JsonValue],
485
+ api_key: ApiKeyData | None,
486
+ *,
487
+ allowed_reasoning_effort: str | None = None,
488
+ materialize_allowed_reasoning_effort: bool = False,
489
+ ) -> None:
490
+ """Mirror :func:`apply_api_key_enforcement` onto a chat-completions wire payload.
491
+
492
+ Source-routed chat requests forward the original chat-shaped payload
493
+ instead of the converted ``ResponsesRequest``, so enforced fields must be
494
+ applied to the outbound dict as well or the upstream receives the
495
+ caller's values while accounting uses the enforced ones.
496
+ """
497
+ if allowed_reasoning_effort is not None:
498
+ wire_effort = resolve_wire_reasoning_effort(allowed_reasoning_effort)
499
+ # Chat requests can express the same setting through several provider
500
+ # aliases. Once the Responses conversion has authorized one effective
501
+ # choice, make every caller-supplied alias agree without adding fields
502
+ # that the selected source may not accept.
503
+ if "reasoning_effort" in payload:
504
+ payload["reasoning_effort"] = wire_effort
505
+ if "reasoningEffort" in payload:
506
+ payload["reasoningEffort"] = wire_effort
507
+ if "thinking" in payload:
508
+ thinking = payload["thinking"]
509
+ if isinstance(thinking, dict):
510
+ thinking_effort = thinking.get("effort")
511
+ if isinstance(thinking_effort, str) and not thinking_effort.strip():
512
+ thinking = {**thinking}
513
+ thinking.pop("effort")
514
+ payload["thinking"] = thinking
515
+ if isinstance(thinking_effort, str) and thinking_effort.strip():
516
+ aligned_thinking = {**thinking, "effort": wire_effort}
517
+ thinking_type = aligned_thinking.get("type")
518
+ if isinstance(thinking_type, str) and thinking_type.strip().lower() == "disabled":
519
+ aligned_thinking.pop("type")
520
+ if aligned_thinking.get("enabled") is False:
521
+ aligned_thinking.pop("enabled")
522
+ payload["thinking"] = aligned_thinking
523
+ else:
524
+ thinking_type = thinking.get("type")
525
+ is_inactive = thinking.get("enabled") is False or (
526
+ isinstance(thinking_type, str) and thinking_type.strip().lower() == "disabled"
527
+ )
528
+ selects_implicit_medium = thinking.get("enabled") is True or (
529
+ isinstance(thinking_type, str) and thinking_type.strip().lower() == "enabled"
530
+ )
531
+ if is_inactive or (selects_implicit_medium and wire_effort != "medium"):
532
+ payload.pop("thinking")
533
+ else:
534
+ payload["thinking"] = wire_effort
535
+ if "enable_thinking" in payload:
536
+ if wire_effort == "medium":
537
+ payload["enable_thinking"] = True
538
+ else:
539
+ payload.pop("enable_thinking", None)
540
+ reasoning = payload.get("reasoning")
541
+ if isinstance(reasoning, dict):
542
+ payload["reasoning"] = {**reasoning, "effort": wire_effort}
543
+ if materialize_allowed_reasoning_effort and not any(
544
+ key in payload
545
+ for key in ("reasoning_effort", "reasoningEffort", "thinking", "enable_thinking", "reasoning")
546
+ ):
547
+ payload["reasoning_effort"] = wire_effort
548
+
549
+ if api_key is None:
550
+ return
551
+
552
+ if api_key.enforced_reasoning_effort is not None:
553
+ enforced_effort = resolve_wire_reasoning_effort(api_key.enforced_reasoning_effort)
554
+ payload["reasoning_effort"] = enforced_effort
555
+ reasoning = payload.get("reasoning")
556
+ if isinstance(reasoning, dict):
557
+ payload["reasoning"] = {**reasoning, "effort": enforced_effort}
558
+ else:
559
+ payload["reasoning"] = {"effort": enforced_effort}
560
+
561
+ if api_key.enforced_service_tier is not None:
562
+ if api_key.enforced_service_tier in _UPSTREAM_OMIT_SERVICE_TIERS:
563
+ payload.pop("service_tier", None)
564
+ else:
565
+ payload["service_tier"] = api_key.enforced_service_tier
566
+
567
+
568
+ def resolve_model_alias(model: str | None) -> str | None:
569
+ alias = _resolve_model_alias_parts(model)
570
+ if alias is None:
571
+ return model
572
+ return alias[0]
573
+
574
+
575
+ def model_alias_requests_fast_mode(model: str | None) -> bool:
576
+ alias = _resolve_model_alias_parts(model)
577
+ return alias is not None and alias[3]
578
+
579
+
580
+ def normalize_upstream_model_alias(payload: ResponsesRequest | ResponsesCompactRequest) -> None:
581
+ requested_model = payload.model
582
+ alias = _resolve_model_alias_parts(requested_model)
583
+ if alias is None:
584
+ return
585
+
586
+ canonical_model, alias_effort, alias_service_tier, _alias_requests_fast_mode = alias
587
+ if payload.model != canonical_model:
588
+ logger.info(
589
+ "model_alias_normalized request_id=%s requested_model=%s normalized_model=%s",
590
+ get_request_id(),
591
+ payload.model,
592
+ canonical_model,
593
+ )
594
+ payload.model = canonical_model
595
+
596
+ if alias_effort is not None:
597
+ requested_effort = payload.reasoning.effort if payload.reasoning else None
598
+ if payload.reasoning is None:
599
+ payload.reasoning = ResponsesReasoning(effort=alias_effort)
600
+ else:
601
+ payload.reasoning.effort = alias_effort
602
+ if requested_effort != alias_effort:
603
+ logger.info(
604
+ "model_alias_reasoning_normalized request_id=%s requested_model=%s "
605
+ "normalized_model=%s requested_effort=%s normalized_effort=%s",
606
+ get_request_id(),
607
+ requested_model,
608
+ canonical_model,
609
+ requested_effort,
610
+ alias_effort,
611
+ )
612
+
613
+ if alias_service_tier is not None and getattr(payload, "service_tier", None) is None:
614
+ setattr(payload, "service_tier", alias_service_tier)
615
+ logger.info(
616
+ "model_alias_service_tier_normalized request_id=%s requested_model=%s "
617
+ "normalized_model=%s normalized_service_tier=%s",
618
+ get_request_id(),
619
+ requested_model,
620
+ canonical_model,
621
+ alias_service_tier,
622
+ )
623
+
624
+
625
+ def _resolve_model_alias_parts(model: str | None) -> tuple[str, str | None, str | None, bool] | None:
626
+ if not isinstance(model, str):
627
+ return None
628
+ normalized = model.strip().lower()
629
+ if not normalized:
630
+ return None
631
+
632
+ for base_model in _GPT5_ALIAS_BASE_MODELS:
633
+ prefix = f"{base_model}-"
634
+ if not normalized.startswith(prefix):
635
+ continue
636
+ suffix = normalized[len(prefix) :]
637
+ tokens = [token for token in suffix.split("-") if token]
638
+ if not tokens or any(token not in _MODEL_ALIAS_TOKENS for token in tokens):
639
+ return None
640
+ return (
641
+ base_model,
642
+ _resolve_alias_reasoning_effort(tokens),
643
+ _resolve_alias_service_tier(tokens),
644
+ "fast" in tokens,
645
+ )
646
+
647
+ return None
648
+
649
+
650
+ def _resolve_alias_reasoning_effort(tokens: list[str]) -> str | None:
651
+ selected: str | None = None
652
+ selected_rank = -1
653
+ for token in tokens:
654
+ effort = _MODEL_ALIAS_REASONING_TOKENS.get(token)
655
+ if effort is None:
656
+ continue
657
+ rank = _MODEL_ALIAS_REASONING_RANK[effort]
658
+ if rank > selected_rank:
659
+ selected = effort
660
+ selected_rank = rank
661
+ return selected
662
+
663
+
664
+ def _resolve_alias_service_tier(tokens: list[str]) -> str | None:
665
+ for token in tokens:
666
+ service_tier = _MODEL_ALIAS_SERVICE_TIER_TOKENS.get(token)
667
+ if service_tier is not None:
668
+ return service_tier
669
+ return None
670
+
671
+
672
+ def normalize_unsupported_reasoning_effort(
673
+ payload: ResponsesRequest | ResponsesCompactRequest,
674
+ *,
675
+ registry: ModelRegistry | None = None,
676
+ ) -> str | None:
677
+ """Rewrite ``reasoning.effort`` values the upstream backend rejects.
678
+
679
+ Some efforts that codex-lb accepts at the API surface (notably
680
+ ``"minimal"``) are silently dropped by the ChatGPT/Codex WebSocket
681
+ backend, which causes the response stream to hang with no completion.
682
+ For those values we map to a value the resolved model actually supports
683
+ so clients (e.g. Codex CLI's ``--reasoning-effort minimal``) keep
684
+ working. Mapping picks the model's lowest advertised effort, falling
685
+ back to ``low`` when the registry has no metadata yet.
686
+
687
+ Client-plane efforts the reference Codex client aliases before sending
688
+ (``ultra`` -> ``max``) are rewritten the same way here.
689
+
690
+ Returns the effort that the unsupported-effort fallback replaced, in
691
+ normalized (trimmed, lowercased) form, or ``None`` when nothing restorable
692
+ was rewritten. Model sources do not have the backend quirk that fallback
693
+ works around, but whether a request is served by one is only known after
694
+ source selection, which happens later; callers that can reach a source
695
+ carry this value forward and restore it there (see
696
+ ``restore_source_reasoning_effort``).
697
+
698
+ The ``ultra`` -> ``max`` wire alias is never reported. That aliasing mirrors
699
+ the reference client and is required on every upstream surface, so it must
700
+ survive source routing too.
701
+
702
+ The reported value is the post-enforcement effort rather than the client's
703
+ original, so restoring it cannot resurrect an effort an API key overrode.
704
+ """
705
+
706
+ if payload.reasoning is None or payload.reasoning.effort is None:
707
+ return None
708
+
709
+ requested_effort = payload.reasoning.effort
710
+ normalized_effort = requested_effort.strip().lower()
711
+
712
+ wire_alias = _REASONING_EFFORT_WIRE_ALIASES.get(normalized_effort)
713
+ if wire_alias is not None:
714
+ payload.reasoning.effort = wire_alias
715
+ logger.info(
716
+ "reasoning_effort_wire_aliased request_id=%s model=%s requested_effort=%s aliased_effort=%s",
717
+ get_request_id(),
718
+ payload.model,
719
+ requested_effort,
720
+ wire_alias,
721
+ )
722
+ # Deliberately not reported as restorable: the ultra -> max alias must
723
+ # hold on every surface, source-routed payloads included.
724
+ return None
725
+
726
+ if normalized_effort not in _UNSUPPORTED_UPSTREAM_REASONING_EFFORTS:
727
+ return None
728
+
729
+ fallback = _resolve_reasoning_effort_fallback(
730
+ payload.model,
731
+ registry=registry or get_model_registry(),
732
+ )
733
+ payload.reasoning.effort = fallback
734
+ logger.info(
735
+ "reasoning_effort_normalized request_id=%s model=%s requested_effort=%s normalized_effort=%s",
736
+ get_request_id(),
737
+ payload.model,
738
+ requested_effort,
739
+ fallback,
740
+ )
741
+ return normalized_effort
742
+
743
+
744
+ def restore_source_reasoning_effort(
745
+ payload: ResponsesRequest | ResponsesCompactRequest,
746
+ source: ModelSource,
747
+ *,
748
+ pre_normalization_effort: str | None,
749
+ ) -> None:
750
+ """Undo :func:`normalize_unsupported_reasoning_effort` for a source-routed request.
751
+
752
+ The rewrite exists solely to work around a ChatGPT/Codex backend quirk, so
753
+ it must not reach an OpenAI-compatible model source. This runs at the point
754
+ where the source has actually been selected, which is the only place the
755
+ routing outcome is known -- inferring it earlier from registry membership
756
+ misfires in both directions (a subscription model missing from a populated
757
+ snapshot, and a source model whose slug shadows a subscription one).
758
+
759
+ The restore is gated on the operator having declared the effort for this
760
+ model: sources without reasoning metadata keep the pre-existing behaviour,
761
+ and an effort the backend never advertised is not sent to it.
762
+ """
763
+ if pre_normalization_effort is None or payload.reasoning is None:
764
+ return
765
+ if payload.model is None:
766
+ return
767
+ restored_effort = pre_normalization_effort.strip().lower()
768
+ declared = {level.effort for level in source_model_reasoning_levels(source, payload.model)}
769
+ if restored_effort not in declared:
770
+ return
771
+ current_effort = payload.reasoning.effort
772
+ # Normalized on assignment rather than trusting the caller: the sole
773
+ # producer already reports the normalized form, but that invariant is
774
+ # non-local and a casing variant must never reach the wire.
775
+ payload.reasoning.effort = restored_effort
776
+ logger.info(
777
+ "reasoning_effort_restored_for_source request_id=%s model=%s source_id=%s "
778
+ "normalized_effort=%s restored_effort=%s",
779
+ get_request_id(),
780
+ payload.model,
781
+ source.id,
782
+ current_effort,
783
+ restored_effort,
784
+ )
785
+
786
+
787
+ def _resolve_reasoning_effort_fallback(
788
+ model: str | None,
789
+ *,
790
+ registry: ModelRegistry,
791
+ ) -> str:
792
+ if not model:
793
+ return _DEFAULT_REASONING_EFFORT_FALLBACK
794
+ snapshot = registry.get_snapshot()
795
+ if snapshot is None:
796
+ return _DEFAULT_REASONING_EFFORT_FALLBACK
797
+ upstream = snapshot.models.get(model) or snapshot.models.get(model.strip().lower())
798
+ if upstream is None:
799
+ return _DEFAULT_REASONING_EFFORT_FALLBACK
800
+ advertised = [level.effort for level in upstream.supported_reasoning_levels if level.effort]
801
+ # Prefer the order the model registry advertises (already lowest -> highest
802
+ # for the GPT-5 family), but always pick the first advertised effort that
803
+ # is not itself an unsupported value.
804
+ for effort in advertised:
805
+ if effort.strip().lower() not in _UNSUPPORTED_UPSTREAM_REASONING_EFFORTS:
806
+ return effort
807
+ return _DEFAULT_REASONING_EFFORT_FALLBACK
808
+
809
+
810
+ def openai_validation_error(exc: ValidationError) -> OpenAIErrorEnvelope:
811
+ error = openai_invalid_payload_error()
812
+ if exc.errors():
813
+ first = exc.errors()[0]
814
+ loc = first.get("loc", [])
815
+ if isinstance(loc, (list, tuple)):
816
+ param = ".".join(str(part) for part in loc if part != "body")
817
+ if param:
818
+ error["error"]["param"] = param
819
+ return error
820
+
821
+
822
+ def openai_invalid_payload_error(param: str | None = None) -> OpenAIErrorEnvelope:
823
+ error = openai_error("invalid_request_error", "Invalid request payload", error_type="invalid_request_error")
824
+ if param:
825
+ error["error"]["param"] = param
826
+ return error
827
+
828
+
829
+ def openai_client_payload_error(exc: ClientPayloadError) -> OpenAIErrorEnvelope:
830
+ """Render a ``ClientPayloadError`` as an OpenAI error envelope.
831
+
832
+ Falls back to ``openai_invalid_payload_error`` for legacy callsites
833
+ that raise ``ClientPayloadError`` without ``code`` / ``error_type``.
834
+ """
835
+ if exc.code is None and exc.error_type is None:
836
+ return openai_invalid_payload_error(exc.param)
837
+ code = exc.code or "invalid_request_error"
838
+ error_type = exc.error_type or "invalid_request_error"
839
+ error = openai_error(code, str(exc), error_type=error_type)
840
+ if exc.param:
841
+ error["error"]["param"] = exc.param
842
+ return error
843
+
844
+
845
+ def normalize_responses_request_payload(
846
+ payload: dict[str, JsonValue],
847
+ *,
848
+ openai_compat: bool,
849
+ ) -> ResponsesRequest:
850
+ if openai_compat:
851
+ responses = V1ResponsesRequest.model_validate(payload).to_responses_request()
852
+ else:
853
+ responses = ResponsesRequest.model_validate(payload)
854
+ enforce_strict_text_format(responses)
855
+ enforce_strict_function_tools_format(responses.tools)
856
+ return responses
857
+
858
+
859
+ def validate_top_level_compaction_trigger_input_shape(payload: Mapping[str, JsonValue]) -> None:
860
+ input_value = payload.get("input")
861
+ if not is_json_list(input_value):
862
+ return
863
+ _validate_terminal_compaction_trigger_input_items(input_value)
864
+
865
+
866
+ def strip_terminal_compaction_trigger_input(
867
+ payload: ResponsesRequest | ResponsesCompactRequest,
868
+ *,
869
+ strip_trigger: bool = True,
870
+ ) -> list[JsonValue] | None:
871
+ input_value = payload.input
872
+ if not is_json_list(input_value):
873
+ return None
874
+ return _strip_terminal_compaction_trigger_input_items(input_value, strip_trigger=strip_trigger)
875
+
876
+
877
+ def _strip_terminal_compaction_trigger_input_items(
878
+ input_value: list[JsonValue],
879
+ *,
880
+ strip_trigger: bool,
881
+ ) -> list[JsonValue] | None:
882
+ trigger_seen = _validate_terminal_compaction_trigger_input_items(input_value)
883
+ if not trigger_seen:
884
+ return None
885
+ if not strip_trigger:
886
+ return input_value
887
+ return [item for item in input_value if not (is_json_mapping(item) and item.get("type") == "compaction_trigger")]
888
+
889
+
890
+ def _validate_terminal_compaction_trigger_input_items(input_value: list[JsonValue]) -> bool:
891
+ trigger_seen = False
892
+ last_index = len(input_value) - 1
893
+
894
+ for index, item in enumerate(input_value):
895
+ if not (is_json_mapping(item) and item.get("type") == "compaction_trigger"):
896
+ continue
897
+
898
+ if trigger_seen or index != last_index:
899
+ raise ClientPayloadError(
900
+ "compaction_trigger must appear exactly once as the final top-level input item",
901
+ param="input",
902
+ code="invalid_request_error",
903
+ error_type="invalid_request_error",
904
+ )
905
+ trigger_seen = True
906
+
907
+ return trigger_seen
908
+
909
+
910
+ def responses_source_route_excluded(
911
+ payload: ResponsesRequest,
912
+ *,
913
+ exclude_compaction: bool = True,
914
+ ) -> bool:
915
+ """True when a Responses request must stay on subscription accounts.
916
+
917
+ A terminal compaction trigger is served by the upstream compact flow on
918
+ the turn's owner account (Codex path only — callers set
919
+ ``exclude_compaction=False`` on ``/v1/responses``, which has no Codex
920
+ compaction path). An ``input_file``/``input_image`` file reference is
921
+ pinned to the subscription account that received the upload. Previous
922
+ response ownership is resolved separately from recorded continuity
923
+ evidence because response identifier syntax is provider-opaque.
924
+
925
+ Raises ``ClientPayloadError`` for a malformed compaction trigger, exactly
926
+ like ``strip_terminal_compaction_trigger_input``.
927
+ """
928
+ if exclude_compaction and strip_terminal_compaction_trigger_input(payload) is not None:
929
+ return True
930
+ return bool(extract_input_file_ids(payload.input))
931
+
932
+
933
+ def enforce_strict_text_format(request: ResponsesRequest) -> None:
934
+ """Reject strict-mode JSON schemas that violate OpenAI structured-outputs rules.
935
+
936
+ The Codex backend mirrors OpenAI's strict-mode policy and closes the
937
+ websocket with ``close_code=1000`` (delivering the original
938
+ ``invalid_json_schema`` payload via ``response.failed``). The local
939
+ pre-check raises a deterministic 400 before any upstream connection
940
+ is opened, keeping ``/v1/responses`` and the chat-conversion path
941
+ consistent and avoiding pointless retry/reconnect cycles for
942
+ permanently invalid schemas.
943
+ """
944
+ if request.text is None or request.text.format is None:
945
+ return
946
+ text_format = request.text.format
947
+ if text_format.type != "json_schema" or text_format.strict is not True:
948
+ return
949
+ if text_format.schema_ is None:
950
+ return
951
+ violation = validate_strict_json_schema(
952
+ text_format.schema_,
953
+ name=text_format.name,
954
+ param="text.format.schema",
955
+ )
956
+ if violation is None:
957
+ return
958
+ raise ClientPayloadError(
959
+ violation.message,
960
+ param=violation.param,
961
+ code=violation.code,
962
+ error_type="invalid_request_error",
963
+ )
964
+
965
+
966
+ def enforce_strict_function_tools_format(
967
+ tools: list[JsonValue] | None,
968
+ *,
969
+ param_template: str = "tools[{index}].parameters",
970
+ nested: bool = False,
971
+ ) -> None:
972
+ """Reject strict-mode function tools whose parameter schemas violate OpenAI rules.
973
+
974
+ Mirrors :func:`enforce_strict_text_format` for function tools that
975
+ set ``strict: true``. The Codex backend rejects an invalid strict
976
+ tool schema by closing the WebSocket with ``close_code=1000``; the
977
+ surfaced error is a generic ``upstream_rejected_input`` 502, which
978
+ well-behaved retry loops misclassify as transient. Real OpenAI
979
+ returns a deterministic ``400 invalid_function_parameters`` for the
980
+ same payload, so codex-lb pre-validates here.
981
+
982
+ ``param_template`` controls how the rejected parameter is named in
983
+ the error envelope: native ``/v1/responses`` callers see
984
+ ``tools[<i>].parameters``; chat-completions callers pass
985
+ ``"tools[{index}].function.parameters"`` to mirror the inbound
986
+ shape.
987
+
988
+ ``nested`` selects the shape of an inbound function tool:
989
+
990
+ * ``False`` (default) — flat ``{"type": "function", "name": ...,
991
+ "parameters": ..., "strict": ...}``. This is the
992
+ ``/v1/responses`` request shape (and the shape produced by
993
+ ``_normalize_chat_tools``).
994
+ * ``True`` — chat-completions shape with the function payload
995
+ wrapped under ``"function"``:
996
+ ``{"type": "function", "function": {"name": ..., "parameters":
997
+ ..., "strict": ...}}``. The chat handler MUST pass ``True`` and
998
+ hand in the *original* request payload's ``tools`` list (not the
999
+ list returned by ``ChatCompletionsRequest.to_responses_request``,
1000
+ which may drop or reorder entries) so the ``{index}`` slot in
1001
+ ``param_template`` lines up with the inbound payload.
1002
+
1003
+ A tool is treated as a function tool whenever ``tool["function"]``
1004
+ is a dict — the same rule ``_normalize_chat_tools`` uses, where
1005
+ missing or other ``type`` values get coerced to ``"function"``
1006
+ (``"type": tool_type or "function"``). Anchoring on the wrapper
1007
+ key keeps pre-validation in lockstep with normalization; otherwise
1008
+ a payload like ``{"function": {"strict": true, "parameters":
1009
+ <invalid>}}`` (no top-level ``type``) bypasses the local 400 and
1010
+ surfaces as a misleading upstream 5xx.
1011
+ """
1012
+ if not tools:
1013
+ return
1014
+ for index, tool in enumerate(tools):
1015
+ if not isinstance(tool, dict):
1016
+ continue
1017
+ if nested:
1018
+ # Chat path: detect function tools the same way
1019
+ # ``_normalize_chat_tools`` does — presence of a ``"function"``
1020
+ # dict is the signal, not a strict ``"type" == "function"``
1021
+ # check. The normalizer coerces missing or other ``type``
1022
+ # values to ``"function"`` whenever ``tool["function"]`` is a
1023
+ # dict (`"type": tool_type or "function"`), so pre-validation
1024
+ # must mirror that or strict violations on type-omitted
1025
+ # nested tools bypass the local 400 and surface as upstream
1026
+ # 5xx instead.
1027
+ function_value = tool.get("function")
1028
+ if not isinstance(function_value, dict):
1029
+ continue
1030
+ descriptor = function_value
1031
+ else:
1032
+ if tool.get("type") != "function":
1033
+ continue
1034
+ descriptor = tool
1035
+ if descriptor.get("strict") is not True:
1036
+ continue
1037
+ parameters = descriptor.get("parameters")
1038
+ if parameters is None:
1039
+ continue
1040
+ raw_name = descriptor.get("name")
1041
+ name = raw_name if isinstance(raw_name, str) else None
1042
+ violation = validate_strict_function_tool_schema(
1043
+ parameters,
1044
+ name=name,
1045
+ param=param_template.format(index=index),
1046
+ )
1047
+ if violation is None:
1048
+ continue
1049
+ raise ClientPayloadError(
1050
+ violation.message,
1051
+ param=violation.param,
1052
+ code=violation.code,
1053
+ error_type="invalid_request_error",
1054
+ )