mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
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:
237
crates/aether-billing/src/service.rs
Normal file
237
crates/aether-billing/src/service.rs
Normal file
@@ -0,0 +1,237 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::default_rule::{normalize_task_type, DefaultBillingRuleGenerator};
|
||||
use crate::precision::quantize_cost;
|
||||
use crate::pricing::{BillingComputation, BillingModelPricingSnapshot, BillingUsageInput};
|
||||
use crate::schema::{
|
||||
BillingSnapshot, BillingSnapshotStatus, CostResult, BILLING_SNAPSHOT_SCHEMA_VERSION,
|
||||
};
|
||||
use crate::{
|
||||
normalize_input_tokens_for_billing, ExpressionEvaluationError, FormulaEngine,
|
||||
FormulaEvaluationStatus,
|
||||
};
|
||||
|
||||
pub struct BillingService {
|
||||
engine: FormulaEngine,
|
||||
}
|
||||
|
||||
impl BillingService {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
engine: FormulaEngine::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn calculate(
|
||||
&self,
|
||||
pricing: &BillingModelPricingSnapshot,
|
||||
input: &BillingUsageInput,
|
||||
) -> Result<BillingComputation, ExpressionEvaluationError> {
|
||||
let Some(rule) =
|
||||
DefaultBillingRuleGenerator::generate_for_pricing(pricing, &input.task_type)
|
||||
else {
|
||||
return Ok(BillingComputation {
|
||||
cost_result: CostResult {
|
||||
cost: 0.0,
|
||||
status: BillingSnapshotStatus::NoRule,
|
||||
snapshot: BillingSnapshot {
|
||||
schema_version: BILLING_SNAPSHOT_SCHEMA_VERSION.to_string(),
|
||||
rule_id: None,
|
||||
rule_name: None,
|
||||
scope: None,
|
||||
expression: None,
|
||||
resolved_dimensions: build_dimensions(input),
|
||||
resolved_variables: BTreeMap::new(),
|
||||
cost_breakdown: BTreeMap::new(),
|
||||
total_cost: 0.0,
|
||||
tier_index: None,
|
||||
tier_info: None,
|
||||
missing_required: Vec::new(),
|
||||
status: BillingSnapshotStatus::NoRule,
|
||||
calculated_at: now_marker(),
|
||||
engine_version: "2.0".to_string(),
|
||||
},
|
||||
},
|
||||
actual_total_cost: 0.0,
|
||||
rate_multiplier: pricing
|
||||
.rate_multiplier_for_api_format(input.api_format.as_deref()),
|
||||
is_free_tier: pricing.is_free_tier(),
|
||||
});
|
||||
};
|
||||
|
||||
let dims = build_dimensions(input);
|
||||
let result = self.engine.evaluate(
|
||||
&rule.expression,
|
||||
Some(&rule.variables),
|
||||
Some(&dims),
|
||||
Some(&rule.dimension_mappings),
|
||||
false,
|
||||
)?;
|
||||
|
||||
let status = match result.status {
|
||||
FormulaEvaluationStatus::Complete => BillingSnapshotStatus::Complete,
|
||||
FormulaEvaluationStatus::Incomplete => BillingSnapshotStatus::Incomplete,
|
||||
};
|
||||
let total_cost = if matches!(status, BillingSnapshotStatus::Complete) {
|
||||
result.cost
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let rate_multiplier = pricing.rate_multiplier_for_api_format(input.api_format.as_deref());
|
||||
let is_free_tier = pricing.is_free_tier();
|
||||
let actual_total_cost = if is_free_tier {
|
||||
0.0
|
||||
} else {
|
||||
quantize_cost(total_cost * rate_multiplier)
|
||||
};
|
||||
|
||||
Ok(BillingComputation {
|
||||
cost_result: CostResult {
|
||||
cost: total_cost,
|
||||
status,
|
||||
snapshot: BillingSnapshot {
|
||||
schema_version: BILLING_SNAPSHOT_SCHEMA_VERSION.to_string(),
|
||||
rule_id: Some(rule.id),
|
||||
rule_name: Some(rule.name),
|
||||
scope: Some(rule.scope),
|
||||
expression: Some(rule.expression),
|
||||
resolved_dimensions: result.resolved_dimensions,
|
||||
resolved_variables: result.resolved_variables,
|
||||
cost_breakdown: result.cost_breakdown,
|
||||
total_cost,
|
||||
tier_index: result.tier_index,
|
||||
tier_info: result.tier_info,
|
||||
missing_required: result.missing_required,
|
||||
status,
|
||||
calculated_at: now_marker(),
|
||||
engine_version: "2.0".to_string(),
|
||||
},
|
||||
},
|
||||
actual_total_cost,
|
||||
rate_multiplier,
|
||||
is_free_tier,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for BillingService {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
fn build_dimensions(input: &BillingUsageInput) -> BTreeMap<String, Value> {
|
||||
let normalized_input_tokens = normalize_input_tokens_for_billing(
|
||||
input.api_format.as_deref(),
|
||||
input.input_tokens,
|
||||
input.cache_read_tokens,
|
||||
);
|
||||
let total_input_context = input
|
||||
.input_tokens
|
||||
.saturating_add(input.cache_creation_tokens)
|
||||
.saturating_add(input.cache_read_tokens);
|
||||
|
||||
let mut out = BTreeMap::from([
|
||||
("input_tokens".to_string(), json!(normalized_input_tokens)),
|
||||
("output_tokens".to_string(), json!(input.output_tokens)),
|
||||
(
|
||||
"cache_creation_tokens".to_string(),
|
||||
json!(input.cache_creation_tokens),
|
||||
),
|
||||
(
|
||||
"cache_read_tokens".to_string(),
|
||||
json!(input.cache_read_tokens),
|
||||
),
|
||||
(
|
||||
"request_count".to_string(),
|
||||
json!(input.request_count.max(0)),
|
||||
),
|
||||
(
|
||||
"total_input_context".to_string(),
|
||||
json!(total_input_context),
|
||||
),
|
||||
(
|
||||
"effective_task_type".to_string(),
|
||||
json!(normalize_task_type(&input.task_type)),
|
||||
),
|
||||
]);
|
||||
|
||||
if let Some(cache_ttl_minutes) = input.cache_ttl_minutes {
|
||||
out.insert(
|
||||
"cache_ttl_minutes".to_string(),
|
||||
json!(cache_ttl_minutes.max(0)),
|
||||
);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn now_marker() -> String {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use super::BillingService;
|
||||
use crate::{BillingModelPricingSnapshot, BillingSnapshotStatus, BillingUsageInput};
|
||||
|
||||
fn pricing() -> BillingModelPricingSnapshot {
|
||||
BillingModelPricingSnapshot {
|
||||
provider_id: "provider-1".to_string(),
|
||||
provider_billing_type: Some("pay_as_you_go".to_string()),
|
||||
provider_api_key_id: Some("key-1".to_string()),
|
||||
provider_api_key_rate_multipliers: Some(json!({"openai:chat": 0.5})),
|
||||
provider_api_key_cache_ttl_minutes: Some(60),
|
||||
global_model_id: "global-model-1".to_string(),
|
||||
global_model_name: "gpt-5".to_string(),
|
||||
global_model_config: None,
|
||||
default_price_per_request: Some(0.02),
|
||||
default_tiered_pricing: Some(json!({
|
||||
"tiers": [{
|
||||
"up_to": null,
|
||||
"input_price_per_1m": 3.0,
|
||||
"output_price_per_1m": 15.0,
|
||||
"cache_creation_price_per_1m": 3.75,
|
||||
"cache_read_price_per_1m": 0.30
|
||||
}]
|
||||
})),
|
||||
model_id: Some("model-1".to_string()),
|
||||
model_provider_model_name: Some("gpt-5-upstream".to_string()),
|
||||
model_config: None,
|
||||
model_price_per_request: None,
|
||||
model_tiered_pricing: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn calculates_complete_snapshot_for_usage() {
|
||||
let result = BillingService::new()
|
||||
.calculate(
|
||||
&pricing(),
|
||||
&BillingUsageInput {
|
||||
task_type: "chat".to_string(),
|
||||
api_format: Some("openai:chat".to_string()),
|
||||
request_count: 1,
|
||||
input_tokens: 1_000,
|
||||
output_tokens: 500,
|
||||
cache_creation_tokens: 0,
|
||||
cache_read_tokens: 100,
|
||||
cache_ttl_minutes: Some(60),
|
||||
},
|
||||
)
|
||||
.expect("billing should calculate");
|
||||
|
||||
assert_eq!(result.cost_result.status, BillingSnapshotStatus::Complete);
|
||||
assert!(result.cost_result.cost > 0.0);
|
||||
assert!(result.actual_total_cost > 0.0);
|
||||
assert_eq!(result.rate_multiplier, 0.5);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user