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,830 @@
|
|
|
1
|
+
"""Sticky-affinity and prompt-cache key helpers for proxy routing.
|
|
2
|
+
|
|
3
|
+
This module owns the pure request/header policy used by ``ProxyService`` to
|
|
4
|
+
choose a sticky session family. Keeping it outside ``service.py`` makes the
|
|
5
|
+
routing decisions testable without adding more responsibility to the proxy
|
|
6
|
+
orchestration class.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from collections.abc import Mapping
|
|
13
|
+
from dataclasses import dataclass, replace
|
|
14
|
+
from hashlib import sha256
|
|
15
|
+
from typing import Literal, TypedDict
|
|
16
|
+
from uuid import uuid4
|
|
17
|
+
|
|
18
|
+
from app.core.metrics.prometheus import (
|
|
19
|
+
PROMETHEUS_AVAILABLE,
|
|
20
|
+
prompt_cache_key_derivation_total,
|
|
21
|
+
)
|
|
22
|
+
from app.core.openai.requests import (
|
|
23
|
+
ResponsesCompactRequest,
|
|
24
|
+
ResponsesRequest,
|
|
25
|
+
extract_input_file_ids,
|
|
26
|
+
responses_request_contains_goal_continuation_context,
|
|
27
|
+
)
|
|
28
|
+
from app.db.models import StickySessionKind
|
|
29
|
+
from app.modules.api_keys.service import ApiKeyData
|
|
30
|
+
from app.modules.proxy.replay_safety import responses_payload_is_account_neutral_fresh_replay
|
|
31
|
+
from app.modules.proxy.thread_anchors import (
|
|
32
|
+
build_thread_window,
|
|
33
|
+
get_thread_anchor_index,
|
|
34
|
+
thread_anchor_domain,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# This typed provenance is a routing capability: callers must never recover it
|
|
38
|
+
# from key text, because a client-controlled turn state can mimic any prefix.
|
|
39
|
+
_CodexSessionSource = Literal["session_header", "thread_header", "turn_state"]
|
|
40
|
+
# Request headers are stripped and HTTP forbids CR/LF, while PostgreSQL/SQLite
|
|
41
|
+
# text keys can safely retain LF. This sentinel makes the internal namespace
|
|
42
|
+
# structurally unreachable by every legacy raw header, even if its digest is
|
|
43
|
+
# disclosed.
|
|
44
|
+
_CODEX_SELECTION_KEY_PREFIX = "\ncodex-lb-affinity-v1"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class _AffinitySelectionKwargs(TypedDict):
|
|
48
|
+
sticky_key: str | None
|
|
49
|
+
sticky_kind: StickySessionKind | None
|
|
50
|
+
reallocate_sticky: bool
|
|
51
|
+
sticky_source: _CodexSessionSource | None
|
|
52
|
+
legacy_sticky_key: str | None
|
|
53
|
+
legacy_continuity_source: _CodexSessionSource | None
|
|
54
|
+
sticky_seed_key: str | None
|
|
55
|
+
sticky_seed_kind: StickySessionKind | None
|
|
56
|
+
spill_bare_session_on_account_cap: bool
|
|
57
|
+
abandon_unavailable_legacy_owner: bool
|
|
58
|
+
require_unambiguous_account: bool
|
|
59
|
+
sticky_max_age_seconds: int | None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True, slots=True)
|
|
63
|
+
class _AffinityPolicy:
|
|
64
|
+
key: str | None = None
|
|
65
|
+
kind: StickySessionKind | None = None
|
|
66
|
+
reallocate_sticky: bool = False
|
|
67
|
+
# Source capability only. Shared selection still revokes spillover for a
|
|
68
|
+
# required owner or any stage that may carry account-local state.
|
|
69
|
+
spill_on_account_cap: bool = False
|
|
70
|
+
# An explicit, self-contained Codex goal restart may retire only a raw
|
|
71
|
+
# compatibility owner whose durable account status is unavailable.
|
|
72
|
+
abandon_unavailable_legacy_owner: bool = False
|
|
73
|
+
max_age_seconds: int | None = None
|
|
74
|
+
codex_session_source: _CodexSessionSource | None = None
|
|
75
|
+
# A thread row is soft locality, but old replicas may have persisted the
|
|
76
|
+
# raw process/session value as hard CODEX_SESSION ownership. Keep that
|
|
77
|
+
# compatibility lookup explicit instead of trying to reconstruct it from
|
|
78
|
+
# the new opaque thread key.
|
|
79
|
+
legacy_codex_session_key: str | None = None
|
|
80
|
+
# Interpretation used when consulting that raw key. Process-session text
|
|
81
|
+
# is session_header even on a thread-scoped request; a thread-only raw
|
|
82
|
+
# key stays thread_header so a session_header tombstone cannot hide it.
|
|
83
|
+
legacy_continuity_source: _CodexSessionSource | None = None
|
|
84
|
+
# A previously unseen thread should inherit the healthy process preference
|
|
85
|
+
# once, then persist its own bounded row. This is never ownership: a
|
|
86
|
+
# missing process default may be initialized once by insert-if-absent, but
|
|
87
|
+
# no thread request may update or delete an established process row.
|
|
88
|
+
seed_selection_key: str | None = None
|
|
89
|
+
seed_selection_kind: StickySessionKind | None = None
|
|
90
|
+
# ``conversation`` has no dedicated owner index. Preserve that provenance
|
|
91
|
+
# until selection can prove one hard owner or a one-account pool.
|
|
92
|
+
require_unambiguous_account: bool = False
|
|
93
|
+
# For a policy whose key IS the prompt cache key: whether the client sent
|
|
94
|
+
# that key (``payload``) or the proxy derived it (``derived``). Recorded by
|
|
95
|
+
# the resolver rather than re-derived by callers, because
|
|
96
|
+
# ``_resolve_prompt_cache_key`` writes the derived key back onto the
|
|
97
|
+
# payload — so a later reader of the payload cannot tell the two apart, and
|
|
98
|
+
# a blank client hint the resolver rejected still looks present. Routing
|
|
99
|
+
# never reads this; it exists so the request log can describe the decision.
|
|
100
|
+
prompt_cache_key_source: str | None = None
|
|
101
|
+
# How the forwarded ``prompt_cache_key`` was obtained this turn. Diagnostic
|
|
102
|
+
# only: it never participates in routing, but it is the signal that tells
|
|
103
|
+
# an operator whether unanchored threads are being held or are churning.
|
|
104
|
+
prompt_cache_derivation_outcome: str | None = None
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def selection_key(self) -> str | None:
|
|
108
|
+
if self.key is None or self.codex_session_source != "session_header":
|
|
109
|
+
return self.key
|
|
110
|
+
# CODEX_SESSION historically mixed raw session and turn-state values.
|
|
111
|
+
# Namespace only the newly soft source; raw legacy rows stay hard so a
|
|
112
|
+
# rolling upgrade cannot reinterpret existing continuity ownership.
|
|
113
|
+
return _codex_session_selection_key(self.key)
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def legacy_selection_key(self) -> str | None:
|
|
117
|
+
# Old replicas persisted bare session headers as raw CODEX_SESSION
|
|
118
|
+
# keys. Always consult this alongside the soft row: any raw hit may be
|
|
119
|
+
# hard turn-state ownership and therefore takes precedence.
|
|
120
|
+
if self.legacy_codex_session_key is not None:
|
|
121
|
+
return self.legacy_codex_session_key
|
|
122
|
+
return self.key if self.codex_session_source == "session_header" else None
|
|
123
|
+
|
|
124
|
+
def selection_kwargs(self) -> _AffinitySelectionKwargs:
|
|
125
|
+
"""Expand routing policy once at the account-selection boundary."""
|
|
126
|
+
|
|
127
|
+
# Keep the compatibility edge from silently omitting new policy
|
|
128
|
+
# fields. In particular, thread locality is incomplete if callers pass
|
|
129
|
+
# its row but forget the process seed or legacy hard-owner lookup.
|
|
130
|
+
return {
|
|
131
|
+
"sticky_key": self.selection_key,
|
|
132
|
+
"sticky_kind": self.kind,
|
|
133
|
+
"reallocate_sticky": self.reallocate_sticky,
|
|
134
|
+
"sticky_source": self.codex_session_source,
|
|
135
|
+
"legacy_sticky_key": self.legacy_selection_key,
|
|
136
|
+
"legacy_continuity_source": (
|
|
137
|
+
None if self.legacy_selection_key is None else (self.legacy_continuity_source or "session_header")
|
|
138
|
+
),
|
|
139
|
+
"sticky_seed_key": self.seed_selection_key,
|
|
140
|
+
"sticky_seed_kind": self.seed_selection_kind,
|
|
141
|
+
"spill_bare_session_on_account_cap": self.spill_on_account_cap,
|
|
142
|
+
"abandon_unavailable_legacy_owner": self.abandon_unavailable_legacy_owner,
|
|
143
|
+
"require_unambiguous_account": self.require_unambiguous_account,
|
|
144
|
+
"sticky_max_age_seconds": self.max_age_seconds,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
@staticmethod
|
|
148
|
+
def cap_spillover_allowed(
|
|
149
|
+
capability: bool,
|
|
150
|
+
preferred_account_id: str | None,
|
|
151
|
+
request_stage: str,
|
|
152
|
+
) -> bool:
|
|
153
|
+
"""Keep soft cap spillover strictly before account-owned transport state."""
|
|
154
|
+
return capability and preferred_account_id is None and request_stage in ("first_turn", "follow_up")
|
|
155
|
+
|
|
156
|
+
@staticmethod
|
|
157
|
+
def preferred_owner_sticky_inputs(
|
|
158
|
+
sticky_key: str | None,
|
|
159
|
+
sticky_kind: StickySessionKind | None,
|
|
160
|
+
reallocate_sticky: bool,
|
|
161
|
+
sticky_max_age_seconds: int | None,
|
|
162
|
+
sticky_source: _CodexSessionSource | None,
|
|
163
|
+
legacy_sticky_key: str | None,
|
|
164
|
+
) -> tuple[
|
|
165
|
+
str | None,
|
|
166
|
+
StickySessionKind | None,
|
|
167
|
+
bool,
|
|
168
|
+
int | None,
|
|
169
|
+
_CodexSessionSource | None,
|
|
170
|
+
str | None,
|
|
171
|
+
]:
|
|
172
|
+
if sticky_source not in {"session_header", "thread_header"}:
|
|
173
|
+
return (
|
|
174
|
+
sticky_key,
|
|
175
|
+
sticky_kind,
|
|
176
|
+
reallocate_sticky,
|
|
177
|
+
sticky_max_age_seconds,
|
|
178
|
+
sticky_source,
|
|
179
|
+
legacy_sticky_key,
|
|
180
|
+
)
|
|
181
|
+
# A resolved response/file/bridge owner bypasses the current-Codex
|
|
182
|
+
# soft row (process-session or thread PROMPT_CACHE). The raw
|
|
183
|
+
# compatibility row still has to be checked for conflicting legacy
|
|
184
|
+
# hard ownership. Selection receives no writable sticky key, so a
|
|
185
|
+
# raw miss cannot manufacture or rebind a mapping. The caller also
|
|
186
|
+
# deliberately omits any broader process seed in this exact-owner path.
|
|
187
|
+
return None, StickySessionKind.CODEX_SESSION, False, sticky_max_age_seconds, sticky_source, legacy_sticky_key
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _codex_session_selection_key(key: str) -> str:
|
|
191
|
+
# The digest avoids storing client values, while the header-impossible
|
|
192
|
+
# sentinel above—not secrecy—provides source separation from raw rows.
|
|
193
|
+
digest = sha256(key.encode()).hexdigest()
|
|
194
|
+
return f"{_CODEX_SELECTION_KEY_PREFIX}:session_header:{digest}"
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@dataclass(frozen=True, slots=True)
|
|
198
|
+
class _CodexBackendIdentity:
|
|
199
|
+
"""Independently parsed process-tree and logical-thread identities."""
|
|
200
|
+
|
|
201
|
+
process_session: str | None
|
|
202
|
+
thread_id: str | None
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def thread_selection_key(self) -> str | None:
|
|
206
|
+
if self.thread_id is None:
|
|
207
|
+
return None
|
|
208
|
+
# The explicit scope tag prevents the thread-only compatibility form
|
|
209
|
+
# from colliding with (process, thread). Length framing keeps distinct
|
|
210
|
+
# client tuples distinct even if a future non-HTTP caller admits NULs
|
|
211
|
+
# or other delimiters. The LF namespace remains unreachable by headers.
|
|
212
|
+
if self.process_session is None:
|
|
213
|
+
parts = ("thread-only", self.thread_id)
|
|
214
|
+
scope = "thread_only"
|
|
215
|
+
else:
|
|
216
|
+
parts = ("process-thread", self.process_session, self.thread_id)
|
|
217
|
+
scope = "process_thread"
|
|
218
|
+
encoded_parts = (part.encode() for part in parts)
|
|
219
|
+
framed = b"".join(len(part).to_bytes(8, "big") + part for part in encoded_parts)
|
|
220
|
+
digest = sha256(framed).hexdigest()
|
|
221
|
+
return f"{_CODEX_SELECTION_KEY_PREFIX}:thread_header:{scope}:{digest}"
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
_CODEX_PROCESS_SESSION_HEADERS = (
|
|
225
|
+
"session_id",
|
|
226
|
+
"session-id",
|
|
227
|
+
"x-codex-session-id",
|
|
228
|
+
"x-codex-conversation-id",
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _normalized_header_value(headers: Mapping[str, str], names: tuple[str, ...]) -> str | None:
|
|
233
|
+
normalized = {key.lower(): value for key, value in headers.items()}
|
|
234
|
+
for name in names:
|
|
235
|
+
value = normalized.get(name)
|
|
236
|
+
if not isinstance(value, str):
|
|
237
|
+
continue
|
|
238
|
+
stripped = value.strip()
|
|
239
|
+
if stripped:
|
|
240
|
+
return stripped
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _process_session_key_from_headers(headers: Mapping[str, str]) -> str | None:
|
|
245
|
+
return _normalized_header_value(headers, _CODEX_PROCESS_SESSION_HEADERS)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _thread_id_from_headers(headers: Mapping[str, str]) -> str | None:
|
|
249
|
+
return _normalized_header_value(headers, ("thread-id",))
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _codex_backend_identity(
|
|
253
|
+
headers: Mapping[str, str],
|
|
254
|
+
*,
|
|
255
|
+
thread_id: str | None = None,
|
|
256
|
+
) -> _CodexBackendIdentity:
|
|
257
|
+
normalized_thread_id = thread_id.strip() if isinstance(thread_id, str) and thread_id.strip() else None
|
|
258
|
+
return _CodexBackendIdentity(
|
|
259
|
+
process_session=_process_session_key_from_headers(headers),
|
|
260
|
+
thread_id=normalized_thread_id if thread_id is not None else _thread_id_from_headers(headers),
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _prompt_cache_key_from_request_model(payload: ResponsesRequest | ResponsesCompactRequest) -> str | None:
|
|
265
|
+
typed_value = getattr(payload, "prompt_cache_key", None)
|
|
266
|
+
if isinstance(typed_value, str) and typed_value:
|
|
267
|
+
return typed_value
|
|
268
|
+
if not payload.model_extra:
|
|
269
|
+
return None
|
|
270
|
+
extra_value = payload.model_extra.get("prompt_cache_key")
|
|
271
|
+
if isinstance(extra_value, str) and extra_value:
|
|
272
|
+
return extra_value
|
|
273
|
+
camel_value = payload.model_extra.get("promptCacheKey")
|
|
274
|
+
if isinstance(camel_value, str) and camel_value:
|
|
275
|
+
return camel_value
|
|
276
|
+
return None
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _extract_model_class(model: str) -> str:
|
|
280
|
+
"""Extract model class from model name for cache key prefix.
|
|
281
|
+
|
|
282
|
+
Classification:
|
|
283
|
+
- "mini" for gpt-5.4-mini
|
|
284
|
+
- "codex" for gpt-5.3-codex* (any variant)
|
|
285
|
+
- "std" for all others
|
|
286
|
+
"""
|
|
287
|
+
if "codex" in model:
|
|
288
|
+
return "codex"
|
|
289
|
+
if "mini" in model:
|
|
290
|
+
return "mini"
|
|
291
|
+
return "std"
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
# Version marker for keys minted by the thread-anchor derivation. It also
|
|
295
|
+
# separates the new shape from the legacy content-hash shape
|
|
296
|
+
# (``{model_class}-{api_key_id[:12]}-{hash}...``) so the stale legacy rows can
|
|
297
|
+
# be swept by key prefix. The readable model class and API-key prefix are kept
|
|
298
|
+
# because operators search ``sticky_sessions`` by key text.
|
|
299
|
+
_ANCHORED_KEY_VERSION = "v2t"
|
|
300
|
+
# Fallback when no caller supplies the dashboard freshness window. Matches the
|
|
301
|
+
# ``openai_cache_affinity_max_age_seconds`` column default.
|
|
302
|
+
_DEFAULT_ANCHOR_TTL_SECONDS = 1800
|
|
303
|
+
|
|
304
|
+
DERIVATION_OUTCOME_PAYLOAD = "payload"
|
|
305
|
+
DERIVATION_OUTCOME_DISABLED = "disabled"
|
|
306
|
+
DERIVATION_OUTCOME_ANCHOR_HIT = "anchor_hit"
|
|
307
|
+
DERIVATION_OUTCOME_ANCHOR_NEW = "anchor_new"
|
|
308
|
+
DERIVATION_OUTCOME_ANCHOR_RESET = "anchor_reset"
|
|
309
|
+
DERIVATION_OUTCOME_UNANCHORABLE = "unanchorable"
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
@dataclass(frozen=True, slots=True)
|
|
313
|
+
class _PromptCacheAnchor:
|
|
314
|
+
"""Result of anchoring one turn.
|
|
315
|
+
|
|
316
|
+
``sticky_key`` is both the value forwarded upstream and the sticky routing
|
|
317
|
+
key -- deliberately the same string, so there is no proxy-minted value that
|
|
318
|
+
is forwarded but must not be routed on. It is ``None`` when the turn is
|
|
319
|
+
unanchorable: there is no transcript to hold, so there is nothing to name,
|
|
320
|
+
selection takes the unbound path, and **nothing is written onto the
|
|
321
|
+
payload**.
|
|
322
|
+
|
|
323
|
+
That last part is load-bearing. The same payload object is resolved a
|
|
324
|
+
second time on the real bridge -> ``_stream_with_retry`` fallback, and a
|
|
325
|
+
written placeholder comes back through the client-supplied branch: a
|
|
326
|
+
constant ``<class>-<apikey12>`` string promoted to a PROMPT_CACHE sticky
|
|
327
|
+
key, collapsing every unanchorable thread of one API key onto one row and
|
|
328
|
+
one account, reported as ``source=payload``.
|
|
329
|
+
"""
|
|
330
|
+
|
|
331
|
+
sticky_key: str | None
|
|
332
|
+
outcome: str
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _input_contains_prior_assistant_turn(payload: ResponsesRequest | ResponsesCompactRequest) -> bool:
|
|
336
|
+
"""Whether this body already carries model output from an earlier turn."""
|
|
337
|
+
|
|
338
|
+
input_value = getattr(payload, "input", None)
|
|
339
|
+
if not isinstance(input_value, list):
|
|
340
|
+
return False
|
|
341
|
+
for item in input_value:
|
|
342
|
+
if not isinstance(item, dict):
|
|
343
|
+
continue
|
|
344
|
+
if item.get("role") == "assistant" or item.get("type") == "reasoning":
|
|
345
|
+
return True
|
|
346
|
+
return False
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _derive_prompt_cache_anchor(
|
|
350
|
+
payload: ResponsesRequest | ResponsesCompactRequest,
|
|
351
|
+
api_key: ApiKeyData | None,
|
|
352
|
+
*,
|
|
353
|
+
max_age_seconds: int = _DEFAULT_ANCHOR_TTL_SECONDS,
|
|
354
|
+
) -> _PromptCacheAnchor:
|
|
355
|
+
"""Anchor a turn with no client-supplied ``prompt_cache_key`` to its thread.
|
|
356
|
+
|
|
357
|
+
The key is minted once per thread and then reused for every turn whose
|
|
358
|
+
transcript verifiably extends the previous one (see
|
|
359
|
+
``thread_anchors``). It is therefore stable across appends, across a
|
|
360
|
+
client trimming its leading history, and across an identical re-derivation
|
|
361
|
+
of the same body (bridge-to-HTTP fallback, cross-transport replay). It
|
|
362
|
+
deliberately does **not** survive compaction: a compacted turn does not
|
|
363
|
+
extend the stored transcript, the upstream prefix cache is cold, and the
|
|
364
|
+
only way to bridge it would be fuzzy matching that merges distinct threads.
|
|
365
|
+
|
|
366
|
+
Unlike the previous content-hash derivation, nothing here is a truncated
|
|
367
|
+
digest of client text, so two threads that share their first 512
|
|
368
|
+
characters -- one `<environment_context>` block for one cwd, for
|
|
369
|
+
``codex_cli_rs`` -- no longer collapse onto one key.
|
|
370
|
+
"""
|
|
371
|
+
|
|
372
|
+
model = getattr(payload, "model", None)
|
|
373
|
+
model_class = _extract_model_class(model) if isinstance(model, str) and model else None
|
|
374
|
+
api_key_id = api_key.id if api_key is not None else ""
|
|
375
|
+
instructions = getattr(payload, "instructions", None)
|
|
376
|
+
instructions_text = instructions if isinstance(instructions, str) else ""
|
|
377
|
+
readable_parts = [part for part in (model_class, api_key_id[:12] or None) if part]
|
|
378
|
+
|
|
379
|
+
window = build_thread_window(
|
|
380
|
+
getattr(payload, "input", None),
|
|
381
|
+
domain=thread_anchor_domain(
|
|
382
|
+
owner_row_id=api_key_id,
|
|
383
|
+
model_class=model_class or "",
|
|
384
|
+
instructions=instructions_text,
|
|
385
|
+
),
|
|
386
|
+
)
|
|
387
|
+
if window is None:
|
|
388
|
+
return _PromptCacheAnchor(sticky_key=None, outcome=DERIVATION_OUTCOME_UNANCHORABLE)
|
|
389
|
+
|
|
390
|
+
index = get_thread_anchor_index()
|
|
391
|
+
anchored_key = index.lookup(window, ttl_seconds=max_age_seconds)
|
|
392
|
+
if anchored_key is not None:
|
|
393
|
+
index.register(anchored_key, window)
|
|
394
|
+
return _PromptCacheAnchor(anchored_key, DERIVATION_OUTCOME_ANCHOR_HIT)
|
|
395
|
+
|
|
396
|
+
minted_key = "-".join([_ANCHORED_KEY_VERSION, *readable_parts, uuid4().hex[:16]])
|
|
397
|
+
index.register(minted_key, window)
|
|
398
|
+
# A body that already carries model output belonged to a thread we could
|
|
399
|
+
# not match: compaction, a restart, or an anchor eviction. Separating it
|
|
400
|
+
# from a genuinely first turn is what makes the counter actionable.
|
|
401
|
+
outcome = (
|
|
402
|
+
DERIVATION_OUTCOME_ANCHOR_RESET
|
|
403
|
+
if _input_contains_prior_assistant_turn(payload)
|
|
404
|
+
else DERIVATION_OUTCOME_ANCHOR_NEW
|
|
405
|
+
)
|
|
406
|
+
return _PromptCacheAnchor(minted_key, outcome)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _derive_prompt_cache_key(
|
|
410
|
+
payload: ResponsesRequest | ResponsesCompactRequest,
|
|
411
|
+
api_key: ApiKeyData | None,
|
|
412
|
+
*,
|
|
413
|
+
max_age_seconds: int = _DEFAULT_ANCHOR_TTL_SECONDS,
|
|
414
|
+
) -> str | None:
|
|
415
|
+
"""Key attached to the payload and forwarded upstream, or ``None``.
|
|
416
|
+
|
|
417
|
+
``None`` means the turn is unanchorable and nothing is attached; see
|
|
418
|
+
``_PromptCacheAnchor``.
|
|
419
|
+
"""
|
|
420
|
+
|
|
421
|
+
return _derive_prompt_cache_anchor(payload, api_key, max_age_seconds=max_age_seconds).sticky_key
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _sticky_key_from_session_header(headers: Mapping[str, str]) -> str | None:
|
|
425
|
+
# Legacy owner/request-log callers still need the historical alias order.
|
|
426
|
+
# New account, bridge, and replay locality MUST use the typed process/thread
|
|
427
|
+
# helpers above; otherwise a shared process id silently hides thread-id.
|
|
428
|
+
return _process_session_key_from_headers(headers) or _thread_id_from_headers(headers)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _sticky_key_from_turn_state_header(headers: Mapping[str, str]) -> str | None:
|
|
432
|
+
normalized = {key.lower(): value for key, value in headers.items()}
|
|
433
|
+
value = normalized.get("x-codex-turn-state")
|
|
434
|
+
if not isinstance(value, str):
|
|
435
|
+
return None
|
|
436
|
+
stripped = value.strip()
|
|
437
|
+
return stripped or None
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _bare_codex_session_affinity(
|
|
441
|
+
headers: Mapping[str, str],
|
|
442
|
+
*,
|
|
443
|
+
enabled: bool,
|
|
444
|
+
allow_cap_spillover: bool,
|
|
445
|
+
) -> _AffinityPolicy | None:
|
|
446
|
+
if not enabled:
|
|
447
|
+
return None
|
|
448
|
+
session_key = _sticky_key_from_session_header(headers)
|
|
449
|
+
if session_key is None:
|
|
450
|
+
return None
|
|
451
|
+
return _AffinityPolicy(
|
|
452
|
+
key=session_key,
|
|
453
|
+
kind=StickySessionKind.CODEX_SESSION,
|
|
454
|
+
spill_on_account_cap=allow_cap_spillover,
|
|
455
|
+
codex_session_source="session_header",
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _thread_codex_session_affinity(
|
|
460
|
+
headers: Mapping[str, str],
|
|
461
|
+
*,
|
|
462
|
+
enabled: bool,
|
|
463
|
+
max_age_seconds: int,
|
|
464
|
+
thread_id: str | None = None,
|
|
465
|
+
) -> _AffinityPolicy | None:
|
|
466
|
+
if not enabled:
|
|
467
|
+
return None
|
|
468
|
+
identity = _codex_backend_identity(headers, thread_id=thread_id)
|
|
469
|
+
thread_key = identity.thread_selection_key
|
|
470
|
+
if thread_key is None:
|
|
471
|
+
return None
|
|
472
|
+
# Current Codex shares process session and prompt_cache_key across a root
|
|
473
|
+
# tree. Thread locality therefore reuses the bounded PROMPT_CACHE lifecycle
|
|
474
|
+
# but does not rewrite the upstream cache hint or create durable child rows.
|
|
475
|
+
legacy_key = identity.process_session or identity.thread_id
|
|
476
|
+
return _AffinityPolicy(
|
|
477
|
+
key=thread_key,
|
|
478
|
+
kind=StickySessionKind.PROMPT_CACHE,
|
|
479
|
+
max_age_seconds=max_age_seconds,
|
|
480
|
+
codex_session_source="thread_header",
|
|
481
|
+
legacy_codex_session_key=legacy_key,
|
|
482
|
+
legacy_continuity_source=("session_header" if identity.process_session is not None else "thread_header"),
|
|
483
|
+
seed_selection_key=(
|
|
484
|
+
_codex_session_selection_key(identity.process_session) if identity.process_session is not None else None
|
|
485
|
+
),
|
|
486
|
+
seed_selection_kind=(StickySessionKind.CODEX_SESSION if identity.process_session is not None else None),
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def _request_allows_bare_session_cap_spillover(
|
|
491
|
+
payload: ResponsesRequest | ResponsesCompactRequest,
|
|
492
|
+
) -> bool:
|
|
493
|
+
if isinstance(payload, ResponsesRequest):
|
|
494
|
+
previous_response_id = payload.previous_response_id
|
|
495
|
+
conversation = payload.conversation
|
|
496
|
+
else:
|
|
497
|
+
extra = payload.model_extra or {}
|
|
498
|
+
previous_response_id = extra.get("previous_response_id")
|
|
499
|
+
conversation = extra.get("conversation")
|
|
500
|
+
# A lookup miss does not make an upstream-stored object portable. Selection
|
|
501
|
+
# must remain fail-closed for every owner-bearing payload shape.
|
|
502
|
+
return not (
|
|
503
|
+
(previous_response_id is not None and not isinstance(previous_response_id, str))
|
|
504
|
+
or (isinstance(previous_response_id, str) and bool(previous_response_id.strip()))
|
|
505
|
+
or (conversation is not None and not isinstance(conversation, str))
|
|
506
|
+
or (isinstance(conversation, str) and bool(conversation.strip()))
|
|
507
|
+
or extract_input_file_ids(payload.input)
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _request_allows_unavailable_legacy_owner_abandonment(payload: ResponsesRequest) -> bool:
|
|
512
|
+
# Intent and safety are separate proofs. The internal goal marker says the
|
|
513
|
+
# client deliberately restarted, while the replay classifier proves that
|
|
514
|
+
# this particular body carries no account-scoped state. Never collapse this
|
|
515
|
+
# into a marker-only or missing-previous-response shortcut.
|
|
516
|
+
if not responses_request_contains_goal_continuation_context(payload):
|
|
517
|
+
return False
|
|
518
|
+
# Classify the same canonical body that subscription egress would send.
|
|
519
|
+
# Raw model dumps retain accepted compatibility-only controls, which are
|
|
520
|
+
# not account state and must not make equivalent request forms disagree.
|
|
521
|
+
replay_payload = dict(payload.to_replay_safety_payload())
|
|
522
|
+
# ``type=response.create`` belongs to the direct-WebSocket envelope, not
|
|
523
|
+
# the HTTP Responses body. Remove only that exact discriminator after
|
|
524
|
+
# canonicalization; unknown envelope values remain fail-closed below.
|
|
525
|
+
if replay_payload.get("type") == "response.create":
|
|
526
|
+
replay_payload.pop("type")
|
|
527
|
+
return responses_payload_is_account_neutral_fresh_replay(replay_payload)
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def _affinity_with_payload_continuity(
|
|
531
|
+
policy: _AffinityPolicy,
|
|
532
|
+
payload: ResponsesRequest | ResponsesCompactRequest,
|
|
533
|
+
) -> _AffinityPolicy:
|
|
534
|
+
if isinstance(payload, ResponsesRequest):
|
|
535
|
+
conversation = payload.conversation
|
|
536
|
+
else:
|
|
537
|
+
conversation = (payload.model_extra or {}).get("conversation")
|
|
538
|
+
if conversation is None or (isinstance(conversation, str) and not conversation.strip()):
|
|
539
|
+
return policy
|
|
540
|
+
return replace(policy, require_unambiguous_account=True)
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def _sticky_key_for_codex_control_request(
|
|
544
|
+
headers: Mapping[str, str],
|
|
545
|
+
*,
|
|
546
|
+
codex_session_affinity: bool,
|
|
547
|
+
) -> _AffinityPolicy:
|
|
548
|
+
turn_state_key = _sticky_key_from_turn_state_header(headers)
|
|
549
|
+
if turn_state_key:
|
|
550
|
+
return _AffinityPolicy(
|
|
551
|
+
key=turn_state_key,
|
|
552
|
+
kind=StickySessionKind.CODEX_SESSION,
|
|
553
|
+
codex_session_source="turn_state",
|
|
554
|
+
)
|
|
555
|
+
session_affinity = _bare_codex_session_affinity(
|
|
556
|
+
headers,
|
|
557
|
+
enabled=codex_session_affinity,
|
|
558
|
+
allow_cap_spillover=False,
|
|
559
|
+
)
|
|
560
|
+
if session_affinity is not None:
|
|
561
|
+
return session_affinity
|
|
562
|
+
return _AffinityPolicy()
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def _sticky_key_for_thread_goal_request(
|
|
566
|
+
payload: Mapping[str, object],
|
|
567
|
+
headers: Mapping[str, str],
|
|
568
|
+
codex_session_affinity: bool,
|
|
569
|
+
max_age_seconds: int,
|
|
570
|
+
) -> _AffinityPolicy:
|
|
571
|
+
turn_state_key = _sticky_key_from_turn_state_header(headers)
|
|
572
|
+
if turn_state_key is not None:
|
|
573
|
+
return _AffinityPolicy(
|
|
574
|
+
key=turn_state_key,
|
|
575
|
+
kind=StickySessionKind.CODEX_SESSION,
|
|
576
|
+
codex_session_source="turn_state",
|
|
577
|
+
)
|
|
578
|
+
payload_thread_id = payload.get("threadId")
|
|
579
|
+
if isinstance(payload_thread_id, str) and payload_thread_id.strip():
|
|
580
|
+
thread_affinity = _thread_codex_session_affinity(
|
|
581
|
+
headers,
|
|
582
|
+
enabled=codex_session_affinity,
|
|
583
|
+
max_age_seconds=max_age_seconds,
|
|
584
|
+
thread_id=payload_thread_id,
|
|
585
|
+
)
|
|
586
|
+
if thread_affinity is not None:
|
|
587
|
+
return thread_affinity
|
|
588
|
+
# Routing only consumes a valid nonblank identity. The upstream thread-goal
|
|
589
|
+
# protocol remains authoritative for payload validation and error shape.
|
|
590
|
+
return _sticky_key_for_codex_control_request(
|
|
591
|
+
headers,
|
|
592
|
+
codex_session_affinity=codex_session_affinity,
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def _owner_lookup_session_id_from_headers(
|
|
597
|
+
headers: Mapping[str, str],
|
|
598
|
+
*,
|
|
599
|
+
synthesized_turn_state: str | None = None,
|
|
600
|
+
) -> str | None:
|
|
601
|
+
# `x-codex-turn-state` is per conversation turn/thread and is more specific
|
|
602
|
+
# than `session_id`, which may be shared across multiple terminals. A turn
|
|
603
|
+
# state generated for the current downstream connection is only an
|
|
604
|
+
# upstream-forwarding placeholder, however; it must not hide a durable
|
|
605
|
+
# client session on reconnect.
|
|
606
|
+
turn_state = _sticky_key_from_turn_state_header(headers)
|
|
607
|
+
if turn_state is not None and turn_state != synthesized_turn_state:
|
|
608
|
+
return turn_state
|
|
609
|
+
return _sticky_key_from_session_header(headers)
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def _websocket_continuity_key_from_headers(
|
|
613
|
+
headers: Mapping[str, str],
|
|
614
|
+
*,
|
|
615
|
+
synthesized_turn_state: str | None = None,
|
|
616
|
+
) -> str | None:
|
|
617
|
+
"""Return the primary count-bounded direct-WebSocket continuity key."""
|
|
618
|
+
|
|
619
|
+
explicit_turn_state = _sticky_key_from_turn_state_header(headers)
|
|
620
|
+
if explicit_turn_state is not None and explicit_turn_state != synthesized_turn_state:
|
|
621
|
+
# Exact client continuation must outrank broader thread locality. A
|
|
622
|
+
# synthesized handshake placeholder is only an alias for the current
|
|
623
|
+
# connection and therefore does not gain this hard precedence.
|
|
624
|
+
return explicit_turn_state
|
|
625
|
+
identity = _codex_backend_identity(headers)
|
|
626
|
+
if identity.thread_selection_key is not None:
|
|
627
|
+
return identity.thread_selection_key
|
|
628
|
+
return _owner_lookup_session_id_from_headers(
|
|
629
|
+
headers,
|
|
630
|
+
synthesized_turn_state=synthesized_turn_state,
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def _websocket_continuity_aliases_from_headers(
|
|
635
|
+
headers: Mapping[str, str],
|
|
636
|
+
*,
|
|
637
|
+
synthesized_turn_state: str | None = None,
|
|
638
|
+
) -> tuple[str, ...]:
|
|
639
|
+
"""Keep exact turn aliases without restoring process-wide thread state."""
|
|
640
|
+
|
|
641
|
+
aliases: list[str] = []
|
|
642
|
+
primary = _websocket_continuity_key_from_headers(
|
|
643
|
+
headers,
|
|
644
|
+
synthesized_turn_state=synthesized_turn_state,
|
|
645
|
+
)
|
|
646
|
+
if primary is not None:
|
|
647
|
+
aliases.append(primary)
|
|
648
|
+
thread_key = _codex_backend_identity(headers).thread_selection_key
|
|
649
|
+
if thread_key is not None:
|
|
650
|
+
# When an exact turn resolved first, refresh the thread alias to that
|
|
651
|
+
# same state so a later unanchored reconnect remains thread-local.
|
|
652
|
+
aliases.append(thread_key)
|
|
653
|
+
explicit_turn_state = _sticky_key_from_turn_state_header(headers)
|
|
654
|
+
if explicit_turn_state is not None and explicit_turn_state != synthesized_turn_state:
|
|
655
|
+
aliases.append(explicit_turn_state)
|
|
656
|
+
if synthesized_turn_state is not None:
|
|
657
|
+
aliases.append(synthesized_turn_state)
|
|
658
|
+
return tuple(dict.fromkeys(aliases))
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
# Pattern matching turn-state values synthesized by the helpers below.
|
|
662
|
+
# A 32-char lowercase hex (uuid4().hex) suffix follows the prefix.
|
|
663
|
+
_SYNTHESIZED_TURN_STATE_PATTERN = re.compile(r"^(?:http_)?turn_[0-9a-f]{32}$")
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def _is_synthesized_turn_state(value: str) -> bool:
|
|
667
|
+
"""True when ``value`` matches a turn-state synthesized by codex-lb itself.
|
|
668
|
+
|
|
669
|
+
Used by the file-pin resolver to distinguish a client-supplied
|
|
670
|
+
continuation marker from a synthesizer-generated placeholder so
|
|
671
|
+
first-turn upload-then-converse requests still benefit from
|
|
672
|
+
file_id pin routing on the websocket / HTTP entry points.
|
|
673
|
+
"""
|
|
674
|
+
return bool(_SYNTHESIZED_TURN_STATE_PATTERN.match(value))
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def ensure_downstream_turn_state(headers: Mapping[str, str]) -> str:
|
|
678
|
+
existing = _sticky_key_from_turn_state_header(headers)
|
|
679
|
+
if existing is not None:
|
|
680
|
+
return existing
|
|
681
|
+
return f"turn_{uuid4().hex}"
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def ensure_http_downstream_turn_state(headers: Mapping[str, str]) -> str:
|
|
685
|
+
existing = _sticky_key_from_turn_state_header(headers)
|
|
686
|
+
if existing is not None:
|
|
687
|
+
return existing
|
|
688
|
+
return f"http_turn_{uuid4().hex}"
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def build_downstream_turn_state_accept_headers(turn_state: str) -> list[tuple[bytes, bytes]]:
|
|
692
|
+
return [(b"x-codex-turn-state", turn_state.encode("utf-8"))]
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def build_downstream_turn_state_response_headers(turn_state: str) -> dict[str, str]:
|
|
696
|
+
return {"x-codex-turn-state": turn_state}
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
@dataclass(frozen=True, slots=True)
|
|
700
|
+
class _PromptCacheResolution:
|
|
701
|
+
"""Routing key plus the provenance operators need to read a shape log."""
|
|
702
|
+
|
|
703
|
+
sticky_key: str | None
|
|
704
|
+
source: str
|
|
705
|
+
outcome: str
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
def _resolve_prompt_cache_key(
|
|
709
|
+
payload: ResponsesRequest | ResponsesCompactRequest,
|
|
710
|
+
*,
|
|
711
|
+
openai_cache_affinity: bool,
|
|
712
|
+
api_key: ApiKeyData | None,
|
|
713
|
+
max_age_seconds: int = _DEFAULT_ANCHOR_TTL_SECONDS,
|
|
714
|
+
) -> _PromptCacheResolution:
|
|
715
|
+
cache_key = _prompt_cache_key_from_request_model(payload)
|
|
716
|
+
if isinstance(cache_key, str):
|
|
717
|
+
stripped = cache_key.strip()
|
|
718
|
+
if stripped:
|
|
719
|
+
if stripped != cache_key:
|
|
720
|
+
payload.prompt_cache_key = stripped
|
|
721
|
+
return _record_prompt_cache_resolution(
|
|
722
|
+
_PromptCacheResolution(stripped, "payload", DERIVATION_OUTCOME_PAYLOAD)
|
|
723
|
+
)
|
|
724
|
+
if not openai_cache_affinity:
|
|
725
|
+
return _record_prompt_cache_resolution(_PromptCacheResolution(None, "none", DERIVATION_OUTCOME_DISABLED))
|
|
726
|
+
anchor = _derive_prompt_cache_anchor(payload, api_key, max_age_seconds=max_age_seconds)
|
|
727
|
+
if anchor.sticky_key is None:
|
|
728
|
+
# Unanchorable: attach nothing. A value written here is indistinguishable
|
|
729
|
+
# from a client-supplied one when this same payload object is resolved
|
|
730
|
+
# again (bridge -> `_stream_with_retry` fallback), which would promote a
|
|
731
|
+
# constant per-API-key string into a real sticky key. There is also
|
|
732
|
+
# nothing for upstream to cache: the body carried no digestible
|
|
733
|
+
# transcript. `http_continuation_signal` falls through to
|
|
734
|
+
# `http_history_signal`, which still classifies a real transcript.
|
|
735
|
+
return _record_prompt_cache_resolution(_PromptCacheResolution(None, "none", anchor.outcome))
|
|
736
|
+
# Attached so that a re-resolution of this same body -- the bridge fallback,
|
|
737
|
+
# or another replica after ring forwarding -- reuses the identical key
|
|
738
|
+
# through the client-supplied branch instead of re-deriving it.
|
|
739
|
+
payload.prompt_cache_key = anchor.sticky_key
|
|
740
|
+
return _record_prompt_cache_resolution(_PromptCacheResolution(anchor.sticky_key, "derived", anchor.outcome))
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
def _record_prompt_cache_resolution(resolution: _PromptCacheResolution) -> _PromptCacheResolution:
|
|
744
|
+
if PROMETHEUS_AVAILABLE and prompt_cache_key_derivation_total is not None:
|
|
745
|
+
prompt_cache_key_derivation_total.labels(outcome=resolution.outcome).inc()
|
|
746
|
+
return resolution
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
def _sticky_key_for_responses_request(
|
|
750
|
+
payload: ResponsesRequest,
|
|
751
|
+
headers: Mapping[str, str],
|
|
752
|
+
*,
|
|
753
|
+
codex_session_affinity: bool,
|
|
754
|
+
openai_cache_affinity: bool,
|
|
755
|
+
openai_cache_affinity_max_age_seconds: int,
|
|
756
|
+
sticky_threads_enabled: bool,
|
|
757
|
+
api_key: ApiKeyData | None = None,
|
|
758
|
+
synthesized_turn_state: str | None = None,
|
|
759
|
+
) -> _AffinityPolicy:
|
|
760
|
+
# This helper only classifies locality keys. Stored-object continuity such
|
|
761
|
+
# as `previous_response_id` is resolved later by ProxyService and must stay
|
|
762
|
+
# hard owner-bound even if this returns a prompt-cache affinity policy.
|
|
763
|
+
resolution = _resolve_prompt_cache_key(
|
|
764
|
+
payload,
|
|
765
|
+
openai_cache_affinity=openai_cache_affinity,
|
|
766
|
+
api_key=api_key,
|
|
767
|
+
max_age_seconds=openai_cache_affinity_max_age_seconds,
|
|
768
|
+
)
|
|
769
|
+
cache_key = resolution.sticky_key
|
|
770
|
+
cache_key_source = resolution.source
|
|
771
|
+
turn_state_key = _sticky_key_from_turn_state_header(headers)
|
|
772
|
+
if turn_state_key and turn_state_key != synthesized_turn_state:
|
|
773
|
+
policy = _AffinityPolicy(
|
|
774
|
+
key=turn_state_key,
|
|
775
|
+
kind=StickySessionKind.CODEX_SESSION,
|
|
776
|
+
codex_session_source="turn_state",
|
|
777
|
+
)
|
|
778
|
+
elif (
|
|
779
|
+
thread_affinity := _thread_codex_session_affinity(
|
|
780
|
+
headers,
|
|
781
|
+
enabled=codex_session_affinity,
|
|
782
|
+
max_age_seconds=openai_cache_affinity_max_age_seconds,
|
|
783
|
+
)
|
|
784
|
+
) is not None:
|
|
785
|
+
policy = thread_affinity
|
|
786
|
+
elif (
|
|
787
|
+
session_affinity := _bare_codex_session_affinity(
|
|
788
|
+
headers,
|
|
789
|
+
enabled=codex_session_affinity,
|
|
790
|
+
allow_cap_spillover=_request_allows_bare_session_cap_spillover(payload),
|
|
791
|
+
)
|
|
792
|
+
) is not None:
|
|
793
|
+
policy = session_affinity
|
|
794
|
+
elif openai_cache_affinity:
|
|
795
|
+
policy = _AffinityPolicy(
|
|
796
|
+
key=cache_key,
|
|
797
|
+
kind=StickySessionKind.PROMPT_CACHE,
|
|
798
|
+
max_age_seconds=openai_cache_affinity_max_age_seconds,
|
|
799
|
+
prompt_cache_key_source=cache_key_source,
|
|
800
|
+
)
|
|
801
|
+
elif sticky_threads_enabled:
|
|
802
|
+
policy = _AffinityPolicy(
|
|
803
|
+
key=cache_key,
|
|
804
|
+
kind=StickySessionKind.STICKY_THREAD,
|
|
805
|
+
reallocate_sticky=True,
|
|
806
|
+
prompt_cache_key_source=cache_key_source,
|
|
807
|
+
)
|
|
808
|
+
elif turn_state_key is not None and turn_state_key == synthesized_turn_state:
|
|
809
|
+
policy = _AffinityPolicy(
|
|
810
|
+
key=turn_state_key,
|
|
811
|
+
kind=StickySessionKind.CODEX_SESSION,
|
|
812
|
+
codex_session_source="turn_state",
|
|
813
|
+
)
|
|
814
|
+
else:
|
|
815
|
+
policy = _AffinityPolicy()
|
|
816
|
+
if (
|
|
817
|
+
# The raw row this escape hatch retires is the process-session key.
|
|
818
|
+
# Current Codex also sends thread-id, so locality source is often
|
|
819
|
+
# thread_header; that must not hide the process-session exception.
|
|
820
|
+
# An explicit turn-state header stays hard even with the same marker.
|
|
821
|
+
policy.codex_session_source in {"session_header", "thread_header"}
|
|
822
|
+
and (
|
|
823
|
+
policy.codex_session_source == "session_header"
|
|
824
|
+
or _codex_backend_identity(headers).process_session is not None
|
|
825
|
+
)
|
|
826
|
+
and _request_allows_unavailable_legacy_owner_abandonment(payload)
|
|
827
|
+
):
|
|
828
|
+
policy = replace(policy, abandon_unavailable_legacy_owner=True)
|
|
829
|
+
policy = replace(policy, prompt_cache_derivation_outcome=resolution.outcome)
|
|
830
|
+
return _affinity_with_payload_continuity(policy, payload)
|