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,2211 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
from collections.abc import Awaitable, Callable, Mapping
|
|
9
|
+
from dataclasses import replace
|
|
10
|
+
from typing import Any, NoReturn, Protocol, TypeVar, cast
|
|
11
|
+
|
|
12
|
+
import aiohttp
|
|
13
|
+
import anyio
|
|
14
|
+
from anyio.lowlevel import checkpoint_if_cancelled
|
|
15
|
+
from pydantic import ValidationError
|
|
16
|
+
|
|
17
|
+
from app.core.auth.refresh import RefreshError, is_transient_refresh_contention, refresh_contention_kind
|
|
18
|
+
from app.core.balancer import ResetPreferenceWindow, RoutingStrategy, failover_decision
|
|
19
|
+
from app.core.clients.proxy import (
|
|
20
|
+
ProxyResponseError,
|
|
21
|
+
UpstreamProxyRouteTrace,
|
|
22
|
+
filter_inbound_headers,
|
|
23
|
+
pop_compact_timeout_overrides,
|
|
24
|
+
push_compact_timeout_overrides,
|
|
25
|
+
)
|
|
26
|
+
from app.core.clients.proxy import compact_responses as core_compact_responses
|
|
27
|
+
from app.core.clients.thread_cache_identity import (
|
|
28
|
+
ThreadCacheIdentity,
|
|
29
|
+
)
|
|
30
|
+
from app.core.clients.thread_cache_identity import (
|
|
31
|
+
effective_thread_cache_identity_mode as _effective_thread_cache_identity_mode,
|
|
32
|
+
)
|
|
33
|
+
from app.core.config.dashboard_overrides import with_dashboard_overrides
|
|
34
|
+
from app.core.config.settings import get_settings
|
|
35
|
+
from app.core.config.settings_cache import get_settings_cache
|
|
36
|
+
from app.core.errors import openai_error
|
|
37
|
+
from app.core.openai.exceptions import ClientPayloadError
|
|
38
|
+
from app.core.openai.models import CompactResponsePayload
|
|
39
|
+
from app.core.openai.requests import ResponsesCompactRequest
|
|
40
|
+
from app.core.resilience.network_recovery import ProcessNetworkRecovery
|
|
41
|
+
from app.core.resilience.toggles import bind_resilience_toggles
|
|
42
|
+
from app.core.types import JsonValue
|
|
43
|
+
from app.core.upstream_proxy import ResolvedUpstreamRoute, UpstreamProxyRouteError
|
|
44
|
+
from app.core.utils.request_id import ensure_request_id, get_request_id
|
|
45
|
+
from app.core.utils.retry import backoff_seconds
|
|
46
|
+
from app.core.utils.shared_future import wait_on_shared_future
|
|
47
|
+
from app.db.models import Account, AccountStatus, DashboardSettings, StickySessionKind
|
|
48
|
+
from app.modules.api_keys.service import (
|
|
49
|
+
ApiKeyData,
|
|
50
|
+
ApiKeyRequestUsageBudget,
|
|
51
|
+
ApiKeyUsageReservationData,
|
|
52
|
+
)
|
|
53
|
+
from app.modules.proxy._service.support import _request_log_client_fields, _RequestLogFailureMetadata
|
|
54
|
+
from app.modules.proxy.affinity import (
|
|
55
|
+
_affinity_with_payload_continuity,
|
|
56
|
+
_AffinityPolicy,
|
|
57
|
+
_bare_codex_session_affinity,
|
|
58
|
+
_is_synthesized_turn_state,
|
|
59
|
+
_owner_lookup_session_id_from_headers,
|
|
60
|
+
_prompt_cache_key_from_request_model,
|
|
61
|
+
_request_allows_bare_session_cap_spillover,
|
|
62
|
+
_resolve_prompt_cache_key,
|
|
63
|
+
_sticky_key_from_turn_state_header,
|
|
64
|
+
_thread_codex_session_affinity,
|
|
65
|
+
)
|
|
66
|
+
from app.modules.proxy.affinity_observation import AffinityObservation
|
|
67
|
+
from app.modules.proxy.api_key_usage import estimate_api_key_request_usage
|
|
68
|
+
from app.modules.proxy.continuity import (
|
|
69
|
+
resolve_required_account_id,
|
|
70
|
+
without_http_bridge_session_affinity_headers,
|
|
71
|
+
)
|
|
72
|
+
from app.modules.proxy.helpers import (
|
|
73
|
+
_header_account_id,
|
|
74
|
+
_normalize_error_code,
|
|
75
|
+
_parse_openai_error,
|
|
76
|
+
classify_upstream_failure,
|
|
77
|
+
)
|
|
78
|
+
from app.modules.proxy.load_balancer import (
|
|
79
|
+
AccountConcurrencyCaps,
|
|
80
|
+
AccountLease,
|
|
81
|
+
AccountSelection,
|
|
82
|
+
effective_account_concurrency_caps,
|
|
83
|
+
)
|
|
84
|
+
from app.modules.proxy.replay_safety import (
|
|
85
|
+
project_responses_input_for_account_neutral_fresh_replay,
|
|
86
|
+
responses_input_suffix_retains_prior_output,
|
|
87
|
+
responses_payload_is_account_neutral_fresh_replay,
|
|
88
|
+
)
|
|
89
|
+
from app.modules.proxy.selection_errors import selection_failure_response
|
|
90
|
+
from app.modules.proxy.work_admission import AdmissionLease, WorkAdmissionController
|
|
91
|
+
|
|
92
|
+
logger = logging.getLogger("app.modules.proxy.service")
|
|
93
|
+
T = TypeVar("T")
|
|
94
|
+
|
|
95
|
+
_REQUEST_TRANSPORT_HTTP = "http"
|
|
96
|
+
_CompactResponses = Callable[
|
|
97
|
+
[ResponsesCompactRequest, Mapping[str, str], str, str | None],
|
|
98
|
+
Awaitable[CompactResponsePayload],
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _compact_turn_state_session_identity(session_key: object | None, session: object | None) -> str | None:
|
|
103
|
+
durable_session_id = getattr(session, "durable_session_id", None)
|
|
104
|
+
if isinstance(durable_session_id, str) and durable_session_id.strip():
|
|
105
|
+
return f"durable:{durable_session_id.strip()}"
|
|
106
|
+
if session_key is None:
|
|
107
|
+
return None
|
|
108
|
+
return f"live:{session_key!r}"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class _CompactServiceProtocol(Protocol):
|
|
112
|
+
_encryptor: Any
|
|
113
|
+
_load_balancer: Any
|
|
114
|
+
_repo_factory: Any
|
|
115
|
+
_http_bridge_lock: Any
|
|
116
|
+
_http_bridge_sessions: Any
|
|
117
|
+
_http_bridge_turn_state_index: Any
|
|
118
|
+
_durable_bridge: Any
|
|
119
|
+
|
|
120
|
+
def _get_work_admission(self) -> WorkAdmissionController: ...
|
|
121
|
+
|
|
122
|
+
def _raise_for_unsupported_input_image_references(self, payload: ResponsesCompactRequest) -> None: ...
|
|
123
|
+
|
|
124
|
+
async def _resolve_file_account_for_responses(
|
|
125
|
+
self, payload: ResponsesCompactRequest, headers: Mapping[str, str]
|
|
126
|
+
) -> str | None: ...
|
|
127
|
+
|
|
128
|
+
async def _resolve_forwarded_file_account_for_responses(
|
|
129
|
+
self,
|
|
130
|
+
payload: ResponsesCompactRequest,
|
|
131
|
+
headers: Mapping[str, str],
|
|
132
|
+
*,
|
|
133
|
+
forwarded_file_owner_account_id: str | None,
|
|
134
|
+
require_forwarded_file_owner: bool = False,
|
|
135
|
+
) -> str | None: ...
|
|
136
|
+
|
|
137
|
+
async def _acquire_account_response_create_lease_or_overload(
|
|
138
|
+
self, *, account_id: str, request_id: str, surface: str, concurrency_caps: AccountConcurrencyCaps
|
|
139
|
+
) -> AccountLease: ...
|
|
140
|
+
|
|
141
|
+
async def _resolve_upstream_route_for_account(
|
|
142
|
+
self, account: Account, *, operation: str
|
|
143
|
+
) -> ResolvedUpstreamRoute | None: ...
|
|
144
|
+
|
|
145
|
+
async def _select_account_with_budget_compatible(self, deadline: float, **kwargs: object) -> AccountSelection: ...
|
|
146
|
+
|
|
147
|
+
async def _resolve_websocket_previous_response_owner(
|
|
148
|
+
self,
|
|
149
|
+
*,
|
|
150
|
+
previous_response_id: str | None,
|
|
151
|
+
api_key: ApiKeyData | None,
|
|
152
|
+
session_id: str | None = None,
|
|
153
|
+
surface: str,
|
|
154
|
+
) -> str | None: ...
|
|
155
|
+
|
|
156
|
+
async def _resolve_compact_turn_state_owner(
|
|
157
|
+
self,
|
|
158
|
+
*,
|
|
159
|
+
turn_state: str,
|
|
160
|
+
api_key: ApiKeyData | None,
|
|
161
|
+
fail_on_missing: bool = True,
|
|
162
|
+
) -> str | None: ...
|
|
163
|
+
|
|
164
|
+
async def _compact_owner_selection_loss_is_quota_caused(self, account_id: str) -> bool: ...
|
|
165
|
+
|
|
166
|
+
async def _ensure_fresh_with_budget(
|
|
167
|
+
self, account: Account, *, force: bool = False, timeout_seconds: float | None = None
|
|
168
|
+
) -> Account: ...
|
|
169
|
+
|
|
170
|
+
async def _handle_stream_error(
|
|
171
|
+
self,
|
|
172
|
+
account: Account,
|
|
173
|
+
error: Any,
|
|
174
|
+
code: str,
|
|
175
|
+
http_status: int | None = None,
|
|
176
|
+
) -> Any: ...
|
|
177
|
+
|
|
178
|
+
async def _handle_proxy_error(self, account: Account, exc: ProxyResponseError) -> None: ...
|
|
179
|
+
|
|
180
|
+
async def _settle_compact_api_key_usage(
|
|
181
|
+
self,
|
|
182
|
+
*,
|
|
183
|
+
api_key: ApiKeyData | None,
|
|
184
|
+
api_key_reservation: ApiKeyUsageReservationData | None,
|
|
185
|
+
response: CompactResponsePayload | None,
|
|
186
|
+
request_service_tier: str | None,
|
|
187
|
+
) -> None: ...
|
|
188
|
+
|
|
189
|
+
async def _write_request_log(self, **kwargs: Any) -> None: ...
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _service_module() -> Any:
|
|
193
|
+
service_module = sys.modules.get("app.modules.proxy.service")
|
|
194
|
+
if service_module is None:
|
|
195
|
+
raise RuntimeError("app.modules.proxy.service is not loaded")
|
|
196
|
+
return service_module
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _service_global(name: str) -> Any:
|
|
200
|
+
return getattr(_service_module(), name)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _service_global_or(name: str, fallback: T) -> T:
|
|
204
|
+
service_module = sys.modules.get("app.modules.proxy.service")
|
|
205
|
+
if service_module is None:
|
|
206
|
+
return fallback
|
|
207
|
+
return cast(T, getattr(service_module, name, fallback))
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _service_get_settings() -> Any:
|
|
211
|
+
return _service_global_or("get_settings", get_settings)()
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _service_get_settings_cache() -> Any:
|
|
215
|
+
return _service_global_or("get_settings_cache", get_settings_cache)()
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _service_time() -> Any:
|
|
219
|
+
return _service_global_or("time", time)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _service_core_compact_responses() -> _CompactResponses:
|
|
223
|
+
return _service_global_or("core_compact_responses", core_compact_responses)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _service_push_compact_timeout_overrides(**kwargs: float) -> object:
|
|
227
|
+
service_module = sys.modules.get("app.modules.proxy.service")
|
|
228
|
+
if service_module is not None:
|
|
229
|
+
func = getattr(service_module, "push_compact_timeout_overrides", push_compact_timeout_overrides)
|
|
230
|
+
return cast(Callable[..., object], func)(**kwargs)
|
|
231
|
+
return push_compact_timeout_overrides(**kwargs)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _service_pop_compact_timeout_overrides(token: object) -> None:
|
|
235
|
+
service_module = sys.modules.get("app.modules.proxy.service")
|
|
236
|
+
if service_module is not None:
|
|
237
|
+
func = getattr(service_module, "pop_compact_timeout_overrides", pop_compact_timeout_overrides)
|
|
238
|
+
cast(Callable[[object], None], func)(token)
|
|
239
|
+
return
|
|
240
|
+
pop_compact_timeout_overrides(cast(Any, token))
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _request_kind_from_headers(headers: Mapping[str, str]) -> str:
|
|
244
|
+
raw_metadata = headers.get("x-codex-turn-metadata") or headers.get("X-Codex-Turn-Metadata")
|
|
245
|
+
if not raw_metadata:
|
|
246
|
+
return "normal"
|
|
247
|
+
try:
|
|
248
|
+
metadata = json.loads(raw_metadata)
|
|
249
|
+
except json.JSONDecodeError:
|
|
250
|
+
return "normal"
|
|
251
|
+
if not isinstance(metadata, dict):
|
|
252
|
+
return "normal"
|
|
253
|
+
raw_request_kind = metadata.get("request_kind")
|
|
254
|
+
if not isinstance(raw_request_kind, str):
|
|
255
|
+
return "normal"
|
|
256
|
+
request_kind = raw_request_kind.strip()
|
|
257
|
+
if request_kind == "compaction":
|
|
258
|
+
return request_kind
|
|
259
|
+
return "normal"
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _remaining_budget_seconds(deadline: float) -> float:
|
|
263
|
+
return cast(Callable[[float], float], _service_global("_remaining_budget_seconds"))(deadline)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _compact_upstream_call_budget_reserve_seconds(remaining_budget: float) -> float:
|
|
267
|
+
if remaining_budget <= 0:
|
|
268
|
+
return 0.0
|
|
269
|
+
return min(30.0, max(1.0, remaining_budget * 0.2), remaining_budget * 0.5)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _compact_freshness_budget_seconds(remaining_budget: float) -> float:
|
|
273
|
+
reserve = _compact_upstream_call_budget_reserve_seconds(remaining_budget)
|
|
274
|
+
return min(20.0, max(0.0, remaining_budget - reserve))
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _compact_upstream_budget_seconds(remaining_budget: float) -> float:
|
|
278
|
+
if remaining_budget <= 0:
|
|
279
|
+
return 0.0
|
|
280
|
+
reserve = _compact_upstream_call_budget_reserve_seconds(remaining_budget)
|
|
281
|
+
return max(0.0, remaining_budget - reserve)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _raise_proxy_budget_exhausted() -> NoReturn:
|
|
285
|
+
cast(Callable[[], NoReturn], _service_global("_raise_proxy_budget_exhausted"))()
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _raise_proxy_unavailable(message: str) -> NoReturn:
|
|
289
|
+
cast(Callable[[str], NoReturn], _service_global("_raise_proxy_unavailable"))(message)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _request_log_failure_metadata(exc: ProxyResponseError) -> _RequestLogFailureMetadata:
|
|
293
|
+
return cast(
|
|
294
|
+
Callable[[ProxyResponseError], _RequestLogFailureMetadata], _service_global("_request_log_failure_metadata")
|
|
295
|
+
)(exc)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _prefer_earlier_reset_window(settings: DashboardSettings) -> ResetPreferenceWindow:
|
|
299
|
+
return cast(Callable[[DashboardSettings], ResetPreferenceWindow], _service_global("_prefer_earlier_reset_window"))(
|
|
300
|
+
settings
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _routing_strategy(settings: DashboardSettings) -> RoutingStrategy:
|
|
305
|
+
return cast(Callable[[DashboardSettings], RoutingStrategy], _service_global("_routing_strategy"))(settings)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _call_with_supported_optional_kwargs(
|
|
309
|
+
func: Callable[..., Awaitable[CompactResponsePayload]],
|
|
310
|
+
*args: object,
|
|
311
|
+
optional_kwargs: Mapping[str, object],
|
|
312
|
+
) -> Awaitable[CompactResponsePayload]:
|
|
313
|
+
return cast(
|
|
314
|
+
Callable[..., Awaitable[CompactResponsePayload]], _service_global("_call_with_supported_optional_kwargs")
|
|
315
|
+
)(func, *args, optional_kwargs=optional_kwargs)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _maybe_log_proxy_request_payload(
|
|
319
|
+
kind: str,
|
|
320
|
+
payload: ResponsesCompactRequest,
|
|
321
|
+
headers: Mapping[str, str],
|
|
322
|
+
) -> None:
|
|
323
|
+
cast(Callable[..., None], _service_global("_maybe_log_proxy_request_payload"))(kind, payload, headers)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _maybe_log_proxy_request_shape(
|
|
327
|
+
kind: str,
|
|
328
|
+
payload: ResponsesCompactRequest,
|
|
329
|
+
headers: Mapping[str, str],
|
|
330
|
+
**kwargs: object,
|
|
331
|
+
) -> None:
|
|
332
|
+
cast(Callable[..., None], _service_global("_maybe_log_proxy_request_shape"))(kind, payload, headers, **kwargs)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _maybe_log_proxy_service_tier_trace(
|
|
336
|
+
kind: str,
|
|
337
|
+
*,
|
|
338
|
+
requested_service_tier: str | None,
|
|
339
|
+
actual_service_tier: str | None,
|
|
340
|
+
) -> None:
|
|
341
|
+
cast(Callable[..., None], _service_global("_maybe_log_proxy_service_tier_trace"))(
|
|
342
|
+
kind,
|
|
343
|
+
requested_service_tier=requested_service_tier,
|
|
344
|
+
actual_service_tier=actual_service_tier,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _should_retry_transient_stream_error(code: str | None, message: str | None) -> bool:
|
|
349
|
+
return cast(Callable[[str | None, str | None], bool], _service_global("_should_retry_transient_stream_error"))(
|
|
350
|
+
code, message
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _compact_previous_response_not_found_error(exc: ProxyResponseError) -> ProxyResponseError | None:
|
|
355
|
+
return cast(
|
|
356
|
+
Callable[[ProxyResponseError], ProxyResponseError | None],
|
|
357
|
+
_service_global("_compact_previous_response_not_found_error"),
|
|
358
|
+
)(exc)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _proxy_response_error_code(exc: ProxyResponseError) -> str | None:
|
|
362
|
+
return cast(Callable[[ProxyResponseError], str | None], _service_global("_proxy_response_error_code"))(exc)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _record_continuity_fail_closed(
|
|
366
|
+
*,
|
|
367
|
+
surface: str,
|
|
368
|
+
reason: str,
|
|
369
|
+
previous_response_id: str | None,
|
|
370
|
+
session_id: str | None,
|
|
371
|
+
upstream_error_code: str | None,
|
|
372
|
+
) -> None:
|
|
373
|
+
cast(Callable[..., None], _service_global("_record_continuity_fail_closed"))(
|
|
374
|
+
surface=surface,
|
|
375
|
+
reason=reason,
|
|
376
|
+
previous_response_id=previous_response_id,
|
|
377
|
+
session_id=session_id,
|
|
378
|
+
upstream_error_code=upstream_error_code,
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _is_security_work_authorization_required_error(code: str | None, message: str | None) -> bool:
|
|
383
|
+
return cast(
|
|
384
|
+
Callable[[str | None, str | None], bool],
|
|
385
|
+
_service_global("_is_security_work_authorization_required_error"),
|
|
386
|
+
)(code, message)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _is_account_neutral_error_code(code: str | None) -> bool:
|
|
390
|
+
return cast(Callable[[str | None], bool], _service_global("_is_account_neutral_error_code"))(code)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _upstream_error_from_openai(error: Any) -> Any:
|
|
394
|
+
return cast(Callable[[Any], Any], _service_global("_upstream_error_from_openai"))(error)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _estimated_lease_tokens_from_request_usage_budget(budget: ApiKeyRequestUsageBudget | None) -> float:
|
|
398
|
+
return cast(
|
|
399
|
+
Callable[[ApiKeyRequestUsageBudget | None], float],
|
|
400
|
+
_service_global("_estimated_lease_tokens_from_request_usage_budget"),
|
|
401
|
+
)(budget)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _service_tier_from_response(response: CompactResponsePayload | None) -> str | None:
|
|
405
|
+
return cast(Callable[[CompactResponsePayload | None], str | None], _service_global("_service_tier_from_response"))(
|
|
406
|
+
response
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _effective_service_tier(requested_service_tier: str | None, actual_service_tier: str | None) -> str | None:
|
|
411
|
+
return cast(
|
|
412
|
+
Callable[[str | None, str | None], str | None],
|
|
413
|
+
_service_global("_effective_service_tier"),
|
|
414
|
+
)(requested_service_tier, actual_service_tier)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _compact_same_contract_retry_budget() -> int:
|
|
418
|
+
return cast(int, _service_global("_COMPACT_SAME_CONTRACT_RETRY_BUDGET"))
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _compact_max_account_attempts() -> int:
|
|
422
|
+
return cast(int, _service_global("_COMPACT_MAX_ACCOUNT_ATTEMPTS"))
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _max_transient_same_account_retries() -> int:
|
|
426
|
+
return cast(int, _service_global("_MAX_TRANSIENT_SAME_ACCOUNT_RETRIES"))
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _no_security_work_authorized_accounts_code() -> str:
|
|
430
|
+
return cast(str, _service_global("_NO_SECURITY_WORK_AUTHORIZED_ACCOUNTS_CODE"))
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _sticky_key_from_compact_payload(payload: ResponsesCompactRequest) -> str | None:
|
|
434
|
+
value = _prompt_cache_key_from_request_model(payload)
|
|
435
|
+
if not isinstance(value, str):
|
|
436
|
+
return None
|
|
437
|
+
stripped = value.strip()
|
|
438
|
+
return stripped or None
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def _sticky_key_for_compact_request(
|
|
442
|
+
payload: ResponsesCompactRequest,
|
|
443
|
+
headers: Mapping[str, str],
|
|
444
|
+
*,
|
|
445
|
+
codex_session_affinity: bool,
|
|
446
|
+
openai_cache_affinity: bool,
|
|
447
|
+
openai_cache_affinity_max_age_seconds: int,
|
|
448
|
+
sticky_threads_enabled: bool,
|
|
449
|
+
api_key: ApiKeyData | None = None,
|
|
450
|
+
) -> _AffinityPolicy:
|
|
451
|
+
# A compact body is already trimmed, so it rarely extends the ordinary
|
|
452
|
+
# turns' transcript and will usually mint a new anchor. That is the same
|
|
453
|
+
# answer the ordinary path gives for a compacted turn, and it is correct:
|
|
454
|
+
# the upstream prefix cache is cold after compaction.
|
|
455
|
+
resolution = _resolve_prompt_cache_key(
|
|
456
|
+
payload,
|
|
457
|
+
openai_cache_affinity=openai_cache_affinity,
|
|
458
|
+
api_key=api_key,
|
|
459
|
+
max_age_seconds=openai_cache_affinity_max_age_seconds,
|
|
460
|
+
)
|
|
461
|
+
cache_key = resolution.sticky_key
|
|
462
|
+
cache_key_source = resolution.source
|
|
463
|
+
turn_state_key = _sticky_key_from_turn_state_header(headers)
|
|
464
|
+
if turn_state_key:
|
|
465
|
+
policy = _AffinityPolicy(
|
|
466
|
+
key=turn_state_key,
|
|
467
|
+
kind=StickySessionKind.CODEX_SESSION,
|
|
468
|
+
codex_session_source="turn_state",
|
|
469
|
+
)
|
|
470
|
+
elif (
|
|
471
|
+
thread_affinity := _thread_codex_session_affinity(
|
|
472
|
+
headers,
|
|
473
|
+
enabled=codex_session_affinity,
|
|
474
|
+
max_age_seconds=openai_cache_affinity_max_age_seconds,
|
|
475
|
+
)
|
|
476
|
+
) is not None:
|
|
477
|
+
policy = thread_affinity
|
|
478
|
+
elif (
|
|
479
|
+
session_affinity := _bare_codex_session_affinity(
|
|
480
|
+
headers,
|
|
481
|
+
enabled=codex_session_affinity,
|
|
482
|
+
allow_cap_spillover=_request_allows_bare_session_cap_spillover(payload),
|
|
483
|
+
)
|
|
484
|
+
) is not None:
|
|
485
|
+
policy = session_affinity
|
|
486
|
+
elif openai_cache_affinity:
|
|
487
|
+
policy = _AffinityPolicy(
|
|
488
|
+
key=cache_key,
|
|
489
|
+
kind=StickySessionKind.PROMPT_CACHE,
|
|
490
|
+
max_age_seconds=openai_cache_affinity_max_age_seconds,
|
|
491
|
+
prompt_cache_key_source=cache_key_source,
|
|
492
|
+
)
|
|
493
|
+
elif sticky_threads_enabled:
|
|
494
|
+
policy = _AffinityPolicy(
|
|
495
|
+
key=cache_key,
|
|
496
|
+
kind=StickySessionKind.STICKY_THREAD,
|
|
497
|
+
reallocate_sticky=True,
|
|
498
|
+
prompt_cache_key_source=cache_key_source,
|
|
499
|
+
)
|
|
500
|
+
else:
|
|
501
|
+
policy = _AffinityPolicy()
|
|
502
|
+
policy = replace(policy, prompt_cache_derivation_outcome=resolution.outcome)
|
|
503
|
+
return _affinity_with_payload_continuity(policy, payload)
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def _service_tier_from_compact_payload(payload: ResponsesCompactRequest) -> str | None:
|
|
507
|
+
normalize = cast(Callable[[JsonValue], str | None], _service_global("_normalize_service_tier_value"))
|
|
508
|
+
return normalize(payload.service_tier)
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
# Account statuses that prove the pinned owner's selection-time loss is caused
|
|
512
|
+
# by upstream quota/rate-limit state rather than authentication, deactivation,
|
|
513
|
+
# or an operator pause. Only these authorize account-neutral replay recovery.
|
|
514
|
+
_COMPACT_OWNER_QUOTA_UNAVAILABLE_STATUSES = (
|
|
515
|
+
AccountStatus.RATE_LIMITED,
|
|
516
|
+
AccountStatus.QUOTA_EXCEEDED,
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _compact_replay_history_retains_prior_output(input_items: list[JsonValue]) -> bool:
|
|
521
|
+
"""Prove the carried history retains prior assistant output before new input.
|
|
522
|
+
|
|
523
|
+
A self-contained account-neutral ``input`` is not by itself a full resend:
|
|
524
|
+
a client could send only the turns after ``previous_response_id`` (for
|
|
525
|
+
example two fresh user messages) and rely on the owner account to hold the
|
|
526
|
+
earlier conversation, so replaying without the anchor would compact a
|
|
527
|
+
truncated history. Without durable prefix metadata for the compact surface,
|
|
528
|
+
the strongest client-side evidence of a full resend is the same
|
|
529
|
+
retained-prior-output shape the HTTP bridge replay path trusts: the input
|
|
530
|
+
must parse as a clean transcript whose final segment is the previous
|
|
531
|
+
response's completed assistant output followed only by fresh client input.
|
|
532
|
+
The split is anchored at the last assistant message so the shared suffix
|
|
533
|
+
walk proves exactly that segment; anything it cannot prove stays
|
|
534
|
+
owner-bound.
|
|
535
|
+
|
|
536
|
+
This is the evidence ceiling of the #1490 rescope: completeness relative to
|
|
537
|
+
the anchored conversation is not provable from the payload alone, and the
|
|
538
|
+
durable prefix metadata that could prove it is deliberately not consulted
|
|
539
|
+
here. A delta resend that itself carries a completed assistant exchange
|
|
540
|
+
ahead of the fresh input is indistinguishable from a full resend and is
|
|
541
|
+
recovered as the client's authoritative local history — the same trust the
|
|
542
|
+
shared account-neutral fresh-replay rules already grant a normal turn that
|
|
543
|
+
abandons an unavailable owner. The rejected shapes below are the ones the
|
|
544
|
+
transcript walk can actually refute.
|
|
545
|
+
"""
|
|
546
|
+
|
|
547
|
+
last_assistant_index: int | None = None
|
|
548
|
+
for index in range(len(input_items) - 1, -1, -1):
|
|
549
|
+
item = input_items[index]
|
|
550
|
+
if isinstance(item, dict) and item.get("type") in (None, "message") and item.get("role") == "assistant":
|
|
551
|
+
last_assistant_index = index
|
|
552
|
+
break
|
|
553
|
+
# ``responses_input_suffix_retains_prior_output`` requires a non-empty
|
|
554
|
+
# stored prefix, so a history that opens with (or lacks) assistant output
|
|
555
|
+
# cannot be proven and stays owner-bound.
|
|
556
|
+
if last_assistant_index is None or last_assistant_index == 0:
|
|
557
|
+
return False
|
|
558
|
+
# The projection is an identity transform for input that already passed
|
|
559
|
+
# the account-neutral fresh-replay gate (no server-assigned ids, no
|
|
560
|
+
# reasoning or omitted bookkeeping types survive that gate), but it is the
|
|
561
|
+
# shared authority for recognizing the canonical Responses-Lite developer
|
|
562
|
+
# instruction behind an ``additional_tools`` bundle — without that index
|
|
563
|
+
# the suffix walk would reject every Lite full resend.
|
|
564
|
+
projection = project_responses_input_for_account_neutral_fresh_replay(
|
|
565
|
+
input_items,
|
|
566
|
+
stored_count=last_assistant_index,
|
|
567
|
+
)
|
|
568
|
+
if projection is None:
|
|
569
|
+
return False
|
|
570
|
+
return responses_input_suffix_retains_prior_output(
|
|
571
|
+
projection.input_items,
|
|
572
|
+
stored_count=projection.stored_prefix_count,
|
|
573
|
+
canonical_lite_developer_index=projection.canonical_lite_developer_index,
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def _compact_account_neutral_replay_payload(
|
|
578
|
+
payload: ResponsesCompactRequest,
|
|
579
|
+
) -> ResponsesCompactRequest | None:
|
|
580
|
+
"""Return the anchor-free replay payload for a verified full resend.
|
|
581
|
+
|
|
582
|
+
A compact request pinned only by ``previous_response_id`` may move off an
|
|
583
|
+
unselectable owner account when the history it carries is provably
|
|
584
|
+
account-neutral: the upstream-bound payload without the anchor must pass
|
|
585
|
+
the shared fresh-replay validation, so no encrypted or compaction state,
|
|
586
|
+
server-assigned item ids, account-scoped file/container handles,
|
|
587
|
+
conversation/prompt handles, or hosted/MCP state can reach the replacement
|
|
588
|
+
account.
|
|
589
|
+
|
|
590
|
+
Neutrality is checked on the serialized upstream-bound payload
|
|
591
|
+
(``to_payload``), never on the request model, matching how the HTTP bridge
|
|
592
|
+
replay paths apply the shared gate to pre-transport serializations. The
|
|
593
|
+
compact transport applies two further mutations after this serialization —
|
|
594
|
+
the Responses-Lite ``reasoning.context`` control and inline image
|
|
595
|
+
fetching — both proxy-injected, account-agnostic, and applied identically
|
|
596
|
+
to the owner send and the replay send, so they are not part of the client
|
|
597
|
+
payload being proven. The serialized history must additionally be a
|
|
598
|
+
complete resend. ``to_payload`` can still drop history on the wire: it
|
|
599
|
+
strips poisoned local-compact fallback messages together with their
|
|
600
|
+
trailing encrypted compaction item, and it trims oversized inputs down to a
|
|
601
|
+
head, a trim marker, and a tail. Both remain multi-item account-neutral
|
|
602
|
+
lists. Sending either to a replacement account without the anchor would
|
|
603
|
+
compact an incomplete conversation, because only the owner can resolve the
|
|
604
|
+
omitted context from the dropped anchor. So the wire input must be
|
|
605
|
+
item-for-item identical to the validated request input, must still carry
|
|
606
|
+
more than one item, and must retain prior assistant output ahead of the new
|
|
607
|
+
client input (see ``_compact_replay_history_retains_prior_output``).
|
|
608
|
+
"""
|
|
609
|
+
|
|
610
|
+
previous_response_id = getattr(payload, "previous_response_id", None)
|
|
611
|
+
if not isinstance(previous_response_id, str) or not previous_response_id.strip():
|
|
612
|
+
return None
|
|
613
|
+
if not isinstance(payload.input, list):
|
|
614
|
+
return None
|
|
615
|
+
replay_source = payload.model_dump(mode="json", exclude_none=True)
|
|
616
|
+
replay_source.pop("previous_response_id", None)
|
|
617
|
+
request_input = replay_source.get("input")
|
|
618
|
+
if not isinstance(request_input, list) or len(request_input) <= 1:
|
|
619
|
+
return None
|
|
620
|
+
try:
|
|
621
|
+
replay_payload = ResponsesCompactRequest.model_validate(replay_source)
|
|
622
|
+
replay_wire_payload = replay_payload.to_payload()
|
|
623
|
+
except (ValidationError, ClientPayloadError):
|
|
624
|
+
return None
|
|
625
|
+
replay_wire_input = replay_wire_payload.get("input")
|
|
626
|
+
if not isinstance(replay_wire_input, list) or len(replay_wire_input) <= 1:
|
|
627
|
+
return None
|
|
628
|
+
if replay_wire_input != request_input:
|
|
629
|
+
return None
|
|
630
|
+
if not responses_payload_is_account_neutral_fresh_replay(replay_wire_payload):
|
|
631
|
+
return None
|
|
632
|
+
if not _compact_replay_history_retains_prior_output(cast(list[JsonValue], replay_wire_input)):
|
|
633
|
+
return None
|
|
634
|
+
return replay_payload
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
class _CompactMixin:
|
|
638
|
+
async def _compact_owner_selection_loss_is_quota_caused(self, account_id: str) -> bool:
|
|
639
|
+
"""Return whether the pinned owner is unselectable because of quota state.
|
|
640
|
+
|
|
641
|
+
Account-neutral replay off a pinned previous-response owner is legal
|
|
642
|
+
only for owner loss the owner's quota state caused. At selection time
|
|
643
|
+
that evidence is the owner's own persisted status: ``RATE_LIMITED`` or
|
|
644
|
+
``QUOTA_EXCEEDED`` is the same upstream usage-exhaustion state the
|
|
645
|
+
selector consulted. Hard authentication loss (``DEACTIVATED``),
|
|
646
|
+
operator pauses, local capacity caps on a request-routable account, and
|
|
647
|
+
a failed lookup all stay owner-bound. ``REAUTH_REQUIRED`` remains
|
|
648
|
+
selectable and therefore does not itself cause owner loss.
|
|
649
|
+
"""
|
|
650
|
+
|
|
651
|
+
proxy = cast(_CompactServiceProtocol, self)
|
|
652
|
+
try:
|
|
653
|
+
async with proxy._repo_factory() as repos:
|
|
654
|
+
account = await repos.accounts.get_by_id_fresh(account_id)
|
|
655
|
+
# Read inside the repository scope: the session expires ORM
|
|
656
|
+
# attributes when it closes.
|
|
657
|
+
status = account.status if account is not None else None
|
|
658
|
+
except Exception:
|
|
659
|
+
logger.warning(
|
|
660
|
+
"Compact previous-response owner status lookup failed; keeping the request owner-bound",
|
|
661
|
+
exc_info=True,
|
|
662
|
+
)
|
|
663
|
+
return False
|
|
664
|
+
return status in _COMPACT_OWNER_QUOTA_UNAVAILABLE_STATUSES
|
|
665
|
+
|
|
666
|
+
async def _resolve_compact_turn_state_owner(
|
|
667
|
+
self,
|
|
668
|
+
*,
|
|
669
|
+
turn_state: str,
|
|
670
|
+
api_key: ApiKeyData | None,
|
|
671
|
+
fail_on_missing: bool = True,
|
|
672
|
+
) -> str | None:
|
|
673
|
+
"""Resolve a turn-state token to its API-key-scoped HTTP bridge owner.
|
|
674
|
+
|
|
675
|
+
A compact request cannot safely fall back to generic sticky routing: an
|
|
676
|
+
opaque turn-state is valid only on the account that created it. The
|
|
677
|
+
local alias index is the fast path; the durable lookup covers a request
|
|
678
|
+
that arrives on another replica. Both lookup surfaces are keyed by the
|
|
679
|
+
exact API key id, so a token observed under one key cannot select an
|
|
680
|
+
account for another key.
|
|
681
|
+
|
|
682
|
+
Synthetic-shaped ``turn_*`` / ``http_turn_*`` values can be real
|
|
683
|
+
registered bridge aliases on later turns. Callers may pass
|
|
684
|
+
``fail_on_missing=False`` only for those synthetic placeholders so an
|
|
685
|
+
unregistered first-turn placeholder still allows weaker routing signals
|
|
686
|
+
such as file ownership to run.
|
|
687
|
+
"""
|
|
688
|
+
proxy = cast(_CompactServiceProtocol, self)
|
|
689
|
+
normalized_turn_state = turn_state.strip()
|
|
690
|
+
if not normalized_turn_state:
|
|
691
|
+
raise ProxyResponseError(
|
|
692
|
+
502,
|
|
693
|
+
openai_error(
|
|
694
|
+
"turn_state_owner_unavailable",
|
|
695
|
+
"Turn-state owner account is unavailable; retry the logical turn.",
|
|
696
|
+
error_type="server_error",
|
|
697
|
+
),
|
|
698
|
+
)
|
|
699
|
+
api_key_id = api_key.id if api_key is not None else None
|
|
700
|
+
owner_refs: list[tuple[str, str, str | None]] = []
|
|
701
|
+
async with proxy._http_bridge_lock:
|
|
702
|
+
session_key = proxy._http_bridge_turn_state_index.get((normalized_turn_state, api_key_id))
|
|
703
|
+
session = proxy._http_bridge_sessions.get(session_key) if session_key is not None else None
|
|
704
|
+
account = getattr(session, "account", None)
|
|
705
|
+
account_id = getattr(account, "id", None)
|
|
706
|
+
if isinstance(account_id, str) and account_id.strip():
|
|
707
|
+
owner_refs.append(
|
|
708
|
+
(
|
|
709
|
+
"turn-state live index",
|
|
710
|
+
account_id,
|
|
711
|
+
_compact_turn_state_session_identity(session_key, session),
|
|
712
|
+
)
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
try:
|
|
716
|
+
durable_lookup = await proxy._durable_bridge.lookup_turn_state_target(
|
|
717
|
+
turn_state=normalized_turn_state,
|
|
718
|
+
api_key_id=api_key_id,
|
|
719
|
+
)
|
|
720
|
+
except Exception as exc:
|
|
721
|
+
raise ProxyResponseError(
|
|
722
|
+
502,
|
|
723
|
+
openai_error(
|
|
724
|
+
"turn_state_owner_unavailable",
|
|
725
|
+
"Turn-state owner account is unavailable; retry the logical turn.",
|
|
726
|
+
error_type="server_error",
|
|
727
|
+
),
|
|
728
|
+
) from exc
|
|
729
|
+
account_id = getattr(durable_lookup, "account_id", None)
|
|
730
|
+
if isinstance(account_id, str) and account_id.strip():
|
|
731
|
+
durable_session_id = getattr(durable_lookup, "session_id", None)
|
|
732
|
+
owner_refs.append(
|
|
733
|
+
(
|
|
734
|
+
"turn-state durable alias",
|
|
735
|
+
account_id,
|
|
736
|
+
f"durable:{durable_session_id.strip()}"
|
|
737
|
+
if isinstance(durable_session_id, str) and durable_session_id.strip()
|
|
738
|
+
else None,
|
|
739
|
+
)
|
|
740
|
+
)
|
|
741
|
+
resolved_owner = resolve_required_account_id(
|
|
742
|
+
*((source, owner_account_id) for source, owner_account_id, _session_id in owner_refs)
|
|
743
|
+
)
|
|
744
|
+
if resolved_owner is not None:
|
|
745
|
+
session_identities = {
|
|
746
|
+
session_identity
|
|
747
|
+
for _source, owner_account_id, session_identity in owner_refs
|
|
748
|
+
if owner_account_id == resolved_owner and session_identity is not None
|
|
749
|
+
}
|
|
750
|
+
if len(session_identities) > 1:
|
|
751
|
+
raise ProxyResponseError(
|
|
752
|
+
502,
|
|
753
|
+
openai_error(
|
|
754
|
+
"continuity_owner_conflict",
|
|
755
|
+
"Turn-state owner sessions conflict; retry the logical turn.",
|
|
756
|
+
error_type="server_error",
|
|
757
|
+
),
|
|
758
|
+
)
|
|
759
|
+
return resolved_owner
|
|
760
|
+
if not fail_on_missing:
|
|
761
|
+
return None
|
|
762
|
+
raise ProxyResponseError(
|
|
763
|
+
502,
|
|
764
|
+
openai_error(
|
|
765
|
+
"turn_state_owner_unavailable",
|
|
766
|
+
"Turn-state owner account is unavailable; retry the logical turn.",
|
|
767
|
+
error_type="server_error",
|
|
768
|
+
),
|
|
769
|
+
)
|
|
770
|
+
|
|
771
|
+
async def compact_responses(
|
|
772
|
+
self,
|
|
773
|
+
payload: ResponsesCompactRequest,
|
|
774
|
+
headers: Mapping[str, str],
|
|
775
|
+
*,
|
|
776
|
+
codex_session_affinity: bool = False,
|
|
777
|
+
openai_cache_affinity: bool = False,
|
|
778
|
+
api_key: ApiKeyData | None = None,
|
|
779
|
+
api_key_reservation: ApiKeyUsageReservationData | None = None,
|
|
780
|
+
client_ip: str | None = None,
|
|
781
|
+
forwarded_request: bool = False,
|
|
782
|
+
forwarded_file_owner_account_id: str | None = None,
|
|
783
|
+
) -> CompactResponsePayload:
|
|
784
|
+
proxy = cast(_CompactServiceProtocol, self)
|
|
785
|
+
_maybe_log_proxy_request_payload("compact", payload, headers)
|
|
786
|
+
filtered = filter_inbound_headers(headers)
|
|
787
|
+
useragent, useragent_group, conversation_id = _request_log_client_fields(headers)
|
|
788
|
+
request_kind = _request_kind_from_headers(headers)
|
|
789
|
+
request_id = get_request_id() or ensure_request_id(None)
|
|
790
|
+
start = _service_time().monotonic()
|
|
791
|
+
base_settings = _service_get_settings()
|
|
792
|
+
deadline = start + base_settings.compact_request_budget_seconds
|
|
793
|
+
account_id_value: str | None = None
|
|
794
|
+
log_status = "error"
|
|
795
|
+
log_error_code: str | None = None
|
|
796
|
+
log_error_message: str | None = None
|
|
797
|
+
failure_metadata = _RequestLogFailureMetadata()
|
|
798
|
+
response: CompactResponsePayload | None = None
|
|
799
|
+
request_service_tier: str | None = None
|
|
800
|
+
actual_service_tier: str | None = None
|
|
801
|
+
route_mode: str | None = None
|
|
802
|
+
route_pool_id: str | None = None
|
|
803
|
+
route_endpoint_id: str | None = None
|
|
804
|
+
route_fallback_used: bool | None = None
|
|
805
|
+
route_fail_closed_reason: str | None = None
|
|
806
|
+
settlement_attempted = False
|
|
807
|
+
|
|
808
|
+
async def settle_compact_usage(
|
|
809
|
+
*,
|
|
810
|
+
api_key: ApiKeyData | None,
|
|
811
|
+
api_key_reservation: ApiKeyUsageReservationData | None,
|
|
812
|
+
response: CompactResponsePayload | None,
|
|
813
|
+
request_service_tier: str | None,
|
|
814
|
+
) -> None:
|
|
815
|
+
nonlocal settlement_attempted
|
|
816
|
+
if settlement_attempted:
|
|
817
|
+
return
|
|
818
|
+
if forwarded_request and response is None:
|
|
819
|
+
# A forwarded receiver has not transferred cleanup ownership
|
|
820
|
+
# until its successful HTTP 200. Every error before that
|
|
821
|
+
# acknowledgement remains the origin's single release path.
|
|
822
|
+
return
|
|
823
|
+
settlement_attempted = True
|
|
824
|
+
await proxy._settle_compact_api_key_usage(
|
|
825
|
+
api_key=api_key,
|
|
826
|
+
api_key_reservation=api_key_reservation,
|
|
827
|
+
response=response,
|
|
828
|
+
request_service_tier=request_service_tier,
|
|
829
|
+
)
|
|
830
|
+
|
|
831
|
+
proxy._raise_for_unsupported_input_image_references(payload)
|
|
832
|
+
try:
|
|
833
|
+
rewritten_file_account_id = await proxy._resolve_forwarded_file_account_for_responses(
|
|
834
|
+
payload,
|
|
835
|
+
headers,
|
|
836
|
+
forwarded_file_owner_account_id=forwarded_file_owner_account_id,
|
|
837
|
+
require_forwarded_file_owner=forwarded_request,
|
|
838
|
+
)
|
|
839
|
+
except ProxyResponseError:
|
|
840
|
+
if not forwarded_request and api_key is not None and api_key_reservation is not None:
|
|
841
|
+
try:
|
|
842
|
+
await settle_compact_usage(
|
|
843
|
+
api_key=api_key,
|
|
844
|
+
api_key_reservation=api_key_reservation,
|
|
845
|
+
response=None,
|
|
846
|
+
request_service_tier=_service_tier_from_compact_payload(payload),
|
|
847
|
+
)
|
|
848
|
+
except Exception:
|
|
849
|
+
logger.warning(
|
|
850
|
+
"Failed to settle compact API key reservation after owner lookup failure",
|
|
851
|
+
exc_info=True,
|
|
852
|
+
)
|
|
853
|
+
raise
|
|
854
|
+
except asyncio.CancelledError:
|
|
855
|
+
if not forwarded_request and api_key is not None and api_key_reservation is not None:
|
|
856
|
+
try:
|
|
857
|
+
await settle_compact_usage(
|
|
858
|
+
api_key=api_key,
|
|
859
|
+
api_key_reservation=api_key_reservation,
|
|
860
|
+
response=None,
|
|
861
|
+
request_service_tier=_service_tier_from_compact_payload(payload),
|
|
862
|
+
)
|
|
863
|
+
except Exception:
|
|
864
|
+
logger.warning(
|
|
865
|
+
"Failed to settle compact API key reservation after cancelled owner lookup",
|
|
866
|
+
exc_info=True,
|
|
867
|
+
)
|
|
868
|
+
raise
|
|
869
|
+
settings = await _service_get_settings_cache().get()
|
|
870
|
+
# C2-3 resilience toggles: this request's snapshot, bound for the client.
|
|
871
|
+
resilience = bind_resilience_toggles(settings, startup_settings=base_settings)
|
|
872
|
+
concurrency_caps = effective_account_concurrency_caps(settings)
|
|
873
|
+
prefer_earlier_reset = settings.prefer_earlier_reset_accounts
|
|
874
|
+
# Resolved once per request: API-key override, then the fleet value read
|
|
875
|
+
# off the *overlaid* settings (dashboard over environment over default),
|
|
876
|
+
# never the raw dashboard row whose column is NULL until an operator sets
|
|
877
|
+
# it. See the matching comment in ``streaming/retry.py``.
|
|
878
|
+
thread_cache_identity_mode, thread_cache_identity_from_key = _effective_thread_cache_identity_mode(
|
|
879
|
+
api_key, with_dashboard_overrides(base_settings)
|
|
880
|
+
)
|
|
881
|
+
affinity = _sticky_key_for_compact_request(
|
|
882
|
+
payload,
|
|
883
|
+
headers,
|
|
884
|
+
codex_session_affinity=codex_session_affinity,
|
|
885
|
+
openai_cache_affinity=openai_cache_affinity,
|
|
886
|
+
openai_cache_affinity_max_age_seconds=settings.openai_cache_affinity_max_age_seconds,
|
|
887
|
+
sticky_threads_enabled=settings.sticky_threads_enabled,
|
|
888
|
+
api_key=api_key,
|
|
889
|
+
)
|
|
890
|
+
affinity_observation = AffinityObservation.from_policy(affinity)
|
|
891
|
+
_maybe_log_proxy_request_shape(
|
|
892
|
+
"compact",
|
|
893
|
+
payload,
|
|
894
|
+
headers,
|
|
895
|
+
sticky_kind=affinity_observation.kind,
|
|
896
|
+
sticky_key_source=affinity_observation.source,
|
|
897
|
+
derivation_outcome=affinity.prompt_cache_derivation_outcome,
|
|
898
|
+
prompt_cache_key_set=_prompt_cache_key_from_request_model(payload) is not None,
|
|
899
|
+
thread_cache_identity_mode=thread_cache_identity_mode,
|
|
900
|
+
thread_cache_identity_from_key=thread_cache_identity_from_key,
|
|
901
|
+
)
|
|
902
|
+
routing_strategy = _routing_strategy(settings)
|
|
903
|
+
turn_state_owner_account_id: str | None = None
|
|
904
|
+
turn_state = _sticky_key_from_turn_state_header(headers)
|
|
905
|
+
if turn_state is not None:
|
|
906
|
+
turn_state_owner_account_id = await proxy._resolve_compact_turn_state_owner(
|
|
907
|
+
turn_state=turn_state,
|
|
908
|
+
api_key=api_key,
|
|
909
|
+
fail_on_missing=not _is_synthesized_turn_state(turn_state),
|
|
910
|
+
)
|
|
911
|
+
previous_response_id = getattr(payload, "previous_response_id", None)
|
|
912
|
+
previous_response_preferred_account_id: str | None = None
|
|
913
|
+
previous_response_lookup_session_id: str | None = None
|
|
914
|
+
if isinstance(previous_response_id, str) and previous_response_id.strip():
|
|
915
|
+
previous_response_id = previous_response_id.strip()
|
|
916
|
+
previous_response_lookup_session_id = _owner_lookup_session_id_from_headers(headers)
|
|
917
|
+
previous_response_preferred_account_id = await proxy._resolve_websocket_previous_response_owner(
|
|
918
|
+
previous_response_id=previous_response_id,
|
|
919
|
+
api_key=api_key,
|
|
920
|
+
session_id=previous_response_lookup_session_id,
|
|
921
|
+
surface="compact",
|
|
922
|
+
)
|
|
923
|
+
if previous_response_preferred_account_id is None:
|
|
924
|
+
selection_inputs = await proxy._load_balancer._load_selection_inputs(
|
|
925
|
+
model=payload.model,
|
|
926
|
+
additional_limit_name=None,
|
|
927
|
+
account_ids=api_key.assigned_account_ids
|
|
928
|
+
if api_key is not None and api_key.account_assignment_scope_enabled
|
|
929
|
+
else None,
|
|
930
|
+
)
|
|
931
|
+
if len(selection_inputs.accounts) != 1:
|
|
932
|
+
message = "Previous response owner account is unavailable; retry later."
|
|
933
|
+
_record_continuity_fail_closed(
|
|
934
|
+
surface="compact",
|
|
935
|
+
reason="owner_account_unavailable",
|
|
936
|
+
previous_response_id=previous_response_id,
|
|
937
|
+
session_id=previous_response_lookup_session_id,
|
|
938
|
+
upstream_error_code="owner_lookup_miss",
|
|
939
|
+
)
|
|
940
|
+
raise ProxyResponseError(
|
|
941
|
+
502,
|
|
942
|
+
openai_error(
|
|
943
|
+
"previous_response_owner_unavailable",
|
|
944
|
+
message,
|
|
945
|
+
error_type="server_error",
|
|
946
|
+
),
|
|
947
|
+
)
|
|
948
|
+
|
|
949
|
+
# File pins are account ownership, not locality. Resolved turn-state or
|
|
950
|
+
# previous-response owners above still take precedence (and conflicts
|
|
951
|
+
# fail closed), while process-session/prompt-cache hints never hide a
|
|
952
|
+
# known file owner.
|
|
953
|
+
preferred_account_id = resolve_required_account_id(
|
|
954
|
+
("turn state", turn_state_owner_account_id),
|
|
955
|
+
("previous response", previous_response_preferred_account_id),
|
|
956
|
+
("input file", rewritten_file_account_id),
|
|
957
|
+
)
|
|
958
|
+
deferred_stream_health: list[tuple[Account, Any, str, int | None]] = []
|
|
959
|
+
deferred_http_500_health: list[tuple[Account, ProxyResponseError, int]] = []
|
|
960
|
+
deferred_proxy_health: list[tuple[Account, ProxyResponseError]] = []
|
|
961
|
+
deferred_permanent_health: list[tuple[Account, str]] = []
|
|
962
|
+
settlement_attempted = False
|
|
963
|
+
|
|
964
|
+
async def flush_deferred_health() -> None:
|
|
965
|
+
stream_pending = list(deferred_stream_health)
|
|
966
|
+
deferred_stream_health.clear()
|
|
967
|
+
http_500_pending = list(deferred_http_500_health)
|
|
968
|
+
deferred_http_500_health.clear()
|
|
969
|
+
proxy_pending = list(deferred_proxy_health)
|
|
970
|
+
deferred_proxy_health.clear()
|
|
971
|
+
permanent_pending = list(deferred_permanent_health)
|
|
972
|
+
deferred_permanent_health.clear()
|
|
973
|
+
for failed_account, failed_error, failed_code, failed_status in stream_pending:
|
|
974
|
+
try:
|
|
975
|
+
await proxy._handle_stream_error(
|
|
976
|
+
failed_account,
|
|
977
|
+
failed_error,
|
|
978
|
+
failed_code,
|
|
979
|
+
http_status=failed_status,
|
|
980
|
+
)
|
|
981
|
+
except Exception:
|
|
982
|
+
logger.warning(
|
|
983
|
+
"Failed to flush deferred compact stream health account_id=%s request_id=%s",
|
|
984
|
+
failed_account.id,
|
|
985
|
+
request_id,
|
|
986
|
+
exc_info=True,
|
|
987
|
+
)
|
|
988
|
+
for failed_account, failed_exc, extra_error_count in http_500_pending:
|
|
989
|
+
try:
|
|
990
|
+
await proxy._handle_proxy_error(failed_account, failed_exc)
|
|
991
|
+
await proxy._load_balancer.record_errors(failed_account, extra_error_count)
|
|
992
|
+
except Exception:
|
|
993
|
+
logger.warning(
|
|
994
|
+
"Failed to flush deferred compact HTTP 500 health account_id=%s request_id=%s",
|
|
995
|
+
failed_account.id,
|
|
996
|
+
request_id,
|
|
997
|
+
exc_info=True,
|
|
998
|
+
)
|
|
999
|
+
for failed_account, failed_exc in proxy_pending:
|
|
1000
|
+
try:
|
|
1001
|
+
await proxy._handle_proxy_error(failed_account, failed_exc)
|
|
1002
|
+
except Exception:
|
|
1003
|
+
logger.warning(
|
|
1004
|
+
"Failed to flush deferred compact proxy health account_id=%s request_id=%s",
|
|
1005
|
+
failed_account.id,
|
|
1006
|
+
request_id,
|
|
1007
|
+
exc_info=True,
|
|
1008
|
+
)
|
|
1009
|
+
for failed_account, failed_code in permanent_pending:
|
|
1010
|
+
try:
|
|
1011
|
+
await proxy._load_balancer.mark_permanent_failure(failed_account, failed_code)
|
|
1012
|
+
except Exception:
|
|
1013
|
+
logger.warning(
|
|
1014
|
+
"Failed to flush deferred compact permanent health account_id=%s request_id=%s",
|
|
1015
|
+
failed_account.id,
|
|
1016
|
+
request_id,
|
|
1017
|
+
exc_info=True,
|
|
1018
|
+
)
|
|
1019
|
+
|
|
1020
|
+
async def settle_compact_usage(
|
|
1021
|
+
*,
|
|
1022
|
+
api_key: ApiKeyData | None,
|
|
1023
|
+
api_key_reservation: ApiKeyUsageReservationData | None,
|
|
1024
|
+
response: CompactResponsePayload | None,
|
|
1025
|
+
request_service_tier: str | None,
|
|
1026
|
+
) -> None:
|
|
1027
|
+
nonlocal settlement_attempted
|
|
1028
|
+
settlement_attempted = True
|
|
1029
|
+
settlement_error: ProxyResponseError | None = None
|
|
1030
|
+
try:
|
|
1031
|
+
await proxy._settle_compact_api_key_usage(
|
|
1032
|
+
api_key=api_key,
|
|
1033
|
+
api_key_reservation=api_key_reservation,
|
|
1034
|
+
response=response,
|
|
1035
|
+
request_service_tier=request_service_tier,
|
|
1036
|
+
)
|
|
1037
|
+
except ProxyResponseError as exc:
|
|
1038
|
+
if exc.failure_phase != "usage_settlement" or not exc.reservation_released:
|
|
1039
|
+
raise
|
|
1040
|
+
settlement_error = exc
|
|
1041
|
+
flush_task = asyncio.create_task(
|
|
1042
|
+
flush_deferred_health(),
|
|
1043
|
+
name=f"compact-deferred-health-{request_id}",
|
|
1044
|
+
)
|
|
1045
|
+
cancellation_pending = False
|
|
1046
|
+
# The anyio shield keeps a level-cancelled Starlette scope from
|
|
1047
|
+
# re-raising into every ``await`` (busy-spin), and
|
|
1048
|
+
# ``wait_on_shared_future`` keeps waits off the task's
|
|
1049
|
+
# done-callback list (3.14 shield leaks one per cancelled wait).
|
|
1050
|
+
with anyio.CancelScope(shield=True):
|
|
1051
|
+
while not flush_task.done():
|
|
1052
|
+
try:
|
|
1053
|
+
await wait_on_shared_future(flush_task)
|
|
1054
|
+
except asyncio.CancelledError:
|
|
1055
|
+
cancellation_pending = True
|
|
1056
|
+
except Exception:
|
|
1057
|
+
break
|
|
1058
|
+
if not cancellation_pending:
|
|
1059
|
+
# The shield also blocks the level cancellation this block
|
|
1060
|
+
# promises to re-raise after the flush. Probe without
|
|
1061
|
+
# suspending so a disconnected compact request still cancels.
|
|
1062
|
+
try:
|
|
1063
|
+
await checkpoint_if_cancelled()
|
|
1064
|
+
except asyncio.CancelledError:
|
|
1065
|
+
cancellation_pending = True
|
|
1066
|
+
try:
|
|
1067
|
+
flush_task.result()
|
|
1068
|
+
except Exception:
|
|
1069
|
+
logger.warning(
|
|
1070
|
+
"Failed to flush deferred compact account health request_id=%s",
|
|
1071
|
+
request_id,
|
|
1072
|
+
exc_info=True,
|
|
1073
|
+
)
|
|
1074
|
+
if cancellation_pending:
|
|
1075
|
+
raise asyncio.CancelledError()
|
|
1076
|
+
if settlement_error is not None:
|
|
1077
|
+
raise settlement_error
|
|
1078
|
+
|
|
1079
|
+
async def settle_on_terminal_exit() -> None:
|
|
1080
|
+
if settlement_attempted:
|
|
1081
|
+
return
|
|
1082
|
+
try:
|
|
1083
|
+
await settle_compact_usage(
|
|
1084
|
+
api_key=api_key,
|
|
1085
|
+
api_key_reservation=api_key_reservation,
|
|
1086
|
+
response=None,
|
|
1087
|
+
request_service_tier=request_service_tier,
|
|
1088
|
+
)
|
|
1089
|
+
except Exception:
|
|
1090
|
+
logger.warning(
|
|
1091
|
+
"Failed to settle compact reservation after unexpected exit request_id=%s",
|
|
1092
|
+
request_id,
|
|
1093
|
+
exc_info=True,
|
|
1094
|
+
)
|
|
1095
|
+
|
|
1096
|
+
async def record_or_defer_proxy_health(
|
|
1097
|
+
failed_account: Account,
|
|
1098
|
+
failed_exc: ProxyResponseError,
|
|
1099
|
+
) -> None:
|
|
1100
|
+
if api_key is not None and api_key_reservation is not None:
|
|
1101
|
+
deferred_proxy_health.append((failed_account, failed_exc))
|
|
1102
|
+
return
|
|
1103
|
+
await proxy._handle_proxy_error(failed_account, failed_exc)
|
|
1104
|
+
|
|
1105
|
+
async def record_or_defer_permanent_health(
|
|
1106
|
+
failed_account: Account,
|
|
1107
|
+
failed_code: str,
|
|
1108
|
+
) -> None:
|
|
1109
|
+
if api_key is not None and api_key_reservation is not None:
|
|
1110
|
+
deferred_permanent_health.append((failed_account, failed_code))
|
|
1111
|
+
return
|
|
1112
|
+
await proxy._load_balancer.mark_permanent_failure(failed_account, failed_code)
|
|
1113
|
+
|
|
1114
|
+
async def record_or_defer_stream_health(
|
|
1115
|
+
failed_account: Account,
|
|
1116
|
+
failed_error: Any,
|
|
1117
|
+
failed_code: str,
|
|
1118
|
+
failed_status: int | None = None,
|
|
1119
|
+
) -> None:
|
|
1120
|
+
if api_key is not None and api_key_reservation is not None:
|
|
1121
|
+
deferred_stream_health.append((failed_account, failed_error, failed_code, failed_status))
|
|
1122
|
+
return
|
|
1123
|
+
await proxy._handle_stream_error(
|
|
1124
|
+
failed_account,
|
|
1125
|
+
failed_error,
|
|
1126
|
+
failed_code,
|
|
1127
|
+
http_status=failed_status,
|
|
1128
|
+
)
|
|
1129
|
+
|
|
1130
|
+
try:
|
|
1131
|
+
|
|
1132
|
+
async def _call_compact(
|
|
1133
|
+
target: Account,
|
|
1134
|
+
account_response_create_lease: AccountLease | None = None,
|
|
1135
|
+
) -> CompactResponsePayload:
|
|
1136
|
+
nonlocal route_fallback_used, route_mode, route_pool_id, route_endpoint_id
|
|
1137
|
+
access_token = proxy._encryptor.decrypt(target.access_token_encrypted)
|
|
1138
|
+
account_id = _header_account_id(target.chatgpt_account_id)
|
|
1139
|
+
remaining_budget = _remaining_budget_seconds(deadline)
|
|
1140
|
+
if remaining_budget <= 0:
|
|
1141
|
+
logger.warning(
|
|
1142
|
+
"Compact request budget exhausted before upstream call request_id=%s account_id=%s",
|
|
1143
|
+
request_id,
|
|
1144
|
+
target.id,
|
|
1145
|
+
)
|
|
1146
|
+
_raise_proxy_budget_exhausted()
|
|
1147
|
+
create_lease: AdmissionLease | None = None
|
|
1148
|
+
timeout_tokens: Any | None = None
|
|
1149
|
+
try:
|
|
1150
|
+
if account_response_create_lease is None:
|
|
1151
|
+
account_response_create_lease = await proxy._acquire_account_response_create_lease_or_overload(
|
|
1152
|
+
account_id=target.id,
|
|
1153
|
+
request_id=request_id,
|
|
1154
|
+
surface="compact",
|
|
1155
|
+
concurrency_caps=concurrency_caps,
|
|
1156
|
+
)
|
|
1157
|
+
create_lease = await proxy._get_work_admission().acquire_response_create(compact=True)
|
|
1158
|
+
route = await proxy._resolve_upstream_route_for_account(target, operation="compact")
|
|
1159
|
+
remaining_budget = _remaining_budget_seconds(deadline)
|
|
1160
|
+
if remaining_budget <= 0:
|
|
1161
|
+
logger.warning(
|
|
1162
|
+
"Compact request budget exhausted after admission waits request_id=%s account_id=%s",
|
|
1163
|
+
request_id,
|
|
1164
|
+
target.id,
|
|
1165
|
+
)
|
|
1166
|
+
_raise_proxy_budget_exhausted()
|
|
1167
|
+
upstream_budget = _compact_upstream_budget_seconds(remaining_budget)
|
|
1168
|
+
if upstream_budget <= 0:
|
|
1169
|
+
logger.warning(
|
|
1170
|
+
"Compact request budget exhausted before upstream call cap request_id=%s account_id=%s",
|
|
1171
|
+
request_id,
|
|
1172
|
+
target.id,
|
|
1173
|
+
)
|
|
1174
|
+
_raise_proxy_budget_exhausted()
|
|
1175
|
+
timeout_tokens = _service_push_compact_timeout_overrides(
|
|
1176
|
+
connect_timeout_seconds=upstream_budget,
|
|
1177
|
+
total_timeout_seconds=upstream_budget,
|
|
1178
|
+
)
|
|
1179
|
+
if route is not None:
|
|
1180
|
+
route_mode = route.mode
|
|
1181
|
+
route_pool_id = route.pool_id
|
|
1182
|
+
route_endpoint_id = route.endpoint_id
|
|
1183
|
+
route_trace = UpstreamProxyRouteTrace()
|
|
1184
|
+
upstream_started_at = time.monotonic()
|
|
1185
|
+
try:
|
|
1186
|
+
logger.info(
|
|
1187
|
+
"Compact upstream call start request_id=%s account_id=%s timeout_seconds=%.2f "
|
|
1188
|
+
"remaining_budget=%.2f",
|
|
1189
|
+
request_id,
|
|
1190
|
+
target.id,
|
|
1191
|
+
upstream_budget,
|
|
1192
|
+
remaining_budget,
|
|
1193
|
+
)
|
|
1194
|
+
response = await asyncio.wait_for(
|
|
1195
|
+
_call_with_supported_optional_kwargs(
|
|
1196
|
+
_service_core_compact_responses(),
|
|
1197
|
+
payload,
|
|
1198
|
+
filtered,
|
|
1199
|
+
access_token,
|
|
1200
|
+
account_id,
|
|
1201
|
+
optional_kwargs={
|
|
1202
|
+
"route": route,
|
|
1203
|
+
"allow_direct_egress": route is None,
|
|
1204
|
+
"route_trace": route_trace,
|
|
1205
|
+
"chatgpt_account_id": account_id,
|
|
1206
|
+
"synthesize_routing_hint": True,
|
|
1207
|
+
# ``target.id`` is the load-balancer account
|
|
1208
|
+
# id; ``account_id`` above is the wire one.
|
|
1209
|
+
# The scope token hashes the LB id in both
|
|
1210
|
+
# the stream and compact paths.
|
|
1211
|
+
"thread_cache_identity": ThreadCacheIdentity(
|
|
1212
|
+
mode=thread_cache_identity_mode,
|
|
1213
|
+
account_id=target.id,
|
|
1214
|
+
),
|
|
1215
|
+
},
|
|
1216
|
+
),
|
|
1217
|
+
timeout=upstream_budget,
|
|
1218
|
+
)
|
|
1219
|
+
logger.info(
|
|
1220
|
+
"Compact upstream call complete request_id=%s account_id=%s elapsed_seconds=%.2f "
|
|
1221
|
+
"timeout_seconds=%.2f",
|
|
1222
|
+
request_id,
|
|
1223
|
+
target.id,
|
|
1224
|
+
time.monotonic() - upstream_started_at,
|
|
1225
|
+
upstream_budget,
|
|
1226
|
+
)
|
|
1227
|
+
return response
|
|
1228
|
+
except ProxyResponseError as exc:
|
|
1229
|
+
error = _parse_openai_error(exc.payload)
|
|
1230
|
+
code = _normalize_error_code(
|
|
1231
|
+
error.code if error else None,
|
|
1232
|
+
error.type if error else None,
|
|
1233
|
+
)
|
|
1234
|
+
error_message = error.message if error and error.message is not None else ""
|
|
1235
|
+
if (
|
|
1236
|
+
code == "upstream_unavailable"
|
|
1237
|
+
and exc.retryable_same_contract
|
|
1238
|
+
and (
|
|
1239
|
+
exc.failure_exception_type in {"TimeoutError", "ServerTimeoutError"}
|
|
1240
|
+
or "timed out" in error_message.lower()
|
|
1241
|
+
or "timeout" in error_message.lower()
|
|
1242
|
+
)
|
|
1243
|
+
):
|
|
1244
|
+
logger.warning(
|
|
1245
|
+
"Compact inner upstream timeout surfaced request_id=%s account_id=%s "
|
|
1246
|
+
"elapsed_seconds=%.2f timeout_seconds=%.2f",
|
|
1247
|
+
request_id,
|
|
1248
|
+
target.id,
|
|
1249
|
+
time.monotonic() - upstream_started_at,
|
|
1250
|
+
upstream_budget,
|
|
1251
|
+
)
|
|
1252
|
+
raise ProxyResponseError(
|
|
1253
|
+
502,
|
|
1254
|
+
openai_error("upstream_request_timeout", "Compact upstream call timed out"),
|
|
1255
|
+
) from exc
|
|
1256
|
+
raise
|
|
1257
|
+
except asyncio.TimeoutError as exc:
|
|
1258
|
+
logger.warning(
|
|
1259
|
+
"Compact upstream call timed out request_id=%s account_id=%s elapsed_seconds=%.2f "
|
|
1260
|
+
"timeout_seconds=%.2f",
|
|
1261
|
+
request_id,
|
|
1262
|
+
target.id,
|
|
1263
|
+
time.monotonic() - upstream_started_at,
|
|
1264
|
+
upstream_budget,
|
|
1265
|
+
)
|
|
1266
|
+
raise ProxyResponseError(
|
|
1267
|
+
502,
|
|
1268
|
+
openai_error("upstream_request_timeout", "Compact upstream call timed out"),
|
|
1269
|
+
) from exc
|
|
1270
|
+
finally:
|
|
1271
|
+
if route_trace.mode is not None:
|
|
1272
|
+
route_mode = route_trace.mode
|
|
1273
|
+
route_pool_id = route_trace.pool_id
|
|
1274
|
+
route_endpoint_id = route_trace.endpoint_id
|
|
1275
|
+
route_fallback_used = route_trace.fallback_used
|
|
1276
|
+
finally:
|
|
1277
|
+
if create_lease is not None:
|
|
1278
|
+
create_lease.release()
|
|
1279
|
+
await proxy._load_balancer.release_account_lease(account_response_create_lease)
|
|
1280
|
+
if timeout_tokens is not None:
|
|
1281
|
+
_service_pop_compact_timeout_overrides(timeout_tokens)
|
|
1282
|
+
|
|
1283
|
+
last_exc: ProxyResponseError | None = None
|
|
1284
|
+
network_recovery = ProcessNetworkRecovery(transport="compact", request_id=request_id)
|
|
1285
|
+
excluded_account_ids: set[str] = set()
|
|
1286
|
+
# Account-neutral replay off a pinned previous-response owner is only
|
|
1287
|
+
# legal for owner loss the owner's quota state caused: either the
|
|
1288
|
+
# owner was never usable at selection time, or it was excluded
|
|
1289
|
+
# mid-request by a pre-visible quota / rate-limit failure. Post-
|
|
1290
|
+
# selection authentication, refresh, transport, and transient
|
|
1291
|
+
# failures also exclude the owner, and those keep their existing
|
|
1292
|
+
# owner-bound handling instead of moving the history to another
|
|
1293
|
+
# account.
|
|
1294
|
+
owner_quota_failover_eligible = False
|
|
1295
|
+
require_security_work_authorized = False
|
|
1296
|
+
estimated_lease_tokens = _estimated_lease_tokens_from_request_usage_budget(
|
|
1297
|
+
estimate_api_key_request_usage(payload)
|
|
1298
|
+
)
|
|
1299
|
+
for _account_attempt in range(_compact_max_account_attempts()):
|
|
1300
|
+
selection = await proxy._select_account_with_budget_compatible(
|
|
1301
|
+
deadline,
|
|
1302
|
+
request_id=request_id,
|
|
1303
|
+
kind="compact",
|
|
1304
|
+
api_key=api_key,
|
|
1305
|
+
affinity_policy=affinity,
|
|
1306
|
+
prefer_earlier_reset_accounts=prefer_earlier_reset,
|
|
1307
|
+
prefer_earlier_reset_window=_prefer_earlier_reset_window(settings),
|
|
1308
|
+
routing_strategy=routing_strategy,
|
|
1309
|
+
model=payload.model,
|
|
1310
|
+
service_tier=payload.service_tier,
|
|
1311
|
+
exclude_account_ids=excluded_account_ids,
|
|
1312
|
+
preferred_account_id=preferred_account_id,
|
|
1313
|
+
require_security_work_authorized=require_security_work_authorized,
|
|
1314
|
+
lease_kind="response_create",
|
|
1315
|
+
estimated_lease_tokens=estimated_lease_tokens,
|
|
1316
|
+
fallback_on_preferred_account_unavailable=preferred_account_id is None,
|
|
1317
|
+
)
|
|
1318
|
+
account = selection.account
|
|
1319
|
+
if not account:
|
|
1320
|
+
if (
|
|
1321
|
+
require_security_work_authorized
|
|
1322
|
+
and selection.error_code == _no_security_work_authorized_accounts_code()
|
|
1323
|
+
and last_exc is not None
|
|
1324
|
+
):
|
|
1325
|
+
logger.info(
|
|
1326
|
+
"No security-work-authorized account available for compact retry; "
|
|
1327
|
+
"continuing normal account failover request_id=%s",
|
|
1328
|
+
request_id,
|
|
1329
|
+
)
|
|
1330
|
+
require_security_work_authorized = False
|
|
1331
|
+
selection = await proxy._select_account_with_budget_compatible(
|
|
1332
|
+
deadline,
|
|
1333
|
+
request_id=request_id,
|
|
1334
|
+
kind="compact",
|
|
1335
|
+
api_key=api_key,
|
|
1336
|
+
affinity_policy=affinity,
|
|
1337
|
+
prefer_earlier_reset_accounts=prefer_earlier_reset,
|
|
1338
|
+
prefer_earlier_reset_window=_prefer_earlier_reset_window(settings),
|
|
1339
|
+
routing_strategy=routing_strategy,
|
|
1340
|
+
model=payload.model,
|
|
1341
|
+
service_tier=payload.service_tier,
|
|
1342
|
+
exclude_account_ids=excluded_account_ids,
|
|
1343
|
+
preferred_account_id=preferred_account_id,
|
|
1344
|
+
require_security_work_authorized=False,
|
|
1345
|
+
lease_kind="response_create",
|
|
1346
|
+
estimated_lease_tokens=estimated_lease_tokens,
|
|
1347
|
+
fallback_on_preferred_account_unavailable=preferred_account_id is None,
|
|
1348
|
+
)
|
|
1349
|
+
account = selection.account
|
|
1350
|
+
if (
|
|
1351
|
+
account is None
|
|
1352
|
+
and previous_response_preferred_account_id is not None
|
|
1353
|
+
and preferred_account_id == previous_response_preferred_account_id
|
|
1354
|
+
):
|
|
1355
|
+
# Narrowed alias: the structural gate above proves the
|
|
1356
|
+
# selection pin names the previous-response owner.
|
|
1357
|
+
unavailable_owner_account_id = previous_response_preferred_account_id
|
|
1358
|
+
# The pinned previous-response owner cannot be selected.
|
|
1359
|
+
# A full resend that is provably account-neutral on the
|
|
1360
|
+
# wire needs nothing from the owner, so the stale anchor
|
|
1361
|
+
# can be dropped and the compact can move to a healthy
|
|
1362
|
+
# account instead of wedging the session until the
|
|
1363
|
+
# owner's quota window resets — the same selection-time
|
|
1364
|
+
# escape normal turns already have. Turn-state and file
|
|
1365
|
+
# pins keep the request owner-bound (the first blocked
|
|
1366
|
+
# reason below), but their selection failure still
|
|
1367
|
+
# records the fail-closed outcome on the common path.
|
|
1368
|
+
recovery_blocked_reason: str | None = None
|
|
1369
|
+
if turn_state_owner_account_id is not None or rewritten_file_account_id is not None:
|
|
1370
|
+
# The previous-response owner is also pinned by a
|
|
1371
|
+
# turn-state or input-file owner. Those pins are
|
|
1372
|
+
# account ownership this recovery must never move,
|
|
1373
|
+
# so the request stays owner-bound regardless of
|
|
1374
|
+
# the owner's quota state — but the unavailable
|
|
1375
|
+
# owner still fails closed and must be recorded.
|
|
1376
|
+
recovery_blocked_reason = "additional_owner_pins"
|
|
1377
|
+
elif previous_response_lookup_session_id is not None:
|
|
1378
|
+
# A session/turn-state identity on the request can
|
|
1379
|
+
# bind live or durable HTTP-bridge continuity rows
|
|
1380
|
+
# that still name the lost owner. Without the
|
|
1381
|
+
# rebinding machinery this recovery deliberately
|
|
1382
|
+
# avoids, moving the history would strand that
|
|
1383
|
+
# continuity, so session-scoped requests stay
|
|
1384
|
+
# owner-bound.
|
|
1385
|
+
recovery_blocked_reason = "session_scoped_continuity"
|
|
1386
|
+
elif (
|
|
1387
|
+
affinity.kind == StickySessionKind.CODEX_SESSION
|
|
1388
|
+
or affinity.legacy_selection_key is not None
|
|
1389
|
+
or affinity.require_unambiguous_account
|
|
1390
|
+
):
|
|
1391
|
+
# CODEX_SESSION affinity (turn-state, thread, or
|
|
1392
|
+
# session-header keys, including raw legacy rows)
|
|
1393
|
+
# is session ownership this recovery would have to
|
|
1394
|
+
# rebind, so those requests stay owner-bound.
|
|
1395
|
+
# PROMPT_CACHE / STICKY_THREAD keys are soft cache
|
|
1396
|
+
# locality the sticky selection path already falls
|
|
1397
|
+
# back from on an unavailable account — the compact
|
|
1398
|
+
# routes derive one unconditionally — so they gate
|
|
1399
|
+
# nothing here and the recovery reselection flows
|
|
1400
|
+
# through that same existing sticky handling.
|
|
1401
|
+
recovery_blocked_reason = "session_affinity"
|
|
1402
|
+
elif (
|
|
1403
|
+
not owner_quota_failover_eligible
|
|
1404
|
+
and selection.error_code == "preferred_account_unavailable"
|
|
1405
|
+
):
|
|
1406
|
+
# The selector skipped the owner before evaluating
|
|
1407
|
+
# its availability (API-key assignment scope,
|
|
1408
|
+
# single-account routing, or an in-request
|
|
1409
|
+
# exclusion that was not a pre-visible quota
|
|
1410
|
+
# failover). Policy-caused loss must not become
|
|
1411
|
+
# replay-eligible just because the owner's
|
|
1412
|
+
# persisted status happens to be quota-exhausted.
|
|
1413
|
+
recovery_blocked_reason = "owner_skipped_by_policy"
|
|
1414
|
+
elif not (
|
|
1415
|
+
owner_quota_failover_eligible
|
|
1416
|
+
or (
|
|
1417
|
+
unavailable_owner_account_id not in excluded_account_ids
|
|
1418
|
+
and await proxy._compact_owner_selection_loss_is_quota_caused(
|
|
1419
|
+
unavailable_owner_account_id
|
|
1420
|
+
)
|
|
1421
|
+
)
|
|
1422
|
+
):
|
|
1423
|
+
recovery_blocked_reason = "non_quota_owner_loss"
|
|
1424
|
+
replay_payload: ResponsesCompactRequest | None = None
|
|
1425
|
+
if recovery_blocked_reason is None:
|
|
1426
|
+
replay_payload = _compact_account_neutral_replay_payload(payload)
|
|
1427
|
+
if replay_payload is None:
|
|
1428
|
+
recovery_blocked_reason = "history_not_account_neutral"
|
|
1429
|
+
if replay_payload is None:
|
|
1430
|
+
logger.info(
|
|
1431
|
+
"Compact previous-response owner unavailable; staying owner-bound "
|
|
1432
|
+
"request_id=%s owner_account_id=%s blocked_reason=%s selection_error_code=%s",
|
|
1433
|
+
request_id,
|
|
1434
|
+
preferred_account_id,
|
|
1435
|
+
recovery_blocked_reason,
|
|
1436
|
+
selection.error_code,
|
|
1437
|
+
)
|
|
1438
|
+
_record_continuity_fail_closed(
|
|
1439
|
+
surface="compact",
|
|
1440
|
+
reason="owner_account_unavailable",
|
|
1441
|
+
previous_response_id=previous_response_id
|
|
1442
|
+
if isinstance(previous_response_id, str)
|
|
1443
|
+
else None,
|
|
1444
|
+
session_id=previous_response_lookup_session_id,
|
|
1445
|
+
upstream_error_code=selection.error_code,
|
|
1446
|
+
)
|
|
1447
|
+
else:
|
|
1448
|
+
logger.warning(
|
|
1449
|
+
"Compact previous-response owner unavailable; replaying verified "
|
|
1450
|
+
"account-neutral full resend request_id=%s owner_account_id=%s "
|
|
1451
|
+
"selection_error_code=%s",
|
|
1452
|
+
request_id,
|
|
1453
|
+
preferred_account_id,
|
|
1454
|
+
selection.error_code,
|
|
1455
|
+
)
|
|
1456
|
+
excluded_account_ids.add(unavailable_owner_account_id)
|
|
1457
|
+
payload = replay_payload
|
|
1458
|
+
filtered = without_http_bridge_session_affinity_headers(filtered)
|
|
1459
|
+
preferred_account_id = None
|
|
1460
|
+
previous_response_preferred_account_id = None
|
|
1461
|
+
selection = await proxy._select_account_with_budget_compatible(
|
|
1462
|
+
deadline,
|
|
1463
|
+
request_id=request_id,
|
|
1464
|
+
kind="compact",
|
|
1465
|
+
api_key=api_key,
|
|
1466
|
+
affinity_policy=affinity,
|
|
1467
|
+
prefer_earlier_reset_accounts=prefer_earlier_reset,
|
|
1468
|
+
prefer_earlier_reset_window=_prefer_earlier_reset_window(settings),
|
|
1469
|
+
routing_strategy=routing_strategy,
|
|
1470
|
+
model=payload.model,
|
|
1471
|
+
service_tier=payload.service_tier,
|
|
1472
|
+
exclude_account_ids=excluded_account_ids,
|
|
1473
|
+
preferred_account_id=None,
|
|
1474
|
+
require_security_work_authorized=require_security_work_authorized,
|
|
1475
|
+
lease_kind="response_create",
|
|
1476
|
+
estimated_lease_tokens=estimated_lease_tokens,
|
|
1477
|
+
fallback_on_preferred_account_unavailable=True,
|
|
1478
|
+
)
|
|
1479
|
+
account = selection.account
|
|
1480
|
+
if account is not None:
|
|
1481
|
+
pass
|
|
1482
|
+
elif last_exc is not None:
|
|
1483
|
+
break
|
|
1484
|
+
else:
|
|
1485
|
+
log_error_code = selection.error_code or "no_accounts"
|
|
1486
|
+
log_error_message = selection.error_message or "No active accounts available"
|
|
1487
|
+
status_code, error_payload = selection_failure_response(selection)
|
|
1488
|
+
raise ProxyResponseError(
|
|
1489
|
+
status_code,
|
|
1490
|
+
error_payload,
|
|
1491
|
+
)
|
|
1492
|
+
assert account is not None
|
|
1493
|
+
account_id_value = account.id
|
|
1494
|
+
selected_account_response_create_lease = selection.lease
|
|
1495
|
+
remaining_budget = _remaining_budget_seconds(deadline)
|
|
1496
|
+
if remaining_budget <= 0:
|
|
1497
|
+
logger.warning("Compact request budget exhausted before freshness check request_id=%s", request_id)
|
|
1498
|
+
await proxy._load_balancer.release_account_lease(selected_account_response_create_lease)
|
|
1499
|
+
# This budget-exhausted terminal exits compact_responses before
|
|
1500
|
+
# reaching the retry loop's settle sites, so on the HTTP bridge /
|
|
1501
|
+
# forwarded path (``owns_reservation`` false, ``compact_responses``
|
|
1502
|
+
# is the sole settler) the API-key reservation would leak held
|
|
1503
|
+
# quota. Settle BEFORE raising, mirroring the transport/permanent
|
|
1504
|
+
# preflight branches above.
|
|
1505
|
+
await settle_compact_usage(
|
|
1506
|
+
api_key=api_key,
|
|
1507
|
+
api_key_reservation=api_key_reservation,
|
|
1508
|
+
response=None,
|
|
1509
|
+
request_service_tier=request_service_tier,
|
|
1510
|
+
)
|
|
1511
|
+
_raise_proxy_budget_exhausted()
|
|
1512
|
+
freshness_budget = _compact_freshness_budget_seconds(remaining_budget)
|
|
1513
|
+
if freshness_budget <= 0:
|
|
1514
|
+
logger.warning(
|
|
1515
|
+
"Compact request budget exhausted before freshness check reserve request_id=%s "
|
|
1516
|
+
"remaining_budget=%.2f",
|
|
1517
|
+
request_id,
|
|
1518
|
+
remaining_budget,
|
|
1519
|
+
)
|
|
1520
|
+
await proxy._load_balancer.release_account_lease(selected_account_response_create_lease)
|
|
1521
|
+
# Sole-settler leak guard (see above): settle the reservation
|
|
1522
|
+
# before this budget-exhausted terminal raise.
|
|
1523
|
+
await settle_compact_usage(
|
|
1524
|
+
api_key=api_key,
|
|
1525
|
+
api_key_reservation=api_key_reservation,
|
|
1526
|
+
response=None,
|
|
1527
|
+
request_service_tier=request_service_tier,
|
|
1528
|
+
)
|
|
1529
|
+
_raise_proxy_budget_exhausted()
|
|
1530
|
+
try:
|
|
1531
|
+
logger.info(
|
|
1532
|
+
"Compact freshness start request_id=%s account_id=%s timeout_seconds=%.2f "
|
|
1533
|
+
"remaining_budget=%.2f",
|
|
1534
|
+
request_id,
|
|
1535
|
+
account.id,
|
|
1536
|
+
freshness_budget,
|
|
1537
|
+
remaining_budget,
|
|
1538
|
+
)
|
|
1539
|
+
account = await proxy._ensure_fresh_with_budget(account, timeout_seconds=freshness_budget)
|
|
1540
|
+
logger.info(
|
|
1541
|
+
"Compact freshness complete request_id=%s account_id=%s",
|
|
1542
|
+
request_id,
|
|
1543
|
+
account.id,
|
|
1544
|
+
)
|
|
1545
|
+
except ProxyResponseError:
|
|
1546
|
+
await proxy._load_balancer.release_account_lease(selected_account_response_create_lease)
|
|
1547
|
+
selected_account_response_create_lease = None
|
|
1548
|
+
# ensure_fresh_with_budget translates terminal process-network
|
|
1549
|
+
# recovery outcomes before the compact upstream settlement
|
|
1550
|
+
# branches run, so this boundary owns reservation cleanup.
|
|
1551
|
+
await settle_compact_usage(
|
|
1552
|
+
api_key=api_key,
|
|
1553
|
+
api_key_reservation=api_key_reservation,
|
|
1554
|
+
response=None,
|
|
1555
|
+
request_service_tier=request_service_tier,
|
|
1556
|
+
)
|
|
1557
|
+
raise
|
|
1558
|
+
except (RefreshError, aiohttp.ClientError, asyncio.TimeoutError) as exc:
|
|
1559
|
+
await proxy._load_balancer.release_account_lease(selected_account_response_create_lease)
|
|
1560
|
+
selected_account_response_create_lease = None
|
|
1561
|
+
if isinstance(exc, RefreshError):
|
|
1562
|
+
if exc.is_permanent:
|
|
1563
|
+
# Permanent refresh failures keep their prior
|
|
1564
|
+
# escalation (they propagate to the caller). On the
|
|
1565
|
+
# HTTP bridge / forwarded path the caller passes an
|
|
1566
|
+
# ``api_key_reservation_override`` with
|
|
1567
|
+
# ``owns_reservation`` false, so ``compact_responses``
|
|
1568
|
+
# is the sole settler; settle BEFORE raising so the
|
|
1569
|
+
# reservation is finalized instead of leaking held
|
|
1570
|
+
# API-key quota (matching the post-401 permanent
|
|
1571
|
+
# branch, which settles before re-raising).
|
|
1572
|
+
await settle_compact_usage(
|
|
1573
|
+
api_key=api_key,
|
|
1574
|
+
api_key_reservation=api_key_reservation,
|
|
1575
|
+
response=None,
|
|
1576
|
+
request_service_tier=request_service_tier,
|
|
1577
|
+
)
|
|
1578
|
+
raise
|
|
1579
|
+
if is_transient_refresh_contention(exc):
|
|
1580
|
+
# Transient CROSS-REPLICA refresh contention: benign
|
|
1581
|
+
# claim contention (``refresh_claim_timeout``: the
|
|
1582
|
+
# account's refresh claim is held by another replica) OR
|
|
1583
|
+
# a post-exchange persist/status CAS conflict
|
|
1584
|
+
# (``token_persist_conflict`` / ``status_downgrade_conflict``).
|
|
1585
|
+
# This is NOT a genuine ``transport_error`` OAuth failure
|
|
1586
|
+
# — the account's credentials are healthy — so it fails
|
|
1587
|
+
# over WITHOUT an account-health penalty. Unlike the
|
|
1588
|
+
# genuine transport-level failure handled below, do NOT
|
|
1589
|
+
# record ``_handle_stream_error`` here: that would push an
|
|
1590
|
+
# otherwise-healthy account into backoff for normal
|
|
1591
|
+
# cross-replica contention. Surface a retryable
|
|
1592
|
+
# ``upstream_unavailable`` on exhaustion (``last_exc``).
|
|
1593
|
+
message = exc.message or str(exc) or "Request to upstream timed out"
|
|
1594
|
+
if refresh_contention_kind(exc) == "persist_conflict":
|
|
1595
|
+
logger.warning(
|
|
1596
|
+
"Compact refresh post-exchange persist conflict code=%s "
|
|
1597
|
+
"request_id=%s account_id=%s",
|
|
1598
|
+
exc.code,
|
|
1599
|
+
request_id,
|
|
1600
|
+
account.id,
|
|
1601
|
+
exc_info=True,
|
|
1602
|
+
)
|
|
1603
|
+
else:
|
|
1604
|
+
logger.warning(
|
|
1605
|
+
"Compact refresh claim contention request_id=%s account_id=%s",
|
|
1606
|
+
request_id,
|
|
1607
|
+
account.id,
|
|
1608
|
+
exc_info=True,
|
|
1609
|
+
)
|
|
1610
|
+
if preferred_account_id is not None:
|
|
1611
|
+
# File/previous-response-pinned requests cannot
|
|
1612
|
+
# fail over. On the HTTP bridge / forwarded path
|
|
1613
|
+
# the caller passes an ``api_key_reservation_override``
|
|
1614
|
+
# with ``owns_reservation`` false, making
|
|
1615
|
+
# ``compact_responses`` responsible for settling the
|
|
1616
|
+
# reservation. Settle it BEFORE raising so the
|
|
1617
|
+
# API-key reservation is finalized instead of leaking
|
|
1618
|
+
# held quota when the pinned refresh claim times out.
|
|
1619
|
+
await settle_compact_usage(
|
|
1620
|
+
api_key=api_key,
|
|
1621
|
+
api_key_reservation=api_key_reservation,
|
|
1622
|
+
response=None,
|
|
1623
|
+
request_service_tier=request_service_tier,
|
|
1624
|
+
)
|
|
1625
|
+
_raise_proxy_unavailable(message)
|
|
1626
|
+
last_exc = ProxyResponseError(502, openai_error("upstream_unavailable", message))
|
|
1627
|
+
excluded_account_ids.add(account.id)
|
|
1628
|
+
continue
|
|
1629
|
+
# A GENUINE OAuth transport failure (``code == "transport_error"``:
|
|
1630
|
+
# the refresh request itself timed out / its upstream
|
|
1631
|
+
# connection failed). This IS the account/route's fault, so
|
|
1632
|
+
# it falls through to the shared transport-failure handling
|
|
1633
|
+
# below — identical to a raw aiohttp/connect failure — which
|
|
1634
|
+
# records the account-health penalty (``_handle_stream_error``)
|
|
1635
|
+
# so a persistently broken account backs off instead of
|
|
1636
|
+
# being kept healthy and reselected on the next request.
|
|
1637
|
+
message = getattr(exc, "message", None) or str(exc) or "Request to upstream timed out"
|
|
1638
|
+
logger.warning(
|
|
1639
|
+
"Compact refresh/connect failed request_id=%s account_id=%s",
|
|
1640
|
+
request_id,
|
|
1641
|
+
account.id,
|
|
1642
|
+
exc_info=True,
|
|
1643
|
+
)
|
|
1644
|
+
# Both terminal (non-failover) transport-failure raises below
|
|
1645
|
+
# exit compact_responses without reaching the retry loop's
|
|
1646
|
+
# settle sites, so on the HTTP bridge / forwarded path
|
|
1647
|
+
# (owns_reservation false, compact_responses is the sole
|
|
1648
|
+
# settler) the API-key reservation would leak held quota.
|
|
1649
|
+
# Settle BEFORE raising, mirroring the claim-contention and
|
|
1650
|
+
# post-401 transport branches.
|
|
1651
|
+
if not _should_retry_transient_stream_error("upstream_unavailable", message):
|
|
1652
|
+
await settle_compact_usage(
|
|
1653
|
+
api_key=api_key,
|
|
1654
|
+
api_key_reservation=api_key_reservation,
|
|
1655
|
+
response=None,
|
|
1656
|
+
request_service_tier=request_service_tier,
|
|
1657
|
+
)
|
|
1658
|
+
_raise_proxy_unavailable(message)
|
|
1659
|
+
if preferred_account_id is not None:
|
|
1660
|
+
await settle_compact_usage(
|
|
1661
|
+
api_key=api_key,
|
|
1662
|
+
api_key_reservation=api_key_reservation,
|
|
1663
|
+
response=None,
|
|
1664
|
+
request_service_tier=request_service_tier,
|
|
1665
|
+
)
|
|
1666
|
+
_raise_proxy_unavailable(message)
|
|
1667
|
+
await record_or_defer_stream_health(
|
|
1668
|
+
account,
|
|
1669
|
+
{"message": message},
|
|
1670
|
+
"upstream_unavailable",
|
|
1671
|
+
)
|
|
1672
|
+
last_exc = ProxyResponseError(502, openai_error("upstream_unavailable", message))
|
|
1673
|
+
excluded_account_ids.add(account.id)
|
|
1674
|
+
continue
|
|
1675
|
+
except BaseException:
|
|
1676
|
+
await proxy._load_balancer.release_account_lease(selected_account_response_create_lease)
|
|
1677
|
+
selected_account_response_create_lease = None
|
|
1678
|
+
raise
|
|
1679
|
+
remaining_budget = _remaining_budget_seconds(deadline)
|
|
1680
|
+
if remaining_budget <= 0:
|
|
1681
|
+
logger.warning(
|
|
1682
|
+
"Compact request budget exhausted after freshness check request_id=%s account_id=%s",
|
|
1683
|
+
request_id,
|
|
1684
|
+
account.id,
|
|
1685
|
+
)
|
|
1686
|
+
await proxy._load_balancer.release_account_lease(selected_account_response_create_lease)
|
|
1687
|
+
# Sole-settler leak guard (see above): settle the reservation
|
|
1688
|
+
# before this budget-exhausted terminal raise.
|
|
1689
|
+
await settle_compact_usage(
|
|
1690
|
+
api_key=api_key,
|
|
1691
|
+
api_key_reservation=api_key_reservation,
|
|
1692
|
+
response=None,
|
|
1693
|
+
request_service_tier=request_service_tier,
|
|
1694
|
+
)
|
|
1695
|
+
_raise_proxy_budget_exhausted()
|
|
1696
|
+
request_service_tier = _service_tier_from_compact_payload(payload)
|
|
1697
|
+
|
|
1698
|
+
safe_retry_budget = _compact_same_contract_retry_budget()
|
|
1699
|
+
transient_retries = 0
|
|
1700
|
+
refresh_retry_used = False
|
|
1701
|
+
transient_exhausted = False
|
|
1702
|
+
while True:
|
|
1703
|
+
try:
|
|
1704
|
+
account_response_create_lease = selected_account_response_create_lease
|
|
1705
|
+
selected_account_response_create_lease = None
|
|
1706
|
+
response = await _call_compact(account, account_response_create_lease)
|
|
1707
|
+
network_recovery.log_recovered()
|
|
1708
|
+
actual_service_tier = _service_tier_from_response(response)
|
|
1709
|
+
await proxy._load_balancer.record_success(account)
|
|
1710
|
+
await settle_compact_usage(
|
|
1711
|
+
api_key=api_key,
|
|
1712
|
+
api_key_reservation=api_key_reservation,
|
|
1713
|
+
response=response,
|
|
1714
|
+
request_service_tier=request_service_tier,
|
|
1715
|
+
)
|
|
1716
|
+
log_status = "success"
|
|
1717
|
+
return response
|
|
1718
|
+
except ProxyResponseError as exc:
|
|
1719
|
+
if exc.failure_phase == "usage_settlement":
|
|
1720
|
+
raise
|
|
1721
|
+
compact_continuity_error = _compact_previous_response_not_found_error(exc)
|
|
1722
|
+
if compact_continuity_error is not None:
|
|
1723
|
+
await settle_compact_usage(
|
|
1724
|
+
api_key=api_key,
|
|
1725
|
+
api_key_reservation=api_key_reservation,
|
|
1726
|
+
response=None,
|
|
1727
|
+
request_service_tier=request_service_tier,
|
|
1728
|
+
)
|
|
1729
|
+
_record_continuity_fail_closed(
|
|
1730
|
+
surface="compact",
|
|
1731
|
+
reason="previous_response_not_found",
|
|
1732
|
+
previous_response_id=None,
|
|
1733
|
+
session_id=_owner_lookup_session_id_from_headers(headers),
|
|
1734
|
+
upstream_error_code=_proxy_response_error_code(exc),
|
|
1735
|
+
)
|
|
1736
|
+
raise compact_continuity_error from exc
|
|
1737
|
+
if exc.status_code == 401:
|
|
1738
|
+
if refresh_retry_used:
|
|
1739
|
+
try:
|
|
1740
|
+
await record_or_defer_proxy_health(account, exc)
|
|
1741
|
+
except Exception:
|
|
1742
|
+
await settle_compact_usage(
|
|
1743
|
+
api_key=api_key,
|
|
1744
|
+
api_key_reservation=api_key_reservation,
|
|
1745
|
+
response=None,
|
|
1746
|
+
request_service_tier=request_service_tier,
|
|
1747
|
+
)
|
|
1748
|
+
raise
|
|
1749
|
+
last_exc = exc
|
|
1750
|
+
excluded_account_ids.add(account.id)
|
|
1751
|
+
transient_exhausted = True
|
|
1752
|
+
break
|
|
1753
|
+
try:
|
|
1754
|
+
remaining_budget = _remaining_budget_seconds(deadline)
|
|
1755
|
+
if remaining_budget <= 0:
|
|
1756
|
+
logger.warning(
|
|
1757
|
+
"Compact request budget exhausted before forced refresh retry request_id=%s "
|
|
1758
|
+
"account_id=%s",
|
|
1759
|
+
request_id,
|
|
1760
|
+
account.id,
|
|
1761
|
+
)
|
|
1762
|
+
# Sole-settler leak guard (see above): this
|
|
1763
|
+
# budget-exhausted terminal exits the retry loop
|
|
1764
|
+
# to the outer handler without settling, so on
|
|
1765
|
+
# the bridge/forwarded path (``owns_reservation``
|
|
1766
|
+
# false) the reservation would leak held quota.
|
|
1767
|
+
# Settle BEFORE raising.
|
|
1768
|
+
await settle_compact_usage(
|
|
1769
|
+
api_key=api_key,
|
|
1770
|
+
api_key_reservation=api_key_reservation,
|
|
1771
|
+
response=None,
|
|
1772
|
+
request_service_tier=request_service_tier,
|
|
1773
|
+
)
|
|
1774
|
+
_raise_proxy_budget_exhausted()
|
|
1775
|
+
account = await proxy._ensure_fresh_with_budget(
|
|
1776
|
+
account,
|
|
1777
|
+
force=True,
|
|
1778
|
+
timeout_seconds=_compact_freshness_budget_seconds(remaining_budget),
|
|
1779
|
+
)
|
|
1780
|
+
except ProxyResponseError:
|
|
1781
|
+
# A translated refresh-recovery error escapes the
|
|
1782
|
+
# current upstream-error handler, so settle before
|
|
1783
|
+
# handing it to the request-level error boundary.
|
|
1784
|
+
await settle_compact_usage(
|
|
1785
|
+
api_key=api_key,
|
|
1786
|
+
api_key_reservation=api_key_reservation,
|
|
1787
|
+
response=None,
|
|
1788
|
+
request_service_tier=request_service_tier,
|
|
1789
|
+
)
|
|
1790
|
+
raise
|
|
1791
|
+
except (RefreshError, aiohttp.ClientError, asyncio.TimeoutError) as refresh_exc:
|
|
1792
|
+
if isinstance(refresh_exc, RefreshError):
|
|
1793
|
+
if refresh_exc.is_permanent:
|
|
1794
|
+
if preferred_account_id is None:
|
|
1795
|
+
# This compact request is not bound to an
|
|
1796
|
+
# account-owned response, turn state, or
|
|
1797
|
+
# file. Retire the revoked account and
|
|
1798
|
+
# continue the same pre-visible request on
|
|
1799
|
+
# another healthy account. For API-key
|
|
1800
|
+
# requests the health write is deferred
|
|
1801
|
+
# until after usage settlement.
|
|
1802
|
+
await record_or_defer_permanent_health(
|
|
1803
|
+
account,
|
|
1804
|
+
refresh_exc.code,
|
|
1805
|
+
)
|
|
1806
|
+
last_exc = exc
|
|
1807
|
+
excluded_account_ids.add(account.id)
|
|
1808
|
+
transient_exhausted = True
|
|
1809
|
+
break
|
|
1810
|
+
await settle_compact_usage(
|
|
1811
|
+
api_key=api_key,
|
|
1812
|
+
api_key_reservation=api_key_reservation,
|
|
1813
|
+
response=None,
|
|
1814
|
+
request_service_tier=request_service_tier,
|
|
1815
|
+
)
|
|
1816
|
+
await proxy._load_balancer.mark_permanent_failure(account, refresh_exc.code)
|
|
1817
|
+
raise exc
|
|
1818
|
+
if is_transient_refresh_contention(refresh_exc):
|
|
1819
|
+
# Transient CROSS-REPLICA refresh contention
|
|
1820
|
+
# on the post-401 forced refresh: benign
|
|
1821
|
+
# claim contention (``refresh_claim_timeout``)
|
|
1822
|
+
# OR a post-exchange persist/status CAS
|
|
1823
|
+
# conflict (``token_persist_conflict`` /
|
|
1824
|
+
# ``status_downgrade_conflict``). This is NOT
|
|
1825
|
+
# a genuine ``transport_error`` OAuth failure
|
|
1826
|
+
# — the account's credentials are healthy — so
|
|
1827
|
+
# fail over WITHOUT an account-health penalty
|
|
1828
|
+
# and surface a retryable
|
|
1829
|
+
# ``upstream_unavailable`` on exhaustion
|
|
1830
|
+
# instead of the misleading original 401. Do
|
|
1831
|
+
# NOT record ``_handle_stream_error`` here
|
|
1832
|
+
# (that is reserved for the genuine transport
|
|
1833
|
+
# failure handled below).
|
|
1834
|
+
message = (
|
|
1835
|
+
refresh_exc.message or str(refresh_exc) or "Request to upstream timed out"
|
|
1836
|
+
)
|
|
1837
|
+
if refresh_contention_kind(refresh_exc) == "persist_conflict":
|
|
1838
|
+
logger.warning(
|
|
1839
|
+
"Compact forced refresh post-exchange persist conflict code=%s "
|
|
1840
|
+
"request_id=%s account_id=%s",
|
|
1841
|
+
refresh_exc.code,
|
|
1842
|
+
request_id,
|
|
1843
|
+
account.id,
|
|
1844
|
+
exc_info=True,
|
|
1845
|
+
)
|
|
1846
|
+
else:
|
|
1847
|
+
logger.warning(
|
|
1848
|
+
"Compact forced refresh claim contention request_id=%s account_id=%s",
|
|
1849
|
+
request_id,
|
|
1850
|
+
account.id,
|
|
1851
|
+
exc_info=True,
|
|
1852
|
+
)
|
|
1853
|
+
if preferred_account_id is not None:
|
|
1854
|
+
await settle_compact_usage(
|
|
1855
|
+
api_key=api_key,
|
|
1856
|
+
api_key_reservation=api_key_reservation,
|
|
1857
|
+
response=None,
|
|
1858
|
+
request_service_tier=request_service_tier,
|
|
1859
|
+
)
|
|
1860
|
+
_raise_proxy_unavailable(message)
|
|
1861
|
+
last_exc = ProxyResponseError(
|
|
1862
|
+
502, openai_error("upstream_unavailable", message)
|
|
1863
|
+
)
|
|
1864
|
+
excluded_account_ids.add(account.id)
|
|
1865
|
+
transient_exhausted = True
|
|
1866
|
+
break
|
|
1867
|
+
if not refresh_exc.transport_error:
|
|
1868
|
+
# Non-transport, non-permanent RefreshError
|
|
1869
|
+
# keeps its prior escalation: re-raise the
|
|
1870
|
+
# original 401 to the caller.
|
|
1871
|
+
await settle_compact_usage(
|
|
1872
|
+
api_key=api_key,
|
|
1873
|
+
api_key_reservation=api_key_reservation,
|
|
1874
|
+
response=None,
|
|
1875
|
+
request_service_tier=request_service_tier,
|
|
1876
|
+
)
|
|
1877
|
+
raise exc
|
|
1878
|
+
# A GENUINE OAuth transport failure
|
|
1879
|
+
# (``code == "transport_error"``): the account/
|
|
1880
|
+
# route is at fault, so it falls through to the
|
|
1881
|
+
# shared transport-failure handling below —
|
|
1882
|
+
# identical to a raw aiohttp/connect failure —
|
|
1883
|
+
# which records the account-health penalty
|
|
1884
|
+
# (``_handle_stream_error``) so the broken
|
|
1885
|
+
# account backs off instead of being reselected.
|
|
1886
|
+
message = getattr(refresh_exc, "message", None) or str(refresh_exc)
|
|
1887
|
+
message = message or "Request to upstream timed out"
|
|
1888
|
+
logger.warning(
|
|
1889
|
+
"Compact forced refresh/connect failed request_id=%s account_id=%s",
|
|
1890
|
+
request_id,
|
|
1891
|
+
account.id,
|
|
1892
|
+
exc_info=True,
|
|
1893
|
+
)
|
|
1894
|
+
if not _should_retry_transient_stream_error("upstream_unavailable", message):
|
|
1895
|
+
await settle_compact_usage(
|
|
1896
|
+
api_key=api_key,
|
|
1897
|
+
api_key_reservation=api_key_reservation,
|
|
1898
|
+
response=None,
|
|
1899
|
+
request_service_tier=request_service_tier,
|
|
1900
|
+
)
|
|
1901
|
+
_raise_proxy_unavailable(message)
|
|
1902
|
+
if preferred_account_id is not None:
|
|
1903
|
+
await settle_compact_usage(
|
|
1904
|
+
api_key=api_key,
|
|
1905
|
+
api_key_reservation=api_key_reservation,
|
|
1906
|
+
response=None,
|
|
1907
|
+
request_service_tier=request_service_tier,
|
|
1908
|
+
)
|
|
1909
|
+
_raise_proxy_unavailable(message)
|
|
1910
|
+
await record_or_defer_stream_health(
|
|
1911
|
+
account,
|
|
1912
|
+
{"message": message},
|
|
1913
|
+
"upstream_unavailable",
|
|
1914
|
+
)
|
|
1915
|
+
last_exc = ProxyResponseError(502, openai_error("upstream_unavailable", message))
|
|
1916
|
+
excluded_account_ids.add(account.id)
|
|
1917
|
+
transient_exhausted = True
|
|
1918
|
+
break
|
|
1919
|
+
refresh_retry_used = True
|
|
1920
|
+
continue
|
|
1921
|
+
if exc.status_code == 500:
|
|
1922
|
+
transient_retries += 1
|
|
1923
|
+
if (
|
|
1924
|
+
transient_retries < _max_transient_same_account_retries()
|
|
1925
|
+
and _remaining_budget_seconds(deadline) > 0
|
|
1926
|
+
):
|
|
1927
|
+
delay = backoff_seconds(transient_retries)
|
|
1928
|
+
logger.info(
|
|
1929
|
+
"Transient compact error, retrying same account "
|
|
1930
|
+
"request_id=%s account_id=%s retry=%s/%s delay=%.2fs",
|
|
1931
|
+
request_id,
|
|
1932
|
+
account.id,
|
|
1933
|
+
transient_retries,
|
|
1934
|
+
_max_transient_same_account_retries(),
|
|
1935
|
+
delay,
|
|
1936
|
+
)
|
|
1937
|
+
await asyncio.sleep(delay)
|
|
1938
|
+
continue
|
|
1939
|
+
# Exhausted same-account transient retries — penalize and failover
|
|
1940
|
+
logger.warning(
|
|
1941
|
+
"Compact transient retries exhausted for account "
|
|
1942
|
+
"request_id=%s account_id=%s retries=%s code=server_error",
|
|
1943
|
+
request_id,
|
|
1944
|
+
account.id,
|
|
1945
|
+
transient_retries,
|
|
1946
|
+
)
|
|
1947
|
+
if api_key is not None and api_key_reservation is not None:
|
|
1948
|
+
deferred_http_500_health.append((account, exc, transient_retries - 1))
|
|
1949
|
+
else:
|
|
1950
|
+
await proxy._handle_proxy_error(account, exc)
|
|
1951
|
+
# Record remaining errors so total equals transient_retries,
|
|
1952
|
+
# meeting the load balancer backoff threshold (error_count >= 3).
|
|
1953
|
+
await proxy._load_balancer.record_errors(account, transient_retries - 1)
|
|
1954
|
+
last_exc = exc
|
|
1955
|
+
excluded_account_ids.add(account.id)
|
|
1956
|
+
transient_exhausted = True
|
|
1957
|
+
break # break inner loop → outer loop tries different account
|
|
1958
|
+
error = _parse_openai_error(exc.payload)
|
|
1959
|
+
code = _normalize_error_code(
|
|
1960
|
+
error.code if error else None,
|
|
1961
|
+
error.type if error else None,
|
|
1962
|
+
)
|
|
1963
|
+
error_message = error.message if error else None
|
|
1964
|
+
network_recovery.account_id = account.id
|
|
1965
|
+
recovery_decision = await network_recovery.wait(
|
|
1966
|
+
error_code=code,
|
|
1967
|
+
retryable_same_contract=exc.retryable_same_contract,
|
|
1968
|
+
deadline=deadline,
|
|
1969
|
+
rotate_shared_client=True,
|
|
1970
|
+
failed_session=exc.failed_session,
|
|
1971
|
+
)
|
|
1972
|
+
if recovery_decision == "retry":
|
|
1973
|
+
continue
|
|
1974
|
+
if recovery_decision == "exhausted":
|
|
1975
|
+
await settle_compact_usage(
|
|
1976
|
+
api_key=api_key,
|
|
1977
|
+
api_key_reservation=api_key_reservation,
|
|
1978
|
+
response=None,
|
|
1979
|
+
request_service_tier=request_service_tier,
|
|
1980
|
+
)
|
|
1981
|
+
_raise_proxy_budget_exhausted()
|
|
1982
|
+
if exc.retryable_same_contract and safe_retry_budget > 0:
|
|
1983
|
+
safe_retry_budget -= 1
|
|
1984
|
+
continue
|
|
1985
|
+
if _is_security_work_authorization_required_error(code, error_message):
|
|
1986
|
+
if (
|
|
1987
|
+
not account.security_work_authorized
|
|
1988
|
+
and account.id != preferred_account_id
|
|
1989
|
+
and _account_attempt < _compact_max_account_attempts() - 1
|
|
1990
|
+
):
|
|
1991
|
+
last_exc = exc
|
|
1992
|
+
excluded_account_ids.add(account.id)
|
|
1993
|
+
require_security_work_authorized = True
|
|
1994
|
+
transient_exhausted = True
|
|
1995
|
+
break
|
|
1996
|
+
await settle_compact_usage(
|
|
1997
|
+
api_key=api_key,
|
|
1998
|
+
api_key_reservation=api_key_reservation,
|
|
1999
|
+
response=None,
|
|
2000
|
+
request_service_tier=request_service_tier,
|
|
2001
|
+
)
|
|
2002
|
+
raise
|
|
2003
|
+
if code == "account_response_create_cap":
|
|
2004
|
+
last_exc = exc
|
|
2005
|
+
excluded_account_ids.add(account.id)
|
|
2006
|
+
transient_exhausted = True
|
|
2007
|
+
break
|
|
2008
|
+
if _is_account_neutral_error_code(code):
|
|
2009
|
+
await settle_compact_usage(
|
|
2010
|
+
api_key=api_key,
|
|
2011
|
+
api_key_reservation=api_key_reservation,
|
|
2012
|
+
response=None,
|
|
2013
|
+
request_service_tier=request_service_tier,
|
|
2014
|
+
)
|
|
2015
|
+
raise
|
|
2016
|
+
if code == "upstream_request_timeout":
|
|
2017
|
+
await settle_compact_usage(
|
|
2018
|
+
api_key=api_key,
|
|
2019
|
+
api_key_reservation=api_key_reservation,
|
|
2020
|
+
response=None,
|
|
2021
|
+
request_service_tier=request_service_tier,
|
|
2022
|
+
)
|
|
2023
|
+
classified = await proxy._handle_stream_error(
|
|
2024
|
+
account,
|
|
2025
|
+
_upstream_error_from_openai(error),
|
|
2026
|
+
code,
|
|
2027
|
+
http_status=exc.status_code,
|
|
2028
|
+
)
|
|
2029
|
+
if (
|
|
2030
|
+
affinity.selection_key is not None
|
|
2031
|
+
and affinity.kind is not None
|
|
2032
|
+
and affinity.kind != StickySessionKind.CODEX_SESSION
|
|
2033
|
+
):
|
|
2034
|
+
# A timeout cannot prove that durable Codex
|
|
2035
|
+
# ownership is invalid. Preserve both raw hard
|
|
2036
|
+
# turn-state rows and namespaced process locality;
|
|
2037
|
+
# runtime health already drives safe fallback.
|
|
2038
|
+
try:
|
|
2039
|
+
async with proxy._repo_factory() as repos:
|
|
2040
|
+
await repos.sticky_sessions.delete(
|
|
2041
|
+
affinity.selection_key,
|
|
2042
|
+
kind=affinity.kind,
|
|
2043
|
+
)
|
|
2044
|
+
logger.info(
|
|
2045
|
+
"Compact sticky mapping cleared after upstream timeout request_id=%s "
|
|
2046
|
+
"sticky_kind=%s",
|
|
2047
|
+
request_id,
|
|
2048
|
+
affinity.kind.value,
|
|
2049
|
+
)
|
|
2050
|
+
except Exception:
|
|
2051
|
+
logger.warning(
|
|
2052
|
+
"Failed to clear compact sticky mapping after upstream timeout "
|
|
2053
|
+
"request_id=%s sticky_kind=%s",
|
|
2054
|
+
request_id,
|
|
2055
|
+
affinity.kind.value,
|
|
2056
|
+
exc_info=True,
|
|
2057
|
+
)
|
|
2058
|
+
logger.info(
|
|
2059
|
+
"Failover decision request_id=%s transport=compact account_id=%s "
|
|
2060
|
+
"attempt=%d failure_class=%s action=surface",
|
|
2061
|
+
request_id,
|
|
2062
|
+
account.id,
|
|
2063
|
+
_account_attempt + 1,
|
|
2064
|
+
classified["failure_class"],
|
|
2065
|
+
)
|
|
2066
|
+
raise
|
|
2067
|
+
classified = classify_upstream_failure(
|
|
2068
|
+
error_code=code,
|
|
2069
|
+
error=_upstream_error_from_openai(error),
|
|
2070
|
+
http_status=exc.status_code,
|
|
2071
|
+
phase="first_event",
|
|
2072
|
+
)
|
|
2073
|
+
if resilience.deterministic_failover_enabled:
|
|
2074
|
+
action = failover_decision(
|
|
2075
|
+
failure_class=classified["failure_class"],
|
|
2076
|
+
downstream_visible=False,
|
|
2077
|
+
candidates_remaining=_compact_max_account_attempts() - _account_attempt - 1,
|
|
2078
|
+
)
|
|
2079
|
+
else:
|
|
2080
|
+
action = "surface"
|
|
2081
|
+
logger.info(
|
|
2082
|
+
"Failover decision request_id=%s transport=compact account_id=%s "
|
|
2083
|
+
"attempt=%d failure_class=%s action=%s",
|
|
2084
|
+
request_id,
|
|
2085
|
+
account.id,
|
|
2086
|
+
_account_attempt + 1,
|
|
2087
|
+
classified["failure_class"],
|
|
2088
|
+
action,
|
|
2089
|
+
)
|
|
2090
|
+
if action == "failover_next":
|
|
2091
|
+
if account.id == preferred_account_id and classified["failure_class"] in (
|
|
2092
|
+
"rate_limit",
|
|
2093
|
+
"quota",
|
|
2094
|
+
):
|
|
2095
|
+
# Only a pre-visible quota / rate-limit exclusion
|
|
2096
|
+
# of the pinned owner makes account-neutral replay
|
|
2097
|
+
# recovery eligible for the remaining attempts.
|
|
2098
|
+
owner_quota_failover_eligible = True
|
|
2099
|
+
last_exc = exc
|
|
2100
|
+
excluded_account_ids.add(account.id)
|
|
2101
|
+
await record_or_defer_stream_health(
|
|
2102
|
+
account,
|
|
2103
|
+
_upstream_error_from_openai(error),
|
|
2104
|
+
code,
|
|
2105
|
+
exc.status_code,
|
|
2106
|
+
)
|
|
2107
|
+
transient_exhausted = True
|
|
2108
|
+
break
|
|
2109
|
+
await settle_compact_usage(
|
|
2110
|
+
api_key=api_key,
|
|
2111
|
+
api_key_reservation=api_key_reservation,
|
|
2112
|
+
response=None,
|
|
2113
|
+
request_service_tier=request_service_tier,
|
|
2114
|
+
)
|
|
2115
|
+
await proxy._handle_stream_error(
|
|
2116
|
+
account,
|
|
2117
|
+
_upstream_error_from_openai(error),
|
|
2118
|
+
code,
|
|
2119
|
+
http_status=exc.status_code,
|
|
2120
|
+
)
|
|
2121
|
+
raise
|
|
2122
|
+
if transient_exhausted:
|
|
2123
|
+
continue # outer loop: try different account
|
|
2124
|
+
# All account attempts exhausted — raise last error
|
|
2125
|
+
await settle_compact_usage(
|
|
2126
|
+
api_key=api_key,
|
|
2127
|
+
api_key_reservation=api_key_reservation,
|
|
2128
|
+
response=None,
|
|
2129
|
+
request_service_tier=request_service_tier,
|
|
2130
|
+
)
|
|
2131
|
+
if last_exc is not None:
|
|
2132
|
+
raise last_exc
|
|
2133
|
+
raise ProxyResponseError(
|
|
2134
|
+
502,
|
|
2135
|
+
openai_error("upstream_unavailable", "All account attempts exhausted"),
|
|
2136
|
+
)
|
|
2137
|
+
except ProxyResponseError as exc:
|
|
2138
|
+
await settle_on_terminal_exit()
|
|
2139
|
+
failure_metadata = _request_log_failure_metadata(exc)
|
|
2140
|
+
error = _parse_openai_error(exc.payload)
|
|
2141
|
+
log_error_code = log_error_code or _normalize_error_code(
|
|
2142
|
+
error.code if error else None,
|
|
2143
|
+
error.type if error else None,
|
|
2144
|
+
)
|
|
2145
|
+
log_error_message = log_error_message or (error.message if error else None)
|
|
2146
|
+
raise
|
|
2147
|
+
except UpstreamProxyRouteError as exc:
|
|
2148
|
+
route_fail_closed_reason = exc.reason
|
|
2149
|
+
log_error_code = "upstream_proxy_unavailable"
|
|
2150
|
+
log_error_message = exc.reason
|
|
2151
|
+
await settle_compact_usage(
|
|
2152
|
+
api_key=api_key,
|
|
2153
|
+
api_key_reservation=api_key_reservation,
|
|
2154
|
+
response=None,
|
|
2155
|
+
request_service_tier=request_service_tier,
|
|
2156
|
+
)
|
|
2157
|
+
raise ProxyResponseError(
|
|
2158
|
+
502,
|
|
2159
|
+
openai_error("upstream_proxy_unavailable", f"Upstream proxy route unavailable: {exc.reason}"),
|
|
2160
|
+
) from exc
|
|
2161
|
+
except BaseException:
|
|
2162
|
+
await settle_on_terminal_exit()
|
|
2163
|
+
raise
|
|
2164
|
+
finally:
|
|
2165
|
+
usage = response.usage if response else None
|
|
2166
|
+
reasoning_effort = payload.reasoning.effort if payload.reasoning else None
|
|
2167
|
+
await proxy._write_request_log(
|
|
2168
|
+
affinity_observation=affinity_observation,
|
|
2169
|
+
account_id=account_id_value,
|
|
2170
|
+
api_key=api_key,
|
|
2171
|
+
request_id=request_id,
|
|
2172
|
+
model=payload.model,
|
|
2173
|
+
latency_ms=int((_service_time().monotonic() - start) * 1000),
|
|
2174
|
+
status=log_status,
|
|
2175
|
+
error_code=log_error_code,
|
|
2176
|
+
error_message=log_error_message,
|
|
2177
|
+
input_tokens=usage.input_tokens if usage else None,
|
|
2178
|
+
output_tokens=usage.output_tokens if usage else None,
|
|
2179
|
+
cached_input_tokens=(
|
|
2180
|
+
usage.input_tokens_details.cached_tokens if usage and usage.input_tokens_details else None
|
|
2181
|
+
),
|
|
2182
|
+
reasoning_tokens=(
|
|
2183
|
+
usage.output_tokens_details.reasoning_tokens if usage and usage.output_tokens_details else None
|
|
2184
|
+
),
|
|
2185
|
+
reasoning_effort=reasoning_effort,
|
|
2186
|
+
transport=_REQUEST_TRANSPORT_HTTP,
|
|
2187
|
+
service_tier=_effective_service_tier(request_service_tier, actual_service_tier),
|
|
2188
|
+
requested_service_tier=request_service_tier,
|
|
2189
|
+
actual_service_tier=actual_service_tier,
|
|
2190
|
+
failure_phase=failure_metadata.failure_phase,
|
|
2191
|
+
failure_detail=failure_metadata.failure_detail,
|
|
2192
|
+
failure_exception_type=failure_metadata.failure_exception_type,
|
|
2193
|
+
upstream_status_code=failure_metadata.upstream_status_code,
|
|
2194
|
+
upstream_error_code=failure_metadata.upstream_error_code,
|
|
2195
|
+
bridge_stage=failure_metadata.bridge_stage,
|
|
2196
|
+
upstream_proxy_route_mode=route_mode,
|
|
2197
|
+
upstream_proxy_pool_id=route_pool_id,
|
|
2198
|
+
upstream_proxy_endpoint_id=route_endpoint_id,
|
|
2199
|
+
upstream_proxy_fallback_used=route_fallback_used if route_endpoint_id else None,
|
|
2200
|
+
upstream_proxy_fail_closed_reason=route_fail_closed_reason,
|
|
2201
|
+
useragent=useragent,
|
|
2202
|
+
useragent_group=useragent_group,
|
|
2203
|
+
conversation_id=conversation_id,
|
|
2204
|
+
client_ip=client_ip,
|
|
2205
|
+
request_kind=request_kind,
|
|
2206
|
+
)
|
|
2207
|
+
_maybe_log_proxy_service_tier_trace(
|
|
2208
|
+
"compact",
|
|
2209
|
+
requested_service_tier=request_service_tier,
|
|
2210
|
+
actual_service_tier=actual_service_tier,
|
|
2211
|
+
)
|