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.
- app/__init__.py +10 -0
- app/admin_cli.py +494 -0
- app/cli.py +339 -0
- app/codex_sessions_retag.py +487 -0
- app/core/__init__.py +0 -0
- app/core/audit/__init__.py +0 -0
- app/core/audit/service.py +216 -0
- app/core/audit/types.py +94 -0
- app/core/auth/__init__.py +236 -0
- app/core/auth/api_key_cache.py +65 -0
- app/core/auth/dashboard_access.py +429 -0
- app/core/auth/dashboard_mode.py +171 -0
- app/core/auth/dashboard_session_ttl.py +74 -0
- app/core/auth/dashboard_users_cache.py +120 -0
- app/core/auth/dependencies.py +710 -0
- app/core/auth/external_identity.py +78 -0
- app/core/auth/guardian.py +374 -0
- app/core/auth/models.py +49 -0
- app/core/auth/providers/__init__.py +129 -0
- app/core/auth/providers/oidc.py +838 -0
- app/core/auth/providers/registry.py +118 -0
- app/core/auth/refresh.py +394 -0
- app/core/auth/step_up.py +117 -0
- app/core/auth/totp.py +81 -0
- app/core/balancer/__init__.py +71 -0
- app/core/balancer/logic.py +1612 -0
- app/core/balancer/rendezvous_hash.py +21 -0
- app/core/balancer/types.py +21 -0
- app/core/bootstrap.py +172 -0
- app/core/cache/invalidation.py +426 -0
- app/core/clients/__init__.py +0 -0
- app/core/clients/codex.py +996 -0
- app/core/clients/codex_version.py +187 -0
- app/core/clients/codex_version_snapshot.py +2 -0
- app/core/clients/files.py +534 -0
- app/core/clients/headers.py +29 -0
- app/core/clients/http.py +511 -0
- app/core/clients/model_fetcher.py +276 -0
- app/core/clients/native_egress.py +1304 -0
- app/core/clients/oauth.py +477 -0
- app/core/clients/proxy.py +6089 -0
- app/core/clients/proxy_websocket.py +1411 -0
- app/core/clients/rate_limit_reset_credits.py +412 -0
- app/core/clients/stream_errors.py +12 -0
- app/core/clients/thread_cache_identity.py +347 -0
- app/core/clients/usage.py +467 -0
- app/core/clock.py +170 -0
- app/core/config/__init__.py +0 -0
- app/core/config/background_jobs.py +73 -0
- app/core/config/context_window_overrides.py +166 -0
- app/core/config/dashboard_overrides.py +145 -0
- app/core/config/inheritable.py +47 -0
- app/core/config/key_fingerprint.py +115 -0
- app/core/config/settings.py +872 -0
- app/core/config/settings_cache.py +100 -0
- app/core/config/spool_retention.py +217 -0
- app/core/config/tiers.py +174 -0
- app/core/conversation_archive.py +568 -0
- app/core/crypto.py +37 -0
- app/core/draining.py +11 -0
- app/core/errors.py +365 -0
- app/core/exceptions.py +155 -0
- app/core/handlers/__init__.py +3 -0
- app/core/handlers/exceptions.py +478 -0
- app/core/http_protocol.py +176 -0
- app/core/http_protocol_httptools.py +204 -0
- app/core/ingress_limits.py +17 -0
- app/core/metadata_store.py +18 -0
- app/core/metrics/__init__.py +0 -0
- app/core/metrics/middleware.py +74 -0
- app/core/metrics/prometheus.py +604 -0
- app/core/middleware/__init__.py +25 -0
- app/core/middleware/api_firewall.py +111 -0
- app/core/middleware/app_version.py +37 -0
- app/core/middleware/dashboard_auth_proxy.py +55 -0
- app/core/middleware/dashboard_csrf.py +177 -0
- app/core/middleware/dashboard_gzip.py +41 -0
- app/core/middleware/dashboard_overrides.py +59 -0
- app/core/middleware/firewall_cache.py +91 -0
- app/core/middleware/inflight.py +126 -0
- app/core/middleware/multipart_content_encoding.py +129 -0
- app/core/middleware/path_rewrite.py +148 -0
- app/core/middleware/request_body_limit.py +196 -0
- app/core/middleware/request_decompression.py +209 -0
- app/core/middleware/request_id.py +72 -0
- app/core/middleware/required_capability_http.py +116 -0
- app/core/middleware/trusted_proxy_headers.py +36 -0
- app/core/multipart.py +237 -0
- app/core/multipart_fields.py +91 -0
- app/core/openai/__init__.py +0 -0
- app/core/openai/chat_requests.py +493 -0
- app/core/openai/chat_responses.py +931 -0
- app/core/openai/contracts.py +46 -0
- app/core/openai/exceptions.py +24 -0
- app/core/openai/host_models.py +14 -0
- app/core/openai/images.py +413 -0
- app/core/openai/message_coercion.py +376 -0
- app/core/openai/model_refresh_scheduler.py +423 -0
- app/core/openai/model_registry.py +997 -0
- app/core/openai/model_registry_store.py +548 -0
- app/core/openai/models.py +172 -0
- app/core/openai/parsing.py +97 -0
- app/core/openai/requests.py +1980 -0
- app/core/openai/strict_schema.py +199 -0
- app/core/openai/tool_call_safety.py +99 -0
- app/core/openai/v1_requests.py +187 -0
- app/core/plan_types.py +90 -0
- app/core/prestop.py +202 -0
- app/core/rate_limiter/__init__.py +0 -0
- app/core/rate_limiter/db_rate_limiter.py +215 -0
- app/core/request_locality.py +473 -0
- app/core/resilience/__init__.py +0 -0
- app/core/resilience/backpressure.py +57 -0
- app/core/resilience/bulkhead.py +202 -0
- app/core/resilience/circuit_breaker.py +150 -0
- app/core/resilience/degradation.py +55 -0
- app/core/resilience/loop_lag_monitor.py +60 -0
- app/core/resilience/memory_monitor.py +137 -0
- app/core/resilience/network_recovery.py +256 -0
- app/core/resilience/overload.py +98 -0
- app/core/resilience/toggles.py +114 -0
- app/core/retention/__init__.py +0 -0
- app/core/retention/job.py +240 -0
- app/core/retention/scheduler.py +69 -0
- app/core/runtime_logging.py +601 -0
- app/core/scheduling/__init__.py +0 -0
- app/core/scheduling/leader_election.py +993 -0
- app/core/scheduling/leader_election_handle.py +25 -0
- app/core/server.py +140 -0
- app/core/shutdown.py +326 -0
- app/core/socket_peer.py +22 -0
- app/core/startup.py +41 -0
- app/core/timeout_invariants.py +347 -0
- app/core/tracing/__init__.py +0 -0
- app/core/tracing/otel.py +207 -0
- app/core/types.py +6 -0
- app/core/upstream_proxy/__init__.py +13 -0
- app/core/upstream_proxy/cache.py +130 -0
- app/core/upstream_proxy/resolver.py +189 -0
- app/core/upstream_proxy/types.py +104 -0
- app/core/usage/__init__.py +407 -0
- app/core/usage/depletion.py +109 -0
- app/core/usage/live_hub.py +41 -0
- app/core/usage/live_snapshots.py +174 -0
- app/core/usage/logs.py +212 -0
- app/core/usage/metadata_scheduler.py +118 -0
- app/core/usage/models.py +54 -0
- app/core/usage/pricing.py +576 -0
- app/core/usage/pricing_catalog.py +241 -0
- app/core/usage/pricing_snapshot.json +821 -0
- app/core/usage/quota.py +133 -0
- app/core/usage/refresh_policy.py +21 -0
- app/core/usage/refresh_scheduler.py +713 -0
- app/core/usage/reset_credits_refresh_scheduler.py +435 -0
- app/core/usage/types.py +168 -0
- app/core/utils/__init__.py +0 -0
- app/core/utils/json_guards.py +18 -0
- app/core/utils/locks.py +33 -0
- app/core/utils/masking.py +20 -0
- app/core/utils/proxy_env.py +104 -0
- app/core/utils/request_id.py +65 -0
- app/core/utils/retry.py +77 -0
- app/core/utils/shared_future.py +172 -0
- app/core/utils/sse.py +286 -0
- app/core/utils/time.py +24 -0
- app/db/__init__.py +0 -0
- app/db/account_identity_lock.py +56 -0
- app/db/alembic/__init__.py +1 -0
- app/db/alembic/env.py +70 -0
- app/db/alembic/revision_ids.py +43 -0
- app/db/alembic/versions/20260213_000000_base_schema.py +248 -0
- app/db/alembic/versions/20260213_000100_normalize_account_plan_types.py +45 -0
- app/db/alembic/versions/20260213_000200_add_request_logs_reasoning_effort.py +45 -0
- app/db/alembic/versions/20260213_000300_add_accounts_reset_at.py +45 -0
- app/db/alembic/versions/20260213_000400_add_accounts_chatgpt_account_id.py +45 -0
- app/db/alembic/versions/20260213_000500_add_dashboard_settings.py +70 -0
- app/db/alembic/versions/20260213_000600_add_dashboard_settings_totp.py +64 -0
- app/db/alembic/versions/20260213_000700_add_dashboard_settings_password.py +59 -0
- app/db/alembic/versions/20260213_000800_add_api_keys.py +87 -0
- app/db/alembic/versions/20260214_000000_add_api_key_limits.py +143 -0
- app/db/alembic/versions/20260215_000000_add_idx_logs_requested_at.py +38 -0
- app/db/alembic/versions/20260218_000000_add_api_key_usage_reservations.py +117 -0
- app/db/alembic/versions/20260218_000100_add_import_without_overwrite_and_drop_accounts_email_unique.py +198 -0
- app/db/alembic/versions/20260225_000000_add_dashboard_settings_routing_strategy.py +53 -0
- app/db/alembic/versions/20260228_020000_align_api_key_limit_enum_types.py +104 -0
- app/db/alembic/versions/20260228_030000_add_api_firewall_allowlist.py +42 -0
- app/db/alembic/versions/20260306_000000_add_request_logs_service_tier.py +45 -0
- app/db/alembic/versions/20260307_000000_add_api_key_enforcement_fields.py +56 -0
- app/db/alembic/versions/20260308_000000_add_sqlite_performance_indexes.py +41 -0
- app/db/alembic/versions/20260309_000000_add_additional_usage_history.py +71 -0
- app/db/alembic/versions/20260309_000000_request_logs_nullable_account_id.py +28 -0
- app/db/alembic/versions/20260310_000000_add_request_logs_transport.py +45 -0
- app/db/alembic/versions/20260310_000000_fix_postgresql_enum_value_casing.py +79 -0
- app/db/alembic/versions/20260310_120000_add_sticky_session_kinds_and_affinity_ttl.py +110 -0
- app/db/alembic/versions/20260312_000000_add_additional_usage_quota_key.py +193 -0
- app/db/alembic/versions/20260312_000000_split_sticky_sessions_primary_key_by_kind.py +96 -0
- app/db/alembic/versions/20260312_010000_merge_additional_usage_and_sticky_session_heads.py +25 -0
- app/db/alembic/versions/20260312_020000_merge_request_logs_transport_and_feature_heads.py +25 -0
- app/db/alembic/versions/20260312_020000_merge_request_logs_transport_and_sticky_heads.py +25 -0
- app/db/alembic/versions/20260312_120000_add_dashboard_upstream_stream_transport.py +62 -0
- app/db/alembic/versions/20260313_024500_merge_request_log_transport_heads.py +27 -0
- app/db/alembic/versions/20260319_100937_increase_prompt_cache_ttl_to_1800s.py +73 -0
- app/db/alembic/versions/20260319_111500_merge_prompt_cache_ttl_and_upstream_transport_heads.py +25 -0
- app/db/alembic/versions/20260319_183000_normalize_sqlite_account_status_casing.py +48 -0
- app/db/alembic/versions/20260320_000000_add_request_log_requested_actual_tiers.py +51 -0
- app/db/alembic/versions/20260321_190000_tighten_dashboard_db_indexes.py +64 -0
- app/db/alembic/versions/20260321_210000_merge_request_log_tiers_and_dashboard_index_heads.py +25 -0
- app/db/alembic/versions/20260325_000000_add_request_log_cost.py +193 -0
- app/db/alembic/versions/20260328_000000_add_rate_limit_attempts.py +48 -0
- app/db/alembic/versions/20260328_120000_add_request_log_ttft.py +37 -0
- app/db/alembic/versions/20260328_130000_add_audit_logs_table.py +45 -0
- app/db/alembic/versions/20260328_140000_add_scheduler_leader_table.py +39 -0
- app/db/alembic/versions/20260330_000000_add_cache_locality_settings.py +69 -0
- app/db/alembic/versions/20260330_010000_merge_scheduler_leader_and_cache_locality_heads.py +25 -0
- app/db/alembic/versions/20260330_020000_add_bridge_ring_members.py +46 -0
- app/db/alembic/versions/20260401_000000_add_api_key_enforced_service_tier.py +52 -0
- app/db/alembic/versions/20260401_000000_add_cache_invalidation.py +41 -0
- app/db/alembic/versions/20260401_020000_merge_cache_invalidation_and_api_key_service_tier_heads.py +22 -0
- app/db/alembic/versions/20260402_000000_switch_dashboard_routing_default_to_capacity_weighted.py +66 -0
- app/db/alembic/versions/20260403_000000_add_credit_api_key_limit_values.py +44 -0
- app/db/alembic/versions/20260406_000000_add_api_key_account_assignments.py +57 -0
- app/db/alembic/versions/20260406_010000_add_api_key_assignment_scope_flag.py +75 -0
- app/db/alembic/versions/20260407_000000_add_bridge_gateway_safe_mode.py +51 -0
- app/db/alembic/versions/20260407_010000_merge_api_key_assignment_and_bridge_gateway_heads.py +25 -0
- app/db/alembic/versions/20260408_000000_switch_import_without_overwrite_default_to_true.py +73 -0
- app/db/alembic/versions/20260408_010000_merge_import_without_overwrite_and_assignment_heads.py +25 -0
- app/db/alembic/versions/20260409_000000_add_http_bridge_sessions.py +130 -0
- app/db/alembic/versions/20260409_000000_switch_sticky_threads_and_prefer_earlier_reset_defaults_to_true.py +89 -0
- app/db/alembic/versions/20260409_010000_add_dashboard_settings_bootstrap_token.py +57 -0
- app/db/alembic/versions/20260409_010000_merge_http_bridge_and_import_without_overwrite_heads.py +25 -0
- app/db/alembic/versions/20260409_020000_fix_http_bridge_last_seen_index.py +46 -0
- app/db/alembic/versions/20260410_000000_backfill_pristine_dashboard_settings_defaults.py +77 -0
- app/db/alembic/versions/20260410_030000_restore_import_without_overwrite_default_true.py +73 -0
- app/db/alembic/versions/20260410_040000_merge_dashboard_defaults_and_import_default_heads.py +25 -0
- app/db/alembic/versions/20260413_000000_add_accounts_blocked_at.py +44 -0
- app/db/alembic/versions/20260415_160000_add_automations_tables.py +189 -0
- app/db/alembic/versions/20260415_160000_add_request_logs_response_lookup_index.py +67 -0
- app/db/alembic/versions/20260417_000000_add_request_log_plan_type.py +44 -0
- app/db/alembic/versions/20260418_190000_add_automation_runs_cycle_key.py +87 -0
- app/db/alembic/versions/20260419_000000_add_automation_run_cycle_metadata.py +56 -0
- app/db/alembic/versions/20260419_020000_add_automation_run_cycles_snapshot_tables.py +342 -0
- app/db/alembic/versions/20260421_000000_add_dashboard_session_ttl_setting.py +51 -0
- app/db/alembic/versions/20260421_120000_merge_request_log_lookup_and_plan_type_heads.py +25 -0
- app/db/alembic/versions/20260421_130000_merge_automation_and_request_log_heads.py +25 -0
- app/db/alembic/versions/20260422_103000_normalize_legacy_automation_run_cycle_keys.py +648 -0
- app/db/alembic/versions/20260423_120000_add_api_key_limit_reset_at_index.py +43 -0
- app/db/alembic/versions/20260424_000000_merge_dashboard_session_ttl_and_request_log_heads.py +25 -0
- app/db/alembic/versions/20260426_000000_add_dashboard_relative_availability_settings.py +68 -0
- app/db/alembic/versions/20260427_130000_add_warmup_model_and_request_kind.py +75 -0
- app/db/alembic/versions/20260506_000000_add_account_routing_policy.py +37 -0
- app/db/alembic/versions/20260509_000000_add_api_key_traffic_class.py +37 -0
- app/db/alembic/versions/20260509_010000_add_additional_quota_routing_policies.py +37 -0
- app/db/alembic/versions/20260513_000000_add_accounts_alias.py +44 -0
- app/db/alembic/versions/20260513_000000_add_api_key_apply_to_codex_model.py +59 -0
- app/db/alembic/versions/20260514_000000_add_request_logs_api_key_time_index.py +43 -0
- app/db/alembic/versions/20260515_000000_add_prefer_earlier_reset_window.py +47 -0
- app/db/alembic/versions/20260515_000000_soft_delete_request_logs_on_account_delete.py +83 -0
- app/db/alembic/versions/20260515_020000_add_split_sticky_budget_thresholds.py +62 -0
- app/db/alembic/versions/20260516_000000_add_sqlite_hot_path_indexes.py +75 -0
- app/db/alembic/versions/20260517_000000_merge_sqlite_hot_path_and_request_log_delete_heads.py +24 -0
- app/db/alembic/versions/20260518_000000_add_http_bridge_durable_input_prefix.py +44 -0
- app/db/alembic/versions/20260518_010000_merge_http_bridge_and_sqlite_recovery_heads.py +25 -0
- app/db/alembic/versions/20260518_120000_add_dashboard_guest_access.py +57 -0
- app/db/alembic/versions/20260520_000000_merge_api_key_and_http_bridge_heads.py +26 -0
- app/db/alembic/versions/20260520_010000_add_request_logs_api_key_account_index.py +45 -0
- app/db/alembic/versions/20260520_010000_merge_automation_and_api_key_heads.py +26 -0
- app/db/alembic/versions/20260520_030000_add_quota_planner.py +194 -0
- app/db/alembic/versions/20260520_040000_merge_quota_planner_and_request_log_index_heads.py +24 -0
- app/db/alembic/versions/20260521_000000_add_account_security_work_authorized.py +46 -0
- app/db/alembic/versions/20260521_000000_merge_dashboard_guest_and_api_key_heads.py +26 -0
- app/db/alembic/versions/20260521_000000_merge_prefer_reset_window_head.py +26 -0
- app/db/alembic/versions/20260521_000000_merge_warmup_and_request_log_index_heads.py +25 -0
- app/db/alembic/versions/20260522_000000_add_limit_warmup_trigger.py +195 -0
- app/db/alembic/versions/20260524_000000_merge_accounts_alias_and_reset_window_heads.py +24 -0
- app/db/alembic/versions/20260525_000000_add_usage_raw_window_latest_index.py +31 -0
- app/db/alembic/versions/20260526_000000_add_request_log_failure_metadata.py +100 -0
- app/db/alembic/versions/20260531_000000_add_account_workspace_identity.py +39 -0
- app/db/alembic/versions/20260531_000000_add_single_account_routing.py +42 -0
- app/db/alembic/versions/20260601_000000_merge_relative_availability_and_usage_raw_heads.py +24 -0
- app/db/alembic/versions/20260601_010000_add_api_key_usage_sections.py +58 -0
- app/db/alembic/versions/20260601_010000_add_routing_policy_persistence.py +67 -0
- app/db/alembic/versions/20260601_010000_merge_security_work_and_relative_availability_heads.py +25 -0
- app/db/alembic/versions/20260601_010000_merge_split_sticky_and_relative_usage_heads.py +25 -0
- app/db/alembic/versions/20260601_010000_merge_warmup_and_relative_availability_heads.py +25 -0
- app/db/alembic/versions/20260601_020000_merge_additional_quota_routing_and_relative_availability_heads.py +25 -0
- app/db/alembic/versions/20260601_020000_merge_quota_planner_and_relative_availability_heads.py +25 -0
- app/db/alembic/versions/20260601_020000_merge_reset_window_and_relative_availability_heads.py +25 -0
- app/db/alembic/versions/20260601_020000_merge_routing_policy_and_split_threshold_heads.py +24 -0
- app/db/alembic/versions/20260601_020000_merge_warmup_and_request_log_failure_heads.py +25 -0
- app/db/alembic/versions/20260601_030000_merge_single_account_and_routing_quota_heads.py +29 -0
- app/db/alembic/versions/20260601_040000_merge_security_work_and_routing_consolidation_heads.py +25 -0
- app/db/alembic/versions/20260601_050000_merge_quota_planner_and_routing_consolidation_heads.py +25 -0
- app/db/alembic/versions/20260602_050000_add_upstream_proxy_routing.py +164 -0
- app/db/alembic/versions/20260602_060000_merge_account_workspace_and_failure_heads.py +25 -0
- app/db/alembic/versions/20260602_070000_add_request_log_useragent_fields.py +86 -0
- app/db/alembic/versions/20260602_070000_merge_quota_planner_and_account_workspace_heads.py +25 -0
- app/db/alembic/versions/20260602_080000_merge_upstream_proxy_and_quota_planner_heads.py +25 -0
- app/db/alembic/versions/20260602_080000_merge_useragent_and_upstream_proxy_heads.py +24 -0
- app/db/alembic/versions/20260603_000000_add_weekly_pace_working_days.py +52 -0
- app/db/alembic/versions/20260603_000000_free_account_monthly_window.py +60 -0
- app/db/alembic/versions/20260604_000000_add_reauth_required_account_status.py +159 -0
- app/db/alembic/versions/20260604_010000_merge_automations_and_reauth_status_heads.py +25 -0
- app/db/alembic/versions/20260604_010000_merge_dashboard_guest_and_reauth_status_heads.py +25 -0
- app/db/alembic/versions/20260604_010000_merge_useragent_and_reauth_heads.py +24 -0
- app/db/alembic/versions/20260607_000000_merge_weekly_monthly_useragent_heads.py +28 -0
- app/db/alembic/versions/20260608_000000_add_hide_upstream_quota_from_api_keys.py +50 -0
- app/db/alembic/versions/20260611_000000_add_request_log_archive_request_id.py +47 -0
- app/db/alembic/versions/20260611_000000_merge_automations_and_weekly_monthly_useragent_heads.py +26 -0
- app/db/alembic/versions/20260611_000000_merge_dashboard_guest_and_weekly_useragent_heads.py +26 -0
- app/db/alembic/versions/20260611_010000_add_request_log_client_ip.py +57 -0
- app/db/alembic/versions/20260611_020000_merge_request_log_archive_and_client_ip_heads.py +26 -0
- app/db/alembic/versions/20260611_030000_merge_dashboard_guest_and_request_log_client_ip_heads.py +26 -0
- app/db/alembic/versions/20260613_000000_add_accounts_codex_installation_id.py +57 -0
- app/db/alembic/versions/20260613_010000_merge_installation_id_and_dashboard_weekly_heads.py +26 -0
- app/db/alembic/versions/20260615_000000_merge_automations_and_dashboard_guest_heads.py +26 -0
- app/db/alembic/versions/20260623_000000_add_limit_warmup_exhausted_threshold.py +51 -0
- app/db/alembic/versions/20260626_000000_add_http_downstream_transport_policy.py +46 -0
- app/db/alembic/versions/20260626_010000_add_request_logs_upstream_transport.py +31 -0
- app/db/alembic/versions/20260629_000000_add_dashboard_query_hot_path_indexes.py +53 -0
- app/db/alembic/versions/20260630_000000_add_limit_warmup_staggered_idle.py +47 -0
- app/db/alembic/versions/20260630_000000_merge_request_log_client_ip_and_dashboard_hot_path_heads.py +26 -0
- app/db/alembic/versions/20260630_010000_merge_warmup_and_request_log_dashboard_heads.py +26 -0
- app/db/alembic/versions/20260630_020000_merge_automations_and_main_heads.py +26 -0
- app/db/alembic/versions/20260630_020000_merge_warmup_threshold_and_main_heads.py +26 -0
- app/db/alembic/versions/20260630_030000_add_automation_run_model_snapshot.py +59 -0
- app/db/alembic/versions/20260630_040000_merge_automation_snapshot_and_warmup_threshold_heads.py +26 -0
- app/db/alembic/versions/20260630_050000_add_automation_run_prompt_snapshot.py +52 -0
- app/db/alembic/versions/20260701_000000_add_weekly_pace_smoothing_minutes.py +51 -0
- app/db/alembic/versions/20260702_000000_add_model_sources.py +176 -0
- app/db/alembic/versions/20260705_000000_add_model_source_audio_transcriptions.py +50 -0
- app/db/alembic/versions/20260705_000000_harden_dashboard_session_ttl.py +62 -0
- app/db/alembic/versions/20260706_000000_add_model_source_audio_per_minute.py +43 -0
- app/db/alembic/versions/20260707_000000_merge_model_sources_and_session_ttl.py +24 -0
- app/db/alembic/versions/20260707_010000_merge_automations_and_model_sources_heads.py +24 -0
- app/db/alembic/versions/20260707_020000_add_automation_job_account_scope.py +125 -0
- app/db/alembic/versions/20260709_000000_add_ttft_phase_observability.py +66 -0
- app/db/alembic/versions/20260711_000000_add_dashboard_prohibit_fast_mode.py +42 -0
- app/db/alembic/versions/20260711_010000_add_account_chatgpt_user_id.py +40 -0
- app/db/alembic/versions/20260711_020000_add_dashboard_account_concurrency_caps.py +71 -0
- app/db/alembic/versions/20260711_030000_add_limit_warmup_idle_threshold.py +51 -0
- app/db/alembic/versions/20260712_010000_add_account_usage_rollups.py +56 -0
- app/db/alembic/versions/20260712_020000_add_api_key_usage_rollups.py +53 -0
- app/db/alembic/versions/20260713_020000_add_model_registry_snapshot.py +42 -0
- app/db/alembic/versions/20260713_040000_add_account_refresh_claims.py +42 -0
- app/db/alembic/versions/20260713_040000_add_replica_guardrails.py +64 -0
- app/db/alembic/versions/20260713_070000_add_reset_credit_redeem_tables.py +58 -0
- app/db/alembic/versions/20260714_000000_add_oauth_flow_states.py +63 -0
- app/db/alembic/versions/20260715_000000_add_request_log_queue_latency.py +44 -0
- app/db/alembic/versions/20260716_000000_add_oauth_device_flow_slots.py +41 -0
- app/db/alembic/versions/20260716_010000_add_dashboard_retention_settings.py +48 -0
- app/db/alembic/versions/20260716_010000_add_reset_credit_display_settings.py +73 -0
- app/db/alembic/versions/20260717_000000_add_http_bridge_retry_circuits.py +47 -0
- app/db/alembic/versions/20260717_000000_merge_retention_and_reset_credit_display_heads.py +26 -0
- app/db/alembic/versions/20260717_000000_optimize_dashboard_hot_path_indexes.py +162 -0
- app/db/alembic/versions/20260717_000001_merge_retry_circuits_and_dashboard_indexes.py +22 -0
- app/db/alembic/versions/20260720_000000_add_request_log_conversation_id.py +57 -0
- app/db/alembic/versions/20260722_000000_backfill_request_log_useragent_families.py +40 -0
- app/db/alembic/versions/20260724_000000_add_request_usage_time_rollups.py +176 -0
- app/db/alembic/versions/20260724_000000_merge_request_log_schema_heads.py +33 -0
- app/db/alembic/versions/20260725_000000_add_http_bridge_pending_tool_calls.py +43 -0
- app/db/alembic/versions/20260726_000000_add_account_plan_downgrade_observations.py +44 -0
- app/db/alembic/versions/20260726_000000_repair_request_usage_rollups_after_merge.py +37 -0
- app/db/alembic/versions/20260727_000000_add_sticky_session_continuity_abandoned_at.py +45 -0
- app/db/alembic/versions/20260728_000000_merge_pending_tool_calls_and_rollup_repair_heads.py +31 -0
- app/db/alembic/versions/20260730_000000_add_api_key_fair_share_threshold.py +44 -0
- app/db/alembic/versions/20260730_000000_add_http_bridge_recovery_attempts.py +60 -0
- app/db/alembic/versions/20260731_000000_add_capability_lineage_markers.py +38 -0
- app/db/alembic/versions/20260803_000000_merge_http_bridge_recovery_and_capability_lineage_heads.py +30 -0
- app/db/alembic/versions/20260804_000000_add_http_bridge_operations.py +63 -0
- app/db/alembic/versions/20260804_000001_add_global_http_bridge_operation_fingerprint.py +53 -0
- app/db/alembic/versions/20260804_230000_add_request_log_connection_request_kind.py +43 -0
- app/db/alembic/versions/20260805_000000_add_http_bridge_operation_spool.py +75 -0
- app/db/alembic/versions/20260805_000001_finalize_http_bridge_operation_spool.py +65 -0
- app/db/alembic/versions/20260806_000000_add_additional_usage_alias_probe_indexes.py +84 -0
- app/db/alembic/versions/20260806_000000_add_anonymous_telemetry.py +49 -0
- app/db/alembic/versions/20260806_010000_add_conversation_presence_rollup.py +72 -0
- app/db/alembic/versions/20260806_020000_add_usage_history_bulk_covering_indexes.py +99 -0
- app/db/alembic/versions/20260806_030000_add_api_key_allowed_reasoning_efforts.py +65 -0
- app/db/alembic/versions/20260806_120000_add_http_bridge_owner_process_epoch.py +57 -0
- app/db/alembic/versions/20260807_000000_merge_http_bridge_operation_spool_and_latest_main.py +24 -0
- app/db/alembic/versions/20260808_000000_tune_usage_history_autovacuum.py +54 -0
- app/db/alembic/versions/20260810_000000_add_http_bridge_recovery_dispatch_count.py +44 -0
- app/db/alembic/versions/20260811_000000_add_hourly_rollup_cancelled_count.py +92 -0
- app/db/alembic/versions/20260812_000000_merge_recovery_dispatch_and_hourly_cancelled_heads.py +25 -0
- app/db/alembic/versions/20260812_120000_add_sticky_abandonment_scope.py +43 -0
- app/db/alembic/versions/20260813_000000_add_file_account_pins.py +40 -0
- app/db/alembic/versions/20260816_000000_add_account_pending_deletion.py +81 -0
- app/db/alembic/versions/20260816_000000_add_model_source_embeddings.py +50 -0
- app/db/alembic/versions/20260821_000000_add_retry_circuit_admission_generation.py +49 -0
- app/db/alembic/versions/20260826_000000_add_http_bridge_event_chunks.py +107 -0
- app/db/alembic/versions/20260828_000000_add_accounts_chatgpt_identity_index.py +55 -0
- app/db/alembic/versions/20260830_000000_add_quota_warmup_claim_expiry.py +83 -0
- app/db/alembic/versions/20260908_000000_add_guest_session_generation.py +37 -0
- app/db/alembic/versions/20260908_000000_add_subscription_overflow.py +140 -0
- app/db/alembic/versions/20260908_000000_replace_upstream_stream_transport_default_sentinel.py +50 -0
- app/db/alembic/versions/20260908_020000_merge_overflow_transport_heads.py +27 -0
- app/db/alembic/versions/20260909_000000_add_dashboard_roles.py +72 -0
- app/db/alembic/versions/20260909_010000_add_dashboard_users.py +183 -0
- app/db/alembic/versions/20260909_020000_reproject_compat_admin_credentials.py +105 -0
- app/db/alembic/versions/20260909_030000_add_audit_actor_columns.py +103 -0
- app/db/alembic/versions/20260909_030000_dashboard_resilience_toggle_settings.py +63 -0
- app/db/alembic/versions/20260909_040000_add_dashboard_user_invites.py +54 -0
- app/db/alembic/versions/20260909_040000_dashboard_timeout_settings.py +68 -0
- app/db/alembic/versions/20260909_050000_dashboard_routing_overload_settings.py +65 -0
- app/db/alembic/versions/20260909_060000_add_report_rollup.py +74 -0
- app/db/alembic/versions/20260909_070000_automation_run_claim_budget.py +50 -0
- app/db/alembic/versions/20260909_080000_dashboard_stream_bridge_budgets.py +62 -0
- app/db/alembic/versions/20260909_090000_dashboard_background_job_toggles.py +63 -0
- app/db/alembic/versions/20260909_100000_dashboard_codex_prewarm.py +49 -0
- app/db/alembic/versions/20260909_110000_model_context_window_overrides.py +45 -0
- app/db/alembic/versions/20260909_120000_dashboard_conversation_archive.py +48 -0
- app/db/alembic/versions/20260909_130000_add_request_logs_live_facet_indexes.py +96 -0
- app/db/alembic/versions/20260910_000000_add_totp_required_for_admin_role.py +44 -0
- app/db/alembic/versions/20260910_000000_request_logs_missing_cost_index.py +52 -0
- app/db/alembic/versions/20260910_010000_add_dashboard_auth_providers.py +134 -0
- app/db/alembic/versions/20260910_010000_dashboard_spool_retention.py +52 -0
- app/db/alembic/versions/20260910_020000_add_dashboard_role_mappings.py +74 -0
- app/db/alembic/versions/20260910_143000_request_log_affinity.py +28 -0
- app/db/alembic/versions/20260910_180000_merge_affinity_guest_heads.py +17 -0
- app/db/alembic/versions/20260910_200000_merge_affinity_identity_heads.py +17 -0
- app/db/alembic/versions/20260910_220000_merge_affinity_invite_heads.py +17 -0
- app/db/alembic/versions/20260911_000000_model_source_pins_kind_expires_index.py +43 -0
- app/db/alembic/versions/20260911_010000_merge_pin_index_and_affinity_heads.py +17 -0
- app/db/alembic/versions/20260911_020000_add_http_bridge_terminal_append_phase.py +54 -0
- app/db/alembic/versions/20260911_030000_add_local_login_policy.py +48 -0
- app/db/alembic/versions/20260911_040000_add_thread_cache_identity_mode.py +48 -0
- app/db/alembic/versions/20260911_060000_add_bridge_session_continuity_abandonment.py +40 -0
- app/db/alembic/versions/20260912_000000_merge_thread_cache_and_bridge_retirement_heads.py +22 -0
- app/db/alembic/versions/20260912_010000_drop_legacy_dashboard_credentials.py +140 -0
- app/db/alembic/versions/20260913_000000_add_oidc_provider_flow.py +134 -0
- app/db/alembic/versions/20260914_000000_add_scim_tokens.py +78 -0
- app/db/alembic/versions/20260914_000000_drop_subscription_overflow_schema.py +132 -0
- app/db/alembic/versions/20260918_000000_merge_scim_and_overflow_heads.py +17 -0
- app/db/alembic/versions/__init__.py +1 -0
- app/db/backup.py +64 -0
- app/db/migrate.py +1249 -0
- app/db/migration_lock.py +160 -0
- app/db/migration_url.py +18 -0
- app/db/models.py +3100 -0
- app/db/recover.py +244 -0
- app/db/session.py +1295 -0
- app/db/snapshot.py +46 -0
- app/db/sqlite_lock_retry.py +190 -0
- app/db/sqlite_utils.py +549 -0
- app/dependencies.py +462 -0
- app/main.py +1240 -0
- app/modules/__init__.py +0 -0
- app/modules/accounts/__init__.py +0 -0
- app/modules/accounts/api.py +426 -0
- app/modules/accounts/auth_manager.py +1439 -0
- app/modules/accounts/background_repository.py +160 -0
- app/modules/accounts/deletion.py +540 -0
- app/modules/accounts/mappers.py +654 -0
- app/modules/accounts/refresh_claims.py +352 -0
- app/modules/accounts/repository.py +1679 -0
- app/modules/accounts/schemas.py +308 -0
- app/modules/accounts/service.py +809 -0
- app/modules/accounts/usage_rollup.py +486 -0
- app/modules/accounts/usage_rollup_scheduler.py +75 -0
- app/modules/accounts/usage_time_rollup.py +1037 -0
- app/modules/accounts/usage_time_rollup_read.py +522 -0
- app/modules/api_keys/__init__.py +1 -0
- app/modules/api_keys/api.py +344 -0
- app/modules/api_keys/last_used_coalescer.py +216 -0
- app/modules/api_keys/limit_windows.py +41 -0
- app/modules/api_keys/repository.py +1200 -0
- app/modules/api_keys/reset_scheduler.py +85 -0
- app/modules/api_keys/schemas.py +135 -0
- app/modules/api_keys/service.py +2123 -0
- app/modules/audit/__init__.py +0 -0
- app/modules/audit/api.py +88 -0
- app/modules/audit/repository.py +74 -0
- app/modules/audit/schemas.py +33 -0
- app/modules/audit/service.py +71 -0
- app/modules/auth_providers/__init__.py +0 -0
- app/modules/auth_providers/api.py +127 -0
- app/modules/auth_providers/config.py +343 -0
- app/modules/auth_providers/repository.py +101 -0
- app/modules/auth_providers/schemas.py +88 -0
- app/modules/auth_providers/seed.py +105 -0
- app/modules/auth_providers/service.py +289 -0
- app/modules/automations/__init__.py +1 -0
- app/modules/automations/api.py +369 -0
- app/modules/automations/repository.py +1896 -0
- app/modules/automations/scheduler.py +107 -0
- app/modules/automations/schemas.py +158 -0
- app/modules/automations/service.py +2548 -0
- app/modules/cache_isolation_probe/__init__.py +0 -0
- app/modules/cache_isolation_probe/api.py +188 -0
- app/modules/cache_isolation_probe/prefix.py +144 -0
- app/modules/cache_isolation_probe/schemas.py +94 -0
- app/modules/cache_isolation_probe/sender.py +229 -0
- app/modules/cache_isolation_probe/service.py +525 -0
- app/modules/conversation_archive/__init__.py +1 -0
- app/modules/conversation_archive/api.py +101 -0
- app/modules/conversation_archive/schemas.py +28 -0
- app/modules/conversation_archive/service.py +165 -0
- app/modules/dashboard/__init__.py +1 -0
- app/modules/dashboard/api.py +106 -0
- app/modules/dashboard/builders.py +107 -0
- app/modules/dashboard/repository.py +304 -0
- app/modules/dashboard/schemas.py +148 -0
- app/modules/dashboard/service.py +534 -0
- app/modules/dashboard/timeframes.py +19 -0
- app/modules/dashboard/weekly_pace.py +617 -0
- app/modules/dashboard_auth/__init__.py +1 -0
- app/modules/dashboard_auth/api.py +1344 -0
- app/modules/dashboard_auth/oidc_api.py +753 -0
- app/modules/dashboard_auth/oidc_flows.py +330 -0
- app/modules/dashboard_auth/repository.py +452 -0
- app/modules/dashboard_auth/schemas.py +232 -0
- app/modules/dashboard_auth/service.py +1384 -0
- app/modules/dashboard_roles/__init__.py +0 -0
- app/modules/dashboard_roles/api.py +64 -0
- app/modules/dashboard_roles/repository.py +54 -0
- app/modules/dashboard_roles/schemas.py +29 -0
- app/modules/dashboard_roles/seed.py +80 -0
- app/modules/dashboard_roles/service.py +142 -0
- app/modules/dashboard_users/__init__.py +0 -0
- app/modules/dashboard_users/api.py +278 -0
- app/modules/dashboard_users/break_glass.py +169 -0
- app/modules/dashboard_users/credentials.py +18 -0
- app/modules/dashboard_users/identity_resolver.py +665 -0
- app/modules/dashboard_users/repository.py +725 -0
- app/modules/dashboard_users/schemas.py +117 -0
- app/modules/dashboard_users/service.py +1054 -0
- app/modules/firewall/__init__.py +1 -0
- app/modules/firewall/api.py +78 -0
- app/modules/firewall/repository.py +51 -0
- app/modules/firewall/schemas.py +28 -0
- app/modules/firewall/service.py +93 -0
- app/modules/fleet/__init__.py +1 -0
- app/modules/fleet/api.py +177 -0
- app/modules/fleet/mappers.py +56 -0
- app/modules/fleet/observability.py +372 -0
- app/modules/fleet/schemas.py +119 -0
- app/modules/health/__init__.py +0 -0
- app/modules/health/api.py +246 -0
- app/modules/health/schemas.py +27 -0
- app/modules/limit_warmup/__init__.py +1 -0
- app/modules/limit_warmup/repository.py +159 -0
- app/modules/limit_warmup/service.py +1001 -0
- app/modules/model_sources/__init__.py +1 -0
- app/modules/model_sources/api.py +99 -0
- app/modules/model_sources/catalog.py +307 -0
- app/modules/model_sources/forwarding.py +1435 -0
- app/modules/model_sources/projection.py +55 -0
- app/modules/model_sources/repository.py +201 -0
- app/modules/model_sources/schemas.py +79 -0
- app/modules/model_sources/selection.py +166 -0
- app/modules/model_sources/service.py +236 -0
- app/modules/oauth/__init__.py +0 -0
- app/modules/oauth/api.py +101 -0
- app/modules/oauth/repository.py +238 -0
- app/modules/oauth/schemas.py +49 -0
- app/modules/oauth/service.py +1196 -0
- app/modules/oauth/templates/oauth_success.html +122 -0
- app/modules/proxy/__init__.py +0 -0
- app/modules/proxy/_load_balancer/__init__.py +1 -0
- app/modules/proxy/_load_balancer/error_rate.py +116 -0
- app/modules/proxy/_load_balancer/latency_cohort.py +154 -0
- app/modules/proxy/_load_balancer/model_eligibility.py +250 -0
- app/modules/proxy/_load_balancer/opportunistic_admission.py +350 -0
- app/modules/proxy/_load_balancer/overload_backoff.py +394 -0
- app/modules/proxy/_load_balancer/sticky_selection.py +2417 -0
- app/modules/proxy/_load_balancer/throughput_cohort.py +233 -0
- app/modules/proxy/_load_balancer/ttft_cohort.py +214 -0
- app/modules/proxy/_load_balancer/tunables.py +97 -0
- app/modules/proxy/_load_balancer/types.py +96 -0
- app/modules/proxy/_load_balancer/unbound_selection.py +534 -0
- app/modules/proxy/_service/__init__.py +6 -0
- app/modules/proxy/_service/api_key_usage.py +745 -0
- app/modules/proxy/_service/clock_budget.py +38 -0
- app/modules/proxy/_service/codex_control.py +611 -0
- app/modules/proxy/_service/compact.py +2211 -0
- app/modules/proxy/_service/file_ops.py +749 -0
- app/modules/proxy/_service/http_bridge/__init__.py +3 -0
- app/modules/proxy/_service/http_bridge/accepted_replay.py +331 -0
- app/modules/proxy/_service/http_bridge/account_sessions.py +57 -0
- app/modules/proxy/_service/http_bridge/activity.py +178 -0
- app/modules/proxy/_service/http_bridge/helpers.py +3959 -0
- app/modules/proxy/_service/http_bridge/mixin.py +2435 -0
- app/modules/proxy/_service/http_bridge/owner_forwarding.py +609 -0
- app/modules/proxy/_service/http_bridge/protocol.py +167 -0
- app/modules/proxy/_service/http_bridge/proxy_failover.py +58 -0
- app/modules/proxy/_service/http_bridge/quarantine.py +443 -0
- app/modules/proxy/_service/http_bridge/request_submit.py +4582 -0
- app/modules/proxy/_service/http_bridge/retry_circuit.py +2442 -0
- app/modules/proxy/_service/http_bridge/service_stubs.py +492 -0
- app/modules/proxy/_service/http_bridge/session_registry.py +778 -0
- app/modules/proxy/_service/http_bridge/streaming.py +5328 -0
- app/modules/proxy/_service/http_bridge/upstream_events.py +4538 -0
- app/modules/proxy/_service/observability.py +410 -0
- app/modules/proxy/_service/rate_limit.py +361 -0
- app/modules/proxy/_service/realtime_live.py +655 -0
- app/modules/proxy/_service/refresh.py +154 -0
- app/modules/proxy/_service/request_log.py +585 -0
- app/modules/proxy/_service/response_create.py +930 -0
- app/modules/proxy/_service/streaming/__init__.py +3 -0
- app/modules/proxy/_service/streaming/helpers.py +1317 -0
- app/modules/proxy/_service/streaming/mixin.py +1100 -0
- app/modules/proxy/_service/streaming/protocol.py +32 -0
- app/modules/proxy/_service/streaming/retry.py +3762 -0
- app/modules/proxy/_service/support.py +2122 -0
- app/modules/proxy/_service/transcribe.py +434 -0
- app/modules/proxy/_service/warmup.py +483 -0
- app/modules/proxy/_service/websocket/__init__.py +3 -0
- app/modules/proxy/_service/websocket/helpers.py +2360 -0
- app/modules/proxy/_service/websocket/mixin.py +7285 -0
- app/modules/proxy/_service/websocket/protocol.py +74 -0
- app/modules/proxy/account_cache.py +229 -0
- app/modules/proxy/account_eligibility.py +87 -0
- app/modules/proxy/additional_model_limits.py +40 -0
- app/modules/proxy/affinity.py +830 -0
- app/modules/proxy/affinity_observation.py +101 -0
- app/modules/proxy/api.py +10254 -0
- app/modules/proxy/api_key_usage.py +113 -0
- app/modules/proxy/cap_partitioning.py +280 -0
- app/modules/proxy/capability_lineage.py +65 -0
- app/modules/proxy/capability_lineage_repository.py +82 -0
- app/modules/proxy/capability_routing.py +253 -0
- app/modules/proxy/continuity.py +112 -0
- app/modules/proxy/downstream_delivery.py +277 -0
- app/modules/proxy/durable_bridge_coordinator.py +1217 -0
- app/modules/proxy/durable_bridge_repository.py +4516 -0
- app/modules/proxy/durable_bridge_runtime.py +9 -0
- app/modules/proxy/durable_bridge_transcript_codec.py +122 -0
- app/modules/proxy/fair_share.py +131 -0
- app/modules/proxy/file_pin_repository.py +210 -0
- app/modules/proxy/helpers.py +439 -0
- app/modules/proxy/http_bridge_event_batcher.py +613 -0
- app/modules/proxy/http_bridge_forwarding.py +797 -0
- app/modules/proxy/http_continuation.py +76 -0
- app/modules/proxy/images_observability.py +62 -0
- app/modules/proxy/images_service.py +1054 -0
- app/modules/proxy/load_balancer.py +3016 -0
- app/modules/proxy/rate_limit_cache.py +46 -0
- app/modules/proxy/replay_safety.py +1063 -0
- app/modules/proxy/repo_bundle.py +31 -0
- app/modules/proxy/request_policy.py +1054 -0
- app/modules/proxy/ring_membership.py +251 -0
- app/modules/proxy/schemas.py +345 -0
- app/modules/proxy/selection_errors.py +41 -0
- app/modules/proxy/service.py +2600 -0
- app/modules/proxy/source_admission.py +125 -0
- app/modules/proxy/source_dispatch.py +999 -0
- app/modules/proxy/sticky_repository.py +831 -0
- app/modules/proxy/thread_anchors.py +491 -0
- app/modules/proxy/tool_call_dedupe.py +802 -0
- app/modules/proxy/types.py +54 -0
- app/modules/proxy/work_admission.py +121 -0
- app/modules/quota_planner/__init__.py +1 -0
- app/modules/quota_planner/api.py +276 -0
- app/modules/quota_planner/logic.py +743 -0
- app/modules/quota_planner/repository.py +722 -0
- app/modules/quota_planner/scheduler.py +212 -0
- app/modules/quota_planner/schemas.py +102 -0
- app/modules/quota_planner/warmup.py +693 -0
- app/modules/rate_limit_reset_credits/__init__.py +0 -0
- app/modules/rate_limit_reset_credits/api.py +601 -0
- app/modules/rate_limit_reset_credits/redeem_coordination.py +274 -0
- app/modules/rate_limit_reset_credits/store.py +114 -0
- app/modules/reports/__init__.py +1 -0
- app/modules/reports/api.py +134 -0
- app/modules/reports/cache.py +74 -0
- app/modules/reports/filters.py +27 -0
- app/modules/reports/repository.py +525 -0
- app/modules/reports/rollup.py +150 -0
- app/modules/reports/rollup_read.py +148 -0
- app/modules/reports/schemas.py +123 -0
- app/modules/reports/service.py +287 -0
- app/modules/reports/thread_identity.py +256 -0
- app/modules/request_logs/__init__.py +0 -0
- app/modules/request_logs/api.py +224 -0
- app/modules/request_logs/cost_backfill.py +154 -0
- app/modules/request_logs/mappers.py +97 -0
- app/modules/request_logs/repository.py +1765 -0
- app/modules/request_logs/schemas.py +145 -0
- app/modules/request_logs/service.py +348 -0
- app/modules/role_mappings/__init__.py +0 -0
- app/modules/role_mappings/api.py +206 -0
- app/modules/role_mappings/matching.py +55 -0
- app/modules/role_mappings/repository.py +106 -0
- app/modules/role_mappings/schemas.py +67 -0
- app/modules/role_mappings/service.py +334 -0
- app/modules/runtime/__init__.py +1 -0
- app/modules/runtime/api.py +18 -0
- app/modules/runtime/schemas.py +14 -0
- app/modules/runtime/service.py +218 -0
- app/modules/scim/__init__.py +1 -0
- app/modules/scim/api.py +194 -0
- app/modules/scim/dependencies.py +112 -0
- app/modules/scim/errors.py +77 -0
- app/modules/scim/management_api.py +156 -0
- app/modules/scim/repository.py +212 -0
- app/modules/scim/schemas.py +145 -0
- app/modules/scim/service.py +786 -0
- app/modules/scim/tokens.py +43 -0
- app/modules/settings/__init__.py +1 -0
- app/modules/settings/api.py +1677 -0
- app/modules/settings/repository.py +606 -0
- app/modules/settings/schemas.py +496 -0
- app/modules/settings/service.py +769 -0
- app/modules/shared/__init__.py +0 -0
- app/modules/shared/schemas.py +22 -0
- app/modules/sticky_sessions/__init__.py +1 -0
- app/modules/sticky_sessions/api.py +118 -0
- app/modules/sticky_sessions/cleanup_scheduler.py +487 -0
- app/modules/sticky_sessions/schemas.py +78 -0
- app/modules/sticky_sessions/service.py +196 -0
- app/modules/telemetry/__init__.py +5 -0
- app/modules/telemetry/api.py +115 -0
- app/modules/telemetry/clients.py +62 -0
- app/modules/telemetry/consent.py +115 -0
- app/modules/telemetry/scheduler.py +85 -0
- app/modules/telemetry/schemas.py +163 -0
- app/modules/telemetry/sender.py +221 -0
- app/modules/telemetry/snapshot.py +490 -0
- app/modules/usage/__init__.py +0 -0
- app/modules/usage/additional_quota_keys.py +379 -0
- app/modules/usage/api.py +45 -0
- app/modules/usage/background_repository.py +147 -0
- app/modules/usage/builders.py +559 -0
- app/modules/usage/depletion_service.py +277 -0
- app/modules/usage/live_ingest.py +356 -0
- app/modules/usage/mappers.py +29 -0
- app/modules/usage/plan_downgrade_observations.py +488 -0
- app/modules/usage/repository.py +1890 -0
- app/modules/usage/schemas.py +68 -0
- app/modules/usage/service.py +102 -0
- app/modules/usage/updater.py +1488 -0
- app/static/assets/Trans-Cj-J9mVA.js +1 -0
- app/static/assets/access-page-DHzZiXZi.js +1 -0
- app/static/assets/account-cost-donut-DGhfA4U8.js +1 -0
- app/static/assets/account-identifiers-CxbdLshZ.js +1 -0
- app/static/assets/account-status-DIsU5sUS.js +1 -0
- app/static/assets/account-trend-chart-BptlfwwQ.js +1 -0
- app/static/assets/accounts-page-Bbai8ift.js +3 -0
- app/static/assets/api-Dwa7XxPe.js +1 -0
- app/static/assets/api-client-dugX1Pti.js +55 -0
- app/static/assets/api-key-create-dialog-DnRm0oYN.js +1 -0
- app/static/assets/api-key-created-dialog-B6jkDG9f.js +1 -0
- app/static/assets/api-key-edit-dialog-Ck3Bnc3V.js +1 -0
- app/static/assets/api-trend-chart-BchCw7Hj.js +1 -0
- app/static/assets/apis-page-BvXz9c6i.js +2 -0
- app/static/assets/arrow-up-Dqh_KzJo.js +1 -0
- app/static/assets/arrow-up-down-LBRgVDGY.js +1 -0
- app/static/assets/automations-page-DQgNAJOR.js +3 -0
- app/static/assets/badge-DqMb9es0.js +1 -0
- app/static/assets/blur-email-HEvKagG2.js +1 -0
- app/static/assets/button-BjVmmiGW.js +1 -0
- app/static/assets/checkbox-apsl9vzk.js +1 -0
- app/static/assets/chevron-left-1d_pHVa5.js +1 -0
- app/static/assets/circle-alert-BS0xd_o1.js +1 -0
- app/static/assets/clsx-DB0hHKMi.js +1 -0
- app/static/assets/colors-BBy8GEtr.js +1 -0
- app/static/assets/copy-button-D7nAvOKR.js +1 -0
- app/static/assets/cost-per-day-chart-YdqlkZVM.js +1 -0
- app/static/assets/createLucideIcon-DBLzLNh2.js +1 -0
- app/static/assets/dashboard-page-CSdhBy0f.js +2 -0
- app/static/assets/distribution-metric-format-yZTy1ZEu.js +1 -0
- app/static/assets/donut-chart-Bxc69cra.js +1 -0
- app/static/assets/download-kQteIcsB.js +1 -0
- app/static/assets/ellipsis-B_5OgIre.js +1 -0
- app/static/assets/empty-state-JcA7tc8m.js +1 -0
- app/static/assets/es2015-bMH8YKZT.js +41 -0
- app/static/assets/es6-B264ni6W.js +69 -0
- app/static/assets/external-link-UVAOgMQ0.js +1 -0
- app/static/assets/i18n-BWqZ9Ua2.js +1 -0
- app/static/assets/inbox-B_RM2s1J.js +1 -0
- app/static/assets/index-DIpd264Z.js +23 -0
- app/static/assets/index-DMeLKRJx.css +2 -0
- app/static/assets/inherit-badge-QhTYVaU6.js +1 -0
- app/static/assets/invite-dialog-0CYh0GiQ.js +1 -0
- app/static/assets/iso-Bpe5Ihbb.js +1 -0
- app/static/assets/label-CGaLmLNf.js +1 -0
- app/static/assets/lazy-recharts-Pv8p9ArM.js +2 -0
- app/static/assets/loading-overlay-CzKQfHLI.js +1 -0
- app/static/assets/mini-quota-bar-cGT_t8Ae.js +1 -0
- app/static/assets/model-distribution-donut-BKjAfT48.js +1 -0
- app/static/assets/model-source-multi-select-D2d06Vc7.js +1 -0
- app/static/assets/multi-select-filter-KIA5qU6Z.js +1 -0
- app/static/assets/oauth-dialog-XeaGGDjS.js +1 -0
- app/static/assets/pagination-controls-D1Ypd0RU.js +1 -0
- app/static/assets/payload-CIYnGZz9.js +1 -0
- app/static/assets/queue-wait-chart-VoV7Yg9L.js +1 -0
- app/static/assets/report-chart-card-BXDliwcc.js +1 -0
- app/static/assets/reports-page-CGCJeMHG.js +3 -0
- app/static/assets/reset-credit-confirm-dialog-QOifTnVB.js +1 -0
- app/static/assets/rolldown-runtime-hePW80VL.js +1 -0
- app/static/assets/search-HfhqwERt.js +1 -0
- app/static/assets/select-Cl33bz-g.js +1 -0
- app/static/assets/settings-page-mZj01HYl.js +2 -0
- app/static/assets/shim-CZwyM4Q7.js +1 -0
- app/static/assets/skeleton-C_EUc4Rp.js +1 -0
- app/static/assets/sparkline-chart-DdNNsddM.js +1 -0
- app/static/assets/status-badge-CHHA5X6c.js +1 -0
- app/static/assets/switch-CzxgokcM.js +1 -0
- app/static/assets/table-DXaNSQ0G.js +1 -0
- app/static/assets/time-to-first-token-chart--68L0u_v.js +1 -0
- app/static/assets/tokens-per-day-chart-Cs82BFuE.js +1 -0
- app/static/assets/tokens-per-second-chart-AD2dfOn6.js +1 -0
- app/static/assets/totp-settings-CRHtxQ7j.js +1 -0
- app/static/assets/trash-D3KyWr1h.js +1 -0
- app/static/assets/use-account-quota-display-BPmrTBTC.js +1 -0
- app/static/assets/use-accounts-PWlVGw3z.js +1 -0
- app/static/assets/use-auth-CuDlW3iC.js +1 -0
- app/static/assets/use-chart-colors-DIu6tClI.js +1 -0
- app/static/assets/use-model-sources-Bul24JBu.js +1 -0
- app/static/assets/use-models-DUCPUuZg.js +1 -0
- app/static/assets/use-reduced-motion-hAm5r6_d.js +1 -0
- app/static/assets/useTranslation--56nHhlH.js +1 -0
- app/static/assets/useragent-distribution-donut-Dup6sf_n.js +1 -0
- app/static/assets/users-uIC3TIlE.js +1 -0
- app/static/assets/vendor-query-_bHZuxEx.js +1 -0
- app/static/assets/vendor-react-BVL0YZPW.js +9 -0
- app/static/favicon.svg +10 -0
- app/static/fonts/GeistSans-Variable.woff2 +0 -0
- app/static/fonts/jetbrains-mono-latin-ext.woff2 +0 -0
- app/static/fonts/jetbrains-mono-latin.woff2 +0 -0
- app/static/index.html +29 -0
- codexlb_up-1.25.0b10.dist-info/METADATA +124 -0
- codexlb_up-1.25.0b10.dist-info/RECORD +827 -0
- codexlb_up-1.25.0b10.dist-info/WHEEL +4 -0
- codexlb_up-1.25.0b10.dist-info/entry_points.txt +3 -0
- codexlb_up-1.25.0b10.dist-info/licenses/LICENSE +21 -0
- config/additional_quota_registry.json +11 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
from enum import Enum
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import Select, func, select, true, update
|
|
9
|
+
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
|
10
|
+
from sqlalchemy.dialects.sqlite import insert as sqlite_insert
|
|
11
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
12
|
+
|
|
13
|
+
from app.core.utils.time import utcnow
|
|
14
|
+
from app.db.models import Account, AccountUsageRollup, AccountUsageRollupState, ApiKey, ApiKeyUsageRollup, RequestLog
|
|
15
|
+
from app.db.session import get_background_session, sqlite_writer_section
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
# Rows younger than the lag stay on the live side of the fold boundary.
|
|
20
|
+
# The lag MUST exceed the maximum possible distance between a log row's
|
|
21
|
+
# requested_at and the moment its insert becomes visible: a row landing below
|
|
22
|
+
# an already-advanced watermark is neither folded nor in the live tail and
|
|
23
|
+
# vanishes from totals. Every insert path stamps requested_at inside
|
|
24
|
+
# ``RequestLogsRepository.add_log`` at write time (``requested_at or
|
|
25
|
+
# utcnow()``; no live caller passes an explicit value — the parameter exists
|
|
26
|
+
# for tests), so the distance is only clock skew between replicas plus the
|
|
27
|
+
# single-row insert transaction's commit latency, NOT the request duration:
|
|
28
|
+
# a stream-end log write is dated at the write, not at the request start.
|
|
29
|
+
# Measured over one full production history (6.0M rows) the worst insert
|
|
30
|
+
# landed 7.9s below the requested_at frontier (p99.9 = 30ms). Post-insert
|
|
31
|
+
# mutators need no allowance here either: ``update_model_for_request``
|
|
32
|
+
# skips rows at/below the watermarks, and consolidation/deletion reassign
|
|
33
|
+
# logs under the fold-state lock while mirroring the folded sums.
|
|
34
|
+
#
|
|
35
|
+
# 2h keeps a ~900x margin over the observed worst case (covering scheduler
|
|
36
|
+
# stalls, NTP drift, paused VMs) while bounding the raw tail every account
|
|
37
|
+
# and API-key summary read must dedupe and re-aggregate; the previous 24h
|
|
38
|
+
# lag — sized for a stream-start dating scheme this codebase never actually
|
|
39
|
+
# had — left an always-rescanned tail of ~660k rows (11% of the table) on
|
|
40
|
+
# the reference deployment and degraded the listing aggregate to a full
|
|
41
|
+
# seq-scan hash join (measured 60s cold / 2.1s warm vs 63ms at 2h).
|
|
42
|
+
#
|
|
43
|
+
# This lag is a shared visibility contract: the hourly/conversation fold
|
|
44
|
+
# targets and the retention min-gate (``now - 2 * FOLD_LAG`` freshness,
|
|
45
|
+
# ``watermark - FOLD_LAG`` prune floor) derive from the same constant.
|
|
46
|
+
# A fold pass after an upgrade from the 24h lag absorbs the watermark jump
|
|
47
|
+
# as one ordinary backfill slice (FOLD_SLICE bounds it).
|
|
48
|
+
FOLD_LAG = timedelta(hours=2)
|
|
49
|
+
# Historical backfill folds at most this much history per transaction.
|
|
50
|
+
FOLD_SLICE = timedelta(days=7)
|
|
51
|
+
|
|
52
|
+
_STATE_ROW_ID = 1
|
|
53
|
+
_EPOCH = datetime(1970, 1, 1)
|
|
54
|
+
_EXCLUDED_REQUEST_KINDS = ("warmup", "limit_warmup")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True, slots=True)
|
|
58
|
+
class UsageRollupSums:
|
|
59
|
+
request_count: int
|
|
60
|
+
input_tokens: int
|
|
61
|
+
output_tokens: int
|
|
62
|
+
cached_input_tokens: int
|
|
63
|
+
total_cost_usd: float
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def deduped_usage_aggregate_stmt(
|
|
67
|
+
*,
|
|
68
|
+
account_ids: list[str] | None = None,
|
|
69
|
+
after_exclusive: datetime | None = None,
|
|
70
|
+
until_inclusive: datetime | None = None,
|
|
71
|
+
) -> Select[tuple[str | None, int, int | None, int, int | None, float | None]]:
|
|
72
|
+
"""Per-account usage aggregate collapsing duplicate rows to the latest id.
|
|
73
|
+
|
|
74
|
+
Duplicate request-log rows share the exact `(account_id, request_id,
|
|
75
|
+
requested_at)` key (see #904); only the `max(id)` row of each group
|
|
76
|
+
contributes. Warmup kinds and soft-deleted rows are excluded. Bounds are
|
|
77
|
+
`requested_at`-based, so a duplicate group can never straddle them.
|
|
78
|
+
"""
|
|
79
|
+
output_tokens_expr = func.coalesce(RequestLog.output_tokens, RequestLog.reasoning_tokens, 0)
|
|
80
|
+
conditions: list = [
|
|
81
|
+
RequestLog.request_kind.not_in(_EXCLUDED_REQUEST_KINDS),
|
|
82
|
+
RequestLog.deleted_at.is_(None),
|
|
83
|
+
RequestLog.account_id.is_not(None),
|
|
84
|
+
]
|
|
85
|
+
if account_ids:
|
|
86
|
+
conditions.append(RequestLog.account_id.in_(account_ids))
|
|
87
|
+
if after_exclusive is not None:
|
|
88
|
+
conditions.append(RequestLog.requested_at > after_exclusive)
|
|
89
|
+
if until_inclusive is not None:
|
|
90
|
+
conditions.append(RequestLog.requested_at <= until_inclusive)
|
|
91
|
+
|
|
92
|
+
latest_request_log_ids = (
|
|
93
|
+
select(func.max(RequestLog.id).label("request_log_id"))
|
|
94
|
+
.where(*conditions)
|
|
95
|
+
.group_by(
|
|
96
|
+
RequestLog.account_id,
|
|
97
|
+
RequestLog.request_id,
|
|
98
|
+
RequestLog.requested_at,
|
|
99
|
+
)
|
|
100
|
+
.subquery("latest_request_log_ids")
|
|
101
|
+
)
|
|
102
|
+
return (
|
|
103
|
+
select(
|
|
104
|
+
RequestLog.account_id,
|
|
105
|
+
func.count(RequestLog.id).label("request_count"),
|
|
106
|
+
func.coalesce(func.sum(RequestLog.input_tokens), 0).label("input_tokens"),
|
|
107
|
+
func.coalesce(func.sum(output_tokens_expr), 0).label("output_tokens"),
|
|
108
|
+
func.coalesce(func.sum(RequestLog.cached_input_tokens), 0).label("cached_input_tokens"),
|
|
109
|
+
func.coalesce(func.sum(RequestLog.cost_usd), 0.0).label("total_cost_usd"),
|
|
110
|
+
)
|
|
111
|
+
.join(latest_request_log_ids, RequestLog.id == latest_request_log_ids.c.request_log_id)
|
|
112
|
+
.group_by(RequestLog.account_id)
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def api_key_usage_aggregate_stmt(
|
|
117
|
+
*,
|
|
118
|
+
api_key_ids: list[str] | None = None,
|
|
119
|
+
after_exclusive: datetime | None = None,
|
|
120
|
+
until_inclusive: datetime | None = None,
|
|
121
|
+
) -> Select[tuple[str | None, int, int | None, int, int | None, float | None]]:
|
|
122
|
+
"""Per-API-key usage aggregate matching the API-key summary semantics:
|
|
123
|
+
no duplicate collapsing, soft-deleted rows included, warmup kinds
|
|
124
|
+
excluded. Bounds are `requested_at`-based, governed by the same fold
|
|
125
|
+
watermark as the account rollup."""
|
|
126
|
+
output_tokens_expr = func.coalesce(RequestLog.output_tokens, RequestLog.reasoning_tokens, 0)
|
|
127
|
+
conditions: list = [
|
|
128
|
+
RequestLog.api_key_id.is_not(None),
|
|
129
|
+
RequestLog.request_kind.not_in(_EXCLUDED_REQUEST_KINDS),
|
|
130
|
+
]
|
|
131
|
+
if api_key_ids:
|
|
132
|
+
conditions.append(RequestLog.api_key_id.in_(api_key_ids))
|
|
133
|
+
if after_exclusive is not None:
|
|
134
|
+
conditions.append(RequestLog.requested_at > after_exclusive)
|
|
135
|
+
if until_inclusive is not None:
|
|
136
|
+
conditions.append(RequestLog.requested_at <= until_inclusive)
|
|
137
|
+
return (
|
|
138
|
+
select(
|
|
139
|
+
RequestLog.api_key_id,
|
|
140
|
+
func.count(RequestLog.id).label("request_count"),
|
|
141
|
+
func.coalesce(func.sum(RequestLog.input_tokens), 0).label("input_tokens"),
|
|
142
|
+
func.coalesce(func.sum(output_tokens_expr), 0).label("output_tokens"),
|
|
143
|
+
func.coalesce(func.sum(RequestLog.cached_input_tokens), 0).label("cached_input_tokens"),
|
|
144
|
+
func.coalesce(func.sum(RequestLog.cost_usd), 0.0).label("total_cost_usd"),
|
|
145
|
+
)
|
|
146
|
+
.where(*conditions)
|
|
147
|
+
.group_by(RequestLog.api_key_id)
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
_SUM_COLUMNS = ("request_count", "input_tokens", "output_tokens", "cached_input_tokens", "total_cost_usd")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _add_rollup_sums_stmt(session: AsyncSession, model, key_field: str, key_value: str, sums: UsageRollupSums):
|
|
155
|
+
stmt = _insert_fn(session)(model).values(
|
|
156
|
+
**{key_field: key_value},
|
|
157
|
+
request_count=sums.request_count,
|
|
158
|
+
input_tokens=sums.input_tokens,
|
|
159
|
+
output_tokens=sums.output_tokens,
|
|
160
|
+
cached_input_tokens=sums.cached_input_tokens,
|
|
161
|
+
total_cost_usd=sums.total_cost_usd,
|
|
162
|
+
)
|
|
163
|
+
return stmt.on_conflict_do_update(
|
|
164
|
+
index_elements=[getattr(model, key_field)],
|
|
165
|
+
set_={column: getattr(model, column) + getattr(stmt.excluded, column) for column in _SUM_COLUMNS},
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _add_sums_stmt(session: AsyncSession, account_id: str, sums: UsageRollupSums):
|
|
170
|
+
return _add_rollup_sums_stmt(session, AccountUsageRollup, "account_id", account_id, sums)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
async def lock_fold_state(session: AsyncSession) -> None:
|
|
174
|
+
"""Serialize the caller's transaction against fold passes.
|
|
175
|
+
|
|
176
|
+
Bootstraps the state row if missing (no commit — the caller's
|
|
177
|
+
transaction owns the insert) and takes its row lock, so a transaction
|
|
178
|
+
that reassigns request-log ownership cannot interleave with a fold
|
|
179
|
+
slice: an in-flight fold commits first, or the fold waits and then
|
|
180
|
+
aggregates the post-commit attribution. Without this, a fold running
|
|
181
|
+
concurrently with duplicate-account consolidation can attribute the
|
|
182
|
+
duplicates' logs to a row that consolidation is about to delete,
|
|
183
|
+
leaving those logs behind the watermark and counted nowhere.
|
|
184
|
+
"""
|
|
185
|
+
await session.execute(_state_bootstrap_stmt(session))
|
|
186
|
+
await _locked_state(session)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
async def merge_rollups_into(session: AsyncSession, canonical_account_id: str, duplicate_ids: list[str]) -> None:
|
|
190
|
+
"""Fold duplicate accounts' rollup sums into the canonical account.
|
|
191
|
+
|
|
192
|
+
Must run in the same transaction that reassigns the duplicates' request
|
|
193
|
+
logs, so folded history follows the logs to the canonical account. The
|
|
194
|
+
caller must hold the fold-state lock (`lock_fold_state`) before its
|
|
195
|
+
first request-log reassignment.
|
|
196
|
+
"""
|
|
197
|
+
if not duplicate_ids:
|
|
198
|
+
return
|
|
199
|
+
duplicates = (
|
|
200
|
+
(await session.execute(select(AccountUsageRollup).where(AccountUsageRollup.account_id.in_(duplicate_ids))))
|
|
201
|
+
.scalars()
|
|
202
|
+
.all()
|
|
203
|
+
)
|
|
204
|
+
if not duplicates:
|
|
205
|
+
return
|
|
206
|
+
merged = UsageRollupSums(
|
|
207
|
+
request_count=sum(row.request_count for row in duplicates),
|
|
208
|
+
input_tokens=sum(row.input_tokens for row in duplicates),
|
|
209
|
+
output_tokens=sum(row.output_tokens for row in duplicates),
|
|
210
|
+
cached_input_tokens=sum(row.cached_input_tokens for row in duplicates),
|
|
211
|
+
total_cost_usd=sum(row.total_cost_usd for row in duplicates),
|
|
212
|
+
)
|
|
213
|
+
for row in duplicates:
|
|
214
|
+
await session.delete(row)
|
|
215
|
+
await session.flush()
|
|
216
|
+
await session.execute(_add_sums_stmt(session, canonical_account_id, merged))
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class AccountUsageRollupRepository:
|
|
220
|
+
def __init__(self, session: AsyncSession) -> None:
|
|
221
|
+
self._session = session
|
|
222
|
+
|
|
223
|
+
async def read_state(
|
|
224
|
+
self, account_ids: list[str] | None = None
|
|
225
|
+
) -> tuple[dict[str, UsageRollupSums], datetime | None]:
|
|
226
|
+
"""Read rollup sums and the fold watermark in ONE statement.
|
|
227
|
+
|
|
228
|
+
A single statement sees a single snapshot even under READ COMMITTED,
|
|
229
|
+
so a fold slice committing concurrently can never yield sums from one
|
|
230
|
+
watermark generation and a watermark from another (which would drop
|
|
231
|
+
the just-folded window from the totals).
|
|
232
|
+
"""
|
|
233
|
+
join_on = AccountUsageRollup.account_id.in_(account_ids) if account_ids else true()
|
|
234
|
+
stmt = (
|
|
235
|
+
select(
|
|
236
|
+
AccountUsageRollupState.folded_through,
|
|
237
|
+
AccountUsageRollup.account_id,
|
|
238
|
+
AccountUsageRollup.request_count,
|
|
239
|
+
AccountUsageRollup.input_tokens,
|
|
240
|
+
AccountUsageRollup.output_tokens,
|
|
241
|
+
AccountUsageRollup.cached_input_tokens,
|
|
242
|
+
AccountUsageRollup.total_cost_usd,
|
|
243
|
+
)
|
|
244
|
+
.select_from(AccountUsageRollupState)
|
|
245
|
+
.outerjoin(AccountUsageRollup, join_on)
|
|
246
|
+
.where(AccountUsageRollupState.id == _STATE_ROW_ID)
|
|
247
|
+
)
|
|
248
|
+
rows = (await self._session.execute(stmt)).all()
|
|
249
|
+
if not rows:
|
|
250
|
+
return {}, None
|
|
251
|
+
watermark = rows[0][0]
|
|
252
|
+
sums = {
|
|
253
|
+
account_id: UsageRollupSums(
|
|
254
|
+
request_count=request_count,
|
|
255
|
+
input_tokens=input_tokens,
|
|
256
|
+
output_tokens=output_tokens,
|
|
257
|
+
cached_input_tokens=cached_input_tokens,
|
|
258
|
+
total_cost_usd=total_cost_usd,
|
|
259
|
+
)
|
|
260
|
+
for (_, account_id, request_count, input_tokens, output_tokens, cached_input_tokens, total_cost_usd) in rows
|
|
261
|
+
if account_id is not None
|
|
262
|
+
}
|
|
263
|
+
return sums, watermark
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
async def read_api_key_rollup_state(
|
|
267
|
+
session: AsyncSession, api_key_ids: list[str] | None = None
|
|
268
|
+
) -> tuple[dict[str, UsageRollupSums], datetime | None]:
|
|
269
|
+
"""Read API-key rollup sums and the fold watermark in ONE statement.
|
|
270
|
+
|
|
271
|
+
Same snapshot-consistency reasoning as
|
|
272
|
+
`AccountUsageRollupRepository.read_state`.
|
|
273
|
+
"""
|
|
274
|
+
join_on = ApiKeyUsageRollup.api_key_id.in_(api_key_ids) if api_key_ids else true()
|
|
275
|
+
stmt = (
|
|
276
|
+
select(
|
|
277
|
+
AccountUsageRollupState.folded_through,
|
|
278
|
+
ApiKeyUsageRollup.api_key_id,
|
|
279
|
+
ApiKeyUsageRollup.request_count,
|
|
280
|
+
ApiKeyUsageRollup.input_tokens,
|
|
281
|
+
ApiKeyUsageRollup.output_tokens,
|
|
282
|
+
ApiKeyUsageRollup.cached_input_tokens,
|
|
283
|
+
ApiKeyUsageRollup.total_cost_usd,
|
|
284
|
+
)
|
|
285
|
+
.select_from(AccountUsageRollupState)
|
|
286
|
+
.outerjoin(ApiKeyUsageRollup, join_on)
|
|
287
|
+
.where(AccountUsageRollupState.id == _STATE_ROW_ID)
|
|
288
|
+
)
|
|
289
|
+
rows = (await session.execute(stmt)).all()
|
|
290
|
+
if not rows:
|
|
291
|
+
return {}, None
|
|
292
|
+
watermark = rows[0][0]
|
|
293
|
+
sums = {
|
|
294
|
+
api_key_id: UsageRollupSums(
|
|
295
|
+
request_count=request_count,
|
|
296
|
+
input_tokens=input_tokens,
|
|
297
|
+
output_tokens=output_tokens,
|
|
298
|
+
cached_input_tokens=cached_input_tokens,
|
|
299
|
+
total_cost_usd=total_cost_usd,
|
|
300
|
+
)
|
|
301
|
+
for (_, api_key_id, request_count, input_tokens, output_tokens, cached_input_tokens, total_cost_usd) in rows
|
|
302
|
+
if api_key_id is not None
|
|
303
|
+
}
|
|
304
|
+
return sums, watermark
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class _FoldStatus(Enum):
|
|
308
|
+
DONE = "done"
|
|
309
|
+
CONTINUE = "continue"
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _insert_fn(session: AsyncSession):
|
|
313
|
+
dialect = session.get_bind().dialect.name
|
|
314
|
+
if dialect == "postgresql":
|
|
315
|
+
return pg_insert
|
|
316
|
+
if dialect == "sqlite":
|
|
317
|
+
return sqlite_insert
|
|
318
|
+
raise RuntimeError(f"AccountUsageRollup upsert unsupported for dialect={dialect!r}")
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _state_bootstrap_stmt(session: AsyncSession):
|
|
322
|
+
# `upgrade_repair_from` is explicitly NULL: a state row bootstrapped by
|
|
323
|
+
# THIS (post-cancelled_count) code is never legacy-suspect. Old code's
|
|
324
|
+
# bootstrap omits the column and gets the epoch server default, which is
|
|
325
|
+
# exactly what marks its post-migration backfill for repair (#1552).
|
|
326
|
+
stmt = _insert_fn(session)(AccountUsageRollupState).values(
|
|
327
|
+
id=_STATE_ROW_ID,
|
|
328
|
+
folded_through=_EPOCH,
|
|
329
|
+
upgrade_repair_from=None,
|
|
330
|
+
)
|
|
331
|
+
return stmt.on_conflict_do_nothing(index_elements=[AccountUsageRollupState.id])
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
async def _locked_state(session: AsyncSession) -> AccountUsageRollupState | None:
|
|
335
|
+
return (
|
|
336
|
+
await session.execute(
|
|
337
|
+
select(AccountUsageRollupState).where(AccountUsageRollupState.id == _STATE_ROW_ID).with_for_update()
|
|
338
|
+
)
|
|
339
|
+
).scalar_one_or_none()
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
async def run_fold_pass(*, now: datetime | None = None) -> int:
|
|
343
|
+
"""Advance the rollup watermark to `now - FOLD_LAG` in bounded slices.
|
|
344
|
+
|
|
345
|
+
Each slice commits in its own transaction so historical backfill never
|
|
346
|
+
holds one giant transaction. Returns the number of committed slices.
|
|
347
|
+
"""
|
|
348
|
+
target = (now or utcnow()) - FOLD_LAG
|
|
349
|
+
committed = 0
|
|
350
|
+
while True:
|
|
351
|
+
async with get_background_session() as session:
|
|
352
|
+
status, wrote = await _fold_next_slice(session, target)
|
|
353
|
+
if wrote:
|
|
354
|
+
committed += 1
|
|
355
|
+
if status is _FoldStatus.DONE:
|
|
356
|
+
return committed
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
async def _fold_next_slice(session: AsyncSession, target: datetime) -> tuple[_FoldStatus, bool]:
|
|
360
|
+
async with sqlite_writer_section():
|
|
361
|
+
# The state row is seeded by the migration, so FOR UPDATE always has a
|
|
362
|
+
# row to lock: concurrent fold passes (second replica, expired leader
|
|
363
|
+
# lease, operator-triggered re-backfill) serialize here and re-read
|
|
364
|
+
# the advanced watermark instead of folding the same window twice.
|
|
365
|
+
state = await _locked_state(session)
|
|
366
|
+
if state is None:
|
|
367
|
+
# Databases created via metadata.create_all (tests, recovery
|
|
368
|
+
# paths) have the table but not the migration-seeded row;
|
|
369
|
+
# bootstrap it. ON CONFLICT DO NOTHING keeps concurrent
|
|
370
|
+
# bootstrappers serialized on the unique id.
|
|
371
|
+
await session.execute(_state_bootstrap_stmt(session))
|
|
372
|
+
await session.commit()
|
|
373
|
+
state = await _locked_state(session)
|
|
374
|
+
if state is None:
|
|
375
|
+
logger.warning("account_usage_rollup_state row missing; skipping fold pass")
|
|
376
|
+
return _FoldStatus.DONE, False
|
|
377
|
+
watermark = state.folded_through
|
|
378
|
+
if watermark >= target:
|
|
379
|
+
return _FoldStatus.DONE, False
|
|
380
|
+
|
|
381
|
+
start = watermark
|
|
382
|
+
# Earliest COUNTABLE row: an old prefix of rows neither aggregate can
|
|
383
|
+
# count would otherwise anchor the backfill start and make passes walk
|
|
384
|
+
# empty 7-day slices while holding the fold-state lock. A row counts
|
|
385
|
+
# if the ACCOUNT aggregate sees it (non-warmup, live, account
|
|
386
|
+
# attached) or the API-KEY aggregate sees it (non-warmup, key
|
|
387
|
+
# attached — soft-deleted rows included), so take the least of both.
|
|
388
|
+
account_earliest = (
|
|
389
|
+
await session.execute(
|
|
390
|
+
select(func.min(RequestLog.requested_at)).where(
|
|
391
|
+
RequestLog.request_kind.not_in(_EXCLUDED_REQUEST_KINDS),
|
|
392
|
+
RequestLog.deleted_at.is_(None),
|
|
393
|
+
RequestLog.account_id.is_not(None),
|
|
394
|
+
)
|
|
395
|
+
)
|
|
396
|
+
).scalar_one_or_none()
|
|
397
|
+
key_earliest = (
|
|
398
|
+
await session.execute(
|
|
399
|
+
select(func.min(RequestLog.requested_at)).where(
|
|
400
|
+
RequestLog.request_kind.not_in(_EXCLUDED_REQUEST_KINDS),
|
|
401
|
+
RequestLog.api_key_id.is_not(None),
|
|
402
|
+
)
|
|
403
|
+
)
|
|
404
|
+
).scalar_one_or_none()
|
|
405
|
+
candidates = [value for value in (account_earliest, key_earliest) if value is not None]
|
|
406
|
+
earliest = min(candidates) if candidates else None
|
|
407
|
+
if earliest is None:
|
|
408
|
+
# Nothing to fold and nothing to skip past; leave the watermark so
|
|
409
|
+
# backdated inserts (if any ever appear) still fold later.
|
|
410
|
+
return _FoldStatus.DONE, False
|
|
411
|
+
if earliest > start:
|
|
412
|
+
start = earliest - timedelta(seconds=1)
|
|
413
|
+
if start >= target:
|
|
414
|
+
return _FoldStatus.DONE, False
|
|
415
|
+
|
|
416
|
+
async def _window_aggregates(window_start: datetime, window_end: datetime):
|
|
417
|
+
account_rows = (
|
|
418
|
+
await session.execute(
|
|
419
|
+
deduped_usage_aggregate_stmt(after_exclusive=window_start, until_inclusive=window_end)
|
|
420
|
+
)
|
|
421
|
+
).all()
|
|
422
|
+
key_rows = (
|
|
423
|
+
await session.execute(
|
|
424
|
+
api_key_usage_aggregate_stmt(after_exclusive=window_start, until_inclusive=window_end)
|
|
425
|
+
)
|
|
426
|
+
).all()
|
|
427
|
+
return account_rows, key_rows
|
|
428
|
+
|
|
429
|
+
slice_end = min(start + FOLD_SLICE, target)
|
|
430
|
+
rows, key_rows = await _window_aggregates(start, slice_end)
|
|
431
|
+
# Skip forward over empty stretches of history within this transaction.
|
|
432
|
+
# Both aggregates must be empty: a window may hold only soft-deleted
|
|
433
|
+
# rows, which the API-key sums still count.
|
|
434
|
+
while not rows and not key_rows and slice_end < target:
|
|
435
|
+
start = slice_end
|
|
436
|
+
slice_end = min(start + FOLD_SLICE, target)
|
|
437
|
+
rows, key_rows = await _window_aggregates(start, slice_end)
|
|
438
|
+
|
|
439
|
+
candidate_ids = [account_id for (account_id, *_rest) in rows if account_id]
|
|
440
|
+
existing_ids: set[str] = set()
|
|
441
|
+
if candidate_ids:
|
|
442
|
+
# An account deleted after the aggregate would fail the rollup FK;
|
|
443
|
+
# re-check existence in this transaction. A delete committing in
|
|
444
|
+
# the remaining window still aborts this slice, which is safe: the
|
|
445
|
+
# watermark has not advanced, and the retry re-aggregates without
|
|
446
|
+
# the (now account_id=NULL or removed) rows.
|
|
447
|
+
existing_ids = set(
|
|
448
|
+
(await session.execute(select(Account.id).where(Account.id.in_(candidate_ids)))).scalars().all()
|
|
449
|
+
)
|
|
450
|
+
for account_id, request_count, input_tokens, output_tokens, cached_input_tokens, total_cost_usd in rows:
|
|
451
|
+
if not account_id or account_id not in existing_ids:
|
|
452
|
+
continue
|
|
453
|
+
sums = UsageRollupSums(
|
|
454
|
+
request_count=int(request_count or 0),
|
|
455
|
+
input_tokens=int(input_tokens or 0),
|
|
456
|
+
output_tokens=int(output_tokens or 0),
|
|
457
|
+
cached_input_tokens=int(cached_input_tokens or 0),
|
|
458
|
+
total_cost_usd=float(total_cost_usd or 0.0),
|
|
459
|
+
)
|
|
460
|
+
await session.execute(_add_sums_stmt(session, account_id, sums))
|
|
461
|
+
|
|
462
|
+
candidate_key_ids = [key_id for (key_id, *_rest) in key_rows if key_id]
|
|
463
|
+
existing_key_ids: set[str] = set()
|
|
464
|
+
if candidate_key_ids:
|
|
465
|
+
existing_key_ids = set(
|
|
466
|
+
(await session.execute(select(ApiKey.id).where(ApiKey.id.in_(candidate_key_ids)))).scalars().all()
|
|
467
|
+
)
|
|
468
|
+
for key_id, request_count, input_tokens, output_tokens, cached_input_tokens, total_cost_usd in key_rows:
|
|
469
|
+
if not key_id or key_id not in existing_key_ids:
|
|
470
|
+
continue
|
|
471
|
+
sums = UsageRollupSums(
|
|
472
|
+
request_count=int(request_count or 0),
|
|
473
|
+
input_tokens=int(input_tokens or 0),
|
|
474
|
+
output_tokens=int(output_tokens or 0),
|
|
475
|
+
cached_input_tokens=int(cached_input_tokens or 0),
|
|
476
|
+
total_cost_usd=float(total_cost_usd or 0.0),
|
|
477
|
+
)
|
|
478
|
+
await session.execute(_add_rollup_sums_stmt(session, ApiKeyUsageRollup, "api_key_id", key_id, sums))
|
|
479
|
+
await session.execute(
|
|
480
|
+
update(AccountUsageRollupState)
|
|
481
|
+
.where(AccountUsageRollupState.id == _STATE_ROW_ID)
|
|
482
|
+
.values(folded_through=slice_end)
|
|
483
|
+
)
|
|
484
|
+
await session.commit()
|
|
485
|
+
logger.info("Folded account usage rollups through %s", slice_end.isoformat())
|
|
486
|
+
return (_FoldStatus.DONE if slice_end >= target else _FoldStatus.CONTINUE), True
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import contextlib
|
|
5
|
+
import logging
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
|
|
8
|
+
from app.core.scheduling.leader_election_handle import get_leader_election as _get_leader_election
|
|
9
|
+
from app.modules.accounts.usage_rollup import run_fold_pass
|
|
10
|
+
from app.modules.accounts.usage_time_rollup import run_conversation_fold_pass, run_hourly_fold_pass
|
|
11
|
+
from app.modules.reports.rollup import run_report_fold_pass
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
FOLD_INTERVAL_SECONDS = 900
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(slots=True)
|
|
19
|
+
class AccountUsageRollupScheduler:
|
|
20
|
+
interval_seconds: int
|
|
21
|
+
_task: asyncio.Task[None] | None = None
|
|
22
|
+
_stop: asyncio.Event = field(default_factory=asyncio.Event)
|
|
23
|
+
_lock: asyncio.Lock = field(default_factory=asyncio.Lock)
|
|
24
|
+
|
|
25
|
+
async def start(self) -> None:
|
|
26
|
+
if self._task and not self._task.done():
|
|
27
|
+
return
|
|
28
|
+
self._stop.clear()
|
|
29
|
+
self._task = asyncio.create_task(self._run_loop())
|
|
30
|
+
|
|
31
|
+
async def stop(self) -> None:
|
|
32
|
+
if not self._task:
|
|
33
|
+
return
|
|
34
|
+
self._stop.set()
|
|
35
|
+
self._task.cancel()
|
|
36
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
37
|
+
await self._task
|
|
38
|
+
self._task = None
|
|
39
|
+
|
|
40
|
+
async def _run_loop(self) -> None:
|
|
41
|
+
while not self._stop.is_set():
|
|
42
|
+
await self._fold_once()
|
|
43
|
+
try:
|
|
44
|
+
await asyncio.wait_for(self._stop.wait(), timeout=self.interval_seconds)
|
|
45
|
+
except asyncio.TimeoutError:
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
async def _fold_once(self) -> None:
|
|
49
|
+
await _get_leader_election().run_if_leader(self._fold_as_leader)
|
|
50
|
+
|
|
51
|
+
async def _fold_as_leader(self) -> None:
|
|
52
|
+
async with self._lock:
|
|
53
|
+
try:
|
|
54
|
+
await run_fold_pass()
|
|
55
|
+
except Exception:
|
|
56
|
+
logger.exception("Account usage rollup fold pass failed")
|
|
57
|
+
# Separate try-blocks on purpose: a bug in any one fold must not
|
|
58
|
+
# stop the others — retention only ever pauses, because its prune
|
|
59
|
+
# gate takes the MIN of every watermark.
|
|
60
|
+
try:
|
|
61
|
+
await run_hourly_fold_pass()
|
|
62
|
+
except Exception:
|
|
63
|
+
logger.exception("Hourly usage rollup fold pass failed")
|
|
64
|
+
try:
|
|
65
|
+
await run_conversation_fold_pass()
|
|
66
|
+
except Exception:
|
|
67
|
+
logger.exception("Conversation rollup fold pass failed")
|
|
68
|
+
try:
|
|
69
|
+
await run_report_fold_pass()
|
|
70
|
+
except Exception:
|
|
71
|
+
logger.exception("Report rollup fold pass failed")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def build_account_usage_rollup_scheduler() -> AccountUsageRollupScheduler:
|
|
75
|
+
return AccountUsageRollupScheduler(interval_seconds=FOLD_INTERVAL_SECONDS)
|