@kalaa/node 1.0.7 → 1.0.9

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 +3 -1
  2. package/costmeter.js +78 -68
  3. package/package.json +6 -4
package/README.md CHANGED
@@ -55,9 +55,11 @@ 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
+
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,80 +475,90 @@ function patchAnthropicModule() {
475
475
  Anthropic.__cm_patched = true;
476
476
  log('anthropic module patched (prototype-level)');
477
477
  }
478
-
479
- function patchGeminiModule() {
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.
482
+ function patchGeminiUnified() {
480
483
  let Mod;
481
- try { Mod = require('@google/generative-ai'); } catch { return; } // not installed, skip
482
- const GoogleGenerativeAI = Mod.GoogleGenerativeAI;
483
- if (!GoogleGenerativeAI || GoogleGenerativeAI.__cm_patched) return;
484
+ try { Mod = require('@google/genai'); } catch { return; } // not installed, skip
485
+ const OrigGoogleGenAI = Mod.GoogleGenAI;
486
+ if (!OrigGoogleGenAI || OrigGoogleGenAI.__cm_patched) return;
487
+
488
+ function wrapModelsInstance(models) {
489
+ if (!models || models.__cm_wrapped) return;
490
+
491
+ if (typeof models.generateContent === 'function' && !models.generateContent.__cm) {
492
+ const orig = models.generateContent.bind(models);
493
+ const wrapped = async function (params, ...rest) {
494
+ const t0 = Date.now();
495
+ const { _user, ...clean } = params || {};
496
+ const modelName = (clean.model || 'gemini').replace(/^models\//, '');
497
+ try {
498
+ const resp = await orig(clean, ...rest);
499
+ track({ provider: 'gemini', model: modelName, usage: resp?.usageMetadata || {}, latencyMs: Date.now() - t0, endUser: _user });
500
+ return resp;
501
+ } catch (err) {
502
+ track({ provider: 'gemini', model: modelName, usage: {}, latencyMs: Date.now() - t0, status: 'error', meta: { error: String(err.message).slice(0, 200) }, endUser: _user });
503
+ throw err;
504
+ }
505
+ };
506
+ wrapped.__cm = true;
507
+ models.generateContent = wrapped;
508
+ }
484
509
 
485
- // Same trick as OpenAI: probe an instance to reach the GenerativeModel
486
- // class prototype, patch it once, every model instance inherits it.
487
- let GenerativeModel;
488
- try {
489
- const probe = new GoogleGenerativeAI('cm_probe');
490
- const model = probe.getGenerativeModel({ model: 'gemini-1.5-flash' });
491
- GenerativeModel = Object.getPrototypeOf(model).constructor;
492
- } catch (e) {
493
- log('gemini probe failed:', e.message);
494
- return;
495
- }
510
+ if (typeof models.generateContentStream === 'function' && !models.generateContentStream.__cm) {
511
+ const orig = models.generateContentStream.bind(models);
512
+ const wrapped = async function (params, ...rest) {
513
+ const t0 = Date.now();
514
+ const { _user, ...clean } = params || {};
515
+ const modelName = (clean.model || 'gemini').replace(/^models\//, '');
516
+ try {
517
+ const stream = await orig(clean, ...rest);
518
+ const origIterator = stream[Symbol.asyncIterator].bind(stream);
519
+ let lastUsage = null;
520
+ stream[Symbol.asyncIterator] = function () {
521
+ const it = origIterator();
522
+ return {
523
+ async next() {
524
+ const result = await it.next();
525
+ if (!result.done && result.value?.usageMetadata) lastUsage = result.value.usageMetadata;
526
+ if (result.done) {
527
+ track({ provider: 'gemini', model: modelName, usage: lastUsage || {}, latencyMs: Date.now() - t0, status: lastUsage ? 'ok' : 'no_usage_streamed', endUser: _user });
528
+ }
529
+ return result;
530
+ },
531
+ return(v) { return it.return ? it.return(v) : Promise.resolve({ value: v, done: true }); },
532
+ throw(err) { return it.throw ? it.throw(err) : Promise.reject(err); },
533
+ };
534
+ };
535
+ return stream;
536
+ } catch (err) {
537
+ track({ provider: 'gemini', model: modelName, usage: {}, latencyMs: Date.now() - t0, status: 'error', meta: { error: String(err.message).slice(0, 200) }, endUser: _user });
538
+ throw err;
539
+ }
540
+ };
541
+ wrapped.__cm = true;
542
+ models.generateContentStream = wrapped;
543
+ }
496
544
 
497
- if (GenerativeModel?.prototype?.generateContent && !GenerativeModel.prototype.generateContent.__cm) {
498
- const orig = GenerativeModel.prototype.generateContent;
499
- const wrapped = async function (params, ...rest) {
500
- 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
- let _user, clean = params;
505
- if (params && typeof params === 'object' && !Array.isArray(params) && '_user' in params) {
506
- ({ _user, ...clean } = params);
507
- }
508
- try {
509
- const resp = await orig.call(this, clean, ...rest);
510
- const usage = resp?.response?.usageMetadata || {};
511
- track({ provider: 'gemini', model: modelName, usage, latencyMs: Date.now() - t0, endUser: _user });
512
- return resp;
513
- } catch (err) {
514
- track({ provider: 'gemini', model: modelName, usage: {}, latencyMs: Date.now() - t0, status: 'error', meta: { error: String(err.message).slice(0, 200) }, endUser: _user });
515
- throw err;
516
- }
517
- };
518
- wrapped.__cm = true;
519
- GenerativeModel.prototype.generateContent = wrapped;
545
+ models.__cm_wrapped = true;
520
546
  }
521
547
 
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) {
526
- const orig = GenerativeModel.prototype.generateContentStream;
527
- const wrapped = async function (params, ...rest) {
528
- const t0 = Date.now();
529
- const modelName = (this.model || 'gemini').replace(/^models\//, '');
530
- let _user, clean = params;
531
- if (params && typeof params === 'object' && !Array.isArray(params) && '_user' in params) {
532
- ({ _user, ...clean } = params);
533
- }
534
- try {
535
- const result = await orig.call(this, clean, ...rest);
536
- result.response.then(finalResp => {
537
- const usage = finalResp?.usageMetadata || {};
538
- track({ provider: 'gemini', model: modelName, usage, latencyMs: Date.now() - t0, status: Object.keys(usage).length ? 'ok' : 'no_usage_streamed', endUser: _user });
539
- }).catch(() => {});
540
- return result;
541
- } catch (err) {
542
- track({ provider: 'gemini', model: modelName, usage: {}, latencyMs: Date.now() - t0, status: 'error', meta: { error: String(err.message).slice(0, 200) }, endUser: _user });
543
- throw err;
544
- }
545
- };
546
- wrapped.__cm = true;
547
- GenerativeModel.prototype.generateContentStream = wrapped;
548
- }
548
+ // Proxy the constructor: every `new GoogleGenAI(...)` still runs the real
549
+ // constructor untouched, we just reach into the instance it produces and
550
+ // wrap `.models` before handing the instance back.
551
+ const PatchedGoogleGenAI = new Proxy(OrigGoogleGenAI, {
552
+ construct(Target, args) {
553
+ const instance = new Target(...args);
554
+ wrapModelsInstance(instance.models);
555
+ return instance;
556
+ },
557
+ });
558
+ PatchedGoogleGenAI.__cm_patched = true;
549
559
 
550
- GoogleGenerativeAI.__cm_patched = true;
551
- log('gemini module patched (prototype-level)');
560
+ Mod.GoogleGenAI = PatchedGoogleGenAI;
561
+ log('gemini (unified @google/genai) module patched (instance-level via constructor proxy)');
552
562
  }
553
563
 
554
564
  // ── 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.9",
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
  }