lumencode 0.4.3
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/README.md +271 -0
- package/data/pricing.json +3708 -0
- package/index.js +266 -0
- package/lib/aggregate.js +626 -0
- package/lib/attribution.js +137 -0
- package/lib/blocks.js +86 -0
- package/lib/cache.js +18 -0
- package/lib/config.js +91 -0
- package/lib/git.js +1106 -0
- package/lib/models/usage-record.js +46 -0
- package/lib/parser.js +160 -0
- package/lib/parsers/base.js +67 -0
- package/lib/parsers/claude.js +316 -0
- package/lib/parsers/codex.js +316 -0
- package/lib/parsers/index.js +151 -0
- package/lib/parsers/opencode.js +216 -0
- package/lib/pricing-loader.js +287 -0
- package/lib/record-utils.js +35 -0
- package/lib/report.js +1446 -0
- package/lib/scenario.js +183 -0
- package/lib/server.js +412 -0
- package/lib/table.js +67 -0
- package/package.json +44 -0
- package/public/api.js +109 -0
- package/public/app.js +647 -0
- package/public/charts.js +197 -0
- package/public/config.js +141 -0
- package/public/export.js +282 -0
- package/public/fonts/inter-0.woff2 +0 -0
- package/public/fonts/inter-1.woff2 +0 -0
- package/public/fonts/inter-2.woff2 +0 -0
- package/public/fonts/inter-3.woff2 +0 -0
- package/public/fonts/inter.css +28 -0
- package/public/git-insights.js +123 -0
- package/public/index.html +347 -0
- package/public/style.css +1864 -0
- package/public/ui-state.js +103 -0
- package/public/utils.js +71 -0
- package/public/vendor/alpine.min.js +5 -0
- package/public/vendor/chart.umd.min.js +20 -0
- package/public/work-report.js +118 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
2
|
+
import { dirname, join } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const DATA_DIR = join(__dirname, '..', 'data');
|
|
7
|
+
const PRICING_FILE = join(DATA_DIR, 'pricing.json');
|
|
8
|
+
const CACHE_FILE = join(DATA_DIR, 'pricing-cache.json');
|
|
9
|
+
|
|
10
|
+
const PROVIDER_PREFIXES = ['anthropic--', 'bedrock--', 'vertex--'];
|
|
11
|
+
|
|
12
|
+
// Portkey API 端点
|
|
13
|
+
const PORTKEY_SINGLE_MODEL_URL = 'https://api.portkey.ai/model-configs/pricing';
|
|
14
|
+
|
|
15
|
+
// 内存中的合并定价表
|
|
16
|
+
let pricingTable = new Map();
|
|
17
|
+
// 记录已尝试过 API 查询但失败的模型,避免重复请求
|
|
18
|
+
const apiFailedModels = new Set();
|
|
19
|
+
|
|
20
|
+
// 模型家族关键词 → 用于 fuzzy match 的子串匹配规则
|
|
21
|
+
// 顺序:更具体的关键词放前面(如 gpt-4.1-mini 在 gpt-4.1 之前),避免被通用关键词截胡
|
|
22
|
+
const FUZZY_KEYWORDS = [
|
|
23
|
+
// Claude
|
|
24
|
+
'opus', 'sonnet', 'haiku',
|
|
25
|
+
// OpenAI
|
|
26
|
+
'gpt-4.1-mini', 'gpt-4.1-nano', 'gpt-4.1', 'gpt-5', 'gpt-4o', 'gpt-4',
|
|
27
|
+
'o4-mini', 'o3-pro', 'o3', 'o1',
|
|
28
|
+
// Google
|
|
29
|
+
'gemini',
|
|
30
|
+
// 中国厂商
|
|
31
|
+
'glm', 'kimi', 'minimax', 'qwen', 'deepseek', 'doubao', 'baichuan',
|
|
32
|
+
'yi-', 'grok', 'llama',
|
|
33
|
+
// Mistral
|
|
34
|
+
'mistral', 'codestral',
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
// ── 内部工具 ──
|
|
38
|
+
|
|
39
|
+
function ensureDataDir() {
|
|
40
|
+
if (!existsSync(DATA_DIR)) {
|
|
41
|
+
mkdirSync(DATA_DIR, { recursive: true });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function loadJsonFile(filePath) {
|
|
46
|
+
if (!existsSync(filePath)) return null;
|
|
47
|
+
try {
|
|
48
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
49
|
+
return JSON.parse(raw);
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function saveJsonFile(filePath, data) {
|
|
56
|
+
ensureDataDir();
|
|
57
|
+
writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function mergeIntoTable(source) {
|
|
61
|
+
if (!source || typeof source !== 'object') return;
|
|
62
|
+
for (const [key, value] of Object.entries(source)) {
|
|
63
|
+
if (key.startsWith('_')) continue;
|
|
64
|
+
// 允许两种形式:{ input, output, ... } 或 { aliasOf: "target-name" }
|
|
65
|
+
if (value && typeof value === 'object' && !value.unknown) {
|
|
66
|
+
pricingTable.set(key, value);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 解析 aliasOf 链:若 entry 是 { aliasOf: "..." },递归查找最终定价
|
|
72
|
+
function resolveAlias(entry, depth = 0) {
|
|
73
|
+
if (!entry || depth >= 5) return entry;
|
|
74
|
+
if (entry.aliasOf && typeof entry.aliasOf === 'string') {
|
|
75
|
+
const target = pricingTable.get(entry.aliasOf);
|
|
76
|
+
if (target) return resolveAlias(target, depth + 1);
|
|
77
|
+
// 别名指向的目标不存在:尝试 fuzzy 查找一次(覆盖 Portkey 同步键)
|
|
78
|
+
const fuzzy = fuzzyLookup(entry.aliasOf.toLowerCase());
|
|
79
|
+
if (fuzzy) return resolveAlias(fuzzy, depth + 1);
|
|
80
|
+
return { unknown: true };
|
|
81
|
+
}
|
|
82
|
+
return entry;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 在表中按 fuzzy 关键词查找匹配项
|
|
86
|
+
function fuzzyLookup(modelLower) {
|
|
87
|
+
let matchedKeyword = null;
|
|
88
|
+
for (const kw of FUZZY_KEYWORDS) {
|
|
89
|
+
if (modelLower.includes(kw)) {
|
|
90
|
+
matchedKeyword = kw;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (!matchedKeyword) return null;
|
|
95
|
+
|
|
96
|
+
for (const [key, pricing] of pricingTable) {
|
|
97
|
+
const keyLower = key.toLowerCase();
|
|
98
|
+
if (!keyLower.includes(matchedKeyword)) continue;
|
|
99
|
+
// o3 不应匹配 o3-pro
|
|
100
|
+
if (matchedKeyword === 'o3' && keyLower.includes('pro')) continue;
|
|
101
|
+
// gpt-4.1 不应匹配 gpt-4.1-mini/nano
|
|
102
|
+
if (matchedKeyword === 'gpt-4.1' && (keyLower.includes('mini') || keyLower.includes('nano'))) continue;
|
|
103
|
+
return pricing;
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function inferProvider(modelName) {
|
|
109
|
+
const lower = (modelName || '').toLowerCase();
|
|
110
|
+
if (lower.includes('claude')) return 'anthropic';
|
|
111
|
+
if (lower.includes('gemini') || lower.includes('palm')) return 'google';
|
|
112
|
+
if (lower.includes('gpt') || lower.includes('o1') || lower.includes('o3') || lower.includes('o4')) return 'openai';
|
|
113
|
+
if (lower.includes('deepseek')) return 'deepseek';
|
|
114
|
+
if (lower.includes('mistral')) return 'mistral-ai';
|
|
115
|
+
if (lower.includes('grok')) return 'xai';
|
|
116
|
+
return 'openai';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Portkey 格式 (cents/token) → 内部格式 ($/1M tokens)
|
|
120
|
+
function convertPortkeyPricing(portkeyData) {
|
|
121
|
+
if (!portkeyData || !portkeyData.pay_as_you_go) return null;
|
|
122
|
+
|
|
123
|
+
const payg = portkeyData.pay_as_you_go;
|
|
124
|
+
const getPrice = (field) => {
|
|
125
|
+
const val = payg[field];
|
|
126
|
+
if (val && typeof val.price === 'number') {
|
|
127
|
+
return val.price * 10000;
|
|
128
|
+
}
|
|
129
|
+
return 0;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const result = {
|
|
133
|
+
input: getPrice('request_token'),
|
|
134
|
+
output: getPrice('response_token'),
|
|
135
|
+
cacheRead: getPrice('cache_read_input_token'),
|
|
136
|
+
cacheCreate: getPrice('cache_write_input_token'),
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
if (result.input === 0 && result.output === 0) return null;
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── 公共接口 ──
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 同步初始化定价模块。
|
|
147
|
+
* 加载顺序(后加载的覆盖先加载的):
|
|
148
|
+
* 1. data/pricing.json 中的 models(Portkey 批量同步的数据)
|
|
149
|
+
* 2. data/pricing-cache.json(运行时 API 查询缓存)
|
|
150
|
+
* 3. data/pricing.json 中的 overrides(权威覆盖 + 别名映射)
|
|
151
|
+
*/
|
|
152
|
+
export function initPricing() {
|
|
153
|
+
pricingTable.clear();
|
|
154
|
+
apiFailedModels.clear();
|
|
155
|
+
|
|
156
|
+
const pricingData = loadJsonFile(PRICING_FILE);
|
|
157
|
+
if (pricingData && pricingData.models) {
|
|
158
|
+
mergeIntoTable(pricingData.models);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const cacheData = loadJsonFile(CACHE_FILE);
|
|
162
|
+
if (cacheData && cacheData.models) {
|
|
163
|
+
mergeIntoTable(cacheData.models);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// overrides 最后加载,优先级最高
|
|
167
|
+
if (pricingData && pricingData.overrides) {
|
|
168
|
+
mergeIntoTable(pricingData.overrides);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 解析模型定价。
|
|
174
|
+
* 四层回退:精确匹配 → 去 provider 前缀 → 家族关键词 fuzzy match → unknown。
|
|
175
|
+
* 支持 aliasOf 别名映射(在 overrides 中可用 { aliasOf: "target" } 形式)。
|
|
176
|
+
*/
|
|
177
|
+
export function resolveModelPricing(model) {
|
|
178
|
+
if (!model) return { unknown: true };
|
|
179
|
+
|
|
180
|
+
if (pricingTable.size === 0) {
|
|
181
|
+
initPricing();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Tier 1: 精确匹配
|
|
185
|
+
if (pricingTable.has(model)) {
|
|
186
|
+
return resolveAlias(pricingTable.get(model));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Tier 2: 去掉 provider 前缀
|
|
190
|
+
let stripped = model;
|
|
191
|
+
for (const prefix of PROVIDER_PREFIXES) {
|
|
192
|
+
if (model.startsWith(prefix)) {
|
|
193
|
+
stripped = model.slice(prefix.length);
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (pricingTable.has(stripped)) {
|
|
198
|
+
return resolveAlias(pricingTable.get(stripped));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Tier 3: 按家族关键词 fuzzy 匹配
|
|
202
|
+
const lower = stripped.toLowerCase();
|
|
203
|
+
const fuzzy = fuzzyLookup(lower);
|
|
204
|
+
if (fuzzy) return resolveAlias(fuzzy);
|
|
205
|
+
|
|
206
|
+
return { unknown: true };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function getModel(record) {
|
|
210
|
+
return record.model || record.metadata?.model || '';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* 异步预加载所有未知模型的定价。
|
|
215
|
+
* 扫描 records,对本地没有的模型调用 Portkey API。
|
|
216
|
+
* - 成功:写入 pricing-cache.json,下次启动自动加载
|
|
217
|
+
* - 失败:标记为已失败,本次会话不再重试;该模型定价为 unknown(不计算费用)
|
|
218
|
+
*/
|
|
219
|
+
export async function preloadUnknownPricing(records) {
|
|
220
|
+
if (!records || records.length === 0) return;
|
|
221
|
+
|
|
222
|
+
const unknownModels = new Set();
|
|
223
|
+
for (const r of records) {
|
|
224
|
+
const model = getModel(r);
|
|
225
|
+
if (!model || apiFailedModels.has(model)) continue;
|
|
226
|
+
const pricing = resolveModelPricing(model);
|
|
227
|
+
if (pricing.unknown) {
|
|
228
|
+
unknownModels.add(model);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (unknownModels.size === 0) return;
|
|
233
|
+
|
|
234
|
+
const CONCURRENCY = 5;
|
|
235
|
+
const models = [...unknownModels];
|
|
236
|
+
const newCache = {};
|
|
237
|
+
|
|
238
|
+
for (let i = 0; i < models.length; i += CONCURRENCY) {
|
|
239
|
+
const batch = models.slice(i, i + CONCURRENCY);
|
|
240
|
+
const results = await Promise.all(
|
|
241
|
+
batch.map(async (model) => {
|
|
242
|
+
try {
|
|
243
|
+
const provider = inferProvider(model);
|
|
244
|
+
const url = `${PORTKEY_SINGLE_MODEL_URL}/${provider}/${encodeURIComponent(model)}`;
|
|
245
|
+
const controller = new AbortController();
|
|
246
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
247
|
+
|
|
248
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
249
|
+
clearTimeout(timeout);
|
|
250
|
+
|
|
251
|
+
if (!res.ok) {
|
|
252
|
+
apiFailedModels.add(model);
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const data = await res.json();
|
|
257
|
+
const pricing = convertPortkeyPricing(data);
|
|
258
|
+
if (pricing) {
|
|
259
|
+
return { model, pricing };
|
|
260
|
+
}
|
|
261
|
+
apiFailedModels.add(model);
|
|
262
|
+
} catch {
|
|
263
|
+
apiFailedModels.add(model);
|
|
264
|
+
}
|
|
265
|
+
return null;
|
|
266
|
+
})
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
for (const result of results) {
|
|
270
|
+
if (result) {
|
|
271
|
+
pricingTable.set(result.model, result.pricing);
|
|
272
|
+
newCache[result.model] = result.pricing;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (Object.keys(newCache).length > 0) {
|
|
278
|
+
const existing = loadJsonFile(CACHE_FILE) || { _meta: { source: 'portkey-api-cache' }, models: {} };
|
|
279
|
+
existing.models = { ...existing.models, ...newCache };
|
|
280
|
+
existing._meta.updatedAt = new Date().toISOString();
|
|
281
|
+
saveJsonFile(CACHE_FILE, existing);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function getLoadedModelCount() {
|
|
286
|
+
return pricingTable.size;
|
|
287
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// UsageRecord 兼容辅助函数
|
|
2
|
+
// 统一处理新格式(inputTokens/outputTokens)和旧格式(tokens.input/tokens.output)
|
|
3
|
+
|
|
4
|
+
export function getInputTokens(r) {
|
|
5
|
+
if (r.inputTokens !== undefined) return r.inputTokens;
|
|
6
|
+
return r.tokens?.input || 0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getOutputTokens(r) {
|
|
10
|
+
if (r.outputTokens !== undefined) return r.outputTokens;
|
|
11
|
+
return r.tokens?.output || 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getCacheRead(r) {
|
|
15
|
+
if (r.cacheReadTokens !== undefined) return r.cacheReadTokens;
|
|
16
|
+
return r.tokens?.cacheRead || 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getCacheCreate(r) {
|
|
20
|
+
if (r.cacheWriteTokens !== undefined) return r.cacheWriteTokens;
|
|
21
|
+
return r.tokens?.cacheCreate || 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getModel(r) {
|
|
25
|
+
return r.model || '';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function isAssistantRecord(r) {
|
|
29
|
+
if (r.metadata?.type === 'assistant') return true;
|
|
30
|
+
if (r.metadata?.type === 'user') return false;
|
|
31
|
+
if (r.tool === 'codex') return true;
|
|
32
|
+
if (r.tool === 'opencode' && r.metadata?.role !== 'user') return true;
|
|
33
|
+
if (r.type === 'assistant' && !r.tool) return true;
|
|
34
|
+
return false;
|
|
35
|
+
}
|