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,467 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import codecs
|
|
5
|
+
import contextlib
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Final, Literal, cast
|
|
9
|
+
|
|
10
|
+
import aiohttp
|
|
11
|
+
from aiohttp_retry import ExponentialRetry, RetryClient
|
|
12
|
+
from pydantic import BaseModel, ConfigDict, ValidationError
|
|
13
|
+
|
|
14
|
+
from app.core.clients.codex import (
|
|
15
|
+
CodexClient,
|
|
16
|
+
CodexTransportError,
|
|
17
|
+
create_codex_session,
|
|
18
|
+
require_route_or_direct_egress_opt_in,
|
|
19
|
+
)
|
|
20
|
+
from app.core.clients.headers import build_chatgpt_auth_headers
|
|
21
|
+
from app.core.clients.http import _safe_json, lease_retry_client
|
|
22
|
+
from app.core.clients.native_egress import (
|
|
23
|
+
NativeEgressClient,
|
|
24
|
+
NativeEgressError,
|
|
25
|
+
NativeEgressRequest,
|
|
26
|
+
NativeEgressResponse,
|
|
27
|
+
NativeEgressTransportError,
|
|
28
|
+
NativeEgressUnavailable,
|
|
29
|
+
discover_native_egress_client,
|
|
30
|
+
)
|
|
31
|
+
from app.core.clients.proxy import _codex_response_status
|
|
32
|
+
from app.core.config.settings import get_settings
|
|
33
|
+
from app.core.types import JsonObject
|
|
34
|
+
from app.core.upstream_proxy import ResolvedUpstreamRoute
|
|
35
|
+
from app.core.usage.models import UsagePayload
|
|
36
|
+
from app.core.utils.proxy_env import resolve_http_proxy_from_env
|
|
37
|
+
from app.core.utils.request_id import get_request_id
|
|
38
|
+
|
|
39
|
+
RETRYABLE_STATUS = {408, 429, 500, 502, 503, 504}
|
|
40
|
+
RETRY_START_TIMEOUT = 0.5
|
|
41
|
+
RETRY_MAX_TIMEOUT = 2.0
|
|
42
|
+
# Per-call total timeout and retry budget of the usage / reset-credits fetches
|
|
43
|
+
# (fixed; issue #1340 / PRINCIPLES.md P2). Callers may still pass explicit
|
|
44
|
+
# ``timeout_seconds`` / ``max_retries`` overrides.
|
|
45
|
+
USAGE_FETCH_TIMEOUT_SECONDS: Final[float] = 10.0
|
|
46
|
+
USAGE_FETCH_MAX_RETRIES: Final[int] = 2
|
|
47
|
+
|
|
48
|
+
logger = logging.getLogger(__name__)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class UsageErrorDetail(BaseModel):
|
|
52
|
+
model_config = ConfigDict(extra="ignore")
|
|
53
|
+
|
|
54
|
+
code: str | None = None
|
|
55
|
+
message: str | None = None
|
|
56
|
+
error_description: str | None = None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class UsageErrorEnvelope(BaseModel):
|
|
60
|
+
model_config = ConfigDict(extra="ignore")
|
|
61
|
+
|
|
62
|
+
error: UsageErrorDetail | str | None = None
|
|
63
|
+
error_description: str | None = None
|
|
64
|
+
message: str | None = None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class UsageFetchError(Exception):
|
|
68
|
+
def __init__(self, status_code: int, message: str, code: str | None = None) -> None:
|
|
69
|
+
super().__init__(message)
|
|
70
|
+
self.status_code = status_code
|
|
71
|
+
self.message = message
|
|
72
|
+
self.code = code
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ConsumeRateLimitResetCreditResponse(BaseModel):
|
|
76
|
+
model_config = ConfigDict(extra="ignore")
|
|
77
|
+
|
|
78
|
+
code: Literal["reset", "nothing_to_reset", "no_credit", "already_redeemed"]
|
|
79
|
+
windows_reset: int = 0
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
async def fetch_usage(
|
|
83
|
+
*,
|
|
84
|
+
access_token: str,
|
|
85
|
+
account_id: str | None,
|
|
86
|
+
base_url: str | None = None,
|
|
87
|
+
timeout_seconds: float | None = None,
|
|
88
|
+
max_retries: int | None = None,
|
|
89
|
+
client: RetryClient | None = None,
|
|
90
|
+
route: ResolvedUpstreamRoute | None = None,
|
|
91
|
+
codex_client: CodexClient | None = None,
|
|
92
|
+
allow_direct_egress: bool = False,
|
|
93
|
+
) -> UsagePayload:
|
|
94
|
+
settings = get_settings()
|
|
95
|
+
usage_base = base_url or settings.upstream_base_url
|
|
96
|
+
url = _usage_url(usage_base)
|
|
97
|
+
timeout = aiohttp.ClientTimeout(total=timeout_seconds or USAGE_FETCH_TIMEOUT_SECONDS)
|
|
98
|
+
retries = max_retries if max_retries is not None else USAGE_FETCH_MAX_RETRIES
|
|
99
|
+
headers = _usage_headers(access_token, account_id)
|
|
100
|
+
retry_options = _retry_options(retries + 1)
|
|
101
|
+
require_route_or_direct_egress_opt_in(
|
|
102
|
+
route=route,
|
|
103
|
+
allow_direct_egress=allow_direct_egress,
|
|
104
|
+
operation="usage fetch",
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
if route is not None:
|
|
109
|
+
return await _fetch_usage_via_codex(
|
|
110
|
+
url=url,
|
|
111
|
+
route=route,
|
|
112
|
+
headers=headers,
|
|
113
|
+
timeout_seconds=timeout_seconds or USAGE_FETCH_TIMEOUT_SECONDS,
|
|
114
|
+
retries=retries,
|
|
115
|
+
codex_client=codex_client,
|
|
116
|
+
)
|
|
117
|
+
native_client = discover_native_egress_client() if client is None else None
|
|
118
|
+
if native_client is not None:
|
|
119
|
+
try:
|
|
120
|
+
return await _fetch_usage_via_native(
|
|
121
|
+
client=native_client,
|
|
122
|
+
url=url,
|
|
123
|
+
headers=headers,
|
|
124
|
+
timeout_seconds=timeout_seconds or USAGE_FETCH_TIMEOUT_SECONDS,
|
|
125
|
+
retries=retries,
|
|
126
|
+
)
|
|
127
|
+
except NativeEgressUnavailable:
|
|
128
|
+
pass
|
|
129
|
+
async with lease_retry_client(client) as retry_client:
|
|
130
|
+
async with retry_client.request(
|
|
131
|
+
"GET",
|
|
132
|
+
url,
|
|
133
|
+
headers=headers,
|
|
134
|
+
timeout=timeout,
|
|
135
|
+
retry_options=retry_options,
|
|
136
|
+
) as resp:
|
|
137
|
+
data = await _safe_json(resp)
|
|
138
|
+
return _usage_payload_or_raise(data, resp.status)
|
|
139
|
+
except (aiohttp.ClientError, asyncio.TimeoutError, CodexTransportError, NativeEgressError) as exc:
|
|
140
|
+
logger.warning(
|
|
141
|
+
"Usage fetch error request_id=%s error=%s",
|
|
142
|
+
get_request_id(),
|
|
143
|
+
exc,
|
|
144
|
+
)
|
|
145
|
+
raise UsageFetchError(0, f"Usage fetch failed: {exc}") from exc
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
async def _fetch_usage_via_native(
|
|
149
|
+
*,
|
|
150
|
+
client: NativeEgressClient,
|
|
151
|
+
url: str,
|
|
152
|
+
headers: dict[str, str],
|
|
153
|
+
timeout_seconds: float,
|
|
154
|
+
retries: int,
|
|
155
|
+
) -> UsagePayload:
|
|
156
|
+
attempts = max(1, retries + 1)
|
|
157
|
+
retry_options = _retry_options(attempts)
|
|
158
|
+
request = NativeEgressRequest(
|
|
159
|
+
method="GET",
|
|
160
|
+
url=url,
|
|
161
|
+
# The helper enables response decompression when negotiation is explicit.
|
|
162
|
+
headers={
|
|
163
|
+
"Accept-Encoding": aiohttp.ClientRequest.DEFAULT_HEADERS[aiohttp.hdrs.ACCEPT_ENCODING],
|
|
164
|
+
**headers,
|
|
165
|
+
},
|
|
166
|
+
timeout_seconds=timeout_seconds,
|
|
167
|
+
proxy_url=resolve_http_proxy_from_env(url),
|
|
168
|
+
)
|
|
169
|
+
for attempt in range(attempts):
|
|
170
|
+
try:
|
|
171
|
+
response = await client.request(request)
|
|
172
|
+
except NativeEgressUnavailable as exc:
|
|
173
|
+
if attempt == 0:
|
|
174
|
+
raise
|
|
175
|
+
# Do not restart the budget through Python after a native attempt.
|
|
176
|
+
raise NativeEgressError("native helper became unavailable during usage retries") from exc
|
|
177
|
+
except NativeEgressTransportError:
|
|
178
|
+
if attempt < attempts - 1:
|
|
179
|
+
await asyncio.sleep(retry_options.get_timeout(attempt + 1))
|
|
180
|
+
continue
|
|
181
|
+
raise
|
|
182
|
+
async with response:
|
|
183
|
+
status = response.status
|
|
184
|
+
if status not in RETRYABLE_STATUS or attempt == attempts - 1:
|
|
185
|
+
data = await _native_usage_json(response)
|
|
186
|
+
return _usage_payload_or_raise(data, status)
|
|
187
|
+
await asyncio.sleep(retry_options.get_timeout(attempt + 1))
|
|
188
|
+
raise RuntimeError("unreachable native usage retry state")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
async def _native_usage_json(response: NativeEgressResponse) -> JsonObject:
|
|
192
|
+
# Match the default aiohttp session's charset and empty-body behavior,
|
|
193
|
+
# keeping body/IPC failures outside the JSON-syntax exception handler.
|
|
194
|
+
body = await response.read()
|
|
195
|
+
encoding = "utf-8"
|
|
196
|
+
content_type = aiohttp.helpers.parse_mimetype(response.headers.get("Content-Type", "").lower())
|
|
197
|
+
charset = content_type.parameters.get("charset")
|
|
198
|
+
if charset:
|
|
199
|
+
with contextlib.suppress(LookupError, ValueError):
|
|
200
|
+
encoding = codecs.lookup(charset).name
|
|
201
|
+
try:
|
|
202
|
+
value = json.loads(body.strip().decode(encoding)) if body.strip() else None
|
|
203
|
+
except ValueError:
|
|
204
|
+
return {"error": {"message": body.decode(encoding).strip()}}
|
|
205
|
+
return cast(JsonObject, value) if isinstance(value, dict) else {"error": {"message": str(value)}}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
async def consume_rate_limit_reset_credit(
|
|
209
|
+
*,
|
|
210
|
+
access_token: str,
|
|
211
|
+
account_id: str,
|
|
212
|
+
redeem_request_id: str,
|
|
213
|
+
base_url: str | None = None,
|
|
214
|
+
timeout_seconds: float | None = None,
|
|
215
|
+
max_retries: int | None = None,
|
|
216
|
+
client: RetryClient | None = None,
|
|
217
|
+
route: ResolvedUpstreamRoute | None = None,
|
|
218
|
+
codex_client: CodexClient | None = None,
|
|
219
|
+
allow_direct_egress: bool = False,
|
|
220
|
+
) -> ConsumeRateLimitResetCreditResponse:
|
|
221
|
+
settings = get_settings()
|
|
222
|
+
usage_base = base_url or settings.upstream_base_url
|
|
223
|
+
url = _rate_limit_reset_url(usage_base)
|
|
224
|
+
timeout = aiohttp.ClientTimeout(total=timeout_seconds or USAGE_FETCH_TIMEOUT_SECONDS)
|
|
225
|
+
retries = max_retries if max_retries is not None else USAGE_FETCH_MAX_RETRIES
|
|
226
|
+
headers = _usage_headers(access_token, account_id)
|
|
227
|
+
payload = {"redeem_request_id": redeem_request_id}
|
|
228
|
+
retry_options = _retry_options(retries + 1)
|
|
229
|
+
require_route_or_direct_egress_opt_in(
|
|
230
|
+
route=route,
|
|
231
|
+
allow_direct_egress=allow_direct_egress,
|
|
232
|
+
operation="usage limit reset consume",
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
if route is not None:
|
|
237
|
+
return await _consume_rate_limit_reset_via_codex(
|
|
238
|
+
url=url,
|
|
239
|
+
route=route,
|
|
240
|
+
headers=headers,
|
|
241
|
+
payload=payload,
|
|
242
|
+
timeout_seconds=timeout_seconds or USAGE_FETCH_TIMEOUT_SECONDS,
|
|
243
|
+
retries=retries,
|
|
244
|
+
codex_client=codex_client,
|
|
245
|
+
)
|
|
246
|
+
async with lease_retry_client(client) as retry_client:
|
|
247
|
+
async with retry_client.request(
|
|
248
|
+
"POST",
|
|
249
|
+
url,
|
|
250
|
+
headers=headers,
|
|
251
|
+
json=payload,
|
|
252
|
+
timeout=timeout,
|
|
253
|
+
retry_options=retry_options,
|
|
254
|
+
) as resp:
|
|
255
|
+
data = await _safe_json(resp)
|
|
256
|
+
return _consume_rate_limit_reset_response_or_raise(data, resp.status)
|
|
257
|
+
except (aiohttp.ClientError, asyncio.TimeoutError, CodexTransportError) as exc:
|
|
258
|
+
logger.warning(
|
|
259
|
+
"Usage limit reset consume error request_id=%s error=%s",
|
|
260
|
+
get_request_id(),
|
|
261
|
+
exc,
|
|
262
|
+
)
|
|
263
|
+
raise UsageFetchError(0, f"Usage limit reset consume failed: {exc}") from exc
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
async def _fetch_usage_via_codex(
|
|
267
|
+
*,
|
|
268
|
+
url: str,
|
|
269
|
+
route: ResolvedUpstreamRoute,
|
|
270
|
+
headers: dict[str, str],
|
|
271
|
+
timeout_seconds: float,
|
|
272
|
+
retries: int,
|
|
273
|
+
codex_client: CodexClient | None,
|
|
274
|
+
) -> UsagePayload:
|
|
275
|
+
attempts = max(1, retries + 1)
|
|
276
|
+
owns_codex_client = codex_client is None
|
|
277
|
+
active_codex_client = codex_client or CodexClient(create_codex_session())
|
|
278
|
+
try:
|
|
279
|
+
for attempt in range(attempts):
|
|
280
|
+
try:
|
|
281
|
+
resp = await active_codex_client.request(
|
|
282
|
+
"GET",
|
|
283
|
+
url,
|
|
284
|
+
route=route,
|
|
285
|
+
headers=headers,
|
|
286
|
+
timeout=timeout_seconds,
|
|
287
|
+
)
|
|
288
|
+
except CodexTransportError:
|
|
289
|
+
if attempt < attempts - 1:
|
|
290
|
+
await asyncio.sleep(_retry_delay_seconds(attempt))
|
|
291
|
+
continue
|
|
292
|
+
raise
|
|
293
|
+
|
|
294
|
+
data = await _safe_codex_json(resp)
|
|
295
|
+
status = _codex_response_status(resp)
|
|
296
|
+
if status in RETRYABLE_STATUS and attempt < attempts - 1:
|
|
297
|
+
await asyncio.sleep(_retry_delay_seconds(attempt))
|
|
298
|
+
continue
|
|
299
|
+
return _usage_payload_or_raise(data, status)
|
|
300
|
+
finally:
|
|
301
|
+
if owns_codex_client:
|
|
302
|
+
close = getattr(active_codex_client, "close", None)
|
|
303
|
+
if callable(close):
|
|
304
|
+
await close()
|
|
305
|
+
raise RuntimeError("unreachable usage retry state")
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
async def _consume_rate_limit_reset_via_codex(
|
|
309
|
+
*,
|
|
310
|
+
url: str,
|
|
311
|
+
route: ResolvedUpstreamRoute,
|
|
312
|
+
headers: dict[str, str],
|
|
313
|
+
payload: dict[str, str],
|
|
314
|
+
timeout_seconds: float,
|
|
315
|
+
retries: int,
|
|
316
|
+
codex_client: CodexClient | None,
|
|
317
|
+
) -> ConsumeRateLimitResetCreditResponse:
|
|
318
|
+
attempts = max(1, retries + 1)
|
|
319
|
+
owns_codex_client = codex_client is None
|
|
320
|
+
active_codex_client = codex_client or CodexClient(create_codex_session())
|
|
321
|
+
try:
|
|
322
|
+
for attempt in range(attempts):
|
|
323
|
+
try:
|
|
324
|
+
resp = await active_codex_client.request(
|
|
325
|
+
"POST",
|
|
326
|
+
url,
|
|
327
|
+
route=route,
|
|
328
|
+
headers=headers,
|
|
329
|
+
json=payload,
|
|
330
|
+
timeout=timeout_seconds,
|
|
331
|
+
)
|
|
332
|
+
except CodexTransportError:
|
|
333
|
+
if attempt < attempts - 1:
|
|
334
|
+
await asyncio.sleep(_retry_delay_seconds(attempt))
|
|
335
|
+
continue
|
|
336
|
+
raise
|
|
337
|
+
|
|
338
|
+
data = await _safe_codex_json(resp)
|
|
339
|
+
status = _codex_response_status(resp)
|
|
340
|
+
if status in RETRYABLE_STATUS and attempt < attempts - 1:
|
|
341
|
+
await asyncio.sleep(_retry_delay_seconds(attempt))
|
|
342
|
+
continue
|
|
343
|
+
return _consume_rate_limit_reset_response_or_raise(data, status)
|
|
344
|
+
finally:
|
|
345
|
+
if owns_codex_client:
|
|
346
|
+
close = getattr(active_codex_client, "close", None)
|
|
347
|
+
if callable(close):
|
|
348
|
+
await close()
|
|
349
|
+
raise RuntimeError("unreachable usage limit reset retry state")
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _usage_payload_or_raise(data: JsonObject, status: int) -> UsagePayload:
|
|
353
|
+
if status >= 400:
|
|
354
|
+
code = _extract_error_code(data)
|
|
355
|
+
message = _extract_error_message(data) or f"Usage fetch failed ({status})"
|
|
356
|
+
logger.warning(
|
|
357
|
+
"Usage fetch failed request_id=%s status=%s code=%s message=%s",
|
|
358
|
+
get_request_id(),
|
|
359
|
+
status,
|
|
360
|
+
code,
|
|
361
|
+
message,
|
|
362
|
+
)
|
|
363
|
+
raise UsageFetchError(status, message, code=code)
|
|
364
|
+
try:
|
|
365
|
+
return UsagePayload.model_validate(data)
|
|
366
|
+
except ValidationError as exc:
|
|
367
|
+
logger.warning(
|
|
368
|
+
"Usage fetch invalid payload request_id=%s",
|
|
369
|
+
get_request_id(),
|
|
370
|
+
)
|
|
371
|
+
raise UsageFetchError(502, "Invalid usage payload") from exc
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _consume_rate_limit_reset_response_or_raise(
|
|
375
|
+
data: JsonObject,
|
|
376
|
+
status: int,
|
|
377
|
+
) -> ConsumeRateLimitResetCreditResponse:
|
|
378
|
+
if status >= 400:
|
|
379
|
+
code = _extract_error_code(data)
|
|
380
|
+
message = _extract_error_message(data) or f"Usage limit reset consume failed ({status})"
|
|
381
|
+
logger.warning(
|
|
382
|
+
"Usage limit reset consume failed request_id=%s status=%s code=%s message=%s",
|
|
383
|
+
get_request_id(),
|
|
384
|
+
status,
|
|
385
|
+
code,
|
|
386
|
+
message,
|
|
387
|
+
)
|
|
388
|
+
raise UsageFetchError(status, message, code=code)
|
|
389
|
+
try:
|
|
390
|
+
return ConsumeRateLimitResetCreditResponse.model_validate(data)
|
|
391
|
+
except ValidationError as exc:
|
|
392
|
+
logger.warning(
|
|
393
|
+
"Usage limit reset consume invalid payload request_id=%s",
|
|
394
|
+
get_request_id(),
|
|
395
|
+
)
|
|
396
|
+
raise UsageFetchError(502, "Invalid usage limit reset payload") from exc
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
async def _safe_codex_json(response: object) -> JsonObject:
|
|
400
|
+
try:
|
|
401
|
+
json_method = getattr(response, "json", None)
|
|
402
|
+
if callable(json_method):
|
|
403
|
+
data = json_method()
|
|
404
|
+
if asyncio.iscoroutine(data):
|
|
405
|
+
data = await data
|
|
406
|
+
return data if isinstance(data, dict) else {"error": {"message": str(data)}}
|
|
407
|
+
except Exception:
|
|
408
|
+
pass
|
|
409
|
+
content = getattr(response, "content", None)
|
|
410
|
+
if isinstance(content, bytes):
|
|
411
|
+
return {"error": {"message": content.decode("utf-8", errors="replace").strip()}}
|
|
412
|
+
if isinstance(content, str):
|
|
413
|
+
return {"error": {"message": content.strip()}}
|
|
414
|
+
return {"error": {"message": ""}}
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _usage_url(base_url: str) -> str:
|
|
418
|
+
normalized = base_url.rstrip("/")
|
|
419
|
+
if "/backend-api" not in normalized:
|
|
420
|
+
normalized = f"{normalized}/backend-api"
|
|
421
|
+
return f"{normalized}/wham/usage"
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _rate_limit_reset_url(base_url: str) -> str:
|
|
425
|
+
normalized = base_url.rstrip("/")
|
|
426
|
+
if "/backend-api" not in normalized:
|
|
427
|
+
normalized = f"{normalized}/backend-api"
|
|
428
|
+
return f"{normalized}/wham/rate-limit-reset-credits/consume"
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _usage_headers(access_token: str, account_id: str | None) -> dict[str, str]:
|
|
432
|
+
return build_chatgpt_auth_headers(access_token, account_id)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _extract_error_message(payload: JsonObject) -> str | None:
|
|
436
|
+
envelope = UsageErrorEnvelope.model_validate(payload)
|
|
437
|
+
error = envelope.error
|
|
438
|
+
if isinstance(error, UsageErrorDetail):
|
|
439
|
+
return error.message or error.error_description
|
|
440
|
+
if isinstance(error, str):
|
|
441
|
+
return envelope.error_description or error
|
|
442
|
+
return envelope.message
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _extract_error_code(payload: JsonObject) -> str | None:
|
|
446
|
+
envelope = UsageErrorEnvelope.model_validate(payload)
|
|
447
|
+
error = envelope.error
|
|
448
|
+
if isinstance(error, UsageErrorDetail) and isinstance(error.code, str):
|
|
449
|
+
normalized = error.code.strip().lower()
|
|
450
|
+
return normalized or None
|
|
451
|
+
return None
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def _retry_options(attempts: int) -> ExponentialRetry:
|
|
455
|
+
return ExponentialRetry(
|
|
456
|
+
attempts=attempts,
|
|
457
|
+
start_timeout=RETRY_START_TIMEOUT,
|
|
458
|
+
max_timeout=RETRY_MAX_TIMEOUT,
|
|
459
|
+
factor=2.0,
|
|
460
|
+
statuses=RETRYABLE_STATUS,
|
|
461
|
+
exceptions={aiohttp.ClientError, asyncio.TimeoutError},
|
|
462
|
+
retry_all_server_errors=False,
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _retry_delay_seconds(attempt: int) -> float:
|
|
467
|
+
return min(RETRY_MAX_TIMEOUT, RETRY_START_TIMEOUT * (2.0**attempt))
|
app/core/clock.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Clock and scheduler seams for deterministic proxy lifecycle tests.
|
|
2
|
+
|
|
3
|
+
Production code reads time and schedules timers/tasks through these two
|
|
4
|
+
protocols so a test can substitute a virtual implementation
|
|
5
|
+
(``tests/simulation/virtual_time.py``) and drive a whole proxy turn without
|
|
6
|
+
wall-clock sleeps. The real adapters are *verbatim* passthroughs: every
|
|
7
|
+
``RealScheduler`` method is the corresponding ``asyncio``/``anyio`` call with
|
|
8
|
+
no task registry, no extra timeout and no wrapper task, so injecting the
|
|
9
|
+
defaults changes nothing about production behavior. The scheduler's timing
|
|
10
|
+
and spawn members *are* the ``asyncio``/``anyio`` functions (``staticmethod``
|
|
11
|
+
aliases), so the per-event relay sites (``scheduler.create_task`` and
|
|
12
|
+
``scheduler.wait`` per upstream message, ``scheduler.wait_for(queue.get(), ...)``
|
|
13
|
+
per delivered block) pay neither a wrapper frame nor a bound-method allocation
|
|
14
|
+
over the raw call.
|
|
15
|
+
|
|
16
|
+
Usage rules (also enforced by ``scripts/check_proxy_timing_seams.py`` once it
|
|
17
|
+
lands):
|
|
18
|
+
|
|
19
|
+
* Objects that own the collaborators (``ProxyService``, ``LoadBalancer``,
|
|
20
|
+
``WorkAdmissionController``) use ``self._scheduler`` / ``self._clock``.
|
|
21
|
+
* Mixin methods and module functions that receive an owner read the seam with
|
|
22
|
+
``scheduler_for(owner)`` / ``clock_for(owner)``; partial doubles without the
|
|
23
|
+
attribute keep the production default.
|
|
24
|
+
* Owner-less turn-path functions take keyword-only ``scheduler`` / ``clock``
|
|
25
|
+
parameters that callers pass explicitly.
|
|
26
|
+
* ``asyncio.sleep(0)`` is a yield point, not a timer, and stays raw.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import asyncio
|
|
32
|
+
import time
|
|
33
|
+
from collections.abc import Awaitable, Coroutine, Iterable
|
|
34
|
+
from contextlib import AbstractContextManager
|
|
35
|
+
from datetime import datetime, timezone
|
|
36
|
+
from typing import Any, Protocol, TypeVar
|
|
37
|
+
|
|
38
|
+
import anyio
|
|
39
|
+
|
|
40
|
+
T = TypeVar("T")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Clock(Protocol):
|
|
44
|
+
def monotonic(self) -> float: ...
|
|
45
|
+
|
|
46
|
+
def time(self) -> float: ...
|
|
47
|
+
|
|
48
|
+
def now(self) -> datetime: ...
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Scheduler(Protocol):
|
|
52
|
+
"""Timing and task-spawn seam.
|
|
53
|
+
|
|
54
|
+
The timing members are declared as plain functions returning a coroutine
|
|
55
|
+
(an ``async def`` implementation satisfies them too) so the real adapter
|
|
56
|
+
can hand back the ``asyncio`` coroutine object without a wrapper frame.
|
|
57
|
+
The awaitable/coroutine parameters are positional-only so the
|
|
58
|
+
``asyncio`` functions themselves (``fut``, ``coro``) satisfy the protocol.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def sleep(self, delay: float, result: T | None = None) -> Coroutine[Any, Any, T | None]: ...
|
|
62
|
+
|
|
63
|
+
def wait_for(self, fut: Awaitable[T], /, timeout: float | None) -> Coroutine[Any, Any, T]:
|
|
64
|
+
"""Bound ``fut`` by ``timeout``.
|
|
65
|
+
|
|
66
|
+
Real: ``asyncio.wait_for`` verbatim. Virtual: the same inline
|
|
67
|
+
``asyncio.timeout`` shape on a virtual deadline (same task, same
|
|
68
|
+
contextvars); see ``tests/simulation/test_virtual_time.py``.
|
|
69
|
+
"""
|
|
70
|
+
...
|
|
71
|
+
|
|
72
|
+
def wait(
|
|
73
|
+
self,
|
|
74
|
+
fs: Iterable[asyncio.Future[Any]],
|
|
75
|
+
*,
|
|
76
|
+
timeout: float | None = None,
|
|
77
|
+
return_when: str = asyncio.ALL_COMPLETED,
|
|
78
|
+
) -> Coroutine[Any, Any, tuple[set[asyncio.Future[Any]], set[asyncio.Future[Any]]]]:
|
|
79
|
+
"""Mirror ``asyncio.wait``: never cancels ``fs``, reports done at the deadline."""
|
|
80
|
+
...
|
|
81
|
+
|
|
82
|
+
def create_task(self, coro: Coroutine[Any, Any, T], /, *, name: str | None = None) -> asyncio.Task[T]: ...
|
|
83
|
+
|
|
84
|
+
def fail_after(self, delay: float) -> AbstractContextManager[Any]:
|
|
85
|
+
"""Mirror ``anyio.fail_after``: cancel the enclosing scope after ``delay``."""
|
|
86
|
+
...
|
|
87
|
+
|
|
88
|
+
def drain(self) -> Coroutine[Any, Any, None]: ...
|
|
89
|
+
|
|
90
|
+
async def cancel_owned_tasks(self) -> None: ...
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class RealClock:
|
|
94
|
+
"""Wall-clock reads through one Python frame each.
|
|
95
|
+
|
|
96
|
+
Kept as ``def`` wrappers (unlike ``RealScheduler``'s aliases) on purpose:
|
|
97
|
+
the unit suite steers production time in well over a hundred places with
|
|
98
|
+
``monkeypatch.setattr(time, "time" | "monotonic", ...)``, which an
|
|
99
|
+
import-time alias would silently bypass. The cost is one frame per read
|
|
100
|
+
(two reads per relayed upstream message) on top of the clock read itself.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
def monotonic(self) -> float:
|
|
104
|
+
return time.monotonic()
|
|
105
|
+
|
|
106
|
+
def time(self) -> float:
|
|
107
|
+
return time.time()
|
|
108
|
+
|
|
109
|
+
def now(self) -> datetime:
|
|
110
|
+
return datetime.now(timezone.utc)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class RealScheduler:
|
|
114
|
+
"""Pure passthrough to ``asyncio``/``anyio``.
|
|
115
|
+
|
|
116
|
+
Deliberately stateless: a registry of every rerouted task would extend task
|
|
117
|
+
lifetimes and let ``cancel_owned_tasks`` on the process-wide singleton
|
|
118
|
+
cancel production work. Ownership tracking exists only in the virtual
|
|
119
|
+
scheduler used by tests.
|
|
120
|
+
|
|
121
|
+
The per-event members are the primitives themselves, bound as
|
|
122
|
+
``staticmethod`` so ``scheduler.create_task(...)`` compiles to the raw
|
|
123
|
+
``asyncio.create_task(...)`` call: no Python frame and no bound-method
|
|
124
|
+
object per seam call on the relay paths (``create_task`` + ``wait`` per
|
|
125
|
+
upstream message, ``wait_for`` per delivered block). The aliases bind at
|
|
126
|
+
import, so a test that needs to observe or replace a timed wait must
|
|
127
|
+
subclass this class or inject a virtual scheduler (``tests/simulation``);
|
|
128
|
+
monkeypatching ``asyncio.wait`` afterwards does not reach them.
|
|
129
|
+
``tests/unit/test_clock_real_parity.py`` pins the identities. ``sleep``
|
|
130
|
+
stays a one-frame wrapper: ``asyncio.sleep`` is overloaded on ``result``
|
|
131
|
+
in typeshed, which ty 0.0.73 cannot match against the protocol member,
|
|
132
|
+
and no per-event site sleeps (backoff and recovery waits only).
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
wait_for = staticmethod(asyncio.wait_for)
|
|
136
|
+
wait = staticmethod(asyncio.wait)
|
|
137
|
+
create_task = staticmethod(asyncio.create_task)
|
|
138
|
+
fail_after = staticmethod(anyio.fail_after)
|
|
139
|
+
|
|
140
|
+
def sleep(self, delay: float, result: T | None = None) -> Coroutine[Any, Any, T | None]:
|
|
141
|
+
return asyncio.sleep(delay, result=result)
|
|
142
|
+
|
|
143
|
+
def drain(self) -> Coroutine[Any, Any, None]:
|
|
144
|
+
return asyncio.sleep(0)
|
|
145
|
+
|
|
146
|
+
async def cancel_owned_tasks(self) -> None:
|
|
147
|
+
# The real scheduler owns nothing; production tasks are never cancelled here.
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
REAL_CLOCK = RealClock()
|
|
152
|
+
REAL_SCHEDULER = RealScheduler()
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def scheduler_for(owner: object) -> Scheduler:
|
|
156
|
+
"""Return the scheduler collaborator of ``owner``.
|
|
157
|
+
|
|
158
|
+
Proxy lifecycle behavior is spread over mixins that are also reached through
|
|
159
|
+
partial collaborators, so the seam reads the collaborator instead of
|
|
160
|
+
requiring every holder to carry one. Anything without an explicit scheduler
|
|
161
|
+
gets the real ``asyncio`` one, which keeps the production default intact.
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
return getattr(owner, "_scheduler", REAL_SCHEDULER)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def clock_for(owner: object) -> Clock:
|
|
168
|
+
"""Return the clock collaborator of ``owner``, defaulting to real time."""
|
|
169
|
+
|
|
170
|
+
return getattr(owner, "_clock", REAL_CLOCK)
|
|
File without changes
|