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,277 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable, Sequence
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
from typing import TypeAlias
|
|
7
|
+
|
|
8
|
+
from app.core.usage.depletion import (
|
|
9
|
+
EWMAState,
|
|
10
|
+
classify_risk,
|
|
11
|
+
compute_burn_rate,
|
|
12
|
+
compute_depletion_risk,
|
|
13
|
+
compute_safe_usage_percent,
|
|
14
|
+
ewma_update,
|
|
15
|
+
)
|
|
16
|
+
from app.core.utils.time import naive_utc_to_epoch, utcnow
|
|
17
|
+
|
|
18
|
+
# Per-account cache key: (account_id, limit_name, window).
|
|
19
|
+
_StateKey: TypeAlias = tuple[str, str, str]
|
|
20
|
+
_RowEdgeSignature = tuple[int | None, datetime, float, float | None, int | None]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class _HistorySignature:
|
|
25
|
+
row_count: int
|
|
26
|
+
first: _RowEdgeSignature
|
|
27
|
+
latest: _RowEdgeSignature
|
|
28
|
+
content_digest: int | None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class _SignedHistory(list):
|
|
32
|
+
def __init__(self, rows: Iterable, signature: _HistorySignature) -> None:
|
|
33
|
+
super().__init__(rows)
|
|
34
|
+
self.depletion_history_signature = signature
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# In-memory EWMA state: keyed by (account_id, limit_name, window)
|
|
38
|
+
# Persists across requests; resets on process restart.
|
|
39
|
+
_ewma_states: dict[_StateKey, EWMAState] = {}
|
|
40
|
+
# Parallel signature map used to memoize EWMA rebuilds across dashboard polls
|
|
41
|
+
# (issue #537). When the in-window history is unchanged between requests we
|
|
42
|
+
# reuse the cached EWMAState instead of replaying the full history.
|
|
43
|
+
_history_signatures: dict[_StateKey, _HistorySignature] = {}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class DepletionMetrics:
|
|
48
|
+
risk: float
|
|
49
|
+
risk_level: str # "safe" | "warning" | "danger" | "critical"
|
|
50
|
+
rate_per_second: float
|
|
51
|
+
burn_rate: float
|
|
52
|
+
safe_usage_percent: float # budget line position
|
|
53
|
+
projected_exhaustion_at: datetime | None
|
|
54
|
+
seconds_until_exhaustion: float | None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class AggregateDepletionMetrics:
|
|
59
|
+
risk: float
|
|
60
|
+
risk_level: str
|
|
61
|
+
burn_rate: float
|
|
62
|
+
safe_usage_percent: float
|
|
63
|
+
projected_exhaustion_at: datetime | None
|
|
64
|
+
seconds_until_exhaustion: float | None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def compute_depletion_for_account(
|
|
68
|
+
account_id: str,
|
|
69
|
+
limit_name: str,
|
|
70
|
+
window: str,
|
|
71
|
+
history: list, # list of objects with: used_percent, recorded_at, reset_at, window_minutes
|
|
72
|
+
now: datetime | None = None,
|
|
73
|
+
) -> DepletionMetrics | None:
|
|
74
|
+
"""
|
|
75
|
+
Compute depletion metrics for a single account using EWMA.
|
|
76
|
+
|
|
77
|
+
- history: list of usage entries ordered by recorded_at ASC
|
|
78
|
+
- Returns None if insufficient data (<2 data points) or rate is unknown
|
|
79
|
+
- Uses module-level _ewma_states for in-memory state
|
|
80
|
+
"""
|
|
81
|
+
if not history:
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
now = now or utcnow()
|
|
85
|
+
key: _StateKey = (account_id, limit_name, window)
|
|
86
|
+
|
|
87
|
+
if len(history) < 2:
|
|
88
|
+
# Only one in-window sample — seed the EWMA but don't compute
|
|
89
|
+
# depletion. Reset any cached state so we never derive a rate
|
|
90
|
+
# from an out-of-window sample plus this one.
|
|
91
|
+
entry = history[0]
|
|
92
|
+
_ewma_states[key] = ewma_update(
|
|
93
|
+
None, entry.used_percent, naive_utc_to_epoch(entry.recorded_at), reset_at=entry.reset_at
|
|
94
|
+
)
|
|
95
|
+
_history_signatures[key] = _history_signature_from_edges(history)
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
signature = _history_signature(history)
|
|
99
|
+
cached_state = _ewma_states.get(key)
|
|
100
|
+
cached_signature = _history_signatures.get(key)
|
|
101
|
+
|
|
102
|
+
if cached_state is not None and cached_signature == signature:
|
|
103
|
+
# Same in-window history as the last call — reuse the cached EWMA
|
|
104
|
+
# state instead of replaying every row. Time-dependent fields below
|
|
105
|
+
# (risk, safe_usage_percent, projected_exhaustion_at) are still
|
|
106
|
+
# recomputed from `now`, so dashboard polls remain live.
|
|
107
|
+
state: EWMAState | None = cached_state
|
|
108
|
+
else:
|
|
109
|
+
state = _rebuild_ewma_state(history)
|
|
110
|
+
if state is not None:
|
|
111
|
+
_ewma_states[key] = state
|
|
112
|
+
_history_signatures[key] = signature
|
|
113
|
+
|
|
114
|
+
if state is None or state.rate is None:
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
latest = history[-1]
|
|
118
|
+
used_percent = latest.used_percent
|
|
119
|
+
|
|
120
|
+
seconds_until_reset = 0.0
|
|
121
|
+
if latest.reset_at is not None:
|
|
122
|
+
seconds_until_reset = max(0.0, latest.reset_at - naive_utc_to_epoch(now))
|
|
123
|
+
if seconds_until_reset == 0.0:
|
|
124
|
+
# Window has already reset — the stale used_percent is
|
|
125
|
+
# meaningless. Clear EWMA state so next refresh starts fresh.
|
|
126
|
+
_ewma_states.pop(key, None)
|
|
127
|
+
return None
|
|
128
|
+
elif latest.window_minutes is not None:
|
|
129
|
+
# Without reset_at we cannot know when the window started. Use
|
|
130
|
+
# the full window duration as a conservative upper bound rather
|
|
131
|
+
# than guessing from the first observed sample (which may appear
|
|
132
|
+
# mid-window and dramatically underestimate remaining time).
|
|
133
|
+
seconds_until_reset = float(latest.window_minutes * 60)
|
|
134
|
+
|
|
135
|
+
total_window_seconds = (latest.window_minutes * 60) if latest.window_minutes else 0.0
|
|
136
|
+
seconds_elapsed = max(0.0, total_window_seconds - seconds_until_reset)
|
|
137
|
+
|
|
138
|
+
risk = compute_depletion_risk(used_percent, state.rate, seconds_until_reset)
|
|
139
|
+
risk_level = classify_risk(risk)
|
|
140
|
+
burn_rate = compute_burn_rate(state.rate, 100.0 - used_percent, seconds_until_reset)
|
|
141
|
+
safe_pct = compute_safe_usage_percent(seconds_elapsed, total_window_seconds)
|
|
142
|
+
|
|
143
|
+
projected_exhaustion_at = None
|
|
144
|
+
seconds_until_exhaustion = None
|
|
145
|
+
if state.rate > 0 and seconds_until_reset > 0:
|
|
146
|
+
remaining = 100.0 - used_percent
|
|
147
|
+
secs = remaining / state.rate
|
|
148
|
+
if secs <= seconds_until_reset:
|
|
149
|
+
seconds_until_exhaustion = secs
|
|
150
|
+
projected_exhaustion_at = now + timedelta(seconds=secs)
|
|
151
|
+
# else: exhaustion falls after the window resets — leave as None
|
|
152
|
+
|
|
153
|
+
return DepletionMetrics(
|
|
154
|
+
risk=risk,
|
|
155
|
+
risk_level=risk_level,
|
|
156
|
+
rate_per_second=state.rate,
|
|
157
|
+
burn_rate=burn_rate,
|
|
158
|
+
safe_usage_percent=safe_pct,
|
|
159
|
+
projected_exhaustion_at=projected_exhaustion_at,
|
|
160
|
+
seconds_until_exhaustion=seconds_until_exhaustion,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def compute_aggregate_depletion(
|
|
165
|
+
per_account_metrics: Sequence[DepletionMetrics | None],
|
|
166
|
+
) -> AggregateDepletionMetrics | None:
|
|
167
|
+
"""
|
|
168
|
+
Aggregate depletion metrics across accounts using max(risk).
|
|
169
|
+
Returns None if no valid metrics.
|
|
170
|
+
"""
|
|
171
|
+
valid = [m for m in per_account_metrics if m is not None]
|
|
172
|
+
if not valid:
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
# Use all fields from the worst-case account so that risk, safe-line,
|
|
176
|
+
# burn rate, and exhaustion ETA are internally consistent.
|
|
177
|
+
worst = max(valid, key=lambda m: m.risk)
|
|
178
|
+
|
|
179
|
+
return AggregateDepletionMetrics(
|
|
180
|
+
risk=worst.risk,
|
|
181
|
+
risk_level=worst.risk_level,
|
|
182
|
+
burn_rate=worst.burn_rate,
|
|
183
|
+
safe_usage_percent=worst.safe_usage_percent,
|
|
184
|
+
projected_exhaustion_at=worst.projected_exhaustion_at,
|
|
185
|
+
seconds_until_exhaustion=worst.seconds_until_exhaustion,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def reset_ewma_state() -> None:
|
|
190
|
+
"""Clear all in-memory EWMA state. Used for testing."""
|
|
191
|
+
_ewma_states.clear()
|
|
192
|
+
_history_signatures.clear()
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def prune_depletion_cache(active_keys: Iterable[tuple[str, str, str]]) -> None:
|
|
196
|
+
"""Drop EWMA/signature cache entries outside the current account/window set."""
|
|
197
|
+
keep = set(active_keys)
|
|
198
|
+
for key in set(_ewma_states) | set(_history_signatures):
|
|
199
|
+
if key not in keep:
|
|
200
|
+
_ewma_states.pop(key, None)
|
|
201
|
+
_history_signatures.pop(key, None)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _rebuild_ewma_state(history: list) -> EWMAState | None:
|
|
205
|
+
state: EWMAState | None = None
|
|
206
|
+
for entry in history:
|
|
207
|
+
ts = naive_utc_to_epoch(entry.recorded_at)
|
|
208
|
+
state = ewma_update(state, entry.used_percent, ts, reset_at=entry.reset_at)
|
|
209
|
+
return state
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def attach_depletion_history_signature(history: Iterable) -> list:
|
|
213
|
+
"""Return a list carrying a compact content signature for cache checks.
|
|
214
|
+
|
|
215
|
+
Dashboard code already iterates fetched rows while grouping/filtering them;
|
|
216
|
+
attaching the digest there keeps cache-hit checks O(1) and avoids retaining
|
|
217
|
+
a tuple per history row in the module-level cache.
|
|
218
|
+
"""
|
|
219
|
+
rows = list(history)
|
|
220
|
+
return _signed_history_from_rows(rows)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def filter_depletion_history_since(history: Iterable, cutoff: datetime) -> list:
|
|
224
|
+
"""Filter rows by cutoff and attach the cache signature."""
|
|
225
|
+
rows = [entry for entry in history if entry.recorded_at >= cutoff]
|
|
226
|
+
if not rows:
|
|
227
|
+
return []
|
|
228
|
+
return _SignedHistory(rows, _history_signature_from_rows(rows))
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _history_signature(history: list) -> _HistorySignature:
|
|
232
|
+
attached = getattr(history, "depletion_history_signature", None)
|
|
233
|
+
if isinstance(attached, _HistorySignature):
|
|
234
|
+
return attached
|
|
235
|
+
return _history_signature_from_edges(history)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _history_signature_from_rows(history: list) -> _HistorySignature:
|
|
239
|
+
if not history:
|
|
240
|
+
raise ValueError("history must not be empty")
|
|
241
|
+
# One fixed-width hash over every row's value-bearing fields: cheap
|
|
242
|
+
# enough to run per dashboard poll on dense histories, and still
|
|
243
|
+
# detects in-place corrections that leave the edges and count intact.
|
|
244
|
+
# The signature cache is process-local, so a process-local ``hash`` is
|
|
245
|
+
# sufficient; only the int is retained, never the per-row tuples.
|
|
246
|
+
edges = tuple(map(_row_edge_signature, history))
|
|
247
|
+
return _HistorySignature(
|
|
248
|
+
row_count=len(edges),
|
|
249
|
+
first=edges[0],
|
|
250
|
+
latest=edges[-1],
|
|
251
|
+
content_digest=hash(edges),
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _signed_history_from_rows(history: list) -> list:
|
|
256
|
+
return _SignedHistory(history, _history_signature_from_rows(history))
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _history_signature_from_edges(history: Sequence) -> _HistorySignature:
|
|
260
|
+
if not history:
|
|
261
|
+
raise ValueError("history must not be empty")
|
|
262
|
+
return _HistorySignature(
|
|
263
|
+
row_count=len(history),
|
|
264
|
+
first=_row_edge_signature(history[0]),
|
|
265
|
+
latest=_row_edge_signature(history[-1]),
|
|
266
|
+
content_digest=None,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _row_edge_signature(entry) -> _RowEdgeSignature:
|
|
271
|
+
return (
|
|
272
|
+
getattr(entry, "id", None),
|
|
273
|
+
entry.recorded_at,
|
|
274
|
+
entry.used_percent,
|
|
275
|
+
entry.reset_at,
|
|
276
|
+
entry.window_minutes,
|
|
277
|
+
)
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import time
|
|
6
|
+
import weakref
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
from app.core import usage as usage_core
|
|
10
|
+
from app.core.usage.live_hub import register_live_usage_publisher
|
|
11
|
+
from app.core.usage.live_snapshots import LiveRateLimitSnapshot, LiveUsageWindow
|
|
12
|
+
from app.db.session import get_background_session
|
|
13
|
+
from app.modules.proxy.account_cache import get_account_selection_cache
|
|
14
|
+
from app.modules.proxy.rate_limit_cache import get_rate_limit_headers_cache
|
|
15
|
+
from app.modules.usage.repository import UsageRepository, UsageWindowWrite
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
# Write-coalescing tuning (fixed; issue #1340 / PRINCIPLES.md P2). The
|
|
20
|
+
# ingestor keeps both as constructor fields so tests can exercise queue
|
|
21
|
+
# overflow and coalescing with small values.
|
|
22
|
+
_QUEUE_SIZE = 512
|
|
23
|
+
_WRITE_MIN_INTERVAL_SECONDS = 5.0
|
|
24
|
+
_CACHE_INVALIDATION_MIN_INTERVAL_SECONDS = 5.0
|
|
25
|
+
|
|
26
|
+
# Ownership accounting for every task any ingestor instance creates (consumer
|
|
27
|
+
# and trailing cache invalidation), so a task an owner lost track of (a stop
|
|
28
|
+
# cancelled mid-await) can never end in a silently dropped exception:
|
|
29
|
+
#
|
|
30
|
+
# - `_owned_tasks` holds weak references (they never extend task lifetime) so
|
|
31
|
+
# the test suite's leak fence can cancel pending tasks and settle completed
|
|
32
|
+
# ones that some reference chain kept alive across a test boundary.
|
|
33
|
+
# - `_record_owned_task_result` runs as each task's done callback: it
|
|
34
|
+
# retrieves the exception (so the loop's unobserved-task warning can never
|
|
35
|
+
# fire at garbage-collection time), logs it, and records it in the bounded
|
|
36
|
+
# `_owned_task_failures` strong handoff for the fence to drain (#1755).
|
|
37
|
+
#
|
|
38
|
+
# `_settled_owned_tasks` (also weak) marks tasks whose result was already
|
|
39
|
+
# recorded, so the done callback and the fence's sweep of completed tasks
|
|
40
|
+
# settle each task exactly once even when both observe it.
|
|
41
|
+
_owned_tasks: weakref.WeakSet[asyncio.Task[None]] = weakref.WeakSet()
|
|
42
|
+
_settled_owned_tasks: weakref.WeakSet[asyncio.Task[None]] = weakref.WeakSet()
|
|
43
|
+
# (task name, exception repr) pairs. Reprs, not exception objects: a stored
|
|
44
|
+
# exception's traceback would keep the failed ingestor's whole object graph
|
|
45
|
+
# (task, queue, cached state) alive for the process lifetime, since production
|
|
46
|
+
# never drains this record.
|
|
47
|
+
_owned_task_failures: list[tuple[str, str]] = []
|
|
48
|
+
_MAX_OWNED_TASK_FAILURES = 16
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _record_owned_task_result(task: asyncio.Task[None]) -> None:
|
|
52
|
+
if task in _settled_owned_tasks:
|
|
53
|
+
return
|
|
54
|
+
_settled_owned_tasks.add(task)
|
|
55
|
+
_owned_tasks.discard(task)
|
|
56
|
+
if task.cancelled():
|
|
57
|
+
return
|
|
58
|
+
exc = task.exception()
|
|
59
|
+
if exc is None:
|
|
60
|
+
return
|
|
61
|
+
logger.error("Live usage ingestor task %r died unexpectedly", task.get_name(), exc_info=exc)
|
|
62
|
+
if len(_owned_task_failures) < _MAX_OWNED_TASK_FAILURES:
|
|
63
|
+
_owned_task_failures.append((task.get_name(), repr(exc)))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _enroll_owned_task(task: asyncio.Task[None]) -> None:
|
|
67
|
+
_owned_tasks.add(task)
|
|
68
|
+
task.add_done_callback(_record_owned_task_result)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True, slots=True)
|
|
72
|
+
class _QueuedSnapshot:
|
|
73
|
+
account_id: str | None
|
|
74
|
+
chatgpt_account_id: str | None
|
|
75
|
+
snapshot: LiveRateLimitSnapshot
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _fingerprint(snapshot: LiveRateLimitSnapshot) -> tuple[object, ...]:
|
|
79
|
+
def window_key(window: LiveUsageWindow | None) -> tuple[object, ...] | None:
|
|
80
|
+
if window is None:
|
|
81
|
+
return None
|
|
82
|
+
return (round(window.used_percent, 2), window.window_minutes, window.reset_at)
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
window_key(snapshot.primary),
|
|
86
|
+
window_key(snapshot.secondary),
|
|
87
|
+
snapshot.credits_has,
|
|
88
|
+
snapshot.credits_unlimited,
|
|
89
|
+
snapshot.credits_balance,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class LiveUsageIngestor:
|
|
94
|
+
"""Fire-and-forget sink for per-turn rate-limit snapshots.
|
|
95
|
+
|
|
96
|
+
Snapshots ride the serving path, so enqueueing must stay O(1) and never
|
|
97
|
+
raise; a single consumer task owns its own background sessions and writes
|
|
98
|
+
usage-history rows with the same shape the background poller produces.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
def __init__(
|
|
102
|
+
self,
|
|
103
|
+
*,
|
|
104
|
+
queue_size: int,
|
|
105
|
+
write_min_interval_seconds: float,
|
|
106
|
+
) -> None:
|
|
107
|
+
self._queue: asyncio.Queue[_QueuedSnapshot] = asyncio.Queue(maxsize=max(1, queue_size))
|
|
108
|
+
self._write_min_interval_seconds = write_min_interval_seconds
|
|
109
|
+
self._last_write: dict[str, tuple[tuple[object, ...], float]] = {}
|
|
110
|
+
self._consumer: asyncio.Task[None] | None = None
|
|
111
|
+
self._dropped = 0
|
|
112
|
+
self._last_cache_invalidation = 0.0
|
|
113
|
+
self._trailing_invalidation: asyncio.Task[None] | None = None
|
|
114
|
+
|
|
115
|
+
def publish(
|
|
116
|
+
self,
|
|
117
|
+
snapshot: LiveRateLimitSnapshot,
|
|
118
|
+
*,
|
|
119
|
+
account_id: str | None = None,
|
|
120
|
+
chatgpt_account_id: str | None = None,
|
|
121
|
+
) -> None:
|
|
122
|
+
item = _QueuedSnapshot(account_id=account_id, chatgpt_account_id=chatgpt_account_id, snapshot=snapshot)
|
|
123
|
+
if account_id is not None and self._should_skip(account_id, snapshot):
|
|
124
|
+
return
|
|
125
|
+
try:
|
|
126
|
+
self._queue.put_nowait(item)
|
|
127
|
+
except asyncio.QueueFull:
|
|
128
|
+
try:
|
|
129
|
+
self._queue.get_nowait()
|
|
130
|
+
except asyncio.QueueEmpty:
|
|
131
|
+
pass
|
|
132
|
+
self._dropped += 1
|
|
133
|
+
if self._dropped % 100 == 1:
|
|
134
|
+
logger.warning("Live usage ingest queue full; dropped_total=%d", self._dropped)
|
|
135
|
+
try:
|
|
136
|
+
self._queue.put_nowait(item)
|
|
137
|
+
except asyncio.QueueFull:
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
def start(self) -> None:
|
|
141
|
+
if self._consumer is None or self._consumer.done():
|
|
142
|
+
self._consumer = asyncio.create_task(self._run(), name="live-usage-ingestor")
|
|
143
|
+
_enroll_owned_task(self._consumer)
|
|
144
|
+
|
|
145
|
+
def is_running(self) -> bool:
|
|
146
|
+
consumer = self._consumer
|
|
147
|
+
return consumer is not None and not consumer.done()
|
|
148
|
+
|
|
149
|
+
async def stop(self) -> None:
|
|
150
|
+
consumer = self._consumer
|
|
151
|
+
self._consumer = None
|
|
152
|
+
trailing = self._trailing_invalidation
|
|
153
|
+
self._trailing_invalidation = None
|
|
154
|
+
for task in (consumer, trailing):
|
|
155
|
+
if task is not None:
|
|
156
|
+
task.cancel()
|
|
157
|
+
try:
|
|
158
|
+
await task
|
|
159
|
+
except asyncio.CancelledError:
|
|
160
|
+
pass
|
|
161
|
+
|
|
162
|
+
def _should_skip(self, account_id: str, snapshot: LiveRateLimitSnapshot) -> bool:
|
|
163
|
+
last = self._last_write.get(account_id)
|
|
164
|
+
if last is None:
|
|
165
|
+
return False
|
|
166
|
+
fingerprint, written_at = last
|
|
167
|
+
if fingerprint != _fingerprint(snapshot):
|
|
168
|
+
return False
|
|
169
|
+
return time.monotonic() - written_at < self._write_min_interval_seconds
|
|
170
|
+
|
|
171
|
+
async def _run(self) -> None:
|
|
172
|
+
while True:
|
|
173
|
+
item = await self._queue.get()
|
|
174
|
+
try:
|
|
175
|
+
await self._ingest(item)
|
|
176
|
+
except asyncio.CancelledError:
|
|
177
|
+
raise
|
|
178
|
+
except Exception:
|
|
179
|
+
logger.warning(
|
|
180
|
+
"Live usage ingest failed account_id=%s chatgpt_account_id=%s",
|
|
181
|
+
item.account_id,
|
|
182
|
+
item.chatgpt_account_id,
|
|
183
|
+
exc_info=True,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
async def _ingest(self, item: _QueuedSnapshot) -> None:
|
|
187
|
+
snapshot = item.snapshot
|
|
188
|
+
primary = snapshot.primary
|
|
189
|
+
secondary = snapshot.secondary
|
|
190
|
+
monthly: LiveUsageWindow | None = None
|
|
191
|
+
# Mirror the poller's write-time normalization: a lone primary window
|
|
192
|
+
# with the monthly duration is the monthly-only free-plan shape and
|
|
193
|
+
# belongs in the monthly slot, not the primary one.
|
|
194
|
+
if (
|
|
195
|
+
primary is not None
|
|
196
|
+
and secondary is None
|
|
197
|
+
and primary.window_minutes == usage_core.DEFAULT_WINDOW_MINUTES_MONTHLY
|
|
198
|
+
):
|
|
199
|
+
monthly, primary = primary, None
|
|
200
|
+
windows: list[UsageWindowWrite] = []
|
|
201
|
+
if primary is not None:
|
|
202
|
+
windows.append(
|
|
203
|
+
UsageWindowWrite(
|
|
204
|
+
window="primary",
|
|
205
|
+
used_percent=float(primary.used_percent),
|
|
206
|
+
reset_at=primary.reset_at,
|
|
207
|
+
window_minutes=primary.window_minutes,
|
|
208
|
+
credits_has=snapshot.credits_has,
|
|
209
|
+
credits_unlimited=snapshot.credits_unlimited,
|
|
210
|
+
credits_balance=snapshot.credits_balance,
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
if secondary is not None:
|
|
214
|
+
# Mirror the poller: credits normally ride the primary row. A
|
|
215
|
+
# secondary-only snapshot must still carry fresh credit state.
|
|
216
|
+
secondary_carries_credits = primary is None
|
|
217
|
+
windows.append(
|
|
218
|
+
UsageWindowWrite(
|
|
219
|
+
window="secondary",
|
|
220
|
+
used_percent=float(secondary.used_percent),
|
|
221
|
+
reset_at=secondary.reset_at,
|
|
222
|
+
window_minutes=secondary.window_minutes,
|
|
223
|
+
credits_has=snapshot.credits_has if secondary_carries_credits else None,
|
|
224
|
+
credits_unlimited=snapshot.credits_unlimited if secondary_carries_credits else None,
|
|
225
|
+
credits_balance=snapshot.credits_balance if secondary_carries_credits else None,
|
|
226
|
+
)
|
|
227
|
+
)
|
|
228
|
+
if monthly is not None:
|
|
229
|
+
windows.append(
|
|
230
|
+
UsageWindowWrite(
|
|
231
|
+
window="monthly",
|
|
232
|
+
used_percent=float(monthly.used_percent),
|
|
233
|
+
reset_at=monthly.reset_at,
|
|
234
|
+
window_minutes=monthly.window_minutes,
|
|
235
|
+
credits_has=snapshot.credits_has,
|
|
236
|
+
credits_unlimited=snapshot.credits_unlimited,
|
|
237
|
+
credits_balance=snapshot.credits_balance,
|
|
238
|
+
)
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
async with get_background_session() as session:
|
|
242
|
+
account_id = await UsageRepository(session).settle_live_account_snapshot(
|
|
243
|
+
account_id=item.account_id,
|
|
244
|
+
chatgpt_account_id=item.chatgpt_account_id,
|
|
245
|
+
windows=windows,
|
|
246
|
+
should_skip=lambda resolved: self._should_skip(resolved, snapshot),
|
|
247
|
+
)
|
|
248
|
+
if account_id is None:
|
|
249
|
+
return
|
|
250
|
+
self._last_write[account_id] = (_fingerprint(snapshot), time.monotonic())
|
|
251
|
+
await self._invalidate_caches_throttled()
|
|
252
|
+
|
|
253
|
+
async def _invalidate_caches_throttled(self) -> None:
|
|
254
|
+
# Invalidations are throttled, but every write must still be covered:
|
|
255
|
+
# a write inside the throttle window schedules one trailing
|
|
256
|
+
# invalidation at window expiry, so cached selection inputs and
|
|
257
|
+
# downstream x-codex-* headers are stale for at most the throttle
|
|
258
|
+
# interval rather than the header cache TTL.
|
|
259
|
+
now = time.monotonic()
|
|
260
|
+
remaining = _CACHE_INVALIDATION_MIN_INTERVAL_SECONDS - (now - self._last_cache_invalidation)
|
|
261
|
+
if remaining <= 0:
|
|
262
|
+
await self._invalidate_caches_now()
|
|
263
|
+
return
|
|
264
|
+
if self._trailing_invalidation is None or self._trailing_invalidation.done():
|
|
265
|
+
self._trailing_invalidation = asyncio.create_task(
|
|
266
|
+
self._trailing_invalidate(remaining),
|
|
267
|
+
name="live-usage-trailing-invalidation",
|
|
268
|
+
)
|
|
269
|
+
_enroll_owned_task(self._trailing_invalidation)
|
|
270
|
+
|
|
271
|
+
async def _trailing_invalidate(self, delay_seconds: float) -> None:
|
|
272
|
+
await asyncio.sleep(delay_seconds)
|
|
273
|
+
await self._invalidate_caches_now()
|
|
274
|
+
|
|
275
|
+
async def _invalidate_caches_now(self) -> None:
|
|
276
|
+
self._last_cache_invalidation = time.monotonic()
|
|
277
|
+
get_account_selection_cache().invalidate()
|
|
278
|
+
# Downstream x-codex-* headers are served from a TTL cache that only
|
|
279
|
+
# the poller invalidates otherwise; drop it so clients see the live
|
|
280
|
+
# values before the TTL expires.
|
|
281
|
+
await get_rate_limit_headers_cache().invalidate()
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
_ingestor: LiveUsageIngestor | None = None
|
|
285
|
+
# Registrations a nested startup displaced, innermost-last. A stack rather
|
|
286
|
+
# than a single prior slot: lifespans can nest more than one level deep (each
|
|
287
|
+
# portal-loop ``TestClient`` adds one), and a stack restores each displaced
|
|
288
|
+
# outer lifespan in LIFO order while an out-of-order stop simply removes its
|
|
289
|
+
# instance from wherever it sits — a single slot would forget everything below
|
|
290
|
+
# the most recent displacement.
|
|
291
|
+
_displaced_ingestors: list[LiveUsageIngestor] = []
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def start_live_usage_ingestor() -> LiveUsageIngestor | None:
|
|
295
|
+
"""Create, start, and register a fresh ingestor as the current singleton.
|
|
296
|
+
|
|
297
|
+
The caller (the app lifespan) MUST hold the returned instance and pass it
|
|
298
|
+
back to ``stop_live_usage_ingestor`` at shutdown. Two lifespans can be
|
|
299
|
+
live in one process (the test suite nests a portal-loop ``TestClient``
|
|
300
|
+
inside an app already running on the session loop); each owns its own
|
|
301
|
+
instance, and the module global only tracks whichever registered last. A
|
|
302
|
+
started ingestor whose only strong root is the module global would become
|
|
303
|
+
an unreferenced reference cycle (task -> coroutine frame -> ingestor ->
|
|
304
|
+
queue -> getter future -> task) the moment a nested startup overwrites the
|
|
305
|
+
global, and the cyclic GC would then destroy its consumer task mid-await.
|
|
306
|
+
"""
|
|
307
|
+
global _ingestor
|
|
308
|
+
ingestor = LiveUsageIngestor(
|
|
309
|
+
queue_size=_QUEUE_SIZE,
|
|
310
|
+
write_min_interval_seconds=_WRITE_MIN_INTERVAL_SECONDS,
|
|
311
|
+
)
|
|
312
|
+
ingestor.start()
|
|
313
|
+
if _ingestor is not None and _ingestor.is_running():
|
|
314
|
+
# A nested startup displaces a still-running outer registration;
|
|
315
|
+
# remember it so the nested shutdown can restore it (a dead instance
|
|
316
|
+
# is never worth remembering).
|
|
317
|
+
_displaced_ingestors.append(_ingestor)
|
|
318
|
+
register_live_usage_publisher(ingestor.publish)
|
|
319
|
+
_ingestor = ingestor
|
|
320
|
+
return ingestor
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
async def stop_live_usage_ingestor(ingestor: LiveUsageIngestor | None = None) -> None:
|
|
324
|
+
"""Stop ``ingestor``, or the current singleton when omitted.
|
|
325
|
+
|
|
326
|
+
The module global and the publisher registration are touched only when
|
|
327
|
+
the stopped instance still owns them, so a lifespan shutting down cannot
|
|
328
|
+
orphan or unregister a nested lifespan's newer instance — and a nested
|
|
329
|
+
lifespan's shutdown cannot leave the outer instance dangling with no
|
|
330
|
+
stop path (the leak behind issue #1755's cross-test poisoning). When the
|
|
331
|
+
stopped instance is the current registration, the most recent displaced
|
|
332
|
+
ingestor that is still running is restored (registration and publisher
|
|
333
|
+
wiring), so a still-live outer lifespan resumes receiving publications
|
|
334
|
+
instead of going silently deaf after a nested shutdown.
|
|
335
|
+
"""
|
|
336
|
+
global _ingestor
|
|
337
|
+
if ingestor is None:
|
|
338
|
+
ingestor = _ingestor
|
|
339
|
+
if ingestor is not None:
|
|
340
|
+
# Whatever happens next, a stopped instance must never be restorable.
|
|
341
|
+
try:
|
|
342
|
+
_displaced_ingestors.remove(ingestor)
|
|
343
|
+
except ValueError:
|
|
344
|
+
pass
|
|
345
|
+
if ingestor is None or _ingestor is ingestor:
|
|
346
|
+
restored: LiveUsageIngestor | None = None
|
|
347
|
+
while _displaced_ingestors:
|
|
348
|
+
candidate = _displaced_ingestors.pop()
|
|
349
|
+
if candidate.is_running():
|
|
350
|
+
restored = candidate
|
|
351
|
+
break
|
|
352
|
+
# Stopped or dead in the meantime — never restore a dead instance.
|
|
353
|
+
_ingestor = restored
|
|
354
|
+
register_live_usage_publisher(restored.publish if restored is not None else None)
|
|
355
|
+
if ingestor is not None:
|
|
356
|
+
await ingestor.stop()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Adapters between usage ORM rows and the `UsageWindowRow` value type.
|
|
2
|
+
|
|
3
|
+
The mapping itself is trivial, but it is shared by proxy, usage,
|
|
4
|
+
dashboard, and account-summary code. Pulling it into a single helper keeps
|
|
5
|
+
future ``UsageWindowRow`` changes from drifting across call sites.
|
|
6
|
+
|
|
7
|
+
Lives in ``app/modules/usage/`` rather than ``app/core/usage/types.py``
|
|
8
|
+
so that ``app/core/`` does not need to depend on ``app/db/models``.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from app.core.usage.types import UsageWindowRow
|
|
14
|
+
from app.db.models import AdditionalUsageHistory, UsageHistory
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def usage_history_to_window_row(entry: UsageHistory | AdditionalUsageHistory) -> UsageWindowRow:
|
|
18
|
+
"""Build a ``UsageWindowRow`` from a usage ORM row.
|
|
19
|
+
|
|
20
|
+
All fields map by name. Callers that need a ``UsageWindowRow`` from a
|
|
21
|
+
usage row should route through this helper.
|
|
22
|
+
"""
|
|
23
|
+
return UsageWindowRow(
|
|
24
|
+
account_id=entry.account_id,
|
|
25
|
+
used_percent=entry.used_percent,
|
|
26
|
+
reset_at=entry.reset_at,
|
|
27
|
+
window_minutes=entry.window_minutes,
|
|
28
|
+
recorded_at=entry.recorded_at,
|
|
29
|
+
)
|