mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
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:
626
apps/aether-gateway/src/handlers/shared/admin_paths.rs
Normal file
626
apps/aether-gateway/src/handlers/shared/admin_paths.rs
Normal 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)
|
||||
}
|
||||
509
apps/aether-gateway/src/handlers/shared/catalog.rs
Normal file
509
apps/aether-gateway/src/handlers/shared/catalog.rs
Normal 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)
|
||||
}
|
||||
452
apps/aether-gateway/src/handlers/shared/payloads.rs
Normal file
452
apps/aether-gateway/src/handlers/shared/payloads.rs
Normal 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>,
|
||||
}
|
||||
437
apps/aether-gateway/src/handlers/shared/request_utils.rs
Normal file
437
apps/aether-gateway/src/handlers/shared/request_utils.rs
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user