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 +1 -1
- package/src/client.js +2 -2
- package/src/pricing.js +86 -3
- package/src/test.js +70 -0
- package/src/wrapper.js +38 -42
package/package.json
CHANGED
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
196
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
235
|
-
|
|
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
|
}
|