@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.
Files changed (3) hide show
  1. package/README.md +2 -0
  2. package/costmeter.js +77 -91
  3. 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; } // not installed, skip
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 GoogleGenAI = Mod.GoogleGenAI;
557
- if (!GoogleGenAI || GoogleGenAI.__cm_patched) return;
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
- 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
- }
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
- 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;
593
+ models.__cm_wrapped = true;
585
594
  }
586
595
 
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
- }
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.__cm_patched = true;
623
- log('gemini (unified @google/genai) module patched (prototype-level)');
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.0.8",
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",