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
+ """OpenAI Images API translation layer.
2
+
3
+ This module turns ``POST /v1/images/generations`` and ``POST /v1/images/edits``
4
+ requests into internal ``/v1/responses`` requests with the built-in
5
+ ``image_generation`` tool, then folds the upstream Responses output (or SSE
6
+ event stream) back into the OpenAI Images response shape.
7
+
8
+ The intent is to keep all auth/account/sticky/usage logic in
9
+ ``ProxyService.stream_responses`` (and friends) and only do data-shape
10
+ translation here.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import base64
16
+ import binascii
17
+ import logging
18
+ import re
19
+ import time
20
+ from collections.abc import AsyncIterator, Mapping
21
+ from typing import Final, cast
22
+
23
+ from app.core.errors import OpenAIErrorEnvelope, openai_error
24
+ from app.core.openai.exceptions import ClientPayloadError
25
+ from app.core.openai.images import (
26
+ DEFAULT_PUBLIC_IMAGE_MODEL,
27
+ V1ImageData,
28
+ V1ImageResponse,
29
+ V1ImagesEditsForm,
30
+ V1ImagesGenerationsRequest,
31
+ V1ImageUsage,
32
+ is_supported_image_model,
33
+ validate_image_request_parameters,
34
+ )
35
+ from app.core.openai.requests import ResponsesRequest
36
+ from app.core.types import JsonValue
37
+ from app.core.utils.json_guards import is_json_mapping
38
+ from app.core.utils.sse import format_sse_event, parse_sse_data_json
39
+
40
+ logger = logging.getLogger(__name__)
41
+
42
+ #: Compact instruction used to deterministically force exactly one
43
+ #: ``image_generation`` tool call from the host Responses model. The string
44
+ #: is intentionally short and self-contained to keep history-cost minimal.
45
+ _IMAGE_GENERATION_INSTRUCTIONS: Final[str] = (
46
+ "You are an image generator. When asked, you MUST call the image_generation "
47
+ "tool exactly once and return only that tool call. Do not produce any "
48
+ "additional text output. Mirror the user's request verbatim into the tool's "
49
+ "prompt argument."
50
+ )
51
+
52
+ #: Instruction tail appended to edit prompts so the host model knows that any
53
+ #: trailing input_image acts as a mask (since OpenAI's Images Edits API has a
54
+ #: distinct ``mask`` slot but the Responses image_generation tool does not).
55
+ _IMAGE_EDIT_MASK_HINT: Final[str] = (
56
+ "\n\n(The final attached image is a transparent mask: only modify the regions where the mask is non-transparent.)"
57
+ )
58
+
59
+ #: SSE event types we *consume* from the upstream Responses stream.
60
+ _UPSTREAM_PARTIAL_IMAGE_EVENT: Final[str] = "response.image_generation_call.partial_image"
61
+ _UPSTREAM_OUTPUT_ITEM_DONE_EVENT: Final[str] = "response.output_item.done"
62
+ _UPSTREAM_RESPONSE_COMPLETED_EVENT: Final[str] = "response.completed"
63
+ _UPSTREAM_RESPONSE_FAILED_EVENT: Final[str] = "response.failed"
64
+ _UPSTREAM_RESPONSE_INCOMPLETE_EVENT: Final[str] = "response.incomplete"
65
+ _UPSTREAM_ERROR_EVENT: Final[str] = "error"
66
+
67
+ #: OpenAI Images SSE event names we *emit* to the client.
68
+ # Downstream OpenAI Images SSE event names. ``/v1/images/generations``
69
+ # emits ``image_generation.*`` and ``/v1/images/edits`` emits
70
+ # ``image_edit.*`` to match the canonical OpenAI Images streaming
71
+ # vocabulary; the route handler picks the prefix when invoking the
72
+ # translator.
73
+ _GENERATION_PARTIAL_EVENT: Final[str] = "image_generation.partial_image"
74
+ _GENERATION_COMPLETED_EVENT: Final[str] = "image_generation.completed"
75
+ _EDIT_PARTIAL_EVENT: Final[str] = "image_edit.partial_image"
76
+ _EDIT_COMPLETED_EVENT: Final[str] = "image_edit.completed"
77
+ _DOWNSTREAM_ERROR_EVENT: Final[str] = "error"
78
+
79
+ _DATA_URL_PATTERN: Final[re.Pattern[str]] = re.compile(
80
+ r"^data:(?P<mime>[^;]+);base64,(?P<b64>.+)$",
81
+ re.DOTALL,
82
+ )
83
+
84
+
85
+ # ---------------------------------------------------------------------------
86
+ # Request translation
87
+ # ---------------------------------------------------------------------------
88
+
89
+
90
+ def _build_image_generation_tool(
91
+ *,
92
+ model: str,
93
+ n: int,
94
+ size: str,
95
+ quality: str,
96
+ background: str,
97
+ output_format: str,
98
+ output_compression: int,
99
+ moderation: str,
100
+ partial_images: int | None,
101
+ input_fidelity: str | None,
102
+ streaming: bool,
103
+ is_edit: bool = False,
104
+ ) -> dict[str, JsonValue]:
105
+ # NOTE: the upstream ``image_generation`` tool config does not accept
106
+ # ``n``. ``validate_image_request_parameters`` unconditionally
107
+ # rejects ``n > 1`` because client-side fan-out is not implemented
108
+ # yet, so this function is only ever called with ``n == 1``. The
109
+ # assert below catches a future regression where the API-boundary
110
+ # cap is loosened without also adding fan-out, instead of silently
111
+ # dropping the requested count.
112
+ assert n == 1, "image_generation tool does not accept n; fan-out is not implemented"
113
+ del n # rejected upstream of this call (fan-out not yet implemented)
114
+ tool: dict[str, JsonValue] = {
115
+ "type": "image_generation",
116
+ "model": model,
117
+ "size": size,
118
+ "quality": quality,
119
+ "background": background,
120
+ "output_format": output_format,
121
+ "output_compression": output_compression,
122
+ "moderation": moderation,
123
+ }
124
+ if is_edit:
125
+ # Force the edit code path so the host model treats the attached
126
+ # input_image(s) as a source/mask pair instead of inspiration for
127
+ # a fresh generation. Without this the default "auto" action lets
128
+ # the model decide between generation and editing, which can
129
+ # silently break the edits contract.
130
+ tool["action"] = "edit"
131
+ if input_fidelity is not None:
132
+ tool["input_fidelity"] = input_fidelity
133
+ if streaming and partial_images is not None and partial_images > 0:
134
+ tool["partial_images"] = partial_images
135
+ return tool
136
+
137
+
138
+ def _build_user_message_input(
139
+ prompt: str, *, attached_images: list[dict[str, JsonValue]] | None = None
140
+ ) -> list[JsonValue]:
141
+ content: list[JsonValue] = [{"type": "input_text", "text": prompt}]
142
+ if attached_images:
143
+ content.extend(attached_images)
144
+ return [
145
+ {
146
+ "type": "message",
147
+ "role": "user",
148
+ "content": content,
149
+ }
150
+ ]
151
+
152
+
153
+ def _build_input_image_part(image_bytes: bytes, *, mime_type: str | None) -> dict[str, JsonValue]:
154
+ """Build a Responses ``input_image`` content part as a base64 data URL."""
155
+ resolved_mime = (mime_type or "image/png").strip() or "image/png"
156
+ encoded = base64.b64encode(image_bytes).decode("ascii")
157
+ return {
158
+ "type": "input_image",
159
+ "image_url": f"data:{resolved_mime};base64,{encoded}",
160
+ }
161
+
162
+
163
+ def images_generation_to_responses_request(
164
+ payload: V1ImagesGenerationsRequest,
165
+ *,
166
+ host_model: str,
167
+ ) -> ResponsesRequest:
168
+ """Translate a ``/v1/images/generations`` request into a Responses request.
169
+
170
+ The upstream Responses backend rejects non-streaming requests that include
171
+ the ``image_generation`` tool (the partial-image and final ``result``
172
+ payloads are only delivered through SSE). We therefore always force
173
+ ``stream=True`` on the internal request and let the caller drain the
174
+ upstream stream into a JSON envelope when the public client did not
175
+ request streaming.
176
+ """
177
+ streaming = bool(payload.stream)
178
+ # ``validate_generations_payload`` resolves ``payload.model`` to a
179
+ # concrete ``gpt-image-*`` value before this is ever called.
180
+ assert payload.model is not None, "payload.model must be resolved before translation"
181
+ tool = _build_image_generation_tool(
182
+ model=payload.model,
183
+ n=payload.n,
184
+ size=payload.size,
185
+ quality=payload.quality,
186
+ background=payload.background,
187
+ output_format=payload.output_format,
188
+ output_compression=payload.output_compression,
189
+ moderation=payload.moderation,
190
+ partial_images=payload.partial_images,
191
+ input_fidelity=None,
192
+ streaming=streaming,
193
+ )
194
+ return ResponsesRequest.model_validate(
195
+ {
196
+ "model": host_model,
197
+ "instructions": _IMAGE_GENERATION_INSTRUCTIONS,
198
+ "input": _build_user_message_input(payload.prompt),
199
+ "tools": [tool],
200
+ # Force the host model to invoke the image_generation tool
201
+ # so it cannot return a refusal or plain text. Without this
202
+ # the auto choice would surface as a 5xx through this
203
+ # adapter even though the request shape was valid.
204
+ "tool_choice": {"type": "image_generation"},
205
+ "stream": True,
206
+ "store": False,
207
+ }
208
+ )
209
+
210
+
211
+ def images_edit_to_responses_request(
212
+ payload: V1ImagesEditsForm,
213
+ *,
214
+ host_model: str,
215
+ images: list[tuple[bytes, str | None]],
216
+ mask: tuple[bytes, str | None] | None,
217
+ ) -> ResponsesRequest:
218
+ """Translate a ``/v1/images/edits`` request into a Responses request.
219
+
220
+ ``images`` is a non-empty list of ``(bytes, content_type)`` tuples
221
+ representing the multipart ``image`` parts. ``mask`` is the optional
222
+ ``mask`` part with the same shape; when provided, it is appended after
223
+ the source images and the prompt is amended with a deterministic hint
224
+ so the host model treats it correctly.
225
+ """
226
+ if not images:
227
+ # Caller is expected to validate this beforehand, but guard so we
228
+ # never silently produce an image-less Responses request.
229
+ raise ValueError("/v1/images/edits requires at least one image part")
230
+
231
+ streaming = bool(payload.stream)
232
+ attached: list[dict[str, JsonValue]] = []
233
+ for image_bytes, mime_type in images:
234
+ attached.append(_build_input_image_part(image_bytes, mime_type=mime_type))
235
+ if mask is not None:
236
+ mask_bytes, mask_mime = mask
237
+ attached.append(_build_input_image_part(mask_bytes, mime_type=mask_mime))
238
+
239
+ prompt_text = payload.prompt
240
+ if mask is not None:
241
+ prompt_text = f"{prompt_text}{_IMAGE_EDIT_MASK_HINT}"
242
+
243
+ # ``validate_edits_payload`` resolves ``payload.model`` to a concrete
244
+ # ``gpt-image-*`` value before this is ever called.
245
+ assert payload.model is not None, "payload.model must be resolved before translation"
246
+ tool = _build_image_generation_tool(
247
+ model=payload.model,
248
+ n=payload.n,
249
+ size=payload.size,
250
+ quality=payload.quality,
251
+ background=payload.background,
252
+ output_format=payload.output_format,
253
+ output_compression=payload.output_compression,
254
+ moderation=payload.moderation,
255
+ partial_images=payload.partial_images,
256
+ input_fidelity=payload.input_fidelity,
257
+ streaming=streaming,
258
+ is_edit=True,
259
+ )
260
+ return ResponsesRequest.model_validate(
261
+ {
262
+ "model": host_model,
263
+ "instructions": _IMAGE_GENERATION_INSTRUCTIONS,
264
+ "input": _build_user_message_input(prompt_text, attached_images=attached),
265
+ "tools": [tool],
266
+ # Force the host model to invoke the image_generation tool.
267
+ # Leaving this on "auto" lets the model return a refusal or
268
+ # plain text instead, which would surface as a 5xx through
269
+ # this adapter even though the request shape was valid. See
270
+ # the matching forced tool call in
271
+ # ``images_generation_to_responses_request``.
272
+ "tool_choice": {"type": "image_generation"},
273
+ # See ``images_generation_to_responses_request`` for why this is
274
+ # always True regardless of what the public client requested.
275
+ "stream": True,
276
+ "store": False,
277
+ }
278
+ )
279
+
280
+
281
+ # ---------------------------------------------------------------------------
282
+ # Public-request validation helpers wired by the route handlers.
283
+ # ---------------------------------------------------------------------------
284
+
285
+
286
+ def resolve_public_image_model(requested: str | None) -> str:
287
+ """Return the publicly-effective ``gpt-image-*`` model.
288
+
289
+ Falls back to the fixed ``DEFAULT_PUBLIC_IMAGE_MODEL`` when the client
290
+ omits ``model``. The returned value is always validated against the
291
+ ``gpt-image-*`` allowlist.
292
+ """
293
+ resolved = requested or DEFAULT_PUBLIC_IMAGE_MODEL
294
+ if not is_supported_image_model(resolved):
295
+ raise ClientPayloadError(
296
+ f"Unsupported image model '{resolved}'. Use a 'gpt-image-*' model.",
297
+ param="model",
298
+ code="invalid_request_error",
299
+ error_type="invalid_request_error",
300
+ )
301
+ return resolved
302
+
303
+
304
+ def validate_generations_payload(payload: V1ImagesGenerationsRequest) -> V1ImagesGenerationsRequest:
305
+ """Apply the cross-field validation matrix and return the payload with
306
+ ``model`` populated to the configured default when the client omitted it.
307
+ """
308
+ resolved_model = resolve_public_image_model(payload.model)
309
+ # Forward ``payload.input_fidelity`` so the validator rejects it on the
310
+ # generations path (it is an edit-only parameter). Without this the
311
+ # field would be silently dropped via the schema's ``extra=ignore``
312
+ # and an invalid request would 200 instead of 400.
313
+ validate_image_request_parameters(
314
+ model=resolved_model,
315
+ quality=payload.quality,
316
+ size=payload.size,
317
+ background=payload.background,
318
+ output_format=payload.output_format,
319
+ moderation=payload.moderation,
320
+ input_fidelity=payload.input_fidelity,
321
+ is_edit=False,
322
+ n=payload.n,
323
+ partial_images=payload.partial_images,
324
+ output_compression=payload.output_compression,
325
+ )
326
+ if payload.model != resolved_model:
327
+ # Pydantic models are immutable by default; build a copy with the
328
+ # resolved model so downstream code can rely on ``payload.model``
329
+ # always being a concrete ``gpt-image-*`` value.
330
+ return payload.model_copy(update={"model": resolved_model})
331
+ return payload
332
+
333
+
334
+ def validate_edits_payload(payload: V1ImagesEditsForm) -> V1ImagesEditsForm:
335
+ """Apply the cross-field validation matrix and return the payload with
336
+ ``model`` populated to the configured default when the client omitted it.
337
+ """
338
+ resolved_model = resolve_public_image_model(payload.model)
339
+ validate_image_request_parameters(
340
+ model=resolved_model,
341
+ quality=payload.quality,
342
+ size=payload.size,
343
+ background=payload.background,
344
+ output_format=payload.output_format,
345
+ moderation=payload.moderation,
346
+ input_fidelity=payload.input_fidelity,
347
+ is_edit=True,
348
+ n=payload.n,
349
+ partial_images=payload.partial_images,
350
+ output_compression=payload.output_compression,
351
+ )
352
+ if payload.model != resolved_model:
353
+ return payload.model_copy(update={"model": resolved_model})
354
+ return payload
355
+
356
+
357
+ # ---------------------------------------------------------------------------
358
+ # Non-streaming response translation
359
+ # ---------------------------------------------------------------------------
360
+
361
+
362
+ def _select_image_items(output: list[JsonValue]) -> list[Mapping[str, JsonValue]]:
363
+ items: list[Mapping[str, JsonValue]] = []
364
+ for entry in output:
365
+ if not is_json_mapping(entry):
366
+ continue
367
+ if entry.get("type") == "image_generation_call":
368
+ items.append(entry)
369
+ return items
370
+
371
+
372
+ def _coerce_int(value: JsonValue | None) -> int | None:
373
+ if isinstance(value, bool):
374
+ return None
375
+ if isinstance(value, int):
376
+ return value
377
+ if isinstance(value, float):
378
+ return int(value)
379
+ return None
380
+
381
+
382
+ _USAGE_TOKEN_FIELDS: Final[frozenset[str]] = frozenset({"input_tokens", "output_tokens", "total_tokens"})
383
+
384
+
385
+ def _extract_image_usage(response: Mapping[str, JsonValue]) -> V1ImageUsage | None:
386
+ tool_usage = response.get("tool_usage")
387
+ if not is_json_mapping(tool_usage):
388
+ return None
389
+ image_usage = tool_usage.get("image_gen")
390
+ if not is_json_mapping(image_usage):
391
+ return None
392
+ input_tokens = _coerce_int(image_usage.get("input_tokens"))
393
+ output_tokens = _coerce_int(image_usage.get("output_tokens"))
394
+ total_tokens = _coerce_int(image_usage.get("total_tokens"))
395
+ if total_tokens is None and input_tokens is not None and output_tokens is not None:
396
+ total_tokens = input_tokens + output_tokens
397
+ input_details_raw = image_usage.get("input_tokens_details")
398
+ output_details_raw = image_usage.get("output_tokens_details")
399
+ input_details = dict(input_details_raw) if is_json_mapping(input_details_raw) else None
400
+ output_details = dict(output_details_raw) if is_json_mapping(output_details_raw) else None
401
+ # Forward any other usage detail keys upstream may add (e.g. cached
402
+ # token counters) so the public response keeps the OpenAI Images
403
+ # usage shape rather than silently dropping new fields.
404
+ extra_usage: dict[str, JsonValue] = {}
405
+ for key, value in image_usage.items():
406
+ if key in _USAGE_TOKEN_FIELDS:
407
+ continue
408
+ if key in ("input_tokens_details", "output_tokens_details"):
409
+ continue
410
+ extra_usage[key] = value
411
+ if (
412
+ input_tokens is None
413
+ and output_tokens is None
414
+ and total_tokens is None
415
+ and input_details is None
416
+ and output_details is None
417
+ and not extra_usage
418
+ ):
419
+ return None
420
+ return V1ImageUsage(
421
+ input_tokens=input_tokens,
422
+ output_tokens=output_tokens,
423
+ total_tokens=total_tokens,
424
+ input_tokens_details=input_details,
425
+ output_tokens_details=output_details,
426
+ **extra_usage,
427
+ )
428
+
429
+
430
+ def images_response_from_responses(response: Mapping[str, JsonValue]) -> V1ImageResponse | OpenAIErrorEnvelope:
431
+ """Build the public Images response from a completed Responses payload.
432
+
433
+ Returns an :class:`OpenAIErrorEnvelope` (TypedDict) when the upstream
434
+ response indicates the image generation failed; otherwise returns a
435
+ :class:`V1ImageResponse`.
436
+ """
437
+ output_value = response.get("output")
438
+ if not isinstance(output_value, list):
439
+ return openai_error(
440
+ "image_generation_failed",
441
+ "Upstream response did not include an output array",
442
+ error_type="server_error",
443
+ )
444
+ items = _select_image_items(cast(list[JsonValue], output_value))
445
+ if not items:
446
+ return openai_error(
447
+ "image_generation_failed",
448
+ "Upstream response did not include any image_generation_call items",
449
+ error_type="server_error",
450
+ )
451
+
452
+ # Surface the first failed image_generation_call as an error envelope.
453
+ for item in items:
454
+ status = item.get("status")
455
+ if isinstance(status, str) and status == "failed":
456
+ error = item.get("error")
457
+ if is_json_mapping(error):
458
+ message = error.get("message")
459
+ code = error.get("code")
460
+ error_type = error.get("type")
461
+ return openai_error(
462
+ code if isinstance(code, str) and code else "image_generation_failed",
463
+ message if isinstance(message, str) and message else "Image generation failed",
464
+ error_type=error_type if isinstance(error_type, str) and error_type else "server_error",
465
+ )
466
+ return openai_error(
467
+ "image_generation_failed",
468
+ "Upstream image_generation_call reported status=failed",
469
+ error_type="server_error",
470
+ )
471
+
472
+ data_entries: list[V1ImageData] = []
473
+ for item in items:
474
+ result = item.get("result")
475
+ if not isinstance(result, str) or not result:
476
+ continue
477
+ revised_prompt = item.get("revised_prompt")
478
+ data_entries.append(
479
+ V1ImageData(
480
+ b64_json=result,
481
+ revised_prompt=revised_prompt if isinstance(revised_prompt, str) and revised_prompt else None,
482
+ )
483
+ )
484
+
485
+ if not data_entries:
486
+ return openai_error(
487
+ "image_generation_failed",
488
+ "Upstream image_generation_call items contained no image data",
489
+ error_type="server_error",
490
+ )
491
+
492
+ usage = _extract_image_usage(response)
493
+ return V1ImageResponse(
494
+ created=int(time.time()),
495
+ data=data_entries,
496
+ usage=usage,
497
+ )
498
+
499
+
500
+ # ---------------------------------------------------------------------------
501
+ # Streaming translation
502
+ # ---------------------------------------------------------------------------
503
+
504
+
505
+ def _stash_image_usage_tokens(captured: dict[str, object], usage: V1ImageUsage) -> None:
506
+ """Persist the ``tool_usage.image_gen`` token counts on ``captured``
507
+ so the route handler can post-hoc record them against the API key.
508
+
509
+ Stored under explicit ``image_input_tokens`` / ``image_output_tokens``
510
+ / ``image_cached_input_tokens`` keys (alongside the existing
511
+ ``response_id``) so the route handler does not have to re-parse the
512
+ SSE stream or know the V1ImageUsage shape. Cached tokens are pulled
513
+ from ``input_tokens_details.cached_tokens`` when upstream reports
514
+ them so cached requests are not billed as fully uncached input.
515
+ """
516
+ if usage.input_tokens is not None:
517
+ captured["image_input_tokens"] = int(usage.input_tokens)
518
+ if usage.output_tokens is not None:
519
+ captured["image_output_tokens"] = int(usage.output_tokens)
520
+ cached = _extract_cached_input_tokens(usage)
521
+ if cached is not None:
522
+ captured["image_cached_input_tokens"] = cached
523
+
524
+
525
+ def _extract_cached_input_tokens(usage: V1ImageUsage) -> int | None:
526
+ details = usage.input_tokens_details
527
+ if not isinstance(details, Mapping):
528
+ return None
529
+ raw = details.get("cached_tokens")
530
+ if isinstance(raw, bool):
531
+ return None
532
+ if isinstance(raw, int):
533
+ return raw
534
+ if isinstance(raw, float):
535
+ return int(raw)
536
+ return None
537
+
538
+
539
+ def _build_partial_image_event(payload: Mapping[str, JsonValue], *, event_type: str) -> dict[str, JsonValue] | None:
540
+ partial_b64 = payload.get("partial_image_b64")
541
+ if not isinstance(partial_b64, str) or not partial_b64:
542
+ return None
543
+ event: dict[str, JsonValue] = {
544
+ "type": event_type,
545
+ "b64_json": partial_b64,
546
+ # OpenAI Images stream event schemas expose ``created_at`` on
547
+ # both partial and completed events. Forward the upstream value
548
+ # if present, otherwise stamp a synthesized one so consumers
549
+ # that deserialize against the official model do not reject the
550
+ # event for a missing field.
551
+ "created_at": _coerce_created_at(payload.get("created_at")),
552
+ }
553
+ for key in ("partial_image_index", "size", "quality", "background", "output_format", "output_index"):
554
+ value = payload.get(key)
555
+ if value is not None:
556
+ event[key] = value
557
+ return event
558
+
559
+
560
+ def _build_completed_event(item: Mapping[str, JsonValue], *, event_type: str) -> dict[str, JsonValue] | None:
561
+ if item.get("type") != "image_generation_call":
562
+ return None
563
+ result = item.get("result")
564
+ if not isinstance(result, str) or not result:
565
+ return None
566
+ event: dict[str, JsonValue] = {
567
+ "type": event_type,
568
+ "b64_json": result,
569
+ # See ``_build_partial_image_event`` for the rationale: keep the
570
+ # upstream ``created_at`` if it is present, otherwise synthesize
571
+ # so the OpenAI Images stream-event schema validates.
572
+ "created_at": _coerce_created_at(item.get("created_at")),
573
+ }
574
+ for key in ("revised_prompt", "size", "quality", "background", "output_format"):
575
+ value = item.get(key)
576
+ if value is not None:
577
+ event[key] = value
578
+ return event
579
+
580
+
581
+ def _coerce_created_at(value: JsonValue | None) -> int:
582
+ """Return ``value`` as an int when upstream supplied it, otherwise
583
+ a synthesized current Unix timestamp."""
584
+ if isinstance(value, int) and not isinstance(value, bool):
585
+ return value
586
+ if isinstance(value, float):
587
+ return int(value)
588
+ return int(time.time())
589
+
590
+
591
+ def _build_error_event(
592
+ code: str,
593
+ message: str,
594
+ *,
595
+ error_type: str = "server_error",
596
+ param: str | None = None,
597
+ ) -> dict[str, JsonValue]:
598
+ envelope = openai_error(code, message, error_type=error_type)
599
+ if param:
600
+ envelope["error"]["param"] = param
601
+ event: dict[str, JsonValue] = {"type": _DOWNSTREAM_ERROR_EVENT}
602
+ for key, value in envelope.items():
603
+ event[key] = cast(JsonValue, value)
604
+ return event
605
+
606
+
607
+ def _failed_image_item_error_event(item: Mapping[str, JsonValue]) -> dict[str, JsonValue]:
608
+ error_value = item.get("error")
609
+ if is_json_mapping(error_value):
610
+ code = error_value.get("code")
611
+ message = error_value.get("message")
612
+ error_type = error_value.get("type")
613
+ return _build_error_event(
614
+ code if isinstance(code, str) and code else "image_generation_failed",
615
+ message if isinstance(message, str) and message else "Image generation failed",
616
+ error_type=error_type if isinstance(error_type, str) and error_type else "server_error",
617
+ )
618
+ return _build_error_event(
619
+ "image_generation_failed",
620
+ "Upstream image_generation_call reported status=failed",
621
+ )
622
+
623
+
624
+ def _response_failed_to_error_event(payload: Mapping[str, JsonValue]) -> dict[str, JsonValue]:
625
+ response = payload.get("response")
626
+ if is_json_mapping(response):
627
+ error_value = response.get("error")
628
+ if is_json_mapping(error_value):
629
+ code = error_value.get("code")
630
+ message = error_value.get("message")
631
+ error_type = error_value.get("type")
632
+ return _build_error_event(
633
+ code if isinstance(code, str) and code else "upstream_error",
634
+ message if isinstance(message, str) and message else "Upstream image generation failed",
635
+ error_type=error_type if isinstance(error_type, str) and error_type else "server_error",
636
+ )
637
+ return _build_error_event("upstream_error", "Upstream image generation failed")
638
+
639
+
640
+ def _error_event_to_error_event(payload: Mapping[str, JsonValue]) -> dict[str, JsonValue]:
641
+ error_value = payload.get("error")
642
+ if is_json_mapping(error_value):
643
+ code = error_value.get("code")
644
+ message = error_value.get("message")
645
+ error_type = error_value.get("type")
646
+ return _build_error_event(
647
+ code if isinstance(code, str) and code else "upstream_error",
648
+ message if isinstance(message, str) and message else "Upstream image generation failed",
649
+ error_type=error_type if isinstance(error_type, str) and error_type else "server_error",
650
+ )
651
+ return _build_error_event("upstream_error", "Upstream image generation failed")
652
+
653
+
654
+ async def translate_responses_stream_to_images_stream(
655
+ upstream: AsyncIterator[str],
656
+ *,
657
+ captured: dict[str, object] | None = None,
658
+ is_edit: bool = False,
659
+ ) -> AsyncIterator[str]:
660
+ """Convert a Responses SSE event stream into an OpenAI Images SSE stream.
661
+
662
+ Yields formatted SSE event blocks (terminated by ``\\n\\n``) suitable
663
+ for streaming directly to the client. Always emits exactly one terminal
664
+ event (``image_generation.completed`` / ``image_edit.completed`` or
665
+ ``error``) followed by ``data: [DONE]``.
666
+
667
+ The ``is_edit`` flag selects the canonical OpenAI event prefix:
668
+ ``/v1/images/generations`` callers leave it ``False`` and receive
669
+ ``image_generation.partial_image`` / ``image_generation.completed``;
670
+ ``/v1/images/edits`` callers pass ``True`` and receive
671
+ ``image_edit.partial_image`` / ``image_edit.completed`` so SDKs that
672
+ listen for the edit-specific event names see the stream.
673
+
674
+ When ``captured`` is provided, the translator stores the upstream
675
+ Responses ``id`` (under the ``response_id`` key) the first time it sees
676
+ one, so route handlers can correlate the resulting request log with the
677
+ public effective model after the stream completes.
678
+ """
679
+ partial_event_type = _EDIT_PARTIAL_EVENT if is_edit else _GENERATION_PARTIAL_EVENT
680
+ completed_event_type = _EDIT_COMPLETED_EVENT if is_edit else _GENERATION_COMPLETED_EVENT
681
+ terminal_emitted = False
682
+ completion_pending = True
683
+ # The Responses backend emits ``response.output_item.done`` for each
684
+ # ``image_generation_call`` *before* the final ``response.completed``
685
+ # event that carries ``tool_usage``. We buffer prepared completed
686
+ # events so we can attach ``usage`` once ``response.completed``
687
+ # arrives, and we keep them in arrival order so multi-image responses
688
+ # do not silently drop earlier completions. If the upstream stream
689
+ # ends without a ``response.completed`` we still flush whatever we
690
+ # have buffered (without ``usage``).
691
+ pending_completed_events: list[dict[str, JsonValue]] = []
692
+
693
+ async for line in upstream:
694
+ if not line:
695
+ continue
696
+ stripped = line.strip()
697
+ if stripped == "data: [DONE]":
698
+ # We emit our own [DONE] after a terminal event below.
699
+ continue
700
+ payload = parse_sse_data_json(line)
701
+ if payload is None:
702
+ continue
703
+ event_type = payload.get("type")
704
+ if not isinstance(event_type, str):
705
+ continue
706
+
707
+ # Capture the upstream Responses id once so the route handler can
708
+ # rewrite the request log with the public effective model after the
709
+ # stream completes.
710
+ if captured is not None and "response_id" not in captured:
711
+ response_value = payload.get("response")
712
+ if is_json_mapping(response_value):
713
+ response_id = response_value.get("id")
714
+ if isinstance(response_id, str) and response_id:
715
+ captured["response_id"] = response_id
716
+
717
+ if event_type == _UPSTREAM_PARTIAL_IMAGE_EVENT:
718
+ event = _build_partial_image_event(payload, event_type=partial_event_type)
719
+ if event is not None:
720
+ yield format_sse_event(event)
721
+ continue
722
+
723
+ if event_type == _UPSTREAM_OUTPUT_ITEM_DONE_EVENT:
724
+ item = payload.get("item")
725
+ if not is_json_mapping(item):
726
+ continue
727
+ if item.get("type") != "image_generation_call":
728
+ continue
729
+ status = item.get("status")
730
+ if isinstance(status, str) and status == "failed":
731
+ if captured is not None:
732
+ captured["image_stream_outcome"] = "image_error"
733
+ yield format_sse_event(_failed_image_item_error_event(item))
734
+ terminal_emitted = True
735
+ completion_pending = False
736
+ break
737
+ event = _build_completed_event(item, event_type=completed_event_type)
738
+ if event is not None:
739
+ # Buffer every completion (NOT a single overwrite-on-update
740
+ # variable) so multi-image responses do not silently drop
741
+ # earlier images. Flushing is deferred until the trailing
742
+ # ``response.completed`` arrives so we can attach
743
+ # ``tool_usage.image_gen`` as ``usage`` to the final event,
744
+ # matching the canonical OpenAI Images streaming shape.
745
+ pending_completed_events.append(event)
746
+ continue
747
+
748
+ if event_type == _UPSTREAM_RESPONSE_COMPLETED_EVENT:
749
+ response_obj = payload.get("response")
750
+ usage = _extract_image_usage(response_obj) if is_json_mapping(response_obj) else None
751
+ # Stash the usage tokens on ``captured`` so the route handler
752
+ # can post-hoc record them against the API key (the
753
+ # standard stream settlement only sees ``response.usage``,
754
+ # which is typically empty for the image_generation tool
755
+ # path).
756
+ if captured is not None and usage is not None:
757
+ _stash_image_usage_tokens(captured, usage)
758
+ if pending_completed_events:
759
+ # Emit every buffered completion in arrival order so
760
+ # multi-image responses do not silently drop earlier
761
+ # images. Only the last completion carries ``usage``.
762
+ last_index = len(pending_completed_events) - 1
763
+ for idx, event in enumerate(pending_completed_events):
764
+ if idx == last_index and usage is not None:
765
+ event["usage"] = usage.model_dump(mode="json", exclude_none=True)
766
+ yield format_sse_event(event)
767
+ pending_completed_events.clear()
768
+ terminal_emitted = True
769
+ if captured is not None:
770
+ captured["image_stream_outcome"] = "success"
771
+ elif not terminal_emitted:
772
+ if captured is not None:
773
+ captured["image_stream_outcome"] = "image_error"
774
+ yield format_sse_event(
775
+ _build_error_event(
776
+ "image_generation_failed",
777
+ "Upstream stream completed without an image_generation_call result",
778
+ )
779
+ )
780
+ terminal_emitted = True
781
+ completion_pending = False
782
+ break
783
+
784
+ if event_type == _UPSTREAM_RESPONSE_INCOMPLETE_EVENT:
785
+ if not terminal_emitted:
786
+ if captured is not None:
787
+ captured["image_stream_outcome"] = "image_error"
788
+ yield format_sse_event(
789
+ _build_error_event(
790
+ "image_generation_failed",
791
+ "Upstream stream ended before the image was generated",
792
+ )
793
+ )
794
+ terminal_emitted = True
795
+ completion_pending = False
796
+ break
797
+
798
+ if event_type == _UPSTREAM_RESPONSE_FAILED_EVENT:
799
+ if captured is not None:
800
+ captured["image_stream_outcome"] = "image_error"
801
+ yield format_sse_event(_response_failed_to_error_event(payload))
802
+ terminal_emitted = True
803
+ completion_pending = False
804
+ break
805
+
806
+ if event_type == _UPSTREAM_ERROR_EVENT:
807
+ if captured is not None:
808
+ captured["image_stream_outcome"] = "image_error"
809
+ yield format_sse_event(_error_event_to_error_event(payload))
810
+ terminal_emitted = True
811
+ completion_pending = False
812
+ break
813
+
814
+ # All other event types (response.created, reasoning, content_part,
815
+ # output_text, image_generation_call.in_progress / .generating,
816
+ # codex.rate_limits, etc.) are intentionally dropped.
817
+ continue
818
+
819
+ # If the upstream stream ended without a ``response.completed`` (e.g.
820
+ # truncation), still flush whatever we have buffered so the client sees
821
+ # a terminal event before [DONE]. ``usage`` is unknown in this case so
822
+ # we omit it.
823
+ if pending_completed_events and not terminal_emitted:
824
+ for event in pending_completed_events:
825
+ yield format_sse_event(event)
826
+ pending_completed_events.clear()
827
+ terminal_emitted = True
828
+ if captured is not None:
829
+ captured["image_stream_outcome"] = "truncated_with_image"
830
+
831
+ if completion_pending and not terminal_emitted:
832
+ if captured is not None:
833
+ captured["image_stream_outcome"] = "truncated"
834
+ yield format_sse_event(
835
+ _build_error_event(
836
+ "image_generation_failed",
837
+ "Upstream stream truncated before a terminal image event",
838
+ )
839
+ )
840
+
841
+ yield "data: [DONE]\n\n"
842
+
843
+
844
+ async def collect_responses_stream_for_images(
845
+ upstream: AsyncIterator[str],
846
+ *,
847
+ captured: dict[str, object] | None = None,
848
+ ) -> tuple[dict[str, JsonValue] | None, OpenAIErrorEnvelope | None]:
849
+ """Drain a Responses SSE stream and return the final ``response`` payload.
850
+
851
+ Returns ``(response_mapping, None)`` when the upstream stream emits a
852
+ ``response.completed`` event; ``(None, error_envelope)`` when it emits
853
+ ``response.failed`` / ``error`` / closes early.
854
+
855
+ When ``captured`` is provided, stores the upstream Responses ``id`` the
856
+ first time it appears in any event, so the caller can correlate the
857
+ resulting request log with the public effective model.
858
+ """
859
+ output_items: dict[int, dict[str, JsonValue]] = {}
860
+ fallback_items: list[dict[str, JsonValue]] = []
861
+ final_response: dict[str, JsonValue] | None = None
862
+ terminal_error: OpenAIErrorEnvelope | None = None
863
+
864
+ async for line in upstream:
865
+ if not line:
866
+ continue
867
+ if line.strip() == "data: [DONE]":
868
+ continue
869
+ payload = parse_sse_data_json(line)
870
+ if payload is None:
871
+ continue
872
+ event_type = payload.get("type")
873
+ if not isinstance(event_type, str):
874
+ continue
875
+
876
+ # Capture the upstream Responses id once so the route handler can
877
+ # rewrite the request log with the public effective model after the
878
+ # stream completes.
879
+ if captured is not None and "response_id" not in captured:
880
+ response_value = payload.get("response")
881
+ if is_json_mapping(response_value):
882
+ response_id = response_value.get("id")
883
+ if isinstance(response_id, str) and response_id:
884
+ captured["response_id"] = response_id
885
+
886
+ if event_type == _UPSTREAM_OUTPUT_ITEM_DONE_EVENT:
887
+ output_index = payload.get("output_index")
888
+ item = payload.get("item")
889
+ if not is_json_mapping(item):
890
+ continue
891
+ if isinstance(output_index, int):
892
+ output_items[output_index] = dict(item)
893
+ else:
894
+ # Some upstream paths omit ``output_index``; preserve the
895
+ # arrival order so we can still surface the item.
896
+ fallback_items.append(dict(item))
897
+ continue
898
+
899
+ if event_type == _UPSTREAM_RESPONSE_COMPLETED_EVENT and final_response is None:
900
+ response_value = payload.get("response")
901
+ base: dict[str, JsonValue]
902
+ if is_json_mapping(response_value):
903
+ base = dict(response_value)
904
+ else:
905
+ base = {}
906
+ existing_output = base.get("output")
907
+ if not (isinstance(existing_output, list) and existing_output):
908
+ merged_output: list[JsonValue] = [item for _, item in sorted(output_items.items())]
909
+ merged_output.extend(fallback_items)
910
+ base["output"] = merged_output
911
+ final_response = base
912
+ # Stash the image usage tokens so the route handler can
913
+ # post-hoc charge the API key. The standard stream
914
+ # settlement only reads ``response.usage`` which is
915
+ # typically empty for the image_generation tool path.
916
+ if captured is not None:
917
+ tool_usage_payload = base
918
+ usage = _extract_image_usage(tool_usage_payload)
919
+ if usage is not None:
920
+ _stash_image_usage_tokens(captured, usage)
921
+ # Don't break — keep draining so the upstream stream_responses
922
+ # generator finalizes (which writes the request log) before we
923
+ # return control to the caller. Once we have ``final_response``
924
+ # we still skip processing of any subsequent events.
925
+ continue
926
+
927
+ if event_type == _UPSTREAM_RESPONSE_INCOMPLETE_EVENT and terminal_error is None:
928
+ # Match the streaming translator: an incomplete upstream
929
+ # response is surfaced as an error envelope rather than a
930
+ # 200 response carrying a half-finished image.
931
+ terminal_error = openai_error(
932
+ "image_generation_failed",
933
+ "Upstream response was incomplete before the image was generated",
934
+ error_type="server_error",
935
+ )
936
+ break
937
+
938
+ if event_type == _UPSTREAM_RESPONSE_FAILED_EVENT:
939
+ # Once a successful ``response.completed`` is captured, ignore
940
+ # any trailing transport-level failure events: they cannot
941
+ # invalidate an already-received image result. Without this
942
+ # guard a late ``response.failed`` could turn a successful
943
+ # 200 envelope into a spurious 502.
944
+ if final_response is not None:
945
+ continue
946
+ response_value = payload.get("response")
947
+ error_value: JsonValue | None = None
948
+ if is_json_mapping(response_value):
949
+ error_value = response_value.get("error")
950
+ if is_json_mapping(error_value):
951
+ code = error_value.get("code")
952
+ message = error_value.get("message")
953
+ error_type = error_value.get("type")
954
+ envelope = openai_error(
955
+ code if isinstance(code, str) and code else "upstream_error",
956
+ message if isinstance(message, str) and message else "Upstream image generation failed",
957
+ error_type=error_type if isinstance(error_type, str) and error_type else "server_error",
958
+ )
959
+ else:
960
+ envelope = openai_error(
961
+ "upstream_error",
962
+ "Upstream image generation failed",
963
+ error_type="server_error",
964
+ )
965
+ terminal_error = envelope
966
+ break
967
+
968
+ if event_type == _UPSTREAM_ERROR_EVENT:
969
+ # Same rationale as the ``response.failed`` branch above:
970
+ # ignore late error events once a complete response has
971
+ # already been captured.
972
+ if final_response is not None:
973
+ continue
974
+ error_value = payload.get("error")
975
+ if is_json_mapping(error_value):
976
+ code = error_value.get("code")
977
+ message = error_value.get("message")
978
+ error_type = error_value.get("type")
979
+ envelope = openai_error(
980
+ code if isinstance(code, str) and code else "upstream_error",
981
+ message if isinstance(message, str) and message else "Upstream image generation failed",
982
+ error_type=error_type if isinstance(error_type, str) and error_type else "server_error",
983
+ )
984
+ else:
985
+ envelope = openai_error(
986
+ "upstream_error",
987
+ "Upstream image generation failed",
988
+ error_type="server_error",
989
+ )
990
+ terminal_error = envelope
991
+ break
992
+
993
+ if terminal_error is not None:
994
+ return None, terminal_error
995
+ if final_response is None:
996
+ return None, openai_error(
997
+ "image_generation_failed",
998
+ "Upstream stream truncated before a terminal event",
999
+ error_type="server_error",
1000
+ )
1001
+ return final_response, None
1002
+
1003
+
1004
+ # ---------------------------------------------------------------------------
1005
+ # Misc utilities used by the API handlers
1006
+ # ---------------------------------------------------------------------------
1007
+
1008
+
1009
+ def decode_data_url(data_url: str) -> tuple[bytes, str | None]:
1010
+ """Decode a ``data:<mime>;base64,<...>`` URL into raw bytes plus mime.
1011
+
1012
+ Raises ``ValueError`` if the URL does not match the expected shape or
1013
+ the base64 payload is malformed.
1014
+ """
1015
+ match = _DATA_URL_PATTERN.match(data_url.strip())
1016
+ if match is None:
1017
+ raise ValueError("Expected a data:<mime>;base64,<payload> URL")
1018
+ mime_type = match.group("mime") or None
1019
+ try:
1020
+ return base64.b64decode(match.group("b64"), validate=True), mime_type
1021
+ except (binascii.Error, ValueError) as exc:
1022
+ raise ValueError("Invalid base64 image payload") from exc
1023
+
1024
+
1025
+ def make_invalid_request_error(
1026
+ message: str,
1027
+ *,
1028
+ param: str | None = None,
1029
+ code: str = "invalid_request_error",
1030
+ ) -> OpenAIErrorEnvelope:
1031
+ envelope = openai_error(code, message, error_type="invalid_request_error")
1032
+ if param:
1033
+ envelope["error"]["param"] = param
1034
+ return envelope
1035
+
1036
+
1037
+ def make_not_found_error(message: str) -> OpenAIErrorEnvelope:
1038
+ return openai_error("not_found_error", message, error_type="invalid_request_error")
1039
+
1040
+
1041
+ __all__ = [
1042
+ "collect_responses_stream_for_images",
1043
+ "decode_data_url",
1044
+ "images_edit_to_responses_request",
1045
+ "images_generation_to_responses_request",
1046
+ "images_response_from_responses",
1047
+ "is_supported_image_model",
1048
+ "make_invalid_request_error",
1049
+ "make_not_found_error",
1050
+ "resolve_public_image_model",
1051
+ "translate_responses_stream_to_images_stream",
1052
+ "validate_edits_payload",
1053
+ "validate_generations_payload",
1054
+ ]