refactor: 移除独立 hub/proxy/executor/gateway crate,统一为 gateway tunnel 架构

- 删除 aether-hub、aether-proxy 独立项目及其 Dockerfile/配置
- 删除 crates/aether-executor 和 crates/aether-gateway 全部模块
- 新增 apps/ 目录作为应用入口
- 将 hub 概念重构为 gateway tunnel transport
- 将 executor 重构为 execution runtime
- 新增 tunnel.rs 合约定义和 testkit tunnel/execution_runtime 模块
- 更新 Python 服务层和测试适配新架构命名
This commit is contained in:
fawney19
2026-04-03 14:59:58 +08:00
parent ddf18fed9a
commit 8f26e1a31f
983 changed files with 103098 additions and 105837 deletions

View File

@@ -0,0 +1,626 @@
use super::*;
pub(crate) fn admin_provider_id_for_keys(request_path: &str) -> Option<String> {
request_path
.strip_prefix("/api/admin/endpoints/providers/")?
.strip_suffix("/keys")
.map(ToOwned::to_owned)
}
pub(crate) fn is_admin_provider_ops_architectures_root(request_path: &str) -> bool {
matches!(
request_path,
"/api/admin/provider-ops/architectures" | "/api/admin/provider-ops/architectures/"
)
}
pub(crate) fn admin_provider_ops_architecture_id_from_path(request_path: &str) -> Option<String> {
let raw = request_path.strip_prefix("/api/admin/provider-ops/architectures/")?;
let normalized = raw.trim().trim_matches('/');
if normalized.is_empty() || normalized.contains('/') {
None
} else {
Some(normalized.to_string())
}
}
pub(crate) fn admin_provider_id_for_provider_ops_status(request_path: &str) -> Option<String> {
request_path
.strip_prefix("/api/admin/provider-ops/providers/")?
.strip_suffix("/status")
.map(|value| value.trim().trim_matches('/').to_string())
.filter(|value| !value.is_empty() && !value.contains('/'))
}
pub(crate) fn admin_provider_id_for_provider_ops_config(request_path: &str) -> Option<String> {
request_path
.strip_prefix("/api/admin/provider-ops/providers/")?
.strip_suffix("/config")
.map(|value| value.trim().trim_matches('/').to_string())
.filter(|value| !value.is_empty() && !value.contains('/'))
}
pub(crate) fn admin_provider_id_for_provider_ops_disconnect(request_path: &str) -> Option<String> {
request_path
.strip_prefix("/api/admin/provider-ops/providers/")?
.strip_suffix("/disconnect")
.map(|value| value.trim().trim_matches('/').to_string())
.filter(|value| !value.is_empty() && !value.contains('/'))
}
pub(crate) fn admin_provider_id_for_provider_ops_connect(request_path: &str) -> Option<String> {
request_path
.strip_prefix("/api/admin/provider-ops/providers/")?
.strip_suffix("/connect")
.map(|value| value.trim().trim_matches('/').to_string())
.filter(|value| !value.is_empty() && !value.contains('/'))
}
pub(crate) fn admin_provider_id_for_provider_ops_verify(request_path: &str) -> Option<String> {
request_path
.strip_prefix("/api/admin/provider-ops/providers/")?
.strip_suffix("/verify")
.map(|value| value.trim().trim_matches('/').to_string())
.filter(|value| !value.is_empty() && !value.contains('/'))
}
pub(crate) fn admin_provider_id_for_provider_ops_balance(request_path: &str) -> Option<String> {
request_path
.strip_prefix("/api/admin/provider-ops/providers/")?
.strip_suffix("/balance")
.map(|value| value.trim().trim_matches('/').to_string())
.filter(|value| !value.is_empty() && !value.contains('/'))
}
pub(crate) fn admin_provider_id_for_provider_ops_checkin(request_path: &str) -> Option<String> {
request_path
.strip_prefix("/api/admin/provider-ops/providers/")?
.strip_suffix("/checkin")
.map(|value| value.trim().trim_matches('/').to_string())
.filter(|value| !value.is_empty() && !value.contains('/'))
}
pub(crate) fn is_admin_provider_strategy_strategies_root(request_path: &str) -> bool {
matches!(
request_path,
"/api/admin/provider-strategy/strategies" | "/api/admin/provider-strategy/strategies/"
)
}
pub(crate) fn admin_provider_id_for_provider_strategy_billing(
request_path: &str,
) -> Option<String> {
request_path
.strip_prefix("/api/admin/provider-strategy/providers/")?
.strip_suffix("/billing")
.map(|value| value.trim().trim_matches('/').to_string())
.filter(|value| !value.is_empty() && !value.contains('/'))
}
pub(crate) fn admin_provider_id_for_provider_strategy_stats(request_path: &str) -> Option<String> {
request_path
.strip_prefix("/api/admin/provider-strategy/providers/")?
.strip_suffix("/stats")
.map(|value| value.trim().trim_matches('/').to_string())
.filter(|value| !value.is_empty() && !value.contains('/'))
}
pub(crate) fn admin_provider_id_for_provider_strategy_quota(request_path: &str) -> Option<String> {
request_path
.strip_prefix("/api/admin/provider-strategy/providers/")?
.strip_suffix("/quota")
.map(|value| value.trim().trim_matches('/').to_string())
.filter(|value| !value.is_empty() && !value.contains('/'))
}
pub(crate) fn admin_provider_ops_action_route_parts(
request_path: &str,
) -> Option<(String, String)> {
let raw = request_path.strip_prefix("/api/admin/provider-ops/providers/")?;
let (provider_id, action_type) = raw.split_once("/actions/")?;
let provider_id = provider_id.trim().trim_matches('/');
let action_type = action_type.trim().trim_matches('/');
if provider_id.is_empty()
|| action_type.is_empty()
|| provider_id.contains('/')
|| action_type.contains('/')
{
None
} else {
Some((provider_id.to_string(), action_type.to_string()))
}
}
pub(crate) fn is_admin_provider_ops_batch_balance_root(request_path: &str) -> bool {
matches!(
request_path,
"/api/admin/provider-ops/batch/balance" | "/api/admin/provider-ops/batch/balance/"
)
}
pub(crate) fn admin_provider_id_for_refresh_quota(request_path: &str) -> Option<String> {
request_path
.strip_prefix("/api/admin/endpoints/providers/")?
.strip_suffix("/refresh-quota")
.map(ToOwned::to_owned)
}
pub(crate) fn admin_provider_id_for_health_monitor(request_path: &str) -> Option<String> {
let raw = request_path.strip_prefix("/api/admin/providers/")?;
let raw = raw.strip_suffix("/health-monitor")?;
let normalized = raw.trim().trim_matches('/');
if normalized.is_empty() || normalized.contains('/') {
None
} else {
Some(normalized.to_string())
}
}
pub(crate) fn admin_provider_id_for_mapping_preview(request_path: &str) -> Option<String> {
let raw = request_path.strip_prefix("/api/admin/providers/")?;
let raw = raw.strip_suffix("/mapping-preview")?;
let normalized = raw.trim().trim_matches('/');
if normalized.is_empty() || normalized.contains('/') {
None
} else {
Some(normalized.to_string())
}
}
pub(crate) fn admin_provider_id_for_pool_status(request_path: &str) -> Option<String> {
admin_provider_id_for_suffix(request_path, "/pool-status")
}
pub(crate) fn admin_provider_pool_key_route_parts(
request_path: &str,
marker: &str,
) -> Option<(String, String)> {
let raw = request_path.strip_prefix("/api/admin/providers/")?;
let (provider_id, key_id) = raw.split_once(marker)?;
let provider_id = provider_id.trim().trim_matches('/');
let key_id = key_id.trim().trim_matches('/');
if provider_id.is_empty()
|| key_id.is_empty()
|| provider_id.contains('/')
|| key_id.contains('/')
{
None
} else {
Some((provider_id.to_string(), key_id.to_string()))
}
}
pub(crate) fn admin_provider_clear_pool_cooldown_parts(
request_path: &str,
) -> Option<(String, String)> {
admin_provider_pool_key_route_parts(request_path, "/pool/clear-cooldown/")
}
pub(crate) fn admin_provider_reset_pool_cost_parts(request_path: &str) -> Option<(String, String)> {
admin_provider_pool_key_route_parts(request_path, "/pool/reset-cost/")
}
pub(crate) fn admin_provider_delete_task_parts(request_path: &str) -> Option<(String, String)> {
let raw = request_path.strip_prefix("/api/admin/providers/")?;
let (provider_id, task_id) = raw.split_once("/delete-task/")?;
let provider_id = provider_id.trim().trim_matches('/');
let task_id = task_id.trim().trim_matches('/');
if provider_id.is_empty()
|| task_id.is_empty()
|| provider_id.contains('/')
|| task_id.contains('/')
{
None
} else {
Some((provider_id.to_string(), task_id.to_string()))
}
}
pub(crate) fn admin_provider_id_for_summary(request_path: &str) -> Option<String> {
let raw = request_path.strip_prefix("/api/admin/providers/")?;
let raw = raw.strip_suffix("/summary")?;
let normalized = raw.trim().trim_matches('/');
if normalized.is_empty() || normalized.contains('/') {
None
} else {
Some(normalized.to_string())
}
}
pub(crate) fn build_admin_provider_delete_task_payload(
task: &LocalProviderDeleteTaskState,
) -> serde_json::Value {
json!({
"task_id": task.task_id,
"provider_id": task.provider_id,
"status": task.status,
"stage": task.stage,
"total_keys": task.total_keys,
"deleted_keys": task.deleted_keys,
"total_endpoints": task.total_endpoints,
"deleted_endpoints": task.deleted_endpoints,
"message": task.message,
})
}
pub(crate) fn put_admin_provider_delete_task(
state: &AppState,
task: &LocalProviderDeleteTaskState,
) {
state.put_provider_delete_task(task.clone());
}
pub(crate) fn admin_provider_id_for_manage_path(request_path: &str) -> Option<String> {
let raw = request_path.strip_prefix("/api/admin/providers/")?;
let normalized = raw.trim().trim_matches('/');
if normalized.is_empty() || normalized.contains('/') {
None
} else {
Some(normalized.to_string())
}
}
pub(crate) fn is_admin_providers_root(request_path: &str) -> bool {
matches!(
request_path,
"/api/admin/providers" | "/api/admin/providers/"
)
}
pub(crate) fn is_admin_management_tokens_root(request_path: &str) -> bool {
matches!(
request_path,
"/api/admin/management-tokens" | "/api/admin/management-tokens/"
)
}
pub(crate) fn is_admin_gemini_files_mappings_root(request_path: &str) -> bool {
matches!(
request_path,
"/api/admin/gemini-files/mappings" | "/api/admin/gemini-files/mappings/"
)
}
pub(crate) fn is_admin_gemini_files_stats_root(request_path: &str) -> bool {
matches!(
request_path,
"/api/admin/gemini-files/stats" | "/api/admin/gemini-files/stats/"
)
}
pub(crate) fn is_admin_gemini_files_capable_keys_root(request_path: &str) -> bool {
matches!(
request_path,
"/api/admin/gemini-files/capable-keys" | "/api/admin/gemini-files/capable-keys/"
)
}
pub(crate) fn is_admin_gemini_files_upload_root(request_path: &str) -> bool {
matches!(
request_path,
"/api/admin/gemini-files/upload" | "/api/admin/gemini-files/upload/"
)
}
pub(crate) fn is_admin_system_configs_root(request_path: &str) -> bool {
matches!(
request_path,
"/api/admin/system/configs" | "/api/admin/system/configs/"
)
}
pub(crate) fn is_admin_system_email_templates_root(request_path: &str) -> bool {
matches!(
request_path,
"/api/admin/system/email/templates" | "/api/admin/system/email/templates/"
)
}
pub(crate) fn admin_system_config_key_from_path(request_path: &str) -> Option<String> {
let value = request_path
.strip_prefix("/api/admin/system/configs/")?
.trim()
.trim_matches('/')
.to_string();
if value.is_empty() || value.contains('/') {
None
} else {
Some(value)
}
}
pub(crate) fn admin_system_email_template_type_from_path(request_path: &str) -> Option<String> {
let value = request_path
.strip_prefix("/api/admin/system/email/templates/")?
.trim()
.trim_matches('/')
.to_string();
if value.is_empty() || value.contains('/') {
None
} else {
Some(value)
}
}
pub(crate) fn admin_system_email_template_preview_type_from_path(
request_path: &str,
) -> Option<String> {
request_path
.strip_prefix("/api/admin/system/email/templates/")?
.strip_suffix("/preview")
.map(|value| value.trim().trim_matches('/').to_string())
.filter(|value| !value.is_empty() && !value.contains('/'))
}
pub(crate) fn admin_system_email_template_reset_type_from_path(
request_path: &str,
) -> Option<String> {
request_path
.strip_prefix("/api/admin/system/email/templates/")?
.strip_suffix("/reset")
.map(|value| value.trim().trim_matches('/').to_string())
.filter(|value| !value.is_empty() && !value.contains('/'))
}
pub(crate) fn admin_management_token_id_from_path(request_path: &str) -> Option<String> {
let raw = request_path.strip_prefix("/api/admin/management-tokens/")?;
let normalized = raw.trim().trim_matches('/');
if normalized.is_empty() || normalized.contains('/') {
None
} else {
Some(normalized.to_string())
}
}
pub(crate) fn admin_management_token_status_id_from_path(request_path: &str) -> Option<String> {
request_path
.strip_prefix("/api/admin/management-tokens/")?
.strip_suffix("/status")
.map(|value| value.trim().trim_matches('/').to_string())
.filter(|value| !value.is_empty() && !value.contains('/'))
}
pub(crate) fn admin_gemini_file_mapping_id_from_path(request_path: &str) -> Option<String> {
let raw = request_path.strip_prefix("/api/admin/gemini-files/mappings/")?;
let normalized = raw.trim().trim_matches('/');
if normalized.is_empty() || normalized.contains('/') {
None
} else {
Some(normalized.to_string())
}
}
pub(crate) fn admin_provider_id_for_models_list(request_path: &str) -> Option<String> {
admin_provider_id_for_suffix(request_path, "/models")
}
pub(crate) fn admin_provider_id_for_suffix(request_path: &str, suffix: &str) -> Option<String> {
let raw = request_path.strip_prefix("/api/admin/providers/")?;
let raw = raw.strip_suffix(suffix)?;
let normalized = raw.trim().trim_matches('/');
if normalized.is_empty() || normalized.contains('/') {
None
} else {
Some(normalized.to_string())
}
}
pub(crate) fn admin_provider_model_route_parts(request_path: &str) -> Option<(String, String)> {
let raw = request_path.strip_prefix("/api/admin/providers/")?;
let (provider_id, model_id) = raw.split_once("/models/")?;
let provider_id = provider_id.trim().trim_matches('/');
let model_id = model_id.trim().trim_matches('/');
if provider_id.is_empty()
|| model_id.is_empty()
|| provider_id.contains('/')
|| model_id.contains('/')
{
None
} else {
Some((provider_id.to_string(), model_id.to_string()))
}
}
pub(crate) fn admin_provider_models_batch_path(request_path: &str) -> Option<String> {
admin_provider_id_for_suffix(request_path, "/models/batch")
}
pub(crate) fn admin_provider_available_source_models_path(request_path: &str) -> Option<String> {
admin_provider_id_for_suffix(request_path, "/available-source-models")
}
pub(crate) fn admin_provider_assign_global_models_path(request_path: &str) -> Option<String> {
admin_provider_id_for_suffix(request_path, "/assign-global-models")
}
pub(crate) fn admin_provider_import_models_path(request_path: &str) -> Option<String> {
admin_provider_id_for_suffix(request_path, "/import-from-upstream")
}
pub(crate) fn is_admin_global_models_root(request_path: &str) -> bool {
matches!(
request_path,
"/api/admin/models/global" | "/api/admin/models/global/"
)
}
pub(crate) fn admin_global_model_id_from_path(request_path: &str) -> Option<String> {
let raw = request_path.strip_prefix("/api/admin/models/global/")?;
let normalized = raw.trim().trim_matches('/');
if normalized.is_empty()
|| normalized.contains('/')
|| normalized == "batch-delete"
|| normalized.ends_with("/providers")
|| normalized.ends_with("/assign-to-providers")
|| normalized.ends_with("/routing")
{
None
} else {
Some(normalized.to_string())
}
}
pub(crate) fn admin_global_model_assign_to_providers_id(request_path: &str) -> Option<String> {
request_path
.strip_prefix("/api/admin/models/global/")?
.strip_suffix("/assign-to-providers")
.map(|value| value.trim().trim_matches('/').to_string())
.filter(|value| !value.is_empty() && !value.contains('/'))
}
pub(crate) fn admin_global_model_routing_id(request_path: &str) -> Option<String> {
request_path
.strip_prefix("/api/admin/models/global/")?
.strip_suffix("/routing")
.map(|value| value.trim().trim_matches('/').to_string())
.filter(|value| !value.is_empty() && !value.contains('/'))
}
pub(crate) fn admin_global_model_providers_id(request_path: &str) -> Option<String> {
request_path
.strip_prefix("/api/admin/models/global/")?
.strip_suffix("/providers")
.map(|value| value.trim().trim_matches('/').to_string())
.filter(|value| !value.is_empty() && !value.contains('/'))
}
pub(crate) fn admin_reveal_key_id(request_path: &str) -> Option<String> {
request_path
.strip_prefix("/api/admin/endpoints/keys/")?
.strip_suffix("/reveal")
.map(ToOwned::to_owned)
}
pub(crate) fn admin_export_key_id(request_path: &str) -> Option<String> {
request_path
.strip_prefix("/api/admin/endpoints/keys/")?
.strip_suffix("/export")
.map(ToOwned::to_owned)
}
pub(crate) fn admin_clear_oauth_invalid_key_id(request_path: &str) -> Option<String> {
request_path
.strip_prefix("/api/admin/endpoints/keys/")?
.strip_suffix("/clear-oauth-invalid")
.map(ToOwned::to_owned)
}
pub(crate) fn admin_update_key_id(request_path: &str) -> Option<String> {
let key_id = request_path.strip_prefix("/api/admin/endpoints/keys/")?;
(!key_id.is_empty() && !key_id.contains('/')).then_some(key_id.to_string())
}
pub(crate) fn admin_oauth_provider_type_from_path(request_path: &str) -> Option<String> {
let provider_type = request_path.strip_prefix("/api/admin/oauth/providers/")?;
(!provider_type.is_empty() && !provider_type.contains('/')).then_some(provider_type.to_string())
}
pub(crate) fn admin_oauth_test_provider_type_from_path(request_path: &str) -> Option<String> {
request_path
.strip_prefix("/api/admin/oauth/providers/")?
.strip_suffix("/test")
.filter(|provider_type| !provider_type.is_empty() && !provider_type.contains('/'))
.map(ToOwned::to_owned)
}
pub(crate) fn admin_provider_oauth_start_key_id(request_path: &str) -> Option<String> {
request_path
.strip_prefix("/api/admin/provider-oauth/keys/")?
.strip_suffix("/start")
.filter(|key_id| !key_id.is_empty() && !key_id.contains('/'))
.map(ToOwned::to_owned)
}
pub(crate) fn admin_provider_oauth_start_provider_id(request_path: &str) -> Option<String> {
request_path
.strip_prefix("/api/admin/provider-oauth/providers/")?
.strip_suffix("/start")
.filter(|provider_id| !provider_id.is_empty() && !provider_id.contains('/'))
.map(ToOwned::to_owned)
}
pub(crate) fn admin_provider_oauth_complete_key_id(request_path: &str) -> Option<String> {
request_path
.strip_prefix("/api/admin/provider-oauth/keys/")?
.strip_suffix("/complete")
.filter(|key_id| !key_id.is_empty() && !key_id.contains('/'))
.map(ToOwned::to_owned)
}
pub(crate) fn admin_provider_oauth_refresh_key_id(request_path: &str) -> Option<String> {
request_path
.strip_prefix("/api/admin/provider-oauth/keys/")?
.strip_suffix("/refresh")
.filter(|key_id| !key_id.is_empty() && !key_id.contains('/'))
.map(ToOwned::to_owned)
}
pub(crate) fn admin_provider_oauth_complete_provider_id(request_path: &str) -> Option<String> {
request_path
.strip_prefix("/api/admin/provider-oauth/providers/")?
.strip_suffix("/complete")
.filter(|provider_id| !provider_id.is_empty() && !provider_id.contains('/'))
.map(ToOwned::to_owned)
}
pub(crate) fn admin_provider_oauth_import_provider_id(request_path: &str) -> Option<String> {
request_path
.strip_prefix("/api/admin/provider-oauth/providers/")?
.strip_suffix("/import-refresh-token")
.filter(|provider_id| !provider_id.is_empty() && !provider_id.contains('/'))
.map(ToOwned::to_owned)
}
pub(crate) fn admin_provider_oauth_batch_import_provider_id(request_path: &str) -> Option<String> {
request_path
.strip_prefix("/api/admin/provider-oauth/providers/")?
.strip_suffix("/batch-import")
.filter(|provider_id| !provider_id.is_empty() && !provider_id.contains('/'))
.map(ToOwned::to_owned)
}
pub(crate) fn admin_provider_oauth_batch_import_task_provider_id(
request_path: &str,
) -> Option<String> {
request_path
.strip_prefix("/api/admin/provider-oauth/providers/")?
.strip_suffix("/batch-import/tasks")
.filter(|provider_id| !provider_id.is_empty() && !provider_id.contains('/'))
.map(ToOwned::to_owned)
}
pub(crate) fn admin_provider_oauth_batch_import_task_path(
request_path: &str,
) -> Option<(String, String)> {
let suffix = request_path
.strip_prefix("/api/admin/provider-oauth/providers/")?
.strip_suffix("/")
.unwrap_or(request_path.strip_prefix("/api/admin/provider-oauth/providers/")?);
let (provider_id, task_path) = suffix.split_once("/batch-import/tasks/")?;
if provider_id.is_empty()
|| provider_id.contains('/')
|| task_path.is_empty()
|| task_path.contains('/')
{
return None;
}
Some((provider_id.to_string(), task_path.to_string()))
}
pub(crate) fn admin_provider_oauth_device_authorize_provider_id(
request_path: &str,
) -> Option<String> {
request_path
.strip_prefix("/api/admin/provider-oauth/providers/")?
.strip_suffix("/device-authorize")
.filter(|provider_id| !provider_id.is_empty() && !provider_id.contains('/'))
.map(ToOwned::to_owned)
}
pub(crate) fn admin_provider_oauth_device_poll_provider_id(request_path: &str) -> Option<String> {
request_path
.strip_prefix("/api/admin/provider-oauth/providers/")?
.strip_suffix("/device-poll")
.filter(|provider_id| !provider_id.is_empty() && !provider_id.contains('/'))
.map(ToOwned::to_owned)
}

