@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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/costmeter.js +89 -18
  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
@@ -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 (e) { log('openai require() failed:', e.message); return; }
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') { log('openai class did not resolve to a function, typeof:', typeof OpenAI); return; }
360
- if (OpenAI.__cm_patched) { log('openai already patched, skipping'); return; }
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
- // generateContentStream: usage arrives only after the stream is fully
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.5",
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
  }