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,999 @@
|
|
|
1
|
+
"""Single-owner lifecycle for a dispatched model-source attempt (#2123 WP-C1).
|
|
2
|
+
|
|
3
|
+
Every request forwarded to an OpenAI-compatible model source is owned by
|
|
4
|
+
exactly one ``SourceDispatch`` that ends in exactly one ``finish()`` (I10): the
|
|
5
|
+
source connection is closed, the API-key reservation is settled or released
|
|
6
|
+
exactly once (I3), the admission claims are released, and one request-log row
|
|
7
|
+
is written per attempt -- each step independently latched and failure-isolated
|
|
8
|
+
(design v3 §6). ``abandon()`` is the cancellation-class ``finish()`` for a
|
|
9
|
+
client that left during the open or before the body started (§6.4 table).
|
|
10
|
+
|
|
11
|
+
Composition rules:
|
|
12
|
+
|
|
13
|
+
* the settlement generator (``settlement_stream``) is the *outermost* body
|
|
14
|
+
layer for every composition: it sees the terminal outcome and the
|
|
15
|
+
cancellation first, and it owns the reservation;
|
|
16
|
+
* ``SourceStreamingResponse.__call__`` wraps the transport in
|
|
17
|
+
``try/finally: _await_cleanup_deferring_cancellation(owner.finalize_transport(), scheduler=owner.scheduler)``
|
|
18
|
+
so a client that leaves before Starlette starts the body still reaches one
|
|
19
|
+
``finish()``; the direct chat-completions stream route, which has no
|
|
20
|
+
dispatch owner, hands the same response class a ``SourceChatStreamOwner``
|
|
21
|
+
so its never-started body closes the source transport too;
|
|
22
|
+
* the handler segment uses ``except BaseException -> abandon(); raise``
|
|
23
|
+
(``CancelledError`` is a ``BaseException`` and is never caught by
|
|
24
|
+
``except Exception``);
|
|
25
|
+
* every timer, wait and task spawn goes through ``owner.scheduler`` /
|
|
26
|
+
``owner.clock`` (``scripts/check_proxy_timing_seams.py`` has a zero
|
|
27
|
+
allowance for this module) and every cleanup await defers cancellation
|
|
28
|
+
(``scripts/check_cancellation_safety.py``).
|
|
29
|
+
|
|
30
|
+
Settle policy (design §6.1, CL-8/P4/CP-16): ``success`` with usage finalizes
|
|
31
|
+
at the source's usage and cost; ``success`` without usage on a limited key
|
|
32
|
+
settles at an estimate (never at zero, never released); a cancel *after*
|
|
33
|
+
content was delivered to the client (an event frame ``settlement_stream``
|
|
34
|
+
handed to the transport -- not the parser's observation, nor the body's flush
|
|
35
|
+
point, which sits below wrapper layers that can still drop or park a frame)
|
|
36
|
+
on a limited key settles at the estimate (a key must not consume most of an
|
|
37
|
+
answer and disconnect unmetered); a cancel before it, and every
|
|
38
|
+
``error``, release. A stream the source ends with a failure terminal, or
|
|
39
|
+
closes without any terminal the client could complete on, is an ``error``
|
|
40
|
+
(``model_source_response_failed`` / ``model_source_stream_truncated``) and
|
|
41
|
+
releases: the client received a failure, so nothing is charged. Estimates are
|
|
42
|
+
never written to the request-log row
|
|
43
|
+
as usage -- they are visible through the WARN line and the
|
|
44
|
+
``codex_lb_model_source_usage_estimated_total`` counter.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
from __future__ import annotations
|
|
48
|
+
|
|
49
|
+
import asyncio
|
|
50
|
+
import logging
|
|
51
|
+
from collections.abc import AsyncIterator, Awaitable, Callable, Coroutine, Mapping
|
|
52
|
+
from dataclasses import dataclass, field
|
|
53
|
+
from typing import Any, Literal, Protocol, TypeVar
|
|
54
|
+
|
|
55
|
+
from fastapi import Request
|
|
56
|
+
from fastapi.responses import StreamingResponse
|
|
57
|
+
from starlette.types import Receive, Scope, Send
|
|
58
|
+
|
|
59
|
+
from app.core.clock import REAL_CLOCK, REAL_SCHEDULER, Clock, Scheduler
|
|
60
|
+
from app.core.metrics.prometheus import (
|
|
61
|
+
model_source_dispatch_abandoned_total,
|
|
62
|
+
model_source_dispatch_total,
|
|
63
|
+
model_source_timeout_total,
|
|
64
|
+
model_source_usage_estimated_total,
|
|
65
|
+
)
|
|
66
|
+
from app.core.openai.parsing import classify_event_type
|
|
67
|
+
from app.core.request_locality import resolve_request_client_host
|
|
68
|
+
from app.core.types import JsonValue
|
|
69
|
+
from app.core.utils.request_id import ensure_request_id
|
|
70
|
+
from app.core.utils.shared_future import (
|
|
71
|
+
_await_cleanup_deferring_cancellation,
|
|
72
|
+
_await_result_deferring_cancellation,
|
|
73
|
+
_await_task_deferring_cancellation,
|
|
74
|
+
)
|
|
75
|
+
from app.core.utils.sse import _SSE_LINE_BOUNDARY, parse_sse_data_json
|
|
76
|
+
from app.db.models import ModelSource
|
|
77
|
+
from app.db.session import get_background_session
|
|
78
|
+
from app.modules.api_keys.service import (
|
|
79
|
+
API_KEY_USAGE_RESERVATION_DEFAULT_INPUT_TOKENS,
|
|
80
|
+
API_KEY_USAGE_RESERVATION_DEFAULT_OUTPUT_TOKENS,
|
|
81
|
+
ApiKeyData,
|
|
82
|
+
ApiKeyRequestUsageBudget,
|
|
83
|
+
ApiKeyUsageReservationData,
|
|
84
|
+
)
|
|
85
|
+
from app.modules.model_sources.catalog import source_model_cost_usd
|
|
86
|
+
from app.modules.model_sources.forwarding import (
|
|
87
|
+
ModelSourceForwardingError,
|
|
88
|
+
SourceChatStream,
|
|
89
|
+
SourceResponsesStream,
|
|
90
|
+
SourceTimings,
|
|
91
|
+
SourceUsage,
|
|
92
|
+
SourceUsageHolder,
|
|
93
|
+
TimeoutPhase,
|
|
94
|
+
classify_responses_frame,
|
|
95
|
+
)
|
|
96
|
+
from app.modules.proxy._service.support import _request_log_client_fields
|
|
97
|
+
from app.modules.proxy.affinity import _owner_lookup_session_id_from_headers
|
|
98
|
+
from app.modules.proxy.source_admission import SourceAdmission
|
|
99
|
+
from app.modules.request_logs.repository import RequestLogsRepository
|
|
100
|
+
|
|
101
|
+
logger = logging.getLogger(__name__)
|
|
102
|
+
|
|
103
|
+
T = TypeVar("T")
|
|
104
|
+
|
|
105
|
+
DispatchStatus = Literal["success", "error", "cancelled"]
|
|
106
|
+
EstimateCause = Literal["missing_usage", "client_cancel"]
|
|
107
|
+
AbandonStage = Literal["during_open", "before_body", "stall"]
|
|
108
|
+
|
|
109
|
+
# Attribution for direct source routing.
|
|
110
|
+
REQUEST_LOG_SOURCE = "model_source"
|
|
111
|
+
DISPATCH_KIND = "direct"
|
|
112
|
+
|
|
113
|
+
# A client that leaves while the open is still pending is a stall abandonment
|
|
114
|
+
# once the source has been silent this long without a first frame.
|
|
115
|
+
STALL_EVIDENCE_SECONDS = 10.0
|
|
116
|
+
# Disconnect poll cadence while the source open is pending.
|
|
117
|
+
OPEN_DISCONNECT_POLL_SECONDS = 0.25
|
|
118
|
+
|
|
119
|
+
# Terminal codes written by the owner itself (stage-naming ``error_code``).
|
|
120
|
+
ABANDON_CLIENT_DISCONNECTED_DURING_OPEN = "client_disconnected_during_open"
|
|
121
|
+
ABANDON_SOURCE_STALL = "source_stall_abandoned"
|
|
122
|
+
ABANDON_CLIENT_DISCONNECTED_BEFORE_BODY = "client_disconnected_before_body"
|
|
123
|
+
ABANDON_DISPATCH_INTERRUPTED = "dispatch_interrupted"
|
|
124
|
+
ERROR_USAGE_SETTLEMENT_FAILED = "usage_settlement_failed"
|
|
125
|
+
ERROR_MODEL_SOURCE_STREAM = "model_source_stream_error"
|
|
126
|
+
# The source ended the stream with ``response.failed`` / ``error``: no answer was
|
|
127
|
+
# delivered, so the attempt is an error and the reservation is released.
|
|
128
|
+
ERROR_MODEL_SOURCE_RESPONSE_FAILED = "model_source_response_failed"
|
|
129
|
+
# The source closed the stream without any terminal frame (the public wrapper
|
|
130
|
+
# synthesizes ``response.failed`` for SDK clients): no answer was delivered.
|
|
131
|
+
ERROR_MODEL_SOURCE_STREAM_TRUNCATED = "model_source_stream_truncated"
|
|
132
|
+
# The source ended with a success terminal the public contract could not accept
|
|
133
|
+
# (``response`` not an object, invalid output items): the wrapper relayed
|
|
134
|
+
# ``response.failed`` in its place, so the client received a failure and no
|
|
135
|
+
# answer was delivered.
|
|
136
|
+
ERROR_MODEL_SOURCE_RESPONSE_INVALID = "model_source_response_invalid"
|
|
137
|
+
_FAILURE_TERMINAL_KINDS = frozenset({"failed", "error"})
|
|
138
|
+
_SUCCESS_TERMINAL_KINDS = frozenset({"completed", "incomplete"})
|
|
139
|
+
_RELAYED_TERMINAL_KINDS: Mapping[str, str] = {
|
|
140
|
+
"response.completed": "completed",
|
|
141
|
+
"response.incomplete": "incomplete",
|
|
142
|
+
"response.failed": "failed",
|
|
143
|
+
"error": "error",
|
|
144
|
+
}
|
|
145
|
+
# Frames the settlement layer never takes as the stream's last event: SSE
|
|
146
|
+
# comments (``: keepalive``), the ``[DONE]`` sentinel and the Codex keepalive.
|
|
147
|
+
_NON_EVENT_FRAME_PREFIXES = ("data: [DONE]", "event: codex.keepalive")
|
|
148
|
+
# ``classify_responses_frame`` kinds that deliver response content to the
|
|
149
|
+
# client (the api-keys delta: an output item, a ``*.delta``, a success terminal
|
|
150
|
+
# or any event type this proxy does not know); bookkeeping and failure
|
|
151
|
+
# terminals do not.
|
|
152
|
+
_DELIVERING_FRAME_KINDS = frozenset({"content", "success_terminal"})
|
|
153
|
+
CANCELLED_CLIENT_DISCONNECTED = "client_disconnected"
|
|
154
|
+
|
|
155
|
+
_ABANDON_STAGES: Mapping[str, AbandonStage] = {
|
|
156
|
+
ABANDON_CLIENT_DISCONNECTED_DURING_OPEN: "during_open",
|
|
157
|
+
ABANDON_DISPATCH_INTERRUPTED: "during_open",
|
|
158
|
+
ABANDON_SOURCE_STALL: "stall",
|
|
159
|
+
ABANDON_CLIENT_DISCONNECTED_BEFORE_BODY: "before_body",
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class CleanupScheduler(Protocol):
|
|
164
|
+
"""``ProxyService`` satisfies this: release retries ride its cancel-safe cleanup tasks."""
|
|
165
|
+
|
|
166
|
+
def _schedule_cancel_safe_cleanup(
|
|
167
|
+
self,
|
|
168
|
+
coro: Coroutine[Any, Any, None],
|
|
169
|
+
*,
|
|
170
|
+
action: str,
|
|
171
|
+
request_id: str,
|
|
172
|
+
) -> asyncio.Task[None]: ...
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class ReservationSettler(Protocol):
|
|
176
|
+
"""``api.py``'s ``_settle_source_reservation`` shape: finalize at ``usage``; ``False`` after a release fallback."""
|
|
177
|
+
|
|
178
|
+
def __call__(
|
|
179
|
+
self,
|
|
180
|
+
reservation: ApiKeyUsageReservationData,
|
|
181
|
+
*,
|
|
182
|
+
source: ModelSource,
|
|
183
|
+
model: str,
|
|
184
|
+
usage: SourceUsage | None,
|
|
185
|
+
cost_usd_override: float | None = None,
|
|
186
|
+
) -> Coroutine[Any, Any, bool]: ...
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class ReservationReleaser(Protocol):
|
|
190
|
+
"""``api.py``'s ``_release_reservation`` shape."""
|
|
191
|
+
|
|
192
|
+
def __call__(self, reservation: ApiKeyUsageReservationData) -> Coroutine[Any, Any, None]: ...
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class ClientDisconnectedDuringOpen(Exception):
|
|
196
|
+
"""The client left while the source open was pending."""
|
|
197
|
+
|
|
198
|
+
def __init__(self, *, pending_seconds: float, stall: bool) -> None:
|
|
199
|
+
super().__init__(f"client disconnected after {pending_seconds:.3f}s (stall={stall})")
|
|
200
|
+
self.pending_seconds = pending_seconds
|
|
201
|
+
self.stall = stall
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def relayed_terminal_kind(frame: str | None) -> str | None:
|
|
205
|
+
"""Terminal kind of an event frame relayed to the client; ``None`` when it is not a terminal.
|
|
206
|
+
|
|
207
|
+
Read from the ``event:`` line the public wrapper frames every typed event
|
|
208
|
+
with, falling back to the ``data:`` JSON for a raw pass-through block.
|
|
209
|
+
``settlement_stream`` latches the kinds it sees into the client-visible
|
|
210
|
+
outcome: when the parser saw no terminal (the frame outgrew its cap, or the
|
|
211
|
+
source closed without one) it decides between a delivered success and a
|
|
212
|
+
truncated stream.
|
|
213
|
+
"""
|
|
214
|
+
|
|
215
|
+
if not frame:
|
|
216
|
+
return None
|
|
217
|
+
event_type = _relayed_event_type(frame)
|
|
218
|
+
return _RELAYED_TERMINAL_KINDS.get(event_type) if event_type is not None else None
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def relayed_frame_delivers_content(frame: str | None) -> bool:
|
|
222
|
+
"""``True`` when a relayed event frame carries response content the client can use.
|
|
223
|
+
|
|
224
|
+
Classified with the parser's own ``classify_responses_frame`` so the body
|
|
225
|
+
and the settlement layer agree on what content is: an output item, a
|
|
226
|
+
``*.delta``, a success terminal or any event type this proxy does not know
|
|
227
|
+
delivers content; ``response.created`` / ``in_progress`` / ``queued``, a
|
|
228
|
+
failure terminal and a frame without a typed event (a comment, ``[DONE]``,
|
|
229
|
+
the Codex keepalive, unparseable ``data``) do not.
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
if not frame or not _is_event_frame(frame):
|
|
233
|
+
return False
|
|
234
|
+
return classify_responses_frame(_relayed_event_type(frame)) in _DELIVERING_FRAME_KINDS
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _is_event_frame(chunk: str) -> bool:
|
|
238
|
+
"""An SSE block that carries an event: not a comment, not ``[DONE]``, not the Codex keepalive."""
|
|
239
|
+
|
|
240
|
+
return bool(chunk) and chunk[0] != ":" and not chunk.startswith(_NON_EVENT_FRAME_PREFIXES)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _relayed_event_type(frame: str) -> str | None:
|
|
244
|
+
"""Event type of a relayed frame: the ``event:`` line the public wrapper frames every typed event with, else the
|
|
245
|
+
type of a raw pass-through block's ``data:`` JSON as the wrapper's own ``classify_event_type`` reads it (a
|
|
246
|
+
typeless ``{"error": {...}}`` record is an ``error`` terminal there too); ``None`` when neither names one.
|
|
247
|
+
|
|
248
|
+
Lines end at the SSE boundary set (CR, LF or CRLF -- ``_SSE_LINE_BOUNDARY``, never ``str.splitlines``, whose
|
|
249
|
+
extra Unicode breaks can sit unescaped inside a JSON string) and a multi-line ``data:`` is joined as the parser
|
|
250
|
+
joins it (``parse_sse_data_json``), so a spec-legal framing the wrapper relays verbatim classifies as the frame
|
|
251
|
+
the client parses: a bare-CR ``response.created`` is bookkeeping, not an unknown content-bearing type.
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
if frame.startswith("event: "):
|
|
255
|
+
return _SSE_LINE_BOUNDARY.split(frame[7:], maxsplit=1)[0]
|
|
256
|
+
return classify_event_type(parse_sse_data_json(frame))
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def error_code_from_payload(payload: Mapping[str, JsonValue]) -> str | None:
|
|
260
|
+
error = payload.get("error")
|
|
261
|
+
if not isinstance(error, Mapping):
|
|
262
|
+
return None
|
|
263
|
+
code = error.get("code")
|
|
264
|
+
return code if isinstance(code, str) else None
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def error_message_from_payload(payload: Mapping[str, JsonValue]) -> str | None:
|
|
268
|
+
error = payload.get("error")
|
|
269
|
+
if not isinstance(error, Mapping):
|
|
270
|
+
return None
|
|
271
|
+
message = error.get("message")
|
|
272
|
+
return message if isinstance(message, str) else None
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def source_usage_cost_usd(source: ModelSource, model: str, usage: SourceUsage | None) -> float | None:
|
|
276
|
+
"""Source pricing for ``usage``; unpriced entries cost ``0.0`` (never ``None`` for known usage)."""
|
|
277
|
+
|
|
278
|
+
if usage is None:
|
|
279
|
+
return None
|
|
280
|
+
cost_usd = source_model_cost_usd(
|
|
281
|
+
source,
|
|
282
|
+
model,
|
|
283
|
+
input_tokens=usage.input_tokens,
|
|
284
|
+
output_tokens=usage.output_tokens,
|
|
285
|
+
cached_input_tokens=usage.cached_input_tokens,
|
|
286
|
+
)
|
|
287
|
+
return 0.0 if cost_usd is None else cost_usd
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _reservation_requires_usage(reservation: ApiKeyUsageReservationData | None) -> bool:
|
|
291
|
+
return bool(reservation is not None and reservation.has_applicable_limits)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def estimate_settlement_usage(*, admission_budget: ApiKeyRequestUsageBudget | None, delta_chars: int) -> SourceUsage:
|
|
295
|
+
"""Settle-at-estimate figures: input = admission estimate or default; output = max(default, delta_chars // 4)."""
|
|
296
|
+
|
|
297
|
+
input_tokens = API_KEY_USAGE_RESERVATION_DEFAULT_INPUT_TOKENS
|
|
298
|
+
if admission_budget is not None and admission_budget.input_tokens is not None:
|
|
299
|
+
input_tokens = admission_budget.input_tokens
|
|
300
|
+
output_tokens = max(API_KEY_USAGE_RESERVATION_DEFAULT_OUTPUT_TOKENS, max(0, delta_chars) // 4)
|
|
301
|
+
return SourceUsage(input_tokens=input_tokens, output_tokens=output_tokens, cached_input_tokens=0)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _inc(counter: Any, **labels: str) -> None:
|
|
305
|
+
if counter is None:
|
|
306
|
+
return
|
|
307
|
+
try:
|
|
308
|
+
counter.labels(**labels).inc()
|
|
309
|
+
except Exception: # metrics never break the request path
|
|
310
|
+
logger.debug("model_source_dispatch_metric_failed labels=%s", labels, exc_info=True)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
@dataclass(slots=True)
|
|
314
|
+
class SourceDispatch:
|
|
315
|
+
"""Owner of one dispatched attempt; ``finish()``/``abandon()`` is the single latch."""
|
|
316
|
+
|
|
317
|
+
request: Request
|
|
318
|
+
source: ModelSource
|
|
319
|
+
model: str
|
|
320
|
+
api_key: ApiKeyData | None
|
|
321
|
+
reservation: ApiKeyUsageReservationData | None
|
|
322
|
+
claims: SourceAdmission
|
|
323
|
+
admission_budget: ApiKeyRequestUsageBudget | None
|
|
324
|
+
requested_service_tier: str | None
|
|
325
|
+
cleanup_scheduler: CleanupScheduler | None = None
|
|
326
|
+
scheduler: Scheduler = REAL_SCHEDULER
|
|
327
|
+
clock: Clock = REAL_CLOCK
|
|
328
|
+
stream: SourceResponsesStream | None = None
|
|
329
|
+
sent_at: float = 0.0
|
|
330
|
+
first_frame_at: float | None = None
|
|
331
|
+
# Set by ``settlement_stream`` when it hands the transport an event frame
|
|
332
|
+
# that carries content (``relayed_frame_delivers_content``); the cancel
|
|
333
|
+
# settlement policy keys on this alone. It is never mirrored from
|
|
334
|
+
# ``SourceUsageHolder.content_delivered`` (the body's flush point): the
|
|
335
|
+
# public wrapper above that point still drops vendor events, parks
|
|
336
|
+
# ``response.*`` frames ahead of ``response.created`` and holds reasoning
|
|
337
|
+
# deltas, so a chunk the body counted may never reach the client.
|
|
338
|
+
content_delivered: bool = False
|
|
339
|
+
delta_chars: int = 0
|
|
340
|
+
body_started: bool = False
|
|
341
|
+
finished: bool = False
|
|
342
|
+
# Reservation primitives are injected by the route (``api.py`` owns them and
|
|
343
|
+
# its tests patch them there); a dispatch that holds a reservation without
|
|
344
|
+
# them cannot settle and logs an error instead of leaking silently.
|
|
345
|
+
settle_reservation: ReservationSettler | None = None
|
|
346
|
+
release_reservation: ReservationReleaser | None = None
|
|
347
|
+
request_id: str | None = None
|
|
348
|
+
# Outermost body iterator handed to ``SourceStreamingResponse`` (closed by ``finalize_transport``).
|
|
349
|
+
body: AsyncIterator[str] | None = None
|
|
350
|
+
# Non-stream completions carry the source response id in the JSON body
|
|
351
|
+
# (streams expose it through the usage holder).
|
|
352
|
+
source_response_id: str | None = None
|
|
353
|
+
settlement_failed: bool = False
|
|
354
|
+
_source_closed: bool = field(default=False, init=False, repr=False)
|
|
355
|
+
_reservation_done: bool = field(default=False, init=False, repr=False)
|
|
356
|
+
_claims_released: bool = field(default=False, init=False, repr=False)
|
|
357
|
+
_result_recorded: bool = field(default=False, init=False, repr=False)
|
|
358
|
+
_row_written: bool = field(default=False, init=False, repr=False)
|
|
359
|
+
|
|
360
|
+
def __post_init__(self) -> None:
|
|
361
|
+
if self.request_id is None:
|
|
362
|
+
self.request_id = ensure_request_id()
|
|
363
|
+
|
|
364
|
+
# -- observations -----------------------------------------------------------
|
|
365
|
+
|
|
366
|
+
@property
|
|
367
|
+
def usage_holder(self) -> SourceUsageHolder | None:
|
|
368
|
+
return self.stream.usage_holder if self.stream is not None else None
|
|
369
|
+
|
|
370
|
+
def observe_stream(self) -> SourceUsageHolder | None:
|
|
371
|
+
"""Mirror the parser's observations (first frame, first output item, delta chars).
|
|
372
|
+
|
|
373
|
+
``content_delivered`` is deliberately not mirrored: the holder's flag is
|
|
374
|
+
the body's flush-point evidence, delivery is decided by
|
|
375
|
+
``settlement_stream`` from the frames it hands to the transport.
|
|
376
|
+
"""
|
|
377
|
+
|
|
378
|
+
holder = self.usage_holder
|
|
379
|
+
if holder is None:
|
|
380
|
+
return None
|
|
381
|
+
if self.first_frame_at is None and holder.first_frame_at is not None:
|
|
382
|
+
self.first_frame_at = holder.first_frame_at
|
|
383
|
+
self.delta_chars = max(self.delta_chars, holder.delta_chars)
|
|
384
|
+
return holder
|
|
385
|
+
|
|
386
|
+
# -- latched steps ----------------------------------------------------------------
|
|
387
|
+
|
|
388
|
+
async def close_source(self) -> None:
|
|
389
|
+
if self._source_closed:
|
|
390
|
+
return
|
|
391
|
+
self._source_closed = True
|
|
392
|
+
stream = self.stream
|
|
393
|
+
if stream is None:
|
|
394
|
+
return
|
|
395
|
+
try:
|
|
396
|
+
await _await_cleanup_deferring_cancellation(stream.aclose(), scheduler=self.scheduler)
|
|
397
|
+
except Exception:
|
|
398
|
+
logger.warning(
|
|
399
|
+
"model_source_dispatch_close_failed request_id=%s source_id=%s",
|
|
400
|
+
self.request_id,
|
|
401
|
+
self.source.id,
|
|
402
|
+
exc_info=True,
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
async def settle_or_release(self, status: DispatchStatus, usage: SourceUsage | None) -> None:
|
|
406
|
+
if self._reservation_done:
|
|
407
|
+
return
|
|
408
|
+
self._reservation_done = True
|
|
409
|
+
reservation = self.reservation
|
|
410
|
+
if reservation is None:
|
|
411
|
+
return
|
|
412
|
+
self.observe_stream()
|
|
413
|
+
if status == "error":
|
|
414
|
+
await self._release_reservation_step(reservation)
|
|
415
|
+
return
|
|
416
|
+
if usage is not None:
|
|
417
|
+
await self._settle_reservation_step(reservation, usage, cause=None)
|
|
418
|
+
return
|
|
419
|
+
if status == "success" and _reservation_requires_usage(reservation):
|
|
420
|
+
await self._settle_reservation_step(reservation, self._estimate(), cause="missing_usage")
|
|
421
|
+
return
|
|
422
|
+
if status == "cancelled" and self.content_delivered and _reservation_requires_usage(reservation):
|
|
423
|
+
# Keyed on a content frame the settlement layer handed to the
|
|
424
|
+
# transport, not on the parser having seen an output item nor on
|
|
425
|
+
# the body having flushed a chunk: a delta-only or completed-only
|
|
426
|
+
# answer that was relayed is charged; an output item still withheld
|
|
427
|
+
# ahead of an unfinished pin write, a vendor event the public
|
|
428
|
+
# contract dropped or a delta parked ahead of ``response.created``
|
|
429
|
+
# is not.
|
|
430
|
+
await self._settle_reservation_step(reservation, self._estimate(), cause="client_cancel")
|
|
431
|
+
return
|
|
432
|
+
await self._release_reservation_step(reservation)
|
|
433
|
+
|
|
434
|
+
def _estimate(self) -> SourceUsage:
|
|
435
|
+
return estimate_settlement_usage(admission_budget=self.admission_budget, delta_chars=self.delta_chars)
|
|
436
|
+
|
|
437
|
+
async def _settle_reservation_step(
|
|
438
|
+
self,
|
|
439
|
+
reservation: ApiKeyUsageReservationData,
|
|
440
|
+
usage: SourceUsage,
|
|
441
|
+
*,
|
|
442
|
+
cause: EstimateCause | None,
|
|
443
|
+
) -> None:
|
|
444
|
+
settle = self.settle_reservation
|
|
445
|
+
if settle is None:
|
|
446
|
+
logger.error(
|
|
447
|
+
"source_dispatch_missing_settler request_id=%s source_id=%s reservation_id=%s",
|
|
448
|
+
self.request_id,
|
|
449
|
+
self.source.id,
|
|
450
|
+
getattr(reservation, "reservation_id", None),
|
|
451
|
+
)
|
|
452
|
+
await self._release_reservation_step(reservation)
|
|
453
|
+
return
|
|
454
|
+
if cause is not None:
|
|
455
|
+
logger.warning(
|
|
456
|
+
"source_usage_missing_settled_at_estimate cause=%s request_id=%s source_id=%s key_id=%s model=%s "
|
|
457
|
+
"input_tokens=%d output_tokens=%d delta_chars=%d",
|
|
458
|
+
cause,
|
|
459
|
+
self.request_id,
|
|
460
|
+
self.source.id,
|
|
461
|
+
self.api_key.id if self.api_key is not None else None,
|
|
462
|
+
self.model,
|
|
463
|
+
usage.input_tokens,
|
|
464
|
+
usage.output_tokens,
|
|
465
|
+
self.delta_chars,
|
|
466
|
+
)
|
|
467
|
+
_inc(model_source_usage_estimated_total, source_id=self.source.id, cause=cause)
|
|
468
|
+
settled = False
|
|
469
|
+
try:
|
|
470
|
+
settled, _cancellation = await _await_result_deferring_cancellation(
|
|
471
|
+
settle(
|
|
472
|
+
reservation,
|
|
473
|
+
source=self.source,
|
|
474
|
+
model=self.model,
|
|
475
|
+
usage=usage,
|
|
476
|
+
cost_usd_override=source_usage_cost_usd(self.source, self.model, usage),
|
|
477
|
+
),
|
|
478
|
+
scheduler=self.scheduler,
|
|
479
|
+
)
|
|
480
|
+
except Exception:
|
|
481
|
+
logger.warning(
|
|
482
|
+
"source_dispatch_settlement_raised request_id=%s source_id=%s",
|
|
483
|
+
self.request_id,
|
|
484
|
+
self.source.id,
|
|
485
|
+
exc_info=True,
|
|
486
|
+
)
|
|
487
|
+
await self._release_reservation_step(reservation)
|
|
488
|
+
if not settled:
|
|
489
|
+
self.settlement_failed = True
|
|
490
|
+
|
|
491
|
+
async def _release_reservation_step(self, reservation: ApiKeyUsageReservationData) -> None:
|
|
492
|
+
release = self.release_reservation
|
|
493
|
+
if release is None:
|
|
494
|
+
logger.error(
|
|
495
|
+
"source_dispatch_missing_releaser request_id=%s source_id=%s reservation_id=%s",
|
|
496
|
+
self.request_id,
|
|
497
|
+
self.source.id,
|
|
498
|
+
getattr(reservation, "reservation_id", None),
|
|
499
|
+
)
|
|
500
|
+
return
|
|
501
|
+
try:
|
|
502
|
+
await _await_cleanup_deferring_cancellation(release(reservation), scheduler=self.scheduler)
|
|
503
|
+
except Exception:
|
|
504
|
+
logger.warning(
|
|
505
|
+
"source_dispatch_release_failed request_id=%s source_id=%s",
|
|
506
|
+
self.request_id,
|
|
507
|
+
self.source.id,
|
|
508
|
+
exc_info=True,
|
|
509
|
+
)
|
|
510
|
+
if self.cleanup_scheduler is not None:
|
|
511
|
+
self.cleanup_scheduler._schedule_cancel_safe_cleanup(
|
|
512
|
+
release(reservation),
|
|
513
|
+
action="source_dispatch_release_retry",
|
|
514
|
+
request_id=self.request_id or ensure_request_id(),
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
def release_claims(self) -> None:
|
|
518
|
+
if self._claims_released:
|
|
519
|
+
return
|
|
520
|
+
self._claims_released = True
|
|
521
|
+
try:
|
|
522
|
+
self.claims.release()
|
|
523
|
+
except Exception:
|
|
524
|
+
logger.warning("source_dispatch_claims_release_failed request_id=%s", self.request_id, exc_info=True)
|
|
525
|
+
|
|
526
|
+
def record_result(self, status: DispatchStatus) -> None:
|
|
527
|
+
if self._result_recorded:
|
|
528
|
+
return
|
|
529
|
+
self._result_recorded = True
|
|
530
|
+
_inc(model_source_dispatch_total, kind=DISPATCH_KIND, status=status)
|
|
531
|
+
|
|
532
|
+
async def write_row(
|
|
533
|
+
self,
|
|
534
|
+
*,
|
|
535
|
+
status: DispatchStatus,
|
|
536
|
+
error_code: str | None,
|
|
537
|
+
error_message: str | None,
|
|
538
|
+
usage: SourceUsage | None,
|
|
539
|
+
timings: SourceTimings | None,
|
|
540
|
+
upstream_status_code: int | None,
|
|
541
|
+
) -> None:
|
|
542
|
+
if self._row_written:
|
|
543
|
+
return
|
|
544
|
+
self._row_written = True
|
|
545
|
+
request = self.request
|
|
546
|
+
headers = request.headers
|
|
547
|
+
holder = self.usage_holder
|
|
548
|
+
proxy_request_id = self.request_id or ensure_request_id()
|
|
549
|
+
source_response_id = self.source_response_id
|
|
550
|
+
if source_response_id is None and holder is not None and holder.response_id:
|
|
551
|
+
source_response_id = holder.response_id
|
|
552
|
+
_useragent, _useragent_group, conversation_id = _request_log_client_fields(headers)
|
|
553
|
+
try:
|
|
554
|
+
async with get_background_session() as session:
|
|
555
|
+
await RequestLogsRepository(session).add_log(
|
|
556
|
+
account_id=None,
|
|
557
|
+
request_id=source_response_id or proxy_request_id,
|
|
558
|
+
archive_request_id=proxy_request_id,
|
|
559
|
+
model_source_id=self.source.id,
|
|
560
|
+
model_source_kind=self.source.kind,
|
|
561
|
+
api_key_id=self.api_key.id if self.api_key is not None else None,
|
|
562
|
+
session_id=_owner_lookup_session_id_from_headers(headers),
|
|
563
|
+
model=self.model,
|
|
564
|
+
input_tokens=usage.input_tokens if usage is not None else None,
|
|
565
|
+
output_tokens=usage.output_tokens if usage is not None else None,
|
|
566
|
+
cached_input_tokens=usage.cached_input_tokens if usage is not None else None,
|
|
567
|
+
cost_usd=source_usage_cost_usd(self.source, self.model, usage),
|
|
568
|
+
latency_ms=timings.latency_ms if timings is not None else None,
|
|
569
|
+
latency_first_token_ms=timings.latency_first_token_ms if timings is not None else None,
|
|
570
|
+
status=status,
|
|
571
|
+
error_code=error_code,
|
|
572
|
+
error_message=error_message,
|
|
573
|
+
upstream_status_code=upstream_status_code,
|
|
574
|
+
transport="http",
|
|
575
|
+
upstream_transport="openai_compatible_http",
|
|
576
|
+
source=REQUEST_LOG_SOURCE,
|
|
577
|
+
requested_service_tier=self.requested_service_tier,
|
|
578
|
+
service_tier=None,
|
|
579
|
+
useragent=headers.get("user-agent"),
|
|
580
|
+
conversation_id=conversation_id,
|
|
581
|
+
client_ip=resolve_request_client_host(request),
|
|
582
|
+
)
|
|
583
|
+
except Exception:
|
|
584
|
+
logger.warning(
|
|
585
|
+
"failed to write source dispatch request log request_id=%s source_id=%s model=%s status=%s",
|
|
586
|
+
proxy_request_id,
|
|
587
|
+
self.source.id,
|
|
588
|
+
self.model,
|
|
589
|
+
status,
|
|
590
|
+
exc_info=True,
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
# -- the latch ----------------------------------------------------------------------
|
|
594
|
+
|
|
595
|
+
async def finish(
|
|
596
|
+
self,
|
|
597
|
+
*,
|
|
598
|
+
status: DispatchStatus,
|
|
599
|
+
error_code: str | None = None,
|
|
600
|
+
error_message: str | None = None,
|
|
601
|
+
usage: SourceUsage | None = None,
|
|
602
|
+
upstream_status_code: int | None = None,
|
|
603
|
+
timings: SourceTimings | None = None,
|
|
604
|
+
) -> None:
|
|
605
|
+
"""``close_source -> settle_or_release -> release_claims + record_result -> write_row``.
|
|
606
|
+
|
|
607
|
+
Idempotent (``finished`` is the latch).
|
|
608
|
+
|
|
609
|
+
Every step is awaited with cancellation deferred and isolated from the
|
|
610
|
+
others: a failing release never skips the row, a failing row write
|
|
611
|
+
never raises. A ``success`` whose settlement fell back to a release is
|
|
612
|
+
recorded as ``error usage_settlement_failed``.
|
|
613
|
+
"""
|
|
614
|
+
|
|
615
|
+
if self.finished:
|
|
616
|
+
return
|
|
617
|
+
self.finished = True
|
|
618
|
+
holder = self.observe_stream()
|
|
619
|
+
if usage is None and holder is not None:
|
|
620
|
+
usage = holder.usage
|
|
621
|
+
if timings is None and holder is not None:
|
|
622
|
+
timings = holder.timings
|
|
623
|
+
if upstream_status_code is None and self.stream is not None:
|
|
624
|
+
upstream_status_code = self.stream.upstream_status_code
|
|
625
|
+
try:
|
|
626
|
+
await _await_cleanup_deferring_cancellation(self.close_source(), scheduler=self.scheduler)
|
|
627
|
+
finally:
|
|
628
|
+
try:
|
|
629
|
+
await _await_cleanup_deferring_cancellation(
|
|
630
|
+
self.settle_or_release(status, usage), scheduler=self.scheduler
|
|
631
|
+
)
|
|
632
|
+
finally:
|
|
633
|
+
if self.settlement_failed:
|
|
634
|
+
status = "error"
|
|
635
|
+
error_code = ERROR_USAGE_SETTLEMENT_FAILED
|
|
636
|
+
error_message = "source usage settlement failed"
|
|
637
|
+
try:
|
|
638
|
+
self.release_claims()
|
|
639
|
+
self.record_result(status)
|
|
640
|
+
finally:
|
|
641
|
+
await _await_cleanup_deferring_cancellation(
|
|
642
|
+
self.write_row(
|
|
643
|
+
status=status,
|
|
644
|
+
error_code=error_code,
|
|
645
|
+
error_message=error_message,
|
|
646
|
+
usage=usage,
|
|
647
|
+
timings=timings,
|
|
648
|
+
upstream_status_code=upstream_status_code,
|
|
649
|
+
),
|
|
650
|
+
scheduler=self.scheduler,
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
async def finish_with_forwarding_error(self, exc: ModelSourceForwardingError) -> None:
|
|
654
|
+
"""``finish("error")`` for a pre-body or mid-stream ``ModelSourceForwardingError`` (deadlines counted)."""
|
|
655
|
+
|
|
656
|
+
if exc.timeout_phase is not None and not self.finished:
|
|
657
|
+
_inc(model_source_timeout_total, phase=exc.timeout_phase)
|
|
658
|
+
await self.finish(
|
|
659
|
+
status="error",
|
|
660
|
+
error_code=error_code_from_payload(exc.payload),
|
|
661
|
+
error_message=error_message_from_payload(exc.payload),
|
|
662
|
+
upstream_status_code=exc.upstream_status_code,
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
async def abandon(self, reason: str) -> None:
|
|
666
|
+
"""Cancellation-class ``finish()`` for a client that left during the open / before the body."""
|
|
667
|
+
|
|
668
|
+
if self.finished:
|
|
669
|
+
return
|
|
670
|
+
pending_seconds = max(0.0, self.clock.monotonic() - self.sent_at) if self.sent_at else 0.0
|
|
671
|
+
stage = _ABANDON_STAGES.get(reason, "during_open")
|
|
672
|
+
logger.warning(
|
|
673
|
+
"source_dispatch_abandoned stage=%s reason=%s pending_seconds=%.3f frame_seen=%s "
|
|
674
|
+
"request_id=%s source_id=%s",
|
|
675
|
+
stage,
|
|
676
|
+
reason,
|
|
677
|
+
pending_seconds,
|
|
678
|
+
self.first_frame_at is not None,
|
|
679
|
+
self.request_id,
|
|
680
|
+
self.source.id,
|
|
681
|
+
)
|
|
682
|
+
_inc(model_source_dispatch_abandoned_total, stage=stage)
|
|
683
|
+
await self.finish(
|
|
684
|
+
status="cancelled",
|
|
685
|
+
error_code=reason,
|
|
686
|
+
error_message="client left before the model-source response was delivered",
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
async def finalize_transport(self) -> None:
|
|
690
|
+
"""``aclose()`` the body, then ``finish("cancelled", "client_disconnected_before_body")`` if unfinished.
|
|
691
|
+
|
|
692
|
+
``aclose()`` on a started body drives the settlement generator's own
|
|
693
|
+
cleanup (which calls ``finish``); on a never-started body it skips the
|
|
694
|
+
generator entirely, which is why the unconditional check follows.
|
|
695
|
+
"""
|
|
696
|
+
|
|
697
|
+
body = self.body
|
|
698
|
+
if body is not None:
|
|
699
|
+
aclose = getattr(body, "aclose", None)
|
|
700
|
+
if aclose is not None:
|
|
701
|
+
try:
|
|
702
|
+
await _await_cleanup_deferring_cancellation(aclose(), scheduler=self.scheduler)
|
|
703
|
+
except Exception:
|
|
704
|
+
logger.warning(
|
|
705
|
+
"source_dispatch_body_close_failed request_id=%s source_id=%s",
|
|
706
|
+
self.request_id,
|
|
707
|
+
self.source.id,
|
|
708
|
+
exc_info=True,
|
|
709
|
+
)
|
|
710
|
+
if not self.finished:
|
|
711
|
+
await self.abandon(ABANDON_CLIENT_DISCONNECTED_BEFORE_BODY)
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
class TransportOwner(Protocol):
|
|
715
|
+
"""Owner of a source transport whose ``finalize_transport()`` a ``SourceStreamingResponse`` always reaches.
|
|
716
|
+
|
|
717
|
+
``body`` is the outermost iterator handed to Starlette (assigned by the
|
|
718
|
+
response); ``finalize_transport`` closes it and settles whatever a body that
|
|
719
|
+
never started could not settle itself. ``SourceDispatch`` implements it for
|
|
720
|
+
Responses dispatch, ``SourceChatStreamOwner`` for the direct chat route.
|
|
721
|
+
"""
|
|
722
|
+
|
|
723
|
+
scheduler: Scheduler
|
|
724
|
+
body: AsyncIterator[Any] | None
|
|
725
|
+
|
|
726
|
+
async def finalize_transport(self) -> None: ...
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
@dataclass(slots=True)
|
|
730
|
+
class SourceChatStreamOwner:
|
|
731
|
+
"""Transport owner for the direct chat-completions stream route, which has no dispatch owner.
|
|
732
|
+
|
|
733
|
+
The route's settlement generator wrapped around ``stream.body`` is the sole
|
|
734
|
+
body iterator: it releases the reservation and writes the row, but only
|
|
735
|
+
once Starlette iterates the body. Between the route returning and
|
|
736
|
+
``http.response.start`` completing there is one await in which a client
|
|
737
|
+
departure (task cancellation) or a failed first write leaves the generator
|
|
738
|
+
never started, and ``aclose()`` on a never-started async generator skips
|
|
739
|
+
its ``finally`` -- the upstream response, the pooled lease and the
|
|
740
|
+
reservation were held until garbage collection. ``finalize_transport``
|
|
741
|
+
mirrors ``SourceDispatch.finalize_transport``: it closes the body (a
|
|
742
|
+
started body settles itself), then, when the generator never ran, closes
|
|
743
|
+
the source transport directly and runs ``on_abandoned_before_body`` (the
|
|
744
|
+
route's reservation release and ``cancelled`` row). Every await defers
|
|
745
|
+
cancellation; the finalizer is idempotent.
|
|
746
|
+
"""
|
|
747
|
+
|
|
748
|
+
stream: SourceChatStream
|
|
749
|
+
# Route-owned: releases the reservation and writes the ``cancelled`` row
|
|
750
|
+
# with ``ABANDON_CLIENT_DISCONNECTED_BEFORE_BODY`` (``api.py`` owns both).
|
|
751
|
+
on_abandoned_before_body: Callable[[], Awaitable[None]]
|
|
752
|
+
scheduler: Scheduler = REAL_SCHEDULER
|
|
753
|
+
# Outermost body iterator handed to ``SourceStreamingResponse`` (assigned by it).
|
|
754
|
+
body: AsyncIterator[bytes] | None = None
|
|
755
|
+
# Set by the settlement generator on entry: from then on it owns the outcome
|
|
756
|
+
# through its own except/finally paths, whether it completes, is cancelled
|
|
757
|
+
# mid-stream or is closed by the finalizer below.
|
|
758
|
+
body_started: bool = False
|
|
759
|
+
# Latch for the abandoned-before-body outcome recorded by the finalizer.
|
|
760
|
+
finished: bool = False
|
|
761
|
+
|
|
762
|
+
async def finalize_transport(self) -> None:
|
|
763
|
+
body = self.body
|
|
764
|
+
if body is not None:
|
|
765
|
+
aclose = getattr(body, "aclose", None)
|
|
766
|
+
if aclose is not None:
|
|
767
|
+
try:
|
|
768
|
+
await _await_cleanup_deferring_cancellation(aclose(), scheduler=self.scheduler)
|
|
769
|
+
except Exception:
|
|
770
|
+
logger.warning("source_chat_stream_body_close_failed", exc_info=True)
|
|
771
|
+
if self.body_started or self.finished:
|
|
772
|
+
return
|
|
773
|
+
self.finished = True
|
|
774
|
+
try:
|
|
775
|
+
await _await_cleanup_deferring_cancellation(self.stream.aclose(), scheduler=self.scheduler)
|
|
776
|
+
except Exception:
|
|
777
|
+
logger.warning("source_chat_stream_close_failed", exc_info=True)
|
|
778
|
+
await _await_cleanup_deferring_cancellation(self.on_abandoned_before_body(), scheduler=self.scheduler)
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
class SourceStreamingResponse(StreamingResponse):
|
|
782
|
+
"""``StreamingResponse`` whose transport exit always reaches ``owner.finalize_transport()``."""
|
|
783
|
+
|
|
784
|
+
def __init__(
|
|
785
|
+
self,
|
|
786
|
+
content: AsyncIterator[str] | AsyncIterator[bytes],
|
|
787
|
+
*,
|
|
788
|
+
owner: TransportOwner,
|
|
789
|
+
media_type: str = "text/event-stream",
|
|
790
|
+
headers: Mapping[str, str] | None = None,
|
|
791
|
+
) -> None:
|
|
792
|
+
super().__init__(content, media_type=media_type, headers=headers)
|
|
793
|
+
self.owner = owner
|
|
794
|
+
owner.body = content
|
|
795
|
+
|
|
796
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
797
|
+
try:
|
|
798
|
+
await super().__call__(scope, receive, send)
|
|
799
|
+
finally:
|
|
800
|
+
await _await_cleanup_deferring_cancellation(self.owner.finalize_transport(), scheduler=self.owner.scheduler)
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
async def open_with_disconnect_watch(request: Request, owner: SourceDispatch, coro: Coroutine[Any, Any, T]) -> T:
|
|
804
|
+
"""Run the source open while polling ``request.is_disconnected()``; raises ``ClientDisconnectedDuringOpen``.
|
|
805
|
+
|
|
806
|
+
Owned concurrency (CL-5): the open runs as a task the watch always cancels
|
|
807
|
+
and awaits to completion, so a stream that completed into a leaving caller
|
|
808
|
+
is closed by ``abandon()`` (the open coroutine assigns ``owner.stream``
|
|
809
|
+
itself before it completes), and the child's exception never replaces the
|
|
810
|
+
caller's.
|
|
811
|
+
"""
|
|
812
|
+
|
|
813
|
+
if not owner.sent_at:
|
|
814
|
+
owner.sent_at = owner.clock.monotonic()
|
|
815
|
+
task = owner.scheduler.create_task(coro)
|
|
816
|
+
consumed = False
|
|
817
|
+
try:
|
|
818
|
+
while True:
|
|
819
|
+
done, _pending = await owner.scheduler.wait({task}, timeout=OPEN_DISCONNECT_POLL_SECONDS)
|
|
820
|
+
if done:
|
|
821
|
+
# Completion observed before the poll is never dropped.
|
|
822
|
+
consumed = True
|
|
823
|
+
return task.result()
|
|
824
|
+
if await request.is_disconnected():
|
|
825
|
+
pending_seconds = max(0.0, owner.clock.monotonic() - owner.sent_at)
|
|
826
|
+
stall = owner.first_frame_at is None and pending_seconds >= STALL_EVIDENCE_SECONDS
|
|
827
|
+
raise ClientDisconnectedDuringOpen(pending_seconds=pending_seconds, stall=stall)
|
|
828
|
+
finally:
|
|
829
|
+
if not task.done():
|
|
830
|
+
task.cancel()
|
|
831
|
+
if not consumed:
|
|
832
|
+
# Always awaited: a child that completed in the disconnect
|
|
833
|
+
# iteration has assigned ``owner.stream`` (closed by ``abandon()``);
|
|
834
|
+
# a child that failed is logged and never replaces our exception.
|
|
835
|
+
try:
|
|
836
|
+
await _await_task_deferring_cancellation(task)
|
|
837
|
+
except asyncio.CancelledError:
|
|
838
|
+
pass
|
|
839
|
+
except Exception:
|
|
840
|
+
logger.warning(
|
|
841
|
+
"model_source_open_failed_after_abandonment request_id=%s source_id=%s",
|
|
842
|
+
owner.request_id,
|
|
843
|
+
owner.source.id,
|
|
844
|
+
exc_info=True,
|
|
845
|
+
)
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
async def _aclose_best_effort(stream: object, *, scheduler: Scheduler) -> None:
|
|
849
|
+
aclose = getattr(stream, "aclose", None)
|
|
850
|
+
if aclose is None:
|
|
851
|
+
return
|
|
852
|
+
try:
|
|
853
|
+
await _await_cleanup_deferring_cancellation(aclose(), scheduler=scheduler)
|
|
854
|
+
except Exception:
|
|
855
|
+
logger.debug("source stream layer close failed", exc_info=True)
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
async def settlement_stream(owner: SourceDispatch, wrapped: AsyncIterator[str]) -> AsyncIterator[str]:
|
|
859
|
+
"""Outermost body layer: settles/releases on the terminal outcome and calls ``owner.finish()`` in ``finally``.
|
|
860
|
+
|
|
861
|
+
A disconnect surfaces as ``CancelledError`` (task cancellation) or
|
|
862
|
+
``GeneratorExit`` (``aclose()`` from ``finalize_transport``); both bypass
|
|
863
|
+
``except Exception`` and are recorded as ``cancelled`` (the settle policy
|
|
864
|
+
decides between release and estimate) unless the client already received a
|
|
865
|
+
terminal: a relayed failure terminal is an ``error``, a relayed success
|
|
866
|
+
terminal a ``success``.
|
|
867
|
+
|
|
868
|
+
Delivery is decided here as well: ``owner.content_delivered`` is set when
|
|
869
|
+
an event frame ``relayed_frame_delivers_content`` classifies as content is
|
|
870
|
+
handed to the transport, so the cancel estimate charges only for frames
|
|
871
|
+
that left the proxy. The body's flush point sits below the public wrapper,
|
|
872
|
+
which drops events outside ``response.*`` / ``error`` under the SDK
|
|
873
|
+
contract, parks ``response.*`` frames ahead of ``response.created`` and
|
|
874
|
+
holds reasoning-summary deltas; a chunk the body flushed may therefore
|
|
875
|
+
never reach the client (the one ``send()`` in flight when the client
|
|
876
|
+
leaves is the residual ambiguity, resolved as delivered).
|
|
877
|
+
|
|
878
|
+
The client-visible terminal is latched from the relayed event frames, not
|
|
879
|
+
read from the last one: a typed event trailing a terminal (a delta the
|
|
880
|
+
source keeps emitting after the wrapper rewrote its ``response.completed``
|
|
881
|
+
into ``response.failed``, a vendor frame after ``response.completed``)
|
|
882
|
+
must not reset the classification, and a relayed failure terminal is
|
|
883
|
+
sticky -- once the client received a failure, nothing that follows turns
|
|
884
|
+
the attempt back into a charged success. The latch costs one
|
|
885
|
+
``startswith`` and a string slice per ``event:``-framed event; only a raw
|
|
886
|
+
data-only pass-through block (a native-mode terminal, an unparseable
|
|
887
|
+
block) pays a JSON parse.
|
|
888
|
+
"""
|
|
889
|
+
|
|
890
|
+
status: DispatchStatus = "success"
|
|
891
|
+
error_code: str | None = None
|
|
892
|
+
error_message: str | None = None
|
|
893
|
+
completed_normally = False
|
|
894
|
+
timeout_phase: TimeoutPhase | None = None
|
|
895
|
+
relayed_kind: str | None = None
|
|
896
|
+
holder = owner.usage_holder
|
|
897
|
+
owner.body_started = True
|
|
898
|
+
try:
|
|
899
|
+
async for chunk in wrapped:
|
|
900
|
+
if _is_event_frame(chunk):
|
|
901
|
+
if relayed_kind not in _FAILURE_TERMINAL_KINDS:
|
|
902
|
+
frame_kind = relayed_terminal_kind(chunk)
|
|
903
|
+
if frame_kind is not None:
|
|
904
|
+
relayed_kind = frame_kind
|
|
905
|
+
if not owner.content_delivered and relayed_frame_delivers_content(chunk):
|
|
906
|
+
owner.content_delivered = True
|
|
907
|
+
yield chunk
|
|
908
|
+
completed_normally = True
|
|
909
|
+
holder = owner.observe_stream()
|
|
910
|
+
if holder is not None:
|
|
911
|
+
if holder.terminal_kind in _FAILURE_TERMINAL_KINDS:
|
|
912
|
+
# The public wrapper relays a failure terminal and ends the
|
|
913
|
+
# stream normally; a limited key must not be charged (not even
|
|
914
|
+
# at the estimate) for an answer the source never produced.
|
|
915
|
+
status = "error"
|
|
916
|
+
error_code = ERROR_MODEL_SOURCE_RESPONSE_FAILED
|
|
917
|
+
error_message = f"source terminated the stream with response.{holder.terminal_kind}"
|
|
918
|
+
elif holder.terminal_kind in _SUCCESS_TERMINAL_KINDS and relayed_kind in _FAILURE_TERMINAL_KINDS:
|
|
919
|
+
# The parser read the source's success terminal, but the public
|
|
920
|
+
# contract rewrote it into ``response.failed`` (``response`` not
|
|
921
|
+
# an object, invalid output items): the client received a
|
|
922
|
+
# failure, so the row and the settlement follow the wire, not
|
|
923
|
+
# the parser's observation.
|
|
924
|
+
status = "error"
|
|
925
|
+
error_code = ERROR_MODEL_SOURCE_RESPONSE_INVALID
|
|
926
|
+
error_message = f"source response.{holder.terminal_kind} was relayed to the client as response.failed"
|
|
927
|
+
elif holder.terminal_kind is None and relayed_kind not in _SUCCESS_TERMINAL_KINDS:
|
|
928
|
+
# The source closed without a terminal the parser could read
|
|
929
|
+
# and the client did not receive a success terminal either (the
|
|
930
|
+
# wrapper synthesized ``response.failed`` or relayed nothing
|
|
931
|
+
# usable): no answer was delivered, so this is an error and the
|
|
932
|
+
# reservation is released. A success terminal that only
|
|
933
|
+
# outgrew the parser's frame cap still reached the client and
|
|
934
|
+
# stays a success (settled at the estimate for a limited key).
|
|
935
|
+
status = "error"
|
|
936
|
+
error_code = ERROR_MODEL_SOURCE_STREAM_TRUNCATED
|
|
937
|
+
error_message = "source ended the stream without a terminal event"
|
|
938
|
+
except (asyncio.CancelledError, GeneratorExit):
|
|
939
|
+
# A client that disconnects right after a relayed failure terminal --
|
|
940
|
+
# Codex tears the stream down on ``response.failed``/``error`` while
|
|
941
|
+
# many sources still send ``[DONE]``/keepalives before their own EOF --
|
|
942
|
+
# received a failure, not a partial answer, so this is an ``error`` that
|
|
943
|
+
# releases the reservation, never a cancel-after-first-item estimate
|
|
944
|
+
# (design §6.4; api-keys "Failure terminal is never charged"). The
|
|
945
|
+
# completed-normally branch already classifies this.
|
|
946
|
+
owner.observe_stream()
|
|
947
|
+
holder = owner.usage_holder
|
|
948
|
+
if (
|
|
949
|
+
holder is not None and holder.terminal_kind in _FAILURE_TERMINAL_KINDS
|
|
950
|
+
) or relayed_kind in _FAILURE_TERMINAL_KINDS:
|
|
951
|
+
status = "error"
|
|
952
|
+
error_code = ERROR_MODEL_SOURCE_RESPONSE_FAILED
|
|
953
|
+
error_message = "source terminated the stream with a failure terminal"
|
|
954
|
+
elif relayed_kind in _SUCCESS_TERMINAL_KINDS:
|
|
955
|
+
# Symmetric: the client received the success terminal before
|
|
956
|
+
# leaving (Codex tears the stream down on ``response.completed``
|
|
957
|
+
# while the source still sends ``[DONE]``/keepalives), so the whole
|
|
958
|
+
# answer was delivered and this is a success settled like a stream
|
|
959
|
+
# that ran to the source's EOF, never a cancel.
|
|
960
|
+
status = "success"
|
|
961
|
+
else:
|
|
962
|
+
status = "cancelled"
|
|
963
|
+
error_code = CANCELLED_CLIENT_DISCONNECTED
|
|
964
|
+
error_message = "client disconnected before stream completed"
|
|
965
|
+
# ``async for`` does not close the inner layer on an exception; the
|
|
966
|
+
# wrapper's own ``finally`` blocks release everything below it.
|
|
967
|
+
await _aclose_best_effort(wrapped, scheduler=owner.scheduler)
|
|
968
|
+
raise
|
|
969
|
+
except ModelSourceForwardingError as exc:
|
|
970
|
+
status = "error"
|
|
971
|
+
error_code = error_code_from_payload(exc.payload)
|
|
972
|
+
error_message = error_message_from_payload(exc.payload)
|
|
973
|
+
timeout_phase = exc.timeout_phase
|
|
974
|
+
raise
|
|
975
|
+
except Exception as exc:
|
|
976
|
+
status = "error"
|
|
977
|
+
error_code = ERROR_MODEL_SOURCE_STREAM
|
|
978
|
+
error_message = exc.__class__.__name__
|
|
979
|
+
raise
|
|
980
|
+
finally:
|
|
981
|
+
if owner.finished:
|
|
982
|
+
pass
|
|
983
|
+
else:
|
|
984
|
+
if timeout_phase is not None:
|
|
985
|
+
_inc(model_source_timeout_total, phase=timeout_phase)
|
|
986
|
+
owner.observe_stream()
|
|
987
|
+
cancellation = await _await_cleanup_deferring_cancellation(
|
|
988
|
+
owner.finish(
|
|
989
|
+
status=status,
|
|
990
|
+
error_code=error_code,
|
|
991
|
+
error_message=error_message,
|
|
992
|
+
),
|
|
993
|
+
scheduler=owner.scheduler,
|
|
994
|
+
)
|
|
995
|
+
if completed_normally and cancellation is not None:
|
|
996
|
+
# The client left while the settlement was being committed: the
|
|
997
|
+
# reservation is settled, the row is written; honour the
|
|
998
|
+
# cancellation instead of continuing to a departed transport.
|
|
999
|
+
raise asyncio.CancelledError
|