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,2360 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
from collections import deque
|
|
8
|
+
from collections.abc import Mapping, Sequence
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Any, cast
|
|
11
|
+
|
|
12
|
+
import anyio
|
|
13
|
+
|
|
14
|
+
from app.core.balancer.types import UpstreamError
|
|
15
|
+
from app.core.clients.files import create_file as core_create_file # noqa: F401
|
|
16
|
+
from app.core.clients.files import finalize_file as core_finalize_file # noqa: F401
|
|
17
|
+
from app.core.clients.http import lease_http_session as lease_http_session # noqa: F401
|
|
18
|
+
from app.core.clients.native_egress import NativeWebSocketRoutingMetadata
|
|
19
|
+
from app.core.clients.proxy import ( # noqa: F401 # noqa: F401
|
|
20
|
+
CODEX_LB_REQUIRED_CAPABILITY_HEADER,
|
|
21
|
+
ImageFetchSession,
|
|
22
|
+
ProxyResponseError,
|
|
23
|
+
UpstreamProxyRouteTrace,
|
|
24
|
+
_as_image_fetch_session,
|
|
25
|
+
_inline_content_images,
|
|
26
|
+
_inline_input_image_urls,
|
|
27
|
+
_ws_transport_payload_budget_bytes,
|
|
28
|
+
filter_inbound_headers,
|
|
29
|
+
pop_compact_timeout_overrides,
|
|
30
|
+
pop_stream_timeout_overrides,
|
|
31
|
+
pop_transcribe_timeout_overrides,
|
|
32
|
+
push_compact_timeout_overrides,
|
|
33
|
+
push_stream_timeout_overrides,
|
|
34
|
+
push_transcribe_timeout_overrides,
|
|
35
|
+
)
|
|
36
|
+
from app.core.clients.proxy import CodexControlResponse as CodexControlResponse
|
|
37
|
+
from app.core.clients.proxy import codex_control_request as core_codex_control_request # noqa: F401
|
|
38
|
+
from app.core.clients.proxy import compact_responses as core_compact_responses # noqa: F401
|
|
39
|
+
from app.core.clients.proxy import transcribe_audio as core_transcribe_audio # noqa: F401
|
|
40
|
+
from app.core.clients.proxy_websocket import (
|
|
41
|
+
UpstreamWebSocketMessage,
|
|
42
|
+
)
|
|
43
|
+
from app.core.clock import Scheduler
|
|
44
|
+
from app.core.errors import (
|
|
45
|
+
PREVIOUS_RESPONSE_MALFORMED_PARAM_REASON,
|
|
46
|
+
PREVIOUS_RESPONSE_NOT_FOUND_CODE,
|
|
47
|
+
PREVIOUS_RESPONSE_NOT_FOUND_MESSAGE,
|
|
48
|
+
PREVIOUS_RESPONSE_STREAM_INCOMPLETE_MESSAGE,
|
|
49
|
+
OpenAIErrorEnvelope,
|
|
50
|
+
OpenAIErrorParam,
|
|
51
|
+
coerce_error_param,
|
|
52
|
+
normalize_public_error_param,
|
|
53
|
+
openai_error,
|
|
54
|
+
previous_response_stream_incomplete_error,
|
|
55
|
+
response_failed_event,
|
|
56
|
+
sanitize_public_error_detail,
|
|
57
|
+
)
|
|
58
|
+
from app.core.exceptions import AppError
|
|
59
|
+
from app.core.openai.models import OpenAIEvent
|
|
60
|
+
from app.core.openai.parsing import parse_sse_event
|
|
61
|
+
from app.core.openai.requests import (
|
|
62
|
+
ResponsesRequest,
|
|
63
|
+
extract_input_file_ids,
|
|
64
|
+
)
|
|
65
|
+
from app.core.types import JsonValue
|
|
66
|
+
from app.core.utils.shared_future import (
|
|
67
|
+
_await_cleanup_deferring_cancellation as _await_cleanup_deferring_cancellation,
|
|
68
|
+
)
|
|
69
|
+
from app.core.utils.sse import CODEX_KEEPALIVE_FRAME as CODEX_KEEPALIVE_FRAME # noqa: F401
|
|
70
|
+
from app.core.utils.sse import format_sse_event, parse_sse_data_json
|
|
71
|
+
from app.core.utils.time import to_utc_naive, utcnow
|
|
72
|
+
from app.db.models import (
|
|
73
|
+
Account,
|
|
74
|
+
AccountStatus, # noqa: F401
|
|
75
|
+
)
|
|
76
|
+
from app.modules.proxy._service.api_key_usage import (
|
|
77
|
+
_API_KEY_RESERVATION_HEARTBEAT_SECONDS as _API_KEY_RESERVATION_HEARTBEAT_SECONDS,
|
|
78
|
+
)
|
|
79
|
+
from app.modules.proxy._service.compact import (
|
|
80
|
+
_service_tier_from_compact_payload as _service_tier_from_compact_payload,
|
|
81
|
+
)
|
|
82
|
+
from app.modules.proxy._service.compact import (
|
|
83
|
+
_sticky_key_for_compact_request as _sticky_key_for_compact_request,
|
|
84
|
+
)
|
|
85
|
+
from app.modules.proxy._service.compact import (
|
|
86
|
+
_sticky_key_from_compact_payload as _sticky_key_from_compact_payload,
|
|
87
|
+
)
|
|
88
|
+
from app.modules.proxy._service.http_bridge.accepted_replay import (
|
|
89
|
+
_websocket_accepted_capacity_retry_error_code,
|
|
90
|
+
)
|
|
91
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
92
|
+
_active_http_bridge_instance_ring as _active_http_bridge_instance_ring,
|
|
93
|
+
)
|
|
94
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
95
|
+
_build_http_bridge_prewarm_text as _build_http_bridge_prewarm_text,
|
|
96
|
+
)
|
|
97
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
98
|
+
_durable_bridge_lookup_active_owner as _durable_bridge_lookup_active_owner,
|
|
99
|
+
)
|
|
100
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
101
|
+
_durable_bridge_lookup_allows_local_reuse as _durable_bridge_lookup_allows_local_reuse,
|
|
102
|
+
)
|
|
103
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
104
|
+
_effective_http_bridge_idle_ttl_seconds as _effective_http_bridge_idle_ttl_seconds,
|
|
105
|
+
)
|
|
106
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
107
|
+
_forwarded_http_bridge_session_key as _forwarded_http_bridge_session_key,
|
|
108
|
+
)
|
|
109
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
110
|
+
_has_http_bridge_response_output_marker as _has_http_bridge_response_output_marker,
|
|
111
|
+
)
|
|
112
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
113
|
+
_http_bridge_allow_durable_takeover as _http_bridge_allow_durable_takeover,
|
|
114
|
+
)
|
|
115
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
116
|
+
_http_bridge_can_local_recover_without_ring as _http_bridge_can_local_recover_without_ring,
|
|
117
|
+
)
|
|
118
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
119
|
+
_http_bridge_can_recover_during_drain as _http_bridge_can_recover_during_drain,
|
|
120
|
+
)
|
|
121
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
122
|
+
_http_bridge_continuity_lost_error_envelope as _http_bridge_continuity_lost_error_envelope,
|
|
123
|
+
)
|
|
124
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
125
|
+
_http_bridge_durable_lease_ttl_seconds as _http_bridge_durable_lease_ttl_seconds,
|
|
126
|
+
)
|
|
127
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
128
|
+
_http_bridge_endpoint_matches_current_instance as _http_bridge_endpoint_matches_current_instance,
|
|
129
|
+
)
|
|
130
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
131
|
+
_http_bridge_eviction_priority as _http_bridge_eviction_priority,
|
|
132
|
+
)
|
|
133
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
134
|
+
_http_bridge_has_durable_recovery_anchor as _http_bridge_has_durable_recovery_anchor,
|
|
135
|
+
)
|
|
136
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
137
|
+
_http_bridge_input_item_type as _http_bridge_input_item_type,
|
|
138
|
+
)
|
|
139
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
140
|
+
_http_bridge_is_context_overflow_error as _http_bridge_is_context_overflow_error,
|
|
141
|
+
)
|
|
142
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
143
|
+
_http_bridge_is_previous_response_owner_unavailable as _http_bridge_is_previous_response_owner_unavailable,
|
|
144
|
+
)
|
|
145
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
146
|
+
_http_bridge_key_strength as _http_bridge_key_strength,
|
|
147
|
+
)
|
|
148
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
149
|
+
_http_bridge_owner_check_required as _http_bridge_owner_check_required,
|
|
150
|
+
)
|
|
151
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
152
|
+
_http_bridge_owner_instance as _http_bridge_owner_instance,
|
|
153
|
+
)
|
|
154
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
155
|
+
_http_bridge_owner_lookup_unavailable_error_envelope as _http_bridge_owner_lookup_unavailable_error_envelope,
|
|
156
|
+
)
|
|
157
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
158
|
+
_http_bridge_payload_looks_like_full_resend as _http_bridge_payload_looks_like_full_resend,
|
|
159
|
+
)
|
|
160
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
161
|
+
_http_bridge_payload_without_previous_response_id as _http_bridge_payload_without_previous_response_id,
|
|
162
|
+
)
|
|
163
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
164
|
+
_http_bridge_precreated_retry_failure_error as _http_bridge_precreated_retry_failure_error,
|
|
165
|
+
)
|
|
166
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
167
|
+
_http_bridge_previous_response_alias_key as _http_bridge_previous_response_alias_key,
|
|
168
|
+
)
|
|
169
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
170
|
+
_http_bridge_previous_response_error_envelope as _http_bridge_previous_response_error_envelope,
|
|
171
|
+
)
|
|
172
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
173
|
+
_http_bridge_request_counts_against_queue as _http_bridge_request_counts_against_queue,
|
|
174
|
+
)
|
|
175
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
176
|
+
_http_bridge_request_stage as _http_bridge_request_stage,
|
|
177
|
+
)
|
|
178
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
179
|
+
_http_bridge_requires_cluster_registration as _http_bridge_requires_cluster_registration,
|
|
180
|
+
)
|
|
181
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
182
|
+
_http_bridge_runtime_config as _http_bridge_runtime_config,
|
|
183
|
+
)
|
|
184
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
185
|
+
_http_bridge_session_allows_api_key as _http_bridge_session_allows_api_key,
|
|
186
|
+
)
|
|
187
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
188
|
+
_http_bridge_session_has_visible_requests as _http_bridge_session_has_visible_requests,
|
|
189
|
+
)
|
|
190
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
191
|
+
_http_bridge_session_matches_preferred_account as _http_bridge_session_matches_preferred_account,
|
|
192
|
+
)
|
|
193
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
194
|
+
_http_bridge_session_retiring_with_visible_requests as _http_bridge_session_retiring_with_visible_requests,
|
|
195
|
+
)
|
|
196
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
197
|
+
_http_bridge_session_reusable_for_request as _http_bridge_session_reusable_for_request,
|
|
198
|
+
)
|
|
199
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
200
|
+
_http_bridge_should_attempt_local_bootstrap_rebind as _http_bridge_should_attempt_local_bootstrap_rebind,
|
|
201
|
+
)
|
|
202
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
203
|
+
_http_bridge_should_attempt_local_previous_response_recovery, # noqa: F401
|
|
204
|
+
)
|
|
205
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
206
|
+
_http_bridge_should_attempt_soft_affinity_reroute as _http_bridge_should_attempt_soft_affinity_reroute,
|
|
207
|
+
)
|
|
208
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
209
|
+
_http_bridge_should_rollover_after_context_overflow as _http_bridge_should_rollover_after_context_overflow,
|
|
210
|
+
)
|
|
211
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
212
|
+
_http_bridge_should_wait_for_registration as _http_bridge_should_wait_for_registration,
|
|
213
|
+
)
|
|
214
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
215
|
+
_http_bridge_startup_wait_timeout_error as _http_bridge_startup_wait_timeout_error,
|
|
216
|
+
)
|
|
217
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
218
|
+
_http_bridge_turn_state_alias_key as _http_bridge_turn_state_alias_key,
|
|
219
|
+
)
|
|
220
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
221
|
+
_is_http_bridge_previous_response_output_item as _is_http_bridge_previous_response_output_item,
|
|
222
|
+
)
|
|
223
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
224
|
+
_is_missing_durable_bridge_table_error as _is_missing_durable_bridge_table_error,
|
|
225
|
+
)
|
|
226
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
227
|
+
_log_http_bridge_event as _log_http_bridge_event,
|
|
228
|
+
)
|
|
229
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
230
|
+
_log_http_bridge_startup_wait_timeout as _log_http_bridge_startup_wait_timeout,
|
|
231
|
+
)
|
|
232
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
233
|
+
_make_http_bridge_session_key as _make_http_bridge_session_key,
|
|
234
|
+
)
|
|
235
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
236
|
+
_normalize_http_bridge_error_event as _normalize_http_bridge_error_event,
|
|
237
|
+
)
|
|
238
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
239
|
+
_normalized_http_bridge_instance_ring as _normalized_http_bridge_instance_ring,
|
|
240
|
+
)
|
|
241
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
242
|
+
_preferred_http_bridge_reconnect_turn_state as _preferred_http_bridge_reconnect_turn_state,
|
|
243
|
+
)
|
|
244
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
245
|
+
_record_bridge_drain_recovery_allowed as _record_bridge_drain_recovery_allowed,
|
|
246
|
+
)
|
|
247
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
248
|
+
_record_bridge_first_turn_timeout as _record_bridge_first_turn_timeout,
|
|
249
|
+
)
|
|
250
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
251
|
+
_record_bridge_reattach as _record_bridge_reattach,
|
|
252
|
+
)
|
|
253
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
254
|
+
_trim_http_bridge_previous_response_input_items as _trim_http_bridge_previous_response_input_items,
|
|
255
|
+
)
|
|
256
|
+
from app.modules.proxy._service.observability import (
|
|
257
|
+
_hash_identifier as _hash_identifier,
|
|
258
|
+
)
|
|
259
|
+
from app.modules.proxy._service.observability import (
|
|
260
|
+
_hash_identifier_or_none as _hash_identifier_or_none,
|
|
261
|
+
)
|
|
262
|
+
from app.modules.proxy._service.observability import (
|
|
263
|
+
_interesting_header_keys as _interesting_header_keys,
|
|
264
|
+
)
|
|
265
|
+
from app.modules.proxy._service.observability import (
|
|
266
|
+
_maybe_log_proxy_request_payload as _maybe_log_proxy_request_payload,
|
|
267
|
+
)
|
|
268
|
+
from app.modules.proxy._service.observability import (
|
|
269
|
+
_maybe_log_proxy_request_shape as _maybe_log_proxy_request_shape,
|
|
270
|
+
)
|
|
271
|
+
from app.modules.proxy._service.observability import (
|
|
272
|
+
_maybe_log_proxy_service_tier_trace as _maybe_log_proxy_service_tier_trace,
|
|
273
|
+
)
|
|
274
|
+
from app.modules.proxy._service.observability import (
|
|
275
|
+
_record_continuity_fail_closed as _record_continuity_fail_closed,
|
|
276
|
+
)
|
|
277
|
+
from app.modules.proxy._service.observability import (
|
|
278
|
+
_record_continuity_owner_resolution as _record_continuity_owner_resolution,
|
|
279
|
+
)
|
|
280
|
+
from app.modules.proxy._service.observability import (
|
|
281
|
+
_summarize_input as _summarize_input,
|
|
282
|
+
)
|
|
283
|
+
from app.modules.proxy._service.observability import (
|
|
284
|
+
_tools_hash as _tools_hash,
|
|
285
|
+
)
|
|
286
|
+
from app.modules.proxy._service.observability import (
|
|
287
|
+
_truncate_identifier as _truncate_identifier,
|
|
288
|
+
)
|
|
289
|
+
from app.modules.proxy._service.support import (
|
|
290
|
+
_ACCOUNT_MODEL_UNSUPPORTED_ERROR_CODE,
|
|
291
|
+
_HARD_HTTP_BRIDGE_AFFINITY_KINDS, # noqa: F401
|
|
292
|
+
_WEBSOCKET_FULL_REPLAY_WAIT_MIN_ITEMS,
|
|
293
|
+
_WEBSOCKET_FULL_REPLAY_WAIT_POLL_SECONDS, # noqa: F401
|
|
294
|
+
_affinity_may_resolve_hard_owner,
|
|
295
|
+
_clear_websocket_request_error_overrides,
|
|
296
|
+
_DeferredKeyedStreamHealthPenalty,
|
|
297
|
+
_event_type_from_payload,
|
|
298
|
+
_websocket_request_can_replay_before_visible_output,
|
|
299
|
+
_WebSocketContinuityAnchor,
|
|
300
|
+
_WebSocketContinuityState,
|
|
301
|
+
_WebSocketReceiveTimeout,
|
|
302
|
+
_WebSocketRequestState,
|
|
303
|
+
_WebSocketUpstreamControl,
|
|
304
|
+
)
|
|
305
|
+
from app.modules.proxy._service.support import (
|
|
306
|
+
_HTTPBridgeOwnerForward as _HTTPBridgeOwnerForward,
|
|
307
|
+
)
|
|
308
|
+
from app.modules.proxy._service.support import (
|
|
309
|
+
_websocket_route_log_kwargs as _websocket_route_log_kwargs,
|
|
310
|
+
)
|
|
311
|
+
from app.modules.proxy._service.warmup import (
|
|
312
|
+
WarmupExecutionData as WarmupExecutionData,
|
|
313
|
+
)
|
|
314
|
+
from app.modules.proxy._service.warmup import (
|
|
315
|
+
WarmupFailedAccountData as WarmupFailedAccountData,
|
|
316
|
+
)
|
|
317
|
+
from app.modules.proxy._service.warmup import (
|
|
318
|
+
WarmupSkippedAccountData as WarmupSkippedAccountData,
|
|
319
|
+
)
|
|
320
|
+
from app.modules.proxy._service.warmup import (
|
|
321
|
+
WarmupSubmittedAccountData as WarmupSubmittedAccountData,
|
|
322
|
+
)
|
|
323
|
+
from app.modules.proxy._service.warmup import (
|
|
324
|
+
_is_warmup_usage_eligible as _is_warmup_usage_eligible,
|
|
325
|
+
)
|
|
326
|
+
from app.modules.proxy._service.warmup import (
|
|
327
|
+
_materialize_warmup_account as _materialize_warmup_account,
|
|
328
|
+
)
|
|
329
|
+
from app.modules.proxy._service.warmup import (
|
|
330
|
+
_snapshot_warmup_account as _snapshot_warmup_account,
|
|
331
|
+
)
|
|
332
|
+
from app.modules.proxy._service.warmup import (
|
|
333
|
+
_WarmupAccountSnapshot as _WarmupAccountSnapshot,
|
|
334
|
+
)
|
|
335
|
+
from app.modules.proxy._service.warmup import (
|
|
336
|
+
_WarmupSubmitResult as _WarmupSubmitResult,
|
|
337
|
+
)
|
|
338
|
+
from app.modules.proxy._service.warmup import (
|
|
339
|
+
_WarmupUsageSnapshot as _WarmupUsageSnapshot,
|
|
340
|
+
)
|
|
341
|
+
from app.modules.proxy.affinity import (
|
|
342
|
+
_AffinityPolicy,
|
|
343
|
+
_sticky_key_from_session_header, # noqa: F401
|
|
344
|
+
)
|
|
345
|
+
from app.modules.proxy.durable_bridge_coordinator import (
|
|
346
|
+
DurableBridgeLookup as DurableBridgeLookup,
|
|
347
|
+
)
|
|
348
|
+
from app.modules.proxy.helpers import (
|
|
349
|
+
_is_account_model_unsupported_error,
|
|
350
|
+
_normalize_error_code,
|
|
351
|
+
_parse_openai_error,
|
|
352
|
+
is_upstream_model_capacity_error,
|
|
353
|
+
is_upstream_usage_limit_rejection,
|
|
354
|
+
)
|
|
355
|
+
from app.modules.proxy.http_bridge_forwarding import (
|
|
356
|
+
HTTPBridgeForwardContext as HTTPBridgeForwardContext,
|
|
357
|
+
)
|
|
358
|
+
from app.modules.proxy.http_bridge_forwarding import (
|
|
359
|
+
OwnerForwardRelayFailure as OwnerForwardRelayFailure,
|
|
360
|
+
)
|
|
361
|
+
from app.modules.proxy.replay_safety import responses_payload_is_account_neutral_fresh_replay
|
|
362
|
+
from app.modules.proxy.selection_errors import USAGE_LIMIT_REACHED
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _facade() -> Any:
|
|
366
|
+
return sys.modules["app.modules.proxy.service"]
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
# A confirmed stale previous-response anchor can otherwise cause every client
|
|
370
|
+
# reconnect to repeat the same owner lookup and doomed upstream connection.
|
|
371
|
+
# Keep this local and short-lived: an owner record may still be committed by a
|
|
372
|
+
# concurrent request, so discovery always invalidates the negative entry.
|
|
373
|
+
_WEBSOCKET_STALE_PREVIOUS_RESPONSE_CACHE_TTL_SECONDS = 60.0
|
|
374
|
+
_WEBSOCKET_STALE_PREVIOUS_RESPONSE_CACHE_LIMIT = 4096
|
|
375
|
+
# The index is process-global and shared by every turn, so its TTL stamps stay
|
|
376
|
+
# on the real monotonic clock instead of a per-turn injected clock; the
|
|
377
|
+
# simulation harness never reaches it (its fake turns carry no stale anchor).
|
|
378
|
+
_websocket_stale_previous_response_index: dict[tuple[str, str | None], float] = {}
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _clear_websocket_stale_previous_response_cache() -> None:
|
|
382
|
+
"""Drop process-local negative entries when a proxy service is created.
|
|
383
|
+
|
|
384
|
+
The cache intentionally is not durable: it only suppresses repeated
|
|
385
|
+
lookups during a short recovery window. Clearing it with the service
|
|
386
|
+
lifecycle prevents entries from one app/test instance from affecting a
|
|
387
|
+
later instance that happens to receive the same synthetic response id.
|
|
388
|
+
"""
|
|
389
|
+
_websocket_stale_previous_response_index.clear()
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _prune_websocket_stale_previous_response_cache(now: float | None = None) -> None:
|
|
393
|
+
current_time = time.monotonic() if now is None else now
|
|
394
|
+
for cache_key, expires_at in tuple(_websocket_stale_previous_response_index.items()):
|
|
395
|
+
if expires_at <= current_time:
|
|
396
|
+
_websocket_stale_previous_response_index.pop(cache_key, None)
|
|
397
|
+
while len(_websocket_stale_previous_response_index) > _WEBSOCKET_STALE_PREVIOUS_RESPONSE_CACHE_LIMIT:
|
|
398
|
+
_websocket_stale_previous_response_index.pop(next(iter(_websocket_stale_previous_response_index)))
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _remember_websocket_stale_previous_response(
|
|
402
|
+
*,
|
|
403
|
+
previous_response_id: str | None,
|
|
404
|
+
api_key_id: str | None,
|
|
405
|
+
) -> None:
|
|
406
|
+
if previous_response_id is None:
|
|
407
|
+
return
|
|
408
|
+
response_id = previous_response_id.strip()
|
|
409
|
+
if not response_id:
|
|
410
|
+
return
|
|
411
|
+
now = time.monotonic()
|
|
412
|
+
_prune_websocket_stale_previous_response_cache(now)
|
|
413
|
+
cache_key = (response_id, api_key_id)
|
|
414
|
+
_websocket_stale_previous_response_index.pop(cache_key, None)
|
|
415
|
+
_websocket_stale_previous_response_index[cache_key] = now + _WEBSOCKET_STALE_PREVIOUS_RESPONSE_CACHE_TTL_SECONDS
|
|
416
|
+
_prune_websocket_stale_previous_response_cache(now)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _forget_websocket_stale_previous_response(
|
|
420
|
+
*,
|
|
421
|
+
previous_response_id: str | None,
|
|
422
|
+
api_key_id: str | None,
|
|
423
|
+
) -> None:
|
|
424
|
+
if previous_response_id is None:
|
|
425
|
+
return
|
|
426
|
+
response_id = previous_response_id.strip()
|
|
427
|
+
if not response_id:
|
|
428
|
+
return
|
|
429
|
+
_websocket_stale_previous_response_index.pop((response_id, api_key_id), None)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _is_websocket_stale_previous_response(
|
|
433
|
+
*,
|
|
434
|
+
previous_response_id: str | None,
|
|
435
|
+
api_key_id: str | None,
|
|
436
|
+
) -> bool:
|
|
437
|
+
if previous_response_id is None:
|
|
438
|
+
return False
|
|
439
|
+
response_id = previous_response_id.strip()
|
|
440
|
+
if not response_id:
|
|
441
|
+
return False
|
|
442
|
+
now = time.monotonic()
|
|
443
|
+
_prune_websocket_stale_previous_response_cache(now)
|
|
444
|
+
expires_at = _websocket_stale_previous_response_index.get((response_id, api_key_id))
|
|
445
|
+
if expires_at is None:
|
|
446
|
+
return False
|
|
447
|
+
if expires_at <= now:
|
|
448
|
+
_websocket_stale_previous_response_index.pop((response_id, api_key_id), None)
|
|
449
|
+
return False
|
|
450
|
+
return True
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _prepare_websocket_request_state_for_visible_output_replay(
|
|
454
|
+
request_state: "_WebSocketRequestState",
|
|
455
|
+
) -> str | None:
|
|
456
|
+
# An identity captured by an earlier replay (or staged by the terminal
|
|
457
|
+
# capacity path before ``response_id`` was cleared) outlives this call, so
|
|
458
|
+
# a bounded extra replay keeps rewriting to the id the client is reading.
|
|
459
|
+
downstream_response_id = request_state.replay_downstream_response_id
|
|
460
|
+
if (
|
|
461
|
+
downstream_response_id is None
|
|
462
|
+
and request_state.response_id is not None
|
|
463
|
+
and not request_state.awaiting_response_created
|
|
464
|
+
):
|
|
465
|
+
downstream_response_id = request_state.response_id
|
|
466
|
+
suppress_in_progress = request_state.suppress_next_in_progress_downstream or (
|
|
467
|
+
downstream_response_id is not None and request_state.response_event_count >= 2
|
|
468
|
+
)
|
|
469
|
+
fresh_request_text = request_state.fresh_upstream_request_text
|
|
470
|
+
if request_state.fresh_upstream_request_is_retry_safe and fresh_request_text:
|
|
471
|
+
# Only a proxy-injected anchor is the proxy's to release: its pin was
|
|
472
|
+
# derived from an anchor the client never asked for. A client-supplied
|
|
473
|
+
# anchor is the client's own continuation, so its fresh body keeps the
|
|
474
|
+
# owner pin and reconnects there (the capacity path re-sends such a
|
|
475
|
+
# turn to its owner; ``_websocket_auth_request_can_switch_account``
|
|
476
|
+
# treats it as account-bound).
|
|
477
|
+
_install_fresh_replay_body(
|
|
478
|
+
request_state,
|
|
479
|
+
fresh_request_text,
|
|
480
|
+
account_neutral=_websocket_request_text_is_account_neutral_fresh_replay(fresh_request_text),
|
|
481
|
+
release_owner_pin=(
|
|
482
|
+
request_state.previous_response_id is None or request_state.proxy_injected_previous_response_id
|
|
483
|
+
),
|
|
484
|
+
)
|
|
485
|
+
request_text = request_state.request_text
|
|
486
|
+
if not isinstance(request_text, str):
|
|
487
|
+
return None
|
|
488
|
+
request_state.replay_count += 1
|
|
489
|
+
request_state.awaiting_response_created = True
|
|
490
|
+
request_state.response_id = None
|
|
491
|
+
request_state.response_event_count = 0
|
|
492
|
+
request_state.replay_downstream_response_id = downstream_response_id
|
|
493
|
+
request_state.suppress_next_created_downstream = downstream_response_id is not None
|
|
494
|
+
request_state.suppress_next_in_progress_downstream = suppress_in_progress
|
|
495
|
+
_clear_websocket_request_error_overrides(request_state)
|
|
496
|
+
return request_text
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _websocket_owner_switch_has_other_pending_requests(
|
|
500
|
+
request_state: "_WebSocketRequestState",
|
|
501
|
+
pending_requests: deque["_WebSocketRequestState"],
|
|
502
|
+
) -> bool:
|
|
503
|
+
return any(pending is not request_state for pending in pending_requests)
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def _websocket_request_text_is_account_neutral_fresh_replay(request_text: str | None) -> bool:
|
|
507
|
+
if not isinstance(request_text, str):
|
|
508
|
+
return False
|
|
509
|
+
try:
|
|
510
|
+
payload = json.loads(request_text)
|
|
511
|
+
except json.JSONDecodeError:
|
|
512
|
+
return False
|
|
513
|
+
if not isinstance(payload, dict):
|
|
514
|
+
return False
|
|
515
|
+
event_type = payload.get("type")
|
|
516
|
+
if event_type is not None and event_type != "response.create":
|
|
517
|
+
return False
|
|
518
|
+
payload.pop("type", None)
|
|
519
|
+
return responses_payload_is_account_neutral_fresh_replay(cast(dict[str, JsonValue], payload))
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def _bind_websocket_request_dispatch_owner(
|
|
523
|
+
request_state: "_WebSocketRequestState",
|
|
524
|
+
*,
|
|
525
|
+
account_id: str,
|
|
526
|
+
exact_request_text: str,
|
|
527
|
+
) -> bool:
|
|
528
|
+
required_account_id = request_state.replay_required_account_id
|
|
529
|
+
if _websocket_request_text_is_account_neutral_fresh_replay(exact_request_text):
|
|
530
|
+
return required_account_id is None or required_account_id == account_id
|
|
531
|
+
if required_account_id is not None and required_account_id != account_id:
|
|
532
|
+
return False
|
|
533
|
+
request_state.preferred_account_id = account_id
|
|
534
|
+
request_state.replay_required_account_id = account_id
|
|
535
|
+
return True
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _install_verified_fresh_replay(
|
|
539
|
+
request_state: "_WebSocketRequestState",
|
|
540
|
+
*,
|
|
541
|
+
require_proxy_injected_previous_response_id: bool = True,
|
|
542
|
+
require_account_neutral: bool = True,
|
|
543
|
+
) -> str | None:
|
|
544
|
+
if not (request_state.fresh_upstream_request_is_retry_safe and request_state.fresh_upstream_request_text):
|
|
545
|
+
return None
|
|
546
|
+
if require_proxy_injected_previous_response_id and not request_state.proxy_injected_previous_response_id:
|
|
547
|
+
return None
|
|
548
|
+
fresh_request_text = request_state.fresh_upstream_request_text
|
|
549
|
+
account_neutral = _websocket_request_text_is_account_neutral_fresh_replay(fresh_request_text)
|
|
550
|
+
if require_account_neutral and not account_neutral:
|
|
551
|
+
return None
|
|
552
|
+
if not account_neutral and (request_state.replay_required_account_id or request_state.preferred_account_id) is None:
|
|
553
|
+
return None
|
|
554
|
+
return _install_fresh_replay_body(request_state, fresh_request_text, account_neutral=account_neutral)
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def _install_fresh_replay_body(
|
|
558
|
+
request_state: "_WebSocketRequestState",
|
|
559
|
+
fresh_request_text: str,
|
|
560
|
+
*,
|
|
561
|
+
account_neutral: bool,
|
|
562
|
+
release_owner_pin: bool = True,
|
|
563
|
+
) -> str:
|
|
564
|
+
"""Swap the retained fresh body in and re-derive the owner requirement from it.
|
|
565
|
+
|
|
566
|
+
The anchored body pinned the request to the anchor's owner
|
|
567
|
+
(``replay_required_account_id`` / ``preferred_account_id``, bound at
|
|
568
|
+
dispatch and on connect). The fresh body carries no anchor: an
|
|
569
|
+
account-neutral one is free to move, a non-neutral one stays with the
|
|
570
|
+
owner that was already required or preferred. Every path that installs
|
|
571
|
+
the fresh body must reconcile the pin with it; a stale pin under a body
|
|
572
|
+
that turned neutral is what let an accepted transport-close replay exclude
|
|
573
|
+
the very account its reconnect still required.
|
|
574
|
+
|
|
575
|
+
A turn-state owner is a session pin, not a body pin. The session loop
|
|
576
|
+
re-resolves it before every reconnect and ``_connect_proxy_websocket``
|
|
577
|
+
hard-requires it (``turn_state_owner_required``) whenever the session
|
|
578
|
+
carries an owner, so the fresh body cannot release it: clearing it here
|
|
579
|
+
only hid the requirement from the exclusion decision, and the replay then
|
|
580
|
+
excluded the account its reconnect was about to require.
|
|
581
|
+
|
|
582
|
+
``release_owner_pin=False`` keeps both pins untouched for a replay whose
|
|
583
|
+
anchor the proxy is not entitled to release (a client-supplied
|
|
584
|
+
``previous_response_id``): the fresh body is still what goes upstream,
|
|
585
|
+
but it stays with the owner the anchored body was bound to.
|
|
586
|
+
"""
|
|
587
|
+
replay_required_account_id = request_state.replay_required_account_id or request_state.preferred_account_id
|
|
588
|
+
turn_state_owner_account_id = (
|
|
589
|
+
request_state.preferred_account_id
|
|
590
|
+
if request_state.affinity_policy.codex_session_source == "turn_state"
|
|
591
|
+
else None
|
|
592
|
+
)
|
|
593
|
+
request_state.request_text = fresh_request_text
|
|
594
|
+
request_state.previous_response_id = None
|
|
595
|
+
if release_owner_pin:
|
|
596
|
+
request_state.preferred_account_id = turn_state_owner_account_id
|
|
597
|
+
request_state.replay_required_account_id = None if account_neutral else replay_required_account_id
|
|
598
|
+
request_state.proxy_injected_previous_response_id = False
|
|
599
|
+
request_state.fresh_upstream_request_is_retry_safe = False
|
|
600
|
+
request_state.responses_lite_model = request_state.fresh_upstream_request_responses_lite_model
|
|
601
|
+
_refresh_websocket_request_input_fingerprint_from_text(request_state)
|
|
602
|
+
return fresh_request_text
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def _websocket_accepted_replay_can_switch_account(request_state: "_WebSocketRequestState") -> bool:
|
|
606
|
+
"""Return whether an accepted replay may leave the account that accepted it.
|
|
607
|
+
|
|
608
|
+
Mirrors the owner requirements ``_connect_proxy_websocket`` enforces on
|
|
609
|
+
the reconnect: a bound replay owner, a file pin, an anchored owner, or a
|
|
610
|
+
turn-state owner keeps the replay on that account, so excluding it would
|
|
611
|
+
leave the reconnect no eligible account. Evaluate this after the replay
|
|
612
|
+
body has been prepared, once the anchor is stripped and the owner pin
|
|
613
|
+
reconciled with the body that will actually be sent; the turn-state owner
|
|
614
|
+
survives that reconciliation (``_install_fresh_replay_body``) because the
|
|
615
|
+
connect requires it regardless of the body. A turn-state session without
|
|
616
|
+
a resolved owner is free to move.
|
|
617
|
+
"""
|
|
618
|
+
if request_state.replay_required_account_id is not None:
|
|
619
|
+
return False
|
|
620
|
+
if request_state.preferred_account_id is not None and (
|
|
621
|
+
request_state.previous_response_id is not None
|
|
622
|
+
or request_state.affinity_policy.codex_session_source == "turn_state"
|
|
623
|
+
):
|
|
624
|
+
return False
|
|
625
|
+
return _websocket_auth_request_can_switch_account(request_state)
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def _websocket_affinity_may_resolve_hard_owner(affinity_policy: _AffinityPolicy) -> bool:
|
|
629
|
+
"""Return whether sticky selection may bind this request to one owner account.
|
|
630
|
+
|
|
631
|
+
The predicate is shared with the HTTP bridge accepted replay
|
|
632
|
+
(``_affinity_may_resolve_hard_owner``): a resolved hard ``CODEX_SESSION``
|
|
633
|
+
row -- turn-state ownership or the raw compatibility row consulted through
|
|
634
|
+
``legacy_selection_key`` -- narrows selection to an owner the request state
|
|
635
|
+
never carries, so excluding that owner would leave every re-selection at
|
|
636
|
+
``hard_affinity_saturated`` until the connect budget runs out.
|
|
637
|
+
"""
|
|
638
|
+
return _affinity_may_resolve_hard_owner(affinity_policy)
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def _websocket_accepted_replay_may_exclude_account(request_state: "_WebSocketRequestState") -> bool:
|
|
642
|
+
"""Return whether an accepted replay may exclude the account that accepted it.
|
|
643
|
+
|
|
644
|
+
The exclusion is what moves an account-neutral accepted replay to another
|
|
645
|
+
account (bridge parity). It is refused when a request-state pin requires
|
|
646
|
+
the owner (``_websocket_accepted_replay_can_switch_account``) and when the
|
|
647
|
+
request's affinity may resolve to a hard owner the state does not carry
|
|
648
|
+
(``_websocket_affinity_may_resolve_hard_owner``). In both cases the replay
|
|
649
|
+
reconnects without an exclusion, as the created-only transport-close replay
|
|
650
|
+
did before accepted replays existed: sticky selection then re-resolves the
|
|
651
|
+
owner, or moves a soft row through the health penalty the failing account
|
|
652
|
+
receives. Only accepted lifecycles decide with this predicate; the
|
|
653
|
+
pre-created owner-switch branch keeps the pin-only predicate it already
|
|
654
|
+
used.
|
|
655
|
+
"""
|
|
656
|
+
if _websocket_affinity_may_resolve_hard_owner(request_state.affinity_policy):
|
|
657
|
+
return False
|
|
658
|
+
return _websocket_accepted_replay_can_switch_account(request_state)
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
async def _record_or_defer_websocket_accepted_replay_health(
|
|
662
|
+
proxy: Any,
|
|
663
|
+
request_state: "_WebSocketRequestState",
|
|
664
|
+
*,
|
|
665
|
+
account: Account,
|
|
666
|
+
error_message: str | None,
|
|
667
|
+
error_code: str,
|
|
668
|
+
) -> None:
|
|
669
|
+
"""Penalize the account an accepted replay leaves, now or after settlement.
|
|
670
|
+
|
|
671
|
+
The accepted request keeps its API-key reservation open across the
|
|
672
|
+
re-send, and account health must not be written while a reservation is
|
|
673
|
+
unsettled (api-keys spec settlement-ordering invariant; bridge parity with
|
|
674
|
+
``_handle_or_defer_precreated_stream_health``). A keyed request queues the
|
|
675
|
+
classified penalty on its state; ``_finalize_websocket_request_state`` and
|
|
676
|
+
``_release_websocket_request_state_reservation`` drain it once the
|
|
677
|
+
reservation settles or is released. Unkeyed requests write immediately.
|
|
678
|
+
``account_health_error_handled`` is deliberately not set: the staged
|
|
679
|
+
terminal is never finalized on this surface, so a later terminal belongs to
|
|
680
|
+
the replacement attempt and earns its own penalty.
|
|
681
|
+
"""
|
|
682
|
+
error: UpstreamError = {"message": error_message or "Upstream error"}
|
|
683
|
+
if request_state.api_key_reservation is not None:
|
|
684
|
+
request_state.deferred_keyed_stream_health.append(
|
|
685
|
+
_DeferredKeyedStreamHealthPenalty(account=account, error=error, code=error_code)
|
|
686
|
+
)
|
|
687
|
+
return
|
|
688
|
+
await proxy._handle_stream_error(account, error, error_code)
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def _prepare_websocket_request_state_for_account_switch(
|
|
692
|
+
request_state: "_WebSocketRequestState",
|
|
693
|
+
) -> str | None:
|
|
694
|
+
"""Return an unsent request body only when moving accounts is proven safe."""
|
|
695
|
+
if request_state.previous_response_id is None:
|
|
696
|
+
if not _websocket_request_text_is_account_neutral_fresh_replay(request_state.request_text):
|
|
697
|
+
return None
|
|
698
|
+
return request_state.request_text
|
|
699
|
+
return _install_verified_fresh_replay(request_state)
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def _retire_websocket_continuity_anchor(continuity_state: _WebSocketContinuityState) -> None:
|
|
703
|
+
"""Drop the completed-response anchor and the state that only exists for it."""
|
|
704
|
+
continuity_state.last_completed_response_id = None
|
|
705
|
+
continuity_state.last_completed_input_count = 0
|
|
706
|
+
continuity_state.last_completed_input_prefix_fingerprint = None
|
|
707
|
+
continuity_state.last_pending_function_call_ids = []
|
|
708
|
+
continuity_state.last_pending_tool_call_types = {}
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
def _websocket_continuity_anchor_for_payload(
|
|
712
|
+
continuity_state: _WebSocketContinuityState | None,
|
|
713
|
+
*,
|
|
714
|
+
responses_payload: ResponsesRequest,
|
|
715
|
+
codex_session_affinity: bool,
|
|
716
|
+
api_key_id: str | None = None,
|
|
717
|
+
) -> _WebSocketContinuityAnchor | None:
|
|
718
|
+
"""Select a matching session anchor, retiring any known upstream rejection."""
|
|
719
|
+
if continuity_state is None or not codex_session_affinity:
|
|
720
|
+
return None
|
|
721
|
+
if responses_payload.previous_response_id is not None:
|
|
722
|
+
return None
|
|
723
|
+
previous_response_id = continuity_state.last_completed_response_id
|
|
724
|
+
if previous_response_id is None:
|
|
725
|
+
return None
|
|
726
|
+
if _is_websocket_stale_previous_response(previous_response_id=previous_response_id, api_key_id=api_key_id):
|
|
727
|
+
# Upstream already denied this anchor (``previous_response_not_found``)
|
|
728
|
+
# and the fail-closed path remembered it. Injecting it again would
|
|
729
|
+
# fail the client's retry identically (#1921): retire it from session
|
|
730
|
+
# continuity so the full-context resend goes unanchored, and so the
|
|
731
|
+
# same id cannot return once the negative cache entry expires.
|
|
732
|
+
_retire_websocket_continuity_anchor(continuity_state)
|
|
733
|
+
_facade().logger.info(
|
|
734
|
+
"websocket_session_anchor_retired response_id=%s reason=stale_previous_response",
|
|
735
|
+
previous_response_id,
|
|
736
|
+
)
|
|
737
|
+
return None
|
|
738
|
+
stored_count = continuity_state.last_completed_input_count
|
|
739
|
+
if not _facade()._input_prefix_matches_stored_context(
|
|
740
|
+
responses_payload.input,
|
|
741
|
+
stored_count=stored_count,
|
|
742
|
+
stored_fingerprint=continuity_state.last_completed_input_prefix_fingerprint,
|
|
743
|
+
):
|
|
744
|
+
return None
|
|
745
|
+
return _WebSocketContinuityAnchor(
|
|
746
|
+
previous_response_id=previous_response_id,
|
|
747
|
+
stored_input_item_count=stored_count,
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
_WEBSOCKET_TOOL_CALL_ITEM_TYPES_BY_OUTPUT_TYPE = {
|
|
752
|
+
"function_call_output": "function_call",
|
|
753
|
+
"custom_tool_call_output": "custom_tool_call",
|
|
754
|
+
"apply_patch_call_output": "apply_patch_call",
|
|
755
|
+
}
|
|
756
|
+
_WEBSOCKET_TOOL_CALL_ITEM_TYPES = frozenset(_WEBSOCKET_TOOL_CALL_ITEM_TYPES_BY_OUTPUT_TYPE.values())
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
def _websocket_input_items_are_self_contained_fresh_replay(input_items: list[JsonValue]) -> bool:
|
|
760
|
+
seen_call_ids_by_type: dict[str, set[str]] = {item_type: set() for item_type in _WEBSOCKET_TOOL_CALL_ITEM_TYPES}
|
|
761
|
+
for item in input_items:
|
|
762
|
+
if not isinstance(item, dict):
|
|
763
|
+
continue
|
|
764
|
+
item_type = _websocket_input_item_type(item)
|
|
765
|
+
call_id_value = item.get("call_id")
|
|
766
|
+
call_id = call_id_value if isinstance(call_id_value, str) and call_id_value else None
|
|
767
|
+
if item_type in _WEBSOCKET_TOOL_CALL_ITEM_TYPES:
|
|
768
|
+
if call_id is not None:
|
|
769
|
+
seen_call_ids_by_type[item_type].add(call_id)
|
|
770
|
+
continue
|
|
771
|
+
call_item_type = _WEBSOCKET_TOOL_CALL_ITEM_TYPES_BY_OUTPUT_TYPE.get(item_type or "")
|
|
772
|
+
if call_item_type is None:
|
|
773
|
+
continue
|
|
774
|
+
if call_id is None or call_id not in seen_call_ids_by_type[call_item_type]:
|
|
775
|
+
return False
|
|
776
|
+
return True
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
def _websocket_client_previous_response_full_resend_is_retry_safe(
|
|
780
|
+
*,
|
|
781
|
+
previous_response_id: str | None,
|
|
782
|
+
input_value: JsonValue,
|
|
783
|
+
continuity_state: _WebSocketContinuityState | None,
|
|
784
|
+
) -> bool:
|
|
785
|
+
if previous_response_id is None or not isinstance(input_value, list):
|
|
786
|
+
return False
|
|
787
|
+
input_items = cast(list[JsonValue], input_value)
|
|
788
|
+
if len(input_items) <= 1:
|
|
789
|
+
return False
|
|
790
|
+
if not _websocket_input_items_are_self_contained_fresh_replay(input_items):
|
|
791
|
+
return False
|
|
792
|
+
if (
|
|
793
|
+
continuity_state is not None
|
|
794
|
+
and continuity_state.last_completed_response_id == previous_response_id
|
|
795
|
+
and (
|
|
796
|
+
continuity_state.last_completed_input_count > 0
|
|
797
|
+
or continuity_state.last_completed_input_prefix_fingerprint is not None
|
|
798
|
+
)
|
|
799
|
+
):
|
|
800
|
+
return _facade()._input_prefix_matches_stored_context(
|
|
801
|
+
input_value,
|
|
802
|
+
stored_count=continuity_state.last_completed_input_count,
|
|
803
|
+
stored_fingerprint=continuity_state.last_completed_input_prefix_fingerprint,
|
|
804
|
+
)
|
|
805
|
+
return True
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
def _record_websocket_continuity_completion(
|
|
809
|
+
continuity_state: _WebSocketContinuityState,
|
|
810
|
+
*,
|
|
811
|
+
request_state: _WebSocketRequestState,
|
|
812
|
+
response_id: str | None,
|
|
813
|
+
) -> None:
|
|
814
|
+
"""Record completed context and pending tools, or clear an absent response anchor."""
|
|
815
|
+
if response_id is None:
|
|
816
|
+
_retire_websocket_continuity_anchor(continuity_state)
|
|
817
|
+
return
|
|
818
|
+
# Record the completed response id and pending tool-call metadata
|
|
819
|
+
# regardless of input shape (string inputs leave ``input_item_count`` at
|
|
820
|
+
# 0), so an anchored follow-up can still match continuity and receive
|
|
821
|
+
# synthetic interrupted tool outputs. Prefix anchoring/trimming is only
|
|
822
|
+
# meaningful for fingerprinted list inputs, so the count/fingerprint pair
|
|
823
|
+
# is cleared rather than left stale when the completed turn cannot
|
|
824
|
+
# provide one.
|
|
825
|
+
continuity_state.last_completed_response_id = response_id
|
|
826
|
+
if request_state.input_item_count > 0 and request_state.input_full_fingerprint is not None:
|
|
827
|
+
continuity_state.last_completed_input_count = request_state.input_item_count
|
|
828
|
+
continuity_state.last_completed_input_prefix_fingerprint = request_state.input_full_fingerprint
|
|
829
|
+
else:
|
|
830
|
+
continuity_state.last_completed_input_count = 0
|
|
831
|
+
continuity_state.last_completed_input_prefix_fingerprint = None
|
|
832
|
+
continuity_state.last_pending_function_call_ids = list(request_state.pending_function_call_ids)
|
|
833
|
+
continuity_state.last_pending_tool_call_types = dict(request_state.pending_tool_call_types)
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
def _record_websocket_responses_lite_acceptance(
|
|
837
|
+
continuity_state: _WebSocketContinuityState,
|
|
838
|
+
*,
|
|
839
|
+
request_state: _WebSocketRequestState,
|
|
840
|
+
) -> None:
|
|
841
|
+
# Codex can reuse an accepted ``generate=false`` prewarm response and send
|
|
842
|
+
# only an empty or user-only input delta next. That frame no longer carries
|
|
843
|
+
# ``additional_tools``, so the accepted prewarm must seed Lite continuity.
|
|
844
|
+
# Only Lite acceptances may update the single-slot state: a non-Lite
|
|
845
|
+
# acceptance must not clobber a previously accepted Lite continuity with
|
|
846
|
+
# ``None``. The accepted response id is recorded so trusted incremental
|
|
847
|
+
# continuity can require ``previous_response_id`` to reference it.
|
|
848
|
+
if request_state.responses_lite_model is None:
|
|
849
|
+
return
|
|
850
|
+
continuity_state.responses_lite_model = request_state.responses_lite_model
|
|
851
|
+
# Prefer the downstream-visible id: a suppressed-created replay keeps
|
|
852
|
+
# exposing the original response id to the client (every downstream event
|
|
853
|
+
# is rewritten to it), so the next incremental frame can only reference
|
|
854
|
+
# that visible id, never the hidden upstream replay id.
|
|
855
|
+
continuity_state.responses_lite_response_id = (
|
|
856
|
+
request_state.replay_downstream_response_id or request_state.response_id
|
|
857
|
+
)
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
def _websocket_response_id(
|
|
861
|
+
event: OpenAIEvent | None,
|
|
862
|
+
payload: dict[str, JsonValue] | None,
|
|
863
|
+
*,
|
|
864
|
+
routing: NativeWebSocketRoutingMetadata | None = None,
|
|
865
|
+
) -> str | None:
|
|
866
|
+
if event is not None and event.response is not None and event.response.id:
|
|
867
|
+
return event.response.id
|
|
868
|
+
if routing is not None:
|
|
869
|
+
return routing.payload_response_id
|
|
870
|
+
if not isinstance(payload, dict):
|
|
871
|
+
return None
|
|
872
|
+
direct_response_id = payload.get("response_id")
|
|
873
|
+
if isinstance(direct_response_id, str):
|
|
874
|
+
stripped_direct_response_id = direct_response_id.strip()
|
|
875
|
+
if stripped_direct_response_id:
|
|
876
|
+
return stripped_direct_response_id
|
|
877
|
+
response = payload.get("response")
|
|
878
|
+
if not isinstance(response, dict):
|
|
879
|
+
return None
|
|
880
|
+
response_id = response.get("id")
|
|
881
|
+
if not isinstance(response_id, str):
|
|
882
|
+
return None
|
|
883
|
+
stripped = response_id.strip()
|
|
884
|
+
return stripped or None
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
def _websocket_downstream_response_id(request_state: "_WebSocketRequestState") -> str:
|
|
888
|
+
return request_state.replay_downstream_response_id or request_state.response_id or request_state.request_id
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
def _websocket_continuity_response_ids(
|
|
892
|
+
request_state: "_WebSocketRequestState",
|
|
893
|
+
upstream_response_id: str | None,
|
|
894
|
+
) -> tuple[str, ...]:
|
|
895
|
+
response_ids: list[str] = []
|
|
896
|
+
for response_id in (upstream_response_id, request_state.replay_downstream_response_id):
|
|
897
|
+
if not isinstance(response_id, str):
|
|
898
|
+
continue
|
|
899
|
+
stripped = response_id.strip()
|
|
900
|
+
if stripped and stripped not in response_ids:
|
|
901
|
+
response_ids.append(stripped)
|
|
902
|
+
return tuple(response_ids)
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
def _rewrite_websocket_downstream_response_id(
|
|
906
|
+
payload: dict[str, JsonValue],
|
|
907
|
+
request_state: "_WebSocketRequestState",
|
|
908
|
+
) -> dict[str, JsonValue]:
|
|
909
|
+
downstream_response_id = request_state.replay_downstream_response_id
|
|
910
|
+
if downstream_response_id is None:
|
|
911
|
+
return payload
|
|
912
|
+
|
|
913
|
+
direct_response_id = payload.get("response_id")
|
|
914
|
+
rewrite_direct = isinstance(direct_response_id, str) and direct_response_id != downstream_response_id
|
|
915
|
+
response = payload.get("response")
|
|
916
|
+
nested_response_id = response.get("id") if isinstance(response, dict) else None
|
|
917
|
+
rewrite_nested = isinstance(nested_response_id, str) and nested_response_id != downstream_response_id
|
|
918
|
+
if not rewrite_direct and not rewrite_nested:
|
|
919
|
+
# Identity fast-path contract: callers skip re-serialization when the
|
|
920
|
+
# original payload object comes back, so an already-aligned frame must
|
|
921
|
+
# not be copied into an equal-but-new dict.
|
|
922
|
+
return payload
|
|
923
|
+
|
|
924
|
+
rewritten = dict(payload)
|
|
925
|
+
if rewrite_direct:
|
|
926
|
+
rewritten["response_id"] = downstream_response_id
|
|
927
|
+
if rewrite_nested and isinstance(response, dict):
|
|
928
|
+
rewritten["response"] = {**response, "id": downstream_response_id}
|
|
929
|
+
return rewritten
|
|
930
|
+
|
|
931
|
+
|
|
932
|
+
def _websocket_event_error_code(event_type: str | None, payload: dict[str, JsonValue] | None) -> str | None:
|
|
933
|
+
error = _websocket_event_error_payload(event_type, payload)
|
|
934
|
+
if not isinstance(error, dict):
|
|
935
|
+
return None
|
|
936
|
+
code_value = error.get("code")
|
|
937
|
+
if not isinstance(code_value, str):
|
|
938
|
+
return None
|
|
939
|
+
stripped = code_value.strip()
|
|
940
|
+
return stripped or None
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
def _websocket_event_error_type(event_type: str | None, payload: dict[str, JsonValue] | None) -> str | None:
|
|
944
|
+
error = _websocket_event_error_payload(event_type, payload)
|
|
945
|
+
if not isinstance(error, dict):
|
|
946
|
+
return None
|
|
947
|
+
type_value = error.get("type")
|
|
948
|
+
if not isinstance(type_value, str):
|
|
949
|
+
return None
|
|
950
|
+
stripped = type_value.strip()
|
|
951
|
+
return stripped or None
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
def _websocket_event_error_param(
|
|
955
|
+
event_type: str | None,
|
|
956
|
+
payload: dict[str, JsonValue] | None,
|
|
957
|
+
) -> OpenAIErrorParam | None:
|
|
958
|
+
error = _websocket_event_error_payload(event_type, payload)
|
|
959
|
+
if not isinstance(error, dict):
|
|
960
|
+
return None
|
|
961
|
+
return OpenAIErrorParam.from_mapping(cast(Mapping[str, JsonValue], error))
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
def _websocket_event_error_message(event_type: str | None, payload: dict[str, JsonValue] | None) -> str | None:
|
|
965
|
+
error = _websocket_event_error_payload(event_type, payload)
|
|
966
|
+
if not isinstance(error, dict):
|
|
967
|
+
return None
|
|
968
|
+
message_value = error.get("message")
|
|
969
|
+
if not isinstance(message_value, str):
|
|
970
|
+
return None
|
|
971
|
+
stripped = message_value.strip()
|
|
972
|
+
return stripped or None
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
def _websocket_precreated_retry_error_code(
|
|
976
|
+
request_state: _WebSocketRequestState | None,
|
|
977
|
+
*,
|
|
978
|
+
event_type: str | None,
|
|
979
|
+
payload: dict[str, JsonValue] | None,
|
|
980
|
+
has_other_pending_requests: bool,
|
|
981
|
+
) -> str | None:
|
|
982
|
+
if request_state is None:
|
|
983
|
+
return None
|
|
984
|
+
if request_state.response_id is not None and not request_state.awaiting_response_created:
|
|
985
|
+
# An accepted response is only replayable under the output-free
|
|
986
|
+
# capacity rule; every pre-created classifier below refuses it.
|
|
987
|
+
return _websocket_accepted_capacity_retry_error_code(
|
|
988
|
+
request_state,
|
|
989
|
+
event_type=event_type,
|
|
990
|
+
error_code=_normalize_error_code(
|
|
991
|
+
_websocket_event_error_code(event_type, payload),
|
|
992
|
+
_websocket_event_error_type(event_type, payload),
|
|
993
|
+
),
|
|
994
|
+
error_message=_websocket_event_error_message(event_type, payload),
|
|
995
|
+
payload_response_id=_websocket_response_id(None, payload),
|
|
996
|
+
payload=payload,
|
|
997
|
+
has_other_pending_requests=has_other_pending_requests,
|
|
998
|
+
)
|
|
999
|
+
if request_state.last_downstream_sequence_number is not None:
|
|
1000
|
+
return None
|
|
1001
|
+
if request_state.downstream_visible:
|
|
1002
|
+
return None
|
|
1003
|
+
if has_other_pending_requests:
|
|
1004
|
+
return None
|
|
1005
|
+
if request_state.response_id is not None:
|
|
1006
|
+
return None
|
|
1007
|
+
if request_state.response_event_count > 0:
|
|
1008
|
+
return None
|
|
1009
|
+
if not request_state.awaiting_response_created:
|
|
1010
|
+
return None
|
|
1011
|
+
if not request_state.request_text:
|
|
1012
|
+
return None
|
|
1013
|
+
if request_state.replay_count >= 1:
|
|
1014
|
+
return None
|
|
1015
|
+
if event_type not in {"error", "response.failed"}:
|
|
1016
|
+
return None
|
|
1017
|
+
|
|
1018
|
+
error_code = _normalize_error_code(
|
|
1019
|
+
_websocket_event_error_code(event_type, payload),
|
|
1020
|
+
_websocket_event_error_type(event_type, payload),
|
|
1021
|
+
)
|
|
1022
|
+
error_param = _websocket_event_error_param(event_type, payload)
|
|
1023
|
+
error_message = _websocket_event_error_message(event_type, payload)
|
|
1024
|
+
if _facade()._is_previous_response_not_found_error(
|
|
1025
|
+
code=error_code,
|
|
1026
|
+
param=error_param,
|
|
1027
|
+
message=error_message,
|
|
1028
|
+
):
|
|
1029
|
+
return "stream_incomplete"
|
|
1030
|
+
if _facade()._is_missing_tool_output_error(
|
|
1031
|
+
code=error_code,
|
|
1032
|
+
param=error_param,
|
|
1033
|
+
message=error_message,
|
|
1034
|
+
):
|
|
1035
|
+
return None
|
|
1036
|
+
if _is_account_model_unsupported_error(
|
|
1037
|
+
code=error_code,
|
|
1038
|
+
message=error_message,
|
|
1039
|
+
model=request_state.model,
|
|
1040
|
+
):
|
|
1041
|
+
# Any recognized response id means upstream has accepted this request,
|
|
1042
|
+
# even when response.created was not observed on this socket. Do not
|
|
1043
|
+
# account-switch an error event that carries either response.id or a
|
|
1044
|
+
# top-level response_id as though it were still pre-created.
|
|
1045
|
+
if _websocket_response_id(None, payload) is not None:
|
|
1046
|
+
return None
|
|
1047
|
+
return _ACCOUNT_MODEL_UNSUPPORTED_ERROR_CODE
|
|
1048
|
+
if is_upstream_model_capacity_error(error_message):
|
|
1049
|
+
if error_code in {
|
|
1050
|
+
"rate_limit_exceeded",
|
|
1051
|
+
"usage_limit_reached",
|
|
1052
|
+
"insufficient_quota",
|
|
1053
|
+
"usage_not_included",
|
|
1054
|
+
"quota_exceeded",
|
|
1055
|
+
}:
|
|
1056
|
+
return error_code
|
|
1057
|
+
if _websocket_response_id(None, payload) is not None:
|
|
1058
|
+
return None
|
|
1059
|
+
return "server_is_overloaded"
|
|
1060
|
+
return _websocket_transparent_replay_error_code(error_code, error_message)
|
|
1061
|
+
|
|
1062
|
+
|
|
1063
|
+
def _websocket_precreated_replay_fallback_error(
|
|
1064
|
+
request_state: _WebSocketRequestState,
|
|
1065
|
+
) -> tuple[int, OpenAIErrorEnvelope, str, str, str | None] | None:
|
|
1066
|
+
if request_state.precreated_replay_reason != _ACCOUNT_MODEL_UNSUPPORTED_ERROR_CODE:
|
|
1067
|
+
return None
|
|
1068
|
+
error_code = request_state.error_code_override or "invalid_request_error"
|
|
1069
|
+
error_message = request_state.error_message_override or "Upstream rejected the requested model for this account"
|
|
1070
|
+
error_type = request_state.error_type_override or "invalid_request_error"
|
|
1071
|
+
payload = openai_error(error_code, error_message, error_type=error_type)
|
|
1072
|
+
error_param_override = coerce_error_param(request_state.error_param_override)
|
|
1073
|
+
if error_param_override.present:
|
|
1074
|
+
public_param = normalize_public_error_param(error_param_override)
|
|
1075
|
+
if public_param is not None:
|
|
1076
|
+
payload["error"]["param"] = public_param
|
|
1077
|
+
return (
|
|
1078
|
+
request_state.error_http_status_override or 400,
|
|
1079
|
+
payload,
|
|
1080
|
+
error_code,
|
|
1081
|
+
error_message,
|
|
1082
|
+
request_state.precreated_replay_account_id,
|
|
1083
|
+
)
|
|
1084
|
+
|
|
1085
|
+
|
|
1086
|
+
def _websocket_precreated_auth_error_code(
|
|
1087
|
+
request_state: _WebSocketRequestState | None,
|
|
1088
|
+
*,
|
|
1089
|
+
event_type: str | None,
|
|
1090
|
+
payload: dict[str, JsonValue] | None,
|
|
1091
|
+
has_other_pending_requests: bool,
|
|
1092
|
+
) -> str | None:
|
|
1093
|
+
if request_state is None:
|
|
1094
|
+
return None
|
|
1095
|
+
if request_state.last_downstream_sequence_number is not None:
|
|
1096
|
+
return None
|
|
1097
|
+
if has_other_pending_requests:
|
|
1098
|
+
return None
|
|
1099
|
+
if request_state.response_id is not None:
|
|
1100
|
+
return None
|
|
1101
|
+
if request_state.response_event_count > 0:
|
|
1102
|
+
return None
|
|
1103
|
+
if not request_state.awaiting_response_created:
|
|
1104
|
+
return None
|
|
1105
|
+
if not request_state.request_text:
|
|
1106
|
+
return None
|
|
1107
|
+
if request_state.downstream_visible:
|
|
1108
|
+
return None
|
|
1109
|
+
if event_type not in {"error", "response.failed"}:
|
|
1110
|
+
return None
|
|
1111
|
+
if _websocket_response_id(None, payload) is not None:
|
|
1112
|
+
return None
|
|
1113
|
+
|
|
1114
|
+
error_code = _normalize_error_code(
|
|
1115
|
+
_websocket_event_error_code(event_type, payload),
|
|
1116
|
+
_websocket_event_error_type(event_type, payload),
|
|
1117
|
+
)
|
|
1118
|
+
error_type = _websocket_event_error_type(event_type, payload)
|
|
1119
|
+
if error_code in _facade()._WEBSOCKET_AUTH_FAILURE_CODES or error_type == "authentication_error":
|
|
1120
|
+
return "invalid_api_key"
|
|
1121
|
+
return None
|
|
1122
|
+
|
|
1123
|
+
|
|
1124
|
+
def _websocket_auth_failure_requires_reauth(message: str | None) -> bool:
|
|
1125
|
+
if not isinstance(message, str):
|
|
1126
|
+
return False
|
|
1127
|
+
lowered = message.lower()
|
|
1128
|
+
return any(marker in lowered for marker in _facade()._WEBSOCKET_REAUTH_REQUIRED_MESSAGE_MARKERS)
|
|
1129
|
+
|
|
1130
|
+
|
|
1131
|
+
def _websocket_auth_failure_permanent_code(message: str | None) -> str:
|
|
1132
|
+
if _websocket_auth_failure_requires_reauth(message):
|
|
1133
|
+
return _facade()._WEBSOCKET_SESSION_EXPIRED_FAILURE_CODE
|
|
1134
|
+
return _facade()._WEBSOCKET_AUTH_INVALIDATED_FAILURE_CODE
|
|
1135
|
+
|
|
1136
|
+
|
|
1137
|
+
def _websocket_fresh_request_blocks_account_switch(request_state: _WebSocketRequestState) -> bool:
|
|
1138
|
+
try:
|
|
1139
|
+
fresh_payload = json.loads(request_state.fresh_upstream_request_text or "null")
|
|
1140
|
+
except (TypeError, json.JSONDecodeError):
|
|
1141
|
+
return True
|
|
1142
|
+
if not isinstance(fresh_payload, dict):
|
|
1143
|
+
return True
|
|
1144
|
+
fresh_input = fresh_payload.get("input") if isinstance(fresh_payload, dict) else None
|
|
1145
|
+
return bool(extract_input_file_ids(fresh_input))
|
|
1146
|
+
|
|
1147
|
+
|
|
1148
|
+
def _websocket_auth_request_can_switch_account(request_state: _WebSocketRequestState) -> bool:
|
|
1149
|
+
if request_state.file_required_preferred_account:
|
|
1150
|
+
return False
|
|
1151
|
+
if request_state.previous_response_id is None:
|
|
1152
|
+
return request_state.request_text is None or _websocket_request_text_is_account_neutral_fresh_replay(
|
|
1153
|
+
request_state.request_text
|
|
1154
|
+
)
|
|
1155
|
+
if not (
|
|
1156
|
+
request_state.proxy_injected_previous_response_id
|
|
1157
|
+
and request_state.fresh_upstream_request_is_retry_safe
|
|
1158
|
+
and request_state.fresh_upstream_request_text
|
|
1159
|
+
):
|
|
1160
|
+
return False
|
|
1161
|
+
return _websocket_request_text_is_account_neutral_fresh_replay(
|
|
1162
|
+
request_state.fresh_upstream_request_text
|
|
1163
|
+
) and not _websocket_fresh_request_blocks_account_switch(request_state)
|
|
1164
|
+
|
|
1165
|
+
|
|
1166
|
+
def _prepare_websocket_request_state_for_auth_replay(
|
|
1167
|
+
request_state: _WebSocketRequestState,
|
|
1168
|
+
*,
|
|
1169
|
+
current_account_id: str | None = None,
|
|
1170
|
+
) -> str | None:
|
|
1171
|
+
if request_state.verified_stale_anchor_replay:
|
|
1172
|
+
return None
|
|
1173
|
+
if request_state.last_downstream_sequence_number is not None:
|
|
1174
|
+
return None
|
|
1175
|
+
can_switch_account = _websocket_auth_request_can_switch_account(request_state)
|
|
1176
|
+
can_retry_bound_owner = (
|
|
1177
|
+
request_state.auth_replay_count == 0
|
|
1178
|
+
and current_account_id is not None
|
|
1179
|
+
and request_state.replay_required_account_id == current_account_id
|
|
1180
|
+
and isinstance(request_state.request_text, str)
|
|
1181
|
+
)
|
|
1182
|
+
if not can_switch_account and not can_retry_bound_owner:
|
|
1183
|
+
return None
|
|
1184
|
+
if can_switch_account and (
|
|
1185
|
+
request_state.proxy_injected_previous_response_id
|
|
1186
|
+
and request_state.fresh_upstream_request_is_retry_safe
|
|
1187
|
+
and request_state.fresh_upstream_request_text
|
|
1188
|
+
):
|
|
1189
|
+
if _install_verified_fresh_replay(request_state) is None:
|
|
1190
|
+
return None
|
|
1191
|
+
request_text = request_state.request_text
|
|
1192
|
+
if not isinstance(request_text, str):
|
|
1193
|
+
return None
|
|
1194
|
+
request_state.replay_count += 1
|
|
1195
|
+
request_state.auth_replay_count += 1
|
|
1196
|
+
request_state.awaiting_response_created = True
|
|
1197
|
+
request_state.response_id = None
|
|
1198
|
+
request_state.response_event_count = 0
|
|
1199
|
+
_clear_websocket_request_error_overrides(request_state)
|
|
1200
|
+
return request_text
|
|
1201
|
+
|
|
1202
|
+
|
|
1203
|
+
def _websocket_owner_pinned_quota_error_code(
|
|
1204
|
+
request_state: _WebSocketRequestState | None,
|
|
1205
|
+
*,
|
|
1206
|
+
event_type: str | None,
|
|
1207
|
+
payload: dict[str, JsonValue] | None,
|
|
1208
|
+
) -> str | None:
|
|
1209
|
+
if request_state is None:
|
|
1210
|
+
return None
|
|
1211
|
+
if request_state.previous_response_id is None or request_state.preferred_account_id is None:
|
|
1212
|
+
return None
|
|
1213
|
+
if request_state.response_id is not None:
|
|
1214
|
+
return None
|
|
1215
|
+
if not request_state.awaiting_response_created:
|
|
1216
|
+
return None
|
|
1217
|
+
if not request_state.request_text:
|
|
1218
|
+
return None
|
|
1219
|
+
if event_type not in {"error", "response.failed"}:
|
|
1220
|
+
return None
|
|
1221
|
+
|
|
1222
|
+
error_code = _normalize_error_code(
|
|
1223
|
+
_websocket_event_error_code(event_type, payload),
|
|
1224
|
+
_websocket_event_error_type(event_type, payload),
|
|
1225
|
+
)
|
|
1226
|
+
error_message = _websocket_event_error_message(event_type, payload)
|
|
1227
|
+
if is_upstream_model_capacity_error(error_message):
|
|
1228
|
+
if error_code in {
|
|
1229
|
+
"rate_limit_exceeded",
|
|
1230
|
+
"usage_limit_reached",
|
|
1231
|
+
"insufficient_quota",
|
|
1232
|
+
"usage_not_included",
|
|
1233
|
+
"quota_exceeded",
|
|
1234
|
+
}:
|
|
1235
|
+
return error_code
|
|
1236
|
+
if _websocket_response_id(None, payload) is not None:
|
|
1237
|
+
return None
|
|
1238
|
+
return "server_is_overloaded"
|
|
1239
|
+
return _websocket_transparent_replay_error_code(error_code, error_message)
|
|
1240
|
+
|
|
1241
|
+
|
|
1242
|
+
def _websocket_transparent_replay_error_code(error_code: str, error_message: str | None) -> str | None:
|
|
1243
|
+
"""The code this terminal frame may be replayed under, or ``None`` to surface it.
|
|
1244
|
+
|
|
1245
|
+
The code set cannot answer for the serialized usage-limit rejection, which
|
|
1246
|
+
upstream sends with no error code at all: that frame normalizes to
|
|
1247
|
+
``upstream_error``, which the set does not contain, so the turn is surfaced
|
|
1248
|
+
on a spent account while the identical coded frame is replayed elsewhere.
|
|
1249
|
+
A frame whose sentence proves the usage limit answers under the usage-limit
|
|
1250
|
+
code, so every later question -- whether an owner-pinned turn may move, what
|
|
1251
|
+
the account's health write records -- gets the same answer for both forms
|
|
1252
|
+
upstream chooses between.
|
|
1253
|
+
"""
|
|
1254
|
+
if error_code in _facade()._WEBSOCKET_TRANSPARENT_REPLAY_ERROR_CODES:
|
|
1255
|
+
return error_code
|
|
1256
|
+
if is_upstream_usage_limit_rejection(error_code=error_code, message=error_message):
|
|
1257
|
+
return USAGE_LIMIT_REACHED
|
|
1258
|
+
return None
|
|
1259
|
+
|
|
1260
|
+
|
|
1261
|
+
async def _pop_replayable_precreated_websocket_request_state(
|
|
1262
|
+
pending_requests: deque[_WebSocketRequestState],
|
|
1263
|
+
*,
|
|
1264
|
+
pending_lock: anyio.Lock,
|
|
1265
|
+
replay_refusal_reasons: list[str] | None = None,
|
|
1266
|
+
) -> _WebSocketRequestState | None:
|
|
1267
|
+
async with pending_lock:
|
|
1268
|
+
pending_count = len(pending_requests)
|
|
1269
|
+
if pending_count != 1:
|
|
1270
|
+
if replay_refusal_reasons is not None:
|
|
1271
|
+
replay_refusal_reasons.append(
|
|
1272
|
+
"multiple_pending_requests" if pending_count > 1 else "no_pending_requests"
|
|
1273
|
+
)
|
|
1274
|
+
if any(request_state.last_downstream_sequence_number is not None for request_state in pending_requests):
|
|
1275
|
+
replay_refusal_reasons.append("sequenced_downstream_frame")
|
|
1276
|
+
return None
|
|
1277
|
+
request_state = pending_requests[0]
|
|
1278
|
+
if not _websocket_request_can_replay_before_visible_output(request_state):
|
|
1279
|
+
if replay_refusal_reasons is not None and request_state.last_downstream_sequence_number is not None:
|
|
1280
|
+
replay_refusal_reasons.append("sequenced_downstream_frame")
|
|
1281
|
+
return None
|
|
1282
|
+
pending_requests.popleft()
|
|
1283
|
+
if _prepare_websocket_request_state_for_visible_output_replay(request_state) is None:
|
|
1284
|
+
return None
|
|
1285
|
+
return request_state
|
|
1286
|
+
|
|
1287
|
+
|
|
1288
|
+
async def _websocket_full_resend_conflicts_with_visible_pending(
|
|
1289
|
+
request_state: _WebSocketRequestState,
|
|
1290
|
+
pending_requests: deque[_WebSocketRequestState],
|
|
1291
|
+
*,
|
|
1292
|
+
pending_lock: anyio.Lock,
|
|
1293
|
+
codex_session_affinity: bool,
|
|
1294
|
+
) -> bool:
|
|
1295
|
+
if (
|
|
1296
|
+
not codex_session_affinity
|
|
1297
|
+
or request_state.previous_response_id is not None
|
|
1298
|
+
or request_state.input_item_count < _WEBSOCKET_FULL_REPLAY_WAIT_MIN_ITEMS
|
|
1299
|
+
):
|
|
1300
|
+
return False
|
|
1301
|
+
async with pending_lock:
|
|
1302
|
+
return any(pending is not request_state and pending.downstream_visible for pending in pending_requests)
|
|
1303
|
+
|
|
1304
|
+
|
|
1305
|
+
def _refresh_websocket_request_input_fingerprint_from_text(request_state: _WebSocketRequestState) -> None:
|
|
1306
|
+
if not request_state.request_text:
|
|
1307
|
+
request_state.input_item_count = 0
|
|
1308
|
+
request_state.input_full_fingerprint = None
|
|
1309
|
+
return
|
|
1310
|
+
try:
|
|
1311
|
+
payload = json.loads(request_state.request_text)
|
|
1312
|
+
except json.JSONDecodeError:
|
|
1313
|
+
request_state.input_item_count = 0
|
|
1314
|
+
request_state.input_full_fingerprint = None
|
|
1315
|
+
return
|
|
1316
|
+
if not isinstance(payload, dict):
|
|
1317
|
+
request_state.input_item_count = 0
|
|
1318
|
+
request_state.input_full_fingerprint = None
|
|
1319
|
+
return
|
|
1320
|
+
input_items = payload.get("input")
|
|
1321
|
+
if not isinstance(input_items, list):
|
|
1322
|
+
request_state.input_item_count = 0
|
|
1323
|
+
request_state.input_full_fingerprint = None
|
|
1324
|
+
return
|
|
1325
|
+
request_state.input_item_count = len(input_items)
|
|
1326
|
+
request_state.input_full_fingerprint = _facade()._fingerprint_input_items(cast(list[JsonValue], input_items))
|
|
1327
|
+
|
|
1328
|
+
|
|
1329
|
+
def _websocket_top_level_error_payload(payload: dict[str, JsonValue]) -> dict[str, JsonValue] | None:
|
|
1330
|
+
if payload.get("type") != "error":
|
|
1331
|
+
return None
|
|
1332
|
+
error: dict[str, JsonValue] = {}
|
|
1333
|
+
for error_field in ("code", "message"):
|
|
1334
|
+
value = payload.get(error_field)
|
|
1335
|
+
if isinstance(value, str) and value.strip():
|
|
1336
|
+
error[error_field] = value.strip()
|
|
1337
|
+
if "param" in payload:
|
|
1338
|
+
error["param"] = payload["param"]
|
|
1339
|
+
error_type = payload.get("error_type")
|
|
1340
|
+
if isinstance(error_type, str) and error_type.strip():
|
|
1341
|
+
error["type"] = error_type.strip()
|
|
1342
|
+
for metadata_field in ("plan_type", "resets_at", "resets_in_seconds"):
|
|
1343
|
+
value = payload.get(metadata_field)
|
|
1344
|
+
if isinstance(value, str | int | float) and not isinstance(value, bool):
|
|
1345
|
+
error[metadata_field] = value
|
|
1346
|
+
return error or None
|
|
1347
|
+
|
|
1348
|
+
|
|
1349
|
+
def _websocket_event_error_payload(
|
|
1350
|
+
event_type: str | None,
|
|
1351
|
+
payload: dict[str, JsonValue] | None,
|
|
1352
|
+
) -> dict[str, JsonValue] | None:
|
|
1353
|
+
if not isinstance(payload, dict):
|
|
1354
|
+
return None
|
|
1355
|
+
if event_type == "error":
|
|
1356
|
+
error = payload.get("error")
|
|
1357
|
+
if isinstance(error, dict):
|
|
1358
|
+
return error
|
|
1359
|
+
# The ChatGPT-backed Codex websocket can emit OpenAI-style error
|
|
1360
|
+
# details directly on the event frame instead of under an ``error``
|
|
1361
|
+
# envelope. Normalize those fields into an error-detail object so the
|
|
1362
|
+
# event discriminator ``type: error`` is not mistaken for the upstream
|
|
1363
|
+
# error type.
|
|
1364
|
+
return _websocket_top_level_error_payload(payload)
|
|
1365
|
+
if event_type == "response.failed":
|
|
1366
|
+
response = payload.get("response")
|
|
1367
|
+
error = response.get("error") if isinstance(response, dict) else None
|
|
1368
|
+
return error if isinstance(error, dict) else None
|
|
1369
|
+
return None
|
|
1370
|
+
|
|
1371
|
+
|
|
1372
|
+
def _sanitize_public_websocket_event_payload(
|
|
1373
|
+
payload: dict[str, JsonValue],
|
|
1374
|
+
*,
|
|
1375
|
+
event_type: str | None,
|
|
1376
|
+
) -> dict[str, JsonValue]:
|
|
1377
|
+
"""Copy a terminal event with only a client-safe ``error.param``."""
|
|
1378
|
+
|
|
1379
|
+
normalized = payload
|
|
1380
|
+
if event_type in {"error", "response.failed"}:
|
|
1381
|
+
error_value = payload.get("error")
|
|
1382
|
+
if isinstance(error_value, dict):
|
|
1383
|
+
sanitized_error = sanitize_public_error_detail(error_value)
|
|
1384
|
+
if sanitized_error != error_value:
|
|
1385
|
+
normalized = dict(normalized)
|
|
1386
|
+
normalized["error"] = sanitized_error
|
|
1387
|
+
if "param" in payload:
|
|
1388
|
+
sanitized = dict(normalized)
|
|
1389
|
+
public_param = normalize_public_error_param(OpenAIErrorParam(True, payload["param"]))
|
|
1390
|
+
if public_param is None:
|
|
1391
|
+
sanitized.pop("param", None)
|
|
1392
|
+
else:
|
|
1393
|
+
sanitized["param"] = public_param
|
|
1394
|
+
if sanitized != normalized:
|
|
1395
|
+
normalized = sanitized
|
|
1396
|
+
|
|
1397
|
+
response_value = payload.get("response")
|
|
1398
|
+
if isinstance(response_value, dict):
|
|
1399
|
+
error_value = response_value.get("error")
|
|
1400
|
+
if isinstance(error_value, dict):
|
|
1401
|
+
sanitized_error = sanitize_public_error_detail(error_value)
|
|
1402
|
+
if sanitized_error != error_value:
|
|
1403
|
+
normalized_response = dict(response_value)
|
|
1404
|
+
normalized_response["error"] = sanitized_error
|
|
1405
|
+
normalized = dict(normalized)
|
|
1406
|
+
normalized["response"] = normalized_response
|
|
1407
|
+
return normalized
|
|
1408
|
+
|
|
1409
|
+
|
|
1410
|
+
def _websocket_event_incomplete_reason(
|
|
1411
|
+
event_type: str | None,
|
|
1412
|
+
payload: dict[str, JsonValue] | None,
|
|
1413
|
+
) -> str | None:
|
|
1414
|
+
if event_type != "response.incomplete" or not isinstance(payload, dict):
|
|
1415
|
+
return None
|
|
1416
|
+
response = payload.get("response")
|
|
1417
|
+
if not isinstance(response, dict):
|
|
1418
|
+
return None
|
|
1419
|
+
incomplete_details = response.get("incomplete_details")
|
|
1420
|
+
if not isinstance(incomplete_details, dict):
|
|
1421
|
+
return None
|
|
1422
|
+
reason = incomplete_details.get("reason")
|
|
1423
|
+
if not isinstance(reason, str):
|
|
1424
|
+
return None
|
|
1425
|
+
stripped = reason.strip()
|
|
1426
|
+
return stripped or None
|
|
1427
|
+
|
|
1428
|
+
|
|
1429
|
+
def _maybe_rewrite_websocket_previous_response_not_found_event(
|
|
1430
|
+
*,
|
|
1431
|
+
request_state: _WebSocketRequestState,
|
|
1432
|
+
event: OpenAIEvent | None,
|
|
1433
|
+
payload: dict[str, JsonValue] | None,
|
|
1434
|
+
event_type: str | None,
|
|
1435
|
+
upstream_control: _WebSocketUpstreamControl,
|
|
1436
|
+
original_text: str,
|
|
1437
|
+
) -> tuple[OpenAIEvent | None, dict[str, JsonValue] | None, str | None, str]:
|
|
1438
|
+
error_code = _normalize_error_code(
|
|
1439
|
+
_websocket_event_error_code(event_type, payload),
|
|
1440
|
+
_websocket_event_error_type(event_type, payload),
|
|
1441
|
+
)
|
|
1442
|
+
error_param = _websocket_event_error_param(event_type, payload)
|
|
1443
|
+
error_message = _websocket_event_error_message(event_type, payload)
|
|
1444
|
+
should_rewrite = _facade()._is_previous_response_not_found_error(
|
|
1445
|
+
code=error_code,
|
|
1446
|
+
param=error_param,
|
|
1447
|
+
message=error_message,
|
|
1448
|
+
)
|
|
1449
|
+
reason = "previous_response_not_found"
|
|
1450
|
+
if not should_rewrite and _facade()._is_previous_response_not_found_public_shape(
|
|
1451
|
+
code=error_code,
|
|
1452
|
+
param=error_param,
|
|
1453
|
+
message=error_message,
|
|
1454
|
+
):
|
|
1455
|
+
# Mask malformed stale-anchor metadata without allowing replay.
|
|
1456
|
+
should_rewrite = True
|
|
1457
|
+
reason = PREVIOUS_RESPONSE_MALFORMED_PARAM_REASON
|
|
1458
|
+
request_state.previous_response_not_found_recovery_blocked = True
|
|
1459
|
+
if not should_rewrite:
|
|
1460
|
+
if request_state.previous_response_id is None:
|
|
1461
|
+
return event, payload, event_type, original_text
|
|
1462
|
+
should_rewrite = _facade()._is_missing_tool_output_error(
|
|
1463
|
+
code=error_code,
|
|
1464
|
+
param=error_param,
|
|
1465
|
+
message=error_message,
|
|
1466
|
+
)
|
|
1467
|
+
reason = "missing_tool_output"
|
|
1468
|
+
if not should_rewrite:
|
|
1469
|
+
return event, payload, event_type, original_text
|
|
1470
|
+
|
|
1471
|
+
reconnect_requested = reason != PREVIOUS_RESPONSE_MALFORMED_PARAM_REASON and (
|
|
1472
|
+
reason == "missing_tool_output" or request_state.preferred_account_id is not None
|
|
1473
|
+
)
|
|
1474
|
+
return _rewrite_websocket_continuity_corruption_event(
|
|
1475
|
+
request_state=request_state,
|
|
1476
|
+
upstream_control=upstream_control,
|
|
1477
|
+
reason=reason,
|
|
1478
|
+
reconnect_requested=reconnect_requested,
|
|
1479
|
+
original_text=original_text,
|
|
1480
|
+
)
|
|
1481
|
+
|
|
1482
|
+
|
|
1483
|
+
def _websocket_continuity_error_fields(
|
|
1484
|
+
*,
|
|
1485
|
+
reason: str,
|
|
1486
|
+
expose_stale_previous_response_classifier: bool,
|
|
1487
|
+
) -> tuple[str, str]:
|
|
1488
|
+
if reason == "previous_response_not_found" and expose_stale_previous_response_classifier:
|
|
1489
|
+
return PREVIOUS_RESPONSE_NOT_FOUND_CODE, PREVIOUS_RESPONSE_NOT_FOUND_MESSAGE
|
|
1490
|
+
return "stream_incomplete", PREVIOUS_RESPONSE_STREAM_INCOMPLETE_MESSAGE
|
|
1491
|
+
|
|
1492
|
+
|
|
1493
|
+
@dataclass(frozen=True, slots=True)
|
|
1494
|
+
class _WebSocketStaleAnchorDiagnostics:
|
|
1495
|
+
previous_response_source: str
|
|
1496
|
+
fresh_replay_available: bool
|
|
1497
|
+
owner_lookup_source: str | None
|
|
1498
|
+
owner_lookup_outcome: str | None
|
|
1499
|
+
previous_response_age_seconds: int | None
|
|
1500
|
+
same_session: bool | None
|
|
1501
|
+
|
|
1502
|
+
|
|
1503
|
+
def _websocket_previous_response_source(request_state: _WebSocketRequestState) -> str:
|
|
1504
|
+
if request_state.previous_response_id is None:
|
|
1505
|
+
return "unknown"
|
|
1506
|
+
if request_state.proxy_injected_previous_response_id:
|
|
1507
|
+
return "proxy_injected"
|
|
1508
|
+
return "client_supplied"
|
|
1509
|
+
|
|
1510
|
+
|
|
1511
|
+
def _websocket_same_session_for_previous_response(
|
|
1512
|
+
request_state: _WebSocketRequestState,
|
|
1513
|
+
) -> bool | None:
|
|
1514
|
+
request_session_id = request_state.session_id.strip() if isinstance(request_state.session_id, str) else ""
|
|
1515
|
+
owner_session_id = (
|
|
1516
|
+
request_state.previous_response_owner_session_id.strip()
|
|
1517
|
+
if isinstance(request_state.previous_response_owner_session_id, str)
|
|
1518
|
+
else ""
|
|
1519
|
+
)
|
|
1520
|
+
if not request_session_id or not owner_session_id:
|
|
1521
|
+
return None
|
|
1522
|
+
return request_session_id == owner_session_id
|
|
1523
|
+
|
|
1524
|
+
|
|
1525
|
+
def _websocket_previous_response_age_seconds(request_state: _WebSocketRequestState) -> int | None:
|
|
1526
|
+
requested_at = request_state.previous_response_owner_requested_at
|
|
1527
|
+
if requested_at is None:
|
|
1528
|
+
return None
|
|
1529
|
+
return max(0, int((utcnow() - to_utc_naive(requested_at)).total_seconds()))
|
|
1530
|
+
|
|
1531
|
+
|
|
1532
|
+
def _websocket_stale_anchor_diagnostics(
|
|
1533
|
+
request_state: _WebSocketRequestState,
|
|
1534
|
+
) -> _WebSocketStaleAnchorDiagnostics:
|
|
1535
|
+
return _WebSocketStaleAnchorDiagnostics(
|
|
1536
|
+
previous_response_source=_websocket_previous_response_source(request_state),
|
|
1537
|
+
fresh_replay_available=bool(
|
|
1538
|
+
request_state.fresh_upstream_request_is_retry_safe and request_state.fresh_upstream_request_text is not None
|
|
1539
|
+
),
|
|
1540
|
+
owner_lookup_source=request_state.previous_response_owner_lookup_source,
|
|
1541
|
+
owner_lookup_outcome=request_state.previous_response_owner_lookup_outcome,
|
|
1542
|
+
previous_response_age_seconds=_websocket_previous_response_age_seconds(request_state),
|
|
1543
|
+
same_session=_websocket_same_session_for_previous_response(request_state),
|
|
1544
|
+
)
|
|
1545
|
+
|
|
1546
|
+
|
|
1547
|
+
def _websocket_stale_anchor_failure_detail(
|
|
1548
|
+
diagnostics: _WebSocketStaleAnchorDiagnostics,
|
|
1549
|
+
) -> str:
|
|
1550
|
+
owner_lookup_source = diagnostics.owner_lookup_source or "unknown"
|
|
1551
|
+
owner_lookup_outcome = diagnostics.owner_lookup_outcome or "unknown"
|
|
1552
|
+
previous_response_age_seconds = (
|
|
1553
|
+
str(diagnostics.previous_response_age_seconds)
|
|
1554
|
+
if diagnostics.previous_response_age_seconds is not None
|
|
1555
|
+
else "unknown"
|
|
1556
|
+
)
|
|
1557
|
+
same_session = str(diagnostics.same_session).lower() if diagnostics.same_session is not None else "unknown"
|
|
1558
|
+
return (
|
|
1559
|
+
"previous_response_not_found "
|
|
1560
|
+
f"previous_response_source={diagnostics.previous_response_source} "
|
|
1561
|
+
f"fresh_replay_available={str(diagnostics.fresh_replay_available).lower()} "
|
|
1562
|
+
f"owner_lookup_source={owner_lookup_source} "
|
|
1563
|
+
f"owner_lookup_outcome={owner_lookup_outcome} "
|
|
1564
|
+
f"previous_response_age_seconds={previous_response_age_seconds} "
|
|
1565
|
+
f"same_session={same_session}"
|
|
1566
|
+
)
|
|
1567
|
+
|
|
1568
|
+
|
|
1569
|
+
def _record_websocket_stale_anchor_failure(
|
|
1570
|
+
request_state: _WebSocketRequestState,
|
|
1571
|
+
*,
|
|
1572
|
+
surface: str,
|
|
1573
|
+
upstream_error_code: str,
|
|
1574
|
+
) -> None:
|
|
1575
|
+
diagnostics = _websocket_stale_anchor_diagnostics(request_state)
|
|
1576
|
+
request_state.failure_phase_override = "upstream"
|
|
1577
|
+
request_state.failure_detail_override = _websocket_stale_anchor_failure_detail(diagnostics)
|
|
1578
|
+
request_state.upstream_error_code_override = upstream_error_code
|
|
1579
|
+
if not diagnostics.fresh_replay_available:
|
|
1580
|
+
_remember_websocket_stale_previous_response(
|
|
1581
|
+
previous_response_id=request_state.previous_response_id,
|
|
1582
|
+
api_key_id=request_state.api_key.id if request_state.api_key is not None else None,
|
|
1583
|
+
)
|
|
1584
|
+
_record_continuity_fail_closed(
|
|
1585
|
+
surface=surface,
|
|
1586
|
+
reason="previous_response_not_found",
|
|
1587
|
+
previous_response_id=request_state.previous_response_id,
|
|
1588
|
+
session_id=request_state.session_id,
|
|
1589
|
+
upstream_error_code=upstream_error_code,
|
|
1590
|
+
previous_response_source=diagnostics.previous_response_source,
|
|
1591
|
+
fresh_replay_available=diagnostics.fresh_replay_available,
|
|
1592
|
+
owner_lookup_source=diagnostics.owner_lookup_source,
|
|
1593
|
+
owner_lookup_outcome=diagnostics.owner_lookup_outcome,
|
|
1594
|
+
previous_response_age_seconds=diagnostics.previous_response_age_seconds,
|
|
1595
|
+
same_session=diagnostics.same_session,
|
|
1596
|
+
)
|
|
1597
|
+
|
|
1598
|
+
|
|
1599
|
+
def _rewrite_websocket_continuity_corruption_event(
|
|
1600
|
+
*,
|
|
1601
|
+
request_state: _WebSocketRequestState,
|
|
1602
|
+
upstream_control: _WebSocketUpstreamControl,
|
|
1603
|
+
reason: str,
|
|
1604
|
+
reconnect_requested: bool,
|
|
1605
|
+
original_text: str,
|
|
1606
|
+
) -> tuple[OpenAIEvent | None, dict[str, JsonValue] | None, str | None, str]:
|
|
1607
|
+
del original_text
|
|
1608
|
+
if reconnect_requested:
|
|
1609
|
+
upstream_control.reconnect_requested = True
|
|
1610
|
+
if reason == "previous_response_not_found":
|
|
1611
|
+
_record_websocket_stale_anchor_failure(
|
|
1612
|
+
request_state,
|
|
1613
|
+
surface="websocket_stream",
|
|
1614
|
+
upstream_error_code="previous_response_not_found",
|
|
1615
|
+
)
|
|
1616
|
+
else:
|
|
1617
|
+
_record_continuity_fail_closed(
|
|
1618
|
+
surface="websocket_stream",
|
|
1619
|
+
reason=reason,
|
|
1620
|
+
previous_response_id=request_state.previous_response_id,
|
|
1621
|
+
session_id=request_state.session_id,
|
|
1622
|
+
upstream_error_code=(
|
|
1623
|
+
PREVIOUS_RESPONSE_NOT_FOUND_CODE if reason == PREVIOUS_RESPONSE_MALFORMED_PARAM_REASON else None
|
|
1624
|
+
),
|
|
1625
|
+
)
|
|
1626
|
+
rewritten_code, rewritten_message = _websocket_continuity_error_fields(
|
|
1627
|
+
reason=reason,
|
|
1628
|
+
expose_stale_previous_response_classifier=request_state.expose_stale_previous_response_classifier,
|
|
1629
|
+
)
|
|
1630
|
+
rewritten_event_payload = response_failed_event(
|
|
1631
|
+
rewritten_code,
|
|
1632
|
+
rewritten_message,
|
|
1633
|
+
error_type="server_error",
|
|
1634
|
+
response_id=_websocket_downstream_response_id(request_state),
|
|
1635
|
+
)
|
|
1636
|
+
rewritten_text = json.dumps(rewritten_event_payload, ensure_ascii=True, separators=(",", ":"))
|
|
1637
|
+
rewritten_event_block = format_sse_event(rewritten_event_payload)
|
|
1638
|
+
rewritten_payload = parse_sse_data_json(rewritten_event_block)
|
|
1639
|
+
rewritten_event = parse_sse_event(rewritten_event_block)
|
|
1640
|
+
rewritten_event_type = _event_type_from_payload(rewritten_event, rewritten_payload)
|
|
1641
|
+
return rewritten_event, rewritten_payload, rewritten_event_type, rewritten_text
|
|
1642
|
+
|
|
1643
|
+
|
|
1644
|
+
def _rewrite_websocket_previous_response_owner_unavailable_event(
|
|
1645
|
+
*,
|
|
1646
|
+
request_state: _WebSocketRequestState,
|
|
1647
|
+
) -> tuple[OpenAIEvent | None, dict[str, JsonValue] | None, str | None, str]:
|
|
1648
|
+
_record_continuity_fail_closed(
|
|
1649
|
+
surface="websocket_stream",
|
|
1650
|
+
reason="owner_account_unavailable",
|
|
1651
|
+
previous_response_id=request_state.previous_response_id,
|
|
1652
|
+
session_id=request_state.session_id,
|
|
1653
|
+
)
|
|
1654
|
+
rewritten_event_payload = response_failed_event(
|
|
1655
|
+
"upstream_unavailable",
|
|
1656
|
+
"Previous response owner account is unavailable; retry later.",
|
|
1657
|
+
error_type="server_error",
|
|
1658
|
+
response_id=_websocket_downstream_response_id(request_state),
|
|
1659
|
+
)
|
|
1660
|
+
rewritten_text = json.dumps(rewritten_event_payload, ensure_ascii=True, separators=(",", ":"))
|
|
1661
|
+
rewritten_event_block = format_sse_event(rewritten_event_payload)
|
|
1662
|
+
rewritten_payload = parse_sse_data_json(rewritten_event_block)
|
|
1663
|
+
rewritten_event = parse_sse_event(rewritten_event_block)
|
|
1664
|
+
rewritten_event_type = _event_type_from_payload(rewritten_event, rewritten_payload)
|
|
1665
|
+
return rewritten_event, rewritten_payload, rewritten_event_type, rewritten_text
|
|
1666
|
+
|
|
1667
|
+
|
|
1668
|
+
def _rewrite_websocket_suppressed_duplicate_tool_call_completion_event(
|
|
1669
|
+
*,
|
|
1670
|
+
request_state: _WebSocketRequestState,
|
|
1671
|
+
) -> tuple[OpenAIEvent | None, dict[str, JsonValue] | None, str | None, str]:
|
|
1672
|
+
rewritten_event_payload = response_failed_event(
|
|
1673
|
+
_facade()._SUPPRESSED_DUPLICATE_TOOL_CALL_ERROR_CODE,
|
|
1674
|
+
_facade()._SUPPRESSED_DUPLICATE_TOOL_CALL_MESSAGE,
|
|
1675
|
+
error_type="server_error",
|
|
1676
|
+
response_id=_websocket_downstream_response_id(request_state),
|
|
1677
|
+
)
|
|
1678
|
+
rewritten_text = json.dumps(rewritten_event_payload, ensure_ascii=True, separators=(",", ":"))
|
|
1679
|
+
rewritten_event_block = format_sse_event(rewritten_event_payload)
|
|
1680
|
+
rewritten_payload = parse_sse_data_json(rewritten_event_block)
|
|
1681
|
+
rewritten_event = parse_sse_event(rewritten_event_block)
|
|
1682
|
+
rewritten_event_type = _event_type_from_payload(rewritten_event, rewritten_payload)
|
|
1683
|
+
return rewritten_event, rewritten_payload, rewritten_event_type, rewritten_text
|
|
1684
|
+
|
|
1685
|
+
|
|
1686
|
+
def _sanitize_websocket_connect_failure(
|
|
1687
|
+
*,
|
|
1688
|
+
request_state: _WebSocketRequestState,
|
|
1689
|
+
status_code: int,
|
|
1690
|
+
payload: OpenAIErrorEnvelope,
|
|
1691
|
+
error_code: str,
|
|
1692
|
+
error_message: str,
|
|
1693
|
+
) -> tuple[int, OpenAIErrorEnvelope, str, str]:
|
|
1694
|
+
return _sanitize_websocket_previous_response_error(
|
|
1695
|
+
previous_response_id=request_state.previous_response_id,
|
|
1696
|
+
session_id=request_state.session_id,
|
|
1697
|
+
status_code=status_code,
|
|
1698
|
+
payload=payload,
|
|
1699
|
+
error_code=error_code,
|
|
1700
|
+
error_message=error_message,
|
|
1701
|
+
surface="websocket_connect",
|
|
1702
|
+
expose_stale_previous_response_classifier=request_state.expose_stale_previous_response_classifier,
|
|
1703
|
+
request_state=request_state,
|
|
1704
|
+
)
|
|
1705
|
+
|
|
1706
|
+
|
|
1707
|
+
def _sanitize_websocket_previous_response_error(
|
|
1708
|
+
*,
|
|
1709
|
+
previous_response_id: str | None,
|
|
1710
|
+
session_id: str | None,
|
|
1711
|
+
status_code: int,
|
|
1712
|
+
payload: OpenAIErrorEnvelope,
|
|
1713
|
+
error_code: str,
|
|
1714
|
+
error_message: str,
|
|
1715
|
+
surface: str,
|
|
1716
|
+
expose_stale_previous_response_classifier: bool = False,
|
|
1717
|
+
request_state: _WebSocketRequestState | None = None,
|
|
1718
|
+
) -> tuple[int, OpenAIErrorEnvelope, str, str]:
|
|
1719
|
+
if previous_response_id is None:
|
|
1720
|
+
return status_code, payload, error_code, error_message
|
|
1721
|
+
parsed_error = _parse_openai_error(payload)
|
|
1722
|
+
normalized_code = _normalize_error_code(
|
|
1723
|
+
parsed_error.code if parsed_error else error_code,
|
|
1724
|
+
parsed_error.type if parsed_error else None,
|
|
1725
|
+
)
|
|
1726
|
+
normalized_message = parsed_error.message if parsed_error and parsed_error.message else error_message
|
|
1727
|
+
reason = "previous_response_not_found"
|
|
1728
|
+
should_rewrite = _facade()._is_previous_response_not_found_error(
|
|
1729
|
+
code=normalized_code,
|
|
1730
|
+
param=parsed_error.param_state if parsed_error else OpenAIErrorParam.absent(),
|
|
1731
|
+
message=normalized_message,
|
|
1732
|
+
)
|
|
1733
|
+
if not should_rewrite:
|
|
1734
|
+
should_rewrite = _facade()._is_missing_tool_output_error(
|
|
1735
|
+
code=normalized_code,
|
|
1736
|
+
param=parsed_error.param_state if parsed_error else OpenAIErrorParam.absent(),
|
|
1737
|
+
message=normalized_message,
|
|
1738
|
+
)
|
|
1739
|
+
reason = "missing_tool_output"
|
|
1740
|
+
if not should_rewrite and _facade()._is_previous_response_not_found_public_shape(
|
|
1741
|
+
code=normalized_code,
|
|
1742
|
+
param=parsed_error.param_state if parsed_error else OpenAIErrorParam.absent(),
|
|
1743
|
+
message=normalized_message,
|
|
1744
|
+
):
|
|
1745
|
+
should_rewrite = True
|
|
1746
|
+
reason = PREVIOUS_RESPONSE_MALFORMED_PARAM_REASON
|
|
1747
|
+
if request_state is not None:
|
|
1748
|
+
request_state.previous_response_not_found_recovery_blocked = True
|
|
1749
|
+
if not should_rewrite:
|
|
1750
|
+
return status_code, payload, error_code, error_message
|
|
1751
|
+
|
|
1752
|
+
rewritten_code, rewritten_message = _websocket_continuity_error_fields(
|
|
1753
|
+
reason=reason,
|
|
1754
|
+
expose_stale_previous_response_classifier=expose_stale_previous_response_classifier,
|
|
1755
|
+
)
|
|
1756
|
+
if request_state is not None and reason == "previous_response_not_found":
|
|
1757
|
+
_record_websocket_stale_anchor_failure(
|
|
1758
|
+
request_state,
|
|
1759
|
+
surface=surface,
|
|
1760
|
+
upstream_error_code=normalized_code,
|
|
1761
|
+
)
|
|
1762
|
+
else:
|
|
1763
|
+
_record_continuity_fail_closed(
|
|
1764
|
+
surface=surface,
|
|
1765
|
+
reason=reason,
|
|
1766
|
+
previous_response_id=previous_response_id,
|
|
1767
|
+
session_id=session_id,
|
|
1768
|
+
upstream_error_code=normalized_code,
|
|
1769
|
+
)
|
|
1770
|
+
return (
|
|
1771
|
+
502,
|
|
1772
|
+
openai_error(
|
|
1773
|
+
rewritten_code,
|
|
1774
|
+
rewritten_message,
|
|
1775
|
+
error_type="server_error",
|
|
1776
|
+
),
|
|
1777
|
+
rewritten_code,
|
|
1778
|
+
rewritten_message,
|
|
1779
|
+
)
|
|
1780
|
+
|
|
1781
|
+
|
|
1782
|
+
def _sanitize_websocket_terminal_error_fields(
|
|
1783
|
+
*,
|
|
1784
|
+
request_state: _WebSocketRequestState,
|
|
1785
|
+
error_code: str,
|
|
1786
|
+
error_message: str,
|
|
1787
|
+
error_type: str,
|
|
1788
|
+
error_param: OpenAIErrorParam | JsonValue | None,
|
|
1789
|
+
) -> tuple[str, str, str, OpenAIErrorParam | None]:
|
|
1790
|
+
error_param_state = coerce_error_param(error_param)
|
|
1791
|
+
if request_state.websocket_terminal_error_fields_sanitized:
|
|
1792
|
+
return (
|
|
1793
|
+
error_code,
|
|
1794
|
+
error_message,
|
|
1795
|
+
error_type,
|
|
1796
|
+
error_param_state if error_param_state.present else None,
|
|
1797
|
+
)
|
|
1798
|
+
normalized_code = _normalize_error_code(error_code, error_type)
|
|
1799
|
+
recoverable = _facade()._is_previous_response_not_found_error(
|
|
1800
|
+
code=normalized_code,
|
|
1801
|
+
param=error_param_state,
|
|
1802
|
+
message=error_message,
|
|
1803
|
+
)
|
|
1804
|
+
if not recoverable and not _facade()._is_previous_response_not_found_public_shape(
|
|
1805
|
+
code=normalized_code,
|
|
1806
|
+
param=error_param_state,
|
|
1807
|
+
message=error_message,
|
|
1808
|
+
):
|
|
1809
|
+
return error_code, error_message, error_type, error_param_state
|
|
1810
|
+
if recoverable:
|
|
1811
|
+
_record_websocket_stale_anchor_failure(
|
|
1812
|
+
request_state,
|
|
1813
|
+
surface="websocket_terminal",
|
|
1814
|
+
upstream_error_code=normalized_code,
|
|
1815
|
+
)
|
|
1816
|
+
else:
|
|
1817
|
+
request_state.previous_response_not_found_recovery_blocked = True
|
|
1818
|
+
_record_continuity_fail_closed(
|
|
1819
|
+
surface="websocket_terminal",
|
|
1820
|
+
reason=PREVIOUS_RESPONSE_MALFORMED_PARAM_REASON,
|
|
1821
|
+
previous_response_id=request_state.previous_response_id,
|
|
1822
|
+
session_id=request_state.session_id,
|
|
1823
|
+
upstream_error_code=normalized_code,
|
|
1824
|
+
)
|
|
1825
|
+
rewritten_code, rewritten_message = _websocket_continuity_error_fields(
|
|
1826
|
+
reason="previous_response_not_found" if recoverable else PREVIOUS_RESPONSE_MALFORMED_PARAM_REASON,
|
|
1827
|
+
expose_stale_previous_response_classifier=request_state.expose_stale_previous_response_classifier,
|
|
1828
|
+
)
|
|
1829
|
+
request_state.websocket_terminal_error_fields_sanitized = True
|
|
1830
|
+
return (
|
|
1831
|
+
rewritten_code,
|
|
1832
|
+
rewritten_message,
|
|
1833
|
+
"server_error",
|
|
1834
|
+
None,
|
|
1835
|
+
)
|
|
1836
|
+
|
|
1837
|
+
|
|
1838
|
+
def _find_websocket_request_state_by_response_id(
|
|
1839
|
+
pending_requests: deque[_WebSocketRequestState],
|
|
1840
|
+
response_id: str,
|
|
1841
|
+
) -> _WebSocketRequestState | None:
|
|
1842
|
+
for request_state in pending_requests:
|
|
1843
|
+
if request_state.response_id == response_id:
|
|
1844
|
+
return request_state
|
|
1845
|
+
return None
|
|
1846
|
+
|
|
1847
|
+
|
|
1848
|
+
def _assign_websocket_response_id(
|
|
1849
|
+
pending_requests: deque[_WebSocketRequestState],
|
|
1850
|
+
response_id: str | None,
|
|
1851
|
+
) -> _WebSocketRequestState | None:
|
|
1852
|
+
if response_id is None:
|
|
1853
|
+
return None
|
|
1854
|
+
existing = _find_websocket_request_state_by_response_id(pending_requests, response_id)
|
|
1855
|
+
if existing is not None:
|
|
1856
|
+
return existing
|
|
1857
|
+
for request_state in pending_requests:
|
|
1858
|
+
if request_state.response_id is None and _http_bridge_request_counts_against_queue(request_state):
|
|
1859
|
+
request_state.response_id = response_id
|
|
1860
|
+
return request_state
|
|
1861
|
+
for request_state in pending_requests:
|
|
1862
|
+
if request_state.response_id is None and request_state.draining_until_terminal:
|
|
1863
|
+
request_state.response_id = response_id
|
|
1864
|
+
return request_state
|
|
1865
|
+
for request_state in pending_requests:
|
|
1866
|
+
if request_state.response_id is None:
|
|
1867
|
+
request_state.response_id = response_id
|
|
1868
|
+
return request_state
|
|
1869
|
+
return None
|
|
1870
|
+
|
|
1871
|
+
|
|
1872
|
+
def _draining_websocket_request_states(
|
|
1873
|
+
pending_requests: deque[_WebSocketRequestState],
|
|
1874
|
+
) -> list[_WebSocketRequestState]:
|
|
1875
|
+
return [request_state for request_state in pending_requests if request_state.draining_until_terminal]
|
|
1876
|
+
|
|
1877
|
+
|
|
1878
|
+
def _is_response_output_event(event_type: str | None) -> bool:
|
|
1879
|
+
"""A ``response.*`` frame that is neither a terminal nor an error carries response output."""
|
|
1880
|
+
return (
|
|
1881
|
+
isinstance(event_type, str)
|
|
1882
|
+
and event_type.startswith("response.")
|
|
1883
|
+
and event_type not in {"response.completed", "response.failed", "response.incomplete"}
|
|
1884
|
+
)
|
|
1885
|
+
|
|
1886
|
+
|
|
1887
|
+
def _match_websocket_request_state_for_anonymous_event(
|
|
1888
|
+
pending_requests: deque[_WebSocketRequestState],
|
|
1889
|
+
*,
|
|
1890
|
+
prefer_previous_response_not_found: bool,
|
|
1891
|
+
previous_response_id_hint: str | None = None,
|
|
1892
|
+
error_message: str | None = None,
|
|
1893
|
+
allow_unanchored_previous_response_error: bool = False,
|
|
1894
|
+
prefer_draining_requests: bool = True,
|
|
1895
|
+
event_type: str | None = None,
|
|
1896
|
+
) -> _WebSocketRequestState | None:
|
|
1897
|
+
if prefer_previous_response_not_found:
|
|
1898
|
+
return _match_websocket_request_state_for_previous_response_error(
|
|
1899
|
+
pending_requests,
|
|
1900
|
+
previous_response_id_hint=previous_response_id_hint,
|
|
1901
|
+
error_message=error_message,
|
|
1902
|
+
allow_unanchored_previous_response_error=allow_unanchored_previous_response_error,
|
|
1903
|
+
)
|
|
1904
|
+
|
|
1905
|
+
if _is_response_output_event(event_type):
|
|
1906
|
+
# Output frames carry no response id. On a pipelined socket they belong
|
|
1907
|
+
# to the one response upstream has already created, whether that request
|
|
1908
|
+
# is still visible or draining, never to a sibling still waiting for its
|
|
1909
|
+
# own response.created (issue #2350). Vendor telemetry such as
|
|
1910
|
+
# ``codex.rate_limits`` keeps the pre-created ownership below.
|
|
1911
|
+
created_requests = [
|
|
1912
|
+
request_state for request_state in pending_requests if request_state.response_id is not None
|
|
1913
|
+
]
|
|
1914
|
+
if len(created_requests) == 1:
|
|
1915
|
+
return created_requests[0]
|
|
1916
|
+
|
|
1917
|
+
visible_requests = [
|
|
1918
|
+
request_state for request_state in pending_requests if _http_bridge_request_counts_against_queue(request_state)
|
|
1919
|
+
]
|
|
1920
|
+
draining_requests = _draining_websocket_request_states(pending_requests)
|
|
1921
|
+
if prefer_draining_requests and draining_requests:
|
|
1922
|
+
unresolved_draining_requests = [
|
|
1923
|
+
request_state for request_state in draining_requests if request_state.response_id is None
|
|
1924
|
+
]
|
|
1925
|
+
if len(unresolved_draining_requests) == 1:
|
|
1926
|
+
return unresolved_draining_requests[0]
|
|
1927
|
+
if not visible_requests:
|
|
1928
|
+
return draining_requests[0]
|
|
1929
|
+
|
|
1930
|
+
if len(visible_requests) == 1:
|
|
1931
|
+
return visible_requests[0]
|
|
1932
|
+
|
|
1933
|
+
unresolved_visible_requests = [
|
|
1934
|
+
request_state for request_state in visible_requests if request_state.response_id is None
|
|
1935
|
+
]
|
|
1936
|
+
if len(unresolved_visible_requests) == 1:
|
|
1937
|
+
return unresolved_visible_requests[0]
|
|
1938
|
+
|
|
1939
|
+
if not visible_requests and draining_requests:
|
|
1940
|
+
unresolved_draining_requests = [
|
|
1941
|
+
request_state for request_state in draining_requests if request_state.response_id is None
|
|
1942
|
+
]
|
|
1943
|
+
if len(unresolved_draining_requests) == 1:
|
|
1944
|
+
return unresolved_draining_requests[0]
|
|
1945
|
+
return draining_requests[0]
|
|
1946
|
+
|
|
1947
|
+
return None
|
|
1948
|
+
|
|
1949
|
+
|
|
1950
|
+
def _match_websocket_request_state_for_precreated_terminal_event(
|
|
1951
|
+
pending_requests: deque[_WebSocketRequestState],
|
|
1952
|
+
) -> _WebSocketRequestState | None:
|
|
1953
|
+
unresolved_requests = [
|
|
1954
|
+
request_state
|
|
1955
|
+
for request_state in pending_requests
|
|
1956
|
+
if request_state.response_id is None and request_state.awaiting_response_created
|
|
1957
|
+
]
|
|
1958
|
+
if len(unresolved_requests) == 1:
|
|
1959
|
+
return unresolved_requests[0]
|
|
1960
|
+
return None
|
|
1961
|
+
|
|
1962
|
+
|
|
1963
|
+
def _match_websocket_request_state_for_previous_response_error(
|
|
1964
|
+
pending_requests: deque[_WebSocketRequestState],
|
|
1965
|
+
*,
|
|
1966
|
+
previous_response_id_hint: str | None = None,
|
|
1967
|
+
error_message: str | None = None,
|
|
1968
|
+
allow_unanchored_previous_response_error: bool = False,
|
|
1969
|
+
) -> _WebSocketRequestState | None:
|
|
1970
|
+
matching_requests = _matching_websocket_request_states_for_previous_response_error(
|
|
1971
|
+
pending_requests,
|
|
1972
|
+
previous_response_id_hint=previous_response_id_hint,
|
|
1973
|
+
error_message=error_message,
|
|
1974
|
+
allow_unanchored_previous_response_error=allow_unanchored_previous_response_error,
|
|
1975
|
+
)
|
|
1976
|
+
if len(matching_requests) == 1:
|
|
1977
|
+
return matching_requests[0]
|
|
1978
|
+
return None
|
|
1979
|
+
|
|
1980
|
+
|
|
1981
|
+
def _is_unanchored_previous_response_error_message(message: str | None) -> bool:
|
|
1982
|
+
if message is None:
|
|
1983
|
+
return False
|
|
1984
|
+
normalized = " ".join(message.casefold().replace("`", "").split()).removesuffix(".").rstrip()
|
|
1985
|
+
return normalized == "invalid previous_response_id"
|
|
1986
|
+
|
|
1987
|
+
|
|
1988
|
+
def _matching_websocket_request_states_for_previous_response_error(
|
|
1989
|
+
pending_requests: deque[_WebSocketRequestState],
|
|
1990
|
+
*,
|
|
1991
|
+
previous_response_id_hint: str | None = None,
|
|
1992
|
+
error_message: str | None = None,
|
|
1993
|
+
allow_unanchored_previous_response_error: bool = False,
|
|
1994
|
+
) -> list[_WebSocketRequestState]:
|
|
1995
|
+
followup_requests = [
|
|
1996
|
+
request_state for request_state in pending_requests if request_state.previous_response_id is not None
|
|
1997
|
+
]
|
|
1998
|
+
if not followup_requests:
|
|
1999
|
+
if allow_unanchored_previous_response_error and len(pending_requests) == 1:
|
|
2000
|
+
return [pending_requests[0]]
|
|
2001
|
+
return []
|
|
2002
|
+
if previous_response_id_hint is not None:
|
|
2003
|
+
matching_requests = [
|
|
2004
|
+
request_state
|
|
2005
|
+
for request_state in followup_requests
|
|
2006
|
+
if request_state.previous_response_id == previous_response_id_hint
|
|
2007
|
+
]
|
|
2008
|
+
if matching_requests:
|
|
2009
|
+
return matching_requests
|
|
2010
|
+
if error_message is not None:
|
|
2011
|
+
matching_requests = [
|
|
2012
|
+
request_state
|
|
2013
|
+
for request_state in followup_requests
|
|
2014
|
+
if _facade()._message_mentions_previous_response_id(error_message, request_state.previous_response_id)
|
|
2015
|
+
]
|
|
2016
|
+
if matching_requests:
|
|
2017
|
+
return matching_requests
|
|
2018
|
+
unresolved_followups = [request_state for request_state in followup_requests if request_state.response_id is None]
|
|
2019
|
+
if len(unresolved_followups) == 1:
|
|
2020
|
+
return unresolved_followups
|
|
2021
|
+
if len(unresolved_followups) > 1:
|
|
2022
|
+
unique_previous_response_ids = {
|
|
2023
|
+
request_state.previous_response_id
|
|
2024
|
+
for request_state in unresolved_followups
|
|
2025
|
+
if request_state.previous_response_id
|
|
2026
|
+
}
|
|
2027
|
+
if len(unique_previous_response_ids) == 1:
|
|
2028
|
+
return unresolved_followups
|
|
2029
|
+
if (
|
|
2030
|
+
allow_unanchored_previous_response_error
|
|
2031
|
+
and len(followup_requests) == 1
|
|
2032
|
+
and _is_unanchored_previous_response_error_message(error_message)
|
|
2033
|
+
):
|
|
2034
|
+
# A post-created terminal error may omit both its response id and the
|
|
2035
|
+
# rejected anchor id. When exactly one follow-up is pending, its
|
|
2036
|
+
# previous-response ownership is the only safe correlation seam even
|
|
2037
|
+
# after ``response.created`` assigned a response id.
|
|
2038
|
+
return followup_requests
|
|
2039
|
+
return []
|
|
2040
|
+
|
|
2041
|
+
|
|
2042
|
+
def _matching_websocket_request_states_for_missing_tool_output_error(
|
|
2043
|
+
pending_requests: deque[_WebSocketRequestState],
|
|
2044
|
+
) -> list[_WebSocketRequestState]:
|
|
2045
|
+
unresolved_followups = [
|
|
2046
|
+
request_state
|
|
2047
|
+
for request_state in pending_requests
|
|
2048
|
+
if request_state.response_id is None and request_state.previous_response_id is not None
|
|
2049
|
+
]
|
|
2050
|
+
if len(unresolved_followups) <= 1:
|
|
2051
|
+
return unresolved_followups
|
|
2052
|
+
unique_previous_response_ids = {
|
|
2053
|
+
request_state.previous_response_id
|
|
2054
|
+
for request_state in unresolved_followups
|
|
2055
|
+
if request_state.previous_response_id
|
|
2056
|
+
}
|
|
2057
|
+
if len(unique_previous_response_ids) == 1:
|
|
2058
|
+
return unresolved_followups
|
|
2059
|
+
return []
|
|
2060
|
+
|
|
2061
|
+
|
|
2062
|
+
def _pop_matching_websocket_request_states(
|
|
2063
|
+
pending_requests: deque[_WebSocketRequestState],
|
|
2064
|
+
matching_requests: list[_WebSocketRequestState],
|
|
2065
|
+
) -> list[_WebSocketRequestState]:
|
|
2066
|
+
popped_requests: list[_WebSocketRequestState] = []
|
|
2067
|
+
for request_state in matching_requests:
|
|
2068
|
+
try:
|
|
2069
|
+
pending_requests.remove(request_state)
|
|
2070
|
+
except ValueError:
|
|
2071
|
+
continue
|
|
2072
|
+
popped_requests.append(request_state)
|
|
2073
|
+
return popped_requests
|
|
2074
|
+
|
|
2075
|
+
|
|
2076
|
+
async def _release_websocket_response_create_gate(
|
|
2077
|
+
request_state: _WebSocketRequestState,
|
|
2078
|
+
response_create_gate: asyncio.Semaphore,
|
|
2079
|
+
*,
|
|
2080
|
+
scheduler: Scheduler,
|
|
2081
|
+
) -> None:
|
|
2082
|
+
"""Release every create owner; ``scheduler`` owns the deferred lease-release task."""
|
|
2083
|
+
|
|
2084
|
+
cancellation: asyncio.CancelledError | None = None
|
|
2085
|
+
account_response_create_lease = request_state.account_response_create_lease
|
|
2086
|
+
account_response_create_release = request_state.account_response_create_release
|
|
2087
|
+
request_state.account_response_create_lease = None
|
|
2088
|
+
request_state.account_response_create_release = None
|
|
2089
|
+
if request_state.response_create_admission is not None:
|
|
2090
|
+
request_state.response_create_admission.release()
|
|
2091
|
+
request_state.response_create_admission = None
|
|
2092
|
+
if account_response_create_lease is not None and account_response_create_release is not None:
|
|
2093
|
+
cancellation = await _await_cleanup_deferring_cancellation(
|
|
2094
|
+
account_response_create_release(account_response_create_lease),
|
|
2095
|
+
scheduler=scheduler,
|
|
2096
|
+
)
|
|
2097
|
+
request_state.awaiting_response_created = False
|
|
2098
|
+
request_state.response_create_gate = None
|
|
2099
|
+
if not request_state.response_create_gate_acquired:
|
|
2100
|
+
if cancellation is not None:
|
|
2101
|
+
raise cancellation
|
|
2102
|
+
return
|
|
2103
|
+
request_state.response_create_gate_acquired = False
|
|
2104
|
+
response_create_gate.release()
|
|
2105
|
+
if cancellation is not None:
|
|
2106
|
+
raise cancellation
|
|
2107
|
+
|
|
2108
|
+
|
|
2109
|
+
def _pop_terminal_websocket_request_state(
|
|
2110
|
+
pending_requests: deque[_WebSocketRequestState],
|
|
2111
|
+
*,
|
|
2112
|
+
response_id: str | None,
|
|
2113
|
+
fallback_request_state: _WebSocketRequestState | None,
|
|
2114
|
+
prefer_previous_response_not_found: bool = False,
|
|
2115
|
+
previous_response_id_hint: str | None = None,
|
|
2116
|
+
error_message: str | None = None,
|
|
2117
|
+
allow_unanchored_previous_response_error: bool = False,
|
|
2118
|
+
allow_precreated_terminal_fallback: bool = False,
|
|
2119
|
+
prefer_draining_requests: bool = True,
|
|
2120
|
+
) -> _WebSocketRequestState | None:
|
|
2121
|
+
if response_id is not None:
|
|
2122
|
+
request_state = _find_websocket_request_state_by_response_id(pending_requests, response_id)
|
|
2123
|
+
if request_state is not None:
|
|
2124
|
+
pending_requests.remove(request_state)
|
|
2125
|
+
return request_state
|
|
2126
|
+
if fallback_request_state is not None and fallback_request_state in pending_requests:
|
|
2127
|
+
pending_requests.remove(fallback_request_state)
|
|
2128
|
+
return fallback_request_state
|
|
2129
|
+
if response_id is not None and allow_precreated_terminal_fallback:
|
|
2130
|
+
request_state = _match_websocket_request_state_for_precreated_terminal_event(pending_requests)
|
|
2131
|
+
if request_state is not None and request_state in pending_requests:
|
|
2132
|
+
pending_requests.remove(request_state)
|
|
2133
|
+
return request_state
|
|
2134
|
+
if response_id is not None and prefer_previous_response_not_found:
|
|
2135
|
+
request_state = _match_websocket_request_state_for_previous_response_error(
|
|
2136
|
+
pending_requests,
|
|
2137
|
+
previous_response_id_hint=previous_response_id_hint,
|
|
2138
|
+
error_message=error_message,
|
|
2139
|
+
allow_unanchored_previous_response_error=allow_unanchored_previous_response_error,
|
|
2140
|
+
)
|
|
2141
|
+
if request_state is not None and request_state in pending_requests:
|
|
2142
|
+
pending_requests.remove(request_state)
|
|
2143
|
+
return request_state
|
|
2144
|
+
if response_id is None:
|
|
2145
|
+
request_state = _match_websocket_request_state_for_anonymous_event(
|
|
2146
|
+
pending_requests,
|
|
2147
|
+
prefer_previous_response_not_found=prefer_previous_response_not_found,
|
|
2148
|
+
previous_response_id_hint=previous_response_id_hint,
|
|
2149
|
+
error_message=error_message,
|
|
2150
|
+
allow_unanchored_previous_response_error=allow_unanchored_previous_response_error,
|
|
2151
|
+
prefer_draining_requests=prefer_draining_requests,
|
|
2152
|
+
)
|
|
2153
|
+
if request_state is not None and request_state in pending_requests:
|
|
2154
|
+
pending_requests.remove(request_state)
|
|
2155
|
+
return request_state
|
|
2156
|
+
return None
|
|
2157
|
+
|
|
2158
|
+
|
|
2159
|
+
def _upstream_websocket_disconnect_message(message: UpstreamWebSocketMessage) -> str:
|
|
2160
|
+
if message.kind == "error" and message.error:
|
|
2161
|
+
return f"Upstream websocket closed before response.completed: {message.error}"
|
|
2162
|
+
if message.close_code is not None:
|
|
2163
|
+
return f"Upstream websocket closed before response.completed (close_code={message.close_code})"
|
|
2164
|
+
return "Upstream websocket closed before response.completed"
|
|
2165
|
+
|
|
2166
|
+
|
|
2167
|
+
def _websocket_receive_timeout_for_pending_requests(
|
|
2168
|
+
started_ats: Sequence[float],
|
|
2169
|
+
*,
|
|
2170
|
+
proxy_request_budget_seconds: float,
|
|
2171
|
+
stream_idle_timeout_seconds: float,
|
|
2172
|
+
now: float,
|
|
2173
|
+
) -> _WebSocketReceiveTimeout | None:
|
|
2174
|
+
if not started_ats:
|
|
2175
|
+
return None
|
|
2176
|
+
|
|
2177
|
+
idle_timeout_seconds = max(0.001, stream_idle_timeout_seconds)
|
|
2178
|
+
oldest_started_at = min(started_ats)
|
|
2179
|
+
budget_deadline = oldest_started_at + proxy_request_budget_seconds
|
|
2180
|
+
# ``started_ats`` come from the owner's clock; ``now`` must too.
|
|
2181
|
+
remaining_budget = max(0.0, budget_deadline - now)
|
|
2182
|
+
idle_timeout_matches_request_budget = idle_timeout_seconds == max(0.001, proxy_request_budget_seconds)
|
|
2183
|
+
|
|
2184
|
+
if remaining_budget <= 0 and idle_timeout_matches_request_budget:
|
|
2185
|
+
return _WebSocketReceiveTimeout(
|
|
2186
|
+
timeout_seconds=0.0,
|
|
2187
|
+
error_code="stream_idle_timeout",
|
|
2188
|
+
error_message="Upstream stream idle timeout",
|
|
2189
|
+
)
|
|
2190
|
+
if idle_timeout_matches_request_budget and remaining_budget >= idle_timeout_seconds:
|
|
2191
|
+
return _WebSocketReceiveTimeout(
|
|
2192
|
+
timeout_seconds=remaining_budget,
|
|
2193
|
+
error_code="stream_idle_timeout",
|
|
2194
|
+
error_message="Upstream stream idle timeout",
|
|
2195
|
+
)
|
|
2196
|
+
if remaining_budget <= 0:
|
|
2197
|
+
return _WebSocketReceiveTimeout(
|
|
2198
|
+
timeout_seconds=0.0,
|
|
2199
|
+
error_code="upstream_request_timeout",
|
|
2200
|
+
error_message="Proxy request budget exhausted",
|
|
2201
|
+
)
|
|
2202
|
+
if idle_timeout_seconds < remaining_budget:
|
|
2203
|
+
return _WebSocketReceiveTimeout(
|
|
2204
|
+
timeout_seconds=idle_timeout_seconds,
|
|
2205
|
+
error_code="stream_idle_timeout",
|
|
2206
|
+
error_message="Upstream stream idle timeout",
|
|
2207
|
+
fail_all_pending=True,
|
|
2208
|
+
)
|
|
2209
|
+
return _WebSocketReceiveTimeout(
|
|
2210
|
+
timeout_seconds=remaining_budget,
|
|
2211
|
+
error_code="upstream_request_timeout",
|
|
2212
|
+
error_message="Proxy request budget exhausted",
|
|
2213
|
+
)
|
|
2214
|
+
|
|
2215
|
+
|
|
2216
|
+
class _WebSocketJsonObject(dict[str, JsonValue]):
|
|
2217
|
+
def __init__(self, pairs: list[tuple[str, JsonValue]]) -> None:
|
|
2218
|
+
super().__init__(pairs)
|
|
2219
|
+
self.raw_pairs = tuple(pairs)
|
|
2220
|
+
|
|
2221
|
+
|
|
2222
|
+
def _parse_websocket_payload(text: str) -> dict[str, JsonValue] | None:
|
|
2223
|
+
try:
|
|
2224
|
+
payload = json.loads(text, object_pairs_hook=_WebSocketJsonObject)
|
|
2225
|
+
except json.JSONDecodeError:
|
|
2226
|
+
return None
|
|
2227
|
+
if not isinstance(payload, dict):
|
|
2228
|
+
return None
|
|
2229
|
+
return payload
|
|
2230
|
+
|
|
2231
|
+
|
|
2232
|
+
def _websocket_capability_metadata_values(payload: dict[str, JsonValue]) -> tuple[JsonValue, ...] | None:
|
|
2233
|
+
normalized_name = CODEX_LB_REQUIRED_CAPABILITY_HEADER.lower()
|
|
2234
|
+
payload_pairs = payload.raw_pairs if isinstance(payload, _WebSocketJsonObject) else tuple(payload.items())
|
|
2235
|
+
misplaced_values = [value for key, value in payload_pairs if key.lower() == normalized_name]
|
|
2236
|
+
if misplaced_values:
|
|
2237
|
+
# The reserved per-frame carrier is valid only inside
|
|
2238
|
+
# ``client_metadata``. Surface a deliberately ambiguous carrier set so
|
|
2239
|
+
# the shared parser rejects any top-level placement before selection.
|
|
2240
|
+
return (misplaced_values[0], misplaced_values[0])
|
|
2241
|
+
if not isinstance(payload, _WebSocketJsonObject):
|
|
2242
|
+
return None
|
|
2243
|
+
metadata_objects = [value for key, value in payload.raw_pairs if key == "client_metadata"]
|
|
2244
|
+
values: list[JsonValue] = []
|
|
2245
|
+
for metadata in metadata_objects:
|
|
2246
|
+
if not isinstance(metadata, _WebSocketJsonObject):
|
|
2247
|
+
continue
|
|
2248
|
+
values.extend(value for key, value in metadata.raw_pairs if key.lower() == normalized_name)
|
|
2249
|
+
if len(metadata_objects) > 1 and values:
|
|
2250
|
+
# A duplicate top-level metadata container can otherwise erase the
|
|
2251
|
+
# sole marker through last-key-wins JSON decoding. Treat the carrier
|
|
2252
|
+
# as ambiguous so routing fails before selection.
|
|
2253
|
+
values.append(values[0])
|
|
2254
|
+
return tuple(values)
|
|
2255
|
+
|
|
2256
|
+
|
|
2257
|
+
def _is_websocket_response_create(payload: dict[str, JsonValue]) -> bool:
|
|
2258
|
+
payload_type = payload.get("type")
|
|
2259
|
+
return isinstance(payload_type, str) and payload_type == "response.create"
|
|
2260
|
+
|
|
2261
|
+
|
|
2262
|
+
def _app_error_to_websocket_event(exc: AppError) -> dict[str, JsonValue]:
|
|
2263
|
+
payload = openai_error(exc.code, exc.message, error_type=getattr(exc, "error_type", "server_error"))
|
|
2264
|
+
if exc.param is not None:
|
|
2265
|
+
payload["error"]["param"] = exc.param
|
|
2266
|
+
return _wrapped_websocket_error_event(
|
|
2267
|
+
exc.status_code,
|
|
2268
|
+
payload,
|
|
2269
|
+
)
|
|
2270
|
+
|
|
2271
|
+
|
|
2272
|
+
def _wrapped_websocket_error_event(
|
|
2273
|
+
status_code: int,
|
|
2274
|
+
payload: OpenAIErrorEnvelope,
|
|
2275
|
+
*,
|
|
2276
|
+
expose_stale_previous_response_classifier: bool = False,
|
|
2277
|
+
) -> dict[str, JsonValue]:
|
|
2278
|
+
error = payload["error"]
|
|
2279
|
+
error_code = _normalize_error_code(
|
|
2280
|
+
error.get("code"),
|
|
2281
|
+
error.get("type"),
|
|
2282
|
+
)
|
|
2283
|
+
error_param = OpenAIErrorParam.from_mapping(cast(Mapping[str, JsonValue], error))
|
|
2284
|
+
error_message = error.get("message")
|
|
2285
|
+
if _facade()._is_previous_response_not_found_public_shape(
|
|
2286
|
+
code=error_code,
|
|
2287
|
+
param=error_param,
|
|
2288
|
+
message=error_message,
|
|
2289
|
+
):
|
|
2290
|
+
status_code = 502
|
|
2291
|
+
# On the Codex-native route, the caller has already sanitized this to
|
|
2292
|
+
# the canonical code (see _sanitize_websocket_previous_response_error);
|
|
2293
|
+
# do not re-mask it back to stream_incomplete. Every other caller
|
|
2294
|
+
# (public /v1, or a raw error this function is seeing for the first
|
|
2295
|
+
# time) keeps the existing stream_incomplete safety net.
|
|
2296
|
+
expose = expose_stale_previous_response_classifier and _facade()._is_previous_response_not_found_error(
|
|
2297
|
+
code=error_code,
|
|
2298
|
+
param=error_param,
|
|
2299
|
+
message=error_message,
|
|
2300
|
+
)
|
|
2301
|
+
if not expose:
|
|
2302
|
+
payload = previous_response_stream_incomplete_error()
|
|
2303
|
+
error_payload = cast(
|
|
2304
|
+
JsonValue,
|
|
2305
|
+
sanitize_public_error_detail(cast(Mapping[str, JsonValue], payload["error"])),
|
|
2306
|
+
)
|
|
2307
|
+
event: dict[str, JsonValue] = {
|
|
2308
|
+
"type": "error",
|
|
2309
|
+
"status": status_code,
|
|
2310
|
+
"error": error_payload,
|
|
2311
|
+
}
|
|
2312
|
+
return event
|
|
2313
|
+
|
|
2314
|
+
|
|
2315
|
+
def _serialize_websocket_error_event(payload: dict[str, JsonValue]) -> str:
|
|
2316
|
+
return json.dumps(payload, ensure_ascii=True, separators=(",", ":"))
|
|
2317
|
+
|
|
2318
|
+
|
|
2319
|
+
def _trim_websocket_previous_response_input_items(input_items: list[JsonValue]) -> list[JsonValue]:
|
|
2320
|
+
first_output_index = next(
|
|
2321
|
+
(
|
|
2322
|
+
index
|
|
2323
|
+
for index, item in enumerate(input_items)
|
|
2324
|
+
if _websocket_input_item_type(item)
|
|
2325
|
+
in {"function_call_output", "custom_tool_call_output", "apply_patch_call_output"}
|
|
2326
|
+
),
|
|
2327
|
+
None,
|
|
2328
|
+
)
|
|
2329
|
+
if first_output_index is None or first_output_index == 0:
|
|
2330
|
+
return input_items
|
|
2331
|
+
prefix = input_items[:first_output_index]
|
|
2332
|
+
if not all(_is_websocket_previous_response_output_item(item) for item in prefix):
|
|
2333
|
+
return input_items
|
|
2334
|
+
return input_items[first_output_index:]
|
|
2335
|
+
|
|
2336
|
+
|
|
2337
|
+
def _is_websocket_previous_response_output_item(item: JsonValue) -> bool:
|
|
2338
|
+
if isinstance(item, dict) and _websocket_input_item_type(item) is None and item.get("role") == "assistant":
|
|
2339
|
+
return True
|
|
2340
|
+
item_type = _websocket_input_item_type(item)
|
|
2341
|
+
if item_type in {"reasoning", "function_call", "custom_tool_call", "apply_patch_call"}:
|
|
2342
|
+
return True
|
|
2343
|
+
if item_type != "message" or not isinstance(item, dict):
|
|
2344
|
+
return False
|
|
2345
|
+
return item.get("role") == "assistant"
|
|
2346
|
+
|
|
2347
|
+
|
|
2348
|
+
def _websocket_input_item_type(item: JsonValue) -> str | None:
|
|
2349
|
+
if not isinstance(item, dict):
|
|
2350
|
+
return None
|
|
2351
|
+
item_type = item.get("type")
|
|
2352
|
+
return item_type if isinstance(item_type, str) else None
|
|
2353
|
+
|
|
2354
|
+
|
|
2355
|
+
def _websocket_connect_deadline(request_state: _WebSocketRequestState, budget_seconds: float, *, now: float) -> float:
|
|
2356
|
+
# ``now`` is the owner's clock sample: the fallback for a request state
|
|
2357
|
+
# without a start stamp must live in the same time domain as the budget
|
|
2358
|
+
# checks that later compare against this deadline.
|
|
2359
|
+
started_at = request_state.started_at if request_state.started_at > 0 else now
|
|
2360
|
+
return started_at + budget_seconds
|