View File

@@ -0,0 +1,509 @@
use super::*;
pub(crate) fn decrypt_catalog_secret_with_fallbacks(
encryption_key: Option<&str>,
ciphertext: &str,
) -> Option<String> {
let encryption_key = encryption_key.map(str::trim).unwrap_or("");
if !encryption_key.is_empty() {
if let Ok(value) = decrypt_python_fernet_ciphertext(encryption_key, ciphertext) {
return Some(value);
}
}
for env_key in ["AETHER_GATEWAY_DATA_ENCRYPTION_KEY", "ENCRYPTION_KEY"] {
let Ok(fallback) = std::env::var(env_key) else {
continue;
};
let fallback = fallback.trim();
if fallback.is_empty() || fallback == encryption_key {
continue;
}
if let Ok(value) = decrypt_python_fernet_ciphertext(fallback, ciphertext) {
return Some(value);
}
}
#[cfg(test)]
if encryption_key != DEVELOPMENT_ENCRYPTION_KEY {
if let Ok(value) = decrypt_python_fernet_ciphertext(DEVELOPMENT_ENCRYPTION_KEY, ciphertext)
{
return Some(value);
}
}
None
}
pub(crate) fn effective_catalog_encryption_key(state: &AppState) -> Option<String> {
let encryption_key = state.encryption_key().map(str::trim).unwrap_or("");
if !encryption_key.is_empty() {
return Some(encryption_key.to_string());
}
for env_key in ["AETHER_GATEWAY_DATA_ENCRYPTION_KEY", "ENCRYPTION_KEY"] {
let Ok(candidate) = std::env::var(env_key) else {
continue;
};
let candidate = candidate.trim();
if !candidate.is_empty() {
return Some(candidate.to_string());
}
}
#[cfg(test)]
{
return Some(DEVELOPMENT_ENCRYPTION_KEY.to_string());
}
#[allow(unreachable_code)]
None
}
pub(crate) fn encrypt_catalog_secret_with_fallbacks(
state: &AppState,
plaintext: &str,
) -> Option<String> {
let encryption_key = effective_catalog_encryption_key(state)?;
encrypt_python_fernet_plaintext(&encryption_key, plaintext).ok()
}
pub(crate) fn masked_catalog_api_key(state: &AppState, key: &StoredProviderCatalogKey) -> String {
match key.auth_type.trim() {
"service_account" | "vertex_ai" => "[Service Account]".to_string(),
"oauth" => "[OAuth Token]".to_string(),
_ => decrypt_catalog_secret_with_fallbacks(state.encryption_key(), &key.encrypted_api_key)
.map(|value| {
if value.len() <= 12 {
format!("{value}***")
} else {
format!(
"{}***{}",
&value[..8],
&value[value.len().saturating_sub(4)..]
)
}
})
.unwrap_or_else(|| "***ERROR***".to_string()),
}
}
pub(crate) fn parse_catalog_auth_config_json(
state: &AppState,
key: &StoredProviderCatalogKey,
) -> Option<serde_json::Map<String, serde_json::Value>> {
let ciphertext = key.encrypted_auth_config.as_deref()?.trim();
if ciphertext.is_empty() {
return None;
}
let plaintext = decrypt_catalog_secret_with_fallbacks(state.encryption_key(), ciphertext)?;
serde_json::from_str::<serde_json::Value>(&plaintext)
.ok()?
.as_object()
.cloned()
}
pub(crate) fn default_provider_key_status_snapshot() -> serde_json::Value {
json!({
"oauth": {
"code": "none",
"label": serde_json::Value::Null,
"reason": serde_json::Value::Null,
"expires_at": serde_json::Value::Null,
"invalid_at": serde_json::Value::Null,
"source": serde_json::Value::Null,
"requires_reauth": false,
"expiring_soon": false,
},
"account": {
"code": "ok",
"label": serde_json::Value::Null,
"reason": serde_json::Value::Null,
"blocked": false,
"source": serde_json::Value::Null,
"recoverable": false,
},
"quota": {
"code": "unknown",
"label": serde_json::Value::Null,
"reason": serde_json::Value::Null,
"exhausted": false,
"usage_ratio": serde_json::Value::Null,
"updated_at": serde_json::Value::Null,
"reset_seconds": serde_json::Value::Null,
"plan_type": serde_json::Value::Null,
}
})
}
pub(crate) fn provider_key_status_snapshot_payload(
key: &StoredProviderCatalogKey,
) -> serde_json::Value {
key.status_snapshot
.clone()
.filter(|value| value.is_object())
.unwrap_or_else(default_provider_key_status_snapshot)
}
pub(crate) fn provider_key_health_summary(
key: &StoredProviderCatalogKey,
) -> (
f64,
i64,
Option<String>,
bool,
serde_json::Map<String, serde_json::Value>,
) {
let health_by_format = key
.health_by_format
.as_ref()
.and_then(serde_json::Value::as_object)
.cloned()
.unwrap_or_default();
let circuit_by_format = key
.circuit_breaker_by_format
.as_ref()
.and_then(serde_json::Value::as_object)
.cloned()
.unwrap_or_default();
let mut min_health_score = 1.0_f64;
let mut max_consecutive = 0_i64;
let mut last_failure_at: Option<String> = None;
for value in health_by_format.values() {
let score = value
.get("health_score")
.and_then(serde_json::Value::as_f64)
.unwrap_or(1.0);
min_health_score = min_health_score.min(score);
let consecutive = value
.get("consecutive_failures")
.and_then(serde_json::Value::as_i64)
.unwrap_or(0);
max_consecutive = max_consecutive.max(consecutive);
if let Some(last_failure) = value
.get("last_failure_at")
.and_then(serde_json::Value::as_str)
.map(ToOwned::to_owned)
{
if last_failure_at
.as_ref()
.is_none_or(|current| last_failure > *current)
{
last_failure_at = Some(last_failure);
}
}
}
let any_circuit_open = circuit_by_format.values().any(|value| {
value
.get("open")
.and_then(serde_json::Value::as_bool)
.unwrap_or(false)
});
(
if health_by_format.is_empty() {
1.0
} else {
min_health_score
},
max_consecutive,
last_failure_at,
any_circuit_open,
circuit_by_format,
)
}
pub(crate) fn build_admin_provider_key_response(
state: &AppState,
key: &StoredProviderCatalogKey,
now_unix_secs: u64,
) -> serde_json::Value {
let request_count = u64::from(key.request_count.unwrap_or(0));
let success_count = u64::from(key.success_count.unwrap_or(0));
let error_count = u64::from(key.error_count.unwrap_or(0));
let total_response_time_ms = f64::from(key.total_response_time_ms.unwrap_or(0));
let success_rate = if request_count > 0 {
success_count as f64 / request_count as f64
} else {
0.0
};
let avg_response_time_ms = if success_count > 0 {
total_response_time_ms / success_count as f64
} else {
0.0
};
let auth_config = parse_catalog_auth_config_json(state, key);
let oauth_organizations = auth_config
.as_ref()
.and_then(|config| config.get("organizations"))
.and_then(serde_json::Value::as_array)
.cloned()
.unwrap_or_default();
let oauth_plan_type = auth_config
.as_ref()
.and_then(|config| config.get("plan_type").and_then(serde_json::Value::as_str))
.map(ToOwned::to_owned)
.or_else(|| {
auth_config
.as_ref()
.and_then(|config| config.get("tier").and_then(serde_json::Value::as_str))
.map(|value| value.to_ascii_lowercase())
});
let (
health_score,
consecutive_failures,
last_failure_at,
circuit_breaker_open,
circuit_by_format,
) = provider_key_health_summary(key);
let circuit_sample = circuit_by_format
.values()
.find(|value| {
value
.get("open")
.and_then(serde_json::Value::as_bool)
.unwrap_or(false)
})
.or_else(|| circuit_by_format.values().next());
let is_adaptive = key.rpm_limit.is_none();
let effective_limit = if is_adaptive {
key.learned_rpm_limit
} else {
key.rpm_limit
};
let mut payload = serde_json::Map::new();
payload.insert("id".to_string(), json!(key.id));
payload.insert("provider_id".to_string(), json!(key.provider_id));
payload.insert(
"api_formats".to_string(),
serde_json::Value::Array(
json_string_list(key.api_formats.as_ref())
.into_iter()
.map(serde_json::Value::String)
.collect(),
),
);
payload.insert(
"api_key_masked".to_string(),
json!(masked_catalog_api_key(state, key)),
);
payload.insert("api_key_plain".to_string(), serde_json::Value::Null);
payload.insert("auth_type".to_string(), json!(key.auth_type));
payload.insert("name".to_string(), json!(key.name));
payload.insert("rate_multipliers".to_string(), json!(key.rate_multipliers));
payload.insert(
"internal_priority".to_string(),
json!(key.internal_priority),
);
payload.insert(
"global_priority_by_format".to_string(),
json!(key.global_priority_by_format),
);
payload.insert("rpm_limit".to_string(), json!(key.rpm_limit));
payload.insert(
"allowed_models".to_string(),
serde_json::Value::Array(
json_string_list(key.allowed_models.as_ref())
.into_iter()
.map(serde_json::Value::String)
.collect(),
),
);
payload.insert("capabilities".to_string(), json!(key.capabilities));
payload.insert(
"oauth_expires_at".to_string(),
json!(key.expires_at_unix_secs),
);
payload.insert(
"oauth_email".to_string(),
auth_config
.as_ref()
.and_then(|config| config.get("email"))
.cloned()
.unwrap_or(serde_json::Value::Null),
);
payload.insert("oauth_plan_type".to_string(), json!(oauth_plan_type));
payload.insert(
"oauth_account_id".to_string(),
auth_config
.as_ref()
.and_then(|config| config.get("account_id"))
.cloned()
.unwrap_or(serde_json::Value::Null),
);
payload.insert(
"oauth_account_name".to_string(),
auth_config
.as_ref()
.and_then(|config| config.get("account_name"))
.cloned()
.unwrap_or(serde_json::Value::Null),
);
payload.insert(
"oauth_account_user_id".to_string(),
auth_config
.as_ref()
.and_then(|config| config.get("account_user_id"))
.cloned()
.unwrap_or(serde_json::Value::Null),
);
payload.insert(
"oauth_organizations".to_string(),
serde_json::Value::Array(oauth_organizations),
);
payload.insert(
"oauth_invalid_at".to_string(),
json!(key.oauth_invalid_at_unix_secs),
);
payload.insert(
"oauth_invalid_reason".to_string(),
json!(key.oauth_invalid_reason),
);
payload.insert(
"status_snapshot".to_string(),
provider_key_status_snapshot_payload(key),
);
payload.insert(
"cache_ttl_minutes".to_string(),
json!(key.cache_ttl_minutes),
);
payload.insert(
"max_probe_interval_minutes".to_string(),
json!(key.max_probe_interval_minutes),
);
payload.insert("health_by_format".to_string(), json!(key.health_by_format));
payload.insert(
"circuit_breaker_by_format".to_string(),
json!(key.circuit_breaker_by_format),
);
payload.insert("health_score".to_string(), json!(health_score));
payload.insert(
"consecutive_failures".to_string(),
json!(consecutive_failures),
);
payload.insert("last_failure_at".to_string(), json!(last_failure_at));
payload.insert(
"circuit_breaker_open".to_string(),
json!(circuit_breaker_open),
);
payload.insert(
"circuit_breaker_open_at".to_string(),
circuit_sample
.and_then(|value| value.get("open_at"))
.cloned()
.unwrap_or(serde_json::Value::Null),
);
payload.insert(
"next_probe_at".to_string(),
circuit_sample
.and_then(|value| value.get("next_probe_at"))
.cloned()
.unwrap_or(serde_json::Value::Null),
);
payload.insert(
"half_open_until".to_string(),
circuit_sample
.and_then(|value| value.get("half_open_until"))
.cloned()
.unwrap_or(serde_json::Value::Null),
);
payload.insert(
"half_open_successes".to_string(),
json!(circuit_sample
.and_then(|value| value.get("half_open_successes"))
.and_then(serde_json::Value::as_i64)
.unwrap_or(0)),
);
payload.insert(
"half_open_failures".to_string(),
json!(circuit_sample
.and_then(|value| value.get("half_open_failures"))
.and_then(serde_json::Value::as_i64)
.unwrap_or(0)),
);
payload.insert(
"request_results_window".to_string(),
circuit_sample
.and_then(|value| value.get("request_results_window"))
.cloned()
.unwrap_or(serde_json::Value::Null),
);
payload.insert("request_count".to_string(), json!(request_count));
payload.insert("success_count".to_string(), json!(success_count));
payload.insert("error_count".to_string(), json!(error_count));
payload.insert("success_rate".to_string(), json!(success_rate));
payload.insert(
"avg_response_time_ms".to_string(),
json!(avg_response_time_ms),
);
payload.insert("is_active".to_string(), json!(key.is_active));
payload.insert("is_adaptive".to_string(), json!(is_adaptive));
payload.insert(
"learned_rpm_limit".to_string(),
json!(key.learned_rpm_limit),
);
payload.insert("effective_limit".to_string(), json!(effective_limit));
payload.insert(
"utilization_samples".to_string(),
json!(key.utilization_samples),
);
payload.insert(
"last_probe_increase_at".to_string(),
json!(key
.last_probe_increase_at_unix_secs
.and_then(unix_secs_to_rfc3339)),
);
payload.insert(
"concurrent_429_count".to_string(),
json!(key.concurrent_429_count),
);
payload.insert("rpm_429_count".to_string(), json!(key.rpm_429_count));
payload.insert(
"last_429_at".to_string(),
json!(key.last_429_at_unix_secs.and_then(unix_secs_to_rfc3339)),
);
payload.insert("last_429_type".to_string(), json!(key.last_429_type));
payload.insert("note".to_string(), json!(key.note));
payload.insert(
"auto_fetch_models".to_string(),
json!(key.auto_fetch_models),
);
payload.insert(
"last_models_fetch_at".to_string(),
json!(key
.last_models_fetch_at_unix_secs
.and_then(unix_secs_to_rfc3339)),
);
payload.insert(
"last_models_fetch_error".to_string(),
json!(key.last_models_fetch_error),
);
payload.insert("locked_models".to_string(), json!(key.locked_models));
payload.insert(
"model_include_patterns".to_string(),
json!(key.model_include_patterns),
);
payload.insert(
"model_exclude_patterns".to_string(),
json!(key.model_exclude_patterns),
);
payload.insert(
"upstream_metadata".to_string(),
json!(key.upstream_metadata),
);
payload.insert("proxy".to_string(), json!(key.proxy));
payload.insert("fingerprint".to_string(), json!(key.fingerprint));
payload.insert(
"last_used_at".to_string(),
json!(key.last_used_at_unix_secs.and_then(unix_secs_to_rfc3339)),
);
payload.insert(
"created_at".to_string(),
json!(unix_secs_to_rfc3339(
key.created_at_unix_secs.unwrap_or(now_unix_secs)
)),
);
payload.insert(
"updated_at".to_string(),
json!(unix_secs_to_rfc3339(
key.updated_at_unix_secs.unwrap_or(now_unix_secs)
)),
);
serde_json::Value::Object(payload)
}

