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,778 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from datetime import timedelta
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from app.core.clients.proxy import ProxyResponseError
|
|
10
|
+
from app.core.clock import scheduler_for
|
|
11
|
+
from app.core.config.dashboard_overrides import effective_settings
|
|
12
|
+
from app.core.errors import openai_error
|
|
13
|
+
from app.core.metrics.prometheus import (
|
|
14
|
+
PROMETHEUS_AVAILABLE,
|
|
15
|
+
bridge_durable_recover_total,
|
|
16
|
+
bridge_instance_mismatch_total,
|
|
17
|
+
http_bridge_operation_abandonment_total,
|
|
18
|
+
)
|
|
19
|
+
from app.core.utils.time import utcnow
|
|
20
|
+
from app.db.models import StickySessionKind
|
|
21
|
+
from app.modules.proxy._service.http_bridge import helpers as _http_bridge_helpers
|
|
22
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
23
|
+
_await_task_deferring_cancellation,
|
|
24
|
+
_forget_http_bridge_denied_anchor_fence_owner,
|
|
25
|
+
_http_bridge_allow_durable_takeover,
|
|
26
|
+
_http_bridge_durable_lease_ttl_seconds,
|
|
27
|
+
_http_bridge_live_previous_response_alias_owner,
|
|
28
|
+
_http_bridge_live_turn_state_alias_owner,
|
|
29
|
+
_http_bridge_owner_lookup_unavailable_error_envelope,
|
|
30
|
+
_http_bridge_previous_response_alias_key,
|
|
31
|
+
_http_bridge_request_budget_seconds,
|
|
32
|
+
_http_bridge_turn_state_alias_key,
|
|
33
|
+
_is_missing_durable_bridge_table_error,
|
|
34
|
+
_log_http_bridge_event,
|
|
35
|
+
_persist_http_bridge_previous_response_alias,
|
|
36
|
+
_persist_http_bridge_turn_state_alias,
|
|
37
|
+
_reconcile_durable_http_bridge_ownership,
|
|
38
|
+
_record_bridge_reattach,
|
|
39
|
+
_register_http_bridge_turn_state_aliases_locked,
|
|
40
|
+
_renew_durable_http_bridge_lease,
|
|
41
|
+
_service_get_settings_cache,
|
|
42
|
+
_track_alias_registration,
|
|
43
|
+
)
|
|
44
|
+
from app.modules.proxy._service.http_bridge.protocol import _HTTPBridgeServiceProtocol
|
|
45
|
+
from app.modules.proxy._service.http_bridge.service_stubs import (
|
|
46
|
+
_headers_with_turn_state,
|
|
47
|
+
_service_get_settings,
|
|
48
|
+
)
|
|
49
|
+
from app.modules.proxy._service.support import _HTTPBridgeSession, _HTTPBridgeSessionKey
|
|
50
|
+
from app.modules.proxy.affinity import _AffinityPolicy, _extract_model_class
|
|
51
|
+
from app.modules.proxy.continuity import (
|
|
52
|
+
HTTP_BRIDGE_ACCOUNT_NEUTRAL_REPLAY_REBINDABLE_KINDS,
|
|
53
|
+
is_http_bridge_account_neutral_replay,
|
|
54
|
+
without_http_bridge_session_affinity_headers,
|
|
55
|
+
)
|
|
56
|
+
from app.modules.proxy.durable_bridge_coordinator import DurableBridgeLookup
|
|
57
|
+
from app.modules.proxy.durable_bridge_repository import (
|
|
58
|
+
DurableBridgeAliasRegistration,
|
|
59
|
+
DurableBridgeAliasRegistrationReceipt,
|
|
60
|
+
)
|
|
61
|
+
from app.modules.proxy.durable_bridge_runtime import http_bridge_owner_process_epoch
|
|
62
|
+
|
|
63
|
+
logger = logging.getLogger("app.modules.proxy.service")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _recovery_can_rebind_live_alias(session: _HTTPBridgeSession) -> bool:
|
|
67
|
+
return session.key.affinity_kind in HTTP_BRIDGE_ACCOUNT_NEUTRAL_REPLAY_REBINDABLE_KINDS or (
|
|
68
|
+
is_http_bridge_account_neutral_replay(
|
|
69
|
+
kind=session.key.affinity_kind,
|
|
70
|
+
key=session.key.affinity_key,
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _requires_durable_recovery_alias_serialization(session: _HTTPBridgeSession) -> bool:
|
|
76
|
+
return (
|
|
77
|
+
session.durable_session_id is not None
|
|
78
|
+
and session.durable_owner_epoch is not None
|
|
79
|
+
and is_http_bridge_account_neutral_replay(
|
|
80
|
+
kind=session.key.affinity_kind,
|
|
81
|
+
key=session.key.affinity_key,
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class _HTTPBridgeSessionRegistryMixin:
|
|
87
|
+
async def prune_idle_http_bridge_sessions(self: Any) -> int:
|
|
88
|
+
"""Run the idle sweep off the request path (issue #1354).
|
|
89
|
+
|
|
90
|
+
The sweep is otherwise reached only from
|
|
91
|
+
``_get_or_create_http_bridge_session``, so a replica that stops taking
|
|
92
|
+
bridge requests keeps idle sessions' upstream WebSockets open until
|
|
93
|
+
restart. Heartbeat-driven, so it runs without traffic or leadership.
|
|
94
|
+
"""
|
|
95
|
+
async with self._http_bridge_lock:
|
|
96
|
+
pruned_sessions = self._prune_http_bridge_sessions_locked()
|
|
97
|
+
if not pruned_sessions:
|
|
98
|
+
return 0
|
|
99
|
+
self._schedule_http_bridge_session_closes(pruned_sessions, reason="idle_sweep")
|
|
100
|
+
return len(pruned_sessions)
|
|
101
|
+
|
|
102
|
+
async def abandon_stale_http_bridge_operations(self: Any) -> int:
|
|
103
|
+
"""Fence ownerless ambiguous operations after the bridge budget.
|
|
104
|
+
|
|
105
|
+
The in-memory registry and event batcher are both part of the safety
|
|
106
|
+
proof. A durable row is only eligible when no canonical or detached
|
|
107
|
+
generation, including terminal event settlement, still references it.
|
|
108
|
+
"""
|
|
109
|
+
# M1 stream/bridge budgets: this pass runs from the ring heartbeat, outside
|
|
110
|
+
# any request binding, so the dashboard-managed bridge budget is applied
|
|
111
|
+
# from one snapshot read here (``effective_settings``), never the bare
|
|
112
|
+
# environment value. A snapshot failure keeps the environment budget.
|
|
113
|
+
settings = _service_get_settings()
|
|
114
|
+
try:
|
|
115
|
+
settings = effective_settings(await _service_get_settings_cache().get(), settings)
|
|
116
|
+
except Exception:
|
|
117
|
+
logger.warning(
|
|
118
|
+
"HTTP bridge stale operation abandonment could not read the dashboard settings snapshot; "
|
|
119
|
+
"using the environment budget",
|
|
120
|
+
exc_info=True,
|
|
121
|
+
)
|
|
122
|
+
inactivity_seconds = max(30.0 * 60.0, _http_bridge_request_budget_seconds(settings))
|
|
123
|
+
maintenance_now = utcnow()
|
|
124
|
+
cutoff = maintenance_now - timedelta(seconds=inactivity_seconds)
|
|
125
|
+
lease_expired_before = maintenance_now - timedelta(seconds=_http_bridge_durable_lease_ttl_seconds())
|
|
126
|
+
protected_operation_ids: set[str] = set()
|
|
127
|
+
async with self._http_bridge_lock:
|
|
128
|
+
local_sessions = [
|
|
129
|
+
*self._http_bridge_sessions.values(),
|
|
130
|
+
*self._http_bridge_detached_sessions.values(),
|
|
131
|
+
]
|
|
132
|
+
for session in local_sessions:
|
|
133
|
+
protected_operation_ids.update(
|
|
134
|
+
operation_id
|
|
135
|
+
for request_state in tuple(session.pending_requests)
|
|
136
|
+
if (operation_id := getattr(request_state, "operation_id", None))
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
batcher = getattr(self, "_http_bridge_operation_event_batcher", None)
|
|
140
|
+
pending_operation_ids = getattr(batcher, "pending_operation_ids", None)
|
|
141
|
+
if callable(pending_operation_ids):
|
|
142
|
+
try:
|
|
143
|
+
protected_operation_ids.update(await pending_operation_ids())
|
|
144
|
+
except Exception:
|
|
145
|
+
logger.warning(
|
|
146
|
+
"Failed to snapshot HTTP bridge operation spool protection",
|
|
147
|
+
exc_info=True,
|
|
148
|
+
)
|
|
149
|
+
return 0
|
|
150
|
+
|
|
151
|
+
abandon_stale_operations = getattr(self._durable_bridge, "abandon_stale_operations", None)
|
|
152
|
+
if not callable(abandon_stale_operations):
|
|
153
|
+
return 0
|
|
154
|
+
try:
|
|
155
|
+
abandonments = await abandon_stale_operations(
|
|
156
|
+
cutoff=cutoff,
|
|
157
|
+
lease_expired_before=lease_expired_before,
|
|
158
|
+
protected_operation_ids=protected_operation_ids,
|
|
159
|
+
)
|
|
160
|
+
except Exception:
|
|
161
|
+
logger.warning("HTTP bridge stale operation abandonment failed", exc_info=True)
|
|
162
|
+
return 0
|
|
163
|
+
for abandonment in abandonments:
|
|
164
|
+
source_state = str(abandonment.source_state)
|
|
165
|
+
if PROMETHEUS_AVAILABLE and http_bridge_operation_abandonment_total is not None:
|
|
166
|
+
http_bridge_operation_abandonment_total.labels(source_state=source_state).inc()
|
|
167
|
+
logger.warning(
|
|
168
|
+
"Abandoned stale HTTP bridge operation",
|
|
169
|
+
extra={
|
|
170
|
+
"source_state": source_state,
|
|
171
|
+
"reason": "stale_owner",
|
|
172
|
+
"age_seconds": round(float(abandonment.age_seconds), 3),
|
|
173
|
+
"owner_lease_outcome": abandonment.owner_lease_outcome,
|
|
174
|
+
"session_hash": abandonment.session_hash,
|
|
175
|
+
},
|
|
176
|
+
)
|
|
177
|
+
return len(abandonments)
|
|
178
|
+
|
|
179
|
+
def _initialize_http_bridge_session_registry(self: _HTTPBridgeServiceProtocol) -> None:
|
|
180
|
+
# Canonical and detached registries both own live generations until
|
|
181
|
+
# common resource finalization removes the latter entry.
|
|
182
|
+
self._http_bridge_sessions = {}
|
|
183
|
+
self._http_bridge_detached_sessions = {}
|
|
184
|
+
|
|
185
|
+
async def close_all_http_bridge_sessions(self: _HTTPBridgeServiceProtocol) -> bool:
|
|
186
|
+
async with self._http_bridge_lock:
|
|
187
|
+
sessions_to_close, inflight_futures = self._take_all_http_bridge_sessions_locked()
|
|
188
|
+
shutdown_error = ProxyResponseError(
|
|
189
|
+
503,
|
|
190
|
+
openai_error(
|
|
191
|
+
"upstream_unavailable",
|
|
192
|
+
"HTTP responses session bridge is shutting down",
|
|
193
|
+
error_type="server_error",
|
|
194
|
+
),
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
async def finish_shutdown() -> bool:
|
|
198
|
+
for inflight_future in inflight_futures:
|
|
199
|
+
if inflight_future.done():
|
|
200
|
+
continue
|
|
201
|
+
inflight_future.set_exception(shutdown_error)
|
|
202
|
+
inflight_future.exception()
|
|
203
|
+
# The registry snapshot is no longer discoverable after the lock is
|
|
204
|
+
# released. Start every close concurrently, then await every result,
|
|
205
|
+
# so cancellation cannot strand the tail of a sequential close loop.
|
|
206
|
+
close_results = await asyncio.gather(
|
|
207
|
+
*(self._close_http_bridge_session(session) for session in sessions_to_close),
|
|
208
|
+
return_exceptions=True,
|
|
209
|
+
)
|
|
210
|
+
background_cleanup_drained = await self._drain_http_bridge_background_cleanup_tasks(reason="shutdown")
|
|
211
|
+
# Session/background cleanup may still enqueue durable operation
|
|
212
|
+
# events, so the spooler must outlive both. Close it before
|
|
213
|
+
# propagating an individual session-close failure: the batcher's
|
|
214
|
+
# flusher is a service-owned task and must not leak merely because
|
|
215
|
+
# one detached generation remains registered for a later retry.
|
|
216
|
+
event_batcher = getattr(self, "_http_bridge_operation_event_batcher", None)
|
|
217
|
+
close_batcher = getattr(event_batcher, "close", None)
|
|
218
|
+
if callable(close_batcher):
|
|
219
|
+
await close_batcher()
|
|
220
|
+
for result in close_results:
|
|
221
|
+
if isinstance(result, BaseException):
|
|
222
|
+
raise result
|
|
223
|
+
return background_cleanup_drained
|
|
224
|
+
|
|
225
|
+
shutdown_task = scheduler_for(self).create_task(finish_shutdown(), name="http-bridge-shutdown-close-all")
|
|
226
|
+
result, cancellation = await _await_task_deferring_cancellation(shutdown_task)
|
|
227
|
+
if cancellation is not None:
|
|
228
|
+
raise cancellation
|
|
229
|
+
return result
|
|
230
|
+
|
|
231
|
+
async def _register_http_bridge_turn_state(
|
|
232
|
+
self: _HTTPBridgeServiceProtocol,
|
|
233
|
+
session: _HTTPBridgeSession,
|
|
234
|
+
turn_state: str,
|
|
235
|
+
) -> bool:
|
|
236
|
+
if _requires_durable_recovery_alias_serialization(session):
|
|
237
|
+
async with session.recovery_alias_lock:
|
|
238
|
+
return await self._register_http_bridge_turn_state_impl(session, turn_state)
|
|
239
|
+
return await self._register_http_bridge_turn_state_impl(session, turn_state)
|
|
240
|
+
|
|
241
|
+
async def _register_http_bridge_turn_state_impl(
|
|
242
|
+
self: _HTTPBridgeServiceProtocol,
|
|
243
|
+
session: _HTTPBridgeSession,
|
|
244
|
+
turn_state: str,
|
|
245
|
+
) -> bool:
|
|
246
|
+
registered, _receipt = await self._register_http_bridge_turn_state_core(
|
|
247
|
+
session,
|
|
248
|
+
turn_state,
|
|
249
|
+
reversible=False,
|
|
250
|
+
)
|
|
251
|
+
return registered
|
|
252
|
+
|
|
253
|
+
async def _register_http_bridge_recovery_turn_state_locked(
|
|
254
|
+
self: _HTTPBridgeServiceProtocol,
|
|
255
|
+
session: _HTTPBridgeSession,
|
|
256
|
+
turn_state: str,
|
|
257
|
+
) -> tuple[bool, DurableBridgeAliasRegistrationReceipt | None]:
|
|
258
|
+
if not _requires_durable_recovery_alias_serialization(session):
|
|
259
|
+
return False, None
|
|
260
|
+
return await self._register_http_bridge_turn_state_core(
|
|
261
|
+
session,
|
|
262
|
+
turn_state,
|
|
263
|
+
reversible=True,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
async def _register_http_bridge_turn_state_core(
|
|
267
|
+
self: _HTTPBridgeServiceProtocol,
|
|
268
|
+
session: _HTTPBridgeSession,
|
|
269
|
+
turn_state: str,
|
|
270
|
+
*,
|
|
271
|
+
reversible: bool,
|
|
272
|
+
) -> tuple[bool, DurableBridgeAliasRegistrationReceipt | None]:
|
|
273
|
+
defer_durable_publication = False
|
|
274
|
+
deferred_live_alias_owner: _HTTPBridgeSession | None = None
|
|
275
|
+
async with self._http_bridge_lock:
|
|
276
|
+
if session.closed or (
|
|
277
|
+
session.upstream_control.retire_after_drain
|
|
278
|
+
and self._http_bridge_sessions.get(session.key) is not session
|
|
279
|
+
):
|
|
280
|
+
# A detached predecessor may finish its admitted response, but
|
|
281
|
+
# publishing continuity aliases under its reused key would make
|
|
282
|
+
# them resolve to the replacement generation (and account).
|
|
283
|
+
return False, None
|
|
284
|
+
account_neutral_recovery = is_http_bridge_account_neutral_replay(
|
|
285
|
+
kind=session.key.affinity_kind,
|
|
286
|
+
key=session.key.affinity_key,
|
|
287
|
+
)
|
|
288
|
+
if account_neutral_recovery and (session.durable_session_id is None or session.durable_owner_epoch is None):
|
|
289
|
+
return False, None
|
|
290
|
+
defer_durable_publication = (
|
|
291
|
+
account_neutral_recovery
|
|
292
|
+
and session.durable_session_id is not None
|
|
293
|
+
and session.durable_owner_epoch is not None
|
|
294
|
+
)
|
|
295
|
+
live_alias_owner = _http_bridge_live_turn_state_alias_owner(self, session, turn_state)
|
|
296
|
+
if live_alias_owner is not None:
|
|
297
|
+
can_rebind_recovery_alias = account_neutral_recovery and _recovery_can_rebind_live_alias(
|
|
298
|
+
live_alias_owner
|
|
299
|
+
)
|
|
300
|
+
if not can_rebind_recovery_alias:
|
|
301
|
+
return not account_neutral_recovery, None
|
|
302
|
+
if defer_durable_publication:
|
|
303
|
+
deferred_live_alias_owner = live_alias_owner
|
|
304
|
+
else:
|
|
305
|
+
live_alias_owner.downstream_turn_state_aliases.discard(turn_state)
|
|
306
|
+
live_alias_owner.turn_state_alias_registration_generations.pop(turn_state, None)
|
|
307
|
+
if live_alias_owner.downstream_turn_state == turn_state:
|
|
308
|
+
live_alias_owner.downstream_turn_state = None
|
|
309
|
+
if account_neutral_recovery:
|
|
310
|
+
session.codex_session = True
|
|
311
|
+
session.idle_ttl_seconds = max(
|
|
312
|
+
session.idle_ttl_seconds,
|
|
313
|
+
float(_http_bridge_helpers.HTTP_BRIDGE_CODEX_IDLE_TTL_SECONDS),
|
|
314
|
+
)
|
|
315
|
+
session.headers = without_http_bridge_session_affinity_headers(session.headers)
|
|
316
|
+
registration_generation = _track_alias_registration(session, turn_state, turn_state=True)
|
|
317
|
+
if not defer_durable_publication:
|
|
318
|
+
session.downstream_turn_state_aliases.add(turn_state)
|
|
319
|
+
if session.downstream_turn_state is None:
|
|
320
|
+
session.downstream_turn_state = turn_state
|
|
321
|
+
if live_alias_owner is not None:
|
|
322
|
+
alias_key = _http_bridge_turn_state_alias_key(turn_state, session.key.api_key_id)
|
|
323
|
+
self._http_bridge_turn_state_index[alias_key] = session.key
|
|
324
|
+
_register_http_bridge_turn_state_aliases_locked(self, session)
|
|
325
|
+
if session.durable_session_id is None or session.durable_owner_epoch is None:
|
|
326
|
+
return True, None
|
|
327
|
+
durable_result, receipt = await _persist_http_bridge_turn_state_alias(
|
|
328
|
+
self,
|
|
329
|
+
session,
|
|
330
|
+
turn_state=turn_state,
|
|
331
|
+
registration_generation=registration_generation,
|
|
332
|
+
instance_id=_service_get_settings().http_responses_session_bridge_instance_id,
|
|
333
|
+
lease_ttl_seconds=_http_bridge_durable_lease_ttl_seconds(),
|
|
334
|
+
local_alias_was_published=not defer_durable_publication,
|
|
335
|
+
reversible=reversible,
|
|
336
|
+
)
|
|
337
|
+
if not defer_durable_publication:
|
|
338
|
+
return True, receipt
|
|
339
|
+
if durable_result != DurableBridgeAliasRegistration.REGISTERED:
|
|
340
|
+
return False, receipt
|
|
341
|
+
async with self._http_bridge_lock:
|
|
342
|
+
if (
|
|
343
|
+
session.closed
|
|
344
|
+
or self._http_bridge_sessions.get(session.key) is not session
|
|
345
|
+
or session.turn_state_alias_registration_generations.get(turn_state) != registration_generation
|
|
346
|
+
):
|
|
347
|
+
if session.turn_state_alias_registration_generations.get(turn_state) == registration_generation:
|
|
348
|
+
session.turn_state_alias_registration_generations.pop(turn_state, None)
|
|
349
|
+
return False, receipt
|
|
350
|
+
current_live_owner = _http_bridge_live_turn_state_alias_owner(self, session, turn_state)
|
|
351
|
+
if (
|
|
352
|
+
current_live_owner is not None
|
|
353
|
+
and current_live_owner is not deferred_live_alias_owner
|
|
354
|
+
and not _recovery_can_rebind_live_alias(current_live_owner)
|
|
355
|
+
):
|
|
356
|
+
session.turn_state_alias_registration_generations.pop(turn_state, None)
|
|
357
|
+
return False, receipt
|
|
358
|
+
if current_live_owner is not None:
|
|
359
|
+
current_live_owner.downstream_turn_state_aliases.discard(turn_state)
|
|
360
|
+
current_live_owner.turn_state_alias_registration_generations.pop(turn_state, None)
|
|
361
|
+
if current_live_owner.downstream_turn_state == turn_state:
|
|
362
|
+
current_live_owner.downstream_turn_state = None
|
|
363
|
+
session.downstream_turn_state_aliases.add(turn_state)
|
|
364
|
+
if session.downstream_turn_state is None:
|
|
365
|
+
session.downstream_turn_state = turn_state
|
|
366
|
+
alias_key = _http_bridge_turn_state_alias_key(turn_state, session.key.api_key_id)
|
|
367
|
+
self._http_bridge_turn_state_index[alias_key] = session.key
|
|
368
|
+
_register_http_bridge_turn_state_aliases_locked(self, session)
|
|
369
|
+
return True, receipt
|
|
370
|
+
|
|
371
|
+
async def _register_http_bridge_previous_response_id(
|
|
372
|
+
self: _HTTPBridgeServiceProtocol,
|
|
373
|
+
session: _HTTPBridgeSession,
|
|
374
|
+
response_id: str,
|
|
375
|
+
*,
|
|
376
|
+
input_item_count: int | None = None,
|
|
377
|
+
input_full_fingerprint: str | None = None,
|
|
378
|
+
pending_tool_calls: Mapping[str, str] | None = None,
|
|
379
|
+
) -> bool:
|
|
380
|
+
if _requires_durable_recovery_alias_serialization(session):
|
|
381
|
+
async with session.recovery_alias_lock:
|
|
382
|
+
return await self._register_http_bridge_previous_response_id_impl(
|
|
383
|
+
session,
|
|
384
|
+
response_id,
|
|
385
|
+
input_item_count=input_item_count,
|
|
386
|
+
input_full_fingerprint=input_full_fingerprint,
|
|
387
|
+
pending_tool_calls=pending_tool_calls,
|
|
388
|
+
)
|
|
389
|
+
return await self._register_http_bridge_previous_response_id_impl(
|
|
390
|
+
session,
|
|
391
|
+
response_id,
|
|
392
|
+
input_item_count=input_item_count,
|
|
393
|
+
input_full_fingerprint=input_full_fingerprint,
|
|
394
|
+
pending_tool_calls=pending_tool_calls,
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
async def _register_http_bridge_previous_response_id_impl(
|
|
398
|
+
self: _HTTPBridgeServiceProtocol,
|
|
399
|
+
session: _HTTPBridgeSession,
|
|
400
|
+
response_id: str,
|
|
401
|
+
*,
|
|
402
|
+
input_item_count: int | None = None,
|
|
403
|
+
input_full_fingerprint: str | None = None,
|
|
404
|
+
pending_tool_calls: Mapping[str, str] | None = None,
|
|
405
|
+
) -> bool:
|
|
406
|
+
stripped_response_id = response_id.strip()
|
|
407
|
+
if not stripped_response_id:
|
|
408
|
+
return False
|
|
409
|
+
defer_durable_publication = False
|
|
410
|
+
deferred_live_alias_owner: _HTTPBridgeSession | None = None
|
|
411
|
+
async with self._http_bridge_lock:
|
|
412
|
+
if session.closed:
|
|
413
|
+
return False
|
|
414
|
+
if (
|
|
415
|
+
session.upstream_control.retire_after_drain
|
|
416
|
+
and self._http_bridge_sessions.get(session.key) is not session
|
|
417
|
+
):
|
|
418
|
+
return False
|
|
419
|
+
alias_key = _http_bridge_previous_response_alias_key(stripped_response_id, session.key.api_key_id)
|
|
420
|
+
account_neutral_recovery = is_http_bridge_account_neutral_replay(
|
|
421
|
+
kind=session.key.affinity_kind,
|
|
422
|
+
key=session.key.affinity_key,
|
|
423
|
+
)
|
|
424
|
+
if account_neutral_recovery and (session.durable_session_id is None or session.durable_owner_epoch is None):
|
|
425
|
+
return False
|
|
426
|
+
defer_durable_publication = (
|
|
427
|
+
account_neutral_recovery
|
|
428
|
+
and session.durable_session_id is not None
|
|
429
|
+
and session.durable_owner_epoch is not None
|
|
430
|
+
)
|
|
431
|
+
live_alias_owner = _http_bridge_live_previous_response_alias_owner(
|
|
432
|
+
self,
|
|
433
|
+
session,
|
|
434
|
+
stripped_response_id,
|
|
435
|
+
)
|
|
436
|
+
if live_alias_owner is not None:
|
|
437
|
+
can_rebind_recovery_alias = account_neutral_recovery and _recovery_can_rebind_live_alias(
|
|
438
|
+
live_alias_owner
|
|
439
|
+
)
|
|
440
|
+
if not can_rebind_recovery_alias:
|
|
441
|
+
return not account_neutral_recovery
|
|
442
|
+
if defer_durable_publication:
|
|
443
|
+
deferred_live_alias_owner = live_alias_owner
|
|
444
|
+
else:
|
|
445
|
+
live_alias_owner.previous_response_ids.discard(stripped_response_id)
|
|
446
|
+
live_alias_owner.previous_response_alias_registration_generations.pop(stripped_response_id, None)
|
|
447
|
+
registration_generation = _track_alias_registration(session, stripped_response_id, turn_state=False)
|
|
448
|
+
if not defer_durable_publication:
|
|
449
|
+
self._http_bridge_previous_response_index[alias_key] = session.key
|
|
450
|
+
session.previous_response_ids.add(stripped_response_id)
|
|
451
|
+
if session.durable_session_id is None or session.durable_owner_epoch is None:
|
|
452
|
+
return True
|
|
453
|
+
durable_result = await _persist_http_bridge_previous_response_alias(
|
|
454
|
+
self,
|
|
455
|
+
session,
|
|
456
|
+
response_id=stripped_response_id,
|
|
457
|
+
registration_generation=registration_generation,
|
|
458
|
+
input_item_count=input_item_count,
|
|
459
|
+
input_full_fingerprint=input_full_fingerprint,
|
|
460
|
+
pending_tool_calls=pending_tool_calls,
|
|
461
|
+
instance_id=_service_get_settings().http_responses_session_bridge_instance_id,
|
|
462
|
+
lease_ttl_seconds=_http_bridge_durable_lease_ttl_seconds(),
|
|
463
|
+
local_alias_was_published=not defer_durable_publication,
|
|
464
|
+
)
|
|
465
|
+
if not defer_durable_publication:
|
|
466
|
+
# The in-memory alias stands either way, but the caller's
|
|
467
|
+
# quarantine clear and supersession are gated on the durable
|
|
468
|
+
# anchor actually advancing: a swallowed durable failure here
|
|
469
|
+
# left the old poisoned anchor stored for other replicas while
|
|
470
|
+
# this worker cleared its only protection.
|
|
471
|
+
return durable_result == DurableBridgeAliasRegistration.REGISTERED
|
|
472
|
+
if durable_result != DurableBridgeAliasRegistration.REGISTERED:
|
|
473
|
+
return False
|
|
474
|
+
async with self._http_bridge_lock:
|
|
475
|
+
if (
|
|
476
|
+
session.closed
|
|
477
|
+
or self._http_bridge_sessions.get(session.key) is not session
|
|
478
|
+
or session.previous_response_alias_registration_generations.get(stripped_response_id)
|
|
479
|
+
!= registration_generation
|
|
480
|
+
):
|
|
481
|
+
if (
|
|
482
|
+
session.previous_response_alias_registration_generations.get(stripped_response_id)
|
|
483
|
+
== registration_generation
|
|
484
|
+
):
|
|
485
|
+
session.previous_response_alias_registration_generations.pop(stripped_response_id, None)
|
|
486
|
+
return False
|
|
487
|
+
current_live_owner = _http_bridge_live_previous_response_alias_owner(
|
|
488
|
+
self,
|
|
489
|
+
session,
|
|
490
|
+
stripped_response_id,
|
|
491
|
+
)
|
|
492
|
+
if (
|
|
493
|
+
current_live_owner is not None
|
|
494
|
+
and current_live_owner is not deferred_live_alias_owner
|
|
495
|
+
and not _recovery_can_rebind_live_alias(current_live_owner)
|
|
496
|
+
):
|
|
497
|
+
session.previous_response_alias_registration_generations.pop(stripped_response_id, None)
|
|
498
|
+
return False
|
|
499
|
+
if current_live_owner is not None:
|
|
500
|
+
current_live_owner.previous_response_ids.discard(stripped_response_id)
|
|
501
|
+
current_live_owner.previous_response_alias_registration_generations.pop(
|
|
502
|
+
stripped_response_id,
|
|
503
|
+
None,
|
|
504
|
+
)
|
|
505
|
+
self._http_bridge_previous_response_index[alias_key] = session.key
|
|
506
|
+
session.previous_response_ids.add(stripped_response_id)
|
|
507
|
+
return True
|
|
508
|
+
|
|
509
|
+
async def _unregister_http_bridge_turn_states(
|
|
510
|
+
self: _HTTPBridgeServiceProtocol,
|
|
511
|
+
session: _HTTPBridgeSession,
|
|
512
|
+
) -> None:
|
|
513
|
+
async with self._http_bridge_lock:
|
|
514
|
+
self._unregister_http_bridge_turn_states_locked(session)
|
|
515
|
+
|
|
516
|
+
async def _unregister_http_bridge_previous_response_ids(
|
|
517
|
+
self: _HTTPBridgeServiceProtocol,
|
|
518
|
+
session: _HTTPBridgeSession,
|
|
519
|
+
) -> None:
|
|
520
|
+
async with self._http_bridge_lock:
|
|
521
|
+
self._unregister_http_bridge_previous_response_ids_locked(session)
|
|
522
|
+
|
|
523
|
+
async def _unregister_http_bridge_previous_response_id(
|
|
524
|
+
self: _HTTPBridgeServiceProtocol,
|
|
525
|
+
session: _HTTPBridgeSession,
|
|
526
|
+
response_id: str,
|
|
527
|
+
*,
|
|
528
|
+
expected_durable_session_id: str | None = None,
|
|
529
|
+
expected_durable_owner_epoch: int | None = None,
|
|
530
|
+
) -> bool:
|
|
531
|
+
async with self._http_bridge_lock:
|
|
532
|
+
if (
|
|
533
|
+
expected_durable_session_id is not None and session.durable_session_id != expected_durable_session_id
|
|
534
|
+
) or (
|
|
535
|
+
expected_durable_owner_epoch is not None and session.durable_owner_epoch != expected_durable_owner_epoch
|
|
536
|
+
):
|
|
537
|
+
return False
|
|
538
|
+
self._unregister_http_bridge_previous_response_id_locked(session, response_id)
|
|
539
|
+
return True
|
|
540
|
+
|
|
541
|
+
def _detach_http_bridge_session_locked(
|
|
542
|
+
self: _HTTPBridgeServiceProtocol,
|
|
543
|
+
key: _HTTPBridgeSessionKey,
|
|
544
|
+
*,
|
|
545
|
+
expected_session: _HTTPBridgeSession | None = None,
|
|
546
|
+
mark_closed: bool = True,
|
|
547
|
+
) -> _HTTPBridgeSession | None:
|
|
548
|
+
session = self._http_bridge_sessions.get(key)
|
|
549
|
+
if session is None or (expected_session is not None and session is not expected_session):
|
|
550
|
+
return None
|
|
551
|
+
self._http_bridge_sessions.pop(key, None)
|
|
552
|
+
if mark_closed:
|
|
553
|
+
session.closed = True
|
|
554
|
+
# Detachment removes only canonical routing. Even an idle generation
|
|
555
|
+
# marked closed may retain a slow-closing socket, reader, durable lease,
|
|
556
|
+
# or account lease, so lifecycle/capacity ownership lasts through the
|
|
557
|
+
# common resource-close finalizer for every detached generation.
|
|
558
|
+
self._http_bridge_detached_sessions[id(session)] = session
|
|
559
|
+
self._unregister_http_bridge_turn_states_locked(session)
|
|
560
|
+
self._unregister_http_bridge_previous_response_ids_locked(session)
|
|
561
|
+
return session
|
|
562
|
+
|
|
563
|
+
def _take_all_http_bridge_sessions_locked(
|
|
564
|
+
self: _HTTPBridgeServiceProtocol,
|
|
565
|
+
) -> tuple[list[_HTTPBridgeSession], list[asyncio.Future[_HTTPBridgeSession]]]:
|
|
566
|
+
sessions = [*self._http_bridge_sessions.values(), *self._http_bridge_detached_sessions.values()]
|
|
567
|
+
inflight_futures = list(self._http_bridge_inflight_sessions.values())
|
|
568
|
+
# Shutdown removes canonical routing immediately, but resource ownership
|
|
569
|
+
# remains discoverable until each close succeeds. A failed close can
|
|
570
|
+
# then be retried by a later shutdown pass instead of orphaning its
|
|
571
|
+
# socket, durable lease, account lease, or unsettled requests.
|
|
572
|
+
for session in self._http_bridge_sessions.values():
|
|
573
|
+
self._http_bridge_detached_sessions[id(session)] = session
|
|
574
|
+
self._http_bridge_sessions.clear()
|
|
575
|
+
self._http_bridge_inflight_sessions.clear()
|
|
576
|
+
self._http_bridge_previous_response_index.clear()
|
|
577
|
+
return sessions, inflight_futures
|
|
578
|
+
|
|
579
|
+
def _unregister_http_bridge_turn_states_locked(
|
|
580
|
+
self: _HTTPBridgeServiceProtocol,
|
|
581
|
+
session: _HTTPBridgeSession,
|
|
582
|
+
) -> None:
|
|
583
|
+
current_session = self._http_bridge_sessions.get(session.key)
|
|
584
|
+
for alias in tuple(session.downstream_turn_state_aliases):
|
|
585
|
+
alias_key = _http_bridge_turn_state_alias_key(alias, session.key.api_key_id)
|
|
586
|
+
if (
|
|
587
|
+
current_session is not None
|
|
588
|
+
and current_session is not session
|
|
589
|
+
and alias in current_session.downstream_turn_state_aliases
|
|
590
|
+
):
|
|
591
|
+
continue
|
|
592
|
+
if self._http_bridge_turn_state_index.get(alias_key) == session.key:
|
|
593
|
+
self._http_bridge_turn_state_index.pop(alias_key, None)
|
|
594
|
+
session.downstream_turn_state_aliases.clear()
|
|
595
|
+
session.turn_state_alias_registration_generations.clear()
|
|
596
|
+
|
|
597
|
+
def _unregister_http_bridge_previous_response_ids_locked(
|
|
598
|
+
self: _HTTPBridgeServiceProtocol,
|
|
599
|
+
session: _HTTPBridgeSession,
|
|
600
|
+
) -> None:
|
|
601
|
+
for response_id in tuple(session.previous_response_ids):
|
|
602
|
+
self._unregister_http_bridge_previous_response_id_locked(session, response_id)
|
|
603
|
+
session.previous_response_ids.clear()
|
|
604
|
+
session.previous_response_alias_registration_generations.clear()
|
|
605
|
+
|
|
606
|
+
def _unregister_http_bridge_previous_response_id_locked(
|
|
607
|
+
self: _HTTPBridgeServiceProtocol,
|
|
608
|
+
session: _HTTPBridgeSession,
|
|
609
|
+
response_id: str,
|
|
610
|
+
) -> None:
|
|
611
|
+
if response_id not in session.previous_response_ids:
|
|
612
|
+
return
|
|
613
|
+
alias_key = _http_bridge_previous_response_alias_key(response_id, session.key.api_key_id)
|
|
614
|
+
current_session = self._http_bridge_sessions.get(session.key)
|
|
615
|
+
if (
|
|
616
|
+
not (
|
|
617
|
+
current_session is not None
|
|
618
|
+
and current_session is not session
|
|
619
|
+
and response_id in current_session.previous_response_ids
|
|
620
|
+
)
|
|
621
|
+
and self._http_bridge_previous_response_index.get(alias_key) == session.key
|
|
622
|
+
):
|
|
623
|
+
self._http_bridge_previous_response_index.pop(alias_key, None)
|
|
624
|
+
session.previous_response_ids.discard(response_id)
|
|
625
|
+
session.previous_response_alias_registration_generations.pop(response_id, None)
|
|
626
|
+
|
|
627
|
+
def _promote_http_bridge_session_to_codex_affinity(
|
|
628
|
+
self: _HTTPBridgeServiceProtocol,
|
|
629
|
+
session: _HTTPBridgeSession,
|
|
630
|
+
*,
|
|
631
|
+
turn_state: str,
|
|
632
|
+
) -> None:
|
|
633
|
+
session.affinity = _AffinityPolicy(key=turn_state, kind=StickySessionKind.CODEX_SESSION)
|
|
634
|
+
session.codex_session = True
|
|
635
|
+
session.downstream_turn_state = turn_state
|
|
636
|
+
session.downstream_turn_state_aliases.add(turn_state)
|
|
637
|
+
session.idle_ttl_seconds = max(
|
|
638
|
+
session.idle_ttl_seconds,
|
|
639
|
+
float(_http_bridge_helpers.HTTP_BRIDGE_CODEX_IDLE_TTL_SECONDS),
|
|
640
|
+
)
|
|
641
|
+
session.headers = _headers_with_turn_state(session.headers, turn_state)
|
|
642
|
+
|
|
643
|
+
async def _claim_durable_http_bridge_session(
|
|
644
|
+
self: _HTTPBridgeServiceProtocol,
|
|
645
|
+
session: _HTTPBridgeSession,
|
|
646
|
+
*,
|
|
647
|
+
allow_takeover: bool,
|
|
648
|
+
force_owner_epoch_advance: bool = False,
|
|
649
|
+
claim_account_id: str | None = None,
|
|
650
|
+
clear_latest_turn_state: bool = False,
|
|
651
|
+
record_restart_takeover: bool = False,
|
|
652
|
+
) -> None:
|
|
653
|
+
current_instance = _service_get_settings().http_responses_session_bridge_instance_id
|
|
654
|
+
current_process_epoch = http_bridge_owner_process_epoch()
|
|
655
|
+
try:
|
|
656
|
+
lookup: DurableBridgeLookup | None = None
|
|
657
|
+
for claim_attempt in range(2):
|
|
658
|
+
lookup = await self._durable_bridge.claim_live_session(
|
|
659
|
+
session_key_kind=session.key.affinity_kind,
|
|
660
|
+
session_key_value=session.key.affinity_key,
|
|
661
|
+
api_key_id=session.key.api_key_id,
|
|
662
|
+
instance_id=current_instance,
|
|
663
|
+
owner_process_epoch=current_process_epoch,
|
|
664
|
+
lease_ttl_seconds=_http_bridge_durable_lease_ttl_seconds(),
|
|
665
|
+
account_id=claim_account_id or session.account.id,
|
|
666
|
+
model=session.request_model,
|
|
667
|
+
service_tier=session.request_service_tier,
|
|
668
|
+
latest_turn_state=None if clear_latest_turn_state else session.downstream_turn_state,
|
|
669
|
+
latest_response_id=None,
|
|
670
|
+
allow_takeover=allow_takeover,
|
|
671
|
+
force_owner_epoch_advance=force_owner_epoch_advance or claim_attempt > 0,
|
|
672
|
+
)
|
|
673
|
+
if lookup.owner_instance_id == current_instance:
|
|
674
|
+
break
|
|
675
|
+
if lookup.owner_instance_id is None and claim_attempt == 0:
|
|
676
|
+
# The lookup used to decide ``allow_takeover`` can race a
|
|
677
|
+
# concurrent close/release. If the first claim lands after
|
|
678
|
+
# ownership has already been cleared, retry once with the
|
|
679
|
+
# forced epoch-advance path instead of treating an ownerless
|
|
680
|
+
# row as a foreign replica.
|
|
681
|
+
await asyncio.sleep(0)
|
|
682
|
+
continue
|
|
683
|
+
if not allow_takeover or claim_attempt > 0:
|
|
684
|
+
break
|
|
685
|
+
if not _http_bridge_allow_durable_takeover(lookup):
|
|
686
|
+
# The claim reported a live foreign owner: we lost the race
|
|
687
|
+
# rather than hitting transient contention. The repository
|
|
688
|
+
# already dropped its takeover permission for that reason,
|
|
689
|
+
# and retrying here with a fresh call would restore it and
|
|
690
|
+
# steal the winner's live lease (issue #1695).
|
|
691
|
+
break
|
|
692
|
+
await asyncio.sleep(0)
|
|
693
|
+
assert lookup is not None
|
|
694
|
+
if lookup.owner_instance_id != current_instance:
|
|
695
|
+
_log_http_bridge_event(
|
|
696
|
+
"owner_mismatch_retry",
|
|
697
|
+
session.key,
|
|
698
|
+
account_id=None,
|
|
699
|
+
model=session.request_model,
|
|
700
|
+
detail=(
|
|
701
|
+
f"expected_instance={lookup.owner_instance_id}, "
|
|
702
|
+
f"current_instance={current_instance}, outcome=claim_rejected"
|
|
703
|
+
),
|
|
704
|
+
cache_key_family=session.key.affinity_kind,
|
|
705
|
+
model_class=_extract_model_class(session.request_model) if session.request_model else None,
|
|
706
|
+
owner_check_applied=True,
|
|
707
|
+
)
|
|
708
|
+
if PROMETHEUS_AVAILABLE and bridge_instance_mismatch_total is not None:
|
|
709
|
+
bridge_instance_mismatch_total.labels(outcome="retry").inc()
|
|
710
|
+
raise ProxyResponseError(
|
|
711
|
+
409,
|
|
712
|
+
openai_error(
|
|
713
|
+
"bridge_instance_mismatch",
|
|
714
|
+
"HTTP bridge session is owned by a different instance; retry to reach the correct replica",
|
|
715
|
+
error_type="server_error",
|
|
716
|
+
),
|
|
717
|
+
)
|
|
718
|
+
async with session.lifecycle_lock:
|
|
719
|
+
previous_durable_session_id = session.durable_session_id
|
|
720
|
+
previous_durable_owner_epoch = session.durable_owner_epoch
|
|
721
|
+
next_owner_changed = (
|
|
722
|
+
previous_durable_session_id != lookup.session_id
|
|
723
|
+
or previous_durable_owner_epoch != lookup.owner_epoch
|
|
724
|
+
)
|
|
725
|
+
if next_owner_changed:
|
|
726
|
+
previous_owner_key = (
|
|
727
|
+
previous_durable_session_id
|
|
728
|
+
if previous_durable_session_id is not None
|
|
729
|
+
else f"local:{id(session)}"
|
|
730
|
+
)
|
|
731
|
+
_forget_http_bridge_denied_anchor_fence_owner(
|
|
732
|
+
self,
|
|
733
|
+
previous_owner_key,
|
|
734
|
+
owner_epoch=previous_durable_owner_epoch,
|
|
735
|
+
)
|
|
736
|
+
session.durable_session_id = lookup.session_id
|
|
737
|
+
session.durable_owner_epoch = lookup.owner_epoch
|
|
738
|
+
session.headers = _headers_with_turn_state(session.headers, session.downstream_turn_state)
|
|
739
|
+
if (
|
|
740
|
+
PROMETHEUS_AVAILABLE
|
|
741
|
+
and bridge_durable_recover_total is not None
|
|
742
|
+
and record_restart_takeover
|
|
743
|
+
and lookup.owner_epoch > 1
|
|
744
|
+
):
|
|
745
|
+
bridge_durable_recover_total.labels(path="restart_takeover").inc()
|
|
746
|
+
_record_bridge_reattach(path="restart_takeover", outcome="success")
|
|
747
|
+
if session.key.affinity_kind == "session_header":
|
|
748
|
+
await self._durable_bridge.register_session_header(
|
|
749
|
+
session_id=lookup.session_id,
|
|
750
|
+
api_key_id=session.key.api_key_id,
|
|
751
|
+
session_header=session.key.affinity_key,
|
|
752
|
+
)
|
|
753
|
+
except Exception as exc:
|
|
754
|
+
if _is_missing_durable_bridge_table_error(exc):
|
|
755
|
+
if is_http_bridge_account_neutral_replay(
|
|
756
|
+
kind=session.key.affinity_kind,
|
|
757
|
+
key=session.key.affinity_key,
|
|
758
|
+
):
|
|
759
|
+
raise ProxyResponseError(
|
|
760
|
+
502,
|
|
761
|
+
_http_bridge_owner_lookup_unavailable_error_envelope(),
|
|
762
|
+
) from exc
|
|
763
|
+
logger.warning("Durable bridge tables missing; using in-memory bridge session fallback", exc_info=True)
|
|
764
|
+
return
|
|
765
|
+
raise
|
|
766
|
+
|
|
767
|
+
async def _refresh_durable_http_bridge_session(
|
|
768
|
+
self: _HTTPBridgeServiceProtocol,
|
|
769
|
+
session: _HTTPBridgeSession,
|
|
770
|
+
) -> None:
|
|
771
|
+
"""Renew the durable lease; callers must hold ``self._http_bridge_lock``."""
|
|
772
|
+
|
|
773
|
+
await _renew_durable_http_bridge_lease(self, session)
|
|
774
|
+
|
|
775
|
+
async def reconcile_durable_http_bridge_ownership(self: _HTTPBridgeServiceProtocol) -> int:
|
|
776
|
+
"""Close local sessions whose durable row is owned by another instance/epoch."""
|
|
777
|
+
|
|
778
|
+
return await _reconcile_durable_http_bridge_ownership(self)
|