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,488 @@
|
|
|
1
|
+
"""Cross-replica evidence for workspace-less paid -> free plan downgrades.
|
|
2
|
+
|
|
3
|
+
A workspace-less usage payload that reports ``free`` for a paid account is not
|
|
4
|
+
trusted on a single sample: it is also the signature of a degraded or
|
|
5
|
+
wrong-identity usage response. The downgrade is applied only once two
|
|
6
|
+
consecutive refreshes agree (issue #1456).
|
|
7
|
+
|
|
8
|
+
Holding that evidence in process memory makes the sequence diverge whenever more
|
|
9
|
+
than one replica shares a database:
|
|
10
|
+
|
|
11
|
+
- replica A records ``free``, replica B observes a paid payload that should clear
|
|
12
|
+
the evidence, then A observes ``free`` again and confirms a downgrade the
|
|
13
|
+
cluster has already contradicted;
|
|
14
|
+
- conversely, two genuine ``free`` samples split across replicas each stall at
|
|
15
|
+
one observation, so a real expiry never converges.
|
|
16
|
+
|
|
17
|
+
:class:`PlanDowngradeObservationStore` therefore keeps one row per account in
|
|
18
|
+
``account_plan_downgrade_observations``, so every replica reads and advances the
|
|
19
|
+
same count.
|
|
20
|
+
|
|
21
|
+
The stored ``credential_fingerprint`` pins evidence to the credential *lineage*
|
|
22
|
+
that produced it: a salted digest over the account's stable seat identity, never
|
|
23
|
+
over token material. Refresh tokens rotate on every successful token refresh
|
|
24
|
+
(``rotate_tokens`` in the accounts repository), and rotation extends the same
|
|
25
|
+
lineage rather than replacing it, so the digest is unmoved by rotation by
|
|
26
|
+
construction -- token bytes are not an input. Because nothing here decrypts, no
|
|
27
|
+
key rotation, re-encryption, or undecryptable row can perturb the digest either.
|
|
28
|
+
|
|
29
|
+
Replacing the credential is a different event from rotating it. Account ids are
|
|
30
|
+
deterministic (``generate_unique_account_id``) and ``upsert_account_slot``
|
|
31
|
+
updates the existing row in place, so a delete-and-re-import or an in-place
|
|
32
|
+
reauthentication reuses the same account id with *new* credentials -- and
|
|
33
|
+
evidence gathered under the previous credential must not count toward a
|
|
34
|
+
downgrade for the new one. Every such replacement flows through
|
|
35
|
+
``_apply_account_updates`` in the accounts repository, which discards this
|
|
36
|
+
account's pending evidence in the same transaction via
|
|
37
|
+
:func:`discard_plan_downgrade_observations`; deleting the account drops its row
|
|
38
|
+
through the schema's ``ondelete="CASCADE"``. The fingerprint's restart-at-one
|
|
39
|
+
comparison remains as defense in depth for any path that rebinds a row's seat
|
|
40
|
+
identity without passing through those seams.
|
|
41
|
+
|
|
42
|
+
Rows are deleted as soon as the downgrade is applied or the evidence is
|
|
43
|
+
invalidated.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from __future__ import annotations
|
|
47
|
+
|
|
48
|
+
import hashlib
|
|
49
|
+
import hmac
|
|
50
|
+
import json
|
|
51
|
+
import logging
|
|
52
|
+
from dataclasses import dataclass
|
|
53
|
+
from typing import Protocol
|
|
54
|
+
|
|
55
|
+
from sqlalchemy import DateTime, String, bindparam, delete, select, text
|
|
56
|
+
from sqlalchemy.exc import OperationalError, ProgrammingError
|
|
57
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
58
|
+
|
|
59
|
+
from app.core.utils.time import utcnow
|
|
60
|
+
from app.db.models import Account, AccountPlanDowngradeObservation
|
|
61
|
+
from app.db.session import get_background_session, sqlite_writer_section
|
|
62
|
+
|
|
63
|
+
logger = logging.getLogger(__name__)
|
|
64
|
+
|
|
65
|
+
_TABLE_NAME = "account_plan_downgrade_observations"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _is_missing_observations_schema(exc: Exception) -> bool:
|
|
69
|
+
"""True when the failure is only "this table has not been created yet".
|
|
70
|
+
|
|
71
|
+
The observations table arrives with this change, so a replica running the new
|
|
72
|
+
code against a not-yet-migrated database must degrade to process-local
|
|
73
|
+
confirmation rather than fail every usage refresh: a stale plan label is a far
|
|
74
|
+
smaller problem than a broken refresh loop. PostgreSQL and SQLite word the
|
|
75
|
+
error differently, so both families are matched.
|
|
76
|
+
"""
|
|
77
|
+
origin = getattr(exc, "orig", None)
|
|
78
|
+
message = str(origin).lower() if origin is not None else str(exc).lower()
|
|
79
|
+
return f"no such table: {_TABLE_NAME}" in message or f'relation "{_TABLE_NAME}" does not exist' in message
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# Domain separation for the lineage digest. The fingerprint only ever needs to
|
|
83
|
+
# answer "is this the same credential lineage as last time?", so a fixed-salt
|
|
84
|
+
# digest is sufficient and keeps the value stable across replicas and restarts
|
|
85
|
+
# (a random per-process salt would make every replica disagree, reintroducing
|
|
86
|
+
# the very divergence this module removes). The salt is public by design: the
|
|
87
|
+
# digested fields are non-secret identity metadata, and the salt exists for
|
|
88
|
+
# domain separation, not secrecy. Bumped to v2 when the digest input changed
|
|
89
|
+
# from token material to seat identity, so values from the two schemes can
|
|
90
|
+
# never read as an agreeing lineage.
|
|
91
|
+
_FINGERPRINT_SALT = b"codex-lb/plan-downgrade-observation/v2"
|
|
92
|
+
_FINGERPRINT_LEN = 64
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def credential_fingerprint(account: Account) -> str:
|
|
96
|
+
"""Return a stable, non-reversible fingerprint of an account's credential lineage.
|
|
97
|
+
|
|
98
|
+
The digest is taken over the account's stable seat identity -- the ChatGPT
|
|
99
|
+
workspace and principal identifiers, the email, and the sticky
|
|
100
|
+
``codex_installation_id`` -- and deliberately NOT over token material.
|
|
101
|
+
Refresh tokens rotate on every successful token refresh, so a token-derived
|
|
102
|
+
digest would read routine rotation as a credential replacement and restart
|
|
103
|
+
pending downgrade evidence; an account whose token-refresh cadence
|
|
104
|
+
interleaves with usage refresh would then never accumulate two agreeing
|
|
105
|
+
observations and a real expiry could be postponed indefinitely (issue
|
|
106
|
+
#1456). Seat identity survives rotation by construction, and because no
|
|
107
|
+
input is encrypted there is no decrypt step to fail and no fallback path.
|
|
108
|
+
|
|
109
|
+
Credential *replacement* (re-import or in-place reauthentication) does not
|
|
110
|
+
move this digest either -- the same seat gets new tokens. That event resets
|
|
111
|
+
pending evidence explicitly instead: every replacement flows through
|
|
112
|
+
``_apply_account_updates`` in the accounts repository, which calls
|
|
113
|
+
:func:`discard_plan_downgrade_observations` in the same transaction.
|
|
114
|
+
|
|
115
|
+
``None`` and empty identity fields are encoded distinctly (JSON), so partial
|
|
116
|
+
identities still compare equal only to themselves. Only equality ever
|
|
117
|
+
matters here, never the preimage.
|
|
118
|
+
"""
|
|
119
|
+
material = json.dumps(
|
|
120
|
+
[
|
|
121
|
+
account.chatgpt_account_id,
|
|
122
|
+
account.chatgpt_user_id,
|
|
123
|
+
account.email,
|
|
124
|
+
account.codex_installation_id,
|
|
125
|
+
],
|
|
126
|
+
separators=(",", ":"),
|
|
127
|
+
).encode("utf-8")
|
|
128
|
+
digest = hmac.new(_FINGERPRINT_SALT, material, hashlib.sha256).hexdigest()
|
|
129
|
+
return digest[:_FINGERPRINT_LEN]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# Logged at most once per process: an unmigrated database hits this on every
|
|
133
|
+
# credential replacement until the migration applies, and the first warning
|
|
134
|
+
# carries all the signal.
|
|
135
|
+
_discard_schema_missing_logged = False
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
async def discard_plan_downgrade_observations(session: AsyncSession, account_id: str) -> None:
|
|
139
|
+
"""Discard pending downgrade evidence inside the caller's transaction.
|
|
140
|
+
|
|
141
|
+
Called by the accounts repository wherever fresh credential material is
|
|
142
|
+
applied to an existing account row (re-import or in-place reauthentication):
|
|
143
|
+
evidence gathered under the previous credential must not count toward a
|
|
144
|
+
downgrade for the new one, so the new credential's first ``free`` payload
|
|
145
|
+
can never land a downgrade on a single sample. Running inside the caller's
|
|
146
|
+
transaction makes the discard atomic with the credential replacement.
|
|
147
|
+
|
|
148
|
+
The ``DELETE`` runs in a SAVEPOINT so a database whose migration has not
|
|
149
|
+
applied yet degrades to a warning instead of poisoning the caller's
|
|
150
|
+
transaction (PostgreSQL aborts the whole transaction on a failed statement)
|
|
151
|
+
and failing the import or reauthentication itself.
|
|
152
|
+
"""
|
|
153
|
+
global _discard_schema_missing_logged
|
|
154
|
+
try:
|
|
155
|
+
async with session.begin_nested():
|
|
156
|
+
await session.execute(
|
|
157
|
+
delete(AccountPlanDowngradeObservation).where(AccountPlanDowngradeObservation.account_id == account_id)
|
|
158
|
+
)
|
|
159
|
+
except (OperationalError, ProgrammingError) as exc:
|
|
160
|
+
if not _is_missing_observations_schema(exc):
|
|
161
|
+
raise
|
|
162
|
+
if not _discard_schema_missing_logged:
|
|
163
|
+
_discard_schema_missing_logged = True
|
|
164
|
+
logger.warning(
|
|
165
|
+
"Plan-downgrade observation table is unavailable; credential replacement proceeds "
|
|
166
|
+
"without discarding pending evidence until the database is migrated table=%s",
|
|
167
|
+
_TABLE_NAME,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@dataclass(frozen=True, slots=True)
|
|
172
|
+
class PlanDowngradeObservation:
|
|
173
|
+
observations: int
|
|
174
|
+
credential_fingerprint: str
|
|
175
|
+
observed_plan_type: str
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class PlanDowngradeObservationStorePort(Protocol):
|
|
179
|
+
async def get(self, account_id: str) -> PlanDowngradeObservation | None: ...
|
|
180
|
+
|
|
181
|
+
async def observe(
|
|
182
|
+
self,
|
|
183
|
+
account_id: str,
|
|
184
|
+
*,
|
|
185
|
+
credential_fingerprint: str,
|
|
186
|
+
observed_plan_type: str,
|
|
187
|
+
) -> int: ...
|
|
188
|
+
|
|
189
|
+
async def record(
|
|
190
|
+
self,
|
|
191
|
+
account_id: str,
|
|
192
|
+
*,
|
|
193
|
+
observations: int,
|
|
194
|
+
credential_fingerprint: str,
|
|
195
|
+
observed_plan_type: str,
|
|
196
|
+
) -> None: ...
|
|
197
|
+
|
|
198
|
+
async def clear(self, account_id: str) -> None: ...
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# Atomic observe: insert the first observation or increment an existing one in a
|
|
202
|
+
# single statement, so two concurrent refreshes for one account cannot both read
|
|
203
|
+
# the same prior count and lose an increment (a read-then-write pair has an await
|
|
204
|
+
# between the two halves and is not safe across replicas either way). The
|
|
205
|
+
# fingerprint check lives inside the same statement: a matching lineage
|
|
206
|
+
# increments, a changed lineage restarts at one. Mirrors the conditional-upsert
|
|
207
|
+
# approach in ``app/modules/accounts/refresh_claims.py``.
|
|
208
|
+
_OBSERVE_SQL_TEMPLATE = """
|
|
209
|
+
INSERT INTO account_plan_downgrade_observations (
|
|
210
|
+
account_id, observations, credential_fingerprint, observed_plan_type,
|
|
211
|
+
first_observed_at, last_observed_at
|
|
212
|
+
)
|
|
213
|
+
VALUES (:account_id, 1, :fingerprint, :plan_type, :now, :now)
|
|
214
|
+
ON CONFLICT (account_id) DO UPDATE SET
|
|
215
|
+
observations = CASE
|
|
216
|
+
WHEN {table}.credential_fingerprint = :fingerprint
|
|
217
|
+
AND {table}.observed_plan_type = :plan_type
|
|
218
|
+
THEN {table}.observations + 1
|
|
219
|
+
ELSE 1
|
|
220
|
+
END,
|
|
221
|
+
credential_fingerprint = :fingerprint,
|
|
222
|
+
observed_plan_type = :plan_type,
|
|
223
|
+
first_observed_at = CASE
|
|
224
|
+
WHEN {table}.credential_fingerprint = :fingerprint
|
|
225
|
+
AND {table}.observed_plan_type = :plan_type
|
|
226
|
+
THEN {table}.first_observed_at
|
|
227
|
+
ELSE :now
|
|
228
|
+
END,
|
|
229
|
+
last_observed_at = :now
|
|
230
|
+
RETURNING observations
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class PlanDowngradeObservationStore:
|
|
235
|
+
"""Database-backed store shared by every replica.
|
|
236
|
+
|
|
237
|
+
When the table has not been migrated yet, every operation degrades to a
|
|
238
|
+
process-local fallback so usage refresh keeps working; confirmation then loses
|
|
239
|
+
only its cross-replica coherence, exactly the pre-change behavior.
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
def __init__(self, *, fallback: PlanDowngradeObservationStorePort | None = None) -> None:
|
|
243
|
+
self._fallback = fallback if fallback is not None else InMemoryPlanDowngradeObservationStore()
|
|
244
|
+
self._schema_missing = False
|
|
245
|
+
|
|
246
|
+
def _degrade(self, exc: Exception) -> bool:
|
|
247
|
+
if not _is_missing_observations_schema(exc):
|
|
248
|
+
return False
|
|
249
|
+
if not self._schema_missing:
|
|
250
|
+
self._schema_missing = True
|
|
251
|
+
logger.warning(
|
|
252
|
+
"Plan-downgrade observation table is unavailable; falling back to process-local "
|
|
253
|
+
"confirmation state until the database is migrated table=%s",
|
|
254
|
+
_TABLE_NAME,
|
|
255
|
+
)
|
|
256
|
+
return True
|
|
257
|
+
|
|
258
|
+
async def get(self, account_id: str) -> PlanDowngradeObservation | None:
|
|
259
|
+
try:
|
|
260
|
+
async with get_background_session() as session:
|
|
261
|
+
row = await session.get(AccountPlanDowngradeObservation, account_id)
|
|
262
|
+
if row is None:
|
|
263
|
+
return None
|
|
264
|
+
return PlanDowngradeObservation(
|
|
265
|
+
observations=row.observations,
|
|
266
|
+
credential_fingerprint=row.credential_fingerprint,
|
|
267
|
+
observed_plan_type=row.observed_plan_type,
|
|
268
|
+
)
|
|
269
|
+
except (OperationalError, ProgrammingError) as exc:
|
|
270
|
+
if not self._degrade(exc):
|
|
271
|
+
raise
|
|
272
|
+
return await self._fallback.get(account_id)
|
|
273
|
+
|
|
274
|
+
async def observe(
|
|
275
|
+
self,
|
|
276
|
+
account_id: str,
|
|
277
|
+
*,
|
|
278
|
+
credential_fingerprint: str,
|
|
279
|
+
observed_plan_type: str,
|
|
280
|
+
) -> int:
|
|
281
|
+
"""Atomically record an observation and return the resulting count.
|
|
282
|
+
|
|
283
|
+
One statement decides between "increment" and "restart at one", so
|
|
284
|
+
concurrent refreshes for the same account cannot lose an increment and a
|
|
285
|
+
changed credential lineage still resets the sequence.
|
|
286
|
+
"""
|
|
287
|
+
try:
|
|
288
|
+
async with sqlite_writer_section():
|
|
289
|
+
async with get_background_session() as session:
|
|
290
|
+
# Bind types are explicit so both drivers receive the same
|
|
291
|
+
# shapes: asyncpg in particular must see the naive UTC
|
|
292
|
+
# ``utcnow()`` values as a plain (timezone-less) TIMESTAMP
|
|
293
|
+
# rather than inferring a type for a textual parameter.
|
|
294
|
+
statement = text(_OBSERVE_SQL_TEMPLATE.format(table=_TABLE_NAME)).bindparams(
|
|
295
|
+
bindparam("account_id", type_=String()),
|
|
296
|
+
bindparam("fingerprint", type_=String()),
|
|
297
|
+
bindparam("plan_type", type_=String()),
|
|
298
|
+
bindparam("now", type_=DateTime()),
|
|
299
|
+
)
|
|
300
|
+
result = await session.execute(
|
|
301
|
+
statement,
|
|
302
|
+
{
|
|
303
|
+
"account_id": account_id,
|
|
304
|
+
"fingerprint": credential_fingerprint,
|
|
305
|
+
"plan_type": observed_plan_type,
|
|
306
|
+
"now": utcnow(),
|
|
307
|
+
},
|
|
308
|
+
)
|
|
309
|
+
observations = result.scalar_one()
|
|
310
|
+
await session.commit()
|
|
311
|
+
return int(observations)
|
|
312
|
+
except (OperationalError, ProgrammingError) as exc:
|
|
313
|
+
if not self._degrade(exc):
|
|
314
|
+
raise
|
|
315
|
+
return await self._fallback.observe(
|
|
316
|
+
account_id,
|
|
317
|
+
credential_fingerprint=credential_fingerprint,
|
|
318
|
+
observed_plan_type=observed_plan_type,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
async def record(
|
|
322
|
+
self,
|
|
323
|
+
account_id: str,
|
|
324
|
+
*,
|
|
325
|
+
observations: int,
|
|
326
|
+
credential_fingerprint: str,
|
|
327
|
+
observed_plan_type: str,
|
|
328
|
+
) -> None:
|
|
329
|
+
now = utcnow()
|
|
330
|
+
try:
|
|
331
|
+
async with sqlite_writer_section():
|
|
332
|
+
async with get_background_session() as session:
|
|
333
|
+
existing = await session.get(AccountPlanDowngradeObservation, account_id)
|
|
334
|
+
if existing is None:
|
|
335
|
+
session.add(
|
|
336
|
+
AccountPlanDowngradeObservation(
|
|
337
|
+
account_id=account_id,
|
|
338
|
+
observations=observations,
|
|
339
|
+
credential_fingerprint=credential_fingerprint,
|
|
340
|
+
observed_plan_type=observed_plan_type,
|
|
341
|
+
first_observed_at=now,
|
|
342
|
+
last_observed_at=now,
|
|
343
|
+
)
|
|
344
|
+
)
|
|
345
|
+
else:
|
|
346
|
+
existing.observations = observations
|
|
347
|
+
existing.credential_fingerprint = credential_fingerprint
|
|
348
|
+
existing.observed_plan_type = observed_plan_type
|
|
349
|
+
existing.last_observed_at = now
|
|
350
|
+
await session.commit()
|
|
351
|
+
except (OperationalError, ProgrammingError) as exc:
|
|
352
|
+
if not self._degrade(exc):
|
|
353
|
+
raise
|
|
354
|
+
await self._fallback.record(
|
|
355
|
+
account_id,
|
|
356
|
+
observations=observations,
|
|
357
|
+
credential_fingerprint=credential_fingerprint,
|
|
358
|
+
observed_plan_type=observed_plan_type,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
async def clear(self, account_id: str) -> None:
|
|
362
|
+
"""Discard pending evidence, touching the writer path only when a row exists.
|
|
363
|
+
|
|
364
|
+
Every workspace-less refresh that reports a paid plan clears here, and on
|
|
365
|
+
a healthy account there is almost never a pending row -- so the common
|
|
366
|
+
case is a cheap primary-key read with no writer lock, no ``DELETE``, and
|
|
367
|
+
no ``COMMIT``. A row inserted concurrently after the read survives until
|
|
368
|
+
the next paid observation, exactly as it would have survived an
|
|
369
|
+
unconditional ``DELETE`` that committed before that insert: the gate
|
|
370
|
+
changes the cost of the no-op case, not the semantics.
|
|
371
|
+
"""
|
|
372
|
+
try:
|
|
373
|
+
async with get_background_session() as session:
|
|
374
|
+
existing = await session.get(AccountPlanDowngradeObservation, account_id)
|
|
375
|
+
if existing is None:
|
|
376
|
+
return
|
|
377
|
+
async with sqlite_writer_section():
|
|
378
|
+
async with get_background_session() as session:
|
|
379
|
+
await session.execute(
|
|
380
|
+
delete(AccountPlanDowngradeObservation).where(
|
|
381
|
+
AccountPlanDowngradeObservation.account_id == account_id
|
|
382
|
+
)
|
|
383
|
+
)
|
|
384
|
+
await session.commit()
|
|
385
|
+
except (OperationalError, ProgrammingError) as exc:
|
|
386
|
+
if not self._degrade(exc):
|
|
387
|
+
raise
|
|
388
|
+
await self._fallback.clear(account_id)
|
|
389
|
+
|
|
390
|
+
async def account_ids(self) -> list[str]:
|
|
391
|
+
"""Every account with pending evidence (diagnostics and tests)."""
|
|
392
|
+
async with get_background_session() as session:
|
|
393
|
+
result = await session.execute(select(AccountPlanDowngradeObservation.account_id))
|
|
394
|
+
return [row[0] for row in result.all()]
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
# Process-wide default store. ``_default_initialized`` distinguishes "not yet
|
|
398
|
+
# initialized" from an explicit override of ``None`` (persistence disabled --
|
|
399
|
+
# used by the test harness so DB-less unit tests keep exercising the guard
|
|
400
|
+
# against an in-memory store).
|
|
401
|
+
_default_store: PlanDowngradeObservationStorePort | None = None
|
|
402
|
+
_default_initialized: bool = False
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def get_plan_downgrade_observation_store() -> PlanDowngradeObservationStorePort | None:
|
|
406
|
+
global _default_store, _default_initialized
|
|
407
|
+
if not _default_initialized:
|
|
408
|
+
_default_store = PlanDowngradeObservationStore()
|
|
409
|
+
_default_initialized = True
|
|
410
|
+
return _default_store
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def set_plan_downgrade_observation_store(store: PlanDowngradeObservationStorePort | None) -> None:
|
|
414
|
+
"""Override the process default (``None`` disables persistence)."""
|
|
415
|
+
global _default_store, _default_initialized
|
|
416
|
+
_default_store = store
|
|
417
|
+
_default_initialized = True
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def reset_plan_downgrade_observation_store() -> None:
|
|
421
|
+
global _default_store, _default_initialized
|
|
422
|
+
_default_store = None
|
|
423
|
+
_default_initialized = False
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
class InMemoryPlanDowngradeObservationStore:
|
|
427
|
+
"""Process-local store used when no database is available.
|
|
428
|
+
|
|
429
|
+
Preserves single-replica behavior for DB-less unit tests and for any
|
|
430
|
+
deployment where the observations table has not been migrated yet; it cannot
|
|
431
|
+
provide cross-replica coherence, which is why the database-backed store is
|
|
432
|
+
the process default.
|
|
433
|
+
"""
|
|
434
|
+
|
|
435
|
+
def __init__(self) -> None:
|
|
436
|
+
self._rows: dict[str, PlanDowngradeObservation] = {}
|
|
437
|
+
|
|
438
|
+
async def get(self, account_id: str) -> PlanDowngradeObservation | None:
|
|
439
|
+
return self._rows.get(account_id)
|
|
440
|
+
|
|
441
|
+
async def observe(
|
|
442
|
+
self,
|
|
443
|
+
account_id: str,
|
|
444
|
+
*,
|
|
445
|
+
credential_fingerprint: str,
|
|
446
|
+
observed_plan_type: str,
|
|
447
|
+
) -> int:
|
|
448
|
+
"""Increment or restart the count without yielding control.
|
|
449
|
+
|
|
450
|
+
No ``await`` between the read and the write, so this is atomic with
|
|
451
|
+
respect to other tasks in this event loop — the in-process analogue of the
|
|
452
|
+
database store's single-statement upsert.
|
|
453
|
+
"""
|
|
454
|
+
existing = self._rows.get(account_id)
|
|
455
|
+
if (
|
|
456
|
+
existing is not None
|
|
457
|
+
and existing.credential_fingerprint == credential_fingerprint
|
|
458
|
+
and existing.observed_plan_type == observed_plan_type
|
|
459
|
+
):
|
|
460
|
+
observations = existing.observations + 1
|
|
461
|
+
else:
|
|
462
|
+
observations = 1
|
|
463
|
+
self._rows[account_id] = PlanDowngradeObservation(
|
|
464
|
+
observations=observations,
|
|
465
|
+
credential_fingerprint=credential_fingerprint,
|
|
466
|
+
observed_plan_type=observed_plan_type,
|
|
467
|
+
)
|
|
468
|
+
return observations
|
|
469
|
+
|
|
470
|
+
async def record(
|
|
471
|
+
self,
|
|
472
|
+
account_id: str,
|
|
473
|
+
*,
|
|
474
|
+
observations: int,
|
|
475
|
+
credential_fingerprint: str,
|
|
476
|
+
observed_plan_type: str,
|
|
477
|
+
) -> None:
|
|
478
|
+
self._rows[account_id] = PlanDowngradeObservation(
|
|
479
|
+
observations=observations,
|
|
480
|
+
credential_fingerprint=credential_fingerprint,
|
|
481
|
+
observed_plan_type=observed_plan_type,
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
async def clear(self, account_id: str) -> None:
|
|
485
|
+
self._rows.pop(account_id, None)
|
|
486
|
+
|
|
487
|
+
def clear_all(self) -> None:
|
|
488
|
+
self._rows.clear()
|