feat: 扩展 cache creation token 细分统计与 effective_input_tokens 计费逻辑

- 新增 cache_creation_ephemeral_5m/1h_input_tokens 字段,区分不同 TTL 的缓存写入 token
- 引入 effective_input_tokens(扣除 cache read 后的有效输入 token),暴露给 usage 接口
- billing 规则生成器支持 5m/1h ephemeral cache 独立定价与分级计费
- usage_mapper 增加 Claude/Anthropic 格式映射,修复 OpenAI responses 格式字段兼容性
- 迁移逻辑增强:支持 checksum 容错、applied/pending 数量日志、逐步执行信息输出
- executor 抽离 LocalExecutionRequestOutcome 类型,统一 sync/stream 路径返回语义
- provider-transport auth 层新增 complete passthrough headers 构建逻辑
- 前端 usage 类型全面补充 effective_input_tokens、cache_creation_tokens、total_input_context 字段
This commit is contained in:
fawney19
2026-04-10 17:44:55 +08:00
parent 5014e2f5fd
commit 010ab127e2
64 changed files with 4217 additions and 477 deletions

View File

@@ -7,6 +7,7 @@ repository.workspace = true
description = "Shared admin contracts and pure helpers for Aether"
[dependencies]
aether-billing.workspace = true
aether-contracts.workspace = true
aether-data.workspace = true
aether-data-contracts.workspace = true

View File

