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,713 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import contextlib
|
|
5
|
+
import logging
|
|
6
|
+
import time
|
|
7
|
+
from collections.abc import Collection
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from typing import Any, AsyncIterator, Protocol, cast
|
|
11
|
+
|
|
12
|
+
from app.core.balancer.logic import RATE_LIMITED_MIN_COOLDOWN_SECONDS
|
|
13
|
+
from app.core.plan_types import normalize_account_plan_type
|
|
14
|
+
from app.core.resilience.toggles import resolve_resilience_toggles
|
|
15
|
+
from app.core.scheduling.leader_election_handle import get_leader_election as _get_leader_election
|
|
16
|
+
from app.core.usage import capacity_for_plan, default_window_minutes
|
|
17
|
+
from app.core.usage.refresh_policy import USAGE_REFRESH_INTERVAL_SECONDS
|
|
18
|
+
from app.core.utils.time import naive_utc_to_epoch
|
|
19
|
+
from app.db.models import Account, AccountLimitWarmup, AccountStatus, UsageHistory
|
|
20
|
+
from app.db.session import detach_session_objects, get_background_session
|
|
21
|
+
from app.modules.accounts.background_repository import BackgroundAccountsRepository
|
|
22
|
+
from app.modules.accounts.repository import AccountsRepository
|
|
23
|
+
from app.modules.limit_warmup.repository import LimitWarmupRepository
|
|
24
|
+
from app.modules.limit_warmup.service import (
|
|
25
|
+
LimitWarmupService,
|
|
26
|
+
StreamingLimitWarmupSender,
|
|
27
|
+
usage_reset_confirmed,
|
|
28
|
+
)
|
|
29
|
+
from app.modules.proxy.account_cache import get_account_selection_cache
|
|
30
|
+
from app.modules.proxy.load_balancer import background_recovery_state_from_account, effective_routing_tunables
|
|
31
|
+
from app.modules.proxy.rate_limit_cache import get_rate_limit_headers_cache
|
|
32
|
+
from app.modules.request_logs.repository import RequestLogsRepository
|
|
33
|
+
from app.modules.settings.repository import SettingsRepository
|
|
34
|
+
from app.modules.usage import updater as usage_updater_module
|
|
35
|
+
from app.modules.usage.repository import UsageRepository
|
|
36
|
+
from app.modules.usage.updater import build_background_usage_updater
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
|
|
40
|
+
_RECOVERABLE_ACCOUNT_STATUSES = frozenset({AccountStatus.RATE_LIMITED, AccountStatus.QUOTA_EXCEEDED})
|
|
41
|
+
_BLOCK_RESET_MATCH_TOLERANCE_SECONDS = 5
|
|
42
|
+
# Quota-window slots a persisted rate-limit deadline can be anchored to.
|
|
43
|
+
# Upstream reports the paid short window and the paid 7d window through the
|
|
44
|
+
# primary/secondary slots and the free 30d window through the monthly slot, so
|
|
45
|
+
# the anchor search covers all three instead of assuming one plan's shape.
|
|
46
|
+
_RESET_EVIDENCE_WINDOWS: tuple[str, ...] = ("primary", "secondary", "monthly")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _normalized_usage_window(entry: UsageHistory) -> str:
|
|
50
|
+
"""Return the slot an entry belongs to, matching the repository's filter.
|
|
51
|
+
|
|
52
|
+
Primary rows may persist a ``NULL`` window; ``UsageRepository`` normalizes
|
|
53
|
+
those to ``"primary"`` when filtering, so window comparisons here must use
|
|
54
|
+
the same normalization or a legacy primary row would never match its slot.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
return entry.window or "primary"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True, slots=True)
|
|
61
|
+
class _ResolvedResetEvidence:
|
|
62
|
+
"""The two independent uses of a reset transition, kept apart on purpose.
|
|
63
|
+
|
|
64
|
+
``warmup`` feeds reset-confirmed warm-up and only ever holds monthly-slot
|
|
65
|
+
pairs. ``recovery`` holds transitions that passed the anchoring and
|
|
66
|
+
uniqueness checks and may therefore override a persisted cooldown. Sharing
|
|
67
|
+
one map let unvalidated warm-up evidence reach the recovery path.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
warmup: "dict[str, _ResetEvidence]"
|
|
71
|
+
recovery: "dict[str, _ResetEvidence]"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True, slots=True)
|
|
75
|
+
class _ResetEvidence:
|
|
76
|
+
baseline: UsageHistory
|
|
77
|
+
before: UsageHistory
|
|
78
|
+
after: UsageHistory
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def window(self) -> str:
|
|
82
|
+
return _normalized_usage_window(self.baseline)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class _RecoverableAccountsRepository(Protocol):
|
|
86
|
+
async def update_status_if_current(
|
|
87
|
+
self,
|
|
88
|
+
account_id: str,
|
|
89
|
+
status: AccountStatus,
|
|
90
|
+
deactivation_reason: str | None = None,
|
|
91
|
+
reset_at: int | None = None,
|
|
92
|
+
blocked_at: int | None | object = None,
|
|
93
|
+
*,
|
|
94
|
+
expected_status: AccountStatus,
|
|
95
|
+
expected_deactivation_reason: str | None = None,
|
|
96
|
+
expected_reset_at: int | None = None,
|
|
97
|
+
expected_blocked_at: int | None | object = None,
|
|
98
|
+
) -> bool: ...
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class _LatestUsageRepository(Protocol):
|
|
102
|
+
async def latest_by_account(
|
|
103
|
+
self,
|
|
104
|
+
window: str | None = None,
|
|
105
|
+
*,
|
|
106
|
+
account_ids: Collection[str] | None = None,
|
|
107
|
+
) -> dict[str, UsageHistory]: ...
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class _BackgroundLimitWarmupRepository:
|
|
111
|
+
async def latest_by_account(self, account_ids: list[str]) -> dict[str, AccountLimitWarmup]:
|
|
112
|
+
async with get_background_session() as session:
|
|
113
|
+
attempts = await LimitWarmupRepository(session).latest_by_account(account_ids)
|
|
114
|
+
detach_session_objects(session)
|
|
115
|
+
return attempts
|
|
116
|
+
|
|
117
|
+
async def try_create_attempt(
|
|
118
|
+
self,
|
|
119
|
+
*,
|
|
120
|
+
account_id: str,
|
|
121
|
+
window: str,
|
|
122
|
+
reset_at: int,
|
|
123
|
+
model: str,
|
|
124
|
+
attempted_at: datetime,
|
|
125
|
+
status: str = "pending",
|
|
126
|
+
reset_at_tolerance_seconds: int = 0,
|
|
127
|
+
require_no_prior_attempt: bool = False,
|
|
128
|
+
) -> AccountLimitWarmup | None:
|
|
129
|
+
async with get_background_session() as session:
|
|
130
|
+
attempt = await LimitWarmupRepository(session).try_create_attempt(
|
|
131
|
+
account_id=account_id,
|
|
132
|
+
window=window,
|
|
133
|
+
reset_at=reset_at,
|
|
134
|
+
model=model,
|
|
135
|
+
attempted_at=attempted_at,
|
|
136
|
+
status=status,
|
|
137
|
+
reset_at_tolerance_seconds=reset_at_tolerance_seconds,
|
|
138
|
+
require_no_prior_attempt=require_no_prior_attempt,
|
|
139
|
+
)
|
|
140
|
+
detach_session_objects(session)
|
|
141
|
+
return attempt
|
|
142
|
+
|
|
143
|
+
async def complete_attempt(
|
|
144
|
+
self,
|
|
145
|
+
attempt_id: int,
|
|
146
|
+
*,
|
|
147
|
+
status: str,
|
|
148
|
+
completed_at: datetime,
|
|
149
|
+
error_code: str | None = None,
|
|
150
|
+
error_message: str | None = None,
|
|
151
|
+
) -> AccountLimitWarmup | None:
|
|
152
|
+
async with get_background_session() as session:
|
|
153
|
+
attempt = await LimitWarmupRepository(session).complete_attempt(
|
|
154
|
+
attempt_id,
|
|
155
|
+
status=status,
|
|
156
|
+
completed_at=completed_at,
|
|
157
|
+
error_code=error_code,
|
|
158
|
+
error_message=error_message,
|
|
159
|
+
)
|
|
160
|
+
detach_session_objects(session)
|
|
161
|
+
return attempt
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class _BackgroundRequestLogsRepository:
|
|
165
|
+
async def add_log(self, *args: Any, **kwargs: Any) -> object:
|
|
166
|
+
async with get_background_session() as session:
|
|
167
|
+
return await RequestLogsRepository(session).add_log(*args, **kwargs)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@dataclass(slots=True)
|
|
171
|
+
class UsageRefreshScheduler:
|
|
172
|
+
interval_seconds: int
|
|
173
|
+
enabled: bool
|
|
174
|
+
_next_account_index: int = 0
|
|
175
|
+
_task: asyncio.Task[None] | None = None
|
|
176
|
+
_stop: asyncio.Event = field(default_factory=asyncio.Event)
|
|
177
|
+
_lock: asyncio.Lock = field(default_factory=asyncio.Lock)
|
|
178
|
+
|
|
179
|
+
async def start(self) -> None:
|
|
180
|
+
if not self.enabled:
|
|
181
|
+
return
|
|
182
|
+
if self._task and not self._task.done():
|
|
183
|
+
return
|
|
184
|
+
self._stop.clear()
|
|
185
|
+
self._task = asyncio.create_task(self._run_loop())
|
|
186
|
+
|
|
187
|
+
async def stop(self) -> None:
|
|
188
|
+
self._stop.set()
|
|
189
|
+
if self._task is not None:
|
|
190
|
+
self._task.cancel()
|
|
191
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
192
|
+
await self._task
|
|
193
|
+
self._task = None
|
|
194
|
+
await usage_updater_module._USAGE_REFRESH_SINGLEFLIGHT.cancel_all()
|
|
195
|
+
|
|
196
|
+
async def _run_loop(self) -> None:
|
|
197
|
+
while not self._stop.is_set():
|
|
198
|
+
started_at = time.monotonic()
|
|
199
|
+
delay = await self._refresh_once()
|
|
200
|
+
remaining_delay = max(0.0, delay - (time.monotonic() - started_at))
|
|
201
|
+
if remaining_delay <= 0:
|
|
202
|
+
continue
|
|
203
|
+
try:
|
|
204
|
+
await asyncio.wait_for(self._stop.wait(), timeout=remaining_delay)
|
|
205
|
+
except asyncio.TimeoutError:
|
|
206
|
+
continue
|
|
207
|
+
|
|
208
|
+
async def _refresh_once(self) -> float:
|
|
209
|
+
delay = await _get_leader_election().run_if_leader(self._refresh_as_leader)
|
|
210
|
+
if delay is None:
|
|
211
|
+
return float(self.interval_seconds)
|
|
212
|
+
return delay
|
|
213
|
+
|
|
214
|
+
async def _refresh_as_leader(self) -> float:
|
|
215
|
+
async with self._lock:
|
|
216
|
+
account_count = 0
|
|
217
|
+
try:
|
|
218
|
+
async with get_background_session() as session:
|
|
219
|
+
usage_repo = UsageRepository(session)
|
|
220
|
+
accounts_repo = AccountsRepository(session)
|
|
221
|
+
accounts = _ordered_usage_refresh_accounts(await accounts_repo.list_accounts())
|
|
222
|
+
selected_account, cycle_complete = self._select_next_account(accounts)
|
|
223
|
+
if selected_account is not None:
|
|
224
|
+
selected_account_ids = [selected_account.id]
|
|
225
|
+
previous_plan_types = {
|
|
226
|
+
selected_account.id: normalize_account_plan_type(selected_account.plan_type)
|
|
227
|
+
}
|
|
228
|
+
before_primary = await usage_repo.latest_by_account(
|
|
229
|
+
window="primary",
|
|
230
|
+
account_ids=selected_account_ids,
|
|
231
|
+
)
|
|
232
|
+
before_secondary = await usage_repo.latest_by_account(
|
|
233
|
+
window="secondary",
|
|
234
|
+
account_ids=selected_account_ids,
|
|
235
|
+
)
|
|
236
|
+
before_monthly = await usage_repo.latest_by_account(
|
|
237
|
+
window="monthly",
|
|
238
|
+
account_ids=selected_account_ids,
|
|
239
|
+
)
|
|
240
|
+
detach_session_objects(session)
|
|
241
|
+
account_count = len(accounts)
|
|
242
|
+
if selected_account is None:
|
|
243
|
+
await _invalidate_usage_refresh_caches()
|
|
244
|
+
return float(self.interval_seconds)
|
|
245
|
+
|
|
246
|
+
updater = build_background_usage_updater()
|
|
247
|
+
refresh_started_at = usage_updater_module.utcnow()
|
|
248
|
+
usage_written = await updater.refresh_accounts([selected_account], before_primary)
|
|
249
|
+
if usage_written:
|
|
250
|
+
async with get_background_session() as session:
|
|
251
|
+
usage_repo = UsageRepository(session)
|
|
252
|
+
accounts_repo = AccountsRepository(session)
|
|
253
|
+
settings_repo = SettingsRepository(session)
|
|
254
|
+
after_primary = await usage_repo.latest_by_account(
|
|
255
|
+
window="primary",
|
|
256
|
+
account_ids=selected_account_ids,
|
|
257
|
+
)
|
|
258
|
+
after_secondary = await usage_repo.latest_by_account(
|
|
259
|
+
window="secondary",
|
|
260
|
+
account_ids=selected_account_ids,
|
|
261
|
+
)
|
|
262
|
+
after_monthly = await usage_repo.latest_by_account(
|
|
263
|
+
window="monthly",
|
|
264
|
+
account_ids=selected_account_ids,
|
|
265
|
+
)
|
|
266
|
+
dashboard_settings = await settings_repo.get_or_create()
|
|
267
|
+
refreshed_accounts = await accounts_repo.list_accounts(refresh_existing=True)
|
|
268
|
+
refreshed_selected_accounts = [
|
|
269
|
+
account for account in refreshed_accounts if account.id == selected_account.id
|
|
270
|
+
]
|
|
271
|
+
resolved = await _resolve_reset_evidence(
|
|
272
|
+
accounts=refreshed_selected_accounts,
|
|
273
|
+
usage_repo=usage_repo,
|
|
274
|
+
before_monthly=before_monthly,
|
|
275
|
+
after_monthly=after_monthly,
|
|
276
|
+
)
|
|
277
|
+
detach_session_objects(session)
|
|
278
|
+
warmup_before_monthly = dict(before_monthly)
|
|
279
|
+
warmup_after_monthly = dict(after_monthly)
|
|
280
|
+
for account_id, account_evidence in resolved.warmup.items():
|
|
281
|
+
warmup_before_monthly[account_id] = account_evidence.before
|
|
282
|
+
warmup_after_monthly[account_id] = account_evidence.after
|
|
283
|
+
async with get_background_session() as session:
|
|
284
|
+
await reconcile_recoverable_account_statuses(
|
|
285
|
+
accounts_repo=AccountsRepository(session),
|
|
286
|
+
usage_repo=UsageRepository(session),
|
|
287
|
+
accounts=refreshed_selected_accounts,
|
|
288
|
+
reset_evidence=resolved.recovery,
|
|
289
|
+
dashboard_settings=dashboard_settings,
|
|
290
|
+
)
|
|
291
|
+
warmup_service = LimitWarmupService(
|
|
292
|
+
cast(Any, _BackgroundLimitWarmupRepository()),
|
|
293
|
+
cast(Any, _BackgroundRequestLogsRepository()),
|
|
294
|
+
sender=StreamingLimitWarmupSender(
|
|
295
|
+
cast(AccountsRepository, BackgroundAccountsRepository()),
|
|
296
|
+
accounts_repo_factory=_background_accounts_repo,
|
|
297
|
+
),
|
|
298
|
+
)
|
|
299
|
+
await warmup_service.run_after_usage_refresh(
|
|
300
|
+
accounts=refreshed_selected_accounts,
|
|
301
|
+
stagger_accounts=refreshed_accounts,
|
|
302
|
+
settings=dashboard_settings,
|
|
303
|
+
before_primary=before_primary,
|
|
304
|
+
before_secondary=_select_long_window_entries(
|
|
305
|
+
accounts=refreshed_selected_accounts,
|
|
306
|
+
monthly_entries=warmup_before_monthly,
|
|
307
|
+
secondary_entries=before_secondary,
|
|
308
|
+
),
|
|
309
|
+
after_primary=after_primary,
|
|
310
|
+
after_secondary=_select_long_window_entries(
|
|
311
|
+
accounts=refreshed_selected_accounts,
|
|
312
|
+
monthly_entries=warmup_after_monthly,
|
|
313
|
+
secondary_entries=after_secondary,
|
|
314
|
+
),
|
|
315
|
+
previous_plan_types=previous_plan_types,
|
|
316
|
+
refresh_started_at=refresh_started_at,
|
|
317
|
+
usage_refresh_interval_seconds=self.interval_seconds,
|
|
318
|
+
)
|
|
319
|
+
if cycle_complete:
|
|
320
|
+
await _invalidate_usage_refresh_caches()
|
|
321
|
+
except Exception:
|
|
322
|
+
logger.exception("Usage refresh loop failed")
|
|
323
|
+
return float(self.interval_seconds)
|
|
324
|
+
return _usage_refresh_slice_seconds(self.interval_seconds, account_count)
|
|
325
|
+
|
|
326
|
+
def _select_next_account(self, accounts: list[Account]) -> tuple[Account | None, bool]:
|
|
327
|
+
if not accounts:
|
|
328
|
+
self._next_account_index = 0
|
|
329
|
+
return None, True
|
|
330
|
+
index = self._next_account_index % len(accounts)
|
|
331
|
+
next_index = (index + 1) % len(accounts)
|
|
332
|
+
self._next_account_index = next_index
|
|
333
|
+
return accounts[index], next_index == 0
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def build_usage_refresh_scheduler() -> UsageRefreshScheduler:
|
|
337
|
+
return UsageRefreshScheduler(interval_seconds=USAGE_REFRESH_INTERVAL_SECONDS, enabled=True)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _ordered_usage_refresh_accounts(accounts: list[Account]) -> list[Account]:
|
|
341
|
+
return sorted(
|
|
342
|
+
(
|
|
343
|
+
account
|
|
344
|
+
for account in accounts
|
|
345
|
+
if account.status not in (AccountStatus.PAUSED, AccountStatus.REAUTH_REQUIRED, AccountStatus.DEACTIVATED)
|
|
346
|
+
),
|
|
347
|
+
key=lambda account: account.id,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _usage_refresh_slice_seconds(interval_seconds: int, account_count: int) -> float:
|
|
352
|
+
if account_count <= 0:
|
|
353
|
+
return float(interval_seconds)
|
|
354
|
+
return float(interval_seconds) / account_count
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
async def _invalidate_usage_refresh_caches() -> None:
|
|
358
|
+
await get_rate_limit_headers_cache().invalidate()
|
|
359
|
+
get_account_selection_cache().invalidate()
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
@contextlib.asynccontextmanager
|
|
363
|
+
async def _background_accounts_repo() -> AsyncIterator[AccountsRepository]:
|
|
364
|
+
async with get_background_session() as session:
|
|
365
|
+
try:
|
|
366
|
+
yield AccountsRepository(session)
|
|
367
|
+
finally:
|
|
368
|
+
detach_session_objects(session)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
async def reconcile_recoverable_account_statuses(
|
|
372
|
+
*,
|
|
373
|
+
accounts_repo: _RecoverableAccountsRepository,
|
|
374
|
+
usage_repo: _LatestUsageRepository,
|
|
375
|
+
accounts: list[Account],
|
|
376
|
+
reset_evidence: dict[str, _ResetEvidence] | None = None,
|
|
377
|
+
dashboard_settings: object | None = None,
|
|
378
|
+
) -> int:
|
|
379
|
+
"""Repair recoverable account statuses from the latest usage evidence.
|
|
380
|
+
|
|
381
|
+
``dashboard_settings`` is the dashboard-settings row the refresh cycle
|
|
382
|
+
already read; the state builds below resolve soft drain and the routing
|
|
383
|
+
tunables from it once, so the health tier they compute follows the
|
|
384
|
+
dashboard toggle exactly like a request-path state build (``None`` = the
|
|
385
|
+
environment layer, for callers without a row).
|
|
386
|
+
"""
|
|
387
|
+
candidates = [account for account in accounts if account.status in _RECOVERABLE_ACCOUNT_STATUSES]
|
|
388
|
+
if not candidates:
|
|
389
|
+
return 0
|
|
390
|
+
routing_tunables = effective_routing_tunables(dashboard_settings)
|
|
391
|
+
soft_drain_enabled = resolve_resilience_toggles(dashboard_settings).soft_drain_enabled
|
|
392
|
+
|
|
393
|
+
candidate_ids = [account.id for account in candidates]
|
|
394
|
+
latest_primary = await usage_repo.latest_by_account(window="primary", account_ids=candidate_ids)
|
|
395
|
+
latest_secondary = await usage_repo.latest_by_account(window="secondary", account_ids=candidate_ids)
|
|
396
|
+
latest_monthly = await usage_repo.latest_by_account(window="monthly", account_ids=candidate_ids)
|
|
397
|
+
|
|
398
|
+
recovered = 0
|
|
399
|
+
for account in candidates:
|
|
400
|
+
monthly_entry = latest_monthly.get(account.id)
|
|
401
|
+
latest_by_window: dict[str, UsageHistory | None] = {
|
|
402
|
+
"primary": latest_primary.get(account.id),
|
|
403
|
+
"secondary": latest_secondary.get(account.id),
|
|
404
|
+
"monthly": monthly_entry,
|
|
405
|
+
}
|
|
406
|
+
if _confirmed_window_reset_recovery(
|
|
407
|
+
account=account,
|
|
408
|
+
evidence=(reset_evidence or {}).get(account.id),
|
|
409
|
+
latest_by_window=latest_by_window,
|
|
410
|
+
):
|
|
411
|
+
status = AccountStatus.ACTIVE
|
|
412
|
+
reset_at = None
|
|
413
|
+
blocked_at = None
|
|
414
|
+
else:
|
|
415
|
+
state = background_recovery_state_from_account(
|
|
416
|
+
account=account,
|
|
417
|
+
primary_entry=latest_primary.get(account.id),
|
|
418
|
+
secondary_entry=_select_long_window_entry(
|
|
419
|
+
account=account,
|
|
420
|
+
monthly_entry=monthly_entry,
|
|
421
|
+
secondary_entry=latest_secondary.get(account.id),
|
|
422
|
+
),
|
|
423
|
+
routing_tunables=routing_tunables,
|
|
424
|
+
soft_drain_enabled=soft_drain_enabled,
|
|
425
|
+
)
|
|
426
|
+
if state.status != AccountStatus.ACTIVE:
|
|
427
|
+
continue
|
|
428
|
+
status = state.status
|
|
429
|
+
reset_at = int(state.reset_at) if state.reset_at else None
|
|
430
|
+
blocked_at = int(state.blocked_at) if state.blocked_at else None
|
|
431
|
+
deactivation_reason = None
|
|
432
|
+
if (
|
|
433
|
+
status == account.status
|
|
434
|
+
and deactivation_reason == account.deactivation_reason
|
|
435
|
+
and reset_at == account.reset_at
|
|
436
|
+
and blocked_at == account.blocked_at
|
|
437
|
+
):
|
|
438
|
+
continue
|
|
439
|
+
updated = await accounts_repo.update_status_if_current(
|
|
440
|
+
account.id,
|
|
441
|
+
status,
|
|
442
|
+
deactivation_reason,
|
|
443
|
+
reset_at,
|
|
444
|
+
blocked_at=blocked_at,
|
|
445
|
+
expected_status=account.status,
|
|
446
|
+
expected_deactivation_reason=account.deactivation_reason,
|
|
447
|
+
expected_reset_at=account.reset_at,
|
|
448
|
+
expected_blocked_at=account.blocked_at,
|
|
449
|
+
)
|
|
450
|
+
if not updated:
|
|
451
|
+
continue
|
|
452
|
+
account.status = status
|
|
453
|
+
account.deactivation_reason = deactivation_reason
|
|
454
|
+
account.reset_at = reset_at
|
|
455
|
+
account.blocked_at = blocked_at
|
|
456
|
+
recovered += 1
|
|
457
|
+
return recovered
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _is_long_window_minutes(window_minutes: int | None) -> bool:
|
|
461
|
+
"""Return whether a duration is one of the recognized long quota windows."""
|
|
462
|
+
|
|
463
|
+
if window_minutes is None:
|
|
464
|
+
return False
|
|
465
|
+
return any(window_minutes == default_window_minutes(window) for window in ("secondary", "monthly"))
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def _short_window_blocks_recovery(entry: UsageHistory | None, *, account: Account, now: float) -> bool:
|
|
469
|
+
"""Return whether the account's short window would immediately re-block it.
|
|
470
|
+
|
|
471
|
+
This is the risk the reset-confirmed exception was originally scoped away
|
|
472
|
+
from by restricting it to Free accounts: a paid account whose short window
|
|
473
|
+
is exhausted must not be released by a reset in a long window. Anchoring
|
|
474
|
+
already prevents an unrelated window's reset from matching this block's
|
|
475
|
+
deadline; this keeps the account blocked when its *own* short window is
|
|
476
|
+
still spent, so recovery cannot hand back an account that would spend one
|
|
477
|
+
request earning a fresh 429.
|
|
478
|
+
|
|
479
|
+
Deliberately only the short window. The long window is what a confirmed
|
|
480
|
+
reset and credit-backed quota act on, and reasoning about it here would
|
|
481
|
+
duplicate ``apply_usage_quota`` and the weekly-shape normalization that own
|
|
482
|
+
that question.
|
|
483
|
+
|
|
484
|
+
Three exclusions:
|
|
485
|
+
|
|
486
|
+
* A primary-slot row is not the short window when it reports a *recognized
|
|
487
|
+
long* window's duration. Upstream also delivers a weekly quota through
|
|
488
|
+
the primary slot (``should_use_weekly_primary``), and that row is a long
|
|
489
|
+
window wearing the short window's slot. Only recognized long durations
|
|
490
|
+
are excluded: an unfamiliar duration is treated as the short window, so
|
|
491
|
+
an upstream change to the short window's length keeps the account
|
|
492
|
+
blocked rather than silently disabling this guard.
|
|
493
|
+
* A slot known to carry zero capacity for the plan is not a window. The Free
|
|
494
|
+
primary row is a normalization artifact of the monthly-only payload, not a
|
|
495
|
+
live 5h window, and mirrors the monthly percentage. An *unknown* capacity
|
|
496
|
+
(an unrecognized stored plan, which ``coerce_account_plan_type``
|
|
497
|
+
preserves) is not evidence that a reported window does not exist, so it
|
|
498
|
+
does not exclude the slot.
|
|
499
|
+
* An elapsed window is stale exhaustion evidence rather than a live block
|
|
500
|
+
(see "Usage refresh does not trust elapsed reset windows"), which also
|
|
501
|
+
covers upstream having stopped reporting the short window. A 100% row with
|
|
502
|
+
no reset metadata is treated as current because nothing proves it rolled.
|
|
503
|
+
"""
|
|
504
|
+
|
|
505
|
+
if entry is None or entry.used_percent < 100.0:
|
|
506
|
+
return False
|
|
507
|
+
if _is_long_window_minutes(entry.window_minutes):
|
|
508
|
+
return False
|
|
509
|
+
capacity = capacity_for_plan(account.plan_type, "primary")
|
|
510
|
+
if capacity is not None and capacity <= 0:
|
|
511
|
+
return False
|
|
512
|
+
return entry.reset_at is None or entry.reset_at > now
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def _confirmed_window_reset_recovery(
|
|
516
|
+
*,
|
|
517
|
+
account: Account,
|
|
518
|
+
evidence: _ResetEvidence | None,
|
|
519
|
+
latest_by_window: dict[str, UsageHistory | None],
|
|
520
|
+
) -> bool:
|
|
521
|
+
"""Return whether usage history proves the blocked quota window already reset.
|
|
522
|
+
|
|
523
|
+
The persisted ``reset_at`` is the cross-replica authority for a 429, so it
|
|
524
|
+
is only overridden when history identifies *the very window that deadline
|
|
525
|
+
came from* and shows it rolling. The baseline match on ``reset_at`` is what
|
|
526
|
+
binds the evidence to this block: a deadline derived from a generic
|
|
527
|
+
Retry-After hint or a model-scoped throttle matches no quota window's reset
|
|
528
|
+
metadata, and a reset in an unrelated window does not match this block's
|
|
529
|
+
deadline. That anchoring -- not the account's plan -- is what keeps a paid
|
|
530
|
+
account with an exhausted short window from being released by unrelated
|
|
531
|
+
long-window availability.
|
|
532
|
+
"""
|
|
533
|
+
|
|
534
|
+
if account.status != AccountStatus.RATE_LIMITED:
|
|
535
|
+
return False
|
|
536
|
+
if account.reset_at is None or account.blocked_at is None:
|
|
537
|
+
return False
|
|
538
|
+
now = time.time()
|
|
539
|
+
if now >= account.reset_at:
|
|
540
|
+
return False
|
|
541
|
+
if now < account.blocked_at + RATE_LIMITED_MIN_COOLDOWN_SECONDS:
|
|
542
|
+
return False
|
|
543
|
+
if evidence is None:
|
|
544
|
+
return False
|
|
545
|
+
baseline = evidence.baseline
|
|
546
|
+
before = evidence.before
|
|
547
|
+
after = evidence.after
|
|
548
|
+
window = evidence.window
|
|
549
|
+
if _normalized_usage_window(before) != window or _normalized_usage_window(after) != window:
|
|
550
|
+
return False
|
|
551
|
+
latest = latest_by_window.get(window)
|
|
552
|
+
if latest is None or _normalized_usage_window(latest) != window:
|
|
553
|
+
return False
|
|
554
|
+
if baseline.reset_at is None:
|
|
555
|
+
return False
|
|
556
|
+
if abs(baseline.reset_at - account.reset_at) > _BLOCK_RESET_MATCH_TOLERANCE_SECONDS:
|
|
557
|
+
return False
|
|
558
|
+
if naive_utc_to_epoch(baseline.recorded_at) <= account.blocked_at:
|
|
559
|
+
return False
|
|
560
|
+
if not usage_reset_confirmed(before=before, after=after):
|
|
561
|
+
return False
|
|
562
|
+
if after.used_percent >= 100.0 or latest.used_percent >= 100.0:
|
|
563
|
+
return False
|
|
564
|
+
if window != "primary" and _short_window_blocks_recovery(latest_by_window.get("primary"), account=account, now=now):
|
|
565
|
+
return False
|
|
566
|
+
return (
|
|
567
|
+
naive_utc_to_epoch(after.recorded_at) > account.blocked_at
|
|
568
|
+
and naive_utc_to_epoch(latest.recorded_at) > account.blocked_at
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
async def _resolve_reset_evidence(
|
|
573
|
+
*,
|
|
574
|
+
accounts: list[Account],
|
|
575
|
+
usage_repo: UsageRepository,
|
|
576
|
+
before_monthly: dict[str, UsageHistory],
|
|
577
|
+
after_monthly: dict[str, UsageHistory],
|
|
578
|
+
) -> _ResolvedResetEvidence:
|
|
579
|
+
"""Resolve reset transitions for warm-up and, separately, for recovery.
|
|
580
|
+
|
|
581
|
+
The in-cycle monthly pair feeds reset-confirmed warm-up for every account
|
|
582
|
+
and is unchanged. For a blocked account the persisted lookup additionally
|
|
583
|
+
searches each quota-window slot for a post-block transition anchored to the
|
|
584
|
+
account's own ``reset_at``, so recovery works after a restart and for
|
|
585
|
+
whichever window upstream actually blocked -- the paid 5h/7d primary and
|
|
586
|
+
secondary slots as well as the free monthly slot.
|
|
587
|
+
|
|
588
|
+
Only that anchored, unique transition is eligible to override a cooldown.
|
|
589
|
+
Warm-up evidence is never promoted into the recovery map, because it carries
|
|
590
|
+
no proof about which window produced the block.
|
|
591
|
+
"""
|
|
592
|
+
|
|
593
|
+
warmup: dict[str, _ResetEvidence] = {}
|
|
594
|
+
recovery: dict[str, _ResetEvidence] = {}
|
|
595
|
+
for account in accounts:
|
|
596
|
+
before = before_monthly.get(account.id)
|
|
597
|
+
after = after_monthly.get(account.id)
|
|
598
|
+
if usage_reset_confirmed(before=before, after=after):
|
|
599
|
+
assert before is not None and after is not None
|
|
600
|
+
warmup[account.id] = _ResetEvidence(baseline=before, before=before, after=after)
|
|
601
|
+
if account.status != AccountStatus.RATE_LIMITED or account.reset_at is None or account.blocked_at is None:
|
|
602
|
+
continue
|
|
603
|
+
since = datetime.fromtimestamp(account.blocked_at, timezone.utc).replace(tzinfo=None)
|
|
604
|
+
matching_slots = 0
|
|
605
|
+
anchored: _ResetEvidence | None = None
|
|
606
|
+
for window in _RESET_EVIDENCE_WINDOWS:
|
|
607
|
+
history = [
|
|
608
|
+
entry
|
|
609
|
+
for entry in await usage_repo.history_since(account.id, window, since)
|
|
610
|
+
if entry.recorded_at > since
|
|
611
|
+
]
|
|
612
|
+
baseline = _matching_baseline(
|
|
613
|
+
history,
|
|
614
|
+
expected_reset_at=account.reset_at,
|
|
615
|
+
reset_at_tolerance_seconds=_BLOCK_RESET_MATCH_TOLERANCE_SECONDS,
|
|
616
|
+
)
|
|
617
|
+
if baseline is None:
|
|
618
|
+
continue
|
|
619
|
+
# Uniqueness is decided by the matching baseline, not by whether a
|
|
620
|
+
# transition followed it. A second slot carrying this deadline means
|
|
621
|
+
# the deadline does not identify the blocked window even when that
|
|
622
|
+
# slot has not reset -- and that unreset slot may be the exhausted
|
|
623
|
+
# one that produced the 429.
|
|
624
|
+
matching_slots += 1
|
|
625
|
+
transition = _latest_confirmed_reset_transition_from(history, baseline)
|
|
626
|
+
if transition is not None:
|
|
627
|
+
anchored = transition
|
|
628
|
+
if matching_slots == 1 and anchored is not None:
|
|
629
|
+
recovery[account.id] = anchored
|
|
630
|
+
if anchored.window == "monthly":
|
|
631
|
+
warmup[account.id] = anchored
|
|
632
|
+
return _ResolvedResetEvidence(warmup=warmup, recovery=recovery)
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def _matching_baseline(
|
|
636
|
+
history: list[UsageHistory],
|
|
637
|
+
*,
|
|
638
|
+
expected_reset_at: int,
|
|
639
|
+
reset_at_tolerance_seconds: int,
|
|
640
|
+
) -> tuple[int, UsageHistory] | None:
|
|
641
|
+
"""Return the earliest row whose reset deadline matches the persisted marker."""
|
|
642
|
+
|
|
643
|
+
return next(
|
|
644
|
+
(
|
|
645
|
+
(index, entry)
|
|
646
|
+
for index, entry in enumerate(history)
|
|
647
|
+
if entry.reset_at is not None and abs(entry.reset_at - expected_reset_at) <= reset_at_tolerance_seconds
|
|
648
|
+
),
|
|
649
|
+
None,
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def _latest_confirmed_reset_transition_after_baseline(
|
|
654
|
+
history: list[UsageHistory],
|
|
655
|
+
*,
|
|
656
|
+
expected_reset_at: int,
|
|
657
|
+
reset_at_tolerance_seconds: int,
|
|
658
|
+
) -> _ResetEvidence | None:
|
|
659
|
+
baseline = _matching_baseline(
|
|
660
|
+
history,
|
|
661
|
+
expected_reset_at=expected_reset_at,
|
|
662
|
+
reset_at_tolerance_seconds=reset_at_tolerance_seconds,
|
|
663
|
+
)
|
|
664
|
+
if baseline is None:
|
|
665
|
+
return None
|
|
666
|
+
return _latest_confirmed_reset_transition_from(history, baseline)
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def _latest_confirmed_reset_transition_from(
|
|
670
|
+
history: list[UsageHistory],
|
|
671
|
+
baseline: tuple[int, UsageHistory],
|
|
672
|
+
) -> _ResetEvidence | None:
|
|
673
|
+
baseline_index, baseline_entry = baseline
|
|
674
|
+
latest_transition: _ResetEvidence | None = None
|
|
675
|
+
for index in range(baseline_index, len(history) - 1):
|
|
676
|
+
before = history[index]
|
|
677
|
+
after = history[index + 1]
|
|
678
|
+
if usage_reset_confirmed(before=before, after=after):
|
|
679
|
+
latest_transition = _ResetEvidence(
|
|
680
|
+
baseline=baseline_entry,
|
|
681
|
+
before=before,
|
|
682
|
+
after=after,
|
|
683
|
+
)
|
|
684
|
+
return latest_transition
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def _select_long_window_entry(
|
|
688
|
+
*,
|
|
689
|
+
account: Account,
|
|
690
|
+
monthly_entry: UsageHistory | None,
|
|
691
|
+
secondary_entry: UsageHistory | None,
|
|
692
|
+
) -> UsageHistory | None:
|
|
693
|
+
if monthly_entry is not None and capacity_for_plan(account.plan_type, "monthly") is not None:
|
|
694
|
+
return monthly_entry
|
|
695
|
+
return secondary_entry
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def _select_long_window_entries(
|
|
699
|
+
*,
|
|
700
|
+
accounts: list[Account],
|
|
701
|
+
monthly_entries: dict[str, UsageHistory],
|
|
702
|
+
secondary_entries: dict[str, UsageHistory],
|
|
703
|
+
) -> dict[str, UsageHistory]:
|
|
704
|
+
selected: dict[str, UsageHistory] = {}
|
|
705
|
+
for account in accounts:
|
|
706
|
+
entry = _select_long_window_entry(
|
|
707
|
+
account=account,
|
|
708
|
+
monthly_entry=monthly_entries.get(account.id),
|
|
709
|
+
secondary_entry=secondary_entries.get(account.id),
|
|
710
|
+
)
|
|
711
|
+
if entry is not None:
|
|
712
|
+
selected[account.id] = entry
|
|
713
|
+
return selected
|