codebuddy-stats 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +475 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/data-loader.js +205 -0
- package/dist/lib/data-loader.js.map +1 -0
- package/dist/lib/paths.js +42 -0
- package/dist/lib/paths.js.map +1 -0
- package/dist/lib/pricing.js +105 -0
- package/dist/lib/pricing.js.map +1 -0
- package/dist/lib/utils.js +58 -0
- package/dist/lib/utils.js.map +1 -0
- package/index.js +16 -0
- package/package.json +35 -0
- package/src/index.ts +549 -0
- package/src/lib/data-loader.ts +302 -0
- package/src/lib/paths.ts +45 -0
- package/src/lib/pricing.ts +128 -0
- package/src/lib/utils.ts +61 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import fsSync from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { createInterface } from 'node:readline'
|
|
5
|
+
|
|
6
|
+
import { getProjectsDir, getSettingsPath } from './paths.js'
|
|
7
|
+
import { DEFAULT_MODEL_ID, getPricingForModel, tokensToCost } from './pricing.js'
|
|
8
|
+
|
|
9
|
+
export const BASE_DIR = getProjectsDir()
|
|
10
|
+
|
|
11
|
+
export interface RawUsage {
|
|
12
|
+
prompt_tokens?: number
|
|
13
|
+
completion_tokens?: number
|
|
14
|
+
total_tokens?: number
|
|
15
|
+
prompt_cache_hit_tokens?: number
|
|
16
|
+
prompt_cache_miss_tokens?: number
|
|
17
|
+
cache_creation_input_tokens?: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface UsageStats {
|
|
21
|
+
promptTokens: number
|
|
22
|
+
completionTokens: number
|
|
23
|
+
totalTokens: number
|
|
24
|
+
cacheHitTokens: number
|
|
25
|
+
cacheMissTokens: number
|
|
26
|
+
cacheWriteTokens: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface DailyModelStats extends UsageStats {
|
|
30
|
+
cost: number
|
|
31
|
+
requests: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface SummaryStats {
|
|
35
|
+
cost: number
|
|
36
|
+
tokens: number
|
|
37
|
+
requests: number
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface GrandTotal extends SummaryStats {
|
|
41
|
+
cacheHitTokens: number
|
|
42
|
+
cacheMissTokens: number
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type DailyData = Record<string, Record<string, Record<string, DailyModelStats>>>
|
|
46
|
+
|
|
47
|
+
export interface AnalysisData {
|
|
48
|
+
defaultModelId: string
|
|
49
|
+
dailyData: DailyData
|
|
50
|
+
dailySummary: Record<string, SummaryStats>
|
|
51
|
+
modelTotals: Record<string, SummaryStats>
|
|
52
|
+
projectTotals: Record<string, SummaryStats>
|
|
53
|
+
grandTotal: GrandTotal
|
|
54
|
+
topModel: (SummaryStats & { id: string }) | null
|
|
55
|
+
topProject: (SummaryStats & { name: string }) | null
|
|
56
|
+
cacheHitRate: number
|
|
57
|
+
activeDays: number
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface LoadUsageOptions {
|
|
61
|
+
days?: number | null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface SettingsFile {
|
|
65
|
+
model?: unknown
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface JsonlRecord {
|
|
69
|
+
providerData?: {
|
|
70
|
+
rawUsage?: RawUsage
|
|
71
|
+
model?: unknown
|
|
72
|
+
}
|
|
73
|
+
timestamp?: unknown
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function loadModelFromSettings(): Promise<string> {
|
|
77
|
+
try {
|
|
78
|
+
const settingsPath = getSettingsPath()
|
|
79
|
+
const settingsRaw = await fs.readFile(settingsPath, 'utf8')
|
|
80
|
+
const settings = JSON.parse(settingsRaw) as SettingsFile
|
|
81
|
+
return typeof settings?.model === 'string' ? settings.model : DEFAULT_MODEL_ID
|
|
82
|
+
} catch {
|
|
83
|
+
return DEFAULT_MODEL_ID
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function findJsonlFiles(dir: string): Promise<string[]> {
|
|
88
|
+
try {
|
|
89
|
+
const dirents = await fs.readdir(dir, { withFileTypes: true })
|
|
90
|
+
const files = await Promise.all(
|
|
91
|
+
dirents.map(async dirent => {
|
|
92
|
+
const res = path.resolve(dir, dirent.name)
|
|
93
|
+
if (dirent.isDirectory()) {
|
|
94
|
+
return findJsonlFiles(res)
|
|
95
|
+
}
|
|
96
|
+
return res.endsWith('.jsonl') ? [res] : ([] as string[])
|
|
97
|
+
})
|
|
98
|
+
)
|
|
99
|
+
return files.flat()
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (typeof error === 'object' && error && 'code' in error && (error as any).code === 'ENOENT') {
|
|
102
|
+
return []
|
|
103
|
+
}
|
|
104
|
+
throw error
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function getProjectName(filePath: string): string {
|
|
109
|
+
const parts = filePath.split(path.sep)
|
|
110
|
+
const projectsIndex = parts.lastIndexOf('projects')
|
|
111
|
+
if (projectsIndex !== -1 && projectsIndex < parts.length - 1) {
|
|
112
|
+
return parts[projectsIndex + 1] ?? 'unknown-project'
|
|
113
|
+
}
|
|
114
|
+
return 'unknown-project'
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function extractUsageStats(usage: RawUsage): UsageStats {
|
|
118
|
+
const promptTokens = usage.prompt_tokens ?? 0
|
|
119
|
+
const completionTokens = usage.completion_tokens ?? 0
|
|
120
|
+
const totalTokens = usage.total_tokens ?? promptTokens + completionTokens
|
|
121
|
+
const cacheHitTokens = usage.prompt_cache_hit_tokens ?? 0
|
|
122
|
+
const cacheMissTokens =
|
|
123
|
+
usage.prompt_cache_miss_tokens ?? (cacheHitTokens > 0 ? Math.max(promptTokens - cacheHitTokens, 0) : 0)
|
|
124
|
+
const cacheWriteTokens = usage.cache_creation_input_tokens ?? 0
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
promptTokens,
|
|
128
|
+
completionTokens,
|
|
129
|
+
totalTokens,
|
|
130
|
+
cacheHitTokens,
|
|
131
|
+
cacheMissTokens,
|
|
132
|
+
cacheWriteTokens,
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function computeUsageCost(
|
|
137
|
+
usage: RawUsage,
|
|
138
|
+
modelId: string | null | undefined
|
|
139
|
+
): { cost: number; stats: UsageStats; modelId: string } {
|
|
140
|
+
const stats = extractUsageStats(usage)
|
|
141
|
+
const pricing = getPricingForModel(modelId)
|
|
142
|
+
|
|
143
|
+
let inputCost = 0
|
|
144
|
+
if (stats.cacheHitTokens || stats.cacheMissTokens || stats.cacheWriteTokens) {
|
|
145
|
+
inputCost += tokensToCost(stats.cacheHitTokens, pricing.cacheRead)
|
|
146
|
+
inputCost += tokensToCost(stats.cacheMissTokens, pricing.prompt)
|
|
147
|
+
inputCost += tokensToCost(stats.cacheWriteTokens, pricing.cacheWrite)
|
|
148
|
+
} else {
|
|
149
|
+
inputCost += tokensToCost(stats.promptTokens, pricing.prompt)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const outputCost = tokensToCost(stats.completionTokens, pricing.completion)
|
|
153
|
+
|
|
154
|
+
return { cost: inputCost + outputCost, stats, modelId: modelId || DEFAULT_MODEL_ID }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* 加载所有用量数据
|
|
159
|
+
*/
|
|
160
|
+
export async function loadUsageData(options: LoadUsageOptions = {}): Promise<AnalysisData> {
|
|
161
|
+
const defaultModelId = await loadModelFromSettings()
|
|
162
|
+
const jsonlFiles = await findJsonlFiles(BASE_DIR)
|
|
163
|
+
|
|
164
|
+
// 计算日期过滤范围
|
|
165
|
+
let minDate: string | null = null
|
|
166
|
+
if (options.days) {
|
|
167
|
+
const d = new Date()
|
|
168
|
+
d.setDate(d.getDate() - options.days + 1)
|
|
169
|
+
d.setHours(0, 0, 0, 0)
|
|
170
|
+
minDate = d.toISOString().split('T')[0] ?? null
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 按日期 -> 项目 -> 模型 组织的数据
|
|
174
|
+
const dailyData: DailyData = {}
|
|
175
|
+
|
|
176
|
+
// 按模型汇总
|
|
177
|
+
const modelTotals: Record<string, SummaryStats> = {}
|
|
178
|
+
|
|
179
|
+
// 按项目汇总
|
|
180
|
+
const projectTotals: Record<string, SummaryStats> = {}
|
|
181
|
+
|
|
182
|
+
// 总计
|
|
183
|
+
const grandTotal: GrandTotal = {
|
|
184
|
+
cost: 0,
|
|
185
|
+
tokens: 0,
|
|
186
|
+
requests: 0,
|
|
187
|
+
cacheHitTokens: 0,
|
|
188
|
+
cacheMissTokens: 0,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
for (const filePath of jsonlFiles) {
|
|
192
|
+
const fileStat = await fs.stat(filePath)
|
|
193
|
+
if (fileStat.size === 0) continue
|
|
194
|
+
|
|
195
|
+
const fileStream = fsSync.createReadStream(filePath)
|
|
196
|
+
const rl = createInterface({
|
|
197
|
+
input: fileStream,
|
|
198
|
+
crlfDelay: Number.POSITIVE_INFINITY,
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
const projectName = getProjectName(filePath)
|
|
202
|
+
|
|
203
|
+
for await (const line of rl) {
|
|
204
|
+
try {
|
|
205
|
+
const record = JSON.parse(line) as JsonlRecord
|
|
206
|
+
const usage = record?.providerData?.rawUsage
|
|
207
|
+
const timestamp = record?.timestamp
|
|
208
|
+
|
|
209
|
+
if (!usage || timestamp == null) continue
|
|
210
|
+
|
|
211
|
+
const dateObj = new Date(timestamp as any)
|
|
212
|
+
if (Number.isNaN(dateObj.getTime())) continue
|
|
213
|
+
const date = dateObj.toISOString().split('T')[0]
|
|
214
|
+
if (!date) continue
|
|
215
|
+
|
|
216
|
+
// 日期过滤
|
|
217
|
+
if (minDate && date < minDate) continue
|
|
218
|
+
|
|
219
|
+
const recordModelId = record?.providerData?.model
|
|
220
|
+
const modelId = typeof recordModelId === 'string' ? recordModelId : null
|
|
221
|
+
const { cost, stats: usageStats, modelId: usedModelId } = computeUsageCost(usage, modelId)
|
|
222
|
+
|
|
223
|
+
dailyData[date] ??= {}
|
|
224
|
+
dailyData[date]![projectName] ??= {}
|
|
225
|
+
dailyData[date]![projectName]![usedModelId] ??= {
|
|
226
|
+
cost: 0,
|
|
227
|
+
promptTokens: 0,
|
|
228
|
+
completionTokens: 0,
|
|
229
|
+
totalTokens: 0,
|
|
230
|
+
cacheHitTokens: 0,
|
|
231
|
+
cacheMissTokens: 0,
|
|
232
|
+
cacheWriteTokens: 0,
|
|
233
|
+
requests: 0,
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const dayStats = dailyData[date]![projectName]![usedModelId]!
|
|
237
|
+
dayStats.cost += cost
|
|
238
|
+
dayStats.promptTokens += usageStats.promptTokens
|
|
239
|
+
dayStats.completionTokens += usageStats.completionTokens
|
|
240
|
+
dayStats.totalTokens += usageStats.totalTokens
|
|
241
|
+
dayStats.cacheHitTokens += usageStats.cacheHitTokens
|
|
242
|
+
dayStats.cacheMissTokens += usageStats.cacheMissTokens
|
|
243
|
+
dayStats.cacheWriteTokens += usageStats.cacheWriteTokens
|
|
244
|
+
dayStats.requests += 1
|
|
245
|
+
|
|
246
|
+
modelTotals[usedModelId] ??= { cost: 0, tokens: 0, requests: 0 }
|
|
247
|
+
modelTotals[usedModelId]!.cost += cost
|
|
248
|
+
modelTotals[usedModelId]!.tokens += usageStats.totalTokens
|
|
249
|
+
modelTotals[usedModelId]!.requests += 1
|
|
250
|
+
|
|
251
|
+
projectTotals[projectName] ??= { cost: 0, tokens: 0, requests: 0 }
|
|
252
|
+
projectTotals[projectName]!.cost += cost
|
|
253
|
+
projectTotals[projectName]!.tokens += usageStats.totalTokens
|
|
254
|
+
projectTotals[projectName]!.requests += 1
|
|
255
|
+
|
|
256
|
+
grandTotal.cost += cost
|
|
257
|
+
grandTotal.tokens += usageStats.totalTokens
|
|
258
|
+
grandTotal.requests += 1
|
|
259
|
+
grandTotal.cacheHitTokens += usageStats.cacheHitTokens
|
|
260
|
+
grandTotal.cacheMissTokens += usageStats.cacheMissTokens
|
|
261
|
+
} catch {
|
|
262
|
+
// 忽略无法解析的行
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// 计算每日汇总
|
|
268
|
+
const dailySummary: Record<string, SummaryStats> = {}
|
|
269
|
+
for (const date of Object.keys(dailyData)) {
|
|
270
|
+
dailySummary[date] = { cost: 0, tokens: 0, requests: 0 }
|
|
271
|
+
for (const project of Object.values(dailyData[date] ?? {})) {
|
|
272
|
+
for (const model of Object.values(project ?? {})) {
|
|
273
|
+
dailySummary[date]!.cost += model.cost
|
|
274
|
+
dailySummary[date]!.tokens += model.totalTokens
|
|
275
|
+
dailySummary[date]!.requests += model.requests
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const topModelEntry = Object.entries(modelTotals).sort((a, b) => b[1].cost - a[1].cost)[0]
|
|
281
|
+
const topProjectEntry =
|
|
282
|
+
Object.entries(projectTotals).sort((a, b) => b[1].cost - a[1].cost)[0]
|
|
283
|
+
|
|
284
|
+
// 计算缓存命中率
|
|
285
|
+
const cacheHitRate =
|
|
286
|
+
grandTotal.cacheHitTokens + grandTotal.cacheMissTokens > 0
|
|
287
|
+
? grandTotal.cacheHitTokens / (grandTotal.cacheHitTokens + grandTotal.cacheMissTokens)
|
|
288
|
+
: 0
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
defaultModelId,
|
|
292
|
+
dailyData,
|
|
293
|
+
dailySummary,
|
|
294
|
+
modelTotals,
|
|
295
|
+
projectTotals,
|
|
296
|
+
grandTotal,
|
|
297
|
+
topModel: topModelEntry ? { id: topModelEntry[0], ...topModelEntry[1] } : null,
|
|
298
|
+
topProject: topProjectEntry ? { name: topProjectEntry[0], ...topProjectEntry[1] } : null,
|
|
299
|
+
cacheHitRate,
|
|
300
|
+
activeDays: Object.keys(dailyData).length,
|
|
301
|
+
}
|
|
302
|
+
}
|
package/src/lib/paths.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import os from 'node:os'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 获取 CodeBuddy 配置目录
|
|
6
|
+
* - Windows: %APPDATA%/CodeBuddy
|
|
7
|
+
* - macOS: ~/.codebuddy
|
|
8
|
+
* - Linux: $XDG_CONFIG_HOME/codebuddy 或 ~/.codebuddy
|
|
9
|
+
*/
|
|
10
|
+
export function getConfigDir(): string {
|
|
11
|
+
if (process.platform === 'win32') {
|
|
12
|
+
const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming')
|
|
13
|
+
return path.join(appData, 'CodeBuddy')
|
|
14
|
+
}
|
|
15
|
+
if (process.platform === 'linux') {
|
|
16
|
+
const xdgConfigHome = process.env.XDG_CONFIG_HOME
|
|
17
|
+
if (xdgConfigHome) {
|
|
18
|
+
return path.join(xdgConfigHome, 'codebuddy')
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
// macOS 和 Linux 默认
|
|
22
|
+
return path.join(os.homedir(), '.codebuddy')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 获取项目数据目录
|
|
27
|
+
*/
|
|
28
|
+
export function getProjectsDir(): string {
|
|
29
|
+
return path.join(getConfigDir(), 'projects')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 获取设置文件路径
|
|
34
|
+
*/
|
|
35
|
+
export function getSettingsPath(): string {
|
|
36
|
+
return path.join(getConfigDir(), 'settings.json')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 简化项目路径显示
|
|
41
|
+
* 保持原始名称不变
|
|
42
|
+
*/
|
|
43
|
+
export function shortenProjectName(name: string): string {
|
|
44
|
+
return name
|
|
45
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
export interface PricingTier {
|
|
2
|
+
limit: number
|
|
3
|
+
pricePerMTok: number
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface ModelPricing {
|
|
7
|
+
prompt: PricingTier[]
|
|
8
|
+
completion: PricingTier[]
|
|
9
|
+
cacheRead: PricingTier[]
|
|
10
|
+
cacheWrite: PricingTier[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// 模型价格 (USD / 1M tokens)
|
|
14
|
+
function createPricing(
|
|
15
|
+
inputPrice: number,
|
|
16
|
+
cachedInputPrice: number,
|
|
17
|
+
outputPrice: number,
|
|
18
|
+
cacheWritePrice?: number
|
|
19
|
+
): ModelPricing {
|
|
20
|
+
return {
|
|
21
|
+
prompt: [{ limit: Number.POSITIVE_INFINITY, pricePerMTok: inputPrice }],
|
|
22
|
+
completion: [{ limit: Number.POSITIVE_INFINITY, pricePerMTok: outputPrice }],
|
|
23
|
+
cacheRead: [{ limit: Number.POSITIVE_INFINITY, pricePerMTok: cachedInputPrice }],
|
|
24
|
+
cacheWrite: [
|
|
25
|
+
{ limit: Number.POSITIVE_INFINITY, pricePerMTok: cacheWritePrice ?? inputPrice },
|
|
26
|
+
],
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const MODEL_PRICING: Record<string, ModelPricing> = {
|
|
31
|
+
'gpt-5.2': createPricing(1.75, 0.175, 14.0),
|
|
32
|
+
'gpt-5.1': createPricing(1.25, 0.125, 10.0),
|
|
33
|
+
'gpt-5': createPricing(1.25, 0.125, 10.0),
|
|
34
|
+
'gpt-5-mini': createPricing(0.25, 0.025, 2.0),
|
|
35
|
+
'gpt-5-nano': createPricing(0.05, 0.005, 0.4),
|
|
36
|
+
'gpt-5.1-chat-latest': createPricing(1.25, 0.125, 10.0),
|
|
37
|
+
'gpt-5-chat-latest': createPricing(1.25, 0.125, 10.0),
|
|
38
|
+
'gpt-5.1-codex': createPricing(1.25, 0.125, 10.0),
|
|
39
|
+
'gpt-5-codex': createPricing(1.25, 0.125, 10.0),
|
|
40
|
+
|
|
41
|
+
'claude-opus-4.5': createPricing(5.0, 0.5, 25.0, 10.0),
|
|
42
|
+
|
|
43
|
+
'claude-4.5': {
|
|
44
|
+
prompt: [
|
|
45
|
+
{ limit: 200_000, pricePerMTok: 3.0 },
|
|
46
|
+
{ limit: Number.POSITIVE_INFINITY, pricePerMTok: 6.0 },
|
|
47
|
+
],
|
|
48
|
+
completion: [
|
|
49
|
+
{ limit: 200_000, pricePerMTok: 15.0 },
|
|
50
|
+
{ limit: Number.POSITIVE_INFINITY, pricePerMTok: 22.5 },
|
|
51
|
+
],
|
|
52
|
+
cacheRead: [
|
|
53
|
+
{ limit: 200_000, pricePerMTok: 0.3 },
|
|
54
|
+
{ limit: Number.POSITIVE_INFINITY, pricePerMTok: 0.6 },
|
|
55
|
+
],
|
|
56
|
+
cacheWrite: [
|
|
57
|
+
{ limit: 200_000, pricePerMTok: 6.0 },
|
|
58
|
+
{ limit: Number.POSITIVE_INFINITY, pricePerMTok: 12.0 },
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
'gemini-3-pro': {
|
|
63
|
+
prompt: [
|
|
64
|
+
{ limit: 200_000, pricePerMTok: 2.0 },
|
|
65
|
+
{ limit: Number.POSITIVE_INFINITY, pricePerMTok: 4.0 },
|
|
66
|
+
],
|
|
67
|
+
completion: [
|
|
68
|
+
{ limit: 200_000, pricePerMTok: 12.0 },
|
|
69
|
+
{ limit: Number.POSITIVE_INFINITY, pricePerMTok: 18.0 },
|
|
70
|
+
],
|
|
71
|
+
cacheRead: [
|
|
72
|
+
{ limit: 200_000, pricePerMTok: 0.2 },
|
|
73
|
+
{ limit: Number.POSITIVE_INFINITY, pricePerMTok: 0.4 },
|
|
74
|
+
],
|
|
75
|
+
cacheWrite: [
|
|
76
|
+
{ limit: 200_000, pricePerMTok: 0.2 },
|
|
77
|
+
{ limit: Number.POSITIVE_INFINITY, pricePerMTok: 0.4 },
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
'gemini-2.5-pro': {
|
|
82
|
+
prompt: [
|
|
83
|
+
{ limit: 200_000, pricePerMTok: 1.25 },
|
|
84
|
+
{ limit: Number.POSITIVE_INFINITY, pricePerMTok: 2.5 },
|
|
85
|
+
],
|
|
86
|
+
completion: [
|
|
87
|
+
{ limit: 200_000, pricePerMTok: 10.0 },
|
|
88
|
+
{ limit: Number.POSITIVE_INFINITY, pricePerMTok: 15.0 },
|
|
89
|
+
],
|
|
90
|
+
cacheRead: [
|
|
91
|
+
{ limit: 200_000, pricePerMTok: 0.125 },
|
|
92
|
+
{ limit: Number.POSITIVE_INFINITY, pricePerMTok: 0.25 },
|
|
93
|
+
],
|
|
94
|
+
cacheWrite: [
|
|
95
|
+
{ limit: 200_000, pricePerMTok: 0.125 },
|
|
96
|
+
{ limit: Number.POSITIVE_INFINITY, pricePerMTok: 0.25 },
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const DEFAULT_MODEL_ID = 'gpt-5.1' as const
|
|
102
|
+
|
|
103
|
+
export function selectTierPrice(tokens: number, tiers: PricingTier[]): number {
|
|
104
|
+
if (tokens <= 0) return tiers[0]?.pricePerMTok ?? 0
|
|
105
|
+
for (const tier of tiers) {
|
|
106
|
+
if (tokens <= tier.limit) {
|
|
107
|
+
return tier.pricePerMTok
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return tiers[tiers.length - 1]?.pricePerMTok ?? 0
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function tokensToCost(tokens: number, tiers: PricingTier[]): number {
|
|
114
|
+
if (!tokens) return 0
|
|
115
|
+
const price = selectTierPrice(tokens, tiers)
|
|
116
|
+
return (tokens / 1_000_000) * price
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function getPricingForModel(modelId: string | null | undefined): ModelPricing {
|
|
120
|
+
if (modelId && MODEL_PRICING[modelId]) {
|
|
121
|
+
return MODEL_PRICING[modelId]
|
|
122
|
+
}
|
|
123
|
+
const fallback = MODEL_PRICING[DEFAULT_MODEL_ID]
|
|
124
|
+
if (!fallback) {
|
|
125
|
+
throw new Error(`Missing pricing for default model: ${DEFAULT_MODEL_ID}`)
|
|
126
|
+
}
|
|
127
|
+
return fallback
|
|
128
|
+
}
|
package/src/lib/utils.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 格式化数字,添加千分位分隔符
|
|
3
|
+
*/
|
|
4
|
+
export function formatNumber(num: number | string | null | undefined): string {
|
|
5
|
+
const n = typeof num === 'number' ? num : Number(num ?? 0)
|
|
6
|
+
if (!Number.isFinite(n)) return '0'
|
|
7
|
+
return n.toLocaleString('en-US')
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 格式化金额
|
|
12
|
+
*/
|
|
13
|
+
export function formatCost(cost: number): string {
|
|
14
|
+
const n = Number.isFinite(cost) ? cost : 0
|
|
15
|
+
return `$${n.toFixed(2)}`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 格式化 tokens 数量 (K/M/B)
|
|
20
|
+
*/
|
|
21
|
+
export function formatTokens(tokens: number): string {
|
|
22
|
+
if (tokens >= 1_000_000_000) {
|
|
23
|
+
return `${(tokens / 1_000_000_000).toFixed(1)}B`
|
|
24
|
+
}
|
|
25
|
+
if (tokens >= 1_000_000) {
|
|
26
|
+
return `${(tokens / 1_000_000).toFixed(1)}M`
|
|
27
|
+
}
|
|
28
|
+
if (tokens >= 1_000) {
|
|
29
|
+
return `${(tokens / 1_000).toFixed(1)}K`
|
|
30
|
+
}
|
|
31
|
+
return String(tokens)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 格式化百分比
|
|
36
|
+
*/
|
|
37
|
+
export function formatPercent(ratio: number): string {
|
|
38
|
+
return `${(ratio * 100).toFixed(1)}%`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 截断字符串
|
|
43
|
+
*/
|
|
44
|
+
export function truncate(str: string, maxLen: number): string {
|
|
45
|
+
if (str.length <= maxLen) return str
|
|
46
|
+
return str.slice(0, maxLen - 3) + '...'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 右对齐字符串
|
|
51
|
+
*/
|
|
52
|
+
export function padLeft(str: string, len: number): string {
|
|
53
|
+
return str.padStart(len)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 左对齐字符串
|
|
58
|
+
*/
|
|
59
|
+
export function padRight(str: string, len: number): string {
|
|
60
|
+
return str.padEnd(len)
|
|
61
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/tsconfig",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"lib": ["ES2022"],
|
|
6
|
+
|
|
7
|
+
"module": "NodeNext",
|
|
8
|
+
"moduleResolution": "NodeNext",
|
|
9
|
+
"verbatimModuleSyntax": true,
|
|
10
|
+
|
|
11
|
+
"rootDir": "./src",
|
|
12
|
+
"outDir": "./dist",
|
|
13
|
+
|
|
14
|
+
"strict": true,
|
|
15
|
+
"noUncheckedIndexedAccess": true,
|
|
16
|
+
|
|
17
|
+
"esModuleInterop": true,
|
|
18
|
+
"resolveJsonModule": true,
|
|
19
|
+
"skipLibCheck": true,
|
|
20
|
+
|
|
21
|
+
"sourceMap": true
|
|
22
|
+
},
|
|
23
|
+
"include": ["src/**/*"],
|
|
24
|
+
"exclude": ["node_modules", "dist"]
|
|
25
|
+
}
|