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,2600 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Awaitable, Callable, Collection
|
|
7
|
+
from contextlib import asynccontextmanager
|
|
8
|
+
from typing import Any, AsyncIterator, Literal, Mapping, NoReturn, TypeVar, cast
|
|
9
|
+
|
|
10
|
+
import aiohttp
|
|
11
|
+
import anyio
|
|
12
|
+
|
|
13
|
+
from app.core.auth.refresh import (
|
|
14
|
+
RefreshError,
|
|
15
|
+
is_transient_refresh_contention,
|
|
16
|
+
pop_token_refresh_timeout_override,
|
|
17
|
+
push_token_refresh_timeout_override,
|
|
18
|
+
)
|
|
19
|
+
from app.core.balancer import (
|
|
20
|
+
PERMANENT_FAILURE_CODES,
|
|
21
|
+
TRAFFIC_CLASS_FOREGROUND,
|
|
22
|
+
TRAFFIC_CLASS_OPPORTUNISTIC,
|
|
23
|
+
ResetPreferenceWindow,
|
|
24
|
+
RoutingStrategy,
|
|
25
|
+
TrafficClass,
|
|
26
|
+
)
|
|
27
|
+
from app.core.clients.files import create_file as core_create_file # noqa: F401
|
|
28
|
+
from app.core.clients.files import finalize_file as core_finalize_file # noqa: F401
|
|
29
|
+
from app.core.clients.http import lease_http_session as lease_http_session # noqa: F401
|
|
30
|
+
from app.core.clients.proxy import (
|
|
31
|
+
_RESPONSE_CREATE_IMAGE_OMISSION_NOTICE as _RESPONSE_CREATE_IMAGE_OMISSION_NOTICE,
|
|
32
|
+
)
|
|
33
|
+
from app.core.clients.proxy import (
|
|
34
|
+
_RESPONSE_CREATE_TOOL_OUTPUT_OMISSION_NOTICE as _RESPONSE_CREATE_TOOL_OUTPUT_OMISSION_NOTICE,
|
|
35
|
+
)
|
|
36
|
+
from app.core.clients.proxy import CodexControlRequestPrivacyPolicy as CodexControlRequestPrivacyPolicy
|
|
37
|
+
from app.core.clients.proxy import CodexControlResponse as CodexControlResponse
|
|
38
|
+
from app.core.clients.proxy import ( # noqa: F401 # noqa: F401
|
|
39
|
+
ImageFetchSession,
|
|
40
|
+
ProxyResponseError,
|
|
41
|
+
UpstreamProxyRouteTrace,
|
|
42
|
+
_as_image_fetch_session,
|
|
43
|
+
_inline_content_images,
|
|
44
|
+
_inline_input_image_urls,
|
|
45
|
+
_ws_transport_payload_budget_bytes,
|
|
46
|
+
filter_inbound_headers,
|
|
47
|
+
pop_compact_timeout_overrides,
|
|
48
|
+
pop_stream_timeout_overrides,
|
|
49
|
+
pop_transcribe_timeout_overrides,
|
|
50
|
+
push_compact_timeout_overrides,
|
|
51
|
+
push_stream_timeout_overrides,
|
|
52
|
+
push_transcribe_timeout_overrides,
|
|
53
|
+
)
|
|
54
|
+
from app.core.clients.proxy import (
|
|
55
|
+
_is_inline_image_reference as _is_inline_image_reference,
|
|
56
|
+
)
|
|
57
|
+
from app.core.clients.proxy import (
|
|
58
|
+
_response_create_inline_image_notice_item as _response_create_inline_image_notice_item,
|
|
59
|
+
)
|
|
60
|
+
from app.core.clients.proxy import (
|
|
61
|
+
_response_create_inline_image_notice_part as _response_create_inline_image_notice_part,
|
|
62
|
+
)
|
|
63
|
+
from app.core.clients.proxy import (
|
|
64
|
+
_response_create_recent_suffix_start as _response_create_recent_suffix_start,
|
|
65
|
+
)
|
|
66
|
+
from app.core.clients.proxy import (
|
|
67
|
+
_response_create_too_large_error_envelope as _response_create_too_large_error_envelope,
|
|
68
|
+
)
|
|
69
|
+
from app.core.clients.proxy import (
|
|
70
|
+
_should_slim_historical_tool_output as _should_slim_historical_tool_output,
|
|
71
|
+
)
|
|
72
|
+
from app.core.clients.proxy import (
|
|
73
|
+
_slim_historical_response_content as _slim_historical_response_content,
|
|
74
|
+
)
|
|
75
|
+
from app.core.clients.proxy import (
|
|
76
|
+
_slim_historical_response_content_part as _slim_historical_response_content_part,
|
|
77
|
+
)
|
|
78
|
+
from app.core.clients.proxy import codex_control_request as core_codex_control_request # noqa: F401
|
|
79
|
+
from app.core.clients.proxy import compact_responses as core_compact_responses # noqa: F401
|
|
80
|
+
from app.core.clients.proxy import stream_responses as core_stream_responses # noqa: F401
|
|
81
|
+
from app.core.clients.proxy import thread_goal_request as core_thread_goal_request
|
|
82
|
+
from app.core.clients.proxy import transcribe_audio as core_transcribe_audio # noqa: F401
|
|
83
|
+
from app.core.clients.proxy_websocket import UpstreamWebSocket as UpstreamWebSocket
|
|
84
|
+
from app.core.clients.proxy_websocket import (
|
|
85
|
+
connect_live_websocket as connect_live_websocket,
|
|
86
|
+
)
|
|
87
|
+
from app.core.clients.proxy_websocket import (
|
|
88
|
+
connect_responses_websocket as connect_responses_websocket,
|
|
89
|
+
)
|
|
90
|
+
from app.core.clock import REAL_CLOCK, REAL_SCHEDULER, Clock, Scheduler
|
|
91
|
+
from app.core.config.dashboard_overrides import with_dashboard_overrides
|
|
92
|
+
from app.core.config.settings import Settings as _Settings
|
|
93
|
+
from app.core.config.settings import get_settings as get_environment_settings
|
|
94
|
+
from app.core.config.settings_cache import get_settings_cache
|
|
95
|
+
from app.core.crypto import TokenEncryptor
|
|
96
|
+
from app.core.errors import PREVIOUS_RESPONSE_NOT_FOUND_CODE as PREVIOUS_RESPONSE_NOT_FOUND_CODE
|
|
97
|
+
from app.core.errors import (
|
|
98
|
+
PREVIOUS_RESPONSE_NOT_FOUND_MESSAGE as PREVIOUS_RESPONSE_NOT_FOUND_MESSAGE,
|
|
99
|
+
)
|
|
100
|
+
from app.core.errors import (
|
|
101
|
+
OpenAIErrorEnvelope,
|
|
102
|
+
OpenAIErrorParam,
|
|
103
|
+
ResponseFailedEvent,
|
|
104
|
+
coerce_error_param,
|
|
105
|
+
is_previous_response_not_found_error,
|
|
106
|
+
openai_error,
|
|
107
|
+
previous_response_id_from_not_found_message,
|
|
108
|
+
previous_response_stream_incomplete_error,
|
|
109
|
+
response_failed_event,
|
|
110
|
+
synthetic_transport_failure_event,
|
|
111
|
+
)
|
|
112
|
+
from app.core.errors import (
|
|
113
|
+
is_previous_response_not_found_public_shape as _is_previous_response_not_found_public_shape, # noqa: F401
|
|
114
|
+
)
|
|
115
|
+
from app.core.metrics.prometheus import (
|
|
116
|
+
PROMETHEUS_AVAILABLE,
|
|
117
|
+
bridge_same_account_takeover_total,
|
|
118
|
+
)
|
|
119
|
+
from app.core.openai.models import CompactResponsePayload, OpenAIResponsePayload
|
|
120
|
+
from app.core.openai.requests import (
|
|
121
|
+
ResponsesCompactRequest,
|
|
122
|
+
ResponsesRequest,
|
|
123
|
+
)
|
|
124
|
+
from app.core.resilience.network_recovery import (
|
|
125
|
+
ProcessNetworkRecovery as ProcessNetworkRecovery,
|
|
126
|
+
)
|
|
127
|
+
from app.core.resilience.toggles import bind_resilience_toggles
|
|
128
|
+
from app.core.types import JsonValue
|
|
129
|
+
from app.core.upstream_proxy import UpstreamProxyRouteError
|
|
130
|
+
from app.core.upstream_proxy.resolver import (
|
|
131
|
+
resolve_upstream_route as resolve_upstream_route,
|
|
132
|
+
)
|
|
133
|
+
from app.core.utils.request_id import ensure_request_id, get_request_id
|
|
134
|
+
from app.core.utils.sse import CODEX_KEEPALIVE_FRAME as CODEX_KEEPALIVE_FRAME # noqa: F401
|
|
135
|
+
from app.core.utils.sse import format_sse_event, parse_sse_data_json # noqa: F401
|
|
136
|
+
from app.core.utils.time import utcnow as utcnow
|
|
137
|
+
from app.db.models import (
|
|
138
|
+
Account,
|
|
139
|
+
AccountStatus, # noqa: F401
|
|
140
|
+
DashboardSettings,
|
|
141
|
+
StickySessionKind,
|
|
142
|
+
)
|
|
143
|
+
from app.db.session import SessionLocal as SessionLocal
|
|
144
|
+
from app.modules.accounts.auth_manager import AccountsRepositoryPort, AuthManager
|
|
145
|
+
from app.modules.api_keys.service import (
|
|
146
|
+
ApiKeyData,
|
|
147
|
+
ApiKeyUsageReservationData, # noqa: F401
|
|
148
|
+
)
|
|
149
|
+
from app.modules.api_keys.service import (
|
|
150
|
+
ApiKeysService as ApiKeysService,
|
|
151
|
+
)
|
|
152
|
+
from app.modules.proxy._service.api_key_usage import (
|
|
153
|
+
_API_KEY_RESERVATION_HEARTBEAT_SECONDS as _API_KEY_RESERVATION_HEARTBEAT_SECONDS,
|
|
154
|
+
)
|
|
155
|
+
from app.modules.proxy._service.api_key_usage import (
|
|
156
|
+
_STREAM_API_KEY_RELEASE_RETRY_MAX_CONCURRENCY as _STREAM_API_KEY_RELEASE_RETRY_MAX_CONCURRENCY,
|
|
157
|
+
)
|
|
158
|
+
from app.modules.proxy._service.api_key_usage import _ApiKeyUsageMixin
|
|
159
|
+
from app.modules.proxy._service.api_key_usage import (
|
|
160
|
+
_bounded_lease_token_estimate as _bounded_lease_token_estimate,
|
|
161
|
+
)
|
|
162
|
+
from app.modules.proxy._service.api_key_usage import (
|
|
163
|
+
_estimated_lease_tokens_from_request_usage_budget as _estimated_lease_tokens_from_request_usage_budget,
|
|
164
|
+
)
|
|
165
|
+
from app.modules.proxy._service.clock_budget import _ClockBudgetMixin, _remaining_budget_seconds # noqa: F401
|
|
166
|
+
from app.modules.proxy._service.codex_control import _CodexControlMixin
|
|
167
|
+
from app.modules.proxy._service.compact import _CompactMixin
|
|
168
|
+
from app.modules.proxy._service.compact import (
|
|
169
|
+
_service_tier_from_compact_payload as _service_tier_from_compact_payload,
|
|
170
|
+
)
|
|
171
|
+
from app.modules.proxy._service.compact import (
|
|
172
|
+
_sticky_key_for_compact_request as _sticky_key_for_compact_request,
|
|
173
|
+
)
|
|
174
|
+
from app.modules.proxy._service.compact import (
|
|
175
|
+
_sticky_key_from_compact_payload as _sticky_key_from_compact_payload,
|
|
176
|
+
)
|
|
177
|
+
from app.modules.proxy._service.file_ops import _FileOpsMixin
|
|
178
|
+
from app.modules.proxy._service.http_bridge import _HTTPBridgeMixin
|
|
179
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
180
|
+
_TASK_CANCEL_TIMEOUT_SECONDS as _TASK_CANCEL_TIMEOUT_SECONDS,
|
|
181
|
+
)
|
|
182
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
183
|
+
_active_http_bridge_instance_ring as _active_http_bridge_instance_ring,
|
|
184
|
+
)
|
|
185
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
186
|
+
_await_cancelled_task as _await_cancelled_task,
|
|
187
|
+
)
|
|
188
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
189
|
+
_build_http_bridge_prewarm_text as _build_http_bridge_prewarm_text,
|
|
190
|
+
)
|
|
191
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
192
|
+
_durable_bridge_lookup_active_owner as _durable_bridge_lookup_active_owner,
|
|
193
|
+
)
|
|
194
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
195
|
+
_durable_bridge_lookup_allows_local_reuse as _durable_bridge_lookup_allows_local_reuse,
|
|
196
|
+
)
|
|
197
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
198
|
+
_effective_http_bridge_idle_ttl_seconds as _effective_http_bridge_idle_ttl_seconds,
|
|
199
|
+
)
|
|
200
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
201
|
+
_forwarded_http_bridge_session_key as _forwarded_http_bridge_session_key,
|
|
202
|
+
)
|
|
203
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
204
|
+
_has_http_bridge_response_output_marker as _has_http_bridge_response_output_marker,
|
|
205
|
+
)
|
|
206
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
207
|
+
_http_bridge_admission_timeout_seconds,
|
|
208
|
+
_http_bridge_is_explicit_previous_response_rejection, # noqa: F401
|
|
209
|
+
_http_bridge_should_attempt_local_previous_response_recovery, # noqa: F401
|
|
210
|
+
)
|
|
211
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
212
|
+
_http_bridge_allow_durable_takeover as _http_bridge_allow_durable_takeover,
|
|
213
|
+
)
|
|
214
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
215
|
+
_http_bridge_can_local_recover_without_ring as _http_bridge_can_local_recover_without_ring,
|
|
216
|
+
)
|
|
217
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
218
|
+
_http_bridge_can_recover_during_drain as _http_bridge_can_recover_during_drain,
|
|
219
|
+
)
|
|
220
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
221
|
+
_http_bridge_continuity_lost_error_envelope as _http_bridge_continuity_lost_error_envelope,
|
|
222
|
+
)
|
|
223
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
224
|
+
_http_bridge_durable_lease_ttl_seconds as _http_bridge_durable_lease_ttl_seconds,
|
|
225
|
+
)
|
|
226
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
227
|
+
_http_bridge_endpoint_matches_current_instance as _http_bridge_endpoint_matches_current_instance,
|
|
228
|
+
)
|
|
229
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
230
|
+
_http_bridge_eviction_priority as _http_bridge_eviction_priority,
|
|
231
|
+
)
|
|
232
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
233
|
+
_http_bridge_has_durable_recovery_anchor as _http_bridge_has_durable_recovery_anchor,
|
|
234
|
+
)
|
|
235
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
236
|
+
_http_bridge_input_item_type as _http_bridge_input_item_type,
|
|
237
|
+
)
|
|
238
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
239
|
+
_http_bridge_is_context_overflow_error as _http_bridge_is_context_overflow_error,
|
|
240
|
+
)
|
|
241
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
242
|
+
_http_bridge_is_previous_response_owner_unavailable as _http_bridge_is_previous_response_owner_unavailable,
|
|
243
|
+
)
|
|
244
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
245
|
+
_http_bridge_key_strength as _http_bridge_key_strength,
|
|
246
|
+
)
|
|
247
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
248
|
+
_http_bridge_owner_check_required as _http_bridge_owner_check_required,
|
|
249
|
+
)
|
|
250
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
251
|
+
_http_bridge_owner_instance as _http_bridge_owner_instance,
|
|
252
|
+
)
|
|
253
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
254
|
+
_http_bridge_owner_lookup_unavailable_error_envelope as _http_bridge_owner_lookup_unavailable_error_envelope,
|
|
255
|
+
)
|
|
256
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
257
|
+
_http_bridge_payload_looks_like_full_resend as _http_bridge_payload_looks_like_full_resend,
|
|
258
|
+
)
|
|
259
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
260
|
+
_http_bridge_payload_without_previous_response_id as _http_bridge_payload_without_previous_response_id,
|
|
261
|
+
)
|
|
262
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
263
|
+
_http_bridge_precreated_retry_failure_error as _http_bridge_precreated_retry_failure_error,
|
|
264
|
+
)
|
|
265
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
266
|
+
_http_bridge_previous_response_alias_key as _http_bridge_previous_response_alias_key,
|
|
267
|
+
)
|
|
268
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
269
|
+
_http_bridge_previous_response_error_envelope as _http_bridge_previous_response_error_envelope,
|
|
270
|
+
)
|
|
271
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
272
|
+
_http_bridge_prewarm_enabled as _http_bridge_prewarm_enabled,
|
|
273
|
+
)
|
|
274
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
275
|
+
_http_bridge_request_budget_seconds as _http_bridge_request_budget_seconds,
|
|
276
|
+
)
|
|
277
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
278
|
+
_http_bridge_request_counts_against_queue as _http_bridge_request_counts_against_queue,
|
|
279
|
+
)
|
|
280
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
281
|
+
_http_bridge_request_stage as _http_bridge_request_stage,
|
|
282
|
+
)
|
|
283
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
284
|
+
_http_bridge_requires_cluster_registration as _http_bridge_requires_cluster_registration,
|
|
285
|
+
)
|
|
286
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
287
|
+
_http_bridge_runtime_config as _http_bridge_runtime_config,
|
|
288
|
+
)
|
|
289
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
290
|
+
_http_bridge_session_allows_api_key as _http_bridge_session_allows_api_key,
|
|
291
|
+
)
|
|
292
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
293
|
+
_http_bridge_session_has_visible_requests as _http_bridge_session_has_visible_requests,
|
|
294
|
+
)
|
|
295
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
296
|
+
_http_bridge_session_matches_preferred_account as _http_bridge_session_matches_preferred_account,
|
|
297
|
+
)
|
|
298
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
299
|
+
_http_bridge_session_retiring_with_visible_requests as _http_bridge_session_retiring_with_visible_requests,
|
|
300
|
+
)
|
|
301
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
302
|
+
_http_bridge_session_reusable_for_request as _http_bridge_session_reusable_for_request,
|
|
303
|
+
)
|
|
304
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
305
|
+
_http_bridge_should_attempt_local_bootstrap_rebind as _http_bridge_should_attempt_local_bootstrap_rebind,
|
|
306
|
+
)
|
|
307
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
308
|
+
_http_bridge_should_attempt_soft_affinity_reroute as _http_bridge_should_attempt_soft_affinity_reroute,
|
|
309
|
+
)
|
|
310
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
311
|
+
_http_bridge_should_rollover_after_context_overflow as _http_bridge_should_rollover_after_context_overflow,
|
|
312
|
+
)
|
|
313
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
314
|
+
_http_bridge_should_wait_for_registration as _http_bridge_should_wait_for_registration,
|
|
315
|
+
)
|
|
316
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
317
|
+
_http_bridge_startup_wait_timeout_error as _http_bridge_startup_wait_timeout_error,
|
|
318
|
+
)
|
|
319
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
320
|
+
_http_bridge_turn_state_alias_key as _http_bridge_turn_state_alias_key,
|
|
321
|
+
)
|
|
322
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
323
|
+
_HTTPBridgeRuntimeConfig as _HTTPBridgeRuntimeConfig,
|
|
324
|
+
)
|
|
325
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
326
|
+
_is_http_bridge_previous_response_output_item as _is_http_bridge_previous_response_output_item,
|
|
327
|
+
)
|
|
328
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
329
|
+
_is_missing_durable_bridge_table_error as _is_missing_durable_bridge_table_error,
|
|
330
|
+
)
|
|
331
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
332
|
+
_log_http_bridge_event as _log_http_bridge_event,
|
|
333
|
+
)
|
|
334
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
335
|
+
_log_http_bridge_startup_wait_timeout as _log_http_bridge_startup_wait_timeout,
|
|
336
|
+
)
|
|
337
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
338
|
+
_make_http_bridge_session_key as _make_http_bridge_session_key,
|
|
339
|
+
)
|
|
340
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
341
|
+
_normalize_http_bridge_error_event as _normalize_http_bridge_error_event,
|
|
342
|
+
)
|
|
343
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
344
|
+
_normalized_http_bridge_instance_ring as _normalized_http_bridge_instance_ring,
|
|
345
|
+
)
|
|
346
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
347
|
+
_preferred_http_bridge_reconnect_turn_state as _preferred_http_bridge_reconnect_turn_state,
|
|
348
|
+
)
|
|
349
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
350
|
+
_record_bridge_drain_recovery_allowed as _record_bridge_drain_recovery_allowed,
|
|
351
|
+
)
|
|
352
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
353
|
+
_record_bridge_first_turn_timeout as _record_bridge_first_turn_timeout,
|
|
354
|
+
)
|
|
355
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
356
|
+
_record_bridge_reattach as _record_bridge_reattach,
|
|
357
|
+
)
|
|
358
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
359
|
+
_record_http_bridge_stuck_retire as _record_http_bridge_stuck_retire,
|
|
360
|
+
)
|
|
361
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
362
|
+
_trim_http_bridge_previous_response_input_items as _trim_http_bridge_previous_response_input_items,
|
|
363
|
+
)
|
|
364
|
+
from app.modules.proxy._service.http_bridge.retry_circuit import (
|
|
365
|
+
_HTTPBridgeRetryCircuitMixin,
|
|
366
|
+
_initialize_http_bridge_retry_circuit,
|
|
367
|
+
)
|
|
368
|
+
from app.modules.proxy._service.observability import _hash_identifier as _hash_identifier
|
|
369
|
+
from app.modules.proxy._service.observability import _hash_identifier_or_none as _hash_identifier_or_none
|
|
370
|
+
from app.modules.proxy._service.observability import _interesting_header_keys as _interesting_header_keys
|
|
371
|
+
from app.modules.proxy._service.observability import (
|
|
372
|
+
_maybe_log_proxy_request_payload as _maybe_log_proxy_request_payload,
|
|
373
|
+
)
|
|
374
|
+
from app.modules.proxy._service.observability import _maybe_log_proxy_request_shape as _maybe_log_proxy_request_shape
|
|
375
|
+
from app.modules.proxy._service.observability import (
|
|
376
|
+
_maybe_log_proxy_service_tier_trace as _maybe_log_proxy_service_tier_trace,
|
|
377
|
+
)
|
|
378
|
+
from app.modules.proxy._service.observability import (
|
|
379
|
+
_record_continuity_fail_closed as _record_continuity_fail_closed,
|
|
380
|
+
)
|
|
381
|
+
from app.modules.proxy._service.observability import (
|
|
382
|
+
_record_continuity_owner_resolution as _record_continuity_owner_resolution,
|
|
383
|
+
)
|
|
384
|
+
from app.modules.proxy._service.observability import _summarize_input as _summarize_input
|
|
385
|
+
from app.modules.proxy._service.observability import _tools_hash as _tools_hash
|
|
386
|
+
from app.modules.proxy._service.observability import _truncate_identifier as _truncate_identifier
|
|
387
|
+
from app.modules.proxy._service.observability import continuity_fail_closed_total as continuity_fail_closed_total
|
|
388
|
+
from app.modules.proxy._service.observability import (
|
|
389
|
+
continuity_owner_resolution_total as continuity_owner_resolution_total,
|
|
390
|
+
)
|
|
391
|
+
from app.modules.proxy._service.rate_limit import (
|
|
392
|
+
_RateLimitMixin,
|
|
393
|
+
)
|
|
394
|
+
from app.modules.proxy._service.realtime_live import LiveWebSocketConnector, _RealtimeLiveMixin
|
|
395
|
+
from app.modules.proxy._service.refresh import (
|
|
396
|
+
ensure_fresh_with_auth_error,
|
|
397
|
+
)
|
|
398
|
+
from app.modules.proxy._service.refresh import (
|
|
399
|
+
ensure_fresh_with_budget as _recover_fresh_account,
|
|
400
|
+
)
|
|
401
|
+
from app.modules.proxy._service.request_log import (
|
|
402
|
+
_normalize_session_id as _normalize_session_id,
|
|
403
|
+
)
|
|
404
|
+
from app.modules.proxy._service.request_log import (
|
|
405
|
+
_RequestLogMixin,
|
|
406
|
+
)
|
|
407
|
+
from app.modules.proxy._service.response_create import (
|
|
408
|
+
_OVERSIZED_RESPONSE_CREATE_DUMP_DIR as _OVERSIZED_RESPONSE_CREATE_DUMP_DIR,
|
|
409
|
+
)
|
|
410
|
+
from app.modules.proxy._service.response_create import (
|
|
411
|
+
_OVERSIZED_RESPONSE_CREATE_LARGEST_ITEMS as _OVERSIZED_RESPONSE_CREATE_LARGEST_ITEMS,
|
|
412
|
+
)
|
|
413
|
+
from app.modules.proxy._service.response_create import (
|
|
414
|
+
_RESPONSE_CREATE_COMPATIBILITY_METADATA_HEADERS as _RESPONSE_CREATE_COMPATIBILITY_METADATA_HEADERS,
|
|
415
|
+
)
|
|
416
|
+
from app.modules.proxy._service.response_create import (
|
|
417
|
+
_RESPONSE_CREATE_DUMP_MAX_PAIRS as _RESPONSE_CREATE_DUMP_MAX_PAIRS,
|
|
418
|
+
)
|
|
419
|
+
from app.modules.proxy._service.response_create import (
|
|
420
|
+
_RESPONSE_CREATE_HISTORY_OMISSION_NOTICE as _RESPONSE_CREATE_HISTORY_OMISSION_NOTICE,
|
|
421
|
+
)
|
|
422
|
+
from app.modules.proxy._service.response_create import (
|
|
423
|
+
_UPSTREAM_RESPONSE_CREATE_MAX_BYTES as _UPSTREAM_RESPONSE_CREATE_MAX_BYTES,
|
|
424
|
+
)
|
|
425
|
+
from app.modules.proxy._service.response_create import (
|
|
426
|
+
_UPSTREAM_RESPONSE_CREATE_WARN_BYTES as _UPSTREAM_RESPONSE_CREATE_WARN_BYTES,
|
|
427
|
+
)
|
|
428
|
+
from app.modules.proxy._service.response_create import (
|
|
429
|
+
_count_external_image_urls as _count_external_image_urls,
|
|
430
|
+
)
|
|
431
|
+
from app.modules.proxy._service.response_create import (
|
|
432
|
+
_enforce_response_create_size_limit as _enforce_response_create_size_limit,
|
|
433
|
+
)
|
|
434
|
+
from app.modules.proxy._service.response_create import (
|
|
435
|
+
_fingerprint_input_items as _fingerprint_input_items,
|
|
436
|
+
)
|
|
437
|
+
from app.modules.proxy._service.response_create import (
|
|
438
|
+
_function_call_output_call_ids as _function_call_output_call_ids,
|
|
439
|
+
)
|
|
440
|
+
from app.modules.proxy._service.response_create import (
|
|
441
|
+
_inject_missing_interrupted_function_call_outputs as _inject_missing_interrupted_function_call_outputs,
|
|
442
|
+
)
|
|
443
|
+
from app.modules.proxy._service.response_create import (
|
|
444
|
+
_inline_top_level_input_image_urls as _inline_top_level_input_image_urls,
|
|
445
|
+
)
|
|
446
|
+
from app.modules.proxy._service.response_create import (
|
|
447
|
+
_input_image_request_requires_http_upstream, # noqa: F401
|
|
448
|
+
_responses_request_contains_input_image, # noqa: F401
|
|
449
|
+
_responses_request_uses_image_generation, # noqa: F401
|
|
450
|
+
)
|
|
451
|
+
from app.modules.proxy._service.response_create import (
|
|
452
|
+
_input_part_is_image as _input_part_is_image,
|
|
453
|
+
)
|
|
454
|
+
from app.modules.proxy._service.response_create import (
|
|
455
|
+
_json_size_bytes as _json_size_bytes,
|
|
456
|
+
)
|
|
457
|
+
from app.modules.proxy._service.response_create import (
|
|
458
|
+
_json_value_contains_input_image_part as _json_value_contains_input_image_part,
|
|
459
|
+
)
|
|
460
|
+
from app.modules.proxy._service.response_create import (
|
|
461
|
+
_maybe_dump_oversized_response_create_request as _maybe_dump_oversized_response_create_request,
|
|
462
|
+
)
|
|
463
|
+
from app.modules.proxy._service.response_create import (
|
|
464
|
+
_missing_function_call_outputs_for_previous_response as _missing_function_call_outputs_for_previous_response,
|
|
465
|
+
)
|
|
466
|
+
from app.modules.proxy._service.response_create import (
|
|
467
|
+
_oversized_response_create_dump_dir as _oversized_response_create_dump_dir,
|
|
468
|
+
)
|
|
469
|
+
from app.modules.proxy._service.response_create import (
|
|
470
|
+
_response_create_client_metadata as _response_create_client_metadata,
|
|
471
|
+
)
|
|
472
|
+
from app.modules.proxy._service.response_create import (
|
|
473
|
+
_response_create_history_omission_notice_item as _response_create_history_omission_notice_item,
|
|
474
|
+
)
|
|
475
|
+
from app.modules.proxy._service.response_create import (
|
|
476
|
+
_response_create_text as _response_create_text,
|
|
477
|
+
)
|
|
478
|
+
from app.modules.proxy._service.response_create import (
|
|
479
|
+
_response_create_text_with_account_installation_id as _response_create_text_with_account_installation_id,
|
|
480
|
+
)
|
|
481
|
+
from app.modules.proxy._service.response_create import (
|
|
482
|
+
_response_create_text_with_size_guard as _response_create_text_with_size_guard,
|
|
483
|
+
)
|
|
484
|
+
from app.modules.proxy._service.response_create import (
|
|
485
|
+
_response_output_item_done_tool_call as _response_output_item_done_tool_call,
|
|
486
|
+
)
|
|
487
|
+
from app.modules.proxy._service.response_create import (
|
|
488
|
+
_safe_dump_slug as _safe_dump_slug,
|
|
489
|
+
)
|
|
490
|
+
from app.modules.proxy._service.response_create import (
|
|
491
|
+
_should_dump_oversized_response_create as _should_dump_oversized_response_create,
|
|
492
|
+
)
|
|
493
|
+
from app.modules.proxy._service.response_create import (
|
|
494
|
+
_slim_historical_response_input_item as _slim_historical_response_input_item,
|
|
495
|
+
)
|
|
496
|
+
from app.modules.proxy._service.response_create import (
|
|
497
|
+
_slim_response_create_payload_for_upstream as _slim_response_create_payload_for_upstream,
|
|
498
|
+
)
|
|
499
|
+
from app.modules.proxy._service.response_create import (
|
|
500
|
+
_summarize_response_create_input as _summarize_response_create_input,
|
|
501
|
+
)
|
|
502
|
+
from app.modules.proxy._service.response_create import (
|
|
503
|
+
_summarize_response_create_payload as _summarize_response_create_payload,
|
|
504
|
+
)
|
|
505
|
+
from app.modules.proxy._service.response_create import (
|
|
506
|
+
_synthetic_interrupted_function_call_output as _synthetic_interrupted_function_call_output,
|
|
507
|
+
)
|
|
508
|
+
from app.modules.proxy._service.response_create import (
|
|
509
|
+
_write_response_create_dump as _write_response_create_dump,
|
|
510
|
+
)
|
|
511
|
+
from app.modules.proxy._service.streaming import _StreamingMixin
|
|
512
|
+
from app.modules.proxy._service.streaming.helpers import (
|
|
513
|
+
_build_rewritten_stream_response_failed_event as _build_rewritten_stream_response_failed_event,
|
|
514
|
+
)
|
|
515
|
+
from app.modules.proxy._service.streaming.helpers import (
|
|
516
|
+
_build_stream_incomplete_terminal_event_for_request as _build_stream_incomplete_terminal_event_for_request,
|
|
517
|
+
)
|
|
518
|
+
from app.modules.proxy._service.streaming.helpers import (
|
|
519
|
+
_call_stream_with_supported_optional_kwargs as _call_stream_with_supported_optional_kwargs,
|
|
520
|
+
)
|
|
521
|
+
from app.modules.proxy._service.streaming.helpers import _classify_upstream_close as _classify_upstream_close
|
|
522
|
+
from app.modules.proxy._service.streaming.helpers import (
|
|
523
|
+
_is_account_neutral_transport_drop as _is_account_neutral_transport_drop,
|
|
524
|
+
)
|
|
525
|
+
from app.modules.proxy._service.streaming.helpers import _is_background_json_ack as _is_background_json_ack
|
|
526
|
+
from app.modules.proxy._service.streaming.helpers import (
|
|
527
|
+
_push_stream_attempt_timeout_overrides as _push_stream_attempt_timeout_overrides,
|
|
528
|
+
)
|
|
529
|
+
from app.modules.proxy._service.streaming.helpers import (
|
|
530
|
+
_rewrite_previous_response_stream_error as _rewrite_previous_response_stream_error,
|
|
531
|
+
)
|
|
532
|
+
from app.modules.proxy._service.streaming.helpers import (
|
|
533
|
+
_should_infer_upstream_status_from_proxy_error as _should_infer_upstream_status_from_proxy_error,
|
|
534
|
+
)
|
|
535
|
+
from app.modules.proxy._service.streaming.helpers import (
|
|
536
|
+
_should_penalize_stream_error as _should_penalize_stream_error,
|
|
537
|
+
)
|
|
538
|
+
from app.modules.proxy._service.streaming.helpers import (
|
|
539
|
+
_should_retry_stream_error as _should_retry_stream_error,
|
|
540
|
+
)
|
|
541
|
+
from app.modules.proxy._service.streaming.helpers import (
|
|
542
|
+
_should_retry_transient_stream_error as _should_retry_transient_stream_error,
|
|
543
|
+
)
|
|
544
|
+
from app.modules.proxy._service.streaming.helpers import (
|
|
545
|
+
_stream_iterator_after_capacity_admission as _stream_iterator_after_capacity_admission,
|
|
546
|
+
)
|
|
547
|
+
from app.modules.proxy._service.streaming.helpers import (
|
|
548
|
+
_stream_request_budget_seconds as _stream_request_budget_seconds,
|
|
549
|
+
)
|
|
550
|
+
from app.modules.proxy._service.streaming.helpers import (
|
|
551
|
+
_upstream_turn_state_from_socket as _upstream_turn_state_from_socket,
|
|
552
|
+
)
|
|
553
|
+
from app.modules.proxy._service.streaming.retry import (
|
|
554
|
+
_http_bridge_allowed_by_transport_policy as _http_bridge_allowed_by_transport_policy,
|
|
555
|
+
)
|
|
556
|
+
from app.modules.proxy._service.streaming.retry import (
|
|
557
|
+
_http_downstream_request_is_sticky as _http_downstream_request_is_sticky,
|
|
558
|
+
)
|
|
559
|
+
from app.modules.proxy._service.streaming.retry import (
|
|
560
|
+
_resolve_http_downstream_transport as _resolve_http_downstream_transport,
|
|
561
|
+
)
|
|
562
|
+
from app.modules.proxy._service.support import (
|
|
563
|
+
_HARD_HTTP_BRIDGE_AFFINITY_KINDS, # noqa: F401
|
|
564
|
+
_REQUEST_TRANSPORT_WEBSOCKET, # noqa: F401
|
|
565
|
+
_WEBSOCKET_FULL_REPLAY_WAIT_MIN_ITEMS, # noqa: F401
|
|
566
|
+
_WEBSOCKET_FULL_REPLAY_WAIT_POLL_SECONDS, # noqa: F401
|
|
567
|
+
_ApiKeyReservationTouchState, # noqa: F401
|
|
568
|
+
_call_with_supported_optional_kwargs,
|
|
569
|
+
_clear_websocket_request_error_overrides, # noqa: F401
|
|
570
|
+
_DownstreamWebSocketActivity, # noqa: F401
|
|
571
|
+
_event_type_from_payload, # noqa: F401
|
|
572
|
+
_finalize_ttft_reasoning_deltas, # noqa: F401
|
|
573
|
+
_http_error_status_from_payload, # noqa: F401
|
|
574
|
+
_HTTPBridgeSession,
|
|
575
|
+
_HTTPBridgeSessionKey,
|
|
576
|
+
_is_account_neutral_error_code,
|
|
577
|
+
_is_local_account_cap_code, # noqa: F401
|
|
578
|
+
_is_ttft_event, # noqa: F401
|
|
579
|
+
_openai_error_envelope_from_response_failed_payload, # noqa: F401
|
|
580
|
+
_PreparedWebSocketRequest, # noqa: F401
|
|
581
|
+
_record_response_event, # noqa: F401
|
|
582
|
+
_record_websocket_route_metadata, # noqa: F401
|
|
583
|
+
_request_log_client_fields, # noqa: F401
|
|
584
|
+
_request_log_useragent_fields, # noqa: F401
|
|
585
|
+
_RequestLogFailureMetadata,
|
|
586
|
+
_RetryableStreamError, # noqa: F401
|
|
587
|
+
_selection_api_key_fair_share_threshold_pct,
|
|
588
|
+
_stream_settlement_error_payload, # noqa: F401
|
|
589
|
+
_StreamSettlement, # noqa: F401
|
|
590
|
+
_supported_optional_kwargs, # noqa: F401
|
|
591
|
+
_TerminalStreamError, # noqa: F401
|
|
592
|
+
_TransientStreamError, # noqa: F401
|
|
593
|
+
_ttft_event_visible_at, # noqa: F401
|
|
594
|
+
_TTFTReasoningDeltaState, # noqa: F401
|
|
595
|
+
_wait_for_websocket_continuity_gap, # noqa: F401
|
|
596
|
+
_websocket_full_replay_should_wait_for_continuity, # noqa: F401
|
|
597
|
+
_websocket_request_can_replay_before_visible_output, # noqa: F401
|
|
598
|
+
_WebSocketConnectFailureEmitted, # noqa: F401
|
|
599
|
+
_WebSocketContinuityAnchor, # noqa: F401
|
|
600
|
+
_WebSocketContinuityState,
|
|
601
|
+
_WebSocketReceiveTimeout, # noqa: F401
|
|
602
|
+
_WebSocketRequestState,
|
|
603
|
+
_WebSocketUpstreamControl, # noqa: F401
|
|
604
|
+
opportunistic_admission_account_scope,
|
|
605
|
+
)
|
|
606
|
+
from app.modules.proxy._service.support import (
|
|
607
|
+
_HTTPBridgeOwnerForward as _HTTPBridgeOwnerForward,
|
|
608
|
+
)
|
|
609
|
+
from app.modules.proxy._service.support import (
|
|
610
|
+
_raise_proxy_unavailable_for_account as _raise_proxy_unavailable_for_account,
|
|
611
|
+
)
|
|
612
|
+
from app.modules.proxy._service.support import (
|
|
613
|
+
_websocket_route_log_kwargs as _websocket_route_log_kwargs,
|
|
614
|
+
)
|
|
615
|
+
from app.modules.proxy._service.support import (
|
|
616
|
+
failover_after_previsible_refresh_error as failover_after_previsible_refresh_error,
|
|
617
|
+
)
|
|
618
|
+
from app.modules.proxy._service.support import (
|
|
619
|
+
is_claim_contention_unpenalized as is_claim_contention_unpenalized,
|
|
620
|
+
)
|
|
621
|
+
from app.modules.proxy._service.support import (
|
|
622
|
+
raise_proxy_unavailable_for_claim_contention as raise_proxy_unavailable_for_claim_contention,
|
|
623
|
+
)
|
|
624
|
+
from app.modules.proxy._service.transcribe import (
|
|
625
|
+
_TranscribeMixin,
|
|
626
|
+
)
|
|
627
|
+
from app.modules.proxy._service.warmup import (
|
|
628
|
+
WarmupExecutionData as WarmupExecutionData,
|
|
629
|
+
)
|
|
630
|
+
from app.modules.proxy._service.warmup import (
|
|
631
|
+
WarmupFailedAccountData as WarmupFailedAccountData,
|
|
632
|
+
)
|
|
633
|
+
from app.modules.proxy._service.warmup import (
|
|
634
|
+
WarmupSkippedAccountData as WarmupSkippedAccountData,
|
|
635
|
+
)
|
|
636
|
+
from app.modules.proxy._service.warmup import (
|
|
637
|
+
WarmupSubmittedAccountData as WarmupSubmittedAccountData,
|
|
638
|
+
)
|
|
639
|
+
from app.modules.proxy._service.warmup import (
|
|
640
|
+
_is_warmup_usage_eligible as _is_warmup_usage_eligible,
|
|
641
|
+
)
|
|
642
|
+
from app.modules.proxy._service.warmup import (
|
|
643
|
+
_materialize_warmup_account as _materialize_warmup_account,
|
|
644
|
+
)
|
|
645
|
+
from app.modules.proxy._service.warmup import (
|
|
646
|
+
_snapshot_warmup_account as _snapshot_warmup_account,
|
|
647
|
+
)
|
|
648
|
+
from app.modules.proxy._service.warmup import (
|
|
649
|
+
_WarmupAccountSnapshot as _WarmupAccountSnapshot,
|
|
650
|
+
)
|
|
651
|
+
from app.modules.proxy._service.warmup import (
|
|
652
|
+
_WarmupMixin,
|
|
653
|
+
)
|
|
654
|
+
from app.modules.proxy._service.warmup import (
|
|
655
|
+
_WarmupSubmitResult as _WarmupSubmitResult,
|
|
656
|
+
)
|
|
657
|
+
from app.modules.proxy._service.warmup import (
|
|
658
|
+
_WarmupUsageSnapshot as _WarmupUsageSnapshot,
|
|
659
|
+
)
|
|
660
|
+
from app.modules.proxy._service.websocket import (
|
|
661
|
+
_WebSocketMixin,
|
|
662
|
+
)
|
|
663
|
+
from app.modules.proxy._service.websocket.helpers import (
|
|
664
|
+
_app_error_to_websocket_event, # noqa: F401
|
|
665
|
+
_assign_websocket_response_id, # noqa: F401
|
|
666
|
+
_clear_websocket_stale_previous_response_cache, # noqa: F401
|
|
667
|
+
_draining_websocket_request_states, # noqa: F401
|
|
668
|
+
_find_websocket_request_state_by_response_id, # noqa: F401
|
|
669
|
+
_is_websocket_previous_response_output_item, # noqa: F401
|
|
670
|
+
_is_websocket_response_create, # noqa: F401
|
|
671
|
+
_match_websocket_request_state_for_anonymous_event, # noqa: F401
|
|
672
|
+
_match_websocket_request_state_for_precreated_terminal_event, # noqa: F401
|
|
673
|
+
_match_websocket_request_state_for_previous_response_error, # noqa: F401
|
|
674
|
+
_matching_websocket_request_states_for_missing_tool_output_error, # noqa: F401
|
|
675
|
+
_matching_websocket_request_states_for_previous_response_error, # noqa: F401
|
|
676
|
+
_maybe_rewrite_websocket_previous_response_not_found_event, # noqa: F401
|
|
677
|
+
_parse_websocket_payload, # noqa: F401
|
|
678
|
+
_pop_matching_websocket_request_states, # noqa: F401
|
|
679
|
+
_pop_replayable_precreated_websocket_request_state, # noqa: F401
|
|
680
|
+
_pop_terminal_websocket_request_state, # noqa: F401
|
|
681
|
+
_prepare_websocket_request_state_for_account_switch, # noqa: F401
|
|
682
|
+
_prepare_websocket_request_state_for_auth_replay, # noqa: F401
|
|
683
|
+
_prepare_websocket_request_state_for_visible_output_replay, # noqa: F401
|
|
684
|
+
_record_websocket_continuity_completion, # noqa: F401
|
|
685
|
+
_record_websocket_responses_lite_acceptance, # noqa: F401
|
|
686
|
+
_refresh_websocket_request_input_fingerprint_from_text, # noqa: F401
|
|
687
|
+
_release_websocket_response_create_gate, # noqa: F401
|
|
688
|
+
_rewrite_websocket_continuity_corruption_event, # noqa: F401
|
|
689
|
+
_rewrite_websocket_downstream_response_id, # noqa: F401
|
|
690
|
+
_rewrite_websocket_previous_response_owner_unavailable_event, # noqa: F401
|
|
691
|
+
_rewrite_websocket_suppressed_duplicate_tool_call_completion_event, # noqa: F401
|
|
692
|
+
_sanitize_websocket_connect_failure, # noqa: F401
|
|
693
|
+
_sanitize_websocket_previous_response_error, # noqa: F401
|
|
694
|
+
_sanitize_websocket_terminal_error_fields, # noqa: F401
|
|
695
|
+
_serialize_websocket_error_event, # noqa: F401
|
|
696
|
+
_trim_websocket_previous_response_input_items, # noqa: F401
|
|
697
|
+
_upstream_websocket_disconnect_message, # noqa: F401
|
|
698
|
+
_websocket_auth_failure_permanent_code, # noqa: F401
|
|
699
|
+
_websocket_auth_failure_requires_reauth, # noqa: F401
|
|
700
|
+
_websocket_auth_request_can_switch_account, # noqa: F401
|
|
701
|
+
_websocket_client_previous_response_full_resend_is_retry_safe, # noqa: F401
|
|
702
|
+
_websocket_connect_deadline, # noqa: F401
|
|
703
|
+
_websocket_continuity_anchor_for_payload, # noqa: F401
|
|
704
|
+
_websocket_continuity_error_fields, # noqa: F401
|
|
705
|
+
_websocket_continuity_response_ids, # noqa: F401
|
|
706
|
+
_websocket_downstream_response_id, # noqa: F401
|
|
707
|
+
_websocket_event_error_code, # noqa: F401
|
|
708
|
+
_websocket_event_error_message, # noqa: F401
|
|
709
|
+
_websocket_event_error_param, # noqa: F401
|
|
710
|
+
_websocket_event_error_payload, # noqa: F401
|
|
711
|
+
_websocket_event_error_type, # noqa: F401
|
|
712
|
+
_websocket_full_resend_conflicts_with_visible_pending, # noqa: F401
|
|
713
|
+
_websocket_input_item_type, # noqa: F401
|
|
714
|
+
_websocket_owner_pinned_quota_error_code, # noqa: F401
|
|
715
|
+
_websocket_precreated_auth_error_code, # noqa: F401
|
|
716
|
+
_websocket_precreated_retry_error_code, # noqa: F401
|
|
717
|
+
_websocket_receive_timeout_for_pending_requests, # noqa: F401
|
|
718
|
+
_websocket_request_text_is_account_neutral_fresh_replay, # noqa: F401
|
|
719
|
+
_websocket_response_id, # noqa: F401
|
|
720
|
+
_websocket_top_level_error_payload, # noqa: F401
|
|
721
|
+
_wrapped_websocket_error_event, # noqa: F401
|
|
722
|
+
)
|
|
723
|
+
from app.modules.proxy.affinity import (
|
|
724
|
+
_AffinityPolicy,
|
|
725
|
+
_CodexSessionSource,
|
|
726
|
+
_sticky_key_for_thread_goal_request,
|
|
727
|
+
_sticky_key_from_session_header, # noqa: F401
|
|
728
|
+
)
|
|
729
|
+
from app.modules.proxy.affinity import (
|
|
730
|
+
_owner_lookup_session_id_from_headers as _owner_lookup_session_id_from_headers,
|
|
731
|
+
)
|
|
732
|
+
from app.modules.proxy.affinity import (
|
|
733
|
+
_sticky_key_for_responses_request as _sticky_key_for_responses_request,
|
|
734
|
+
)
|
|
735
|
+
from app.modules.proxy.capability_routing import CapabilityRouter
|
|
736
|
+
from app.modules.proxy.durable_bridge_coordinator import (
|
|
737
|
+
DurableBridgeLookup as DurableBridgeLookup,
|
|
738
|
+
)
|
|
739
|
+
from app.modules.proxy.durable_bridge_coordinator import (
|
|
740
|
+
DurableBridgeSessionCoordinator,
|
|
741
|
+
)
|
|
742
|
+
from app.modules.proxy.helpers import (
|
|
743
|
+
_apply_error_metadata,
|
|
744
|
+
_header_account_id,
|
|
745
|
+
_normalize_error_code,
|
|
746
|
+
_parse_openai_error,
|
|
747
|
+
_upstream_error_from_openai,
|
|
748
|
+
)
|
|
749
|
+
from app.modules.proxy.http_bridge_event_batcher import HttpBridgeOperationEventBatcher
|
|
750
|
+
from app.modules.proxy.http_bridge_forwarding import (
|
|
751
|
+
HTTPBridgeForwardContext as HTTPBridgeForwardContext,
|
|
752
|
+
)
|
|
753
|
+
from app.modules.proxy.http_bridge_forwarding import (
|
|
754
|
+
HTTPBridgeOwnerClient,
|
|
755
|
+
)
|
|
756
|
+
from app.modules.proxy.http_bridge_forwarding import (
|
|
757
|
+
OwnerForwardRelayFailure as OwnerForwardRelayFailure,
|
|
758
|
+
)
|
|
759
|
+
from app.modules.proxy.load_balancer import (
|
|
760
|
+
AccountConcurrencyCaps,
|
|
761
|
+
AccountLease,
|
|
762
|
+
AccountLeaseKind,
|
|
763
|
+
AccountSelection,
|
|
764
|
+
LoadBalancer,
|
|
765
|
+
RoutingTunables,
|
|
766
|
+
effective_account_concurrency_caps,
|
|
767
|
+
effective_routing_tunables,
|
|
768
|
+
)
|
|
769
|
+
from app.modules.proxy.repo_bundle import ProxyRepoFactory
|
|
770
|
+
from app.modules.proxy.ring_membership import (
|
|
771
|
+
RingMembershipService,
|
|
772
|
+
)
|
|
773
|
+
from app.modules.proxy.selection_errors import selection_failure_response
|
|
774
|
+
from app.modules.proxy.work_admission import (
|
|
775
|
+
ADMISSION_WAIT_TIMEOUT_SECONDS,
|
|
776
|
+
COMPACT_RESPONSE_CREATE_LIMIT,
|
|
777
|
+
TOKEN_REFRESH_LIMIT,
|
|
778
|
+
UPSTREAM_WEBSOCKET_CONNECT_LIMIT,
|
|
779
|
+
WorkAdmissionController,
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
def get_settings() -> _Settings:
|
|
784
|
+
"""Startup ``Settings`` with the request-bound dashboard overrides applied.
|
|
785
|
+
|
|
786
|
+
Every proxy consumer reads settings through this facade (directly or via
|
|
787
|
+
``_service_get_settings()``), so the dashboard-managed timeouts (C2-1) take
|
|
788
|
+
effect here without touching each call site; outside a bound request context
|
|
789
|
+
the environment values apply unchanged.
|
|
790
|
+
"""
|
|
791
|
+
return with_dashboard_overrides(get_environment_settings())
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
logger = logging.getLogger(__name__)
|
|
795
|
+
|
|
796
|
+
_ResponsesPayloadT = TypeVar("_ResponsesPayloadT", ResponsesRequest, ResponsesCompactRequest)
|
|
797
|
+
_DOWNSTREAM_WEBSOCKET_IDLE_CLOSE_REASON = "Idle downstream websocket timeout"
|
|
798
|
+
_DOWNSTREAM_WEBSOCKET_RECEIVE_POLL_SECONDS = 1.0
|
|
799
|
+
# Keep the first HTTP bridge liveness frame behind the API layer's startup
|
|
800
|
+
# error probe window. If a keepalive becomes the first yielded chunk, the HTTP
|
|
801
|
+
# status is committed as 200 and startup ProxyResponseError handling is masked.
|
|
802
|
+
_HTTP_BRIDGE_STARTUP_KEEPALIVE_GRACE_SECONDS = 0.5
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
def _proxy_admission_wait_timeout_seconds() -> float:
|
|
806
|
+
# Module-level indirection so the HTTP bridge helpers and tests share one
|
|
807
|
+
# patch point for the fixed admission wait.
|
|
808
|
+
return ADMISSION_WAIT_TIMEOUT_SECONDS
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
# Maximum time (seconds) to wait for a prewarm upstream response before
|
|
812
|
+
# giving up and letting the actual request proceed without prewarming.
|
|
813
|
+
# A blocked prewarm holds the response_create_gate semaphore and prevents
|
|
814
|
+
# the real request from being sent, leading to an indefinite :keepalive hang.
|
|
815
|
+
_PREWARM_RESPONSE_TIMEOUT_SECONDS = 2.0
|
|
816
|
+
_HTTP_BRIDGE_BACKGROUND_CLOSE_TIMEOUT_SECONDS = 5.0
|
|
817
|
+
_HTTP_BRIDGE_BACKGROUND_CLEANUP_WARN_THRESHOLD = 100
|
|
818
|
+
# Maximum consecutive keepalive frames sent before terminating the stream.
|
|
819
|
+
# 6 × 10s (default interval) = 60s. Combined with the 0.5s startup-probe
|
|
820
|
+
# window this ensures the client sees a terminal event within ≈70s when the
|
|
821
|
+
# upstream silently stops responding.
|
|
822
|
+
_STREAM_KEEPALIVE_MAX_COUNT = 6
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
_TEXT_DELTA_EVENT_TYPES = frozenset({"response.output_text.delta", "response.refusal.delta"})
|
|
826
|
+
_TEXT_DONE_CONTENT_PART_TYPES = frozenset({"output_text", "refusal"})
|
|
827
|
+
_REQUEST_TRANSPORT_HTTP = "http"
|
|
828
|
+
_COMPACT_SAME_CONTRACT_RETRY_BUDGET = 1
|
|
829
|
+
_ACCOUNT_RECOVERY_RETRY_CODES = frozenset(
|
|
830
|
+
{
|
|
831
|
+
"rate_limit_exceeded",
|
|
832
|
+
"usage_limit_reached",
|
|
833
|
+
"insufficient_quota",
|
|
834
|
+
"usage_not_included",
|
|
835
|
+
"quota_exceeded",
|
|
836
|
+
*PERMANENT_FAILURE_CODES.keys(),
|
|
837
|
+
}
|
|
838
|
+
)
|
|
839
|
+
|
|
840
|
+
_TRANSIENT_RETRY_CODES = frozenset(
|
|
841
|
+
{
|
|
842
|
+
"overloaded_error",
|
|
843
|
+
"server_error",
|
|
844
|
+
"server_is_overloaded",
|
|
845
|
+
"stream_incomplete",
|
|
846
|
+
"stream_idle_timeout",
|
|
847
|
+
"upstream_request_timeout",
|
|
848
|
+
}
|
|
849
|
+
)
|
|
850
|
+
_UPSTREAM_UNAVAILABLE_TRANSIENT_MESSAGE_MARKERS = (
|
|
851
|
+
"broken pipe",
|
|
852
|
+
"cannot connect",
|
|
853
|
+
"connection aborted",
|
|
854
|
+
"connection closed",
|
|
855
|
+
"connection reset",
|
|
856
|
+
"keepalive ping timeout",
|
|
857
|
+
"no close frame",
|
|
858
|
+
"server disconnected",
|
|
859
|
+
"timed out",
|
|
860
|
+
"timeout",
|
|
861
|
+
"upstream closed",
|
|
862
|
+
)
|
|
863
|
+
_UPSTREAM_UNAVAILABLE_NON_TRANSIENT_MESSAGE_MARKERS = (
|
|
864
|
+
"certificate verify failed",
|
|
865
|
+
"clientconnectorcertificateerror",
|
|
866
|
+
"sslcertverificationerror",
|
|
867
|
+
)
|
|
868
|
+
_UPSTREAM_CLOSE_CODES_SKIP_SAME_ACCOUNT_RETRY = frozenset({1011})
|
|
869
|
+
_MAX_TRANSIENT_SAME_ACCOUNT_RETRIES = 3
|
|
870
|
+
_COMPACT_MAX_ACCOUNT_ATTEMPTS = 2
|
|
871
|
+
_STREAM_MAX_ACCOUNT_ATTEMPTS = 3
|
|
872
|
+
_WEBSOCKET_MAX_ACCOUNT_ATTEMPTS = 3
|
|
873
|
+
_WEBSOCKET_TRANSPARENT_REPLAY_ERROR_CODES = frozenset(
|
|
874
|
+
{
|
|
875
|
+
"rate_limit_exceeded",
|
|
876
|
+
"usage_limit_reached",
|
|
877
|
+
"insufficient_quota",
|
|
878
|
+
"usage_not_included",
|
|
879
|
+
"quota_exceeded",
|
|
880
|
+
"overloaded_error",
|
|
881
|
+
"server_is_overloaded",
|
|
882
|
+
}
|
|
883
|
+
)
|
|
884
|
+
_WEBSOCKET_AUTH_FAILURE_CODES = frozenset({"invalid_api_key", "invalid_authentication", "token_invalidated"})
|
|
885
|
+
_WEBSOCKET_REAUTH_REQUIRED_MESSAGE_MARKERS = (
|
|
886
|
+
"session has ended",
|
|
887
|
+
"session expired",
|
|
888
|
+
"log in again",
|
|
889
|
+
"login again",
|
|
890
|
+
"reauth",
|
|
891
|
+
"re-auth",
|
|
892
|
+
)
|
|
893
|
+
_WEBSOCKET_SESSION_EXPIRED_FAILURE_CODE = "account_session_expired"
|
|
894
|
+
_WEBSOCKET_AUTH_INVALIDATED_FAILURE_CODE = "account_auth_invalidated"
|
|
895
|
+
_SUPPRESSED_DUPLICATE_TOOL_CALL_MESSAGE = (
|
|
896
|
+
"Suppressed duplicate side-effect tool call; upstream response cannot be continued safely."
|
|
897
|
+
)
|
|
898
|
+
_SUPPRESSED_DUPLICATE_TOOL_CALL_ERROR_CODE = "duplicate_tool_call_replay_suppressed"
|
|
899
|
+
_WEBSOCKET_PREVIOUS_RESPONSE_ACCOUNT_CACHE_LIMIT = 4096
|
|
900
|
+
_WEBSOCKET_CONTINUITY_CACHE_LIMIT = 4096
|
|
901
|
+
_SECURITY_WORK_AUTHORIZATION_REQUIRED_CODE = "security_work_authorization_required"
|
|
902
|
+
_NO_SECURITY_WORK_AUTHORIZED_ACCOUNTS_CODE = "no_security_work_authorized_accounts"
|
|
903
|
+
_SECURITY_WORK_AUTHORIZATION_REQUIRED_HINTS = (
|
|
904
|
+
"flagged for possible cybersecurity risk",
|
|
905
|
+
"authorized for security work",
|
|
906
|
+
"chatgpt.com/cyber",
|
|
907
|
+
)
|
|
908
|
+
_SECURITY_WORK_RETRY_MESSAGE = (
|
|
909
|
+
"Upstream flagged this request as possible cybersecurity work. "
|
|
910
|
+
"codex-lb is retrying on an account marked as authorized for security work."
|
|
911
|
+
)
|
|
912
|
+
_SECURITY_WORK_NO_AUTHORIZED_ACCOUNTS_MESSAGE = (
|
|
913
|
+
"Upstream flagged this request as possible cybersecurity work, but no account is marked as authorized for "
|
|
914
|
+
"security work. codex-lb is continuing with normal account selection; the upstream request may still fail until "
|
|
915
|
+
"an account with Trusted Access for Cyber is marked as security-work-authorized."
|
|
916
|
+
)
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
class ProxyService(
|
|
920
|
+
_ApiKeyUsageMixin,
|
|
921
|
+
_RequestLogMixin,
|
|
922
|
+
_RateLimitMixin,
|
|
923
|
+
_WarmupMixin,
|
|
924
|
+
_FileOpsMixin,
|
|
925
|
+
_TranscribeMixin,
|
|
926
|
+
_CodexControlMixin,
|
|
927
|
+
_RealtimeLiveMixin,
|
|
928
|
+
_CompactMixin,
|
|
929
|
+
_StreamingMixin,
|
|
930
|
+
_WebSocketMixin,
|
|
931
|
+
_HTTPBridgeRetryCircuitMixin,
|
|
932
|
+
_HTTPBridgeMixin,
|
|
933
|
+
_ClockBudgetMixin,
|
|
934
|
+
):
|
|
935
|
+
def __init__(
|
|
936
|
+
self,
|
|
937
|
+
repo_factory: ProxyRepoFactory,
|
|
938
|
+
*,
|
|
939
|
+
clock: Clock = REAL_CLOCK,
|
|
940
|
+
scheduler: Scheduler = REAL_SCHEDULER,
|
|
941
|
+
live_websocket_connector: LiveWebSocketConnector = connect_live_websocket,
|
|
942
|
+
) -> None:
|
|
943
|
+
self._repo_factory = repo_factory
|
|
944
|
+
self._clock, self._scheduler = clock, scheduler
|
|
945
|
+
self._encryptor = TokenEncryptor()
|
|
946
|
+
self._load_balancer = LoadBalancer(repo_factory, encryptor=self._encryptor, clock=clock)
|
|
947
|
+
self._capability_router = CapabilityRouter(repo_factory)
|
|
948
|
+
self._live_websocket_connector = live_websocket_connector
|
|
949
|
+
self._ring_membership = RingMembershipService(SessionLocal)
|
|
950
|
+
self._durable_bridge = DurableBridgeSessionCoordinator(SessionLocal)
|
|
951
|
+
self._http_bridge_operation_event_batcher = HttpBridgeOperationEventBatcher.from_settings(self._durable_bridge)
|
|
952
|
+
self._http_bridge_owner_client = HTTPBridgeOwnerClient()
|
|
953
|
+
self._initialize_http_bridge_session_registry()
|
|
954
|
+
_initialize_http_bridge_retry_circuit(self, _clear_websocket_stale_previous_response_cache)
|
|
955
|
+
self._http_bridge_account_timeout_failures, self._http_bridge_account_timeout_lock = {}, asyncio.Lock()
|
|
956
|
+
self._http_bridge_inflight_sessions: dict[_HTTPBridgeSessionKey, asyncio.Future[_HTTPBridgeSession]] = {}
|
|
957
|
+
self._http_bridge_turn_state_index: dict[tuple[str, str | None], _HTTPBridgeSessionKey] = {}
|
|
958
|
+
self._http_bridge_previous_response_index: dict[tuple[str, str | None], _HTTPBridgeSessionKey] = {}
|
|
959
|
+
self._websocket_previous_response_account_index: dict[tuple[str, str | None, str | None], str] = {}
|
|
960
|
+
self._websocket_continuity_index: dict[tuple[str, str | None], _WebSocketContinuityState] = {}
|
|
961
|
+
self._background_cleanup_tasks: set[asyncio.Task[None]] = set()
|
|
962
|
+
self._http_bridge_background_cleanup_failed = False
|
|
963
|
+
self._stream_api_key_release_retry_semaphore = asyncio.Semaphore(_STREAM_API_KEY_RELEASE_RETRY_MAX_CONCURRENCY)
|
|
964
|
+
self._file_pin_session_factory = SessionLocal
|
|
965
|
+
self._http_bridge_lock = anyio.Lock()
|
|
966
|
+
self._work_admission: WorkAdmissionController | None = None
|
|
967
|
+
self._request_log_tasks: set[asyncio.Task[None]] = set()
|
|
968
|
+
|
|
969
|
+
def _get_work_admission(self) -> WorkAdmissionController:
|
|
970
|
+
if self._work_admission is None:
|
|
971
|
+
self._work_admission = WorkAdmissionController(
|
|
972
|
+
token_refresh_limit=TOKEN_REFRESH_LIMIT,
|
|
973
|
+
websocket_connect_limit=UPSTREAM_WEBSOCKET_CONNECT_LIMIT,
|
|
974
|
+
response_create_limit=get_settings().proxy_response_create_limit,
|
|
975
|
+
compact_response_create_limit=COMPACT_RESPONSE_CREATE_LIMIT,
|
|
976
|
+
admission_wait_timeout_seconds=_proxy_admission_wait_timeout_seconds(),
|
|
977
|
+
scheduler=self._scheduler,
|
|
978
|
+
)
|
|
979
|
+
return self._work_admission
|
|
980
|
+
|
|
981
|
+
async def thread_goal_request(
|
|
982
|
+
self,
|
|
983
|
+
operation: str,
|
|
984
|
+
payload: Mapping[str, JsonValue],
|
|
985
|
+
headers: Mapping[str, str],
|
|
986
|
+
*,
|
|
987
|
+
method: str = "POST",
|
|
988
|
+
codex_session_affinity: bool = True,
|
|
989
|
+
api_key: ApiKeyData | None = None,
|
|
990
|
+
) -> dict[str, JsonValue]:
|
|
991
|
+
filtered = filter_inbound_headers(headers)
|
|
992
|
+
useragent, useragent_group, conversation_id = _request_log_client_fields(headers)
|
|
993
|
+
request_id = get_request_id() or ensure_request_id(None)
|
|
994
|
+
start = self._clock.monotonic()
|
|
995
|
+
base_settings = get_settings()
|
|
996
|
+
deadline = start + base_settings.proxy_request_budget_seconds
|
|
997
|
+
settings = await get_settings_cache().get()
|
|
998
|
+
bind_resilience_toggles(settings, startup_settings=base_settings) # C2-3 resilience toggles
|
|
999
|
+
affinity = _sticky_key_for_thread_goal_request(
|
|
1000
|
+
payload, headers, codex_session_affinity, settings.openai_cache_affinity_max_age_seconds
|
|
1001
|
+
)
|
|
1002
|
+
selection_model = api_key.enforced_model if api_key is not None else None
|
|
1003
|
+
routing_strategy = _routing_strategy(settings)
|
|
1004
|
+
account_id_value: str | None = None
|
|
1005
|
+
log_status = "error"
|
|
1006
|
+
log_error_code: str | None = None
|
|
1007
|
+
log_error_message: str | None = None
|
|
1008
|
+
failure_metadata = _RequestLogFailureMetadata()
|
|
1009
|
+
route_mode: str | None = None
|
|
1010
|
+
route_pool_id: str | None = None
|
|
1011
|
+
route_endpoint_id: str | None = None
|
|
1012
|
+
route_fallback_used: bool | None = None
|
|
1013
|
+
route_fail_closed_reason: str | None = None
|
|
1014
|
+
request_kind = f"thread_goal_{operation}"
|
|
1015
|
+
|
|
1016
|
+
try:
|
|
1017
|
+
selection = await self._select_account_with_budget_compatible(
|
|
1018
|
+
deadline,
|
|
1019
|
+
request_id=request_id,
|
|
1020
|
+
kind=request_kind,
|
|
1021
|
+
api_key=api_key,
|
|
1022
|
+
affinity_policy=affinity,
|
|
1023
|
+
prefer_earlier_reset_accounts=settings.prefer_earlier_reset_accounts,
|
|
1024
|
+
prefer_earlier_reset_window=_prefer_earlier_reset_window(settings),
|
|
1025
|
+
routing_strategy=routing_strategy,
|
|
1026
|
+
model=selection_model,
|
|
1027
|
+
)
|
|
1028
|
+
account = selection.account
|
|
1029
|
+
if not account:
|
|
1030
|
+
account = await self._select_codex_control_account_without_budget(
|
|
1031
|
+
affinity=affinity,
|
|
1032
|
+
api_key=api_key,
|
|
1033
|
+
traffic_class=TRAFFIC_CLASS_OPPORTUNISTIC
|
|
1034
|
+
if api_key is not None and api_key.traffic_class == TRAFFIC_CLASS_OPPORTUNISTIC
|
|
1035
|
+
else TRAFFIC_CLASS_FOREGROUND,
|
|
1036
|
+
prefer_earlier_reset_window=_prefer_earlier_reset_window(settings),
|
|
1037
|
+
)
|
|
1038
|
+
if account is None:
|
|
1039
|
+
log_error_code = selection.error_code or "no_accounts"
|
|
1040
|
+
log_error_message = selection.error_message or "No active accounts available"
|
|
1041
|
+
status_code, error_payload = selection_failure_response(selection)
|
|
1042
|
+
raise ProxyResponseError(status_code, error_payload)
|
|
1043
|
+
account_id_value = account.id
|
|
1044
|
+
|
|
1045
|
+
async def _call_goal(target: Account) -> dict[str, JsonValue]:
|
|
1046
|
+
nonlocal route_fallback_used, route_mode, route_pool_id, route_endpoint_id
|
|
1047
|
+
access_token = self._encryptor.decrypt(target.access_token_encrypted)
|
|
1048
|
+
upstream_account_id = _header_account_id(target.chatgpt_account_id)
|
|
1049
|
+
remaining_budget = self._remaining_budget_seconds(deadline)
|
|
1050
|
+
if remaining_budget <= 0:
|
|
1051
|
+
logger.warning(
|
|
1052
|
+
"Thread goal request budget exhausted before upstream call request_id=%s operation=%s "
|
|
1053
|
+
"account_id=%s",
|
|
1054
|
+
request_id,
|
|
1055
|
+
operation,
|
|
1056
|
+
target.id,
|
|
1057
|
+
)
|
|
1058
|
+
_raise_proxy_budget_exhausted()
|
|
1059
|
+
route = await self._resolve_upstream_route_for_account(target, operation=request_kind)
|
|
1060
|
+
if route is not None:
|
|
1061
|
+
route_mode = route.mode
|
|
1062
|
+
route_pool_id = route.pool_id
|
|
1063
|
+
route_endpoint_id = route.endpoint_id
|
|
1064
|
+
route_trace = UpstreamProxyRouteTrace()
|
|
1065
|
+
try:
|
|
1066
|
+
return await core_thread_goal_request(
|
|
1067
|
+
operation,
|
|
1068
|
+
payload,
|
|
1069
|
+
filtered,
|
|
1070
|
+
access_token,
|
|
1071
|
+
upstream_account_id,
|
|
1072
|
+
method=method,
|
|
1073
|
+
timeout_seconds=remaining_budget,
|
|
1074
|
+
route=route,
|
|
1075
|
+
allow_direct_egress=route is None,
|
|
1076
|
+
route_trace=route_trace,
|
|
1077
|
+
)
|
|
1078
|
+
finally:
|
|
1079
|
+
if route_trace.mode is not None:
|
|
1080
|
+
route_mode = route_trace.mode
|
|
1081
|
+
route_pool_id = route_trace.pool_id
|
|
1082
|
+
route_endpoint_id = route_trace.endpoint_id
|
|
1083
|
+
route_fallback_used = route_trace.fallback_used
|
|
1084
|
+
|
|
1085
|
+
async def _select_goal_failover(excluded_account_ids: set[str]) -> AccountSelection:
|
|
1086
|
+
return await self._select_account_with_budget(
|
|
1087
|
+
deadline,
|
|
1088
|
+
request_id=request_id,
|
|
1089
|
+
kind=request_kind,
|
|
1090
|
+
api_key=api_key,
|
|
1091
|
+
sticky_key=affinity.selection_key,
|
|
1092
|
+
sticky_kind=affinity.kind,
|
|
1093
|
+
reallocate_sticky=affinity.reallocate_sticky,
|
|
1094
|
+
sticky_source=affinity.codex_session_source,
|
|
1095
|
+
legacy_sticky_key=affinity.legacy_selection_key,
|
|
1096
|
+
legacy_continuity_source=affinity.legacy_continuity_source,
|
|
1097
|
+
sticky_seed_key=affinity.seed_selection_key,
|
|
1098
|
+
sticky_seed_kind=affinity.seed_selection_kind,
|
|
1099
|
+
sticky_max_age_seconds=affinity.max_age_seconds,
|
|
1100
|
+
prefer_earlier_reset_accounts=settings.prefer_earlier_reset_accounts,
|
|
1101
|
+
routing_strategy=routing_strategy,
|
|
1102
|
+
model=selection_model,
|
|
1103
|
+
exclude_account_ids=excluded_account_ids,
|
|
1104
|
+
)
|
|
1105
|
+
|
|
1106
|
+
try:
|
|
1107
|
+
account = await self._ensure_previsible_unary_fresh_with_failover(
|
|
1108
|
+
account,
|
|
1109
|
+
deadline=deadline,
|
|
1110
|
+
request_id=request_id,
|
|
1111
|
+
kind=request_kind,
|
|
1112
|
+
select_next_account=_select_goal_failover,
|
|
1113
|
+
)
|
|
1114
|
+
account_id_value = account.id
|
|
1115
|
+
response = await _call_goal(account)
|
|
1116
|
+
await self._load_balancer.record_success(account)
|
|
1117
|
+
log_status = "success"
|
|
1118
|
+
return response
|
|
1119
|
+
except RefreshError as refresh_exc:
|
|
1120
|
+
if refresh_exc.is_permanent:
|
|
1121
|
+
failed_account = _refresh_error_failed_account(refresh_exc, account)
|
|
1122
|
+
account_id_value = failed_account.id
|
|
1123
|
+
await self._load_balancer.mark_permanent_failure(failed_account, refresh_exc.code)
|
|
1124
|
+
raise ProxyResponseError(
|
|
1125
|
+
401,
|
|
1126
|
+
openai_error(
|
|
1127
|
+
"invalid_api_key",
|
|
1128
|
+
refresh_exc.message,
|
|
1129
|
+
error_type="invalid_request_error",
|
|
1130
|
+
),
|
|
1131
|
+
) from refresh_exc
|
|
1132
|
+
except ProxyResponseError as exc:
|
|
1133
|
+
if exc.status_code != 401:
|
|
1134
|
+
failover = await self._retry_previsible_unary_call_failover(
|
|
1135
|
+
exc,
|
|
1136
|
+
account,
|
|
1137
|
+
deadline=deadline,
|
|
1138
|
+
select_next_account=_select_goal_failover,
|
|
1139
|
+
call_next=_call_goal,
|
|
1140
|
+
)
|
|
1141
|
+
if failover is not None:
|
|
1142
|
+
account, response = failover
|
|
1143
|
+
account_id_value = account.id
|
|
1144
|
+
log_status = "success"
|
|
1145
|
+
return response
|
|
1146
|
+
if exc.status_code == 401:
|
|
1147
|
+
try:
|
|
1148
|
+
remaining_budget = self._remaining_budget_seconds(deadline)
|
|
1149
|
+
if remaining_budget <= 0:
|
|
1150
|
+
logger.warning(
|
|
1151
|
+
"Thread goal request budget exhausted before forced refresh retry request_id=%s "
|
|
1152
|
+
"operation=%s account_id=%s",
|
|
1153
|
+
request_id,
|
|
1154
|
+
operation,
|
|
1155
|
+
account.id,
|
|
1156
|
+
)
|
|
1157
|
+
_raise_proxy_budget_exhausted()
|
|
1158
|
+
try:
|
|
1159
|
+
account = await self._ensure_previsible_unary_fresh_with_failover(
|
|
1160
|
+
account,
|
|
1161
|
+
deadline=deadline,
|
|
1162
|
+
request_id=request_id,
|
|
1163
|
+
kind=request_kind,
|
|
1164
|
+
select_next_account=_select_goal_failover,
|
|
1165
|
+
force=True,
|
|
1166
|
+
)
|
|
1167
|
+
except ProxyResponseError as refresh_failover_exc:
|
|
1168
|
+
failed_account = _proxy_response_failed_account(refresh_failover_exc, account)
|
|
1169
|
+
account_id_value = failed_account.id
|
|
1170
|
+
await self._handle_proxy_error(failed_account, refresh_failover_exc)
|
|
1171
|
+
raise
|
|
1172
|
+
account_id_value = account.id
|
|
1173
|
+
try:
|
|
1174
|
+
response = await _call_goal(account)
|
|
1175
|
+
await self._load_balancer.record_success(account)
|
|
1176
|
+
log_status = "success"
|
|
1177
|
+
return response
|
|
1178
|
+
except ProxyResponseError as retry_exc:
|
|
1179
|
+
await self._handle_proxy_error(account, retry_exc)
|
|
1180
|
+
if retry_exc.status_code == 401:
|
|
1181
|
+
selection = await self._select_account_with_budget_compatible(
|
|
1182
|
+
deadline,
|
|
1183
|
+
request_id=request_id,
|
|
1184
|
+
kind=request_kind,
|
|
1185
|
+
api_key=api_key,
|
|
1186
|
+
affinity_policy=affinity,
|
|
1187
|
+
prefer_earlier_reset_accounts=settings.prefer_earlier_reset_accounts,
|
|
1188
|
+
prefer_earlier_reset_window=_prefer_earlier_reset_window(settings),
|
|
1189
|
+
routing_strategy=routing_strategy,
|
|
1190
|
+
model=selection_model,
|
|
1191
|
+
exclude_account_ids={account.id},
|
|
1192
|
+
)
|
|
1193
|
+
if selection.account is not None:
|
|
1194
|
+
account = selection.account
|
|
1195
|
+
account_id_value = account.id
|
|
1196
|
+
account = await self._ensure_fresh_with_budget_or_auth_error(
|
|
1197
|
+
account,
|
|
1198
|
+
timeout_seconds=self._remaining_budget_seconds(deadline),
|
|
1199
|
+
)
|
|
1200
|
+
try:
|
|
1201
|
+
response = await _call_goal(account)
|
|
1202
|
+
await self._load_balancer.record_success(account)
|
|
1203
|
+
log_status = "success"
|
|
1204
|
+
return response
|
|
1205
|
+
except ProxyResponseError as failover_exc:
|
|
1206
|
+
await self._handle_proxy_error(account, failover_exc)
|
|
1207
|
+
raise
|
|
1208
|
+
raise
|
|
1209
|
+
except RefreshError as refresh_exc:
|
|
1210
|
+
if refresh_exc.is_permanent:
|
|
1211
|
+
failed_account = _refresh_error_failed_account(refresh_exc, account)
|
|
1212
|
+
account_id_value = failed_account.id
|
|
1213
|
+
await self._load_balancer.mark_permanent_failure(failed_account, refresh_exc.code)
|
|
1214
|
+
raise exc
|
|
1215
|
+
except (aiohttp.ClientError, asyncio.TimeoutError) as timeout_exc:
|
|
1216
|
+
logger.warning(
|
|
1217
|
+
"Thread goal forced refresh/connect failed request_id=%s operation=%s account_id=%s",
|
|
1218
|
+
request_id,
|
|
1219
|
+
operation,
|
|
1220
|
+
account.id,
|
|
1221
|
+
exc_info=True,
|
|
1222
|
+
)
|
|
1223
|
+
_raise_proxy_unavailable(str(timeout_exc) or "Request to upstream timed out")
|
|
1224
|
+
if operation == "get" and _is_missing_thread_goal_protocol_error(exc):
|
|
1225
|
+
log_status = "success"
|
|
1226
|
+
return {"goal": None}
|
|
1227
|
+
failed_account = _proxy_response_failed_account(exc, account)
|
|
1228
|
+
account_id_value = failed_account.id
|
|
1229
|
+
await self._handle_proxy_error(failed_account, exc)
|
|
1230
|
+
raise
|
|
1231
|
+
except ProxyResponseError as exc:
|
|
1232
|
+
failed_account = getattr(exc, _FAILED_ACCOUNT_ATTR, None)
|
|
1233
|
+
if isinstance(failed_account, Account):
|
|
1234
|
+
account_id_value = failed_account.id
|
|
1235
|
+
failure_metadata = _request_log_failure_metadata(exc)
|
|
1236
|
+
error = _parse_openai_error(exc.payload)
|
|
1237
|
+
log_error_code = log_error_code or _normalize_error_code(
|
|
1238
|
+
error.code if error else None,
|
|
1239
|
+
error.type if error else None,
|
|
1240
|
+
)
|
|
1241
|
+
log_error_message = log_error_message or (error.message if error else None)
|
|
1242
|
+
raise
|
|
1243
|
+
except UpstreamProxyRouteError as exc:
|
|
1244
|
+
route_fail_closed_reason = exc.reason
|
|
1245
|
+
log_error_code = "upstream_proxy_unavailable"
|
|
1246
|
+
log_error_message = exc.reason
|
|
1247
|
+
raise ProxyResponseError(
|
|
1248
|
+
502,
|
|
1249
|
+
openai_error("upstream_proxy_unavailable", f"Upstream proxy route unavailable: {exc.reason}"),
|
|
1250
|
+
) from exc
|
|
1251
|
+
finally:
|
|
1252
|
+
await self._write_request_log(
|
|
1253
|
+
account_id=account_id_value,
|
|
1254
|
+
api_key=api_key,
|
|
1255
|
+
request_id=request_id,
|
|
1256
|
+
model=None,
|
|
1257
|
+
latency_ms=int((self._clock.monotonic() - start) * 1000),
|
|
1258
|
+
status=log_status,
|
|
1259
|
+
error_code=log_error_code,
|
|
1260
|
+
error_message=log_error_message,
|
|
1261
|
+
transport=_REQUEST_TRANSPORT_HTTP,
|
|
1262
|
+
failure_phase=failure_metadata.failure_phase,
|
|
1263
|
+
failure_detail=failure_metadata.failure_detail,
|
|
1264
|
+
failure_exception_type=failure_metadata.failure_exception_type,
|
|
1265
|
+
upstream_status_code=failure_metadata.upstream_status_code,
|
|
1266
|
+
upstream_error_code=failure_metadata.upstream_error_code,
|
|
1267
|
+
bridge_stage=failure_metadata.bridge_stage,
|
|
1268
|
+
upstream_proxy_route_mode=route_mode,
|
|
1269
|
+
upstream_proxy_pool_id=route_pool_id,
|
|
1270
|
+
upstream_proxy_endpoint_id=route_endpoint_id,
|
|
1271
|
+
upstream_proxy_fallback_used=route_fallback_used if route_endpoint_id else None,
|
|
1272
|
+
upstream_proxy_fail_closed_reason=route_fail_closed_reason,
|
|
1273
|
+
useragent=useragent,
|
|
1274
|
+
useragent_group=useragent_group,
|
|
1275
|
+
conversation_id=conversation_id,
|
|
1276
|
+
)
|
|
1277
|
+
|
|
1278
|
+
async def _acquire_request_state_response_create_admission(
|
|
1279
|
+
self,
|
|
1280
|
+
request_state: _WebSocketRequestState,
|
|
1281
|
+
*,
|
|
1282
|
+
response_create_gate: asyncio.Semaphore,
|
|
1283
|
+
bridge_session: "_HTTPBridgeSession | None" = None,
|
|
1284
|
+
compact: bool = False,
|
|
1285
|
+
account_id: str | None = None,
|
|
1286
|
+
surface: str = "websocket",
|
|
1287
|
+
routing_tunables: RoutingTunables | None = None,
|
|
1288
|
+
dashboard_settings: DashboardSettings | None = None,
|
|
1289
|
+
) -> None:
|
|
1290
|
+
scheduler = self._scheduler
|
|
1291
|
+
timeout_seconds = _proxy_admission_wait_timeout_seconds()
|
|
1292
|
+
if bridge_session is not None:
|
|
1293
|
+
timeout_seconds = _http_bridge_admission_timeout_seconds(
|
|
1294
|
+
request_state, timeout_seconds, get_settings(), now=self._clock.monotonic()
|
|
1295
|
+
)
|
|
1296
|
+
request_state.response_create_gate = response_create_gate
|
|
1297
|
+
request_state.response_create_gate_wait_started_at = self._clock.monotonic()
|
|
1298
|
+
if account_id is not None:
|
|
1299
|
+
# One cached snapshot for this lease operation. A caller that
|
|
1300
|
+
# already resolved the row for the same turn passes it in, and then
|
|
1301
|
+
# this helper awaits nothing here at all -- which the prewarm path
|
|
1302
|
+
# needs: it admits its warm-up while holding the session's
|
|
1303
|
+
# ``prewarm_lock``, and a cache refresh behind this read runs a DB
|
|
1304
|
+
# query under a process-global lock, so one stalled refresh would
|
|
1305
|
+
# suspend that critical section (issues #1971/#1972 wedged every
|
|
1306
|
+
# keyed submit on exactly this pattern). Tunables resolved by the
|
|
1307
|
+
# caller (bridge submit) still override the snapshot's.
|
|
1308
|
+
settings = dashboard_settings if dashboard_settings is not None else await get_settings_cache().get()
|
|
1309
|
+
request_state.account_response_create_lease = await self._acquire_account_response_create_lease_or_overload(
|
|
1310
|
+
account_id=account_id,
|
|
1311
|
+
request_id=request_state.request_id,
|
|
1312
|
+
surface=surface,
|
|
1313
|
+
concurrency_caps=effective_account_concurrency_caps(settings),
|
|
1314
|
+
routing_tunables=routing_tunables or effective_routing_tunables(settings),
|
|
1315
|
+
)
|
|
1316
|
+
request_state.account_response_create_release = self._load_balancer.release_account_lease
|
|
1317
|
+
try:
|
|
1318
|
+
await scheduler.wait_for(response_create_gate.acquire(), timeout=timeout_seconds)
|
|
1319
|
+
except TimeoutError as exc:
|
|
1320
|
+
await self._release_request_state_account_response_create_lease(request_state)
|
|
1321
|
+
request_state.response_create_gate = None
|
|
1322
|
+
request_state.response_create_gate_acquired = False
|
|
1323
|
+
request_state.awaiting_response_created = False
|
|
1324
|
+
pending_count = None
|
|
1325
|
+
queued_count = None
|
|
1326
|
+
pending_request_ids: list[str] | None = None
|
|
1327
|
+
pending_request_ages_seconds: list[float] | None = None
|
|
1328
|
+
should_retire_stuck_session = False
|
|
1329
|
+
stale_pending_requests_to_fail: list[_WebSocketRequestState] = []
|
|
1330
|
+
retry_circuit_attempt_selection = None
|
|
1331
|
+
if bridge_session is not None:
|
|
1332
|
+
now = self._clock.monotonic()
|
|
1333
|
+
stale_gate_snapshot = await self._snapshot_http_bridge_stale_gate_state(bridge_session, now=now)
|
|
1334
|
+
pending_states = stale_gate_snapshot.pending_states
|
|
1335
|
+
pending_count = len(pending_states)
|
|
1336
|
+
queued_count = stale_gate_snapshot.queued_count
|
|
1337
|
+
threshold_seconds = stale_gate_snapshot.threshold_seconds
|
|
1338
|
+
stale_pending_requests_to_fail = stale_gate_snapshot.stale_request_states
|
|
1339
|
+
should_retire_stuck_session = stale_gate_snapshot.should_retire
|
|
1340
|
+
retry_circuit_attempt_selection = stale_gate_snapshot.retry_circuit_attempt_selection
|
|
1341
|
+
pending_request_ids = [state.request_log_id or state.request_id for state in pending_states]
|
|
1342
|
+
pending_request_ages_seconds = [max(0.0, now - state.started_at) for state in pending_states]
|
|
1343
|
+
if not should_retire_stuck_session and any(
|
|
1344
|
+
max(0.0, now - state.started_at) >= threshold_seconds for state in pending_states
|
|
1345
|
+
):
|
|
1346
|
+
# A gate waiter starved past the stuck threshold without the
|
|
1347
|
+
# watchdog firing: dump every pending state's verdict inputs
|
|
1348
|
+
# so the blocking condition is identifiable from prod logs.
|
|
1349
|
+
logger.warning(
|
|
1350
|
+
"http_bridge_stuck_watchdog_skipped session_closed=%s candidates=%s states=%s",
|
|
1351
|
+
bridge_session.closed,
|
|
1352
|
+
len(stale_pending_requests_to_fail),
|
|
1353
|
+
"; ".join(
|
|
1354
|
+
(
|
|
1355
|
+
f"id={state.request_log_id or state.request_id}"
|
|
1356
|
+
f" transport={state.transport}"
|
|
1357
|
+
f" skip_log={state.skip_request_log}"
|
|
1358
|
+
f" draining={state.draining_until_terminal}"
|
|
1359
|
+
f" prev_resp={state.previous_response_id is not None}"
|
|
1360
|
+
f" hard_anchor={state.hard_continuity_anchor}"
|
|
1361
|
+
f" gate_acq={state.response_create_gate_acquired}"
|
|
1362
|
+
f" awaiting={state.awaiting_response_created}"
|
|
1363
|
+
f" resp_id={state.response_id is not None}"
|
|
1364
|
+
f" events={state.response_event_count}"
|
|
1365
|
+
f" created_ms={state.latency_response_created_ms}"
|
|
1366
|
+
f" dsvis={state.downstream_visible}"
|
|
1367
|
+
f" age={max(0.0, now - state.started_at):.0f}"
|
|
1368
|
+
f" gate_wait_age={max(0.0, now - state.response_create_gate_wait_started_at) if state.response_create_gate_wait_started_at is not None else None}" # noqa: E501
|
|
1369
|
+
)
|
|
1370
|
+
for state in pending_states
|
|
1371
|
+
),
|
|
1372
|
+
)
|
|
1373
|
+
_log_http_bridge_startup_wait_timeout(
|
|
1374
|
+
stage="response_create_gate",
|
|
1375
|
+
timeout_seconds=timeout_seconds,
|
|
1376
|
+
key=bridge_session.key if bridge_session is not None else None,
|
|
1377
|
+
request_id=request_state.request_id,
|
|
1378
|
+
request_model=request_state.model,
|
|
1379
|
+
pending_count=pending_count,
|
|
1380
|
+
queued_count=queued_count,
|
|
1381
|
+
available=getattr(response_create_gate, "_value", None),
|
|
1382
|
+
pending_request_ids=pending_request_ids,
|
|
1383
|
+
pending_request_ages_seconds=pending_request_ages_seconds,
|
|
1384
|
+
)
|
|
1385
|
+
if bridge_session is not None and stale_pending_requests_to_fail:
|
|
1386
|
+
await self._fail_stale_http_bridge_pending_requests(
|
|
1387
|
+
bridge_session,
|
|
1388
|
+
stale_pending_requests_to_fail,
|
|
1389
|
+
detail="response_create_gate_timeout_stuck_pending",
|
|
1390
|
+
retry_circuit_attempt_selection=retry_circuit_attempt_selection,
|
|
1391
|
+
)
|
|
1392
|
+
elif bridge_session is not None and should_retire_stuck_session:
|
|
1393
|
+
_record_http_bridge_stuck_retire(
|
|
1394
|
+
reason="response_create_gate_timeout_stuck_pending",
|
|
1395
|
+
session=bridge_session,
|
|
1396
|
+
)
|
|
1397
|
+
await self._retire_stale_pending_http_bridge_session(
|
|
1398
|
+
bridge_session,
|
|
1399
|
+
detail="response_create_gate_timeout_stuck_pending",
|
|
1400
|
+
retry_circuit_attempt_selection=retry_circuit_attempt_selection,
|
|
1401
|
+
)
|
|
1402
|
+
raise _http_bridge_startup_wait_timeout_error(
|
|
1403
|
+
"http_bridge_response_create_gate",
|
|
1404
|
+
code="response_create_gate_timeout",
|
|
1405
|
+
) from exc
|
|
1406
|
+
except BaseException:
|
|
1407
|
+
await self._release_request_state_account_response_create_lease(request_state)
|
|
1408
|
+
request_state.response_create_gate = None
|
|
1409
|
+
request_state.response_create_gate_acquired = False
|
|
1410
|
+
request_state.awaiting_response_created = False
|
|
1411
|
+
raise
|
|
1412
|
+
request_state.response_create_gate_acquired = True
|
|
1413
|
+
request_state.awaiting_response_created = True
|
|
1414
|
+
if request_state.response_create_gate_wait_started_at is not None:
|
|
1415
|
+
request_state.latency_response_create_gate_wait_ms = int(
|
|
1416
|
+
max(0.0, self._clock.monotonic() - request_state.response_create_gate_wait_started_at) * 1000
|
|
1417
|
+
)
|
|
1418
|
+
try:
|
|
1419
|
+
request_state.response_create_admission = await self._get_work_admission().acquire_response_create(
|
|
1420
|
+
compact=compact
|
|
1421
|
+
)
|
|
1422
|
+
except BaseException:
|
|
1423
|
+
await self._release_request_state_account_response_create_lease(request_state)
|
|
1424
|
+
await _release_websocket_response_create_gate(request_state, response_create_gate, scheduler=scheduler)
|
|
1425
|
+
raise
|
|
1426
|
+
# The global response-create admission is a queue wait too, and a direct
|
|
1427
|
+
# WebSocket has no bridge-queue measurement: fold it into the gate wait
|
|
1428
|
+
# so the row carries the whole pre-send wait (the TTFT cohort sampler
|
|
1429
|
+
# skips any row with a non-zero wait).
|
|
1430
|
+
if request_state.response_create_gate_wait_started_at is not None:
|
|
1431
|
+
request_state.latency_response_create_gate_wait_ms = int(
|
|
1432
|
+
max(0.0, self._clock.monotonic() - request_state.response_create_gate_wait_started_at) * 1000
|
|
1433
|
+
)
|
|
1434
|
+
|
|
1435
|
+
async def _release_request_state_account_response_create_lease(
|
|
1436
|
+
self,
|
|
1437
|
+
request_state: "_WebSocketRequestState",
|
|
1438
|
+
) -> None:
|
|
1439
|
+
lease = request_state.account_response_create_lease
|
|
1440
|
+
request_state.account_response_create_lease = None
|
|
1441
|
+
request_state.account_response_create_release = None
|
|
1442
|
+
await self._load_balancer.release_account_lease(lease)
|
|
1443
|
+
|
|
1444
|
+
async def _select_account_with_budget_compatible(self, deadline: float, **kwargs: object) -> AccountSelection:
|
|
1445
|
+
affinity_policy = kwargs.pop("affinity_policy", None)
|
|
1446
|
+
if isinstance(affinity_policy, _AffinityPolicy):
|
|
1447
|
+
# Expand once at the compatibility edge so transport callers cannot drift.
|
|
1448
|
+
kwargs.update(affinity_policy.selection_kwargs())
|
|
1449
|
+
required_capability_kwargs = {}
|
|
1450
|
+
if kwargs.get("require_security_work_authorized") is True:
|
|
1451
|
+
required_capability_kwargs["require_security_work_authorized"] = kwargs.pop(
|
|
1452
|
+
"require_security_work_authorized"
|
|
1453
|
+
)
|
|
1454
|
+
return await _call_with_supported_optional_kwargs(
|
|
1455
|
+
self._select_account_with_budget,
|
|
1456
|
+
deadline,
|
|
1457
|
+
optional_kwargs=kwargs,
|
|
1458
|
+
**required_capability_kwargs,
|
|
1459
|
+
)
|
|
1460
|
+
|
|
1461
|
+
@asynccontextmanager
|
|
1462
|
+
async def _accounts_refresh_scope(self) -> AsyncIterator[AccountsRepositoryPort]:
|
|
1463
|
+
# A self-contained repo prevents request cancellation from closing the
|
|
1464
|
+
# session under AuthManager's shielded refresh and stranding a connection.
|
|
1465
|
+
async with self._repo_factory() as repos:
|
|
1466
|
+
yield repos.accounts
|
|
1467
|
+
|
|
1468
|
+
async def _ensure_fresh(
|
|
1469
|
+
self,
|
|
1470
|
+
account: Account,
|
|
1471
|
+
*,
|
|
1472
|
+
force: bool = False,
|
|
1473
|
+
timeout_seconds: float | None = None,
|
|
1474
|
+
redact_sensitive_details: bool = False,
|
|
1475
|
+
) -> Account:
|
|
1476
|
+
token = push_token_refresh_timeout_override(timeout_seconds)
|
|
1477
|
+
try:
|
|
1478
|
+
async with self._repo_factory() as repos:
|
|
1479
|
+
auth_manager = AuthManager(
|
|
1480
|
+
repos.accounts,
|
|
1481
|
+
acquire_refresh_admission=self._get_work_admission().acquire_token_refresh,
|
|
1482
|
+
refresh_repo_factory=self._accounts_refresh_scope,
|
|
1483
|
+
redact_sensitive_details=redact_sensitive_details,
|
|
1484
|
+
)
|
|
1485
|
+
refresh = auth_manager.ensure_fresh(account, force=force)
|
|
1486
|
+
if timeout_seconds is None:
|
|
1487
|
+
return await refresh
|
|
1488
|
+
return await self._scheduler.wait_for(refresh, timeout=max(0.001, timeout_seconds))
|
|
1489
|
+
finally:
|
|
1490
|
+
pop_token_refresh_timeout_override(token)
|
|
1491
|
+
|
|
1492
|
+
async def _ensure_fresh_with_budget(
|
|
1493
|
+
self,
|
|
1494
|
+
account: Account,
|
|
1495
|
+
*,
|
|
1496
|
+
force: bool = False,
|
|
1497
|
+
timeout_seconds: float | None = None,
|
|
1498
|
+
privacy_policy: CodexControlRequestPrivacyPolicy = CodexControlRequestPrivacyPolicy.STANDARD,
|
|
1499
|
+
) -> Account:
|
|
1500
|
+
deadline = None if timeout_seconds is None else self._clock.monotonic() + timeout_seconds
|
|
1501
|
+
return await _recover_fresh_account(
|
|
1502
|
+
self,
|
|
1503
|
+
account,
|
|
1504
|
+
force=force,
|
|
1505
|
+
deadline=deadline,
|
|
1506
|
+
remaining_budget_seconds=self._remaining_budget_seconds,
|
|
1507
|
+
request_id=get_request_id(),
|
|
1508
|
+
privacy_policy=privacy_policy,
|
|
1509
|
+
)
|
|
1510
|
+
|
|
1511
|
+
async def _ensure_previsible_unary_fresh_with_failover(
|
|
1512
|
+
self,
|
|
1513
|
+
account: Account,
|
|
1514
|
+
*,
|
|
1515
|
+
deadline: float,
|
|
1516
|
+
request_id: str,
|
|
1517
|
+
kind: str,
|
|
1518
|
+
select_next_account: Callable[[set[str]], Awaitable[AccountSelection]],
|
|
1519
|
+
strict_account_id: str | None = None,
|
|
1520
|
+
force: bool = False,
|
|
1521
|
+
max_account_attempts: int = 2,
|
|
1522
|
+
privacy_policy: CodexControlRequestPrivacyPolicy = CodexControlRequestPrivacyPolicy.STANDARD,
|
|
1523
|
+
) -> Account:
|
|
1524
|
+
current: Account = account
|
|
1525
|
+
excluded_account_ids: set[str] = set()
|
|
1526
|
+
attempt = 0
|
|
1527
|
+
force_current = force
|
|
1528
|
+
while True:
|
|
1529
|
+
attempt += 1
|
|
1530
|
+
remaining_budget = self._remaining_budget_seconds(deadline)
|
|
1531
|
+
if remaining_budget <= 0:
|
|
1532
|
+
logger.warning(
|
|
1533
|
+
"%s request budget exhausted before freshness check request_id=%s account_id=%s",
|
|
1534
|
+
kind,
|
|
1535
|
+
request_id,
|
|
1536
|
+
"<redacted>" if privacy_policy.redacts_sensitive_details else current.id,
|
|
1537
|
+
)
|
|
1538
|
+
_raise_proxy_budget_exhausted()
|
|
1539
|
+
try:
|
|
1540
|
+
if privacy_policy.redacts_sensitive_details:
|
|
1541
|
+
return await self._ensure_fresh_with_budget(
|
|
1542
|
+
current,
|
|
1543
|
+
force=force_current,
|
|
1544
|
+
timeout_seconds=remaining_budget,
|
|
1545
|
+
privacy_policy=privacy_policy,
|
|
1546
|
+
)
|
|
1547
|
+
return await self._ensure_fresh_with_budget(
|
|
1548
|
+
current,
|
|
1549
|
+
force=force_current,
|
|
1550
|
+
timeout_seconds=remaining_budget,
|
|
1551
|
+
)
|
|
1552
|
+
except RefreshError as exc:
|
|
1553
|
+
if not (is_transient_refresh_contention(exc) or exc.transport_error):
|
|
1554
|
+
# Permanent / non-transport refresh failures keep their prior
|
|
1555
|
+
# escalation: tag the failed account and propagate.
|
|
1556
|
+
setattr(exc, _FAILED_ACCOUNT_ATTR, current)
|
|
1557
|
+
raise
|
|
1558
|
+
current = await failover_after_previsible_refresh_error(
|
|
1559
|
+
self,
|
|
1560
|
+
exc,
|
|
1561
|
+
current,
|
|
1562
|
+
attempt=attempt,
|
|
1563
|
+
max_account_attempts=max_account_attempts,
|
|
1564
|
+
strict_account_id=strict_account_id,
|
|
1565
|
+
excluded_account_ids=excluded_account_ids,
|
|
1566
|
+
select_next_account=select_next_account,
|
|
1567
|
+
request_id=request_id,
|
|
1568
|
+
kind=kind,
|
|
1569
|
+
privacy_policy=privacy_policy,
|
|
1570
|
+
)
|
|
1571
|
+
force_current = False
|
|
1572
|
+
continue
|
|
1573
|
+
except (aiohttp.ClientError, asyncio.TimeoutError) as exc:
|
|
1574
|
+
current = await failover_after_previsible_refresh_error(
|
|
1575
|
+
self,
|
|
1576
|
+
exc,
|
|
1577
|
+
current,
|
|
1578
|
+
attempt=attempt,
|
|
1579
|
+
max_account_attempts=max_account_attempts,
|
|
1580
|
+
strict_account_id=strict_account_id,
|
|
1581
|
+
excluded_account_ids=excluded_account_ids,
|
|
1582
|
+
select_next_account=select_next_account,
|
|
1583
|
+
request_id=request_id,
|
|
1584
|
+
kind=kind,
|
|
1585
|
+
privacy_policy=privacy_policy,
|
|
1586
|
+
)
|
|
1587
|
+
force_current = False
|
|
1588
|
+
|
|
1589
|
+
async def _retry_previsible_unary_call_failover(
|
|
1590
|
+
self,
|
|
1591
|
+
exc: ProxyResponseError,
|
|
1592
|
+
account: Account,
|
|
1593
|
+
*,
|
|
1594
|
+
deadline: float,
|
|
1595
|
+
select_next_account: Callable[[set[str]], Awaitable[AccountSelection]],
|
|
1596
|
+
call_next: Callable[[Account], Awaitable[Any]],
|
|
1597
|
+
strict_account_id: str | None = None,
|
|
1598
|
+
privacy_policy: CodexControlRequestPrivacyPolicy = CodexControlRequestPrivacyPolicy.STANDARD,
|
|
1599
|
+
) -> tuple[Account, Any] | None:
|
|
1600
|
+
async def _handle_proxy_error(account: Account, error: ProxyResponseError) -> None:
|
|
1601
|
+
if privacy_policy.redacts_sensitive_details:
|
|
1602
|
+
await self._handle_proxy_error(
|
|
1603
|
+
account,
|
|
1604
|
+
error,
|
|
1605
|
+
privacy_policy=privacy_policy,
|
|
1606
|
+
)
|
|
1607
|
+
return
|
|
1608
|
+
await self._handle_proxy_error(account, error)
|
|
1609
|
+
|
|
1610
|
+
async def _ensure_fresh(account: Account, *, force: bool = False) -> Account:
|
|
1611
|
+
return await ensure_fresh_with_auth_error(
|
|
1612
|
+
self,
|
|
1613
|
+
account,
|
|
1614
|
+
force=force,
|
|
1615
|
+
timeout_seconds=self._remaining_budget_seconds(deadline),
|
|
1616
|
+
privacy_policy=privacy_policy,
|
|
1617
|
+
)
|
|
1618
|
+
|
|
1619
|
+
if hasattr(exc, _FAILED_ACCOUNT_ATTR):
|
|
1620
|
+
return None
|
|
1621
|
+
if not _should_failover_previsible_unary_proxy_error(exc):
|
|
1622
|
+
return None
|
|
1623
|
+
failed_account = _proxy_response_failed_account(exc, account)
|
|
1624
|
+
if strict_account_id is not None and failed_account.id == strict_account_id:
|
|
1625
|
+
return None
|
|
1626
|
+
selection = await select_next_account({failed_account.id})
|
|
1627
|
+
if selection.account is None:
|
|
1628
|
+
return None
|
|
1629
|
+
await _handle_proxy_error(failed_account, exc)
|
|
1630
|
+
try:
|
|
1631
|
+
next_account = await _ensure_fresh(selection.account)
|
|
1632
|
+
except ProxyResponseError as failover_exc:
|
|
1633
|
+
failover_failed_account = _proxy_response_failed_account(failover_exc, selection.account)
|
|
1634
|
+
setattr(failover_exc, _FAILED_ACCOUNT_ATTR, failover_failed_account)
|
|
1635
|
+
if failover_exc.status_code != 401:
|
|
1636
|
+
await _handle_proxy_error(failover_failed_account, failover_exc)
|
|
1637
|
+
raise
|
|
1638
|
+
try:
|
|
1639
|
+
result = await call_next(next_account)
|
|
1640
|
+
except ProxyResponseError as failover_exc:
|
|
1641
|
+
failover_failed_account = _proxy_response_failed_account(failover_exc, next_account)
|
|
1642
|
+
setattr(failover_exc, _FAILED_ACCOUNT_ATTR, failover_failed_account)
|
|
1643
|
+
if failover_exc.status_code == 401:
|
|
1644
|
+
remaining_budget = self._remaining_budget_seconds(deadline)
|
|
1645
|
+
if remaining_budget <= 0:
|
|
1646
|
+
_raise_proxy_budget_exhausted()
|
|
1647
|
+
try:
|
|
1648
|
+
refreshed_account = await _ensure_fresh(next_account, force=True)
|
|
1649
|
+
except ProxyResponseError as refresh_exc:
|
|
1650
|
+
refresh_failed_account = _proxy_response_failed_account(refresh_exc, next_account)
|
|
1651
|
+
setattr(refresh_exc, _FAILED_ACCOUNT_ATTR, refresh_failed_account)
|
|
1652
|
+
if refresh_exc.status_code != 401:
|
|
1653
|
+
await _handle_proxy_error(refresh_failed_account, refresh_exc)
|
|
1654
|
+
raise
|
|
1655
|
+
try:
|
|
1656
|
+
retry_result = await call_next(refreshed_account)
|
|
1657
|
+
except ProxyResponseError as retry_exc:
|
|
1658
|
+
retry_failed_account = _proxy_response_failed_account(retry_exc, refreshed_account)
|
|
1659
|
+
setattr(retry_exc, _FAILED_ACCOUNT_ATTR, retry_failed_account)
|
|
1660
|
+
await _handle_proxy_error(retry_failed_account, retry_exc)
|
|
1661
|
+
raise
|
|
1662
|
+
await self._load_balancer.record_success(refreshed_account)
|
|
1663
|
+
return refreshed_account, retry_result
|
|
1664
|
+
await _handle_proxy_error(failover_failed_account, failover_exc)
|
|
1665
|
+
raise
|
|
1666
|
+
await self._load_balancer.record_success(next_account)
|
|
1667
|
+
return next_account, result
|
|
1668
|
+
|
|
1669
|
+
async def _ensure_fresh_with_budget_or_auth_error(
|
|
1670
|
+
self,
|
|
1671
|
+
account: Account,
|
|
1672
|
+
*,
|
|
1673
|
+
force: bool = False,
|
|
1674
|
+
timeout_seconds: float | None = None,
|
|
1675
|
+
error_type: str = "invalid_request_error",
|
|
1676
|
+
privacy_policy: CodexControlRequestPrivacyPolicy = CodexControlRequestPrivacyPolicy.STANDARD,
|
|
1677
|
+
) -> Account:
|
|
1678
|
+
try:
|
|
1679
|
+
if privacy_policy.redacts_sensitive_details:
|
|
1680
|
+
return await self._ensure_fresh_with_budget(
|
|
1681
|
+
account,
|
|
1682
|
+
force=force,
|
|
1683
|
+
timeout_seconds=timeout_seconds,
|
|
1684
|
+
privacy_policy=privacy_policy,
|
|
1685
|
+
)
|
|
1686
|
+
return await self._ensure_fresh_with_budget(account, force=force, timeout_seconds=timeout_seconds)
|
|
1687
|
+
except RefreshError as refresh_exc:
|
|
1688
|
+
failed_account = _refresh_error_failed_account(refresh_exc, account)
|
|
1689
|
+
if is_transient_refresh_contention(refresh_exc):
|
|
1690
|
+
# Transient cross-replica refresh contention (benign claim
|
|
1691
|
+
# contention OR a post-exchange persist/status CAS conflict): the
|
|
1692
|
+
# account is healthy. Surface a retryable, unpenalized
|
|
1693
|
+
# ``upstream_unavailable`` instead of a bogus 401 ``invalid_api_key``
|
|
1694
|
+
# — matching the previsible-unary/compact/streaming/WebSocket paths
|
|
1695
|
+
# — so file-upload, transcription, and codex-control post-401 forced
|
|
1696
|
+
# refreshes fail retryably.
|
|
1697
|
+
raise_proxy_unavailable_for_claim_contention(
|
|
1698
|
+
refresh_exc.message or str(refresh_exc) or "Request to upstream timed out",
|
|
1699
|
+
failed_account,
|
|
1700
|
+
)
|
|
1701
|
+
if refresh_exc.transport_error:
|
|
1702
|
+
# Genuine OAuth transport failure (NOT claim contention): keeps its
|
|
1703
|
+
# penalizable retryable ``upstream_unavailable``.
|
|
1704
|
+
_raise_proxy_unavailable_for_account(
|
|
1705
|
+
refresh_exc.message or str(refresh_exc) or "Request to upstream timed out",
|
|
1706
|
+
failed_account,
|
|
1707
|
+
)
|
|
1708
|
+
if refresh_exc.is_permanent:
|
|
1709
|
+
await self._load_balancer.mark_permanent_failure(failed_account, refresh_exc.code)
|
|
1710
|
+
raise ProxyResponseError(
|
|
1711
|
+
401,
|
|
1712
|
+
openai_error(
|
|
1713
|
+
"invalid_api_key",
|
|
1714
|
+
refresh_exc.message,
|
|
1715
|
+
error_type=error_type,
|
|
1716
|
+
),
|
|
1717
|
+
) from refresh_exc
|
|
1718
|
+
except (aiohttp.ClientError, asyncio.TimeoutError) as exc:
|
|
1719
|
+
_raise_proxy_unavailable(str(exc) or "Request to upstream timed out")
|
|
1720
|
+
|
|
1721
|
+
async def _select_account_with_budget(
|
|
1722
|
+
self,
|
|
1723
|
+
deadline: float,
|
|
1724
|
+
*,
|
|
1725
|
+
request_id: str,
|
|
1726
|
+
kind: str,
|
|
1727
|
+
request_stage: str = "first_turn",
|
|
1728
|
+
api_key: ApiKeyData | None = None,
|
|
1729
|
+
sticky_key: str | None = None,
|
|
1730
|
+
sticky_kind: StickySessionKind | None = None,
|
|
1731
|
+
reallocate_sticky: bool = False,
|
|
1732
|
+
sticky_source: _CodexSessionSource | None = None,
|
|
1733
|
+
legacy_sticky_key: str | None = None,
|
|
1734
|
+
legacy_continuity_source: _CodexSessionSource | None = None,
|
|
1735
|
+
sticky_seed_key: str | None = None,
|
|
1736
|
+
sticky_seed_kind: StickySessionKind | None = None,
|
|
1737
|
+
spill_bare_session_on_account_cap: bool = False,
|
|
1738
|
+
abandon_unavailable_legacy_owner: bool = False,
|
|
1739
|
+
require_unambiguous_account: bool = False,
|
|
1740
|
+
sticky_max_age_seconds: int | None = None,
|
|
1741
|
+
prefer_earlier_reset_accounts: bool = False,
|
|
1742
|
+
prefer_earlier_reset_window: ResetPreferenceWindow = "secondary",
|
|
1743
|
+
routing_strategy: RoutingStrategy = "capacity_weighted",
|
|
1744
|
+
model: str | None = None,
|
|
1745
|
+
service_tier: str | None = None,
|
|
1746
|
+
additional_limit_name: str | None = None,
|
|
1747
|
+
exclude_account_ids: Collection[str] | None = None,
|
|
1748
|
+
preferred_account_id: str | None = None,
|
|
1749
|
+
preferred_account_is_continuity_owner: bool = False,
|
|
1750
|
+
preferred_account_overrides_single_account_routing: bool = False,
|
|
1751
|
+
require_security_work_authorized: bool = False,
|
|
1752
|
+
lease_kind: Literal["response_create", "stream"] | None = None,
|
|
1753
|
+
estimated_lease_tokens: float = 0.0,
|
|
1754
|
+
fallback_on_preferred_account_unavailable: bool = True,
|
|
1755
|
+
traffic_class: TrafficClass = TRAFFIC_CLASS_FOREGROUND,
|
|
1756
|
+
redact_sensitive_details: bool = False,
|
|
1757
|
+
) -> AccountSelection:
|
|
1758
|
+
remaining_budget = self._remaining_budget_seconds(deadline)
|
|
1759
|
+
if remaining_budget <= 0:
|
|
1760
|
+
logger.warning(
|
|
1761
|
+
"%s request budget exhausted before account selection request_id=%s", kind.title(), request_id
|
|
1762
|
+
)
|
|
1763
|
+
_raise_proxy_budget_exhausted()
|
|
1764
|
+
scoped_account_ids = (
|
|
1765
|
+
set(api_key.assigned_account_ids)
|
|
1766
|
+
if api_key is not None and api_key.account_assignment_scope_enabled
|
|
1767
|
+
else None
|
|
1768
|
+
)
|
|
1769
|
+
effective_traffic_class = (
|
|
1770
|
+
TRAFFIC_CLASS_OPPORTUNISTIC
|
|
1771
|
+
if api_key is not None and api_key.traffic_class == TRAFFIC_CLASS_OPPORTUNISTIC
|
|
1772
|
+
else traffic_class
|
|
1773
|
+
)
|
|
1774
|
+
excluded_account_ids_set = set(exclude_account_ids or ())
|
|
1775
|
+
|
|
1776
|
+
def log_account_id(account_id: str | None) -> str | None:
|
|
1777
|
+
return "<redacted>" if redact_sensitive_details and account_id is not None else account_id
|
|
1778
|
+
|
|
1779
|
+
logger.info(
|
|
1780
|
+
"Proxy account selection start request_id=%s kind=%s request_stage=%s model=%s "
|
|
1781
|
+
"additional_limit=%s sticky=%s sticky_kind=%s reallocate_sticky=%s prefer_earlier_reset=%s "
|
|
1782
|
+
"routing_strategy=%s api_key_present=%s api_key_scope_enabled=%s scoped_count=%s "
|
|
1783
|
+
"excluded_count=%s preferred_account_id=%s remaining_budget=%.2f",
|
|
1784
|
+
request_id,
|
|
1785
|
+
kind,
|
|
1786
|
+
request_stage,
|
|
1787
|
+
model,
|
|
1788
|
+
additional_limit_name,
|
|
1789
|
+
bool(sticky_key),
|
|
1790
|
+
sticky_kind.value if sticky_kind is not None else None,
|
|
1791
|
+
reallocate_sticky,
|
|
1792
|
+
prefer_earlier_reset_accounts,
|
|
1793
|
+
routing_strategy,
|
|
1794
|
+
api_key is not None,
|
|
1795
|
+
bool(api_key is not None and api_key.account_assignment_scope_enabled),
|
|
1796
|
+
None if scoped_account_ids is None else len(scoped_account_ids),
|
|
1797
|
+
len(excluded_account_ids_set),
|
|
1798
|
+
log_account_id(preferred_account_id),
|
|
1799
|
+
remaining_budget,
|
|
1800
|
+
)
|
|
1801
|
+
try:
|
|
1802
|
+
with self._scheduler.fail_after(remaining_budget):
|
|
1803
|
+
settings = await get_settings_cache().get()
|
|
1804
|
+
concurrency_caps = effective_account_concurrency_caps(settings)
|
|
1805
|
+
routing_tunables = effective_routing_tunables(settings) # C2-2 routing/overload
|
|
1806
|
+
stream_reserve_slots = (
|
|
1807
|
+
(
|
|
1808
|
+
get_settings().proxy_account_stream_recovery_reserve
|
|
1809
|
+
if getattr(settings, "proxy_account_stream_recovery_reserve", None) is None
|
|
1810
|
+
else settings.proxy_account_stream_recovery_reserve
|
|
1811
|
+
)
|
|
1812
|
+
if lease_kind == "stream" and request_stage != "reattach"
|
|
1813
|
+
else 0
|
|
1814
|
+
)
|
|
1815
|
+
api_key_fair_share_threshold_pct = _selection_api_key_fair_share_threshold_pct(
|
|
1816
|
+
settings, lease_kind=lease_kind, request_stage=request_stage
|
|
1817
|
+
)
|
|
1818
|
+
api_key_id = api_key.id if api_key is not None else None
|
|
1819
|
+
required_preferred_account = (
|
|
1820
|
+
preferred_account_id is not None and not fallback_on_preferred_account_unavailable
|
|
1821
|
+
)
|
|
1822
|
+
required_continuity = required_preferred_account and preferred_account_is_continuity_owner
|
|
1823
|
+
single_account_routing_id: str | None = None
|
|
1824
|
+
if _routing_strategy(settings) == "single_account" and (
|
|
1825
|
+
not required_preferred_account
|
|
1826
|
+
or (required_continuity and not preferred_account_overrides_single_account_routing)
|
|
1827
|
+
):
|
|
1828
|
+
selected_account_id = (settings.single_account_id or "").strip()
|
|
1829
|
+
if not selected_account_id:
|
|
1830
|
+
return AccountSelection(
|
|
1831
|
+
account=None,
|
|
1832
|
+
error_message="Single account routing is enabled but no account is selected",
|
|
1833
|
+
error_code="single_account_not_configured",
|
|
1834
|
+
)
|
|
1835
|
+
if selected_account_id in excluded_account_ids_set:
|
|
1836
|
+
return AccountSelection(
|
|
1837
|
+
account=None,
|
|
1838
|
+
error_message="Selected single account is unavailable",
|
|
1839
|
+
error_code="single_account_unavailable",
|
|
1840
|
+
)
|
|
1841
|
+
if scoped_account_ids is not None and selected_account_id not in scoped_account_ids:
|
|
1842
|
+
return AccountSelection(
|
|
1843
|
+
account=None,
|
|
1844
|
+
error_message="Selected single account is outside the API key account scope",
|
|
1845
|
+
error_code="single_account_scope_mismatch",
|
|
1846
|
+
)
|
|
1847
|
+
single_account_routing_id = selected_account_id
|
|
1848
|
+
routing_strategy = "single_account"
|
|
1849
|
+
preferred_eligible = (
|
|
1850
|
+
preferred_account_id is not None
|
|
1851
|
+
and preferred_account_id not in excluded_account_ids_set
|
|
1852
|
+
and (
|
|
1853
|
+
scoped_account_ids is None
|
|
1854
|
+
or preferred_account_id in scoped_account_ids
|
|
1855
|
+
or (required_continuity and not preferred_account_overrides_single_account_routing)
|
|
1856
|
+
)
|
|
1857
|
+
and (
|
|
1858
|
+
single_account_routing_id is None
|
|
1859
|
+
or preferred_account_id == single_account_routing_id
|
|
1860
|
+
or required_continuity
|
|
1861
|
+
)
|
|
1862
|
+
)
|
|
1863
|
+
if preferred_account_id is not None and not preferred_eligible:
|
|
1864
|
+
logger.warning(
|
|
1865
|
+
"Proxy preferred account skipped request_id=%s kind=%s request_stage=%s "
|
|
1866
|
+
"preferred_account_id=%s excluded=%s outside_api_key_scope=%s",
|
|
1867
|
+
request_id,
|
|
1868
|
+
kind,
|
|
1869
|
+
request_stage,
|
|
1870
|
+
log_account_id(preferred_account_id),
|
|
1871
|
+
preferred_account_id in excluded_account_ids_set,
|
|
1872
|
+
scoped_account_ids is not None and preferred_account_id not in scoped_account_ids,
|
|
1873
|
+
)
|
|
1874
|
+
if not fallback_on_preferred_account_unavailable:
|
|
1875
|
+
return AccountSelection(
|
|
1876
|
+
account=None,
|
|
1877
|
+
error_message="Preferred account is not available",
|
|
1878
|
+
error_code="preferred_account_unavailable",
|
|
1879
|
+
)
|
|
1880
|
+
if preferred_eligible:
|
|
1881
|
+
preferred_sticky_inputs = _AffinityPolicy.preferred_owner_sticky_inputs(
|
|
1882
|
+
sticky_key,
|
|
1883
|
+
sticky_kind,
|
|
1884
|
+
reallocate_sticky,
|
|
1885
|
+
sticky_max_age_seconds,
|
|
1886
|
+
sticky_source,
|
|
1887
|
+
legacy_sticky_key,
|
|
1888
|
+
)
|
|
1889
|
+
preferred_selection = await self._load_balancer.select_account(
|
|
1890
|
+
dashboard_settings=settings, # C2-3 resilience toggles
|
|
1891
|
+
sticky_key=preferred_sticky_inputs[0],
|
|
1892
|
+
sticky_kind=preferred_sticky_inputs[1],
|
|
1893
|
+
reallocate_sticky=preferred_sticky_inputs[2],
|
|
1894
|
+
sticky_max_age_seconds=preferred_sticky_inputs[3],
|
|
1895
|
+
sticky_source=preferred_sticky_inputs[4],
|
|
1896
|
+
legacy_sticky_key=preferred_sticky_inputs[5],
|
|
1897
|
+
legacy_continuity_source=legacy_continuity_source,
|
|
1898
|
+
# Exact ownership chooses the account; a first-ever thread
|
|
1899
|
+
# still seeds atomically without overwriting a process default.
|
|
1900
|
+
sticky_seed_key=sticky_seed_key,
|
|
1901
|
+
sticky_seed_kind=sticky_seed_kind,
|
|
1902
|
+
abandon_unavailable_legacy_owner=abandon_unavailable_legacy_owner,
|
|
1903
|
+
prefer_earlier_reset_accounts=prefer_earlier_reset_accounts,
|
|
1904
|
+
prefer_earlier_reset_window=prefer_earlier_reset_window,
|
|
1905
|
+
routing_strategy=routing_strategy,
|
|
1906
|
+
relative_availability_power=_relative_availability_power(settings),
|
|
1907
|
+
relative_availability_top_k=_relative_availability_top_k(settings),
|
|
1908
|
+
routing_tunables=routing_tunables,
|
|
1909
|
+
model=model,
|
|
1910
|
+
service_tier=service_tier,
|
|
1911
|
+
additional_limit_name=additional_limit_name,
|
|
1912
|
+
account_ids=(
|
|
1913
|
+
{single_account_routing_id}
|
|
1914
|
+
if required_continuity and single_account_routing_id is not None
|
|
1915
|
+
else scoped_account_ids
|
|
1916
|
+
),
|
|
1917
|
+
required_account_id=preferred_account_id,
|
|
1918
|
+
required_account_is_ownership_constraint=required_preferred_account,
|
|
1919
|
+
required_continuity_owner=required_continuity,
|
|
1920
|
+
require_unambiguous_account=require_unambiguous_account,
|
|
1921
|
+
require_security_work_authorized=require_security_work_authorized,
|
|
1922
|
+
budget_threshold_pct=_sticky_reallocation_primary_budget_threshold_pct(settings),
|
|
1923
|
+
secondary_budget_threshold_pct=_sticky_reallocation_secondary_budget_threshold_pct(settings),
|
|
1924
|
+
lease_kind=lease_kind,
|
|
1925
|
+
estimated_lease_tokens=estimated_lease_tokens,
|
|
1926
|
+
stream_reserve_slots=stream_reserve_slots,
|
|
1927
|
+
traffic_class=effective_traffic_class,
|
|
1928
|
+
concurrency_caps=concurrency_caps,
|
|
1929
|
+
redact_sensitive_details=redact_sensitive_details,
|
|
1930
|
+
allow_usage_exhaustion_error=not required_preferred_account,
|
|
1931
|
+
api_key_id=api_key_id,
|
|
1932
|
+
api_key_stream_fair_share_threshold_pct=api_key_fair_share_threshold_pct,
|
|
1933
|
+
)
|
|
1934
|
+
if preferred_selection.account is not None:
|
|
1935
|
+
logger.info(
|
|
1936
|
+
"Selected preferred account request_id=%s kind=%s request_stage=%s account_id=%s",
|
|
1937
|
+
request_id,
|
|
1938
|
+
kind,
|
|
1939
|
+
request_stage,
|
|
1940
|
+
log_account_id(preferred_account_id),
|
|
1941
|
+
)
|
|
1942
|
+
return preferred_selection
|
|
1943
|
+
if not fallback_on_preferred_account_unavailable:
|
|
1944
|
+
logger.warning(
|
|
1945
|
+
"Proxy preferred account unavailable request_id=%s kind=%s request_stage=%s "
|
|
1946
|
+
"preferred_account_id=%s error_code=%s error=%s",
|
|
1947
|
+
request_id,
|
|
1948
|
+
kind,
|
|
1949
|
+
request_stage,
|
|
1950
|
+
log_account_id(preferred_account_id),
|
|
1951
|
+
preferred_selection.error_code,
|
|
1952
|
+
preferred_selection.error_message,
|
|
1953
|
+
)
|
|
1954
|
+
return preferred_selection
|
|
1955
|
+
selection = await self._load_balancer.select_account(
|
|
1956
|
+
dashboard_settings=settings, # C2-3 resilience toggles
|
|
1957
|
+
sticky_key=sticky_key,
|
|
1958
|
+
sticky_kind=sticky_kind,
|
|
1959
|
+
reallocate_sticky=reallocate_sticky,
|
|
1960
|
+
sticky_source=sticky_source,
|
|
1961
|
+
legacy_sticky_key=legacy_sticky_key,
|
|
1962
|
+
legacy_continuity_source=legacy_continuity_source,
|
|
1963
|
+
sticky_seed_key=sticky_seed_key,
|
|
1964
|
+
sticky_seed_kind=sticky_seed_kind,
|
|
1965
|
+
spill_bare_session_on_account_cap=_AffinityPolicy.cap_spillover_allowed(
|
|
1966
|
+
spill_bare_session_on_account_cap,
|
|
1967
|
+
preferred_account_id,
|
|
1968
|
+
request_stage,
|
|
1969
|
+
),
|
|
1970
|
+
abandon_unavailable_legacy_owner=abandon_unavailable_legacy_owner,
|
|
1971
|
+
require_unambiguous_account=require_unambiguous_account,
|
|
1972
|
+
sticky_max_age_seconds=sticky_max_age_seconds,
|
|
1973
|
+
prefer_earlier_reset_accounts=prefer_earlier_reset_accounts,
|
|
1974
|
+
prefer_earlier_reset_window=prefer_earlier_reset_window,
|
|
1975
|
+
routing_strategy=routing_strategy,
|
|
1976
|
+
relative_availability_power=_relative_availability_power(settings),
|
|
1977
|
+
relative_availability_top_k=_relative_availability_top_k(settings),
|
|
1978
|
+
model=model,
|
|
1979
|
+
service_tier=service_tier,
|
|
1980
|
+
additional_limit_name=additional_limit_name,
|
|
1981
|
+
account_ids=scoped_account_ids,
|
|
1982
|
+
required_account_id=single_account_routing_id,
|
|
1983
|
+
exclude_account_ids=excluded_account_ids_set,
|
|
1984
|
+
require_security_work_authorized=require_security_work_authorized,
|
|
1985
|
+
budget_threshold_pct=_sticky_reallocation_primary_budget_threshold_pct(settings),
|
|
1986
|
+
secondary_budget_threshold_pct=_sticky_reallocation_secondary_budget_threshold_pct(settings),
|
|
1987
|
+
lease_kind=lease_kind,
|
|
1988
|
+
estimated_lease_tokens=estimated_lease_tokens,
|
|
1989
|
+
stream_reserve_slots=stream_reserve_slots,
|
|
1990
|
+
traffic_class=effective_traffic_class,
|
|
1991
|
+
concurrency_caps=concurrency_caps,
|
|
1992
|
+
redact_sensitive_details=redact_sensitive_details,
|
|
1993
|
+
api_key_id=api_key_id,
|
|
1994
|
+
api_key_stream_fair_share_threshold_pct=api_key_fair_share_threshold_pct,
|
|
1995
|
+
routing_tunables=routing_tunables,
|
|
1996
|
+
)
|
|
1997
|
+
if selection.account is not None and selection.account.id in excluded_account_ids_set:
|
|
1998
|
+
logger.warning(
|
|
1999
|
+
"Proxy account selection returned excluded account request_id=%s kind=%s request_stage=%s "
|
|
2000
|
+
"account_id=%s excluded_count=%s",
|
|
2001
|
+
request_id,
|
|
2002
|
+
kind,
|
|
2003
|
+
request_stage,
|
|
2004
|
+
log_account_id(selection.account.id),
|
|
2005
|
+
len(excluded_account_ids_set),
|
|
2006
|
+
)
|
|
2007
|
+
return AccountSelection(
|
|
2008
|
+
account=None,
|
|
2009
|
+
error_message="No active accounts available",
|
|
2010
|
+
error_code="no_accounts",
|
|
2011
|
+
)
|
|
2012
|
+
logger.info(
|
|
2013
|
+
"Proxy account selection result request_id=%s kind=%s request_stage=%s model=%s "
|
|
2014
|
+
"selected_account_id=%s error_code=%s error=%s scoped_count=%s excluded_count=%s",
|
|
2015
|
+
request_id,
|
|
2016
|
+
kind,
|
|
2017
|
+
request_stage,
|
|
2018
|
+
model,
|
|
2019
|
+
log_account_id(selection.account.id) if selection.account is not None else None,
|
|
2020
|
+
selection.error_code,
|
|
2021
|
+
selection.error_message,
|
|
2022
|
+
None if scoped_account_ids is None else len(scoped_account_ids),
|
|
2023
|
+
len(excluded_account_ids_set),
|
|
2024
|
+
)
|
|
2025
|
+
return selection
|
|
2026
|
+
except TimeoutError:
|
|
2027
|
+
logger.warning("%s account selection exceeded request budget request_id=%s", kind.title(), request_id)
|
|
2028
|
+
_raise_proxy_budget_exhausted()
|
|
2029
|
+
|
|
2030
|
+
async def _acquire_account_response_create_lease_or_overload(
|
|
2031
|
+
self,
|
|
2032
|
+
*,
|
|
2033
|
+
account_id: str,
|
|
2034
|
+
request_id: str,
|
|
2035
|
+
surface: str,
|
|
2036
|
+
concurrency_caps: AccountConcurrencyCaps,
|
|
2037
|
+
routing_tunables: RoutingTunables | None = None,
|
|
2038
|
+
) -> AccountLease:
|
|
2039
|
+
lease = await self._load_balancer.acquire_account_lease(
|
|
2040
|
+
account_id,
|
|
2041
|
+
kind="response_create",
|
|
2042
|
+
concurrency_caps=concurrency_caps,
|
|
2043
|
+
routing_tunables=routing_tunables,
|
|
2044
|
+
)
|
|
2045
|
+
if lease is not None:
|
|
2046
|
+
return lease
|
|
2047
|
+
inflight_create, inflight_stream, leased_tokens = await self._load_balancer.account_pressure_snapshot(
|
|
2048
|
+
account_id
|
|
2049
|
+
)
|
|
2050
|
+
logger.warning(
|
|
2051
|
+
"Responses account response-create cap reached request_id=%s surface=%s account_id=%s "
|
|
2052
|
+
"inflight_create=%s inflight_stream=%s leased_tokens=%.3f",
|
|
2053
|
+
request_id,
|
|
2054
|
+
surface,
|
|
2055
|
+
account_id,
|
|
2056
|
+
inflight_create,
|
|
2057
|
+
inflight_stream,
|
|
2058
|
+
leased_tokens,
|
|
2059
|
+
)
|
|
2060
|
+
raise ProxyResponseError(
|
|
2061
|
+
429,
|
|
2062
|
+
openai_error(
|
|
2063
|
+
"account_response_create_cap",
|
|
2064
|
+
"Account response-create capacity is exhausted",
|
|
2065
|
+
error_type="rate_limit_error",
|
|
2066
|
+
),
|
|
2067
|
+
)
|
|
2068
|
+
|
|
2069
|
+
async def check_opportunistic_admission(
|
|
2070
|
+
self,
|
|
2071
|
+
*,
|
|
2072
|
+
api_key: ApiKeyData | None,
|
|
2073
|
+
model: str | None,
|
|
2074
|
+
lease_kind: AccountLeaseKind | None = None,
|
|
2075
|
+
service_tier: str | None = None,
|
|
2076
|
+
observe_only: bool = False,
|
|
2077
|
+
) -> AccountSelection:
|
|
2078
|
+
settings = await get_settings_cache().get()
|
|
2079
|
+
return await self._load_balancer.check_opportunistic_admission(
|
|
2080
|
+
dashboard_settings=settings, # C2-3 resilience toggles
|
|
2081
|
+
model=model,
|
|
2082
|
+
service_tier=service_tier,
|
|
2083
|
+
observe_only=observe_only,
|
|
2084
|
+
account_ids=opportunistic_admission_account_scope(settings, api_key),
|
|
2085
|
+
prefer_earlier_reset_accounts=settings.prefer_earlier_reset_accounts,
|
|
2086
|
+
prefer_earlier_reset_window=_prefer_earlier_reset_window(settings),
|
|
2087
|
+
routing_strategy=_routing_strategy(settings),
|
|
2088
|
+
budget_threshold_pct=_sticky_reallocation_primary_budget_threshold_pct(settings),
|
|
2089
|
+
secondary_budget_threshold_pct=_sticky_reallocation_secondary_budget_threshold_pct(settings),
|
|
2090
|
+
lease_kind=lease_kind,
|
|
2091
|
+
concurrency_caps=effective_account_concurrency_caps(settings),
|
|
2092
|
+
routing_tunables=effective_routing_tunables(settings),
|
|
2093
|
+
stream_reserve_slots=(
|
|
2094
|
+
(
|
|
2095
|
+
get_settings().proxy_account_stream_recovery_reserve
|
|
2096
|
+
if getattr(settings, "proxy_account_stream_recovery_reserve", None) is None
|
|
2097
|
+
else settings.proxy_account_stream_recovery_reserve
|
|
2098
|
+
)
|
|
2099
|
+
if lease_kind == "stream"
|
|
2100
|
+
else 0
|
|
2101
|
+
),
|
|
2102
|
+
)
|
|
2103
|
+
|
|
2104
|
+
async def _handle_proxy_error(
|
|
2105
|
+
self,
|
|
2106
|
+
account: Account,
|
|
2107
|
+
exc: ProxyResponseError,
|
|
2108
|
+
*,
|
|
2109
|
+
privacy_policy: CodexControlRequestPrivacyPolicy = CodexControlRequestPrivacyPolicy.STANDARD,
|
|
2110
|
+
) -> None:
|
|
2111
|
+
if is_claim_contention_unpenalized(exc):
|
|
2112
|
+
# Transient refresh-claim contention: the account is healthy (a peer
|
|
2113
|
+
# replica merely held its refresh claim), so it must not be penalized.
|
|
2114
|
+
return
|
|
2115
|
+
error = _parse_openai_error(exc.payload)
|
|
2116
|
+
code = _normalize_error_code(
|
|
2117
|
+
error.code if error else None,
|
|
2118
|
+
error.type if error else None,
|
|
2119
|
+
)
|
|
2120
|
+
if _is_account_neutral_error_code(code):
|
|
2121
|
+
return
|
|
2122
|
+
if privacy_policy.redacts_sensitive_details:
|
|
2123
|
+
await self._handle_stream_error(
|
|
2124
|
+
account,
|
|
2125
|
+
_upstream_error_from_openai(error),
|
|
2126
|
+
code,
|
|
2127
|
+
http_status=exc.status_code,
|
|
2128
|
+
privacy_policy=privacy_policy,
|
|
2129
|
+
retry_after_seconds=exc.retry_after_seconds,
|
|
2130
|
+
)
|
|
2131
|
+
return
|
|
2132
|
+
await self._handle_stream_error(
|
|
2133
|
+
account,
|
|
2134
|
+
_upstream_error_from_openai(error),
|
|
2135
|
+
code,
|
|
2136
|
+
http_status=exc.status_code,
|
|
2137
|
+
retry_after_seconds=exc.retry_after_seconds,
|
|
2138
|
+
)
|
|
2139
|
+
|
|
2140
|
+
|
|
2141
|
+
def _previous_response_id_from_not_found_message(message: str | None) -> str | None:
|
|
2142
|
+
return previous_response_id_from_not_found_message(message)
|
|
2143
|
+
|
|
2144
|
+
|
|
2145
|
+
def _message_mentions_previous_response_id(message: str | None, previous_response_id: str | None) -> bool:
|
|
2146
|
+
if message is None or previous_response_id is None:
|
|
2147
|
+
return False
|
|
2148
|
+
normalized_message = " ".join(message.split())
|
|
2149
|
+
normalized_previous_response_id = previous_response_id.strip()
|
|
2150
|
+
if not normalized_previous_response_id:
|
|
2151
|
+
return False
|
|
2152
|
+
identifier_pattern = re.escape(normalized_previous_response_id)
|
|
2153
|
+
return (
|
|
2154
|
+
re.search(
|
|
2155
|
+
rf"(?<![A-Za-z0-9_-]){identifier_pattern}(?![A-Za-z0-9_-])",
|
|
2156
|
+
normalized_message,
|
|
2157
|
+
)
|
|
2158
|
+
is not None
|
|
2159
|
+
)
|
|
2160
|
+
|
|
2161
|
+
|
|
2162
|
+
_MISSING_TOOL_OUTPUT_MESSAGE_PREFIXES = (
|
|
2163
|
+
"no tool output found for function call call_",
|
|
2164
|
+
"no tool output found for custom tool call call_",
|
|
2165
|
+
"no tool output found for apply patch call call_",
|
|
2166
|
+
"no tool output found for tool search call call_",
|
|
2167
|
+
)
|
|
2168
|
+
|
|
2169
|
+
|
|
2170
|
+
def _is_missing_tool_output_message(message: str | None) -> bool:
|
|
2171
|
+
if message is None:
|
|
2172
|
+
return False
|
|
2173
|
+
normalized = " ".join(message.lower().split())
|
|
2174
|
+
return normalized.startswith(_MISSING_TOOL_OUTPUT_MESSAGE_PREFIXES)
|
|
2175
|
+
|
|
2176
|
+
|
|
2177
|
+
def _is_missing_tool_output_error(
|
|
2178
|
+
*,
|
|
2179
|
+
code: str | None,
|
|
2180
|
+
param: OpenAIErrorParam | JsonValue,
|
|
2181
|
+
message: str | None,
|
|
2182
|
+
) -> bool:
|
|
2183
|
+
param_state = coerce_error_param(param)
|
|
2184
|
+
if code != "invalid_request_error" or param_state.normalized != "input" or param_state.malformed:
|
|
2185
|
+
return False
|
|
2186
|
+
return _is_missing_tool_output_message(message)
|
|
2187
|
+
|
|
2188
|
+
|
|
2189
|
+
def _is_previous_response_not_found_error(
|
|
2190
|
+
*,
|
|
2191
|
+
code: str | None,
|
|
2192
|
+
param: OpenAIErrorParam | JsonValue,
|
|
2193
|
+
message: str | None,
|
|
2194
|
+
) -> bool:
|
|
2195
|
+
return is_previous_response_not_found_error(code=code, param=param, message=message)
|
|
2196
|
+
|
|
2197
|
+
|
|
2198
|
+
def _compact_previous_response_not_found_error(exc: ProxyResponseError) -> ProxyResponseError | None:
|
|
2199
|
+
error = _parse_openai_error(exc.payload)
|
|
2200
|
+
if error is None:
|
|
2201
|
+
return None
|
|
2202
|
+
code = _normalize_error_code(error.code, error.type)
|
|
2203
|
+
if not _is_previous_response_not_found_error(
|
|
2204
|
+
code=code,
|
|
2205
|
+
param=error.param_state,
|
|
2206
|
+
message=error.message,
|
|
2207
|
+
):
|
|
2208
|
+
return None
|
|
2209
|
+
return ProxyResponseError(
|
|
2210
|
+
502,
|
|
2211
|
+
previous_response_stream_incomplete_error(),
|
|
2212
|
+
failure_phase=exc.failure_phase,
|
|
2213
|
+
retryable_same_contract=False,
|
|
2214
|
+
failure_detail="previous_response_not_found",
|
|
2215
|
+
failure_exception_type=exc.failure_exception_type,
|
|
2216
|
+
upstream_status_code=exc.upstream_status_code if exc.upstream_status_code is not None else exc.status_code,
|
|
2217
|
+
upstream_error_code=code,
|
|
2218
|
+
)
|
|
2219
|
+
|
|
2220
|
+
|
|
2221
|
+
def _proxy_response_error_code(exc: ProxyResponseError) -> str | None:
|
|
2222
|
+
error = _parse_openai_error(exc.payload)
|
|
2223
|
+
if error is None:
|
|
2224
|
+
return None
|
|
2225
|
+
return _normalize_error_code(error.code, error.type)
|
|
2226
|
+
|
|
2227
|
+
|
|
2228
|
+
_LOCAL_PROXY_ERROR_CODES = frozenset(
|
|
2229
|
+
{
|
|
2230
|
+
"bridge_owner_forward_failed",
|
|
2231
|
+
"bridge_instance_mismatch",
|
|
2232
|
+
"bridge_owner_unreachable",
|
|
2233
|
+
"preferred_account_unavailable",
|
|
2234
|
+
"previous_response_owner_unavailable",
|
|
2235
|
+
"insufficient_image_quota",
|
|
2236
|
+
"ip_forbidden",
|
|
2237
|
+
"no_accounts",
|
|
2238
|
+
"no_plan_support_for_model",
|
|
2239
|
+
"additional_quota_data_unavailable",
|
|
2240
|
+
"no_additional_quota_eligible_accounts",
|
|
2241
|
+
"payload_too_large",
|
|
2242
|
+
"proxy_overloaded",
|
|
2243
|
+
"upstream_request_timeout",
|
|
2244
|
+
"upstream_unavailable",
|
|
2245
|
+
}
|
|
2246
|
+
)
|
|
2247
|
+
|
|
2248
|
+
|
|
2249
|
+
def _request_log_failure_metadata(
|
|
2250
|
+
exc: ProxyResponseError,
|
|
2251
|
+
*,
|
|
2252
|
+
bridge_stage: str | None = None,
|
|
2253
|
+
) -> _RequestLogFailureMetadata:
|
|
2254
|
+
upstream_error_code = exc.upstream_error_code or _proxy_response_error_code(exc)
|
|
2255
|
+
resolved_bridge_stage = bridge_stage
|
|
2256
|
+
if resolved_bridge_stage is None and (
|
|
2257
|
+
exc.failure_phase in {"owner_forward", "owner_forward_status"}
|
|
2258
|
+
or upstream_error_code in {"bridge_owner_unreachable", "bridge_owner_forward_failed"}
|
|
2259
|
+
):
|
|
2260
|
+
resolved_bridge_stage = "owner_forward"
|
|
2261
|
+
upstream_status_code = exc.upstream_status_code
|
|
2262
|
+
if upstream_status_code is None and _should_infer_upstream_status_from_proxy_error(exc, upstream_error_code):
|
|
2263
|
+
upstream_status_code = exc.status_code
|
|
2264
|
+
return _RequestLogFailureMetadata(
|
|
2265
|
+
failure_phase=exc.failure_phase,
|
|
2266
|
+
failure_detail=exc.failure_detail,
|
|
2267
|
+
failure_exception_type=exc.failure_exception_type,
|
|
2268
|
+
upstream_status_code=upstream_status_code,
|
|
2269
|
+
upstream_error_code=upstream_error_code,
|
|
2270
|
+
bridge_stage=resolved_bridge_stage,
|
|
2271
|
+
)
|
|
2272
|
+
|
|
2273
|
+
|
|
2274
|
+
def _previous_response_id_from_payload(payload: Mapping[str, JsonValue] | None) -> str | None:
|
|
2275
|
+
if not isinstance(payload, Mapping):
|
|
2276
|
+
return None
|
|
2277
|
+
previous_response_id = payload.get("previous_response_id")
|
|
2278
|
+
if isinstance(previous_response_id, str) and previous_response_id.strip():
|
|
2279
|
+
return previous_response_id.strip()
|
|
2280
|
+
return None
|
|
2281
|
+
|
|
2282
|
+
|
|
2283
|
+
def _partial_output_proxy_error_event_block(
|
|
2284
|
+
exc: ProxyResponseError,
|
|
2285
|
+
*,
|
|
2286
|
+
response_id: str,
|
|
2287
|
+
previous_response_id: str | None,
|
|
2288
|
+
preferred_account_id: str | None,
|
|
2289
|
+
default_code: str,
|
|
2290
|
+
default_message: str,
|
|
2291
|
+
) -> str:
|
|
2292
|
+
error = _parse_openai_error(exc.payload)
|
|
2293
|
+
error_code = _normalize_error_code(
|
|
2294
|
+
error.code if error else None,
|
|
2295
|
+
error.type if error else None,
|
|
2296
|
+
)
|
|
2297
|
+
error_message = error.message if error else None
|
|
2298
|
+
effective_previous_response_id = previous_response_id or _previous_response_id_from_not_found_message(
|
|
2299
|
+
error_message,
|
|
2300
|
+
)
|
|
2301
|
+
rewritten_error = _rewrite_previous_response_stream_error(
|
|
2302
|
+
previous_response_id=effective_previous_response_id,
|
|
2303
|
+
preferred_account_id=preferred_account_id,
|
|
2304
|
+
error_code=error_code,
|
|
2305
|
+
error_type=error.type if error else None,
|
|
2306
|
+
error_message=error_message,
|
|
2307
|
+
error_param=error.param_state if error else None,
|
|
2308
|
+
)
|
|
2309
|
+
if rewritten_error is not None:
|
|
2310
|
+
rewritten_code, rewritten_message, upstream_error_code = rewritten_error
|
|
2311
|
+
if upstream_error_code is None:
|
|
2312
|
+
event = response_failed_event(
|
|
2313
|
+
rewritten_code,
|
|
2314
|
+
rewritten_message,
|
|
2315
|
+
error_type="server_error",
|
|
2316
|
+
response_id=response_id,
|
|
2317
|
+
)
|
|
2318
|
+
return format_sse_event(event)
|
|
2319
|
+
event = response_failed_event(
|
|
2320
|
+
error_code or default_code,
|
|
2321
|
+
error_message or default_message,
|
|
2322
|
+
error_type=(error.type if error and error.type else "server_error"),
|
|
2323
|
+
response_id=response_id,
|
|
2324
|
+
error_param=error.param_state if error else None,
|
|
2325
|
+
)
|
|
2326
|
+
_apply_error_metadata(event["response"]["error"], error)
|
|
2327
|
+
return format_sse_event(event)
|
|
2328
|
+
|
|
2329
|
+
|
|
2330
|
+
def _routing_strategy(settings: DashboardSettings) -> RoutingStrategy:
|
|
2331
|
+
value = getattr(settings, "routing_strategy", None) or "capacity_weighted"
|
|
2332
|
+
if value == "single_account":
|
|
2333
|
+
return "single_account"
|
|
2334
|
+
if value == "sequential_drain":
|
|
2335
|
+
return "sequential_drain"
|
|
2336
|
+
if value == "reset_drain":
|
|
2337
|
+
return "reset_drain"
|
|
2338
|
+
if value == "round_robin":
|
|
2339
|
+
return "round_robin"
|
|
2340
|
+
if value == "usage_weighted":
|
|
2341
|
+
return "usage_weighted"
|
|
2342
|
+
if value == "relative_availability":
|
|
2343
|
+
return "relative_availability"
|
|
2344
|
+
if value == "fill_first":
|
|
2345
|
+
return "fill_first"
|
|
2346
|
+
return "capacity_weighted"
|
|
2347
|
+
|
|
2348
|
+
|
|
2349
|
+
def _relative_availability_power(settings: DashboardSettings) -> float:
|
|
2350
|
+
raw_value = getattr(settings, "relative_availability_power", None)
|
|
2351
|
+
value = float(raw_value) if raw_value is not None else 2.0
|
|
2352
|
+
return value if value > 0.0 else 2.0
|
|
2353
|
+
|
|
2354
|
+
|
|
2355
|
+
def _relative_availability_top_k(settings: DashboardSettings) -> int:
|
|
2356
|
+
raw_value = getattr(settings, "relative_availability_top_k", None)
|
|
2357
|
+
value = int(raw_value) if raw_value is not None else 5
|
|
2358
|
+
return min(max(value, 1), 20)
|
|
2359
|
+
|
|
2360
|
+
|
|
2361
|
+
def _prefer_earlier_reset_window(settings: DashboardSettings) -> ResetPreferenceWindow:
|
|
2362
|
+
return "primary" if getattr(settings, "prefer_earlier_reset_window", None) == "primary" else "secondary"
|
|
2363
|
+
|
|
2364
|
+
|
|
2365
|
+
def _sticky_reallocation_primary_budget_threshold_pct(settings: DashboardSettings) -> float:
|
|
2366
|
+
value = getattr(settings, "sticky_reallocation_primary_budget_threshold_pct", None)
|
|
2367
|
+
if value is None:
|
|
2368
|
+
value = getattr(settings, "sticky_reallocation_budget_threshold_pct", None)
|
|
2369
|
+
return float(value if value is not None else 95.0)
|
|
2370
|
+
|
|
2371
|
+
|
|
2372
|
+
def _sticky_reallocation_secondary_budget_threshold_pct(settings: DashboardSettings) -> float:
|
|
2373
|
+
value = getattr(settings, "sticky_reallocation_secondary_budget_threshold_pct", None)
|
|
2374
|
+
return float(value if value is not None else 100.0)
|
|
2375
|
+
|
|
2376
|
+
|
|
2377
|
+
def _proxy_request_timeout_event(request_id: str) -> ResponseFailedEvent:
|
|
2378
|
+
event = response_failed_event("upstream_request_timeout", "Proxy request budget exhausted", response_id=request_id)
|
|
2379
|
+
return synthetic_transport_failure_event(event)
|
|
2380
|
+
|
|
2381
|
+
|
|
2382
|
+
def _security_work_advisory_event(
|
|
2383
|
+
*,
|
|
2384
|
+
code: str,
|
|
2385
|
+
message: str,
|
|
2386
|
+
request_id: str | None,
|
|
2387
|
+
action: str,
|
|
2388
|
+
account_id: str | None = None,
|
|
2389
|
+
) -> dict[str, JsonValue]:
|
|
2390
|
+
warning: dict[str, JsonValue] = {
|
|
2391
|
+
"code": code,
|
|
2392
|
+
"message": message,
|
|
2393
|
+
"category": "security_work_authorization",
|
|
2394
|
+
"action": action,
|
|
2395
|
+
}
|
|
2396
|
+
if request_id:
|
|
2397
|
+
warning["request_id"] = request_id
|
|
2398
|
+
if account_id:
|
|
2399
|
+
warning["account_id"] = account_id
|
|
2400
|
+
return {
|
|
2401
|
+
"type": "codex_lb.warning",
|
|
2402
|
+
"warning": warning,
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
|
|
2406
|
+
def _is_security_work_authorization_required_error(code: str | None, message: str | None) -> bool:
|
|
2407
|
+
normalized_code = (code or "").strip().lower()
|
|
2408
|
+
if normalized_code == _SECURITY_WORK_AUTHORIZATION_REQUIRED_CODE:
|
|
2409
|
+
return True
|
|
2410
|
+
normalized_message = (message or "").strip().lower()
|
|
2411
|
+
if not normalized_message:
|
|
2412
|
+
return False
|
|
2413
|
+
return all(hint in normalized_message for hint in _SECURITY_WORK_AUTHORIZATION_REQUIRED_HINTS)
|
|
2414
|
+
|
|
2415
|
+
|
|
2416
|
+
def _raise_proxy_budget_exhausted() -> NoReturn:
|
|
2417
|
+
raise ProxyResponseError(
|
|
2418
|
+
502,
|
|
2419
|
+
openai_error("upstream_request_timeout", "Proxy request budget exhausted"),
|
|
2420
|
+
)
|
|
2421
|
+
|
|
2422
|
+
|
|
2423
|
+
def _raise_proxy_unavailable(message: str) -> NoReturn:
|
|
2424
|
+
raise ProxyResponseError(
|
|
2425
|
+
502,
|
|
2426
|
+
openai_error("upstream_unavailable", message),
|
|
2427
|
+
)
|
|
2428
|
+
|
|
2429
|
+
|
|
2430
|
+
_FAILED_ACCOUNT_ATTR = "_codex_lb_failed_account"
|
|
2431
|
+
|
|
2432
|
+
|
|
2433
|
+
def _proxy_response_failed_account(exc: ProxyResponseError, fallback: Account) -> Account:
|
|
2434
|
+
account = getattr(exc, _FAILED_ACCOUNT_ATTR, None)
|
|
2435
|
+
return account if isinstance(account, Account) else fallback
|
|
2436
|
+
|
|
2437
|
+
|
|
2438
|
+
def _refresh_error_failed_account(exc: RefreshError, fallback: Account) -> Account:
|
|
2439
|
+
account = getattr(exc, _FAILED_ACCOUNT_ATTR, None)
|
|
2440
|
+
return account if isinstance(account, Account) else fallback
|
|
2441
|
+
|
|
2442
|
+
|
|
2443
|
+
def _should_failover_previsible_unary_proxy_error(exc: ProxyResponseError) -> bool:
|
|
2444
|
+
if exc.failure_phase != "connect":
|
|
2445
|
+
return False
|
|
2446
|
+
error = _parse_openai_error(exc.payload)
|
|
2447
|
+
error_code = _normalize_error_code(error.code if error else None, error.type if error else None)
|
|
2448
|
+
# Typed transport provenance takes precedence over sanitized message text.
|
|
2449
|
+
return error_code == "upstream_unavailable" and (
|
|
2450
|
+
exc.retryable_same_contract
|
|
2451
|
+
if exc.failure_detail == "transport_error"
|
|
2452
|
+
else _should_retry_transient_stream_error("upstream_unavailable", error.message if error else None)
|
|
2453
|
+
)
|
|
2454
|
+
|
|
2455
|
+
|
|
2456
|
+
def _is_proxy_budget_exhausted_error(exc: ProxyResponseError) -> bool:
|
|
2457
|
+
error = _parse_openai_error(exc.payload)
|
|
2458
|
+
error_code = _normalize_error_code(error.code if error else None, error.type if error else None)
|
|
2459
|
+
error_message = error.message if error else None
|
|
2460
|
+
return error_code in {"upstream_request_timeout", "upstream_unavailable"} and (
|
|
2461
|
+
error_message == "Proxy request budget exhausted"
|
|
2462
|
+
)
|
|
2463
|
+
|
|
2464
|
+
|
|
2465
|
+
def _should_suppress_text_done_event(
|
|
2466
|
+
*,
|
|
2467
|
+
event_type: str | None,
|
|
2468
|
+
payload: dict[str, JsonValue] | None,
|
|
2469
|
+
suppress_text_done_events: bool,
|
|
2470
|
+
saw_text_delta: bool,
|
|
2471
|
+
) -> bool:
|
|
2472
|
+
if not suppress_text_done_events or not saw_text_delta or event_type is None:
|
|
2473
|
+
return False
|
|
2474
|
+
if event_type == "response.output_text.done":
|
|
2475
|
+
return True
|
|
2476
|
+
if event_type == "response.content_part.done":
|
|
2477
|
+
return _is_text_content_part(payload)
|
|
2478
|
+
return False
|
|
2479
|
+
|
|
2480
|
+
|
|
2481
|
+
def _is_text_content_part(payload: dict[str, JsonValue] | None) -> bool:
|
|
2482
|
+
if payload is None:
|
|
2483
|
+
return False
|
|
2484
|
+
part = payload.get("part")
|
|
2485
|
+
if not isinstance(part, dict):
|
|
2486
|
+
return False
|
|
2487
|
+
part_type = part.get("type")
|
|
2488
|
+
return isinstance(part_type, str) and part_type in _TEXT_DONE_CONTENT_PART_TYPES
|
|
2489
|
+
|
|
2490
|
+
|
|
2491
|
+
def _input_prefix_matches_stored_context(
|
|
2492
|
+
input_value: JsonValue,
|
|
2493
|
+
*,
|
|
2494
|
+
stored_count: int,
|
|
2495
|
+
stored_fingerprint: str | None,
|
|
2496
|
+
) -> bool:
|
|
2497
|
+
if stored_count <= 0 or stored_fingerprint is None:
|
|
2498
|
+
return False
|
|
2499
|
+
if not isinstance(input_value, list):
|
|
2500
|
+
return False
|
|
2501
|
+
if len(input_value) <= stored_count:
|
|
2502
|
+
return False
|
|
2503
|
+
return _fingerprint_input_items(cast(list[JsonValue], input_value)[:stored_count]) == stored_fingerprint
|
|
2504
|
+
|
|
2505
|
+
|
|
2506
|
+
def _is_missing_thread_goal_protocol_error(exc: ProxyResponseError) -> bool:
|
|
2507
|
+
if exc.status_code not in {404, 405}:
|
|
2508
|
+
return False
|
|
2509
|
+
error = _parse_openai_error(exc.payload)
|
|
2510
|
+
code = _normalize_error_code(
|
|
2511
|
+
error.code if error else None,
|
|
2512
|
+
error.type if error else None,
|
|
2513
|
+
)
|
|
2514
|
+
message = (error.message if error and error.message else "").strip().lower()
|
|
2515
|
+
if exc.status_code == 404:
|
|
2516
|
+
return code == "not_found" and message == "not found"
|
|
2517
|
+
return code == "method_not_allowed" and message == "method not allowed"
|
|
2518
|
+
|
|
2519
|
+
|
|
2520
|
+
def _headers_with_turn_state(headers: Mapping[str, str], turn_state: str | None) -> dict[str, str]:
|
|
2521
|
+
forwarded = dict(headers)
|
|
2522
|
+
if turn_state:
|
|
2523
|
+
forwarded["x-codex-turn-state"] = turn_state
|
|
2524
|
+
return forwarded
|
|
2525
|
+
|
|
2526
|
+
|
|
2527
|
+
def _record_same_account_takeover(*, preferred_account_id: str | None, selected_account_id: str | None) -> None:
|
|
2528
|
+
if not PROMETHEUS_AVAILABLE or bridge_same_account_takeover_total is None or preferred_account_id is None:
|
|
2529
|
+
return
|
|
2530
|
+
if selected_account_id is None:
|
|
2531
|
+
bridge_same_account_takeover_total.labels(outcome="fail").inc()
|
|
2532
|
+
elif selected_account_id == preferred_account_id:
|
|
2533
|
+
bridge_same_account_takeover_total.labels(outcome="success").inc()
|
|
2534
|
+
else:
|
|
2535
|
+
bridge_same_account_takeover_total.labels(outcome="fallback").inc()
|
|
2536
|
+
|
|
2537
|
+
|
|
2538
|
+
def _previous_response_owner_lookup_failed_error_envelope() -> OpenAIErrorEnvelope:
|
|
2539
|
+
return openai_error(
|
|
2540
|
+
"upstream_unavailable",
|
|
2541
|
+
"Previous response owner lookup failed; retry later.",
|
|
2542
|
+
error_type="server_error",
|
|
2543
|
+
)
|
|
2544
|
+
|
|
2545
|
+
|
|
2546
|
+
def _header_value_case_insensitive(headers: Mapping[str, str], name: str) -> str | None:
|
|
2547
|
+
target = name.lower()
|
|
2548
|
+
for key, value in headers.items():
|
|
2549
|
+
if key.lower() == target:
|
|
2550
|
+
return value
|
|
2551
|
+
return None
|
|
2552
|
+
|
|
2553
|
+
|
|
2554
|
+
def _headers_with_authorization(headers: Mapping[str, str], authorization: str | None) -> dict[str, str]:
|
|
2555
|
+
merged = dict(headers)
|
|
2556
|
+
if authorization is None:
|
|
2557
|
+
return merged
|
|
2558
|
+
if _header_value_case_insensitive(merged, "authorization") is not None:
|
|
2559
|
+
return merged
|
|
2560
|
+
merged["Authorization"] = authorization
|
|
2561
|
+
return merged
|
|
2562
|
+
|
|
2563
|
+
|
|
2564
|
+
def _service_tier_from_response(
|
|
2565
|
+
response: OpenAIResponsePayload | CompactResponsePayload | None,
|
|
2566
|
+
) -> str | None:
|
|
2567
|
+
if response is None:
|
|
2568
|
+
return None
|
|
2569
|
+
extra = response.model_extra
|
|
2570
|
+
if not isinstance(extra, Mapping):
|
|
2571
|
+
return None
|
|
2572
|
+
return _normalize_service_tier_value(extra.get("service_tier"))
|
|
2573
|
+
|
|
2574
|
+
|
|
2575
|
+
def _service_tier_from_event_payload(payload: dict[str, JsonValue] | None) -> str | None:
|
|
2576
|
+
if not isinstance(payload, dict):
|
|
2577
|
+
return None
|
|
2578
|
+
response = payload.get("response")
|
|
2579
|
+
if not isinstance(response, dict):
|
|
2580
|
+
return None
|
|
2581
|
+
return _normalize_service_tier_value(response.get("service_tier"))
|
|
2582
|
+
|
|
2583
|
+
|
|
2584
|
+
def _effective_service_tier(requested_service_tier: str | None, actual_service_tier: str | None) -> str | None:
|
|
2585
|
+
if isinstance(actual_service_tier, str):
|
|
2586
|
+
return actual_service_tier
|
|
2587
|
+
if isinstance(requested_service_tier, str):
|
|
2588
|
+
return requested_service_tier
|
|
2589
|
+
return None
|
|
2590
|
+
|
|
2591
|
+
|
|
2592
|
+
def _normalize_service_tier_value(value: JsonValue) -> str | None:
|
|
2593
|
+
if not isinstance(value, str):
|
|
2594
|
+
return None
|
|
2595
|
+
stripped = value.strip()
|
|
2596
|
+
if not stripped:
|
|
2597
|
+
return None
|
|
2598
|
+
if stripped.lower() == "fast":
|
|
2599
|
+
return "priority"
|
|
2600
|
+
return stripped
|