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,601 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import uuid
|
|
6
|
+
from collections.abc import Awaitable, Callable
|
|
7
|
+
from contextlib import asynccontextmanager, suppress
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
|
|
11
|
+
from fastapi import APIRouter, Depends, Request
|
|
12
|
+
from pydantic import Field
|
|
13
|
+
from sqlalchemy import text
|
|
14
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
15
|
+
|
|
16
|
+
from app.core.audit.service import AuditActor, AuditService, AuditTarget
|
|
17
|
+
from app.core.auth.dashboard_access import DashboardPrincipal, Permission
|
|
18
|
+
from app.core.auth.dependencies import (
|
|
19
|
+
require_dashboard_permission,
|
|
20
|
+
set_dashboard_error_format,
|
|
21
|
+
validate_dashboard_session,
|
|
22
|
+
)
|
|
23
|
+
from app.core.auth.refresh import RefreshError
|
|
24
|
+
from app.core.cache.invalidation import NAMESPACE_RESET_CREDITS, bump_cache_invalidation_local
|
|
25
|
+
from app.core.clients.rate_limit_reset_credits import (
|
|
26
|
+
ConsumeResetCreditError,
|
|
27
|
+
ConsumeResetCreditResponse,
|
|
28
|
+
RateLimitResetCreditsSnapshot,
|
|
29
|
+
ResetCreditFetchError,
|
|
30
|
+
ResetCreditItem,
|
|
31
|
+
ResetCreditsResponse,
|
|
32
|
+
build_snapshot,
|
|
33
|
+
consume_reset_credit,
|
|
34
|
+
fetch_reset_credits,
|
|
35
|
+
)
|
|
36
|
+
from app.core.crypto import TokenEncryptor
|
|
37
|
+
from app.core.exceptions import (
|
|
38
|
+
DashboardAuthError,
|
|
39
|
+
DashboardConflictError,
|
|
40
|
+
DashboardNotFoundError,
|
|
41
|
+
DashboardPermissionError,
|
|
42
|
+
DashboardServiceUnavailableError,
|
|
43
|
+
)
|
|
44
|
+
from app.core.upstream_proxy import ResolvedUpstreamRoute, UpstreamProxyRouteError
|
|
45
|
+
from app.core.utils.shared_future import _await_cleanup_deferring_cancellation
|
|
46
|
+
from app.db.models import Account, AccountStatus
|
|
47
|
+
from app.dependencies import AccountsContext, get_accounts_context
|
|
48
|
+
from app.modules.accounts.auth_manager import AuthManager
|
|
49
|
+
from app.modules.accounts.schemas import AccountUsageResetConsumeRequest
|
|
50
|
+
from app.modules.proxy.account_cache import get_account_selection_cache
|
|
51
|
+
from app.modules.rate_limit_reset_credits.redeem_coordination import (
|
|
52
|
+
RedeemClaimTimeoutError,
|
|
53
|
+
acquire_redeem_claim,
|
|
54
|
+
get_pinned_redeem_credit_id,
|
|
55
|
+
new_redeem_claim_holder_id,
|
|
56
|
+
pin_redeem_request,
|
|
57
|
+
release_redeem_claim,
|
|
58
|
+
renew_redeem_claim_periodically,
|
|
59
|
+
)
|
|
60
|
+
from app.modules.rate_limit_reset_credits.store import (
|
|
61
|
+
RateLimitResetCreditsStore,
|
|
62
|
+
get_rate_limit_reset_credits_store,
|
|
63
|
+
)
|
|
64
|
+
from app.modules.shared.schemas import DashboardModel
|
|
65
|
+
from app.modules.usage.updater import _resolve_upstream_route_for_account
|
|
66
|
+
|
|
67
|
+
logger = logging.getLogger(__name__)
|
|
68
|
+
|
|
69
|
+
router = APIRouter(
|
|
70
|
+
prefix="/api/accounts",
|
|
71
|
+
tags=["dashboard"],
|
|
72
|
+
dependencies=[Depends(validate_dashboard_session), Depends(set_dashboard_error_format)],
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
FetchFn = Callable[..., Awaitable[ResetCreditsResponse]]
|
|
76
|
+
ConsumeFn = Callable[..., Awaitable[ConsumeResetCreditResponse]]
|
|
77
|
+
RefreshUsageFn = Callable[[Account], Awaitable[None]]
|
|
78
|
+
ResolveRouteFn = Callable[[Account], Awaitable[ResolvedUpstreamRoute | None]]
|
|
79
|
+
|
|
80
|
+
_NON_REDEEMABLE_STATUSES = frozenset({AccountStatus.PAUSED, AccountStatus.REAUTH_REQUIRED, AccountStatus.DEACTIVATED})
|
|
81
|
+
|
|
82
|
+
_redeem_locks: dict[str, asyncio.Lock] = {}
|
|
83
|
+
_redeem_locks_registry_lock = asyncio.Lock()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class ResetCreditItemResponse(DashboardModel):
|
|
87
|
+
id: str
|
|
88
|
+
reset_type: str | None = None
|
|
89
|
+
status: str | None = None
|
|
90
|
+
granted_at: datetime | None = None
|
|
91
|
+
expires_at: datetime | None = None
|
|
92
|
+
title: str | None = None
|
|
93
|
+
description: str | None = None
|
|
94
|
+
redeem_started_at: datetime | None = None
|
|
95
|
+
redeemed_at: datetime | None = None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class RateLimitResetCreditsSnapshotResponse(DashboardModel):
|
|
99
|
+
available_count: int = 0
|
|
100
|
+
nearest_expires_at: datetime | None = None
|
|
101
|
+
credits: list[ResetCreditItemResponse] = Field(default_factory=list)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class ConsumeResetCreditResponseSchema(DashboardModel):
|
|
105
|
+
code: str | None = None
|
|
106
|
+
windows_reset: int | None = None
|
|
107
|
+
redeemed_at: datetime | None = None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass(frozen=True, slots=True)
|
|
111
|
+
class _RedeemResetCreditOutcome:
|
|
112
|
+
response: ConsumeResetCreditResponseSchema
|
|
113
|
+
available_count_before: int
|
|
114
|
+
available_count_after: int
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class ResetCreditRedeemRequestAlreadyPinned(Exception):
|
|
118
|
+
"""Automatic redeem request already has a durable pin and should not retry upstream."""
|
|
119
|
+
|
|
120
|
+
def __init__(self, *, account_id: str, redeem_request_id: str, credit_id: str) -> None:
|
|
121
|
+
self.account_id = account_id
|
|
122
|
+
self.redeem_request_id = redeem_request_id
|
|
123
|
+
self.credit_id = credit_id
|
|
124
|
+
super().__init__(f"reset-credit redeem request already pinned for account {account_id}")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@router.get(
|
|
128
|
+
"/{account_id}/rate-limit-reset-credits",
|
|
129
|
+
response_model=RateLimitResetCreditsSnapshotResponse | None,
|
|
130
|
+
)
|
|
131
|
+
async def get_rate_limit_reset_credits(
|
|
132
|
+
account_id: str,
|
|
133
|
+
context: AccountsContext = Depends(get_accounts_context),
|
|
134
|
+
) -> RateLimitResetCreditsSnapshotResponse | None:
|
|
135
|
+
store = get_rate_limit_reset_credits_store()
|
|
136
|
+
account = await context.repository.get_by_id(account_id)
|
|
137
|
+
# A pending-deletion account is gone from the operator's point of view.
|
|
138
|
+
if account is None or account.delete_requested_at is not None:
|
|
139
|
+
await store.invalidate(account_id)
|
|
140
|
+
return None
|
|
141
|
+
if account.status in _NON_REDEEMABLE_STATUSES or not account.chatgpt_account_id:
|
|
142
|
+
await store.invalidate(account_id)
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
snapshot = store.get(account_id)
|
|
146
|
+
if snapshot is not None:
|
|
147
|
+
return _snapshot_to_response(snapshot)
|
|
148
|
+
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@router.post(
|
|
153
|
+
"/{account_id}/rate-limit-reset-credits/consume",
|
|
154
|
+
response_model=ConsumeResetCreditResponseSchema,
|
|
155
|
+
)
|
|
156
|
+
async def consume_rate_limit_reset_credit(
|
|
157
|
+
request: Request,
|
|
158
|
+
account_id: str,
|
|
159
|
+
payload: AccountUsageResetConsumeRequest | None = None,
|
|
160
|
+
principal: DashboardPrincipal = Depends(require_dashboard_permission(Permission.ACCOUNTS_WRITE)),
|
|
161
|
+
context: AccountsContext = Depends(get_accounts_context),
|
|
162
|
+
) -> ConsumeResetCreditResponseSchema:
|
|
163
|
+
account = await context.repository.get_by_id(account_id)
|
|
164
|
+
# A pending-deletion account is gone from the operator's point of view:
|
|
165
|
+
# the synchronous delete 404'd here once the row was removed.
|
|
166
|
+
if account is None or account.delete_requested_at is not None:
|
|
167
|
+
raise DashboardNotFoundError("Account not found", code="account_not_found")
|
|
168
|
+
|
|
169
|
+
store = get_rate_limit_reset_credits_store()
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
outcome = await _redeem_soonest_reset_credit(
|
|
173
|
+
account=account,
|
|
174
|
+
store=store,
|
|
175
|
+
encryptor=TokenEncryptor(),
|
|
176
|
+
lock_session=getattr(context, "session", None),
|
|
177
|
+
auth_manager=context.service._auth_manager,
|
|
178
|
+
refresh_usage=_build_refresh_usage_callback(context),
|
|
179
|
+
resolve_route=_resolve_reset_credit_route,
|
|
180
|
+
redeem_request_id=payload.redeem_request_id if payload is not None else None,
|
|
181
|
+
)
|
|
182
|
+
except RefreshError as exc:
|
|
183
|
+
if exc.is_permanent:
|
|
184
|
+
get_account_selection_cache().invalidate()
|
|
185
|
+
raise DashboardConflictError(
|
|
186
|
+
f"Reset credit consume could not refresh account credentials: {exc.message}",
|
|
187
|
+
code="account_reset_credit_refresh_failed",
|
|
188
|
+
) from exc
|
|
189
|
+
except UpstreamProxyRouteError as exc:
|
|
190
|
+
raise DashboardServiceUnavailableError(
|
|
191
|
+
f"Reset credit consume upstream proxy route unavailable: {exc.reason}",
|
|
192
|
+
code="account_reset_credit_upstream_route_unavailable",
|
|
193
|
+
) from exc
|
|
194
|
+
|
|
195
|
+
AuditService.log_async(
|
|
196
|
+
"account_rate_limit_reset_credit_consumed",
|
|
197
|
+
actor_ip=request.client.host if request.client else None,
|
|
198
|
+
actor=AuditActor.from_principal(principal),
|
|
199
|
+
target=AuditTarget("account", account_id),
|
|
200
|
+
details={
|
|
201
|
+
"account_id": account_id,
|
|
202
|
+
"consume_code": outcome.response.code,
|
|
203
|
+
"windows_reset": outcome.response.windows_reset,
|
|
204
|
+
"available_reset_credits_before": outcome.available_count_before,
|
|
205
|
+
"available_reset_credits_after": outcome.available_count_after,
|
|
206
|
+
},
|
|
207
|
+
)
|
|
208
|
+
return outcome.response
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
async def _redeem_soonest_reset_credit(
|
|
212
|
+
*,
|
|
213
|
+
account: Account,
|
|
214
|
+
store: RateLimitResetCreditsStore,
|
|
215
|
+
encryptor: TokenEncryptor,
|
|
216
|
+
lock_session: AsyncSession | None = None,
|
|
217
|
+
fetch_fn: FetchFn | None = None,
|
|
218
|
+
consume_fn: ConsumeFn | None = None,
|
|
219
|
+
auth_manager: AuthManager | None = None,
|
|
220
|
+
refresh_usage: RefreshUsageFn | None = None,
|
|
221
|
+
resolve_route: ResolveRouteFn | None = None,
|
|
222
|
+
redeem_request_id: str | None = None,
|
|
223
|
+
skip_if_redeem_request_pinned: bool = False,
|
|
224
|
+
expected_credit_id: str | None = None,
|
|
225
|
+
expected_credit_expires_at: datetime | None = None,
|
|
226
|
+
) -> _RedeemResetCreditOutcome:
|
|
227
|
+
_assert_account_can_redeem_reset_credit(account)
|
|
228
|
+
effective_fetch_fn = fetch_fn or fetch_reset_credits
|
|
229
|
+
effective_consume_fn = consume_fn or consume_reset_credit
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
async with serialize_reset_credit_redeem(account.id, session=lock_session):
|
|
233
|
+
return await _redeem_soonest_reset_credit_locked(
|
|
234
|
+
account=account,
|
|
235
|
+
store=store,
|
|
236
|
+
encryptor=encryptor,
|
|
237
|
+
effective_fetch_fn=effective_fetch_fn,
|
|
238
|
+
effective_consume_fn=effective_consume_fn,
|
|
239
|
+
auth_manager=auth_manager,
|
|
240
|
+
refresh_usage=refresh_usage,
|
|
241
|
+
resolve_route=resolve_route,
|
|
242
|
+
redeem_request_id=redeem_request_id,
|
|
243
|
+
skip_if_redeem_request_pinned=skip_if_redeem_request_pinned,
|
|
244
|
+
expected_credit_id=expected_credit_id,
|
|
245
|
+
expected_credit_expires_at=expected_credit_expires_at,
|
|
246
|
+
)
|
|
247
|
+
except RedeemClaimTimeoutError as exc:
|
|
248
|
+
raise DashboardConflictError(
|
|
249
|
+
"Another reset credit redemption is already in progress for this account",
|
|
250
|
+
code="reset_credit_redeem_in_progress",
|
|
251
|
+
) from exc
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@asynccontextmanager
|
|
255
|
+
async def serialize_reset_credit_redeem(
|
|
256
|
+
account_id: str,
|
|
257
|
+
*,
|
|
258
|
+
session: AsyncSession | None,
|
|
259
|
+
):
|
|
260
|
+
"""Serialize the per-account redeem section across replicas.
|
|
261
|
+
|
|
262
|
+
Raises ``RedeemClaimTimeoutError`` when the SQLite claim stays contended
|
|
263
|
+
past the acquisition timeout; callers map it to their surface's error
|
|
264
|
+
envelope (dashboard vs ``/v1/*`` OpenAI).
|
|
265
|
+
"""
|
|
266
|
+
if session is not None:
|
|
267
|
+
dialect = session.get_bind().dialect.name
|
|
268
|
+
if dialect == "postgresql":
|
|
269
|
+
await _acquire_postgresql_reset_credit_redeem_lock(session, account_id)
|
|
270
|
+
yield
|
|
271
|
+
return
|
|
272
|
+
if dialect == "sqlite":
|
|
273
|
+
# A durable claim row is the sole serializer here so processes
|
|
274
|
+
# sharing one SQLite file exclude each other, not just tasks in
|
|
275
|
+
# this process. Crashed holders are recovered via lease expiry;
|
|
276
|
+
# live holders keep the lease renewed via a heartbeat task so a
|
|
277
|
+
# legitimately slow redemption is not taken over mid-section.
|
|
278
|
+
holder_id = new_redeem_claim_holder_id()
|
|
279
|
+
await acquire_redeem_claim(account_id, holder_id)
|
|
280
|
+
heartbeat = asyncio.create_task(
|
|
281
|
+
renew_redeem_claim_periodically(account_id, holder_id),
|
|
282
|
+
name=f"reset-credit-redeem-claim-heartbeat:{account_id}",
|
|
283
|
+
)
|
|
284
|
+
try:
|
|
285
|
+
yield
|
|
286
|
+
finally:
|
|
287
|
+
|
|
288
|
+
async def cleanup_redeem_claim() -> None:
|
|
289
|
+
heartbeat.cancel()
|
|
290
|
+
with suppress(asyncio.CancelledError):
|
|
291
|
+
await heartbeat
|
|
292
|
+
await release_redeem_claim(account_id, holder_id)
|
|
293
|
+
|
|
294
|
+
cancellation = await _await_cleanup_deferring_cancellation(cleanup_redeem_claim())
|
|
295
|
+
if cancellation is not None:
|
|
296
|
+
raise cancellation
|
|
297
|
+
return
|
|
298
|
+
|
|
299
|
+
# Direct callers without a DB session keep the in-process lock.
|
|
300
|
+
lock = await get_reset_credit_redeem_lock(account_id)
|
|
301
|
+
async with lock:
|
|
302
|
+
yield
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
async def _acquire_postgresql_reset_credit_redeem_lock(session: AsyncSession, account_id: str) -> None:
|
|
306
|
+
lock_key = f"reset-credit-redeem:{account_id}"
|
|
307
|
+
await session.execute(
|
|
308
|
+
text("SELECT pg_advisory_xact_lock(hashtext(:lock_key))"),
|
|
309
|
+
{"lock_key": lock_key},
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
async def _redeem_soonest_reset_credit_locked(
|
|
314
|
+
*,
|
|
315
|
+
account: Account,
|
|
316
|
+
store: RateLimitResetCreditsStore,
|
|
317
|
+
encryptor: TokenEncryptor,
|
|
318
|
+
effective_fetch_fn: FetchFn,
|
|
319
|
+
effective_consume_fn: ConsumeFn,
|
|
320
|
+
auth_manager: AuthManager | None,
|
|
321
|
+
refresh_usage: RefreshUsageFn | None,
|
|
322
|
+
resolve_route: ResolveRouteFn | None,
|
|
323
|
+
redeem_request_id: str | None,
|
|
324
|
+
skip_if_redeem_request_pinned: bool,
|
|
325
|
+
expected_credit_id: str | None,
|
|
326
|
+
expected_credit_expires_at: datetime | None,
|
|
327
|
+
) -> _RedeemResetCreditOutcome:
|
|
328
|
+
redeem_account = account
|
|
329
|
+
if auth_manager is not None:
|
|
330
|
+
redeem_account = await auth_manager.ensure_fresh(account, force=False)
|
|
331
|
+
|
|
332
|
+
# Every accepted consume MUST participate in the durable ledger. When the
|
|
333
|
+
# caller supplies no redeem_request_id (the still-supported no-body dashboard
|
|
334
|
+
# path), synthesize a UUID v4 so the selected credit is pinned before the
|
|
335
|
+
# upstream consume instead of forwarding an unrecorded id. Only a
|
|
336
|
+
# CLIENT-supplied id can be an idempotent retry with a pre-existing pin; a
|
|
337
|
+
# freshly synthesized id is never already pinned, so the pin lookup is
|
|
338
|
+
# skipped for it (and would be a guaranteed miss anyway).
|
|
339
|
+
client_supplied_id = redeem_request_id is not None
|
|
340
|
+
if redeem_request_id is None:
|
|
341
|
+
redeem_request_id = uuid.uuid4().hex
|
|
342
|
+
|
|
343
|
+
cached_snapshot = store.get(account.id)
|
|
344
|
+
cached_credit = _select_soonest_available_credit(cached_snapshot)
|
|
345
|
+
pending_credit_id = await get_pinned_redeem_credit_id(account.id, redeem_request_id) if client_supplied_id else None
|
|
346
|
+
if skip_if_redeem_request_pinned and pending_credit_id is not None:
|
|
347
|
+
raise ResetCreditRedeemRequestAlreadyPinned(
|
|
348
|
+
account_id=account.id,
|
|
349
|
+
redeem_request_id=redeem_request_id,
|
|
350
|
+
credit_id=pending_credit_id,
|
|
351
|
+
)
|
|
352
|
+
if cached_credit is None and pending_credit_id is None:
|
|
353
|
+
raise DashboardConflictError("No available reset credit", code="no_available_reset_credit")
|
|
354
|
+
|
|
355
|
+
access_token = encryptor.decrypt(redeem_account.access_token_encrypted)
|
|
356
|
+
route: ResolvedUpstreamRoute | None = None
|
|
357
|
+
if resolve_route is not None:
|
|
358
|
+
route = await resolve_route(redeem_account)
|
|
359
|
+
|
|
360
|
+
try:
|
|
361
|
+
credits_response = await effective_fetch_fn(
|
|
362
|
+
access_token,
|
|
363
|
+
redeem_account.chatgpt_account_id,
|
|
364
|
+
route=route,
|
|
365
|
+
allow_direct_egress=route is None,
|
|
366
|
+
)
|
|
367
|
+
except ResetCreditFetchError as exc:
|
|
368
|
+
raise _translate_fetch_error(exc) from exc
|
|
369
|
+
|
|
370
|
+
credit = (
|
|
371
|
+
_select_available_credit_by_id(credits_response, expected_credit_id)
|
|
372
|
+
if expected_credit_id is not None
|
|
373
|
+
else _select_soonest_available_credit_from_response(credits_response)
|
|
374
|
+
)
|
|
375
|
+
if pending_credit_id is not None:
|
|
376
|
+
credit_id = pending_credit_id
|
|
377
|
+
elif credit is None:
|
|
378
|
+
# The fresh pre-consume fetch is authoritative: no available credit
|
|
379
|
+
# means none is redeemable now. With no durable pin
|
|
380
|
+
# (``pending_credit_id is None`` here), there is no proof this is an
|
|
381
|
+
# idempotent retry of a prior attempt, so a stale local snapshot MUST
|
|
382
|
+
# NOT be pinned and consumed — that would burn an upstream consume for
|
|
383
|
+
# an unavailable/already-redeemed credit. Replace the stale snapshot
|
|
384
|
+
# with the fresh (empty) one and return a conflict.
|
|
385
|
+
await store.set(account.id, build_snapshot(credits_response))
|
|
386
|
+
raise DashboardConflictError("No available reset credit", code="no_available_reset_credit")
|
|
387
|
+
elif expected_credit_expires_at is not None and not _same_credit_expiry(
|
|
388
|
+
credit.expires_at,
|
|
389
|
+
expected_credit_expires_at,
|
|
390
|
+
):
|
|
391
|
+
await store.set(account.id, build_snapshot(credits_response))
|
|
392
|
+
raise DashboardConflictError("Target reset credit changed", code="target_reset_credit_changed")
|
|
393
|
+
else:
|
|
394
|
+
# Pin the selected credit before the consume: a synthesized or
|
|
395
|
+
# client-supplied id is now always recorded, so a lost consume response
|
|
396
|
+
# leaves a durable pin any replica can reuse.
|
|
397
|
+
credit_id = await pin_redeem_request(account.id, redeem_request_id, credit.id)
|
|
398
|
+
|
|
399
|
+
try:
|
|
400
|
+
result = await effective_consume_fn(
|
|
401
|
+
access_token,
|
|
402
|
+
redeem_account.chatgpt_account_id,
|
|
403
|
+
credit_id,
|
|
404
|
+
redeem_request_id=redeem_request_id,
|
|
405
|
+
route=route,
|
|
406
|
+
allow_direct_egress=route is None,
|
|
407
|
+
)
|
|
408
|
+
except ConsumeResetCreditError as exc:
|
|
409
|
+
raise _translate_consume_error(exc) from exc
|
|
410
|
+
|
|
411
|
+
redeemed_at = result.credit.redeemed_at if result.credit else None
|
|
412
|
+
available_count_after = max(0, credits_response.available_count - 1)
|
|
413
|
+
await store.invalidate(account.id)
|
|
414
|
+
await bump_cache_invalidation_local(NAMESPACE_RESET_CREDITS)
|
|
415
|
+
|
|
416
|
+
if refresh_usage is not None:
|
|
417
|
+
try:
|
|
418
|
+
await refresh_usage(redeem_account)
|
|
419
|
+
except Exception:
|
|
420
|
+
logger.warning(
|
|
421
|
+
"Reset credit consume succeeded but usage refresh failed account_id=%s",
|
|
422
|
+
account.id,
|
|
423
|
+
exc_info=True,
|
|
424
|
+
)
|
|
425
|
+
await _try_restore_reset_credits_snapshot_after_consume(
|
|
426
|
+
account=account,
|
|
427
|
+
redeem_account=redeem_account,
|
|
428
|
+
encryptor=encryptor,
|
|
429
|
+
store=store,
|
|
430
|
+
fetch_fn=effective_fetch_fn,
|
|
431
|
+
resolve_route=resolve_route,
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
return _RedeemResetCreditOutcome(
|
|
435
|
+
response=ConsumeResetCreditResponseSchema(
|
|
436
|
+
code=result.code,
|
|
437
|
+
windows_reset=result.windows_reset,
|
|
438
|
+
redeemed_at=redeemed_at,
|
|
439
|
+
),
|
|
440
|
+
available_count_before=credits_response.available_count,
|
|
441
|
+
available_count_after=available_count_after,
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _assert_account_can_redeem_reset_credit(account: Account) -> None:
|
|
446
|
+
if account.status in _NON_REDEEMABLE_STATUSES or not account.chatgpt_account_id:
|
|
447
|
+
msg = (
|
|
448
|
+
f"Account is {account.status.value} and cannot redeem a reset credit"
|
|
449
|
+
if account.status in _NON_REDEEMABLE_STATUSES
|
|
450
|
+
else "Account has no ChatGPT account ID and cannot redeem a reset credit"
|
|
451
|
+
)
|
|
452
|
+
raise DashboardConflictError(
|
|
453
|
+
msg,
|
|
454
|
+
code="account_not_reset_credit_applicable",
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _build_refresh_usage_callback(context: AccountsContext) -> RefreshUsageFn | None:
|
|
459
|
+
usage_updater = context.service._usage_updater
|
|
460
|
+
if usage_updater is None:
|
|
461
|
+
return None
|
|
462
|
+
|
|
463
|
+
async def refresh_usage(account: Account) -> None:
|
|
464
|
+
refreshed = await usage_updater.force_refresh(account)
|
|
465
|
+
if not refreshed:
|
|
466
|
+
raise RuntimeError(f"Forced usage refresh returned no update for account {account.id}")
|
|
467
|
+
get_account_selection_cache().invalidate()
|
|
468
|
+
|
|
469
|
+
return refresh_usage
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
async def _resolve_reset_credit_route(account: Account) -> ResolvedUpstreamRoute | None:
|
|
473
|
+
return await _resolve_upstream_route_for_account(account, operation="rate_limit_reset_consume")
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
async def _try_restore_reset_credits_snapshot_after_consume(
|
|
477
|
+
*,
|
|
478
|
+
account: Account,
|
|
479
|
+
redeem_account: Account,
|
|
480
|
+
encryptor: TokenEncryptor,
|
|
481
|
+
store: RateLimitResetCreditsStore,
|
|
482
|
+
fetch_fn: FetchFn,
|
|
483
|
+
resolve_route: ResolveRouteFn | None,
|
|
484
|
+
) -> None:
|
|
485
|
+
"""Best-effort cache repopulation when usage refresh fails after a successful consume."""
|
|
486
|
+
try:
|
|
487
|
+
access_token = encryptor.decrypt(redeem_account.access_token_encrypted)
|
|
488
|
+
route: ResolvedUpstreamRoute | None = None
|
|
489
|
+
if resolve_route is not None:
|
|
490
|
+
route = await resolve_route(redeem_account)
|
|
491
|
+
credits_response = await fetch_fn(
|
|
492
|
+
access_token,
|
|
493
|
+
redeem_account.chatgpt_account_id,
|
|
494
|
+
route=route,
|
|
495
|
+
allow_direct_egress=route is None,
|
|
496
|
+
)
|
|
497
|
+
except Exception:
|
|
498
|
+
logger.warning(
|
|
499
|
+
"Reset credit consume post-refresh re-fetch failed account_id=%s",
|
|
500
|
+
account.id,
|
|
501
|
+
exc_info=True,
|
|
502
|
+
)
|
|
503
|
+
return
|
|
504
|
+
await store.set(account.id, build_snapshot(credits_response))
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
async def get_reset_credit_redeem_lock(account_id: str) -> asyncio.Lock:
|
|
508
|
+
lock = _redeem_locks.get(account_id)
|
|
509
|
+
if lock is not None:
|
|
510
|
+
return lock
|
|
511
|
+
async with _redeem_locks_registry_lock:
|
|
512
|
+
lock = _redeem_locks.get(account_id)
|
|
513
|
+
if lock is None:
|
|
514
|
+
lock = asyncio.Lock()
|
|
515
|
+
_redeem_locks[account_id] = lock
|
|
516
|
+
return lock
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def _translate_fetch_error(exc: ResetCreditFetchError) -> Exception:
|
|
520
|
+
if exc.status_code == 401:
|
|
521
|
+
return DashboardAuthError(exc.message, code=exc.code)
|
|
522
|
+
if exc.status_code == 403:
|
|
523
|
+
return DashboardPermissionError(exc.message, code=exc.code)
|
|
524
|
+
if exc.status_code == 409:
|
|
525
|
+
return DashboardConflictError(exc.message, code=exc.code)
|
|
526
|
+
return DashboardServiceUnavailableError(exc.message, code=exc.code)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _translate_consume_error(exc: ConsumeResetCreditError) -> Exception:
|
|
530
|
+
if exc.status_code == 401:
|
|
531
|
+
return DashboardAuthError(exc.message, code=exc.code)
|
|
532
|
+
if exc.status_code == 403:
|
|
533
|
+
return DashboardPermissionError(exc.message, code=exc.code)
|
|
534
|
+
if exc.status_code == 409:
|
|
535
|
+
return DashboardConflictError(exc.message, code=exc.code)
|
|
536
|
+
return DashboardServiceUnavailableError(exc.message, code=exc.code)
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _select_soonest_available_credit(
|
|
540
|
+
snapshot: RateLimitResetCreditsSnapshot | None,
|
|
541
|
+
) -> ResetCreditItem | None:
|
|
542
|
+
if snapshot is None:
|
|
543
|
+
return None
|
|
544
|
+
return _select_soonest_available_credit_from_items(snapshot.credits, snapshot.available_count)
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def _select_soonest_available_credit_from_response(
|
|
548
|
+
response: ResetCreditsResponse,
|
|
549
|
+
) -> ResetCreditItem | None:
|
|
550
|
+
return _select_soonest_available_credit_from_items(response.credits, response.available_count)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _select_available_credit_by_id(
|
|
554
|
+
response: ResetCreditsResponse,
|
|
555
|
+
credit_id: str,
|
|
556
|
+
) -> ResetCreditItem | None:
|
|
557
|
+
if response.available_count <= 0:
|
|
558
|
+
return None
|
|
559
|
+
for credit in response.credits:
|
|
560
|
+
if credit.id == credit_id and credit.status == "available":
|
|
561
|
+
return credit
|
|
562
|
+
return None
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def _same_credit_expiry(left: datetime | None, right: datetime | None) -> bool:
|
|
566
|
+
if left is None or right is None:
|
|
567
|
+
return left is right
|
|
568
|
+
if left.tzinfo is None:
|
|
569
|
+
left = left.replace(tzinfo=timezone.utc)
|
|
570
|
+
else:
|
|
571
|
+
left = left.astimezone(timezone.utc)
|
|
572
|
+
if right.tzinfo is None:
|
|
573
|
+
right = right.replace(tzinfo=timezone.utc)
|
|
574
|
+
else:
|
|
575
|
+
right = right.astimezone(timezone.utc)
|
|
576
|
+
return left == right
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def _select_soonest_available_credit_from_items(
|
|
580
|
+
credits: list[ResetCreditItem],
|
|
581
|
+
available_count: int,
|
|
582
|
+
) -> ResetCreditItem | None:
|
|
583
|
+
if available_count <= 0:
|
|
584
|
+
return None
|
|
585
|
+
available = [credit for credit in credits if credit.status == "available"]
|
|
586
|
+
if not available:
|
|
587
|
+
return None
|
|
588
|
+
far_future = datetime.max.replace(tzinfo=timezone.utc)
|
|
589
|
+
return min(available, key=lambda credit: credit.expires_at or far_future)
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def _snapshot_to_response(
|
|
593
|
+
snapshot: RateLimitResetCreditsSnapshot | None,
|
|
594
|
+
) -> RateLimitResetCreditsSnapshotResponse | None:
|
|
595
|
+
if snapshot is None:
|
|
596
|
+
return None
|
|
597
|
+
return RateLimitResetCreditsSnapshotResponse(
|
|
598
|
+
available_count=snapshot.available_count,
|
|
599
|
+
nearest_expires_at=snapshot.nearest_expires_at,
|
|
600
|
+
credits=[ResetCreditItemResponse.model_validate(credit.model_dump()) for credit in snapshot.credits],
|
|
601
|
+
)
|