ccc-notifier 0.2.0 → 0.3.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/README.md +15 -9
- package/dist/{budget-5H2GQRXH.js → budget-KKTFYAXH.js} +2 -2
- package/dist/{chunk-KUJZYZSG.js → chunk-4RZ6OGTD.js} +1 -1
- package/dist/{chunk-OXMHOVUL.js → chunk-7KVOQ4UZ.js} +228 -34
- package/dist/chunk-DSV75EF7.js +357 -0
- package/dist/{chunk-IIYMGLV4.js → chunk-ECADO26T.js} +9 -2
- package/dist/chunk-HTYUYKFW.js +21 -0
- package/dist/{chunk-TBFKGFZX.js → chunk-J5QAYTFE.js} +3 -0
- package/dist/{chunk-OEG3AVU6.js → chunk-LHKBGA5K.js} +27 -1
- package/dist/{chunk-4JNZ7BHO.js → chunk-NV5UOHJA.js} +1 -1
- package/dist/{chunk-64N5SGTT.js → chunk-QX5KIRSU.js} +109 -13
- package/dist/cli.js +67 -16
- package/dist/{dashboard-LKK6NOBN.js → dashboard-TH3NOVT3.js} +3 -3
- package/dist/{history-MWFHO5VR.js → history-DLFYQJFM.js} +1 -1
- package/dist/{mute-2JW3NQXN.js → mute-EPLYH67P.js} +2 -2
- package/dist/{setup-IXPUR4CM.js → setup-PTO5N46B.js} +5 -4
- package/dist/{sweep-XKKODGMA.js → sweep-25EZAOGC.js} +121 -9
- package/dist/{track-76Q5CZM5.js → track-PXFSGMGL.js} +29 -13
- package/package.json +5 -2
- package/dist/chunk-TB5A7U7G.js +0 -82
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
aggregateNewTurn
|
|
4
|
+
} from "./chunk-LHKBGA5K.js";
|
|
5
|
+
import {
|
|
6
|
+
loadCursor,
|
|
7
|
+
logError,
|
|
8
|
+
sanitizeCursor
|
|
9
|
+
} from "./chunk-ECADO26T.js";
|
|
10
|
+
|
|
11
|
+
// src/codex/transcript.ts
|
|
12
|
+
import { readFile } from "fs/promises";
|
|
13
|
+
import { basename } from "path";
|
|
14
|
+
var NEWLINE = 10;
|
|
15
|
+
function isRecord(v) {
|
|
16
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
17
|
+
}
|
|
18
|
+
function numOf(v) {
|
|
19
|
+
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
20
|
+
}
|
|
21
|
+
function strOrNull(v) {
|
|
22
|
+
return typeof v === "string" ? v : null;
|
|
23
|
+
}
|
|
24
|
+
function zeroTotals() {
|
|
25
|
+
return { input: 0, cached: 0, output: 0 };
|
|
26
|
+
}
|
|
27
|
+
function isZeroTotals(t) {
|
|
28
|
+
return t.input === 0 && t.cached === 0 && t.output === 0;
|
|
29
|
+
}
|
|
30
|
+
function addTotals(target, d) {
|
|
31
|
+
target.input += d.input;
|
|
32
|
+
target.cached += d.cached;
|
|
33
|
+
target.output += d.output;
|
|
34
|
+
}
|
|
35
|
+
function readTotals(v) {
|
|
36
|
+
if (!isRecord(v)) return null;
|
|
37
|
+
return {
|
|
38
|
+
input: numOf(v.input_tokens),
|
|
39
|
+
cached: numOf(v.cached_input_tokens),
|
|
40
|
+
output: numOf(v.output_tokens)
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function totalsToBuckets(acc) {
|
|
44
|
+
return {
|
|
45
|
+
input: Math.max(0, acc.input - acc.cached),
|
|
46
|
+
output: acc.output,
|
|
47
|
+
cacheWrite5m: 0,
|
|
48
|
+
cacheWrite1h: 0,
|
|
49
|
+
cacheRead: acc.cached
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function sessionIdFromFilename(rolloutPath) {
|
|
53
|
+
const m = /^rollout-.*-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i.exec(
|
|
54
|
+
basename(rolloutPath)
|
|
55
|
+
);
|
|
56
|
+
return m !== null ? m[1] : "";
|
|
57
|
+
}
|
|
58
|
+
async function readAll(path) {
|
|
59
|
+
try {
|
|
60
|
+
return await readFile(path);
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function newSegmentBuf() {
|
|
66
|
+
return {
|
|
67
|
+
acc: zeroTotals(),
|
|
68
|
+
apiCalls: 0,
|
|
69
|
+
prompt: null,
|
|
70
|
+
turnCtxCwd: null,
|
|
71
|
+
firstTs: null,
|
|
72
|
+
endTs: null,
|
|
73
|
+
hasLines: false
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
async function scanWindow(rolloutPath, cursor) {
|
|
77
|
+
const buffer = await readAll(rolloutPath);
|
|
78
|
+
if (buffer === null) return null;
|
|
79
|
+
const fileSize = buffer.length;
|
|
80
|
+
let startOffset;
|
|
81
|
+
let rescan;
|
|
82
|
+
if (cursor !== null && cursor.offset > 0 && cursor.offset <= fileSize && buffer[cursor.offset - 1] === NEWLINE) {
|
|
83
|
+
startOffset = cursor.offset;
|
|
84
|
+
rescan = false;
|
|
85
|
+
} else {
|
|
86
|
+
startOffset = 0;
|
|
87
|
+
rescan = cursor !== null;
|
|
88
|
+
}
|
|
89
|
+
const tsFloor = cursor?.lastTs ?? null;
|
|
90
|
+
const initTotals = cursor?.codexTotals;
|
|
91
|
+
let prev = initTotals !== void 0 ? { ...initTotals } : zeroTotals();
|
|
92
|
+
const acc = zeroTotals();
|
|
93
|
+
let apiCalls = 0;
|
|
94
|
+
let lastModel = null;
|
|
95
|
+
let windowPrompt = null;
|
|
96
|
+
let windowTurnCtxCwd = null;
|
|
97
|
+
let sessionMetaCwd = null;
|
|
98
|
+
let sessionMetaSid = null;
|
|
99
|
+
let firstTs = null;
|
|
100
|
+
let lastTs = null;
|
|
101
|
+
const segments = [];
|
|
102
|
+
let seg = newSegmentBuf();
|
|
103
|
+
const snapshotSegment = (endOffset) => ({
|
|
104
|
+
acc: seg.acc,
|
|
105
|
+
apiCalls: seg.apiCalls,
|
|
106
|
+
prompt: seg.prompt,
|
|
107
|
+
model: lastModel,
|
|
108
|
+
cwd: seg.turnCtxCwd ?? sessionMetaCwd,
|
|
109
|
+
firstTs: seg.firstTs,
|
|
110
|
+
endTs: seg.endTs,
|
|
111
|
+
endOffset,
|
|
112
|
+
prevAtEnd: { ...prev },
|
|
113
|
+
lastTsAtEnd: lastTs
|
|
114
|
+
});
|
|
115
|
+
const handleLine = (raw, endOffset) => {
|
|
116
|
+
if (raw.trim().length === 0) return;
|
|
117
|
+
let obj;
|
|
118
|
+
try {
|
|
119
|
+
obj = JSON.parse(raw);
|
|
120
|
+
} catch {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (!isRecord(obj)) return;
|
|
124
|
+
const ts = strOrNull(obj.timestamp);
|
|
125
|
+
if (rescan && tsFloor !== null && ts !== null && ts <= tsFloor) return;
|
|
126
|
+
if (ts !== null) {
|
|
127
|
+
if (firstTs === null || ts < firstTs) firstTs = ts;
|
|
128
|
+
if (lastTs === null || ts > lastTs) lastTs = ts;
|
|
129
|
+
if (seg.firstTs === null || ts < seg.firstTs) seg.firstTs = ts;
|
|
130
|
+
seg.endTs = ts;
|
|
131
|
+
}
|
|
132
|
+
seg.hasLines = true;
|
|
133
|
+
const payload = isRecord(obj.payload) ? obj.payload : null;
|
|
134
|
+
if (payload === null) return;
|
|
135
|
+
const type = obj.type;
|
|
136
|
+
if (type === "session_meta") {
|
|
137
|
+
const sid = strOrNull(payload.session_id);
|
|
138
|
+
if (sid !== null) sessionMetaSid = sid;
|
|
139
|
+
const c = strOrNull(payload.cwd);
|
|
140
|
+
if (c !== null) sessionMetaCwd = c;
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (type === "turn_context") {
|
|
144
|
+
const m = strOrNull(payload.model);
|
|
145
|
+
if (m !== null) lastModel = m;
|
|
146
|
+
const c = strOrNull(payload.cwd);
|
|
147
|
+
if (c !== null) {
|
|
148
|
+
seg.turnCtxCwd = c;
|
|
149
|
+
windowTurnCtxCwd = c;
|
|
150
|
+
}
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (type !== "event_msg") return;
|
|
154
|
+
const kind = payload.type;
|
|
155
|
+
if (kind === "user_message") {
|
|
156
|
+
const msg = strOrNull(payload.message);
|
|
157
|
+
if (msg !== null) {
|
|
158
|
+
seg.prompt = msg;
|
|
159
|
+
windowPrompt = msg;
|
|
160
|
+
}
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (kind === "token_count") {
|
|
164
|
+
const info = isRecord(payload.info) ? payload.info : null;
|
|
165
|
+
if (info === null) return;
|
|
166
|
+
const total = readTotals(info.total_token_usage);
|
|
167
|
+
if (total === null) return;
|
|
168
|
+
let step = {
|
|
169
|
+
input: total.input - prev.input,
|
|
170
|
+
cached: total.cached - prev.cached,
|
|
171
|
+
output: total.output - prev.output
|
|
172
|
+
};
|
|
173
|
+
if (step.input < 0 || step.cached < 0 || step.output < 0) {
|
|
174
|
+
step = readTotals(info.last_token_usage) ?? zeroTotals();
|
|
175
|
+
}
|
|
176
|
+
addTotals(acc, step);
|
|
177
|
+
addTotals(seg.acc, step);
|
|
178
|
+
prev = total;
|
|
179
|
+
if (!isZeroTotals(step)) {
|
|
180
|
+
apiCalls++;
|
|
181
|
+
seg.apiCalls++;
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (kind === "task_complete") {
|
|
186
|
+
segments.push(snapshotSegment(endOffset));
|
|
187
|
+
seg = newSegmentBuf();
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
let lineStart = startOffset;
|
|
191
|
+
for (let pos = startOffset; pos < fileSize; pos++) {
|
|
192
|
+
if (buffer[pos] !== NEWLINE) continue;
|
|
193
|
+
handleLine(buffer.toString("utf8", lineStart, pos), pos + 1);
|
|
194
|
+
lineStart = pos + 1;
|
|
195
|
+
}
|
|
196
|
+
const newOffset = lineStart;
|
|
197
|
+
const open = seg.hasLines ? snapshotSegment(newOffset) : null;
|
|
198
|
+
return {
|
|
199
|
+
segments,
|
|
200
|
+
open,
|
|
201
|
+
acc,
|
|
202
|
+
prev,
|
|
203
|
+
apiCalls,
|
|
204
|
+
model: lastModel,
|
|
205
|
+
prompt: windowPrompt,
|
|
206
|
+
cwd: windowTurnCtxCwd ?? sessionMetaCwd,
|
|
207
|
+
sessionId: sessionMetaSid ?? sessionIdFromFilename(rolloutPath),
|
|
208
|
+
firstTs,
|
|
209
|
+
lastTs,
|
|
210
|
+
newOffset
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function windowCursor(scan) {
|
|
214
|
+
return {
|
|
215
|
+
offset: scan.newOffset,
|
|
216
|
+
lastUuid: null,
|
|
217
|
+
// rollout に uuid 行は無い
|
|
218
|
+
lastTs: scan.lastTs,
|
|
219
|
+
seenMessageKeys: [],
|
|
220
|
+
// 去重は codexTotals の差分方式が担う
|
|
221
|
+
codexTotals: { ...scan.prev }
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
async function aggregateCodexTurn(rolloutPath, cursor) {
|
|
225
|
+
const scan = await scanWindow(rolloutPath, cursor);
|
|
226
|
+
if (scan === null || isZeroTotals(scan.acc)) return null;
|
|
227
|
+
return {
|
|
228
|
+
sessionId: scan.sessionId,
|
|
229
|
+
main: { [scan.model ?? "unknown"]: totalsToBuckets(scan.acc) },
|
|
230
|
+
sidechain: {},
|
|
231
|
+
// Codex にサブエージェント概念は無い
|
|
232
|
+
apiCalls: scan.apiCalls,
|
|
233
|
+
prompt: scan.prompt,
|
|
234
|
+
cwd: scan.cwd,
|
|
235
|
+
gitBranch: null,
|
|
236
|
+
// rollout に無い
|
|
237
|
+
firstTs: scan.firstTs,
|
|
238
|
+
lastTs: scan.lastTs,
|
|
239
|
+
newCursor: windowCursor(scan)
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
async function splitIntoCodexTurnDrafts(rolloutPath, cursor) {
|
|
243
|
+
const scan = await scanWindow(rolloutPath, cursor);
|
|
244
|
+
if (scan === null || isZeroTotals(scan.acc)) return null;
|
|
245
|
+
const picked = scan.segments.filter((s) => !isZeroTotals(s.acc));
|
|
246
|
+
if (scan.open !== null && !isZeroTotals(scan.open.acc)) {
|
|
247
|
+
const last = picked[picked.length - 1];
|
|
248
|
+
if (last !== void 0) {
|
|
249
|
+
addTotals(last.acc, scan.open.acc);
|
|
250
|
+
last.apiCalls += scan.open.apiCalls;
|
|
251
|
+
if (scan.open.endTs !== null) last.endTs = scan.open.endTs;
|
|
252
|
+
} else {
|
|
253
|
+
picked.push(scan.open);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const lastIndex = picked.length - 1;
|
|
257
|
+
return picked.map((s, i) => ({
|
|
258
|
+
agg: {
|
|
259
|
+
sessionId: scan.sessionId,
|
|
260
|
+
// session_meta はファイル先頭にしか無いので全ドラフト共通
|
|
261
|
+
main: { [s.model ?? "unknown"]: totalsToBuckets(s.acc) },
|
|
262
|
+
sidechain: {},
|
|
263
|
+
apiCalls: s.apiCalls,
|
|
264
|
+
prompt: s.prompt,
|
|
265
|
+
cwd: s.cwd,
|
|
266
|
+
gitBranch: null,
|
|
267
|
+
firstTs: s.firstTs,
|
|
268
|
+
lastTs: s.endTs,
|
|
269
|
+
// 最後のドラフトはウィンドウ全体を消費した状態(= aggregateCodexTurn の newCursor と同一。
|
|
270
|
+
// 末尾の usage ゼロな行の読み捨てもここに含まれる)。途中のドラフトはそのセグメント末尾を
|
|
271
|
+
// 指す有効な再開点(そこから読み直せば残りが差分になる)。
|
|
272
|
+
newCursor: i === lastIndex ? windowCursor(scan) : {
|
|
273
|
+
offset: s.endOffset,
|
|
274
|
+
lastUuid: null,
|
|
275
|
+
lastTs: s.lastTsAtEnd,
|
|
276
|
+
seenMessageKeys: [],
|
|
277
|
+
codexTotals: { ...s.prevAtEnd }
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
endTs: s.endTs
|
|
281
|
+
}));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// src/subagents.ts
|
|
285
|
+
import { promises as fs } from "fs";
|
|
286
|
+
import { join } from "path";
|
|
287
|
+
var MAX_AGENT_FILES = 200;
|
|
288
|
+
function emptyBuckets() {
|
|
289
|
+
return { input: 0, output: 0, cacheWrite5m: 0, cacheWrite1h: 0, cacheRead: 0 };
|
|
290
|
+
}
|
|
291
|
+
function addToModel(target, model, b) {
|
|
292
|
+
const cur = target[model] ?? emptyBuckets();
|
|
293
|
+
cur.input += b.input;
|
|
294
|
+
cur.output += b.output;
|
|
295
|
+
cur.cacheWrite5m += b.cacheWrite5m;
|
|
296
|
+
cur.cacheWrite1h += b.cacheWrite1h;
|
|
297
|
+
cur.cacheRead += b.cacheRead;
|
|
298
|
+
target[model] = cur;
|
|
299
|
+
}
|
|
300
|
+
function mergeUsage(target, src) {
|
|
301
|
+
for (const [model, b] of Object.entries(src)) addToModel(target, model, b);
|
|
302
|
+
}
|
|
303
|
+
function subagentsDirOf(mainTranscriptPath) {
|
|
304
|
+
const base = mainTranscriptPath.endsWith(".jsonl") ? mainTranscriptPath.slice(0, -".jsonl".length) : mainTranscriptPath;
|
|
305
|
+
return join(base, "subagents");
|
|
306
|
+
}
|
|
307
|
+
async function listAgentFiles(dir, entries) {
|
|
308
|
+
const files = entries.filter((e) => e.isFile() && e.name.startsWith("agent-") && e.name.endsWith(".jsonl")).map((e) => join(dir, e.name));
|
|
309
|
+
if (files.length <= MAX_AGENT_FILES) return files;
|
|
310
|
+
const withMtime = [];
|
|
311
|
+
for (const p of files) {
|
|
312
|
+
let mtime = 0;
|
|
313
|
+
try {
|
|
314
|
+
mtime = (await fs.stat(p)).mtimeMs;
|
|
315
|
+
} catch {
|
|
316
|
+
mtime = 0;
|
|
317
|
+
}
|
|
318
|
+
withMtime.push({ path: p, mtime });
|
|
319
|
+
}
|
|
320
|
+
withMtime.sort((a, b) => b.mtime - a.mtime);
|
|
321
|
+
return withMtime.slice(0, MAX_AGENT_FILES).map((x) => x.path);
|
|
322
|
+
}
|
|
323
|
+
async function collectSubagentUsage(mainTranscriptPath) {
|
|
324
|
+
const dir = subagentsDirOf(mainTranscriptPath);
|
|
325
|
+
let entries;
|
|
326
|
+
try {
|
|
327
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
328
|
+
} catch {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
const files = await listAgentFiles(dir, entries);
|
|
332
|
+
const perModel = {};
|
|
333
|
+
let apiCalls = 0;
|
|
334
|
+
let agentFiles = 0;
|
|
335
|
+
const newCursors = [];
|
|
336
|
+
for (const filePath of files) {
|
|
337
|
+
try {
|
|
338
|
+
const cursor = sanitizeCursor(loadCursor(filePath));
|
|
339
|
+
const agg = await aggregateNewTurn(filePath, cursor);
|
|
340
|
+
if (agg === null) continue;
|
|
341
|
+
mergeUsage(perModel, agg.main);
|
|
342
|
+
mergeUsage(perModel, agg.sidechain);
|
|
343
|
+
apiCalls += agg.apiCalls;
|
|
344
|
+
agentFiles += 1;
|
|
345
|
+
newCursors.push({ path: filePath, cursor: agg.newCursor });
|
|
346
|
+
} catch (err) {
|
|
347
|
+
logError("subagents:file", err);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return { perModel, apiCalls, agentFiles, newCursors };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export {
|
|
354
|
+
aggregateCodexTurn,
|
|
355
|
+
splitIntoCodexTurnDrafts,
|
|
356
|
+
collectSubagentUsage
|
|
357
|
+
};
|
|
@@ -170,7 +170,7 @@ function loadCursor(transcriptPath) {
|
|
|
170
170
|
}
|
|
171
171
|
function sanitizeCursor(raw) {
|
|
172
172
|
if (!isPlainObject(raw)) return null;
|
|
173
|
-
const { offset, lastUuid, lastTs, seenMessageKeys } = raw;
|
|
173
|
+
const { offset, lastUuid, lastTs, seenMessageKeys, codexTotals } = raw;
|
|
174
174
|
if (typeof offset !== "number" || !Number.isFinite(offset)) return null;
|
|
175
175
|
if (lastUuid !== null && typeof lastUuid !== "string") return null;
|
|
176
176
|
if (lastTs !== null && typeof lastTs !== "string") return null;
|
|
@@ -180,7 +180,14 @@ function sanitizeCursor(raw) {
|
|
|
180
180
|
if (typeof key !== "string") return null;
|
|
181
181
|
keys.push(key);
|
|
182
182
|
}
|
|
183
|
-
|
|
183
|
+
const cursor = { offset, lastUuid, lastTs, seenMessageKeys: keys };
|
|
184
|
+
if (isPlainObject(codexTotals)) {
|
|
185
|
+
const { input, cached, output } = codexTotals;
|
|
186
|
+
if (typeof input === "number" && Number.isFinite(input) && input >= 0 && typeof cached === "number" && Number.isFinite(cached) && cached >= 0 && typeof output === "number" && Number.isFinite(output) && output >= 0) {
|
|
187
|
+
cursor.codexTotals = { input, cached, output };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return cursor;
|
|
184
191
|
}
|
|
185
192
|
function saveCursor(transcriptPath, c) {
|
|
186
193
|
const p = paths();
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/codex/env.ts
|
|
4
|
+
import { statSync } from "fs";
|
|
5
|
+
import { homedir } from "os";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
function codexHome() {
|
|
8
|
+
return process.env.CCCN_CODEX_HOME || join(homedir(), ".codex");
|
|
9
|
+
}
|
|
10
|
+
function detectCodex() {
|
|
11
|
+
try {
|
|
12
|
+
return statSync(codexHome()).isDirectory();
|
|
13
|
+
} catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
codexHome,
|
|
20
|
+
detectCodex
|
|
21
|
+
};
|
|
@@ -29,6 +29,9 @@ function capitalize(token) {
|
|
|
29
29
|
var ALPHA_ONLY = /^[A-Za-z]+$/;
|
|
30
30
|
var NUMERIC_ONLY = /^\d+$/;
|
|
31
31
|
function modelDisplayName(id) {
|
|
32
|
+
if (id.startsWith("gpt-")) {
|
|
33
|
+
return `GPT-${id.slice(4)}`.replace(/-codex$/, " Codex");
|
|
34
|
+
}
|
|
32
35
|
let s = id;
|
|
33
36
|
s = s.replace(/^claude-/, "");
|
|
34
37
|
s = s.replace(/-20\d{6}/, "");
|
|
@@ -29,7 +29,14 @@ function builtinPriceTable() {
|
|
|
29
29
|
"claude-3-5-sonnet": price(3, 15, 3.75, 6, 0.3, "builtin"),
|
|
30
30
|
"claude-haiku-4-5": price(1, 5, 1.25, 2, 0.1, "builtin"),
|
|
31
31
|
"claude-3-5-haiku": price(0.8, 4, 1, 1.6, 0.08, "builtin"),
|
|
32
|
-
"claude-3-haiku": price(0.25, 1.25, 0.3125, 0.5, 0.025, "builtin")
|
|
32
|
+
"claude-3-haiku": price(0.25, 1.25, 0.3125, 0.5, 0.025, "builtin"),
|
|
33
|
+
// OpenAI Codex CLI 対応(公式レートに基づく単価。キャッシュ書き込み課金は無いため 0)
|
|
34
|
+
"gpt-5.5": price(5, 30, 0, 0, 0.5, "builtin"),
|
|
35
|
+
"gpt-5.1": price(1.25, 10, 0, 0, 0.125, "builtin"),
|
|
36
|
+
"gpt-5": price(1.25, 10, 0, 0, 0.125, "builtin"),
|
|
37
|
+
"gpt-5-codex": price(1.25, 10, 0, 0, 0.125, "builtin"),
|
|
38
|
+
"gpt-5.1-codex": price(1.25, 10, 0, 0, 0.125, "builtin"),
|
|
39
|
+
"o3": price(2, 8, 0, 0, 0.5, "builtin")
|
|
33
40
|
};
|
|
34
41
|
}
|
|
35
42
|
function normalizeModelId(modelId) {
|
|
@@ -104,6 +111,7 @@ function isCacheFresh(fetchedAt) {
|
|
|
104
111
|
function toFiniteNumber(v) {
|
|
105
112
|
return typeof v === "number" && Number.isFinite(v) ? v : null;
|
|
106
113
|
}
|
|
114
|
+
var LITELLM_OPENAI_KEY_RE = /^(gpt-|o3($|-)|codex-)/;
|
|
107
115
|
function convertLiteLLMPayload(payload) {
|
|
108
116
|
if (payload === null || typeof payload !== "object") {
|
|
109
117
|
throw new Error("invalid litellm payload: not an object");
|
|
@@ -113,6 +121,24 @@ function convertLiteLLMPayload(payload) {
|
|
|
113
121
|
if (rawEntry === null || typeof rawEntry !== "object") continue;
|
|
114
122
|
const entry = rawEntry;
|
|
115
123
|
const provider = entry.litellm_provider;
|
|
124
|
+
if (provider === "openai") {
|
|
125
|
+
const key2 = rawKey.toLowerCase();
|
|
126
|
+
if (!LITELLM_OPENAI_KEY_RE.test(key2)) continue;
|
|
127
|
+
const inputRaw2 = toFiniteNumber(entry.input_cost_per_token);
|
|
128
|
+
const outputRaw2 = toFiniteNumber(entry.output_cost_per_token);
|
|
129
|
+
if (inputRaw2 === null || inputRaw2 <= 0) continue;
|
|
130
|
+
if (outputRaw2 === null || outputRaw2 <= 0) continue;
|
|
131
|
+
const cacheReadRaw2 = toFiniteNumber(entry.cache_read_input_token_cost);
|
|
132
|
+
table[key2] = {
|
|
133
|
+
input: inputRaw2 * 1e6,
|
|
134
|
+
output: outputRaw2 * 1e6,
|
|
135
|
+
cacheRead: cacheReadRaw2 !== null ? cacheReadRaw2 * 1e6 : 0,
|
|
136
|
+
cacheWrite5m: 0,
|
|
137
|
+
cacheWrite1h: 0,
|
|
138
|
+
source: "litellm"
|
|
139
|
+
};
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
116
142
|
if (typeof provider === "string" && provider !== "anthropic") continue;
|
|
117
143
|
let key = rawKey.toLowerCase();
|
|
118
144
|
if (key.startsWith("anthropic/")) key = key.slice("anthropic/".length);
|