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,435 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import contextlib
|
|
5
|
+
import hashlib
|
|
6
|
+
import logging
|
|
7
|
+
import random
|
|
8
|
+
from collections.abc import Awaitable, Callable
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from datetime import UTC, datetime
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from app.core.clients.rate_limit_reset_credits import (
|
|
14
|
+
RateLimitResetCreditsSnapshot,
|
|
15
|
+
ResetCreditFetchError,
|
|
16
|
+
ResetCreditItem,
|
|
17
|
+
ResetCreditsResponse,
|
|
18
|
+
build_snapshot,
|
|
19
|
+
fetch_reset_credits,
|
|
20
|
+
)
|
|
21
|
+
from app.core.config.background_jobs import background_job_enabled, resolve_background_job_toggle
|
|
22
|
+
from app.core.crypto import TokenEncryptor
|
|
23
|
+
from app.core.upstream_proxy import ResolvedUpstreamRoute, UpstreamProxyRouteError
|
|
24
|
+
from app.db.models import Account, AccountStatus
|
|
25
|
+
from app.db.session import detach_session_objects, get_background_session
|
|
26
|
+
from app.modules.accounts.auth_manager import AuthManager
|
|
27
|
+
from app.modules.accounts.repository import AccountsRepository
|
|
28
|
+
from app.modules.proxy.account_cache import get_account_selection_cache
|
|
29
|
+
from app.modules.rate_limit_reset_credits.store import (
|
|
30
|
+
RateLimitResetCreditsStore,
|
|
31
|
+
get_rate_limit_reset_credits_store,
|
|
32
|
+
)
|
|
33
|
+
from app.modules.settings.repository import SettingsRepository
|
|
34
|
+
from app.modules.usage.repository import AdditionalUsageRepository, UsageRepository
|
|
35
|
+
from app.modules.usage.updater import UsageUpdater, _resolve_upstream_route_for_account
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
_RESET_CREDITS_SKIP_STATUSES = frozenset(
|
|
40
|
+
{AccountStatus.PAUSED, AccountStatus.REAUTH_REQUIRED, AccountStatus.DEACTIVATED}
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
ResetCreditsFetchFn = Callable[..., Awaitable[ResetCreditsResponse]]
|
|
44
|
+
ResetCreditsRedeemFn = Callable[..., Awaitable[Any]]
|
|
45
|
+
ResolveRouteFn = Callable[[Account], Awaitable[ResolvedUpstreamRoute | None]]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
_TICK_JITTER_LOW = 0.9
|
|
49
|
+
_TICK_JITTER_HIGH = 1.1
|
|
50
|
+
_AUTO_REDEEM_WINDOW_SECONDS = 5 * 60
|
|
51
|
+
# Polling cadence per replica (fixed; issue #1340 / PRINCIPLES.md P2). The
|
|
52
|
+
# dataclass field stays so tests can inject a shorter interval.
|
|
53
|
+
_REFRESH_INTERVAL_SECONDS = 60
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(slots=True)
|
|
57
|
+
class RateLimitResetCreditsRefreshScheduler:
|
|
58
|
+
"""Per-replica reset-credits refresh loop with desynchronized ticks.
|
|
59
|
+
|
|
60
|
+
Every replica refreshes its own process-local snapshot store (the store is
|
|
61
|
+
not shared, so the loop MUST NOT be leader-gated). The randomized startup
|
|
62
|
+
delay and per-tick jitter only spread replica ticks over the interval so
|
|
63
|
+
N replicas do not hit upstream in lockstep; aggregate upstream fetch rate
|
|
64
|
+
still scales with replica count (one fetch per account per
|
|
65
|
+
``_REFRESH_INTERVAL_SECONDS`` per replica).
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
interval_seconds: int
|
|
69
|
+
rng: random.Random = field(default_factory=random.Random)
|
|
70
|
+
enabled: bool = True
|
|
71
|
+
# M2 background jobs: the loop always runs; each cycle reads the effective
|
|
72
|
+
# ``rate_limit_reset_credits_refresh_enabled`` toggle from the settings
|
|
73
|
+
# cache before taking the lock and skips while it is False, so a dashboard
|
|
74
|
+
# change applies on the next tick without a restart.
|
|
75
|
+
dashboard_enabled: Callable[[], Awaitable[bool]] = field(default_factory=lambda: _dashboard_polling_enabled)
|
|
76
|
+
_task: asyncio.Task[None] | None = None
|
|
77
|
+
_stop: asyncio.Event = field(default_factory=asyncio.Event)
|
|
78
|
+
_lock: asyncio.Lock = field(default_factory=asyncio.Lock)
|
|
79
|
+
|
|
80
|
+
async def start(self) -> None:
|
|
81
|
+
await self._warn_if_auto_redeem_conflicts()
|
|
82
|
+
if not self.enabled:
|
|
83
|
+
return
|
|
84
|
+
if self._task and not self._task.done():
|
|
85
|
+
return
|
|
86
|
+
self._stop.clear()
|
|
87
|
+
self._task = asyncio.create_task(self._run_loop())
|
|
88
|
+
|
|
89
|
+
async def _warn_if_auto_redeem_conflicts(self) -> None:
|
|
90
|
+
# The refresh loop is the only driver of automatic redemption, so a
|
|
91
|
+
# disabled toggle silently starves a persisted auto-redeem opt-in. M2:
|
|
92
|
+
# the toggle is the effective dashboard value, not the env alias alone.
|
|
93
|
+
try:
|
|
94
|
+
async with get_background_session() as session:
|
|
95
|
+
dashboard_settings = await SettingsRepository(session).get_or_create()
|
|
96
|
+
auto_redeem_enabled = dashboard_settings.auto_redeem_reset_credits_before_expiry
|
|
97
|
+
polling_enabled = resolve_background_job_toggle(
|
|
98
|
+
dashboard_settings, "rate_limit_reset_credits_refresh_enabled"
|
|
99
|
+
)
|
|
100
|
+
except Exception:
|
|
101
|
+
logger.exception("Reset credits auto-redeem conflict check failed")
|
|
102
|
+
return
|
|
103
|
+
if auto_redeem_enabled and not polling_enabled:
|
|
104
|
+
logger.warning(
|
|
105
|
+
"rate_limit_reset_credits_refresh_enabled=false disables automatic reset-credit "
|
|
106
|
+
"redemption, but dashboard setting auto_redeem_reset_credits_before_expiry is "
|
|
107
|
+
"enabled; credits will expire without redemption until polling is re-enabled"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
async def stop(self) -> None:
|
|
111
|
+
if not self._task:
|
|
112
|
+
return
|
|
113
|
+
self._stop.set()
|
|
114
|
+
self._task.cancel()
|
|
115
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
116
|
+
await self._task
|
|
117
|
+
self._task = None
|
|
118
|
+
|
|
119
|
+
def _startup_delay_seconds(self) -> float:
|
|
120
|
+
return self.rng.uniform(0.0, float(self.interval_seconds))
|
|
121
|
+
|
|
122
|
+
def _tick_delay_seconds(self) -> float:
|
|
123
|
+
return float(self.interval_seconds) * self.rng.uniform(_TICK_JITTER_LOW, _TICK_JITTER_HIGH)
|
|
124
|
+
|
|
125
|
+
async def _run_loop(self) -> None:
|
|
126
|
+
if await self._wait_or_stop(self._startup_delay_seconds()):
|
|
127
|
+
return
|
|
128
|
+
while not self._stop.is_set():
|
|
129
|
+
# The whole cycle, including the settings read that decides whether
|
|
130
|
+
# polling runs, is guarded: a transient database error must not kill
|
|
131
|
+
# the loop task (a dead task also aborts the shutdown stop chain).
|
|
132
|
+
try:
|
|
133
|
+
await self._refresh_once()
|
|
134
|
+
except Exception:
|
|
135
|
+
logger.exception("Reset credits refresh cycle failed")
|
|
136
|
+
if await self._wait_or_stop(self._tick_delay_seconds()):
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
async def _wait_or_stop(self, delay_seconds: float) -> bool:
|
|
140
|
+
try:
|
|
141
|
+
await asyncio.wait_for(self._stop.wait(), timeout=delay_seconds)
|
|
142
|
+
except asyncio.TimeoutError:
|
|
143
|
+
return False
|
|
144
|
+
return True
|
|
145
|
+
|
|
146
|
+
async def _refresh_once(self) -> None:
|
|
147
|
+
if not await self.dashboard_enabled():
|
|
148
|
+
logger.debug("Reset credits refresh skipped: polling disabled in the dashboard settings")
|
|
149
|
+
return
|
|
150
|
+
async with self._lock:
|
|
151
|
+
try:
|
|
152
|
+
async with get_background_session() as session:
|
|
153
|
+
accounts_repo = AccountsRepository(session)
|
|
154
|
+
settings_repo = SettingsRepository(session)
|
|
155
|
+
accounts = await accounts_repo.list_accounts()
|
|
156
|
+
dashboard_settings = await settings_repo.get_or_create()
|
|
157
|
+
auto_redeem_before_expiry = dashboard_settings.auto_redeem_reset_credits_before_expiry
|
|
158
|
+
detach_session_objects(session)
|
|
159
|
+
await refresh_reset_credits_for_accounts(
|
|
160
|
+
accounts=accounts,
|
|
161
|
+
encryptor=TokenEncryptor(),
|
|
162
|
+
store=get_rate_limit_reset_credits_store(),
|
|
163
|
+
fetch_fn=fetch_reset_credits,
|
|
164
|
+
resolve_route=_resolve_reset_credits_refresh_route,
|
|
165
|
+
auto_redeem_resolve_route=_resolve_reset_credits_consume_route,
|
|
166
|
+
auto_redeem_before_expiry=auto_redeem_before_expiry,
|
|
167
|
+
auto_redeem_window_seconds=float(_AUTO_REDEEM_WINDOW_SECONDS),
|
|
168
|
+
)
|
|
169
|
+
except Exception:
|
|
170
|
+
logger.exception("Reset credits refresh loop failed")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
async def refresh_reset_credits_for_accounts(
|
|
174
|
+
*,
|
|
175
|
+
accounts: list[Account],
|
|
176
|
+
encryptor: TokenEncryptor,
|
|
177
|
+
store: RateLimitResetCreditsStore,
|
|
178
|
+
fetch_fn: ResetCreditsFetchFn = fetch_reset_credits,
|
|
179
|
+
redeem_fn: ResetCreditsRedeemFn | None = None,
|
|
180
|
+
resolve_route: ResolveRouteFn | None = None,
|
|
181
|
+
auto_redeem_resolve_route: ResolveRouteFn | None = None,
|
|
182
|
+
auto_redeem_before_expiry: bool = False,
|
|
183
|
+
auto_redeem_window_seconds: float | None = None,
|
|
184
|
+
) -> None:
|
|
185
|
+
"""Refresh the cached reset-credits snapshot for each eligible account.
|
|
186
|
+
|
|
187
|
+
CRITICAL invariant: this function MUST NOT mutate any account's persisted
|
|
188
|
+
status. On upstream error it logs and retains the prior cached snapshot
|
|
189
|
+
(i.e. it simply skips overwriting the cache) so account-status derivation
|
|
190
|
+
stays owned by usage refresh. One account failing must not abort the loop.
|
|
191
|
+
"""
|
|
192
|
+
for account in accounts:
|
|
193
|
+
if account.status in _RESET_CREDITS_SKIP_STATUSES:
|
|
194
|
+
continue
|
|
195
|
+
if not account.chatgpt_account_id:
|
|
196
|
+
continue
|
|
197
|
+
await _refresh_account_reset_credits(
|
|
198
|
+
account,
|
|
199
|
+
encryptor=encryptor,
|
|
200
|
+
store=store,
|
|
201
|
+
fetch_fn=fetch_fn,
|
|
202
|
+
redeem_fn=redeem_fn,
|
|
203
|
+
resolve_route=resolve_route,
|
|
204
|
+
auto_redeem_resolve_route=auto_redeem_resolve_route,
|
|
205
|
+
auto_redeem_before_expiry=auto_redeem_before_expiry,
|
|
206
|
+
auto_redeem_window_seconds=auto_redeem_window_seconds,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
async def _resolve_reset_credits_refresh_route(account: Account) -> ResolvedUpstreamRoute | None:
|
|
211
|
+
return await _resolve_upstream_route_for_account(account, operation="usage_refresh")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
async def _resolve_reset_credits_consume_route(account: Account) -> ResolvedUpstreamRoute | None:
|
|
215
|
+
return await _resolve_upstream_route_for_account(account, operation="rate_limit_reset_consume")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
async def _refresh_account_reset_credits(
|
|
219
|
+
account: Account,
|
|
220
|
+
*,
|
|
221
|
+
encryptor: TokenEncryptor,
|
|
222
|
+
store: RateLimitResetCreditsStore,
|
|
223
|
+
fetch_fn: ResetCreditsFetchFn,
|
|
224
|
+
redeem_fn: ResetCreditsRedeemFn | None = None,
|
|
225
|
+
resolve_route: ResolveRouteFn | None = None,
|
|
226
|
+
auto_redeem_resolve_route: ResolveRouteFn | None = None,
|
|
227
|
+
auto_redeem_before_expiry: bool = False,
|
|
228
|
+
auto_redeem_window_seconds: float | None = None,
|
|
229
|
+
) -> None:
|
|
230
|
+
snapshot_generation = store.generation(account.id)
|
|
231
|
+
route: ResolvedUpstreamRoute | None = None
|
|
232
|
+
if resolve_route is not None:
|
|
233
|
+
try:
|
|
234
|
+
route = await resolve_route(account)
|
|
235
|
+
except UpstreamProxyRouteError as exc:
|
|
236
|
+
logger.warning(
|
|
237
|
+
"Reset credits refresh upstream proxy route unavailable account_id=%s reason=%s",
|
|
238
|
+
account.id,
|
|
239
|
+
exc.reason,
|
|
240
|
+
)
|
|
241
|
+
return
|
|
242
|
+
try:
|
|
243
|
+
access_token = encryptor.decrypt(account.access_token_encrypted)
|
|
244
|
+
response = await fetch_fn(
|
|
245
|
+
access_token,
|
|
246
|
+
account.chatgpt_account_id,
|
|
247
|
+
route=route,
|
|
248
|
+
allow_direct_egress=route is None,
|
|
249
|
+
)
|
|
250
|
+
except ResetCreditFetchError as exc:
|
|
251
|
+
logger.warning(
|
|
252
|
+
"Reset credits refresh failed account_id=%s error=%s",
|
|
253
|
+
account.id,
|
|
254
|
+
exc,
|
|
255
|
+
)
|
|
256
|
+
return
|
|
257
|
+
except Exception as exc:
|
|
258
|
+
logger.warning(
|
|
259
|
+
"Reset credits refresh failed account_id=%s error=%s",
|
|
260
|
+
account.id,
|
|
261
|
+
exc,
|
|
262
|
+
)
|
|
263
|
+
return
|
|
264
|
+
|
|
265
|
+
snapshot = build_snapshot(response)
|
|
266
|
+
stored = await store.set_if_generation(account.id, snapshot, snapshot_generation)
|
|
267
|
+
if not stored:
|
|
268
|
+
logger.info(
|
|
269
|
+
"Skipped stale reset credits snapshot account_id=%s",
|
|
270
|
+
account.id,
|
|
271
|
+
)
|
|
272
|
+
return
|
|
273
|
+
if auto_redeem_before_expiry and _should_auto_redeem_snapshot(
|
|
274
|
+
snapshot,
|
|
275
|
+
window_seconds=auto_redeem_window_seconds,
|
|
276
|
+
):
|
|
277
|
+
try:
|
|
278
|
+
await _auto_redeem_reset_credit(
|
|
279
|
+
account,
|
|
280
|
+
snapshot=snapshot,
|
|
281
|
+
encryptor=encryptor,
|
|
282
|
+
store=store,
|
|
283
|
+
fetch_fn=fetch_fn,
|
|
284
|
+
redeem_fn=redeem_fn,
|
|
285
|
+
resolve_route=auto_redeem_resolve_route or resolve_route,
|
|
286
|
+
)
|
|
287
|
+
except Exception:
|
|
288
|
+
logger.warning(
|
|
289
|
+
"Automatic reset credit redeem failed account_id=%s",
|
|
290
|
+
account.id,
|
|
291
|
+
exc_info=True,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _should_auto_redeem_snapshot(
|
|
296
|
+
snapshot: RateLimitResetCreditsSnapshot,
|
|
297
|
+
*,
|
|
298
|
+
window_seconds: float | None,
|
|
299
|
+
) -> bool:
|
|
300
|
+
if snapshot.available_count <= 0 or snapshot.nearest_expires_at is None:
|
|
301
|
+
return False
|
|
302
|
+
expires_at = snapshot.nearest_expires_at
|
|
303
|
+
if expires_at.tzinfo is None:
|
|
304
|
+
expires_at = expires_at.replace(tzinfo=UTC)
|
|
305
|
+
seconds_until_expiry = (expires_at - datetime.now(UTC)).total_seconds()
|
|
306
|
+
return 0 <= seconds_until_expiry <= float(window_seconds or 0)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
async def _auto_redeem_reset_credit(
|
|
310
|
+
account: Account,
|
|
311
|
+
*,
|
|
312
|
+
snapshot: RateLimitResetCreditsSnapshot,
|
|
313
|
+
encryptor: TokenEncryptor,
|
|
314
|
+
store: RateLimitResetCreditsStore,
|
|
315
|
+
fetch_fn: ResetCreditsFetchFn,
|
|
316
|
+
redeem_fn: ResetCreditsRedeemFn | None,
|
|
317
|
+
resolve_route: ResolveRouteFn | None,
|
|
318
|
+
) -> None:
|
|
319
|
+
from app.modules.rate_limit_reset_credits.api import (
|
|
320
|
+
ResetCreditRedeemRequestAlreadyPinned,
|
|
321
|
+
_redeem_soonest_reset_credit,
|
|
322
|
+
)
|
|
323
|
+
from app.modules.rate_limit_reset_credits.redeem_coordination import get_pinned_redeem_credit_id
|
|
324
|
+
|
|
325
|
+
redeem_request_id = _auto_redeem_request_id(account, snapshot)
|
|
326
|
+
if redeem_request_id is None:
|
|
327
|
+
return
|
|
328
|
+
pinned_credit_id = await get_pinned_redeem_credit_id(account.id, redeem_request_id)
|
|
329
|
+
if pinned_credit_id is not None:
|
|
330
|
+
logger.info(
|
|
331
|
+
"Skipping automatic reset credit redeem because an automatic redeem is already pinned "
|
|
332
|
+
"account_id=%s credit_id=%s",
|
|
333
|
+
account.id,
|
|
334
|
+
pinned_credit_id,
|
|
335
|
+
)
|
|
336
|
+
return
|
|
337
|
+
target_credit = _select_auto_redeem_target_credit(snapshot)
|
|
338
|
+
if target_credit is None:
|
|
339
|
+
return
|
|
340
|
+
|
|
341
|
+
effective_redeem_fn = redeem_fn or _redeem_soonest_reset_credit
|
|
342
|
+
async with get_background_session() as lock_session:
|
|
343
|
+
latest_account = await lock_session.get(Account, account.id)
|
|
344
|
+
if latest_account is None:
|
|
345
|
+
logger.info(
|
|
346
|
+
"Skipping automatic reset credit redeem because account no longer exists account_id=%s",
|
|
347
|
+
account.id,
|
|
348
|
+
)
|
|
349
|
+
return
|
|
350
|
+
if latest_account.status in _RESET_CREDITS_SKIP_STATUSES or not latest_account.chatgpt_account_id:
|
|
351
|
+
logger.info(
|
|
352
|
+
"Skipping automatic reset credit redeem because account is no longer eligible "
|
|
353
|
+
"account_id=%s status=%s has_chatgpt_account_id=%s",
|
|
354
|
+
latest_account.id,
|
|
355
|
+
latest_account.status.value,
|
|
356
|
+
bool(latest_account.chatgpt_account_id),
|
|
357
|
+
)
|
|
358
|
+
return
|
|
359
|
+
try:
|
|
360
|
+
await effective_redeem_fn(
|
|
361
|
+
account=latest_account,
|
|
362
|
+
store=store,
|
|
363
|
+
encryptor=encryptor,
|
|
364
|
+
lock_session=lock_session,
|
|
365
|
+
fetch_fn=fetch_fn,
|
|
366
|
+
resolve_route=resolve_route,
|
|
367
|
+
refresh_usage=_refresh_usage_after_auto_redeem,
|
|
368
|
+
redeem_request_id=redeem_request_id,
|
|
369
|
+
skip_if_redeem_request_pinned=True,
|
|
370
|
+
expected_credit_id=target_credit.id,
|
|
371
|
+
expected_credit_expires_at=target_credit.expires_at,
|
|
372
|
+
)
|
|
373
|
+
except ResetCreditRedeemRequestAlreadyPinned as exc:
|
|
374
|
+
logger.info(
|
|
375
|
+
"Skipping automatic reset credit redeem because an automatic redeem was pinned "
|
|
376
|
+
"inside the redeem lock account_id=%s credit_id=%s",
|
|
377
|
+
exc.account_id,
|
|
378
|
+
exc.credit_id,
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _auto_redeem_request_id(account: Account, snapshot: RateLimitResetCreditsSnapshot) -> str | None:
|
|
383
|
+
expires_at = snapshot.nearest_expires_at
|
|
384
|
+
if expires_at is None:
|
|
385
|
+
return None
|
|
386
|
+
if expires_at.tzinfo is None:
|
|
387
|
+
expires_at = expires_at.replace(tzinfo=UTC)
|
|
388
|
+
else:
|
|
389
|
+
expires_at = expires_at.astimezone(UTC)
|
|
390
|
+
# Intentionally one automatic request per account and UTC expiry date. If
|
|
391
|
+
# multiple credits expire on the same day, the automatic path favors not
|
|
392
|
+
# burning a second coupon over trying to exhaust every expiring credit.
|
|
393
|
+
digest = hashlib.sha256(f"{account.id}:{expires_at.date().isoformat()}".encode("utf-8")).hexdigest()[:32]
|
|
394
|
+
return f"auto-reset-credit:{digest}"
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _select_auto_redeem_target_credit(snapshot: RateLimitResetCreditsSnapshot) -> ResetCreditItem | None:
|
|
398
|
+
if snapshot.available_count <= 0:
|
|
399
|
+
return None
|
|
400
|
+
available = [
|
|
401
|
+
credit for credit in snapshot.credits if credit.status == "available" and credit.expires_at is not None
|
|
402
|
+
]
|
|
403
|
+
if not available:
|
|
404
|
+
return None
|
|
405
|
+
return min(available, key=lambda credit: credit.expires_at)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
async def _refresh_usage_after_auto_redeem(account: Account) -> None:
|
|
409
|
+
async with get_background_session() as session:
|
|
410
|
+
accounts_repo = AccountsRepository(session)
|
|
411
|
+
usage_repo = UsageRepository(session)
|
|
412
|
+
additional_usage_repo = AdditionalUsageRepository(session)
|
|
413
|
+
current = await accounts_repo.get_by_id(account.id)
|
|
414
|
+
if current is None:
|
|
415
|
+
raise RuntimeError(f"Account {account.id} disappeared before automatic reset-credit usage refresh")
|
|
416
|
+
refreshed = await UsageUpdater(
|
|
417
|
+
usage_repo,
|
|
418
|
+
accounts_repo,
|
|
419
|
+
additional_usage_repo,
|
|
420
|
+
auth_manager=AuthManager(accounts_repo),
|
|
421
|
+
).force_refresh(current)
|
|
422
|
+
if not refreshed:
|
|
423
|
+
raise RuntimeError(f"Forced usage refresh returned no update for account {account.id}")
|
|
424
|
+
get_account_selection_cache().invalidate()
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
async def _dashboard_polling_enabled() -> bool:
|
|
428
|
+
return await background_job_enabled("rate_limit_reset_credits_refresh_enabled")
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def build_rate_limit_reset_credits_scheduler() -> RateLimitResetCreditsRefreshScheduler:
|
|
432
|
+
return RateLimitResetCreditsRefreshScheduler(
|
|
433
|
+
interval_seconds=_REFRESH_INTERVAL_SECONDS,
|
|
434
|
+
enabled=True,
|
|
435
|
+
)
|
app/core/usage/types.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class UsageWindowRow:
|
|
9
|
+
account_id: str
|
|
10
|
+
used_percent: float | None
|
|
11
|
+
reset_at: int | None = None
|
|
12
|
+
window_minutes: int | None = None
|
|
13
|
+
recorded_at: datetime | None = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class UsageAggregateRow:
|
|
18
|
+
account_id: str
|
|
19
|
+
used_percent_avg: float | None
|
|
20
|
+
input_tokens_sum: int | None
|
|
21
|
+
output_tokens_sum: int | None
|
|
22
|
+
samples: int
|
|
23
|
+
last_recorded_at: datetime | None
|
|
24
|
+
reset_at_max: int | None
|
|
25
|
+
window_minutes_max: int | None
|
|
26
|
+
|
|
27
|
+
def to_window_row(self) -> UsageWindowRow:
|
|
28
|
+
return UsageWindowRow(
|
|
29
|
+
account_id=self.account_id,
|
|
30
|
+
used_percent=self.used_percent_avg,
|
|
31
|
+
reset_at=self.reset_at_max,
|
|
32
|
+
window_minutes=self.window_minutes_max,
|
|
33
|
+
recorded_at=self.last_recorded_at,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class UsageWindowSummary:
|
|
39
|
+
used_percent: float | None
|
|
40
|
+
capacity_credits: float
|
|
41
|
+
used_credits: float
|
|
42
|
+
reset_at: int | None
|
|
43
|
+
window_minutes: int | None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class UsageWindowSnapshot:
|
|
48
|
+
used_percent: float
|
|
49
|
+
capacity_credits: float
|
|
50
|
+
used_credits: float
|
|
51
|
+
reset_at: int | None
|
|
52
|
+
window_minutes: int | None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class UsageCostByModel:
|
|
57
|
+
model: str
|
|
58
|
+
usd: float
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class UsageCostSummary:
|
|
63
|
+
currency: str
|
|
64
|
+
total_usd_7d: float
|
|
65
|
+
by_model: list[UsageCostByModel]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(frozen=True)
|
|
69
|
+
class UsageMetricsSummary:
|
|
70
|
+
requests_7d: int | None
|
|
71
|
+
tokens_secondary_window: int | None
|
|
72
|
+
cached_tokens_secondary_window: int | None = None
|
|
73
|
+
error_rate_7d: float | None = None
|
|
74
|
+
top_error: str | None = None
|
|
75
|
+
# Cancelled (client-disconnect) terminals in the window; excluded from
|
|
76
|
+
# the error-rate numerator, surfaced so the breakdown stays visible.
|
|
77
|
+
cancelled_7d: int | None = None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True)
|
|
81
|
+
class UsageSummaryPayload:
|
|
82
|
+
primary_window: UsageWindowSnapshot
|
|
83
|
+
secondary_window: UsageWindowSnapshot | None
|
|
84
|
+
monthly_window: UsageWindowSnapshot | None
|
|
85
|
+
cost: UsageCostSummary
|
|
86
|
+
metrics: UsageMetricsSummary | None = None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass(frozen=True)
|
|
90
|
+
class UsageHistoryEntry:
|
|
91
|
+
account_id: str
|
|
92
|
+
email: str
|
|
93
|
+
used_percent_avg: float
|
|
94
|
+
used_credits: float
|
|
95
|
+
request_count: int
|
|
96
|
+
cost_usd: float
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass(frozen=True)
|
|
100
|
+
class UsageHistoryPayload:
|
|
101
|
+
window_hours: int
|
|
102
|
+
accounts: list[UsageHistoryEntry]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass(frozen=True)
|
|
106
|
+
class UsageTrendBucket:
|
|
107
|
+
bucket_epoch: int
|
|
108
|
+
account_id: str
|
|
109
|
+
window: str
|
|
110
|
+
avg_used_percent: float
|
|
111
|
+
samples: int
|
|
112
|
+
reset_at: int | None = None
|
|
113
|
+
window_minutes: int | None = None
|
|
114
|
+
recorded_at: datetime | None = None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass(frozen=True)
|
|
118
|
+
class BucketModelAggregate:
|
|
119
|
+
bucket_epoch: int
|
|
120
|
+
model: str
|
|
121
|
+
service_tier: str | None
|
|
122
|
+
request_count: int
|
|
123
|
+
error_count: int
|
|
124
|
+
input_tokens: int
|
|
125
|
+
output_tokens: int
|
|
126
|
+
cached_input_tokens: int
|
|
127
|
+
reasoning_tokens: int
|
|
128
|
+
cost_usd: float = 0.0
|
|
129
|
+
cancelled_count: int = 0
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@dataclass(frozen=True)
|
|
133
|
+
class BucketConversationAggregate:
|
|
134
|
+
bucket_epoch: int
|
|
135
|
+
conversation_count: int
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@dataclass(frozen=True)
|
|
139
|
+
class RequestActivityAggregate:
|
|
140
|
+
request_count: int
|
|
141
|
+
error_count: int
|
|
142
|
+
input_tokens: int
|
|
143
|
+
output_tokens: int
|
|
144
|
+
cached_input_tokens: int
|
|
145
|
+
cost_usd: float
|
|
146
|
+
cancelled_count: int = 0
|
|
147
|
+
conversation_count: int = 0
|
|
148
|
+
conversation_request_count: int = 0
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@dataclass(frozen=True)
|
|
152
|
+
class UsageSummaryLogsAggregate:
|
|
153
|
+
"""SQL-side replacement for summing a window of RequestLog ORM rows in
|
|
154
|
+
Python; field semantics mirror the log helpers exactly (reasoning-token
|
|
155
|
+
fallback for output, per-row cached<=input clamp, None-cost rows excluded
|
|
156
|
+
from per-model cost)."""
|
|
157
|
+
|
|
158
|
+
request_count: int
|
|
159
|
+
error_count: int
|
|
160
|
+
total_tokens: int
|
|
161
|
+
cached_input_tokens: int
|
|
162
|
+
top_error: str | None
|
|
163
|
+
cost_by_model: list[tuple[str, float]]
|
|
164
|
+
cancelled_count: int = 0
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def cost_total_usd(self) -> float:
|
|
168
|
+
return sum(cost for _, cost in self.cost_by_model)
|
|
File without changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from typing import TypeGuard
|
|
5
|
+
|
|
6
|
+
from app.core.types import JsonValue
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def is_json_mapping(value: object) -> TypeGuard[Mapping[str, JsonValue]]:
|
|
10
|
+
return isinstance(value, Mapping)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def is_json_dict(value: object) -> TypeGuard[dict[str, JsonValue]]:
|
|
14
|
+
return isinstance(value, dict)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def is_json_list(value: object) -> TypeGuard[list[JsonValue]]:
|
|
18
|
+
return isinstance(value, list)
|
app/core/utils/locks.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Lock constructors for per-session hot paths.
|
|
2
|
+
|
|
3
|
+
anyio's default ``Lock`` runs a cancel-shielded scheduling checkpoint
|
|
4
|
+
(``sleep(0)`` under a shielded ``CancelScope``) after every *uncontended*
|
|
5
|
+
acquire so that lock-heavy loops still yield to the event loop. The HTTP
|
|
6
|
+
bridge relay loop acquires its session ``pending_lock`` several times per
|
|
7
|
+
upstream message, each time for a few microseconds of synchronous
|
|
8
|
+
bookkeeping, and already yields on real network I/O every iteration — so the
|
|
9
|
+
checkpoint only adds a full event-loop round trip per acquire (~4 us plus one
|
|
10
|
+
loop iteration under uvloop; 1.4% of GIL samples in the 2026-09-03 production
|
|
11
|
+
profile).
|
|
12
|
+
|
|
13
|
+
``fast_lock`` documents the choice once: ``fast_acquire=True`` skips only the
|
|
14
|
+
post-acquire checkpoint on the uncontended path. Contended acquires still
|
|
15
|
+
suspend, ``checkpoint_if_cancelled`` still runs first, and ``acquire_nowait``
|
|
16
|
+
/ ``WouldBlock`` keep working, so mutual exclusion and cancellation delivery
|
|
17
|
+
are unchanged. Use it for locks whose critical sections are short and whose
|
|
18
|
+
callers await I/O between acquisitions; keep the default constructor for
|
|
19
|
+
module-level locks and for loops that could otherwise spin without awaiting.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import anyio
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def fast_lock() -> anyio.Lock:
|
|
28
|
+
"""Return an ``anyio.Lock`` whose uncontended acquire does not yield to the loop."""
|
|
29
|
+
|
|
30
|
+
return anyio.Lock(fast_acquire=True)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
__all__ = ["fast_lock"]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""One masking rule, for every surface that shows an address it may not reveal.
|
|
2
|
+
|
|
3
|
+
It lives here rather than beside its first caller because two places now apply
|
|
4
|
+
it to the same value and have to agree character for character: the account
|
|
5
|
+
list redacts an operator's address with it, and a refused company sign-in hands
|
|
6
|
+
the person the masked form of the address the identity provider asserted so an
|
|
7
|
+
administrator can match it against the refusal in the audit log. Two
|
|
8
|
+
implementations of "mask an e-mail" would eventually disagree, and the disagreement
|
|
9
|
+
would show up as a reference nobody can find.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def mask_email(email: str) -> str:
|
|
16
|
+
"""Keep the first character of the local part and the domain: ``a***@example.com``."""
|
|
17
|
+
|
|
18
|
+
local, sep, domain = email.partition("@")
|
|
19
|
+
prefix = local[:1] if local else ""
|
|
20
|
+
return f"{prefix}***{sep}{domain}" if sep else f"{prefix}***"
|