Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -47,21 +47,24 @@ struct DailyUsage: Codable, Identifiable {
var date: String
var tools: [String: Int]
var models: [String: Int]
var toolModels: [String: [String: Int]]?
var totalTokens: Int
var cost: Double

enum CodingKeys: String, CodingKey {
case date
case tools
case models
case toolModels = "tool_models"
case totalTokens = "total_tokens"
case cost
}

init(date: String, tools: [String: Int], models: [String: Int] = [:], totalTokens: Int, cost: Double) {
init(date: String, tools: [String: Int], models: [String: Int] = [:], toolModels: [String: [String: Int]]? = nil, totalTokens: Int, cost: Double) {
self.date = date
self.tools = tools
self.models = models
self.toolModels = toolModels
self.totalTokens = totalTokens
self.cost = cost
}
Expand All @@ -71,6 +74,7 @@ struct DailyUsage: Codable, Identifiable {
date = try container.decode(String.self, forKey: .date)
tools = try container.decodeIfPresent([String: Int].self, forKey: .tools) ?? [:]
models = try container.decodeIfPresent([String: Int].self, forKey: .models) ?? [:]
toolModels = try container.decodeIfPresent([String: [String: Int]].self, forKey: .toolModels)
totalTokens = try container.decode(Int.self, forKey: .totalTokens)
cost = try container.decode(Double.self, forKey: .cost)
}
Expand Down
499 changes: 494 additions & 5 deletions TokenStepSwift/Sources/TokenStepSwift/Services/UsageCollector.swift

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,10 @@ enum TokenStepLocalization {
"每日 Token 消耗追踪": "Daily Token Tracking",
"Codex 剩余额度": "Codex Quota",
"Agent 剩余额度": "Agent Quota",
"今日模型用量": "Today's Model Usage",
"模型用量": "Model Usage",
"暂无模型用量数据": "No model usage data",
"使用支持的 Agent 后这里会显示模型分布。": "Model distribution will appear after using supported agents.",
"Codex 额度显示": "Show Codex Quota",
"Agent 额度显示": "Show Agent Quota",
"生财 Token 榜单": "Shengcai Token Rank",
Expand Down Expand Up @@ -475,6 +479,10 @@ enum TokenStepLocalization {
"每日 Token 消耗追踪": "每日 Token 消耗追蹤",
"Codex 剩余额度": "Codex 剩餘額度",
"Agent 剩余额度": "Agent 剩餘額度",
"今日模型用量": "今日模型用量",
"模型用量": "模型用量",
"暂无模型用量数据": "暫無模型用量資料",
"使用支持的 Agent 后这里会显示模型分布。": "使用支援的 Agent 後這裡會顯示模型分佈。",
"Codex 额度显示": "顯示 Codex 額度",
"Agent 额度显示": "顯示 Agent 額度",
"生财 Token 榜单": "生財 Token 榜單",
Expand Down
12 changes: 11 additions & 1 deletion TokenStepSwift/Sources/TokenStepSwift/Views/Components.swift
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,16 @@ func tokenToolColor(_ tool: String) -> Color {
return .tokenGreen
case "Claude Code":
return Color(red: 0.88, green: 0.42, blue: 0.24)
case "Kimi Code":
return Color(red: 0.49, green: 0.23, blue: 0.93)
case "Kimi Code CLI":
return Color(red: 0.60, green: 0.35, blue: 0.95)
case "ZCode":
return Color(red: 0.06, green: 0.73, blue: 0.51)
case "Pi":
return Color(red: 0.96, green: 0.62, blue: 0.04)
case "Reasonix":
return Color(red: 0.93, green: 0.29, blue: 0.60)
case "Hermes", "Hermes Agent":
return Color(red: 0.50, green: 0.28, blue: 0.92)
default:
Expand All @@ -635,7 +645,7 @@ func tokenToolColor(_ tool: String) -> Color {
}

func orderedToolEntries(_ tools: [String: Int]) -> [(name: String, tokens: Int)] {
let preferred = ["Codex", "Claude Code", "Hermes", "Hermes Agent"]
let preferred = ["Codex", "Claude Code", "Kimi Code", "Kimi Code CLI", "ZCode", "Pi", "Reasonix", "Hermes", "Hermes Agent"]
var entries: [(name: String, tokens: Int)] = preferred.compactMap { name in
guard let value = tools[name], value > 0 else { return nil }
return (name, value)
Expand Down
22 changes: 19 additions & 3 deletions TokenStepSwift/Sources/TokenStepSwift/Views/StatsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,29 @@ struct StatsView: View {
usageList(title: L("按客户端"), subtitle: L("累计总量分布"), rows: appState.snapshot.tools.map {
UsageStatRow(name: $0.tool, value: $0.tokens, percent: $0.percentValue, color: $0.displayColor)
})
usageList(title: L("按模型"), subtitle: "Top \(min(appState.snapshot.models.count, 10)) / \(appState.snapshot.models.count)", rows: appState.snapshot.models.prefix(10).map {
UsageStatRow(name: $0.model, value: $0.tokens, percent: $0.percentValue, color: $0.displayColor)
})
usageList(title: L("按模型"), subtitle: "Top \(min(modelRows.count, 10)) / \(modelRows.count)", rows: modelRows)
}
}
}

private var modelRows: [UsageStatRow] {
let merged = appState.snapshot.models.reduce(into: [:]) { counts, usage in
counts[usage.model, default: 0] += usage.tokens
}
let total = max(appState.snapshot.totals.tokens, 1)
return merged
.sorted { $0.value > $1.value }
.prefix(10)
.map { name, tokens in
UsageStatRow(
name: name,
value: tokens,
percent: Double(tokens) * 100.0 / Double(total),
color: .tokenGreen
)
}
}

private var recentActivityCard: some View {
TokenCard {
VStack(alignment: .leading, spacing: 18) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ struct CCSwitchProxyFixtureCheck {
try assertEqual(snapshot.daily.first?.models["claude-priced"], 155, "priced model tokens")
try assertNil(snapshot.daily.first?.models["claude-session-priced"], "session model tokens")
try assertNil(snapshot.daily.first?.models["codex-session-priced"], "codex session model tokens")
try assertEqual(snapshot.daily.first?.models["gpt-5.4"], 13, "model fallback tokens")
try assertEqual(snapshot.daily.first?.models["GPT-5.4"], 13, "model fallback tokens")

let emptyDatabase = try makeFixtureDatabase(rowsSQL: """
insert into proxy_request_logs (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ final class UsageCollectorCCSwitchTests: XCTestCase {
XCTAssertEqual(snapshot.daily.first?.models["claude-priced"], 155)
XCTAssertNil(snapshot.daily.first?.models["claude-session-priced"])
XCTAssertNil(snapshot.daily.first?.models["codex-session-priced"])
XCTAssertEqual(snapshot.daily.first?.models["gpt-5.4"], 13)
XCTAssertEqual(snapshot.daily.first?.models["GPT-5.4"], 13)

let tools = Dictionary(uniqueKeysWithValues: snapshot.tools.map { ($0.tool, $0.tokens) })
XCTAssertEqual(tools["Claude Code via CC Switch"], 155)
Expand All @@ -42,7 +42,7 @@ final class UsageCollectorCCSwitchTests: XCTestCase {
XCTAssertEqual(models["Claude Code via CC Switch|claude-priced"], 155)
XCTAssertNil(models["Claude Code via CC Switch|claude-session-priced"])
XCTAssertNil(models["Codex via CC Switch|codex-session-priced"])
XCTAssertEqual(models["Codex via CC Switch|gpt-5.4"], 13)
XCTAssertEqual(models["Codex via CC Switch|GPT-5.4"], 13)
}

func testValidDatabaseWithoutSuccessfulTokenRowsReportsMissingValidRows() throws {
Expand Down
24 changes: 24 additions & 0 deletions config/pricing.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@
},
"Claude Code": {
"total_usd_per_1m": 3.0
},
"Kimi Code": {
"total_usd_per_1m": 0.8
},
"ZCode": {
"total_usd_per_1m": 0.5
},
"Pi": {
"total_usd_per_1m": 0.5
},
"Reasonix": {
"total_usd_per_1m": 0.5
}
},
"models": {
Expand Down Expand Up @@ -39,6 +51,18 @@
},
"minimax": {
"total_usd_per_1m": 0.2
},
"GLM-5.2": {
"total_usd_per_1m": 0.5
},
"deepseek-v4-pro": {
"total_usd_per_1m": 0.5
},
"deepseek-v4-flash": {
"total_usd_per_1m": 0.1
},
"daimon-kimi-code": {
"total_usd_per_1m": 0.8
}
}
}
4 changes: 4 additions & 0 deletions docs/AGENT_SUPPORT.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ TokenStep 的原则是:能从本地日志中稳定读到 token 数,才进入
| --- | --- | --- | --- |
| Codex | 已支持 | `~/.codex/sessions` / `~/.codex/archived_sessions`,必要时回退 SQLite | 读取本地 token_count 事件,只统计数量;可选读取 5h / 7d 额度。 |
| Claude Code | 已支持 | `~/.claude/projects` | 读取 assistant message 的 usage 字段,按 `message.id` 去重,避免 thinking / text / tool_use 多行重复累计;可选通过 Claude Code 本机钥匙串凭证读取 usage 额度。 |
| Kimi Code | 已支持 | `~/Library/Application Support/kimi-desktop/daimon-share/daimon/runtime/kimi-code/home/sessions/**/agents/main/wire.jsonl` | 读取 `usage.record` 事件,字段映射为 `inputOther`/`output`/`inputCacheRead`/`inputCacheCreation`。 |
| ZCode / GLM | 已支持 | `~/.zcode/cli/db/db.sqlite` → `model_usage` | 只读 SQLite `model_usage` 表的 token 数值列,不读 `message`/`part` 正文。 |
| Pi / SPI | 已支持 | `~/.pi/agent/sessions/**/*.jsonl` | 读取 `message` 事件中 assistant 角色的 `usage` 字段。 |
| Reasonix / DeepSeek | 已支持 | `~/.reasonix/sessions/*.events.jsonl` / `~/.reasonix/projects/**/*.events.jsonl` | 读取 `model.final` 事件的 `usage` 字段,并与 `model.turn.started` 关联模型名。 |

## 实验支持:CC Switch Proxy

Expand Down
Loading