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,1980 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from collections.abc import Iterable, Mapping
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import cast
|
|
8
|
+
|
|
9
|
+
from pydantic import (
|
|
10
|
+
BaseModel,
|
|
11
|
+
ConfigDict,
|
|
12
|
+
Field,
|
|
13
|
+
PrivateAttr,
|
|
14
|
+
SerializeAsAny,
|
|
15
|
+
SkipValidation,
|
|
16
|
+
field_validator,
|
|
17
|
+
model_validator,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from app.core.openai.exceptions import ClientPayloadError
|
|
21
|
+
from app.core.openai.tool_call_safety import is_downstream_side_effect_tool_call_item
|
|
22
|
+
from app.core.types import JsonObject, JsonValue
|
|
23
|
+
from app.core.utils.json_guards import is_json_list, is_json_mapping
|
|
24
|
+
|
|
25
|
+
type MutableJsonObject = dict[str, JsonValue]
|
|
26
|
+
|
|
27
|
+
# Passthrough request fields (``input``, ``tools``, ``messages``, ``schema``)
|
|
28
|
+
# arrive from ``json.loads`` and are forwarded upstream as-is, so validating
|
|
29
|
+
# every node against the recursive ``JsonValue`` union only burns CPU (a
|
|
30
|
+
# Python-level ``Mapping`` ABC ``isinstance`` per dict node) and copies the
|
|
31
|
+
# tree. ``SkipValidation`` keeps the client's objects; ``SerializeAsAny``
|
|
32
|
+
# (inner) serializes them by runtime type instead of through a wrap
|
|
33
|
+
# serializer, which keeps ``model_dump(mode="json")`` byte-identical and
|
|
34
|
+
# faster. Field validators still run, so shape checks live there. The
|
|
35
|
+
# subscript form expands to ``Annotated[T, SerializeAsAny(), SkipValidation()]``
|
|
36
|
+
# (``SerializeAsAny`` inner, ``SkipValidation`` outermost) and type-checks as ``T``.
|
|
37
|
+
type PassthroughJsonValue = SkipValidation[SerializeAsAny[JsonValue]]
|
|
38
|
+
type PassthroughJsonList = SkipValidation[SerializeAsAny[list[JsonValue]]]
|
|
39
|
+
|
|
40
|
+
# pydantic-core's serializer refuses container nesting deeper than ~250 levels
|
|
41
|
+
# (a 500 from ``model_dump`` long after validation); deep validation used to
|
|
42
|
+
# reject such bodies first (``recursion_loop`` -> 400), so the passthrough
|
|
43
|
+
# validators keep that contract with an explicit, cheaper depth check.
|
|
44
|
+
PASSTHROUGH_MAX_DEPTH = 200
|
|
45
|
+
_JSON_CONTAINER_TYPES = (dict, list)
|
|
46
|
+
|
|
47
|
+
_RESPONSES_INCLUDE_ALLOWLIST = {
|
|
48
|
+
"code_interpreter_call.outputs",
|
|
49
|
+
"computer_call_output.output.image_url",
|
|
50
|
+
"file_search_call.results",
|
|
51
|
+
"message.input_image.image_url",
|
|
52
|
+
"message.output_text.logprobs",
|
|
53
|
+
"output.encrypted_content",
|
|
54
|
+
"reasoning.encrypted_content",
|
|
55
|
+
"web_search_call.action.sources",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
UNSUPPORTED_TOOL_TYPES = {
|
|
59
|
+
"file_search",
|
|
60
|
+
"code_interpreter",
|
|
61
|
+
"computer_use",
|
|
62
|
+
"computer_use_preview",
|
|
63
|
+
"image_generation",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
_TOOL_TYPE_ALIASES = {
|
|
67
|
+
"web_search_preview": "web_search",
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
_INTERLEAVED_REASONING_KEYS = frozenset({"reasoning_content", "reasoning_details", "tool_calls", "function_call"})
|
|
71
|
+
_INTERLEAVED_REASONING_PART_TYPES = frozenset({"reasoning", "reasoning_content", "reasoning_details"})
|
|
72
|
+
_ASSISTANT_TEXT_PART_TYPES = frozenset({"text", "input_text", "output_text"})
|
|
73
|
+
_TOOL_TEXT_PART_TYPES = frozenset({"text", "input_text", "output_text", "refusal"})
|
|
74
|
+
_COMPACT_STATE_TOOL_NAMES = frozenset({"create_goal", "get_goal", "update_goal", "update_plan"})
|
|
75
|
+
# A call_id can be reused by different item protocols. Compact pairing must
|
|
76
|
+
# therefore keep each protocol's occurrence stream separate.
|
|
77
|
+
_COMPACT_TOOL_CALL_TYPE_BY_OUTPUT_TYPE: dict[str, str] = {
|
|
78
|
+
"function_call_output": "function_call",
|
|
79
|
+
"custom_tool_call_output": "custom_tool_call",
|
|
80
|
+
"apply_patch_call_output": "apply_patch_call",
|
|
81
|
+
}
|
|
82
|
+
_COMPACT_TOOL_CALL_ITEM_TYPES = frozenset(_COMPACT_TOOL_CALL_TYPE_BY_OUTPUT_TYPE.values())
|
|
83
|
+
_COMPACT_TOOL_CALL_OUTPUT_ITEM_TYPES = frozenset(_COMPACT_TOOL_CALL_TYPE_BY_OUTPUT_TYPE)
|
|
84
|
+
_COMPACT_SIDE_EFFECT_TOOL_ITEM_TYPES = frozenset({"apply_patch_call", "apply_patch_call_output"})
|
|
85
|
+
_TOOL_CALL_ITEM_TYPES = frozenset(_COMPACT_TOOL_CALL_TYPE_BY_OUTPUT_TYPE.values())
|
|
86
|
+
_COMPACT_INLINE_IMAGE_DATA_URL_RE = re.compile(r"""data:image/[^,\s]+,[^\s"'<>]+""")
|
|
87
|
+
_GOAL_CONTINUATION_CONTEXT_PREFIX = '<codex_internal_context source="goal">'
|
|
88
|
+
_PLAN_MODE_CONTEXT_PREFIX = "<collaboration_mode># Plan Mode"
|
|
89
|
+
_EXPLICIT_PROMPT_CACHE_CONTENT_TYPES = frozenset({"input_text", "input_image", "input_file"})
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _json_mapping_or_none(value: JsonValue) -> Mapping[str, JsonValue] | None:
|
|
93
|
+
if not is_json_mapping(value):
|
|
94
|
+
return None
|
|
95
|
+
return value
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _json_parts(value: JsonValue) -> list[JsonValue]:
|
|
99
|
+
if is_json_list(value):
|
|
100
|
+
return value
|
|
101
|
+
return [value]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def normalize_tool_type(tool_type: str) -> str:
|
|
105
|
+
return _TOOL_TYPE_ALIASES.get(tool_type, tool_type)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def normalize_tool_choice(choice: JsonValue | None) -> JsonValue | None:
|
|
109
|
+
if not is_json_mapping(choice):
|
|
110
|
+
return choice
|
|
111
|
+
choice_mapping = choice
|
|
112
|
+
tool_type = choice_mapping.get("type")
|
|
113
|
+
if isinstance(tool_type, str):
|
|
114
|
+
normalized_type = normalize_tool_type(tool_type)
|
|
115
|
+
if normalized_type != tool_type:
|
|
116
|
+
updated = dict(choice_mapping)
|
|
117
|
+
updated["type"] = normalized_type
|
|
118
|
+
return updated
|
|
119
|
+
return choice
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def validate_passthrough_depth(value: object, *, limit: int = PASSTHROUGH_MAX_DEPTH) -> None:
|
|
123
|
+
"""Reject JSON nested deeper than ``limit`` container levels (iterative walk over
|
|
124
|
+
``dict``/``list`` nodes; ``isinstance`` because the WebSocket parser yields ``dict`` subclasses)."""
|
|
125
|
+
if not isinstance(value, _JSON_CONTAINER_TYPES):
|
|
126
|
+
return
|
|
127
|
+
frontier: list[object] = [value]
|
|
128
|
+
depth = 0
|
|
129
|
+
while frontier:
|
|
130
|
+
depth += 1
|
|
131
|
+
if depth > limit:
|
|
132
|
+
raise ValueError(f"nesting exceeds {limit} levels")
|
|
133
|
+
next_frontier: list[object] = []
|
|
134
|
+
for node in frontier:
|
|
135
|
+
children: Iterable[object] = node.values() if isinstance(node, dict) else cast(list[object], node)
|
|
136
|
+
next_frontier.extend([child for child in children if isinstance(child, _JSON_CONTAINER_TYPES)])
|
|
137
|
+
frontier = next_frontier
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def validate_tool_types(tools: list[JsonValue], *, allow_builtin_tools: bool = False) -> list[JsonValue]:
|
|
141
|
+
# ``tools`` skips pydantic's ``list`` validation, so the array check that
|
|
142
|
+
# used to yield ``list_type`` lives here (same ``param``: ``tools``).
|
|
143
|
+
if not isinstance(tools, list):
|
|
144
|
+
raise ValueError("tools must be an array")
|
|
145
|
+
normalized_tools: list[JsonValue] = []
|
|
146
|
+
for tool in tools:
|
|
147
|
+
if not is_json_mapping(tool):
|
|
148
|
+
normalized_tools.append(tool)
|
|
149
|
+
continue
|
|
150
|
+
tool_mapping = tool
|
|
151
|
+
tool_type = tool_mapping.get("type")
|
|
152
|
+
if isinstance(tool_type, str):
|
|
153
|
+
normalized_type = normalize_tool_type(tool_type)
|
|
154
|
+
if normalized_type != tool_type:
|
|
155
|
+
tool = dict(tool_mapping)
|
|
156
|
+
tool["type"] = normalized_type
|
|
157
|
+
tool_type = normalized_type
|
|
158
|
+
if not allow_builtin_tools and tool_type in UNSUPPORTED_TOOL_TYPES:
|
|
159
|
+
raise ValueError(f"Unsupported tool type: {tool_type}")
|
|
160
|
+
normalized_tools.append(tool)
|
|
161
|
+
return normalized_tools
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _is_input_file_with_id(item: Mapping[str, JsonValue]) -> bool:
|
|
165
|
+
if item.get("type") != "input_file":
|
|
166
|
+
return False
|
|
167
|
+
file_id = item.get("file_id")
|
|
168
|
+
return isinstance(file_id, str) and bool(file_id)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@dataclass(frozen=True, slots=True)
|
|
172
|
+
class InputImageFileReference:
|
|
173
|
+
item_index: int
|
|
174
|
+
content_index: int | None
|
|
175
|
+
file_id: str
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _input_image_file_reference(item: Mapping[str, JsonValue]) -> str | None:
|
|
179
|
+
if item.get("type") != "input_image":
|
|
180
|
+
return None
|
|
181
|
+
file_id = item.get("file_id")
|
|
182
|
+
if isinstance(file_id, str) and file_id:
|
|
183
|
+
return file_id
|
|
184
|
+
image_url = item.get("image_url")
|
|
185
|
+
if not isinstance(image_url, str) or not image_url.startswith("sediment://"):
|
|
186
|
+
return None
|
|
187
|
+
resolved = image_url.removeprefix("sediment://").strip()
|
|
188
|
+
return resolved or None
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def extract_input_file_ids(input_value: JsonValue) -> set[str]:
|
|
192
|
+
"""Return all ``file_id`` strings referenced by ``input_file`` / ``input_image`` items.
|
|
193
|
+
|
|
194
|
+
Walks top-level input items, role-message ``content`` parts, and retained
|
|
195
|
+
tool-output content, matching the actual reference shapes accepted by
|
|
196
|
+
``ResponsesRequest.input`` / ``ResponsesCompactRequest.input``. Tool
|
|
197
|
+
metadata and arbitrary nested objects are deliberately not references.
|
|
198
|
+
Returns an empty set when the input is a plain string or has no
|
|
199
|
+
``input_file`` parts. Used by the
|
|
200
|
+
``/responses`` flow to look up account pins recorded by
|
|
201
|
+
``POST /backend-api/files`` so the response request lands on the
|
|
202
|
+
upstream account that registered the file (the upstream contract is
|
|
203
|
+
account-scoped via ``chatgpt-account-id``).
|
|
204
|
+
"""
|
|
205
|
+
if not is_json_list(input_value):
|
|
206
|
+
return set()
|
|
207
|
+
file_ids: set[str] = set()
|
|
208
|
+
|
|
209
|
+
def collect_part(part: JsonValue) -> None:
|
|
210
|
+
if not is_json_mapping(part):
|
|
211
|
+
return
|
|
212
|
+
if _is_input_file_with_id(part):
|
|
213
|
+
file_id = part.get("file_id")
|
|
214
|
+
if isinstance(file_id, str) and file_id:
|
|
215
|
+
file_ids.add(file_id)
|
|
216
|
+
image_file_id = _input_image_file_reference(part)
|
|
217
|
+
if image_file_id is not None:
|
|
218
|
+
file_ids.add(image_file_id)
|
|
219
|
+
|
|
220
|
+
for item in input_value:
|
|
221
|
+
if not is_json_mapping(item):
|
|
222
|
+
continue
|
|
223
|
+
collect_part(item)
|
|
224
|
+
for part in _json_parts(item.get("content")):
|
|
225
|
+
collect_part(part)
|
|
226
|
+
if item.get("type") in _COMPACT_TOOL_CALL_OUTPUT_ITEM_TYPES:
|
|
227
|
+
for part in _json_parts(item.get("output")):
|
|
228
|
+
collect_part(part)
|
|
229
|
+
return file_ids
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _append_input_image_file_references(
|
|
233
|
+
references: list[InputImageFileReference],
|
|
234
|
+
value: JsonValue,
|
|
235
|
+
*,
|
|
236
|
+
item_index: int,
|
|
237
|
+
content_index: int | None,
|
|
238
|
+
) -> None:
|
|
239
|
+
if is_json_mapping(value):
|
|
240
|
+
file_id = _input_image_file_reference(value)
|
|
241
|
+
if file_id is not None:
|
|
242
|
+
references.append(
|
|
243
|
+
InputImageFileReference(
|
|
244
|
+
item_index=item_index,
|
|
245
|
+
content_index=content_index,
|
|
246
|
+
file_id=file_id,
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
for child in value.values():
|
|
250
|
+
_append_input_image_file_references(
|
|
251
|
+
references,
|
|
252
|
+
child,
|
|
253
|
+
item_index=item_index,
|
|
254
|
+
content_index=content_index,
|
|
255
|
+
)
|
|
256
|
+
return
|
|
257
|
+
if is_json_list(value):
|
|
258
|
+
for child in value:
|
|
259
|
+
_append_input_image_file_references(
|
|
260
|
+
references,
|
|
261
|
+
child,
|
|
262
|
+
item_index=item_index,
|
|
263
|
+
content_index=content_index,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def extract_input_image_file_references(input_value: JsonValue) -> list[InputImageFileReference]:
|
|
268
|
+
if not is_json_list(input_value):
|
|
269
|
+
return []
|
|
270
|
+
references: list[InputImageFileReference] = []
|
|
271
|
+
for item_index, item in enumerate(input_value):
|
|
272
|
+
if not is_json_mapping(item):
|
|
273
|
+
continue
|
|
274
|
+
item_mapping = item
|
|
275
|
+
top_level_file_id = _input_image_file_reference(item_mapping)
|
|
276
|
+
if top_level_file_id is not None:
|
|
277
|
+
references.append(
|
|
278
|
+
InputImageFileReference(
|
|
279
|
+
item_index=item_index,
|
|
280
|
+
content_index=None,
|
|
281
|
+
file_id=top_level_file_id,
|
|
282
|
+
)
|
|
283
|
+
)
|
|
284
|
+
content = item_mapping.get("content")
|
|
285
|
+
if is_json_list(content):
|
|
286
|
+
parts: list[JsonValue] = content
|
|
287
|
+
elif is_json_mapping(content):
|
|
288
|
+
parts = [content]
|
|
289
|
+
else:
|
|
290
|
+
parts = []
|
|
291
|
+
for content_index, part in enumerate(parts):
|
|
292
|
+
_append_input_image_file_references(
|
|
293
|
+
references,
|
|
294
|
+
part,
|
|
295
|
+
item_index=item_index,
|
|
296
|
+
content_index=content_index,
|
|
297
|
+
)
|
|
298
|
+
output = item_mapping.get("output")
|
|
299
|
+
_append_input_image_file_references(
|
|
300
|
+
references,
|
|
301
|
+
output,
|
|
302
|
+
item_index=item_index,
|
|
303
|
+
content_index=None,
|
|
304
|
+
)
|
|
305
|
+
return references
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _is_preserved_non_message_directive(item: Mapping[str, JsonValue]) -> bool:
|
|
309
|
+
# Shared preservation rule: any system/developer-role input item whose type
|
|
310
|
+
# is present and not "message" must reach upstream byte-identical. It gates
|
|
311
|
+
# instruction hoisting, input sanitization, and compact trim anchoring.
|
|
312
|
+
if item.get("role") not in ("system", "developer"):
|
|
313
|
+
return False
|
|
314
|
+
item_type = item.get("type")
|
|
315
|
+
return item_type is not None and item_type != "message"
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _sanitize_input_items(input_items: list[JsonValue]) -> list[JsonValue]:
|
|
319
|
+
sanitized_input: list[JsonValue] = []
|
|
320
|
+
for item in input_items:
|
|
321
|
+
item_mapping = _json_mapping_or_none(item)
|
|
322
|
+
if item_mapping is not None and _is_preserved_non_message_directive(item_mapping):
|
|
323
|
+
# Preserved directives are forwarded unchanged; the interleaved
|
|
324
|
+
# reasoning sanitizer targets assistant-message echo fields and
|
|
325
|
+
# must not strip keys such as tool_calls or reasoning_content
|
|
326
|
+
# from a typed directive.
|
|
327
|
+
sanitized_input.append(item)
|
|
328
|
+
continue
|
|
329
|
+
sanitized_item = _sanitize_interleaved_reasoning_input_item(item)
|
|
330
|
+
if sanitized_item is None:
|
|
331
|
+
continue
|
|
332
|
+
sanitized_input.append(_normalize_role_input_item(sanitized_item))
|
|
333
|
+
return sanitized_input
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _normalize_responses_input_instructions(data: JsonValue) -> JsonValue:
|
|
337
|
+
if not is_json_mapping(data):
|
|
338
|
+
return data
|
|
339
|
+
input_value = data.get("input")
|
|
340
|
+
if not is_json_list(input_value):
|
|
341
|
+
return data
|
|
342
|
+
# Codex deliberately places the Lite tool bundle and base instructions in
|
|
343
|
+
# the input prefix. Keep that wire shape intact instead of lifting its
|
|
344
|
+
# developer message into the top-level ``instructions`` field.
|
|
345
|
+
if responses_input_uses_lite_tools(input_value):
|
|
346
|
+
return data
|
|
347
|
+
|
|
348
|
+
instruction_parts: list[str] = []
|
|
349
|
+
input_items: list[JsonValue] = []
|
|
350
|
+
changed = False
|
|
351
|
+
for item in input_value:
|
|
352
|
+
item_mapping = _json_mapping_or_none(item)
|
|
353
|
+
if item_mapping is None:
|
|
354
|
+
input_items.append(item)
|
|
355
|
+
continue
|
|
356
|
+
# Only hoist actual message items (type omitted or "message"). Non-message
|
|
357
|
+
# typed system/developer items (e.g. the Codex responses-lite
|
|
358
|
+
# {"type": "additional_tools", "role": "developer", "tools": [...]} bundle, or
|
|
359
|
+
# any future typed item) carry no instruction content; hoisting used to drop
|
|
360
|
+
# them entirely, so pass them through untouched regardless of role.
|
|
361
|
+
if _is_preserved_non_message_directive(item_mapping):
|
|
362
|
+
input_items.append(item)
|
|
363
|
+
# Still counts as a normalization outcome: directive-only inputs
|
|
364
|
+
# must default top-level ``instructions`` (to "") so the request
|
|
365
|
+
# validates instead of failing on the required field.
|
|
366
|
+
changed = True
|
|
367
|
+
continue
|
|
368
|
+
role = item_mapping.get("role")
|
|
369
|
+
if role not in ("system", "developer"):
|
|
370
|
+
input_items.append(item)
|
|
371
|
+
continue
|
|
372
|
+
instruction_text, preserved_content = _split_responses_instruction_item_content(item_mapping)
|
|
373
|
+
if instruction_text:
|
|
374
|
+
instruction_parts.append(instruction_text)
|
|
375
|
+
if preserved_content is not None:
|
|
376
|
+
preserved_item = dict(item_mapping)
|
|
377
|
+
preserved_item["role"] = "user"
|
|
378
|
+
preserved_item["content"] = preserved_content
|
|
379
|
+
input_items.append(preserved_item)
|
|
380
|
+
changed = True
|
|
381
|
+
|
|
382
|
+
if not changed:
|
|
383
|
+
return data
|
|
384
|
+
|
|
385
|
+
normalized: MutableJsonObject = dict(data)
|
|
386
|
+
existing_instructions = normalized.get("instructions")
|
|
387
|
+
merged_instructions = _merge_responses_instructions(
|
|
388
|
+
existing_instructions if isinstance(existing_instructions, str) else "",
|
|
389
|
+
instruction_parts,
|
|
390
|
+
)
|
|
391
|
+
normalized["instructions"] = merged_instructions
|
|
392
|
+
normalized["input"] = input_items
|
|
393
|
+
return normalized
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _is_responses_lite_input(input_value: list[JsonValue]) -> bool:
|
|
397
|
+
# Responses Lite requests carry their tool bundle as an input item with
|
|
398
|
+
# type=additional_tools and deliberately place base instructions as a
|
|
399
|
+
# developer message in input (with empty top-level instructions). That
|
|
400
|
+
# shape is exactly what the lite upstream expects, so the instruction
|
|
401
|
+
# lift must leave the whole request untouched.
|
|
402
|
+
return any(
|
|
403
|
+
(mapping := _json_mapping_or_none(item)) is not None and mapping.get("type") == "additional_tools"
|
|
404
|
+
for item in input_value
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def responses_input_uses_lite_tools(input_value: JsonValue) -> bool:
|
|
409
|
+
return is_json_list(input_value) and _is_responses_lite_input(input_value)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _merge_responses_instructions(existing: str, extra_parts: list[str]) -> str:
|
|
413
|
+
extra = "\n".join(part for part in extra_parts if part)
|
|
414
|
+
if not extra:
|
|
415
|
+
return existing
|
|
416
|
+
if existing:
|
|
417
|
+
return f"{existing}\n{extra}"
|
|
418
|
+
return extra
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _split_responses_instruction_item_content(item: Mapping[str, JsonValue]) -> tuple[str, JsonValue | None]:
|
|
422
|
+
content = item.get("content")
|
|
423
|
+
if content is None:
|
|
424
|
+
return "", None
|
|
425
|
+
if isinstance(content, str):
|
|
426
|
+
return content, None
|
|
427
|
+
if is_json_list(content):
|
|
428
|
+
instruction_parts: list[str] = []
|
|
429
|
+
preserved_parts: list[JsonValue] = []
|
|
430
|
+
for part in _json_parts(content):
|
|
431
|
+
text = _responses_instruction_content_text(part)
|
|
432
|
+
if text is not None:
|
|
433
|
+
if text:
|
|
434
|
+
instruction_parts.append(text)
|
|
435
|
+
continue
|
|
436
|
+
preserved_parts.append(part)
|
|
437
|
+
preserved_content: JsonValue | None = preserved_parts if preserved_parts else None
|
|
438
|
+
return "\n".join(instruction_parts), preserved_content
|
|
439
|
+
text = _responses_instruction_content_text(content)
|
|
440
|
+
if text is not None:
|
|
441
|
+
return text, None
|
|
442
|
+
return "", content
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _responses_instruction_content_text(content: JsonValue) -> str | None:
|
|
446
|
+
if isinstance(content, str):
|
|
447
|
+
return content
|
|
448
|
+
content_mapping = _json_mapping_or_none(content)
|
|
449
|
+
if content_mapping is None:
|
|
450
|
+
return None
|
|
451
|
+
text = content_mapping.get("text")
|
|
452
|
+
return text if isinstance(text, str) else None
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _sanitize_interleaved_reasoning_input_item(item: JsonValue) -> JsonValue | None:
|
|
456
|
+
item_mapping = _json_mapping_or_none(item)
|
|
457
|
+
if item_mapping is None:
|
|
458
|
+
return item
|
|
459
|
+
|
|
460
|
+
sanitized_item: MutableJsonObject = {}
|
|
461
|
+
for key, value in item_mapping.items():
|
|
462
|
+
if key in _INTERLEAVED_REASONING_KEYS:
|
|
463
|
+
continue
|
|
464
|
+
if key == "content":
|
|
465
|
+
sanitized_content = _sanitize_interleaved_reasoning_content(value)
|
|
466
|
+
if sanitized_content is None:
|
|
467
|
+
continue
|
|
468
|
+
sanitized_item[key] = sanitized_content
|
|
469
|
+
continue
|
|
470
|
+
sanitized_item[key] = value
|
|
471
|
+
return sanitized_item
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _sanitize_interleaved_reasoning_content(content: JsonValue) -> JsonValue | None:
|
|
475
|
+
if is_json_list(content):
|
|
476
|
+
sanitized_parts: list[JsonValue] = []
|
|
477
|
+
for part in _json_parts(content):
|
|
478
|
+
sanitized_part = _sanitize_interleaved_reasoning_content_part(part)
|
|
479
|
+
if sanitized_part is None:
|
|
480
|
+
continue
|
|
481
|
+
sanitized_parts.append(sanitized_part)
|
|
482
|
+
return sanitized_parts
|
|
483
|
+
content_mapping = _json_mapping_or_none(content)
|
|
484
|
+
if content_mapping is not None:
|
|
485
|
+
return _sanitize_interleaved_reasoning_content_part(content_mapping)
|
|
486
|
+
return content
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _sanitize_interleaved_reasoning_content_part(part: JsonValue) -> JsonValue | None:
|
|
490
|
+
part_mapping = _json_mapping_or_none(part)
|
|
491
|
+
if part_mapping is None:
|
|
492
|
+
return part
|
|
493
|
+
|
|
494
|
+
part_type = part_mapping.get("type")
|
|
495
|
+
if isinstance(part_type, str) and part_type in _INTERLEAVED_REASONING_PART_TYPES:
|
|
496
|
+
return None
|
|
497
|
+
|
|
498
|
+
sanitized_part = dict(part_mapping)
|
|
499
|
+
for key in _INTERLEAVED_REASONING_KEYS:
|
|
500
|
+
sanitized_part.pop(key, None)
|
|
501
|
+
return sanitized_part
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _normalize_role_input_item(value: JsonValue) -> JsonValue:
|
|
505
|
+
value_mapping = _json_mapping_or_none(value)
|
|
506
|
+
if value_mapping is None:
|
|
507
|
+
return value
|
|
508
|
+
role = value_mapping.get("role")
|
|
509
|
+
if role == "assistant":
|
|
510
|
+
return _normalize_assistant_input_item(value_mapping)
|
|
511
|
+
if role == "tool":
|
|
512
|
+
return _normalize_tool_input_item(value_mapping)
|
|
513
|
+
return value
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def _normalize_tool_input_item(value: Mapping[str, JsonValue]) -> JsonValue:
|
|
517
|
+
tool_call_id = value.get("tool_call_id")
|
|
518
|
+
tool_call_id_camel = value.get("toolCallId")
|
|
519
|
+
call_id = value.get("call_id")
|
|
520
|
+
resolved_call_id = tool_call_id if isinstance(tool_call_id, str) and tool_call_id else None
|
|
521
|
+
if resolved_call_id is None and isinstance(tool_call_id_camel, str) and tool_call_id_camel:
|
|
522
|
+
resolved_call_id = tool_call_id_camel
|
|
523
|
+
if resolved_call_id is None and isinstance(call_id, str) and call_id:
|
|
524
|
+
resolved_call_id = call_id
|
|
525
|
+
if not isinstance(resolved_call_id, str) or not resolved_call_id:
|
|
526
|
+
raise ValueError("tool input items must include 'tool_call_id'")
|
|
527
|
+
output = value.get("output")
|
|
528
|
+
output_value = output if output is not None else value.get("content")
|
|
529
|
+
return {
|
|
530
|
+
"type": "function_call_output",
|
|
531
|
+
"call_id": resolved_call_id,
|
|
532
|
+
"output": _normalize_tool_output_value(output_value),
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def _normalize_tool_output_value(content: JsonValue) -> str:
|
|
537
|
+
if content is None:
|
|
538
|
+
return ""
|
|
539
|
+
if isinstance(content, str):
|
|
540
|
+
return content
|
|
541
|
+
if is_json_list(content):
|
|
542
|
+
parts: list[str] = []
|
|
543
|
+
for part in _json_parts(content):
|
|
544
|
+
if isinstance(part, str):
|
|
545
|
+
parts.append(part)
|
|
546
|
+
continue
|
|
547
|
+
extracted = _extract_text_content_part(part, _TOOL_TEXT_PART_TYPES)
|
|
548
|
+
if extracted is not None:
|
|
549
|
+
parts.append(extracted)
|
|
550
|
+
if parts:
|
|
551
|
+
return "".join(parts)
|
|
552
|
+
return json.dumps(content, ensure_ascii=False, separators=(",", ":"))
|
|
553
|
+
if is_json_mapping(content):
|
|
554
|
+
extracted = _extract_text_content_part(content, _TOOL_TEXT_PART_TYPES)
|
|
555
|
+
if extracted is not None:
|
|
556
|
+
return extracted
|
|
557
|
+
return json.dumps(content, ensure_ascii=False, separators=(",", ":"))
|
|
558
|
+
return str(content)
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def _normalize_assistant_input_item(value: Mapping[str, JsonValue]) -> JsonValue:
|
|
562
|
+
content = value.get("content")
|
|
563
|
+
normalized_content = _normalize_assistant_content(content)
|
|
564
|
+
if normalized_content == content:
|
|
565
|
+
return value
|
|
566
|
+
updated = dict(value)
|
|
567
|
+
updated["content"] = normalized_content
|
|
568
|
+
return updated
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def _normalize_assistant_content(content: JsonValue) -> JsonValue:
|
|
572
|
+
if content is None:
|
|
573
|
+
return None
|
|
574
|
+
if isinstance(content, str):
|
|
575
|
+
return cast(JsonValue, [{"type": "output_text", "text": content}])
|
|
576
|
+
if is_json_list(content):
|
|
577
|
+
return cast(JsonValue, [_normalize_assistant_content_part(part) for part in _json_parts(content)])
|
|
578
|
+
content_mapping = _json_mapping_or_none(content)
|
|
579
|
+
if content_mapping is not None:
|
|
580
|
+
return [_normalize_assistant_content_part(content_mapping)]
|
|
581
|
+
return content
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def _normalize_assistant_content_part(part: JsonValue) -> JsonValue:
|
|
585
|
+
if isinstance(part, str):
|
|
586
|
+
return {"type": "output_text", "text": part}
|
|
587
|
+
if not is_json_mapping(part):
|
|
588
|
+
return part
|
|
589
|
+
text = _extract_text_content_part(part, _ASSISTANT_TEXT_PART_TYPES)
|
|
590
|
+
if text is not None:
|
|
591
|
+
return {"type": "output_text", "text": text}
|
|
592
|
+
return part
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def _extract_text_content_part(part: JsonValue, allowed_types: frozenset[str]) -> str | None:
|
|
596
|
+
part_mapping = _json_mapping_or_none(part)
|
|
597
|
+
if part_mapping is None:
|
|
598
|
+
return None
|
|
599
|
+
part_type = part_mapping.get("type")
|
|
600
|
+
text = part_mapping.get("text")
|
|
601
|
+
if ((isinstance(part_type, str) and part_type in allowed_types) or part_type is None) and isinstance(text, str):
|
|
602
|
+
return text
|
|
603
|
+
refusal = part_mapping.get("refusal")
|
|
604
|
+
if isinstance(part_type, str) and part_type == "refusal" and isinstance(refusal, str):
|
|
605
|
+
return refusal
|
|
606
|
+
return None
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def _json_list_or_none(value: JsonValue) -> list[JsonValue] | None:
|
|
610
|
+
if not is_json_list(value):
|
|
611
|
+
return None
|
|
612
|
+
return value
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
class ResponsesReasoning(BaseModel):
|
|
616
|
+
model_config = ConfigDict(extra="allow")
|
|
617
|
+
|
|
618
|
+
effort: str | None = None
|
|
619
|
+
summary: str | None = None
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
class ResponsesTextFormat(BaseModel):
|
|
623
|
+
model_config = ConfigDict(extra="allow", populate_by_name=True, serialize_by_alias=True)
|
|
624
|
+
|
|
625
|
+
type: str | None = None
|
|
626
|
+
strict: bool | None = None
|
|
627
|
+
schema_: PassthroughJsonValue = Field(default=None, alias="schema")
|
|
628
|
+
name: str | None = None
|
|
629
|
+
|
|
630
|
+
@field_validator("schema_")
|
|
631
|
+
@classmethod
|
|
632
|
+
def _validate_schema_depth(cls, value: JsonValue) -> JsonValue:
|
|
633
|
+
validate_passthrough_depth(value)
|
|
634
|
+
return value
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
class ResponsesTextControls(BaseModel):
|
|
638
|
+
model_config = ConfigDict(extra="allow")
|
|
639
|
+
|
|
640
|
+
verbosity: str | None = None
|
|
641
|
+
format: ResponsesTextFormat | None = None
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
class ResponsesRequest(BaseModel):
|
|
645
|
+
model_config = ConfigDict(extra="allow")
|
|
646
|
+
_codex_lb_client_reasoning_effort: str | None = PrivateAttr(default=None)
|
|
647
|
+
_codex_lb_provider_reasoning_effort_materialized: bool = PrivateAttr(default=False)
|
|
648
|
+
|
|
649
|
+
@model_validator(mode="before")
|
|
650
|
+
@classmethod
|
|
651
|
+
def _move_input_instruction_messages(cls, data: JsonValue) -> JsonValue:
|
|
652
|
+
return _normalize_responses_input_instructions(data)
|
|
653
|
+
|
|
654
|
+
model: str = Field(min_length=1)
|
|
655
|
+
instructions: str
|
|
656
|
+
input: PassthroughJsonValue
|
|
657
|
+
tools: PassthroughJsonList = Field(default_factory=list)
|
|
658
|
+
tool_choice: str | JsonObject | None = None
|
|
659
|
+
parallel_tool_calls: bool | None = None
|
|
660
|
+
reasoning: ResponsesReasoning | None = None
|
|
661
|
+
store: bool = False
|
|
662
|
+
stream: bool | None = None
|
|
663
|
+
include: list[str] = Field(default_factory=list)
|
|
664
|
+
service_tier: str | None = None
|
|
665
|
+
conversation: str | None = None
|
|
666
|
+
previous_response_id: str | None = None
|
|
667
|
+
truncation: str | None = None
|
|
668
|
+
prompt_cache_key: str | None = None
|
|
669
|
+
text: ResponsesTextControls | None = None
|
|
670
|
+
|
|
671
|
+
@field_validator("input")
|
|
672
|
+
@classmethod
|
|
673
|
+
def _validate_input_type(cls, value: JsonValue) -> JsonValue:
|
|
674
|
+
# ``input_file`` content items with a ``file_id`` are now allowed
|
|
675
|
+
# and forwarded verbatim. They reference uploads registered via
|
|
676
|
+
# ``POST /backend-api/files`` (see the file upload protocol),
|
|
677
|
+
# which lets large attachments bypass the 16 MiB websocket
|
|
678
|
+
# ceiling on `/responses`.
|
|
679
|
+
if isinstance(value, str):
|
|
680
|
+
normalized = _normalize_input_text(value)
|
|
681
|
+
return _sanitize_input_items(normalized)
|
|
682
|
+
if is_json_list(value):
|
|
683
|
+
validate_passthrough_depth(value)
|
|
684
|
+
return _sanitize_input_items(value)
|
|
685
|
+
raise ValueError("input must be a string or array")
|
|
686
|
+
|
|
687
|
+
@field_validator("include")
|
|
688
|
+
@classmethod
|
|
689
|
+
def _validate_include(cls, value: list[str]) -> list[str]:
|
|
690
|
+
for entry in value:
|
|
691
|
+
if entry not in _RESPONSES_INCLUDE_ALLOWLIST:
|
|
692
|
+
raise ValueError(f"Unsupported include value: {entry}")
|
|
693
|
+
return value
|
|
694
|
+
|
|
695
|
+
@field_validator("truncation")
|
|
696
|
+
@classmethod
|
|
697
|
+
def _validate_truncation(cls, value: str | None) -> str | None:
|
|
698
|
+
if value is None:
|
|
699
|
+
return value
|
|
700
|
+
if value not in {"auto", "disabled"}:
|
|
701
|
+
raise ValueError("truncation must be 'auto' or 'disabled'")
|
|
702
|
+
return value
|
|
703
|
+
|
|
704
|
+
@field_validator("store")
|
|
705
|
+
@classmethod
|
|
706
|
+
def _ensure_store_false(cls, value: bool | None) -> bool:
|
|
707
|
+
return False
|
|
708
|
+
|
|
709
|
+
@field_validator("previous_response_id")
|
|
710
|
+
@classmethod
|
|
711
|
+
def _normalize_previous_response_id(cls, value: str | None) -> str | None:
|
|
712
|
+
if value is None:
|
|
713
|
+
return value
|
|
714
|
+
stripped = value.strip()
|
|
715
|
+
return stripped or None
|
|
716
|
+
|
|
717
|
+
@field_validator("tools")
|
|
718
|
+
@classmethod
|
|
719
|
+
def _validate_tools(cls, value: list[JsonValue]) -> list[JsonValue]:
|
|
720
|
+
validated = validate_tool_types(value, allow_builtin_tools=True)
|
|
721
|
+
validate_passthrough_depth(validated)
|
|
722
|
+
return validated
|
|
723
|
+
|
|
724
|
+
@field_validator("tool_choice")
|
|
725
|
+
@classmethod
|
|
726
|
+
def _normalize_tool_choice_field(cls, value: JsonValue | None) -> JsonValue | None:
|
|
727
|
+
return normalize_tool_choice(value)
|
|
728
|
+
|
|
729
|
+
@field_validator("service_tier")
|
|
730
|
+
@classmethod
|
|
731
|
+
def _normalize_service_tier_field(cls, value: str | None) -> str | None:
|
|
732
|
+
if value is None:
|
|
733
|
+
return None
|
|
734
|
+
normalized = _normalize_service_tier_alias_value(value)
|
|
735
|
+
return normalized if isinstance(normalized, str) else value
|
|
736
|
+
|
|
737
|
+
@model_validator(mode="after")
|
|
738
|
+
def _validate_conversation(self) -> "ResponsesRequest":
|
|
739
|
+
if self.conversation and self.previous_response_id:
|
|
740
|
+
raise ValueError("Provide either 'conversation' or 'previous_response_id', not both.")
|
|
741
|
+
return self
|
|
742
|
+
|
|
743
|
+
def model_dump_for_forwarding(self) -> MutableJsonObject:
|
|
744
|
+
"""Dump the request for re-serialization onto another wire.
|
|
745
|
+
|
|
746
|
+
Like ``model_dump(mode="json", exclude_none=True)`` but without
|
|
747
|
+
synthesizing fields the client never sent. Used by every path that
|
|
748
|
+
forwards this request as a JSON body — the multi-instance owner
|
|
749
|
+
forward (``HTTPBridgeOwnerClient``) and model-source Responses
|
|
750
|
+
egress — so that field omission survives the hop and the receiving
|
|
751
|
+
side does not re-mark ``tools`` as explicitly set.
|
|
752
|
+
"""
|
|
753
|
+
payload: MutableJsonObject = self.model_dump(mode="json", exclude_none=True)
|
|
754
|
+
if "tools" not in self.model_fields_set:
|
|
755
|
+
# ``tools`` is declared with ``default_factory=list``, so
|
|
756
|
+
# ``model_dump(exclude_none=True)`` synthesizes an explicit
|
|
757
|
+
# ``"tools": []`` even when the client omitted the field. Codex
|
|
758
|
+
# Responses-Lite clients omit top-level ``tools`` entirely (the
|
|
759
|
+
# bundle rides in the ``additional_tools`` input item), and models
|
|
760
|
+
# with reserved model tools (e.g. ``collaboration.spawn_agent`` on
|
|
761
|
+
# gpt-5.6 ``multi_agent_version: v2``) reject any explicit
|
|
762
|
+
# ``tools`` param that cannot match the reserved schema. Only
|
|
763
|
+
# forward the field when the client actually sent it — including
|
|
764
|
+
# an explicit client-sent ``[]``. See issue #1184.
|
|
765
|
+
payload.pop("tools", None)
|
|
766
|
+
return payload
|
|
767
|
+
|
|
768
|
+
def to_payload(self) -> JsonObject:
|
|
769
|
+
payload = _strip_unsupported_fields(self.model_dump_for_forwarding())
|
|
770
|
+
_normalize_compaction_trigger_singleton(payload)
|
|
771
|
+
return payload
|
|
772
|
+
|
|
773
|
+
def to_replay_safety_payload(self) -> JsonObject:
|
|
774
|
+
return _strip_unsupported_fields(self.model_dump_for_forwarding(), strip_replayed_tool_call_namespaces=False)
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
class ResponsesCompactRequest(BaseModel):
|
|
778
|
+
model_config = ConfigDict(extra="allow")
|
|
779
|
+
_codex_lb_client_reasoning_effort: str | None = PrivateAttr(default=None)
|
|
780
|
+
|
|
781
|
+
@model_validator(mode="before")
|
|
782
|
+
@classmethod
|
|
783
|
+
def _move_input_instruction_messages(cls, data: JsonValue) -> JsonValue:
|
|
784
|
+
return _normalize_responses_input_instructions(data)
|
|
785
|
+
|
|
786
|
+
model: str = Field(min_length=1)
|
|
787
|
+
instructions: str
|
|
788
|
+
input: PassthroughJsonValue
|
|
789
|
+
reasoning: ResponsesReasoning | None = None
|
|
790
|
+
store: bool = False
|
|
791
|
+
service_tier: str | None = None
|
|
792
|
+
prompt_cache_key: str | None = None
|
|
793
|
+
|
|
794
|
+
@field_validator("input")
|
|
795
|
+
@classmethod
|
|
796
|
+
def _validate_input_type(cls, value: JsonValue) -> JsonValue:
|
|
797
|
+
# ``input_file`` content items with a ``file_id`` are forwarded
|
|
798
|
+
# verbatim; see ``ResponsesRequest._validate_input_type``.
|
|
799
|
+
if isinstance(value, str):
|
|
800
|
+
normalized = _normalize_input_text(value)
|
|
801
|
+
return _sanitize_input_items(normalized)
|
|
802
|
+
if is_json_list(value):
|
|
803
|
+
validate_passthrough_depth(value)
|
|
804
|
+
return _sanitize_input_items(value)
|
|
805
|
+
raise ValueError("input must be a string or array")
|
|
806
|
+
|
|
807
|
+
@model_validator(mode="before")
|
|
808
|
+
@classmethod
|
|
809
|
+
def _normalize_service_tier_aliases_before_validation(cls, data: JsonValue) -> JsonValue:
|
|
810
|
+
if not is_json_mapping(data):
|
|
811
|
+
return data
|
|
812
|
+
normalized = dict(data)
|
|
813
|
+
service_tier = normalized.get("service_tier")
|
|
814
|
+
normalized_service_tier = _normalize_service_tier_alias_value(service_tier)
|
|
815
|
+
if isinstance(normalized_service_tier, str):
|
|
816
|
+
normalized["service_tier"] = normalized_service_tier
|
|
817
|
+
return normalized
|
|
818
|
+
|
|
819
|
+
@field_validator("store")
|
|
820
|
+
@classmethod
|
|
821
|
+
def _ensure_store_false(cls, value: bool) -> bool:
|
|
822
|
+
return False
|
|
823
|
+
|
|
824
|
+
def to_payload(self) -> JsonObject:
|
|
825
|
+
payload: MutableJsonObject = self.model_dump(mode="json", exclude_none=True)
|
|
826
|
+
return _strip_compact_unsupported_fields(payload)
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
_UNSUPPORTED_UPSTREAM_FIELDS = {
|
|
830
|
+
"max_output_tokens",
|
|
831
|
+
"metadata",
|
|
832
|
+
"prompt_cache_retention",
|
|
833
|
+
"safety_identifier",
|
|
834
|
+
"temperature",
|
|
835
|
+
"top_p",
|
|
836
|
+
"truncation",
|
|
837
|
+
"user",
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
def responses_request_has_explicit_prompt_cache_controls(payload: ResponsesRequest) -> bool:
|
|
842
|
+
extra = payload.model_extra
|
|
843
|
+
if isinstance(extra, dict) and "prompt_cache_options" in extra:
|
|
844
|
+
return True
|
|
845
|
+
return _contains_explicit_prompt_cache_breakpoint(payload.input)
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
def _contains_explicit_prompt_cache_breakpoint(value: JsonValue) -> bool:
|
|
849
|
+
if isinstance(value, list):
|
|
850
|
+
return any(_contains_explicit_prompt_cache_breakpoint(item) for item in value)
|
|
851
|
+
if not isinstance(value, dict):
|
|
852
|
+
return False
|
|
853
|
+
value_type = value.get("type")
|
|
854
|
+
if (
|
|
855
|
+
isinstance(value_type, str)
|
|
856
|
+
and value_type in _EXPLICIT_PROMPT_CACHE_CONTENT_TYPES
|
|
857
|
+
and "prompt_cache_breakpoint" in value
|
|
858
|
+
):
|
|
859
|
+
return True
|
|
860
|
+
return any(_contains_explicit_prompt_cache_breakpoint(child) for child in value.values())
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
def _strip_subscription_prompt_cache_controls(payload: MutableJsonObject) -> None:
|
|
864
|
+
payload.pop("prompt_cache_options", None)
|
|
865
|
+
_strip_subscription_prompt_cache_breakpoints(payload.get("input"))
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
def _strip_subscription_prompt_cache_breakpoints(value: JsonValue | None) -> None:
|
|
869
|
+
if isinstance(value, list):
|
|
870
|
+
for item in value:
|
|
871
|
+
_strip_subscription_prompt_cache_breakpoints(item)
|
|
872
|
+
return
|
|
873
|
+
if not isinstance(value, dict):
|
|
874
|
+
return
|
|
875
|
+
value_type = value.get("type")
|
|
876
|
+
if isinstance(value_type, str) and value_type in _EXPLICIT_PROMPT_CACHE_CONTENT_TYPES:
|
|
877
|
+
value.pop("prompt_cache_breakpoint", None)
|
|
878
|
+
for child in value.values():
|
|
879
|
+
_strip_subscription_prompt_cache_breakpoints(child)
|
|
880
|
+
|
|
881
|
+
|
|
882
|
+
def strip_replayed_tool_call_namespaces_from_payload(payload: MutableJsonObject) -> None:
|
|
883
|
+
input_value = payload.get("input")
|
|
884
|
+
if not is_json_list(input_value):
|
|
885
|
+
return
|
|
886
|
+
normalized_items: list[JsonValue] = []
|
|
887
|
+
changed = False
|
|
888
|
+
for item in input_value:
|
|
889
|
+
if not is_json_mapping(item):
|
|
890
|
+
normalized_items.append(item)
|
|
891
|
+
continue
|
|
892
|
+
item_type = item.get("type")
|
|
893
|
+
if isinstance(item_type, str) and item_type in _TOOL_CALL_ITEM_TYPES and "namespace" in item:
|
|
894
|
+
normalized_item = dict(item)
|
|
895
|
+
normalized_item.pop("namespace")
|
|
896
|
+
normalized_items.append(normalized_item)
|
|
897
|
+
changed = True
|
|
898
|
+
else:
|
|
899
|
+
normalized_items.append(item)
|
|
900
|
+
if changed:
|
|
901
|
+
payload["input"] = normalized_items
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
_POISONED_LOCAL_COMPACT_FALLBACK_TEXT = "Local compact fallback preserved the latest encrypted reasoning state."
|
|
905
|
+
_MAX_COMPACT_UPSTREAM_ESTIMATED_TOKENS = 100_000
|
|
906
|
+
_COMPACT_UPSTREAM_HEAD_ESTIMATED_TOKENS = 12_000
|
|
907
|
+
_ESTIMATED_CHARS_PER_TOKEN = 4
|
|
908
|
+
_COMPACT_OMITTED_INLINE_IMAGE_TEXT = (
|
|
909
|
+
"[compact trim] Omitted inline image bytes that were already observed before compaction"
|
|
910
|
+
)
|
|
911
|
+
|
|
912
|
+
|
|
913
|
+
def _strip_unsupported_fields(
|
|
914
|
+
payload: MutableJsonObject,
|
|
915
|
+
*,
|
|
916
|
+
strip_replayed_tool_call_namespaces: bool = True,
|
|
917
|
+
) -> MutableJsonObject:
|
|
918
|
+
_normalize_openai_compatible_aliases(payload)
|
|
919
|
+
_normalize_service_tier_aliases(payload)
|
|
920
|
+
_strip_subscription_prompt_cache_controls(payload)
|
|
921
|
+
_sanitize_interleaved_reasoning_input(payload)
|
|
922
|
+
_strip_poisoned_local_compact_fallback_items(payload)
|
|
923
|
+
if strip_replayed_tool_call_namespaces:
|
|
924
|
+
strip_replayed_tool_call_namespaces_from_payload(payload)
|
|
925
|
+
# ``tools`` is deliberately NOT canonicalized here: the wire payload must
|
|
926
|
+
# forward client tool entries byte-preserved (array order, key order, and
|
|
927
|
+
# unknown keys untouched) so reserved model tools survive upstream
|
|
928
|
+
# byte/structural-equality checks. Order-insensitive canonicalization is
|
|
929
|
+
# cache-affinity/observability-only; see ``canonicalized_tools``.
|
|
930
|
+
for key in _UNSUPPORTED_UPSTREAM_FIELDS:
|
|
931
|
+
payload.pop(key, None)
|
|
932
|
+
return payload
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
def _strip_poisoned_local_compact_fallback_items(payload: MutableJsonObject) -> None:
|
|
936
|
+
input_value = payload.get("input")
|
|
937
|
+
if not is_json_list(input_value):
|
|
938
|
+
return
|
|
939
|
+
|
|
940
|
+
input_items = input_value
|
|
941
|
+
kept: list[JsonValue] = []
|
|
942
|
+
skip_next_poison_compaction = False
|
|
943
|
+
changed = False
|
|
944
|
+
for item in input_items:
|
|
945
|
+
if skip_next_poison_compaction and is_json_mapping(item) and item.get("type") == "compaction":
|
|
946
|
+
encrypted_content = item.get("encrypted_content")
|
|
947
|
+
if isinstance(encrypted_content, str) and encrypted_content:
|
|
948
|
+
skip_next_poison_compaction = False
|
|
949
|
+
changed = True
|
|
950
|
+
continue
|
|
951
|
+
skip_next_poison_compaction = False
|
|
952
|
+
|
|
953
|
+
if _is_poisoned_local_compact_fallback_message(item):
|
|
954
|
+
skip_next_poison_compaction = True
|
|
955
|
+
changed = True
|
|
956
|
+
continue
|
|
957
|
+
|
|
958
|
+
kept.append(item)
|
|
959
|
+
|
|
960
|
+
if changed:
|
|
961
|
+
payload["input"] = kept
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
def _is_poisoned_local_compact_fallback_message(item: JsonValue) -> bool:
|
|
965
|
+
if not is_json_mapping(item):
|
|
966
|
+
return False
|
|
967
|
+
if item.get("type") != "message" or item.get("role") != "assistant":
|
|
968
|
+
return False
|
|
969
|
+
content = item.get("content")
|
|
970
|
+
if not is_json_list(content):
|
|
971
|
+
return False
|
|
972
|
+
for part in content:
|
|
973
|
+
if not is_json_mapping(part):
|
|
974
|
+
continue
|
|
975
|
+
if part.get("text") == _POISONED_LOCAL_COMPACT_FALLBACK_TEXT:
|
|
976
|
+
return True
|
|
977
|
+
return False
|
|
978
|
+
|
|
979
|
+
|
|
980
|
+
def canonicalized_tools(tools: list[JsonValue]) -> list[JsonValue]:
|
|
981
|
+
"""Return an order- and key-order-insensitive canonical form of ``tools``.
|
|
982
|
+
|
|
983
|
+
Used only for prompt-cache affinity/observability hashing (change #228):
|
|
984
|
+
two requests that differ solely in tool array order or object key order
|
|
985
|
+
hash identically. The result MUST NOT feed the upstream wire payload —
|
|
986
|
+
outgoing requests forward the client's tool entries byte-preserved (see
|
|
987
|
+
issue #1184). Array values (e.g. ``parameters.required``) are never
|
|
988
|
+
reordered; only mapping keys are sorted.
|
|
989
|
+
"""
|
|
990
|
+
sorted_tools = sorted(tools, key=_tool_sort_key)
|
|
991
|
+
return [_sort_keys_recursive(t) for t in sorted_tools]
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
def _tool_sort_key(tool: JsonValue) -> str:
|
|
995
|
+
if not is_json_mapping(tool):
|
|
996
|
+
return ""
|
|
997
|
+
tool_map = tool
|
|
998
|
+
name = tool_map.get("name")
|
|
999
|
+
if isinstance(name, str):
|
|
1000
|
+
return name
|
|
1001
|
+
func = tool_map.get("function")
|
|
1002
|
+
if is_json_mapping(func):
|
|
1003
|
+
func_name = func.get("name")
|
|
1004
|
+
if isinstance(func_name, str):
|
|
1005
|
+
return func_name
|
|
1006
|
+
return ""
|
|
1007
|
+
|
|
1008
|
+
|
|
1009
|
+
def _sort_keys_recursive(value: JsonValue) -> JsonValue:
|
|
1010
|
+
if is_json_mapping(value):
|
|
1011
|
+
mapping = value
|
|
1012
|
+
return {k: _sort_keys_recursive(v) for k, v in sorted(mapping.items())}
|
|
1013
|
+
if is_json_list(value):
|
|
1014
|
+
return [_sort_keys_recursive(item) for item in value]
|
|
1015
|
+
return value
|
|
1016
|
+
|
|
1017
|
+
|
|
1018
|
+
def _strip_compact_unsupported_fields(payload: MutableJsonObject) -> MutableJsonObject:
|
|
1019
|
+
payload = _strip_unsupported_fields(payload)
|
|
1020
|
+
normalized_payload = _normalize_responses_input_instructions(payload)
|
|
1021
|
+
if is_json_mapping(normalized_payload):
|
|
1022
|
+
payload = dict(normalized_payload)
|
|
1023
|
+
_trim_compact_input_for_upstream(payload)
|
|
1024
|
+
_normalize_compaction_trigger_singleton(payload)
|
|
1025
|
+
payload.pop("store", None)
|
|
1026
|
+
payload.pop("text", None)
|
|
1027
|
+
payload.pop("tools", None)
|
|
1028
|
+
payload.pop("tool_choice", None)
|
|
1029
|
+
payload.pop("client_metadata", None)
|
|
1030
|
+
payload["parallel_tool_calls"] = False
|
|
1031
|
+
return payload
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
def _normalize_compaction_trigger_singleton(payload: MutableJsonObject) -> None:
|
|
1035
|
+
input_value = payload.get("input")
|
|
1036
|
+
if not is_json_list(input_value) or not input_value:
|
|
1037
|
+
return
|
|
1038
|
+
|
|
1039
|
+
trigger_items = [item for item in input_value if is_json_mapping(item) and item.get("type") == "compaction_trigger"]
|
|
1040
|
+
if not trigger_items:
|
|
1041
|
+
return
|
|
1042
|
+
|
|
1043
|
+
payload["input"] = [
|
|
1044
|
+
item for item in input_value if not (is_json_mapping(item) and item.get("type") == "compaction_trigger")
|
|
1045
|
+
]
|
|
1046
|
+
cast(list[JsonValue], payload["input"]).append({"type": "compaction_trigger"})
|
|
1047
|
+
|
|
1048
|
+
|
|
1049
|
+
def _trim_compact_input_for_upstream(payload: MutableJsonObject) -> None:
|
|
1050
|
+
input_value = payload.get("input")
|
|
1051
|
+
if not is_json_list(input_value):
|
|
1052
|
+
return
|
|
1053
|
+
total_tokens = _estimated_json_tokens(input_value)
|
|
1054
|
+
if total_tokens <= _MAX_COMPACT_UPSTREAM_ESTIMATED_TOKENS:
|
|
1055
|
+
return
|
|
1056
|
+
|
|
1057
|
+
has_continuity_anchor = _compact_has_continuity_anchor(payload)
|
|
1058
|
+
losslessly_trimmed_input = _compact_losslessly_trim_input(
|
|
1059
|
+
input_value,
|
|
1060
|
+
has_continuity_anchor=has_continuity_anchor,
|
|
1061
|
+
)
|
|
1062
|
+
if losslessly_trimmed_input is not None:
|
|
1063
|
+
payload["input"] = losslessly_trimmed_input
|
|
1064
|
+
return
|
|
1065
|
+
|
|
1066
|
+
token_counts = [_estimated_json_array_item_tokens(item) for item in input_value]
|
|
1067
|
+
required_indices = _compact_required_indices(
|
|
1068
|
+
input_value,
|
|
1069
|
+
token_counts,
|
|
1070
|
+
has_continuity_anchor=has_continuity_anchor,
|
|
1071
|
+
)
|
|
1072
|
+
required_input = _compact_trimmed_input_with_markers(input_value, token_counts, required_indices)
|
|
1073
|
+
if _estimated_json_tokens(required_input) <= _MAX_COMPACT_UPSTREAM_ESTIMATED_TOKENS:
|
|
1074
|
+
payload["input"] = required_input
|
|
1075
|
+
return
|
|
1076
|
+
rewritten_input, images_elided = _compact_elide_required_tool_output_images(
|
|
1077
|
+
input_value,
|
|
1078
|
+
required_indices=required_indices,
|
|
1079
|
+
)
|
|
1080
|
+
if images_elided:
|
|
1081
|
+
input_value = rewritten_input
|
|
1082
|
+
payload["input"] = input_value
|
|
1083
|
+
if _estimated_json_tokens(input_value) <= _MAX_COMPACT_UPSTREAM_ESTIMATED_TOKENS:
|
|
1084
|
+
return
|
|
1085
|
+
losslessly_trimmed_input = _compact_losslessly_trim_input(
|
|
1086
|
+
input_value,
|
|
1087
|
+
has_continuity_anchor=has_continuity_anchor,
|
|
1088
|
+
)
|
|
1089
|
+
if losslessly_trimmed_input is not None:
|
|
1090
|
+
payload["input"] = losslessly_trimmed_input
|
|
1091
|
+
return
|
|
1092
|
+
raise ClientPayloadError(
|
|
1093
|
+
"Compact input exceeds the upstream size limit and cannot be trimmed without removing required state anchors.",
|
|
1094
|
+
param="input",
|
|
1095
|
+
code="responses_compact_input_too_large",
|
|
1096
|
+
)
|
|
1097
|
+
|
|
1098
|
+
|
|
1099
|
+
def _compact_losslessly_trim_input(
|
|
1100
|
+
input_value: list[JsonValue],
|
|
1101
|
+
*,
|
|
1102
|
+
has_continuity_anchor: bool = False,
|
|
1103
|
+
) -> list[JsonValue] | None:
|
|
1104
|
+
"""Return a budget-fitting context selection without changing any retained bytes."""
|
|
1105
|
+
|
|
1106
|
+
token_counts = [_estimated_json_array_item_tokens(item) for item in input_value]
|
|
1107
|
+
head_count = _compact_trim_prefix_count(token_counts)
|
|
1108
|
+
state_anchor_indices = _compact_state_anchor_indices(input_value)
|
|
1109
|
+
marker_tokens = _estimated_json_array_item_tokens(_compact_trim_marker(omitted_items=0, omitted_tokens=0))
|
|
1110
|
+
side_effect_indices = _compact_side_effect_anchor_indices(input_value)
|
|
1111
|
+
unusable_side_effect_indices = {
|
|
1112
|
+
index
|
|
1113
|
+
for index, item in enumerate(input_value)
|
|
1114
|
+
if is_json_mapping(item)
|
|
1115
|
+
and _compact_item_is_side_effect_anchor(item)
|
|
1116
|
+
and (not isinstance(item.get("call_id"), str) or not item["call_id"])
|
|
1117
|
+
}
|
|
1118
|
+
side_effect_indices = _compact_reconciled_tool_call_indices(
|
|
1119
|
+
input_value,
|
|
1120
|
+
side_effect_indices,
|
|
1121
|
+
token_counts=token_counts,
|
|
1122
|
+
token_budget=sum(token_counts),
|
|
1123
|
+
)
|
|
1124
|
+
required_indices = _compact_required_indices(
|
|
1125
|
+
input_value,
|
|
1126
|
+
token_counts,
|
|
1127
|
+
preserved_indices=state_anchor_indices,
|
|
1128
|
+
has_continuity_anchor=has_continuity_anchor,
|
|
1129
|
+
)
|
|
1130
|
+
if required_indices & unusable_side_effect_indices:
|
|
1131
|
+
raise ClientPayloadError(
|
|
1132
|
+
"Compact input cannot retain a required side-effect call without a usable call_id.",
|
|
1133
|
+
param="input",
|
|
1134
|
+
code="responses_compact_input_too_large",
|
|
1135
|
+
)
|
|
1136
|
+
required_input = _compact_trimmed_input_with_markers(input_value, token_counts, required_indices)
|
|
1137
|
+
if _estimated_json_tokens(required_input) > _MAX_COMPACT_UPSTREAM_ESTIMATED_TOKENS:
|
|
1138
|
+
return None
|
|
1139
|
+
wire_budget = max(0, _MAX_COMPACT_UPSTREAM_ESTIMATED_TOKENS - marker_tokens)
|
|
1140
|
+
side_effect_indices &= _compact_reconciled_tool_call_indices(
|
|
1141
|
+
input_value,
|
|
1142
|
+
required_indices | side_effect_indices,
|
|
1143
|
+
token_counts=token_counts,
|
|
1144
|
+
token_budget=wire_budget,
|
|
1145
|
+
required_indices=required_indices,
|
|
1146
|
+
)
|
|
1147
|
+
selected_indices = set(state_anchor_indices)
|
|
1148
|
+
selected_indices.update(side_effect_indices)
|
|
1149
|
+
selected_indices.update(range(head_count))
|
|
1150
|
+
selected_tokens = sum(token_counts[index] for index in selected_indices)
|
|
1151
|
+
tail_budget = max(0, _MAX_COMPACT_UPSTREAM_ESTIMATED_TOKENS - selected_tokens - marker_tokens)
|
|
1152
|
+
selected_indices.update(
|
|
1153
|
+
_compact_trim_suffix_indices(
|
|
1154
|
+
token_counts,
|
|
1155
|
+
selected_indices=selected_indices,
|
|
1156
|
+
start_index=head_count,
|
|
1157
|
+
token_budget=tail_budget,
|
|
1158
|
+
)
|
|
1159
|
+
)
|
|
1160
|
+
selected_indices.update(required_indices)
|
|
1161
|
+
if has_continuity_anchor:
|
|
1162
|
+
_compact_discard_consumed_continuity_output_pairs(
|
|
1163
|
+
input_value,
|
|
1164
|
+
selected_indices=selected_indices,
|
|
1165
|
+
required_indices=required_indices,
|
|
1166
|
+
)
|
|
1167
|
+
selected_indices.difference_update(unusable_side_effect_indices)
|
|
1168
|
+
selected_indices = _compact_reconciled_tool_call_indices(
|
|
1169
|
+
input_value,
|
|
1170
|
+
selected_indices,
|
|
1171
|
+
token_counts=token_counts,
|
|
1172
|
+
token_budget=max(0, _MAX_COMPACT_UPSTREAM_ESTIMATED_TOKENS - marker_tokens),
|
|
1173
|
+
required_indices=required_indices | side_effect_indices,
|
|
1174
|
+
)
|
|
1175
|
+
selected_indices = _compact_fit_selected_indices_to_wire_budget(
|
|
1176
|
+
input_value,
|
|
1177
|
+
token_counts,
|
|
1178
|
+
selected_indices=selected_indices,
|
|
1179
|
+
required_indices=required_indices,
|
|
1180
|
+
priority_indices=side_effect_indices,
|
|
1181
|
+
)
|
|
1182
|
+
trimmed_input = (
|
|
1183
|
+
input_value
|
|
1184
|
+
if len(selected_indices) == len(input_value)
|
|
1185
|
+
else _compact_trimmed_input_with_markers(input_value, token_counts, selected_indices)
|
|
1186
|
+
)
|
|
1187
|
+
trimmed_tokens = _estimated_json_tokens(trimmed_input)
|
|
1188
|
+
if trimmed_tokens > _MAX_COMPACT_UPSTREAM_ESTIMATED_TOKENS:
|
|
1189
|
+
return None
|
|
1190
|
+
return trimmed_input
|
|
1191
|
+
|
|
1192
|
+
|
|
1193
|
+
def _compact_required_indices(
|
|
1194
|
+
input_value: list[JsonValue],
|
|
1195
|
+
token_counts: list[int],
|
|
1196
|
+
*,
|
|
1197
|
+
preserved_indices: set[int] | None = None,
|
|
1198
|
+
has_continuity_anchor: bool = False,
|
|
1199
|
+
) -> set[int]:
|
|
1200
|
+
if preserved_indices is None:
|
|
1201
|
+
preserved_indices = _compact_state_anchor_indices(input_value)
|
|
1202
|
+
required_indices = _compact_reconciled_tool_call_indices(
|
|
1203
|
+
input_value,
|
|
1204
|
+
preserved_indices,
|
|
1205
|
+
token_counts=token_counts,
|
|
1206
|
+
token_budget=sum(token_counts),
|
|
1207
|
+
required_indices=preserved_indices,
|
|
1208
|
+
)
|
|
1209
|
+
terminal_indices, terminal_is_required, reconcile_terminal_pairs = _compact_terminal_required_indices(
|
|
1210
|
+
input_value,
|
|
1211
|
+
token_counts=token_counts,
|
|
1212
|
+
has_continuity_anchor=has_continuity_anchor,
|
|
1213
|
+
)
|
|
1214
|
+
if terminal_indices:
|
|
1215
|
+
prospective_required_indices = required_indices | terminal_indices
|
|
1216
|
+
if reconcile_terminal_pairs:
|
|
1217
|
+
prospective_required_indices = _compact_reconciled_tool_call_indices(
|
|
1218
|
+
input_value,
|
|
1219
|
+
prospective_required_indices,
|
|
1220
|
+
token_counts=token_counts,
|
|
1221
|
+
token_budget=sum(token_counts),
|
|
1222
|
+
required_indices=prospective_required_indices,
|
|
1223
|
+
)
|
|
1224
|
+
prospective_required_input = _compact_trimmed_input_with_markers(
|
|
1225
|
+
input_value,
|
|
1226
|
+
token_counts,
|
|
1227
|
+
prospective_required_indices,
|
|
1228
|
+
)
|
|
1229
|
+
if terminal_is_required or (
|
|
1230
|
+
_estimated_json_tokens(prospective_required_input) <= _MAX_COMPACT_UPSTREAM_ESTIMATED_TOKENS
|
|
1231
|
+
):
|
|
1232
|
+
required_indices = prospective_required_indices
|
|
1233
|
+
return required_indices
|
|
1234
|
+
|
|
1235
|
+
|
|
1236
|
+
def _compact_has_continuity_anchor(payload: Mapping[str, JsonValue]) -> bool:
|
|
1237
|
+
return any(
|
|
1238
|
+
isinstance(payload.get(field), str) and bool(cast(str, payload[field]).strip())
|
|
1239
|
+
for field in ("previous_response_id", "conversation")
|
|
1240
|
+
)
|
|
1241
|
+
|
|
1242
|
+
|
|
1243
|
+
def _compact_discard_consumed_continuity_output_pairs(
|
|
1244
|
+
input_value: list[JsonValue],
|
|
1245
|
+
*,
|
|
1246
|
+
selected_indices: set[int],
|
|
1247
|
+
required_indices: set[int],
|
|
1248
|
+
) -> None:
|
|
1249
|
+
if not input_value:
|
|
1250
|
+
return
|
|
1251
|
+
latest_index = len(input_value) - 1
|
|
1252
|
+
latest_mapping = _json_mapping_or_none(input_value[latest_index])
|
|
1253
|
+
latest_type = latest_mapping.get("type") if latest_mapping is not None else None
|
|
1254
|
+
if latest_type == "compaction_trigger":
|
|
1255
|
+
if latest_index == 0:
|
|
1256
|
+
return
|
|
1257
|
+
latest_index -= 1
|
|
1258
|
+
latest_mapping = _json_mapping_or_none(input_value[latest_index])
|
|
1259
|
+
latest_type = latest_mapping.get("type") if latest_mapping is not None else None
|
|
1260
|
+
if (
|
|
1261
|
+
latest_mapping is None
|
|
1262
|
+
or not isinstance(latest_type, str)
|
|
1263
|
+
or latest_type not in _COMPACT_TOOL_CALL_OUTPUT_ITEM_TYPES
|
|
1264
|
+
or _compact_matching_tool_call_index(input_value, latest_index) is not None
|
|
1265
|
+
):
|
|
1266
|
+
return
|
|
1267
|
+
latest_call_id = latest_mapping.get("call_id")
|
|
1268
|
+
matching_call_type = _COMPACT_TOOL_CALL_TYPE_BY_OUTPUT_TYPE.get(latest_type)
|
|
1269
|
+
selected_indices.difference_update(
|
|
1270
|
+
index
|
|
1271
|
+
for index, item in enumerate(input_value[:latest_index])
|
|
1272
|
+
if is_json_mapping(item)
|
|
1273
|
+
and item.get("call_id") == latest_call_id
|
|
1274
|
+
and item.get("type") in {latest_type, matching_call_type}
|
|
1275
|
+
and index not in required_indices
|
|
1276
|
+
)
|
|
1277
|
+
|
|
1278
|
+
|
|
1279
|
+
def _compact_terminal_required_indices(
|
|
1280
|
+
input_value: list[JsonValue],
|
|
1281
|
+
*,
|
|
1282
|
+
token_counts: list[int],
|
|
1283
|
+
has_continuity_anchor: bool,
|
|
1284
|
+
) -> tuple[set[int], bool, bool]:
|
|
1285
|
+
"""Return terminal context and whether it must remain even when oversized."""
|
|
1286
|
+
|
|
1287
|
+
if not input_value:
|
|
1288
|
+
return set(), False, False
|
|
1289
|
+
|
|
1290
|
+
latest_index = len(input_value) - 1
|
|
1291
|
+
latest_mapping = _json_mapping_or_none(input_value[latest_index])
|
|
1292
|
+
latest_type = latest_mapping.get("type") if latest_mapping is not None else None
|
|
1293
|
+
terminal_trigger_indices: set[int] = set()
|
|
1294
|
+
if latest_type == "compaction_trigger":
|
|
1295
|
+
terminal_trigger_indices.add(latest_index)
|
|
1296
|
+
if latest_index == 0:
|
|
1297
|
+
return terminal_trigger_indices, True, False
|
|
1298
|
+
latest_index -= 1
|
|
1299
|
+
latest_mapping = _json_mapping_or_none(input_value[latest_index])
|
|
1300
|
+
latest_type = latest_mapping.get("type") if latest_mapping is not None else None
|
|
1301
|
+
|
|
1302
|
+
def with_terminal_trigger(indices: set[int]) -> set[int]:
|
|
1303
|
+
return indices | terminal_trigger_indices
|
|
1304
|
+
|
|
1305
|
+
if latest_type not in _COMPACT_TOOL_CALL_ITEM_TYPES | _COMPACT_TOOL_CALL_OUTPUT_ITEM_TYPES:
|
|
1306
|
+
return with_terminal_trigger({latest_index}), True, False
|
|
1307
|
+
if latest_mapping is not None and _compact_item_is_state_anchor(latest_mapping):
|
|
1308
|
+
terminal_indices = _compact_required_terminal_indices(input_value, latest_index, token_counts)
|
|
1309
|
+
return with_terminal_trigger(terminal_indices), True, True
|
|
1310
|
+
if latest_mapping is not None and _compact_item_has_elidable_inline_image(latest_mapping):
|
|
1311
|
+
terminal_indices = _compact_required_terminal_indices(input_value, latest_index, token_counts)
|
|
1312
|
+
return with_terminal_trigger(terminal_indices), True, True
|
|
1313
|
+
matching_call_index = _compact_matching_tool_call_index(input_value, latest_index)
|
|
1314
|
+
if latest_mapping is not None and _compact_terminal_item_is_side_effect(
|
|
1315
|
+
input_value,
|
|
1316
|
+
latest_index,
|
|
1317
|
+
matching_call_index=matching_call_index,
|
|
1318
|
+
):
|
|
1319
|
+
terminal_indices = _compact_required_terminal_indices(input_value, latest_index, token_counts)
|
|
1320
|
+
return with_terminal_trigger(terminal_indices), True, True
|
|
1321
|
+
if latest_type in _COMPACT_TOOL_CALL_OUTPUT_ITEM_TYPES and has_continuity_anchor and matching_call_index is None:
|
|
1322
|
+
return with_terminal_trigger({latest_index}), True, False
|
|
1323
|
+
if latest_type in _COMPACT_TOOL_CALL_ITEM_TYPES:
|
|
1324
|
+
terminal_indices = _compact_required_terminal_indices(input_value, latest_index, token_counts)
|
|
1325
|
+
return with_terminal_trigger(terminal_indices), True, True
|
|
1326
|
+
|
|
1327
|
+
if terminal_trigger_indices:
|
|
1328
|
+
# The trigger is the only mandatory suffix sentinel. An ordinary
|
|
1329
|
+
# matched tool pair immediately before it remains best-effort context
|
|
1330
|
+
# and may be dropped when other anchors consume the wire budget.
|
|
1331
|
+
return terminal_trigger_indices, True, False
|
|
1332
|
+
|
|
1333
|
+
paired_tail = _compact_reconciled_tool_call_indices(
|
|
1334
|
+
input_value,
|
|
1335
|
+
with_terminal_trigger({latest_index}),
|
|
1336
|
+
token_counts=token_counts,
|
|
1337
|
+
token_budget=_MAX_COMPACT_UPSTREAM_ESTIMATED_TOKENS,
|
|
1338
|
+
)
|
|
1339
|
+
if latest_index in paired_tail:
|
|
1340
|
+
return paired_tail, False, False
|
|
1341
|
+
return terminal_trigger_indices, bool(terminal_trigger_indices), False
|
|
1342
|
+
|
|
1343
|
+
|
|
1344
|
+
def _compact_item_has_elidable_inline_image(item: JsonValue) -> bool:
|
|
1345
|
+
_, changed = _compact_elide_inline_images(item)
|
|
1346
|
+
return changed
|
|
1347
|
+
|
|
1348
|
+
|
|
1349
|
+
def _compact_matching_tool_call_index(input_value: list[JsonValue], output_index: int) -> int | None:
|
|
1350
|
+
output = _json_mapping_or_none(input_value[output_index])
|
|
1351
|
+
if output is None:
|
|
1352
|
+
return None
|
|
1353
|
+
call_id = output.get("call_id")
|
|
1354
|
+
if not isinstance(call_id, str) or not call_id:
|
|
1355
|
+
return None
|
|
1356
|
+
output_type = output.get("type")
|
|
1357
|
+
expected_call_type = (
|
|
1358
|
+
_COMPACT_TOOL_CALL_TYPE_BY_OUTPUT_TYPE.get(output_type) if isinstance(output_type, str) else None
|
|
1359
|
+
)
|
|
1360
|
+
if expected_call_type is None:
|
|
1361
|
+
return None
|
|
1362
|
+
unmatched_call_indices: list[int] = []
|
|
1363
|
+
for index in range(output_index):
|
|
1364
|
+
item = _json_mapping_or_none(input_value[index])
|
|
1365
|
+
if item is None or item.get("call_id") != call_id:
|
|
1366
|
+
continue
|
|
1367
|
+
if item.get("type") == expected_call_type:
|
|
1368
|
+
unmatched_call_indices.append(index)
|
|
1369
|
+
elif item.get("type") == output_type and unmatched_call_indices:
|
|
1370
|
+
unmatched_call_indices.pop()
|
|
1371
|
+
return unmatched_call_indices[-1] if unmatched_call_indices else None
|
|
1372
|
+
|
|
1373
|
+
|
|
1374
|
+
def _compact_terminal_item_is_side_effect(
|
|
1375
|
+
input_value: list[JsonValue],
|
|
1376
|
+
index: int,
|
|
1377
|
+
*,
|
|
1378
|
+
matching_call_index: int | None,
|
|
1379
|
+
) -> bool:
|
|
1380
|
+
item = _json_mapping_or_none(input_value[index])
|
|
1381
|
+
if item is None:
|
|
1382
|
+
return False
|
|
1383
|
+
if item.get("type") in _COMPACT_SIDE_EFFECT_TOOL_ITEM_TYPES or _compact_item_is_side_effect_anchor(item):
|
|
1384
|
+
return True
|
|
1385
|
+
if matching_call_index is None:
|
|
1386
|
+
return False
|
|
1387
|
+
matching_call = _json_mapping_or_none(input_value[matching_call_index])
|
|
1388
|
+
return matching_call is not None and (
|
|
1389
|
+
matching_call.get("type") in _COMPACT_SIDE_EFFECT_TOOL_ITEM_TYPES
|
|
1390
|
+
or _compact_item_is_side_effect_anchor(matching_call)
|
|
1391
|
+
)
|
|
1392
|
+
|
|
1393
|
+
|
|
1394
|
+
def _compact_required_terminal_indices(
|
|
1395
|
+
input_value: list[JsonValue],
|
|
1396
|
+
latest_index: int,
|
|
1397
|
+
token_counts: list[int],
|
|
1398
|
+
) -> set[int]:
|
|
1399
|
+
return _compact_reconciled_tool_call_indices(
|
|
1400
|
+
input_value,
|
|
1401
|
+
{latest_index},
|
|
1402
|
+
token_counts=token_counts,
|
|
1403
|
+
token_budget=sum(token_counts),
|
|
1404
|
+
required_indices={latest_index},
|
|
1405
|
+
)
|
|
1406
|
+
|
|
1407
|
+
|
|
1408
|
+
def _compact_elide_required_tool_output_images(
|
|
1409
|
+
input_value: list[JsonValue],
|
|
1410
|
+
*,
|
|
1411
|
+
required_indices: set[int],
|
|
1412
|
+
) -> tuple[list[JsonValue], bool]:
|
|
1413
|
+
rewritten: list[JsonValue] = []
|
|
1414
|
+
changed = False
|
|
1415
|
+
for index, item in enumerate(input_value):
|
|
1416
|
+
if (
|
|
1417
|
+
index in required_indices
|
|
1418
|
+
and is_json_mapping(item)
|
|
1419
|
+
and item.get("type") in _COMPACT_TOOL_CALL_OUTPUT_ITEM_TYPES
|
|
1420
|
+
):
|
|
1421
|
+
rewritten_item, item_changed = _compact_elide_inline_images(item)
|
|
1422
|
+
rewritten.append(rewritten_item)
|
|
1423
|
+
changed = changed or item_changed
|
|
1424
|
+
else:
|
|
1425
|
+
rewritten.append(item)
|
|
1426
|
+
return rewritten, changed
|
|
1427
|
+
|
|
1428
|
+
|
|
1429
|
+
def _compact_elide_inline_images(value: JsonValue) -> tuple[JsonValue, bool]:
|
|
1430
|
+
"""Replace inline image bytes with an explicit compact-only text marker."""
|
|
1431
|
+
|
|
1432
|
+
if is_json_mapping(value):
|
|
1433
|
+
if value.get("type") == "input_image":
|
|
1434
|
+
image_url = value.get("image_url")
|
|
1435
|
+
if isinstance(image_url, str) and image_url.startswith("data:image/"):
|
|
1436
|
+
return (
|
|
1437
|
+
{
|
|
1438
|
+
"type": "input_text",
|
|
1439
|
+
"text": f"{_COMPACT_OMITTED_INLINE_IMAGE_TEXT} ({len(image_url)} encoded characters).",
|
|
1440
|
+
},
|
|
1441
|
+
True,
|
|
1442
|
+
)
|
|
1443
|
+
if value.get("type") == "image_url":
|
|
1444
|
+
image_url = value.get("image_url")
|
|
1445
|
+
url = image_url.get("url") if is_json_mapping(image_url) else image_url
|
|
1446
|
+
if isinstance(url, str) and url.startswith("data:image/"):
|
|
1447
|
+
return (
|
|
1448
|
+
{
|
|
1449
|
+
"type": "text",
|
|
1450
|
+
"text": f"{_COMPACT_OMITTED_INLINE_IMAGE_TEXT} ({len(url)} encoded characters).",
|
|
1451
|
+
},
|
|
1452
|
+
True,
|
|
1453
|
+
)
|
|
1454
|
+
rewritten_mapping: JsonObject = {}
|
|
1455
|
+
changed = False
|
|
1456
|
+
for key, item in value.items():
|
|
1457
|
+
rewritten_item, item_changed = _compact_elide_inline_images(item)
|
|
1458
|
+
rewritten_mapping[key] = rewritten_item
|
|
1459
|
+
changed = changed or item_changed
|
|
1460
|
+
return rewritten_mapping, changed
|
|
1461
|
+
if is_json_list(value):
|
|
1462
|
+
rewritten: list[JsonValue] = []
|
|
1463
|
+
changed = False
|
|
1464
|
+
for item in value:
|
|
1465
|
+
rewritten_item, item_changed = _compact_elide_inline_images(item)
|
|
1466
|
+
rewritten.append(rewritten_item)
|
|
1467
|
+
changed = changed or item_changed
|
|
1468
|
+
return rewritten, changed
|
|
1469
|
+
if isinstance(value, str) and "data:image/" in value:
|
|
1470
|
+
rewritten_value, replacements = _COMPACT_INLINE_IMAGE_DATA_URL_RE.subn(
|
|
1471
|
+
lambda match: f"{_COMPACT_OMITTED_INLINE_IMAGE_TEXT} ({len(match.group(0))} encoded characters).",
|
|
1472
|
+
value,
|
|
1473
|
+
)
|
|
1474
|
+
if replacements:
|
|
1475
|
+
return rewritten_value, True
|
|
1476
|
+
return value, False
|
|
1477
|
+
|
|
1478
|
+
|
|
1479
|
+
def _compact_fit_selected_indices_to_wire_budget(
|
|
1480
|
+
input_value: list[JsonValue],
|
|
1481
|
+
token_counts: list[int],
|
|
1482
|
+
*,
|
|
1483
|
+
selected_indices: set[int],
|
|
1484
|
+
required_indices: set[int],
|
|
1485
|
+
priority_indices: set[int] | None = None,
|
|
1486
|
+
) -> set[int]:
|
|
1487
|
+
"""Drop best-effort middle context until the exact serialized input fits."""
|
|
1488
|
+
|
|
1489
|
+
selected = set(selected_indices)
|
|
1490
|
+
prioritized = priority_indices or set()
|
|
1491
|
+
|
|
1492
|
+
def optional_drop_key(index: int) -> tuple[int, int, int]:
|
|
1493
|
+
if index in prioritized:
|
|
1494
|
+
return (0, 0, -index)
|
|
1495
|
+
return (1, min(index, len(input_value) - 1 - index), index)
|
|
1496
|
+
|
|
1497
|
+
optional_indices = sorted(
|
|
1498
|
+
selected - required_indices,
|
|
1499
|
+
key=optional_drop_key,
|
|
1500
|
+
reverse=True,
|
|
1501
|
+
)
|
|
1502
|
+
marker_budget = max(
|
|
1503
|
+
0,
|
|
1504
|
+
_MAX_COMPACT_UPSTREAM_ESTIMATED_TOKENS
|
|
1505
|
+
- _estimated_json_array_item_tokens(_compact_trim_marker(omitted_items=0, omitted_tokens=0)),
|
|
1506
|
+
)
|
|
1507
|
+
for index in optional_indices:
|
|
1508
|
+
candidate_input = _compact_trimmed_input_with_markers(input_value, token_counts, selected)
|
|
1509
|
+
if _estimated_json_tokens(candidate_input) <= _MAX_COMPACT_UPSTREAM_ESTIMATED_TOKENS:
|
|
1510
|
+
break
|
|
1511
|
+
if index not in selected:
|
|
1512
|
+
continue
|
|
1513
|
+
trial = set(selected)
|
|
1514
|
+
trial.remove(index)
|
|
1515
|
+
trial = _compact_reconciled_tool_call_indices(
|
|
1516
|
+
input_value,
|
|
1517
|
+
trial,
|
|
1518
|
+
token_counts=token_counts,
|
|
1519
|
+
token_budget=marker_budget,
|
|
1520
|
+
required_indices=required_indices,
|
|
1521
|
+
allow_pair_additions=False,
|
|
1522
|
+
)
|
|
1523
|
+
if required_indices <= trial and trial != selected:
|
|
1524
|
+
selected = trial
|
|
1525
|
+
return selected
|
|
1526
|
+
|
|
1527
|
+
|
|
1528
|
+
def validate_compact_input_wire_budget(payload: Mapping[str, JsonValue]) -> None:
|
|
1529
|
+
"""Reject compact input that exceeds the upstream budget after transformations."""
|
|
1530
|
+
|
|
1531
|
+
input_value = payload.get("input")
|
|
1532
|
+
if not is_json_list(input_value):
|
|
1533
|
+
return
|
|
1534
|
+
if _estimated_json_tokens(input_value) <= _MAX_COMPACT_UPSTREAM_ESTIMATED_TOKENS:
|
|
1535
|
+
return
|
|
1536
|
+
raise ClientPayloadError(
|
|
1537
|
+
"Compact input exceeds the upstream size limit after preparing the final wire payload.",
|
|
1538
|
+
param="input",
|
|
1539
|
+
code="responses_compact_input_too_large",
|
|
1540
|
+
error_type="invalid_request_error",
|
|
1541
|
+
)
|
|
1542
|
+
|
|
1543
|
+
|
|
1544
|
+
def _compact_state_anchor_indices(input_value: list[JsonValue]) -> set[int]:
|
|
1545
|
+
preserved_indices: set[int] = set()
|
|
1546
|
+
for index, item in enumerate(input_value):
|
|
1547
|
+
if not is_json_mapping(item):
|
|
1548
|
+
continue
|
|
1549
|
+
item_mapping = item
|
|
1550
|
+
if item_mapping.get("type") == "additional_tools":
|
|
1551
|
+
preserved_indices.add(index)
|
|
1552
|
+
developer_index = index + 1
|
|
1553
|
+
if developer_index < len(input_value):
|
|
1554
|
+
developer_item = input_value[developer_index]
|
|
1555
|
+
if is_json_mapping(developer_item) and developer_item.get("role") == "developer":
|
|
1556
|
+
developer_type = developer_item.get("type")
|
|
1557
|
+
if developer_type is None or developer_type == "message":
|
|
1558
|
+
preserved_indices.add(developer_index)
|
|
1559
|
+
if _is_preserved_non_message_directive(item_mapping):
|
|
1560
|
+
preserved_indices.add(index)
|
|
1561
|
+
if _compact_item_is_state_anchor(item_mapping):
|
|
1562
|
+
preserved_indices.add(index)
|
|
1563
|
+
return preserved_indices
|
|
1564
|
+
|
|
1565
|
+
|
|
1566
|
+
def _compact_side_effect_anchor_indices(input_value: list[JsonValue]) -> set[int]:
|
|
1567
|
+
preserved_indices: set[int] = set()
|
|
1568
|
+
for index, item in enumerate(input_value):
|
|
1569
|
+
if (
|
|
1570
|
+
is_json_mapping(item)
|
|
1571
|
+
and _compact_item_is_side_effect_anchor(item)
|
|
1572
|
+
and isinstance(item.get("call_id"), str)
|
|
1573
|
+
and item["call_id"]
|
|
1574
|
+
):
|
|
1575
|
+
preserved_indices.add(index)
|
|
1576
|
+
return preserved_indices
|
|
1577
|
+
|
|
1578
|
+
|
|
1579
|
+
def _compact_reconciled_tool_call_indices(
|
|
1580
|
+
input_value: list[JsonValue],
|
|
1581
|
+
selected_indices: set[int],
|
|
1582
|
+
*,
|
|
1583
|
+
token_counts: list[int],
|
|
1584
|
+
token_budget: int,
|
|
1585
|
+
required_indices: set[int] | None = None,
|
|
1586
|
+
allow_pair_additions: bool = True,
|
|
1587
|
+
) -> set[int]:
|
|
1588
|
+
call_indices_by_key: dict[tuple[str, str], list[int]] = {}
|
|
1589
|
+
output_indices_by_key: dict[tuple[str, str], list[int]] = {}
|
|
1590
|
+
for index, item in enumerate(input_value):
|
|
1591
|
+
if not is_json_mapping(item):
|
|
1592
|
+
continue
|
|
1593
|
+
call_id = item.get("call_id")
|
|
1594
|
+
if not isinstance(call_id, str) or not call_id:
|
|
1595
|
+
continue
|
|
1596
|
+
item_type = item.get("type")
|
|
1597
|
+
if isinstance(item_type, str) and item_type in _COMPACT_TOOL_CALL_ITEM_TYPES:
|
|
1598
|
+
call_indices_by_key.setdefault((call_id, item_type), []).append(index)
|
|
1599
|
+
elif isinstance(item_type, str):
|
|
1600
|
+
matching_call_type = _COMPACT_TOOL_CALL_TYPE_BY_OUTPUT_TYPE.get(item_type)
|
|
1601
|
+
if matching_call_type is not None:
|
|
1602
|
+
output_indices_by_key.setdefault((call_id, matching_call_type), []).append(index)
|
|
1603
|
+
|
|
1604
|
+
reconciled = set(selected_indices)
|
|
1605
|
+
protected_indices = required_indices or set()
|
|
1606
|
+
selected_tokens = sum(token_counts[index] for index in reconciled)
|
|
1607
|
+
|
|
1608
|
+
def add_indices(indices: Iterable[int]) -> bool:
|
|
1609
|
+
nonlocal selected_tokens
|
|
1610
|
+
missing_indices = [index for index in indices if index not in reconciled]
|
|
1611
|
+
missing_tokens = sum(token_counts[index] for index in missing_indices)
|
|
1612
|
+
if selected_tokens + missing_tokens > token_budget:
|
|
1613
|
+
return False
|
|
1614
|
+
reconciled.update(missing_indices)
|
|
1615
|
+
selected_tokens += missing_tokens
|
|
1616
|
+
return True
|
|
1617
|
+
|
|
1618
|
+
def remove_indices(indices: Iterable[int]) -> None:
|
|
1619
|
+
nonlocal selected_tokens
|
|
1620
|
+
for index in indices:
|
|
1621
|
+
if index in reconciled and index not in protected_indices:
|
|
1622
|
+
reconciled.remove(index)
|
|
1623
|
+
selected_tokens -= token_counts[index]
|
|
1624
|
+
|
|
1625
|
+
def matching_call_index(call_indices: list[int], output_indices: list[int], output_index: int) -> int | None:
|
|
1626
|
+
unmatched_calls: list[int] = []
|
|
1627
|
+
call_index_set = set(call_indices)
|
|
1628
|
+
output_index_set = set(output_indices)
|
|
1629
|
+
for index in range(output_index):
|
|
1630
|
+
if index in call_index_set:
|
|
1631
|
+
unmatched_calls.append(index)
|
|
1632
|
+
elif index in output_index_set and unmatched_calls:
|
|
1633
|
+
unmatched_calls.pop()
|
|
1634
|
+
return unmatched_calls[-1] if unmatched_calls else None
|
|
1635
|
+
|
|
1636
|
+
def matching_output_indices(call_indices: list[int], call_index: int, output_indices: list[int]) -> list[int]:
|
|
1637
|
+
next_call_indices = [next_call_index for next_call_index in call_indices if next_call_index > call_index]
|
|
1638
|
+
next_call_index = next_call_indices[0] if next_call_indices else None
|
|
1639
|
+
return [
|
|
1640
|
+
output_index
|
|
1641
|
+
for output_index in output_indices
|
|
1642
|
+
if output_index > call_index and (next_call_index is None or output_index < next_call_index)
|
|
1643
|
+
][:1]
|
|
1644
|
+
|
|
1645
|
+
for pair_key, output_indices in output_indices_by_key.items():
|
|
1646
|
+
selected_outputs = [index for index in output_indices if index in reconciled]
|
|
1647
|
+
if not selected_outputs:
|
|
1648
|
+
continue
|
|
1649
|
+
call_indices = call_indices_by_key.get(pair_key, [])
|
|
1650
|
+
for output_index in selected_outputs:
|
|
1651
|
+
call_index = matching_call_index(call_indices, output_indices, output_index)
|
|
1652
|
+
if call_index is None:
|
|
1653
|
+
remove_indices([output_index])
|
|
1654
|
+
elif not allow_pair_additions and call_index not in reconciled:
|
|
1655
|
+
remove_indices([output_index])
|
|
1656
|
+
elif not add_indices([call_index]):
|
|
1657
|
+
remove_indices([output_index])
|
|
1658
|
+
for pair_key, call_indices in call_indices_by_key.items():
|
|
1659
|
+
output_indices = output_indices_by_key.get(pair_key, [])
|
|
1660
|
+
for call_index in call_indices:
|
|
1661
|
+
if call_index not in reconciled:
|
|
1662
|
+
continue
|
|
1663
|
+
matched_output_indices = matching_output_indices(call_indices, call_index, output_indices)
|
|
1664
|
+
if not matched_output_indices:
|
|
1665
|
+
remove_indices([call_index])
|
|
1666
|
+
continue
|
|
1667
|
+
if not allow_pair_additions and any(index not in reconciled for index in matched_output_indices):
|
|
1668
|
+
remove_indices([call_index, *matched_output_indices])
|
|
1669
|
+
continue
|
|
1670
|
+
if not add_indices(matched_output_indices):
|
|
1671
|
+
remove_indices([call_index, *matched_output_indices])
|
|
1672
|
+
return reconciled
|
|
1673
|
+
|
|
1674
|
+
|
|
1675
|
+
def _compact_item_is_state_anchor(item: Mapping[str, JsonValue]) -> bool:
|
|
1676
|
+
item_type = item.get("type")
|
|
1677
|
+
if item_type == "additional_tools":
|
|
1678
|
+
return True
|
|
1679
|
+
if item_type in {None, "message"} and item.get("role") in {"system", "developer"}:
|
|
1680
|
+
return True
|
|
1681
|
+
if item_type == "function_call":
|
|
1682
|
+
name = item.get("name")
|
|
1683
|
+
if isinstance(name, str) and name in _COMPACT_STATE_TOOL_NAMES:
|
|
1684
|
+
return True
|
|
1685
|
+
function = item.get("function")
|
|
1686
|
+
if is_json_mapping(function):
|
|
1687
|
+
function_name = function.get("name")
|
|
1688
|
+
if isinstance(function_name, str) and function_name in _COMPACT_STATE_TOOL_NAMES:
|
|
1689
|
+
return True
|
|
1690
|
+
for text in _compact_item_texts(item):
|
|
1691
|
+
stripped = text.lstrip()
|
|
1692
|
+
if stripped.startswith(_GOAL_CONTINUATION_CONTEXT_PREFIX):
|
|
1693
|
+
return True
|
|
1694
|
+
if stripped.startswith(_PLAN_MODE_CONTEXT_PREFIX):
|
|
1695
|
+
return True
|
|
1696
|
+
return False
|
|
1697
|
+
|
|
1698
|
+
|
|
1699
|
+
def _compact_item_is_side_effect_anchor(item: Mapping[str, JsonValue]) -> bool:
|
|
1700
|
+
return is_downstream_side_effect_tool_call_item(item)
|
|
1701
|
+
|
|
1702
|
+
|
|
1703
|
+
def _compact_item_texts(item: Mapping[str, JsonValue]) -> list[str]:
|
|
1704
|
+
content = item.get("content")
|
|
1705
|
+
if isinstance(content, str):
|
|
1706
|
+
return [content]
|
|
1707
|
+
if is_json_mapping(content):
|
|
1708
|
+
content_parts: list[JsonValue] = [content]
|
|
1709
|
+
elif is_json_list(content):
|
|
1710
|
+
content_parts = content
|
|
1711
|
+
else:
|
|
1712
|
+
return []
|
|
1713
|
+
|
|
1714
|
+
texts: list[str] = []
|
|
1715
|
+
for part in content_parts:
|
|
1716
|
+
if isinstance(part, str):
|
|
1717
|
+
texts.append(part)
|
|
1718
|
+
continue
|
|
1719
|
+
if not is_json_mapping(part):
|
|
1720
|
+
continue
|
|
1721
|
+
text = part.get("text")
|
|
1722
|
+
if isinstance(text, str):
|
|
1723
|
+
texts.append(text)
|
|
1724
|
+
return texts
|
|
1725
|
+
|
|
1726
|
+
|
|
1727
|
+
def responses_input_contains_goal_continuation_context(input_value: JsonValue) -> bool:
|
|
1728
|
+
"""Return whether Responses input carries Codex's goal-continuation marker."""
|
|
1729
|
+
|
|
1730
|
+
if not is_json_list(input_value):
|
|
1731
|
+
return False
|
|
1732
|
+
for item in input_value:
|
|
1733
|
+
if not is_json_mapping(item):
|
|
1734
|
+
continue
|
|
1735
|
+
for text in _compact_item_texts(item):
|
|
1736
|
+
if text.lstrip().startswith(_GOAL_CONTINUATION_CONTEXT_PREFIX):
|
|
1737
|
+
return True
|
|
1738
|
+
return False
|
|
1739
|
+
|
|
1740
|
+
|
|
1741
|
+
def responses_request_contains_goal_continuation_context(payload: ResponsesRequest) -> bool:
|
|
1742
|
+
"""Return whether a normalized request carries Codex's goal restart marker."""
|
|
1743
|
+
|
|
1744
|
+
# ResponsesRequest normalization lifts developer/system input messages into
|
|
1745
|
+
# ``instructions``. The marker can therefore disappear from ``input`` and
|
|
1746
|
+
# follow pre-existing instruction text by the time affinity is classified.
|
|
1747
|
+
# Keep both locations in this check or a harmless parser refactor can
|
|
1748
|
+
# silently break restart recovery while marker-preservation tests still pass.
|
|
1749
|
+
instructions = payload.instructions
|
|
1750
|
+
if isinstance(instructions, str) and any(
|
|
1751
|
+
line.lstrip().startswith(_GOAL_CONTINUATION_CONTEXT_PREFIX) for line in instructions.splitlines()
|
|
1752
|
+
):
|
|
1753
|
+
return True
|
|
1754
|
+
return responses_input_contains_goal_continuation_context(payload.input)
|
|
1755
|
+
|
|
1756
|
+
|
|
1757
|
+
def _compact_trimmed_input_with_markers(
|
|
1758
|
+
input_value: list[JsonValue], token_counts: list[int], selected_indices: set[int]
|
|
1759
|
+
) -> list[JsonValue]:
|
|
1760
|
+
trimmed: list[JsonValue] = []
|
|
1761
|
+
omitted_items = 0
|
|
1762
|
+
omitted_tokens = 0
|
|
1763
|
+
for index, item in enumerate(input_value):
|
|
1764
|
+
if index in selected_indices:
|
|
1765
|
+
if omitted_items:
|
|
1766
|
+
trimmed.append(_compact_trim_marker(omitted_items=omitted_items, omitted_tokens=omitted_tokens))
|
|
1767
|
+
omitted_items = 0
|
|
1768
|
+
omitted_tokens = 0
|
|
1769
|
+
trimmed.append(item)
|
|
1770
|
+
else:
|
|
1771
|
+
omitted_items += 1
|
|
1772
|
+
omitted_tokens += token_counts[index]
|
|
1773
|
+
if omitted_items:
|
|
1774
|
+
trimmed.append(_compact_trim_marker(omitted_items=omitted_items, omitted_tokens=omitted_tokens))
|
|
1775
|
+
return trimmed
|
|
1776
|
+
|
|
1777
|
+
|
|
1778
|
+
def _compact_trim_prefix_count(token_counts: list[int]) -> int:
|
|
1779
|
+
used = 0
|
|
1780
|
+
count = 0
|
|
1781
|
+
for token_count in token_counts:
|
|
1782
|
+
if used + token_count > _COMPACT_UPSTREAM_HEAD_ESTIMATED_TOKENS:
|
|
1783
|
+
break
|
|
1784
|
+
used += token_count
|
|
1785
|
+
count += 1
|
|
1786
|
+
return count
|
|
1787
|
+
|
|
1788
|
+
|
|
1789
|
+
def _compact_trim_suffix_indices(
|
|
1790
|
+
token_counts: list[int], *, selected_indices: set[int], start_index: int, token_budget: int
|
|
1791
|
+
) -> set[int]:
|
|
1792
|
+
used = 0
|
|
1793
|
+
indices: set[int] = set()
|
|
1794
|
+
for index in range(len(token_counts) - 1, start_index - 1, -1):
|
|
1795
|
+
if index in selected_indices:
|
|
1796
|
+
continue
|
|
1797
|
+
token_count = token_counts[index]
|
|
1798
|
+
if indices and used + token_count > token_budget:
|
|
1799
|
+
break
|
|
1800
|
+
if not indices and token_count > token_budget:
|
|
1801
|
+
indices.add(index)
|
|
1802
|
+
break
|
|
1803
|
+
used += token_count
|
|
1804
|
+
indices.add(index)
|
|
1805
|
+
return indices
|
|
1806
|
+
|
|
1807
|
+
|
|
1808
|
+
def _compact_trim_marker(*, omitted_items: int, omitted_tokens: int) -> JsonObject:
|
|
1809
|
+
return {
|
|
1810
|
+
"type": "message",
|
|
1811
|
+
"role": "user",
|
|
1812
|
+
"content": [
|
|
1813
|
+
{
|
|
1814
|
+
"type": "input_text",
|
|
1815
|
+
"text": (
|
|
1816
|
+
"[compact trim] Omitted "
|
|
1817
|
+
f"{omitted_items} input items (~{omitted_tokens} estimated tokens) "
|
|
1818
|
+
"before forwarding this oversized compact request upstream. Required compact "
|
|
1819
|
+
"state anchors and retained input items remain in their original order; "
|
|
1820
|
+
"omitted items may include terminal context."
|
|
1821
|
+
),
|
|
1822
|
+
}
|
|
1823
|
+
],
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
|
|
1827
|
+
def _estimated_json_tokens(value: JsonValue) -> int:
|
|
1828
|
+
# Match stdlib/aiohttp's default JSON wire escaping and separators. Calling
|
|
1829
|
+
# this on the whole input list also counts brackets, commas, and inter-item
|
|
1830
|
+
# whitespace, so many individually small items cannot bypass the cap.
|
|
1831
|
+
serialized = json.dumps(value, ensure_ascii=True, sort_keys=True)
|
|
1832
|
+
return max(1, (len(serialized) + _ESTIMATED_CHARS_PER_TOKEN - 1) // _ESTIMATED_CHARS_PER_TOKEN)
|
|
1833
|
+
|
|
1834
|
+
|
|
1835
|
+
def _estimated_json_array_item_tokens(value: JsonValue) -> int:
|
|
1836
|
+
serialized = json.dumps(value, ensure_ascii=True, sort_keys=True)
|
|
1837
|
+
# Every retained item also needs the array's comma-space delimiter. The
|
|
1838
|
+
# final whole-array validation below accounts exactly for brackets and the
|
|
1839
|
+
# missing delimiter after the last item.
|
|
1840
|
+
wire_chars = len(serialized) + 2
|
|
1841
|
+
return max(1, (wire_chars + _ESTIMATED_CHARS_PER_TOKEN - 1) // _ESTIMATED_CHARS_PER_TOKEN)
|
|
1842
|
+
|
|
1843
|
+
|
|
1844
|
+
def _sanitize_interleaved_reasoning_input(payload: MutableJsonObject) -> None:
|
|
1845
|
+
input_value = payload.get("input")
|
|
1846
|
+
input_items = _json_list_or_none(input_value)
|
|
1847
|
+
if input_items is None:
|
|
1848
|
+
return
|
|
1849
|
+
payload["input"] = _sanitize_input_items(input_items)
|
|
1850
|
+
|
|
1851
|
+
|
|
1852
|
+
def normalize_reasoning_aliases(payload: MutableJsonObject) -> None:
|
|
1853
|
+
reasoning_effort = payload.pop("reasoningEffort", None)
|
|
1854
|
+
snake_case_reasoning_effort = payload.pop("reasoning_effort", None)
|
|
1855
|
+
reasoning_summary = payload.pop("reasoningSummary", None)
|
|
1856
|
+
provider_thinking = payload.pop("thinking", None)
|
|
1857
|
+
provider_enable_thinking = payload.pop("enable_thinking", None)
|
|
1858
|
+
|
|
1859
|
+
reasoning_payload = _json_mapping_or_none(payload.get("reasoning"))
|
|
1860
|
+
if reasoning_payload is not None:
|
|
1861
|
+
reasoning_map: MutableJsonObject = dict(reasoning_payload.items())
|
|
1862
|
+
else:
|
|
1863
|
+
reasoning_map = {}
|
|
1864
|
+
|
|
1865
|
+
existing_effort = reasoning_map.get("effort")
|
|
1866
|
+
if isinstance(existing_effort, str) and not existing_effort.strip():
|
|
1867
|
+
reasoning_map.pop("effort")
|
|
1868
|
+
|
|
1869
|
+
alias_effort = next(
|
|
1870
|
+
(
|
|
1871
|
+
candidate.strip()
|
|
1872
|
+
for candidate in (reasoning_effort, snake_case_reasoning_effort)
|
|
1873
|
+
if isinstance(candidate, str) and candidate.strip()
|
|
1874
|
+
),
|
|
1875
|
+
None,
|
|
1876
|
+
)
|
|
1877
|
+
if alias_effort is not None and "effort" not in reasoning_map:
|
|
1878
|
+
reasoning_map["effort"] = alias_effort
|
|
1879
|
+
if isinstance(reasoning_summary, str) and "summary" not in reasoning_map:
|
|
1880
|
+
reasoning_map["summary"] = reasoning_summary
|
|
1881
|
+
|
|
1882
|
+
provider_reasoning = _normalize_thinking_alias(
|
|
1883
|
+
provider_thinking,
|
|
1884
|
+
enable_thinking=provider_enable_thinking,
|
|
1885
|
+
)
|
|
1886
|
+
if provider_reasoning is not None:
|
|
1887
|
+
if "effort" not in reasoning_map and "effort" in provider_reasoning:
|
|
1888
|
+
reasoning_map["effort"] = provider_reasoning["effort"]
|
|
1889
|
+
if "summary" not in reasoning_map and "summary" in provider_reasoning:
|
|
1890
|
+
reasoning_map["summary"] = provider_reasoning["summary"]
|
|
1891
|
+
|
|
1892
|
+
if reasoning_map:
|
|
1893
|
+
payload["reasoning"] = reasoning_map
|
|
1894
|
+
|
|
1895
|
+
|
|
1896
|
+
def _normalize_thinking_alias(
|
|
1897
|
+
thinking: JsonValue,
|
|
1898
|
+
*,
|
|
1899
|
+
enable_thinking: JsonValue,
|
|
1900
|
+
) -> MutableJsonObject | None:
|
|
1901
|
+
if isinstance(thinking, bool):
|
|
1902
|
+
if thinking:
|
|
1903
|
+
return {"effort": "medium"}
|
|
1904
|
+
if isinstance(thinking, str):
|
|
1905
|
+
normalized = thinking.strip().lower()
|
|
1906
|
+
if normalized in {"minimal", "low", "medium", "high", "xhigh", "max", "ultra"}:
|
|
1907
|
+
return {"effort": normalized}
|
|
1908
|
+
if normalized in {"enabled", "true", "on"}:
|
|
1909
|
+
return {"effort": "medium"}
|
|
1910
|
+
thinking_mapping = _json_mapping_or_none(thinking)
|
|
1911
|
+
if thinking_mapping is not None:
|
|
1912
|
+
normalized: MutableJsonObject = {}
|
|
1913
|
+
effort = thinking_mapping.get("effort")
|
|
1914
|
+
summary = thinking_mapping.get("summary")
|
|
1915
|
+
if isinstance(effort, str) and effort.strip():
|
|
1916
|
+
normalized["effort"] = effort.strip().lower()
|
|
1917
|
+
if isinstance(summary, str) and summary.strip():
|
|
1918
|
+
normalized["summary"] = summary.strip()
|
|
1919
|
+
thinking_type = thinking_mapping.get("type")
|
|
1920
|
+
if "effort" not in normalized and isinstance(thinking_type, str) and thinking_type.strip().lower() == "enabled":
|
|
1921
|
+
normalized["effort"] = "medium"
|
|
1922
|
+
enabled = thinking_mapping.get("enabled")
|
|
1923
|
+
if "effort" not in normalized and enabled is True:
|
|
1924
|
+
normalized["effort"] = "medium"
|
|
1925
|
+
if "effort" not in normalized and enable_thinking is True:
|
|
1926
|
+
normalized["effort"] = "medium"
|
|
1927
|
+
if normalized:
|
|
1928
|
+
return normalized
|
|
1929
|
+
|
|
1930
|
+
# Disabled `thinking` spellings are inactive, not authoritative: a
|
|
1931
|
+
# separate enabled alias must still participate in policy evaluation.
|
|
1932
|
+
if isinstance(enable_thinking, bool):
|
|
1933
|
+
return {"effort": "medium"} if enable_thinking else None
|
|
1934
|
+
return None
|
|
1935
|
+
|
|
1936
|
+
|
|
1937
|
+
def _normalize_openai_compatible_aliases(payload: MutableJsonObject) -> None:
|
|
1938
|
+
text_verbosity = payload.pop("textVerbosity", None)
|
|
1939
|
+
top_level_verbosity = payload.pop("verbosity", None)
|
|
1940
|
+
prompt_cache_key = payload.pop("promptCacheKey", None)
|
|
1941
|
+
prompt_cache_retention = payload.pop("promptCacheRetention", None)
|
|
1942
|
+
|
|
1943
|
+
if isinstance(prompt_cache_key, str) and "prompt_cache_key" not in payload:
|
|
1944
|
+
payload["prompt_cache_key"] = prompt_cache_key
|
|
1945
|
+
if isinstance(prompt_cache_retention, str) and "prompt_cache_retention" not in payload:
|
|
1946
|
+
payload["prompt_cache_retention"] = prompt_cache_retention
|
|
1947
|
+
|
|
1948
|
+
normalize_reasoning_aliases(payload)
|
|
1949
|
+
|
|
1950
|
+
text_payload = _json_mapping_or_none(payload.get("text"))
|
|
1951
|
+
if text_payload is not None:
|
|
1952
|
+
text_map: MutableJsonObject = dict(text_payload.items())
|
|
1953
|
+
else:
|
|
1954
|
+
text_map = {}
|
|
1955
|
+
|
|
1956
|
+
if isinstance(text_verbosity, str) and "verbosity" not in text_map:
|
|
1957
|
+
text_map["verbosity"] = text_verbosity
|
|
1958
|
+
if isinstance(top_level_verbosity, str) and "verbosity" not in text_map:
|
|
1959
|
+
text_map["verbosity"] = top_level_verbosity
|
|
1960
|
+
if text_map:
|
|
1961
|
+
payload["text"] = text_map
|
|
1962
|
+
|
|
1963
|
+
|
|
1964
|
+
def _normalize_service_tier_aliases(payload: MutableJsonObject) -> None:
|
|
1965
|
+
service_tier = payload.get("service_tier")
|
|
1966
|
+
normalized = _normalize_service_tier_alias_value(service_tier)
|
|
1967
|
+
if isinstance(normalized, str):
|
|
1968
|
+
payload["service_tier"] = normalized
|
|
1969
|
+
|
|
1970
|
+
|
|
1971
|
+
def _normalize_service_tier_alias_value(value: JsonValue) -> JsonValue:
|
|
1972
|
+
if not isinstance(value, str):
|
|
1973
|
+
return value
|
|
1974
|
+
if value.strip().lower() == "fast":
|
|
1975
|
+
return "priority"
|
|
1976
|
+
return value
|
|
1977
|
+
|
|
1978
|
+
|
|
1979
|
+
def _normalize_input_text(text: str) -> list[JsonValue]:
|
|
1980
|
+
return [{"role": "user", "content": [{"type": "input_text", "text": text}]}]
|