From ee75be0fe7246403374d6f80b5ac27a400810989 Mon Sep 17 00:00:00 2001 From: LB7666 Date: Fri, 26 Jun 2026 03:37:44 +0800 Subject: [PATCH 1/3] feat(llm-access): add standalone Codex image gateway --- Cargo.lock | 21 + Cargo.toml | 1 + crates/frontend/src/api.rs | 44 ++ .../frontend/src/pages/admin_kiro_gateway.rs | 2 + .../frontend/src/pages/admin_llm_gateway.rs | 205 +++++ crates/llm-access-codex-image/Cargo.toml | 27 + crates/llm-access-codex-image/src/dispatch.rs | 54 ++ crates/llm-access-codex-image/src/lib.rs | 10 + crates/llm-access-codex-image/src/limiter.rs | 61 ++ crates/llm-access-codex-image/src/logging.rs | 262 +++++++ crates/llm-access-codex-image/src/main.rs | 703 ++++++++++++++++++ crates/llm-access-codex-image/src/request.rs | 240 ++++++ .../tests/codex_image_dispatch.rs | 129 ++++ .../tests/codex_image_request.rs | 99 +++ .../src/store/codex_account.rs | 17 + crates/llm-access-core/src/store/empty.rs | 6 +- crates/llm-access-core/src/store/keys.rs | 5 + crates/llm-access-core/src/store/mod.rs | 2 + crates/llm-access-core/src/store/routes.rs | 6 + .../migrations/postgres/0001_init.sql | 1 + .../0030_codex_image_generation_toggle.sql | 2 + crates/llm-access-migrations/src/lib.rs | 5 + crates/llm-access-store/src/duckdb.rs | 1 + crates/llm-access-store/src/lib.rs | 1 + crates/llm-access-store/src/postgres.rs | 24 + crates/llm-access-store/src/postgres/cache.rs | 7 + .../src/postgres/codex_account.rs | 71 ++ .../llm-access-store/src/postgres/decode.rs | 54 +- crates/llm-access-store/src/postgres/keys.rs | 17 +- .../llm-access-store/src/postgres/routes.rs | 8 + crates/llm-access-store/src/records.rs | 2 + crates/llm-access-store/src/request_cache.rs | 92 ++- crates/llm-access/src/admin.rs | 37 + crates/llm-access/src/codex_refresh.rs | 4 + crates/llm-access/src/codex_status.rs | 3 + .../src/provider/route_selection.rs | 3 + crates/llm-access/src/provider/tests.rs | 16 + .../caddy/llm-access-path-split.Caddyfile | 16 + .../llm-access-codex-image.service.template | 33 + scripts/activate_llm_access_cloud_release.sh | 68 +- scripts/prepare_llm_access_cloud_release.sh | 25 +- ...lease_llm_access_cloud_codex_image_only.sh | 74 ++ scripts/render_llm_access_cloud_bundle.sh | 1 + scripts/test_llm_access_cloud_bundle.sh | 6 + .../test_llm_access_cloud_release_scripts.sh | 19 +- 45 files changed, 2444 insertions(+), 40 deletions(-) create mode 100644 crates/llm-access-codex-image/Cargo.toml create mode 100644 crates/llm-access-codex-image/src/dispatch.rs create mode 100644 crates/llm-access-codex-image/src/lib.rs create mode 100644 crates/llm-access-codex-image/src/limiter.rs create mode 100644 crates/llm-access-codex-image/src/logging.rs create mode 100644 crates/llm-access-codex-image/src/main.rs create mode 100644 crates/llm-access-codex-image/src/request.rs create mode 100644 crates/llm-access-codex-image/tests/codex_image_dispatch.rs create mode 100644 crates/llm-access-codex-image/tests/codex_image_request.rs create mode 100644 crates/llm-access-migrations/migrations/postgres/0030_codex_image_generation_toggle.sql create mode 100644 deployment-examples/systemd/llm-access-codex-image.service.template create mode 100755 scripts/release_llm_access_cloud_codex_image_only.sh diff --git a/Cargo.lock b/Cargo.lock index ea297d72..d1efb78e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5745,6 +5745,27 @@ dependencies = [ "zstd", ] +[[package]] +name = "llm-access-codex-image" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "bytes", + "clap", + "llm-access-codex", + "llm-access-core", + "llm-access-store", + "reqwest", + "serde", + "serde_json", + "sha2 0.10.9", + "tokio", + "tower 0.5.2", + "tracing", + "tracing-subscriber", +] + [[package]] name = "llm-access-core" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 427b4062..a43b62ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ members = [ "crates/llm-access", "crates/llm-access-core", "crates/llm-access-codex", + "crates/llm-access-codex-image", "crates/llm-access-kiro", "crates/llm-access-migrations", "crates/llm-access-store", diff --git a/crates/frontend/src/api.rs b/crates/frontend/src/api.rs index 27cfc4f5..0e358d45 100644 --- a/crates/frontend/src/api.rs +++ b/crates/frontend/src/api.rs @@ -6066,6 +6066,10 @@ const fn default_true() -> bool { true } +const fn default_codex_image_generation_max_concurrency() -> u64 { + 3 +} + fn default_kiro_pool_strategy() -> String { llm_store::default_kiro_pool_strategy() } @@ -6112,6 +6116,8 @@ pub struct AdminLlmGatewayKeyView { pub codex_fast_enabled: bool, #[serde(default)] pub codex_strict_session_rejection_enabled: bool, + #[serde(default)] + pub codex_image_generation_enabled: bool, #[serde(default = "default_true")] pub kiro_request_validation_enabled: bool, #[serde(default = "default_true")] @@ -8985,6 +8991,7 @@ pub async fn create_admin_llm_gateway_key( uses_global_kiro_billable_model_multipliers: true, codex_fast_enabled: true, codex_strict_session_rejection_enabled: false, + codex_image_generation_enabled: false, kiro_candidate_credit_summary: None, }) } @@ -9032,6 +9039,7 @@ pub struct PatchAdminLlmGatewayKeyRequest<'a> { pub request_min_start_interval_ms: Option, pub codex_fast_enabled: Option, pub codex_strict_session_rejection_enabled: Option, + pub codex_image_generation_enabled: Option, pub kiro_request_validation_enabled: Option, pub kiro_cache_estimation_enabled: Option, pub kiro_zero_cache_debug_enabled: Option, @@ -9068,6 +9076,7 @@ pub async fn patch_admin_llm_gateway_key( request.request_min_start_interval_ms, request.codex_fast_enabled, request.codex_strict_session_rejection_enabled, + request.codex_image_generation_enabled, request.kiro_request_validation_enabled, request.kiro_cache_estimation_enabled, request.kiro_zero_cache_debug_enabled, @@ -9179,6 +9188,12 @@ pub async fn patch_admin_llm_gateway_key( serde_json::Value::Bool(enabled), ); } + if let Some(enabled) = request.codex_image_generation_enabled { + body.insert( + "codex_image_generation_enabled".to_string(), + serde_json::Value::Bool(enabled), + ); + } if let Some(kiro_request_validation_enabled) = request.kiro_request_validation_enabled { body.insert( "kiro_request_validation_enabled".to_string(), @@ -9855,6 +9870,10 @@ pub struct AccountSummaryView { pub auto_refresh_enabled: bool, pub request_max_concurrency: Option, pub request_min_start_interval_ms: Option, + #[serde(default)] + pub codex_image_generation_enabled: bool, + #[serde(default = "default_codex_image_generation_max_concurrency")] + pub codex_image_generation_max_concurrency: u64, pub proxy_mode: String, pub proxy_config_id: Option, pub effective_proxy_source: String, @@ -9883,6 +9902,9 @@ impl Default for AccountSummaryView { auto_refresh_enabled: true, request_max_concurrency: None, request_min_start_interval_ms: None, + codex_image_generation_enabled: false, + codex_image_generation_max_concurrency: default_codex_image_generation_max_concurrency( + ), proxy_mode: "inherit".to_string(), proxy_config_id: None, effective_proxy_source: "binding".to_string(), @@ -10248,6 +10270,9 @@ pub async fn import_admin_llm_gateway_account( auto_refresh_enabled: true, request_max_concurrency: None, request_min_start_interval_ms: None, + codex_image_generation_enabled: false, + codex_image_generation_max_concurrency: default_codex_image_generation_max_concurrency( + ), proxy_mode: "inherit".to_string(), proxy_config_id: None, effective_proxy_source: "binding".to_string(), @@ -10347,6 +10372,8 @@ pub struct PatchAdminLlmGatewayAccountInput { pub proxy_config_id: Option, pub request_max_concurrency: Option, pub request_min_start_interval_ms: Option, + pub codex_image_generation_enabled: Option, + pub codex_image_generation_max_concurrency: Option, pub request_max_concurrency_unlimited: bool, pub request_min_start_interval_ms_unlimited: bool, } @@ -10373,6 +10400,10 @@ pub async fn patch_admin_llm_gateway_account( auto_refresh_enabled: input.auto_refresh_enabled.unwrap_or(true), request_max_concurrency: input.request_max_concurrency, request_min_start_interval_ms: input.request_min_start_interval_ms, + codex_image_generation_enabled: input.codex_image_generation_enabled.unwrap_or(false), + codex_image_generation_max_concurrency: input + .codex_image_generation_max_concurrency + .unwrap_or_else(default_codex_image_generation_max_concurrency), proxy_mode: input .proxy_mode .clone() @@ -10430,6 +10461,9 @@ pub async fn refresh_admin_llm_gateway_account(name: &str) -> Result Html { request_min_start_interval_ms: None, codex_fast_enabled: None, codex_strict_session_rejection_enabled: None, + codex_image_generation_enabled: None, kiro_request_validation_enabled: Some(kiro_request_validation_enabled_value), kiro_cache_estimation_enabled: Some(kiro_cache_estimation_enabled_value), kiro_zero_cache_debug_enabled: Some(kiro_zero_cache_debug_enabled_value), @@ -2006,6 +2007,7 @@ fn kiro_key_editor_card(props: &KiroKeyEditorCardProps) -> Html { request_min_start_interval_ms: None, codex_fast_enabled: None, codex_strict_session_rejection_enabled: None, + codex_image_generation_enabled: None, kiro_request_validation_enabled: None, kiro_cache_estimation_enabled: Some(kiro_cache_estimation_enabled_value), kiro_zero_cache_debug_enabled: Some(kiro_zero_cache_debug_enabled_value), diff --git a/crates/frontend/src/pages/admin_llm_gateway.rs b/crates/frontend/src/pages/admin_llm_gateway.rs index 31a3503f..e91c33a7 100644 --- a/crates/frontend/src/pages/admin_llm_gateway.rs +++ b/crates/frontend/src/pages/admin_llm_gateway.rs @@ -90,6 +90,8 @@ const PROXY_TRAFFIC_QUERY_WINDOW_DAYS: u64 = 30; const ADMIN_CODEX_IMPORT_JOB_LIST_LIMIT: usize = 10; const ACCOUNT_PAGE_SIZE: usize = 8; const KEY_PAGE_SIZE: usize = 8; +const CODEX_IMAGE_DEFAULT_CONCURRENCY: u64 = 3; +const CODEX_IMAGE_MAX_CONCURRENCY: u64 = 1024; const ACCOUNT_ACCENT_BORDERS: &[&str] = &[ "border-l-4 border-l-teal-500/70", "border-l-4 border-l-violet-500/70", @@ -484,6 +486,28 @@ fn account_rate_limit_bucket<'a>( }) } +fn account_image_rate_limit_bucket<'a>( + status: Option<&'a LlmGatewayRateLimitStatusResponse>, + account_name: &str, +) -> Option<&'a LlmGatewayRateLimitBucketView> { + let status = status?; + status.buckets.iter().find(|bucket| { + if bucket.account_name.as_deref() != Some(account_name) { + return false; + } + let limit_id = bucket.limit_id.to_ascii_lowercase(); + let limit_name = bucket + .limit_name + .as_deref() + .unwrap_or_default() + .to_ascii_lowercase(); + let display_name = bucket.display_name.to_ascii_lowercase(); + [limit_id.as_str(), limit_name.as_str(), display_name.as_str()] + .iter() + .any(|value| value.contains("gpt-image") || value.contains("image")) + }) +} + fn account_limit_remaining_percent( window: Option<&LlmGatewayRateLimitWindowView>, fallback: Option, @@ -1245,6 +1269,7 @@ fn key_editor_card(props: &KeyEditorCardProps) -> Html { let codex_fast_enabled = use_state(|| key_item.codex_fast_enabled); let codex_strict_session_rejection_enabled = use_state(|| key_item.codex_strict_session_rejection_enabled); + let codex_image_generation_enabled = use_state(|| key_item.codex_image_generation_enabled); let saving = use_state(|| false); let feedback = use_state(|| None::); @@ -1262,6 +1287,7 @@ fn key_editor_card(props: &KeyEditorCardProps) -> Html { let request_min_start_interval_ms = request_min_start_interval_ms.clone(); let codex_fast_enabled = codex_fast_enabled.clone(); let codex_strict_session_rejection_enabled = codex_strict_session_rejection_enabled.clone(); + let codex_image_generation_enabled = codex_image_generation_enabled.clone(); use_effect_with((props.key_item.clone(), props.account_groups.clone()), move |_| { name.set(key_item.name.clone()); quota.set(key_item.quota_billable_limit.to_string()); @@ -1293,6 +1319,7 @@ fn key_editor_card(props: &KeyEditorCardProps) -> Html { codex_fast_enabled.set(key_item.codex_fast_enabled); codex_strict_session_rejection_enabled .set(key_item.codex_strict_session_rejection_enabled); + codex_image_generation_enabled.set(key_item.codex_image_generation_enabled); || () }); } @@ -1361,6 +1388,7 @@ fn key_editor_card(props: &KeyEditorCardProps) -> Html { let request_min_start_interval_ms = request_min_start_interval_ms.clone(); let codex_fast_enabled = codex_fast_enabled.clone(); let codex_strict_session_rejection_enabled = codex_strict_session_rejection_enabled.clone(); + let codex_image_generation_enabled = codex_image_generation_enabled.clone(); let saving = saving.clone(); let feedback = feedback.clone(); let on_flash = props.on_flash.clone(); @@ -1381,6 +1409,7 @@ fn key_editor_card(props: &KeyEditorCardProps) -> Html { let codex_fast_enabled_value = *codex_fast_enabled; let codex_strict_session_rejection_enabled_value = *codex_strict_session_rejection_enabled; + let codex_image_generation_enabled_value = *codex_image_generation_enabled; let saving = saving.clone(); let feedback = feedback.clone(); let on_flash = on_flash.clone(); @@ -1440,6 +1469,7 @@ fn key_editor_card(props: &KeyEditorCardProps) -> Html { codex_strict_session_rejection_enabled: Some( codex_strict_session_rejection_enabled_value, ), + codex_image_generation_enabled: Some(codex_image_generation_enabled_value), kiro_request_validation_enabled: None, kiro_cache_estimation_enabled: None, kiro_zero_cache_debug_enabled: None, @@ -1704,6 +1734,21 @@ fn key_editor_card(props: &KeyEditorCardProps) -> Html { /> { "严格拒绝 fatal session" } + + + () { + let mut next = (*account_image_concurrency_inputs).clone(); + next.insert(acc_name_for_image_concurrency_change.clone(), target.value()); + account_image_concurrency_inputs.set(next); + } + }) + }} + />