feat: 扩展 Rust gateway 全功能模块,新增 billing/crypto/wallet crate 及完整数据层

- 新增 aether-billing、aether-crypto、aether-wallet 独立 crate
- aether-data 扩展 repository 层:announcements、auth_modules、billing、
  candidate_selection、gemini_file_mappings、global_models、management_tokens、
  oauth_providers、proxy_nodes、quota、users、wallet 等模块
- aether-gateway 新增 api/auth/billing/control/middleware/scheduler/usage/
  video_tasks/hooks/maintenance/model_fetch/provider_transport 等功能模块
- 重构 executor decision 和 gateway state 为模块目录结构
- 新增 gateway router、frontdoor 路由层及对应测试
- Python 侧 API 路由重构,新增 compat/support 模块
- 前端 Logo 组件更新及 Provider 管理页面调整
This commit is contained in:
fawney19
2026-03-31 19:19:04 +08:00
parent b5a0070023
commit ddf18fed9a
690 changed files with 235085 additions and 16299 deletions
@@ -0,0 +1,789 @@
use std::sync::RwLock;
use async_trait::async_trait;
use super::types::{
AdminGlobalModelListQuery, AdminProviderModelListQuery, CreateAdminGlobalModelRecord,
GlobalModelReadRepository, GlobalModelWriteRepository, PublicCatalogModelListQuery,
PublicCatalogModelSearchQuery, PublicGlobalModelQuery, StoredAdminGlobalModel,
StoredAdminGlobalModelPage, StoredAdminProviderModel, StoredProviderActiveGlobalModel,
StoredProviderModelStats, StoredPublicCatalogModel, StoredPublicGlobalModel,
StoredPublicGlobalModelPage, UpdateAdminGlobalModelRecord, UpsertAdminProviderModelRecord,
};
use crate::DataLayerError;
#[derive(Debug, Default)]
pub struct InMemoryGlobalModelReadRepository {
items: RwLock<Vec<StoredPublicGlobalModel>>,
admin_global_model_items: RwLock<Vec<StoredAdminGlobalModel>>,
public_catalog_items: RwLock<Vec<StoredPublicCatalogModel>>,
admin_provider_model_items: RwLock<Vec<StoredAdminProviderModel>>,
provider_model_stats: RwLock<Vec<StoredProviderModelStats>>,
active_global_model_refs: RwLock<Vec<StoredProviderActiveGlobalModel>>,
}
impl InMemoryGlobalModelReadRepository {
pub fn seed<I>(items: I) -> Self
where
I: IntoIterator<Item = StoredPublicGlobalModel>,
{
Self {
items: RwLock::new(items.into_iter().collect()),
admin_global_model_items: RwLock::new(Vec::new()),
public_catalog_items: RwLock::new(Vec::new()),
admin_provider_model_items: RwLock::new(Vec::new()),
provider_model_stats: RwLock::new(Vec::new()),
active_global_model_refs: RwLock::new(Vec::new()),
}
}
pub fn with_public_catalog_models<I>(self, items: I) -> Self
where
I: IntoIterator<Item = StoredPublicCatalogModel>,
{
*self
.public_catalog_items
.write()
.expect("public catalog model repository lock") = items.into_iter().collect();
self
}
pub fn with_provider_model_stats<I>(self, items: I) -> Self
where
I: IntoIterator<Item = StoredProviderModelStats>,
{
*self
.provider_model_stats
.write()
.expect("provider model stats repository lock") = items.into_iter().collect();
self
}
pub fn with_admin_provider_models<I>(self, items: I) -> Self
where
I: IntoIterator<Item = StoredAdminProviderModel>,
{
*self
.admin_provider_model_items
.write()
.expect("admin provider model repository lock") = items.into_iter().collect();
self
}
pub fn with_active_global_model_refs<I>(self, items: I) -> Self
where
I: IntoIterator<Item = StoredProviderActiveGlobalModel>,
{
*self
.active_global_model_refs
.write()
.expect("active global model repository lock") = items.into_iter().collect();
self
}
pub fn with_admin_global_models<I>(self, items: I) -> Self
where
I: IntoIterator<Item = StoredAdminGlobalModel>,
{
*self
.admin_global_model_items
.write()
.expect("admin global model repository lock") = items.into_iter().collect();
self
}
}
#[async_trait]
impl GlobalModelReadRepository for InMemoryGlobalModelReadRepository {
async fn list_public_models(
&self,
query: &PublicGlobalModelQuery,
) -> Result<StoredPublicGlobalModelPage, DataLayerError> {
let items = self.items.read().expect("global model repository lock");
let search = query
.search
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(|value| value.to_ascii_lowercase());
let mut filtered = items
.iter()
.filter(|item| match query.is_active {
Some(is_active) => item.is_active == is_active,
None => item.is_active,
})
.filter(|item| {
let Some(search) = search.as_deref() else {
return true;
};
item.name.to_ascii_lowercase().contains(search)
|| item
.display_name
.as_deref()
.map(|value| value.to_ascii_lowercase().contains(search))
.unwrap_or(false)
})
.cloned()
.collect::<Vec<_>>();
filtered.sort_by(|left, right| left.name.cmp(&right.name));
let total = filtered.len();
let items = filtered
.into_iter()
.skip(query.offset)
.take(query.limit)
.collect();
Ok(StoredPublicGlobalModelPage { items, total })
}
async fn get_public_model_by_name(
&self,
model_name: &str,
) -> Result<Option<StoredPublicGlobalModel>, DataLayerError> {
let items = self.items.read().expect("global model repository lock");
Ok(items
.iter()
.find(|item| item.is_active && item.name == model_name)
.cloned())
}
async fn list_public_catalog_models(
&self,
query: &PublicCatalogModelListQuery,
) -> Result<Vec<StoredPublicCatalogModel>, DataLayerError> {
let items = self
.public_catalog_items
.read()
.expect("public catalog model repository lock");
let provider_id = query
.provider_id
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty());
let mut filtered = items
.iter()
.filter(|item| item.is_active)
.filter(|item| match provider_id {
Some(provider_id) => item.provider_id == provider_id,
None => true,
})
.cloned()
.collect::<Vec<_>>();
filtered.sort_by(|left, right| {
left.provider_name
.cmp(&right.provider_name)
.then_with(|| left.name.cmp(&right.name))
.then_with(|| left.id.cmp(&right.id))
});
Ok(filtered
.into_iter()
.skip(query.offset)
.take(query.limit)
.collect())
}
async fn search_public_catalog_models(
&self,
query: &PublicCatalogModelSearchQuery,
) -> Result<Vec<StoredPublicCatalogModel>, DataLayerError> {
let items = self
.public_catalog_items
.read()
.expect("public catalog model repository lock");
let provider_id = query
.provider_id
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty());
let search = query.search.trim().to_ascii_lowercase();
let mut filtered = items
.iter()
.filter(|item| item.is_active)
.filter(|item| match provider_id {
Some(provider_id) => item.provider_id == provider_id,
None => true,
})
.filter(|item| {
item.provider_model_name
.to_ascii_lowercase()
.contains(&search)
|| item.name.to_ascii_lowercase().contains(&search)
|| item.display_name.to_ascii_lowercase().contains(&search)
})
.cloned()
.collect::<Vec<_>>();
filtered.sort_by(|left, right| {
left.provider_name
.cmp(&right.provider_name)
.then_with(|| left.name.cmp(&right.name))
.then_with(|| left.id.cmp(&right.id))
});
filtered.truncate(query.limit);
Ok(filtered)
}
async fn list_admin_global_models(
&self,
query: &AdminGlobalModelListQuery,
) -> Result<StoredAdminGlobalModelPage, DataLayerError> {
let items = self
.admin_global_model_items
.read()
.expect("admin global model repository lock");
let search = query
.search
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(|value| value.to_ascii_lowercase());
let mut filtered = items
.iter()
.filter(|item| match query.is_active {
Some(is_active) => item.is_active == is_active,
None => true,
})
.filter(|item| {
let Some(search) = search.as_deref() else {
return true;
};
item.name.to_ascii_lowercase().contains(search)
|| item.display_name.to_ascii_lowercase().contains(search)
})
.cloned()
.collect::<Vec<_>>();
filtered.sort_by(|left, right| left.name.cmp(&right.name));
let total = filtered.len();
let items = filtered
.into_iter()
.skip(query.offset)
.take(query.limit)
.collect();
Ok(StoredAdminGlobalModelPage { items, total })
}
async fn list_admin_provider_models(
&self,
query: &AdminProviderModelListQuery,
) -> Result<Vec<StoredAdminProviderModel>, DataLayerError> {
let items = self
.admin_provider_model_items
.read()
.expect("admin provider model repository lock");
let mut filtered = items
.iter()
.filter(|item| item.provider_id == query.provider_id)
.filter(|item| match query.is_active {
Some(is_active) => item.is_active == is_active,
None => true,
})
.cloned()
.collect::<Vec<_>>();
filtered.sort_by(|left, right| {
right
.created_at_unix_secs
.unwrap_or_default()
.cmp(&left.created_at_unix_secs.unwrap_or_default())
.then_with(|| left.id.cmp(&right.id))
});
Ok(filtered
.into_iter()
.skip(query.offset)
.take(query.limit)
.collect())
}
async fn get_admin_provider_model(
&self,
provider_id: &str,
model_id: &str,
) -> Result<Option<StoredAdminProviderModel>, DataLayerError> {
Ok(self
.admin_provider_model_items
.read()
.expect("admin provider model repository lock")
.iter()
.find(|item| item.provider_id == provider_id && item.id == model_id)
.cloned())
}
async fn list_admin_provider_available_source_models(
&self,
provider_id: &str,
) -> Result<Vec<StoredAdminProviderModel>, DataLayerError> {
let items = self
.admin_provider_model_items
.read()
.expect("admin provider model repository lock");
let active_globals = self
.admin_global_model_items
.read()
.expect("admin global model repository lock");
let mut filtered = items
.iter()
.filter(|item| item.provider_id == provider_id && item.is_active)
.filter(|item| {
active_globals
.iter()
.find(|global| global.id == item.global_model_id)
.map(|global| global.is_active)
.unwrap_or(false)
})
.cloned()
.collect::<Vec<_>>();
filtered.sort_by(|left, right| {
left.global_model_name
.cmp(&right.global_model_name)
.then_with(|| right.created_at_unix_secs.cmp(&left.created_at_unix_secs))
.then_with(|| left.id.cmp(&right.id))
});
Ok(filtered)
}
async fn get_admin_global_model_by_id(
&self,
global_model_id: &str,
) -> Result<Option<StoredAdminGlobalModel>, DataLayerError> {
let items = self
.admin_global_model_items
.read()
.expect("admin global model repository lock");
Ok(items
.iter()
.find(|item| item.id == global_model_id)
.cloned())
}
async fn get_admin_global_model_by_name(
&self,
model_name: &str,
) -> Result<Option<StoredAdminGlobalModel>, DataLayerError> {
let items = self
.admin_global_model_items
.read()
.expect("admin global model repository lock");
Ok(items.iter().find(|item| item.name == model_name).cloned())
}
async fn list_admin_provider_models_by_global_model_id(
&self,
global_model_id: &str,
) -> Result<Vec<StoredAdminProviderModel>, DataLayerError> {
let items = self
.admin_provider_model_items
.read()
.expect("admin provider model repository lock");
let mut filtered = items
.iter()
.filter(|item| item.global_model_id == global_model_id)
.cloned()
.collect::<Vec<_>>();
filtered.sort_by(|left, right| {
right
.created_at_unix_secs
.unwrap_or_default()
.cmp(&left.created_at_unix_secs.unwrap_or_default())
.then_with(|| left.id.cmp(&right.id))
});
Ok(filtered)
}
async fn list_provider_model_stats(
&self,
provider_ids: &[String],
) -> Result<Vec<StoredProviderModelStats>, DataLayerError> {
let provider_ids = provider_ids
.iter()
.cloned()
.collect::<std::collections::BTreeSet<_>>();
Ok(self
.provider_model_stats
.read()
.expect("provider model stats repository lock")
.iter()
.filter(|item| provider_ids.contains(&item.provider_id))
.cloned()
.collect())
}
async fn list_active_global_model_ids_by_provider_ids(
&self,
provider_ids: &[String],
) -> Result<Vec<StoredProviderActiveGlobalModel>, DataLayerError> {
let provider_ids = provider_ids
.iter()
.cloned()
.collect::<std::collections::BTreeSet<_>>();
Ok(self
.active_global_model_refs
.read()
.expect("active global model repository lock")
.iter()
.filter(|item| provider_ids.contains(&item.provider_id))
.cloned()
.collect())
}
}
#[async_trait]
impl GlobalModelWriteRepository for InMemoryGlobalModelReadRepository {
async fn create_admin_provider_model(
&self,
record: &UpsertAdminProviderModelRecord,
) -> Result<Option<StoredAdminProviderModel>, DataLayerError> {
let global_model = self
.get_admin_global_model_by_id(&record.global_model_id)
.await?
.ok_or_else(|| DataLayerError::UnexpectedValue("global model not found".to_string()))?;
let stored = StoredAdminProviderModel::new(
record.id.clone(),
record.provider_id.clone(),
record.global_model_id.clone(),
record.provider_model_name.clone(),
record.provider_model_mappings.clone(),
record.price_per_request,
record.tiered_pricing.clone(),
record.supports_vision,
record.supports_function_calling,
record.supports_streaming,
record.supports_extended_thinking,
record.supports_image_generation,
record.is_active,
record.is_available,
record.config.clone(),
Some(1_711_000_000),
Some(1_711_000_000),
Some(global_model.name.clone()),
Some(global_model.display_name.clone()),
global_model.default_price_per_request,
global_model.default_tiered_pricing.clone(),
global_model.config.clone(),
)?;
self.admin_provider_model_items
.write()
.expect("admin provider model repository lock")
.push(stored.clone());
Ok(Some(stored))
}
async fn update_admin_provider_model(
&self,
record: &UpsertAdminProviderModelRecord,
) -> Result<Option<StoredAdminProviderModel>, DataLayerError> {
let global_model = self
.get_admin_global_model_by_id(&record.global_model_id)
.await?
.ok_or_else(|| DataLayerError::UnexpectedValue("global model not found".to_string()))?;
let mut items = self
.admin_provider_model_items
.write()
.expect("admin provider model repository lock");
let Some(existing) = items
.iter_mut()
.find(|item| item.id == record.id && item.provider_id == record.provider_id)
else {
return Ok(None);
};
existing.global_model_id = record.global_model_id.clone();
existing.provider_model_name = record.provider_model_name.clone();
existing.provider_model_mappings = record.provider_model_mappings.clone();
existing.price_per_request = record.price_per_request;
existing.tiered_pricing = record.tiered_pricing.clone();
existing.supports_vision = record.supports_vision;
existing.supports_function_calling = record.supports_function_calling;
existing.supports_streaming = record.supports_streaming;
existing.supports_extended_thinking = record.supports_extended_thinking;
existing.supports_image_generation = record.supports_image_generation;
existing.is_active = record.is_active;
existing.is_available = record.is_available;
existing.config = record.config.clone();
existing.updated_at_unix_secs = Some(1_711_000_100);
existing.global_model_name = Some(global_model.name.clone());
existing.global_model_display_name = Some(global_model.display_name.clone());
existing.global_model_default_price_per_request = global_model.default_price_per_request;
existing.global_model_default_tiered_pricing = global_model.default_tiered_pricing.clone();
existing.global_model_config = global_model.config.clone();
Ok(Some(existing.clone()))
}
async fn delete_admin_provider_model(
&self,
provider_id: &str,
model_id: &str,
) -> Result<bool, DataLayerError> {
let mut items = self
.admin_provider_model_items
.write()
.expect("admin provider model repository lock");
let original_len = items.len();
items.retain(|item| !(item.provider_id == provider_id && item.id == model_id));
Ok(items.len() != original_len)
}
async fn create_admin_global_model(
&self,
record: &CreateAdminGlobalModelRecord,
) -> Result<Option<StoredAdminGlobalModel>, DataLayerError> {
let stored = StoredAdminGlobalModel::new(
record.id.clone(),
record.name.clone(),
record.display_name.clone(),
record.is_active,
record.default_price_per_request,
record.default_tiered_pricing.clone(),
record.supported_capabilities.clone(),
record.config.clone(),
Some(1_711_000_000),
Some(1_711_000_000),
)?;
self.admin_global_model_items
.write()
.expect("admin global model repository lock")
.push(stored.clone());
Ok(Some(stored))
}
async fn update_admin_global_model(
&self,
record: &UpdateAdminGlobalModelRecord,
) -> Result<Option<StoredAdminGlobalModel>, DataLayerError> {
let mut items = self
.admin_global_model_items
.write()
.expect("admin global model repository lock");
let Some(existing) = items.iter_mut().find(|item| item.id == record.id) else {
return Ok(None);
};
existing.display_name = record.display_name.clone();
existing.is_active = record.is_active;
existing.default_price_per_request = record.default_price_per_request;
existing.default_tiered_pricing = record.default_tiered_pricing.clone();
existing.supported_capabilities = record.supported_capabilities.clone();
existing.config = record.config.clone();
existing.updated_at_unix_secs = Some(1_711_000_100);
Ok(Some(existing.clone()))
}
async fn delete_admin_global_model(
&self,
global_model_id: &str,
) -> Result<bool, DataLayerError> {
let mut globals = self
.admin_global_model_items
.write()
.expect("admin global model repository lock");
let original_len = globals.len();
globals.retain(|item| item.id != global_model_id);
drop(globals);
self.admin_provider_model_items
.write()
.expect("admin provider model repository lock")
.retain(|item| item.global_model_id != global_model_id);
Ok(original_len
!= self
.admin_global_model_items
.read()
.expect("admin global model repository lock")
.len())
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::InMemoryGlobalModelReadRepository;
use crate::repository::global_models::{
GlobalModelReadRepository, PublicCatalogModelListQuery, PublicCatalogModelSearchQuery,
PublicGlobalModelQuery, StoredPublicCatalogModel, StoredPublicGlobalModel,
};
fn sample_model(
id: &str,
name: &str,
display_name: &str,
is_active: bool,
) -> StoredPublicGlobalModel {
StoredPublicGlobalModel::new(
id.to_string(),
name.to_string(),
Some(display_name.to_string()),
is_active,
Some(0.02),
Some(json!({"tiers":[{"up_to": null, "input_price_per_1m": 3.0, "output_price_per_1m": 15.0}]})),
Some(json!(["vision"])),
Some(json!({"family": "test"})),
0,
)
.expect("global model should build")
}
fn sample_public_catalog_model(
id: &str,
provider_id: &str,
provider_name: &str,
provider_model_name: &str,
name: &str,
display_name: &str,
) -> StoredPublicCatalogModel {
StoredPublicCatalogModel::new(
id.to_string(),
provider_id.to_string(),
provider_name.to_string(),
provider_model_name.to_string(),
name.to_string(),
display_name.to_string(),
Some(format!("{display_name} description")),
Some(format!("https://cdn.example/{name}.png")),
Some(3.0),
Some(15.0),
Some(1.5),
Some(0.3),
Some(true),
Some(true),
Some(true),
true,
)
.expect("public catalog model should build")
}
#[tokio::test]
async fn defaults_to_active_models_only() {
let repository = InMemoryGlobalModelReadRepository::seed(vec![
sample_model("gm-1", "claude-sonnet-4-5", "Claude Sonnet 4.5", true),
sample_model("gm-2", "legacy-model", "Legacy Model", false),
]);
let page = repository
.list_public_models(&PublicGlobalModelQuery {
offset: 0,
limit: 50,
is_active: None,
search: None,
})
.await
.expect("list should succeed");
assert_eq!(page.total, 1);
assert_eq!(page.items[0].name, "claude-sonnet-4-5");
}
#[tokio::test]
async fn search_matches_name_and_display_name() {
let repository = InMemoryGlobalModelReadRepository::seed(vec![
sample_model("gm-1", "gpt-5", "GPT 5", true),
sample_model("gm-2", "claude-sonnet-4-5", "Claude Sonnet 4.5", true),
]);
let page = repository
.list_public_models(&PublicGlobalModelQuery {
offset: 0,
limit: 50,
is_active: None,
search: Some("sonnet".to_string()),
})
.await
.expect("list should succeed");
assert_eq!(page.total, 1);
assert_eq!(page.items[0].name, "claude-sonnet-4-5");
}
#[tokio::test]
async fn get_public_model_by_name_only_returns_active_exact_match() {
let repository = InMemoryGlobalModelReadRepository::seed(vec![
sample_model("gm-1", "gpt-5", "GPT 5", true),
sample_model("gm-2", "gpt-5-old", "GPT 5 Old", false),
]);
let model = repository
.get_public_model_by_name("gpt-5")
.await
.expect("lookup should succeed");
assert_eq!(model.expect("model should exist").name, "gpt-5");
let missing = repository
.get_public_model_by_name("gpt-5-old")
.await
.expect("lookup should succeed");
assert!(missing.is_none());
}
#[tokio::test]
async fn lists_public_catalog_models_with_provider_filter() {
let repository =
InMemoryGlobalModelReadRepository::seed(Vec::<StoredPublicGlobalModel>::new())
.with_public_catalog_models(vec![
sample_public_catalog_model(
"model-1",
"provider-openai",
"openai",
"gpt-5-preview",
"gpt-5",
"GPT 5",
),
sample_public_catalog_model(
"model-2",
"provider-claude",
"claude",
"claude-3-7-sonnet",
"claude-3-7-sonnet",
"Claude 3.7 Sonnet",
),
]);
let items = repository
.list_public_catalog_models(&PublicCatalogModelListQuery {
provider_id: Some("provider-openai".to_string()),
offset: 0,
limit: 50,
})
.await
.expect("list should succeed");
assert_eq!(items.len(), 1);
assert_eq!(items[0].provider_id, "provider-openai");
assert_eq!(items[0].name, "gpt-5");
}
#[tokio::test]
async fn searches_public_catalog_models_by_provider_and_display_name() {
let repository =
InMemoryGlobalModelReadRepository::seed(Vec::<StoredPublicGlobalModel>::new())
.with_public_catalog_models(vec![
sample_public_catalog_model(
"model-1",
"provider-openai",
"openai",
"gpt-5-preview",
"gpt-5",
"GPT 5",
),
sample_public_catalog_model(
"model-2",
"provider-claude",
"claude",
"claude-3-7-sonnet",
"claude-3-7-sonnet",
"Claude 3.7 Sonnet",
),
]);
let items = repository
.search_public_catalog_models(&PublicCatalogModelSearchQuery {
search: "sonnet".to_string(),
provider_id: Some("provider-claude".to_string()),
limit: 20,
})
.await
.expect("search should succeed");
assert_eq!(items.len(), 1);
assert_eq!(items[0].provider_name, "claude");
assert_eq!(items[0].display_name, "Claude 3.7 Sonnet");
}
}
@@ -0,0 +1,14 @@
mod memory;
mod sql;
mod types;
pub use memory::InMemoryGlobalModelReadRepository;
pub use sql::SqlxGlobalModelReadRepository;
pub use types::{
AdminGlobalModelListQuery, AdminProviderModelListQuery, CreateAdminGlobalModelRecord,
GlobalModelReadRepository, GlobalModelWriteRepository, PublicCatalogModelListQuery,
PublicCatalogModelSearchQuery, PublicGlobalModelQuery, StoredAdminGlobalModel,
StoredAdminGlobalModelPage, StoredAdminProviderModel, StoredProviderActiveGlobalModel,
StoredProviderModelStats, StoredPublicCatalogModel, StoredPublicGlobalModel,
StoredPublicGlobalModelPage, UpdateAdminGlobalModelRecord, UpsertAdminProviderModelRecord,
};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,688 @@
use async_trait::async_trait;
use serde_json::Value;
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct StoredPublicGlobalModel {
pub id: String,
pub name: String,
pub display_name: Option<String>,
pub is_active: bool,
pub default_price_per_request: Option<f64>,
pub default_tiered_pricing: Option<Value>,
pub supported_capabilities: Option<Value>,
pub config: Option<Value>,
pub usage_count: u64,
}
impl StoredPublicGlobalModel {
#[allow(clippy::too_many_arguments)]
pub fn new(
id: String,
name: String,
display_name: Option<String>,
is_active: bool,
default_price_per_request: Option<f64>,
default_tiered_pricing: Option<Value>,
supported_capabilities: Option<Value>,
config: Option<Value>,
usage_count: u64,
) -> Result<Self, crate::DataLayerError> {
if id.trim().is_empty() {
return Err(crate::DataLayerError::UnexpectedValue(
"global_models.id is empty".to_string(),
));
}
if name.trim().is_empty() {
return Err(crate::DataLayerError::UnexpectedValue(
"global_models.name is empty".to_string(),
));
}
Ok(Self {
id,
name,
display_name,
is_active,
default_price_per_request,
default_tiered_pricing,
supported_capabilities,
config,
usage_count,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct PublicGlobalModelQuery {
pub offset: usize,
pub limit: usize,
pub is_active: Option<bool>,
pub search: Option<String>,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct StoredPublicCatalogModel {
pub id: String,
pub provider_id: String,
pub provider_name: String,
pub provider_model_name: String,
pub name: String,
pub display_name: String,
pub description: Option<String>,
pub icon_url: Option<String>,
pub input_price_per_1m: Option<f64>,
pub output_price_per_1m: Option<f64>,
pub cache_creation_price_per_1m: Option<f64>,
pub cache_read_price_per_1m: Option<f64>,
pub supports_vision: Option<bool>,
pub supports_function_calling: Option<bool>,
pub supports_streaming: Option<bool>,
pub is_active: bool,
}
impl StoredPublicCatalogModel {
#[allow(clippy::too_many_arguments)]
pub fn new(
id: String,
provider_id: String,
provider_name: String,
provider_model_name: String,
name: String,
display_name: String,
description: Option<String>,
icon_url: Option<String>,
input_price_per_1m: Option<f64>,
output_price_per_1m: Option<f64>,
cache_creation_price_per_1m: Option<f64>,
cache_read_price_per_1m: Option<f64>,
supports_vision: Option<bool>,
supports_function_calling: Option<bool>,
supports_streaming: Option<bool>,
is_active: bool,
) -> Result<Self, crate::DataLayerError> {
if id.trim().is_empty() {
return Err(crate::DataLayerError::UnexpectedValue(
"models.id is empty".to_string(),
));
}
if provider_id.trim().is_empty() {
return Err(crate::DataLayerError::UnexpectedValue(
"models.provider_id is empty".to_string(),
));
}
if provider_name.trim().is_empty() {
return Err(crate::DataLayerError::UnexpectedValue(
"providers.name is empty".to_string(),
));
}
if provider_model_name.trim().is_empty() {
return Err(crate::DataLayerError::UnexpectedValue(
"models.provider_model_name is empty".to_string(),
));
}
if name.trim().is_empty() {
return Err(crate::DataLayerError::UnexpectedValue(
"public model name is empty".to_string(),
));
}
if display_name.trim().is_empty() {
return Err(crate::DataLayerError::UnexpectedValue(
"public model display_name is empty".to_string(),
));
}
Ok(Self {
id,
provider_id,
provider_name,
provider_model_name,
name,
display_name,
description,
icon_url,
input_price_per_1m,
output_price_per_1m,
cache_creation_price_per_1m,
cache_read_price_per_1m,
supports_vision,
supports_function_calling,
supports_streaming,
is_active,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct PublicCatalogModelListQuery {
pub provider_id: Option<String>,
pub offset: usize,
pub limit: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PublicCatalogModelSearchQuery {
pub search: String,
pub provider_id: Option<String>,
pub limit: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AdminProviderModelListQuery {
pub provider_id: String,
pub is_active: Option<bool>,
pub offset: usize,
pub limit: usize,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct StoredAdminGlobalModel {
pub id: String,
pub name: String,
pub display_name: String,
pub is_active: bool,
pub default_price_per_request: Option<f64>,
pub default_tiered_pricing: Option<Value>,
pub supported_capabilities: Option<Value>,
pub config: Option<Value>,
pub created_at_unix_secs: Option<u64>,
pub updated_at_unix_secs: Option<u64>,
}
impl StoredAdminGlobalModel {
#[allow(clippy::too_many_arguments)]
pub fn new(
id: String,
name: String,
display_name: String,
is_active: bool,
default_price_per_request: Option<f64>,
default_tiered_pricing: Option<Value>,
supported_capabilities: Option<Value>,
config: Option<Value>,
created_at_unix_secs: Option<u64>,
updated_at_unix_secs: Option<u64>,
) -> Result<Self, crate::DataLayerError> {
if id.trim().is_empty() {
return Err(crate::DataLayerError::UnexpectedValue(
"global_models.id is empty".to_string(),
));
}
if name.trim().is_empty() {
return Err(crate::DataLayerError::UnexpectedValue(
"global_models.name is empty".to_string(),
));
}
if display_name.trim().is_empty() {
return Err(crate::DataLayerError::UnexpectedValue(
"global_models.display_name is empty".to_string(),
));
}
Ok(Self {
id,
name,
display_name,
is_active,
default_price_per_request,
default_tiered_pricing,
supported_capabilities,
config,
created_at_unix_secs,
updated_at_unix_secs,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct AdminGlobalModelListQuery {
pub offset: usize,
pub limit: usize,
pub is_active: Option<bool>,
pub search: Option<String>,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct StoredAdminProviderModel {
pub id: String,
pub provider_id: String,
pub global_model_id: String,
pub provider_model_name: String,
pub provider_model_mappings: Option<Value>,
pub price_per_request: Option<f64>,
pub tiered_pricing: Option<Value>,
pub supports_vision: Option<bool>,
pub supports_function_calling: Option<bool>,
pub supports_streaming: Option<bool>,
pub supports_extended_thinking: Option<bool>,
pub supports_image_generation: Option<bool>,
pub is_active: bool,
pub is_available: bool,
pub config: Option<Value>,
pub created_at_unix_secs: Option<u64>,
pub updated_at_unix_secs: Option<u64>,
pub global_model_name: Option<String>,
pub global_model_display_name: Option<String>,
pub global_model_default_price_per_request: Option<f64>,
pub global_model_default_tiered_pricing: Option<Value>,
pub global_model_config: Option<Value>,
}
impl StoredAdminProviderModel {
#[allow(clippy::too_many_arguments)]
pub fn new(
id: String,
provider_id: String,
global_model_id: String,
provider_model_name: String,
provider_model_mappings: Option<Value>,
price_per_request: Option<f64>,
tiered_pricing: Option<Value>,
supports_vision: Option<bool>,
supports_function_calling: Option<bool>,
supports_streaming: Option<bool>,
supports_extended_thinking: Option<bool>,
supports_image_generation: Option<bool>,
is_active: bool,
is_available: bool,
config: Option<Value>,
created_at_unix_secs: Option<u64>,
updated_at_unix_secs: Option<u64>,
global_model_name: Option<String>,
global_model_display_name: Option<String>,
global_model_default_price_per_request: Option<f64>,
global_model_default_tiered_pricing: Option<Value>,
global_model_config: Option<Value>,
) -> Result<Self, crate::DataLayerError> {
if id.trim().is_empty() {
return Err(crate::DataLayerError::UnexpectedValue(
"models.id is empty".to_string(),
));
}
if provider_id.trim().is_empty() {
return Err(crate::DataLayerError::UnexpectedValue(
"models.provider_id is empty".to_string(),
));
}
if global_model_id.trim().is_empty() {
return Err(crate::DataLayerError::UnexpectedValue(
"models.global_model_id is empty".to_string(),
));
}
if provider_model_name.trim().is_empty() {
return Err(crate::DataLayerError::UnexpectedValue(
"models.provider_model_name is empty".to_string(),
));
}
Ok(Self {
id,
provider_id,
global_model_id,
provider_model_name,
provider_model_mappings,
price_per_request,
tiered_pricing,
supports_vision,
supports_function_calling,
supports_streaming,
supports_extended_thinking,
supports_image_generation,
is_active,
is_available,
config,
created_at_unix_secs,
updated_at_unix_secs,
global_model_name,
global_model_display_name,
global_model_default_price_per_request,
global_model_default_tiered_pricing,
global_model_config,
})
}
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct UpsertAdminProviderModelRecord {
pub id: String,
pub provider_id: String,
pub global_model_id: String,
pub provider_model_name: String,
pub provider_model_mappings: Option<Value>,
pub price_per_request: Option<f64>,
pub tiered_pricing: Option<Value>,
pub supports_vision: Option<bool>,
pub supports_function_calling: Option<bool>,
pub supports_streaming: Option<bool>,
pub supports_extended_thinking: Option<bool>,
pub supports_image_generation: Option<bool>,
pub is_active: bool,
pub is_available: bool,
pub config: Option<Value>,
}
impl UpsertAdminProviderModelRecord {
#[allow(clippy::too_many_arguments)]
pub fn new(
id: String,
provider_id: String,
global_model_id: String,
provider_model_name: String,
provider_model_mappings: Option<Value>,
price_per_request: Option<f64>,
tiered_pricing: Option<Value>,
supports_vision: Option<bool>,
supports_function_calling: Option<bool>,
supports_streaming: Option<bool>,
supports_extended_thinking: Option<bool>,
supports_image_generation: Option<bool>,
is_active: bool,
is_available: bool,
config: Option<Value>,
) -> Result<Self, crate::DataLayerError> {
if id.trim().is_empty() {
return Err(crate::DataLayerError::UnexpectedValue(
"models.id is empty".to_string(),
));
}
if provider_id.trim().is_empty() {
return Err(crate::DataLayerError::UnexpectedValue(
"models.provider_id is empty".to_string(),
));
}
if global_model_id.trim().is_empty() {
return Err(crate::DataLayerError::UnexpectedValue(
"models.global_model_id is empty".to_string(),
));
}
if provider_model_name.trim().is_empty() {
return Err(crate::DataLayerError::UnexpectedValue(
"models.provider_model_name is empty".to_string(),
));
}
Ok(Self {
id,
provider_id,
global_model_id,
provider_model_name,
provider_model_mappings,
price_per_request,
tiered_pricing,
supports_vision,
supports_function_calling,
supports_streaming,
supports_extended_thinking,
supports_image_generation,
is_active,
is_available,
config,
})
}
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct CreateAdminGlobalModelRecord {
pub id: String,
pub name: String,
pub display_name: String,
pub is_active: bool,
pub default_price_per_request: Option<f64>,
pub default_tiered_pricing: Option<Value>,
pub supported_capabilities: Option<Value>,
pub config: Option<Value>,
}
impl CreateAdminGlobalModelRecord {
#[allow(clippy::too_many_arguments)]
pub fn new(
id: String,
name: String,
display_name: String,
is_active: bool,
default_price_per_request: Option<f64>,
default_tiered_pricing: Option<Value>,
supported_capabilities: Option<Value>,
config: Option<Value>,
) -> Result<Self, crate::DataLayerError> {
if id.trim().is_empty() {
return Err(crate::DataLayerError::UnexpectedValue(
"global_models.id is empty".to_string(),
));
}
if name.trim().is_empty() {
return Err(crate::DataLayerError::UnexpectedValue(
"global_models.name is empty".to_string(),
));
}
if display_name.trim().is_empty() {
return Err(crate::DataLayerError::UnexpectedValue(
"global_models.display_name is empty".to_string(),
));
}
Ok(Self {
id,
name,
display_name,
is_active,
default_price_per_request,
default_tiered_pricing,
supported_capabilities,
config,
})
}
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct UpdateAdminGlobalModelRecord {
pub id: String,
pub display_name: String,
pub is_active: bool,
pub default_price_per_request: Option<f64>,
pub default_tiered_pricing: Option<Value>,
pub supported_capabilities: Option<Value>,
pub config: Option<Value>,
}
impl UpdateAdminGlobalModelRecord {
#[allow(clippy::too_many_arguments)]
pub fn new(
id: String,
display_name: String,
is_active: bool,
default_price_per_request: Option<f64>,
default_tiered_pricing: Option<Value>,
supported_capabilities: Option<Value>,
config: Option<Value>,
) -> Result<Self, crate::DataLayerError> {
if id.trim().is_empty() {
return Err(crate::DataLayerError::UnexpectedValue(
"global_models.id is empty".to_string(),
));
}
if display_name.trim().is_empty() {
return Err(crate::DataLayerError::UnexpectedValue(
"global_models.display_name is empty".to_string(),
));
}
Ok(Self {
id,
display_name,
is_active,
default_price_per_request,
default_tiered_pricing,
supported_capabilities,
config,
})
}
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct StoredPublicGlobalModelPage {
pub items: Vec<StoredPublicGlobalModel>,
pub total: usize,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct StoredAdminGlobalModelPage {
pub items: Vec<StoredAdminGlobalModel>,
pub total: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct StoredProviderModelStats {
pub provider_id: String,
pub total_models: u64,
pub active_models: u64,
}
impl StoredProviderModelStats {
pub fn new(
provider_id: String,
total_models: i64,
active_models: i64,
) -> Result<Self, crate::DataLayerError> {
if provider_id.trim().is_empty() {
return Err(crate::DataLayerError::UnexpectedValue(
"provider model stats provider_id is empty".to_string(),
));
}
if total_models < 0 || active_models < 0 {
return Err(crate::DataLayerError::UnexpectedValue(
"provider model stats count is negative".to_string(),
));
}
Ok(Self {
provider_id,
total_models: total_models as u64,
active_models: active_models as u64,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct StoredProviderActiveGlobalModel {
pub provider_id: String,
pub global_model_id: String,
}
impl StoredProviderActiveGlobalModel {
pub fn new(
provider_id: String,
global_model_id: String,
) -> Result<Self, crate::DataLayerError> {
if provider_id.trim().is_empty() || global_model_id.trim().is_empty() {
return Err(crate::DataLayerError::UnexpectedValue(
"provider active global model identity is empty".to_string(),
));
}
Ok(Self {
provider_id,
global_model_id,
})
}
}
#[async_trait]
pub trait GlobalModelReadRepository: Send + Sync {
async fn list_public_models(
&self,
query: &PublicGlobalModelQuery,
) -> Result<StoredPublicGlobalModelPage, crate::DataLayerError>;
async fn get_public_model_by_name(
&self,
model_name: &str,
) -> Result<Option<StoredPublicGlobalModel>, crate::DataLayerError>;
async fn list_public_catalog_models(
&self,
query: &PublicCatalogModelListQuery,
) -> Result<Vec<StoredPublicCatalogModel>, crate::DataLayerError>;
async fn search_public_catalog_models(
&self,
query: &PublicCatalogModelSearchQuery,
) -> Result<Vec<StoredPublicCatalogModel>, crate::DataLayerError>;
async fn list_admin_global_models(
&self,
query: &AdminGlobalModelListQuery,
) -> Result<StoredAdminGlobalModelPage, crate::DataLayerError>;
async fn list_admin_provider_models(
&self,
query: &AdminProviderModelListQuery,
) -> Result<Vec<StoredAdminProviderModel>, crate::DataLayerError>;
async fn list_admin_provider_available_source_models(
&self,
provider_id: &str,
) -> Result<Vec<StoredAdminProviderModel>, crate::DataLayerError>;
async fn get_admin_provider_model(
&self,
provider_id: &str,
model_id: &str,
) -> Result<Option<StoredAdminProviderModel>, crate::DataLayerError>;
async fn get_admin_global_model_by_id(
&self,
global_model_id: &str,
) -> Result<Option<StoredAdminGlobalModel>, crate::DataLayerError>;
async fn get_admin_global_model_by_name(
&self,
model_name: &str,
) -> Result<Option<StoredAdminGlobalModel>, crate::DataLayerError>;
async fn list_admin_provider_models_by_global_model_id(
&self,
global_model_id: &str,
) -> Result<Vec<StoredAdminProviderModel>, crate::DataLayerError>;
async fn list_provider_model_stats(
&self,
provider_ids: &[String],
) -> Result<Vec<StoredProviderModelStats>, crate::DataLayerError>;
async fn list_active_global_model_ids_by_provider_ids(
&self,
provider_ids: &[String],
) -> Result<Vec<StoredProviderActiveGlobalModel>, crate::DataLayerError>;
}
#[async_trait]
pub trait GlobalModelWriteRepository: Send + Sync {
async fn create_admin_provider_model(
&self,
record: &UpsertAdminProviderModelRecord,
) -> Result<Option<StoredAdminProviderModel>, crate::DataLayerError>;
async fn update_admin_provider_model(
&self,
record: &UpsertAdminProviderModelRecord,
) -> Result<Option<StoredAdminProviderModel>, crate::DataLayerError>;
async fn delete_admin_provider_model(
&self,
provider_id: &str,
model_id: &str,
) -> Result<bool, crate::DataLayerError>;
async fn create_admin_global_model(
&self,
record: &CreateAdminGlobalModelRecord,
) -> Result<Option<StoredAdminGlobalModel>, crate::DataLayerError>;
async fn update_admin_global_model(
&self,
record: &UpdateAdminGlobalModelRecord,
) -> Result<Option<StoredAdminGlobalModel>, crate::DataLayerError>;
async fn delete_admin_global_model(
&self,
global_model_id: &str,
) -> Result<bool, crate::DataLayerError>;
}