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
|
File without changes
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Read-only roles API. Reading roles is part of managing users (``users:manage``);
|
|
2
|
+
writing roles (``roles:manage``) arrives with the custom-role editor."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from fastapi import APIRouter, Depends
|
|
7
|
+
|
|
8
|
+
from app.core.auth.dashboard_access import Permission, RoleKind
|
|
9
|
+
from app.core.auth.dependencies import require_dashboard_permission, set_dashboard_error_format
|
|
10
|
+
from app.dependencies import DashboardRolesContext, get_dashboard_roles_context
|
|
11
|
+
from app.modules.dashboard_roles.schemas import DashboardRoleResponse, PermissionDescriptorResponse, RoleGrantResponse
|
|
12
|
+
from app.modules.dashboard_roles.service import (
|
|
13
|
+
permission_descriptors,
|
|
14
|
+
resolve_role_grants,
|
|
15
|
+
role_assignable_to_users,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
router = APIRouter(
|
|
19
|
+
prefix="/api/dashboard-roles",
|
|
20
|
+
tags=["dashboard"],
|
|
21
|
+
dependencies=[Depends(require_dashboard_permission(Permission.USERS_MANAGE)), Depends(set_dashboard_error_format)],
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@router.get("", response_model=list[DashboardRoleResponse])
|
|
26
|
+
async def list_roles(
|
|
27
|
+
context: DashboardRolesContext = Depends(get_dashboard_roles_context),
|
|
28
|
+
) -> list[DashboardRoleResponse]:
|
|
29
|
+
users_by_role = await context.repository.users_count_by_role()
|
|
30
|
+
responses: list[DashboardRoleResponse] = []
|
|
31
|
+
for role in await context.repository.list_roles():
|
|
32
|
+
is_preset = RoleKind(role.kind) is RoleKind.PRESET
|
|
33
|
+
responses.append(
|
|
34
|
+
DashboardRoleResponse(
|
|
35
|
+
id=role.id,
|
|
36
|
+
slug=role.slug,
|
|
37
|
+
name=role.name,
|
|
38
|
+
description=role.description,
|
|
39
|
+
kind=role.kind,
|
|
40
|
+
locked=is_preset,
|
|
41
|
+
# Code is the truth for presets: the row's flag only mirrors it.
|
|
42
|
+
assignable_to_users=role_assignable_to_users(role),
|
|
43
|
+
grants=[
|
|
44
|
+
RoleGrantResponse(permission=permission.value, scope=scope.value)
|
|
45
|
+
for permission, scope in sorted(resolve_role_grants(role).items(), key=lambda item: item[0].value)
|
|
46
|
+
],
|
|
47
|
+
users_count=users_by_role.get(role.id, 0),
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
return responses
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@router.get("/permissions", response_model=list[PermissionDescriptorResponse])
|
|
54
|
+
async def list_permissions() -> list[PermissionDescriptorResponse]:
|
|
55
|
+
return [
|
|
56
|
+
PermissionDescriptorResponse(
|
|
57
|
+
permission=descriptor.permission.value,
|
|
58
|
+
description=descriptor.description,
|
|
59
|
+
implies=[dependency.value for dependency in descriptor.implies],
|
|
60
|
+
own_supported=descriptor.own_supported,
|
|
61
|
+
privileged=descriptor.privileged,
|
|
62
|
+
)
|
|
63
|
+
for descriptor in permission_descriptors()
|
|
64
|
+
]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import func, select
|
|
6
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
7
|
+
from sqlalchemy.orm import selectinload
|
|
8
|
+
|
|
9
|
+
from app.core.auth.dashboard_access import PRESET_ROLE_IDS, PresetRoleSlug, RoleKind
|
|
10
|
+
from app.db.models import DashboardRoleRecord, DashboardUser
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DashboardRolesRepository:
|
|
14
|
+
def __init__(self, session: AsyncSession) -> None:
|
|
15
|
+
self._session = session
|
|
16
|
+
|
|
17
|
+
async def list_roles(self) -> Sequence[DashboardRoleRecord]:
|
|
18
|
+
stmt = (
|
|
19
|
+
select(DashboardRoleRecord)
|
|
20
|
+
.options(selectinload(DashboardRoleRecord.grants))
|
|
21
|
+
.order_by(DashboardRoleRecord.kind.desc(), DashboardRoleRecord.slug.asc())
|
|
22
|
+
)
|
|
23
|
+
return (await self._session.execute(stmt)).scalars().all()
|
|
24
|
+
|
|
25
|
+
async def get_role(self, role_id: str) -> DashboardRoleRecord | None:
|
|
26
|
+
stmt = (
|
|
27
|
+
select(DashboardRoleRecord)
|
|
28
|
+
.options(selectinload(DashboardRoleRecord.grants))
|
|
29
|
+
.where(DashboardRoleRecord.id == role_id)
|
|
30
|
+
)
|
|
31
|
+
return (await self._session.execute(stmt)).scalar_one_or_none()
|
|
32
|
+
|
|
33
|
+
async def get_role_by_slug(self, slug: str) -> DashboardRoleRecord | None:
|
|
34
|
+
stmt = (
|
|
35
|
+
select(DashboardRoleRecord)
|
|
36
|
+
.options(selectinload(DashboardRoleRecord.grants))
|
|
37
|
+
.where(DashboardRoleRecord.slug == slug)
|
|
38
|
+
)
|
|
39
|
+
return (await self._session.execute(stmt)).scalar_one_or_none()
|
|
40
|
+
|
|
41
|
+
async def get_preset_role(self, slug: PresetRoleSlug) -> DashboardRoleRecord | None:
|
|
42
|
+
return await self.get_role(PRESET_ROLE_IDS[slug])
|
|
43
|
+
|
|
44
|
+
async def count_custom_roles(self) -> int:
|
|
45
|
+
stmt = (
|
|
46
|
+
select(func.count())
|
|
47
|
+
.select_from(DashboardRoleRecord)
|
|
48
|
+
.where(DashboardRoleRecord.kind == RoleKind.CUSTOM.value)
|
|
49
|
+
)
|
|
50
|
+
return int((await self._session.execute(stmt)).scalar_one())
|
|
51
|
+
|
|
52
|
+
async def users_count_by_role(self) -> dict[str, int]:
|
|
53
|
+
stmt = select(DashboardUser.role_id, func.count()).group_by(DashboardUser.role_id)
|
|
54
|
+
return {str(role_id): int(count) for role_id, count in (await self._session.execute(stmt)).all()}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from app.modules.shared.schemas import DashboardModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class RoleGrantResponse(DashboardModel):
|
|
7
|
+
permission: str
|
|
8
|
+
scope: str
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DashboardRoleResponse(DashboardModel):
|
|
12
|
+
id: str
|
|
13
|
+
slug: str
|
|
14
|
+
name: str
|
|
15
|
+
description: str | None = None
|
|
16
|
+
kind: str
|
|
17
|
+
#: Presets are defined by the product and cannot be edited by anyone.
|
|
18
|
+
locked: bool
|
|
19
|
+
assignable_to_users: bool
|
|
20
|
+
grants: list[RoleGrantResponse]
|
|
21
|
+
users_count: int
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PermissionDescriptorResponse(DashboardModel):
|
|
25
|
+
permission: str
|
|
26
|
+
description: str
|
|
27
|
+
implies: list[str]
|
|
28
|
+
own_supported: bool
|
|
29
|
+
privileged: bool
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Idempotent seeding of the preset ``dashboard_roles`` rows.
|
|
2
|
+
|
|
3
|
+
The Alembic revision (with its own revision-pinned table definition) and the
|
|
4
|
+
test schema reset (which builds the schema with ``create_all`` instead of
|
|
5
|
+
Alembic) both call this so the five preset rows exist wherever the table does. Rows are inserted with
|
|
6
|
+
"do nothing on conflict" semantics and are never updated: preset grants live
|
|
7
|
+
in code (``PRESET_ROLE_GRANTS``), so there is nothing version-dependent to
|
|
8
|
+
reconcile and a replica running older code can never strip a newer grant.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from sqlalchemy import Table, select
|
|
14
|
+
from sqlalchemy.dialects import postgresql, sqlite
|
|
15
|
+
from sqlalchemy.engine import Connection
|
|
16
|
+
from sqlalchemy.ext.asyncio import AsyncConnection
|
|
17
|
+
|
|
18
|
+
from app.core.auth.dashboard_access import (
|
|
19
|
+
ASSIGNABLE_PRESET_ROLES,
|
|
20
|
+
PRESET_ROLE_IDS,
|
|
21
|
+
PRESET_ROLE_NAMES,
|
|
22
|
+
PresetRoleSlug,
|
|
23
|
+
RoleKind,
|
|
24
|
+
)
|
|
25
|
+
from app.db.models import Base, DashboardRoleRecord
|
|
26
|
+
|
|
27
|
+
_ROLES_TABLE: Table = Base.metadata.tables[DashboardRoleRecord.__tablename__]
|
|
28
|
+
|
|
29
|
+
_PRESET_DESCRIPTIONS: dict[PresetRoleSlug, str] = {
|
|
30
|
+
PresetRoleSlug.ADMIN: "Everything: users, security settings, credentials export, conversations, audit.",
|
|
31
|
+
PresetRoleSlug.OPERATOR: "Day-to-day operations: accounts, all API keys, model sources, automations.",
|
|
32
|
+
PresetRoleSlug.MEMBER: "Own API keys and own usage only.",
|
|
33
|
+
PresetRoleSlug.VIEWER: "Read-only dashboard, reports and account status.",
|
|
34
|
+
PresetRoleSlug.GUEST: "Anonymous shared read-only session; cannot be assigned to a user.",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def preset_role_rows() -> list[dict[str, object]]:
|
|
39
|
+
return [
|
|
40
|
+
{
|
|
41
|
+
"id": PRESET_ROLE_IDS[slug],
|
|
42
|
+
"slug": slug.value,
|
|
43
|
+
"name": PRESET_ROLE_NAMES[slug],
|
|
44
|
+
"description": _PRESET_DESCRIPTIONS[slug],
|
|
45
|
+
"kind": RoleKind.PRESET.value,
|
|
46
|
+
"assignable_to_users": slug in ASSIGNABLE_PRESET_ROLES,
|
|
47
|
+
"permissions_version": 1,
|
|
48
|
+
}
|
|
49
|
+
for slug in PresetRoleSlug
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _insert_ignore(connection: Connection, table: Table, rows: list[dict[str, object]]) -> None:
|
|
54
|
+
dialect = connection.dialect.name
|
|
55
|
+
if dialect == "postgresql":
|
|
56
|
+
stmt = postgresql.insert(table).values(rows).on_conflict_do_nothing(index_elements=["id"])
|
|
57
|
+
connection.execute(stmt)
|
|
58
|
+
return
|
|
59
|
+
if dialect == "sqlite":
|
|
60
|
+
stmt = sqlite.insert(table).values(rows).on_conflict_do_nothing(index_elements=["id"])
|
|
61
|
+
connection.execute(stmt)
|
|
62
|
+
return
|
|
63
|
+
existing = {row[0] for row in connection.execute(select(table.c.id)).all()}
|
|
64
|
+
missing = [row for row in rows if row["id"] not in existing]
|
|
65
|
+
if missing:
|
|
66
|
+
connection.execute(table.insert().values(missing))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def seed_preset_dashboard_roles(connection: Connection, table: Table = _ROLES_TABLE) -> None:
|
|
70
|
+
"""Insert any missing preset role row using a synchronous connection.
|
|
71
|
+
|
|
72
|
+
The migration passes its own revision-pinned table so it stays frozen
|
|
73
|
+
when the ORM model evolves; every other caller uses the current model.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
_insert_ignore(connection, table, preset_role_rows())
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
async def ensure_preset_dashboard_roles(connection: AsyncConnection) -> None:
|
|
80
|
+
await connection.run_sync(seed_preset_dashboard_roles)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Resolve a role row into the grant table the authorization layer consumes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from types import MappingProxyType
|
|
9
|
+
|
|
10
|
+
from app.core.auth.dashboard_access import (
|
|
11
|
+
ASSIGNABLE_PRESET_ROLES,
|
|
12
|
+
OWN_SCOPED_PERMISSIONS,
|
|
13
|
+
PERMISSION_IMPLIES,
|
|
14
|
+
PRESET_ROLE_GRANTS,
|
|
15
|
+
PRIVILEGED_PERMISSIONS,
|
|
16
|
+
Grants,
|
|
17
|
+
Permission,
|
|
18
|
+
PresetRoleSlug,
|
|
19
|
+
RoleKind,
|
|
20
|
+
Scope,
|
|
21
|
+
)
|
|
22
|
+
from app.db.models import DashboardRoleGrant, DashboardRoleRecord
|
|
23
|
+
from app.modules.dashboard_roles.repository import DashboardRolesRepository
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def grants_from_rows(rows: Iterable[DashboardRoleGrant]) -> Grants:
|
|
29
|
+
"""Build a grant table from custom-role rows.
|
|
30
|
+
|
|
31
|
+
Unknown permission or scope strings are skipped, not raised: during a
|
|
32
|
+
rolling upgrade a newer replica may have written a permission this
|
|
33
|
+
replica's vocabulary does not know yet, and that must not break every
|
|
34
|
+
request for holders of the role.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
grants: dict[Permission, Scope] = {}
|
|
38
|
+
for row in rows:
|
|
39
|
+
try:
|
|
40
|
+
permission = Permission(row.permission)
|
|
41
|
+
scope = Scope(row.scope)
|
|
42
|
+
except ValueError:
|
|
43
|
+
logger.warning(
|
|
44
|
+
"dashboard_role_grant_ignored role_id=%s permission=%s scope=%s",
|
|
45
|
+
row.role_id,
|
|
46
|
+
row.permission,
|
|
47
|
+
row.scope,
|
|
48
|
+
)
|
|
49
|
+
continue
|
|
50
|
+
grants[permission] = scope
|
|
51
|
+
return MappingProxyType(grants)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def resolve_role_grants(role: DashboardRoleRecord) -> Grants:
|
|
55
|
+
"""Preset roles resolve from code; custom roles from their stored grant rows.
|
|
56
|
+
|
|
57
|
+
An unknown ``kind`` or an unknown preset slug raises: preset rows are only
|
|
58
|
+
written by the migration, so such a row is a provisioning error and must
|
|
59
|
+
not fall back to a quiet default.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
if RoleKind(role.kind) is RoleKind.PRESET:
|
|
63
|
+
return PRESET_ROLE_GRANTS[PresetRoleSlug(role.slug)]
|
|
64
|
+
return grants_from_rows(role.grants)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class RoleNotAssignableError(ValueError):
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
#: Presets a person may hold. Code is the truth; the row's flag only mirrors it.
|
|
72
|
+
_ASSIGNABLE_PRESET_SLUGS = frozenset(slug.value for slug in ASSIGNABLE_PRESET_ROLES)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def role_assignable_to_users(role: DashboardRoleRecord) -> bool:
|
|
76
|
+
"""Whether this role may be given to a person, presets from code and custom roles from the row.
|
|
77
|
+
|
|
78
|
+
One predicate for every surface that hands a role out or offers one, so a
|
|
79
|
+
picker can never list a role the write path would refuse.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
if RoleKind(role.kind) is RoleKind.PRESET:
|
|
83
|
+
return role.slug in _ASSIGNABLE_PRESET_SLUGS
|
|
84
|
+
return role.assignable_to_users
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
async def resolve_assignable_role(roles: DashboardRolesRepository, role_id: str) -> DashboardRoleRecord:
|
|
88
|
+
"""The role row a person may be given: an assignable preset or an assignable custom role.
|
|
89
|
+
|
|
90
|
+
Used wherever a role is handed out (account creation and edits, the role
|
|
91
|
+
an unknown identity receives) so every path applies the same rule.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
role = await roles.get_role(role_id)
|
|
95
|
+
if role is None:
|
|
96
|
+
raise RoleNotAssignableError("Unknown role")
|
|
97
|
+
if not role_assignable_to_users(role):
|
|
98
|
+
raise RoleNotAssignableError(f"Role '{role.slug}' cannot be assigned to an account")
|
|
99
|
+
return role
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
#: One plain sentence per permission for role pickers and the editor grid.
|
|
103
|
+
#: The grant tables themselves stay in ``dashboard_access``; this only describes them.
|
|
104
|
+
PERMISSION_DESCRIPTIONS: dict[Permission, str] = {
|
|
105
|
+
Permission.DASHBOARD_READ: "Read the dashboard overview, reports, request logs and the model catalog.",
|
|
106
|
+
Permission.ACCOUNTS_READ: "See upstream accounts and their usage windows.",
|
|
107
|
+
Permission.ACCOUNTS_WRITE: "Add, edit, pause and remove upstream accounts and their routing.",
|
|
108
|
+
Permission.ACCOUNTS_EXPORT: "Export upstream account credentials.",
|
|
109
|
+
Permission.API_KEYS_READ: "See API keys, their policies and their usage.",
|
|
110
|
+
Permission.API_KEYS_WRITE: "Create, edit, rotate and delete API keys.",
|
|
111
|
+
Permission.API_KEYS_ASSIGN: "Assign upstream accounts, model sources and owners to API keys.",
|
|
112
|
+
Permission.OPS_WRITE: "Change operational settings such as model sources, automations and the quota planner.",
|
|
113
|
+
Permission.SECURITY_WRITE: "Change security settings: guest access, firewall, upstream proxy credentials.",
|
|
114
|
+
Permission.USERS_MANAGE: "Invite, edit, disable and remove dashboard accounts.",
|
|
115
|
+
Permission.ROLES_MANAGE: "Create, edit and delete custom roles.",
|
|
116
|
+
Permission.CONVERSATIONS_READ: "Read conversation contents and archives.",
|
|
117
|
+
Permission.AUDIT_READ: "Read the audit log.",
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass(frozen=True, slots=True)
|
|
122
|
+
class PermissionDescriptor:
|
|
123
|
+
permission: Permission
|
|
124
|
+
description: str
|
|
125
|
+
implies: tuple[Permission, ...]
|
|
126
|
+
own_supported: bool
|
|
127
|
+
privileged: bool
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def permission_descriptors() -> list[PermissionDescriptor]:
|
|
131
|
+
"""The permission vocabulary with its rules, derived from the single code source."""
|
|
132
|
+
|
|
133
|
+
return [
|
|
134
|
+
PermissionDescriptor(
|
|
135
|
+
permission=permission,
|
|
136
|
+
description=PERMISSION_DESCRIPTIONS[permission],
|
|
137
|
+
implies=tuple(sorted(PERMISSION_IMPLIES.get(permission, frozenset()), key=lambda dep: dep.value)),
|
|
138
|
+
own_supported=permission in OWN_SCOPED_PERMISSIONS,
|
|
139
|
+
privileged=permission in PRIVILEGED_PERMISSIONS,
|
|
140
|
+
)
|
|
141
|
+
for permission in Permission
|
|
142
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""Account management API (``users:manage``).
|
|
2
|
+
|
|
3
|
+
Every mutation additionally requires the caller to *be* an account
|
|
4
|
+
(``principal.user_id``): the implicit local admin, the disabled-auth principal
|
|
5
|
+
and a trusted-header principal without an account row get
|
|
6
|
+
``409 admin_account_required`` instead of creating people nobody can attribute.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import Iterator
|
|
12
|
+
from contextlib import contextmanager
|
|
13
|
+
|
|
14
|
+
from fastapi import APIRouter, Body, Depends, Request, Response
|
|
15
|
+
|
|
16
|
+
from app.core.auth.dashboard_access import DashboardPrincipal, InsufficientDelegationError, Permission
|
|
17
|
+
from app.core.auth.dependencies import require_dashboard_permission, set_dashboard_error_format
|
|
18
|
+
from app.core.exceptions import (
|
|
19
|
+
AppError,
|
|
20
|
+
DashboardConflictError,
|
|
21
|
+
DashboardNotFoundError,
|
|
22
|
+
DashboardPermissionError,
|
|
23
|
+
DashboardValidationError,
|
|
24
|
+
)
|
|
25
|
+
from app.db.models import DashboardUser, DashboardUserInvite
|
|
26
|
+
from app.dependencies import DashboardUsersContext, get_dashboard_users_context
|
|
27
|
+
from app.modules.dashboard_auth.service import role_summary
|
|
28
|
+
from app.modules.dashboard_roles.service import RoleNotAssignableError
|
|
29
|
+
from app.modules.dashboard_users.break_glass import BreakGlassRoleRequiredError, LastBreakGlassProtectedError
|
|
30
|
+
from app.modules.dashboard_users.credentials import CredentialRequiredError
|
|
31
|
+
from app.modules.dashboard_users.schemas import (
|
|
32
|
+
DashboardUserCreateRequest,
|
|
33
|
+
DashboardUserCreateResponse,
|
|
34
|
+
DashboardUserResponse,
|
|
35
|
+
DashboardUserUpdateRequest,
|
|
36
|
+
IssuedInviteResponse,
|
|
37
|
+
PendingInviteResponse,
|
|
38
|
+
PendingInviteSummary,
|
|
39
|
+
ReactivateKeysResponse,
|
|
40
|
+
)
|
|
41
|
+
from app.modules.dashboard_users.service import (
|
|
42
|
+
AdminAccountRequiredError,
|
|
43
|
+
EmailTakenError,
|
|
44
|
+
ForceWithoutRoleChangeError,
|
|
45
|
+
IdentityTakenError,
|
|
46
|
+
InvalidEmailError,
|
|
47
|
+
InvalidUsernameError,
|
|
48
|
+
InviteNotPendingError,
|
|
49
|
+
InvitePendingError,
|
|
50
|
+
IssuedInvite,
|
|
51
|
+
LastAdminProtectedError,
|
|
52
|
+
RoleManagedExternallyError,
|
|
53
|
+
SelfModificationForbiddenError,
|
|
54
|
+
SsoNotAvailableError,
|
|
55
|
+
SsoOnlyInviteError,
|
|
56
|
+
UsernameTakenError,
|
|
57
|
+
UserNotActiveError,
|
|
58
|
+
UserNotFoundError,
|
|
59
|
+
as_utc,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
router = APIRouter(
|
|
63
|
+
prefix="/api/dashboard-users",
|
|
64
|
+
tags=["dashboard"],
|
|
65
|
+
dependencies=[Depends(require_dashboard_permission(Permission.USERS_MANAGE)), Depends(set_dashboard_error_format)],
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
_ERROR_MAP: dict[type[Exception], tuple[type[AppError], str]] = {
|
|
69
|
+
AdminAccountRequiredError: (DashboardConflictError, "admin_account_required"),
|
|
70
|
+
UserNotFoundError: (DashboardNotFoundError, "user_not_found"),
|
|
71
|
+
UsernameTakenError: (DashboardConflictError, "username_taken"),
|
|
72
|
+
EmailTakenError: (DashboardConflictError, "email_taken"),
|
|
73
|
+
InvalidUsernameError: (DashboardValidationError, "validation_error"),
|
|
74
|
+
InvalidEmailError: (DashboardValidationError, "validation_error"),
|
|
75
|
+
RoleNotAssignableError: (DashboardValidationError, "role_not_assignable"),
|
|
76
|
+
InsufficientDelegationError: (DashboardPermissionError, "insufficient_delegation"),
|
|
77
|
+
SelfModificationForbiddenError: (DashboardConflictError, "self_modification_forbidden"),
|
|
78
|
+
LastAdminProtectedError: (DashboardConflictError, "last_admin_protected"),
|
|
79
|
+
LastBreakGlassProtectedError: (DashboardConflictError, "last_break_glass_protected"),
|
|
80
|
+
BreakGlassRoleRequiredError: (DashboardValidationError, "validation_error"),
|
|
81
|
+
InviteNotPendingError: (DashboardConflictError, "invite_not_pending"),
|
|
82
|
+
InvitePendingError: (DashboardConflictError, "invite_pending"),
|
|
83
|
+
UserNotActiveError: (DashboardConflictError, "user_not_active"),
|
|
84
|
+
RoleManagedExternallyError: (DashboardConflictError, "role_managed_externally"),
|
|
85
|
+
ForceWithoutRoleChangeError: (DashboardValidationError, "validation_error"),
|
|
86
|
+
CredentialRequiredError: (DashboardConflictError, "credential_required"),
|
|
87
|
+
SsoNotAvailableError: (DashboardConflictError, "sso_not_available"),
|
|
88
|
+
IdentityTakenError: (DashboardConflictError, "identity_taken"),
|
|
89
|
+
SsoOnlyInviteError: (DashboardConflictError, "sso_only_invite"),
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@contextmanager
|
|
94
|
+
def mapped_user_errors() -> Iterator[None]:
|
|
95
|
+
"""Translate service-level refusals into the dashboard error envelope."""
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
yield
|
|
99
|
+
except tuple(_ERROR_MAP) as exc:
|
|
100
|
+
error_type, code = _ERROR_MAP[type(exc)]
|
|
101
|
+
raise error_type(str(exc), code=code) from exc
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
async def require_admin_account(
|
|
105
|
+
principal: DashboardPrincipal = Depends(require_dashboard_permission(Permission.USERS_MANAGE)),
|
|
106
|
+
) -> DashboardPrincipal:
|
|
107
|
+
if principal.user_id is None:
|
|
108
|
+
raise DashboardConflictError(
|
|
109
|
+
"Set a dashboard password and sign in before managing accounts", code="admin_account_required"
|
|
110
|
+
)
|
|
111
|
+
return principal
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _client_host(request: Request) -> str | None:
|
|
115
|
+
return request.client.host if request.client else None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def pending_invite_summary(invite: DashboardUserInvite | None) -> PendingInviteSummary | None:
|
|
119
|
+
"""An SSO-only account waits for its first sign-in and has no expiry to show."""
|
|
120
|
+
|
|
121
|
+
if invite is None:
|
|
122
|
+
return None
|
|
123
|
+
return PendingInviteSummary(
|
|
124
|
+
expires_at=None if invite.sso_only else as_utc(invite.expires_at), sso_only=invite.sso_only
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def user_response(user: DashboardUser, *, pending_invite: PendingInviteSummary | None = None) -> DashboardUserResponse:
|
|
129
|
+
return DashboardUserResponse(
|
|
130
|
+
id=user.id,
|
|
131
|
+
username=user.username,
|
|
132
|
+
display_name=user.display_name,
|
|
133
|
+
email=user.email,
|
|
134
|
+
role=role_summary(user),
|
|
135
|
+
role_source=user.role_source,
|
|
136
|
+
status=user.status,
|
|
137
|
+
is_break_glass=user.is_break_glass,
|
|
138
|
+
totp_configured=user.totp_secret_encrypted is not None,
|
|
139
|
+
has_password=user.password_hash is not None,
|
|
140
|
+
created_at=user.created_at,
|
|
141
|
+
last_login_at=user.last_login_at,
|
|
142
|
+
pending_invite=pending_invite,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _invite_response(invite: IssuedInvite) -> IssuedInviteResponse:
|
|
147
|
+
return IssuedInviteResponse(token=invite.token, expires_at=invite.expires_at)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@router.get("", response_model=list[DashboardUserResponse])
|
|
151
|
+
async def list_users(
|
|
152
|
+
context: DashboardUsersContext = Depends(get_dashboard_users_context),
|
|
153
|
+
) -> list[DashboardUserResponse]:
|
|
154
|
+
return [
|
|
155
|
+
user_response(listing.user, pending_invite=pending_invite_summary(listing.pending_invite))
|
|
156
|
+
for listing in await context.service.list_users()
|
|
157
|
+
]
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@router.post("", response_model=DashboardUserCreateResponse, status_code=201)
|
|
161
|
+
async def create_user(
|
|
162
|
+
request: Request,
|
|
163
|
+
payload: DashboardUserCreateRequest = Body(...),
|
|
164
|
+
principal: DashboardPrincipal = Depends(require_admin_account),
|
|
165
|
+
context: DashboardUsersContext = Depends(get_dashboard_users_context),
|
|
166
|
+
) -> DashboardUserCreateResponse:
|
|
167
|
+
with mapped_user_errors():
|
|
168
|
+
created = await context.service.create_user(principal, payload, actor_ip=_client_host(request))
|
|
169
|
+
# An SSO-only account has no link to hand over: the plaintext token never leaves the server.
|
|
170
|
+
summary = PendingInviteSummary(
|
|
171
|
+
expires_at=None if created.sso_only else created.invite.expires_at, sso_only=created.sso_only
|
|
172
|
+
)
|
|
173
|
+
return DashboardUserCreateResponse(
|
|
174
|
+
user=user_response(created.user, pending_invite=summary),
|
|
175
|
+
invite=None if created.sso_only else _invite_response(created.invite),
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@router.get("/invites", response_model=list[PendingInviteResponse])
|
|
180
|
+
async def list_pending_invites(
|
|
181
|
+
context: DashboardUsersContext = Depends(get_dashboard_users_context),
|
|
182
|
+
) -> list[PendingInviteResponse]:
|
|
183
|
+
return [
|
|
184
|
+
PendingInviteResponse(
|
|
185
|
+
user_id=invite.user_id,
|
|
186
|
+
username=invite.user.username,
|
|
187
|
+
role_id=invite.user.role_id,
|
|
188
|
+
expires_at=None if invite.sso_only else as_utc(invite.expires_at),
|
|
189
|
+
created_by_user_id=invite.created_by_user_id,
|
|
190
|
+
sso_only=invite.sso_only,
|
|
191
|
+
)
|
|
192
|
+
for invite in await context.service.list_pending_invites()
|
|
193
|
+
]
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@router.patch("/{user_id}", response_model=DashboardUserResponse)
|
|
197
|
+
async def update_user(
|
|
198
|
+
user_id: str,
|
|
199
|
+
request: Request,
|
|
200
|
+
payload: DashboardUserUpdateRequest = Body(...),
|
|
201
|
+
principal: DashboardPrincipal = Depends(require_admin_account),
|
|
202
|
+
context: DashboardUsersContext = Depends(get_dashboard_users_context),
|
|
203
|
+
) -> DashboardUserResponse:
|
|
204
|
+
with mapped_user_errors():
|
|
205
|
+
listing = await context.service.update_user(principal, user_id, payload, actor_ip=_client_host(request))
|
|
206
|
+
return user_response(listing.user, pending_invite=pending_invite_summary(listing.pending_invite))
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@router.delete("/{user_id}", status_code=204)
|
|
210
|
+
async def delete_user(
|
|
211
|
+
user_id: str,
|
|
212
|
+
request: Request,
|
|
213
|
+
principal: DashboardPrincipal = Depends(require_admin_account),
|
|
214
|
+
context: DashboardUsersContext = Depends(get_dashboard_users_context),
|
|
215
|
+
) -> Response:
|
|
216
|
+
with mapped_user_errors():
|
|
217
|
+
await context.service.delete_user(principal, user_id, actor_ip=_client_host(request))
|
|
218
|
+
return Response(status_code=204)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@router.post("/{user_id}/invite", response_model=IssuedInviteResponse)
|
|
222
|
+
async def resend_invite(
|
|
223
|
+
user_id: str,
|
|
224
|
+
request: Request,
|
|
225
|
+
principal: DashboardPrincipal = Depends(require_admin_account),
|
|
226
|
+
context: DashboardUsersContext = Depends(get_dashboard_users_context),
|
|
227
|
+
) -> IssuedInviteResponse:
|
|
228
|
+
with mapped_user_errors():
|
|
229
|
+
invite = await context.service.resend_invite(principal, user_id, actor_ip=_client_host(request))
|
|
230
|
+
return _invite_response(invite)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@router.delete("/{user_id}/invite", status_code=204)
|
|
234
|
+
async def revoke_invite(
|
|
235
|
+
user_id: str,
|
|
236
|
+
request: Request,
|
|
237
|
+
principal: DashboardPrincipal = Depends(require_admin_account),
|
|
238
|
+
context: DashboardUsersContext = Depends(get_dashboard_users_context),
|
|
239
|
+
) -> Response:
|
|
240
|
+
with mapped_user_errors():
|
|
241
|
+
await context.service.revoke_invite(principal, user_id, actor_ip=_client_host(request))
|
|
242
|
+
return Response(status_code=204)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@router.post("/{user_id}/reset-totp")
|
|
246
|
+
async def reset_totp(
|
|
247
|
+
user_id: str,
|
|
248
|
+
request: Request,
|
|
249
|
+
principal: DashboardPrincipal = Depends(require_admin_account),
|
|
250
|
+
context: DashboardUsersContext = Depends(get_dashboard_users_context),
|
|
251
|
+
) -> dict[str, str]:
|
|
252
|
+
with mapped_user_errors():
|
|
253
|
+
await context.service.reset_totp(principal, user_id, actor_ip=_client_host(request))
|
|
254
|
+
return {"status": "ok"}
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@router.post("/{user_id}/revoke-sessions")
|
|
258
|
+
async def revoke_sessions(
|
|
259
|
+
user_id: str,
|
|
260
|
+
request: Request,
|
|
261
|
+
principal: DashboardPrincipal = Depends(require_admin_account),
|
|
262
|
+
context: DashboardUsersContext = Depends(get_dashboard_users_context),
|
|
263
|
+
) -> dict[str, str]:
|
|
264
|
+
with mapped_user_errors():
|
|
265
|
+
await context.service.revoke_sessions(principal, user_id, actor_ip=_client_host(request))
|
|
266
|
+
return {"status": "ok"}
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
@router.post("/{user_id}/reactivate-keys", response_model=ReactivateKeysResponse)
|
|
270
|
+
async def reactivate_keys(
|
|
271
|
+
user_id: str,
|
|
272
|
+
request: Request,
|
|
273
|
+
principal: DashboardPrincipal = Depends(require_admin_account),
|
|
274
|
+
context: DashboardUsersContext = Depends(get_dashboard_users_context),
|
|
275
|
+
) -> ReactivateKeysResponse:
|
|
276
|
+
with mapped_user_errors():
|
|
277
|
+
count = await context.service.reactivate_keys(principal, user_id, actor_ip=_client_host(request))
|
|
278
|
+
return ReactivateKeysResponse(reactivated=count)
|