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,797 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import hashlib
|
|
5
|
+
import hmac
|
|
6
|
+
import json
|
|
7
|
+
from collections.abc import AsyncIterator, Callable, Mapping
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import cast
|
|
10
|
+
|
|
11
|
+
import aiohttp
|
|
12
|
+
|
|
13
|
+
from app.core.clients.proxy import ProxyResponseError, filter_inbound_headers
|
|
14
|
+
from app.core.clock import REAL_CLOCK, REAL_SCHEDULER, Clock, Scheduler
|
|
15
|
+
from app.core.config.dashboard_overrides import with_dashboard_overrides
|
|
16
|
+
from app.core.config.settings import get_settings
|
|
17
|
+
from app.core.crypto import get_or_create_key
|
|
18
|
+
from app.core.errors import OpenAIErrorEnvelope, openai_error, response_failed_event
|
|
19
|
+
from app.core.openai.requests import ResponsesRequest, extract_input_file_ids
|
|
20
|
+
from app.core.types import JsonObject
|
|
21
|
+
from app.core.utils.json_guards import is_json_mapping
|
|
22
|
+
from app.core.utils.request_id import get_request_id
|
|
23
|
+
from app.core.utils.sse import format_sse_event
|
|
24
|
+
from app.modules.api_keys.service import ApiKeyUsageReservationData
|
|
25
|
+
from app.modules.proxy._service.http_bridge.helpers import _http_bridge_request_budget_seconds
|
|
26
|
+
|
|
27
|
+
# HTTP-only and hop-by-hop headers that must not be forwarded through the
|
|
28
|
+
# internal bridge. These headers are either illegal in WebSocket handshakes or
|
|
29
|
+
# carry HTTP framing semantics that the aiohttp upstream session manages itself.
|
|
30
|
+
# Applies on top of filter_inbound_headers (which already strips authorization,
|
|
31
|
+
# host, content-length, and x-forwarded-* / cf-* headers).
|
|
32
|
+
_BRIDGE_UNSAFE_HEADER_NAMES = frozenset(
|
|
33
|
+
{
|
|
34
|
+
"accept",
|
|
35
|
+
"accept-encoding",
|
|
36
|
+
"connection",
|
|
37
|
+
"content-type",
|
|
38
|
+
"cookie",
|
|
39
|
+
"keep-alive",
|
|
40
|
+
"te",
|
|
41
|
+
"trailer",
|
|
42
|
+
"transfer-encoding",
|
|
43
|
+
"upgrade",
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
_OWNER_FORWARD_SKIP_AUTO_HEADERS = frozenset({aiohttp.hdrs.ACCEPT, aiohttp.hdrs.ACCEPT_ENCODING})
|
|
47
|
+
_LEGACY_SIGNATURE_DELIMITER = "|"
|
|
48
|
+
|
|
49
|
+
HTTP_BRIDGE_INTERNAL_FORWARD_PATH = "/internal/bridge/responses"
|
|
50
|
+
HTTP_BRIDGE_FORWARDED_HEADER = "x-codex-bridge-forwarded"
|
|
51
|
+
HTTP_BRIDGE_ORIGIN_INSTANCE_HEADER = "x-codex-bridge-origin-instance"
|
|
52
|
+
HTTP_BRIDGE_TARGET_INSTANCE_HEADER = "x-codex-bridge-target-instance"
|
|
53
|
+
HTTP_BRIDGE_CODEX_AFFINITY_HEADER = "x-codex-bridge-codex-session-affinity"
|
|
54
|
+
HTTP_BRIDGE_RESERVATION_ID_HEADER = "x-codex-bridge-reservation-id"
|
|
55
|
+
HTTP_BRIDGE_RESERVATION_KEY_ID_HEADER = "x-codex-bridge-reservation-key-id"
|
|
56
|
+
HTTP_BRIDGE_RESERVATION_MODEL_HEADER = "x-codex-bridge-reservation-model"
|
|
57
|
+
HTTP_BRIDGE_AFFINITY_KIND_HEADER = "x-codex-bridge-affinity-kind"
|
|
58
|
+
HTTP_BRIDGE_AFFINITY_KEY_HEADER = "x-codex-bridge-affinity-key"
|
|
59
|
+
HTTP_BRIDGE_FILE_OWNER_HEADER = "x-codex-bridge-file-owner"
|
|
60
|
+
HTTP_BRIDGE_ORIGINAL_UNANCHORED_HEADER = "x-codex-bridge-original-unanchored"
|
|
61
|
+
HTTP_BRIDGE_SIGNATURE_VERSION_HEADER = "x-codex-bridge-signature-version"
|
|
62
|
+
HTTP_BRIDGE_CLIENT_IP_HEADER = "x-codex-bridge-client-ip"
|
|
63
|
+
HTTP_BRIDGE_CLIENT_IP_SIGNATURE_HEADER = "x-codex-bridge-client-ip-signature"
|
|
64
|
+
HTTP_BRIDGE_SIGNATURE_HEADER = "x-codex-bridge-signature"
|
|
65
|
+
# Additive tamper-proofing header (#1203): a second signature bound to the
|
|
66
|
+
# exact forwarding body (``model_dump_for_forwarding``) that is posted, so an
|
|
67
|
+
# in-transit rewrite injecting ``"tools": []`` is detected even though the
|
|
68
|
+
# primary signature hashes a plain ``model_dump`` that synthesizes the same
|
|
69
|
+
# empty list. Orthogonal to ``x-codex-bridge-signature-version`` below (which
|
|
70
|
+
# domain-separates the *primary* signature for unanchored parallel requests,
|
|
71
|
+
# #1169): this header carries its own full-context structured signature. Kept
|
|
72
|
+
# as a one-release rolling-upgrade shim alongside the legacy primary
|
|
73
|
+
# signature; see the ROLLOUT SHIM notes in ``build_owner_forward_headers`` and
|
|
74
|
+
# ``parse_forwarded_request``.
|
|
75
|
+
HTTP_BRIDGE_SIGNATURE_V2_HEADER = "x-codex-bridge-signature-v2"
|
|
76
|
+
_HTTP_BRIDGE_SIGNATURE_VERSION_V2 = "2"
|
|
77
|
+
# Response header (owner -> origin) carrying ``ProxyResponseError``
|
|
78
|
+
# provenance back across the forward hop. The owner's error body is an
|
|
79
|
+
# ordinary OpenAI envelope, so a refusal the owner raised before it sent any
|
|
80
|
+
# upstream frame is indistinguishable by code from a transport failure it
|
|
81
|
+
# observed: both end as ``stream_incomplete``. Without this marker the origin
|
|
82
|
+
# rebuilds the error without the provenance and its own native Codex
|
|
83
|
+
# transport-failure lifecycle aborts the committed body, moving issue #2364's
|
|
84
|
+
# empty 200 from the owner onto the origin.
|
|
85
|
+
HTTP_BRIDGE_LOCAL_PRE_DISPATCH_REFUSAL_HEADER = "x-codex-bridge-local-pre-dispatch-refusal"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(frozen=True, slots=True)
|
|
89
|
+
class HTTPBridgeForwardContext:
|
|
90
|
+
origin_instance: str
|
|
91
|
+
target_instance: str
|
|
92
|
+
codex_session_affinity: bool
|
|
93
|
+
downstream_turn_state: str | None
|
|
94
|
+
original_request_unanchored: bool = False
|
|
95
|
+
original_affinity_kind: str | None = None
|
|
96
|
+
original_affinity_key: str | None = None
|
|
97
|
+
file_owner_account_id: str | None = None
|
|
98
|
+
client_ip: str | None = None
|
|
99
|
+
reservation: ApiKeyUsageReservationData | None = None
|
|
100
|
+
signature_version: str | None = None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass(frozen=True, slots=True)
|
|
104
|
+
class HTTPBridgeForwardedRequest:
|
|
105
|
+
context: HTTPBridgeForwardContext
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass(frozen=True, slots=True)
|
|
109
|
+
class _OwnerForwardReceiveTimeout:
|
|
110
|
+
timeout_seconds: float
|
|
111
|
+
error_code: str
|
|
112
|
+
error_message: str
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class _OwnerForwardStreamTimeoutError(Exception):
|
|
116
|
+
def __init__(self, *, error_code: str, error_message: str) -> None:
|
|
117
|
+
super().__init__(error_message)
|
|
118
|
+
self.error_code = error_code
|
|
119
|
+
self.error_message = error_message
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass(frozen=True, slots=True)
|
|
123
|
+
class OwnerForwardRelayFailure(Exception):
|
|
124
|
+
event_block: str
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _bridge_header_name_has_illegal_control_char(name: str) -> bool:
|
|
128
|
+
return any(ord(char) < 32 or ord(char) == 127 for char in name)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _bridge_header_value_has_illegal_control_char(value: str) -> bool:
|
|
132
|
+
return any((ord(char) < 32 and char != "\t") or ord(char) == 127 for char in value)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _bridge_header_has_illegal_control_char(name: str, value: str) -> bool:
|
|
136
|
+
"""Return whether reconstructed metadata is unsafe as an HTTP header."""
|
|
137
|
+
|
|
138
|
+
return _bridge_header_name_has_illegal_control_char(name) or _bridge_header_value_has_illegal_control_char(value)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _reject_illegal_bridge_header_value(name: str, value: str | None) -> None:
|
|
142
|
+
if value is None or not _bridge_header_has_illegal_control_char(name, value):
|
|
143
|
+
return
|
|
144
|
+
raise ProxyResponseError(
|
|
145
|
+
400,
|
|
146
|
+
openai_error(
|
|
147
|
+
"bridge_forward_invalid",
|
|
148
|
+
"Internal bridge forward metadata is not safe to forward",
|
|
149
|
+
error_type="invalid_request_error",
|
|
150
|
+
),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _validate_bridge_forward_context_headers(context: HTTPBridgeForwardContext) -> None:
|
|
155
|
+
for name, value in (
|
|
156
|
+
(HTTP_BRIDGE_ORIGIN_INSTANCE_HEADER, context.origin_instance),
|
|
157
|
+
(HTTP_BRIDGE_TARGET_INSTANCE_HEADER, context.target_instance),
|
|
158
|
+
("x-codex-turn-state", context.downstream_turn_state),
|
|
159
|
+
(HTTP_BRIDGE_AFFINITY_KIND_HEADER, context.original_affinity_kind),
|
|
160
|
+
(HTTP_BRIDGE_AFFINITY_KEY_HEADER, context.original_affinity_key),
|
|
161
|
+
(HTTP_BRIDGE_FILE_OWNER_HEADER, context.file_owner_account_id),
|
|
162
|
+
(HTTP_BRIDGE_CLIENT_IP_HEADER, context.client_ip),
|
|
163
|
+
):
|
|
164
|
+
_reject_illegal_bridge_header_value(name, value)
|
|
165
|
+
if context.reservation is None:
|
|
166
|
+
return
|
|
167
|
+
for name, value in (
|
|
168
|
+
(HTTP_BRIDGE_RESERVATION_ID_HEADER, context.reservation.reservation_id),
|
|
169
|
+
(HTTP_BRIDGE_RESERVATION_KEY_ID_HEADER, context.reservation.key_id),
|
|
170
|
+
(HTTP_BRIDGE_RESERVATION_MODEL_HEADER, context.reservation.model),
|
|
171
|
+
):
|
|
172
|
+
_reject_illegal_bridge_header_value(name, value)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class HTTPBridgeOwnerClient:
|
|
176
|
+
async def stream_responses(
|
|
177
|
+
self,
|
|
178
|
+
*,
|
|
179
|
+
owner_endpoint: str,
|
|
180
|
+
payload: ResponsesRequest,
|
|
181
|
+
headers: Mapping[str, str],
|
|
182
|
+
context: HTTPBridgeForwardContext,
|
|
183
|
+
request_started_at: float,
|
|
184
|
+
on_request_dispatched: Callable[[], None] | None = None,
|
|
185
|
+
on_response_rejected: Callable[[], None] | None = None,
|
|
186
|
+
on_response_wait: Callable[[], None] | None = None,
|
|
187
|
+
on_response_ready: Callable[[], None] | None = None,
|
|
188
|
+
scheduler: Scheduler = REAL_SCHEDULER,
|
|
189
|
+
clock: Clock = REAL_CLOCK,
|
|
190
|
+
) -> AsyncIterator[str]:
|
|
191
|
+
settings = with_dashboard_overrides(get_settings())
|
|
192
|
+
timeout = _owner_forward_timeout(
|
|
193
|
+
connect_timeout_seconds=settings.upstream_connect_timeout_seconds,
|
|
194
|
+
idle_timeout_seconds=settings.stream_idle_timeout_seconds,
|
|
195
|
+
)
|
|
196
|
+
if on_response_wait is not None:
|
|
197
|
+
on_response_wait()
|
|
198
|
+
async with aiohttp.ClientSession(timeout=timeout, trust_env=False) as session:
|
|
199
|
+
request_url = f"{owner_endpoint}{HTTP_BRIDGE_INTERNAL_FORWARD_PATH}"
|
|
200
|
+
request_payload = payload.model_dump_for_forwarding()
|
|
201
|
+
request_headers = build_owner_forward_headers(headers=headers, payload=payload, context=context)
|
|
202
|
+
request_context = session.post(
|
|
203
|
+
request_url,
|
|
204
|
+
json=request_payload,
|
|
205
|
+
headers=request_headers,
|
|
206
|
+
skip_auto_headers=_OWNER_FORWARD_SKIP_AUTO_HEADERS,
|
|
207
|
+
)
|
|
208
|
+
# I/O begins when __aenter__ is awaited. Cancellation after that
|
|
209
|
+
# point can leave the receiver settling the reservation.
|
|
210
|
+
transport_started = False
|
|
211
|
+
observed_status = False
|
|
212
|
+
try:
|
|
213
|
+
transport_started = True
|
|
214
|
+
async with request_context as response:
|
|
215
|
+
observed_status = True
|
|
216
|
+
if response.status != 200:
|
|
217
|
+
if on_response_rejected is not None:
|
|
218
|
+
# The receiver contract never transfers cleanup on a
|
|
219
|
+
# non-200 response, so the origin may safely release.
|
|
220
|
+
on_response_rejected()
|
|
221
|
+
payload_text = await response.text()
|
|
222
|
+
raise ProxyResponseError(
|
|
223
|
+
response.status,
|
|
224
|
+
_owner_forward_error_payload(status_code=response.status, payload_text=payload_text),
|
|
225
|
+
failure_phase="owner_forward_status",
|
|
226
|
+
failure_detail="owner_forward_non_200",
|
|
227
|
+
upstream_status_code=response.status,
|
|
228
|
+
# Rebuilding the error from the owner's body alone
|
|
229
|
+
# would drop the provenance the owner attached, so
|
|
230
|
+
# carry it over the hop: a non-200 by itself does
|
|
231
|
+
# not prove the owner sent no upstream frame, and
|
|
232
|
+
# only the owner knows which of its failures was a
|
|
233
|
+
# local pre-dispatch refusal (issue #2364).
|
|
234
|
+
local_pre_dispatch_refusal=_bool_header(
|
|
235
|
+
response.headers.get(HTTP_BRIDGE_LOCAL_PRE_DISPATCH_REFUSAL_HEADER)
|
|
236
|
+
),
|
|
237
|
+
)
|
|
238
|
+
if on_response_ready is not None:
|
|
239
|
+
on_response_ready()
|
|
240
|
+
yielded_event = False
|
|
241
|
+
try:
|
|
242
|
+
async for event_block in _iter_sse_event_blocks(
|
|
243
|
+
response,
|
|
244
|
+
request_started_at=request_started_at,
|
|
245
|
+
proxy_request_budget_seconds=_http_bridge_request_budget_seconds(settings),
|
|
246
|
+
stream_idle_timeout_seconds=settings.stream_idle_timeout_seconds,
|
|
247
|
+
scheduler=scheduler,
|
|
248
|
+
clock=clock,
|
|
249
|
+
):
|
|
250
|
+
yielded_event = True
|
|
251
|
+
yield event_block
|
|
252
|
+
except _OwnerForwardStreamTimeoutError as exc:
|
|
253
|
+
raise OwnerForwardRelayFailure(
|
|
254
|
+
format_sse_event(
|
|
255
|
+
response_failed_event(
|
|
256
|
+
exc.error_code,
|
|
257
|
+
exc.error_message,
|
|
258
|
+
response_id=get_request_id(),
|
|
259
|
+
)
|
|
260
|
+
)
|
|
261
|
+
)
|
|
262
|
+
if not yielded_event:
|
|
263
|
+
yield format_sse_event(
|
|
264
|
+
response_failed_event(
|
|
265
|
+
"stream_incomplete",
|
|
266
|
+
"Upstream websocket closed before response.completed",
|
|
267
|
+
response_id=get_request_id(),
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
except aiohttp.ClientConnectorError:
|
|
271
|
+
# DNS/connect refusal never delivered the reservation.
|
|
272
|
+
raise
|
|
273
|
+
except asyncio.CancelledError:
|
|
274
|
+
if transport_started and not observed_status and on_request_dispatched is not None:
|
|
275
|
+
on_request_dispatched()
|
|
276
|
+
raise
|
|
277
|
+
except (aiohttp.ClientError, asyncio.TimeoutError):
|
|
278
|
+
if not observed_status and on_request_dispatched is not None:
|
|
279
|
+
# The request left local construction and may have reached
|
|
280
|
+
# the owner; origin must not release or replay.
|
|
281
|
+
on_request_dispatched()
|
|
282
|
+
raise
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def build_owner_forward_headers(
|
|
286
|
+
*,
|
|
287
|
+
headers: Mapping[str, str],
|
|
288
|
+
payload: ResponsesRequest,
|
|
289
|
+
context: HTTPBridgeForwardContext,
|
|
290
|
+
) -> dict[str, str]:
|
|
291
|
+
# WebSocket client metadata is reconstructed as a header mapping without
|
|
292
|
+
# passing through an HTTP parser. Validate it before signing so aiohttp is
|
|
293
|
+
# never the first component to discover illegal wire bytes.
|
|
294
|
+
_validate_bridge_forward_context_headers(context)
|
|
295
|
+
filtered = filter_inbound_headers(headers)
|
|
296
|
+
# Per the hop-by-hop contract, also drop any header named by the inbound
|
|
297
|
+
# Connection header in addition to the fixed unsafe set.
|
|
298
|
+
connection_value = next(
|
|
299
|
+
(value for key, value in headers.items() if key.lower() == "connection"),
|
|
300
|
+
"",
|
|
301
|
+
)
|
|
302
|
+
connection_named = {token.strip().lower() for token in connection_value.split(",") if token.strip()}
|
|
303
|
+
drop = _BRIDGE_UNSAFE_HEADER_NAMES | connection_named
|
|
304
|
+
# Drop any client-supplied ``x-codex-bridge-*`` header: those names are
|
|
305
|
+
# reserved for the internal forward contract and are set below from the
|
|
306
|
+
# trusted context. Relaying unknown bridge headers verbatim would let an
|
|
307
|
+
# external client plant a spoofed ``x-codex-bridge-signature-v2`` (or any
|
|
308
|
+
# other bridge header) on an honestly signed forward; upgraded origins
|
|
309
|
+
# must never relay externally injected bridge headers.
|
|
310
|
+
forwarded = {
|
|
311
|
+
key: value
|
|
312
|
+
for key, value in filtered.items()
|
|
313
|
+
if key.lower() not in drop
|
|
314
|
+
and not key.lower().startswith("x-codex-bridge-")
|
|
315
|
+
and not _bridge_header_has_illegal_control_char(key, value)
|
|
316
|
+
}
|
|
317
|
+
# filter_inbound_headers strips Authorization, but the owner instance
|
|
318
|
+
# re-validates the client API key from this header (see
|
|
319
|
+
# _validate_internal_bridge_api_key) before swapping in its own upstream
|
|
320
|
+
# access token. Preserve it so api_key_auth_enabled deployments still
|
|
321
|
+
# authenticate forwarded bridge requests.
|
|
322
|
+
authorization = next(
|
|
323
|
+
(value for key, value in headers.items() if key.lower() == "authorization"),
|
|
324
|
+
None,
|
|
325
|
+
)
|
|
326
|
+
if authorization is not None and not _bridge_header_has_illegal_control_char("authorization", authorization):
|
|
327
|
+
forwarded["authorization"] = authorization
|
|
328
|
+
forwarded[HTTP_BRIDGE_FORWARDED_HEADER] = "1"
|
|
329
|
+
forwarded[HTTP_BRIDGE_ORIGIN_INSTANCE_HEADER] = context.origin_instance
|
|
330
|
+
forwarded[HTTP_BRIDGE_TARGET_INSTANCE_HEADER] = context.target_instance
|
|
331
|
+
forwarded[HTTP_BRIDGE_CODEX_AFFINITY_HEADER] = "1" if context.codex_session_affinity else "0"
|
|
332
|
+
signature_version = _HTTP_BRIDGE_SIGNATURE_VERSION_V2 if context.original_request_unanchored else None
|
|
333
|
+
if signature_version is not None:
|
|
334
|
+
forwarded[HTTP_BRIDGE_SIGNATURE_VERSION_HEADER] = signature_version
|
|
335
|
+
forwarded[HTTP_BRIDGE_ORIGINAL_UNANCHORED_HEADER] = "1"
|
|
336
|
+
if context.original_affinity_kind and context.original_affinity_key:
|
|
337
|
+
forwarded[HTTP_BRIDGE_AFFINITY_KIND_HEADER] = context.original_affinity_kind
|
|
338
|
+
forwarded[HTTP_BRIDGE_AFFINITY_KEY_HEADER] = context.original_affinity_key
|
|
339
|
+
if context.file_owner_account_id:
|
|
340
|
+
# This proof is accepted only with the full-context signature below;
|
|
341
|
+
# the legacy primary signature intentionally remains byte-compatible.
|
|
342
|
+
forwarded[HTTP_BRIDGE_FILE_OWNER_HEADER] = context.file_owner_account_id
|
|
343
|
+
if context.client_ip:
|
|
344
|
+
forwarded[HTTP_BRIDGE_CLIENT_IP_HEADER] = context.client_ip
|
|
345
|
+
forwarded[HTTP_BRIDGE_CLIENT_IP_SIGNATURE_HEADER] = _bridge_forward_signature(
|
|
346
|
+
payload=payload,
|
|
347
|
+
context=context,
|
|
348
|
+
include_client_ip=True,
|
|
349
|
+
signature_version=signature_version,
|
|
350
|
+
)
|
|
351
|
+
if context.downstream_turn_state:
|
|
352
|
+
forwarded["x-codex-turn-state"] = context.downstream_turn_state
|
|
353
|
+
if context.reservation is not None:
|
|
354
|
+
forwarded[HTTP_BRIDGE_RESERVATION_ID_HEADER] = context.reservation.reservation_id
|
|
355
|
+
forwarded[HTTP_BRIDGE_RESERVATION_KEY_ID_HEADER] = context.reservation.key_id
|
|
356
|
+
forwarded[HTTP_BRIDGE_RESERVATION_MODEL_HEADER] = context.reservation.model
|
|
357
|
+
# ROLLOUT SHIM (#1203, remove with HTTP_BRIDGE_SIGNATURE_V2_HEADER
|
|
358
|
+
# follow-up): keep sending the primary signature computed over the plain
|
|
359
|
+
# ``model_dump`` (with synthesized ``"tools": []``) so owners running code
|
|
360
|
+
# that predates the tamper-proofing header can still verify requests from
|
|
361
|
+
# updated origins during a rolling upgrade. New-code receivers verify the
|
|
362
|
+
# tamper-proofing header below first and fall back to this primary
|
|
363
|
+
# signature only when the tamper-proofing header does not validate.
|
|
364
|
+
forwarded[HTTP_BRIDGE_SIGNATURE_HEADER] = _bridge_forward_signature(
|
|
365
|
+
payload=payload,
|
|
366
|
+
context=context,
|
|
367
|
+
include_client_ip=False,
|
|
368
|
+
signature_version=signature_version,
|
|
369
|
+
)
|
|
370
|
+
# Additive tamper-proofing signature bound to the exact posted forwarding
|
|
371
|
+
# body; covers the full authenticated context (including the unanchored /
|
|
372
|
+
# signature-version domain) so it cannot be replayed against a different
|
|
373
|
+
# forward.
|
|
374
|
+
forwarded[HTTP_BRIDGE_SIGNATURE_V2_HEADER] = _bridge_forward_tools_bound_signature(
|
|
375
|
+
payload=payload,
|
|
376
|
+
context=context,
|
|
377
|
+
signature_version=signature_version,
|
|
378
|
+
)
|
|
379
|
+
return forwarded
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def parse_forwarded_request(
|
|
383
|
+
headers: Mapping[str, str],
|
|
384
|
+
*,
|
|
385
|
+
payload: ResponsesRequest,
|
|
386
|
+
current_instance: str,
|
|
387
|
+
) -> tuple[HTTPBridgeForwardedRequest | None, ProxyResponseError | None]:
|
|
388
|
+
if headers.get(HTTP_BRIDGE_FORWARDED_HEADER) != "1":
|
|
389
|
+
return None, ProxyResponseError(
|
|
390
|
+
400,
|
|
391
|
+
openai_error(
|
|
392
|
+
"bridge_forward_invalid",
|
|
393
|
+
"Internal bridge forward marker is required",
|
|
394
|
+
error_type="invalid_request_error",
|
|
395
|
+
),
|
|
396
|
+
)
|
|
397
|
+
target_instance = headers.get(HTTP_BRIDGE_TARGET_INSTANCE_HEADER, "").strip()
|
|
398
|
+
if not target_instance or target_instance != current_instance:
|
|
399
|
+
return None, ProxyResponseError(
|
|
400
|
+
503,
|
|
401
|
+
openai_error(
|
|
402
|
+
"bridge_owner_forward_failed",
|
|
403
|
+
"Internal bridge forward reached a non-target instance",
|
|
404
|
+
error_type="server_error",
|
|
405
|
+
),
|
|
406
|
+
)
|
|
407
|
+
client_ip = _optional_header(headers.get(HTTP_BRIDGE_CLIENT_IP_HEADER))
|
|
408
|
+
signature_version = _optional_header(headers.get(HTTP_BRIDGE_SIGNATURE_VERSION_HEADER))
|
|
409
|
+
original_unanchored_value = _optional_header(headers.get(HTTP_BRIDGE_ORIGINAL_UNANCHORED_HEADER))
|
|
410
|
+
if signature_version == _HTTP_BRIDGE_SIGNATURE_VERSION_V2:
|
|
411
|
+
if original_unanchored_value not in {"0", "1"}:
|
|
412
|
+
return None, _invalid_bridge_forward_signature_error()
|
|
413
|
+
original_request_unanchored = original_unanchored_value == "1"
|
|
414
|
+
elif signature_version is None:
|
|
415
|
+
original_request_unanchored = False
|
|
416
|
+
else:
|
|
417
|
+
return None, _invalid_bridge_forward_signature_error()
|
|
418
|
+
context = HTTPBridgeForwardContext(
|
|
419
|
+
origin_instance=headers.get(HTTP_BRIDGE_ORIGIN_INSTANCE_HEADER, "").strip() or "unknown",
|
|
420
|
+
target_instance=target_instance,
|
|
421
|
+
codex_session_affinity=_bool_header(headers.get(HTTP_BRIDGE_CODEX_AFFINITY_HEADER)),
|
|
422
|
+
downstream_turn_state=_optional_header(headers.get("x-codex-turn-state")),
|
|
423
|
+
original_request_unanchored=original_request_unanchored,
|
|
424
|
+
original_affinity_kind=_optional_header(headers.get(HTTP_BRIDGE_AFFINITY_KIND_HEADER)),
|
|
425
|
+
original_affinity_key=_optional_header(headers.get(HTTP_BRIDGE_AFFINITY_KEY_HEADER)),
|
|
426
|
+
file_owner_account_id=_optional_header(headers.get(HTTP_BRIDGE_FILE_OWNER_HEADER)),
|
|
427
|
+
client_ip=client_ip,
|
|
428
|
+
reservation=_reservation_from_headers(headers),
|
|
429
|
+
signature_version=signature_version,
|
|
430
|
+
)
|
|
431
|
+
# Tamper-proofing fast path (#1203): a VALIDATING tamper-proofing
|
|
432
|
+
# signature proves the received body was not rewritten in transit —
|
|
433
|
+
# including an injected ``"tools": []`` that the primary plain-dump
|
|
434
|
+
# signature cannot distinguish from an omitted field — and it also
|
|
435
|
+
# authenticates the full forward context (structured, delimiter-safe), so
|
|
436
|
+
# it accepts even when the legacy delimiter check below would bail. It is
|
|
437
|
+
# authoritative only when it validates: mere header presence is not a
|
|
438
|
+
# trustworthy signal, because an external client could plant a garbage
|
|
439
|
+
# value on an honestly primary-signed forward, so a present-but-invalid
|
|
440
|
+
# header simply falls through to the primary verification.
|
|
441
|
+
tools_bound_signature = _optional_header(headers.get(HTTP_BRIDGE_SIGNATURE_V2_HEADER))
|
|
442
|
+
tools_bound_valid = tools_bound_signature is not None and hmac.compare_digest(
|
|
443
|
+
tools_bound_signature,
|
|
444
|
+
_bridge_forward_tools_bound_signature(
|
|
445
|
+
payload=payload,
|
|
446
|
+
context=context,
|
|
447
|
+
signature_version=signature_version,
|
|
448
|
+
),
|
|
449
|
+
)
|
|
450
|
+
if tools_bound_valid:
|
|
451
|
+
return HTTPBridgeForwardedRequest(context=context), None
|
|
452
|
+
if context.file_owner_account_id is not None or extract_input_file_ids(payload.input):
|
|
453
|
+
# The rolling-upgrade primary signature does not bind the additive
|
|
454
|
+
# file-owner proof. Never allow a stripped/forged proof to downgrade to
|
|
455
|
+
# it, and never allow payloads with file references to fall back after a
|
|
456
|
+
# stripped proof made the owner value look absent.
|
|
457
|
+
return None, _invalid_bridge_forward_signature_error()
|
|
458
|
+
# ROLLOUT SHIM (#1203, remove with HTTP_BRIDGE_SIGNATURE_V2_HEADER
|
|
459
|
+
# follow-up): fall back to the primary signature (#1169's versioned /
|
|
460
|
+
# legacy scheme) so owners predating the tamper-proofing header — and
|
|
461
|
+
# genuinely old origins that only send the primary signature — keep
|
|
462
|
+
# verifying during a rolling upgrade. Known residual until the shim is
|
|
463
|
+
# removed: this fallback is exactly as strong as the pre-#1203 scheme, so
|
|
464
|
+
# a body-only rewrite that injects ``"tools": []`` downgrades to the
|
|
465
|
+
# plain-dump digest (which is insensitive to synthesized-vs-injected empty
|
|
466
|
+
# tools) and verifies. Dropping the shim restores strict tamper-proof
|
|
467
|
+
# rejection.
|
|
468
|
+
if signature_version is None and _legacy_signature_context_has_ambiguous_delimiter(context):
|
|
469
|
+
return None, _invalid_bridge_forward_signature_error()
|
|
470
|
+
signature = _optional_header(headers.get(HTTP_BRIDGE_SIGNATURE_HEADER))
|
|
471
|
+
client_ip_signature = _optional_header(headers.get(HTTP_BRIDGE_CLIENT_IP_SIGNATURE_HEADER))
|
|
472
|
+
expected_signature = _bridge_forward_signature(
|
|
473
|
+
payload=payload,
|
|
474
|
+
context=context,
|
|
475
|
+
signature_version=signature_version,
|
|
476
|
+
)
|
|
477
|
+
signature_without_client_ip = _bridge_forward_signature(
|
|
478
|
+
payload=payload,
|
|
479
|
+
context=context,
|
|
480
|
+
include_client_ip=False,
|
|
481
|
+
signature_version=signature_version,
|
|
482
|
+
)
|
|
483
|
+
primary_signature_valid = signature is not None and hmac.compare_digest(signature, expected_signature)
|
|
484
|
+
signature_without_client_ip_valid = signature is not None and hmac.compare_digest(
|
|
485
|
+
signature,
|
|
486
|
+
signature_without_client_ip,
|
|
487
|
+
)
|
|
488
|
+
client_ip_signature_valid = client_ip_signature is not None and hmac.compare_digest(
|
|
489
|
+
client_ip_signature,
|
|
490
|
+
expected_signature,
|
|
491
|
+
)
|
|
492
|
+
signature_valid = primary_signature_valid or (
|
|
493
|
+
signature_without_client_ip_valid and (client_ip is None or client_ip_signature_valid)
|
|
494
|
+
)
|
|
495
|
+
if not signature_valid:
|
|
496
|
+
return None, _invalid_bridge_forward_signature_error()
|
|
497
|
+
return HTTPBridgeForwardedRequest(context=context), None
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def _invalid_bridge_forward_signature_error() -> ProxyResponseError:
|
|
501
|
+
return ProxyResponseError(
|
|
502
|
+
400,
|
|
503
|
+
openai_error(
|
|
504
|
+
"bridge_forward_invalid",
|
|
505
|
+
"Internal bridge forward signature is invalid",
|
|
506
|
+
error_type="invalid_request_error",
|
|
507
|
+
),
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _legacy_signature_context_has_ambiguous_delimiter(context: HTTPBridgeForwardContext) -> bool:
|
|
512
|
+
"""Reject legacy fields whose boundaries cannot be authenticated safely."""
|
|
513
|
+
|
|
514
|
+
values = [
|
|
515
|
+
context.origin_instance,
|
|
516
|
+
context.target_instance,
|
|
517
|
+
context.downstream_turn_state,
|
|
518
|
+
context.original_affinity_kind,
|
|
519
|
+
context.original_affinity_key,
|
|
520
|
+
context.client_ip,
|
|
521
|
+
]
|
|
522
|
+
if context.reservation is not None:
|
|
523
|
+
values.extend(
|
|
524
|
+
(
|
|
525
|
+
context.reservation.reservation_id,
|
|
526
|
+
context.reservation.key_id,
|
|
527
|
+
context.reservation.model,
|
|
528
|
+
)
|
|
529
|
+
)
|
|
530
|
+
return any(_LEGACY_SIGNATURE_DELIMITER in value for value in values if value is not None)
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _owner_forward_timeout(*, connect_timeout_seconds: float, idle_timeout_seconds: float) -> aiohttp.ClientTimeout:
|
|
534
|
+
return aiohttp.ClientTimeout(
|
|
535
|
+
total=None,
|
|
536
|
+
sock_connect=connect_timeout_seconds,
|
|
537
|
+
sock_read=max(0.001, idle_timeout_seconds),
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def _reservation_from_headers(headers: Mapping[str, str]) -> ApiKeyUsageReservationData | None:
|
|
542
|
+
reservation_id = _optional_header(headers.get(HTTP_BRIDGE_RESERVATION_ID_HEADER))
|
|
543
|
+
key_id = _optional_header(headers.get(HTTP_BRIDGE_RESERVATION_KEY_ID_HEADER))
|
|
544
|
+
model = _optional_header(headers.get(HTTP_BRIDGE_RESERVATION_MODEL_HEADER))
|
|
545
|
+
if reservation_id is None or key_id is None or model is None:
|
|
546
|
+
return None
|
|
547
|
+
return ApiKeyUsageReservationData(
|
|
548
|
+
reservation_id=reservation_id,
|
|
549
|
+
key_id=key_id,
|
|
550
|
+
model=model,
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def _bool_header(value: str | None) -> bool:
|
|
555
|
+
if value is None:
|
|
556
|
+
return False
|
|
557
|
+
return value.strip().lower() in {"1", "true", "yes", "on"}
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def _optional_header(value: str | None) -> str | None:
|
|
561
|
+
if value is None:
|
|
562
|
+
return None
|
|
563
|
+
stripped = value.strip()
|
|
564
|
+
return stripped or None
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def _bridge_forward_signature(
|
|
568
|
+
*,
|
|
569
|
+
payload: ResponsesRequest,
|
|
570
|
+
context: HTTPBridgeForwardContext,
|
|
571
|
+
include_client_ip: bool = True,
|
|
572
|
+
signature_version: str | None = None,
|
|
573
|
+
) -> str:
|
|
574
|
+
"""Primary forward signature (#1169), computed over a plain ``model_dump``.
|
|
575
|
+
|
|
576
|
+
Because the plain dump synthesizes ``"tools": []`` for clients that
|
|
577
|
+
omitted the field, this digest cannot distinguish an omitted-tools body
|
|
578
|
+
from one with an injected explicit empty list; the tamper-proof binding
|
|
579
|
+
lives in ``_bridge_forward_tools_bound_signature`` (#1203). This signature
|
|
580
|
+
is retained for the unanchored/legacy wire contract and rolling-upgrade
|
|
581
|
+
fallback. ``signature_version is None`` uses the deployed legacy
|
|
582
|
+
delimiter-joined format; the versioned path uses a canonical structured
|
|
583
|
+
encoding whose object boundaries make field re-packing impossible.
|
|
584
|
+
"""
|
|
585
|
+
body_digest = _bridge_forward_body_digest(payload.model_dump(mode="json", exclude_none=True))
|
|
586
|
+
if signature_version is None:
|
|
587
|
+
# Preserve the deployed legacy wire format for anchored requests during
|
|
588
|
+
# rolling upgrades. Its delimiter-based encoding is intentionally not
|
|
589
|
+
# reused by the versioned path because attacker-controlled affinity
|
|
590
|
+
# values can contain the delimiter and make different field layouts
|
|
591
|
+
# authenticate alike.
|
|
592
|
+
fields = [
|
|
593
|
+
context.origin_instance,
|
|
594
|
+
context.target_instance,
|
|
595
|
+
"1" if context.codex_session_affinity else "0",
|
|
596
|
+
context.downstream_turn_state or "",
|
|
597
|
+
context.original_affinity_kind or "",
|
|
598
|
+
context.original_affinity_key or "",
|
|
599
|
+
]
|
|
600
|
+
if include_client_ip:
|
|
601
|
+
fields.append(context.client_ip or "")
|
|
602
|
+
fields.extend(
|
|
603
|
+
(
|
|
604
|
+
context.reservation.reservation_id if context.reservation is not None else "",
|
|
605
|
+
context.reservation.key_id if context.reservation is not None else "",
|
|
606
|
+
context.reservation.model if context.reservation is not None else "",
|
|
607
|
+
body_digest,
|
|
608
|
+
)
|
|
609
|
+
)
|
|
610
|
+
signing_payload = _LEGACY_SIGNATURE_DELIMITER.join(fields)
|
|
611
|
+
else:
|
|
612
|
+
signing_payload = _structured_bridge_signing_payload(
|
|
613
|
+
body_digest=body_digest,
|
|
614
|
+
context=context,
|
|
615
|
+
include_client_ip=include_client_ip,
|
|
616
|
+
signature_version=signature_version,
|
|
617
|
+
protocol="codex-lb-http-bridge-forward",
|
|
618
|
+
)
|
|
619
|
+
return _sign_bridge_payload(signing_payload)
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def _bridge_forward_tools_bound_signature(
|
|
623
|
+
*,
|
|
624
|
+
payload: ResponsesRequest,
|
|
625
|
+
context: HTTPBridgeForwardContext,
|
|
626
|
+
signature_version: str | None = None,
|
|
627
|
+
) -> str:
|
|
628
|
+
"""Tamper-proofing signature bound to the exact posted forwarding body.
|
|
629
|
+
|
|
630
|
+
Signs the same forwarding dump that is actually posted
|
|
631
|
+
(``model_dump_for_forwarding``), not a plain ``model_dump`` that
|
|
632
|
+
synthesizes ``"tools": []`` for clients that omitted the field. A plain
|
|
633
|
+
dump would make the omitted-tools and explicit-``tools: []`` bodies sign
|
|
634
|
+
identically, so a body rewritten in transit to inject ``"tools": []``
|
|
635
|
+
would still verify on the owner instance and re-mark ``tools`` as
|
|
636
|
+
explicitly set (issue #1184). Uses a distinct protocol domain so it can
|
|
637
|
+
never be confused with the primary signature, and reuses the same
|
|
638
|
+
canonical structured encoding (covering the full authenticated context,
|
|
639
|
+
including the unanchored / signature-version domain) so the binding also
|
|
640
|
+
carries #1169's isolation guarantees. Always authenticates ``client_ip``.
|
|
641
|
+
"""
|
|
642
|
+
body_digest = _bridge_forward_body_digest(payload.model_dump_for_forwarding())
|
|
643
|
+
signing_payload = _structured_bridge_signing_payload(
|
|
644
|
+
body_digest=body_digest,
|
|
645
|
+
context=context,
|
|
646
|
+
include_client_ip=True,
|
|
647
|
+
signature_version=signature_version,
|
|
648
|
+
protocol="codex-lb-http-bridge-forward-tools-bound",
|
|
649
|
+
)
|
|
650
|
+
return _sign_bridge_payload(signing_payload)
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def _bridge_forward_body_digest(payload_dump: JsonObject) -> str:
|
|
654
|
+
payload_json = json.dumps(payload_dump, ensure_ascii=True, sort_keys=True, separators=(",", ":"))
|
|
655
|
+
return hashlib.sha256(payload_json.encode("utf-8")).hexdigest()
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def _structured_bridge_signing_payload(
|
|
659
|
+
*,
|
|
660
|
+
body_digest: str,
|
|
661
|
+
context: HTTPBridgeForwardContext,
|
|
662
|
+
include_client_ip: bool,
|
|
663
|
+
signature_version: str | None,
|
|
664
|
+
protocol: str,
|
|
665
|
+
) -> str:
|
|
666
|
+
# Canonical structured encoding: object boundaries make field re-packing
|
|
667
|
+
# impossible, the client-IP mode is itself authenticated, and ``protocol``
|
|
668
|
+
# domain-separates the primary and tamper-proofing signatures.
|
|
669
|
+
return json.dumps(
|
|
670
|
+
{
|
|
671
|
+
"body_digest": body_digest,
|
|
672
|
+
"client_ip": context.client_ip if include_client_ip else None,
|
|
673
|
+
"client_ip_present": context.client_ip is not None,
|
|
674
|
+
"codex_session_affinity": context.codex_session_affinity,
|
|
675
|
+
"downstream_turn_state": context.downstream_turn_state,
|
|
676
|
+
"file_owner_account_id": context.file_owner_account_id,
|
|
677
|
+
"include_client_ip": include_client_ip,
|
|
678
|
+
"origin_instance": context.origin_instance,
|
|
679
|
+
"original_affinity_key": context.original_affinity_key,
|
|
680
|
+
"original_affinity_kind": context.original_affinity_kind,
|
|
681
|
+
"original_request_unanchored": context.original_request_unanchored,
|
|
682
|
+
"protocol": protocol,
|
|
683
|
+
"reservation": (
|
|
684
|
+
{
|
|
685
|
+
"id": context.reservation.reservation_id,
|
|
686
|
+
"key_id": context.reservation.key_id,
|
|
687
|
+
"model": context.reservation.model,
|
|
688
|
+
}
|
|
689
|
+
if context.reservation is not None
|
|
690
|
+
else None
|
|
691
|
+
),
|
|
692
|
+
"signature_version": signature_version,
|
|
693
|
+
"target_instance": context.target_instance,
|
|
694
|
+
},
|
|
695
|
+
ensure_ascii=True,
|
|
696
|
+
sort_keys=True,
|
|
697
|
+
separators=(",", ":"),
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def _sign_bridge_payload(signing_payload: str) -> str:
|
|
702
|
+
secret = get_or_create_key(get_settings().encryption_key_file)
|
|
703
|
+
return hmac.new(secret, signing_payload.encode("utf-8"), hashlib.sha256).hexdigest()
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
async def _iter_sse_event_blocks(
|
|
707
|
+
response: aiohttp.ClientResponse,
|
|
708
|
+
*,
|
|
709
|
+
request_started_at: float,
|
|
710
|
+
proxy_request_budget_seconds: float,
|
|
711
|
+
stream_idle_timeout_seconds: float,
|
|
712
|
+
scheduler: Scheduler,
|
|
713
|
+
clock: Clock,
|
|
714
|
+
) -> AsyncIterator[str]:
|
|
715
|
+
buffer = b""
|
|
716
|
+
chunks = response.content.iter_chunked(65536)
|
|
717
|
+
while True:
|
|
718
|
+
receive_timeout = _owner_forward_receive_timeout(
|
|
719
|
+
request_started_at=request_started_at,
|
|
720
|
+
proxy_request_budget_seconds=proxy_request_budget_seconds,
|
|
721
|
+
stream_idle_timeout_seconds=stream_idle_timeout_seconds,
|
|
722
|
+
now=clock.monotonic(),
|
|
723
|
+
)
|
|
724
|
+
try:
|
|
725
|
+
chunk = await scheduler.wait_for(chunks.__anext__(), timeout=receive_timeout.timeout_seconds)
|
|
726
|
+
except StopAsyncIteration:
|
|
727
|
+
break
|
|
728
|
+
except asyncio.TimeoutError as exc:
|
|
729
|
+
raise _OwnerForwardStreamTimeoutError(
|
|
730
|
+
error_code=receive_timeout.error_code,
|
|
731
|
+
error_message=receive_timeout.error_message,
|
|
732
|
+
) from exc
|
|
733
|
+
if not chunk:
|
|
734
|
+
continue
|
|
735
|
+
buffer += chunk
|
|
736
|
+
while b"\n\n" in buffer:
|
|
737
|
+
raw_block, buffer = buffer.split(b"\n\n", 1)
|
|
738
|
+
text = raw_block.decode("utf-8")
|
|
739
|
+
if text:
|
|
740
|
+
yield f"{text}\n\n"
|
|
741
|
+
if buffer.strip():
|
|
742
|
+
yield buffer.decode("utf-8")
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
def _owner_forward_receive_timeout(
|
|
746
|
+
*,
|
|
747
|
+
request_started_at: float,
|
|
748
|
+
proxy_request_budget_seconds: float,
|
|
749
|
+
stream_idle_timeout_seconds: float,
|
|
750
|
+
now: float,
|
|
751
|
+
) -> _OwnerForwardReceiveTimeout:
|
|
752
|
+
idle_timeout_seconds = max(0.001, stream_idle_timeout_seconds)
|
|
753
|
+
remaining_budget = max(0.0, request_started_at + proxy_request_budget_seconds - now)
|
|
754
|
+
idle_timeout_matches_request_budget = idle_timeout_seconds == max(0.001, proxy_request_budget_seconds)
|
|
755
|
+
if remaining_budget <= 0 and idle_timeout_matches_request_budget:
|
|
756
|
+
return _OwnerForwardReceiveTimeout(
|
|
757
|
+
timeout_seconds=0.0,
|
|
758
|
+
error_code="stream_idle_timeout",
|
|
759
|
+
error_message="Upstream stream idle timeout",
|
|
760
|
+
)
|
|
761
|
+
if idle_timeout_matches_request_budget and remaining_budget >= idle_timeout_seconds:
|
|
762
|
+
return _OwnerForwardReceiveTimeout(
|
|
763
|
+
timeout_seconds=remaining_budget,
|
|
764
|
+
error_code="stream_idle_timeout",
|
|
765
|
+
error_message="Upstream stream idle timeout",
|
|
766
|
+
)
|
|
767
|
+
if remaining_budget <= 0:
|
|
768
|
+
return _OwnerForwardReceiveTimeout(
|
|
769
|
+
timeout_seconds=0.0,
|
|
770
|
+
error_code="upstream_request_timeout",
|
|
771
|
+
error_message="Proxy request budget exhausted",
|
|
772
|
+
)
|
|
773
|
+
if idle_timeout_seconds <= remaining_budget:
|
|
774
|
+
return _OwnerForwardReceiveTimeout(
|
|
775
|
+
timeout_seconds=idle_timeout_seconds,
|
|
776
|
+
error_code="stream_idle_timeout",
|
|
777
|
+
error_message="Upstream stream idle timeout",
|
|
778
|
+
)
|
|
779
|
+
return _OwnerForwardReceiveTimeout(
|
|
780
|
+
timeout_seconds=remaining_budget,
|
|
781
|
+
error_code="upstream_request_timeout",
|
|
782
|
+
error_message="Proxy request budget exhausted",
|
|
783
|
+
)
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
def _owner_forward_error_payload(*, status_code: int, payload_text: str) -> OpenAIErrorEnvelope:
|
|
787
|
+
try:
|
|
788
|
+
payload = json.loads(payload_text)
|
|
789
|
+
except json.JSONDecodeError:
|
|
790
|
+
payload = None
|
|
791
|
+
if is_json_mapping(payload) and is_json_mapping(payload.get("error")):
|
|
792
|
+
return cast(OpenAIErrorEnvelope, payload)
|
|
793
|
+
return openai_error(
|
|
794
|
+
"bridge_owner_forward_failed",
|
|
795
|
+
payload_text or f"HTTP bridge owner request failed with status {status_code}",
|
|
796
|
+
error_type="server_error",
|
|
797
|
+
)
|