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,809 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
from datetime import timedelta
|
|
7
|
+
from typing import cast
|
|
8
|
+
from uuid import uuid4
|
|
9
|
+
|
|
10
|
+
import aiohttp
|
|
11
|
+
from pydantic import ValidationError
|
|
12
|
+
from sqlalchemy.exc import OperationalError
|
|
13
|
+
|
|
14
|
+
from app.core.auth import (
|
|
15
|
+
DEFAULT_EMAIL,
|
|
16
|
+
DEFAULT_PLAN,
|
|
17
|
+
claims_from_auth,
|
|
18
|
+
generate_unique_account_id,
|
|
19
|
+
parse_auth_json,
|
|
20
|
+
token_expiry_epoch_ms,
|
|
21
|
+
)
|
|
22
|
+
from app.core.auth.api_key_cache import get_api_key_cache
|
|
23
|
+
from app.core.auth.refresh import RefreshError
|
|
24
|
+
from app.core.cache.invalidation import NAMESPACE_API_KEY, get_cache_invalidation_poller
|
|
25
|
+
from app.core.clients.http import lease_http_session
|
|
26
|
+
from app.core.clients.usage import (
|
|
27
|
+
ConsumeRateLimitResetCreditResponse,
|
|
28
|
+
UsageFetchError,
|
|
29
|
+
consume_rate_limit_reset_credit,
|
|
30
|
+
fetch_usage,
|
|
31
|
+
)
|
|
32
|
+
from app.core.config.settings import get_settings
|
|
33
|
+
from app.core.crypto import TokenEncryptor
|
|
34
|
+
from app.core.openai.host_models import resolve_default_host_model
|
|
35
|
+
from app.core.plan_types import coerce_account_plan_type
|
|
36
|
+
from app.core.upstream_proxy import ResolvedUpstreamRoute, UpstreamProxyRouteError, resolve_upstream_route
|
|
37
|
+
from app.core.upstream_proxy.cache import get_upstream_route_cache
|
|
38
|
+
from app.core.upstream_proxy.resolver import _is_missing_upstream_proxy_schema
|
|
39
|
+
from app.core.usage.models import UsagePayload
|
|
40
|
+
from app.core.utils.time import naive_utc_to_epoch, to_utc_naive, utcnow
|
|
41
|
+
from app.db.models import Account, AccountStatus, DashboardSettings
|
|
42
|
+
from app.db.session import get_background_session
|
|
43
|
+
from app.modules.accounts.auth_manager import AuthManager
|
|
44
|
+
from app.modules.accounts.deletion import request_account_deletion_run
|
|
45
|
+
from app.modules.accounts.mappers import build_account_summaries, build_account_usage_trends
|
|
46
|
+
from app.modules.accounts.repository import AccountsRepository
|
|
47
|
+
from app.modules.accounts.schemas import (
|
|
48
|
+
AccountAdditionalQuota,
|
|
49
|
+
AccountAdditionalWindow,
|
|
50
|
+
AccountAuthExportResponse,
|
|
51
|
+
AccountAuthExportTokens,
|
|
52
|
+
AccountImportResponse,
|
|
53
|
+
AccountOpenCodeAuthExportAccount,
|
|
54
|
+
AccountProbeResponse,
|
|
55
|
+
AccountRequestUsage,
|
|
56
|
+
AccountSummary,
|
|
57
|
+
AccountTrendsResponse,
|
|
58
|
+
AccountUsageResetConsumeResponse,
|
|
59
|
+
AccountUsageResetCredits,
|
|
60
|
+
AccountUsageResetCreditsResponse,
|
|
61
|
+
CodexAuthJson,
|
|
62
|
+
CodexAuthTokens,
|
|
63
|
+
OpenCodeAuthJson,
|
|
64
|
+
OpenCodeOAuthAuth,
|
|
65
|
+
)
|
|
66
|
+
from app.modules.limit_warmup.repository import LimitWarmupRepository
|
|
67
|
+
from app.modules.proxy.account_cache import (
|
|
68
|
+
clear_account_routing_unavailable,
|
|
69
|
+
get_account_selection_cache,
|
|
70
|
+
mark_account_routing_unavailable,
|
|
71
|
+
propagate_account_routing_change,
|
|
72
|
+
)
|
|
73
|
+
from app.modules.rate_limit_reset_credits.store import get_rate_limit_reset_credits_store
|
|
74
|
+
from app.modules.usage.additional_quota_keys import (
|
|
75
|
+
get_additional_display_label_for_quota_key,
|
|
76
|
+
get_additional_quota_routing_policy,
|
|
77
|
+
)
|
|
78
|
+
from app.modules.usage.repository import AdditionalUsageRepository, UsageRepository
|
|
79
|
+
from app.modules.usage.updater import AdditionalUsageRepositoryPort, UsageUpdater
|
|
80
|
+
|
|
81
|
+
logger = logging.getLogger(__name__)
|
|
82
|
+
|
|
83
|
+
_SPARKLINE_DAYS = 7
|
|
84
|
+
_DETAIL_BUCKET_SECONDS = 3600 # 1h → 168 points
|
|
85
|
+
|
|
86
|
+
PROBE_REQUEST_TIMEOUT_SECONDS = 30.0
|
|
87
|
+
PROBE_CONNECT_TIMEOUT_SECONDS = 10.0
|
|
88
|
+
# Network/upstream failure sentinel for ``probe_status_code`` — kept as ``0`` so
|
|
89
|
+
# the value is distinguishable from any real HTTP status the upstream might
|
|
90
|
+
# return.
|
|
91
|
+
PROBE_NETWORK_FAILURE_STATUS = 0
|
|
92
|
+
IMPORT_PROXY_REQUIRED_PAUSE_REASON = "upstream_proxy_required_on_import"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class InvalidAuthJsonError(Exception):
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class AccountNotProbableError(Exception):
|
|
100
|
+
"""Raised when an account is in a status that disallows probing."""
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class AccountStateTransitionError(Exception):
|
|
104
|
+
"""Raised when an operator action is not valid for the account state."""
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class AccountUsageResetCreditsUnavailableError(Exception):
|
|
108
|
+
"""Raised when a dashboard account cannot read upstream reset credits."""
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class AccountUsageResetConsumeUnavailableError(Exception):
|
|
112
|
+
"""Raised when a dashboard account cannot consume upstream reset credits."""
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class AccountsService:
|
|
116
|
+
def __init__(
|
|
117
|
+
self,
|
|
118
|
+
repo: AccountsRepository,
|
|
119
|
+
usage_repo: UsageRepository | None = None,
|
|
120
|
+
additional_usage_repo: AdditionalUsageRepository | AdditionalUsageRepositoryPort | None = None,
|
|
121
|
+
limit_warmup_repo: LimitWarmupRepository | None = None,
|
|
122
|
+
auth_manager: AuthManager | None = None,
|
|
123
|
+
) -> None:
|
|
124
|
+
self._repo = repo
|
|
125
|
+
self._usage_repo = usage_repo
|
|
126
|
+
self._additional_usage_repo = additional_usage_repo
|
|
127
|
+
self._limit_warmup_repo = limit_warmup_repo
|
|
128
|
+
self._usage_updater = UsageUpdater(usage_repo, repo, additional_usage_repo) if usage_repo else None
|
|
129
|
+
self._encryptor = TokenEncryptor()
|
|
130
|
+
self._auth_manager = auth_manager
|
|
131
|
+
|
|
132
|
+
async def list_accounts(
|
|
133
|
+
self,
|
|
134
|
+
*,
|
|
135
|
+
account_ids: list[str] | None = None,
|
|
136
|
+
redact_identity: bool = False,
|
|
137
|
+
) -> list[AccountSummary]:
|
|
138
|
+
accounts = (
|
|
139
|
+
await self._repo.list_accounts_by_ids(account_ids)
|
|
140
|
+
if account_ids is not None
|
|
141
|
+
else await self._repo.list_accounts()
|
|
142
|
+
)
|
|
143
|
+
if not accounts:
|
|
144
|
+
return []
|
|
145
|
+
visible_account_ids = [account.id for account in accounts]
|
|
146
|
+
account_id_set = set(visible_account_ids)
|
|
147
|
+
usage_account_ids = visible_account_ids if account_ids is not None else None
|
|
148
|
+
primary_usage = (
|
|
149
|
+
await self._usage_repo.latest_by_account(window="primary", account_ids=usage_account_ids)
|
|
150
|
+
if self._usage_repo
|
|
151
|
+
else {}
|
|
152
|
+
)
|
|
153
|
+
secondary_usage = (
|
|
154
|
+
await self._usage_repo.latest_by_account(window="secondary", account_ids=usage_account_ids)
|
|
155
|
+
if self._usage_repo
|
|
156
|
+
else {}
|
|
157
|
+
)
|
|
158
|
+
monthly_usage = (
|
|
159
|
+
await self._usage_repo.latest_by_account(window="monthly", account_ids=usage_account_ids)
|
|
160
|
+
if self._usage_repo
|
|
161
|
+
else {}
|
|
162
|
+
)
|
|
163
|
+
request_usage_rows = await self._repo.list_request_usage_summary_by_account(visible_account_ids)
|
|
164
|
+
limit_warmups_by_account = (
|
|
165
|
+
await self._limit_warmup_repo.latest_by_account(visible_account_ids) if self._limit_warmup_repo else {}
|
|
166
|
+
)
|
|
167
|
+
request_usage_by_account = {
|
|
168
|
+
account_id: AccountRequestUsage(
|
|
169
|
+
request_count=row.request_count,
|
|
170
|
+
total_tokens=row.total_tokens,
|
|
171
|
+
cached_input_tokens=row.cached_input_tokens,
|
|
172
|
+
total_cost_usd=row.total_cost_usd,
|
|
173
|
+
)
|
|
174
|
+
for account_id, row in request_usage_rows.items()
|
|
175
|
+
}
|
|
176
|
+
additional_quotas_by_account: dict[str, list[AccountAdditionalQuota]] = {}
|
|
177
|
+
additional_usage_repo = cast(AdditionalUsageRepository | None, self._additional_usage_repo)
|
|
178
|
+
if additional_usage_repo:
|
|
179
|
+
additional_quota_routing_overrides = await self._repo.additional_quota_routing_policy_overrides()
|
|
180
|
+
quota_keys = await additional_usage_repo.list_quota_keys(account_ids=visible_account_ids)
|
|
181
|
+
for quota_key in quota_keys:
|
|
182
|
+
primary_entries = await additional_usage_repo.latest_by_account(
|
|
183
|
+
quota_key,
|
|
184
|
+
"primary",
|
|
185
|
+
account_ids=visible_account_ids,
|
|
186
|
+
)
|
|
187
|
+
secondary_entries = await additional_usage_repo.latest_by_account(
|
|
188
|
+
quota_key,
|
|
189
|
+
"secondary",
|
|
190
|
+
account_ids=visible_account_ids,
|
|
191
|
+
)
|
|
192
|
+
for account_id in (set(primary_entries) | set(secondary_entries)) & account_id_set:
|
|
193
|
+
primary_entry = primary_entries.get(account_id)
|
|
194
|
+
secondary_entry = secondary_entries.get(account_id)
|
|
195
|
+
reference_entry = primary_entry or secondary_entry
|
|
196
|
+
if reference_entry is None:
|
|
197
|
+
continue
|
|
198
|
+
additional_quotas_by_account.setdefault(account_id, []).append(
|
|
199
|
+
AccountAdditionalQuota(
|
|
200
|
+
quota_key=quota_key,
|
|
201
|
+
limit_name=reference_entry.limit_name,
|
|
202
|
+
metered_feature=reference_entry.metered_feature,
|
|
203
|
+
display_label=get_additional_display_label_for_quota_key(quota_key)
|
|
204
|
+
or reference_entry.limit_name,
|
|
205
|
+
routing_policy=get_additional_quota_routing_policy(
|
|
206
|
+
quota_key,
|
|
207
|
+
overrides=additional_quota_routing_overrides,
|
|
208
|
+
),
|
|
209
|
+
primary_window=AccountAdditionalWindow(
|
|
210
|
+
used_percent=primary_entry.used_percent,
|
|
211
|
+
reset_at=primary_entry.reset_at,
|
|
212
|
+
window_minutes=primary_entry.window_minutes,
|
|
213
|
+
)
|
|
214
|
+
if primary_entry is not None
|
|
215
|
+
else None,
|
|
216
|
+
secondary_window=AccountAdditionalWindow(
|
|
217
|
+
used_percent=secondary_entry.used_percent,
|
|
218
|
+
reset_at=secondary_entry.reset_at,
|
|
219
|
+
window_minutes=secondary_entry.window_minutes,
|
|
220
|
+
)
|
|
221
|
+
if secondary_entry is not None
|
|
222
|
+
else None,
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
for account_quota_list in additional_quotas_by_account.values():
|
|
226
|
+
account_quota_list.sort(key=lambda quota: quota.display_label or quota.quota_key or quota.limit_name)
|
|
227
|
+
|
|
228
|
+
return build_account_summaries(
|
|
229
|
+
accounts=accounts,
|
|
230
|
+
primary_usage=primary_usage,
|
|
231
|
+
secondary_usage=secondary_usage,
|
|
232
|
+
monthly_usage=monthly_usage,
|
|
233
|
+
request_usage_by_account=request_usage_by_account,
|
|
234
|
+
additional_quotas_by_account=additional_quotas_by_account,
|
|
235
|
+
limit_warmups_by_account=limit_warmups_by_account,
|
|
236
|
+
encryptor=self._encryptor,
|
|
237
|
+
redact_identity=redact_identity,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
async def get_account_trends(self, account_id: str) -> AccountTrendsResponse | None:
|
|
241
|
+
account = await self._get_visible_account(account_id)
|
|
242
|
+
if not account or not self._usage_repo:
|
|
243
|
+
return None
|
|
244
|
+
now = utcnow()
|
|
245
|
+
since = now - timedelta(days=_SPARKLINE_DAYS)
|
|
246
|
+
since_epoch = naive_utc_to_epoch(since)
|
|
247
|
+
bucket_count = (_SPARKLINE_DAYS * 24 * 3600) // _DETAIL_BUCKET_SECONDS
|
|
248
|
+
buckets = await self._usage_repo.trends_by_bucket(
|
|
249
|
+
since=since,
|
|
250
|
+
bucket_seconds=_DETAIL_BUCKET_SECONDS,
|
|
251
|
+
account_id=account_id,
|
|
252
|
+
)
|
|
253
|
+
trends = build_account_usage_trends(buckets, since_epoch, _DETAIL_BUCKET_SECONDS, bucket_count)
|
|
254
|
+
trend = trends.get(account_id)
|
|
255
|
+
return AccountTrendsResponse(
|
|
256
|
+
account_id=account_id,
|
|
257
|
+
primary=trend.primary if trend else [],
|
|
258
|
+
secondary=trend.secondary if trend else [],
|
|
259
|
+
secondary_scheduled=trend.secondary_scheduled if trend else [],
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
async def get_usage_reset_credits(self, account_id: str) -> AccountUsageResetCreditsResponse | None:
|
|
263
|
+
account = await self._get_visible_account(account_id)
|
|
264
|
+
if account is None:
|
|
265
|
+
return None
|
|
266
|
+
if account.status in (AccountStatus.PAUSED, AccountStatus.DEACTIVATED):
|
|
267
|
+
raise AccountUsageResetCreditsUnavailableError(
|
|
268
|
+
f"Account is {account.status.value} and cannot fetch usage reset credits",
|
|
269
|
+
)
|
|
270
|
+
if self._auth_manager is not None:
|
|
271
|
+
try:
|
|
272
|
+
account = await self._auth_manager.ensure_fresh(account)
|
|
273
|
+
except RefreshError as exc:
|
|
274
|
+
raise AccountUsageResetCreditsUnavailableError(
|
|
275
|
+
f"Account credentials could not be refreshed: {exc.message}",
|
|
276
|
+
) from exc
|
|
277
|
+
if not account.chatgpt_account_id:
|
|
278
|
+
raise AccountUsageResetCreditsUnavailableError("Account is missing ChatGPT account identity")
|
|
279
|
+
|
|
280
|
+
payload = await self._fetch_usage_payload_for_reset_credits(account)
|
|
281
|
+
reset_credits = payload.rate_limit_reset_credits
|
|
282
|
+
available_count = reset_credits.available_count if reset_credits is not None else None
|
|
283
|
+
return AccountUsageResetCreditsResponse(
|
|
284
|
+
account_id=account.id,
|
|
285
|
+
rate_limit_reset_credits=AccountUsageResetCredits(
|
|
286
|
+
available_count=max(0, int(available_count or 0)),
|
|
287
|
+
),
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
async def _fetch_usage_payload_for_reset_credits(self, account: Account) -> UsagePayload:
|
|
291
|
+
access_token = self._encryptor.decrypt(account.access_token_encrypted)
|
|
292
|
+
route = await self._resolve_usage_reset_credit_route(account, operation="usage_reset_credits_read")
|
|
293
|
+
try:
|
|
294
|
+
return await fetch_usage(
|
|
295
|
+
access_token=access_token,
|
|
296
|
+
account_id=account.chatgpt_account_id,
|
|
297
|
+
route=route,
|
|
298
|
+
allow_direct_egress=route is None,
|
|
299
|
+
)
|
|
300
|
+
except UsageFetchError as exc:
|
|
301
|
+
if exc.status_code != 401 or self._auth_manager is None:
|
|
302
|
+
raise
|
|
303
|
+
try:
|
|
304
|
+
account = await self._auth_manager.ensure_fresh(account, force=True)
|
|
305
|
+
except RefreshError as refresh_exc:
|
|
306
|
+
raise AccountUsageResetCreditsUnavailableError(
|
|
307
|
+
f"Account credentials could not be refreshed: {refresh_exc.message}",
|
|
308
|
+
) from refresh_exc
|
|
309
|
+
if not account.chatgpt_account_id:
|
|
310
|
+
raise AccountUsageResetCreditsUnavailableError("Account is missing ChatGPT account identity") from exc
|
|
311
|
+
access_token = self._encryptor.decrypt(account.access_token_encrypted)
|
|
312
|
+
retry_route = await self._resolve_usage_reset_credit_route(account, operation="usage_reset_credits_read")
|
|
313
|
+
return await fetch_usage(
|
|
314
|
+
access_token=access_token,
|
|
315
|
+
account_id=account.chatgpt_account_id,
|
|
316
|
+
route=retry_route,
|
|
317
|
+
allow_direct_egress=retry_route is None,
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
async def consume_usage_reset_credit(
|
|
321
|
+
self,
|
|
322
|
+
account_id: str,
|
|
323
|
+
*,
|
|
324
|
+
redeem_request_id: str | None = None,
|
|
325
|
+
) -> AccountUsageResetConsumeResponse | None:
|
|
326
|
+
account = await self._get_visible_account(account_id)
|
|
327
|
+
if account is None:
|
|
328
|
+
return None
|
|
329
|
+
if account.status in (AccountStatus.PAUSED, AccountStatus.DEACTIVATED):
|
|
330
|
+
raise AccountUsageResetConsumeUnavailableError(
|
|
331
|
+
f"Account is {account.status.value} and cannot consume usage reset credits",
|
|
332
|
+
)
|
|
333
|
+
if self._auth_manager is not None:
|
|
334
|
+
try:
|
|
335
|
+
account = await self._auth_manager.ensure_fresh(account)
|
|
336
|
+
except RefreshError as exc:
|
|
337
|
+
raise AccountUsageResetConsumeUnavailableError(
|
|
338
|
+
f"Account credentials could not be refreshed: {exc.message}",
|
|
339
|
+
) from exc
|
|
340
|
+
if not account.chatgpt_account_id:
|
|
341
|
+
raise AccountUsageResetConsumeUnavailableError("Account is missing ChatGPT account identity")
|
|
342
|
+
|
|
343
|
+
primary_before, secondary_before = await self._latest_usage_percents(account_id)
|
|
344
|
+
status_before = account.status.value
|
|
345
|
+
upstream_response, account = await self._consume_usage_reset_credit(
|
|
346
|
+
account, redeem_request_id=redeem_request_id
|
|
347
|
+
)
|
|
348
|
+
if upstream_response.code in ("reset", "already_redeemed", "no_credit", "nothing_to_reset"):
|
|
349
|
+
await get_rate_limit_reset_credits_store().invalidate(account_id)
|
|
350
|
+
|
|
351
|
+
usage_written = False
|
|
352
|
+
if upstream_response.code in ("reset", "already_redeemed") and self._usage_repo and self._usage_updater:
|
|
353
|
+
usage_written = await self._usage_updater.force_refresh(account)
|
|
354
|
+
get_account_selection_cache().invalidate()
|
|
355
|
+
|
|
356
|
+
refreshed = await self._repo.get_by_id(account_id) or account
|
|
357
|
+
primary_after, secondary_after = await self._latest_usage_percents(account_id)
|
|
358
|
+
|
|
359
|
+
return AccountUsageResetConsumeResponse(
|
|
360
|
+
status="reset",
|
|
361
|
+
account_id=account_id,
|
|
362
|
+
code=upstream_response.code,
|
|
363
|
+
windows_reset=upstream_response.windows_reset,
|
|
364
|
+
usage_written=usage_written,
|
|
365
|
+
primary_used_percent_before=primary_before,
|
|
366
|
+
primary_used_percent_after=primary_after,
|
|
367
|
+
secondary_used_percent_before=secondary_before,
|
|
368
|
+
secondary_used_percent_after=secondary_after,
|
|
369
|
+
account_status_before=status_before,
|
|
370
|
+
account_status_after=refreshed.status.value,
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
async def _consume_usage_reset_credit(
|
|
374
|
+
self,
|
|
375
|
+
account: Account,
|
|
376
|
+
*,
|
|
377
|
+
redeem_request_id: str | None = None,
|
|
378
|
+
) -> tuple[ConsumeRateLimitResetCreditResponse, Account]:
|
|
379
|
+
chatgpt_account_id = account.chatgpt_account_id
|
|
380
|
+
if not chatgpt_account_id:
|
|
381
|
+
raise AccountUsageResetConsumeUnavailableError("Account is missing ChatGPT account identity")
|
|
382
|
+
effective_redeem_request_id = redeem_request_id or str(uuid4())
|
|
383
|
+
access_token = self._encryptor.decrypt(account.access_token_encrypted)
|
|
384
|
+
route = await self._resolve_usage_reset_credit_route(account, operation="usage_reset_credits_consume")
|
|
385
|
+
try:
|
|
386
|
+
response = await consume_rate_limit_reset_credit(
|
|
387
|
+
access_token=access_token,
|
|
388
|
+
account_id=chatgpt_account_id,
|
|
389
|
+
redeem_request_id=effective_redeem_request_id,
|
|
390
|
+
route=route,
|
|
391
|
+
allow_direct_egress=route is None,
|
|
392
|
+
)
|
|
393
|
+
return response, account
|
|
394
|
+
except UsageFetchError as exc:
|
|
395
|
+
if exc.status_code != 401 or self._auth_manager is None:
|
|
396
|
+
raise
|
|
397
|
+
try:
|
|
398
|
+
account = await self._auth_manager.ensure_fresh(account, force=True)
|
|
399
|
+
except RefreshError as refresh_exc:
|
|
400
|
+
raise AccountUsageResetConsumeUnavailableError(
|
|
401
|
+
f"Account credentials could not be refreshed: {refresh_exc.message}",
|
|
402
|
+
) from refresh_exc
|
|
403
|
+
if not account.chatgpt_account_id:
|
|
404
|
+
raise AccountUsageResetConsumeUnavailableError("Account is missing ChatGPT account identity") from exc
|
|
405
|
+
access_token = self._encryptor.decrypt(account.access_token_encrypted)
|
|
406
|
+
retry_route = await self._resolve_usage_reset_credit_route(account, operation="usage_reset_credits_consume")
|
|
407
|
+
response = await consume_rate_limit_reset_credit(
|
|
408
|
+
access_token=access_token,
|
|
409
|
+
account_id=account.chatgpt_account_id,
|
|
410
|
+
redeem_request_id=effective_redeem_request_id,
|
|
411
|
+
route=retry_route,
|
|
412
|
+
allow_direct_egress=retry_route is None,
|
|
413
|
+
)
|
|
414
|
+
return response, account
|
|
415
|
+
|
|
416
|
+
async def _resolve_usage_reset_credit_route(
|
|
417
|
+
self,
|
|
418
|
+
account: Account,
|
|
419
|
+
*,
|
|
420
|
+
operation: str,
|
|
421
|
+
) -> ResolvedUpstreamRoute | None:
|
|
422
|
+
async with get_background_session() as session:
|
|
423
|
+
return await resolve_upstream_route(
|
|
424
|
+
session,
|
|
425
|
+
account_id=account.id,
|
|
426
|
+
operation=operation,
|
|
427
|
+
scope="account",
|
|
428
|
+
encryptor=self._encryptor,
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
async def _get_visible_account(self, account_id: str) -> Account | None:
|
|
432
|
+
"""Account fetch for ID-based operator routes; marked-for-deletion
|
|
433
|
+
rows are gone from the operator's perspective (the synchronous delete
|
|
434
|
+
returned 404 on every one of these routes once the row was removed)
|
|
435
|
+
and MUST NOT keep serving reads, mutations, or decrypted tokens
|
|
436
|
+
during the background drain window."""
|
|
437
|
+
account = await self._repo.get_by_id(account_id)
|
|
438
|
+
if account is None or account.delete_requested_at is not None:
|
|
439
|
+
return None
|
|
440
|
+
return account
|
|
441
|
+
|
|
442
|
+
async def export_auth(self, account_id: str) -> AccountAuthExportResponse | None:
|
|
443
|
+
account = await self._get_visible_account(account_id)
|
|
444
|
+
if account is None:
|
|
445
|
+
return None
|
|
446
|
+
|
|
447
|
+
access_token = self._encryptor.decrypt(account.access_token_encrypted)
|
|
448
|
+
refresh_token = self._encryptor.decrypt(account.refresh_token_encrypted)
|
|
449
|
+
id_token = self._encryptor.decrypt(account.id_token_encrypted)
|
|
450
|
+
expires = token_expiry_epoch_ms(access_token) or 0
|
|
451
|
+
|
|
452
|
+
tokens = AccountAuthExportTokens(
|
|
453
|
+
id_token=id_token,
|
|
454
|
+
access_token=access_token,
|
|
455
|
+
refresh_token=refresh_token,
|
|
456
|
+
expires_at_ms=expires,
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
codex_auth_json = CodexAuthJson(
|
|
460
|
+
auth_mode="chatgpt",
|
|
461
|
+
openai_api_key=None,
|
|
462
|
+
tokens=CodexAuthTokens(
|
|
463
|
+
id_token=id_token,
|
|
464
|
+
access_token=access_token,
|
|
465
|
+
refresh_token=refresh_token,
|
|
466
|
+
account_id=account.chatgpt_account_id,
|
|
467
|
+
),
|
|
468
|
+
last_refresh=account.last_refresh.strftime("%Y-%m-%dT%H:%M:%S.%f") + "Z",
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
opencode_auth_json = OpenCodeAuthJson(
|
|
472
|
+
openai=OpenCodeOAuthAuth(
|
|
473
|
+
refresh=refresh_token,
|
|
474
|
+
access=access_token,
|
|
475
|
+
expires=expires,
|
|
476
|
+
account_id=account.chatgpt_account_id,
|
|
477
|
+
),
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
return AccountAuthExportResponse(
|
|
481
|
+
filename=_opencode_auth_export_filename(account),
|
|
482
|
+
account=AccountOpenCodeAuthExportAccount(
|
|
483
|
+
account_id=account.id,
|
|
484
|
+
chatgpt_account_id=account.chatgpt_account_id,
|
|
485
|
+
email=account.email,
|
|
486
|
+
),
|
|
487
|
+
tokens=tokens,
|
|
488
|
+
codex_auth_json=codex_auth_json,
|
|
489
|
+
opencode_auth_json=opencode_auth_json,
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
async def import_account(self, raw: bytes) -> AccountImportResponse:
|
|
493
|
+
try:
|
|
494
|
+
auth = parse_auth_json(raw)
|
|
495
|
+
except (json.JSONDecodeError, ValidationError, UnicodeDecodeError, TypeError) as exc:
|
|
496
|
+
raise InvalidAuthJsonError("Invalid auth.json payload") from exc
|
|
497
|
+
claims = claims_from_auth(auth)
|
|
498
|
+
|
|
499
|
+
email = claims.email or DEFAULT_EMAIL
|
|
500
|
+
raw_account_id = claims.account_id
|
|
501
|
+
account_id = generate_unique_account_id(raw_account_id, email, claims.workspace_id, claims.workspace_label)
|
|
502
|
+
plan_type = coerce_account_plan_type(claims.plan_type, DEFAULT_PLAN)
|
|
503
|
+
last_refresh = to_utc_naive(auth.last_refresh_at) if auth.last_refresh_at else utcnow()
|
|
504
|
+
|
|
505
|
+
account = Account(
|
|
506
|
+
id=account_id,
|
|
507
|
+
chatgpt_account_id=raw_account_id,
|
|
508
|
+
email=email,
|
|
509
|
+
workspace_id=claims.workspace_id,
|
|
510
|
+
workspace_label=claims.workspace_label,
|
|
511
|
+
seat_type=claims.seat_type,
|
|
512
|
+
plan_type=plan_type,
|
|
513
|
+
access_token_encrypted=self._encryptor.encrypt(auth.tokens.access_token),
|
|
514
|
+
refresh_token_encrypted=self._encryptor.encrypt(auth.tokens.refresh_token),
|
|
515
|
+
id_token_encrypted=self._encryptor.encrypt(auth.tokens.id_token),
|
|
516
|
+
last_refresh=last_refresh,
|
|
517
|
+
status=AccountStatus.ACTIVE,
|
|
518
|
+
deactivation_reason=None,
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
saved = await self._repo.upsert_account_slot(account)
|
|
522
|
+
import_usage_refresh_allowed = await self._import_usage_refresh_allowed(saved)
|
|
523
|
+
if not import_usage_refresh_allowed:
|
|
524
|
+
await self._repo.update_status(
|
|
525
|
+
saved.id,
|
|
526
|
+
AccountStatus.PAUSED,
|
|
527
|
+
IMPORT_PROXY_REQUIRED_PAUSE_REASON,
|
|
528
|
+
None,
|
|
529
|
+
blocked_at=None,
|
|
530
|
+
)
|
|
531
|
+
mark_account_routing_unavailable(saved.id)
|
|
532
|
+
saved = await self._repo.get_by_id(saved.id) or saved
|
|
533
|
+
if import_usage_refresh_allowed and self._usage_repo and self._usage_updater:
|
|
534
|
+
latest_usage = await self._usage_repo.latest_by_account(window="primary")
|
|
535
|
+
await self._usage_updater.refresh_accounts([saved], latest_usage)
|
|
536
|
+
if saved.status == AccountStatus.ACTIVE:
|
|
537
|
+
clear_account_routing_unavailable(saved.id)
|
|
538
|
+
get_account_selection_cache().invalidate()
|
|
539
|
+
return AccountImportResponse(
|
|
540
|
+
account_id=saved.id,
|
|
541
|
+
email=saved.email,
|
|
542
|
+
workspace_id=saved.workspace_id,
|
|
543
|
+
workspace_label=saved.workspace_label,
|
|
544
|
+
seat_type=saved.seat_type,
|
|
545
|
+
plan_type=saved.plan_type,
|
|
546
|
+
status=saved.status,
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
async def _import_usage_refresh_allowed(self, account: Account) -> bool:
|
|
550
|
+
try:
|
|
551
|
+
route = await resolve_upstream_route(
|
|
552
|
+
self._repo.session,
|
|
553
|
+
account_id=account.id,
|
|
554
|
+
operation="usage_refresh",
|
|
555
|
+
scope="account",
|
|
556
|
+
encryptor=self._encryptor,
|
|
557
|
+
)
|
|
558
|
+
except UpstreamProxyRouteError as exc:
|
|
559
|
+
logger.info(
|
|
560
|
+
"Pausing imported account until upstream proxy binding is available account_id=%s reason=%s",
|
|
561
|
+
account.id,
|
|
562
|
+
exc.reason,
|
|
563
|
+
)
|
|
564
|
+
return False
|
|
565
|
+
if route is not None:
|
|
566
|
+
return True
|
|
567
|
+
|
|
568
|
+
try:
|
|
569
|
+
settings = await self._repo.session.get(DashboardSettings, 1)
|
|
570
|
+
except OperationalError as exc:
|
|
571
|
+
if not _is_missing_upstream_proxy_schema(exc):
|
|
572
|
+
raise
|
|
573
|
+
return True
|
|
574
|
+
if settings is not None and settings.upstream_proxy_routing_enabled:
|
|
575
|
+
logger.info(
|
|
576
|
+
"Pausing imported account until upstream proxy default pool is configured account_id=%s",
|
|
577
|
+
account.id,
|
|
578
|
+
)
|
|
579
|
+
return False
|
|
580
|
+
return True
|
|
581
|
+
|
|
582
|
+
async def reactivate_account(self, account_id: str) -> bool:
|
|
583
|
+
account = await self._repo.get_by_id(account_id)
|
|
584
|
+
if account is None:
|
|
585
|
+
return False
|
|
586
|
+
if account.delete_requested_at is not None:
|
|
587
|
+
# Marked for background deletion: already invisible in listings
|
|
588
|
+
# and about to be removed — report it as gone rather than racing
|
|
589
|
+
# the deletion worker back to ACTIVE.
|
|
590
|
+
return False
|
|
591
|
+
if account.status == AccountStatus.REAUTH_REQUIRED:
|
|
592
|
+
raise AccountStateTransitionError("Account requires re-authentication and cannot be reactivated directly")
|
|
593
|
+
result = await self._repo.update_status_if_current(
|
|
594
|
+
account_id,
|
|
595
|
+
AccountStatus.ACTIVE,
|
|
596
|
+
None,
|
|
597
|
+
None,
|
|
598
|
+
blocked_at=None,
|
|
599
|
+
expected_status=account.status,
|
|
600
|
+
expected_deactivation_reason=account.deactivation_reason,
|
|
601
|
+
expected_reset_at=account.reset_at,
|
|
602
|
+
expected_blocked_at=account.blocked_at,
|
|
603
|
+
)
|
|
604
|
+
if not result:
|
|
605
|
+
raise AccountStateTransitionError("Account state changed; retry the operation")
|
|
606
|
+
if result:
|
|
607
|
+
clear_account_routing_unavailable(account_id)
|
|
608
|
+
get_account_selection_cache().invalidate()
|
|
609
|
+
await propagate_account_routing_change()
|
|
610
|
+
return result
|
|
611
|
+
|
|
612
|
+
async def pause_account(self, account_id: str) -> bool:
|
|
613
|
+
account = await self._get_visible_account(account_id)
|
|
614
|
+
if account is None:
|
|
615
|
+
return False
|
|
616
|
+
if account.status in (AccountStatus.REAUTH_REQUIRED, AccountStatus.DEACTIVATED):
|
|
617
|
+
raise AccountStateTransitionError(f"Account is {account.status.value} and cannot be paused")
|
|
618
|
+
result = await self._repo.update_status_if_current(
|
|
619
|
+
account_id,
|
|
620
|
+
AccountStatus.PAUSED,
|
|
621
|
+
None,
|
|
622
|
+
None,
|
|
623
|
+
blocked_at=None,
|
|
624
|
+
expected_status=account.status,
|
|
625
|
+
expected_deactivation_reason=account.deactivation_reason,
|
|
626
|
+
expected_reset_at=account.reset_at,
|
|
627
|
+
expected_blocked_at=account.blocked_at,
|
|
628
|
+
)
|
|
629
|
+
if not result:
|
|
630
|
+
raise AccountStateTransitionError("Account state changed; retry the operation")
|
|
631
|
+
if result:
|
|
632
|
+
mark_account_routing_unavailable(account_id)
|
|
633
|
+
get_account_selection_cache().invalidate()
|
|
634
|
+
await propagate_account_routing_change()
|
|
635
|
+
return result
|
|
636
|
+
|
|
637
|
+
async def update_account(self, account_id: str, *, security_work_authorized: bool | None = None) -> bool:
|
|
638
|
+
result = False
|
|
639
|
+
if security_work_authorized is not None:
|
|
640
|
+
result = await self._repo.update_security_work_authorized(account_id, security_work_authorized)
|
|
641
|
+
if result:
|
|
642
|
+
get_account_selection_cache().invalidate()
|
|
643
|
+
return result
|
|
644
|
+
|
|
645
|
+
async def set_limit_warmup_enabled(self, account_id: str, enabled: bool) -> bool:
|
|
646
|
+
result = await self._repo.update_limit_warmup_enabled(account_id, enabled)
|
|
647
|
+
if result:
|
|
648
|
+
get_account_selection_cache().invalidate()
|
|
649
|
+
return result
|
|
650
|
+
|
|
651
|
+
async def set_routing_policy(self, account_id: str, routing_policy: str) -> bool:
|
|
652
|
+
result = await self._repo.update_routing_policy(account_id, routing_policy)
|
|
653
|
+
if result:
|
|
654
|
+
get_account_selection_cache().invalidate()
|
|
655
|
+
return result
|
|
656
|
+
|
|
657
|
+
async def delete_account(self, account_id: str, *, delete_history: bool = False) -> bool:
|
|
658
|
+
# Fast path: stamp the pending-deletion marker (terminal status, hidden
|
|
659
|
+
# from listings, sticky/bridge cleanup) and return in milliseconds; the
|
|
660
|
+
# background deletion worker drains the bulk rows and removes the
|
|
661
|
+
# account row afterwards (see app.modules.accounts.deletion).
|
|
662
|
+
result = await self._repo.begin_delete(account_id, delete_history=delete_history)
|
|
663
|
+
if result:
|
|
664
|
+
mark_account_routing_unavailable(account_id)
|
|
665
|
+
get_account_selection_cache().invalidate()
|
|
666
|
+
get_api_key_cache().clear()
|
|
667
|
+
# Finalization cascades the account_proxy_bindings row away, and
|
|
668
|
+
# account ids are deterministic (delete-then-re-import regenerates
|
|
669
|
+
# the same id), so the cached route outcome must not survive the
|
|
670
|
+
# delete request; the worker invalidates again after finalizing.
|
|
671
|
+
await get_upstream_route_cache().invalidate()
|
|
672
|
+
await propagate_account_routing_change()
|
|
673
|
+
poller = get_cache_invalidation_poller()
|
|
674
|
+
if poller is not None:
|
|
675
|
+
await poller.bump(NAMESPACE_API_KEY)
|
|
676
|
+
request_account_deletion_run()
|
|
677
|
+
return result
|
|
678
|
+
|
|
679
|
+
async def set_account_alias(self, account_id: str, alias: str | None) -> bool:
|
|
680
|
+
normalized = alias.strip() if isinstance(alias, str) else None
|
|
681
|
+
if normalized == "":
|
|
682
|
+
normalized = None
|
|
683
|
+
return await self._repo.update_alias(account_id, normalized)
|
|
684
|
+
|
|
685
|
+
async def probe_account(
|
|
686
|
+
self,
|
|
687
|
+
account_id: str,
|
|
688
|
+
model: str | None = None,
|
|
689
|
+
) -> AccountProbeResponse | None:
|
|
690
|
+
"""Send a minimal upstream ``responses.create`` pinned to one account.
|
|
691
|
+
|
|
692
|
+
Bypasses load-balancer scoring so an operator can wake the upstream
|
|
693
|
+
rate-limiter for a stuck account (see upstream issues #676 / #677).
|
|
694
|
+
Triggers an immediate usage refresh after the probe and returns the
|
|
695
|
+
before/after snapshot so the operator can see whether the upstream
|
|
696
|
+
state changed.
|
|
697
|
+
"""
|
|
698
|
+
account = await self._get_visible_account(account_id)
|
|
699
|
+
if account is None:
|
|
700
|
+
return None
|
|
701
|
+
if account.status in (AccountStatus.PAUSED, AccountStatus.DEACTIVATED):
|
|
702
|
+
raise AccountNotProbableError(f"Account is {account.status.value} and cannot be probed")
|
|
703
|
+
|
|
704
|
+
primary_before, secondary_before = await self._latest_usage_percents(account_id)
|
|
705
|
+
status_before = account.status.value
|
|
706
|
+
|
|
707
|
+
probe_account = account
|
|
708
|
+
if self._auth_manager is not None:
|
|
709
|
+
probe_account = await self._auth_manager.ensure_fresh(account, force=False)
|
|
710
|
+
|
|
711
|
+
access_token = self._encryptor.decrypt(probe_account.access_token_encrypted)
|
|
712
|
+
probe_model = model or resolve_default_host_model()
|
|
713
|
+
probe_status = await self._send_probe_request(
|
|
714
|
+
access_token=access_token,
|
|
715
|
+
chatgpt_account_id=probe_account.chatgpt_account_id,
|
|
716
|
+
model=probe_model,
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
usage_refresh_fetch_succeeded: bool | None = None
|
|
720
|
+
if self._usage_repo and self._usage_updater:
|
|
721
|
+
usage_refresh_result = await self._usage_updater.force_refresh_result(probe_account)
|
|
722
|
+
usage_refresh_fetch_succeeded = usage_refresh_result.fetch_succeeded
|
|
723
|
+
# Forced refresh can still persist fresh OAuth credentials before a
|
|
724
|
+
# later upstream usage fetch fails. Selection-cache rows carry
|
|
725
|
+
# cloned encrypted tokens, so every forced attempt must invalidate
|
|
726
|
+
# cached accounts, not only attempts that wrote usage rows.
|
|
727
|
+
get_account_selection_cache().invalidate()
|
|
728
|
+
|
|
729
|
+
refreshed = await self._repo.get_by_id(account_id) or account
|
|
730
|
+
primary_after, secondary_after = await self._latest_usage_percents(account_id)
|
|
731
|
+
|
|
732
|
+
response = AccountProbeResponse(
|
|
733
|
+
status="probed",
|
|
734
|
+
account_id=account_id,
|
|
735
|
+
probe_status_code=probe_status,
|
|
736
|
+
primary_used_percent_before=primary_before,
|
|
737
|
+
primary_used_percent_after=primary_after,
|
|
738
|
+
secondary_used_percent_before=secondary_before,
|
|
739
|
+
secondary_used_percent_after=secondary_after,
|
|
740
|
+
account_status_before=status_before,
|
|
741
|
+
account_status_after=refreshed.status.value,
|
|
742
|
+
)
|
|
743
|
+
response._usage_refresh_fetch_succeeded = usage_refresh_fetch_succeeded
|
|
744
|
+
return response
|
|
745
|
+
|
|
746
|
+
async def _latest_usage_percents(self, account_id: str) -> tuple[float | None, float | None]:
|
|
747
|
+
if self._usage_repo is None:
|
|
748
|
+
return None, None
|
|
749
|
+
primary_entry = await self._usage_repo.latest_entry_for_account(account_id, window="primary")
|
|
750
|
+
secondary_entry = await self._usage_repo.latest_entry_for_account(account_id, window="secondary")
|
|
751
|
+
return (
|
|
752
|
+
primary_entry.used_percent if primary_entry is not None else None,
|
|
753
|
+
secondary_entry.used_percent if secondary_entry is not None else None,
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
async def _send_probe_request(
|
|
757
|
+
self,
|
|
758
|
+
*,
|
|
759
|
+
access_token: str,
|
|
760
|
+
chatgpt_account_id: str | None,
|
|
761
|
+
model: str,
|
|
762
|
+
) -> int:
|
|
763
|
+
settings = get_settings()
|
|
764
|
+
base = settings.upstream_base_url.rstrip("/")
|
|
765
|
+
if "/backend-api" not in base:
|
|
766
|
+
base = f"{base}/backend-api"
|
|
767
|
+
url = f"{base}/codex/responses"
|
|
768
|
+
headers = {
|
|
769
|
+
"Authorization": f"Bearer {access_token}",
|
|
770
|
+
"Accept": "text/event-stream",
|
|
771
|
+
"Content-Type": "application/json",
|
|
772
|
+
}
|
|
773
|
+
if chatgpt_account_id and not chatgpt_account_id.startswith(("email_", "local_")):
|
|
774
|
+
headers["chatgpt-account-id"] = chatgpt_account_id
|
|
775
|
+
body = {
|
|
776
|
+
"model": model,
|
|
777
|
+
"instructions": "Respond with a single dot.",
|
|
778
|
+
"input": [
|
|
779
|
+
{
|
|
780
|
+
"role": "user",
|
|
781
|
+
"content": [{"type": "input_text", "text": "."}],
|
|
782
|
+
}
|
|
783
|
+
],
|
|
784
|
+
"stream": True,
|
|
785
|
+
"store": False,
|
|
786
|
+
}
|
|
787
|
+
timeout = aiohttp.ClientTimeout(
|
|
788
|
+
total=PROBE_REQUEST_TIMEOUT_SECONDS,
|
|
789
|
+
sock_connect=PROBE_CONNECT_TIMEOUT_SECONDS,
|
|
790
|
+
)
|
|
791
|
+
try:
|
|
792
|
+
async with lease_http_session() as session:
|
|
793
|
+
async with session.post(url, headers=headers, json=body, timeout=timeout) as resp:
|
|
794
|
+
# Initiating the request is enough to wake the upstream
|
|
795
|
+
# rate-limiter; we do not consume the SSE body.
|
|
796
|
+
return resp.status
|
|
797
|
+
except (aiohttp.ClientError, asyncio.TimeoutError) as exc:
|
|
798
|
+
logger.warning(
|
|
799
|
+
"Probe upstream request failed account=%s error=%s",
|
|
800
|
+
chatgpt_account_id,
|
|
801
|
+
exc,
|
|
802
|
+
)
|
|
803
|
+
return PROBE_NETWORK_FAILURE_STATUS
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
def _opencode_auth_export_filename(account: Account) -> str:
|
|
807
|
+
source = account.email or account.id
|
|
808
|
+
safe = "".join(char if char.isalnum() or char in "._-" else "-" for char in source).strip("-._")
|
|
809
|
+
return f"opencode-auth-{safe or account.id}.json"
|