codexLB-up 1.25.0b10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- app/__init__.py +10 -0
- app/admin_cli.py +494 -0
- app/cli.py +339 -0
- app/codex_sessions_retag.py +487 -0
- app/core/__init__.py +0 -0
- app/core/audit/__init__.py +0 -0
- app/core/audit/service.py +216 -0
- app/core/audit/types.py +94 -0
- app/core/auth/__init__.py +236 -0
- app/core/auth/api_key_cache.py +65 -0
- app/core/auth/dashboard_access.py +429 -0
- app/core/auth/dashboard_mode.py +171 -0
- app/core/auth/dashboard_session_ttl.py +74 -0
- app/core/auth/dashboard_users_cache.py +120 -0
- app/core/auth/dependencies.py +710 -0
- app/core/auth/external_identity.py +78 -0
- app/core/auth/guardian.py +374 -0
- app/core/auth/models.py +49 -0
- app/core/auth/providers/__init__.py +129 -0
- app/core/auth/providers/oidc.py +838 -0
- app/core/auth/providers/registry.py +118 -0
- app/core/auth/refresh.py +394 -0
- app/core/auth/step_up.py +117 -0
- app/core/auth/totp.py +81 -0
- app/core/balancer/__init__.py +71 -0
- app/core/balancer/logic.py +1612 -0
- app/core/balancer/rendezvous_hash.py +21 -0
- app/core/balancer/types.py +21 -0
- app/core/bootstrap.py +172 -0
- app/core/cache/invalidation.py +426 -0
- app/core/clients/__init__.py +0 -0
- app/core/clients/codex.py +996 -0
- app/core/clients/codex_version.py +187 -0
- app/core/clients/codex_version_snapshot.py +2 -0
- app/core/clients/files.py +534 -0
- app/core/clients/headers.py +29 -0
- app/core/clients/http.py +511 -0
- app/core/clients/model_fetcher.py +276 -0
- app/core/clients/native_egress.py +1304 -0
- app/core/clients/oauth.py +477 -0
- app/core/clients/proxy.py +6089 -0
- app/core/clients/proxy_websocket.py +1411 -0
- app/core/clients/rate_limit_reset_credits.py +412 -0
- app/core/clients/stream_errors.py +12 -0
- app/core/clients/thread_cache_identity.py +347 -0
- app/core/clients/usage.py +467 -0
- app/core/clock.py +170 -0
- app/core/config/__init__.py +0 -0
- app/core/config/background_jobs.py +73 -0
- app/core/config/context_window_overrides.py +166 -0
- app/core/config/dashboard_overrides.py +145 -0
- app/core/config/inheritable.py +47 -0
- app/core/config/key_fingerprint.py +115 -0
- app/core/config/settings.py +872 -0
- app/core/config/settings_cache.py +100 -0
- app/core/config/spool_retention.py +217 -0
- app/core/config/tiers.py +174 -0
- app/core/conversation_archive.py +568 -0
- app/core/crypto.py +37 -0
- app/core/draining.py +11 -0
- app/core/errors.py +365 -0
- app/core/exceptions.py +155 -0
- app/core/handlers/__init__.py +3 -0
- app/core/handlers/exceptions.py +478 -0
- app/core/http_protocol.py +176 -0
- app/core/http_protocol_httptools.py +204 -0
- app/core/ingress_limits.py +17 -0
- app/core/metadata_store.py +18 -0
- app/core/metrics/__init__.py +0 -0
- app/core/metrics/middleware.py +74 -0
- app/core/metrics/prometheus.py +604 -0
- app/core/middleware/__init__.py +25 -0
- app/core/middleware/api_firewall.py +111 -0
- app/core/middleware/app_version.py +37 -0
- app/core/middleware/dashboard_auth_proxy.py +55 -0
- app/core/middleware/dashboard_csrf.py +177 -0
- app/core/middleware/dashboard_gzip.py +41 -0
- app/core/middleware/dashboard_overrides.py +59 -0
- app/core/middleware/firewall_cache.py +91 -0
- app/core/middleware/inflight.py +126 -0
- app/core/middleware/multipart_content_encoding.py +129 -0
- app/core/middleware/path_rewrite.py +148 -0
- app/core/middleware/request_body_limit.py +196 -0
- app/core/middleware/request_decompression.py +209 -0
- app/core/middleware/request_id.py +72 -0
- app/core/middleware/required_capability_http.py +116 -0
- app/core/middleware/trusted_proxy_headers.py +36 -0
- app/core/multipart.py +237 -0
- app/core/multipart_fields.py +91 -0
- app/core/openai/__init__.py +0 -0
- app/core/openai/chat_requests.py +493 -0
- app/core/openai/chat_responses.py +931 -0
- app/core/openai/contracts.py +46 -0
- app/core/openai/exceptions.py +24 -0
- app/core/openai/host_models.py +14 -0
- app/core/openai/images.py +413 -0
- app/core/openai/message_coercion.py +376 -0
- app/core/openai/model_refresh_scheduler.py +423 -0
- app/core/openai/model_registry.py +997 -0
- app/core/openai/model_registry_store.py +548 -0
- app/core/openai/models.py +172 -0
- app/core/openai/parsing.py +97 -0
- app/core/openai/requests.py +1980 -0
- app/core/openai/strict_schema.py +199 -0
- app/core/openai/tool_call_safety.py +99 -0
- app/core/openai/v1_requests.py +187 -0
- app/core/plan_types.py +90 -0
- app/core/prestop.py +202 -0
- app/core/rate_limiter/__init__.py +0 -0
- app/core/rate_limiter/db_rate_limiter.py +215 -0
- app/core/request_locality.py +473 -0
- app/core/resilience/__init__.py +0 -0
- app/core/resilience/backpressure.py +57 -0
- app/core/resilience/bulkhead.py +202 -0
- app/core/resilience/circuit_breaker.py +150 -0
- app/core/resilience/degradation.py +55 -0
- app/core/resilience/loop_lag_monitor.py +60 -0
- app/core/resilience/memory_monitor.py +137 -0
- app/core/resilience/network_recovery.py +256 -0
- app/core/resilience/overload.py +98 -0
- app/core/resilience/toggles.py +114 -0
- app/core/retention/__init__.py +0 -0
- app/core/retention/job.py +240 -0
- app/core/retention/scheduler.py +69 -0
- app/core/runtime_logging.py +601 -0
- app/core/scheduling/__init__.py +0 -0
- app/core/scheduling/leader_election.py +993 -0
- app/core/scheduling/leader_election_handle.py +25 -0
- app/core/server.py +140 -0
- app/core/shutdown.py +326 -0
- app/core/socket_peer.py +22 -0
- app/core/startup.py +41 -0
- app/core/timeout_invariants.py +347 -0
- app/core/tracing/__init__.py +0 -0
- app/core/tracing/otel.py +207 -0
- app/core/types.py +6 -0
- app/core/upstream_proxy/__init__.py +13 -0
- app/core/upstream_proxy/cache.py +130 -0
- app/core/upstream_proxy/resolver.py +189 -0
- app/core/upstream_proxy/types.py +104 -0
- app/core/usage/__init__.py +407 -0
- app/core/usage/depletion.py +109 -0
- app/core/usage/live_hub.py +41 -0
- app/core/usage/live_snapshots.py +174 -0
- app/core/usage/logs.py +212 -0
- app/core/usage/metadata_scheduler.py +118 -0
- app/core/usage/models.py +54 -0
- app/core/usage/pricing.py +576 -0
- app/core/usage/pricing_catalog.py +241 -0
- app/core/usage/pricing_snapshot.json +821 -0
- app/core/usage/quota.py +133 -0
- app/core/usage/refresh_policy.py +21 -0
- app/core/usage/refresh_scheduler.py +713 -0
- app/core/usage/reset_credits_refresh_scheduler.py +435 -0
- app/core/usage/types.py +168 -0
- app/core/utils/__init__.py +0 -0
- app/core/utils/json_guards.py +18 -0
- app/core/utils/locks.py +33 -0
- app/core/utils/masking.py +20 -0
- app/core/utils/proxy_env.py +104 -0
- app/core/utils/request_id.py +65 -0
- app/core/utils/retry.py +77 -0
- app/core/utils/shared_future.py +172 -0
- app/core/utils/sse.py +286 -0
- app/core/utils/time.py +24 -0
- app/db/__init__.py +0 -0
- app/db/account_identity_lock.py +56 -0
- app/db/alembic/__init__.py +1 -0
- app/db/alembic/env.py +70 -0
- app/db/alembic/revision_ids.py +43 -0
- app/db/alembic/versions/20260213_000000_base_schema.py +248 -0
- app/db/alembic/versions/20260213_000100_normalize_account_plan_types.py +45 -0
- app/db/alembic/versions/20260213_000200_add_request_logs_reasoning_effort.py +45 -0
- app/db/alembic/versions/20260213_000300_add_accounts_reset_at.py +45 -0
- app/db/alembic/versions/20260213_000400_add_accounts_chatgpt_account_id.py +45 -0
- app/db/alembic/versions/20260213_000500_add_dashboard_settings.py +70 -0
- app/db/alembic/versions/20260213_000600_add_dashboard_settings_totp.py +64 -0
- app/db/alembic/versions/20260213_000700_add_dashboard_settings_password.py +59 -0
- app/db/alembic/versions/20260213_000800_add_api_keys.py +87 -0
- app/db/alembic/versions/20260214_000000_add_api_key_limits.py +143 -0
- app/db/alembic/versions/20260215_000000_add_idx_logs_requested_at.py +38 -0
- app/db/alembic/versions/20260218_000000_add_api_key_usage_reservations.py +117 -0
- app/db/alembic/versions/20260218_000100_add_import_without_overwrite_and_drop_accounts_email_unique.py +198 -0
- app/db/alembic/versions/20260225_000000_add_dashboard_settings_routing_strategy.py +53 -0
- app/db/alembic/versions/20260228_020000_align_api_key_limit_enum_types.py +104 -0
- app/db/alembic/versions/20260228_030000_add_api_firewall_allowlist.py +42 -0
- app/db/alembic/versions/20260306_000000_add_request_logs_service_tier.py +45 -0
- app/db/alembic/versions/20260307_000000_add_api_key_enforcement_fields.py +56 -0
- app/db/alembic/versions/20260308_000000_add_sqlite_performance_indexes.py +41 -0
- app/db/alembic/versions/20260309_000000_add_additional_usage_history.py +71 -0
- app/db/alembic/versions/20260309_000000_request_logs_nullable_account_id.py +28 -0
- app/db/alembic/versions/20260310_000000_add_request_logs_transport.py +45 -0
- app/db/alembic/versions/20260310_000000_fix_postgresql_enum_value_casing.py +79 -0
- app/db/alembic/versions/20260310_120000_add_sticky_session_kinds_and_affinity_ttl.py +110 -0
- app/db/alembic/versions/20260312_000000_add_additional_usage_quota_key.py +193 -0
- app/db/alembic/versions/20260312_000000_split_sticky_sessions_primary_key_by_kind.py +96 -0
- app/db/alembic/versions/20260312_010000_merge_additional_usage_and_sticky_session_heads.py +25 -0
- app/db/alembic/versions/20260312_020000_merge_request_logs_transport_and_feature_heads.py +25 -0
- app/db/alembic/versions/20260312_020000_merge_request_logs_transport_and_sticky_heads.py +25 -0
- app/db/alembic/versions/20260312_120000_add_dashboard_upstream_stream_transport.py +62 -0
- app/db/alembic/versions/20260313_024500_merge_request_log_transport_heads.py +27 -0
- app/db/alembic/versions/20260319_100937_increase_prompt_cache_ttl_to_1800s.py +73 -0
- app/db/alembic/versions/20260319_111500_merge_prompt_cache_ttl_and_upstream_transport_heads.py +25 -0
- app/db/alembic/versions/20260319_183000_normalize_sqlite_account_status_casing.py +48 -0
- app/db/alembic/versions/20260320_000000_add_request_log_requested_actual_tiers.py +51 -0
- app/db/alembic/versions/20260321_190000_tighten_dashboard_db_indexes.py +64 -0
- app/db/alembic/versions/20260321_210000_merge_request_log_tiers_and_dashboard_index_heads.py +25 -0
- app/db/alembic/versions/20260325_000000_add_request_log_cost.py +193 -0
- app/db/alembic/versions/20260328_000000_add_rate_limit_attempts.py +48 -0
- app/db/alembic/versions/20260328_120000_add_request_log_ttft.py +37 -0
- app/db/alembic/versions/20260328_130000_add_audit_logs_table.py +45 -0
- app/db/alembic/versions/20260328_140000_add_scheduler_leader_table.py +39 -0
- app/db/alembic/versions/20260330_000000_add_cache_locality_settings.py +69 -0
- app/db/alembic/versions/20260330_010000_merge_scheduler_leader_and_cache_locality_heads.py +25 -0
- app/db/alembic/versions/20260330_020000_add_bridge_ring_members.py +46 -0
- app/db/alembic/versions/20260401_000000_add_api_key_enforced_service_tier.py +52 -0
- app/db/alembic/versions/20260401_000000_add_cache_invalidation.py +41 -0
- app/db/alembic/versions/20260401_020000_merge_cache_invalidation_and_api_key_service_tier_heads.py +22 -0
- app/db/alembic/versions/20260402_000000_switch_dashboard_routing_default_to_capacity_weighted.py +66 -0
- app/db/alembic/versions/20260403_000000_add_credit_api_key_limit_values.py +44 -0
- app/db/alembic/versions/20260406_000000_add_api_key_account_assignments.py +57 -0
- app/db/alembic/versions/20260406_010000_add_api_key_assignment_scope_flag.py +75 -0
- app/db/alembic/versions/20260407_000000_add_bridge_gateway_safe_mode.py +51 -0
- app/db/alembic/versions/20260407_010000_merge_api_key_assignment_and_bridge_gateway_heads.py +25 -0
- app/db/alembic/versions/20260408_000000_switch_import_without_overwrite_default_to_true.py +73 -0
- app/db/alembic/versions/20260408_010000_merge_import_without_overwrite_and_assignment_heads.py +25 -0
- app/db/alembic/versions/20260409_000000_add_http_bridge_sessions.py +130 -0
- app/db/alembic/versions/20260409_000000_switch_sticky_threads_and_prefer_earlier_reset_defaults_to_true.py +89 -0
- app/db/alembic/versions/20260409_010000_add_dashboard_settings_bootstrap_token.py +57 -0
- app/db/alembic/versions/20260409_010000_merge_http_bridge_and_import_without_overwrite_heads.py +25 -0
- app/db/alembic/versions/20260409_020000_fix_http_bridge_last_seen_index.py +46 -0
- app/db/alembic/versions/20260410_000000_backfill_pristine_dashboard_settings_defaults.py +77 -0
- app/db/alembic/versions/20260410_030000_restore_import_without_overwrite_default_true.py +73 -0
- app/db/alembic/versions/20260410_040000_merge_dashboard_defaults_and_import_default_heads.py +25 -0
- app/db/alembic/versions/20260413_000000_add_accounts_blocked_at.py +44 -0
- app/db/alembic/versions/20260415_160000_add_automations_tables.py +189 -0
- app/db/alembic/versions/20260415_160000_add_request_logs_response_lookup_index.py +67 -0
- app/db/alembic/versions/20260417_000000_add_request_log_plan_type.py +44 -0
- app/db/alembic/versions/20260418_190000_add_automation_runs_cycle_key.py +87 -0
- app/db/alembic/versions/20260419_000000_add_automation_run_cycle_metadata.py +56 -0
- app/db/alembic/versions/20260419_020000_add_automation_run_cycles_snapshot_tables.py +342 -0
- app/db/alembic/versions/20260421_000000_add_dashboard_session_ttl_setting.py +51 -0
- app/db/alembic/versions/20260421_120000_merge_request_log_lookup_and_plan_type_heads.py +25 -0
- app/db/alembic/versions/20260421_130000_merge_automation_and_request_log_heads.py +25 -0
- app/db/alembic/versions/20260422_103000_normalize_legacy_automation_run_cycle_keys.py +648 -0
- app/db/alembic/versions/20260423_120000_add_api_key_limit_reset_at_index.py +43 -0
- app/db/alembic/versions/20260424_000000_merge_dashboard_session_ttl_and_request_log_heads.py +25 -0
- app/db/alembic/versions/20260426_000000_add_dashboard_relative_availability_settings.py +68 -0
- app/db/alembic/versions/20260427_130000_add_warmup_model_and_request_kind.py +75 -0
- app/db/alembic/versions/20260506_000000_add_account_routing_policy.py +37 -0
- app/db/alembic/versions/20260509_000000_add_api_key_traffic_class.py +37 -0
- app/db/alembic/versions/20260509_010000_add_additional_quota_routing_policies.py +37 -0
- app/db/alembic/versions/20260513_000000_add_accounts_alias.py +44 -0
- app/db/alembic/versions/20260513_000000_add_api_key_apply_to_codex_model.py +59 -0
- app/db/alembic/versions/20260514_000000_add_request_logs_api_key_time_index.py +43 -0
- app/db/alembic/versions/20260515_000000_add_prefer_earlier_reset_window.py +47 -0
- app/db/alembic/versions/20260515_000000_soft_delete_request_logs_on_account_delete.py +83 -0
- app/db/alembic/versions/20260515_020000_add_split_sticky_budget_thresholds.py +62 -0
- app/db/alembic/versions/20260516_000000_add_sqlite_hot_path_indexes.py +75 -0
- app/db/alembic/versions/20260517_000000_merge_sqlite_hot_path_and_request_log_delete_heads.py +24 -0
- app/db/alembic/versions/20260518_000000_add_http_bridge_durable_input_prefix.py +44 -0
- app/db/alembic/versions/20260518_010000_merge_http_bridge_and_sqlite_recovery_heads.py +25 -0
- app/db/alembic/versions/20260518_120000_add_dashboard_guest_access.py +57 -0
- app/db/alembic/versions/20260520_000000_merge_api_key_and_http_bridge_heads.py +26 -0
- app/db/alembic/versions/20260520_010000_add_request_logs_api_key_account_index.py +45 -0
- app/db/alembic/versions/20260520_010000_merge_automation_and_api_key_heads.py +26 -0
- app/db/alembic/versions/20260520_030000_add_quota_planner.py +194 -0
- app/db/alembic/versions/20260520_040000_merge_quota_planner_and_request_log_index_heads.py +24 -0
- app/db/alembic/versions/20260521_000000_add_account_security_work_authorized.py +46 -0
- app/db/alembic/versions/20260521_000000_merge_dashboard_guest_and_api_key_heads.py +26 -0
- app/db/alembic/versions/20260521_000000_merge_prefer_reset_window_head.py +26 -0
- app/db/alembic/versions/20260521_000000_merge_warmup_and_request_log_index_heads.py +25 -0
- app/db/alembic/versions/20260522_000000_add_limit_warmup_trigger.py +195 -0
- app/db/alembic/versions/20260524_000000_merge_accounts_alias_and_reset_window_heads.py +24 -0
- app/db/alembic/versions/20260525_000000_add_usage_raw_window_latest_index.py +31 -0
- app/db/alembic/versions/20260526_000000_add_request_log_failure_metadata.py +100 -0
- app/db/alembic/versions/20260531_000000_add_account_workspace_identity.py +39 -0
- app/db/alembic/versions/20260531_000000_add_single_account_routing.py +42 -0
- app/db/alembic/versions/20260601_000000_merge_relative_availability_and_usage_raw_heads.py +24 -0
- app/db/alembic/versions/20260601_010000_add_api_key_usage_sections.py +58 -0
- app/db/alembic/versions/20260601_010000_add_routing_policy_persistence.py +67 -0
- app/db/alembic/versions/20260601_010000_merge_security_work_and_relative_availability_heads.py +25 -0
- app/db/alembic/versions/20260601_010000_merge_split_sticky_and_relative_usage_heads.py +25 -0
- app/db/alembic/versions/20260601_010000_merge_warmup_and_relative_availability_heads.py +25 -0
- app/db/alembic/versions/20260601_020000_merge_additional_quota_routing_and_relative_availability_heads.py +25 -0
- app/db/alembic/versions/20260601_020000_merge_quota_planner_and_relative_availability_heads.py +25 -0
- app/db/alembic/versions/20260601_020000_merge_reset_window_and_relative_availability_heads.py +25 -0
- app/db/alembic/versions/20260601_020000_merge_routing_policy_and_split_threshold_heads.py +24 -0
- app/db/alembic/versions/20260601_020000_merge_warmup_and_request_log_failure_heads.py +25 -0
- app/db/alembic/versions/20260601_030000_merge_single_account_and_routing_quota_heads.py +29 -0
- app/db/alembic/versions/20260601_040000_merge_security_work_and_routing_consolidation_heads.py +25 -0
- app/db/alembic/versions/20260601_050000_merge_quota_planner_and_routing_consolidation_heads.py +25 -0
- app/db/alembic/versions/20260602_050000_add_upstream_proxy_routing.py +164 -0
- app/db/alembic/versions/20260602_060000_merge_account_workspace_and_failure_heads.py +25 -0
- app/db/alembic/versions/20260602_070000_add_request_log_useragent_fields.py +86 -0
- app/db/alembic/versions/20260602_070000_merge_quota_planner_and_account_workspace_heads.py +25 -0
- app/db/alembic/versions/20260602_080000_merge_upstream_proxy_and_quota_planner_heads.py +25 -0
- app/db/alembic/versions/20260602_080000_merge_useragent_and_upstream_proxy_heads.py +24 -0
- app/db/alembic/versions/20260603_000000_add_weekly_pace_working_days.py +52 -0
- app/db/alembic/versions/20260603_000000_free_account_monthly_window.py +60 -0
- app/db/alembic/versions/20260604_000000_add_reauth_required_account_status.py +159 -0
- app/db/alembic/versions/20260604_010000_merge_automations_and_reauth_status_heads.py +25 -0
- app/db/alembic/versions/20260604_010000_merge_dashboard_guest_and_reauth_status_heads.py +25 -0
- app/db/alembic/versions/20260604_010000_merge_useragent_and_reauth_heads.py +24 -0
- app/db/alembic/versions/20260607_000000_merge_weekly_monthly_useragent_heads.py +28 -0
- app/db/alembic/versions/20260608_000000_add_hide_upstream_quota_from_api_keys.py +50 -0
- app/db/alembic/versions/20260611_000000_add_request_log_archive_request_id.py +47 -0
- app/db/alembic/versions/20260611_000000_merge_automations_and_weekly_monthly_useragent_heads.py +26 -0
- app/db/alembic/versions/20260611_000000_merge_dashboard_guest_and_weekly_useragent_heads.py +26 -0
- app/db/alembic/versions/20260611_010000_add_request_log_client_ip.py +57 -0
- app/db/alembic/versions/20260611_020000_merge_request_log_archive_and_client_ip_heads.py +26 -0
- app/db/alembic/versions/20260611_030000_merge_dashboard_guest_and_request_log_client_ip_heads.py +26 -0
- app/db/alembic/versions/20260613_000000_add_accounts_codex_installation_id.py +57 -0
- app/db/alembic/versions/20260613_010000_merge_installation_id_and_dashboard_weekly_heads.py +26 -0
- app/db/alembic/versions/20260615_000000_merge_automations_and_dashboard_guest_heads.py +26 -0
- app/db/alembic/versions/20260623_000000_add_limit_warmup_exhausted_threshold.py +51 -0
- app/db/alembic/versions/20260626_000000_add_http_downstream_transport_policy.py +46 -0
- app/db/alembic/versions/20260626_010000_add_request_logs_upstream_transport.py +31 -0
- app/db/alembic/versions/20260629_000000_add_dashboard_query_hot_path_indexes.py +53 -0
- app/db/alembic/versions/20260630_000000_add_limit_warmup_staggered_idle.py +47 -0
- app/db/alembic/versions/20260630_000000_merge_request_log_client_ip_and_dashboard_hot_path_heads.py +26 -0
- app/db/alembic/versions/20260630_010000_merge_warmup_and_request_log_dashboard_heads.py +26 -0
- app/db/alembic/versions/20260630_020000_merge_automations_and_main_heads.py +26 -0
- app/db/alembic/versions/20260630_020000_merge_warmup_threshold_and_main_heads.py +26 -0
- app/db/alembic/versions/20260630_030000_add_automation_run_model_snapshot.py +59 -0
- app/db/alembic/versions/20260630_040000_merge_automation_snapshot_and_warmup_threshold_heads.py +26 -0
- app/db/alembic/versions/20260630_050000_add_automation_run_prompt_snapshot.py +52 -0
- app/db/alembic/versions/20260701_000000_add_weekly_pace_smoothing_minutes.py +51 -0
- app/db/alembic/versions/20260702_000000_add_model_sources.py +176 -0
- app/db/alembic/versions/20260705_000000_add_model_source_audio_transcriptions.py +50 -0
- app/db/alembic/versions/20260705_000000_harden_dashboard_session_ttl.py +62 -0
- app/db/alembic/versions/20260706_000000_add_model_source_audio_per_minute.py +43 -0
- app/db/alembic/versions/20260707_000000_merge_model_sources_and_session_ttl.py +24 -0
- app/db/alembic/versions/20260707_010000_merge_automations_and_model_sources_heads.py +24 -0
- app/db/alembic/versions/20260707_020000_add_automation_job_account_scope.py +125 -0
- app/db/alembic/versions/20260709_000000_add_ttft_phase_observability.py +66 -0
- app/db/alembic/versions/20260711_000000_add_dashboard_prohibit_fast_mode.py +42 -0
- app/db/alembic/versions/20260711_010000_add_account_chatgpt_user_id.py +40 -0
- app/db/alembic/versions/20260711_020000_add_dashboard_account_concurrency_caps.py +71 -0
- app/db/alembic/versions/20260711_030000_add_limit_warmup_idle_threshold.py +51 -0
- app/db/alembic/versions/20260712_010000_add_account_usage_rollups.py +56 -0
- app/db/alembic/versions/20260712_020000_add_api_key_usage_rollups.py +53 -0
- app/db/alembic/versions/20260713_020000_add_model_registry_snapshot.py +42 -0
- app/db/alembic/versions/20260713_040000_add_account_refresh_claims.py +42 -0
- app/db/alembic/versions/20260713_040000_add_replica_guardrails.py +64 -0
- app/db/alembic/versions/20260713_070000_add_reset_credit_redeem_tables.py +58 -0
- app/db/alembic/versions/20260714_000000_add_oauth_flow_states.py +63 -0
- app/db/alembic/versions/20260715_000000_add_request_log_queue_latency.py +44 -0
- app/db/alembic/versions/20260716_000000_add_oauth_device_flow_slots.py +41 -0
- app/db/alembic/versions/20260716_010000_add_dashboard_retention_settings.py +48 -0
- app/db/alembic/versions/20260716_010000_add_reset_credit_display_settings.py +73 -0
- app/db/alembic/versions/20260717_000000_add_http_bridge_retry_circuits.py +47 -0
- app/db/alembic/versions/20260717_000000_merge_retention_and_reset_credit_display_heads.py +26 -0
- app/db/alembic/versions/20260717_000000_optimize_dashboard_hot_path_indexes.py +162 -0
- app/db/alembic/versions/20260717_000001_merge_retry_circuits_and_dashboard_indexes.py +22 -0
- app/db/alembic/versions/20260720_000000_add_request_log_conversation_id.py +57 -0
- app/db/alembic/versions/20260722_000000_backfill_request_log_useragent_families.py +40 -0
- app/db/alembic/versions/20260724_000000_add_request_usage_time_rollups.py +176 -0
- app/db/alembic/versions/20260724_000000_merge_request_log_schema_heads.py +33 -0
- app/db/alembic/versions/20260725_000000_add_http_bridge_pending_tool_calls.py +43 -0
- app/db/alembic/versions/20260726_000000_add_account_plan_downgrade_observations.py +44 -0
- app/db/alembic/versions/20260726_000000_repair_request_usage_rollups_after_merge.py +37 -0
- app/db/alembic/versions/20260727_000000_add_sticky_session_continuity_abandoned_at.py +45 -0
- app/db/alembic/versions/20260728_000000_merge_pending_tool_calls_and_rollup_repair_heads.py +31 -0
- app/db/alembic/versions/20260730_000000_add_api_key_fair_share_threshold.py +44 -0
- app/db/alembic/versions/20260730_000000_add_http_bridge_recovery_attempts.py +60 -0
- app/db/alembic/versions/20260731_000000_add_capability_lineage_markers.py +38 -0
- app/db/alembic/versions/20260803_000000_merge_http_bridge_recovery_and_capability_lineage_heads.py +30 -0
- app/db/alembic/versions/20260804_000000_add_http_bridge_operations.py +63 -0
- app/db/alembic/versions/20260804_000001_add_global_http_bridge_operation_fingerprint.py +53 -0
- app/db/alembic/versions/20260804_230000_add_request_log_connection_request_kind.py +43 -0
- app/db/alembic/versions/20260805_000000_add_http_bridge_operation_spool.py +75 -0
- app/db/alembic/versions/20260805_000001_finalize_http_bridge_operation_spool.py +65 -0
- app/db/alembic/versions/20260806_000000_add_additional_usage_alias_probe_indexes.py +84 -0
- app/db/alembic/versions/20260806_000000_add_anonymous_telemetry.py +49 -0
- app/db/alembic/versions/20260806_010000_add_conversation_presence_rollup.py +72 -0
- app/db/alembic/versions/20260806_020000_add_usage_history_bulk_covering_indexes.py +99 -0
- app/db/alembic/versions/20260806_030000_add_api_key_allowed_reasoning_efforts.py +65 -0
- app/db/alembic/versions/20260806_120000_add_http_bridge_owner_process_epoch.py +57 -0
- app/db/alembic/versions/20260807_000000_merge_http_bridge_operation_spool_and_latest_main.py +24 -0
- app/db/alembic/versions/20260808_000000_tune_usage_history_autovacuum.py +54 -0
- app/db/alembic/versions/20260810_000000_add_http_bridge_recovery_dispatch_count.py +44 -0
- app/db/alembic/versions/20260811_000000_add_hourly_rollup_cancelled_count.py +92 -0
- app/db/alembic/versions/20260812_000000_merge_recovery_dispatch_and_hourly_cancelled_heads.py +25 -0
- app/db/alembic/versions/20260812_120000_add_sticky_abandonment_scope.py +43 -0
- app/db/alembic/versions/20260813_000000_add_file_account_pins.py +40 -0
- app/db/alembic/versions/20260816_000000_add_account_pending_deletion.py +81 -0
- app/db/alembic/versions/20260816_000000_add_model_source_embeddings.py +50 -0
- app/db/alembic/versions/20260821_000000_add_retry_circuit_admission_generation.py +49 -0
- app/db/alembic/versions/20260826_000000_add_http_bridge_event_chunks.py +107 -0
- app/db/alembic/versions/20260828_000000_add_accounts_chatgpt_identity_index.py +55 -0
- app/db/alembic/versions/20260830_000000_add_quota_warmup_claim_expiry.py +83 -0
- app/db/alembic/versions/20260908_000000_add_guest_session_generation.py +37 -0
- app/db/alembic/versions/20260908_000000_add_subscription_overflow.py +140 -0
- app/db/alembic/versions/20260908_000000_replace_upstream_stream_transport_default_sentinel.py +50 -0
- app/db/alembic/versions/20260908_020000_merge_overflow_transport_heads.py +27 -0
- app/db/alembic/versions/20260909_000000_add_dashboard_roles.py +72 -0
- app/db/alembic/versions/20260909_010000_add_dashboard_users.py +183 -0
- app/db/alembic/versions/20260909_020000_reproject_compat_admin_credentials.py +105 -0
- app/db/alembic/versions/20260909_030000_add_audit_actor_columns.py +103 -0
- app/db/alembic/versions/20260909_030000_dashboard_resilience_toggle_settings.py +63 -0
- app/db/alembic/versions/20260909_040000_add_dashboard_user_invites.py +54 -0
- app/db/alembic/versions/20260909_040000_dashboard_timeout_settings.py +68 -0
- app/db/alembic/versions/20260909_050000_dashboard_routing_overload_settings.py +65 -0
- app/db/alembic/versions/20260909_060000_add_report_rollup.py +74 -0
- app/db/alembic/versions/20260909_070000_automation_run_claim_budget.py +50 -0
- app/db/alembic/versions/20260909_080000_dashboard_stream_bridge_budgets.py +62 -0
- app/db/alembic/versions/20260909_090000_dashboard_background_job_toggles.py +63 -0
- app/db/alembic/versions/20260909_100000_dashboard_codex_prewarm.py +49 -0
- app/db/alembic/versions/20260909_110000_model_context_window_overrides.py +45 -0
- app/db/alembic/versions/20260909_120000_dashboard_conversation_archive.py +48 -0
- app/db/alembic/versions/20260909_130000_add_request_logs_live_facet_indexes.py +96 -0
- app/db/alembic/versions/20260910_000000_add_totp_required_for_admin_role.py +44 -0
- app/db/alembic/versions/20260910_000000_request_logs_missing_cost_index.py +52 -0
- app/db/alembic/versions/20260910_010000_add_dashboard_auth_providers.py +134 -0
- app/db/alembic/versions/20260910_010000_dashboard_spool_retention.py +52 -0
- app/db/alembic/versions/20260910_020000_add_dashboard_role_mappings.py +74 -0
- app/db/alembic/versions/20260910_143000_request_log_affinity.py +28 -0
- app/db/alembic/versions/20260910_180000_merge_affinity_guest_heads.py +17 -0
- app/db/alembic/versions/20260910_200000_merge_affinity_identity_heads.py +17 -0
- app/db/alembic/versions/20260910_220000_merge_affinity_invite_heads.py +17 -0
- app/db/alembic/versions/20260911_000000_model_source_pins_kind_expires_index.py +43 -0
- app/db/alembic/versions/20260911_010000_merge_pin_index_and_affinity_heads.py +17 -0
- app/db/alembic/versions/20260911_020000_add_http_bridge_terminal_append_phase.py +54 -0
- app/db/alembic/versions/20260911_030000_add_local_login_policy.py +48 -0
- app/db/alembic/versions/20260911_040000_add_thread_cache_identity_mode.py +48 -0
- app/db/alembic/versions/20260911_060000_add_bridge_session_continuity_abandonment.py +40 -0
- app/db/alembic/versions/20260912_000000_merge_thread_cache_and_bridge_retirement_heads.py +22 -0
- app/db/alembic/versions/20260912_010000_drop_legacy_dashboard_credentials.py +140 -0
- app/db/alembic/versions/20260913_000000_add_oidc_provider_flow.py +134 -0
- app/db/alembic/versions/20260914_000000_add_scim_tokens.py +78 -0
- app/db/alembic/versions/20260914_000000_drop_subscription_overflow_schema.py +132 -0
- app/db/alembic/versions/20260918_000000_merge_scim_and_overflow_heads.py +17 -0
- app/db/alembic/versions/__init__.py +1 -0
- app/db/backup.py +64 -0
- app/db/migrate.py +1249 -0
- app/db/migration_lock.py +160 -0
- app/db/migration_url.py +18 -0
- app/db/models.py +3100 -0
- app/db/recover.py +244 -0
- app/db/session.py +1295 -0
- app/db/snapshot.py +46 -0
- app/db/sqlite_lock_retry.py +190 -0
- app/db/sqlite_utils.py +549 -0
- app/dependencies.py +462 -0
- app/main.py +1240 -0
- app/modules/__init__.py +0 -0
- app/modules/accounts/__init__.py +0 -0
- app/modules/accounts/api.py +426 -0
- app/modules/accounts/auth_manager.py +1439 -0
- app/modules/accounts/background_repository.py +160 -0
- app/modules/accounts/deletion.py +540 -0
- app/modules/accounts/mappers.py +654 -0
- app/modules/accounts/refresh_claims.py +352 -0
- app/modules/accounts/repository.py +1679 -0
- app/modules/accounts/schemas.py +308 -0
- app/modules/accounts/service.py +809 -0
- app/modules/accounts/usage_rollup.py +486 -0
- app/modules/accounts/usage_rollup_scheduler.py +75 -0
- app/modules/accounts/usage_time_rollup.py +1037 -0
- app/modules/accounts/usage_time_rollup_read.py +522 -0
- app/modules/api_keys/__init__.py +1 -0
- app/modules/api_keys/api.py +344 -0
- app/modules/api_keys/last_used_coalescer.py +216 -0
- app/modules/api_keys/limit_windows.py +41 -0
- app/modules/api_keys/repository.py +1200 -0
- app/modules/api_keys/reset_scheduler.py +85 -0
- app/modules/api_keys/schemas.py +135 -0
- app/modules/api_keys/service.py +2123 -0
- app/modules/audit/__init__.py +0 -0
- app/modules/audit/api.py +88 -0
- app/modules/audit/repository.py +74 -0
- app/modules/audit/schemas.py +33 -0
- app/modules/audit/service.py +71 -0
- app/modules/auth_providers/__init__.py +0 -0
- app/modules/auth_providers/api.py +127 -0
- app/modules/auth_providers/config.py +343 -0
- app/modules/auth_providers/repository.py +101 -0
- app/modules/auth_providers/schemas.py +88 -0
- app/modules/auth_providers/seed.py +105 -0
- app/modules/auth_providers/service.py +289 -0
- app/modules/automations/__init__.py +1 -0
- app/modules/automations/api.py +369 -0
- app/modules/automations/repository.py +1896 -0
- app/modules/automations/scheduler.py +107 -0
- app/modules/automations/schemas.py +158 -0
- app/modules/automations/service.py +2548 -0
- app/modules/cache_isolation_probe/__init__.py +0 -0
- app/modules/cache_isolation_probe/api.py +188 -0
- app/modules/cache_isolation_probe/prefix.py +144 -0
- app/modules/cache_isolation_probe/schemas.py +94 -0
- app/modules/cache_isolation_probe/sender.py +229 -0
- app/modules/cache_isolation_probe/service.py +525 -0
- app/modules/conversation_archive/__init__.py +1 -0
- app/modules/conversation_archive/api.py +101 -0
- app/modules/conversation_archive/schemas.py +28 -0
- app/modules/conversation_archive/service.py +165 -0
- app/modules/dashboard/__init__.py +1 -0
- app/modules/dashboard/api.py +106 -0
- app/modules/dashboard/builders.py +107 -0
- app/modules/dashboard/repository.py +304 -0
- app/modules/dashboard/schemas.py +148 -0
- app/modules/dashboard/service.py +534 -0
- app/modules/dashboard/timeframes.py +19 -0
- app/modules/dashboard/weekly_pace.py +617 -0
- app/modules/dashboard_auth/__init__.py +1 -0
- app/modules/dashboard_auth/api.py +1344 -0
- app/modules/dashboard_auth/oidc_api.py +753 -0
- app/modules/dashboard_auth/oidc_flows.py +330 -0
- app/modules/dashboard_auth/repository.py +452 -0
- app/modules/dashboard_auth/schemas.py +232 -0
- app/modules/dashboard_auth/service.py +1384 -0
- app/modules/dashboard_roles/__init__.py +0 -0
- app/modules/dashboard_roles/api.py +64 -0
- app/modules/dashboard_roles/repository.py +54 -0
- app/modules/dashboard_roles/schemas.py +29 -0
- app/modules/dashboard_roles/seed.py +80 -0
- app/modules/dashboard_roles/service.py +142 -0
- app/modules/dashboard_users/__init__.py +0 -0
- app/modules/dashboard_users/api.py +278 -0
- app/modules/dashboard_users/break_glass.py +169 -0
- app/modules/dashboard_users/credentials.py +18 -0
- app/modules/dashboard_users/identity_resolver.py +665 -0
- app/modules/dashboard_users/repository.py +725 -0
- app/modules/dashboard_users/schemas.py +117 -0
- app/modules/dashboard_users/service.py +1054 -0
- app/modules/firewall/__init__.py +1 -0
- app/modules/firewall/api.py +78 -0
- app/modules/firewall/repository.py +51 -0
- app/modules/firewall/schemas.py +28 -0
- app/modules/firewall/service.py +93 -0
- app/modules/fleet/__init__.py +1 -0
- app/modules/fleet/api.py +177 -0
- app/modules/fleet/mappers.py +56 -0
- app/modules/fleet/observability.py +372 -0
- app/modules/fleet/schemas.py +119 -0
- app/modules/health/__init__.py +0 -0
- app/modules/health/api.py +246 -0
- app/modules/health/schemas.py +27 -0
- app/modules/limit_warmup/__init__.py +1 -0
- app/modules/limit_warmup/repository.py +159 -0
- app/modules/limit_warmup/service.py +1001 -0
- app/modules/model_sources/__init__.py +1 -0
- app/modules/model_sources/api.py +99 -0
- app/modules/model_sources/catalog.py +307 -0
- app/modules/model_sources/forwarding.py +1435 -0
- app/modules/model_sources/projection.py +55 -0
- app/modules/model_sources/repository.py +201 -0
- app/modules/model_sources/schemas.py +79 -0
- app/modules/model_sources/selection.py +166 -0
- app/modules/model_sources/service.py +236 -0
- app/modules/oauth/__init__.py +0 -0
- app/modules/oauth/api.py +101 -0
- app/modules/oauth/repository.py +238 -0
- app/modules/oauth/schemas.py +49 -0
- app/modules/oauth/service.py +1196 -0
- app/modules/oauth/templates/oauth_success.html +122 -0
- app/modules/proxy/__init__.py +0 -0
- app/modules/proxy/_load_balancer/__init__.py +1 -0
- app/modules/proxy/_load_balancer/error_rate.py +116 -0
- app/modules/proxy/_load_balancer/latency_cohort.py +154 -0
- app/modules/proxy/_load_balancer/model_eligibility.py +250 -0
- app/modules/proxy/_load_balancer/opportunistic_admission.py +350 -0
- app/modules/proxy/_load_balancer/overload_backoff.py +394 -0
- app/modules/proxy/_load_balancer/sticky_selection.py +2417 -0
- app/modules/proxy/_load_balancer/throughput_cohort.py +233 -0
- app/modules/proxy/_load_balancer/ttft_cohort.py +214 -0
- app/modules/proxy/_load_balancer/tunables.py +97 -0
- app/modules/proxy/_load_balancer/types.py +96 -0
- app/modules/proxy/_load_balancer/unbound_selection.py +534 -0
- app/modules/proxy/_service/__init__.py +6 -0
- app/modules/proxy/_service/api_key_usage.py +745 -0
- app/modules/proxy/_service/clock_budget.py +38 -0
- app/modules/proxy/_service/codex_control.py +611 -0
- app/modules/proxy/_service/compact.py +2211 -0
- app/modules/proxy/_service/file_ops.py +749 -0
- app/modules/proxy/_service/http_bridge/__init__.py +3 -0
- app/modules/proxy/_service/http_bridge/accepted_replay.py +331 -0
- app/modules/proxy/_service/http_bridge/account_sessions.py +57 -0
- app/modules/proxy/_service/http_bridge/activity.py +178 -0
- app/modules/proxy/_service/http_bridge/helpers.py +3959 -0
- app/modules/proxy/_service/http_bridge/mixin.py +2435 -0
- app/modules/proxy/_service/http_bridge/owner_forwarding.py +609 -0
- app/modules/proxy/_service/http_bridge/protocol.py +167 -0
- app/modules/proxy/_service/http_bridge/proxy_failover.py +58 -0
- app/modules/proxy/_service/http_bridge/quarantine.py +443 -0
- app/modules/proxy/_service/http_bridge/request_submit.py +4582 -0
- app/modules/proxy/_service/http_bridge/retry_circuit.py +2442 -0
- app/modules/proxy/_service/http_bridge/service_stubs.py +492 -0
- app/modules/proxy/_service/http_bridge/session_registry.py +778 -0
- app/modules/proxy/_service/http_bridge/streaming.py +5328 -0
- app/modules/proxy/_service/http_bridge/upstream_events.py +4538 -0
- app/modules/proxy/_service/observability.py +410 -0
- app/modules/proxy/_service/rate_limit.py +361 -0
- app/modules/proxy/_service/realtime_live.py +655 -0
- app/modules/proxy/_service/refresh.py +154 -0
- app/modules/proxy/_service/request_log.py +585 -0
- app/modules/proxy/_service/response_create.py +930 -0
- app/modules/proxy/_service/streaming/__init__.py +3 -0
- app/modules/proxy/_service/streaming/helpers.py +1317 -0
- app/modules/proxy/_service/streaming/mixin.py +1100 -0
- app/modules/proxy/_service/streaming/protocol.py +32 -0
- app/modules/proxy/_service/streaming/retry.py +3762 -0
- app/modules/proxy/_service/support.py +2122 -0
- app/modules/proxy/_service/transcribe.py +434 -0
- app/modules/proxy/_service/warmup.py +483 -0
- app/modules/proxy/_service/websocket/__init__.py +3 -0
- app/modules/proxy/_service/websocket/helpers.py +2360 -0
- app/modules/proxy/_service/websocket/mixin.py +7285 -0
- app/modules/proxy/_service/websocket/protocol.py +74 -0
- app/modules/proxy/account_cache.py +229 -0
- app/modules/proxy/account_eligibility.py +87 -0
- app/modules/proxy/additional_model_limits.py +40 -0
- app/modules/proxy/affinity.py +830 -0
- app/modules/proxy/affinity_observation.py +101 -0
- app/modules/proxy/api.py +10254 -0
- app/modules/proxy/api_key_usage.py +113 -0
- app/modules/proxy/cap_partitioning.py +280 -0
- app/modules/proxy/capability_lineage.py +65 -0
- app/modules/proxy/capability_lineage_repository.py +82 -0
- app/modules/proxy/capability_routing.py +253 -0
- app/modules/proxy/continuity.py +112 -0
- app/modules/proxy/downstream_delivery.py +277 -0
- app/modules/proxy/durable_bridge_coordinator.py +1217 -0
- app/modules/proxy/durable_bridge_repository.py +4516 -0
- app/modules/proxy/durable_bridge_runtime.py +9 -0
- app/modules/proxy/durable_bridge_transcript_codec.py +122 -0
- app/modules/proxy/fair_share.py +131 -0
- app/modules/proxy/file_pin_repository.py +210 -0
- app/modules/proxy/helpers.py +439 -0
- app/modules/proxy/http_bridge_event_batcher.py +613 -0
- app/modules/proxy/http_bridge_forwarding.py +797 -0
- app/modules/proxy/http_continuation.py +76 -0
- app/modules/proxy/images_observability.py +62 -0
- app/modules/proxy/images_service.py +1054 -0
- app/modules/proxy/load_balancer.py +3016 -0
- app/modules/proxy/rate_limit_cache.py +46 -0
- app/modules/proxy/replay_safety.py +1063 -0
- app/modules/proxy/repo_bundle.py +31 -0
- app/modules/proxy/request_policy.py +1054 -0
- app/modules/proxy/ring_membership.py +251 -0
- app/modules/proxy/schemas.py +345 -0
- app/modules/proxy/selection_errors.py +41 -0
- app/modules/proxy/service.py +2600 -0
- app/modules/proxy/source_admission.py +125 -0
- app/modules/proxy/source_dispatch.py +999 -0
- app/modules/proxy/sticky_repository.py +831 -0
- app/modules/proxy/thread_anchors.py +491 -0
- app/modules/proxy/tool_call_dedupe.py +802 -0
- app/modules/proxy/types.py +54 -0
- app/modules/proxy/work_admission.py +121 -0
- app/modules/quota_planner/__init__.py +1 -0
- app/modules/quota_planner/api.py +276 -0
- app/modules/quota_planner/logic.py +743 -0
- app/modules/quota_planner/repository.py +722 -0
- app/modules/quota_planner/scheduler.py +212 -0
- app/modules/quota_planner/schemas.py +102 -0
- app/modules/quota_planner/warmup.py +693 -0
- app/modules/rate_limit_reset_credits/__init__.py +0 -0
- app/modules/rate_limit_reset_credits/api.py +601 -0
- app/modules/rate_limit_reset_credits/redeem_coordination.py +274 -0
- app/modules/rate_limit_reset_credits/store.py +114 -0
- app/modules/reports/__init__.py +1 -0
- app/modules/reports/api.py +134 -0
- app/modules/reports/cache.py +74 -0
- app/modules/reports/filters.py +27 -0
- app/modules/reports/repository.py +525 -0
- app/modules/reports/rollup.py +150 -0
- app/modules/reports/rollup_read.py +148 -0
- app/modules/reports/schemas.py +123 -0
- app/modules/reports/service.py +287 -0
- app/modules/reports/thread_identity.py +256 -0
- app/modules/request_logs/__init__.py +0 -0
- app/modules/request_logs/api.py +224 -0
- app/modules/request_logs/cost_backfill.py +154 -0
- app/modules/request_logs/mappers.py +97 -0
- app/modules/request_logs/repository.py +1765 -0
- app/modules/request_logs/schemas.py +145 -0
- app/modules/request_logs/service.py +348 -0
- app/modules/role_mappings/__init__.py +0 -0
- app/modules/role_mappings/api.py +206 -0
- app/modules/role_mappings/matching.py +55 -0
- app/modules/role_mappings/repository.py +106 -0
- app/modules/role_mappings/schemas.py +67 -0
- app/modules/role_mappings/service.py +334 -0
- app/modules/runtime/__init__.py +1 -0
- app/modules/runtime/api.py +18 -0
- app/modules/runtime/schemas.py +14 -0
- app/modules/runtime/service.py +218 -0
- app/modules/scim/__init__.py +1 -0
- app/modules/scim/api.py +194 -0
- app/modules/scim/dependencies.py +112 -0
- app/modules/scim/errors.py +77 -0
- app/modules/scim/management_api.py +156 -0
- app/modules/scim/repository.py +212 -0
- app/modules/scim/schemas.py +145 -0
- app/modules/scim/service.py +786 -0
- app/modules/scim/tokens.py +43 -0
- app/modules/settings/__init__.py +1 -0
- app/modules/settings/api.py +1677 -0
- app/modules/settings/repository.py +606 -0
- app/modules/settings/schemas.py +496 -0
- app/modules/settings/service.py +769 -0
- app/modules/shared/__init__.py +0 -0
- app/modules/shared/schemas.py +22 -0
- app/modules/sticky_sessions/__init__.py +1 -0
- app/modules/sticky_sessions/api.py +118 -0
- app/modules/sticky_sessions/cleanup_scheduler.py +487 -0
- app/modules/sticky_sessions/schemas.py +78 -0
- app/modules/sticky_sessions/service.py +196 -0
- app/modules/telemetry/__init__.py +5 -0
- app/modules/telemetry/api.py +115 -0
- app/modules/telemetry/clients.py +62 -0
- app/modules/telemetry/consent.py +115 -0
- app/modules/telemetry/scheduler.py +85 -0
- app/modules/telemetry/schemas.py +163 -0
- app/modules/telemetry/sender.py +221 -0
- app/modules/telemetry/snapshot.py +490 -0
- app/modules/usage/__init__.py +0 -0
- app/modules/usage/additional_quota_keys.py +379 -0
- app/modules/usage/api.py +45 -0
- app/modules/usage/background_repository.py +147 -0
- app/modules/usage/builders.py +559 -0
- app/modules/usage/depletion_service.py +277 -0
- app/modules/usage/live_ingest.py +356 -0
- app/modules/usage/mappers.py +29 -0
- app/modules/usage/plan_downgrade_observations.py +488 -0
- app/modules/usage/repository.py +1890 -0
- app/modules/usage/schemas.py +68 -0
- app/modules/usage/service.py +102 -0
- app/modules/usage/updater.py +1488 -0
- app/static/assets/Trans-Cj-J9mVA.js +1 -0
- app/static/assets/access-page-DHzZiXZi.js +1 -0
- app/static/assets/account-cost-donut-DGhfA4U8.js +1 -0
- app/static/assets/account-identifiers-CxbdLshZ.js +1 -0
- app/static/assets/account-status-DIsU5sUS.js +1 -0
- app/static/assets/account-trend-chart-BptlfwwQ.js +1 -0
- app/static/assets/accounts-page-Bbai8ift.js +3 -0
- app/static/assets/api-Dwa7XxPe.js +1 -0
- app/static/assets/api-client-dugX1Pti.js +55 -0
- app/static/assets/api-key-create-dialog-DnRm0oYN.js +1 -0
- app/static/assets/api-key-created-dialog-B6jkDG9f.js +1 -0
- app/static/assets/api-key-edit-dialog-Ck3Bnc3V.js +1 -0
- app/static/assets/api-trend-chart-BchCw7Hj.js +1 -0
- app/static/assets/apis-page-BvXz9c6i.js +2 -0
- app/static/assets/arrow-up-Dqh_KzJo.js +1 -0
- app/static/assets/arrow-up-down-LBRgVDGY.js +1 -0
- app/static/assets/automations-page-DQgNAJOR.js +3 -0
- app/static/assets/badge-DqMb9es0.js +1 -0
- app/static/assets/blur-email-HEvKagG2.js +1 -0
- app/static/assets/button-BjVmmiGW.js +1 -0
- app/static/assets/checkbox-apsl9vzk.js +1 -0
- app/static/assets/chevron-left-1d_pHVa5.js +1 -0
- app/static/assets/circle-alert-BS0xd_o1.js +1 -0
- app/static/assets/clsx-DB0hHKMi.js +1 -0
- app/static/assets/colors-BBy8GEtr.js +1 -0
- app/static/assets/copy-button-D7nAvOKR.js +1 -0
- app/static/assets/cost-per-day-chart-YdqlkZVM.js +1 -0
- app/static/assets/createLucideIcon-DBLzLNh2.js +1 -0
- app/static/assets/dashboard-page-CSdhBy0f.js +2 -0
- app/static/assets/distribution-metric-format-yZTy1ZEu.js +1 -0
- app/static/assets/donut-chart-Bxc69cra.js +1 -0
- app/static/assets/download-kQteIcsB.js +1 -0
- app/static/assets/ellipsis-B_5OgIre.js +1 -0
- app/static/assets/empty-state-JcA7tc8m.js +1 -0
- app/static/assets/es2015-bMH8YKZT.js +41 -0
- app/static/assets/es6-B264ni6W.js +69 -0
- app/static/assets/external-link-UVAOgMQ0.js +1 -0
- app/static/assets/i18n-BWqZ9Ua2.js +1 -0
- app/static/assets/inbox-B_RM2s1J.js +1 -0
- app/static/assets/index-DIpd264Z.js +23 -0
- app/static/assets/index-DMeLKRJx.css +2 -0
- app/static/assets/inherit-badge-QhTYVaU6.js +1 -0
- app/static/assets/invite-dialog-0CYh0GiQ.js +1 -0
- app/static/assets/iso-Bpe5Ihbb.js +1 -0
- app/static/assets/label-CGaLmLNf.js +1 -0
- app/static/assets/lazy-recharts-Pv8p9ArM.js +2 -0
- app/static/assets/loading-overlay-CzKQfHLI.js +1 -0
- app/static/assets/mini-quota-bar-cGT_t8Ae.js +1 -0
- app/static/assets/model-distribution-donut-BKjAfT48.js +1 -0
- app/static/assets/model-source-multi-select-D2d06Vc7.js +1 -0
- app/static/assets/multi-select-filter-KIA5qU6Z.js +1 -0
- app/static/assets/oauth-dialog-XeaGGDjS.js +1 -0
- app/static/assets/pagination-controls-D1Ypd0RU.js +1 -0
- app/static/assets/payload-CIYnGZz9.js +1 -0
- app/static/assets/queue-wait-chart-VoV7Yg9L.js +1 -0
- app/static/assets/report-chart-card-BXDliwcc.js +1 -0
- app/static/assets/reports-page-CGCJeMHG.js +3 -0
- app/static/assets/reset-credit-confirm-dialog-QOifTnVB.js +1 -0
- app/static/assets/rolldown-runtime-hePW80VL.js +1 -0
- app/static/assets/search-HfhqwERt.js +1 -0
- app/static/assets/select-Cl33bz-g.js +1 -0
- app/static/assets/settings-page-mZj01HYl.js +2 -0
- app/static/assets/shim-CZwyM4Q7.js +1 -0
- app/static/assets/skeleton-C_EUc4Rp.js +1 -0
- app/static/assets/sparkline-chart-DdNNsddM.js +1 -0
- app/static/assets/status-badge-CHHA5X6c.js +1 -0
- app/static/assets/switch-CzxgokcM.js +1 -0
- app/static/assets/table-DXaNSQ0G.js +1 -0
- app/static/assets/time-to-first-token-chart--68L0u_v.js +1 -0
- app/static/assets/tokens-per-day-chart-Cs82BFuE.js +1 -0
- app/static/assets/tokens-per-second-chart-AD2dfOn6.js +1 -0
- app/static/assets/totp-settings-CRHtxQ7j.js +1 -0
- app/static/assets/trash-D3KyWr1h.js +1 -0
- app/static/assets/use-account-quota-display-BPmrTBTC.js +1 -0
- app/static/assets/use-accounts-PWlVGw3z.js +1 -0
- app/static/assets/use-auth-CuDlW3iC.js +1 -0
- app/static/assets/use-chart-colors-DIu6tClI.js +1 -0
- app/static/assets/use-model-sources-Bul24JBu.js +1 -0
- app/static/assets/use-models-DUCPUuZg.js +1 -0
- app/static/assets/use-reduced-motion-hAm5r6_d.js +1 -0
- app/static/assets/useTranslation--56nHhlH.js +1 -0
- app/static/assets/useragent-distribution-donut-Dup6sf_n.js +1 -0
- app/static/assets/users-uIC3TIlE.js +1 -0
- app/static/assets/vendor-query-_bHZuxEx.js +1 -0
- app/static/assets/vendor-react-BVL0YZPW.js +9 -0
- app/static/favicon.svg +10 -0
- app/static/fonts/GeistSans-Variable.woff2 +0 -0
- app/static/fonts/jetbrains-mono-latin-ext.woff2 +0 -0
- app/static/fonts/jetbrains-mono-latin.woff2 +0 -0
- app/static/index.html +29 -0
- codexlb_up-1.25.0b10.dist-info/METADATA +124 -0
- codexlb_up-1.25.0b10.dist-info/RECORD +827 -0
- codexlb_up-1.25.0b10.dist-info/WHEEL +4 -0
- codexlb_up-1.25.0b10.dist-info/entry_points.txt +3 -0
- codexlb_up-1.25.0b10.dist-info/licenses/LICENSE +21 -0
- config/additional_quota_registry.json +11 -0
|
@@ -0,0 +1,1054 @@
|
|
|
1
|
+
"""OpenAI Images API translation layer.
|
|
2
|
+
|
|
3
|
+
This module turns ``POST /v1/images/generations`` and ``POST /v1/images/edits``
|
|
4
|
+
requests into internal ``/v1/responses`` requests with the built-in
|
|
5
|
+
``image_generation`` tool, then folds the upstream Responses output (or SSE
|
|
6
|
+
event stream) back into the OpenAI Images response shape.
|
|
7
|
+
|
|
8
|
+
The intent is to keep all auth/account/sticky/usage logic in
|
|
9
|
+
``ProxyService.stream_responses`` (and friends) and only do data-shape
|
|
10
|
+
translation here.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import base64
|
|
16
|
+
import binascii
|
|
17
|
+
import logging
|
|
18
|
+
import re
|
|
19
|
+
import time
|
|
20
|
+
from collections.abc import AsyncIterator, Mapping
|
|
21
|
+
from typing import Final, cast
|
|
22
|
+
|
|
23
|
+
from app.core.errors import OpenAIErrorEnvelope, openai_error
|
|
24
|
+
from app.core.openai.exceptions import ClientPayloadError
|
|
25
|
+
from app.core.openai.images import (
|
|
26
|
+
DEFAULT_PUBLIC_IMAGE_MODEL,
|
|
27
|
+
V1ImageData,
|
|
28
|
+
V1ImageResponse,
|
|
29
|
+
V1ImagesEditsForm,
|
|
30
|
+
V1ImagesGenerationsRequest,
|
|
31
|
+
V1ImageUsage,
|
|
32
|
+
is_supported_image_model,
|
|
33
|
+
validate_image_request_parameters,
|
|
34
|
+
)
|
|
35
|
+
from app.core.openai.requests import ResponsesRequest
|
|
36
|
+
from app.core.types import JsonValue
|
|
37
|
+
from app.core.utils.json_guards import is_json_mapping
|
|
38
|
+
from app.core.utils.sse import format_sse_event, parse_sse_data_json
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
#: Compact instruction used to deterministically force exactly one
|
|
43
|
+
#: ``image_generation`` tool call from the host Responses model. The string
|
|
44
|
+
#: is intentionally short and self-contained to keep history-cost minimal.
|
|
45
|
+
_IMAGE_GENERATION_INSTRUCTIONS: Final[str] = (
|
|
46
|
+
"You are an image generator. When asked, you MUST call the image_generation "
|
|
47
|
+
"tool exactly once and return only that tool call. Do not produce any "
|
|
48
|
+
"additional text output. Mirror the user's request verbatim into the tool's "
|
|
49
|
+
"prompt argument."
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
#: Instruction tail appended to edit prompts so the host model knows that any
|
|
53
|
+
#: trailing input_image acts as a mask (since OpenAI's Images Edits API has a
|
|
54
|
+
#: distinct ``mask`` slot but the Responses image_generation tool does not).
|
|
55
|
+
_IMAGE_EDIT_MASK_HINT: Final[str] = (
|
|
56
|
+
"\n\n(The final attached image is a transparent mask: only modify the regions where the mask is non-transparent.)"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
#: SSE event types we *consume* from the upstream Responses stream.
|
|
60
|
+
_UPSTREAM_PARTIAL_IMAGE_EVENT: Final[str] = "response.image_generation_call.partial_image"
|
|
61
|
+
_UPSTREAM_OUTPUT_ITEM_DONE_EVENT: Final[str] = "response.output_item.done"
|
|
62
|
+
_UPSTREAM_RESPONSE_COMPLETED_EVENT: Final[str] = "response.completed"
|
|
63
|
+
_UPSTREAM_RESPONSE_FAILED_EVENT: Final[str] = "response.failed"
|
|
64
|
+
_UPSTREAM_RESPONSE_INCOMPLETE_EVENT: Final[str] = "response.incomplete"
|
|
65
|
+
_UPSTREAM_ERROR_EVENT: Final[str] = "error"
|
|
66
|
+
|
|
67
|
+
#: OpenAI Images SSE event names we *emit* to the client.
|
|
68
|
+
# Downstream OpenAI Images SSE event names. ``/v1/images/generations``
|
|
69
|
+
# emits ``image_generation.*`` and ``/v1/images/edits`` emits
|
|
70
|
+
# ``image_edit.*`` to match the canonical OpenAI Images streaming
|
|
71
|
+
# vocabulary; the route handler picks the prefix when invoking the
|
|
72
|
+
# translator.
|
|
73
|
+
_GENERATION_PARTIAL_EVENT: Final[str] = "image_generation.partial_image"
|
|
74
|
+
_GENERATION_COMPLETED_EVENT: Final[str] = "image_generation.completed"
|
|
75
|
+
_EDIT_PARTIAL_EVENT: Final[str] = "image_edit.partial_image"
|
|
76
|
+
_EDIT_COMPLETED_EVENT: Final[str] = "image_edit.completed"
|
|
77
|
+
_DOWNSTREAM_ERROR_EVENT: Final[str] = "error"
|
|
78
|
+
|
|
79
|
+
_DATA_URL_PATTERN: Final[re.Pattern[str]] = re.compile(
|
|
80
|
+
r"^data:(?P<mime>[^;]+);base64,(?P<b64>.+)$",
|
|
81
|
+
re.DOTALL,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
# Request translation
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _build_image_generation_tool(
|
|
91
|
+
*,
|
|
92
|
+
model: str,
|
|
93
|
+
n: int,
|
|
94
|
+
size: str,
|
|
95
|
+
quality: str,
|
|
96
|
+
background: str,
|
|
97
|
+
output_format: str,
|
|
98
|
+
output_compression: int,
|
|
99
|
+
moderation: str,
|
|
100
|
+
partial_images: int | None,
|
|
101
|
+
input_fidelity: str | None,
|
|
102
|
+
streaming: bool,
|
|
103
|
+
is_edit: bool = False,
|
|
104
|
+
) -> dict[str, JsonValue]:
|
|
105
|
+
# NOTE: the upstream ``image_generation`` tool config does not accept
|
|
106
|
+
# ``n``. ``validate_image_request_parameters`` unconditionally
|
|
107
|
+
# rejects ``n > 1`` because client-side fan-out is not implemented
|
|
108
|
+
# yet, so this function is only ever called with ``n == 1``. The
|
|
109
|
+
# assert below catches a future regression where the API-boundary
|
|
110
|
+
# cap is loosened without also adding fan-out, instead of silently
|
|
111
|
+
# dropping the requested count.
|
|
112
|
+
assert n == 1, "image_generation tool does not accept n; fan-out is not implemented"
|
|
113
|
+
del n # rejected upstream of this call (fan-out not yet implemented)
|
|
114
|
+
tool: dict[str, JsonValue] = {
|
|
115
|
+
"type": "image_generation",
|
|
116
|
+
"model": model,
|
|
117
|
+
"size": size,
|
|
118
|
+
"quality": quality,
|
|
119
|
+
"background": background,
|
|
120
|
+
"output_format": output_format,
|
|
121
|
+
"output_compression": output_compression,
|
|
122
|
+
"moderation": moderation,
|
|
123
|
+
}
|
|
124
|
+
if is_edit:
|
|
125
|
+
# Force the edit code path so the host model treats the attached
|
|
126
|
+
# input_image(s) as a source/mask pair instead of inspiration for
|
|
127
|
+
# a fresh generation. Without this the default "auto" action lets
|
|
128
|
+
# the model decide between generation and editing, which can
|
|
129
|
+
# silently break the edits contract.
|
|
130
|
+
tool["action"] = "edit"
|
|
131
|
+
if input_fidelity is not None:
|
|
132
|
+
tool["input_fidelity"] = input_fidelity
|
|
133
|
+
if streaming and partial_images is not None and partial_images > 0:
|
|
134
|
+
tool["partial_images"] = partial_images
|
|
135
|
+
return tool
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _build_user_message_input(
|
|
139
|
+
prompt: str, *, attached_images: list[dict[str, JsonValue]] | None = None
|
|
140
|
+
) -> list[JsonValue]:
|
|
141
|
+
content: list[JsonValue] = [{"type": "input_text", "text": prompt}]
|
|
142
|
+
if attached_images:
|
|
143
|
+
content.extend(attached_images)
|
|
144
|
+
return [
|
|
145
|
+
{
|
|
146
|
+
"type": "message",
|
|
147
|
+
"role": "user",
|
|
148
|
+
"content": content,
|
|
149
|
+
}
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _build_input_image_part(image_bytes: bytes, *, mime_type: str | None) -> dict[str, JsonValue]:
|
|
154
|
+
"""Build a Responses ``input_image`` content part as a base64 data URL."""
|
|
155
|
+
resolved_mime = (mime_type or "image/png").strip() or "image/png"
|
|
156
|
+
encoded = base64.b64encode(image_bytes).decode("ascii")
|
|
157
|
+
return {
|
|
158
|
+
"type": "input_image",
|
|
159
|
+
"image_url": f"data:{resolved_mime};base64,{encoded}",
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def images_generation_to_responses_request(
|
|
164
|
+
payload: V1ImagesGenerationsRequest,
|
|
165
|
+
*,
|
|
166
|
+
host_model: str,
|
|
167
|
+
) -> ResponsesRequest:
|
|
168
|
+
"""Translate a ``/v1/images/generations`` request into a Responses request.
|
|
169
|
+
|
|
170
|
+
The upstream Responses backend rejects non-streaming requests that include
|
|
171
|
+
the ``image_generation`` tool (the partial-image and final ``result``
|
|
172
|
+
payloads are only delivered through SSE). We therefore always force
|
|
173
|
+
``stream=True`` on the internal request and let the caller drain the
|
|
174
|
+
upstream stream into a JSON envelope when the public client did not
|
|
175
|
+
request streaming.
|
|
176
|
+
"""
|
|
177
|
+
streaming = bool(payload.stream)
|
|
178
|
+
# ``validate_generations_payload`` resolves ``payload.model`` to a
|
|
179
|
+
# concrete ``gpt-image-*`` value before this is ever called.
|
|
180
|
+
assert payload.model is not None, "payload.model must be resolved before translation"
|
|
181
|
+
tool = _build_image_generation_tool(
|
|
182
|
+
model=payload.model,
|
|
183
|
+
n=payload.n,
|
|
184
|
+
size=payload.size,
|
|
185
|
+
quality=payload.quality,
|
|
186
|
+
background=payload.background,
|
|
187
|
+
output_format=payload.output_format,
|
|
188
|
+
output_compression=payload.output_compression,
|
|
189
|
+
moderation=payload.moderation,
|
|
190
|
+
partial_images=payload.partial_images,
|
|
191
|
+
input_fidelity=None,
|
|
192
|
+
streaming=streaming,
|
|
193
|
+
)
|
|
194
|
+
return ResponsesRequest.model_validate(
|
|
195
|
+
{
|
|
196
|
+
"model": host_model,
|
|
197
|
+
"instructions": _IMAGE_GENERATION_INSTRUCTIONS,
|
|
198
|
+
"input": _build_user_message_input(payload.prompt),
|
|
199
|
+
"tools": [tool],
|
|
200
|
+
# Force the host model to invoke the image_generation tool
|
|
201
|
+
# so it cannot return a refusal or plain text. Without this
|
|
202
|
+
# the auto choice would surface as a 5xx through this
|
|
203
|
+
# adapter even though the request shape was valid.
|
|
204
|
+
"tool_choice": {"type": "image_generation"},
|
|
205
|
+
"stream": True,
|
|
206
|
+
"store": False,
|
|
207
|
+
}
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def images_edit_to_responses_request(
|
|
212
|
+
payload: V1ImagesEditsForm,
|
|
213
|
+
*,
|
|
214
|
+
host_model: str,
|
|
215
|
+
images: list[tuple[bytes, str | None]],
|
|
216
|
+
mask: tuple[bytes, str | None] | None,
|
|
217
|
+
) -> ResponsesRequest:
|
|
218
|
+
"""Translate a ``/v1/images/edits`` request into a Responses request.
|
|
219
|
+
|
|
220
|
+
``images`` is a non-empty list of ``(bytes, content_type)`` tuples
|
|
221
|
+
representing the multipart ``image`` parts. ``mask`` is the optional
|
|
222
|
+
``mask`` part with the same shape; when provided, it is appended after
|
|
223
|
+
the source images and the prompt is amended with a deterministic hint
|
|
224
|
+
so the host model treats it correctly.
|
|
225
|
+
"""
|
|
226
|
+
if not images:
|
|
227
|
+
# Caller is expected to validate this beforehand, but guard so we
|
|
228
|
+
# never silently produce an image-less Responses request.
|
|
229
|
+
raise ValueError("/v1/images/edits requires at least one image part")
|
|
230
|
+
|
|
231
|
+
streaming = bool(payload.stream)
|
|
232
|
+
attached: list[dict[str, JsonValue]] = []
|
|
233
|
+
for image_bytes, mime_type in images:
|
|
234
|
+
attached.append(_build_input_image_part(image_bytes, mime_type=mime_type))
|
|
235
|
+
if mask is not None:
|
|
236
|
+
mask_bytes, mask_mime = mask
|
|
237
|
+
attached.append(_build_input_image_part(mask_bytes, mime_type=mask_mime))
|
|
238
|
+
|
|
239
|
+
prompt_text = payload.prompt
|
|
240
|
+
if mask is not None:
|
|
241
|
+
prompt_text = f"{prompt_text}{_IMAGE_EDIT_MASK_HINT}"
|
|
242
|
+
|
|
243
|
+
# ``validate_edits_payload`` resolves ``payload.model`` to a concrete
|
|
244
|
+
# ``gpt-image-*`` value before this is ever called.
|
|
245
|
+
assert payload.model is not None, "payload.model must be resolved before translation"
|
|
246
|
+
tool = _build_image_generation_tool(
|
|
247
|
+
model=payload.model,
|
|
248
|
+
n=payload.n,
|
|
249
|
+
size=payload.size,
|
|
250
|
+
quality=payload.quality,
|
|
251
|
+
background=payload.background,
|
|
252
|
+
output_format=payload.output_format,
|
|
253
|
+
output_compression=payload.output_compression,
|
|
254
|
+
moderation=payload.moderation,
|
|
255
|
+
partial_images=payload.partial_images,
|
|
256
|
+
input_fidelity=payload.input_fidelity,
|
|
257
|
+
streaming=streaming,
|
|
258
|
+
is_edit=True,
|
|
259
|
+
)
|
|
260
|
+
return ResponsesRequest.model_validate(
|
|
261
|
+
{
|
|
262
|
+
"model": host_model,
|
|
263
|
+
"instructions": _IMAGE_GENERATION_INSTRUCTIONS,
|
|
264
|
+
"input": _build_user_message_input(prompt_text, attached_images=attached),
|
|
265
|
+
"tools": [tool],
|
|
266
|
+
# Force the host model to invoke the image_generation tool.
|
|
267
|
+
# Leaving this on "auto" lets the model return a refusal or
|
|
268
|
+
# plain text instead, which would surface as a 5xx through
|
|
269
|
+
# this adapter even though the request shape was valid. See
|
|
270
|
+
# the matching forced tool call in
|
|
271
|
+
# ``images_generation_to_responses_request``.
|
|
272
|
+
"tool_choice": {"type": "image_generation"},
|
|
273
|
+
# See ``images_generation_to_responses_request`` for why this is
|
|
274
|
+
# always True regardless of what the public client requested.
|
|
275
|
+
"stream": True,
|
|
276
|
+
"store": False,
|
|
277
|
+
}
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
# ---------------------------------------------------------------------------
|
|
282
|
+
# Public-request validation helpers wired by the route handlers.
|
|
283
|
+
# ---------------------------------------------------------------------------
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def resolve_public_image_model(requested: str | None) -> str:
|
|
287
|
+
"""Return the publicly-effective ``gpt-image-*`` model.
|
|
288
|
+
|
|
289
|
+
Falls back to the fixed ``DEFAULT_PUBLIC_IMAGE_MODEL`` when the client
|
|
290
|
+
omits ``model``. The returned value is always validated against the
|
|
291
|
+
``gpt-image-*`` allowlist.
|
|
292
|
+
"""
|
|
293
|
+
resolved = requested or DEFAULT_PUBLIC_IMAGE_MODEL
|
|
294
|
+
if not is_supported_image_model(resolved):
|
|
295
|
+
raise ClientPayloadError(
|
|
296
|
+
f"Unsupported image model '{resolved}'. Use a 'gpt-image-*' model.",
|
|
297
|
+
param="model",
|
|
298
|
+
code="invalid_request_error",
|
|
299
|
+
error_type="invalid_request_error",
|
|
300
|
+
)
|
|
301
|
+
return resolved
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def validate_generations_payload(payload: V1ImagesGenerationsRequest) -> V1ImagesGenerationsRequest:
|
|
305
|
+
"""Apply the cross-field validation matrix and return the payload with
|
|
306
|
+
``model`` populated to the configured default when the client omitted it.
|
|
307
|
+
"""
|
|
308
|
+
resolved_model = resolve_public_image_model(payload.model)
|
|
309
|
+
# Forward ``payload.input_fidelity`` so the validator rejects it on the
|
|
310
|
+
# generations path (it is an edit-only parameter). Without this the
|
|
311
|
+
# field would be silently dropped via the schema's ``extra=ignore``
|
|
312
|
+
# and an invalid request would 200 instead of 400.
|
|
313
|
+
validate_image_request_parameters(
|
|
314
|
+
model=resolved_model,
|
|
315
|
+
quality=payload.quality,
|
|
316
|
+
size=payload.size,
|
|
317
|
+
background=payload.background,
|
|
318
|
+
output_format=payload.output_format,
|
|
319
|
+
moderation=payload.moderation,
|
|
320
|
+
input_fidelity=payload.input_fidelity,
|
|
321
|
+
is_edit=False,
|
|
322
|
+
n=payload.n,
|
|
323
|
+
partial_images=payload.partial_images,
|
|
324
|
+
output_compression=payload.output_compression,
|
|
325
|
+
)
|
|
326
|
+
if payload.model != resolved_model:
|
|
327
|
+
# Pydantic models are immutable by default; build a copy with the
|
|
328
|
+
# resolved model so downstream code can rely on ``payload.model``
|
|
329
|
+
# always being a concrete ``gpt-image-*`` value.
|
|
330
|
+
return payload.model_copy(update={"model": resolved_model})
|
|
331
|
+
return payload
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def validate_edits_payload(payload: V1ImagesEditsForm) -> V1ImagesEditsForm:
|
|
335
|
+
"""Apply the cross-field validation matrix and return the payload with
|
|
336
|
+
``model`` populated to the configured default when the client omitted it.
|
|
337
|
+
"""
|
|
338
|
+
resolved_model = resolve_public_image_model(payload.model)
|
|
339
|
+
validate_image_request_parameters(
|
|
340
|
+
model=resolved_model,
|
|
341
|
+
quality=payload.quality,
|
|
342
|
+
size=payload.size,
|
|
343
|
+
background=payload.background,
|
|
344
|
+
output_format=payload.output_format,
|
|
345
|
+
moderation=payload.moderation,
|
|
346
|
+
input_fidelity=payload.input_fidelity,
|
|
347
|
+
is_edit=True,
|
|
348
|
+
n=payload.n,
|
|
349
|
+
partial_images=payload.partial_images,
|
|
350
|
+
output_compression=payload.output_compression,
|
|
351
|
+
)
|
|
352
|
+
if payload.model != resolved_model:
|
|
353
|
+
return payload.model_copy(update={"model": resolved_model})
|
|
354
|
+
return payload
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
# ---------------------------------------------------------------------------
|
|
358
|
+
# Non-streaming response translation
|
|
359
|
+
# ---------------------------------------------------------------------------
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _select_image_items(output: list[JsonValue]) -> list[Mapping[str, JsonValue]]:
|
|
363
|
+
items: list[Mapping[str, JsonValue]] = []
|
|
364
|
+
for entry in output:
|
|
365
|
+
if not is_json_mapping(entry):
|
|
366
|
+
continue
|
|
367
|
+
if entry.get("type") == "image_generation_call":
|
|
368
|
+
items.append(entry)
|
|
369
|
+
return items
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _coerce_int(value: JsonValue | None) -> int | None:
|
|
373
|
+
if isinstance(value, bool):
|
|
374
|
+
return None
|
|
375
|
+
if isinstance(value, int):
|
|
376
|
+
return value
|
|
377
|
+
if isinstance(value, float):
|
|
378
|
+
return int(value)
|
|
379
|
+
return None
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
_USAGE_TOKEN_FIELDS: Final[frozenset[str]] = frozenset({"input_tokens", "output_tokens", "total_tokens"})
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _extract_image_usage(response: Mapping[str, JsonValue]) -> V1ImageUsage | None:
|
|
386
|
+
tool_usage = response.get("tool_usage")
|
|
387
|
+
if not is_json_mapping(tool_usage):
|
|
388
|
+
return None
|
|
389
|
+
image_usage = tool_usage.get("image_gen")
|
|
390
|
+
if not is_json_mapping(image_usage):
|
|
391
|
+
return None
|
|
392
|
+
input_tokens = _coerce_int(image_usage.get("input_tokens"))
|
|
393
|
+
output_tokens = _coerce_int(image_usage.get("output_tokens"))
|
|
394
|
+
total_tokens = _coerce_int(image_usage.get("total_tokens"))
|
|
395
|
+
if total_tokens is None and input_tokens is not None and output_tokens is not None:
|
|
396
|
+
total_tokens = input_tokens + output_tokens
|
|
397
|
+
input_details_raw = image_usage.get("input_tokens_details")
|
|
398
|
+
output_details_raw = image_usage.get("output_tokens_details")
|
|
399
|
+
input_details = dict(input_details_raw) if is_json_mapping(input_details_raw) else None
|
|
400
|
+
output_details = dict(output_details_raw) if is_json_mapping(output_details_raw) else None
|
|
401
|
+
# Forward any other usage detail keys upstream may add (e.g. cached
|
|
402
|
+
# token counters) so the public response keeps the OpenAI Images
|
|
403
|
+
# usage shape rather than silently dropping new fields.
|
|
404
|
+
extra_usage: dict[str, JsonValue] = {}
|
|
405
|
+
for key, value in image_usage.items():
|
|
406
|
+
if key in _USAGE_TOKEN_FIELDS:
|
|
407
|
+
continue
|
|
408
|
+
if key in ("input_tokens_details", "output_tokens_details"):
|
|
409
|
+
continue
|
|
410
|
+
extra_usage[key] = value
|
|
411
|
+
if (
|
|
412
|
+
input_tokens is None
|
|
413
|
+
and output_tokens is None
|
|
414
|
+
and total_tokens is None
|
|
415
|
+
and input_details is None
|
|
416
|
+
and output_details is None
|
|
417
|
+
and not extra_usage
|
|
418
|
+
):
|
|
419
|
+
return None
|
|
420
|
+
return V1ImageUsage(
|
|
421
|
+
input_tokens=input_tokens,
|
|
422
|
+
output_tokens=output_tokens,
|
|
423
|
+
total_tokens=total_tokens,
|
|
424
|
+
input_tokens_details=input_details,
|
|
425
|
+
output_tokens_details=output_details,
|
|
426
|
+
**extra_usage,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def images_response_from_responses(response: Mapping[str, JsonValue]) -> V1ImageResponse | OpenAIErrorEnvelope:
|
|
431
|
+
"""Build the public Images response from a completed Responses payload.
|
|
432
|
+
|
|
433
|
+
Returns an :class:`OpenAIErrorEnvelope` (TypedDict) when the upstream
|
|
434
|
+
response indicates the image generation failed; otherwise returns a
|
|
435
|
+
:class:`V1ImageResponse`.
|
|
436
|
+
"""
|
|
437
|
+
output_value = response.get("output")
|
|
438
|
+
if not isinstance(output_value, list):
|
|
439
|
+
return openai_error(
|
|
440
|
+
"image_generation_failed",
|
|
441
|
+
"Upstream response did not include an output array",
|
|
442
|
+
error_type="server_error",
|
|
443
|
+
)
|
|
444
|
+
items = _select_image_items(cast(list[JsonValue], output_value))
|
|
445
|
+
if not items:
|
|
446
|
+
return openai_error(
|
|
447
|
+
"image_generation_failed",
|
|
448
|
+
"Upstream response did not include any image_generation_call items",
|
|
449
|
+
error_type="server_error",
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
# Surface the first failed image_generation_call as an error envelope.
|
|
453
|
+
for item in items:
|
|
454
|
+
status = item.get("status")
|
|
455
|
+
if isinstance(status, str) and status == "failed":
|
|
456
|
+
error = item.get("error")
|
|
457
|
+
if is_json_mapping(error):
|
|
458
|
+
message = error.get("message")
|
|
459
|
+
code = error.get("code")
|
|
460
|
+
error_type = error.get("type")
|
|
461
|
+
return openai_error(
|
|
462
|
+
code if isinstance(code, str) and code else "image_generation_failed",
|
|
463
|
+
message if isinstance(message, str) and message else "Image generation failed",
|
|
464
|
+
error_type=error_type if isinstance(error_type, str) and error_type else "server_error",
|
|
465
|
+
)
|
|
466
|
+
return openai_error(
|
|
467
|
+
"image_generation_failed",
|
|
468
|
+
"Upstream image_generation_call reported status=failed",
|
|
469
|
+
error_type="server_error",
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
data_entries: list[V1ImageData] = []
|
|
473
|
+
for item in items:
|
|
474
|
+
result = item.get("result")
|
|
475
|
+
if not isinstance(result, str) or not result:
|
|
476
|
+
continue
|
|
477
|
+
revised_prompt = item.get("revised_prompt")
|
|
478
|
+
data_entries.append(
|
|
479
|
+
V1ImageData(
|
|
480
|
+
b64_json=result,
|
|
481
|
+
revised_prompt=revised_prompt if isinstance(revised_prompt, str) and revised_prompt else None,
|
|
482
|
+
)
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
if not data_entries:
|
|
486
|
+
return openai_error(
|
|
487
|
+
"image_generation_failed",
|
|
488
|
+
"Upstream image_generation_call items contained no image data",
|
|
489
|
+
error_type="server_error",
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
usage = _extract_image_usage(response)
|
|
493
|
+
return V1ImageResponse(
|
|
494
|
+
created=int(time.time()),
|
|
495
|
+
data=data_entries,
|
|
496
|
+
usage=usage,
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
# ---------------------------------------------------------------------------
|
|
501
|
+
# Streaming translation
|
|
502
|
+
# ---------------------------------------------------------------------------
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _stash_image_usage_tokens(captured: dict[str, object], usage: V1ImageUsage) -> None:
|
|
506
|
+
"""Persist the ``tool_usage.image_gen`` token counts on ``captured``
|
|
507
|
+
so the route handler can post-hoc record them against the API key.
|
|
508
|
+
|
|
509
|
+
Stored under explicit ``image_input_tokens`` / ``image_output_tokens``
|
|
510
|
+
/ ``image_cached_input_tokens`` keys (alongside the existing
|
|
511
|
+
``response_id``) so the route handler does not have to re-parse the
|
|
512
|
+
SSE stream or know the V1ImageUsage shape. Cached tokens are pulled
|
|
513
|
+
from ``input_tokens_details.cached_tokens`` when upstream reports
|
|
514
|
+
them so cached requests are not billed as fully uncached input.
|
|
515
|
+
"""
|
|
516
|
+
if usage.input_tokens is not None:
|
|
517
|
+
captured["image_input_tokens"] = int(usage.input_tokens)
|
|
518
|
+
if usage.output_tokens is not None:
|
|
519
|
+
captured["image_output_tokens"] = int(usage.output_tokens)
|
|
520
|
+
cached = _extract_cached_input_tokens(usage)
|
|
521
|
+
if cached is not None:
|
|
522
|
+
captured["image_cached_input_tokens"] = cached
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _extract_cached_input_tokens(usage: V1ImageUsage) -> int | None:
|
|
526
|
+
details = usage.input_tokens_details
|
|
527
|
+
if not isinstance(details, Mapping):
|
|
528
|
+
return None
|
|
529
|
+
raw = details.get("cached_tokens")
|
|
530
|
+
if isinstance(raw, bool):
|
|
531
|
+
return None
|
|
532
|
+
if isinstance(raw, int):
|
|
533
|
+
return raw
|
|
534
|
+
if isinstance(raw, float):
|
|
535
|
+
return int(raw)
|
|
536
|
+
return None
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _build_partial_image_event(payload: Mapping[str, JsonValue], *, event_type: str) -> dict[str, JsonValue] | None:
|
|
540
|
+
partial_b64 = payload.get("partial_image_b64")
|
|
541
|
+
if not isinstance(partial_b64, str) or not partial_b64:
|
|
542
|
+
return None
|
|
543
|
+
event: dict[str, JsonValue] = {
|
|
544
|
+
"type": event_type,
|
|
545
|
+
"b64_json": partial_b64,
|
|
546
|
+
# OpenAI Images stream event schemas expose ``created_at`` on
|
|
547
|
+
# both partial and completed events. Forward the upstream value
|
|
548
|
+
# if present, otherwise stamp a synthesized one so consumers
|
|
549
|
+
# that deserialize against the official model do not reject the
|
|
550
|
+
# event for a missing field.
|
|
551
|
+
"created_at": _coerce_created_at(payload.get("created_at")),
|
|
552
|
+
}
|
|
553
|
+
for key in ("partial_image_index", "size", "quality", "background", "output_format", "output_index"):
|
|
554
|
+
value = payload.get(key)
|
|
555
|
+
if value is not None:
|
|
556
|
+
event[key] = value
|
|
557
|
+
return event
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def _build_completed_event(item: Mapping[str, JsonValue], *, event_type: str) -> dict[str, JsonValue] | None:
|
|
561
|
+
if item.get("type") != "image_generation_call":
|
|
562
|
+
return None
|
|
563
|
+
result = item.get("result")
|
|
564
|
+
if not isinstance(result, str) or not result:
|
|
565
|
+
return None
|
|
566
|
+
event: dict[str, JsonValue] = {
|
|
567
|
+
"type": event_type,
|
|
568
|
+
"b64_json": result,
|
|
569
|
+
# See ``_build_partial_image_event`` for the rationale: keep the
|
|
570
|
+
# upstream ``created_at`` if it is present, otherwise synthesize
|
|
571
|
+
# so the OpenAI Images stream-event schema validates.
|
|
572
|
+
"created_at": _coerce_created_at(item.get("created_at")),
|
|
573
|
+
}
|
|
574
|
+
for key in ("revised_prompt", "size", "quality", "background", "output_format"):
|
|
575
|
+
value = item.get(key)
|
|
576
|
+
if value is not None:
|
|
577
|
+
event[key] = value
|
|
578
|
+
return event
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def _coerce_created_at(value: JsonValue | None) -> int:
|
|
582
|
+
"""Return ``value`` as an int when upstream supplied it, otherwise
|
|
583
|
+
a synthesized current Unix timestamp."""
|
|
584
|
+
if isinstance(value, int) and not isinstance(value, bool):
|
|
585
|
+
return value
|
|
586
|
+
if isinstance(value, float):
|
|
587
|
+
return int(value)
|
|
588
|
+
return int(time.time())
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def _build_error_event(
|
|
592
|
+
code: str,
|
|
593
|
+
message: str,
|
|
594
|
+
*,
|
|
595
|
+
error_type: str = "server_error",
|
|
596
|
+
param: str | None = None,
|
|
597
|
+
) -> dict[str, JsonValue]:
|
|
598
|
+
envelope = openai_error(code, message, error_type=error_type)
|
|
599
|
+
if param:
|
|
600
|
+
envelope["error"]["param"] = param
|
|
601
|
+
event: dict[str, JsonValue] = {"type": _DOWNSTREAM_ERROR_EVENT}
|
|
602
|
+
for key, value in envelope.items():
|
|
603
|
+
event[key] = cast(JsonValue, value)
|
|
604
|
+
return event
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def _failed_image_item_error_event(item: Mapping[str, JsonValue]) -> dict[str, JsonValue]:
|
|
608
|
+
error_value = item.get("error")
|
|
609
|
+
if is_json_mapping(error_value):
|
|
610
|
+
code = error_value.get("code")
|
|
611
|
+
message = error_value.get("message")
|
|
612
|
+
error_type = error_value.get("type")
|
|
613
|
+
return _build_error_event(
|
|
614
|
+
code if isinstance(code, str) and code else "image_generation_failed",
|
|
615
|
+
message if isinstance(message, str) and message else "Image generation failed",
|
|
616
|
+
error_type=error_type if isinstance(error_type, str) and error_type else "server_error",
|
|
617
|
+
)
|
|
618
|
+
return _build_error_event(
|
|
619
|
+
"image_generation_failed",
|
|
620
|
+
"Upstream image_generation_call reported status=failed",
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def _response_failed_to_error_event(payload: Mapping[str, JsonValue]) -> dict[str, JsonValue]:
|
|
625
|
+
response = payload.get("response")
|
|
626
|
+
if is_json_mapping(response):
|
|
627
|
+
error_value = response.get("error")
|
|
628
|
+
if is_json_mapping(error_value):
|
|
629
|
+
code = error_value.get("code")
|
|
630
|
+
message = error_value.get("message")
|
|
631
|
+
error_type = error_value.get("type")
|
|
632
|
+
return _build_error_event(
|
|
633
|
+
code if isinstance(code, str) and code else "upstream_error",
|
|
634
|
+
message if isinstance(message, str) and message else "Upstream image generation failed",
|
|
635
|
+
error_type=error_type if isinstance(error_type, str) and error_type else "server_error",
|
|
636
|
+
)
|
|
637
|
+
return _build_error_event("upstream_error", "Upstream image generation failed")
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def _error_event_to_error_event(payload: Mapping[str, JsonValue]) -> dict[str, JsonValue]:
|
|
641
|
+
error_value = payload.get("error")
|
|
642
|
+
if is_json_mapping(error_value):
|
|
643
|
+
code = error_value.get("code")
|
|
644
|
+
message = error_value.get("message")
|
|
645
|
+
error_type = error_value.get("type")
|
|
646
|
+
return _build_error_event(
|
|
647
|
+
code if isinstance(code, str) and code else "upstream_error",
|
|
648
|
+
message if isinstance(message, str) and message else "Upstream image generation failed",
|
|
649
|
+
error_type=error_type if isinstance(error_type, str) and error_type else "server_error",
|
|
650
|
+
)
|
|
651
|
+
return _build_error_event("upstream_error", "Upstream image generation failed")
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
async def translate_responses_stream_to_images_stream(
|
|
655
|
+
upstream: AsyncIterator[str],
|
|
656
|
+
*,
|
|
657
|
+
captured: dict[str, object] | None = None,
|
|
658
|
+
is_edit: bool = False,
|
|
659
|
+
) -> AsyncIterator[str]:
|
|
660
|
+
"""Convert a Responses SSE event stream into an OpenAI Images SSE stream.
|
|
661
|
+
|
|
662
|
+
Yields formatted SSE event blocks (terminated by ``\\n\\n``) suitable
|
|
663
|
+
for streaming directly to the client. Always emits exactly one terminal
|
|
664
|
+
event (``image_generation.completed`` / ``image_edit.completed`` or
|
|
665
|
+
``error``) followed by ``data: [DONE]``.
|
|
666
|
+
|
|
667
|
+
The ``is_edit`` flag selects the canonical OpenAI event prefix:
|
|
668
|
+
``/v1/images/generations`` callers leave it ``False`` and receive
|
|
669
|
+
``image_generation.partial_image`` / ``image_generation.completed``;
|
|
670
|
+
``/v1/images/edits`` callers pass ``True`` and receive
|
|
671
|
+
``image_edit.partial_image`` / ``image_edit.completed`` so SDKs that
|
|
672
|
+
listen for the edit-specific event names see the stream.
|
|
673
|
+
|
|
674
|
+
When ``captured`` is provided, the translator stores the upstream
|
|
675
|
+
Responses ``id`` (under the ``response_id`` key) the first time it sees
|
|
676
|
+
one, so route handlers can correlate the resulting request log with the
|
|
677
|
+
public effective model after the stream completes.
|
|
678
|
+
"""
|
|
679
|
+
partial_event_type = _EDIT_PARTIAL_EVENT if is_edit else _GENERATION_PARTIAL_EVENT
|
|
680
|
+
completed_event_type = _EDIT_COMPLETED_EVENT if is_edit else _GENERATION_COMPLETED_EVENT
|
|
681
|
+
terminal_emitted = False
|
|
682
|
+
completion_pending = True
|
|
683
|
+
# The Responses backend emits ``response.output_item.done`` for each
|
|
684
|
+
# ``image_generation_call`` *before* the final ``response.completed``
|
|
685
|
+
# event that carries ``tool_usage``. We buffer prepared completed
|
|
686
|
+
# events so we can attach ``usage`` once ``response.completed``
|
|
687
|
+
# arrives, and we keep them in arrival order so multi-image responses
|
|
688
|
+
# do not silently drop earlier completions. If the upstream stream
|
|
689
|
+
# ends without a ``response.completed`` we still flush whatever we
|
|
690
|
+
# have buffered (without ``usage``).
|
|
691
|
+
pending_completed_events: list[dict[str, JsonValue]] = []
|
|
692
|
+
|
|
693
|
+
async for line in upstream:
|
|
694
|
+
if not line:
|
|
695
|
+
continue
|
|
696
|
+
stripped = line.strip()
|
|
697
|
+
if stripped == "data: [DONE]":
|
|
698
|
+
# We emit our own [DONE] after a terminal event below.
|
|
699
|
+
continue
|
|
700
|
+
payload = parse_sse_data_json(line)
|
|
701
|
+
if payload is None:
|
|
702
|
+
continue
|
|
703
|
+
event_type = payload.get("type")
|
|
704
|
+
if not isinstance(event_type, str):
|
|
705
|
+
continue
|
|
706
|
+
|
|
707
|
+
# Capture the upstream Responses id once so the route handler can
|
|
708
|
+
# rewrite the request log with the public effective model after the
|
|
709
|
+
# stream completes.
|
|
710
|
+
if captured is not None and "response_id" not in captured:
|
|
711
|
+
response_value = payload.get("response")
|
|
712
|
+
if is_json_mapping(response_value):
|
|
713
|
+
response_id = response_value.get("id")
|
|
714
|
+
if isinstance(response_id, str) and response_id:
|
|
715
|
+
captured["response_id"] = response_id
|
|
716
|
+
|
|
717
|
+
if event_type == _UPSTREAM_PARTIAL_IMAGE_EVENT:
|
|
718
|
+
event = _build_partial_image_event(payload, event_type=partial_event_type)
|
|
719
|
+
if event is not None:
|
|
720
|
+
yield format_sse_event(event)
|
|
721
|
+
continue
|
|
722
|
+
|
|
723
|
+
if event_type == _UPSTREAM_OUTPUT_ITEM_DONE_EVENT:
|
|
724
|
+
item = payload.get("item")
|
|
725
|
+
if not is_json_mapping(item):
|
|
726
|
+
continue
|
|
727
|
+
if item.get("type") != "image_generation_call":
|
|
728
|
+
continue
|
|
729
|
+
status = item.get("status")
|
|
730
|
+
if isinstance(status, str) and status == "failed":
|
|
731
|
+
if captured is not None:
|
|
732
|
+
captured["image_stream_outcome"] = "image_error"
|
|
733
|
+
yield format_sse_event(_failed_image_item_error_event(item))
|
|
734
|
+
terminal_emitted = True
|
|
735
|
+
completion_pending = False
|
|
736
|
+
break
|
|
737
|
+
event = _build_completed_event(item, event_type=completed_event_type)
|
|
738
|
+
if event is not None:
|
|
739
|
+
# Buffer every completion (NOT a single overwrite-on-update
|
|
740
|
+
# variable) so multi-image responses do not silently drop
|
|
741
|
+
# earlier images. Flushing is deferred until the trailing
|
|
742
|
+
# ``response.completed`` arrives so we can attach
|
|
743
|
+
# ``tool_usage.image_gen`` as ``usage`` to the final event,
|
|
744
|
+
# matching the canonical OpenAI Images streaming shape.
|
|
745
|
+
pending_completed_events.append(event)
|
|
746
|
+
continue
|
|
747
|
+
|
|
748
|
+
if event_type == _UPSTREAM_RESPONSE_COMPLETED_EVENT:
|
|
749
|
+
response_obj = payload.get("response")
|
|
750
|
+
usage = _extract_image_usage(response_obj) if is_json_mapping(response_obj) else None
|
|
751
|
+
# Stash the usage tokens on ``captured`` so the route handler
|
|
752
|
+
# can post-hoc record them against the API key (the
|
|
753
|
+
# standard stream settlement only sees ``response.usage``,
|
|
754
|
+
# which is typically empty for the image_generation tool
|
|
755
|
+
# path).
|
|
756
|
+
if captured is not None and usage is not None:
|
|
757
|
+
_stash_image_usage_tokens(captured, usage)
|
|
758
|
+
if pending_completed_events:
|
|
759
|
+
# Emit every buffered completion in arrival order so
|
|
760
|
+
# multi-image responses do not silently drop earlier
|
|
761
|
+
# images. Only the last completion carries ``usage``.
|
|
762
|
+
last_index = len(pending_completed_events) - 1
|
|
763
|
+
for idx, event in enumerate(pending_completed_events):
|
|
764
|
+
if idx == last_index and usage is not None:
|
|
765
|
+
event["usage"] = usage.model_dump(mode="json", exclude_none=True)
|
|
766
|
+
yield format_sse_event(event)
|
|
767
|
+
pending_completed_events.clear()
|
|
768
|
+
terminal_emitted = True
|
|
769
|
+
if captured is not None:
|
|
770
|
+
captured["image_stream_outcome"] = "success"
|
|
771
|
+
elif not terminal_emitted:
|
|
772
|
+
if captured is not None:
|
|
773
|
+
captured["image_stream_outcome"] = "image_error"
|
|
774
|
+
yield format_sse_event(
|
|
775
|
+
_build_error_event(
|
|
776
|
+
"image_generation_failed",
|
|
777
|
+
"Upstream stream completed without an image_generation_call result",
|
|
778
|
+
)
|
|
779
|
+
)
|
|
780
|
+
terminal_emitted = True
|
|
781
|
+
completion_pending = False
|
|
782
|
+
break
|
|
783
|
+
|
|
784
|
+
if event_type == _UPSTREAM_RESPONSE_INCOMPLETE_EVENT:
|
|
785
|
+
if not terminal_emitted:
|
|
786
|
+
if captured is not None:
|
|
787
|
+
captured["image_stream_outcome"] = "image_error"
|
|
788
|
+
yield format_sse_event(
|
|
789
|
+
_build_error_event(
|
|
790
|
+
"image_generation_failed",
|
|
791
|
+
"Upstream stream ended before the image was generated",
|
|
792
|
+
)
|
|
793
|
+
)
|
|
794
|
+
terminal_emitted = True
|
|
795
|
+
completion_pending = False
|
|
796
|
+
break
|
|
797
|
+
|
|
798
|
+
if event_type == _UPSTREAM_RESPONSE_FAILED_EVENT:
|
|
799
|
+
if captured is not None:
|
|
800
|
+
captured["image_stream_outcome"] = "image_error"
|
|
801
|
+
yield format_sse_event(_response_failed_to_error_event(payload))
|
|
802
|
+
terminal_emitted = True
|
|
803
|
+
completion_pending = False
|
|
804
|
+
break
|
|
805
|
+
|
|
806
|
+
if event_type == _UPSTREAM_ERROR_EVENT:
|
|
807
|
+
if captured is not None:
|
|
808
|
+
captured["image_stream_outcome"] = "image_error"
|
|
809
|
+
yield format_sse_event(_error_event_to_error_event(payload))
|
|
810
|
+
terminal_emitted = True
|
|
811
|
+
completion_pending = False
|
|
812
|
+
break
|
|
813
|
+
|
|
814
|
+
# All other event types (response.created, reasoning, content_part,
|
|
815
|
+
# output_text, image_generation_call.in_progress / .generating,
|
|
816
|
+
# codex.rate_limits, etc.) are intentionally dropped.
|
|
817
|
+
continue
|
|
818
|
+
|
|
819
|
+
# If the upstream stream ended without a ``response.completed`` (e.g.
|
|
820
|
+
# truncation), still flush whatever we have buffered so the client sees
|
|
821
|
+
# a terminal event before [DONE]. ``usage`` is unknown in this case so
|
|
822
|
+
# we omit it.
|
|
823
|
+
if pending_completed_events and not terminal_emitted:
|
|
824
|
+
for event in pending_completed_events:
|
|
825
|
+
yield format_sse_event(event)
|
|
826
|
+
pending_completed_events.clear()
|
|
827
|
+
terminal_emitted = True
|
|
828
|
+
if captured is not None:
|
|
829
|
+
captured["image_stream_outcome"] = "truncated_with_image"
|
|
830
|
+
|
|
831
|
+
if completion_pending and not terminal_emitted:
|
|
832
|
+
if captured is not None:
|
|
833
|
+
captured["image_stream_outcome"] = "truncated"
|
|
834
|
+
yield format_sse_event(
|
|
835
|
+
_build_error_event(
|
|
836
|
+
"image_generation_failed",
|
|
837
|
+
"Upstream stream truncated before a terminal image event",
|
|
838
|
+
)
|
|
839
|
+
)
|
|
840
|
+
|
|
841
|
+
yield "data: [DONE]\n\n"
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
async def collect_responses_stream_for_images(
|
|
845
|
+
upstream: AsyncIterator[str],
|
|
846
|
+
*,
|
|
847
|
+
captured: dict[str, object] | None = None,
|
|
848
|
+
) -> tuple[dict[str, JsonValue] | None, OpenAIErrorEnvelope | None]:
|
|
849
|
+
"""Drain a Responses SSE stream and return the final ``response`` payload.
|
|
850
|
+
|
|
851
|
+
Returns ``(response_mapping, None)`` when the upstream stream emits a
|
|
852
|
+
``response.completed`` event; ``(None, error_envelope)`` when it emits
|
|
853
|
+
``response.failed`` / ``error`` / closes early.
|
|
854
|
+
|
|
855
|
+
When ``captured`` is provided, stores the upstream Responses ``id`` the
|
|
856
|
+
first time it appears in any event, so the caller can correlate the
|
|
857
|
+
resulting request log with the public effective model.
|
|
858
|
+
"""
|
|
859
|
+
output_items: dict[int, dict[str, JsonValue]] = {}
|
|
860
|
+
fallback_items: list[dict[str, JsonValue]] = []
|
|
861
|
+
final_response: dict[str, JsonValue] | None = None
|
|
862
|
+
terminal_error: OpenAIErrorEnvelope | None = None
|
|
863
|
+
|
|
864
|
+
async for line in upstream:
|
|
865
|
+
if not line:
|
|
866
|
+
continue
|
|
867
|
+
if line.strip() == "data: [DONE]":
|
|
868
|
+
continue
|
|
869
|
+
payload = parse_sse_data_json(line)
|
|
870
|
+
if payload is None:
|
|
871
|
+
continue
|
|
872
|
+
event_type = payload.get("type")
|
|
873
|
+
if not isinstance(event_type, str):
|
|
874
|
+
continue
|
|
875
|
+
|
|
876
|
+
# Capture the upstream Responses id once so the route handler can
|
|
877
|
+
# rewrite the request log with the public effective model after the
|
|
878
|
+
# stream completes.
|
|
879
|
+
if captured is not None and "response_id" not in captured:
|
|
880
|
+
response_value = payload.get("response")
|
|
881
|
+
if is_json_mapping(response_value):
|
|
882
|
+
response_id = response_value.get("id")
|
|
883
|
+
if isinstance(response_id, str) and response_id:
|
|
884
|
+
captured["response_id"] = response_id
|
|
885
|
+
|
|
886
|
+
if event_type == _UPSTREAM_OUTPUT_ITEM_DONE_EVENT:
|
|
887
|
+
output_index = payload.get("output_index")
|
|
888
|
+
item = payload.get("item")
|
|
889
|
+
if not is_json_mapping(item):
|
|
890
|
+
continue
|
|
891
|
+
if isinstance(output_index, int):
|
|
892
|
+
output_items[output_index] = dict(item)
|
|
893
|
+
else:
|
|
894
|
+
# Some upstream paths omit ``output_index``; preserve the
|
|
895
|
+
# arrival order so we can still surface the item.
|
|
896
|
+
fallback_items.append(dict(item))
|
|
897
|
+
continue
|
|
898
|
+
|
|
899
|
+
if event_type == _UPSTREAM_RESPONSE_COMPLETED_EVENT and final_response is None:
|
|
900
|
+
response_value = payload.get("response")
|
|
901
|
+
base: dict[str, JsonValue]
|
|
902
|
+
if is_json_mapping(response_value):
|
|
903
|
+
base = dict(response_value)
|
|
904
|
+
else:
|
|
905
|
+
base = {}
|
|
906
|
+
existing_output = base.get("output")
|
|
907
|
+
if not (isinstance(existing_output, list) and existing_output):
|
|
908
|
+
merged_output: list[JsonValue] = [item for _, item in sorted(output_items.items())]
|
|
909
|
+
merged_output.extend(fallback_items)
|
|
910
|
+
base["output"] = merged_output
|
|
911
|
+
final_response = base
|
|
912
|
+
# Stash the image usage tokens so the route handler can
|
|
913
|
+
# post-hoc charge the API key. The standard stream
|
|
914
|
+
# settlement only reads ``response.usage`` which is
|
|
915
|
+
# typically empty for the image_generation tool path.
|
|
916
|
+
if captured is not None:
|
|
917
|
+
tool_usage_payload = base
|
|
918
|
+
usage = _extract_image_usage(tool_usage_payload)
|
|
919
|
+
if usage is not None:
|
|
920
|
+
_stash_image_usage_tokens(captured, usage)
|
|
921
|
+
# Don't break — keep draining so the upstream stream_responses
|
|
922
|
+
# generator finalizes (which writes the request log) before we
|
|
923
|
+
# return control to the caller. Once we have ``final_response``
|
|
924
|
+
# we still skip processing of any subsequent events.
|
|
925
|
+
continue
|
|
926
|
+
|
|
927
|
+
if event_type == _UPSTREAM_RESPONSE_INCOMPLETE_EVENT and terminal_error is None:
|
|
928
|
+
# Match the streaming translator: an incomplete upstream
|
|
929
|
+
# response is surfaced as an error envelope rather than a
|
|
930
|
+
# 200 response carrying a half-finished image.
|
|
931
|
+
terminal_error = openai_error(
|
|
932
|
+
"image_generation_failed",
|
|
933
|
+
"Upstream response was incomplete before the image was generated",
|
|
934
|
+
error_type="server_error",
|
|
935
|
+
)
|
|
936
|
+
break
|
|
937
|
+
|
|
938
|
+
if event_type == _UPSTREAM_RESPONSE_FAILED_EVENT:
|
|
939
|
+
# Once a successful ``response.completed`` is captured, ignore
|
|
940
|
+
# any trailing transport-level failure events: they cannot
|
|
941
|
+
# invalidate an already-received image result. Without this
|
|
942
|
+
# guard a late ``response.failed`` could turn a successful
|
|
943
|
+
# 200 envelope into a spurious 502.
|
|
944
|
+
if final_response is not None:
|
|
945
|
+
continue
|
|
946
|
+
response_value = payload.get("response")
|
|
947
|
+
error_value: JsonValue | None = None
|
|
948
|
+
if is_json_mapping(response_value):
|
|
949
|
+
error_value = response_value.get("error")
|
|
950
|
+
if is_json_mapping(error_value):
|
|
951
|
+
code = error_value.get("code")
|
|
952
|
+
message = error_value.get("message")
|
|
953
|
+
error_type = error_value.get("type")
|
|
954
|
+
envelope = openai_error(
|
|
955
|
+
code if isinstance(code, str) and code else "upstream_error",
|
|
956
|
+
message if isinstance(message, str) and message else "Upstream image generation failed",
|
|
957
|
+
error_type=error_type if isinstance(error_type, str) and error_type else "server_error",
|
|
958
|
+
)
|
|
959
|
+
else:
|
|
960
|
+
envelope = openai_error(
|
|
961
|
+
"upstream_error",
|
|
962
|
+
"Upstream image generation failed",
|
|
963
|
+
error_type="server_error",
|
|
964
|
+
)
|
|
965
|
+
terminal_error = envelope
|
|
966
|
+
break
|
|
967
|
+
|
|
968
|
+
if event_type == _UPSTREAM_ERROR_EVENT:
|
|
969
|
+
# Same rationale as the ``response.failed`` branch above:
|
|
970
|
+
# ignore late error events once a complete response has
|
|
971
|
+
# already been captured.
|
|
972
|
+
if final_response is not None:
|
|
973
|
+
continue
|
|
974
|
+
error_value = payload.get("error")
|
|
975
|
+
if is_json_mapping(error_value):
|
|
976
|
+
code = error_value.get("code")
|
|
977
|
+
message = error_value.get("message")
|
|
978
|
+
error_type = error_value.get("type")
|
|
979
|
+
envelope = openai_error(
|
|
980
|
+
code if isinstance(code, str) and code else "upstream_error",
|
|
981
|
+
message if isinstance(message, str) and message else "Upstream image generation failed",
|
|
982
|
+
error_type=error_type if isinstance(error_type, str) and error_type else "server_error",
|
|
983
|
+
)
|
|
984
|
+
else:
|
|
985
|
+
envelope = openai_error(
|
|
986
|
+
"upstream_error",
|
|
987
|
+
"Upstream image generation failed",
|
|
988
|
+
error_type="server_error",
|
|
989
|
+
)
|
|
990
|
+
terminal_error = envelope
|
|
991
|
+
break
|
|
992
|
+
|
|
993
|
+
if terminal_error is not None:
|
|
994
|
+
return None, terminal_error
|
|
995
|
+
if final_response is None:
|
|
996
|
+
return None, openai_error(
|
|
997
|
+
"image_generation_failed",
|
|
998
|
+
"Upstream stream truncated before a terminal event",
|
|
999
|
+
error_type="server_error",
|
|
1000
|
+
)
|
|
1001
|
+
return final_response, None
|
|
1002
|
+
|
|
1003
|
+
|
|
1004
|
+
# ---------------------------------------------------------------------------
|
|
1005
|
+
# Misc utilities used by the API handlers
|
|
1006
|
+
# ---------------------------------------------------------------------------
|
|
1007
|
+
|
|
1008
|
+
|
|
1009
|
+
def decode_data_url(data_url: str) -> tuple[bytes, str | None]:
|
|
1010
|
+
"""Decode a ``data:<mime>;base64,<...>`` URL into raw bytes plus mime.
|
|
1011
|
+
|
|
1012
|
+
Raises ``ValueError`` if the URL does not match the expected shape or
|
|
1013
|
+
the base64 payload is malformed.
|
|
1014
|
+
"""
|
|
1015
|
+
match = _DATA_URL_PATTERN.match(data_url.strip())
|
|
1016
|
+
if match is None:
|
|
1017
|
+
raise ValueError("Expected a data:<mime>;base64,<payload> URL")
|
|
1018
|
+
mime_type = match.group("mime") or None
|
|
1019
|
+
try:
|
|
1020
|
+
return base64.b64decode(match.group("b64"), validate=True), mime_type
|
|
1021
|
+
except (binascii.Error, ValueError) as exc:
|
|
1022
|
+
raise ValueError("Invalid base64 image payload") from exc
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
def make_invalid_request_error(
|
|
1026
|
+
message: str,
|
|
1027
|
+
*,
|
|
1028
|
+
param: str | None = None,
|
|
1029
|
+
code: str = "invalid_request_error",
|
|
1030
|
+
) -> OpenAIErrorEnvelope:
|
|
1031
|
+
envelope = openai_error(code, message, error_type="invalid_request_error")
|
|
1032
|
+
if param:
|
|
1033
|
+
envelope["error"]["param"] = param
|
|
1034
|
+
return envelope
|
|
1035
|
+
|
|
1036
|
+
|
|
1037
|
+
def make_not_found_error(message: str) -> OpenAIErrorEnvelope:
|
|
1038
|
+
return openai_error("not_found_error", message, error_type="invalid_request_error")
|
|
1039
|
+
|
|
1040
|
+
|
|
1041
|
+
__all__ = [
|
|
1042
|
+
"collect_responses_stream_for_images",
|
|
1043
|
+
"decode_data_url",
|
|
1044
|
+
"images_edit_to_responses_request",
|
|
1045
|
+
"images_generation_to_responses_request",
|
|
1046
|
+
"images_response_from_responses",
|
|
1047
|
+
"is_supported_image_model",
|
|
1048
|
+
"make_invalid_request_error",
|
|
1049
|
+
"make_not_found_error",
|
|
1050
|
+
"resolve_public_image_model",
|
|
1051
|
+
"translate_responses_stream_to_images_stream",
|
|
1052
|
+
"validate_edits_payload",
|
|
1053
|
+
"validate_generations_payload",
|
|
1054
|
+
]
|