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,996 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import inspect
|
|
5
|
+
import json
|
|
6
|
+
import secrets
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Any, Mapping
|
|
9
|
+
from urllib.parse import urlencode
|
|
10
|
+
|
|
11
|
+
import aiohttp
|
|
12
|
+
from aiohttp_socks import ProxyConnector
|
|
13
|
+
from python_socks import ProxyType
|
|
14
|
+
from yarl import URL
|
|
15
|
+
|
|
16
|
+
from app.core.clients.native_egress import (
|
|
17
|
+
NativeEgressClient,
|
|
18
|
+
NativeEgressError,
|
|
19
|
+
NativeEgressRequest,
|
|
20
|
+
NativeEgressTransportError,
|
|
21
|
+
NativeEgressUnavailable,
|
|
22
|
+
NativeSseOptions,
|
|
23
|
+
NativeWebSocketRequest,
|
|
24
|
+
discover_native_egress_client,
|
|
25
|
+
)
|
|
26
|
+
from app.core.resilience.network_recovery import (
|
|
27
|
+
PROCESS_NETWORK_UNAVAILABLE_CODE,
|
|
28
|
+
is_pre_dispatch_connection_failure,
|
|
29
|
+
is_process_network_failure,
|
|
30
|
+
)
|
|
31
|
+
from app.core.upstream_proxy import ResolvedProxyEndpoint, ResolvedUpstreamRoute
|
|
32
|
+
|
|
33
|
+
_RESERVED = frozenset({"akamai", "extra_fp", "impersonate", "ja3", "proxies", "proxy", "proxy_headers"})
|
|
34
|
+
_TLS_TARGET_SCHEMES = frozenset({"https", "wss"})
|
|
35
|
+
_CODEX_SKIP_AUTO_IDENTITY_HEADERS = frozenset({aiohttp.hdrs.ACCEPT, aiohttp.hdrs.ACCEPT_ENCODING})
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class CodexTransportError(RuntimeError):
|
|
39
|
+
"""Credential-safe routed transport failure.
|
|
40
|
+
|
|
41
|
+
``retryable_same_contract`` is provenance, not a synonym for transient:
|
|
42
|
+
callers may replay only when the failing layer proved dispatch never
|
|
43
|
+
happened.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
message: str,
|
|
49
|
+
*,
|
|
50
|
+
status_code: int | None = None,
|
|
51
|
+
error_code: str | None = None,
|
|
52
|
+
retryable_same_contract: bool = False,
|
|
53
|
+
failure_phase: str | None = None,
|
|
54
|
+
is_tls_verification_failure: bool = False,
|
|
55
|
+
handshake_headers: Mapping[str, str] | None = None,
|
|
56
|
+
handshake_message: str | None = None,
|
|
57
|
+
) -> None:
|
|
58
|
+
super().__init__(message)
|
|
59
|
+
self.status_code = status_code
|
|
60
|
+
self.error_code = error_code
|
|
61
|
+
self.retryable_same_contract = retryable_same_contract
|
|
62
|
+
self.failure_phase = failure_phase
|
|
63
|
+
self.is_tls_verification_failure = is_tls_verification_failure
|
|
64
|
+
# Handshake response metadata preserved across the sanitizing
|
|
65
|
+
# boundary so callers can classify edge-level rejections (for
|
|
66
|
+
# example Cloudflare browser challenges) that the sanitized message
|
|
67
|
+
# and status alone cannot prove. In-process only; never surfaced to
|
|
68
|
+
# clients.
|
|
69
|
+
self.handshake_headers = handshake_headers
|
|
70
|
+
self.handshake_message = handshake_message
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def require_route_or_direct_egress_opt_in(
|
|
74
|
+
*,
|
|
75
|
+
route: ResolvedUpstreamRoute | None,
|
|
76
|
+
allow_direct_egress: bool,
|
|
77
|
+
operation: str,
|
|
78
|
+
) -> None:
|
|
79
|
+
if route is None and not allow_direct_egress:
|
|
80
|
+
raise ValueError(f"Direct Codex upstream egress for {operation} requires allow_direct_egress=True")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass(frozen=True, slots=True)
|
|
84
|
+
class CodexRequestResult:
|
|
85
|
+
response: Any
|
|
86
|
+
route: ResolvedUpstreamRoute
|
|
87
|
+
fallback_used: bool
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass(frozen=True, slots=True)
|
|
91
|
+
class CodexWebSocketResult:
|
|
92
|
+
websocket: Any
|
|
93
|
+
context: Any | None
|
|
94
|
+
route: ResolvedUpstreamRoute
|
|
95
|
+
fallback_used: bool
|
|
96
|
+
native: bool = False
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass(frozen=True, slots=True)
|
|
100
|
+
class _BufferedResponse:
|
|
101
|
+
status: int
|
|
102
|
+
status_code: int
|
|
103
|
+
headers: Mapping[str, str]
|
|
104
|
+
content: bytes
|
|
105
|
+
|
|
106
|
+
async def read(self) -> bytes:
|
|
107
|
+
return self.content
|
|
108
|
+
|
|
109
|
+
async def text(self) -> str:
|
|
110
|
+
return self.content.decode("utf-8", errors="replace")
|
|
111
|
+
|
|
112
|
+
def json(self) -> Any:
|
|
113
|
+
return json.loads(self.content)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass(frozen=True, slots=True)
|
|
117
|
+
class _PreparedNativeRequest:
|
|
118
|
+
url: str
|
|
119
|
+
headers: dict[str, str]
|
|
120
|
+
body: bytes | None
|
|
121
|
+
timeout_seconds: float | None
|
|
122
|
+
connect_timeout_seconds: float | None
|
|
123
|
+
response_head_timeout_seconds: float | None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
async def release_codex_response(response: Any) -> None:
|
|
127
|
+
"""Release an unbuffered upstream response once its consumer stops reading.
|
|
128
|
+
|
|
129
|
+
Streams routinely end before body EOF (terminal SSE event, idle timeout,
|
|
130
|
+
cancellation, downstream disconnect). Closing the owning session alone
|
|
131
|
+
leaves the aiohttp ``Connection`` acquired, so the cyclic GC later finalizes
|
|
132
|
+
it as ``Unclosed connection``. Duck-typed on purpose: aiohttp exposes
|
|
133
|
+
``release()``, the native egress response exposes ``aclose()``, and buffered
|
|
134
|
+
or test responses expose neither.
|
|
135
|
+
"""
|
|
136
|
+
for name in ("release", "close", "aclose"):
|
|
137
|
+
method = getattr(response, name, None)
|
|
138
|
+
if callable(method):
|
|
139
|
+
result = method()
|
|
140
|
+
if inspect.isawaitable(result):
|
|
141
|
+
await result
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class _SessionOwnedContent:
|
|
146
|
+
def __init__(self, response: Any, session: aiohttp.ClientSession) -> None:
|
|
147
|
+
self._response = response
|
|
148
|
+
self._session = session
|
|
149
|
+
|
|
150
|
+
def iter_chunked(self, size: int) -> Any:
|
|
151
|
+
return self._iter_and_close(self._response.content.iter_chunked(size))
|
|
152
|
+
|
|
153
|
+
async def _iter_and_close(self, iterator: Any) -> Any:
|
|
154
|
+
try:
|
|
155
|
+
async for chunk in iterator:
|
|
156
|
+
yield chunk
|
|
157
|
+
finally:
|
|
158
|
+
await release_codex_response(self._response)
|
|
159
|
+
await self._session.close()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class _SessionOwnedResponse:
|
|
163
|
+
def __init__(self, response: Any, session: aiohttp.ClientSession) -> None:
|
|
164
|
+
self._response = response
|
|
165
|
+
self._session = session
|
|
166
|
+
self.status = getattr(response, "status", getattr(response, "status_code", 0))
|
|
167
|
+
self.status_code = getattr(response, "status_code", self.status)
|
|
168
|
+
self.headers = getattr(response, "headers", {}) or {}
|
|
169
|
+
self.content = _SessionOwnedContent(response, session)
|
|
170
|
+
|
|
171
|
+
async def release(self) -> None:
|
|
172
|
+
try:
|
|
173
|
+
await release_codex_response(self._response)
|
|
174
|
+
finally:
|
|
175
|
+
await self._session.close()
|
|
176
|
+
|
|
177
|
+
async def read(self) -> bytes:
|
|
178
|
+
try:
|
|
179
|
+
result = self._response.read()
|
|
180
|
+
if asyncio.iscoroutine(result):
|
|
181
|
+
result = await result
|
|
182
|
+
if isinstance(result, bytes):
|
|
183
|
+
return result
|
|
184
|
+
if isinstance(result, str):
|
|
185
|
+
return result.encode()
|
|
186
|
+
return b""
|
|
187
|
+
finally:
|
|
188
|
+
await self.release()
|
|
189
|
+
|
|
190
|
+
async def text(self) -> str:
|
|
191
|
+
return (await self.read()).decode("utf-8", errors="replace")
|
|
192
|
+
|
|
193
|
+
async def json(self, *args: Any, **kwargs: Any) -> Any:
|
|
194
|
+
del args, kwargs
|
|
195
|
+
import json
|
|
196
|
+
|
|
197
|
+
return json.loads(await self.read())
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class _SessionOwnedWebSocketContext:
|
|
201
|
+
def __init__(self, context: Any, session: aiohttp.ClientSession) -> None:
|
|
202
|
+
self._context = context
|
|
203
|
+
self._session = session
|
|
204
|
+
|
|
205
|
+
async def __aenter__(self) -> Any:
|
|
206
|
+
return self._context
|
|
207
|
+
|
|
208
|
+
async def __aexit__(self, exc_type: Any, exc: Any, traceback: Any) -> None:
|
|
209
|
+
try:
|
|
210
|
+
if hasattr(self._context, "__aexit__"):
|
|
211
|
+
await self._context.__aexit__(exc_type, exc, traceback)
|
|
212
|
+
else:
|
|
213
|
+
close = getattr(self._context, "close", None)
|
|
214
|
+
if callable(close):
|
|
215
|
+
result = close()
|
|
216
|
+
if asyncio.iscoroutine(result):
|
|
217
|
+
await result
|
|
218
|
+
finally:
|
|
219
|
+
await self._session.close()
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class CodexClient:
|
|
223
|
+
def __init__(
|
|
224
|
+
self,
|
|
225
|
+
session: Any,
|
|
226
|
+
*,
|
|
227
|
+
native_egress_client: NativeEgressClient | None = None,
|
|
228
|
+
) -> None:
|
|
229
|
+
self._session = session
|
|
230
|
+
self._native_egress_client = (
|
|
231
|
+
native_egress_client if native_egress_client is not None else discover_native_egress_client()
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
async def request(self, method: str, url: str, *, route: ResolvedUpstreamRoute, **kwargs: Any) -> Any:
|
|
235
|
+
return (await self.request_with_route_metadata(method, url, route=route, **kwargs)).response
|
|
236
|
+
|
|
237
|
+
async def request_with_route_metadata(
|
|
238
|
+
self,
|
|
239
|
+
method: str,
|
|
240
|
+
url: str,
|
|
241
|
+
*,
|
|
242
|
+
route: ResolvedUpstreamRoute,
|
|
243
|
+
native_sse: NativeSseOptions | None = None,
|
|
244
|
+
**kwargs: Any,
|
|
245
|
+
) -> CodexRequestResult:
|
|
246
|
+
if route is None:
|
|
247
|
+
raise ValueError("Codex upstream calls require a resolved upstream proxy route")
|
|
248
|
+
buffer_response = bool(kwargs.pop("buffer_response", True))
|
|
249
|
+
if native_sse is not None and buffer_response:
|
|
250
|
+
raise ValueError("Native SSE framing requires buffer_response=False")
|
|
251
|
+
_reject_reserved(kwargs)
|
|
252
|
+
native_request = _prepare_native_request(url, kwargs)
|
|
253
|
+
aiohttp_kwargs = dict(kwargs)
|
|
254
|
+
_normalize_aiohttp_request_kwargs(aiohttp_kwargs)
|
|
255
|
+
endpoints = (route.endpoint, *route.fallbacks)
|
|
256
|
+
_reject_credentialed_plaintext_target(url, endpoints)
|
|
257
|
+
allow_fallback = _is_idempotent_method(method)
|
|
258
|
+
for index, endpoint in enumerate(endpoints):
|
|
259
|
+
candidate = route.with_endpoint(endpoint, tuple(endpoints[index + 1 :]))
|
|
260
|
+
try:
|
|
261
|
+
response: Any | None = None
|
|
262
|
+
if self._native_egress_client is not None and native_request is not None:
|
|
263
|
+
try:
|
|
264
|
+
response = await self._native_egress_client.request(
|
|
265
|
+
NativeEgressRequest(
|
|
266
|
+
method=method,
|
|
267
|
+
url=native_request.url,
|
|
268
|
+
headers=native_request.headers,
|
|
269
|
+
body=native_request.body,
|
|
270
|
+
timeout_seconds=native_request.timeout_seconds,
|
|
271
|
+
connect_timeout_seconds=native_request.connect_timeout_seconds,
|
|
272
|
+
response_head_timeout_seconds=native_request.response_head_timeout_seconds,
|
|
273
|
+
proxy_url=endpoint.proxy_url,
|
|
274
|
+
sse=native_sse,
|
|
275
|
+
)
|
|
276
|
+
)
|
|
277
|
+
if buffer_response:
|
|
278
|
+
response = await _buffer_response(response)
|
|
279
|
+
except NativeEgressUnavailable:
|
|
280
|
+
response = None
|
|
281
|
+
except NativeEgressError as exc:
|
|
282
|
+
raise _native_transport_error("request", endpoint.id, exc) from None
|
|
283
|
+
if response is not None:
|
|
284
|
+
pass
|
|
285
|
+
elif endpoint.scheme.startswith("socks"):
|
|
286
|
+
response = await _request_via_socks_proxy(
|
|
287
|
+
method,
|
|
288
|
+
url,
|
|
289
|
+
endpoint,
|
|
290
|
+
buffer_response=buffer_response,
|
|
291
|
+
**aiohttp_kwargs,
|
|
292
|
+
)
|
|
293
|
+
else:
|
|
294
|
+
try:
|
|
295
|
+
response = await self._session.request(
|
|
296
|
+
method,
|
|
297
|
+
url,
|
|
298
|
+
**endpoint.aiohttp_proxy_kwargs(),
|
|
299
|
+
**aiohttp_kwargs,
|
|
300
|
+
)
|
|
301
|
+
except Exception as exc:
|
|
302
|
+
raise _transport_error(
|
|
303
|
+
"request",
|
|
304
|
+
endpoint.id,
|
|
305
|
+
exc,
|
|
306
|
+
failure_phase="connect" if is_pre_dispatch_connection_failure(exc) else "request",
|
|
307
|
+
retryable_same_contract=is_pre_dispatch_connection_failure(exc),
|
|
308
|
+
) from None
|
|
309
|
+
if buffer_response:
|
|
310
|
+
try:
|
|
311
|
+
response = await _buffer_response(response)
|
|
312
|
+
except Exception as exc:
|
|
313
|
+
# Headers prove the POST reached the response phase;
|
|
314
|
+
# a failed buffered read is neutral when classified
|
|
315
|
+
# as host-network loss, but it is never replay-safe.
|
|
316
|
+
raise _transport_error(
|
|
317
|
+
"response body",
|
|
318
|
+
endpoint.id,
|
|
319
|
+
exc,
|
|
320
|
+
failure_phase="body_read",
|
|
321
|
+
retryable_same_contract=False,
|
|
322
|
+
) from None
|
|
323
|
+
return CodexRequestResult(response, candidate, index > 0)
|
|
324
|
+
except CodexTransportError as exc:
|
|
325
|
+
# A confirmed pre-dispatch connect failure proves the request
|
|
326
|
+
# never left for upstream, so trying the next endpoint in the
|
|
327
|
+
# same resolved pool is safe even for a non-idempotent POST.
|
|
328
|
+
# TLS verification failures are stable endpoint configuration
|
|
329
|
+
# errors rather than transient connect losses; they keep the
|
|
330
|
+
# idempotent-only rule.
|
|
331
|
+
if index == len(endpoints) - 1 or not (
|
|
332
|
+
allow_fallback or (exc.retryable_same_contract and not exc.is_tls_verification_failure)
|
|
333
|
+
):
|
|
334
|
+
raise
|
|
335
|
+
except Exception as exc:
|
|
336
|
+
pre_dispatch = is_pre_dispatch_connection_failure(exc) and not isinstance(exc, aiohttp.ClientSSLError)
|
|
337
|
+
if index == len(endpoints) - 1 or not (allow_fallback or pre_dispatch):
|
|
338
|
+
raise _transport_error(
|
|
339
|
+
"request",
|
|
340
|
+
endpoint.id,
|
|
341
|
+
exc,
|
|
342
|
+
failure_phase="connect" if is_pre_dispatch_connection_failure(exc) else "request",
|
|
343
|
+
retryable_same_contract=is_pre_dispatch_connection_failure(exc),
|
|
344
|
+
) from None
|
|
345
|
+
raise RuntimeError("unreachable Codex client fallback state")
|
|
346
|
+
|
|
347
|
+
async def ws_connect(self, url: str, *, route: ResolvedUpstreamRoute, **kwargs: Any) -> Any:
|
|
348
|
+
if route is None:
|
|
349
|
+
raise ValueError("Codex upstream calls require a resolved upstream proxy route")
|
|
350
|
+
_reject_reserved(kwargs)
|
|
351
|
+
_reject_credentialed_plaintext_target(url, (route.endpoint,))
|
|
352
|
+
result = self._session.ws_connect(url, **route.endpoint.aiohttp_proxy_kwargs(), **kwargs)
|
|
353
|
+
if asyncio.iscoroutine(result):
|
|
354
|
+
return await result
|
|
355
|
+
return result
|
|
356
|
+
|
|
357
|
+
async def open_ws_with_route_metadata(
|
|
358
|
+
self,
|
|
359
|
+
url: str,
|
|
360
|
+
*,
|
|
361
|
+
route: ResolvedUpstreamRoute,
|
|
362
|
+
retry_handshake_status: bool = True,
|
|
363
|
+
retry_network_errors: bool = True,
|
|
364
|
+
**kwargs: Any,
|
|
365
|
+
) -> CodexWebSocketResult:
|
|
366
|
+
if route is None:
|
|
367
|
+
raise ValueError("Codex upstream calls require a resolved upstream proxy route")
|
|
368
|
+
interpret_responses = bool(kwargs.pop("native_interpret_responses", False))
|
|
369
|
+
_reject_reserved(kwargs)
|
|
370
|
+
endpoints = (route.endpoint, *route.fallbacks)
|
|
371
|
+
_reject_credentialed_plaintext_target(url, endpoints)
|
|
372
|
+
for index, endpoint in enumerate(endpoints):
|
|
373
|
+
candidate = route.with_endpoint(endpoint, tuple(endpoints[index + 1 :]))
|
|
374
|
+
context: Any | None = None
|
|
375
|
+
entered_context: Any | None = None
|
|
376
|
+
try:
|
|
377
|
+
websocket: Any | None = None
|
|
378
|
+
native_request = _prepare_native_websocket_request(
|
|
379
|
+
url,
|
|
380
|
+
endpoint.proxy_url,
|
|
381
|
+
kwargs,
|
|
382
|
+
interpret_responses=interpret_responses,
|
|
383
|
+
)
|
|
384
|
+
if self._native_egress_client is not None and native_request is not None:
|
|
385
|
+
try:
|
|
386
|
+
websocket = await self._native_egress_client.websocket(native_request)
|
|
387
|
+
except NativeEgressUnavailable:
|
|
388
|
+
websocket = None
|
|
389
|
+
except NativeEgressError as exc:
|
|
390
|
+
raise _native_transport_error("websocket", endpoint.id, exc) from None
|
|
391
|
+
if websocket is not None:
|
|
392
|
+
return CodexWebSocketResult(
|
|
393
|
+
websocket,
|
|
394
|
+
None,
|
|
395
|
+
candidate,
|
|
396
|
+
index > 0,
|
|
397
|
+
native=True,
|
|
398
|
+
)
|
|
399
|
+
elif endpoint.scheme.startswith("socks"):
|
|
400
|
+
websocket, context = await _open_ws_via_socks_proxy(url, endpoint, **kwargs)
|
|
401
|
+
entered_context = context
|
|
402
|
+
else:
|
|
403
|
+
context = self._session.ws_connect(
|
|
404
|
+
url,
|
|
405
|
+
**endpoint.aiohttp_proxy_kwargs(),
|
|
406
|
+
**kwargs,
|
|
407
|
+
)
|
|
408
|
+
if asyncio.iscoroutine(context):
|
|
409
|
+
context = await context
|
|
410
|
+
if hasattr(context, "__aenter__"):
|
|
411
|
+
websocket = await context.__aenter__()
|
|
412
|
+
entered_context = context
|
|
413
|
+
else:
|
|
414
|
+
websocket = context
|
|
415
|
+
return CodexWebSocketResult(
|
|
416
|
+
websocket,
|
|
417
|
+
entered_context,
|
|
418
|
+
candidate,
|
|
419
|
+
index > 0,
|
|
420
|
+
)
|
|
421
|
+
except CodexTransportError as exc:
|
|
422
|
+
if entered_context is not None and hasattr(entered_context, "__aexit__"):
|
|
423
|
+
await entered_context.__aexit__(None, None, None)
|
|
424
|
+
if exc.is_tls_verification_failure:
|
|
425
|
+
raise
|
|
426
|
+
if exc.status_code is not None and not retry_handshake_status:
|
|
427
|
+
raise _without_same_contract_retry(exc) from None
|
|
428
|
+
if exc.status_code is None and not retry_network_errors:
|
|
429
|
+
raise _without_same_contract_retry(exc) from None
|
|
430
|
+
if index == len(endpoints) - 1:
|
|
431
|
+
raise
|
|
432
|
+
except Exception as exc:
|
|
433
|
+
if entered_context is not None and hasattr(entered_context, "__aexit__"):
|
|
434
|
+
await entered_context.__aexit__(None, None, None)
|
|
435
|
+
handshake_status = _transport_error_status_code(exc)
|
|
436
|
+
if handshake_status is not None and not retry_handshake_status:
|
|
437
|
+
raise _transport_error(
|
|
438
|
+
"websocket",
|
|
439
|
+
endpoint.id,
|
|
440
|
+
exc,
|
|
441
|
+
failure_phase="connect",
|
|
442
|
+
retryable_same_contract=False,
|
|
443
|
+
) from None
|
|
444
|
+
if handshake_status is None and not retry_network_errors:
|
|
445
|
+
raise _transport_error(
|
|
446
|
+
"websocket",
|
|
447
|
+
endpoint.id,
|
|
448
|
+
exc,
|
|
449
|
+
failure_phase="connect",
|
|
450
|
+
retryable_same_contract=False,
|
|
451
|
+
) from None
|
|
452
|
+
if index == len(endpoints) - 1:
|
|
453
|
+
raise _transport_error(
|
|
454
|
+
"websocket",
|
|
455
|
+
endpoint.id,
|
|
456
|
+
exc,
|
|
457
|
+
failure_phase="connect",
|
|
458
|
+
# No response.create frame can be delivered before the
|
|
459
|
+
# websocket open returns to its caller.
|
|
460
|
+
retryable_same_contract=is_pre_dispatch_connection_failure(exc),
|
|
461
|
+
) from None
|
|
462
|
+
raise RuntimeError("unreachable Codex client websocket fallback state")
|
|
463
|
+
|
|
464
|
+
async def close(self) -> None:
|
|
465
|
+
close = getattr(self._session, "close", None)
|
|
466
|
+
if not callable(close):
|
|
467
|
+
close = getattr(self._session, "aclose", None)
|
|
468
|
+
if not callable(close):
|
|
469
|
+
return
|
|
470
|
+
result = close()
|
|
471
|
+
if asyncio.iscoroutine(result):
|
|
472
|
+
await result
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def create_codex_session(*, max_clients: int = 10) -> Any:
|
|
476
|
+
from app.core.clients.http import _shared_ssl_context
|
|
477
|
+
|
|
478
|
+
connector = aiohttp.TCPConnector(limit=max_clients, ssl=_shared_ssl_context())
|
|
479
|
+
return aiohttp.ClientSession(
|
|
480
|
+
connector=connector,
|
|
481
|
+
timeout=aiohttp.ClientTimeout(total=None),
|
|
482
|
+
trust_env=False,
|
|
483
|
+
skip_auto_headers=_CODEX_SKIP_AUTO_IDENTITY_HEADERS,
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
async def _request_via_socks_proxy(
|
|
488
|
+
method: str,
|
|
489
|
+
url: str,
|
|
490
|
+
endpoint: ResolvedProxyEndpoint,
|
|
491
|
+
*,
|
|
492
|
+
buffer_response: bool,
|
|
493
|
+
**kwargs: Any,
|
|
494
|
+
) -> Any:
|
|
495
|
+
connector = _socks_proxy_connector(endpoint)
|
|
496
|
+
session = aiohttp.ClientSession(
|
|
497
|
+
connector=connector,
|
|
498
|
+
timeout=aiohttp.ClientTimeout(total=None),
|
|
499
|
+
trust_env=False,
|
|
500
|
+
skip_auto_headers=_CODEX_SKIP_AUTO_IDENTITY_HEADERS,
|
|
501
|
+
)
|
|
502
|
+
try:
|
|
503
|
+
try:
|
|
504
|
+
response = await session.request(method, url, **kwargs)
|
|
505
|
+
except Exception as exc:
|
|
506
|
+
raise _transport_error(
|
|
507
|
+
"request",
|
|
508
|
+
endpoint.id,
|
|
509
|
+
exc,
|
|
510
|
+
failure_phase="connect" if is_pre_dispatch_connection_failure(exc) else "request",
|
|
511
|
+
retryable_same_contract=is_pre_dispatch_connection_failure(exc),
|
|
512
|
+
) from None
|
|
513
|
+
if buffer_response:
|
|
514
|
+
try:
|
|
515
|
+
return await _buffer_response(response)
|
|
516
|
+
except Exception as exc:
|
|
517
|
+
raise _transport_error(
|
|
518
|
+
"response body",
|
|
519
|
+
endpoint.id,
|
|
520
|
+
exc,
|
|
521
|
+
failure_phase="body_read",
|
|
522
|
+
retryable_same_contract=False,
|
|
523
|
+
) from None
|
|
524
|
+
return _SessionOwnedResponse(response, session)
|
|
525
|
+
except Exception:
|
|
526
|
+
await session.close()
|
|
527
|
+
raise
|
|
528
|
+
finally:
|
|
529
|
+
if buffer_response:
|
|
530
|
+
await session.close()
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
async def _open_ws_via_socks_proxy(url: str, endpoint: ResolvedProxyEndpoint, **kwargs: Any) -> tuple[Any, Any]:
|
|
534
|
+
connector = _socks_proxy_connector(endpoint)
|
|
535
|
+
session = aiohttp.ClientSession(
|
|
536
|
+
connector=connector,
|
|
537
|
+
timeout=aiohttp.ClientTimeout(total=None),
|
|
538
|
+
trust_env=False,
|
|
539
|
+
skip_auto_headers=_CODEX_SKIP_AUTO_IDENTITY_HEADERS,
|
|
540
|
+
)
|
|
541
|
+
try:
|
|
542
|
+
context = session.ws_connect(url, **kwargs)
|
|
543
|
+
if asyncio.iscoroutine(context):
|
|
544
|
+
context = await context
|
|
545
|
+
websocket = await context.__aenter__() if hasattr(context, "__aenter__") else context
|
|
546
|
+
return websocket, _SessionOwnedWebSocketContext(context, session)
|
|
547
|
+
except BaseException:
|
|
548
|
+
await session.close()
|
|
549
|
+
raise
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def _socks_proxy_connector(endpoint: ResolvedProxyEndpoint) -> ProxyConnector:
|
|
553
|
+
from app.core.clients.http import _shared_ssl_context
|
|
554
|
+
|
|
555
|
+
proxy_scheme = endpoint.proxy_url.split(":", 1)[0]
|
|
556
|
+
return ProxyConnector(
|
|
557
|
+
host=endpoint.host,
|
|
558
|
+
port=endpoint.port,
|
|
559
|
+
proxy_type=ProxyType.SOCKS5,
|
|
560
|
+
username=endpoint.username,
|
|
561
|
+
password=endpoint.password,
|
|
562
|
+
rdns=proxy_scheme == "socks5h",
|
|
563
|
+
ssl=_shared_ssl_context(),
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def _normalize_aiohttp_request_kwargs(kwargs: dict[str, Any]) -> None:
|
|
568
|
+
files = kwargs.pop("files", None)
|
|
569
|
+
if files is None:
|
|
570
|
+
return
|
|
571
|
+
form = aiohttp.FormData()
|
|
572
|
+
data = kwargs.pop("data", None)
|
|
573
|
+
if isinstance(data, Mapping):
|
|
574
|
+
for name, value in data.items():
|
|
575
|
+
form.add_field(str(name), value)
|
|
576
|
+
elif data is not None:
|
|
577
|
+
form.add_field("data", data)
|
|
578
|
+
|
|
579
|
+
file_items = files.items() if isinstance(files, Mapping) else files
|
|
580
|
+
for name, value in file_items:
|
|
581
|
+
_add_form_file(form, str(name), value)
|
|
582
|
+
kwargs["data"] = form
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def _prepare_native_request(
|
|
586
|
+
url: str,
|
|
587
|
+
kwargs: Mapping[str, Any],
|
|
588
|
+
) -> _PreparedNativeRequest | None:
|
|
589
|
+
supported = {
|
|
590
|
+
"data",
|
|
591
|
+
"files",
|
|
592
|
+
"headers",
|
|
593
|
+
"json",
|
|
594
|
+
"params",
|
|
595
|
+
"skip_auto_headers",
|
|
596
|
+
"timeout",
|
|
597
|
+
}
|
|
598
|
+
if set(kwargs).difference(supported):
|
|
599
|
+
return None
|
|
600
|
+
|
|
601
|
+
raw_headers = kwargs.get("headers") or {}
|
|
602
|
+
if not isinstance(raw_headers, Mapping):
|
|
603
|
+
return None
|
|
604
|
+
headers = {str(name): str(value) for name, value in raw_headers.items()}
|
|
605
|
+
|
|
606
|
+
params = kwargs.get("params")
|
|
607
|
+
if params is not None:
|
|
608
|
+
try:
|
|
609
|
+
url = str(URL(url).update_query(params))
|
|
610
|
+
except (TypeError, ValueError):
|
|
611
|
+
return None
|
|
612
|
+
|
|
613
|
+
has_json = "json" in kwargs
|
|
614
|
+
data = kwargs.get("data")
|
|
615
|
+
files = kwargs.get("files")
|
|
616
|
+
if has_json and (data is not None or files is not None):
|
|
617
|
+
return None
|
|
618
|
+
|
|
619
|
+
body: bytes | None
|
|
620
|
+
if has_json:
|
|
621
|
+
try:
|
|
622
|
+
body = json.dumps(
|
|
623
|
+
kwargs.get("json"),
|
|
624
|
+
ensure_ascii=True,
|
|
625
|
+
separators=(",", ":"),
|
|
626
|
+
).encode("utf-8")
|
|
627
|
+
except (TypeError, ValueError):
|
|
628
|
+
return None
|
|
629
|
+
_set_header_default(headers, "Content-Type", "application/json")
|
|
630
|
+
elif files is not None:
|
|
631
|
+
encoded = _encode_native_multipart(data, files)
|
|
632
|
+
if encoded is None:
|
|
633
|
+
return None
|
|
634
|
+
body, content_type = encoded
|
|
635
|
+
_set_header_default(headers, "Content-Type", content_type)
|
|
636
|
+
elif data is None:
|
|
637
|
+
body = None
|
|
638
|
+
elif isinstance(data, bytes):
|
|
639
|
+
body = data
|
|
640
|
+
elif isinstance(data, (bytearray, memoryview)):
|
|
641
|
+
body = bytes(data)
|
|
642
|
+
elif isinstance(data, str):
|
|
643
|
+
body = data.encode("utf-8")
|
|
644
|
+
_set_header_default(headers, "Content-Type", "text/plain; charset=utf-8")
|
|
645
|
+
elif isinstance(data, Mapping):
|
|
646
|
+
body = urlencode([(str(name), str(value)) for name, value in data.items()], doseq=True).encode()
|
|
647
|
+
_set_header_default(headers, "Content-Type", "application/x-www-form-urlencoded")
|
|
648
|
+
else:
|
|
649
|
+
return None
|
|
650
|
+
|
|
651
|
+
timeouts = _native_timeout_parts(kwargs.get("timeout"))
|
|
652
|
+
if timeouts is None:
|
|
653
|
+
return None
|
|
654
|
+
timeout_seconds, connect_timeout_seconds, response_head_timeout_seconds = timeouts
|
|
655
|
+
return _PreparedNativeRequest(
|
|
656
|
+
url=url,
|
|
657
|
+
headers=headers,
|
|
658
|
+
body=body,
|
|
659
|
+
timeout_seconds=timeout_seconds,
|
|
660
|
+
connect_timeout_seconds=connect_timeout_seconds,
|
|
661
|
+
response_head_timeout_seconds=response_head_timeout_seconds,
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def _native_timeout_parts(value: Any) -> tuple[float | None, float | None, float | None] | None:
|
|
666
|
+
if value is None:
|
|
667
|
+
return 60.0, None, None
|
|
668
|
+
if isinstance(value, aiohttp.ClientTimeout):
|
|
669
|
+
total = float(value.total) if value.total is not None else None
|
|
670
|
+
connect = float(value.sock_connect) if value.sock_connect is not None else None
|
|
671
|
+
response_head = float(value.sock_read) if value.sock_read is not None else None
|
|
672
|
+
else:
|
|
673
|
+
try:
|
|
674
|
+
total = float(value)
|
|
675
|
+
except (TypeError, ValueError):
|
|
676
|
+
return None
|
|
677
|
+
connect = None
|
|
678
|
+
response_head = None
|
|
679
|
+
if any(part is not None and part <= 0 for part in (total, connect, response_head)):
|
|
680
|
+
return None
|
|
681
|
+
return total, connect, response_head
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def _encode_native_multipart(data: Any, files: Any) -> tuple[bytes, str] | None:
|
|
685
|
+
data_items = _form_items(data)
|
|
686
|
+
file_items = _form_items(files)
|
|
687
|
+
if data_items is None or file_items is None:
|
|
688
|
+
return None
|
|
689
|
+
boundary = f"codex-lb-{secrets.token_hex(16)}"
|
|
690
|
+
chunks: list[bytes] = []
|
|
691
|
+
for name, value in data_items:
|
|
692
|
+
if not isinstance(value, (str, bytes, bytearray, memoryview, int, float, bool)):
|
|
693
|
+
return None
|
|
694
|
+
payload = (
|
|
695
|
+
value.encode()
|
|
696
|
+
if isinstance(value, str)
|
|
697
|
+
else bytes(value)
|
|
698
|
+
if not isinstance(value, (int, float, bool))
|
|
699
|
+
else str(value).encode()
|
|
700
|
+
)
|
|
701
|
+
chunks.extend(
|
|
702
|
+
(
|
|
703
|
+
f"--{boundary}\r\n".encode(),
|
|
704
|
+
f'Content-Disposition: form-data; name="{_multipart_parameter(name)}"\r\n\r\n'.encode(),
|
|
705
|
+
payload,
|
|
706
|
+
b"\r\n",
|
|
707
|
+
)
|
|
708
|
+
)
|
|
709
|
+
for name, value in file_items:
|
|
710
|
+
if not isinstance(value, tuple) or len(value) < 2:
|
|
711
|
+
return None
|
|
712
|
+
filename = str(value[0] or name)
|
|
713
|
+
file_value = value[1]
|
|
714
|
+
content_type = str(value[2]) if len(value) >= 3 and value[2] else "application/octet-stream"
|
|
715
|
+
if isinstance(file_value, str):
|
|
716
|
+
payload = file_value.encode()
|
|
717
|
+
elif isinstance(file_value, (bytes, bytearray, memoryview)):
|
|
718
|
+
payload = bytes(file_value)
|
|
719
|
+
else:
|
|
720
|
+
return None
|
|
721
|
+
chunks.extend(
|
|
722
|
+
(
|
|
723
|
+
f"--{boundary}\r\n".encode(),
|
|
724
|
+
(
|
|
725
|
+
f'Content-Disposition: form-data; name="{_multipart_parameter(name)}"; '
|
|
726
|
+
f'filename="{_multipart_parameter(filename)}"\r\n'
|
|
727
|
+
).encode(),
|
|
728
|
+
f"Content-Type: {content_type}\r\n\r\n".encode(),
|
|
729
|
+
payload,
|
|
730
|
+
b"\r\n",
|
|
731
|
+
)
|
|
732
|
+
)
|
|
733
|
+
chunks.append(f"--{boundary}--\r\n".encode())
|
|
734
|
+
return b"".join(chunks), f"multipart/form-data; boundary={boundary}"
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def _form_items(value: Any) -> list[tuple[str, Any]] | None:
|
|
738
|
+
if value is None:
|
|
739
|
+
return []
|
|
740
|
+
items = value.items() if isinstance(value, Mapping) else value
|
|
741
|
+
try:
|
|
742
|
+
return [(str(name), item) for name, item in items]
|
|
743
|
+
except (TypeError, ValueError):
|
|
744
|
+
return None
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
def _multipart_parameter(value: str) -> str:
|
|
748
|
+
return value.replace("\\", "\\\\").replace('"', '\\"').replace("\r", "").replace("\n", "")
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
def _set_header_default(headers: dict[str, str], name: str, value: str) -> None:
|
|
752
|
+
if not any(existing.lower() == name.lower() for existing in headers):
|
|
753
|
+
headers[name] = value
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def _prepare_native_websocket_request(
|
|
757
|
+
url: str,
|
|
758
|
+
proxy_url: str,
|
|
759
|
+
kwargs: Mapping[str, Any],
|
|
760
|
+
*,
|
|
761
|
+
interpret_responses: bool = False,
|
|
762
|
+
) -> NativeWebSocketRequest | None:
|
|
763
|
+
supported = {
|
|
764
|
+
"compress",
|
|
765
|
+
"headers",
|
|
766
|
+
"heartbeat",
|
|
767
|
+
"max_msg_size",
|
|
768
|
+
"protocols",
|
|
769
|
+
"timeout",
|
|
770
|
+
}
|
|
771
|
+
if set(kwargs).difference(supported):
|
|
772
|
+
return None
|
|
773
|
+
raw_headers = kwargs.get("headers") or {}
|
|
774
|
+
if not isinstance(raw_headers, Mapping):
|
|
775
|
+
return None
|
|
776
|
+
headers = {str(name): str(value) for name, value in raw_headers.items()}
|
|
777
|
+
protocols = kwargs.get("protocols")
|
|
778
|
+
if protocols:
|
|
779
|
+
try:
|
|
780
|
+
headers["sec-websocket-protocol"] = ", ".join(str(value) for value in protocols)
|
|
781
|
+
except TypeError:
|
|
782
|
+
return None
|
|
783
|
+
try:
|
|
784
|
+
connect_timeout = float(kwargs.get("timeout", 10.0))
|
|
785
|
+
max_message_bytes = int(kwargs.get("max_msg_size", 4 * 1024 * 1024))
|
|
786
|
+
compress = int(kwargs.get("compress", 0))
|
|
787
|
+
heartbeat = kwargs.get("heartbeat")
|
|
788
|
+
ping_interval_seconds = float(heartbeat) if heartbeat is not None else None
|
|
789
|
+
ping_timeout_seconds = ping_interval_seconds / 2 if ping_interval_seconds is not None else None
|
|
790
|
+
except (TypeError, ValueError):
|
|
791
|
+
return None
|
|
792
|
+
if (
|
|
793
|
+
compress != 15
|
|
794
|
+
or connect_timeout <= 0
|
|
795
|
+
or max_message_bytes <= 0
|
|
796
|
+
or (ping_interval_seconds is not None and ping_interval_seconds <= 0)
|
|
797
|
+
):
|
|
798
|
+
return None
|
|
799
|
+
return NativeWebSocketRequest(
|
|
800
|
+
url=url,
|
|
801
|
+
headers=headers,
|
|
802
|
+
connect_timeout_seconds=connect_timeout,
|
|
803
|
+
max_message_bytes=max_message_bytes,
|
|
804
|
+
ping_interval_seconds=ping_interval_seconds,
|
|
805
|
+
ping_timeout_seconds=ping_timeout_seconds,
|
|
806
|
+
proxy_url=proxy_url,
|
|
807
|
+
interpret_responses=interpret_responses,
|
|
808
|
+
)
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
def _add_form_file(form: aiohttp.FormData, name: str, value: Any) -> None:
|
|
812
|
+
if isinstance(value, tuple):
|
|
813
|
+
filename = value[0] if len(value) >= 1 else None
|
|
814
|
+
file_value = value[1] if len(value) >= 2 else b""
|
|
815
|
+
content_type = value[2] if len(value) >= 3 else None
|
|
816
|
+
form.add_field(
|
|
817
|
+
name,
|
|
818
|
+
file_value,
|
|
819
|
+
filename=str(filename) if filename else None,
|
|
820
|
+
content_type=str(content_type) if content_type else None,
|
|
821
|
+
)
|
|
822
|
+
return
|
|
823
|
+
form.add_field(name, value)
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
async def _buffer_response(response: Any) -> Any:
|
|
827
|
+
body = await _read_response_body(response)
|
|
828
|
+
if body is None:
|
|
829
|
+
return response
|
|
830
|
+
status = _response_status(response)
|
|
831
|
+
headers = getattr(response, "headers", {}) or {}
|
|
832
|
+
return _BufferedResponse(
|
|
833
|
+
status=status,
|
|
834
|
+
status_code=status,
|
|
835
|
+
headers={str(key): str(value) for key, value in headers.items()},
|
|
836
|
+
content=body,
|
|
837
|
+
)
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
async def _read_response_body(response: Any) -> bytes | None:
|
|
841
|
+
read = getattr(response, "read", None)
|
|
842
|
+
if callable(read):
|
|
843
|
+
result = read()
|
|
844
|
+
if asyncio.iscoroutine(result):
|
|
845
|
+
result = await result
|
|
846
|
+
if isinstance(result, bytes):
|
|
847
|
+
return result
|
|
848
|
+
if isinstance(result, str):
|
|
849
|
+
return result.encode()
|
|
850
|
+
content = getattr(response, "content", None)
|
|
851
|
+
if isinstance(content, bytes):
|
|
852
|
+
return content
|
|
853
|
+
if isinstance(content, str):
|
|
854
|
+
return content.encode()
|
|
855
|
+
return None
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
def _response_status(response: Any) -> int:
|
|
859
|
+
value = getattr(response, "status", getattr(response, "status_code", 0))
|
|
860
|
+
return int(value or 0)
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
def _reject_credentialed_plaintext_target(url: str, endpoints: tuple[ResolvedProxyEndpoint, ...]) -> None:
|
|
864
|
+
# Proxy credentials ride in a Proxy-Authorization header (never URL
|
|
865
|
+
# userinfo, which aiohttp reprs into ConnectionKey/ClientHttpProxyError).
|
|
866
|
+
# aiohttp only forwards proxy_headers on the CONNECT tunnel, so a
|
|
867
|
+
# credentialed endpoint requires a TLS target. Checked once for the whole
|
|
868
|
+
# ordered pool, ahead of every transport branch and before any dispatch,
|
|
869
|
+
# so a credential-free fallback cannot quietly absorb a misconfigured
|
|
870
|
+
# primary. Raised as a connect-phase transport error so callers map it to
|
|
871
|
+
# the usual upstream-unavailable response instead of an unhandled failure.
|
|
872
|
+
if any(endpoint.username for endpoint in endpoints) and URL(url).scheme not in _TLS_TARGET_SCHEMES:
|
|
873
|
+
raise CodexTransportError(
|
|
874
|
+
"credentialed aiohttp proxy routes require an https/wss upstream target",
|
|
875
|
+
failure_phase="connect",
|
|
876
|
+
retryable_same_contract=False,
|
|
877
|
+
)
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
def _reject_reserved(kwargs: Mapping[str, Any]) -> None:
|
|
881
|
+
forbidden = sorted(_RESERVED.intersection(kwargs))
|
|
882
|
+
if forbidden:
|
|
883
|
+
raise ValueError(f"Codex upstream route/TLS kwargs are controlled centrally: {', '.join(forbidden)}")
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
def _is_idempotent_method(method: str) -> bool:
|
|
887
|
+
return method.upper() in {"GET", "HEAD", "OPTIONS", "TRACE"}
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
def _transport_error(
|
|
891
|
+
operation: str,
|
|
892
|
+
endpoint_id: str,
|
|
893
|
+
exc: Exception,
|
|
894
|
+
*,
|
|
895
|
+
failure_phase: str,
|
|
896
|
+
retryable_same_contract: bool,
|
|
897
|
+
) -> CodexTransportError:
|
|
898
|
+
process_network_failure = is_process_network_failure(exc, include_permanent_dns=False)
|
|
899
|
+
return CodexTransportError(
|
|
900
|
+
codex_transport_error_message(operation, endpoint_id, exc),
|
|
901
|
+
status_code=_transport_error_status_code(exc),
|
|
902
|
+
# A missing proxy hostname is endpoint configuration, not evidence
|
|
903
|
+
# that the host lost DNS. Transient DNS and local route loss remain
|
|
904
|
+
# process-wide failures and are safe to classify without exposing the
|
|
905
|
+
# credential-bearing original exception.
|
|
906
|
+
error_code=PROCESS_NETWORK_UNAVAILABLE_CODE if process_network_failure else None,
|
|
907
|
+
retryable_same_contract=retryable_same_contract,
|
|
908
|
+
failure_phase=failure_phase,
|
|
909
|
+
is_tls_verification_failure=isinstance(exc, aiohttp.ClientSSLError),
|
|
910
|
+
handshake_headers=_transport_error_handshake_headers(exc),
|
|
911
|
+
handshake_message=_transport_error_handshake_message(exc),
|
|
912
|
+
)
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
def _transport_error_handshake_headers(exc: Exception) -> Mapping[str, str] | None:
|
|
916
|
+
"""Handshake response headers of a wrapped upgrade rejection, else ``None``.
|
|
917
|
+
|
|
918
|
+
``aiohttp.WSServerHandshakeError`` carries the response headers directly;
|
|
919
|
+
exceptions wrapping a response object carry them one level down.
|
|
920
|
+
"""
|
|
921
|
+
|
|
922
|
+
response = getattr(exc, "response", None)
|
|
923
|
+
for source in (exc, response):
|
|
924
|
+
if source is None:
|
|
925
|
+
continue
|
|
926
|
+
headers = getattr(source, "headers", None)
|
|
927
|
+
if headers is not None:
|
|
928
|
+
return headers
|
|
929
|
+
return None
|
|
930
|
+
|
|
931
|
+
|
|
932
|
+
def _transport_error_handshake_message(exc: Exception) -> str | None:
|
|
933
|
+
message = getattr(exc, "message", None)
|
|
934
|
+
return message if isinstance(message, str) else None
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
def _native_transport_error(
|
|
938
|
+
operation: str,
|
|
939
|
+
endpoint_id: str,
|
|
940
|
+
exc: NativeEgressError,
|
|
941
|
+
) -> CodexTransportError:
|
|
942
|
+
if isinstance(exc, NativeEgressTransportError):
|
|
943
|
+
process_failure = exc.failure_phase in {
|
|
944
|
+
"helper_exit",
|
|
945
|
+
"helper_read",
|
|
946
|
+
"helper_write",
|
|
947
|
+
"shutdown",
|
|
948
|
+
}
|
|
949
|
+
return CodexTransportError(
|
|
950
|
+
codex_transport_error_message(operation, endpoint_id, exc),
|
|
951
|
+
status_code=exc.status_code,
|
|
952
|
+
error_code=PROCESS_NETWORK_UNAVAILABLE_CODE if process_failure else None,
|
|
953
|
+
retryable_same_contract=(exc.retryable_same_contract and not exc.is_tls_verification_failure),
|
|
954
|
+
failure_phase=exc.failure_phase,
|
|
955
|
+
is_tls_verification_failure=exc.is_tls_verification_failure,
|
|
956
|
+
handshake_headers=dict(exc.headers) if exc.headers else None,
|
|
957
|
+
handshake_message=(exc.body.decode("utf-8", errors="replace") if exc.body else None),
|
|
958
|
+
)
|
|
959
|
+
return CodexTransportError(
|
|
960
|
+
codex_transport_error_message(operation, endpoint_id, exc),
|
|
961
|
+
retryable_same_contract=False,
|
|
962
|
+
failure_phase="protocol",
|
|
963
|
+
)
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
def _without_same_contract_retry(exc: CodexTransportError) -> CodexTransportError:
|
|
967
|
+
return CodexTransportError(
|
|
968
|
+
str(exc),
|
|
969
|
+
status_code=exc.status_code,
|
|
970
|
+
error_code=exc.error_code,
|
|
971
|
+
retryable_same_contract=False,
|
|
972
|
+
failure_phase=exc.failure_phase,
|
|
973
|
+
is_tls_verification_failure=exc.is_tls_verification_failure,
|
|
974
|
+
handshake_headers=exc.handshake_headers,
|
|
975
|
+
handshake_message=exc.handshake_message,
|
|
976
|
+
)
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
def _transport_error_status_code(exc: Exception) -> int | None:
|
|
980
|
+
response = getattr(exc, "response", None)
|
|
981
|
+
for source in (response, exc):
|
|
982
|
+
if source is None:
|
|
983
|
+
continue
|
|
984
|
+
value = getattr(source, "status_code", getattr(source, "status", None))
|
|
985
|
+
if value is None:
|
|
986
|
+
continue
|
|
987
|
+
try:
|
|
988
|
+
return int(value)
|
|
989
|
+
except (TypeError, ValueError):
|
|
990
|
+
continue
|
|
991
|
+
return None
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
def codex_transport_error_message(operation: str, endpoint_id: str | None, exc: Exception) -> str:
|
|
995
|
+
endpoint = endpoint_id or "unknown"
|
|
996
|
+
return f"Codex upstream {operation} failed via proxy endpoint {endpoint}: {type(exc).__name__}"
|