feat(gateway): 重构 usage 数据层、迁移系统与系统导入

数据库迁移:
- 引入 baseline v2 bootstrap,空库首次启动自动初始化
- 服务启动不再自动执行迁移,需显式 `--migrate` 运行
- 新增 pending migration 检测,schema 落后时拒绝启动

Usage 数据层:
- usage body 存储外部化为独立 blob 表
- 新增 HTTP audit 表拆分存储请求/响应头与 body ref
- 后台清理任务支持 legacy body ref 元数据迁移
- usage runtime 写入迁移到专用 tokio runtime(独立线程池, 8MB 栈)

系统导入/导出:
- 支持用户、API Keys、钱包数据的完整导入
- 兼容 legacy 与 v1.3+ 两种导出格式

其他改进:
- executor outcome 增加 runtime miss 诊断上下文
- 主 tokio runtime 栈大小调整为 8MB
- 前端 provider 管理支持 base URL 配置
- dev.sh 支持 --migrate 参数
This commit is contained in:
fawney19
2026-04-13 14:01:22 +08:00
parent 3698e5a833
commit 5bb08e6aa4
106 changed files with 21736 additions and 1529 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -9,3 +9,92 @@ pub(crate) use aether_data_contracts::repository::usage::{
};
pub use memory::InMemoryUsageReadRepository;
pub use sql::SqlxUsageReadRepository;
pub(crate) fn strip_deprecated_usage_display_fields(
mut usage: UpsertUsageRecord,
) -> UpsertUsageRecord {
usage.username = None;
usage.api_key_name = None;
usage
}
#[cfg(test)]
mod tests {
use super::{strip_deprecated_usage_display_fields, UpsertUsageRecord};
#[test]
fn strip_deprecated_usage_display_fields_clears_legacy_display_columns() {
let usage = strip_deprecated_usage_display_fields(UpsertUsageRecord {
request_id: "req-1".to_string(),
user_id: Some("user-1".to_string()),
api_key_id: Some("key-1".to_string()),
username: Some("alice".to_string()),
api_key_name: Some("default".to_string()),
provider_name: "OpenAI".to_string(),
model: "gpt-5".to_string(),
target_model: None,
provider_id: None,
provider_endpoint_id: None,
provider_api_key_id: None,
request_type: Some("chat".to_string()),
api_format: Some("openai:chat".to_string()),
api_family: Some("openai".to_string()),
endpoint_kind: Some("chat".to_string()),
endpoint_api_format: Some("openai:chat".to_string()),
provider_api_family: Some("openai".to_string()),
provider_endpoint_kind: Some("chat".to_string()),
has_format_conversion: Some(false),
is_stream: Some(false),
input_tokens: Some(10),
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,
output_price_per_1m: None,
total_cost_usd: Some(0.25),
actual_total_cost_usd: Some(0.15),
status_code: Some(200),
error_message: None,
error_category: None,
response_time_ms: Some(120),
first_byte_time_ms: Some(40),
status: "completed".to_string(),
billing_status: "pending".to_string(),
request_headers: None,
request_body: None,
request_body_ref: None,
provider_request_headers: None,
provider_request_body: None,
provider_request_body_ref: None,
response_headers: None,
response_body: None,
response_body_ref: None,
client_response_headers: None,
client_response_body: None,
client_response_body_ref: None,
candidate_id: None,
candidate_index: None,
key_name: None,
planner_kind: None,
route_family: None,
route_kind: None,
execution_path: None,
local_execution_runtime_miss_reason: None,
request_metadata: None,
finalized_at_unix_secs: None,
created_at_unix_ms: Some(100),
updated_at_unix_secs: 101,
});
assert_eq!(usage.user_id.as_deref(), Some("user-1"));
assert_eq!(usage.api_key_id.as_deref(), Some("key-1"));
assert_eq!(usage.username, None);
assert_eq!(usage.api_key_name, None);
assert_eq!(usage.provider_name, "OpenAI");
assert_eq!(usage.model, "gpt-5");
}
}

File diff suppressed because it is too large Load Diff