@kalaa/node 1.0.8 → 1.1.1
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 +2 -0
- package/costmeter.js +77 -91
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -58,6 +58,8 @@ kalaa.withContext('nightly-digest', () => {
|
|
|
58
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
|
+
|
|
62
|
+
> **Order matters for Gemini:** call `kalaa.observe()` before you `require('@google/genai')` anywhere in your app. (OpenAI and Anthropic aren't order-sensitive — only the current Gemini SDK's instance-bound methods require this.)
|
|
61
63
|
## Configuration
|
|
62
64
|
|
|
63
65
|
| Option | Env var | Default |
|
package/costmeter.js
CHANGED
|
@@ -475,15 +475,18 @@ function patchAnthropicModule() {
|
|
|
475
475
|
Anthropic.__cm_patched = true;
|
|
476
476
|
log('anthropic module patched (prototype-level)');
|
|
477
477
|
}
|
|
478
|
+
// New SDK: @google/genai — generateContent/generateContentStream are bound
|
|
479
|
+
// per-instance in the Models constructor, not on Models.prototype. Prototype
|
|
480
|
+
// patching is a no-op here; we wrap the instance's own methods instead, right
|
|
481
|
+
// after GoogleGenAI's constructor creates them.
|
|
478
482
|
function patchGeminiModule() {
|
|
479
483
|
patchGeminiLegacy();
|
|
480
484
|
patchGeminiUnified();
|
|
481
485
|
}
|
|
482
486
|
|
|
483
|
-
// Old SDK: @google/generative-ai — GoogleGenerativeAI().getGenerativeModel().generateContent()
|
|
484
487
|
function patchGeminiLegacy() {
|
|
485
488
|
let Mod;
|
|
486
|
-
try { Mod = require('@google/generative-ai'); } catch { return; }
|
|
489
|
+
try { Mod = require('@google/generative-ai'); } catch { return; }
|
|
487
490
|
const GoogleGenerativeAI = Mod.GoogleGenerativeAI;
|
|
488
491
|
if (!GoogleGenerativeAI || GoogleGenerativeAI.__cm_patched) return;
|
|
489
492
|
|
|
@@ -520,107 +523,90 @@ function patchGeminiLegacy() {
|
|
|
520
523
|
GenerativeModel.prototype.generateContent = wrapped;
|
|
521
524
|
}
|
|
522
525
|
|
|
523
|
-
if (GenerativeModel?.prototype?.generateContentStream && !GenerativeModel.prototype.generateContentStream.__cm) {
|
|
524
|
-
const orig = GenerativeModel.prototype.generateContentStream;
|
|
525
|
-
const wrapped = async function (params, ...rest) {
|
|
526
|
-
const t0 = Date.now();
|
|
527
|
-
const modelName = (this.model || 'gemini').replace(/^models\//, '');
|
|
528
|
-
let _user, clean = params;
|
|
529
|
-
if (params && typeof params === 'object' && !Array.isArray(params) && '_user' in params) {
|
|
530
|
-
({ _user, ...clean } = params);
|
|
531
|
-
}
|
|
532
|
-
try {
|
|
533
|
-
const result = await orig.call(this, clean, ...rest);
|
|
534
|
-
result.response.then(finalResp => {
|
|
535
|
-
const usage = finalResp?.usageMetadata || {};
|
|
536
|
-
track({ provider: 'gemini', model: modelName, usage, latencyMs: Date.now() - t0, status: Object.keys(usage).length ? 'ok' : 'no_usage_streamed', endUser: _user });
|
|
537
|
-
}).catch(() => {});
|
|
538
|
-
return result;
|
|
539
|
-
} catch (err) {
|
|
540
|
-
track({ provider: 'gemini', model: modelName, usage: {}, latencyMs: Date.now() - t0, status: 'error', meta: { error: String(err.message).slice(0, 200) }, endUser: _user });
|
|
541
|
-
throw err;
|
|
542
|
-
}
|
|
543
|
-
};
|
|
544
|
-
wrapped.__cm = true;
|
|
545
|
-
GenerativeModel.prototype.generateContentStream = wrapped;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
526
|
GoogleGenerativeAI.__cm_patched = true;
|
|
549
527
|
log('gemini (legacy @google/generative-ai) module patched (prototype-level)');
|
|
550
528
|
}
|
|
551
529
|
|
|
552
|
-
// New SDK: @google/genai — new GoogleGenAI({apiKey}).models.generateContent({model, contents})
|
|
553
530
|
function patchGeminiUnified() {
|
|
554
531
|
let Mod;
|
|
555
532
|
try { Mod = require('@google/genai'); } catch { return; } // not installed, skip
|
|
556
|
-
const
|
|
557
|
-
if (!
|
|
533
|
+
const OrigGoogleGenAI = Mod.GoogleGenAI;
|
|
534
|
+
if (!OrigGoogleGenAI || OrigGoogleGenAI.__cm_patched) return;
|
|
535
|
+
|
|
536
|
+
function wrapModelsInstance(models) {
|
|
537
|
+
if (!models || models.__cm_wrapped) return;
|
|
538
|
+
|
|
539
|
+
if (typeof models.generateContent === 'function' && !models.generateContent.__cm) {
|
|
540
|
+
const orig = models.generateContent.bind(models);
|
|
541
|
+
const wrapped = async function (params, ...rest) {
|
|
542
|
+
const t0 = Date.now();
|
|
543
|
+
const { _user, ...clean } = params || {};
|
|
544
|
+
const modelName = (clean.model || 'gemini').replace(/^models\//, '');
|
|
545
|
+
try {
|
|
546
|
+
const resp = await orig(clean, ...rest);
|
|
547
|
+
track({ provider: 'gemini', model: modelName, usage: resp?.usageMetadata || {}, latencyMs: Date.now() - t0, endUser: _user });
|
|
548
|
+
return resp;
|
|
549
|
+
} catch (err) {
|
|
550
|
+
track({ provider: 'gemini', model: modelName, usage: {}, latencyMs: Date.now() - t0, status: 'error', meta: { error: String(err.message).slice(0, 200) }, endUser: _user });
|
|
551
|
+
throw err;
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
wrapped.__cm = true;
|
|
555
|
+
models.generateContent = wrapped;
|
|
556
|
+
}
|
|
558
557
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
558
|
+
if (typeof models.generateContentStream === 'function' && !models.generateContentStream.__cm) {
|
|
559
|
+
const orig = models.generateContentStream.bind(models);
|
|
560
|
+
const wrapped = async function (params, ...rest) {
|
|
561
|
+
const t0 = Date.now();
|
|
562
|
+
const { _user, ...clean } = params || {};
|
|
563
|
+
const modelName = (clean.model || 'gemini').replace(/^models\//, '');
|
|
564
|
+
try {
|
|
565
|
+
const stream = await orig(clean, ...rest);
|
|
566
|
+
const origIterator = stream[Symbol.asyncIterator].bind(stream);
|
|
567
|
+
let lastUsage = null;
|
|
568
|
+
stream[Symbol.asyncIterator] = function () {
|
|
569
|
+
const it = origIterator();
|
|
570
|
+
return {
|
|
571
|
+
async next() {
|
|
572
|
+
const result = await it.next();
|
|
573
|
+
if (!result.done && result.value?.usageMetadata) lastUsage = result.value.usageMetadata;
|
|
574
|
+
if (result.done) {
|
|
575
|
+
track({ provider: 'gemini', model: modelName, usage: lastUsage || {}, latencyMs: Date.now() - t0, status: lastUsage ? 'ok' : 'no_usage_streamed', endUser: _user });
|
|
576
|
+
}
|
|
577
|
+
return result;
|
|
578
|
+
},
|
|
579
|
+
return(v) { return it.return ? it.return(v) : Promise.resolve({ value: v, done: true }); },
|
|
580
|
+
throw(err) { return it.throw ? it.throw(err) : Promise.reject(err); },
|
|
581
|
+
};
|
|
582
|
+
};
|
|
583
|
+
return stream;
|
|
584
|
+
} catch (err) {
|
|
585
|
+
track({ provider: 'gemini', model: modelName, usage: {}, latencyMs: Date.now() - t0, status: 'error', meta: { error: String(err.message).slice(0, 200) }, endUser: _user });
|
|
586
|
+
throw err;
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
wrapped.__cm = true;
|
|
590
|
+
models.generateContentStream = wrapped;
|
|
591
|
+
}
|
|
567
592
|
|
|
568
|
-
|
|
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;
|
|
593
|
+
models.__cm_wrapped = true;
|
|
585
594
|
}
|
|
586
595
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
const
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
-
}
|
|
596
|
+
// Proxy the constructor: every `new GoogleGenAI(...)` still runs the real
|
|
597
|
+
// constructor untouched, we just reach into the instance it produces and
|
|
598
|
+
// wrap `.models` before handing the instance back.
|
|
599
|
+
const PatchedGoogleGenAI = new Proxy(OrigGoogleGenAI, {
|
|
600
|
+
construct(Target, args) {
|
|
601
|
+
const instance = new Target(...args);
|
|
602
|
+
wrapModelsInstance(instance.models);
|
|
603
|
+
return instance;
|
|
604
|
+
},
|
|
605
|
+
});
|
|
606
|
+
PatchedGoogleGenAI.__cm_patched = true;
|
|
621
607
|
|
|
622
|
-
GoogleGenAI
|
|
623
|
-
log('gemini (unified @google/genai) module patched (
|
|
608
|
+
Mod.GoogleGenAI = PatchedGoogleGenAI;
|
|
609
|
+
log('gemini (unified @google/genai) module patched (instance-level via constructor proxy)');
|
|
624
610
|
}
|
|
625
611
|
|
|
626
612
|
// ── PUBLIC API ───────────────────────────────────────────────────────
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kalaa/node",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
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",
|