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 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
- label: (req) => {
164
- if (req.path.startsWith("/api/auth")) return "Autenticação";
165
- if (req.path.startsWith("/api/billing")) return "Billing";
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 módulo retornado — sem cbLabel manual.
172
+ // é atribuído ao grupo/app/trecho — sem cbLabel manual.
172
173
  ```
173
174
 
174
- Prioridade do label: `cbLabel` explícito no `.send()` > contexto do middleware > `null`.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudbroker-sdk",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "CloudBroker SDK — Token Intelligence & Operational Financial Intelligence",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
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.3.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.3.0",
96
+ sdkVersion: "0.5.0",
97
97
  sentAt: new Date().toISOString(),
98
98
  };
99
99
 
@@ -24,7 +24,7 @@
24
24
 
25
25
  import { CloudBrokerClient } from "./client.js";
26
26
  import { estimateCloudCost } from "./pricing-cloud.js";
27
- import { cbStore, currentLabel } from "./context.js";
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
- // Label: explícito no .send() tem prioridade; senão usa o do contexto da
58
- // requisição (expressMiddleware), senão null.
59
- const cbLabel = sendOptions?.cbLabel || currentLabel() || null;
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 cbLabel pro AWS SDK.
63
- const { cbLabel: _omit, ...awsOptions } = sendOptions || {};
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: product || null,
77
- team: team || null,
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 um label de negócio a todas as chamadas de
99
- * nuvem feitas durante a requisição (via AsyncLocalStorage). Assim o custo por
100
- * código agrupa por módulo/rota sem precisar de cbLabel manual em cada send.
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] — label fixo, ou função que
104
- * recebe o req e devolve o label (ex: mapear rota → módulo de negócio).
105
- * Default: "MÉTODO /caminho" da requisição.
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 resolve =
110
- typeof options.label === "function" ? options.label :
111
- typeof options.label === "string" ? () => options.label :
112
- (req) => `${req.method || ""} ${req.baseUrl || ""}${req.route?.path || req.path || ""}`.trim();
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
- let label = null;
116
- try { label = resolve(req); } catch { label = null; }
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({ label: (req) => `modulo:${req.path}` });
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 cbLabel — herda do contexto
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, "modulo:/api/billing", `label do contexto esperado, veio ${ctxEvent.label}`);
168
- console.log("✅ expressMiddleware: label do contexto aplicado sem cbLabel explícito");
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, options);
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, options);
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, options);
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, options);
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,