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,122 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hmac
|
|
4
|
+
import struct
|
|
5
|
+
import zlib
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from hashlib import sha256
|
|
9
|
+
|
|
10
|
+
DURABLE_BRIDGE_CHUNK_CODEC = "zlib-length-prefixed-utf8-v1"
|
|
11
|
+
DURABLE_BRIDGE_CHUNK_MAX_EVENTS = 256
|
|
12
|
+
DURABLE_BRIDGE_TRANSCRIPT_MAX_EVENTS = 65_536
|
|
13
|
+
_FRAME_LENGTH = struct.Struct(">I")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DurableBridgeTranscriptDecodeError(ValueError):
|
|
17
|
+
"""Raised when a durable chunk cannot be decoded without ambiguity."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class EncodedDurableBridgeTranscriptChunk:
|
|
22
|
+
codec: str
|
|
23
|
+
payload: bytes
|
|
24
|
+
event_count: int
|
|
25
|
+
uncompressed_bytes: int
|
|
26
|
+
payload_sha256: str
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def encode_durable_bridge_transcript_chunk(
|
|
30
|
+
events: Sequence[str],
|
|
31
|
+
) -> EncodedDurableBridgeTranscriptChunk:
|
|
32
|
+
if not events:
|
|
33
|
+
raise ValueError("durable transcript chunks require at least one event")
|
|
34
|
+
if len(events) > DURABLE_BRIDGE_CHUNK_MAX_EVENTS:
|
|
35
|
+
raise ValueError("durable transcript chunk exceeds event-count limit")
|
|
36
|
+
framed = bytearray()
|
|
37
|
+
for event in events:
|
|
38
|
+
encoded = event.encode("utf-8")
|
|
39
|
+
if len(encoded) > 0xFFFFFFFF:
|
|
40
|
+
raise ValueError("durable transcript event exceeds framing limit")
|
|
41
|
+
framed.extend(_FRAME_LENGTH.pack(len(encoded)))
|
|
42
|
+
framed.extend(encoded)
|
|
43
|
+
canonical = bytes(framed)
|
|
44
|
+
return EncodedDurableBridgeTranscriptChunk(
|
|
45
|
+
codec=DURABLE_BRIDGE_CHUNK_CODEC,
|
|
46
|
+
payload=zlib.compress(canonical),
|
|
47
|
+
event_count=len(events),
|
|
48
|
+
uncompressed_bytes=len(canonical),
|
|
49
|
+
payload_sha256=sha256(canonical).hexdigest(),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def decode_durable_bridge_transcript_chunk(
|
|
54
|
+
*,
|
|
55
|
+
codec: str,
|
|
56
|
+
payload: bytes,
|
|
57
|
+
event_count: int,
|
|
58
|
+
uncompressed_bytes: int,
|
|
59
|
+
payload_sha256: str,
|
|
60
|
+
max_uncompressed_bytes: int,
|
|
61
|
+
) -> tuple[str, ...]:
|
|
62
|
+
if codec != DURABLE_BRIDGE_CHUNK_CODEC:
|
|
63
|
+
raise DurableBridgeTranscriptDecodeError("unknown durable transcript chunk codec")
|
|
64
|
+
if type(event_count) is not int or type(uncompressed_bytes) is not int:
|
|
65
|
+
raise DurableBridgeTranscriptDecodeError("durable transcript chunk numeric metadata must be integral")
|
|
66
|
+
if event_count <= 0:
|
|
67
|
+
raise DurableBridgeTranscriptDecodeError("durable transcript chunk event count must be positive")
|
|
68
|
+
if event_count > DURABLE_BRIDGE_CHUNK_MAX_EVENTS:
|
|
69
|
+
raise DurableBridgeTranscriptDecodeError("durable transcript chunk exceeds event-count limit")
|
|
70
|
+
if uncompressed_bytes < 0 or uncompressed_bytes > max_uncompressed_bytes:
|
|
71
|
+
raise DurableBridgeTranscriptDecodeError("durable transcript chunk exceeds decode byte limit")
|
|
72
|
+
|
|
73
|
+
decompressor = zlib.decompressobj()
|
|
74
|
+
try:
|
|
75
|
+
canonical = decompressor.decompress(payload, uncompressed_bytes + 1)
|
|
76
|
+
except zlib.error as exc:
|
|
77
|
+
raise DurableBridgeTranscriptDecodeError("invalid durable transcript compressed payload") from exc
|
|
78
|
+
if (
|
|
79
|
+
len(canonical) != uncompressed_bytes
|
|
80
|
+
or decompressor.unconsumed_tail
|
|
81
|
+
or decompressor.unused_data
|
|
82
|
+
or not decompressor.eof
|
|
83
|
+
):
|
|
84
|
+
raise DurableBridgeTranscriptDecodeError("durable transcript chunk byte count mismatch")
|
|
85
|
+
try:
|
|
86
|
+
expected_digest = bytes.fromhex(payload_sha256)
|
|
87
|
+
except (TypeError, ValueError) as exc:
|
|
88
|
+
raise DurableBridgeTranscriptDecodeError("durable transcript chunk hash is not hexadecimal") from exc
|
|
89
|
+
if len(expected_digest) != sha256().digest_size:
|
|
90
|
+
raise DurableBridgeTranscriptDecodeError("durable transcript chunk hash has invalid length")
|
|
91
|
+
if not hmac.compare_digest(sha256(canonical).digest(), expected_digest):
|
|
92
|
+
raise DurableBridgeTranscriptDecodeError("durable transcript chunk hash mismatch")
|
|
93
|
+
|
|
94
|
+
events: list[str] = []
|
|
95
|
+
offset = 0
|
|
96
|
+
for _ in range(event_count):
|
|
97
|
+
header_end = offset + _FRAME_LENGTH.size
|
|
98
|
+
if header_end > len(canonical):
|
|
99
|
+
raise DurableBridgeTranscriptDecodeError("durable transcript chunk has truncated frame header")
|
|
100
|
+
(event_size,) = _FRAME_LENGTH.unpack(canonical[offset:header_end])
|
|
101
|
+
event_end = header_end + event_size
|
|
102
|
+
if event_end > len(canonical):
|
|
103
|
+
raise DurableBridgeTranscriptDecodeError("durable transcript chunk has truncated event")
|
|
104
|
+
try:
|
|
105
|
+
events.append(canonical[header_end:event_end].decode("utf-8"))
|
|
106
|
+
except UnicodeDecodeError as exc:
|
|
107
|
+
raise DurableBridgeTranscriptDecodeError("durable transcript chunk contains invalid UTF-8") from exc
|
|
108
|
+
offset = event_end
|
|
109
|
+
if offset != len(canonical):
|
|
110
|
+
raise DurableBridgeTranscriptDecodeError("durable transcript chunk contains trailing bytes")
|
|
111
|
+
return tuple(events)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
__all__ = [
|
|
115
|
+
"DURABLE_BRIDGE_CHUNK_CODEC",
|
|
116
|
+
"DURABLE_BRIDGE_CHUNK_MAX_EVENTS",
|
|
117
|
+
"DURABLE_BRIDGE_TRANSCRIPT_MAX_EVENTS",
|
|
118
|
+
"DurableBridgeTranscriptDecodeError",
|
|
119
|
+
"EncodedDurableBridgeTranscriptChunk",
|
|
120
|
+
"decode_durable_bridge_transcript_chunk",
|
|
121
|
+
"encode_durable_bridge_transcript_chunk",
|
|
122
|
+
]
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Congestion-aware per-API-key fair-share stream admission.
|
|
2
|
+
|
|
3
|
+
Stream slots are a pool-wide scarce resource: the per-account stream cap
|
|
4
|
+
protects individual accounts from upstream rate limits, but slot allocation
|
|
5
|
+
across API keys is otherwise first-come-first-served, so one bursty key can
|
|
6
|
+
hold every slot and starve every other key. This module provides the pure
|
|
7
|
+
max-min fair-share decision used by the load balancer's stream admission:
|
|
8
|
+
|
|
9
|
+
- Work-conserving: while pool utilization is below the configured congestion
|
|
10
|
+
threshold, every key admits unconditionally — idle capacity is never wasted
|
|
11
|
+
on a static per-key limit.
|
|
12
|
+
- Under congestion, a key may take a new slot only while it holds fewer than
|
|
13
|
+
``max(MIN_GUARANTEE_STREAMS, pool_capacity // active_keys)`` streams. Keys
|
|
14
|
+
under their fair share always admit (a key holding fewer than the minimum
|
|
15
|
+
guarantee can never be denied, making light interactive keys
|
|
16
|
+
starvation-proof), while over-share keys cannot reacquire freed slots until
|
|
17
|
+
they drop back under — freed capacity flows to the keys the congestion was
|
|
18
|
+
starving.
|
|
19
|
+
|
|
20
|
+
All quantities are computed over the selection's candidate account set, so
|
|
21
|
+
account-scoped keys are measured against the pool they can actually use. All
|
|
22
|
+
arithmetic is integer-only so decisions are deterministic and exactly
|
|
23
|
+
testable. The module performs no I/O and holds no state; the impure edge
|
|
24
|
+
(runtime counters, locking, metrics) lives in the load balancer.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from dataclasses import dataclass
|
|
30
|
+
|
|
31
|
+
API_KEY_STREAM_FAIR_SHARE_ERROR_CODE = "api_key_stream_fair_share"
|
|
32
|
+
|
|
33
|
+
MIN_GUARANTEE_STREAMS = 2
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True, slots=True)
|
|
37
|
+
class FairShareDecision:
|
|
38
|
+
admitted: bool
|
|
39
|
+
congested: bool
|
|
40
|
+
fair_share: int
|
|
41
|
+
pool_capacity: int
|
|
42
|
+
pool_inflight: int
|
|
43
|
+
active_key_count: int
|
|
44
|
+
requester_inflight: int
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def effective_stream_pool_capacity(
|
|
48
|
+
*,
|
|
49
|
+
candidate_account_count: int,
|
|
50
|
+
stream_limit: int,
|
|
51
|
+
stream_reserve_slots: int,
|
|
52
|
+
) -> int:
|
|
53
|
+
"""Return the candidate pool's stream capacity, or 0 when ungateable.
|
|
54
|
+
|
|
55
|
+
Mirrors the per-account admission arithmetic: each account contributes
|
|
56
|
+
``max(1, stream_limit - stream_reserve_slots)`` slots. A nonpositive
|
|
57
|
+
stream limit means unlimited caps and a zero return disables the gate.
|
|
58
|
+
"""
|
|
59
|
+
if stream_limit <= 0 or candidate_account_count <= 0:
|
|
60
|
+
return 0
|
|
61
|
+
per_account = max(1, stream_limit - max(0, stream_reserve_slots))
|
|
62
|
+
return candidate_account_count * per_account
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def evaluate_stream_fair_share(
|
|
66
|
+
*,
|
|
67
|
+
pool_capacity: int,
|
|
68
|
+
pool_inflight: int,
|
|
69
|
+
requester_inflight: int,
|
|
70
|
+
other_active_key_count: int,
|
|
71
|
+
threshold_pct: int,
|
|
72
|
+
min_guarantee: int = MIN_GUARANTEE_STREAMS,
|
|
73
|
+
) -> FairShareDecision:
|
|
74
|
+
"""Return the fair-share admission decision for one stream request.
|
|
75
|
+
|
|
76
|
+
``other_active_key_count`` counts keys other than the requester holding at
|
|
77
|
+
least one in-flight stream on the candidate accounts; the requester is
|
|
78
|
+
always counted exactly once on top of it, whether or not it is already
|
|
79
|
+
active. Congestion is ``pool_inflight * 100 >= pool_capacity *
|
|
80
|
+
threshold_pct`` (integer math); a ``threshold_pct`` or ``pool_capacity``
|
|
81
|
+
of zero disables the gate and admits.
|
|
82
|
+
"""
|
|
83
|
+
active_key_count = other_active_key_count + 1
|
|
84
|
+
if threshold_pct <= 0 or pool_capacity <= 0:
|
|
85
|
+
return FairShareDecision(
|
|
86
|
+
admitted=True,
|
|
87
|
+
congested=False,
|
|
88
|
+
fair_share=0,
|
|
89
|
+
pool_capacity=pool_capacity,
|
|
90
|
+
pool_inflight=pool_inflight,
|
|
91
|
+
active_key_count=active_key_count,
|
|
92
|
+
requester_inflight=requester_inflight,
|
|
93
|
+
)
|
|
94
|
+
congested = pool_inflight * 100 >= pool_capacity * threshold_pct
|
|
95
|
+
fair_share = max(min_guarantee, pool_capacity // active_key_count)
|
|
96
|
+
admitted = not congested or requester_inflight + 1 <= fair_share
|
|
97
|
+
return FairShareDecision(
|
|
98
|
+
admitted=admitted,
|
|
99
|
+
congested=congested,
|
|
100
|
+
fair_share=fair_share,
|
|
101
|
+
pool_capacity=pool_capacity,
|
|
102
|
+
pool_inflight=pool_inflight,
|
|
103
|
+
active_key_count=active_key_count,
|
|
104
|
+
requester_inflight=requester_inflight,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class ApiKeyFairShareDenialError(Exception):
|
|
109
|
+
"""Fair-share gate denial from a direct (selection-less) lease acquire.
|
|
110
|
+
|
|
111
|
+
``LoadBalancer.select_account`` reports denials through the selection
|
|
112
|
+
error surface; ``LoadBalancer.acquire_account_lease`` returns ``None``
|
|
113
|
+
only for plain per-account cap denials, so the fair-share denial raises
|
|
114
|
+
this instead of overloading that return value. Carries the full
|
|
115
|
+
``FairShareDecision`` for callers that surface the denial numbers.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
def __init__(self, decision: FairShareDecision) -> None:
|
|
119
|
+
super().__init__(fair_share_denial_message(decision))
|
|
120
|
+
self.decision = decision
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def fair_share_denial_message(decision: FairShareDecision) -> str:
|
|
124
|
+
"""Human-readable denial message carrying the decision's numbers."""
|
|
125
|
+
return (
|
|
126
|
+
"API key stream fair share exceeded under pool congestion; "
|
|
127
|
+
f"key holds {decision.requester_inflight} of a fair share of {decision.fair_share} streams "
|
|
128
|
+
f"({decision.pool_inflight}/{decision.pool_capacity} pool streams in flight across "
|
|
129
|
+
f"{decision.active_key_count} active keys). "
|
|
130
|
+
"Waiting for the key's streams to finish or the pool to decongest."
|
|
131
|
+
)
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Collection
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import Integer, bindparam, text
|
|
6
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
7
|
+
from sqlalchemy.sql.elements import TextClause
|
|
8
|
+
|
|
9
|
+
from app.db.session import sqlite_writer_section
|
|
10
|
+
|
|
11
|
+
_TABLE = "file_account_pins"
|
|
12
|
+
|
|
13
|
+
# Ownership TTLs stay entirely in the database clock domain. PostgreSQL's
|
|
14
|
+
# clock_timestamp() is evaluated when each clause executes. A successful claim
|
|
15
|
+
# is followed by a guarded refresh in the same transaction so even an INSERT
|
|
16
|
+
# that waited behind an ultimately rolled-back unique contender receives its
|
|
17
|
+
# full TTL after the wait. SQLite's padded strftime form matches SQLAlchemy
|
|
18
|
+
# DateTime's six-digit fractional width, preserving exact lexicographic expiry
|
|
19
|
+
# checks.
|
|
20
|
+
_POSTGRES_NOW = "clock_timestamp()"
|
|
21
|
+
_POSTGRES_NOW_PLUS_TTL = "clock_timestamp() + make_interval(secs => :ttl)"
|
|
22
|
+
_POSTGRES_STATEMENT_NOW = "statement_timestamp()"
|
|
23
|
+
_SQLITE_NOW = "(strftime('%Y-%m-%d %H:%M:%f', 'now') || '000')"
|
|
24
|
+
_SQLITE_NOW_PLUS_TTL = "(strftime('%Y-%m-%d %H:%M:%f', 'now', '+' || :ttl || ' seconds') || '000')"
|
|
25
|
+
|
|
26
|
+
_POSTGRES_CLAIM = text(
|
|
27
|
+
f"""
|
|
28
|
+
INSERT INTO {_TABLE} (file_id, account_id, expires_at)
|
|
29
|
+
VALUES (:file_id, :account_id, {_POSTGRES_NOW_PLUS_TTL})
|
|
30
|
+
ON CONFLICT (file_id) DO UPDATE SET
|
|
31
|
+
account_id = excluded.account_id,
|
|
32
|
+
expires_at = {_POSTGRES_NOW_PLUS_TTL}
|
|
33
|
+
WHERE {_TABLE}.account_id = :account_id
|
|
34
|
+
OR {_TABLE}.expires_at <= {_POSTGRES_NOW}
|
|
35
|
+
RETURNING account_id
|
|
36
|
+
"""
|
|
37
|
+
).bindparams(bindparam("ttl", type_=Integer))
|
|
38
|
+
|
|
39
|
+
_SQLITE_CLAIM = text(
|
|
40
|
+
f"""
|
|
41
|
+
INSERT INTO {_TABLE} (file_id, account_id, expires_at)
|
|
42
|
+
VALUES (:file_id, :account_id, {_SQLITE_NOW_PLUS_TTL})
|
|
43
|
+
ON CONFLICT (file_id) DO UPDATE SET
|
|
44
|
+
account_id = excluded.account_id,
|
|
45
|
+
expires_at = {_SQLITE_NOW_PLUS_TTL}
|
|
46
|
+
WHERE {_TABLE}.account_id = :account_id
|
|
47
|
+
OR {_TABLE}.expires_at <= {_SQLITE_NOW}
|
|
48
|
+
RETURNING account_id
|
|
49
|
+
"""
|
|
50
|
+
).bindparams(bindparam("ttl", type_=Integer))
|
|
51
|
+
|
|
52
|
+
_POSTGRES_CLEANUP = text(f"DELETE FROM {_TABLE} WHERE expires_at <= {_POSTGRES_STATEMENT_NOW}")
|
|
53
|
+
_SQLITE_CLEANUP = text(f"DELETE FROM {_TABLE} WHERE expires_at <= {_SQLITE_NOW}")
|
|
54
|
+
|
|
55
|
+
_POSTGRES_REFRESH = text(
|
|
56
|
+
f"""
|
|
57
|
+
UPDATE {_TABLE}
|
|
58
|
+
SET expires_at = {_POSTGRES_NOW_PLUS_TTL}
|
|
59
|
+
WHERE file_id = :file_id
|
|
60
|
+
AND account_id = :account_id
|
|
61
|
+
RETURNING account_id
|
|
62
|
+
"""
|
|
63
|
+
).bindparams(bindparam("ttl", type_=Integer))
|
|
64
|
+
_SQLITE_REFRESH = text(
|
|
65
|
+
f"""
|
|
66
|
+
UPDATE {_TABLE}
|
|
67
|
+
SET expires_at = {_SQLITE_NOW_PLUS_TTL}
|
|
68
|
+
WHERE file_id = :file_id
|
|
69
|
+
AND account_id = :account_id
|
|
70
|
+
RETURNING account_id
|
|
71
|
+
"""
|
|
72
|
+
).bindparams(bindparam("ttl", type_=Integer))
|
|
73
|
+
|
|
74
|
+
_POSTGRES_GET_LIVE = text(f"SELECT account_id FROM {_TABLE} WHERE file_id = :file_id AND expires_at > {_POSTGRES_NOW}")
|
|
75
|
+
_SQLITE_GET_LIVE = text(f"SELECT account_id FROM {_TABLE} WHERE file_id = :file_id AND expires_at > {_SQLITE_NOW}")
|
|
76
|
+
|
|
77
|
+
_POSTGRES_GET_LIVE_MANY = text(
|
|
78
|
+
f"""
|
|
79
|
+
SELECT file_id, account_id
|
|
80
|
+
FROM {_TABLE}
|
|
81
|
+
WHERE file_id IN :file_ids
|
|
82
|
+
AND expires_at > {_POSTGRES_NOW}
|
|
83
|
+
"""
|
|
84
|
+
).bindparams(bindparam("file_ids", expanding=True))
|
|
85
|
+
_SQLITE_GET_LIVE_MANY = text(
|
|
86
|
+
f"""
|
|
87
|
+
SELECT file_id, account_id
|
|
88
|
+
FROM {_TABLE}
|
|
89
|
+
WHERE file_id IN :file_ids
|
|
90
|
+
AND expires_at > {_SQLITE_NOW}
|
|
91
|
+
"""
|
|
92
|
+
).bindparams(bindparam("file_ids", expanding=True))
|
|
93
|
+
|
|
94
|
+
_GET_ACCOUNT = text(f"SELECT account_id FROM {_TABLE} WHERE file_id = :file_id")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class FileAccountPinOwnershipConflict(RuntimeError):
|
|
98
|
+
def __init__(self, file_id: str, persisted_account_id: str, requested_account_id: str) -> None:
|
|
99
|
+
super().__init__(
|
|
100
|
+
f"Live file ownership conflict for {file_id!r}: "
|
|
101
|
+
f"persisted={persisted_account_id!r} requested={requested_account_id!r}"
|
|
102
|
+
)
|
|
103
|
+
self.file_id = file_id
|
|
104
|
+
self.persisted_account_id = persisted_account_id
|
|
105
|
+
self.requested_account_id = requested_account_id
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def build_file_account_pin_claim(*, dialect_name: str) -> TextClause:
|
|
109
|
+
if dialect_name == "postgresql":
|
|
110
|
+
return _POSTGRES_CLAIM
|
|
111
|
+
if dialect_name == "sqlite":
|
|
112
|
+
return _SQLITE_CLAIM
|
|
113
|
+
raise RuntimeError(f"Unsupported database dialect for file account pins: {dialect_name}")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def build_file_account_pin_cleanup(*, dialect_name: str) -> TextClause:
|
|
117
|
+
if dialect_name == "postgresql":
|
|
118
|
+
return _POSTGRES_CLEANUP
|
|
119
|
+
if dialect_name == "sqlite":
|
|
120
|
+
return _SQLITE_CLEANUP
|
|
121
|
+
raise RuntimeError(f"Unsupported database dialect for file account pins: {dialect_name}")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def build_file_account_pin_refresh(*, dialect_name: str) -> TextClause:
|
|
125
|
+
if dialect_name == "postgresql":
|
|
126
|
+
return _POSTGRES_REFRESH
|
|
127
|
+
if dialect_name == "sqlite":
|
|
128
|
+
return _SQLITE_REFRESH
|
|
129
|
+
raise RuntimeError(f"Unsupported database dialect for file account pins: {dialect_name}")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def build_file_account_pin_live_lookup(*, dialect_name: str, many: bool = False) -> TextClause:
|
|
133
|
+
if dialect_name == "postgresql":
|
|
134
|
+
return _POSTGRES_GET_LIVE_MANY if many else _POSTGRES_GET_LIVE
|
|
135
|
+
if dialect_name == "sqlite":
|
|
136
|
+
return _SQLITE_GET_LIVE_MANY if many else _SQLITE_GET_LIVE
|
|
137
|
+
raise RuntimeError(f"Unsupported database dialect for file account pins: {dialect_name}")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class FileAccountPinRepository:
|
|
141
|
+
def __init__(self, session: AsyncSession) -> None:
|
|
142
|
+
self._session = session
|
|
143
|
+
|
|
144
|
+
async def claim(self, file_id: str, account_id: str, *, ttl_seconds: int) -> None:
|
|
145
|
+
if ttl_seconds <= 0:
|
|
146
|
+
raise ValueError("File account pin TTL must be positive")
|
|
147
|
+
dialect_name = self._dialect_name()
|
|
148
|
+
params = {
|
|
149
|
+
"file_id": file_id,
|
|
150
|
+
"account_id": account_id,
|
|
151
|
+
"ttl": ttl_seconds,
|
|
152
|
+
}
|
|
153
|
+
async with sqlite_writer_section():
|
|
154
|
+
await self._session.execute(build_file_account_pin_cleanup(dialect_name=dialect_name))
|
|
155
|
+
persisted_account_id = (
|
|
156
|
+
await self._session.execute(
|
|
157
|
+
build_file_account_pin_claim(dialect_name=dialect_name),
|
|
158
|
+
params,
|
|
159
|
+
)
|
|
160
|
+
).scalar_one_or_none()
|
|
161
|
+
if persisted_account_id is None:
|
|
162
|
+
persisted_account_id = await self._session.scalar(
|
|
163
|
+
_GET_ACCOUNT,
|
|
164
|
+
{"file_id": file_id},
|
|
165
|
+
)
|
|
166
|
+
if persisted_account_id != account_id:
|
|
167
|
+
await self._session.rollback()
|
|
168
|
+
raise FileAccountPinOwnershipConflict(
|
|
169
|
+
file_id,
|
|
170
|
+
persisted_account_id or "<missing>",
|
|
171
|
+
account_id,
|
|
172
|
+
)
|
|
173
|
+
refreshed_account_id = (
|
|
174
|
+
await self._session.execute(
|
|
175
|
+
build_file_account_pin_refresh(dialect_name=dialect_name),
|
|
176
|
+
params,
|
|
177
|
+
)
|
|
178
|
+
).scalar_one_or_none()
|
|
179
|
+
if refreshed_account_id != account_id:
|
|
180
|
+
await self._session.rollback()
|
|
181
|
+
raise RuntimeError(f"Failed to refresh file account pin after claim: {file_id!r}")
|
|
182
|
+
await self._session.commit()
|
|
183
|
+
|
|
184
|
+
async def get_live_account_id(self, file_id: str) -> str | None:
|
|
185
|
+
return await self._session.scalar(
|
|
186
|
+
build_file_account_pin_live_lookup(dialect_name=self._dialect_name()),
|
|
187
|
+
{"file_id": file_id},
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
async def get_live_account_ids(self, file_ids: Collection[str]) -> dict[str, str]:
|
|
191
|
+
unique_file_ids = tuple(dict.fromkeys(file_ids))
|
|
192
|
+
if not unique_file_ids:
|
|
193
|
+
return {}
|
|
194
|
+
rows = (
|
|
195
|
+
(
|
|
196
|
+
await self._session.execute(
|
|
197
|
+
build_file_account_pin_live_lookup(
|
|
198
|
+
dialect_name=self._dialect_name(),
|
|
199
|
+
many=True,
|
|
200
|
+
),
|
|
201
|
+
{"file_ids": unique_file_ids},
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
.tuples()
|
|
205
|
+
.all()
|
|
206
|
+
)
|
|
207
|
+
return dict(rows)
|
|
208
|
+
|
|
209
|
+
def _dialect_name(self) -> str:
|
|
210
|
+
return self._session.get_bind().dialect.name
|