cloudbroker-sdk 0.1.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.
@@ -0,0 +1,28 @@
1
+ name: Publish to npm
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ paths:
7
+ - 'package.json'
8
+
9
+ jobs:
10
+ publish:
11
+ runs-on: ubuntu-latest
12
+ permissions:
13
+ contents: read
14
+ id-token: write
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - name: Setup Node
20
+ uses: actions/setup-node@v4
21
+ with:
22
+ node-version: 20
23
+ registry-url: 'https://registry.npmjs.org'
24
+
25
+ - name: Publish to npm
26
+ run: npm publish
27
+ env:
28
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/README.md ADDED
@@ -0,0 +1,118 @@
1
+ # cloudbroker-sdk
2
+
3
+ Token Intelligence & Operational Financial Intelligence para Node.js.
4
+
5
+ ## Instalação
6
+
7
+ ```bash
8
+ npm install cloudbroker-sdk
9
+ ```
10
+
11
+ ## Uso com OpenAI
12
+
13
+ ```js
14
+ import OpenAI from "openai";
15
+ import { CloudBrokerAI } from "cloudbroker-sdk";
16
+
17
+ const openai = CloudBrokerAI.wrap(new OpenAI(), {
18
+ apiKey: "cb-key-xxxx", // sua chave CloudBroker
19
+ product: "checkout", // nome do produto
20
+ team: "mobile", // nome do squad (opcional)
21
+ });
22
+
23
+ // Todas as chamadas seguem iguais — nada muda
24
+ const res = await openai.chat.completions.create({
25
+ model: "gpt-4o",
26
+ messages: [{ role: "user", content: "Olá!" }],
27
+ });
28
+ ```
29
+
30
+ ## Uso com Anthropic
31
+
32
+ ```js
33
+ import Anthropic from "@anthropic-ai/sdk";
34
+ import { CloudBrokerAI } from "cloudbroker-sdk";
35
+
36
+ const anthropic = CloudBrokerAI.wrap(new Anthropic(), {
37
+ apiKey: "cb-key-xxxx",
38
+ product: "checkout",
39
+ team: "mobile",
40
+ });
41
+
42
+ const res = await anthropic.messages.create({
43
+ model: "claude-sonnet-4-6",
44
+ max_tokens: 1024,
45
+ messages: [{ role: "user", content: "Olá!" }],
46
+ });
47
+ ```
48
+
49
+ ## Streaming
50
+
51
+ Chamadas com `stream: true` também são rastreadas — o SDK observa os chunks
52
+ conforme você os consome e registra o `usage` ao final, sem alterar o fluxo:
53
+
54
+ ```js
55
+ const stream = await openai.chat.completions.create({
56
+ model: "gpt-4o",
57
+ stream: true,
58
+ messages: [{ role: "user", content: "Olá!" }],
59
+ });
60
+
61
+ for await (const chunk of stream) {
62
+ process.stdout.write(chunk.choices[0]?.delta?.content || "");
63
+ }
64
+ // tokens/custo são registrados automaticamente quando o stream termina
65
+ ```
66
+
67
+ Para OpenAI, o SDK injeta `stream_options: { include_usage: true }` para receber
68
+ o `usage` no chunk final. Para Anthropic, o `usage` vem dos eventos
69
+ `message_start` / `message_delta`. O objeto de stream original é preservado
70
+ (`.tee()`, `.controller`, etc.).
71
+
72
+ ## Rastreamento de Deploy
73
+
74
+ ```js
75
+ import { CloudBrokerAI } from "cloudbroker-sdk";
76
+
77
+ // Opção 1: Via wrapper (se você está usando OpenAI/Anthropic)
78
+ const openai = CloudBrokerAI.wrap(new OpenAI(), {
79
+ apiKey: "cb-key-xxxx",
80
+ product: "checkout",
81
+ });
82
+
83
+ openai.trackDeploy({
84
+ repo: "myapp",
85
+ branch: "main",
86
+ prTitle: "feat: add new feature",
87
+ prUrl: "https://github.com/org/myapp/pull/42",
88
+ author: "john.doe",
89
+ prNumber: 42,
90
+ filesChanged: 5,
91
+ linesAdded: 120,
92
+ linesRemoved: 30,
93
+ });
94
+
95
+ // Opção 2: Direto (sem OpenAI/Anthropic)
96
+ CloudBrokerAI.trackDeploy("cb-key-xxxx", {
97
+ repo: "myapp",
98
+ branch: "main",
99
+ prTitle: "feat: add new feature",
100
+ prUrl: "https://github.com/org/myapp/pull/42",
101
+ author: "john.doe",
102
+ prNumber: 42,
103
+ filesChanged: 5,
104
+ linesAdded: 120,
105
+ linesRemoved: 30,
106
+ });
107
+ ```
108
+
109
+ ## Garantias
110
+
111
+ - **Fire-and-forget**: se o CloudBroker estiver fora, sua aplicação não é afetada
112
+ - **Zero latência adicional**: o tracking acontece em background
113
+ - **Retry automático**: eventos com falha são reenviados em 30s
114
+ - **Fila local**: até 500 eventos em memória se o servidor estiver indisponível
115
+
116
+ ## API Key CloudBroker
117
+
118
+ Gere sua chave em `app.cloudbroker.app.br` → Configurações → API Keys.
@@ -0,0 +1,30 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ BACKEND="$HOME/Downloads/cloudbroker-backend-repo"
5
+ SDK_DIR="$(cd "$(dirname "$0")" && pwd)"
6
+
7
+ echo "📂 Copiando patches para o backend..."
8
+ cp "$SDK_DIR/patch_backend.py" "$BACKEND/"
9
+ cp "$SDK_DIR/patch_db.py" "$BACKEND/"
10
+
11
+ echo ""
12
+ echo "🔧 Patch backend — server.js (AI Gateway endpoints)..."
13
+ cd "$BACKEND"
14
+ python3 patch_backend.py
15
+
16
+ echo ""
17
+ echo "🔧 Patch db — db.js (putAiEvent, listAiEvents, getUserByApiKey)..."
18
+ python3 patch_db.py
19
+
20
+ echo ""
21
+ echo "🔍 Verificando..."
22
+ grep -n "ai/events\|ai/gateway\|putAiEvent\|listAiEvents\|getUserByApiKey" server.js db.js | head -15
23
+
24
+ echo ""
25
+ echo "🚀 Deploy backend..."
26
+ git add server.js db.js
27
+ git commit -m "feat: AI Gateway — endpoint /api/ai/events e /api/ai/gateway/usage para SDK"
28
+ git push
29
+
30
+ echo "✅ Backend pronto!"
package/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "cloudbroker-sdk",
3
+ "version": "0.1.0",
4
+ "description": "CloudBroker SDK — Token Intelligence & Operational Financial Intelligence",
5
+ "main": "src/index.js",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": "./src/index.js"
9
+ },
10
+ "scripts": {
11
+ "test": "node src/test.js"
12
+ },
13
+ "keywords": ["cloudbroker", "finops", "token-intelligence", "openai", "anthropic"],
14
+ "license": "MIT",
15
+ "dependencies": {}
16
+ }
@@ -0,0 +1,138 @@
1
+ import sys
2
+
3
+ with open("server.js", "r") as f:
4
+ content = f.read()
5
+
6
+ # Adicionar tabela cb-ai-events no db.js via server.js não é possível diretamente
7
+ # Vamos adicionar os endpoints no server.js antes do app.listen
8
+
9
+ OLD = """app.post("/api/chat", requireAuth, async (req, res) => {"""
10
+
11
+ NEW = """// ─── AI GATEWAY — recebe eventos do SDK ──────────────────────────────────────
12
+ app.post("/api/ai/events", async (req, res) => {
13
+ try {
14
+ const cbKey = req.headers["x-cloudbroker-key"];
15
+ if (!cbKey) return res.status(401).json({ error: "x-cloudbroker-key obrigatório." });
16
+
17
+ // Valida key e busca usuário
18
+ const { getUserByApiKey } = await import("./db.js");
19
+ const user = await getUserByApiKey(cbKey).catch(() => null);
20
+ if (!user) return res.status(401).json({ error: "API key inválida." });
21
+
22
+ const { events = [] } = req.body;
23
+ if (!Array.isArray(events) || events.length === 0)
24
+ return res.json({ ok: true, received: 0 });
25
+
26
+ const { putAiEvent } = await import("./db.js");
27
+ const userId = user.userId || user.email;
28
+ const now = new Date().toISOString();
29
+
30
+ await Promise.all(events.map(ev => putAiEvent({
31
+ userId,
32
+ eventId: `${Date.now()}-${Math.random().toString(36).slice(2,8)}`,
33
+ provider: ev.provider || "unknown",
34
+ model: ev.model || "unknown",
35
+ product: ev.product || null,
36
+ team: ev.team || null,
37
+ inputTokens: ev.inputTokens || 0,
38
+ outputTokens: ev.outputTokens || 0,
39
+ totalTokens: ev.totalTokens || 0,
40
+ costUSD: ev.costUSD || 0,
41
+ latencyMs: ev.latencyMs || 0,
42
+ endpoint: ev.endpoint || null,
43
+ sdkVersion: ev.sdkVersion || null,
44
+ createdAt: ev.sentAt || now,
45
+ })));
46
+
47
+ return res.json({ ok: true, received: events.length });
48
+ } catch (err) {
49
+ console.warn("ai/events:", err.message);
50
+ return res.status(500).json({ error: err.message });
51
+ }
52
+ });
53
+
54
+ // ─── AI GATEWAY — usage agregado por produto/squad/modelo ────────────────────
55
+ app.get("/api/ai/gateway/usage", requireAuth, async (req, res) => {
56
+ try {
57
+ const { listAiEvents } = await import("./db.js");
58
+ const userId = req.user.sub || req.user.email;
59
+ const days = parseInt(req.query.days || 30);
60
+ const events = await listAiEvents(userId, days);
61
+
62
+ // Agrupa por provider + modelo
63
+ const byModel = {};
64
+ const byProduct = {};
65
+ const dailyMap = {};
66
+
67
+ for (const ev of events) {
68
+ // Por modelo
69
+ const mk = `${ev.provider}::${ev.model}`;
70
+ if (!byModel[mk]) byModel[mk] = { provider: ev.provider, model: ev.model, inputTokens: 0, outputTokens: 0, totalTokens: 0, costUSD: 0, calls: 0 };
71
+ byModel[mk].inputTokens += ev.inputTokens || 0;
72
+ byModel[mk].outputTokens += ev.outputTokens || 0;
73
+ byModel[mk].totalTokens += ev.totalTokens || 0;
74
+ byModel[mk].costUSD += ev.costUSD || 0;
75
+ byModel[mk].calls += 1;
76
+
77
+ // Por produto
78
+ const pk = ev.product || "sem produto";
79
+ if (!byProduct[pk]) byProduct[pk] = { product: pk, totalTokens: 0, costUSD: 0, calls: 0, models: {} };
80
+ byProduct[pk].totalTokens += ev.totalTokens || 0;
81
+ byProduct[pk].costUSD += ev.costUSD || 0;
82
+ byProduct[pk].calls += 1;
83
+ if (!byProduct[pk].models[ev.model]) byProduct[pk].models[ev.model] = 0;
84
+ byProduct[pk].models[ev.model] += ev.costUSD || 0;
85
+
86
+ // Diário
87
+ const date = (ev.createdAt || "").slice(0,10);
88
+ if (date) {
89
+ if (!dailyMap[date]) dailyMap[date] = { date, totalTokens: 0, costUSD: 0, calls: 0 };
90
+ dailyMap[date].totalTokens += ev.totalTokens || 0;
91
+ dailyMap[date].costUSD += ev.costUSD || 0;
92
+ dailyMap[date].calls += 1;
93
+ }
94
+ }
95
+
96
+ const models = Object.values(byModel).sort((a,b) => b.costUSD - a.costUSD);
97
+ const products = Object.values(byProduct)
98
+ .map(p => ({ ...p, models: Object.entries(p.models).map(([m,c]) => ({ model:m, costUSD:c })).sort((a,b) => b.costUSD-a.costUSD) }))
99
+ .sort((a,b) => b.costUSD - a.costUSD);
100
+ const daily = Object.values(dailyMap).sort((a,b) => a.date.localeCompare(b.date));
101
+
102
+ const totalCostUSD = models.reduce((s,m) => s + m.costUSD, 0);
103
+ const totalTokens = models.reduce((s,m) => s + m.totalTokens, 0);
104
+ const totalCalls = models.reduce((s,m) => s + m.calls, 0);
105
+
106
+ // Detecta ineficiência: modelo mais caro com mais de 20% do volume
107
+ const expensive = models[0];
108
+ const cheap = models[models.length - 1];
109
+ let efficiencyAlert = null;
110
+ if (expensive && cheap && expensive.model !== cheap.model) {
111
+ const expRate = expensive.totalTokens > 0 ? expensive.costUSD / expensive.totalTokens * 1000 : 0;
112
+ const chpRate = cheap.totalTokens > 0 ? cheap.costUSD / cheap.totalTokens * 1000 : 0;
113
+ if (expRate > chpRate * 5 && expensive.costUSD > 0.01) {
114
+ efficiencyAlert = {
115
+ expensiveModel: expensive.model,
116
+ cheapModel: cheap.model,
117
+ potentialSaving: parseFloat((expensive.costUSD * 0.7).toFixed(4)),
118
+ };
119
+ }
120
+ }
121
+
122
+ return res.json({ ok: true, models, products, daily, totalCostUSD: parseFloat(totalCostUSD.toFixed(4)), totalTokens, totalCalls, efficiencyAlert, source: "sdk" });
123
+ } catch (err) {
124
+ console.warn("ai/gateway/usage:", err.message);
125
+ return res.status(500).json({ error: err.message });
126
+ }
127
+ });
128
+
129
+ app.post("/api/chat", requireAuth, async (req, res) => {"""
130
+
131
+ if OLD in content:
132
+ content = content.replace(OLD, NEW)
133
+ with open("server.js", "w") as f:
134
+ f.write(content)
135
+ print("✅ Endpoints AI Gateway adicionados ao server.js!")
136
+ else:
137
+ print("❌ Trecho não encontrado.")
138
+ sys.exit(1)
package/patch_db.py ADDED
@@ -0,0 +1,59 @@
1
+ import sys
2
+
3
+ with open("db.js", "r") as f:
4
+ content = f.read()
5
+
6
+ OLD = """export async function deleteAiService(userId, serviceId) {
7
+ await client.send(new DeleteItemCommand({
8
+ TableName: "cb-ai-services",
9
+ Key: marshall({ userId, serviceId }, { removeUndefinedValues: true }),
10
+ }));
11
+ }"""
12
+
13
+ NEW = """export async function deleteAiService(userId, serviceId) {
14
+ await client.send(new DeleteItemCommand({
15
+ TableName: "cb-ai-services",
16
+ Key: marshall({ userId, serviceId }, { removeUndefinedValues: true }),
17
+ }));
18
+ }
19
+
20
+ // ─── AI GATEWAY EVENTS ────────────────────────────────────────────────────────
21
+ export async function putAiEvent(event) {
22
+ await client.send(new PutItemCommand({
23
+ TableName: "cb-ai-events",
24
+ Item: marshall(event, { removeUndefinedValues: true }),
25
+ }));
26
+ }
27
+
28
+ export async function listAiEvents(userId, days = 30) {
29
+ const since = new Date();
30
+ since.setDate(since.getDate() - days);
31
+ const sinceStr = since.toISOString();
32
+
33
+ const res = await client.send(new QueryCommand({
34
+ TableName: "cb-ai-events",
35
+ KeyConditionExpression: "userId = :u",
36
+ FilterExpression: "createdAt >= :since",
37
+ ExpressionAttributeValues: marshall({ ":u": userId, ":since": sinceStr }, { removeUndefinedValues: true }),
38
+ }));
39
+ return (res.Items || []).map(i => unmarshall(i));
40
+ }
41
+
42
+ export async function getUserByApiKey(apiKey) {
43
+ // Scan pelo apiKey — para produção considere GSI no campo apiKey
44
+ const res = await client.send(new ScanCommand({
45
+ TableName: "cb-users",
46
+ FilterExpression: "apiKey = :k",
47
+ ExpressionAttributeValues: marshall({ ":k": apiKey }, { removeUndefinedValues: true }),
48
+ }));
49
+ return (res.Items || []).map(i => unmarshall(i))[0] || null;
50
+ }"""
51
+
52
+ if OLD in content:
53
+ content = content.replace(OLD, NEW)
54
+ with open("db.js", "w") as f:
55
+ f.write(content)
56
+ print("✅ db.js atualizado com putAiEvent, listAiEvents, getUserByApiKey!")
57
+ else:
58
+ print("❌ Trecho não encontrado.")
59
+ sys.exit(1)
package/src/client.js ADDED
@@ -0,0 +1,154 @@
1
+ /**
2
+ * CloudBrokerClient — cliente HTTP leve para envio de eventos
3
+ * Fire-and-forget: nunca bloqueia a aplicação do cliente
4
+ */
5
+
6
+ const DEFAULT_AI_ENDPOINT = "https://api.cloudbroker.app.br/api/ai/events";
7
+ const DEFAULT_API_BASE = "https://api.cloudbroker.app.br/api";
8
+ const RETRY_DELAY_MS = 30_000; // 30s entre retries
9
+ const MAX_QUEUE_SIZE = 500; // máximo de eventos em memória
10
+
11
+ export class CloudBrokerClient {
12
+ #apiKey;
13
+ #aiEndpoint;
14
+ #apiBase;
15
+ #queue = [];
16
+ #flushing = false;
17
+ #debug;
18
+
19
+ constructor({ apiKey, endpoint = DEFAULT_AI_ENDPOINT, apiBase = DEFAULT_API_BASE, debug = false } = {}) {
20
+ if (!apiKey) throw new Error("[CloudBroker] apiKey é obrigatório.");
21
+ this.#apiKey = apiKey;
22
+ this.#aiEndpoint = endpoint;
23
+ this.#apiBase = apiBase;
24
+ this.#debug = debug;
25
+ }
26
+
27
+ /**
28
+ * Envia um evento de uso de IA
29
+ * Fire-and-forget — nunca lança erro para o caller
30
+ */
31
+ track(event) {
32
+ try {
33
+ const payload = {
34
+ ...event,
35
+ sdkVersion: "0.1.0",
36
+ sentAt: new Date().toISOString(),
37
+ };
38
+
39
+ // Adiciona à fila
40
+ if (this.#queue.length < MAX_QUEUE_SIZE) {
41
+ this.#queue.push(payload);
42
+ }
43
+
44
+ // Tenta enviar imediatamente em background
45
+ this.#flush();
46
+ } catch (err) {
47
+ if (this.#debug) console.warn("[CloudBroker] track error:", err.message);
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Registra um deploy para análise de custo
53
+ * Fire-and-forget — nunca lança erro para o caller
54
+ *
55
+ * @param {object} deploy - Dados do deploy
56
+ * @param {string} deploy.repo - Repositório (ex: "myapp")
57
+ * @param {string} deploy.branch - Branch (ex: "main")
58
+ * @param {string} deploy.prTitle - Título da PR
59
+ * @param {string} deploy.prUrl - URL da PR
60
+ * @param {string} deploy.author - Autor do commit
61
+ * @param {number} deploy.prNumber - Número da PR (GitHub)
62
+ * @param {string} [deploy.provider="github"] - Provider (github, gitlab, bitbucket)
63
+ * @param {string} [deploy.terraformPlan] - Saída do `terraform plan`
64
+ * @param {string} [deploy.dockerfileChanged] - true se Dockerfile foi alterado
65
+ * @param {number} [deploy.filesChanged] - Número de arquivos alterados
66
+ * @param {number} [deploy.linesAdded] - Linhas adicionadas
67
+ * @param {number} [deploy.linesRemoved] - Linhas removidas
68
+ */
69
+ trackDeploy(deploy) {
70
+ if (!deploy) {
71
+ if (this.#debug) console.warn("[CloudBroker] trackDeploy: deploy é obrigatório");
72
+ return;
73
+ }
74
+
75
+ // Fire-and-forget: envia para o backend sem bloquear
76
+ (async () => {
77
+ try {
78
+ const payload = {
79
+ ...deploy,
80
+ apiKey: this.#apiKey,
81
+ };
82
+
83
+ const res = await fetch(`${this.#apiBase}/analyze/pr`, {
84
+ method: "POST",
85
+ headers: {
86
+ "Content-Type": "application/json",
87
+ "x-api-key": this.#apiKey,
88
+ },
89
+ body: JSON.stringify(payload),
90
+ signal: AbortSignal.timeout(5000),
91
+ });
92
+
93
+ if (!res.ok && this.#debug) {
94
+ console.warn(`[CloudBroker] trackDeploy failed: ${res.status}`);
95
+ } else if (this.#debug) {
96
+ console.log("[CloudBroker] deploy registrado com sucesso");
97
+ }
98
+ } catch (err) {
99
+ if (this.#debug) console.warn("[CloudBroker] trackDeploy error:", err.message);
100
+ }
101
+ })();
102
+ }
103
+
104
+ async #flush() {
105
+ if (this.#flushing || this.#queue.length === 0) return;
106
+ this.#flushing = true;
107
+
108
+ const batch = this.#queue.splice(0, 50); // processa até 50 por vez
109
+
110
+ try {
111
+ const res = await fetch(this.#aiEndpoint, {
112
+ method: "POST",
113
+ headers: {
114
+ "Content-Type": "application/json",
115
+ "x-cloudbroker-key": this.#apiKey,
116
+ },
117
+ body: JSON.stringify({ events: batch }),
118
+ signal: AbortSignal.timeout(5000), // timeout de 5s
119
+ });
120
+
121
+ if (!res.ok) {
122
+ if (this.#debug) console.warn("[CloudBroker] flush failed:", res.status);
123
+ // Recoloca na fila e agenda retry com backoff — nunca recursa na hora,
124
+ // senão um erro persistente (ex: 401) martela o endpoint em loop apertado.
125
+ this.#queue.unshift(...batch);
126
+ setTimeout(() => { this.#flushing = false; this.#flush(); }, RETRY_DELAY_MS);
127
+ return;
128
+ }
129
+
130
+ if (this.#debug) console.log(`[CloudBroker] flushed ${batch.length} events`);
131
+ } catch (err) {
132
+ // Falha silenciosa — recoloca na fila
133
+ this.#queue.unshift(...batch);
134
+ if (this.#debug) console.warn("[CloudBroker] flush error:", err.message);
135
+
136
+ // Retry em 30s
137
+ setTimeout(() => { this.#flushing = false; this.#flush(); }, RETRY_DELAY_MS);
138
+ return;
139
+ }
140
+
141
+ this.#flushing = false;
142
+
143
+ // Se ainda tem eventos na fila, continua
144
+ if (this.#queue.length > 0) this.#flush();
145
+ }
146
+
147
+ /** Força flush imediato — útil em shutdown */
148
+ async flushSync() {
149
+ while (this.#queue.length > 0) {
150
+ await this.#flush();
151
+ await new Promise(r => setTimeout(r, 100));
152
+ }
153
+ }
154
+ }
package/src/index.js ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * cloudbroker-sdk
3
+ * Token Intelligence & Operational Financial Intelligence
4
+ *
5
+ * Uso:
6
+ * import { CloudBrokerAI } from "cloudbroker-sdk";
7
+ * const openai = CloudBrokerAI.wrap(new OpenAI(), { apiKey: "cb-key-xxx", product: "checkout", team: "mobile" });
8
+ */
9
+
10
+ export { CloudBrokerAI } from "./wrapper.js";
11
+ export { CloudBrokerClient } from "./client.js";
package/src/pricing.js ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Tabela de preços por provider/modelo
3
+ * Preços em USD por token
4
+ * Atualizado: Mai/2026
5
+ */
6
+ export const PRICING = {
7
+ openai: {
8
+ "gpt-4o": { input: 0.000005, output: 0.000015 },
9
+ "gpt-4o-mini": { input: 0.00000015, output: 0.0000006 },
10
+ "gpt-4-turbo": { input: 0.00001, output: 0.00003 },
11
+ "gpt-4": { input: 0.00003, output: 0.00006 },
12
+ "gpt-3.5-turbo": { input: 0.0000005, output: 0.0000015 },
13
+ "o1": { input: 0.000015, output: 0.00006 },
14
+ "o1-mini": { input: 0.000003, output: 0.000012 },
15
+ "text-embedding-3-small": { input: 0.00000002, output: 0 },
16
+ "text-embedding-3-large": { input: 0.00000013, output: 0 },
17
+ },
18
+ anthropic: {
19
+ "claude-opus-4-6": { input: 0.000015, output: 0.000075 },
20
+ "claude-sonnet-4-6": { input: 0.000003, output: 0.000015 },
21
+ "claude-sonnet-4-5": { input: 0.000003, output: 0.000015 },
22
+ "claude-haiku-4-5-20251001":{ input: 0.00000025, output: 0.00000125 },
23
+ "claude-3-opus-20240229": { input: 0.000015, output: 0.000075 },
24
+ "claude-3-sonnet-20240229": { input: 0.000003, output: 0.000015 },
25
+ "claude-3-haiku-20240307": { input: 0.00000025, output: 0.00000125 },
26
+ },
27
+ gemini: {
28
+ "gemini-1.5-pro": { input: 0.0000035, output: 0.0000105 },
29
+ "gemini-1.5-flash": { input: 0.00000035, output: 0.00000105 },
30
+ "gemini-2.0-flash": { input: 0.0000001, output: 0.0000004 },
31
+ },
32
+ };
package/src/test.js ADDED
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Teste básico do SDK — simula uma chamada de IA e verifica o tracking
3
+ * Execute: node src/test.js
4
+ */
5
+
6
+ import { CloudBrokerClient } from "./client.js";
7
+ import { CloudBrokerAI } from "./wrapper.js";
8
+
9
+ console.log("🧪 Testando CloudBroker SDK...\n");
10
+
11
+ // Testa o cliente
12
+ const client = new CloudBrokerClient({
13
+ apiKey: "cb-test-key",
14
+ endpoint: "http://localhost:3001/api/ai/events",
15
+ debug: true,
16
+ });
17
+
18
+ // Simula um evento
19
+ client.track({
20
+ provider: "openai",
21
+ model: "gpt-4o",
22
+ product: "checkout",
23
+ team: "mobile",
24
+ inputTokens: 1200,
25
+ outputTokens: 450,
26
+ totalTokens: 1650,
27
+ costUSD: 0.00813,
28
+ latencyMs: 920,
29
+ endpoint: "chat.completions",
30
+ });
31
+
32
+ console.log("✅ Evento trackado (fire-and-forget)\n");
33
+
34
+ // Simula wrap de cliente OpenAI mock
35
+ const mockOpenAI = {
36
+ chat: {
37
+ completions: {
38
+ create: async (params) => ({
39
+ model: params.model,
40
+ choices: [{ message: { role: "assistant", content: "Resposta simulada" } }],
41
+ usage: { prompt_tokens: 100, completion_tokens: 50 },
42
+ }),
43
+ },
44
+ },
45
+ };
46
+
47
+ const wrapped = CloudBrokerAI.wrap(mockOpenAI, {
48
+ apiKey: "cb-test-key",
49
+ product: "checkout",
50
+ team: "mobile",
51
+ debug: true,
52
+ });
53
+
54
+ const res = await wrapped.chat.completions.create({
55
+ model: "gpt-4o",
56
+ messages: [{ role: "user", content: "teste" }],
57
+ });
58
+
59
+ console.log("✅ Wrap OpenAI funcionando");
60
+ console.log(" Resposta:", res.choices[0].message.content);
61
+ console.log(" Tokens: ", res.usage);
62
+
63
+ // Testa trackDeploy
64
+ console.log("\n📦 Testando trackDeploy...");
65
+ wrapped.trackDeploy({
66
+ repo: "myapp",
67
+ branch: "main",
68
+ prTitle: "feat: add new feature",
69
+ prUrl: "https://github.com/org/myapp/pull/42",
70
+ author: "john.doe",
71
+ prNumber: 42,
72
+ filesChanged: 5,
73
+ linesAdded: 120,
74
+ linesRemoved: 30,
75
+ });
76
+ console.log("✅ Deploy trackado (fire-and-forget)");
package/src/wrapper.js ADDED
@@ -0,0 +1,248 @@
1
+ /**
2
+ * CloudBrokerAI — wrapper para clientes de IA
3
+ * Intercepta respostas e registra tokens sem modificar o comportamento original
4
+ */
5
+
6
+ import { CloudBrokerClient } from "./client.js";
7
+ import { PRICING } from "./pricing.js";
8
+
9
+ export class CloudBrokerAI {
10
+ /**
11
+ * Envolve um cliente OpenAI ou Anthropic
12
+ *
13
+ * @param {object} client — instância do cliente (new OpenAI() ou Anthropic())
14
+ * @param {object} options — configuração
15
+ * @param {string} options.apiKey — CloudBroker API key
16
+ * @param {string} options.product — nome do produto (ex: "checkout")
17
+ * @param {string} options.team — nome do squad (ex: "mobile")
18
+ * @param {string} options.endpoint — endpoint CloudBroker (opcional)
19
+ * @param {boolean} options.debug — logs de debug (opcional)
20
+ */
21
+ static wrap(client, options = {}) {
22
+ const { apiKey, product, team, endpoint, debug = false } = options;
23
+ if (!apiKey) throw new Error("[CloudBroker] apiKey é obrigatório.");
24
+
25
+ const cb = new CloudBrokerClient({ apiKey, endpoint, debug });
26
+
27
+ // Adiciona trackDeploy ao cliente wrappado
28
+ client.trackDeploy = cb.trackDeploy.bind(cb);
29
+
30
+ // Detecta provider pelo tipo do cliente
31
+ const isOpenAI = client?.constructor?.name === "OpenAI" || !!client?.chat?.completions;
32
+ const isAnthropic = client?.constructor?.name === "Anthropic" || !!client?.messages?.create;
33
+
34
+ if (isOpenAI) return CloudBrokerAI.#wrapOpenAI(client, cb, { product, team, debug });
35
+ if (isAnthropic) return CloudBrokerAI.#wrapAnthropic(client, cb, { product, team, debug });
36
+
37
+ if (debug) console.warn("[CloudBroker] Provider não reconhecido — retornando cliente original.");
38
+ return client;
39
+ }
40
+
41
+ /**
42
+ * Instrumenta um stream (async iterable) sem consumi-lo: substitui o
43
+ * [Symbol.asyncIterator] para observar cada chunk conforme o consumidor itera,
44
+ * preservando o objeto original (.tee(), .controller, etc.). Ao terminar a
45
+ * iteração (ou em erro), dispara onDone com o usage acumulado.
46
+ *
47
+ * @param {AsyncIterable} stream — stream retornado pelo provider
48
+ * @param {(chunk:any)=>void} onChunk — acumula usage a partir de cada chunk
49
+ * @param {()=>void} onDone — chamado ao fim da iteração (fire-and-forget)
50
+ */
51
+ static #instrumentStream(stream, onChunk, onDone) {
52
+ if (!stream || typeof stream[Symbol.asyncIterator] !== "function") return stream;
53
+ // Captura o método original e dirige o iterador via .next() — nunca via
54
+ // for-await sobre o resultado, senão um generator (cujo asyncIterator retorna
55
+ // ele mesmo) reentraria no método já substituído e causaria recursão infinita.
56
+ const originalMethod = stream[Symbol.asyncIterator].bind(stream);
57
+ stream[Symbol.asyncIterator] = function () {
58
+ const source = originalMethod();
59
+ return (async function* () {
60
+ try {
61
+ while (true) {
62
+ const { value, done } = await source.next();
63
+ if (done) return;
64
+ try { onChunk(value); } catch { /* não interfere no consumo */ }
65
+ yield value;
66
+ }
67
+ } finally {
68
+ try { onDone(); } catch { /* fire-and-forget */ }
69
+ }
70
+ })();
71
+ };
72
+ return stream;
73
+ }
74
+
75
+ static #wrapOpenAI(client, cb, { product, team, debug }) {
76
+ const originalCreate = client.chat.completions.create.bind(client.chat.completions);
77
+
78
+ client.chat.completions.create = async (params, options) => {
79
+ const start = Date.now();
80
+
81
+ // ── Streaming: pede usage no chunk final e instrumenta o iterador ────────
82
+ if (params?.stream) {
83
+ const streamParams = {
84
+ ...params,
85
+ stream_options: { include_usage: true, ...(params.stream_options || {}) },
86
+ };
87
+ const stream = await originalCreate(streamParams, options);
88
+
89
+ let model = params.model || "unknown";
90
+ let inputTokens = 0, outputTokens = 0;
91
+ const onChunk = (chunk) => {
92
+ if (chunk?.model) model = chunk.model;
93
+ if (chunk?.usage) {
94
+ inputTokens = chunk.usage.prompt_tokens || inputTokens;
95
+ outputTokens = chunk.usage.completion_tokens || outputTokens;
96
+ }
97
+ };
98
+ const onDone = () => {
99
+ try {
100
+ const pricing = PRICING.openai[model] || PRICING.openai["gpt-4o"];
101
+ const costUSD = (inputTokens * pricing.input) + (outputTokens * pricing.output);
102
+ cb.track({
103
+ provider: "openai",
104
+ model,
105
+ product: product || null,
106
+ team: team || null,
107
+ inputTokens,
108
+ outputTokens,
109
+ totalTokens: inputTokens + outputTokens,
110
+ costUSD: parseFloat(costUSD.toFixed(6)),
111
+ latencyMs: Date.now() - start,
112
+ endpoint: "chat.completions",
113
+ streaming: true,
114
+ });
115
+ } catch (err) {
116
+ if (debug) console.warn("[CloudBroker] track error:", err.message);
117
+ }
118
+ };
119
+
120
+ return CloudBrokerAI.#instrumentStream(stream, onChunk, onDone);
121
+ }
122
+
123
+ // ── Não-streaming ───────────────────────────────────────────────────────
124
+ const res = await originalCreate(params, options);
125
+ const ms = Date.now() - start;
126
+
127
+ // Fire-and-forget
128
+ try {
129
+ const model = res.model || params.model || "unknown";
130
+ const inputTokens = res.usage?.prompt_tokens || 0;
131
+ const outputTokens = res.usage?.completion_tokens || 0;
132
+ const pricing = PRICING.openai[model] || PRICING.openai["gpt-4o"];
133
+ const costUSD = (inputTokens * pricing.input) + (outputTokens * pricing.output);
134
+
135
+ cb.track({
136
+ provider: "openai",
137
+ model,
138
+ product: product || null,
139
+ team: team || null,
140
+ inputTokens,
141
+ outputTokens,
142
+ totalTokens: inputTokens + outputTokens,
143
+ costUSD: parseFloat(costUSD.toFixed(6)),
144
+ latencyMs: ms,
145
+ endpoint: "chat.completions",
146
+ });
147
+ } catch (err) {
148
+ if (debug) console.warn("[CloudBroker] track error:", err.message);
149
+ }
150
+
151
+ return res;
152
+ };
153
+
154
+ return client;
155
+ }
156
+
157
+ static #wrapAnthropic(client, cb, { product, team, debug }) {
158
+ const originalCreate = client.messages.create.bind(client.messages);
159
+
160
+ client.messages.create = async (params, options) => {
161
+ const start = Date.now();
162
+
163
+ // ── Streaming: acumula usage dos eventos message_start / message_delta ───
164
+ if (params?.stream) {
165
+ const stream = await originalCreate(params, options);
166
+
167
+ let model = params.model || "unknown";
168
+ let inputTokens = 0, outputTokens = 0;
169
+ const onChunk = (event) => {
170
+ if (event?.type === "message_start" && event.message) {
171
+ model = event.message.model || model;
172
+ inputTokens = event.message.usage?.input_tokens || inputTokens;
173
+ outputTokens = event.message.usage?.output_tokens || outputTokens;
174
+ } else if (event?.type === "message_delta" && event.usage) {
175
+ // message_delta traz o total acumulado de output_tokens
176
+ outputTokens = event.usage.output_tokens || outputTokens;
177
+ }
178
+ };
179
+ const onDone = () => {
180
+ try {
181
+ const pricing = PRICING.anthropic[model] || PRICING.anthropic["claude-sonnet-4-6"];
182
+ const costUSD = (inputTokens * pricing.input) + (outputTokens * pricing.output);
183
+ cb.track({
184
+ provider: "anthropic",
185
+ model,
186
+ product: product || null,
187
+ team: team || null,
188
+ inputTokens,
189
+ outputTokens,
190
+ totalTokens: inputTokens + outputTokens,
191
+ costUSD: parseFloat(costUSD.toFixed(6)),
192
+ latencyMs: Date.now() - start,
193
+ endpoint: "messages",
194
+ streaming: true,
195
+ });
196
+ } catch (err) {
197
+ if (debug) console.warn("[CloudBroker] track error:", err.message);
198
+ }
199
+ };
200
+
201
+ return CloudBrokerAI.#instrumentStream(stream, onChunk, onDone);
202
+ }
203
+
204
+ // ── Não-streaming ───────────────────────────────────────────────────────
205
+ const res = await originalCreate(params, options);
206
+ const ms = Date.now() - start;
207
+
208
+ try {
209
+ const model = res.model || params.model || "unknown";
210
+ const inputTokens = res.usage?.input_tokens || 0;
211
+ const outputTokens = res.usage?.output_tokens || 0;
212
+ const pricing = PRICING.anthropic[model] || PRICING.anthropic["claude-sonnet-4-6"];
213
+ const costUSD = (inputTokens * pricing.input) + (outputTokens * pricing.output);
214
+
215
+ cb.track({
216
+ provider: "anthropic",
217
+ model,
218
+ product: product || null,
219
+ team: team || null,
220
+ inputTokens,
221
+ outputTokens,
222
+ totalTokens: inputTokens + outputTokens,
223
+ costUSD: parseFloat(costUSD.toFixed(6)),
224
+ latencyMs: ms,
225
+ endpoint: "messages",
226
+ });
227
+ } catch (err) {
228
+ if (debug) console.warn("[CloudBroker] track error:", err.message);
229
+ }
230
+
231
+ return res;
232
+ };
233
+
234
+ return client;
235
+ }
236
+
237
+ /**
238
+ * Registra um deploy para análise de custo
239
+ * Atalho para CloudBrokerClient.trackDeploy() sem precisar envolver um cliente de IA
240
+ *
241
+ * @param {string} apiKey - CloudBroker API key
242
+ * @param {object} deploy - Dados do deploy (repo, branch, prTitle, etc)
243
+ */
244
+ static trackDeploy(apiKey, deploy) {
245
+ const cb = new CloudBrokerClient({ apiKey });
246
+ cb.trackDeploy(deploy);
247
+ }
248
+ }