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,491 @@
|
|
|
1
|
+
"""Bounded, process-local thread anchoring for unanchored Responses turns.
|
|
2
|
+
|
|
3
|
+
A Responses request that carries no continuity identifier (no
|
|
4
|
+
``conversation``, no ``previous_response_id``, no Codex session/thread header,
|
|
5
|
+
no client ``prompt_cache_key``) still belongs to a logical thread: every turn
|
|
6
|
+
resends the transcript, so consecutive turns overlap. This module recognises
|
|
7
|
+
that overlap so the proxy can keep one stable ``prompt_cache_key`` for the
|
|
8
|
+
thread instead of re-deriving a content hash that moves whenever the client
|
|
9
|
+
trims history or writes a volatile field into its first item.
|
|
10
|
+
|
|
11
|
+
Matching contract
|
|
12
|
+
-----------------
|
|
13
|
+
A turn extends an anchor **only** when the anchor's stored digest window is
|
|
14
|
+
consumed exactly by the head of the new turn's window::
|
|
15
|
+
|
|
16
|
+
stored[offset:] == incoming[:len(stored) - offset]
|
|
17
|
+
|
|
18
|
+
and **either** at least ``_MIN_OVERLAP_ITEMS`` items are matched, **or** the
|
|
19
|
+
two windows are byte-equal (an identical re-derivation of the same body, which
|
|
20
|
+
must return the same key). Nothing else matches.
|
|
21
|
+
|
|
22
|
+
``offset > 0`` is the append/leading-trim case: the window slid forward because
|
|
23
|
+
the client appended a turn, or dropped its oldest items and kept a contiguous
|
|
24
|
+
recent window. ``offset == 0`` with the incoming window strictly longer is the
|
|
25
|
+
append case for a thread whose transcript is still shorter than the retained
|
|
26
|
+
window.
|
|
27
|
+
|
|
28
|
+
The ``_MIN_OVERLAP_ITEMS`` floor is the whole false-merge guard, and it applies
|
|
29
|
+
to the ``offset == 0`` case too. A short stored window that is a *prefix* of an
|
|
30
|
+
incoming window is structurally indistinguishable from "the same thread
|
|
31
|
+
appended two items": the only defence is to require the shared run to be long
|
|
32
|
+
enough that it cannot be a generic opening. One or two shared leading items
|
|
33
|
+
routinely *are* generic -- every session in a repository opens with the same
|
|
34
|
+
``<environment_context>`` block, and parallel workers of one agent open with a
|
|
35
|
+
byte-identical preamble -- so accepting that evidence would hand one thread's
|
|
36
|
+
key to another and then (via ``register``) overwrite the first thread's window,
|
|
37
|
+
costing it the key it owns. Requiring four keeps both named shapes out with two
|
|
38
|
+
items of margin. The price is that a thread does not hold a key until its
|
|
39
|
+
transcript reaches four items; at two items per turn that is one extra mint, on
|
|
40
|
+
the smallest body of the thread's life.
|
|
41
|
+
|
|
42
|
+
A compacted or summarised turn does not extend any stored window, so it mints a
|
|
43
|
+
new anchor -- which is correct, because the upstream prefix cache is genuinely
|
|
44
|
+
cold after compaction. There is no fuzzy, suffix-only, or "longest common
|
|
45
|
+
prefix" fallback: partial evidence merges unrelated threads onto one account,
|
|
46
|
+
which is strictly worse than the churn this module removes.
|
|
47
|
+
|
|
48
|
+
Item digests are domain-separated by (api key id, model class, **full**
|
|
49
|
+
instructions). Two threads whose instructions differ only after the first 512
|
|
50
|
+
characters -- which collide today -- therefore never share an anchor.
|
|
51
|
+
|
|
52
|
+
Large items
|
|
53
|
+
-----------
|
|
54
|
+
Two ceilings bound the work, and neither may collapse the window, because a
|
|
55
|
+
window that differs every turn mints a key every turn -- the exact churn this
|
|
56
|
+
module removes, and worst for the p90 453k-token cohort it exists to serve:
|
|
57
|
+
|
|
58
|
+
* ``_MAX_WINDOW_ENCODED_CHARS`` stops the backward walk, but never before
|
|
59
|
+
``_MIN_WINDOW_ITEMS`` items are recorded. A single trailing item larger than
|
|
60
|
+
the whole-window ceiling therefore still yields a window deep enough to
|
|
61
|
+
survive an ordinary append.
|
|
62
|
+
* an item whose canonical encoding exceeds ``_MAX_ITEM_ENCODED_CHARS`` is a
|
|
63
|
+
**window boundary**: the walk stops there and the window is the items that
|
|
64
|
+
follow it. It is not digested (a truncated digest would re-introduce the
|
|
65
|
+
prefix collision this module removes) and it does not make the body
|
|
66
|
+
unanchorable -- one 1 MiB tool result used to keep the whole thread
|
|
67
|
+
unanchorable for as many turns as the window is deep. Two threads that differ
|
|
68
|
+
only inside such an item and agree on the four items after it can merge; that
|
|
69
|
+
requires four byte-identical items after the divergence and is accepted as
|
|
70
|
+
the price of not stranding the thread.
|
|
71
|
+
|
|
72
|
+
Memory bound
|
|
73
|
+
------------
|
|
74
|
+
All state is process-local and positive-only, mirroring the repository's
|
|
75
|
+
existing bounded TTL-LRU process cache shape:
|
|
76
|
+
|
|
77
|
+
* at most ``_MAX_ANCHORS`` (2048) anchors, each holding at most
|
|
78
|
+
``_MAX_WINDOW_ITEMS`` (32) x ``_ITEM_DIGEST_BYTES`` (8) = 256 bytes of
|
|
79
|
+
digests plus its key string;
|
|
80
|
+
* at most ``_MAX_INDEX_DIGESTS`` (65536) reverse-index rows, each holding at
|
|
81
|
+
most ``_MAX_CANDIDATES_PER_DIGEST`` (8) thread-key references.
|
|
82
|
+
|
|
83
|
+
Both are LRU-evicted ``OrderedDict``s, so the worst case is **~17.6 MiB** per
|
|
84
|
+
replica -- measured with every cap saturated, counting the ``OrderedDict``
|
|
85
|
+
tables, the key strings and the per-row candidate lists, not just the digest
|
|
86
|
+
payload -- and never grows with traffic. Anchors also expire after the caller's
|
|
87
|
+
TTL (the dashboard ``openai_cache_affinity_max_age_seconds``) so an anchor can
|
|
88
|
+
never outlive the ``sticky_sessions`` row it names. Losing an anchor is always
|
|
89
|
+
safe: the next turn mints a new key and is reported as such.
|
|
90
|
+
|
|
91
|
+
State is per process. A restart, or a blue/green swap where both colors serve,
|
|
92
|
+
loses anchors: a thread can hold two keys and two owners for that window. That
|
|
93
|
+
is bounded by the TTL and costs one prefix-cache miss; it is never incorrect.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
from __future__ import annotations
|
|
97
|
+
|
|
98
|
+
import json
|
|
99
|
+
from collections import OrderedDict
|
|
100
|
+
from collections.abc import Sequence
|
|
101
|
+
from dataclasses import dataclass
|
|
102
|
+
from enum import Enum
|
|
103
|
+
from hashlib import sha256
|
|
104
|
+
|
|
105
|
+
from app.core.clock import REAL_CLOCK, Clock
|
|
106
|
+
from app.core.types import JsonValue
|
|
107
|
+
|
|
108
|
+
# Truncated SHA-256 per input item. 64 bits over a window that is additionally
|
|
109
|
+
# verified item-by-item and domain-separated per (api key, model, instructions).
|
|
110
|
+
_ITEM_DIGEST_BYTES = 8
|
|
111
|
+
# Trailing items retained per thread. Bounds both the memory per anchor and the
|
|
112
|
+
# leading-trim depth that can still be recognised.
|
|
113
|
+
_MAX_WINDOW_ITEMS = 32
|
|
114
|
+
# A body must carry at least one item to be anchorable at all.
|
|
115
|
+
_MIN_ANCHORABLE_ITEMS = 1
|
|
116
|
+
# Floor the whole-window byte ceiling may not undercut. A window shallower than
|
|
117
|
+
# `_MIN_OVERLAP_ITEMS + items appended per turn` cannot survive an append, so a
|
|
118
|
+
# byte ceiling that stops the walk early turns the window into "the newest
|
|
119
|
+
# item", which differs every turn and mints a key every turn. Eight leaves room
|
|
120
|
+
# for a four-item turn (reasoning + call + output + message) on top of the
|
|
121
|
+
# four-item acceptance floor. Worst-case encoding work per request is therefore
|
|
122
|
+
# `_MIN_WINDOW_ITEMS * _MAX_ITEM_ENCODED_CHARS`, i.e. 8 MiB -- reachable only by
|
|
123
|
+
# a body that is itself at least 8 MiB, which the proxy has already parsed.
|
|
124
|
+
_MIN_WINDOW_ITEMS = 8
|
|
125
|
+
# Least evidence accepted for any reuse other than a byte-equal re-derivation.
|
|
126
|
+
# See "Matching contract": one or two shared leading/trailing items are
|
|
127
|
+
# routinely generic across independent threads, so accepting them merges those
|
|
128
|
+
# threads onto one key and one account and destroys the loser's window.
|
|
129
|
+
_MIN_OVERLAP_ITEMS = 4
|
|
130
|
+
# Serialization ceilings, applied *during* encoding so a 450k-token item is
|
|
131
|
+
# never materialised. Items are hashed in full -- a truncated digest would
|
|
132
|
+
# re-introduce exactly the prefix collision this module removes -- so an item
|
|
133
|
+
# past the per-item ceiling ends the window instead of being digested.
|
|
134
|
+
_MAX_ITEM_ENCODED_CHARS = 1024 * 1024
|
|
135
|
+
_MAX_WINDOW_ENCODED_CHARS = 256 * 1024
|
|
136
|
+
# LRU caps. See "Memory bound" above. Measured at ~17.6 MiB with every cap
|
|
137
|
+
# saturated; a replica serving the observed unanchored volume (16.6k requests
|
|
138
|
+
# per 10 h, 1800 s freshness window) stays far below the thread cap.
|
|
139
|
+
_MAX_ANCHORS = 2048
|
|
140
|
+
_MAX_CANDIDATES_PER_DIGEST = 8
|
|
141
|
+
_MAX_INDEX_DIGESTS = _MAX_ANCHORS * _MAX_WINDOW_ITEMS
|
|
142
|
+
# Index rows are capped, and a popular item -- one `<environment_context>`
|
|
143
|
+
# block shared by every session in a repository -- overflows its row. Probing
|
|
144
|
+
# the first few positions of the incoming window finds the thread through one
|
|
145
|
+
# of its rarer items instead. Candidates are only candidates: every one of
|
|
146
|
+
# them is still verified item by item.
|
|
147
|
+
_MAX_LOOKUP_PROBE_ITEMS = 8
|
|
148
|
+
|
|
149
|
+
# Deterministic per-item encoding: sorted keys, no insignificant whitespace,
|
|
150
|
+
# applied one item at a time so the window is bounded without materialising the
|
|
151
|
+
# whole list. These digests are compared only against each other inside this
|
|
152
|
+
# process, so the encoding has to be stable, not identical to any other
|
|
153
|
+
# fingerprint in the codebase.
|
|
154
|
+
#
|
|
155
|
+
# ``ensure_ascii`` is deliberately **off**. With it on, every non-ASCII code
|
|
156
|
+
# point becomes ``\uXXXX``: a Korean or Japanese transcript expands about
|
|
157
|
+
# sixfold, which both materialises megabytes for an item the walk may be about
|
|
158
|
+
# to refuse and trips ``_MAX_ITEM_ENCODED_CHARS`` at roughly a sixth of the
|
|
159
|
+
# documented size. Off, the encoded length tracks the raw length closely, which
|
|
160
|
+
# is also what makes the pre-check in ``_exceeds_item_ceiling`` tight. A str
|
|
161
|
+
# holding an unpaired surrogate then fails to encode and is reported
|
|
162
|
+
# ``UNENCODABLE`` (``UnicodeEncodeError`` is a ``ValueError``), which is the
|
|
163
|
+
# conservative answer for a body that is not representable anyway.
|
|
164
|
+
_ITEM_ENCODER = json.JSONEncoder(ensure_ascii=False, separators=(",", ":"), sort_keys=True)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@dataclass(frozen=True, slots=True)
|
|
168
|
+
class ThreadWindow:
|
|
169
|
+
"""Chronological, bounded digest window over the tail of one input list."""
|
|
170
|
+
|
|
171
|
+
digests: bytes
|
|
172
|
+
item_count: int
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@dataclass(slots=True)
|
|
176
|
+
class _Anchor:
|
|
177
|
+
digests: bytes
|
|
178
|
+
stored_at: float
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def thread_anchor_domain(*, owner_row_id: str, model_class: str, instructions: str) -> bytes:
|
|
182
|
+
"""Domain separator mixed into every item digest.
|
|
183
|
+
|
|
184
|
+
Length framing keeps distinct tuples distinct even when a component
|
|
185
|
+
contains the separator. ``instructions`` is hashed in full, not truncated:
|
|
186
|
+
two Codex system prompts that diverge only past 512 characters are
|
|
187
|
+
different threads.
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
parts = (owner_row_id.encode(), model_class.encode(), instructions.encode())
|
|
191
|
+
framed = b"".join(len(part).to_bytes(8, "big") + part for part in parts)
|
|
192
|
+
# ``owner_row_id`` is the API key's row id, not the key or any secret
|
|
193
|
+
# material, and this digest is never stored or compared as a credential: it
|
|
194
|
+
# is a process-local domain separator that keeps one tenant's thread anchors
|
|
195
|
+
# from colliding with another's. A deliberately slow password hash would run
|
|
196
|
+
# on every turn for no security benefit.
|
|
197
|
+
return sha256(framed).digest() # codeql[py/weak-sensitive-data-hashing]
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class _ItemVerdict(Enum):
|
|
201
|
+
"""Why an item was not digested. Both refuse a truncated digest."""
|
|
202
|
+
|
|
203
|
+
# Encoding exceeds ``_MAX_ITEM_ENCODED_CHARS``. The walk stops here and
|
|
204
|
+
# keeps the items that follow: the alternative -- reporting the whole body
|
|
205
|
+
# unanchorable -- strands the thread for as many turns as the window is
|
|
206
|
+
# deep while the oversized item stays in reach of the backward walk.
|
|
207
|
+
OVERSIZED = "oversized"
|
|
208
|
+
# The canonical encoder cannot represent the item, so no window containing
|
|
209
|
+
# or bounded by it is reproducible. The body is unanchorable.
|
|
210
|
+
UNENCODABLE = "unencodable"
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _exceeds_item_ceiling(item: JsonValue) -> bool:
|
|
214
|
+
"""Cheap structural pre-check for the per-item ceiling.
|
|
215
|
+
|
|
216
|
+
``iterencode`` streams a container, but it yields a single *scalar* -- a
|
|
217
|
+
450k-token text part is one string -- as one already-materialised chunk, so
|
|
218
|
+
the size check inside the encode loop can only fire after allocating it.
|
|
219
|
+
The sum of raw string lengths is a lower bound on the canonical encoding
|
|
220
|
+
length (escaping and punctuation only add), so exceeding the ceiling here
|
|
221
|
+
proves the encoding would exceed it, and the walk never materialises the
|
|
222
|
+
encoding of an item it is going to refuse. Staying under is not a proof, so
|
|
223
|
+
the encode loop keeps its own exact check -- but with ``ensure_ascii`` off
|
|
224
|
+
the two are close, rather than a factor of six apart on non-ASCII text.
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
pending: list[JsonValue] = [item]
|
|
228
|
+
total = 0
|
|
229
|
+
while pending:
|
|
230
|
+
node = pending.pop()
|
|
231
|
+
if isinstance(node, str):
|
|
232
|
+
total += len(node)
|
|
233
|
+
elif isinstance(node, dict):
|
|
234
|
+
pending.extend(node.keys())
|
|
235
|
+
pending.extend(node.values())
|
|
236
|
+
elif isinstance(node, list):
|
|
237
|
+
pending.extend(node)
|
|
238
|
+
else:
|
|
239
|
+
# Numbers, booleans and null all encode to a handful of characters.
|
|
240
|
+
total += 1
|
|
241
|
+
if total > _MAX_ITEM_ENCODED_CHARS:
|
|
242
|
+
return True
|
|
243
|
+
return False
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _bounded_item_digest(domain: bytes, item: JsonValue) -> tuple[bytes, int] | _ItemVerdict:
|
|
247
|
+
"""Digest one item's full canonical JSON, or say why it was not digested.
|
|
248
|
+
|
|
249
|
+
``iterencode`` yields chunks lazily and each chunk is folded straight into
|
|
250
|
+
the hash, so the encoding is never materialised.
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
if _exceeds_item_ceiling(item):
|
|
254
|
+
return _ItemVerdict.OVERSIZED
|
|
255
|
+
hasher = sha256(domain + b"\x1e")
|
|
256
|
+
size = 0
|
|
257
|
+
try:
|
|
258
|
+
for chunk in _ITEM_ENCODER.iterencode(item):
|
|
259
|
+
size += len(chunk)
|
|
260
|
+
if size > _MAX_ITEM_ENCODED_CHARS:
|
|
261
|
+
return _ItemVerdict.OVERSIZED
|
|
262
|
+
hasher.update(chunk.encode())
|
|
263
|
+
except (TypeError, ValueError):
|
|
264
|
+
return _ItemVerdict.UNENCODABLE
|
|
265
|
+
return hasher.digest()[:_ITEM_DIGEST_BYTES], size
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def build_thread_window(input_value: JsonValue, *, domain: bytes) -> ThreadWindow | None:
|
|
269
|
+
"""Digest the trailing items of ``input_value``, or ``None`` if unanchorable.
|
|
270
|
+
|
|
271
|
+
Only a list of items is anchorable: discrete items are what make "the
|
|
272
|
+
client appended a turn" distinguishable from "this is a different thread
|
|
273
|
+
that happens to share a prefix". ``ResponsesRequest`` already normalises a
|
|
274
|
+
bare string input into a one-item list, so the non-list branch is
|
|
275
|
+
defensive.
|
|
276
|
+
"""
|
|
277
|
+
|
|
278
|
+
if not isinstance(input_value, list):
|
|
279
|
+
return None
|
|
280
|
+
items: Sequence[JsonValue] = input_value
|
|
281
|
+
if len(items) < _MIN_ANCHORABLE_ITEMS:
|
|
282
|
+
return None
|
|
283
|
+
digests: list[bytes] = []
|
|
284
|
+
encoded_chars = 0
|
|
285
|
+
for item in reversed(items):
|
|
286
|
+
if len(digests) >= _MAX_WINDOW_ITEMS:
|
|
287
|
+
break
|
|
288
|
+
# The byte ceiling may only stop a walk that already holds a window
|
|
289
|
+
# deep enough to survive an append. Stopping earlier records "the
|
|
290
|
+
# newest item", which differs every turn.
|
|
291
|
+
if encoded_chars >= _MAX_WINDOW_ENCODED_CHARS and len(digests) >= _MIN_WINDOW_ITEMS:
|
|
292
|
+
break
|
|
293
|
+
digested = _bounded_item_digest(domain, item)
|
|
294
|
+
if digested is _ItemVerdict.UNENCODABLE:
|
|
295
|
+
return None
|
|
296
|
+
if digested is _ItemVerdict.OVERSIZED:
|
|
297
|
+
# Window boundary, not poison: keep the items already collected.
|
|
298
|
+
break
|
|
299
|
+
assert not isinstance(digested, _ItemVerdict)
|
|
300
|
+
digest, encoded_size = digested
|
|
301
|
+
encoded_chars += encoded_size
|
|
302
|
+
digests.append(digest)
|
|
303
|
+
if len(digests) < _MIN_ANCHORABLE_ITEMS:
|
|
304
|
+
return None
|
|
305
|
+
digests.reverse()
|
|
306
|
+
return ThreadWindow(digests=b"".join(digests), item_count=len(digests))
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _overlap_items(stored: bytes, incoming: bytes) -> int:
|
|
310
|
+
"""Items matched when ``stored``'s suffix is consumed by ``incoming``'s head.
|
|
311
|
+
|
|
312
|
+
Offsets ascend, so the first match is the deepest one.
|
|
313
|
+
"""
|
|
314
|
+
|
|
315
|
+
for offset in range(0, len(stored), _ITEM_DIGEST_BYTES):
|
|
316
|
+
suffix_length = len(stored) - offset
|
|
317
|
+
if suffix_length > len(incoming):
|
|
318
|
+
continue
|
|
319
|
+
if stored[offset:] == incoming[:suffix_length]:
|
|
320
|
+
return suffix_length // _ITEM_DIGEST_BYTES
|
|
321
|
+
return 0
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _accepts_overlap(stored: bytes, incoming: bytes, overlap_items: int) -> bool:
|
|
325
|
+
"""Whether ``overlap_items`` matched items may reuse ``stored``'s key.
|
|
326
|
+
|
|
327
|
+
The rule, and nothing else:
|
|
328
|
+
|
|
329
|
+
1. at least ``_MIN_OVERLAP_ITEMS`` items matched, or
|
|
330
|
+
2. the two windows are byte-equal -- the same items in the same order, an
|
|
331
|
+
identical re-derivation of one body, where nothing is being extended and
|
|
332
|
+
so there is no shared-opening ambiguity to resolve.
|
|
333
|
+
|
|
334
|
+
Rule 1 is deliberately applied to a stored window that the incoming window
|
|
335
|
+
merely *extends* as well. "Thread B opens with everything thread A has
|
|
336
|
+
recorded" and "thread A appended a turn" produce byte-identical evidence,
|
|
337
|
+
so the only available defence is to demand a shared run too long to be a
|
|
338
|
+
generic opening. Accepting less hands A's key to B and then lets B's
|
|
339
|
+
``register`` overwrite A's window, so A permanently loses the key it owns:
|
|
340
|
+
a false merge pins unrelated work to one account and is worse than the
|
|
341
|
+
churn this module removes.
|
|
342
|
+
"""
|
|
343
|
+
|
|
344
|
+
if overlap_items <= 0:
|
|
345
|
+
return False
|
|
346
|
+
if overlap_items >= _MIN_OVERLAP_ITEMS:
|
|
347
|
+
return True
|
|
348
|
+
return stored == incoming
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
class ThreadAnchorIndex:
|
|
352
|
+
"""Positive-only, TTL- and LRU-bounded index from digest window to thread key.
|
|
353
|
+
|
|
354
|
+
Not synchronised: the proxy resolves affinity synchronously inside one
|
|
355
|
+
event-loop task, so no await point can interleave two mutations.
|
|
356
|
+
"""
|
|
357
|
+
|
|
358
|
+
def __init__(
|
|
359
|
+
self,
|
|
360
|
+
*,
|
|
361
|
+
max_anchors: int = _MAX_ANCHORS,
|
|
362
|
+
max_index_digests: int = _MAX_INDEX_DIGESTS,
|
|
363
|
+
max_candidates_per_digest: int = _MAX_CANDIDATES_PER_DIGEST,
|
|
364
|
+
clock: Clock = REAL_CLOCK,
|
|
365
|
+
) -> None:
|
|
366
|
+
if max_anchors <= 0 or max_index_digests <= 0 or max_candidates_per_digest <= 0:
|
|
367
|
+
raise ValueError("thread anchor bounds must be positive")
|
|
368
|
+
self._clock = clock
|
|
369
|
+
self._max_anchors = max_anchors
|
|
370
|
+
self._max_index_digests = max_index_digests
|
|
371
|
+
self._max_candidates_per_digest = max_candidates_per_digest
|
|
372
|
+
self._anchors: OrderedDict[str, _Anchor] = OrderedDict()
|
|
373
|
+
self._index: OrderedDict[bytes, list[str]] = OrderedDict()
|
|
374
|
+
|
|
375
|
+
def __len__(self) -> int:
|
|
376
|
+
return len(self._anchors)
|
|
377
|
+
|
|
378
|
+
@property
|
|
379
|
+
def index_size(self) -> int:
|
|
380
|
+
return len(self._index)
|
|
381
|
+
|
|
382
|
+
def clear(self) -> None:
|
|
383
|
+
self._anchors.clear()
|
|
384
|
+
self._index.clear()
|
|
385
|
+
|
|
386
|
+
def lookup(self, window: ThreadWindow, *, ttl_seconds: float) -> str | None:
|
|
387
|
+
"""Return the thread key this window verifiably extends, if any.
|
|
388
|
+
|
|
389
|
+
The index is only a candidate source; the returned key is always
|
|
390
|
+
confirmed by an exact item-digest comparison, and the deepest verified
|
|
391
|
+
overlap wins.
|
|
392
|
+
"""
|
|
393
|
+
|
|
394
|
+
now = self._clock.monotonic()
|
|
395
|
+
best_key: str | None = None
|
|
396
|
+
best_overlap = 0
|
|
397
|
+
probes = min(window.item_count, _MAX_LOOKUP_PROBE_ITEMS)
|
|
398
|
+
seen: set[str] = set()
|
|
399
|
+
for probe in range(probes):
|
|
400
|
+
offset = probe * _ITEM_DIGEST_BYTES
|
|
401
|
+
digest = window.digests[offset : offset + _ITEM_DIGEST_BYTES]
|
|
402
|
+
candidates = self._index.get(digest)
|
|
403
|
+
if not candidates:
|
|
404
|
+
continue
|
|
405
|
+
self._index.move_to_end(digest)
|
|
406
|
+
for thread_key in list(candidates):
|
|
407
|
+
anchor = self._anchors.get(thread_key)
|
|
408
|
+
if anchor is None or now - anchor.stored_at >= ttl_seconds:
|
|
409
|
+
dropped = self._anchors.pop(thread_key, None)
|
|
410
|
+
if dropped is not None:
|
|
411
|
+
self._forget_index_references(thread_key, dropped.digests)
|
|
412
|
+
elif thread_key in candidates:
|
|
413
|
+
candidates.remove(thread_key)
|
|
414
|
+
continue
|
|
415
|
+
if thread_key in seen:
|
|
416
|
+
continue
|
|
417
|
+
seen.add(thread_key)
|
|
418
|
+
overlap = _overlap_items(anchor.digests, window.digests)
|
|
419
|
+
accepted = _accepts_overlap(anchor.digests, window.digests, overlap)
|
|
420
|
+
if accepted and overlap > best_overlap:
|
|
421
|
+
best_key = thread_key
|
|
422
|
+
best_overlap = overlap
|
|
423
|
+
if not candidates:
|
|
424
|
+
self._index.pop(digest, None)
|
|
425
|
+
if best_key is not None:
|
|
426
|
+
self._anchors.move_to_end(best_key)
|
|
427
|
+
return best_key
|
|
428
|
+
|
|
429
|
+
def _forget_index_references(self, thread_key: str, digests: bytes) -> None:
|
|
430
|
+
"""Remove one thread key from every digest row its window indexed.
|
|
431
|
+
|
|
432
|
+
Called whenever an anchor stops being reachable under a window --
|
|
433
|
+
expiry, LRU eviction, or a re-register that replaces the window. Left
|
|
434
|
+
behind, a dead key keeps occupying one of the
|
|
435
|
+
``_max_candidates_per_digest`` slots on a popular digest row and can
|
|
436
|
+
crowd a live anchor out of a row before that anchor is ever verified.
|
|
437
|
+
"""
|
|
438
|
+
|
|
439
|
+
for offset in range(0, len(digests), _ITEM_DIGEST_BYTES):
|
|
440
|
+
digest = digests[offset : offset + _ITEM_DIGEST_BYTES]
|
|
441
|
+
row = self._index.get(digest)
|
|
442
|
+
if row is None:
|
|
443
|
+
continue
|
|
444
|
+
if thread_key in row:
|
|
445
|
+
row.remove(thread_key)
|
|
446
|
+
if not row:
|
|
447
|
+
self._index.pop(digest, None)
|
|
448
|
+
|
|
449
|
+
def register(self, thread_key: str, window: ThreadWindow) -> None:
|
|
450
|
+
"""Record this turn's window as the thread's anchor and index it."""
|
|
451
|
+
|
|
452
|
+
previous = self._anchors.get(thread_key)
|
|
453
|
+
if previous is not None and previous.digests != window.digests:
|
|
454
|
+
# The old window's digests no longer reach this thread.
|
|
455
|
+
self._forget_index_references(thread_key, previous.digests)
|
|
456
|
+
self._anchors[thread_key] = _Anchor(digests=window.digests, stored_at=self._clock.monotonic())
|
|
457
|
+
self._anchors.move_to_end(thread_key)
|
|
458
|
+
while len(self._anchors) > self._max_anchors:
|
|
459
|
+
evicted_key, evicted = self._anchors.popitem(last=False)
|
|
460
|
+
self._forget_index_references(evicted_key, evicted.digests)
|
|
461
|
+
for offset in range(0, len(window.digests), _ITEM_DIGEST_BYTES):
|
|
462
|
+
digest = window.digests[offset : offset + _ITEM_DIGEST_BYTES]
|
|
463
|
+
row = self._index.get(digest)
|
|
464
|
+
if row is None:
|
|
465
|
+
row = []
|
|
466
|
+
self._index[digest] = row
|
|
467
|
+
elif thread_key in row:
|
|
468
|
+
row.remove(thread_key)
|
|
469
|
+
row.append(thread_key)
|
|
470
|
+
del row[: -self._max_candidates_per_digest]
|
|
471
|
+
self._index.move_to_end(digest)
|
|
472
|
+
while len(self._index) > self._max_index_digests:
|
|
473
|
+
self._index.popitem(last=False)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
_THREAD_ANCHOR_INDEX: ThreadAnchorIndex | None = None
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def get_thread_anchor_index() -> ThreadAnchorIndex:
|
|
480
|
+
"""Process-wide anchor index (one per replica worker)."""
|
|
481
|
+
|
|
482
|
+
global _THREAD_ANCHOR_INDEX
|
|
483
|
+
if _THREAD_ANCHOR_INDEX is None:
|
|
484
|
+
_THREAD_ANCHOR_INDEX = ThreadAnchorIndex()
|
|
485
|
+
return _THREAD_ANCHOR_INDEX
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def reset_thread_anchor_index() -> None:
|
|
489
|
+
"""Drop all anchors. Test helper; safe at runtime (anchors are a cache)."""
|
|
490
|
+
|
|
491
|
+
get_thread_anchor_index().clear()
|