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/.github/workflows/publish.yml +18 -0
- package/README.md +139 -5
- package/docs/COST_PER_FEATURE.md +296 -0
- package/example-checkout-app/.env.example +13 -0
- package/example-checkout-app/README.md +225 -0
- package/example-checkout-app/package.json +28 -0
- package/example-checkout-app/server.js +209 -0
- package/example-checkout-app/test/e2e.js +97 -0
- package/package.json +14 -4
- package/src/client.js +17 -6
- package/src/cloud-wrapper.js +531 -5
- package/src/pricing-cloud.js +165 -10
- package/src/pricing.js +161 -3
- package/src/test-azure-real.js +169 -0
- package/src/test-gemini-real.js +270 -0
- package/src/test.js +264 -0
- package/src/wrapper.js +125 -2
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
|