@kalaa/node 1.0.7 → 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 +86 -14
- 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
|
@@ -475,32 +475,33 @@ function patchAnthropicModule() {
|
|
|
475
475
|
Anthropic.__cm_patched = true;
|
|
476
476
|
log('anthropic module patched (prototype-level)');
|
|
477
477
|
}
|
|
478
|
-
|
|
479
478
|
function patchGeminiModule() {
|
|
479
|
+
patchGeminiLegacy();
|
|
480
|
+
patchGeminiUnified();
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Old SDK: @google/generative-ai — GoogleGenerativeAI().getGenerativeModel().generateContent()
|
|
484
|
+
function patchGeminiLegacy() {
|
|
480
485
|
let Mod;
|
|
481
486
|
try { Mod = require('@google/generative-ai'); } catch { return; } // not installed, skip
|
|
482
487
|
const GoogleGenerativeAI = Mod.GoogleGenerativeAI;
|
|
483
488
|
if (!GoogleGenerativeAI || GoogleGenerativeAI.__cm_patched) return;
|
|
484
489
|
|
|
485
|
-
// Same trick as OpenAI: probe an instance to reach the GenerativeModel
|
|
486
|
-
// class prototype, patch it once, every model instance inherits it.
|
|
487
490
|
let GenerativeModel;
|
|
488
491
|
try {
|
|
489
492
|
const probe = new GoogleGenerativeAI('cm_probe');
|
|
490
493
|
const model = probe.getGenerativeModel({ model: 'gemini-1.5-flash' });
|
|
491
494
|
GenerativeModel = Object.getPrototypeOf(model).constructor;
|
|
492
495
|
} catch (e) {
|
|
493
|
-
log('gemini probe failed:', e.message);
|
|
496
|
+
log('gemini (legacy) probe failed:', e.message);
|
|
494
497
|
return;
|
|
495
498
|
}
|
|
496
499
|
|
|
497
|
-
if (GenerativeModel?.prototype?.generateContent && !GenerativeModel.prototype.generateContent.__cm) {
|
|
500
|
+
if (GenerativeModel?.prototype?.generateContent && !GenerativeModel.prototype.generateContent.__cm) {
|
|
498
501
|
const orig = GenerativeModel.prototype.generateContent;
|
|
499
502
|
const wrapped = async function (params, ...rest) {
|
|
500
503
|
const t0 = Date.now();
|
|
501
|
-
const modelName = (this.model || 'gemini').replace(/^models\//, '');
|
|
502
|
-
// Gemini params can be a plain object, a string, or an array — only
|
|
503
|
-
// strip _user when it's actually a plain request object carrying one.
|
|
504
|
+
const modelName = (this.model || 'gemini').replace(/^models\//, '');
|
|
504
505
|
let _user, clean = params;
|
|
505
506
|
if (params && typeof params === 'object' && !Array.isArray(params) && '_user' in params) {
|
|
506
507
|
({ _user, ...clean } = params);
|
|
@@ -519,14 +520,11 @@ const modelName = (this.model || 'gemini').replace(/^models\//, '');
|
|
|
519
520
|
GenerativeModel.prototype.generateContent = wrapped;
|
|
520
521
|
}
|
|
521
522
|
|
|
522
|
-
|
|
523
|
-
// consumed, via response.usageMetadata on the final resolved response
|
|
524
|
-
// object the SDK exposes — same tap pattern as OpenAI/Anthropic streaming.
|
|
525
|
-
if (GenerativeModel?.prototype?.generateContentStream && !GenerativeModel.prototype.generateContentStream.__cm) {
|
|
523
|
+
if (GenerativeModel?.prototype?.generateContentStream && !GenerativeModel.prototype.generateContentStream.__cm) {
|
|
526
524
|
const orig = GenerativeModel.prototype.generateContentStream;
|
|
527
525
|
const wrapped = async function (params, ...rest) {
|
|
528
526
|
const t0 = Date.now();
|
|
529
|
-
const modelName = (this.model || 'gemini').replace(/^models\//, '');
|
|
527
|
+
const modelName = (this.model || 'gemini').replace(/^models\//, '');
|
|
530
528
|
let _user, clean = params;
|
|
531
529
|
if (params && typeof params === 'object' && !Array.isArray(params) && '_user' in params) {
|
|
532
530
|
({ _user, ...clean } = params);
|
|
@@ -548,7 +546,81 @@ const modelName = (this.model || 'gemini').replace(/^models\//, '');
|
|
|
548
546
|
}
|
|
549
547
|
|
|
550
548
|
GoogleGenerativeAI.__cm_patched = true;
|
|
551
|
-
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)');
|
|
552
624
|
}
|
|
553
625
|
|
|
554
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
|
}
|