cloudbroker-sdk 0.8.0 → 0.8.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudbroker-sdk",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "CloudBroker SDK — Token Intelligence & Operational Financial Intelligence",
5
5
  "repository": {
6
6
  "type": "git",
package/src/client.js CHANGED
@@ -48,7 +48,7 @@ export class CloudBrokerClient {
48
48
  try {
49
49
  const payload = {
50
50
  ...event,
51
- sdkVersion: "0.8.0",
51
+ sdkVersion: "0.8.1",
52
52
  sentAt: new Date().toISOString(),
53
53
  };
54
54
  if (this.#cloudQueue.length < MAX_QUEUE_SIZE) {
@@ -104,7 +104,7 @@ export class CloudBrokerClient {
104
104
  try {
105
105
  const payload = {
106
106
  ...event,
107
- sdkVersion: "0.8.0",
107
+ sdkVersion: "0.8.1",
108
108
  sentAt: new Date().toISOString(),
109
109
  };
110
110
 
package/src/pricing.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Tabela de preços por provider/modelo
3
3
  * Preços em USD por token
4
- * Atualizado: Mai/2026
4
+ * Atualizado: Mai/2026 (Anthropic conferida em Out/2026)
5
5
  */
6
6
  export const PRICING = {
7
7
  openai: {
@@ -16,10 +16,13 @@ export const PRICING = {
16
16
  "text-embedding-3-large": { input: 0.00000013, output: 0 },
17
17
  },
18
18
  anthropic: {
19
- "claude-opus-4-6": { input: 0.000015, output: 0.000075 },
19
+ // Conferidos em 2026-10-12 contra a tabela oficial da Anthropic. Até essa
20
+ // data o Haiku 4.5 estava com o preço do Haiku 3 (4x abaixo do real) e o
21
+ // Opus 4.6 com o do Opus 4/4.1 (3x acima).
22
+ "claude-opus-4-6": { input: 0.000005, output: 0.000025 },
20
23
  "claude-sonnet-4-6": { input: 0.000003, output: 0.000015 },
21
24
  "claude-sonnet-4-5": { input: 0.000003, output: 0.000015 },
22
- "claude-haiku-4-5-20251001":{ input: 0.00000025, output: 0.00000125 },
25
+ "claude-haiku-4-5-20251001":{ input: 0.000001, output: 0.000005 },
23
26
  "claude-3-opus-20240229": { input: 0.000015, output: 0.000075 },
24
27
  "claude-3-sonnet-20240229": { input: 0.000003, output: 0.000015 },
25
28
  "claude-3-haiku-20240307": { input: 0.00000025, output: 0.00000125 },
@@ -188,3 +191,83 @@ export function priceGemini(model, usage = {}) {
188
191
  : null,
189
192
  };
190
193
  }
194
+
195
+ // ── Anthropic ────────────────────────────────────────────────────────────────
196
+
197
+ /**
198
+ * Prompt caching da Anthropic: gravação no cache custa um múltiplo do preço de
199
+ * ENTRADA (1,25x com validade de 5 min, 2x com 1 h) e leitura, 0,1x. Os tokens
200
+ * de cache NÃO aparecem em `usage.input_tokens` — vêm em campos próprios.
201
+ * Calcular só sobre `input_tokens` subconta o custo assim que o cache é ligado.
202
+ */
203
+ export const ANTHROPIC_CACHE_MULTIPLIERS = Object.freeze({ write5m: 1.25, write1h: 2, read: 0.1 });
204
+
205
+ // Snapshot datado da Anthropic: `claude-haiku-4-5-20251001`.
206
+ const ANTHROPIC_VERSION_SUFFIX = /-\d{8}$/;
207
+
208
+ const nonNeg = (v) => (Number.isFinite(Number(v)) && Number(v) > 0 ? Number(v) : 0);
209
+
210
+ /**
211
+ * Custo de uma chamada da Messages API da Anthropic a partir do `usage`.
212
+ *
213
+ * Modelo fora da tabela sai com `costUSD: null` + `unpriced: true` — nunca o
214
+ * preço de outro modelo (antes caía no do Sonnet 4.6, o que superfaturava um
215
+ * Haiku em 3x e subfaturava um Opus). Snapshot com sufixo de data não listado
216
+ * usa o preço do modelo-base, declarado em `approxPricing`.
217
+ *
218
+ * `inputTokens` é o total de entrada processada (sem cache + gravado + lido),
219
+ * mesma semântica do Gemini (`promptTokenCount` já inclui o cache). A parte de
220
+ * cache vai também separada em `cacheWriteTokens`/`cacheReadTokens`.
221
+ */
222
+ export function priceAnthropic(model, usage = {}) {
223
+ const u = usage || {};
224
+ const plain = nonNeg(u.input_tokens);
225
+ const output = nonNeg(u.output_tokens);
226
+ const read = nonNeg(u.cache_read_input_tokens);
227
+ const br = u.cache_creation;
228
+ let write5m, write1h;
229
+ if (br && typeof br === "object" && ("ephemeral_5m_input_tokens" in br || "ephemeral_1h_input_tokens" in br)) {
230
+ write5m = nonNeg(br.ephemeral_5m_input_tokens);
231
+ write1h = nonNeg(br.ephemeral_1h_input_tokens);
232
+ } else {
233
+ write5m = nonNeg(u.cache_creation_input_tokens);
234
+ write1h = 0;
235
+ }
236
+ const inputTokens = plain + write5m + write1h + read;
237
+ const base = {
238
+ inputTokens,
239
+ outputTokens: output,
240
+ totalTokens: inputTokens + output,
241
+ cacheWriteTokens: write5m + write1h,
242
+ cacheReadTokens: read,
243
+ };
244
+
245
+ const table = PRICING.anthropic;
246
+ const name = typeof model === "string" ? model : "";
247
+ let id = null, approx = false;
248
+ if (name && Object.hasOwn(table, name)) {
249
+ id = name;
250
+ } else {
251
+ const b = name.replace(ANTHROPIC_VERSION_SUFFIX, "");
252
+ if (b !== name && Object.hasOwn(table, b)) { id = b; approx = true; }
253
+ }
254
+ if (!id) {
255
+ return {
256
+ ...base, costUSD: null, pricedAs: null, approxPricing: false, unpriced: true,
257
+ reason: `Modelo "${name.slice(0, 60) || "desconhecido"}" não está na tabela de preço da Anthropic — tokens registrados, custo não estimado.`,
258
+ };
259
+ }
260
+ const r = table[id];
261
+ const m = ANTHROPIC_CACHE_MULTIPLIERS;
262
+ const costUSD = plain * r.input
263
+ + (write5m * m.write5m + write1h * m.write1h + read * m.read) * r.input
264
+ + output * r.output;
265
+ return {
266
+ ...base,
267
+ costUSD: parseFloat(costUSD.toFixed(8)),
268
+ pricedAs: id,
269
+ approxPricing: approx,
270
+ unpriced: false,
271
+ reason: approx ? `Preço do modelo base "${id}" aplicado a "${name.slice(0, 60)}" — snapshot não listado na tabela.` : null,
272
+ };
273
+ }
package/src/test.js CHANGED
@@ -489,5 +489,75 @@ console.warn = origWarn;
489
489
  assert.strictEqual(await untouched.foo(), 42);
490
490
  assert.ok(warns.some(w => w.includes("não reconhecido")), "deve avisar mesmo sem debug:true");
491
491
  console.log("✅ wrapAzure: client não reconhecido passa intocado, com aviso incondicional");
492
+ // 10) Anthropic: preço conferido (2026-10-12), tokens de cache e modelo
493
+ // desconhecido sem preço fabricado.
494
+ {
495
+ const { priceAnthropic, PRICING: P } = await import("./pricing.js");
496
+ assert.strictEqual(P.anthropic["claude-haiku-4-5-20251001"].input, 0.000001, "Haiku 4.5 entrada = US$ 1/M (não o preço do Haiku 3)");
497
+ assert.strictEqual(P.anthropic["claude-haiku-4-5-20251001"].output, 0.000005, "Haiku 4.5 saída = US$ 5/M");
498
+ assert.strictEqual(P.anthropic["claude-opus-4-6"].input, 0.000005, "Opus 4.6 entrada = US$ 5/M");
499
+ assert.strictEqual(P.anthropic["claude-opus-4-6"].output, 0.000025, "Opus 4.6 saída = US$ 25/M");
500
+
501
+ const h = priceAnthropic("claude-haiku-4-5-20251001", { input_tokens: 1_000_000, output_tokens: 1_000_000 });
502
+ assert.ok(Math.abs(h.costUSD - 6) < 1e-9, `1M+1M no Haiku 4.5 = US$ 6, veio ${h.costUSD}`);
503
+
504
+ const c = priceAnthropic("claude-haiku-4-5-20251001", {
505
+ input_tokens: 1000, output_tokens: 100, cache_creation_input_tokens: 10000, cache_read_input_tokens: 20000,
506
+ });
507
+ const esperado = 1000 * 1e-6 + 100 * 5e-6 + 10000 * 1e-6 * 1.25 + 20000 * 1e-6 * 0.1;
508
+ assert.ok(Math.abs(c.costUSD - esperado) < 1e-9, `gravação 1,25x e leitura 0,1x, esperado ${esperado}, veio ${c.costUSD}`);
509
+ assert.strictEqual(c.inputTokens, 31000, "inputTokens = sem cache + gravado + lido");
510
+ assert.strictEqual(c.cacheWriteTokens, 10000);
511
+ assert.strictEqual(c.cacheReadTokens, 20000);
512
+
513
+ const h1 = priceAnthropic("claude-haiku-4-5-20251001", {
514
+ cache_creation_input_tokens: 1000, cache_creation: { ephemeral_5m_input_tokens: 0, ephemeral_1h_input_tokens: 1000 },
515
+ });
516
+ assert.ok(Math.abs(h1.costUSD - 1000 * 1e-6 * 2) < 1e-12, "gravação com validade de 1 h custa 2x");
517
+
518
+ const d = priceAnthropic("claude-modelo-inventado", { input_tokens: 10, output_tokens: 10 });
519
+ assert.strictEqual(d.costUSD, null, "modelo desconhecido não recebe o preço de outro modelo");
520
+ assert.strictEqual(d.unpriced, true);
521
+ assert.strictEqual(d.totalTokens, 20, "tokens registrados mesmo sem preço");
522
+
523
+ const a = priceAnthropic("claude-sonnet-4-6-20991231", { input_tokens: 1000 });
524
+ assert.ok(a.approxPricing === true && a.pricedAs === "claude-sonnet-4-6", "snapshot datado não listado usa o modelo-base, declarado");
525
+
526
+ for (const nome of ["constructor", "__proto__", "toString", "hasOwnProperty"]) {
527
+ const v = priceAnthropic(nome, { input_tokens: 10 });
528
+ assert.strictEqual(v.costUSD, null, `nome venenoso "${nome}" não vira tabela de preço`);
529
+ }
530
+ const lixo = priceAnthropic("claude-haiku-4-5-20251001", { input_tokens: -5, output_tokens: "x", cache_read_input_tokens: NaN });
531
+ assert.strictEqual(lixo.costUSD, 0, "valor negativo ou não numérico conta como 0, nunca NaN");
532
+ assert.strictEqual(priceAnthropic(undefined, undefined).costUSD, null, "sem modelo e sem usage não lança");
533
+ console.log("✅ priceAnthropic: preço conferido, cache 1,25x/2x/0,1x, desconhecido = null");
534
+
535
+ // wrap: o evento enviado leva o custo com cache e o sinal de sem preço
536
+ const capt = [];
537
+ globalThis.fetch = async (url, opts) => {
538
+ if (String(url).includes("/api/ai/events")) capt.push(...JSON.parse(opts.body).events);
539
+ return { ok: true, status: 200, json: async () => ({}) };
540
+ };
541
+ const mockAnthropic = {
542
+ messages: {
543
+ create: async (params) => ({
544
+ model: params.model, content: [],
545
+ usage: { input_tokens: 1000, output_tokens: 100, cache_creation_input_tokens: 10000, cache_read_input_tokens: 20000 },
546
+ }),
547
+ },
548
+ };
549
+ const wa = CloudBrokerAI.wrap(mockAnthropic, { apiKey: "cb-test-key", product: "FinOps Advisor" });
550
+ await wa.messages.create({ model: "claude-haiku-4-5-20251001", max_tokens: 10, messages: [] }, { cbLabel: "finops-advisor:agent" });
551
+ await wa.messages.create({ model: "claude-modelo-inventado", max_tokens: 10, messages: [] });
552
+ await new Promise((r) => setTimeout(r, 50));
553
+ assert.strictEqual(capt.length, 2, `esperado 2 eventos de IA, veio ${capt.length}`);
554
+ assert.ok(Math.abs(capt[0].costUSD - esperado) < 1e-9, `evento leva o custo com cache, veio ${capt[0].costUSD}`);
555
+ assert.strictEqual(capt[0].cacheReadTokens, 20000);
556
+ assert.strictEqual(capt[0].label, "finops-advisor:agent");
557
+ assert.strictEqual(capt[1].costUSD, null, "evento de modelo desconhecido sai com costUSD null");
558
+ assert.strictEqual(capt[1].unpriced, true);
559
+ console.log("✅ CloudBrokerAI.wrap (Anthropic): custo com cache no evento, desconhecido = null");
560
+ }
561
+
492
562
  globalThis.fetch = realFetch;
493
563
  console.log("\n🎉 Todos os testes passaram.");
package/src/wrapper.js CHANGED
@@ -4,9 +4,37 @@
4
4
  */
5
5
 
6
6
  import { CloudBrokerClient } from "./client.js";
7
- import { PRICING, priceGemini } from "./pricing.js";
7
+ import { PRICING, priceGemini, priceAnthropic } from "./pricing.js";
8
8
  import { cbStore } from "./context.js";
9
9
 
10
+ /**
11
+ * Evento de uma chamada da Anthropic. O preço vem de `priceAnthropic`: inclui
12
+ * os tokens de cache e devolve `costUSD: null` + `unpriced` para modelo fora
13
+ * da tabela (nunca o preço de outro modelo).
14
+ */
15
+ function anthropicEvent(model, usage, { product, team, label, start, latencyMs, streaming }) {
16
+ const p = priceAnthropic(model, usage || {});
17
+ const ev = {
18
+ provider: "anthropic",
19
+ model,
20
+ product: product || null,
21
+ team: team || null,
22
+ label,
23
+ inputTokens: p.inputTokens,
24
+ outputTokens: p.outputTokens,
25
+ totalTokens: p.totalTokens,
26
+ costUSD: p.costUSD,
27
+ cacheWriteTokens: p.cacheWriteTokens,
28
+ cacheReadTokens: p.cacheReadTokens,
29
+ unpriced: p.unpriced,
30
+ pricingNote: p.reason,
31
+ latencyMs: latencyMs ?? (Date.now() - start),
32
+ endpoint: "messages",
33
+ };
34
+ if (streaming) ev.streaming = true;
35
+ return ev;
36
+ }
37
+
10
38
  export class CloudBrokerAI {
11
39
  /**
12
40
  * Envolve um cliente OpenAI ou Anthropic
@@ -189,35 +217,20 @@ export class CloudBrokerAI {
189
217
  const stream = await originalCreate(params, fwdOptions);
190
218
 
191
219
  let model = params.model || "unknown";
192
- let inputTokens = 0, outputTokens = 0;
220
+ // usage de entrada (incl. tokens de cache) vem no message_start;
221
+ // message_delta traz o total acumulado de output_tokens.
222
+ let usage = {};
193
223
  const onChunk = (event) => {
194
224
  if (event?.type === "message_start" && event.message) {
195
- model = event.message.model || model;
196
- inputTokens = event.message.usage?.input_tokens || inputTokens;
197
- outputTokens = event.message.usage?.output_tokens || outputTokens;
225
+ model = event.message.model || model;
226
+ if (event.message.usage) usage = { ...event.message.usage };
198
227
  } else if (event?.type === "message_delta" && event.usage) {
199
- // message_delta traz o total acumulado de output_tokens
200
- outputTokens = event.usage.output_tokens || outputTokens;
228
+ if (event.usage.output_tokens != null) usage.output_tokens = event.usage.output_tokens;
201
229
  }
202
230
  };
203
231
  const onDone = () => {
204
232
  try {
205
- const pricing = PRICING.anthropic[model] || PRICING.anthropic["claude-sonnet-4-6"];
206
- const costUSD = (inputTokens * pricing.input) + (outputTokens * pricing.output);
207
- cb.track({
208
- provider: "anthropic",
209
- model,
210
- product: product || null,
211
- team: team || null,
212
- label: cbLabel,
213
- inputTokens,
214
- outputTokens,
215
- totalTokens: inputTokens + outputTokens,
216
- costUSD: parseFloat(costUSD.toFixed(6)),
217
- latencyMs: Date.now() - start,
218
- endpoint: "messages",
219
- streaming: true,
220
- });
233
+ cb.track(anthropicEvent(model, usage, { product, team, label: cbLabel, start, streaming: true }));
221
234
  } catch (err) {
222
235
  if (debug) console.warn("[CloudBroker] track error:", err.message);
223
236
  }
@@ -231,25 +244,8 @@ export class CloudBrokerAI {
231
244
  const ms = Date.now() - start;
232
245
 
233
246
  try {
234
- const model = res.model || params.model || "unknown";
235
- const inputTokens = res.usage?.input_tokens || 0;
236
- const outputTokens = res.usage?.output_tokens || 0;
237
- const pricing = PRICING.anthropic[model] || PRICING.anthropic["claude-sonnet-4-6"];
238
- const costUSD = (inputTokens * pricing.input) + (outputTokens * pricing.output);
239
-
240
- cb.track({
241
- provider: "anthropic",
242
- model,
243
- product: product || null,
244
- team: team || null,
245
- label: cbLabel,
246
- inputTokens,
247
- outputTokens,
248
- totalTokens: inputTokens + outputTokens,
249
- costUSD: parseFloat(costUSD.toFixed(6)),
250
- latencyMs: ms,
251
- endpoint: "messages",
252
- });
247
+ const model = res.model || params.model || "unknown";
248
+ cb.track(anthropicEvent(model, res.usage, { product, team, label: cbLabel, start, latencyMs: ms }));
253
249
  } catch (err) {
254
250
  if (debug) console.warn("[CloudBroker] track error:", err.message);
255
251
  }