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,999 @@
1
+ """Single-owner lifecycle for a dispatched model-source attempt (#2123 WP-C1).
2
+
3
+ Every request forwarded to an OpenAI-compatible model source is owned by
4
+ exactly one ``SourceDispatch`` that ends in exactly one ``finish()`` (I10): the
5
+ source connection is closed, the API-key reservation is settled or released
6
+ exactly once (I3), the admission claims are released, and one request-log row
7
+ is written per attempt -- each step independently latched and failure-isolated
8
+ (design v3 §6). ``abandon()`` is the cancellation-class ``finish()`` for a
9
+ client that left during the open or before the body started (§6.4 table).
10
+
11
+ Composition rules:
12
+
13
+ * the settlement generator (``settlement_stream``) is the *outermost* body
14
+ layer for every composition: it sees the terminal outcome and the
15
+ cancellation first, and it owns the reservation;
16
+ * ``SourceStreamingResponse.__call__`` wraps the transport in
17
+ ``try/finally: _await_cleanup_deferring_cancellation(owner.finalize_transport(), scheduler=owner.scheduler)``
18
+ so a client that leaves before Starlette starts the body still reaches one
19
+ ``finish()``; the direct chat-completions stream route, which has no
20
+ dispatch owner, hands the same response class a ``SourceChatStreamOwner``
21
+ so its never-started body closes the source transport too;
22
+ * the handler segment uses ``except BaseException -> abandon(); raise``
23
+ (``CancelledError`` is a ``BaseException`` and is never caught by
24
+ ``except Exception``);
25
+ * every timer, wait and task spawn goes through ``owner.scheduler`` /
26
+ ``owner.clock`` (``scripts/check_proxy_timing_seams.py`` has a zero
27
+ allowance for this module) and every cleanup await defers cancellation
28
+ (``scripts/check_cancellation_safety.py``).
29
+
30
+ Settle policy (design §6.1, CL-8/P4/CP-16): ``success`` with usage finalizes
31
+ at the source's usage and cost; ``success`` without usage on a limited key
32
+ settles at an estimate (never at zero, never released); a cancel *after*
33
+ content was delivered to the client (an event frame ``settlement_stream``
34
+ handed to the transport -- not the parser's observation, nor the body's flush
35
+ point, which sits below wrapper layers that can still drop or park a frame)
36
+ on a limited key settles at the estimate (a key must not consume most of an
37
+ answer and disconnect unmetered); a cancel before it, and every
38
+ ``error``, release. A stream the source ends with a failure terminal, or
39
+ closes without any terminal the client could complete on, is an ``error``
40
+ (``model_source_response_failed`` / ``model_source_stream_truncated``) and
41
+ releases: the client received a failure, so nothing is charged. Estimates are
42
+ never written to the request-log row
43
+ as usage -- they are visible through the WARN line and the
44
+ ``codex_lb_model_source_usage_estimated_total`` counter.
45
+ """
46
+
47
+ from __future__ import annotations
48
+
49
+ import asyncio
50
+ import logging
51
+ from collections.abc import AsyncIterator, Awaitable, Callable, Coroutine, Mapping
52
+ from dataclasses import dataclass, field
53
+ from typing import Any, Literal, Protocol, TypeVar
54
+
55
+ from fastapi import Request
56
+ from fastapi.responses import StreamingResponse
57
+ from starlette.types import Receive, Scope, Send
58
+
59
+ from app.core.clock import REAL_CLOCK, REAL_SCHEDULER, Clock, Scheduler
60
+ from app.core.metrics.prometheus import (
61
+ model_source_dispatch_abandoned_total,
62
+ model_source_dispatch_total,
63
+ model_source_timeout_total,
64
+ model_source_usage_estimated_total,
65
+ )
66
+ from app.core.openai.parsing import classify_event_type
67
+ from app.core.request_locality import resolve_request_client_host
68
+ from app.core.types import JsonValue
69
+ from app.core.utils.request_id import ensure_request_id
70
+ from app.core.utils.shared_future import (
71
+ _await_cleanup_deferring_cancellation,
72
+ _await_result_deferring_cancellation,
73
+ _await_task_deferring_cancellation,
74
+ )
75
+ from app.core.utils.sse import _SSE_LINE_BOUNDARY, parse_sse_data_json
76
+ from app.db.models import ModelSource
77
+ from app.db.session import get_background_session
78
+ from app.modules.api_keys.service import (
79
+ API_KEY_USAGE_RESERVATION_DEFAULT_INPUT_TOKENS,
80
+ API_KEY_USAGE_RESERVATION_DEFAULT_OUTPUT_TOKENS,
81
+ ApiKeyData,
82
+ ApiKeyRequestUsageBudget,
83
+ ApiKeyUsageReservationData,
84
+ )
85
+ from app.modules.model_sources.catalog import source_model_cost_usd
86
+ from app.modules.model_sources.forwarding import (
87
+ ModelSourceForwardingError,
88
+ SourceChatStream,
89
+ SourceResponsesStream,
90
+ SourceTimings,
91
+ SourceUsage,
92
+ SourceUsageHolder,
93
+ TimeoutPhase,
94
+ classify_responses_frame,
95
+ )
96
+ from app.modules.proxy._service.support import _request_log_client_fields
97
+ from app.modules.proxy.affinity import _owner_lookup_session_id_from_headers
98
+ from app.modules.proxy.source_admission import SourceAdmission
99
+ from app.modules.request_logs.repository import RequestLogsRepository
100
+
101
+ logger = logging.getLogger(__name__)
102
+
103
+ T = TypeVar("T")
104
+
105
+ DispatchStatus = Literal["success", "error", "cancelled"]
106
+ EstimateCause = Literal["missing_usage", "client_cancel"]
107
+ AbandonStage = Literal["during_open", "before_body", "stall"]
108
+
109
+ # Attribution for direct source routing.
110
+ REQUEST_LOG_SOURCE = "model_source"
111
+ DISPATCH_KIND = "direct"
112
+
113
+ # A client that leaves while the open is still pending is a stall abandonment
114
+ # once the source has been silent this long without a first frame.
115
+ STALL_EVIDENCE_SECONDS = 10.0
116
+ # Disconnect poll cadence while the source open is pending.
117
+ OPEN_DISCONNECT_POLL_SECONDS = 0.25
118
+
119
+ # Terminal codes written by the owner itself (stage-naming ``error_code``).
120
+ ABANDON_CLIENT_DISCONNECTED_DURING_OPEN = "client_disconnected_during_open"
121
+ ABANDON_SOURCE_STALL = "source_stall_abandoned"
122
+ ABANDON_CLIENT_DISCONNECTED_BEFORE_BODY = "client_disconnected_before_body"
123
+ ABANDON_DISPATCH_INTERRUPTED = "dispatch_interrupted"
124
+ ERROR_USAGE_SETTLEMENT_FAILED = "usage_settlement_failed"
125
+ ERROR_MODEL_SOURCE_STREAM = "model_source_stream_error"
126
+ # The source ended the stream with ``response.failed`` / ``error``: no answer was
127
+ # delivered, so the attempt is an error and the reservation is released.
128
+ ERROR_MODEL_SOURCE_RESPONSE_FAILED = "model_source_response_failed"
129
+ # The source closed the stream without any terminal frame (the public wrapper
130
+ # synthesizes ``response.failed`` for SDK clients): no answer was delivered.
131
+ ERROR_MODEL_SOURCE_STREAM_TRUNCATED = "model_source_stream_truncated"
132
+ # The source ended with a success terminal the public contract could not accept
133
+ # (``response`` not an object, invalid output items): the wrapper relayed
134
+ # ``response.failed`` in its place, so the client received a failure and no
135
+ # answer was delivered.
136
+ ERROR_MODEL_SOURCE_RESPONSE_INVALID = "model_source_response_invalid"
137
+ _FAILURE_TERMINAL_KINDS = frozenset({"failed", "error"})
138
+ _SUCCESS_TERMINAL_KINDS = frozenset({"completed", "incomplete"})
139
+ _RELAYED_TERMINAL_KINDS: Mapping[str, str] = {
140
+ "response.completed": "completed",
141
+ "response.incomplete": "incomplete",
142
+ "response.failed": "failed",
143
+ "error": "error",
144
+ }
145
+ # Frames the settlement layer never takes as the stream's last event: SSE
146
+ # comments (``: keepalive``), the ``[DONE]`` sentinel and the Codex keepalive.
147
+ _NON_EVENT_FRAME_PREFIXES = ("data: [DONE]", "event: codex.keepalive")
148
+ # ``classify_responses_frame`` kinds that deliver response content to the
149
+ # client (the api-keys delta: an output item, a ``*.delta``, a success terminal
150
+ # or any event type this proxy does not know); bookkeeping and failure
151
+ # terminals do not.
152
+ _DELIVERING_FRAME_KINDS = frozenset({"content", "success_terminal"})
153
+ CANCELLED_CLIENT_DISCONNECTED = "client_disconnected"
154
+
155
+ _ABANDON_STAGES: Mapping[str, AbandonStage] = {
156
+ ABANDON_CLIENT_DISCONNECTED_DURING_OPEN: "during_open",
157
+ ABANDON_DISPATCH_INTERRUPTED: "during_open",
158
+ ABANDON_SOURCE_STALL: "stall",
159
+ ABANDON_CLIENT_DISCONNECTED_BEFORE_BODY: "before_body",
160
+ }
161
+
162
+
163
+ class CleanupScheduler(Protocol):
164
+ """``ProxyService`` satisfies this: release retries ride its cancel-safe cleanup tasks."""
165
+
166
+ def _schedule_cancel_safe_cleanup(
167
+ self,
168
+ coro: Coroutine[Any, Any, None],
169
+ *,
170
+ action: str,
171
+ request_id: str,
172
+ ) -> asyncio.Task[None]: ...
173
+
174
+
175
+ class ReservationSettler(Protocol):
176
+ """``api.py``'s ``_settle_source_reservation`` shape: finalize at ``usage``; ``False`` after a release fallback."""
177
+
178
+ def __call__(
179
+ self,
180
+ reservation: ApiKeyUsageReservationData,
181
+ *,
182
+ source: ModelSource,
183
+ model: str,
184
+ usage: SourceUsage | None,
185
+ cost_usd_override: float | None = None,
186
+ ) -> Coroutine[Any, Any, bool]: ...
187
+
188
+
189
+ class ReservationReleaser(Protocol):
190
+ """``api.py``'s ``_release_reservation`` shape."""
191
+
192
+ def __call__(self, reservation: ApiKeyUsageReservationData) -> Coroutine[Any, Any, None]: ...
193
+
194
+
195
+ class ClientDisconnectedDuringOpen(Exception):
196
+ """The client left while the source open was pending."""
197
+
198
+ def __init__(self, *, pending_seconds: float, stall: bool) -> None:
199
+ super().__init__(f"client disconnected after {pending_seconds:.3f}s (stall={stall})")
200
+ self.pending_seconds = pending_seconds
201
+ self.stall = stall
202
+
203
+
204
+ def relayed_terminal_kind(frame: str | None) -> str | None:
205
+ """Terminal kind of an event frame relayed to the client; ``None`` when it is not a terminal.
206
+
207
+ Read from the ``event:`` line the public wrapper frames every typed event
208
+ with, falling back to the ``data:`` JSON for a raw pass-through block.
209
+ ``settlement_stream`` latches the kinds it sees into the client-visible
210
+ outcome: when the parser saw no terminal (the frame outgrew its cap, or the
211
+ source closed without one) it decides between a delivered success and a
212
+ truncated stream.
213
+ """
214
+
215
+ if not frame:
216
+ return None
217
+ event_type = _relayed_event_type(frame)
218
+ return _RELAYED_TERMINAL_KINDS.get(event_type) if event_type is not None else None
219
+
220
+
221
+ def relayed_frame_delivers_content(frame: str | None) -> bool:
222
+ """``True`` when a relayed event frame carries response content the client can use.
223
+
224
+ Classified with the parser's own ``classify_responses_frame`` so the body
225
+ and the settlement layer agree on what content is: an output item, a
226
+ ``*.delta``, a success terminal or any event type this proxy does not know
227
+ delivers content; ``response.created`` / ``in_progress`` / ``queued``, a
228
+ failure terminal and a frame without a typed event (a comment, ``[DONE]``,
229
+ the Codex keepalive, unparseable ``data``) do not.
230
+ """
231
+
232
+ if not frame or not _is_event_frame(frame):
233
+ return False
234
+ return classify_responses_frame(_relayed_event_type(frame)) in _DELIVERING_FRAME_KINDS
235
+
236
+
237
+ def _is_event_frame(chunk: str) -> bool:
238
+ """An SSE block that carries an event: not a comment, not ``[DONE]``, not the Codex keepalive."""
239
+
240
+ return bool(chunk) and chunk[0] != ":" and not chunk.startswith(_NON_EVENT_FRAME_PREFIXES)
241
+
242
+
243
+ def _relayed_event_type(frame: str) -> str | None:
244
+ """Event type of a relayed frame: the ``event:`` line the public wrapper frames every typed event with, else the
245
+ type of a raw pass-through block's ``data:`` JSON as the wrapper's own ``classify_event_type`` reads it (a
246
+ typeless ``{"error": {...}}`` record is an ``error`` terminal there too); ``None`` when neither names one.
247
+
248
+ Lines end at the SSE boundary set (CR, LF or CRLF -- ``_SSE_LINE_BOUNDARY``, never ``str.splitlines``, whose
249
+ extra Unicode breaks can sit unescaped inside a JSON string) and a multi-line ``data:`` is joined as the parser
250
+ joins it (``parse_sse_data_json``), so a spec-legal framing the wrapper relays verbatim classifies as the frame
251
+ the client parses: a bare-CR ``response.created`` is bookkeeping, not an unknown content-bearing type.
252
+ """
253
+
254
+ if frame.startswith("event: "):
255
+ return _SSE_LINE_BOUNDARY.split(frame[7:], maxsplit=1)[0]
256
+ return classify_event_type(parse_sse_data_json(frame))
257
+
258
+
259
+ def error_code_from_payload(payload: Mapping[str, JsonValue]) -> str | None:
260
+ error = payload.get("error")
261
+ if not isinstance(error, Mapping):
262
+ return None
263
+ code = error.get("code")
264
+ return code if isinstance(code, str) else None
265
+
266
+
267
+ def error_message_from_payload(payload: Mapping[str, JsonValue]) -> str | None:
268
+ error = payload.get("error")
269
+ if not isinstance(error, Mapping):
270
+ return None
271
+ message = error.get("message")
272
+ return message if isinstance(message, str) else None
273
+
274
+
275
+ def source_usage_cost_usd(source: ModelSource, model: str, usage: SourceUsage | None) -> float | None:
276
+ """Source pricing for ``usage``; unpriced entries cost ``0.0`` (never ``None`` for known usage)."""
277
+
278
+ if usage is None:
279
+ return None
280
+ cost_usd = source_model_cost_usd(
281
+ source,
282
+ model,
283
+ input_tokens=usage.input_tokens,
284
+ output_tokens=usage.output_tokens,
285
+ cached_input_tokens=usage.cached_input_tokens,
286
+ )
287
+ return 0.0 if cost_usd is None else cost_usd
288
+
289
+
290
+ def _reservation_requires_usage(reservation: ApiKeyUsageReservationData | None) -> bool:
291
+ return bool(reservation is not None and reservation.has_applicable_limits)
292
+
293
+
294
+ def estimate_settlement_usage(*, admission_budget: ApiKeyRequestUsageBudget | None, delta_chars: int) -> SourceUsage:
295
+ """Settle-at-estimate figures: input = admission estimate or default; output = max(default, delta_chars // 4)."""
296
+
297
+ input_tokens = API_KEY_USAGE_RESERVATION_DEFAULT_INPUT_TOKENS
298
+ if admission_budget is not None and admission_budget.input_tokens is not None:
299
+ input_tokens = admission_budget.input_tokens
300
+ output_tokens = max(API_KEY_USAGE_RESERVATION_DEFAULT_OUTPUT_TOKENS, max(0, delta_chars) // 4)
301
+ return SourceUsage(input_tokens=input_tokens, output_tokens=output_tokens, cached_input_tokens=0)
302
+
303
+
304
+ def _inc(counter: Any, **labels: str) -> None:
305
+ if counter is None:
306
+ return
307
+ try:
308
+ counter.labels(**labels).inc()
309
+ except Exception: # metrics never break the request path
310
+ logger.debug("model_source_dispatch_metric_failed labels=%s", labels, exc_info=True)
311
+
312
+
313
+ @dataclass(slots=True)
314
+ class SourceDispatch:
315
+ """Owner of one dispatched attempt; ``finish()``/``abandon()`` is the single latch."""
316
+
317
+ request: Request
318
+ source: ModelSource
319
+ model: str
320
+ api_key: ApiKeyData | None
321
+ reservation: ApiKeyUsageReservationData | None
322
+ claims: SourceAdmission
323
+ admission_budget: ApiKeyRequestUsageBudget | None
324
+ requested_service_tier: str | None
325
+ cleanup_scheduler: CleanupScheduler | None = None
326
+ scheduler: Scheduler = REAL_SCHEDULER
327
+ clock: Clock = REAL_CLOCK
328
+ stream: SourceResponsesStream | None = None
329
+ sent_at: float = 0.0
330
+ first_frame_at: float | None = None
331
+ # Set by ``settlement_stream`` when it hands the transport an event frame
332
+ # that carries content (``relayed_frame_delivers_content``); the cancel
333
+ # settlement policy keys on this alone. It is never mirrored from
334
+ # ``SourceUsageHolder.content_delivered`` (the body's flush point): the
335
+ # public wrapper above that point still drops vendor events, parks
336
+ # ``response.*`` frames ahead of ``response.created`` and holds reasoning
337
+ # deltas, so a chunk the body counted may never reach the client.
338
+ content_delivered: bool = False
339
+ delta_chars: int = 0
340
+ body_started: bool = False
341
+ finished: bool = False
342
+ # Reservation primitives are injected by the route (``api.py`` owns them and
343
+ # its tests patch them there); a dispatch that holds a reservation without
344
+ # them cannot settle and logs an error instead of leaking silently.
345
+ settle_reservation: ReservationSettler | None = None
346
+ release_reservation: ReservationReleaser | None = None
347
+ request_id: str | None = None
348
+ # Outermost body iterator handed to ``SourceStreamingResponse`` (closed by ``finalize_transport``).
349
+ body: AsyncIterator[str] | None = None
350
+ # Non-stream completions carry the source response id in the JSON body
351
+ # (streams expose it through the usage holder).
352
+ source_response_id: str | None = None
353
+ settlement_failed: bool = False
354
+ _source_closed: bool = field(default=False, init=False, repr=False)
355
+ _reservation_done: bool = field(default=False, init=False, repr=False)
356
+ _claims_released: bool = field(default=False, init=False, repr=False)
357
+ _result_recorded: bool = field(default=False, init=False, repr=False)
358
+ _row_written: bool = field(default=False, init=False, repr=False)
359
+
360
+ def __post_init__(self) -> None:
361
+ if self.request_id is None:
362
+ self.request_id = ensure_request_id()
363
+
364
+ # -- observations -----------------------------------------------------------
365
+
366
+ @property
367
+ def usage_holder(self) -> SourceUsageHolder | None:
368
+ return self.stream.usage_holder if self.stream is not None else None
369
+
370
+ def observe_stream(self) -> SourceUsageHolder | None:
371
+ """Mirror the parser's observations (first frame, first output item, delta chars).
372
+
373
+ ``content_delivered`` is deliberately not mirrored: the holder's flag is
374
+ the body's flush-point evidence, delivery is decided by
375
+ ``settlement_stream`` from the frames it hands to the transport.
376
+ """
377
+
378
+ holder = self.usage_holder
379
+ if holder is None:
380
+ return None
381
+ if self.first_frame_at is None and holder.first_frame_at is not None:
382
+ self.first_frame_at = holder.first_frame_at
383
+ self.delta_chars = max(self.delta_chars, holder.delta_chars)
384
+ return holder
385
+
386
+ # -- latched steps ----------------------------------------------------------------
387
+
388
+ async def close_source(self) -> None:
389
+ if self._source_closed:
390
+ return
391
+ self._source_closed = True
392
+ stream = self.stream
393
+ if stream is None:
394
+ return
395
+ try:
396
+ await _await_cleanup_deferring_cancellation(stream.aclose(), scheduler=self.scheduler)
397
+ except Exception:
398
+ logger.warning(
399
+ "model_source_dispatch_close_failed request_id=%s source_id=%s",
400
+ self.request_id,
401
+ self.source.id,
402
+ exc_info=True,
403
+ )
404
+
405
+ async def settle_or_release(self, status: DispatchStatus, usage: SourceUsage | None) -> None:
406
+ if self._reservation_done:
407
+ return
408
+ self._reservation_done = True
409
+ reservation = self.reservation
410
+ if reservation is None:
411
+ return
412
+ self.observe_stream()
413
+ if status == "error":
414
+ await self._release_reservation_step(reservation)
415
+ return
416
+ if usage is not None:
417
+ await self._settle_reservation_step(reservation, usage, cause=None)
418
+ return
419
+ if status == "success" and _reservation_requires_usage(reservation):
420
+ await self._settle_reservation_step(reservation, self._estimate(), cause="missing_usage")
421
+ return
422
+ if status == "cancelled" and self.content_delivered and _reservation_requires_usage(reservation):
423
+ # Keyed on a content frame the settlement layer handed to the
424
+ # transport, not on the parser having seen an output item nor on
425
+ # the body having flushed a chunk: a delta-only or completed-only
426
+ # answer that was relayed is charged; an output item still withheld
427
+ # ahead of an unfinished pin write, a vendor event the public
428
+ # contract dropped or a delta parked ahead of ``response.created``
429
+ # is not.
430
+ await self._settle_reservation_step(reservation, self._estimate(), cause="client_cancel")
431
+ return
432
+ await self._release_reservation_step(reservation)
433
+
434
+ def _estimate(self) -> SourceUsage:
435
+ return estimate_settlement_usage(admission_budget=self.admission_budget, delta_chars=self.delta_chars)
436
+
437
+ async def _settle_reservation_step(
438
+ self,
439
+ reservation: ApiKeyUsageReservationData,
440
+ usage: SourceUsage,
441
+ *,
442
+ cause: EstimateCause | None,
443
+ ) -> None:
444
+ settle = self.settle_reservation
445
+ if settle is None:
446
+ logger.error(
447
+ "source_dispatch_missing_settler request_id=%s source_id=%s reservation_id=%s",
448
+ self.request_id,
449
+ self.source.id,
450
+ getattr(reservation, "reservation_id", None),
451
+ )
452
+ await self._release_reservation_step(reservation)
453
+ return
454
+ if cause is not None:
455
+ logger.warning(
456
+ "source_usage_missing_settled_at_estimate cause=%s request_id=%s source_id=%s key_id=%s model=%s "
457
+ "input_tokens=%d output_tokens=%d delta_chars=%d",
458
+ cause,
459
+ self.request_id,
460
+ self.source.id,
461
+ self.api_key.id if self.api_key is not None else None,
462
+ self.model,
463
+ usage.input_tokens,
464
+ usage.output_tokens,
465
+ self.delta_chars,
466
+ )
467
+ _inc(model_source_usage_estimated_total, source_id=self.source.id, cause=cause)
468
+ settled = False
469
+ try:
470
+ settled, _cancellation = await _await_result_deferring_cancellation(
471
+ settle(
472
+ reservation,
473
+ source=self.source,
474
+ model=self.model,
475
+ usage=usage,
476
+ cost_usd_override=source_usage_cost_usd(self.source, self.model, usage),
477
+ ),
478
+ scheduler=self.scheduler,
479
+ )
480
+ except Exception:
481
+ logger.warning(
482
+ "source_dispatch_settlement_raised request_id=%s source_id=%s",
483
+ self.request_id,
484
+ self.source.id,
485
+ exc_info=True,
486
+ )
487
+ await self._release_reservation_step(reservation)
488
+ if not settled:
489
+ self.settlement_failed = True
490
+
491
+ async def _release_reservation_step(self, reservation: ApiKeyUsageReservationData) -> None:
492
+ release = self.release_reservation
493
+ if release is None:
494
+ logger.error(
495
+ "source_dispatch_missing_releaser request_id=%s source_id=%s reservation_id=%s",
496
+ self.request_id,
497
+ self.source.id,
498
+ getattr(reservation, "reservation_id", None),
499
+ )
500
+ return
501
+ try:
502
+ await _await_cleanup_deferring_cancellation(release(reservation), scheduler=self.scheduler)
503
+ except Exception:
504
+ logger.warning(
505
+ "source_dispatch_release_failed request_id=%s source_id=%s",
506
+ self.request_id,
507
+ self.source.id,
508
+ exc_info=True,
509
+ )
510
+ if self.cleanup_scheduler is not None:
511
+ self.cleanup_scheduler._schedule_cancel_safe_cleanup(
512
+ release(reservation),
513
+ action="source_dispatch_release_retry",
514
+ request_id=self.request_id or ensure_request_id(),
515
+ )
516
+
517
+ def release_claims(self) -> None:
518
+ if self._claims_released:
519
+ return
520
+ self._claims_released = True
521
+ try:
522
+ self.claims.release()
523
+ except Exception:
524
+ logger.warning("source_dispatch_claims_release_failed request_id=%s", self.request_id, exc_info=True)
525
+
526
+ def record_result(self, status: DispatchStatus) -> None:
527
+ if self._result_recorded:
528
+ return
529
+ self._result_recorded = True
530
+ _inc(model_source_dispatch_total, kind=DISPATCH_KIND, status=status)
531
+
532
+ async def write_row(
533
+ self,
534
+ *,
535
+ status: DispatchStatus,
536
+ error_code: str | None,
537
+ error_message: str | None,
538
+ usage: SourceUsage | None,
539
+ timings: SourceTimings | None,
540
+ upstream_status_code: int | None,
541
+ ) -> None:
542
+ if self._row_written:
543
+ return
544
+ self._row_written = True
545
+ request = self.request
546
+ headers = request.headers
547
+ holder = self.usage_holder
548
+ proxy_request_id = self.request_id or ensure_request_id()
549
+ source_response_id = self.source_response_id
550
+ if source_response_id is None and holder is not None and holder.response_id:
551
+ source_response_id = holder.response_id
552
+ _useragent, _useragent_group, conversation_id = _request_log_client_fields(headers)
553
+ try:
554
+ async with get_background_session() as session:
555
+ await RequestLogsRepository(session).add_log(
556
+ account_id=None,
557
+ request_id=source_response_id or proxy_request_id,
558
+ archive_request_id=proxy_request_id,
559
+ model_source_id=self.source.id,
560
+ model_source_kind=self.source.kind,
561
+ api_key_id=self.api_key.id if self.api_key is not None else None,
562
+ session_id=_owner_lookup_session_id_from_headers(headers),
563
+ model=self.model,
564
+ input_tokens=usage.input_tokens if usage is not None else None,
565
+ output_tokens=usage.output_tokens if usage is not None else None,
566
+ cached_input_tokens=usage.cached_input_tokens if usage is not None else None,
567
+ cost_usd=source_usage_cost_usd(self.source, self.model, usage),
568
+ latency_ms=timings.latency_ms if timings is not None else None,
569
+ latency_first_token_ms=timings.latency_first_token_ms if timings is not None else None,
570
+ status=status,
571
+ error_code=error_code,
572
+ error_message=error_message,
573
+ upstream_status_code=upstream_status_code,
574
+ transport="http",
575
+ upstream_transport="openai_compatible_http",
576
+ source=REQUEST_LOG_SOURCE,
577
+ requested_service_tier=self.requested_service_tier,
578
+ service_tier=None,
579
+ useragent=headers.get("user-agent"),
580
+ conversation_id=conversation_id,
581
+ client_ip=resolve_request_client_host(request),
582
+ )
583
+ except Exception:
584
+ logger.warning(
585
+ "failed to write source dispatch request log request_id=%s source_id=%s model=%s status=%s",
586
+ proxy_request_id,
587
+ self.source.id,
588
+ self.model,
589
+ status,
590
+ exc_info=True,
591
+ )
592
+
593
+ # -- the latch ----------------------------------------------------------------------
594
+
595
+ async def finish(
596
+ self,
597
+ *,
598
+ status: DispatchStatus,
599
+ error_code: str | None = None,
600
+ error_message: str | None = None,
601
+ usage: SourceUsage | None = None,
602
+ upstream_status_code: int | None = None,
603
+ timings: SourceTimings | None = None,
604
+ ) -> None:
605
+ """``close_source -> settle_or_release -> release_claims + record_result -> write_row``.
606
+
607
+ Idempotent (``finished`` is the latch).
608
+
609
+ Every step is awaited with cancellation deferred and isolated from the
610
+ others: a failing release never skips the row, a failing row write
611
+ never raises. A ``success`` whose settlement fell back to a release is
612
+ recorded as ``error usage_settlement_failed``.
613
+ """
614
+
615
+ if self.finished:
616
+ return
617
+ self.finished = True
618
+ holder = self.observe_stream()
619
+ if usage is None and holder is not None:
620
+ usage = holder.usage
621
+ if timings is None and holder is not None:
622
+ timings = holder.timings
623
+ if upstream_status_code is None and self.stream is not None:
624
+ upstream_status_code = self.stream.upstream_status_code
625
+ try:
626
+ await _await_cleanup_deferring_cancellation(self.close_source(), scheduler=self.scheduler)
627
+ finally:
628
+ try:
629
+ await _await_cleanup_deferring_cancellation(
630
+ self.settle_or_release(status, usage), scheduler=self.scheduler
631
+ )
632
+ finally:
633
+ if self.settlement_failed:
634
+ status = "error"
635
+ error_code = ERROR_USAGE_SETTLEMENT_FAILED
636
+ error_message = "source usage settlement failed"
637
+ try:
638
+ self.release_claims()
639
+ self.record_result(status)
640
+ finally:
641
+ await _await_cleanup_deferring_cancellation(
642
+ self.write_row(
643
+ status=status,
644
+ error_code=error_code,
645
+ error_message=error_message,
646
+ usage=usage,
647
+ timings=timings,
648
+ upstream_status_code=upstream_status_code,
649
+ ),
650
+ scheduler=self.scheduler,
651
+ )
652
+
653
+ async def finish_with_forwarding_error(self, exc: ModelSourceForwardingError) -> None:
654
+ """``finish("error")`` for a pre-body or mid-stream ``ModelSourceForwardingError`` (deadlines counted)."""
655
+
656
+ if exc.timeout_phase is not None and not self.finished:
657
+ _inc(model_source_timeout_total, phase=exc.timeout_phase)
658
+ await self.finish(
659
+ status="error",
660
+ error_code=error_code_from_payload(exc.payload),
661
+ error_message=error_message_from_payload(exc.payload),
662
+ upstream_status_code=exc.upstream_status_code,
663
+ )
664
+
665
+ async def abandon(self, reason: str) -> None:
666
+ """Cancellation-class ``finish()`` for a client that left during the open / before the body."""
667
+
668
+ if self.finished:
669
+ return
670
+ pending_seconds = max(0.0, self.clock.monotonic() - self.sent_at) if self.sent_at else 0.0
671
+ stage = _ABANDON_STAGES.get(reason, "during_open")
672
+ logger.warning(
673
+ "source_dispatch_abandoned stage=%s reason=%s pending_seconds=%.3f frame_seen=%s "
674
+ "request_id=%s source_id=%s",
675
+ stage,
676
+ reason,
677
+ pending_seconds,
678
+ self.first_frame_at is not None,
679
+ self.request_id,
680
+ self.source.id,
681
+ )
682
+ _inc(model_source_dispatch_abandoned_total, stage=stage)
683
+ await self.finish(
684
+ status="cancelled",
685
+ error_code=reason,
686
+ error_message="client left before the model-source response was delivered",
687
+ )
688
+
689
+ async def finalize_transport(self) -> None:
690
+ """``aclose()`` the body, then ``finish("cancelled", "client_disconnected_before_body")`` if unfinished.
691
+
692
+ ``aclose()`` on a started body drives the settlement generator's own
693
+ cleanup (which calls ``finish``); on a never-started body it skips the
694
+ generator entirely, which is why the unconditional check follows.
695
+ """
696
+
697
+ body = self.body
698
+ if body is not None:
699
+ aclose = getattr(body, "aclose", None)
700
+ if aclose is not None:
701
+ try:
702
+ await _await_cleanup_deferring_cancellation(aclose(), scheduler=self.scheduler)
703
+ except Exception:
704
+ logger.warning(
705
+ "source_dispatch_body_close_failed request_id=%s source_id=%s",
706
+ self.request_id,
707
+ self.source.id,
708
+ exc_info=True,
709
+ )
710
+ if not self.finished:
711
+ await self.abandon(ABANDON_CLIENT_DISCONNECTED_BEFORE_BODY)
712
+
713
+
714
+ class TransportOwner(Protocol):
715
+ """Owner of a source transport whose ``finalize_transport()`` a ``SourceStreamingResponse`` always reaches.
716
+
717
+ ``body`` is the outermost iterator handed to Starlette (assigned by the
718
+ response); ``finalize_transport`` closes it and settles whatever a body that
719
+ never started could not settle itself. ``SourceDispatch`` implements it for
720
+ Responses dispatch, ``SourceChatStreamOwner`` for the direct chat route.
721
+ """
722
+
723
+ scheduler: Scheduler
724
+ body: AsyncIterator[Any] | None
725
+
726
+ async def finalize_transport(self) -> None: ...
727
+
728
+
729
+ @dataclass(slots=True)
730
+ class SourceChatStreamOwner:
731
+ """Transport owner for the direct chat-completions stream route, which has no dispatch owner.
732
+
733
+ The route's settlement generator wrapped around ``stream.body`` is the sole
734
+ body iterator: it releases the reservation and writes the row, but only
735
+ once Starlette iterates the body. Between the route returning and
736
+ ``http.response.start`` completing there is one await in which a client
737
+ departure (task cancellation) or a failed first write leaves the generator
738
+ never started, and ``aclose()`` on a never-started async generator skips
739
+ its ``finally`` -- the upstream response, the pooled lease and the
740
+ reservation were held until garbage collection. ``finalize_transport``
741
+ mirrors ``SourceDispatch.finalize_transport``: it closes the body (a
742
+ started body settles itself), then, when the generator never ran, closes
743
+ the source transport directly and runs ``on_abandoned_before_body`` (the
744
+ route's reservation release and ``cancelled`` row). Every await defers
745
+ cancellation; the finalizer is idempotent.
746
+ """
747
+
748
+ stream: SourceChatStream
749
+ # Route-owned: releases the reservation and writes the ``cancelled`` row
750
+ # with ``ABANDON_CLIENT_DISCONNECTED_BEFORE_BODY`` (``api.py`` owns both).
751
+ on_abandoned_before_body: Callable[[], Awaitable[None]]
752
+ scheduler: Scheduler = REAL_SCHEDULER
753
+ # Outermost body iterator handed to ``SourceStreamingResponse`` (assigned by it).
754
+ body: AsyncIterator[bytes] | None = None
755
+ # Set by the settlement generator on entry: from then on it owns the outcome
756
+ # through its own except/finally paths, whether it completes, is cancelled
757
+ # mid-stream or is closed by the finalizer below.
758
+ body_started: bool = False
759
+ # Latch for the abandoned-before-body outcome recorded by the finalizer.
760
+ finished: bool = False
761
+
762
+ async def finalize_transport(self) -> None:
763
+ body = self.body
764
+ if body is not None:
765
+ aclose = getattr(body, "aclose", None)
766
+ if aclose is not None:
767
+ try:
768
+ await _await_cleanup_deferring_cancellation(aclose(), scheduler=self.scheduler)
769
+ except Exception:
770
+ logger.warning("source_chat_stream_body_close_failed", exc_info=True)
771
+ if self.body_started or self.finished:
772
+ return
773
+ self.finished = True
774
+ try:
775
+ await _await_cleanup_deferring_cancellation(self.stream.aclose(), scheduler=self.scheduler)
776
+ except Exception:
777
+ logger.warning("source_chat_stream_close_failed", exc_info=True)
778
+ await _await_cleanup_deferring_cancellation(self.on_abandoned_before_body(), scheduler=self.scheduler)
779
+
780
+
781
+ class SourceStreamingResponse(StreamingResponse):
782
+ """``StreamingResponse`` whose transport exit always reaches ``owner.finalize_transport()``."""
783
+
784
+ def __init__(
785
+ self,
786
+ content: AsyncIterator[str] | AsyncIterator[bytes],
787
+ *,
788
+ owner: TransportOwner,
789
+ media_type: str = "text/event-stream",
790
+ headers: Mapping[str, str] | None = None,
791
+ ) -> None:
792
+ super().__init__(content, media_type=media_type, headers=headers)
793
+ self.owner = owner
794
+ owner.body = content
795
+
796
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
797
+ try:
798
+ await super().__call__(scope, receive, send)
799
+ finally:
800
+ await _await_cleanup_deferring_cancellation(self.owner.finalize_transport(), scheduler=self.owner.scheduler)
801
+
802
+
803
+ async def open_with_disconnect_watch(request: Request, owner: SourceDispatch, coro: Coroutine[Any, Any, T]) -> T:
804
+ """Run the source open while polling ``request.is_disconnected()``; raises ``ClientDisconnectedDuringOpen``.
805
+
806
+ Owned concurrency (CL-5): the open runs as a task the watch always cancels
807
+ and awaits to completion, so a stream that completed into a leaving caller
808
+ is closed by ``abandon()`` (the open coroutine assigns ``owner.stream``
809
+ itself before it completes), and the child's exception never replaces the
810
+ caller's.
811
+ """
812
+
813
+ if not owner.sent_at:
814
+ owner.sent_at = owner.clock.monotonic()
815
+ task = owner.scheduler.create_task(coro)
816
+ consumed = False
817
+ try:
818
+ while True:
819
+ done, _pending = await owner.scheduler.wait({task}, timeout=OPEN_DISCONNECT_POLL_SECONDS)
820
+ if done:
821
+ # Completion observed before the poll is never dropped.
822
+ consumed = True
823
+ return task.result()
824
+ if await request.is_disconnected():
825
+ pending_seconds = max(0.0, owner.clock.monotonic() - owner.sent_at)
826
+ stall = owner.first_frame_at is None and pending_seconds >= STALL_EVIDENCE_SECONDS
827
+ raise ClientDisconnectedDuringOpen(pending_seconds=pending_seconds, stall=stall)
828
+ finally:
829
+ if not task.done():
830
+ task.cancel()
831
+ if not consumed:
832
+ # Always awaited: a child that completed in the disconnect
833
+ # iteration has assigned ``owner.stream`` (closed by ``abandon()``);
834
+ # a child that failed is logged and never replaces our exception.
835
+ try:
836
+ await _await_task_deferring_cancellation(task)
837
+ except asyncio.CancelledError:
838
+ pass
839
+ except Exception:
840
+ logger.warning(
841
+ "model_source_open_failed_after_abandonment request_id=%s source_id=%s",
842
+ owner.request_id,
843
+ owner.source.id,
844
+ exc_info=True,
845
+ )
846
+
847
+
848
+ async def _aclose_best_effort(stream: object, *, scheduler: Scheduler) -> None:
849
+ aclose = getattr(stream, "aclose", None)
850
+ if aclose is None:
851
+ return
852
+ try:
853
+ await _await_cleanup_deferring_cancellation(aclose(), scheduler=scheduler)
854
+ except Exception:
855
+ logger.debug("source stream layer close failed", exc_info=True)
856
+
857
+
858
+ async def settlement_stream(owner: SourceDispatch, wrapped: AsyncIterator[str]) -> AsyncIterator[str]:
859
+ """Outermost body layer: settles/releases on the terminal outcome and calls ``owner.finish()`` in ``finally``.
860
+
861
+ A disconnect surfaces as ``CancelledError`` (task cancellation) or
862
+ ``GeneratorExit`` (``aclose()`` from ``finalize_transport``); both bypass
863
+ ``except Exception`` and are recorded as ``cancelled`` (the settle policy
864
+ decides between release and estimate) unless the client already received a
865
+ terminal: a relayed failure terminal is an ``error``, a relayed success
866
+ terminal a ``success``.
867
+
868
+ Delivery is decided here as well: ``owner.content_delivered`` is set when
869
+ an event frame ``relayed_frame_delivers_content`` classifies as content is
870
+ handed to the transport, so the cancel estimate charges only for frames
871
+ that left the proxy. The body's flush point sits below the public wrapper,
872
+ which drops events outside ``response.*`` / ``error`` under the SDK
873
+ contract, parks ``response.*`` frames ahead of ``response.created`` and
874
+ holds reasoning-summary deltas; a chunk the body flushed may therefore
875
+ never reach the client (the one ``send()`` in flight when the client
876
+ leaves is the residual ambiguity, resolved as delivered).
877
+
878
+ The client-visible terminal is latched from the relayed event frames, not
879
+ read from the last one: a typed event trailing a terminal (a delta the
880
+ source keeps emitting after the wrapper rewrote its ``response.completed``
881
+ into ``response.failed``, a vendor frame after ``response.completed``)
882
+ must not reset the classification, and a relayed failure terminal is
883
+ sticky -- once the client received a failure, nothing that follows turns
884
+ the attempt back into a charged success. The latch costs one
885
+ ``startswith`` and a string slice per ``event:``-framed event; only a raw
886
+ data-only pass-through block (a native-mode terminal, an unparseable
887
+ block) pays a JSON parse.
888
+ """
889
+
890
+ status: DispatchStatus = "success"
891
+ error_code: str | None = None
892
+ error_message: str | None = None
893
+ completed_normally = False
894
+ timeout_phase: TimeoutPhase | None = None
895
+ relayed_kind: str | None = None
896
+ holder = owner.usage_holder
897
+ owner.body_started = True
898
+ try:
899
+ async for chunk in wrapped:
900
+ if _is_event_frame(chunk):
901
+ if relayed_kind not in _FAILURE_TERMINAL_KINDS:
902
+ frame_kind = relayed_terminal_kind(chunk)
903
+ if frame_kind is not None:
904
+ relayed_kind = frame_kind
905
+ if not owner.content_delivered and relayed_frame_delivers_content(chunk):
906
+ owner.content_delivered = True
907
+ yield chunk
908
+ completed_normally = True
909
+ holder = owner.observe_stream()
910
+ if holder is not None:
911
+ if holder.terminal_kind in _FAILURE_TERMINAL_KINDS:
912
+ # The public wrapper relays a failure terminal and ends the
913
+ # stream normally; a limited key must not be charged (not even
914
+ # at the estimate) for an answer the source never produced.
915
+ status = "error"
916
+ error_code = ERROR_MODEL_SOURCE_RESPONSE_FAILED
917
+ error_message = f"source terminated the stream with response.{holder.terminal_kind}"
918
+ elif holder.terminal_kind in _SUCCESS_TERMINAL_KINDS and relayed_kind in _FAILURE_TERMINAL_KINDS:
919
+ # The parser read the source's success terminal, but the public
920
+ # contract rewrote it into ``response.failed`` (``response`` not
921
+ # an object, invalid output items): the client received a
922
+ # failure, so the row and the settlement follow the wire, not
923
+ # the parser's observation.
924
+ status = "error"
925
+ error_code = ERROR_MODEL_SOURCE_RESPONSE_INVALID
926
+ error_message = f"source response.{holder.terminal_kind} was relayed to the client as response.failed"
927
+ elif holder.terminal_kind is None and relayed_kind not in _SUCCESS_TERMINAL_KINDS:
928
+ # The source closed without a terminal the parser could read
929
+ # and the client did not receive a success terminal either (the
930
+ # wrapper synthesized ``response.failed`` or relayed nothing
931
+ # usable): no answer was delivered, so this is an error and the
932
+ # reservation is released. A success terminal that only
933
+ # outgrew the parser's frame cap still reached the client and
934
+ # stays a success (settled at the estimate for a limited key).
935
+ status = "error"
936
+ error_code = ERROR_MODEL_SOURCE_STREAM_TRUNCATED
937
+ error_message = "source ended the stream without a terminal event"
938
+ except (asyncio.CancelledError, GeneratorExit):
939
+ # A client that disconnects right after a relayed failure terminal --
940
+ # Codex tears the stream down on ``response.failed``/``error`` while
941
+ # many sources still send ``[DONE]``/keepalives before their own EOF --
942
+ # received a failure, not a partial answer, so this is an ``error`` that
943
+ # releases the reservation, never a cancel-after-first-item estimate
944
+ # (design §6.4; api-keys "Failure terminal is never charged"). The
945
+ # completed-normally branch already classifies this.
946
+ owner.observe_stream()
947
+ holder = owner.usage_holder
948
+ if (
949
+ holder is not None and holder.terminal_kind in _FAILURE_TERMINAL_KINDS
950
+ ) or relayed_kind in _FAILURE_TERMINAL_KINDS:
951
+ status = "error"
952
+ error_code = ERROR_MODEL_SOURCE_RESPONSE_FAILED
953
+ error_message = "source terminated the stream with a failure terminal"
954
+ elif relayed_kind in _SUCCESS_TERMINAL_KINDS:
955
+ # Symmetric: the client received the success terminal before
956
+ # leaving (Codex tears the stream down on ``response.completed``
957
+ # while the source still sends ``[DONE]``/keepalives), so the whole
958
+ # answer was delivered and this is a success settled like a stream
959
+ # that ran to the source's EOF, never a cancel.
960
+ status = "success"
961
+ else:
962
+ status = "cancelled"
963
+ error_code = CANCELLED_CLIENT_DISCONNECTED
964
+ error_message = "client disconnected before stream completed"
965
+ # ``async for`` does not close the inner layer on an exception; the
966
+ # wrapper's own ``finally`` blocks release everything below it.
967
+ await _aclose_best_effort(wrapped, scheduler=owner.scheduler)
968
+ raise
969
+ except ModelSourceForwardingError as exc:
970
+ status = "error"
971
+ error_code = error_code_from_payload(exc.payload)
972
+ error_message = error_message_from_payload(exc.payload)
973
+ timeout_phase = exc.timeout_phase
974
+ raise
975
+ except Exception as exc:
976
+ status = "error"
977
+ error_code = ERROR_MODEL_SOURCE_STREAM
978
+ error_message = exc.__class__.__name__
979
+ raise
980
+ finally:
981
+ if owner.finished:
982
+ pass
983
+ else:
984
+ if timeout_phase is not None:
985
+ _inc(model_source_timeout_total, phase=timeout_phase)
986
+ owner.observe_stream()
987
+ cancellation = await _await_cleanup_deferring_cancellation(
988
+ owner.finish(
989
+ status=status,
990
+ error_code=error_code,
991
+ error_message=error_message,
992
+ ),
993
+ scheduler=owner.scheduler,
994
+ )
995
+ if completed_normally and cancellation is not None:
996
+ # The client left while the settlement was being committed: the
997
+ # reservation is settled, the row is written; honour the
998
+ # cancellation instead of continuing to a departed transport.
999
+ raise asyncio.CancelledError