cloudbroker-sdk 0.5.0 → 0.7.0

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/src/wrapper.js CHANGED
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { CloudBrokerClient } from "./client.js";
7
- import { PRICING } from "./pricing.js";
7
+ import { PRICING, priceGemini } from "./pricing.js";
8
8
  import { cbStore } from "./context.js";
9
9
 
10
10
  export class CloudBrokerAI {
@@ -35,12 +35,18 @@ export class CloudBrokerAI {
35
35
  // Adiciona trackDeploy ao cliente wrappado
36
36
  client.trackDeploy = cb.trackDeploy.bind(cb);
37
37
 
38
- // Detecta provider pelo tipo do cliente
38
+ // Detecta provider pelo tipo do cliente.
39
+ // O sinal do Gemini é `models.generateContent`, não `models`: o cliente da
40
+ // OpenAI também tem `.models` (list/retrieve), então checar só o objeto
41
+ // casaria com o cliente errado.
39
42
  const isOpenAI = client?.constructor?.name === "OpenAI" || !!client?.chat?.completions;
40
43
  const isAnthropic = client?.constructor?.name === "Anthropic" || !!client?.messages?.create;
44
+ const isGemini = client?.constructor?.name === "GoogleGenAI"
45
+ || typeof client?.models?.generateContent === "function";
41
46
 
42
47
  if (isOpenAI) return CloudBrokerAI.#wrapOpenAI(client, cb, { product, team, debug });
43
48
  if (isAnthropic) return CloudBrokerAI.#wrapAnthropic(client, cb, { product, team, debug });
49
+ if (isGemini) return CloudBrokerAI.#wrapGemini(client, cb, { product, team, debug });
44
50
 
45
51
  if (debug) console.warn("[CloudBroker] Provider não reconhecido — retornando cliente original.");
46
52
  return client;
@@ -254,6 +260,123 @@ export class CloudBrokerAI {
254
260
  return client;
255
261
  }
256
262
 
263
+ /**
264
+ * Instrumenta o cliente do @google/genai (`new GoogleGenAI({apiKey})`).
265
+ *
266
+ * Diferenças relevantes em relação a OpenAI/Anthropic:
267
+ *
268
+ * - A API é de argumento único (`models.generateContent({model, contents})`),
269
+ * sem segundo parâmetro de options. Por isso o `cbLabel` é aceito das duas
270
+ * formas: no segundo argumento (convenção dos outros wrappers) e dentro do
271
+ * próprio params — neste caso removido antes de repassar, pra nunca vazar
272
+ * campo desconhecido para a API do provider.
273
+ * - O usage vem em `usageMetadata` (camelCase). Os nomes foram conferidos nos
274
+ * tipos do @google/genai@2.15.0, não na doc pública: a doc mostra um
275
+ * formato snake_case que pertence a outra superfície da API, e usá-lo
276
+ * daria token zero em silêncio.
277
+ * - `costUSD` pode ser `null` (modelo fora da tabela de preço). Isso é
278
+ * deliberado: registrar tokens sem inventar preço. Quem consome precisa
279
+ * olhar `unpriced` — tratar `null` como zero vira "custo real zero".
280
+ */
281
+ static #wrapGemini(client, cb, { product, team, debug }) {
282
+ const models = client.models;
283
+ if (!models || typeof models.generateContent !== "function") return client;
284
+
285
+ // Extrai o cbLabel e devolve os params limpos pra repassar ao provider.
286
+ const splitLabel = (params, options) => {
287
+ let label = null, fwd = params;
288
+ try {
289
+ label = options?.cbLabel ?? null;
290
+ if (!label && params && typeof params === "object" && params.cbLabel != null) {
291
+ label = params.cbLabel;
292
+ }
293
+ if (!label) label = cbStore.getStore()?.label ?? null;
294
+ if (params && typeof params === "object" && "cbLabel" in params) {
295
+ const { cbLabel: _l, ...rest } = params;
296
+ fwd = rest;
297
+ }
298
+ } catch { /* instrumentação nunca altera a chamada do cliente */ }
299
+ return { label, fwd };
300
+ };
301
+
302
+ const buildEvent = (model, usage, { label, start, streaming, endpoint }) => {
303
+ const priced = priceGemini(model, usage || {});
304
+ return {
305
+ provider: "gemini",
306
+ model,
307
+ product: product || null,
308
+ team: team || null,
309
+ label,
310
+ inputTokens: priced.inputTokens,
311
+ outputTokens: priced.outputTokens,
312
+ totalTokens: priced.totalTokens,
313
+ costUSD: priced.costUSD,
314
+ // Só o Gemini separa esses dois, e são justamente os que mais mexem no
315
+ // valor: cache barateia ~10x, thinking é cobrado como output.
316
+ cachedTokens: priced.cachedTokens,
317
+ thoughtsTokens: priced.thoughtsTokens,
318
+ unpriced: priced.unpriced,
319
+ pricingNote: priced.reason,
320
+ latencyMs: Date.now() - start,
321
+ endpoint,
322
+ streaming: !!streaming,
323
+ };
324
+ };
325
+
326
+ // ── generateContent (não-streaming) ───────────────────────────────────────
327
+ const originalGenerate = models.generateContent.bind(models);
328
+ models.generateContent = async (params, options) => {
329
+ const start = Date.now();
330
+ const { label, fwd } = splitLabel(params, options);
331
+
332
+ const res = await originalGenerate(fwd);
333
+
334
+ try {
335
+ const model = res?.modelVersion || params?.model || "unknown";
336
+ cb.track(buildEvent(model, res?.usageMetadata, {
337
+ label, start, streaming: false, endpoint: "models.generateContent",
338
+ }));
339
+ } catch (err) {
340
+ if (debug) console.warn("[CloudBroker] track error:", err.message);
341
+ }
342
+
343
+ return res;
344
+ };
345
+
346
+ // ── generateContentStream ────────────────────────────────────────────────
347
+ if (typeof models.generateContentStream === "function") {
348
+ const originalStream = models.generateContentStream.bind(models);
349
+ models.generateContentStream = async (params, options) => {
350
+ const start = Date.now();
351
+ const { label, fwd } = splitLabel(params, options);
352
+
353
+ const stream = await originalStream(fwd);
354
+
355
+ let model = params?.model || "unknown";
356
+ let usage = null;
357
+ const onChunk = (chunk) => {
358
+ if (chunk?.modelVersion) model = chunk.modelVersion;
359
+ // O usage do Gemini é cumulativo e chega no(s) chunk(s) final(is):
360
+ // o último que vier é o total da chamada.
361
+ if (chunk?.usageMetadata) usage = chunk.usageMetadata;
362
+ };
363
+ const onDone = () => {
364
+ try {
365
+ cb.track(buildEvent(model, usage, {
366
+ label, start, streaming: true, endpoint: "models.generateContentStream",
367
+ }));
368
+ } catch (err) {
369
+ if (debug) console.warn("[CloudBroker] track error:", err.message);
370
+ }
371
+ };
372
+
373
+ return CloudBrokerAI.#instrumentStream(stream, onChunk, onDone);
374
+ };
375
+ }
376
+
377
+ return client;
378
+ }
379
+
257
380
  /**
258
381
  * Registra um deploy para análise de custo
259
382
  * Atalho para CloudBrokerClient.trackDeploy() sem precisar envolver um cliente de IA