costly 0.1.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/cli.js +302 -0
- package/dist/index.cjs +335 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +38 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +308 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import * as fs from "fs";
|
|
5
|
+
import * as path from "path";
|
|
6
|
+
import * as readline from "readline";
|
|
7
|
+
var CYAN = "\x1B[36m";
|
|
8
|
+
var GREEN = "\x1B[32m";
|
|
9
|
+
var DIM = "\x1B[2m";
|
|
10
|
+
var RED = "\x1B[31m";
|
|
11
|
+
var YELLOW = "\x1B[33m";
|
|
12
|
+
var RESET = "\x1B[0m";
|
|
13
|
+
var BOLD = "\x1B[1m";
|
|
14
|
+
var VERIFY_URL = "https://www.getcostly.dev/api/v1/verify-key";
|
|
15
|
+
function log(msg) {
|
|
16
|
+
console.log(msg);
|
|
17
|
+
}
|
|
18
|
+
function success(msg) {
|
|
19
|
+
log(`${GREEN} \u2713${RESET} ${msg}`);
|
|
20
|
+
}
|
|
21
|
+
function warn(msg) {
|
|
22
|
+
log(`${YELLOW} !${RESET} ${msg}`);
|
|
23
|
+
}
|
|
24
|
+
function error(msg) {
|
|
25
|
+
log(`${RED} \u2717${RESET} ${msg}`);
|
|
26
|
+
}
|
|
27
|
+
function dim(msg) {
|
|
28
|
+
return `${DIM}${msg}${RESET}`;
|
|
29
|
+
}
|
|
30
|
+
function ask(question) {
|
|
31
|
+
const rl = readline.createInterface({
|
|
32
|
+
input: process.stdin,
|
|
33
|
+
output: process.stdout
|
|
34
|
+
});
|
|
35
|
+
return new Promise((resolve) => {
|
|
36
|
+
rl.question(question, (answer) => {
|
|
37
|
+
rl.close();
|
|
38
|
+
resolve(answer.trim());
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
async function confirm(question) {
|
|
43
|
+
const answer = await ask(`${question} ${dim("(Y/n)")} `);
|
|
44
|
+
return answer === "" || answer.toLowerCase() === "y";
|
|
45
|
+
}
|
|
46
|
+
function detectPackageManager(cwd) {
|
|
47
|
+
if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
48
|
+
if (fs.existsSync(path.join(cwd, "bun.lockb")) || fs.existsSync(path.join(cwd, "bun.lock"))) return "bun";
|
|
49
|
+
if (fs.existsSync(path.join(cwd, "yarn.lock"))) return "yarn";
|
|
50
|
+
return "npm";
|
|
51
|
+
}
|
|
52
|
+
function installCommand(pm) {
|
|
53
|
+
switch (pm) {
|
|
54
|
+
case "pnpm":
|
|
55
|
+
return "pnpm add costly";
|
|
56
|
+
case "yarn":
|
|
57
|
+
return "yarn add costly";
|
|
58
|
+
case "bun":
|
|
59
|
+
return "bun add costly";
|
|
60
|
+
default:
|
|
61
|
+
return "npm install costly";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".mts"]);
|
|
65
|
+
var IGNORE_DIRS = /* @__PURE__ */ new Set(["node_modules", ".next", "dist", "build", ".git", ".vercel", "coverage"]);
|
|
66
|
+
function scanDirectory(dir, cwd) {
|
|
67
|
+
const results = [];
|
|
68
|
+
let entries;
|
|
69
|
+
try {
|
|
70
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
71
|
+
} catch {
|
|
72
|
+
return results;
|
|
73
|
+
}
|
|
74
|
+
for (const entry of entries) {
|
|
75
|
+
if (entry.name.startsWith(".") && entry.name !== ".") continue;
|
|
76
|
+
if (IGNORE_DIRS.has(entry.name)) continue;
|
|
77
|
+
const fullPath = path.join(dir, entry.name);
|
|
78
|
+
if (entry.isDirectory()) {
|
|
79
|
+
results.push(...scanDirectory(fullPath, cwd));
|
|
80
|
+
} else if (entry.isFile() && SOURCE_EXTENSIONS.has(path.extname(entry.name))) {
|
|
81
|
+
const found = scanFile(fullPath, cwd);
|
|
82
|
+
if (found.length > 0) results.push(...found);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return results;
|
|
86
|
+
}
|
|
87
|
+
function scanFile(filePath, cwd) {
|
|
88
|
+
const results = [];
|
|
89
|
+
let content;
|
|
90
|
+
try {
|
|
91
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
92
|
+
} catch {
|
|
93
|
+
return results;
|
|
94
|
+
}
|
|
95
|
+
if (content.includes('from "costly"') || content.includes("from 'costly'") || content.includes('require("costly")') || content.includes("require('costly')")) {
|
|
96
|
+
return results;
|
|
97
|
+
}
|
|
98
|
+
const lines = content.split("\n");
|
|
99
|
+
const pattern = /(?:(?:export\s+)?(?:const|let|var)\s+)(\w+)\s*=\s*(new\s+Anthropic\s*\([^)]*\))/;
|
|
100
|
+
for (let i = 0; i < lines.length; i++) {
|
|
101
|
+
const match = lines[i].match(pattern);
|
|
102
|
+
if (match) {
|
|
103
|
+
results.push({
|
|
104
|
+
filePath,
|
|
105
|
+
relativePath: path.relative(cwd, filePath),
|
|
106
|
+
line: i + 1,
|
|
107
|
+
match: lines[i].trim(),
|
|
108
|
+
varName: match[1],
|
|
109
|
+
fullInit: match[2]
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return results;
|
|
114
|
+
}
|
|
115
|
+
function applyChanges(found) {
|
|
116
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
117
|
+
for (const f of found) {
|
|
118
|
+
const existing = byFile.get(f.filePath) || [];
|
|
119
|
+
existing.push(f);
|
|
120
|
+
byFile.set(f.filePath, existing);
|
|
121
|
+
}
|
|
122
|
+
for (const [filePath, items] of byFile) {
|
|
123
|
+
let content = fs.readFileSync(filePath, "utf-8");
|
|
124
|
+
const hasCostlyImport = content.includes('from "costly"') || content.includes("from 'costly'") || content.includes('require("costly")') || content.includes("require('costly')");
|
|
125
|
+
const usesEsm = content.includes("import ") && (content.includes(" from ") || content.includes("import {"));
|
|
126
|
+
if (!hasCostlyImport) {
|
|
127
|
+
const importStatement = usesEsm ? 'import { costly } from "costly";\n' : 'const { costly } = require("costly");\n';
|
|
128
|
+
const lines = content.split("\n");
|
|
129
|
+
let insertIndex = 0;
|
|
130
|
+
for (let i = 0; i < lines.length; i++) {
|
|
131
|
+
const line = lines[i].trim();
|
|
132
|
+
if (line.startsWith("import ") || line.startsWith("import{") || line.includes("require(") && (line.startsWith("const ") || line.startsWith("let ") || line.startsWith("var "))) {
|
|
133
|
+
insertIndex = i + 1;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
lines.splice(insertIndex, 0, importStatement.trimEnd());
|
|
137
|
+
content = lines.join("\n");
|
|
138
|
+
}
|
|
139
|
+
for (const item of items) {
|
|
140
|
+
content = content.replace(
|
|
141
|
+
item.fullInit,
|
|
142
|
+
`costly().wrap(${item.fullInit})`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function addToEnvFile(cwd, apiKey) {
|
|
149
|
+
const envPath = path.join(cwd, ".env");
|
|
150
|
+
let content = "";
|
|
151
|
+
if (fs.existsSync(envPath)) {
|
|
152
|
+
content = fs.readFileSync(envPath, "utf-8");
|
|
153
|
+
if (content.includes("COSTLY_API_KEY=")) {
|
|
154
|
+
content = content.replace(/COSTLY_API_KEY=.*/, `COSTLY_API_KEY=${apiKey}`);
|
|
155
|
+
fs.writeFileSync(envPath, content, "utf-8");
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const newLine = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
160
|
+
fs.writeFileSync(envPath, content + newLine + `COSTLY_API_KEY=${apiKey}
|
|
161
|
+
`, "utf-8");
|
|
162
|
+
}
|
|
163
|
+
async function verifyApiKey(apiKey) {
|
|
164
|
+
try {
|
|
165
|
+
const res = await fetch(VERIFY_URL, {
|
|
166
|
+
method: "POST",
|
|
167
|
+
headers: {
|
|
168
|
+
"Content-Type": "application/json",
|
|
169
|
+
Authorization: `Bearer ${apiKey}`
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
return res.ok;
|
|
173
|
+
} catch {
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async function main() {
|
|
178
|
+
const cwd = process.cwd();
|
|
179
|
+
log("");
|
|
180
|
+
log(` ${BOLD}costly${RESET} ${dim("v0.1.0")}`);
|
|
181
|
+
log("");
|
|
182
|
+
const apiKey = await ask(` ${CYAN}?${RESET} Paste your API key ${dim("(from getcostly.dev/dashboard)")}: `);
|
|
183
|
+
if (!apiKey || !apiKey.startsWith("ck_")) {
|
|
184
|
+
log("");
|
|
185
|
+
error("Invalid API key. It should start with ck_");
|
|
186
|
+
log(` Get yours at ${CYAN}https://getcostly.dev/dashboard${RESET}`);
|
|
187
|
+
log("");
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
const valid = await verifyApiKey(apiKey);
|
|
191
|
+
if (!valid) {
|
|
192
|
+
log("");
|
|
193
|
+
error("API key not recognized. Check your key and try again.");
|
|
194
|
+
log(` Get yours at ${CYAN}https://getcostly.dev/dashboard${RESET}`);
|
|
195
|
+
log("");
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
success("API key verified");
|
|
199
|
+
log("");
|
|
200
|
+
log(` Scanning for Anthropic SDK usage...`);
|
|
201
|
+
log("");
|
|
202
|
+
const found = scanDirectory(cwd, cwd);
|
|
203
|
+
if (found.length === 0) {
|
|
204
|
+
warn("No `new Anthropic()` calls found in your codebase.");
|
|
205
|
+
log("");
|
|
206
|
+
log(` You can add Costly manually:`);
|
|
207
|
+
log("");
|
|
208
|
+
log(` ${DIM}import { costly } from "costly";${RESET}`);
|
|
209
|
+
log(` ${DIM}const client = costly().wrap(new Anthropic());${RESET}`);
|
|
210
|
+
log("");
|
|
211
|
+
const shouldContinue = await confirm(` Install costly and add API key to .env anyway?`);
|
|
212
|
+
if (shouldContinue) {
|
|
213
|
+
const pm2 = detectPackageManager(cwd);
|
|
214
|
+
log("");
|
|
215
|
+
await installPackage(pm2);
|
|
216
|
+
addToEnvFile(cwd, apiKey);
|
|
217
|
+
success("Added COSTLY_API_KEY to .env");
|
|
218
|
+
log("");
|
|
219
|
+
log(` Add the wrapper to your code when you're ready.`);
|
|
220
|
+
}
|
|
221
|
+
log("");
|
|
222
|
+
process.exit(0);
|
|
223
|
+
}
|
|
224
|
+
log(` Found ${found.length} file${found.length === 1 ? "" : "s"} using the Anthropic SDK:`);
|
|
225
|
+
log("");
|
|
226
|
+
for (const f of found) {
|
|
227
|
+
log(` ${f.relativePath}:${f.line}`);
|
|
228
|
+
log(` ${dim(f.match)}`);
|
|
229
|
+
log("");
|
|
230
|
+
}
|
|
231
|
+
const shouldWrap = await confirm(` Wrap ${found.length === 1 ? "this file" : "these files"} with Costly?`);
|
|
232
|
+
if (!shouldWrap) {
|
|
233
|
+
log("");
|
|
234
|
+
log(` No worries. You can add Costly manually:`);
|
|
235
|
+
log(` ${DIM}const client = costly().wrap(new Anthropic());${RESET}`);
|
|
236
|
+
log("");
|
|
237
|
+
process.exit(0);
|
|
238
|
+
}
|
|
239
|
+
log("");
|
|
240
|
+
log(` Here's what will change:`);
|
|
241
|
+
log("");
|
|
242
|
+
for (const f of found) {
|
|
243
|
+
log(` ${BOLD}${f.relativePath}${RESET}`);
|
|
244
|
+
log(` ${"\u2500".repeat(40)}`);
|
|
245
|
+
log(` ${GREEN}+${RESET} import { costly } from "costly";`);
|
|
246
|
+
log(` ${RED}-${RESET} ${f.match}`);
|
|
247
|
+
const wrappedInit = `costly().wrap(${f.fullInit})`;
|
|
248
|
+
const newLine = f.match.replace(f.fullInit, wrappedInit);
|
|
249
|
+
log(` ${GREEN}+${RESET} ${newLine}`);
|
|
250
|
+
log("");
|
|
251
|
+
}
|
|
252
|
+
const shouldApply = await confirm(` Apply changes?`);
|
|
253
|
+
if (!shouldApply) {
|
|
254
|
+
log("");
|
|
255
|
+
log(` Cancelled. No files were modified.`);
|
|
256
|
+
log("");
|
|
257
|
+
process.exit(0);
|
|
258
|
+
}
|
|
259
|
+
log("");
|
|
260
|
+
const pm = detectPackageManager(cwd);
|
|
261
|
+
await installPackage(pm);
|
|
262
|
+
applyChanges(found);
|
|
263
|
+
for (const f of found) {
|
|
264
|
+
success(`Updated ${f.relativePath}`);
|
|
265
|
+
}
|
|
266
|
+
addToEnvFile(cwd, apiKey);
|
|
267
|
+
success("Added COSTLY_API_KEY to .env");
|
|
268
|
+
log("");
|
|
269
|
+
log(` ${GREEN}${BOLD}You're all set.${RESET} Your dashboard will light up within 48 hours.`);
|
|
270
|
+
log(` ${dim("https://getcostly.dev/dashboard")}`);
|
|
271
|
+
log("");
|
|
272
|
+
}
|
|
273
|
+
async function installPackage(pm) {
|
|
274
|
+
const { execSync } = await import("child_process");
|
|
275
|
+
const cmd = installCommand(pm);
|
|
276
|
+
try {
|
|
277
|
+
execSync(cmd, { stdio: "pipe", cwd: process.cwd() });
|
|
278
|
+
success(`Installed costly ${dim(`via ${pm}`)}`);
|
|
279
|
+
} catch {
|
|
280
|
+
warn(`Could not auto-install. Run manually: ${cmd}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function showHelp() {
|
|
284
|
+
log("");
|
|
285
|
+
log(` ${BOLD}costly${RESET} ${dim("v0.1.0")}`);
|
|
286
|
+
log("");
|
|
287
|
+
log(` ${BOLD}Usage:${RESET}`);
|
|
288
|
+
log(` costly init Set up Costly in your project`);
|
|
289
|
+
log("");
|
|
290
|
+
log(` ${BOLD}Learn more:${RESET}`);
|
|
291
|
+
log(` ${CYAN}https://getcostly.dev${RESET}`);
|
|
292
|
+
log("");
|
|
293
|
+
}
|
|
294
|
+
var command = process.argv[2];
|
|
295
|
+
if (command === "init") {
|
|
296
|
+
main().catch((err) => {
|
|
297
|
+
error(err.message || "An unexpected error occurred");
|
|
298
|
+
process.exit(1);
|
|
299
|
+
});
|
|
300
|
+
} else {
|
|
301
|
+
showHelp();
|
|
302
|
+
}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
costly: () => costly
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(index_exports);
|
|
26
|
+
|
|
27
|
+
// src/batcher.ts
|
|
28
|
+
var LogBatcher = class {
|
|
29
|
+
constructor(opts) {
|
|
30
|
+
this.queue = [];
|
|
31
|
+
this.timer = null;
|
|
32
|
+
this.pendingSends = [];
|
|
33
|
+
this.endpoint = opts.endpoint;
|
|
34
|
+
this.apiKey = opts.apiKey;
|
|
35
|
+
this.flushInterval = opts.flushInterval;
|
|
36
|
+
this.flushBatchSize = opts.flushBatchSize;
|
|
37
|
+
this.debug = opts.debug;
|
|
38
|
+
if (typeof process !== "undefined") {
|
|
39
|
+
process.on("beforeExit", () => {
|
|
40
|
+
this.flush();
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
add(log) {
|
|
45
|
+
this.queue.push(log);
|
|
46
|
+
if (this.queue.length >= this.flushBatchSize) {
|
|
47
|
+
this.flush();
|
|
48
|
+
} else if (!this.timer) {
|
|
49
|
+
this.timer = setTimeout(() => this.flush(), this.flushInterval);
|
|
50
|
+
if (this.timer && typeof this.timer === "object" && "unref" in this.timer) {
|
|
51
|
+
this.timer.unref();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
flush() {
|
|
56
|
+
if (this.timer) {
|
|
57
|
+
clearTimeout(this.timer);
|
|
58
|
+
this.timer = null;
|
|
59
|
+
}
|
|
60
|
+
if (this.queue.length === 0) return;
|
|
61
|
+
const batch = this.queue.splice(0);
|
|
62
|
+
const sendPromise = this.send(batch).catch((err) => {
|
|
63
|
+
if (this.debug) {
|
|
64
|
+
console.warn("[costly] Failed to send logs:", err.message);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
this.pendingSends.push(sendPromise);
|
|
68
|
+
sendPromise.finally(() => {
|
|
69
|
+
const idx = this.pendingSends.indexOf(sendPromise);
|
|
70
|
+
if (idx !== -1) this.pendingSends.splice(idx, 1);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
async flushAsync() {
|
|
74
|
+
this.flush();
|
|
75
|
+
await Promise.all(this.pendingSends);
|
|
76
|
+
}
|
|
77
|
+
async shutdown() {
|
|
78
|
+
await this.flushAsync();
|
|
79
|
+
if (this.timer) {
|
|
80
|
+
clearTimeout(this.timer);
|
|
81
|
+
this.timer = null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async send(batch) {
|
|
85
|
+
const res = await fetch(this.endpoint, {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers: {
|
|
88
|
+
"Content-Type": "application/json",
|
|
89
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
90
|
+
},
|
|
91
|
+
body: JSON.stringify({ logs: batch })
|
|
92
|
+
});
|
|
93
|
+
if (!res.ok && this.debug) {
|
|
94
|
+
console.warn(`[costly] Ingest responded ${res.status}: ${res.statusText}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// src/pricing.ts
|
|
100
|
+
var PRICING = {
|
|
101
|
+
// Claude 4.x family
|
|
102
|
+
"claude-opus-4-20250514": { inputPerMillion: 15, outputPerMillion: 75 },
|
|
103
|
+
"claude-sonnet-4-20250514": { inputPerMillion: 3, outputPerMillion: 15 },
|
|
104
|
+
"claude-haiku-4-5-20251001": { inputPerMillion: 0.8, outputPerMillion: 4 },
|
|
105
|
+
// Claude 3.5 family (deprecated but still in use)
|
|
106
|
+
"claude-3-5-sonnet-20241022": { inputPerMillion: 3, outputPerMillion: 15 },
|
|
107
|
+
"claude-3-5-sonnet-20240620": { inputPerMillion: 3, outputPerMillion: 15 },
|
|
108
|
+
"claude-3-5-haiku-20241022": { inputPerMillion: 0.8, outputPerMillion: 4 },
|
|
109
|
+
// Claude 3 family (deprecated)
|
|
110
|
+
"claude-3-opus-20240229": { inputPerMillion: 15, outputPerMillion: 75 },
|
|
111
|
+
"claude-3-sonnet-20240229": { inputPerMillion: 3, outputPerMillion: 15 },
|
|
112
|
+
"claude-3-haiku-20240307": { inputPerMillion: 0.25, outputPerMillion: 1.25 }
|
|
113
|
+
};
|
|
114
|
+
var ALIASES = {
|
|
115
|
+
"claude-opus-4-0": "claude-opus-4-20250514",
|
|
116
|
+
"claude-sonnet-4-0": "claude-sonnet-4-20250514",
|
|
117
|
+
"claude-haiku-4-5-latest": "claude-haiku-4-5-20251001",
|
|
118
|
+
"claude-3-5-sonnet-latest": "claude-3-5-sonnet-20241022",
|
|
119
|
+
"claude-3-5-haiku-latest": "claude-3-5-haiku-20241022",
|
|
120
|
+
"claude-3-opus-latest": "claude-3-opus-20240229",
|
|
121
|
+
"claude-3-sonnet-latest": "claude-3-sonnet-20240229",
|
|
122
|
+
"claude-3-haiku-latest": "claude-3-haiku-20240307"
|
|
123
|
+
};
|
|
124
|
+
function calculateCost(model, inputTokens, outputTokens) {
|
|
125
|
+
const resolvedModel = ALIASES[model] || model;
|
|
126
|
+
const pricing = PRICING[resolvedModel];
|
|
127
|
+
if (!pricing) {
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
const inputCost = inputTokens / 1e6 * pricing.inputPerMillion;
|
|
131
|
+
const outputCost = outputTokens / 1e6 * pricing.outputPerMillion;
|
|
132
|
+
return Math.round((inputCost + outputCost) * 1e6) / 1e6;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/hash.ts
|
|
136
|
+
function hashString(str) {
|
|
137
|
+
let hash = 5381;
|
|
138
|
+
for (let i = 0; i < str.length; i++) {
|
|
139
|
+
hash = (hash << 5) + hash + str.charCodeAt(i) & 4294967295;
|
|
140
|
+
}
|
|
141
|
+
return (hash >>> 0).toString(36);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/callsite.ts
|
|
145
|
+
function getCallSite() {
|
|
146
|
+
const err = new Error();
|
|
147
|
+
const stack = err.stack;
|
|
148
|
+
if (!stack) return null;
|
|
149
|
+
const lines = stack.split("\n");
|
|
150
|
+
for (const line of lines) {
|
|
151
|
+
const trimmed = line.trim();
|
|
152
|
+
if (trimmed.startsWith("Error")) continue;
|
|
153
|
+
if (trimmed.includes("node_modules/costly/") || trimmed.includes("costlyWrappedMethod") || trimmed.includes("wrapMethod") || trimmed.includes("wrapClient") || trimmed.includes("wrapStream") || trimmed.includes("callsite.") || trimmed.includes("batcher.") || trimmed.includes("LogBatcher")) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const match = trimmed.match(/\((.+):(\d+):\d+\)/) || trimmed.match(/at (.+):(\d+):\d+/);
|
|
157
|
+
if (match) {
|
|
158
|
+
const file = match[1].replace(/^.*[/\\]/, "");
|
|
159
|
+
const line2 = match[2];
|
|
160
|
+
return `${file}:${line2}`;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// src/index.ts
|
|
167
|
+
var DEFAULT_ENDPOINT = "https://www.getcostly.dev/api/v1/ingest";
|
|
168
|
+
var DEFAULT_FLUSH_INTERVAL = 5e3;
|
|
169
|
+
var DEFAULT_FLUSH_BATCH_SIZE = 10;
|
|
170
|
+
var INTERCEPTED_METHODS = /* @__PURE__ */ new Set(["create", "stream"]);
|
|
171
|
+
function costly(config) {
|
|
172
|
+
const apiKey = config?.apiKey ?? process.env.COSTLY_API_KEY;
|
|
173
|
+
const projectId = config?.projectId ?? process.env.COSTLY_PROJECT_ID ?? "_";
|
|
174
|
+
if (!apiKey) {
|
|
175
|
+
throw new Error("[costly] apiKey is required \u2014 pass it in config or set COSTLY_API_KEY env var");
|
|
176
|
+
}
|
|
177
|
+
const batcher = new LogBatcher({
|
|
178
|
+
endpoint: config?.endpoint ?? DEFAULT_ENDPOINT,
|
|
179
|
+
apiKey,
|
|
180
|
+
flushInterval: config?.flushInterval ?? DEFAULT_FLUSH_INTERVAL,
|
|
181
|
+
flushBatchSize: config?.flushBatchSize ?? DEFAULT_FLUSH_BATCH_SIZE,
|
|
182
|
+
debug: config?.debug ?? false
|
|
183
|
+
});
|
|
184
|
+
return {
|
|
185
|
+
wrap(client) {
|
|
186
|
+
return wrapClient(client, projectId, batcher);
|
|
187
|
+
},
|
|
188
|
+
flush() {
|
|
189
|
+
return batcher.flushAsync();
|
|
190
|
+
},
|
|
191
|
+
shutdown() {
|
|
192
|
+
return batcher.shutdown();
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
var proxyCache = /* @__PURE__ */ new WeakMap();
|
|
197
|
+
function wrapClient(client, projectId, batcher) {
|
|
198
|
+
const cached = proxyCache.get(client);
|
|
199
|
+
if (cached) return cached;
|
|
200
|
+
const proxy = new Proxy(client, {
|
|
201
|
+
get(target, prop, receiver) {
|
|
202
|
+
const value = Reflect.get(target, prop, receiver);
|
|
203
|
+
if (INTERCEPTED_METHODS.has(prop) && typeof value === "function") {
|
|
204
|
+
return wrapMethod(value.bind(target), projectId, batcher);
|
|
205
|
+
}
|
|
206
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
207
|
+
return wrapClient(value, projectId, batcher);
|
|
208
|
+
}
|
|
209
|
+
return value;
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
proxyCache.set(client, proxy);
|
|
213
|
+
return proxy;
|
|
214
|
+
}
|
|
215
|
+
function wrapMethod(originalMethod, projectId, batcher) {
|
|
216
|
+
return function costlyWrappedMethod(...args) {
|
|
217
|
+
const callSite = getCallSite();
|
|
218
|
+
const startTime = Date.now();
|
|
219
|
+
const params = args[0] ?? {};
|
|
220
|
+
const costlyMeta = params.costly;
|
|
221
|
+
if (costlyMeta) {
|
|
222
|
+
const { costly: _, ...cleanParams } = params;
|
|
223
|
+
args[0] = cleanParams;
|
|
224
|
+
}
|
|
225
|
+
const model = params.model ?? "unknown";
|
|
226
|
+
const maxTokens = params.max_tokens ?? null;
|
|
227
|
+
const messages = params.messages;
|
|
228
|
+
const systemPrompt = params.system;
|
|
229
|
+
const promptContent = messages ? JSON.stringify(messages.map((m) => ({ role: m.role, content: m.content }))) : "";
|
|
230
|
+
const promptHash = hashString(promptContent);
|
|
231
|
+
const systemPromptHash = systemPrompt ? hashString(typeof systemPrompt === "string" ? systemPrompt : JSON.stringify(systemPrompt)) : null;
|
|
232
|
+
const tag = costlyMeta?.tag ?? null;
|
|
233
|
+
const userId = costlyMeta?.userId ?? null;
|
|
234
|
+
const autoCallSite = tag ? null : callSite;
|
|
235
|
+
function buildLog(inputTokens, outputTokens, status, errorType) {
|
|
236
|
+
return {
|
|
237
|
+
projectId,
|
|
238
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
239
|
+
model,
|
|
240
|
+
tag,
|
|
241
|
+
userId,
|
|
242
|
+
inputTokens,
|
|
243
|
+
outputTokens,
|
|
244
|
+
totalCost: calculateCost(model, inputTokens, outputTokens),
|
|
245
|
+
maxTokens,
|
|
246
|
+
status,
|
|
247
|
+
errorType,
|
|
248
|
+
promptHash,
|
|
249
|
+
systemPromptHash,
|
|
250
|
+
callSite: autoCallSite,
|
|
251
|
+
durationMs: Date.now() - startTime
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
let result;
|
|
255
|
+
try {
|
|
256
|
+
result = originalMethod(...args);
|
|
257
|
+
} catch (err) {
|
|
258
|
+
const error = err;
|
|
259
|
+
batcher.add(buildLog(0, 0, "error", error.error?.type ?? error.message ?? "unknown_error"));
|
|
260
|
+
throw err;
|
|
261
|
+
}
|
|
262
|
+
if (result && typeof result.then === "function") {
|
|
263
|
+
result.then(
|
|
264
|
+
(response) => {
|
|
265
|
+
try {
|
|
266
|
+
const res = response;
|
|
267
|
+
const usage = res.usage;
|
|
268
|
+
batcher.add(buildLog(usage?.input_tokens ?? 0, usage?.output_tokens ?? 0, "success", null));
|
|
269
|
+
} catch {
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
(error) => {
|
|
273
|
+
try {
|
|
274
|
+
const err = error;
|
|
275
|
+
batcher.add(buildLog(0, 0, "error", err.error?.type ?? err.message ?? "unknown_error"));
|
|
276
|
+
} catch {
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
if (result && typeof result[Symbol.asyncIterator] === "function") {
|
|
282
|
+
return wrapStream(result, buildLog, batcher);
|
|
283
|
+
}
|
|
284
|
+
return result;
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
function wrapStream(stream, buildLog, batcher) {
|
|
288
|
+
const originalIterator = stream[Symbol.asyncIterator]();
|
|
289
|
+
const wrappedStream = Object.create(stream);
|
|
290
|
+
wrappedStream[Symbol.asyncIterator] = () => {
|
|
291
|
+
let inputTokens = 0;
|
|
292
|
+
let outputTokens = 0;
|
|
293
|
+
return {
|
|
294
|
+
async next() {
|
|
295
|
+
try {
|
|
296
|
+
const result = await originalIterator.next();
|
|
297
|
+
if (result.done) {
|
|
298
|
+
batcher.add(buildLog(inputTokens, outputTokens, "success", null));
|
|
299
|
+
return result;
|
|
300
|
+
}
|
|
301
|
+
const event = result.value;
|
|
302
|
+
if (event.type === "message_delta") {
|
|
303
|
+
const usage = event.usage;
|
|
304
|
+
if (usage?.output_tokens) {
|
|
305
|
+
outputTokens = usage.output_tokens;
|
|
306
|
+
}
|
|
307
|
+
} else if (event.type === "message_start") {
|
|
308
|
+
const message = event.message;
|
|
309
|
+
if (message?.usage?.input_tokens) {
|
|
310
|
+
inputTokens = message.usage.input_tokens;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return result;
|
|
314
|
+
} catch (err) {
|
|
315
|
+
const error = err;
|
|
316
|
+
batcher.add(buildLog(inputTokens, outputTokens, "error", error.error?.type ?? error.message ?? "unknown_error"));
|
|
317
|
+
throw err;
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
async return(value) {
|
|
321
|
+
batcher.add(buildLog(inputTokens, outputTokens, "success", null));
|
|
322
|
+
if (originalIterator.return) {
|
|
323
|
+
return originalIterator.return(value);
|
|
324
|
+
}
|
|
325
|
+
return { done: true, value: void 0 };
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
};
|
|
329
|
+
return wrappedStream;
|
|
330
|
+
}
|
|
331
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
332
|
+
0 && (module.exports = {
|
|
333
|
+
costly
|
|
334
|
+
});
|
|
335
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/batcher.ts","../src/pricing.ts","../src/hash.ts","../src/callsite.ts"],"sourcesContent":["import type { CostlyConfig, CostlyCallMetadata, RequestLog } from \"./types.js\";\nimport { LogBatcher } from \"./batcher.js\";\nimport { calculateCost } from \"./pricing.js\";\nimport { hashString } from \"./hash.js\";\nimport { getCallSite } from \"./callsite.js\";\n\nexport type { CostlyConfig, CostlyCallMetadata, RequestLog };\n\nconst DEFAULT_ENDPOINT = \"https://www.getcostly.dev/api/v1/ingest\";\nconst DEFAULT_FLUSH_INTERVAL = 5000; // 5 seconds\nconst DEFAULT_FLUSH_BATCH_SIZE = 10;\n\n// Methods to intercept on the Anthropic SDK\nconst INTERCEPTED_METHODS = new Set([\"create\", \"stream\"]);\n\ninterface CostlyClient {\n wrap<T extends object>(client: T): T;\n flush(): Promise<void>;\n shutdown(): Promise<void>;\n}\n\nexport function costly(config?: Partial<CostlyConfig>): CostlyClient {\n const apiKey = config?.apiKey ?? process.env.COSTLY_API_KEY;\n const projectId = config?.projectId ?? process.env.COSTLY_PROJECT_ID ?? \"_\";\n\n if (!apiKey) {\n throw new Error(\"[costly] apiKey is required — pass it in config or set COSTLY_API_KEY env var\");\n }\n\n const batcher = new LogBatcher({\n endpoint: config?.endpoint ?? DEFAULT_ENDPOINT,\n apiKey,\n flushInterval: config?.flushInterval ?? DEFAULT_FLUSH_INTERVAL,\n flushBatchSize: config?.flushBatchSize ?? DEFAULT_FLUSH_BATCH_SIZE,\n debug: config?.debug ?? false,\n });\n\n return {\n wrap<T extends object>(client: T): T {\n return wrapClient(client, projectId, batcher);\n },\n flush() {\n return batcher.flushAsync();\n },\n shutdown() {\n return batcher.shutdown();\n },\n };\n}\n\n// Cache proxies to avoid creating a new one on every property access\nconst proxyCache = new WeakMap<object, object>();\n\nfunction wrapClient<T extends object>(\n client: T,\n projectId: string,\n batcher: LogBatcher,\n): T {\n const cached = proxyCache.get(client);\n if (cached) return cached as T;\n\n const proxy = new Proxy(client, {\n get(target, prop, receiver) {\n const value = Reflect.get(target, prop, receiver);\n\n // Wrap intercepted methods (create, stream)\n if (INTERCEPTED_METHODS.has(prop as string) && typeof value === \"function\") {\n return wrapMethod(value.bind(target), projectId, batcher);\n }\n\n // Recursively wrap nested objects (e.g., anthropic.messages)\n if (value && typeof value === \"object\" && !Array.isArray(value)) {\n return wrapClient(value as object, projectId, batcher);\n }\n\n return value;\n },\n });\n\n proxyCache.set(client, proxy);\n return proxy as T;\n}\n\nfunction wrapMethod(\n originalMethod: (...args: unknown[]) => unknown,\n projectId: string,\n batcher: LogBatcher,\n): (...args: unknown[]) => unknown {\n return function costlyWrappedMethod(...args: unknown[]): unknown {\n const callSite = getCallSite();\n const startTime = Date.now();\n\n // Extract and strip costly metadata from the request params\n const params = (args[0] ?? {}) as Record<string, unknown>;\n const costlyMeta = params.costly as CostlyCallMetadata | undefined;\n\n // Create a clean copy without the costly property\n if (costlyMeta) {\n const { costly: _, ...cleanParams } = params;\n args[0] = cleanParams;\n }\n\n // Extract data we need for logging before the call\n const model = (params.model as string) ?? \"unknown\";\n const maxTokens = (params.max_tokens as number) ?? null;\n const messages = params.messages as Array<{ role: string; content: unknown }> | undefined;\n const systemPrompt = params.system;\n\n // Build prompt hash from messages content\n const promptContent = messages\n ? JSON.stringify(messages.map((m) => ({ role: m.role, content: m.content })))\n : \"\";\n const promptHash = hashString(promptContent);\n const systemPromptHash = systemPrompt\n ? hashString(typeof systemPrompt === \"string\" ? systemPrompt : JSON.stringify(systemPrompt))\n : null;\n\n const tag = costlyMeta?.tag ?? null;\n const userId = costlyMeta?.userId ?? null;\n const autoCallSite = tag ? null : callSite;\n\n function buildLog(\n inputTokens: number,\n outputTokens: number,\n status: \"success\" | \"error\",\n errorType: string | null,\n ): RequestLog {\n return {\n projectId,\n timestamp: new Date().toISOString(),\n model,\n tag,\n userId,\n inputTokens,\n outputTokens,\n totalCost: calculateCost(model, inputTokens, outputTokens),\n maxTokens,\n status,\n errorType,\n promptHash,\n systemPromptHash,\n callSite: autoCallSite,\n durationMs: Date.now() - startTime,\n };\n }\n\n let result: unknown;\n try {\n result = originalMethod(...args);\n } catch (err) {\n // Synchronous error — log it but rethrow\n const error = err as { error?: { type?: string }; message?: string };\n batcher.add(buildLog(0, 0, \"error\", error.error?.type ?? error.message ?? \"unknown_error\"));\n throw err;\n }\n\n // Handle thenable results (Promises, Streams with .finalMessage(), etc.)\n if (result && typeof (result as { then?: unknown }).then === \"function\") {\n (result as Promise<unknown>).then(\n (response) => {\n try {\n const res = response as Record<string, unknown>;\n const usage = res.usage as { input_tokens?: number; output_tokens?: number } | undefined;\n batcher.add(buildLog(usage?.input_tokens ?? 0, usage?.output_tokens ?? 0, \"success\", null));\n } catch {\n // Never crash user code from logging\n }\n },\n (error) => {\n try {\n const err = error as { error?: { type?: string }; message?: string };\n batcher.add(buildLog(0, 0, \"error\", err.error?.type ?? err.message ?? \"unknown_error\"));\n } catch {\n // Never crash user code from logging\n }\n },\n );\n }\n\n // If result is a streaming object (has Symbol.asyncIterator), wrap it to capture usage\n if (result && typeof (result as { [Symbol.asyncIterator]?: unknown })[Symbol.asyncIterator] === \"function\") {\n return wrapStream(result as AsyncIterable<unknown>, buildLog, batcher);\n }\n\n return result;\n };\n}\n\nfunction wrapStream(\n stream: AsyncIterable<unknown>,\n buildLog: (input: number, output: number, status: \"success\" | \"error\", errorType: string | null) => RequestLog,\n batcher: LogBatcher,\n): AsyncIterable<unknown> {\n const originalIterator = stream[Symbol.asyncIterator]();\n\n // Preserve all properties of the original stream object (e.g., .finalMessage(), .controller, etc.)\n const wrappedStream = Object.create(stream);\n\n wrappedStream[Symbol.asyncIterator] = () => {\n let inputTokens = 0;\n let outputTokens = 0;\n\n return {\n async next(): Promise<IteratorResult<unknown>> {\n try {\n const result = await originalIterator.next();\n\n if (result.done) {\n // Stream ended — log total usage\n batcher.add(buildLog(inputTokens, outputTokens, \"success\", null));\n return result;\n }\n\n // Accumulate usage from stream events\n const event = result.value as Record<string, unknown>;\n if (event.type === \"message_delta\") {\n const usage = event.usage as { output_tokens?: number } | undefined;\n if (usage?.output_tokens) {\n outputTokens = usage.output_tokens;\n }\n } else if (event.type === \"message_start\") {\n const message = event.message as { usage?: { input_tokens?: number } } | undefined;\n if (message?.usage?.input_tokens) {\n inputTokens = message.usage.input_tokens;\n }\n }\n\n return result;\n } catch (err) {\n const error = err as { error?: { type?: string }; message?: string };\n batcher.add(buildLog(inputTokens, outputTokens, \"error\", error.error?.type ?? error.message ?? \"unknown_error\"));\n throw err;\n }\n },\n async return(value?: unknown): Promise<IteratorResult<unknown>> {\n // Stream was cancelled early — still log what we have\n batcher.add(buildLog(inputTokens, outputTokens, \"success\", null));\n if (originalIterator.return) {\n return originalIterator.return(value);\n }\n return { done: true, value: undefined };\n },\n };\n };\n\n return wrappedStream;\n}\n","import type { RequestLog } from \"./types.js\";\n\nexport class LogBatcher {\n private queue: RequestLog[] = [];\n private timer: ReturnType<typeof setTimeout> | null = null;\n private pendingSends: Promise<void>[] = [];\n private endpoint: string;\n private apiKey: string;\n private flushInterval: number;\n private flushBatchSize: number;\n private debug: boolean;\n\n constructor(opts: {\n endpoint: string;\n apiKey: string;\n flushInterval: number;\n flushBatchSize: number;\n debug: boolean;\n }) {\n this.endpoint = opts.endpoint;\n this.apiKey = opts.apiKey;\n this.flushInterval = opts.flushInterval;\n this.flushBatchSize = opts.flushBatchSize;\n this.debug = opts.debug;\n\n if (typeof process !== \"undefined\") {\n process.on(\"beforeExit\", () => {\n this.flush();\n });\n }\n }\n\n add(log: RequestLog): void {\n this.queue.push(log);\n\n if (this.queue.length >= this.flushBatchSize) {\n this.flush();\n } else if (!this.timer) {\n this.timer = setTimeout(() => this.flush(), this.flushInterval);\n // Don't keep the Node process alive just for logging\n if (this.timer && typeof this.timer === \"object\" && \"unref\" in this.timer) {\n this.timer.unref();\n }\n }\n }\n\n flush(): void {\n if (this.timer) {\n clearTimeout(this.timer);\n this.timer = null;\n }\n\n if (this.queue.length === 0) return;\n\n const batch = this.queue.splice(0);\n\n const sendPromise = this.send(batch).catch((err) => {\n if (this.debug) {\n console.warn(\"[costly] Failed to send logs:\", err.message);\n }\n });\n\n this.pendingSends.push(sendPromise);\n sendPromise.finally(() => {\n const idx = this.pendingSends.indexOf(sendPromise);\n if (idx !== -1) this.pendingSends.splice(idx, 1);\n });\n }\n\n async flushAsync(): Promise<void> {\n this.flush();\n await Promise.all(this.pendingSends);\n }\n\n async shutdown(): Promise<void> {\n await this.flushAsync();\n if (this.timer) {\n clearTimeout(this.timer);\n this.timer = null;\n }\n }\n\n private async send(batch: RequestLog[]): Promise<void> {\n const res = await fetch(this.endpoint, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify({ logs: batch }),\n });\n\n if (!res.ok && this.debug) {\n console.warn(`[costly] Ingest responded ${res.status}: ${res.statusText}`);\n }\n }\n}\n","// Anthropic pricing per million tokens (as of March 2026)\n// Source: https://docs.anthropic.com/en/docs/about-claude/models\n\ninterface ModelPricing {\n inputPerMillion: number;\n outputPerMillion: number;\n}\n\nconst PRICING: Record<string, ModelPricing> = {\n // Claude 4.x family\n \"claude-opus-4-20250514\": { inputPerMillion: 15, outputPerMillion: 75 },\n \"claude-sonnet-4-20250514\": { inputPerMillion: 3, outputPerMillion: 15 },\n \"claude-haiku-4-5-20251001\": { inputPerMillion: 0.8, outputPerMillion: 4 },\n\n // Claude 3.5 family (deprecated but still in use)\n \"claude-3-5-sonnet-20241022\": { inputPerMillion: 3, outputPerMillion: 15 },\n \"claude-3-5-sonnet-20240620\": { inputPerMillion: 3, outputPerMillion: 15 },\n \"claude-3-5-haiku-20241022\": { inputPerMillion: 0.8, outputPerMillion: 4 },\n\n // Claude 3 family (deprecated)\n \"claude-3-opus-20240229\": { inputPerMillion: 15, outputPerMillion: 75 },\n \"claude-3-sonnet-20240229\": { inputPerMillion: 3, outputPerMillion: 15 },\n \"claude-3-haiku-20240307\": { inputPerMillion: 0.25, outputPerMillion: 1.25 },\n};\n\n// Model alias resolution\nconst ALIASES: Record<string, string> = {\n \"claude-opus-4-0\": \"claude-opus-4-20250514\",\n \"claude-sonnet-4-0\": \"claude-sonnet-4-20250514\",\n \"claude-haiku-4-5-latest\": \"claude-haiku-4-5-20251001\",\n \"claude-3-5-sonnet-latest\": \"claude-3-5-sonnet-20241022\",\n \"claude-3-5-haiku-latest\": \"claude-3-5-haiku-20241022\",\n \"claude-3-opus-latest\": \"claude-3-opus-20240229\",\n \"claude-3-sonnet-latest\": \"claude-3-sonnet-20240229\",\n \"claude-3-haiku-latest\": \"claude-3-haiku-20240307\",\n};\n\nexport function calculateCost(\n model: string,\n inputTokens: number,\n outputTokens: number,\n): number {\n const resolvedModel = ALIASES[model] || model;\n const pricing = PRICING[resolvedModel];\n\n if (!pricing) {\n // Unknown model — return 0 rather than crashing the user's app\n return 0;\n }\n\n const inputCost = (inputTokens / 1_000_000) * pricing.inputPerMillion;\n const outputCost = (outputTokens / 1_000_000) * pricing.outputPerMillion;\n\n return Math.round((inputCost + outputCost) * 1_000_000) / 1_000_000; // 6 decimal places\n}\n","// Simple fast hash for prompt deduplication\n// Uses djb2 algorithm — not cryptographic, just for grouping identical prompts\n\nexport function hashString(str: string): string {\n let hash = 5381;\n for (let i = 0; i < str.length; i++) {\n hash = ((hash << 5) + hash + str.charCodeAt(i)) & 0xffffffff;\n }\n return (hash >>> 0).toString(36);\n}\n","// Extract the caller's file + line number from the stack trace\n// Used for auto-tagging when no manual tag is provided\n\nexport function getCallSite(): string | null {\n const err = new Error();\n const stack = err.stack;\n if (!stack) return null;\n\n const lines = stack.split(\"\\n\");\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n // Skip the Error line itself\n if (trimmed.startsWith(\"Error\")) continue;\n\n // Skip costly SDK internals by matching on our known file names\n if (\n trimmed.includes(\"node_modules/costly/\") ||\n trimmed.includes(\"costlyWrappedMethod\") ||\n trimmed.includes(\"wrapMethod\") ||\n trimmed.includes(\"wrapClient\") ||\n trimmed.includes(\"wrapStream\") ||\n trimmed.includes(\"callsite.\") ||\n trimmed.includes(\"batcher.\") ||\n trimmed.includes(\"LogBatcher\")\n ) {\n continue;\n }\n\n // Extract file:line from the stack frame\n const match = trimmed.match(/\\((.+):(\\d+):\\d+\\)/) ||\n trimmed.match(/at (.+):(\\d+):\\d+/);\n\n if (match) {\n const file = match[1].replace(/^.*[/\\\\]/, \"\"); // basename only\n const line = match[2];\n return `${file}:${line}`;\n }\n }\n\n return null;\n}\n"],"mappings":"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","names":["line"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
interface CostlyConfig {
|
|
2
|
+
projectId?: string;
|
|
3
|
+
apiKey: string;
|
|
4
|
+
endpoint?: string;
|
|
5
|
+
flushInterval?: number;
|
|
6
|
+
flushBatchSize?: number;
|
|
7
|
+
debug?: boolean;
|
|
8
|
+
}
|
|
9
|
+
interface CostlyCallMetadata {
|
|
10
|
+
tag?: string;
|
|
11
|
+
userId?: string;
|
|
12
|
+
}
|
|
13
|
+
interface RequestLog {
|
|
14
|
+
projectId: string;
|
|
15
|
+
timestamp: string;
|
|
16
|
+
model: string;
|
|
17
|
+
tag: string | null;
|
|
18
|
+
userId: string | null;
|
|
19
|
+
inputTokens: number;
|
|
20
|
+
outputTokens: number;
|
|
21
|
+
totalCost: number;
|
|
22
|
+
maxTokens: number | null;
|
|
23
|
+
status: "success" | "error";
|
|
24
|
+
errorType: string | null;
|
|
25
|
+
promptHash: string;
|
|
26
|
+
systemPromptHash: string | null;
|
|
27
|
+
callSite: string | null;
|
|
28
|
+
durationMs: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface CostlyClient {
|
|
32
|
+
wrap<T extends object>(client: T): T;
|
|
33
|
+
flush(): Promise<void>;
|
|
34
|
+
shutdown(): Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
declare function costly(config?: Partial<CostlyConfig>): CostlyClient;
|
|
37
|
+
|
|
38
|
+
export { type CostlyCallMetadata, type CostlyConfig, type RequestLog, costly };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
interface CostlyConfig {
|
|
2
|
+
projectId?: string;
|
|
3
|
+
apiKey: string;
|
|
4
|
+
endpoint?: string;
|
|
5
|
+
flushInterval?: number;
|
|
6
|
+
flushBatchSize?: number;
|
|
7
|
+
debug?: boolean;
|
|
8
|
+
}
|
|
9
|
+
interface CostlyCallMetadata {
|
|
10
|
+
tag?: string;
|
|
11
|
+
userId?: string;
|
|
12
|
+
}
|
|
13
|
+
interface RequestLog {
|
|
14
|
+
projectId: string;
|
|
15
|
+
timestamp: string;
|
|
16
|
+
model: string;
|
|
17
|
+
tag: string | null;
|
|
18
|
+
userId: string | null;
|
|
19
|
+
inputTokens: number;
|
|
20
|
+
outputTokens: number;
|
|
21
|
+
totalCost: number;
|
|
22
|
+
maxTokens: number | null;
|
|
23
|
+
status: "success" | "error";
|
|
24
|
+
errorType: string | null;
|
|
25
|
+
promptHash: string;
|
|
26
|
+
systemPromptHash: string | null;
|
|
27
|
+
callSite: string | null;
|
|
28
|
+
durationMs: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface CostlyClient {
|
|
32
|
+
wrap<T extends object>(client: T): T;
|
|
33
|
+
flush(): Promise<void>;
|
|
34
|
+
shutdown(): Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
declare function costly(config?: Partial<CostlyConfig>): CostlyClient;
|
|
37
|
+
|
|
38
|
+
export { type CostlyCallMetadata, type CostlyConfig, type RequestLog, costly };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
// src/batcher.ts
|
|
2
|
+
var LogBatcher = class {
|
|
3
|
+
constructor(opts) {
|
|
4
|
+
this.queue = [];
|
|
5
|
+
this.timer = null;
|
|
6
|
+
this.pendingSends = [];
|
|
7
|
+
this.endpoint = opts.endpoint;
|
|
8
|
+
this.apiKey = opts.apiKey;
|
|
9
|
+
this.flushInterval = opts.flushInterval;
|
|
10
|
+
this.flushBatchSize = opts.flushBatchSize;
|
|
11
|
+
this.debug = opts.debug;
|
|
12
|
+
if (typeof process !== "undefined") {
|
|
13
|
+
process.on("beforeExit", () => {
|
|
14
|
+
this.flush();
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
add(log) {
|
|
19
|
+
this.queue.push(log);
|
|
20
|
+
if (this.queue.length >= this.flushBatchSize) {
|
|
21
|
+
this.flush();
|
|
22
|
+
} else if (!this.timer) {
|
|
23
|
+
this.timer = setTimeout(() => this.flush(), this.flushInterval);
|
|
24
|
+
if (this.timer && typeof this.timer === "object" && "unref" in this.timer) {
|
|
25
|
+
this.timer.unref();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
flush() {
|
|
30
|
+
if (this.timer) {
|
|
31
|
+
clearTimeout(this.timer);
|
|
32
|
+
this.timer = null;
|
|
33
|
+
}
|
|
34
|
+
if (this.queue.length === 0) return;
|
|
35
|
+
const batch = this.queue.splice(0);
|
|
36
|
+
const sendPromise = this.send(batch).catch((err) => {
|
|
37
|
+
if (this.debug) {
|
|
38
|
+
console.warn("[costly] Failed to send logs:", err.message);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
this.pendingSends.push(sendPromise);
|
|
42
|
+
sendPromise.finally(() => {
|
|
43
|
+
const idx = this.pendingSends.indexOf(sendPromise);
|
|
44
|
+
if (idx !== -1) this.pendingSends.splice(idx, 1);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
async flushAsync() {
|
|
48
|
+
this.flush();
|
|
49
|
+
await Promise.all(this.pendingSends);
|
|
50
|
+
}
|
|
51
|
+
async shutdown() {
|
|
52
|
+
await this.flushAsync();
|
|
53
|
+
if (this.timer) {
|
|
54
|
+
clearTimeout(this.timer);
|
|
55
|
+
this.timer = null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async send(batch) {
|
|
59
|
+
const res = await fetch(this.endpoint, {
|
|
60
|
+
method: "POST",
|
|
61
|
+
headers: {
|
|
62
|
+
"Content-Type": "application/json",
|
|
63
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
64
|
+
},
|
|
65
|
+
body: JSON.stringify({ logs: batch })
|
|
66
|
+
});
|
|
67
|
+
if (!res.ok && this.debug) {
|
|
68
|
+
console.warn(`[costly] Ingest responded ${res.status}: ${res.statusText}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// src/pricing.ts
|
|
74
|
+
var PRICING = {
|
|
75
|
+
// Claude 4.x family
|
|
76
|
+
"claude-opus-4-20250514": { inputPerMillion: 15, outputPerMillion: 75 },
|
|
77
|
+
"claude-sonnet-4-20250514": { inputPerMillion: 3, outputPerMillion: 15 },
|
|
78
|
+
"claude-haiku-4-5-20251001": { inputPerMillion: 0.8, outputPerMillion: 4 },
|
|
79
|
+
// Claude 3.5 family (deprecated but still in use)
|
|
80
|
+
"claude-3-5-sonnet-20241022": { inputPerMillion: 3, outputPerMillion: 15 },
|
|
81
|
+
"claude-3-5-sonnet-20240620": { inputPerMillion: 3, outputPerMillion: 15 },
|
|
82
|
+
"claude-3-5-haiku-20241022": { inputPerMillion: 0.8, outputPerMillion: 4 },
|
|
83
|
+
// Claude 3 family (deprecated)
|
|
84
|
+
"claude-3-opus-20240229": { inputPerMillion: 15, outputPerMillion: 75 },
|
|
85
|
+
"claude-3-sonnet-20240229": { inputPerMillion: 3, outputPerMillion: 15 },
|
|
86
|
+
"claude-3-haiku-20240307": { inputPerMillion: 0.25, outputPerMillion: 1.25 }
|
|
87
|
+
};
|
|
88
|
+
var ALIASES = {
|
|
89
|
+
"claude-opus-4-0": "claude-opus-4-20250514",
|
|
90
|
+
"claude-sonnet-4-0": "claude-sonnet-4-20250514",
|
|
91
|
+
"claude-haiku-4-5-latest": "claude-haiku-4-5-20251001",
|
|
92
|
+
"claude-3-5-sonnet-latest": "claude-3-5-sonnet-20241022",
|
|
93
|
+
"claude-3-5-haiku-latest": "claude-3-5-haiku-20241022",
|
|
94
|
+
"claude-3-opus-latest": "claude-3-opus-20240229",
|
|
95
|
+
"claude-3-sonnet-latest": "claude-3-sonnet-20240229",
|
|
96
|
+
"claude-3-haiku-latest": "claude-3-haiku-20240307"
|
|
97
|
+
};
|
|
98
|
+
function calculateCost(model, inputTokens, outputTokens) {
|
|
99
|
+
const resolvedModel = ALIASES[model] || model;
|
|
100
|
+
const pricing = PRICING[resolvedModel];
|
|
101
|
+
if (!pricing) {
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
const inputCost = inputTokens / 1e6 * pricing.inputPerMillion;
|
|
105
|
+
const outputCost = outputTokens / 1e6 * pricing.outputPerMillion;
|
|
106
|
+
return Math.round((inputCost + outputCost) * 1e6) / 1e6;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/hash.ts
|
|
110
|
+
function hashString(str) {
|
|
111
|
+
let hash = 5381;
|
|
112
|
+
for (let i = 0; i < str.length; i++) {
|
|
113
|
+
hash = (hash << 5) + hash + str.charCodeAt(i) & 4294967295;
|
|
114
|
+
}
|
|
115
|
+
return (hash >>> 0).toString(36);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/callsite.ts
|
|
119
|
+
function getCallSite() {
|
|
120
|
+
const err = new Error();
|
|
121
|
+
const stack = err.stack;
|
|
122
|
+
if (!stack) return null;
|
|
123
|
+
const lines = stack.split("\n");
|
|
124
|
+
for (const line of lines) {
|
|
125
|
+
const trimmed = line.trim();
|
|
126
|
+
if (trimmed.startsWith("Error")) continue;
|
|
127
|
+
if (trimmed.includes("node_modules/costly/") || trimmed.includes("costlyWrappedMethod") || trimmed.includes("wrapMethod") || trimmed.includes("wrapClient") || trimmed.includes("wrapStream") || trimmed.includes("callsite.") || trimmed.includes("batcher.") || trimmed.includes("LogBatcher")) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const match = trimmed.match(/\((.+):(\d+):\d+\)/) || trimmed.match(/at (.+):(\d+):\d+/);
|
|
131
|
+
if (match) {
|
|
132
|
+
const file = match[1].replace(/^.*[/\\]/, "");
|
|
133
|
+
const line2 = match[2];
|
|
134
|
+
return `${file}:${line2}`;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/index.ts
|
|
141
|
+
var DEFAULT_ENDPOINT = "https://www.getcostly.dev/api/v1/ingest";
|
|
142
|
+
var DEFAULT_FLUSH_INTERVAL = 5e3;
|
|
143
|
+
var DEFAULT_FLUSH_BATCH_SIZE = 10;
|
|
144
|
+
var INTERCEPTED_METHODS = /* @__PURE__ */ new Set(["create", "stream"]);
|
|
145
|
+
function costly(config) {
|
|
146
|
+
const apiKey = config?.apiKey ?? process.env.COSTLY_API_KEY;
|
|
147
|
+
const projectId = config?.projectId ?? process.env.COSTLY_PROJECT_ID ?? "_";
|
|
148
|
+
if (!apiKey) {
|
|
149
|
+
throw new Error("[costly] apiKey is required \u2014 pass it in config or set COSTLY_API_KEY env var");
|
|
150
|
+
}
|
|
151
|
+
const batcher = new LogBatcher({
|
|
152
|
+
endpoint: config?.endpoint ?? DEFAULT_ENDPOINT,
|
|
153
|
+
apiKey,
|
|
154
|
+
flushInterval: config?.flushInterval ?? DEFAULT_FLUSH_INTERVAL,
|
|
155
|
+
flushBatchSize: config?.flushBatchSize ?? DEFAULT_FLUSH_BATCH_SIZE,
|
|
156
|
+
debug: config?.debug ?? false
|
|
157
|
+
});
|
|
158
|
+
return {
|
|
159
|
+
wrap(client) {
|
|
160
|
+
return wrapClient(client, projectId, batcher);
|
|
161
|
+
},
|
|
162
|
+
flush() {
|
|
163
|
+
return batcher.flushAsync();
|
|
164
|
+
},
|
|
165
|
+
shutdown() {
|
|
166
|
+
return batcher.shutdown();
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
var proxyCache = /* @__PURE__ */ new WeakMap();
|
|
171
|
+
function wrapClient(client, projectId, batcher) {
|
|
172
|
+
const cached = proxyCache.get(client);
|
|
173
|
+
if (cached) return cached;
|
|
174
|
+
const proxy = new Proxy(client, {
|
|
175
|
+
get(target, prop, receiver) {
|
|
176
|
+
const value = Reflect.get(target, prop, receiver);
|
|
177
|
+
if (INTERCEPTED_METHODS.has(prop) && typeof value === "function") {
|
|
178
|
+
return wrapMethod(value.bind(target), projectId, batcher);
|
|
179
|
+
}
|
|
180
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
181
|
+
return wrapClient(value, projectId, batcher);
|
|
182
|
+
}
|
|
183
|
+
return value;
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
proxyCache.set(client, proxy);
|
|
187
|
+
return proxy;
|
|
188
|
+
}
|
|
189
|
+
function wrapMethod(originalMethod, projectId, batcher) {
|
|
190
|
+
return function costlyWrappedMethod(...args) {
|
|
191
|
+
const callSite = getCallSite();
|
|
192
|
+
const startTime = Date.now();
|
|
193
|
+
const params = args[0] ?? {};
|
|
194
|
+
const costlyMeta = params.costly;
|
|
195
|
+
if (costlyMeta) {
|
|
196
|
+
const { costly: _, ...cleanParams } = params;
|
|
197
|
+
args[0] = cleanParams;
|
|
198
|
+
}
|
|
199
|
+
const model = params.model ?? "unknown";
|
|
200
|
+
const maxTokens = params.max_tokens ?? null;
|
|
201
|
+
const messages = params.messages;
|
|
202
|
+
const systemPrompt = params.system;
|
|
203
|
+
const promptContent = messages ? JSON.stringify(messages.map((m) => ({ role: m.role, content: m.content }))) : "";
|
|
204
|
+
const promptHash = hashString(promptContent);
|
|
205
|
+
const systemPromptHash = systemPrompt ? hashString(typeof systemPrompt === "string" ? systemPrompt : JSON.stringify(systemPrompt)) : null;
|
|
206
|
+
const tag = costlyMeta?.tag ?? null;
|
|
207
|
+
const userId = costlyMeta?.userId ?? null;
|
|
208
|
+
const autoCallSite = tag ? null : callSite;
|
|
209
|
+
function buildLog(inputTokens, outputTokens, status, errorType) {
|
|
210
|
+
return {
|
|
211
|
+
projectId,
|
|
212
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
213
|
+
model,
|
|
214
|
+
tag,
|
|
215
|
+
userId,
|
|
216
|
+
inputTokens,
|
|
217
|
+
outputTokens,
|
|
218
|
+
totalCost: calculateCost(model, inputTokens, outputTokens),
|
|
219
|
+
maxTokens,
|
|
220
|
+
status,
|
|
221
|
+
errorType,
|
|
222
|
+
promptHash,
|
|
223
|
+
systemPromptHash,
|
|
224
|
+
callSite: autoCallSite,
|
|
225
|
+
durationMs: Date.now() - startTime
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
let result;
|
|
229
|
+
try {
|
|
230
|
+
result = originalMethod(...args);
|
|
231
|
+
} catch (err) {
|
|
232
|
+
const error = err;
|
|
233
|
+
batcher.add(buildLog(0, 0, "error", error.error?.type ?? error.message ?? "unknown_error"));
|
|
234
|
+
throw err;
|
|
235
|
+
}
|
|
236
|
+
if (result && typeof result.then === "function") {
|
|
237
|
+
result.then(
|
|
238
|
+
(response) => {
|
|
239
|
+
try {
|
|
240
|
+
const res = response;
|
|
241
|
+
const usage = res.usage;
|
|
242
|
+
batcher.add(buildLog(usage?.input_tokens ?? 0, usage?.output_tokens ?? 0, "success", null));
|
|
243
|
+
} catch {
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
(error) => {
|
|
247
|
+
try {
|
|
248
|
+
const err = error;
|
|
249
|
+
batcher.add(buildLog(0, 0, "error", err.error?.type ?? err.message ?? "unknown_error"));
|
|
250
|
+
} catch {
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
if (result && typeof result[Symbol.asyncIterator] === "function") {
|
|
256
|
+
return wrapStream(result, buildLog, batcher);
|
|
257
|
+
}
|
|
258
|
+
return result;
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function wrapStream(stream, buildLog, batcher) {
|
|
262
|
+
const originalIterator = stream[Symbol.asyncIterator]();
|
|
263
|
+
const wrappedStream = Object.create(stream);
|
|
264
|
+
wrappedStream[Symbol.asyncIterator] = () => {
|
|
265
|
+
let inputTokens = 0;
|
|
266
|
+
let outputTokens = 0;
|
|
267
|
+
return {
|
|
268
|
+
async next() {
|
|
269
|
+
try {
|
|
270
|
+
const result = await originalIterator.next();
|
|
271
|
+
if (result.done) {
|
|
272
|
+
batcher.add(buildLog(inputTokens, outputTokens, "success", null));
|
|
273
|
+
return result;
|
|
274
|
+
}
|
|
275
|
+
const event = result.value;
|
|
276
|
+
if (event.type === "message_delta") {
|
|
277
|
+
const usage = event.usage;
|
|
278
|
+
if (usage?.output_tokens) {
|
|
279
|
+
outputTokens = usage.output_tokens;
|
|
280
|
+
}
|
|
281
|
+
} else if (event.type === "message_start") {
|
|
282
|
+
const message = event.message;
|
|
283
|
+
if (message?.usage?.input_tokens) {
|
|
284
|
+
inputTokens = message.usage.input_tokens;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return result;
|
|
288
|
+
} catch (err) {
|
|
289
|
+
const error = err;
|
|
290
|
+
batcher.add(buildLog(inputTokens, outputTokens, "error", error.error?.type ?? error.message ?? "unknown_error"));
|
|
291
|
+
throw err;
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
async return(value) {
|
|
295
|
+
batcher.add(buildLog(inputTokens, outputTokens, "success", null));
|
|
296
|
+
if (originalIterator.return) {
|
|
297
|
+
return originalIterator.return(value);
|
|
298
|
+
}
|
|
299
|
+
return { done: true, value: void 0 };
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
};
|
|
303
|
+
return wrappedStream;
|
|
304
|
+
}
|
|
305
|
+
export {
|
|
306
|
+
costly
|
|
307
|
+
};
|
|
308
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/batcher.ts","../src/pricing.ts","../src/hash.ts","../src/callsite.ts","../src/index.ts"],"sourcesContent":["import type { RequestLog } from \"./types.js\";\n\nexport class LogBatcher {\n private queue: RequestLog[] = [];\n private timer: ReturnType<typeof setTimeout> | null = null;\n private pendingSends: Promise<void>[] = [];\n private endpoint: string;\n private apiKey: string;\n private flushInterval: number;\n private flushBatchSize: number;\n private debug: boolean;\n\n constructor(opts: {\n endpoint: string;\n apiKey: string;\n flushInterval: number;\n flushBatchSize: number;\n debug: boolean;\n }) {\n this.endpoint = opts.endpoint;\n this.apiKey = opts.apiKey;\n this.flushInterval = opts.flushInterval;\n this.flushBatchSize = opts.flushBatchSize;\n this.debug = opts.debug;\n\n if (typeof process !== \"undefined\") {\n process.on(\"beforeExit\", () => {\n this.flush();\n });\n }\n }\n\n add(log: RequestLog): void {\n this.queue.push(log);\n\n if (this.queue.length >= this.flushBatchSize) {\n this.flush();\n } else if (!this.timer) {\n this.timer = setTimeout(() => this.flush(), this.flushInterval);\n // Don't keep the Node process alive just for logging\n if (this.timer && typeof this.timer === \"object\" && \"unref\" in this.timer) {\n this.timer.unref();\n }\n }\n }\n\n flush(): void {\n if (this.timer) {\n clearTimeout(this.timer);\n this.timer = null;\n }\n\n if (this.queue.length === 0) return;\n\n const batch = this.queue.splice(0);\n\n const sendPromise = this.send(batch).catch((err) => {\n if (this.debug) {\n console.warn(\"[costly] Failed to send logs:\", err.message);\n }\n });\n\n this.pendingSends.push(sendPromise);\n sendPromise.finally(() => {\n const idx = this.pendingSends.indexOf(sendPromise);\n if (idx !== -1) this.pendingSends.splice(idx, 1);\n });\n }\n\n async flushAsync(): Promise<void> {\n this.flush();\n await Promise.all(this.pendingSends);\n }\n\n async shutdown(): Promise<void> {\n await this.flushAsync();\n if (this.timer) {\n clearTimeout(this.timer);\n this.timer = null;\n }\n }\n\n private async send(batch: RequestLog[]): Promise<void> {\n const res = await fetch(this.endpoint, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify({ logs: batch }),\n });\n\n if (!res.ok && this.debug) {\n console.warn(`[costly] Ingest responded ${res.status}: ${res.statusText}`);\n }\n }\n}\n","// Anthropic pricing per million tokens (as of March 2026)\n// Source: https://docs.anthropic.com/en/docs/about-claude/models\n\ninterface ModelPricing {\n inputPerMillion: number;\n outputPerMillion: number;\n}\n\nconst PRICING: Record<string, ModelPricing> = {\n // Claude 4.x family\n \"claude-opus-4-20250514\": { inputPerMillion: 15, outputPerMillion: 75 },\n \"claude-sonnet-4-20250514\": { inputPerMillion: 3, outputPerMillion: 15 },\n \"claude-haiku-4-5-20251001\": { inputPerMillion: 0.8, outputPerMillion: 4 },\n\n // Claude 3.5 family (deprecated but still in use)\n \"claude-3-5-sonnet-20241022\": { inputPerMillion: 3, outputPerMillion: 15 },\n \"claude-3-5-sonnet-20240620\": { inputPerMillion: 3, outputPerMillion: 15 },\n \"claude-3-5-haiku-20241022\": { inputPerMillion: 0.8, outputPerMillion: 4 },\n\n // Claude 3 family (deprecated)\n \"claude-3-opus-20240229\": { inputPerMillion: 15, outputPerMillion: 75 },\n \"claude-3-sonnet-20240229\": { inputPerMillion: 3, outputPerMillion: 15 },\n \"claude-3-haiku-20240307\": { inputPerMillion: 0.25, outputPerMillion: 1.25 },\n};\n\n// Model alias resolution\nconst ALIASES: Record<string, string> = {\n \"claude-opus-4-0\": \"claude-opus-4-20250514\",\n \"claude-sonnet-4-0\": \"claude-sonnet-4-20250514\",\n \"claude-haiku-4-5-latest\": \"claude-haiku-4-5-20251001\",\n \"claude-3-5-sonnet-latest\": \"claude-3-5-sonnet-20241022\",\n \"claude-3-5-haiku-latest\": \"claude-3-5-haiku-20241022\",\n \"claude-3-opus-latest\": \"claude-3-opus-20240229\",\n \"claude-3-sonnet-latest\": \"claude-3-sonnet-20240229\",\n \"claude-3-haiku-latest\": \"claude-3-haiku-20240307\",\n};\n\nexport function calculateCost(\n model: string,\n inputTokens: number,\n outputTokens: number,\n): number {\n const resolvedModel = ALIASES[model] || model;\n const pricing = PRICING[resolvedModel];\n\n if (!pricing) {\n // Unknown model — return 0 rather than crashing the user's app\n return 0;\n }\n\n const inputCost = (inputTokens / 1_000_000) * pricing.inputPerMillion;\n const outputCost = (outputTokens / 1_000_000) * pricing.outputPerMillion;\n\n return Math.round((inputCost + outputCost) * 1_000_000) / 1_000_000; // 6 decimal places\n}\n","// Simple fast hash for prompt deduplication\n// Uses djb2 algorithm — not cryptographic, just for grouping identical prompts\n\nexport function hashString(str: string): string {\n let hash = 5381;\n for (let i = 0; i < str.length; i++) {\n hash = ((hash << 5) + hash + str.charCodeAt(i)) & 0xffffffff;\n }\n return (hash >>> 0).toString(36);\n}\n","// Extract the caller's file + line number from the stack trace\n// Used for auto-tagging when no manual tag is provided\n\nexport function getCallSite(): string | null {\n const err = new Error();\n const stack = err.stack;\n if (!stack) return null;\n\n const lines = stack.split(\"\\n\");\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n // Skip the Error line itself\n if (trimmed.startsWith(\"Error\")) continue;\n\n // Skip costly SDK internals by matching on our known file names\n if (\n trimmed.includes(\"node_modules/costly/\") ||\n trimmed.includes(\"costlyWrappedMethod\") ||\n trimmed.includes(\"wrapMethod\") ||\n trimmed.includes(\"wrapClient\") ||\n trimmed.includes(\"wrapStream\") ||\n trimmed.includes(\"callsite.\") ||\n trimmed.includes(\"batcher.\") ||\n trimmed.includes(\"LogBatcher\")\n ) {\n continue;\n }\n\n // Extract file:line from the stack frame\n const match = trimmed.match(/\\((.+):(\\d+):\\d+\\)/) ||\n trimmed.match(/at (.+):(\\d+):\\d+/);\n\n if (match) {\n const file = match[1].replace(/^.*[/\\\\]/, \"\"); // basename only\n const line = match[2];\n return `${file}:${line}`;\n }\n }\n\n return null;\n}\n","import type { CostlyConfig, CostlyCallMetadata, RequestLog } from \"./types.js\";\nimport { LogBatcher } from \"./batcher.js\";\nimport { calculateCost } from \"./pricing.js\";\nimport { hashString } from \"./hash.js\";\nimport { getCallSite } from \"./callsite.js\";\n\nexport type { CostlyConfig, CostlyCallMetadata, RequestLog };\n\nconst DEFAULT_ENDPOINT = \"https://www.getcostly.dev/api/v1/ingest\";\nconst DEFAULT_FLUSH_INTERVAL = 5000; // 5 seconds\nconst DEFAULT_FLUSH_BATCH_SIZE = 10;\n\n// Methods to intercept on the Anthropic SDK\nconst INTERCEPTED_METHODS = new Set([\"create\", \"stream\"]);\n\ninterface CostlyClient {\n wrap<T extends object>(client: T): T;\n flush(): Promise<void>;\n shutdown(): Promise<void>;\n}\n\nexport function costly(config?: Partial<CostlyConfig>): CostlyClient {\n const apiKey = config?.apiKey ?? process.env.COSTLY_API_KEY;\n const projectId = config?.projectId ?? process.env.COSTLY_PROJECT_ID ?? \"_\";\n\n if (!apiKey) {\n throw new Error(\"[costly] apiKey is required — pass it in config or set COSTLY_API_KEY env var\");\n }\n\n const batcher = new LogBatcher({\n endpoint: config?.endpoint ?? DEFAULT_ENDPOINT,\n apiKey,\n flushInterval: config?.flushInterval ?? DEFAULT_FLUSH_INTERVAL,\n flushBatchSize: config?.flushBatchSize ?? DEFAULT_FLUSH_BATCH_SIZE,\n debug: config?.debug ?? false,\n });\n\n return {\n wrap<T extends object>(client: T): T {\n return wrapClient(client, projectId, batcher);\n },\n flush() {\n return batcher.flushAsync();\n },\n shutdown() {\n return batcher.shutdown();\n },\n };\n}\n\n// Cache proxies to avoid creating a new one on every property access\nconst proxyCache = new WeakMap<object, object>();\n\nfunction wrapClient<T extends object>(\n client: T,\n projectId: string,\n batcher: LogBatcher,\n): T {\n const cached = proxyCache.get(client);\n if (cached) return cached as T;\n\n const proxy = new Proxy(client, {\n get(target, prop, receiver) {\n const value = Reflect.get(target, prop, receiver);\n\n // Wrap intercepted methods (create, stream)\n if (INTERCEPTED_METHODS.has(prop as string) && typeof value === \"function\") {\n return wrapMethod(value.bind(target), projectId, batcher);\n }\n\n // Recursively wrap nested objects (e.g., anthropic.messages)\n if (value && typeof value === \"object\" && !Array.isArray(value)) {\n return wrapClient(value as object, projectId, batcher);\n }\n\n return value;\n },\n });\n\n proxyCache.set(client, proxy);\n return proxy as T;\n}\n\nfunction wrapMethod(\n originalMethod: (...args: unknown[]) => unknown,\n projectId: string,\n batcher: LogBatcher,\n): (...args: unknown[]) => unknown {\n return function costlyWrappedMethod(...args: unknown[]): unknown {\n const callSite = getCallSite();\n const startTime = Date.now();\n\n // Extract and strip costly metadata from the request params\n const params = (args[0] ?? {}) as Record<string, unknown>;\n const costlyMeta = params.costly as CostlyCallMetadata | undefined;\n\n // Create a clean copy without the costly property\n if (costlyMeta) {\n const { costly: _, ...cleanParams } = params;\n args[0] = cleanParams;\n }\n\n // Extract data we need for logging before the call\n const model = (params.model as string) ?? \"unknown\";\n const maxTokens = (params.max_tokens as number) ?? null;\n const messages = params.messages as Array<{ role: string; content: unknown }> | undefined;\n const systemPrompt = params.system;\n\n // Build prompt hash from messages content\n const promptContent = messages\n ? JSON.stringify(messages.map((m) => ({ role: m.role, content: m.content })))\n : \"\";\n const promptHash = hashString(promptContent);\n const systemPromptHash = systemPrompt\n ? hashString(typeof systemPrompt === \"string\" ? systemPrompt : JSON.stringify(systemPrompt))\n : null;\n\n const tag = costlyMeta?.tag ?? null;\n const userId = costlyMeta?.userId ?? null;\n const autoCallSite = tag ? null : callSite;\n\n function buildLog(\n inputTokens: number,\n outputTokens: number,\n status: \"success\" | \"error\",\n errorType: string | null,\n ): RequestLog {\n return {\n projectId,\n timestamp: new Date().toISOString(),\n model,\n tag,\n userId,\n inputTokens,\n outputTokens,\n totalCost: calculateCost(model, inputTokens, outputTokens),\n maxTokens,\n status,\n errorType,\n promptHash,\n systemPromptHash,\n callSite: autoCallSite,\n durationMs: Date.now() - startTime,\n };\n }\n\n let result: unknown;\n try {\n result = originalMethod(...args);\n } catch (err) {\n // Synchronous error — log it but rethrow\n const error = err as { error?: { type?: string }; message?: string };\n batcher.add(buildLog(0, 0, \"error\", error.error?.type ?? error.message ?? \"unknown_error\"));\n throw err;\n }\n\n // Handle thenable results (Promises, Streams with .finalMessage(), etc.)\n if (result && typeof (result as { then?: unknown }).then === \"function\") {\n (result as Promise<unknown>).then(\n (response) => {\n try {\n const res = response as Record<string, unknown>;\n const usage = res.usage as { input_tokens?: number; output_tokens?: number } | undefined;\n batcher.add(buildLog(usage?.input_tokens ?? 0, usage?.output_tokens ?? 0, \"success\", null));\n } catch {\n // Never crash user code from logging\n }\n },\n (error) => {\n try {\n const err = error as { error?: { type?: string }; message?: string };\n batcher.add(buildLog(0, 0, \"error\", err.error?.type ?? err.message ?? \"unknown_error\"));\n } catch {\n // Never crash user code from logging\n }\n },\n );\n }\n\n // If result is a streaming object (has Symbol.asyncIterator), wrap it to capture usage\n if (result && typeof (result as { [Symbol.asyncIterator]?: unknown })[Symbol.asyncIterator] === \"function\") {\n return wrapStream(result as AsyncIterable<unknown>, buildLog, batcher);\n }\n\n return result;\n };\n}\n\nfunction wrapStream(\n stream: AsyncIterable<unknown>,\n buildLog: (input: number, output: number, status: \"success\" | \"error\", errorType: string | null) => RequestLog,\n batcher: LogBatcher,\n): AsyncIterable<unknown> {\n const originalIterator = stream[Symbol.asyncIterator]();\n\n // Preserve all properties of the original stream object (e.g., .finalMessage(), .controller, etc.)\n const wrappedStream = Object.create(stream);\n\n wrappedStream[Symbol.asyncIterator] = () => {\n let inputTokens = 0;\n let outputTokens = 0;\n\n return {\n async next(): Promise<IteratorResult<unknown>> {\n try {\n const result = await originalIterator.next();\n\n if (result.done) {\n // Stream ended — log total usage\n batcher.add(buildLog(inputTokens, outputTokens, \"success\", null));\n return result;\n }\n\n // Accumulate usage from stream events\n const event = result.value as Record<string, unknown>;\n if (event.type === \"message_delta\") {\n const usage = event.usage as { output_tokens?: number } | undefined;\n if (usage?.output_tokens) {\n outputTokens = usage.output_tokens;\n }\n } else if (event.type === \"message_start\") {\n const message = event.message as { usage?: { input_tokens?: number } } | undefined;\n if (message?.usage?.input_tokens) {\n inputTokens = message.usage.input_tokens;\n }\n }\n\n return result;\n } catch (err) {\n const error = err as { error?: { type?: string }; message?: string };\n batcher.add(buildLog(inputTokens, outputTokens, \"error\", error.error?.type ?? error.message ?? \"unknown_error\"));\n throw err;\n }\n },\n async return(value?: unknown): Promise<IteratorResult<unknown>> {\n // Stream was cancelled early — still log what we have\n batcher.add(buildLog(inputTokens, outputTokens, \"success\", null));\n if (originalIterator.return) {\n return originalIterator.return(value);\n }\n return { done: true, value: undefined };\n },\n };\n };\n\n return wrappedStream;\n}\n"],"mappings":"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","names":["line"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "costly",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AI cost waste detector — lightweight wrapper for the Anthropic SDK",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"bin": {
|
|
10
|
+
"costly": "./dist/cli.js"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"import": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"require": {
|
|
19
|
+
"types": "./dist/index.d.cts",
|
|
20
|
+
"default": "./dist/index.cjs"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsup",
|
|
29
|
+
"dev": "tsup --watch",
|
|
30
|
+
"test": "vitest run",
|
|
31
|
+
"test:watch": "vitest"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"@anthropic-ai/sdk": ">=0.30.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@anthropic-ai/sdk": "^0.78.0",
|
|
38
|
+
"@types/node": "^20.19.37",
|
|
39
|
+
"tsup": "^8.4.0",
|
|
40
|
+
"typescript": "^5.7.0",
|
|
41
|
+
"vitest": "^4.0.18"
|
|
42
|
+
},
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=18"
|
|
46
|
+
},
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "https://github.com/dannytapia/costly"
|
|
50
|
+
},
|
|
51
|
+
"keywords": [
|
|
52
|
+
"anthropic",
|
|
53
|
+
"claude",
|
|
54
|
+
"ai",
|
|
55
|
+
"cost",
|
|
56
|
+
"monitoring",
|
|
57
|
+
"waste",
|
|
58
|
+
"optimization"
|
|
59
|
+
]
|
|
60
|
+
}
|