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,274 @@
|
|
|
1
|
+
"""Cross-replica coordination primitives for reset-credit redemption.
|
|
2
|
+
|
|
3
|
+
Two shared-database primitives back the redeem path:
|
|
4
|
+
|
|
5
|
+
- A durable idempotency ledger (``reset_credit_redeem_requests``) that pins the
|
|
6
|
+
(account_id, redeem_request_id) pair to the credit selected on the first
|
|
7
|
+
attempt, so a retry served by ANY replica retargets the same credit instead
|
|
8
|
+
of burning a second one.
|
|
9
|
+
- A per-account claim row (``reset_credit_redeem_claims``) that serializes
|
|
10
|
+
redemption across processes sharing one SQLite file via a single atomic
|
|
11
|
+
conditional upsert with a lease the holder renews on a heartbeat while the
|
|
12
|
+
redeem section runs. PostgreSQL keeps ``pg_advisory_xact_lock``.
|
|
13
|
+
|
|
14
|
+
All statements run on dedicated short-lived sessions committed immediately;
|
|
15
|
+
they never join the caller's transaction.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import asyncio
|
|
21
|
+
import logging
|
|
22
|
+
import uuid
|
|
23
|
+
from datetime import UTC, datetime, timedelta
|
|
24
|
+
|
|
25
|
+
from sqlalchemy import delete, select, update
|
|
26
|
+
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
|
27
|
+
from sqlalchemy.dialects.sqlite import insert as sqlite_insert
|
|
28
|
+
|
|
29
|
+
from app.db.models import ResetCreditRedeemClaim, ResetCreditRedeemRequest
|
|
30
|
+
from app.db.session import SessionLocal, close_session
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
REDEEM_CLAIM_LEASE_SECONDS = 30.0
|
|
35
|
+
REDEEM_CLAIM_RENEW_INTERVAL_SECONDS = 10.0
|
|
36
|
+
REDEEM_CLAIM_RETRY_INTERVAL_SECONDS = 0.1
|
|
37
|
+
REDEEM_CLAIM_TIMEOUT_SECONDS = 15.0
|
|
38
|
+
REDEEM_REQUEST_TTL = timedelta(hours=24)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class RedeemClaimTimeoutError(Exception):
|
|
42
|
+
"""The per-account redeem claim stayed held past the acquisition timeout."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def new_redeem_claim_holder_id() -> str:
|
|
46
|
+
return uuid.uuid4().hex
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def try_acquire_redeem_claim(
|
|
50
|
+
account_id: str,
|
|
51
|
+
holder_id: str,
|
|
52
|
+
*,
|
|
53
|
+
lease_seconds: float = REDEEM_CLAIM_LEASE_SECONDS,
|
|
54
|
+
) -> bool:
|
|
55
|
+
"""Attempt to claim the per-account redeem slot; True when claimed.
|
|
56
|
+
|
|
57
|
+
Uses one atomic ``INSERT ... ON CONFLICT(account_id) DO UPDATE ... WHERE
|
|
58
|
+
expires_at < now`` so only a missing or lease-expired claim can be taken —
|
|
59
|
+
the same conditional-upsert shape as the scheduler-leader lease.
|
|
60
|
+
"""
|
|
61
|
+
now = datetime.now(UTC)
|
|
62
|
+
session = SessionLocal()
|
|
63
|
+
try:
|
|
64
|
+
insert_stmt = sqlite_insert(ResetCreditRedeemClaim).values(
|
|
65
|
+
account_id=account_id,
|
|
66
|
+
holder_id=holder_id,
|
|
67
|
+
expires_at=now + timedelta(seconds=lease_seconds),
|
|
68
|
+
)
|
|
69
|
+
stmt = insert_stmt.on_conflict_do_update(
|
|
70
|
+
index_elements=[ResetCreditRedeemClaim.account_id],
|
|
71
|
+
set_={
|
|
72
|
+
"holder_id": insert_stmt.excluded.holder_id,
|
|
73
|
+
"expires_at": insert_stmt.excluded.expires_at,
|
|
74
|
+
},
|
|
75
|
+
where=ResetCreditRedeemClaim.expires_at < now,
|
|
76
|
+
)
|
|
77
|
+
result = await session.execute(stmt.returning(ResetCreditRedeemClaim.account_id))
|
|
78
|
+
await session.commit()
|
|
79
|
+
return result.scalar_one_or_none() is not None
|
|
80
|
+
finally:
|
|
81
|
+
await close_session(session)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async def acquire_redeem_claim(
|
|
85
|
+
account_id: str,
|
|
86
|
+
holder_id: str,
|
|
87
|
+
*,
|
|
88
|
+
lease_seconds: float = REDEEM_CLAIM_LEASE_SECONDS,
|
|
89
|
+
retry_interval_seconds: float = REDEEM_CLAIM_RETRY_INTERVAL_SECONDS,
|
|
90
|
+
timeout_seconds: float = REDEEM_CLAIM_TIMEOUT_SECONDS,
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Acquire the per-account redeem claim, retrying until the timeout."""
|
|
93
|
+
loop = asyncio.get_running_loop()
|
|
94
|
+
deadline = loop.time() + timeout_seconds
|
|
95
|
+
while True:
|
|
96
|
+
if await try_acquire_redeem_claim(account_id, holder_id, lease_seconds=lease_seconds):
|
|
97
|
+
return
|
|
98
|
+
remaining = deadline - loop.time()
|
|
99
|
+
if remaining <= 0:
|
|
100
|
+
raise RedeemClaimTimeoutError(
|
|
101
|
+
f"reset-credit redeem claim for account {account_id} not acquired within {timeout_seconds}s"
|
|
102
|
+
)
|
|
103
|
+
await asyncio.sleep(min(retry_interval_seconds, remaining))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
async def renew_redeem_claim(
|
|
107
|
+
account_id: str,
|
|
108
|
+
holder_id: str,
|
|
109
|
+
*,
|
|
110
|
+
lease_seconds: float = REDEEM_CLAIM_LEASE_SECONDS,
|
|
111
|
+
) -> bool:
|
|
112
|
+
"""Extend the holder's lease; False when the row is no longer held by this holder."""
|
|
113
|
+
now = datetime.now(UTC)
|
|
114
|
+
session = SessionLocal()
|
|
115
|
+
try:
|
|
116
|
+
result = await session.execute(
|
|
117
|
+
update(ResetCreditRedeemClaim)
|
|
118
|
+
.where(
|
|
119
|
+
ResetCreditRedeemClaim.account_id == account_id,
|
|
120
|
+
ResetCreditRedeemClaim.holder_id == holder_id,
|
|
121
|
+
)
|
|
122
|
+
.values(expires_at=now + timedelta(seconds=lease_seconds))
|
|
123
|
+
.returning(ResetCreditRedeemClaim.account_id)
|
|
124
|
+
)
|
|
125
|
+
await session.commit()
|
|
126
|
+
return result.scalar_one_or_none() is not None
|
|
127
|
+
finally:
|
|
128
|
+
await close_session(session)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
async def renew_redeem_claim_periodically(
|
|
132
|
+
account_id: str,
|
|
133
|
+
holder_id: str,
|
|
134
|
+
*,
|
|
135
|
+
lease_seconds: float = REDEEM_CLAIM_LEASE_SECONDS,
|
|
136
|
+
renew_interval_seconds: float = REDEEM_CLAIM_RENEW_INTERVAL_SECONDS,
|
|
137
|
+
) -> None:
|
|
138
|
+
"""Heartbeat that keeps a held claim's lease alive while the redeem section runs.
|
|
139
|
+
|
|
140
|
+
Mirrors the scheduler-leader renew pattern: the holder extends
|
|
141
|
+
``expires_at`` every ``renew_interval_seconds`` (a fraction of the lease)
|
|
142
|
+
so a legitimately slow redemption — usage fetch retries plus the upstream
|
|
143
|
+
consume can exceed one lease — is not taken over by a second process.
|
|
144
|
+
Transient renewal errors are logged and retried on the next tick; a renew
|
|
145
|
+
reporting the row gone means an expired claim was already taken over, so
|
|
146
|
+
the loop stops (exclusivity is lost and lease takeover semantics apply).
|
|
147
|
+
The caller cancels this task before releasing the claim.
|
|
148
|
+
"""
|
|
149
|
+
while True:
|
|
150
|
+
await asyncio.sleep(renew_interval_seconds)
|
|
151
|
+
try:
|
|
152
|
+
renewed = await renew_redeem_claim(account_id, holder_id, lease_seconds=lease_seconds)
|
|
153
|
+
except Exception:
|
|
154
|
+
logger.warning(
|
|
155
|
+
"reset-credit redeem claim renewal failed account_id=%s (retrying next tick)",
|
|
156
|
+
account_id,
|
|
157
|
+
exc_info=True,
|
|
158
|
+
)
|
|
159
|
+
continue
|
|
160
|
+
if not renewed:
|
|
161
|
+
logger.warning(
|
|
162
|
+
"reset-credit redeem claim lost before renewal account_id=%s holder_id=%s",
|
|
163
|
+
account_id,
|
|
164
|
+
holder_id,
|
|
165
|
+
)
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
async def release_redeem_claim(account_id: str, holder_id: str) -> None:
|
|
170
|
+
"""Release the claim if this holder still owns it (lease expiry is the backstop)."""
|
|
171
|
+
session = SessionLocal()
|
|
172
|
+
try:
|
|
173
|
+
await session.execute(
|
|
174
|
+
delete(ResetCreditRedeemClaim).where(
|
|
175
|
+
ResetCreditRedeemClaim.account_id == account_id,
|
|
176
|
+
ResetCreditRedeemClaim.holder_id == holder_id,
|
|
177
|
+
)
|
|
178
|
+
)
|
|
179
|
+
await session.commit()
|
|
180
|
+
except Exception:
|
|
181
|
+
logger.warning(
|
|
182
|
+
"reset-credit redeem claim release failed account_id=%s (lease expiry will recover)",
|
|
183
|
+
account_id,
|
|
184
|
+
exc_info=True,
|
|
185
|
+
)
|
|
186
|
+
finally:
|
|
187
|
+
await close_session(session)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
async def get_pinned_redeem_credit_id(account_id: str, redeem_request_id: str) -> str | None:
|
|
191
|
+
"""Read the credit pinned to this redeem_request_id by any replica.
|
|
192
|
+
|
|
193
|
+
Rows older than the 24h TTL are ignored (the read TTL matches the purge
|
|
194
|
+
TTL applied by ``pin_redeem_request``), so an expired pin reads as absent.
|
|
195
|
+
The caller then re-selects a fresh credit and re-pins it via
|
|
196
|
+
``pin_redeem_request`` instead of forwarding the redemption for a stale
|
|
197
|
+
credit id that the purge path would otherwise drop.
|
|
198
|
+
"""
|
|
199
|
+
now = datetime.now(UTC)
|
|
200
|
+
session = SessionLocal()
|
|
201
|
+
try:
|
|
202
|
+
return await session.scalar(
|
|
203
|
+
select(ResetCreditRedeemRequest.credit_id).where(
|
|
204
|
+
ResetCreditRedeemRequest.account_id == account_id,
|
|
205
|
+
ResetCreditRedeemRequest.redeem_request_id == redeem_request_id,
|
|
206
|
+
ResetCreditRedeemRequest.created_at >= now - REDEEM_REQUEST_TTL,
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
finally:
|
|
210
|
+
await close_session(session)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
async def pin_redeem_request(account_id: str, redeem_request_id: str, credit_id: str) -> str:
|
|
214
|
+
"""Durably pin the selected credit to this redeem request; first writer wins.
|
|
215
|
+
|
|
216
|
+
Returns the authoritative credit id (the previously pinned one on
|
|
217
|
+
conflict). Rows older than the 24h TTL for this account are purged in the
|
|
218
|
+
same transaction BEFORE the insert, so a ``redeem_request_id`` reused after
|
|
219
|
+
its prior row has aged past the TTL is re-pinned to the new credit instead
|
|
220
|
+
of colliding with the stale row via ``ON CONFLICT DO NOTHING`` and losing
|
|
221
|
+
the new pin.
|
|
222
|
+
"""
|
|
223
|
+
now = datetime.now(UTC)
|
|
224
|
+
session = SessionLocal()
|
|
225
|
+
try:
|
|
226
|
+
values = {
|
|
227
|
+
"account_id": account_id,
|
|
228
|
+
"redeem_request_id": redeem_request_id,
|
|
229
|
+
"credit_id": credit_id,
|
|
230
|
+
"created_at": now,
|
|
231
|
+
}
|
|
232
|
+
# Purge expired rows first: an expired row for the SAME
|
|
233
|
+
# (account_id, redeem_request_id) would otherwise absorb the insert
|
|
234
|
+
# below via ON CONFLICT DO NOTHING and then be deleted, leaving the new
|
|
235
|
+
# attempt with no durable pin.
|
|
236
|
+
await session.execute(
|
|
237
|
+
delete(ResetCreditRedeemRequest).where(
|
|
238
|
+
ResetCreditRedeemRequest.account_id == account_id,
|
|
239
|
+
ResetCreditRedeemRequest.created_at < now - REDEEM_REQUEST_TTL,
|
|
240
|
+
)
|
|
241
|
+
)
|
|
242
|
+
dialect = session.get_bind().dialect.name
|
|
243
|
+
if dialect == "postgresql":
|
|
244
|
+
await session.execute(
|
|
245
|
+
pg_insert(ResetCreditRedeemRequest)
|
|
246
|
+
.values(**values)
|
|
247
|
+
.on_conflict_do_nothing(
|
|
248
|
+
index_elements=[
|
|
249
|
+
ResetCreditRedeemRequest.account_id,
|
|
250
|
+
ResetCreditRedeemRequest.redeem_request_id,
|
|
251
|
+
]
|
|
252
|
+
)
|
|
253
|
+
)
|
|
254
|
+
else:
|
|
255
|
+
await session.execute(
|
|
256
|
+
sqlite_insert(ResetCreditRedeemRequest)
|
|
257
|
+
.values(**values)
|
|
258
|
+
.on_conflict_do_nothing(
|
|
259
|
+
index_elements=[
|
|
260
|
+
ResetCreditRedeemRequest.account_id,
|
|
261
|
+
ResetCreditRedeemRequest.redeem_request_id,
|
|
262
|
+
]
|
|
263
|
+
)
|
|
264
|
+
)
|
|
265
|
+
await session.commit()
|
|
266
|
+
stored = await session.scalar(
|
|
267
|
+
select(ResetCreditRedeemRequest.credit_id).where(
|
|
268
|
+
ResetCreditRedeemRequest.account_id == account_id,
|
|
269
|
+
ResetCreditRedeemRequest.redeem_request_id == redeem_request_id,
|
|
270
|
+
)
|
|
271
|
+
)
|
|
272
|
+
return stored if stored is not None else credit_id
|
|
273
|
+
finally:
|
|
274
|
+
await close_session(session)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
import anyio
|
|
6
|
+
|
|
7
|
+
from app.core.clients.rate_limit_reset_credits import (
|
|
8
|
+
RateLimitResetCreditsSnapshot,
|
|
9
|
+
ResetCreditItem,
|
|
10
|
+
_nearest_available_expires_at,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RateLimitResetCreditsStore:
|
|
15
|
+
"""In-memory cache of the most recent reset-credits snapshot per account.
|
|
16
|
+
|
|
17
|
+
Mirrors the lock-guarded shape of :class:`RateLimitHeadersCache` /
|
|
18
|
+
:class:`AccountSelectionCache`. Snapshots are keyed by account id and are
|
|
19
|
+
repopulated by each replica's refresh scheduler on every tick; reads from
|
|
20
|
+
the dashboard (GET + the AccountSummary mapper) never hit upstream.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self) -> None:
|
|
24
|
+
self._snapshots: dict[str, RateLimitResetCreditsSnapshot] = {}
|
|
25
|
+
self._lock = anyio.Lock()
|
|
26
|
+
self._clear_generation = 0
|
|
27
|
+
self._account_generations: dict[str, int] = {}
|
|
28
|
+
|
|
29
|
+
async def set(self, account_id: str, snapshot: RateLimitResetCreditsSnapshot) -> None:
|
|
30
|
+
async with self._lock:
|
|
31
|
+
self._snapshots[account_id] = snapshot
|
|
32
|
+
self._bump_account_generation(account_id)
|
|
33
|
+
|
|
34
|
+
def generation(self, account_id: str) -> int:
|
|
35
|
+
return self._generation_for(account_id)
|
|
36
|
+
|
|
37
|
+
async def set_if_generation(
|
|
38
|
+
self,
|
|
39
|
+
account_id: str,
|
|
40
|
+
snapshot: RateLimitResetCreditsSnapshot,
|
|
41
|
+
expected_generation: int,
|
|
42
|
+
) -> bool:
|
|
43
|
+
async with self._lock:
|
|
44
|
+
if self._generation_for(account_id) != expected_generation:
|
|
45
|
+
return False
|
|
46
|
+
self._snapshots[account_id] = snapshot
|
|
47
|
+
self._bump_account_generation(account_id)
|
|
48
|
+
return True
|
|
49
|
+
|
|
50
|
+
async def mark_credit_redeemed(
|
|
51
|
+
self,
|
|
52
|
+
account_id: str,
|
|
53
|
+
credit_id: str,
|
|
54
|
+
*,
|
|
55
|
+
redeemed_at: datetime | None,
|
|
56
|
+
) -> None:
|
|
57
|
+
async with self._lock:
|
|
58
|
+
snapshot = self._snapshots.get(account_id)
|
|
59
|
+
if snapshot is None:
|
|
60
|
+
return
|
|
61
|
+
updated_credits, matched = _mark_credit_redeemed(snapshot.credits, credit_id, redeemed_at=redeemed_at)
|
|
62
|
+
if not matched:
|
|
63
|
+
return
|
|
64
|
+
available_count = sum(1 for credit in updated_credits if credit.status == "available")
|
|
65
|
+
self._snapshots[account_id] = snapshot.model_copy(
|
|
66
|
+
update={
|
|
67
|
+
"available_count": available_count,
|
|
68
|
+
"nearest_expires_at": _nearest_available_expires_at(updated_credits),
|
|
69
|
+
"credits": updated_credits,
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
self._bump_account_generation(account_id)
|
|
73
|
+
|
|
74
|
+
def get(self, account_id: str) -> RateLimitResetCreditsSnapshot | None:
|
|
75
|
+
return self._snapshots.get(account_id)
|
|
76
|
+
|
|
77
|
+
async def invalidate(self, account_id: str | None = None) -> None:
|
|
78
|
+
async with self._lock:
|
|
79
|
+
if account_id is None:
|
|
80
|
+
self._snapshots.clear()
|
|
81
|
+
self._clear_generation += 1
|
|
82
|
+
return
|
|
83
|
+
self._snapshots.pop(account_id, None)
|
|
84
|
+
self._bump_account_generation(account_id)
|
|
85
|
+
|
|
86
|
+
def _generation_for(self, account_id: str) -> int:
|
|
87
|
+
return self._clear_generation + self._account_generations.get(account_id, 0)
|
|
88
|
+
|
|
89
|
+
def _bump_account_generation(self, account_id: str) -> None:
|
|
90
|
+
self._account_generations[account_id] = self._account_generations.get(account_id, 0) + 1
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
_rate_limit_reset_credits_store = RateLimitResetCreditsStore()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def get_rate_limit_reset_credits_store() -> RateLimitResetCreditsStore:
|
|
97
|
+
return _rate_limit_reset_credits_store
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _mark_credit_redeemed(
|
|
101
|
+
credits: list[ResetCreditItem],
|
|
102
|
+
credit_id: str,
|
|
103
|
+
*,
|
|
104
|
+
redeemed_at: datetime | None,
|
|
105
|
+
) -> tuple[list[ResetCreditItem], bool]:
|
|
106
|
+
matched = False
|
|
107
|
+
updated: list[ResetCreditItem] = []
|
|
108
|
+
for credit in credits:
|
|
109
|
+
if credit.id != credit_id:
|
|
110
|
+
updated.append(credit)
|
|
111
|
+
continue
|
|
112
|
+
matched = True
|
|
113
|
+
updated.append(credit.model_copy(update={"status": "redeemed", "redeemed_at": redeemed_at}))
|
|
114
|
+
return updated, matched
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import date
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
|
7
|
+
|
|
8
|
+
from app.core.auth.dependencies import (
|
|
9
|
+
set_dashboard_error_format,
|
|
10
|
+
validate_dashboard_session,
|
|
11
|
+
)
|
|
12
|
+
from app.core.exceptions import DashboardBadRequestError
|
|
13
|
+
from app.dependencies import ReportsContext, get_reports_caches, get_reports_context
|
|
14
|
+
from app.modules.reports.cache import ReportCacheKey, ReportsCaches
|
|
15
|
+
from app.modules.reports.repository import DailyReportRangeTooLargeError
|
|
16
|
+
from app.modules.reports.schemas import ReportsOptionsResponse, ReportsResponse, ThreadIdentityResponse
|
|
17
|
+
from app.modules.reports.service import InvalidReportDateRangeError, resolve_report_range
|
|
18
|
+
|
|
19
|
+
router = APIRouter(
|
|
20
|
+
prefix="/api/reports",
|
|
21
|
+
tags=["dashboard"],
|
|
22
|
+
dependencies=[Depends(validate_dashboard_session), Depends(set_dashboard_error_format)],
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@router.get("", response_model=ReportsResponse)
|
|
27
|
+
async def get_reports(
|
|
28
|
+
context: ReportsContext = Depends(get_reports_context),
|
|
29
|
+
caches: ReportsCaches = Depends(get_reports_caches),
|
|
30
|
+
start_date: Annotated[date | None, Query()] = None,
|
|
31
|
+
end_date: Annotated[date | None, Query()] = None,
|
|
32
|
+
report_timezone: Annotated[str | None, Query(alias="timezone")] = None,
|
|
33
|
+
account_id: Annotated[list[str] | None, Query()] = None,
|
|
34
|
+
api_key_id: Annotated[list[str] | None, Query()] = None,
|
|
35
|
+
model: Annotated[str | None, Query()] = None,
|
|
36
|
+
useragent_group: Annotated[str | None, Query()] = None,
|
|
37
|
+
) -> ReportsResponse:
|
|
38
|
+
try:
|
|
39
|
+
key = _cache_key(start_date, end_date, report_timezone, account_id, api_key_id, model, useragent_group)
|
|
40
|
+
return await caches.reports.get(
|
|
41
|
+
key,
|
|
42
|
+
lambda: context.service.get_reports(
|
|
43
|
+
start_date=key.start,
|
|
44
|
+
end_date=key.end,
|
|
45
|
+
report_timezone=key.timezone,
|
|
46
|
+
account_ids=list(key.accounts) or None,
|
|
47
|
+
api_key_ids=list(key.api_keys) or None,
|
|
48
|
+
model=key.model or None,
|
|
49
|
+
useragent_group=key.useragent or None,
|
|
50
|
+
),
|
|
51
|
+
)
|
|
52
|
+
except InvalidReportDateRangeError as exc:
|
|
53
|
+
raise DashboardBadRequestError(str(exc), code="invalid_report_date_range") from exc
|
|
54
|
+
except DailyReportRangeTooLargeError as exc:
|
|
55
|
+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@router.get("/thread-identity", response_model=ThreadIdentityResponse)
|
|
59
|
+
async def get_thread_identity(
|
|
60
|
+
context: ReportsContext = Depends(get_reports_context),
|
|
61
|
+
caches: ReportsCaches = Depends(get_reports_caches),
|
|
62
|
+
start_date: Annotated[date | None, Query()] = None,
|
|
63
|
+
end_date: Annotated[date | None, Query()] = None,
|
|
64
|
+
report_timezone: Annotated[str | None, Query(alias="timezone")] = None,
|
|
65
|
+
) -> ThreadIdentityResponse:
|
|
66
|
+
"""Thread identity and cache locality over the selected range, pool-wide.
|
|
67
|
+
|
|
68
|
+
The Reports filters are intentionally not accepted here: filtering by
|
|
69
|
+
account would pin every conversation to one account and make the
|
|
70
|
+
accounts-per-conversation factor meaningless.
|
|
71
|
+
"""
|
|
72
|
+
try:
|
|
73
|
+
key = _cache_key(start_date, end_date, report_timezone, None, None)
|
|
74
|
+
return await caches.thread_identity.get(
|
|
75
|
+
key,
|
|
76
|
+
lambda: context.service.get_thread_identity(
|
|
77
|
+
start_date=key.start,
|
|
78
|
+
end_date=key.end,
|
|
79
|
+
report_timezone=key.timezone,
|
|
80
|
+
),
|
|
81
|
+
)
|
|
82
|
+
except InvalidReportDateRangeError as exc:
|
|
83
|
+
raise DashboardBadRequestError(str(exc), code="invalid_report_date_range") from exc
|
|
84
|
+
except DailyReportRangeTooLargeError as exc:
|
|
85
|
+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@router.get("/options", response_model=ReportsOptionsResponse)
|
|
89
|
+
async def get_report_options(
|
|
90
|
+
context: ReportsContext = Depends(get_reports_context),
|
|
91
|
+
caches: ReportsCaches = Depends(get_reports_caches),
|
|
92
|
+
start_date: Annotated[date | None, Query()] = None,
|
|
93
|
+
end_date: Annotated[date | None, Query()] = None,
|
|
94
|
+
report_timezone: Annotated[str | None, Query(alias="timezone")] = None,
|
|
95
|
+
account_id: Annotated[list[str] | None, Query()] = None,
|
|
96
|
+
api_key_id: Annotated[list[str] | None, Query()] = None,
|
|
97
|
+
) -> ReportsOptionsResponse:
|
|
98
|
+
try:
|
|
99
|
+
key = _cache_key(start_date, end_date, report_timezone, account_id, api_key_id)
|
|
100
|
+
return await caches.options.get(
|
|
101
|
+
key,
|
|
102
|
+
lambda: context.service.get_options(
|
|
103
|
+
start_date=key.start,
|
|
104
|
+
end_date=key.end,
|
|
105
|
+
report_timezone=key.timezone,
|
|
106
|
+
account_ids=list(key.accounts) or None,
|
|
107
|
+
api_key_ids=list(key.api_keys) or None,
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
except InvalidReportDateRangeError as exc:
|
|
111
|
+
raise DashboardBadRequestError(str(exc), code="invalid_report_date_range") from exc
|
|
112
|
+
except DailyReportRangeTooLargeError as exc:
|
|
113
|
+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _cache_key(
|
|
117
|
+
start: date | None,
|
|
118
|
+
end: date | None,
|
|
119
|
+
timezone: str | None,
|
|
120
|
+
accounts: list[str] | None,
|
|
121
|
+
api_keys: list[str] | None,
|
|
122
|
+
model: str | None = None,
|
|
123
|
+
useragent: str | None = None,
|
|
124
|
+
) -> ReportCacheKey:
|
|
125
|
+
start, end, tz = resolve_report_range(start, end, timezone)
|
|
126
|
+
return ReportCacheKey(
|
|
127
|
+
start,
|
|
128
|
+
end,
|
|
129
|
+
str(tz),
|
|
130
|
+
tuple(sorted(set(accounts or []))),
|
|
131
|
+
tuple(sorted(set(api_keys or []))),
|
|
132
|
+
model or "",
|
|
133
|
+
useragent or "",
|
|
134
|
+
)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Small process-local report caches; one owned computation per cache at a time."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from collections import OrderedDict
|
|
7
|
+
from collections.abc import Awaitable, Callable, Hashable
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from datetime import date
|
|
10
|
+
from time import monotonic
|
|
11
|
+
|
|
12
|
+
from app.modules.reports.schemas import ReportsOptionsResponse, ReportsResponse, ThreadIdentityResponse
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class ReportCacheKey:
|
|
17
|
+
start: date
|
|
18
|
+
end: date
|
|
19
|
+
timezone: str
|
|
20
|
+
accounts: tuple[str, ...]
|
|
21
|
+
api_keys: tuple[str, ...]
|
|
22
|
+
model: str
|
|
23
|
+
useragent: str
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ReportCache[K: Hashable, T]:
|
|
27
|
+
def __init__(self, *, ttl_seconds: float = 60, max_entries: int = 64) -> None:
|
|
28
|
+
self._ttl = ttl_seconds
|
|
29
|
+
self._capacity = max_entries
|
|
30
|
+
self._entries: OrderedDict[K, tuple[float, T]] = OrderedDict()
|
|
31
|
+
self._compute_slot = asyncio.Semaphore(1)
|
|
32
|
+
|
|
33
|
+
def _cached(self, key: K) -> tuple[float, T] | None:
|
|
34
|
+
# All state access runs synchronously on the application's event loop.
|
|
35
|
+
# No await occurs while inspecting, pruning or publishing entries.
|
|
36
|
+
now = monotonic()
|
|
37
|
+
for expired in [key for key, (expires, _) in self._entries.items() if expires <= now]:
|
|
38
|
+
del self._entries[expired]
|
|
39
|
+
if key in self._entries:
|
|
40
|
+
self._entries.move_to_end(key)
|
|
41
|
+
return self._entries[key]
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
async def get(self, key: K, compute: Callable[[], Awaitable[T]]) -> T:
|
|
45
|
+
cached = self._cached(key)
|
|
46
|
+
if cached is not None:
|
|
47
|
+
return cached[1]
|
|
48
|
+
# Cache hits bypass the compute limit. Misses recheck after admission
|
|
49
|
+
# so identical requests reuse the completed result. The caller owns
|
|
50
|
+
# computation and its AsyncSession; cancellation releases the slot.
|
|
51
|
+
async with self._compute_slot:
|
|
52
|
+
cached = self._cached(key)
|
|
53
|
+
if cached is not None:
|
|
54
|
+
return cached[1]
|
|
55
|
+
value = await compute()
|
|
56
|
+
self._entries[key] = (monotonic() + self._ttl, value)
|
|
57
|
+
while len(self._entries) > self._capacity:
|
|
58
|
+
self._entries.popitem(last=False)
|
|
59
|
+
return value
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# Thread identity scans raw request logs instead of the hourly rollup, so a
|
|
63
|
+
# miss is seconds rather than milliseconds. The figures move slowly (they are
|
|
64
|
+
# whole-window aggregates), so they tolerate a longer TTL than the cost report.
|
|
65
|
+
_THREAD_IDENTITY_TTL_SECONDS = 300.0
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ReportsCaches:
|
|
69
|
+
def __init__(self) -> None:
|
|
70
|
+
self.reports = ReportCache[ReportCacheKey, ReportsResponse]()
|
|
71
|
+
self.options = ReportCache[ReportCacheKey, ReportsOptionsResponse]()
|
|
72
|
+
self.thread_identity = ReportCache[ReportCacheKey, ThreadIdentityResponse](
|
|
73
|
+
ttl_seconds=_THREAD_IDENTITY_TTL_SECONDS
|
|
74
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from sqlalchemy import and_, or_
|
|
4
|
+
|
|
5
|
+
from app.db.models import RequestLog
|
|
6
|
+
|
|
7
|
+
_INTERNAL_LIMIT_WARMUP_SOURCE = "limit_warmup"
|
|
8
|
+
_INTERNAL_WARMUP_REQUEST_KINDS = ("warmup", "limit_warmup")
|
|
9
|
+
MISSING_USERAGENT_GROUP = "Missing User-Agent"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _useragent_group_filter_clause(useragent_group: str | None):
|
|
13
|
+
if not useragent_group:
|
|
14
|
+
return None
|
|
15
|
+
if useragent_group == MISSING_USERAGENT_GROUP:
|
|
16
|
+
return RequestLog.useragent_group.is_(None)
|
|
17
|
+
return RequestLog.useragent_group == useragent_group
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _normal_traffic_clause():
|
|
21
|
+
return and_(
|
|
22
|
+
or_(RequestLog.source.is_(None), RequestLog.source != _INTERNAL_LIMIT_WARMUP_SOURCE),
|
|
23
|
+
or_(
|
|
24
|
+
RequestLog.request_kind.is_(None),
|
|
25
|
+
RequestLog.request_kind.not_in(_INTERNAL_WARMUP_REQUEST_KINDS),
|
|
26
|
+
),
|
|
27
|
+
)
|