View File

@@ -0,0 +1,452 @@
use super::*;
#[derive(Debug, Deserialize)]
pub(crate) struct AdminProviderKeyCreateRequest {
#[serde(default)]
pub(crate) api_formats: Option<Vec<String>>,
#[serde(default)]
pub(crate) api_key: Option<String>,
#[serde(default)]
pub(crate) auth_type: Option<String>,
#[serde(default)]
pub(crate) auth_config: Option<serde_json::Value>,
pub(crate) name: String,
#[serde(default)]
pub(crate) rate_multipliers: Option<serde_json::Value>,
#[serde(default)]
pub(crate) internal_priority: Option<i32>,
#[serde(default)]
pub(crate) rpm_limit: Option<u32>,
#[serde(default)]
pub(crate) allowed_models: Option<Vec<String>>,
#[serde(default)]
pub(crate) capabilities: Option<serde_json::Value>,
#[serde(default)]
pub(crate) cache_ttl_minutes: Option<i32>,
#[serde(default)]
pub(crate) max_probe_interval_minutes: Option<i32>,
#[serde(default)]
pub(crate) note: Option<String>,
#[serde(default)]
pub(crate) auto_fetch_models: Option<bool>,
#[serde(default)]
pub(crate) locked_models: Option<Vec<String>>,
#[serde(default)]
pub(crate) model_include_patterns: Option<Vec<String>>,
#[serde(default)]
pub(crate) model_exclude_patterns: Option<Vec<String>>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct AdminProviderKeyUpdateRequest {
#[serde(default)]
pub(crate) api_formats: Option<Vec<String>>,
#[serde(default)]
pub(crate) api_key: Option<String>,
#[serde(default)]
pub(crate) auth_type: Option<String>,
#[serde(default)]
pub(crate) auth_config: Option<serde_json::Value>,
#[serde(default)]
pub(crate) name: Option<String>,
#[serde(default)]
pub(crate) rate_multipliers: Option<serde_json::Value>,
#[serde(default)]
pub(crate) internal_priority: Option<i32>,
#[serde(default)]
pub(crate) global_priority_by_format: Option<serde_json::Value>,
#[serde(default)]
pub(crate) rpm_limit: Option<u32>,
#[serde(default)]
pub(crate) allowed_models: Option<Vec<String>>,
#[serde(default)]
pub(crate) capabilities: Option<serde_json::Value>,
#[serde(default)]
pub(crate) cache_ttl_minutes: Option<i32>,
#[serde(default)]
pub(crate) max_probe_interval_minutes: Option<i32>,
#[serde(default)]
pub(crate) is_active: Option<bool>,
#[serde(default)]
pub(crate) note: Option<String>,
#[serde(default)]
pub(crate) auto_fetch_models: Option<bool>,
#[serde(default)]
pub(crate) locked_models: Option<Vec<String>>,
#[serde(default)]
pub(crate) model_include_patterns: Option<Vec<String>>,
#[serde(default)]
pub(crate) model_exclude_patterns: Option<Vec<String>>,
#[serde(default)]
pub(crate) proxy: Option<serde_json::Value>,
#[serde(default)]
pub(crate) fingerprint: Option<serde_json::Value>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct AdminProviderKeyBatchDeleteRequest {
pub(crate) ids: Vec<String>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct AdminProviderQuotaRefreshRequest {
#[serde(default)]
pub(crate) key_ids: Option<Vec<String>>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct AdminOAuthProviderUpsertRequest {
pub(crate) display_name: String,
pub(crate) client_id: String,
#[serde(default)]
pub(crate) client_secret: Option<String>,
#[serde(default)]
pub(crate) authorization_url_override: Option<String>,
#[serde(default)]
pub(crate) token_url_override: Option<String>,
#[serde(default)]
pub(crate) userinfo_url_override: Option<String>,
#[serde(default)]
pub(crate) scopes: Option<Vec<String>>,
pub(crate) redirect_uri: String,
pub(crate) frontend_callback_url: String,
#[serde(default)]
pub(crate) attribute_mapping: Option<serde_json::Value>,
#[serde(default)]
pub(crate) extra_config: Option<serde_json::Value>,
#[serde(default)]
pub(crate) is_enabled: bool,
#[serde(default)]
pub(crate) force: bool,
}
#[derive(Debug, Deserialize)]
pub(crate) struct InternalTunnelHeartbeatRequest {
pub(crate) node_id: String,
#[serde(default)]
pub(crate) heartbeat_interval: Option<i32>,
#[serde(default)]
pub(crate) active_connections: Option<i32>,
#[serde(default)]
pub(crate) total_requests: Option<i64>,
#[serde(default)]
pub(crate) avg_latency_ms: Option<f64>,
#[serde(default)]
pub(crate) failed_requests: Option<i64>,
#[serde(default)]
pub(crate) dns_failures: Option<i64>,
#[serde(default)]
pub(crate) stream_errors: Option<i64>,
#[serde(default)]
pub(crate) proxy_metadata: Option<serde_json::Value>,
#[serde(default)]
pub(crate) proxy_version: Option<String>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct InternalTunnelNodeStatusRequest {
pub(crate) node_id: String,
pub(crate) connected: bool,
#[serde(default)]
pub(crate) conn_count: i32,
}
#[derive(Debug, Deserialize)]
pub(crate) struct LegacyGatewayResolveRequest {
#[serde(default)]
pub(crate) trace_id: Option<String>,
pub(crate) method: String,
pub(crate) path: String,
#[serde(default)]
pub(crate) query_string: Option<String>,
#[serde(default)]
pub(crate) headers: BTreeMap<String, String>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct LegacyGatewayAuthContextRequest {
#[serde(default)]
pub(crate) trace_id: Option<String>,
#[serde(default)]
pub(crate) query_string: Option<String>,
#[serde(default)]
pub(crate) headers: BTreeMap<String, String>,
pub(crate) auth_endpoint_signature: String,
}
#[derive(Debug, Deserialize)]
pub(crate) struct LegacyGatewayExecuteRequest {
#[serde(default)]
pub(crate) trace_id: Option<String>,
pub(crate) method: String,
pub(crate) path: String,
#[serde(default)]
pub(crate) query_string: Option<String>,
#[serde(default)]
pub(crate) headers: BTreeMap<String, String>,
#[serde(default)]
pub(crate) body_json: serde_json::Value,
#[serde(default)]
pub(crate) body_base64: Option<String>,
#[serde(default)]
pub(crate) auth_context: Option<crate::gateway::GatewayControlAuthContext>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct AdminProviderCreateRequest {
pub(crate) name: String,
#[serde(default)]
pub(crate) provider_type: Option<String>,
#[serde(default)]
pub(crate) description: Option<String>,
#[serde(default)]
pub(crate) website: Option<String>,
#[serde(default)]
pub(crate) billing_type: Option<String>,
#[serde(default)]
pub(crate) monthly_quota_usd: Option<f64>,
#[serde(default)]
pub(crate) quota_reset_day: Option<u64>,
#[serde(default)]
pub(crate) quota_last_reset_at: Option<String>,
#[serde(default)]
pub(crate) quota_expires_at: Option<String>,
#[serde(default)]
pub(crate) provider_priority: Option<i32>,
#[serde(default)]
pub(crate) keep_priority_on_conversion: Option<bool>,
#[serde(default)]
pub(crate) is_active: Option<bool>,
#[serde(default)]
pub(crate) concurrent_limit: Option<i32>,
#[serde(default)]
pub(crate) max_retries: Option<i32>,
#[serde(default)]
pub(crate) proxy: Option<serde_json::Value>,
#[serde(default)]
pub(crate) stream_first_byte_timeout: Option<f64>,
#[serde(default)]
pub(crate) request_timeout: Option<f64>,
#[serde(default)]
pub(crate) pool_advanced: Option<serde_json::Value>,
#[serde(default)]
pub(crate) claude_code_advanced: Option<serde_json::Value>,
#[serde(default)]
pub(crate) failover_rules: Option<serde_json::Value>,
#[serde(default)]
pub(crate) config: Option<serde_json::Value>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct AdminProviderUpdateRequest {
#[serde(default)]
pub(crate) name: Option<String>,
#[serde(default)]
pub(crate) provider_type: Option<String>,
#[serde(default)]
pub(crate) description: Option<String>,
#[serde(default)]
pub(crate) website: Option<String>,
#[serde(default)]
pub(crate) billing_type: Option<String>,
#[serde(default)]
pub(crate) monthly_quota_usd: Option<f64>,
#[serde(default)]
pub(crate) quota_reset_day: Option<u64>,
#[serde(default)]
pub(crate) quota_last_reset_at: Option<String>,
#[serde(default)]
pub(crate) quota_expires_at: Option<String>,
#[serde(default)]
pub(crate) provider_priority: Option<i32>,
#[serde(default)]
pub(crate) keep_priority_on_conversion: Option<bool>,
#[serde(default)]
pub(crate) is_active: Option<bool>,
#[serde(default)]
pub(crate) concurrent_limit: Option<i32>,
#[serde(default)]
pub(crate) max_retries: Option<i32>,
#[serde(default)]
pub(crate) proxy: Option<serde_json::Value>,
#[serde(default)]
pub(crate) stream_first_byte_timeout: Option<f64>,
#[serde(default)]
pub(crate) request_timeout: Option<f64>,
#[serde(default)]
pub(crate) pool_advanced: Option<serde_json::Value>,
#[serde(default)]
pub(crate) claude_code_advanced: Option<serde_json::Value>,
#[serde(default)]
pub(crate) failover_rules: Option<serde_json::Value>,
#[serde(default)]
pub(crate) enable_format_conversion: Option<bool>,
#[serde(default)]
pub(crate) config: Option<serde_json::Value>,
}
pub(crate) const CODEX_WHAM_USAGE_URL: &str = "https://chatgpt.com/backend-api/wham/usage";
pub(crate) const KIRO_USAGE_LIMITS_PATH: &str = "/getUsageLimits";
pub(crate) const KIRO_USAGE_SDK_VERSION: &str = "1.0.0";
pub(crate) const ANTIGRAVITY_FETCH_AVAILABLE_MODELS_PATH: &str = "/v1internal:fetchAvailableModels";
pub(crate) const OAUTH_ACCOUNT_BLOCK_PREFIX: &str = "[ACCOUNT_BLOCK] ";
pub(crate) const OAUTH_REFRESH_FAILED_PREFIX: &str = "[REFRESH_FAILED] ";
pub(crate) const OAUTH_EXPIRED_PREFIX: &str = "[OAUTH_EXPIRED] ";
pub(crate) const OAUTH_REQUEST_FAILED_PREFIX: &str = "[REQUEST_FAILED] ";
pub(crate) fn default_admin_endpoint_max_retries() -> i32 {
2
}
#[derive(Debug, Deserialize)]
pub(crate) struct AdminProviderEndpointCreateRequest {
pub(crate) provider_id: String,
pub(crate) api_format: String,
pub(crate) base_url: String,
#[serde(default)]
pub(crate) custom_path: Option<String>,
#[serde(default)]
pub(crate) header_rules: Option<serde_json::Value>,
#[serde(default)]
pub(crate) body_rules: Option<serde_json::Value>,
#[serde(default = "default_admin_endpoint_max_retries")]
pub(crate) max_retries: i32,
#[serde(default)]
pub(crate) config: Option<serde_json::Value>,
#[serde(default)]
pub(crate) proxy: Option<serde_json::Value>,
#[serde(default)]
pub(crate) format_acceptance_config: Option<serde_json::Value>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct AdminProviderEndpointUpdateRequest {
#[serde(default)]
pub(crate) base_url: Option<String>,
#[serde(default)]
pub(crate) custom_path: Option<String>,
#[serde(default)]
pub(crate) header_rules: Option<serde_json::Value>,
#[serde(default)]
pub(crate) body_rules: Option<serde_json::Value>,
#[serde(default)]
pub(crate) max_retries: Option<i32>,
#[serde(default)]
pub(crate) is_active: Option<bool>,
#[serde(default)]
pub(crate) config: Option<serde_json::Value>,
#[serde(default)]
pub(crate) proxy: Option<serde_json::Value>,
#[serde(default)]
pub(crate) format_acceptance_config: Option<serde_json::Value>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct AdminProviderModelCreateRequest {
pub(crate) provider_model_name: String,
#[serde(default)]
pub(crate) provider_model_mappings: Option<serde_json::Value>,
pub(crate) global_model_id: String,
#[serde(default)]
pub(crate) price_per_request: Option<f64>,
#[serde(default)]
pub(crate) tiered_pricing: Option<serde_json::Value>,
#[serde(default)]
pub(crate) supports_vision: Option<bool>,
#[serde(default)]
pub(crate) supports_function_calling: Option<bool>,
#[serde(default)]
pub(crate) supports_streaming: Option<bool>,
#[serde(default)]
pub(crate) supports_extended_thinking: Option<bool>,
#[serde(default)]
pub(crate) is_active: Option<bool>,
#[serde(default)]
pub(crate) config: Option<serde_json::Value>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct AdminProviderModelUpdateRequest {
#[serde(default)]
pub(crate) provider_model_name: Option<String>,
#[serde(default)]
pub(crate) provider_model_mappings: Option<serde_json::Value>,
#[serde(default)]
pub(crate) global_model_id: Option<String>,
#[serde(default)]
pub(crate) price_per_request: Option<f64>,
#[serde(default)]
pub(crate) tiered_pricing: Option<serde_json::Value>,
#[serde(default)]
pub(crate) supports_vision: Option<bool>,
#[serde(default)]
pub(crate) supports_function_calling: Option<bool>,
#[serde(default)]
pub(crate) supports_streaming: Option<bool>,
#[serde(default)]
pub(crate) supports_extended_thinking: Option<bool>,
#[serde(default)]
pub(crate) is_active: Option<bool>,
#[serde(default)]
pub(crate) is_available: Option<bool>,
#[serde(default)]
pub(crate) config: Option<serde_json::Value>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct AdminGlobalModelCreateRequest {
pub(crate) name: String,
pub(crate) display_name: String,
#[serde(default)]
pub(crate) default_price_per_request: Option<f64>,
#[serde(default)]
pub(crate) default_tiered_pricing: Option<serde_json::Value>,
#[serde(default)]
pub(crate) supported_capabilities: Option<Vec<String>>,
#[serde(default)]
pub(crate) config: Option<serde_json::Value>,
#[serde(default)]
pub(crate) is_active: Option<bool>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct AdminGlobalModelUpdateRequest {
#[serde(default)]
pub(crate) display_name: Option<String>,
#[serde(default)]
pub(crate) is_active: Option<bool>,
#[serde(default)]
pub(crate) default_price_per_request: Option<f64>,
#[serde(default)]
pub(crate) default_tiered_pricing: Option<serde_json::Value>,
#[serde(default)]
pub(crate) supported_capabilities: Option<Vec<String>>,
#[serde(default)]
pub(crate) config: Option<serde_json::Value>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct AdminBatchDeleteIdsRequest {
pub(crate) ids: Vec<String>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct AdminBatchAssignToProvidersRequest {
pub(crate) provider_ids: Vec<String>,
#[serde(default)]
pub(crate) create_models: Option<bool>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct AdminBatchAssignGlobalModelsRequest {
pub(crate) global_model_ids: Vec<String>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct AdminImportProviderModelsRequest {
pub(crate) model_ids: Vec<String>,
#[serde(default)]
pub(crate) tiered_pricing: Option<serde_json::Value>,
#[serde(default)]
pub(crate) price_per_request: Option<f64>,
}

View File

@@ -0,0 +1,437 @@
use super::*;
pub(crate) fn rust_auth_terminates_provider_credentials(
decision: Option<&GatewayControlDecision>,
) -> bool {
decision.is_some_and(|decision| {
decision.route_class.as_deref() == Some("ai_public") && decision.auth_context.is_some()
})
}
pub(crate) fn mark_external_models_official_providers(
value: &serde_json::Value,
) -> Option<serde_json::Value> {
let providers = value.as_object()?;
Some(serde_json::Value::Object(
providers
.iter()
.map(|(provider_id, provider_value)| {
let updated = match provider_value.as_object() {
Some(object) => {
let mut cloned = object.clone();
cloned.insert(
"official".to_string(),
serde_json::Value::Bool(
OFFICIAL_EXTERNAL_MODEL_PROVIDERS
.iter()
.any(|value| value == provider_id),
),
);
serde_json::Value::Object(cloned)
}
None => provider_value.clone(),
};
(provider_id.clone(), updated)
})
.collect(),
))
}
pub(crate) fn should_strip_forwarded_provider_credential_header(
decision: Option<&GatewayControlDecision>,
header_name: &HeaderName,
) -> bool {
if !rust_auth_terminates_provider_credentials(decision) {
return false;
}
matches!(
header_name.as_str(),
"authorization" | "x-api-key" | "api-key" | "x-goog-api-key"
)
}
pub(crate) fn should_strip_forwarded_trusted_admin_header(
decision: Option<&GatewayControlDecision>,
header_name: &HeaderName,
) -> bool {
let Some(decision) = decision else {
return false;
};
if decision.route_class.as_deref() != Some("admin_proxy") {
return false;
}
matches!(
header_name.as_str(),
TRUSTED_ADMIN_USER_ID_HEADER
| TRUSTED_ADMIN_USER_ROLE_HEADER
| TRUSTED_ADMIN_SESSION_ID_HEADER
| TRUSTED_ADMIN_MANAGEMENT_TOKEN_ID_HEADER
)
}
pub(crate) fn sanitize_upstream_path_and_query(
decision: Option<&GatewayControlDecision>,
default_path_and_query: &str,
) -> String {
let base = decision
.map(GatewayControlDecision::proxy_path_and_query)
.unwrap_or_else(|| default_path_and_query.to_string());
let Some(decision) = decision else {
return base;
};
if !rust_auth_terminates_provider_credentials(Some(decision))
|| decision.route_family.as_deref() != Some("gemini")
{
return base;
}
strip_query_param(&base, "key")
}
pub(crate) fn strip_query_param(path_and_query: &str, key_to_strip: &str) -> String {
let Some((path, query)) = path_and_query.split_once('?') else {
return path_and_query.to_string();
};
let mut serializer = form_urlencoded::Serializer::new(String::new());
let mut kept_any = false;
for (key, value) in form_urlencoded::parse(query.as_bytes()) {
if key == key_to_strip {
continue;
}
serializer.append_pair(key.as_ref(), value.as_ref());
kept_any = true;
}
if !kept_any {
return path.to_string();
}
format!("{path}?{}", serializer.finish())
}
pub(crate) fn query_param_bool(query: Option<&str>, key: &str, default: bool) -> bool {
let Some(query) = query else {
return default;
};
for (entry_key, value) in form_urlencoded::parse(query.as_bytes()) {
if entry_key == key {
let normalized = value.trim().to_ascii_lowercase();
return matches!(normalized.as_str(), "1" | "true" | "yes" | "on");
}
}
default
}
pub(crate) fn query_param_optional_bool(query: Option<&str>, key: &str) -> Option<bool> {
let query = query?;
for (entry_key, value) in form_urlencoded::parse(query.as_bytes()) {
if entry_key == key {
let normalized = value.trim().to_ascii_lowercase();
return match normalized.as_str() {
"1" | "true" | "yes" | "on" => Some(true),
"0" | "false" | "no" | "off" => Some(false),
_ => None,
};
}
}
None
}
pub(crate) fn query_param_value(query: Option<&str>, key: &str) -> Option<String> {
let query = query?;
for (entry_key, value) in form_urlencoded::parse(query.as_bytes()) {
if entry_key == key {
let value = value.trim();
if !value.is_empty() {
return Some(value.to_string());
}
}
}
None
}
pub(crate) fn request_enables_control_execute(headers: &http::HeaderMap) -> bool {
[
CONTROL_EXECUTE_FALLBACK_HEADER,
LEGACY_INTERNAL_GATEWAY_HEADER,
]
.into_iter()
.any(|header| {
header_value_str(headers, header).is_some_and(|value| {
matches!(
value.to_ascii_lowercase().as_str(),
"1" | "true" | "yes" | "on"
)
})
})
}
pub(crate) fn unix_secs_to_rfc3339(unix_secs: u64) -> Option<String> {
let timestamp = i64::try_from(unix_secs).ok()?;
Some(
chrono::DateTime::<Utc>::from_timestamp(timestamp, 0)?
.to_rfc3339_opts(SecondsFormat::Secs, true),
)
}
pub(crate) fn json_string_list(value: Option<&serde_json::Value>) -> Vec<String> {
value
.and_then(serde_json::Value::as_array)
.into_iter()
.flatten()
.filter_map(serde_json::Value::as_str)
.map(ToOwned::to_owned)
.collect()
}
pub(crate) fn admin_proxy_local_requires_buffered_body(
request_context: &GatewayPublicRequestContext,
) -> bool {
request_context
.control_decision
.as_ref()
.is_some_and(|decision| {
if decision.route_class.as_deref() != Some("admin_proxy") {
return false;
}
match (
decision.route_family.as_deref(),
request_context.request_method.clone(),
decision.route_kind.as_deref(),
) {
(Some("endpoints_manage"), http::Method::POST, Some("create_provider_key"))
| (Some("endpoints_manage"), http::Method::POST, Some("create_endpoint"))
| (Some("endpoints_manage"), http::Method::POST, Some("batch_delete_keys"))
| (Some("endpoints_manage"), http::Method::PUT, Some("update_key"))
| (Some("endpoints_manage"), http::Method::PUT, Some("update_endpoint"))
| (Some("modules_manage"), http::Method::PUT, Some("set_enabled"))
| (Some("oauth_manage"), http::Method::PUT, Some("upsert_provider"))
| (Some("oauth_manage"), http::Method::POST, Some("test_provider"))
| (Some("provider_oauth_manage"), http::Method::POST, Some("complete_key_oauth"))
| (
Some("provider_oauth_manage"),
http::Method::POST,
Some("complete_provider_oauth"),
)
| (
Some("provider_oauth_manage"),
http::Method::POST,
Some("import_refresh_token"),
)
| (Some("provider_oauth_manage"), http::Method::POST, Some("batch_import_oauth"))
| (
Some("provider_oauth_manage"),
http::Method::POST,
Some("start_batch_import_oauth_task"),
)
| (Some("provider_oauth_manage"), http::Method::POST, Some("device_authorize"))
| (Some("provider_oauth_manage"), http::Method::POST, Some("device_poll"))
| (Some("system_manage"), http::Method::PUT, Some("settings_set"))
| (Some("system_manage"), http::Method::PUT, Some("config_set"))
| (Some("system_manage"), http::Method::PUT, Some("email_template_set"))
| (Some("system_manage"), http::Method::POST, Some("email_template_preview"))
| (
Some("provider_models_manage"),
http::Method::POST,
Some("create_provider_model"),
)
| (
Some("provider_models_manage"),
http::Method::PATCH,
Some("update_provider_model"),
)
| (
Some("provider_models_manage"),
http::Method::POST,
Some("batch_create_provider_models"),
)
| (
Some("provider_models_manage"),
http::Method::POST,
Some("assign_global_models"),
)
| (
Some("provider_models_manage"),
http::Method::POST,
Some("import_from_upstream"),
)
| (
Some("provider_ops_manage"),
http::Method::POST,
Some("execute_provider_action"),
)
| (Some("provider_ops_manage"), http::Method::POST, Some("batch_balance"))
| (Some("provider_ops_manage"), http::Method::POST, Some("connect_provider"))
| (Some("provider_ops_manage"), http::Method::POST, Some("verify_provider"))
| (Some("provider_ops_manage"), http::Method::PUT, Some("save_provider_config"))
| (Some("announcements_manage"), http::Method::POST, Some("create_announcement"))
| (Some("announcements_manage"), http::Method::PUT, Some("update_announcement"))
| (
Some("provider_strategy_manage"),
http::Method::PUT,
Some("update_provider_billing"),
)
| (
Some("provider_query_manage"),
http::Method::POST,
Some("query_models" | "test_model" | "test_model_failover"),
)
| (Some("billing_manage"), http::Method::POST, Some("apply_preset"))
| (Some("billing_manage"), http::Method::POST, Some("create_rule"))
| (Some("billing_manage"), http::Method::PUT, Some("update_rule"))
| (Some("billing_manage"), http::Method::POST, Some("create_collector"))
| (Some("billing_manage"), http::Method::PUT, Some("update_collector"))
| (Some("payments_manage"), http::Method::POST, Some("credit_order"))
| (Some("api_keys_manage"), http::Method::POST, Some("create_api_key"))
| (Some("api_keys_manage"), http::Method::PUT, Some("update_api_key"))
| (Some("api_keys_manage"), http::Method::PATCH, Some("toggle_api_key"))
| (Some("adaptive_manage"), http::Method::PATCH, Some("toggle_mode"))
| (Some("security_manage"), http::Method::POST, Some("blacklist_add"))
| (Some("security_manage"), http::Method::POST, Some("whitelist_add"))
| (Some("users_manage"), http::Method::POST, Some("create_user"))
| (Some("users_manage"), http::Method::PUT, Some("update_user"))
| (Some("users_manage"), http::Method::POST, Some("create_user_api_key"))
| (Some("users_manage"), http::Method::PUT, Some("update_user_api_key"))
| (Some("users_manage"), http::Method::PATCH, Some("lock_user_api_key"))
| (Some("pool_manage"), http::Method::POST, Some("batch_import_keys"))
| (Some("pool_manage"), http::Method::POST, Some("batch_action_keys"))
| (Some("pool_manage"), http::Method::POST, Some("resolve_selection"))
| (Some("usage_manage"), http::Method::POST, Some("replay"))
| (Some("wallets_manage"), http::Method::POST, Some("adjust_balance"))
| (Some("wallets_manage"), http::Method::POST, Some("recharge_balance"))
| (Some("wallets_manage"), http::Method::POST, Some("process_refund"))
| (Some("wallets_manage"), http::Method::POST, Some("complete_refund"))
| (Some("wallets_manage"), http::Method::POST, Some("fail_refund"))
| (Some("gemini_files_manage"), http::Method::POST, Some("upload"))
| (Some("ldap_manage"), http::Method::PUT, Some("set_config"))
| (Some("ldap_manage"), http::Method::POST, Some("test_connection"))
| (Some("global_models_manage"), http::Method::POST, Some("create_global_model"))
| (
Some("global_models_manage"),
http::Method::PATCH,
Some("update_global_model"),
)
| (
Some("global_models_manage"),
http::Method::POST,
Some("batch_delete_global_models"),
)
| (Some("global_models_manage"), http::Method::POST, Some("assign_to_providers"))
| (Some("providers_manage"), http::Method::POST, Some("create_provider"))
| (Some("providers_manage"), http::Method::PATCH, Some("update_provider")) => true,
_ => false,
}
})
}
pub(crate) fn internal_proxy_local_requires_buffered_body(
request_context: &GatewayPublicRequestContext,
) -> bool {
request_context
.control_decision
.as_ref()
.is_some_and(|decision| {
if decision.route_class.as_deref() != Some("internal_proxy")
|| request_context.request_method != http::Method::POST
{
return false;
}
matches!(
(
decision.route_family.as_deref(),
decision.route_kind.as_deref()
),
(
Some(crate::gateway::TUNNEL_ROUTE_FAMILY),
Some("heartbeat" | "node_status")
) | (
Some("gateway_legacy"),
Some(
"resolve"
| "auth_context"
| "decision_sync"
| "decision_stream"
| "execute_sync"
| "execute_stream"
| "plan_sync"
| "plan_stream"
| "report_sync"
| "report_stream"
| "finalize_sync"
)
)
)
})
}
pub(crate) fn public_support_local_requires_buffered_body(
request_context: &GatewayPublicRequestContext,
) -> bool {
request_context
.control_decision
.as_ref()
.is_some_and(|decision| {
if decision.route_class.as_deref() != Some("public_support") {
return false;
}
matches!(
(
decision.route_family.as_deref(),
request_context.request_method.clone(),
decision.route_kind.as_deref(),
),
(
Some("auth_legacy"),
http::Method::POST,
Some(
"login"
| "register"
| "send_verification_code"
| "verify_email"
| "verification_status"
),
) | (
Some("users_me_legacy"),
http::Method::PUT,
Some(
"update_detail"
| "model_capabilities_update"
| "preferences_update"
| "api_key_update"
| "management_token_update"
| "api_key_providers_update"
| "api_key_capabilities_update",
),
) | (
Some("users_me_legacy"),
http::Method::PATCH,
Some("password" | "session_update" | "api_key_patch"),
) | (
Some("users_me_legacy"),
http::Method::POST,
Some("api_keys_create" | "management_tokens_create"),
) | (
Some("wallet_legacy"),
http::Method::POST,
Some("create_refund" | "create_recharge_order"),
) | (
Some("payment_callback_legacy"),
http::Method::POST,
Some("callback"),
)
)
})
}
pub(crate) fn local_proxy_route_requires_buffered_body(
request_context: &GatewayPublicRequestContext,
) -> bool {
admin_proxy_local_requires_buffered_body(request_context)
|| internal_proxy_local_requires_buffered_body(request_context)
|| public_support_local_requires_buffered_body(request_context)
}