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.
- package/.github/workflows/publish.yml +28 -0
- package/README.md +118 -0
- package/apply_backend.sh +30 -0
- package/package.json +16 -0
- package/patch_backend.py +138 -0
- package/patch_db.py +59 -0
- package/src/client.js +154 -0
- package/src/index.js +11 -0
- package/src/pricing.js +32 -0
- package/src/test.js +76 -0
- package/src/wrapper.js +248 -0
|
@@ -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.
|
package/apply_backend.sh
ADDED
|
@@ -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
|
+
}
|
package/patch_backend.py
ADDED
|
@@ -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
|
+
}
|