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,997 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ import time
6
+ from collections.abc import Iterable
7
+ from dataclasses import dataclass, field, replace
8
+ from fnmatch import fnmatchcase
9
+
10
+ import anyio
11
+
12
+ from app.core.types import JsonValue
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ MODEL_SOURCE_KIND_SUBSCRIPTION = "subscription"
17
+ MODEL_SOURCE_KIND_OPENAI_COMPATIBLE = "openai_compatible"
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class ReasoningLevel:
22
+ effort: str
23
+ description: str
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class UpstreamModel:
28
+ slug: str
29
+ display_name: str
30
+ description: str
31
+ context_window: int
32
+ input_modalities: tuple[str, ...]
33
+ supported_reasoning_levels: tuple[ReasoningLevel, ...]
34
+ default_reasoning_level: str | None
35
+ supports_reasoning_summaries: bool
36
+ support_verbosity: bool
37
+ default_verbosity: str | None
38
+ prefer_websockets: bool
39
+ supports_parallel_tool_calls: bool
40
+ supported_in_api: bool
41
+ minimal_client_version: str | None
42
+ priority: int
43
+ available_in_plans: frozenset[str]
44
+ base_instructions: str = ""
45
+ source_kind: str = MODEL_SOURCE_KIND_SUBSCRIPTION
46
+ source_id: str | None = None
47
+ raw: dict[str, JsonValue] = field(default_factory=dict, hash=False, compare=False)
48
+
49
+
50
+ @dataclass
51
+ class ModelRegistrySnapshot:
52
+ models: dict[str, UpstreamModel]
53
+ model_plans: dict[str, frozenset[str]]
54
+ plan_models: dict[str, frozenset[str]]
55
+ model_service_tier_plans: dict[str, dict[str, frozenset[str]]]
56
+ model_service_tier_accounts: dict[str, dict[str, frozenset[str]]]
57
+ account_plans: dict[str, str]
58
+ fetched_at: float
59
+ model_accounts: dict[str, frozenset[str]] = field(default_factory=dict)
60
+ account_catalogs_authoritative: bool = False
61
+ bootstrap_floor_active: bool = False
62
+ suppressed_model_slugs: frozenset[str] = field(default_factory=frozenset)
63
+
64
+
65
+ @dataclass(slots=True)
66
+ class ModelRegistryExport:
67
+ """Complete registry state as exchanged with the persisted snapshot store.
68
+
69
+ ``snapshot is None`` represents the cleared/bootstrap-floor state.
70
+ """
71
+
72
+ snapshot: ModelRegistrySnapshot | None
73
+ metadata_models: dict[str, UpstreamModel] | None
74
+
75
+
76
+ _BOOTSTRAP_WEBSOCKET_PREFERRED_MODEL_PATTERNS = (
77
+ "gpt-5.6-*",
78
+ "gpt-5.5",
79
+ "gpt-5.5-*",
80
+ "gpt-5.4",
81
+ "gpt-5.4-*",
82
+ )
83
+
84
+ _REASONING_LEVELS_STANDARD = (
85
+ ReasoningLevel(effort="low", description="Low reasoning effort"),
86
+ ReasoningLevel(effort="medium", description="Medium reasoning effort"),
87
+ ReasoningLevel(effort="high", description="High reasoning effort"),
88
+ )
89
+
90
+ _REASONING_LEVELS_EXTENDED = (
91
+ ReasoningLevel(effort="low", description="Low reasoning effort"),
92
+ ReasoningLevel(effort="medium", description="Medium reasoning effort"),
93
+ ReasoningLevel(effort="high", description="High reasoning effort"),
94
+ ReasoningLevel(effort="xhigh", description="Extra high reasoning effort"),
95
+ )
96
+
97
+ _REASONING_LEVELS_MAX = (
98
+ ReasoningLevel(effort="low", description="Fast responses with lighter reasoning"),
99
+ ReasoningLevel(effort="medium", description="Balances speed and reasoning depth for everyday tasks"),
100
+ ReasoningLevel(effort="high", description="Greater reasoning depth for complex problems"),
101
+ ReasoningLevel(effort="xhigh", description="Extra high reasoning depth for complex problems"),
102
+ ReasoningLevel(effort="max", description="Maximum reasoning depth for the hardest problems"),
103
+ )
104
+
105
+ _REASONING_LEVELS_ULTRA = (
106
+ *_REASONING_LEVELS_MAX,
107
+ ReasoningLevel(effort="ultra", description="Maximum reasoning with automatic task delegation"),
108
+ )
109
+
110
+ _BOOTSTRAP_FAST_SERVICE_TIERS: list[JsonValue] = [
111
+ {
112
+ "id": "priority",
113
+ "name": "Fast",
114
+ "description": "1.5x speed, increased usage",
115
+ }
116
+ ]
117
+
118
+ _BOOTSTRAP_AVAILABLE_IN_PLANS = frozenset(
119
+ {
120
+ "plus",
121
+ "pro",
122
+ "prolite",
123
+ "team",
124
+ "business",
125
+ "enterprise",
126
+ "edu",
127
+ "education",
128
+ "k12",
129
+ "go",
130
+ "hc",
131
+ "finserv",
132
+ "free",
133
+ "free_workspace",
134
+ "quorum",
135
+ "self_serve_business_usage_based",
136
+ "enterprise_cbp_usage_based",
137
+ }
138
+ )
139
+
140
+ _BOOTSTRAP_CORE_AVAILABLE_IN_PLANS = frozenset(
141
+ plan for plan in _BOOTSTRAP_AVAILABLE_IN_PLANS if plan not in {"free", "free_workspace", "k12"}
142
+ )
143
+
144
+ # GPT-5.6 ships to four additional plan tiers upstream
145
+ # (codex-rs/models-manager/models.json at rust-v0.145.0).
146
+ _BOOTSTRAP_GPT56_AVAILABLE_IN_PLANS = frozenset(
147
+ {
148
+ *_BOOTSTRAP_AVAILABLE_IN_PLANS,
149
+ "edu_plus",
150
+ "edu_pro",
151
+ "enterprise_cbp_automation",
152
+ "sci",
153
+ }
154
+ )
155
+
156
+ _GPT56_SOL_AVAILABILITY_NUX: dict[str, JsonValue] = {
157
+ "message": (
158
+ "Our most capable model yet. GPT-5.6 Sol can tackle complex code changes, "
159
+ "dig into research, produce polished documents, and take on your most "
160
+ "ambitious work. Sol is highly capable at lower reasoning efforts—try "
161
+ "starting lower, then turn it up for harder jobs."
162
+ )
163
+ }
164
+
165
+
166
+ def _bootstrap_model(
167
+ slug: str,
168
+ display_name: str,
169
+ *,
170
+ prefer_websockets: bool,
171
+ minimal_client_version: str | None,
172
+ description: str | None = None,
173
+ reasoning_levels: tuple[ReasoningLevel, ...] = _REASONING_LEVELS_EXTENDED,
174
+ context_window: int = 272_000,
175
+ input_modalities: tuple[str, ...] = ("text", "image"),
176
+ default_reasoning_level: str | None = "medium",
177
+ default_verbosity: str | None = "low",
178
+ supported_in_api: bool = True,
179
+ available_in_plans: frozenset[str] = _BOOTSTRAP_AVAILABLE_IN_PLANS,
180
+ visibility: str = "list",
181
+ shell_type: str = "shell_command",
182
+ priority: int = 0,
183
+ raw: dict[str, JsonValue] | None = None,
184
+ ) -> UpstreamModel:
185
+ raw_fields: dict[str, JsonValue] = {
186
+ "shell_type": shell_type,
187
+ "visibility": visibility,
188
+ "availability_nux": None,
189
+ "max_context_window": context_window,
190
+ "truncation_policy": {"mode": "tokens", "limit": 10_000},
191
+ "experimental_supported_tools": [],
192
+ }
193
+ if raw:
194
+ raw_fields.update(raw)
195
+ return UpstreamModel(
196
+ slug=slug,
197
+ display_name=display_name,
198
+ description=description or display_name,
199
+ context_window=context_window,
200
+ input_modalities=input_modalities,
201
+ supported_reasoning_levels=reasoning_levels,
202
+ default_reasoning_level=default_reasoning_level,
203
+ supports_reasoning_summaries=True,
204
+ support_verbosity=True,
205
+ default_verbosity=default_verbosity,
206
+ prefer_websockets=prefer_websockets,
207
+ supports_parallel_tool_calls=True,
208
+ supported_in_api=supported_in_api,
209
+ minimal_client_version=minimal_client_version,
210
+ priority=priority,
211
+ available_in_plans=available_in_plans,
212
+ raw=raw_fields,
213
+ )
214
+
215
+
216
+ def _gpt56_raw(
217
+ *,
218
+ multi_agent_version: str,
219
+ availability_nux: dict[str, JsonValue] | None = None,
220
+ ) -> dict[str, JsonValue]:
221
+ """Raw catalog fields for the GPT-5.6 family, mirroring the upstream
222
+ bundled catalog (codex-rs/models-manager/models.json at rust-v0.145.0)
223
+ field-for-field, with one tracked exception: ``max_context_window``, which
224
+ upstream later raised from 272,000 to 872,000 (see the field comment
225
+ below). The ~16.5 KB ``base_instructions`` string and the
226
+ personality-templated ``model_messages`` object are deliberately not
227
+ bundled; the live upstream registry supplies them on the first refresh.
228
+ """
229
+ return {
230
+ "apply_patch_tool_type": "freeform",
231
+ "web_search_tool_type": "text_and_image",
232
+ "supports_image_detail_original": True,
233
+ "truncation_policy": {"mode": "tokens", "limit": 10_000},
234
+ # ``tool_mode`` / ``multi_agent_version`` drive Codex client tool
235
+ # assembly and the ``ultra`` proactive multi-agent mode; omitting them
236
+ # would make a bootstrap-served catalog change client behavior.
237
+ "tool_mode": "code_mode_only",
238
+ "multi_agent_version": multi_agent_version,
239
+ "use_responses_lite": True,
240
+ "include_skills_usage_instructions": False,
241
+ "auto_review_model_override": None,
242
+ # Upstream raised only the ceiling: ``max_context_window`` 272000 ->
243
+ # 872000 with ``context_window`` unchanged at 272000. ``_bootstrap_model``
244
+ # synthesizes ``max_context_window == context_window``, so the family
245
+ # ceiling has to override it here, the same decoupling ``gpt-5.4`` uses.
246
+ # Pinned evidence: openai/codex commit
247
+ # 2eee483e49f88b868f67364134a658b3298e6c14 -- "Raise the GPT-5.6 maximum
248
+ # context window" (openai/codex#39102). Not yet in a ``rust-v*`` release
249
+ # tag; ``rust-v0.148.0-alpha.21`` still ships 272000. Re-pin to the tag
250
+ # once one carries it.
251
+ "max_context_window": 872_000,
252
+ "auto_compact_token_limit": None,
253
+ "comp_hash": "3000",
254
+ "reasoning_summary_format": "experimental",
255
+ "default_reasoning_summary": "none",
256
+ "availability_nux": availability_nux,
257
+ "upgrade": None,
258
+ # Required (no serde default) by the Rust ``ModelInfo`` deserializer;
259
+ # a Codex client rejects catalog entries that omit it.
260
+ "experimental_supported_tools": [],
261
+ "supports_search_tool": True,
262
+ "default_service_tier": None,
263
+ "service_tiers": _BOOTSTRAP_FAST_SERVICE_TIERS,
264
+ "additional_speed_tiers": ["fast"],
265
+ }
266
+
267
+
268
+ # Static bundled fallback models used before the first upstream registry refresh.
269
+ # This mirrors Codex's model-manager pattern: ship a conservative catalog so
270
+ # startup/offline paths have usable metadata, then treat the live upstream
271
+ # registry as authoritative once a refresh succeeds. Keep compatibility fields
272
+ # explicit rather than inherited from helper defaults; every slug must be a
273
+ # real upstream slug (never invented), and live upstream data always takes
274
+ # precedence once available. ``gpt-5.3-codex`` / ``gpt-5.3-codex-spark`` were
275
+ # dropped from upstream's bundled catalog at rust-v0.144.x but are retained
276
+ # here for older pinned clients; the upstream backend still serves them.
277
+ _BOOTSTRAP_STATIC_MODELS: tuple[UpstreamModel, ...] = (
278
+ _bootstrap_model(
279
+ "gpt-5.6-sol",
280
+ "GPT-5.6-Sol",
281
+ description="Latest frontier agentic coding model.",
282
+ prefer_websockets=True,
283
+ minimal_client_version="0.144.0",
284
+ reasoning_levels=_REASONING_LEVELS_ULTRA,
285
+ context_window=272_000,
286
+ default_reasoning_level="low",
287
+ priority=1,
288
+ available_in_plans=_BOOTSTRAP_GPT56_AVAILABLE_IN_PLANS,
289
+ raw=_gpt56_raw(multi_agent_version="v2", availability_nux=_GPT56_SOL_AVAILABILITY_NUX),
290
+ ),
291
+ _bootstrap_model(
292
+ "gpt-5.6-terra",
293
+ "GPT-5.6-Terra",
294
+ description="Balanced agentic coding model for everyday work.",
295
+ prefer_websockets=True,
296
+ minimal_client_version="0.144.0",
297
+ reasoning_levels=_REASONING_LEVELS_ULTRA,
298
+ context_window=272_000,
299
+ default_reasoning_level="medium",
300
+ priority=2,
301
+ available_in_plans=_BOOTSTRAP_GPT56_AVAILABLE_IN_PLANS,
302
+ raw=_gpt56_raw(multi_agent_version="v2"),
303
+ ),
304
+ _bootstrap_model(
305
+ "gpt-5.6-luna",
306
+ "GPT-5.6-Luna",
307
+ description="Fast and affordable agentic coding model.",
308
+ prefer_websockets=True,
309
+ minimal_client_version="0.144.0",
310
+ reasoning_levels=_REASONING_LEVELS_MAX,
311
+ context_window=272_000,
312
+ default_reasoning_level="medium",
313
+ priority=3,
314
+ available_in_plans=_BOOTSTRAP_GPT56_AVAILABLE_IN_PLANS,
315
+ raw=_gpt56_raw(multi_agent_version="v1"),
316
+ ),
317
+ _bootstrap_model(
318
+ "gpt-5.5",
319
+ "GPT-5.5",
320
+ prefer_websockets=True,
321
+ minimal_client_version="0.124.0",
322
+ ),
323
+ _bootstrap_model(
324
+ "gpt-5.4",
325
+ "GPT-5.4",
326
+ prefer_websockets=True,
327
+ minimal_client_version="0.98.0",
328
+ available_in_plans=_BOOTSTRAP_CORE_AVAILABLE_IN_PLANS,
329
+ raw={"max_context_window": 1_000_000},
330
+ ),
331
+ _bootstrap_model(
332
+ "gpt-5.4-mini",
333
+ "GPT-5.4 Mini",
334
+ prefer_websockets=True,
335
+ default_verbosity="medium",
336
+ minimal_client_version="0.98.0",
337
+ ),
338
+ _bootstrap_model(
339
+ "gpt-5.3-codex",
340
+ "GPT-5.3 Codex",
341
+ prefer_websockets=True,
342
+ minimal_client_version="0.98.0",
343
+ available_in_plans=_BOOTSTRAP_CORE_AVAILABLE_IN_PLANS,
344
+ ),
345
+ _bootstrap_model(
346
+ "gpt-5.3-codex-spark",
347
+ "GPT-5.3 Codex Spark",
348
+ prefer_websockets=True,
349
+ context_window=128_000,
350
+ input_modalities=("text",),
351
+ default_reasoning_level="high",
352
+ minimal_client_version="0.100.0",
353
+ ),
354
+ _bootstrap_model(
355
+ "gpt-5.2",
356
+ "GPT-5.2",
357
+ prefer_websockets=True,
358
+ minimal_client_version="0.0.1",
359
+ raw={"truncation_policy": {"mode": "bytes", "limit": 10_000}},
360
+ ),
361
+ _bootstrap_model(
362
+ "codex-auto-review",
363
+ "Codex Auto Review",
364
+ prefer_websockets=True,
365
+ minimal_client_version="0.98.0",
366
+ available_in_plans=_BOOTSTRAP_CORE_AVAILABLE_IN_PLANS,
367
+ visibility="hide",
368
+ raw={"max_context_window": 1_000_000},
369
+ ),
370
+ )
371
+
372
+
373
+ # Speed/service-tier metadata must aggregate (union) when the same slug is
374
+ # fetched from multiple accounts/plans, rather than be overwritten
375
+ # last-writer-wins. Otherwise a single account without Fast entitlement returns
376
+ # an empty tier list and erases Fast from the shared catalog for every account
377
+ # (issue #1100).
378
+ _SERVICE_TIER_OBJECT_KEY_FIELDS = ("slug", "name", "id", "tier")
379
+
380
+
381
+ def _union_string_tiers(primary: JsonValue, secondary: JsonValue) -> list[JsonValue] | None:
382
+ merged: list[JsonValue] = []
383
+ seen: set[str] = set()
384
+ for source in (primary, secondary):
385
+ if isinstance(source, list):
386
+ for item in source:
387
+ if not isinstance(item, str):
388
+ continue
389
+ identity = canonical_service_tier_value(item)
390
+ if identity not in seen:
391
+ seen.add(identity)
392
+ merged.append(item)
393
+ return merged or None
394
+
395
+
396
+ def _service_tier_identity(entry: JsonValue) -> str:
397
+ if isinstance(entry, dict):
398
+ identities: list[str] = []
399
+ for key in _SERVICE_TIER_OBJECT_KEY_FIELDS:
400
+ value = entry.get(key)
401
+ if isinstance(value, str) and value:
402
+ canonical = canonical_service_tier_value(value)
403
+ if canonical:
404
+ identities.append(canonical)
405
+ if "priority" in identities:
406
+ return "tier:priority"
407
+ if identities:
408
+ return f"tier:{identities[0]}"
409
+ return json.dumps(entry, sort_keys=True, default=str)
410
+
411
+
412
+ def canonical_service_tier_value(value: str) -> str:
413
+ canonical = value.strip().lower()
414
+ if canonical in {"fast", "priority"}:
415
+ return "priority"
416
+ return canonical
417
+
418
+
419
+ def _union_object_tiers(primary: JsonValue, secondary: JsonValue) -> list[JsonValue] | None:
420
+ merged: list[JsonValue] = []
421
+ seen: set[str] = set()
422
+ for source in (primary, secondary):
423
+ if isinstance(source, list):
424
+ for item in source:
425
+ identity = _service_tier_identity(item)
426
+ if identity not in seen:
427
+ seen.add(identity)
428
+ merged.append(item)
429
+ return merged or None
430
+
431
+
432
+ def _merge_default_service_tier(primary: JsonValue, secondary: JsonValue) -> str | None:
433
+ if not isinstance(primary, str) or not primary:
434
+ return secondary if isinstance(secondary, str) and secondary else None
435
+ if not isinstance(secondary, str) or not secondary:
436
+ return primary
437
+ if primary == secondary:
438
+ return primary
439
+
440
+ plain_defaults = {"auto", "default"}
441
+ if primary in plain_defaults and secondary not in plain_defaults:
442
+ return secondary
443
+ return primary
444
+
445
+
446
+ def _model_service_tier_keys(model: UpstreamModel) -> frozenset[str]:
447
+ tiers: set[str] = set()
448
+
449
+ def add_tier(value: str) -> None:
450
+ tiers.add(value)
451
+ canonical = canonical_service_tier_value(value)
452
+ if canonical:
453
+ tiers.add(canonical)
454
+
455
+ additional_speed_tiers = model.raw.get("additional_speed_tiers")
456
+ if isinstance(additional_speed_tiers, list):
457
+ for item in additional_speed_tiers:
458
+ if isinstance(item, str) and item:
459
+ add_tier(item)
460
+
461
+ service_tiers = model.raw.get("service_tiers")
462
+ if isinstance(service_tiers, list):
463
+ for item in service_tiers:
464
+ if not isinstance(item, dict):
465
+ continue
466
+ for key in _SERVICE_TIER_OBJECT_KEY_FIELDS:
467
+ value = item.get(key)
468
+ if isinstance(value, str) and value:
469
+ add_tier(value)
470
+
471
+ default_service_tier = model.raw.get("default_service_tier")
472
+ if isinstance(default_service_tier, str) and default_service_tier:
473
+ add_tier(default_service_tier)
474
+
475
+ return frozenset(tiers)
476
+
477
+
478
+ def _merge_service_tier_metadata(existing: UpstreamModel, incoming: UpstreamModel) -> UpstreamModel:
479
+ """Combine two same-slug models so the speed/service-tier metadata is the
480
+ union of both. ``incoming`` stays the base (last-writer-wins for every other
481
+ field); only the tier fields aggregate, so an account without Fast cannot
482
+ strip Fast contributed by another account (issue #1100)."""
483
+ merged_raw = dict(incoming.raw)
484
+
485
+ speed_tiers = _union_string_tiers(
486
+ incoming.raw.get("additional_speed_tiers"),
487
+ existing.raw.get("additional_speed_tiers"),
488
+ )
489
+ if speed_tiers is not None:
490
+ merged_raw["additional_speed_tiers"] = speed_tiers
491
+
492
+ service_tiers = _union_object_tiers(
493
+ incoming.raw.get("service_tiers"),
494
+ existing.raw.get("service_tiers"),
495
+ )
496
+ if service_tiers is not None:
497
+ merged_raw["service_tiers"] = service_tiers
498
+
499
+ default_service_tier = _merge_default_service_tier(
500
+ incoming.raw.get("default_service_tier"),
501
+ existing.raw.get("default_service_tier"),
502
+ )
503
+ if default_service_tier is not None:
504
+ merged_raw["default_service_tier"] = default_service_tier
505
+
506
+ if merged_raw == incoming.raw:
507
+ return incoming
508
+ return replace(incoming, raw=merged_raw)
509
+
510
+
511
+ class ModelRegistry:
512
+ def __init__(self, *, ttl_seconds: float = 300.0) -> None:
513
+ if ttl_seconds <= 0:
514
+ raise ValueError("ttl_seconds must be positive")
515
+ self._ttl_seconds = ttl_seconds
516
+ self._snapshot: ModelRegistrySnapshot | None = None
517
+ self._bootstrap_models: dict[str, UpstreamModel] = {m.slug: m for m in _BOOTSTRAP_STATIC_MODELS}
518
+ self._metadata_models: dict[str, UpstreamModel] | None = None
519
+ self._applied_content_hash: str | None = None
520
+ self._lock = anyio.Lock()
521
+
522
+ @property
523
+ def applied_content_hash(self) -> str | None:
524
+ """Content hash of the persisted snapshot this registry last applied or persisted."""
525
+ return self._applied_content_hash
526
+
527
+ def note_applied_content_hash(self, content_hash: str | None) -> None:
528
+ self._applied_content_hash = content_hash
529
+
530
+ async def export_state(self) -> ModelRegistryExport:
531
+ async with self._lock:
532
+ return ModelRegistryExport(snapshot=self._snapshot, metadata_models=self._metadata_models)
533
+
534
+ async def import_state(self, state: ModelRegistryExport, *, content_hash: str) -> None:
535
+ """Replace the registry state with a decoded persisted snapshot."""
536
+ async with self._lock:
537
+ self._snapshot = state.snapshot
538
+ self._metadata_models = state.metadata_models
539
+ self._applied_content_hash = content_hash
540
+
541
+ def get_snapshot(self) -> ModelRegistrySnapshot | None:
542
+ return self._snapshot
543
+
544
+ def get_models_with_fallback(self) -> dict[str, UpstreamModel]:
545
+ snapshot = self._snapshot
546
+ if snapshot is not None:
547
+ if snapshot.bootstrap_floor_active:
548
+ models = dict(self._bootstrap_models)
549
+ for slug in snapshot.suppressed_model_slugs:
550
+ models.pop(slug, None)
551
+ models.update(snapshot.models)
552
+ return models
553
+ return snapshot.models
554
+ return self._bootstrap_models
555
+
556
+ def get_models_for_metadata(self) -> dict[str, UpstreamModel]:
557
+ if self._metadata_models is not None:
558
+ return self._metadata_models
559
+ return self._bootstrap_models
560
+
561
+ def plan_types_for_model(self, slug: str) -> frozenset[str] | None:
562
+ normalized_slug = slug.strip().lower()
563
+ bootstrap_model = self._bootstrap_models.get(slug) or self._bootstrap_models.get(normalized_slug)
564
+ if self._snapshot is None:
565
+ return bootstrap_model.available_in_plans if bootstrap_model is not None else None
566
+ if slug in self._snapshot.model_plans:
567
+ return self._snapshot.model_plans[slug]
568
+ if normalized_slug in self._snapshot.model_plans:
569
+ return self._snapshot.model_plans[normalized_slug]
570
+ if (
571
+ self._snapshot.bootstrap_floor_active
572
+ and bootstrap_model is not None
573
+ and normalized_slug not in self._snapshot.suppressed_model_slugs
574
+ ):
575
+ return bootstrap_model.available_in_plans
576
+ snapshot_plans = frozenset()
577
+ return snapshot_plans
578
+
579
+ def is_suppressed_model(self, slug: str) -> bool:
580
+ """Whether catalog evidence explicitly withdrew a previously known slug.
581
+
582
+ This differs from an operator-mapped slug simply absent from catalog
583
+ discovery: that slug keeps the existing unfiltered fallback, whereas a
584
+ suppressed model must not select any account.
585
+ """
586
+ snapshot = self._snapshot
587
+ if snapshot is None:
588
+ return False
589
+ return slug.strip().lower() in snapshot.suppressed_model_slugs
590
+
591
+ def plan_types_for_model_service_tier(self, slug: str, service_tier: str | None) -> frozenset[str] | None:
592
+ if service_tier is None:
593
+ return self.plan_types_for_model(slug)
594
+ normalized_slug = slug.strip().lower()
595
+ normalized_service_tier = canonical_service_tier_value(service_tier)
596
+ if not normalized_slug or not normalized_service_tier:
597
+ return self.plan_types_for_model(slug)
598
+
599
+ if self._snapshot is None:
600
+ return self.plan_types_for_model(slug)
601
+ if not self._snapshot.account_catalogs_authoritative and self._snapshot.account_plans:
602
+ return self.plan_types_for_model(slug)
603
+
604
+ tier_plans = self._snapshot.model_service_tier_plans.get(slug) or self._snapshot.model_service_tier_plans.get(
605
+ normalized_slug
606
+ )
607
+ if tier_plans is None:
608
+ return frozenset() if self._snapshot.account_catalogs_authoritative else self.plan_types_for_model(slug)
609
+ return tier_plans.get(normalized_service_tier, frozenset())
610
+
611
+ def account_ids_for_model_service_tier(self, slug: str, service_tier: str | None) -> frozenset[str] | None:
612
+ if service_tier is None or self._snapshot is None:
613
+ return None
614
+ if not self._snapshot.account_catalogs_authoritative:
615
+ return None
616
+ normalized_slug = slug.strip().lower()
617
+ normalized_service_tier = canonical_service_tier_value(service_tier)
618
+ if not normalized_slug or not normalized_service_tier:
619
+ return None
620
+
621
+ tier_accounts = self._snapshot.model_service_tier_accounts.get(
622
+ slug
623
+ ) or self._snapshot.model_service_tier_accounts.get(normalized_slug)
624
+ if tier_accounts is None:
625
+ if self._snapshot.account_catalogs_authoritative:
626
+ return frozenset()
627
+ return None
628
+ return tier_accounts.get(normalized_service_tier, frozenset())
629
+
630
+ def model_advertises_service_tier(self, slug: str, service_tier: str | None) -> bool:
631
+ """Report whether the catalog lists ``service_tier`` for ``slug`` at all.
632
+
633
+ This is deliberately distinct from "no account carries the tier": a model
634
+ that never advertises the tier cannot be routed at it by any account, so
635
+ an enforced tier must not be treated as an account-eligibility filter for
636
+ that model. Returns ``True`` whenever the answer is unknown (no snapshot,
637
+ non-authoritative catalogs, unusable slug/tier) so callers keep their
638
+ existing behavior until the catalog can actually answer.
639
+ """
640
+ if service_tier is None or self._snapshot is None:
641
+ return True
642
+ if not self._snapshot.account_catalogs_authoritative:
643
+ return True
644
+ normalized_slug = slug.strip().lower()
645
+ normalized_service_tier = canonical_service_tier_value(service_tier)
646
+ if not normalized_slug or not normalized_service_tier:
647
+ return True
648
+
649
+ model_is_known = (
650
+ slug in self._snapshot.model_plans
651
+ or normalized_slug in self._snapshot.model_plans
652
+ or slug in self._snapshot.model_accounts
653
+ or normalized_slug in self._snapshot.model_accounts
654
+ )
655
+ if not model_is_known:
656
+ # An authoritative subscription snapshot cannot describe the tier
657
+ # capabilities of an operator-mapped or source-routed model that it
658
+ # does not contain. Keep the enforced tier when model identity is
659
+ # unknown instead of broadening fallback beyond catalog evidence.
660
+ return True
661
+
662
+ tier_accounts = self._snapshot.model_service_tier_accounts.get(
663
+ slug
664
+ ) or self._snapshot.model_service_tier_accounts.get(normalized_slug)
665
+ tier_plans = self._snapshot.model_service_tier_plans.get(slug) or self._snapshot.model_service_tier_plans.get(
666
+ normalized_slug
667
+ )
668
+ if tier_accounts is None and tier_plans is None:
669
+ return False
670
+ return normalized_service_tier in (tier_accounts or {}) or normalized_service_tier in (tier_plans or {})
671
+
672
+ def account_ids_for_model(self, slug: str) -> frozenset[str] | None:
673
+ """Return exact supporting accounts, or ``None`` while coverage is incomplete."""
674
+ if self._snapshot is None or not self._snapshot.account_catalogs_authoritative:
675
+ return None
676
+ normalized_slug = slug.strip().lower()
677
+ if not normalized_slug:
678
+ return None
679
+ return self._snapshot.model_accounts.get(slug) or self._snapshot.model_accounts.get(
680
+ normalized_slug, frozenset()
681
+ )
682
+
683
+ def prefers_websockets(self, slug: str | None) -> bool:
684
+ if not isinstance(slug, str):
685
+ return False
686
+ normalized_slug = slug.strip().lower()
687
+ if not normalized_slug:
688
+ return False
689
+
690
+ if self._snapshot is not None:
691
+ model = self._snapshot.models.get(slug) or self._snapshot.models.get(normalized_slug)
692
+ if model is not None:
693
+ return model.prefer_websockets
694
+ if self._snapshot.bootstrap_floor_active and normalized_slug not in self._snapshot.suppressed_model_slugs:
695
+ bootstrap_model = self._bootstrap_models.get(slug) or self._bootstrap_models.get(normalized_slug)
696
+ if bootstrap_model is not None:
697
+ return bootstrap_model.prefer_websockets
698
+ return False
699
+
700
+ bootstrap_model = self._bootstrap_models.get(slug) or self._bootstrap_models.get(normalized_slug)
701
+ if bootstrap_model is not None:
702
+ return bootstrap_model.prefer_websockets
703
+
704
+ return any(fnmatchcase(normalized_slug, pattern) for pattern in _BOOTSTRAP_WEBSOCKET_PREFERRED_MODEL_PATTERNS)
705
+
706
+ def needs_refresh(self) -> bool:
707
+ if self._snapshot is None:
708
+ return True
709
+ return (time.monotonic() - self._snapshot.fetched_at) >= self._ttl_seconds
710
+
711
+ async def clear(self) -> None:
712
+ """Drop live capability state when no active accounts remain.
713
+
714
+ This resets to the bootstrap-floor state (equivalent to never having
715
+ refreshed): every reader falls back to the static bootstrap catalog. We
716
+ must NOT publish an authoritative-empty snapshot here. If we did, then in
717
+ the window after an account is added but before the next scheduled refresh
718
+ tick, ``plan_types_for_model`` / ``get_models_with_fallback`` would report
719
+ even canonical models as absent (because ``_snapshot`` is no longer
720
+ ``None``), so ``_mapped_model_has_registry_entry`` would skip model/plan
721
+ filtering and let an unsupported plan be selected, and ``/v1/models`` would
722
+ stay empty until the timer fires. Bootstrap remains the discovery/gating
723
+ floor whenever there is no authoritative account coverage.
724
+ """
725
+ async with self._lock:
726
+ self._snapshot = None
727
+ self._metadata_models = None
728
+ self._applied_content_hash = None
729
+
730
+ async def update(
731
+ self,
732
+ per_plan_results: dict[str, list[UpstreamModel]],
733
+ *,
734
+ per_account_results: dict[str, tuple[str, list[UpstreamModel]]] | None = None,
735
+ active_account_plans: dict[str, str] | None = None,
736
+ ) -> None:
737
+ if not per_plan_results:
738
+ logger.warning("Model registry refresh produced no plan results; keeping cached snapshot")
739
+ return
740
+
741
+ async with self._lock:
742
+ previous = self._snapshot
743
+ try:
744
+ models: dict[str, UpstreamModel] = {}
745
+ model_plans: dict[str, set[str]] = {}
746
+ model_service_tier_plans: dict[str, dict[str, set[str]]] = {}
747
+ model_service_tier_accounts: dict[str, dict[str, set[str]]] = {}
748
+ model_accounts: dict[str, set[str]] = {}
749
+ account_plans: dict[str, str] = {}
750
+ suppressed_model_slugs: set[str] = (
751
+ set(previous.suppressed_model_slugs) if previous is not None else set()
752
+ )
753
+
754
+ # Carry over data from plans not present in per_plan_results
755
+ if previous is not None:
756
+ previous_plans = set(previous.plan_models.keys())
757
+ refreshed_plans = set(per_plan_results.keys())
758
+ stale_plans = previous_plans - refreshed_plans
759
+ if active_account_plans is not None:
760
+ stale_plans.intersection_update(active_account_plans.values())
761
+ stale_account_ids = {
762
+ account_id
763
+ for account_id, plan_type in previous.account_plans.items()
764
+ if account_id in active_account_plans
765
+ and plan_type in stale_plans
766
+ and active_account_plans[account_id] == plan_type
767
+ }
768
+ refreshed_account_ids = set(per_account_results or {})
769
+ stale_account_ids.update(
770
+ account_id
771
+ for account_id, previous_plan_type in previous.account_plans.items()
772
+ if account_id in active_account_plans
773
+ and account_id not in refreshed_account_ids
774
+ # A retained catalog proves capabilities only for the
775
+ # plan that produced it. A plan change without a fresh
776
+ # catalog leaves the active account unknown instead of
777
+ # re-labeling old entitlements as new-plan support.
778
+ and active_account_plans[account_id] == previous_plan_type
779
+ )
780
+ else:
781
+ stale_account_ids = {
782
+ account_id
783
+ for account_id, plan_type in previous.account_plans.items()
784
+ if plan_type in stale_plans
785
+ }
786
+
787
+ # Carryover invariant: never carry forward a stale-plan model
788
+ # that no currently-active account of that plan advertises, per
789
+ # the last-known per-account catalogs in ``previous.model_accounts``.
790
+ #
791
+ # This runs whenever we know the active account set
792
+ # (``active_account_plans`` is not None), REGARDLESS of whether the
793
+ # previous snapshot was authoritative. The authoritative flag governs
794
+ # whether per-account *routing* is trusted (see ``account_ids_for_model``),
795
+ # not whether a dead model is dropped from discovery: a first-refresh
796
+ # (non-authoritative) snapshot can still record that a private model
797
+ # was advertised only by an account that is now removed/paused, and
798
+ # that model must not linger in discovery once no active account serves it.
799
+ #
800
+ # An active account whose refresh transiently failed keeps its
801
+ # last-known models (it is in ``stale_account_ids`` and still appears
802
+ # in ``previous.model_accounts``). A model with NO per-account
803
+ # provenance at all (an older/plan-only snapshot that never captured
804
+ # per-account catalogs) is preserved rather than dropped, so we
805
+ # degrade safe when we cannot attribute a model to any account.
806
+ supported_stale_slugs: dict[str, set[str]] | None = None
807
+ if active_account_plans is not None:
808
+ supported_stale_slugs = {}
809
+ for slug, account_ids in previous.model_accounts.items():
810
+ for account_id in account_ids:
811
+ if account_id not in stale_account_ids:
812
+ continue
813
+ plan_of_account = active_account_plans.get(
814
+ account_id, previous.account_plans.get(account_id)
815
+ )
816
+ if plan_of_account in stale_plans:
817
+ supported_stale_slugs.setdefault(plan_of_account, set()).add(slug)
818
+
819
+ for plan_type in stale_plans:
820
+ stale_slugs: Iterable[str] = previous.plan_models.get(plan_type, frozenset())
821
+ if supported_stale_slugs is not None:
822
+ supported = supported_stale_slugs.get(plan_type, set())
823
+ stale_slugs = [
824
+ slug
825
+ for slug in stale_slugs
826
+ # Unknown per-account provenance -> keep (degrade safe).
827
+ if not previous.model_accounts.get(slug)
828
+ # Known provenance -> keep only with an active advertiser.
829
+ or slug in supported
830
+ ]
831
+ for slug in stale_slugs:
832
+ if slug not in models and slug in previous.models:
833
+ models[slug] = previous.models[slug]
834
+ model_plans.setdefault(slug, set()).add(plan_type)
835
+ stale_tier_plans = previous.model_service_tier_plans.get(slug, {})
836
+ for service_tier, plans in stale_tier_plans.items():
837
+ if plan_type in plans:
838
+ model_service_tier_plans.setdefault(slug, {}).setdefault(service_tier, set()).add(
839
+ plan_type
840
+ )
841
+ for account_id in stale_account_ids:
842
+ plan_type = (
843
+ active_account_plans.get(account_id)
844
+ if active_account_plans is not None
845
+ else previous.account_plans.get(account_id)
846
+ )
847
+ if plan_type is not None:
848
+ account_plans[account_id] = plan_type
849
+ for slug, account_ids in previous.model_accounts.items():
850
+ if slug not in previous.models:
851
+ continue
852
+ stale_model_accounts = account_ids & stale_account_ids
853
+ if not stale_model_accounts:
854
+ continue
855
+ model_accounts.setdefault(slug, set()).update(stale_model_accounts)
856
+ if slug in previous.models:
857
+ models.setdefault(slug, previous.models[slug])
858
+ for account_id in stale_model_accounts:
859
+ plan_type = account_plans.get(account_id)
860
+ if plan_type is not None:
861
+ model_plans.setdefault(slug, set()).add(plan_type)
862
+ for slug, tier_accounts in previous.model_service_tier_accounts.items():
863
+ if slug not in previous.models:
864
+ continue
865
+ for service_tier, account_ids in tier_accounts.items():
866
+ stale_tier_accounts = account_ids & stale_account_ids
867
+ if stale_tier_accounts:
868
+ model_service_tier_accounts.setdefault(slug, {}).setdefault(service_tier, set()).update(
869
+ stale_tier_accounts
870
+ )
871
+ for account_id in stale_tier_accounts:
872
+ plan_type = account_plans.get(account_id)
873
+ if plan_type is not None:
874
+ model_service_tier_plans.setdefault(slug, {}).setdefault(
875
+ service_tier, set()
876
+ ).add(plan_type)
877
+
878
+ # Merge newly fetched results, aggregating service-tier metadata
879
+ # across plans refreshed in this pass. Stale plan slugs still map
880
+ # to the model, but their previous global union must not re-add a
881
+ # tier that no current per-plan source has (issue #1106).
882
+ refreshed_model_slugs: set[str] = set()
883
+ for plan_type, plan_models_list in per_plan_results.items():
884
+ for model in plan_models_list:
885
+ existing = models.get(model.slug)
886
+ if existing is not None and model.slug in refreshed_model_slugs:
887
+ models[model.slug] = _merge_service_tier_metadata(existing, model)
888
+ else:
889
+ models[model.slug] = model
890
+ refreshed_model_slugs.add(model.slug)
891
+ model_plans.setdefault(model.slug, set()).add(plan_type)
892
+ for service_tier in _model_service_tier_keys(model):
893
+ model_service_tier_plans.setdefault(model.slug, {}).setdefault(service_tier, set()).add(
894
+ plan_type
895
+ )
896
+ if per_account_results is not None:
897
+ for account_id, (plan_type, account_models) in per_account_results.items():
898
+ account_plans[account_id] = plan_type
899
+ for model in account_models:
900
+ if model.slug not in models:
901
+ continue
902
+ model_accounts.setdefault(model.slug, set()).add(account_id)
903
+ for service_tier in _model_service_tier_keys(model):
904
+ model_service_tier_accounts.setdefault(model.slug, {}).setdefault(
905
+ service_tier, set()
906
+ ).add(account_id)
907
+
908
+ frozen_model_plans: dict[str, frozenset[str]] = {
909
+ slug: frozenset(plans) for slug, plans in model_plans.items()
910
+ }
911
+ frozen_model_service_tier_plans: dict[str, dict[str, frozenset[str]]] = {
912
+ slug: {service_tier: frozenset(plans) for service_tier, plans in tier_plans.items()}
913
+ for slug, tier_plans in model_service_tier_plans.items()
914
+ }
915
+ frozen_model_service_tier_accounts: dict[str, dict[str, frozenset[str]]] = {
916
+ slug: {service_tier: frozenset(account_ids) for service_tier, account_ids in tier_accounts.items()}
917
+ for slug, tier_accounts in model_service_tier_accounts.items()
918
+ }
919
+ frozen_model_accounts = {slug: frozenset(account_ids) for slug, account_ids in model_accounts.items()}
920
+ authoritative_account_catalogs = per_account_results is not None and set(
921
+ active_account_plans or per_account_results
922
+ ).issubset(account_plans)
923
+
924
+ if authoritative_account_catalogs:
925
+ for slug in self._bootstrap_models:
926
+ if slug not in models:
927
+ suppressed_model_slugs.add(slug)
928
+ for _plan_type, account_models in (per_account_results or {}).values():
929
+ for model in account_models:
930
+ if model.slug not in models:
931
+ suppressed_model_slugs.add(model.slug)
932
+ if previous is not None:
933
+ for slug, previous_account_ids in previous.model_accounts.items():
934
+ if slug in models or not previous_account_ids:
935
+ continue
936
+ suppressed_model_slugs.add(slug)
937
+ for slug in tuple(suppressed_model_slugs):
938
+ if slug in models:
939
+ suppressed_model_slugs.discard(slug)
940
+
941
+ # Build reverse index: plan_type -> set of slugs
942
+ plan_models_index: dict[str, set[str]] = {}
943
+ for slug, plans in frozen_model_plans.items():
944
+ for plan_type in plans:
945
+ plan_models_index.setdefault(plan_type, set()).add(slug)
946
+
947
+ frozen_plan_models: dict[str, frozenset[str]] = {
948
+ plan_type: frozenset(slugs) for plan_type, slugs in plan_models_index.items()
949
+ }
950
+ bootstrap_floor_active = per_account_results is not None and not authoritative_account_catalogs
951
+
952
+ metadata_models = {
953
+ slug: model
954
+ for slug, model in (self._metadata_models or self._bootstrap_models).items()
955
+ if slug in self._bootstrap_models
956
+ }
957
+ if per_account_results is not None:
958
+ account_metadata_models: dict[str, UpstreamModel] = {}
959
+ for _account_id, (_plan_type, account_models) in per_account_results.items():
960
+ for model in account_models:
961
+ if model.slug in self._bootstrap_models:
962
+ account_metadata_models.setdefault(model.slug, model)
963
+ metadata_models.update(account_metadata_models)
964
+ metadata_models.update(models)
965
+ self._snapshot = ModelRegistrySnapshot(
966
+ models=models,
967
+ model_plans=frozen_model_plans,
968
+ plan_models=frozen_plan_models,
969
+ model_service_tier_plans=frozen_model_service_tier_plans,
970
+ model_service_tier_accounts=frozen_model_service_tier_accounts,
971
+ account_plans=account_plans,
972
+ fetched_at=time.monotonic(),
973
+ model_accounts=frozen_model_accounts,
974
+ account_catalogs_authoritative=authoritative_account_catalogs,
975
+ bootstrap_floor_active=bootstrap_floor_active,
976
+ suppressed_model_slugs=frozenset(suppressed_model_slugs),
977
+ )
978
+ self._metadata_models = metadata_models
979
+ except Exception:
980
+ self._snapshot = previous
981
+ logger.warning("Model registry refresh failed; keeping cached snapshot", exc_info=True)
982
+ raise
983
+
984
+
985
+ _model_registry = ModelRegistry()
986
+
987
+
988
+ def get_model_registry() -> ModelRegistry:
989
+ return _model_registry
990
+
991
+
992
+ def is_public_model(model: UpstreamModel, allowed_models: set[str] | None) -> bool:
993
+ if not model.supported_in_api:
994
+ return False
995
+ if allowed_models is None:
996
+ return True
997
+ return model.slug in allowed_models