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,1054 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from typing import NamedTuple
|
|
6
|
+
|
|
7
|
+
from pydantic import ValidationError
|
|
8
|
+
|
|
9
|
+
from app.core.errors import OpenAIErrorEnvelope, openai_error
|
|
10
|
+
from app.core.exceptions import ProxyModelNotAllowed, ProxyReasoningEffortNotAllowed
|
|
11
|
+
from app.core.openai.exceptions import ClientPayloadError
|
|
12
|
+
from app.core.openai.model_registry import ModelRegistry, canonical_service_tier_value, get_model_registry
|
|
13
|
+
from app.core.openai.requests import (
|
|
14
|
+
ResponsesCompactRequest,
|
|
15
|
+
ResponsesReasoning,
|
|
16
|
+
ResponsesRequest,
|
|
17
|
+
extract_input_file_ids,
|
|
18
|
+
normalize_reasoning_aliases,
|
|
19
|
+
responses_input_uses_lite_tools,
|
|
20
|
+
)
|
|
21
|
+
from app.core.openai.strict_schema import (
|
|
22
|
+
validate_strict_function_tool_schema,
|
|
23
|
+
validate_strict_json_schema,
|
|
24
|
+
)
|
|
25
|
+
from app.core.openai.v1_requests import V1ResponsesRequest
|
|
26
|
+
from app.core.types import JsonValue
|
|
27
|
+
from app.core.utils.json_guards import is_json_list, is_json_mapping
|
|
28
|
+
from app.core.utils.request_id import get_request_id
|
|
29
|
+
from app.db.models import ModelSource
|
|
30
|
+
from app.modules.api_keys.service import ApiKeyData
|
|
31
|
+
from app.modules.model_sources.catalog import source_model_reasoning_levels
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
# Reasoning efforts that the upstream ChatGPT/Codex backend silently drops
|
|
36
|
+
# (the WebSocket never produces ``response.completed``). When a client sends
|
|
37
|
+
# one of these we transparently rewrite it to a value the resolved model
|
|
38
|
+
# advertises in its ``supported_reasoning_levels`` so the request does not
|
|
39
|
+
# hang. ``minimal`` is a valid value on the OpenAI Platform Responses API for
|
|
40
|
+
# GPT-5 family models, but the ChatGPT backend codex-lb proxies to does not
|
|
41
|
+
# accept it as of 2026-04. See https://github.com/Soju06/codex-lb/issues/493
|
|
42
|
+
_UNSUPPORTED_UPSTREAM_REASONING_EFFORTS: frozenset[str] = frozenset({"minimal"})
|
|
43
|
+
_DEFAULT_REASONING_EFFORT_FALLBACK = "low"
|
|
44
|
+
|
|
45
|
+
# Client-plane reasoning efforts the reference Codex client never sends on the
|
|
46
|
+
# wire. GPT-5.6 Sol/Terra advertise ``ultra`` in their catalog entries, but the
|
|
47
|
+
# official client rewrites it to ``max`` before building the Responses request
|
|
48
|
+
# (``reasoning_effort_for_request`` in codex-rs ``core/src/client.rs`` at
|
|
49
|
+
# rust-v0.144.1); ``ultra``'s extra effect (proactive multi-agent mode) is
|
|
50
|
+
# purely client-side. Mirror that aliasing for API-key enforcement and raw
|
|
51
|
+
# API callers so the upstream backend only ever sees wire-safe values.
|
|
52
|
+
_REASONING_EFFORT_WIRE_ALIASES: dict[str, str] = {"ultra": "max"}
|
|
53
|
+
|
|
54
|
+
# Cursor exposes GPT-5 family model labels with UI suffixes such as "Extra
|
|
55
|
+
# High Fast". The ChatGPT/Codex upstream accepts the canonical GPT-5-family
|
|
56
|
+
# slug plus request fields, not those synthetic suffixes in the model name.
|
|
57
|
+
# Keep this deliberately narrow: only strip known Cursor-style suffix tokens
|
|
58
|
+
# from known GPT-5 base model slugs, and leave every other model untouched.
|
|
59
|
+
_GPT5_ALIAS_BASE_MODELS: tuple[str, ...] = (
|
|
60
|
+
"gpt-5.6-sol",
|
|
61
|
+
"gpt-5.6-terra",
|
|
62
|
+
"gpt-5.6-luna",
|
|
63
|
+
"gpt-5.4-mini",
|
|
64
|
+
"gpt-5.3-codex",
|
|
65
|
+
"gpt-5.2-codex",
|
|
66
|
+
"gpt-5.1-codex-max",
|
|
67
|
+
"gpt-5.1-codex-mini",
|
|
68
|
+
"gpt-5.1-codex",
|
|
69
|
+
"gpt-5-codex",
|
|
70
|
+
"gpt-5.5",
|
|
71
|
+
"gpt-5.4",
|
|
72
|
+
"gpt-5.3",
|
|
73
|
+
"gpt-5.2",
|
|
74
|
+
"gpt-5.1",
|
|
75
|
+
"gpt-5",
|
|
76
|
+
)
|
|
77
|
+
_MODEL_ALIAS_REASONING_TOKENS: dict[str, str] = {
|
|
78
|
+
"minimal": "minimal",
|
|
79
|
+
"low": "low",
|
|
80
|
+
"medium": "medium",
|
|
81
|
+
"high": "high",
|
|
82
|
+
"xhigh": "high",
|
|
83
|
+
"extra": "high",
|
|
84
|
+
}
|
|
85
|
+
_MODEL_ALIAS_REASONING_RANK: dict[str, int] = {"minimal": 0, "low": 1, "medium": 2, "high": 3}
|
|
86
|
+
_MODEL_ALIAS_SERVICE_TIER_TOKENS: dict[str, str] = {
|
|
87
|
+
"fast": "priority",
|
|
88
|
+
"priority": "priority",
|
|
89
|
+
}
|
|
90
|
+
_MODEL_ALIAS_IGNORED_TOKENS: frozenset[str] = frozenset({"reasoning", "thinking"})
|
|
91
|
+
_MODEL_ALIAS_TOKENS: frozenset[str] = frozenset(
|
|
92
|
+
{
|
|
93
|
+
*tuple(_MODEL_ALIAS_REASONING_TOKENS),
|
|
94
|
+
*tuple(_MODEL_ALIAS_SERVICE_TIER_TOKENS),
|
|
95
|
+
*tuple(_MODEL_ALIAS_IGNORED_TOKENS),
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Service tier values codex-lb accepts at the API-key surface but that the
|
|
100
|
+
# ChatGPT/Codex backend rejects with ``Unsupported service_tier: <value>``.
|
|
101
|
+
# Semantically both ``auto`` and ``default`` mean "let upstream pick" -- the
|
|
102
|
+
# same thing as omitting the field entirely -- so when an enforced API-key
|
|
103
|
+
# policy resolves to one of these, we forward the request without a
|
|
104
|
+
# ``service_tier`` instead of sending a literal that fails upstream. See
|
|
105
|
+
# https://github.com/Soju06/codex-lb/issues/546
|
|
106
|
+
_UPSTREAM_OMIT_SERVICE_TIERS: frozenset[str] = frozenset({"auto", "default"})
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def resolve_wire_reasoning_effort(effort: str) -> str:
|
|
110
|
+
"""Return the wire-safe value for a client-plane reasoning effort.
|
|
111
|
+
|
|
112
|
+
The reference Codex client rewrites client-plane efforts (``ultra`` ->
|
|
113
|
+
``max``) before building the upstream Responses request; every codex-lb
|
|
114
|
+
code path that builds an upstream payload directly (proxy enforcement,
|
|
115
|
+
automation compact pings) applies the same aliasing so the upstream
|
|
116
|
+
backend never sees a client-plane literal. Unknown values pass through
|
|
117
|
+
unchanged.
|
|
118
|
+
"""
|
|
119
|
+
return _REASONING_EFFORT_WIRE_ALIASES.get(effort.strip().lower(), effort)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def validate_model_access(api_key: ApiKeyData | None, model: str | None) -> None:
|
|
123
|
+
if api_key is None:
|
|
124
|
+
return
|
|
125
|
+
if not api_key.allowed_models:
|
|
126
|
+
return
|
|
127
|
+
allowed_models = {resolve_model_alias(allowed_model) for allowed_model in api_key.allowed_models}
|
|
128
|
+
effective_model = resolve_model_alias(model)
|
|
129
|
+
if model is None or effective_model in allowed_models:
|
|
130
|
+
return
|
|
131
|
+
raise ProxyModelNotAllowed(f"This API key does not have access to model '{model}'")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def validate_reasoning_effort_access(api_key: ApiKeyData | None, effort: str | None) -> None:
|
|
135
|
+
if api_key is None:
|
|
136
|
+
return
|
|
137
|
+
allowed_reasoning_efforts = getattr(api_key, "allowed_reasoning_efforts", None)
|
|
138
|
+
if allowed_reasoning_efforts is None or effort is None:
|
|
139
|
+
return
|
|
140
|
+
normalized_effort = effort.strip().lower()
|
|
141
|
+
if normalized_effort in allowed_reasoning_efforts:
|
|
142
|
+
return
|
|
143
|
+
logger.info(
|
|
144
|
+
"api_key_reasoning_effort_not_allowed request_id=%s key_id=%s reasoning_effort=%s",
|
|
145
|
+
get_request_id(),
|
|
146
|
+
api_key.id,
|
|
147
|
+
normalized_effort,
|
|
148
|
+
)
|
|
149
|
+
raise ProxyReasoningEffortNotAllowed(
|
|
150
|
+
f"This API key does not have access to reasoning effort '{normalized_effort}'",
|
|
151
|
+
param="reasoning.effort",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _client_reasoning_effort(payload: ResponsesRequest | ResponsesCompactRequest) -> str | None:
|
|
156
|
+
"""Return the effort selected by the client before wire normalization.
|
|
157
|
+
|
|
158
|
+
Cursor encodes its effort in an accepted model alias, where ``xhigh`` is
|
|
159
|
+
later lowered to the upstream's ``high`` value. API-key policies are an
|
|
160
|
+
operator-facing client-plane control, so they must compare against the
|
|
161
|
+
original selection rather than that wire representation.
|
|
162
|
+
"""
|
|
163
|
+
model_effort = _client_reasoning_effort_from_model(payload.model)
|
|
164
|
+
if model_effort is not None:
|
|
165
|
+
return model_effort
|
|
166
|
+
|
|
167
|
+
reasoning = payload.reasoning.model_dump(mode="json", exclude_none=True) if payload.reasoning is not None else None
|
|
168
|
+
if is_json_mapping(reasoning):
|
|
169
|
+
effort = reasoning.get("effort")
|
|
170
|
+
if isinstance(effort, str) and effort.strip():
|
|
171
|
+
return effort.strip().lower()
|
|
172
|
+
return _client_reasoning_effort_from_provider_aliases(payload)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _client_reasoning_effort_from_provider_aliases(
|
|
176
|
+
payload: ResponsesRequest | ResponsesCompactRequest,
|
|
177
|
+
) -> str | None:
|
|
178
|
+
reasoning = payload.reasoning.model_dump(mode="json", exclude_none=True) if payload.reasoning is not None else None
|
|
179
|
+
extra = payload.model_extra
|
|
180
|
+
if isinstance(extra, dict):
|
|
181
|
+
alias_payload = dict(extra)
|
|
182
|
+
if reasoning is not None:
|
|
183
|
+
alias_payload["reasoning"] = reasoning
|
|
184
|
+
normalize_reasoning_aliases(alias_payload)
|
|
185
|
+
normalized_reasoning = alias_payload.get("reasoning")
|
|
186
|
+
if is_json_mapping(normalized_reasoning):
|
|
187
|
+
extra_effort = normalized_reasoning.get("effort")
|
|
188
|
+
if isinstance(extra_effort, str) and extra_effort.strip():
|
|
189
|
+
return extra_effort.strip().lower()
|
|
190
|
+
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _materialize_provider_reasoning_effort(
|
|
195
|
+
payload: ResponsesRequest | ResponsesCompactRequest,
|
|
196
|
+
effort: str | None,
|
|
197
|
+
) -> None:
|
|
198
|
+
existing_effort = payload.reasoning.effort if payload.reasoning is not None else None
|
|
199
|
+
if effort is None or (isinstance(existing_effort, str) and existing_effort.strip()):
|
|
200
|
+
return
|
|
201
|
+
if payload.reasoning is None:
|
|
202
|
+
payload.reasoning = ResponsesReasoning(effort=effort)
|
|
203
|
+
else:
|
|
204
|
+
payload.reasoning.effort = effort
|
|
205
|
+
if isinstance(payload, ResponsesRequest):
|
|
206
|
+
payload._codex_lb_provider_reasoning_effort_materialized = True
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _client_reasoning_effort_from_model(model: str | None) -> str | None:
|
|
210
|
+
alias = _resolve_model_alias_parts(model)
|
|
211
|
+
if alias is not None:
|
|
212
|
+
normalized_model = model.strip().lower() if isinstance(model, str) else ""
|
|
213
|
+
suffix = normalized_model[len(alias[0]) + 1 :]
|
|
214
|
+
tokens = {token for token in suffix.split("-") if token}
|
|
215
|
+
if "xhigh" in tokens or "extra" in tokens:
|
|
216
|
+
return "xhigh"
|
|
217
|
+
if alias[1] is not None:
|
|
218
|
+
return alias[1]
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def normalize_source_reasoning_aliases(payload: dict[str, JsonValue]) -> None:
|
|
223
|
+
"""Align effort-bearing aliases while preserving unrelated source controls."""
|
|
224
|
+
provider_thinking = payload.get("thinking")
|
|
225
|
+
preserve_provider_thinking = False
|
|
226
|
+
if "thinking" in payload:
|
|
227
|
+
probe: dict[str, JsonValue] = {"thinking": provider_thinking}
|
|
228
|
+
normalize_reasoning_aliases(probe)
|
|
229
|
+
normalized_reasoning = probe.get("reasoning")
|
|
230
|
+
normalized_effort = normalized_reasoning.get("effort") if is_json_mapping(normalized_reasoning) else None
|
|
231
|
+
thinking_mapping = provider_thinking if is_json_mapping(provider_thinking) else None
|
|
232
|
+
thinking_type = thinking_mapping.get("type") if thinking_mapping is not None else None
|
|
233
|
+
is_inactive = thinking_mapping is not None and (
|
|
234
|
+
thinking_mapping.get("enabled") is False
|
|
235
|
+
or (isinstance(thinking_type, str) and thinking_type.strip().lower() == "disabled")
|
|
236
|
+
)
|
|
237
|
+
preserve_provider_thinking = (
|
|
238
|
+
thinking_mapping is not None
|
|
239
|
+
and not is_inactive
|
|
240
|
+
and not (isinstance(normalized_effort, str) and bool(normalized_effort.strip()))
|
|
241
|
+
)
|
|
242
|
+
if preserve_provider_thinking:
|
|
243
|
+
payload.pop("thinking", None)
|
|
244
|
+
normalize_reasoning_aliases(payload)
|
|
245
|
+
if preserve_provider_thinking and thinking_mapping is not None:
|
|
246
|
+
preserved_thinking = dict(thinking_mapping.items())
|
|
247
|
+
preserved_effort = preserved_thinking.get("effort")
|
|
248
|
+
if isinstance(preserved_effort, str) and not preserved_effort.strip():
|
|
249
|
+
preserved_thinking.pop("effort")
|
|
250
|
+
payload["thinking"] = preserved_thinking
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class ApiKeyEnforcementResult(NamedTuple):
|
|
254
|
+
"""What :func:`apply_api_key_enforcement` observed while mutating the payload.
|
|
255
|
+
|
|
256
|
+
``pre_normalization_reasoning_effort`` carries the effort that
|
|
257
|
+
:func:`normalize_unsupported_reasoning_effort` replaced, so a caller that
|
|
258
|
+
later routes the request to an OpenAI-compatible model source can restore
|
|
259
|
+
it. It is the post-enforcement value: restoring it cannot resurrect an
|
|
260
|
+
effort an API key overrode.
|
|
261
|
+
"""
|
|
262
|
+
|
|
263
|
+
service_tier_was_enforced: bool
|
|
264
|
+
pre_normalization_reasoning_effort: str | None
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def apply_api_key_enforcement(
|
|
268
|
+
payload: ResponsesRequest | ResponsesCompactRequest,
|
|
269
|
+
api_key: ApiKeyData | None,
|
|
270
|
+
*,
|
|
271
|
+
registry: ModelRegistry | None = None,
|
|
272
|
+
prohibit_fast_mode: bool = False,
|
|
273
|
+
) -> ApiKeyEnforcementResult:
|
|
274
|
+
"""Apply API-key policy and report whether it supplied the service tier.
|
|
275
|
+
|
|
276
|
+
The returned provenance is captured before mutating ``payload``. Callers
|
|
277
|
+
must not infer it later by comparing values: an explicit client request can
|
|
278
|
+
equal the enforced value (including after ``fast`` canonicalizes to
|
|
279
|
+
``priority``).
|
|
280
|
+
"""
|
|
281
|
+
client_reasoning_effort = payload._codex_lb_client_reasoning_effort or _client_reasoning_effort(payload)
|
|
282
|
+
payload._codex_lb_client_reasoning_effort = client_reasoning_effort
|
|
283
|
+
provider_reasoning_effort = _client_reasoning_effort_from_provider_aliases(payload)
|
|
284
|
+
normalize_upstream_model_alias(payload)
|
|
285
|
+
|
|
286
|
+
if api_key is None:
|
|
287
|
+
_materialize_provider_reasoning_effort(payload, provider_reasoning_effort)
|
|
288
|
+
pre_normalization_effort = normalize_unsupported_reasoning_effort(payload, registry=registry)
|
|
289
|
+
apply_prohibit_fast_mode(payload, prohibit_fast_mode=prohibit_fast_mode)
|
|
290
|
+
return ApiKeyEnforcementResult(False, pre_normalization_effort)
|
|
291
|
+
|
|
292
|
+
if api_key.enforced_model:
|
|
293
|
+
enforced_model_reasoning_effort = _client_reasoning_effort_from_model(api_key.enforced_model)
|
|
294
|
+
requested_model = payload.model
|
|
295
|
+
if requested_model != api_key.enforced_model:
|
|
296
|
+
logger.info(
|
|
297
|
+
"api_key_model_enforced request_id=%s key_id=%s requested_model=%s enforced_model=%s",
|
|
298
|
+
get_request_id(),
|
|
299
|
+
api_key.id,
|
|
300
|
+
requested_model,
|
|
301
|
+
api_key.enforced_model,
|
|
302
|
+
)
|
|
303
|
+
payload.model = api_key.enforced_model
|
|
304
|
+
if enforced_model_reasoning_effort is not None:
|
|
305
|
+
client_reasoning_effort = enforced_model_reasoning_effort
|
|
306
|
+
payload._codex_lb_client_reasoning_effort = client_reasoning_effort
|
|
307
|
+
normalize_upstream_model_alias(payload)
|
|
308
|
+
if (
|
|
309
|
+
responses_input_uses_lite_tools(payload.input)
|
|
310
|
+
and _model_responses_lite_capability(
|
|
311
|
+
payload.model,
|
|
312
|
+
registry=registry or get_model_registry(),
|
|
313
|
+
)
|
|
314
|
+
is False
|
|
315
|
+
):
|
|
316
|
+
raise ProxyModelNotAllowed(
|
|
317
|
+
f"API key enforced model '{payload.model}' does not support Responses Lite",
|
|
318
|
+
code="responses_lite_model_mismatch",
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
if api_key.enforced_reasoning_effort is not None:
|
|
322
|
+
requested_effort = payload.reasoning.effort if payload.reasoning else None
|
|
323
|
+
if payload.reasoning is None:
|
|
324
|
+
payload.reasoning = ResponsesReasoning(effort=api_key.enforced_reasoning_effort)
|
|
325
|
+
else:
|
|
326
|
+
payload.reasoning.effort = api_key.enforced_reasoning_effort
|
|
327
|
+
if requested_effort != api_key.enforced_reasoning_effort:
|
|
328
|
+
logger.info(
|
|
329
|
+
"api_key_reasoning_enforced request_id=%s key_id=%s requested_effort=%s enforced_effort=%s",
|
|
330
|
+
get_request_id(),
|
|
331
|
+
api_key.id,
|
|
332
|
+
requested_effort,
|
|
333
|
+
api_key.enforced_reasoning_effort,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
_materialize_provider_reasoning_effort(payload, provider_reasoning_effort)
|
|
337
|
+
if client_reasoning_effort is not None:
|
|
338
|
+
validate_reasoning_effort_access(api_key, client_reasoning_effort)
|
|
339
|
+
if (
|
|
340
|
+
payload.reasoning is not None
|
|
341
|
+
and isinstance(payload.reasoning.effort, str)
|
|
342
|
+
and payload.reasoning.effort.strip().lower() == client_reasoning_effort
|
|
343
|
+
):
|
|
344
|
+
payload.reasoning.effort = client_reasoning_effort
|
|
345
|
+
pre_normalization_effort = normalize_unsupported_reasoning_effort(payload, registry=registry)
|
|
346
|
+
|
|
347
|
+
service_tier_was_enforced = False
|
|
348
|
+
if api_key.enforced_service_tier is not None:
|
|
349
|
+
requested_service_tier = getattr(payload, "service_tier", None)
|
|
350
|
+
service_tier_was_enforced = requested_service_tier is None or (
|
|
351
|
+
requested_service_tier.strip().lower() in _UPSTREAM_OMIT_SERVICE_TIERS
|
|
352
|
+
)
|
|
353
|
+
# ``auto``/``default`` are accepted at the API-key surface but
|
|
354
|
+
# the ChatGPT/Codex backend rejects them as literal values. Map
|
|
355
|
+
# them onto the wire-level absence of ``service_tier`` (which
|
|
356
|
+
# already means "use upstream default") so the enforcement
|
|
357
|
+
# actually reaches upstream instead of failing with
|
|
358
|
+
# ``Unsupported service_tier``. See issue #546.
|
|
359
|
+
if api_key.enforced_service_tier in _UPSTREAM_OMIT_SERVICE_TIERS:
|
|
360
|
+
effective_service_tier: str | None = None
|
|
361
|
+
else:
|
|
362
|
+
effective_service_tier = api_key.enforced_service_tier
|
|
363
|
+
setattr(payload, "service_tier", effective_service_tier)
|
|
364
|
+
if requested_service_tier != api_key.enforced_service_tier:
|
|
365
|
+
logger.info(
|
|
366
|
+
"api_key_service_tier_enforced request_id=%s key_id=%s "
|
|
367
|
+
"requested_service_tier=%s enforced_service_tier=%s "
|
|
368
|
+
"outbound_service_tier=%s",
|
|
369
|
+
get_request_id(),
|
|
370
|
+
api_key.id,
|
|
371
|
+
requested_service_tier,
|
|
372
|
+
api_key.enforced_service_tier,
|
|
373
|
+
effective_service_tier,
|
|
374
|
+
)
|
|
375
|
+
apply_prohibit_fast_mode(payload, prohibit_fast_mode=prohibit_fast_mode)
|
|
376
|
+
return ApiKeyEnforcementResult(service_tier_was_enforced, pre_normalization_effort)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def apply_prohibit_fast_mode(
|
|
380
|
+
payload: ResponsesRequest | ResponsesCompactRequest | dict[str, JsonValue],
|
|
381
|
+
*,
|
|
382
|
+
prohibit_fast_mode: bool,
|
|
383
|
+
request_id: str | None = None,
|
|
384
|
+
) -> bool:
|
|
385
|
+
"""Remove a resolved priority tier when the global policy prohibits it."""
|
|
386
|
+
raw_service_tier = payload.get("service_tier") if isinstance(payload, dict) else payload.service_tier
|
|
387
|
+
service_tier = raw_service_tier if isinstance(raw_service_tier, str) else None
|
|
388
|
+
if not prohibit_fast_mode or service_tier is None or canonical_service_tier_value(service_tier) != "priority":
|
|
389
|
+
return False
|
|
390
|
+
|
|
391
|
+
logger.info(
|
|
392
|
+
"fast_mode_service_tier_prohibited request_id=%s stripped_service_tier=%s",
|
|
393
|
+
request_id or get_request_id(),
|
|
394
|
+
service_tier,
|
|
395
|
+
)
|
|
396
|
+
if isinstance(payload, dict):
|
|
397
|
+
payload.pop("service_tier", None)
|
|
398
|
+
else:
|
|
399
|
+
payload.service_tier = None
|
|
400
|
+
return True
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def apply_enforced_service_tier_model_fallback(
|
|
404
|
+
payload: ResponsesRequest | ResponsesCompactRequest,
|
|
405
|
+
*,
|
|
406
|
+
service_tier_was_enforced: bool,
|
|
407
|
+
registry: ModelRegistry | None = None,
|
|
408
|
+
) -> bool:
|
|
409
|
+
"""Drop an enforced tier only when an account model truly lacks it.
|
|
410
|
+
|
|
411
|
+
This runs after model-source selection, and only on subscription-account
|
|
412
|
+
routes. Mutating the request at that boundary makes account selection,
|
|
413
|
+
bridge compatibility, API-key accounting, request logs, and the upstream
|
|
414
|
+
wire payload agree on the same effective tier.
|
|
415
|
+
"""
|
|
416
|
+
service_tier = payload.service_tier
|
|
417
|
+
if not service_tier_was_enforced or service_tier is None:
|
|
418
|
+
return False
|
|
419
|
+
|
|
420
|
+
model_registry = registry or get_model_registry()
|
|
421
|
+
advertises_service_tier = getattr(model_registry, "model_advertises_service_tier", None)
|
|
422
|
+
if not callable(advertises_service_tier) or advertises_service_tier(payload.model, service_tier):
|
|
423
|
+
return False
|
|
424
|
+
|
|
425
|
+
logger.info(
|
|
426
|
+
"api_key_enforced_service_tier_model_fallback request_id=%s model=%s enforced_service_tier=%s",
|
|
427
|
+
get_request_id(),
|
|
428
|
+
payload.model,
|
|
429
|
+
service_tier,
|
|
430
|
+
)
|
|
431
|
+
payload.service_tier = None
|
|
432
|
+
return True
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _model_responses_lite_capability(
|
|
436
|
+
model: str,
|
|
437
|
+
*,
|
|
438
|
+
registry: ModelRegistry,
|
|
439
|
+
) -> bool | None:
|
|
440
|
+
normalized_model = model.strip().lower()
|
|
441
|
+
models = registry.get_models_for_metadata()
|
|
442
|
+
model_entry = models.get(model) or models.get(normalized_model)
|
|
443
|
+
if model_entry is None:
|
|
444
|
+
return None
|
|
445
|
+
capability = model_entry.raw.get("use_responses_lite")
|
|
446
|
+
return capability if isinstance(capability, bool) else None
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
# Non-standard reasoning toggles some OpenAI-compatible clients attach
|
|
450
|
+
# (OpenRouter/SGLang style). Forwarding them to a source whose model does not
|
|
451
|
+
# support reasoning flips the upstream into reasoning-extraction mode: the
|
|
452
|
+
# answer lands in ``message.reasoning`` instead of ``message.content``.
|
|
453
|
+
_SOURCE_REASONING_TOGGLE_KEYS: tuple[str, ...] = (
|
|
454
|
+
"include_reasoning",
|
|
455
|
+
"separate_reasoning",
|
|
456
|
+
"stream_reasoning",
|
|
457
|
+
"reasoning",
|
|
458
|
+
"reasoning_effort",
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def sanitize_source_chat_payload(
|
|
463
|
+
payload: dict[str, JsonValue],
|
|
464
|
+
*,
|
|
465
|
+
allow_reasoning: bool,
|
|
466
|
+
) -> None:
|
|
467
|
+
"""Clean a chat-completions wire payload before forwarding to a source.
|
|
468
|
+
|
|
469
|
+
``ChatCompletionsRequest`` allows extra fields and defaults ``tools`` to an
|
|
470
|
+
empty list, so an unfiltered ``model_dump`` forwards ``"tools": []`` and any
|
|
471
|
+
client-side reasoning toggles verbatim.
|
|
472
|
+
"""
|
|
473
|
+
tools = payload.get("tools")
|
|
474
|
+
if isinstance(tools, list) and not tools:
|
|
475
|
+
payload.pop("tools", None)
|
|
476
|
+
payload.pop("tool_choice", None)
|
|
477
|
+
payload.pop("parallel_tool_calls", None)
|
|
478
|
+
if not allow_reasoning:
|
|
479
|
+
for key in _SOURCE_REASONING_TOGGLE_KEYS:
|
|
480
|
+
payload.pop(key, None)
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def apply_api_key_enforcement_to_chat_payload(
|
|
484
|
+
payload: dict[str, JsonValue],
|
|
485
|
+
api_key: ApiKeyData | None,
|
|
486
|
+
*,
|
|
487
|
+
allowed_reasoning_effort: str | None = None,
|
|
488
|
+
materialize_allowed_reasoning_effort: bool = False,
|
|
489
|
+
) -> None:
|
|
490
|
+
"""Mirror :func:`apply_api_key_enforcement` onto a chat-completions wire payload.
|
|
491
|
+
|
|
492
|
+
Source-routed chat requests forward the original chat-shaped payload
|
|
493
|
+
instead of the converted ``ResponsesRequest``, so enforced fields must be
|
|
494
|
+
applied to the outbound dict as well or the upstream receives the
|
|
495
|
+
caller's values while accounting uses the enforced ones.
|
|
496
|
+
"""
|
|
497
|
+
if allowed_reasoning_effort is not None:
|
|
498
|
+
wire_effort = resolve_wire_reasoning_effort(allowed_reasoning_effort)
|
|
499
|
+
# Chat requests can express the same setting through several provider
|
|
500
|
+
# aliases. Once the Responses conversion has authorized one effective
|
|
501
|
+
# choice, make every caller-supplied alias agree without adding fields
|
|
502
|
+
# that the selected source may not accept.
|
|
503
|
+
if "reasoning_effort" in payload:
|
|
504
|
+
payload["reasoning_effort"] = wire_effort
|
|
505
|
+
if "reasoningEffort" in payload:
|
|
506
|
+
payload["reasoningEffort"] = wire_effort
|
|
507
|
+
if "thinking" in payload:
|
|
508
|
+
thinking = payload["thinking"]
|
|
509
|
+
if isinstance(thinking, dict):
|
|
510
|
+
thinking_effort = thinking.get("effort")
|
|
511
|
+
if isinstance(thinking_effort, str) and not thinking_effort.strip():
|
|
512
|
+
thinking = {**thinking}
|
|
513
|
+
thinking.pop("effort")
|
|
514
|
+
payload["thinking"] = thinking
|
|
515
|
+
if isinstance(thinking_effort, str) and thinking_effort.strip():
|
|
516
|
+
aligned_thinking = {**thinking, "effort": wire_effort}
|
|
517
|
+
thinking_type = aligned_thinking.get("type")
|
|
518
|
+
if isinstance(thinking_type, str) and thinking_type.strip().lower() == "disabled":
|
|
519
|
+
aligned_thinking.pop("type")
|
|
520
|
+
if aligned_thinking.get("enabled") is False:
|
|
521
|
+
aligned_thinking.pop("enabled")
|
|
522
|
+
payload["thinking"] = aligned_thinking
|
|
523
|
+
else:
|
|
524
|
+
thinking_type = thinking.get("type")
|
|
525
|
+
is_inactive = thinking.get("enabled") is False or (
|
|
526
|
+
isinstance(thinking_type, str) and thinking_type.strip().lower() == "disabled"
|
|
527
|
+
)
|
|
528
|
+
selects_implicit_medium = thinking.get("enabled") is True or (
|
|
529
|
+
isinstance(thinking_type, str) and thinking_type.strip().lower() == "enabled"
|
|
530
|
+
)
|
|
531
|
+
if is_inactive or (selects_implicit_medium and wire_effort != "medium"):
|
|
532
|
+
payload.pop("thinking")
|
|
533
|
+
else:
|
|
534
|
+
payload["thinking"] = wire_effort
|
|
535
|
+
if "enable_thinking" in payload:
|
|
536
|
+
if wire_effort == "medium":
|
|
537
|
+
payload["enable_thinking"] = True
|
|
538
|
+
else:
|
|
539
|
+
payload.pop("enable_thinking", None)
|
|
540
|
+
reasoning = payload.get("reasoning")
|
|
541
|
+
if isinstance(reasoning, dict):
|
|
542
|
+
payload["reasoning"] = {**reasoning, "effort": wire_effort}
|
|
543
|
+
if materialize_allowed_reasoning_effort and not any(
|
|
544
|
+
key in payload
|
|
545
|
+
for key in ("reasoning_effort", "reasoningEffort", "thinking", "enable_thinking", "reasoning")
|
|
546
|
+
):
|
|
547
|
+
payload["reasoning_effort"] = wire_effort
|
|
548
|
+
|
|
549
|
+
if api_key is None:
|
|
550
|
+
return
|
|
551
|
+
|
|
552
|
+
if api_key.enforced_reasoning_effort is not None:
|
|
553
|
+
enforced_effort = resolve_wire_reasoning_effort(api_key.enforced_reasoning_effort)
|
|
554
|
+
payload["reasoning_effort"] = enforced_effort
|
|
555
|
+
reasoning = payload.get("reasoning")
|
|
556
|
+
if isinstance(reasoning, dict):
|
|
557
|
+
payload["reasoning"] = {**reasoning, "effort": enforced_effort}
|
|
558
|
+
else:
|
|
559
|
+
payload["reasoning"] = {"effort": enforced_effort}
|
|
560
|
+
|
|
561
|
+
if api_key.enforced_service_tier is not None:
|
|
562
|
+
if api_key.enforced_service_tier in _UPSTREAM_OMIT_SERVICE_TIERS:
|
|
563
|
+
payload.pop("service_tier", None)
|
|
564
|
+
else:
|
|
565
|
+
payload["service_tier"] = api_key.enforced_service_tier
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def resolve_model_alias(model: str | None) -> str | None:
|
|
569
|
+
alias = _resolve_model_alias_parts(model)
|
|
570
|
+
if alias is None:
|
|
571
|
+
return model
|
|
572
|
+
return alias[0]
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def model_alias_requests_fast_mode(model: str | None) -> bool:
|
|
576
|
+
alias = _resolve_model_alias_parts(model)
|
|
577
|
+
return alias is not None and alias[3]
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def normalize_upstream_model_alias(payload: ResponsesRequest | ResponsesCompactRequest) -> None:
|
|
581
|
+
requested_model = payload.model
|
|
582
|
+
alias = _resolve_model_alias_parts(requested_model)
|
|
583
|
+
if alias is None:
|
|
584
|
+
return
|
|
585
|
+
|
|
586
|
+
canonical_model, alias_effort, alias_service_tier, _alias_requests_fast_mode = alias
|
|
587
|
+
if payload.model != canonical_model:
|
|
588
|
+
logger.info(
|
|
589
|
+
"model_alias_normalized request_id=%s requested_model=%s normalized_model=%s",
|
|
590
|
+
get_request_id(),
|
|
591
|
+
payload.model,
|
|
592
|
+
canonical_model,
|
|
593
|
+
)
|
|
594
|
+
payload.model = canonical_model
|
|
595
|
+
|
|
596
|
+
if alias_effort is not None:
|
|
597
|
+
requested_effort = payload.reasoning.effort if payload.reasoning else None
|
|
598
|
+
if payload.reasoning is None:
|
|
599
|
+
payload.reasoning = ResponsesReasoning(effort=alias_effort)
|
|
600
|
+
else:
|
|
601
|
+
payload.reasoning.effort = alias_effort
|
|
602
|
+
if requested_effort != alias_effort:
|
|
603
|
+
logger.info(
|
|
604
|
+
"model_alias_reasoning_normalized request_id=%s requested_model=%s "
|
|
605
|
+
"normalized_model=%s requested_effort=%s normalized_effort=%s",
|
|
606
|
+
get_request_id(),
|
|
607
|
+
requested_model,
|
|
608
|
+
canonical_model,
|
|
609
|
+
requested_effort,
|
|
610
|
+
alias_effort,
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
if alias_service_tier is not None and getattr(payload, "service_tier", None) is None:
|
|
614
|
+
setattr(payload, "service_tier", alias_service_tier)
|
|
615
|
+
logger.info(
|
|
616
|
+
"model_alias_service_tier_normalized request_id=%s requested_model=%s "
|
|
617
|
+
"normalized_model=%s normalized_service_tier=%s",
|
|
618
|
+
get_request_id(),
|
|
619
|
+
requested_model,
|
|
620
|
+
canonical_model,
|
|
621
|
+
alias_service_tier,
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def _resolve_model_alias_parts(model: str | None) -> tuple[str, str | None, str | None, bool] | None:
|
|
626
|
+
if not isinstance(model, str):
|
|
627
|
+
return None
|
|
628
|
+
normalized = model.strip().lower()
|
|
629
|
+
if not normalized:
|
|
630
|
+
return None
|
|
631
|
+
|
|
632
|
+
for base_model in _GPT5_ALIAS_BASE_MODELS:
|
|
633
|
+
prefix = f"{base_model}-"
|
|
634
|
+
if not normalized.startswith(prefix):
|
|
635
|
+
continue
|
|
636
|
+
suffix = normalized[len(prefix) :]
|
|
637
|
+
tokens = [token for token in suffix.split("-") if token]
|
|
638
|
+
if not tokens or any(token not in _MODEL_ALIAS_TOKENS for token in tokens):
|
|
639
|
+
return None
|
|
640
|
+
return (
|
|
641
|
+
base_model,
|
|
642
|
+
_resolve_alias_reasoning_effort(tokens),
|
|
643
|
+
_resolve_alias_service_tier(tokens),
|
|
644
|
+
"fast" in tokens,
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
return None
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def _resolve_alias_reasoning_effort(tokens: list[str]) -> str | None:
|
|
651
|
+
selected: str | None = None
|
|
652
|
+
selected_rank = -1
|
|
653
|
+
for token in tokens:
|
|
654
|
+
effort = _MODEL_ALIAS_REASONING_TOKENS.get(token)
|
|
655
|
+
if effort is None:
|
|
656
|
+
continue
|
|
657
|
+
rank = _MODEL_ALIAS_REASONING_RANK[effort]
|
|
658
|
+
if rank > selected_rank:
|
|
659
|
+
selected = effort
|
|
660
|
+
selected_rank = rank
|
|
661
|
+
return selected
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def _resolve_alias_service_tier(tokens: list[str]) -> str | None:
|
|
665
|
+
for token in tokens:
|
|
666
|
+
service_tier = _MODEL_ALIAS_SERVICE_TIER_TOKENS.get(token)
|
|
667
|
+
if service_tier is not None:
|
|
668
|
+
return service_tier
|
|
669
|
+
return None
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def normalize_unsupported_reasoning_effort(
|
|
673
|
+
payload: ResponsesRequest | ResponsesCompactRequest,
|
|
674
|
+
*,
|
|
675
|
+
registry: ModelRegistry | None = None,
|
|
676
|
+
) -> str | None:
|
|
677
|
+
"""Rewrite ``reasoning.effort`` values the upstream backend rejects.
|
|
678
|
+
|
|
679
|
+
Some efforts that codex-lb accepts at the API surface (notably
|
|
680
|
+
``"minimal"``) are silently dropped by the ChatGPT/Codex WebSocket
|
|
681
|
+
backend, which causes the response stream to hang with no completion.
|
|
682
|
+
For those values we map to a value the resolved model actually supports
|
|
683
|
+
so clients (e.g. Codex CLI's ``--reasoning-effort minimal``) keep
|
|
684
|
+
working. Mapping picks the model's lowest advertised effort, falling
|
|
685
|
+
back to ``low`` when the registry has no metadata yet.
|
|
686
|
+
|
|
687
|
+
Client-plane efforts the reference Codex client aliases before sending
|
|
688
|
+
(``ultra`` -> ``max``) are rewritten the same way here.
|
|
689
|
+
|
|
690
|
+
Returns the effort that the unsupported-effort fallback replaced, in
|
|
691
|
+
normalized (trimmed, lowercased) form, or ``None`` when nothing restorable
|
|
692
|
+
was rewritten. Model sources do not have the backend quirk that fallback
|
|
693
|
+
works around, but whether a request is served by one is only known after
|
|
694
|
+
source selection, which happens later; callers that can reach a source
|
|
695
|
+
carry this value forward and restore it there (see
|
|
696
|
+
``restore_source_reasoning_effort``).
|
|
697
|
+
|
|
698
|
+
The ``ultra`` -> ``max`` wire alias is never reported. That aliasing mirrors
|
|
699
|
+
the reference client and is required on every upstream surface, so it must
|
|
700
|
+
survive source routing too.
|
|
701
|
+
|
|
702
|
+
The reported value is the post-enforcement effort rather than the client's
|
|
703
|
+
original, so restoring it cannot resurrect an effort an API key overrode.
|
|
704
|
+
"""
|
|
705
|
+
|
|
706
|
+
if payload.reasoning is None or payload.reasoning.effort is None:
|
|
707
|
+
return None
|
|
708
|
+
|
|
709
|
+
requested_effort = payload.reasoning.effort
|
|
710
|
+
normalized_effort = requested_effort.strip().lower()
|
|
711
|
+
|
|
712
|
+
wire_alias = _REASONING_EFFORT_WIRE_ALIASES.get(normalized_effort)
|
|
713
|
+
if wire_alias is not None:
|
|
714
|
+
payload.reasoning.effort = wire_alias
|
|
715
|
+
logger.info(
|
|
716
|
+
"reasoning_effort_wire_aliased request_id=%s model=%s requested_effort=%s aliased_effort=%s",
|
|
717
|
+
get_request_id(),
|
|
718
|
+
payload.model,
|
|
719
|
+
requested_effort,
|
|
720
|
+
wire_alias,
|
|
721
|
+
)
|
|
722
|
+
# Deliberately not reported as restorable: the ultra -> max alias must
|
|
723
|
+
# hold on every surface, source-routed payloads included.
|
|
724
|
+
return None
|
|
725
|
+
|
|
726
|
+
if normalized_effort not in _UNSUPPORTED_UPSTREAM_REASONING_EFFORTS:
|
|
727
|
+
return None
|
|
728
|
+
|
|
729
|
+
fallback = _resolve_reasoning_effort_fallback(
|
|
730
|
+
payload.model,
|
|
731
|
+
registry=registry or get_model_registry(),
|
|
732
|
+
)
|
|
733
|
+
payload.reasoning.effort = fallback
|
|
734
|
+
logger.info(
|
|
735
|
+
"reasoning_effort_normalized request_id=%s model=%s requested_effort=%s normalized_effort=%s",
|
|
736
|
+
get_request_id(),
|
|
737
|
+
payload.model,
|
|
738
|
+
requested_effort,
|
|
739
|
+
fallback,
|
|
740
|
+
)
|
|
741
|
+
return normalized_effort
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
def restore_source_reasoning_effort(
|
|
745
|
+
payload: ResponsesRequest | ResponsesCompactRequest,
|
|
746
|
+
source: ModelSource,
|
|
747
|
+
*,
|
|
748
|
+
pre_normalization_effort: str | None,
|
|
749
|
+
) -> None:
|
|
750
|
+
"""Undo :func:`normalize_unsupported_reasoning_effort` for a source-routed request.
|
|
751
|
+
|
|
752
|
+
The rewrite exists solely to work around a ChatGPT/Codex backend quirk, so
|
|
753
|
+
it must not reach an OpenAI-compatible model source. This runs at the point
|
|
754
|
+
where the source has actually been selected, which is the only place the
|
|
755
|
+
routing outcome is known -- inferring it earlier from registry membership
|
|
756
|
+
misfires in both directions (a subscription model missing from a populated
|
|
757
|
+
snapshot, and a source model whose slug shadows a subscription one).
|
|
758
|
+
|
|
759
|
+
The restore is gated on the operator having declared the effort for this
|
|
760
|
+
model: sources without reasoning metadata keep the pre-existing behaviour,
|
|
761
|
+
and an effort the backend never advertised is not sent to it.
|
|
762
|
+
"""
|
|
763
|
+
if pre_normalization_effort is None or payload.reasoning is None:
|
|
764
|
+
return
|
|
765
|
+
if payload.model is None:
|
|
766
|
+
return
|
|
767
|
+
restored_effort = pre_normalization_effort.strip().lower()
|
|
768
|
+
declared = {level.effort for level in source_model_reasoning_levels(source, payload.model)}
|
|
769
|
+
if restored_effort not in declared:
|
|
770
|
+
return
|
|
771
|
+
current_effort = payload.reasoning.effort
|
|
772
|
+
# Normalized on assignment rather than trusting the caller: the sole
|
|
773
|
+
# producer already reports the normalized form, but that invariant is
|
|
774
|
+
# non-local and a casing variant must never reach the wire.
|
|
775
|
+
payload.reasoning.effort = restored_effort
|
|
776
|
+
logger.info(
|
|
777
|
+
"reasoning_effort_restored_for_source request_id=%s model=%s source_id=%s "
|
|
778
|
+
"normalized_effort=%s restored_effort=%s",
|
|
779
|
+
get_request_id(),
|
|
780
|
+
payload.model,
|
|
781
|
+
source.id,
|
|
782
|
+
current_effort,
|
|
783
|
+
restored_effort,
|
|
784
|
+
)
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def _resolve_reasoning_effort_fallback(
|
|
788
|
+
model: str | None,
|
|
789
|
+
*,
|
|
790
|
+
registry: ModelRegistry,
|
|
791
|
+
) -> str:
|
|
792
|
+
if not model:
|
|
793
|
+
return _DEFAULT_REASONING_EFFORT_FALLBACK
|
|
794
|
+
snapshot = registry.get_snapshot()
|
|
795
|
+
if snapshot is None:
|
|
796
|
+
return _DEFAULT_REASONING_EFFORT_FALLBACK
|
|
797
|
+
upstream = snapshot.models.get(model) or snapshot.models.get(model.strip().lower())
|
|
798
|
+
if upstream is None:
|
|
799
|
+
return _DEFAULT_REASONING_EFFORT_FALLBACK
|
|
800
|
+
advertised = [level.effort for level in upstream.supported_reasoning_levels if level.effort]
|
|
801
|
+
# Prefer the order the model registry advertises (already lowest -> highest
|
|
802
|
+
# for the GPT-5 family), but always pick the first advertised effort that
|
|
803
|
+
# is not itself an unsupported value.
|
|
804
|
+
for effort in advertised:
|
|
805
|
+
if effort.strip().lower() not in _UNSUPPORTED_UPSTREAM_REASONING_EFFORTS:
|
|
806
|
+
return effort
|
|
807
|
+
return _DEFAULT_REASONING_EFFORT_FALLBACK
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
def openai_validation_error(exc: ValidationError) -> OpenAIErrorEnvelope:
|
|
811
|
+
error = openai_invalid_payload_error()
|
|
812
|
+
if exc.errors():
|
|
813
|
+
first = exc.errors()[0]
|
|
814
|
+
loc = first.get("loc", [])
|
|
815
|
+
if isinstance(loc, (list, tuple)):
|
|
816
|
+
param = ".".join(str(part) for part in loc if part != "body")
|
|
817
|
+
if param:
|
|
818
|
+
error["error"]["param"] = param
|
|
819
|
+
return error
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
def openai_invalid_payload_error(param: str | None = None) -> OpenAIErrorEnvelope:
|
|
823
|
+
error = openai_error("invalid_request_error", "Invalid request payload", error_type="invalid_request_error")
|
|
824
|
+
if param:
|
|
825
|
+
error["error"]["param"] = param
|
|
826
|
+
return error
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
def openai_client_payload_error(exc: ClientPayloadError) -> OpenAIErrorEnvelope:
|
|
830
|
+
"""Render a ``ClientPayloadError`` as an OpenAI error envelope.
|
|
831
|
+
|
|
832
|
+
Falls back to ``openai_invalid_payload_error`` for legacy callsites
|
|
833
|
+
that raise ``ClientPayloadError`` without ``code`` / ``error_type``.
|
|
834
|
+
"""
|
|
835
|
+
if exc.code is None and exc.error_type is None:
|
|
836
|
+
return openai_invalid_payload_error(exc.param)
|
|
837
|
+
code = exc.code or "invalid_request_error"
|
|
838
|
+
error_type = exc.error_type or "invalid_request_error"
|
|
839
|
+
error = openai_error(code, str(exc), error_type=error_type)
|
|
840
|
+
if exc.param:
|
|
841
|
+
error["error"]["param"] = exc.param
|
|
842
|
+
return error
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
def normalize_responses_request_payload(
|
|
846
|
+
payload: dict[str, JsonValue],
|
|
847
|
+
*,
|
|
848
|
+
openai_compat: bool,
|
|
849
|
+
) -> ResponsesRequest:
|
|
850
|
+
if openai_compat:
|
|
851
|
+
responses = V1ResponsesRequest.model_validate(payload).to_responses_request()
|
|
852
|
+
else:
|
|
853
|
+
responses = ResponsesRequest.model_validate(payload)
|
|
854
|
+
enforce_strict_text_format(responses)
|
|
855
|
+
enforce_strict_function_tools_format(responses.tools)
|
|
856
|
+
return responses
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
def validate_top_level_compaction_trigger_input_shape(payload: Mapping[str, JsonValue]) -> None:
|
|
860
|
+
input_value = payload.get("input")
|
|
861
|
+
if not is_json_list(input_value):
|
|
862
|
+
return
|
|
863
|
+
_validate_terminal_compaction_trigger_input_items(input_value)
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
def strip_terminal_compaction_trigger_input(
|
|
867
|
+
payload: ResponsesRequest | ResponsesCompactRequest,
|
|
868
|
+
*,
|
|
869
|
+
strip_trigger: bool = True,
|
|
870
|
+
) -> list[JsonValue] | None:
|
|
871
|
+
input_value = payload.input
|
|
872
|
+
if not is_json_list(input_value):
|
|
873
|
+
return None
|
|
874
|
+
return _strip_terminal_compaction_trigger_input_items(input_value, strip_trigger=strip_trigger)
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
def _strip_terminal_compaction_trigger_input_items(
|
|
878
|
+
input_value: list[JsonValue],
|
|
879
|
+
*,
|
|
880
|
+
strip_trigger: bool,
|
|
881
|
+
) -> list[JsonValue] | None:
|
|
882
|
+
trigger_seen = _validate_terminal_compaction_trigger_input_items(input_value)
|
|
883
|
+
if not trigger_seen:
|
|
884
|
+
return None
|
|
885
|
+
if not strip_trigger:
|
|
886
|
+
return input_value
|
|
887
|
+
return [item for item in input_value if not (is_json_mapping(item) and item.get("type") == "compaction_trigger")]
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
def _validate_terminal_compaction_trigger_input_items(input_value: list[JsonValue]) -> bool:
|
|
891
|
+
trigger_seen = False
|
|
892
|
+
last_index = len(input_value) - 1
|
|
893
|
+
|
|
894
|
+
for index, item in enumerate(input_value):
|
|
895
|
+
if not (is_json_mapping(item) and item.get("type") == "compaction_trigger"):
|
|
896
|
+
continue
|
|
897
|
+
|
|
898
|
+
if trigger_seen or index != last_index:
|
|
899
|
+
raise ClientPayloadError(
|
|
900
|
+
"compaction_trigger must appear exactly once as the final top-level input item",
|
|
901
|
+
param="input",
|
|
902
|
+
code="invalid_request_error",
|
|
903
|
+
error_type="invalid_request_error",
|
|
904
|
+
)
|
|
905
|
+
trigger_seen = True
|
|
906
|
+
|
|
907
|
+
return trigger_seen
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
def responses_source_route_excluded(
|
|
911
|
+
payload: ResponsesRequest,
|
|
912
|
+
*,
|
|
913
|
+
exclude_compaction: bool = True,
|
|
914
|
+
) -> bool:
|
|
915
|
+
"""True when a Responses request must stay on subscription accounts.
|
|
916
|
+
|
|
917
|
+
A terminal compaction trigger is served by the upstream compact flow on
|
|
918
|
+
the turn's owner account (Codex path only — callers set
|
|
919
|
+
``exclude_compaction=False`` on ``/v1/responses``, which has no Codex
|
|
920
|
+
compaction path). An ``input_file``/``input_image`` file reference is
|
|
921
|
+
pinned to the subscription account that received the upload. Previous
|
|
922
|
+
response ownership is resolved separately from recorded continuity
|
|
923
|
+
evidence because response identifier syntax is provider-opaque.
|
|
924
|
+
|
|
925
|
+
Raises ``ClientPayloadError`` for a malformed compaction trigger, exactly
|
|
926
|
+
like ``strip_terminal_compaction_trigger_input``.
|
|
927
|
+
"""
|
|
928
|
+
if exclude_compaction and strip_terminal_compaction_trigger_input(payload) is not None:
|
|
929
|
+
return True
|
|
930
|
+
return bool(extract_input_file_ids(payload.input))
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
def enforce_strict_text_format(request: ResponsesRequest) -> None:
|
|
934
|
+
"""Reject strict-mode JSON schemas that violate OpenAI structured-outputs rules.
|
|
935
|
+
|
|
936
|
+
The Codex backend mirrors OpenAI's strict-mode policy and closes the
|
|
937
|
+
websocket with ``close_code=1000`` (delivering the original
|
|
938
|
+
``invalid_json_schema`` payload via ``response.failed``). The local
|
|
939
|
+
pre-check raises a deterministic 400 before any upstream connection
|
|
940
|
+
is opened, keeping ``/v1/responses`` and the chat-conversion path
|
|
941
|
+
consistent and avoiding pointless retry/reconnect cycles for
|
|
942
|
+
permanently invalid schemas.
|
|
943
|
+
"""
|
|
944
|
+
if request.text is None or request.text.format is None:
|
|
945
|
+
return
|
|
946
|
+
text_format = request.text.format
|
|
947
|
+
if text_format.type != "json_schema" or text_format.strict is not True:
|
|
948
|
+
return
|
|
949
|
+
if text_format.schema_ is None:
|
|
950
|
+
return
|
|
951
|
+
violation = validate_strict_json_schema(
|
|
952
|
+
text_format.schema_,
|
|
953
|
+
name=text_format.name,
|
|
954
|
+
param="text.format.schema",
|
|
955
|
+
)
|
|
956
|
+
if violation is None:
|
|
957
|
+
return
|
|
958
|
+
raise ClientPayloadError(
|
|
959
|
+
violation.message,
|
|
960
|
+
param=violation.param,
|
|
961
|
+
code=violation.code,
|
|
962
|
+
error_type="invalid_request_error",
|
|
963
|
+
)
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
def enforce_strict_function_tools_format(
|
|
967
|
+
tools: list[JsonValue] | None,
|
|
968
|
+
*,
|
|
969
|
+
param_template: str = "tools[{index}].parameters",
|
|
970
|
+
nested: bool = False,
|
|
971
|
+
) -> None:
|
|
972
|
+
"""Reject strict-mode function tools whose parameter schemas violate OpenAI rules.
|
|
973
|
+
|
|
974
|
+
Mirrors :func:`enforce_strict_text_format` for function tools that
|
|
975
|
+
set ``strict: true``. The Codex backend rejects an invalid strict
|
|
976
|
+
tool schema by closing the WebSocket with ``close_code=1000``; the
|
|
977
|
+
surfaced error is a generic ``upstream_rejected_input`` 502, which
|
|
978
|
+
well-behaved retry loops misclassify as transient. Real OpenAI
|
|
979
|
+
returns a deterministic ``400 invalid_function_parameters`` for the
|
|
980
|
+
same payload, so codex-lb pre-validates here.
|
|
981
|
+
|
|
982
|
+
``param_template`` controls how the rejected parameter is named in
|
|
983
|
+
the error envelope: native ``/v1/responses`` callers see
|
|
984
|
+
``tools[<i>].parameters``; chat-completions callers pass
|
|
985
|
+
``"tools[{index}].function.parameters"`` to mirror the inbound
|
|
986
|
+
shape.
|
|
987
|
+
|
|
988
|
+
``nested`` selects the shape of an inbound function tool:
|
|
989
|
+
|
|
990
|
+
* ``False`` (default) — flat ``{"type": "function", "name": ...,
|
|
991
|
+
"parameters": ..., "strict": ...}``. This is the
|
|
992
|
+
``/v1/responses`` request shape (and the shape produced by
|
|
993
|
+
``_normalize_chat_tools``).
|
|
994
|
+
* ``True`` — chat-completions shape with the function payload
|
|
995
|
+
wrapped under ``"function"``:
|
|
996
|
+
``{"type": "function", "function": {"name": ..., "parameters":
|
|
997
|
+
..., "strict": ...}}``. The chat handler MUST pass ``True`` and
|
|
998
|
+
hand in the *original* request payload's ``tools`` list (not the
|
|
999
|
+
list returned by ``ChatCompletionsRequest.to_responses_request``,
|
|
1000
|
+
which may drop or reorder entries) so the ``{index}`` slot in
|
|
1001
|
+
``param_template`` lines up with the inbound payload.
|
|
1002
|
+
|
|
1003
|
+
A tool is treated as a function tool whenever ``tool["function"]``
|
|
1004
|
+
is a dict — the same rule ``_normalize_chat_tools`` uses, where
|
|
1005
|
+
missing or other ``type`` values get coerced to ``"function"``
|
|
1006
|
+
(``"type": tool_type or "function"``). Anchoring on the wrapper
|
|
1007
|
+
key keeps pre-validation in lockstep with normalization; otherwise
|
|
1008
|
+
a payload like ``{"function": {"strict": true, "parameters":
|
|
1009
|
+
<invalid>}}`` (no top-level ``type``) bypasses the local 400 and
|
|
1010
|
+
surfaces as a misleading upstream 5xx.
|
|
1011
|
+
"""
|
|
1012
|
+
if not tools:
|
|
1013
|
+
return
|
|
1014
|
+
for index, tool in enumerate(tools):
|
|
1015
|
+
if not isinstance(tool, dict):
|
|
1016
|
+
continue
|
|
1017
|
+
if nested:
|
|
1018
|
+
# Chat path: detect function tools the same way
|
|
1019
|
+
# ``_normalize_chat_tools`` does — presence of a ``"function"``
|
|
1020
|
+
# dict is the signal, not a strict ``"type" == "function"``
|
|
1021
|
+
# check. The normalizer coerces missing or other ``type``
|
|
1022
|
+
# values to ``"function"`` whenever ``tool["function"]`` is a
|
|
1023
|
+
# dict (`"type": tool_type or "function"`), so pre-validation
|
|
1024
|
+
# must mirror that or strict violations on type-omitted
|
|
1025
|
+
# nested tools bypass the local 400 and surface as upstream
|
|
1026
|
+
# 5xx instead.
|
|
1027
|
+
function_value = tool.get("function")
|
|
1028
|
+
if not isinstance(function_value, dict):
|
|
1029
|
+
continue
|
|
1030
|
+
descriptor = function_value
|
|
1031
|
+
else:
|
|
1032
|
+
if tool.get("type") != "function":
|
|
1033
|
+
continue
|
|
1034
|
+
descriptor = tool
|
|
1035
|
+
if descriptor.get("strict") is not True:
|
|
1036
|
+
continue
|
|
1037
|
+
parameters = descriptor.get("parameters")
|
|
1038
|
+
if parameters is None:
|
|
1039
|
+
continue
|
|
1040
|
+
raw_name = descriptor.get("name")
|
|
1041
|
+
name = raw_name if isinstance(raw_name, str) else None
|
|
1042
|
+
violation = validate_strict_function_tool_schema(
|
|
1043
|
+
parameters,
|
|
1044
|
+
name=name,
|
|
1045
|
+
param=param_template.format(index=index),
|
|
1046
|
+
)
|
|
1047
|
+
if violation is None:
|
|
1048
|
+
continue
|
|
1049
|
+
raise ClientPayloadError(
|
|
1050
|
+
violation.message,
|
|
1051
|
+
param=violation.param,
|
|
1052
|
+
code=violation.code,
|
|
1053
|
+
error_type="invalid_request_error",
|
|
1054
|
+
)
|