@kalaa/node 1.0.5 → 1.0.8
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 +1 -1
- package/costmeter.js +89 -18
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ kalaa.withContext('nightly-digest', () => {
|
|
|
55
55
|
|
|
56
56
|
- OpenAI: chat completions, including streaming (`stream_options.include_usage`)
|
|
57
57
|
- Anthropic: messages, including streaming
|
|
58
|
-
- Gemini: `generateContent`
|
|
58
|
+
- Gemini: `generateContent` and `generateContentStream` — both `@google/genai` (current) and `@google/generative-ai` (legacy) are supported
|
|
59
59
|
- Whisper/audio transcription duration
|
|
60
60
|
|
|
61
61
|
## Configuration
|
package/costmeter.js
CHANGED
|
@@ -353,11 +353,10 @@ function wrapAnthropicStreamForUsage(stream, { _step, _user, model, t0 }) {
|
|
|
353
353
|
|
|
354
354
|
function patchOpenAIModule() {
|
|
355
355
|
let OpenAIMod;
|
|
356
|
-
try { OpenAIMod = require('openai'); } catch
|
|
357
|
-
log('openai require() succeeded, module keys:', Object.keys(OpenAIMod).slice(0, 10).join(','));
|
|
356
|
+
try { OpenAIMod = require('openai'); } catch { return; } // not installed, skip
|
|
358
357
|
const OpenAI = OpenAIMod.OpenAI || OpenAIMod.default || OpenAIMod;
|
|
359
|
-
if (!OpenAI || typeof OpenAI !== 'function')
|
|
360
|
-
if (OpenAI.__cm_patched)
|
|
358
|
+
if (!OpenAI || typeof OpenAI !== 'function') return;
|
|
359
|
+
if (OpenAI.__cm_patched) return;
|
|
361
360
|
|
|
362
361
|
|
|
363
362
|
let Completions, Transcriptions;
|
|
@@ -476,32 +475,33 @@ function patchAnthropicModule() {
|
|
|
476
475
|
Anthropic.__cm_patched = true;
|
|
477
476
|
log('anthropic module patched (prototype-level)');
|
|
478
477
|
}
|
|
479
|
-
|
|
480
478
|
function patchGeminiModule() {
|
|
479
|
+
patchGeminiLegacy();
|
|
480
|
+
patchGeminiUnified();
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Old SDK: @google/generative-ai — GoogleGenerativeAI().getGenerativeModel().generateContent()
|
|
484
|
+
function patchGeminiLegacy() {
|
|
481
485
|
let Mod;
|
|
482
486
|
try { Mod = require('@google/generative-ai'); } catch { return; } // not installed, skip
|
|
483
487
|
const GoogleGenerativeAI = Mod.GoogleGenerativeAI;
|
|
484
488
|
if (!GoogleGenerativeAI || GoogleGenerativeAI.__cm_patched) return;
|
|
485
489
|
|
|
486
|
-
// Same trick as OpenAI: probe an instance to reach the GenerativeModel
|
|
487
|
-
// class prototype, patch it once, every model instance inherits it.
|
|
488
490
|
let GenerativeModel;
|
|
489
491
|
try {
|
|
490
492
|
const probe = new GoogleGenerativeAI('cm_probe');
|
|
491
493
|
const model = probe.getGenerativeModel({ model: 'gemini-1.5-flash' });
|
|
492
494
|
GenerativeModel = Object.getPrototypeOf(model).constructor;
|
|
493
495
|
} catch (e) {
|
|
494
|
-
log('gemini probe failed:', e.message);
|
|
496
|
+
log('gemini (legacy) probe failed:', e.message);
|
|
495
497
|
return;
|
|
496
498
|
}
|
|
497
499
|
|
|
498
|
-
if (GenerativeModel?.prototype?.generateContent && !GenerativeModel.prototype.generateContent.__cm) {
|
|
500
|
+
if (GenerativeModel?.prototype?.generateContent && !GenerativeModel.prototype.generateContent.__cm) {
|
|
499
501
|
const orig = GenerativeModel.prototype.generateContent;
|
|
500
502
|
const wrapped = async function (params, ...rest) {
|
|
501
503
|
const t0 = Date.now();
|
|
502
|
-
const modelName = (this.model || 'gemini').replace(/^models\//, '');
|
|
503
|
-
// Gemini params can be a plain object, a string, or an array — only
|
|
504
|
-
// strip _user when it's actually a plain request object carrying one.
|
|
504
|
+
const modelName = (this.model || 'gemini').replace(/^models\//, '');
|
|
505
505
|
let _user, clean = params;
|
|
506
506
|
if (params && typeof params === 'object' && !Array.isArray(params) && '_user' in params) {
|
|
507
507
|
({ _user, ...clean } = params);
|
|
@@ -520,14 +520,11 @@ const modelName = (this.model || 'gemini').replace(/^models\//, '');
|
|
|
520
520
|
GenerativeModel.prototype.generateContent = wrapped;
|
|
521
521
|
}
|
|
522
522
|
|
|
523
|
-
|
|
524
|
-
// consumed, via response.usageMetadata on the final resolved response
|
|
525
|
-
// object the SDK exposes — same tap pattern as OpenAI/Anthropic streaming.
|
|
526
|
-
if (GenerativeModel?.prototype?.generateContentStream && !GenerativeModel.prototype.generateContentStream.__cm) {
|
|
523
|
+
if (GenerativeModel?.prototype?.generateContentStream && !GenerativeModel.prototype.generateContentStream.__cm) {
|
|
527
524
|
const orig = GenerativeModel.prototype.generateContentStream;
|
|
528
525
|
const wrapped = async function (params, ...rest) {
|
|
529
526
|
const t0 = Date.now();
|
|
530
|
-
const modelName = (this.model || 'gemini').replace(/^models\//, '');
|
|
527
|
+
const modelName = (this.model || 'gemini').replace(/^models\//, '');
|
|
531
528
|
let _user, clean = params;
|
|
532
529
|
if (params && typeof params === 'object' && !Array.isArray(params) && '_user' in params) {
|
|
533
530
|
({ _user, ...clean } = params);
|
|
@@ -549,7 +546,81 @@ const modelName = (this.model || 'gemini').replace(/^models\//, '');
|
|
|
549
546
|
}
|
|
550
547
|
|
|
551
548
|
GoogleGenerativeAI.__cm_patched = true;
|
|
552
|
-
log('gemini module patched (prototype-level)');
|
|
549
|
+
log('gemini (legacy @google/generative-ai) module patched (prototype-level)');
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// New SDK: @google/genai — new GoogleGenAI({apiKey}).models.generateContent({model, contents})
|
|
553
|
+
function patchGeminiUnified() {
|
|
554
|
+
let Mod;
|
|
555
|
+
try { Mod = require('@google/genai'); } catch { return; } // not installed, skip
|
|
556
|
+
const GoogleGenAI = Mod.GoogleGenAI;
|
|
557
|
+
if (!GoogleGenAI || GoogleGenAI.__cm_patched) return;
|
|
558
|
+
|
|
559
|
+
let Models;
|
|
560
|
+
try {
|
|
561
|
+
const probe = new GoogleGenAI({ apiKey: 'cm_probe' });
|
|
562
|
+
Models = Object.getPrototypeOf(probe.models).constructor;
|
|
563
|
+
} catch (e) {
|
|
564
|
+
log('gemini (unified) probe failed:', e.message);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (Models?.prototype?.generateContent && !Models.prototype.generateContent.__cm) {
|
|
569
|
+
const orig = Models.prototype.generateContent;
|
|
570
|
+
const wrapped = async function (params, ...rest) {
|
|
571
|
+
const t0 = Date.now();
|
|
572
|
+
const { _user, ...clean } = params || {};
|
|
573
|
+
const modelName = (clean.model || 'gemini').replace(/^models\//, '');
|
|
574
|
+
try {
|
|
575
|
+
const resp = await orig.call(this, clean, ...rest);
|
|
576
|
+
track({ provider: 'gemini', model: modelName, usage: resp?.usageMetadata || {}, latencyMs: Date.now() - t0, endUser: _user });
|
|
577
|
+
return resp;
|
|
578
|
+
} catch (err) {
|
|
579
|
+
track({ provider: 'gemini', model: modelName, usage: {}, latencyMs: Date.now() - t0, status: 'error', meta: { error: String(err.message).slice(0, 200) }, endUser: _user });
|
|
580
|
+
throw err;
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
wrapped.__cm = true;
|
|
584
|
+
Models.prototype.generateContent = wrapped;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (Models?.prototype?.generateContentStream && !Models.prototype.generateContentStream.__cm) {
|
|
588
|
+
const orig = Models.prototype.generateContentStream;
|
|
589
|
+
const wrapped = async function (params, ...rest) {
|
|
590
|
+
const t0 = Date.now();
|
|
591
|
+
const { _user, ...clean } = params || {};
|
|
592
|
+
const modelName = (clean.model || 'gemini').replace(/^models\//, '');
|
|
593
|
+
try {
|
|
594
|
+
const stream = await orig.call(this, clean, ...rest);
|
|
595
|
+
const origIterator = stream[Symbol.asyncIterator].bind(stream);
|
|
596
|
+
let lastUsage = null;
|
|
597
|
+
stream[Symbol.asyncIterator] = function () {
|
|
598
|
+
const it = origIterator();
|
|
599
|
+
return {
|
|
600
|
+
async next() {
|
|
601
|
+
const result = await it.next();
|
|
602
|
+
if (!result.done && result.value?.usageMetadata) lastUsage = result.value.usageMetadata;
|
|
603
|
+
if (result.done) {
|
|
604
|
+
track({ provider: 'gemini', model: modelName, usage: lastUsage || {}, latencyMs: Date.now() - t0, status: lastUsage ? 'ok' : 'no_usage_streamed', endUser: _user });
|
|
605
|
+
}
|
|
606
|
+
return result;
|
|
607
|
+
},
|
|
608
|
+
return(v) { return it.return ? it.return(v) : Promise.resolve({ value: v, done: true }); },
|
|
609
|
+
throw(err) { return it.throw ? it.throw(err) : Promise.reject(err); },
|
|
610
|
+
};
|
|
611
|
+
};
|
|
612
|
+
return stream;
|
|
613
|
+
} catch (err) {
|
|
614
|
+
track({ provider: 'gemini', model: modelName, usage: {}, latencyMs: Date.now() - t0, status: 'error', meta: { error: String(err.message).slice(0, 200) }, endUser: _user });
|
|
615
|
+
throw err;
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
wrapped.__cm = true;
|
|
619
|
+
Models.prototype.generateContentStream = wrapped;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
GoogleGenAI.__cm_patched = true;
|
|
623
|
+
log('gemini (unified @google/genai) module patched (prototype-level)');
|
|
553
624
|
}
|
|
554
625
|
|
|
555
626
|
// ── PUBLIC API ───────────────────────────────────────────────────────
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kalaa/node",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
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",
|
|
@@ -39,14 +39,16 @@
|
|
|
39
39
|
"music-metadata": "^7.0.0",
|
|
40
40
|
"node-fetch": "^2.7.0"
|
|
41
41
|
},
|
|
42
|
-
|
|
42
|
+
"peerDependencies": {
|
|
43
43
|
"openai": ">=4",
|
|
44
44
|
"@anthropic-ai/sdk": ">=0.20",
|
|
45
|
-
"@google/generative-ai": ">=0.10"
|
|
45
|
+
"@google/generative-ai": ">=0.10",
|
|
46
|
+
"@google/genai": ">=0.1"
|
|
46
47
|
},
|
|
47
48
|
"peerDependenciesMeta": {
|
|
48
49
|
"openai": { "optional": true },
|
|
49
50
|
"@anthropic-ai/sdk": { "optional": true },
|
|
50
|
-
"@google/generative-ai": { "optional": true }
|
|
51
|
+
"@google/generative-ai": { "optional": true },
|
|
52
|
+
"@google/genai": { "optional": true }
|
|
51
53
|
}
|
|
52
54
|
}
|