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,710 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import logging
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from functools import lru_cache
|
|
7
|
+
from ipaddress import ip_address, ip_network
|
|
8
|
+
from typing import cast
|
|
9
|
+
|
|
10
|
+
from fastapi import Request, Security
|
|
11
|
+
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|
12
|
+
from starlette.requests import HTTPConnection
|
|
13
|
+
|
|
14
|
+
from app.core.auth import generate_unique_account_id
|
|
15
|
+
from app.core.auth.api_key_cache import get_api_key_cache
|
|
16
|
+
from app.core.auth.dashboard_access import (
|
|
17
|
+
STEP_UP_PERMISSIONS,
|
|
18
|
+
DashboardPermission,
|
|
19
|
+
DashboardPrincipal,
|
|
20
|
+
Permission,
|
|
21
|
+
Scope,
|
|
22
|
+
admin_principal,
|
|
23
|
+
guest_principal,
|
|
24
|
+
scope_satisfies,
|
|
25
|
+
totp_policy_applies,
|
|
26
|
+
user_principal,
|
|
27
|
+
)
|
|
28
|
+
from app.core.auth.dashboard_mode import DashboardAuthMode, DashboardRequestAuth, get_dashboard_request_auth
|
|
29
|
+
from app.core.auth.dashboard_users_cache import DashboardUsersCache, get_dashboard_users_cache
|
|
30
|
+
from app.core.auth.external_identity import resolve_trusted_header_request
|
|
31
|
+
from app.core.auth.step_up import (
|
|
32
|
+
STEP_UP_COOKIE,
|
|
33
|
+
STEP_UP_UNAVAILABLE_MESSAGE,
|
|
34
|
+
account_step_up_methods,
|
|
35
|
+
is_step_up_fresh,
|
|
36
|
+
)
|
|
37
|
+
from app.core.clients.proxy import CODEX_LB_REQUIRED_CAPABILITY_HEADER
|
|
38
|
+
from app.core.clients.usage import UsageFetchError, fetch_usage
|
|
39
|
+
from app.core.config.settings import get_settings
|
|
40
|
+
from app.core.config.settings_cache import get_settings_cache
|
|
41
|
+
from app.core.crypto import TokenEncryptor
|
|
42
|
+
from app.core.exceptions import DashboardAuthError, DashboardPermissionError, ProxyAuthError, ProxyUpstreamError
|
|
43
|
+
from app.core.request_locality import is_local_request
|
|
44
|
+
from app.core.socket_peer import raw_socket_peer_host
|
|
45
|
+
from app.core.upstream_proxy import UpstreamProxyRouteError, resolve_upstream_route
|
|
46
|
+
from app.core.utils.time import utcnow
|
|
47
|
+
from app.db.models import AccountStatus, DashboardSettings, DashboardUser, DashboardUserStatus
|
|
48
|
+
from app.db.session import get_background_session
|
|
49
|
+
from app.modules.accounts.repository import AccountsRepository
|
|
50
|
+
from app.modules.api_keys.repository import ApiKeysRepository
|
|
51
|
+
from app.modules.api_keys.service import ApiKeyData, ApiKeyInvalidError, ApiKeysService
|
|
52
|
+
from app.modules.dashboard_auth.service import (
|
|
53
|
+
DASHBOARD_SESSION_COOKIE,
|
|
54
|
+
DashboardSessionState,
|
|
55
|
+
get_dashboard_session_store,
|
|
56
|
+
get_step_up_cookie_store,
|
|
57
|
+
is_local_password_session,
|
|
58
|
+
session_clock,
|
|
59
|
+
)
|
|
60
|
+
from app.modules.dashboard_roles.service import resolve_role_grants
|
|
61
|
+
from app.modules.dashboard_users.break_glass import break_glass_second_factor_required, local_login_admits
|
|
62
|
+
|
|
63
|
+
logger = logging.getLogger(__name__)
|
|
64
|
+
|
|
65
|
+
_bearer = HTTPBearer(description="API key (e.g. sk-clb-…)", auto_error=False)
|
|
66
|
+
#: The only routes an account that still has to enrol a TOTP secret may reach
|
|
67
|
+
#: (an explicit allow-list, not the module prefix: guest-password management,
|
|
68
|
+
#: password removal and TOTP disable live under the same prefix and stay closed).
|
|
69
|
+
TOTP_ENROLLMENT_ALLOWED_PATHS: frozenset[str] = frozenset(
|
|
70
|
+
{
|
|
71
|
+
"/api/dashboard-auth/session",
|
|
72
|
+
"/api/dashboard-auth/totp/setup/start",
|
|
73
|
+
"/api/dashboard-auth/totp/setup/confirm",
|
|
74
|
+
"/api/dashboard-auth/totp/verify",
|
|
75
|
+
"/api/dashboard-auth/logout",
|
|
76
|
+
"/api/dashboard-auth/logout-all",
|
|
77
|
+
"/api/dashboard-auth/me",
|
|
78
|
+
"/api/dashboard-auth/password/change",
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
#: ``auth_method`` of the implicit admin on a local install without a password.
|
|
82
|
+
AUTH_METHOD_LOCAL_BOOTSTRAP = "local_bootstrap"
|
|
83
|
+
_CODEX_USAGE_IDENTITY_INACTIVE_WORKSPACE_STATUSES = {
|
|
84
|
+
AccountStatus.PAUSED,
|
|
85
|
+
AccountStatus.DEACTIVATED,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# --- Error format markers ---
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def set_openai_error_format(request: Request) -> None:
|
|
93
|
+
request.state.error_format = "openai"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def set_dashboard_error_format(request: Request) -> None:
|
|
97
|
+
request.state.error_format = "dashboard"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def set_scim_error_format(request: Request) -> None:
|
|
101
|
+
"""Mark the request so every refusal answers RFC 7644's envelope.
|
|
102
|
+
|
|
103
|
+
A router-level marker, exactly like the two above; unmatched ``/scim``
|
|
104
|
+
paths fall back on the path prefix in ``_error_format`` instead.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
request.state.error_format = "scim"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# --- Proxy API key auth ---
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
async def validate_proxy_api_key(
|
|
114
|
+
request: Request,
|
|
115
|
+
credentials: HTTPAuthorizationCredentials | None = Security(_bearer),
|
|
116
|
+
) -> ApiKeyData | None:
|
|
117
|
+
"""A required-capability header authenticates even when global proxy API-key auth is disabled."""
|
|
118
|
+
|
|
119
|
+
authorization = None if credentials is None else f"Bearer {credentials.credentials}"
|
|
120
|
+
if request.headers.getlist(CODEX_LB_REQUIRED_CAPABILITY_HEADER):
|
|
121
|
+
return await validate_required_proxy_api_key_authorization(authorization)
|
|
122
|
+
return await validate_proxy_api_key_authorization(authorization, request=request)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
async def validate_proxy_api_key_authorization(
|
|
126
|
+
authorization: str | None,
|
|
127
|
+
*,
|
|
128
|
+
request: HTTPConnection | None = None,
|
|
129
|
+
) -> ApiKeyData | None:
|
|
130
|
+
settings = await get_settings_cache().get()
|
|
131
|
+
if not settings.api_key_auth_enabled:
|
|
132
|
+
if request is not None and not is_local_request(request):
|
|
133
|
+
if not _is_proxy_unauthenticated_socket_peer_allowed(request):
|
|
134
|
+
raise ProxyAuthError("Proxy authentication must be configured before remote access is allowed")
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
token = _extract_bearer_token(authorization)
|
|
138
|
+
if not token:
|
|
139
|
+
raise ProxyAuthError("Missing API key in Authorization header")
|
|
140
|
+
|
|
141
|
+
return await _validate_api_key_token(token)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
async def validate_required_proxy_api_key_authorization(authorization: str | None) -> ApiKeyData:
|
|
145
|
+
"""Validate a proxy API key even when global proxy auth is disabled."""
|
|
146
|
+
|
|
147
|
+
token = _extract_bearer_token(authorization)
|
|
148
|
+
if not token:
|
|
149
|
+
raise ProxyAuthError("Missing API key in Authorization header")
|
|
150
|
+
return await _validate_api_key_token(token)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
async def _validate_api_key_token(token: str) -> ApiKeyData:
|
|
154
|
+
"""Validate a plain API key token and return the typed key data."""
|
|
155
|
+
|
|
156
|
+
token_hash = hashlib.sha256(token.encode("utf-8")).hexdigest()
|
|
157
|
+
cache = get_api_key_cache()
|
|
158
|
+
cached = cast(ApiKeyData | None, await cache.get(token_hash))
|
|
159
|
+
if cached is not None:
|
|
160
|
+
if cached.expires_at is not None and cached.expires_at <= utcnow():
|
|
161
|
+
await cache.invalidate(token_hash)
|
|
162
|
+
else:
|
|
163
|
+
return cached
|
|
164
|
+
|
|
165
|
+
version_before_read = cache.version
|
|
166
|
+
async with get_background_session() as session:
|
|
167
|
+
service = ApiKeysService(ApiKeysRepository(session))
|
|
168
|
+
try:
|
|
169
|
+
validated = await service.validate_key(token)
|
|
170
|
+
await cache.set(token_hash, validated, if_version=version_before_read)
|
|
171
|
+
return validated
|
|
172
|
+
except ApiKeyInvalidError as exc:
|
|
173
|
+
raise ProxyAuthError(str(exc)) from exc
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
async def validate_required_proxy_api_key(
|
|
177
|
+
request: Request,
|
|
178
|
+
credentials: HTTPAuthorizationCredentials | None = Security(_bearer),
|
|
179
|
+
) -> ApiKeyData:
|
|
180
|
+
"""Require a valid proxy API key regardless of the global auth setting."""
|
|
181
|
+
|
|
182
|
+
authorization = None if credentials is None else f"Bearer {credentials.credentials}"
|
|
183
|
+
return await validate_required_proxy_api_key_authorization(authorization)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# --- Self-service usage endpoint auth (always requires valid key) ---
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
async def validate_usage_api_key(
|
|
190
|
+
request: Request,
|
|
191
|
+
credentials: HTTPAuthorizationCredentials | None = Security(_bearer),
|
|
192
|
+
) -> ApiKeyData:
|
|
193
|
+
"""Validate API key for self-service usage endpoint.
|
|
194
|
+
|
|
195
|
+
Unlike ``validate_proxy_api_key``, this dependency always requires a valid
|
|
196
|
+
Bearer API key, regardless of the global ``api_key_auth_enabled`` setting.
|
|
197
|
+
Raises ProxyAuthError when the key is missing or invalid.
|
|
198
|
+
"""
|
|
199
|
+
authorization = None if credentials is None else f"Bearer {credentials.credentials}"
|
|
200
|
+
return await validate_required_proxy_api_key_authorization(authorization)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# --- Dashboard session auth ---
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _set_dashboard_principal(request: Request, principal: DashboardPrincipal) -> DashboardPrincipal:
|
|
207
|
+
request.state.dashboard_principal = principal
|
|
208
|
+
return principal
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _get_cached_dashboard_principal(request: Request) -> DashboardPrincipal | None:
|
|
212
|
+
principal = getattr(request.state, "dashboard_principal", None)
|
|
213
|
+
return principal if isinstance(principal, DashboardPrincipal) else None
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
async def _resolve_session_user(
|
|
217
|
+
state: DashboardSessionState | None, users_cache: DashboardUsersCache
|
|
218
|
+
) -> DashboardUser | None:
|
|
219
|
+
"""The account behind a user cookie, or ``None`` when the cookie no longer binds to one.
|
|
220
|
+
|
|
221
|
+
A missing, disabled, or invited account and a stale ``session_generation``
|
|
222
|
+
all fail the same way; the caller answers ``authentication_required``.
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
if state is None or not state.is_user or state.user_id is None:
|
|
226
|
+
return None
|
|
227
|
+
user = await users_cache.get_user(state.user_id)
|
|
228
|
+
if user is None or user.status != DashboardUserStatus.ACTIVE.value:
|
|
229
|
+
return None
|
|
230
|
+
if user.session_generation != state.session_generation:
|
|
231
|
+
return None
|
|
232
|
+
return user
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _user_session_principal(
|
|
236
|
+
request: Request,
|
|
237
|
+
user: DashboardUser,
|
|
238
|
+
state: DashboardSessionState,
|
|
239
|
+
*,
|
|
240
|
+
settings: DashboardSettings,
|
|
241
|
+
) -> DashboardPrincipal:
|
|
242
|
+
grants = resolve_role_grants(user.role)
|
|
243
|
+
# An emergency account that holds a secret always presents it, whatever
|
|
244
|
+
# the two toggles say: the account a tightened policy relies on must never
|
|
245
|
+
# be reachable on a password alone.
|
|
246
|
+
totp_required = totp_policy_applies(
|
|
247
|
+
required_on_login=settings.totp_required_on_login,
|
|
248
|
+
required_for_admin_role=settings.totp_required_for_admin_role,
|
|
249
|
+
grants=grants,
|
|
250
|
+
) or break_glass_second_factor_required(user)
|
|
251
|
+
totp_configured = user.totp_secret_encrypted is not None
|
|
252
|
+
if totp_required and totp_configured and not state.totp_verified:
|
|
253
|
+
raise DashboardAuthError("TOTP verification is required for dashboard access", code="totp_required")
|
|
254
|
+
totp_enrollment_required = totp_required and not totp_configured
|
|
255
|
+
# Routes guarded only by this dependency (account inventory, request logs,
|
|
256
|
+
# ...) assume a reader who may see everything. Until the self-service phase
|
|
257
|
+
# makes own-scoped routes declare their own requirement (this guard then
|
|
258
|
+
# moves to route level), a role without dashboard:read at scope `all`
|
|
259
|
+
# (e.g. member) is refused outright rather than admitted to all-scope views.
|
|
260
|
+
if not scope_satisfies(grants.get(Permission.DASHBOARD_READ), Scope.ALL):
|
|
261
|
+
raise DashboardPermissionError(
|
|
262
|
+
f"Dashboard permission '{Permission.DASHBOARD_READ.value}' is required",
|
|
263
|
+
code="permission_required",
|
|
264
|
+
param=Permission.DASHBOARD_READ.value,
|
|
265
|
+
)
|
|
266
|
+
principal = user_principal(
|
|
267
|
+
user,
|
|
268
|
+
grants,
|
|
269
|
+
auth_method=state.auth_method,
|
|
270
|
+
totp_enrollment_required=totp_enrollment_required,
|
|
271
|
+
step_up_verified_at=state.step_up_verified_at,
|
|
272
|
+
)
|
|
273
|
+
# An account that still has to enrol may only reach the self-service routes
|
|
274
|
+
# listed above; those routes gate themselves, so every other route that
|
|
275
|
+
# reaches this dependency is refused here.
|
|
276
|
+
if totp_enrollment_required and request.url.path not in TOTP_ENROLLMENT_ALLOWED_PATHS:
|
|
277
|
+
raise DashboardPermissionError(
|
|
278
|
+
"TOTP enrollment is required before dashboard access",
|
|
279
|
+
code="totp_enrollment_required",
|
|
280
|
+
)
|
|
281
|
+
return _set_dashboard_principal(request, principal)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
async def _password_fallback_principal(request: Request) -> DashboardPrincipal | None:
|
|
285
|
+
"""A password-verified cookie the local login policy admits, so an emergency
|
|
286
|
+
account stays reachable while the proxy asserts an identity the resolver refuses.
|
|
287
|
+
|
|
288
|
+
``local_login_policy`` decides which cookie counts: ``enabled`` (the
|
|
289
|
+
default) admits every active account, which is what shipped before this
|
|
290
|
+
change; ``admins_only`` admits the admin preset; ``break_glass_only``
|
|
291
|
+
admits only a designated emergency account. One function, so the gate and
|
|
292
|
+
the session response that advertises the fallback cannot disagree.
|
|
293
|
+
"""
|
|
294
|
+
|
|
295
|
+
users_cache = get_dashboard_users_cache()
|
|
296
|
+
state = get_dashboard_session_store().get(request.cookies.get(DASHBOARD_SESSION_COOKIE))
|
|
297
|
+
session_user = await _resolve_session_user(state, users_cache)
|
|
298
|
+
if state is None or session_user is None or not is_local_password_session(state):
|
|
299
|
+
return None
|
|
300
|
+
settings = await get_settings_cache().get()
|
|
301
|
+
if not local_login_admits(session_user, settings.local_login_policy):
|
|
302
|
+
return None
|
|
303
|
+
return _user_session_principal(request, session_user, state, settings=settings)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
async def _trusted_header_principal(request: Request, request_auth: DashboardRequestAuth) -> DashboardPrincipal:
|
|
307
|
+
"""The account behind the proxy-asserted identity, or a 401 naming why there is none.
|
|
308
|
+
|
|
309
|
+
Unknown identities become accounts through the provider's
|
|
310
|
+
``unknown_identity_role_id`` (admin by default, D10). A refused identity
|
|
311
|
+
answers ``identity_not_provisioned`` and a disabled account
|
|
312
|
+
``account_disabled`` -- unless a password-verified cookie rides along, in
|
|
313
|
+
which case that account is served (a resolved header account always wins
|
|
314
|
+
over the cookie). The TOTP policy is not applied to header sessions yet:
|
|
315
|
+
there is no cookie to carry a verified step, so applying it would lock
|
|
316
|
+
every header user out (the step-up change wires it, honouring the
|
|
317
|
+
provider's ``idp_mfa_enforced``).
|
|
318
|
+
"""
|
|
319
|
+
|
|
320
|
+
resolution = await resolve_trusted_header_request(request, request_auth)
|
|
321
|
+
if resolution is None:
|
|
322
|
+
raise DashboardAuthError("Reverse proxy authentication is required", code="proxy_auth_required")
|
|
323
|
+
if resolution.user is None:
|
|
324
|
+
fallback = await _password_fallback_principal(request)
|
|
325
|
+
if fallback is not None:
|
|
326
|
+
return fallback
|
|
327
|
+
if resolution.denial == "account_disabled":
|
|
328
|
+
raise DashboardAuthError("This account is disabled", code="account_disabled")
|
|
329
|
+
raise DashboardAuthError(
|
|
330
|
+
"Your account is not ready yet; ask an administrator to add you", code="identity_not_provisioned"
|
|
331
|
+
)
|
|
332
|
+
grants = resolve_role_grants(resolution.user.role)
|
|
333
|
+
if not scope_satisfies(grants.get(Permission.DASHBOARD_READ), Scope.ALL):
|
|
334
|
+
raise DashboardPermissionError(
|
|
335
|
+
f"Dashboard permission '{Permission.DASHBOARD_READ.value}' is required",
|
|
336
|
+
code="permission_required",
|
|
337
|
+
param=Permission.DASHBOARD_READ.value,
|
|
338
|
+
)
|
|
339
|
+
return user_principal(
|
|
340
|
+
resolution.user, grants, auth_method=request_auth.mode.value, auth_mode=DashboardAuthMode.TRUSTED_HEADER
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
async def validate_dashboard_session(request: Request) -> DashboardPrincipal:
|
|
345
|
+
cached = _get_cached_dashboard_principal(request)
|
|
346
|
+
if cached is not None:
|
|
347
|
+
return cached
|
|
348
|
+
|
|
349
|
+
request_auth = get_dashboard_request_auth(request)
|
|
350
|
+
if request_auth is not None and request_auth.mode == DashboardAuthMode.TRUSTED_HEADER:
|
|
351
|
+
return _set_dashboard_principal(request, await _trusted_header_principal(request, request_auth))
|
|
352
|
+
if request_auth is not None:
|
|
353
|
+
return _set_dashboard_principal(
|
|
354
|
+
request,
|
|
355
|
+
admin_principal(auth_mode=request_auth.mode, actor=request_auth.actor, auth_method=request_auth.mode.value),
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
settings = await get_settings_cache().get()
|
|
359
|
+
users_cache = get_dashboard_users_cache()
|
|
360
|
+
auth_state = await users_cache.local_auth_state()
|
|
361
|
+
password_required = auth_state.requires_auth
|
|
362
|
+
# The admin-role requirement binds accounts only, so on its own it never
|
|
363
|
+
# turns a passwordless install into one that demands a login.
|
|
364
|
+
requires_auth = password_required or settings.totp_required_on_login
|
|
365
|
+
guest_access_enabled = settings.guest_access_enabled
|
|
366
|
+
guest_password_required = guest_access_enabled and settings.guest_password_hash is not None
|
|
367
|
+
passwordless_guest_fallback_allowed = not (
|
|
368
|
+
get_dashboard_request_auth_mode() == DashboardAuthMode.TRUSTED_HEADER
|
|
369
|
+
and requires_auth
|
|
370
|
+
and guest_access_enabled
|
|
371
|
+
and not guest_password_required
|
|
372
|
+
)
|
|
373
|
+
session_id = request.cookies.get(DASHBOARD_SESSION_COOKIE)
|
|
374
|
+
state = get_dashboard_session_store().get(session_id)
|
|
375
|
+
session_user = await _resolve_session_user(state, users_cache)
|
|
376
|
+
|
|
377
|
+
# Behind a reverse proxy a header-less request only gets in on a password
|
|
378
|
+
# cookie the local login policy still admits (the same decision
|
|
379
|
+
# ``_password_fallback_principal`` makes for a refused identity).
|
|
380
|
+
has_password_fallback_session = (
|
|
381
|
+
state is not None
|
|
382
|
+
and session_user is not None
|
|
383
|
+
and is_local_password_session(state)
|
|
384
|
+
and local_login_admits(session_user, settings.local_login_policy)
|
|
385
|
+
)
|
|
386
|
+
if get_dashboard_request_auth_mode() == DashboardAuthMode.TRUSTED_HEADER and not has_password_fallback_session:
|
|
387
|
+
raise DashboardAuthError("Reverse proxy authentication is required", code="proxy_auth_required")
|
|
388
|
+
# A guest cookie is only ever minted under the current guest generation, so
|
|
389
|
+
# a matching generation proves it passed whatever guest credential applied.
|
|
390
|
+
if (
|
|
391
|
+
state is not None
|
|
392
|
+
and state.is_guest
|
|
393
|
+
and guest_access_enabled
|
|
394
|
+
and state.guest_session_generation == settings.guest_session_generation
|
|
395
|
+
and (guest_password_required or passwordless_guest_fallback_allowed)
|
|
396
|
+
):
|
|
397
|
+
return _set_dashboard_principal(request, guest_principal())
|
|
398
|
+
if state is not None and session_user is not None and state.password_verified:
|
|
399
|
+
return _user_session_principal(request, session_user, state, settings=settings)
|
|
400
|
+
|
|
401
|
+
if not requires_auth:
|
|
402
|
+
if not is_local_request(request):
|
|
403
|
+
if guest_access_enabled:
|
|
404
|
+
if not guest_password_required:
|
|
405
|
+
return _set_dashboard_principal(request, guest_principal())
|
|
406
|
+
raise DashboardAuthError("Authentication is required")
|
|
407
|
+
raise DashboardAuthError(
|
|
408
|
+
"Remote bootstrap is required before dashboard access is allowed",
|
|
409
|
+
code="bootstrap_required",
|
|
410
|
+
)
|
|
411
|
+
return _set_dashboard_principal(
|
|
412
|
+
request,
|
|
413
|
+
admin_principal(auth_mode=DashboardAuthMode.STANDARD, auth_method=AUTH_METHOD_LOCAL_BOOTSTRAP),
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
if guest_access_enabled and not guest_password_required and passwordless_guest_fallback_allowed:
|
|
417
|
+
return _set_dashboard_principal(request, guest_principal())
|
|
418
|
+
|
|
419
|
+
if auth_state.active_local_password_users == 0 and settings.totp_required_on_login:
|
|
420
|
+
logger.warning(
|
|
421
|
+
"dashboard_auth_migration_inconsistency no active local password user"
|
|
422
|
+
" while totp_required_on_login=true metric=dashboard_auth_migration_inconsistency"
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
raise DashboardAuthError("Authentication is required")
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
async def require_dashboard_write_access(request: Request) -> DashboardPrincipal:
|
|
429
|
+
principal = await validate_dashboard_session(request)
|
|
430
|
+
if not principal.can(DashboardPermission.WRITE):
|
|
431
|
+
raise DashboardPermissionError(
|
|
432
|
+
"Read-only dashboard access cannot modify dashboard state",
|
|
433
|
+
code="read_only_access",
|
|
434
|
+
)
|
|
435
|
+
return principal
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
@dataclass(frozen=True, slots=True)
|
|
439
|
+
class PermissionRequirement:
|
|
440
|
+
"""Declares the permission a route dependency enforces.
|
|
441
|
+
|
|
442
|
+
Attached to the dependency callables produced by
|
|
443
|
+
:func:`require_dashboard_permission` so route-authorization audits can read
|
|
444
|
+
the requirement without invoking the dependency.
|
|
445
|
+
"""
|
|
446
|
+
|
|
447
|
+
permission: Permission
|
|
448
|
+
minimum_scope: Scope = Scope.ALL
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def ensure_dashboard_permission(
|
|
452
|
+
principal: DashboardPrincipal,
|
|
453
|
+
permission: Permission,
|
|
454
|
+
*,
|
|
455
|
+
minimum_scope: Scope = Scope.ALL,
|
|
456
|
+
) -> None:
|
|
457
|
+
if principal.has(permission, minimum_scope=minimum_scope):
|
|
458
|
+
return
|
|
459
|
+
raise DashboardPermissionError(
|
|
460
|
+
f"Dashboard permission '{permission.value}' is required",
|
|
461
|
+
code="permission_required",
|
|
462
|
+
param=permission.value,
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
_SAFE_METHODS: frozenset[str] = frozenset({"GET", "HEAD", "OPTIONS"})
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def recorded_step_up(request: Request, user: DashboardUser) -> int | None:
|
|
470
|
+
"""The most recent step-up this request carries for ``user``, wherever it rides.
|
|
471
|
+
|
|
472
|
+
A trusted-header account has no session cookie, so its step-up rides in
|
|
473
|
+
the separate step-up cookie — or in a fallback password session of the
|
|
474
|
+
same account that happens to accompany the request. Both are bound to the
|
|
475
|
+
account's ``session_generation``, so revoking its sessions or resetting
|
|
476
|
+
its TOTP voids the proof instead of leaving it usable for the rest of the
|
|
477
|
+
window. Enforcement and the session response read the same sources, so
|
|
478
|
+
what the dashboard reports is what the gate accepts.
|
|
479
|
+
"""
|
|
480
|
+
|
|
481
|
+
from_cookie = get_step_up_cookie_store().get(
|
|
482
|
+
request.cookies.get(STEP_UP_COOKIE),
|
|
483
|
+
user_id=user.id,
|
|
484
|
+
session_generation=user.session_generation,
|
|
485
|
+
)
|
|
486
|
+
state = get_dashboard_session_store().get(request.cookies.get(DASHBOARD_SESSION_COOKIE))
|
|
487
|
+
from_session = (
|
|
488
|
+
state.step_up_verified_at
|
|
489
|
+
if state is not None
|
|
490
|
+
and state.is_user
|
|
491
|
+
and state.user_id == user.id
|
|
492
|
+
and state.password_verified
|
|
493
|
+
and state.session_generation == user.session_generation
|
|
494
|
+
else None
|
|
495
|
+
)
|
|
496
|
+
recorded = [value for value in (from_cookie, from_session) if value is not None]
|
|
497
|
+
return max(recorded) if recorded else None
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
async def ensure_step_up(request: Request, principal: DashboardPrincipal, permission: Permission) -> None:
|
|
501
|
+
"""Require a fresh step-up (PLAN §5 H5) before a mutation guarded by ``permission``.
|
|
502
|
+
|
|
503
|
+
Reads are never gated. Principals without an account (the implicit local
|
|
504
|
+
admin, the disabled-auth principal) have no credential to re-verify and are
|
|
505
|
+
exempt; every account is held to it, whatever provider signed it in. A
|
|
506
|
+
stale or missing step-up answers ``403 step_up_required`` naming the
|
|
507
|
+
factors the account can present — including its identity provider when
|
|
508
|
+
that is the only thing it has; an account with no factor at all answers
|
|
509
|
+
``403 step_up_unavailable``.
|
|
510
|
+
"""
|
|
511
|
+
|
|
512
|
+
if request.method in _SAFE_METHODS or principal.user_id is None:
|
|
513
|
+
return
|
|
514
|
+
user = await get_dashboard_users_cache().get_user(principal.user_id)
|
|
515
|
+
# The principal's own claim came from a cookie this request already
|
|
516
|
+
# validated against the account's generation; the other sources are
|
|
517
|
+
# checked against it inside recorded_step_up.
|
|
518
|
+
recorded = [principal.step_up_verified_at] if principal.step_up_verified_at is not None else []
|
|
519
|
+
if user is not None:
|
|
520
|
+
recorded += [value for value in (recorded_step_up(request, user),) if value is not None]
|
|
521
|
+
if recorded and is_step_up_fresh(max(recorded), now=session_clock()):
|
|
522
|
+
return
|
|
523
|
+
methods = await account_step_up_methods(user) if user is not None else []
|
|
524
|
+
if not methods:
|
|
525
|
+
raise DashboardPermissionError(STEP_UP_UNAVAILABLE_MESSAGE, code="step_up_unavailable", param=permission.value)
|
|
526
|
+
raise DashboardPermissionError(
|
|
527
|
+
"Confirm your identity to continue",
|
|
528
|
+
code="step_up_required",
|
|
529
|
+
param=permission.value,
|
|
530
|
+
details={"methods": list(methods)},
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
class DashboardPermissionDependency:
|
|
535
|
+
"""Route dependency: validate the dashboard session, then enforce one permission.
|
|
536
|
+
|
|
537
|
+
Mutations guarded by a :data:`STEP_UP_PERMISSIONS` member also require a
|
|
538
|
+
recent step-up. Instances are callable so FastAPI can resolve them with
|
|
539
|
+
``Depends`` while route-authorization audits read :attr:`requirement`
|
|
540
|
+
without invoking them.
|
|
541
|
+
"""
|
|
542
|
+
|
|
543
|
+
__slots__ = ("requirement",)
|
|
544
|
+
|
|
545
|
+
def __init__(self, requirement: PermissionRequirement) -> None:
|
|
546
|
+
self.requirement = requirement
|
|
547
|
+
|
|
548
|
+
async def __call__(self, request: Request) -> DashboardPrincipal:
|
|
549
|
+
principal = await validate_dashboard_session(request)
|
|
550
|
+
ensure_dashboard_permission(
|
|
551
|
+
principal,
|
|
552
|
+
self.requirement.permission,
|
|
553
|
+
minimum_scope=self.requirement.minimum_scope,
|
|
554
|
+
)
|
|
555
|
+
if self.requirement.permission in STEP_UP_PERMISSIONS:
|
|
556
|
+
await ensure_step_up(request, principal, self.requirement.permission)
|
|
557
|
+
return principal
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
@lru_cache(maxsize=None)
|
|
561
|
+
def _dependency_for(requirement: PermissionRequirement) -> DashboardPermissionDependency:
|
|
562
|
+
return DashboardPermissionDependency(requirement)
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def require_dashboard_permission(
|
|
566
|
+
permission: Permission,
|
|
567
|
+
*,
|
|
568
|
+
minimum_scope: Scope = Scope.ALL,
|
|
569
|
+
) -> DashboardPermissionDependency:
|
|
570
|
+
"""Return the shared dependency enforcing ``permission`` at ``minimum_scope``.
|
|
571
|
+
|
|
572
|
+
Cached per :class:`PermissionRequirement` (not per call spelling) so every
|
|
573
|
+
router shares one callable per requirement, keeping dependency overrides
|
|
574
|
+
and OpenAPI stable.
|
|
575
|
+
"""
|
|
576
|
+
|
|
577
|
+
return _dependency_for(PermissionRequirement(permission=permission, minimum_scope=minimum_scope))
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def ensure_dashboard_admin_access(principal: DashboardPrincipal) -> None:
|
|
581
|
+
"""Backward-compatible alias for the ``conversations:read`` requirement."""
|
|
582
|
+
|
|
583
|
+
ensure_dashboard_permission(principal, Permission.CONVERSATIONS_READ)
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
async def require_dashboard_admin_access(request: Request) -> DashboardPrincipal:
|
|
587
|
+
"""Backward-compatible alias for ``require_dashboard_permission(CONVERSATIONS_READ)``."""
|
|
588
|
+
|
|
589
|
+
return await require_dashboard_permission(Permission.CONVERSATIONS_READ)(request)
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def get_dashboard_request_auth_mode() -> DashboardAuthMode:
|
|
593
|
+
from app.core.config.settings import get_settings
|
|
594
|
+
|
|
595
|
+
return get_settings().dashboard_auth_mode
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def _is_proxy_unauthenticated_socket_peer_allowed(request: HTTPConnection) -> bool:
|
|
599
|
+
socket_host = raw_socket_peer_host(request)
|
|
600
|
+
if socket_host is None:
|
|
601
|
+
return False
|
|
602
|
+
|
|
603
|
+
try:
|
|
604
|
+
socket_ip = ip_address(socket_host)
|
|
605
|
+
except ValueError:
|
|
606
|
+
return False
|
|
607
|
+
|
|
608
|
+
configured_cidrs = get_settings().proxy_unauthenticated_client_cidrs
|
|
609
|
+
return any(socket_ip in ip_network(cidr, strict=False) for cidr in configured_cidrs)
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
# --- Codex usage caller identity auth ---
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
async def validate_codex_usage_identity(request: Request) -> ApiKeyData | None:
|
|
616
|
+
token = _extract_bearer_token(request.headers.get("Authorization"))
|
|
617
|
+
if not token:
|
|
618
|
+
raise ProxyAuthError("Missing ChatGPT token in Authorization header")
|
|
619
|
+
|
|
620
|
+
raw_account_id = request.headers.get("chatgpt-account-id")
|
|
621
|
+
account_id = raw_account_id.strip() if raw_account_id else ""
|
|
622
|
+
if not account_id:
|
|
623
|
+
if token.startswith("sk-clb-"):
|
|
624
|
+
return await _validate_api_key_token(token)
|
|
625
|
+
raise ProxyAuthError("Missing chatgpt-account-id header")
|
|
626
|
+
|
|
627
|
+
async with get_background_session() as session:
|
|
628
|
+
accounts_repo = AccountsRepository(session)
|
|
629
|
+
account = await accounts_repo.get_active_by_chatgpt_account_id(account_id)
|
|
630
|
+
if account is None:
|
|
631
|
+
raise ProxyAuthError("Unknown or inactive chatgpt-account-id")
|
|
632
|
+
local_account_id = account.id
|
|
633
|
+
local_account_email = account.email
|
|
634
|
+
try:
|
|
635
|
+
route = await resolve_upstream_route(
|
|
636
|
+
session,
|
|
637
|
+
account_id=local_account_id,
|
|
638
|
+
operation="usage_identity",
|
|
639
|
+
scope="account",
|
|
640
|
+
encryptor=TokenEncryptor(),
|
|
641
|
+
)
|
|
642
|
+
except UpstreamProxyRouteError as exc:
|
|
643
|
+
raise ProxyUpstreamError("Unable to resolve upstream proxy route for ChatGPT credentials") from exc
|
|
644
|
+
|
|
645
|
+
try:
|
|
646
|
+
usage_payload = await fetch_usage(
|
|
647
|
+
access_token=token,
|
|
648
|
+
account_id=account_id,
|
|
649
|
+
route=route,
|
|
650
|
+
allow_direct_egress=route is None,
|
|
651
|
+
)
|
|
652
|
+
except UsageFetchError as exc:
|
|
653
|
+
if exc.status_code == 429:
|
|
654
|
+
from app.core.exceptions import ProxyRateLimitError
|
|
655
|
+
|
|
656
|
+
raise ProxyRateLimitError(exc.message) from exc
|
|
657
|
+
if exc.status_code in (401, 403):
|
|
658
|
+
raise ProxyAuthError("Invalid ChatGPT token or chatgpt-account-id") from exc
|
|
659
|
+
raise ProxyUpstreamError("Unable to validate ChatGPT credentials at this time") from exc
|
|
660
|
+
if usage_payload is not None and (usage_payload.workspace_id or usage_payload.workspace_label):
|
|
661
|
+
expected_account_id = generate_unique_account_id(
|
|
662
|
+
account_id,
|
|
663
|
+
local_account_email,
|
|
664
|
+
usage_payload.workspace_id,
|
|
665
|
+
usage_payload.workspace_label,
|
|
666
|
+
)
|
|
667
|
+
async with get_background_session() as session:
|
|
668
|
+
accounts_repo = AccountsRepository(session)
|
|
669
|
+
workspace_account = await accounts_repo.get_by_id(expected_account_id)
|
|
670
|
+
if workspace_account is not None and workspace_account.chatgpt_account_id == account_id:
|
|
671
|
+
if workspace_account.status in _CODEX_USAGE_IDENTITY_INACTIVE_WORKSPACE_STATUSES:
|
|
672
|
+
raise ProxyAuthError("Unknown or inactive chatgpt-account-id")
|
|
673
|
+
local_account_id = workspace_account.id
|
|
674
|
+
try:
|
|
675
|
+
route = await resolve_upstream_route(
|
|
676
|
+
session,
|
|
677
|
+
account_id=local_account_id,
|
|
678
|
+
operation="usage_identity",
|
|
679
|
+
scope="account",
|
|
680
|
+
encryptor=TokenEncryptor(),
|
|
681
|
+
)
|
|
682
|
+
except UpstreamProxyRouteError as exc:
|
|
683
|
+
raise ProxyUpstreamError("Unable to resolve upstream proxy route for ChatGPT credentials") from exc
|
|
684
|
+
request.state.codex_usage_identity_access_token = token
|
|
685
|
+
request.state.codex_usage_identity_chatgpt_account_id = account_id
|
|
686
|
+
request.state.codex_usage_identity_account_id = local_account_id
|
|
687
|
+
request.state.codex_usage_identity_route = route
|
|
688
|
+
request.state.codex_usage_identity_payload = usage_payload
|
|
689
|
+
return None
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
async def validate_codex_provider_usage_identity(request: Request) -> ApiKeyData | None:
|
|
693
|
+
"""Bind provider capability intent to a proxy API-key principal before usage I/O."""
|
|
694
|
+
|
|
695
|
+
if request.headers.getlist(CODEX_LB_REQUIRED_CAPABILITY_HEADER):
|
|
696
|
+
return await validate_required_proxy_api_key_authorization(request.headers.get("authorization"))
|
|
697
|
+
return await validate_codex_usage_identity(request)
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
def _extract_bearer_token(authorization: str | None) -> str | None:
|
|
701
|
+
if authorization is None:
|
|
702
|
+
return None
|
|
703
|
+
prefix = "bearer "
|
|
704
|
+
value = authorization.strip()
|
|
705
|
+
if not value.lower().startswith(prefix):
|
|
706
|
+
return None
|
|
707
|
+
token = value[len(prefix) :].strip()
|
|
708
|
+
if not token:
|
|
709
|
+
return None
|
|
710
|
+
return token
|