@@ -1,4 +1,5 @@
use crate::observability::stats::{aggregate_usage_stats, parse_bounded_u32, round_to};
use aether_billing::normalize_input_tokens_for_billing;
use aether_data::repository::users::StoredUserSummary;
use aether_data_contracts::repository::{
provider_catalog::{StoredProviderCatalogEndpoint, StoredProviderCatalogProvider},
@@ -257,8 +258,11 @@ pub fn admin_usage_record_json(
"model": item.model,
"target_model": item.target_model,
"input_tokens": item.input_tokens,
"effective_input_tokens": admin_usage_effective_input_tokens(item),
"output_tokens": item.output_tokens,
"cache_creation_input_tokens": item.cache_creation_input_tokens,
"cache_creation_ephemeral_5m_input_tokens": item.cache_creation_ephemeral_5m_input_tokens,
"cache_creation_ephemeral_1h_input_tokens": item.cache_creation_ephemeral_1h_input_tokens,
"cache_read_input_tokens": item.cache_read_input_tokens,
"total_tokens": admin_usage_total_tokens(item),
"cost": round_to(item.total_cost_usd, 6),
@@ -290,12 +294,38 @@ pub fn admin_usage_record_json(
pub fn admin_usage_total_tokens(item: &StoredRequestUsageAudit) -> u64 {
item.input_tokens
.saturating_add(item.output_tokens)
.saturating_add(item.cache_creation_input_tokens)
.saturating_add(admin_usage_cache_creation_tokens(item))
.saturating_add(item.cache_read_input_tokens)
}
pub fn admin_usage_token_cache_hit_rate(input_tokens: u64, cache_read_tokens: u64) -> f64 {
let total_input_context = input_tokens.saturating_add(cache_read_tokens);
pub fn admin_usage_cache_creation_tokens(item: &StoredRequestUsageAudit) -> u64 {
let classified = item
.cache_creation_ephemeral_5m_input_tokens
.saturating_add(item.cache_creation_ephemeral_1h_input_tokens);
if item.cache_creation_input_tokens == 0 && classified > 0 {
classified
} else {
item.cache_creation_input_tokens
}
}
pub fn admin_usage_total_input_context(item: &StoredRequestUsageAudit) -> u64 {
item.input_tokens
.saturating_add(admin_usage_cache_creation_tokens(item))
.saturating_add(item.cache_read_input_tokens)
}
pub fn admin_usage_effective_input_tokens(item: &StoredRequestUsageAudit) -> u64 {
let api_format = item
.endpoint_api_format
.as_deref()
.or(item.api_format.as_deref());
let input_tokens = i64::try_from(item.input_tokens).unwrap_or(i64::MAX);
let cache_read_tokens = i64::try_from(item.cache_read_input_tokens).unwrap_or(i64::MAX);
normalize_input_tokens_for_billing(api_format, input_tokens, cache_read_tokens) as u64
}
pub fn admin_usage_token_cache_hit_rate(total_input_context: u64, cache_read_tokens: u64) -> f64 {
if total_input_context == 0 {
0.0
} else {
@@ -306,20 +336,46 @@ pub fn admin_usage_token_cache_hit_rate(input_tokens: u64, cache_read_tokens: u6
}
}
fn admin_usage_provider_display_name(item: &StoredRequestUsageAudit) -> Option<String> {
let provider_name = item.provider_name.trim();
if provider_name.is_empty() || matches!(provider_name, "unknown" | "pending") {
None
} else {
Some(item.provider_name.clone())
}
}
pub fn admin_usage_aggregation_by_model_json(
usage: &[StoredRequestUsageAudit],
limit: usize,
) -> Value {
let mut grouped: BTreeMap<String, (u64, u64, u64, u64, f64, f64)> = BTreeMap::new();
#[allow(clippy::type_complexity)]
let mut grouped: BTreeMap<String, (u64, u64, u64, u64, u64, u64, u64, u64, u64, f64, f64)> =
BTreeMap::new();
for item in usage {
let key = item.model.clone();
let entry = grouped.entry(key).or_insert((0, 0, 0, 0, 0.0, 0.0));
let entry = grouped
.entry(key)
.or_insert((0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0, 0.0));
entry.0 = entry.0.saturating_add(1);
entry.1 = entry.1.saturating_add(item.total_tokens);
entry.2 = entry.2.saturating_add(item.input_tokens);
entry.3 = entry.3.saturating_add(item.cache_read_input_tokens);
entry.4 += item.total_cost_usd;
entry.5 += item.actual_total_cost_usd;
entry.3 = entry.3.saturating_add(item.output_tokens);
entry.4 = entry
.4
.saturating_add(admin_usage_effective_input_tokens(item));
entry.5 = entry
.5
.saturating_add(admin_usage_cache_creation_tokens(item));
entry.6 = entry
.6
.saturating_add(item.cache_creation_ephemeral_5m_input_tokens);
entry.7 = entry
.7
.saturating_add(item.cache_creation_ephemeral_1h_input_tokens);
entry.8 = entry.8.saturating_add(item.cache_read_input_tokens);
entry.9 += item.total_cost_usd;
entry.10 += item.actual_total_cost_usd;
}
let mut items: Vec<Value> = grouped
@@ -331,6 +387,11 @@ pub fn admin_usage_aggregation_by_model_json(
request_count,
total_tokens,
input_tokens,
output_tokens,
effective_input_tokens,
cache_creation_tokens,
cache_creation_ephemeral_5m_tokens,
cache_creation_ephemeral_1h_tokens,
cache_read_tokens,
total_cost,
actual_cost,
@@ -340,13 +401,23 @@ pub fn admin_usage_aggregation_by_model_json(
"model": model,
"request_count": request_count,
"total_tokens": total_tokens,
"total_input_context": input_tokens.saturating_add(cache_read_tokens),
"output_tokens": total_tokens.saturating_sub(input_tokens),
"effective_input_tokens": effective_input_tokens,
"total_input_context": input_tokens
.saturating_add(cache_creation_tokens)
.saturating_add(cache_read_tokens),
"output_tokens": output_tokens,
"total_cost": round_to(total_cost, 6),
"actual_cost": round_to(actual_cost, 6),
"cache_creation_tokens": cache_creation_tokens,
"cache_creation_ephemeral_5m_tokens": cache_creation_ephemeral_5m_tokens,
"cache_creation_ephemeral_1h_tokens": cache_creation_ephemeral_1h_tokens,
"cache_read_tokens": cache_read_tokens,
"cache_creation_tokens": 0,
"cache_hit_rate": admin_usage_token_cache_hit_rate(input_tokens, cache_read_tokens),
"cache_hit_rate": admin_usage_token_cache_hit_rate(
input_tokens
.saturating_add(cache_creation_tokens)
.saturating_add(cache_read_tokens),
cache_read_tokens,
),
})
},
)
@@ -372,24 +443,75 @@ pub fn admin_usage_aggregation_by_provider_json(
limit: usize,
) -> Value {
#[allow(clippy::type_complexity)]
let mut grouped: BTreeMap<String, (u64, u64, u64, u64, f64, f64, u64, u64)> = BTreeMap::new();
let mut grouped: BTreeMap<
String,
(
String,
u64,
u64,
u64,
u64,
u64,
u64,
u64,
u64,
u64,
f64,
f64,
u64,
u64,
),
> = BTreeMap::new();
for item in usage {
let key = item
.provider_id
.clone()
.unwrap_or_else(|| "unknown".to_string());
let entry = grouped.entry(key).or_insert((0, 0, 0, 0, 0.0, 0.0, 0, 0));
entry.0 = entry.0.saturating_add(1);
entry.1 = entry.1.saturating_add(item.total_tokens);
entry.2 = entry.2.saturating_add(item.input_tokens);
entry.3 = entry.3.saturating_add(item.cache_read_input_tokens);
entry.4 += item.total_cost_usd;
entry.5 += item.actual_total_cost_usd;
let provider_name =
admin_usage_provider_display_name(item).unwrap_or_else(|| "Unknown".to_string());
let entry = grouped.entry(key).or_insert((
provider_name.clone(),
0,
0,
0,
0,
0,
0,
0,
0,
0,
0.0,
0.0,
0,
0,
));
if entry.0 == "Unknown" && provider_name != "Unknown" {
entry.0 = provider_name;
}
entry.1 = entry.1.saturating_add(1);
entry.2 = entry.2.saturating_add(item.total_tokens);
entry.3 = entry.3.saturating_add(item.input_tokens);
entry.4 = entry.4.saturating_add(item.output_tokens);
entry.5 = entry
.5
.saturating_add(admin_usage_effective_input_tokens(item));
entry.6 = entry
.6
.saturating_add(item.response_time_ms.unwrap_or_default());
.saturating_add(admin_usage_cache_creation_tokens(item));
entry.7 = entry
.7
.saturating_add(item.cache_creation_ephemeral_5m_input_tokens);
entry.8 = entry
.8
.saturating_add(item.cache_creation_ephemeral_1h_input_tokens);
entry.9 = entry.9.saturating_add(item.cache_read_input_tokens);
entry.10 += item.total_cost_usd;
entry.11 += item.actual_total_cost_usd;
entry.12 = entry
.12
.saturating_add(item.response_time_ms.unwrap_or_default());
entry.13 = entry
.13
.saturating_add(if admin_usage_is_success(item) { 1 } else { 0 });
}
@@ -399,9 +521,15 @@ pub fn admin_usage_aggregation_by_provider_json(
|(
provider_id,
(
provider_name,
request_count,
total_tokens,
input_tokens,
output_tokens,
effective_input_tokens,
cache_creation_tokens,
cache_creation_ephemeral_5m_tokens,
cache_creation_ephemeral_1h_tokens,
cache_read_tokens,
total_cost,
actual_cost,
@@ -422,19 +550,29 @@ pub fn admin_usage_aggregation_by_provider_json(
};
json!({
"provider_id": provider_id,
"provider": Value::Null,
"provider": provider_name,
"request_count": request_count,
"total_tokens": total_tokens,
"total_input_context": input_tokens.saturating_add(cache_read_tokens),
"output_tokens": total_tokens.saturating_sub(input_tokens),
"effective_input_tokens": effective_input_tokens,
"total_input_context": input_tokens
.saturating_add(cache_creation_tokens)
.saturating_add(cache_read_tokens),
"output_tokens": output_tokens,
"total_cost": round_to(total_cost, 6),
"actual_cost": round_to(actual_cost, 6),
"avg_response_time_ms": avg_response_time_ms,
"success_rate": success_rate,
"error_count": error_count,
"cache_creation_tokens": cache_creation_tokens,
"cache_creation_ephemeral_5m_tokens": cache_creation_ephemeral_5m_tokens,
"cache_creation_ephemeral_1h_tokens": cache_creation_ephemeral_1h_tokens,
"cache_read_tokens": cache_read_tokens,
"cache_creation_tokens": 0,
"cache_hit_rate": admin_usage_token_cache_hit_rate(input_tokens, cache_read_tokens),
"cache_hit_rate": admin_usage_token_cache_hit_rate(
input_tokens
.saturating_add(cache_creation_tokens)
.saturating_add(cache_read_tokens),
cache_read_tokens,
),
})
},
)
@@ -460,21 +598,39 @@ pub fn admin_usage_aggregation_by_api_format_json(
limit: usize,
) -> Value {
#[allow(clippy::type_complexity)]
let mut grouped: BTreeMap<String, (u64, u64, u64, u64, f64, f64, u64)> = BTreeMap::new();
let mut grouped: BTreeMap<
String,
(u64, u64, u64, u64, u64, u64, u64, u64, u64, f64, f64, u64),
> = BTreeMap::new();
for item in usage {
let key = item
.api_format
.clone()
.unwrap_or_else(|| "unknown".to_string());
let entry = grouped.entry(key).or_insert((0, 0, 0, 0, 0.0, 0.0, 0));
let entry = grouped
.entry(key)
.or_insert((0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0, 0.0, 0));
entry.0 = entry.0.saturating_add(1);
entry.1 = entry.1.saturating_add(item.total_tokens);
entry.2 = entry.2.saturating_add(item.input_tokens);
entry.3 = entry.3.saturating_add(item.cache_read_input_tokens);
entry.4 += item.total_cost_usd;
entry.5 += item.actual_total_cost_usd;
entry.3 = entry.3.saturating_add(item.output_tokens);
entry.4 = entry
.4
.saturating_add(admin_usage_effective_input_tokens(item));
entry.5 = entry
.5
.saturating_add(admin_usage_cache_creation_tokens(item));
entry.6 = entry
.6
.saturating_add(item.cache_creation_ephemeral_5m_input_tokens);
entry.7 = entry
.7
.saturating_add(item.cache_creation_ephemeral_1h_input_tokens);
entry.8 = entry.8.saturating_add(item.cache_read_input_tokens);
entry.9 += item.total_cost_usd;
entry.10 += item.actual_total_cost_usd;
entry.11 = entry
.11
.saturating_add(item.response_time_ms.unwrap_or_default());
}
@@ -487,6 +643,11 @@ pub fn admin_usage_aggregation_by_api_format_json(
request_count,
total_tokens,
input_tokens,
output_tokens,
effective_input_tokens,
cache_creation_tokens,
cache_creation_ephemeral_5m_tokens,
cache_creation_ephemeral_1h_tokens,
cache_read_tokens,
total_cost,
actual_cost,
@@ -502,14 +663,24 @@ pub fn admin_usage_aggregation_by_api_format_json(
"api_format": api_format,
"request_count": request_count,
"total_tokens": total_tokens,
"total_input_context": input_tokens.saturating_add(cache_read_tokens),
"output_tokens": total_tokens.saturating_sub(input_tokens),
"effective_input_tokens": effective_input_tokens,
"total_input_context": input_tokens
.saturating_add(cache_creation_tokens)
.saturating_add(cache_read_tokens),
"output_tokens": output_tokens,
"total_cost": round_to(total_cost, 6),
"actual_cost": round_to(actual_cost, 6),
"avg_response_time_ms": avg_response_time_ms,
"cache_creation_tokens": cache_creation_tokens,
"cache_creation_ephemeral_5m_tokens": cache_creation_ephemeral_5m_tokens,
"cache_creation_ephemeral_1h_tokens": cache_creation_ephemeral_1h_tokens,
"cache_read_tokens": cache_read_tokens,
"cache_creation_tokens": 0,
"cache_hit_rate": admin_usage_token_cache_hit_rate(input_tokens, cache_read_tokens),
"cache_hit_rate": admin_usage_token_cache_hit_rate(
input_tokens
.saturating_add(cache_creation_tokens)
.saturating_add(cache_read_tokens),
cache_read_tokens,
),
})
},
)
@@ -868,9 +1039,14 @@ pub fn build_admin_usage_summary_stats_response(
usage: &[StoredRequestUsageAudit],
) -> Response<Body> {
let aggregate = aggregate_usage_stats(usage);
let cache_creation_tokens: u64 = usage
let cache_creation_tokens: u64 = usage.iter().map(admin_usage_cache_creation_tokens).sum();
let cache_creation_ephemeral_5m_tokens: u64 = usage
.iter()
.map(|item| item.cache_creation_input_tokens)
.map(|item| item.cache_creation_ephemeral_5m_input_tokens)
.sum();
let cache_creation_ephemeral_1h_tokens: u64 = usage
.iter()
.map(|item| item.cache_creation_ephemeral_1h_input_tokens)
.sum();
let cache_read_tokens: u64 = usage.iter().map(|item| item.cache_read_input_tokens).sum();
let cache_creation_cost: f64 = usage.iter().map(|item| item.cache_creation_cost_usd).sum();
@@ -896,6 +1072,8 @@ pub fn build_admin_usage_summary_stats_response(
"error_rate": error_rate,
"cache_stats": {
"cache_creation_tokens": cache_creation_tokens,
"cache_creation_ephemeral_5m_tokens": cache_creation_ephemeral_5m_tokens,
"cache_creation_ephemeral_1h_tokens": cache_creation_ephemeral_1h_tokens,
"cache_read_tokens": cache_read_tokens,
"cache_creation_cost": round_to(cache_creation_cost, 6),
"cache_read_cost": round_to(cache_read_cost, 6),
@@ -916,8 +1094,11 @@ pub fn build_admin_usage_active_requests_response(
"id": item.id,
"status": item.status,
"input_tokens": item.input_tokens,
"effective_input_tokens": admin_usage_effective_input_tokens(item),
"output_tokens": item.output_tokens,
"cache_creation_input_tokens": item.cache_creation_input_tokens,
"cache_creation_ephemeral_5m_input_tokens": item.cache_creation_ephemeral_5m_input_tokens,
"cache_creation_ephemeral_1h_input_tokens": item.cache_creation_ephemeral_1h_input_tokens,
"cache_read_input_tokens": item.cache_read_input_tokens,
"cost": round_to(item.total_cost_usd, 6),
"actual_cost": round_to(item.actual_total_cost_usd, 6),

View File

@@ -184,6 +184,7 @@ pub fn apply_codex_openai_cli_special_headers(
if let Some(account_id) = extract_codex_account_id(decrypted_auth_config_raw) {
provider_request_headers.insert("chatgpt-account-id".to_string(), account_id);
}
if !provider_request_headers
.get("x-client-request-id")
.map(|value| !value.trim().is_empty())
@@ -200,6 +201,7 @@ pub fn apply_codex_openai_cli_special_headers(
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty());
let Some(short_id) = prompt_cache_key.and_then(build_short_codex_header_id) else {
return;
};
@@ -213,6 +215,7 @@ pub fn apply_codex_openai_cli_special_headers(
.eq_ignore_ascii_case("openai:compact")
&& !header_map_has_non_empty_value(original_headers, "conversation_id")
{
provider_request_headers.insert("conversation_id".to_string(), short_id);
let session_id = provider_request_headers.get("session_id").unwrap();
provider_request_headers.insert("conversation_id".to_string(), session_id.clone());
}
}

View File

@@ -124,8 +124,18 @@ pub fn build_cross_format_openai_cli_request_body(
#[cfg(test)]
mod tests {
use super::build_local_openai_cli_request_body;
use super::{build_cross_format_openai_cli_request_body, build_local_openai_chat_request_body};
use serde_json::json;
use serde_json::{json, Value};
fn object_keys(value: &Value) -> Vec<&str> {
value
.as_object()
.expect("json object")
.keys()
.map(String::as_str)
.collect()
}
#[test]
fn builds_openai_chat_cross_format_request_body_from_openai_cli_source() {
@@ -148,6 +158,29 @@ mod tests {
assert_eq!(provider_request_body["messages"][0]["content"], "hello");
}
#[test]
fn local_openai_cli_request_body_preserves_original_field_order() {
let body_json: Value = serde_json::from_str(
r#"{
"model": "gpt-5",
"include": ["reasoning.encrypted_content"],
"input": [],
"instructions": "Keep order"
}"#,
)
.expect("request json should parse");
let provider_request_body =
build_local_openai_cli_request_body(&body_json, "gpt-5-upstream", false)
.expect("openai cli body should build");
assert_eq!(
object_keys(&provider_request_body),
vec!["model", "include", "input", "instructions"]
);
assert_eq!(provider_request_body["model"], "gpt-5-upstream");
}
#[test]
fn builds_streaming_local_openai_chat_request_body_with_include_usage() {
let body_json = json!({

View File

@@ -50,6 +50,14 @@ impl DefaultBillingRuleGenerator {
"cache_creation_price_per_1m".to_string(),
json!(base_cache_creation_price),
);
variables.insert(
"cache_creation_ephemeral_5m_price_per_1m".to_string(),
json!(base_cache_creation_price),
);
variables.insert(
"cache_creation_ephemeral_1h_price_per_1m".to_string(),
json!(base_cache_creation_price),
);
variables.insert(
"cache_read_price_per_1m".to_string(),
json!(base_cache_read_price),
@@ -61,6 +69,21 @@ impl DefaultBillingRuleGenerator {
("input_tokens", "input_tokens", json!(0)),
("output_tokens", "output_tokens", json!(0)),
("cache_creation_tokens", "cache_creation_tokens", json!(0)),
(
"cache_creation_ephemeral_5m_tokens",
"cache_creation_ephemeral_5m_tokens",
json!(0),
),
(
"cache_creation_ephemeral_1h_tokens",
"cache_creation_ephemeral_1h_tokens",
json!(0),
),
(
"cache_creation_uncategorized_tokens",
"cache_creation_uncategorized_tokens",
json!(0),
),
("cache_read_tokens", "cache_read_tokens", json!(0)),
("request_count", "request_count", json!(1)),
] {
@@ -83,8 +106,16 @@ impl DefaultBillingRuleGenerator {
"output_tokens * output_price_per_1m / 1000000",
),
(
"cache_creation_cost",
"cache_creation_tokens * cache_creation_price_per_1m / 1000000",
"cache_creation_uncategorized_cost",
"cache_creation_uncategorized_tokens * cache_creation_price_per_1m / 1000000",
),
(
"cache_creation_ephemeral_5m_cost",
"cache_creation_ephemeral_5m_tokens * cache_creation_ephemeral_5m_price_per_1m / 1000000",
),
(
"cache_creation_ephemeral_1h_cost",
"cache_creation_ephemeral_1h_tokens * cache_creation_ephemeral_1h_price_per_1m / 1000000",
),
(
"cache_read_cost",
@@ -136,6 +167,30 @@ impl DefaultBillingRuleGenerator {
"default": base_cache_creation_price,
}),
);
dimension_mappings.insert(
"cache_creation_ephemeral_5m_price_per_1m".to_string(),
json!({
"source": "tiered",
"tier_key": "total_input_context",
"allow_zero": true,
"ttl_key": "cache_creation_ephemeral_5m_ttl_minutes",
"ttl_value_key": "cache_creation_price_per_1m",
"tiers": build_tier_entries(&tiers, "cache_creation_price_per_1m", Some(1.25), true),
"default": base_cache_creation_price,
}),
);
dimension_mappings.insert(
"cache_creation_ephemeral_1h_price_per_1m".to_string(),
json!({
"source": "tiered",
"tier_key": "total_input_context",
"allow_zero": true,
"ttl_key": "cache_creation_ephemeral_1h_ttl_minutes",
"ttl_value_key": "cache_creation_price_per_1m",
"tiers": build_tier_entries(&tiers, "cache_creation_price_per_1m", Some(1.25), true),
"default": base_cache_creation_price,
}),
);
dimension_mappings.insert(
"cache_read_price_per_1m".to_string(),
json!({
@@ -154,9 +209,7 @@ impl DefaultBillingRuleGenerator {
id: "__default__".to_string(),
name: format!("Default rule for {}", pricing.global_model_name),
task_type: normalize_task_type(task_type).to_string(),
expression:
"input_cost + output_cost + cache_creation_cost + cache_read_cost + request_cost"
.to_string(),
expression: "input_cost + output_cost + cache_creation_uncategorized_cost + cache_creation_ephemeral_5m_cost + cache_creation_ephemeral_1h_cost + cache_read_cost + request_cost".to_string(),
variables,
dimension_mappings,
scope: "default".to_string(),

View File

@@ -73,6 +73,14 @@ pub async fn enrich_usage_event_with_billing(
input_tokens: event.data.input_tokens.unwrap_or_default() as i64,
output_tokens: event.data.output_tokens.unwrap_or_default() as i64,
cache_creation_tokens: event.data.cache_creation_input_tokens.unwrap_or_default() as i64,
cache_creation_ephemeral_5m_tokens: event
.data
.cache_creation_ephemeral_5m_input_tokens
.unwrap_or_default() as i64,
cache_creation_ephemeral_1h_tokens: event
.data
.cache_creation_ephemeral_1h_input_tokens
.unwrap_or_default() as i64,
cache_read_tokens: event.data.cache_read_input_tokens.unwrap_or_default() as i64,
cache_ttl_minutes: pricing.provider_api_key_cache_ttl_minutes,
};

View File

@@ -66,6 +66,8 @@ pub struct BillingUsageInput {
pub input_tokens: i64,
pub output_tokens: i64,
pub cache_creation_tokens: i64,
pub cache_creation_ephemeral_5m_tokens: i64,
pub cache_creation_ephemeral_1h_tokens: i64,
pub cache_read_tokens: i64,
pub cache_ttl_minutes: Option<i64>,
}
@@ -79,6 +81,8 @@ impl BillingUsageInput {
input_tokens: 0,
output_tokens: 0,
cache_creation_tokens: 0,
cache_creation_ephemeral_5m_tokens: 0,
cache_creation_ephemeral_1h_tokens: 0,
cache_read_tokens: 0,
cache_ttl_minutes: None,
}

View File

@@ -129,6 +129,13 @@ fn build_dimensions(input: &BillingUsageInput) -> BTreeMap<String, Value> {
input.input_tokens,
input.cache_read_tokens,
);
let classified_cache_creation_tokens = input
.cache_creation_ephemeral_5m_tokens
.saturating_add(input.cache_creation_ephemeral_1h_tokens);
let cache_creation_uncategorized_tokens = input
.cache_creation_tokens
.saturating_sub(classified_cache_creation_tokens)
.max(0);
let total_input_context = input
.input_tokens
.saturating_add(input.cache_creation_tokens)
@@ -141,6 +148,18 @@ fn build_dimensions(input: &BillingUsageInput) -> BTreeMap<String, Value> {
"cache_creation_tokens".to_string(),
json!(input.cache_creation_tokens),
),
(
"cache_creation_ephemeral_5m_tokens".to_string(),
json!(input.cache_creation_ephemeral_5m_tokens),
),
(
"cache_creation_ephemeral_1h_tokens".to_string(),
json!(input.cache_creation_ephemeral_1h_tokens),
),
(
"cache_creation_uncategorized_tokens".to_string(),
json!(cache_creation_uncategorized_tokens),
),
(
"cache_read_tokens".to_string(),
json!(input.cache_read_tokens),
@@ -159,6 +178,15 @@ fn build_dimensions(input: &BillingUsageInput) -> BTreeMap<String, Value> {
),
]);
out.insert(
"cache_creation_ephemeral_5m_ttl_minutes".to_string(),
json!(5),
);
out.insert(
"cache_creation_ephemeral_1h_ttl_minutes".to_string(),
json!(60),
);
if let Some(cache_ttl_minutes) = input.cache_ttl_minutes {
out.insert(
"cache_ttl_minutes".to_string(),
@@ -223,6 +251,8 @@ mod tests {
input_tokens: 1_000,
output_tokens: 500,
cache_creation_tokens: 0,
cache_creation_ephemeral_5m_tokens: 0,
cache_creation_ephemeral_1h_tokens: 0,
cache_read_tokens: 100,
cache_ttl_minutes: Some(60),
},

View File

@@ -28,6 +28,8 @@ pub struct StoredRequestUsageAudit {
pub output_tokens: u64,
pub total_tokens: u64,
pub cache_creation_input_tokens: u64,
pub cache_creation_ephemeral_5m_input_tokens: u64,
pub cache_creation_ephemeral_1h_input_tokens: u64,
pub cache_read_input_tokens: u64,
pub cache_creation_cost_usd: f64,
pub cache_read_cost_usd: f64,
@@ -166,6 +168,8 @@ impl StoredRequestUsageAudit {
output_tokens: parse_u64(output_tokens, "usage.output_tokens")?,
total_tokens: parse_u64(total_tokens, "usage.total_tokens")?,
cache_creation_input_tokens: 0,
cache_creation_ephemeral_5m_input_tokens: 0,
cache_creation_ephemeral_1h_input_tokens: 0,
cache_read_input_tokens: 0,
cache_creation_cost_usd: 0.0,
cache_read_cost_usd: 0.0,
@@ -346,6 +350,8 @@ pub struct UpsertUsageRecord {
pub output_tokens: Option<u64>,
pub total_tokens: Option<u64>,
pub cache_creation_input_tokens: Option<u64>,
pub cache_creation_ephemeral_5m_input_tokens: Option<u64>,
pub cache_creation_ephemeral_1h_input_tokens: Option<u64>,
pub cache_read_input_tokens: Option<u64>,
pub cache_creation_cost_usd: Option<f64>,
pub cache_read_cost_usd: Option<f64>,
@@ -604,6 +610,8 @@ mod tests {
output_tokens: Some(20),
total_tokens: Some(30),
cache_creation_input_tokens: None,
cache_creation_ephemeral_5m_input_tokens: None,
cache_creation_ephemeral_1h_input_tokens: None,
cache_read_input_tokens: None,
cache_creation_cost_usd: None,
cache_read_cost_usd: None,

View File

@@ -20,5 +20,6 @@ sha2.workspace = true
sqlx = { workspace = true, features = ["migrate", "macros"] }
thiserror.workspace = true
tokio.workspace = true
tracing.workspace = true
url.workspace = true
uuid.workspace = true

View File

@@ -0,0 +1,124 @@
-- Align Rust-managed migrations with legacy Alembic revision
-- c3d4e5f6a7b8 (usage_token_semantics_v2).
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'usage'
AND column_name = 'total_tokens'
) AND NOT EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'usage'
AND column_name = 'input_output_total_tokens'
) THEN
ALTER TABLE "usage" RENAME COLUMN total_tokens TO input_output_total_tokens;
END IF;
END $$;
ALTER TABLE "usage"
ADD COLUMN IF NOT EXISTS input_output_total_tokens INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS cache_creation_input_tokens_5m INTEGER NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS cache_creation_input_tokens_1h INTEGER NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS input_context_tokens INTEGER NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS total_tokens INTEGER NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS cache_creation_cost_usd_5m NUMERIC(20, 8) NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS cache_creation_cost_usd_1h NUMERIC(20, 8) NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS actual_cache_creation_cost_usd_5m NUMERIC(20, 8) NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS actual_cache_creation_cost_usd_1h NUMERIC(20, 8) NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS actual_cache_cost_usd NUMERIC(20, 8) NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS cache_creation_price_per_1m_5m NUMERIC(20, 8),
ADD COLUMN IF NOT EXISTS cache_creation_price_per_1m_1h NUMERIC(20, 8);
UPDATE "usage"
SET
input_output_total_tokens = src.new_iot,
input_context_tokens = src.new_ict,
total_tokens = src.new_total,
cache_creation_cost_usd_5m = src.new_cc5m,
cache_creation_cost_usd_1h = src.new_cc1h,
actual_cache_creation_cost_usd_5m = src.new_acc5m,
actual_cache_creation_cost_usd_1h = src.new_acc1h,
actual_cache_cost_usd = src.new_accu,
cache_creation_price_per_1m_5m = src.new_cp5m,
cache_creation_price_per_1m_1h = src.new_cp1h,
cache_cost_usd = src.new_ccu
FROM (
SELECT
id,
COALESCE(
input_output_total_tokens,
COALESCE(input_tokens, 0) + COALESCE(output_tokens, 0)
) AS new_iot,
COALESCE(input_tokens, 0) + COALESCE(cache_read_input_tokens, 0) AS new_ict,
COALESCE(
input_output_total_tokens,
COALESCE(input_tokens, 0) + COALESCE(output_tokens, 0)
) + COALESCE(cache_creation_input_tokens, 0) + COALESCE(cache_read_input_tokens, 0) AS new_total,
CASE
WHEN COALESCE(cache_creation_input_tokens_5m, 0) > 0
AND COALESCE(cache_creation_input_tokens_1h, 0) = 0
THEN COALESCE(cache_creation_cost_usd, 0)
WHEN COALESCE(cache_creation_input_tokens_5m, 0) > 0
AND COALESCE(cache_creation_input_tokens, 0) > 0
THEN COALESCE(cache_creation_cost_usd, 0)
* (COALESCE(cache_creation_input_tokens_5m, 0) * 1.0
/ GREATEST(COALESCE(cache_creation_input_tokens, 0), 1))
ELSE 0
END AS new_cc5m,
CASE
WHEN COALESCE(cache_creation_input_tokens_1h, 0) > 0
AND COALESCE(cache_creation_input_tokens_5m, 0) = 0
THEN COALESCE(cache_creation_cost_usd, 0)
WHEN COALESCE(cache_creation_input_tokens_1h, 0) > 0
AND COALESCE(cache_creation_input_tokens, 0) > 0
THEN COALESCE(cache_creation_cost_usd, 0)
* (COALESCE(cache_creation_input_tokens_1h, 0) * 1.0
/ GREATEST(COALESCE(cache_creation_input_tokens, 0), 1))
ELSE 0
END AS new_cc1h,
CASE
WHEN COALESCE(cache_creation_input_tokens_5m, 0) > 0
AND COALESCE(cache_creation_input_tokens_1h, 0) = 0
THEN COALESCE(actual_cache_creation_cost_usd, 0)
WHEN COALESCE(cache_creation_input_tokens_5m, 0) > 0
AND COALESCE(cache_creation_input_tokens, 0) > 0
THEN COALESCE(actual_cache_creation_cost_usd, 0)
* (COALESCE(cache_creation_input_tokens_5m, 0) * 1.0
/ GREATEST(COALESCE(cache_creation_input_tokens, 0), 1))
ELSE 0
END AS new_acc5m,
CASE
WHEN COALESCE(cache_creation_input_tokens_1h, 0) > 0
AND COALESCE(cache_creation_input_tokens_5m, 0) = 0
THEN COALESCE(actual_cache_creation_cost_usd, 0)
WHEN COALESCE(cache_creation_input_tokens_1h, 0) > 0
AND COALESCE(cache_creation_input_tokens, 0) > 0
THEN COALESCE(actual_cache_creation_cost_usd, 0)
* (COALESCE(cache_creation_input_tokens_1h, 0) * 1.0
/ GREATEST(COALESCE(cache_creation_input_tokens, 0), 1))
ELSE 0
END AS new_acc1h,
COALESCE(actual_cache_creation_cost_usd, 0)
+ COALESCE(actual_cache_read_cost_usd, 0) AS new_accu,
CASE
WHEN COALESCE(cache_creation_input_tokens_5m, 0) > 0
AND COALESCE(cache_creation_input_tokens_1h, 0) = 0
THEN cache_creation_price_per_1m
ELSE NULL
END AS new_cp5m,
CASE
WHEN COALESCE(cache_creation_input_tokens_1h, 0) > 0
AND COALESCE(cache_creation_input_tokens_5m, 0) = 0
THEN cache_creation_price_per_1m
ELSE NULL
END AS new_cp1h,
COALESCE(cache_creation_cost_usd, 0)
+ COALESCE(cache_read_cost_usd, 0) AS new_ccu
FROM "usage"
) AS src
WHERE "usage".id = src.id;

View File

@@ -0,0 +1,22 @@
# Public Schema Snapshot
This directory stores a searchable snapshot of the current local Postgres `public` schema.
Purpose:
- Make legacy Python-era columns discoverable without reverse-tracing every migration.
- Keep `migrations/20260403000000_baseline.sql` as a no-op handoff point instead of stuffing the full legacy schema into a fake baseline.
Files:
- `current-public-tables.tsv`: `table_name`, `column_count`
- `current-public-columns.tsv`: `table_name`, `ordinal_position`, `column_name`, `data_type`, `is_nullable`, `column_default`, `column_comment`
Source:
- Generated from the local `aether` Postgres database on `2026-04-10`.
Refresh commands:
```bash
docker compose exec -T postgres psql -U postgres -d aether -At -F $'\t' -c "SELECT table_name, COUNT(*) FROM information_schema.columns WHERE table_schema = 'public' GROUP BY table_name ORDER BY table_name;"
docker compose exec -T postgres psql -U postgres -d aether -At -F $'\t' -c "SELECT c.table_name, c.ordinal_position, c.column_name, c.data_type, c.is_nullable, COALESCE(c.column_default, ''), COALESCE(pg_catalog.col_description((quote_ident(c.table_schema) || '.' || quote_ident(c.table_name))::regclass::oid, c.ordinal_position), '') FROM information_schema.columns c WHERE c.table_schema = 'public' ORDER BY c.table_name, c.ordinal_position;"
```
This snapshot is documentation only. Runtime code must still treat the actual database as source of truth.

View File

@@ -0,0 +1,831 @@
table_name ordinal_position column_name data_type is_nullable column_default column_comment
_orphan_api_keys_backup 1 id character varying YES
_orphan_api_keys_backup 2 api_key character varying YES
_orphan_api_keys_backup 3 name character varying YES
_orphan_api_keys_backup 4 note character varying YES
_orphan_api_keys_backup 5 rate_multiplier double precision YES
_orphan_api_keys_backup 6 internal_priority integer YES
_orphan_api_keys_backup 7 global_priority integer YES
_orphan_api_keys_backup 8 max_concurrent integer YES
_orphan_api_keys_backup 9 allowed_models json YES
_orphan_api_keys_backup 10 capabilities json YES
_orphan_api_keys_backup 11 learned_max_concurrent integer YES
_orphan_api_keys_backup 12 concurrent_429_count integer YES
_orphan_api_keys_backup 13 rpm_429_count integer YES
_orphan_api_keys_backup 14 last_429_at timestamp with time zone YES
_orphan_api_keys_backup 15 last_429_type character varying YES
_orphan_api_keys_backup 16 last_concurrent_peak integer YES
_orphan_api_keys_backup 17 adjustment_history json YES
_orphan_api_keys_backup 18 utilization_samples json YES
_orphan_api_keys_backup 19 last_probe_increase_at timestamp with time zone YES
_orphan_api_keys_backup 20 cache_ttl_minutes integer YES
_orphan_api_keys_backup 21 max_probe_interval_minutes integer YES
_orphan_api_keys_backup 22 request_count integer YES
_orphan_api_keys_backup 23 success_count integer YES
_orphan_api_keys_backup 24 error_count integer YES
_orphan_api_keys_backup 25 total_response_time_ms integer YES
_orphan_api_keys_backup 26 last_used_at timestamp with time zone YES
_orphan_api_keys_backup 27 last_error_at timestamp with time zone YES
_orphan_api_keys_backup 28 last_error_msg text YES
_orphan_api_keys_backup 29 is_active boolean YES
_orphan_api_keys_backup 30 expires_at timestamp with time zone YES
_orphan_api_keys_backup 31 created_at timestamp with time zone YES
_orphan_api_keys_backup 32 updated_at timestamp with time zone YES
_orphan_api_keys_backup 33 endpoint_id character varying YES
_orphan_api_keys_backup 34 rate_limit integer YES
_orphan_api_keys_backup 35 daily_limit integer YES
_orphan_api_keys_backup 36 monthly_limit integer YES
_orphan_api_keys_backup 37 provider_id character varying YES
_orphan_api_keys_backup 38 backup_at timestamp with time zone YES
_sqlx_migrations 1 version bigint NO
_sqlx_migrations 2 description text NO
_sqlx_migrations 3 installed_on timestamp with time zone NO now()
_sqlx_migrations 4 success boolean NO
_sqlx_migrations 5 checksum bytea NO
_sqlx_migrations 6 execution_time bigint NO
alembic_version 1 version_num character varying NO
announcement_reads 1 id character varying NO
announcement_reads 2 user_id character varying NO
announcement_reads 3 announcement_id character varying NO
announcement_reads 4 read_at timestamp with time zone NO now()
announcements 1 id character varying NO
announcements 2 title character varying NO
announcements 3 content text NO
announcements 4 type character varying YES 'info'::character varying
announcements 5 priority integer YES 0
announcements 6 author_id character varying YES
announcements 7 is_active boolean YES true
announcements 8 is_pinned boolean YES false
announcements 9 start_time timestamp with time zone YES
announcements 10 end_time timestamp with time zone YES
announcements 11 created_at timestamp with time zone NO now()
announcements 12 updated_at timestamp with time zone NO now()
api_key_provider_mappings 1 id character varying NO
api_key_provider_mappings 2 api_key_id character varying NO
api_key_provider_mappings 3 provider_id character varying NO
api_key_provider_mappings 4 priority_adjustment integer YES 0
api_key_provider_mappings 5 weight_multiplier double precision YES '1'::double precision
api_key_provider_mappings 6 is_enabled boolean NO true
api_key_provider_mappings 7 created_at timestamp with time zone NO now()
api_key_provider_mappings 8 updated_at timestamp with time zone NO now()
api_keys 1 id character varying NO
api_keys 2 user_id character varying NO
api_keys 3 key_hash character varying NO
api_keys 4 key_encrypted text YES
api_keys 5 name character varying YES
api_keys 6 total_requests integer YES 0
api_keys 7 total_cost_usd numeric YES '0'::double precision
api_keys 10 is_standalone boolean NO false
api_keys 11 allowed_providers json YES
api_keys 13 allowed_api_formats json YES
api_keys 14 allowed_models json YES
api_keys 15 rate_limit integer YES 100
api_keys 16 concurrent_limit integer YES 5
api_keys 17 force_capabilities json YES
api_keys 18 is_active boolean NO true
api_keys 19 last_used_at timestamp with time zone YES
api_keys 20 expires_at timestamp with time zone YES
api_keys 21 auto_delete_on_expiry boolean NO false
api_keys 22 created_at timestamp with time zone NO now()
api_keys 23 updated_at timestamp with time zone NO now()
api_keys 24 is_locked boolean NO false
audit_logs 1 id character varying NO
audit_logs 2 event_type character varying NO
audit_logs 3 user_id character varying YES
audit_logs 4 api_key_id character varying YES
audit_logs 5 description text NO
audit_logs 6 ip_address character varying YES
audit_logs 7 user_agent character varying YES
audit_logs 8 request_id character varying YES
audit_logs 9 event_metadata json YES
audit_logs 10 status_code integer YES
audit_logs 11 error_message text YES
audit_logs 12 created_at timestamp with time zone NO now()
billing_rules 1 id character varying NO
billing_rules 2 global_model_id character varying YES
billing_rules 3 model_id character varying YES
billing_rules 4 name character varying NO
billing_rules 5 task_type character varying NO 'chat'::character varying
billing_rules 6 expression text NO
billing_rules 7 variables jsonb NO '{}'::jsonb
billing_rules 8 dimension_mappings jsonb NO '{}'::jsonb
billing_rules 9 is_enabled boolean NO true
billing_rules 10 created_at timestamp with time zone NO now()
billing_rules 11 updated_at timestamp with time zone NO now()
dimension_collectors 1 id character varying NO
dimension_collectors 2 api_format character varying NO
dimension_collectors 3 task_type character varying NO
dimension_collectors 4 dimension_name character varying NO
dimension_collectors 5 source_type character varying NO
dimension_collectors 6 source_path character varying YES
dimension_collectors 7 value_type character varying NO 'float'::character varying
dimension_collectors 8 transform_expression text YES
dimension_collectors 9 default_value character varying YES
dimension_collectors 10 priority integer NO 0
dimension_collectors 11 is_enabled boolean NO true
dimension_collectors 12 created_at timestamp with time zone NO now()
dimension_collectors 13 updated_at timestamp with time zone NO now()
gemini_file_mappings 1 id character varying NO
gemini_file_mappings 2 file_name character varying NO
gemini_file_mappings 3 key_id character varying NO
gemini_file_mappings 4 user_id character varying YES
gemini_file_mappings 5 display_name character varying YES
gemini_file_mappings 6 mime_type character varying YES
gemini_file_mappings 7 source_hash character varying YES
gemini_file_mappings 8 created_at timestamp with time zone NO
gemini_file_mappings 9 expires_at timestamp with time zone NO
global_models 1 id character varying NO
global_models 2 name character varying NO
global_models 3 display_name character varying NO
global_models 7 default_price_per_request numeric YES
global_models 8 default_tiered_pricing json NO
global_models 14 supported_capabilities json YES
global_models 15 is_active boolean NO true
global_models 16 usage_count integer NO 0
global_models 17 created_at timestamp with time zone NO now()
global_models 18 updated_at timestamp with time zone NO now()
global_models 19 config jsonb YES
ldap_configs 1 id integer NO nextval('ldap_configs_id_seq'::regclass)
ldap_configs 2 server_url character varying NO
ldap_configs 3 bind_dn text NO
ldap_configs 4 bind_password_encrypted text YES
ldap_configs 5 base_dn text NO
ldap_configs 6 user_search_filter text NO '(uid={username})'::character varying
ldap_configs 7 username_attr character varying NO 'uid'::character varying
ldap_configs 8 email_attr character varying NO 'mail'::character varying
ldap_configs 9 display_name_attr character varying NO 'cn'::character varying
ldap_configs 10 is_enabled boolean NO false
ldap_configs 11 is_exclusive boolean NO false
ldap_configs 12 use_starttls boolean NO false
ldap_configs 13 connect_timeout integer NO 10
ldap_configs 14 created_at timestamp with time zone NO now()
ldap_configs 15 updated_at timestamp with time zone NO now()
management_tokens 1 id character varying NO
management_tokens 2 user_id character varying NO
management_tokens 3 token_hash character varying NO
management_tokens 4 token_prefix character varying YES
management_tokens 5 name character varying NO
management_tokens 6 description text YES
management_tokens 7 allowed_ips json YES
management_tokens 8 expires_at timestamp with time zone YES
management_tokens 9 last_used_at timestamp with time zone YES
management_tokens 10 last_used_ip character varying YES
management_tokens 11 usage_count integer NO 0
management_tokens 12 is_active boolean NO true
management_tokens 13 created_at timestamp with time zone NO now()
management_tokens 14 updated_at timestamp with time zone NO now()
models 1 id character varying NO
models 2 provider_id character varying NO
models 3 global_model_id character varying NO
models 4 provider_model_name character varying NO
models 5 price_per_request numeric YES
models 6 tiered_pricing json YES
models 7 supports_vision boolean YES
models 8 supports_function_calling boolean YES
models 9 supports_streaming boolean YES
models 10 supports_extended_thinking boolean YES
models 11 supports_image_generation boolean YES
models 12 is_active boolean NO true
models 13 is_available boolean YES true
models 14 config json YES
models 15 created_at timestamp with time zone NO now()
models 16 updated_at timestamp with time zone NO now()
models 17 provider_model_mappings jsonb YES
oauth_providers 1 provider_type character varying NO
oauth_providers 2 display_name character varying NO
oauth_providers 3 client_id text NO
oauth_providers 4 client_secret_encrypted text YES
oauth_providers 5 authorization_url_override character varying YES
oauth_providers 6 token_url_override character varying YES
oauth_providers 7 userinfo_url_override character varying YES
oauth_providers 8 scopes json YES
oauth_providers 9 redirect_uri character varying NO
oauth_providers 10 frontend_callback_url character varying NO
oauth_providers 11 attribute_mapping json YES
oauth_providers 12 extra_config json YES
oauth_providers 13 is_enabled boolean NO false
oauth_providers 14 created_at timestamp with time zone NO CURRENT_TIMESTAMP
oauth_providers 15 updated_at timestamp with time zone NO CURRENT_TIMESTAMP
payment_callbacks 1 id character varying NO
payment_callbacks 2 payment_order_id character varying YES
payment_callbacks 3 payment_method character varying NO
payment_callbacks 4 callback_key character varying NO
payment_callbacks 5 order_no character varying YES
payment_callbacks 6 gateway_order_id character varying YES
payment_callbacks 7 payload_hash character varying YES
payment_callbacks 8 signature_valid boolean NO false
payment_callbacks 9 status character varying NO 'received'::character varying
payment_callbacks 10 payload jsonb YES
payment_callbacks 11 error_message text YES
payment_callbacks 12 created_at timestamp with time zone NO
payment_callbacks 13 processed_at timestamp with time zone YES
payment_orders 1 id character varying NO
payment_orders 2 order_no character varying NO
payment_orders 3 wallet_id character varying NO
payment_orders 4 user_id character varying YES
payment_orders 5 amount_usd numeric NO
payment_orders 6 pay_amount numeric YES
payment_orders 7 pay_currency character varying YES
payment_orders 8 exchange_rate numeric YES
payment_orders 9 refunded_amount_usd numeric NO '0'::numeric
payment_orders 10 refundable_amount_usd numeric NO '0'::numeric
payment_orders 11 payment_method character varying NO
payment_orders 12 gateway_order_id character varying YES
payment_orders 13 gateway_response jsonb YES
payment_orders 14 status character varying NO 'pending'::character varying
payment_orders 15 created_at timestamp with time zone NO
payment_orders 16 paid_at timestamp with time zone YES
payment_orders 17 credited_at timestamp with time zone YES
payment_orders 18 expires_at timestamp with time zone YES
provider_api_keys 1 id character varying NO
provider_api_keys 3 api_key text NO
provider_api_keys 4 name character varying NO
provider_api_keys 5 note character varying YES
provider_api_keys 7 internal_priority integer YES 50
provider_api_keys 9 rpm_limit integer YES RPM限制NULL=自适应模式)
provider_api_keys 13 allowed_models json YES
provider_api_keys 14 capabilities json YES
provider_api_keys 15 learned_rpm_limit integer YES 学习到的RPM限制
provider_api_keys 16 concurrent_429_count integer NO 0
provider_api_keys 17 rpm_429_count integer NO 0
provider_api_keys 18 last_429_at timestamp with time zone YES
provider_api_keys 19 last_429_type character varying YES
provider_api_keys 20 last_rpm_peak integer YES 触发429时的RPM峰值
provider_api_keys 21 adjustment_history json YES
provider_api_keys 22 utilization_samples json YES
provider_api_keys 23 last_probe_increase_at timestamp with time zone YES
provider_api_keys 27 cache_ttl_minutes integer NO 5
provider_api_keys 28 max_probe_interval_minutes integer NO 32
provider_api_keys 36 request_count integer YES 0
provider_api_keys 37 success_count integer YES 0
provider_api_keys 38 error_count integer YES 0
provider_api_keys 39 total_response_time_ms integer YES 0
provider_api_keys 40 last_used_at timestamp with time zone YES
provider_api_keys 41 last_error_at timestamp with time zone YES
provider_api_keys 42 last_error_msg text YES
provider_api_keys 43 is_active boolean NO true
provider_api_keys 44 expires_at timestamp with time zone YES
provider_api_keys 45 created_at timestamp with time zone NO now()
provider_api_keys 46 updated_at timestamp with time zone NO now()
provider_api_keys 64 provider_id character varying NO
provider_api_keys 65 api_formats json NO '[]'::json
provider_api_keys 66 rate_multipliers json YES
provider_api_keys 67 health_by_format jsonb YES 按API格式存储的健康度数据
provider_api_keys 68 circuit_breaker_by_format jsonb YES 按API格式存储的熔断器状态
provider_api_keys 73 auto_fetch_models boolean NO false
provider_api_keys 74 last_models_fetch_at timestamp with time zone YES
provider_api_keys 75 last_models_fetch_error text YES
provider_api_keys 76 locked_models json YES
provider_api_keys 77 global_priority_by_format json YES
provider_api_keys 80 model_include_patterns json YES
provider_api_keys 81 model_exclude_patterns json YES
provider_api_keys 82 auth_type character varying NO 'api_key'::character varying
provider_api_keys 83 auth_config text YES
provider_api_keys 84 upstream_metadata jsonb YES
provider_api_keys 85 oauth_invalid_at timestamp with time zone YES
provider_api_keys 86 oauth_invalid_reason character varying YES
provider_api_keys 87 proxy json YES Key 级别代理配置(覆盖 Provider 级别代理),如 {node_id, enabled}
provider_api_keys 88 fingerprint json YES
provider_api_keys 89 total_tokens bigint NO
provider_api_keys 90 total_cost_usd numeric NO
provider_api_keys 91 status_snapshot json YES
provider_endpoints 1 id character varying NO
provider_endpoints 2 provider_id character varying NO
provider_endpoints 3 api_format character varying NO
provider_endpoints 4 base_url character varying NO
provider_endpoints 7 max_retries integer YES 3
provider_endpoints 10 is_active boolean NO true
provider_endpoints 11 custom_path character varying YES
provider_endpoints 12 config json YES
provider_endpoints 13 created_at timestamp with time zone NO now()
provider_endpoints 14 updated_at timestamp with time zone NO now()
provider_endpoints 15 proxy jsonb YES
provider_endpoints 22 header_rules json YES
provider_endpoints 23 format_acceptance_config json YES
provider_endpoints 24 api_family character varying YES
provider_endpoints 25 endpoint_kind character varying YES
provider_endpoints 27 body_rules json YES
provider_endpoints 28 health_score double precision NO 1.0
provider_usage_tracking 1 id character varying NO
provider_usage_tracking 2 provider_id character varying NO
provider_usage_tracking 3 window_start timestamp with time zone NO
provider_usage_tracking 4 window_end timestamp with time zone NO
provider_usage_tracking 5 total_requests integer YES 0
provider_usage_tracking 6 successful_requests integer YES 0
provider_usage_tracking 7 failed_requests integer YES 0
provider_usage_tracking 8 avg_response_time_ms double precision YES '0'::double precision
provider_usage_tracking 9 total_response_time_ms double precision YES '0'::double precision
provider_usage_tracking 10 total_cost_usd double precision YES '0'::double precision
provider_usage_tracking 11 created_at timestamp with time zone NO now()
provider_usage_tracking 12 updated_at timestamp with time zone NO now()
providers 1 id character varying NO
providers 3 name character varying NO
providers 4 description text YES
providers 5 website character varying YES
providers 6 billing_type USER-DEFINED NO 'pay_as_you_go'::providerbillingtype
providers 7 monthly_quota_usd numeric YES
providers 8 monthly_used_usd numeric YES '0'::double precision
providers 9 quota_reset_day integer YES 30
providers 10 quota_last_reset_at timestamp with time zone YES
providers 11 quota_expires_at timestamp with time zone YES
providers 15 provider_priority integer YES 100
providers 16 is_active boolean NO true
providers 18 concurrent_limit integer YES
providers 19 config json YES
providers 20 created_at timestamp with time zone NO now()
providers 21 updated_at timestamp with time zone NO now()
providers 34 max_retries integer YES 最大重试次数
providers 35 proxy jsonb YES 代理配置
providers 36 stream_first_byte_timeout double precision YES
providers 37 request_timeout double precision YES
providers 38 keep_priority_on_conversion boolean NO false
providers 39 enable_format_conversion boolean NO true
providers 40 provider_type character varying NO 'custom'::character varying
proxy_node_events 1 id bigint NO nextval('proxy_node_events_id_seq'::regclass)
proxy_node_events 2 node_id character varying NO
proxy_node_events 3 event_type character varying NO 事件类型: connected, disconnected, error
proxy_node_events 4 detail character varying YES 事件详情(如断开原因)
proxy_node_events 5 created_at timestamp with time zone NO
proxy_nodes 1 id character varying NO
proxy_nodes 2 name character varying NO
proxy_nodes 3 ip character varying NO
proxy_nodes 4 port integer NO
proxy_nodes 5 region character varying YES
proxy_nodes 6 status USER-DEFINED NO 'online'::proxynodestatus
proxy_nodes 7 registered_by character varying YES
proxy_nodes 8 last_heartbeat_at timestamp with time zone YES
proxy_nodes 9 heartbeat_interval integer NO 30
proxy_nodes 10 active_connections integer NO 0
proxy_nodes 11 total_requests bigint NO 0
proxy_nodes 12 avg_latency_ms double precision YES
proxy_nodes 13 is_manual boolean NO false 是否为手动添加的代理节点
proxy_nodes 14 proxy_url character varying YES 手动节点的完整代理 URL
proxy_nodes 15 proxy_username character varying YES 手动节点的代理用户名
proxy_nodes 16 proxy_password character varying YES 手动节点的代理密码
proxy_nodes 17 created_at timestamp with time zone NO CURRENT_TIMESTAMP
proxy_nodes 18 updated_at timestamp with time zone NO CURRENT_TIMESTAMP
proxy_nodes 19 remote_config json YES 管理端下发的远程配置 (allowed_ports, log_level, heartbeat_interval, timestamp_tolerance)
proxy_nodes 20 config_version integer NO 0 远程配置版本号,每次更新 +1
proxy_nodes 23 hardware_info json YES 硬件信息 (cpu_cores, total_memory_mb, os_info, fd_limit, ...)
proxy_nodes 24 estimated_max_concurrency integer YES 基于硬件估算的最大并发连接数
proxy_nodes 28 tunnel_mode boolean NO false 是否使用 WebSocket 隧道模式
proxy_nodes 29 tunnel_connected boolean NO false 隧道是否已连接
proxy_nodes 30 tunnel_connected_at timestamp with time zone YES 隧道最近一次建立时间
proxy_nodes 31 failed_requests bigint NO '0'::bigint 累计失败请求数
proxy_nodes 32 dns_failures bigint NO '0'::bigint 累计 DNS 失败数
proxy_nodes 33 stream_errors bigint NO '0'::bigint 累计流错误数
proxy_nodes 34 proxy_metadata json YES aether-proxy 上报元数据(版本等)
refund_requests 1 id character varying NO
refund_requests 2 refund_no character varying NO
refund_requests 3 wallet_id character varying NO
refund_requests 4 user_id character varying YES
refund_requests 5 payment_order_id character varying YES
refund_requests 6 source_type character varying NO
refund_requests 7 source_id character varying YES
refund_requests 8 refund_mode character varying NO
refund_requests 9 amount_usd numeric NO
refund_requests 10 status character varying NO 'pending_approval'::character varying
refund_requests 11 reason text YES
refund_requests 12 requested_by character varying YES
refund_requests 13 approved_by character varying YES
refund_requests 14 processed_by character varying YES
refund_requests 15 gateway_refund_id character varying YES
refund_requests 16 payout_method character varying YES
refund_requests 17 payout_reference character varying YES
refund_requests 18 payout_proof jsonb YES
refund_requests 19 failure_reason text YES
refund_requests 20 idempotency_key character varying YES
refund_requests 21 created_at timestamp with time zone NO
refund_requests 22 updated_at timestamp with time zone NO
refund_requests 23 processed_at timestamp with time zone YES
refund_requests 24 completed_at timestamp with time zone YES
request_candidates 1 id character varying NO
request_candidates 2 request_id character varying NO
request_candidates 3 user_id character varying YES
request_candidates 4 api_key_id character varying YES
request_candidates 5 candidate_index integer NO
request_candidates 6 retry_index integer NO 0
request_candidates 7 provider_id character varying YES
request_candidates 8 endpoint_id character varying YES
request_candidates 9 key_id character varying YES
request_candidates 10 status character varying NO
request_candidates 11 skip_reason text YES
request_candidates 12 is_cached boolean YES false
request_candidates 13 status_code integer YES
request_candidates 14 error_type character varying YES
request_candidates 15 error_message text YES
request_candidates 16 latency_ms integer YES
request_candidates 17 concurrent_requests integer YES
request_candidates 18 extra_data json YES
request_candidates 19 required_capabilities json YES
request_candidates 20 created_at timestamp with time zone NO now()
request_candidates 21 started_at timestamp with time zone YES
request_candidates 22 finished_at timestamp with time zone YES
request_candidates 23 username character varying YES 用户名快照
request_candidates 24 api_key_name character varying YES API Key 名称快照
stats_daily 1 id character varying NO
stats_daily 2 date timestamp with time zone NO
stats_daily 3 total_requests integer NO 0
stats_daily 4 success_requests integer NO 0
stats_daily 5 error_requests integer NO 0
stats_daily 6 input_tokens bigint NO '0'::bigint
stats_daily 7 output_tokens bigint NO '0'::bigint
stats_daily 8 cache_creation_tokens bigint NO '0'::bigint
stats_daily 9 cache_read_tokens bigint NO '0'::bigint
stats_daily 10 total_cost numeric NO '0'::double precision
stats_daily 11 actual_total_cost numeric NO '0'::double precision
stats_daily 12 input_cost numeric NO '0'::double precision
stats_daily 13 output_cost numeric NO '0'::double precision
stats_daily 14 cache_creation_cost numeric NO '0'::double precision
stats_daily 15 cache_read_cost numeric NO '0'::double precision
stats_daily 16 avg_response_time_ms double precision NO '0'::double precision
stats_daily 17 fallback_count integer NO 0
stats_daily 18 unique_models integer NO 0
stats_daily 19 unique_providers integer NO 0
stats_daily 20 created_at timestamp with time zone NO now()
stats_daily 21 updated_at timestamp with time zone NO now()
stats_daily 22 is_complete boolean NO false
stats_daily 23 aggregated_at timestamp with time zone YES
stats_daily 24 p50_response_time_ms integer YES
stats_daily 25 p90_response_time_ms integer YES
stats_daily 26 p99_response_time_ms integer YES
stats_daily 27 p50_first_byte_time_ms integer YES
stats_daily 28 p90_first_byte_time_ms integer YES
stats_daily 29 p99_first_byte_time_ms integer YES
stats_daily_api_key 1 id character varying NO
stats_daily_api_key 2 api_key_id character varying YES
stats_daily_api_key 3 date timestamp with time zone NO
stats_daily_api_key 4 total_requests integer NO 0
stats_daily_api_key 5 success_requests integer NO 0
stats_daily_api_key 6 error_requests integer NO 0
stats_daily_api_key 7 input_tokens bigint NO '0'::bigint
stats_daily_api_key 8 output_tokens bigint NO '0'::bigint
stats_daily_api_key 9 cache_creation_tokens bigint NO '0'::bigint
stats_daily_api_key 10 cache_read_tokens bigint NO '0'::bigint
stats_daily_api_key 11 total_cost numeric NO '0'::double precision
stats_daily_api_key 12 created_at timestamp with time zone NO CURRENT_TIMESTAMP
stats_daily_api_key 13 updated_at timestamp with time zone NO CURRENT_TIMESTAMP
stats_daily_api_key 14 api_key_name character varying YES API Key 名称快照(删除 Key 后仍可追溯)
stats_daily_error 1 id character varying NO
stats_daily_error 2 date timestamp with time zone NO
stats_daily_error 3 error_category character varying NO
stats_daily_error 4 provider_name character varying YES
stats_daily_error 5 model character varying YES
stats_daily_error 6 count integer NO 0
stats_daily_error 7 created_at timestamp with time zone NO CURRENT_TIMESTAMP
stats_daily_error 8 updated_at timestamp with time zone NO CURRENT_TIMESTAMP
stats_daily_model 1 id character varying NO
stats_daily_model 2 date timestamp with time zone NO
stats_daily_model 3 model character varying NO
stats_daily_model 4 total_requests integer NO
stats_daily_model 5 input_tokens bigint NO
stats_daily_model 6 output_tokens bigint NO
stats_daily_model 7 cache_creation_tokens bigint NO
stats_daily_model 8 cache_read_tokens bigint NO
stats_daily_model 9 total_cost numeric NO
stats_daily_model 10 avg_response_time_ms double precision NO
stats_daily_model 11 created_at timestamp with time zone NO now()
stats_daily_model 12 updated_at timestamp with time zone NO now()
stats_daily_provider 1 id character varying NO
stats_daily_provider 2 date timestamp with time zone NO
stats_daily_provider 3 provider_name character varying NO
stats_daily_provider 4 total_requests integer NO
stats_daily_provider 5 input_tokens bigint NO
stats_daily_provider 6 output_tokens bigint NO
stats_daily_provider 7 cache_creation_tokens bigint NO
stats_daily_provider 8 cache_read_tokens bigint NO
stats_daily_provider 9 total_cost numeric NO
stats_daily_provider 10 created_at timestamp with time zone NO
stats_daily_provider 11 updated_at timestamp with time zone NO
stats_hourly 1 id character varying NO
stats_hourly 2 hour_utc timestamp with time zone NO
stats_hourly 3 total_requests integer NO
stats_hourly 4 success_requests integer NO
stats_hourly 5 error_requests integer NO
stats_hourly 6 input_tokens bigint NO
stats_hourly 7 output_tokens bigint NO
stats_hourly 8 cache_creation_tokens bigint NO
stats_hourly 9 cache_read_tokens bigint NO
stats_hourly 10 total_cost numeric NO
stats_hourly 11 actual_total_cost numeric NO
stats_hourly 12 avg_response_time_ms double precision NO
stats_hourly 13 is_complete boolean NO
stats_hourly 14 aggregated_at timestamp with time zone YES
stats_hourly 15 created_at timestamp with time zone NO
stats_hourly 16 updated_at timestamp with time zone NO
stats_hourly_model 1 id character varying NO
stats_hourly_model 2 hour_utc timestamp with time zone NO
stats_hourly_model 3 model character varying NO
stats_hourly_model 4 total_requests integer NO
stats_hourly_model 5 input_tokens bigint NO
stats_hourly_model 6 output_tokens bigint NO
stats_hourly_model 7 total_cost numeric NO
stats_hourly_model 8 avg_response_time_ms double precision NO
stats_hourly_model 9 created_at timestamp with time zone NO
stats_hourly_model 10 updated_at timestamp with time zone NO
stats_hourly_provider 1 id character varying NO
stats_hourly_provider 2 hour_utc timestamp with time zone NO
stats_hourly_provider 3 provider_name character varying NO
stats_hourly_provider 4 total_requests integer NO
stats_hourly_provider 5 input_tokens bigint NO
stats_hourly_provider 6 output_tokens bigint NO
stats_hourly_provider 7 total_cost numeric NO
stats_hourly_provider 8 created_at timestamp with time zone NO
stats_hourly_provider 9 updated_at timestamp with time zone NO
stats_hourly_user 1 id character varying NO
stats_hourly_user 2 hour_utc timestamp with time zone NO
stats_hourly_user 3 user_id character varying NO
stats_hourly_user 4 total_requests integer NO
stats_hourly_user 5 success_requests integer NO
stats_hourly_user 6 error_requests integer NO
stats_hourly_user 7 input_tokens bigint NO
stats_hourly_user 8 output_tokens bigint NO
stats_hourly_user 9 total_cost numeric NO
stats_hourly_user 10 created_at timestamp with time zone NO
stats_hourly_user 11 updated_at timestamp with time zone NO
stats_summary 1 id character varying NO
stats_summary 2 cutoff_date timestamp with time zone NO
stats_summary 3 all_time_requests integer NO 0
stats_summary 4 all_time_success_requests integer NO 0
stats_summary 5 all_time_error_requests integer NO 0
stats_summary 6 all_time_input_tokens bigint NO '0'::bigint
stats_summary 7 all_time_output_tokens bigint NO '0'::bigint
stats_summary 8 all_time_cache_creation_tokens bigint NO '0'::bigint
stats_summary 9 all_time_cache_read_tokens bigint NO '0'::bigint
stats_summary 10 all_time_cost numeric NO '0'::double precision
stats_summary 11 all_time_actual_cost numeric NO '0'::double precision
stats_summary 12 total_users integer NO 0
stats_summary 13 active_users integer NO 0
stats_summary 14 total_api_keys integer NO 0
stats_summary 15 active_api_keys integer NO 0
stats_summary 16 created_at timestamp with time zone NO now()
stats_summary 17 updated_at timestamp with time zone NO now()
stats_user_daily 1 id character varying NO
stats_user_daily 2 user_id character varying YES
stats_user_daily 3 date timestamp with time zone NO
stats_user_daily 4 total_requests integer NO 0
stats_user_daily 5 success_requests integer NO 0
stats_user_daily 6 error_requests integer NO 0
stats_user_daily 7 input_tokens bigint NO '0'::bigint
stats_user_daily 8 output_tokens bigint NO '0'::bigint
stats_user_daily 9 cache_creation_tokens bigint NO '0'::bigint
stats_user_daily 10 cache_read_tokens bigint NO '0'::bigint
stats_user_daily 11 total_cost numeric NO '0'::double precision
stats_user_daily 12 created_at timestamp with time zone NO now()
stats_user_daily 13 updated_at timestamp with time zone NO now()
stats_user_daily 14 username character varying YES 用户名快照(删除用户后仍可追溯)
system_configs 1 id character varying NO
system_configs 2 key character varying NO
system_configs 3 value json NO
system_configs 4 description text YES
system_configs 5 created_at timestamp with time zone NO now()
system_configs 6 updated_at timestamp with time zone NO now()
usage 1 id character varying NO
usage 2 user_id character varying YES
usage 3 api_key_id character varying YES
usage 4 request_id character varying NO
usage 5 provider_name character varying NO
usage 6 model character varying NO
usage 7 target_model character varying YES
usage 8 provider_id character varying YES
usage 9 provider_endpoint_id character varying YES
usage 10 provider_api_key_id character varying YES
usage 11 input_tokens integer YES 0
usage 12 output_tokens integer YES 0
usage 13 input_output_total_tokens integer YES 0
usage 14 cache_creation_input_tokens integer YES 0
usage 15 cache_read_input_tokens integer YES 0
usage 16 input_cost_usd numeric YES '0'::double precision
usage 17 output_cost_usd numeric YES '0'::double precision
usage 18 cache_cost_usd numeric YES '0'::double precision
usage 19 cache_creation_cost_usd numeric YES '0'::double precision
usage 20 cache_read_cost_usd numeric YES '0'::double precision
usage 21 request_cost_usd numeric YES '0'::double precision
usage 22 total_cost_usd numeric YES '0'::double precision
usage 23 actual_input_cost_usd numeric YES '0'::double precision
usage 24 actual_output_cost_usd numeric YES '0'::double precision
usage 25 actual_cache_creation_cost_usd numeric YES '0'::double precision
usage 26 actual_cache_read_cost_usd numeric YES '0'::double precision
usage 27 actual_request_cost_usd numeric YES '0'::double precision
usage 28 actual_total_cost_usd numeric YES '0'::double precision
usage 29 rate_multiplier numeric YES '1'::double precision
usage 30 input_price_per_1m numeric YES
usage 31 output_price_per_1m numeric YES
usage 32 cache_creation_price_per_1m numeric YES
usage 33 cache_read_price_per_1m numeric YES
usage 34 price_per_request numeric YES
usage 35 request_type character varying YES
usage 36 api_format character varying YES
usage 37 is_stream boolean YES false
usage 38 status_code integer YES
usage 39 error_message text YES
usage 40 response_time_ms integer YES
usage 41 status character varying NO 'completed'::character varying
usage 42 request_headers json YES
usage 43 request_body json YES
usage 44 provider_request_headers json YES
usage 45 response_headers json YES
usage 46 response_body json YES
usage 47 request_body_compressed bytea YES
usage 48 response_body_compressed bytea YES
usage 49 request_metadata json YES
usage 50 created_at timestamp with time zone NO now()
usage 51 first_byte_time_ms integer YES
usage 54 client_response_headers json YES
usage 59 endpoint_api_format character varying YES
usage 60 has_format_conversion boolean YES false
usage 63 billing_status character varying NO 'pending'::character varying
usage 64 finalized_at timestamp with time zone YES
usage 65 error_category character varying YES
usage 66 provider_request_body json YES
usage 67 provider_request_body_compressed bytea YES
usage 68 client_response_body json YES
usage 69 client_response_body_compressed bytea YES
usage 70 api_family character varying YES
usage 71 endpoint_kind character varying YES
usage 72 provider_api_family character varying YES
usage 73 provider_endpoint_kind character varying YES
usage 74 cache_creation_input_tokens_5m integer NO 0 5min TTL cache creation input tokens
usage 75 cache_creation_input_tokens_1h integer NO 0 1h TTL cache creation input tokens
usage 76 wallet_id character varying YES
usage 77 wallet_balance_before numeric YES
usage 78 wallet_balance_after numeric YES
usage 79 wallet_recharge_balance_before numeric YES
usage 80 wallet_recharge_balance_after numeric YES
usage 81 wallet_gift_balance_before numeric YES
usage 82 wallet_gift_balance_after numeric YES
usage 83 username character varying YES 用户名快照
usage 84 api_key_name character varying YES API Key 名称快照
usage 85 input_context_tokens integer NO 0
usage 86 total_tokens integer NO 0
usage 87 cache_creation_cost_usd_5m numeric NO '0'::numeric
usage 88 cache_creation_cost_usd_1h numeric NO '0'::numeric
usage 89 actual_cache_creation_cost_usd_5m numeric NO '0'::numeric
usage 90 actual_cache_creation_cost_usd_1h numeric NO '0'::numeric
usage 91 actual_cache_cost_usd numeric NO '0'::numeric
usage 92 cache_creation_price_per_1m_5m numeric YES
usage 93 cache_creation_price_per_1m_1h numeric YES
user_model_usage_counts 1 id character varying NO
user_model_usage_counts 2 user_id character varying NO
user_model_usage_counts 3 model character varying NO
user_model_usage_counts 4 usage_count integer NO 0
user_model_usage_counts 5 created_at timestamp with time zone NO now()
user_model_usage_counts 6 updated_at timestamp with time zone NO now()
user_oauth_links 1 id character varying NO
user_oauth_links 2 user_id character varying NO
user_oauth_links 3 provider_type character varying NO
user_oauth_links 4 provider_user_id character varying NO
user_oauth_links 5 provider_username character varying YES
user_oauth_links 6 provider_email character varying YES
user_oauth_links 7 extra_data json YES
user_oauth_links 8 linked_at timestamp with time zone NO CURRENT_TIMESTAMP
user_oauth_links 9 last_login_at timestamp with time zone YES
user_preferences 1 id character varying NO
user_preferences 2 user_id character varying NO
user_preferences 3 avatar_url character varying YES
user_preferences 4 bio text YES
user_preferences 5 default_provider_id character varying YES
user_preferences 6 theme character varying YES 'light'::character varying
user_preferences 7 language character varying YES 'zh-CN'::character varying
user_preferences 8 timezone character varying YES 'Asia/Shanghai'::character varying
user_preferences 9 email_notifications boolean YES true
user_preferences 10 usage_alerts boolean YES true
user_preferences 11 announcement_notifications boolean YES true
user_preferences 12 created_at timestamp with time zone NO now()
user_preferences 13 updated_at timestamp with time zone NO now()
user_sessions 1 id character varying NO
user_sessions 2 user_id character varying NO
user_sessions 3 client_device_id character varying NO
user_sessions 4 device_label character varying YES
user_sessions 5 device_type character varying NO 'unknown'::character varying
user_sessions 6 browser_name character varying YES
user_sessions 7 browser_version character varying YES
user_sessions 8 os_name character varying YES
user_sessions 9 os_version character varying YES
user_sessions 10 device_model character varying YES
user_sessions 11 ip_address character varying YES
user_sessions 12 user_agent character varying YES
user_sessions 13 client_hints json YES
user_sessions 14 refresh_token_hash character varying NO
user_sessions 15 prev_refresh_token_hash character varying YES
user_sessions 16 rotated_at timestamp with time zone YES
user_sessions 17 last_seen_at timestamp with time zone NO now()
user_sessions 18 expires_at timestamp with time zone NO
user_sessions 19 revoked_at timestamp with time zone YES
user_sessions 20 revoke_reason character varying YES
user_sessions 21 created_at timestamp with time zone NO now()
user_sessions 22 updated_at timestamp with time zone NO now()
users 1 id character varying NO
users 2 email character varying YES
users 3 username character varying NO
users 4 password_hash character varying YES
users 5 role USER-DEFINED NO 'user'::userrole
users 6 allowed_providers json YES
users 7 allowed_api_formats json YES
users 8 allowed_models json YES
users 9 model_capability_settings json YES
users 13 is_active boolean NO true
users 14 is_deleted boolean NO false
users 15 created_at timestamp with time zone NO now()
users 16 updated_at timestamp with time zone NO now()
users 17 last_login_at timestamp with time zone YES
users 18 auth_source USER-DEFINED NO 'local'::authsource
users 19 ldap_dn character varying YES
users 20 ldap_username character varying YES
users 21 email_verified boolean NO
users 22 rate_limit integer YES
video_tasks 1 id character varying NO
video_tasks 2 external_task_id character varying YES
video_tasks 3 user_id character varying YES
video_tasks 4 api_key_id character varying YES
video_tasks 5 provider_id character varying YES
video_tasks 6 endpoint_id character varying YES
video_tasks 7 key_id character varying YES
video_tasks 8 client_api_format character varying NO
video_tasks 9 provider_api_format character varying NO
video_tasks 10 format_converted boolean YES false
video_tasks 11 model character varying NO
video_tasks 12 prompt text NO
video_tasks 13 original_request_body json YES
video_tasks 14 converted_request_body json YES
video_tasks 15 duration_seconds integer YES 4
video_tasks 16 resolution character varying YES '720p'::character varying
video_tasks 17 aspect_ratio character varying YES '16:9'::character varying
video_tasks 18 size character varying YES
video_tasks 19 status character varying YES 'pending'::character varying
video_tasks 20 progress_percent integer YES 0
video_tasks 21 progress_message character varying YES
video_tasks 22 video_url character varying YES
video_tasks 23 video_urls json YES
video_tasks 24 thumbnail_url character varying YES
video_tasks 25 video_size_bytes bigint YES
video_tasks 26 video_expires_at timestamp with time zone YES
video_tasks 27 stored_video_path character varying YES
video_tasks 28 storage_provider character varying YES
video_tasks 29 error_code character varying YES
video_tasks 30 error_message text YES
video_tasks 31 retry_count integer YES 0
video_tasks 32 max_retries integer YES 3
video_tasks 33 poll_interval_seconds integer YES 10
video_tasks 34 next_poll_at timestamp with time zone YES
video_tasks 35 poll_count integer YES 0
video_tasks 36 max_poll_count integer YES 360
video_tasks 37 remixed_from_task_id character varying YES
video_tasks 38 webhook_url character varying YES
video_tasks 39 webhook_sent boolean YES false
video_tasks 40 webhook_sent_at timestamp with time zone YES
video_tasks 41 created_at timestamp with time zone YES CURRENT_TIMESTAMP
video_tasks 42 submitted_at timestamp with time zone YES
video_tasks 43 completed_at timestamp with time zone YES
video_tasks 44 updated_at timestamp with time zone YES CURRENT_TIMESTAMP
video_tasks 46 request_metadata json YES
video_tasks 49 request_id character varying NO
video_tasks 50 short_id character varying NO
video_tasks 52 video_duration_seconds double precision YES
video_tasks 53 username character varying YES 用户名快照
video_tasks 54 api_key_name character varying YES API Key 名称快照
wallet_daily_usage_ledgers 1 id character varying NO
wallet_daily_usage_ledgers 2 wallet_id character varying NO
wallet_daily_usage_ledgers 3 billing_date date NO
wallet_daily_usage_ledgers 4 billing_timezone character varying NO
wallet_daily_usage_ledgers 5 total_cost_usd numeric NO '0'::numeric
wallet_daily_usage_ledgers 6 total_requests integer NO 0
wallet_daily_usage_ledgers 7 input_tokens bigint NO '0'::bigint
wallet_daily_usage_ledgers 8 output_tokens bigint NO '0'::bigint
wallet_daily_usage_ledgers 9 cache_creation_tokens bigint NO '0'::bigint
wallet_daily_usage_ledgers 10 cache_read_tokens bigint NO '0'::bigint
wallet_daily_usage_ledgers 11 first_finalized_at timestamp with time zone YES
wallet_daily_usage_ledgers 12 last_finalized_at timestamp with time zone YES
wallet_daily_usage_ledgers 13 aggregated_at timestamp with time zone NO
wallet_daily_usage_ledgers 14 created_at timestamp with time zone NO
wallet_daily_usage_ledgers 15 updated_at timestamp with time zone NO
wallet_transactions 1 id character varying NO
wallet_transactions 2 wallet_id character varying NO
wallet_transactions 3 category character varying NO
wallet_transactions 4 reason_code character varying NO
wallet_transactions 5 amount numeric NO
wallet_transactions 6 balance_before numeric NO
wallet_transactions 7 balance_after numeric NO
wallet_transactions 8 recharge_balance_before numeric NO
wallet_transactions 9 recharge_balance_after numeric NO
wallet_transactions 10 gift_balance_before numeric NO
wallet_transactions 11 gift_balance_after numeric NO
wallet_transactions 12 link_type character varying YES
wallet_transactions 13 link_id character varying YES
wallet_transactions 14 operator_id character varying YES
wallet_transactions 15 description text YES
wallet_transactions 16 created_at timestamp with time zone NO
wallets 1 id character varying NO
wallets 2 user_id character varying YES
wallets 3 api_key_id character varying YES
wallets 4 balance numeric NO '0'::numeric
wallets 5 gift_balance numeric NO '0'::numeric
wallets 6 limit_mode character varying NO 'finite'::character varying
wallets 7 currency character varying NO 'USD'::character varying
wallets 8 status character varying NO 'active'::character varying
wallets 9 total_recharged numeric NO '0'::numeric
wallets 10 total_consumed numeric NO '0'::numeric
wallets 11 total_refunded numeric NO '0'::numeric
wallets 12 total_adjusted numeric NO '0'::numeric
wallets 14 created_at timestamp with time zone NO
wallets 15 updated_at timestamp with time zone NO
1 table_name ordinal_position column_name data_type is_nullable column_default column_comment
2 _orphan_api_keys_backup 1 id character varying YES
3 _orphan_api_keys_backup 2 api_key character varying YES
4 _orphan_api_keys_backup 3 name character varying YES
5 _orphan_api_keys_backup 4 note character varying YES
6 _orphan_api_keys_backup 5 rate_multiplier double precision YES
7 _orphan_api_keys_backup 6 internal_priority integer YES
8 _orphan_api_keys_backup 7 global_priority integer YES
9 _orphan_api_keys_backup 8 max_concurrent integer YES
10 _orphan_api_keys_backup 9 allowed_models json YES
11 _orphan_api_keys_backup 10 capabilities json YES
12 _orphan_api_keys_backup 11 learned_max_concurrent integer YES
13 _orphan_api_keys_backup 12 concurrent_429_count integer YES
14 _orphan_api_keys_backup 13 rpm_429_count integer YES
15 _orphan_api_keys_backup 14 last_429_at timestamp with time zone YES
16 _orphan_api_keys_backup 15 last_429_type character varying YES
17 _orphan_api_keys_backup 16 last_concurrent_peak integer YES
18 _orphan_api_keys_backup 17 adjustment_history json YES
19 _orphan_api_keys_backup 18 utilization_samples json YES
20 _orphan_api_keys_backup 19 last_probe_increase_at timestamp with time zone YES
21 _orphan_api_keys_backup 20 cache_ttl_minutes integer YES
22 _orphan_api_keys_backup 21 max_probe_interval_minutes integer YES
23 _orphan_api_keys_backup 22 request_count integer YES
24 _orphan_api_keys_backup 23 success_count integer YES
25 _orphan_api_keys_backup 24 error_count integer YES
26 _orphan_api_keys_backup 25 total_response_time_ms integer YES
27 _orphan_api_keys_backup 26 last_used_at timestamp with time zone YES
28 _orphan_api_keys_backup 27 last_error_at timestamp with time zone YES
29 _orphan_api_keys_backup 28 last_error_msg text YES
30 _orphan_api_keys_backup 29 is_active boolean YES
31 _orphan_api_keys_backup 30 expires_at timestamp with time zone YES
32 _orphan_api_keys_backup 31 created_at timestamp with time zone YES
33 _orphan_api_keys_backup 32 updated_at timestamp with time zone YES
34 _orphan_api_keys_backup 33 endpoint_id character varying YES
35 _orphan_api_keys_backup 34 rate_limit integer YES
36 _orphan_api_keys_backup 35 daily_limit integer YES
37 _orphan_api_keys_backup 36 monthly_limit integer YES
38 _orphan_api_keys_backup 37 provider_id character varying YES
39 _orphan_api_keys_backup 38 backup_at timestamp with time zone YES
40 _sqlx_migrations 1 version bigint NO
41 _sqlx_migrations 2 description text NO
42 _sqlx_migrations 3 installed_on timestamp with time zone NO now()
43 _sqlx_migrations 4 success boolean NO
44 _sqlx_migrations 5 checksum bytea NO
45 _sqlx_migrations 6 execution_time bigint NO
46 alembic_version 1 version_num character varying NO
47 announcement_reads 1 id character varying NO
48 announcement_reads 2 user_id character varying NO
49 announcement_reads 3 announcement_id character varying NO
50 announcement_reads 4 read_at timestamp with time zone NO now()
51 announcements 1 id character varying NO
52 announcements 2 title character varying NO
53 announcements 3 content text NO
54 announcements 4 type character varying YES 'info'::character varying
55 announcements 5 priority integer YES 0
56 announcements 6 author_id character varying YES
57 announcements 7 is_active boolean YES true
58 announcements 8 is_pinned boolean YES false
59 announcements 9 start_time timestamp with time zone YES
60 announcements 10 end_time timestamp with time zone YES
61 announcements 11 created_at timestamp with time zone NO now()
62 announcements 12 updated_at timestamp with time zone NO now()
63 api_key_provider_mappings 1 id character varying NO
64 api_key_provider_mappings 2 api_key_id character varying NO
65 api_key_provider_mappings 3 provider_id character varying NO
66 api_key_provider_mappings 4 priority_adjustment integer YES 0
67 api_key_provider_mappings 5 weight_multiplier double precision YES '1'::double precision
68 api_key_provider_mappings 6 is_enabled boolean NO true
69 api_key_provider_mappings 7 created_at timestamp with time zone NO now()
70 api_key_provider_mappings 8 updated_at timestamp with time zone NO now()
71 api_keys 1 id character varying NO
72 api_keys 2 user_id character varying NO
73 api_keys 3 key_hash character varying NO
74 api_keys 4 key_encrypted text YES
75 api_keys 5 name character varying YES
76 api_keys 6 total_requests integer YES 0
77 api_keys 7 total_cost_usd numeric YES '0'::double precision
78 api_keys 10 is_standalone boolean NO false
79 api_keys 11 allowed_providers json YES
80 api_keys 13 allowed_api_formats json YES
81 api_keys 14 allowed_models json YES
82 api_keys 15 rate_limit integer YES 100
83 api_keys 16 concurrent_limit integer YES 5
84 api_keys 17 force_capabilities json YES
85 api_keys 18 is_active boolean NO true
86 api_keys 19 last_used_at timestamp with time zone YES
87 api_keys 20 expires_at timestamp with time zone YES
88 api_keys 21 auto_delete_on_expiry boolean NO false
89 api_keys 22 created_at timestamp with time zone NO now()
90 api_keys 23 updated_at timestamp with time zone NO now()
91 api_keys 24 is_locked boolean NO false
92 audit_logs 1 id character varying NO
93 audit_logs 2 event_type character varying NO
94 audit_logs 3 user_id character varying YES
95 audit_logs 4 api_key_id character varying YES
96 audit_logs 5 description text NO
97 audit_logs 6 ip_address character varying YES
98 audit_logs 7 user_agent character varying YES
99 audit_logs 8 request_id character varying YES
100 audit_logs 9 event_metadata json YES
101 audit_logs 10 status_code integer YES
102 audit_logs 11 error_message text YES
103 audit_logs 12 created_at timestamp with time zone NO now()
104 billing_rules 1 id character varying NO
105 billing_rules 2 global_model_id character varying YES
106 billing_rules 3 model_id character varying YES
107 billing_rules 4 name character varying NO
108 billing_rules 5 task_type character varying NO 'chat'::character varying
109 billing_rules 6 expression text NO
110 billing_rules 7 variables jsonb NO '{}'::jsonb
111 billing_rules 8 dimension_mappings jsonb NO '{}'::jsonb
112 billing_rules 9 is_enabled boolean NO true
113 billing_rules 10 created_at timestamp with time zone NO now()
114 billing_rules 11 updated_at timestamp with time zone NO now()
115 dimension_collectors 1 id character varying NO
116 dimension_collectors 2 api_format character varying NO
117 dimension_collectors 3 task_type character varying NO
118 dimension_collectors 4 dimension_name character varying NO
119 dimension_collectors 5 source_type character varying NO
120 dimension_collectors 6 source_path character varying YES
121 dimension_collectors 7 value_type character varying NO 'float'::character varying
122 dimension_collectors 8 transform_expression text YES
123 dimension_collectors 9 default_value character varying YES
124 dimension_collectors 10 priority integer NO 0
125 dimension_collectors 11 is_enabled boolean NO true
126 dimension_collectors 12 created_at timestamp with time zone NO now()
127 dimension_collectors 13 updated_at timestamp with time zone NO now()
128 gemini_file_mappings 1 id character varying NO
129 gemini_file_mappings 2 file_name character varying NO
130 gemini_file_mappings 3 key_id character varying NO
131 gemini_file_mappings 4 user_id character varying YES
132 gemini_file_mappings 5 display_name character varying YES
133 gemini_file_mappings 6 mime_type character varying YES
134 gemini_file_mappings 7 source_hash character varying YES
135 gemini_file_mappings 8 created_at timestamp with time zone NO
136 gemini_file_mappings 9 expires_at timestamp with time zone NO
137 global_models 1 id character varying NO
138 global_models 2 name character varying NO
139 global_models 3 display_name character varying NO
140 global_models 7 default_price_per_request numeric YES
141 global_models 8 default_tiered_pricing json NO
142 global_models 14 supported_capabilities json YES
143 global_models 15 is_active boolean NO true
144 global_models 16 usage_count integer NO 0
145 global_models 17 created_at timestamp with time zone NO now()
146 global_models 18 updated_at timestamp with time zone NO now()
147 global_models 19 config jsonb YES
148 ldap_configs 1 id integer NO nextval('ldap_configs_id_seq'::regclass)
149 ldap_configs 2 server_url character varying NO
150 ldap_configs 3 bind_dn text NO
151 ldap_configs 4 bind_password_encrypted text YES
152 ldap_configs 5 base_dn text NO
153 ldap_configs 6 user_search_filter text NO '(uid={username})'::character varying
154 ldap_configs 7 username_attr character varying NO 'uid'::character varying
155 ldap_configs 8 email_attr character varying NO 'mail'::character varying
156 ldap_configs 9 display_name_attr character varying NO 'cn'::character varying
157 ldap_configs 10 is_enabled boolean NO false
158 ldap_configs 11 is_exclusive boolean NO false
159 ldap_configs 12 use_starttls boolean NO false
160 ldap_configs 13 connect_timeout integer NO 10
161 ldap_configs 14 created_at timestamp with time zone NO now()
162 ldap_configs 15 updated_at timestamp with time zone NO now()
163 management_tokens 1 id character varying NO
164 management_tokens 2 user_id character varying NO
165 management_tokens 3 token_hash character varying NO
166 management_tokens 4 token_prefix character varying YES
167 management_tokens 5 name character varying NO
168 management_tokens 6 description text YES
169 management_tokens 7 allowed_ips json YES
170 management_tokens 8 expires_at timestamp with time zone YES
171 management_tokens 9 last_used_at timestamp with time zone YES
172 management_tokens 10 last_used_ip character varying YES
173 management_tokens 11 usage_count integer NO 0
174 management_tokens 12 is_active boolean NO true
175 management_tokens 13 created_at timestamp with time zone NO now()
176 management_tokens 14 updated_at timestamp with time zone NO now()
177 models 1 id character varying NO
178 models 2 provider_id character varying NO
179 models 3 global_model_id character varying NO
180 models 4 provider_model_name character varying NO
181 models 5 price_per_request numeric YES
182 models 6 tiered_pricing json YES
183 models 7 supports_vision boolean YES
184 models 8 supports_function_calling boolean YES
185 models 9 supports_streaming boolean YES
186 models 10 supports_extended_thinking boolean YES
187 models 11 supports_image_generation boolean YES
188 models 12 is_active boolean NO true
189 models 13 is_available boolean YES true
190 models 14 config json YES
191 models 15 created_at timestamp with time zone NO now()
192 models 16 updated_at timestamp with time zone NO now()
193 models 17 provider_model_mappings jsonb YES
194 oauth_providers 1 provider_type character varying NO
195 oauth_providers 2 display_name character varying NO
196 oauth_providers 3 client_id text NO
197 oauth_providers 4 client_secret_encrypted text YES
198 oauth_providers 5 authorization_url_override character varying YES
199 oauth_providers 6 token_url_override character varying YES
200 oauth_providers 7 userinfo_url_override character varying YES
201 oauth_providers 8 scopes json YES
202 oauth_providers 9 redirect_uri character varying NO
203 oauth_providers 10 frontend_callback_url character varying NO
204 oauth_providers 11 attribute_mapping json YES
205 oauth_providers 12 extra_config json YES
206 oauth_providers 13 is_enabled boolean NO false
207 oauth_providers 14 created_at timestamp with time zone NO CURRENT_TIMESTAMP
208 oauth_providers 15 updated_at timestamp with time zone NO CURRENT_TIMESTAMP
209 payment_callbacks 1 id character varying NO
210 payment_callbacks 2 payment_order_id character varying YES
211 payment_callbacks 3 payment_method character varying NO
212 payment_callbacks 4 callback_key character varying NO
213 payment_callbacks 5 order_no character varying YES
214 payment_callbacks 6 gateway_order_id character varying YES
215 payment_callbacks 7 payload_hash character varying YES
216 payment_callbacks 8 signature_valid boolean NO false
217 payment_callbacks 9 status character varying NO 'received'::character varying
218 payment_callbacks 10 payload jsonb YES
219 payment_callbacks 11 error_message text YES
220 payment_callbacks 12 created_at timestamp with time zone NO
221 payment_callbacks 13 processed_at timestamp with time zone YES
222 payment_orders 1 id character varying NO
223 payment_orders 2 order_no character varying NO
224 payment_orders 3 wallet_id character varying NO
225 payment_orders 4 user_id character varying YES
226 payment_orders 5 amount_usd numeric NO
227 payment_orders 6 pay_amount numeric YES
228 payment_orders 7 pay_currency character varying YES
229 payment_orders 8 exchange_rate numeric YES
230 payment_orders 9 refunded_amount_usd numeric NO '0'::numeric
231 payment_orders 10 refundable_amount_usd numeric NO '0'::numeric
232 payment_orders 11 payment_method character varying NO
233 payment_orders 12 gateway_order_id character varying YES
234 payment_orders 13 gateway_response jsonb YES
235 payment_orders 14 status character varying NO 'pending'::character varying
236 payment_orders 15 created_at timestamp with time zone NO
237 payment_orders 16 paid_at timestamp with time zone YES
238 payment_orders 17 credited_at timestamp with time zone YES
239 payment_orders 18 expires_at timestamp with time zone YES
240 provider_api_keys 1 id character varying NO
241 provider_api_keys 3 api_key text NO
242 provider_api_keys 4 name character varying NO
243 provider_api_keys 5 note character varying YES
244 provider_api_keys 7 internal_priority integer YES 50
245 provider_api_keys 9 rpm_limit integer YES RPM限制(NULL=自适应模式)
246 provider_api_keys 13 allowed_models json YES
247 provider_api_keys 14 capabilities json YES
248 provider_api_keys 15 learned_rpm_limit integer YES 学习到的RPM限制
249 provider_api_keys 16 concurrent_429_count integer NO 0
250 provider_api_keys 17 rpm_429_count integer NO 0
251 provider_api_keys 18 last_429_at timestamp with time zone YES
252 provider_api_keys 19 last_429_type character varying YES
253 provider_api_keys 20 last_rpm_peak integer YES 触发429时的RPM峰值
254 provider_api_keys 21 adjustment_history json YES
255 provider_api_keys 22 utilization_samples json YES
256 provider_api_keys 23 last_probe_increase_at timestamp with time zone YES
257 provider_api_keys 27 cache_ttl_minutes integer NO 5
258 provider_api_keys 28 max_probe_interval_minutes integer NO 32
259 provider_api_keys 36 request_count integer YES 0
260 provider_api_keys 37 success_count integer YES 0
261 provider_api_keys 38 error_count integer YES 0
262 provider_api_keys 39 total_response_time_ms integer YES 0
263 provider_api_keys 40 last_used_at timestamp with time zone YES
264 provider_api_keys 41 last_error_at timestamp with time zone YES
265 provider_api_keys 42 last_error_msg text YES
266 provider_api_keys 43 is_active boolean NO true
267 provider_api_keys 44 expires_at timestamp with time zone YES
268 provider_api_keys 45 created_at timestamp with time zone NO now()
269 provider_api_keys 46 updated_at timestamp with time zone NO now()
270 provider_api_keys 64 provider_id character varying NO
271 provider_api_keys 65 api_formats json NO '[]'::json
272 provider_api_keys 66 rate_multipliers json YES
273 provider_api_keys 67 health_by_format jsonb YES 按API格式存储的健康度数据
274 provider_api_keys 68 circuit_breaker_by_format jsonb YES 按API格式存储的熔断器状态
275 provider_api_keys 73 auto_fetch_models boolean NO false
276 provider_api_keys 74 last_models_fetch_at timestamp with time zone YES
277 provider_api_keys 75 last_models_fetch_error text YES
278 provider_api_keys 76 locked_models json YES
279 provider_api_keys 77 global_priority_by_format json YES
280 provider_api_keys 80 model_include_patterns json YES
281 provider_api_keys 81 model_exclude_patterns json YES
282 provider_api_keys 82 auth_type character varying NO 'api_key'::character varying
283 provider_api_keys 83 auth_config text YES
284 provider_api_keys 84 upstream_metadata jsonb YES
285 provider_api_keys 85 oauth_invalid_at timestamp with time zone YES
286 provider_api_keys 86 oauth_invalid_reason character varying YES
287 provider_api_keys 87 proxy json YES Key 级别代理配置(覆盖 Provider 级别代理),如 {node_id, enabled}
288 provider_api_keys 88 fingerprint json YES
289 provider_api_keys 89 total_tokens bigint NO
290 provider_api_keys 90 total_cost_usd numeric NO
291 provider_api_keys 91 status_snapshot json YES
292 provider_endpoints 1 id character varying NO
293 provider_endpoints 2 provider_id character varying NO
294 provider_endpoints 3 api_format character varying NO
295 provider_endpoints 4 base_url character varying NO
296 provider_endpoints 7 max_retries integer YES 3
297 provider_endpoints 10 is_active boolean NO true
298 provider_endpoints 11 custom_path character varying YES
299 provider_endpoints 12 config json YES
300 provider_endpoints 13 created_at timestamp with time zone NO now()
301 provider_endpoints 14 updated_at timestamp with time zone NO now()
302 provider_endpoints 15 proxy jsonb YES
303 provider_endpoints 22 header_rules json YES
304 provider_endpoints 23 format_acceptance_config json YES
305 provider_endpoints 24 api_family character varying YES
306 provider_endpoints 25 endpoint_kind character varying YES
307 provider_endpoints 27 body_rules json YES
308 provider_endpoints 28 health_score double precision NO 1.0
309 provider_usage_tracking 1 id character varying NO
310 provider_usage_tracking 2 provider_id character varying NO
311 provider_usage_tracking 3 window_start timestamp with time zone NO
312 provider_usage_tracking 4 window_end timestamp with time zone NO
313 provider_usage_tracking 5 total_requests integer YES 0
314 provider_usage_tracking 6 successful_requests integer YES 0
315 provider_usage_tracking 7 failed_requests integer YES 0
316 provider_usage_tracking 8 avg_response_time_ms double precision YES '0'::double precision
317 provider_usage_tracking 9 total_response_time_ms double precision YES '0'::double precision
318 provider_usage_tracking 10 total_cost_usd double precision YES '0'::double precision
319 provider_usage_tracking 11 created_at timestamp with time zone NO now()
320 provider_usage_tracking 12 updated_at timestamp with time zone NO now()
321 providers 1 id character varying NO
322 providers 3 name character varying NO
323 providers 4 description text YES
324 providers 5 website character varying YES
325 providers 6 billing_type USER-DEFINED NO 'pay_as_you_go'::providerbillingtype
326 providers 7 monthly_quota_usd numeric YES
327 providers 8 monthly_used_usd numeric YES '0'::double precision
328 providers 9 quota_reset_day integer YES 30
329 providers 10 quota_last_reset_at timestamp with time zone YES
330 providers 11 quota_expires_at timestamp with time zone YES
331 providers 15 provider_priority integer YES 100
332 providers 16 is_active boolean NO true
333 providers 18 concurrent_limit integer YES
334 providers 19 config json YES
335 providers 20 created_at timestamp with time zone NO now()
336 providers 21 updated_at timestamp with time zone NO now()
337 providers 34 max_retries integer YES 最大重试次数
338 providers 35 proxy jsonb YES 代理配置
339 providers 36 stream_first_byte_timeout double precision YES
340 providers 37 request_timeout double precision YES
341 providers 38 keep_priority_on_conversion boolean NO false
342 providers 39 enable_format_conversion boolean NO true
343 providers 40 provider_type character varying NO 'custom'::character varying
344 proxy_node_events 1 id bigint NO nextval('proxy_node_events_id_seq'::regclass)
345 proxy_node_events 2 node_id character varying NO
346 proxy_node_events 3 event_type character varying NO 事件类型: connected, disconnected, error
347 proxy_node_events 4 detail character varying YES 事件详情(如断开原因)
348 proxy_node_events 5 created_at timestamp with time zone NO
349 proxy_nodes 1 id character varying NO
350 proxy_nodes 2 name character varying NO
351 proxy_nodes 3 ip character varying NO
352 proxy_nodes 4 port integer NO
353 proxy_nodes 5 region character varying YES
354 proxy_nodes 6 status USER-DEFINED NO 'online'::proxynodestatus
355 proxy_nodes 7 registered_by character varying YES
356 proxy_nodes 8 last_heartbeat_at timestamp with time zone YES
357 proxy_nodes 9 heartbeat_interval integer NO 30
358 proxy_nodes 10 active_connections integer NO 0
359 proxy_nodes 11 total_requests bigint NO 0
360 proxy_nodes 12 avg_latency_ms double precision YES
361 proxy_nodes 13 is_manual boolean NO false 是否为手动添加的代理节点
362 proxy_nodes 14 proxy_url character varying YES 手动节点的完整代理 URL
363 proxy_nodes 15 proxy_username character varying YES 手动节点的代理用户名
364 proxy_nodes 16 proxy_password character varying YES 手动节点的代理密码
365 proxy_nodes 17 created_at timestamp with time zone NO CURRENT_TIMESTAMP
366 proxy_nodes 18 updated_at timestamp with time zone NO CURRENT_TIMESTAMP
367 proxy_nodes 19 remote_config json YES 管理端下发的远程配置 (allowed_ports, log_level, heartbeat_interval, timestamp_tolerance)
368 proxy_nodes 20 config_version integer NO 0 远程配置版本号,每次更新 +1
369 proxy_nodes 23 hardware_info json YES 硬件信息 (cpu_cores, total_memory_mb, os_info, fd_limit, ...)
370 proxy_nodes 24 estimated_max_concurrency integer YES 基于硬件估算的最大并发连接数
371 proxy_nodes 28 tunnel_mode boolean NO false 是否使用 WebSocket 隧道模式
372 proxy_nodes 29 tunnel_connected boolean NO false 隧道是否已连接
373 proxy_nodes 30 tunnel_connected_at timestamp with time zone YES 隧道最近一次建立时间
374 proxy_nodes 31 failed_requests bigint NO '0'::bigint 累计失败请求数
375 proxy_nodes 32 dns_failures bigint NO '0'::bigint 累计 DNS 失败数
376 proxy_nodes 33 stream_errors bigint NO '0'::bigint 累计流错误数
377 proxy_nodes 34 proxy_metadata json YES aether-proxy 上报元数据(版本等)
378 refund_requests 1 id character varying NO
379 refund_requests 2 refund_no character varying NO
380 refund_requests 3 wallet_id character varying NO
381 refund_requests 4 user_id character varying YES
382 refund_requests 5 payment_order_id character varying YES
383 refund_requests 6 source_type character varying NO
384 refund_requests 7 source_id character varying YES
385 refund_requests 8 refund_mode character varying NO
386 refund_requests 9 amount_usd numeric NO
387 refund_requests 10 status character varying NO 'pending_approval'::character varying
388 refund_requests 11 reason text YES
389 refund_requests 12 requested_by character varying YES
390 refund_requests 13 approved_by character varying YES
391 refund_requests 14 processed_by character varying YES
392 refund_requests 15 gateway_refund_id character varying YES
393 refund_requests 16 payout_method character varying YES
394 refund_requests 17 payout_reference character varying YES
395 refund_requests 18 payout_proof jsonb YES
396 refund_requests 19 failure_reason text YES
397 refund_requests 20 idempotency_key character varying YES
398 refund_requests 21 created_at timestamp with time zone NO
399 refund_requests 22 updated_at timestamp with time zone NO
400 refund_requests 23 processed_at timestamp with time zone YES
401 refund_requests 24 completed_at timestamp with time zone YES
402 request_candidates 1 id character varying NO
403 request_candidates 2 request_id character varying NO
404 request_candidates 3 user_id character varying YES
405 request_candidates 4 api_key_id character varying YES
406 request_candidates 5 candidate_index integer NO
407 request_candidates 6 retry_index integer NO 0
408 request_candidates 7 provider_id character varying YES
409 request_candidates 8 endpoint_id character varying YES
410 request_candidates 9 key_id character varying YES
411 request_candidates 10 status character varying NO
412 request_candidates 11 skip_reason text YES
413 request_candidates 12 is_cached boolean YES false
414 request_candidates 13 status_code integer YES
415 request_candidates 14 error_type character varying YES
416 request_candidates 15 error_message text YES
417 request_candidates 16 latency_ms integer YES
418 request_candidates 17 concurrent_requests integer YES
419 request_candidates 18 extra_data json YES
420 request_candidates 19 required_capabilities json YES
421 request_candidates 20 created_at timestamp with time zone NO now()
422 request_candidates 21 started_at timestamp with time zone YES
423 request_candidates 22 finished_at timestamp with time zone YES
424 request_candidates 23 username character varying YES 用户名快照
425 request_candidates 24 api_key_name character varying YES API Key 名称快照
426 stats_daily 1 id character varying NO
427 stats_daily 2 date timestamp with time zone NO
428 stats_daily 3 total_requests integer NO 0
429 stats_daily 4 success_requests integer NO 0
430 stats_daily 5 error_requests integer NO 0
431 stats_daily 6 input_tokens bigint NO '0'::bigint
432 stats_daily 7 output_tokens bigint NO '0'::bigint
433 stats_daily 8 cache_creation_tokens bigint NO '0'::bigint
434 stats_daily 9 cache_read_tokens bigint NO '0'::bigint
435 stats_daily 10 total_cost numeric NO '0'::double precision
436 stats_daily 11 actual_total_cost numeric NO '0'::double precision
437 stats_daily 12 input_cost numeric NO '0'::double precision
438 stats_daily 13 output_cost numeric NO '0'::double precision
439 stats_daily 14 cache_creation_cost numeric NO '0'::double precision
440 stats_daily 15 cache_read_cost numeric NO '0'::double precision
441 stats_daily 16 avg_response_time_ms double precision NO '0'::double precision
442 stats_daily 17 fallback_count integer NO 0
443 stats_daily 18 unique_models integer NO 0
444 stats_daily 19 unique_providers integer NO 0
445 stats_daily 20 created_at timestamp with time zone NO now()
446 stats_daily 21 updated_at timestamp with time zone NO now()
447 stats_daily 22 is_complete boolean NO false
448 stats_daily 23 aggregated_at timestamp with time zone YES
449 stats_daily 24 p50_response_time_ms integer YES
450 stats_daily 25 p90_response_time_ms integer YES
451 stats_daily 26 p99_response_time_ms integer YES
452 stats_daily 27 p50_first_byte_time_ms integer YES
453 stats_daily 28 p90_first_byte_time_ms integer YES
454 stats_daily 29 p99_first_byte_time_ms integer YES
455 stats_daily_api_key 1 id character varying NO
456 stats_daily_api_key 2 api_key_id character varying YES
457 stats_daily_api_key 3 date timestamp with time zone NO
458 stats_daily_api_key 4 total_requests integer NO 0
459 stats_daily_api_key 5 success_requests integer NO 0
460 stats_daily_api_key 6 error_requests integer NO 0
461 stats_daily_api_key 7 input_tokens bigint NO '0'::bigint
462 stats_daily_api_key 8 output_tokens bigint NO '0'::bigint
463 stats_daily_api_key 9 cache_creation_tokens bigint NO '0'::bigint
464 stats_daily_api_key 10 cache_read_tokens bigint NO '0'::bigint
465 stats_daily_api_key 11 total_cost numeric NO '0'::double precision
466 stats_daily_api_key 12 created_at timestamp with time zone NO CURRENT_TIMESTAMP
467 stats_daily_api_key 13 updated_at timestamp with time zone NO CURRENT_TIMESTAMP
468 stats_daily_api_key 14 api_key_name character varying YES API Key 名称快照(删除 Key 后仍可追溯)
469 stats_daily_error 1 id character varying NO
470 stats_daily_error 2 date timestamp with time zone NO
471 stats_daily_error 3 error_category character varying NO
472 stats_daily_error 4 provider_name character varying YES
473 stats_daily_error 5 model character varying YES
474 stats_daily_error 6 count integer NO 0
475 stats_daily_error 7 created_at timestamp with time zone NO CURRENT_TIMESTAMP
476 stats_daily_error 8 updated_at timestamp with time zone NO CURRENT_TIMESTAMP
477 stats_daily_model 1 id character varying NO
478 stats_daily_model 2 date timestamp with time zone NO
479 stats_daily_model 3 model character varying NO
480 stats_daily_model 4 total_requests integer NO
481 stats_daily_model 5 input_tokens bigint NO
482 stats_daily_model 6 output_tokens bigint NO
483 stats_daily_model 7 cache_creation_tokens bigint NO
484 stats_daily_model 8 cache_read_tokens bigint NO
485 stats_daily_model 9 total_cost numeric NO
486 stats_daily_model 10 avg_response_time_ms double precision NO
487 stats_daily_model 11 created_at timestamp with time zone NO now()
488 stats_daily_model 12 updated_at timestamp with time zone NO now()
489 stats_daily_provider 1 id character varying NO
490 stats_daily_provider 2 date timestamp with time zone NO
491 stats_daily_provider 3 provider_name character varying NO
492 stats_daily_provider 4 total_requests integer NO
493 stats_daily_provider 5 input_tokens bigint NO
494 stats_daily_provider 6 output_tokens bigint NO
495 stats_daily_provider 7 cache_creation_tokens bigint NO
496 stats_daily_provider 8 cache_read_tokens bigint NO
497 stats_daily_provider 9 total_cost numeric NO
498 stats_daily_provider 10 created_at timestamp with time zone NO
499 stats_daily_provider 11 updated_at timestamp with time zone NO
500 stats_hourly 1 id character varying NO
501 stats_hourly 2 hour_utc timestamp with time zone NO
502 stats_hourly 3 total_requests integer NO
503 stats_hourly 4 success_requests integer NO
504 stats_hourly 5 error_requests integer NO
505 stats_hourly 6 input_tokens bigint NO
506 stats_hourly 7 output_tokens bigint NO
507 stats_hourly 8 cache_creation_tokens bigint NO
508 stats_hourly 9 cache_read_tokens bigint NO
509 stats_hourly 10 total_cost numeric NO
510 stats_hourly 11 actual_total_cost numeric NO
511 stats_hourly 12 avg_response_time_ms double precision NO
512 stats_hourly 13 is_complete boolean NO
513 stats_hourly 14 aggregated_at timestamp with time zone YES
514 stats_hourly 15 created_at timestamp with time zone NO
515 stats_hourly 16 updated_at timestamp with time zone NO
516 stats_hourly_model 1 id character varying NO
517 stats_hourly_model 2 hour_utc timestamp with time zone NO
518 stats_hourly_model 3 model character varying NO
519 stats_hourly_model 4 total_requests integer NO
520 stats_hourly_model 5 input_tokens bigint NO
521 stats_hourly_model 6 output_tokens bigint NO
522 stats_hourly_model 7 total_cost numeric NO
523 stats_hourly_model 8 avg_response_time_ms double precision NO
524 stats_hourly_model 9 created_at timestamp with time zone NO
525 stats_hourly_model 10 updated_at timestamp with time zone NO
526 stats_hourly_provider 1 id character varying NO
527 stats_hourly_provider 2 hour_utc timestamp with time zone NO
528 stats_hourly_provider 3 provider_name character varying NO
529 stats_hourly_provider 4 total_requests integer NO
530 stats_hourly_provider 5 input_tokens bigint NO
531 stats_hourly_provider 6 output_tokens bigint NO
532 stats_hourly_provider 7 total_cost numeric NO
533 stats_hourly_provider 8 created_at timestamp with time zone NO
534 stats_hourly_provider 9 updated_at timestamp with time zone NO
535 stats_hourly_user 1 id character varying NO
536 stats_hourly_user 2 hour_utc timestamp with time zone NO
537 stats_hourly_user 3 user_id character varying NO
538 stats_hourly_user 4 total_requests integer NO
539 stats_hourly_user 5 success_requests integer NO
540 stats_hourly_user 6 error_requests integer NO
541 stats_hourly_user 7 input_tokens bigint NO
542 stats_hourly_user 8 output_tokens bigint NO
543 stats_hourly_user 9 total_cost numeric NO
544 stats_hourly_user 10 created_at timestamp with time zone NO
545 stats_hourly_user 11 updated_at timestamp with time zone NO
546 stats_summary 1 id character varying NO
547 stats_summary 2 cutoff_date timestamp with time zone NO
548 stats_summary 3 all_time_requests integer NO 0
549 stats_summary 4 all_time_success_requests integer NO 0
550 stats_summary 5 all_time_error_requests integer NO 0
551 stats_summary 6 all_time_input_tokens bigint NO '0'::bigint
552 stats_summary 7 all_time_output_tokens bigint NO '0'::bigint
553 stats_summary 8 all_time_cache_creation_tokens bigint NO '0'::bigint
554 stats_summary 9 all_time_cache_read_tokens bigint NO '0'::bigint
555 stats_summary 10 all_time_cost numeric NO '0'::double precision
556 stats_summary 11 all_time_actual_cost numeric NO '0'::double precision
557 stats_summary 12 total_users integer NO 0
558 stats_summary 13 active_users integer NO 0
559 stats_summary 14 total_api_keys integer NO 0
560 stats_summary 15 active_api_keys integer NO 0
561 stats_summary 16 created_at timestamp with time zone NO now()
562 stats_summary 17 updated_at timestamp with time zone NO now()
563 stats_user_daily 1 id character varying NO
564 stats_user_daily 2 user_id character varying YES
565 stats_user_daily 3 date timestamp with time zone NO
566 stats_user_daily 4 total_requests integer NO 0
567 stats_user_daily 5 success_requests integer NO 0
568 stats_user_daily 6 error_requests integer NO 0
569 stats_user_daily 7 input_tokens bigint NO '0'::bigint
570 stats_user_daily 8 output_tokens bigint NO '0'::bigint
571 stats_user_daily 9 cache_creation_tokens bigint NO '0'::bigint
572 stats_user_daily 10 cache_read_tokens bigint NO '0'::bigint
573 stats_user_daily 11 total_cost numeric NO '0'::double precision
574 stats_user_daily 12 created_at timestamp with time zone NO now()
575 stats_user_daily 13 updated_at timestamp with time zone NO now()
576 stats_user_daily 14 username character varying YES 用户名快照(删除用户后仍可追溯)
577 system_configs 1 id character varying NO
578 system_configs 2 key character varying NO
579 system_configs 3 value json NO
580 system_configs 4 description text YES
581 system_configs 5 created_at timestamp with time zone NO now()
582 system_configs 6 updated_at timestamp with time zone NO now()
583 usage 1 id character varying NO
584 usage 2 user_id character varying YES
585 usage 3 api_key_id character varying YES
586 usage 4 request_id character varying NO
587 usage 5 provider_name character varying NO
588 usage 6 model character varying NO
589 usage 7 target_model character varying YES
590 usage 8 provider_id character varying YES
591 usage 9 provider_endpoint_id character varying YES
592 usage 10 provider_api_key_id character varying YES
593 usage 11 input_tokens integer YES 0
594 usage 12 output_tokens integer YES 0
595 usage 13 input_output_total_tokens integer YES 0
596 usage 14 cache_creation_input_tokens integer YES 0
597 usage 15 cache_read_input_tokens integer YES 0
598 usage 16 input_cost_usd numeric YES '0'::double precision
599 usage 17 output_cost_usd numeric YES '0'::double precision
600 usage 18 cache_cost_usd numeric YES '0'::double precision
601 usage 19 cache_creation_cost_usd numeric YES '0'::double precision
602 usage 20 cache_read_cost_usd numeric YES '0'::double precision
603 usage 21 request_cost_usd numeric YES '0'::double precision
604 usage 22 total_cost_usd numeric YES '0'::double precision
605 usage 23 actual_input_cost_usd numeric YES '0'::double precision
606 usage 24 actual_output_cost_usd numeric YES '0'::double precision
607 usage 25 actual_cache_creation_cost_usd numeric YES '0'::double precision
608 usage 26 actual_cache_read_cost_usd numeric YES '0'::double precision
609 usage 27 actual_request_cost_usd numeric YES '0'::double precision
610 usage 28 actual_total_cost_usd numeric YES '0'::double precision
611 usage 29 rate_multiplier numeric YES '1'::double precision
612 usage 30 input_price_per_1m numeric YES
613 usage 31 output_price_per_1m numeric YES
614 usage 32 cache_creation_price_per_1m numeric YES
615 usage 33 cache_read_price_per_1m numeric YES
616 usage 34 price_per_request numeric YES
617 usage 35 request_type character varying YES
618 usage 36 api_format character varying YES
619 usage 37 is_stream boolean YES false
620 usage 38 status_code integer YES
621 usage 39 error_message text YES
622 usage 40 response_time_ms integer YES
623 usage 41 status character varying NO 'completed'::character varying
624 usage 42 request_headers json YES
625 usage 43 request_body json YES
626 usage 44 provider_request_headers json YES
627 usage 45 response_headers json YES
628 usage 46 response_body json YES
629 usage 47 request_body_compressed bytea YES
630 usage 48 response_body_compressed bytea YES
631 usage 49 request_metadata json YES
632 usage 50 created_at timestamp with time zone NO now()
633 usage 51 first_byte_time_ms integer YES
634 usage 54 client_response_headers json YES
635 usage 59 endpoint_api_format character varying YES
636 usage 60 has_format_conversion boolean YES false
637 usage 63 billing_status character varying NO 'pending'::character varying
638 usage 64 finalized_at timestamp with time zone YES
639 usage 65 error_category character varying YES
640 usage 66 provider_request_body json YES
641 usage 67 provider_request_body_compressed bytea YES
642 usage 68 client_response_body json YES
643 usage 69 client_response_body_compressed bytea YES
644 usage 70 api_family character varying YES
645 usage 71 endpoint_kind character varying YES
646 usage 72 provider_api_family character varying YES
647 usage 73 provider_endpoint_kind character varying YES
648 usage 74 cache_creation_input_tokens_5m integer NO 0 5min TTL cache creation input tokens
649 usage 75 cache_creation_input_tokens_1h integer NO 0 1h TTL cache creation input tokens
650 usage 76 wallet_id character varying YES
651 usage 77 wallet_balance_before numeric YES
652 usage 78 wallet_balance_after numeric YES
653 usage 79 wallet_recharge_balance_before numeric YES
654 usage 80 wallet_recharge_balance_after numeric YES
655 usage 81 wallet_gift_balance_before numeric YES
656 usage 82 wallet_gift_balance_after numeric YES
657 usage 83 username character varying YES 用户名快照
658 usage 84 api_key_name character varying YES API Key 名称快照
659 usage 85 input_context_tokens integer NO 0
660 usage 86 total_tokens integer NO 0
661 usage 87 cache_creation_cost_usd_5m numeric NO '0'::numeric
662 usage 88 cache_creation_cost_usd_1h numeric NO '0'::numeric
663 usage 89 actual_cache_creation_cost_usd_5m numeric NO '0'::numeric
664 usage 90 actual_cache_creation_cost_usd_1h numeric NO '0'::numeric
665 usage 91 actual_cache_cost_usd numeric NO '0'::numeric
666 usage 92 cache_creation_price_per_1m_5m numeric YES
667 usage 93 cache_creation_price_per_1m_1h numeric YES
668 user_model_usage_counts 1 id character varying NO
669 user_model_usage_counts 2 user_id character varying NO
670 user_model_usage_counts 3 model character varying NO
671 user_model_usage_counts 4 usage_count integer NO 0
672 user_model_usage_counts 5 created_at timestamp with time zone NO now()
673 user_model_usage_counts 6 updated_at timestamp with time zone NO now()
674 user_oauth_links 1 id character varying NO
675 user_oauth_links 2 user_id character varying NO
676 user_oauth_links 3 provider_type character varying NO
677 user_oauth_links 4 provider_user_id character varying NO
678 user_oauth_links 5 provider_username character varying YES
679 user_oauth_links 6 provider_email character varying YES
680 user_oauth_links 7 extra_data json YES
681 user_oauth_links 8 linked_at timestamp with time zone NO CURRENT_TIMESTAMP
682 user_oauth_links 9 last_login_at timestamp with time zone YES
683 user_preferences 1 id character varying NO
684 user_preferences 2 user_id character varying NO
685 user_preferences 3 avatar_url character varying YES
686 user_preferences 4 bio text YES
687 user_preferences 5 default_provider_id character varying YES
688 user_preferences 6 theme character varying YES 'light'::character varying
689 user_preferences 7 language character varying YES 'zh-CN'::character varying
690 user_preferences 8 timezone character varying YES 'Asia/Shanghai'::character varying
691 user_preferences 9 email_notifications boolean YES true
692 user_preferences 10 usage_alerts boolean YES true
693 user_preferences 11 announcement_notifications boolean YES true
694 user_preferences 12 created_at timestamp with time zone NO now()
695 user_preferences 13 updated_at timestamp with time zone NO now()
696 user_sessions 1 id character varying NO
697 user_sessions 2 user_id character varying NO
698 user_sessions 3 client_device_id character varying NO
699 user_sessions 4 device_label character varying YES
700 user_sessions 5 device_type character varying NO 'unknown'::character varying
701 user_sessions 6 browser_name character varying YES
702 user_sessions 7 browser_version character varying YES
703 user_sessions 8 os_name character varying YES
704 user_sessions 9 os_version character varying YES
705 user_sessions 10 device_model character varying YES
706 user_sessions 11 ip_address character varying YES
707 user_sessions 12 user_agent character varying YES
708 user_sessions 13 client_hints json YES
709 user_sessions 14 refresh_token_hash character varying NO
710 user_sessions 15 prev_refresh_token_hash character varying YES
711 user_sessions 16 rotated_at timestamp with time zone YES
712 user_sessions 17 last_seen_at timestamp with time zone NO now()
713 user_sessions 18 expires_at timestamp with time zone NO
714 user_sessions 19 revoked_at timestamp with time zone YES
715 user_sessions 20 revoke_reason character varying YES
716 user_sessions 21 created_at timestamp with time zone NO now()
717 user_sessions 22 updated_at timestamp with time zone NO now()
718 users 1 id character varying NO
719 users 2 email character varying YES
720 users 3 username character varying NO
721 users 4 password_hash character varying YES
722 users 5 role USER-DEFINED NO 'user'::userrole
723 users 6 allowed_providers json YES
724 users 7 allowed_api_formats json YES
725 users 8 allowed_models json YES
726 users 9 model_capability_settings json YES
727 users 13 is_active boolean NO true
728 users 14 is_deleted boolean NO false
729 users 15 created_at timestamp with time zone NO now()
730 users 16 updated_at timestamp with time zone NO now()
731 users 17 last_login_at timestamp with time zone YES
732 users 18 auth_source USER-DEFINED NO 'local'::authsource
733 users 19 ldap_dn character varying YES
734 users 20 ldap_username character varying YES
735 users 21 email_verified boolean NO
736 users 22 rate_limit integer YES
737 video_tasks 1 id character varying NO
738 video_tasks 2 external_task_id character varying YES
739 video_tasks 3 user_id character varying YES
740 video_tasks 4 api_key_id character varying YES
741 video_tasks 5 provider_id character varying YES
742 video_tasks 6 endpoint_id character varying YES
743 video_tasks 7 key_id character varying YES
744 video_tasks 8 client_api_format character varying NO
745 video_tasks 9 provider_api_format character varying NO
746 video_tasks 10 format_converted boolean YES false
747 video_tasks 11 model character varying NO
748 video_tasks 12 prompt text NO
749 video_tasks 13 original_request_body json YES
750 video_tasks 14 converted_request_body json YES
751 video_tasks 15 duration_seconds integer YES 4
752 video_tasks 16 resolution character varying YES '720p'::character varying
753 video_tasks 17 aspect_ratio character varying YES '16:9'::character varying
754 video_tasks 18 size character varying YES
755 video_tasks 19 status character varying YES 'pending'::character varying
756 video_tasks 20 progress_percent integer YES 0
757 video_tasks 21 progress_message character varying YES
758 video_tasks 22 video_url character varying YES
759 video_tasks 23 video_urls json YES
760 video_tasks 24 thumbnail_url character varying YES
761 video_tasks 25 video_size_bytes bigint YES
762 video_tasks 26 video_expires_at timestamp with time zone YES
763 video_tasks 27 stored_video_path character varying YES
764 video_tasks 28 storage_provider character varying YES
765 video_tasks 29 error_code character varying YES
766 video_tasks 30 error_message text YES
767 video_tasks 31 retry_count integer YES 0
768 video_tasks 32 max_retries integer YES 3
769 video_tasks 33 poll_interval_seconds integer YES 10
770 video_tasks 34 next_poll_at timestamp with time zone YES
771 video_tasks 35 poll_count integer YES 0
772 video_tasks 36 max_poll_count integer YES 360
773 video_tasks 37 remixed_from_task_id character varying YES
774 video_tasks 38 webhook_url character varying YES
775 video_tasks 39 webhook_sent boolean YES false
776 video_tasks 40 webhook_sent_at timestamp with time zone YES
777 video_tasks 41 created_at timestamp with time zone YES CURRENT_TIMESTAMP
778 video_tasks 42 submitted_at timestamp with time zone YES
779 video_tasks 43 completed_at timestamp with time zone YES
780 video_tasks 44 updated_at timestamp with time zone YES CURRENT_TIMESTAMP
781 video_tasks 46 request_metadata json YES
782 video_tasks 49 request_id character varying NO
783 video_tasks 50 short_id character varying NO
784 video_tasks 52 video_duration_seconds double precision YES
785 video_tasks 53 username character varying YES 用户名快照
786 video_tasks 54 api_key_name character varying YES API Key 名称快照
787 wallet_daily_usage_ledgers 1 id character varying NO
788 wallet_daily_usage_ledgers 2 wallet_id character varying NO
789 wallet_daily_usage_ledgers 3 billing_date date NO
790 wallet_daily_usage_ledgers 4 billing_timezone character varying NO
791 wallet_daily_usage_ledgers 5 total_cost_usd numeric NO '0'::numeric
792 wallet_daily_usage_ledgers 6 total_requests integer NO 0
793 wallet_daily_usage_ledgers 7 input_tokens bigint NO '0'::bigint
794 wallet_daily_usage_ledgers 8 output_tokens bigint NO '0'::bigint
795 wallet_daily_usage_ledgers 9 cache_creation_tokens bigint NO '0'::bigint
796 wallet_daily_usage_ledgers 10 cache_read_tokens bigint NO '0'::bigint
797 wallet_daily_usage_ledgers 11 first_finalized_at timestamp with time zone YES
798 wallet_daily_usage_ledgers 12 last_finalized_at timestamp with time zone YES
799 wallet_daily_usage_ledgers 13 aggregated_at timestamp with time zone NO
800 wallet_daily_usage_ledgers 14 created_at timestamp with time zone NO
801 wallet_daily_usage_ledgers 15 updated_at timestamp with time zone NO
802 wallet_transactions 1 id character varying NO
803 wallet_transactions 2 wallet_id character varying NO
804 wallet_transactions 3 category character varying NO
805 wallet_transactions 4 reason_code character varying NO
806 wallet_transactions 5 amount numeric NO
807 wallet_transactions 6 balance_before numeric NO
808 wallet_transactions 7 balance_after numeric NO
809 wallet_transactions 8 recharge_balance_before numeric NO
810 wallet_transactions 9 recharge_balance_after numeric NO
811 wallet_transactions 10 gift_balance_before numeric NO
812 wallet_transactions 11 gift_balance_after numeric NO
813 wallet_transactions 12 link_type character varying YES
814 wallet_transactions 13 link_id character varying YES
815 wallet_transactions 14 operator_id character varying YES
816 wallet_transactions 15 description text YES
817 wallet_transactions 16 created_at timestamp with time zone NO
818 wallets 1 id character varying NO
819 wallets 2 user_id character varying YES
820 wallets 3 api_key_id character varying YES
821 wallets 4 balance numeric NO '0'::numeric
822 wallets 5 gift_balance numeric NO '0'::numeric
823 wallets 6 limit_mode character varying NO 'finite'::character varying
824 wallets 7 currency character varying NO 'USD'::character varying
825 wallets 8 status character varying NO 'active'::character varying
826 wallets 9 total_recharged numeric NO '0'::numeric
827 wallets 10 total_consumed numeric NO '0'::numeric
828 wallets 11 total_refunded numeric NO '0'::numeric
829 wallets 12 total_adjusted numeric NO '0'::numeric
830 wallets 14 created_at timestamp with time zone NO
831 wallets 15 updated_at timestamp with time zone NO

View File

@@ -0,0 +1,49 @@
table_name column_count
_orphan_api_keys_backup 38
_sqlx_migrations 6
alembic_version 1
announcement_reads 4
announcements 12
api_key_provider_mappings 8
api_keys 21
audit_logs 12
billing_rules 11
dimension_collectors 13
gemini_file_mappings 9
global_models 11
ldap_configs 15
management_tokens 14
models 17
oauth_providers 15
payment_callbacks 13
payment_orders 18
provider_api_keys 52
provider_endpoints 17
provider_usage_tracking 12
providers 23
proxy_node_events 5
proxy_nodes 29
refund_requests 24
request_candidates 24
stats_daily 29
stats_daily_api_key 14
stats_daily_error 8
stats_daily_model 12
stats_daily_provider 11
stats_hourly 16
stats_hourly_model 10
stats_hourly_provider 9
stats_hourly_user 11
stats_summary 17
stats_user_daily 14
system_configs 6
usage 85
user_model_usage_counts 6
user_oauth_links 9
user_preferences 13
user_sessions 22
users 19
video_tasks 50
wallet_daily_usage_ledgers 15
wallet_transactions 16
wallets 14
1 table_name column_count
2 _orphan_api_keys_backup 38
3 _sqlx_migrations 6
4 alembic_version 1
5 announcement_reads 4
6 announcements 12
7 api_key_provider_mappings 8
8 api_keys 21
9 audit_logs 12
10 billing_rules 11
11 dimension_collectors 13
12 gemini_file_mappings 9
13 global_models 11
14 ldap_configs 15
15 management_tokens 14
16 models 17
17 oauth_providers 15
18 payment_callbacks 13
19 payment_orders 18
20 provider_api_keys 52
21 provider_endpoints 17
22 provider_usage_tracking 12
23 providers 23
24 proxy_node_events 5
25 proxy_nodes 29
26 refund_requests 24
27 request_candidates 24
28 stats_daily 29
29 stats_daily_api_key 14
30 stats_daily_error 8
31 stats_daily_model 12
32 stats_daily_provider 11
33 stats_hourly 16
34 stats_hourly_model 10
35 stats_hourly_provider 9
36 stats_hourly_user 11
37 stats_summary 17
38 stats_user_daily 14
39 system_configs 6
40 usage 85
41 user_model_usage_counts 6
42 user_oauth_links 9
43 user_preferences 13
44 user_sessions 22
45 users 19
46 video_tasks 50
47 wallet_daily_usage_ledgers 15
48 wallet_transactions 16
49 wallets 14

View File

@@ -1,6 +1,160 @@
use sqlx::PgPool;
use std::collections::{HashMap, HashSet};
use sqlx::{
migrate::{Migrate, MigrateError, Migrator},
PgPool,
};
use tracing::{error, info, warn};
static MIGRATOR: Migrator = sqlx::migrate!("./migrations");
/// Run all pending migrations embedded at compile time from `migrations/`.
pub async fn run_migrations(pool: &PgPool) -> Result<(), sqlx::migrate::MigrateError> {
sqlx::migrate!("./migrations").run(pool).await
pub async fn run_migrations(pool: &PgPool) -> Result<(), MigrateError> {
let mut conn = pool.acquire().await?;
if MIGRATOR.locking {
conn.lock().await?;
}
let result = run_migrations_locked(&mut *conn).await;
if MIGRATOR.locking {
match conn.unlock().await {
Ok(()) => {}
Err(unlock_error) if result.is_ok() => return Err(unlock_error),
Err(unlock_error) => {
warn!(
error = %unlock_error,
"database migration lock release failed after migration error"
);
}
}
}
result
}
async fn run_migrations_locked<C>(conn: &mut C) -> Result<(), MigrateError>
where
C: Migrate,
{
conn.ensure_migrations_table().await?;
if let Some(version) = conn.dirty_version().await? {
error!(version, "database migration state is dirty");
return Err(MigrateError::Dirty(version));
}
let applied_migrations = conn.list_applied_migrations().await?;
validate_applied_migrations(&applied_migrations)?;
let known_versions: HashSet<_> = MIGRATOR
.iter()
.filter(|migration| migration.migration_type.is_up_migration())
.map(|migration| migration.version)
.collect();
let applied_migrations_by_version: HashMap<_, _> = applied_migrations
.into_iter()
.map(|migration| (migration.version, migration))
.collect();
let pending_migrations: Vec<_> = MIGRATOR
.iter()
.filter(|migration| migration.migration_type.is_up_migration())
.filter(|migration| !applied_migrations_by_version.contains_key(&migration.version))
.collect();
let total_migrations = known_versions.len();
let applied_count = total_migrations.saturating_sub(pending_migrations.len());
if pending_migrations.is_empty() {
info!(
total_migrations,
applied_migrations = applied_count,
pending_migrations = 0,
"database migrations already up to date"
);
return Ok(());
}
info!(
total_migrations,
applied_migrations = applied_count,
pending_migrations = pending_migrations.len(),
"database migrations pending"
);
for (index, migration) in pending_migrations.iter().enumerate() {
let current = index + 1;
let total = pending_migrations.len();
info!(
current,
total,
version = migration.version,
description = %migration.description,
"applying database migration"
);
let elapsed = conn.apply(migration).await?;
info!(
current,
total,
version = migration.version,
description = %migration.description,
elapsed_ms = elapsed.as_millis() as u64,
"applied database migration"
);
}
info!(
total_migrations,
applied_migrations = total_migrations,
pending_migrations = 0,
"database migrations complete"
);
Ok(())
}
fn validate_applied_migrations(
applied_migrations: &[sqlx::migrate::AppliedMigration],
) -> Result<(), MigrateError> {
if MIGRATOR.ignore_missing {
return Ok(());
}
let known_versions: HashSet<_> = MIGRATOR.iter().map(|migration| migration.version).collect();
for applied_migration in applied_migrations {
if !known_versions.contains(&applied_migration.version) {
error!(
version = applied_migration.version,
"applied database migration is missing from embedded migrations"
);
return Err(MigrateError::VersionMissing(applied_migration.version));
}
}
for migration in MIGRATOR
.iter()
.filter(|migration| migration.migration_type.is_up_migration())
{
if let Some(applied_migration) = applied_migrations
.iter()
.find(|applied_migration| applied_migration.version == migration.version)
{
if migration.checksum != applied_migration.checksum {
error!(
version = migration.version,
description = %migration.description,
"database migration checksum mismatch detected"
);
return Err(MigrateError::VersionMismatch(migration.version));
}
}
}
Ok(())
}

View File

@@ -258,6 +258,20 @@ impl UsageWriteRepository for InMemoryUsageReadRepository {
.map(|existing| existing.cache_creation_input_tokens)
.unwrap_or_default()
}),
cache_creation_ephemeral_5m_input_tokens: usage
.cache_creation_ephemeral_5m_input_tokens
.unwrap_or_else(|| {
existing
.map(|existing| existing.cache_creation_ephemeral_5m_input_tokens)
.unwrap_or_default()
}),
cache_creation_ephemeral_1h_input_tokens: usage
.cache_creation_ephemeral_1h_input_tokens
.unwrap_or_else(|| {
existing
.map(|existing| existing.cache_creation_ephemeral_1h_input_tokens)
.unwrap_or_default()
}),
cache_read_input_tokens: usage.cache_read_input_tokens.unwrap_or_else(|| {
existing
.map(|existing| existing.cache_read_input_tokens)
@@ -427,6 +441,8 @@ mod tests {
output_tokens: Some(20),
total_tokens: None,
cache_creation_input_tokens: None,
cache_creation_ephemeral_5m_input_tokens: None,
cache_creation_ephemeral_1h_input_tokens: None,
cache_read_input_tokens: None,
cache_creation_cost_usd: None,
cache_read_cost_usd: None,

View File

@@ -1,5 +1,6 @@
use async_trait::async_trait;
use futures_util::future::BoxFuture;
use serde_json::Value;
use sqlx::{PgPool, Postgres, QueryBuilder, Row};
use uuid::Uuid;
@@ -37,6 +38,8 @@ SELECT
output_tokens,
total_tokens,
COALESCE(cache_creation_input_tokens, 0) AS cache_creation_input_tokens,
COALESCE(cache_creation_input_tokens_5m, 0) AS cache_creation_ephemeral_5m_input_tokens,
COALESCE(cache_creation_input_tokens_1h, 0) AS cache_creation_ephemeral_1h_input_tokens,
COALESCE(cache_read_input_tokens, 0) AS cache_read_input_tokens,
COALESCE(CAST(cache_creation_cost_usd AS DOUBLE PRECISION), 0) AS cache_creation_cost_usd,
COALESCE(CAST(cache_read_cost_usd AS DOUBLE PRECISION), 0) AS cache_read_cost_usd,
@@ -94,6 +97,8 @@ SELECT
output_tokens,
total_tokens,
COALESCE(cache_creation_input_tokens, 0) AS cache_creation_input_tokens,
COALESCE(cache_creation_input_tokens_5m, 0) AS cache_creation_ephemeral_5m_input_tokens,
COALESCE(cache_creation_input_tokens_1h, 0) AS cache_creation_ephemeral_1h_input_tokens,
COALESCE(cache_read_input_tokens, 0) AS cache_read_input_tokens,
COALESCE(CAST(cache_creation_cost_usd AS DOUBLE PRECISION), 0) AS cache_creation_cost_usd,
COALESCE(CAST(cache_read_cost_usd AS DOUBLE PRECISION), 0) AS cache_read_cost_usd,
@@ -181,6 +186,8 @@ SELECT
output_tokens,
total_tokens,
COALESCE(cache_creation_input_tokens, 0) AS cache_creation_input_tokens,
COALESCE(cache_creation_input_tokens_5m, 0) AS cache_creation_ephemeral_5m_input_tokens,
COALESCE(cache_creation_input_tokens_1h, 0) AS cache_creation_ephemeral_1h_input_tokens,
COALESCE(cache_read_input_tokens, 0) AS cache_read_input_tokens,
COALESCE(CAST(cache_creation_cost_usd AS DOUBLE PRECISION), 0) AS cache_creation_cost_usd,
COALESCE(CAST(cache_read_cost_usd AS DOUBLE PRECISION), 0) AS cache_read_cost_usd,
@@ -194,15 +201,15 @@ SELECT
first_byte_time_ms,
status,
billing_status,
NULL::jsonb AS request_headers,
NULL::jsonb AS request_body,
NULL::jsonb AS provider_request_headers,
NULL::jsonb AS provider_request_body,
NULL::jsonb AS response_headers,
NULL::jsonb AS response_body,
NULL::jsonb AS client_response_headers,
NULL::jsonb AS client_response_body,
NULL::jsonb AS request_metadata,
NULL::json AS request_headers,
NULL::json AS request_body,
NULL::json AS provider_request_headers,
NULL::json AS provider_request_body,
NULL::json AS response_headers,
NULL::json AS response_body,
NULL::json AS client_response_headers,
NULL::json AS client_response_body,
NULL::json AS request_metadata,
CAST(EXTRACT(EPOCH FROM created_at) AS BIGINT) AS created_at_unix_ms,
CAST(EXTRACT(EPOCH FROM COALESCE(finalized_at, created_at)) AS BIGINT) AS updated_at_unix_secs,
CAST(EXTRACT(EPOCH FROM finalized_at) AS BIGINT) AS finalized_at_unix_secs
@@ -236,6 +243,8 @@ SELECT
output_tokens,
total_tokens,
COALESCE(cache_creation_input_tokens, 0) AS cache_creation_input_tokens,
COALESCE(cache_creation_input_tokens_5m, 0) AS cache_creation_ephemeral_5m_input_tokens,
COALESCE(cache_creation_input_tokens_1h, 0) AS cache_creation_ephemeral_1h_input_tokens,
COALESCE(cache_read_input_tokens, 0) AS cache_read_input_tokens,
COALESCE(CAST(cache_creation_cost_usd AS DOUBLE PRECISION), 0) AS cache_creation_cost_usd,
COALESCE(CAST(cache_read_cost_usd AS DOUBLE PRECISION), 0) AS cache_read_cost_usd,
@@ -249,15 +258,15 @@ SELECT
first_byte_time_ms,
status,
billing_status,
NULL::jsonb AS request_headers,
NULL::jsonb AS request_body,
NULL::jsonb AS provider_request_headers,
NULL::jsonb AS provider_request_body,
NULL::jsonb AS response_headers,
NULL::jsonb AS response_body,
NULL::jsonb AS client_response_headers,
NULL::jsonb AS client_response_body,
NULL::jsonb AS request_metadata,
NULL::json AS request_headers,
NULL::json AS request_body,
NULL::json AS provider_request_headers,
NULL::json AS provider_request_body,
NULL::json AS response_headers,
NULL::json AS response_body,
NULL::json AS client_response_headers,
NULL::json AS client_response_body,
NULL::json AS request_metadata,
CAST(EXTRACT(EPOCH FROM created_at) AS BIGINT) AS created_at_unix_ms,
CAST(EXTRACT(EPOCH FROM COALESCE(finalized_at, created_at)) AS BIGINT) AS updated_at_unix_secs,
CAST(EXTRACT(EPOCH FROM finalized_at) AS BIGINT) AS finalized_at_unix_secs
@@ -291,6 +300,8 @@ INSERT INTO "usage" (
output_tokens,
total_tokens,
cache_creation_input_tokens,
cache_creation_input_tokens_5m,
cache_creation_input_tokens_1h,
cache_read_input_tokens,
cache_creation_cost_usd,
cache_read_cost_usd,
@@ -344,10 +355,10 @@ INSERT INTO "usage" (
COALESCE($26, 0),
COALESCE($27, 0),
COALESCE($28, 0),
$29,
COALESCE($30, 0),
COALESCE($29, 0),
$30,
COALESCE($31, 0),
$32,
COALESCE($32, 0),
$33,
$34,
$35,
@@ -356,18 +367,20 @@ INSERT INTO "usage" (
$38,
$39,
$40,
$41,
$42,
$43,
$44,
$45,
$46,
$47,
$41::json,
$42::json,
$43::json,
$44::json,
$45::json,
$46::json,
$47::json,
$48::json,
$49::json,
CASE
WHEN $48 IS NULL THEN NULL
ELSE TO_TIMESTAMP($48::double precision)
WHEN $50 IS NULL THEN NULL
ELSE TO_TIMESTAMP($50::double precision)
END,
COALESCE(TO_TIMESTAMP($49::double precision), NOW())
COALESCE(TO_TIMESTAMP($51::double precision), NOW())
)
ON CONFLICT (request_id)
DO UPDATE SET
@@ -394,6 +407,8 @@ DO UPDATE SET
output_tokens = CASE WHEN "usage".billing_status = 'pending' THEN COALESCE(EXCLUDED.output_tokens, "usage".output_tokens) ELSE "usage".output_tokens END,
total_tokens = CASE WHEN "usage".billing_status = 'pending' THEN COALESCE(EXCLUDED.total_tokens, "usage".total_tokens) ELSE "usage".total_tokens END,
cache_creation_input_tokens = CASE WHEN "usage".billing_status = 'pending' THEN COALESCE(EXCLUDED.cache_creation_input_tokens, "usage".cache_creation_input_tokens) ELSE "usage".cache_creation_input_tokens END,
cache_creation_input_tokens_5m = CASE WHEN "usage".billing_status = 'pending' THEN COALESCE(EXCLUDED.cache_creation_input_tokens_5m, "usage".cache_creation_input_tokens_5m) ELSE "usage".cache_creation_input_tokens_5m END,
cache_creation_input_tokens_1h = CASE WHEN "usage".billing_status = 'pending' THEN COALESCE(EXCLUDED.cache_creation_input_tokens_1h, "usage".cache_creation_input_tokens_1h) ELSE "usage".cache_creation_input_tokens_1h END,
cache_read_input_tokens = CASE WHEN "usage".billing_status = 'pending' THEN COALESCE(EXCLUDED.cache_read_input_tokens, "usage".cache_read_input_tokens) ELSE "usage".cache_read_input_tokens END,
cache_creation_cost_usd = CASE WHEN "usage".billing_status = 'pending' THEN COALESCE(EXCLUDED.cache_creation_cost_usd, "usage".cache_creation_cost_usd) ELSE "usage".cache_creation_cost_usd END,
cache_read_cost_usd = CASE WHEN "usage".billing_status = 'pending' THEN COALESCE(EXCLUDED.cache_read_cost_usd, "usage".cache_read_cost_usd) ELSE "usage".cache_read_cost_usd END,
@@ -443,6 +458,8 @@ RETURNING
output_tokens,
total_tokens,
COALESCE(cache_creation_input_tokens, 0) AS cache_creation_input_tokens,
COALESCE(cache_creation_input_tokens_5m, 0) AS cache_creation_ephemeral_5m_input_tokens,
COALESCE(cache_creation_input_tokens_1h, 0) AS cache_creation_ephemeral_1h_input_tokens,
COALESCE(cache_read_input_tokens, 0) AS cache_read_input_tokens,
COALESCE(CAST(cache_creation_cost_usd AS DOUBLE PRECISION), 0) AS cache_creation_cost_usd,
COALESCE(CAST(cache_read_cost_usd AS DOUBLE PRECISION), 0) AS cache_read_cost_usd,
@@ -653,6 +670,19 @@ impl SqlxUsageReadRepository {
self.tx_runner
.run_read_write(|tx| {
Box::pin(async move {
let request_headers_json = json_bind_text(usage.request_headers.as_ref())?;
let request_body_json = json_bind_text(usage.request_body.as_ref())?;
let provider_request_headers_json =
json_bind_text(usage.provider_request_headers.as_ref())?;
let provider_request_body_json =
json_bind_text(usage.provider_request_body.as_ref())?;
let response_headers_json = json_bind_text(usage.response_headers.as_ref())?;
let response_body_json = json_bind_text(usage.response_body.as_ref())?;
let client_response_headers_json =
json_bind_text(usage.client_response_headers.as_ref())?;
let client_response_body_json =
json_bind_text(usage.client_response_body.as_ref())?;
let request_metadata_json = json_bind_text(usage.request_metadata.as_ref())?;
let row = sqlx::query(UPSERT_SQL)
.bind(Uuid::new_v4().to_string())
.bind(&usage.request_id)
@@ -690,6 +720,18 @@ impl SqlxUsageReadRepository {
.transpose()?,
)
.bind(usage.cache_creation_input_tokens.map(to_i32).transpose()?)
.bind(
usage
.cache_creation_ephemeral_5m_input_tokens
.map(to_i32)
.transpose()?,
)
.bind(
usage
.cache_creation_ephemeral_1h_input_tokens
.map(to_i32)
.transpose()?,
)
.bind(usage.cache_read_input_tokens.map(to_i32).transpose()?)
.bind(usage.cache_creation_cost_usd)
.bind(usage.cache_read_cost_usd)
@@ -703,15 +745,15 @@ impl SqlxUsageReadRepository {
.bind(usage.first_byte_time_ms.map(to_i32).transpose()?)
.bind(&usage.status)
.bind(&usage.billing_status)
.bind(&usage.request_headers)
.bind(&usage.request_body)
.bind(&usage.provider_request_headers)
.bind(&usage.provider_request_body)
.bind(&usage.response_headers)
.bind(&usage.response_body)
.bind(&usage.client_response_headers)
.bind(&usage.client_response_body)
.bind(&usage.request_metadata)
.bind(&request_headers_json)
.bind(&request_body_json)
.bind(&provider_request_headers_json)
.bind(&provider_request_body_json)
.bind(&response_headers_json)
.bind(&response_body_json)
.bind(&client_response_headers_json)
.bind(&client_response_body_json)
.bind(&request_metadata_json)
.bind(usage.finalized_at_unix_secs.map(|value| value as f64))
.bind(usage.created_at_unix_ms.map(|value| value as f64))
.fetch_one(&mut **tx)
@@ -826,6 +868,18 @@ fn map_usage_row(row: &sqlx::postgres::PgRow) -> Result<StoredRequestUsageAudit,
.map(|value| to_u64(value, "usage.cache_creation_input_tokens"))
.transpose()?
.unwrap_or_default();
usage.cache_creation_ephemeral_5m_input_tokens = row
.try_get::<Option<i32>, _>("cache_creation_ephemeral_5m_input_tokens")
.map_postgres_err()?
.map(|value| to_u64(value, "usage.cache_creation_ephemeral_5m_input_tokens"))
.transpose()?
.unwrap_or_default();
usage.cache_creation_ephemeral_1h_input_tokens = row
.try_get::<Option<i32>, _>("cache_creation_ephemeral_1h_input_tokens")
.map_postgres_err()?
.map(|value| to_u64(value, "usage.cache_creation_ephemeral_1h_input_tokens"))
.transpose()?
.unwrap_or_default();
usage.cache_read_input_tokens = row
.try_get::<Option<i32>, _>("cache_read_input_tokens")
.map_postgres_err()?
@@ -862,6 +916,16 @@ fn to_u64(value: i32, field_name: &str) -> Result<u64, DataLayerError> {
.map_err(|_| DataLayerError::UnexpectedValue(format!("invalid {field_name}: {value}")))
}
fn json_bind_text(value: Option<&Value>) -> Result<Option<String>, DataLayerError> {
value
.map(|value| {
serde_json::to_string(value).map_err(|err| {
DataLayerError::UnexpectedValue(format!("failed to serialize usage json: {err}"))
})
})
.transpose()
}
#[cfg(test)]
mod tests {
use super::SqlxUsageReadRepository;
@@ -930,6 +994,8 @@ mod tests {
output_tokens: Some(20),
total_tokens: Some(30),
cache_creation_input_tokens: None,
cache_creation_ephemeral_5m_input_tokens: None,
cache_creation_ephemeral_1h_input_tokens: None,
cache_read_input_tokens: None,
cache_creation_cost_usd: None,
cache_read_cost_usd: None,
@@ -980,10 +1046,32 @@ mod tests {
assert!(super::LIST_RECENT_USAGE_AUDITS_PREFIX.contains("FROM \"usage\""));
}
#[test]
fn usage_sql_uses_json_null_placeholders_for_usage_payload_columns() {
assert!(super::LIST_USAGE_AUDITS_PREFIX.contains("NULL::json AS request_headers"));
assert!(super::LIST_USAGE_AUDITS_PREFIX.contains("NULL::json AS provider_request_body"));
assert!(super::LIST_RECENT_USAGE_AUDITS_PREFIX.contains("NULL::json AS request_headers"));
assert!(
super::LIST_RECENT_USAGE_AUDITS_PREFIX.contains("NULL::json AS provider_request_body")
);
assert!(!super::LIST_USAGE_AUDITS_PREFIX.contains("NULL::jsonb"));
assert!(!super::LIST_RECENT_USAGE_AUDITS_PREFIX.contains("NULL::jsonb"));
}
#[test]
fn usage_sql_casts_json_payload_bind_parameters_explicitly() {
for placeholder in 41..=49 {
assert!(
super::UPSERT_SQL.contains(format!("${placeholder}::json").as_str()),
"missing ::json cast for placeholder ${placeholder}"
);
}
}
#[test]
fn usage_sql_insert_values_aligns_request_metadata_and_timestamps() {
assert!(super::UPSERT_SQL.contains("\n $46,\n $47,\n CASE"));
assert!(super::UPSERT_SQL.contains("WHEN $48 IS NULL THEN NULL"));
assert!(super::UPSERT_SQL.contains("TO_TIMESTAMP($49::double precision)"));
assert!(super::UPSERT_SQL.contains("\n $48::json,\n $49::json,\n CASE"));
assert!(super::UPSERT_SQL.contains("WHEN $50 IS NULL THEN NULL"));
assert!(super::UPSERT_SQL.contains("TO_TIMESTAMP($51::double precision)"));
}
}

View File

@@ -1,8 +1,12 @@
use std::collections::BTreeMap;
use super::headers::should_skip_upstream_passthrough_header;
use super::headers::{
should_skip_upstream_complete_passthrough_header, should_skip_upstream_passthrough_header,
};
use super::snapshot::GatewayProviderTransportSnapshot;
const DEFAULT_ANTHROPIC_VERSION: &str = "2023-06-01";
fn collect_passthrough_headers(
headers: &http::HeaderMap,
extra_headers: &BTreeMap<String, String>,
@@ -35,6 +39,38 @@ fn collect_passthrough_headers(
out
}
fn collect_complete_passthrough_headers(
headers: &http::HeaderMap,
extra_headers: &BTreeMap<String, String>,
) -> BTreeMap<String, String> {
let mut out = BTreeMap::new();
for (name, value) in headers.iter() {
let Ok(value) = value.to_str() else {
continue;
};
let key = name.as_str().to_ascii_lowercase();
if should_skip_upstream_complete_passthrough_header(&key) {
continue;
}
let value = value.trim();
if value.is_empty() {
continue;
}
out.insert(key, value.to_string());
}
for (key, value) in extra_headers {
let normalized_key = key.to_ascii_lowercase();
let value = value.trim();
if value.is_empty() {
continue;
}
out.insert(normalized_key, value.to_string());
}
out
}
pub fn build_passthrough_headers(
headers: &http::HeaderMap,
extra_headers: &BTreeMap<String, String>,
@@ -64,6 +100,76 @@ pub fn build_openai_passthrough_headers(
out
}
pub fn build_complete_passthrough_headers(
headers: &http::HeaderMap,
extra_headers: &BTreeMap<String, String>,
content_type: Option<&str>,
) -> BTreeMap<String, String> {
let mut out = collect_complete_passthrough_headers(headers, extra_headers);
out.entry("content-type".to_string()).or_insert_with(|| {
content_type
.filter(|value| !value.trim().is_empty())
.unwrap_or("application/json")
.trim()
.to_string()
});
out.remove("content-length");
out
}
pub fn build_complete_passthrough_headers_with_auth(
headers: &http::HeaderMap,
auth_header: &str,
auth_value: &str,
extra_headers: &BTreeMap<String, String>,
content_type: Option<&str>,
) -> BTreeMap<String, String> {
let mut out = build_complete_passthrough_headers(headers, extra_headers, content_type);
ensure_upstream_auth_header(&mut out, auth_header, auth_value);
out
}
pub fn build_claude_passthrough_headers(
headers: &http::HeaderMap,
auth_header: &str,
auth_value: &str,
extra_headers: &BTreeMap<String, String>,
content_type: Option<&str>,
) -> BTreeMap<String, String> {
let mut out = build_openai_passthrough_headers(
headers,
auth_header,
auth_value,
extra_headers,
content_type,
);
for (name, value) in headers.iter() {
let Ok(value) = value.to_str() else {
continue;
};
let key = name.as_str().to_ascii_lowercase();
let value = value.trim();
if value.is_empty() || !should_restore_claude_passthrough_header(&key) {
continue;
}
if key == "anthropic-beta" {
let merged = merge_comma_header_values(out.get(&key).map(String::as_str), Some(value));
if let Some(merged) = merged {
out.insert(key, merged);
}
continue;
}
out.entry(key).or_insert_with(|| value.to_string());
}
out.entry("anthropic-version".to_string())
.or_insert_with(|| DEFAULT_ANTHROPIC_VERSION.to_string());
out
}
pub fn build_passthrough_headers_with_auth(
headers: &http::HeaderMap,
auth_header: &str,
@@ -96,6 +202,30 @@ pub fn ensure_upstream_auth_header(
}
}
fn should_restore_claude_passthrough_header(name: &str) -> bool {
name.starts_with("anthropic-") || name.starts_with("x-stainless-") || name == "x-app"
}
fn merge_comma_header_values(left: Option<&str>, right: Option<&str>) -> Option<String> {
let mut merged = Vec::new();
for raw in [left, right].into_iter().flatten() {
for token in raw.split(',') {
let token = token.trim();
if token.is_empty() || merged.iter().any(|existing: &String| existing == token) {
continue;
}
merged.push(token.to_string());
}
}
if merged.is_empty() {
None
} else {
Some(merged.join(","))
}
}
pub fn resolve_local_openai_chat_auth(
transport: &GatewayProviderTransportSnapshot,
) -> Option<(String, String)> {
@@ -142,3 +272,112 @@ pub fn resolve_local_gemini_auth(
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::{build_claude_passthrough_headers, build_complete_passthrough_headers_with_auth};
use std::collections::BTreeMap;
#[test]
fn claude_passthrough_headers_restore_stripped_anthropic_headers() {
let mut headers = http::HeaderMap::new();
headers.insert(
"anthropic-beta",
http::HeaderValue::from_static("prompt-caching-2024-07-31,context-1m-2025-08-07"),
);
headers.insert(
"x-stainless-runtime-version",
http::HeaderValue::from_static("v22.14.0"),
);
headers.insert("x-app", http::HeaderValue::from_static("cli"));
let built = build_claude_passthrough_headers(
&headers,
"x-api-key",
"sk-upstream-claude",
&BTreeMap::from([("anthropic-beta".to_string(), "custom-beta".to_string())]),
Some("application/json"),
);
assert_eq!(
built.get("anthropic-version").map(String::as_str),
Some("2023-06-01")
);
assert_eq!(
built.get("anthropic-beta").map(String::as_str),
Some("custom-beta,prompt-caching-2024-07-31,context-1m-2025-08-07")
);
assert_eq!(
built.get("x-stainless-runtime-version").map(String::as_str),
Some("v22.14.0")
);
assert_eq!(built.get("x-app").map(String::as_str), Some("cli"));
assert_eq!(
built.get("x-api-key").map(String::as_str),
Some("sk-upstream-claude")
);
}
#[test]
fn claude_passthrough_headers_preserve_explicit_anthropic_version_override() {
let mut headers = http::HeaderMap::new();
headers.insert(
"anthropic-version",
http::HeaderValue::from_static("2024-01-01"),
);
let built = build_claude_passthrough_headers(
&headers,
"authorization",
"Bearer upstream-token",
&BTreeMap::new(),
Some("application/json"),
);
assert_eq!(
built.get("anthropic-version").map(String::as_str),
Some("2024-01-01")
);
}
#[test]
fn complete_passthrough_headers_preserve_business_headers() {
let mut headers = http::HeaderMap::new();
headers.insert(
"anthropic-beta",
http::HeaderValue::from_static("prompt-caching-2024-07-31"),
);
headers.insert(
"x-stainless-runtime-version",
http::HeaderValue::from_static("v24.0.0"),
);
headers.insert("x-app", http::HeaderValue::from_static("cli"));
headers.insert(
"authorization",
http::HeaderValue::from_static("Bearer client-token"),
);
let built = build_complete_passthrough_headers_with_auth(
&headers,
"x-api-key",
"sk-upstream",
&BTreeMap::new(),
Some("application/json"),
);
assert_eq!(
built.get("anthropic-beta").map(String::as_str),
Some("prompt-caching-2024-07-31")
);
assert_eq!(
built.get("x-stainless-runtime-version").map(String::as_str),
Some("v24.0.0")
);
assert_eq!(built.get("x-app").map(String::as_str), Some("cli"));
assert_eq!(built.get("authorization"), None);
assert_eq!(
built.get("x-api-key").map(String::as_str),
Some("sk-upstream")
);
}
}

View File

@@ -53,6 +53,29 @@ pub fn should_skip_upstream_passthrough_header(name: &str) -> bool {
) || should_skip_request_header(name)
}
pub(crate) fn should_skip_upstream_complete_passthrough_header(name: &str) -> bool {
let lower = name.to_ascii_lowercase();
matches!(
lower.as_str(),
"authorization"
| "x-api-key"
| "x-goog-api-key"
| "host"
| "content-length"
| "transfer-encoding"
| "connection"
| "accept-encoding"
| "content-encoding"
| "x-real-ip"
| "x-real-proto"
| "x-forwarded-for"
| "x-forwarded-proto"
| "x-forwarded-scheme"
| "x-forwarded-host"
| "x-forwarded-port"
) || should_skip_request_header(name)
}
#[cfg(test)]
mod tests {
use super::should_skip_upstream_passthrough_header;

View File

@@ -258,13 +258,13 @@ CREATE TABLE IF NOT EXISTS proxy_nodes (
failed_requests BIGINT NOT NULL DEFAULT 0,
dns_failures BIGINT NOT NULL DEFAULT 0,
stream_errors BIGINT NOT NULL DEFAULT 0,
proxy_metadata JSONB NULL,
hardware_info JSONB NULL,
proxy_metadata JSON NULL,
hardware_info JSON NULL,
estimated_max_concurrency INTEGER NULL,
tunnel_mode BOOLEAN NOT NULL DEFAULT FALSE,
tunnel_connected BOOLEAN NOT NULL DEFAULT FALSE,
tunnel_connected_at TIMESTAMPTZ NULL,
remote_config JSONB NULL,
remote_config JSON NULL,
config_version INTEGER NOT NULL DEFAULT 1,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP

View File

@@ -64,6 +64,10 @@ pub struct UsageEventData {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_creation_input_tokens: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_creation_ephemeral_5m_input_tokens: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_creation_ephemeral_1h_input_tokens: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_read_input_tokens: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_creation_cost_usd: Option<f64>,

View File

@@ -41,5 +41,6 @@ pub use write::{
build_pending_usage_record, build_stream_terminal_usage_event,
build_stream_terminal_usage_outcome, build_streaming_usage_record,
build_sync_terminal_usage_event, build_sync_terminal_usage_outcome,
build_terminal_usage_event_from_outcome, TerminalUsageOutcome, UsageTerminalState,
build_terminal_usage_event_from_outcome, build_usage_event_data_seed, TerminalUsageOutcome,
UsageTerminalState,
};

View File

@@ -36,6 +36,8 @@ pub fn build_upsert_usage_record_from_event(
output_tokens: data.output_tokens,
total_tokens: data.total_tokens,
cache_creation_input_tokens: data.cache_creation_input_tokens,
cache_creation_ephemeral_5m_input_tokens: data.cache_creation_ephemeral_5m_input_tokens,
cache_creation_ephemeral_1h_input_tokens: data.cache_creation_ephemeral_1h_input_tokens,
cache_read_input_tokens: data.cache_read_input_tokens,
cache_creation_cost_usd: data.cache_creation_cost_usd,
cache_read_cost_usd: data.cache_read_cost_usd,

View File

@@ -238,6 +238,25 @@ impl UsageRuntime {
}
}
pub async fn record_terminal_event<T>(&self, data: &T, mut event: UsageEvent)
where
T: UsageRuntimeAccess,
{
if !self.is_enabled() {
return;
}
if let Err(err) = data.enrich_usage_event(&mut event).await {
warn!(
event_name = "usage_terminal_billing_enrichment_failed",
log_type = "event",
request_id = %event.request_id,
error = %err,
"usage runtime failed to enrich terminal usage event with billing"
);
}
self.enqueue_or_write_terminal(data, event).await;
}
async fn enqueue_or_write_terminal<T>(&self, data: &T, event: UsageEvent)
where
T: UsageRuntimeAccess,

View File

@@ -5,6 +5,8 @@ pub struct StandardizedUsage {
pub input_tokens: i64,
pub output_tokens: i64,
pub cache_creation_tokens: i64,
pub cache_creation_ephemeral_5m_tokens: i64,
pub cache_creation_ephemeral_1h_tokens: i64,
pub cache_read_tokens: i64,
pub reasoning_tokens: i64,
pub cache_storage_token_hours: f64,
@@ -25,6 +27,12 @@ impl StandardizedUsage {
"input_tokens" => Some(serde_json::json!(self.input_tokens)),
"output_tokens" => Some(serde_json::json!(self.output_tokens)),
"cache_creation_tokens" => Some(serde_json::json!(self.cache_creation_tokens)),
"cache_creation_ephemeral_5m_tokens" => {
Some(serde_json::json!(self.cache_creation_ephemeral_5m_tokens))
}
"cache_creation_ephemeral_1h_tokens" => {
Some(serde_json::json!(self.cache_creation_ephemeral_1h_tokens))
}
"cache_read_tokens" => Some(serde_json::json!(self.cache_read_tokens)),
"reasoning_tokens" => Some(serde_json::json!(self.reasoning_tokens)),
"cache_storage_token_hours" => Some(serde_json::json!(self.cache_storage_token_hours)),
@@ -40,6 +48,12 @@ impl StandardizedUsage {
"input_tokens" => self.input_tokens = as_i64(&value, 0),
"output_tokens" => self.output_tokens = as_i64(&value, 0),
"cache_creation_tokens" => self.cache_creation_tokens = as_i64(&value, 0),
"cache_creation_ephemeral_5m_tokens" => {
self.cache_creation_ephemeral_5m_tokens = as_i64(&value, 0)
}
"cache_creation_ephemeral_1h_tokens" => {
self.cache_creation_ephemeral_1h_tokens = as_i64(&value, 0)
}
"cache_read_tokens" => self.cache_read_tokens = as_i64(&value, 0),
"reasoning_tokens" => self.reasoning_tokens = as_i64(&value, 0),
"cache_storage_token_hours" => self.cache_storage_token_hours = as_f64(&value, 0.0),
@@ -55,6 +69,18 @@ impl StandardizedUsage {
}
}
}
pub fn normalize_cache_creation_breakdown(mut self) -> Self {
if self.cache_creation_tokens <= 0 {
let derived = self
.cache_creation_ephemeral_5m_tokens
.saturating_add(self.cache_creation_ephemeral_1h_tokens);
if derived > 0 {
self.cache_creation_tokens = derived;
}
}
self
}
}
fn as_i64(value: &serde_json::Value, default: i64) -> i64 {

View File

@@ -26,23 +26,13 @@ impl UsageMapper {
}
}
usage
usage.normalize_cache_creation_breakdown()
}
pub fn map_from_response(response: &serde_json::Value, api_format: &str) -> StandardizedUsage {
let family = api_family(api_format);
let usage_value = if family == "gemini" {
response
.get("usageMetadata")
.or_else(|| {
response
.get("candidates")
.and_then(|v| v.get(0))
.and_then(|v| v.get("usageMetadata"))
})
.unwrap_or(&serde_json::Value::Null)
} else {
response.get("usage").unwrap_or(&serde_json::Value::Null)
let Some(usage_value) = resolve_usage_value(response, family.as_str()) else {
return StandardizedUsage::new();
};
Self::map(usage_value, api_format, None)
}
@@ -74,14 +64,32 @@ fn base_mapping(api_format: &str) -> BTreeMap<String, String> {
"openai" => {
mapping.insert("prompt_tokens".to_string(), "input_tokens".to_string());
mapping.insert("completion_tokens".to_string(), "output_tokens".to_string());
mapping.insert("input_tokens".to_string(), "input_tokens".to_string());
mapping.insert("output_tokens".to_string(), "output_tokens".to_string());
mapping.insert(
"prompt_tokens_details.cached_tokens".to_string(),
"cache_read_tokens".to_string(),
);
mapping.insert(
"input_tokens_details.cached_tokens".to_string(),
"cache_read_tokens".to_string(),
);
mapping.insert(
"prompt_tokens_details.cached_creation_tokens".to_string(),
"cache_creation_tokens".to_string(),
);
mapping.insert(
"input_tokens_details.cached_creation_tokens".to_string(),
"cache_creation_tokens".to_string(),
);
mapping.insert(
"completion_tokens_details.reasoning_tokens".to_string(),
"reasoning_tokens".to_string(),
);
mapping.insert(
"output_tokens_details.reasoning_tokens".to_string(),
"reasoning_tokens".to_string(),
);
}
"gemini" => {
mapping.insert("promptTokenCount".to_string(), "input_tokens".to_string());
@@ -106,6 +114,26 @@ fn base_mapping(api_format: &str) -> BTreeMap<String, String> {
"cache_read_tokens".to_string(),
);
}
"claude" | "anthropic" => {
mapping.insert("input_tokens".to_string(), "input_tokens".to_string());
mapping.insert("output_tokens".to_string(), "output_tokens".to_string());
mapping.insert(
"cache_creation_input_tokens".to_string(),
"cache_creation_tokens".to_string(),
);
mapping.insert(
"cache_creation.ephemeral_5m_input_tokens".to_string(),
"cache_creation_ephemeral_5m_tokens".to_string(),
);
mapping.insert(
"cache_creation.ephemeral_1h_input_tokens".to_string(),
"cache_creation_ephemeral_1h_tokens".to_string(),
);
mapping.insert(
"cache_read_input_tokens".to_string(),
"cache_read_tokens".to_string(),
);
}
_ => {
mapping.insert("input_tokens".to_string(), "input_tokens".to_string());
mapping.insert("output_tokens".to_string(), "output_tokens".to_string());
@@ -130,6 +158,47 @@ fn get_nested_value<'a>(value: &'a serde_json::Value, path: &str) -> Option<&'a
Some(current)
}
fn resolve_usage_value<'a>(
response: &'a serde_json::Value,
family: &str,
) -> Option<&'a serde_json::Value> {
match family {
"gemini" => {
if let Some(usage) = response.get("usageMetadata") {
return Some(usage);
}
if let Some(usage) = response
.get("candidates")
.and_then(|value| value.get(0))
.and_then(|value| value.get("usageMetadata"))
{
return Some(usage);
}
}
_ => {
if let Some(usage) = response.get("usage") {
return Some(usage);
}
}
}
if let Some(nested) = response.get("response") {
if let Some(usage) = resolve_usage_value(nested, family) {
return Some(usage);
}
}
if let Some(chunks) = response.get("chunks").and_then(serde_json::Value::as_array) {
for chunk in chunks.iter().rev() {
if let Some(usage) = resolve_usage_value(chunk, family) {
return Some(usage);
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::{map_usage, map_usage_from_response};
@@ -140,7 +209,10 @@ mod tests {
&serde_json::json!({
"prompt_tokens": 12,
"completion_tokens": 8,
"prompt_tokens_details": { "cached_tokens": 2 },
"prompt_tokens_details": {
"cached_tokens": 2,
"cached_creation_tokens": 1
},
"completion_tokens_details": { "reasoning_tokens": 3 }
}),
"openai:chat",
@@ -148,10 +220,81 @@ mod tests {
assert_eq!(usage.input_tokens, 12);
assert_eq!(usage.output_tokens, 8);
assert_eq!(usage.cache_creation_tokens, 1);
assert_eq!(usage.cache_read_tokens, 2);
assert_eq!(usage.reasoning_tokens, 3);
}
#[test]
fn maps_openai_responses_usage_from_response() {
let usage = map_usage_from_response(
&serde_json::json!({
"usage": {
"input_tokens": 14,
"output_tokens": 6,
"total_tokens": 20,
"input_tokens_details": {
"cached_tokens": 3,
"cached_creation_tokens": 2
},
"output_tokens_details": {
"reasoning_tokens": 1
}
}
}),
"openai:cli",
);
assert_eq!(usage.input_tokens, 14);
assert_eq!(usage.output_tokens, 6);
assert_eq!(usage.cache_creation_tokens, 2);
assert_eq!(usage.cache_read_tokens, 3);
assert_eq!(usage.reasoning_tokens, 1);
}
#[test]
fn maps_openai_responses_usage_from_stream_chunks() {
let usage = map_usage_from_response(
&serde_json::json!({
"chunks": [
{
"type": "response.created",
"response": {
"id": "resp_123",
"object": "response"
}
},
{
"type": "response.completed",
"response": {
"id": "resp_123",
"object": "response",
"usage": {
"input_tokens": 9,
"output_tokens": 4,
"total_tokens": 13,
"input_tokens_details": {
"cached_tokens": 5,
"cached_creation_tokens": 2
},
"output_tokens_details": {
"reasoning_tokens": 1
}
}
}
}
]
}),
"openai:cli",
);
assert_eq!(usage.input_tokens, 9);
assert_eq!(usage.output_tokens, 4);
assert_eq!(usage.cache_creation_tokens, 2);
assert_eq!(usage.cache_read_tokens, 5);
assert_eq!(usage.reasoning_tokens, 1);
}
#[test]
fn maps_claude_usage() {
let usage = map_usage(
@@ -170,6 +313,30 @@ mod tests {
assert_eq!(usage.cache_read_tokens, 1);
}
#[test]
fn maps_claude_usage_with_ephemeral_cache_breakdown() {
let usage = map_usage(
&serde_json::json!({
"input_tokens": 1,
"output_tokens": 8,
"cache_creation": {
"ephemeral_1h_input_tokens": 0,
"ephemeral_5m_input_tokens": 5191
},
"cache_creation_input_tokens": 5191,
"cache_read_input_tokens": 97634
}),
"claude:chat",
);
assert_eq!(usage.input_tokens, 1);
assert_eq!(usage.output_tokens, 8);
assert_eq!(usage.cache_creation_tokens, 5191);
assert_eq!(usage.cache_creation_ephemeral_5m_tokens, 5191);
assert_eq!(usage.cache_creation_ephemeral_1h_tokens, 0);
assert_eq!(usage.cache_read_tokens, 97634);
}
#[test]
fn maps_gemini_usage_from_response() {
let usage = map_usage_from_response(

View File

@@ -409,6 +409,8 @@ fn build_upsert_usage_record(
output_tokens: data.output_tokens,
total_tokens: data.total_tokens,
cache_creation_input_tokens: data.cache_creation_input_tokens,
cache_creation_ephemeral_5m_input_tokens: data.cache_creation_ephemeral_5m_input_tokens,
cache_creation_ephemeral_1h_input_tokens: data.cache_creation_ephemeral_1h_input_tokens,
cache_read_input_tokens: data.cache_read_input_tokens,
cache_creation_cost_usd: data.cache_creation_cost_usd,
cache_read_cost_usd: data.cache_read_cost_usd,
@@ -437,7 +439,10 @@ fn build_upsert_usage_record(
})
}
fn build_base_usage_data(plan: &ExecutionPlan, report_context: Option<&Value>) -> UsageEventData {
pub fn build_usage_event_data_seed(
plan: &ExecutionPlan,
report_context: Option<&Value>,
) -> UsageEventData {
let context = report_context.and_then(Value::as_object);
let api_format = context_string(context, "client_api_format")
.or_else(|| non_empty_string(Some(plan.client_api_format.clone())));
@@ -497,6 +502,10 @@ fn build_base_usage_data(plan: &ExecutionPlan, report_context: Option<&Value>) -
}
}
fn build_base_usage_data(plan: &ExecutionPlan, report_context: Option<&Value>) -> UsageEventData {
build_usage_event_data_seed(plan, report_context)
}
fn merge_usage_data(base: UsageEventData, override_data: UsageEventData) -> UsageEventData {
UsageEventData {
user_id: override_data.user_id.or(base.user_id),
@@ -544,6 +553,12 @@ fn merge_usage_data(base: UsageEventData, override_data: UsageEventData) -> Usag
cache_creation_input_tokens: override_data
.cache_creation_input_tokens
.or(base.cache_creation_input_tokens),
cache_creation_ephemeral_5m_input_tokens: override_data
.cache_creation_ephemeral_5m_input_tokens
.or(base.cache_creation_ephemeral_5m_input_tokens),
cache_creation_ephemeral_1h_input_tokens: override_data
.cache_creation_ephemeral_1h_input_tokens
.or(base.cache_creation_ephemeral_1h_input_tokens),
cache_read_input_tokens: override_data
.cache_read_input_tokens
.or(base.cache_read_input_tokens),
@@ -668,6 +683,14 @@ fn apply_standardized_usage(
if usage.cache_creation_tokens > 0 {
data.cache_creation_input_tokens = Some(usage.cache_creation_tokens as u64);
}
if usage.cache_creation_ephemeral_5m_tokens > 0 {
data.cache_creation_ephemeral_5m_input_tokens =
Some(usage.cache_creation_ephemeral_5m_tokens as u64);
}
if usage.cache_creation_ephemeral_1h_tokens > 0 {
data.cache_creation_ephemeral_1h_input_tokens =
Some(usage.cache_creation_ephemeral_1h_tokens as u64);
}
if usage.cache_read_tokens > 0 {
data.cache_read_input_tokens = Some(usage.cache_read_tokens as u64);
}
@@ -1167,7 +1190,7 @@ mod tests {
"event: response.output_text.delta\n",
"data: {\"type\":\"response.output_text.delta\",\"delta\":\"Hello from CLI stream\"}\n\n",
"event: response.completed\n",
"data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_123\",\"object\":\"response\",\"model\":\"gpt-5.4\",\"status\":\"completed\",\"output\":[{\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"Hello from CLI stream\"}]}],\"usage\":{\"input_tokens\":3,\"output_tokens\":5,\"total_tokens\":8}}}\n\n",
"data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_123\",\"object\":\"response\",\"model\":\"gpt-5.4\",\"status\":\"completed\",\"output\":[{\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"Hello from CLI stream\"}]}],\"usage\":{\"input_tokens\":3,\"input_tokens_details\":{\"cached_tokens\":2,\"cached_creation_tokens\":1},\"output_tokens\":5,\"output_tokens_details\":{\"reasoning_tokens\":1},\"total_tokens\":8}}}\n\n",
"data: [DONE]\n",
);
let payload = GatewayStreamReportRequest {
@@ -1191,6 +1214,8 @@ mod tests {
assert_eq!(event.data.input_tokens, Some(3));
assert_eq!(event.data.output_tokens, Some(5));
assert_eq!(event.data.total_tokens, Some(8));
assert_eq!(event.data.cache_creation_input_tokens, Some(1));
assert_eq!(event.data.cache_read_input_tokens, Some(2));
assert_eq!(
event.data.response_body,
Some(json!({
@@ -1229,7 +1254,14 @@ mod tests {
],
"usage": {
"input_tokens": 3,
"input_tokens_details": {
"cached_tokens": 2,
"cached_creation_tokens": 1
},
"output_tokens": 5,
"output_tokens_details": {
"reasoning_tokens": 1
},
"total_tokens": 8
}
}