Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .gemini-clipboard/clipboard-1769308510109.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .gemini-clipboard/clipboard-1769308817019.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,5 @@ DEPLOYMENT*.md
mirror-server/
/nul
/.claude/
/src-tauri/NUL
/docs
31 changes: 31 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,37 @@ last-updated: 2025-12-16
- **GlobalConfig 清理**:删除 6 个废弃字段(`transparent_proxy_enabled`、`transparent_proxy_port`、`transparent_proxy_api_key`、`transparent_proxy_allow_public`、`transparent_proxy_real_api_key`、`transparent_proxy_real_base_url`)
- **迁移逻辑**:`migrations/proxy_config.rs` 使用 `serde_json::Value` 手动操作 JSON,保持向后兼容
- **代码质量**:遵循 DRY 原则,所有检查通过(Clippy + fmt + ESLint + Prettier),测试 199 通过
- **Codex 会话管理与计费系统(2026-01-18)**:
- **会话 ID 提取**:从请求体的 `prompt_cache_key` 字段提取(区别于 Claude 的 `metadata.user_id`)
- **SSE 事件结构**:
- `"type": "response.created"` → 提取 `response.id`(消息 ID)
- `"type": "response.completed"` → 提取完整 `response.usage`(所有 token 统计)
- **Token 字段映射**:
- `input_tokens` → input_tokens
- `input_tokens_details.cached_tokens` → cache_read_tokens
- `output_tokens` → output_tokens
- `output_tokens_details.reasoning_tokens` → 记录日志(暂不计费)
- `cache_creation_tokens` → 0(Codex 不报告缓存创建)
- **Tool Processor Pattern 架构(2026-01-18 重构)**:
- **核心理念**:每个工具独立实现 Token 提取逻辑,互不影响
- **三层架构**:
- `ToolProcessor` trait(位于 `services/token_stats/processor/mod.rs`):定义提取接口,输出统一的 `TokenInfo`
- `TokenLogger` trait(位于 `services/token_stats/logger/mod.rs`):封装 Processor + 成本计算,输出完整的 `TokenLog`
- `TokenStatsManager`(简化版):仅负责批量写入数据库,单一职责
- **工具实现**:
- Claude: `ClaudeProcessor` + `ClaudeLogger`(支持 message_start/message_delta 事件,嵌套 cache_creation 对象)
- Codex: `CodexProcessor` + `CodexLogger`(支持 response.created/response.completed 事件,平铺 usage 结构)
- **扩展性**:添加新工具仅需实现两个 trait,工厂函数自动注册
- **优势**:工具逻辑完全隔离,Claude 和 Codex 互不影响,维护性和可测试性显著提高
- **会话模型增强**:
- `ProxySession::extract_display_id()` 支持多种格式:
- Claude 格式:`user_xxx_session_<uuid>` → 提取 UUID
- Codex 格式:`prompt_cache_key` → 使用前 12 字符
- `RequestLogContext` 根据 tool_id 自动选择提取逻辑
- **代码质量**:
- 新增 38 个测试(9 个 Processor 测试 + 10 个 Logger 测试 + 15 个数据库测试 + 4 个命令测试)
- 代码量减少:manager.rs 从 626 行减少到 286 行(-54%)
- 所有测试通过,编译 0 警告
- **配置管理机制(2025-12-12)**:
- 代理启动时自动创建内置 Profile(`dc_proxy_*`),通过 `ProfileManager` 切换配置
- 内置 Profile 在 UI 中不可见(列表查询时过滤 `dc_proxy_` 前缀)
Expand Down
14 changes: 9 additions & 5 deletions src-tauri/src/commands/analytics_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ mod tests {

for i in 0..10 {
let log = TokenLog::new(
"claude_code".to_string(),
"claude-code".to_string(),
base_time - (i * 3600 * 1000), // 每小时一条
"127.0.0.1".to_string(),
"test_session".to_string(),
Expand All @@ -274,6 +274,7 @@ mod tests {
50,
10,
20,
0, // reasoning_tokens
"success".to_string(),
"json".to_string(),
None,
Expand All @@ -283,6 +284,7 @@ mod tests {
Some(0.002),
Some(0.0001),
Some(0.0002),
None, // reasoning_price
0.0033,
Some("test_template".to_string()),
);
Expand All @@ -291,7 +293,7 @@ mod tests {

// 创建查询
let query = TrendQuery {
tool_type: Some("claude_code".to_string()),
tool_type: Some("claude-code".to_string()),
granularity: TimeGranularity::Hour,
..Default::default()
};
Expand Down Expand Up @@ -327,7 +329,7 @@ mod tests {
for (j, config) in configs.iter().enumerate() {
for k in 0..3 {
let log = TokenLog::new(
"claude_code".to_string(),
"claude-code".to_string(),
base_time - (k * 1000),
"127.0.0.1".to_string(),
format!("session_{}_{}", i, j),
Expand All @@ -338,6 +340,7 @@ mod tests {
50,
10,
20,
0, // reasoning_tokens
"success".to_string(),
"json".to_string(),
None,
Expand All @@ -347,6 +350,7 @@ mod tests {
Some(0.002),
Some(0.0001),
Some(0.0002),
None, // reasoning_price
0.0033,
Some("test_template".to_string()),
);
Expand All @@ -360,7 +364,7 @@ mod tests {

// 按模型分组
let model_query = CostSummaryQuery {
tool_type: Some("claude_code".to_string()),
tool_type: Some("claude-code".to_string()),
group_by: CostGroupBy::Model,
..Default::default()
};
Expand All @@ -375,7 +379,7 @@ mod tests {

// 按配置分组
let config_query = CostSummaryQuery {
tool_type: Some("claude_code".to_string()),
tool_type: Some("claude-code".to_string()),
group_by: CostGroupBy::Config,
..Default::default()
};
Expand Down
36 changes: 31 additions & 5 deletions src-tauri/src/models/pricing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ pub struct ModelPrice {
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_read_price_per_1m: Option<f64>,

/// 推理输出价格(USD/百万 Token,可选,如 OpenAI o1 系列)
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning_output_price_per_1m: Option<f64>,

/// 货币类型(默认:USD)
#[serde(default = "default_currency")]
pub currency: String,
Expand All @@ -39,6 +43,7 @@ impl ModelPrice {
output_price_per_1m: f64,
cache_write_price_per_1m: Option<f64>,
cache_read_price_per_1m: Option<f64>,
reasoning_output_price_per_1m: Option<f64>,
aliases: Vec<String>,
) -> Self {
Self {
Expand All @@ -47,6 +52,7 @@ impl ModelPrice {
output_price_per_1m,
cache_write_price_per_1m,
cache_read_price_per_1m,
reasoning_output_price_per_1m,
currency: default_currency(),
aliases,
}
Expand Down Expand Up @@ -168,28 +174,47 @@ impl PricingTemplate {
/// 工具默认模板配置(存储在 default_templates.json)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DefaultTemplatesConfig {
/// 配置版本号(用于自动迁移)
#[serde(default = "default_config_version")]
pub version: u32,

/// 工具 -> 默认模板 ID 的映射
///
/// 例如:
/// ```json
/// {
/// "claude-code": "claude_official_2025_01",
/// "codex": "claude_official_2025_01",
/// "gemini-cli": "claude_official_2025_01"
/// "version": 2,
/// "claude-code": "builtin_claude",
/// "codex": "builtin_openai",
/// "gemini-cli": "builtin_claude"
/// }
/// ```
#[serde(flatten)]
pub tool_defaults: HashMap<String, String>,
}

/// 当前配置版本号
const CURRENT_CONFIG_VERSION: u32 = 2;

/// 默认配置版本号
fn default_config_version() -> u32 {
1 // 旧配置默认为版本 1
}

impl DefaultTemplatesConfig {
/// 创建新的默认模板配置
/// 创建新的默认模板配置(使用最新版本)
pub fn new() -> Self {
Self {
version: CURRENT_CONFIG_VERSION,
tool_defaults: HashMap::new(),
}
}

/// 获取当前配置版本号
pub fn current_version() -> u32 {
CURRENT_CONFIG_VERSION
}

/// 获取工具的默认模板 ID
pub fn get_default(&self, tool_id: &str) -> Option<&String> {
self.tool_defaults.get(tool_id)
Expand Down Expand Up @@ -224,6 +249,7 @@ mod tests {
15.0,
Some(3.75),
Some(0.3),
None, // No reasoning price
vec![
"claude-sonnet-4.5".to_string(),
"claude-sonnet-4-5".to_string(),
Expand Down Expand Up @@ -258,7 +284,7 @@ mod tests {
let mut custom_models = HashMap::new();
custom_models.insert(
"model1".to_string(),
ModelPrice::new("provider1".to_string(), 1.0, 2.0, None, None, vec![]),
ModelPrice::new("provider1".to_string(), 1.0, 2.0, None, None, None, vec![]),
);

let full_custom = PricingTemplate::new(
Expand Down
21 changes: 21 additions & 0 deletions src-tauri/src/models/token_stats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ pub struct TokenLog {
/// 缓存读取Token数量
pub cache_read_tokens: i64,

/// 推理Token数量(如 OpenAI o1 系列)
#[serde(default)]
pub reasoning_tokens: i64,

/// 请求状态:success, failed
pub request_status: String,

Expand Down Expand Up @@ -79,6 +83,11 @@ pub struct TokenLog {
#[serde(with = "crate::utils::precision::option_price_precision")]
pub cache_read_price: Option<f64>,

/// 推理Token部分价格(USD)
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(with = "crate::utils::precision::option_price_precision")]
pub reasoning_price: Option<f64>,

/// 总成本(USD)
#[serde(default)]
#[serde(with = "crate::utils::precision::price_precision")]
Expand All @@ -104,6 +113,7 @@ impl TokenLog {
output_tokens: i64,
cache_creation_tokens: i64,
cache_read_tokens: i64,
reasoning_tokens: i64,
request_status: String,
response_type: String,
error_type: Option<String>,
Expand All @@ -113,6 +123,7 @@ impl TokenLog {
output_price: Option<f64>,
cache_write_price: Option<f64>,
cache_read_price: Option<f64>,
reasoning_price: Option<f64>,
total_cost: f64,
pricing_template_id: Option<String>,
) -> Self {
Expand All @@ -129,6 +140,7 @@ impl TokenLog {
output_tokens,
cache_creation_tokens,
cache_read_tokens,
reasoning_tokens,
request_status,
response_type,
error_type,
Expand All @@ -138,6 +150,7 @@ impl TokenLog {
output_price,
cache_write_price,
cache_read_price,
reasoning_price,
total_cost,
pricing_template_id,
}
Expand Down Expand Up @@ -174,6 +187,10 @@ pub struct SessionStats {
/// 总缓存读取Token数量
pub total_cache_read: i64,

/// 总推理Token数量
#[serde(default)]
pub total_reasoning: i64,

/// 请求总数
pub request_count: i64,
}
Expand All @@ -186,6 +203,7 @@ impl SessionStats {
total_output: 0,
total_cache_creation: 0,
total_cache_read: 0,
total_reasoning: 0,
request_count: 0,
}
}
Expand Down Expand Up @@ -274,6 +292,7 @@ mod tests {
500,
100,
200,
0, // reasoning_tokens
"success".to_string(),
"sse".to_string(),
None,
Expand All @@ -283,6 +302,7 @@ mod tests {
Some(0.0075),
Some(0.000375),
Some(0.00006),
None, // reasoning_price
0.011235,
Some("builtin_claude".to_string()),
);
Expand All @@ -303,6 +323,7 @@ mod tests {
total_output: 5000,
total_cache_creation: 1000,
total_cache_read: 2000,
total_reasoning: 0, // 新增字段
request_count: 10,
};

Expand Down
2 changes: 2 additions & 0 deletions src-tauri/src/services/migration_manager/migrations/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

mod balance_localstorage_to_json;
mod global_to_providers;
mod pricing_default_templates;
mod profile_v2;
mod proxy_config;
mod proxy_config_split;
Expand All @@ -12,6 +13,7 @@ mod sqlite_to_json;

pub use balance_localstorage_to_json::BalanceLocalstorageToJsonMigration;
pub use global_to_providers::GlobalConfigToProvidersMigration;
pub use pricing_default_templates::PricingDefaultTemplatesMigration;
pub use profile_v2::ProfileV2Migration;
pub use proxy_config::ProxyConfigMigration;
pub use proxy_config_split::ProxyConfigSplitMigration;
Expand Down
Loading
Loading