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,1317 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from copy import deepcopy
|
|
8
|
+
from typing import Any, AsyncIterator, Literal, Mapping, cast
|
|
9
|
+
|
|
10
|
+
from app.core.balancer import PERMANENT_FAILURE_CODES
|
|
11
|
+
from app.core.balancer.types import ClassifiedFailure, UpstreamError
|
|
12
|
+
from app.core.clients.files import create_file as core_create_file # noqa: F401
|
|
13
|
+
from app.core.clients.files import finalize_file as core_finalize_file # noqa: F401
|
|
14
|
+
from app.core.clients.http import lease_http_session as lease_http_session # noqa: F401
|
|
15
|
+
from app.core.clients.proxy import CodexControlRequestPrivacyPolicy as CodexControlRequestPrivacyPolicy
|
|
16
|
+
from app.core.clients.proxy import CodexControlResponse as CodexControlResponse
|
|
17
|
+
from app.core.clients.proxy import ( # noqa: F401 # noqa: F401
|
|
18
|
+
ImageFetchSession,
|
|
19
|
+
ProxyResponseError,
|
|
20
|
+
UpstreamProxyRouteTrace,
|
|
21
|
+
_agent_control_tool_output_occurrences,
|
|
22
|
+
_as_image_fetch_session,
|
|
23
|
+
_inline_content_images,
|
|
24
|
+
_inline_input_image_urls,
|
|
25
|
+
_ws_transport_payload_budget_bytes,
|
|
26
|
+
filter_inbound_headers,
|
|
27
|
+
pop_compact_timeout_overrides,
|
|
28
|
+
pop_stream_timeout_overrides,
|
|
29
|
+
pop_transcribe_timeout_overrides,
|
|
30
|
+
push_compact_timeout_overrides,
|
|
31
|
+
push_stream_timeout_overrides,
|
|
32
|
+
push_transcribe_timeout_overrides,
|
|
33
|
+
)
|
|
34
|
+
from app.core.clients.proxy import codex_control_request as core_codex_control_request # noqa: F401
|
|
35
|
+
from app.core.clients.proxy import compact_responses as core_compact_responses # noqa: F401
|
|
36
|
+
from app.core.clients.proxy import transcribe_audio as core_transcribe_audio # noqa: F401
|
|
37
|
+
from app.core.clients.proxy_websocket import (
|
|
38
|
+
UpstreamWebSocket,
|
|
39
|
+
)
|
|
40
|
+
from app.core.clock import Clock
|
|
41
|
+
from app.core.errors import (
|
|
42
|
+
PREVIOUS_RESPONSE_MALFORMED_PARAM_REASON,
|
|
43
|
+
PREVIOUS_RESPONSE_STREAM_INCOMPLETE_MESSAGE,
|
|
44
|
+
SYNTHETIC_TRANSPORT_FAILURE_CODES,
|
|
45
|
+
SYNTHETIC_TRANSPORT_FAILURE_MARKER,
|
|
46
|
+
OpenAIErrorParam,
|
|
47
|
+
openai_error,
|
|
48
|
+
response_failed_event,
|
|
49
|
+
synthetic_stream_failure_event,
|
|
50
|
+
synthetic_transport_failure_event,
|
|
51
|
+
)
|
|
52
|
+
from app.core.errors import (
|
|
53
|
+
PREVIOUS_RESPONSE_NOT_FOUND_CODE as PREVIOUS_RESPONSE_NOT_FOUND_CODE,
|
|
54
|
+
)
|
|
55
|
+
from app.core.errors import (
|
|
56
|
+
PREVIOUS_RESPONSE_NOT_FOUND_MESSAGE as PREVIOUS_RESPONSE_NOT_FOUND_MESSAGE,
|
|
57
|
+
)
|
|
58
|
+
from app.core.openai.models import OpenAIError, OpenAIEvent, OpenAIResponsePayload, ResponseUsage
|
|
59
|
+
from app.core.openai.parsing import classify_event_type, parse_sse_event
|
|
60
|
+
from app.core.openai.requests import ResponsesRequest
|
|
61
|
+
from app.core.resilience.network_recovery import (
|
|
62
|
+
PROCESS_NETWORK_UNAVAILABLE_CODE,
|
|
63
|
+
)
|
|
64
|
+
from app.core.types import JsonValue
|
|
65
|
+
from app.core.upstream_proxy import ResolvedUpstreamRoute, UpstreamProxyRouteError
|
|
66
|
+
from app.core.upstream_proxy.cache import get_upstream_route_cache
|
|
67
|
+
from app.core.utils.request_id import get_request_id
|
|
68
|
+
from app.core.utils.sse import CODEX_KEEPALIVE_FRAME as CODEX_KEEPALIVE_FRAME # noqa: F401
|
|
69
|
+
from app.core.utils.sse import ParsedSseBlock, format_sse_event, parse_sse_data_json
|
|
70
|
+
from app.core.utils.time import utcnow as utcnow
|
|
71
|
+
from app.db.models import (
|
|
72
|
+
Account,
|
|
73
|
+
AccountStatus, # noqa: F401
|
|
74
|
+
)
|
|
75
|
+
from app.modules.api_keys.service import ApiKeyData
|
|
76
|
+
from app.modules.proxy._load_balancer.overload_backoff import (
|
|
77
|
+
UPSTREAM_OVERLOAD_CODES,
|
|
78
|
+
UPSTREAM_SOFT_OVERLOAD_CODES,
|
|
79
|
+
record_upstream_burst_rejection,
|
|
80
|
+
record_upstream_overload,
|
|
81
|
+
)
|
|
82
|
+
from app.modules.proxy._service.api_key_usage import (
|
|
83
|
+
_API_KEY_RESERVATION_HEARTBEAT_SECONDS as _API_KEY_RESERVATION_HEARTBEAT_SECONDS,
|
|
84
|
+
)
|
|
85
|
+
from app.modules.proxy._service.compact import (
|
|
86
|
+
_service_tier_from_compact_payload as _service_tier_from_compact_payload,
|
|
87
|
+
)
|
|
88
|
+
from app.modules.proxy._service.compact import (
|
|
89
|
+
_sticky_key_for_compact_request as _sticky_key_for_compact_request,
|
|
90
|
+
)
|
|
91
|
+
from app.modules.proxy._service.compact import (
|
|
92
|
+
_sticky_key_from_compact_payload as _sticky_key_from_compact_payload,
|
|
93
|
+
)
|
|
94
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
95
|
+
_active_http_bridge_instance_ring as _active_http_bridge_instance_ring,
|
|
96
|
+
)
|
|
97
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
98
|
+
_build_http_bridge_prewarm_text as _build_http_bridge_prewarm_text,
|
|
99
|
+
)
|
|
100
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
101
|
+
_durable_bridge_lookup_active_owner as _durable_bridge_lookup_active_owner,
|
|
102
|
+
)
|
|
103
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
104
|
+
_durable_bridge_lookup_allows_local_reuse as _durable_bridge_lookup_allows_local_reuse,
|
|
105
|
+
)
|
|
106
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
107
|
+
_effective_http_bridge_idle_ttl_seconds as _effective_http_bridge_idle_ttl_seconds,
|
|
108
|
+
)
|
|
109
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
110
|
+
_forwarded_http_bridge_session_key as _forwarded_http_bridge_session_key,
|
|
111
|
+
)
|
|
112
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
113
|
+
_has_http_bridge_response_output_marker as _has_http_bridge_response_output_marker,
|
|
114
|
+
)
|
|
115
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
116
|
+
_http_bridge_allow_durable_takeover as _http_bridge_allow_durable_takeover,
|
|
117
|
+
)
|
|
118
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
119
|
+
_http_bridge_can_local_recover_without_ring as _http_bridge_can_local_recover_without_ring,
|
|
120
|
+
)
|
|
121
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
122
|
+
_http_bridge_can_recover_during_drain as _http_bridge_can_recover_during_drain,
|
|
123
|
+
)
|
|
124
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
125
|
+
_http_bridge_continuity_lost_error_envelope as _http_bridge_continuity_lost_error_envelope,
|
|
126
|
+
)
|
|
127
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
128
|
+
_http_bridge_durable_lease_ttl_seconds as _http_bridge_durable_lease_ttl_seconds,
|
|
129
|
+
)
|
|
130
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
131
|
+
_http_bridge_endpoint_matches_current_instance as _http_bridge_endpoint_matches_current_instance,
|
|
132
|
+
)
|
|
133
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
134
|
+
_http_bridge_eviction_priority as _http_bridge_eviction_priority,
|
|
135
|
+
)
|
|
136
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
137
|
+
_http_bridge_has_durable_recovery_anchor as _http_bridge_has_durable_recovery_anchor,
|
|
138
|
+
)
|
|
139
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
140
|
+
_http_bridge_input_item_type as _http_bridge_input_item_type,
|
|
141
|
+
)
|
|
142
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
143
|
+
_http_bridge_is_context_overflow_error as _http_bridge_is_context_overflow_error,
|
|
144
|
+
)
|
|
145
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
146
|
+
_http_bridge_is_previous_response_owner_unavailable as _http_bridge_is_previous_response_owner_unavailable,
|
|
147
|
+
)
|
|
148
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
149
|
+
_http_bridge_key_strength as _http_bridge_key_strength,
|
|
150
|
+
)
|
|
151
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
152
|
+
_http_bridge_owner_check_required as _http_bridge_owner_check_required,
|
|
153
|
+
)
|
|
154
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
155
|
+
_http_bridge_owner_instance as _http_bridge_owner_instance,
|
|
156
|
+
)
|
|
157
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
158
|
+
_http_bridge_owner_lookup_unavailable_error_envelope as _http_bridge_owner_lookup_unavailable_error_envelope,
|
|
159
|
+
)
|
|
160
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
161
|
+
_http_bridge_payload_looks_like_full_resend as _http_bridge_payload_looks_like_full_resend,
|
|
162
|
+
)
|
|
163
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
164
|
+
_http_bridge_payload_without_previous_response_id as _http_bridge_payload_without_previous_response_id,
|
|
165
|
+
)
|
|
166
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
167
|
+
_http_bridge_precreated_retry_failure_error as _http_bridge_precreated_retry_failure_error,
|
|
168
|
+
)
|
|
169
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
170
|
+
_http_bridge_previous_response_alias_key as _http_bridge_previous_response_alias_key,
|
|
171
|
+
)
|
|
172
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
173
|
+
_http_bridge_previous_response_error_envelope as _http_bridge_previous_response_error_envelope,
|
|
174
|
+
)
|
|
175
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
176
|
+
_http_bridge_request_counts_against_queue as _http_bridge_request_counts_against_queue,
|
|
177
|
+
)
|
|
178
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
179
|
+
_http_bridge_request_stage as _http_bridge_request_stage,
|
|
180
|
+
)
|
|
181
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
182
|
+
_http_bridge_requires_cluster_registration as _http_bridge_requires_cluster_registration,
|
|
183
|
+
)
|
|
184
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
185
|
+
_http_bridge_runtime_config as _http_bridge_runtime_config,
|
|
186
|
+
)
|
|
187
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
188
|
+
_http_bridge_session_allows_api_key as _http_bridge_session_allows_api_key,
|
|
189
|
+
)
|
|
190
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
191
|
+
_http_bridge_session_has_visible_requests as _http_bridge_session_has_visible_requests,
|
|
192
|
+
)
|
|
193
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
194
|
+
_http_bridge_session_matches_preferred_account as _http_bridge_session_matches_preferred_account,
|
|
195
|
+
)
|
|
196
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
197
|
+
_http_bridge_session_retiring_with_visible_requests as _http_bridge_session_retiring_with_visible_requests,
|
|
198
|
+
)
|
|
199
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
200
|
+
_http_bridge_session_reusable_for_request as _http_bridge_session_reusable_for_request,
|
|
201
|
+
)
|
|
202
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
203
|
+
_http_bridge_should_attempt_local_bootstrap_rebind as _http_bridge_should_attempt_local_bootstrap_rebind,
|
|
204
|
+
)
|
|
205
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
206
|
+
_http_bridge_should_attempt_local_previous_response_recovery, # noqa: F401
|
|
207
|
+
)
|
|
208
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
209
|
+
_http_bridge_should_attempt_soft_affinity_reroute as _http_bridge_should_attempt_soft_affinity_reroute,
|
|
210
|
+
)
|
|
211
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
212
|
+
_http_bridge_should_rollover_after_context_overflow as _http_bridge_should_rollover_after_context_overflow,
|
|
213
|
+
)
|
|
214
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
215
|
+
_http_bridge_should_wait_for_registration as _http_bridge_should_wait_for_registration,
|
|
216
|
+
)
|
|
217
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
218
|
+
_http_bridge_startup_wait_timeout_error as _http_bridge_startup_wait_timeout_error,
|
|
219
|
+
)
|
|
220
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
221
|
+
_http_bridge_turn_state_alias_key as _http_bridge_turn_state_alias_key,
|
|
222
|
+
)
|
|
223
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
224
|
+
_is_http_bridge_previous_response_output_item as _is_http_bridge_previous_response_output_item,
|
|
225
|
+
)
|
|
226
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
227
|
+
_is_missing_durable_bridge_table_error as _is_missing_durable_bridge_table_error,
|
|
228
|
+
)
|
|
229
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
230
|
+
_log_http_bridge_event as _log_http_bridge_event,
|
|
231
|
+
)
|
|
232
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
233
|
+
_log_http_bridge_startup_wait_timeout as _log_http_bridge_startup_wait_timeout,
|
|
234
|
+
)
|
|
235
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
236
|
+
_make_http_bridge_session_key as _make_http_bridge_session_key,
|
|
237
|
+
)
|
|
238
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
239
|
+
_normalize_http_bridge_error_event as _normalize_http_bridge_error_event,
|
|
240
|
+
)
|
|
241
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
242
|
+
_normalized_http_bridge_instance_ring as _normalized_http_bridge_instance_ring,
|
|
243
|
+
)
|
|
244
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
245
|
+
_preferred_http_bridge_reconnect_turn_state as _preferred_http_bridge_reconnect_turn_state,
|
|
246
|
+
)
|
|
247
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
248
|
+
_record_bridge_drain_recovery_allowed as _record_bridge_drain_recovery_allowed,
|
|
249
|
+
)
|
|
250
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
251
|
+
_record_bridge_first_turn_timeout as _record_bridge_first_turn_timeout,
|
|
252
|
+
)
|
|
253
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
254
|
+
_record_bridge_reattach as _record_bridge_reattach,
|
|
255
|
+
)
|
|
256
|
+
from app.modules.proxy._service.http_bridge.helpers import (
|
|
257
|
+
_trim_http_bridge_previous_response_input_items as _trim_http_bridge_previous_response_input_items,
|
|
258
|
+
)
|
|
259
|
+
from app.modules.proxy._service.observability import (
|
|
260
|
+
_hash_identifier as _hash_identifier,
|
|
261
|
+
)
|
|
262
|
+
from app.modules.proxy._service.observability import (
|
|
263
|
+
_hash_identifier_or_none as _hash_identifier_or_none,
|
|
264
|
+
)
|
|
265
|
+
from app.modules.proxy._service.observability import (
|
|
266
|
+
_interesting_header_keys as _interesting_header_keys,
|
|
267
|
+
)
|
|
268
|
+
from app.modules.proxy._service.observability import (
|
|
269
|
+
_is_reasoning_replay_rejection as _is_reasoning_replay_rejection,
|
|
270
|
+
)
|
|
271
|
+
from app.modules.proxy._service.observability import (
|
|
272
|
+
_maybe_log_proxy_request_payload as _maybe_log_proxy_request_payload,
|
|
273
|
+
)
|
|
274
|
+
from app.modules.proxy._service.observability import (
|
|
275
|
+
_maybe_log_proxy_request_shape as _maybe_log_proxy_request_shape,
|
|
276
|
+
)
|
|
277
|
+
from app.modules.proxy._service.observability import (
|
|
278
|
+
_maybe_log_proxy_service_tier_trace as _maybe_log_proxy_service_tier_trace,
|
|
279
|
+
)
|
|
280
|
+
from app.modules.proxy._service.observability import (
|
|
281
|
+
_observe_terminal_stream_error_frame as _observe_terminal_stream_error_frame,
|
|
282
|
+
)
|
|
283
|
+
from app.modules.proxy._service.observability import (
|
|
284
|
+
_record_continuity_fail_closed as _record_continuity_fail_closed,
|
|
285
|
+
)
|
|
286
|
+
from app.modules.proxy._service.observability import (
|
|
287
|
+
_record_continuity_owner_resolution as _record_continuity_owner_resolution,
|
|
288
|
+
)
|
|
289
|
+
from app.modules.proxy._service.observability import (
|
|
290
|
+
_record_upstream_reasoning_replay_rejection as _record_upstream_reasoning_replay_rejection,
|
|
291
|
+
)
|
|
292
|
+
from app.modules.proxy._service.observability import (
|
|
293
|
+
_summarize_input as _summarize_input,
|
|
294
|
+
)
|
|
295
|
+
from app.modules.proxy._service.observability import (
|
|
296
|
+
_tools_hash as _tools_hash,
|
|
297
|
+
)
|
|
298
|
+
from app.modules.proxy._service.observability import (
|
|
299
|
+
_truncate_identifier as _truncate_identifier,
|
|
300
|
+
)
|
|
301
|
+
from app.modules.proxy._service.streaming.protocol import _StreamingServiceProtocol
|
|
302
|
+
from app.modules.proxy._service.support import (
|
|
303
|
+
_HARD_HTTP_BRIDGE_AFFINITY_KINDS, # noqa: F401
|
|
304
|
+
_REQUEST_TRANSPORT_WEBSOCKET, # noqa: F401
|
|
305
|
+
_WEBSOCKET_FULL_REPLAY_WAIT_MIN_ITEMS, # noqa: F401
|
|
306
|
+
_WEBSOCKET_FULL_REPLAY_WAIT_POLL_SECONDS, # noqa: F401
|
|
307
|
+
_event_type_from_payload,
|
|
308
|
+
_RequestLogFailureMetadata,
|
|
309
|
+
_signal_propagated_capacity_startup_ready,
|
|
310
|
+
_StreamSettlement,
|
|
311
|
+
_WebSocketRequestState,
|
|
312
|
+
)
|
|
313
|
+
from app.modules.proxy._service.support import (
|
|
314
|
+
_HTTPBridgeOwnerForward as _HTTPBridgeOwnerForward,
|
|
315
|
+
)
|
|
316
|
+
from app.modules.proxy._service.support import (
|
|
317
|
+
_record_websocket_route_metadata as _record_websocket_route_metadata,
|
|
318
|
+
)
|
|
319
|
+
from app.modules.proxy._service.support import (
|
|
320
|
+
_websocket_route_log_kwargs as _websocket_route_log_kwargs,
|
|
321
|
+
)
|
|
322
|
+
from app.modules.proxy._service.warmup import (
|
|
323
|
+
WarmupExecutionData as WarmupExecutionData,
|
|
324
|
+
)
|
|
325
|
+
from app.modules.proxy._service.warmup import (
|
|
326
|
+
WarmupFailedAccountData as WarmupFailedAccountData,
|
|
327
|
+
)
|
|
328
|
+
from app.modules.proxy._service.warmup import (
|
|
329
|
+
WarmupSkippedAccountData as WarmupSkippedAccountData,
|
|
330
|
+
)
|
|
331
|
+
from app.modules.proxy._service.warmup import (
|
|
332
|
+
WarmupSubmittedAccountData as WarmupSubmittedAccountData,
|
|
333
|
+
)
|
|
334
|
+
from app.modules.proxy._service.warmup import (
|
|
335
|
+
_is_warmup_usage_eligible as _is_warmup_usage_eligible,
|
|
336
|
+
)
|
|
337
|
+
from app.modules.proxy._service.warmup import (
|
|
338
|
+
_materialize_warmup_account as _materialize_warmup_account,
|
|
339
|
+
)
|
|
340
|
+
from app.modules.proxy._service.warmup import (
|
|
341
|
+
_snapshot_warmup_account as _snapshot_warmup_account,
|
|
342
|
+
)
|
|
343
|
+
from app.modules.proxy._service.warmup import (
|
|
344
|
+
_WarmupAccountSnapshot as _WarmupAccountSnapshot,
|
|
345
|
+
)
|
|
346
|
+
from app.modules.proxy._service.warmup import (
|
|
347
|
+
_WarmupSubmitResult as _WarmupSubmitResult,
|
|
348
|
+
)
|
|
349
|
+
from app.modules.proxy._service.warmup import (
|
|
350
|
+
_WarmupUsageSnapshot as _WarmupUsageSnapshot,
|
|
351
|
+
)
|
|
352
|
+
from app.modules.proxy._service.websocket.helpers import (
|
|
353
|
+
_app_error_to_websocket_event, # noqa: F401
|
|
354
|
+
_assign_websocket_response_id, # noqa: F401
|
|
355
|
+
_draining_websocket_request_states, # noqa: F401
|
|
356
|
+
_find_websocket_request_state_by_response_id, # noqa: F401
|
|
357
|
+
_is_websocket_previous_response_output_item, # noqa: F401
|
|
358
|
+
_is_websocket_response_create, # noqa: F401
|
|
359
|
+
_match_websocket_request_state_for_anonymous_event, # noqa: F401
|
|
360
|
+
_match_websocket_request_state_for_precreated_terminal_event, # noqa: F401
|
|
361
|
+
_match_websocket_request_state_for_previous_response_error, # noqa: F401
|
|
362
|
+
_matching_websocket_request_states_for_missing_tool_output_error, # noqa: F401
|
|
363
|
+
_matching_websocket_request_states_for_previous_response_error, # noqa: F401
|
|
364
|
+
_maybe_rewrite_websocket_previous_response_not_found_event, # noqa: F401
|
|
365
|
+
_parse_websocket_payload, # noqa: F401
|
|
366
|
+
_pop_matching_websocket_request_states, # noqa: F401
|
|
367
|
+
_pop_replayable_precreated_websocket_request_state, # noqa: F401
|
|
368
|
+
_pop_terminal_websocket_request_state, # noqa: F401
|
|
369
|
+
_prepare_websocket_request_state_for_auth_replay, # noqa: F401
|
|
370
|
+
_prepare_websocket_request_state_for_visible_output_replay, # noqa: F401
|
|
371
|
+
_record_websocket_continuity_completion, # noqa: F401
|
|
372
|
+
_refresh_websocket_request_input_fingerprint_from_text, # noqa: F401
|
|
373
|
+
_release_websocket_response_create_gate, # noqa: F401
|
|
374
|
+
_rewrite_websocket_continuity_corruption_event, # noqa: F401
|
|
375
|
+
_rewrite_websocket_downstream_response_id, # noqa: F401
|
|
376
|
+
_rewrite_websocket_previous_response_owner_unavailable_event, # noqa: F401
|
|
377
|
+
_rewrite_websocket_suppressed_duplicate_tool_call_completion_event, # noqa: F401
|
|
378
|
+
_sanitize_websocket_connect_failure, # noqa: F401
|
|
379
|
+
_sanitize_websocket_previous_response_error, # noqa: F401
|
|
380
|
+
_sanitize_websocket_terminal_error_fields, # noqa: F401
|
|
381
|
+
_serialize_websocket_error_event, # noqa: F401
|
|
382
|
+
_trim_websocket_previous_response_input_items, # noqa: F401
|
|
383
|
+
_upstream_websocket_disconnect_message, # noqa: F401
|
|
384
|
+
_websocket_auth_failure_permanent_code, # noqa: F401
|
|
385
|
+
_websocket_auth_failure_requires_reauth, # noqa: F401
|
|
386
|
+
_websocket_auth_request_can_switch_account, # noqa: F401
|
|
387
|
+
_websocket_client_previous_response_full_resend_is_retry_safe, # noqa: F401
|
|
388
|
+
_websocket_connect_deadline, # noqa: F401
|
|
389
|
+
_websocket_continuity_anchor_for_payload, # noqa: F401
|
|
390
|
+
_websocket_continuity_error_fields, # noqa: F401
|
|
391
|
+
_websocket_continuity_response_ids, # noqa: F401
|
|
392
|
+
_websocket_downstream_response_id, # noqa: F401
|
|
393
|
+
_websocket_event_error_code, # noqa: F401
|
|
394
|
+
_websocket_event_error_message, # noqa: F401
|
|
395
|
+
_websocket_event_error_param, # noqa: F401
|
|
396
|
+
_websocket_event_error_payload, # noqa: F401
|
|
397
|
+
_websocket_event_error_type, # noqa: F401
|
|
398
|
+
_websocket_full_resend_conflicts_with_visible_pending, # noqa: F401
|
|
399
|
+
_websocket_input_item_type, # noqa: F401
|
|
400
|
+
_websocket_owner_pinned_quota_error_code, # noqa: F401
|
|
401
|
+
_websocket_precreated_auth_error_code, # noqa: F401
|
|
402
|
+
_websocket_precreated_retry_error_code, # noqa: F401
|
|
403
|
+
_websocket_receive_timeout_for_pending_requests, # noqa: F401
|
|
404
|
+
_websocket_response_id, # noqa: F401
|
|
405
|
+
_websocket_top_level_error_payload, # noqa: F401
|
|
406
|
+
_wrapped_websocket_error_event, # noqa: F401
|
|
407
|
+
)
|
|
408
|
+
from app.modules.proxy.affinity import (
|
|
409
|
+
_sticky_key_from_session_header, # noqa: F401
|
|
410
|
+
)
|
|
411
|
+
from app.modules.proxy.durable_bridge_coordinator import (
|
|
412
|
+
DurableBridgeLookup as DurableBridgeLookup,
|
|
413
|
+
)
|
|
414
|
+
from app.modules.proxy.helpers import (
|
|
415
|
+
_normalize_error_code,
|
|
416
|
+
classify_upstream_failure,
|
|
417
|
+
is_account_neutral_safety_policy_rejection,
|
|
418
|
+
is_model_scoped_upstream_rejection,
|
|
419
|
+
is_upstream_model_capacity_error,
|
|
420
|
+
is_upstream_usage_limit_rejection,
|
|
421
|
+
)
|
|
422
|
+
from app.modules.proxy.http_bridge_forwarding import (
|
|
423
|
+
HTTPBridgeForwardContext as HTTPBridgeForwardContext,
|
|
424
|
+
)
|
|
425
|
+
from app.modules.proxy.http_bridge_forwarding import (
|
|
426
|
+
OwnerForwardRelayFailure as OwnerForwardRelayFailure,
|
|
427
|
+
)
|
|
428
|
+
from app.modules.proxy.load_balancer import AccountSelection
|
|
429
|
+
from app.modules.usage.updater import UsageUpdater
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _facade() -> Any:
|
|
433
|
+
return sys.modules["app.modules.proxy.service"]
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _canonical_background_ack(
|
|
437
|
+
event_payload: dict[str, JsonValue] | None,
|
|
438
|
+
event_type: str | None,
|
|
439
|
+
) -> tuple[str, OpenAIResponsePayload] | None:
|
|
440
|
+
if event_type not in {"response.queued", "response.in_progress"}:
|
|
441
|
+
return None
|
|
442
|
+
response = event_payload.get("response") if event_payload is not None else None
|
|
443
|
+
response_id = response.get("id") if isinstance(response, dict) else None
|
|
444
|
+
if not (
|
|
445
|
+
isinstance(response, dict)
|
|
446
|
+
and response.get("object") == "response"
|
|
447
|
+
and isinstance(response_id, str)
|
|
448
|
+
and bool(response_id)
|
|
449
|
+
and response_id == response_id.strip()
|
|
450
|
+
and response.get("status") == event_type.removeprefix("response.")
|
|
451
|
+
and response.get("output") == []
|
|
452
|
+
):
|
|
453
|
+
return None
|
|
454
|
+
# The relayed payload model drops known submodels that fail validation
|
|
455
|
+
# instead of raising, so a malformed `usage` or `error` would otherwise be
|
|
456
|
+
# discarded silently on a response classified as successful.
|
|
457
|
+
payload = OpenAIResponsePayload.model_validate(response)
|
|
458
|
+
if (response.get("usage") is None) != (payload.usage is None):
|
|
459
|
+
return None
|
|
460
|
+
if (response.get("error") is None) != (payload.error is None):
|
|
461
|
+
return None
|
|
462
|
+
return response_id, payload
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _canonical_background_ack_response_id(
|
|
466
|
+
event_payload: dict[str, JsonValue] | None,
|
|
467
|
+
event_type: str | None,
|
|
468
|
+
) -> str | None:
|
|
469
|
+
canonical_ack = _canonical_background_ack(event_payload, event_type)
|
|
470
|
+
return canonical_ack[0] if canonical_ack is not None else None
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _is_background_json_ack(
|
|
474
|
+
stream: bool | None,
|
|
475
|
+
event_payload: dict[str, JsonValue] | None,
|
|
476
|
+
event_type: str | None,
|
|
477
|
+
) -> bool:
|
|
478
|
+
return stream is False and _canonical_background_ack_response_id(event_payload, event_type) is not None
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _publish_http_response_owner(
|
|
482
|
+
proxy: _StreamingServiceProtocol,
|
|
483
|
+
event: OpenAIEvent | None,
|
|
484
|
+
event_payload: dict[str, JsonValue] | None,
|
|
485
|
+
block: str,
|
|
486
|
+
account_id: str,
|
|
487
|
+
api_key: ApiKeyData | None,
|
|
488
|
+
session_id: str | None,
|
|
489
|
+
) -> None:
|
|
490
|
+
if event is None or event.response is None or not event.response.id or event_payload is None:
|
|
491
|
+
return
|
|
492
|
+
if isinstance(block, ParsedSseBlock) and (block.is_local or block.response_id_is_local):
|
|
493
|
+
return
|
|
494
|
+
if event_payload.get(SYNTHETIC_TRANSPORT_FAILURE_MARKER):
|
|
495
|
+
return
|
|
496
|
+
proxy._remember_websocket_previous_response_owner(
|
|
497
|
+
previous_response_id=event.response.id,
|
|
498
|
+
api_key_id=api_key.id if api_key is not None else None,
|
|
499
|
+
account_id=account_id,
|
|
500
|
+
session_id=session_id,
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _settle_background_ack(
|
|
505
|
+
settlement: _StreamSettlement,
|
|
506
|
+
payload: ResponsesRequest,
|
|
507
|
+
event_payload: dict[str, JsonValue] | None,
|
|
508
|
+
response_id: str,
|
|
509
|
+
) -> tuple[bool, str, ResponseUsage | None]:
|
|
510
|
+
"""Treat a canonical background acknowledgement as the terminal event of a `stream: false` request."""
|
|
511
|
+
canonical_ack = (
|
|
512
|
+
_canonical_background_ack(event_payload, classify_event_type(event_payload))
|
|
513
|
+
if payload.stream is False
|
|
514
|
+
else None
|
|
515
|
+
)
|
|
516
|
+
if canonical_ack is None:
|
|
517
|
+
return False, response_id, None
|
|
518
|
+
ack_response_id, ack_payload = canonical_ack
|
|
519
|
+
settlement.response_id = ack_response_id
|
|
520
|
+
return True, ack_response_id, ack_payload.usage
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _stream_iterator_after_capacity_admission(
|
|
524
|
+
stream: AsyncIterator[str],
|
|
525
|
+
) -> AsyncIterator[str]:
|
|
526
|
+
"""Expose a slow stream once local response-create admission has succeeded."""
|
|
527
|
+
_signal_propagated_capacity_startup_ready()
|
|
528
|
+
return stream.__aiter__()
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
_REQUEST_TRANSPORT_HTTP = "http"
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _should_penalize_stream_error(code: str | None, message: str | None = None) -> bool:
|
|
535
|
+
"""Whether this stream failure owes the account a health write.
|
|
536
|
+
|
|
537
|
+
``message`` is the sentence the failure carried, and every caller that
|
|
538
|
+
reads an upstream-authored one passes it: the code table cannot answer for
|
|
539
|
+
the serialized usage-limit rejection, which upstream sends with no error
|
|
540
|
+
code at all. That frame normalizes to ``upstream_error``, which is in
|
|
541
|
+
neither code set, so an account that just said its subscription window is
|
|
542
|
+
spent would be left ACTIVE and handed the next request -- while the
|
|
543
|
+
identical coded frame benches it.
|
|
544
|
+
|
|
545
|
+
Callers whose message is proxy-authored rather than upstream's own (the
|
|
546
|
+
WebSocket terminal-cleanup path, whose codes and sentences are this
|
|
547
|
+
proxy's: ``client_disconnected``, ``stream_incomplete``, an idle timeout)
|
|
548
|
+
pass no message and keep the pure code answer, because there is nothing of
|
|
549
|
+
upstream's there to read.
|
|
550
|
+
"""
|
|
551
|
+
if code is None:
|
|
552
|
+
return False
|
|
553
|
+
should_penalize = (
|
|
554
|
+
code in _facade()._ACCOUNT_RECOVERY_RETRY_CODES
|
|
555
|
+
or code in _facade()._TRANSIENT_RETRY_CODES
|
|
556
|
+
or is_upstream_usage_limit_rejection(error_code=code, message=message)
|
|
557
|
+
)
|
|
558
|
+
if not should_penalize and _is_account_neutral_request_rejection(
|
|
559
|
+
code=code,
|
|
560
|
+
http_status=None,
|
|
561
|
+
message=message,
|
|
562
|
+
):
|
|
563
|
+
_facade().logger.info(
|
|
564
|
+
"Skipped account error penalty for account-neutral request rejection code=%s request_id=%s",
|
|
565
|
+
code,
|
|
566
|
+
get_request_id(),
|
|
567
|
+
)
|
|
568
|
+
return should_penalize
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
_MODEL_CAPACITY_LIMIT_CODES = {
|
|
572
|
+
"rate_limit_exceeded",
|
|
573
|
+
"usage_limit_reached",
|
|
574
|
+
"insufficient_quota",
|
|
575
|
+
"usage_not_included",
|
|
576
|
+
"quota_exceeded",
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def _should_retry_transient_stream_error(
|
|
581
|
+
code: str | None,
|
|
582
|
+
message: str | None,
|
|
583
|
+
*,
|
|
584
|
+
response_id: str | None = None,
|
|
585
|
+
) -> bool:
|
|
586
|
+
if code is None or code == "stream_idle_timeout":
|
|
587
|
+
return False
|
|
588
|
+
if code == PROCESS_NETWORK_UNAVAILABLE_CODE:
|
|
589
|
+
# Serialized terminal events arrive only after upstream dispatch. The
|
|
590
|
+
# stable code keeps settlement account-neutral, but cannot prove that
|
|
591
|
+
# replaying the POST is safe.
|
|
592
|
+
return False
|
|
593
|
+
if response_id is not None:
|
|
594
|
+
return False
|
|
595
|
+
if code in _facade()._TRANSIENT_RETRY_CODES:
|
|
596
|
+
return True
|
|
597
|
+
if is_upstream_model_capacity_error(message):
|
|
598
|
+
if code in _MODEL_CAPACITY_LIMIT_CODES:
|
|
599
|
+
return False
|
|
600
|
+
return True
|
|
601
|
+
if code != "upstream_unavailable" or not message:
|
|
602
|
+
return False
|
|
603
|
+
normalized_message = message.lower()
|
|
604
|
+
if any(marker in normalized_message for marker in _facade()._UPSTREAM_UNAVAILABLE_NON_TRANSIENT_MESSAGE_MARKERS):
|
|
605
|
+
return False
|
|
606
|
+
return any(marker in normalized_message for marker in _facade()._UPSTREAM_UNAVAILABLE_TRANSIENT_MESSAGE_MARKERS)
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def _classify_upstream_close(
|
|
610
|
+
close_code: int | None,
|
|
611
|
+
*,
|
|
612
|
+
response_events_seen: int,
|
|
613
|
+
) -> Literal["clean", "transient"]:
|
|
614
|
+
if close_code == 1000 and response_events_seen == 0:
|
|
615
|
+
# A clean websocket close before response.created does not prove that
|
|
616
|
+
# the request was invalid. The upstream can close a socket during a
|
|
617
|
+
# handoff, so the caller may safely recreate the socket and replay the
|
|
618
|
+
# still-unstarted request once.
|
|
619
|
+
return "clean"
|
|
620
|
+
return "transient"
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
def _is_account_neutral_transport_drop(
|
|
624
|
+
close_code: int | None,
|
|
625
|
+
*,
|
|
626
|
+
response_events_seen: int,
|
|
627
|
+
) -> bool:
|
|
628
|
+
"""Return whether an upstream websocket ending is account-neutral evidence.
|
|
629
|
+
|
|
630
|
+
An abrupt transport drop that carries no close frame and arrived before
|
|
631
|
+
any application-layer response event is the weakest possible evidence of
|
|
632
|
+
account ill-health: the account never spoke at the application layer for
|
|
633
|
+
this request. Charging the account lets a few infrastructure resets push
|
|
634
|
+
it into error backoff and 502 continuity-bound follow-ups while healthy
|
|
635
|
+
pool siblings idle (issue #1754). Any close frame — even a non-clean one —
|
|
636
|
+
is upstream-authored evidence and keeps the existing penalty semantics, as
|
|
637
|
+
does a drop after response events started streaming.
|
|
638
|
+
|
|
639
|
+
Close code 1006 (abnormal closure) is reserved by RFC 6455 and can never
|
|
640
|
+
appear in an actual close frame: adapters synthesize it locally when the
|
|
641
|
+
socket dies without one (aiohttp stores 1006 on ``close_code`` for an
|
|
642
|
+
abnormal CLOSED), so it counts as frame-less here.
|
|
643
|
+
"""
|
|
644
|
+
|
|
645
|
+
return close_code in (None, 1006) and response_events_seen == 0
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def _should_infer_upstream_status_from_proxy_error(exc: ProxyResponseError, upstream_error_code: str | None) -> bool:
|
|
649
|
+
if exc.failure_phase == "status":
|
|
650
|
+
return True
|
|
651
|
+
if exc.failure_phase is not None:
|
|
652
|
+
return False
|
|
653
|
+
return upstream_error_code not in _facade()._LOCAL_PROXY_ERROR_CODES
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def _rewrite_previous_response_stream_error(
|
|
657
|
+
*,
|
|
658
|
+
previous_response_id: str | None,
|
|
659
|
+
preferred_account_id: str | None,
|
|
660
|
+
error_code: str | None,
|
|
661
|
+
error_type: str | None,
|
|
662
|
+
error_message: str | None,
|
|
663
|
+
error_param: OpenAIErrorParam | JsonValue,
|
|
664
|
+
) -> tuple[str, str, str | None] | None:
|
|
665
|
+
if previous_response_id is None:
|
|
666
|
+
return None
|
|
667
|
+
if _facade()._is_previous_response_not_found_error(
|
|
668
|
+
code=error_code,
|
|
669
|
+
param=error_param,
|
|
670
|
+
message=error_message,
|
|
671
|
+
):
|
|
672
|
+
_record_continuity_fail_closed(
|
|
673
|
+
surface="http_stream",
|
|
674
|
+
reason="previous_response_not_found",
|
|
675
|
+
previous_response_id=previous_response_id,
|
|
676
|
+
upstream_error_code=error_code,
|
|
677
|
+
)
|
|
678
|
+
return (
|
|
679
|
+
"stream_incomplete",
|
|
680
|
+
PREVIOUS_RESPONSE_STREAM_INCOMPLETE_MESSAGE,
|
|
681
|
+
None,
|
|
682
|
+
)
|
|
683
|
+
if _facade()._is_missing_tool_output_error(
|
|
684
|
+
code=error_code,
|
|
685
|
+
param=error_param,
|
|
686
|
+
message=error_message,
|
|
687
|
+
):
|
|
688
|
+
_record_continuity_fail_closed(
|
|
689
|
+
surface="http_stream",
|
|
690
|
+
reason="missing_tool_output",
|
|
691
|
+
previous_response_id=previous_response_id,
|
|
692
|
+
upstream_error_code=error_code,
|
|
693
|
+
)
|
|
694
|
+
return (
|
|
695
|
+
"stream_incomplete",
|
|
696
|
+
PREVIOUS_RESPONSE_STREAM_INCOMPLETE_MESSAGE,
|
|
697
|
+
None,
|
|
698
|
+
)
|
|
699
|
+
if _facade()._is_previous_response_not_found_public_shape(
|
|
700
|
+
code=error_code,
|
|
701
|
+
param=error_param,
|
|
702
|
+
message=error_message,
|
|
703
|
+
):
|
|
704
|
+
# Preserve masking when malformed ``param`` metadata makes the
|
|
705
|
+
# recovery classifier fail closed. This branch never authorizes
|
|
706
|
+
# replay or account switching.
|
|
707
|
+
_record_continuity_fail_closed(
|
|
708
|
+
surface="http_stream",
|
|
709
|
+
reason=PREVIOUS_RESPONSE_MALFORMED_PARAM_REASON,
|
|
710
|
+
previous_response_id=previous_response_id,
|
|
711
|
+
upstream_error_code=error_code,
|
|
712
|
+
)
|
|
713
|
+
return (
|
|
714
|
+
"stream_incomplete",
|
|
715
|
+
PREVIOUS_RESPONSE_STREAM_INCOMPLETE_MESSAGE,
|
|
716
|
+
None,
|
|
717
|
+
)
|
|
718
|
+
normalized_code = _normalize_error_code(error_code, error_type)
|
|
719
|
+
if preferred_account_id is not None and normalized_code in _facade()._ACCOUNT_RECOVERY_RETRY_CODES:
|
|
720
|
+
_record_continuity_fail_closed(
|
|
721
|
+
surface="http_stream",
|
|
722
|
+
reason="owner_account_unavailable",
|
|
723
|
+
previous_response_id=previous_response_id,
|
|
724
|
+
upstream_error_code=normalized_code,
|
|
725
|
+
)
|
|
726
|
+
return (
|
|
727
|
+
"previous_response_owner_unavailable",
|
|
728
|
+
"Previous response owner account is unavailable; retry later.",
|
|
729
|
+
normalized_code,
|
|
730
|
+
)
|
|
731
|
+
return None
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
def _raw_stream_error_fields(
|
|
735
|
+
event_type: str | None,
|
|
736
|
+
event_payload: dict[str, JsonValue] | None,
|
|
737
|
+
) -> tuple[str | None, str | None, OpenAIErrorParam | None, str]:
|
|
738
|
+
raw_error_type = _websocket_event_error_type(event_type, event_payload)
|
|
739
|
+
raw_error_message = _websocket_event_error_message(event_type, event_payload)
|
|
740
|
+
raw_error_param = _websocket_event_error_param(event_type, event_payload)
|
|
741
|
+
return (
|
|
742
|
+
raw_error_type,
|
|
743
|
+
raw_error_message,
|
|
744
|
+
raw_error_param,
|
|
745
|
+
_normalize_error_code(_websocket_event_error_code(event_type, event_payload), raw_error_type),
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
def _openai_error_fields(
|
|
750
|
+
error: OpenAIError | None,
|
|
751
|
+
) -> tuple[str | None, str | None, OpenAIErrorParam | None]:
|
|
752
|
+
if error is None:
|
|
753
|
+
return None, None, None
|
|
754
|
+
return error.type, error.message, error.param_state
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def _raw_stream_error_code_or_upstream(
|
|
758
|
+
event_type: str | None,
|
|
759
|
+
event_payload: dict[str, JsonValue] | None,
|
|
760
|
+
error_code: str,
|
|
761
|
+
) -> str:
|
|
762
|
+
if (
|
|
763
|
+
event_type == "error"
|
|
764
|
+
and error_code == "error"
|
|
765
|
+
and _websocket_event_error_code(event_type, event_payload) is None
|
|
766
|
+
):
|
|
767
|
+
return "upstream_error"
|
|
768
|
+
return error_code
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
def _classify_terminal_stream_error_frame(
|
|
772
|
+
event_type: str | None,
|
|
773
|
+
event_payload: dict[str, JsonValue] | None,
|
|
774
|
+
error_code: str,
|
|
775
|
+
error_message: str | None,
|
|
776
|
+
) -> str:
|
|
777
|
+
"""Resolve a terminal frame's error code and record its observability in one step.
|
|
778
|
+
|
|
779
|
+
``streaming/mixin.py`` sits at its line ceiling, so the two parsed
|
|
780
|
+
terminal-frame sites resolve the code (``_raw_stream_error_code_or_upstream``)
|
|
781
|
+
and observe the frame (``_observe_terminal_stream_error_frame``) through
|
|
782
|
+
this single call instead of one statement each.
|
|
783
|
+
"""
|
|
784
|
+
resolved_code = _raw_stream_error_code_or_upstream(event_type, event_payload, error_code)
|
|
785
|
+
_observe_terminal_stream_error_frame(resolved_code, error_message)
|
|
786
|
+
return resolved_code
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def _mark_stream_settlement_interrupted(
|
|
790
|
+
settlement: _StreamSettlement,
|
|
791
|
+
*,
|
|
792
|
+
status: str,
|
|
793
|
+
error_code: str,
|
|
794
|
+
error_message: str,
|
|
795
|
+
failure_phase: Literal["upstream", "downstream"],
|
|
796
|
+
failure_detail: str,
|
|
797
|
+
account_health_error: bool,
|
|
798
|
+
) -> tuple[str, str, str, _RequestLogFailureMetadata]:
|
|
799
|
+
settlement.record_success = False
|
|
800
|
+
settlement.account_health_error = account_health_error
|
|
801
|
+
settlement.error = {"message": error_message}
|
|
802
|
+
return (
|
|
803
|
+
status,
|
|
804
|
+
error_code,
|
|
805
|
+
error_message,
|
|
806
|
+
_RequestLogFailureMetadata(failure_phase=failure_phase, failure_detail=failure_detail),
|
|
807
|
+
)
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
def _stamp_terminal(settlement: _StreamSettlement, event_type: str | None, clock: Clock) -> bool:
|
|
811
|
+
"""Return whether ``event_type`` is an upstream terminal frame, stamping its parse instant on the settlement.
|
|
812
|
+
|
|
813
|
+
Called at the HTTP stream's terminal-detection sites before the frame is
|
|
814
|
+
yielded downstream, so the throughput cohort sample's span ends when the
|
|
815
|
+
upstream finished generating, not when a slow downstream consumer drained
|
|
816
|
+
the frame or the upstream connection finally closed.
|
|
817
|
+
"""
|
|
818
|
+
if event_type not in {"response.completed", "response.failed", "response.incomplete", "error"}:
|
|
819
|
+
return False
|
|
820
|
+
settlement.upstream_terminal_at = clock.monotonic()
|
|
821
|
+
return True
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
def _upstream_terminal_latency_ms(settlement: _StreamSettlement, started_at: float) -> int | None:
|
|
825
|
+
"""Attempt-start-to-upstream-terminal latency for the request-log funnel; ``None`` without a terminal stamp."""
|
|
826
|
+
if settlement.upstream_terminal_at is None:
|
|
827
|
+
return None
|
|
828
|
+
return max(0, int((settlement.upstream_terminal_at - started_at) * 1000))
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
def _mark_upstream_stream_incomplete(
|
|
832
|
+
settlement: _StreamSettlement,
|
|
833
|
+
) -> tuple[str, str, str, _RequestLogFailureMetadata]:
|
|
834
|
+
return _mark_stream_settlement_interrupted(
|
|
835
|
+
settlement,
|
|
836
|
+
status="error",
|
|
837
|
+
error_code="stream_incomplete",
|
|
838
|
+
error_message="Upstream stream ended before response.completed",
|
|
839
|
+
failure_phase="upstream",
|
|
840
|
+
failure_detail="upstream_eof_before_terminal_event",
|
|
841
|
+
account_health_error=True,
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
def _mark_downstream_stream_cancelled(
|
|
846
|
+
settlement: _StreamSettlement,
|
|
847
|
+
) -> tuple[str, str, str, _RequestLogFailureMetadata]:
|
|
848
|
+
return _mark_stream_settlement_interrupted(
|
|
849
|
+
settlement,
|
|
850
|
+
status="cancelled",
|
|
851
|
+
error_code="client_disconnected",
|
|
852
|
+
error_message="Downstream client disconnected before response.completed",
|
|
853
|
+
failure_phase="downstream",
|
|
854
|
+
failure_detail="client_disconnected_before_terminal_event",
|
|
855
|
+
account_health_error=False,
|
|
856
|
+
)
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
def _rewrite_malformed_stream_error_event(
|
|
860
|
+
*,
|
|
861
|
+
enforce_openai_sdk_contract: bool,
|
|
862
|
+
event: OpenAIEvent | None,
|
|
863
|
+
event_type: str | None,
|
|
864
|
+
event_payload: dict[str, JsonValue] | None,
|
|
865
|
+
response_id: str,
|
|
866
|
+
) -> tuple[str, OpenAIEvent | None, dict[str, JsonValue] | None, str | None] | None:
|
|
867
|
+
"""Rewrite a schema-less upstream ``error`` frame under the SDK contract.
|
|
868
|
+
|
|
869
|
+
A malformed frame like ``{"type":"error","message":"..."}`` classifies as
|
|
870
|
+
``error`` but carries no error envelope (``event`` is None or has no
|
|
871
|
+
``error``), so it must become a terminal ``response.failed`` instead of
|
|
872
|
+
leaking the raw frame with a success settlement. Returns None when the
|
|
873
|
+
frame is not a malformed error (well-formed errors keep their
|
|
874
|
+
envelope-driven handling).
|
|
875
|
+
"""
|
|
876
|
+
if not enforce_openai_sdk_contract or event_type != "error":
|
|
877
|
+
return None
|
|
878
|
+
if (event is not None and event.error is not None) or not isinstance(event_payload, dict):
|
|
879
|
+
return None
|
|
880
|
+
message_value = event_payload.get("message")
|
|
881
|
+
message = message_value.strip() if isinstance(message_value, str) and message_value.strip() else "Upstream error"
|
|
882
|
+
return _build_rewritten_stream_response_failed_event(
|
|
883
|
+
response_id=response_id,
|
|
884
|
+
error_code="upstream_error",
|
|
885
|
+
error_message=message,
|
|
886
|
+
)
|
|
887
|
+
|
|
888
|
+
|
|
889
|
+
def _build_rewritten_stream_response_failed_event(
|
|
890
|
+
*,
|
|
891
|
+
response_id: str,
|
|
892
|
+
error_code: str,
|
|
893
|
+
error_message: str,
|
|
894
|
+
) -> tuple[str, OpenAIEvent | None, dict[str, JsonValue] | None, str | None]:
|
|
895
|
+
rewritten_event_payload = response_failed_event(
|
|
896
|
+
error_code,
|
|
897
|
+
error_message,
|
|
898
|
+
error_type="server_error",
|
|
899
|
+
response_id=response_id,
|
|
900
|
+
)
|
|
901
|
+
if error_code in SYNTHETIC_TRANSPORT_FAILURE_CODES:
|
|
902
|
+
rewritten_event_payload = synthetic_transport_failure_event(rewritten_event_payload)
|
|
903
|
+
rewritten_event_block = format_sse_event(rewritten_event_payload)
|
|
904
|
+
rewritten_payload = parse_sse_data_json(rewritten_event_block)
|
|
905
|
+
rewritten_event = parse_sse_event(rewritten_event_block)
|
|
906
|
+
rewritten_event_type = _event_type_from_payload(rewritten_event, rewritten_payload)
|
|
907
|
+
return rewritten_event_block, rewritten_event, rewritten_payload, rewritten_event_type
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
def _stream_transport_failure_event_or_raise(
|
|
911
|
+
error_code: str,
|
|
912
|
+
error_message: str,
|
|
913
|
+
*,
|
|
914
|
+
response_id: str,
|
|
915
|
+
preserve_native_failure_lifecycle: bool,
|
|
916
|
+
) -> str:
|
|
917
|
+
if preserve_native_failure_lifecycle:
|
|
918
|
+
raise ProxyResponseError(
|
|
919
|
+
502,
|
|
920
|
+
openai_error(error_code, error_message),
|
|
921
|
+
failure_phase="upstream",
|
|
922
|
+
)
|
|
923
|
+
return format_sse_event(synthetic_stream_failure_event(error_code, error_message, response_id=response_id))
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
def _build_stream_incomplete_terminal_event_for_request(
|
|
927
|
+
request_state: _WebSocketRequestState,
|
|
928
|
+
*,
|
|
929
|
+
reason: str = "stream_incomplete",
|
|
930
|
+
) -> tuple[str, str, OpenAIEvent | None, dict[str, JsonValue] | None, str | None]:
|
|
931
|
+
error_code, error_message = _websocket_continuity_error_fields(
|
|
932
|
+
reason=reason,
|
|
933
|
+
expose_stale_previous_response_classifier=request_state.expose_stale_previous_response_classifier,
|
|
934
|
+
)
|
|
935
|
+
event_block, event, payload, event_type = _build_rewritten_stream_response_failed_event(
|
|
936
|
+
response_id=_websocket_downstream_response_id(request_state),
|
|
937
|
+
error_code=error_code,
|
|
938
|
+
error_message=error_message,
|
|
939
|
+
)
|
|
940
|
+
if payload is None: # pragma: no cover - formatter/parser contract
|
|
941
|
+
raise RuntimeError("rewritten stream failure event did not contain a JSON payload")
|
|
942
|
+
downstream_text = json.dumps(
|
|
943
|
+
payload,
|
|
944
|
+
ensure_ascii=True,
|
|
945
|
+
separators=(",", ":"),
|
|
946
|
+
)
|
|
947
|
+
return downstream_text, event_block, event, payload, event_type
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
def _slim_response_create_payload_for_upstream(
|
|
951
|
+
payload: dict[str, JsonValue],
|
|
952
|
+
*,
|
|
953
|
+
max_bytes: int,
|
|
954
|
+
protected_agent_control_output_occurrences: Mapping[tuple[str, str], tuple[bool, ...]] | None = None,
|
|
955
|
+
) -> tuple[dict[str, JsonValue], dict[str, int] | None]:
|
|
956
|
+
input_value = payload.get("input")
|
|
957
|
+
if not isinstance(input_value, list) or not input_value:
|
|
958
|
+
return payload, None
|
|
959
|
+
|
|
960
|
+
input_items = cast(list[JsonValue], deepcopy(input_value))
|
|
961
|
+
preserve_from = _facade()._response_create_recent_suffix_start(input_items)
|
|
962
|
+
historical = input_items[:preserve_from]
|
|
963
|
+
recent = input_items[preserve_from:]
|
|
964
|
+
|
|
965
|
+
tool_outputs_slimmed = 0
|
|
966
|
+
images_slimmed = 0
|
|
967
|
+
if protected_agent_control_output_occurrences is None:
|
|
968
|
+
protected_agent_control_output_occurrences = _agent_control_tool_output_occurrences(historical)
|
|
969
|
+
agent_control_output_counts: dict[tuple[str, str], int] = {}
|
|
970
|
+
|
|
971
|
+
slimmed_historical: list[JsonValue] = []
|
|
972
|
+
for item in historical:
|
|
973
|
+
(
|
|
974
|
+
slimmed_item,
|
|
975
|
+
item_tool_outputs_slimmed,
|
|
976
|
+
item_images_slimmed,
|
|
977
|
+
) = _facade()._slim_historical_response_input_item(
|
|
978
|
+
item,
|
|
979
|
+
protected_agent_control_output_occurrences=protected_agent_control_output_occurrences,
|
|
980
|
+
agent_control_output_counts=agent_control_output_counts,
|
|
981
|
+
)
|
|
982
|
+
tool_outputs_slimmed += item_tool_outputs_slimmed
|
|
983
|
+
images_slimmed += item_images_slimmed
|
|
984
|
+
slimmed_historical.append(slimmed_item)
|
|
985
|
+
|
|
986
|
+
candidate_payload = dict(payload)
|
|
987
|
+
candidate_payload["input"] = slimmed_historical + recent
|
|
988
|
+
|
|
989
|
+
if tool_outputs_slimmed == 0 and images_slimmed == 0:
|
|
990
|
+
return payload, None
|
|
991
|
+
|
|
992
|
+
return candidate_payload, {
|
|
993
|
+
"historical_tool_outputs_slimmed": tool_outputs_slimmed,
|
|
994
|
+
"historical_images_slimmed": images_slimmed,
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
def _call_stream_with_supported_optional_kwargs(
|
|
999
|
+
func: Callable[..., AsyncIterator[str]],
|
|
1000
|
+
/,
|
|
1001
|
+
*args: Any,
|
|
1002
|
+
optional_kwargs: Mapping[str, Any],
|
|
1003
|
+
**required_kwargs: Any,
|
|
1004
|
+
) -> AsyncIterator[str]:
|
|
1005
|
+
return func(*args, **_facade()._supported_optional_kwargs(func, optional_kwargs, required_kwargs))
|
|
1006
|
+
|
|
1007
|
+
|
|
1008
|
+
def _stream_request_budget_seconds(settings: object, *, request_transport: str) -> float:
|
|
1009
|
+
del request_transport
|
|
1010
|
+
budget = getattr(settings, "http_responses_stream_request_budget_seconds", None)
|
|
1011
|
+
if budget is not None:
|
|
1012
|
+
return float(budget)
|
|
1013
|
+
return float(getattr(settings, "proxy_request_budget_seconds"))
|
|
1014
|
+
|
|
1015
|
+
|
|
1016
|
+
async def _resolve_upstream_route_for_account(
|
|
1017
|
+
proxy: Any,
|
|
1018
|
+
account: Account,
|
|
1019
|
+
*,
|
|
1020
|
+
operation: str,
|
|
1021
|
+
) -> ResolvedUpstreamRoute | None:
|
|
1022
|
+
# Outcomes (route / direct-egress None / fail-closed error) are cached
|
|
1023
|
+
# verbatim, so a hit can never change the degradation path the resolver
|
|
1024
|
+
# chose; errors re-raise with their original reason.
|
|
1025
|
+
cache = get_upstream_route_cache()
|
|
1026
|
+
cached = cache.get(account.id)
|
|
1027
|
+
if cached is not None:
|
|
1028
|
+
return cached.unwrap(account.id)
|
|
1029
|
+
generation = cache.generation
|
|
1030
|
+
async with _facade().SessionLocal() as session:
|
|
1031
|
+
try:
|
|
1032
|
+
route = await _facade().resolve_upstream_route(
|
|
1033
|
+
session,
|
|
1034
|
+
account_id=account.id,
|
|
1035
|
+
operation=operation,
|
|
1036
|
+
scope="account",
|
|
1037
|
+
encryptor=proxy._encryptor,
|
|
1038
|
+
)
|
|
1039
|
+
except UpstreamProxyRouteError as exc:
|
|
1040
|
+
cache.store_error(account.id, exc, generation=generation)
|
|
1041
|
+
raise
|
|
1042
|
+
cache.store_route(account.id, route, generation=generation)
|
|
1043
|
+
return route
|
|
1044
|
+
|
|
1045
|
+
|
|
1046
|
+
async def _select_account_with_budget_for_stream(proxy: Any, deadline: float, **kwargs: Any) -> AccountSelection:
|
|
1047
|
+
selector = proxy._select_account_with_budget_compatible
|
|
1048
|
+
optional_kwargs = (
|
|
1049
|
+
"require_security_work_authorized",
|
|
1050
|
+
"lease_kind",
|
|
1051
|
+
"estimated_lease_tokens",
|
|
1052
|
+
"fallback_on_preferred_account_unavailable",
|
|
1053
|
+
"preferred_account_is_continuity_owner",
|
|
1054
|
+
"preferred_account_overrides_single_account_routing",
|
|
1055
|
+
"spill_bare_session_on_account_cap",
|
|
1056
|
+
"require_unambiguous_account",
|
|
1057
|
+
)
|
|
1058
|
+
if any(name in kwargs for name in optional_kwargs):
|
|
1059
|
+
try:
|
|
1060
|
+
signature = inspect.signature(selector)
|
|
1061
|
+
except (TypeError, ValueError):
|
|
1062
|
+
signature = None
|
|
1063
|
+
accepts_var_keyword = signature is not None and any(
|
|
1064
|
+
parameter.kind is inspect.Parameter.VAR_KEYWORD for parameter in signature.parameters.values()
|
|
1065
|
+
)
|
|
1066
|
+
if signature is not None and not accepts_var_keyword:
|
|
1067
|
+
kwargs = dict(kwargs)
|
|
1068
|
+
for name in optional_kwargs:
|
|
1069
|
+
if name not in signature.parameters:
|
|
1070
|
+
kwargs.pop(name, None)
|
|
1071
|
+
return await selector(deadline, **kwargs)
|
|
1072
|
+
|
|
1073
|
+
|
|
1074
|
+
def _is_account_neutral_request_rejection(
|
|
1075
|
+
*,
|
|
1076
|
+
code: str,
|
|
1077
|
+
http_status: int | None,
|
|
1078
|
+
message: str | None,
|
|
1079
|
+
) -> bool:
|
|
1080
|
+
"""Return whether upstream rejected the request payload, not the account.
|
|
1081
|
+
|
|
1082
|
+
An account-neutral request rejection reproduces identically on every
|
|
1083
|
+
account, so it must never mutate one account's health: otherwise a single
|
|
1084
|
+
client looping on a rejected conversation drives its serving accounts into
|
|
1085
|
+
``error_count`` backoff and starves unrelated tenants.
|
|
1086
|
+
|
|
1087
|
+
Keep this set narrow: membership is decided by the specific classified
|
|
1088
|
+
code and message, never by a broad HTTP status alone. The model-entitlement
|
|
1089
|
+
rejection is deliberately not a member -- it is handled by
|
|
1090
|
+
``_is_model_scoped_rejection`` below, which likewise keeps the account's
|
|
1091
|
+
health untouched but still lets failover try accounts whose entitlements
|
|
1092
|
+
may differ.
|
|
1093
|
+
"""
|
|
1094
|
+
if http_status is not None and http_status != 400:
|
|
1095
|
+
return False
|
|
1096
|
+
if is_account_neutral_safety_policy_rejection(code=code, http_status=http_status, message=message):
|
|
1097
|
+
return True
|
|
1098
|
+
return code == "invalid_request_error" and bool(_facade()._is_missing_tool_output_message(message))
|
|
1099
|
+
|
|
1100
|
+
|
|
1101
|
+
def _is_model_scoped_rejection(
|
|
1102
|
+
*,
|
|
1103
|
+
http_status: int | None,
|
|
1104
|
+
message: str | None,
|
|
1105
|
+
) -> bool:
|
|
1106
|
+
"""Return whether upstream rejected the requested model, not the account.
|
|
1107
|
+
|
|
1108
|
+
The ChatGPT model-entitlement rejection is scoped to the model named in the
|
|
1109
|
+
message. It proves nothing about the account's ability to serve the models
|
|
1110
|
+
it *is* entitled to, so it must never move that account's ``error_count``:
|
|
1111
|
+
otherwise one client looping on a model no account can serve drives every
|
|
1112
|
+
serving account into ``ERROR_BACKOFF_THRESHOLD`` backoff and starves all
|
|
1113
|
+
unrelated traffic on those accounts -- the exact failure mode when a model
|
|
1114
|
+
reaches subscription selection because its OpenAI-compatible model source
|
|
1115
|
+
is disabled or unreachable.
|
|
1116
|
+
|
|
1117
|
+
Failover is deliberately untouched: the classified failure is still
|
|
1118
|
+
returned, so the caller keeps trying other accounts, whose entitlements may
|
|
1119
|
+
differ.
|
|
1120
|
+
|
|
1121
|
+
The normalized code is not part of the match. Upstream delivers this
|
|
1122
|
+
rejection with neither ``code`` nor ``type`` on the streaming path, which
|
|
1123
|
+
normalizes to ``upstream_error``; on other paths it arrives as
|
|
1124
|
+
``invalid_request_error``. Only the exact message shape decides membership.
|
|
1125
|
+
"""
|
|
1126
|
+
if http_status is not None and http_status != 400:
|
|
1127
|
+
return False
|
|
1128
|
+
return is_model_scoped_upstream_rejection(message)
|
|
1129
|
+
|
|
1130
|
+
|
|
1131
|
+
def _request_usage_refresh(proxy: Any, account_id: str) -> None:
|
|
1132
|
+
"""Schedule a tracked, coalesced usage refresh after a streamed ``usage_limit_reached``.
|
|
1133
|
+
|
|
1134
|
+
``mark_rate_limit`` persists status only, while the pool-exhaustion
|
|
1135
|
+
predicate also needs a >= 100 % usage row that would otherwise wait for
|
|
1136
|
+
the next scheduler tick. The refresh runs on its own background session
|
|
1137
|
+
and never touches this request's ``Account``.
|
|
1138
|
+
"""
|
|
1139
|
+
schedule = getattr(proxy, "_schedule_cancel_safe_cleanup", None)
|
|
1140
|
+
if schedule is None:
|
|
1141
|
+
return
|
|
1142
|
+
refresh = UsageUpdater.request_refresh(account_id)
|
|
1143
|
+
if refresh is None:
|
|
1144
|
+
return
|
|
1145
|
+
schedule(refresh, action="request_usage_refresh", request_id=get_request_id() or "unknown")
|
|
1146
|
+
|
|
1147
|
+
|
|
1148
|
+
async def _handle_stream_error(
|
|
1149
|
+
proxy: Any,
|
|
1150
|
+
account: Account,
|
|
1151
|
+
error: UpstreamError,
|
|
1152
|
+
code: str,
|
|
1153
|
+
http_status: int | None = None,
|
|
1154
|
+
*,
|
|
1155
|
+
privacy_policy: CodexControlRequestPrivacyPolicy = CodexControlRequestPrivacyPolicy.STANDARD,
|
|
1156
|
+
retry_after_seconds: float | None = None,
|
|
1157
|
+
burst_cooldown_recorded: bool = False,
|
|
1158
|
+
upstream_http_status: int | None = None,
|
|
1159
|
+
) -> ClassifiedFailure:
|
|
1160
|
+
"""Write account health for a stream failure and return its classification.
|
|
1161
|
+
|
|
1162
|
+
``burst_cooldown_recorded`` is set by a keyed stream whose health write was
|
|
1163
|
+
deferred until after usage settlement: the replica-local burst cooldown
|
|
1164
|
+
was already engaged at rejection time, so the deferred write must not
|
|
1165
|
+
re-engage it (that would bench an account that has since succeeded).
|
|
1166
|
+
|
|
1167
|
+
``upstream_http_status`` is evidence-only: it lets a caller that knows the
|
|
1168
|
+
upstream HTTP status of this failure say so WITHOUT taking any of the
|
|
1169
|
+
``http_status`` side effects. It is read by the soft-overload gate below
|
|
1170
|
+
and by nothing else -- never by ``classify_upstream_failure``, the
|
|
1171
|
+
reasoning-replay metric, the account-neutral / model-scoped rejection
|
|
1172
|
+
predicates, or the HTTP 429 burst-cooldown branch. Callers that want those
|
|
1173
|
+
behaviours must pass the positional ``http_status`` instead.
|
|
1174
|
+
"""
|
|
1175
|
+
classified = classify_upstream_failure(
|
|
1176
|
+
error_code=code,
|
|
1177
|
+
error=error,
|
|
1178
|
+
http_status=http_status,
|
|
1179
|
+
phase="first_event",
|
|
1180
|
+
)
|
|
1181
|
+
# Terminal frames are counted where they are classified
|
|
1182
|
+
# (``_observe_terminal_stream_error_frame``); only HTTP status rejections
|
|
1183
|
+
# reach the counter from here, so a failure is never counted twice.
|
|
1184
|
+
if http_status is not None and _is_reasoning_replay_rejection(
|
|
1185
|
+
code=code, http_status=http_status, message=error.get("message")
|
|
1186
|
+
):
|
|
1187
|
+
_record_upstream_reasoning_replay_rejection()
|
|
1188
|
+
if _facade()._is_account_neutral_error_code(code):
|
|
1189
|
+
return classified
|
|
1190
|
+
if _is_account_neutral_request_rejection(
|
|
1191
|
+
code=code,
|
|
1192
|
+
http_status=http_status,
|
|
1193
|
+
message=error.get("message"),
|
|
1194
|
+
):
|
|
1195
|
+
_facade().logger.info(
|
|
1196
|
+
"Skipped account error penalty for account-neutral request rejection account_id=%s request_id=%s code=%s",
|
|
1197
|
+
"<redacted>" if privacy_policy.redacts_sensitive_details else account.id,
|
|
1198
|
+
get_request_id(),
|
|
1199
|
+
code,
|
|
1200
|
+
)
|
|
1201
|
+
return classified
|
|
1202
|
+
if _is_model_scoped_rejection(
|
|
1203
|
+
http_status=http_status,
|
|
1204
|
+
message=error.get("message"),
|
|
1205
|
+
):
|
|
1206
|
+
_facade().logger.info(
|
|
1207
|
+
"Skipped account error penalty for model-scoped upstream rejection account_id=%s request_id=%s code=%s",
|
|
1208
|
+
"<redacted>" if privacy_policy.redacts_sensitive_details else account.id,
|
|
1209
|
+
get_request_id(),
|
|
1210
|
+
code,
|
|
1211
|
+
)
|
|
1212
|
+
return classified
|
|
1213
|
+
if classified["failure_class"] == "rate_limit":
|
|
1214
|
+
await proxy._load_balancer.mark_rate_limit(account, error)
|
|
1215
|
+
# The refresh is owed to the account whose window is spent, and the
|
|
1216
|
+
# literal code is the one piece of that evidence upstream omits at will:
|
|
1217
|
+
# the serialized terminal frame asserts the same limit in its message
|
|
1218
|
+
# alone. Reading only the code leaves the pool's usage picture stale for
|
|
1219
|
+
# exactly the accounts it most needs to be current about. Plain
|
|
1220
|
+
# throttling still asks for nothing.
|
|
1221
|
+
if is_upstream_usage_limit_rejection(error_code=code, message=error.get("message")):
|
|
1222
|
+
_request_usage_refresh(proxy, account.id)
|
|
1223
|
+
elif classified["failure_class"] == "quota":
|
|
1224
|
+
await proxy._load_balancer.mark_quota_exceeded(account, error)
|
|
1225
|
+
elif code in PERMANENT_FAILURE_CODES:
|
|
1226
|
+
await proxy._load_balancer.mark_permanent_failure(account, code)
|
|
1227
|
+
else:
|
|
1228
|
+
await proxy._load_balancer.record_error(account)
|
|
1229
|
+
_facade().logger.info(
|
|
1230
|
+
"Recorded transient account error account_id=%s request_id=%s code=%s",
|
|
1231
|
+
"<redacted>" if privacy_policy.redacts_sensitive_details else account.id,
|
|
1232
|
+
get_request_id(),
|
|
1233
|
+
code,
|
|
1234
|
+
)
|
|
1235
|
+
hard_overload = code in UPSTREAM_OVERLOAD_CODES
|
|
1236
|
+
# A bare ``server_error`` *stream terminal* is the same observable
|
|
1237
|
+
# condition as an explicit overload: upstream admitted the turn and then
|
|
1238
|
+
# refused to run it. The terminal shape is what makes it an admission
|
|
1239
|
+
# rejection, so it is keyed on the absence of an HTTP status. A coded
|
|
1240
|
+
# HTTP failure carrying the same string is an ordinary transient error
|
|
1241
|
+
# (and an HTTP 429 is a burst rejection with its own cooldown branch
|
|
1242
|
+
# below); neither may deprioritize an otherwise healthy account.
|
|
1243
|
+
#
|
|
1244
|
+
# Two status keywords are checked because several callers KNOW the
|
|
1245
|
+
# upstream status yet deliberately do not forward it as ``http_status``:
|
|
1246
|
+
# doing so would also flip the account-neutral / model-scoped predicates
|
|
1247
|
+
# above from "skip the penalty" to "record it", arm the 429 branch below,
|
|
1248
|
+
# and double-count the reasoning-replay metric for a frame already
|
|
1249
|
+
# counted at ``_observe_terminal_stream_error_frame``. They pass
|
|
1250
|
+
# ``upstream_http_status`` instead, which gates this window only. A
|
|
1251
|
+
# status from EITHER keyword means the failure was not a status-less
|
|
1252
|
+
# terminal and so must stay out of the soft window.
|
|
1253
|
+
soft_overload = code in UPSTREAM_SOFT_OVERLOAD_CODES and http_status is None and upstream_http_status is None
|
|
1254
|
+
if hard_overload or soft_overload:
|
|
1255
|
+
# Overload is an admission rejection that successes on the same
|
|
1256
|
+
# account's warm sessions keep masking from ``error_count``; feed
|
|
1257
|
+
# the dedicated sliding window so fresh selection can deprioritize.
|
|
1258
|
+
# Soft observations count at a fractional weight, so a sustained
|
|
1259
|
+
# refusal still trips the window while a lone fault never does.
|
|
1260
|
+
await record_upstream_overload(
|
|
1261
|
+
proxy._load_balancer,
|
|
1262
|
+
account,
|
|
1263
|
+
redact_account_id=privacy_policy.redacts_sensitive_details,
|
|
1264
|
+
soft=not hard_overload,
|
|
1265
|
+
)
|
|
1266
|
+
elif http_status == 429 and not burst_cooldown_recorded:
|
|
1267
|
+
# A code-less HTTP 429 (rate_limit / quota classes returned above)
|
|
1268
|
+
# is a per-account burst/concurrency rejection: keyed on the status
|
|
1269
|
+
# so ``server_error`` rewrites and ``detail``-parsed bodies are
|
|
1270
|
+
# covered. Short replica-local cooldown only -- never
|
|
1271
|
+
# ``mark_rate_limit`` / persisted RATE_LIMITED.
|
|
1272
|
+
await record_upstream_burst_rejection(
|
|
1273
|
+
proxy._load_balancer,
|
|
1274
|
+
account,
|
|
1275
|
+
retry_after_seconds=retry_after_seconds,
|
|
1276
|
+
redact_account_id=privacy_policy.redacts_sensitive_details,
|
|
1277
|
+
)
|
|
1278
|
+
return classified
|
|
1279
|
+
|
|
1280
|
+
|
|
1281
|
+
def _push_stream_attempt_timeout_overrides(
|
|
1282
|
+
timeout_seconds: float,
|
|
1283
|
+
) -> tuple[float | None, float | None, float | None]:
|
|
1284
|
+
return _facade().push_stream_timeout_overrides(
|
|
1285
|
+
connect_timeout_seconds=timeout_seconds,
|
|
1286
|
+
idle_timeout_seconds=timeout_seconds,
|
|
1287
|
+
total_timeout_seconds=timeout_seconds,
|
|
1288
|
+
)
|
|
1289
|
+
|
|
1290
|
+
|
|
1291
|
+
def _should_retry_stream_error(code: str, message: str | None = None) -> bool:
|
|
1292
|
+
"""Whether a pre-visible terminal frame may be retried on a sibling account.
|
|
1293
|
+
|
|
1294
|
+
The code allowlist cannot answer this for the serialized form of the
|
|
1295
|
+
usage-limit rejection: upstream sends that frame with no error code, which
|
|
1296
|
+
normalizes to ``upstream_error`` and is in no transport retry list -- so a
|
|
1297
|
+
frame saying the account is spent gets surfaced on the first account while
|
|
1298
|
+
the identical HTTP body walks the pool. The frame's sentence is put through
|
|
1299
|
+
the same predicate the health write beside it uses, because that sentence
|
|
1300
|
+
is the only evidence a status-less, code-less frame carries.
|
|
1301
|
+
"""
|
|
1302
|
+
return code in _facade()._ACCOUNT_RECOVERY_RETRY_CODES or is_upstream_usage_limit_rejection(
|
|
1303
|
+
error_code=code, message=message
|
|
1304
|
+
)
|
|
1305
|
+
|
|
1306
|
+
|
|
1307
|
+
def _upstream_turn_state_from_socket(upstream: UpstreamWebSocket | None) -> str | None:
|
|
1308
|
+
if upstream is None:
|
|
1309
|
+
return None
|
|
1310
|
+
getter = getattr(upstream, "response_header", None)
|
|
1311
|
+
if not callable(getter):
|
|
1312
|
+
return None
|
|
1313
|
+
value = getter("x-codex-turn-state")
|
|
1314
|
+
if not isinstance(value, str):
|
|
1315
|
+
return None
|
|
1316
|
+
stripped = value.strip()
|
|
1317
|
+
return stripped or None
|