diff --git a/Cargo.lock b/Cargo.lock index ea297d72..7c5c30c0 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", + "tempfile", + "tokio", + "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..dad0fa99 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() } @@ -6095,6 +6099,12 @@ pub struct AdminLlmGatewayKeyView { pub usage_output_tokens: u64, pub usage_credit_total: f64, pub usage_credit_missing_events: u64, + #[serde(default)] + pub codex_image_usage_tokens: u64, + #[serde(default)] + pub codex_image_usage_missing_events: u64, + #[serde(default)] + pub codex_image_last_used_at: Option, pub remaining_billable: i64, pub last_used_at: Option, pub created_at: i64, @@ -6112,6 +6122,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")] @@ -6170,6 +6182,10 @@ pub struct AdminLlmGatewayKeysSummaryView { pub usage_billable_tokens_sum: u64, pub usage_credit_total: f64, pub usage_credit_missing_events: u64, + #[serde(default)] + pub codex_image_usage_tokens_sum: u64, + #[serde(default)] + pub codex_image_usage_missing_events: u64, } /// Combined admin payload for the key inventory screen. @@ -8956,6 +8972,9 @@ pub async fn create_admin_llm_gateway_key( usage_output_tokens: 0, usage_credit_total: 0.0, usage_credit_missing_events: 0, + codex_image_usage_tokens: 0, + codex_image_usage_missing_events: 0, + codex_image_last_used_at: None, remaining_billable: quota_billable_limit as i64, last_used_at: None, created_at: 0, @@ -8985,6 +9004,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 +9052,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 +9089,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 +9201,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 +9883,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 +9915,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 +10283,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 +10385,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 +10413,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 +10474,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..9cbf51f2 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("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); + } + }) + }} + />