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,126 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from importlib import import_module
|
|
4
|
+
from types import ModuleType
|
|
5
|
+
|
|
6
|
+
from starlette.responses import JSONResponse
|
|
7
|
+
from starlette.types import ASGIApp, Receive, Scope, Send
|
|
8
|
+
|
|
9
|
+
from app.core.resilience.overload import (
|
|
10
|
+
deny_websocket_with_http_response,
|
|
11
|
+
is_proxy_path,
|
|
12
|
+
local_unavailable_error,
|
|
13
|
+
merge_retry_after_headers,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
_DRAIN_MESSAGE = "Server is draining"
|
|
17
|
+
|
|
18
|
+
_DRAIN_ALLOWED_HTTP_PATHS = frozenset(
|
|
19
|
+
{
|
|
20
|
+
"/health/live",
|
|
21
|
+
"/internal/drain/start",
|
|
22
|
+
"/internal/drain/stop",
|
|
23
|
+
"/internal/drain/status",
|
|
24
|
+
"/internal/bridge/responses",
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
_IN_FLIGHT_EXCLUDED_HTTP_PATHS = frozenset(
|
|
29
|
+
{
|
|
30
|
+
"/health/live",
|
|
31
|
+
"/health/ready",
|
|
32
|
+
"/health/startup",
|
|
33
|
+
"/internal/drain/start",
|
|
34
|
+
"/internal/drain/stop",
|
|
35
|
+
"/internal/drain/status",
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
_IN_FLIGHT_WEBSOCKET_PATHS = frozenset(
|
|
40
|
+
{
|
|
41
|
+
"/backend-api/codex/responses",
|
|
42
|
+
"/v1/responses",
|
|
43
|
+
}
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class InFlightMiddleware:
|
|
48
|
+
def __init__(self, app: ASGIApp) -> None:
|
|
49
|
+
self.app = app
|
|
50
|
+
# Resolved lazily on the first request (import-cycle safety) and cached
|
|
51
|
+
# so the hot path skips the per-request sys.modules lookup.
|
|
52
|
+
self._shutdown_state: ModuleType | None = None
|
|
53
|
+
|
|
54
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
55
|
+
"""Track admitted work and reject new requests while the server drains."""
|
|
56
|
+
scope_type = scope["type"]
|
|
57
|
+
if scope_type not in {"http", "websocket"}:
|
|
58
|
+
await self.app(scope, receive, send)
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
shutdown_state = self._shutdown_state
|
|
62
|
+
if shutdown_state is None:
|
|
63
|
+
shutdown_state = self._shutdown_state = import_module("app.core.shutdown")
|
|
64
|
+
|
|
65
|
+
if scope_type == "websocket":
|
|
66
|
+
# Register before checking the drain barrier. A synchronous signal
|
|
67
|
+
# can interleave anywhere between Python bytecodes; increment-first
|
|
68
|
+
# guarantees that the scope is either rejected and unregistered or
|
|
69
|
+
# already visible to shutdown as pre-barrier work.
|
|
70
|
+
shutdown_state.increment_in_flight()
|
|
71
|
+
if shutdown_state.is_draining():
|
|
72
|
+
shutdown_state.decrement_in_flight()
|
|
73
|
+
# ``websocket.close`` before the handshake is surfaced by ASGI
|
|
74
|
+
# servers as an opaque HTTP 403, which SDK clients treat as a
|
|
75
|
+
# terminal access error. Deny the upgrade with the same
|
|
76
|
+
# retryable 503 + Retry-After the overload bulkhead uses so the
|
|
77
|
+
# drain reads as transient on every client.
|
|
78
|
+
path = scope.get("path", "")
|
|
79
|
+
await deny_websocket_with_http_response(
|
|
80
|
+
receive,
|
|
81
|
+
send,
|
|
82
|
+
status_code=503,
|
|
83
|
+
payload=(
|
|
84
|
+
local_unavailable_error(_DRAIN_MESSAGE) if is_proxy_path(path) else {"detail": _DRAIN_MESSAGE}
|
|
85
|
+
),
|
|
86
|
+
headers=merge_retry_after_headers(),
|
|
87
|
+
)
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
if scope.get("path", "") not in _IN_FLIGHT_WEBSOCKET_PATHS:
|
|
91
|
+
# Admission is global, but only Responses sockets participate
|
|
92
|
+
# in turn-aware drain. Other long-lived protocols are closed by
|
|
93
|
+
# Uvicorn after the pre-connection drain barrier.
|
|
94
|
+
shutdown_state.decrement_in_flight()
|
|
95
|
+
await self.app(scope, receive, send)
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
await self.app(scope, receive, send)
|
|
100
|
+
finally:
|
|
101
|
+
shutdown_state.decrement_in_flight()
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
path = scope.get("path", "")
|
|
105
|
+
track_in_flight = path not in _IN_FLIGHT_EXCLUDED_HTTP_PATHS
|
|
106
|
+
if track_in_flight:
|
|
107
|
+
# As with Responses WebSockets, register before checking the
|
|
108
|
+
# barrier so a synchronous signal cannot admit invisible work.
|
|
109
|
+
shutdown_state.increment_in_flight()
|
|
110
|
+
try:
|
|
111
|
+
if shutdown_state.is_draining() and path not in _DRAIN_ALLOWED_HTTP_PATHS:
|
|
112
|
+
response = JSONResponse(
|
|
113
|
+
status_code=503,
|
|
114
|
+
content={
|
|
115
|
+
"error": {
|
|
116
|
+
"type": "service_unavailable",
|
|
117
|
+
"message": _DRAIN_MESSAGE,
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
)
|
|
121
|
+
await response(scope, receive, send)
|
|
122
|
+
return
|
|
123
|
+
await self.app(scope, receive, send)
|
|
124
|
+
finally:
|
|
125
|
+
if track_in_flight:
|
|
126
|
+
shutdown_state.decrement_in_flight()
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from fastapi import FastAPI
|
|
6
|
+
from starlette._utils import get_route_path
|
|
7
|
+
from starlette.datastructures import Headers
|
|
8
|
+
from starlette.requests import Request
|
|
9
|
+
from starlette.responses import JSONResponse
|
|
10
|
+
from starlette.types import ASGIApp, Receive, Scope, Send
|
|
11
|
+
|
|
12
|
+
from app.core.errors import dashboard_error, openai_error
|
|
13
|
+
from app.core.runtime_logging import log_error_response
|
|
14
|
+
|
|
15
|
+
_LIMITED_MULTIPART_PATHS = frozenset(
|
|
16
|
+
{
|
|
17
|
+
"/api/accounts/import",
|
|
18
|
+
"/backend-api/transcribe",
|
|
19
|
+
"/v1/audio/transcriptions",
|
|
20
|
+
"/v1/images/edits",
|
|
21
|
+
}
|
|
22
|
+
)
|
|
23
|
+
_OPENAI_MULTIPART_PATH_PREFIXES = ("/backend-api", "/v1")
|
|
24
|
+
_CONTENT_ENCODING_GATE_HANDLED_STATE = "_codex_lb_multipart_content_encoding_gate_handled"
|
|
25
|
+
_UNSUPPORTED_CONTENT_ENCODING_STATE = "_codex_lb_unsupported_multipart_content_encoding"
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _canonical_route_path(scope: Scope) -> str:
|
|
31
|
+
path = get_route_path(scope)
|
|
32
|
+
if path != "/":
|
|
33
|
+
path = path.rstrip("/")
|
|
34
|
+
return path
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class UnsupportedMultipartContentEncoding(Exception):
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def is_route_owned_multipart_operation(scope: Scope) -> bool:
|
|
42
|
+
return scope.get("method") == "POST" and _canonical_route_path(scope) in _LIMITED_MULTIPART_PATHS
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _uses_openai_multipart_errors(scope: Scope) -> bool:
|
|
46
|
+
path = _canonical_route_path(scope)
|
|
47
|
+
return any(path == prefix or path.startswith(f"{prefix}/") for prefix in _OPENAI_MULTIPART_PATH_PREFIXES)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def multipart_error_response(
|
|
51
|
+
request: Request,
|
|
52
|
+
*,
|
|
53
|
+
status_code: int,
|
|
54
|
+
code: str,
|
|
55
|
+
message: str,
|
|
56
|
+
param: str | None = None,
|
|
57
|
+
) -> JSONResponse:
|
|
58
|
+
uses_openai_errors = _uses_openai_multipart_errors(request.scope)
|
|
59
|
+
response_code = "invalid_request_error" if uses_openai_errors and code == "invalid_request" else code
|
|
60
|
+
log_error_response(
|
|
61
|
+
logger,
|
|
62
|
+
request,
|
|
63
|
+
status_code,
|
|
64
|
+
response_code,
|
|
65
|
+
message,
|
|
66
|
+
category="openai_error_response" if uses_openai_errors else "dashboard_error_response",
|
|
67
|
+
)
|
|
68
|
+
if uses_openai_errors:
|
|
69
|
+
error = openai_error(response_code, message, error_type="invalid_request_error")
|
|
70
|
+
if param is not None:
|
|
71
|
+
error["error"]["param"] = param
|
|
72
|
+
return JSONResponse(status_code=status_code, content=error)
|
|
73
|
+
return JSONResponse(status_code=status_code, content=dashboard_error(response_code, message))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _without_content_encoding(scope: Scope, *, unsupported: bool = False) -> Scope:
|
|
77
|
+
copied = dict(scope)
|
|
78
|
+
copied["headers"] = [
|
|
79
|
+
(name, value) for name, value in scope.get("headers", []) if name.lower() != b"content-encoding"
|
|
80
|
+
]
|
|
81
|
+
state = dict(scope.get("state", {}))
|
|
82
|
+
state[_CONTENT_ENCODING_GATE_HANDLED_STATE] = True
|
|
83
|
+
if unsupported:
|
|
84
|
+
state[_UNSUPPORTED_CONTENT_ENCODING_STATE] = True
|
|
85
|
+
copied["state"] = state
|
|
86
|
+
return copied
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def multipart_content_encoding_gate_was_applied(scope: Scope) -> bool:
|
|
90
|
+
return scope.get("state", {}).get(_CONTENT_ENCODING_GATE_HANDLED_STATE) is True
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def raise_for_unsupported_multipart_content_encoding(request: Request) -> None:
|
|
94
|
+
if getattr(request.state, _UNSUPPORTED_CONTENT_ENCODING_STATE, False) is True:
|
|
95
|
+
raise UnsupportedMultipartContentEncoding
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class MultipartContentEncodingMiddleware:
|
|
99
|
+
def __init__(self, app: ASGIApp) -> None:
|
|
100
|
+
self.app = app
|
|
101
|
+
|
|
102
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
103
|
+
if scope["type"] != "http":
|
|
104
|
+
await self.app(scope, receive, send)
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
headers = Headers(scope=scope)
|
|
108
|
+
if not is_route_owned_multipart_operation(scope):
|
|
109
|
+
await self.app(scope, receive, send)
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
content_encoding_values = headers.getlist("content-encoding")
|
|
113
|
+
if not content_encoding_values:
|
|
114
|
+
await self.app(scope, receive, send)
|
|
115
|
+
return
|
|
116
|
+
encodings = [
|
|
117
|
+
encoding.strip().lower()
|
|
118
|
+
for value in content_encoding_values
|
|
119
|
+
for encoding in value.split(",")
|
|
120
|
+
if encoding.strip()
|
|
121
|
+
]
|
|
122
|
+
if not encodings or all(value == "identity" for value in encodings):
|
|
123
|
+
await self.app(_without_content_encoding(scope), receive, send)
|
|
124
|
+
return
|
|
125
|
+
await self.app(_without_content_encoding(scope, unsupported=True), receive, send)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def add_multipart_content_encoding_middleware(app: FastAPI) -> None:
|
|
129
|
+
app.add_middleware(MultipartContentEncodingMiddleware)
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Redact Live WebSocket server scopes and rewrite duplicated Codex paths.
|
|
2
|
+
|
|
3
|
+
Some OpenAI-compatible clients unconditionally append ``/v1/`` to a configured
|
|
4
|
+
``/backend-api/codex`` base URL. For non-Live requests this middleware collapses
|
|
5
|
+
``/backend-api/codex/v1/<rest>`` to the canonical
|
|
6
|
+
``/backend-api/codex/<rest>`` route for both HTTP and WebSocket scopes.
|
|
7
|
+
|
|
8
|
+
Live WebSocket handshakes require the inverse ownership rule: Uvicorn retains
|
|
9
|
+
the original ASGI scope for accepted and rejected handshake logs, while the
|
|
10
|
+
application still needs the original path and query for routing. The middleware
|
|
11
|
+
therefore copies the original routing scope for downstream use, then redacts the
|
|
12
|
+
server-owned scope before any downstream accept or rejection can be emitted.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from fastapi import FastAPI
|
|
21
|
+
from starlette.types import Receive, Scope, Send
|
|
22
|
+
|
|
23
|
+
from app.core.clients.proxy_websocket import REALTIME_LIVE_CALL_ID_ROUTE_REGEX
|
|
24
|
+
|
|
25
|
+
# Alias rewriting stays scoped to the duplicated Codex prefix. Live WebSocket
|
|
26
|
+
# redaction separately covers every routed ingress family plus the unsupported
|
|
27
|
+
# duplicated Live alias.
|
|
28
|
+
_CODEX_V1_PREFIX = "/backend-api/codex/v1/"
|
|
29
|
+
_CODEX_V1_PREFIX_BYTES = _CODEX_V1_PREFIX.encode("ascii")
|
|
30
|
+
_CODEX_CANONICAL_PREFIX = "/backend-api/codex/"
|
|
31
|
+
_CODEX_CANONICAL_PREFIX_BYTES = _CODEX_CANONICAL_PREFIX.encode("ascii")
|
|
32
|
+
_V1_LIVE_PREFIX = "/v1/live/"
|
|
33
|
+
_V1_LIVE_PREFIX_BYTES = _V1_LIVE_PREFIX.encode("ascii")
|
|
34
|
+
_V1_REALTIME_PATH = "/v1/realtime"
|
|
35
|
+
_V1_REALTIME_RAW_PATH = _V1_REALTIME_PATH.encode("ascii")
|
|
36
|
+
_REDACTED_CALL_ID = "<redacted>"
|
|
37
|
+
_REDACTED_CALL_ID_BYTES = b"%3Credacted%3E"
|
|
38
|
+
_REALTIME_LIVE_CALL_ID_PATTERN = re.compile(rf"{REALTIME_LIVE_CALL_ID_ROUTE_REGEX}\Z")
|
|
39
|
+
_REALTIME_LIVE_PATH_REDACTIONS = (
|
|
40
|
+
(
|
|
41
|
+
_CODEX_V1_PREFIX,
|
|
42
|
+
f"{_CODEX_V1_PREFIX}{_REDACTED_CALL_ID}",
|
|
43
|
+
_CODEX_V1_PREFIX_BYTES + _REDACTED_CALL_ID_BYTES,
|
|
44
|
+
),
|
|
45
|
+
(
|
|
46
|
+
_CODEX_CANONICAL_PREFIX,
|
|
47
|
+
f"{_CODEX_CANONICAL_PREFIX}{_REDACTED_CALL_ID}",
|
|
48
|
+
_CODEX_CANONICAL_PREFIX_BYTES + _REDACTED_CALL_ID_BYTES,
|
|
49
|
+
),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _canonicalize_backend_api_codex_path(path: str) -> str:
|
|
54
|
+
"""Collapse ``/backend-api/codex/v1/<rest>`` -> ``/backend-api/codex/<rest>``.
|
|
55
|
+
|
|
56
|
+
Returns the input unchanged for any path that is not the duplicated
|
|
57
|
+
Codex ``/v1/`` shape. In particular, ``/backend-api/codex`` (no
|
|
58
|
+
rest) and ``/backend-api/codex/v1`` (no further rest) are left
|
|
59
|
+
alone -- those are legal request paths a future contributor might
|
|
60
|
+
register, and collapsing them would silently change routing
|
|
61
|
+
semantics.
|
|
62
|
+
"""
|
|
63
|
+
if not path.startswith(_CODEX_V1_PREFIX):
|
|
64
|
+
return path
|
|
65
|
+
return _CODEX_CANONICAL_PREFIX + path[len(_CODEX_V1_PREFIX) :]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _realtime_live_scope_redaction(path: str) -> tuple[str, bytes] | None:
|
|
69
|
+
if path == _V1_REALTIME_PATH:
|
|
70
|
+
return _V1_REALTIME_PATH, _V1_REALTIME_RAW_PATH
|
|
71
|
+
|
|
72
|
+
# The v3 ingress owns this whole namespace, including requests whose
|
|
73
|
+
# suffix cannot match a route. Rejected handshakes must therefore redact
|
|
74
|
+
# malformed, empty, and overlong suffixes before Uvicorn can log them.
|
|
75
|
+
if path.startswith(_V1_LIVE_PREFIX):
|
|
76
|
+
return (
|
|
77
|
+
f"{_V1_LIVE_PREFIX}{_REDACTED_CALL_ID}",
|
|
78
|
+
_V1_LIVE_PREFIX_BYTES + _REDACTED_CALL_ID_BYTES,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Generic Codex paths share their namespace with non-Live routes, so their
|
|
82
|
+
# redaction boundary remains the exact Live call-id grammar.
|
|
83
|
+
for prefix, redacted_path, redacted_raw_path in _REALTIME_LIVE_PATH_REDACTIONS:
|
|
84
|
+
if path.startswith(prefix) and _REALTIME_LIVE_CALL_ID_PATTERN.fullmatch(path[len(prefix) :]) is not None:
|
|
85
|
+
return redacted_path, redacted_raw_path
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def redact_realtime_live_path(path: str) -> str:
|
|
90
|
+
"""Return the credential-safe path used by pre-routing diagnostics."""
|
|
91
|
+
|
|
92
|
+
redaction = _realtime_live_scope_redaction(path)
|
|
93
|
+
return path if redaction is None else redaction[0]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _canonicalize_raw_path(raw_path: bytes) -> bytes:
|
|
97
|
+
if not raw_path.startswith(_CODEX_V1_PREFIX_BYTES):
|
|
98
|
+
return raw_path
|
|
99
|
+
return _CODEX_CANONICAL_PREFIX_BYTES + raw_path[len(_CODEX_V1_PREFIX_BYTES) :]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class BackendApiCodexV1AliasMiddleware:
|
|
103
|
+
"""Keep Live handshake logs private and canonicalise the duplicated Codex prefix.
|
|
104
|
+
|
|
105
|
+
This runs immediately inside trusted-proxy projection. For a Live WebSocket,
|
|
106
|
+
the downstream application receives a copy containing the projected client
|
|
107
|
+
metadata and original routing values while the server-owned scope is
|
|
108
|
+
redacted in place. Lifespan and non-Live scopes preserve the existing
|
|
109
|
+
alias-only behavior.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
def __init__(self, app: Any) -> None:
|
|
113
|
+
self.app = app
|
|
114
|
+
|
|
115
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
116
|
+
scope_type = scope.get("type")
|
|
117
|
+
path = scope.get("path")
|
|
118
|
+
|
|
119
|
+
if scope_type == "websocket" and isinstance(path, str):
|
|
120
|
+
redaction = _realtime_live_scope_redaction(path)
|
|
121
|
+
if redaction is not None:
|
|
122
|
+
routing_scope = dict(scope)
|
|
123
|
+
routing_scope["path"] = _canonicalize_backend_api_codex_path(path)
|
|
124
|
+
if isinstance(raw_path := routing_scope.get("raw_path"), bytes):
|
|
125
|
+
routing_scope["raw_path"] = _canonicalize_raw_path(raw_path)
|
|
126
|
+
redacted_path, redacted_raw_path = redaction
|
|
127
|
+
scope["path"] = redacted_path
|
|
128
|
+
scope["raw_path"] = redacted_raw_path
|
|
129
|
+
scope["query_string"] = b""
|
|
130
|
+
await self.app(routing_scope, receive, send)
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
if scope_type in {"http", "websocket"} and isinstance(path, str) and path.startswith(_CODEX_V1_PREFIX):
|
|
134
|
+
rewritten = _canonicalize_backend_api_codex_path(path)
|
|
135
|
+
if rewritten != path:
|
|
136
|
+
# Preserve the server-owned scope when only downstream routing changes.
|
|
137
|
+
routing_scope = dict(scope)
|
|
138
|
+
routing_scope["path"] = rewritten
|
|
139
|
+
if isinstance(raw_path := routing_scope.get("raw_path"), bytes):
|
|
140
|
+
routing_scope["raw_path"] = _canonicalize_raw_path(raw_path)
|
|
141
|
+
scope = routing_scope
|
|
142
|
+
await self.app(scope, receive, send)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def add_backend_api_codex_v1_alias_middleware(app: FastAPI) -> None:
|
|
146
|
+
"""Register path rewriting and Live scope redaction inside trusted proxying."""
|
|
147
|
+
|
|
148
|
+
app.add_middleware(BackendApiCodexV1AliasMiddleware)
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from fastapi import FastAPI
|
|
6
|
+
from starlette._utils import get_route_path
|
|
7
|
+
from starlette.datastructures import Headers
|
|
8
|
+
from starlette.requests import Request
|
|
9
|
+
from starlette.responses import JSONResponse
|
|
10
|
+
from starlette.types import ASGIApp, Message, Receive, Scope, Send
|
|
11
|
+
|
|
12
|
+
from app.core.errors import SCIM_CONTENT_TYPE, dashboard_error, openai_error, scim_error
|
|
13
|
+
from app.core.ingress_limits import MAX_DECOMPRESSED_BODY_BYTES, MAX_DECOMPRESSED_RESPONSES_BODY_BYTES
|
|
14
|
+
from app.core.middleware.multipart_content_encoding import (
|
|
15
|
+
is_route_owned_multipart_operation,
|
|
16
|
+
multipart_content_encoding_gate_was_applied,
|
|
17
|
+
)
|
|
18
|
+
from app.core.runtime_logging import log_error_response
|
|
19
|
+
|
|
20
|
+
REQUEST_BODY_TOO_LARGE_MESSAGE = "Request body exceeds the maximum allowed size"
|
|
21
|
+
|
|
22
|
+
_RESPONSES_INGRESS_PATHS = frozenset(
|
|
23
|
+
{
|
|
24
|
+
"/backend-api/codex/responses",
|
|
25
|
+
"/v1/responses",
|
|
26
|
+
}
|
|
27
|
+
)
|
|
28
|
+
_OPENAI_INGRESS_PATH_PREFIXES = (
|
|
29
|
+
"/api/codex",
|
|
30
|
+
"/backend-api",
|
|
31
|
+
"/internal/bridge",
|
|
32
|
+
"/v1",
|
|
33
|
+
)
|
|
34
|
+
_REQUEST_BODY_TOO_LARGE_STATE = "_codex_lb_request_body_too_large"
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class _RequestBodyTooLarge(Exception):
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def request_body_limit_for_path(path: str) -> int:
|
|
44
|
+
if path.rstrip("/") in _RESPONSES_INGRESS_PATHS:
|
|
45
|
+
return max(MAX_DECOMPRESSED_BODY_BYTES, MAX_DECOMPRESSED_RESPONSES_BODY_BYTES)
|
|
46
|
+
return MAX_DECOMPRESSED_BODY_BYTES
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _path_belongs_to(path: str, prefix: str) -> bool:
|
|
50
|
+
return path == prefix or path.startswith(f"{prefix}/")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _uses_openai_ingress_errors(path: str) -> bool:
|
|
54
|
+
return any(_path_belongs_to(path, prefix) for prefix in _OPENAI_INGRESS_PATH_PREFIXES)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _uses_scim_ingress_errors(path: str) -> bool:
|
|
58
|
+
"""``/scim`` answers RFC 7644's envelope even before routing.
|
|
59
|
+
|
|
60
|
+
The ingress guard runs ahead of the router, so a refusal here would
|
|
61
|
+
otherwise be the dashboard's shape on a surface no dashboard client reads.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
return _path_belongs_to(path, "/scim")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def request_ingress_error_response(
|
|
68
|
+
request: Request,
|
|
69
|
+
*,
|
|
70
|
+
status_code: int,
|
|
71
|
+
code: str,
|
|
72
|
+
message: str,
|
|
73
|
+
) -> JSONResponse:
|
|
74
|
+
path = get_route_path(request.scope)
|
|
75
|
+
uses_openai_errors = _uses_openai_ingress_errors(path)
|
|
76
|
+
uses_scim_errors = _uses_scim_ingress_errors(path)
|
|
77
|
+
response_code = "invalid_request_error" if uses_openai_errors and code == "invalid_request" else code
|
|
78
|
+
category = "dashboard_error_response"
|
|
79
|
+
if uses_openai_errors:
|
|
80
|
+
category = "openai_error_response"
|
|
81
|
+
elif uses_scim_errors:
|
|
82
|
+
category = "scim_error_response"
|
|
83
|
+
log_error_response(
|
|
84
|
+
logger,
|
|
85
|
+
request,
|
|
86
|
+
status_code,
|
|
87
|
+
response_code,
|
|
88
|
+
message,
|
|
89
|
+
category=category,
|
|
90
|
+
)
|
|
91
|
+
if uses_scim_errors:
|
|
92
|
+
return JSONResponse(
|
|
93
|
+
status_code=status_code,
|
|
94
|
+
content=scim_error(status_code, message),
|
|
95
|
+
media_type=SCIM_CONTENT_TYPE,
|
|
96
|
+
)
|
|
97
|
+
if uses_openai_errors:
|
|
98
|
+
return JSONResponse(
|
|
99
|
+
status_code=status_code,
|
|
100
|
+
content=openai_error(response_code, message, error_type="invalid_request_error"),
|
|
101
|
+
)
|
|
102
|
+
return JSONResponse(
|
|
103
|
+
status_code=status_code,
|
|
104
|
+
content=dashboard_error(response_code, message),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def request_body_limit_was_exceeded(request: Request) -> bool:
|
|
109
|
+
return getattr(request.state, _REQUEST_BODY_TOO_LARGE_STATE, False) is True
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _mark_request_body_limit_exceeded(scope: Scope) -> None:
|
|
113
|
+
state = scope.setdefault("state", {})
|
|
114
|
+
state[_REQUEST_BODY_TOO_LARGE_STATE] = True
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _is_unencoded_multipart(headers: Headers) -> bool:
|
|
118
|
+
content_type = headers.get("content-type", "")
|
|
119
|
+
media_type = content_type.partition(";")[0].strip().lower()
|
|
120
|
+
return media_type == "multipart/form-data" and headers.get("content-encoding") is None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _declared_content_length(headers: Headers) -> int | None:
|
|
124
|
+
value = headers.get("content-length")
|
|
125
|
+
if value is None:
|
|
126
|
+
return None
|
|
127
|
+
try:
|
|
128
|
+
parsed = int(value)
|
|
129
|
+
except ValueError:
|
|
130
|
+
return None
|
|
131
|
+
return parsed if parsed >= 0 else None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class RequestBodyLimitMiddleware:
|
|
135
|
+
def __init__(self, app: ASGIApp) -> None:
|
|
136
|
+
self.app = app
|
|
137
|
+
|
|
138
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
139
|
+
if scope["type"] != "http":
|
|
140
|
+
await self.app(scope, receive, send)
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
headers = Headers(scope=scope)
|
|
144
|
+
route_owned_multipart = is_route_owned_multipart_operation(scope) and _is_unencoded_multipart(headers)
|
|
145
|
+
if multipart_content_encoding_gate_was_applied(scope) or route_owned_multipart:
|
|
146
|
+
await self.app(scope, receive, send)
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
path = get_route_path(scope)
|
|
150
|
+
limit = request_body_limit_for_path(path)
|
|
151
|
+
declared_length = _declared_content_length(headers)
|
|
152
|
+
if declared_length is not None and declared_length > limit:
|
|
153
|
+
response = request_ingress_error_response(
|
|
154
|
+
Request(scope),
|
|
155
|
+
status_code=413,
|
|
156
|
+
code="payload_too_large",
|
|
157
|
+
message=REQUEST_BODY_TOO_LARGE_MESSAGE,
|
|
158
|
+
)
|
|
159
|
+
await response(scope, receive, send)
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
received = 0
|
|
163
|
+
response_started = False
|
|
164
|
+
|
|
165
|
+
async def limited_receive() -> Message:
|
|
166
|
+
nonlocal received
|
|
167
|
+
message = await receive()
|
|
168
|
+
if message["type"] == "http.request":
|
|
169
|
+
received += len(message.get("body", b""))
|
|
170
|
+
if received > limit:
|
|
171
|
+
_mark_request_body_limit_exceeded(scope)
|
|
172
|
+
raise _RequestBodyTooLarge
|
|
173
|
+
return message
|
|
174
|
+
|
|
175
|
+
async def tracked_send(message: Message) -> None:
|
|
176
|
+
nonlocal response_started
|
|
177
|
+
if message["type"] == "http.response.start":
|
|
178
|
+
response_started = True
|
|
179
|
+
await send(message)
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
await self.app(scope, limited_receive, tracked_send)
|
|
183
|
+
except _RequestBodyTooLarge:
|
|
184
|
+
if response_started:
|
|
185
|
+
raise
|
|
186
|
+
response = request_ingress_error_response(
|
|
187
|
+
Request(scope),
|
|
188
|
+
status_code=413,
|
|
189
|
+
code="payload_too_large",
|
|
190
|
+
message=REQUEST_BODY_TOO_LARGE_MESSAGE,
|
|
191
|
+
)
|
|
192
|
+
await response(scope, receive, send)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def add_request_body_limit_middleware(app: FastAPI) -> None:
|
|
196
|
+
app.add_middleware(RequestBodyLimitMiddleware)
|