@kalaa/node 1.0.3 → 1.0.4
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/costmeter.js +44 -57
- package/package.json +1 -1
package/costmeter.js
CHANGED
|
@@ -1,26 +1,8 @@
|
|
|
1
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
2
|
-
// costmeter.js — drop-in AI cost tracking SDK
|
|
3
|
-
//
|
|
4
|
-
// Two-line setup, paste anywhere, order doesn't matter:
|
|
5
|
-
//
|
|
6
|
-
// const costmeter = require('costmeter');
|
|
7
|
-
// costmeter.observe();
|
|
8
|
-
//
|
|
9
|
-
// Put COSTMETER_KEY=cm_live_xxx in your .env and you're done.
|
|
10
|
-
//
|
|
11
|
-
// observe() patches the OpenAI module at the class level (so any client
|
|
12
|
-
// you create, whenever you create it, is already instrumented) and, if
|
|
13
|
-
// Express is installed, auto-inserts route-attribution middleware. No
|
|
14
|
-
// app.use, no "after express.json", no manual instrument call.
|
|
15
|
-
//
|
|
16
|
-
// It never touches a database, never computes cost, and never sits in
|
|
17
|
-
// the path of your AI calls — if ingest is unreachable, events are
|
|
18
|
-
// dropped silently and your app is unaffected.
|
|
19
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
20
|
-
|
|
21
1
|
const { AsyncLocalStorage } = require('async_hooks');
|
|
22
2
|
const crypto = require('crypto');
|
|
23
3
|
const http = require('http');
|
|
4
|
+
const { Worker } = require('worker_threads');
|
|
5
|
+
const path = require('path');
|
|
24
6
|
|
|
25
7
|
const als = new AsyncLocalStorage();
|
|
26
8
|
|
|
@@ -181,38 +163,58 @@ function getOrSetAnonId(req) {
|
|
|
181
163
|
}
|
|
182
164
|
return null;
|
|
183
165
|
}
|
|
184
|
-
|
|
185
|
-
// Real duration from the audio container's own metadata (wav/mp3/m4a/webm all
|
|
186
|
-
// expose this in their header — no decoding, no guessing). This is the
|
|
187
|
-
// actual value Whisper bills against, just read client-side instead of
|
|
188
|
-
// waiting for the API to hand it back (which it only does with verbose_json).
|
|
166
|
+
|
|
189
167
|
async function getRealAudioDuration(file) {
|
|
190
|
-
let
|
|
191
|
-
try { mm = require('music-metadata'); } catch { return null; } // not installed — degrade below
|
|
168
|
+
let input;
|
|
192
169
|
try {
|
|
193
|
-
let metadata;
|
|
194
170
|
if (Buffer.isBuffer(file)) {
|
|
195
|
-
|
|
171
|
+
input = { type: 'buffer', data: file };
|
|
196
172
|
} else if (file?.path) {
|
|
197
|
-
|
|
173
|
+
input = { type: 'path', data: file.path };
|
|
198
174
|
} else if (file?.arrayBuffer) { // Blob/File
|
|
199
|
-
|
|
200
|
-
metadata = await mm.parseBuffer(buf);
|
|
175
|
+
input = { type: 'buffer', data: Buffer.from(await file.arrayBuffer()) };
|
|
201
176
|
} else {
|
|
202
177
|
return null;
|
|
203
178
|
}
|
|
204
|
-
const seconds = metadata?.format?.duration;
|
|
205
|
-
return typeof seconds === 'number' && seconds > 0 ? Math.round(seconds) : null;
|
|
206
179
|
} catch (e) {
|
|
207
180
|
log('duration read failed:', e.message);
|
|
208
181
|
return null;
|
|
209
182
|
}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
return new Promise((resolve) => {
|
|
186
|
+
let settled = false;
|
|
187
|
+
let worker;
|
|
188
|
+
try {
|
|
189
|
+
worker = new Worker(path.join(__dirname, 'duration-worker.js'), { workerData: input });
|
|
190
|
+
} catch (e) {
|
|
191
|
+
log('duration worker spawn failed:', e.message);
|
|
192
|
+
return resolve(null);
|
|
193
|
+
}
|
|
194
|
+
const timer = setTimeout(() => {
|
|
195
|
+
if (settled) return;
|
|
196
|
+
settled = true;
|
|
197
|
+
worker.terminate();
|
|
198
|
+
log('duration worker timed out — likely a malformed file, degrading to null');
|
|
199
|
+
resolve(null);
|
|
200
|
+
}, 3000);
|
|
201
|
+
worker.once('message', (msg) => {
|
|
202
|
+
if (settled) return;
|
|
203
|
+
settled = true;
|
|
204
|
+
clearTimeout(timer);
|
|
205
|
+
worker.terminate();
|
|
206
|
+
resolve(msg?.seconds ?? null);
|
|
207
|
+
});
|
|
208
|
+
worker.once('error', () => {
|
|
209
|
+
if (settled) return;
|
|
210
|
+
settled = true;
|
|
211
|
+
clearTimeout(timer);
|
|
212
|
+
resolve(null);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
210
215
|
}
|
|
211
216
|
|
|
212
|
-
|
|
213
|
-
// (mp3/m4a) around ~128kbps voice audio ≈ 16KB/sec. This is intentionally
|
|
214
|
-
// conservative — good enough for cost visibility, not billing-grade exact.
|
|
215
|
-
// ── USAGE NORMALIZATION (raw tokens only — backend prices) ──────────
|
|
217
|
+
|
|
216
218
|
function normalizeUsage(provider, usage = {}) {
|
|
217
219
|
if (provider === 'anthropic') {
|
|
218
220
|
return {
|
|
@@ -260,9 +262,7 @@ function track({ step, provider = 'openai', model = '', usage = {}, latencyMs =
|
|
|
260
262
|
} catch (err) { log('track error:', err.message); }
|
|
261
263
|
}
|
|
262
264
|
|
|
263
|
-
|
|
264
|
-
// altering what the caller receives — every chunk still flows through
|
|
265
|
-
// exactly as OpenAI sent it.
|
|
265
|
+
|
|
266
266
|
function wrapStreamForUsage(streamPromise, { _step, _user, model, t0 }) {
|
|
267
267
|
return streamPromise.then(stream => {
|
|
268
268
|
const origIterator = stream[Symbol.asyncIterator].bind(stream);
|
|
@@ -301,10 +301,7 @@ function wrapStreamForUsage(streamPromise, { _step, _user, model, t0 }) {
|
|
|
301
301
|
});
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
-
|
|
305
|
-
// input_tokens, message_delta carries the final output_tokens. Same tap
|
|
306
|
-
// pattern as OpenAI — caller's iteration is untouched, we just watch what
|
|
307
|
-
// passes through.
|
|
304
|
+
|
|
308
305
|
function wrapAnthropicStreamForUsage(stream, { _step, _user, model, t0 }) {
|
|
309
306
|
const origIterator = stream[Symbol.asyncIterator].bind(stream);
|
|
310
307
|
let inputTokens = 0, cachedTokens = 0, cacheWriteTokens = 0, outputTokens = 0;
|
|
@@ -354,9 +351,6 @@ function wrapAnthropicStreamForUsage(stream, { _step, _user, model, t0 }) {
|
|
|
354
351
|
return stream;
|
|
355
352
|
}
|
|
356
353
|
|
|
357
|
-
// ── MODULE-LEVEL PATCHING (order-independent instrumentation) ───────
|
|
358
|
-
// Patches the OpenAI class prototype so EVERY client — created before or
|
|
359
|
-
// after observe() — is instrumented. No need to hand us the client.
|
|
360
354
|
function patchOpenAIModule() {
|
|
361
355
|
let OpenAIMod;
|
|
362
356
|
try { OpenAIMod = require('openai'); } catch { return; } // not installed, skip
|
|
@@ -364,10 +358,7 @@ function patchOpenAIModule() {
|
|
|
364
358
|
if (!OpenAI || typeof OpenAI !== 'function') return;
|
|
365
359
|
if (OpenAI.__cm_patched) return;
|
|
366
360
|
|
|
367
|
-
|
|
368
|
-
// PROTOTYPES, not on each instance. Patch the prototype ONCE and every
|
|
369
|
-
// client — created before or after observe() — inherits the wrapper.
|
|
370
|
-
// We spin up a throwaway client (no network) just to reach those classes.
|
|
361
|
+
|
|
371
362
|
let Completions, Transcriptions;
|
|
372
363
|
try {
|
|
373
364
|
const probe = new OpenAI({ apiKey: 'cm_probe' });
|
|
@@ -441,11 +432,7 @@ function patchAnthropicModule() {
|
|
|
441
432
|
if (!Anthropic || typeof Anthropic !== 'function') return;
|
|
442
433
|
if (Anthropic.__cm_patched) return;
|
|
443
434
|
|
|
444
|
-
|
|
445
|
-
// PROTOTYPE. Patch it once and every client — constructed before or after
|
|
446
|
-
// observe(), via any import style — inherits the wrapper. No constructor
|
|
447
|
-
// swapping: the SDK's module exports are non-writable, so reassigning
|
|
448
|
-
// Mod.Anthropic silently does nothing.
|
|
435
|
+
|
|
449
436
|
let Messages;
|
|
450
437
|
try {
|
|
451
438
|
const probe = new Anthropic({ apiKey: 'cm_probe' });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kalaa/node",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Drop-in AI cost tracking for Node.js and TypeScript. Auto-instruments OpenAI, Anthropic, and Gemini at the class level, with zero code at each call site.",
|
|
5
5
|
"main": "costmeter.js",
|
|
6
6
|
"types": "costmeter.d.ts",
|