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,478 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from fastapi import FastAPI, Request
|
|
8
|
+
from fastapi.exception_handlers import (
|
|
9
|
+
http_exception_handler,
|
|
10
|
+
request_validation_exception_handler,
|
|
11
|
+
)
|
|
12
|
+
from fastapi.exceptions import RequestValidationError
|
|
13
|
+
from fastapi.responses import JSONResponse, Response
|
|
14
|
+
from starlette._utils import get_route_path
|
|
15
|
+
from starlette.exceptions import HTTPException as StarletteHTTPException
|
|
16
|
+
from starlette.types import ASGIApp, Receive, Scope, Send
|
|
17
|
+
|
|
18
|
+
from app.core.errors import SCIM_CONTENT_TYPE, dashboard_error, openai_error, scim_error
|
|
19
|
+
from app.core.exceptions import (
|
|
20
|
+
AppError,
|
|
21
|
+
DashboardAuthError,
|
|
22
|
+
DashboardBadRequestError,
|
|
23
|
+
DashboardConflictError,
|
|
24
|
+
DashboardNotFoundError,
|
|
25
|
+
DashboardPermissionError,
|
|
26
|
+
DashboardRateLimitError,
|
|
27
|
+
DashboardServiceUnavailableError,
|
|
28
|
+
DashboardUpstreamError,
|
|
29
|
+
DashboardValidationError,
|
|
30
|
+
ProxyAuthError,
|
|
31
|
+
ProxyModelNotAllowed,
|
|
32
|
+
ProxyRateLimitError,
|
|
33
|
+
ProxyReasoningEffortNotAllowed,
|
|
34
|
+
ProxyRequiredCapabilityTransportError,
|
|
35
|
+
ProxyUpstreamError,
|
|
36
|
+
ScimError,
|
|
37
|
+
)
|
|
38
|
+
from app.core.middleware.multipart_content_encoding import (
|
|
39
|
+
UnsupportedMultipartContentEncoding,
|
|
40
|
+
multipart_error_response,
|
|
41
|
+
)
|
|
42
|
+
from app.core.middleware.request_body_limit import (
|
|
43
|
+
REQUEST_BODY_TOO_LARGE_MESSAGE,
|
|
44
|
+
request_body_limit_was_exceeded,
|
|
45
|
+
request_ingress_error_response,
|
|
46
|
+
)
|
|
47
|
+
from app.core.multipart import MultipartPayloadTooLarge
|
|
48
|
+
from app.core.runtime_logging import log_error_response
|
|
49
|
+
from app.modules.proxy.images_observability import (
|
|
50
|
+
IMAGE_ROUTE_MODEL_STATE,
|
|
51
|
+
IMAGE_ROUTE_STARTED_AT_STATE,
|
|
52
|
+
IMAGE_ROUTE_STREAM_STATE,
|
|
53
|
+
ImageRoute,
|
|
54
|
+
record_images_route_observability,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
logger = logging.getLogger(__name__)
|
|
58
|
+
|
|
59
|
+
_OPENAI_EXCEPTION_TYPES: tuple[type[AppError], ...] = (
|
|
60
|
+
ProxyAuthError,
|
|
61
|
+
ProxyModelNotAllowed,
|
|
62
|
+
ProxyReasoningEffortNotAllowed,
|
|
63
|
+
ProxyRateLimitError,
|
|
64
|
+
ProxyRequiredCapabilityTransportError,
|
|
65
|
+
ProxyUpstreamError,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
_DASHBOARD_EXCEPTION_TYPES: tuple[type[AppError], ...] = (
|
|
69
|
+
DashboardAuthError,
|
|
70
|
+
DashboardPermissionError,
|
|
71
|
+
DashboardNotFoundError,
|
|
72
|
+
DashboardConflictError,
|
|
73
|
+
DashboardBadRequestError,
|
|
74
|
+
DashboardValidationError,
|
|
75
|
+
DashboardRateLimitError,
|
|
76
|
+
DashboardServiceUnavailableError,
|
|
77
|
+
DashboardUpstreamError,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _error_format(request: Request) -> str | None:
|
|
82
|
+
fmt = getattr(request.state, "error_format", None)
|
|
83
|
+
if fmt is not None:
|
|
84
|
+
return fmt
|
|
85
|
+
# Fallback for unmatched routes (e.g. SPA fallback 404s)
|
|
86
|
+
path = request.url.path
|
|
87
|
+
if path.startswith("/api/"):
|
|
88
|
+
return "dashboard"
|
|
89
|
+
if path in {"/v1", "/backend-api"} or path.startswith(("/v1/", "/backend-api/")):
|
|
90
|
+
return "openai"
|
|
91
|
+
# Unmatched ``/scim/...`` paths never reach the router's marker dependency,
|
|
92
|
+
# so the 404 an identity provider probing for an unimplemented resource
|
|
93
|
+
# gets has to be decided here. Without it the answer is FastAPI's
|
|
94
|
+
# ``{"detail": ...}``, which no SCIM client parses.
|
|
95
|
+
if path == "/scim" or path.startswith("/scim/"):
|
|
96
|
+
return "scim"
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _scim_response(status_code: int, detail: str, *, scim_type: str | None = None) -> JSONResponse:
|
|
101
|
+
return JSONResponse(
|
|
102
|
+
status_code=status_code,
|
|
103
|
+
content=scim_error(status_code, detail, scim_type=scim_type),
|
|
104
|
+
media_type=SCIM_CONTENT_TYPE,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# Codex-native aliases under /backend-api/codex map to the same route labels
|
|
109
|
+
# as their /v1 counterparts so observability stays consistent across surfaces.
|
|
110
|
+
_IMAGE_ROUTE_PATHS: dict[str, ImageRoute] = {
|
|
111
|
+
"/v1/images/generations": "generations",
|
|
112
|
+
"/backend-api/codex/images/generations": "generations",
|
|
113
|
+
"/v1/images/edits": "edits",
|
|
114
|
+
"/backend-api/codex/images/edits": "edits",
|
|
115
|
+
}
|
|
116
|
+
_CODEX_JSON_IMAGE_EDIT_PATH = "/backend-api/codex/images/edits"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _image_route_from_path(path: str) -> ImageRoute | None:
|
|
120
|
+
return _IMAGE_ROUTE_PATHS.get(path)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _is_json_request(request: Request) -> bool:
|
|
124
|
+
content_type = request.headers.get("content-type", "")
|
|
125
|
+
return content_type.split(";", 1)[0].strip().lower() == "application/json"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
async def _image_request_model_and_stream(request: Request, route: ImageRoute) -> tuple[str | None, bool]:
|
|
129
|
+
model: str | None = None
|
|
130
|
+
stream = False
|
|
131
|
+
|
|
132
|
+
# Generations is always JSON; only the codex edits alias accepts a JSON
|
|
133
|
+
# payload. The /v1 edits surface must never consume its body here, even
|
|
134
|
+
# when a client supplies the wrong Content-Type before authorization.
|
|
135
|
+
is_codex_json_edit = get_route_path(request.scope) == _CODEX_JSON_IMAGE_EDIT_PATH and _is_json_request(request)
|
|
136
|
+
if route == "generations" or is_codex_json_edit:
|
|
137
|
+
try:
|
|
138
|
+
payload: Any = await request.json()
|
|
139
|
+
except Exception:
|
|
140
|
+
payload = None
|
|
141
|
+
if isinstance(payload, dict):
|
|
142
|
+
model_value = payload.get("model")
|
|
143
|
+
if isinstance(model_value, str) and model_value:
|
|
144
|
+
model = model_value
|
|
145
|
+
stream = payload.get("stream") is True
|
|
146
|
+
return model, stream
|
|
147
|
+
|
|
148
|
+
if route == "edits":
|
|
149
|
+
# Multipart edit bodies are parsed only inside the authorized handler.
|
|
150
|
+
# Pre-handler errors use bounded unknown/false labels rather than
|
|
151
|
+
# consuming and spooling an unauthenticated upload for observability.
|
|
152
|
+
parsed_model = getattr(request.state, IMAGE_ROUTE_MODEL_STATE, None)
|
|
153
|
+
if isinstance(parsed_model, str) and parsed_model:
|
|
154
|
+
model = parsed_model
|
|
155
|
+
parsed_stream = getattr(request.state, IMAGE_ROUTE_STREAM_STATE, None)
|
|
156
|
+
if isinstance(parsed_stream, bool):
|
|
157
|
+
stream = parsed_stream
|
|
158
|
+
return model, stream
|
|
159
|
+
|
|
160
|
+
return model, stream
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
async def _record_image_route_exception_observability(
|
|
164
|
+
request: Request,
|
|
165
|
+
*,
|
|
166
|
+
status: int,
|
|
167
|
+
outcome: str,
|
|
168
|
+
) -> None:
|
|
169
|
+
path = get_route_path(request.scope)
|
|
170
|
+
route = _image_route_from_path(path)
|
|
171
|
+
if route is None:
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
model, stream = await _image_request_model_and_stream(request, route)
|
|
175
|
+
started_at = getattr(request.state, IMAGE_ROUTE_STARTED_AT_STATE, None)
|
|
176
|
+
if not isinstance(started_at, float):
|
|
177
|
+
started_at = time.perf_counter()
|
|
178
|
+
|
|
179
|
+
record_images_route_observability(
|
|
180
|
+
route=route,
|
|
181
|
+
model=model,
|
|
182
|
+
stream=stream,
|
|
183
|
+
status=status,
|
|
184
|
+
outcome=outcome,
|
|
185
|
+
started_at=started_at,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class ImageRouteStartedAtMiddleware:
|
|
190
|
+
"""Stamp the ingress start time for image routes into ``scope["state"]``.
|
|
191
|
+
|
|
192
|
+
Pure ASGI (no ``BaseHTTPMiddleware``): the previous ``@app.middleware("http")``
|
|
193
|
+
form ran the whole HTTP app in a child task and relayed every response
|
|
194
|
+
chunk through an anyio memory stream. ``Request.state`` is backed by
|
|
195
|
+
``scope["state"]``, so the handler and the exception observability paths
|
|
196
|
+
keep reading the value through ``request.state`` unchanged.
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
def __init__(self, app: ASGIApp) -> None:
|
|
200
|
+
self.app = app
|
|
201
|
+
|
|
202
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
203
|
+
if scope["type"] == "http" and _image_route_from_path(get_route_path(scope)) is not None:
|
|
204
|
+
scope.setdefault("state", {})[IMAGE_ROUTE_STARTED_AT_STATE] = time.perf_counter()
|
|
205
|
+
await self.app(scope, receive, send)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def add_exception_handlers(app: FastAPI) -> None:
|
|
209
|
+
app.add_middleware(ImageRouteStartedAtMiddleware)
|
|
210
|
+
|
|
211
|
+
@app.exception_handler(MultipartPayloadTooLarge)
|
|
212
|
+
async def multipart_payload_too_large_handler(
|
|
213
|
+
request: Request,
|
|
214
|
+
exc: MultipartPayloadTooLarge,
|
|
215
|
+
) -> JSONResponse:
|
|
216
|
+
await _record_image_route_exception_observability(
|
|
217
|
+
request,
|
|
218
|
+
status=413,
|
|
219
|
+
outcome="invalid_request",
|
|
220
|
+
)
|
|
221
|
+
return multipart_error_response(
|
|
222
|
+
request,
|
|
223
|
+
status_code=413,
|
|
224
|
+
code="payload_too_large",
|
|
225
|
+
message=exc.message,
|
|
226
|
+
param=exc.param,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
@app.exception_handler(UnsupportedMultipartContentEncoding)
|
|
230
|
+
async def unsupported_multipart_content_encoding_handler(
|
|
231
|
+
request: Request,
|
|
232
|
+
exc: UnsupportedMultipartContentEncoding,
|
|
233
|
+
) -> JSONResponse:
|
|
234
|
+
del exc
|
|
235
|
+
await _record_image_route_exception_observability(
|
|
236
|
+
request,
|
|
237
|
+
status=400,
|
|
238
|
+
outcome="invalid_request",
|
|
239
|
+
)
|
|
240
|
+
return multipart_error_response(
|
|
241
|
+
request,
|
|
242
|
+
status_code=400,
|
|
243
|
+
code="invalid_request",
|
|
244
|
+
message="Compressed multipart uploads are not supported",
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
# --- Domain exceptions: OpenAI envelope ---
|
|
248
|
+
|
|
249
|
+
for exc_cls in _OPENAI_EXCEPTION_TYPES:
|
|
250
|
+
|
|
251
|
+
@app.exception_handler(exc_cls)
|
|
252
|
+
async def _openai_domain_handler(request: Request, exc: AppError) -> JSONResponse:
|
|
253
|
+
error_type = getattr(exc, "error_type", "server_error")
|
|
254
|
+
log_error_response(
|
|
255
|
+
logger,
|
|
256
|
+
request,
|
|
257
|
+
exc.status_code,
|
|
258
|
+
exc.code,
|
|
259
|
+
exc.message,
|
|
260
|
+
category="openai_error_response",
|
|
261
|
+
)
|
|
262
|
+
if isinstance(exc, ProxyAuthError):
|
|
263
|
+
await _record_image_route_exception_observability(
|
|
264
|
+
request,
|
|
265
|
+
status=exc.status_code,
|
|
266
|
+
outcome="auth_error",
|
|
267
|
+
)
|
|
268
|
+
elif isinstance(exc, ProxyRequiredCapabilityTransportError):
|
|
269
|
+
await _record_image_route_exception_observability(
|
|
270
|
+
request,
|
|
271
|
+
status=exc.status_code,
|
|
272
|
+
outcome="invalid_request",
|
|
273
|
+
)
|
|
274
|
+
error = openai_error(exc.code, exc.message, error_type=error_type)
|
|
275
|
+
if exc.param is not None:
|
|
276
|
+
error["error"]["param"] = exc.param
|
|
277
|
+
return JSONResponse(status_code=exc.status_code, content=error)
|
|
278
|
+
|
|
279
|
+
# --- Domain exceptions: Dashboard envelope ---
|
|
280
|
+
|
|
281
|
+
for exc_cls in _DASHBOARD_EXCEPTION_TYPES:
|
|
282
|
+
|
|
283
|
+
@app.exception_handler(exc_cls)
|
|
284
|
+
async def _dashboard_domain_handler(request: Request, exc: AppError) -> JSONResponse:
|
|
285
|
+
headers: dict[str, str] | None = None
|
|
286
|
+
if isinstance(exc, DashboardRateLimitError):
|
|
287
|
+
headers = {"Retry-After": str(exc.retry_after)}
|
|
288
|
+
log_error_response(
|
|
289
|
+
logger,
|
|
290
|
+
request,
|
|
291
|
+
exc.status_code,
|
|
292
|
+
exc.code,
|
|
293
|
+
exc.message,
|
|
294
|
+
category="dashboard_error_response",
|
|
295
|
+
)
|
|
296
|
+
return JSONResponse(
|
|
297
|
+
status_code=exc.status_code,
|
|
298
|
+
content=dashboard_error(exc.code, exc.message, param=exc.param, details=exc.details),
|
|
299
|
+
headers=headers,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
# --- Domain exceptions: SCIM envelope ---
|
|
303
|
+
|
|
304
|
+
@app.exception_handler(ScimError)
|
|
305
|
+
async def _scim_domain_handler(request: Request, exc: ScimError) -> JSONResponse:
|
|
306
|
+
log_error_response(
|
|
307
|
+
logger,
|
|
308
|
+
request,
|
|
309
|
+
exc.status_code,
|
|
310
|
+
exc.scim_type or f"http_{exc.status_code}",
|
|
311
|
+
exc.message,
|
|
312
|
+
category="scim_error_response",
|
|
313
|
+
)
|
|
314
|
+
response = _scim_response(exc.status_code, exc.message, scim_type=exc.scim_type)
|
|
315
|
+
for name, value in (exc.headers or {}).items():
|
|
316
|
+
response.headers[name] = value
|
|
317
|
+
return response
|
|
318
|
+
|
|
319
|
+
# --- Framework exceptions: format based on router marker ---
|
|
320
|
+
|
|
321
|
+
@app.exception_handler(RequestValidationError)
|
|
322
|
+
async def validation_error_handler(
|
|
323
|
+
request: Request,
|
|
324
|
+
exc: RequestValidationError,
|
|
325
|
+
) -> Response:
|
|
326
|
+
first_message: str | None = None
|
|
327
|
+
if exc.errors():
|
|
328
|
+
first = exc.errors()[0]
|
|
329
|
+
message = first.get("msg")
|
|
330
|
+
if isinstance(message, str):
|
|
331
|
+
first_message = message
|
|
332
|
+
fmt = _error_format(request)
|
|
333
|
+
if fmt == "dashboard":
|
|
334
|
+
log_error_response(
|
|
335
|
+
logger,
|
|
336
|
+
request,
|
|
337
|
+
422,
|
|
338
|
+
"validation_error",
|
|
339
|
+
first_message or "Invalid request payload",
|
|
340
|
+
category="dashboard_error_response",
|
|
341
|
+
)
|
|
342
|
+
return JSONResponse(
|
|
343
|
+
status_code=422,
|
|
344
|
+
content=dashboard_error("validation_error", "Invalid request payload"),
|
|
345
|
+
)
|
|
346
|
+
if fmt == "openai":
|
|
347
|
+
error = openai_error("invalid_request_error", "Invalid request payload", error_type="invalid_request_error")
|
|
348
|
+
if exc.errors():
|
|
349
|
+
first = exc.errors()[0]
|
|
350
|
+
loc = first.get("loc", [])
|
|
351
|
+
if isinstance(loc, (list, tuple)):
|
|
352
|
+
param = ".".join(str(part) for part in loc if part != "body")
|
|
353
|
+
if param:
|
|
354
|
+
error["error"]["param"] = param
|
|
355
|
+
log_error_response(
|
|
356
|
+
logger,
|
|
357
|
+
request,
|
|
358
|
+
400,
|
|
359
|
+
"invalid_request_error",
|
|
360
|
+
first_message or "Invalid request payload",
|
|
361
|
+
category="openai_error_response",
|
|
362
|
+
)
|
|
363
|
+
await _record_image_route_exception_observability(
|
|
364
|
+
request,
|
|
365
|
+
status=400,
|
|
366
|
+
outcome="invalid_request",
|
|
367
|
+
)
|
|
368
|
+
return JSONResponse(status_code=400, content=error)
|
|
369
|
+
if fmt == "scim":
|
|
370
|
+
log_error_response(
|
|
371
|
+
logger,
|
|
372
|
+
request,
|
|
373
|
+
400,
|
|
374
|
+
"invalidSyntax",
|
|
375
|
+
first_message or "Invalid request",
|
|
376
|
+
category="scim_error_response",
|
|
377
|
+
)
|
|
378
|
+
return _scim_response(400, "The request could not be understood.", scim_type="invalidSyntax")
|
|
379
|
+
return await request_validation_exception_handler(request, exc)
|
|
380
|
+
|
|
381
|
+
@app.exception_handler(StarletteHTTPException)
|
|
382
|
+
async def http_error_handler(
|
|
383
|
+
request: Request,
|
|
384
|
+
exc: StarletteHTTPException,
|
|
385
|
+
) -> Response:
|
|
386
|
+
if request_body_limit_was_exceeded(request):
|
|
387
|
+
return request_ingress_error_response(
|
|
388
|
+
request,
|
|
389
|
+
status_code=413,
|
|
390
|
+
code="payload_too_large",
|
|
391
|
+
message=REQUEST_BODY_TOO_LARGE_MESSAGE,
|
|
392
|
+
)
|
|
393
|
+
fmt = _error_format(request)
|
|
394
|
+
detail = exc.detail if isinstance(exc.detail, str) else "Request failed"
|
|
395
|
+
if fmt == "dashboard":
|
|
396
|
+
log_error_response(
|
|
397
|
+
logger,
|
|
398
|
+
request,
|
|
399
|
+
exc.status_code,
|
|
400
|
+
f"http_{exc.status_code}",
|
|
401
|
+
detail,
|
|
402
|
+
category="dashboard_error_response",
|
|
403
|
+
)
|
|
404
|
+
return JSONResponse(
|
|
405
|
+
status_code=exc.status_code,
|
|
406
|
+
content=dashboard_error(f"http_{exc.status_code}", detail),
|
|
407
|
+
)
|
|
408
|
+
if fmt == "openai":
|
|
409
|
+
if exc.status_code == 400 and _image_route_from_path(get_route_path(request.scope)) == "edits":
|
|
410
|
+
await _record_image_route_exception_observability(
|
|
411
|
+
request,
|
|
412
|
+
status=400,
|
|
413
|
+
outcome="invalid_request",
|
|
414
|
+
)
|
|
415
|
+
error_type = "invalid_request_error"
|
|
416
|
+
code = "invalid_request_error"
|
|
417
|
+
if exc.status_code == 401:
|
|
418
|
+
error_type = "authentication_error"
|
|
419
|
+
code = "invalid_api_key"
|
|
420
|
+
elif exc.status_code == 403:
|
|
421
|
+
error_type = "permission_error"
|
|
422
|
+
code = "insufficient_permissions"
|
|
423
|
+
elif exc.status_code == 404:
|
|
424
|
+
error_type = "invalid_request_error"
|
|
425
|
+
code = "not_found"
|
|
426
|
+
elif exc.status_code == 429:
|
|
427
|
+
error_type = "rate_limit_error"
|
|
428
|
+
code = "rate_limit_exceeded"
|
|
429
|
+
elif exc.status_code >= 500:
|
|
430
|
+
error_type = "server_error"
|
|
431
|
+
code = "server_error"
|
|
432
|
+
log_error_response(
|
|
433
|
+
logger,
|
|
434
|
+
request,
|
|
435
|
+
exc.status_code,
|
|
436
|
+
code,
|
|
437
|
+
detail,
|
|
438
|
+
category="openai_error_response",
|
|
439
|
+
)
|
|
440
|
+
return JSONResponse(status_code=exc.status_code, content=openai_error(code, detail, error_type=error_type))
|
|
441
|
+
if fmt == "scim":
|
|
442
|
+
log_error_response(
|
|
443
|
+
logger,
|
|
444
|
+
request,
|
|
445
|
+
exc.status_code,
|
|
446
|
+
f"http_{exc.status_code}",
|
|
447
|
+
detail,
|
|
448
|
+
category="scim_error_response",
|
|
449
|
+
)
|
|
450
|
+
response = _scim_response(exc.status_code, detail)
|
|
451
|
+
allow = (exc.headers or {}).get("Allow")
|
|
452
|
+
if allow is not None:
|
|
453
|
+
response.headers["Allow"] = allow
|
|
454
|
+
return response
|
|
455
|
+
return await http_exception_handler(request, exc)
|
|
456
|
+
|
|
457
|
+
# --- Catch-all for unhandled exceptions ---
|
|
458
|
+
|
|
459
|
+
@app.exception_handler(Exception)
|
|
460
|
+
async def unhandled_error_handler(request: Request, exc: Exception) -> JSONResponse:
|
|
461
|
+
fmt = _error_format(request)
|
|
462
|
+
category = "unhandled_error_response"
|
|
463
|
+
code = "server_error"
|
|
464
|
+
message = str(exc) or "Unexpected error"
|
|
465
|
+
log_error_response(logger, request, 500, code, message, category=category, exc_info=True)
|
|
466
|
+
if fmt == "dashboard":
|
|
467
|
+
return JSONResponse(
|
|
468
|
+
status_code=500,
|
|
469
|
+
content=dashboard_error("internal_error", "Unexpected error"),
|
|
470
|
+
)
|
|
471
|
+
if fmt == "openai":
|
|
472
|
+
return JSONResponse(
|
|
473
|
+
status_code=500,
|
|
474
|
+
content=openai_error("server_error", "Internal server error", error_type="server_error"),
|
|
475
|
+
)
|
|
476
|
+
if fmt == "scim":
|
|
477
|
+
return _scim_response(500, "The request could not be completed.")
|
|
478
|
+
return JSONResponse(status_code=500, content={"detail": "Internal Server Error"})
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Uvicorn HTTP protocol selection tolerant of opportunistic upgrade offers.
|
|
2
|
+
|
|
3
|
+
JetBrains/Ktor clients attach cleartext HTTP/2 upgrade headers
|
|
4
|
+
(``Connection: Upgrade, HTTP2-Settings`` + ``Upgrade: h2c`` +
|
|
5
|
+
``HTTP2-Settings``) to ordinary HTTP/1.1 Responses API POSTs. RFC 9110
|
|
6
|
+
section 7.8 lets a server ignore such an offer and answer over HTTP/1.1 —
|
|
7
|
+
upstream OpenAI endpoints do exactly that — but uvicorn's stock protocol
|
|
8
|
+
implementations either wedge on the offer (httptools) or leak the declined
|
|
9
|
+
offer's hop-by-hop headers into the ASGI scope (h11). See
|
|
10
|
+
https://github.com/Soju06/codex-lb/issues/1757 and the module docstring of
|
|
11
|
+
``app.core.http_protocol_httptools`` for the full failure analysis.
|
|
12
|
+
|
|
13
|
+
This module exposes :func:`load_http_protocol_class`, which returns the
|
|
14
|
+
tolerant httptools subclass when httptools is importable (matching uvicorn's
|
|
15
|
+
``auto`` preference) and an h11 subclass with the same header hygiene
|
|
16
|
+
otherwise.
|
|
17
|
+
|
|
18
|
+
Both subclasses also release the keep-alive timer on every connection loss.
|
|
19
|
+
Stock uvicorn cancels it only on a clean close (``exc is None``), so after an
|
|
20
|
+
RST/``ECONNRESET`` the armed ``TimerHandle`` keeps the protocol graph alive for
|
|
21
|
+
``--timeout-keep-alive`` seconds — a per-request leak behind reverse proxies
|
|
22
|
+
that purge idle connections with RST. See
|
|
23
|
+
``tests/integration/test_http_keepalive_timer.py``.
|
|
24
|
+
|
|
25
|
+
Their third job is observational: when a connection is lost while a response
|
|
26
|
+
is still in flight, :func:`stamp_disconnect_into_scope` records the loss in
|
|
27
|
+
that request's ASGI ``scope["state"]`` (key :data:`HTTP_DISCONNECTED_STATE`) —
|
|
28
|
+
on a pipelined connection, in every request that is still open (the httptools
|
|
29
|
+
subclass tracks the active cycle separately from uvicorn's ``self.cycle``).
|
|
30
|
+
Uvicorn's ``send`` silently drops every message once the cycle is marked
|
|
31
|
+
disconnected, and the ASGI ``receive()`` channel cannot tell a mid-stream loss
|
|
32
|
+
from the ``http.disconnect`` it reports after every normal completion, so the
|
|
33
|
+
stamp is the only deterministic way for a streaming response to learn that a
|
|
34
|
+
late write — typically the SSE terminal frame — never reached the transport.
|
|
35
|
+
See ``app.modules.proxy.downstream_delivery`` and
|
|
36
|
+
``tests/integration/test_http_disconnect_stamp.py``.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
import asyncio
|
|
42
|
+
from typing import Any
|
|
43
|
+
|
|
44
|
+
from uvicorn.protocols.http.h11_impl import H11Protocol
|
|
45
|
+
|
|
46
|
+
# ``scope["state"]`` key stamped by ``stamp_disconnect_into_scope``. The value
|
|
47
|
+
# is ``"eof"`` for a clean peer close or the exception type name reported to
|
|
48
|
+
# ``connection_lost`` (e.g. ``"ConnectionResetError"``).
|
|
49
|
+
HTTP_DISCONNECTED_STATE = "codex_lb.http_disconnected"
|
|
50
|
+
|
|
51
|
+
# Hop-by-hop headers that only exist to carry the declined protocol switch.
|
|
52
|
+
# ``HTTP2-Settings`` is defined exclusively for the h2c upgrade (RFC 9113
|
|
53
|
+
# section 3.1) and MUST NOT be forwarded once the offer is declined.
|
|
54
|
+
UPGRADE_HOP_BY_HOP_HEADERS = frozenset({b"upgrade", b"http2-settings"})
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def stamp_disconnect_into_scope(cycle: Any, exc: BaseException | None) -> None:
|
|
58
|
+
"""Record a mid-response connection loss in the in-flight request's ``scope["state"]``.
|
|
59
|
+
|
|
60
|
+
Uvicorn creates ``scope["state"]`` per request (``app_state.copy()``) and
|
|
61
|
+
pure-ASGI middlewares hand the same dict down, so the streaming response
|
|
62
|
+
that is still iterating can read the stamp. Nothing is written once the
|
|
63
|
+
response is complete: keep-alive idle closes leave the key absent. The
|
|
64
|
+
stamp only mutates the existing per-request dict — it adds no reference to
|
|
65
|
+
the protocol and no transport side effect, so it cannot re-introduce the
|
|
66
|
+
keep-alive-timer retention this module also guards against.
|
|
67
|
+
"""
|
|
68
|
+
if cycle is None or cycle.response_complete:
|
|
69
|
+
return
|
|
70
|
+
state = cycle.scope.setdefault("state", {})
|
|
71
|
+
state.setdefault(HTTP_DISCONNECTED_STATE, "eof" if exc is None else type(exc).__name__)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def combined_upgrade_offer(headers: list[tuple[bytes, bytes]]) -> bytes | None:
|
|
75
|
+
"""Return the accepted ``Upgrade`` token, honoring repeated/list-valued fields.
|
|
76
|
+
|
|
77
|
+
Unlike uvicorn's ``_get_upgrade`` — which keeps only the tokens of the
|
|
78
|
+
*last* ``Connection`` field (so ``Connection: Upgrade`` followed by
|
|
79
|
+
``Connection: keep-alive`` hides the offer) and the last ``Upgrade``
|
|
80
|
+
field's raw value (so ``Upgrade: websocket, h2c`` matches nothing) —
|
|
81
|
+
repeated fields are combined per RFC 9110 section 5.3 and the ``Upgrade``
|
|
82
|
+
protocol list is tokenized per section 7.8. ``websocket`` is returned
|
|
83
|
+
whenever it is among the offered protocols (the server may pick any
|
|
84
|
+
offered protocol it supports); otherwise the client's first preference is
|
|
85
|
+
returned. Header names must already be lowercased (both uvicorn
|
|
86
|
+
implementations store them that way).
|
|
87
|
+
"""
|
|
88
|
+
connection_tokens: list[bytes] = []
|
|
89
|
+
upgrade_tokens: list[bytes] = []
|
|
90
|
+
for name, value in headers:
|
|
91
|
+
if name == b"connection":
|
|
92
|
+
connection_tokens.extend(token.lower().strip() for token in value.split(b","))
|
|
93
|
+
elif name == b"upgrade":
|
|
94
|
+
upgrade_tokens.extend(token for token in (token.lower().strip() for token in value.split(b",")) if token)
|
|
95
|
+
if b"upgrade" not in connection_tokens or not upgrade_tokens:
|
|
96
|
+
return None
|
|
97
|
+
if b"websocket" in upgrade_tokens:
|
|
98
|
+
return b"websocket"
|
|
99
|
+
return upgrade_tokens[0]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def offers_ignorable_upgrade(headers: list[tuple[bytes, bytes]]) -> bool:
|
|
103
|
+
"""True when the request offers a non-WebSocket protocol switch (e.g. h2c)."""
|
|
104
|
+
upgrade = combined_upgrade_offer(headers)
|
|
105
|
+
return upgrade is not None and upgrade != b"websocket"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def without_upgrade_headers(headers: list[tuple[bytes, bytes]]) -> list[tuple[bytes, bytes]]:
|
|
109
|
+
"""Drop the declined offer's hop-by-hop headers and ``Connection`` tokens."""
|
|
110
|
+
sanitized: list[tuple[bytes, bytes]] = []
|
|
111
|
+
for name, value in headers:
|
|
112
|
+
if name in UPGRADE_HOP_BY_HOP_HEADERS:
|
|
113
|
+
continue
|
|
114
|
+
if name == b"connection":
|
|
115
|
+
tokens = [token.strip() for token in value.split(b",")]
|
|
116
|
+
kept = [token for token in tokens if token and token.lower() not in UPGRADE_HOP_BY_HOP_HEADERS]
|
|
117
|
+
if not kept:
|
|
118
|
+
continue
|
|
119
|
+
value = b", ".join(kept)
|
|
120
|
+
sanitized.append((name, value))
|
|
121
|
+
return sanitized
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class UpgradeTolerantH11Protocol(H11Protocol):
|
|
125
|
+
"""h11 protocol that hides declined non-WebSocket upgrade offers from the app.
|
|
126
|
+
|
|
127
|
+
The stock h11 implementation already serves such requests as plain
|
|
128
|
+
HTTP/1.1 with the full body, but it exposes the declined offer's
|
|
129
|
+
hop-by-hop headers in the ASGI scope and logs a spurious
|
|
130
|
+
"Unsupported upgrade request." warning. ``_should_upgrade`` is the seam:
|
|
131
|
+
it runs right after ``self.headers`` (the same list object referenced by
|
|
132
|
+
``scope["headers"]``) is populated, so sanitizing in place here is enough.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
def connection_lost(self, exc: Exception | None) -> None:
|
|
136
|
+
# Same defect as httptools_impl: stock h11_impl cancels the keep-alive
|
|
137
|
+
# timer only when exc is None, so an RST-closed idle connection stays
|
|
138
|
+
# pinned by the armed TimerHandle for timeout_keep_alive seconds.
|
|
139
|
+
# super() first: the base sends h11.ConnectionClosed before the exc
|
|
140
|
+
# check; the cancel touches only ``timeout_keep_alive_task``.
|
|
141
|
+
super().connection_lost(exc)
|
|
142
|
+
self._unset_keepalive_if_required()
|
|
143
|
+
# super() marked the in-flight cycle disconnected and keeps self.cycle
|
|
144
|
+
# referenced, so the stamp lands on the request that lost its peer.
|
|
145
|
+
# Unlike httptools, h11 never replaces ``self.cycle`` while a response
|
|
146
|
+
# is in flight: a pipelined follow-up stays unparsed inside the h11
|
|
147
|
+
# connection (``PAUSED``) until ``start_next_cycle``, so the newest
|
|
148
|
+
# cycle *is* the active one.
|
|
149
|
+
stamp_disconnect_into_scope(self.cycle, exc)
|
|
150
|
+
|
|
151
|
+
def _should_upgrade(self) -> bool:
|
|
152
|
+
# Reimplements the stock decision on top of combined Connection fields
|
|
153
|
+
# (RFC 9110 section 5.3): the stock ``_get_upgrade`` keeps only the
|
|
154
|
+
# last field's tokens, so ``Connection: Upgrade`` followed by
|
|
155
|
+
# ``Connection: keep-alive`` would hide the offer entirely.
|
|
156
|
+
upgrade = combined_upgrade_offer(self.headers)
|
|
157
|
+
if upgrade is None:
|
|
158
|
+
return False
|
|
159
|
+
if upgrade == b"websocket":
|
|
160
|
+
if self._should_upgrade_to_ws():
|
|
161
|
+
return True
|
|
162
|
+
self._unsupported_upgrade_warning()
|
|
163
|
+
return False
|
|
164
|
+
self.headers[:] = without_upgrade_headers(self.headers)
|
|
165
|
+
return False
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def load_http_protocol_class() -> type[asyncio.Protocol]:
|
|
169
|
+
"""Return the HTTP protocol implementation for ``uvicorn.Config(http=...)``."""
|
|
170
|
+
try:
|
|
171
|
+
from app.core.http_protocol_httptools import UpgradeTolerantHttpToolsProtocol
|
|
172
|
+
except ImportError:
|
|
173
|
+
# httptools is an optional (transitive) dependency; uvicorn's "auto"
|
|
174
|
+
# selection would fall back to h11 as well.
|
|
175
|
+
return UpgradeTolerantH11Protocol
|
|
176
|
+
return UpgradeTolerantHttpToolsProtocol
|