@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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/costmeter.js +86 -14
  3. 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
- // generateContentStream: usage arrives only after the stream is fully
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.7",
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
- "peerDependencies": {
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
  }