cloudbroker-sdk 0.3.0 → 0.5.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/README.md +10 -8
- package/package.json +1 -1
- package/src/client.js +2 -2
- package/src/cloud-wrapper.js +27 -21
- package/src/test.js +62 -4
- package/src/wrapper.js +24 -4
package/README.md
CHANGED
|
@@ -152,26 +152,28 @@ Em vez de passar `cbLabel` em cada `.send()`, use o middleware: todas as chamada
|
|
|
152
152
|
de nuvem feitas durante a requisição herdam um label de negócio automaticamente
|
|
153
153
|
(via `AsyncLocalStorage`). Ótimo pra agrupar custo por **módulo** ou **rota**.
|
|
154
154
|
|
|
155
|
+
Cada campo (`label`, `product`, `team`) pode ser fixo (string) ou uma função
|
|
156
|
+
`(req) => string`. Isso permite a hierarquia **Grupo (team) → App (product) →
|
|
157
|
+
Trecho (label) → Serviço** na visão de custo.
|
|
158
|
+
|
|
155
159
|
```js
|
|
156
160
|
import express from "express";
|
|
157
161
|
import { CloudBrokerCloud } from "cloudbroker-sdk";
|
|
158
162
|
|
|
159
163
|
const app = express();
|
|
160
164
|
|
|
161
|
-
// label fixo, "MÉTODO /caminho" (default), ou uma função req → label:
|
|
162
165
|
app.use(CloudBrokerCloud.expressMiddleware({
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
return "Outros";
|
|
167
|
-
},
|
|
166
|
+
team: (req) => (req.path.startsWith("/api/admin") ? "Plataforma" : "Produto"), // Grupo
|
|
167
|
+
product: (req) => req.path.split("/")[2] || "core", // App
|
|
168
|
+
label: (req) => `${req.method} ${req.path}`, // Trecho
|
|
168
169
|
}));
|
|
169
170
|
|
|
170
171
|
// daqui pra frente, qualquer s3.send(...)/ddb.send(...) dentro da request
|
|
171
|
-
// é atribuído ao
|
|
172
|
+
// é atribuído ao grupo/app/trecho — sem cbLabel manual.
|
|
172
173
|
```
|
|
173
174
|
|
|
174
|
-
Prioridade
|
|
175
|
+
Prioridade de cada campo: explícito no `.send()` (`cbLabel`/`cbProduct`/`cbTeam`) >
|
|
176
|
+
contexto do middleware > valor estático do `wrapAWS` > `null`.
|
|
175
177
|
|
|
176
178
|
## Garantias
|
|
177
179
|
|
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -37,7 +37,7 @@ export class CloudBrokerClient {
|
|
|
37
37
|
try {
|
|
38
38
|
const payload = {
|
|
39
39
|
...event,
|
|
40
|
-
sdkVersion: "0.
|
|
40
|
+
sdkVersion: "0.5.0",
|
|
41
41
|
sentAt: new Date().toISOString(),
|
|
42
42
|
};
|
|
43
43
|
if (this.#cloudQueue.length < MAX_QUEUE_SIZE) {
|
|
@@ -93,7 +93,7 @@ export class CloudBrokerClient {
|
|
|
93
93
|
try {
|
|
94
94
|
const payload = {
|
|
95
95
|
...event,
|
|
96
|
-
sdkVersion: "0.
|
|
96
|
+
sdkVersion: "0.5.0",
|
|
97
97
|
sentAt: new Date().toISOString(),
|
|
98
98
|
};
|
|
99
99
|
|
package/src/cloud-wrapper.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
import { CloudBrokerClient } from "./client.js";
|
|
26
26
|
import { estimateCloudCost } from "./pricing-cloud.js";
|
|
27
|
-
import { cbStore
|
|
27
|
+
import { cbStore } from "./context.js";
|
|
28
28
|
|
|
29
29
|
export class CloudBrokerCloud {
|
|
30
30
|
/**
|
|
@@ -54,13 +54,16 @@ export class CloudBrokerCloud {
|
|
|
54
54
|
client.send = async (command, sendOptions) => {
|
|
55
55
|
const start = Date.now();
|
|
56
56
|
const operation = (command?.constructor?.name || "").replace(/Command$/, "");
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
const
|
|
57
|
+
// Prioridade: explícito no .send() > contexto da requisição (expressMiddleware)
|
|
58
|
+
// > valor estático do wrapAWS. Permite Grupo/App/Trecho por requisição.
|
|
59
|
+
const ctx = cbStore.getStore() || {};
|
|
60
|
+
const cbLabel = sendOptions?.cbLabel || ctx.label || null;
|
|
61
|
+
const evProduct = sendOptions?.cbProduct || ctx.product || product || null;
|
|
62
|
+
const evTeam = sendOptions?.cbTeam || ctx.team || team || null;
|
|
60
63
|
const bytesIn = CloudBrokerCloud.#inputBytes(command?.input);
|
|
61
64
|
|
|
62
|
-
// Passa adiante apenas as options nativas — nunca vaza
|
|
63
|
-
const { cbLabel:
|
|
65
|
+
// Passa adiante apenas as options nativas — nunca vaza campos cb* pro AWS SDK.
|
|
66
|
+
const { cbLabel: _l, cbProduct: _p, cbTeam: _t, ...awsOptions } = sendOptions || {};
|
|
64
67
|
const result = await originalSend(command, Object.keys(awsOptions).length ? awsOptions : undefined);
|
|
65
68
|
|
|
66
69
|
try {
|
|
@@ -73,8 +76,8 @@ export class CloudBrokerCloud {
|
|
|
73
76
|
provider: "aws",
|
|
74
77
|
service,
|
|
75
78
|
operation,
|
|
76
|
-
product:
|
|
77
|
-
team:
|
|
79
|
+
product: evProduct,
|
|
80
|
+
team: evTeam,
|
|
78
81
|
label: cbLabel,
|
|
79
82
|
requests: 1,
|
|
80
83
|
bytesIn,
|
|
@@ -95,26 +98,29 @@ export class CloudBrokerCloud {
|
|
|
95
98
|
}
|
|
96
99
|
|
|
97
100
|
/**
|
|
98
|
-
* Middleware Express que atribui
|
|
99
|
-
* nuvem feitas durante a requisição (via AsyncLocalStorage).
|
|
100
|
-
*
|
|
101
|
+
* Middleware Express que atribui contexto de negócio a todas as chamadas de
|
|
102
|
+
* nuvem feitas durante a requisição (via AsyncLocalStorage). Cada campo pode ser
|
|
103
|
+
* fixo (string) ou uma função (req) => string — ex: mapear rota → módulo/app/grupo.
|
|
104
|
+
* Sem precisar passar cbLabel/cbProduct/cbTeam manual em cada send.
|
|
101
105
|
*
|
|
102
106
|
* @param {object} [options]
|
|
103
|
-
* @param {string|((req)=>string)} [options.label]
|
|
104
|
-
*
|
|
105
|
-
*
|
|
107
|
+
* @param {string|((req)=>string)} [options.label] — trecho (default: "MÉTODO /caminho")
|
|
108
|
+
* @param {string|((req)=>string)} [options.product] — App (sobrescreve o product do wrapAWS)
|
|
109
|
+
* @param {string|((req)=>string)} [options.team] — Grupo (sobrescreve o team do wrapAWS)
|
|
106
110
|
* @returns {(req,res,next)=>void}
|
|
107
111
|
*/
|
|
108
112
|
static expressMiddleware(options = {}) {
|
|
109
|
-
const
|
|
110
|
-
typeof
|
|
111
|
-
typeof
|
|
112
|
-
|
|
113
|
+
const mk = (opt, fallback) =>
|
|
114
|
+
typeof opt === "function" ? opt :
|
|
115
|
+
typeof opt === "string" ? () => opt :
|
|
116
|
+
fallback;
|
|
117
|
+
const resolveLabel = mk(options.label, (req) => `${req.method || ""} ${req.baseUrl || ""}${req.route?.path || req.path || ""}`.trim());
|
|
118
|
+
const resolveProduct = mk(options.product, () => undefined);
|
|
119
|
+
const resolveTeam = mk(options.team, () => undefined);
|
|
113
120
|
|
|
114
121
|
return (req, res, next) => {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
cbStore.run({ label }, () => next());
|
|
122
|
+
const safe = (fn) => { try { return fn(req); } catch { return undefined; } };
|
|
123
|
+
cbStore.run({ label: safe(resolveLabel), product: safe(resolveProduct), team: safe(resolveTeam) }, () => next());
|
|
118
124
|
};
|
|
119
125
|
}
|
|
120
126
|
|
package/src/test.js
CHANGED
|
@@ -154,18 +154,76 @@ assert.ok(ev.costUSD > 0, "deve ter custo estimado");
|
|
|
154
154
|
console.log("✅ wrapAWS: evento capturado, cbLabel não vazou, resultado preservado");
|
|
155
155
|
|
|
156
156
|
// 3) expressMiddleware — label do contexto (ALS) entra sem cbLabel explícito
|
|
157
|
-
const mw = CloudBrokerCloud.expressMiddleware({
|
|
157
|
+
const mw = CloudBrokerCloud.expressMiddleware({
|
|
158
|
+
label: (req) => `modulo:${req.path}`,
|
|
159
|
+
product: (req) => (req.path.startsWith("/api/billing") ? "Billing" : "Outros"),
|
|
160
|
+
team: "Plataforma",
|
|
161
|
+
});
|
|
158
162
|
const fakeReq = { method: "GET", path: "/api/billing" };
|
|
159
163
|
await new Promise((resolve) => {
|
|
160
164
|
mw(fakeReq, {}, async () => {
|
|
161
|
-
await s3.send(new PutObjectCommand({ Bucket: "b", Key: "k", Body: "x" })); // sem
|
|
165
|
+
await s3.send(new PutObjectCommand({ Bucket: "b", Key: "k", Body: "x" })); // sem cb* — herda do contexto
|
|
162
166
|
resolve();
|
|
163
167
|
});
|
|
164
168
|
});
|
|
165
169
|
await new Promise((r) => setTimeout(r, 50));
|
|
166
170
|
const ctxEvent = captured[captured.length - 1].events[0];
|
|
167
|
-
assert.strictEqual(ctxEvent.label,
|
|
168
|
-
|
|
171
|
+
assert.strictEqual(ctxEvent.label, "modulo:/api/billing", `label do contexto esperado, veio ${ctxEvent.label}`);
|
|
172
|
+
assert.strictEqual(ctxEvent.product, "Billing", `product do contexto esperado, veio ${ctxEvent.product}`);
|
|
173
|
+
assert.strictEqual(ctxEvent.team, "Plataforma", `team do contexto esperado, veio ${ctxEvent.team}`);
|
|
174
|
+
console.log("✅ expressMiddleware: label/product/team do contexto aplicados sem cb* explícito");
|
|
175
|
+
|
|
176
|
+
// 4) CloudBrokerAI.wrap — label de código nas chamadas de IA (mesma
|
|
177
|
+
// convenção do wrapAWS: explícito na chamada > contexto cbStore > nenhum),
|
|
178
|
+
// pra agregar custo de IA junto com custo de nuvem na visão "Custo por Código".
|
|
179
|
+
const capturedAi = [];
|
|
180
|
+
globalThis.fetch = async (url, opts) => {
|
|
181
|
+
if (String(url).includes("/api/ai/events")) {
|
|
182
|
+
capturedAi.push(JSON.parse(opts.body));
|
|
183
|
+
}
|
|
184
|
+
return { ok: true, status: 200, json: async () => ({}) };
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
let sawOpenAiOptions;
|
|
188
|
+
const mockOpenAI2 = {
|
|
189
|
+
chat: {
|
|
190
|
+
completions: {
|
|
191
|
+
create: async (params, options) => {
|
|
192
|
+
sawOpenAiOptions = options;
|
|
193
|
+
return {
|
|
194
|
+
model: params.model,
|
|
195
|
+
choices: [{ message: { role: "assistant", content: "ok" } }],
|
|
196
|
+
usage: { prompt_tokens: 10, completion_tokens: 5 },
|
|
197
|
+
};
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
const wrappedAi = CloudBrokerAI.wrap(mockOpenAI2, {
|
|
203
|
+
apiKey: "cb-test-key", product: "FinOps Advisor", team: "platform", debug: true,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
await wrappedAi.chat.completions.create(
|
|
207
|
+
{ model: "gpt-4o", messages: [{ role: "user", content: "x" }] },
|
|
208
|
+
{ cbLabel: "finops-advisor:intent-classify" }
|
|
209
|
+
);
|
|
210
|
+
assert.strictEqual(sawOpenAiOptions, undefined, "cbLabel não pode vazar pro SDK do provider");
|
|
211
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
212
|
+
assert.strictEqual(capturedAi.length, 1, `esperado 1 evento de IA, veio ${capturedAi.length}`);
|
|
213
|
+
assert.strictEqual(capturedAi[0].events[0].label, "finops-advisor:intent-classify");
|
|
214
|
+
console.log("✅ CloudBrokerAI.wrap: label explícito (cbLabel) capturado, sem vazar pro provider");
|
|
215
|
+
|
|
216
|
+
// label via contexto (cbStore, mesmo ALS de CloudBrokerCloud.expressMiddleware)
|
|
217
|
+
const mw2 = CloudBrokerCloud.expressMiddleware({ label: "finops-advisor:root-cause-agent" });
|
|
218
|
+
await new Promise((resolve) => {
|
|
219
|
+
mw2({ method: "POST", path: "/api/chat/response" }, {}, async () => {
|
|
220
|
+
await wrappedAi.chat.completions.create({ model: "gpt-4o", messages: [{ role: "user", content: "y" }] });
|
|
221
|
+
resolve();
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
225
|
+
assert.strictEqual(capturedAi[1].events[0].label, "finops-advisor:root-cause-agent");
|
|
226
|
+
console.log("✅ CloudBrokerAI.wrap: label via contexto cbStore (sem cbLabel explícito)");
|
|
169
227
|
|
|
170
228
|
globalThis.fetch = realFetch;
|
|
171
229
|
console.log("\n🎉 Todos os testes passaram.");
|
package/src/wrapper.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { CloudBrokerClient } from "./client.js";
|
|
7
7
|
import { PRICING } from "./pricing.js";
|
|
8
|
+
import { cbStore } from "./context.js";
|
|
8
9
|
|
|
9
10
|
export class CloudBrokerAI {
|
|
10
11
|
/**
|
|
@@ -17,6 +18,13 @@ export class CloudBrokerAI {
|
|
|
17
18
|
* @param {string} options.team — nome do squad (ex: "mobile")
|
|
18
19
|
* @param {string} options.endpoint — endpoint CloudBroker (opcional)
|
|
19
20
|
* @param {boolean} options.debug — logs de debug (opcional)
|
|
21
|
+
*
|
|
22
|
+
* Label opcional do trecho de código, pra agregar custo de IA junto com
|
|
23
|
+
* custo de nuvem na mesma visão "Custo por Código" (mesma convenção de
|
|
24
|
+
* CloudBrokerCloud.wrapAWS):
|
|
25
|
+
* await client.chat.completions.create(params, { cbLabel: "checkout:resumo-pedido" });
|
|
26
|
+
* await client.messages.create(params, { cbLabel: "checkout:resumo-pedido" });
|
|
27
|
+
* Ou via CloudBrokerCloud.expressMiddleware() (contexto automático por requisição).
|
|
20
28
|
*/
|
|
21
29
|
static wrap(client, options = {}) {
|
|
22
30
|
const { apiKey, product, team, endpoint, debug = false } = options;
|
|
@@ -77,6 +85,11 @@ export class CloudBrokerAI {
|
|
|
77
85
|
|
|
78
86
|
client.chat.completions.create = async (params, options) => {
|
|
79
87
|
const start = Date.now();
|
|
88
|
+
// Mesma precedência de cloud-wrapper.js: explícito na chamada > contexto
|
|
89
|
+
// cbStore (expressMiddleware) > nenhum. Nunca vaza cbLabel pro SDK do provider.
|
|
90
|
+
const cbLabel = options?.cbLabel || cbStore.getStore()?.label || null;
|
|
91
|
+
const { cbLabel: _l, ...providerOptions } = options || {};
|
|
92
|
+
const fwdOptions = Object.keys(providerOptions).length ? providerOptions : undefined;
|
|
80
93
|
|
|
81
94
|
// ── Streaming: pede usage no chunk final e instrumenta o iterador ────────
|
|
82
95
|
if (params?.stream) {
|
|
@@ -84,7 +97,7 @@ export class CloudBrokerAI {
|
|
|
84
97
|
...params,
|
|
85
98
|
stream_options: { include_usage: true, ...(params.stream_options || {}) },
|
|
86
99
|
};
|
|
87
|
-
const stream = await originalCreate(streamParams,
|
|
100
|
+
const stream = await originalCreate(streamParams, fwdOptions);
|
|
88
101
|
|
|
89
102
|
let model = params.model || "unknown";
|
|
90
103
|
let inputTokens = 0, outputTokens = 0;
|
|
@@ -104,6 +117,7 @@ export class CloudBrokerAI {
|
|
|
104
117
|
model,
|
|
105
118
|
product: product || null,
|
|
106
119
|
team: team || null,
|
|
120
|
+
label: cbLabel,
|
|
107
121
|
inputTokens,
|
|
108
122
|
outputTokens,
|
|
109
123
|
totalTokens: inputTokens + outputTokens,
|
|
@@ -121,7 +135,7 @@ export class CloudBrokerAI {
|
|
|
121
135
|
}
|
|
122
136
|
|
|
123
137
|
// ── Não-streaming ───────────────────────────────────────────────────────
|
|
124
|
-
const res = await originalCreate(params,
|
|
138
|
+
const res = await originalCreate(params, fwdOptions);
|
|
125
139
|
const ms = Date.now() - start;
|
|
126
140
|
|
|
127
141
|
// Fire-and-forget
|
|
@@ -137,6 +151,7 @@ export class CloudBrokerAI {
|
|
|
137
151
|
model,
|
|
138
152
|
product: product || null,
|
|
139
153
|
team: team || null,
|
|
154
|
+
label: cbLabel,
|
|
140
155
|
inputTokens,
|
|
141
156
|
outputTokens,
|
|
142
157
|
totalTokens: inputTokens + outputTokens,
|
|
@@ -159,10 +174,13 @@ export class CloudBrokerAI {
|
|
|
159
174
|
|
|
160
175
|
client.messages.create = async (params, options) => {
|
|
161
176
|
const start = Date.now();
|
|
177
|
+
const cbLabel = options?.cbLabel || cbStore.getStore()?.label || null;
|
|
178
|
+
const { cbLabel: _l, ...providerOptions } = options || {};
|
|
179
|
+
const fwdOptions = Object.keys(providerOptions).length ? providerOptions : undefined;
|
|
162
180
|
|
|
163
181
|
// ── Streaming: acumula usage dos eventos message_start / message_delta ───
|
|
164
182
|
if (params?.stream) {
|
|
165
|
-
const stream = await originalCreate(params,
|
|
183
|
+
const stream = await originalCreate(params, fwdOptions);
|
|
166
184
|
|
|
167
185
|
let model = params.model || "unknown";
|
|
168
186
|
let inputTokens = 0, outputTokens = 0;
|
|
@@ -185,6 +203,7 @@ export class CloudBrokerAI {
|
|
|
185
203
|
model,
|
|
186
204
|
product: product || null,
|
|
187
205
|
team: team || null,
|
|
206
|
+
label: cbLabel,
|
|
188
207
|
inputTokens,
|
|
189
208
|
outputTokens,
|
|
190
209
|
totalTokens: inputTokens + outputTokens,
|
|
@@ -202,7 +221,7 @@ export class CloudBrokerAI {
|
|
|
202
221
|
}
|
|
203
222
|
|
|
204
223
|
// ── Não-streaming ───────────────────────────────────────────────────────
|
|
205
|
-
const res = await originalCreate(params,
|
|
224
|
+
const res = await originalCreate(params, fwdOptions);
|
|
206
225
|
const ms = Date.now() - start;
|
|
207
226
|
|
|
208
227
|
try {
|
|
@@ -217,6 +236,7 @@ export class CloudBrokerAI {
|
|
|
217
236
|
model,
|
|
218
237
|
product: product || null,
|
|
219
238
|
team: team || null,
|
|
239
|
+
label: cbLabel,
|
|
220
240
|
inputTokens,
|
|
221
241
|
outputTokens,
|
|
222
242
|
totalTokens: inputTokens + outputTokens,
|